entasis 0.7.1 → 0.8.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 (43) hide show
  1. package/dist/components/FloatingWindow/FloatingWindow.svelte +26 -2
  2. package/dist/components/FloatingWindow/floatingWindow.dock.svelte.js +11 -2
  3. package/dist/components/FloatingWindow/floatingWindow.mcp.d.ts +1 -1
  4. package/dist/components/FloatingWindow/floatingWindow.mcp.js +7 -4
  5. package/dist/components/FloatingWindow/floatingWindow.props.d.ts +6 -0
  6. package/dist/components/FloatingWindow/floatingWindow.state.svelte.d.ts +3 -0
  7. package/dist/components/FloatingWindow/floatingWindow.state.svelte.js +18 -4
  8. package/dist/components/FloatingWindow/floatingWindow.theme.d.ts +3 -0
  9. package/dist/components/FloatingWindow/floatingWindow.theme.js +20 -7
  10. package/dist/components/Form/File/FileInput.svelte +6 -42
  11. package/dist/components/Sidebar/Sidebar.svelte +52 -9
  12. package/dist/components/Sidebar/Sidebar.svelte.d.ts +1 -1
  13. package/dist/components/Sidebar/SidebarMenuItem.svelte +19 -2
  14. package/dist/components/Sidebar/SidebarMenuItem.svelte.d.ts +4 -0
  15. package/dist/components/Sidebar/SidebarPanel.svelte +199 -88
  16. package/dist/components/Sidebar/SidebarPanel.svelte.d.ts +3 -0
  17. package/dist/components/Sidebar/SidebarViewStage.svelte +103 -0
  18. package/dist/components/Sidebar/SidebarViewStage.svelte.d.ts +18 -0
  19. package/dist/components/Sidebar/index.d.ts +1 -1
  20. package/dist/components/Sidebar/sidebar.mcp.d.ts +1 -1
  21. package/dist/components/Sidebar/sidebar.mcp.js +18 -1
  22. package/dist/components/Sidebar/sidebar.props.d.ts +56 -1
  23. package/dist/components/Sidebar/sidebar.state.svelte.d.ts +4 -0
  24. package/dist/components/Sidebar/sidebar.state.svelte.js +9 -1
  25. package/dist/components/Sidebar/sidebar.theme.d.ts +116 -5
  26. package/dist/components/Sidebar/sidebar.theme.js +42 -0
  27. package/dist/components/Sidebar/sidebar.views.svelte.d.ts +138 -0
  28. package/dist/components/Sidebar/sidebar.views.svelte.js +303 -0
  29. package/dist/components/Theme/theme.floatingWindows.d.ts +6 -1
  30. package/dist/components/Theme/theme.floatingWindows.js +7 -2
  31. package/dist/components/Theme/theme.mcp.d.ts +1 -1
  32. package/dist/components/Theme/theme.mcp.js +1 -0
  33. package/dist/generated/componentContract.d.ts +1 -1
  34. package/dist/generated/componentContract.js +1 -0
  35. package/dist/generated/componentMcpRegistry.d.ts +4 -4
  36. package/dist/tailwind/index.mcp.d.ts +1 -1
  37. package/dist/tailwind/index.mcp.js +4 -0
  38. package/dist/tailwind/scales.js +11 -3
  39. package/dist/tailwind/spacing.js +17 -0
  40. package/dist/utils/cva/merge.d.ts +7 -0
  41. package/dist/utils/cva/merge.js +6 -1
  42. package/dist/utils/pointerDrag.js +7 -1
  43. package/package.json +1 -1
@@ -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)
@@ -18,6 +18,7 @@
18
18
  import { SidebarResizeState } from './sidebar.resize.svelte.js';
19
19
  import { SidebarStateController } from './sidebar.state.svelte.js';
20
20
  import { useSidebarTheme } from './sidebar.theme.js';
21
+ import { SidebarViewsState } from './sidebar.views.svelte.js';
21
22
  import { useDefaultColor } from '../Theme/theme.state.svelte.js';
