mirror of
https://github.com/PerpetualSoftware/pad.git
synced 2026-09-24 19:32:10 +00:00
eca03e663dab51c3b359759aedead488b5087f31
1227 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
eca03e663d |
Merge pull request #1061 from PerpetualSoftware/dependabot/npm_and_yarn/web/npm-minor-and-patch-0fbc33a3b3
chore(deps)(deps): bump the npm-minor-and-patch group across 1 directory with 24 updates |
||
|
|
68907e4d92 |
Merge pull request #1062 from PerpetualSoftware/dependabot/go_modules/go-minor-and-patch-48971f1ce0
chore(deps)(deps): bump the go-minor-and-patch group across 1 directory with 13 updates |
||
|
|
0a547283db |
fix(deps): keep @dagrejs/dagre pinned at 3.0.0
The group bump to 3.1.0 reproducibly breaks the graph-pane anchor E2E (pane-content-link-anchors.spec.ts:238, node click timeout, 3/3 runs) — layout changes move the node hit-target. dagre was exact-pinned at 3.0.0 by the renderer's author (TASK-1783); keeping it that way. Follow-up for the 3.1 upgrade tracked separately. Claude-Session: https://claude.ai/code/session_01RNcrc3CtXwJwreubtHTgN6 |
||
|
|
a62a50d672 |
fix(nix): refresh vendorHash for go-minor-and-patch group bump
Claude-Session: https://claude.ai/code/session_01RNcrc3CtXwJwreubtHTgN6 |
||
|
|
3f0b390288 |
chore(deps)(deps): bump the npm-minor-and-patch group across 1 directory with 24 updates
Bumps the npm-minor-and-patch group with 23 updates in the /web directory: | Package | From | To | | --- | --- | --- | | [@dagrejs/dagre](https://github.com/dagrejs/dagre) | `3.0.0` | `3.1.0` | | [@tiptap/core](https://github.com/ueberdosis/tiptap/tree/HEAD/packages/core) | `3.22.5` | `3.29.2` | | [@tiptap/extension-bubble-menu](https://github.com/ueberdosis/tiptap/tree/HEAD/packages/extension-bubble-menu) | `3.22.5` | `3.29.2` | | [@tiptap/extension-code-block-lowlight](https://github.com/ueberdosis/tiptap/tree/HEAD/packages/extension-code-block-lowlight) | `3.22.5` | `3.29.2` | | [@tiptap/extension-collaboration](https://github.com/ueberdosis/tiptap/tree/HEAD/packages/extension-collaboration) | `3.22.5` | `3.29.2` | | [@tiptap/extension-collaboration-caret](https://github.com/ueberdosis/tiptap/tree/HEAD/packages/extension-collaboration-caret) | `3.22.5` | `3.29.2` | | [@tiptap/extension-placeholder](https://github.com/ueberdosis/tiptap/tree/HEAD/packages-deprecated/extension-placeholder) | `3.22.5` | `3.29.2` | | [@tiptap/extension-table](https://github.com/ueberdosis/tiptap/tree/HEAD/packages/extension-table) | `3.22.5` | `3.29.2` | | [@tiptap/extension-task-item](https://github.com/ueberdosis/tiptap/tree/HEAD/packages/extension-task-item) | `3.22.5` | `3.29.2` | | [@tiptap/extension-task-list](https://github.com/ueberdosis/tiptap/tree/HEAD/packages/extension-task-list) | `3.22.5` | `3.29.2` | | [@tiptap/pm](https://github.com/ueberdosis/tiptap/tree/HEAD/packages/pm) | `3.22.5` | `3.29.2` | | [@tiptap/starter-kit](https://github.com/ueberdosis/tiptap/tree/HEAD/packages/starter-kit) | `3.22.5` | `3.29.2` | | [@tiptap/suggestion](https://github.com/ueberdosis/tiptap/tree/HEAD/packages/suggestion) | `3.22.5` | `3.29.2` | | [@tiptap/y-tiptap](https://github.com/ueberdosis/y-tiptap) | `3.0.3` | `3.0.8` | | [dompurify](https://github.com/cure53/DOMPurify) | `3.4.12` | `3.4.13` | | [svelte-dnd-action](https://github.com/isaacHagoel/svelte-dnd-action) | `0.9.74` | `0.9.77` | | [yjs](https://github.com/yjs/yjs) | `13.6.30` | `13.6.31` | | [@playwright/test](https://github.com/microsoft/playwright) | `1.61.1` | `1.62.1` | | [@sveltejs/kit](https://github.com/sveltejs/kit/tree/HEAD/packages/kit) | `2.70.1` | `2.70.2` | | [@sveltejs/vite-plugin-svelte](https://github.com/sveltejs/vite-plugin-svelte/tree/HEAD/packages/vite-plugin-svelte) | `7.1.2` | `7.2.0` | | [svelte](https://github.com/sveltejs/svelte/tree/HEAD/packages/svelte) | `5.55.8` | `5.56.8` | | [svelte-check](https://github.com/sveltejs/language-tools) | `4.7.3` | `4.7.4` | | [vite](https://github.com/vitejs/vite/tree/HEAD/packages/vite) | `8.1.5` | `8.2.0` | Updates `@dagrejs/dagre` from 3.0.0 to 3.1.0 - [Release notes](https://github.com/dagrejs/dagre/releases) - [Changelog](https://github.com/dagrejs/dagre/blob/master/changelog.md) - [Commits](https://github.com/dagrejs/dagre/compare/v3.0.0...v3.1.0) Updates `@tiptap/core` from 3.22.5 to 3.29.2 - [Release notes](https://github.com/ueberdosis/tiptap/releases) - [Changelog](https://github.com/ueberdosis/tiptap/blob/main/packages/core/CHANGELOG.md) - [Commits](https://github.com/ueberdosis/tiptap/commits/v3.29.2/packages/core) Updates `@tiptap/extension-bubble-menu` from 3.22.5 to 3.29.2 - [Release notes](https://github.com/ueberdosis/tiptap/releases) - [Changelog](https://github.com/ueberdosis/tiptap/blob/main/packages/extension-bubble-menu/CHANGELOG.md) - [Commits](https://github.com/ueberdosis/tiptap/commits/v3.29.2/packages/extension-bubble-menu) Updates `@tiptap/extension-code-block-lowlight` from 3.22.5 to 3.29.2 - [Release notes](https://github.com/ueberdosis/tiptap/releases) - [Changelog](https://github.com/ueberdosis/tiptap/blob/main/packages/extension-code-block-lowlight/CHANGELOG.md) - [Commits](https://github.com/ueberdosis/tiptap/commits/v3.29.2/packages/extension-code-block-lowlight) Updates `@tiptap/extension-collaboration` from 3.22.5 to 3.29.2 - [Release notes](https://github.com/ueberdosis/tiptap/releases) - [Changelog](https://github.com/ueberdosis/tiptap/blob/main/packages/extension-collaboration/CHANGELOG.md) - [Commits](https://github.com/ueberdosis/tiptap/commits/v3.29.2/packages/extension-collaboration) Updates `@tiptap/extension-collaboration-caret` from 3.22.5 to 3.29.2 - [Release notes](https://github.com/ueberdosis/tiptap/releases) - [Changelog](https://github.com/ueberdosis/tiptap/blob/main/packages/extension-collaboration-caret/CHANGELOG.md) - [Commits](https://github.com/ueberdosis/tiptap/commits/v3.29.2/packages/extension-collaboration-caret) Updates `@tiptap/extension-link` from 3.22.5 to 3.29.2 - [Release notes](https://github.com/ueberdosis/tiptap/releases) - [Changelog](https://github.com/ueberdosis/tiptap/blob/main/packages/extension-link/CHANGELOG.md) - [Commits](https://github.com/ueberdosis/tiptap/commits/v3.29.2/packages/extension-link) Updates `@tiptap/extension-placeholder` from 3.22.5 to 3.29.2 - [Release notes](https://github.com/ueberdosis/tiptap/releases) - [Changelog](https://github.com/ueberdosis/tiptap/blob/main/packages-deprecated/extension-placeholder/CHANGELOG.md) - [Commits](https://github.com/ueberdosis/tiptap/commits/v3.29.2/packages-deprecated/extension-placeholder) Updates `@tiptap/extension-table` from 3.22.5 to 3.29.2 - [Release notes](https://github.com/ueberdosis/tiptap/releases) - [Changelog](https://github.com/ueberdosis/tiptap/blob/main/packages/extension-table/CHANGELOG.md) - [Commits](https://github.com/ueberdosis/tiptap/commits/v3.29.2/packages/extension-table) Updates `@tiptap/extension-task-item` from 3.22.5 to 3.29.2 - [Release notes](https://github.com/ueberdosis/tiptap/releases) - [Commits](https://github.com/ueberdosis/tiptap/commits/v3.29.2/packages/extension-task-item) Updates `@tiptap/extension-task-list` from 3.22.5 to 3.29.2 - [Release notes](https://github.com/ueberdosis/tiptap/releases) - [Commits](https://github.com/ueberdosis/tiptap/commits/v3.29.2/packages/extension-task-list) Updates `@tiptap/pm` from 3.22.5 to 3.29.2 - [Release notes](https://github.com/ueberdosis/tiptap/releases) - [Changelog](https://github.com/ueberdosis/tiptap/blob/main/packages/pm/CHANGELOG.md) - [Commits](https://github.com/ueberdosis/tiptap/commits/v3.29.2/packages/pm) Updates `@tiptap/starter-kit` from 3.22.5 to 3.29.2 - [Release notes](https://github.com/ueberdosis/tiptap/releases) - [Changelog](https://github.com/ueberdosis/tiptap/blob/main/packages/starter-kit/CHANGELOG.md) - [Commits](https://github.com/ueberdosis/tiptap/commits/v3.29.2/packages/starter-kit) Updates `@tiptap/suggestion` from 3.22.5 to 3.29.2 - [Release notes](https://github.com/ueberdosis/tiptap/releases) - [Changelog](https://github.com/ueberdosis/tiptap/blob/main/packages/suggestion/CHANGELOG.md) - [Commits](https://github.com/ueberdosis/tiptap/commits/v3.29.2/packages/suggestion) Updates `@tiptap/y-tiptap` from 3.0.3 to 3.0.8 - [Changelog](https://github.com/ueberdosis/y-tiptap/blob/main/CHANGELOG.md) - [Commits](https://github.com/ueberdosis/y-tiptap/commits) Updates `dompurify` from 3.4.12 to 3.4.13 - [Release notes](https://github.com/cure53/DOMPurify/releases) - [Commits](https://github.com/cure53/DOMPurify/compare/3.4.12...3.4.13) Updates `svelte-dnd-action` from 0.9.74 to 0.9.77 - [Changelog](https://github.com/isaacHagoel/svelte-dnd-action/blob/master/release-notes.md) - [Commits](https://github.com/isaacHagoel/svelte-dnd-action/commits) Updates `yjs` from 13.6.30 to 13.6.31 - [Release notes](https://github.com/yjs/yjs/releases) - [Commits](https://github.com/yjs/yjs/compare/v13.6.30...v13.6.31) Updates `@playwright/test` from 1.61.1 to 1.62.1 - [Release notes](https://github.com/microsoft/playwright/releases) - [Commits](https://github.com/microsoft/playwright/compare/v1.61.1...v1.62.1) Updates `@sveltejs/kit` from 2.70.1 to 2.70.2 - [Release notes](https://github.com/sveltejs/kit/releases) - [Changelog](https://github.com/sveltejs/kit/blob/version-3/packages/kit/CHANGELOG.md) - [Commits](https://github.com/sveltejs/kit/commits/@sveltejs/kit@2.70.2/packages/kit) Updates `@sveltejs/vite-plugin-svelte` from 7.1.2 to 7.2.0 - [Release notes](https://github.com/sveltejs/vite-plugin-svelte/releases) - [Changelog](https://github.com/sveltejs/vite-plugin-svelte/blob/main/packages/vite-plugin-svelte/CHANGELOG.md) - [Commits](https://github.com/sveltejs/vite-plugin-svelte/commits/@sveltejs/vite-plugin-svelte@7.2.0/packages/vite-plugin-svelte) Updates `svelte` from 5.55.8 to 5.56.8 - [Release notes](https://github.com/sveltejs/svelte/releases) - [Changelog](https://github.com/sveltejs/svelte/blob/main/packages/svelte/CHANGELOG.md) - [Commits](https://github.com/sveltejs/svelte/commits/svelte@5.56.8/packages/svelte) Updates `svelte-check` from 4.7.3 to 4.7.4 - [Release notes](https://github.com/sveltejs/language-tools/releases) - [Commits](https://github.com/sveltejs/language-tools/compare/svelte-check@4.7.3...svelte-check@4.7.4) Updates `vite` from 8.1.5 to 8.2.0 - [Release notes](https://github.com/vitejs/vite/releases) - [Changelog](https://github.com/vitejs/vite/blob/main/packages/vite/CHANGELOG.md) - [Commits](https://github.com/vitejs/vite/commits/create-vite@8.2.0/packages/vite) --- updated-dependencies: - dependency-name: "@dagrejs/dagre" dependency-version: 3.1.0 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: npm-minor-and-patch - dependency-name: "@playwright/test" dependency-version: 1.62.1 dependency-type: direct:development update-type: version-update:semver-minor dependency-group: npm-minor-and-patch - dependency-name: "@sveltejs/kit" dependency-version: 2.70.2 dependency-type: direct:development update-type: version-update:semver-patch dependency-group: npm-minor-and-patch - dependency-name: "@sveltejs/vite-plugin-svelte" dependency-version: 7.2.0 dependency-type: direct:development update-type: version-update:semver-minor dependency-group: npm-minor-and-patch - dependency-name: "@tiptap/core" dependency-version: 3.29.2 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: npm-minor-and-patch - dependency-name: "@tiptap/extension-bubble-menu" dependency-version: 3.29.2 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: npm-minor-and-patch - dependency-name: "@tiptap/extension-code-block-lowlight" dependency-version: 3.29.2 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: npm-minor-and-patch - dependency-name: "@tiptap/extension-collaboration" dependency-version: 3.29.2 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: npm-minor-and-patch - dependency-name: "@tiptap/extension-collaboration-caret" dependency-version: 3.29.2 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: npm-minor-and-patch - dependency-name: "@tiptap/extension-link" dependency-version: 3.29.2 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: npm-minor-and-patch - dependency-name: "@tiptap/extension-placeholder" dependency-version: 3.29.2 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: npm-minor-and-patch - dependency-name: "@tiptap/extension-table" dependency-version: 3.29.2 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: npm-minor-and-patch - dependency-name: "@tiptap/extension-task-item" dependency-version: 3.29.2 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: npm-minor-and-patch - dependency-name: "@tiptap/extension-task-list" dependency-version: 3.29.2 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: npm-minor-and-patch - dependency-name: "@tiptap/pm" dependency-version: 3.29.2 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: npm-minor-and-patch - dependency-name: "@tiptap/starter-kit" dependency-version: 3.29.2 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: npm-minor-and-patch - dependency-name: "@tiptap/suggestion" dependency-version: 3.29.2 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: npm-minor-and-patch - dependency-name: "@tiptap/y-tiptap" dependency-version: 3.0.8 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: npm-minor-and-patch - dependency-name: dompurify dependency-version: 3.4.13 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: npm-minor-and-patch - dependency-name: svelte dependency-version: 5.56.8 dependency-type: direct:development update-type: version-update:semver-minor dependency-group: npm-minor-and-patch - dependency-name: svelte-check dependency-version: 4.7.4 dependency-type: direct:development update-type: version-update:semver-patch dependency-group: npm-minor-and-patch - dependency-name: svelte-dnd-action dependency-version: 0.9.77 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: npm-minor-and-patch - dependency-name: vite dependency-version: 8.2.0 dependency-type: direct:development update-type: version-update:semver-minor dependency-group: npm-minor-and-patch - dependency-name: yjs dependency-version: 13.6.31 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: npm-minor-and-patch ... Signed-off-by: dependabot[bot] <support@github.com> |
||
|
|
ca45539925 |
chore(deps)(deps): bump the go-minor-and-patch group across 1 directory with 13 updates
Bumps the go-minor-and-patch group with 10 updates in the / directory: | Package | From | To | | --- | --- | --- | | [github.com/JohannesKaufmann/html-to-markdown/v2](https://github.com/JohannesKaufmann/html-to-markdown) | `2.5.1` | `2.5.2` | | [github.com/go-chi/chi/v5](https://github.com/go-chi/chi) | `5.2.5` | `5.3.1` | | [github.com/jackc/pgx/v5](https://github.com/jackc/pgx) | `5.9.2` | `5.10.0` | | [github.com/mark3labs/mcp-go](https://github.com/mark3labs/mcp-go) | `0.56.0` | `0.57.0` | | [github.com/pb33f/libopenapi](https://github.com/pb33f/libopenapi) | `0.36.3` | `0.38.7` | | [github.com/prometheus/client_golang](https://github.com/prometheus/client_golang) | `1.23.2` | `1.24.1` | | [github.com/redis/go-redis/v9](https://github.com/redis/go-redis) | `9.19.0` | `9.22.0` | | [github.com/santhosh-tekuri/jsonschema/v6](https://github.com/santhosh-tekuri/jsonschema) | `6.0.2` | `6.0.3` | | [golang.org/x/image](https://github.com/golang/image) | `0.43.0` | `0.44.0` | | [modernc.org/sqlite](https://gitlab.com/cznic/sqlite) | `1.50.0` | `1.56.0` | Updates `github.com/JohannesKaufmann/html-to-markdown/v2` from 2.5.1 to 2.5.2 - [Release notes](https://github.com/JohannesKaufmann/html-to-markdown/releases) - [Commits](https://github.com/JohannesKaufmann/html-to-markdown/compare/v2.5.1...v2.5.2) Updates `github.com/go-chi/chi/v5` from 5.2.5 to 5.3.1 - [Release notes](https://github.com/go-chi/chi/releases) - [Changelog](https://github.com/go-chi/chi/blob/master/CHANGELOG.md) - [Commits](https://github.com/go-chi/chi/compare/v5.2.5...v5.3.1) Updates `github.com/jackc/pgx/v5` from 5.9.2 to 5.10.0 - [Changelog](https://github.com/jackc/pgx/blob/master/CHANGELOG.md) - [Commits](https://github.com/jackc/pgx/compare/v5.9.2...v5.10.0) Updates `github.com/mark3labs/mcp-go` from 0.56.0 to 0.57.0 - [Release notes](https://github.com/mark3labs/mcp-go/releases) - [Commits](https://github.com/mark3labs/mcp-go/compare/v0.56.0...v0.57.0) Updates `github.com/pb33f/libopenapi` from 0.36.3 to 0.38.7 - [Release notes](https://github.com/pb33f/libopenapi/releases) - [Commits](https://github.com/pb33f/libopenapi/compare/v0.36.3...v0.38.7) Updates `github.com/prometheus/client_golang` from 1.23.2 to 1.24.1 - [Release notes](https://github.com/prometheus/client_golang/releases) - [Changelog](https://github.com/prometheus/client_golang/blob/v1.24.1/CHANGELOG.md) - [Commits](https://github.com/prometheus/client_golang/compare/v1.23.2...v1.24.1) Updates `github.com/redis/go-redis/v9` from 9.19.0 to 9.22.0 - [Release notes](https://github.com/redis/go-redis/releases) - [Changelog](https://github.com/redis/go-redis/blob/master/RELEASE-NOTES.md) - [Commits](https://github.com/redis/go-redis/compare/v9.19.0...v9.22.0) Updates `github.com/santhosh-tekuri/jsonschema/v6` from 6.0.2 to 6.0.3 - [Release notes](https://github.com/santhosh-tekuri/jsonschema/releases) - [Commits](https://github.com/santhosh-tekuri/jsonschema/compare/v6.0.2...v6.0.3) Updates `golang.org/x/crypto` from 0.53.0 to 0.54.0 - [Commits](https://github.com/golang/crypto/compare/v0.53.0...v0.54.0) Updates `golang.org/x/image` from 0.43.0 to 0.44.0 - [Commits](https://github.com/golang/image/compare/v0.43.0...v0.44.0) Updates `golang.org/x/term` from 0.44.0 to 0.45.0 - [Commits](https://github.com/golang/term/compare/v0.44.0...v0.45.0) Updates `golang.org/x/text` from 0.39.0 to 0.40.0 - [Release notes](https://github.com/golang/text/releases) - [Commits](https://github.com/golang/text/compare/v0.39.0...v0.40.0) Updates `modernc.org/sqlite` from 1.50.0 to 1.56.0 - [Changelog](https://gitlab.com/cznic/sqlite/blob/master/CHANGELOG.md) - [Commits](https://gitlab.com/cznic/sqlite/compare/v1.50.0...v1.56.0) --- updated-dependencies: - dependency-name: github.com/JohannesKaufmann/html-to-markdown/v2 dependency-version: 2.5.2 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: go-minor-and-patch - dependency-name: github.com/go-chi/chi/v5 dependency-version: 5.3.1 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: go-minor-and-patch - dependency-name: github.com/jackc/pgx/v5 dependency-version: 5.10.0 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: go-minor-and-patch - dependency-name: github.com/mark3labs/mcp-go dependency-version: 0.57.0 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: go-minor-and-patch - dependency-name: github.com/pb33f/libopenapi dependency-version: 0.38.7 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: go-minor-and-patch - dependency-name: github.com/prometheus/client_golang dependency-version: 1.24.1 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: go-minor-and-patch - dependency-name: github.com/redis/go-redis/v9 dependency-version: 9.22.0 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: go-minor-and-patch - dependency-name: github.com/santhosh-tekuri/jsonschema/v6 dependency-version: 6.0.3 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: go-minor-and-patch - dependency-name: golang.org/x/crypto dependency-version: 0.54.0 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: go-minor-and-patch - dependency-name: golang.org/x/image dependency-version: 0.44.0 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: go-minor-and-patch - dependency-name: golang.org/x/term dependency-version: 0.45.0 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: go-minor-and-patch - dependency-name: golang.org/x/text dependency-version: 0.40.0 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: go-minor-and-patch - dependency-name: modernc.org/sqlite dependency-version: 1.56.0 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: go-minor-and-patch ... Signed-off-by: dependabot[bot] <support@github.com> |
||
|
|
143d6dd4fc |
Merge pull request #1060 from PerpetualSoftware/feat/attachment-viewer-unification
PLAN-2392 phase 3a: viewer unification (parity commit + DR-4b a11y contract) |
||
|
|
5320eebe9b |
Merge pull request #927 from PerpetualSoftware/dependabot/github_actions/actions/cache-6.1.0
chore(ci)(deps): bump actions/cache from 5.0.5 to 6.1.0 |
||
|
|
0cdd4620fa |
Merge pull request #982 from PerpetualSoftware/dependabot/github_actions/actions/setup-go-7.0.0
chore(ci)(deps): bump actions/setup-go from 6.5.0 to 7.0.0 |
||
|
|
08732f2653 |
Merge pull request #983 from PerpetualSoftware/dependabot/github_actions/actions/setup-node-7.0.0
chore(ci)(deps): bump actions/setup-node from 6.4.0 to 7.0.0 |
||
|
|
6be16e33db |
Merge pull request #1040 from PerpetualSoftware/dependabot/github_actions/actions/checkout-7.0.1
chore(ci)(deps): bump actions/checkout from 6.0.2 to 7.0.1 |
||
|
|
10c7b9a8b0 |
Merge pull request #1039 from PerpetualSoftware/dependabot/github_actions/actions-minor-and-patch-dee7dd5463
chore(ci)(deps): bump docker/login-action from 4.4.0 to 4.6.0 in the actions-minor-and-patch group across 1 directory |
||
|
|
124ebf8cef |
fix(a11y): stop a HELD Escape cascading past the viewer (TASK-2448)
The final full-diff review found BUG-2441 surviving by a second route. The per-event consumption mark cannot cover a HOLD: every auto-repeat keydown is a FRESH event object, and by the second one the viewer's lease is already released — so the event is unmarked and the owner underneath acts. Holding Escape closed the viewer and then the sheet or menu beneath it, from one physical press. The two route guards already rejected `e.repeat` for exactly this reason (with a comment saying so). `DockedSheet`, `BottomSheet` and `TopBar` did not. They do now. Regression tests come in the pair this file already establishes: the BLOCKED case (a repeat must not close) and the EMPTY-STACK REGRESSION (a repeat is ignored, but the next REAL press still closes) — the second is what fails if a guard declines unconditionally, which is how a deference change silently deadens a control. Mutation-verified: removing the guard fails both, restoring it passes both. Also corrects a stale comment in `attachments/events.ts` — `Lightbox` imports `LightboxImage`, it does not re-export it. Claude-Session: https://claude.ai/code/session_01LmbFxQFDjcYKBLcTnor6DC |
||
|
|
2ab5b1033f |
chore(ci)(deps): bump actions/setup-go from 6.5.0 to 7.0.0
Bumps [actions/setup-go](https://github.com/actions/setup-go) from 6.5.0 to 7.0.0. - [Release notes](https://github.com/actions/setup-go/releases) - [Commits](https://github.com/actions/setup-go/compare/924ae3a1cded613372ab5595356fb5720e22ba16...b7ad1dad31e06c5925ef5d2fc7ad053ef454303e) --- updated-dependencies: - dependency-name: actions/setup-go dependency-version: 7.0.0 dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] <support@github.com> |
||
|
|
df741172fe |
fix(a11y): keep Escape consumption event-scoped so one press closes one layer (BUG-2441, TASK-2448)
`isBlockedByModal()` answers "is a viewer lease held RIGHT NOW". `Lightbox`'s
escape-stack handler calls `onClose()` synchronously, and Svelte flushes the
teardown — hence `lease.release()` — inside that call. Every `window` keydown
listener later in the SAME dispatch therefore asked against an empty stack, was
told "nothing is in front of you", and closed a second layer: one Escape closed
the viewer AND the `DockedSheet` / `BottomSheet` underneath it. The query was
right; the moment it was read was not.
Consumption is now recorded per EVENT. The viewer marks the dispatch it
consumed (`noteEscapeConsumedByViewer`) before closing, and `isBlockedByModal`
takes an optional event: a marked one blocks every later owner outright,
whatever the lease says by then. `runTopEscape(event)` forwards the driving
event to handlers so the viewer has something to mark; the stack itself never
reads it.
Keyed on the event object, NOT on `defaultPrevented` — that flag says only
"somebody handled this key", is set by controls that are not viewers, and
honouring it would change sheet behaviour with no viewer present. This marker
can only ever be set by a frontmost viewer, so on an empty lease stack it is
unreachable by construction.
DELIBERATE, NAMED BEHAVIOUR CHANGE — the FOURTH named parity exception of
PLAN-2392 phase 3a, alongside the three already recorded. `DockedSheet`,
`BottomSheet` and the `TopBar` overflow menu now decline an Escape a viewer has
already consumed. TASK-2430 shipped `DockedSheet` declining an already-
`defaultPrevented` Escape unannounced and it was reverted; this is approximately
that change made deliberately, with a stated reason, a narrower trigger and
tests. `TopBar` is not known to be broken today — its listener happens to run
before the route driver — but that is mount-order luck, not a guarantee, so it
is closed too.
EMPTY-STACK PARITY, per owner: with no viewer, the marker cannot exist, so each
touched call site reduces to exactly its previous expression. Asserted rather
than argued — `DockedSheet` and `BottomSheet` each gain an unmarked-Escape
regression beside the new blocked case, `TopBar`'s existing owner-5 e2e covers
both directions, and a `viewerBackdrop` unit test states the equivalence
directly (`isBlockedByModal(o, unmarked) === isBlockedByModal(o)`). The reverted
2430 `defaultPrevented` regression test still passes untouched.
The two `test.fail()` cases pinning BUG-2441 are now real assertions, each
extended with a second press proving the sheet keeps its own Escape rather than
going permanently deaf.
MUTATION-VERIFIED, both halves (TASK-2436's precedent):
• drop the viewer's mark → owners 3 and 4 fail: "the sheet is a LOWER layer
and must survive the press / element(s) not found", plus the new Lightbox
jsdom case ("expected spy to not be called at all, but actually been
called 1 times").
• drop the sheets' event argument → the same two e2e cases fail identically.
• drop the driver's `runTopEscape(e)` → the wiring contract fails
("expected … to match /runTopEscape\s*\(\s*e\s*\)/").
Gates: npm run check 0 errors; npm run test 1090 passed; the three viewer e2e
specs 32 passed.
|
||
|
|
228f99318b |
test(e2e): prove the viewer's modal contract in a browser (TASK-2436)
Phase 3a deleted a native `<dialog>` that `showModal()` was giving five
guarantees for free — top-layer stacking, background inertness, a focus trap,
focus restore and Escape — and hand-wrote each one. jsdom's `<dialog>` polyfill
(`src/test/setup-jsdom.ts`) only toggles attributes, so the phase's unit suites
cannot see ANY of those five: no inertness, no top layer, no `:modal`, no real
Tab traversal, no stacking. DR-9 says this is verified in a real browser or it
is not verified.
Three specs, 32 tests, each written against "what mutation would this catch
that a jsdom-equivalent implementation would survive":
attachment-viewer-modal.spec.ts — portal + MEASURED viewport geometry (a
`transform`/`contain` ancestor changes the rect, not the declaration); focus
entry; background inertness proven by injecting a focusable probe into every
body child AND by the REAL top-bar control, which can only go inert by
cascade; focus restore asserted as an ORDERING (the invoker is verified
UNFOCUSABLE while the viewer is up, so a restore-before-release could not
pass) AND on its DECLINE path, with a detached invoker — the ordinary case,
since the NodeView that opens the viewer is re-rendered on any document
change; the focus trap in BOTH directions, including the backward-wrap branch
(`nextTrapTarget` returns `last` only for Shift+first) and the single-control
viewer where first === last; `showModal()` vs `show()` vs a dialog mounted
closed and shown later, plus a native modal opened OVER the viewer winning
both Escape and Tab outright; paint order hit-tested against a 99999
body-portaled rival, with a raised-z-index control so the measurement is
provably sensitive to stacking (Chromium excludes inert subtrees from hit
testing, which would otherwise make it vacuous); Escape through BOTH real
route guards, asserting which layer closed; two stacked viewers; and the
mobile pane integration, where the pane's nested `inert` writes and the
backdrop's body-child writes are shown to be disjoint at every transition.
attachment-viewer-owners.spec.ts — all seven TASK-2430 owners, each with a
viewer-frontmost case AND an empty-stack regression: the six root shortcuts,
the collection route's navigation half, DockedSheet, BottomSheet, the TopBar
overflow menu, the sidebar edge swipe (including a gesture that STRADDLES the
viewer opening) and the co-mounted item graph.
attachment-viewer-parity.spec.ts — the finite parity matrix, four producers ×
{open, ←/→, Escape, backdrop click, close}; Enter/Space activation of inline
images including explicit `repeat: true` keydowns; Cmd/Ctrl+Enter still being
the comment editor's SUBMIT; hostile/long/bidi accessible names and RTL
geometry; the host lifecycle, driven through CLIENT-SIDE navigation with the
document verifiably still mounted (a `page.goto()` version would prove only
that unloading a document removes its DOM); and two-host isolation.
TWO KNOWN DEFECTS ARE RECORDED AS `test.fail()`, not papered over — BUG-2441.
One Escape over a DockedSheet or a BottomSheet closes BOTH that sheet and the
viewer. The sheets' `isBlockedByModal()` guards are correct; they are READ too
late. Both they and the route's escape driver are `window` keydown listeners,
and Svelte flushes the viewer's teardown synchronously inside the driver's
handler, so a sheet listener running later in the SAME dispatch sees an
already-empty lease stack. Invisible to the unit suites (one component's
handler, nothing releasing a lease mid-dispatch) and invisible to a
click-driven test — closing the same viewer with its Close button leaves the
sheet open, which is how it was isolated. The annotations are applied AFTER
setup, so a login/seed/navigation failure cannot hide behind them. The tests
assert the CONTRACT, so the day it is fixed they go red and the annotations
must come off. The TopBar overflow menu, checked the same way, is unaffected.
Documented gaps, stated rather than papered over: the paint-order rival is a
synthetic overlay at the picker's declared z-index (the real picker cannot be
co-present — opening the viewer by pointer dismisses it) and must be de-inerted
to be hit-testable; `expectBackgroundInert`'s floor is one behaviourally-proven
background child; the pane test's inert-set comparison identifies elements by
tag plus first class; and gestures under a frontmost viewer are dispatched
rather than delivered, since a real wheel or touch cannot reach a covered
element (the graph's baseline leg does use real input).
The shared fixture builds a real 200x150 PNG rather than reusing the 1x1 the
older attachment specs share: that one has a bad IDAT checksum, so thumbnail
decoding skips and the rendered `<img>` has no box — unclickable, and "not
visible" to Playwright. It also has to out-size the editor's image toolbar,
which is absolutely positioned over a small image's whole area.
Claude-Session: https://claude.ai/code/session_01LmbFxQFDjcYKBLcTnor6DC
|
||
|
|
f8ecb69b1b |
fix(editor): deliver server capabilities to the image NodeView (BUG-2426, TASK-2435)
The body editor fetched /server/capabilities and wrote the processor's format list onto the extension — `ext.options.supportedFormats = …`. Tiptap's `options` is a getter returning a fresh spread per access, so that write landed on a temporary; the NodeView (which snapshots `this.options` once at construction) never saw it, and rotate/crop sat permanently in the degraded "no image processor" state. Same root cause `$lib/attachments/hostAddress` exists for, so the same shape: `supportedFormats` becomes a READER the host supplies, read at the moment the toolbar gates on it. Editor.svelte closes over its own capability state; the assignment is gone. CommentEditor keeps transforms deliberately OFF — its reader is a constant `[]`, not the server's list. The new spec drives both REAL mount sites: a body toolbar built before capabilities resolve snaps to correct per-format gating afterwards, one built after is correct immediately, an unsupported format (image/tiff) stays refused with the format-specific tooltip, and the comment composer stays empty through the capability fan-out. Reverting the reader to an assignment turns all four red. Claude-Session: https://claude.ai/code/session_01LmbFxQFDjcYKBLcTnor6DC |
||
|
|
03e0edb605 |
feat(attachments): complete the MIME matrix and address fence (TASK-2434)
TASK-2433 made the inline-image activation path resolve the attachment's
MIME before emitting and refuse anything not positively allowlisted. That
refusal was binary: `ok` + raster opened the viewer and every other result
returned silently, leaving two states where a focused, button-announced
image swallowed the gesture — a `transient` probe, and a resolved MIME the
viewer will not take.
This completes the four-branch matrix, each arm with a destination:
- `ok` + allowlisted raster → the viewer, unchanged.
- `ok` + anything else → the options PANEL (DR-7). A REDIRECT, not a
refusal: an SVG or a PDF referenced as an inline image is a real
attachment with real options, it is just not something to hand a viewer
that would execute it. The image therefore stays a real activation
target and its accessible name names the panel — taking the semantics
off (as the binary gate did) would hide a working control, and would
make the redirect fire exactly once before the recorded MIME closed the
gate on every later tap.
- `missing` (authoritative 404) → the permanent placeholder, latched,
nothing opened.
- `transient` → the RETRYABLE placeholder. Never an open and never a
latch: only a 404 is authoritative (DR-17).
The fence is hardened to the FULL address. The whole address is captured
before the await and compared after, and both emissions stamp the CAPTURED
values — the reader is live (`CommentEditor` is reused across an item
switch) so a re-read can address the wrong host. The continuation also
re-checks `deleted` on its own terms: a delete does not change which
attachment the node points at, so a probe issued before it can resolve `ok`
afterwards with the uuid still current. Check and emit stay adjacent and
synchronous — no timer, no microtask between them.
Also adds the minimal pending contract the await needs: `aria-busy` plus a
wait cursor while the MIME resolves, cleared by the resolution's finalizer
and by a uuid swap. No new chrome.
Seam with PLAN-2411, stated in comments and deliberately not built: the
`deleted` latch is cleared ONLY by an authoritative restore signal on
2411's channel, never by editor undo — DR-17 requires Ctrl-Z to leave an
inert placeholder rather than resurrect a working attachment.
Claude-Session: https://claude.ai/code/session_01LmbFxQFDjcYKBLcTnor6DC
|
||
|
|
30fa9fc3dd |
refactor(attachments): route inline images through the unified viewer (TASK-2433)
The inline `pad-attachment:` image NodeView opened its own hand-rolled `<dialog>`: `openImageLightbox` appended one to `document.body` and `showModal()`d it. It worked, and everything the modal contract is made of came free from the platform — top layer, inertness, focus trap, focus restore, Escape. This commit deletes it and emits on the viewer channel instead, so the `Lightbox` that `AttachmentViewerHost` mounts is the only viewer on this route, with the lease-stacked backdrop, the escape ordering and the DR-16 filter re-applied over the whole set. Shaped as a pure swap so the deletion is reviewable on its own. THE MIME IS RESOLVED BEFORE ANYTHING IS EMITTED, revising the decomposition's "keep today's positively-known gate". TASK-2431 made `Lightbox` fail closed on an unresolved MIME, so an event carrying `mime_type: null` is not "let the viewer decide" — it is a viewer that mounts and renders no image. Activation now awaits `fetchAttachmentMetadata` (a cache hit in the common case) and emits only on a positively-known allowlisted answer. That also closes a mid-phase bypass: the old gate read `knownMime` only when truthy, so a click landing before the lazy probe resolved opened the original file, and a later unsafe answer did not close it. What it costs, deliberately and temporarily, is that a `transient` probe now opens nothing — the four-branch matrix that makes the gate total is TASK-2434's. AND THE CHANNEL ENFORCES IT, because a rule only one producer follows is a convention, not an invariant — and the failure mode is silent precisely because the viewer fails closed. `notifyViewerOpen` now takes a set whose `mime_type` is non-nullable (`ViewerReadyImage` / `ViewerOpenRequest`), so a forgetful producer is a compile error rather than an image that does not open, and it refuses a set at runtime unless every entry is positively allowlisted, the way it already no-ops on a missing address. The WHOLE emission is dropped rather than the offending entry: `index` and `attachmentId` name a position in the set the producer built, and silently renumbering it would open the viewer on a different image than the one activated. `LightboxImage` is untouched — the consumer side stays nullable, because its records are live and `size_bytes` / `width` / `height` must remain optional for 3b. `events.test.ts` asserted the permissive behaviour; it now asserts the refusal, with a control so a gate that refused everything cannot pass. The await is new, so the fences are too: the gate's premises are re-checked on the far side (teardown, a uuid swap, a deletion, and a host that moved — the comment composer's address is live across an item switch), and one activation at a time. That latch is generation-stamped: it is released by the resolution's own finalizer AND by a uuid swap, which this NodeView outlives, so an unconditional release would let a superseded request unlock the one that replaced it, and a HEAD that never settles would otherwise leave the new image permanently unopenable. The `.attachment-image-lightbox` CSS goes with it; it lived in `app.css`, not the TS file, where a JS-only sweep would have left it. Tests assert the emitted PAYLOAD, not the dialog's absence — an implementation that deleted the dialog and emitted nothing satisfies "no dialog" — and a new spec drives the WHOLE route with nothing stubbed but the network: real NodeView, real bus, real `AttachmentViewerHost`, real `Lightbox`, asserting a viewer in the document, addressed to the right host, showing the un-variant original. Verified by mutation: emit-nothing, emit-unresolved-MIME, drop-the-allowlist-check at either the producer or the channel, gate the set on its first entry only, drop the one-at-a-time latch or release it unconditionally, drop any post-await fence, null the invoker, and append an overlay to `documentElement` each fail at least one test. Two of those needed cases the existing specs could not reach: every prior DR-16 test selected the node first, which pre-resolves the MIME through `canActivate()` and leaves the activation path's own check unexercised — precisely the state a body image is in when it is clicked. Claude-Session: https://claude.ai/code/session_01LmbFxQFDjcYKBLcTnor6DC |
||
|
|
092498da23 |
feat(a11y): make inline body images keyboard-activatable (TASK-2432)
The AttachmentImage NodeView's <img> opened the viewer on click and was reachable by no other means: no role, no tabindex, no accessible name. Give it the DR-12 button contract, and route BOTH activation paths through one shared activate(). The MIME gate used to live inside the click handler, which made it a property of the MOUSE rather than of activation — a keyboard path that opened on its own would have bypassed it. One canActivate() predicate now owns the gate and is read by activate() AND by the semantics pass, because 'can be opened' and 'announces itself as openable' are the same question and a dead focus stop is what they look like when they disagree. Keyboard handling: stops propagation before activating (ItemTimeline delegates thumbnail handlers across its whole entry list, and that list contains live CommentEditor instances rendering this very NodeView, so without it one keypress opened two viewers); ignores MODIFIED keys, since Cmd/Ctrl+Enter is CommentEditor's submit binding; and suppresses key REPEATS without re-activating, so a held key opens one viewer rather than one per repeat. Semantics are conditional on the image actually being a control — no uuid, a confirmed deletion, a load-failure placeholder, or a probed non-allowlisted MIME each take role/tabindex/aria-label back off (and blur it first) rather than leave a focus stop that announces itself as a button and does nothing. The MIME clause is a judgment call: the contract names only deleted/missing, but it is the same dead-stop rule and the same thing ItemTimeline does, and its cost is documented in place. The accessible name is alt with a GENERIC fallback: there is no filename on the node's attrs and the HEAD metadata carries none, so the filename form DR-12 sketches has no source here. DECLARED CROSS-FILE FOLD-IN — ItemTimeline.svelte no longer manages images a live editor owns (new isEditorOwnedImage predicate). Making the NodeView image focusable made two of its behaviours reachable that were not before: its accessibility pass stripped role/tabindex from any image whose UUID is not in attMeta — which is every image in a DRAFT comment, since attMeta is probed from SAVED bodies only — and its delegated keydown, having no modifier check, opened a viewer on the Cmd+Enter this node now deliberately lets through. Both are regressions this commit introduces, so both are fixed here. Its own rendered thumbnails are untouched. That wiring is covered by an integration spec that mounts the REAL ItemTimeline with a REAL unstubbed CommentEditor, rather than a copy of the delegation logic: every assertion about a guard fails when that guard is deleted from the component. Claude-Session: https://claude.ai/code/session_01LmbFxQFDjcYKBLcTnor6DC |
||
|
|
7cfe50d842 |
feat(a11y): defer global key and gesture owners to a frontmost viewer (TASK-2430)
Global keyboard/gesture owners now consult the shared arbitration helper
(`isBlockedByModal`) instead of acting unconditionally, so the
native-`dialog:modal` branch is enforced everywhere rather than only in the
two route files TASK-2429 rewired.
This is modal-contract work, not parity work: route, graph, pane, sidebar and
sheet behaviour deliberately changes while a viewer is frontmost. With NO
viewer and no native modal the helper returns false, so EMPTY-LEASE BEHAVIOUR
IS UNCHANGED FOR EVERY OWNER — each ships an empty-stack regression test, and
forcing any owner's guard to decline unconditionally fails one (verified per
owner). Every guard is also mutation-verified to kill at least one test in the
other direction; there is no guard left that a test cannot fail on.
Two earlier revisions of this commit broke that promise and were REVERTED:
DockedSheet declining an already-`defaultPrevented` Escape, and the overflow
menu's arrow-nav being revived past `svelte-dnd-action`'s role rewrite. Both
fire with no viewer present, both are defensible on their own merits, and both
belong in their own item rather than arriving unannounced inside an attachments
phase. The one remaining empty-lease change is named and intended: `?` no
longer closes the Keyboard Shortcuts modal from inside itself, which falls out
of the native-dialog branch this task exists to enforce (Escape and its close
button still dismiss it).
The seven owners:
1. root app-shell shortcuts (+layout) — were entirely unguarded
2. both route keydown handlers (see the asymmetry below)
3. DockedSheet — an unregistered role="dialog" Escape owner
4. BottomSheet — front layer wins over the sheet-only frontmost check
5. TopBar overflow menu — Escape + Up/Down
6. Sidebar — mobile edge-open swipe and the swipe-to-close
7. ItemGraph — wheel zoom and pan; it co-mounts with the viewer
ESCAPE IS NOT ARBITRATED ON THE TWO PANE ROUTES, deliberately. Those handlers
are the only code that runs `escapeStack`, and the VIEWER's Escape lives there
— an arbitration bail above the dispatch would return first and leave a
frontmost viewer undismissable by keyboard, reintroducing exactly the dead key
TASK-2429 fixed. What DID need arbitrating is the collection route's NAVIGATION
half (j/k, arrows, h/l, Enter, Tab), which would otherwise keep re-targeting
the list under the viewer — so the guard sits below the Escape dispatch and
above the nav switch, and both bounds are asserted. The item route, being
Escape-only, gains no arbitration guard at all (its existing `defaultPrevented`
/ text-entry / `hasForeignEscapeOwner` guards are untouched). Hoisting the
guard, dropping it, and adding one to the item route are all mutation-verified
to fail a test.
`hasForeignEscapeOwner()`'s ARIA branch becomes LEASE-AWARE, because 3b changes
its premise. It used to be right that a sheet open beneath a viewer still owned
Escape — the sheets acted unconditionally. Now they stand down, so reporting
one would leave Escape with NO owner: driver returns, sheet declines, viewer's
stack never runs. The branch now counts only sheets NOT behind the frontmost
viewer, by CONTAINMENT rather than a blanket "a lease exists": a sheet nested
INSIDE the viewer is in front of its content and does still own its Escape.
The native branch is checked first and wins outright, on both the
`dialog:modal` path and the `dialog[open]` fallback: nothing in the app guards
a native `<dialog>`, so unlike a sheet it never stood down and does still own
Escape. Applying the containment rule to it as well was tried and reverted —
the fallback cannot tell a modal from a non-modal dialog, so letting the lease
out-rank it would fire the browser's native `cancel` AND run the stack, closing
two layers on one press. The residual asymmetry that leaves (a NON-modal
`<dialog open>` beside a viewer, on an engine without `:modal`) is documented
at the branch and is unreachable here twice over: `Modal.svelte` is the only
`<dialog>` in the tree and only ever calls `showModal()`, and every engine that
ships `<dialog>` ships `:modal`.
Plus two more global Escape owners found by review sweep: the workspace graph
route and the console shell. Neither can host a viewer and neither drives the
escape stack, but the root layout mounts native dialogs on both, so one press
would cancel the dialog AND mutate the layer underneath.
Captured gestures that straddle the viewer opening are gated at START and on
the captured move/end: the graph has no `lostpointercapture` handler, so its
pan is torn down (capture released) rather than merely skipped; the pane
divider ends its resize; the sheet and sidebar swipes are abandoned. The start
gates are separately load-bearing — a gesture begun under a viewer must not
come alive when the viewer closes — and are tested as such.
Owner arguments are the ACTING SURFACE (a bound element, `e.currentTarget`, or
`null` for the app shell), never `event.target`. The four WINDOW-level call
sites — +layout, TopBar, DockedSheet, BottomSheet — each have a test that
dispatches from inside the viewer, which is the case that distinguishes the two
choices; the element-bound listeners (PaneHost's divider, Sidebar's aside,
ItemGraph's viewport) cannot receive an event originating in the viewer at all,
so there is nothing to distinguish there.
Deliberately NOT guarded: pure pointer-dismissers (clickOutside, the pickers,
board lanes, and TopBar's outside-click), which only tear down lower UI.
DEFERRED, not covered here: `svelte-dnd-action`'s global drag handlers (nine
call sites) and the editor's block-drag action own gestures whose finalize can
persist a reorder if a viewer opens mid-drag. Gating them needs a reactive
lease signal rather than a call-site guard, which is a materially larger change
than this task's contract — flagged for a follow-up item.
ItemGraph's pointerup path carries NO gate: the obvious symmetry with the move
gate is unfalsifiable — teardown is identical either way, so no test can fail on
its removal — and an unkillable guard reads as coverage without being any. The
straddle is covered by the move gate, which releases the pointer capture. The
one sequence neither gate can see (a capture-less press whose release RETARGETS
to the portaled viewer, leaving `maybeDrag` latched) is pre-existing and already
mitigated by the `buttons & 1` abort in `onPointerMove`; a test now pins that
mitigation so it cannot be removed silently.
Also in this commit:
- e2e: target the create-workspace dialog by accessible name, not a bare
`dialog` role
- test infra: `$app/navigation` mock + a localStorage shim for the jsdom
project, without which Sidebar/TopBar/PaneHost/+layout could not be
mounted at all. Both Storages are cleared before every TEST (not per
setup-file load) so the shim is deterministic under any pool config; the
trade-off — in-memory stand-ins cannot reproduce real Storage failures — is
documented at the shim. NOTE: `svelte-dnd-action`'s role rewrite
(`menu`/`menuitem` → `list`/`listitem`) leaves TopBar's roving-focus query
matching nothing in the browser. Pre-existing, left as-is, documented at
both the query and its test.
The native top-layer leg of the precedence rule is not asserted against a real
engine (jsdom has no top layer and throws on `:modal`; the suite emulates it);
end-to-end proof belongs to TASK-2436's Playwright suite.
Claude-Session: https://claude.ai/code/session_01LmbFxQFDjcYKBLcTnor6DC
|
||
|
|
a3f272a97a |
feat(attachments): make the viewer open gate total across every surface (TASK-2431)
DR-16's allowlist gated the image the user CLICKED. That is not a gate.
The timeline built its ←/→ sibling list from every `img[data-attachment-id]`
in the comment body with no MIME consulted at all, while the markdown renderer
emits an `<img>` for any `image/*` — correct for RENDERING, wrong for OPENING.
So a user could open a safe PNG and press Right onto an `image/svg+xml`. The
whole list is now resolved from the CACHED probe metadata and filtered through
`canOpenInViewer`, on both the mouse and the keyboard path; the index is
derived from the clicked attachment's ID rather than its DOM position, because
filtering reindexes everything after the first refusal. An unresolved MIME
fails safe and is retried on a later probe run — no cold-start regression,
since an unprobed thumbnail renders as a placeholder, not an image.
A refused thumbnail is no longer a dead control either: `role="button"`,
`tabindex` and the "View image" name now track the same predicate, so a
filtered-out SVG is not a focus stop whose activation does nothing. That pass
tracks the RENDERED set, not the fetched one — the pane's Activity / Versions
tabs rebuild every comment card without changing `entries`, and the rebuilt
images were left mouse-openable with no keyboard route at all.
The timeline also had NO A→B viewer reset — `lightbox` was cleared on close and
nowhere else — so a workspace switch under the same ref left a viewer up,
rebuilding URLs for the previous workspace's ids. It now clears on a view
change. Both direct mounts are keyed per open, like the bus host's, so the
viewer's untracked capture of its index can never be reused.
`Lightbox` re-states the rule at the point of USE, and FAILS CLOSED: only a
positively allowlisted `mime_type` is viewable, so a null / unresolved one is
not. It is the last thing between a set and a rendered image — the place where
the benefit of the doubt is worth least — and admitting null let an emitter
hand over `[safe, unresolved]` and the user arrow onto the unresolved one. The
producers lose nothing: the strip always has the MIME from its list row, and
the timeline already excludes unresolved entries. The contract for new
producers is therefore to RESOLVE BEFORE EMITTING. The filter is `$derived`
rather than captured, so a record whose MIME resolves to something unsafe
after open, a set replaced under an open viewer, or an entry removed beneath
the position the user navigated to are all re-answered rather than trusted;
the shown index clamps instead of blanking.
`LightboxImage` gains `mime_type`, `filename` and — nullable — `size_bytes`,
`width`, `height`. The dimensions have no reader yet: they land now so phase
3b's pixel-based loading policy need not reopen the event, the host and every
producer. The component's own `{id, alt}` twin is gone; the channel's
declaration is the only one. The strip threads the full row (it had been
dropping `width`/`height` at `StripAttachment` and `size_bytes` at the mapping)
and both producers now pass the invoking element, so focus returns to the tile
rather than relying on the viewer's held-focus fallback.
NOT changed: `isImageMime`. It decides `<img>` vs chip and governs deferred
share-page surfaces; this phase gates the viewer OPEN, not the render.
Tests: a mixed safe/unsafe/unresolved list driven through mouse, keyboard and a
full ←/→ cycle against the REAL viewer (what an arrow key lands on is the
claim); the set changing UNDER an open viewer — resolved-unsafe-after-open,
removed, replaced, appended; and the payload each producer emits, fed unsafe
and unresolved rows rather than only safe ones, since a stub-based payload test
on safe inputs cannot fail when the gate does. One earlier test asserted that
an unresolved MIME OPENS — it pinned the hole open, and is now the test that
it must not. Every guard was mutation-checked; each kills the tests that
cover it.
|
||
|
|
13852439e1 |
feat(a11y): give the attachment viewer a real modal contract (TASK-2429)
`Lightbox` was a `role="presentation"` fixed div with a local `<svelte:window>`
keydown handler and no focus management at all. 3a's later tasks delete the
editor's hand-rolled `showModal()` dialog and route inline body images here, so
everything that dialog was getting from the platform for free has to exist here
first (DR-4b).
The contract:
- `role="dialog"` + `aria-modal="true"` + an accessible name (the image alt,
else "Attachment viewer"), tracking the image CURRENTLY shown. The controls
get real `aria-label`s too — their text is "✕" / "‹" / "›", and `title` does
not win over element content for the accessible name.
- Portaled to `<body>` DIRECTLY — deliberately not `portalAction.ts`, which
targets the nearest ancestor `<dialog>` when one exists: the opposite of
what a top-most surface needs. `<body>` is also the parent the backdrop
manager's inert bookkeeping requires, and the only one with no ancestor that
could establish a containing block and silently trap a `position: fixed`
overlay.
- `wsSlug` CAPTURED at open, not read live: the pane switches workspace
without remounting what is above it, so a live read could rebuild
already-captured attachment ids against a different workspace.
- Focus entry to the first TABBABLE DESCENDANT via `paneFocusables` (the root
is `tabindex="-1"` only as the no-controls-yet fallback); restore on close to
an OPTIONAL `invoker` prop, verified still connected AND focusable — else
focus is parked on `<body>` deliberately. With no invoker threaded it falls
back to whatever held focus at open, so the producers that thread one only in
TASK-2431 don't come out of this commit worse than they went in. The restore
declines when something else already owns focus.
- Background inertness via TASK-2427's manager (`acquire(exemptRoot)`), never
a hand-rolled `inert`. Released BEFORE the focus restore, and the returned
`stackEmpty` decides: with a viewer still open the manager has already handed
focus into it and this one stands down.
- Tab trap through `paneFocusables` / `nextTrapTarget` — the pane's tested trap
math, not a second implementation.
- ONLY the frontmost viewer (`isViewerFrontmost`) traps Tab, handles ←/→ and
consumes Escape. Handlers are global and `nextTrapTarget` deliberately
redirects out-of-container focus INWARD, so a background viewer would
otherwise drag focus out of the viewer in front of it. And the viewer stands
down entirely (`isBlockedByModal`) while a `showModal()` dialog is open over
it: the top layer is above any body-portaled surface, so the frontmost LEASE
is not always the frontmost SURFACE, and the manager keeps such a dialog
operable on purpose.
- Escape: the local branch is DELETED, not gated — it ignored
`defaultPrevented`, so alongside the stack it gave Escape two owners and let
one press collapse two layers. `escapeStack` is now the sole owner, at a new
`viewer` priority (50) above `menu` (40).
`AttachmentViewerHost` stops restoring focus itself and threads `invoker` down
instead. Its own restore ran while the viewer still held the backdrop lease —
i.e. while the invoker sat inside an `inert` body child, where it is not
focusable at all — so it would have silently become a no-op the moment this
commit landed. The only correct moment is after the lease is released, which is
inside the viewer's teardown.
Atomic with the route guards, because the stack is unreachable without them:
both `[collection]` and `[collection]/[slug]` bailed out of the ESC chain on
`document.querySelector('dialog[open], [role="dialog"]:not(.item-pane)')`, which
the viewer now matches — Escape would have been dead. Both call the shared
`hasForeignEscapeOwner()` instead. It KEEPS the ARIA branch (`BottomSheet` /
`DockedSheet` are shipped `role="dialog"` Escape owners with no stack
registration; dropping it would regress them) and narrows only the NATIVE branch
to a feature-detected `dialog:modal`, falling back to today's `dialog[open]`
where the pseudo-class is unsupported.
The backdrop's `z-index` goes to 100000. At 1000 it was under the desktop emoji
picker's body-portaled dropdown (99999) — and the app shell wrapper is
`display: contents`, so every fixed overlay in the tree competes in the ROOT
stacking context; being a body child is not protection. A surface that paints
over a viewer that has inerted it is visible-but-untouchable, the worst of both.
The full sweep, and the rule that a new overlay above this value is a bug, are
recorded at the declaration. The one thing legitimately above it is a native
`showModal()` dialog, which gets there via the top layer and no z-index at all.
Collision audit for `[role="dialog"]`-as-foreign-modal consumers:
- `app.css` `@media print` — the viewer matches, and should: a transient
overlay must not print. Documented in place (a JS-only grep misses this).
- `paneFocus.ts` `PANE_EXEMPT_SURFACE_SELECTOR` and `PaneHost`'s mobile trap +
focus-follows classifier — the viewer matches, and must: it runs its own
trap and key handling.
- `web/e2e/**` incl. `e2e/lib/*.ts` — no shared dialog selector constants; the
only bare `getByRole('dialog')` is
`workspace-bundle-roundtrip.spec.ts:204`, claimed by TASK-2430 by name.
Tests: 36 cases for the modal contract, 10 for `hasForeignEscapeOwner`, plus the
fallout of portaling (host tests can no longer scope by container — ownership is
proven by distinguishable payloads and destroying a known host; the strip's
Escape case now drives the stack). Comments state what jsdom cannot prove — real
inertness, layout/stacking, real Tab traversal — which is TASK-2436's browser
suite, and mark the one assertion whose guard is genuinely indistinguishable from
its neighbour in jsdom. Verified by mutation: 26 mutations run, all caught except
that documented one.
`escapeGuardWiring.svelte.test.ts` is a deliberately narrow addition: the
Lightbox tests drive a route-SHAPED driver they define themselves, so they prove
the shape and not that either route still calls it. This asserts against the real
source of both route files that `hasForeignEscapeOwner()` is imported, called as
an early return, and called BEFORE `runTopEscape()` — catching deletion,
re-inlining of the old selector, and reordering, which is the actual regression
risk. Mounting a route under vitest to prove it behaviourally is not worth its
cost; the behavioural proof is TASK-2436's browser suite. Two things stop it
being a grep that lies: comments are stripped first (every one of these strings
now appears in prose in those files, so a whole-file search could be satisfied by
a comment), and the assertions are scoped to the handler that actually calls
`runTopEscape` rather than to the file. Verified against a commented-out guard, a
guard moved to an unrelated helper, and a reverted selector under different
quoting — while a prettier-style reflow of the guard still passes.
TASK-2429
|
||
|
|
884ddc575e |
feat(attachments): add viewer open channel and per-host viewer host (TASK-2428)
Claude-Session: https://claude.ai/code/session_01LmbFxQFDjcYKBLcTnor6DC |
||
|
|
4fb946c528 |
feat(a11y): add refcounted viewer backdrop manager (TASK-2427)
New `web/src/lib/a11y/viewerBackdrop.ts` (PLAN-2392 phase 3a): a refcounted, lease-stacked owner of `inert` on `document.body.children` for body-portaled viewer surfaces, plus the modal-arbitration helpers. - Only the FRONTMOST lease's exempt root stays interactive; release RECOMPUTES the desired inert set from the stack rather than undoing its own writes, so out-of-order release is correct. - Records only what it set, so pre-existing `inert` survives. - ONE shared childList observer, disconnected at zero leases, so a body portal arriving mid-lease is inerted too. - Releasing the frontmost lease hands focus to the next viewer's first tabbable descendant; releasing a background lease changes nothing. - `isBlockedByModal` derives from lease state (never DOM `inert`), returns false on an empty stack, and uses a feature-detected `dialog:modal` for the native branch. Nothing consumes it yet — independently green. Claude-Session: https://claude.ai/code/session_01LmbFxQFDjcYKBLcTnor6DC |
||
|
|
417929c5a4 |
Merge pull request #1037 from jairbj/feat/nix-flake-packaging
feat(nix): add flake packaging with CI build |
||
|
|
0d31bae61b |
fix(nix): refresh vendorHash for current main's go.sum
The hash was computed against July-27 main; go.sum has since moved (dependabot + mainline work), and CI builds the PR merged with main. Claude-Session: https://claude.ai/code/session_01RNcrc3CtXwJwreubtHTgN6 |
||
|
|
bda6987125 |
Merge pull request #1058 from danfinn5/fix/cloud-mode-error-messages
fix(cli): surface actionable errors for cloud-mode setup failures |
||
|
|
71c18200c5 |
ci(nix): pin nix-installer-action to the actual v22 commit
The pinned SHA was a genuine ancestor commit from the upstream repo but ~68 commits behind the v22 tag it claimed; the workflow's own convention requires SHA and version comment to move together. Claude-Session: https://claude.ai/code/session_01RNcrc3CtXwJwreubtHTgN6 |
||
|
|
c491a4dfcd |
Merge pull request #1059 from PerpetualSoftware/feat/attachment-options-panel
feat(attachments): an options panel for files, everywhere you meet them (PLAN-2392 phase 2) |
||
|
|
5a6625ed24 |
fix(attachments): abandon an open confirmation when permission is withdrawn
Confirm round on the six acceptance criteria: five met, one not. "A peeked pane offers no delete" held for the actions but not for a confirmation already on screen. The pane can go peeked between opening the confirmation and answering it, and blocking only the eventual request left the user looking at a live "Delete file" for something that can no longer happen — on a surface that in that state is supposed to offer no delete at all. Both surfaces now abandon it as a rejection, the same way a subject change already does: the panel's confirm sub-view and the strip's tile-anchored prompt. Plain latches written under untrack, since as $state these effects would depend on what they write. Mutation-tested: neutering the panel's effect fails the new test. |
||
|
|
a40049c214 |
fix(attachments): meet criteria 1, 3 and 5 as written
Final round, scoped to this phase's six acceptance criteria rather than to whatever the diff suggested — the previous rounds had started finding issues in adjacent surfaces, which is the signal that the core had converged and the review was expanding. Three of the six were not actually met: 3. Download returned `undefined` for a nameless row, which drops the attribute entirely and turns Download back into a navigation. The server sends an inline disposition for most types, so the file would have OPENED instead of saving — the precise regression this action exists to prevent, reachable whenever a chip's metadata is partial. The attribute is now always present; empty just lets the browser name the file. 5. "A peeked pane offers no delete" was implemented as a visible, disabled Delete row. The strip hides its delete control outright in the same state, so one object was offering two different affordances for one permission depending on which surface you met it through. Delete is absent now, with `enabled` and `run` still gating behind it. That change surfaced a conflation in the panel: its action context ANDed permission with `missing`, so once the descriptor used it to decide EXISTENCE, a gone row lost Delete while Open and Download stayed present-and-disabled beside it. Permission and reachability are separate questions again — the render site already disables every action while missing. 1. The editor-chip half of "the same panel wherever you meet an attachment" had no end-to-end coverage: the chip's tests mock the bus and the host's inject events directly, so nothing exercised a real NodeView reaching a real host. Covered now by a browser test that drops a text file and clicks the resulting chip. make check exit 0, 745 unit tests, 6/6 e2e locally. |
||
|
|
d16ba34e68 |
fix(attachments): finish the gate and the rollout the last round started
Round 7, run one level down: consistency of the FIXES themselves. P1 — the transform toolbar was outside everything the previous rounds did. Two halves: `mimeToFormat` returns null both for "never asked" and for "asked, and the processor does not handle this", and the gating read the second as the first — so Crop stayed enabled for image/svg+xml and handed the original to the crop modal for a transform the server refuses. A known mime that maps to no supported format is now unsupported, with copy that says which case it is. And a confirmed deletion inertized only the placeholder, leaving rotate and crop live on a row that is gone, where they can only 404 — the same dead-control gap the placeholder's own role/tabindex removal closes. P2 — displayFilename had been applied to the labels I was looking at and not the rest: the strip's lightbox alt and confirmation titles, Storage's visible filename, its image alt, and both surfaces' toasts still rendered a raw name. A partial rollout of a consistency fix is its own inconsistency. Deferred, per the plan's own sequencing: an already-open viewer surviving a deletion is DR-5c, which belongs with the unified viewer in 3a/3c — the strip passes a snapshot list and the inline dialog has no deletion subscription, and fixing either here means building half of that work against a viewer that is about to be replaced. The toolbar's deleted state is tested; the SVG crop gate is covered by the same spec's existing MIME cases. |
||
|
|
2cbce05051 |
fix(attachments): make the surfaces of one object agree
Final review round 6, run on the consistency class the previous round proved productive: enumerate every surface this feature touches and ask where two views of the SAME object now disagree. P1 — the preview-safety gate was applied to the strip only. DR-16 says the exact raster allowlist replaces isImage() at EVERY open-the-viewer decision point, and two were left: the Storage list handed the original to a new tab for anything image-ish, and the inline editor image opened its lightbox the same way. Both are gated now. The inline one is gated on what is POSITIVELY KNOWN, deliberately: that node's MIME comes from a lazy probe, so at click time it is often simply unasked, and refusing on unknown would stop ordinary images opening — a certain regression traded for a marginal risk. A probed non-allowlisted type is refused; an unprobed one keeps today's behaviour. Phase 3a threads mime_type onto the image list itself, which is what turns this into a complete gate. Storage keeps rendering the thumbnail (an <img> either way) and loses only the link that hands over the original. Three consistency P2s, all of them mine: - A nameless attachment was "Untitled file" in the panel and prompts, "attachment" in the chip, and blank in the strip and Storage. One helper now, everywhere. - Storage's delete buttons all announced themselves as just "Delete", making rows indistinguishable to assistive tech, while the strip's name the file. - The panel offered its actions while an ARCHIVED parent's reachability probe was still in flight — actions whose only outcome is a 404. They wait for that specific probe, and only that one: the ordinary open-immediately behaviour (DR-2) is untouched. Deferred with a reason: making loaded inline images keyboard-activatable is DR-12's contract for body images, but it is bound to DR-4a's unified viewer in phase 3a; pulling it forward means shipping half a contract. The placeholder's key handling is fixed here since that code is this phase's. The DR-16 gate is mutation-tested, in both directions. |
||
|
|
95b6e1b3e8 |
fix(attachments): a deleted inline image is inert, like the chip beside it
Final review round 5, asked as "what would you most regret merging". Making the file chip inert on deletion left its sibling behind: the inline image's placeholder kept role=button and tabindex=0 after a CONFIRMED deletion, while `retryLoad` refuses from that point on. A keyboard or screen-reader user got a focus stop that announces itself as a button and does nothing — the exact dead stop DR-12 names, and the one I had just closed on the surface next to it. Two surfaces, one object, and they disagreed. The semantics now follow the cause rather than being set once at construction: a transient failure IS retryable and keeps the button, a confirmed deletion drops role and tabindex and blurs the element first, so focus is not stranded somewhere no further keystroke can reach. Adds the image NodeView's first test file — through a real Tiptap editor, like the chip's, since these are properties of imperative NodeView DOM. All three cases fail without the fix. |
||
|
|
74ccada739 |
fix(attachments): name the nameless, and stop a hung read looking like a wait
Final review round 4, on error and empty paths.
A hung metadata HEAD produced no rejection, so nothing downstream ever
fired: the panel sat on "Reading details…" forever with no Retry, which
to a user is indistinguishable from a hang and is the exact
loading-vs-failed confusion DR-10 exists to prevent. It now calls a read
that has not settled in 10s a failure — without aborting it, so a slow
answer that does arrive still corrects the error state.
A blank filename rendered as a blank tile, an accessible name that said
nothing, and a confirmation reading "Delete ?" — the one place it
matters, because the user is being asked to approve destroying something
the prompt cannot name. One `displayFilename` helper now, used by the
strip, the panel and both delete prompts; the panel's own "Attachment"
fallback is gone, since two words for one nameless thing is how the
surfaces drift.
Declined, with reasons recorded at the sites:
- The body editor snapshots its workspace at mount, which the review read
as staleable. It is not: both <Editor> mounts sit inside `{#key
item.id}`, and a workspace switch necessarily lands on a different
item. Noted in the code, because the safety comes from the key rather
than from anything visible at the snapshot.
- A download 403 has no in-app error path. Download is a real anchor by
DR-16 — the server sends an inline disposition for most types, so a
fetch-and-blob would stop it being a download. The browser's own
response is the honest surface.
Also corrects the `workspaceSlug` option comments in both extensions,
which described behaviour the previous round moved to the address reader.
|
||
|
|
b253a2be6f |
test(attachments): cover the wiring the unit suites structurally cannot
Final review round 3, on the test suite as a deliverable. The panel had no producer-to-host test: the strip's tests mock the event bus, the panel host's tests emit on it directly, and between them a broken hostToken thread through ItemDetail would have passed everything. Verified by breaking that thread — the new browser test fails, the whole unit suite stays green. It is also the only place DR-12's "activates exactly once per key press" can be demonstrated at all: jsdom does not synthesise a button's activation click, so the unit test could only ever prove the narrower "no hand-rolled handler races the UA click". That test is renamed to claim exactly that, with a pointer to where the real one lives. Also folds the workspace into the host-address reader. It was captured once in the Tiptap options while the URL builder stayed live, so a mounted chip surviving a pane workspace switch would probe under the PREVIOUS workspace's key — a cross-workspace answer, cached under the wrong key. Same staleness class as the item id, one axis over. This incidentally makes the image extension's `address` option load-bearing rather than the dead plumbing the review flagged: its probes read the live workspace through it now. isAddressable deliberately takes only the two ROUTING fields — the workspace rides along for cache keying and says nothing about whether an event can find its host. |
||
|
|
850d3559b4 |
fix(attachments): reconcile the panel on deletion, guard the storage delete
Final review round 2, probing what a user can do that the code did not anticipate. - An open panel kept offering Download and Delete for an attachment another surface had just deleted. The strip already reconciles on that broadcast; the panel now does too, and CLOSES rather than latching the missing state — unlike a 404 found while opening, where the user asked about this file and deserves the answer, this is an answer to a question nobody asked, and a tombstone panel would be stranger than dismissing it. - Storage could send two DELETEs for one row. `confirm()` blocked the thread, so a second confirmation could not be raised while a request was in flight; an in-app one can, and unlike the item strip this list does not remove the row optimistically, so it stays clickable throughout. The user's single action produced a success AND an "already deleted". Guarded per id, released in a `finally` so a FAILED delete does not strand the row as permanently undeletable. - Storage's 404 arm claimed parity with its success arm — same broadcast, same refresh — and nothing tested it; the shared descriptor's 404 test covers a different implementation. Now tested. Both guards are mutation-tested. |
||
|
|
7bb9c4acb1 |
fix(attachments): close the final-review findings across the feature
From the orchestrator's full-diff pass over main...HEAD — the altitude per-task reviews structurally cannot reach. - Opening the panel on an ALREADY-archived parent left Open, Download and Copy link enabled against endpoints that 404: the host only handled the archive TRANSITION, and the strip's event carries complete metadata, which is exactly what lets the panel skip its probe. The panel is now told the parent is archived and probes anyway, landing in the authoritative missing state it already knows how to render. It probes through the INVALIDATING path, because reachability is an existence question and the cache can hold an `ok` observed before the archive — the same lesson as the image placeholder earlier on this branch. - A deleted chip stayed inert after its node was repointed at a different attachment: the uuid-swap path cleared `deleted` and the CSS but not `disabled`, giving a chip that announces itself as live and does nothing. Reachable through a collaborative peer's edit. - actions.ts claimed to be "rendered twice". It has one consumer today; the viewer is phase 3a. Says so now, including that the image NodeView's threaded address is held open for the same phase — a list with a single consumer is worth re-justifying if 3a stops coming. - The shared confirmation claimed to own the prompt wording while StorageTab built its own inline. Both builders now live in that module side by side: an item surface can check the body it has and must hedge about the ones it cannot, while a workspace-wide list has nothing to check and should say what happens to the blob instead. - Descriptor `description` was never rendered; it is the row's tooltip now rather than a dead field. Both behavioural fixes are mutation-tested. |
||
|
|
61e2ce1340 | Merge branch 'task/2425-inapp-confirm' into feat/attachment-options-panel | ||
|
|
c4189b1dc6 |
fix(attachments): one in-app delete confirmation, everywhere (TASK-2425)
The strip's hover `×` raised a browser-native `window.confirm` while the options panel — and the rest of the item UI — drilled down to an in-app sub-view. Two confirmation styles for one object is exactly what DR-18 exists to prevent, and the settings Storage tab's row Delete was on a native `confirm()` too. All three now render one shared `AttachmentDeleteConfirm`: prompt as `role="presentation"` carrying an id, `aria-describedby` back-reference from the destructive row, Cancel FIRST so the focus handoff can never land Enter on Delete, destructive row last. It renders rows only — each surface supplies its own `Menu`, so ESC ordering, outside-click, portal placement, focus return and the mobile sheet swap stay the app's existing behaviours rather than a second implementation. Both warning arms carry through verbatim, from one shared builder: the referenced arm and the hedged one, which stays hedged because the check can only ever speak for the item it has. The Storage tab keeps its own wording (the GC grace period) — a reference check has no meaning in a workspace-wide list — but shares the shape. The delete REQUEST paths are untouched: same entry paint fence, same `viewFence.begin()`, same optimistic removal and single-row rollback, same 404-is-authoritative arm, same `announceAttachmentDeleted`. One addition each: `window.confirm` blocked the thread, so the entry fence was still true by definition when it returned — an in-app confirmation does not, so the fence is re-checked where the request is actually sent, and an open confirmation is abandoned when the view changes under it or another surface deletes the row. Also fixes an unhandled rejection the suite surfaced: `Menu` places itself in a `tick().then()` that can run after its block is torn down, so every prop expression reading the pending state needs `?.`. Tests: the ~18 strip tests (and 4 Storage tab tests) that spied on `window.confirm` now drive the real rows; every message-arm, fence and rollback assertion is preserved. New coverage for the confirmation's shape, Cancel's focus return, the confirm-time fence, and abandonment on switch / external delete. The e2e strip spec drops its `dialog` handler and pins the 24×24 target size in a real browser. Claude-Session: https://claude.ai/code/session_01LmbFxQFDjcYKBLcTnor6DC |
||
|
|
74589e98bb |
fix(attachments): make the live chip a button, not a link (TASK-2424)
From the orchestrator's pass on TASK-2424. The chip opens the options panel — it does not navigate — but it was still an <a href download> that only intercepted primary click. Middle-click and aux-click therefore opened or downloaded the file straight past the panel, which is the exact accidental download the panel exists to prevent, and screen readers announced a link for a control that opens a menu. A button has no URL to activate, so the bypass cannot exist rather than being intercepted, and it matches the strip's file tile — the same control, now with the same semantics. renderHTML stays an <a download>: that is the clipboard / read-only shape, where a link is honest and there is no panel to open. Deleted chips now use `disabled` rather than a dropped href: a disabled button receives no click or keydown at all, where the anchor's handlers still ran and had to swallow events. It is also blurred explicitly first, since a chip the user is focused on when another surface deletes the row would otherwise strand focus on an inert element. The keydown suppression moved ahead of the deleted bail so a stray Enter can never reach ProseMirror's keymap and split the paragraph the chip sits in. .file-chip carries the button reset explicitly (font: inherit), since the UA's 13.33px Arial would otherwise shrink the live chip away from the read-only one. |
||
|
|
3d45418c52 | Merge branch 'task/2424-panel-routing' into feat/attachment-options-panel | ||
|
|
749abbf6e4 |
fix(attachments): fence the panel's teardown and its deferred close
Three from the orchestrator's independent pass on the panel, all the same shape as the two the panel's own tests caught: something the panel started keeps running after the host has moved on. - The host destroys the panel by nulling its request, but a delete already in flight still called onclose() when it resolved — closing whatever panel was open BY THEN. Attachment A's delete could dismiss the panel the user had just opened on B. Fixed at the boundary: the host's close handler is bound to the request it was rendered for and ignores a call from any other. The panel also invalidates its fences on destroy, so the continuation reads stale rather than merely being ignored, and resolves a confirmation still on screen — an unresolved one strands the descriptor's await forever. - The deferred close after an anchor navigation (deliberately a macrotask, so the download is not cancelled) was unfenced: reopening on another attachment before the timer fired let the old timer close the new panel. - A forced revalidation that came back transient left the authoritative 'missing' latched, and the missing branch suppresses Retry — 'no longer available' with no way to ask again. Reachable exactly when a panel is opened on an already-archived parent and the item is then restored, which is the case DR-14 added the revalidation for. The bound close handler is mutation-tested: unbinding it fails the item-switch test. |
||
|
|
1a42a6df44 |
feat(attachments): open the options panel from tiles and chips (TASK-2424)
The behaviour change users actually see (PLAN-2392 DR-1 / DR-12 / DR-16).
A non-image strip tile stops being an `<a download>` — one tap and it was
in your Downloads folder, with no way to see what a file IS first. It is
now a real `<button>` that emits the open-panel event with the tile as the
anchor and all three metadata fields (the strip always has them from its
list row). The whole tile is the trigger and no `⋯` affordance is added,
per DR-1; a native button is what guarantees DR-12's "Enter and Space
both activate, exactly once" — the UA converts both to a single click and
already eats Space's page scroll, so there is no hand-rolled keydown
handler racing the click one.
The live editor chip opens the SAME panel instead of `window.open`ing the
download URL, stamped from the NodeView's address reader at emit time.
Its keyboard contract is hand-rolled because it lives inside a
ProseMirror editable region: an un-suppressed Enter bubbles to the
editor's split-block keymap, which preventDefaults it and thereby cancels
the anchor's own activation click. So Enter and Space are both handled on
keydown and both cancelled — a cancelled keydown produces no activation
click, which is what keeps the count at one. A chip in the deleted state
stays inert AND unfocusable (no href, and no tabindex is ever set) rather
than being a focus stop that does nothing.
Both surfaces' accessible names now carry filename, type and the ACTION
("Options for spec.pdf, PDF, 1.5 KB") — the only signpost for the changed
behaviour, since DR-1 deliberately adds no visible one.
Image tiles and `lightboxImages` gate on `canOpenInViewer`, not `isImage`
(DR-16): an `image/svg+xml` row renders as a FILE tile, opens the panel,
and is not a member of the viewer's set, because SVG can carry active
content and a legacy / mislabelled / sniffed row can arrive wearing an
`image/*` label.
Folded in: `.att-tile` gains `font: inherit`. The file tile is the one
with text in it, and as a button the UA font (13.33px Arial) would have
replaced the inherited one — `.att-name`'s 0.6em measured against it is a
visibly smaller, differently-faced filename than the anchor rendered.
The strip test that pinned the tile as an `<a href download>` is
deliberately falsified and replaced. New coverage: the emitted payload,
single activation per key on both surfaces, the SVG file-tile path, and a
Tiptap-driven chip suite (emit-time addressing, click suppression, the
dead-chip contract).
Claude-Session: https://claude.ai/code/session_01LmbFxQFDjcYKBLcTnor6DC
|
||
|
|
01e2bc3b5d | Merge branch 'task/2423-details-panel' into feat/attachment-options-panel | ||
|
|
b8909befdf |
feat(attachments): an options panel for files (TASK-2423)
Tapping a file used to do the most destructive-adjacent thing available: a strip tile was a bare `<a download>`, so one tap put the file in your Downloads folder with no way to see what it was first. This is what a tap opens instead — `AttachmentDetailsPanel`, plus the one host that owns it. PLAN-2392 phase 2, wave B. Nothing routes into the panel yet; the strip's tiles and the editor's chips start emitting the open event in TASK-2424, so the panel is driven here through its host and the events bus. Presentation is the existing `Menu` with `sheetOnMobile` — a popover on desktop, a BottomSheet at the mobile breakpoint (DR-6). No new overlay primitive, so ESC ordering, outside-click, placement and the sheet's focus handling are the app's existing ones rather than second implementations. The actions are NOT defined here: they are rendered from the shared descriptor list (DR-5), choosing between MenuItem's anchor and button branches on the descriptor's own `element` discriminant and never calling `run()` on an anchor. Adding an action stays a one-descriptor change. It opens IMMEDIATELY and completes the metadata after (DR-2, DR-10). The event's filename / mime / size are nullable by contract, so the panel paints what it was handed and fetches the rest itself: `ok` fills the gaps, `missing` (404) latches an authoritative "no longer available" with every action inert, and `transient` shows an inline error beside the row it already knows, with a Retry that goes through `revalidateAttachmentMetadata` — a plain refetch would replay the cached failure and look broken. Delete is an in-app drill-down sub-view (DR-18), the item menu's shape exactly: prompt as `role="presentation"` with an aria-describedby back-reference, Cancel FIRST, destructive row last, and the strip's contextual "still used in this item's content" warning carried through (read at confirm time from the LIVE editor markdown, since the persisted body lags). It is wired as the delete descriptor's `confirmDelete` promise rather than as a bespoke path, so the descriptor's identity snapshot and permission re-check across the confirmation stay in force. The unreferenced arm stays hedged: this can only speak for the HOST's content, and the event's `itemId` is routing, not ownership. The host is `ItemDetail`, through a small `AttachmentPanelHost` it mounts beside the strip. It consumes an event only when BOTH `itemId` and `hostToken` are its own (DR-8), and supplies `mutationsEnabled` itself — never the NodeView's (it has no mutation context) and never the timeline's `canEdit` (which ignores `peeking` and would let a peeked pane mutate). The host is a component rather than a block inside ItemDetail because the addressing rule has to be testable with two hosts mounted at once, which is what the pane host does at runtime. Parent lifecycle (DR-14): an archived parent's attachment fetch returns a generic 404, so archive CLOSES the panel and restore REVALIDATES it rather than assuming the previous state holds. The strip sits outside ItemDetail's keyed lifecycle block, so this is added, not inherited; it arrives declaratively as `parentArchived`, following the item ItemDetail already refetches on the SSE lifecycle events. Long filenames and RTL are handled with logical properties throughout, `min-width: 0` on every flex child holding the name, and the full unelided filename in both `title` and the panel's accessible name (DR-13). No `state_generation` and no Undo (DR-19) — Delete behaves exactly like today's tile Delete; PLAN-2411 adds the generation token and the Undo toast to all three entry points at once. Also here: - `describeAttachmentType` in the shared display helpers, built on `iconForAttachment` so the words and the icon beside them cannot disagree about what a file is. - `liveEditorMarkdown` extracted in ItemDetail — the strip and the panel now read the live body through one accessor instead of two copies. Tested through the host (20 jsdom cases): addressing with two hosts mounted, open-with-partial-then-complete, all three metadata arms, Retry's invalidate-before-refetch, host-supplied permission for peeked vs master, the full confirm/cancel/failure delete paths, both warning arms, archive- closes / restore-revalidates, item switch, and re-targeting in place. Focus entry and return, background inertness, real placement, the sheet swap and Enter/Space activation are browser-only and belong to phase 3d. |
||
|
|
2501046777 |
fix(attachments): fence transform failures and state what itemId means
Two from the orchestrator's security/Svelte-angle Codex pass. A rotate or crop that FAILS was unfenced where its success path was fenced, so an error from work on attachment A could alert the user after they had already switched to B. Same guard, both catches. The other finding read the event's itemId as an authorization claim and concluded a stale chip in a reused composer could delete an attachment belonging to another item. Checked rather than assumed: the delete endpoint is never told which item the client thought it was acting from, and the server authorizes per attachment against the parent it resolves itself — visibility first, then edit permission (handlers_storage.go). So it is routing, not ownership, and the two legitimately differ. Declined as written, but the confusion is the finding: itemId now says what it is at the point of definition, including the one place the distinction leaks into UX — a panel can only speak for the host's own content, so its in-use warning has to stay hedged. |
||
|
|
ebe531ebaa |
fix(attachments): make the host address a reader, not written-in options
The addressing fix in the previous commit could not have worked. Tiptap's `options` is a getter returning a fresh spread on every access (@tiptap/core 3.22.5, dist/index.cjs:3452), so `ext.options.itemId = next` mutates a temporary and is discarded — an assignment that looks exactly like working code. So the address stops being two strings pushed in after the fact and becomes a reader the host supplies once and keeps honest: a closure over its own live props, called at emit time. That is correct for a host that is remounted per item (the body editor) and one that is reused across an item switch (the comment composer) without either knowing which it is. New $lib/attachments/hostAddress.ts states the contract; its test pins the dependency behaviour that forces it, so a future Tiptap bump that makes options writable fails a test instead of quietly inviting the mutation approach back. The event predicate now reuses isAddressable so 'both halves required' is stated once. Also completes the NodeView teardown fence (both MIME probe continuations and swapNodeUuid could run after destroy) and adds the stable-async-confirm and ordering cases the delete tests were missing — without them, a regression dropping every async-confirmed delete passed. The same dependency trap makes the editor's existing capabilities push a no-op; that is pre-existing and independent, filed as BUG-2426. All three from the orchestrator's second fresh-angle Codex pass. |
||
|
|
042bd7e477 |
fix(attachments): close four review findings across the wave-A surfaces
From the orchestrator's fresh-angle Codex pass. All four are the same
shape — something read after an await, or captured once and never
refreshed — on a component tree built around a no-{#key} item switch.
- The delete descriptor snapshotted identity AFTER its confirmation, so
an async in-app confirm (which is what DR-18 asks for) left a window
where the user could switch items and delete the attachment they were
no longer looking at. Snapshot first, re-check the gate and the
identity on the way out.
- MenuItem's anchor rows had no Space activation. Native anchors take
Enter only, and role=menuitem does not add it, so Space would do
nothing on Download and Open while working on every button row beside
them.
- The image NodeView had no destroyed flag, so a HEAD probe in flight at
teardown could latch a placeholder onto detached DOM. The chip NodeView
has always had one.
- CommentEditor configures its extensions once in onMount, but the
composer is deliberately reused across an item switch, so its chips
kept emitting events addressed to the PREVIOUS item — which the host
then correctly ignored, i.e. a tap that silently did nothing. Push the
addressing onto the live options, the same way capabilities are pushed.
The first two fixes are mutation-tested: reverting either fails the new
test.
|