entasis 0.9.2 → 0.10.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 (85) hide show
  1. package/dist/components/AppShell/appShell.theme.js +3 -3
  2. package/dist/components/ButtonGroup/ButtonGroup.svelte +10 -4
  3. package/dist/components/ButtonGroup/buttonGroup.mcp.d.ts +1 -1
  4. package/dist/components/ButtonGroup/buttonGroup.mcp.js +23 -3
  5. package/dist/components/ButtonGroup/buttonGroup.props.d.ts +19 -5
  6. package/dist/components/Chart/Chart.svelte +1 -1
  7. package/dist/components/Chart/chart.cartesian.js +3 -2
  8. package/dist/components/Chart/chart.fit.d.ts +53 -0
  9. package/dist/components/Chart/chart.fit.js +81 -0
  10. package/dist/components/Chart/chart.mcp.d.ts +1 -1
  11. package/dist/components/Chart/chart.mcp.js +2 -1
  12. package/dist/components/Chart/chart.polar.js +137 -18
  13. package/dist/components/Chart/chart.polar.props.d.ts +5 -0
  14. package/dist/components/Chart/chart.proportion.js +12 -6
  15. package/dist/components/Chart/chart.relation.network.js +32 -4
  16. package/dist/components/Chart/chart.relation.sankey.js +53 -6
  17. package/dist/components/Chart/chart.relation.tree.js +84 -24
  18. package/dist/components/Chart/chart.series.props.d.ts +6 -0
  19. package/dist/components/Chart/chart.state.svelte.d.ts +1 -0
  20. package/dist/components/Chart/chart.state.svelte.js +9 -6
  21. package/dist/components/Chart/chart.viewport.svelte.d.ts +1 -0
  22. package/dist/components/Chart/chart.viewport.svelte.js +25 -7
  23. package/dist/components/DataTable/DataTable.svelte +25 -8
  24. package/dist/components/DataTable/DataTableRow.svelte +33 -10
  25. package/dist/components/DataTable/dataTable.mcp.d.ts +1 -1
  26. package/dist/components/DataTable/dataTable.mcp.js +12 -1
  27. package/dist/components/DataTable/dataTable.model.svelte.js +4 -3
  28. package/dist/components/DataTable/dataTable.props.d.ts +17 -0
  29. package/dist/components/DataTable/dataTable.theme.d.ts +12 -0
  30. package/dist/components/DataTable/dataTable.theme.js +3 -2
  31. package/dist/components/DataTable/index.d.ts +1 -1
  32. package/dist/components/Dialog/Dialog.svelte +12 -7
  33. package/dist/components/Dialog/dialog.state.svelte.d.ts +2 -1
  34. package/dist/components/Dialog/dialog.state.svelte.js +7 -7
  35. package/dist/components/Dialog/dialog.theme.js +1 -1
  36. package/dist/components/FloatingWindow/floatingWindow.theme.js +1 -1
  37. package/dist/components/Form/Select/Select.svelte +3 -1
  38. package/dist/components/PageShell/pageShell.state.svelte.d.ts +1 -1
  39. package/dist/components/PageShell/pageShell.state.svelte.js +6 -13
  40. package/dist/components/Popover/Popover.svelte +5 -5
  41. package/dist/components/Popover/index.d.ts +1 -1
  42. package/dist/components/Popover/popover.mcp.d.ts +1 -1
  43. package/dist/components/Popover/popover.mcp.js +33 -6
  44. package/dist/components/Popover/popover.state.svelte.d.ts +9 -1
  45. package/dist/components/Popover/popover.state.svelte.js +61 -8
  46. package/dist/components/Popover/popover.theme.js +1 -1
  47. package/dist/components/Sidebar/Sidebar.svelte +1 -1
  48. package/dist/components/Sidebar/SidebarDesktopShell.svelte +26 -6
  49. package/dist/components/Sidebar/sidebar-layout.js +7 -6
  50. package/dist/components/Sidebar/sidebar.mcp.d.ts +1 -1
  51. package/dist/components/Sidebar/sidebar.mcp.js +2 -2
  52. package/dist/components/Sidebar/sidebar.theme.js +36 -20
  53. package/dist/components/Theme/index.d.ts +1 -1
  54. package/dist/components/Theme/index.js +1 -1
  55. package/dist/components/Theme/theme.mcp.d.ts +1 -1
  56. package/dist/components/Theme/theme.mcp.js +3 -0
  57. package/dist/components/Theme/theme.state.svelte.d.ts +4 -0
  58. package/dist/components/Theme/theme.state.svelte.js +4 -0
  59. package/dist/components/Tooltip/Tooltip.svelte +2 -4
  60. package/dist/components/Tooltip/tooltip.attachment.svelte.js +5 -0
  61. package/dist/components/Tooltip/tooltip.mcp.d.ts +1 -1
  62. package/dist/components/Tooltip/tooltip.mcp.js +4 -2
  63. package/dist/generated/componentAliases.d.ts +1 -0
  64. package/dist/generated/componentAliases.js +1 -0
  65. package/dist/generated/componentContract.d.ts +13 -3
  66. package/dist/generated/componentContract.js +15 -1
  67. package/dist/generated/componentMcpRegistry.d.ts +8 -7
  68. package/dist/generated/componentMcpRegistry.js +2 -0
  69. package/dist/i18n/ar.js +1 -1
  70. package/dist/i18n/de.js +1 -1
  71. package/dist/i18n/en.js +1 -1
  72. package/dist/i18n/es.js +1 -1
  73. package/dist/i18n/fr.js +1 -1
  74. package/dist/i18n/pt.js +1 -1
  75. package/dist/i18n/zh.js +1 -1
  76. package/dist/tailwind/colors.d.ts +10 -3
  77. package/dist/tailwind/colors.js +6 -0
  78. package/dist/tailwind/palette.d.ts +5 -0
  79. package/dist/tailwind/palette.js +4 -0
  80. package/dist/tailwind/palette.mcp.d.ts +1 -0
  81. package/dist/tailwind/palette.mcp.js +34 -0
  82. package/dist/utils/layers.svelte.js +9 -8
  83. package/dist/utils/registry.svelte.d.ts +21 -0
  84. package/dist/utils/registry.svelte.js +49 -0
  85. package/package.json +8 -1
@@ -422,11 +422,12 @@ export class DataTableModel {
422
422
  });
423
423
  }
