diff --git a/package.json b/package.json index 8ff3064..e42ae0a 100644 --- a/package.json +++ b/package.json @@ -44,7 +44,6 @@ "@types/jsdom": "28.0.1", "@vitest/browser-playwright": "4.1.5", "@vitest/coverage-v8": "4.1.5", - "bits-ui": "2.18.1", "clsx": "^2.1.1", "dprint": "0.54.0", "jsdom": "29.1.1", diff --git a/src/features/AdjustTypography/ui/TypographyMenu/TypographyMenu.svelte b/src/features/AdjustTypography/ui/TypographyMenu/TypographyMenu.svelte index 1e234dd..e52b422 100644 --- a/src/features/AdjustTypography/ui/TypographyMenu/TypographyMenu.svelte +++ b/src/features/AdjustTypography/ui/TypographyMenu/TypographyMenu.svelte @@ -16,11 +16,11 @@ import { Button, ComboControl, ControlGroup, + Popover, Slider, } from '$shared/ui'; import Settings2Icon from '@lucide/svelte/icons/settings-2'; import XIcon from '@lucide/svelte/icons/x'; -import { Popover } from 'bits-ui'; import { getContext } from 'svelte'; import { cubicOut } from 'svelte/easing'; import { fly } from 'svelte/transition'; @@ -73,33 +73,21 @@ $effect(() => { {#if !hidden} {#if responsive.isMobileOrTablet}
- - - {#snippet child({ props })} - - {/snippet} - + + {#snippet trigger(props)} + + {/snippet} - -
@@ -111,17 +99,13 @@ $effect(() => { CONTROLS
- - {#snippet child({ props })} - - {/snippet} - +
@@ -135,9 +119,9 @@ $effect(() => { /> {/each} - - - + + {/snippet} + {:else}
import type { TypographyControl } from '$shared/lib'; import { cn } from '$shared/lib'; -import { Slider } from '$shared/ui'; +import { + Popover, + Slider, +} from '$shared/ui'; import { Button } from '$shared/ui/Button'; import MinusIcon from '@lucide/svelte/icons/minus'; import PlusIcon from '@lucide/svelte/icons/plus'; -import { Popover } from 'bits-ui'; import TechText from '../TechText/TechText.svelte'; interface Props { @@ -114,59 +116,55 @@ const displayLabel = $derived(label ?? controlLabel ?? '');
- - - {#snippet child({ props })} - - {/snippet} - + + + {formattedValue()} + + + {/snippet} - - - - + {#snippet children()} +
+ +
+ {/snippet} +
diff --git a/src/shared/ui/ComboControl/ComboControl.svelte.test.ts b/src/shared/ui/ComboControl/ComboControl.svelte.test.ts index 8eade6a..61e2e35 100644 --- a/src/shared/ui/ComboControl/ComboControl.svelte.test.ts +++ b/src/shared/ui/ComboControl/ComboControl.svelte.test.ts @@ -4,6 +4,7 @@ import { render, screen, waitFor, + within, } from '@testing-library/svelte'; import ComboControl from './ComboControl.svelte'; @@ -16,6 +17,16 @@ function makeControl(value: number, opts: { min?: number; max?: number; step?: n }); } +/** + * The trigger is the button wired to the popover (has popovertarget). The native + * Popover always renders its content (the vertical slider, which also displays the + * value) in the DOM, so value assertions must be scoped to the trigger to avoid + * matching the slider's own value label. + */ +function getTrigger(): HTMLElement { + return document.querySelector('button[popovertarget]') as HTMLElement; +} + describe('ComboControl', () => { describe('Rendering', () => { it('renders decrease and increase buttons', () => { @@ -26,17 +37,17 @@ describe('ComboControl', () => { it('renders the current integer value', () => { render(ComboControl, { control: makeControl(42) }); - expect(screen.getByText('42')).toBeInTheDocument(); + expect(within(getTrigger()).getByText('42')).toBeInTheDocument(); }); it('formats decimal value to 1 decimal place when step >= 0.1', () => { render(ComboControl, { control: makeControl(1.5, { step: 0.1 }) }); - expect(screen.getByText('1.5')).toBeInTheDocument(); + expect(within(getTrigger()).getByText('1.5')).toBeInTheDocument(); }); it('formats decimal value to 2 decimal places when step < 0.1', () => { render(ComboControl, { control: makeControl(1.55, { step: 0.01 }) }); - expect(screen.getByText('1.55')).toBeInTheDocument(); + expect(within(getTrigger()).getByText('1.55')).toBeInTheDocument(); }); it('renders label when label prop is provided', () => { @@ -106,16 +117,32 @@ describe('ComboControl', () => { const control = makeControl(50); render(ComboControl, { control }); await fireEvent.click(screen.getByLabelText('Increase')); - await waitFor(() => expect(screen.getByText('51')).toBeInTheDocument()); + await waitFor(() => expect(within(getTrigger()).getByText('51')).toBeInTheDocument()); }); }); describe('Popover', () => { - it('opens popover with vertical slider on trigger click', async () => { + /** + * The native Popover always renders its content; opening is driven by the + * browser's declarative popovertarget invoker, which jsdom does not simulate + * on click (mirrors Popover.svelte.test.ts). So assert the wired-but-closed + * state, then drive the open through the API the browser would call. + */ + it('exposes a popover trigger with the vertical slider as its content', async () => { render(ComboControl, { control: makeControl(50), controlLabel: 'Size control' }); - expect(screen.queryByRole('slider')).not.toBeInTheDocument(); - await fireEvent.click(screen.getByText('Size control')); - await waitFor(() => expect(screen.getByRole('slider')).toBeInTheDocument()); + + const trigger = getTrigger(); + expect(trigger).toHaveAttribute('aria-expanded', 'false'); + + const content = document.getElementById(trigger.getAttribute('popovertarget')!) as HTMLElement; + expect(content).toHaveAttribute('data-state', 'closed'); + // The vertical slider lives inside the popover content. While closed the + // content is visibility:hidden, so query including hidden elements. + expect(within(content).getByRole('slider', { hidden: true })).toBeInTheDocument(); + + content.showPopover(); + await waitFor(() => expect(content).toHaveAttribute('data-state', 'open')); + expect(trigger).toHaveAttribute('aria-expanded', 'true'); }); }); diff --git a/src/shared/ui/Popover/Popover.stories.svelte b/src/shared/ui/Popover/Popover.stories.svelte new file mode 100644 index 0000000..a141ad1 --- /dev/null +++ b/src/shared/ui/Popover/Popover.stories.svelte @@ -0,0 +1,117 @@ + + + + + + {#snippet template()} +
+ + {#snippet trigger(props)} + + {/snippet} + {#snippet children()} +
Popover content
+ {/snippet} +
+
+ {/snippet} +
+ + + {#snippet template()} +
+ + {#snippet trigger(props)} + + {/snippet} + {#snippet children()} +
Popover content
+ {/snippet} +
+
+ {/snippet} +
+ + + + {#snippet template()} +
+ + {#snippet trigger(props)} + + {/snippet} + {#snippet children({ close })} +
+

Menu header

+

+ Aligned to the trigger's end edge. +

+ +
+ {/snippet} +
+
+ {/snippet} +
+ + + + {#snippet template()} +
+ + {#snippet trigger(props)} + + {/snippet} + {#snippet children()} +
+ +
+ {/snippet} +
+
+ {/snippet} +
diff --git a/src/shared/ui/Popover/Popover.svelte b/src/shared/ui/Popover/Popover.svelte new file mode 100644 index 0000000..c1443d1 --- /dev/null +++ b/src/shared/ui/Popover/Popover.svelte @@ -0,0 +1,225 @@ + + + +{@render trigger(triggerProps)} + + +
+ {@render children({ close })} +
diff --git a/src/shared/ui/Popover/Popover.svelte.test.ts b/src/shared/ui/Popover/Popover.svelte.test.ts new file mode 100644 index 0000000..603c7a9 --- /dev/null +++ b/src/shared/ui/Popover/Popover.svelte.test.ts @@ -0,0 +1,49 @@ +import { + fireEvent, + render, + screen, +} from '@testing-library/svelte'; +import Harness from './PopoverHarness.svelte'; + +/** + * Resolve the popover content element (the [popover] ancestor of the test content). + */ +function getContent(): HTMLElement { + return screen.getByTestId('content').closest('[popover]') as HTMLElement; +} + +describe('Popover', () => { + it('renders the trigger with aria wiring, closed by default', () => { + render(Harness); + const trigger = screen.getByRole('button', { name: 'Open' }); + expect(trigger).toHaveAttribute('aria-expanded', 'false'); + expect(trigger).toHaveAttribute('aria-haspopup', 'dialog'); + expect(trigger).toHaveAttribute('popovertarget'); + expect(getContent()).toHaveAttribute('data-state', 'closed'); + }); + + it('opens via the popover toggle and syncs aria-expanded + data-state', async () => { + render(Harness); + const trigger = screen.getByRole('button', { name: 'Open' }); + // jsdom does not auto-invoke popovertarget; call the API the browser would. + getContent().showPopover(); + await Promise.resolve(); + expect(getContent()).toHaveAttribute('data-state', 'open'); + expect(trigger).toHaveAttribute('aria-expanded', 'true'); + }); + + it('opens when the parent sets open=true (state -> browser)', async () => { + render(Harness, { open: true }); + await Promise.resolve(); + expect(getContent()).toHaveAttribute('data-state', 'open'); + }); + + it('close() hides the popover and resets aria-expanded', async () => { + render(Harness, { open: true }); + await Promise.resolve(); + const trigger = screen.getByRole('button', { name: 'Open' }); + await fireEvent.click(screen.getByTestId('close')); + expect(getContent()).toHaveAttribute('data-state', 'closed'); + expect(trigger).toHaveAttribute('aria-expanded', 'false'); + }); +}); diff --git a/src/shared/ui/Popover/PopoverHarness.svelte b/src/shared/ui/Popover/PopoverHarness.svelte new file mode 100644 index 0000000..566e154 --- /dev/null +++ b/src/shared/ui/Popover/PopoverHarness.svelte @@ -0,0 +1,21 @@ + + + + + {#snippet trigger(props)} + + {/snippet} + {#snippet children({ close })} +
+ +
+ {/snippet} +
diff --git a/src/shared/ui/Popover/popover-position.test.ts b/src/shared/ui/Popover/popover-position.test.ts new file mode 100644 index 0000000..25f13d8 --- /dev/null +++ b/src/shared/ui/Popover/popover-position.test.ts @@ -0,0 +1,119 @@ +import { + type Align, + type Side, + computePosition, +} from './popover-position'; + +/** + * Build a DOMRect-like object (jsdom/node has no layout). + */ +function rect(x: number, y: number, width: number, height: number): DOMRect { + return { + x, + y, + width, + height, + top: y, + left: x, + right: x + width, + bottom: y + height, + toJSON: () => ({}), + } as DOMRect; +} + +const viewport = { width: 1000, height: 800 }; +const content = { width: 200, height: 100 }; + +function compute(side: Side, align: Align, sideOffset = 0, trigger = rect(400, 400, 100, 40)) { + return computePosition({ triggerRect: trigger, contentRect: content, viewport, side, align, sideOffset }); +} + +describe('computePosition', () => { + it('places below the trigger for side="bottom"', () => { + const r = compute('bottom', 'center'); + expect(r.side).toBe('bottom'); + expect(r.y).toBe(440); // trigger.bottom (400+40) + }); + + it('places above the trigger for side="top"', () => { + const r = compute('top', 'center'); + expect(r.side).toBe('top'); + expect(r.y).toBe(300); // trigger.top (400) - content.height (100) + }); + + it('applies sideOffset on the main axis', () => { + const r = compute('bottom', 'center', 8); + expect(r.y).toBe(448); + }); + + it('aligns center on the cross axis (vertical side)', () => { + const r = compute('bottom', 'center'); + // trigger center x = 450; content half = 100 -> 350 + expect(r.x).toBe(350); + }); + + it('aligns start and end on the cross axis (vertical side)', () => { + expect(compute('bottom', 'start').x).toBe(400); // trigger.left + expect(compute('bottom', 'end').x).toBe(300); // trigger.right(500) - content.width(200) + }); + + it('places left/right with vertical cross-axis alignment', () => { + const right = compute('right', 'start'); + expect(right.side).toBe('right'); + expect(right.x).toBe(500); // trigger.right + expect(right.y).toBe(400); // trigger.top (align start) + const left = compute('left', 'center'); + expect(left.side).toBe('left'); + expect(left.x).toBe(200); // trigger.left(400) - content.width(200) + }); + + it('flips top->bottom when there is no room above', () => { + const nearTop = rect(400, 20, 100, 40); // only 20px above, content needs 100 + const r = computePosition({ + triggerRect: nearTop, + contentRect: content, + viewport, + side: 'top', + align: 'center', + sideOffset: 0, + }); + expect(r.side).toBe('bottom'); + expect(r.y).toBe(60); // nearTop.bottom + }); + + it('does NOT flip when neither side fits (keeps requested side)', () => { + const tall = { width: 200, height: 700 }; + const r = computePosition({ + triggerRect: rect(400, 400, 100, 40), + contentRect: tall, + viewport, + side: 'top', + align: 'center', + sideOffset: 0, + }); + expect(r.side).toBe('top'); + }); + + it('shifts on the cross axis to stay within the viewport', () => { + const nearRight = rect(950, 400, 40, 40); // center x ~970, content 200 would overflow right + const r = computePosition({ + triggerRect: nearRight, + contentRect: content, + viewport, + side: 'bottom', + align: 'center', + sideOffset: 0, + }); + expect(r.x).toBe(800); // clamped to viewport.width(1000) - content.width(200) + const nearLeft = rect(10, 400, 40, 40); + const r2 = computePosition({ + triggerRect: nearLeft, + contentRect: content, + viewport, + side: 'bottom', + align: 'center', + sideOffset: 0, + }); + expect(r2.x).toBe(0); // clamped to 0 + }); +}); diff --git a/src/shared/ui/Popover/popover-position.ts b/src/shared/ui/Popover/popover-position.ts new file mode 100644 index 0000000..f9b1a11 --- /dev/null +++ b/src/shared/ui/Popover/popover-position.ts @@ -0,0 +1,149 @@ +import { clampNumber } from '$shared/lib/utils'; + +/** + * Side of the trigger the content prefers to open toward. + */ +export type Side = 'top' | 'bottom' | 'left' | 'right'; + +/** + * Cross-axis alignment of the content relative to the trigger. + */ +export type Align = 'start' | 'center' | 'end'; + +/** + * Inputs for a single placement computation. All geometry is injected + * (no DOM reads) so the function stays pure and unit-testable. + */ +type ComputeArgs = { + /** + * Trigger bounding rect (viewport coordinates). + */ + triggerRect: DOMRect; + /** + * Measured content size. + */ + contentRect: { width: number; height: number }; + /** + * Viewport size. + */ + viewport: { width: number; height: number }; + /** + * Preferred side. + */ + side: Side; + /** + * Cross-axis alignment. + */ + align: Align; + /** + * Gap between trigger and content on the main axis. + */ + sideOffset: number; +}; + +/** + * Resolved placement: fixed-position coordinates plus the side actually used + * (may differ from the requested side after a flip). + */ +type ComputeResult = { x: number; y: number; side: Side }; + +const OPPOSITE: Record = { + top: 'bottom', + bottom: 'top', + left: 'right', + right: 'left', +}; + +/** + * True for sides whose main axis is vertical (content sits above/below). + */ +function isVertical(side: Side): boolean { + return side === 'top' || side === 'bottom'; +} + +/** + * Main-axis coordinate (top for vertical sides, left for horizontal sides). + */ +function mainAxisCoord(side: Side, t: DOMRect, c: { width: number; height: number }, offset: number): number { + switch (side) { + case 'top': + return t.top - c.height - offset; + case 'bottom': + return t.bottom + offset; + case 'left': + return t.left - c.width - offset; + case 'right': + return t.right + offset; + } +} + +/** + * Whether the content fits on the given side within the viewport. + */ +function fitsOnSide( + side: Side, + t: DOMRect, + c: { width: number; height: number }, + v: { width: number; height: number }, + offset: number, +): boolean { + const coord = mainAxisCoord(side, t, c, offset); + switch (side) { + case 'top': + return coord >= 0; + case 'left': + return coord >= 0; + case 'bottom': + return coord + c.height <= v.height; + case 'right': + return coord + c.width <= v.width; + } +} + +/** + * Cross-axis coordinate for the requested alignment. + */ +function crossAxisCoord(side: Side, align: Align, t: DOMRect, c: { width: number; height: number }): number { + if (isVertical(side)) { + if (align === 'start') { + return t.left; + } + if (align === 'end') { + return t.right - c.width; + } + return t.left + t.width / 2 - c.width / 2; + } + if (align === 'start') { + return t.top; + } + if (align === 'end') { + return t.bottom - c.height; + } + return t.top + t.height / 2 - c.height / 2; +} + +/** + * Compute an anchored placement with flip (to the opposite side when the + * preferred side doesn't fit but the opposite does) and shift (clamp the + * cross axis so the content stays within the viewport). + */ +export function computePosition(args: ComputeArgs): ComputeResult { + const { triggerRect: t, contentRect: c, viewport: v, align, sideOffset } = args; + let side = args.side; + + if (!fitsOnSide(side, t, c, v, sideOffset) && fitsOnSide(OPPOSITE[side], t, c, v, sideOffset)) { + side = OPPOSITE[side]; + } + + let x: number; + let y: number; + if (isVertical(side)) { + y = mainAxisCoord(side, t, c, sideOffset); + x = clampNumber(crossAxisCoord(side, align, t, c), 0, Math.max(0, v.width - c.width)); + } else { + x = mainAxisCoord(side, t, c, sideOffset); + y = clampNumber(crossAxisCoord(side, align, t, c), 0, Math.max(0, v.height - c.height)); + } + + return { x, y, side }; +} diff --git a/src/shared/ui/index.ts b/src/shared/ui/index.ts index d02efb3..8400a36 100644 --- a/src/shared/ui/index.ts +++ b/src/shared/ui/index.ts @@ -94,6 +94,12 @@ export { */ default as PerspectivePlan, } from './PerspectivePlan/PerspectivePlan.svelte'; +export { + /** + * Anchored popover on the native Popover API + */ + default as Popover, +} from './Popover/Popover.svelte'; export { /** * Specialized input with search icon and clear state diff --git a/vitest.setup.jsdom.ts b/vitest.setup.jsdom.ts index 228df6a..36b8174 100644 --- a/vitest.setup.jsdom.ts +++ b/vitest.setup.jsdom.ts @@ -61,3 +61,48 @@ if (typeof PointerEvent === 'undefined') { // jsdom lacks pointer capture HTMLElement.prototype.setPointerCapture = vi.fn(); HTMLElement.prototype.releasePointerCapture = vi.fn(); + +// jsdom lacks the Popover API. Minimal shim: methods toggle an internal flag, +// dispatch a `toggle` event ({ oldState, newState }), and make +// matches(':popover-open') reflect the flag so components can sync state. +if (typeof HTMLElement.prototype.showPopover !== 'function') { + const openFlag = new WeakSet(); + + const fireToggle = (el: HTMLElement, oldState: string, newState: string) => { + const event = new Event('toggle') as Event & { oldState: string; newState: string }; + event.oldState = oldState; + event.newState = newState; + el.dispatchEvent(event); + }; + + HTMLElement.prototype.showPopover = function showPopover(this: HTMLElement) { + if (openFlag.has(this)) { + return; + } + openFlag.add(this); + fireToggle(this, 'closed', 'open'); + }; + HTMLElement.prototype.hidePopover = function hidePopover(this: HTMLElement) { + if (!openFlag.has(this)) { + return; + } + openFlag.delete(this); + fireToggle(this, 'open', 'closed'); + }; + HTMLElement.prototype.togglePopover = function togglePopover(this: HTMLElement) { + if (openFlag.has(this)) { + this.hidePopover(); + return !openFlag.has(this); + } + this.showPopover(); + return openFlag.has(this); + }; + + const originalMatches = Element.prototype.matches; + Element.prototype.matches = function matches(this: Element, selector: string): boolean { + if (selector === ':popover-open') { + return this instanceof HTMLElement && openFlag.has(this); + } + return originalMatches.call(this, selector); + } as typeof Element.prototype.matches; +} diff --git a/yarn.lock b/yarn.lock index 992f596..d607270 100644 --- a/yarn.lock +++ b/yarn.lock @@ -565,32 +565,6 @@ __metadata: languageName: node linkType: hard -"@floating-ui/core@npm:^1.7.1, @floating-ui/core@npm:^1.7.3": - version: 1.7.3 - resolution: "@floating-ui/core@npm:1.7.3" - dependencies: - "@floating-ui/utils": "npm:^0.2.10" - checksum: 10c0/edfc23800122d81df0df0fb780b7328ae6c5f00efbb55bd48ea340f4af8c5b3b121ceb4bb81220966ab0f87b443204d37105abdd93d94846468be3243984144c - languageName: node - linkType: hard - -"@floating-ui/dom@npm:^1.7.1": - version: 1.7.4 - resolution: "@floating-ui/dom@npm:1.7.4" - dependencies: - "@floating-ui/core": "npm:^1.7.3" - "@floating-ui/utils": "npm:^0.2.10" - checksum: 10c0/da6166c25f9b0729caa9f498685a73a0e28251613b35d27db8de8014bc9d045158a23c092b405321a3d67c2064909b6e2a7e6c1c9cc0f62967dca5779f5aef30 - languageName: node - linkType: hard - -"@floating-ui/utils@npm:^0.2.10": - version: 0.2.10 - resolution: "@floating-ui/utils@npm:0.2.10" - checksum: 10c0/e9bc2a1730ede1ee25843937e911ab6e846a733a4488623cd353f94721b05ec2c9ec6437613a2ac9379a94c2fd40c797a2ba6fa1df2716f5ce4aa6ddb1cf9ea4 - languageName: node - linkType: hard - "@internationalized/date@npm:3.12.1": version: 3.12.1 resolution: "@internationalized/date@npm:3.12.1" @@ -1891,23 +1865,6 @@ __metadata: languageName: node linkType: hard -"bits-ui@npm:2.18.1": - version: 2.18.1 - resolution: "bits-ui@npm:2.18.1" - dependencies: - "@floating-ui/core": "npm:^1.7.1" - "@floating-ui/dom": "npm:^1.7.1" - esm-env: "npm:^1.1.2" - runed: "npm:^0.35.1" - svelte-toolbelt: "npm:^0.10.6" - tabbable: "npm:^6.2.0" - peerDependencies: - "@internationalized/date": ^3.8.1 - svelte: ^5.33.0 - checksum: 10c0/1ed513a994d66449ab00c091f70111de30182856a167110ec3a413317014e7b949c50a8501aaa8a7603829394e5499bcb5a2b7c4a38a541b3820aad03e01f3cf - languageName: node - linkType: hard - "bundle-name@npm:^4.1.0": version: 4.1.0 resolution: "bundle-name@npm:4.1.0" @@ -2382,7 +2339,7 @@ __metadata: languageName: node linkType: hard -"esm-env@npm:^1.0.0, esm-env@npm:^1.1.2, esm-env@npm:^1.2.1": +"esm-env@npm:^1.2.1": version: 1.2.2 resolution: "esm-env@npm:1.2.2" checksum: 10c0/3d25c973f2fd69c25ffff29c964399cea573fe10795ecc1d26f6f957ce0483d3254e1cceddb34bf3296a0d7b0f1d53a28992f064ba509dfe6366751e752c4166 @@ -2569,7 +2526,6 @@ __metadata: "@types/jsdom": "npm:28.0.1" "@vitest/browser-playwright": "npm:4.1.5" "@vitest/coverage-v8": "npm:4.1.5" - bits-ui: "npm:2.18.1" clsx: "npm:^2.1.1" dprint: "npm:0.54.0" jsdom: "npm:29.1.1" @@ -2671,13 +2627,6 @@ __metadata: languageName: node linkType: hard -"inline-style-parser@npm:0.2.7": - version: 0.2.7 - resolution: "inline-style-parser@npm:0.2.7" - checksum: 10c0/d884d76f84959517430ae6c22f0bda59bb3f58f539f99aac75a8d786199ec594ed648c6ab4640531f9fc244b0ed5cd8c458078e592d016ef06de793beb1debff - languageName: node - linkType: hard - "ip-address@npm:^10.0.1": version: 10.1.0 resolution: "ip-address@npm:10.1.0" @@ -3742,23 +3691,6 @@ __metadata: languageName: node linkType: hard -"runed@npm:^0.35.1": - version: 0.35.1 - resolution: "runed@npm:0.35.1" - dependencies: - dequal: "npm:^2.0.3" - esm-env: "npm:^1.0.0" - lz-string: "npm:^1.5.0" - peerDependencies: - "@sveltejs/kit": ^2.21.0 - svelte: ^5.7.0 - peerDependenciesMeta: - "@sveltejs/kit": - optional: true - checksum: 10c0/ea6c6ba684b52075a5991a0b79d4c381d987f802eabe5689afd495589fdf6fa5aae7eae6843091364b8602643b342deda85f99267c2ff837c83c28d5d9e771ce - languageName: node - linkType: hard - "sade@npm:^1.7.4": version: 1.8.1 resolution: "sade@npm:1.8.1" @@ -3948,15 +3880,6 @@ __metadata: languageName: node linkType: hard -"style-to-object@npm:^1.0.8": - version: 1.0.14 - resolution: "style-to-object@npm:1.0.14" - dependencies: - inline-style-parser: "npm:0.2.7" - checksum: 10c0/854d9e9b77afc336e6d7b09348e7939f2617b34eb0895824b066d8cd1790284cb6d8b2ba36be88025b2595d715dba14b299ae76e4628a366541106f639e13679 - languageName: node - linkType: hard - "supports-color@npm:^7.1.0": version: 7.2.0 resolution: "supports-color@npm:7.2.0" @@ -4026,19 +3949,6 @@ __metadata: languageName: node linkType: hard -"svelte-toolbelt@npm:^0.10.6": - version: 0.10.6 - resolution: "svelte-toolbelt@npm:0.10.6" - dependencies: - clsx: "npm:^2.1.1" - runed: "npm:^0.35.1" - style-to-object: "npm:^1.0.8" - peerDependencies: - svelte: ^5.30.2 - checksum: 10c0/1d8edc5ba5daba4b97e427f1a324f86157b0e9efd98acdd88e852a3c901a7e0ad06170422376d24bf9dad8016ef06075f298778b37b91335ba51599b5ae9c8af - languageName: node - linkType: hard - "svelte2tsx@npm:^0.7.44": version: 0.7.46 resolution: "svelte2tsx@npm:0.7.46" @@ -4118,13 +4028,6 @@ __metadata: languageName: node linkType: hard -"tabbable@npm:^6.2.0": - version: 6.3.0 - resolution: "tabbable@npm:6.3.0" - checksum: 10c0/57ba019d29b5cfa0c862248883bcec0e6d29d8f156ba52a1f425e7cfeca4a0fc701ab8d035c4c86ddf74ecdbd0e9f454a88d9b55d924a51f444038e9cd14d7a0 - languageName: node - linkType: hard - "tailwind-merge@npm:3.5.0": version: 3.5.0 resolution: "tailwind-merge@npm:3.5.0"