Share register dialogs with Bits UI and fix Woodpecker E2E image #36

Merged
eleith merged 1 commits from refactor/bits-dialogs into main 2026-11-04 15:23:14 +00:00
15 changed files with 341 additions and 138 deletions

No files matched your search

@@ -0,0 +1,9 @@
---
'@hledger-reports/web': patch
---
Use a small shared Bits UI shell for Cash transaction breakdowns and Investment details. Keep native financial content and payload-bound selection in their features while the shell handles the portal, focus trapping, scroll lock, close control and return focus.
Preserve Escape/close-button dismissal and non-dismissive backdrop clicks, clear selections on navigation or changed page data, and retain readable report tables without JavaScript. No new financial transformations or unrelated UI controls are introduced.
Use the private `git.eleith.com/eleith.private/playwright` image for Woodpecker E2E runs, matching the approved reference pipelines and the pinned Playwright version.
+1 -1
View File
@@ -82,7 +82,7 @@ steps:
- pnpm --filter @hledger-reports/worker build
e2e:
image: ghcr.io/eleith/containers-playwright:1.61.1-node26
image: git.eleith.com/eleith.private/playwright:1.61.1-node26
commands:
- pnpm config set store-dir .pnpm-store
- export PATH="$PWD/.cache/ci-bin:$PATH"
+1 -1
View File
@@ -18,7 +18,7 @@ The hledger library reads the journal and organizes neutral, table/chart-ready `
**`/queries` remains a separate live report:** each Run executes native balance at recorded cost against the journal current at execution. Exact results persist in SQLite as operational workflow output, but the page displays only its latest response; reload clears it. Neither page offers user-facing run history.
Sign-in is required, but this remains loopback-only development software. Do not expose it publicly or through a reverse proxy. See [shared access and authentication](docs/authentication.md). General report catalogs and a transaction-detail UI are not implemented.
Sign-in is required, but this remains loopback-only development software. Do not expose it publicly or through a reverse proxy. See [shared access and authentication](docs/authentication.md). Curated report catalogs and search remain deferred.
## Setup and run
+1 -1
View File
@@ -26,7 +26,7 @@ RUN pnpm --filter "@hledger-reports/web..." build
FROM builder AS pruner
RUN --mount=type=cache,id=pnpm-store,target=/pnpm/store pnpm --config.hoist-workspace-packages=false --filter @hledger-reports/web --prod deploy --legacy /pruned
# Match When's browser image and the @playwright/test version together.
# Keep the browser image and @playwright/test versions aligned.
FROM ghcr.io/eleith/containers-playwright:1.61.1-node26 AS e2e
WORKDIR /m
ENV CI=true
+12
View File
@@ -69,6 +69,18 @@ labels, split flags, changes and attached running balances survive pagination.
Empty accounts have no invented transaction; ambiguous native selectors show no
account amounts. First/Previous/Next/Last are ordinary links, including without JS.
## Transaction dialogs
Cash breakdowns and Investment details share a small Bits UI shell. Their financial
content and selection stay in the owning feature. Escape and the close button dismiss;
backdrop clicks do not. Closing restores focus to the opener when available, otherwise
to main content. Navigation or replacement page data invalidates the selected record.
The portal inherits document theme/typography and keeps the desktop dialog and narrow
bottom-sheet layout. Bits UI supplies focus trapping and body scroll lock. Detail
buttons remain disabled before hydration; the underlying financial tables work
without JavaScript. There are no forced SSR dialog contents or unrelated Bits controls.
## Files and state
Compose shares ignored **`apps/web/data/`** at `/app/data` in both processes; no
+13 -2
View File
@@ -15,12 +15,23 @@ test('Cash register opens saved activity and a transaction breakdown', async ({
'8520.00 USD'
);
await activity.getByRole('button', { name: 'Breakdown', exact: true }).first().click();
const opener = activity.getByRole('button', { name: 'Breakdown', exact: true }).first();
await opener.click();
const dialog = page.getByRole('dialog', { name: 'Transaction breakdown', exact: true });
await expect(dialog).toBeVisible();
const splits = dialog.getByRole('table', { name: 'Split details', exact: true });
await expect(splits).toContainText('Brokerage');
await expect(splits.getByRole('cell', { name: '10.00 VTI', exact: true })).toBeVisible();
await dialog.getByRole('button', { name: 'Close breakdown', exact: true }).click();
const close = dialog.getByRole('button', { name: 'Close breakdown', exact: true });
await expect(close).toBeFocused();
await page.mouse.click(1, 1);
await expect(dialog).toBeVisible();
await page.keyboard.press('Escape');
await expect(dialog).not.toBeVisible();
await expect(opener).toBeFocused();
await opener.press('Enter');
await expect(dialog).toBeVisible();
await close.click();
await expect(dialog).not.toBeVisible();
await expect(opener).toBeFocused();
});
+10 -1
View File
@@ -20,7 +20,8 @@ test('Investment register shows cash once per transaction and opens details', as
await expect(purchase.locator('[data-column="cash-balance"]')).toHaveCount(1);
await expect(groups.last().locator('[data-column="cash-balance"]')).toHaveText('200.00 USD');
await purchase.getByRole('button', { name: 'Details', exact: true }).click();
const opener = purchase.getByRole('button', { name: 'Details', exact: true });
await opener.click();
const details = page.getByRole('dialog', { name: 'Investment details', exact: true });
await expect(details).toBeVisible();
await expect(details.getByRole('table', { name: 'Related activity', exact: true })).toContainText(
@@ -28,4 +29,12 @@ test('Investment register shows cash once per transaction and opens details', as
);
await details.getByRole('button', { name: 'Close details', exact: true }).click();
await expect(details).not.toBeVisible();
await expect(opener).toBeFocused();
await opener.click();
await expect(details).toBeVisible();
await page.goBack();
await expect(
page.getByRole('heading', { name: 'Investment register', exact: true })
).toBeVisible();
await expect(details).not.toBeVisible();
});
+1
View File
@@ -28,6 +28,7 @@
"@hledger-reports/jobs": "workspace:*",
"@hledger-reports/snapshots": "workspace:*",
"better-auth": "1.7.14",
"bits-ui": "2.18.1",
"chart.js": "4.5.1"
},
"devDependencies": {
+123
View File
@@ -0,0 +1,123 @@
<script lang="ts">
import type { Snippet } from 'svelte';
import { Dialog } from 'bits-ui';
let {
open,
title,
label,
closeLabel,
onClose,
returnFocus = null,
children
}: {
open: boolean;
title: string;
label: string;
closeLabel: string;
onClose: () => void;
returnFocus?: HTMLElement | null;
children: Snippet;
} = $props();
function restoreFocus(event: Event) {
event.preventDefault();
const target =
returnFocus?.isConnected && !returnFocus.matches(':disabled')
? returnFocus
: document.querySelector<HTMLElement>('#main');
target?.focus({ preventScroll: true });
}
</script>
<Dialog.Root
bind:open={
() => open,
(next) => {
if (!next) onClose();
}
}
>
<Dialog.Portal>
<Dialog.Overlay>
{#snippet child({ props })}
<div {...props} class="overlay"></div>
{/snippet}
</Dialog.Overlay>
<Dialog.Content interactOutsideBehavior="ignore" onCloseAutoFocus={restoreFocus}>
{#snippet child({ props })}
<section {...props} class="content" aria-label={label} aria-labelledby={undefined}>
<header>
<Dialog.Title level={2}>
{#snippet child({ props: titleProps })}
<h2 {...titleProps} class="section-title">{title}</h2>
{/snippet}
</Dialog.Title>
<Dialog.Close>
{#snippet child({ props: closeProps })}
<button
{...closeProps}
type="button"
class="ui-button ui-button--icon"
aria-label={closeLabel}
title={closeLabel}
>
<span aria-hidden="true">×</span>
</button>
{/snippet}
</Dialog.Close>
</header>
{@render children()}
</section>
{/snippet}
</Dialog.Content>
</Dialog.Portal>
</Dialog.Root>
<style>
.overlay {
position: fixed;
inset: 0;
z-index: 10;
background: var(--color-backdrop);
}
.content {
position: fixed;
top: 50%;
left: 50%;
z-index: 11;
transform: translate(-50%, -50%);
width: min(var(--dialog-width), calc(100% - var(--space-medium)));
max-height: calc(100dvh - var(--space-large));
padding: var(--space-medium);
border: var(--border-width) solid var(--color-border);
border-radius: var(--radius-panel);
background: var(--color-surface);
color: var(--color-text);
overflow: auto;
overscroll-behavior: contain;
overflow-wrap: anywhere;
}
header {
display: flex;
align-items: start;
justify-content: space-between;
gap: var(--space-small);
}
h2 {
min-width: 0;
white-space: pre-wrap;
}
@media (max-width: 40rem) {
.content {
top: auto;
bottom: 0;
left: 0;
transform: none;
width: 100%;
max-height: 90dvh;
padding-bottom: max(var(--space-medium), env(safe-area-inset-bottom));
border-radius: var(--radius-panel) var(--radius-panel) 0 0;
}
}
</style>
@@ -10,6 +10,7 @@
let { data, labels = [] }: { data: CashRegisterData; labels?: readonly AccountLabel[] } =
$props();
let hydrated = $state(false);
let opener = $state<HTMLButtonElement | null>(null);
let selection = $state.raw<{ context: CashRegisterData; record: CashBreakdown } | null>(null);
// Invalidate synchronously with a replacement payload, before rendering its rows.
let selectedRecord = $derived(selection?.context === data ? selection.record : null);
@@ -18,6 +19,10 @@
onMount(() => {
hydrated = true;
});
function openBreakdown(record: CashBreakdown, button: HTMLButtonElement) {
opener = button;
selection = { context: data, record };
}
function closeBreakdown() {
selection = null;
}
@@ -113,8 +118,9 @@
class="ui-button ui-button--compact"
type="button"
disabled={!hydrated}
onclick={() => {
if (row.breakdown) selection = { context: data, record: row.breakdown };
aria-haspopup="dialog"
onclick={(event) => {
if (row.breakdown) openBreakdown(row.breakdown, event.currentTarget);
}}>Breakdown</button
>{/if}</td
>
@@ -159,7 +165,12 @@
</nav>
{/if}
<TransactionBreakdown {labels} selected={selectedRecord} onClose={closeBreakdown} />
<TransactionBreakdown
{labels}
selected={selectedRecord}
onClose={closeBreakdown}
returnFocus={opener}
/>
<style>
nav {
@@ -1,42 +1,30 @@
<script lang="ts">
import { accountLabel, type AccountLabel } from '#lib/accounts.js';
import DetailDialog from '#lib/DetailDialog.svelte';
import type { CashBreakdown } from './data.js';
let {
selected,
onClose,
returnFocus = null,
labels = []
}: {
selected: CashBreakdown | null;
onClose: () => void;
returnFocus?: HTMLElement | null;
labels?: readonly AccountLabel[];
} = $props();
let dialog: HTMLDialogElement;
$effect(() => {
if (selected) {
if (!dialog.open) dialog.showModal();
} else if (dialog.open) dialog.close();
});
function close() {
dialog.close();
}
</script>
<dialog bind:this={dialog} aria-label="Transaction breakdown" onclose={onClose}>
<DetailDialog
open={selected !== null}
title={selected?.payee || 'Transaction breakdown'}
label="Transaction breakdown"
closeLabel="Close breakdown"
{onClose}
{returnFocus}
>
{#if selected}
<header>
<h2 class="text section-title">{selected.payee || 'Transaction breakdown'}</h2>
<!-- Native showModal focuses this first focusable control and restores the opener on close. -->
<button
class="ui-button ui-button--icon"
type="button"
onclick={close}
aria-label="Close breakdown"
title="Close breakdown"><span aria-hidden="true">×</span></button
>
</header>
<p class="context">
{selected.date} ·
{#if selected.direction === 'unreported' || !selected.amount}
@@ -101,35 +89,9 @@
</table>
</div>
{/if}
</dialog>
</DetailDialog>
<style>
dialog {
width: min(var(--dialog-width), calc(100% - var(--space-medium)));
max-width: none;
max-height: calc(100dvh - var(--space-large));
margin: auto;
padding: var(--space-medium);
border: var(--border-width) solid var(--color-border);
border-radius: var(--radius-panel);
background: var(--color-surface);
color: var(--color-text);
font: inherit;
overflow: auto;
overflow-wrap: anywhere;
}
dialog::backdrop {
background: var(--color-backdrop);
}
header {
display: flex;
align-items: start;
justify-content: space-between;
gap: var(--space-small);
}
h2 {
min-width: 0;
}
h3 {
margin-block: var(--space-small);
font-size: inherit;
@@ -174,13 +136,4 @@
display: block;
color: var(--color-muted);
}
@media (max-width: 40rem) {
dialog {
width: 100%;
max-height: 90dvh;
padding-bottom: max(var(--space-medium), env(safe-area-inset-bottom));
margin: auto 0 0;
border-radius: var(--radius-panel) var(--radius-panel) 0 0;
}
}
</style>
@@ -1,5 +1,6 @@
<script lang="ts">
import { accountLabel, type AccountLabel } from '#lib/accounts.js';
import DetailDialog from '#lib/DetailDialog.svelte';
import type { InvestmentIssue, ReportedAmount } from '@hledger-reports/hledger';
import type { InvestmentTransaction } from './data.js';
import { securityActionLabels } from './action-labels.js';
@@ -7,13 +8,14 @@
let {
selectedTransaction,
labels = [],
returnFocus = null,
onClose
}: {
selectedTransaction: InvestmentTransaction | null;
labels?: readonly AccountLabel[];
onClose: () => void;
returnFocus?: HTMLElement | null;
} = $props();
let dialog: HTMLDialogElement;
const roles = { cash: 'Cash', income: 'Income', expense: 'Expense', gain: 'Gain' };
const issues: Record<InvestmentIssue, string> = {
'unclassified-commodity': 'Some amounts could not be classified as cash or securities.',
@@ -25,20 +27,6 @@
'virtual-postings': 'Virtual postings affect this activity.',
'unavailable-transacted-values': 'Some investment amounts are unavailable.'
};
$effect(() => {
if (selectedTransaction) {
if (!dialog.open) {
dialog.showModal();
}
} else if (dialog.open) {
dialog.close();
}
});
function closeDetails() {
dialog.close();
}
</script>
{#snippet amountList(amounts: ReportedAmount[] | null)}
@@ -53,22 +41,16 @@
{/if}
{/snippet}
<dialog bind:this={dialog} aria-label="Investment details" onclose={onClose}>
<DetailDialog
open={selectedTransaction !== null}
title={selectedTransaction?.payee || 'Investment details'}
label="Investment details"
closeLabel="Close details"
{onClose}
{returnFocus}
>
{#if selectedTransaction}
{@const cash = selectedTransaction.cash}
<header>
<h2 class="text section-title">{selectedTransaction.payee || 'Investment details'}</h2>
<!-- Native showModal focuses this control and restores the opener on close. -->
<button
class="ui-button ui-button--icon"
type="button"
onclick={closeDetails}
aria-label="Close details"
title="Close details"
>
<span aria-hidden="true">×</span>
</button>
</header>
<p class="context">{selectedTransaction.date}</p>
{#if selectedTransaction.notes.some((note) => note.trim().length > 0)}
<section aria-label="Notes">
@@ -189,35 +171,9 @@
</ul>
{/if}
{/if}
</dialog>
</DetailDialog>
<style>
dialog {
width: min(var(--dialog-width), calc(100% - var(--space-medium)));
max-width: none;
max-height: calc(100dvh - var(--space-large));
margin: auto;
padding: var(--space-medium);
border: var(--border-width) solid var(--color-border);
border-radius: var(--radius-panel);
background: var(--color-surface);
color: var(--color-text);
font: inherit;
overflow: auto;
overflow-wrap: anywhere;
}
dialog::backdrop {
background: var(--color-backdrop);
}
header {
display: flex;
align-items: start;
justify-content: space-between;
gap: var(--space-small);
}
h2 {
min-width: 0;
}
h3 {
margin-block: var(--space-small);
font-size: inherit;
@@ -278,13 +234,4 @@
dd {
margin: 0;
}
@media (max-width: 40rem) {
dialog {
width: 100%;
max-height: 90dvh;
padding-bottom: max(var(--space-medium), env(safe-area-inset-bottom));
margin: auto 0 0;
border-radius: var(--radius-panel) var(--radius-panel) 0 0;
}
}
</style>
@@ -11,6 +11,7 @@
let { data, labels = [] }: { data: InvestmentRegisterData; labels?: readonly AccountLabel[] } =
$props();
let hydrated = $state(false);
let opener = $state<HTMLButtonElement | null>(null);
let selection = $state.raw<{
pageData: InvestmentRegisterData;
transaction: InvestmentTransaction;
@@ -22,7 +23,8 @@
hydrated = true;
});
function openDetails(transaction: InvestmentTransaction) {
function openDetails(transaction: InvestmentTransaction, button: HTMLButtonElement) {
opener = button;
selection = { pageData: data, transaction };
}
@@ -205,7 +207,8 @@
class="ui-button ui-button--compact"
type="button"
disabled={!hydrated}
onclick={() => openDetails(transaction)}
aria-haspopup="dialog"
onclick={(event) => openDetails(transaction, event.currentTarget)}
>
Details
</button>
@@ -268,7 +271,7 @@
</a>
{/if}
</nav>
<InvestmentDetails {labels} {selectedTransaction} onClose={closeDetails} />
<InvestmentDetails {labels} {selectedTransaction} onClose={closeDetails} returnFocus={opener} />
<style>
nav {
+6
View File
@@ -116,6 +116,12 @@ layout imports `lib/styles/{reset,theme,layout,controls}.css` once. Page/panel s
focus and scrolling mechanics use classes, not wrapper components. Financial cell
alignment, transaction grouping and dialog content remain feature-owned.
`DetailDialog.svelte` is the shared interactive shell for Cash/Investment details.
Bits UI owns the portal, focus trap, Escape dismissal and scroll lock; the shell
keeps outside clicks non-dismissive and restores focus to the opener or main content.
Registers retain their payload-bound selections and clear them on navigation. The
shell does not know about transactions, postings or financial values.
CSS owns chart colors and font choices. `ChartCanvas` resolves them after mounting
and on system-theme changes; pure chart builders receive that resolved theme while
retaining their existing native-value and geometry rules. Server-rendered exact
+120 -2
View File
@@ -83,6 +83,9 @@ importers:
better-auth:
specifier: 1.7.14
version: 1.7.14(@sveltejs/kit@3.1.1(@sveltejs/vite-plugin-svelte@7.1.2(svelte@5.57.3(@typescript-eslint/types@8.60.0))(vite@8.0.16(@types/node@26.0.0)(yaml@2.9.0)))(svelte@5.57.3(@typescript-eslint/types@8.60.0))(typescript@6.0.3)(vite@8.0.16(@types/node@26.0.0)(yaml@2.9.0)))(svelte@5.57.3(@typescript-eslint/types@8.60.0))(vitest@4.1.7(@types/node@26.0.0)(vite@8.0.16(@types/node@26.0.0)(yaml@2.9.0)))
bits-ui:
specifier: 2.18.1
version: 2.18.1(@internationalized/date@3.12.4)(@sveltejs/kit@3.1.1(@sveltejs/vite-plugin-svelte@7.1.2(svelte@5.57.3(@typescript-eslint/types@8.60.0))(vite@8.0.16(@types/node@26.0.0)(yaml@2.9.0)))(svelte@5.57.3(@typescript-eslint/types@8.60.0))(typescript@6.0.3)(vite@8.0.16(@types/node@26.0.0)(yaml@2.9.0)))(svelte@5.57.3(@typescript-eslint/types@8.60.0))
chart.js:
specifier: 4.5.1
version: 4.5.1
@@ -542,6 +545,15 @@ packages:
resolution: {integrity: sha512-IkO+/KEUvwbVpiURZg+P7zF74z5Jxe0UgJxVni+RtoHQ6IZieXaO02kmadomap/q+l6bc/jdPGGqTjhuZnuz1Q==}
engines: {node: ^20.19.0 || ^22.13.0 || >=24}
'@floating-ui/core@1.8.0':
resolution: {integrity: sha512-0CIZ5itps/8x7BG8dEIhs53BvCUH2PCoogtakwRTut+Arm58sJooJ0AuZhLw2HJYIR5cMLNPBSS728sPho2khQ==}
'@floating-ui/dom@1.8.0':
resolution: {integrity: sha512-yXSrzeHZBTZadLOlfyhCkJHNeLJnHRnRInwdZ40L7ZiaAtrBwoYlsDrX3v5zB1Utk7CLfzcOVnVVWoXEky7Ceg==}
'@floating-ui/utils@0.2.12':
resolution: {integrity: sha512-HpCo8tmWzLVad5s2d19EhAz5zqrrQ6s69qd6moPMQvkOuSwDT1YgRfWSVuc4ennqrgv3OHppiOGMQ7oC13yIww==}
'@humanfs/core@0.19.2':
resolution: {integrity: sha512-UhXNm+CFMWcbChXywFwkmhqjs3PRCmcSa/hfBgLIb7oQ5HNb1wS0icWsGtSAUNgefHeI+eBrA8I1fxmbHsGdvA==}
engines: {node: '>=18.18.0'}
@@ -562,6 +574,9 @@ packages:
resolution: {integrity: sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==}
engines: {node: '>=18.18'}
'@internationalized/date@3.12.4':
resolution: {integrity: sha512-M1dEn4c1U1HsSlaVR8upZtSqvXrTkHDfv18H01uCSJyjVLDxnBR38v/fMxecmlwXKR4i9HeZcmgQAPE6A+aGJQ==}
'@jridgewell/gen-mapping@0.3.13':
resolution: {integrity: sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==}
@@ -768,6 +783,9 @@ packages:
svelte: ^5.46.4
vite: ^8.0.0-beta.7 || ^8.0.0
'@swc/helpers@0.5.23':
resolution: {integrity: sha512-5lSsMOTXURePglDfvuAQUqkGek9Hg2kksOYay2m0+XR++b2NWYL/4sWyuvVBIs8oKnJaxkdi9whaL/sqN13afw==}
'@tybys/wasm-util@0.10.4':
resolution: {integrity: sha512-W3c4gRigFS0T/Ma4qIYF3GDAc5AQdHb1yL5znJT1Zv1YaD9Kitx656wBjvr19qbiosmZT8lWDM5BEMynUqX65A==}
@@ -973,6 +991,13 @@ packages:
zod:
optional: true
bits-ui@2.18.1:
resolution: {integrity: sha512-KkemzKFH4T3gt3H+P86JcnAWExjByv/6vlwjm/BoCwTPHu03yiCdxbghdJLvFReQTe0acCAiRcKfmixxD6XvlA==}
engines: {node: '>=20'}
peerDependencies:
'@internationalized/date': ^3.8.1
svelte: ^5.33.0
brace-expansion@5.0.12:
resolution: {integrity: sha512-YovQ3rzhaLMIrDjNDMkNS01tea93qhEhG5xy8f6+R0l+dw3Ki+5sCoIoI942iuLZTHWogWktgwVDhU09iNEimQ==}
engines: {node: 20 || >=22}
@@ -1036,6 +1061,10 @@ packages:
defu@6.1.7:
resolution: {integrity: sha512-7z22QmUWiQ/2d0KkdYmANbRUVABpZ9SNYyH5vx6PZ+nE5bcC0l7uFvEfHlyld/HcGBFTL536ClDt3DEcSlEJAQ==}
dequal@2.0.3:
resolution: {integrity: sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==}
engines: {node: '>=6'}
detect-libc@2.1.2:
resolution: {integrity: sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==}
engines: {node: '>=8'}
@@ -1221,6 +1250,9 @@ packages:
resolution: {integrity: sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==}
engines: {node: '>=0.8.19'}
inline-style-parser@0.2.7:
resolution: {integrity: sha512-Nb2ctOyNR8DqQoR0OwRG95uNWIC0C1lCgf5Naz5H6Ji72KZ8OcFZLz2P5sNgwlyoJ8Yif11oMuYs5pBQa86csA==}
is-extglob@2.1.1:
resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==}
engines: {node: '>=0.10.0'}
@@ -1359,6 +1391,10 @@ packages:
resolution: {integrity: sha512-vtEhXh/gNjI9Yg1u4jX/0YVPMvxzHuGgCm6tC5kZyb08yjGWGnqAjGJvcXbqQR2P3MyMEFnRbpcdFS6PBcLqew==}
engines: {node: '>=12'}
lz-string@1.5.0:
resolution: {integrity: sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==}
hasBin: true
magic-string@0.30.21:
resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==}
@@ -1523,6 +1559,15 @@ packages:
rou3@0.9.2:
resolution: {integrity: sha512-3SOzvaAg8rkHrXtRjpCvCvbyO5to9oOO27Z/XqHEYXfMRVSw/qMIVdmaOk9W2lcRLtR6dlqTjo9hDeJk70QBYQ==}
runed@0.35.1:
resolution: {integrity: sha512-2F4Q/FZzbeJTFdIS/PuOoPRSm92sA2LhzTnv6FXhCoENb3huf5+fDuNOg1LNvGOouy3u/225qxmuJvcV3IZK5Q==}
peerDependencies:
'@sveltejs/kit': ^2.21.0
svelte: ^5.7.0
peerDependenciesMeta:
'@sveltejs/kit':
optional: true
sade@1.8.1:
resolution: {integrity: sha512-xal3CZX1Xlo/k4ApwCFrHVACi9fBqJ7V+mwhBsuf/1IOKbBy098Fex+Wa/5QMubw09pSZ/u8EY8PWgevJsXp1A==}
engines: {node: '>=6'}
@@ -1567,6 +1612,9 @@ packages:
std-env@4.3.0:
resolution: {integrity: sha512-OtU/EgQ1kIm5KwqQpBC6ZEMXrZRui11w8zgfTWp8cdO9B8OaPsbA8bTHO2P+HNo1VlUTGMVBwPhydu6poeXiag==}
style-to-object@1.0.14:
resolution: {integrity: sha512-LIN7rULI0jBscWQYaSswptyderlarFkjQ+t79nzty8tcIAceVomEVlLzH5VP4Cmsv6MtKhs7qaAiwlcp+Mgaxw==}
svelte-check@4.7.7:
resolution: {integrity: sha512-Ge2IMdeTpbJuzNFGRiiMg0RWgbScysKQu7xwcyFJ22+DU9F5nPX51+Q3gHmGqRxYQMx2knreVWt5oCtfe0vf2g==}
engines: {node: '>= 18.0.0'}
@@ -1584,10 +1632,19 @@ packages:
svelte:
optional: true
svelte-toolbelt@0.10.6:
resolution: {integrity: sha512-YWuX+RE+CnWYx09yseAe4ZVMM7e7GRFZM6OYWpBKOb++s+SQ8RBIMMe+Bs/CznBMc0QPLjr+vDBxTAkozXsFXQ==}
engines: {node: '>=18', pnpm: '>=8.7.0'}
peerDependencies:
svelte: ^5.30.2
svelte@5.57.3:
resolution: {integrity: sha512-5mAftM3SoDV9X0zdF386F9p2pv8iKOwi/KfBS1XnBAkVCKFzVOLHUoa8iGWAt4buoicLmyWVRpuuD5wplXOQsg==}
engines: {node: '>=18'}
tabbable@6.5.1:
resolution: {integrity: sha512-dMCuwDUY6Noz3zVKLefBC4wq+aJV20yc7vWmM7uTNGwtQSFF5kX7OUIUsxuVajz/bdt5Xqdah3/0NLjth+cM5A==}
tinybench@2.9.0:
resolution: {integrity: sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==}
@@ -1990,6 +2047,17 @@ snapshots:
'@eslint/core': 1.2.1
levn: 0.4.1
'@floating-ui/core@1.8.0':
dependencies:
'@floating-ui/utils': 0.2.12
'@floating-ui/dom@1.8.0':
dependencies:
'@floating-ui/core': 1.8.0
'@floating-ui/utils': 0.2.12
'@floating-ui/utils@0.2.12': {}
'@humanfs/core@0.19.2':
dependencies:
'@humanfs/types': 0.15.0
@@ -2006,6 +2074,10 @@ snapshots:
'@humanwhocodes/retry@0.4.3': {}
'@internationalized/date@3.12.4':
dependencies:
'@swc/helpers': 0.5.23
'@jridgewell/gen-mapping@0.3.13':
dependencies:
'@jridgewell/sourcemap-codec': 1.6.0
@@ -2155,6 +2227,10 @@ snapshots:
vite: 8.0.16(@types/node@26.0.0)(yaml@2.9.0)
vitefu: 1.1.3(vite@8.0.16(@types/node@26.0.0)(yaml@2.9.0))
'@swc/helpers@0.5.23':
dependencies:
tslib: 2.8.1
'@tybys/wasm-util@0.10.4':
dependencies:
tslib: 2.8.1
@@ -2365,6 +2441,19 @@ snapshots:
optionalDependencies:
zod: 4.6.7
bits-ui@2.18.1(@internationalized/date@3.12.4)(@sveltejs/kit@3.1.1(@sveltejs/vite-plugin-svelte@7.1.2(svelte@5.57.3(@typescript-eslint/types@8.60.0))(vite@8.0.16(@types/node@26.0.0)(yaml@2.9.0)))(svelte@5.57.3(@typescript-eslint/types@8.60.0))(typescript@6.0.3)(vite@8.0.16(@types/node@26.0.0)(yaml@2.9.0)))(svelte@5.57.3(@typescript-eslint/types@8.60.0)):
dependencies:
'@floating-ui/core': 1.8.0
'@floating-ui/dom': 1.8.0
'@internationalized/date': 3.12.4
esm-env: 1.2.2
runed: 0.35.1(@sveltejs/kit@3.1.1(@sveltejs/vite-plugin-svelte@7.1.2(svelte@5.57.3(@typescript-eslint/types@8.60.0))(vite@8.0.16(@types/node@26.0.0)(yaml@2.9.0)))(svelte@5.57.3(@typescript-eslint/types@8.60.0))(typescript@6.0.3)(vite@8.0.16(@types/node@26.0.0)(yaml@2.9.0)))(svelte@5.57.3(@typescript-eslint/types@8.60.0))
svelte: 5.57.3(@typescript-eslint/types@8.60.0)
svelte-toolbelt: 0.10.6(@sveltejs/kit@3.1.1(@sveltejs/vite-plugin-svelte@7.1.2(svelte@5.57.3(@typescript-eslint/types@8.60.0))(vite@8.0.16(@types/node@26.0.0)(yaml@2.9.0)))(svelte@5.57.3(@typescript-eslint/types@8.60.0))(typescript@6.0.3)(vite@8.0.16(@types/node@26.0.0)(yaml@2.9.0)))(svelte@5.57.3(@typescript-eslint/types@8.60.0))
tabbable: 6.5.1
transitivePeerDependencies:
- '@sveltejs/kit'
brace-expansion@5.0.12:
dependencies:
balanced-match: 4.0.4
@@ -2409,6 +2498,8 @@ snapshots:
defu@6.1.7: {}
dequal@2.0.3: {}
detect-libc@2.1.2: {}
devalue@5.9.4: {}
@@ -2590,6 +2681,8 @@ snapshots:
imurmurhash@0.1.4: {}
inline-style-parser@0.2.7: {}
is-extglob@2.1.1: {}
is-glob@4.0.3:
@@ -2691,6 +2784,8 @@ snapshots:
luxon@3.7.2: {}
lz-string@1.5.0: {}
magic-string@0.30.21:
dependencies:
'@jridgewell/sourcemap-codec': 1.6.0
@@ -2822,6 +2917,15 @@ snapshots:
rou3@0.9.2: {}
runed@0.35.1(@sveltejs/kit@3.1.1(@sveltejs/vite-plugin-svelte@7.1.2(svelte@5.57.3(@typescript-eslint/types@8.60.0))(vite@8.0.16(@types/node@26.0.0)(yaml@2.9.0)))(svelte@5.57.3(@typescript-eslint/types@8.60.0))(typescript@6.0.3)(vite@8.0.16(@types/node@26.0.0)(yaml@2.9.0)))(svelte@5.57.3(@typescript-eslint/types@8.60.0)):
dependencies:
dequal: 2.0.3
esm-env: 1.2.2
lz-string: 1.5.0
svelte: 5.57.3(@typescript-eslint/types@8.60.0)
optionalDependencies:
'@sveltejs/kit': 3.1.1(@sveltejs/vite-plugin-svelte@7.1.2(svelte@5.57.3(@typescript-eslint/types@8.60.0))(vite@8.0.16(@types/node@26.0.0)(yaml@2.9.0)))(svelte@5.57.3(@typescript-eslint/types@8.60.0))(typescript@6.0.3)(vite@8.0.16(@types/node@26.0.0)(yaml@2.9.0))
sade@1.8.1:
dependencies:
mri: 1.2.0
@@ -2854,6 +2958,10 @@ snapshots:
std-env@4.3.0: {}
style-to-object@1.0.14:
dependencies:
inline-style-parser: 0.2.7
svelte-check@4.7.7(picomatch@4.0.7)(svelte@5.57.3(@typescript-eslint/types@8.60.0))(typescript@6.0.3):
dependencies:
'@jridgewell/trace-mapping': 0.3.31
@@ -2879,6 +2987,15 @@ snapshots:
optionalDependencies:
svelte: 5.57.3(@typescript-eslint/types@8.60.0)
svelte-toolbelt@0.10.6(@sveltejs/kit@3.1.1(@sveltejs/vite-plugin-svelte@7.1.2(svelte@5.57.3(@typescript-eslint/types@8.60.0))(vite@8.0.16(@types/node@26.0.0)(yaml@2.9.0)))(svelte@5.57.3(@typescript-eslint/types@8.60.0))(typescript@6.0.3)(vite@8.0.16(@types/node@26.0.0)(yaml@2.9.0)))(svelte@5.57.3(@typescript-eslint/types@8.60.0)):
dependencies:
clsx: 2.1.1
runed: 0.35.1(@sveltejs/kit@3.1.1(@sveltejs/vite-plugin-svelte@7.1.2(svelte@5.57.3(@typescript-eslint/types@8.60.0))(vite@8.0.16(@types/node@26.0.0)(yaml@2.9.0)))(svelte@5.57.3(@typescript-eslint/types@8.60.0))(typescript@6.0.3)(vite@8.0.16(@types/node@26.0.0)(yaml@2.9.0)))(svelte@5.57.3(@typescript-eslint/types@8.60.0))
style-to-object: 1.0.14
svelte: 5.57.3(@typescript-eslint/types@8.60.0)
transitivePeerDependencies:
- '@sveltejs/kit'
svelte@5.57.3(@typescript-eslint/types@8.60.0):
dependencies:
'@jridgewell/remapping': 2.3.5
@@ -2899,6 +3016,8 @@ snapshots:
transitivePeerDependencies:
- '@typescript-eslint/types'
tabbable@6.5.1: {}
tinybench@2.9.0: {}
tinyexec@1.3.1: {}
@@ -2916,8 +3035,7 @@ snapshots:
dependencies:
typescript: 6.0.3
tslib@2.8.1:
optional: true
tslib@2.8.1: {}
type-check@0.4.0:
dependencies: