rift-ds 1.13.0 → 1.14.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 (173) hide show
  1. package/README.md +13 -5
  2. package/behaviors/ScrollRegion.d.ts +8 -0
  3. package/behaviors/ScrollRegion.js +51 -0
  4. package/behaviors/devWarning.d.ts +9 -0
  5. package/behaviors/devWarning.js +16 -0
  6. package/behaviors/useAnchoredPosition.d.ts +111 -0
  7. package/behaviors/useAnchoredPosition.js +153 -0
  8. package/behaviors/useFocusScope.d.ts +9 -2
  9. package/behaviors/useFocusScope.js +5 -1
  10. package/bin/rift-ds.mjs +181 -25
  11. package/bin/templates/app/components/rift/ColorModeScript.tsx +10 -0
  12. package/bin/templates/app/components/rift/RiftRoot.tsx +13 -0
  13. package/bin/templates/app/components/rift/font.css +7 -0
  14. package/bin/templates/app/components/rift/font.ts +14 -0
  15. package/bin/templates/blocks/app/dashboard/page.module.css +90 -0
  16. package/bin/templates/blocks/app/dashboard/page.tsx +141 -0
  17. package/bin/templates/blocks/app/sign-in/page.module.css +91 -0
  18. package/bin/templates/blocks/app/sign-in/page.tsx +95 -0
  19. package/bin/templates/blocks/app/starter/page.module.css +7 -0
  20. package/bin/templates/blocks/app/starter/page.tsx +31 -0
  21. package/bin/templates/blocks/components/blocks/AppShell.module.css +160 -0
  22. package/bin/templates/blocks/components/blocks/AppShell.tsx +113 -0
  23. package/bin/templates/blocks/registry.json +39 -0
  24. package/chat/AssistantTurn.js +4 -2
  25. package/chat/types.d.ts +8 -7
  26. package/chat/useChat.js +2 -2
  27. package/components/AgentStatus/AgentRing.d.ts +27 -0
  28. package/components/AgentStatus/AgentRing.js +9 -0
  29. package/components/AgentStatus/AgentStatus.css +637 -9
  30. package/components/AgentStatus/AgentStatus.d.ts +31 -3
  31. package/components/AgentStatus/AgentStatus.js +22 -4
  32. package/components/AgentStatus/AgentStatusPatterns.d.ts +1 -1
  33. package/components/AgentStatus/AgentStatusPatterns.js +2 -1
  34. package/components/AiButton/AiButton.css +211 -38
  35. package/components/AiButton/AiButton.d.ts +34 -0
  36. package/components/AiButton/AiButton.js +129 -9
  37. package/components/Avatar/Avatar.css +54 -0
  38. package/components/Avatar/Avatar.d.ts +9 -1
  39. package/components/Avatar/Avatar.js +5 -1
  40. package/components/Avatar/avatarFill.d.ts +9 -0
  41. package/components/Avatar/avatarFill.js +13 -0
  42. package/components/AvatarGroup/AvatarGroup.js +13 -1
  43. package/components/BottomNav/BottomNav.css +2 -2
  44. package/components/Button/Button.css +7 -0
  45. package/components/Button/Button.d.ts +8 -1
  46. package/components/Button/Button.js +10 -1
  47. package/components/Checkbox/Checkbox.js +1 -1
  48. package/components/CircularButton/CircularButton.css +8 -7
  49. package/components/CodeBlock/CodeBlock.d.ts +2 -0
  50. package/components/CodeBlock/CodeBlock.js +2 -0
  51. package/components/ColorPicker/ColorPicker.css +3 -4
  52. package/components/ColorPicker/ColorPicker.js +11 -0
  53. package/components/Combobox/Combobox.css +20 -5
  54. package/components/Combobox/Combobox.js +75 -55
  55. package/components/CommandPaletteTrigger/CommandPaletteTrigger.css +88 -0
  56. package/components/CommandPaletteTrigger/CommandPaletteTrigger.d.ts +31 -0
  57. package/components/CommandPaletteTrigger/CommandPaletteTrigger.js +49 -0
  58. package/components/Composer/Composer.css +3 -3
  59. package/components/DataTable/CellEditor.d.ts +37 -0
  60. package/components/DataTable/CellEditor.js +153 -0
  61. package/components/DataTable/DataTable.css +1180 -0
  62. package/components/DataTable/DataTable.d.ts +498 -27
  63. package/components/DataTable/DataTable.js +2363 -189
  64. package/components/DataTable/FilterEditor.d.ts +10 -0
  65. package/components/DataTable/FilterEditor.js +181 -0
  66. package/components/DataTable/TypedCell.d.ts +22 -0
  67. package/components/DataTable/TypedCell.js +80 -0
  68. package/components/DataTable/VirtualRows.d.ts +21 -0
  69. package/components/DataTable/VirtualRows.js +38 -0
  70. package/components/DataTable/aggregates.d.ts +29 -0
  71. package/components/DataTable/aggregates.js +52 -0
  72. package/components/DataTable/columnTypes.d.ts +49 -0
  73. package/components/DataTable/columnTypes.js +96 -0
  74. package/components/DataTable/dragGesture.d.ts +24 -0
  75. package/components/DataTable/dragGesture.js +62 -0
  76. package/components/DataTable/editorKinds.d.ts +24 -0
  77. package/components/DataTable/editorKinds.js +20 -0
  78. package/components/DataTable/filterModel.d.ts +84 -0
  79. package/components/DataTable/filterModel.js +159 -0
  80. package/components/DataTable/tableContext.d.ts +82 -0
  81. package/components/DataTable/tableContext.js +7 -0
  82. package/components/DataTable/views.d.ts +50 -0
  83. package/components/DataTable/views.js +39 -0
  84. package/components/DateInput/DateInput.css +6 -0
  85. package/components/Drawer/Drawer.js +2 -1
  86. package/components/Dropdown/Dropdown.css +22 -6
  87. package/components/Dropdown/Dropdown.d.ts +6 -0
  88. package/components/Dropdown/Dropdown.js +37 -7
  89. package/components/DropdownMenu/DropdownMenu.css +30 -12
  90. package/components/DropdownMenu/DropdownMenu.d.ts +4 -0
  91. package/components/DropdownMenu/DropdownMenu.js +65 -14
  92. package/components/FilterBar/FilterBar.css +10 -6
  93. package/components/FilterBar/FilterBar.js +68 -44
  94. package/components/HoverCard/HoverCard.css +24 -44
  95. package/components/HoverCard/HoverCard.js +18 -2
  96. package/components/Input/Input.css +6 -0
  97. package/components/InterruptCard/InterruptCard.css +8 -0
  98. package/components/InterruptCard/InterruptCard.d.ts +2 -0
  99. package/components/InterruptCard/InterruptCard.js +7 -1
  100. package/components/ModelPicker/ModelPicker.css +14 -11
  101. package/components/ModelPicker/ModelPicker.d.ts +2 -0
  102. package/components/ModelPicker/ModelPicker.js +15 -1
  103. package/components/Pagination/Pagination.css +8 -6
  104. package/components/PixelAvatar/pixelInks.d.ts +6 -0
  105. package/components/PixelAvatar/pixelInks.js +7 -3
  106. package/components/Popover/Popover.css +32 -30
  107. package/components/Popover/Popover.d.ts +8 -0
  108. package/components/Popover/Popover.js +16 -2
  109. package/components/Prose/Prose.css +47 -8
  110. package/components/Prose/Prose.d.ts +2 -0
  111. package/components/Prose/Prose.js +7 -2
  112. package/components/RichDropdown/RichDropdown.css +17 -5
  113. package/components/RichDropdown/RichDropdown.js +13 -0
  114. package/components/SegmentedControl/SegmentedControl.css +8 -0
  115. package/components/ShaderField/useShaderField.js +13 -1
  116. package/components/SourceChip/SourceChip.css +6 -12
  117. package/components/SourceChip/SourceChip.js +10 -35
  118. package/components/Table/Table.css +61 -19
  119. package/components/Table/Table.d.ts +8 -0
  120. package/components/Table/Table.js +31 -11
  121. package/components/TableToolbar/TableToolbar.css +494 -0
  122. package/components/TableToolbar/TableToolbar.d.ts +281 -0
  123. package/components/TableToolbar/TableToolbar.js +1321 -0
  124. package/components/ThemePicker/ThemePicker.css +189 -0
  125. package/components/ThemePicker/ThemePicker.d.ts +67 -0
  126. package/components/ThemePicker/ThemePicker.js +165 -0
  127. package/components/ThemeToggle/ThemeToggle.css +82 -0
  128. package/components/ThemeToggle/ThemeToggle.d.ts +42 -0
  129. package/components/ThemeToggle/ThemeToggle.js +98 -0
  130. package/components/ThemeToggle/colorMode.d.ts +42 -0
  131. package/components/ThemeToggle/colorMode.js +26 -0
  132. package/components/ThemeToggle/useColorMode.d.ts +8 -0
  133. package/components/ThemeToggle/useColorMode.js +44 -0
  134. package/components/TimePicker/TimePicker.css +11 -5
  135. package/components/TimePicker/TimePicker.js +11 -0
  136. package/components/ToggleGroup/ToggleGroup.css +10 -0
  137. package/components/Tooltip/Tooltip.css +24 -25
  138. package/components/Tooltip/Tooltip.js +16 -60
  139. package/components/TopAppBar/TopAppBar.css +5 -3
  140. package/components/registry.json +36 -4
  141. package/components/registry.json.d.ts +36 -4
  142. package/components/registry.json.js +1 -1
  143. package/index.d.ts +11 -0
  144. package/index.js +31 -2
  145. package/package.json +5 -1
  146. package/tokens/base.css +84 -0
  147. package/tokens/motion.d.ts +2 -0
  148. package/tokens/motion.js +2 -0
  149. package/tokens/presets/contrast.css +6 -2
  150. package/tokens/presets/coral.css +4 -2
  151. package/tokens/presets/forest.css +6 -0
  152. package/tokens/presets/gold.css +10 -2
  153. package/tokens/presets/mono.css +4 -2
  154. package/tokens/presets/pink.css +10 -0
  155. package/tokens/presets/terminal.css +8 -2
  156. package/tokens/presets/violet.css +10 -0
  157. package/tokens/presets/warm.css +4 -0
  158. package/tokens/presets/zest.css +6 -0
  159. package/tokens/registry.d.ts +1 -1
  160. package/tokens/registry.json +27 -1
  161. package/tokens/registry.json.d.ts +27 -1
  162. package/tokens/registry.json.js +1 -1
  163. package/tokens/tokens-dark.css +36 -0
  164. package/tokens/tokens-forced-colors.css +31 -0
  165. package/tokens/tokens-light.css +49 -0
  166. package/tokens/tokens-primitives.css +9 -0
  167. package/tokens/tokens.css +1 -0
  168. package/cli/templates/chat/app/api/chat/feedback/route.d.ts +0 -3
  169. package/cli/templates/chat/app/api/chat/followups/route.d.ts +0 -3
  170. package/cli/templates/chat/app/api/chat/guardrails.d.ts +0 -10
  171. package/cli/templates/chat/app/api/chat/route.d.ts +0 -4
  172. package/cli/templates/chat/components/chat/ChatMount.d.ts +0 -1
  173. package/cli/templates/chat/components/chat/models.d.ts +0 -11
