@vjdesigns/vjdesigns-ui 2.0.0 → 2.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (87) hide show
  1. package/README.md +781 -724
  2. package/dist/ai/llms-full.txt +550 -479
  3. package/dist/ai/llms.txt +1 -0
  4. package/dist/ai/registry.json +215 -6
  5. package/dist/components/data-display/Chat/Chat.cjs.js +1 -0
  6. package/dist/components/data-display/Chat/Chat.d.ts +114 -0
  7. package/dist/components/data-display/Chat/Chat.mjs +301 -0
  8. package/dist/components/data-display/Chat/Chat.module.css.cjs.js +1 -0
  9. package/dist/components/data-display/Chat/Chat.module.css.mjs +32 -0
  10. package/dist/components/data-display/Chat/index.d.ts +2 -0
  11. package/dist/components/data-display/Chat/streams.cjs.js +4 -0
  12. package/dist/components/data-display/Chat/streams.d.ts +32 -0
  13. package/dist/components/data-display/Chat/streams.mjs +35 -0
  14. package/dist/components/forms/Combobox/Combobox.cjs.js +1 -1
  15. package/dist/components/forms/Combobox/Combobox.mjs +5 -5
  16. package/dist/components/forms/TagInput/TagInput.cjs.js +1 -1
  17. package/dist/components/forms/TagInput/TagInput.mjs +5 -5
  18. package/dist/i18n/en.cjs.js +1 -1
  19. package/dist/i18n/en.mjs +21 -0
  20. package/dist/i18n/types.d.ts +35 -0
  21. package/dist/index.cjs.js +1 -1
  22. package/dist/index.d.ts +1 -0
  23. package/dist/index.mjs +85 -83
  24. package/dist/locales/ar.cjs.js +1 -1
  25. package/dist/locales/ar.mjs +21 -0
  26. package/dist/locales/bn.cjs.js +1 -1
  27. package/dist/locales/bn.mjs +21 -0
  28. package/dist/locales/cs.cjs.js +1 -1
  29. package/dist/locales/cs.mjs +21 -0
  30. package/dist/locales/da.cjs.js +1 -1
  31. package/dist/locales/da.mjs +21 -0
  32. package/dist/locales/de.cjs.js +1 -1
  33. package/dist/locales/de.mjs +21 -0
  34. package/dist/locales/el.cjs.js +1 -1
  35. package/dist/locales/el.mjs +21 -0
  36. package/dist/locales/es.cjs.js +1 -1
  37. package/dist/locales/es.mjs +21 -0
  38. package/dist/locales/fa.cjs.js +1 -1
  39. package/dist/locales/fa.mjs +21 -0
  40. package/dist/locales/fi.cjs.js +1 -1
  41. package/dist/locales/fi.mjs +21 -0
  42. package/dist/locales/fr.cjs.js +1 -1
  43. package/dist/locales/fr.mjs +21 -0
  44. package/dist/locales/he.cjs.js +1 -1
  45. package/dist/locales/he.mjs +21 -0
  46. package/dist/locales/hi.cjs.js +1 -1
  47. package/dist/locales/hi.mjs +21 -0
  48. package/dist/locales/hu.cjs.js +1 -1
  49. package/dist/locales/hu.mjs +21 -0
  50. package/dist/locales/id.cjs.js +1 -1
  51. package/dist/locales/id.mjs +21 -0
  52. package/dist/locales/it.cjs.js +1 -1
  53. package/dist/locales/it.mjs +21 -0
  54. package/dist/locales/ja.cjs.js +1 -1
  55. package/dist/locales/ja.mjs +21 -0
  56. package/dist/locales/ko.cjs.js +1 -1
  57. package/dist/locales/ko.mjs +21 -0
  58. package/dist/locales/nb.cjs.js +1 -1
  59. package/dist/locales/nb.mjs +21 -0
  60. package/dist/locales/nl.cjs.js +1 -1
  61. package/dist/locales/nl.mjs +21 -0
  62. package/dist/locales/pl.cjs.js +1 -1
  63. package/dist/locales/pl.mjs +21 -0
  64. package/dist/locales/pt-BR.cjs.js +1 -1
  65. package/dist/locales/pt-BR.mjs +21 -0
  66. package/dist/locales/ro.cjs.js +1 -1
  67. package/dist/locales/ro.mjs +21 -0
  68. package/dist/locales/ru.cjs.js +1 -1
  69. package/dist/locales/ru.mjs +21 -0
  70. package/dist/locales/sv.cjs.js +1 -1
  71. package/dist/locales/sv.mjs +21 -0
  72. package/dist/locales/th.cjs.js +1 -1
  73. package/dist/locales/th.mjs +21 -0
  74. package/dist/locales/tr.cjs.js +1 -1
  75. package/dist/locales/tr.mjs +21 -0
  76. package/dist/locales/uk.cjs.js +1 -1
  77. package/dist/locales/uk.mjs +21 -0
  78. package/dist/locales/ur.cjs.js +1 -1
  79. package/dist/locales/ur.mjs +21 -0
  80. package/dist/locales/vi.cjs.js +1 -1
  81. package/dist/locales/vi.mjs +21 -0
  82. package/dist/locales/zh-Hans.cjs.js +1 -1
  83. package/dist/locales/zh-Hans.mjs +21 -0
  84. package/dist/locales/zh-Hant.cjs.js +1 -1
  85. package/dist/locales/zh-Hant.mjs +21 -0
  86. package/dist/vjdesigns-ui.css +1 -1
  87. package/package.json +90 -88
@@ -1,4 +1,4 @@
1
- # @vjdesigns/vjdesigns-ui 1.5.0: full reference
1
+ # @vjdesigns/vjdesigns-ui 2.0.0: full reference
2
2
 
3
3
  > Accessible React 19 component library: WCAG 2.2 AA verified (axe on every story, a contrast check on every color pair), 31 languages with right-to-left support, themeable with any brand color (auto-adjusted to stay accessible), tree-shakable, no runtime dependencies besides React.
4
4
 
@@ -12,500 +12,500 @@
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" | "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
-
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";