22
23
  let {
23
24
  id: customId,
@@ -47,6 +48,10 @@
47
48
  expandOnHover = false,
48
49
  activityBar,
49
50
  items,
51
+ views,
52
+ view = $bindable(),
53
+ defaultView,
54
+ onViewChange,
50
55
  class: className,
51
56
  collapseIcon = 'chevron',
52
57
  tooltips = 'auto',
@@ -111,19 +116,26 @@
111
116
  const navLabel = $derived(`${t.sidebar} ${t.navigation}`);
112
117
  const classes = $derived(useSidebarTheme(theme));
113
118
  const resolvedColor = $derived(useDefaultColor());
119
+ // Every view counts, not just the one on screen: icon collapse must not come and go as the
120
+ // view changes.
114
121
  const canCollapseToIcon = $derived.by(() => {
115
- if (content) return false;
122
+ const viewList = Object.values(views ?? {});
123
+ if (viewList.length ? viewList.some((entry) => entry.content) : content) return false;
116
124
 
117
- const menuEntries = [
118
- ...(headerMenu ?? []),
119
- ...(items?.flatMap((group) => group.items ?? []) ?? []),
120
- ...(footerMenu ?? [])
121
- ];
125
+ const sources = [{ headerMenu, items, footerMenu, headerButton, footerButton }, ...viewList];
126
+ const menuEntries = sources.flatMap((source) => [
127
+ ...(source.headerMenu ?? []),
128
+ ...(source.items?.flatMap((group) => group.items ?? []) ?? []),
129
+ ...(source.footerMenu ?? [])
130
+ ]);
122
131
 
123
132
  return (
124
133
  menuEntries.every(hasIcon) &&
125
- hasCollapsibleMedia(headerButton) &&
126
- hasCollapsibleMedia(footerButton)
134
+ sources.every(
135
+ (source) =>
136
+ hasCollapsibleMedia(source.headerButton ?? undefined) &&
137
+ hasCollapsibleMedia(source.footerButton ?? undefined)
138
+ )
127
139
  );
128
140
  });
129
141
  const resolvedCollapsible = $derived(
@@ -150,6 +162,32 @@
150
162
  return onDisplayStateChange;
151
163
  }
152
164
  });
