entasis 0.7.1 → 0.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (62) hide show
  1. package/README.md +7 -1
  2. package/dist/components/AppShell/appShell.theme.js +2 -2
  3. package/dist/components/FloatingWindow/FloatingWindow.svelte +26 -2
  4. package/dist/components/FloatingWindow/floatingWindow.dock.svelte.js +11 -2
  5. package/dist/components/FloatingWindow/floatingWindow.mcp.d.ts +1 -1
  6. package/dist/components/FloatingWindow/floatingWindow.mcp.js +7 -4
  7. package/dist/components/FloatingWindow/floatingWindow.props.d.ts +6 -0
  8. package/dist/components/FloatingWindow/floatingWindow.state.svelte.d.ts +3 -0
  9. package/dist/components/FloatingWindow/floatingWindow.state.svelte.js +18 -4
  10. package/dist/components/FloatingWindow/floatingWindow.theme.d.ts +3 -0
  11. package/dist/components/FloatingWindow/floatingWindow.theme.js +20 -7
  12. package/dist/components/Form/File/FileInput.svelte +6 -42
  13. package/dist/components/Form/Form/form.state.svelte.d.ts +4 -0
  14. package/dist/components/Form/Form/visibility.d.ts +2 -0
  15. package/dist/components/Form/MultiStepForm/multiStepForm.state.svelte.d.ts +4 -0
  16. package/dist/components/Form/Select/Select.svelte +28 -2
  17. package/dist/components/Form/Select/select.align.d.ts +55 -0
  18. package/dist/components/Form/Select/select.align.js +41 -0
  19. package/dist/components/Form/Select/select.mcp.d.ts +1 -1
  20. package/dist/components/Form/Select/select.mcp.js +7 -3
  21. package/dist/components/Form/Select/select.props.d.ts +8 -0
  22. package/dist/components/Form/Select/select.state.svelte.d.ts +24 -0
  23. package/dist/components/Form/Select/select.state.svelte.js +111 -1
  24. package/dist/components/Popover/Popover.svelte +4 -0
  25. package/dist/components/Popover/popover.mcp.d.ts +1 -1
  26. package/dist/components/Popover/popover.mcp.js +1 -0
  27. package/dist/components/Popover/popover.props.d.ts +16 -0
  28. package/dist/components/Popover/popover.state.svelte.d.ts +1 -1
  29. package/dist/components/Popover/popover.state.svelte.js +13 -0
  30. package/dist/components/Sidebar/Sidebar.svelte +58 -10
  31. package/dist/components/Sidebar/Sidebar.svelte.d.ts +1 -1
  32. package/dist/components/Sidebar/SidebarMenuItem.svelte +19 -2
  33. package/dist/components/Sidebar/SidebarMenuItem.svelte.d.ts +4 -0
  34. package/dist/components/Sidebar/SidebarPanel.svelte +199 -88
  35. package/dist/components/Sidebar/SidebarPanel.svelte.d.ts +3 -0
  36. package/dist/components/Sidebar/SidebarViewStage.svelte +103 -0
  37. package/dist/components/Sidebar/SidebarViewStage.svelte.d.ts +18 -0
  38. package/dist/components/Sidebar/index.d.ts +1 -1
  39. package/dist/components/Sidebar/sidebar.mcp.d.ts +1 -1
  40. package/dist/components/Sidebar/sidebar.mcp.js +19 -2
  41. package/dist/components/Sidebar/sidebar.props.d.ts +56 -1
  42. package/dist/components/Sidebar/sidebar.state.svelte.d.ts +4 -0
  43. package/dist/components/Sidebar/sidebar.state.svelte.js +9 -1
  44. package/dist/components/Sidebar/sidebar.theme.d.ts +128 -5
  45. package/dist/components/Sidebar/sidebar.theme.js +69 -3
  46. package/dist/components/Sidebar/sidebar.views.svelte.d.ts +138 -0
  47. package/dist/components/Sidebar/sidebar.views.svelte.js +303 -0
  48. package/dist/components/Theme/theme.floatingWindows.d.ts +6 -1
  49. package/dist/components/Theme/theme.floatingWindows.js +7 -2
  50. package/dist/components/Theme/theme.mcp.d.ts +1 -1
  51. package/dist/components/Theme/theme.mcp.js +1 -0
  52. package/dist/generated/componentContract.d.ts +1 -1
  53. package/dist/generated/componentContract.js +1 -0
  54. package/dist/generated/componentMcpRegistry.d.ts +6 -6
  55. package/dist/tailwind/index.mcp.d.ts +1 -1
  56. package/dist/tailwind/index.mcp.js +4 -0
  57. package/dist/tailwind/scales.js +11 -3
  58. package/dist/tailwind/spacing.js +17 -0
  59. package/dist/utils/cva/merge.d.ts +7 -0
  60. package/dist/utils/cva/merge.js +6 -1
  61. package/dist/utils/pointerDrag.js +7 -1
  62. package/package.json +1 -1
