@vjdesigns/vjdesigns-ui 1.5.0 → 2.1.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 (121) hide show
  1. package/README.md +781 -723
  2. package/dist/ai/llms-full.txt +1003 -923
  3. package/dist/ai/llms.txt +2 -1
  4. package/dist/ai/registry.json +340 -121
  5. package/dist/components/data-display/Chat/Chat.cjs.js +1 -0
  6. package/dist/components/data-display/Chat/Chat.d.ts +114 -0
  7. package/dist/components/data-display/Chat/Chat.mjs +301 -0
  8. package/dist/components/data-display/Chat/Chat.module.css.cjs.js +1 -0
  9. package/dist/components/data-display/Chat/Chat.module.css.mjs +32 -0
  10. package/dist/components/data-display/Chat/index.d.ts +2 -0
  11. package/dist/components/data-display/Chat/streams.cjs.js +4 -0
  12. package/dist/components/data-display/Chat/streams.d.ts +32 -0
  13. package/dist/components/data-display/Chat/streams.mjs +35 -0
  14. package/dist/components/data-display/DataGrid/DataGrid.module.css.cjs.js +1 -1
  15. package/dist/components/data-display/DataGrid/DataGrid.module.css.mjs +1 -1
  16. package/dist/components/data-display/VirtualList/VirtualList.module.css.cjs.js +1 -1
  17. package/dist/components/data-display/VirtualList/VirtualList.module.css.mjs +1 -1
  18. package/dist/components/feedback/ErrorSummary/ErrorSummary.module.css.cjs.js +1 -1
  19. package/dist/components/feedback/ErrorSummary/ErrorSummary.module.css.mjs +1 -1
  20. package/dist/components/forms/Combobox/Combobox.cjs.js +1 -1
  21. package/dist/components/forms/Combobox/Combobox.mjs +5 -5
  22. package/dist/components/forms/DateTimePicker/DateTimePicker.module.css.cjs.js +1 -1
  23. package/dist/components/forms/DateTimePicker/DateTimePicker.module.css.mjs +1 -1
  24. package/dist/components/forms/Rating/Rating.module.css.cjs.js +1 -1
  25. package/dist/components/forms/Rating/Rating.module.css.mjs +1 -1
  26. package/dist/components/forms/TagInput/TagInput.cjs.js +1 -1
  27. package/dist/components/forms/TagInput/TagInput.mjs +5 -5
  28. package/dist/components/forms/TimePicker/TimePicker.module.css.cjs.js +1 -1
  29. package/dist/components/forms/TimePicker/TimePicker.module.css.mjs +1 -1
  30. package/dist/components/layout/ResizablePanels/ResizablePanels.cjs.js +1 -1
  31. package/dist/components/layout/ResizablePanels/ResizablePanels.mjs +1 -0
  32. package/dist/components/layout/ResizablePanels/ResizablePanels.module.css.cjs.js +1 -1
  33. package/dist/components/layout/ResizablePanels/ResizablePanels.module.css.mjs +1 -1
  34. package/dist/components/layout/ScrollArea/ScrollArea.module.css.cjs.js +1 -1
  35. package/dist/components/layout/ScrollArea/ScrollArea.module.css.mjs +1 -1
  36. package/dist/components/navigation/ContextMenu/ContextMenu.module.css.cjs.js +1 -1
  37. package/dist/components/navigation/ContextMenu/ContextMenu.module.css.mjs +1 -1
  38. package/dist/components/overlays/HoverCard/HoverCard.module.css.cjs.js +1 -1
  39. package/dist/components/overlays/HoverCard/HoverCard.module.css.mjs +2 -2
  40. package/dist/components/primitives/Button/Button.cjs.js +1 -1
  41. package/dist/components/primitives/Button/Button.mjs +37 -37
  42. package/dist/components/utilities/LocaleProvider/LocaleProvider.module.css.cjs.js +1 -1
  43. package/dist/components/utilities/LocaleProvider/LocaleProvider.module.css.mjs +1 -1
  44. package/dist/components/utilities/Theme/Theme.cjs.js +1 -1
  45. package/dist/components/utilities/Theme/Theme.d.ts +6 -6
  46. package/dist/components/utilities/Theme/Theme.mjs +49 -45
  47. package/dist/hooks/useFormValidation.cjs.js +1 -1
  48. package/dist/hooks/useFormValidation.mjs +1 -1
  49. package/dist/i18n/en.cjs.js +1 -1
  50. package/dist/i18n/en.mjs +71 -0
  51. package/dist/i18n/types.d.ts +91 -0
  52. package/dist/index.cjs.js +1 -1
  53. package/dist/index.d.ts +1 -0
  54. package/dist/index.mjs +108 -106
  55. package/dist/locales/ar.cjs.js +1 -1
  56. package/dist/locales/ar.mjs +71 -0
  57. package/dist/locales/bn.cjs.js +1 -1
  58. package/dist/locales/bn.mjs +71 -0
  59. package/dist/locales/cs.cjs.js +1 -1
  60. package/dist/locales/cs.mjs +71 -0
  61. package/dist/locales/da.cjs.js +1 -1
  62. package/dist/locales/da.mjs +71 -0
  63. package/dist/locales/de.cjs.js +1 -1
  64. package/dist/locales/de.mjs +71 -0
  65. package/dist/locales/el.cjs.js +1 -1
  66. package/dist/locales/el.mjs +71 -0
  67. package/dist/locales/es.cjs.js +1 -1
  68. package/dist/locales/es.mjs +71 -0
  69. package/dist/locales/fa.cjs.js +1 -1
  70. package/dist/locales/fa.mjs +71 -0
  71. package/dist/locales/fi.cjs.js +1 -1
  72. package/dist/locales/fi.mjs +71 -0
  73. package/dist/locales/fr.cjs.js +1 -1
  74. package/dist/locales/fr.mjs +71 -0
  75. package/dist/locales/he.cjs.js +1 -1
  76. package/dist/locales/he.mjs +71 -0
  77. package/dist/locales/hi.cjs.js +1 -1
  78. package/dist/locales/hi.mjs +71 -0
  79. package/dist/locales/hu.cjs.js +1 -1
  80. package/dist/locales/hu.mjs +71 -0
  81. package/dist/locales/id.cjs.js +1 -1
  82. package/dist/locales/id.mjs +71 -0
  83. package/dist/locales/it.cjs.js +1 -1
  84. package/dist/locales/it.mjs +71 -0
  85. package/dist/locales/ja.cjs.js +1 -1
  86. package/dist/locales/ja.mjs +71 -0
  87. package/dist/locales/ko.cjs.js +1 -1
  88. package/dist/locales/ko.mjs +71 -0
  89. package/dist/locales/nb.cjs.js +1 -1
  90. package/dist/locales/nb.mjs +71 -0
  91. package/dist/locales/nl.cjs.js +1 -1
  92. package/dist/locales/nl.mjs +71 -0
  93. package/dist/locales/pl.cjs.js +1 -1
  94. package/dist/locales/pl.mjs +71 -0
  95. package/dist/locales/pt-BR.cjs.js +1 -1
  96. package/dist/locales/pt-BR.mjs +71 -0
  97. package/dist/locales/ro.cjs.js +1 -1
  98. package/dist/locales/ro.mjs +71 -0
  99. package/dist/locales/ru.cjs.js +1 -1
  100. package/dist/locales/ru.mjs +71 -0
  101. package/dist/locales/sv.cjs.js +1 -1
  102. package/dist/locales/sv.mjs +71 -0
  103. package/dist/locales/th.cjs.js +1 -1
  104. package/dist/locales/th.mjs +71 -0
  105. package/dist/locales/tr.cjs.js +1 -1
  106. package/dist/locales/tr.mjs +71 -0
  107. package/dist/locales/uk.cjs.js +1 -1
  108. package/dist/locales/uk.mjs +71 -0
  109. package/dist/locales/ur.cjs.js +1 -1
  110. package/dist/locales/ur.mjs +71 -0
  111. package/dist/locales/vi.cjs.js +1 -1
  112. package/dist/locales/vi.mjs +71 -0
  113. package/dist/locales/zh-Hans.cjs.js +1 -1
  114. package/dist/locales/zh-Hans.mjs +71 -0
  115. package/dist/locales/zh-Hant.cjs.js +1 -1
  116. package/dist/locales/zh-Hant.mjs +71 -0
  117. package/dist/utils/brandPalette.cjs.js +3 -3
  118. package/dist/utils/brandPalette.d.ts +4 -2
  119. package/dist/utils/brandPalette.mjs +20 -20
  120. package/dist/vjdesigns-ui.css +1 -1
  121. package/package.json +90 -88
@@ -1,511 +1,511 @@
1
- # @vjdesigns/vjdesigns-ui 1.5.0: full reference
1
+ # @vjdesigns/vjdesigns-ui 2.0.0: full reference
2
2
 
3
3
  > Accessible React 19 component library: WCAG 2.2 AA verified (axe on every story, a contrast check on every color pair), 31 languages with right-to-left support, themeable with any brand color (auto-adjusted to stay accessible), tree-shakable, no runtime dependencies besides React.
4
4
 
5
5
  - Install: `npm install @vjdesigns/vjdesigns-ui` (peer: react, react-dom 18 or 19).
6
6
  - Import the stylesheet once: `import "@vjdesigns/vjdesigns-ui/dist/vjdesigns-ui.css";`
7
7
  - Import components from the package root: `import { Button, Field, Input } from "@vjdesigns/vjdesigns-ui";`
8
- - Theme: wrap the app in `<Theme brand="blue" | "purple" | ... | "#hex" mode="light" | "dark" accent="brand" | "volt" | "#hex">`.
8
+ - Theme: wrap the app in `<Theme brand="blue" | "purple" | ... | "#hex" mode="light" | "dark" accent="brand" | "lime" | "#hex">`.
9
9
  - Language: `<LocaleProvider locale="es" messages={es}>` with `import es from "@vjdesigns/vjdesigns-ui/locales/es"`.
10
10
  - Forms: wrap each control in `Field` (label, hint, error, required); validate with `useFormValidation` + `ErrorSummary`.
11
11
  - Style with the props (variant, tone, size) and the --color-*, --space-*, --radius-* CSS variables; don't override internal class names.
12
12
 
13
13
  # Guide: Installation
14
14
 