424
424
  if (this.props.rowActions) {
425
+ const width = this.props.rowActionsWidth ?? 36;
425
426
  definitions.push({
426
427
  id: DATA_TABLE_ACTIONS_COLUMN,
427
- size: 36,
428
- minSize: 36,
429
- maxSize: 36,
428
+ size: width,
429
+ minSize: width,
430
+ maxSize: width,
430
431
  enableHiding: false,
431
432
  enablePinning: false,
432
433
  enableResizing: false,
@@ -131,6 +131,10 @@ export type DataTableRowPayload<TData> = {
131
131
  toggleSelected: () => void;
132
132
  toggleExpanded: () => void;
133
133
  };
134
+ export type DataTableRowActivation<TData> = DataTableRowPayload<TData> & {
135
+ /** The click or Enter key press that activated the row, for modifier keys. */
136
+ event: MouseEvent | KeyboardEvent;
137
+ };
134
138
  export type DataTableCellPayload<TData, TValue = unknown> = DataTableRowPayload<TData> & {
135
139
  columnId: string;
136
140
  value: TValue;
@@ -275,6 +279,19 @@ type DataTableBaseProps<TData> = {
275
279
  bulkActions?: Slot<DataTableToolbarPayload<TData>>;
276
280
  /** Actions rendered for an individual row. */
277
281
  rowActions?: Slot<DataTableRowPayload<TData>>;
282
+ /**
283
+ * Width of the row actions column, in pixels. The default fits one icon button; widen it for
284
+ * text buttons or several actions.
285
+ * @default 36
286
+ */
287
+ rowActionsWidth?: number;
288
+ /**
289
+ * Called when a row is activated: clicked anywhere that is not a control inside it, or, in
290
+ * `grid` mode, given Enter on a cell with no editor or control. Rows show a pointer while
291
+ * it is set. In `table` mode rows are not focusable, so keep a link or row action for
292
+ * keyboard users.
293
+ */
294
+ onRowActivate?: (payload: DataTableRowActivation<TData>) => void;
278
295
  /** Additional content displayed below an expanded row. */
279
296
  expandedContent?: Slot<DataTableRowPayload<TData>>;
280
297
  /** Display the loading state while rows are being fetched. */
@@ -73,6 +73,10 @@ export declare const dataTableTheme: {
73
73
  true: string;
74
74
  false: string;
75
75
  };
76
+ activatable: {
77
+ true: string;
78
+ false: string;
79
+ };
76
80
  }> & import("../../utils/cva/types.js").CVAClassProp) | undefined) => string;
77
81
  cell: (props?: (import("../../utils/cva/types.js").CVAVariantSchema<{
78
82
  density: {
@@ -234,6 +238,10 @@ export declare const setDataTableTheme: (theme: InferComponentTheme<{
234
238
  true: string;
235
239
  false: string;
236
240
  };
241
+ activatable: {
242
+ true: string;
243
+ false: string;
244
+ };
237
245
  }> & import("../../utils/cva/types.js").CVAClassProp) | undefined) => string;
238
246
  cell: (props?: (import("../../utils/cva/types.js").CVAVariantSchema<{
239
247
  density: {
@@ -393,6 +401,10 @@ export declare const useDataTableTheme: import("../../utils/cva/theme.js").UseCo
393
401
  true: string;
394
402
  false: string;
395
403
  };
404
+ activatable: {
405
+ true: string;
406
+ false: string;
407
+ };
396
408
  }> & import("../../utils/cva/types.js").CVAClassProp) | undefined) => string;
397
409
  cell: (props?: (import("../../utils/cva/types.js").CVAVariantSchema<{
398
410
  density: {
@@ -113,9 +113,10 @@ const row = cva({
113
113
  base: 'state-layer grid min-w-full border-b border-neutral-muted transition-colors last:border-b-0 data-[selected=true]:bg-selected-muted data-[selected=true]:text-selected-muted-readable',
114
114
  variants: {
115
115
  density: { compact: 'min-h-row-sm', normal: 'min-h-row-md', comfortable: 'min-h-row-lg' },
116
- grouped: { true: 'bg-surface-raised font-medium', false: '' }
116
+ grouped: { true: 'bg-surface-raised font-medium', false: '' },
117
+ activatable: { true: 'cursor-pointer', false: '' }
117
118
  },
118
- defaultVariants: { density: 'normal', grouped: false }
119
+ defaultVariants: { density: 'normal', grouped: false, activatable: false }
119
120
  });
120
121
  const cell = cva({
121
122
  base: 'relative flex min-w-0 items-center overflow-hidden border-neutral-muted outline-none',
@@ -1,6 +1,6 @@
1
1
  export { default as DataTable } from './DataTable.svelte';
2
2
  export { createDataTableColumnHelper } from './dataTable.column-helper.js';
3
3
  export { createDataTableState } from './dataTable.model.svelte.js';
4
- export type { DataTableAggregation, DataTableAlignment, DataTableApi, DataTableBooleanFilter, DataTableBuiltInEditor, DataTableCellCommit, DataTableCellPayload, DataTableCellRenderPayload, DataTableColumn, DataTableColumnFilter, DataTableCustomEditor, DataTableCustomFilter, DataTableDateEditor, DataTableDateFilter, DataTableEditor, DataTableEditorPayload, DataTableFilter, DataTableFilterPayload, DataTableHeaderPayload, DataTableHeaderRenderPayload, DataTableInteractionMode, DataTableNumberEditor, DataTableNumberFilter, DataTableOption, DataTablePaginationConfig, DataTablePaginationState, DataTablePinning, DataTableProcessingMode, DataTableProps, DataTableRowPayload, DataTableSearchConfig, DataTableSelectionMode, DataTableSelectEditor, DataTableSelectFilter, DataTableSorting, DataTableState, DataTableSwitchEditor, DataTableTextEditor, DataTableTextFilter, DataTableToolbarPayload } from './dataTable.props.js';
4
+ export type { DataTableAggregation, DataTableAlignment, DataTableApi, DataTableBooleanFilter, DataTableBuiltInEditor, DataTableCellCommit, DataTableCellPayload, DataTableCellRenderPayload, DataTableColumn, DataTableColumnFilter, DataTableCustomEditor, DataTableCustomFilter, DataTableDateEditor, DataTableDateFilter, DataTableEditor, DataTableEditorPayload, DataTableFilter, DataTableFilterPayload, DataTableHeaderPayload, DataTableHeaderRenderPayload, DataTableInteractionMode, DataTableNumberEditor, DataTableNumberFilter, DataTableOption, DataTablePaginationConfig, DataTablePaginationState, DataTablePinning, DataTableProcessingMode, DataTableProps, DataTableRowActivation, DataTableRowPayload, DataTableSearchConfig, DataTableSelectionMode, DataTableSelectEditor, DataTableSelectFilter, DataTableSorting, DataTableState, DataTableSwitchEditor, DataTableTextEditor, DataTableTextFilter, DataTableToolbarPayload } from './dataTable.props.js';
5
5
  export { dataTableTheme, setDataTableTheme, useDataTableTheme, type DataTableClasses, type DataTableTheme, type DataTableThemeProps } from './dataTable.theme.js';
6
6
  export { dataTableDescription } from './dataTable.mcp.js';
@@ -104,7 +104,12 @@
104
104
  </script>
105
105
 
106
106
  {#snippet CLOSE_BUTTON()}
107
- <Slot class={classes.closeButton({ size: dialog.computedSize })} render={closeButton}>
107
+ {#if closeButton}
108
+ <!-- The part positions a custom close button through this wrapper. -->
109
+ <Slot class={classes.closeButton({ size: dialog.computedSize })} render={closeButton} />
110
+ {:else}
111
+ <!-- The default button carries the part itself. Wrapped in the same classes, the empty
112
+ wrapper drew a dot at the corner when hovered (padding, rounded, state layer). -->
108
113
  <Button
109
114
  squared
110
115
  class={classes.closeButton({ size: dialog.computedSize })}
@@ -115,7 +120,7 @@
115
120
  >
116
121
  {@render xIcon({ size: 20 })}
117
122
  </Button>
118
- </Slot>
123
+ {/if}
119
124
  {/snippet}
120
125
 
121
126
  {#if dialog.isOpen}
@@ -212,15 +217,15 @@
212
217
  {#if typeof trigger === 'function'}
213
218
  {@render trigger?.(dialog)}
214
219
  {:else}
220
+ {@const { content, children, onclick, ...buttonProps } = trigger}
215
221
  <Button
216
- {...trigger}
222
+ {...buttonProps}
217
223
  onclick={(event) => {
218
- trigger.onclick?.(event);
224
+ onclick?.(event);
219
225
  dialog.open();
220
226
  }}
221
- >
222
- {trigger.content}
223
- </Button>
227
+ children={children ?? content}
228
+ />
224
229
  {/if}
225
230
  {/if}
226
231
 
@@ -10,8 +10,9 @@ interface DialogOptions extends MakeRequired<Pick<DialogProps, 'id' | 'type' | '
10
10
  }
11
11
  declare const DialogState_base: new (props: DialogOptions) => {} & DialogOptions;
12
12
  export declare class DialogState extends DialogState_base {
13
+ #private;
13
14
  parent: DialogState | null;
14
- children: DialogState[];
15
+ get children(): readonly DialogState[];
15
16
  hasTransitioned: boolean;
16
17
  theme: import("../Theme/theme.state.svelte.js").ThemeState;
17
18
  layer: import("../../utils/layers.svelte.js").LayerHandle;
@@ -1,4 +1,5 @@
1
1
  import { createBindableStateClass } from '../../utils/state.svelte.js';
2
+ import { Registry } from '../../utils/registry.svelte.js';
2
3
  import { createPointerDrag } from '../../utils/pointerDrag.js';
3
4
  // import { useTheme } from '../../utils/theme.svelte.js';
4
5
  import { getContext, onMount, setContext, untrack } from 'svelte';
@@ -8,7 +9,11 @@ import { useDialogMotion } from './dialog.theme.js';
8
9
  export { DIALOG_Z_BASE, DIALOG_Z_STEP } from '../Theme/theme.layers.js';
9
10
  export class DialogState extends createBindableStateClass() {
10
11
  parent = getContext('dialog');
11
- children = $state([]);
12
+ // Nested dialogs join while they mount and leave when torn down; see `Registry`.
13
+ #children = new Registry();
14
+ get children() {
15
+ return this.#children.items;
16
+ }
12
17
  hasTransitioned = $state(false);
13
18
  theme = useTheme();
14
19
  // Registered in the shared layer stack, which owns Escape / outside-press dismissal,
@@ -68,12 +73,7 @@ export class DialogState extends createBindableStateClass() {
68
73
  isActive: () => this.isTop,
69
74
  inertSiblings: () => true
70
75
  });
71
- addChild = (child) => () => {
72
- this.children.push(child);
73
- return () => {
74
- this.children = this.children.filter((d) => d.id !== child.id);
75
- };
76
- };
76
+ addChild = (child) => () => this.#children.add(child);
77
77
  constructor(options) {
78
78
  super(options);
79
79
  setContext('dialog', this);
@@ -39,7 +39,7 @@ export const defaultDialogAlign = cva({
39
39
  }
40
40
  });
41
41
  export const defaultDialogBackdrop = cva({
42
- base: 'fixed inset-0 bg-neutral/40 backdrop-blur-xs'
42
+ base: 'fixed inset-0 bg-black/40 backdrop-blur-xs dark:bg-black/60'
43
43
  });
44
44
  export const defaultDialogContent = cva({
45
45
  base: 'z-10 relative px-xl py-md raised-xl h-fit bg-surface-floating text-neutral rounded-xl flex flex-col z-50 will-change-transform transition-transform duration-normal ease-standard',
@@ -145,7 +145,7 @@ const defaultFloatingWindowDockActions = cva({
145
145
  // The page dim behind a window opened with `backdrop`: the same wash as Dialog's backdrop, so a
146
146
  // modal window and a modal dialog dim the page alike.
147
147
  const defaultFloatingWindowBackdrop = cva({
148
- base: 'fixed inset-0 bg-neutral/40 backdrop-blur-xs'
148
+ base: 'fixed inset-0 bg-black/40 backdrop-blur-xs dark:bg-black/60'
149
149
  });
150
150
  export const defaultFloatingWindowMotion = motion({
151
151
  base: {
@@ -231,7 +231,9 @@
231
231
  aria-activedescendant={select.isOpen ? select.nav.activeDescendant : undefined}
232
232
  aria-required={required || undefined}
233
233
  aria-label={triggerAttrs?.['aria-label'] ??
234
- (label ? undefined : (placeholder ?? t.selectOption))}
234
+ (label || triggerAttrs?.['aria-labelledby']
235
+ ? undefined
236
+ : (placeholder ?? t.selectOption))}
235
237
  data-placeholder={select.selectedOption ? undefined : ''}
236
238
  disabled={field.disabled}
237
239
  class={classes.input({ size, disabled: field.disabled })}
@@ -3,8 +3,8 @@ type PageShellStateOptions = Readonly<PageShellConfig> & {
3
3
  readonly isContentScrolled?: boolean;
4
4
  };
5
5
  export declare class PageShellState {
6
+ #private;
6
7
  private options;
7
- private overrides;
8
8
  readonly api: PageShellApi;
9
9
  constructor(options: PageShellStateOptions);
10
10
  get current(): PageShellConfig;
@@ -1,8 +1,10 @@
1
1
  import { getContext, onDestroy, setContext } from 'svelte';
2
+ import { Registry } from '../../utils/registry.svelte.js';
2
3
  const PAGE_SHELL_CONTEXT = Symbol('page-shell');
3
4
  export class PageShellState {
4
5
  options;
5
- overrides = $state([]);
6
+ // Pages join on mount and leave on navigation; see `Registry`.
7
+ #overrides = new Registry();
6
8
  api;
7
9
  constructor(options) {
8
10
  this.options = options;
@@ -94,7 +96,7 @@ export class PageShellState {
94
96
  contentWidth: this.options.contentWidth,
95
97
  actionOverflow: this.options.actionOverflow,
96
98
  mobileActionCount: this.options.mobileActionCount
97
- }, ...this.overrides);
99
+ }, ...this.#overrides.items);
98
100
  }
99
101
  get hasHeader() {
100
102
  const current = this.current;
@@ -110,16 +112,7 @@ export class PageShellState {
110
112
  const current = this.current;
111
113
  return Boolean(current.footer || current.footerActions);
112
114
  }
113
- set = (config) => {
114
- this.overrides = [...this.overrides, config];
115
- let isActive = true;
116
- return () => {
117
- if (!isActive)
118
- return;
119
- isActive = false;
120
- this.overrides = this.overrides.filter((override) => override !== config);
121
- };
122
- };
115
+ set = (config) => this.#overrides.add(config);
123
116
  setEyebrow = (eyebrow) => this.set({ eyebrow });
124
117
  setBreadcrumbs = (breadcrumbs) => this.set({ breadcrumbs });
125
118
  setBack = (back) => this.set({ back });
@@ -130,7 +123,7 @@ export class PageShellState {
130
123
  setFooter = (footer) => this.set({ footer });
131
124
  setFooterActions = (footerActions) => this.set({ footerActions });
132
125
  reset = () => {
133
- this.overrides = [];
126
+ this.#overrides.clear();
134
127
  };
135
128
  }
136
129
  export function usePageShell() {
@@ -168,17 +168,17 @@
168
168
  {#if typeof trigger === 'function'}
169
169
  {@render trigger?.(popover)}
170
170
  {:else if typeof trigger !== 'boolean'}
171
+ {@const { content, children, onclick, ...buttonProps } = trigger}
171
172
  <Button
172
- {...trigger}
173
+ {...buttonProps}
173
174
  {...popover.triggerProps}
174
175
  onclick={(event) => {
175
- trigger.onclick?.(event);
176
+ onclick?.(event);
176
177
  if (openOnClick) popover.toggle();
177
178
  }}
179
+ children={children ?? content}
178
180
  {@attach popover.reference}
179
- >
180
- {trigger.content}
181
- </Button>
181
+ />
182
182
  {/if}
183
183
  {/if}
184
184
 
@@ -1,4 +1,4 @@
1
1
  export { default as Popover } from './Popover.svelte';
2
2
  export type { PopoverProps } from './popover.props.js';
3
3
  export { popoverTheme, setPopoverTheme, usePopoverTheme, type PopoverTheme, type PopoverThemeProps } from './popover.theme.js';
4
- export { usePopoverContext } from './popover.state.svelte.js';
4
+ export { usePopoverContext, type PopoverState } from './popover.state.svelte.js';
@@ -1 +1 @@
1
- export declare const popoverDescription = "\n# Popover Component\n\nThe Popover component displays floating content positioned relative to a trigger element. It's ideal for tooltips, dropdown menus, and contextual information.\n\n## Basic Usage\n\n```svelte\n<script>\n\timport { Popover } from 'entasis/popover';\n\t\t\n</script>\n// By default Popover comes with a button that triggers them, no need to define a callback and a $state\n<Popover trigger={{ content: \"Toggle Popover\" }}>\n\tPopover content here\n</Popover>\n```\n\n## Props\n\n### Core Props\n- **open**: boolean (bindable) - Controls popover visibility (optional when using trigger prop)\n- **defaultOpen**: boolean (default: false) - Initial state when open is not provided\n- **ref**: HTMLElement | null - Reference element to position popover against (optional when using trigger prop)\n- **id**: string - Unique identifier\n\n### Layout Props\n- **position**: 'top' | 'bottom' | 'left' | 'right' | 'top-start' | 'top-end' | 'bottom-start' | 'bottom-end' | 'left-start' | 'left-end' | 'right-start' | 'right-end' (default: 'bottom')\n - Determines where popover appears relative to trigger\n- **offset**: number - Distance in pixels from the reference element\n- **fitTrigger**: boolean (default: false) - Whether popover should match the width of the trigger element\n- **positionPanel**: ({ panel, reference }) => { x: number; y: number; minWidth?: number } | null - Place the panel yourself instead of floating-ui: return its viewport coordinates (the panel is `position: fixed`) and optionally a `minWidth` in px that replaces `fitTrigger`'s, or `null` to fall back to `position`. Called when the panel mounts and whenever floating-ui would reposition it. Select uses it to open over its trigger\n- **mobileSheet**: boolean (default: false) - On mobile viewports (<768px), render as a bottom sheet instead of an anchored floating panel\n- **inline**: boolean (default: false) - Render the panel in normal document flow where the component sits instead of portaling to the viewport-fixed layer: no floating-ui positioning, no scroll lock, no outside-press dismissal (Escape still closes it). Same panel classes and motion, and the trigger still toggles it. Wins over `mobileSheet`\n- **mobileSheetSizeTransition**: boolean (default: true) - Whether mobile-sheet panels animate intrinsic size changes\n\n### Event Props\n- **onOpenChange**: (open: boolean) => void - Called once for each library-requested state change\n- **onAfterOpen**: (payload: PopoverState) => void - Called after the open transition finishes\n- **onAfterClose**: (payload: PopoverState) => void - Called after the close transition finishes\n\n### Slot Props\n- **children**: Snippet<[PopoverState]> - Popover content\n- **trigger**: Snippet<[PopoverState]> | (ButtonProps & { content?: string }) | false - Trigger element\n - Pass a snippet function for custom trigger: `{#snippet trigger(popover)}...</snippet>`\n - Pass button props object for default button: `trigger={{ content: \"Click Me\", color: \"primary\" }}`\n - Pass `false` to disable trigger (use with external ref)\n\n### Interaction Props\n- **openOnHover**: boolean (default: false) - Open on mouse hover\n- **openOnClick**: boolean (default: true) - Open on click\n- **delay**: number (default: 100) - Delay in ms before opening on hover\n- **closeOnEscape**: boolean (default: true) - Close on Escape. Only the topmost open layer closes, so Escape inside a nested popover leaves its parent open\n- **closeOnClickOutside**: boolean (default: true) - Close on an outside press. A press dismisses this popover and every layer stacked above it, but never the layer that was pressed\n- **closeOnMouseLeave**: boolean (default: false) - Close when the pointer leaves the hover safe area. The safe area is the trigger, the panel, and a prediction cone toward the panel, so a diagonal move to the panel keeps it open.\n- **debugSafeArea**: boolean (default: false) - Show hover safe-area overlays. Trigger/panel rectangles render in blue; the prediction cone toward the panel renders in orange.\n\n### Focus & ARIA Props\n- **focusOnOpen**: 'first' | 'container' | false (default: false) - Where focus goes when the panel opens: `'first'` moves it to an `[autofocus]` / `[data-autofocus]` target or the first tabbable control, `'container'` focuses the panel itself, `false` keeps it on the trigger. Focus always returns to the trigger when the popover closes (Escape or outside press)\n- **haspopup**: 'dialog' | 'menu' | 'listbox' | 'tree' | 'grid' | true (default: 'dialog') - Value of `aria-haspopup` on the trigger, describing what the panel contains. `aria-expanded` and `aria-controls` are managed automatically alongside it, for the built-in Button trigger and for a snippet trigger using `{@attach popover.reference}`\n\n```svelte\n<script>\n\timport { Popover } from 'entasis/popover';\n</script>\n\n<Popover focusOnOpen=\"first\" haspopup=\"listbox\" trigger={{ content: 'Pick one' }}>\n\t<ul role=\"listbox\"><li role=\"option\" tabindex=\"0\">First</li></ul>\n</Popover>\n```\n\n### Visual Props\n- **size**: 'small' | 'normal' | 'large' (default: 'normal')\n - small: Compact popover size\n - normal: Standard popover size\n - large: Larger popover size\n- **transition**: TransitionConfig - Custom transition animation\n- **directedTransition**: boolean (default: true) - Transition direction follows position\n\n### Behavior Props\n- **lockScroll**: boolean (default: true) - Lock body scroll when open\n\n### Styling Props\n- **class**: string - Additional CSS classes\n- **theme**: ComponentTheme - Custom theme overrides\n\n## Structure\n\n```\n<PopoverTrigger>\n\t<Trigger />\n</PopoverTrigger>\n\n<PopoverContent>\n\t<Children />\n</PopoverContent>\n```\n\n## Examples\n\n### More Examples\n\n### With a custom trigger snippet\n```svelte\n<script>\n\timport { Popover } from 'entasis/popover';\n\timport { Button } from 'entasis/button';\n</script>\n\n<!-- {@attach popover.reference} anchors the panel to the element and keeps\n aria-haspopup / aria-expanded / aria-controls in sync on it -->\n<Popover>\n\t{#snippet trigger(popover)}\n\t\t<Button onclick={() => popover.toggle()} {@attach popover.reference}>Open</Button>\n\t{/snippet}\n\t\n\t<p>This is a popover!</p>\n</Popover>\n```\n\n### With button props\n```svelte\n<script>\n\timport { Popover } from 'entasis/popover';\n</script>\n\n<Popover \n\ttrigger={{\n\t\tcontent: \"Click Me\",\n\t\tcolor: \"secondary\",\n\t\tsize: \"small\"\n\t}}\n>\n\t<p>This is a popover!</p>\n</Popover>\n```\n\n### Different Positions\n\n```svelte\n<script>\n\timport { Popover } from 'entasis/popover';\n</script>\n\n<!-- Top -->\n<Popover position=\"top\" trigger={{ content: \"Top\" }}>\n\tTop popover\n</Popover>\n\n<!-- Bottom -->\n<Popover position=\"bottom\" trigger={{ content: \"Bottom\" }}>\n\tBottom popover\n</Popover>\n\n<!-- Left -->\n<Popover position=\"left\" trigger={{ content: \"Left\" }}>\n\tLeft popover\n</Popover>\n\n<!-- Right -->\n<Popover position=\"right\" trigger={{ content: \"Right\" }}>\n\tRight popover\n</Popover>\n```\n\n### Advanced Example\n\n```svelte\n<script>\n\timport { Popover } from 'entasis/popover';\n\timport { Button } from 'entasis/button';\n</script>\n\n<Popover position=\"bottom-start\" trigger={{ content: \"Menu\" }}>\n\t<div class=\"flex flex-col gap-1\">\n\t\t<Button variant=\"ghost\" fullWidth>Profile</Button>\n\t\t<Button variant=\"ghost\" fullWidth>Settings</Button>\n\t\t<Button variant=\"ghost\" fullWidth>Logout</Button>\n\t</div>\n</Popover>\n```\n\n### Open on Hover\n\n```svelte\n<script>\n\timport { Popover } from 'entasis/popover';\n</script>\n\n<Popover \n\ttrigger={{ content: \"Hover Me\" }}\n\topenOnHover\n\topenOnClick={false}\n\tdelay={200}\n>\n\tHover content\n</Popover>\n```\n\n### With Custom Offset\n\n```svelte\n<script>\n\timport { Popover } from 'entasis/popover';\n</script>\n\n<Popover \n\ttrigger={{ content: \"Trigger\" }}\n\tposition=\"bottom\"\n\toffset={20}\n>\n\t20px away from trigger\n</Popover>\n```\n\n### Fit Trigger Width\n\n```svelte\n<script>\n\timport { Popover } from 'entasis/popover';\n</script>\n\n<Popover \n\ttrigger={{ content: \"Click Me\" }}\n\tfitTrigger\n>\n\tPopover matches trigger width\n</Popover>\n```\n\n### Inline (static, in flow)\n\n```svelte\n<script>\n\timport { Popover } from 'entasis/popover';\n</script>\n\n<!-- Open in place, no portal: useful for docs, visual tests, or an always-visible panel -->\n<Popover inline open trigger={false}>\n\t<p>Rendered where the component sits.</p>\n</Popover>\n\n<!-- The trigger still toggles an inline panel -->\n<Popover inline trigger={{ content: 'Toggle' }}>\n\t<p>Expands below the trigger, in the flow.</p>\n</Popover>\n```\n\n### Mobile Bottom Sheet\n\n```svelte\n<script>\n\timport { Popover } from 'entasis/popover';\n</script>\n\n<Popover\n\ttrigger={{ content: \"Open filters\" }}\n\tposition=\"bottom\"\n\tmobileSheet\n>\n\tFilters content\n</Popover>\n```\n\n### Different Sizes\n\n```svelte\n<script>\n\timport { Popover } from 'entasis/popover';\n</script>\n\n<!-- Small -->\n<Popover size=\"small\" trigger={{ content: \"Small\" }}>\n\tSmall popover\n</Popover>\n\n<!-- Large -->\n<Popover size=\"large\" trigger={{ content: \"Large\" }}>\n\tLarge popover with more content\n</Popover>\n```\n\n### Close on Mouse Leave\n\n```svelte\n<script>\n\timport { Popover } from 'entasis/popover';\n</script>\n\n<Popover \n\ttrigger={{ content: \"Hover Me\" }}\n\topenOnHover\n\tcloseOnMouseLeave\n>\n\tCloses when you move outside the rectangle tolerance\n</Popover>\n```\n\n### With Lifecycle Hooks\n\n```svelte\n<script>\n\timport { Popover } from 'entasis/popover';\n</script>\n\n<Popover \n\ttrigger={{ content: \"Trigger\" }}\n\tonAfterOpen={(payload) => console.log('Popover opened', payload)}\n\tonAfterClose={(payload) => console.log('Popover closed', payload)}\n>\n\tWatch the console\n</Popover>\n```\n\n### User Card Popover with External Ref\n\n```svelte\n<script lang=\"ts\">\n\timport { Popover } from 'entasis/popover';\n\timport { Button } from 'entasis/button';\n\timport { Avatar } from 'entasis/avatar';\n\n\tlet avatarRef = $state<HTMLElement | null>(null);\n\tlet open = $state(false);\n\tconst user = { name: 'John Doe', email: 'john@example.com' };\n</script>\n\n<button type=\"button\" bind:this={avatarRef} onclick={() => (open = !open)}>\n\t<Avatar name={user.name} />\n</button>\n\n<Popover bind:open ref={avatarRef} position=\"bottom\">\n\t<div class=\"p-4\">\n\t\t<h3>{user.name}</h3>\n\t\t<p>{user.email}</p>\n\t\t<Button fullWidth>View Profile</Button>\n\t</div>\n</Popover>\n```\n\n## State Management\n\nThe Popover component uses a `PopoverState` instance that is passed to all slot snippets. This state object provides:\n\n- **id**: string - Popover identifier\n- **size**: Size - Current popover size\n- **position**: Placement - Current popover position\n- **offset**: number - Current offset value\n- **open()**: () => void - Method to open the popover\n- **close()**: () => void - Method to close the popover\n- **toggle()**: () => void - Method to toggle the popover\n- **reference**: attachment for a custom trigger element (`{@attach popover.reference}`); anchors the panel to it and keeps its `aria-haspopup`, `aria-expanded`, and `aria-controls` in sync\n\n## Accessibility\n\n- The trigger carries `aria-haspopup` (from `haspopup`), `aria-expanded`, and `aria-controls` \u2014 automatically for the built-in Button and for a snippet trigger using `{@attach popover.reference}`\n- `focusOnOpen` decides where focus lands on open; focus returns to the trigger on close, whether by Escape or an outside press\n- Non-modal: Tab is not contained and the page is not made inert (use Dialog for that)\n- Escape closes only the topmost open layer; an outside press dismisses every layer stacked above the one pressed. Popovers, menus, and dialogs share one layer stack\n- Keyboard navigation support\n\n## Notes\n\n- Popover is positioned using floating-ui, except with `inline`, where the document lays the panel out\n- Automatically adjusts position to stay in viewport\n- Multiple popovers can be stacked\n- Scroll locking prevents background scroll (when enabled)\n- Transitions animate based on position direction\n\n## Theme Customization\n\nThe Popover component uses a theme object that can be customized using the `theme` prop or by setting a global theme.\n\n### Theme Structure\n\nThe theme object contains the following parts:\n- **motion**: Open/close transition preset, keyed by `mode` (a floating panel scales, the mobile\n sheet slides up). Takes `in` / `out` FSO params plus a `duration` / `easing` motion token;\n the `transition` prop wins over it\n- **popover**: Main popover container styles\n\n### Theme Type Definition\n\n```typescript\nimport type { PopoverThemeProps } from 'entasis/popover';\n\n// Example theme customization\nconst customTheme: PopoverThemeProps = {\n popover: {\n base: 'z-[+50] fixed bg-surface-floating text-neutral w-fit rounded-xl raised isolate h-fit',\n size: {\n small: 'max-w-3xs w-full p-2',\n normal: 'max-w-xs w-full p-3',\n large: 'max-w-sm w-full p-4'\n }\n }\n};\n```\n\n### Available Variants\n\n**popover**:\n- base: Base classes applied to all popovers\n- Variants:\n - size: 'small' | 'normal' | 'large' - Controls max-width, width, and padding\n - mode: 'floating' | 'inline' | 'mobileSheet' - Set from `inline` / `mobileSheet`; `root` positions the wrapper (fixed, in flow, or full-screen sheet)\n\n### Usage Examples\n\n**Basic Theme Override**:\n```svelte\n<Popover \n trigger={{ content: \"Click Me\" }}\n theme={{\n popover: {\n base: 'rounded-xl lift-5 border-2 border-primary',\n size: {\n normal: 'max-w-md p-4'\n }\n }\n }}\n>\n Custom styled popover content\n</Popover>\n```\n\n**Size Customization**:\n```svelte\n<Popover \n size=\"large\"\n trigger={{ content: \"Large Popover\" }}\n theme={{\n popover: {\n size: {\n large: 'max-w-lg p-6'\n }\n }\n }}\n>\n Large popover with more padding\n</Popover>\n```\n\n**Global Theme Setting**:\n```svelte\n<script>\n import { setPopoverTheme } from './index.ts';\n \n setPopoverTheme({\n popover: {\n base: 'rounded-lg lift-5 backdrop-blur-sm bg-white/95',\n size: {\n normal: 'max-w-sm p-4'\n }\n }\n });\n</script>\n```\n";
1
+ export declare const popoverDescription = "\n# Popover Component\n\nThe Popover component displays floating content positioned relative to a trigger element. It's ideal for tooltips, dropdown menus, and contextual information.\n\n## Basic Usage\n\n```svelte\n<script>\n\timport { Popover } from 'entasis/popover';\n\t\t\n</script>\n// By default Popover comes with a button that triggers them, no need to define a callback and a $state\n<Popover trigger={{ content: \"Toggle Popover\" }}>\n\tPopover content here\n</Popover>\n```\n\n## Props\n\n### Core Props\n- **open**: boolean (bindable) - Controls popover visibility (optional when using trigger prop)\n- **defaultOpen**: boolean (default: false) - Initial state when open is not provided\n- **ref**: HTMLElement | null - Reference element to position popover against (optional when using trigger prop)\n- **id**: string - Unique identifier\n\n### Layout Props\n- **position**: 'top' | 'bottom' | 'left' | 'right' | 'top-start' | 'top-end' | 'bottom-start' | 'bottom-end' | 'left-start' | 'left-end' | 'right-start' | 'right-end' (default: 'bottom')\n - Determines where popover appears relative to trigger\n- **offset**: number - Distance in pixels from the reference element\n- **fitTrigger**: boolean (default: false) - Whether popover should match the width of the trigger element\n- **positionPanel**: ({ panel, reference }) => { x: number; y: number; minWidth?: number } | null - Place the panel yourself instead of floating-ui: return its viewport coordinates (the panel is `position: fixed`) and optionally a `minWidth` in px that replaces `fitTrigger`'s, or `null` to fall back to `position`. Called when the panel mounts and whenever floating-ui would reposition it. Select uses it to open over its trigger\n- **mobileSheet**: boolean (default: false) - On mobile viewports (<768px), render as a bottom sheet instead of an anchored floating panel\n- **inline**: boolean (default: false) - Render the panel in normal document flow where the component sits instead of portaling to the viewport-fixed layer: no floating-ui positioning, no scroll lock, no outside-press dismissal (Escape still closes it). Same panel classes and motion, and the trigger still toggles it. Wins over `mobileSheet`\n- **mobileSheetSizeTransition**: boolean (default: true) - Whether mobile-sheet panels animate intrinsic size changes\n\n### Event Props\n- **onOpenChange**: (open: boolean) => void - Called once for each library-requested state change\n- **onAfterOpen**: (payload: PopoverState) => void - Called after the open transition finishes\n- **onAfterClose**: (payload: PopoverState) => void - Called after the close transition finishes\n\n### Slot Props\n- **children**: Snippet<[PopoverState]> - Popover content\n- **trigger**: Snippet<[PopoverState]> | (ButtonProps & { content?: string }) | false - Trigger element\n - Pass a snippet function for custom trigger: `{#snippet trigger(popover)}...</snippet>`. Put\n `{@attach popover.trigger}` on any element (a card, an avatar, a Button) and it opens the\n panel on click and from the keyboard, with the ARIA state kept in sync. Type the parameter\n with `PopoverState` from `entasis/popover`.\n - Pass button props object for default button: `trigger={{ content: \"Click Me\", color: \"primary\" }}`.\n `children` (a string or a snippet) replaces `content` for a richer body; `prefix` and\n `suffix` work as on Button.\n - Pass `false` to disable trigger (use with external ref)\n\n### Interaction Props\n- **openOnHover**: boolean (default: false) - Open on mouse hover\n- **openOnClick**: boolean (default: true) - Open on click\n- **delay**: number (default: 100) - Delay in ms before opening on hover\n- **closeOnEscape**: boolean (default: true) - Close on Escape. Only the topmost open layer closes, so Escape inside a nested popover leaves its parent open\n- **closeOnClickOutside**: boolean (default: true) - Close on an outside press. A press dismisses this popover and every layer stacked above it, but never the layer that was pressed\n- **closeOnMouseLeave**: boolean (default: false) - Close when the pointer leaves the hover safe area. The safe area is the trigger, the panel, and a prediction cone toward the panel, so a diagonal move to the panel keeps it open.\n- **debugSafeArea**: boolean (default: false) - Show hover safe-area overlays. Trigger/panel rectangles render in blue; the prediction cone toward the panel renders in orange.\n\n### Focus & ARIA Props\n- **focusOnOpen**: 'first' | 'container' | false (default: false) - Where focus goes when the panel opens: `'first'` moves it to an `[autofocus]` / `[data-autofocus]` target or the first tabbable control, `'container'` focuses the panel itself, `false` keeps it on the trigger. Focus always returns to the trigger when the popover closes (Escape or outside press)\n- **haspopup**: 'dialog' | 'menu' | 'listbox' | 'tree' | 'grid' | true (default: 'dialog') - Value of `aria-haspopup` on the trigger, describing what the panel contains. `aria-expanded` and `aria-controls` are managed automatically alongside it, for the built-in Button trigger and for a snippet trigger using `{@attach popover.reference}`\n\n```svelte\n<script>\n\timport { Popover } from 'entasis/popover';\n</script>\n\n<Popover focusOnOpen=\"first\" haspopup=\"listbox\" trigger={{ content: 'Pick one' }}>\n\t<ul role=\"listbox\"><li role=\"option\" tabindex=\"0\">First</li></ul>\n</Popover>\n```\n\n### Visual Props\n- **size**: 'small' | 'normal' | 'large' (default: 'normal')\n - small: Compact popover size\n - normal: Standard popover size\n - large: Larger popover size\n- **transition**: TransitionConfig - Custom transition animation\n- **directedTransition**: boolean (default: true) - Transition direction follows position\n\n### Behavior Props\n- **lockScroll**: boolean (default: true) - Lock body scroll when open\n\n### Styling Props\n- **class**: string - Additional CSS classes\n- **theme**: ComponentTheme - Custom theme overrides\n\n## Structure\n\n```\n<PopoverTrigger>\n\t<Trigger />\n</PopoverTrigger>\n\n<PopoverContent>\n\t<Children />\n</PopoverContent>\n```\n\n## Examples\n\n### More Examples\n\n### With a custom trigger snippet\n```svelte\n<script>\n\timport { Popover } from 'entasis/popover';\n\timport { Button } from 'entasis/button';\n</script>\n\n<!-- {@attach popover.trigger} anchors the panel to the element, toggles it on click and keeps\n aria-haspopup / aria-expanded / aria-controls in sync on it -->\n<Popover>\n\t{#snippet trigger(popover)}\n\t\t<Button {@attach popover.trigger}>Open</Button>\n\t{/snippet}\n\t\n\t<p>This is a popover!</p>\n</Popover>\n```\n\nAny element works. One that is not a control gets `role=\"button\"`, `tabindex=\"0\"` and Enter/Space\nactivation:\n\n```svelte\n<script lang=\"ts\">\n\timport { Popover, type PopoverState } from 'entasis/popover';\n\timport { Avatar } from 'entasis/avatar';\n</script>\n\n<Popover>\n\t{#snippet trigger(popover: PopoverState)}\n\t\t<div class=\"flex items-center gap-sm\" {@attach popover.trigger}>\n\t\t\t<Avatar name=\"Ada Lovelace\" />\n\t\t\t<span>Ada Lovelace</span>\n\t\t</div>\n\t{/snippet}\n\n\t<p>Profile details</p>\n</Popover>\n```\n\n### With button props\n```svelte\n<script>\n\timport { Popover } from 'entasis/popover';\n</script>\n\n<Popover \n\ttrigger={{\n\t\tcontent: \"Click Me\",\n\t\tcolor: \"secondary\",\n\t\tsize: \"small\"\n\t}}\n>\n\t<p>This is a popover!</p>\n</Popover>\n```\n\n### Different Positions\n\n```svelte\n<script>\n\timport { Popover } from 'entasis/popover';\n</script>\n\n<!-- Top -->\n<Popover position=\"top\" trigger={{ content: \"Top\" }}>\n\tTop popover\n</Popover>\n\n<!-- Bottom -->\n<Popover position=\"bottom\" trigger={{ content: \"Bottom\" }}>\n\tBottom popover\n</Popover>\n\n<!-- Left -->\n<Popover position=\"left\" trigger={{ content: \"Left\" }}>\n\tLeft popover\n</Popover>\n\n<!-- Right -->\n<Popover position=\"right\" trigger={{ content: \"Right\" }}>\n\tRight popover\n</Popover>\n```\n\n### Advanced Example\n\n```svelte\n<script>\n\timport { Popover } from 'entasis/popover';\n\timport { Button } from 'entasis/button';\n</script>\n\n<Popover position=\"bottom-start\" trigger={{ content: \"Menu\" }}>\n\t<div class=\"flex flex-col gap-1\">\n\t\t<Button variant=\"ghost\" fullWidth>Profile</Button>\n\t\t<Button variant=\"ghost\" fullWidth>Settings</Button>\n\t\t<Button variant=\"ghost\" fullWidth>Logout</Button>\n\t</div>\n</Popover>\n```\n\n### Open on Hover\n\n```svelte\n<script>\n\timport { Popover } from 'entasis/popover';\n</script>\n\n<Popover \n\ttrigger={{ content: \"Hover Me\" }}\n\topenOnHover\n\topenOnClick={false}\n\tdelay={200}\n>\n\tHover content\n</Popover>\n```\n\n### With Custom Offset\n\n```svelte\n<script>\n\timport { Popover } from 'entasis/popover';\n</script>\n\n<Popover \n\ttrigger={{ content: \"Trigger\" }}\n\tposition=\"bottom\"\n\toffset={20}\n>\n\t20px away from trigger\n</Popover>\n```\n\n### Fit Trigger Width\n\n```svelte\n<script>\n\timport { Popover } from 'entasis/popover';\n</script>\n\n<Popover \n\ttrigger={{ content: \"Click Me\" }}\n\tfitTrigger\n>\n\tPopover matches trigger width\n</Popover>\n```\n\n### Inline (static, in flow)\n\n```svelte\n<script>\n\timport { Popover } from 'entasis/popover';\n</script>\n\n<!-- Open in place, no portal: useful for docs, visual tests, or an always-visible panel -->\n<Popover inline open trigger={false}>\n\t<p>Rendered where the component sits.</p>\n</Popover>\n\n<!-- The trigger still toggles an inline panel -->\n<Popover inline trigger={{ content: 'Toggle' }}>\n\t<p>Expands below the trigger, in the flow.</p>\n</Popover>\n```\n\n### Mobile Bottom Sheet\n\n```svelte\n<script>\n\timport { Popover } from 'entasis/popover';\n</script>\n\n<Popover\n\ttrigger={{ content: \"Open filters\" }}\n\tposition=\"bottom\"\n\tmobileSheet\n>\n\tFilters content\n</Popover>\n```\n\n### Different Sizes\n\n```svelte\n<script>\n\timport { Popover } from 'entasis/popover';\n</script>\n\n<!-- Small -->\n<Popover size=\"small\" trigger={{ content: \"Small\" }}>\n\tSmall popover\n</Popover>\n\n<!-- Large -->\n<Popover size=\"large\" trigger={{ content: \"Large\" }}>\n\tLarge popover with more content\n</Popover>\n```\n\n### Close on Mouse Leave\n\n```svelte\n<script>\n\timport { Popover } from 'entasis/popover';\n</script>\n\n<Popover \n\ttrigger={{ content: \"Hover Me\" }}\n\topenOnHover\n\tcloseOnMouseLeave\n>\n\tCloses when you move outside the rectangle tolerance\n</Popover>\n```\n\n### With Lifecycle Hooks\n\n```svelte\n<script>\n\timport { Popover } from 'entasis/popover';\n</script>\n\n<Popover \n\ttrigger={{ content: \"Trigger\" }}\n\tonAfterOpen={(payload) => console.log('Popover opened', payload)}\n\tonAfterClose={(payload) => console.log('Popover closed', payload)}\n>\n\tWatch the console\n</Popover>\n```\n\n### User Card Popover with External Ref\n\n```svelte\n<script lang=\"ts\">\n\timport { Popover } from 'entasis/popover';\n\timport { Button } from 'entasis/button';\n\timport { Avatar } from 'entasis/avatar';\n\n\tlet avatarRef = $state<HTMLElement | null>(null);\n\tlet open = $state(false);\n\tconst user = { name: 'John Doe', email: 'john@example.com' };\n</script>\n\n<button type=\"button\" bind:this={avatarRef} onclick={() => (open = !open)}>\n\t<Avatar name={user.name} />\n</button>\n\n<Popover bind:open ref={avatarRef} position=\"bottom\">\n\t<div class=\"p-4\">\n\t\t<h3>{user.name}</h3>\n\t\t<p>{user.email}</p>\n\t\t<Button fullWidth>View Profile</Button>\n\t</div>\n</Popover>\n```\n\n## State Management\n\nThe Popover component uses a `PopoverState` instance that is passed to all slot snippets. This state object provides:\n\n- **id**: string - Popover identifier\n- **size**: Size - Current popover size\n- **position**: Placement - Current popover position\n- **offset**: number - Current offset value\n- **open()**: () => void - Method to open the popover\n- **close()**: () => void - Method to close the popover\n- **toggle()**: () => void - Method to toggle the popover\n- **trigger**: attachment that makes any element the trigger (`{@attach popover.trigger}`): `reference` plus a click toggle (when `openOnClick`), and on a non-control `role=\"button\"`, `tabindex=\"0\"` and Enter/Space activation. The element's own handler wins: when it calls `open`, `close`, `toggle` or `setOpen` during the click, the default toggle stands down, so a kept `onclick={popover.toggle}` does not toggle twice\n- **reference**: attachment for a custom trigger element (`{@attach popover.reference}`); anchors the panel to it and keeps its `aria-haspopup`, `aria-expanded`, and `aria-controls` in sync, leaving clicks and keyboard to the element\n\n## Accessibility\n\n- The trigger carries `aria-haspopup` (from `haspopup`), `aria-expanded`, and `aria-controls` \u2014 automatically for the built-in Button and for a snippet trigger using `{@attach popover.trigger}` or `{@attach popover.reference}`\n- `focusOnOpen` decides where focus lands on open; focus returns to the trigger on close, whether by Escape or an outside press\n- Non-modal: Tab is not contained and the page is not made inert (use Dialog for that)\n- Escape closes only the topmost open layer; an outside press dismisses every layer stacked above the one pressed. Popovers, menus, and dialogs share one layer stack\n- Keyboard navigation support\n\n## Notes\n\n- Popover is positioned using floating-ui, except with `inline`, where the document lays the panel out\n- Automatically adjusts position to stay in viewport\n- Multiple popovers can be stacked\n- Scroll locking prevents background scroll (when enabled)\n- Transitions animate based on position direction\n\n## Theme Customization\n\nThe Popover component uses a theme object that can be customized using the `theme` prop or by setting a global theme.\n\n### Theme Structure\n\nThe theme object contains the following parts:\n- **motion**: Open/close transition preset, keyed by `mode` (a floating panel scales, the mobile\n sheet slides up). Takes `in` / `out` FSO params plus a `duration` / `easing` motion token;\n the `transition` prop wins over it\n- **popover**: Main popover container styles\n\n### Theme Type Definition\n\n```typescript\nimport type { PopoverThemeProps } from 'entasis/popover';\n\n// Example theme customization\nconst customTheme: PopoverThemeProps = {\n popover: {\n base: 'z-[+50] fixed bg-surface-floating text-neutral w-fit rounded-xl raised isolate h-fit',\n size: {\n small: 'max-w-3xs w-full p-2',\n normal: 'max-w-xs w-full p-3',\n large: 'max-w-sm w-full p-4'\n }\n }\n};\n```\n\n### Available Variants\n\n**popover**:\n- base: Base classes applied to all popovers\n- Variants:\n - size: 'small' | 'normal' | 'large' - Controls max-width, width, and padding\n - mode: 'floating' | 'inline' | 'mobileSheet' - Set from `inline` / `mobileSheet`; `root` positions the wrapper (fixed, in flow, or full-screen sheet)\n\n### Usage Examples\n\n**Basic Theme Override**:\n```svelte\n<Popover \n trigger={{ content: \"Click Me\" }}\n theme={{\n popover: {\n base: 'rounded-xl lift-5 border-2 border-primary',\n size: {\n normal: 'max-w-md p-4'\n }\n }\n }}\n>\n Custom styled popover content\n</Popover>\n```\n\n**Size Customization**:\n```svelte\n<Popover \n size=\"large\"\n trigger={{ content: \"Large Popover\" }}\n theme={{\n popover: {\n size: {\n large: 'max-w-lg p-6'\n }\n }\n }}\n>\n Large popover with more padding\n</Popover>\n```\n\n**Global Theme Setting**:\n```svelte\n<script>\n import { setPopoverTheme } from './index.ts';\n \n setPopoverTheme({\n popover: {\n base: 'rounded-lg lift-5 backdrop-blur-sm bg-white/95',\n size: {\n normal: 'max-w-sm p-4'\n }\n }\n });\n</script>\n```\n";
@@ -42,8 +42,13 @@ The Popover component displays floating content positioned relative to a trigger
42
42
  ### Slot Props
43
43
  - **children**: Snippet<[PopoverState]> - Popover content
44
44
  - **trigger**: Snippet<[PopoverState]> | (ButtonProps & { content?: string }) | false - Trigger element
45
- - Pass a snippet function for custom trigger: \`{#snippet trigger(popover)}...</snippet>\`
46
- - Pass button props object for default button: \`trigger={{ content: "Click Me", color: "primary" }}\`
45
+ - Pass a snippet function for custom trigger: \`{#snippet trigger(popover)}...</snippet>\`. Put
46
+ \`{@attach popover.trigger}\` on any element (a card, an avatar, a Button) and it opens the
47
+ panel on click and from the keyboard, with the ARIA state kept in sync. Type the parameter
48
+ with \`PopoverState\` from \`entasis/popover\`.
49
+ - Pass button props object for default button: \`trigger={{ content: "Click Me", color: "primary" }}\`.
50
+ \`children\` (a string or a snippet) replaces \`content\` for a richer body; \`prefix\` and
51
+ \`suffix\` work as on Button.
47
52
  - Pass \`false\` to disable trigger (use with external ref)
48
53
 
49
54
  ### Interaction Props
@@ -107,17 +112,38 @@ The Popover component displays floating content positioned relative to a trigger
107
112
  import { Button } from '../Button/index.ts';
108
113
  </script>
109
114
 
110
- <!-- {@attach popover.reference} anchors the panel to the element and keeps
115
+ <!-- {@attach popover.trigger} anchors the panel to the element, toggles it on click and keeps
111
116
  aria-haspopup / aria-expanded / aria-controls in sync on it -->
112
117
  <Popover>
113
118
  {#snippet trigger(popover)}
114
- <Button onclick={() => popover.toggle()} {@attach popover.reference}>Open</Button>
119
+ <Button {@attach popover.trigger}>Open</Button>
115
120
  {/snippet}
116
121
 
117
122
  <p>This is a popover!</p>
118
123
  </Popover>
119
124
  \`\`\`
120
125
 
126
+ Any element works. One that is not a control gets \`role="button"\`, \`tabindex="0"\` and Enter/Space
127
+ activation:
128
+
129
+ \`\`\`svelte
130
+ <script lang="ts">
131
+ import { Popover, type PopoverState } from './index.ts';
132
+ import { Avatar } from '../Avatar/index.ts';
133
+ </script>
134
+
135
+ <Popover>
136
+ {#snippet trigger(popover: PopoverState)}
137
+ <div class="flex items-center gap-sm" {@attach popover.trigger}>
138
+ <Avatar name="Ada Lovelace" />
139
+ <span>Ada Lovelace</span>
140
+ </div>
141
+ {/snippet}
142
+
143
+ <p>Profile details</p>
144
+ </Popover>
145
+ \`\`\`
146
+
121
147
  ### With button props
122
148
  \`\`\`svelte
123
149
  <script>
@@ -349,11 +375,12 @@ The Popover component uses a \`PopoverState\` instance that is passed to all slo
349
375
  - **open()**: () => void - Method to open the popover
350
376
  - **close()**: () => void - Method to close the popover
351
377
  - **toggle()**: () => void - Method to toggle the popover
352
- - **reference**: attachment for a custom trigger element (\`{@attach popover.reference}\`); anchors the panel to it and keeps its \`aria-haspopup\`, \`aria-expanded\`, and \`aria-controls\` in sync
378
+ - **trigger**: attachment that makes any element the trigger (\`{@attach popover.trigger}\`): \`reference\` plus a click toggle (when \`openOnClick\`), and on a non-control \`role="button"\`, \`tabindex="0"\` and Enter/Space activation. The element's own handler wins: when it calls \`open\`, \`close\`, \`toggle\` or \`setOpen\` during the click, the default toggle stands down, so a kept \`onclick={popover.toggle}\` does not toggle twice
379
+ - **reference**: attachment for a custom trigger element (\`{@attach popover.reference}\`); anchors the panel to it and keeps its \`aria-haspopup\`, \`aria-expanded\`, and \`aria-controls\` in sync, leaving clicks and keyboard to the element
353
380
 
354
381
  ## Accessibility
355
382
 
356
- - The trigger carries \`aria-haspopup\` (from \`haspopup\`), \`aria-expanded\`, and \`aria-controls\` — automatically for the built-in Button and for a snippet trigger using \`{@attach popover.reference}\`
383
+ - The trigger carries \`aria-haspopup\` (from \`haspopup\`), \`aria-expanded\`, and \`aria-controls\` — automatically for the built-in Button and for a snippet trigger using \`{@attach popover.trigger}\` or \`{@attach popover.reference}\`
357
384
  - \`focusOnOpen\` decides where focus lands on open; focus returns to the trigger on close, whether by Escape or an outside press
358
385
  - Non-modal: Tab is not contained and the page is not made inert (use Dialog for that)
359
386
  - Escape closes only the topmost open layer; an outside press dismisses every layer stacked above the one pressed. Popovers, menus, and dialogs share one layer stack
@@ -11,13 +11,14 @@ interface PopoverOptions extends MakeRequired<Pick<PopoverProps, 'id' | 'size' |
11
11
  }
12
12
  declare const PopoverOptionsBase: new () => PopoverOptions;
13
13
  export declare class PopoverState extends PopoverOptionsBase {
14
+ #private;
14
15
  triggerReference: HTMLElement | null;
15
16
  referenceElement: HTMLElement | VirtualElement | null;
16
17
  dialogElement: HTMLElement | null;
17
18
  transformOrigin: string;
18
19
  triggerWidth: number | null;
19
20
  parent: PopoverState | null;
20
- children: PopoverState[];
21
+ get children(): readonly PopoverState[];
21
22
  hasChildOpen: boolean;
22
23
  hasTransitioned: boolean;
23
24
  theme: import("../Theme/theme.state.svelte.js").ThemeState;
@@ -64,6 +65,13 @@ export declare class PopoverState extends PopoverOptionsBase {
64
65
  dialog: (node: HTMLDialogElement) => () => void;
65
66
  panel: (node: HTMLElement) => () => void;
66
67
  reference: (node: HTMLElement) => () => void;
68
+ /**
69
+ * Makes any element a complete trigger: it anchors the panel (as \`reference\` does), toggles
70
+ * it on click when \`openOnClick\` allows, and keeps \`aria-expanded\` and friends in sync.
71
+ * An element that is not already a control also gets \`role="button"\`, \`tabindex="0"\` and
72
+ * Enter/Space activation, so a card or an avatar opens the panel from the keyboard too.
73
+ */
74
+ trigger: (node: HTMLElement) => () => void;
67
75
  }
68
76
  export declare const usePopoverContext: () => PopoverState | null;
69
77
  export {};
@@ -1,4 +1,5 @@
1
1
  import { bind } from '../../utils/state.svelte.js';
2
+ import { Registry } from '../../utils/registry.svelte.js';
2
3
  import { getContext, hasContext, onMount, setContext, untrack } from 'svelte';
3
4
  import { useTheme } from '../Theme/theme.state.svelte.js';
4
5
  import { computePosition, autoUpdate, hide, offset, shift, flip } from '@floating-ui/dom';
@@ -14,6 +15,10 @@ const PopoverOptionsBase = class {
14
15
  };
15
16
  // The one place the trigger's semantic state becomes ARIA, for triggers the library cannot
16
17
  // pass props to (a snippet trigger carrying `{@attach popover.reference}`).
18
+ /** Elements that may carry popup ARIA state. */
19
+ const POPUP_CONTROL = 'button, a[href], input, select, textarea, summary, [role="button"], [role="link"], [role="combobox"], [role="menuitem"], [role="menuitemcheckbox"], [role="menuitemradio"], [role="tab"], [role="option"], [role="treeitem"], [role="gridcell"], [role="switch"], [role="checkbox"]';
20
+ /** Elements the browser already focuses and activates from the keyboard. */
21
+ const NATIVE_CONTROL = 'button, a[href], input, select, textarea, summary';
17
22
  const TRIGGER_ARIA = {
18
23
  haspopup: 'aria-haspopup',
19
24
  expanded: 'aria-expanded',
@@ -29,7 +34,11 @@ export class PopoverState extends PopoverOptionsBase {
29
34
  // Trigger-matched width (fitTrigger), applied to the panel.
30
35
  triggerWidth = $state(null);
31
36
  parent = getContext('popover');
32
- children = $state([]);
37
+ // Nested popovers join while they mount and leave when torn down; see `Registry`.
38
+ #children = new Registry();
39
+ get children() {
40
+ return this.#children.items;
41
+ }
33
42
  hasChildOpen = $derived(this.children.some((d) => d.isOpen));
34
43
  hasTransitioned = $state(false);
35
44
  theme = useTheme();
@@ -84,12 +93,7 @@ export class PopoverState extends PopoverOptionsBase {
84
93
  },
85
94
  delay: this.delay
86
95
  });
87
- addChild = (child) => () => {
88
- this.children.push(child);
89
- return () => {
90
- this.children = this.children.filter((d) => d.id !== child.id);
91
- };
92
- };
96
+ addChild = (child) => () => this.#children.add(child);
93
97
  constructor(options) {
94
98
  super();
95
99
  bind(this, options);
@@ -109,7 +113,10 @@ export class PopoverState extends PopoverOptionsBase {
109
113
  close = () => {
110
114
  this.setOpen(false);
111
115
  };
116
+ /** Calls to `setOpen`, direct or through `open`, `close` and `toggle`, unchanged state included. */
117
+ #openRequests = 0;
112
118
  setOpen = (nextOpen) => {
119
+ this.#openRequests += 1;
113
120
  if (this.isOpen === nextOpen)
114
121
  return;
115
122
  this.isOpen = nextOpen;
@@ -236,7 +243,7 @@ export class PopoverState extends PopoverOptionsBase {
236
243
  // Keep the trigger's ARIA in sync whichever way it was rendered (snippet or Button).
237
244
  // Only real controls carry aria-haspopup/aria-expanded; a wrapper div or a
238
245
  // presentational span used as the anchor must not (axe: aria-allowed-attr).
239
- const canCarryPopupState = node.matches('button, a[href], input, select, textarea, summary, [role="button"], [role="link"], [role="combobox"], [role="menuitem"], [role="menuitemcheckbox"], [role="menuitemradio"], [role="tab"], [role="option"], [role="treeitem"], [role="gridcell"], [role="switch"], [role="checkbox"]');
246
+ const canCarryPopupState = node.matches(POPUP_CONTROL);
240
247
  $effect(() => {
241
248
  if (!canCarryPopupState)
242
249
  return;
@@ -261,6 +268,52 @@ export class PopoverState extends PopoverOptionsBase {
261
268
  };
262
269
  });
263
270
  };
271
+ /**
272
+ * Makes any element a complete trigger: it anchors the panel (as \`reference\` does), toggles
273
+ * it on click when \`openOnClick\` allows, and keeps \`aria-expanded\` and friends in sync.
274
+ * An element that is not already a control also gets \`role="button"\`, \`tabindex="0"\` and
275
+ * Enter/Space activation, so a card or an avatar opens the panel from the keyboard too.
276
+ */
277
+ trigger = (node) => {
278
+ const nativeControl = node.matches(NATIVE_CONTROL);
279
+ if (!node.matches(POPUP_CONTROL) && !node.hasAttribute('role')) {
280
+ node.setAttribute('role', 'button');
281
+ }
282
+ if (!nativeControl && !node.hasAttribute('tabindex'))
283
+ node.tabIndex = 0;
284
+ // The element's own handler wins. One that still opens, closes or toggles the popover
285
+ // (`onclick={popover.toggle}` kept from before this attachment existed) has acted by the
286
+ // time the event finishes dispatching, so the default toggle waits until then and runs only
287
+ // if nothing asked for an open state meanwhile; toggling regardless would undo that call.
288
+ let pending;
289
+ const activate = () => {
290
+ if (!this.openOnClick)
291
+ return;
292
+ const requests = this.#openRequests;
293
+ clearTimeout(pending);
294
+ pending = setTimeout(() => {
295
+ if (this.#openRequests === requests)
296
+ this.toggle();
297
+ });
298
+ };
299
+ const onKeydown = (event) => {
300
+ if (nativeControl || event.target !== node)
301
+ return;
302
+ if (event.key !== 'Enter' && event.key !== ' ')
303
+ return;
304
+ event.preventDefault();
305
+ activate();
306
+ };
307
+ node.addEventListener('click', activate);
308
+ node.addEventListener('keydown', onKeydown);
309
+ const offReference = this.reference(node);
310
+ return () => {
311
+ clearTimeout(pending);
312
+ node.removeEventListener('click', activate);
313
+ node.removeEventListener('keydown', onKeydown);
314
+ offReference?.();
315
+ };
316
+ };
264
317
  }
265
318
  // Use interface merging to add the properties
266
319
  export const usePopoverContext = () => {
@@ -12,7 +12,7 @@ const defaultPopoverContainer = cva({
12
12
  // `inline` keeps the panel in normal flow where the component sits: no portal, no
13
13
  // fixed positioning, no floating-ui placement.
14
14
  inline: 'static h-fit w-fit',
15
- mobileSheet: 'inset-0 flex h-dvh w-dvw max-w-none items-end justify-center overflow-hidden bg-neutral/40 backdrop-blur-xs'
15
+ mobileSheet: 'inset-0 flex h-dvh w-dvw max-w-none items-end justify-center overflow-hidden bg-black/40 backdrop-blur-xs dark:bg-black/60'
16
16
  }
17
17
  },
18
18
  defaultVariants: {