@sovereignfs/ui 0.23.0 → 0.33.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 (109) hide show
  1. package/dist/Button.module.css +75 -8
  2. package/dist/Calendar.module.css +126 -0
  3. package/dist/Card.module.css +4 -2
  4. package/dist/Checkbox.module.css +14 -2
  5. package/dist/CodeTextarea.module.css +53 -0
  6. package/dist/ConfirmDialog.module.css +109 -0
  7. package/dist/DatePicker.module.css +57 -0
  8. package/dist/Dialog.module.css +73 -46
  9. package/dist/DragHandleRow.module.css +9 -3
  10. package/dist/Drawer.module.css +61 -0
  11. package/dist/Icon.module.css +5 -0
  12. package/dist/Menu.module.css +82 -0
  13. package/dist/NavTabs.module.css +4 -2
  14. package/dist/OverlayHeader.module.css +98 -0
  15. package/dist/Popover.module.css +31 -1
  16. package/dist/SegmentedControl.module.css +4 -2
  17. package/dist/Sheet.module.css +55 -0
  18. package/dist/SplitPane.module.css +85 -0
  19. package/dist/StatusBadge.module.css +59 -0
  20. package/dist/SystemBanner.module.css +4 -2
  21. package/dist/Tabs.module.css +4 -2
  22. package/dist/TagInput.module.css +141 -0
  23. package/dist/Toast.module.css +4 -2
  24. package/dist/Tooltip.module.css +6 -1
  25. package/dist/index.d.ts +493 -17
  26. package/dist/index.js +1390 -209
  27. package/dist/tokens/primitives.css +19 -0
  28. package/dist/tokens/semantic.css +13 -0
  29. package/package.json +1 -1
  30. package/src/__tests__/motion.test.tsx +105 -0
  31. package/src/components/Button/Button.module.css +75 -8
  32. package/src/components/Button/Button.stories.tsx +6 -0
  33. package/src/components/Button/Button.tsx +17 -1
  34. package/src/components/Button/__tests__/Button.test.tsx +19 -0
  35. package/src/components/Calendar/Calendar.module.css +126 -0
  36. package/src/components/Calendar/Calendar.tsx +213 -0
  37. package/src/components/Calendar/__tests__/Calendar.test.tsx +98 -0
  38. package/src/components/Calendar/dateUtils.ts +97 -0
  39. package/src/components/Card/Card.module.css +4 -2
  40. package/src/components/Checkbox/Checkbox.module.css +14 -2
  41. package/src/components/Checkbox/Checkbox.tsx +22 -12
  42. package/src/components/CodeTextarea/CodeTextarea.module.css +53 -0
  43. package/src/components/CodeTextarea/CodeTextarea.stories.tsx +77 -0
  44. package/src/components/CodeTextarea/CodeTextarea.tsx +36 -0
  45. package/src/components/ConfirmDialog/ConfirmDialog.module.css +109 -0
  46. package/src/components/ConfirmDialog/ConfirmDialog.tsx +124 -0
  47. package/src/components/ConfirmDialog/__tests__/ConfirmDialog.test.tsx +146 -0
  48. package/src/components/DatePicker/DatePicker.module.css +57 -0
  49. package/src/components/DatePicker/DatePicker.tsx +103 -0
  50. package/src/components/DatePicker/__tests__/DatePicker.test.tsx +89 -0
  51. package/src/components/Dialog/Dialog.module.css +73 -46
  52. package/src/components/Dialog/Dialog.stories.tsx +51 -1
  53. package/src/components/Dialog/Dialog.tsx +86 -22
  54. package/src/components/Dialog/__tests__/Dialog.test.tsx +67 -3
  55. package/src/components/DragHandleRow/DragHandleRow.module.css +9 -3
  56. package/src/components/Drawer/Drawer.module.css +61 -0
  57. package/src/components/Drawer/Drawer.tsx +117 -13
  58. package/src/components/Drawer/__tests__/Drawer.test.tsx +123 -2
  59. package/src/components/Icon/Icon.module.css +5 -0
  60. package/src/components/Icon/Icon.stories.tsx +4 -3
  61. package/src/components/Icon/Icon.tsx +1 -1
  62. package/src/components/Icon/icons/calendar.tsx +23 -0
  63. package/src/components/Icon/icons/ellipsis-vertical.tsx +22 -0
  64. package/src/components/Icon/icons/index.ts +8 -4
  65. package/src/components/Icon/icons/sliders-horizontal.tsx +9 -9
  66. package/src/components/Menu/Menu.module.css +82 -0
  67. package/src/components/Menu/Menu.tsx +137 -0
  68. package/src/components/Menu/__tests__/Menu.test.tsx +197 -0
  69. package/src/components/NavTabs/NavTabs.module.css +4 -2
  70. package/src/components/OverlayHeader/OverlayHeader.module.css +98 -0
  71. package/src/components/OverlayHeader/OverlayHeader.tsx +62 -0
  72. package/src/components/OverlayHeader/__tests__/OverlayHeader.test.tsx +51 -0
  73. package/src/components/Popover/Popover.module.css +31 -1
  74. package/src/components/Popover/Popover.stories.tsx +126 -1
  75. package/src/components/Popover/Popover.tsx +140 -7
  76. package/src/components/SegmentedControl/SegmentedControl.module.css +4 -2
  77. package/src/components/Sheet/Sheet.module.css +55 -0
  78. package/src/components/Sheet/Sheet.tsx +151 -0
  79. package/src/components/Sheet/__tests__/Sheet.test.tsx +99 -0
  80. package/src/components/SplitPane/SplitPane.module.css +85 -0
  81. package/src/components/SplitPane/SplitPane.stories.tsx +160 -0
  82. package/src/components/SplitPane/SplitPane.tsx +149 -0
  83. package/src/components/SplitPane/__tests__/SplitPane.test.tsx +51 -0
  84. package/src/components/StatusBadge/StatusBadge.module.css +59 -0
  85. package/src/components/StatusBadge/StatusBadge.stories.tsx +74 -0
  86. package/src/components/StatusBadge/StatusBadge.tsx +65 -0
  87. package/src/components/SystemBanner/SystemBanner.module.css +4 -2
  88. package/src/components/Tabs/Tabs.module.css +4 -2
  89. package/src/components/TagInput/TagInput.module.css +141 -0
  90. package/src/components/TagInput/TagInput.stories.tsx +109 -0
  91. package/src/components/TagInput/TagInput.tsx +174 -0
  92. package/src/components/TagInput/__tests__/TagInput.test.tsx +69 -0
  93. package/src/components/Toast/Toast.module.css +4 -2
  94. package/src/components/Tooltip/Tooltip.module.css +6 -1
  95. package/src/hooks/__tests__/useDoubleTap.test.tsx +117 -0
  96. package/src/hooks/__tests__/useIsMobile.test.tsx +80 -0
  97. package/src/hooks/__tests__/useLongPress.test.tsx +158 -0
  98. package/src/hooks/index.ts +4 -0
  99. package/src/hooks/useDoubleTap.ts +91 -0
  100. package/src/hooks/useIsMobile.ts +33 -0
  101. package/src/hooks/useLongPress.ts +185 -0
  102. package/src/index.ts +30 -1
  103. package/src/motion.ts +78 -0
  104. package/src/stories/DesignSystemOverview.stories.tsx +292 -4
  105. package/src/stories/InteractionHooks.stories.tsx +388 -0
  106. package/src/stories/TokenGallery.stories.tsx +85 -1
  107. package/src/tokens/primitives.css +19 -0
  108. package/src/tokens/semantic.css +13 -0
  109. package/src/components/Icon/icons/terminal.tsx +0 -19
