From 0ffaf6fbe4e4cc14b8262ed5a75deaff63c0f302 Mon Sep 17 00:00:00 2001 From: dani-polani Date: Tue, 20 Oct 2026 02:41:34 +0300 Subject: [PATCH 1/4] fix(columns): lay out joined morphemes of upright words as a staircase MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit In column mode a morpheme joined with a split character took its own row under the word, so `ĥuʒaba|x` read as two words. Upright words are now staircases: each piece keeps its own row, so connectors still reach both of its sides, and starts where the previous piece ends. RTL lines step right to left. Vertical and sideways lines already continued the word along the column and are unchanged. Co-Authored-By: Claude Opus 5.5 (1M context) --- bitext/src/app.css | 7 ++ .../lib/components/preview/TokenRow.svelte | 77 ++++++++++++++----- bitext/src/lib/domain/tokens.test.ts | 26 ++++++- bitext/src/lib/domain/tokens.ts | 17 ++++ 4 files changed, 106 insertions(+), 21 deletions(-) diff --git a/bitext/src/app.css b/bitext/src/app.css index 0bf5577..ba5547c 100644 --- a/bitext/src/app.css +++ b/bitext/src/app.css @@ -425,6 +425,13 @@ path.link-path { align-items: center; } +/* Upright word in a column: each joined piece sits in its own row and column of the grid, which + lays the pieces out as a staircase. `dir="rtl"` on the word runs the steps right to left. */ +.token-row__word { + display: inline-grid; + writing-mode: horizontal-tb; +} + /* Glyph setting per line. `upright` is explicit so it resets the inherited vertical row above. */ .token-view--upright { writing-mode: horizontal-tb; diff --git a/bitext/src/lib/components/preview/TokenRow.svelte b/bitext/src/lib/components/preview/TokenRow.svelte index 03c21e9..61b3aa8 100644 --- a/bitext/src/lib/components/preview/TokenRow.svelte +++ b/bitext/src/lib/components/preview/TokenRow.svelte @@ -1,5 +1,5 @@
- {#each tokens as t, i (t.id)} - {@const nextTok = tokens[i + 1]} - - + - - {/each} + style:margin-top="{wi === 0 ? 0 : gapWordPx}px" + > + {#each word as { token, index }, k (token.id)} + + 0} + joinTightEnd={k < word.length - 1} + /> + + {/each} + + {/each} + {:else} + {#each tokens as t, i (t.id)} + {@const nextTok = tokens[i + 1]} + + + + {/each} + {/if}
diff --git a/bitext/src/lib/domain/tokens.test.ts b/bitext/src/lib/domain/tokens.test.ts index e3e04e9..5f59018 100644 --- a/bitext/src/lib/domain/tokens.test.ts +++ b/bitext/src/lib/domain/tokens.test.ts @@ -1,5 +1,5 @@ import { describe, expect, it } from 'vitest'; -import { tokenize, tokenizeOptionsFromVisualSettings } from './tokens.js'; +import { groupJoinedTokens, tokenize, tokenizeOptionsFromVisualSettings } from './tokens.js'; const base = () => tokenizeOptionsFromVisualSettings({ @@ -102,3 +102,27 @@ describe('tokenize', () => { expect(t3.map((x) => x.text)).toEqual(['Hi', ',', 'there']); }); }); + +describe('groupJoinedTokens', () => { + const opts = () => ({ ...base(), splitChars: '|' }); + const words = (raw: string) => + groupJoinedTokens(tokenize(raw, 'L', opts())).map((w) => + w.map(({ token, index }) => `${index}:${token.text}`) + ); + + it('keeps each joined morpheme with the word it attaches to', () => { + expect(words('ĥuʒaba|x la|x|y forêt')).toEqual([ + ['0:ĥuʒaba', '1:x'], + ['2:la', '3:x', '4:y'], + ['5:forêt'] + ]); + }); + + it('starts a word at a leading joined token instead of dropping it', () => { + expect(words('|x la')).toEqual([['0:x'], ['1:la']]); + }); + + it('returns no words for an empty line', () => { + expect(groupJoinedTokens([])).toEqual([]); + }); +}); diff --git a/bitext/src/lib/domain/tokens.ts b/bitext/src/lib/domain/tokens.ts index 5457b0c..e2eacef 100644 --- a/bitext/src/lib/domain/tokens.ts +++ b/bitext/src/lib/domain/tokens.ts @@ -166,6 +166,23 @@ export function tokenize(raw: string, lineId: string, opts: TokenizeOptions): To return out; } +export interface IndexedToken { + token: Token; + /** Position of the token in its line. */ + index: number; +} + +/** Split a line's tokens into words: each token plus the tokens joined to it with `joinLeft`. */ +export function groupJoinedTokens(tokens: Token[]): IndexedToken[][] { + const words: IndexedToken[][] = []; + tokens.forEach((token, index) => { + const current = words[words.length - 1]; + if (current && token.joinLeft) current.push({ token, index }); + else words.push([{ token, index }]); + }); + return words; +} + /** * Preserve token ids when text edits add/remove words. */ From e39271b1573db447c3caadfccbb11e4529cae682 Mon Sep 17 00:00:00 2001 From: dani-polani Date: Tue, 20 Oct 2026 02:41:34 +0300 Subject: [PATCH 2/4] fix(columns): never wrap columns; shrink the diagram to the frame width Columns used to wrap onto a new row when they did not fit, so the last column landed below the others with links across the whole diagram, and the export copied that. Columns now always stay in one row. With auto-fit, text and the gaps between columns shrink until the diagram fits the frame's width; without it, or when even the minimum scale does not fit, the frame scrolls sideways. Auto-fit now re-measures until its scales settle, which width fitting needs. That exposed a feedback loop in readonly column embeds: the frame hugs its content, so fitting to its height shrank the text on every pass (36px down to 9px for the Japanese gallery example). Readonly columns no longer fit to height. Co-Authored-By: Claude Opus 5.5 (1M context) --- bitext/src/app.css | 36 ++- .../preview/AlignmentPreview.svelte | 258 ++++++++++-------- .../preview/LinePairGapSlider.svelte | 12 +- bitext/src/lib/domain/autofit.test.ts | 35 ++- bitext/src/lib/domain/autofit.ts | 14 + bitext/src/routes/+page.svelte | 2 +- 6 files changed, 234 insertions(+), 123 deletions(-) diff --git a/bitext/src/app.css b/bitext/src/app.css index ba5547c..de96a95 100644 --- a/bitext/src/app.css +++ b/bitext/src/app.css @@ -244,15 +244,37 @@ html { align-items: stretch; } +/* Rows: the wrapper around the lines takes no part in layout. */ +.preview-lines { + display: contents; +} + /* Column mode: lines sit side by side, first line leftmost — the same "first is at the start of the stack" rule as rows. Which script ends up on which side is set by line order. */ /* Columns stretch to the tallest one rather than to a percentage height, so the same rules work whether the frame is bounded (editor) or hugs its content (readonly embed, headless render). */ -.preview-stack--columns { - flex-direction: row; - flex-wrap: wrap; +.preview-stack--columns .preview-lines { + display: flex; + flex: 1; + min-height: 0; align-items: stretch; - justify-content: center; + /* Columns never wrap: a wrapped column would be linked across the whole diagram. Auto-fit + shrinks it to the frame's width; when it still does not fit, the frame scrolls. Auto margins + center a narrow diagram and let a wide one start at the left edge, so all of it scrolls into + view. */ + width: max-content; + margin-inline: auto; +} + +/* While pinch-zoomed, panning moves the diagram; a scrollbar would also grow with the zoom. */ +.preview-frame[data-aligner-axis='columns']:not([data-zoomed]) { + overflow-x: auto; +} + +/* The connector layer covers the zoom box, so the box grows with a diagram wider than the frame. */ +.preview-frame[data-aligner-axis='columns'] .preview-zoom { + width: max-content; + min-width: 100%; } /* Column mode grows sideways, so the block axis needs a bound for auto-fit to shrink a long @@ -277,12 +299,6 @@ html { align-items: center; } -/* The credit is a flex item of the stack, so in a row it would sit beside the diagram. Give it a - full basis and it wraps onto its own line underneath. */ -.preview-stack--columns .preview-frame__attribution { - flex-basis: 100%; -} - .preview-frame__attribution { position: relative; z-index: 3; diff --git a/bitext/src/lib/components/preview/AlignmentPreview.svelte b/bitext/src/lib/components/preview/AlignmentPreview.svelte index c7ad85d..3121fd8 100644 --- a/bitext/src/lib/components/preview/AlignmentPreview.svelte +++ b/bitext/src/lib/components/preview/AlignmentPreview.svelte @@ -19,6 +19,7 @@ } from '$lib/domain/styles.js'; import { computeAutoFitScales, + computeWidthFitCap, scalesChanged, chromeScale, AUTOFIT_LINE_STRENGTH, @@ -50,6 +51,7 @@ } = $props(); let rootEl = $state(null); + let linesEl = $state(null); const style = $derived(getStyle(settingsStore.settings.style)); const resolvedCanvas = $derived( @@ -80,6 +82,10 @@ const autoFit = $derived(settingsStore.settings.autoFit); const autoFitVariance = $derived(settingsStore.settings.autoFitVariance); let effScale = $state>({}); + /** Column mode only: uniform cap on line scales and column gaps so the diagram fits the width. */ + let widthFit = $state(1); + /** Each pass re-measures after the previous one applied its scales; this bounds the loop. */ + const MAX_AUTOFIT_PASSES = 12; function scaleFor(id: string): number { return autoFit ? (effScale[id] ?? 1) : 1; @@ -109,6 +115,7 @@ void settingsStore.settings.tokenSplitChars; void settingsStore.settings.style; void settingsStore.settings.layoutAxis; + void projectStore.linePairGaps; void writesExportLayout; void layoutExportStore.layoutRemeasureTick; @@ -116,6 +123,7 @@ if (!autoFit) { if (Object.keys(effScale).length) effScale = {}; + widthFit = 1; if (writesExportLayout) { layoutExportStore.setFontScaleByLine({}); layoutExportStore.setContentScale(1); @@ -123,8 +131,13 @@ return; } + let passes = 0; + function scheduleRecompute() { + requestAnimationFrame(() => requestAnimationFrame(() => recompute())); + } + function recompute() { - if (!rootEl) return; + if (cancelled || !rootEl) return; // Auto-fit works along the flow axis: row width in `rows`, column height in `columns`. const vertical = settingsStore.settings.layoutAxis === 'columns'; const rows: { lineId: string; extent: number; effScale: number }[] = []; @@ -144,8 +157,31 @@ }); }); if (!rows.length || !Number.isFinite(avail)) return; - const next = computeAutoFitScales(rows, avail * 0.98, autoFitVariance); - if (scalesChanged(effScale, next)) effScale = next; + // A readonly column diagram hugs its content, so its height is no bound: fitting into it + // shrank the text on every pass, as each pass also shrank the frame. + const next = + vertical && readonly + ? Object.fromEntries(rows.map((r) => [r.lineId, 1])) + : computeAutoFitScales(rows, avail * 0.98, autoFitVariance); + // Columns sit side by side, so the whole diagram must also fit the frame's width. + let nextWidthFit = 1; + if (vertical && linesEl) { + const frameStyle = getComputedStyle(rootEl); + const availWidth = + rootEl.clientWidth - + parseFloat(frameStyle.paddingLeft) - + parseFloat(frameStyle.paddingRight); + nextWidthFit = computeWidthFitCap(widthFit, linesEl.offsetWidth, availWidth * 0.98); + for (const id in next) next[id] = Math.min(next[id]!, nextWidthFit); + } + const changed = + scalesChanged(effScale, next) || scalesChanged({ w: widthFit }, { w: nextWidthFit }); + if (changed) { + effScale = next; + widthFit = nextWidthFit; + passes++; + if (passes < MAX_AUTOFIT_PASSES) scheduleRecompute(); + } if (writesExportLayout) { layoutExportStore.setFontScaleByLine(effScale); const vals = Object.values(effScale); @@ -155,23 +191,27 @@ } } + /** A new size or font starts a fresh round of passes. */ + function remeasure() { + passes = 0; + recompute(); + } + + let cancelled = false; const ro = new ResizeObserver(() => { - requestAnimationFrame(() => recompute()); + requestAnimationFrame(() => remeasure()); }); ro.observe(rootEl); - requestAnimationFrame(() => requestAnimationFrame(() => recompute())); + scheduleRecompute(); - let cancelled = false; if (document.fonts) { - void document.fonts.ready.then(() => { - if (!cancelled) recompute(); - }); - document.fonts.addEventListener('loadingdone', recompute); + void document.fonts.ready.then(() => remeasure()); + document.fonts.addEventListener('loadingdone', remeasure); } return () => { cancelled = true; ro.disconnect(); - if (document.fonts) document.fonts.removeEventListener('loadingdone', recompute); + if (document.fonts) document.fonts.removeEventListener('loadingdone', remeasure); }; }); @@ -300,9 +340,10 @@ data-aligner-axis={axis} data-aligner-readonly={readonly ? '' : undefined} data-autofit={autoFit ? 'on' : 'off'} + data-zoomed={zoom.z > 1 ? '' : undefined} style:background={plainCanvas ? undefined : canvas.previewBackground} style:color={plainCanvas ? undefined : canvas.textColor} - style:touch-action={zoom.z > 1 ? 'none' : 'pan-y'} + style:touch-action={zoom.z > 1 ? 'none' : columns ? 'pan-x pan-y' : 'pan-y'} onpointerdown={onPointerDown} onpointermove={onPointerMove} onpointerup={onPointerUp} @@ -337,110 +378,113 @@ {/if}
- {#if !readonly} -
- -
- {/if} - {#each projectStore.lines as line, li (line.id)} - {@const gearDomId = `${instancePrefix}-line-gear-${line.id}`} - {@const pending = selectionStore.pending} - {@const rowDimmed = - pending != null && !lineIsLinkTargetWhilePending(lineIds, pending.lineId, line.id)} -
- {#if !readonly} -
= MAX_LINES} + onclick={() => projectStore.addLine(0)} > - +
+ {/if} + {#each projectStore.lines as line, li (line.id)} + {@const gearDomId = `${instancePrefix}-line-gear-${line.id}`} + {@const pending = selectionStore.pending} + {@const rowDimmed = + pending != null && !lineIsLinkTargetWhilePending(lineIds, pending.lineId, line.id)} +
+ {#if !readonly} +
+ +
+ {/if} +
+
- {/if} -
- + +
+ {/if} +
+ {#if li < projectStore.lines.length - 1} + {@const lowerLine = projectStore.lines[li + 1]!} + -
- {#if !readonly} -
- -
{/if} -
- {#if li < projectStore.lines.length - 1} - {@const lowerLine = projectStore.lines[li + 1]!} - - {/if} - {/each} - {#if !readonly} -
- -
- {/if} + +
+ {/if} + {#if hideChrome}

Created with diff --git a/bitext/src/lib/components/preview/LinePairGapSlider.svelte b/bitext/src/lib/components/preview/LinePairGapSlider.svelte index 92e31cb..de37402 100644 --- a/bitext/src/lib/components/preview/LinePairGapSlider.svelte +++ b/bitext/src/lib/components/preview/LinePairGapSlider.svelte @@ -9,6 +9,7 @@ lowerLineId, previewDark = false, axis = 'rows', + gapScale = 1, showControls = true }: { upperLineId: string; @@ -16,6 +17,8 @@ previewDark?: boolean; /** `rows` reserves gap height between stacked rows; `columns` reserves width between columns. */ axis?: LayoutAxis; + /** Auto-fit shrink applied to the reserved gap; the control keeps showing the set value. */ + gapScale?: number; /** When false, only reserve the gap (gallery / headless render). */ showControls?: boolean; } = $props(); @@ -27,6 +30,7 @@ void projectStore.linePairGaps; return projectStore.lineGapPxBetween(upperLineId, lowerLineId); }); + const reservedPx = $derived(gapPx * gapScale);

-
+
Date: Tue, 20 Oct 2026 03:08:19 +0300 Subject: [PATCH 3/4] fix(columns): stop width fitting at half size and scroll the rest On a phone the per-line buttons alone can be wider than the frame, so width fitting drove the text down to the 0.1 minimum (3px for the Mongolian example) and the diagram still did not fit. Width fitting now stops at half size; whatever does not fit then scrolls. Co-Authored-By: Claude Opus 5.5 (1M context) --- bitext/src/lib/domain/autofit.test.ts | 12 +++++++++--- bitext/src/lib/domain/autofit.ts | 14 +++++++++++++- 2 files changed, 22 insertions(+), 4 deletions(-) diff --git a/bitext/src/lib/domain/autofit.test.ts b/bitext/src/lib/domain/autofit.test.ts index b1a9e99..7fdbf94 100644 --- a/bitext/src/lib/domain/autofit.test.ts +++ b/bitext/src/lib/domain/autofit.test.ts @@ -1,5 +1,11 @@ import { describe, expect, it } from 'vitest'; -import { chromeScale, computeAutoFitScales, computeWidthFitCap, scalesChanged } from './autofit.js'; +import { + chromeScale, + computeAutoFitScales, + computeWidthFitCap, + scalesChanged, + WIDTH_FIT_MIN_SCALE +} from './autofit.js'; describe('computeAutoFitScales', () => { it('leaves lines that fit at scale 1', () => { @@ -91,7 +97,7 @@ describe('computeWidthFitCap', () => { it('grows back to 1 once the diagram fits, without overshooting the width', () => { const avail = 1000; - let cap = 0.3; + let cap = WIDTH_FIT_MIN_SCALE; for (let pass = 0; pass < 10; pass++) { cap = computeWidthFitCap(cap, widthAt(cap, 300, 600), avail); expect(widthAt(cap, 300, 600)).toBeLessThanOrEqual(avail); @@ -100,7 +106,7 @@ describe('computeWidthFitCap', () => { }); it('stops at the minimum scale when even that cannot fit', () => { - expect(computeWidthFitCap(0.12, 5000, 400)).toBe(0.1); + expect(computeWidthFitCap(0.6, 5000, 400)).toBe(WIDTH_FIT_MIN_SCALE); }); it('keeps the previous cap while nothing is measured yet', () => { diff --git a/bitext/src/lib/domain/autofit.ts b/bitext/src/lib/domain/autofit.ts index 62ef9a8..92223da 100644 --- a/bitext/src/lib/domain/autofit.ts +++ b/bitext/src/lib/domain/autofit.ts @@ -51,6 +51,13 @@ export function computeAutoFitScales( return out; } +/** + * Width fitting stops here and leaves the rest to scrolling. On a phone the line buttons alone can + * be wider than the frame, and shrinking text further would make it unreadable without making the + * diagram fit. + */ +export const WIDTH_FIT_MIN_SCALE = 0.5; + /** * Column mode also has to fit the frame's width, because lines sit side by side. Returns the * uniform cap on every line's scale (also applied to the gaps between columns) that brings @@ -60,7 +67,12 @@ export function computeAutoFitScales( * re-measures and steps again until the cap settles. Growing back never overshoots for the same * reason. */ -export function computeWidthFitCap(prev: number, extent: number, avail: number, min = 0.1): number { +export function computeWidthFitCap( + prev: number, + extent: number, + avail: number, + min = WIDTH_FIT_MIN_SCALE +): number { if (extent <= 0 || avail <= 0) return prev; return clamp((prev * avail) / extent, min, 1); } From 524e19e82c0f0e5cd0e2a5d3ede18ae3167205b1 Mon Sep 17 00:00:00 2001 From: dani-polani Date: Tue, 20 Oct 2026 03:13:24 +0300 Subject: [PATCH 4/4] fix(deps): bump devalue, dompurify, source-map-js and others for new advisories `npm audit fix` without breaking changes: patch and minor bumps of @sveltejs/kit, vitest, brace-expansion, devalue, dompurify, fflate, postcss-selector-parser and source-map-js. Three low advisories in `cookie` remain; they need a major bump. Co-Authored-By: Claude Opus 5.5 (1M context) --- bitext/package-lock.json | 142 +++++++++++++++++++-------------------- 1 file changed, 71 insertions(+), 71 deletions(-) diff --git a/bitext/package-lock.json b/bitext/package-lock.json index 2b80a1a..27cbdb3 100644 --- a/bitext/package-lock.json +++ b/bitext/package-lock.json @@ -2453,9 +2453,9 @@ } }, "node_modules/@sveltejs/kit": { - "version": "2.70.2", - "resolved": "https://registry.npmjs.org/@sveltejs/kit/-/kit-2.70.2.tgz", - "integrity": "sha512-RzRoRpuR2KXqc5yMO0akQHDZeT4AslOlznGITURsqHaVbtyYP4Wn3eE3gxj9JcDyNYO0crkxhdwFHc+2vkVm6w==", + "version": "2.70.3", + "resolved": "https://registry.npmjs.org/@sveltejs/kit/-/kit-2.70.3.tgz", + "integrity": "sha512-UDvEYuZqAMbfB/oXIoqKvbKcb7YczK5zYrzmsGV1zRJk03jntwp8dXiYoIJotxAndsKvcPFtx9H1GRSKFdSHgg==", "dev": true, "license": "MIT", "dependencies": { @@ -3139,16 +3139,16 @@ } }, "node_modules/@vitest/expect": { - "version": "4.1.4", - "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-4.1.4.tgz", - "integrity": "sha512-iPBpra+VDuXmBFI3FMKHSFXp3Gx5HfmSCE8X67Dn+bwephCnQCaB7qWK2ldHa+8ncN8hJU8VTMcxjPpyMkUjww==", + "version": "4.1.11", + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-4.1.11.tgz", + "integrity": "sha512-VX2x5vNJXET47KAFzwERI+KRMtTTCSWTfSMKsW7JsUsXV4psq++e3DvZpuTDOpHcxytiDs6p2nhVb2tVDiiUYw==", "dev": true, "license": "MIT", "dependencies": { "@standard-schema/spec": "^1.1.0", "@types/chai": "^5.2.2", - "@vitest/spy": "4.1.4", - "@vitest/utils": "4.1.4", + "@vitest/spy": "4.1.11", + "@vitest/utils": "4.1.11", "chai": "^6.2.2", "tinyrainbow": "^3.1.0" }, @@ -3157,13 +3157,13 @@ } }, "node_modules/@vitest/mocker": { - "version": "4.1.4", - "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-4.1.4.tgz", - "integrity": "sha512-R9HTZBhW6yCSGbGQnDnH3QHfJxokKN4KB+Yvk9Q1le7eQNYwiCyKxmLmurSpFy6BzJanSLuEUDrD+j97Q+ZLPg==", + "version": "4.1.11", + "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-4.1.11.tgz", + "integrity": "sha512-2XJVD55d1o5AZous5CCGKS74g/riOj9odEt2bQpCVZeblHyHdnMeFl4jl0XjU21stf4mbjUkew2eXQZt65g5CQ==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/spy": "4.1.4", + "@vitest/spy": "4.1.11", "estree-walker": "^3.0.3", "magic-string": "^0.30.21" }, @@ -3194,9 +3194,9 @@ } }, "node_modules/@vitest/pretty-format": { - "version": "4.1.4", - "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-4.1.4.tgz", - "integrity": "sha512-ddmDHU0gjEUyEVLxtZa7xamrpIefdEETu3nZjWtHeZX4QxqJ7tRxSteHVXJOcr8jhiLoGAhkK4WJ3WqBpjx42A==", + "version": "4.1.11", + "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-4.1.11.tgz", + "integrity": "sha512-yiZzPbGTS9Sr/JpFl8zHrcIkAofNbFV6k21vIgQN/cY/oxZeXhJv5sc/MBJ5jFKWmWs+oJHw0UXLZjmf931+Vw==", "dev": true, "license": "MIT", "dependencies": { @@ -3207,13 +3207,13 @@ } }, "node_modules/@vitest/runner": { - "version": "4.1.4", - "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-4.1.4.tgz", - "integrity": "sha512-xTp7VZ5aXP5ZJrn15UtJUWlx6qXLnGtF6jNxHepdPHpMfz/aVPx+htHtgcAL2mDXJgKhpoo2e9/hVJsIeFbytQ==", + "version": "4.1.11", + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-4.1.11.tgz", + "integrity": "sha512-LztvUgdwMNJMIkj3hQnnxiC2Xy1zNxq928W/xhjCLaNCzqTZOudjwbQf6v9IntZGPw132i2Lq2rgTRZHD3JHNw==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/utils": "4.1.4", + "@vitest/utils": "4.1.11", "pathe": "^2.0.3" }, "funding": { @@ -3221,14 +3221,14 @@ } }, "node_modules/@vitest/snapshot": { - "version": "4.1.4", - "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-4.1.4.tgz", - "integrity": "sha512-MCjCFgaS8aZz+m5nTcEcgk/xhWv0rEH4Yl53PPlMXOZ1/Ka2VcZU6CJ+MgYCZbcJvzGhQRjVrGQNZqkGPttIKw==", + "version": "4.1.11", + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-4.1.11.tgz", + "integrity": "sha512-pN7ikn1ON7h8ee4gIAp4AzyK+zBtJPzVbqOgu5LCEh4VaJVbPQcgYQYJIMGQPXVeJJq1fnfazis7a5pFNPahog==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/pretty-format": "4.1.4", - "@vitest/utils": "4.1.4", + "@vitest/pretty-format": "4.1.11", + "@vitest/utils": "4.1.11", "magic-string": "^0.30.21", "pathe": "^2.0.3" }, @@ -3237,9 +3237,9 @@ } }, "node_modules/@vitest/spy": { - "version": "4.1.4", - "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-4.1.4.tgz", - "integrity": "sha512-XxNdAsKW7C+FLydqFJLb5KhJtl3PGCMmYwFRfhvIgxJvLSXhhVI1zM8f1qD3Zg7RCjTSzDVyct6sghs9UEgBEQ==", + "version": "4.1.11", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-4.1.11.tgz", + "integrity": "sha512-apNa/prQy2qCeywhnixOHPRCgGNhvg7T4Dapfl1GahLp/R+uhBm5cPyFoNVyqsNd2h1nJxL6BqqdIjiABL60YA==", "dev": true, "license": "MIT", "funding": { @@ -3247,13 +3247,13 @@ } }, "node_modules/@vitest/utils": { - "version": "4.1.4", - "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-4.1.4.tgz", - "integrity": "sha512-13QMT+eysM5uVGa1rG4kegGYNp6cnQcsTc67ELFbhNLQO+vgsygtYJx2khvdt4gVQqSSpC/KT5FZZxUpP3Oatw==", + "version": "4.1.11", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-4.1.11.tgz", + "integrity": "sha512-zTCVGpyFsGWBhllOyKlTw/vnr6D9qxsfSDyfbyZmTyjHw5N/VuvzHpHoQjm2ZJzn4RJgx5w4r7V0er69CmLgPQ==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/pretty-format": "4.1.4", + "@vitest/pretty-format": "4.1.11", "convert-source-map": "^2.0.0", "tinyrainbow": "^3.1.0" }, @@ -3389,9 +3389,9 @@ "license": "MIT" }, "node_modules/brace-expansion": { - "version": "5.0.9", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.9.tgz", - "integrity": "sha512-ScQ4IuvIEF1TMlP7Zt+vjJ//9zlPb2SDcxWxM3bk8s6t6GGdJ7KO1dCcTidOPJKePW30LE/2cT7wCyPho9/Wxg==", + "version": "5.0.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.12.tgz", + "integrity": "sha512-YovQ3rzhaLMIrDjNDMkNS01tea93qhEhG5xy8f6+R0l+dw3Ki+5sCoIoI942iuLZTHWogWktgwVDhU09iNEimQ==", "dev": true, "license": "MIT", "dependencies": { @@ -3411,9 +3411,9 @@ } }, "node_modules/chai": { - "version": "6.2.2", - "resolved": "https://registry.npmjs.org/chai/-/chai-6.2.2.tgz", - "integrity": "sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg==", + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/chai/-/chai-6.3.0.tgz", + "integrity": "sha512-XWAtwJ6OHO+tj0EKCs0Y2UamnyOxseZWltU4x2U2wh8g4AigdjwvtUjvLP2tqkA/avxHEtzxNaqGq/YGNwckKg==", "dev": true, "license": "MIT", "engines": { @@ -3612,9 +3612,9 @@ } }, "node_modules/devalue": { - "version": "5.8.1", - "resolved": "https://registry.npmjs.org/devalue/-/devalue-5.8.1.tgz", - "integrity": "sha512-4CXDYRBGqN+57wVJkuXBYmpAVUSg3L6JAQa/DFqm238G73E1wuyc/JhGQJzN7vUf/CMphYau2zXbfWzDR5aTEw==", + "version": "5.9.4", + "resolved": "https://registry.npmjs.org/devalue/-/devalue-5.9.4.tgz", + "integrity": "sha512-sPAT4pztbu6586/hrhOnMKS17IJrvg12mXiSPSS3W5qDeN2RGgvZ0diZCm31dBbnevfVmujNO3IM2wrS4Y2Rhg==", "dev": true, "license": "MIT" }, @@ -3625,9 +3625,9 @@ "license": "MIT" }, "node_modules/dompurify": { - "version": "3.4.13", - "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.4.13.tgz", - "integrity": "sha512-2vmYIoqjze2d+kakP8S/nS5shfsl587kzwEjcGlTdiksUVgFHnFCsLYDVj/JNqJVOQZGSYBTmuycv0PodwmnMQ==", + "version": "3.4.17", + "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.4.17.tgz", + "integrity": "sha512-B/pSlYVHQ58kqISD/yDjuJ1yfzI34m1gF4lAod2bwSRhiN4K5Cl3H8Af7hqB2PmTL911tdEudnnXn5FnhRoz0w==", "license": "(MPL-2.0 OR Apache-2.0)", "optional": true, "optionalDependencies": { @@ -4067,9 +4067,9 @@ } }, "node_modules/fflate": { - "version": "0.8.2", - "resolved": "https://registry.npmjs.org/fflate/-/fflate-0.8.2.tgz", - "integrity": "sha512-cPJU47OaAoCbg0pBvzsgpTPhmhqI5eJjh/JIu8tPj5q+T7iLvW/JAYUqmE7KOB4R1ZyEhzBaIQpQpardBF5z8A==", + "version": "0.8.3", + "resolved": "https://registry.npmjs.org/fflate/-/fflate-0.8.3.tgz", + "integrity": "sha512-tbZNuJrLwGUp3zshBtdy4W+ORxZuIh8a5ilyIEQDC5rY1f3U20JMry0Ll3WBzU58EZKsEuJFXhb5gwv8CsPvgA==", "license": "MIT" }, "node_modules/file-entry-cache": { @@ -5304,9 +5304,9 @@ } }, "node_modules/postcss-selector-parser": { - "version": "7.1.1", - "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-7.1.1.tgz", - "integrity": "sha512-orRsuYpJVw8LdAwqqLykBj9ecS5/cRHlI5+nvTo8LcCKmzDmqVORXtOIYEEQuL9D4BxtA1lm5isAqzQZCoQ6Eg==", + "version": "7.1.6", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-7.1.6.tgz", + "integrity": "sha512-7qASPzhKF2l2KLboRZux8CCTRMdGiV08vWmyKzPz22qZ7ZjQBOeY7rNzNoCLSUiftJ7HUq0GERHmxw/t0dCdMw==", "dev": true, "license": "MIT", "dependencies": { @@ -5619,9 +5619,9 @@ } }, "node_modules/source-map-js": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", - "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.2.tgz", + "integrity": "sha512-KGj/8Y43x35aZVDtt+J4mK1hoLGHULMYfSkODJNQjNDC3oW1PqPoxMwo0pLUsWM/UEGzON/NxeHywEfNXNP3Vw==", "dev": true, "license": "BSD-3-Clause", "engines": { @@ -5950,9 +5950,9 @@ } }, "node_modules/tinyrainbow": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-3.1.0.tgz", - "integrity": "sha512-Bf+ILmBgretUrdJxzXM0SgXLZ3XfiaUuOj/IKQHuTXip+05Xn+uyEYdVg0kYDipTBcLrCVyUzAPz7QmArb0mmw==", + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-3.2.0.tgz", + "integrity": "sha512-LgO3D9yZJjApUiuUfl9iFAwrtaX4+lok3wJIqttGoKCHlWUqHqbQpnxCf82L8FjgKsh4iGo78hqJwgL8F6To2A==", "dev": true, "license": "MIT", "engines": { @@ -6191,19 +6191,19 @@ } }, "node_modules/vitest": { - "version": "4.1.4", - "resolved": "https://registry.npmjs.org/vitest/-/vitest-4.1.4.tgz", - "integrity": "sha512-tFuJqTxKb8AvfyqMfnavXdzfy3h3sWZRWwfluGbkeR7n0HUev+FmNgZ8SDrRBTVrVCjgH5cA21qGbCffMNtWvg==", + "version": "4.1.11", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-4.1.11.tgz", + "integrity": "sha512-fhACrNXUidIbGSBr5FlbuBkO7VWC1ZyLl0DO4CU2DrQoAPxX84Ysxs+HeGQpii5lZWV1Q4gBZTTu49mF+A6Edw==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/expect": "4.1.4", - "@vitest/mocker": "4.1.4", - "@vitest/pretty-format": "4.1.4", - "@vitest/runner": "4.1.4", - "@vitest/snapshot": "4.1.4", - "@vitest/spy": "4.1.4", - "@vitest/utils": "4.1.4", + "@vitest/expect": "4.1.11", + "@vitest/mocker": "4.1.11", + "@vitest/pretty-format": "4.1.11", + "@vitest/runner": "4.1.11", + "@vitest/snapshot": "4.1.11", + "@vitest/spy": "4.1.11", + "@vitest/utils": "4.1.11", "es-module-lexer": "^2.0.0", "expect-type": "^1.3.0", "magic-string": "^0.30.21", @@ -6231,12 +6231,12 @@ "@edge-runtime/vm": "*", "@opentelemetry/api": "^1.9.0", "@types/node": "^20.0.0 || ^22.0.0 || >=24.0.0", - "@vitest/browser-playwright": "4.1.4", - "@vitest/browser-preview": "4.1.4", - "@vitest/browser-webdriverio": "4.1.4", - "@vitest/coverage-istanbul": "4.1.4", - "@vitest/coverage-v8": "4.1.4", - "@vitest/ui": "4.1.4", + "@vitest/browser-playwright": "4.1.11", + "@vitest/browser-preview": "4.1.11", + "@vitest/browser-webdriverio": "4.1.11", + "@vitest/coverage-istanbul": "4.1.11", + "@vitest/coverage-v8": "4.1.11", + "@vitest/ui": "4.1.11", "happy-dom": "*", "jsdom": "*", "vite": "^6.0.0 || ^7.0.0 || ^8.0.0"