@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.
- package/README.md +781 -723
- package/dist/ai/llms-full.txt +1003 -923
- package/dist/ai/llms.txt +2 -1
- package/dist/ai/registry.json +340 -121
- package/dist/components/data-display/Chat/Chat.cjs.js +1 -0
- package/dist/components/data-display/Chat/Chat.d.ts +114 -0
- package/dist/components/data-display/Chat/Chat.mjs +301 -0
- package/dist/components/data-display/Chat/Chat.module.css.cjs.js +1 -0
- package/dist/components/data-display/Chat/Chat.module.css.mjs +32 -0
- package/dist/components/data-display/Chat/index.d.ts +2 -0
- package/dist/components/data-display/Chat/streams.cjs.js +4 -0
- package/dist/components/data-display/Chat/streams.d.ts +32 -0
- package/dist/components/data-display/Chat/streams.mjs +35 -0
- package/dist/components/data-display/DataGrid/DataGrid.module.css.cjs.js +1 -1
- package/dist/components/data-display/DataGrid/DataGrid.module.css.mjs +1 -1
- package/dist/components/data-display/VirtualList/VirtualList.module.css.cjs.js +1 -1
- package/dist/components/data-display/VirtualList/VirtualList.module.css.mjs +1 -1
- package/dist/components/feedback/ErrorSummary/ErrorSummary.module.css.cjs.js +1 -1
- package/dist/components/feedback/ErrorSummary/ErrorSummary.module.css.mjs +1 -1
- package/dist/components/forms/Combobox/Combobox.cjs.js +1 -1
- package/dist/components/forms/Combobox/Combobox.mjs +5 -5
- package/dist/components/forms/DateTimePicker/DateTimePicker.module.css.cjs.js +1 -1
- package/dist/components/forms/DateTimePicker/DateTimePicker.module.css.mjs +1 -1
- package/dist/components/forms/Rating/Rating.module.css.cjs.js +1 -1
- package/dist/components/forms/Rating/Rating.module.css.mjs +1 -1
- package/dist/components/forms/TagInput/TagInput.cjs.js +1 -1
- package/dist/components/forms/TagInput/TagInput.mjs +5 -5
- package/dist/components/forms/TimePicker/TimePicker.module.css.cjs.js +1 -1
- package/dist/components/forms/TimePicker/TimePicker.module.css.mjs +1 -1
- package/dist/components/layout/ResizablePanels/ResizablePanels.cjs.js +1 -1
- package/dist/components/layout/ResizablePanels/ResizablePanels.mjs +1 -0
- package/dist/components/layout/ResizablePanels/ResizablePanels.module.css.cjs.js +1 -1
- package/dist/components/layout/ResizablePanels/ResizablePanels.module.css.mjs +1 -1
- package/dist/components/layout/ScrollArea/ScrollArea.module.css.cjs.js +1 -1
- package/dist/components/layout/ScrollArea/ScrollArea.module.css.mjs +1 -1
- package/dist/components/navigation/ContextMenu/ContextMenu.module.css.cjs.js +1 -1
- package/dist/components/navigation/ContextMenu/ContextMenu.module.css.mjs +1 -1
- package/dist/components/overlays/HoverCard/HoverCard.module.css.cjs.js +1 -1
- package/dist/components/overlays/HoverCard/HoverCard.module.css.mjs +2 -2
- package/dist/components/primitives/Button/Button.cjs.js +1 -1
- package/dist/components/primitives/Button/Button.mjs +37 -37
- package/dist/components/utilities/LocaleProvider/LocaleProvider.module.css.cjs.js +1 -1
- package/dist/components/utilities/LocaleProvider/LocaleProvider.module.css.mjs +1 -1
- package/dist/components/utilities/Theme/Theme.cjs.js +1 -1
- package/dist/components/utilities/Theme/Theme.d.ts +6 -6
- package/dist/components/utilities/Theme/Theme.mjs +49 -45
- package/dist/hooks/useFormValidation.cjs.js +1 -1
- package/dist/hooks/useFormValidation.mjs +1 -1
- package/dist/i18n/en.cjs.js +1 -1
- package/dist/i18n/en.mjs +71 -0
- package/dist/i18n/types.d.ts +91 -0
- package/dist/index.cjs.js +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.mjs +108 -106
- package/dist/locales/ar.cjs.js +1 -1
- package/dist/locales/ar.mjs +71 -0
- package/dist/locales/bn.cjs.js +1 -1
- package/dist/locales/bn.mjs +71 -0
- package/dist/locales/cs.cjs.js +1 -1
- package/dist/locales/cs.mjs +71 -0
- package/dist/locales/da.cjs.js +1 -1
- package/dist/locales/da.mjs +71 -0
- package/dist/locales/de.cjs.js +1 -1
- package/dist/locales/de.mjs +71 -0
- package/dist/locales/el.cjs.js +1 -1
- package/dist/locales/el.mjs +71 -0
- package/dist/locales/es.cjs.js +1 -1
- package/dist/locales/es.mjs +71 -0
- package/dist/locales/fa.cjs.js +1 -1
- package/dist/locales/fa.mjs +71 -0
- package/dist/locales/fi.cjs.js +1 -1
- package/dist/locales/fi.mjs +71 -0
- package/dist/locales/fr.cjs.js +1 -1
- package/dist/locales/fr.mjs +71 -0
- package/dist/locales/he.cjs.js +1 -1
- package/dist/locales/he.mjs +71 -0
- package/dist/locales/hi.cjs.js +1 -1
- package/dist/locales/hi.mjs +71 -0
- package/dist/locales/hu.cjs.js +1 -1
- package/dist/locales/hu.mjs +71 -0
- package/dist/locales/id.cjs.js +1 -1
- package/dist/locales/id.mjs +71 -0
- package/dist/locales/it.cjs.js +1 -1
- package/dist/locales/it.mjs +71 -0
- package/dist/locales/ja.cjs.js +1 -1
- package/dist/locales/ja.mjs +71 -0
- package/dist/locales/ko.cjs.js +1 -1
- package/dist/locales/ko.mjs +71 -0
- package/dist/locales/nb.cjs.js +1 -1
- package/dist/locales/nb.mjs +71 -0
- package/dist/locales/nl.cjs.js +1 -1
- package/dist/locales/nl.mjs +71 -0
- package/dist/locales/pl.cjs.js +1 -1
- package/dist/locales/pl.mjs +71 -0
- package/dist/locales/pt-BR.cjs.js +1 -1
- package/dist/locales/pt-BR.mjs +71 -0
- package/dist/locales/ro.cjs.js +1 -1
- package/dist/locales/ro.mjs +71 -0
- package/dist/locales/ru.cjs.js +1 -1
- package/dist/locales/ru.mjs +71 -0
- package/dist/locales/sv.cjs.js +1 -1
- package/dist/locales/sv.mjs +71 -0
- package/dist/locales/th.cjs.js +1 -1
- package/dist/locales/th.mjs +71 -0
- package/dist/locales/tr.cjs.js +1 -1
- package/dist/locales/tr.mjs +71 -0
- package/dist/locales/uk.cjs.js +1 -1
- package/dist/locales/uk.mjs +71 -0
- package/dist/locales/ur.cjs.js +1 -1
- package/dist/locales/ur.mjs +71 -0
- package/dist/locales/vi.cjs.js +1 -1
- package/dist/locales/vi.mjs +71 -0
- package/dist/locales/zh-Hans.cjs.js +1 -1
- package/dist/locales/zh-Hans.mjs +71 -0
- package/dist/locales/zh-Hant.cjs.js +1 -1
- package/dist/locales/zh-Hant.mjs +71 -0
- package/dist/utils/brandPalette.cjs.js +3 -3
- package/dist/utils/brandPalette.d.ts +4 -2
- package/dist/utils/brandPalette.mjs +20 -20
- package/dist/vjdesigns-ui.css +1 -1
- package/package.json +90 -88
package/dist/ai/llms-full.txt
CHANGED
|
@@ -1,511 +1,511 @@
|
|
|
1
|
-
# @vjdesigns/vjdesigns-ui
|
|
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" | "
|
|
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" | "
|
|
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="
|
|
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` |
|
|
304
|
-
| a custom hex | generated from that color |
|
|
305
|
-
|
|
306
|
-
To choose a different one:
|
|
307
|
-
|
|
308
|
-
```tsx
|
|
309
|
-
<Theme accent="
|
|
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
|
-
-
|
|
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="
|
|
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). `"
|
|
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="
|
|
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
|
|
8115
|
+
VOLT_ACCENT
|
|
8036
8116
|
```
|
|
8037
8117
|
|
|
8038
|
-
|
|
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
|
|
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.
|