package/README.md CHANGED
@@ -13,7 +13,7 @@ An open source React design system built for AI products and coding agents: comp
13
13
 
14
14
  ## Why Rift DS
15
15
 
16
- - **<!-- component-count -->155<!-- /component-count --> components.** Buttons, forms and tables through to charts, navigation and overlays, each typed and documented with live examples. Every one forwards a ref and your own attributes to its element, state you can own works controlled or uncontrolled (a modal opens when your app says so, by design), and every word a component writes has a prop you can localise. The build checks the first two on every component, and the words wherever they sit in markup. React and React DOM are the only required peer dependencies.
16
+ - **<!-- component-count -->159<!-- /component-count --> components.** Buttons, forms and tables through to charts, navigation and overlays, each typed and documented with live examples. Every one forwards a ref and your own attributes to its element, state you can own works controlled or uncontrolled (a modal opens when your app says so, by design), and every word a component writes has a prop you can localise. The build checks the first two on every component, and the words wherever they sit in markup. React and React DOM are the only required peer dependencies.
17
17
  - **AI patterns, the whole chat and templates, ready to use.** Composer, streaming chat thread, tool calls, reasoning and thread panel are in the box, the complete AI chat ships assembled as one widget that one command scaffolds, and the docs site has complete template screens built from them.