15
- Install the package along with React and React DOM (version 18 or 19), which the components need in order to render:
16
-
17
- ```bash
18
- npm install @vjdesigns/vjdesigns-ui react react-dom
19
- ```
20
-
21
- If your project already uses React, you only need the first package:
22
-
23
- ```bash
24
- npm install @vjdesigns/vjdesigns-ui
25
- ```
26
-
27
- ### Import the styles
28
-
29
- The component styles and shared design tokens ship as a separate stylesheet. Import it **once** at your app's entry point, or the components will render unstyled.
30
-
31
- **React:** in `main.tsx` (or `App.tsx`)
32
-
33
- ```ts
34
- import "@vjdesigns/vjdesigns-ui/dist/vjdesigns-ui.css";
35
- ```
36
-
37
- **Angular:** add it to the `styles` array in `angular.json`
38
-
39
- ```json
40
- "styles": [
41
- "node_modules/@vjdesigns/vjdesigns-ui/dist/vjdesigns-ui.css",
42
- "src/styles.css"
43
- ]
44
- ```
45
-
46
- **Vue:** in `main.ts`
47
-
48
- ```ts
49
- import "@vjdesigns/vjdesigns-ui/dist/vjdesigns-ui.css";
15
+ Install the package along with React and React DOM (version 18 or 19), which the components need in order to render:
16
+
17
+ ```bash
18
+ npm install @vjdesigns/vjdesigns-ui react react-dom
19
+ ```
20
+
21
+ If your project already uses React, you only need the first package:
22
+
23
+ ```bash
24
+ npm install @vjdesigns/vjdesigns-ui
25
+ ```
26
+
27
+ ### Import the styles
28
+
29
+ The component styles and shared design tokens ship as a separate stylesheet. Import it **once** at your app's entry point, or the components will render unstyled.
30
+
31
+ **React:** in `main.tsx` (or `App.tsx`)
32
+
33
+ ```ts
34
+ import "@vjdesigns/vjdesigns-ui/dist/vjdesigns-ui.css";
35
+ ```
36
+
37
+ **Angular:** add it to the `styles` array in `angular.json`
38
+
39
+ ```json
40
+ "styles": [
41
+ "node_modules/@vjdesigns/vjdesigns-ui/dist/vjdesigns-ui.css",
42
+ "src/styles.css"
43
+ ]
44
+ ```
45
+
46
+ **Vue:** in `main.ts`
47
+
48
+ ```ts
49
+ import "@vjdesigns/vjdesigns-ui/dist/vjdesigns-ui.css";
50
50
  ```
51
51
 
52
52
  # Guide: Usage
53
53
 
54
- The components are built with React. You can use them directly in a React project. In Angular or Vue, you mount them inside a small wrapper component, as shown below.
55
-
56
- ### React
57
-
58
- ```tsx
59
- import { useState } from "react";
60
- import { Button, CheckBox } from "@vjdesigns/vjdesigns-ui";
61
-
62
- export function SignupForm() {
63
- const [accepted, setAccepted] = useState(false);
64
-
65
- return (
66
- <form>
67
- <CheckBox
68
- label="Accept terms"
69
- checked={accepted}
70
- onCheckedChange={setAccepted}
71
- />
72
- <Button
73
- variant="solid"
74
- tone="primary"
75
- size="md"
76
- disabled={!accepted}
77
- onClick={() => console.log("Submitted")}
78
- >
79
- Submit
80
- </Button>
81
- </form>
82
- );
83
- }
84
- ```
85
-
86
- ### Angular
87
-
88
- Make a standalone wrapper that renders the React component into its host element. The wrapper uses `createElement`, so you don't need to change your JSX or TSX settings.
89
-
90
- ```ts
91
- // vj-button.component.ts
92
- import {
93
- AfterViewInit,
94
- Component,
95
- ElementRef,
96
- EventEmitter,
97
- Input,
98
- OnChanges,
99
- OnDestroy,
100
- Output,
101
- ViewChild,
102
- } from "@angular/core";
103
- import { createElement, type MouseEvent as ReactMouseEvent, type ReactElement } from "react";
104
- import { createRoot, Root } from "react-dom/client";
105
- import {
106
- Button,
107
- type ButtonProps,
108
- type ButtonVariant,
109
- type Size,
110
- type Tone,
111
- } from "@vjdesigns/vjdesigns-ui";
112
-
113
- // Button has overloaded types (for `href` and `as`). Narrowing it here lets createElement type-check.
114
- const ReactButton = Button as (props: ButtonProps) => ReactElement;
115
-
116
- @Component({
117
- selector: "vj-button",
118
- standalone: true,
119
- template: `<span #host></span>`,
120
- })
121
- export class VjButtonComponent implements AfterViewInit, OnChanges, OnDestroy {
122
- @ViewChild("host", { static: true }) host!: ElementRef<HTMLElement>;
123
-
124
- @Input() label = "";
125
- @Input() variant: ButtonVariant = "solid";
126
- @Input() tone: Tone = "primary";
127
- @Input() size: Size = "md";
128
- @Input() disabled = false;
129
- @Output() clicked = new EventEmitter<MouseEvent>();
130
-
131
- private root?: Root;
132
-
133
- ngAfterViewInit() {
134
- this.root = createRoot(this.host.nativeElement);
135
- this.render();
136
- }
137
-
138
- ngOnChanges() {
139
- this.render();
140
- }
141
-
142
- ngOnDestroy() {
143
- this.root?.unmount();
144
- }
145
-
146
- private render() {
147
- this.root?.render(
148
- createElement(
149
- ReactButton,
150
- {
151
- variant: this.variant,
152
- tone: this.tone,
153
- size: this.size,
154
- disabled: this.disabled,
155
- onClick: (event: ReactMouseEvent) => this.clicked.emit(event.nativeEvent),
156
- },
157
- this.label // the button's text is passed as children
158
- )
159
- );
160
- }
161
- }
162
- ```
163
-
164
- Import the wrapper wherever you need it:
165
-
166
- ```ts
167
- // app.component.ts
168
- import { Component } from "@angular/core";
169
- import { VjButtonComponent } from "./vj-button.component";
170
-
171
- @Component({
172
- selector: "app-root",
173
- standalone: true,
174
- imports: [VjButtonComponent],
175
- template: `<vj-button label="Submit" tone="primary" (clicked)="onSubmit()" />`,
176
- })
177
- export class AppComponent {
178
- onSubmit() {
179
- console.log("Submitted");
180
- }
181
- }
182
- ```
183
-
184
- ### Vue 3
185
-
186
- Make a wrapper component that mounts the React component when it loads and re-renders it whenever its props change.
187
-
188
- ```vue
189
- <!-- VjButton.vue -->
190
- <script setup lang="ts">
191
- import { onBeforeUnmount, onMounted, ref, watch } from "vue";
192
- import { createElement, type MouseEvent as ReactMouseEvent, type ReactElement } from "react";
193
- import { createRoot, type Root } from "react-dom/client";
194
- import {
195
- Button,
196
- type ButtonProps,
197
- type ButtonVariant,
198
- type Size,
199
- type Tone,
200
- } from "@vjdesigns/vjdesigns-ui";
201
-
202
- // Button has overloaded types (for `href` and `as`). Narrowing it here lets createElement type-check.
203
- const ReactButton = Button as (props: ButtonProps) => ReactElement;
204
-
205
- const props = withDefaults(
206
- defineProps<{
207
- label: string;
208
- variant?: ButtonVariant;
209
- tone?: Tone;
210
- size?: Size;
211
- disabled?: boolean;
212
- }>(),
213
- { variant: "solid", tone: "primary", size: "md", disabled: false }
214
- );
215
-
216
- const emit = defineEmits<{ click: [event: MouseEvent] }>();
217
-
218
- const host = ref<HTMLElement | null>(null);
219
- let root: Root | undefined;
220
-
221
- function render() {
222
- root?.render(
223
- createElement(
224
- ReactButton,
225
- {
226
- variant: props.variant,
227
- tone: props.tone,
228
- size: props.size,
229
- disabled: props.disabled,
230
- onClick: (event: ReactMouseEvent) => emit("click", event.nativeEvent),
231
- },
232
- props.label // the button's text is passed as children
233
- )
234
- );
235
- }
236
-
237
- onMounted(() => {
238
- root = createRoot(host.value!);
239
- render();
240
- });
241
-
242
- watch(() => ({ ...props }), render);
243
-
244
- onBeforeUnmount(() => root?.unmount());
245
- </script>
246
-
247
- <template>
248
- <span ref="host" />
249
- </template>
250
- ```
251
-
252
- Use it like any other Vue component:
253
-
254
- ```vue
255
- <script setup lang="ts">
256
- import VjButton from "./VjButton.vue";
257
-
258
- const onSubmit = () => console.log("Submitted");
259
- </script>
260
-
261
- <template>
262
- <VjButton label="Submit" tone="primary" @click="onSubmit" />
263
- </template>
264
- ```
265
-
54
+ The components are built with React. You can use them directly in a React project. In Angular or Vue, you mount them inside a small wrapper component, as shown below.
55
+
56
+ ### React
57
+
58
+ ```tsx
59
+ import { useState } from "react";
60
+ import { Button, CheckBox } from "@vjdesigns/vjdesigns-ui";
61
+
62
+ export function SignupForm() {
63
+ const [accepted, setAccepted] = useState(false);
64
+
65
+ return (
66
+ <form>
67
+ <CheckBox
68
+ label="Accept terms"
69
+ checked={accepted}
70
+ onCheckedChange={setAccepted}
71
+ />
72
+ <Button
73
+ variant="solid"
74
+ tone="primary"
75
+ size="md"
76
+ disabled={!accepted}
77
+ onClick={() => console.log("Submitted")}
78
+ >
79
+ Submit
80
+ </Button>
81
+ </form>
82
+ );
83
+ }
84
+ ```
85
+
86
+ ### Angular
87
+
88
+ Make a standalone wrapper that renders the React component into its host element. The wrapper uses `createElement`, so you don't need to change your JSX or TSX settings.
89
+
90
+ ```ts
91
+ // vj-button.component.ts
92
+ import {
93
+ AfterViewInit,
94
+ Component,
95
+ ElementRef,
96
+ EventEmitter,
97
+ Input,
98
+ OnChanges,
99
+ OnDestroy,
100
+ Output,
101
+ ViewChild,
102
+ } from "@angular/core";
103
+ import { createElement, type MouseEvent as ReactMouseEvent, type ReactElement } from "react";
104
+ import { createRoot, Root } from "react-dom/client";
105
+ import {
106
+ Button,
107
+ type ButtonProps,
108
+ type ButtonVariant,
109
+ type Size,
110
+ type Tone,
111
+ } from "@vjdesigns/vjdesigns-ui";
112
+
113
+ // Button has overloaded types (for `href` and `as`). Narrowing it here lets createElement type-check.
114
+ const ReactButton = Button as (props: ButtonProps) => ReactElement;
115
+
116
+ @Component({
117
+ selector: "vj-button",
118
+ standalone: true,
119
+ template: `<span #host></span>`,
120
+ })
121
+ export class VjButtonComponent implements AfterViewInit, OnChanges, OnDestroy {
122
+ @ViewChild("host", { static: true }) host!: ElementRef<HTMLElement>;
123
+
124
+ @Input() label = "";
125
+ @Input() variant: ButtonVariant = "solid";
126
+ @Input() tone: Tone = "primary";
127
+ @Input() size: Size = "md";
128
+ @Input() disabled = false;
129
+ @Output() clicked = new EventEmitter<MouseEvent>();
130
+
131
+ private root?: Root;
132
+
133
+ ngAfterViewInit() {
134
+ this.root = createRoot(this.host.nativeElement);
135
+ this.render();
136
+ }
137
+
138
+ ngOnChanges() {
139
+ this.render();
140
+ }
141
+
142
+ ngOnDestroy() {
143
+ this.root?.unmount();
144
+ }
145
+
146
+ private render() {
147
+ this.root?.render(
148
+ createElement(
149
+ ReactButton,
150
+ {
151
+ variant: this.variant,
152
+ tone: this.tone,
153
+ size: this.size,
154
+ disabled: this.disabled,
155
+ onClick: (event: ReactMouseEvent) => this.clicked.emit(event.nativeEvent),
156
+ },
157
+ this.label // the button's text is passed as children
158
+ )
159
+ );
160
+ }
161
+ }
162
+ ```
163
+
164
+ Import the wrapper wherever you need it:
165
+
166
+ ```ts
167
+ // app.component.ts
168
+ import { Component } from "@angular/core";
169
+ import { VjButtonComponent } from "./vj-button.component";
170
+
171
+ @Component({
172
+ selector: "app-root",
173
+ standalone: true,
174
+ imports: [VjButtonComponent],
175
+ template: `<vj-button label="Submit" tone="primary" (clicked)="onSubmit()" />`,
176
+ })
177
+ export class AppComponent {
178
+ onSubmit() {
179
+ console.log("Submitted");
180
+ }
181
+ }
182
+ ```
183
+
184
+ ### Vue 3
185
+
186
+ Make a wrapper component that mounts the React component when it loads and re-renders it whenever its props change.
187
+
188
+ ```vue
189
+ <!-- VjButton.vue -->
190
+ <script setup lang="ts">
191
+ import { onBeforeUnmount, onMounted, ref, watch } from "vue";
192
+ import { createElement, type MouseEvent as ReactMouseEvent, type ReactElement } from "react";
193
+ import { createRoot, type Root } from "react-dom/client";
194
+ import {
195
+ Button,
196
+ type ButtonProps,
197
+ type ButtonVariant,
198
+ type Size,
199
+ type Tone,
200
+ } from "@vjdesigns/vjdesigns-ui";
201
+
202
+ // Button has overloaded types (for `href` and `as`). Narrowing it here lets createElement type-check.
203
+ const ReactButton = Button as (props: ButtonProps) => ReactElement;
204
+
205
+ const props = withDefaults(
206
+ defineProps<{
207
+ label: string;
208
+ variant?: ButtonVariant;
209
+ tone?: Tone;
210
+ size?: Size;
211
+ disabled?: boolean;
212
+ }>(),
213
+ { variant: "solid", tone: "primary", size: "md", disabled: false }
214
+ );
215
+
216
+ const emit = defineEmits<{ click: [event: MouseEvent] }>();
217
+
218
+ const host = ref<HTMLElement | null>(null);
219
+ let root: Root | undefined;
220
+
221
+ function render() {
222
+ root?.render(
223
+ createElement(
224
+ ReactButton,
225
+ {
226
+ variant: props.variant,
227
+ tone: props.tone,
228
+ size: props.size,
229
+ disabled: props.disabled,
230
+ onClick: (event: ReactMouseEvent) => emit("click", event.nativeEvent),
231
+ },
232
+ props.label // the button's text is passed as children
233
+ )
234
+ );
235
+ }
236
+
237
+ onMounted(() => {
238
+ root = createRoot(host.value!);
239
+ render();
240
+ });
241
+
242
+ watch(() => ({ ...props }), render);
243
+
244
+ onBeforeUnmount(() => root?.unmount());
245
+ </script>
246
+
247
+ <template>
248
+ <span ref="host" />
249
+ </template>
250
+ ```
251
+
252
+ Use it like any other Vue component:
253
+
254
+ ```vue
255
+ <script setup lang="ts">
256
+ import VjButton from "./VjButton.vue";
257
+
258
+ const onSubmit = () => console.log("Submitted");
259
+ </script>
260
+
261
+ <template>
262
+ <VjButton label="Submit" tone="primary" @click="onSubmit" />
263
+ </template>
264
+ ```
265
+
266
266
  > **Tip:** Wrap `CheckBox` and any other component the same way. Pass its props into `createElement`, and send its callbacks back out as Angular outputs or Vue emits. For `CheckBox`, that means sending `onCheckedChange` back out.
267
267
 
268
268
  # Guide: Theming
269
269
 
270
- Wrap any part of your app in `Theme` to set the color mode, brand palette, and accent for everything inside it:
271
-
272
- ```tsx
273
- import { Theme, Button } from "@vjdesigns/vjdesigns-ui";
274
-
275
- <Theme mode="dark" brand="purple" fill>
276
- <Button>Dark, purple button</Button>
277
- </Theme>
278
- ```
279
-
280
- | Prop | Type | Default | Description |
281
- | --- | --- | --- | --- |
282
- | `mode` | `"light" \| "dark"` | inherited | Color mode for everything inside. |
283
- | `brand` | `"black" \| "blue" \| "gold" \| "green" \| "purple"`, a hex color, or `{ light, dark }` | inherited | Brand palette for buttons, links, focus rings and indicators. |
284
- | `accent` | `"brand" | "volt"`, a hex color, or `{ light, dark }` | inherited (the brand's own) | The bright highlight on badges, switches, progress bars, sliders, steppers, and active navigation. |
285
- | `fill` | `boolean` | `false` | Paints the theme's background and text color on the wrapper. |
286
-
287
- Outside React (or to theme the whole page), set the attributes on `<html>` instead:
288
-
289
- ```html
290
- <html data-theme="dark" data-brand="purple" data-accent="volt">
291
- ```
292
-
293
- ### Accent color
294
-
295
- The accent is the energetic highlight: badges, switches, progress bars, sliders, steppers, and the active link in the Navbar. By default it follows the brand, as a bright shade of the brand's own color, so it always fits:
296
-
297
- | Brand | Default accent |
298
- | --- | --- |
299
- | `blue` (default) | sky blue `#76c7ff` |
300
- | `purple` | lavender `#c0b0ff` |
301
- | `gold` | amber `#f5ad5f` |
302
- | `green` | mint `#72d4ad` |
303
- | `black` | volt `#c8f031` (black has no hue to build on) |
304
- | a custom hex | generated from that color |
305
-
306
- To choose a different one:
307
-
308
- ```tsx
309
- <Theme accent="volt">…</Theme> // the original bright yellow-green
310
- <Theme accent="#ff6b00">…</Theme> // any color
311
- <Theme accent="brand">…</Theme> // back to the brand's own, inside an area with another accent
312
- ```
313
-
314
- Accents always carry dark text. A color too dark for that is lightened (same hue) just enough to meet WCAG AA, with a warning in development. An accent you set also holds inside nested brands. In CSS, `accentCss("name", "#ff6b00")` generates a `[data-accent="name"]` rule, like `brandCss` below.
315
-
316
- ### Custom brand colors
317
-
318
- Pass any hex color as `brand` and a full palette is generated for both modes:
319
-
320
- ```tsx
321
- <Theme brand="#e11d48">…</Theme>
322
- <Theme brand={{ light: "#4338ca", dark: "#f472b6" }}>…</Theme>
323
- ```
324
-
325
- Every generated palette meets WCAG 2.2 AA. A single color is used exactly as given in the mode where it has enough contrast (dark colors in light mode, light colors in dark mode), and the other mode gets a matching shade of the same hue. If a color doesn't have enough contrast (for example, a mid-tone that fails in both modes), its lightness is shifted just enough to pass, and a warning is logged in development.
326
-
327
- To define a brand in CSS instead (for the whole page, applied before any JavaScript runs), generate a stylesheet rule with `brandCss`:
328
-
329
- ```ts
330
- import { brandCss } from "@vjdesigns/vjdesigns-ui";
331
-
332
- brandCss("acme", "#e11d48"); // '[data-brand="acme"] { --brand-light-primary: … }'
333
- ```
334
-
270
+ Wrap any part of your app in `Theme` to set the color mode, brand palette, and accent for everything inside it:
271
+
272
+ ```tsx
273
+ import { Theme, Button } from "@vjdesigns/vjdesigns-ui";
274
+
275
+ <Theme mode="dark" brand="purple" fill>
276
+ <Button>Dark, purple button</Button>
277
+ </Theme>
278
+ ```
279
+
280
+ | Prop | Type | Default | Description |
281
+ | --- | --- | --- | --- |
282
+ | `mode` | `"light" \| "dark"` | inherited | Color mode for everything inside. |
283
+ | `brand` | `"black" \| "blue" \| "gold" \| "green" \| "purple"`, a hex color, or `{ light, dark }` | inherited | Brand palette for buttons, links, focus rings and indicators. |
284
+ | `accent` | `"brand" | "lime"`, a hex color, or `{ light, dark }` | inherited (the brand's own) | The bright highlight on badges, switches, progress bars, sliders, steppers, and active navigation. |
285
+ | `fill` | `boolean` | `false` | Paints the theme's background and text color on the wrapper. |
286
+
287
+ Outside React (or to theme the whole page), set the attributes on `<html>` instead:
288
+
289
+ ```html
290
+ <html data-theme="dark" data-brand="purple" data-accent="lime">
291
+ ```
292
+
293
+ ### Accent color
294
+
295
+ The accent is the energetic highlight: badges, switches, progress bars, sliders, steppers, and the active link in the Navbar. By default it follows the brand, as a bright shade of the brand's own color, so it always fits:
296
+
297
+ | Brand | Default accent |
298
+ | --- | --- |
299
+ | `blue` (default) | sky blue `#76c7ff` |
300
+ | `purple` | lavender `#c0b0ff` |
301
+ | `gold` | amber `#f5ad5f` |
302
+ | `green` | mint `#72d4ad` |
303
+ | `black` | lime `#c8f031` (black has no hue to build on) |
304
+ | a custom hex | generated from that color |
305
+
306
+ To choose a different one:
307
+
308
+ ```tsx
309
+ <Theme accent="lime">…</Theme> // the original bright yellow-green ("volt", its former name, still works)
310
+ <Theme accent="#ff6b00">…</Theme> // any color
311
+ <Theme accent="brand">…</Theme> // back to the brand's own, inside an area with another accent
312
+ ```
313
+
314
+ Accents always carry dark text. A color too dark for that is lightened (same hue) just enough to meet WCAG AA, with a warning in development. An accent you set also holds inside nested brands. In CSS, `accentCss("name", "#ff6b00")` generates a `[data-accent="name"]` rule, like `brandCss` below.
315
+
316
+ ### Custom brand colors
317
+
318
+ Pass any hex color as `brand` and a full palette is generated for both modes:
319
+
320
+ ```tsx
321
+ <Theme brand="#e11d48">…</Theme>
322
+ <Theme brand={{ light: "#4338ca", dark: "#f472b6" }}>…</Theme>
323
+ ```
324
+
325
+ Every generated palette meets WCAG 2.2 AA. A single color is used exactly as given in the mode where it has enough contrast (dark colors in light mode, light colors in dark mode), and the other mode gets a matching shade of the same hue. If a color doesn't have enough contrast (for example, a mid-tone that fails in both modes), its lightness is shifted just enough to pass, and a warning is logged in development.
326
+
327
+ To define a brand in CSS instead (for the whole page, applied before any JavaScript runs), generate a stylesheet rule with `brandCss`:
328
+
329
+ ```ts
330
+ import { brandCss } from "@vjdesigns/vjdesigns-ui";
331
+
332
+ brandCss("acme", "#e11d48"); // '[data-brand="acme"] { --brand-light-primary: … }'
333
+ ```
334
+
335
335
  Add the rule to your stylesheet and use `<html data-brand="acme">`. `createBrandPalette(color)` returns the generated colors as an object, if you need them directly.
336
336
 
337
337
  # Guide: Languages
338
338
 
339
- Components have built-in text: button names like "Close" and "Next page", screen-reader announcements like "3 results available", validation messages, and date formats. Wrap your app in `LocaleProvider` to show all of it in another language:
340
-
341
- ```tsx
342
- import { LocaleProvider } from "@vjdesigns/vjdesigns-ui";
343
- import es from "@vjdesigns/vjdesigns-ui/locales/es";
344
-
345
- <LocaleProvider locale="es" messages={es}>
346
- <App />
347
- </LocaleProvider>
348
- ```
349
-
350
- `LocaleProvider` sets:
351
-
352
- - **Built-in text** for every component, from `messages`.
353
- - **Dates and numbers** in the locale's format (`23/10/2026`, `1.234,5`), and plurals that follow the language's rules.
354
- - **Writing direction**: Arabic, Hebrew, Persian, and Urdu get `dir="rtl"`, and layouts and arrow keys mirror.
355
- - **`lang`** on its wrapper, so screen readers pronounce the text correctly. Popups (menus, dialogs, tooltips) keep the language of the area that opened them.
356
-
357
- Each locale is a separate import, so your app only downloads the languages it uses. Without a `LocaleProvider`, components use English text and the browser's date and number format.
358
-
359
- This library only translates its own built-in text. Text your app passes in, like `<Button>Save</Button>`, is your app's content: translate it with your app's i18n library (react-i18next, FormatJS, and so on).
360
-
361
- ### What gets translated
362
-
363
- The language is never picked automatically from where the user lives or their browser settings. Your app chooses it by rendering a `LocaleProvider`. For example, with `<LocaleProvider locale="de" messages={de}>`:
364
-
365
- | | Translated? |
366
- | --- | --- |
367
- | Text your app passes in, like `<Button>Save</Button>` | No: your app translates it |
368
- | Built-in names, like "Close", "Next page", "Clear search" | Yes: "Schließen", "Nächste Seite", "Suche löschen" |
369
- | Screen-reader announcements, like "3 results available" | Yes: "3 Ergebnisse verfügbar" |
370
- | Validation messages, empty states, file-upload prompts | Yes |
371
- | Dates and numbers | Yes: "23.10.2026", "4,5" |
372
-
373
- To follow the user's browser language, pass it yourself: `locale={navigator.language}` together with the matching `messages` (see [Switching language at runtime](#switching-language-at-runtime)).
374
-
375
- ### Bundled languages
376
-
377
- `ar` Arabic · `bn` Bengali · `cs` Czech · `da` Danish · `de` German · `el` Greek · `es` Spanish · `fa` Persian · `fi` Finnish · `fr` French · `he` Hebrew · `hi` Hindi · `hu` Hungarian · `id` Indonesian · `it` Italian · `ja` Japanese · `ko` Korean · `nb` Norwegian Bokmål · `nl` Dutch · `pl` Polish · `pt-BR` Portuguese (Brazil) · `ro` Romanian · `ru` Russian · `sv` Swedish · `th` Thai · `tr` Turkish · `uk` Ukrainian · `ur` Urdu · `vi` Vietnamese · `zh-Hans` Chinese (Simplified) · `zh-Hant` Chinese (Traditional), plus English (the default).
378
-
379
- The translations are machine-generated and haven't been reviewed by native speakers yet. If you find a wrong or awkward phrase, override it (below) and report it so the bundled locale can be fixed.
380
-
381
- ### Changing a few strings
382
-
383
- Pass only the strings you want to change. Everything else falls back to the outer provider's language (English at the top):
384
-
385
- ```tsx
386
- <LocaleProvider locale="en-GB" messages={{ pagination: { next: "Onwards" } }}>
387
- ```
388
-
389
- Providers nest, so an inner provider can switch the language for one part of the page. Per-component props such as `clearLabel` or `labels` still win over the provider.
390
-
391
- ### Other languages
392
-
393
- For a language that isn't bundled, write a complete messages object. TypeScript flags any missing string. Use English as the template:
394
-
395
- ```ts
396
- import { en, plural, type Messages } from "@vjdesigns/vjdesigns-ui";
397
-
398
- const p = plural("sw");
399
- export const sw: Messages = {
400
- ...en,
401
- pagination: { ...en.pagination, next: "Ukurasa unaofuata", previous: "Ukurasa uliopita" },
402
- searchInput: {
403
- ...en.searchInput,
404
- results: (count) => p(count, { "=0": "Hakuna matokeo", one: "Tokeo #", other: "Matokeo #" }),
405
- },
406
- // ...
407
- };
408
- ```
409
-
410
- `plural(locale)` picks the right plural form for any language (`#` is the formatted number).
411
-
412
- ### Switching language at runtime
413
-
414
- List the languages your app offers as loaders, so each one is downloaded only when picked:
415
-
416
- ```tsx
417
- import { LocaleProvider, type Messages } from "@vjdesigns/vjdesigns-ui";
418
-
419
- const loaders: Record<string, () => Promise<{ default: Messages }>> = {
420
- es: () => import("@vjdesigns/vjdesigns-ui/locales/es"),
421
- fr: () => import("@vjdesigns/vjdesigns-ui/locales/fr"),
422
- ar: () => import("@vjdesigns/vjdesigns-ui/locales/ar"),
423
- };
424
-
425
- function App({ lang }: { lang: string }) {
426
- const [messages, setMessages] = useState<Messages>();
427
- useEffect(() => {
428
- const load = loaders[lang];
429
- if (load) load().then((mod) => setMessages(mod.default));
430
- else setMessages(undefined); // English
431
- }, [lang]);
432
- // Until a language loads, the previous one (or English) shows.
433
- return <LocaleProvider locale={lang} messages={messages}>…</LocaleProvider>;
434
- }
435
- ```
436
-
339
+ Components have built-in text: button names like "Close" and "Next page", screen-reader announcements like "3 results available", validation messages, and date formats. Wrap your app in `LocaleProvider` to show all of it in another language:
340
+
341
+ ```tsx
342
+ import { LocaleProvider } from "@vjdesigns/vjdesigns-ui";
343
+ import es from "@vjdesigns/vjdesigns-ui/locales/es";
344
+
345
+ <LocaleProvider locale="es" messages={es}>
346
+ <App />
347
+ </LocaleProvider>
348
+ ```
349
+
350
+ `LocaleProvider` sets:
351
+
352
+ - **Built-in text** for every component, from `messages`.
353
+ - **Dates and numbers** in the locale's format (`23/10/2026`, `1.234,5`), and plurals that follow the language's rules.
354
+ - **Writing direction**: Arabic, Hebrew, Persian, and Urdu get `dir="rtl"`, and layouts and arrow keys mirror.
355
+ - **`lang`** on its wrapper, so screen readers pronounce the text correctly. Popups (menus, dialogs, tooltips) keep the language of the area that opened them.
356
+
357
+ Each locale is a separate import, so your app only downloads the languages it uses. Without a `LocaleProvider`, components use English text and the browser's date and number format.
358
+
359
+ This library only translates its own built-in text. Text your app passes in, like `<Button>Save</Button>`, is your app's content: translate it with your app's i18n library (react-i18next, FormatJS, and so on).
360
+
361
+ ### What gets translated
362
+
363
+ The language is never picked automatically from where the user lives or their browser settings. Your app chooses it by rendering a `LocaleProvider`. For example, with `<LocaleProvider locale="de" messages={de}>`:
364
+
365
+ | | Translated? |
366
+ | --- | --- |
367
+ | Text your app passes in, like `<Button>Save</Button>` | No: your app translates it |
368
+ | Built-in names, like "Close", "Next page", "Clear search" | Yes: "Schließen", "Nächste Seite", "Suche löschen" |
369
+ | Screen-reader announcements, like "3 results available" | Yes: "3 Ergebnisse verfügbar" |
370
+ | Validation messages, empty states, file-upload prompts | Yes |
371
+ | Dates and numbers | Yes: "23.10.2026", "4,5" |
372
+
373
+ To follow the user's browser language, pass it yourself: `locale={navigator.language}` together with the matching `messages` (see [Switching language at runtime](#switching-language-at-runtime)).
374
+
375
+ ### Bundled languages
376
+
377
+ `ar` Arabic · `bn` Bengali · `cs` Czech · `da` Danish · `de` German · `el` Greek · `es` Spanish · `fa` Persian · `fi` Finnish · `fr` French · `he` Hebrew · `hi` Hindi · `hu` Hungarian · `id` Indonesian · `it` Italian · `ja` Japanese · `ko` Korean · `nb` Norwegian Bokmål · `nl` Dutch · `pl` Polish · `pt-BR` Portuguese (Brazil) · `ro` Romanian · `ru` Russian · `sv` Swedish · `th` Thai · `tr` Turkish · `uk` Ukrainian · `ur` Urdu · `vi` Vietnamese · `zh-Hans` Chinese (Simplified) · `zh-Hant` Chinese (Traditional), plus English (the default).
378
+
379
+ The translations are machine-generated and haven't been reviewed by native speakers yet. If you find a wrong or awkward phrase, override it (below) and report it so the bundled locale can be fixed. (Working on the library itself? See [Translating built-in text](#translating-built-in-text).)
380
+
381
+ ### Changing a few strings
382
+
383
+ Pass only the strings you want to change. Everything else falls back to the outer provider's language (English at the top):
384
+
385
+ ```tsx
386
+ <LocaleProvider locale="en-GB" messages={{ pagination: { next: "Onwards" } }}>
387
+ ```
388
+
389
+ Providers nest, so an inner provider can switch the language for one part of the page. Per-component props such as `clearLabel` or `labels` still win over the provider.
390
+
391
+ ### Other languages
392
+
393
+ For a language that isn't bundled, write a complete messages object. TypeScript flags any missing string. Use English as the template:
394
+
395
+ ```ts
396
+ import { en, plural, type Messages } from "@vjdesigns/vjdesigns-ui";
397
+
398
+ const p = plural("sw");
399
+ export const sw: Messages = {
400
+ ...en,
401
+ pagination: { ...en.pagination, next: "Ukurasa unaofuata", previous: "Ukurasa uliopita" },
402
+ searchInput: {
403
+ ...en.searchInput,
404
+ results: (count) => p(count, { "=0": "Hakuna matokeo", one: "Tokeo #", other: "Matokeo #" }),
405
+ },
406
+ // ...
407
+ };
408
+ ```
409
+
410
+ `plural(locale)` picks the right plural form for any language (`#` is the formatted number).
411
+
412
+ ### Switching language at runtime
413
+
414
+ List the languages your app offers as loaders, so each one is downloaded only when picked:
415
+
416
+ ```tsx
417
+ import { LocaleProvider, type Messages } from "@vjdesigns/vjdesigns-ui";
418
+
419
+ const loaders: Record<string, () => Promise<{ default: Messages }>> = {
420
+ es: () => import("@vjdesigns/vjdesigns-ui/locales/es"),
421
+ fr: () => import("@vjdesigns/vjdesigns-ui/locales/fr"),
422
+ ar: () => import("@vjdesigns/vjdesigns-ui/locales/ar"),
423
+ };
424
+
425
+ function App({ lang }: { lang: string }) {
426
+ const [messages, setMessages] = useState<Messages>();
427
+ useEffect(() => {
428
+ const load = loaders[lang];
429
+ if (load) load().then((mod) => setMessages(mod.default));
430
+ else setMessages(undefined); // English
431
+ }, [lang]);
432
+ // Until a language loads, the previous one (or English) shows.
433
+ return <LocaleProvider locale={lang} messages={messages}>…</LocaleProvider>;
434
+ }
435
+ ```
436
+
437
437
  For server rendering, also set `<html lang="…" dir="…">` on the page. `getLocaleDirection(locale)` returns `"ltr"` or `"rtl"`.
438
438
 
439
439
  # Guide: Form validation
440
440
 
441
- > **Front-end validation is not security.** It helps people fill in a form correctly, but anyone can turn off JavaScript or send requests straight to your server. Always validate again on the server. The format checks below (`isEmail`, `isIP`, …) are plain functions you can reuse there.
442
-
443
- `useFormValidation` checks a form when it's submitted, using the browser's own rules on each input (`required`, `type="email"`, `minLength`, `pattern`, `min` / `max`) plus any rules you add. Messages follow the current language.
444
-
445
- ```tsx
446
- import {
447
- useFormValidation, rules, ErrorSummary, Field, Input, PasswordInput, Button,
448
- } from "@vjdesigns/vjdesigns-ui";
449
-
450
- function SignUp() {
451
- const form = useFormValidation({
452
- rules: {
453
- confirm: rules.matches("password", "The passwords don't match."),
454
- serverIp: rules.ip(), // IPv4 or IPv6
455
- },
456
- // Runs only when everything is valid. Return the server's errors to show them on their fields.
457
- onValid: async (values) => {
458
- const response = await api.signUp(values);
459
- return response.errors; // e.g. { username: "That username is taken." }
460
- },
461
- });
462
-
463
- return (
464
- <form {...form.formProps}>
465
- <ErrorSummary ref={form.summaryRef} errors={form.errorList} />
466
- <Field label="Email" required error={form.errors.email}>
467
- <Input name="email" type="email" />
468
- </Field>
469
- <Field label="Password" required error={form.errors.password}>
470
- <PasswordInput name="password" minLength={12} />
471
- </Field>
472
- <Field label="Confirm password" required error={form.errors.confirm}>
473
- <PasswordInput name="confirm" />
474
- </Field>
475
- <Field label="Server IP" error={form.errors.serverIp}>
476
- <Input name="serverIp" />
477
- </Field>
478
- <Button type="submit" loading={form.isSubmitting}>Create account</Button>
479
- </form>
480
- );
481
- }
482
- ```
483
-
484
- How it behaves:
485
-
486
- - **On submit**, every field is checked. If anything is wrong, the `ErrorSummary` at the top lists the problems and takes focus, so screen readers announce it. Each item links to its field. Without a summary, focus goes to the first invalid field.
487
- - **Errors are keyed by the field's `name`.** Pass `form.errors.<name>` to each `Field`'s `error`.
488
- - **After a failed submit**, an error disappears as soon as the field is fixed (while typing), and a field is fully re-checked when you leave it. It doesn't nag while you're still typing.
489
- - **Server errors** (returned from `onValid`, or set with `form.setErrors({...})`) show on their fields and stay until that field is edited.
490
- - **Emails** must have a real domain (`name@example.com`). The browser's own check also accepts `name@localhost`.
491
-
492
- ### Rules and format checks
493
-
494
- | Rule | Checks |
495
- | --- | --- |
496
- | `rules.required()` | Not empty (for controls without a native `required`) |
497
- | `rules.email()` | An email address in the usual format, with a top-level domain |
498
- | `rules.ip()` / `rules.ip({ version: 4 })` / `rules.ip({ version: 6 })` | An IPv4 or IPv6 address |
499
- | `rules.minLength(n)` / `rules.maxLength(n)` | Length in characters |
500
- | `rules.pattern(/regex/, "message")` | The whole value matches |
501
- | `rules.matches("otherField")` | Equals another field (confirm password) |
502
- | your own `(value, { values }) => message` | Anything else |
503
-
504
- Each rule takes an optional custom message. The checks behind them are exported for use anywhere, including your server:
505
-
506
- - `isEmail(value)`
507
- - `isIPv4(value)`: four numbers 0–255, no leading zeros
508
- - `isIPv6(value)`: including `::` shorthand and IPv4 tails like `::ffff:192.168.0.1`
441
+ > **Front-end validation is not security.** It helps people fill in a form correctly, but anyone can turn off JavaScript or send requests straight to your server. Always validate again on the server. The format checks below (`isEmail`, `isIP`, …) are plain functions you can reuse there.
442
+
443
+ `useFormValidation` checks a form when it's submitted, using the browser's own rules on each input (`required`, `type="email"`, `minLength`, `pattern`, `min` / `max`) plus any rules you add. Messages follow the current language.
444
+
445
+ ```tsx
446
+ import {
447
+ useFormValidation, rules, ErrorSummary, Field, Input, PasswordInput, Button,
448
+ } from "@vjdesigns/vjdesigns-ui";
449
+
450
+ function SignUp() {
451
+ const form = useFormValidation({
452
+ rules: {
453
+ confirm: rules.matches("password", "The passwords don't match."),
454
+ serverIp: rules.ip(), // IPv4 or IPv6
455
+ },
456
+ // Runs only when everything is valid. Return the server's errors to show them on their fields.
457
+ onValid: async (values) => {
458
+ const response = await api.signUp(values);
459
+ return response.errors; // e.g. { username: "That username is taken." }
460
+ },
461
+ });
462
+
463
+ return (
464
+ <form {...form.formProps}>
465
+ <ErrorSummary ref={form.summaryRef} errors={form.errorList} />
466
+ <Field label="Email" required error={form.errors.email}>
467
+ <Input name="email" type="email" />
468
+ </Field>
469
+ <Field label="Password" required error={form.errors.password}>
470
+ <PasswordInput name="password" minLength={12} />
471
+ </Field>
472
+ <Field label="Confirm password" required error={form.errors.confirm}>
473
+ <PasswordInput name="confirm" />
474
+ </Field>
475
+ <Field label="Server IP" error={form.errors.serverIp}>
476
+ <Input name="serverIp" />
477
+ </Field>
478
+ <Button type="submit" loading={form.isSubmitting}>Create account</Button>
479
+ </form>
480
+ );
481
+ }
482
+ ```
483
+
484
+ How it behaves:
485
+
486
+ - **On submit**, every field is checked. If anything is wrong, the `ErrorSummary` at the top lists the problems and takes focus, so screen readers announce it. Each item links to its field. Without a summary, focus goes to the first invalid field.
487
+ - **Errors are keyed by the field's `name`.** Pass `form.errors.<name>` to each `Field`'s `error`.
488
+ - **After a failed submit**, an error disappears as soon as the field is fixed (while typing), and a field is fully re-checked when you leave it. It doesn't nag while you're still typing.
489
+ - **Server errors** (returned from `onValid`, or set with `form.setErrors({...})`) show on their fields and stay until that field is edited.
490
+ - **Emails** must have a real domain (`name@example.com`). The browser's own check also accepts `name@localhost`.
491
+
492
+ ### Rules and format checks
493
+
494
+ | Rule | Checks |
495
+ | --- | --- |
496
+ | `rules.required()` | Not empty (for controls without a native `required`) |
497
+ | `rules.email()` | An email address in the usual format, with a top-level domain |
498
+ | `rules.ip()` / `rules.ip({ version: 4 })` / `rules.ip({ version: 6 })` | An IPv4 or IPv6 address |
499
+ | `rules.minLength(n)` / `rules.maxLength(n)` | Length in characters |
500
+ | `rules.pattern(/regex/, "message")` | The whole value matches |
501
+ | `rules.matches("otherField")` | Equals another field (confirm password) |
502
+ | your own `(value, { values }) => message` | Anything else |
503
+
504
+ Each rule takes an optional custom message. The checks behind them are exported for use anywhere, including your server:
505
+
506
+ - `isEmail(value)`
507
+ - `isIPv4(value)`: four numbers 0–255, no leading zeros
508
+ - `isIPv6(value)`: including `::` shorthand and IPv4 tails like `::ffff:192.168.0.1`
509
509
  - `isIP(value, version?)`
510
510
 
511
511
  # Components
@@ -927,6 +927,77 @@ view, so keyboard and screen reader users only reach what is shown.
927
927
  - Controlled Edge Cases: Controlled: the viewport only moves when the parent accepts the new index. A parent that ignores `onIndexChange` keeps the slide (and dots) in place, and an out-of-range `index` is reported back clamped.
928
928
  - Resize Keeps Slide: After a resize the same slide stays in view (slide widths change, so scroll is realigned).
929
929
 
930
+ ## Chat
931
+
932
+ import { Chat } from "@vjdesigns/vjdesigns-ui";
933
+
934
+ Category: data-display · Storybook: Data Display/Chat
935
+
936
+ A chat interface for an AI assistant (or a person), with no backend of its
937
+ own: `onSend` connects it to any API and can stream the reply. Includes a
938
+ scrolling conversation that follows new messages, an auto-growing message
939
+ box (Enter sends, Shift+Enter adds a line), Stop and Retry, copy buttons,
940
+ suggested prompts, and screen reader announcements of each reply.
941
+
942
+ ```tsx
943
+ <Chat
944
+ style={{ blockSize: 560 }}
945
+ onSend={async function* (text, { messages, signal }) {
946
+ const res = await fetch("/api/chat", {
947
+ method: "POST",
948
+ body: JSON.stringify({ messages }),
949
+ signal,
950
+ });
951
+ yield* readTextStream(res);
952
+ }}
953
+ />
954
+ ```
955
+
956
+ ### Props
957
+
958
+ - `messages`: `ChatMessage[]`. Controlled conversation.
959
+ - `defaultMessages`: `ChatMessage[]`. Initial conversation when uncontrolled.
960
+ - `onMessagesChange`: `(messages: ChatMessage[]) => void`. Called with the whole conversation whenever it changes (new messages, every streamed chunk, status changes).
961
+ - `onSend`: `( text: string, context: ChatSendContext, ) => ChatSendResult | Promise<ChatSendResult>`. Connects the Chat to your backend. Called with the user's text when they send a message (or retry). Return the reply as a string, stream it as an `AsyncIterable<string>`, or return nothing and add it yourself through controlled `messages`. Throwing (or rejecting) turns the reply into an error with a Retry button.
962
+ - `onStop`: `() => void`. Called when the user stops a reply, after its `signal` is aborted.
963
+ - `onError`: `(error: unknown) => void`. Called with whatever `onSend` threw, for logging. The user sees `labels.error` (or the message's `error`).
964
+ - `draft`: `string`. Controlled text of the message box.
965
+ - `defaultDraft`: `string`. Initial text of the message box when uncontrolled.
966
+ - `onDraftChange`: `(draft: string) => void`. Called with the message box text on every edit.
967
+ - `suggestions`: `string[]`. Prompts offered as buttons while the conversation is empty. Pressing one sends it.
968
+ - `emptyState`: `ReactNode`. Shown while the conversation is empty, above the suggestions. Defaults to a short greeting (`labels.emptyTitle`).
969
+ - `renderContent`: `(message: ChatMessage) => ReactNode`. Renders a message's content, e.g. through your Markdown renderer. Defaults to plain text with line breaks kept. Never render unsanitized HTML from a model or a user.
970
+ - `assistantName`: `string`. Name of the assistant, read to screen readers ("Coach said:"). Defaults to `labels.assistant`.
971
+ - `assistantAvatar`: `ReactNode`. Shown beside assistant messages (e.g. an `Avatar` or `Icon`). Decorative: hidden from assistive technology.
972
+ - `userAvatar`: `ReactNode`. Shown beside user messages. Decorative: hidden from assistive technology.
973
+ - `copyable`: `boolean` (default true). Show a Copy button under finished assistant messages.
974
+ - `disclaimer`: `ReactNode`. Small print under the message box, e.g. "AI can make mistakes."
975
+ - `placeholder`: `string`. Placeholder of the message box. Defaults to `labels.placeholder`.
976
+ - `maxLength`: `number`. Maximum length of a message, in characters. Shows a counter near the limit.
977
+ - `disabled`: `boolean`. Disables the message box and the suggestions.
978
+ - `labels`: `Partial<ChatLabels>`. Overrides for the built-in text.
979
+ - Plus every native `<section>` attribute (className, style, id, aria-*, data-*, event handlers), spread onto the root.
980
+
981
+ ### Examples (Storybook stories)
982
+
983
+ - Playground
984
+ - Send And Stream: Type a message and press Enter: it is sent, and the streamed reply is announced when done.
985
+ - Shift Enter Adds Line: Shift+Enter adds a line instead of sending.
986
+ - String Reply: `onSend` can return the whole reply as a string (or a promise of one).
987
+ - With Suggestions: While the conversation is empty, `emptyState` and `suggestions` show. Pressing a suggestion sends it.
988
+ - With Avatars: Avatars beside each speaker. They're decorative; the speaker is read from hidden text.
989
+ - Long Conversation: A long conversation. Scroll up, and a button jumps back to the latest message.
990
+ - Long Content: A long unbroken string (a URL, say) wraps instead of overflowing the bubble.
991
+ - Error And Retry: When `onSend` throws, the reply shows an error with a Retry button. This backend fails the first time.
992
+ - Stop Streaming: While a reply streams, the send button becomes Stop (Escape in the message box works too).
993
+ - Disabled
994
+ - With Max Length: `maxLength` limits a message and shows a counter near the limit.
995
+ - System Messages: System messages (joins, handoffs, context notes) are centered notes.
996
+ - Custom Rendering: `renderContent` renders messages your way, e.g. through a Markdown renderer. This one turns `**bold**` into `<strong>` and keeps paragraphs. Never render unsanitized HTML from a model.
997
+ - Controlled: Controlled: the parent owns the messages, so it can save, clear, or seed them. Here, `onSend` returns nothing and the parent adds the reply itself (the way a WebSocket or a human agent would).
998
+ - Connecting ABackend: How to connect a real backend: stream server-sent events from your API route with `readEventStream`, passing `signal` so Stop cancels the request. ```tsx import { Chat, readEventStream } from "@vjdesigns/vjdesigns-ui"; <Chat onSend={async function* (_text, { messages, signal }) { const res = await fetch("/api/chat", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ messages: messages.map(({ role, content }) => ({ role, content })), }), signal, }); if (!res.ok) throw new Error(`Chat request failed: ${res.status}`); for await (const data of readEventStream(res)) { if (data === "[DONE]") break; yield JSON.parse(data).text ?? ""; } }} /> ``` Keep API keys on your server: the browser should call your own route, which calls the model provider. This story fakes the server with an in-memory event stream.
999
+ - Right To Left: Right-to-left: user messages sit on the inline end, which is the left in RTL.
1000
+
930
1001
  ## Chip
931
1002
 
932
1003
  import { Chip } from "@vjdesigns/vjdesigns-ui";
@@ -1107,36 +1178,36 @@ import { DataGrid } from "@vjdesigns/vjdesigns-ui";
1107
1178
 
1108
1179
  Category: data-display · Storybook: Data display/DataGrid
1109
1180
 
1110
- An interactive data table (WAI-ARIA APG "Data Grid"): sortable columns
1111
- (with Shift for multi-column sorts), search filtering, single or
1112
- multiple row selection, pagination, and a full keyboard model. Works with
1113
- client-side data, or server-side data with `manual`.
1114
-
1115
- Keyboard: the grid is one Tab stop. Arrow keys move between cells
1116
- (mirrored in RTL), Home / End go to the row's first or last cell, Ctrl+Home
1117
- / Ctrl+End to the first or last cell of the grid, Page Up / Page Down move
1118
- ten rows. On a header, Enter sorts (Shift+Enter adds a secondary sort).
1119
- Space selects the row, Shift+click or Shift+Space selects a range, and
1120
- Ctrl/⌘+A selects every row. A cell with a single button, link, or
1121
- checkbox focuses it directly; for cells with several controls or a text
1122
- field, Enter or F2 moves into the cell, Tab cycles inside it, and Escape
1123
- returns to the grid.
1124
-
1125
- Screen readers hear sort changes and filter result counts; the grid is a
1126
- real `<table role="grid">` with `aria-sort`, `aria-selected`,
1127
- `aria-rowcount`, and `aria-rowindex`.
1128
-
1129
- ```tsx
1130
- <DataGrid
1131
- aria-label="Roster"
1132
- rows={players}
1133
- columns={[
1134
- { key: "name", header: "Player", sortable: true, rowHeader: true },
1135
- { key: "ppg", header: "PPG", sortable: true, numeric: true },
1136
- ]}
1137
- selectionMode="multiple"
1138
- pageSize={10}
1139
- />
1181
+ An interactive data table (WAI-ARIA APG "Data Grid"): sortable columns
1182
+ (with Shift for multi-column sorts), search filtering, single or
1183
+ multiple row selection, pagination, and a full keyboard model. Works with
1184
+ client-side data, or server-side data with `manual`.
1185
+
1186
+ Keyboard: the grid is one Tab stop. Arrow keys move between cells
1187
+ (mirrored in RTL), Home / End go to the row's first or last cell, Ctrl+Home
1188
+ / Ctrl+End to the first or last cell of the grid, Page Up / Page Down move
1189
+ ten rows. On a header, Enter sorts (Shift+Enter adds a secondary sort).
1190
+ Space selects the row, Shift+click or Shift+Space selects a range, and
1191
+ Ctrl/⌘+A selects every row. A cell with a single button, link, or
1192
+ checkbox focuses it directly; for cells with several controls or a text
1193
+ field, Enter or F2 moves into the cell, Tab cycles inside it, and Escape
1194
+ returns to the grid.
1195
+
1196
+ Screen readers hear sort changes and filter result counts; the grid is a
1197
+ real `<table role="grid">` with `aria-sort`, `aria-selected`,
1198
+ `aria-rowcount`, and `aria-rowindex`.
1199
+
1200
+ ```tsx
1201
+ <DataGrid
1202
+ aria-label="Roster"
1203
+ rows={players}
1204
+ columns={[
1205
+ { key: "name", header: "Player", sortable: true, rowHeader: true },
1206
+ { key: "ppg", header: "PPG", sortable: true, numeric: true },
1207
+ ]}
1208
+ selectionMode="multiple"
1209
+ pageSize={10}
1210
+ />
1140
1211
  ```
1141
1212
 
1142
1213
  ### Props
@@ -1816,25 +1887,25 @@ import { VirtualList } from "@vjdesigns/vjdesigns-ui";
1816
1887
 
1817
1888
  Category: data-display · Storybook: Data display/VirtualList
1818
1889
 
1819
- A list that renders only the items in view, so tens of thousands of rows
1820
- scroll smoothly. Items may have any height (they're measured as they
1821
- render). Built on `useVirtualizer` and `ScrollArea`.
1822
-
1823
- Accessibility: a real `role="list"` whose items carry `aria-setsize` and
1824
- `aria-posinset`, so screen readers announce "item 4,812 of 10,000" even
1825
- though only a few dozen exist in the page. The scroll area is keyboard
1826
- scrollable, and Tab through items' links and buttons keeps working (new
1827
- items render as focus scrolls them into view). Browser find-in-page only
1828
- sees rendered items, so offer search for long lists.
1829
-
1830
- ```tsx
1831
- <VirtualList
1832
- aria-label="All players"
1833
- items={players}
1834
- height="24rem"
1835
- getKey={(p) => p.id}
1836
- renderItem={(p) => <PlayerRow player={p} />}
1837
- />
1890
+ A list that renders only the items in view, so tens of thousands of rows
1891
+ scroll smoothly. Items may have any height (they're measured as they
1892
+ render). Built on `useVirtualizer` and `ScrollArea`.
1893
+
1894
+ Accessibility: a real `role="list"` whose items carry `aria-setsize` and
1895
+ `aria-posinset`, so screen readers announce "item 4,812 of 10,000" even
1896
+ though only a few dozen exist in the page. The scroll area is keyboard
1897
+ scrollable, and Tab through items' links and buttons keeps working (new
1898
+ items render as focus scrolls them into view). Browser find-in-page only
1899
+ sees rendered items, so offer search for long lists.
1900
+
1901
+ ```tsx
1902
+ <VirtualList
1903
+ aria-label="All players"
1904
+ items={players}
1905
+ height="24rem"
1906
+ getKey={(p) => p.id}
1907
+ renderItem={(p) => <PlayerRow player={p} />}
1908
+ />
1838
1909
  ```
1839
1910
 
1840
1911
  ### Props
@@ -1969,7 +2040,7 @@ Accessibility:
1969
2040
 
1970
2041
  - Playground
1971
2042
  - Tones: Every tone, solid. `accent` is the promo strip, always with dark text.
1972
- - Volt Promo: The accent promo strip: bold, dark text on accent, with a strong call to action.
2043
+ - Accent Promo: The accent promo strip: bold, dark text on accent, with a strong call to action.
1973
2044
  - Variants: Solid and soft side by side for every tone. Check both themes.
1974
2045
  - Sizes: Small, medium, and large. Buttons inside are one step smaller than the strip.
1975
2046
  - With Actions: Buttons get a verified default look for the strip. Links in the message use `tone="neutral"` so they take the strip's text color and stay underlined.
@@ -2013,15 +2084,15 @@ import { ErrorSummary } from "@vjdesigns/vjdesigns-ui";
2013
2084
 
2014
2085
  Category: feedback · Storybook: Feedback/ErrorSummary
2015
2086
 
2016
- The list of a form's errors, shown above it after a failed submit: a danger
2017
- `Alert` whose items link to their fields (activating one moves focus to the
2018
- field). It's focusable (`tabIndex={-1}`): give `useFormValidation`'s
2019
- `summaryRef` as `ref` and a failed submit moves focus here, so screen
2020
- readers announce the heading and the list. The title is a real heading
2021
- (`h2` by default; set `headingLevel` to fit the page outline).
2022
-
2023
- ```tsx
2024
- <ErrorSummary ref={form.summaryRef} errors={form.errorList} />
2087
+ The list of a form's errors, shown above it after a failed submit: a danger
2088
+ `Alert` whose items link to their fields (activating one moves focus to the
2089
+ field). It's focusable (`tabIndex={-1}`): give `useFormValidation`'s
2090
+ `summaryRef` as `ref` and a failed submit moves focus here, so screen
2091
+ readers announce the heading and the list. The title is a real heading
2092
+ (`h2` by default; set `headingLevel` to fit the page outline).
2093
+
2094
+ ```tsx
2095
+ <ErrorSummary ref={form.summaryRef} errors={form.errorList} />
2025
2096
  ```
2026
2097
 
2027
2098
  ### Props
@@ -2519,16 +2590,16 @@ import { ColorInput } from "@vjdesigns/vjdesigns-ui";
2519
2590
 
2520
2591
  Category: forms · Storybook: Forms/ColorPicker
2521
2592
 
2522
- A pill field showing a swatch and the color value; pressing it opens a
2523
- `ColorPicker` in a `Popover` (a non-modal dialog on the shared overlay
2524
- layer stack: Escape and outside presses close it, focus returns to the
2525
- field). Takes the same value, format, alpha, swatch, and state props as
2526
- `ColorPicker`.
2527
-
2528
- The trigger is a native `<button>` with `aria-haspopup="dialog"` and
2529
- `aria-expanded`. Its name is the label plus the current value and a plain
2530
- description ("Jersey color, #1d4ed8, dark blue"). Inside a `Field`, the
2531
- label, hint, error, invalid, disabled, and read-only state are wired to
2593
+ A pill field showing a swatch and the color value; pressing it opens a
2594
+ `ColorPicker` in a `Popover` (a non-modal dialog on the shared overlay
2595
+ layer stack: Escape and outside presses close it, focus returns to the
2596
+ field). Takes the same value, format, alpha, swatch, and state props as
2597
+ `ColorPicker`.
2598
+
2599
+ The trigger is a native `<button>` with `aria-haspopup="dialog"` and
2600
+ `aria-expanded`. Its name is the label plus the current value and a plain
2601
+ description ("Jersey color, #1d4ed8, dark blue"). Inside a `Field`, the
2602
+ label, hint, error, invalid, disabled, and read-only state are wired to
2532
2603
  it. A read-only field still opens, so the values can be inspected.
2533
2604
 
2534
2605
  ### Props
@@ -2729,22 +2800,22 @@ import { DatePicker } from "@vjdesigns/vjdesigns-ui";
2729
2800
 
2730
2801
  Category: forms · Storybook: Forms/DatePicker
2731
2802
 
2732
- A date field: type a date in the locale's short format (or ISO), or press
2733
- the calendar button to pick one from a `Calendar` in a popup dialog.
2734
-
2735
- The value is an ISO date string ("YYYY-MM-DD"), so it never shifts with
2736
- time zones. The text field shows it in the locale's numeric format; on
2737
- blur (or Enter), typed text is parsed and normalized, or rejected with an
2738
- inline error. Inside a `Field`, label, hint, error, required, disabled,
2739
- and invalid state are wired to the text input.
2740
-
2741
- The popup is a modal `Popover` on the shared overlay layer stack, so
2742
- Escape closes only the calendar, also inside a `Modal` or `Drawer`.
2743
-
2744
- ```tsx
2745
- <Field label="Game day">
2746
- <DatePicker name="gameDay" min="2026-10-01" onValueChange={setDay} />
2747
- </Field>
2803
+ A date field: type a date in the locale's short format (or ISO), or press
2804
+ the calendar button to pick one from a `Calendar` in a popup dialog.
2805
+
2806
+ The value is an ISO date string ("YYYY-MM-DD"), so it never shifts with
2807
+ time zones. The text field shows it in the locale's numeric format; on
2808
+ blur (or Enter), typed text is parsed and normalized, or rejected with an
2809
+ inline error. Inside a `Field`, label, hint, error, required, disabled,
2810
+ and invalid state are wired to the text input.
2811
+
2812
+ The popup is a modal `Popover` on the shared overlay layer stack, so
2813
+ Escape closes only the calendar, also inside a `Modal` or `Drawer`.
2814
+
2815
+ ```tsx
2816
+ <Field label="Game day">
2817
+ <DatePicker name="gameDay" min="2026-10-01" onValueChange={setDay} />
2818
+ </Field>
2748
2819
  ```
2749
2820
 
2750
2821
  ### Props
@@ -2816,25 +2887,25 @@ import { DateRangePicker } from "@vjdesigns/vjdesigns-ui";
2816
2887
 
2817
2888
  Category: forms · Storybook: Forms/DatePicker
2818
2889
 
2819
- A date range field: two text fields (start and end) typed in the locale's
2820
- short format, plus a calendar button that opens a range `Calendar` (two
2821
- months by default) in a modal popup.
2822
-
2823
- The value is `{ start, end }` with ISO dates ("YYYY-MM-DD"). In the
2824
- calendar, the first pick sets the start and the second the end; the range
2825
- is committed (and the popup closes) only when both are picked, so Escape
2826
- after one pick changes nothing. Typed dates are validated like
2827
- `DatePicker`'s, plus an end before the start is rejected.
2828
-
2829
- Accessibility: the pair is a `role="group"` named by the `Field` label
2830
- (or `aria-label`); each field is named by that label plus its own
2831
- visible label ("Season dates, Start date"). Field hint, error, required, disabled, and
2832
- invalid state reach both fields. Errors are announced politely.
2833
-
2834
- ```tsx
2835
- <Field label="Season dates">
2836
- <DateRangePicker startName="from" endName="to" onValueChange={setRange} />
2837
- </Field>
2890
+ A date range field: two text fields (start and end) typed in the locale's
2891
+ short format, plus a calendar button that opens a range `Calendar` (two
2892
+ months by default) in a modal popup.
2893
+
2894
+ The value is `{ start, end }` with ISO dates ("YYYY-MM-DD"). In the
2895
+ calendar, the first pick sets the start and the second the end; the range
2896
+ is committed (and the popup closes) only when both are picked, so Escape
2897
+ after one pick changes nothing. Typed dates are validated like
2898
+ `DatePicker`'s, plus an end before the start is rejected.
2899
+
2900
+ Accessibility: the pair is a `role="group"` named by the `Field` label
2901
+ (or `aria-label`); each field is named by that label plus its own
2902
+ visible label ("Season dates, Start date"). Field hint, error, required, disabled, and
2903
+ invalid state reach both fields. Errors are announced politely.
2904
+
2905
+ ```tsx
2906
+ <Field label="Season dates">
2907
+ <DateRangePicker startName="from" endName="to" onValueChange={setRange} />
2908
+ </Field>
2838
2909
  ```
2839
2910
 
2840
2911
  ### Props
@@ -2881,25 +2952,25 @@ import { DateTimePicker } from "@vjdesigns/vjdesigns-ui";
2881
2952
 
2882
2953
  Category: forms · Storybook: Forms/DateTimePicker
2883
2954
 
2884
- A date and a time together: a `DatePicker` and a `TimePicker` side by
2885
- side, with one value, "YYYY-MM-DDTHH:mm" (the format of a native
2886
- `datetime-local` input, with no time zone). Each half keeps its full
2887
- behavior: typed dates in the locale's format or the calendar popup, typed
2888
- times in any common form or the list of times.
2889
-
2890
- `min` and `max` limit the calendar by their dates, and the time list by
2891
- their times on those boundary dates. `defaultTime` fills the time when a
2892
- date is picked first.
2893
-
2894
- Accessibility: the pair is a `role="group"` named by the `Field` label
2895
- (or `aria-label`); each field is named by that label plus its own
2896
- visible label ("Tip-off, Date", "Tip-off, Time"). Field hint, error,
2897
- required, disabled, and invalid state reach both.
2898
-
2899
- ```tsx
2900
- <Field label="Tip-off">
2901
- <DateTimePicker name="tipoff" defaultTime="19:00" step={15} />
2902
- </Field>
2955
+ A date and a time together: a `DatePicker` and a `TimePicker` side by
2956
+ side, with one value, "YYYY-MM-DDTHH:mm" (the format of a native
2957
+ `datetime-local` input, with no time zone). Each half keeps its full
2958
+ behavior: typed dates in the locale's format or the calendar popup, typed
2959
+ times in any common form or the list of times.
2960
+
2961
+ `min` and `max` limit the calendar by their dates, and the time list by
2962
+ their times on those boundary dates. `defaultTime` fills the time when a
2963
+ date is picked first.
2964
+
2965
+ Accessibility: the pair is a `role="group"` named by the `Field` label
2966
+ (or `aria-label`); each field is named by that label plus its own
2967
+ visible label ("Tip-off, Date", "Tip-off, Time"). Field hint, error,
2968
+ required, disabled, and invalid state reach both.
2969
+
2970
+ ```tsx
2971
+ <Field label="Tip-off">
2972
+ <DateTimePicker name="tipoff" defaultTime="19:00" step={15} />
2973
+ </Field>
2903
2974
  ```
2904
2975
 
2905
2976
  ### Props
@@ -3586,10 +3657,10 @@ import { Rating } from "@vjdesigns/vjdesigns-ui";
3586
3657
 
3587
3658
  Category: forms · Storybook: Forms/Rating
3588
3659
 
3589
- An editable star (or custom icon) rating, built on real native radios
3590
- (visually hidden but focusable, sharing one `name`) inside
3591
- `role="radiogroup"`. Tab lands on the selected value (or the first option),
3592
- arrow keys move and select, and the value submits with a form. Supports
3660
+ An editable star (or custom icon) rating, built on real native radios
3661
+ (visually hidden but focusable, sharing one `name`) inside
3662
+ `role="radiogroup"`. Tab lands on the selected value (or the first option),
3663
+ arrow keys move and select, and the value submits with a form. Supports
3593
3664
  half values, clearing, hover preview, and Field/Fieldset wiring.
3594
3665
 
3595
3666
  ### Props
@@ -3645,9 +3716,9 @@ import { RatingDisplay } from "@vjdesigns/vjdesigns-ui";
3645
3716
 
3646
3717
  Category: forms · Storybook: Forms/Rating
3647
3718
 
3648
- A read-only rating: a row of icons with fractional fill, exposed to
3649
- assistive tech as a single `role="img"` named "Rated 4.5 out of 5",
3650
- optionally followed by the value ("4.5 / 5") and a review count.
3719
+ A read-only rating: a row of icons with fractional fill, exposed to
3720
+ assistive tech as a single `role="img"` named "Rated 4.5 out of 5",
3721
+ optionally followed by the value ("4.5 / 5") and a review count.
3651
3722
  Not focusable. For an editable rating use `Rating`.
3652
3723
 
3653
3724
  ### Props
@@ -3994,29 +4065,29 @@ import { TagInput } from "@vjdesigns/vjdesigns-ui";
3994
4065
 
3995
4066
  Category: forms · Storybook: Forms/TagInput
3996
4067
 
3997
- A text field that turns entries into removable tags (chips). Type and
3998
- press Enter (or a delimiter such as a comma) to add; paste "a, b, c" to
3999
- add several at once.
4000
-
4001
- Keyboard:
4002
- - Enter / delimiters (and Tab with `addOnTab`): add the typed text.
4003
- - Backspace in an empty input, or Left Arrow with the caret at the
4004
- start: move focus to the last tag's remove button. A second Backspace
4005
- (or Delete) removes that tag and focus returns to the next tag, or the
4006
- input. This two-step pattern avoids deleting a tag by accident and
4007
- lets screen readers announce which tag is about to be removed.
4008
- - On a tag: Left / Right Arrow move between tags and back to the input;
4009
- Home / End jump to the first tag / the input; Backspace or Delete removes.
4010
-
4011
- Inside a `Field`, the label, hint, error, required, disabled, read-only,
4012
- and size are wired automatically. `ref` points at the `<input>`;
4013
- native input props are spread onto it, while `className` and `style` go
4014
- to the outer wrapper.
4015
-
4016
- ```tsx
4017
- <Field label="Skills">
4018
- <TagInput defaultValue={["Defense"]} placeholder="Add a skill" />
4019
- </Field>
4068
+ A text field that turns entries into removable tags (chips). Type and
4069
+ press Enter (or a delimiter such as a comma) to add; paste "a, b, c" to
4070
+ add several at once.
4071
+
4072
+ Keyboard:
4073
+ - Enter / delimiters (and Tab with `addOnTab`): add the typed text.
4074
+ - Backspace in an empty input, or Left Arrow with the caret at the
4075
+ start: move focus to the last tag's remove button. A second Backspace
4076
+ (or Delete) removes that tag and focus returns to the next tag, or the
4077
+ input. This two-step pattern avoids deleting a tag by accident and
4078
+ lets screen readers announce which tag is about to be removed.
4079
+ - On a tag: Left / Right Arrow move between tags and back to the input;
4080
+ Home / End jump to the first tag / the input; Backspace or Delete removes.
4081
+
4082
+ Inside a `Field`, the label, hint, error, required, disabled, read-only,
4083
+ and size are wired automatically. `ref` points at the `<input>`;
4084
+ native input props are spread onto it, while `className` and `style` go
4085
+ to the outer wrapper.
4086
+
4087
+ ```tsx
4088
+ <Field label="Skills">
4089
+ <TagInput defaultValue={["Defense"]} placeholder="Add a skill" />
4090
+ </Field>
4020
4091
  ```
4021
4092
 
4022
4093
  ### Props
@@ -4125,23 +4196,23 @@ import { TimePicker } from "@vjdesigns/vjdesigns-ui";
4125
4196
 
4126
4197
  Category: forms · Storybook: Forms/TimePicker
4127
4198
 
4128
- A time field: type a time in almost any form ("2:30 pm", "1430", "230p",
4129
- "14h30", the locale's own format), or pick one from a list of times
4130
- `step` minutes apart. Built on `Combobox`, so the list follows the
4131
- WAI-ARIA combobox pattern: ArrowDown opens it on the selected time,
4132
- arrows move, Enter picks, Escape closes, and typing filters it.
4133
-
4134
- The value is a 24-hour "HH:mm" string, the same as a native
4135
- `<input type="time">`, so it never shifts with time zones. The field
4136
- shows it in the locale's clock (12- or 24-hour, override with
4137
- `hourCycle`). On blur (or Enter), typed text is parsed and normalized, or
4138
- rejected with an inline, announced error. Inside a `Field`, label, hint,
4139
- error, required, disabled, and invalid state are wired to the input.
4140
-
4141
- ```tsx
4142
- <Field label="Tip-off">
4143
- <TimePicker name="tipoff" min="08:00" max="22:00" step={15} />
4144
- </Field>
4199
+ A time field: type a time in almost any form ("2:30 pm", "1430", "230p",
4200
+ "14h30", the locale's own format), or pick one from a list of times
4201
+ `step` minutes apart. Built on `Combobox`, so the list follows the
4202
+ WAI-ARIA combobox pattern: ArrowDown opens it on the selected time,
4203
+ arrows move, Enter picks, Escape closes, and typing filters it.
4204
+
4205
+ The value is a 24-hour "HH:mm" string, the same as a native
4206
+ `<input type="time">`, so it never shifts with time zones. The field
4207
+ shows it in the locale's clock (12- or 24-hour, override with
4208
+ `hourCycle`). On blur (or Enter), typed text is parsed and normalized, or
4209
+ rejected with an inline, announced error. Inside a `Field`, label, hint,
4210
+ error, required, disabled, and invalid state are wired to the input.
4211
+
4212
+ ```tsx
4213
+ <Field label="Tip-off">
4214
+ <TimePicker name="tipoff" min="08:00" max="22:00" step={15} />
4215
+ </Field>
4145
4216
  ```
4146
4217
 
4147
4218
  ### Props
@@ -4689,24 +4760,24 @@ import { ScrollArea } from "@vjdesigns/vjdesigns-ui";
4689
4760
 
4690
4761
  Category: layout · Storybook: Layout/ScrollArea
4691
4762
 
4692
- A scrolling region with slim, on-brand scrollbars that work the same in
4693
- every browser. Scrolling itself stays native (wheel, touch, trackpad
4694
- momentum, keyboard, find-in-page, scroll anchoring); only the look of the
4695
- bars is custom. The thumbs can be dragged, and pressing the track pages
4696
- toward the press.
4697
-
4698
- Accessibility: while the content overflows, the viewport is focusable
4699
- (`tabIndex=0`) so keyboard users can scroll it with the arrow keys, Page
4700
- Up / Page Down, and Home / End. Give it a name with `aria-label` or
4701
- `aria-labelledby`, which also makes it a `region` landmark. The custom
4702
- bars are hidden from assistive technology (they duplicate native
4703
- scrolling). Edge shadows show there is more to see. In Windows High
4704
- Contrast mode the native scrollbars come back.
4705
-
4706
- ```tsx
4707
- <ScrollArea maxHeight="20rem" aria-label="Roster">
4708
- <List>…</List>
4709
- </ScrollArea>
4763
+ A scrolling region with slim, on-brand scrollbars that work the same in
4764
+ every browser. Scrolling itself stays native (wheel, touch, trackpad
4765
+ momentum, keyboard, find-in-page, scroll anchoring); only the look of the
4766
+ bars is custom. The thumbs can be dragged, and pressing the track pages
4767
+ toward the press.
4768
+
4769
+ Accessibility: while the content overflows, the viewport is focusable
4770
+ (`tabIndex=0`) so keyboard users can scroll it with the arrow keys, Page
4771
+ Up / Page Down, and Home / End. Give it a name with `aria-label` or
4772
+ `aria-labelledby`, which also makes it a `region` landmark. The custom
4773
+ bars are hidden from assistive technology (they duplicate native
4774
+ scrolling). Edge shadows show there is more to see. In Windows High
4775
+ Contrast mode the native scrollbars come back.
4776
+
4777
+ ```tsx
4778
+ <ScrollArea maxHeight="20rem" aria-label="Roster">
4779
+ <List>…</List>
4780
+ </ScrollArea>
4710
4781
  ```
4711
4782
 
4712
4783
  ### Props
@@ -4857,32 +4928,32 @@ import { ContextMenu } from "@vjdesigns/vjdesigns-ui";
4857
4928
 
4858
4929
  Category: navigation · Storybook: Navigation/ContextMenu
4859
4930
 
4860
- A menu that opens at the pointer on right-click (or a long press on touch
4861
- screens), for actions on the thing under the pointer. It is a `Menu` in
4862
- every other way: compose it from the same items (`ContextMenu.Item`,
4863
- `.CheckboxItem`, `.RadioGroup` / `.RadioItem`, `.Group` / `.Label`,
4864
- `.Separator`, and submenus), with the same keyboard model (arrows, Home /
4865
- End, typeahead, Escape, Tab) and RTL support.
4866
-
4867
- Keyboard: with focus inside the trigger area, Shift+F10 or the Menu key
4868
- opens the menu next to the focused element and focuses its first item.
4869
- On close, focus goes back to where it was.
4870
-
4871
- Right-click menus are hard to discover, and many touch screens and
4872
- assistive technologies never open them, so offer the same actions
4873
- somewhere visible too (a `Menu` button, a toolbar). Treat the context
4874
- menu as a shortcut, never the only way.
4875
-
4876
- ```tsx
4877
- <ContextMenu>
4878
- <ContextMenu.Trigger>
4879
- <Card tabIndex={0}>Game film</Card>
4880
- </ContextMenu.Trigger>
4881
- <ContextMenu.Content>
4882
- <ContextMenu.Item onSelect={rename}>Rename</ContextMenu.Item>
4883
- <ContextMenu.Item tone="danger" onSelect={remove}>Delete</ContextMenu.Item>
4884
- </ContextMenu.Content>
4885
- </ContextMenu>
4931
+ A menu that opens at the pointer on right-click (or a long press on touch
4932
+ screens), for actions on the thing under the pointer. It is a `Menu` in
4933
+ every other way: compose it from the same items (`ContextMenu.Item`,
4934
+ `.CheckboxItem`, `.RadioGroup` / `.RadioItem`, `.Group` / `.Label`,
4935
+ `.Separator`, and submenus), with the same keyboard model (arrows, Home /
4936
+ End, typeahead, Escape, Tab) and RTL support.
4937
+
4938
+ Keyboard: with focus inside the trigger area, Shift+F10 or the Menu key
4939
+ opens the menu next to the focused element and focuses its first item.
4940
+ On close, focus goes back to where it was.
4941
+
4942
+ Right-click menus are hard to discover, and many touch screens and
4943
+ assistive technologies never open them, so offer the same actions
4944
+ somewhere visible too (a `Menu` button, a toolbar). Treat the context
4945
+ menu as a shortcut, never the only way.
4946
+
4947
+ ```tsx
4948
+ <ContextMenu>
4949
+ <ContextMenu.Trigger>
4950
+ <Card tabIndex={0}>Game film</Card>
4951
+ </ContextMenu.Trigger>
4952
+ <ContextMenu.Content>
4953
+ <ContextMenu.Item onSelect={rename}>Rename</ContextMenu.Item>
4954
+ <ContextMenu.Item tone="danger" onSelect={remove}>Delete</ContextMenu.Item>
4955
+ </ContextMenu.Content>
4956
+ </ContextMenu>
4886
4957
  ```
4887
4958
 
4888
4959
  ### Props
@@ -4900,8 +4971,8 @@ menu as a shortcut, never the only way.
4900
4971
 
4901
4972
  Also exported as `ContextMenuTrigger`.
4902
4973
 
4903
- The area that opens the menu: right-click, a touch long press, or
4904
- Shift+F10 / the Menu key while focus is inside it. Make the area (or the
4974
+ The area that opens the menu: right-click, a touch long press, or
4975
+ Shift+F10 / the Menu key while focus is inside it. Make the area (or the
4905
4976
  items in it) focusable so keyboard users can reach the menu.
4906
4977
 
4907
4978
  - `children`: `ReactNode`. The area that answers right-clicks: plain content (wrapped in a `div`) or a single element.
@@ -4912,8 +4983,8 @@ items in it) focusable so keyboard users can reach the menu.
4912
4983
 
4913
4984
  Also exported as `ContextMenuContent`.
4914
4985
 
4915
- The floating menu (`role="menu"`), opened at the pointer. Takes the same
4916
- props as `Menu.Content`; `placement` is relative to the pointer, and
4986
+ The floating menu (`role="menu"`), opened at the pointer. Takes the same
4987
+ props as `Menu.Content`; `placement` is relative to the pointer, and
4917
4988
  flips and clamps to stay on screen. @see MenuContent
4918
4989
 
4919
4990
  - `children`: `ReactNode`. Menu items, groups, separators, and submenus.
@@ -5130,30 +5201,30 @@ import { Menu } from "@vjdesigns/vjdesigns-ui";
5130
5201
 
5131
5202
  Category: navigation · Storybook: Navigation/Menu
5132
5203
 
5133
- A dropdown menu of actions or options, opened from a button (WAI-ARIA APG
5134
- "Menu Button" and "Menu" patterns). Compose it from `Menu.Trigger`,
5135
- `Menu.Content`, `Menu.Item`, `Menu.CheckboxItem`, `Menu.RadioGroup` /
5136
- `Menu.RadioItem`, `Menu.Group` / `Menu.Label`, `Menu.Separator`, and
5137
- submenus (`Menu.Sub`, `Menu.SubTrigger`, `Menu.SubContent`).
5138
-
5139
- Keyboard: Enter, Space, or ArrowDown on the trigger opens the menu and
5140
- focuses the first item (ArrowUp: the last). Inside, ArrowUp/ArrowDown move
5141
- (wrapping), Home/End jump, typing a character jumps to the next matching
5142
- item, Enter/Space activates, ArrowRight opens a submenu and ArrowLeft
5143
- closes it (mirrored in RTL), Escape closes the current level and returns
5144
- focus to its trigger, and Tab closes the whole menu.
5145
-
5146
- Disabled items stay focusable (`aria-disabled="true"`) so keyboard and
5147
- screen reader users can still discover them, but can't be activated.
5148
-
5149
- ```tsx
5150
- <Menu>
5151
- <Menu.Trigger>Actions</Menu.Trigger>
5152
- <Menu.Content>
5153
- <Menu.Item onSelect={edit}>Edit</Menu.Item>
5154
- <Menu.Item tone="danger" onSelect={remove}>Delete</Menu.Item>
5155
- </Menu.Content>
5156
- </Menu>
5204
+ A dropdown menu of actions or options, opened from a button (WAI-ARIA APG
5205
+ "Menu Button" and "Menu" patterns). Compose it from `Menu.Trigger`,
5206
+ `Menu.Content`, `Menu.Item`, `Menu.CheckboxItem`, `Menu.RadioGroup` /
5207
+ `Menu.RadioItem`, `Menu.Group` / `Menu.Label`, `Menu.Separator`, and
5208
+ submenus (`Menu.Sub`, `Menu.SubTrigger`, `Menu.SubContent`).
5209
+
5210
+ Keyboard: Enter, Space, or ArrowDown on the trigger opens the menu and
5211
+ focuses the first item (ArrowUp: the last). Inside, ArrowUp/ArrowDown move
5212
+ (wrapping), Home/End jump, typing a character jumps to the next matching
5213
+ item, Enter/Space activates, ArrowRight opens a submenu and ArrowLeft
5214
+ closes it (mirrored in RTL), Escape closes the current level and returns
5215
+ focus to its trigger, and Tab closes the whole menu.
5216
+
5217
+ Disabled items stay focusable (`aria-disabled="true"`) so keyboard and
5218
+ screen reader users can still discover them, but can't be activated.
5219
+
5220
+ ```tsx
5221
+ <Menu>
5222
+ <Menu.Trigger>Actions</Menu.Trigger>
5223
+ <Menu.Content>
5224
+ <Menu.Item onSelect={edit}>Edit</Menu.Item>
5225
+ <Menu.Item tone="danger" onSelect={remove}>Delete</Menu.Item>
5226
+ </Menu.Content>
5227
+ </Menu>
5157
5228
  ```
5158
5229
 
5159
5230
  ### Props
@@ -5169,8 +5240,8 @@ screen reader users can still discover them, but can't be activated.
5169
5240
 
5170
5241
  Also exported as `MenuTrigger`.
5171
5242
 
5172
- The button that opens the menu. Adds `aria-haspopup="menu"`,
5173
- `aria-expanded`, and `aria-controls`. Pass a single `Button`/`IconButton`
5243
+ The button that opens the menu. Adds `aria-haspopup="menu"`,
5244
+ `aria-expanded`, and `aria-controls`. Pass a single `Button`/`IconButton`
5174
5245
  to use it as-is, or plain content to get a default `Button`.
5175
5246
 
5176
5247
  - `variant`: `ButtonVariant` (default "solid"). Visual style. Inherits from a surrounding `ButtonGroup`.
@@ -6266,27 +6337,27 @@ import { HoverCard } from "@vjdesigns/vjdesigns-ui";
6266
6337
 
6267
6338
  Category: overlays · Storybook: Overlays/HoverCard
6268
6339
 
6269
- A preview card that opens while the pointer rests on a link or button, or
6270
- while it has keyboard focus: a player's profile behind their name, a page
6271
- preview behind a link. Unlike `Tooltip`, the card can hold rich and
6272
- interactive content (links, buttons).
6273
-
6274
- Meets WCAG 1.4.13: the pointer can move onto the card, it stays open
6275
- while hovered or focused, and Escape closes it. Keyboard users get it
6276
- too: focusing the trigger opens it, Tab moves into the card's links, and
6277
- Tab past the last one continues to whatever follows the trigger.
6278
-
6279
- Touch screens have no hover, so the card never opens from a tap (the tap
6280
- follows the link). Treat it as a preview: everything in it must also be
6281
- reachable from the page the trigger leads to.
6282
-
6283
- ```tsx
6284
- <HoverCard>
6285
- <HoverCard.Trigger>
6286
- <Link href="/players/23">Jordan Reyes</Link>
6287
- </HoverCard.Trigger>
6288
- <HoverCard.Content>…profile…</HoverCard.Content>
6289
- </HoverCard>
6340
+ A preview card that opens while the pointer rests on a link or button, or
6341
+ while it has keyboard focus: a player's profile behind their name, a page
6342
+ preview behind a link. Unlike `Tooltip`, the card can hold rich and
6343
+ interactive content (links, buttons).
6344
+
6345
+ Meets WCAG 1.4.13: the pointer can move onto the card, it stays open
6346
+ while hovered or focused, and Escape closes it. Keyboard users get it
6347
+ too: focusing the trigger opens it, Tab moves into the card's links, and
6348
+ Tab past the last one continues to whatever follows the trigger.
6349
+
6350
+ Touch screens have no hover, so the card never opens from a tap (the tap
6351
+ follows the link). Treat it as a preview: everything in it must also be
6352
+ reachable from the page the trigger leads to.
6353
+
6354
+ ```tsx
6355
+ <HoverCard>
6356
+ <HoverCard.Trigger>
6357
+ <Link href="/players/23">Jordan Reyes</Link>
6358
+ </HoverCard.Trigger>
6359
+ <HoverCard.Content>…profile…</HoverCard.Content>
6360
+ </HoverCard>
6290
6361
  ```
6291
6362
 
6292
6363
  ### Props
@@ -6310,7 +6381,7 @@ The link or button that opens the card. Its own role and name are left untouched
6310
6381
 
6311
6382
  Also exported as `HoverCardContent`.
6312
6383
 
6313
- The floating card. Rendered in a portal, positioned against the trigger
6384
+ The floating card. Rendered in a portal, positioned against the trigger
6314
6385
  with flip and viewport clamping, and mounted only while open.
6315
6386
 
6316
6387
  - `placement`: `Placement` (default "bottom"). Preferred side and alignment. Flips to the other side when there isn't room.
@@ -6730,6 +6801,7 @@ is passed.
6730
6801
  - With Icons
6731
6802
  - Loading: The button keeps its width, sets `aria-busy`, ignores clicks, and stays focusable.
6732
6803
  - Disabled
6804
+ - Disabled but focusable: `aria-disabled="true"` instead of `disabled`: the button looks and acts unavailable (clicks are ignored) but stays focusable, so a tooltip or a countdown ("Resend code in 0:24") can still be reached and read.
6733
6805
  - As Router Link: `as` renders your router's link with the button's styling, and its props (here `to`) are typed. While disabled or loading it becomes a plain `<a aria-disabled="true">`, since a router link can't render without a destination.
6734
6806
  - As Link: Passing `href` renders an `<a>` with the same styling. Disabled links drop `href` and get `aria-disabled`.
6735
6807
  - Full Width
@@ -7064,12 +7136,12 @@ any part of it; themes nest (a blue area inside a purple app, a dark panel
7064
7136
  on a light page). Overlays opened inside (Popover, Menu, Modal, Tooltip,
7065
7137
  Toast, ...) render elsewhere in the page but keep this theme. For the whole
7066
7138
  page you can instead set the attributes on `<html>`:
7067
- `<html data-theme="dark" data-brand="purple" data-accent="volt">`.
7139
+ `<html data-theme="dark" data-brand="purple" data-accent="lime">`.
7068
7140
 
7069
7141
  ### Props
7070
7142
 
7071
7143
  - `brand`: `ThemeBrand | BrandColor`. Brand palette for everything inside: buttons, links, focus rings, selection, and indicators all switch at once. Leave unset to inherit. A preset name, or any hex color (`"#e11d48"`) for a custom palette generated in both modes. One hex is used as given in the mode where it meets WCAG AA (dark colors in light mode, light colors in dark mode) and the other mode gets a matching shade; pass `{ light, dark }` to choose both. A color that fails contrast is shifted lighter or darker (same hue) just enough to pass, with a console warning in development.
7072
- - `accent`: `ThemeAccent | BrandColor`. The accent: the energetic highlight on badges, switches, progress bars, sliders, steppers, and active navigation. Leave unset to inherit; with no accent set anywhere, each brand uses its own (a bright shade of its hue). `"volt"` for the original yellow-green, `"brand"` to go back to the brand's own inside an area with another accent, or any hex color for a custom accent. Accents always carry dark text, so a color too dark for that is lightened (same hue), with a console warning in development. An accent set here also holds inside nested brands.
7144
+ - `accent`: `ThemeAccent | BrandColor`. The accent: the energetic highlight on badges, switches, progress bars, sliders, steppers, and active navigation. Leave unset to inherit; with no accent set anywhere, each brand uses its own (a bright shade of its hue). `"lime"` for the original bright yellow-green, `"brand"` to go back to the brand's own inside an area with another accent, or any hex color for a custom accent. Accents always carry dark text, so a color too dark for that is lightened (same hue), with a console warning in development. An accent set here also holds inside nested brands.
7073
7145
  - `mode`: `ThemeMode`. Color mode for everything inside. Leave unset to inherit.
7074
7146
  - `fill`: `boolean` (default false). Paint the page background and text color of the theme on this wrapper (useful when `mode` differs from the page around it).
7075
7147
  - Plus every native `<div>` attribute (className, style, id, aria-*, data-*, event handlers), spread onto the root.
@@ -7084,7 +7156,7 @@ page you can instead set the attributes on `<html>`:
7084
7156
  - Custom colors per mode: `{ light, dark }` picks the color for each mode; each is still checked for contrast.
7085
7157
  - Custom colors in overlays: Overlays opened inside a custom-colored Theme keep the generated palette.
7086
7158
  - Theme builder: Pick any color and see the generated palette in both modes, with code to copy.
7087
- - Accents: The accent (badges, switches, progress, sliders, active navigation) follows the brand by default: a bright shade of its hue, always with dark text. `accent="volt"` restores the original yellow-green, and any hex makes a custom accent (kept accessible: too-dark colors are lightened).
7159
+ - Accents: The accent (badges, switches, progress, sliders, active navigation) follows the brand by default: a bright shade of its hue, always with dark text. `accent="lime"` restores the original bright yellow-green, and any hex makes a custom accent (kept accessible: too-dark colors are lightened).
7088
7160
  - Accent nesting: An explicit accent holds inside nested brands; `accent="brand"` goes back to the nearest brand's own accent. Overlays keep the accent of their area.
7089
7161
 
7090
7162
  ## VisuallyHidden
@@ -7303,13 +7375,13 @@ During server rendering it returns `base` (or `fallback`).
7303
7375
  useDisclosure(initial = false, options: UseDisclosureOptions = {}): [opened: boolean, handlers: DisclosureHandlers]
7304
7376
  ```
7305
7377
 
7306
- Open/closed state for anything that shows and hides (a Modal, Drawer,
7307
- Popover, a collapsible section), with stable handlers.
7308
-
7309
- ```tsx
7310
- const [opened, { open, close }] = useDisclosure();
7311
- <Button onClick={open}>Edit</Button>
7312
- <Modal open={opened} onOpenChange={(next) => (next ? open() : close())}>…</Modal>
7378
+ Open/closed state for anything that shows and hides (a Modal, Drawer,
7379
+ Popover, a collapsible section), with stable handlers.
7380
+
7381
+ ```tsx
7382
+ const [opened, { open, close }] = useDisclosure();
7383
+ <Button onClick={open}>Edit</Button>
7384
+ <Modal open={opened} onOpenChange={(next) => (next ? open() : close())}>…</Modal>
7313
7385
  ```
7314
7386
 
7315
7387
  ## useToggle
@@ -7318,13 +7390,13 @@ const [opened, { open, close }] = useDisclosure();
7318
7390
  useToggle(values: readonly T[] = [false, true] as unknown as readonly T[]): [value: T, toggle: (value?: T) => void]
7319
7391
  ```
7320
7392
 
7321
- Cycles through values: `[false, true]` by default (a boolean toggle), or
7322
- any list (`["list", "grid"]`). `toggle()` moves to the next value (wrapping);
7323
- `toggle(value)` jumps to one.
7324
-
7325
- ```tsx
7326
- const [view, toggleView] = useToggle(["list", "grid"] as const);
7327
- <Button onClick={() => toggleView()}>Show {view === "list" ? "grid" : "list"}</Button>
7393
+ Cycles through values: `[false, true]` by default (a boolean toggle), or
7394
+ any list (`["list", "grid"]`). `toggle()` moves to the next value (wrapping);
7395
+ `toggle(value)` jumps to one.
7396
+
7397
+ ```tsx
7398
+ const [view, toggleView] = useToggle(["list", "grid"] as const);
7399
+ <Button onClick={() => toggleView()}>Show {view === "list" ? "grid" : "list"}</Button>
7328
7400
  ```
7329
7401
 
7330
7402
  ## useDebouncedCallback
@@ -7333,13 +7405,13 @@ const [view, toggleView] = useToggle(["list", "grid"] as const);
7333
7405
  useDebouncedCallback(callback: (...args: A) => void, wait: number, { leading = false, maxWait }: { leading?: boolean; maxWait?: number } = {}): ControlledCallback<A>
7334
7406
  ```
7335
7407
 
7336
- A function that waits until calls stop for `wait` ms, then runs once with
7337
- the last arguments. Always calls the latest `callback`; a pending call is
7338
- dropped on unmount. Use it to save, search, or validate after typing pauses.
7339
-
7340
- ```tsx
7341
- const save = useDebouncedCallback((text: string) => api.saveDraft(text), 500);
7342
- <Textarea onChange={(e) => save(e.target.value)} />
7408
+ A function that waits until calls stop for `wait` ms, then runs once with
7409
+ the last arguments. Always calls the latest `callback`; a pending call is
7410
+ dropped on unmount. Use it to save, search, or validate after typing pauses.
7411
+
7412
+ ```tsx
7413
+ const save = useDebouncedCallback((text: string) => api.saveDraft(text), 500);
7414
+ <Textarea onChange={(e) => save(e.target.value)} />
7343
7415
  ```
7344
7416
 
7345
7417
  ## useThrottledCallback
@@ -7348,12 +7420,12 @@ const save = useDebouncedCallback((text: string) => api.saveDraft(text), 500);
7348
7420
  useThrottledCallback(callback: (...args: A) => void, wait: number): ControlledCallback<A>
7349
7421
  ```
7350
7422
 
7351
- A function that runs at most once every `wait` ms: the first call runs
7352
- right away, and the last call made during the wait runs when it ends.
7353
- Use it for scroll, resize, or pointer-move handlers.
7354
-
7355
- ```tsx
7356
- const track = useThrottledCallback((x: number) => setPosition(x), 100);
7423
+ A function that runs at most once every `wait` ms: the first call runs
7424
+ right away, and the last call made during the wait runs when it ends.
7425
+ Use it for scroll, resize, or pointer-move handlers.
7426
+
7427
+ ```tsx
7428
+ const track = useThrottledCallback((x: number) => setPosition(x), 100);
7357
7429
  ```
7358
7430
 
7359
7431
  ## useDebouncedValue
@@ -7362,13 +7434,13 @@ const track = useThrottledCallback((x: number) => setPosition(x), 100);
7362
7434
  useDebouncedValue(value: T, wait: number): [debounced: T, controls: { cancel: () => void; flush: () => void }]
7363
7435
  ```
7364
7436
 
7365
- The value, updated only after it stops changing for `wait` ms. Use it to
7366
- search or fetch as someone types, without a request per keystroke.
7367
-
7368
- ```tsx
7369
- const [query, setQuery] = useState("");
7370
- const [debouncedQuery] = useDebouncedValue(query, 300);
7371
- useEffect(() => { search(debouncedQuery); }, [debouncedQuery]);
7437
+ The value, updated only after it stops changing for `wait` ms. Use it to
7438
+ search or fetch as someone types, without a request per keystroke.
7439
+
7440
+ ```tsx
7441
+ const [query, setQuery] = useState("");
7442
+ const [debouncedQuery] = useDebouncedValue(query, 300);
7443
+ useEffect(() => { search(debouncedQuery); }, [debouncedQuery]);
7372
7444
  ```
7373
7445
 
7374
7446
  ## useInterval
@@ -7377,12 +7449,12 @@ useEffect(() => { search(debouncedQuery); }, [debouncedQuery]);
7377
7449
  useInterval(callback: () => void, delay: number | null): void
7378
7450
  ```
7379
7451
 
7380
- Calls `callback` every `delay` ms while `delay` is a number; pass `null`
7381
- to pause. Always calls the latest `callback`, and stops on unmount.
7382
-
7383
- ```tsx
7384
- const [running, setRunning] = useState(true);
7385
- useInterval(() => setSeconds((s) => s + 1), running ? 1000 : null);
7452
+ Calls `callback` every `delay` ms while `delay` is a number; pass `null`
7453
+ to pause. Always calls the latest `callback`, and stops on unmount.
7454
+
7455
+ ```tsx
7456
+ const [running, setRunning] = useState(true);
7457
+ useInterval(() => setSeconds((s) => s + 1), running ? 1000 : null);
7386
7458
  ```
7387
7459
 
7388
7460
  ## useTimeout
@@ -7391,12 +7463,12 @@ useInterval(() => setSeconds((s) => s + 1), running ? 1000 : null);
7391
7463
  useTimeout(callback: () => void, delay: number | null): { clear: () => void; reset: () => void }
7392
7464
  ```
7393
7465
 
7394
- Calls `callback` once, `delay` ms after mount (or after `delay` changes);
7395
- pass `null` to not schedule it. Returns `clear` and `reset` (restart the
7396
- wait).
7397
-
7398
- ```tsx
7399
- const { reset } = useTimeout(() => setSaved(false), 2000);
7466
+ Calls `callback` once, `delay` ms after mount (or after `delay` changes);
7467
+ pass `null` to not schedule it. Returns `clear` and `reset` (restart the
7468
+ wait).
7469
+
7470
+ ```tsx
7471
+ const { reset } = useTimeout(() => setSaved(false), 2000);
7400
7472
  ```
7401
7473
 
7402
7474
  ## usePrevious
@@ -7405,10 +7477,10 @@ const { reset } = useTimeout(() => setSaved(false), 2000);
7405
7477
  usePrevious(value: T): T | undefined
7406
7478
  ```
7407
7479
 
7408
- The value from before its last change (undefined until it first changes).
7409
-
7410
- ```tsx
7411
- const previousCount = usePrevious(count);
7480
+ The value from before its last change (undefined until it first changes).
7481
+
7482
+ ```tsx
7483
+ const previousCount = usePrevious(count);
7412
7484
  ```
7413
7485
 
7414
7486
  ## useLocalStorage
@@ -7417,13 +7489,13 @@ const previousCount = usePrevious(count);
7417
7489
  useLocalStorage(key: string, defaultValue: T, options?: UseStorageOptions<T>)
7418
7490
  ```
7419
7491
 
7420
- State saved in `localStorage` under `key`: it survives reloads and stays in
7421
- sync across every component using the key and across browser tabs. Values
7422
- are JSON by default. Returns `defaultValue` during server rendering, when
7423
- nothing is stored, or when storage is blocked (it never throws).
7424
-
7425
- ```tsx
7426
- const [density, setDensity] = useLocalStorage("table-density", "comfortable");
7492
+ State saved in `localStorage` under `key`: it survives reloads and stays in
7493
+ sync across every component using the key and across browser tabs. Values
7494
+ are JSON by default. Returns `defaultValue` during server rendering, when
7495
+ nothing is stored, or when storage is blocked (it never throws).
7496
+
7497
+ ```tsx
7498
+ const [density, setDensity] = useLocalStorage("table-density", "comfortable");
7427
7499
  ```
7428
7500
 
7429
7501
  ## useSessionStorage
@@ -7440,14 +7512,14 @@ Like `useLocalStorage`, in `sessionStorage`: kept until the tab closes.
7440
7512
  useClipboard({ timeout = 2000 }: { timeout?: number } = {}): UseClipboardResult
7441
7513
  ```
7442
7514
 
7443
- Copies text to the clipboard, with a `copied` flag that resets after
7444
- `timeout` ms. Uses the Clipboard API, with a fallback for browsers or
7445
- contexts that block it. Pair a visible "Copied" change with an
7446
- announcement (`announce`) so screen readers hear it too.
7447
-
7448
- ```tsx
7449
- const { copy, copied } = useClipboard();
7450
- <Button onClick={() => copy(inviteLink)}>{copied ? "Copied" : "Copy link"}</Button>
7515
+ Copies text to the clipboard, with a `copied` flag that resets after
7516
+ `timeout` ms. Uses the Clipboard API, with a fallback for browsers or
7517
+ contexts that block it. Pair a visible "Copied" change with an
7518
+ announcement (`announce`) so screen readers hear it too.
7519
+
7520
+ ```tsx
7521
+ const { copy, copied } = useClipboard();
7522
+ <Button onClick={() => copy(inviteLink)}>{copied ? "Copied" : "Copy link"}</Button>
7451
7523
  ```
7452
7524
 
7453
7525
  ## useHotkeys
@@ -7456,19 +7528,19 @@ const { copy, copied } = useClipboard();
7456
7528
  useHotkeys(hotkeys: HotkeyItem[], { enabled = true, target }: UseHotkeysOptions = {}): void
7457
7529
  ```
7458
7530
 
7459
- Keyboard shortcuts for the whole page (or one element). `mod` is ⌘ on Mac
7460
- and Ctrl elsewhere; separate alternatives with commas (`"mod+k, /"`).
7461
- Shortcuts with ⌘, Ctrl, or Alt work everywhere; plain keys don't fire while
7462
- typing in a text field. Show each shortcut to users (`Kbd`,
7463
- `aria-keyshortcuts`) so it can be found, and avoid single-key shortcuts that
7464
- screen readers or browsers use, or let users turn them off (WCAG 2.1.4).
7465
-
7466
- ```tsx
7467
- useHotkeys([
7468
- ["mod+k", () => setPaletteOpen(true)],
7469
- ["/", () => searchRef.current?.focus()],
7470
- ["shift+?", () => setHelpOpen(true)],
7471
- ]);
7531
+ Keyboard shortcuts for the whole page (or one element). `mod` is ⌘ on Mac
7532
+ and Ctrl elsewhere; separate alternatives with commas (`"mod+k, /"`).
7533
+ Shortcuts with ⌘, Ctrl, or Alt work everywhere; plain keys don't fire while
7534
+ typing in a text field. Show each shortcut to users (`Kbd`,
7535
+ `aria-keyshortcuts`) so it can be found, and avoid single-key shortcuts that
7536
+ screen readers or browsers use, or let users turn them off (WCAG 2.1.4).
7537
+
7538
+ ```tsx
7539
+ useHotkeys([
7540
+ ["mod+k", () => setPaletteOpen(true)],
7541
+ ["/", () => searchRef.current?.focus()],
7542
+ ["shift+?", () => setHelpOpen(true)],
7543
+ ]);
7472
7544
  ```
7473
7545
 
7474
7546
  ## useEventListener
@@ -7477,13 +7549,13 @@ useHotkeys([
7477
7549
  useEventListener(type: K, handler: (event: WindowEventMap[K]) => void, target?: ListenerTarget | RefObject<HTMLElement | null>, options?: boolean | AddEventListenerOptions): void
7478
7550
  ```
7479
7551
 
7480
- Adds an event listener and removes it on unmount. Always calls the latest
7481
- `handler` (no re-subscribing when it changes). `target` defaults to
7482
- `window`; pass an element, `document`, or a ref.
7483
-
7484
- ```tsx
7485
- useEventListener("beforeunload", (e) => { if (dirty) e.preventDefault(); });
7486
- useEventListener("scroll", onScroll, panelRef, { passive: true });
7552
+ Adds an event listener and removes it on unmount. Always calls the latest
7553
+ `handler` (no re-subscribing when it changes). `target` defaults to
7554
+ `window`; pass an element, `document`, or a ref.
7555
+
7556
+ ```tsx
7557
+ useEventListener("beforeunload", (e) => { if (dirty) e.preventDefault(); });
7558
+ useEventListener("scroll", onScroll, panelRef, { passive: true });
7487
7559
  ```
7488
7560
 
7489
7561
  ## useClickOutside
@@ -7492,14 +7564,14 @@ useEventListener("scroll", onScroll, panelRef, { passive: true });
7492
7564
  useClickOutside(handler: (event: Event) => void, { enabled = true, ignore = [] }: { enabled?: boolean; ignore?: Array<RefObject<HTMLElement | null> | HTMLElement | null> } = {}): RefObject<T | null>
7493
7565
  ```
7494
7566
 
7495
- Calls `handler` when a pointer press or a focus move lands outside the
7496
- returned ref's element (and outside any `ignore` elements). Use it for
7497
- custom popups; the library's own overlays already close on outside
7498
- presses, through the shared layer stack.
7499
-
7500
- ```tsx
7501
- const ref = useClickOutside<HTMLDivElement>(() => setOpen(false));
7502
- <div ref={ref}>…</div>
7567
+ Calls `handler` when a pointer press or a focus move lands outside the
7568
+ returned ref's element (and outside any `ignore` elements). Use it for
7569
+ custom popups; the library's own overlays already close on outside
7570
+ presses, through the shared layer stack.
7571
+
7572
+ ```tsx
7573
+ const ref = useClickOutside<HTMLDivElement>(() => setOpen(false));
7574
+ <div ref={ref}>…</div>
7503
7575
  ```
7504
7576
 
7505
7577
  ## useIntersection
@@ -7508,13 +7580,13 @@ const ref = useClickOutside<HTMLDivElement>(() => setOpen(false));
7508
7580
  useIntersection({ root, rootMargin, threshold = 0, once = false, }: UseIntersectionOptions = {}): { ref: (node: T | null) => void; entry: IntersectionObserverEntry | null; isIntersecting: boolean }
7509
7581
  ```
7510
7582
 
7511
- Whether an element is in view (IntersectionObserver). Attach the returned
7512
- `ref`; read `isIntersecting` or the full `entry`. Returns false during
7513
- server rendering.
7514
-
7515
- ```tsx
7516
- const { ref, isIntersecting } = useIntersection<HTMLDivElement>({ rootMargin: "200px", once: true });
7517
- <div ref={ref}>{isIntersecting && <HeavyChart />}</div>
7583
+ Whether an element is in view (IntersectionObserver). Attach the returned
7584
+ `ref`; read `isIntersecting` or the full `entry`. Returns false during
7585
+ server rendering.
7586
+
7587
+ ```tsx
7588
+ const { ref, isIntersecting } = useIntersection<HTMLDivElement>({ rootMargin: "200px", once: true });
7589
+ <div ref={ref}>{isIntersecting && <HeavyChart />}</div>
7518
7590
  ```
7519
7591
 
7520
7592
  ## useElementSize
@@ -7523,13 +7595,13 @@ const { ref, isIntersecting } = useIntersection<HTMLDivElement>({ rootMargin: "2
7523
7595
  useElementSize(): { ref: (node: T | null) => void; width: number; height: number; }
7524
7596
  ```
7525
7597
 
7526
- An element's content-box size, kept current as it resizes
7527
- (ResizeObserver). Attach the returned `ref`. Both are 0 until measured
7528
- (and during server rendering).
7529
-
7530
- ```tsx
7531
- const { ref, width } = useElementSize<HTMLDivElement>();
7532
- <div ref={ref}>{width > 600 ? <WideLayout /> : <NarrowLayout />}</div>
7598
+ An element's content-box size, kept current as it resizes
7599
+ (ResizeObserver). Attach the returned `ref`. Both are 0 until measured
7600
+ (and during server rendering).
7601
+
7602
+ ```tsx
7603
+ const { ref, width } = useElementSize<HTMLDivElement>();
7604
+ <div ref={ref}>{width > 600 ? <WideLayout /> : <NarrowLayout />}</div>
7533
7605
  ```
7534
7606
 
7535
7607
  ## useHydrated
@@ -7538,13 +7610,13 @@ const { ref, width } = useElementSize<HTMLDivElement>();
7538
7610
  useHydrated(): boolean
7539
7611
  ```
7540
7612
 
7541
- False during server rendering and hydration, true after. Use it to render
7542
- browser-only content (dates in the user's time zone, `window` values)
7543
- without a hydration mismatch.
7544
-
7545
- ```tsx
7546
- const hydrated = useHydrated();
7547
- return <span>{hydrated ? new Date().toLocaleTimeString() : null}</span>;
7613
+ False during server rendering and hydration, true after. Use it to render
7614
+ browser-only content (dates in the user's time zone, `window` values)
7615
+ without a hydration mismatch.
7616
+
7617
+ ```tsx
7618
+ const hydrated = useHydrated();
7619
+ return <span>{hydrated ? new Date().toLocaleTimeString() : null}</span>;
7548
7620
  ```
7549
7621
 
7550
7622
  ## useReducedMotion
@@ -7553,8 +7625,8 @@ return <span>{hydrated ? new Date().toLocaleTimeString() : null}</span>;
7553
7625
  useReducedMotion(): boolean
7554
7626
  ```
7555
7627
 
7556
- True when the user asked the system to reduce motion. The library's own
7557
- transitions already respect it (through the duration tokens); use this for
7628
+ True when the user asked the system to reduce motion. The library's own
7629
+ transitions already respect it (through the duration tokens); use this for
7558
7630
  motion you drive from JavaScript (autoplay, scroll animations).
7559
7631
 
7560
7632
  ## useColorScheme
@@ -7563,7 +7635,7 @@ motion you drive from JavaScript (autoplay, scroll animations).
7563
7635
  useColorScheme(): "light" | "dark"
7564
7636
  ```
7565
7637
 
7566
- The system color scheme, `"light"` or `"dark"`, kept in sync (`"light"`
7638
+ The system color scheme, `"light"` or `"dark"`, kept in sync (`"light"`
7567
7639
  during server rendering). To follow it, pass it to `Theme mode`.
7568
7640
 
7569
7641
  ## useOnline
@@ -7572,8 +7644,8 @@ during server rendering). To follow it, pass it to `Theme mode`.
7572
7644
  useOnline(): boolean
7573
7645
  ```
7574
7646
 
7575
- Whether the browser has a network connection, kept in sync (true during
7576
- server rendering). Pair a visible change (a Banner) with a polite
7647
+ Whether the browser has a network connection, kept in sync (true during
7648
+ server rendering). Pair a visible change (a Banner) with a polite
7577
7649
  announcement when it goes offline.
7578
7650
 
7579
7651
  ## useDocumentVisibility
@@ -7582,7 +7654,7 @@ announcement when it goes offline.
7582
7654
  useDocumentVisibility(): DocumentVisibilityState
7583
7655
  ```
7584
7656
 
7585
- `"visible"` or `"hidden"` (another tab, minimized window), kept in sync.
7657
+ `"visible"` or `"hidden"` (another tab, minimized window), kept in sync.
7586
7658
  Use it to pause polling, video, or animations in the background.
7587
7659
 
7588
7660
  ## useViewportSize
@@ -7591,8 +7663,8 @@ Use it to pause polling, video, or animations in the background.
7591
7663
  useViewportSize(): { width: number; height: number }
7592
7664
  ```
7593
7665
 
7594
- The viewport's width and height in CSS pixels, kept in sync (0 × 0 during
7595
- server rendering). For layout decisions, prefer CSS or `useMediaQuery`;
7666
+ The viewport's width and height in CSS pixels, kept in sync (0 × 0 during
7667
+ server rendering). For layout decisions, prefer CSS or `useMediaQuery`;
7596
7668
  use this when you need the numbers.
7597
7669
 
7598
7670
  ## useVirtualizer
@@ -7601,27 +7673,27 @@ use this when you need the numbers.
7601
7673
  useVirtualizer({ count, scrollElement, estimateSize, overscan = 6, gap = 0, paddingStart = 0, paddingEnd = 0, initialCount = 10, }: UseVirtualizerOptions): Virtualizer
7602
7674
  ```
7603
7675
 
7604
- Headless list virtualization: renders only the items in view (plus
7605
- `overscan`), so lists of 100,000 rows stay fast. Works with variable
7606
- heights by measuring rendered items, and is SSR-safe (the server renders
7607
- the first `initialCount` items). `VirtualList` is built on it; use the
7608
- hook directly for custom layouts.
7609
-
7610
- ```tsx
7611
- const scrollRef = useRef<HTMLDivElement>(null);
7612
- const { virtualItems, totalSize, measureElement } = useVirtualizer({
7613
- count: rows.length, scrollElement: scrollRef, estimateSize: 48,
7614
- });
7615
- <div ref={scrollRef} style={{ overflow: "auto", height: 400 }}>
7616
- <div style={{ position: "relative", height: totalSize }}>
7617
- {virtualItems.map((item) => (
7618
- <div key={item.index} data-index={item.index} ref={measureElement}
7619
- style={{ position: "absolute", top: 0, width: "100%", transform: `translateY(${item.start}px)` }}>
7620
- {rows[item.index].name}
7621
- </div>
7622
- ))}
7623
- </div>
7624
- </div>
7676
+ Headless list virtualization: renders only the items in view (plus
7677
+ `overscan`), so lists of 100,000 rows stay fast. Works with variable
7678
+ heights by measuring rendered items, and is SSR-safe (the server renders
7679
+ the first `initialCount` items). `VirtualList` is built on it; use the
7680
+ hook directly for custom layouts.
7681
+
7682
+ ```tsx
7683
+ const scrollRef = useRef<HTMLDivElement>(null);
7684
+ const { virtualItems, totalSize, measureElement } = useVirtualizer({
7685
+ count: rows.length, scrollElement: scrollRef, estimateSize: 48,
7686
+ });
7687
+ <div ref={scrollRef} style={{ overflow: "auto", height: 400 }}>
7688
+ <div style={{ position: "relative", height: totalSize }}>
7689
+ {virtualItems.map((item) => (
7690
+ <div key={item.index} data-index={item.index} ref={measureElement}
7691
+ style={{ position: "absolute", top: 0, width: "100%", transform: `translateY(${item.start}px)` }}>
7692
+ {rows[item.index].name}
7693
+ </div>
7694
+ ))}
7695
+ </div>
7696
+ </div>
7625
7697
  ```
7626
7698
 
7627
7699
  ## useFillSlot
@@ -7648,7 +7720,7 @@ Pixel widths for each breakpoint, for use in JS.
7648
7720
  getLocaleDirection(locale: string): "ltr" | "rtl"
7649
7721
  ```
7650
7722
 
7651
- The writing direction of a BCP 47 locale: `"rtl"` for Arabic, Hebrew,
7723
+ The writing direction of a BCP 47 locale: `"rtl"` for Arabic, Hebrew,
7652
7724
  Persian, Urdu, and other right-to-left languages and scripts, else `"ltr"`.
7653
7725
 
7654
7726
  ## plural
@@ -7657,11 +7729,11 @@ Persian, Urdu, and other right-to-left languages and scripts, else `"ltr"`.
7657
7729
  plural(locale: string): PluralFn
7658
7730
  ```
7659
7731
 
7660
- Plural selector bound to a locale:
7661
-
7662
- ```ts
7663
- const p = plural("ru");
7664
- p(3, { one: "# файл", few: "# файла", many: "# файлов", other: "# файла" }); // "3 файла"
7732
+ Plural selector bound to a locale:
7733
+
7734
+ ```ts
7735
+ const p = plural("ru");
7736
+ p(3, { one: "# файл", few: "# файла", many: "# файлов", other: "# файла" }); // "3 файла"
7665
7737
  ```
7666
7738
 
7667
7739
  ## en
@@ -7678,7 +7750,7 @@ English: the default messages, and the template for every other locale.
7678
7750
  mergeMessages(base: Messages, override: PartialMessages | undefined): Messages
7679
7751
  ```
7680
7752
 
7681
- Overlays partial messages on a complete set: missing strings keep the
7753
+ Overlays partial messages on a complete set: missing strings keep the
7682
7754
  base value. Functions and arrays replace whole.
7683
7755
 
7684
7756
  ## resolveElement
@@ -7822,12 +7894,12 @@ Locks page scrolling. Returns a function that releases this lock (safe to call t
7822
7894
  getHotkeyHandler(hotkeys: HotkeyItem[])
7823
7895
  ```
7824
7896
 
7825
- A `keydown` handler for one element (an editor, a list): pass it to
7826
- `onKeyDown`. Same shortcut format as `useHotkeys`; shortcuts fire while
7827
- typing in that element.
7828
-
7829
- ```tsx
7830
- <Textarea onKeyDown={getHotkeyHandler([["mod+enter", send]])} />
7897
+ A `keydown` handler for one element (an editor, a list): pass it to
7898
+ `onKeyDown`. Same shortcut format as `useHotkeys`; shortcuts fire while
7899
+ typing in that element.
7900
+
7901
+ ```tsx
7902
+ <Textarea onKeyDown={getHotkeyHandler([["mod+enter", send]])} />
7831
7903
  ```
7832
7904
 
7833
7905
  ## cx
@@ -8029,13 +8101,21 @@ createBrandPalette(color: BrandColor): BrandPalette
8029
8101
  Builds an accessible brand palette, for both modes, from any hex color.
8030
8102
  Throws a TypeError for anything that isn't `#rgb` or `#rrggbb`.
8031
8103
 
8104
+ ## LIME_ACCENT
8105
+
8106
+ ```ts
8107
+ LIME_ACCENT: AccentPalette
8108
+ ```
8109
+
8110
+ The lime accent (`accent="lime"`, and the black brand's accent): the original bright yellow-green.
8111
+
8032
8112
  ## VOLT_ACCENT
8033
8113
 
8034
8114
  ```ts
8035
- VOLT_ACCENT: AccentPalette
8115
+ VOLT_ACCENT
8036
8116
  ```
8037
8117
 
8038
- The original volt accent (`accent="volt"`, and the black brand's accent).
8118
+ @deprecated Renamed to `LIME_ACCENT`.
8039
8119
 
8040
8120
  ## createAccentPalette
8041
8121
 
@@ -8056,7 +8136,7 @@ brandAccent(primary: string): AccentPalette
8056
8136
 
8057
8137
  The default accent of a brand: a bright, lighter shade of its primary
8058
8138
  hue (blue gets a sky blue, purple a lavender, gold an amber, green a
8059
- mint). Colorless brands (black) keep the volt.
8139
+ mint). Colorless brands (black) keep the lime.
8060
8140
 
8061
8141
  ## accentPaletteVars
8062
8142
 
@@ -8108,9 +8188,9 @@ Then use `<html data-brand="acme">`. Names are letters, digits, and dashes.
8108
8188
  isEmail(value: string, { requireTld = true }: EmailOptions = {}): boolean
8109
8189
  ```
8110
8190
 
8111
- True for an email address in the usual format: the HTML spec's rules
8112
- (what browsers check for `type="email"`), plus no leading, trailing, or
8113
- doubled dots before the `@`, length limits (64 before the `@`, 254 in all),
8191
+ True for an email address in the usual format: the HTML spec's rules
8192
+ (what browsers check for `type="email"`), plus no leading, trailing, or
8193
+ doubled dots before the `@`, length limits (64 before the `@`, 254 in all),
8114
8194
  and, by default, a top-level domain.
8115
8195
 
8116
8196
  ## isIPv4
@@ -8119,7 +8199,7 @@ and, by default, a top-level domain.
8119
8199
  isIPv4(value: string): boolean
8120
8200
  ```
8121
8201
 
8122
- True for a dotted IPv4 address: four numbers 0-255 (`192.168.0.1`), no
8202
+ True for a dotted IPv4 address: four numbers 0-255 (`192.168.0.1`), no
8123
8203
  leading zeros (`010.0.0.1` is ambiguous, so it's rejected).
8124
8204
 
8125
8205
  ## isIPv6
@@ -8128,9 +8208,9 @@ leading zeros (`010.0.0.1` is ambiguous, so it's rejected).
8128
8208
  isIPv6(value: string): boolean
8129
8209
  ```
8130
8210
 
8131
- True for an IPv6 address: eight groups of 1-4 hex digits, with `::`
8132
- standing for one or more zero groups (once at most), and an optional
8133
- IPv4 tail (`::ffff:192.168.0.1`). Zone ids (`%eth0`) and brackets aren't
8211
+ True for an IPv6 address: eight groups of 1-4 hex digits, with `::`
8212
+ standing for one or more zero groups (once at most), and an optional
8213
+ IPv4 tail (`::ffff:192.168.0.1`). Zone ids (`%eth0`) and brackets aren't
8134
8214
  accepted.
8135
8215
 
8136
8216
  ## isIP
@@ -8163,10 +8243,10 @@ True when a value counts as empty (nothing, blank text, or an empty file input).
8163
8243
  rules
8164
8244
  ```
8165
8245
 
8166
- Rule helpers for `useFormValidation`, with messages from the locale:
8167
-
8168
- ```ts
8169
- useFormValidation({ rules: { email: rules.email(), server: rules.ip({ version: 4 }) } });
8246
+ Rule helpers for `useFormValidation`, with messages from the locale:
8247
+
8248
+ ```ts
8249
+ useFormValidation({ rules: { email: rules.email(), server: rules.ip({ version: 4 }) } });
8170
8250
  ```
8171
8251
 
8172
8252
  ## detectPlatform
@@ -8183,8 +8263,8 @@ The current platform from the browser (`"windows"` during server rendering).
8183
8263
  parseHotkey(hotkey: string, platform: HotkeyPlatform = detectPlatform()): ParsedHotkey | null
8184
8264
  ```
8185
8265
 
8186
- Parses a shortcut like `"mod+k"`, `"shift+mod+p"`, `"ctrl+/"`, or
8187
- `"escape"` into modifiers and a key. `mod` is ⌘ on Mac and Ctrl
8266
+ Parses a shortcut like `"mod+k"`, `"shift+mod+p"`, `"ctrl+/"`, or
8267
+ `"escape"` into modifiers and a key. `mod` is ⌘ on Mac and Ctrl
8188
8268
  elsewhere. Returns null when there is no key.
8189
8269
 
8190
8270
  ## matchesHotkey
@@ -8217,6 +8297,6 @@ True when the element accepts typed text, so a global hotkey must not steal its
8217
8297
  copyToClipboard(text: string): Promise<void>
8218
8298
  ```
8219
8299
 
8220
- Copies text to the clipboard: the async Clipboard API when it's allowed,
8221
- else a hidden-textarea fallback (insecure contexts, denied permission,
8300
+ Copies text to the clipboard: the async Clipboard API when it's allowed,
8301
+ else a hidden-textarea fallback (insecure contexts, denied permission,
8222
8302
  older browsers). Focus returns to where it was. Rejects when both fail.