package/dist/index.d.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import * as react from 'react';
2
- import { ReactNode, ButtonHTMLAttributes, SelectHTMLAttributes, ReactElement, InputHTMLAttributes, TextareaHTMLAttributes, HTMLAttributes } from 'react';
2
+ import { ReactNode, ButtonHTMLAttributes, SelectHTMLAttributes, ReactElement, CSSProperties, InputHTMLAttributes, TextareaHTMLAttributes, HTMLAttributes, PointerEvent, MouseEvent } from 'react';
3
3
 
4
4
  type BadgeVariant = 'role' | 'status' | 'mono';
5
5
  type BadgeStatus = 'active' | 'enabled' | 'deactivated' | 'failed' | 'invited' | 'pending' | 'neutral';
@@ -18,7 +18,7 @@ interface BadgeProps {
18
18
  */
19
19
  declare function Badge({ variant, status, children }: BadgeProps): react.JSX.Element;
20
20
 
21
- type IconName = 'house' | 'settings' | 'log-out' | 'chevron-right' | 'chevron-left' | 'chevron-down' | 'chevron-up' | 'x' | 'check' | 'plus' | 'trash-2' | 'pencil' | 'rotate-ccw' | 'search' | 'user' | 'shield' | 'lock' | 'eye' | 'eye-off' | 'mail' | 'bell' | 'activity' | 'package' | 'grid-2x2' | 'info' | 'alert-triangle' | 'sliders-horizontal' | 'terminal';
21
+ type IconName = 'house' | 'settings' | 'log-out' | 'chevron-right' | 'chevron-left' | 'chevron-down' | 'chevron-up' | 'x' | 'check' | 'plus' | 'trash-2' | 'pencil' | 'rotate-ccw' | 'search' | 'user' | 'shield' | 'lock' | 'eye' | 'eye-off' | 'mail' | 'bell' | 'activity' | 'package' | 'grid-2x2' | 'info' | 'alert-triangle' | 'calendar' | 'sliders-horizontal' | 'ellipsis-vertical';
22
22
 
23
23
  /** Decorative icon — visually meaningful but described by surrounding text. */
24
24
  type DecorativeProps = {
@@ -34,7 +34,7 @@ interface IconProps {
34
34
  /** Name of the icon from the curated Sovereign set. */
35
35
  name: IconName;
36
36
  /** Visual size. Defaults to `"md"` (20 px). */
37
- size?: 'sm' | 'md' | 'lg';
37
+ size?: 'xs' | 'sm' | 'md' | 'lg';
38
38
  /** Additional CSS class applied to the SVG element. */
39
39
  className?: string;
40
40
  }
@@ -153,25 +153,53 @@ interface PopoverProps {
153
153
  onClose: () => void;
154
154
  /** Which edge of the trigger the panel aligns to. Default: 'right'. */
155
155
  align?: 'left' | 'right';
156
- /** Panel width in px. Default: 288. */
157
- width?: number;
156
+ /** Panel width in px, or `'trigger'` to match the trigger element's own
157
+ * rendered width exactly (e.g. a dropdown under a full-width form field,
158
+ * where a fixed pixel width would either overflow or leave a gap
159
+ * depending on viewport size). Default: 288. */
160
+ width?: number | 'trigger';
161
+ /**
162
+ * Inline style overrides for the panel, merged after `width`. Escape hatch
163
+ * for callers that need to override the panel's own chrome (e.g. no
164
+ * rounded corners for a compact swatch picker) — inline styles always win
165
+ * regardless of CSS module load order, unlike passing an extra className.
166
+ */
167
+ panelStyle?: CSSProperties;
158
168
  'aria-label': string;
159
169
  children: ReactNode;
160
170
  }
161
171
  /**
162
- * Popover — floating panel anchored below a trigger element.
172
+ * Popover — floating panel anchored to a trigger element, below it by
173
+ * default.
163
174
  *
164
175
  * Positioning is `position: absolute` inside a `position: relative` wrapper —
165
- * no floating-ui dependency. Supports left/right alignment against the trigger edge.
176
+ * no floating-ui dependency. Supports left/right alignment against the trigger
177
+ * edge, and automatically opens on whichever side (above or below the
178
+ * trigger) has more room within the viewport.
179
+ *
180
+ * If the panel doesn't fit on *either* side as the trigger currently sits —
181
+ * common for a tall panel (e.g. a full calendar month) nested partway down a
182
+ * long scrollable mobile sheet — the trigger's scrollable ancestor is
183
+ * scrolled so the trigger sits at the top of it first, maximizing the room
184
+ * available below, then the panel opens downward against that. This mirrors
185
+ * how most mobile date pickers behave (scroll the page, not the picker) and
186
+ * avoids the panel needing its own internal scroll in the vast majority of
187
+ * cases. If it's still too tall even after that (a genuinely tiny viewport,
188
+ * or an unusually tall panel), it's capped to whatever room actually exists
189
+ * and scrolls internally as a last resort, rather than rendering past the
190
+ * edge of the viewport with no way to reach the rest of it.
191
+ *
166
192
  * Closes on outside click or Escape. Does not trap focus (it is non-modal).
167
193
  */
168
- declare function Popover({ trigger, open, onClose, align, width, 'aria-label': ariaLabel, children, }: PopoverProps): react.JSX.Element;
194
+ declare function Popover({ trigger, open, onClose, align, width, panelStyle, 'aria-label': ariaLabel, children, }: PopoverProps): react.JSX.Element;
169
195
 
170
196
  type ButtonVariant = 'primary' | 'secondary' | 'ghost' | 'destructive';
171
197
  type ButtonSize = 'sm' | 'md';
172
198
  interface ButtonProps extends ButtonHTMLAttributes<HTMLButtonElement> {
173
199
  variant?: ButtonVariant;
174
200
  size?: ButtonSize;
201
+ /** Shows a spinner and disables the button while an async action is pending. */
202
+ loading?: boolean;
175
203
  children: ReactNode;
176
204
  }
177
205
  /**
@@ -180,7 +208,7 @@ interface ButtonProps extends ButtonHTMLAttributes<HTMLButtonElement> {
180
208
  * renders in both Server and Client Components. All styling references `--sv-*`
181
209
  * tokens via CSS Modules; there are no hardcoded values.
182
210
  */
183
- declare function Button({ variant, size, type, className, children, ...rest }: ButtonProps): react.JSX.Element;
211
+ declare function Button({ variant, size, type, loading, disabled, className, children, ...rest }: ButtonProps): react.JSX.Element;
184
212
 
185
213
  type InputProps = InputHTMLAttributes<HTMLInputElement>;
186
214
  /**
@@ -199,6 +227,96 @@ type TextareaProps = TextareaHTMLAttributes<HTMLTextAreaElement>;
199
227
  */
200
228
  declare function Textarea({ className, rows, ...rest }: TextareaProps): react.JSX.Element;
201
229
 
230
+ interface CodeTextareaProps extends TextareaHTMLAttributes<HTMLTextAreaElement> {
231
+ invalid?: boolean;
232
+ }
233
+ /**
234
+ * CodeTextarea — monospace textarea for Markdown, YAML, JSON, and other
235
+ * whitespace-sensitive content. Pair with FormField for labels, hints, and
236
+ * validation text.
237
+ */
238
+ declare function CodeTextarea({ className, rows, spellCheck, wrap, invalid, 'aria-invalid': ariaInvalid, ...rest }: CodeTextareaProps): react.JSX.Element;
239
+
240
+ type StatusBadgeStatus = 'unmodified' | 'draft' | 'committed' | 'conflict' | 'pending-delete' | 'synced' | 'warning' | 'error';
241
+ interface StatusBadgeProps {
242
+ status: StatusBadgeStatus;
243
+ children?: ReactNode;
244
+ /** Accessible label when the visible text is abbreviated or contextual. */
245
+ 'aria-label'?: string;
246
+ className?: string;
247
+ }
248
+ /**
249
+ * StatusBadge — compact inline status indicator for editor lists, draft
250
+ * workflows, and sync states.
251
+ */
252
+ declare function StatusBadge({ status, children, className, 'aria-label': ariaLabel, }: StatusBadgeProps): react.JSX.Element;
253
+
254
+ interface SplitPaneProps {
255
+ primary: ReactNode;
256
+ secondary: ReactNode;
257
+ defaultPrimarySize?: number;
258
+ minPrimarySize?: number;
259
+ maxPrimarySize?: number;
260
+ resizable?: boolean;
261
+ primaryLabel?: string;
262
+ secondaryLabel?: string;
263
+ resizeLabel?: string;
264
+ className?: string;
265
+ }
266
+ /**
267
+ * SplitPane — responsive two-pane layout for editor/preview and list/detail
268
+ * workflows. Desktop panes can be resized; narrow viewports fall back to a
269
+ * stable single-column stack.
270
+ */
271
+ declare function SplitPane({ primary, secondary, defaultPrimarySize, minPrimarySize, maxPrimarySize, resizable, primaryLabel, secondaryLabel, resizeLabel, className, }: SplitPaneProps): react.JSX.Element;
272
+
273
+ interface TagInputProps {
274
+ value: string[];
275
+ onChange: (value: string[]) => void;
276
+ id?: string;
277
+ name?: string;
278
+ placeholder?: string;
279
+ disabled?: boolean;
280
+ required?: boolean;
281
+ error?: string;
282
+ hint?: string;
283
+ validateTag?: (tag: string, currentTags: string[]) => string | undefined;
284
+ separators?: RegExp;
285
+ className?: string;
286
+ 'aria-describedby'?: string;
287
+ 'aria-invalid'?: boolean;
288
+ 'aria-label'?: string;
289
+ }
290
+ /**
291
+ * TagInput — controlled multi-value input for frontmatter tags, labels, and
292
+ * lightweight taxonomies. Use inside FormField just like a native input.
293
+ */
294
+ declare function TagInput({ value, onChange, id, name, placeholder, disabled, required, error, hint, validateTag, separators, className, 'aria-describedby': ariaDescribedBy, 'aria-invalid': ariaInvalid, 'aria-label': ariaLabel, }: TagInputProps): react.JSX.Element;
295
+
296
+ /**
297
+ * Lets content deep inside a `Dialog` (e.g. a plugin's own route layout,
298
+ * rendered several levels below wherever the `Dialog` itself is
299
+ * instantiated) supply the second-row content of the Dialog's mobile
300
+ * `OverlayHeader` — typically a tab strip. Solves the "double header"
301
+ * problem: without this, a plugin's own tab strip has no way to reach the
302
+ * Dialog's header and ends up rendered a second time, as its own sticky bar,
303
+ * inside the scrolling content area.
304
+ *
305
+ * A no-op when there is no enclosing `Dialog` (e.g. the same plugin layout
306
+ * also rendered on a plain, non-overlay route) — safe to call unconditionally.
307
+ * Returns whether an enclosing `Dialog` actually received the content, so a
308
+ * caller that also renders its own inline header/tab-strip copy for the
309
+ * no-Dialog case can hide that copy on mobile specifically when this
310
+ * returned `true` (the Dialog's own header is showing it instead there).
311
+ *
312
+ * ```tsx
313
+ * function AccountLayout({ children }) {
314
+ * const insideOverlay = useOverlaySecondRow(<nav>...tab strip...</nav>);
315
+ * return <div>{children}</div>;
316
+ * }
317
+ * ```
318
+ */
319
+ declare function useOverlaySecondRow(node: ReactNode | null): boolean;
202
320
  type DialogSize = 'sm' | 'md' | 'lg' | 'full';
203
321
  interface DialogProps {
204
322
  /** Whether the dialog is shown. When false, nothing renders. */
@@ -223,29 +341,235 @@ interface DialogProps {
223
341
  * is restored to the previously-focused element on close; Tab is trapped within
224
342
  * the panel. Styling references `--sv-*` tokens only; on mobile the panel becomes
225
343
  * a full-screen sheet regardless of `size`.
344
+ *
345
+ * Animated open/close: fade + scale on desktop, slide-up on mobile (matching
346
+ * the "feels like a page push" framing of the mobile sheet). The `open`/
347
+ * `onClose` API is unchanged — closing still stays mounted internally for the
348
+ * exit transition before actually unmounting; `prefers-reduced-motion: reduce`
349
+ * collapses both to near-instant.
226
350
  */
227
351
  declare function Dialog({ open, onClose, size, 'aria-label': ariaLabel, title, children, }: DialogProps): react.JSX.Element | null;
228
352
 
229
353
  interface DrawerProps {
230
354
  /** Whether the drawer is shown. When false, nothing renders. */
231
355
  open: boolean;
232
- /** Called on Esc or scrim click. */
356
+ /** Called on Esc, scrim click, or a swipe-down past the dismiss threshold. */
233
357
  onClose: () => void;
234
358
  /** Accessible name for the drawer panel (sets `aria-label`). */
235
359
  'aria-label'?: string;
360
+ /** `'content'` (default) sizes to content, capped at 80dvh. `'half'` fixes
361
+ * the panel to 50dvh regardless of content — per the design-system's
362
+ * "half screen or less" convention for Drawer, this is the only other
363
+ * supported size; taller content should use `Sheet` instead. */
364
+ snapHeight?: 'content' | 'half';
236
365
  children: ReactNode;
237
366
  }
238
367
  /**
239
368
  * Drawer — a dismissable bottom-sheet panel. Used by the mobile shell for
240
369
  * plugin navigation; also available to plugins for any bottom-up surface.
241
370
  *
242
- * Behaviour: Esc and scrim-click dismiss; focus moves to the first focusable
243
- * element on open and is restored on close; Tab is trapped within the panel.
244
- * The panel respects `env(safe-area-inset-bottom)` so it clears the home
245
- * indicator in standalone/fullscreen mode. Shares dismissal conventions and
246
- * tokens (`--sv-color-scrim`, `--sv-shadow-overlay`) with `Dialog`.
371
+ * Behaviour: Esc, scrim click, or dragging the grab handle down past
372
+ * `SWIPE_DISMISS_THRESHOLD_PX` all dismiss; focus moves to the first
373
+ * focusable element on open and is restored on close; Tab is trapped within
374
+ * the panel. The panel respects `env(safe-area-inset-bottom)` so it clears
375
+ * the home indicator in standalone/fullscreen mode. Shares dismissal
376
+ * conventions and tokens (`--sv-color-scrim`, `--sv-shadow-overlay`) with
377
+ * `Dialog`.
378
+ *
379
+ * Animated open/close: slides up from the bottom, scrim fades. The `open`/
380
+ * `onClose` API is unchanged — closing stays mounted internally for the exit
381
+ * transition before actually unmounting; `prefers-reduced-motion: reduce`
382
+ * collapses it to near-instant.
383
+ *
384
+ * The grab handle is the *only* drag-initiation region (not the whole panel)
385
+ * — dragging from body content would fight that content's own internal
386
+ * scroll, the same reasoning behind the tasks plugin's edge-zone-only
387
+ * swipe-to-reveal technique.
388
+ */
389
+ declare function Drawer({ open, onClose, 'aria-label': ariaLabel, snapHeight, children, }: DrawerProps): react.JSX.Element | null;
390
+
391
+ interface OverlayHeaderProps {
392
+ /** Overlay title, shown between the optional back button and the trailing
393
+ * slots. Optional — some consumers (e.g. `Dialog`'s mobile bar) need the
394
+ * row's close button present even when no title applies. */
395
+ title?: string;
396
+ /** Always renders a close button; called on click. */
397
+ onClose: () => void;
398
+ /** When provided, renders a back-chevron button before the title (for
399
+ * hierarchical navigation within a single overlay — going back without
400
+ * fully closing it). Omit for a flat overlay that only ever closes. */
401
+ onBack?: () => void;
402
+ /** Trailing slot next to the close button — e.g. a "Save" action. */
403
+ action?: ReactNode;
404
+ /** A second row below the title row — e.g. a tab strip. */
405
+ secondRow?: ReactNode;
406
+ className?: string;
407
+ }
408
+ /**
409
+ * OverlayHeader — the shared fixed secondary header for `Dialog`'s mobile
410
+ * mode, `Sheet`, and `Drawer`: title + close (+ optional back button /
411
+ * trailing action / tab-strip row). Not itself `position: sticky` or
412
+ * `position: fixed` — it stays visually pinned because the consuming overlay
413
+ * renders it as a non-scrolling flex sibling before its own scrollable
414
+ * content region, the same technique `Dialog`'s `.mobileBar` already used
415
+ * before this component existed to generalize it.
416
+ *
417
+ * Content that needs a fully custom header (e.g. an editable title, or
418
+ * controls that aren't a simple title/back/action shape) should not use this
419
+ * component — render its own header row instead; `Sheet`'s `title` prop is
420
+ * optional specifically to leave that escape hatch open.
421
+ */
422
+ declare function OverlayHeader({ title, onClose, onBack, action, secondRow, className, }: OverlayHeaderProps): react.JSX.Element;
423
+
424
+ interface SheetProps {
425
+ /** Whether the sheet is shown. When false, nothing renders. */
426
+ open: boolean;
427
+ /** Called on Esc, or the built-in `OverlayHeader`'s close button. There is
428
+ * no scrim to tap — a Sheet replaces the content area it fills, not a
429
+ * layer floating above it (see the component doc comment). */
430
+ onClose: () => void;
431
+ /** Accessible name for the sheet. Defaults to `title` when omitted. */
432
+ 'aria-label'?: string;
433
+ /** Renders a built-in `OverlayHeader` (title + close, optional back button
434
+ * and trailing action). Omit when the content supplies its own header —
435
+ * e.g. a form whose title doubles as an editable field. */
436
+ title?: string;
437
+ /** Forwarded to `OverlayHeader` — see its own `onBack` doc. Has no effect
438
+ * without `title`. */
439
+ onBack?: () => void;
440
+ /** Forwarded to `OverlayHeader`'s trailing-action slot. Has no effect
441
+ * without `title`. */
442
+ headerAction?: ReactNode;
443
+ /**
444
+ * Which edge the panel slides in from. Defaults to `'bottom'` (detail
445
+ * views, forms — content the user is reaching down into). `'top'` suits a
446
+ * short options/actions menu opened from a header button near the top of
447
+ * the screen, where sliding up from the bottom would travel past empty
448
+ * space before reaching content that visually belongs near the trigger.
449
+ */
450
+ slideFrom?: 'bottom' | 'top';
451
+ children: ReactNode;
452
+ }
453
+ /**
454
+ * Sheet — a full-page overlay that replaces a plugin's content area between
455
+ * the shell's fixed header and footer (RFC 0013's mobile chrome stays visible
456
+ * and interactive around it), sliding in from an edge instead of centering
457
+ * like `Dialog`. Promoted from the tasks plugin's `MobileFullPageOverlay`
458
+ * (now the reference pattern for this component). Desktop has no equivalent
459
+ * presentation — a desktop layout shows the same content inline (columns,
460
+ * panes) instead of mounting a `Sheet` at all; this component is for the
461
+ * mobile case specifically.
462
+ *
463
+ * Unlike `Dialog`/`Drawer` there is no scrim: a Sheet visually *replaces* the
464
+ * region it covers rather than floating a layer above it, so there is no
465
+ * "outside" to tap-dismiss. Behaviour: Esc dismisses; focus moves into the
466
+ * panel on open and is restored on close; Tab is trapped within the panel;
467
+ * animated open/close via the same two-phase mount as `Dialog`/`Drawer`,
468
+ * respecting `prefers-reduced-motion`.
247
469
  */
248
- declare function Drawer({ open, onClose, 'aria-label': ariaLabel, children }: DrawerProps): react.JSX.Element | null;
470
+ declare function Sheet({ open, onClose, 'aria-label': ariaLabel, title, onBack, headerAction, slideFrom, children, }: SheetProps): react.JSX.Element | null;
471
+
472
+ interface ConfirmDialogProps {
473
+ /** Whether the dialog is shown. */
474
+ open: boolean;
475
+ /** Called on Esc, backdrop click, or the Cancel button. */
476
+ onClose: () => void;
477
+ title: string;
478
+ /** Accepts rich content (e.g. `Remove <strong>{name}</strong>?`), not just
479
+ * plain text. */
480
+ message: ReactNode;
481
+ /** Called when the confirm action is clicked. Does not close the dialog
482
+ * itself — pass `pending`/close it via `onClose` from the caller once the
483
+ * action settles, so an async action that fails can keep the dialog open
484
+ * to show `error`. */
485
+ onConfirm: () => void;
486
+ confirmLabel?: string;
487
+ cancelLabel?: string;
488
+ /** Styles the confirm action as a destructive (red) action. */
489
+ destructive?: boolean;
490
+ /** Disables both actions — pair with a `confirmLabel` reflecting the
491
+ * in-flight state (e.g. "Removing…") for an `onConfirm` that triggers an
492
+ * async action. */
493
+ pending?: boolean;
494
+ /** Optional inline error, shown between the message and the actions — e.g.
495
+ * a failed async `onConfirm` that keeps the dialog open to report it. */
496
+ error?: ReactNode;
497
+ }
498
+ /**
499
+ * ConfirmDialog — a small, content-sized confirm/cancel prompt. Deliberately
500
+ * NOT built on `Dialog`, which is a fixed-size box by design (so switching
501
+ * views inside it never resizes the panel) — a confirm prompt is the opposite
502
+ * case, sized to its message. Built on the native `<dialog>` element instead,
503
+ * same as the pattern this component replaces (duplicated across the account
504
+ * and console plugins): top-layer rendering and `::backdrop` come from the
505
+ * browser for free, which is what makes a confirm opened *from inside*
506
+ * another overlay (Dialog, Sheet) reliably stack above it without a manual
507
+ * z-index scheme.
508
+ *
509
+ * Same presentation on desktop and mobile — a small centered card, not a
510
+ * full-screen sheet (unlike Dialog/Sheet, which do fork by viewport).
511
+ */
512
+ declare function ConfirmDialog({ open, onClose, title, message, onConfirm, confirmLabel, cancelLabel, destructive, pending, error, }: ConfirmDialogProps): react.JSX.Element;
513
+
514
+ interface MenuItem {
515
+ type?: 'item';
516
+ label: string;
517
+ onSelect: () => void;
518
+ icon?: IconName;
519
+ /** Styles the item as a destructive action (e.g. "Delete list"). */
520
+ destructive?: boolean;
521
+ disabled?: boolean;
522
+ /**
523
+ * Marks the item as one of a mutually-exclusive set (e.g. the active sort
524
+ * order) — renders a leading checkmark and `role="menuitemradio"` instead
525
+ * of `role="menuitem"`. Pass this on every item in the group, including
526
+ * the unchecked ones, so their labels stay aligned with the checked one's
527
+ * reserved checkmark gutter. Omit entirely for plain action items (e.g.
528
+ * "Delete list") — they render with no reserved leading space at all.
529
+ */
530
+ checked?: boolean;
531
+ }
532
+ /** A non-interactive section heading above a run of items (e.g. "Sort by"). */
533
+ interface MenuLabel {
534
+ type: 'label';
535
+ label: string;
536
+ }
537
+ /** A visual divider between sections. */
538
+ interface MenuSeparator {
539
+ type: 'separator';
540
+ }
541
+ type MenuEntry = MenuItem | MenuLabel | MenuSeparator;
542
+ interface MenuProps {
543
+ /** The element that opens the menu — rendered as-is; the caller wires its
544
+ * onClick to flip `open` (same controlled pattern as `Popover`). */
545
+ trigger: ReactElement;
546
+ open: boolean;
547
+ onClose: () => void;
548
+ items: MenuEntry[];
549
+ 'aria-label': string;
550
+ /** Forwarded to `Popover` on desktop. Has no effect on mobile, where the
551
+ * menu is always a full-width `Drawer`. */
552
+ align?: 'left' | 'right';
553
+ }
554
+ /**
555
+ * Menu — an adaptive action menu: `Popover` on desktop, a bottom-sheet
556
+ * `Drawer` on mobile. Replaces the desktop-Popover/mobile-Drawer fork that
557
+ * `⋯` action menus (list options, row actions) otherwise re-derive per
558
+ * plugin. Same list of `items` renders in both presentations — only the
559
+ * surrounding chrome (floating panel vs. bottom sheet) differs, matching the
560
+ * platform's `useIsMobile` breakpoint.
561
+ *
562
+ * `items` accepts three entry shapes: a plain action (`MenuItem`, `type`
563
+ * omitted or `'item'`), a non-interactive section heading (`MenuLabel`,
564
+ * `type: 'label'`), and a `MenuSeparator` (`type: 'separator'`) — enough to
565
+ * express grouped sections like "Filter" / "Sort by" above a run of
566
+ * destructive actions, matching how native OS menus lay out mixed
567
+ * action/selection groups.
568
+ *
569
+ * Selecting an item both closes the menu and calls its `onSelect` — a
570
+ * consumer's `onSelect` never needs to call `onClose` itself.
571
+ */
572
+ declare function Menu({ trigger, open, onClose, items, 'aria-label': ariaLabel, align }: MenuProps): react.JSX.Element;
249
573
 
250
574
  interface ToastItem {
251
575
  id: string;
@@ -377,4 +701,156 @@ interface DragHandleRowProps extends HTMLAttributes<HTMLDivElement> {
377
701
  */
378
702
  declare function DragHandleRow({ children, handleProps, isDragging, className, ...rest }: DragHandleRowProps): react.JSX.Element;
379
703
 
380
- export { Avatar, type AvatarProps, Badge, type BadgeProps, type BadgeStatus, type BadgeVariant, type BannerVariant, Button, type ButtonProps, type ButtonSize, type ButtonVariant, Card, type CardProps, Checkbox, type CheckboxProps, Dialog, type DialogProps, type DialogSize, DragHandleRow, type DragHandleRowProps, Drawer, type DrawerProps, EmptyState, type EmptyStateProps, FormField, type FormFieldProps, type FormFieldRenderProps, Icon, type IconName, type IconProps, Input, type InputProps, type NavTabItem, NavTabs, type NavTabsProps, PageHeader, type PageHeaderProps, Popover, type PopoverProps, SegmentedControl, type SegmentedControlProps, type SegmentedOption, Select, type SelectProps, Spinner, type SpinnerProps, SystemBanner, type SystemBannerProps, type TabItem, Tabs, type TabsProps, Textarea, type TextareaProps, type ToastContextValue, type ToastItem, ToastProvider, Toggle, type ToggleProps, Tooltip, type TooltipProps, useToast };
704
+ interface CalendarProps {
705
+ /** Selected date, or `null` for no selection. */
706
+ value: Date | null;
707
+ onChange: (date: Date) => void;
708
+ /** Dates before this are disabled and unselectable. */
709
+ minDate?: Date;
710
+ /** Dates after this are disabled and unselectable. */
711
+ maxDate?: Date;
712
+ 'aria-label'?: string;
713
+ }
714
+ /**
715
+ * Calendar — a keyboard-navigable month grid. Date-only (no time or range
716
+ * selection — decision D6 in the mobile design-system plan); a `DatePicker`
717
+ * field wraps this in a `Popover` (desktop) or `Drawer` (mobile).
718
+ *
719
+ * Keyboard (WAI-ARIA APG grid pattern, roving tabindex — exactly one day
720
+ * button is ever tab-stoppable): arrow keys move focus by day/week, Home/End
721
+ * jump to the start/end of the focused week, PageUp/PageDown change month,
722
+ * Enter/Space selects the focused date. Navigating into an adjacent month
723
+ * (arrow keys at a week's edge, or PageUp/PageDown) updates the displayed
724
+ * month to follow focus.
725
+ */
726
+ declare function Calendar({ value, onChange, minDate, maxDate, 'aria-label': ariaLabel, }: CalendarProps): react.JSX.Element;
727
+
728
+ interface DatePickerProps {
729
+ value: Date | null;
730
+ onChange: (date: Date) => void;
731
+ minDate?: Date;
732
+ maxDate?: Date;
733
+ placeholder?: string;
734
+ 'aria-label': string;
735
+ disabled?: boolean;
736
+ }
737
+ /**
738
+ * DatePicker — a form field pairing a compact trigger with `Calendar`:
739
+ * `Popover` on desktop, a bottom-sheet `Drawer` on mobile (the platform's
740
+ * standard adaptive-surface pattern, matching `Menu`). Date-only (decision
741
+ * D6 in the mobile design-system plan) — time and range selection are future
742
+ * scope; recurrence UI stays plugin-side.
743
+ *
744
+ * Unlike `Menu`, the trigger is built in rather than supplied by the caller
745
+ * — a date picker is a form field (consistent with `Input`/`Select`), not an
746
+ * arbitrary action-menu trigger.
747
+ */
748
+ declare function DatePicker({ value, onChange, minDate, maxDate, placeholder, 'aria-label': ariaLabel, disabled, }: DatePickerProps): react.JSX.Element;
749
+
750
+ interface UseLongPressOptions {
751
+ /** Called once the press has been held for `delay` without exceeding `moveTolerance`. */
752
+ onLongPress: () => void;
753
+ /** Hold duration in ms before `onLongPress` fires. Default 500. */
754
+ delay?: number;
755
+ /** Accumulated pointer movement (px) that cancels a pending press. Default 10 —
756
+ * a real finger jitters a couple of px on a still hold; this must be forgiving
757
+ * of that or the gesture never fires. */
758
+ moveTolerance?: number;
759
+ /** How long (ms) after firing to swallow the click that may or may not follow.
760
+ * Default 700. Time-boxed rather than "clear on next click" — iOS frequently
761
+ * sends no click at all after a long hold, so a flag that only clears on the
762
+ * next click stays armed forever and silently eats the user's next real tap. */
763
+ suppressClickMs?: number;
764
+ /** Fire `navigator.vibrate(10)` when the press triggers, where supported. Default true. */
765
+ vibrate?: boolean;
766
+ /** Skip entirely (hooks can't be called conditionally, so this is the escape hatch
767
+ * for a caller whose long-press action isn't available in the current state). */
768
+ disabled?: boolean;
769
+ }
770
+ interface LongPressHandlers {
771
+ onPointerDown: (e: PointerEvent) => void;
772
+ onPointerMove: (e: PointerEvent) => void;
773
+ onPointerUp: (e: PointerEvent) => void;
774
+ onPointerCancel: (e: PointerEvent) => void;
775
+ onPointerLeave: (e: PointerEvent) => void;
776
+ onContextMenu: (e: MouseEvent) => void;
777
+ onClick: (e: MouseEvent) => void;
778
+ /** Spread onto the target element. Empty on devices whose primary pointer
779
+ * isn't coarse (desktop mouse/trackpad) — see the coarse-pointer gate below. */
780
+ style: CSSProperties;
781
+ }
782
+ /**
783
+ * useLongPress — touch-and-hold gesture with the full suppression recipe a
784
+ * long-press needs to behave, not just a bare timer.
785
+ *
786
+ * A naive `setTimeout` started on `pointerdown` and cleared on `pointermove`
787
+ * fails in three specific ways this hook exists to fix:
788
+ * 1. Any movement at all cancels it — but a real finger jitters a few px even
789
+ * on a "still" hold, so the gesture misfires constantly. `moveTolerance`
790
+ * requires deliberate movement before cancelling.
791
+ * 2. `pointercancel` (the browser converting the touch into a scroll) isn't
792
+ * handled — the timer survives and fires mid-scroll. Handled here.
793
+ * 3. Nothing suppresses the OS's own reaction to a long touch-hold: iOS shows
794
+ * its link-preview callout, Android starts text selection or opens a
795
+ * context menu, and either can fire *alongside* the gesture this hook
796
+ * triggers. `onContextMenu` + the returned `style` (touch-callout,
797
+ * user-select, touch-action) close all three, gated to coarse-pointer
798
+ * devices only — unconditionally disabling `user-select` would break
799
+ * mouse text selection on desktop, which never held this long-press to
800
+ * begin with (see `onPointerDown`'s own `pointerType` check).
801
+ *
802
+ * The click that may or may not follow a fired long-press (iOS often sends
803
+ * none at all) is swallowed for `suppressClickMs`, not indefinitely — an
804
+ * unbounded suppression flag left armed by a browser that never sends that
805
+ * click silently eats the user's *next* unrelated tap on the same element.
806
+ */
807
+ declare function useLongPress({ onLongPress, delay, moveTolerance, suppressClickMs, vibrate, disabled, }: UseLongPressOptions): LongPressHandlers;
808
+
809
+ /**
810
+ * useDoubleTapHandler — fires `onDoubleTap` on a real double-click (desktop)
811
+ * or two taps within `DOUBLE_TAP_MS` (touch).
812
+ *
813
+ * Only safe when the *single* tap/click has no default action of its own to
814
+ * preempt — e.g. a colour swatch with nothing to cancel. If the single tap
815
+ * navigates, opens something, or otherwise does something a following double
816
+ * needs to be able to undo before it happens, use `useSingleOrDoubleTap`
817
+ * instead: this hook fires `onDoubleTap` only, leaving any single-tap handling
818
+ * to the caller's own `onClick`, which already ran immediately and can't be
819
+ * un-done here.
820
+ *
821
+ * The event is passed through to `onDoubleTap` rather than calling
822
+ * `preventDefault` internally — not every call site needs to cancel
823
+ * something, so the decision is left to the caller.
824
+ */
825
+ declare function useDoubleTapHandler<E extends {
826
+ detail: number;
827
+ }>(onDoubleTap: (e: E) => void): (e: E) => void;
828
+ /**
829
+ * useSingleOrDoubleTap — for call sites where the single tap/click *does*
830
+ * have a default action (e.g. navigating) that a following double-tap must be
831
+ * able to preempt.
832
+ *
833
+ * A real double-click gets away with firing the single action immediately and
834
+ * only detecting the double afterward, because `e.detail === 2` is the
835
+ * browser's own resolved signal, arriving on the very click that matters. A
836
+ * touch double-tap has no equivalent "hold on, there might be a second one"
837
+ * signal — the only way to know is to wait out the window before committing
838
+ * to the single action, which is what this hook does: `onSingle` is deferred
839
+ * by `DOUBLE_TAP_MS` and only actually runs if no second tap arrives in that
840
+ * time. This means every single tap through this hook incurs that latency —
841
+ * only use it where a genuine double-tap gesture must be able to preempt the
842
+ * single action; `useDoubleTapHandler` above has no such delay.
843
+ */
844
+ declare function useSingleOrDoubleTap<E extends {
845
+ detail: number;
846
+ }>(onSingle: (e: E) => void, onDouble: (e: E) => void): (e: E) => void;
847
+
848
+ declare const MOBILE_BREAKPOINT_PX = 768;
849
+ /**
850
+ * useIsMobile — whether the viewport is at or below the platform's mobile
851
+ * breakpoint. SSR-safe: defaults to `false` (desktop) until the client mounts
852
+ * and reads the real viewport, avoiding a hydration mismatch on first paint.
853
+ */
854
+ declare function useIsMobile(breakpointPx?: number): boolean;
855
+
856
+ export { Avatar, type AvatarProps, Badge, type BadgeProps, type BadgeStatus, type BadgeVariant, type BannerVariant, Button, type ButtonProps, type ButtonSize, type ButtonVariant, Calendar, type CalendarProps, Card, type CardProps, Checkbox, type CheckboxProps, CodeTextarea, type CodeTextareaProps, ConfirmDialog, type ConfirmDialogProps, DatePicker, type DatePickerProps, Dialog, type DialogProps, type DialogSize, DragHandleRow, type DragHandleRowProps, Drawer, type DrawerProps, EmptyState, type EmptyStateProps, FormField, type FormFieldProps, type FormFieldRenderProps, Icon, type IconName, type IconProps, Input, type InputProps, type LongPressHandlers, MOBILE_BREAKPOINT_PX, Menu, type MenuEntry, type MenuItem, type MenuLabel, type MenuProps, type MenuSeparator, type NavTabItem, NavTabs, type NavTabsProps, OverlayHeader, type OverlayHeaderProps, PageHeader, type PageHeaderProps, Popover, type PopoverProps, SegmentedControl, type SegmentedControlProps, type SegmentedOption, Select, type SelectProps, Sheet, type SheetProps, Spinner, type SpinnerProps, SplitPane, type SplitPaneProps, StatusBadge, type StatusBadgeProps, type StatusBadgeStatus, SystemBanner, type SystemBannerProps, type TabItem, Tabs, type TabsProps, TagInput, type TagInputProps, Textarea, type TextareaProps, type ToastContextValue, type ToastItem, ToastProvider, Toggle, type ToggleProps, Tooltip, type TooltipProps, type UseLongPressOptions, useDoubleTapHandler, useIsMobile, useLongPress, useOverlaySecondRow, useSingleOrDoubleTap, useToast };