18
18
  - **Fully themeable.** The shipped themes are complete looks to start from, each in light and dark. One `data-brand` attribute applies a theme, and a few CSS overrides make your own.
19
19
  - **Ready for coding agents.** One command installs the agent skills, and a public MCP endpoint serves every component's props, so your agent builds with the library as it actually is. No key, no account.
@@ -25,18 +25,25 @@ Everything deep lives on the docs site: **[rift-ds.com](https://rift-ds.com/)**,
25
25
 
26
26
  ## Install
27
27
 
28
+ In a Next.js app router project (a fresh `create-next-app` included), three commands give you a themed first page:
29
+
28
30
  ```bash
29
31
  npm install rift-ds
32
+ npx rift-ds init app
33
+ npx rift-ds init block dashboard
30
34
  ```
31
35
 
32
- Import the token stylesheet once, then use components:
36
+ `init app` adds a root wrapper that routes every link through Next's `Link`, the script that applies the visitor's colour mode before first paint, and the base theme's font, then prints your root layout: make `app/layout.tsx` that, and drop the `globals.css` import `create-next-app` added. `init block dashboard` adds a dashboard inside an app shell (the sidebar, the top bar and the page header), laid out for a desktop and a phone; the other blocks are on the [blocks page](https://rift-ds.com/docs/blocks). Neither overwrites a file, and the files are yours to change.
37
+
38
+ In any other React app, import the token stylesheet and the base sheet once, then use components:
33
39
 
34
40
  ```tsx
35
41
  import 'rift-ds/tokens/tokens.css';
42
+ import 'rift-ds/tokens/base.css'; // the reset and page every component is tuned against
36
43
  import { Button, Card, Badge } from 'rift-ds';
37
44
  ```
38
45
 
39
- React 19+ is a peer dependency. The package is ESM-only and resolved via `exports` subpaths: use a bundler that handles CSS and font imports from `node_modules` (Vite, Next.js, webpack) and set TypeScript's `moduleResolution` to `"bundler"` or `"nodenext"`. The Recharts-backed charts live behind `rift-ds/charts`, so the optional `recharts` peer dependency is only needed if you use them. The chat widget's subpaths bring their own optional peers (`react-markdown` and `remark-gfm` for the panel, `@anthropic-ai/sdk` for the server handlers, `@upstash/ratelimit` and `@upstash/redis` for the guardrails), needed only if you use them. Prefer owning the source? Pull single components through the shadcn CLI (`npx shadcn@latest add https://rift-ds.com/r/button.json`, with details in the [get-started guide](https://rift-ds.com/docs/get-started)), or clone this repo and build on it directly; all of it is MIT.
46
+ React 19+ is a peer dependency. The package's own runtime dependencies, TanStack Table and TanStack Virtual (the headless engine under `DataTable` and the windowing behind its long tables), install with it. The package is ESM-only and resolved via `exports` subpaths: use a bundler that handles CSS and font imports from `node_modules` (Vite, Next.js, webpack) and set TypeScript's `moduleResolution` to `"bundler"` or `"nodenext"`. The Recharts-backed charts live behind `rift-ds/charts`, so the optional `recharts` peer dependency is only needed if you use them. The chat widget's subpaths bring their own optional peers (`react-markdown` and `remark-gfm` for the panel, `@anthropic-ai/sdk` for the server handlers, `@upstash/ratelimit` and `@upstash/redis` for the guardrails), needed only if you use them. Prefer owning the source? Pull single components through the shadcn CLI (`npx shadcn@latest add https://rift-ds.com/r/button.json`, with details in the [get-started guide](https://rift-ds.com/docs/get-started)), or clone this repo and build on it directly; all of it is MIT.
40
47
 
41
48
  ## The chat widget
42
49
 
@@ -100,13 +107,13 @@ import 'rift-ds/tokens/presets/presets.css';
100
107
 
101
108
  ## Components
102
109
 
103
- <!-- component-count -->155<!-- /component-count --> components, including a chat set for AI products (Chat thread, Composer, Tool call, Reasoning, Thread panel) and Shader field, a WebGL2 ambient background whose light sources read your colour tokens at runtime, so it re-themes with everything else. Details and live examples are on the [docs site](https://rift-ds.com/components).
110
+ <!-- component-count -->159<!-- /component-count --> components, including a chat set for AI products (Chat thread, Composer, Tool call, Reasoning, Thread panel) and Shader field, a WebGL2 ambient background whose light sources read your colour tokens at runtime, so it re-themes with everything else. Details and live examples are on the [docs site](https://rift-ds.com/components).
104
111
 
105
112
  <details>
106
113
  <summary>The full list</summary>
107
114
 
108
115
  <!-- component-list:start -->
109
- Accordion · Agent plan · Agent rail · Agent status · AI button · Alert · Alert dialog · Anchor nav · Animated number · App layout · App sidebar · Area chart · Artifact card · Artifact panel · Attachment drop zone · Attachment group · Attachment tile · Attachment viewer · Avatar · Avatar group · Badge · Banner · Bar chart · Bento tile · Bento tile art · Bottom nav · Breadcrumb · Button · Button group · Card · Card stack · Carousel · Chat header · Chat marker · Chat message · Chat thread · Checkbox · Chip · Circular button · Code block · Code diff · Colour picker · Combo chart · Combobox · Command palette · Composer · Contact card · Context menu · Contribution graph · Data table · Date input · Date picker · Dialog · Divider · Document chip · Drawer · Dropdown · Dropdown menu · Empty state · Entity card · Event calendar · Field · Figure · File input · Filter bar · Funnel chart · Gantt chart · Gauge · Globe · Hover card · Image compare · Input · Inspector colour picker · Inspector dropdown · Inspector input · Inspector section · Inspector segments · Inspector slider · Inspector toggle switch · Instructions · Interrupt card · Kbd · Legend tile · Lightbox · Line chart · Link list · Map callout · Map legend · Mention · Message actions · Message card · Meter · Model picker · Nav · Nav list · Notification centre · Number input · Pagination · Panel · Pie chart · Pin input · Pixel avatar · Popover · Progress bar · Prompt suggestions · Prose · Question card · Quote · Radar chart · Radial chart · Radio button · Rating · Reasoning · Rich dropdown · Scatter chart · Section title · Segmented control · Selection card · Shader field · Sidebar panel · Skeleton · Slider · Source chip · Source trail · Sparkline · Spinner · Split button · Split pane · Stacked bar chart · Stat · Status dot · Stepper · Streaming text · Swatch · Table · Tabs · Tag input · Textarea · Thread panel · Thread tabs · Time picker · Timeline · Toast · Toggle group · Toggle switch · Tool call · Toolbar · Tooltip · Top app bar · Tree view · Treemap · Usage card · Waveform · Widget · World map
116
+ Accordion · Agent plan · Agent rail · Agent status · AI button · Alert · Alert dialog · Anchor nav · Animated number · App layout · App sidebar · Area chart · Artifact card · Artifact panel · Attachment drop zone · Attachment group · Attachment tile · Attachment viewer · Avatar · Avatar group · Badge · Banner · Bar chart · Bento tile · Bento tile art · Bottom nav · Breadcrumb · Button · Button group · Card · Card stack · Carousel · Chat header · Chat marker · Chat message · Chat thread · Checkbox · Chip · Circular button · Code block · Code diff · Colour picker · Combo chart · Combobox · Command palette · Command palette trigger · Composer · Contact card · Context menu · Contribution graph · Data table · Date input · Date picker · Dialog · Divider · Document chip · Drawer · Dropdown · Dropdown menu · Empty state · Entity card · Event calendar · Field · Figure · File input · Filter bar · Funnel chart · Gantt chart · Gauge · Globe · Hover card · Image compare · Input · Inspector colour picker · Inspector dropdown · Inspector input · Inspector section · Inspector segments · Inspector slider · Inspector toggle switch · Instructions · Interrupt card · Kbd · Legend tile · Lightbox · Line chart · Link list · Map callout · Map legend · Mention · Message actions · Message card · Meter · Model picker · Nav · Nav list · Notification centre · Number input · Pagination · Panel · Pie chart · Pin input · Pixel avatar · Popover · Progress bar · Prompt suggestions · Prose · Question card · Quote · Radar chart · Radial chart · Radio button · Rating · Reasoning · Rich dropdown · Scatter chart · Section title · Segmented control · Selection card · Shader field · Sidebar panel · Skeleton · Slider · Source chip · Source trail · Sparkline · Spinner · Split button · Split pane · Stacked bar chart · Stat · Status dot · Stepper · Streaming text · Swatch · Table · Table toolbar · Tabs · Tag input · Textarea · Theme picker · Theme toggle · Thread panel · Thread tabs · Time picker · Timeline · Toast · Toggle group · Toggle switch · Tool call · Toolbar · Tooltip · Top app bar · Tree view · Treemap · Usage card · Waveform · Widget · World map
110
117
  <!-- component-list:end -->
111
118
 
112
119
  </details>
@@ -118,6 +125,7 @@ Accordion · Agent plan · Agent rail · Agent status · AI button · Alert · A
118
125
  - **Next.js 16**: the documentation site
119
126
  - **Storybook 10**: component explorer
120
127
  - **Vitest + Playwright + axe**: every Storybook story runs as a render test in headless Chromium, with an accessibility audit on each
128
+ - **TanStack Table + TanStack Virtual**: the headless engine and the windowing under `DataTable`, the package's runtime dependencies
121
129
  - **CSS custom properties**: all theming via semantic tokens, no CSS-in-JS
122
130
 
123
131
  The [overview](https://rift-ds.com/overview) shows the generate-and-validate pipeline behind the docs.
@@ -0,0 +1,8 @@
1
+ import { CSSProperties, ReactNode } from 'react';
2
+ export declare function ScrollRegion({ className, style, children, onNode, }: {
3
+ className?: string;
4
+ style?: CSSProperties;
5
+ children: ReactNode;
6
+ /** Hands the scrolling element to its owner (a virtualised table scrolls its rows inside it). */
7
+ onNode?: (node: HTMLDivElement | null) => void;
8
+ }): import("react").JSX.Element;
@@ -0,0 +1,51 @@
1
+ "use client";
2
+ import { jsx } from "react/jsx-runtime";
3
+ import { useRef, useCallback, useId, useState, useEffect } from "react";
4
+ function ScrollRegion({
5
+ className,
6
+ style,
7
+ children,
8
+ onNode
9
+ }) {
10
+ const ref = useRef(null);
11
+ const setNode = useCallback(
12
+ (node) => {
13
+ ref.current = node;
14
+ onNode?.(node);
15
+ },
16
+ [onNode]
17
+ );
18
+ const captionId = useId();
19
+ const [overflows, setOverflows] = useState(false);
20
+ const [labelledBy, setLabelledBy] = useState();
21
+ useEffect(() => {
22
+ const node = ref.current;
23
+ if (!node) return;
24
+ const caption = node.querySelector("caption");
25
+ if (caption) {
26
+ if (!caption.id) caption.id = captionId;
27
+ setLabelledBy(caption.id);
28
+ }
29
+ const measure = () => setOverflows(node.scrollWidth > node.clientWidth + 1);
30
+ measure();
31
+ const observer = new ResizeObserver(measure);
32
+ observer.observe(node);
33
+ if (node.firstElementChild) observer.observe(node.firstElementChild);
34
+ return () => observer.disconnect();
35
+ }, [captionId]);
36
+ return /* @__PURE__ */ jsx(
37
+ "div",
38
+ {
39
+ ref: setNode,
40
+ className,
41
+ style,
42
+ role: overflows ? "region" : void 0,
43
+ "aria-labelledby": overflows ? labelledBy : void 0,
44
+ tabIndex: overflows ? 0 : void 0,
45
+ children
46
+ }
47
+ );
48
+ }
49
+ export {
50
+ ScrollRegion
51
+ };
@@ -0,0 +1,9 @@
1
+ /**
2
+ * Development-only console warnings, the way React writes its own: the
3
+ * check reads `process.env.NODE_ENV`, which every bundler replaces at build
4
+ * time, so a production bundle drops the warning and a page with no
5
+ * bundler (and no `process`) stays silent rather than throwing. Each
6
+ * message prints once per page load, however many instances trip it.
7
+ */
8
+ /** Warns once, in development only. */
9
+ export declare function warnOnce(message: string): void;
@@ -0,0 +1,16 @@
1
+ const isDevelopment = (() => {
2
+ try {
3
+ return process.env.NODE_ENV !== "production";
4
+ } catch {
5
+ return false;
6
+ }
7
+ })();
8
+ const warned = /* @__PURE__ */ new Set();
9
+ function warnOnce(message) {
10
+ if (!isDevelopment || warned.has(message)) return;
11
+ warned.add(message);
12
+ console.warn(`[rift-ds] ${message}`);
13
+ }
14
+ export {
15
+ warnOnce
16
+ };
@@ -0,0 +1,111 @@
1
+ /**
2
+ * Anchored positioning: the one way every popup in the library places itself
3
+ * against the control that opened it (menus, listboxes, pickers, popovers,
4
+ * hover cards, tooltips).
5
+ *
6
+ * The panel stays where it is in the DOM, so it keeps the trigger's stacking
7
+ * context and any theme scoped above it, but is `position: fixed`, placed
8
+ * from the anchor's rectangle. A fixed panel is not clipped by an ancestor's
9
+ * `overflow`, which is what lets a menu inside a table's scroll wrapper, a
10
+ * rail or a card show whole. Portals would escape the same way but would
11
+ * leave the theme scope; floating-ui would solve it at the cost of a
12
+ * dependency. This is the approach Tooltip proved first.
13
+ *
14
+ * On every placement it:
15
+ * - puts the panel on the preferred side, flipping to the opposite side when
16
+ * the preferred one has too little room and the opposite has more;
17
+ * - shifts it along the edge to stay inside its box, a margin clear of the
18
+ * edge (`--gap-200` unless the component says otherwise);
19
+ * - optionally caps its height to the room on the side it took (a list then
20
+ * scrolls inside rather than running off the screen);
21
+ * - optionally sets its minimum width to the anchor's, so a listbox is never
22
+ * narrower than the control that opened it, and always publishes the
23
+ * anchor's width as `--ds-anchor-width` for a panel that should match it
24
+ * exactly;
25
+ * - marks the side it landed on as `data-side` (and `data-align`), so the
26
+ * entrance can grow from the anchor whichever way the panel flipped;
27
+ * - hides it (an inline `visibility: hidden`, flagged `data-anchor-hidden`)
28
+ * while the anchor is scrolled wholly out of view, so a panel never hangs
29
+ * at the screen's edge over nothing.
30
+ *
31
+ * The placement outlives the open state on purpose: a panel that eases out
32
+ * when it closes keeps its place for the exit instead of snapping back into
33
+ * flow mid-fade.
34
+ *
35
+ * It re-places on scroll (any scroll container, captured), on resize, and
36
+ * when the panel or the anchor changes size, once per animation frame, and
37
+ * writes straight to the DOM: placement never costs a React render, the same
38
+ * reasoning the gesture rule gives (design.md, the playground's resize
39
+ * record).
40
+ *
41
+ * The room a panel has is the viewport's. An ancestor with a transform,
42
+ * filter, backdrop filter, perspective or containment (a Dialog panel's
43
+ * entrance scale, the chat panel's blur) becomes the fixed panel's
44
+ * containing block, which moves the
45
+ * coordinates it is placed in; only when that ancestor also clips its
46
+ * overflow does the room narrow to its box.
47
+ */
48
+ export type AnchoredSide = 'top' | 'bottom' | 'left' | 'right';
49
+ export type AnchoredAlign = 'start' | 'center' | 'end';
50
+ export type AnchoredPlacement = {
51
+ /** The side the panel took after flipping. */
52
+ side: AnchoredSide;
53
+ /** The panel's position in the box, in px from the box's top left. */
54
+ x: number;
55
+ y: number;
56
+ /** The panel's size as placed. */
57
+ width: number;
58
+ height: number;
59
+ /** The anchor's rectangle, in the same coordinates. */
60
+ anchor: {
61
+ left: number;
62
+ top: number;
63
+ width: number;
64
+ height: number;
65
+ };
66
+ };
67
+ export type AnchoredPositionOptions = {
68
+ /** Whether the panel is showing. Placement runs only while true. */
69
+ open: boolean;
70
+ /** The control the panel is anchored to. */
71
+ anchorRef: React.RefObject<HTMLElement | null>;
72
+ /** The panel being placed. */
73
+ panelRef: React.RefObject<HTMLElement | null>;
74
+ /** The preferred side. Default `bottom`. */
75
+ side?: AnchoredSide;
76
+ /** Where along that side the panel lines up with the anchor. Default `start`. */
77
+ align?: AnchoredAlign;
78
+ /**
79
+ * The custom property that holds the gap between anchor and panel, read
80
+ * where it resolves so it is never restated as a number. Default
81
+ * `--gap-200`.
82
+ */
83
+ gapProperty?: string;
84
+ /**
85
+ * The custom property that holds the margin a panel keeps from the box's
86
+ * edge when it shifts to stay inside. Default `--gap-200`, so a panel never
87
+ * sits tighter to the screen's edge than that, whatever its trigger gap.
88
+ */
89
+ edgeProperty?: string;
90
+ /** Whether the panel may flip to the opposite side. Default true. */
91
+ flip?: boolean;
92
+ /** Cap the panel's height to the room on its side, for a list that scrolls inside. Default false. */
93
+ fitHeight?: boolean;
94
+ /** Set the panel's minimum width to the anchor's. Default false. */
95
+ matchWidth?: boolean;
96
+ /** Called after every placement, for a detail the panel draws itself (an arrow). */
97
+ onPlace?: (placement: AnchoredPlacement) => void;
98
+ /**
99
+ * Which of several anchors is open, for a component that shows one panel
100
+ * at a time against different controls (FilterBar's chips): a change
101
+ * re-reads the refs and places afresh. Leave undefined for one anchor.
102
+ */
103
+ target?: string | number | null;
104
+ };
105
+ /**
106
+ * The nearest ancestor a fixed-position descendant is placed from instead of
107
+ * the viewport. Null means the viewport.
108
+ */
109
+ export declare function fixedContainingBlock(element: HTMLElement): HTMLElement | null;
110
+ /** Places `panelRef` against `anchorRef` while `open`. See the module comment. */
111
+ export declare function useAnchoredPosition({ open, anchorRef, panelRef, side, align, gapProperty, edgeProperty, flip, fitHeight, matchWidth, onPlace, target, }: AnchoredPositionOptions): void;
@@ -0,0 +1,153 @@
1
+ "use client";
2
+ import { useRef, useLayoutEffect } from "react";
3
+ function fixedContainingBlock(element) {
4
+ for (let node = element.parentElement; node; node = node.parentElement) {
5
+ const style = getComputedStyle(node);
6
+ const read = (property) => style.getPropertyValue(property).trim();
7
+ if (read("transform") !== "none" || read("filter") !== "none" || read("backdrop-filter") !== "none" || read("perspective") !== "none" || /\b(layout|paint|strict|content)\b/.test(read("contain")) || /\b(transform|filter|perspective)\b/.test(read("will-change"))) {
8
+ return node;
9
+ }
10
+ }
11
+ return null;
12
+ }
13
+ const clamp = (value, min, max) => max < min ? min : Math.min(Math.max(value, min), max);
14
+ const OPPOSITE = {
15
+ top: "bottom",
16
+ bottom: "top",
17
+ left: "right",
18
+ right: "left"
19
+ };
20
+ function useAnchoredPosition({
21
+ open,
22
+ anchorRef,
23
+ panelRef,
24
+ side = "bottom",
25
+ align = "start",
26
+ gapProperty = "--gap-200",
27
+ edgeProperty = "--gap-200",
28
+ flip = true,
29
+ fitHeight = false,
30
+ matchWidth = false,
31
+ onPlace,
32
+ target
33
+ }) {
34
+ const onPlaceRef = useRef(onPlace);
35
+ useLayoutEffect(() => {
36
+ onPlaceRef.current = onPlace;
37
+ });
38
+ useLayoutEffect(() => {
39
+ if (!open) return;
40
+ const anchor = anchorRef.current;
41
+ const panel = panelRef.current;
42
+ if (!anchor || !panel) return;
43
+ panel.style.position = "fixed";
44
+ panel.style.inset = "auto";
45
+ panel.style.margin = "0";
46
+ const place = () => {
47
+ const block = fixedContainingBlock(panel);
48
+ const blockBox = block ? block.getBoundingClientRect() : null;
49
+ const origin = blockBox ? { left: blockBox.left + block.clientLeft, top: blockBox.top + block.clientTop } : { left: 0, top: 0 };
50
+ const bounds = {
51
+ left: 0,
52
+ top: 0,
53
+ right: document.documentElement.clientWidth,
54
+ bottom: document.documentElement.clientHeight
55
+ };
56
+ if (block && blockBox) {
57
+ const style = getComputedStyle(block);
58
+ if (style.overflowX !== "visible") {
59
+ bounds.left = Math.max(bounds.left, origin.left);
60
+ bounds.right = Math.min(bounds.right, origin.left + block.clientWidth);
61
+ }
62
+ if (style.overflowY !== "visible") {
63
+ bounds.top = Math.max(bounds.top, origin.top);
64
+ bounds.bottom = Math.min(bounds.bottom, origin.top + block.clientHeight);
65
+ }
66
+ }
67
+ const a = anchor.getBoundingClientRect();
68
+ const panelStyle = getComputedStyle(panel);
69
+ const gap = parseFloat(panelStyle.getPropertyValue(gapProperty)) || 0;
70
+ const edge = parseFloat(panelStyle.getPropertyValue(edgeProperty)) || 0;
71
+ panel.style.setProperty("--ds-anchor-width", `${a.width}px`);
72
+ if (matchWidth) panel.style.minWidth = `${a.width}px`;
73
+ if (fitHeight) panel.style.maxHeight = "";
74
+ const width = panel.offsetWidth;
75
+ let height = panel.offsetHeight;
76
+ const room = {
77
+ top: a.top - bounds.top - gap - edge,
78
+ bottom: bounds.bottom - a.bottom - gap - edge,
79
+ left: a.left - bounds.left - gap - edge,
80
+ right: bounds.right - a.right - gap - edge
81
+ };
82
+ const vertical = side === "top" || side === "bottom";
83
+ const need = vertical ? height : width;
84
+ let chosen = side;
85
+ if (flip && room[side] < need && room[OPPOSITE[side]] > room[side]) {
86
+ chosen = OPPOSITE[side];
87
+ }
88
+ if (fitHeight && vertical && height > room[chosen]) {
89
+ height = Math.max(room[chosen], 0);
90
+ panel.style.maxHeight = `${height}px`;
91
+ }
92
+ let x;
93
+ let y;
94
+ if (chosen === "top" || chosen === "bottom") {
95
+ y = chosen === "bottom" ? a.bottom + gap : a.top - gap - height;
96
+ x = align === "center" ? a.left + a.width / 2 - width / 2 : align === "end" ? a.right - width : a.left;
97
+ } else {
98
+ x = chosen === "right" ? a.right + gap : a.left - gap - width;
99
+ y = align === "center" ? a.top + a.height / 2 - height / 2 : align === "end" ? a.bottom - height : a.top;
100
+ }
101
+ x = clamp(x, bounds.left + edge, bounds.right - edge - width);
102
+ y = clamp(y, bounds.top + edge, bounds.bottom - edge - height);
103
+ panel.style.left = `${x - origin.left}px`;
104
+ panel.style.top = `${y - origin.top}px`;
105
+ panel.dataset.side = chosen;
106
+ panel.dataset.align = align;
107
+ const offscreen = a.bottom < bounds.top || a.top > bounds.bottom || a.right < bounds.left || a.left > bounds.right;
108
+ if (offscreen) {
109
+ panel.dataset.anchorHidden = "";
110
+ panel.style.visibility = "hidden";
111
+ } else if ("anchorHidden" in panel.dataset) {
112
+ delete panel.dataset.anchorHidden;
113
+ panel.style.removeProperty("visibility");
114
+ }
115
+ onPlaceRef.current?.({
116
+ side: chosen,
117
+ x: x - origin.left,
118
+ y: y - origin.top,
119
+ width,
120
+ height,
121
+ anchor: { left: a.left - origin.left, top: a.top - origin.top, width: a.width, height: a.height }
122
+ });
123
+ };
124
+ let frame = 0;
125
+ const schedule = () => {
126
+ if (frame) return;
127
+ frame = requestAnimationFrame(() => {
128
+ frame = 0;
129
+ place();
130
+ });
131
+ };
132
+ place();
133
+ window.addEventListener("scroll", schedule, true);
134
+ window.addEventListener("resize", schedule);
135
+ const observer = typeof ResizeObserver === "undefined" ? null : new ResizeObserver(schedule);
136
+ observer?.observe(panel);
137
+ observer?.observe(anchor);
138
+ return () => {
139
+ cancelAnimationFrame(frame);
140
+ window.removeEventListener("scroll", schedule, true);
141
+ window.removeEventListener("resize", schedule);
142
+ observer?.disconnect();
143
+ if ("anchorHidden" in panel.dataset) {
144
+ delete panel.dataset.anchorHidden;
145
+ panel.style.removeProperty("visibility");
146
+ }
147
+ };
148
+ }, [open, target, anchorRef, panelRef, side, align, gapProperty, edgeProperty, flip, fitHeight, matchWidth]);
149
+ }
150
+ export {
151
+ fixedContainingBlock,
152
+ useAnchoredPosition
153
+ };
@@ -30,8 +30,15 @@ type FocusScopeOptions = {
30
30
  active: boolean;
31
31
  /** Wrap Tab / Shift+Tab within the scope. Default true. */
32
32
  trap?: boolean;
33
- /** Restore focus to the previously focused element on deactivate. Default true. */
34
- restoreFocus?: boolean;
33
+ /**
34
+ * Restore focus on deactivate. Default true: the previously focused
35
+ * element, or its nearest visible stand-in. A function names the target
36
+ * instead, read at the moment of closing, for a host that knows better
37
+ * than the opener: a table whose row deleted itself from this dialog
38
+ * sends focus to the next row rather than the table's first control. A
39
+ * function that returns null falls back to the default.
40
+ */
41
+ restoreFocus?: boolean | (() => HTMLElement | null);
35
42
  /**
36
43
  * Where initial focus lands. Defaults to the first visible real control,
37
44
  * then the first focusable of any kind, then the container itself.
@@ -81,7 +81,11 @@ function useFocusScope(ref, options) {
81
81
  inertScopes.splice(index, 1);
82
82
  applyInert();
83
83
  }
84
- if (optionsRef.current.restoreFocus !== false && previousFocus) {
84
+ const restore = optionsRef.current.restoreFocus;
85
+ const chosen = typeof restore === "function" ? restore() : null;
86
+ if (chosen) {
87
+ chosen.focus();
88
+ } else if (restore !== false && previousFocus) {
85
89
  restoreTarget(previousFocus)?.focus();
86
90
  }
87
91
  };