diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index e39a0521..319def1c 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -27,6 +27,9 @@ jobs: - name: install frontend dependencies run: npm ci + - name: audit complete dependency lockfile + run: npm audit + - name: check frontend run: npm run check diff --git a/docs/superpowers/plans/2026-07-31-toc-toggle-surface.md b/docs/superpowers/plans/2026-07-31-toc-toggle-surface.md new file mode 100644 index 00000000..82fe882b --- /dev/null +++ b/docs/superpowers/plans/2026-07-31-toc-toggle-surface.md @@ -0,0 +1,107 @@ +# Floating Table-of-Contents Toggle Surface Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** Make the floating table-of-contents toggle a compact translucent square that is distinguishable from adjacent toolbar icons in all modes. + +**Architecture:** Keep behavior in `MarkdownViewer.svelte` unchanged. Consolidate the existing edit-mode-only card declarations into the base `.toc-toggle-floating` selector, then assert the visual contract with the established source-level Node test suite. + +**Tech Stack:** Svelte 5, scoped component CSS, TypeScript Node test runner. + +## Global Constraints + +- Keep the 28 by 28 pixel target, placement, expanded offsets, icon transforms, tooltip, click handler, and print exclusion unchanged. +- Use existing color tokens; add no dependency, component, preference, or text label. +- The base selector must provide the translucent surface, border, subtle shadow, standard/WebKit backdrop filtering, hover contrast, active feedback, and visible keyboard focus. +- The edit-mode-only card rule must be removed after the base selector subsumes it. +- Verify with `npm run check`, `npm test`, and an isolated macOS bundle visual smoke test. + +--- + +### Task 1: Unify the floating TOC toggle surface + +**Files:** +- Modify: `src/lib/MarkdownViewer.svelte:4033-4146` +- Modify: `scripts/issue261EditorPdf.test.ts:1-32` + +**Interfaces:** +- Consumes: the existing `.toc-toggle-floating`, `.expanded`, `.on-right`, and `.in-edit-mode` class names rendered by `MarkdownViewer.svelte`. +- Produces: a mode-independent visual contract owned by `.toc-toggle-floating`; no TypeScript API or event contract changes. + +- [ ] **Step 1: Write the failing test** + +Add a `viewer` source string beside the existing stylesheet fixture, then append this test: + +```ts +test('floating toc toggle keeps a visible translucent surface outside edit mode', () => { + const selector = viewer.slice( + viewer.indexOf('\t.toc-toggle-floating {'), + viewer.indexOf('\n\t.toc-toggle-floating.expanded {'), + ); + + assert.match(selector, /background-color:\s*color-mix\(in srgb, var\(--color-canvas-default\) 82%, transparent\);/); + assert.match(selector, /border:\s*1px solid var\(--color-border-default\);/); + assert.match(selector, /box-shadow:\s*0 2px 8px rgba\(0, 0, 0, 0\.12\);/); + assert.match(selector, /backdrop-filter:\s*blur\(8px\);/); + assert.match(viewer, /\.toc-toggle-floating:focus-visible\s*\{[\s\S]*?outline:\s*2px solid var\(--color-accent-fg\);/); + assert.doesNotMatch(viewer, /\.toc-toggle-floating\.in-edit-mode:not\(\.expanded\)/); +}); +``` + +- [ ] **Step 2: Run the test to verify it fails** + +Run: + +```bash +npm test -- --test-name-pattern='floating toc toggle keeps a visible translucent surface' +``` + +Expected: FAIL because the existing base selector has `background-color: transparent`, no border/shadow/blur, no focus-visible rule, and retains the edit-only selector. + +- [ ] **Step 3: Write the minimal implementation** + +In `.toc-toggle-floating`, replace the transparent/no-border declarations and extend the transition list: + +```css +background-color: color-mix(in srgb, var(--color-canvas-default) 82%, transparent); +border: 1px solid var(--color-border-default); +box-shadow: 0 2px 8px rgba(0, 0, 0, 0.12); +backdrop-filter: blur(8px); +-webkit-backdrop-filter: blur(8px); +``` + +Keep `opacity: 0.6` and add `border-color` and `box-shadow` to the transition list. Update hover to set `color: var(--color-fg-default)` and a slightly more opaque `color-mix` background. Add: + +```css +.toc-toggle-floating:focus-visible { + outline: 2px solid var(--color-accent-fg); + outline-offset: 2px; + opacity: 1; +} +``` + +Delete `.toc-toggle-floating.in-edit-mode:not(.expanded)` because the base selector now owns all of its surface declarations. + +- [ ] **Step 4: Run source checks** + +Run: + +```bash +npm run check +npm test +``` + +Expected: Svelte check reports zero errors and warnings; all Node tests pass. + +- [ ] **Step 5: Build and visually smoke-test the isolated bundle** + +Create a temporary Tauri config with product name `Markpad PR Test` and identifier `com.alecdotdev.markpad.prtest`, build the release bundle, then inspect it without launching `/Applications/Markpad.app`. + +Test the collapsed TOC toggle in preview-only mode and edit mode. Confirm that it is a 28-pixel translucent square with visible border/shadow, opens and closes the TOC, preserves its side-specific icon direction, and shows a focus ring via keyboard navigation. Remove the temporary config, fixture, output files, and test app data after verification. + +- [ ] **Step 6: Commit** + +```bash +git add src/lib/MarkdownViewer.svelte scripts/issue261EditorPdf.test.ts +git commit -m 'fix(ui): surface floating toc toggle' +``` diff --git a/docs/superpowers/specs/2026-07-31-toc-toggle-surface-design.md b/docs/superpowers/specs/2026-07-31-toc-toggle-surface-design.md new file mode 100644 index 00000000..e6b51037 --- /dev/null +++ b/docs/superpowers/specs/2026-07-31-toc-toggle-surface-design.md @@ -0,0 +1,35 @@ +# Floating table-of-contents toggle surface + +## Goal + +Make the floating table-of-contents toggle visually distinct from nearby toolbar icons without changing its behavior, dimensions, placement, animation, or translated accessible label. + +## Scope + +The change is limited to `.toc-toggle-floating` in `src/lib/MarkdownViewer.svelte`. + +- Keep the existing 28 by 28 pixel hit target, left/right placement, expanded offsets, icon rotation, tooltip, click handler, and print exclusion. +- Give the button a translucent canvas background, one-pixel border, subtle shadow, and backdrop blur in every viewing mode. +- Increase surface and foreground contrast on hover and keyboard focus. +- Keep active feedback; it must use the existing muted-border token rather than a new color token. +- Remove the edit-mode-only card rule after its declarations are absorbed by the base selector. + +## Non-goals + +- No text label, persistent preference, layout movement, icon replacement, or table-of-contents behavior change. +- No changes to the table-of-contents panel, resize handle, title-bar menu, or print layout. +- No new dependency or component. + +## Styling contract + +The base selector owns the complete surface. It uses an alpha-adjusted canvas color, existing border and foreground tokens, a small shadow, and both standard and WebKit backdrop filtering. It has a visible `:focus-visible` outline using the existing accent token. Hover increases opacity and foreground contrast without changing geometry. The existing `.expanded`, `.on-right`, and icon transforms remain separate. + +## Verification + +1. Add a source-level regression test asserting that the base selector contains the translucent surface, border, shadow, backdrop filtering, and focus-visible state. +2. Run `npm run check` and `npm test`. +3. Build an isolated macOS bundle and confirm visually that the collapsed control is a compact translucent square in preview-only and edit modes, remains reachable and understandable on both sides, and keeps its expanded/close animation. + +## Compatibility + +`backdrop-filter` is progressive enhancement. The explicit translucent background, border, and shadow preserve contrast where it is unavailable. The rule remains inside the component style block and does not affect the app toolbar or the print CSS. diff --git a/package-lock.json b/package-lock.json index 7a59f0ac..1399fabc 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "markpad", - "version": "2.6.13", + "version": "2.6.14", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "markpad", - "version": "2.6.13", + "version": "2.6.14", "license": "MIT", "dependencies": { "@tauri-apps/api": "^2", @@ -16,26 +16,26 @@ "@tauri-apps/plugin-process": "^2.3.1", "@tauri-apps/plugin-updater": "^2.10.1", "@types/dompurify": "^3.0.5", - "dompurify": "^3.3.1", + "dompurify": "3.4.12", "highlight.js": "^11.11.1", "highlightjs-svelte": "^1.0.6", "katex": "^0.16.27", - "mermaid": "^11.12.2", + "mermaid": "^11.16.0", "monaco-editor": "^0.55.1", "monaco-vim": "^0.4.4", "node-stream-zip": "^1.15.0", "yaml": "^2.9.0" }, "devDependencies": { - "@sveltejs/adapter-static": "^3.0.6", - "@sveltejs/kit": "^2.9.0", - "@sveltejs/vite-plugin-svelte": "^5.0.0", + "@sveltejs/adapter-static": "^3.0.10", + "@sveltejs/kit": "^2.70.2", + "@sveltejs/vite-plugin-svelte": "^5.1.1", "@tauri-apps/cli": "^2", - "svelte": "^5.0.0", - "svelte-check": "^4.0.0", + "svelte": "^5.56.8", + "svelte-check": "^4.7.4", "tsx": "^4.22.4", "typescript": "~5.6.2", - "vite": "^6.0.3" + "vite": "^6.4.3" } }, "node_modules/@antfu/install-pkg": { @@ -57,55 +57,10 @@ "integrity": "sha512-jigsZK+sMF/cuiB7sERuo9V7N9jx+dhmHHnQyDSVdpZwVutaBu7WvNYqMDLSgFgfB30n452TP3vjDAvFC973mA==", "license": "MIT" }, - "node_modules/@chevrotain/cst-dts-gen": { - "version": "11.0.3", - "resolved": "https://registry.npmjs.org/@chevrotain/cst-dts-gen/-/cst-dts-gen-11.0.3.tgz", - "integrity": "sha512-BvIKpRLeS/8UbfxXxgC33xOumsacaeCKAjAeLyOn7Pcp95HiRbrpl14S+9vaZLolnbssPIUuiUd8IvgkRyt6NQ==", - "license": "Apache-2.0", - "dependencies": { - "@chevrotain/gast": "11.0.3", - "@chevrotain/types": "11.0.3", - "lodash-es": "4.17.21" - } - }, - "node_modules/@chevrotain/cst-dts-gen/node_modules/lodash-es": { - "version": "4.17.21", - "resolved": "https://registry.npmjs.org/lodash-es/-/lodash-es-4.17.21.tgz", - "integrity": "sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw==", - "license": "MIT" - }, - "node_modules/@chevrotain/gast": { - "version": "11.0.3", - "resolved": "https://registry.npmjs.org/@chevrotain/gast/-/gast-11.0.3.tgz", - "integrity": "sha512-+qNfcoNk70PyS/uxmj3li5NiECO+2YKZZQMbmjTqRI3Qchu8Hig/Q9vgkHpI3alNjr7M+a2St5pw5w5F6NL5/Q==", - "license": "Apache-2.0", - "dependencies": { - "@chevrotain/types": "11.0.3", - "lodash-es": "4.17.21" - } - }, - "node_modules/@chevrotain/gast/node_modules/lodash-es": { - "version": "4.17.21", - "resolved": "https://registry.npmjs.org/lodash-es/-/lodash-es-4.17.21.tgz", - "integrity": "sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw==", - "license": "MIT" - }, - "node_modules/@chevrotain/regexp-to-ast": { - "version": "11.0.3", - "resolved": "https://registry.npmjs.org/@chevrotain/regexp-to-ast/-/regexp-to-ast-11.0.3.tgz", - "integrity": "sha512-1fMHaBZxLFvWI067AVbGJav1eRY7N8DDvYCTwGBiE/ytKBgP8azTdgyrKyWZ9Mfh09eHWb5PgTSO8wi7U824RA==", - "license": "Apache-2.0" - }, "node_modules/@chevrotain/types": { - "version": "11.0.3", - "resolved": "https://registry.npmjs.org/@chevrotain/types/-/types-11.0.3.tgz", - "integrity": "sha512-gsiM3G8b58kZC2HaWR50gu6Y1440cHiJ+i3JUvcp/35JchYejb2+5MVeJK0iKThYpAa/P2PYFV4hoi44HD+aHQ==", - "license": "Apache-2.0" - }, - "node_modules/@chevrotain/utils": { - "version": "11.0.3", - "resolved": "https://registry.npmjs.org/@chevrotain/utils/-/utils-11.0.3.tgz", - "integrity": "sha512-YslZMgtJUyuMbZ+aKvfF3x1f5liK4mWNxghFRv7jqRR9C3R3fAOGTTKvxXDa2Y1s9zSbcpuO0cAxDYsc9SrXoQ==", + "version": "11.1.2", + "resolved": "https://registry.npmjs.org/@chevrotain/types/-/types-11.1.2.tgz", + "integrity": "sha512-U+HFai5+zmJCkK86QsaJtoITlboZHBqrVketcO2ROv865xfCMSFpELQoz1GkX5GzME8pTa+3kbKrZHQtI0gdbw==", "license": "Apache-2.0" }, "node_modules/@esbuild/aix-ppc64": { @@ -618,12 +573,12 @@ } }, "node_modules/@mermaid-js/parser": { - "version": "0.6.3", - "resolved": "https://registry.npmjs.org/@mermaid-js/parser/-/parser-0.6.3.tgz", - "integrity": "sha512-lnjOhe7zyHjc+If7yT4zoedx2vo4sHaTmtkl1+or8BRTnCtDmcTpAjpzDSfCZrshM5bCoz0GyidzadJAH1xobA==", + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@mermaid-js/parser/-/parser-1.2.0.tgz", + "integrity": "sha512-oYPyv8A4As1yH5Bx+04iQEQxXuIQDe0GKCNSRgao6z8AM9jixXIfP0vsppRLvGf+nKIOb9/LdpWA4YuJiVvESA==", "license": "MIT", "dependencies": { - "langium": "3.3.1" + "@chevrotain/types": "~11.1.2" } }, "node_modules/@polka/url": { @@ -634,9 +589,9 @@ "license": "MIT" }, "node_modules/@rollup/rollup-android-arm-eabi": { - "version": "4.55.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.55.1.tgz", - "integrity": "sha512-9R0DM/ykwfGIlNu6+2U09ga0WXeZ9MRC2Ter8jnz8415VbuIykVuc6bhdrbORFZANDmTDvq26mJrEVTl8TdnDg==", + "version": "4.62.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.62.3.tgz", + "integrity": "sha512-c0wdcekXtQvvn5Tsrk/+op/gUArrbWaFduBnTLP2l1cKLSQs4diMWjJw3m6A0DdzT8dAAX95KpkJ3qynCePbmw==", "cpu": [ "arm" ], @@ -648,9 +603,9 @@ ] }, "node_modules/@rollup/rollup-android-arm64": { - "version": "4.55.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.55.1.tgz", - "integrity": "sha512-eFZCb1YUqhTysgW3sj/55du5cG57S7UTNtdMjCW7LwVcj3dTTcowCsC8p7uBdzKsZYa8J7IDE8lhMI+HX1vQvg==", + "version": "4.62.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.62.3.tgz", + "integrity": "sha512-3YjElDdWN+qXAFbJ/CzPV+0wspLqh54k/I6GfdYtEJRqg7buSgc1yPM3B+93j1M4neobtkATHZTmxK2AMVGfnA==", "cpu": [ "arm64" ], @@ -662,9 +617,9 @@ ] }, "node_modules/@rollup/rollup-darwin-arm64": { - "version": "4.55.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.55.1.tgz", - "integrity": "sha512-p3grE2PHcQm2e8PSGZdzIhCKbMCw/xi9XvMPErPhwO17vxtvCN5FEA2mSLgmKlCjHGMQTP6phuQTYWUnKewwGg==", + "version": "4.62.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.62.3.tgz", + "integrity": "sha512-Pch2pFNOxxz1hTjypIdPyRTR6riiwRl84+VcN9djS680fw+Co1nAJINrdpqp7KV0NvyuU8ilZXZCjd7ykJl1GQ==", "cpu": [ "arm64" ], @@ -676,9 +631,9 @@ ] }, "node_modules/@rollup/rollup-darwin-x64": { - "version": "4.55.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.55.1.tgz", - "integrity": "sha512-rDUjG25C9qoTm+e02Esi+aqTKSBYwVTaoS1wxcN47/Luqef57Vgp96xNANwt5npq9GDxsH7kXxNkJVEsWEOEaQ==", + "version": "4.62.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.62.3.tgz", + "integrity": "sha512-LEuncFUHFiF8t4yZVZvvZA1wk0pjAscRnsrn1EfTEmN4HXotBi2YtcnLRyaK6UbuczW7xZS5ES+81Rdz8Z0T6g==", "cpu": [ "x64" ], @@ -690,9 +645,9 @@ ] }, "node_modules/@rollup/rollup-freebsd-arm64": { - "version": "4.55.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.55.1.tgz", - "integrity": "sha512-+JiU7Jbp5cdxekIgdte0jfcu5oqw4GCKr6i3PJTlXTCU5H5Fvtkpbs4XJHRmWNXF+hKmn4v7ogI5OQPaupJgOg==", + "version": "4.62.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.62.3.tgz", + "integrity": "sha512-zvBUvsQUpOWALdDsk6qbS8bXf2VxmPisuudNDrY7x0p0jBdsoZl8HsHczIOgkQiZldmcacMKtBzpoGVNeIe2bQ==", "cpu": [ "arm64" ], @@ -704,9 +659,9 @@ ] }, "node_modules/@rollup/rollup-freebsd-x64": { - "version": "4.55.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.55.1.tgz", - "integrity": "sha512-V5xC1tOVWtLLmr3YUk2f6EJK4qksksOYiz/TCsFHu/R+woubcLWdC9nZQmwjOAbmExBIVKsm1/wKmEy4z4u4Bw==", + "version": "4.62.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.62.3.tgz", + "integrity": "sha512-C2KmNrcSem/AMg984H/dev+si0lieQGdXdR/lYGJnuumXnFb9Y7QdiI62obFdLlxRYLBv4P0eUVIDbD4c1vVvw==", "cpu": [ "x64" ], @@ -718,13 +673,16 @@ ] }, "node_modules/@rollup/rollup-linux-arm-gnueabihf": { - "version": "4.55.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.55.1.tgz", - "integrity": "sha512-Rn3n+FUk2J5VWx+ywrG/HGPTD9jXNbicRtTM11e/uorplArnXZYsVifnPPqNNP5BsO3roI4n8332ukpY/zN7rQ==", + "version": "4.62.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.62.3.tgz", + "integrity": "sha512-ggXnsTAEzNQx74XpunRsiZ9aBZDsI7XIa0hm2nzR9f4WzH5/f/d73ZSDaC5ejJ8YLY4NW+V3wr0tjOaeCq8hqA==", "cpu": [ "arm" ], "dev": true, + "libc": [ + "glibc" + ], "license": "MIT", "optional": true, "os": [ @@ -732,13 +690,16 @@ ] }, "node_modules/@rollup/rollup-linux-arm-musleabihf": { - "version": "4.55.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.55.1.tgz", - "integrity": "sha512-grPNWydeKtc1aEdrJDWk4opD7nFtQbMmV7769hiAaYyUKCT1faPRm2av8CX1YJsZ4TLAZcg9gTR1KvEzoLjXkg==", + "version": "4.62.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.62.3.tgz", + "integrity": "sha512-2vng+FlzNUhKZxtej3IUqJgbZoQk2M/dwQM20+ULV0R/E/8tr9/P6uEf2iiGIk4HL0zMKh5Jry7mUHdUOvyGgA==", "cpu": [ "arm" ], "dev": true, + "libc": [ + "musl" + ], "license": "MIT", "optional": true, "os": [ @@ -746,13 +707,16 @@ ] }, "node_modules/@rollup/rollup-linux-arm64-gnu": { - "version": "4.55.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.55.1.tgz", - "integrity": "sha512-a59mwd1k6x8tXKcUxSyISiquLwB5pX+fJW9TkWU46lCqD/GRDe9uDN31jrMmVP3feI3mhAdvcCClhV8V5MhJFQ==", + "version": "4.62.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.62.3.tgz", + "integrity": "sha512-LLLFZKt4/Nraf9rxDkhiU8QVgLF4WmCkfr0L4fj0fPfIZFBib0DeiFk1hhaYKd03LFAFJcxHslhDFlNJLylf5Q==", "cpu": [ "arm64" ], "dev": true, + "libc": [ + "glibc" + ], "license": "MIT", "optional": true, "os": [ @@ -760,13 +724,16 @@ ] }, "node_modules/@rollup/rollup-linux-arm64-musl": { - "version": "4.55.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.55.1.tgz", - "integrity": "sha512-puS1MEgWX5GsHSoiAsF0TYrpomdvkaXm0CofIMG5uVkP6IBV+ZO9xhC5YEN49nsgYo1DuuMquF9+7EDBVYu4uA==", + "version": "4.62.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.62.3.tgz", + "integrity": "sha512-WJkdQCvS9sWNOUBJZfQRKpZGFBztRzcowI+nndmflKgU4XY+3a420FgTOSKTsVqJbnzSxeT4vaJalpOaPo2YCQ==", "cpu": [ "arm64" ], "dev": true, + "libc": [ + "musl" + ], "license": "MIT", "optional": true, "os": [ @@ -774,13 +741,16 @@ ] }, "node_modules/@rollup/rollup-linux-loong64-gnu": { - "version": "4.55.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.55.1.tgz", - "integrity": "sha512-r3Wv40in+lTsULSb6nnoudVbARdOwb2u5fpeoOAZjFLznp6tDU8kd+GTHmJoqZ9lt6/Sys33KdIHUaQihFcu7g==", + "version": "4.62.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.62.3.tgz", + "integrity": "sha512-PwHXCCS2n64/1Ot6rP1YEYA02MGYBcQlr8CSZZyrUG2O7NH6NklYmvr9v3Jy+5e/eDeNchc/ukmKJi9LuflMIQ==", "cpu": [ "loong64" ], "dev": true, + "libc": [ + "glibc" + ], "license": "MIT", "optional": true, "os": [ @@ -788,13 +758,16 @@ ] }, "node_modules/@rollup/rollup-linux-loong64-musl": { - "version": "4.55.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.55.1.tgz", - "integrity": "sha512-MR8c0+UxAlB22Fq4R+aQSPBayvYa3+9DrwG/i1TKQXFYEaoW3B5b/rkSRIypcZDdWjWnpcvxbNaAJDcSbJU3Lw==", + "version": "4.62.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.62.3.tgz", + "integrity": "sha512-vUjxINQu3RC8NZS3ykk1gN65gIz8pAopOq2HXuZhiIxHdx7TFvDG+jgrdSgInu1Eza4/Rfi2VzZgyIgEH4WOaw==", "cpu": [ "loong64" ], "dev": true, + "libc": [ + "musl" + ], "license": "MIT", "optional": true, "os": [ @@ -802,13 +775,16 @@ ] }, "node_modules/@rollup/rollup-linux-ppc64-gnu": { - "version": "4.55.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.55.1.tgz", - "integrity": "sha512-3KhoECe1BRlSYpMTeVrD4sh2Pw2xgt4jzNSZIIPLFEsnQn9gAnZagW9+VqDqAHgm1Xc77LzJOo2LdigS5qZ+gw==", + "version": "4.62.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.62.3.tgz", + "integrity": "sha512-wzko4aJ13+0G3kGnviCg5gnXFKd40izKsrf2uOw12US4XqprkDrmwOpeW14aSNa37V8bfPcz5Fkob6LZ3BAPmA==", "cpu": [ "ppc64" ], "dev": true, + "libc": [ + "glibc" + ], "license": "MIT", "optional": true, "os": [ @@ -816,13 +792,16 @@ ] }, "node_modules/@rollup/rollup-linux-ppc64-musl": { - "version": "4.55.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.55.1.tgz", - "integrity": "sha512-ziR1OuZx0vdYZZ30vueNZTg73alF59DicYrPViG0NEgDVN8/Jl87zkAPu4u6VjZST2llgEUjaiNl9JM6HH1Vdw==", + "version": "4.62.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.62.3.tgz", + "integrity": "sha512-8120ue0JUMSwy11stlwnfdX3pPd+WZYGCDBwEHWtIHi6pOpZmsEF5QKB7a/UN+XFdqvobxz98kv8RTqikyCEBw==", "cpu": [ "ppc64" ], "dev": true, + "libc": [ + "musl" + ], "license": "MIT", "optional": true, "os": [ @@ -830,13 +809,16 @@ ] }, "node_modules/@rollup/rollup-linux-riscv64-gnu": { - "version": "4.55.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.55.1.tgz", - "integrity": "sha512-uW0Y12ih2XJRERZ4jAfKamTyIHVMPQnTZcQjme2HMVDAHY4amf5u414OqNYC+x+LzRdRcnIG1YodLrrtA8xsxw==", + "version": "4.62.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.62.3.tgz", + "integrity": "sha512-XLFHnR3tXMjbOCh2vtVJHmxt+995uJsTERQyseFDRA0xxMxyTZPLa3OIUlyFaO4mF/Lu0FjmWHCuPXJT1n/IOg==", "cpu": [ "riscv64" ], "dev": true, + "libc": [ + "glibc" + ], "license": "MIT", "optional": true, "os": [ @@ -844,13 +826,16 @@ ] }, "node_modules/@rollup/rollup-linux-riscv64-musl": { - "version": "4.55.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.55.1.tgz", - "integrity": "sha512-u9yZ0jUkOED1BFrqu3BwMQoixvGHGZ+JhJNkNKY/hyoEgOwlqKb62qu+7UjbPSHYjiVy8kKJHvXKv5coH4wDeg==", + "version": "4.62.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.62.3.tgz", + "integrity": "sha512-se6yXvNGMIl0f+RQzyh7XAmia8/9kplQx424wnG2w0C1oi6XgO6Y8otKhdXFHbHs88Ihavzmvh1NWjuovE76BQ==", "cpu": [ "riscv64" ], "dev": true, + "libc": [ + "musl" + ], "license": "MIT", "optional": true, "os": [ @@ -858,13 +843,16 @@ ] }, "node_modules/@rollup/rollup-linux-s390x-gnu": { - "version": "4.55.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.55.1.tgz", - "integrity": "sha512-/0PenBCmqM4ZUd0190j7J0UsQ/1nsi735iPRakO8iPciE7BQ495Y6msPzaOmvx0/pn+eJVVlZrNrSh4WSYLxNg==", + "version": "4.62.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.62.3.tgz", + "integrity": "sha512-gNoxRefktVIiGflpONuxWWXZAzIQG++z9qHO3xKwk4WdDMuQja3JHGfE1u0i3PfPDyvhypdk+WrgIJqLhGG7sg==", "cpu": [ "s390x" ], "dev": true, + "libc": [ + "glibc" + ], "license": "MIT", "optional": true, "os": [ @@ -872,13 +860,16 @@ ] }, "node_modules/@rollup/rollup-linux-x64-gnu": { - "version": "4.55.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.55.1.tgz", - "integrity": "sha512-a8G4wiQxQG2BAvo+gU6XrReRRqj+pLS2NGXKm8io19goR+K8lw269eTrPkSdDTALwMmJp4th2Uh0D8J9bEV1vg==", + "version": "4.62.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.62.3.tgz", + "integrity": "sha512-V4KtWtQfAFMU7+9/A/VDps/VI8CHd3cYz0L8sgJzz8qK7eY7wI4ruFD82UYIYvW9Z4DtlTfhQcsl4XyPHW5uSg==", "cpu": [ "x64" ], "dev": true, + "libc": [ + "glibc" + ], "license": "MIT", "optional": true, "os": [ @@ -886,13 +877,16 @@ ] }, "node_modules/@rollup/rollup-linux-x64-musl": { - "version": "4.55.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.55.1.tgz", - "integrity": "sha512-bD+zjpFrMpP/hqkfEcnjXWHMw5BIghGisOKPj+2NaNDuVT+8Ds4mPf3XcPHuat1tz89WRL+1wbcxKY3WSbiT7w==", + "version": "4.62.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.62.3.tgz", + "integrity": "sha512-LBx9LYXvj2CBkMkjLdNAWLwH0MLMin7do2VcVo9kVPibGLkY0BQQut2fv7NVqkXqZ/CrAu9LqDHVV1xHCMpCPw==", "cpu": [ "x64" ], "dev": true, + "libc": [ + "musl" + ], "license": "MIT", "optional": true, "os": [ @@ -900,9 +894,9 @@ ] }, "node_modules/@rollup/rollup-openbsd-x64": { - "version": "4.55.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.55.1.tgz", - "integrity": "sha512-eLXw0dOiqE4QmvikfQ6yjgkg/xDM+MdU9YJuP4ySTibXU0oAvnEWXt7UDJmD4UkYialMfOGFPJnIHSe/kdzPxg==", + "version": "4.62.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.62.3.tgz", + "integrity": "sha512-ABVf3Q0RCu7NcyCCOZQI0pJ3GuSdfSl8EXcy88QtdceIMIoCUdfhsJChZ64L9zVM2aJHjde1Bhn5uqSRcX9ySA==", "cpu": [ "x64" ], @@ -914,9 +908,9 @@ ] }, "node_modules/@rollup/rollup-openharmony-arm64": { - "version": "4.55.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.55.1.tgz", - "integrity": "sha512-xzm44KgEP11te3S2HCSyYf5zIzWmx3n8HDCc7EE59+lTcswEWNpvMLfd9uJvVX8LCg9QWG67Xt75AuHn4vgsXw==", + "version": "4.62.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.62.3.tgz", + "integrity": "sha512-+2Cy/ldweGBLlPIKsQLF8U5N44a0KDdbrk1rAjHOM9M2K+kGdIVjHLmmrZIcx+9Ny3ke/1JomCsDI1ocb11+sg==", "cpu": [ "arm64" ], @@ -928,9 +922,9 @@ ] }, "node_modules/@rollup/rollup-win32-arm64-msvc": { - "version": "4.55.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.55.1.tgz", - "integrity": "sha512-yR6Bl3tMC/gBok5cz/Qi0xYnVbIxGx5Fcf/ca0eB6/6JwOY+SRUcJfI0OpeTpPls7f194as62thCt/2BjxYN8g==", + "version": "4.62.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.62.3.tgz", + "integrity": "sha512-dtZvzc8BedpSaFNy75x6uiWwAGTH+aZHDtdrqP6qk+WcLJrfti6sGje1ZJ9UxyzDLF23d/mV+PaMwuC0hL7UVA==", "cpu": [ "arm64" ], @@ -942,9 +936,9 @@ ] }, "node_modules/@rollup/rollup-win32-ia32-msvc": { - "version": "4.55.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.55.1.tgz", - "integrity": "sha512-3fZBidchE0eY0oFZBnekYCfg+5wAB0mbpCBuofh5mZuzIU/4jIVkbESmd2dOsFNS78b53CYv3OAtwqkZZmU5nA==", + "version": "4.62.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.62.3.tgz", + "integrity": "sha512-Rj8Ra4noo+aYy7sKBggCx0407mws34kAb1ySyWuq5DAtFBQdkSwnsjCgPrhPe9cvgBKZIukpE+CVHvORCS93kQ==", "cpu": [ "ia32" ], @@ -956,9 +950,9 @@ ] }, "node_modules/@rollup/rollup-win32-x64-gnu": { - "version": "4.55.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.55.1.tgz", - "integrity": "sha512-xGGY5pXj69IxKb4yv/POoocPy/qmEGhimy/FoTpTSVju3FYXUQQMFCaZZXJVidsmGxRioZAwpThl/4zX41gRKg==", + "version": "4.62.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.62.3.tgz", + "integrity": "sha512-vp7N084ew/odXn2gi/mzm9mUkQu9l6AiN6dt4IeUM2Uvm9o+cVmP+YkqbMOteLbiGgqBBlJZjIMYVCfOOIVbVQ==", "cpu": [ "x64" ], @@ -970,9 +964,9 @@ ] }, "node_modules/@rollup/rollup-win32-x64-msvc": { - "version": "4.55.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.55.1.tgz", - "integrity": "sha512-SPEpaL6DX4rmcXtnhdrQYgzQ5W2uW3SCJch88lB2zImhJRhIIK44fkUrgIV/Q8yUNfw5oyZ5vkeQsZLhCb06lw==", + "version": "4.62.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.62.3.tgz", + "integrity": "sha512-MOG/3gTOn4Fwf574RVOaY61I5o6P90legkFADiTyn1hyjNydT+cerU2rLUwPdZkKKyJ+iT+K9p7WXK4LM1Ka6g==", "cpu": [ "x64" ], @@ -991,9 +985,9 @@ "license": "MIT" }, "node_modules/@sveltejs/acorn-typescript": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/@sveltejs/acorn-typescript/-/acorn-typescript-1.0.8.tgz", - "integrity": "sha512-esgN+54+q0NjB0Y/4BomT9samII7jGwNy/2a3wNZbT2A2RpmXsXwUt24LvLhx6jUq2gVk4cWEvcRO6MFQbOfNA==", + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/@sveltejs/acorn-typescript/-/acorn-typescript-1.0.11.tgz", + "integrity": "sha512-LFuZUkjJ9iF7JZye/aG5XM0SFcQ5VyL0oVX4WJ9dc0Va3R3s0OauX1BESVCb+YN/ol8TAfqGDDAQsTG627Y5kw==", "dev": true, "license": "MIT", "peerDependencies": { @@ -1011,24 +1005,23 @@ } }, "node_modules/@sveltejs/kit": { - "version": "2.49.4", - "resolved": "https://registry.npmjs.org/@sveltejs/kit/-/kit-2.49.4.tgz", - "integrity": "sha512-JFtOqDoU0DI/+QSG8qnq5bKcehVb3tCHhOG4amsSYth5/KgO4EkJvi42xSAiyKmXAAULW1/Zdb6lkgGEgSxdZg==", + "version": "2.70.2", + "resolved": "https://registry.npmjs.org/@sveltejs/kit/-/kit-2.70.2.tgz", + "integrity": "sha512-RzRoRpuR2KXqc5yMO0akQHDZeT4AslOlznGITURsqHaVbtyYP4Wn3eE3gxj9JcDyNYO0crkxhdwFHc+2vkVm6w==", "dev": true, "license": "MIT", "dependencies": { "@standard-schema/spec": "^1.0.0", - "@sveltejs/acorn-typescript": "^1.0.5", + "@sveltejs/acorn-typescript": "^1.0.9", "@types/cookie": "^0.6.0", - "acorn": "^8.14.1", + "acorn": "^8.16.0", "cookie": "^0.6.0", - "devalue": "^5.3.2", + "devalue": "^5.8.1", "esm-env": "^1.2.2", "kleur": "^4.1.5", "magic-string": "^0.30.5", "mrmime": "^2.0.0", - "sade": "^1.8.1", - "set-cookie-parser": "^2.6.0", + "set-cookie-parser": "^3.0.0", "sirv": "^3.0.0" }, "bin": { @@ -1039,10 +1032,10 @@ }, "peerDependencies": { "@opentelemetry/api": "^1.0.0", - "@sveltejs/vite-plugin-svelte": "^3.0.0 || ^4.0.0-next.1 || ^5.0.0 || ^6.0.0-next.0", + "@sveltejs/vite-plugin-svelte": "^3.0.0 || ^4.0.0-next.1 || ^5.0.0 || ^6.0.0-next.0 || ^7.0.0", "svelte": "^4.0.0 || ^5.0.0-next.0", - "typescript": "^5.3.3", - "vite": "^5.0.3 || ^6.0.0 || ^7.0.0-beta.0" + "typescript": "^5.3.3 || ^6.0.0", + "vite": "^5.0.3 || ^6.0.0 || ^7.0.0-beta.0 || ^8.0.0" }, "peerDependenciesMeta": { "@opentelemetry/api": { @@ -1053,6 +1046,16 @@ } } }, + "node_modules/@sveltejs/load-config": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/@sveltejs/load-config/-/load-config-0.2.1.tgz", + "integrity": "sha512-5m3B2cbqQ4TbwW6Xkh66Ntw6dD7gNc77cCxABTTesWcq9jxIzMgTk97pZx5vEtvQx8iokgi7GIphqZe+PGwcZA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 18.0.0" + } + }, "node_modules/@sveltejs/vite-plugin-svelte": { "version": "5.1.1", "resolved": "https://registry.npmjs.org/@sveltejs/vite-plugin-svelte/-/vite-plugin-svelte-5.1.1.tgz", @@ -1601,9 +1604,9 @@ } }, "node_modules/@types/estree": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", - "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.9.tgz", + "integrity": "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==", "dev": true, "license": "MIT" }, @@ -1619,10 +1622,20 @@ "integrity": "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==", "license": "MIT" }, + "node_modules/@upsetjs/venn.js": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@upsetjs/venn.js/-/venn.js-2.0.0.tgz", + "integrity": "sha512-WbBhLrooyePuQ1VZxrJjtLvTc4NVfpOyKx0sKqioq9bX1C1m7Jgykkn8gLrtwumBioXIqam8DLxp88Adbue6Hw==", + "license": "MIT", + "optionalDependencies": { + "d3-selection": "^3.0.0", + "d3-transition": "^3.0.1" + } + }, "node_modules/acorn": { - "version": "8.15.0", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", - "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", + "version": "8.18.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.18.0.tgz", + "integrity": "sha512-lGq+9yr1/GuAWaVYIHRjvvySG5/4VfKIvC8EWxStPdcDh/Ka7FG3twP6v4d5BkravUilhIAsG4Qj83t02LWUPQ==", "license": "MIT", "bin": { "acorn": "bin/acorn" @@ -1632,9 +1645,9 @@ } }, "node_modules/aria-query": { - "version": "5.3.2", - "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.2.tgz", - "integrity": "sha512-COROpnaoap1E2F000S62r6A60uHZnmlvomhfyT2DlTcrY1OrBKn2UhH7qn5wTC9zMvD0AY7csdPSNwKP+7WiQw==", + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.1.tgz", + "integrity": "sha512-Z/ZeOgVl7bcSYZ/u/rh0fOpvEpq//LZmdbkXyc7syVzjPAhfOa9ebsdTSjEBDU4vs5nC98Kfduj1uFo0qyET3g==", "dev": true, "license": "Apache-2.0", "engines": { @@ -1651,38 +1664,6 @@ "node": ">= 0.4" } }, - "node_modules/chevrotain": { - "version": "11.0.3", - "resolved": "https://registry.npmjs.org/chevrotain/-/chevrotain-11.0.3.tgz", - "integrity": "sha512-ci2iJH6LeIkvP9eJW6gpueU8cnZhv85ELY8w8WiFtNjMHA5ad6pQLaJo9mEly/9qUyCpvqX8/POVUTf18/HFdw==", - "license": "Apache-2.0", - "dependencies": { - "@chevrotain/cst-dts-gen": "11.0.3", - "@chevrotain/gast": "11.0.3", - "@chevrotain/regexp-to-ast": "11.0.3", - "@chevrotain/types": "11.0.3", - "@chevrotain/utils": "11.0.3", - "lodash-es": "4.17.21" - } - }, - "node_modules/chevrotain-allstar": { - "version": "0.3.1", - "resolved": "https://registry.npmjs.org/chevrotain-allstar/-/chevrotain-allstar-0.3.1.tgz", - "integrity": "sha512-b7g+y9A0v4mxCW1qUhf3BSVPg+/NvGErk/dOkrDaHA0nQIQGAtrOjlX//9OQtRlSCy+x9rfB5N8yC71lH1nvMw==", - "license": "MIT", - "dependencies": { - "lodash-es": "^4.17.21" - }, - "peerDependencies": { - "chevrotain": "^11.0.0" - } - }, - "node_modules/chevrotain/node_modules/lodash-es": { - "version": "4.17.21", - "resolved": "https://registry.npmjs.org/lodash-es/-/lodash-es-4.17.21.tgz", - "integrity": "sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw==", - "license": "MIT" - }, "node_modules/chokidar": { "version": "4.0.3", "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz", @@ -1725,9 +1706,9 @@ "license": "MIT" }, "node_modules/cookie": { - "version": "0.6.0", - "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.6.0.tgz", - "integrity": "sha512-U71cyTamuh1CRNCfpGY6to28lxvNwPG4Guz/EVjgf3Jmzv0vlDp1atT9eS5dDjMYHucpHbWns6Lwf3BKz6svdw==", + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", + "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", "dev": true, "license": "MIT", "engines": { @@ -1744,9 +1725,9 @@ } }, "node_modules/cytoscape": { - "version": "3.33.1", - "resolved": "https://registry.npmjs.org/cytoscape/-/cytoscape-3.33.1.tgz", - "integrity": "sha512-iJc4TwyANnOGR1OmWhsS9ayRS3s+XQ185FmuHObThD+5AeJCakAAbWv8KimMTt08xCCLNgneQwFp+JRJOr9qGQ==", + "version": "3.34.0", + "resolved": "https://registry.npmjs.org/cytoscape/-/cytoscape-3.34.0.tgz", + "integrity": "sha512-62rNSrioXw93uliKFBwjukeQyeWwH2PqDrTac31r2P6464u3AUvTk0xS4LVvT251g7IgkFunrI48ZEZGjywSOg==", "license": "MIT", "engines": { "node": ">=0.10" @@ -2242,9 +2223,9 @@ } }, "node_modules/dagre-d3-es": { - "version": "7.0.13", - "resolved": "https://registry.npmjs.org/dagre-d3-es/-/dagre-d3-es-7.0.13.tgz", - "integrity": "sha512-efEhnxpSuwpYOKRm/L5KbqoZmNNukHa/Flty4Wp62JRvgH2ojwVgPgdYyr4twpieZnyRDdIH7PY2mopX26+j2Q==", + "version": "7.0.14", + "resolved": "https://registry.npmjs.org/dagre-d3-es/-/dagre-d3-es-7.0.14.tgz", + "integrity": "sha512-P4rFMVq9ESWqmOgK+dlXvOtLwYg0i7u0HBGJER0LZDJT2VHIPAMZ/riPxqJceWMStH5+E61QxFra9kIS3AqdMg==", "license": "MIT", "dependencies": { "d3": "^7.9.0", @@ -2252,9 +2233,9 @@ } }, "node_modules/dayjs": { - "version": "1.11.19", - "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.19.tgz", - "integrity": "sha512-t5EcLVS6QPBNqM2z8fakk/NKel+Xzshgt8FFKAn+qwlD1pzZWxh0nVCrvFK7ZDb6XucZeF9z8C7CBWTRIVApAw==", + "version": "1.11.21", + "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.21.tgz", + "integrity": "sha512-98IT+HOahAisibz/yjKbzuOBwYcjJ7BCLPzARyHiyEBmRz4fatF+KPJszEHXsGYjUG234aH/cOjW1wwTbKUZlA==", "license": "MIT" }, "node_modules/debug": { @@ -2286,30 +2267,41 @@ } }, "node_modules/delaunator": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/delaunator/-/delaunator-5.0.1.tgz", - "integrity": "sha512-8nvh+XBe96aCESrGOqMp/84b13H9cdKbG5P2ejQCh4d4sK9RL4371qou9drQjMhvnPmhWl5hnmqbEE0fXr9Xnw==", + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/delaunator/-/delaunator-5.1.0.tgz", + "integrity": "sha512-AGrQ4QSgssa1NGmWmLPqN5NY2KajF5MqxetNEO+o0n3ZwZZeTmt7bBnvzHWrmkZFxGgr4HdyFgelzgi06otLuQ==", "license": "ISC", "dependencies": { "robust-predicates": "^3.0.2" } }, "node_modules/devalue": { - "version": "5.6.1", - "resolved": "https://registry.npmjs.org/devalue/-/devalue-5.6.1.tgz", - "integrity": "sha512-jDwizj+IlEZBunHcOuuFVBnIMPAEHvTsJj0BcIp94xYguLRVBcXO853px/MyIJvbVzWdsGvrRweIUWJw8hBP7A==", + "version": "5.9.0", + "resolved": "https://registry.npmjs.org/devalue/-/devalue-5.9.0.tgz", + "integrity": "sha512-RWrqdArjvPbsATEhOPUo6Wndc/iWnkWKlhIrdlF3zMMYo/c3CVtoaVAyLtWxz5h8nSlkHzxnzV2uLydPXmtF+A==", "dev": true, "license": "MIT" }, "node_modules/dompurify": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.3.1.tgz", - "integrity": "sha512-qkdCKzLNtrgPFP1Vo+98FRzJnBRGe4ffyCea9IwHB1fyxPOeNTHpLKYGd4Uk9xvNoH0ZoOjwZxNptyMwqrId1Q==", + "version": "3.4.12", + "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.4.12.tgz", + "integrity": "sha512-zQvGet8Z2sWbQhCmfFz/T5QWH2oBmjnqK3qvOjaqaNLrLEF912WamU+ohnTp0TCep/MFVHpdJuCZEdFOdTnEFg==", "license": "(MPL-2.0 OR Apache-2.0)", "optionalDependencies": { "@types/trusted-types": "^2.0.7" } }, + "node_modules/es-toolkit": { + "version": "1.50.0", + "resolved": "https://registry.npmjs.org/es-toolkit/-/es-toolkit-1.50.0.tgz", + "integrity": "sha512-OyZKhUVvEep9ITEiwHn8GKnMRQIVqoSIX7WnRbkWgJkllCujilqP2rD0u979tkl8wqyc8ICwlc1UBVv/Sl1G6w==", + "license": "MIT", + "workspaces": [ + "docs", + "benchmarks", + "tests/types" + ] + }, "node_modules/esbuild": { "version": "0.25.12", "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.12.tgz", @@ -2360,13 +2352,21 @@ "license": "MIT" }, "node_modules/esrap": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/esrap/-/esrap-2.2.1.tgz", - "integrity": "sha512-GiYWG34AN/4CUyaWAgunGt0Rxvr1PTMlGC0vvEov/uOQYWne2bpN03Um+k8jT+q3op33mKouP2zeJ6OlM+qeUg==", + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/esrap/-/esrap-2.3.0.tgz", + "integrity": "sha512-GQ/7RN8uOtEfNpzZzBMTzW9JBcX42oaSVtPzdF+6cEL8pqIL094iUpr9jzYGn4O4P/1S60dJ6izyT8F4LYARng==", "dev": true, "license": "MIT", "dependencies": { "@jridgewell/sourcemap-codec": "^1.4.15" + }, + "peerDependencies": { + "@typescript-eslint/types": "^8.2.0" + }, + "peerDependenciesMeta": { + "@typescript-eslint/types": { + "optional": true + } } }, "node_modules/fdir": { @@ -2454,9 +2454,9 @@ } }, "node_modules/katex": { - "version": "0.16.27", - "resolved": "https://registry.npmjs.org/katex/-/katex-0.16.27.tgz", - "integrity": "sha512-aeQoDkuRWSqQN6nSvVCEFvfXdqo1OQiCmmW1kc9xSdjutPv7BGO7pqY9sQRJpMOGrEdfDgF2TfRXe5eUAD2Waw==", + "version": "0.16.47", + "resolved": "https://registry.npmjs.org/katex/-/katex-0.16.47.tgz", + "integrity": "sha512-Eeo8Ys1doU1z+x8AZsPpQu+p/QcZBI5PeOo7QGQdy2x2m0MU/hYagBbGOmXwr5KVbEfVuWv9LpnQWeehogurjg==", "funding": [ "https://opencollective.com/katex", "https://github.com/sponsors/katex" @@ -2484,22 +2484,6 @@ "node": ">=6" } }, - "node_modules/langium": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/langium/-/langium-3.3.1.tgz", - "integrity": "sha512-QJv/h939gDpvT+9SiLVlY7tZC3xB2qK57v0J04Sh9wpMb6MP1q8gB21L3WIo8T5P1MSMg3Ep14L7KkDCFG3y4w==", - "license": "MIT", - "dependencies": { - "chevrotain": "~11.0.3", - "chevrotain-allstar": "~0.3.0", - "vscode-languageserver": "~9.0.1", - "vscode-languageserver-textdocument": "~1.0.11", - "vscode-uri": "~3.0.8" - }, - "engines": { - "node": ">=16.0.0" - } - }, "node_modules/layout-base": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/layout-base/-/layout-base-1.0.2.tgz", @@ -2514,9 +2498,9 @@ "license": "MIT" }, "node_modules/lodash-es": { - "version": "4.17.23", - "resolved": "https://registry.npmjs.org/lodash-es/-/lodash-es-4.17.23.tgz", - "integrity": "sha512-kVI48u3PZr38HdYz98UmfPnXl2DXrpdctLrFLCd3kOx1xUkOmpFPx7gCWWM5MPkL/fD8zb+Ph0QzjGFs4+hHWg==", + "version": "4.18.1", + "resolved": "https://registry.npmjs.org/lodash-es/-/lodash-es-4.18.1.tgz", + "integrity": "sha512-J8xewKD/Gk22OZbhpOVSwcs60zhd95ESDwezOFuA3/099925PdHJ7OFHNTGtajL3AlZkykD32HykiMo+BIBI8A==", "license": "MIT" }, "node_modules/magic-string": { @@ -2542,31 +2526,32 @@ } }, "node_modules/mermaid": { - "version": "11.12.2", - "resolved": "https://registry.npmjs.org/mermaid/-/mermaid-11.12.2.tgz", - "integrity": "sha512-n34QPDPEKmaeCG4WDMGy0OT6PSyxKCfy2pJgShP+Qow2KLrvWjclwbc3yXfSIf4BanqWEhQEpngWwNp/XhZt6w==", + "version": "11.16.0", + "resolved": "https://registry.npmjs.org/mermaid/-/mermaid-11.16.0.tgz", + "integrity": "sha512-Zvm3kbstgdpvIJPPItlL7fppIZ3kibvc1oZIGxdvk9t6UFz6flv+Jw7FtRGKwfcI8OckmH04LqG6LlS6X4B1pA==", "license": "MIT", "dependencies": { - "@braintree/sanitize-url": "^7.1.1", - "@iconify/utils": "^3.0.1", - "@mermaid-js/parser": "^0.6.3", + "@braintree/sanitize-url": "^7.1.2", + "@iconify/utils": "^3.0.2", + "@mermaid-js/parser": "^1.2.0", "@types/d3": "^7.4.3", - "cytoscape": "^3.29.3", + "@upsetjs/venn.js": "^2.0.0", + "cytoscape": "^3.33.3", "cytoscape-cose-bilkent": "^4.1.0", "cytoscape-fcose": "^2.2.0", "d3": "^7.9.0", "d3-sankey": "^0.12.3", - "dagre-d3-es": "7.0.13", - "dayjs": "^1.11.18", - "dompurify": "^3.2.5", - "katex": "^0.16.22", + "dagre-d3-es": "7.0.14", + "dayjs": "^1.11.20", + "dompurify": "^3.3.3", + "es-toolkit": "^1.45.1", + "katex": "^0.16.45", "khroma": "^2.1.0", - "lodash-es": "^4.17.21", - "marked": "^16.2.1", + "marked": "^16.3.0", "roughjs": "^4.6.6", "stylis": "^4.3.6", "ts-dedent": "^2.2.0", - "uuid": "^11.1.0" + "uuid": "^11.1.0 || ^12 || ^13 || ^14.0.0" } }, "node_modules/mermaid/node_modules/marked": { @@ -2603,15 +2588,6 @@ "marked": "14.0.0" } }, - "node_modules/monaco-editor/node_modules/dompurify": { - "version": "3.2.7", - "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.2.7.tgz", - "integrity": "sha512-WhL/YuveyGXJaerVlMYGWhvQswa7myDG17P7Vu65EWC05o8vfeNbvNf4d/BOvH99+ZW+LlQsc1GDKMa1vNK6dw==", - "license": "(MPL-2.0 OR Apache-2.0)", - "optionalDependencies": { - "@types/trusted-types": "^2.0.7" - } - }, "node_modules/monaco-vim": { "version": "0.4.4", "resolved": "https://registry.npmjs.org/monaco-vim/-/monaco-vim-0.4.4.tgz", @@ -2648,9 +2624,9 @@ "license": "MIT" }, "node_modules/nanoid": { - "version": "3.3.11", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", - "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", + "version": "3.3.16", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.16.tgz", + "integrity": "sha512-bzlKTyNJ7+LdGIIwy8ijFpIqEQIvafahV7eYykJ8Cvh42EdJeODoJ6gUJXpQJvej1BddH8OqTXZNE/KfbWAu8Q==", "dev": true, "funding": [ { @@ -2704,9 +2680,9 @@ "license": "ISC" }, "node_modules/picomatch": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", - "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.5.tgz", + "integrity": "sha512-RvwwcruNjI1ncT5xRakeyS9Lf8lcItv34KD+aif+VH9kduAyfYBipGh12274xtenIPZ119/R9BdTBa8gAwSh0A==", "dev": true, "license": "MIT", "engines": { @@ -2744,9 +2720,9 @@ } }, "node_modules/postcss": { - "version": "8.5.6", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz", - "integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==", + "version": "8.5.25", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.25.tgz", + "integrity": "sha512-DTPx3RWSSnWyzLxQnlH0rJP+EW5ekl16ZU4/psbIhA0e53kJfdgaN5vKM+xP7yJtXVu+nfdVFmlgFDEKAe4Pyw==", "dev": true, "funding": [ { @@ -2764,7 +2740,7 @@ ], "license": "MIT", "dependencies": { - "nanoid": "^3.3.11", + "nanoid": "^3.3.16", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" }, @@ -2787,19 +2763,19 @@ } }, "node_modules/robust-predicates": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/robust-predicates/-/robust-predicates-3.0.2.tgz", - "integrity": "sha512-IXgzBWvWQwE6PrDI05OvmXUIruQTcoMDzRsOd5CDvHCVLcLHMTSYvOK5Cm46kWqlV3yAbuSpBZdJ5oP5OUoStg==", + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/robust-predicates/-/robust-predicates-3.0.3.tgz", + "integrity": "sha512-NS3levdsRIUOmiJ8FZWCP7LG3QpJyrs/TE0Zpf1yvZu8cAJJ6QMW92H1c7kWpdIHo8RvmLxN/o2JXTKHp74lUA==", "license": "Unlicense" }, "node_modules/rollup": { - "version": "4.55.1", - "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.55.1.tgz", - "integrity": "sha512-wDv/Ht1BNHB4upNbK74s9usvl7hObDnvVzknxqY/E/O3X6rW1U1rV1aENEfJ54eFZDTNo7zv1f5N4edCluH7+A==", + "version": "4.62.3", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.62.3.tgz", + "integrity": "sha512-Gu0c0iH9FzgX1L1t7ByIbbS3Vmdz+6KHm/EsqmmC71gUQ82yvZRkTK6XzrFObSka91WUVdynqp6nsfilzr5k6Q==", "dev": true, "license": "MIT", "dependencies": { - "@types/estree": "1.0.8" + "@types/estree": "1.0.9" }, "bin": { "rollup": "dist/bin/rollup" @@ -2809,31 +2785,31 @@ "npm": ">=8.0.0" }, "optionalDependencies": { - "@rollup/rollup-android-arm-eabi": "4.55.1", - "@rollup/rollup-android-arm64": "4.55.1", - "@rollup/rollup-darwin-arm64": "4.55.1", - "@rollup/rollup-darwin-x64": "4.55.1", - "@rollup/rollup-freebsd-arm64": "4.55.1", - "@rollup/rollup-freebsd-x64": "4.55.1", - "@rollup/rollup-linux-arm-gnueabihf": "4.55.1", - "@rollup/rollup-linux-arm-musleabihf": "4.55.1", - "@rollup/rollup-linux-arm64-gnu": "4.55.1", - "@rollup/rollup-linux-arm64-musl": "4.55.1", - "@rollup/rollup-linux-loong64-gnu": "4.55.1", - "@rollup/rollup-linux-loong64-musl": "4.55.1", - "@rollup/rollup-linux-ppc64-gnu": "4.55.1", - "@rollup/rollup-linux-ppc64-musl": "4.55.1", - "@rollup/rollup-linux-riscv64-gnu": "4.55.1", - "@rollup/rollup-linux-riscv64-musl": "4.55.1", - "@rollup/rollup-linux-s390x-gnu": "4.55.1", - "@rollup/rollup-linux-x64-gnu": "4.55.1", - "@rollup/rollup-linux-x64-musl": "4.55.1", - "@rollup/rollup-openbsd-x64": "4.55.1", - "@rollup/rollup-openharmony-arm64": "4.55.1", - "@rollup/rollup-win32-arm64-msvc": "4.55.1", - "@rollup/rollup-win32-ia32-msvc": "4.55.1", - "@rollup/rollup-win32-x64-gnu": "4.55.1", - "@rollup/rollup-win32-x64-msvc": "4.55.1", + "@rollup/rollup-android-arm-eabi": "4.62.3", + "@rollup/rollup-android-arm64": "4.62.3", + "@rollup/rollup-darwin-arm64": "4.62.3", + "@rollup/rollup-darwin-x64": "4.62.3", + "@rollup/rollup-freebsd-arm64": "4.62.3", + "@rollup/rollup-freebsd-x64": "4.62.3", + "@rollup/rollup-linux-arm-gnueabihf": "4.62.3", + "@rollup/rollup-linux-arm-musleabihf": "4.62.3", + "@rollup/rollup-linux-arm64-gnu": "4.62.3", + "@rollup/rollup-linux-arm64-musl": "4.62.3", + "@rollup/rollup-linux-loong64-gnu": "4.62.3", + "@rollup/rollup-linux-loong64-musl": "4.62.3", + "@rollup/rollup-linux-ppc64-gnu": "4.62.3", + "@rollup/rollup-linux-ppc64-musl": "4.62.3", + "@rollup/rollup-linux-riscv64-gnu": "4.62.3", + "@rollup/rollup-linux-riscv64-musl": "4.62.3", + "@rollup/rollup-linux-s390x-gnu": "4.62.3", + "@rollup/rollup-linux-x64-gnu": "4.62.3", + "@rollup/rollup-linux-x64-musl": "4.62.3", + "@rollup/rollup-openbsd-x64": "4.62.3", + "@rollup/rollup-openharmony-arm64": "4.62.3", + "@rollup/rollup-win32-arm64-msvc": "4.62.3", + "@rollup/rollup-win32-ia32-msvc": "4.62.3", + "@rollup/rollup-win32-x64-gnu": "4.62.3", + "@rollup/rollup-win32-x64-msvc": "4.62.3", "fsevents": "~2.3.2" } }, @@ -2875,9 +2851,9 @@ "license": "MIT" }, "node_modules/set-cookie-parser": { - "version": "2.7.2", - "resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-2.7.2.tgz", - "integrity": "sha512-oeM1lpU/UvhTxw+g3cIfxXHyJRc/uidd3yK1P242gzHds0udQBYzs3y8j4gCCW+ZJ7ad0yctld8RYO+bdurlvw==", + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-3.1.2.tgz", + "integrity": "sha512-5/r/lTwbJ3zQ+qwdUFZYeRNqda7P5HD8zQKqlSjdGt1/S0cjLAphHusj4Y58ahDtWn/g32xrIS58/ikOvwl0Lw==", "dev": true, "license": "MIT" }, @@ -2913,23 +2889,24 @@ "license": "MIT" }, "node_modules/svelte": { - "version": "5.46.3", - "resolved": "https://registry.npmjs.org/svelte/-/svelte-5.46.3.tgz", - "integrity": "sha512-Y5juST3x+/ySty5tYJCVWa6Corkxpt25bUZQHqOceg9xfMUtDsFx6rCsG6cYf1cA6vzDi66HIvaki0byZZX95A==", + "version": "5.56.8", + "resolved": "https://registry.npmjs.org/svelte/-/svelte-5.56.8.tgz", + "integrity": "sha512-PY8LOw7xP6c8IOiVqdo0sbbZVYhXRSfklOQLAUyGBKqjTX0wx/z4l/9J+PmBpmlLnxzEb1NqltxQ5/wZme/Cmg==", "dev": true, "license": "MIT", "dependencies": { "@jridgewell/remapping": "^2.3.4", "@jridgewell/sourcemap-codec": "^1.5.0", - "@sveltejs/acorn-typescript": "^1.0.5", + "@sveltejs/acorn-typescript": "^1.0.10", "@types/estree": "^1.0.5", + "@types/trusted-types": "^2.0.7", "acorn": "^8.12.1", - "aria-query": "^5.3.1", + "aria-query": "5.3.1", "axobject-query": "^4.1.0", "clsx": "^2.1.1", - "devalue": "^5.5.0", + "devalue": "^5.8.1", "esm-env": "^1.2.1", - "esrap": "^2.2.1", + "esrap": "^2.2.12", "is-reference": "^3.0.3", "locate-character": "^3.0.0", "magic-string": "^0.30.11", @@ -2940,13 +2917,14 @@ } }, "node_modules/svelte-check": { - "version": "4.3.5", - "resolved": "https://registry.npmjs.org/svelte-check/-/svelte-check-4.3.5.tgz", - "integrity": "sha512-e4VWZETyXaKGhpkxOXP+B/d0Fp/zKViZoJmneZWe/05Y2aqSKj3YN2nLfYPJBQ87WEiY4BQCQ9hWGu9mPT1a1Q==", + "version": "4.7.4", + "resolved": "https://registry.npmjs.org/svelte-check/-/svelte-check-4.7.4.tgz", + "integrity": "sha512-IW9ot9YqAoyv8FvyN+eb4ZTe8zgcKZrJLNYU6dzSKkGwEBsSPc4K7lmQ8bKn8W2YMXM6WDfZSSVOaGtekyUfOQ==", "dev": true, "license": "MIT", "dependencies": { "@jridgewell/trace-mapping": "^0.3.25", + "@sveltejs/load-config": "^0.2.1", "chokidar": "^4.0.1", "fdir": "^6.2.0", "picocolors": "^1.0.0", @@ -2960,7 +2938,7 @@ }, "peerDependencies": { "svelte": "^4.0.0 || ^5.0.0-next.0", - "typescript": ">=5.0.0" + "typescript": "^5.0.0 || ^6.0.0" } }, "node_modules/tinyexec": { @@ -3532,22 +3510,22 @@ "license": "MIT" }, "node_modules/uuid": { - "version": "11.1.0", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-11.1.0.tgz", - "integrity": "sha512-0/A9rDy9P7cJ+8w1c9WD9V//9Wj15Ce2MPz8Ri6032usz+NfePxx5AcN3bN+r6ZL6jEo066/yNYB3tn4pQEx+A==", + "version": "14.0.1", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-14.0.1.tgz", + "integrity": "sha512-6ZxzVpzDXDa3bJWaHilVayA+BH/1zmxCJoVgvmqJnid/gPoKHxUrS/aC/T6LGQtNHT+XHG9fXPJB4d+IrU30Ew==", "funding": [ "https://github.com/sponsors/broofa", "https://github.com/sponsors/ctavan" ], "license": "MIT", "bin": { - "uuid": "dist/esm/bin/uuid" + "uuid": "dist-node/bin/uuid" } }, "node_modules/vite": { - "version": "6.4.1", - "resolved": "https://registry.npmjs.org/vite/-/vite-6.4.1.tgz", - "integrity": "sha512-+Oxm7q9hDoLMyJOYfUYBuHQo+dkAloi33apOPP56pzj+vsdJDzr+j1NISE5pyaAuKL4A3UD34qd0lx5+kfKp2g==", + "version": "6.4.3", + "resolved": "https://registry.npmjs.org/vite/-/vite-6.4.3.tgz", + "integrity": "sha512-NTKlcQjlAK7MlQoyb6LgaqHc8sso/pVyUJYWMws3jg21uTJw/LddqIFPcPqP6PzpgbIcZyKI85sFE4HBrQDA8A==", "dev": true, "license": "MIT", "dependencies": { @@ -3639,55 +3617,6 @@ } } }, - "node_modules/vscode-jsonrpc": { - "version": "8.2.0", - "resolved": "https://registry.npmjs.org/vscode-jsonrpc/-/vscode-jsonrpc-8.2.0.tgz", - "integrity": "sha512-C+r0eKJUIfiDIfwJhria30+TYWPtuHJXHtI7J0YlOmKAo7ogxP20T0zxB7HZQIFhIyvoBPwWskjxrvAtfjyZfA==", - "license": "MIT", - "engines": { - "node": ">=14.0.0" - } - }, - "node_modules/vscode-languageserver": { - "version": "9.0.1", - "resolved": "https://registry.npmjs.org/vscode-languageserver/-/vscode-languageserver-9.0.1.tgz", - "integrity": "sha512-woByF3PDpkHFUreUa7Hos7+pUWdeWMXRd26+ZX2A8cFx6v/JPTtd4/uN0/jB6XQHYaOlHbio03NTHCqrgG5n7g==", - "license": "MIT", - "dependencies": { - "vscode-languageserver-protocol": "3.17.5" - }, - "bin": { - "installServerIntoExtension": "bin/installServerIntoExtension" - } - }, - "node_modules/vscode-languageserver-protocol": { - "version": "3.17.5", - "resolved": "https://registry.npmjs.org/vscode-languageserver-protocol/-/vscode-languageserver-protocol-3.17.5.tgz", - "integrity": "sha512-mb1bvRJN8SVznADSGWM9u/b07H7Ecg0I3OgXDuLdn307rl/J3A9YD6/eYOssqhecL27hK1IPZAsaqh00i/Jljg==", - "license": "MIT", - "dependencies": { - "vscode-jsonrpc": "8.2.0", - "vscode-languageserver-types": "3.17.5" - } - }, - "node_modules/vscode-languageserver-textdocument": { - "version": "1.0.12", - "resolved": "https://registry.npmjs.org/vscode-languageserver-textdocument/-/vscode-languageserver-textdocument-1.0.12.tgz", - "integrity": "sha512-cxWNPesCnQCcMPeenjKKsOCKQZ/L6Tv19DTRIGuLWe32lyzWhihGVJ/rcckZXJxfdKCFvRLS3fpBIsV/ZGX4zA==", - "license": "MIT" - }, - "node_modules/vscode-languageserver-types": { - "version": "3.17.5", - "resolved": "https://registry.npmjs.org/vscode-languageserver-types/-/vscode-languageserver-types-3.17.5.tgz", - "integrity": "sha512-Ld1VelNuX9pdF39h2Hgaeb5hEZM2Z3jUrrMgWQAu82jMtZp7p3vJT3BzToKtZI7NgQssZje5o0zryOrhQvzQAg==", - "license": "MIT" - }, - "node_modules/vscode-uri": { - "version": "3.0.8", - "resolved": "https://registry.npmjs.org/vscode-uri/-/vscode-uri-3.0.8.tgz", - "integrity": "sha512-AyFQ0EVmsOZOlAnxoFOGOq1SQDWAB7C6aqMGS23svWAllfOaxbuFvcT8D1i8z3Gyn8fraVeZNNmN6e9bxxXkKw==", - "license": "MIT" - }, "node_modules/yaml": { "version": "2.9.0", "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.9.0.tgz", diff --git a/package.json b/package.json index c4f9621f..f2b2f637 100644 --- a/package.json +++ b/package.json @@ -25,25 +25,29 @@ "@tauri-apps/plugin-process": "^2.3.1", "@tauri-apps/plugin-updater": "^2.10.1", "@types/dompurify": "^3.0.5", - "dompurify": "^3.3.1", + "dompurify": "3.4.12", "highlight.js": "^11.11.1", "highlightjs-svelte": "^1.0.6", "katex": "^0.16.27", - "mermaid": "^11.12.2", + "mermaid": "^11.16.0", "monaco-editor": "^0.55.1", "monaco-vim": "^0.4.4", "node-stream-zip": "^1.15.0", "yaml": "^2.9.0" }, "devDependencies": { - "@sveltejs/adapter-static": "^3.0.6", - "@sveltejs/kit": "^2.9.0", - "@sveltejs/vite-plugin-svelte": "^5.0.0", + "@sveltejs/adapter-static": "^3.0.10", + "@sveltejs/kit": "^2.70.2", + "@sveltejs/vite-plugin-svelte": "^5.1.1", "@tauri-apps/cli": "^2", - "svelte": "^5.0.0", - "svelte-check": "^4.0.0", + "svelte": "^5.56.8", + "svelte-check": "^4.7.4", "tsx": "^4.22.4", "typescript": "~5.6.2", - "vite": "^6.0.3" + "vite": "^6.4.3" + }, + "overrides": { + "cookie": "0.7.2", + "dompurify": "3.4.12" } -} \ No newline at end of file +} diff --git a/scripts/documentLoadFailure.test.ts b/scripts/documentLoadFailure.test.ts new file mode 100644 index 00000000..8fc579d0 --- /dev/null +++ b/scripts/documentLoadFailure.test.ts @@ -0,0 +1,10 @@ +import assert from 'node:assert/strict'; +import { readFileSync } from 'node:fs'; +import test from 'node:test'; + +const session = readFileSync('src/lib/sessions/documentSession.svelte.ts', 'utf8'); + +test('a transient missing-file read error preserves the open tab', () => { + assert.doesNotMatch(session, /tabManager\.closeTab/); + assert.match(session, /options\.onError\('Error loading file', error\);/); +}); diff --git a/scripts/fileAssociations.test.ts b/scripts/fileAssociations.test.ts new file mode 100644 index 00000000..03ade7b9 --- /dev/null +++ b/scripts/fileAssociations.test.ts @@ -0,0 +1,13 @@ +import assert from 'node:assert/strict'; +import { readFileSync } from 'node:fs'; +import test from 'node:test'; + +const config = JSON.parse(readFileSync('src-tauri/tauri.conf.json', 'utf8')) as { + bundle: { fileAssociations: Array<{ ext: string[] }> }; +}; + +test('installer advertises only Markdown file associations', () => { + const extensions = config.bundle.fileAssociations.flatMap((association) => association.ext); + assert.deepEqual(extensions, ['md', 'markdown']); + assert.equal(extensions.includes('txt'), false); +}); diff --git a/scripts/issue261EditorPdf.test.ts b/scripts/issue261EditorPdf.test.ts index 97c6aea2..45f5cd5d 100644 --- a/scripts/issue261EditorPdf.test.ts +++ b/scripts/issue261EditorPdf.test.ts @@ -25,3 +25,24 @@ test('print layout releases measured fold heights before text reflows', () => { /@media print\s*\{[\s\S]*?\.foldable-content-wrapper\.is-collapsed\s*\{[\s\S]*?height:\s*0\s*!important;/, ); }); + +test('print layout lets the viewer pane escape the interactive split flex ratio', () => { + assert.match( + styles, + /@media print\s*\{[\s\S]*?\.pane\.viewer-pane\s*\{[\s\S]*?flex:\s*none\s*!important;/, + ); +}); + +test('floating toc toggle keeps a visible translucent surface outside edit mode', () => { + const selector = viewer.slice( + viewer.indexOf('\t.toc-toggle-floating {'), + viewer.indexOf('\n\t.toc-toggle-floating.expanded {'), + ); + + assert.match(selector, /background-color:\s*color-mix\(in srgb, var\(--color-canvas-default\) 82%, transparent\);/); + assert.match(selector, /border:\s*1px solid var\(--color-border-default\);/); + assert.match(selector, /box-shadow:\s*0 2px 8px rgba\(0, 0, 0, 0\.12\);/); + assert.match(selector, /backdrop-filter:\s*blur\(8px\);/); + assert.match(viewer, /\.toc-toggle-floating:focus-visible\s*\{[\s\S]*?outline:\s*2px solid var\(--color-accent-fg\);/); + assert.doesNotMatch(viewer, /\.toc-toggle-floating\.in-edit-mode:not\(\.expanded\)/); +}); diff --git a/scripts/liveModeWatchedPath.test.ts b/scripts/liveModeWatchedPath.test.ts new file mode 100644 index 00000000..7f009744 --- /dev/null +++ b/scripts/liveModeWatchedPath.test.ts @@ -0,0 +1,20 @@ +import assert from 'node:assert/strict'; +import { readFileSync } from 'node:fs'; +import test from 'node:test'; + +const runtime = readFileSync('src-tauri/src/window_runtime.rs', 'utf8'); +const viewer = readFileSync('src/lib/MarkdownViewer.svelte', 'utf8'); + +test('Live Mode routes a watcher notification to its watched path', () => { + assert.match(runtime, /emit_to\(event_label\.as_str\(\), "file-changed", watched_path\.clone\(\)\)/); + assert.match(viewer, /await appWindow\.listen\('file-changed', \(event\) => \{/); + assert.match(viewer, /const changedPath = event\.payload as string;/); + assert.match(viewer, /if \(!liveMode \|\| !currentFile \|\| changedPath !== currentFile\) return;/); +}); + +test('Live Mode follows the active file instead of retaining a previous tab watcher', () => { + assert.match(viewer, /if \(liveMode && currentFile\) \{\n\t\t\tinvoke\('watch_file', \{ path: currentFile \}\)/); + assert.doesNotMatch(readFileSync('src/lib/sessions/documentSession.svelte.ts', 'utf8'), /isLiveMode\(\)\) invoke\('watch_file'/); + const toggleLiveMode = viewer.slice(viewer.indexOf('function toggleLiveMode'), viewer.indexOf('async function saveImageAs')); + assert.doesNotMatch(toggleLiveMode, /loadMarkdown\(/); +}); diff --git a/scripts/macosOpenDocumentPaths.test.ts b/scripts/macosOpenDocumentPaths.test.ts new file mode 100644 index 00000000..6b373ab7 --- /dev/null +++ b/scripts/macosOpenDocumentPaths.test.ts @@ -0,0 +1,16 @@ +import assert from 'node:assert/strict'; +import { readFileSync } from 'node:fs'; +import test from 'node:test'; + +const runtime = readFileSync('src-tauri/src/window_runtime.rs', 'utf8'); +const tauriLib = readFileSync('src-tauri/src/lib.rs', 'utf8'); +const viewer = readFileSync('src/lib/MarkdownViewer.svelte', 'utf8'); + +test('macOS open-document events preserve every delivered file path', () => { + assert.match(runtime, /startup_files: Mutex>/); + assert.match(tauriLib, /for url in urls/); + assert.match(tauriLib, /startup_files\.lock\(\)\.unwrap\(\)\.push\(path_str\.clone\(\)\)/); + assert.match(runtime, /startup_files\.lock\(\)\.unwrap\(\)\.drain\(\.\.\)\.collect\(\)/); + assert.match(runtime, /for path in startup_files\.into_iter\(\)\.rev\(\)/); + assert.match(viewer, /for \(const path of args\) await loadMarkdown\(path\);/); +}); diff --git a/scripts/macosPdfExport.test.ts b/scripts/macosPdfExport.test.ts new file mode 100644 index 00000000..d563937c --- /dev/null +++ b/scripts/macosPdfExport.test.ts @@ -0,0 +1,16 @@ +import assert from 'node:assert/strict'; +import { readFileSync } from 'node:fs'; +import test from 'node:test'; + +test('all Markpad webviews may invoke Tauri native printing', () => { + const capability = JSON.parse(readFileSync('src-tauri/capabilities/default.json', 'utf8')) as { + windows: string[]; + permissions: string[]; + }; + + assert.deepEqual(capability.windows, ['main', 'installer', 'window-*']); + assert.ok( + capability.permissions.includes('core:webview:allow-print'), + 'macOS replaces window.print() with Tauri native printing, which requires this permission', + ); +}); diff --git a/scripts/openMultipleFiles.test.ts b/scripts/openMultipleFiles.test.ts new file mode 100644 index 00000000..3efbbd33 --- /dev/null +++ b/scripts/openMultipleFiles.test.ts @@ -0,0 +1,12 @@ +import assert from 'node:assert/strict'; +import { readFileSync } from 'node:fs'; +import test from 'node:test'; + +const viewer = readFileSync('src/lib/MarkdownViewer.svelte', 'utf8'); +const selectFile = viewer.slice(viewer.indexOf('async function selectFile'), viewer.indexOf('async function reloadFromDisk')); + +test('Open File accepts multiple documents and loads each selected path', () => { + assert.match(selectFile, /multiple: true/); + assert.match(selectFile, /const paths = Array\.isArray\(selected\) \? selected : \[selected\];/); + assert.match(selectFile, /for \(const path of paths\) await loadMarkdown\(path\);/); +}); diff --git a/scripts/previewScrollSync.test.ts b/scripts/previewScrollSync.test.ts index 731ec681..da147f5b 100644 --- a/scripts/previewScrollSync.test.ts +++ b/scripts/previewScrollSync.test.ts @@ -19,8 +19,12 @@ test('editor emits and applies pixel segment scroll positions', () => { assert.match(editor, /type ScrollSyncPosition = \{\s*section: 'frontmatter' \| 'body';\s*ratio: number;\s*\}/); assert.match(editor, /onscrollsync\?: \(position: ScrollSyncPosition\) => void/); assert.match(editor, /function getEditorFrontMatterScrollEnd\(\)/); + assert.match(editor, /function getEditorContentScrollMax\(\)/); assert.match(editor, /function getEditorScrollSyncPosition\(\)/); assert.match(editor, /export function syncScrollToPosition\(position: ScrollSyncPosition\)/); + assert.match(editor, /editor\.getContentHeight\(\) - layout\.height/); + assert.match(editor, /getEditorScrollSyncPosition\(\)[\s\S]*getEditorContentScrollMax\(\)/); + assert.match(editor, /syncScrollToPosition\(position: ScrollSyncPosition\)[\s\S]*getEditorContentScrollMax\(\)/); assert.match(editor, /onscrollsync\?\.\(getEditorScrollSyncPosition\(\)\)/); assert.doesNotMatch(editor, /onscrollsync\?\.\(position\.lineNumber, ratio\)/); assert.doesNotMatch(editor, /export function syncScrollToLine/); diff --git a/scripts/previewWidth.test.ts b/scripts/previewWidth.test.ts new file mode 100644 index 00000000..2161db39 --- /dev/null +++ b/scripts/previewWidth.test.ts @@ -0,0 +1,89 @@ +import assert from 'node:assert/strict'; +import { readFileSync } from 'node:fs'; +import test from 'node:test'; + +import { + adjustPreviewMaxWidth, + DEFAULT_PREVIEW_MAX_WIDTH, + getPreviewContentWidth, + getStoredPreviewFullWidth, + MAX_PREVIEW_MAX_WIDTH, + MIN_PREVIEW_MAX_WIDTH, + normalizePreviewMaxWidth, +} from '../src/lib/utils/previewWidth.js'; + +const settingsSource = readFileSync(new URL('../src/lib/stores/settings.svelte.ts', import.meta.url), 'utf8'); +const viewerSource = readFileSync(new URL('../src/lib/MarkdownViewer.svelte', import.meta.url), 'utf8'); +const settingsComponentSource = readFileSync(new URL('../src/lib/components/Settings.svelte', import.meta.url), 'utf8'); + +test('preview width defaults and clamps persisted numeric values', () => { + assert.equal(DEFAULT_PREVIEW_MAX_WIDTH, 880); + assert.equal(normalizePreviewMaxWidth(null), DEFAULT_PREVIEW_MAX_WIDTH); + assert.equal(normalizePreviewMaxWidth('not-a-number'), DEFAULT_PREVIEW_MAX_WIDTH); + assert.equal(normalizePreviewMaxWidth('639'), MIN_PREVIEW_MAX_WIDTH); + assert.equal(normalizePreviewMaxWidth('1601'), MAX_PREVIEW_MAX_WIDTH); + assert.equal(normalizePreviewMaxWidth('921.8'), 922); +}); + +test('preview width accepts only finite numeric values', () => { + assert.equal(normalizePreviewMaxWidth(Number.NaN), DEFAULT_PREVIEW_MAX_WIDTH); + assert.equal(normalizePreviewMaxWidth(Number.POSITIVE_INFINITY), DEFAULT_PREVIEW_MAX_WIDTH); + assert.equal(normalizePreviewMaxWidth(1200), 1200); +}); + +test('full mode has no configured content-width cap', () => { + assert.equal(getPreviewContentWidth(1200, false), 1200); + assert.equal(getPreviewContentWidth(1200, true), null); +}); + +test('preview width shortcut adjustments retain the configured bounds', () => { + assert.equal(adjustPreviewMaxWidth(DEFAULT_PREVIEW_MAX_WIDTH, 40), 920); + assert.equal(adjustPreviewMaxWidth(MIN_PREVIEW_MAX_WIDTH, -40), MIN_PREVIEW_MAX_WIDTH); + assert.equal(adjustPreviewMaxWidth(MAX_PREVIEW_MAX_WIDTH, 40), MAX_PREVIEW_MAX_WIDTH); +}); + +test('the dedicated full-width preference takes priority over its legacy key', () => { + assert.equal(getStoredPreviewFullWidth('true', 'false'), true); + assert.equal(getStoredPreviewFullWidth('false', 'true'), false); + assert.equal(getStoredPreviewFullWidth(null, 'true'), true); + assert.equal(getStoredPreviewFullWidth(null, null), false); +}); + +test('settings load, persist, and reset the preview width through one normalizer', () => { + assert.match(settingsSource, /previewMaxWidth = \$state\(DEFAULT_PREVIEW_MAX_WIDTH\)/); + assert.match(settingsSource, /localStorage\.getItem\('preview\.maxWidth'\)/); + assert.match(settingsSource, /normalizePreviewMaxWidth\(savedPreviewMaxWidth\)/); + assert.match(settingsSource, /localStorage\.setItem\('preview\.maxWidth', String\(this\.previewMaxWidth\)\)/); + assert.match(settingsSource, /resetPreviewMaxWidth\(\)[\s\S]*this\.previewMaxWidth = DEFAULT_PREVIEW_MAX_WIDTH/); +}); + +test('preview layout derives width and ToC geometry from the same preference', () => { + assert.match(viewerSource, /getPreviewContentWidth\(settings\.previewMaxWidth, isFullWidth\)/); + assert.match(viewerSource, /viewerWidth - previewContentWidth/); + assert.match(viewerSource, /--preview-max-width:/); + assert.match(viewerSource, /max-width: var\(--preview-max-width, 880px\)/); +}); + +test('Settings exposes a bounded preview-width input and reset action', () => { + assert.match(settingsComponentSource, /id="preview-max-width"/); + assert.match(settingsComponentSource, /min=\{MIN_PREVIEW_MAX_WIDTH\}/); + assert.match(settingsComponentSource, /max=\{MAX_PREVIEW_MAX_WIDTH\}/); + assert.match(settingsComponentSource, /bind:value=\{settings\.previewMaxWidth\}/); + assert.match(settingsComponentSource, /settings\.resetPreviewMaxWidth\(\)/); +}); + +test('preview width keyboard shortcuts avoid editable controls and app modals', () => { + assert.match(viewerSource, /function canUsePreviewWidthShortcut\(target: EventTarget \| null, isSplit: boolean\)/); + assert.match(viewerSource, /showSettings \|\| modalState\.show \|\| promptModal\.show \|\| showHome/); + assert.match(viewerSource, /closest\('input, textarea, select, \[contenteditable="true"\], \[role="textbox"\]'\)/); + assert.match(viewerSource, /cmdOrCtrl && e\.altKey && !e\.shiftKey && \(code === 'BracketLeft' \|\| code === 'BracketRight'\)/); + assert.match(viewerSource, /isFullWidth = false/); + assert.match(viewerSource, /settings\.previewMaxWidth = adjustPreviewMaxWidth\(settings\.previewMaxWidth, code === 'BracketLeft' \? -40 : 40\)/); +}); + +test('preview full-width state migrates from the legacy localStorage key', () => { + assert.match(viewerSource, /localStorage\.getItem\('preview\.fullWidth'\)/); + assert.match(viewerSource, /localStorage\.getItem\('isFullWidth'\)/); + assert.match(viewerSource, /localStorage\.setItem\('preview\.fullWidth', String\(isFullWidth\)\)/); + assert.match(viewerSource, /localStorage\.removeItem\('isFullWidth'\)/); +}); diff --git a/scripts/reloadOpenToolbar.test.ts b/scripts/reloadOpenToolbar.test.ts index b7830656..194660df 100644 --- a/scripts/reloadOpenToolbar.test.ts +++ b/scripts/reloadOpenToolbar.test.ts @@ -24,10 +24,10 @@ test('preview-level open shortcut handles Ctrl/Cmd+O outside Monaco without doub assert.match(markdownViewer, /cmdOrCtrl[\s\S]*key === 'o'[\s\S]*selectFile\(\)/); }); -test('editor toolbar delegates to Monaco actions and exposes expected Markdown commands', () => { +test('editor toolbar forwards Monaco actions and optional payloads', () => { assert.match(markdownViewer, /import EditorToolbar from '\.\/components\/EditorToolbar\.svelte'/); - assert.match(markdownViewer, / editorPane\?\.runEditorAction\(actionId\)\}/); - assert.match(editor, /export function runEditorAction\(actionId: string\)/); + assert.match(markdownViewer, / editorPane\?\.runEditorAction\(actionId, payload\)\}/); + assert.match(editor, /export function runEditorAction\(actionId: string, payload\?: any\)/); for (const actionId of [ 'fmt-bold', diff --git a/scripts/scrollSyncInput.test.ts b/scripts/scrollSyncInput.test.ts new file mode 100644 index 00000000..bbbd5373 --- /dev/null +++ b/scripts/scrollSyncInput.test.ts @@ -0,0 +1,11 @@ +import assert from 'node:assert/strict'; +import { readFileSync } from 'node:fs'; +import test from 'node:test'; + +const editor = readFileSync('src/lib/components/Editor.svelte', 'utf8'); +const syncEffect = editor.slice(editor.indexOf('if (editor && onscrollsync)'), editor.indexOf('\n\t$effect(() => {', editor.indexOf('if (editor && onscrollsync)') + 1)); + +test('typing does not initiate split scroll synchronization', () => { + assert.match(syncEffect, /editor\.onDidScrollChange/); + assert.doesNotMatch(syncEffect, /onDidChangeCursorPosition/); +}); diff --git a/scripts/taskToggleMemory.test.ts b/scripts/taskToggleMemory.test.ts index cbd9bf25..2084c5c0 100644 --- a/scripts/taskToggleMemory.test.ts +++ b/scripts/taskToggleMemory.test.ts @@ -3,12 +3,36 @@ import { readFileSync } from 'node:fs'; import test from 'node:test'; const session = readFileSync(new URL('../src/lib/sessions/documentSession.svelte.ts', import.meta.url), 'utf8'); +const markdownProcessing: string = readFileSync(new URL('../src/lib/utils/markdown.ts', import.meta.url), 'utf8'); +const viewer = readFileSync(new URL('../src/lib/MarkdownViewer.svelte', import.meta.url), 'utf8'); test('preview task toggles transform the active in-memory buffer before saving', () => { const taskToggle = session.match(/async function toggleTaskCheckbox[\s\S]*?\n\t}\n\n\treturn/); assert.ok(taskToggle); assert.doesNotMatch(taskToggle[0], /read_file_content/); assert.match(taskToggle[0], /const raw = tab\.rawContent;/); + assert.match(taskToggle[0], /getMarkdownBodyWithoutFrontMatter\(raw\)/); + assert.match(taskToggle[0], /body\.slice\(0, offset\)\.split\('\\n'\)\.length/); + assert.match(taskToggle[0], /(?:\[-\+\*\]|\\d\+\[\.\)\])/); assert.match(taskToggle[0], /tabManager\.updateTabRawContent\(tab\.id, updated\);/); assert.match(taskToggle[0], /await saveContent\(tab\.id\)/); }); + +test('preview task toggles use the renderer source line instead of checkbox order', () => { + const viewerToggle = viewer.match(/async function toggleTaskCheckbox[\s\S]*?\n\t}\n\n/); + assert.ok(viewerToggle); + assert.match(viewerToggle[0], /closest\('li'\)\?\.getAttribute\('data-sourcepos'\)/); + assert.match(viewerToggle[0], /documentSession\.toggleTaskCheckbox\(sourceLine, nowChecked\)/); + assert.doesNotMatch(viewerToggle[0], /allBoxes/); + assert.match(viewer, /onchange=\{handleTaskCheckboxChange\}/); + assert.match(viewer, /const nowChecked = checkbox\.checked/); + assert.doesNotMatch(viewerToggle[0], /const nowChecked = !checkbox\.checked/); +}); + +test('preview task processing trusts the Markdown renderer task marker', () => { + const taskProcessing = markdownProcessing.match(/function processTaskItems[\s\S]*?\n}\n\nexport function processMarkdownHtml/); + assert.ok(taskProcessing); + assert.match(taskProcessing[0], /input\.hasAttribute\("data-task-checkbox"\)/); + assert.match(taskProcessing[0], /input\.setAttribute\("disabled", ""\)/); + assert.match(markdownProcessing, /input\.removeAttribute\("disabled"\)/); +}); diff --git a/scripts/toolbarCustomizationWiring.test.ts b/scripts/toolbarCustomizationWiring.test.ts index 1af4c54a..fd581013 100644 --- a/scripts/toolbarCustomizationWiring.test.ts +++ b/scripts/toolbarCustomizationWiring.test.ts @@ -63,9 +63,13 @@ test('settings modal uses a dedicated resize handle instead of native CSS resize assert.match(settingsComponent, /\.settings-modal[\s\S]*max-height:/); }); -test('settings modal can be dragged by the header and resized from every edge', () => { +test('settings modal starts dragging only from the non-interactive header surface', () => { assert.match(settingsComponent, /handleSettingsModalDragPointerDown/); - assert.match(settingsComponent, /class="settings-header"[\s\S]*onpointerdown=\{handleSettingsModalDragPointerDown\}/); + assert.match(settingsComponent, /class="settings-modal"[\s\S]*onpointerdown=\{handleSettingsModalDragPointerDown\}/); + assert.match(settingsComponent, /closest\('\.settings-header'\)/); + assert.doesNotMatch(settingsComponent, /class="settings-header"[^>]*onpointerdown=/); + assert.match(settingsComponent, /isSettingsHeaderInteractiveTarget\(e\.target\)/); + assert.match(settingsComponent, /settingsResizeHandles/); for (const handleClass of ['resize-n', 'resize-ne', 'resize-e', 'resize-se', 'resize-s', 'resize-sw', 'resize-w', 'resize-nw']) { assert.match(settingsComponent, new RegExp(`className: '${handleClass}'`)); diff --git a/scripts/windowStartupHandle.test.ts b/scripts/windowStartupHandle.test.ts new file mode 100644 index 00000000..151f7b7d --- /dev/null +++ b/scripts/windowStartupHandle.test.ts @@ -0,0 +1,10 @@ +import assert from 'node:assert/strict'; +import { readFileSync } from 'node:fs'; +import test from 'node:test'; + +const backend = readFileSync('src-tauri/src/lib.rs', 'utf8'); + +test('startup reuses the window handle returned by the builder', () => { + assert.match(backend, /let window = window_builder\.build\(\)\?;/); + assert.doesNotMatch(backend, /app\.get_webview_window\(label\)\.unwrap\(\)/); +}); diff --git a/scripts/windowTitleCapability.test.ts b/scripts/windowTitleCapability.test.ts new file mode 100644 index 00000000..78c0a8ef --- /dev/null +++ b/scripts/windowTitleCapability.test.ts @@ -0,0 +1,14 @@ +import assert from 'node:assert/strict'; +import { readFileSync } from 'node:fs'; +import test from 'node:test'; + +test('all Markpad windows may update their native title', () => { + const capability = JSON.parse(readFileSync('src-tauri/capabilities/default.json', 'utf8')) as { + permissions: string[]; + }; + + assert.ok( + capability.permissions.includes('core:window:allow-set-title'), + 'window tags and active document names call appWindow.setTitle(), which requires this permission', + ); +}); diff --git a/scripts/workflowSecurityAudit.test.ts b/scripts/workflowSecurityAudit.test.ts new file mode 100644 index 00000000..eb890c7f --- /dev/null +++ b/scripts/workflowSecurityAudit.test.ts @@ -0,0 +1,10 @@ +import assert from 'node:assert/strict'; +import { readFileSync } from 'node:fs'; +import test from 'node:test'; + +const workflow = readFileSync('.github/workflows/test.yml', 'utf8'); + +test('pull-request checks reject vulnerabilities anywhere in the resolved lockfile', () => { + assert.match(workflow, /name: audit complete dependency lockfile[\s\S]*run: npm audit(?:\s|$)/); + assert.doesNotMatch(workflow, /npm audit --omit=dev/); +}); diff --git a/src-tauri/capabilities/default.json b/src-tauri/capabilities/default.json index 955f6753..c41704e0 100644 --- a/src-tauri/capabilities/default.json +++ b/src-tauri/capabilities/default.json @@ -13,7 +13,9 @@ "opener:allow-open-url", "opener:allow-open-path", "dialog:default", + "core:webview:allow-print", "core:window:allow-start-dragging", + "core:window:allow-set-title", "core:window:allow-minimize", "core:window:allow-toggle-maximize", "core:window:allow-close", @@ -25,4 +27,4 @@ "updater:default", "process:default" ] -} \ No newline at end of file +} diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index 99ce9cd8..7586c228 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -198,6 +198,47 @@ mod tests { ); } + #[test] + fn task_list_checkbox_is_emitted_at_the_start_of_its_list_item() { + let html = convert_markdown("- [ ] open task\n- [x] completed task\n"); + assert!( + html.contains("
  • open task
  • "), + "unexpected task-list HTML: {html}", + ); + assert!( + html.contains("
  • completed task
  • "), + "unexpected task-list HTML: {html}", + ); + } + + #[test] + fn raw_html_checkboxes_are_not_marked_as_tasks() { + let html = convert_markdown("- raw control\n"); + assert!( + !html.contains("data-task-checkbox"), + "raw HTML control was incorrectly marked as a task: {html}", + ); + } + + #[test] + fn nested_and_quoted_task_checkboxes_are_marked() { + let html = convert_markdown("- [ ] parent\n - [x] nested\n\n> - [ ] quoted\n"); + assert_eq!( + html.matches("data-task-checkbox").count(), + 3, + "unexpected task-list HTML: {html}", + ); + } + + #[test] + fn multiline_wikilinks_do_not_shift_task_source_positions() { + let html = convert_markdown("[[#first\nsecond|alias]]\n- [ ] task\n"); + assert!( + html.contains("data-task-checkbox"), + "task source position was shifted by a multiline wikilink: {html}", + ); + } + #[test] fn embed_protection_survives_longer_backtick_runs_earlier_in_the_doc() { // A 4-backtick inline sample desynchronized the old regex pairing and @@ -303,6 +344,29 @@ mod tests { fs::remove_dir_all(root).unwrap(); fs::remove_dir_all(outside).unwrap(); } + + #[test] + fn theme_slug_collapses_punctuation_runs() { + assert_eq!(theme_slug("SynthWave '84"), "synthwave-84"); + } + + #[test] + fn display_math_keeps_multiple_braced_subscripts_out_of_markdown_emphasis() { + let html = convert_markdown("$$\\bar{b}_{1} + \\bar{b}_{2}$$\n"); + assert!( + html.contains("$$\\bar{b}_{1} + \\bar{b}_{2}$$"), + "unexpected parser output: {html}", + ); + assert!(!html.contains("(content: &'a str) -> Cow<'a, str> { processed } +const DISPLAY_MATH_UNDERSCORE_SENTINEL: &str = "\u{E000}"; + +fn protect_display_math_underscores(content: &str) -> String { + content + .split("$$") + .enumerate() + .map(|(index, segment)| { + if index % 2 == 1 { + segment.replace('_', DISPLAY_MATH_UNDERSCORE_SENTINEL) + } else { + segment.to_owned() + } + }) + .collect::>() + .join("$$") +} + #[tauri::command] fn convert_markdown(content: &str) -> String { let processed_embeds = process_internal_embeds(content); let processed_links = process_wikilinks(&processed_embeds); + let protected_math = protect_display_math_underscores(&processed_links); let mut options = ComrakOptions { extension: ComrakExtensionOptions { @@ -654,7 +736,38 @@ fn convert_markdown(content: &str) -> String { options.render.hardbreaks = true; options.render.sourcepos = true; - markdown_to_html(&processed_links, &options) + let html = markdown_to_html(&protected_math, &options) + .replace(DISPLAY_MATH_UNDERSCORE_SENTINEL, "_"); + annotate_task_checkboxes(html, content) +} + +fn annotate_task_checkboxes(html: String, markdown: &str) -> String { + let task_item = Regex::new( + r#"
  • (?)"#, + ) + .unwrap(); + let task_source = Regex::new(r"^\s*(?:>\s*)*(?:[-+*]|\d+[.)])\s+\[[ xX]\](?:\s|$)").unwrap(); + + task_item + .replace_all(&html, |captures: &Captures| { + let line = captures["line"].parse::().unwrap_or_default(); + let source_line = markdown.lines().nth(line.saturating_sub(1)); + if !source_line.is_some_and(|line| task_source.is_match(line)) { + return captures[0].to_string(); + } + + let input = captures["input"].replacen( + " disabled=\"\"", + " data-task-checkbox=\"\" disabled=\"\"", + 1, + ); + format!( + "
  • {}", + &captures["sourcepos"], + input, + ) + }) + .into_owned() } #[tauri::command] @@ -818,6 +931,15 @@ async fn get_app_mode() -> String { } } +fn theme_slug(value: &str) -> String { + let lowercase = value.to_lowercase(); + lowercase + .split(|c: char| !c.is_alphanumeric()) + .filter(|segment| !segment.is_empty()) + .collect::>() + .join("-") +} + #[tauri::command] async fn fetch_vscode_theme(app: AppHandle, url: String) -> Result { use std::io::{Cursor, Read}; @@ -892,9 +1014,7 @@ async fn fetch_vscode_theme(app: AppHandle, url: String) -> Result Some(tauri::window::Color(24, 24, 24, 255)), "light" => Some(tauri::window::Color(253, 253, 253, 255)), @@ -1467,9 +1585,7 @@ pub fn run() { let _ = window.set_background_color(bg_color); - let _ = _window.set_shadow(true); - - let window = app.get_webview_window(label).unwrap(); + let _ = window.set_shadow(true); let file_path = args.iter().skip(1).find(|arg| !arg.starts_with("-")); @@ -1569,12 +1685,12 @@ pub fn run() { .run(|_app_handle, _event| { #[cfg(target_os = "macos")] if let tauri::RunEvent::Opened { urls } = _event { - if let Some(url) = urls.first() { + for url in urls { if let Ok(path_buf) = url.to_file_path() { let path_str = path_buf.to_string_lossy().to_string(); let state = _app_handle.state::(); - *state.startup_file.lock().unwrap() = Some(path_str.clone()); + state.startup_files.lock().unwrap().push(path_str.clone()); if let Some(window) = pick_delivery_window(_app_handle) { let _ = _app_handle.emit_to(window.label(), "file-path", path_str); diff --git a/src-tauri/src/window_runtime.rs b/src-tauri/src/window_runtime.rs index a1a77709..41767b7d 100644 --- a/src-tauri/src/window_runtime.rs +++ b/src-tauri/src/window_runtime.rs @@ -21,7 +21,7 @@ impl WatcherState { } pub struct AppState { - pub(crate) startup_file: Mutex>, + pub(crate) startup_files: Mutex>, pub(crate) last_focused_viewer: Mutex>, window_registry: Mutex>, window_counter: AtomicU64, @@ -30,7 +30,7 @@ pub struct AppState { impl AppState { pub fn new() -> Self { Self { - startup_file: Mutex::new(None), + startup_files: Mutex::new(Vec::new()), last_focused_viewer: Mutex::new(None), window_registry: Mutex::new(HashMap::new()), window_counter: AtomicU64::new(0), @@ -288,10 +288,11 @@ pub fn watch_file( let mut watchers = state.watchers.lock().unwrap(); watchers.remove(&label); let event_label = label.clone(); + let watched_path = path.clone(); let mut watcher = RecommendedWatcher::new( move |result: Result| { if result.is_ok() { - let _ = handle.emit_to(event_label.as_str(), "file-changed", ()); + let _ = handle.emit_to(event_label.as_str(), "file-changed", watched_path.clone()); } }, Config::default(), @@ -314,7 +315,8 @@ pub fn send_markdown_path(state: State<'_, AppState>) -> Vec { .skip(1) .filter(|arg| !arg.starts_with('-')) .collect(); - if let Some(path) = state.startup_file.lock().unwrap().take() { + let startup_files: Vec = state.startup_files.lock().unwrap().drain(..).collect(); + for path in startup_files.into_iter().rev() { if !files.contains(&path) { files.insert(0, path); } diff --git a/src-tauri/tauri.conf.json b/src-tauri/tauri.conf.json index 3c9c6b8a..d416c61e 100644 --- a/src-tauri/tauri.conf.json +++ b/src-tauri/tauri.conf.json @@ -49,14 +49,6 @@ "name": "Markdown File", "description": "Opens Markdown files with Markpad", "role": "Editor" - }, - { - "ext": ["txt"], - "contentTypes": ["public.plain-text"], - "mimeType": "text/plain", - "name": "Text File", - "description": "Opens Text files with Markpad", - "role": "Editor" } ], "windows": { diff --git a/src/lib/MarkdownViewer.svelte b/src/lib/MarkdownViewer.svelte index d2044950..705596d7 100644 --- a/src/lib/MarkdownViewer.svelte +++ b/src/lib/MarkdownViewer.svelte @@ -51,6 +51,7 @@ import { import HomePage from './components/HomePage.svelte'; import { tabManager } from './stores/tabs.svelte.js'; import { snapshotTab } from './utils/tabTransfer.js'; +import { adjustPreviewMaxWidth, getPreviewContentWidth, getStoredPreviewFullWidth } from './utils/previewWidth.js'; import { settings } from './stores/settings.svelte.js'; import { t } from './utils/i18n.js'; import { createWindowSession } from './sessions/windowSession.svelte.js'; @@ -217,16 +218,23 @@ import { createDocumentSession, type LoadMarkdownOptions } from './sessions/docu let isAtBottom = $state(false); let showHome = $state(false); - let isFullWidth = $state(localStorage.getItem('isFullWidth') === 'true'); + let isFullWidth = $state(getStoredPreviewFullWidth( + localStorage.getItem('preview.fullWidth'), + localStorage.getItem('isFullWidth'), + )); let viewerWidth = $state(0); const TOC_MIN_WIDTH = 180; const TOC_MAX_WIDTH = 420; const TOC_RESIZE_STEP = 16; let isTocResizing = $state(false); - let isOverhanging = $derived(isFullWidth || (viewerWidth > 0 && settings.tocWidth > Math.max(50, (viewerWidth - 780) / 2))); + let previewContentWidth = $derived(getPreviewContentWidth(settings.previewMaxWidth, isFullWidth)); + let isOverhanging = $derived( + isFullWidth || (viewerWidth > 0 && previewContentWidth !== null && settings.tocWidth > Math.max(50, (viewerWidth - previewContentWidth) / 2)), + ); $effect(() => { - localStorage.setItem('isFullWidth', String(isFullWidth)); + localStorage.setItem('preview.fullWidth', String(isFullWidth)); + localStorage.removeItem('isFullWidth'); }); import { parseAndApplyVscodeTheme, clearVscodeTheme } from './utils/theme'; @@ -525,7 +533,6 @@ import { createDocumentSession, type LoadMarkdownOptions } from './sessions/docu scrollFuture = []; }, renderMarkdown: renderMarkdownPreview, - isLiveMode: () => liveMode, afterLoad: tick, saveRecentFile, deleteRecentFile, @@ -620,6 +627,14 @@ import { createDocumentSession, type LoadMarkdownOptions } from './sessions/docu findOpen = false; }); + $effect(() => { + if (liveMode && currentFile) { + invoke('watch_file', { path: currentFile }).catch(console.error); + } else { + invoke('unwatch_file').catch(console.error); + } + }); + function processHighlights(root: Element) { const walker = document.createTreeWalker(root, NodeFilter.SHOW_TEXT, { acceptNode(node) { @@ -1477,14 +1492,6 @@ import { createDocumentSession, type LoadMarkdownOptions } from './sessions/docu return; } - // task checkbox toggle in read mode - if (target.tagName === 'INPUT' && (target as HTMLInputElement).type === 'checkbox' && target.hasAttribute('data-task-checkbox')) { - e.preventDefault(); - e.stopPropagation(); - toggleTaskCheckbox(target as HTMLInputElement); - return; - } - const a = target.closest('a'); if (a) { const href = a.getAttribute('href'); @@ -1526,17 +1533,26 @@ import { createDocumentSession, type LoadMarkdownOptions } from './sessions/docu } } - async function toggleTaskCheckbox(checkbox: HTMLInputElement) { - const allBoxes = Array.from(markdownBody?.querySelectorAll('[data-task-checkbox]') || []); - const index = allBoxes.indexOf(checkbox); - if (index === -1) return; - const nowChecked = !checkbox.checked; - if (!(await documentSession.toggleTaskCheckbox(index, nowChecked))) return; - checkbox.checked = nowChecked; + async function handleTaskCheckboxChange(event: Event) { + const checkbox = event.target as HTMLInputElement; + if (checkbox.tagName !== 'INPUT' || checkbox.type !== 'checkbox' || !checkbox.hasAttribute('data-task-checkbox')) return; + + const nowChecked = checkbox.checked; + if (!(await toggleTaskCheckbox(checkbox, nowChecked))) { + checkbox.checked = !nowChecked; + } + } + + async function toggleTaskCheckbox(checkbox: HTMLInputElement, nowChecked: boolean): Promise { + const sourcePosition = checkbox.closest('li')?.getAttribute('data-sourcepos'); + const sourceLine = Number(sourcePosition?.match(/^(\d+):/)?.[1]); + if (!Number.isInteger(sourceLine) || sourceLine < 1) return false; + if (!(await documentSession.toggleTaskCheckbox(sourceLine, nowChecked))) return false; const li = checkbox.closest('li'); if (li) { li.classList.toggle('task-done', nowChecked); } + return true; } @@ -1841,13 +1857,15 @@ import { createDocumentSession, type LoadMarkdownOptions } from './sessions/docu async function selectFile() { const selected = await open({ - multiple: false, + multiple: true, filters: [ { name: 'Markdown', extensions: ['md', 'markdown', 'mdown', 'mkd'] }, { name: 'All Files', extensions: ['*'] }, ], }); - if (selected && typeof selected === 'string') loadMarkdown(selected); + if (!selected) return; + const paths = Array.isArray(selected) ? selected : [selected]; + for (const path of paths) await loadMarkdown(path); } async function reloadFromDisk() { @@ -1907,14 +1925,8 @@ import { createDocumentSession, type LoadMarkdownOptions } from './sessions/docu if (currentFile) await invoke('open_file_folder', { path: currentFile }); } - async function toggleLiveMode() { + function toggleLiveMode() { liveMode = !liveMode; - if (liveMode && currentFile) { - await invoke('watch_file', { path: currentFile }); - if (tabManager.activeTabId) await loadMarkdown(currentFile); - } else { - await invoke('unwatch_file'); - } } async function saveImageAs(src: string) { @@ -2258,6 +2270,12 @@ import { createDocumentSession, type LoadMarkdownOptions } from './sessions/docu } } + function canUsePreviewWidthShortcut(target: EventTarget | null, isSplit: boolean) { + if (showSettings || modalState.show || promptModal.show || showHome || (isEditing && !isSplit)) return false; + const element = target instanceof Element ? target : null; + return !element?.closest('input, textarea, select, [contenteditable="true"], [role="textbox"]'); + } + function handleKeyDown(e: KeyboardEvent) { if (mode !== 'app') return; @@ -2280,6 +2298,14 @@ import { createDocumentSession, type LoadMarkdownOptions } from './sessions/docu const isSplit = tabManager.activeTab?.isSplit; + if (cmdOrCtrl && e.altKey && !e.shiftKey && (code === 'BracketLeft' || code === 'BracketRight')) { + if (!canUsePreviewWidthShortcut(e.target, !!isSplit)) return; + e.preventDefault(); + isFullWidth = false; + settings.previewMaxWidth = adjustPreviewMaxWidth(settings.previewMaxWidth, code === 'BracketLeft' ? -40 : 40); + return; + } + if (!cmdOrCtrl && !e.shiftKey && !e.altKey && code === 'F5') { e.preventDefault(); reloadFromDisk(); @@ -2638,8 +2664,9 @@ import { createDocumentSession, type LoadMarkdownOptions } from './sessions/docu }), ); unlisteners.push( - await appWindow.listen('file-changed', () => { - if (!liveMode || !currentFile) return; + await appWindow.listen('file-changed', (event) => { + const changedPath = event.payload as string; + if (!liveMode || !currentFile || changedPath !== currentFile) return; // Skip events caused by our own auto-save / save invocations, // otherwise the reload would clobber any keystrokes that landed // between fs::write and this listener firing. @@ -2943,7 +2970,7 @@ import { createDocumentSession, type LoadMarkdownOptions } from './sessions/docu try { const args: string[] = await invoke('send_markdown_path'); if (!isDisposed && args?.length > 0) { - await loadMarkdown(args[0]); + for (const path of args) await loadMarkdown(path); } } catch (error) { console.error('Error receiving Markdown file path:', error); @@ -3151,13 +3178,14 @@ import { createDocumentSession, type LoadMarkdownOptions } from './sessions/docu class="markdown-body {isFullWidth ? 'full-width' : ''} {settings.showToc ? 'toc-active' : ''}" onscroll={handleScroll} onclick={handleLinkClick} + onchange={handleTaskCheckboxChange} onkeydown={(e) => { const target = e.target as HTMLElement; if (target.closest('.frontmatter-panel')) return; if(e.key === 'Enter' || e.key === ' ') handleLinkClick(e as unknown as MouseEvent); }} tabindex="-1" - style="outline: none; font-family: {settings.previewFont}, sans-serif; font-size: {settings.previewFontSize}px; flex: 1;"> + style="outline: none; font-family: {settings.previewFont}, sans-serif; font-size: {settings.previewFontSize}px; flex: 1; --preview-max-width: {previewContentWidth === null ? '100%' : `${previewContentWidth}px`};"> {#if frontMatterInfo.exists}
    { - emitSync(); - }); - const d2 = editor.onDidScrollChange((e) => { + const scrollListener = editor.onDidScrollChange((e) => { if (e.scrollTopChanged) { emitSync(); } }); return () => { - d1.dispose(); - d2.dispose(); + scrollListener.dispose(); }; } }); diff --git a/src/lib/components/Settings.svelte b/src/lib/components/Settings.svelte index f1b8d7d6..27bedb99 100644 --- a/src/lib/components/Settings.svelte +++ b/src/lib/components/Settings.svelte @@ -9,6 +9,12 @@ import type { LanguageCode } from '../utils/i18n.js'; import { getEditorToolbarTools } from '../utils/editorToolbar.js'; import { getTitlebarToolbarActions, type TitlebarToolbarPlacement } from '../utils/titlebarToolbar.js'; + import { + DEFAULT_PREVIEW_MAX_WIDTH, + MAX_PREVIEW_MAX_WIDTH, + MIN_PREVIEW_MAX_WIDTH, + normalizePreviewMaxWidth, + } from '../utils/previewWidth.js'; let { show = false, @@ -19,6 +25,10 @@ let activeCategory = $state<'editor' | 'preview' | 'appearance' | 'toolbars' | 'files'>('editor'); let highlightMenuOpen = $state(false); + + function updatePreviewMaxWidth(value: unknown) { + settings.previewMaxWidth = normalizePreviewMaxWidth(value); + } const highlightColors = [ { value: 'default', color: 'var(--color-accent-fg)' }, { value: 'yellow', color: '#ffd000' }, @@ -313,7 +323,8 @@ } function handleSettingsModalDragPointerDown(e: PointerEvent) { - if (e.button !== 0 || isSettingsHeaderInteractiveTarget(e.target)) return; + const header = e.target instanceof HTMLElement ? e.target.closest('.settings-header') : null; + if (e.button !== 0 || !header || !settingsModal?.contains(header) || isSettingsHeaderInteractiveTarget(e.target)) return; const frame = getCurrentSettingsModalFrame(); if (!frame) return; @@ -575,8 +586,9 @@ aria-modal="true" aria-labelledby="settings-title" tabindex="-1" + onpointerdown={handleSettingsModalDragPointerDown} onkeydown={handleModalKeydown}> -
    +

    {t('settings.title', settings.language)}

    +
    + +
    +
    + + updatePreviewMaxWidth(settings.previewMaxWidth)} + class="number-input" + style="width: 62px" + /> + +
    + px · {MIN_PREVIEW_MAX_WIDTH}–{MAX_PREVIEW_MAX_WIDTH} · {t('settings.default', settings.language)} {DEFAULT_PREVIEW_MAX_WIDTH} +
    +
    +
    @@ -1172,9 +1215,13 @@

    {t('settings.toolbarsSettings', settings.language)}

    -
    -
    -

    {t('settings.applicationToolbar', settings.language)}

    +
    + + + {t('settings.applicationToolbar', settings.language)} + +
    +
    {/each} +
    -
    - -
    -
    -

    {t('settings.editorToolbar', settings.language)}

    +
    + +
    + + + {t('settings.editorToolbar', settings.language)} + +
    +
    {/each} +
    - +
    {:else if activeCategory === 'files'}
    @@ -1712,10 +1765,21 @@ display: none; } - .toolbar-section { - display: flex; - flex-direction: column; - gap: 8px; + .toolbar-settings-chevron { + width: 7px; + height: 7px; + border-right: 1.5px solid var(--color-fg-muted); + border-bottom: 1.5px solid var(--color-fg-muted); + transform: rotate(-45deg); + transition: transform 0.12s ease; + } + + .toolbar-settings[open] .toolbar-settings-chevron { + transform: rotate(45deg); + } + + .toolbar-settings-body { + padding-bottom: 12px; } .toolbar-section-header { @@ -1725,15 +1789,6 @@ padding-bottom: 2px; } - .toolbar-section-header h3 { - margin: 0; - font-size: 12px; - font-weight: 600; - color: var(--color-fg-muted); - text-transform: uppercase; - letter-spacing: 0.5px; - } - .toolbar-settings-list { display: flex; flex-direction: column; diff --git a/src/lib/sessions/documentSession.svelte.ts b/src/lib/sessions/documentSession.svelte.ts index 4817f096..7c975d07 100644 --- a/src/lib/sessions/documentSession.svelte.ts +++ b/src/lib/sessions/documentSession.svelte.ts @@ -2,6 +2,7 @@ import { invoke } from '@tauri-apps/api/core'; import { save } from '@tauri-apps/plugin-dialog'; import { settings } from '../stores/settings.svelte.js'; import { tabManager } from '../stores/tabs.svelte.js'; +import { getMarkdownBodyWithoutFrontMatter } from '../utils/frontMatter.js'; import { hasMarkdownLinkExtension } from '../utils/markdownLinks.js'; export type LoadMarkdownOptions = { @@ -16,7 +17,6 @@ type DocumentSessionOptions = { currentFile: () => string; resetScrollHistory: () => void; renderMarkdown: (raw: string, path: string) => Promise; - isLiveMode: () => boolean; afterLoad: () => Promise; saveRecentFile: (path: string) => void; deleteRecentFile: (path: string) => void; @@ -137,16 +137,14 @@ export function createDocumentSession(options: DocumentSessionOptions) { if (tab) tab.isEditing = true; tabManager.setTabRawContent(activeId, content); } - if (options.isLiveMode()) invoke('watch_file', { path: filePath }).catch(console.error); await options.afterLoad(); if (filePath) options.saveRecentFile(filePath); } catch (error) { console.error('Error loading file:', error); - const errorText = String(error); - if (errorText.includes('The system cannot find the file specified') || errorText.includes('No such file or directory')) { - options.deleteRecentFile(filePath); - if (tabManager.activeTab?.path === filePath) tabManager.closeTab(tabManager.activeTab.id); - } else options.onError('Error loading file', error); + // A watcher can observe a transient gap while another process replaces + // the file. Keep the already-open buffer and recent-file entry instead + // of closing the tab and discarding the user's recovery path. + options.onError('Error loading file', error); } } @@ -212,15 +210,17 @@ export function createDocumentSession(options: DocumentSessionOptions) { } } - async function toggleTaskCheckbox(index: number, nowChecked: boolean) { + async function toggleTaskCheckbox(sourceLine: number, nowChecked: boolean) { const tab = tabManager.activeTab; if (!tab || !tab.path) return false; const raw = tab.rawContent; - let count = 0; - const updated = raw.replace(/^(\s*[-*+] )\[( |x|X)\]/gm, (match, prefix) => { - if (count++ === index) return `${prefix}[${nowChecked ? 'x' : ' '}]`; + const body = getMarkdownBodyWithoutFrontMatter(raw); + const updatedBody = body.replace(/^(\s*(?:>\s*)*(?:[-+*]|\d+[.)])\s+)\[( |x|X)\]/gm, (match, prefix, _state, offset) => { + const line = body.slice(0, offset).split('\n').length; + if (line === sourceLine) return `${prefix}[${nowChecked ? 'x' : ' '}]`; return match; }); + const updated = `${raw.slice(0, raw.length - body.length)}${updatedBody}`; if (updated === raw) return false; tabManager.updateTabRawContent(tab.id, updated); await saveContent(tab.id); diff --git a/src/lib/stores/settings.svelte.ts b/src/lib/stores/settings.svelte.ts index 30ca2f2d..1196ee3b 100644 --- a/src/lib/stores/settings.svelte.ts +++ b/src/lib/stores/settings.svelte.ts @@ -18,6 +18,10 @@ import { normalizeTitlebarToolbarPlacement, type TitlebarToolbarPlacement, } from '../utils/titlebarToolbar.js'; +import { + DEFAULT_PREVIEW_MAX_WIDTH, + normalizePreviewMaxWidth, +} from '../utils/previewWidth.js'; export type OSType = 'macos' | 'windows' | 'linux' | 'unknown'; export type LanguageCode = @@ -169,6 +173,7 @@ export class SettingsStore { newFileDefaultMode = $state(true); showRecentFiles = $state(true); editorMaxWidth = $state(80); + previewMaxWidth = $state(DEFAULT_PREVIEW_MAX_WIDTH); pinnedToc = $state(false); tocSide = $state<'left' | 'right'>('left'); tocWidth = $state(240); @@ -218,6 +223,7 @@ export class SettingsStore { const savedNewFileDefaultMode = localStorage.getItem('editor.newFileDefaultMode'); const savedShowRecentFiles = localStorage.getItem('editor.showRecentFiles'); const savedEditorMaxWidth = localStorage.getItem('editor.maxWidth'); + const savedPreviewMaxWidth = localStorage.getItem('preview.maxWidth'); const savedPinnedToc = localStorage.getItem('editor.pinnedToc'); const savedTocSide = localStorage.getItem('editor.tocSide'); const savedTocWidth = localStorage.getItem('editor.tocWidth'); @@ -287,6 +293,7 @@ export class SettingsStore { if (savedNewFileDefaultMode !== null) this.newFileDefaultMode = savedNewFileDefaultMode === 'true'; if (savedShowRecentFiles !== null) this.showRecentFiles = savedShowRecentFiles === 'true'; if (savedEditorMaxWidth !== null) this.editorMaxWidth = parseFontSize(savedEditorMaxWidth, 80, 20, 500); + this.previewMaxWidth = normalizePreviewMaxWidth(savedPreviewMaxWidth); if (savedPinnedToc !== null) this.pinnedToc = savedPinnedToc === 'true'; if (savedTocSide !== null) this.tocSide = savedTocSide as 'left' | 'right'; if (savedTocWidth !== null) this.tocWidth = parseFontSize(savedTocWidth, 240, 180, 420); @@ -361,6 +368,7 @@ export class SettingsStore { localStorage.setItem('editor.newFileDefaultMode', String(this.newFileDefaultMode)); localStorage.setItem('editor.showRecentFiles', String(this.showRecentFiles)); localStorage.setItem('editor.maxWidth', String(this.editorMaxWidth)); + localStorage.setItem('preview.maxWidth', String(this.previewMaxWidth)); localStorage.setItem('editor.pinnedToc', String(this.pinnedToc)); localStorage.setItem('editor.tocSide', this.tocSide); localStorage.setItem('editor.tocWidth', String(this.tocWidth)); @@ -583,6 +591,10 @@ export class SettingsStore { this.editorMaxWidth = 80; } + resetPreviewMaxWidth() { + this.previewMaxWidth = DEFAULT_PREVIEW_MAX_WIDTH; + } + async initOSType() { try { const osType = await invoke('get_os_type'); diff --git a/src/lib/utils/i18n.ts b/src/lib/utils/i18n.ts index 9edf629f..454ef14f 100644 --- a/src/lib/utils/i18n.ts +++ b/src/lib/utils/i18n.ts @@ -79,8 +79,10 @@ export const translations: Record = { confirmBeforeSave: 'Ask for confirmation before saving', resetEditorSettings: 'Reset editor settings', resetFontSettings: 'Reset font settings', + resetPreviewSettings: 'Reset preview settings', font: 'Font', fontSize: 'Font Size', + previewMaxWidth: 'Preview max width', wrapColumn: 'Wrap Column', wordWrap: 'Word Wrap', lineNumbers: 'Line Numbers', @@ -414,8 +416,10 @@ export const translations: Record = { appearanceSettings: '外观设置', resetEditorSettings: '重置编辑器设置', resetFontSettings: '重置字体设置', + resetPreviewSettings: '重置预览设置', font: '字体', fontSize: '字体大小', + previewMaxWidth: '预览最大宽度', wrapColumn: '换行列', wordWrap: '自动换行', lineNumbers: '行号', diff --git a/src/lib/utils/markdown.ts b/src/lib/utils/markdown.ts index d1a040b3..e8f5e9ef 100644 --- a/src/lib/utils/markdown.ts +++ b/src/lib/utils/markdown.ts @@ -361,43 +361,16 @@ function stripLeadingWhitespace(nodes: Node[]) { } } -function isWhitespaceText(node: Node) { - return node.nodeType === 3 && !node.textContent?.trim(); -} - -function startsWithNode(element: Element, target: Node) { - const firstContentNode = Array.from(element.childNodes).find( - (node) => !isWhitespaceText(node), - ); - return firstContentNode === target; -} - -function isTaskCheckbox(input: Element, li: Element) { - const looksRenderedByTaskList = - input.hasAttribute("data-task-checkbox") || - li.classList.contains("task-list-item"); - if (!looksRenderedByTaskList) return false; - - const inputParent = input.parentElement; - if (inputParent === li) { - return startsWithNode(li, input); - } - - return ( - inputParent?.tagName === "P" && - inputParent.parentElement === li && - startsWithNode(li, inputParent) && - startsWithNode(inputParent, input) - ); -} - function processTaskItems(root: Element) { for (const input of Array.from( root.querySelectorAll('li input[type="checkbox"]'), )) { const li = input.closest("li"); if (!li) continue; - if (!isTaskCheckbox(input, li)) continue; + if (!input.hasAttribute("data-task-checkbox")) { + input.setAttribute("disabled", ""); + continue; + } input.setAttribute("data-task-checkbox", ""); input.removeAttribute("disabled"); diff --git a/src/lib/utils/previewWidth.ts b/src/lib/utils/previewWidth.ts new file mode 100644 index 00000000..2ff082f6 --- /dev/null +++ b/src/lib/utils/previewWidth.ts @@ -0,0 +1,22 @@ +export const MIN_PREVIEW_MAX_WIDTH = 640; +export const MAX_PREVIEW_MAX_WIDTH = 1600; +export const DEFAULT_PREVIEW_MAX_WIDTH = 880; + +export function normalizePreviewMaxWidth(value: unknown): number { + if (value === null || value === '') return DEFAULT_PREVIEW_MAX_WIDTH; + const parsed = typeof value === 'number' ? value : Number(value); + if (!Number.isFinite(parsed)) return DEFAULT_PREVIEW_MAX_WIDTH; + return Math.min(MAX_PREVIEW_MAX_WIDTH, Math.max(MIN_PREVIEW_MAX_WIDTH, Math.round(parsed))); +} + +export function getPreviewContentWidth(value: unknown, isFullWidth: boolean): number | null { + return isFullWidth ? null : normalizePreviewMaxWidth(value); +} + +export function adjustPreviewMaxWidth(value: unknown, delta: number): number { + return normalizePreviewMaxWidth(normalizePreviewMaxWidth(value) + delta); +} + +export function getStoredPreviewFullWidth(value: string | null, legacyValue: string | null): boolean { + return (value ?? legacyValue) === 'true'; +} diff --git a/src/styles.css b/src/styles.css index ff43128c..13c00e7d 100644 --- a/src/styles.css +++ b/src/styles.css @@ -1438,6 +1438,12 @@ body { border-radius: 0 !important; } + /* The viewer pane carries an inline split ratio during normal interaction. + Width alone cannot override that flex value in the print layout. */ + .pane.viewer-pane { + flex: none !important; + } + .markdown-body { padding: 0 !important; margin: 0 !important;