165
+ const panelViews = new SidebarViewsState({
166
+ get views() {
167
+ return views;
168
+ },
169
+ get view() {
170
+ return view;
171
+ },
172
+ get defaultView() {
173
+ return defaultView;
174
+ },
175
+ get onViewChange() {
176
+ return onViewChange;
177
+ },
178
+ get isMobile(): boolean {
179
+ return controller.isMobile;
180
+ },
181
+ get side() {
182
+ return side;
183
+ },
184
+ get root() {
185
+ return { headerButton, search, headerMenu, header, footerButton, footerMenu, footer };
186
+ },
187
+ setViewProp: (next) => {
188
+ view = next;
189
+ }
190
+ });
153
191
  const controller = new SidebarStateController({
154
192
  get mode() {
155
193
  return mode;
@@ -169,7 +207,11 @@
169
207
  get peeking() {
170
208
  return hoverExpanded;
171
209
  },
172
- setDisplayState: displayStateBridge.setDisplayState
210
+ get view(): string | undefined {
211
+ return panelViews.current;
212
+ },
213
+ setDisplayState: displayStateBridge.setDisplayState,
214
+ setView: panelViews.setView
173
215
  });
174
216
  const resize = new SidebarResizeState({
175
217
  get width() {
@@ -251,6 +293,7 @@
251
293
  {activeVariant}
252
294
  {density}
253
295
  label={navLabel}
296
+ views={panelViews.enabled ? panelViews : undefined}
254
297
  {theme}
255
298
  />
256
299
  {/snippet}
@@ -1,4 +1,4 @@
1
1
  import type { SidebarProps } from './sidebar.props.js';
2
- declare const Sidebar: import("svelte").Component<SidebarProps, {}, "width" | "ref" | "open" | "displayState">;
2
+ declare const Sidebar: import("svelte").Component<SidebarProps, {}, "view" | "width" | "ref" | "open" | "displayState">;
3
3
  type Sidebar = ReturnType<typeof Sidebar>;
4
4
  export default Sidebar;
@@ -30,6 +30,8 @@
30
30
  size,
31
31
  activeVariant,
32
32
  density,
33
+ back = false,
34
+ ariaLabel,
33
35
  theme
34
36
  }: {
35
37
  item: SidebarMenuEntry;
@@ -39,6 +41,10 @@
39
41
  size: SidebarSize;
40
42
  activeVariant: SidebarActiveVariant;
41
43
  density: SidebarDensity;
44
+ /** The row a nested view opens on, back to its parent: a leading back arrow, no chevron. */
45
+ back?: boolean;
46
+ /** Accessible name when the visible label alone does not say what the row does. */
47
+ ariaLabel?: string;
42
48
  theme?: SidebarThemeProps;
43
49
  } = $props();
44
50
 
@@ -62,7 +68,9 @@
62
68
  api.displayState === 'collapsed' && !api.isMobile && !api.isPeeking
63
69
  );
64
70
  const showTooltip = $derived((tooltips === 'always' || isIconCollapsed) && !api.isMobile);
65
- const tooltipContent = $derived(showTooltip ? (item.tooltip ?? item.label) : undefined);
71
+ const tooltipContent = $derived(
72
+ showTooltip ? (item.tooltip ?? ariaLabel ?? item.label) : undefined
73
+ );
66
74
  const hasSubmenu = $derived(!!item.items?.length);
67
75
  // A bare glyph stays bare: the wrapper only appears when the entry asks for a role tint or a
68
76
  // tile, so an untinted row never inherits the ambient `data-color`.
@@ -97,6 +105,7 @@
97
105
  return;
98
106
  }
99
107
  item.onclick?.(event);
108
+ if (item.view !== undefined && !event.defaultPrevented) api.setView(item.view);
100
109
  }
101
110
  </script>
102
111
 
@@ -110,7 +119,7 @@
110
119
  <SidebarIcon icon={item.icon} />
111
120
  </span>
112
121
  {:else}
113
- <SidebarIcon icon={item.icon} />
122
+ <SidebarIcon icon={item.icon} class={back ? 'rtl:-scale-x-100' : undefined} />
114
123
  {/if}
115
124
  <span class={classes.menuLabel()}>{item.label}</span>
116
125
  {/snippet}
@@ -170,6 +179,8 @@
170
179
  data-active={item.isActive ? 'true' : undefined}
171
180
  data-active-variant={activeVariant}
172
181
  aria-current={item.isActive ? 'page' : undefined}
182
+ aria-label={ariaLabel}
183
+ data-sidebar-view-target={item.view}
173
184
  disabled={item.disabled || undefined}
174
185
  class={classes.menuButton({
175
186
  variant: item.variant,
@@ -183,6 +194,12 @@
183
194
  onclick={handleClick}
184
195
  >
185
196
  {@render entryContent()}
197
+ {#if item.view !== undefined && !back}
198
+ <SidebarIcon
199
+ icon={caretRightIcon}
200
+ class={classes.menuTrailing({ size, className: 'rtl:-scale-x-100' })}
201
+ />
202
+ {/if}
186
203
  </button>
187
204
  {/if}
188
205
  {/snippet}
@@ -9,6 +9,10 @@ type $$ComponentProps = {
9
9
  size: SidebarSize;
10
10
  activeVariant: SidebarActiveVariant;
11
11
  density: SidebarDensity;
12
+ /** The row a nested view opens on, back to its parent: a leading back arrow, no chevron. */
13
+ back?: boolean;
14
+ /** Accessible name when the visible label alone does not say what the row does. */
15
+ ariaLabel?: string;
12
16
  theme?: SidebarThemeProps;
13
17
  };
14
18
  declare const SidebarMenuItem: import("svelte").Component<$$ComponentProps, {}, "">;