rift-ds 1.12.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.
- package/README.md +21 -7
- package/behaviors/ScrollRegion.d.ts +8 -0
- package/behaviors/ScrollRegion.js +51 -0
- package/behaviors/devWarning.d.ts +9 -0
- package/behaviors/devWarning.js +16 -0
- package/behaviors/useAnchoredPosition.d.ts +111 -0
- package/behaviors/useAnchoredPosition.js +153 -0
- package/behaviors/useFocusScope.d.ts +9 -2
- package/behaviors/useFocusScope.js +5 -1
- package/behaviors/visually-hidden.css +24 -0
- package/bin/rift-ds.mjs +186 -27
- package/bin/templates/app/components/rift/ColorModeScript.tsx +10 -0
- package/bin/templates/app/components/rift/RiftRoot.tsx +13 -0
- package/bin/templates/app/components/rift/font.css +7 -0
- package/bin/templates/app/components/rift/font.ts +14 -0
- package/bin/templates/blocks/app/dashboard/page.module.css +90 -0
- package/bin/templates/blocks/app/dashboard/page.tsx +141 -0
- package/bin/templates/blocks/app/sign-in/page.module.css +91 -0
- package/bin/templates/blocks/app/sign-in/page.tsx +95 -0
- package/bin/templates/blocks/app/starter/page.module.css +7 -0
- package/bin/templates/blocks/app/starter/page.tsx +31 -0
- package/bin/templates/blocks/components/blocks/AppShell.module.css +160 -0
- package/bin/templates/blocks/components/blocks/AppShell.tsx +113 -0
- package/bin/templates/blocks/registry.json +39 -0
- package/chat/AssistantTurn.js +4 -2
- package/chat/ChatAgentPane.css +5 -1
- package/chat/ChatWidget.css +0 -36
- package/chat/ResponseActions.js +2 -1
- package/chat/types.d.ts +8 -7
- package/chat/useChat.js +2 -2
- package/components/Accordion/Accordion.css +10 -3
- package/components/AgentPlan/AgentPlan.css +0 -12
- package/components/AgentPlan/AgentPlan.js +2 -1
- package/components/AgentStatus/AgentRing.d.ts +27 -0
- package/components/AgentStatus/AgentRing.js +9 -0
- package/components/AgentStatus/AgentStatus.css +638 -10
- package/components/AgentStatus/AgentStatus.d.ts +31 -3
- package/components/AgentStatus/AgentStatus.js +22 -4
- package/components/AgentStatus/AgentStatusPatterns.d.ts +1 -1
- package/components/AgentStatus/AgentStatusPatterns.js +2 -1
- package/components/AiButton/AiButton.css +211 -38
- package/components/AiButton/AiButton.d.ts +34 -0
- package/components/AiButton/AiButton.js +129 -9
- package/components/AnimatedNumber/AnimatedNumber.css +0 -12
- package/components/AnimatedNumber/AnimatedNumber.js +2 -1
- package/components/AppSidebar/AppSidebar.css +2 -2
- package/components/Attachment/Attachment.css +0 -17
- package/components/Attachment/AttachmentTile.js +3 -2
- package/components/Avatar/Avatar.css +54 -0
- package/components/Avatar/Avatar.d.ts +9 -1
- package/components/Avatar/Avatar.js +5 -1
- package/components/Avatar/avatarFill.d.ts +9 -0
- package/components/Avatar/avatarFill.js +13 -0
- package/components/AvatarGroup/AvatarGroup.js +13 -1
- package/components/BottomNav/BottomNav.css +2 -11
- package/components/BottomNav/BottomNav.js +2 -1
- package/components/Button/Button.css +7 -0
- package/components/Button/Button.d.ts +8 -1
- package/components/Button/Button.js +10 -1
- package/components/CardStack/CardStack.css +0 -11
- package/components/CardStack/CardStack.js +2 -1
- package/components/ChatMessage/ChatMessage.css +0 -12
- package/components/ChatMessage/ChatMessage.js +2 -1
- package/components/Checkbox/Checkbox.css +34 -25
- package/components/Checkbox/Checkbox.d.ts +34 -0
- package/components/Checkbox/Checkbox.js +65 -37
- package/components/CircularButton/CircularButton.css +8 -7
- package/components/CodeBlock/CodeBlock.d.ts +2 -0
- package/components/CodeBlock/CodeBlock.js +2 -0
- package/components/CodeDiff/CodeDiff.css +0 -11
- package/components/CodeDiff/CodeDiff.js +3 -2
- package/components/ColorPicker/ColorPicker.css +3 -4
- package/components/ColorPicker/ColorPicker.js +11 -0
- package/components/ColourSwatch/ColourSwatch.css +1 -0
- package/components/Combobox/Combobox.css +21 -13
- package/components/Combobox/Combobox.js +77 -56
- package/components/CommandPalette/CommandPalette.css +2 -25
- package/components/CommandPalette/CommandPalette.js +8 -5
- package/components/CommandPaletteTrigger/CommandPaletteTrigger.css +88 -0
- package/components/CommandPaletteTrigger/CommandPaletteTrigger.d.ts +31 -0
- package/components/CommandPaletteTrigger/CommandPaletteTrigger.js +49 -0
- package/components/Composer/Composer.css +3 -15
- package/components/Composer/Composer.js +3 -2
- package/components/ContactCard/ContactCard.css +1 -1
- package/components/ContextMenu/ContextMenu.css +1 -9
- package/components/ContextMenu/ContextMenu.js +3 -2
- package/components/DataTable/CellEditor.d.ts +37 -0
- package/components/DataTable/CellEditor.js +153 -0
- package/components/DataTable/DataTable.css +1180 -0
- package/components/DataTable/DataTable.d.ts +498 -27
- package/components/DataTable/DataTable.js +2363 -189
- package/components/DataTable/FilterEditor.d.ts +10 -0
- package/components/DataTable/FilterEditor.js +181 -0
- package/components/DataTable/TypedCell.d.ts +22 -0
- package/components/DataTable/TypedCell.js +80 -0
- package/components/DataTable/VirtualRows.d.ts +21 -0
- package/components/DataTable/VirtualRows.js +38 -0
- package/components/DataTable/aggregates.d.ts +29 -0
- package/components/DataTable/aggregates.js +52 -0
- package/components/DataTable/columnTypes.d.ts +49 -0
- package/components/DataTable/columnTypes.js +96 -0
- package/components/DataTable/dragGesture.d.ts +24 -0
- package/components/DataTable/dragGesture.js +62 -0
- package/components/DataTable/editorKinds.d.ts +24 -0
- package/components/DataTable/editorKinds.js +20 -0
- package/components/DataTable/filterModel.d.ts +84 -0
- package/components/DataTable/filterModel.js +159 -0
- package/components/DataTable/tableContext.d.ts +82 -0
- package/components/DataTable/tableContext.js +7 -0
- package/components/DataTable/views.d.ts +50 -0
- package/components/DataTable/views.js +39 -0
- package/components/DateInput/DateInput.css +6 -0
- package/components/Dialog/Dialog.css +2 -26
- package/components/Dialog/Dialog.js +8 -12
- package/components/Drawer/Drawer.css +2 -26
- package/components/Drawer/Drawer.js +10 -13
- package/components/Dropdown/ChosenMark.css +19 -0
- package/components/Dropdown/Dropdown.css +22 -18
- package/components/Dropdown/Dropdown.d.ts +29 -0
- package/components/Dropdown/Dropdown.js +55 -24
- package/components/DropdownMenu/DropdownMenu.css +33 -21
- package/components/DropdownMenu/DropdownMenu.d.ts +4 -0
- package/components/DropdownMenu/DropdownMenu.js +68 -16
- package/components/FilterBar/FilterBar.css +10 -14
- package/components/FilterBar/FilterBar.js +77 -50
- package/components/GanttChart/GanttChart.css +0 -13
- package/components/GanttChart/GanttChart.js +3 -2
- package/components/HoverCard/HoverCard.css +24 -44
- package/components/HoverCard/HoverCard.js +18 -2
- package/components/Input/Input.css +6 -0
- package/components/Inspector/InspectorToggleSwitch.js +5 -10
- package/components/InterruptCard/InterruptCard.css +8 -12
- package/components/InterruptCard/InterruptCard.d.ts +2 -0
- package/components/InterruptCard/InterruptCard.js +9 -2
- package/components/ModelPicker/ModelPicker.css +18 -56
- package/components/ModelPicker/ModelPicker.d.ts +2 -0
- package/components/ModelPicker/ModelPicker.js +28 -51
- package/components/MotionSwatch/MotionSwatch.css +1 -1
- package/components/NotificationCenter/NotificationCenter.css +1 -13
- package/components/NotificationCenter/NotificationCenter.js +2 -1
- package/components/Pagination/Pagination.css +8 -6
- package/components/PixelAvatar/pixelInks.d.ts +6 -0
- package/components/PixelAvatar/pixelInks.js +7 -3
- package/components/Popover/Popover.css +32 -30
- package/components/Popover/Popover.d.ts +8 -0
- package/components/Popover/Popover.js +16 -2
- package/components/PromptSuggestions/PromptSuggestions.css +0 -11
- package/components/PromptSuggestions/PromptSuggestions.js +3 -2
- package/components/Prose/Prose.css +47 -8
- package/components/Prose/Prose.d.ts +2 -0
- package/components/Prose/Prose.js +7 -2
- package/components/QuestionCard/QuestionCard.css +13 -22
- package/components/QuestionCard/QuestionCard.js +8 -13
- package/components/RadioButton/RadioButton.css +27 -11
- package/components/RadioButton/RadioButton.d.ts +18 -0
- package/components/RadioButton/RadioButton.js +12 -2
- package/components/RichDropdown/RichDropdown.css +17 -14
- package/components/RichDropdown/RichDropdown.js +15 -8
- package/components/SegmentedControl/SegmentedControl.css +10 -14
- package/components/SegmentedControl/SegmentedControl.d.ts +7 -0
- package/components/SegmentedControl/SegmentedControl.js +4 -2
- package/components/SelectionCard/SelectionCard.css +29 -160
- package/components/SelectionCard/SelectionCard.js +5 -35
- package/components/ShaderField/useShaderField.js +13 -1
- package/components/SidebarPanel/SidebarPanel.css +1 -1
- package/components/SourceChip/SourceChip.css +6 -12
- package/components/SourceChip/SourceChip.js +10 -35
- package/components/SourceTrail/SourceTrail.css +0 -13
- package/components/SourceTrail/SourceTrail.js +2 -1
- package/components/SpacingSwatch/SpacingSwatch.css +1 -1
- package/components/Stepper/Stepper.css +0 -13
- package/components/Stepper/Stepper.js +2 -1
- package/components/Table/Table.css +60 -40
- package/components/Table/Table.d.ts +8 -0
- package/components/Table/Table.js +33 -12
- package/components/TableToolbar/TableToolbar.css +494 -0
- package/components/TableToolbar/TableToolbar.d.ts +281 -0
- package/components/TableToolbar/TableToolbar.js +1321 -0
- package/components/ThemePicker/ThemePicker.css +189 -0
- package/components/ThemePicker/ThemePicker.d.ts +67 -0
- package/components/ThemePicker/ThemePicker.js +165 -0
- package/components/ThemeToggle/ThemeToggle.css +82 -0
- package/components/ThemeToggle/ThemeToggle.d.ts +42 -0
- package/components/ThemeToggle/ThemeToggle.js +98 -0
- package/components/ThemeToggle/colorMode.d.ts +42 -0
- package/components/ThemeToggle/colorMode.js +26 -0
- package/components/ThemeToggle/useColorMode.d.ts +8 -0
- package/components/ThemeToggle/useColorMode.js +44 -0
- package/components/ThreadPanel/ThreadPanel.css +2 -16
- package/components/ThreadPanel/ThreadPanel.js +3 -2
- package/components/ThreadTabs/ThreadTabs.css +1 -15
- package/components/ThreadTabs/ThreadTabs.js +2 -1
- package/components/TimePicker/TimePicker.css +11 -17
- package/components/TimePicker/TimePicker.js +13 -8
- package/components/ToggleGroup/ToggleGroup.css +19 -2
- package/components/ToggleSwitch/ToggleSwitch.css +33 -27
- package/components/ToggleSwitch/ToggleSwitch.d.ts +19 -0
- package/components/ToggleSwitch/ToggleSwitch.js +17 -10
- package/components/Tooltip/Tooltip.css +48 -52
- package/components/Tooltip/Tooltip.d.ts +10 -0
- package/components/Tooltip/Tooltip.js +27 -2
- package/components/TopAppBar/TopAppBar.css +5 -3
- package/components/Waveform/Waveform.css +0 -12
- package/components/Waveform/Waveform.js +2 -1
- package/components/registry.json +36 -4
- package/components/registry.json.d.ts +36 -4
- package/components/registry.json.js +1 -1
- package/index.d.ts +11 -0
- package/index.js +41 -6
- package/package.json +5 -1
- package/tokens/base.css +84 -0
- package/tokens/motion.d.ts +2 -0
- package/tokens/motion.js +2 -0
- package/tokens/presets/contrast.css +6 -2
- package/tokens/presets/coral.css +4 -2
- package/tokens/presets/forest.css +6 -0
- package/tokens/presets/gold.css +10 -2
- package/tokens/presets/mono.css +4 -2
- package/tokens/presets/pink.css +10 -0
- package/tokens/presets/terminal.css +8 -2
- package/tokens/presets/violet.css +10 -0
- package/tokens/presets/warm.css +4 -0
- package/tokens/presets/zest.css +6 -0
- package/tokens/registry.d.ts +1 -1
- package/tokens/registry.json +27 -1
- package/tokens/registry.json.d.ts +27 -1
- package/tokens/registry.json.js +1 -1
- package/tokens/tokens-dark.css +36 -0
- package/tokens/tokens-forced-colors.css +31 -0
- package/tokens/tokens-light.css +49 -0
- package/tokens/tokens-primitives.css +9 -0
- package/tokens/tokens.css +1 -0
- package/cli/templates/chat/app/api/chat/feedback/route.d.ts +0 -3
- package/cli/templates/chat/app/api/chat/followups/route.d.ts +0 -3
- package/cli/templates/chat/app/api/chat/guardrails.d.ts +0 -10
- package/cli/templates/chat/app/api/chat/route.d.ts +0 -4
- package/cli/templates/chat/components/chat/ChatMount.d.ts +0 -1
- 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 -->
|
|
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
|
-
|
|
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
|
|
|
@@ -62,7 +69,13 @@ Swap the scripted transport for `createFetchTransport('/api/chat')` and the pane
|
|
|
62
69
|
|
|
63
70
|
## Set up your agent
|
|
64
71
|
|
|
65
|
-
|
|
72
|
+
The quickest start is one file: save [AGENTS.md](https://rift-ds.com/AGENTS.md) at your project root, or paste it into your agent. It holds the install, the setup, the rules, and where to look things up.
|
|
73
|
+
|
|
74
|
+
```bash
|
|
75
|
+
curl -o AGENTS.md https://rift-ds.com/AGENTS.md
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
For the full setup, one command teaches a coding agent the system: it installs the generated agent skills into your project and prints the MCP connect line. The skills cover the library's rules, moving existing screens onto the tokens and components, and auditing styles against them.
|
|
66
79
|
|
|
67
80
|
```bash
|
|
68
81
|
npx rift-ds init
|
|
@@ -74,7 +87,7 @@ The MCP endpoint serves the component catalogue, per-component prop APIs, the to
|
|
|
74
87
|
claude mcp add --transport http rift-ds https://rift-ds.com/api/mcp
|
|
75
88
|
```
|
|
76
89
|
|
|
77
|
-
Every component's prop contract is also plain markdown (append `.md` to its docs URL), and [llms.txt](https://rift-ds.com/llms.txt) indexes every machine surface. A theme made in the [playground](https://rift-ds.com/playground) exports as
|
|
90
|
+
Every component's prop contract is also plain markdown (append `.md` to its docs URL), and [llms.txt](https://rift-ds.com/llms.txt) indexes every machine surface. A theme made in the [playground](https://rift-ds.com/playground) exports as one AGENTS.md that carries the setup, your theme in words and its CSS, so the agent can read the look as well as the code. Then just ask: "Build a settings page with Rift components."
|
|
78
91
|
|
|
79
92
|
## Theming
|
|
80
93
|
|
|
@@ -94,13 +107,13 @@ import 'rift-ds/tokens/presets/presets.css';
|
|
|
94
107
|
|
|
95
108
|
## Components
|
|
96
109
|
|
|
97
|
-
<!-- component-count -->
|
|
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).
|
|
98
111
|
|
|
99
112
|
<details>
|
|
100
113
|
<summary>The full list</summary>
|
|
101
114
|
|
|
102
115
|
<!-- component-list:start -->
|
|
103
|
-
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
|
|
104
117
|
<!-- component-list:end -->
|
|
105
118
|
|
|
106
119
|
</details>
|
|
@@ -112,6 +125,7 @@ Accordion · Agent plan · Agent rail · Agent status · AI button · Alert · A
|
|
|
112
125
|
- **Next.js 16**: the documentation site
|
|
113
126
|
- **Storybook 10**: component explorer
|
|
114
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
|
|
115
129
|
- **CSS custom properties**: all theming via semantic tokens, no CSS-in-JS
|
|
116
130
|
|
|
117
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
|
-
/**
|
|
34
|
-
|
|
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
|
-
|
|
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
|
};
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/* ============================================
|
|
2
|
+
VISUALLY HIDDEN
|
|
3
|
+
The one recipe for text that is read but not
|
|
4
|
+
seen: a status for a live region, a control's
|
|
5
|
+
spoken name, a marker beside a visual state.
|
|
6
|
+
Every component imports this sheet and uses
|
|
7
|
+
ds-visually-hidden rather than writing the
|
|
8
|
+
recipe again (scripts/validate-shared-primitives.mjs
|
|
9
|
+
fails a second copy). A hidden native input
|
|
10
|
+
that must stay focusable is not this: it
|
|
11
|
+
keeps its own rule, with the reason beside it.
|
|
12
|
+
============================================ */
|
|
13
|
+
|
|
14
|
+
.ds-visually-hidden {
|
|
15
|
+
position: absolute;
|
|
16
|
+
width: 1px;
|
|
17
|
+
height: 1px;
|
|
18
|
+
padding: 0;
|
|
19
|
+
margin: -1px;
|
|
20
|
+
overflow: hidden;
|
|
21
|
+
clip: rect(0, 0, 0, 0);
|
|
22
|
+
white-space: nowrap;
|
|
23
|
+
border: 0;
|
|
24
|
+
}
|