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}
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 })}
-
+
+
+ {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)}
+ Open popover
+ {/snippet}
+ {#snippet children()}
+ Popover content
+ {/snippet}
+
+
+ {/snippet}
+
+
+
+ {#snippet template()}
+
+
+ {#snippet trigger(props)}
+ Open popover
+ {/snippet}
+ {#snippet children()}
+ Popover content
+ {/snippet}
+
+
+ {/snippet}
+
+
+
+
+ {#snippet template()}
+
+
+ {#snippet trigger(props)}
+ Open menu
+ {/snippet}
+ {#snippet children({ close })}
+
+
Menu header
+
+ Aligned to the trigger's end edge.
+
+
+ Close
+
+
+ {/snippet}
+
+
+ {/snippet}
+
+
+
+
+ {#snippet template()}
+
+
+ {#snippet trigger(props)}
+ Adjust value
+ {/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)}
+ Open
+ {/snippet}
+ {#snippet children({ close })}
+
+ 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"