package/README.md CHANGED
@@ -320,7 +320,13 @@ Snippet props: `size` (px number or CSS length, default `1lh`), `color` (a role
320
320
 
321
321
  - `pnpm dev` runs the documentation site with a page per component under `/components/<name>`.
322
322
  - `.claude/skills/entasis/` (mirrored in `.agents/skills/entasis/`) holds the coding-agent skill: import conventions, per-component references, theming notes.
323
- - Each component folder ships a `*.mcp.ts` description used by the MCP integration.
323
+ - Each component folder ships a `*.mcp.ts` description, served by the MCP server the docs site runs at
324
+ `https://entasis.beynar.workers.dev/mcp` (one `components` tool that returns a component's documentation).
325
+ Add it to a project for Claude Code with:
326
+
327
+ ```bash
328
+ claude mcp add --transport http --scope project entasis https://entasis.beynar.workers.dev/mcp
329
+ ```
324
330
 
325
331
  ## Releasing
326
332
 
@@ -42,8 +42,8 @@ const defaultPage = cva({
42
42
  variant: {
43
43
  admin: 'bg-surface [--page-shell-chrome:var(--color-surface-canvas)] [--page-shell-surface:var(--color-surface)]',
44
44
  floating: 'bg-surface-canvas [--page-shell-chrome:var(--color-surface)] [--page-shell-surface:var(--color-surface-canvas)] [--page-shell-chrome-inline-gap:0.5rem] [--page-shell-chrome-block-gap:0.5rem] [--page-shell-header-top-radius:var(--radius-lg)] [--page-shell-header-bottom-radius:var(--radius-lg)] [--page-shell-footer-top-radius:var(--radius-lg)] [--page-shell-footer-bottom-radius:var(--radius-lg)] [--page-shell-chrome-border:var(--color-neutral-muted)] [--page-shell-chrome-shadow:var(--elevation-1)]',
45
- inset: 'bg-surface [--page-shell-chrome:var(--color-surface)] [--page-shell-surface:var(--color-surface)] transition-[border-color,box-shadow] md:raised-1 md:group-data-[display-state=hidden]/sidebar-wrapper:border-transparent md:group-data-[display-state=hidden]/sidebar-wrapper:shadow-none',
46
- split: 'bg-surface [--page-shell-chrome:var(--color-surface)] [--page-shell-surface:var(--color-surface)] transition-[border-color,box-shadow] md:raised-1 md:group-data-[display-state=hidden]/sidebar-wrapper:border-transparent md:group-data-[display-state=hidden]/sidebar-wrapper:shadow-none',
45
+ inset: 'bg-surface [--page-shell-chrome:var(--color-surface)] [--page-shell-surface:var(--color-surface)] transition-[border-color,box-shadow] md:raised-1 md:group-data-[page-flush=true]/sidebar-wrapper:border-transparent md:group-data-[page-flush=true]/sidebar-wrapper:shadow-none',
46
+ split: 'bg-surface [--page-shell-chrome:var(--color-surface)] [--page-shell-surface:var(--color-surface)] transition-[border-color,box-shadow] md:raised-1 md:group-data-[page-flush=true]/sidebar-wrapper:border-transparent md:group-data-[page-flush=true]/sidebar-wrapper:shadow-none',
47
47
  // The frame already owns the radius, border and elevation, so the page inside it is flat,
48
48
  // and its header chrome is the same surface as the content: the white column is one
49
49
  // piece whose only corners are the frame's own.
@@ -1,7 +1,7 @@
1
1
  <script lang="ts">
2
2
  import { untrack } from 'svelte';
3
3
  import { createBindableValue } from '../../utils/state.svelte.js';
4
- import { crossfade, scale } from 'svelte/transition';
4
+ import { crossfade, fade, scale } from 'svelte/transition';
5
5
  import { easingFunctions } from '../../transitions/easingFunctions.js';
6
6
  import Button from '../Button/Button.svelte';
7
7
  import { minusIcon } from '../Icons/minus.js';
@@ -32,6 +32,7 @@
32
32
  minimizable = true,
33
33
  closable = true,
34
34
  closeOnEscape = true,
35
+ backdrop = false,
35
36
  position = $bindable(),
36
37
  dimensions = $bindable({ width: 480, height: 320 }),
37
38
  class: className,
@@ -116,6 +117,9 @@
116
117
  get closeOnEscape() {
117
118
  return closeOnEscape;
118
119
  },
120
+ get backdrop() {
121
+ return backdrop;
122
+ },
119
123
  get position() {
120
124
  return position;
121
125
  },
@@ -202,6 +206,26 @@
202
206
  };
203
207
  </script>
204
208
 
209
+ {#if backdrop && openState.value && !minimized && windowState.position}
210
+ <!-- Portaled into the window layer one index below the window; it fades on the window's own
211
+ enter / exit timing, so minimizing lifts it while the window flies to the dock. -->
212
+ <div
213
+ {@attach windowState.theme.floatingWindows.portal}
214
+ data-slot="floating-window-backdrop"
215
+ aria-hidden="true"
216
+ class={classes.backdrop()}
217
+ style:z-index={windowState.backdropZIndex}
218
+ in:fade={{
219
+ duration: enter.in.duration ?? 0,
220
+ easing: easingFunctions[enter.in.easing ?? 'cubicOut']
221
+ }}
222
+ out:fade={{
223
+ duration: exit.out.duration ?? 0,
224
+ easing: easingFunctions[exit.out.easing ?? 'cubicOut']
225
+ }}
226
+ ></div>
227
+ {/if}
228
+
205
229
  {#if openState.value && !minimized && windowState.position}
206
230
  <div
207
231
  {@attach windowState.theme.floatingWindows.portal}
@@ -214,7 +238,7 @@
214
238
  bind:this={ref}
215
239
  {id}
216
240
  role="dialog"
217
- aria-modal="false"
241
+ aria-modal={backdrop ? 'true' : 'false'}
218
242
  aria-labelledby={`${id}-title`}
219
243
  aria-keyshortcuts={draggable
220
244
  ? 'Alt+ArrowUp Alt+ArrowRight Alt+ArrowDown Alt+ArrowLeft'
@@ -5,6 +5,11 @@ const DOCK_ITEM_WIDTH = 176;
5
5
  const DOCK_ITEM_HEIGHT = 36;
6
6
  const DOCK_GAP = 8;
7
7
  const DOCK_INLINE_GAP = 8;
8
+ const DOCK_DRAG_ACTIVATION = Object.freeze({
9
+ distancePx: 4,
10
+ touchDelayMs: 300,
11
+ touchTolerancePx: 8
12
+ });
8
13
  const clamp = (value, min, max) => Math.min(Math.max(value, min), max);
9
14
  export class FloatingWindowDockState {
10
15
  windowState;
@@ -98,6 +103,10 @@ export class FloatingWindowDockState {
98
103
  };
99
104
  drag = createPointerDrag({
100
105
  disabled: () => !this.windowState.draggable || !this.windowState.minimized,
106
+ // The item is the title button's container, not a handle: a press that never travels must
107
+ // keep the pointer on the button, or its click lands on the item and never restores.
108
+ capture: 'on-activate',
109
+ activation: () => DOCK_DRAG_ACTIVATION,
101
110
  onStart: (payload) => this.startMove(payload),
102
111
  onMove: (payload) => this.updateMove(payload),
103
112
  onEnd: (payload) => this.endMove(payload)
@@ -115,8 +124,8 @@ export class FloatingWindowDockState {
115
124
  return Math.max(1, Math.floor((availableLength - DOCK_INLINE_GAP * 2 + DOCK_GAP) / (itemLength + DOCK_GAP)));
116
125
  }
117
126
  startMove(payload) {
118
- if (payload.event.target instanceof Element &&
119
- payload.event.target.closest('[data-floating-window-dock-actions]')) {
127
+ if (payload.startTarget instanceof Element &&
128
+ payload.startTarget.closest('[data-floating-window-dock-actions]')) {
120
129
  return false;
121
130
  }
122
131
  this.windowState.activateSurface('dock');
@@ -1 +1 @@
1
- export declare const floatingWindowDescription = "\n# FloatingWindow Component\n\nFloatingWindow renders a non-modal, portaled utility window that can be moved, resized, minimized into a configurable viewport-edge dock, restored, and closed. Multiple windows inside the same Theme provider coordinate their z-order and stack independently by dock placement. The Theme keeps floating windows below modal Dialog surfaces, so an open window remains mounted behind a dialog and returns unchanged when the dialog closes.\n\n## Basic Usage\n\n```svelte\n<script lang=\"ts\">\n import { Button } from '../Button/index.ts';\n import { FloatingWindow } from './index.ts';\n\n let open = $state(false);\n</script>\n\n<Button onclick={() => (open = true)}>Open notes</Button>\n\n<FloatingWindow bind:open title=\"Notes\">\n <p>Window content remains interactive alongside the page.</p>\n</FloatingWindow>\n```\n\n## Props\n\n- **id**: string - Stable DOM id. Generated when omitted.\n- **open**: boolean (default: true) - Bindable rendered state.\n- **defaultOpen**: boolean (default: true) - Initial state when open is not provided.\n- **minimized**: boolean (default: false) - Bindable docked state.\n- **dockPlacement**: 'bottom-left' | 'bottom-right' | 'top-left' | 'top-right' | 'left-top' | 'left-bottom' | 'right-top' | 'right-bottom' (default: 'bottom-left') - Edge and alignment used by the minimized dock. Top and bottom placements stack horizontally; left and right placements use a vertical title bar and stack vertically.\n- **title**: Slot<FloatingWindowPayload> (required) - Window title as text or a snippet.\n- **children**: Slot<FloatingWindowPayload> - Main content.\n- **dragFrom**: 'header' | 'window' (default: 'header') - Restricts dragging to the header or allows any non-interactive surface to start a drag.\n- **draggable**: boolean (default: true) - Enables pointer dragging.\n- **resizable**: boolean (default: true) - Enables four edge and four corner resize handles.\n- **minimizable**: boolean (default: true) - Shows the minimize control.\n- **closable**: boolean (default: true) - Shows the close control.\n- **closeOnEscape**: boolean (default: true) - Closes the topmost expanded floating window when Escape is pressed.\n- **position**: { x: number; y: number } - Bindable viewport-relative top-left position. The first render is centered when omitted.\n- **dimensions**: { width: number; height: number; min?: [width, height]; max?: [width, height] } (default: 480 x 320, minimum 280 x 160) - Bindable pixel dimensions and optional constraint tuples. Maximum dimensions remain additionally constrained to the viewport.\n- **class**: string - Additional classes on the visible window.\n- **theme**: FloatingWindowThemeProps - Per-instance theme overrides.\n- **ref**: HTMLDivElement - Bindable reference to the visible window or minimized dock item.\n- **onOpenChange**: (open: boolean) => void - Runs once for each library-requested state change.\n- **onAfterOpen**: (payload) => void - Runs after the open transition finishes.\n- **onAfterClose**: (payload) => void - Runs after the close transition finishes.\n- **onMinimize**: (payload) => void - Runs after minimize state updates.\n- **onRestore**: (payload) => void - Runs after restore state updates.\n- **onMove**: ({ position, window }) => void - Runs when a move commits.\n- **onResize**: ({ dimensions, window }) => void - Runs when a pointer or keyboard resize commits.\n\n## Dragging\n\nHeader dragging is the default because it preserves text selection and content interactions. With `dragFrom=\"window\"`, buttons, links, inputs, editable content, resize handles, and descendants marked `data-floating-window-no-drag` remain excluded from drag starts.\n\n## Accessibility\n\n- The expanded surface uses a non-modal `dialog` role and is labelled by its title.\n- Close, minimize, and restore controls are native Entasis buttons with accessible labels.\n- Edge resize handles use `separator` semantics and support arrow-key resizing; hold Shift for a larger step.\n- Opening focuses the non-modal window, closing restores focus to its previous owner, and only the topmost expanded floating window handles Escape.\n- Alt+Arrow moves the focused window; hold Shift for a larger step.\n- Corner handles are pointer-only because a diagonal separator has no valid ARIA orientation.\n- The component does not trap focus or hide page content because it is explicitly non-modal.\n\n## Theme Parts\n\n- **root**: Floating window surface and drag/resize states.\n- **header**: Default title bar and drag handle.\n- **title**: Header title.\n- **actions**: Header control group.\n- **control**: Header and dock icon buttons.\n- **scrollArea**: Flexible ScrollArea root that owns body scrolling.\n- **content**: Padded content inside the ScrollArea viewport.\n- **resizeHandle**: Edge and corner handles by direction.\n- **dockItem**: Minimized surface.\n- **dockTitle**: Full-width restore button in the dock item.\n- **dockTitleText**: Truncated title text and lateral writing direction.\n- **dockActions**: Dock restore and close controls.\n\n## Motion\n\n- **motion** theme slot, keyed by `phase`: `flight` times the crossfade between window and\n dock pill, `enter` / `exit` the scale fallback when there is no counterpart.\n- Only `duration` / `easing` (plus the fallback's `scale` / `opacity`) are read.\n- Ladder: `<Theme components={{ 'floating-window': { motion } }}>` \u2192\n `setFloatingWindowTheme({ motion })` \u2192 `theme.motion`. Read once, at mount.\n";
1
+ export declare const floatingWindowDescription = "\n# FloatingWindow Component\n\nFloatingWindow renders a portaled utility window, non-modal unless `backdrop` is set, that can be moved, resized, minimized into a configurable viewport-edge dock, restored, and closed. Multiple windows inside the same Theme provider coordinate their z-order and stack independently by dock placement. The Theme keeps floating windows below modal Dialog surfaces, so an open window remains mounted behind a dialog and returns unchanged when the dialog closes.\n\n## Basic Usage\n\n```svelte\n<script lang=\"ts\">\n import { Button } from '../Button/index.ts';\n import { FloatingWindow } from './index.ts';\n\n let open = $state(false);\n</script>\n\n<Button onclick={() => (open = true)}>Open notes</Button>\n\n<FloatingWindow bind:open title=\"Notes\">\n <p>Window content remains interactive alongside the page.</p>\n</FloatingWindow>\n```\n\n## Props\n\n- **id**: string - Stable DOM id. Generated when omitted.\n- **open**: boolean (default: true) - Bindable rendered state.\n- **defaultOpen**: boolean (default: true) - Initial state when open is not provided.\n- **minimized**: boolean (default: false) - Bindable docked state.\n- **dockPlacement**: 'bottom-left' | 'bottom-right' | 'top-left' | 'top-right' | 'left-top' | 'left-bottom' | 'right-top' | 'right-bottom' (default: 'bottom-left') - Edge and alignment used by the minimized dock. Top and bottom placements stack horizontally; left and right placements use a vertical title bar and stack vertically.\n- **title**: Slot<FloatingWindowPayload> (required) - Window title as text or a snippet.\n- **children**: Slot<FloatingWindowPayload> - Main content.\n- **dragFrom**: 'header' | 'window' (default: 'header') - Restricts dragging to the header or allows any non-interactive surface to start a drag.\n- **draggable**: boolean (default: true) - Enables pointer dragging.\n- **resizable**: boolean (default: true) - Enables four edge and four corner resize handles.\n- **minimizable**: boolean (default: true) - Shows the minimize control.\n- **closable**: boolean (default: true) - Shows the close control.\n- **closeOnEscape**: boolean (default: true) - Closes the topmost expanded floating window when Escape is pressed.\n- **backdrop**: boolean (default: false) - Dims the page behind the expanded window and makes it modal like a Dialog: Tab stays inside, the rest of the page is inert, and page scroll is locked. Minimizing into the dock lifts all of it; restoring brings it back. Clicking the backdrop does nothing: close with the close control or Escape.\n- **position**: { x: number; y: number } - Bindable viewport-relative top-left position. The first render is centered when omitted.\n- **dimensions**: { width: number; height: number; min?: [width, height]; max?: [width, height] } (default: 480 x 320, minimum 280 x 160) - Bindable pixel dimensions and optional constraint tuples. Maximum dimensions remain additionally constrained to the viewport.\n- **class**: string - Additional classes on the visible window.\n- **theme**: FloatingWindowThemeProps - Per-instance theme overrides.\n- **ref**: HTMLDivElement - Bindable reference to the visible window or minimized dock item.\n- **onOpenChange**: (open: boolean) => void - Runs once for each library-requested state change.\n- **onAfterOpen**: (payload) => void - Runs after the open transition finishes.\n- **onAfterClose**: (payload) => void - Runs after the close transition finishes.\n- **onMinimize**: (payload) => void - Runs after minimize state updates.\n- **onRestore**: (payload) => void - Runs after restore state updates.\n- **onMove**: ({ position, window }) => void - Runs when a move commits.\n- **onResize**: ({ dimensions, window }) => void - Runs when a pointer or keyboard resize commits.\n\n## Dragging\n\nHeader dragging is the default because it preserves text selection and content interactions. With `dragFrom=\"window\"`, buttons, links, inputs, editable content, resize handles, and descendants marked `data-floating-window-no-drag` remain excluded from drag starts.\n\n## Accessibility\n\n- The expanded surface uses a `dialog` role labelled by its title: `aria-modal=\"false\"`, or `\"true\"` with `backdrop`.\n- Close, minimize, and restore controls are native Entasis buttons with accessible labels.\n- Edge resize handles use `separator` semantics and support arrow-key resizing; hold Shift for a larger step.\n- Opening focuses the window, closing restores focus to its previous owner, and only the topmost expanded floating window handles Escape.\n- Alt+Arrow moves the focused window; hold Shift for a larger step.\n- Corner handles are pointer-only because a diagonal separator has no valid ARIA orientation.\n- Without `backdrop` it does not trap focus or hide page content. With it, Tab and Shift+Tab stay inside the window, every sibling subtree up to `<body>` is `inert` (other windows and dock items included), and page scroll is locked, until the window closes or is minimized.\n\n## Theme Parts\n\n- **backdrop**: Page dim behind a window opened with `backdrop`, one layer below the window.\n- **root**: Floating window surface and drag/resize states.\n- **header**: Default title bar and drag handle.\n- **title**: Header title.\n- **actions**: Header control group.\n- **control**: Header and dock icon buttons.\n- **scrollArea**: Flexible ScrollArea root that owns body scrolling.\n- **content**: Padded content inside the ScrollArea viewport.\n- **resizeHandle**: Edge and corner handles by direction.\n- **dockItem**: Minimized surface.\n- **dockTitle**: Full-width restore button in the dock item.\n- **dockTitleText**: Truncated title text and lateral writing direction.\n- **dockActions**: Dock restore and close controls.\n\n## Motion\n\n- **motion** theme slot, keyed by `phase`: `flight` times the crossfade between window and\n dock pill, `enter` / `exit` the scale fallback when there is no counterpart.\n- Only `duration` / `easing` (plus the fallback's `scale` / `opacity`) are read.\n- The backdrop fades on the `enter` / `exit` timings.\n- Ladder: `<Theme components={{ 'floating-window': { motion } }}>` \u2192\n `setFloatingWindowTheme({ motion })` \u2192 `theme.motion`. Read once, at mount.\n";
@@ -1,7 +1,7 @@
1
1
  export const floatingWindowDescription = `
2
2
  # FloatingWindow Component
3
3
 
4
- FloatingWindow renders a non-modal, portaled utility window that can be moved, resized, minimized into a configurable viewport-edge dock, restored, and closed. Multiple windows inside the same Theme provider coordinate their z-order and stack independently by dock placement. The Theme keeps floating windows below modal Dialog surfaces, so an open window remains mounted behind a dialog and returns unchanged when the dialog closes.
4
+ FloatingWindow renders a portaled utility window, non-modal unless \`backdrop\` is set, that can be moved, resized, minimized into a configurable viewport-edge dock, restored, and closed. Multiple windows inside the same Theme provider coordinate their z-order and stack independently by dock placement. The Theme keeps floating windows below modal Dialog surfaces, so an open window remains mounted behind a dialog and returns unchanged when the dialog closes.
5
5
 
6
6
  ## Basic Usage
7
7
 
@@ -35,6 +35,7 @@ FloatingWindow renders a non-modal, portaled utility window that can be moved, r
35
35
  - **minimizable**: boolean (default: true) - Shows the minimize control.
36
36
  - **closable**: boolean (default: true) - Shows the close control.
37
37
  - **closeOnEscape**: boolean (default: true) - Closes the topmost expanded floating window when Escape is pressed.
38
+ - **backdrop**: boolean (default: false) - Dims the page behind the expanded window and makes it modal like a Dialog: Tab stays inside, the rest of the page is inert, and page scroll is locked. Minimizing into the dock lifts all of it; restoring brings it back. Clicking the backdrop does nothing: close with the close control or Escape.
38
39
  - **position**: { x: number; y: number } - Bindable viewport-relative top-left position. The first render is centered when omitted.
39
40
  - **dimensions**: { width: number; height: number; min?: [width, height]; max?: [width, height] } (default: 480 x 320, minimum 280 x 160) - Bindable pixel dimensions and optional constraint tuples. Maximum dimensions remain additionally constrained to the viewport.
40
41
  - **class**: string - Additional classes on the visible window.
@@ -54,16 +55,17 @@ Header dragging is the default because it preserves text selection and content i
54
55
 
55
56
  ## Accessibility
56
57
 
57
- - The expanded surface uses a non-modal \`dialog\` role and is labelled by its title.
58
+ - The expanded surface uses a \`dialog\` role labelled by its title: \`aria-modal="false"\`, or \`"true"\` with \`backdrop\`.
58
59
  - Close, minimize, and restore controls are native Entasis buttons with accessible labels.
59
60
  - Edge resize handles use \`separator\` semantics and support arrow-key resizing; hold Shift for a larger step.
60
- - Opening focuses the non-modal window, closing restores focus to its previous owner, and only the topmost expanded floating window handles Escape.
61
+ - Opening focuses the window, closing restores focus to its previous owner, and only the topmost expanded floating window handles Escape.
61
62
  - Alt+Arrow moves the focused window; hold Shift for a larger step.
62
63
  - Corner handles are pointer-only because a diagonal separator has no valid ARIA orientation.
63
- - The component does not trap focus or hide page content because it is explicitly non-modal.
64
+ - Without \`backdrop\` it does not trap focus or hide page content. With it, Tab and Shift+Tab stay inside the window, every sibling subtree up to \`<body>\` is \`inert\` (other windows and dock items included), and page scroll is locked, until the window closes or is minimized.
64
65
 
65
66
  ## Theme Parts
66
67
 
68
+ - **backdrop**: Page dim behind a window opened with \`backdrop\`, one layer below the window.
67
69
  - **root**: Floating window surface and drag/resize states.
68
70
  - **header**: Default title bar and drag handle.
69
71
  - **title**: Header title.
@@ -82,6 +84,7 @@ Header dragging is the default because it preserves text selection and content i
82
84
  - **motion** theme slot, keyed by \`phase\`: \`flight\` times the crossfade between window and
83
85
  dock pill, \`enter\` / \`exit\` the scale fallback when there is no counterpart.
84
86
  - Only \`duration\` / \`easing\` (plus the fallback's \`scale\` / \`opacity\`) are read.
87
+ - The backdrop fades on the \`enter\` / \`exit\` timings.
85
88
  - Ladder: \`<Theme components={{ 'floating-window': { motion } }}>\` →
86
89
  \`setFloatingWindowTheme({ motion })\` → \`theme.motion\`. Read once, at mount.
87
90
  `;
@@ -66,6 +66,12 @@ export type FloatingWindowProps = WithAttachments<{
66
66
  closable?: boolean;
67
67
  /** Closes the topmost expanded floating window when Escape is pressed. */
68
68
  closeOnEscape?: boolean;
69
+ /**
70
+ * Dims the page behind the expanded window and makes the window modal, like a Dialog: Tab
71
+ * stays inside, the rest of the page is inert, and page scroll is locked. Minimizing into the
72
+ * dock lifts all of it; restoring brings it back.
73
+ */
74
+ backdrop?: boolean;
69
75
  /** Bindable viewport-relative top-left position in pixels. Defaults to centered. */
70
76
  position?: FloatingWindowPosition;
71
77
  /** Bindable width and height in pixels, with optional [width, height] constraint tuples. */
@@ -12,6 +12,7 @@ type FloatingWindowStateOptions = {
12
12
  minimizable: boolean;
13
13
  closable: boolean;
14
14
  closeOnEscape: boolean;
15
+ backdrop: boolean;
15
16
  position?: FloatingWindowPosition;
16
17
  dimensions: FloatingWindowDimensions;
17
18
  onOpenChange?: (open: boolean) => void;
@@ -61,6 +62,8 @@ export declare class FloatingWindowState extends FloatingWindowState_base {
61
62
  windowDrag: Attachment<HTMLElement>;
62
63
  resize(direction: FloatingWindowResizeDirection): Attachment<HTMLElement>;
63
64
  activateSurface(type: FloatingWindowSurface): void;
65
+ /** The index reserved just below the window when it activated with a backdrop. */
66
+ get backdropZIndex(): number;
64
67
  bringToFront(): void;
65
68
  close(): void;
66
69
  minimize(): void;
@@ -4,6 +4,7 @@ import { createPointerDrag } from '../../utils/pointerDrag.js';
4
4
  import { withOptions } from '../../utils/state.svelte.js';
5
5
  import { useTheme } from '../Theme/theme.state.svelte.js';
6
6
  import { useFocusScope } from '../../utils/useFocusScope.svelte.js';
7
+ import { useScrollLock } from '../../utils/useScrollLock.svelte.js';
7
8
  import { FloatingWindowDockState } from './floatingWindow.dock.svelte.js';
8
9
  import { FLOATING_WINDOW_VIEWPORT_GAP, FloatingWindowGeometry } from './floatingWindow.geometry.js';
9
10
  const KEYBOARD_STEP = 10;
@@ -25,17 +26,20 @@ export class FloatingWindowState extends withOptions() {
25
26
  viewportHeight = $state(0);
26
27
  geometry;
27
28
  docking;
28
- // Non-modal: focus lands on the window when it opens and returns to the opener on close.
29
+ // Focus lands on the window when it opens and returns to the opener on close. Non-modal by
30
+ // default; with a backdrop the window is modal like a Dialog: Tab stays inside and every
31
+ // sibling subtree up to <body> is inert.
29
32
  focusScope = useFocusScope({
30
33
  isActive: () => this.open && !this.minimized,
31
- trap: () => false,
34
+ trap: () => this.backdrop,
35
+ inertSiblings: () => this.backdrop,
32
36
  initialFocus: () => 'container'
33
37
  });
34
38
  // Escape is dispatched by the shared layer stack to the topmost layer only.
35
39
  layer = this.theme.layers.register({
36
40
  kind: 'floating-window',
37
41
  isOpen: () => this.open && !this.minimized,
38
- isModal: () => false,
42
+ isModal: () => this.backdrop,
39
43
  dismissOnEscape: () => this.closeOnEscape && this.closable && this.theme.floatingWindows.isTopWindow(this.id),
40
44
  onDismiss: () => this.close(),
41
45
  state: this
@@ -47,6 +51,12 @@ export class FloatingWindowState extends withOptions() {
47
51
  resizeAttachments = plainMap();
48
52
  constructor(options) {
49
53
  super(options);
54
+ useScrollLock({ isActive: () => this.backdrop && this.open && !this.minimized });
55
+ // A backdrop switched on while the window is open needs the index below it reserved.
56
+ $effect(() => {
57
+ if (this.backdrop && this.open && !this.minimized)
58
+ untrack(() => this.bringToFront());
59
+ });
50
60
  this.geometry = new FloatingWindowGeometry(this);
51
61
  this.docking = new FloatingWindowDockState(this);
52
62
  if (typeof window !== 'undefined') {
@@ -153,7 +163,11 @@ export class FloatingWindowState extends withOptions() {
153
163
  return attachment;
154
164
  }
155
165
  activateSurface(type) {
156
- this.zIndex = this.theme.floatingWindows.activate(this.id, type);
166
+ this.zIndex = this.theme.floatingWindows.activate(this.id, type, type === 'window' && this.backdrop);
167
+ }
168
+ /** The index reserved just below the window when it activated with a backdrop. */
169
+ get backdropZIndex() {
170
+ return this.zIndex - 1;
157
171
  }
158
172
  bringToFront() {
159
173
  this.activateSurface(this.minimized ? 'dock' : 'window');
@@ -32,6 +32,7 @@ export declare const floatingWindowTheme: {
32
32
  };
33
33
  };
34
34
  }>;
35
+ backdrop: (props?: import("../../utils/cva/types.js").CVAClassProp | undefined) => string;
35
36
  root: (props?: (import("../../utils/cva/types.js").CVAVariantSchema<{
36
37
  dragFrom: {
37
38
  header: string;
@@ -129,6 +130,7 @@ export declare const setFloatingWindowTheme: (theme: InferComponentTheme<{
129
130
  };
130
131
  };
131
132
  }>;
133
+ backdrop: (props?: import("../../utils/cva/types.js").CVAClassProp | undefined) => string;
132
134
  root: (props?: (import("../../utils/cva/types.js").CVAVariantSchema<{
133
135
  dragFrom: {
134
136
  header: string;
@@ -224,6 +226,7 @@ export declare const useFloatingWindowTheme: import("../../utils/cva/theme.js").
224
226
  };
225
227
  };
226
228
  }>;
229
+ backdrop: (props?: import("../../utils/cva/types.js").CVAClassProp | undefined) => string;
227
230
  root: (props?: (import("../../utils/cva/types.js").CVAVariantSchema<{
228
231
  dragFrom: {
229
232
  header: string;
@@ -24,7 +24,7 @@ const defaultFloatingWindow = cva({
24
24
  }
25
25
  });
26
26
  const defaultFloatingWindowHeader = cva({
27
- base: 'border-neutral-muted flex h-9 shrink-0 items-center gap-sm border-b px-md select-none',
27
+ base: 'border-neutral-muted flex h-9 shrink-0 items-center gap-sm border-b px-md-concentric select-none',
28
28
  variants: {
29
29
  draggable: {
30
30
  true: 'cursor-move touch-none',
@@ -66,17 +66,19 @@ const defaultFloatingWindowResizeHandle = cva({
66
66
  }
67
67
  });
68
68
  const defaultFloatingWindowDockItem = cva({
69
- base: 'bg-surface-floating text-neutral pointer-events-auto fixed flex touch-none items-center overflow-hidden lift-4 ring-1 ring-neutral-muted',
69
+ base: 'bg-surface-floating text-neutral pointer-events-auto fixed flex touch-none items-center overflow-hidden lift-4 ring-1 ring-neutral-muted [--dock-radius:min(var(--radius-xl),calc(var(--spacing)*3))]',
70
70
  variants: {
71
71
  orientation: {
72
72
  horizontal: 'h-9 flex-row',
73
73
  vertical: 'w-9 flex-col'
74
74
  },
75
75
  side: {
76
- top: 'rounded-t-none rounded-b-xl',
77
- right: 'rounded-r-none rounded-l-xl',
78
- bottom: 'rounded-t-xl rounded-b-none',
79
- left: 'rounded-r-xl rounded-l-none'
76
+ // The window's step, capped at a third of the 36px (`spacing × 9`) item: any rounder and a
77
+ // round theme turns the tab into a pill. At that cap `md` padding always clears the corner.
78
+ top: 'rounded-[var(--dock-radius)] rounded-t-none',
79
+ right: 'rounded-[var(--dock-radius)] rounded-r-none',
80
+ bottom: 'rounded-[var(--dock-radius)] rounded-b-none',
81
+ left: 'rounded-[var(--dock-radius)] rounded-l-none'
80
82
  },
81
83
  dragging: {
82
84
  true: 'cursor-grabbing select-none',
@@ -89,8 +91,13 @@ const defaultFloatingWindowDockItem = cva({
89
91
  dragging: false
90
92
  }
91
93
  });
94
+ // `!static`: the title is the pill's drag and restore surface, so its hover / press overlay (the
95
+ // Button's `state-layer` ::before) anchors to the fixed dock item instead of the button and tints
96
+ // the whole pill, clipped by its rounded overflow. The dock controls keep their own overlay.
97
+ // `active:translate-none` drops the Button's press nudge: any `translate` but `none`, 0 included,
98
+ // makes the title the anchor of its own overlay again and pulls the press tint back inside it.
92
99
  const defaultFloatingWindowDockTitle = cva({
93
- base: '!min-w-0 !flex-1 !rounded-none !text-xs !font-medium',
100
+ base: '!static active:translate-none !min-w-0 !flex-1 !rounded-none !text-xs !font-medium',
94
101
  variants: {
95
102
  orientation: {
96
103
  horizontal: '!h-full !w-auto !justify-start !px-md !text-left',
@@ -135,6 +142,11 @@ const defaultFloatingWindowDockActions = cva({
135
142
  // The window and its dock pill are two halves of one crossfade: `flight` times the
136
143
  // shared transform between them, `enter` / `exit` the scale fallback used when there
137
144
  // is no counterpart to fly to. Only `duration` / `easing` are read from each.
145
+ // The page dim behind a window opened with `backdrop`: the same wash as Dialog's backdrop, so a
146
+ // modal window and a modal dialog dim the page alike.
147
+ const defaultFloatingWindowBackdrop = cva({
148
+ base: 'fixed inset-0 bg-neutral/40 backdrop-blur-xs'
149
+ });
138
150
  export const defaultFloatingWindowMotion = motion({
139
151
  base: {
140
152
  in: { x: 0, y: 0, scale: 0.97, opacity: 0 },
@@ -153,6 +165,7 @@ export const defaultFloatingWindowMotion = motion({
153
165
  });
154
166
  export const floatingWindowTheme = {
155
167
  motion: defaultFloatingWindowMotion,
168
+ backdrop: defaultFloatingWindowBackdrop,
156
169
  root: defaultFloatingWindow,
157
170
  header: defaultFloatingWindowHeader,
158
171
  title: defaultFloatingWindowTitle,
@@ -9,6 +9,9 @@
9
9
  import { useFileInputTheme } from './fileInput.theme.js';
10
10
  import { slide } from 'svelte/transition';
11
11
  import Slot from '../../Slot/Slot.svelte';
12
+ import { fileIcon } from '../../Icons/file.js';
13
+ import { plusIcon } from '../../Icons/plus.js';
14
+ import { xIcon } from '../../Icons/x.js';
12
15
  import { useI18n } from '../../../i18n/context.svelte.js';
13
16
 
14
17
  let {
@@ -153,20 +156,7 @@
153
156
  />
154
157
  {#if dropzone.files.length === 0}
155
158
  <div transition:slide class={classes.placeholder({ class: placeholderClass, size: rest.size })}>
156
- <svg
157
- xmlns="http://www.w3.org/2000/svg"
158
- width="40"
159
- height="40"
160
- viewBox="0 0 24 24"
161
- fill="none"
162
- stroke="currentColor"
163
- stroke-width="2"
164
- stroke-linecap="round"
165
- stroke-linejoin="round"
166
- >
167
- <path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z" />
168
- <polyline points="14 2 14 8 20 8" />
169
- </svg>
159
+ {@render fileIcon({ size: 40 })}
170
160
  {#if resolvedPlaceholder}
171
161
  <span class="mt-2">{resolvedPlaceholder}</span>
172
162
  {/if}
@@ -192,20 +182,7 @@
192
182
  </div>
193
183
  <Button variant="ghost" size="small" squared onclick={() => dropzone.removeFile(fil)}>
194
184
  {#snippet prefix()}
195
- <svg
196
- xmlns="http://www.w3.org/2000/svg"
197
- width="16"
198
- height="16"
199
- viewBox="0 0 24 24"
200
- fill="none"
201
- stroke="currentColor"
202
- stroke-width="2"
203
- stroke-linecap="round"
204
- stroke-linejoin="round"
205
- >
206
- <line x1="18" y1="6" x2="6" y2="18" />
207
- <line x1="6" y1="6" x2="18" y2="18" />
208
- </svg>
185
+ {@render xIcon({ size: 16 })}
209
186
  {/snippet}
210
187
  </Button>
211
188
  </Slot>
@@ -214,20 +191,7 @@
214
191
  {#if mode === 'multiple' && dropzone.files.length > 0 && dropzone.files.length < maxFiles}
215
192
  <Button variant="soft" color="primary" fullWidth onclick={() => dropzone.open()}>
216
193
  {#snippet prefix()}
217
- <svg
218
- xmlns="http://www.w3.org/2000/svg"
219
- width="16"
220
- height="16"
221
- viewBox="0 0 24 24"
222
- fill="none"
223
- stroke="currentColor"
224
- stroke-width="2"
225
- stroke-linecap="round"
226
- stroke-linejoin="round"
227
- >
228
- <line x1="12" y1="5" x2="12" y2="19" />
229
- <line x1="5" y1="12" x2="19" y2="12" />
230
- </svg>
194
+ {@render plusIcon({ size: 16 })}
231
195
  {/snippet}
232
196
  {Number.isFinite(maxFiles)
233
197
  ? t.addMoreFiles(dropzone.files.length, maxFiles)
@@ -634,6 +634,7 @@ export declare class FormState<I extends FormInputs = FormInputs> {
634
634
  items?: import("../Select/select.props.js").SelectItems;
635
635
  density?: import("../../../types/theme.js").Density;
636
636
  separators?: boolean;
637
+ alignItemWithTrigger?: boolean;
637
638
  triggerAttrs?: import("../Select/select.props.js").SelectTriggerAttributes;
638
639
  i18n?: Partial<import("../../../i18n/en.js").Messages>;
639
640
  }, "visible"> & {
@@ -1405,6 +1406,7 @@ export declare class FormState<I extends FormInputs = FormInputs> {
1405
1406
  items?: import("../Select/select.props.js").SelectItems;
1406
1407
  density?: import("../../../types/theme.js").Density;
1407
1408
  separators?: boolean;
1409
+ alignItemWithTrigger?: boolean;
1408
1410
  triggerAttrs?: import("../Select/select.props.js").SelectTriggerAttributes;
1409
1411
  i18n?: Partial<import("../../../i18n/en.js").Messages>;
1410
1412
  }, "visible"> & {
@@ -2210,6 +2212,7 @@ export declare class FormState<I extends FormInputs = FormInputs> {
2210
2212
  items?: import("../Select/select.props.js").SelectItems;
2211
2213
  density?: import("../../../types/theme.js").Density;
2212
2214
  separators?: boolean;
2215
+ alignItemWithTrigger?: boolean;
2213
2216
  triggerAttrs?: import("../Select/select.props.js").SelectTriggerAttributes;
2214
2217
  i18n?: Partial<import("../../../i18n/en.js").Messages>;
2215
2218
  }, "visible"> & {
@@ -2981,6 +2984,7 @@ export declare class FormState<I extends FormInputs = FormInputs> {
2981
2984
  items?: import("../Select/select.props.js").SelectItems;
2982
2985
  density?: import("../../../types/theme.js").Density;
2983
2986
  separators?: boolean;
2987
+ alignItemWithTrigger?: boolean;
2984
2988
  triggerAttrs?: import("../Select/select.props.js").SelectTriggerAttributes;
2985
2989
  i18n?: Partial<import("../../../i18n/en.js").Messages>;
2986
2990
  }, "visible"> & {
@@ -3349,6 +3349,7 @@ export declare function prepareInputProps(input: FormFieldInput, size: Sizes, la
3349
3349
  focused?: boolean | undefined;
3350
3350
  i18n?: Partial<import("../../../i18n/en.js").Messages> | undefined;
3351
3351
  separators?: boolean | undefined;
3352
+ alignItemWithTrigger?: boolean | undefined;
3352
3353
  triggerAttrs?: import("../Select/select.props.js").SelectTriggerAttributes | undefined;
3353
3354
  display?: string;
3354
3355
  } | {
@@ -8972,6 +8973,7 @@ export declare function prepareInputProps(input: FormFieldInput, size: Sizes, la
8972
8973
  focused?: boolean | undefined;
8973
8974
  i18n?: Partial<import("../../../i18n/en.js").Messages> | undefined;
8974
8975
  separators?: boolean | undefined;
8976
+ alignItemWithTrigger?: boolean | undefined;
8975
8977
  triggerAttrs?: import("../Select/select.props.js").SelectTriggerAttributes | undefined;
8976
8978
  display?: string;
8977
8979
  } | {
@@ -614,6 +614,7 @@ export declare class MultiStepFormState<I extends MultiStepFormItems = FormStep[
614
614
  items?: import("../Select/select.props.js").SelectItems;
615
615
  density?: import("../../../types/theme.js").Density;
616
616
  separators?: boolean;
617
+ alignItemWithTrigger?: boolean;
617
618
  triggerAttrs?: import("../Select/select.props.js").SelectTriggerAttributes;
618
619
  i18n?: Partial<import("../../../i18n/en.js").Messages>;
619
620
  }, "visible"> & {
@@ -1385,6 +1386,7 @@ export declare class MultiStepFormState<I extends MultiStepFormItems = FormStep[
1385
1386
  items?: import("../Select/select.props.js").SelectItems;
1386
1387
  density?: import("../../../types/theme.js").Density;
1387
1388
  separators?: boolean;
1389
+ alignItemWithTrigger?: boolean;
1388
1390
  triggerAttrs?: import("../Select/select.props.js").SelectTriggerAttributes;
1389
1391
  i18n?: Partial<import("../../../i18n/en.js").Messages>;
1390
1392
  }, "visible"> & {
@@ -2190,6 +2192,7 @@ export declare class MultiStepFormState<I extends MultiStepFormItems = FormStep[
2190
2192
  items?: import("../Select/select.props.js").SelectItems;
2191
2193
  density?: import("../../../types/theme.js").Density;
2192
2194
  separators?: boolean;
2195
+ alignItemWithTrigger?: boolean;
2193
2196
  triggerAttrs?: import("../Select/select.props.js").SelectTriggerAttributes;
2194
2197
  i18n?: Partial<import("../../../i18n/en.js").Messages>;
2195
2198
  }, "visible"> & {
@@ -2961,6 +2964,7 @@ export declare class MultiStepFormState<I extends MultiStepFormItems = FormStep[
2961
2964
  items?: import("../Select/select.props.js").SelectItems;
2962
2965
  density?: import("../../../types/theme.js").Density;
2963
2966
  separators?: boolean;
2967
+ alignItemWithTrigger?: boolean;
2964
2968
  triggerAttrs?: import("../Select/select.props.js").SelectTriggerAttributes;
2965
2969
  i18n?: Partial<import("../../../i18n/en.js").Messages>;
2966
2970
  }, "visible"> & {
@@ -30,6 +30,7 @@
30
30
  visible,
31
31
  items,
32
32
  separators = true,
33
+ alignItemWithTrigger = true,
33
34
  triggerAttrs,
34
35
  label,
35
36
  ...rest
@@ -37,6 +38,9 @@
37
38
  if (value === undefined) value = untrack(() => defaultValue);
38
39
 
39
40
  const id = $props.id();
41
+ let valueEl = $state<HTMLElement | null>(null);
42
+ let listEl = $state<HTMLDivElement | null>(null);
43
+ let viewportEl = $state<HTMLDivElement | null>(null);
40
44
  const t = $derived(useI18n(i18n));
41
45
 
42
46
  const field = createFieldState({
@@ -105,6 +109,18 @@
105
109
  },
106
110
  set triggerEl(_) {
107
111
  // field.node is owned by the bind:this below.
112
+ },
113
+ get alignItemWithTrigger() {
114
+ return alignItemWithTrigger;
115
+ },
116
+ get valueEl() {
117
+ return valueEl;
118
+ },
119
+ get listEl() {
120
+ return listEl;
121
+ },
122
+ get viewportEl() {
123
+ return viewportEl;
108
124
  }
109
125
  });
110
126
 
@@ -121,6 +137,7 @@
121
137
  fitTrigger
122
138
  position="bottom"
123
139
  ref={field.node?.parentElement}
140
+ positionPanel={select.positionPanel}
124
141
  size="small"
125
142
  transition={{
126
143
  in: { scale: 1, opacity: 0 },
@@ -143,7 +160,13 @@
143
160
  event.preventDefault();
144
161
  }}
145
162
  >
146
- <ScrollArea scrollOnEdges type="auto" class="flex max-h-[240px] flex-col">
163
+ <ScrollArea
164
+ bind:ref={listEl}
165
+ bind:viewportRef={viewportEl}
166
+ scrollOnEdges
167
+ type="auto"
168
+ class="flex max-h-[240px] flex-col"
169
+ >
147
170
  {#each select.renderGroups as group, groupIndex (groupIndex)}
148
171
  {#if separators && groupIndex > 0}
149
172
  <div role="separator" class={classes.separator({ size })}></div>
@@ -230,7 +253,10 @@
230
253
  triggerAttrs?.onblur?.(event);
231
254
  }}
232
255
  >
233
- <span class={classes.value({ size, placeholder: !select.selectedOption })}>
256
+ <span
257
+ bind:this={valueEl}
258
+ class={classes.value({ size, placeholder: !select.selectedOption })}
259
+ >
234
260
  {select.selectedOption?.label ?? placeholder ?? t.selectOption}
235
261
  </span>
236
262
  {@render caretDownIcon({ class: classes.triggerIcon({ size }) })}