@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
package/README.md CHANGED
@@ -1,724 +1,781 @@
1
- # vjdesigns-ui
2
-
3
- An accessible React component library from VJ Designs, built to meet WCAG 2.2 AA. It covers buttons, form controls, layout, navigation, overlays, feedback and data display, with light and dark modes and five brand palettes.
4
-
5
- 📚 **Live docs and Storybook:** [component.vjdesigns.org](https://component.vjdesigns.org)
6
- 📦 **npm:** [@vjdesigns/vjdesigns-ui](https://www.npmjs.com/package/@vjdesigns/vjdesigns-ui)
7
-
8
- ---
9
-
10
- ## Installation
11
-
12
- Install the package along with React and React DOM (version 18 or 19), which the components need in order to render:
13
-
14
- ```bash
15
- npm install @vjdesigns/vjdesigns-ui react react-dom
16
- ```
17
-
18
- If your project already uses React, you only need the first package:
19
-
20
- ```bash
21
- npm install @vjdesigns/vjdesigns-ui
22
- ```
23
-
24
- ### Import the styles
25
-
26
- 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.
27
-
28
- **React:** in `main.tsx` (or `App.tsx`)
29
-
30
- ```ts
31
- import "@vjdesigns/vjdesigns-ui/dist/vjdesigns-ui.css";
32
- ```
33
-
34
- **Angular:** add it to the `styles` array in `angular.json`
35
-
36
- ```json
37
- "styles": [
38
- "node_modules/@vjdesigns/vjdesigns-ui/dist/vjdesigns-ui.css",
39
- "src/styles.css"
40
- ]
41
- ```
42
-
43
- **Vue:** in `main.ts`
44
-
45
- ```ts
46
- import "@vjdesigns/vjdesigns-ui/dist/vjdesigns-ui.css";
47
- ```
48
-
49
- ---
50
-
51
- ## Usage
52
-
53
- 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.
54
-
55
- ### React
56
-
57
- ```tsx
58
- import { useState } from "react";
59
- import { Button, CheckBox } from "@vjdesigns/vjdesigns-ui";
60
-
61
- export function SignupForm() {
62
- const [accepted, setAccepted] = useState(false);
63
-
64
- return (
65
- <form>
66
- <CheckBox
67
- label="Accept terms"
68
- checked={accepted}
69
- onCheckedChange={setAccepted}
70
- />
71
- <Button
72
- variant="solid"
73
- tone="primary"
74
- size="md"
75
- disabled={!accepted}
76
- onClick={() => console.log("Submitted")}
77
- >
78
- Submit
79
- </Button>
80
- </form>
81
- );
82
- }
83
- ```
84
-
85
- ### Angular
86
-
87
- 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.
88
-
89
- ```ts
90
- // vj-button.component.ts
91
- import {
92
- AfterViewInit,
93
- Component,
94
- ElementRef,
95
- EventEmitter,
96
- Input,
97
- OnChanges,
98
- OnDestroy,
99
- Output,
100
- ViewChild,
101
- } from "@angular/core";
102
- import { createElement, type MouseEvent as ReactMouseEvent, type ReactElement } from "react";
103
- import { createRoot, Root } from "react-dom/client";
104
- import {
105
- Button,
106
- type ButtonProps,
107
- type ButtonVariant,
108
- type Size,
109
- type Tone,
110
- } from "@vjdesigns/vjdesigns-ui";
111
-
112
- // Button has overloaded types (for `href` and `as`). Narrowing it here lets createElement type-check.
113
- const ReactButton = Button as (props: ButtonProps) => ReactElement;
114
-
115
- @Component({
116
- selector: "vj-button",
117
- standalone: true,
118
- template: `<span #host></span>`,
119
- })
120
- export class VjButtonComponent implements AfterViewInit, OnChanges, OnDestroy {
121
- @ViewChild("host", { static: true }) host!: ElementRef<HTMLElement>;
122
-
123
- @Input() label = "";
124
- @Input() variant: ButtonVariant = "solid";
125
- @Input() tone: Tone = "primary";
126
- @Input() size: Size = "md";
127
- @Input() disabled = false;
128
- @Output() clicked = new EventEmitter<MouseEvent>();
129
-
130
- private root?: Root;
131
-
132
- ngAfterViewInit() {
133
- this.root = createRoot(this.host.nativeElement);
134
- this.render();
135
- }
136
-
137
- ngOnChanges() {
138
- this.render();
139
- }
140
-
141
- ngOnDestroy() {
142
- this.root?.unmount();
143
- }
144
-
145
- private render() {
146
- this.root?.render(
147
- createElement(
148
- ReactButton,
149
- {
150
- variant: this.variant,
151
- tone: this.tone,
152
- size: this.size,
153
- disabled: this.disabled,
154
- onClick: (event: ReactMouseEvent) => this.clicked.emit(event.nativeEvent),
155
- },
156
- this.label // the button's text is passed as children
157
- )
158
- );
159
- }
160
- }
161
- ```
162
-
163
- Import the wrapper wherever you need it:
164
-
165
- ```ts
166
- // app.component.ts
167
- import { Component } from "@angular/core";
168
- import { VjButtonComponent } from "./vj-button.component";
169
-
170
- @Component({
171
- selector: "app-root",
172
- standalone: true,
173
- imports: [VjButtonComponent],
174
- template: `<vj-button label="Submit" tone="primary" (clicked)="onSubmit()" />`,
175
- })
176
- export class AppComponent {
177
- onSubmit() {
178
- console.log("Submitted");
179
- }
180
- }
181
- ```
182
-
183
- ### Vue 3
184
-
185
- Make a wrapper component that mounts the React component when it loads and re-renders it whenever its props change.
186
-
187
- ```vue
188
- <!-- VjButton.vue -->
189
- <script setup lang="ts">
190
- import { onBeforeUnmount, onMounted, ref, watch } from "vue";
191
- import { createElement, type MouseEvent as ReactMouseEvent, type ReactElement } from "react";
192
- import { createRoot, type Root } from "react-dom/client";
193
- import {
194
- Button,
195
- type ButtonProps,
196
- type ButtonVariant,
197
- type Size,
198
- type Tone,
199
- } from "@vjdesigns/vjdesigns-ui";
200
-
201
- // Button has overloaded types (for `href` and `as`). Narrowing it here lets createElement type-check.
202
- const ReactButton = Button as (props: ButtonProps) => ReactElement;
203
-
204
- const props = withDefaults(
205
- defineProps<{
206
- label: string;
207
- variant?: ButtonVariant;
208
- tone?: Tone;
209
- size?: Size;
210
- disabled?: boolean;
211
- }>(),
212
- { variant: "solid", tone: "primary", size: "md", disabled: false }
213
- );
214
-
215
- const emit = defineEmits<{ click: [event: MouseEvent] }>();
216
-
217
- const host = ref<HTMLElement | null>(null);
218
- let root: Root | undefined;
219
-
220
- function render() {
221
- root?.render(
222
- createElement(
223
- ReactButton,
224
- {
225
- variant: props.variant,
226
- tone: props.tone,
227
- size: props.size,
228
- disabled: props.disabled,
229
- onClick: (event: ReactMouseEvent) => emit("click", event.nativeEvent),
230
- },
231
- props.label // the button's text is passed as children
232
- )
233
- );
234
- }
235
-
236
- onMounted(() => {
237
- root = createRoot(host.value!);
238
- render();
239
- });
240
-
241
- watch(() => ({ ...props }), render);
242
-
243
- onBeforeUnmount(() => root?.unmount());
244
- </script>
245
-
246
- <template>
247
- <span ref="host" />
248
- </template>
249
- ```
250
-
251
- Use it like any other Vue component:
252
-
253
- ```vue
254
- <script setup lang="ts">
255
- import VjButton from "./VjButton.vue";
256
-
257
- const onSubmit = () => console.log("Submitted");
258
- </script>
259
-
260
- <template>
261
- <VjButton label="Submit" tone="primary" @click="onSubmit" />
262
- </template>
263
- ```
264
-
265
- > **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.
266
-
267
- ---
268
-
269
- ## Theming
270
-
271
- Wrap any part of your app in `Theme` to set the color mode, brand palette, and accent for everything inside it:
272
-
273
- ```tsx
274
- import { Theme, Button } from "@vjdesigns/vjdesigns-ui";
275
-
276
- <Theme mode="dark" brand="purple" fill>
277
- <Button>Dark, purple button</Button>
278
- </Theme>
279
- ```
280
-
281
- | Prop | Type | Default | Description |
282
- | --- | --- | --- | --- |
283
- | `mode` | `"light" \| "dark"` | inherited | Color mode for everything inside. |
284
- | `brand` | `"black" \| "blue" \| "gold" \| "green" \| "purple"`, a hex color, or `{ light, dark }` | inherited | Brand palette for buttons, links, focus rings and indicators. |
285
- | `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. |
286
- | `fill` | `boolean` | `false` | Paints the theme's background and text color on the wrapper. |
287
-
288
- Outside React (or to theme the whole page), set the attributes on `<html>` instead:
289
-
290
- ```html
291
- <html data-theme="dark" data-brand="purple" data-accent="lime">
292
- ```
293
-
294
- ### Accent color
295
-
296
- 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:
297
-
298
- | Brand | Default accent |
299
- | --- | --- |
300
- | `blue` (default) | sky blue `#76c7ff` |
301
- | `purple` | lavender `#c0b0ff` |
302
- | `gold` | amber `#f5ad5f` |
303
- | `green` | mint `#72d4ad` |
304
- | `black` | lime `#c8f031` (black has no hue to build on) |
305
- | a custom hex | generated from that color |
306
-
307
- To choose a different one:
308
-
309
- ```tsx
310
- <Theme accent="lime">…</Theme> // the original bright yellow-green ("volt", its former name, still works)
311
- <Theme accent="#ff6b00">…</Theme> // any color
312
- <Theme accent="brand">…</Theme> // back to the brand's own, inside an area with another accent
313
- ```
314
-
315
- 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.
316
-
317
- ### Custom brand colors
318
-
319
- Pass any hex color as `brand` and a full palette is generated for both modes:
320
-
321
- ```tsx
322
- <Theme brand="#e11d48">…</Theme>
323
- <Theme brand={{ light: "#4338ca", dark: "#f472b6" }}>…</Theme>
324
- ```
325
-
326
- 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.
327
-
328
- To define a brand in CSS instead (for the whole page, applied before any JavaScript runs), generate a stylesheet rule with `brandCss`:
329
-
330
- ```ts
331
- import { brandCss } from "@vjdesigns/vjdesigns-ui";
332
-
333
- brandCss("acme", "#e11d48"); // '[data-brand="acme"] { --brand-light-primary: … }'
334
- ```
335
-
336
- 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.
337
-
338
- ---
339
-
340
- ## Languages
341
-
342
- 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:
343
-
344
- ```tsx
345
- import { LocaleProvider } from "@vjdesigns/vjdesigns-ui";
346
- import es from "@vjdesigns/vjdesigns-ui/locales/es";
347
-
348
- <LocaleProvider locale="es" messages={es}>
349
- <App />
350
- </LocaleProvider>
351
- ```
352
-
353
- `LocaleProvider` sets:
354
-
355
- - **Built-in text** for every component, from `messages`.
356
- - **Dates and numbers** in the locale's format (`23/10/2026`, `1.234,5`), and plurals that follow the language's rules.
357
- - **Writing direction**: Arabic, Hebrew, Persian, and Urdu get `dir="rtl"`, and layouts and arrow keys mirror.
358
- - **`lang`** on its wrapper, so screen readers pronounce the text correctly. Popups (menus, dialogs, tooltips) keep the language of the area that opened them.
359
-
360
- 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.
361
-
362
- 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).
363
-
364
- ### What gets translated
365
-
366
- 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}>`:
367
-
368
- | | Translated? |
369
- | --- | --- |
370
- | Text your app passes in, like `<Button>Save</Button>` | No: your app translates it |
371
- | Built-in names, like "Close", "Next page", "Clear search" | Yes: "Schließen", "Nächste Seite", "Suche löschen" |
372
- | Screen-reader announcements, like "3 results available" | Yes: "3 Ergebnisse verfügbar" |
373
- | Validation messages, empty states, file-upload prompts | Yes |
374
- | Dates and numbers | Yes: "23.10.2026", "4,5" |
375
-
376
- 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)).
377
-
378
- ### Bundled languages
379
-
380
- `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).
381
-
382
- 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.
383
-
384
- ### Changing a few strings
385
-
386
- Pass only the strings you want to change. Everything else falls back to the outer provider's language (English at the top):
387
-
388
- ```tsx
389
- <LocaleProvider locale="en-GB" messages={{ pagination: { next: "Onwards" } }}>
390
- ```
391
-
392
- 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.
393
-
394
- ### Other languages
395
-
396
- For a language that isn't bundled, write a complete messages object. TypeScript flags any missing string. Use English as the template:
397
-
398
- ```ts
399
- import { en, plural, type Messages } from "@vjdesigns/vjdesigns-ui";
400
-
401
- const p = plural("sw");
402
- export const sw: Messages = {
403
- ...en,
404
- pagination: { ...en.pagination, next: "Ukurasa unaofuata", previous: "Ukurasa uliopita" },
405
- searchInput: {
406
- ...en.searchInput,
407
- results: (count) => p(count, { "=0": "Hakuna matokeo", one: "Tokeo #", other: "Matokeo #" }),
408
- },
409
- // ...
410
- };
411
- ```
412
-
413
- `plural(locale)` picks the right plural form for any language (`#` is the formatted number).
414
-
415
- ### Switching language at runtime
416
-
417
- List the languages your app offers as loaders, so each one is downloaded only when picked:
418
-
419
- ```tsx
420
- import { LocaleProvider, type Messages } from "@vjdesigns/vjdesigns-ui";
421
-
422
- const loaders: Record<string, () => Promise<{ default: Messages }>> = {
423
- es: () => import("@vjdesigns/vjdesigns-ui/locales/es"),
424
- fr: () => import("@vjdesigns/vjdesigns-ui/locales/fr"),
425
- ar: () => import("@vjdesigns/vjdesigns-ui/locales/ar"),
426
- };
427
-
428
- function App({ lang }: { lang: string }) {
429
- const [messages, setMessages] = useState<Messages>();
430
- useEffect(() => {
431
- const load = loaders[lang];
432
- if (load) load().then((mod) => setMessages(mod.default));
433
- else setMessages(undefined); // English
434
- }, [lang]);
435
- // Until a language loads, the previous one (or English) shows.
436
- return <LocaleProvider locale={lang} messages={messages}>…</LocaleProvider>;
437
- }
438
- ```
439
-
440
- For server rendering, also set `<html lang="…" dir="…">` on the page. `getLocaleDirection(locale)` returns `"ltr"` or `"rtl"`.
441
-
442
- ---
443
-
444
- ## Form validation
445
-
446
- > **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.
447
-
448
- `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.
449
-
450
- ```tsx
451
- import {
452
- useFormValidation, rules, ErrorSummary, Field, Input, PasswordInput, Button,
453
- } from "@vjdesigns/vjdesigns-ui";
454
-
455
- function SignUp() {
456
- const form = useFormValidation({
457
- rules: {
458
- confirm: rules.matches("password", "The passwords don't match."),
459
- serverIp: rules.ip(), // IPv4 or IPv6
460
- },
461
- // Runs only when everything is valid. Return the server's errors to show them on their fields.
462
- onValid: async (values) => {
463
- const response = await api.signUp(values);
464
- return response.errors; // e.g. { username: "That username is taken." }
465
- },
466
- });
467
-
468
- return (
469
- <form {...form.formProps}>
470
- <ErrorSummary ref={form.summaryRef} errors={form.errorList} />
471
- <Field label="Email" required error={form.errors.email}>
472
- <Input name="email" type="email" />
473
- </Field>
474
- <Field label="Password" required error={form.errors.password}>
475
- <PasswordInput name="password" minLength={12} />
476
- </Field>
477
- <Field label="Confirm password" required error={form.errors.confirm}>
478
- <PasswordInput name="confirm" />
479
- </Field>
480
- <Field label="Server IP" error={form.errors.serverIp}>
481
- <Input name="serverIp" />
482
- </Field>
483
- <Button type="submit" loading={form.isSubmitting}>Create account</Button>
484
- </form>
485
- );
486
- }
487
- ```
488
-
489
- How it behaves:
490
-
491
- - **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.
492
- - **Errors are keyed by the field's `name`.** Pass `form.errors.<name>` to each `Field`'s `error`.
493
- - **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.
494
- - **Server errors** (returned from `onValid`, or set with `form.setErrors({...})`) show on their fields and stay until that field is edited.
495
- - **Emails** must have a real domain (`name@example.com`). The browser's own check also accepts `name@localhost`.
496
-
497
- ### Rules and format checks
498
-
499
- | Rule | Checks |
500
- | --- | --- |
501
- | `rules.required()` | Not empty (for controls without a native `required`) |
502
- | `rules.email()` | An email address in the usual format, with a top-level domain |
503
- | `rules.ip()` / `rules.ip({ version: 4 })` / `rules.ip({ version: 6 })` | An IPv4 or IPv6 address |
504
- | `rules.minLength(n)` / `rules.maxLength(n)` | Length in characters |
505
- | `rules.pattern(/regex/, "message")` | The whole value matches |
506
- | `rules.matches("otherField")` | Equals another field (confirm password) |
507
- | your own `(value, { values }) => message` | Anything else |
508
-
509
- Each rule takes an optional custom message. The checks behind them are exported for use anywhere, including your server:
510
-
511
- - `isEmail(value)`
512
- - `isIPv4(value)`: four numbers 0–255, no leading zeros
513
- - `isIPv6(value)`: including `::` shorthand and IPv4 tails like `::ffff:192.168.0.1`
514
- - `isIP(value, version?)`
515
-
516
- ---
517
-
518
- ## Hooks
519
-
520
- Tested hooks for everyday UI work, exported from the package root (`import { useDisclosure } from "@vjdesigns/vjdesigns-ui"`). All are safe in server rendering and tree-shakable: each costs only itself, mostly well under 1 KB. Live demos are in Storybook under Hooks.
521
-
522
- | Hook | What it does |
523
- | --- | --- |
524
- | `useDisclosure(initial?)` | Open/closed state with stable `open`, `close`, `toggle` handlers (for Modal, Drawer, Popover) |
525
- | `useToggle(values?)` | Cycles through values: a boolean, or a list like `["list", "grid"]` |
526
- | `usePrevious(value)` | The value from before its last change |
527
- | `useDebouncedValue(value, ms)` | The value, updated after it stops changing (search as you type) |
528
- | `useDebouncedCallback(fn, ms)` | A function that runs once after calls stop, with `cancel` and `flush` |
529
- | `useThrottledCallback(fn, ms)` | A function that runs at most once per interval (scroll, resize) |
530
- | `useInterval(fn, ms \| null)` / `useTimeout(fn, ms \| null)` | Timers that always call the latest function; `null` pauses |
531
- | `useLocalStorage(key, default)` / `useSessionStorage` | State saved in storage, synced across components and tabs, and never throws when storage is blocked |
532
- | `useClipboard()` | `copy(text)` with a `copied` flag, and a fallback when the Clipboard API is blocked |
533
- | `useHotkeys([[keys, fn]])` | Page shortcuts. `mod` is ⌘ or Ctrl. Plain keys don't fire while typing. `getHotkeyHandler` does the same for one element |
534
- | `useEventListener(type, fn, target?)` | An event listener that always calls the latest handler |
535
- | `useClickOutside(fn)` | Calls `fn` on a press or focus move outside the element |
536
- | `useIntersection(options)` | Whether an element is in view (lazy loading, entrance effects) |
537
- | `useElementSize()` | An element's size as it resizes |
538
- | `useVirtualizer(options)` | Renders only the visible items of a long list (what `VirtualList` uses), with variable heights and `scrollToIndex` |
539
- | `useMediaQuery(query)` / `useResponsiveValue(value)` | A synced media query, or a responsive value resolved for the viewport |
540
- | `useHydrated()` | False during server rendering, true after (avoids hydration mismatches) |
541
- | `useReducedMotion()` / `useColorScheme()` | The user's motion and color-scheme preferences |
542
- | `useOnline()` / `useDocumentVisibility()` / `useViewportSize()` | Connection, tab visibility, and viewport size |
543
- | `useControllableState`, `useFormValidation`, `useFocusTrap`, `useScrollLock`, … | The building blocks the components use, for your own components |
544
-
545
- ---
546
-
547
- ## AI assistants
548
-
549
- The package ships documentation written for AI coding assistants, generated from the source on every build, so it always matches the version you have installed:
550
-
551
- | File | What it is |
552
- | --- | --- |
553
- | `@vjdesigns/vjdesigns-ui/llms.txt` | A short index in the [llms.txt](https://llmstxt.org) format: setup essentials and one line per component |
554
- | `@vjdesigns/vjdesigns-ui/llms-full.txt` | The full reference in one file: guides, and every component's props (type, default, description), subcomponents, and examples |
555
- | `@vjdesigns/vjdesigns-ui/registry.json` | The same, as structured JSON |
556
-
557
- The files are in `node_modules/@vjdesigns/vjdesigns-ui/dist/ai/`. Point your assistant's rules or context at `llms-full.txt`.
558
-
559
- ### MCP server
560
-
561
- For assistants that support the [Model Context Protocol](https://modelcontextprotocol.io), the package includes a small MCP server with four tools:
562
-
563
- - `list_components`
564
- - `get_component`, for exact props
565
- - `search_docs`
566
- - `get_guide`
567
-
568
- It reads the docs of your installed version and needs no network access.
569
-
570
- Claude Code:
571
-
572
- ```bash
573
- claude mcp add vjdesigns-ui -- npx -p @vjdesigns/vjdesigns-ui vjdesigns-ui-mcp
574
- ```
575
-
576
- Cursor (`.cursor/mcp.json`), Claude Desktop, and other clients (VS Code's `.vscode/mcp.json` uses `"servers"` instead of `"mcpServers"`):
577
-
578
- ```json
579
- {
580
- "mcpServers": {
581
- "vjdesigns-ui": {
582
- "command": "npx",
583
- "args": ["-p", "@vjdesigns/vjdesigns-ui", "vjdesigns-ui-mcp"]
584
- }
585
- }
586
- }
587
- ```
588
-
589
- ---
590
-
591
- ## Components
592
-
593
- Every component below is exported from the package root. Full prop docs and live examples are in [Storybook](https://component.vjdesigns.org).
594
-
595
- | Category | Components |
596
- | --- | --- |
597
- | **Primitives** | Button, IconButton, ButtonGroup, Icon, Kbd, Typography |
598
- | **Layout** | Box, Container, Divider, Flex, Grid, ResizablePanels, ScrollArea, Stack |
599
- | **Forms** | CheckBox, CheckBoxGroup, ColorPicker, Combobox, DatePicker, DateRangePicker, DateTimePicker, Field, FileUpload, Input, NumberInput, PasswordInput, PinInput, Radio, Rating, SearchInput, SegmentedControl, Select, Slider, Switch, TagInput, Textarea, TimePicker, ToggleGroup |
600
- | **Navigation** | Breadcrumbs, ContextMenu, Link, Menu, Navbar, Pagination, Sidebar, SkipLink, Stepper, Tabs |
601
- | **Overlays** | CommandPalette, Drawer, HoverCard, Modal, Popover, Tooltip |
602
- | **Feedback** | Alert, Banner, ErrorSummary, Progress, Skeleton, Spinner, Toast |
603
- | **Data display** | Accordion, Avatar, Badge, Card, Carousel, Chip, CodeBlock, DataGrid, EmptyState, Image, List, Stat, Table, Timeline, TreeView, VirtualList |
604
- | **Utilities** | Portal, Theme, VisuallyHidden |
605
-
606
- ### Button
607
-
608
- ```tsx
609
- <Button variant="solid" tone="primary">Save</Button>
610
- <Button variant="outline" tone="danger" startIcon={<TrashIcon />}>Delete</Button>
611
- <Button href="/pricing" variant="link">See pricing</Button>
612
- <Button loading loadingText="Saving…">Save</Button>
613
- ```
614
-
615
- | Prop | Type | Default | Description |
616
- | --- | --- | --- | --- |
617
- | `children` | `ReactNode` | — | The visible label. |
618
- | `variant` | `"solid" \| "soft" \| "outline" \| "ghost" \| "link" \| "inverse"` | `"solid"` | Visual style. |
619
- | `tone` | `"neutral" \| "primary" \| "accent" \| "success" \| "warning" \| "danger" \| "info"` | `"primary"` | Semantic color. |
620
- | `size` | `"sm" \| "md" \| "lg"` | `"md"` | Height, padding and type size. |
621
- | `radius` | `"none" \| "xs" \| "sm" \| "md" \| "lg" \| "xl" \| "2xl" \| "pill"` | `"pill"` | Corner rounding. |
622
- | `startIcon` / `endIcon` | `ReactNode` | — | Decorative icon before or after the label. Hidden from screen readers. |
623
- | `loading` | `boolean` | `false` | Shows a spinner, sets `aria-busy` and ignores clicks. |
624
- | `loadingText` | `ReactNode` | — | Label shown next to the spinner while loading. |
625
- | `fullWidth` | `boolean` | `false` | Stretches the button to fill its container. |
626
- | `compact` | `boolean` | `false` | Tighter side padding for short labels like "OK". |
627
- | `disabled` | `boolean` | `false` | Disables the button. |
628
- | `href` | `string` | — | Renders the button as an `<a>` link with the same styling. |
629
- | `as` | `ElementType` | — | Renders as your own component, such as a React Router or Next.js `Link`. |
630
-
631
- Also accepts all standard `<button>` attributes (or `<a>` attributes when `href` is set), such as `onClick` and `type`.
632
-
633
- For icon-only buttons, use `IconButton`. `aria-label` is required:
634
-
635
- ```tsx
636
- <IconButton aria-label="Close" variant="ghost"><CloseIcon /></IconButton>
637
- ```
638
-
639
- To group related buttons, use `ButtonGroup`. It passes `variant`, `tone`, `size`, `radius` and `disabled` down to every button inside.
640
-
641
- ### CheckBox
642
-
643
- ```tsx
644
- <CheckBox label="Email me updates" description="About once a month." />
645
- <CheckBox label="Remember me" labelPosition="start" defaultChecked />
646
- ```
647
-
648
- | Prop | Type | Default | Description |
649
- | --- | --- | --- | --- |
650
- | `label` | `ReactNode` | — | Visible label. Clicking it toggles the box. Without it, pass `aria-label`. |
651
- | `description` | `ReactNode` | — | Supporting text below the label, linked with `aria-describedby`. |
652
- | `checked` | `boolean` | — | Checked state (controlled). |
653
- | `defaultChecked` | `boolean` | — | Initial checked state (uncontrolled). |
654
- | `onCheckedChange` | `(checked: boolean) => void` | — | Called with the new state when the user toggles the box. |
655
- | `indeterminate` | `boolean` | `false` | Shows a "mixed" dash, for a "select all" box whose options are partly checked. |
656
- | `labelPosition` | `"start" \| "end"` | `"end"` | Puts the label before (`start`) or after (`end`) the box. |
657
- | `size` | `"sm" \| "md" \| "lg"` | `"md"` | Box and text size. |
658
- | `tone` | `Tone` | `"primary"` | Fill color when checked. |
659
- | `invalid` | `boolean` | `false` | Danger styling plus `aria-invalid`. |
660
- | `readOnly` | `boolean` | `false` | Keeps the box focusable but prevents toggling. |
661
-
662
- Also accepts native `<input type="checkbox">` attributes, such as `name`, `required` and `disabled`.
663
-
664
- To manage several options as one `string[]` value, use `CheckBoxGroup`:
665
-
666
- ```tsx
667
- <CheckBoxGroup label="Toppings" defaultValue={["cheese"]} onValueChange={setToppings}>
668
- <CheckBox value="cheese" label="Cheese" />
669
- <CheckBox value="peppers" label="Peppers" />
670
- </CheckBoxGroup>
671
- ```
672
-
673
- ---
674
-
675
- ## Pro
676
-
677
- Everything above is free. `@vjdesigns/vjdesigns-ui-pro` is a commercial add-on for teams that need more from their data grids:
678
-
679
- - **DataGridPro:** everything `DataGrid` does, plus resizable columns (pointer and keyboard), saved column layouts, and columns pinned to either edge.
680
- - **CSV export:** export exactly what a grid shows, safe to open in a spreadsheet.
681
- - **Charts:** accessible `BarChart` and `LineChart`: keyboard-navigable points with spoken values, a data table view, color-blind-safe series colors, and High Contrast support.
682
- - **Account forms:** ready-made sign-in, sign-up, forgot-password, reset-password, and verification-code forms that you connect to your own backend, with validation, autofill, passkey support, and safe error messages built in.
683
- - **Figma kit:** a plugin that creates the library's variables (brands, light and dark themes, spacing, type) and shadow styles in Figma, generated from the same tokens as the code, plus W3C design token files.
684
-
685
- It uses the same tokens, themes, languages, and accessibility standard as the free package.
686
-
687
- ---
688
-
689
- ## Development
690
-
691
- ```bash
692
- npm run storybook # run Storybook locally at http://localhost:6006
693
- npm run build # type-check and build the library to dist/
694
- npm run build-storybook # build a static Storybook site
695
- ```
696
-
697
- Components live under `src/components/<category>/<Name>/`, with each component's implementation, CSS module and Storybook stories kept together. Shared design tokens live in `src/styles/tokens.css`. The build bundles all styles into `dist/vjdesigns-ui.css`.
698
-
699
- ### Releasing
700
-
701
- ```bash
702
- npm whoami # check you're logged in (if not: npm login)
703
- npm run build
704
- npm version patch # or minor / major
705
- npm publish
706
- git push --follow-tags
707
- ```
708
-
709
- See [RELEASING.md](RELEASING.md) for the full process, including how to update the projects that use the library and fixes for common errors.
710
-
711
- ---
712
-
713
- ## Access to the production Storybook
714
-
715
- 1. Go to [component.vjdesigns.org](https://component.vjdesigns.org/).
716
- 2. Enter your email in the Cloudflare prompt to receive a 6-digit access code.
717
-
718
- <img width="1917" height="927" alt="Cloudflare access prompt for component.vjdesigns.org" src="https://github.com/user-attachments/assets/bfe58737-52d2-4bf3-b7f7-72ba5ee54969" />
719
-
720
- ---
721
-
722
- ## License
723
-
724
- ISC
1
+ # vjdesigns-ui
2
+
3
+ An accessible React component library from VJ Designs, built to meet WCAG 2.2 AA. It covers buttons, form controls, layout, navigation, overlays, feedback and data display, with light and dark modes and five brand palettes.
4
+
5
+ 📚 **Live docs and Storybook:** [component.vjdesigns.org](https://component.vjdesigns.org)
6
+ 📦 **npm:** [@vjdesigns/vjdesigns-ui](https://www.npmjs.com/package/@vjdesigns/vjdesigns-ui)
7
+
8
+ ---
9
+
10
+ ## Installation
11
+
12
+ Install the package along with React and React DOM (version 18 or 19), which the components need in order to render:
13
+
14
+ ```bash
15
+ npm install @vjdesigns/vjdesigns-ui react react-dom
16
+ ```
17
+
18
+ If your project already uses React, you only need the first package:
19
+
20
+ ```bash
21
+ npm install @vjdesigns/vjdesigns-ui
22
+ ```
23
+
24
+ ### Import the styles
25
+
26
+ 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.
27
+
28
+ **React:** in `main.tsx` (or `App.tsx`)
29
+
30
+ ```ts
31
+ import "@vjdesigns/vjdesigns-ui/dist/vjdesigns-ui.css";
32
+ ```
33
+
34
+ **Angular:** add it to the `styles` array in `angular.json`
35
+
36
+ ```json
37
+ "styles": [
38
+ "node_modules/@vjdesigns/vjdesigns-ui/dist/vjdesigns-ui.css",
39
+ "src/styles.css"
40
+ ]
41
+ ```
42
+
43
+ **Vue:** in `main.ts`
44
+
45
+ ```ts
46
+ import "@vjdesigns/vjdesigns-ui/dist/vjdesigns-ui.css";
47
+ ```
48
+
49
+ ---
50
+
51
+ ## Usage
52
+
53
+ 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.
54
+
55
+ ### React
56
+
57
+ ```tsx
58
+ import { useState } from "react";
59
+ import { Button, CheckBox } from "@vjdesigns/vjdesigns-ui";
60
+
61
+ export function SignupForm() {
62
+ const [accepted, setAccepted] = useState(false);
63
+
64
+ return (
65
+ <form>
66
+ <CheckBox
67
+ label="Accept terms"
68
+ checked={accepted}
69
+ onCheckedChange={setAccepted}
70
+ />
71
+ <Button
72
+ variant="solid"
73
+ tone="primary"
74
+ size="md"
75
+ disabled={!accepted}
76
+ onClick={() => console.log("Submitted")}
77
+ >
78
+ Submit
79
+ </Button>
80
+ </form>
81
+ );
82
+ }
83
+ ```
84
+
85
+ ### Angular
86
+
87
+ 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.
88
+
89
+ ```ts
90
+ // vj-button.component.ts
91
+ import {
92
+ AfterViewInit,
93
+ Component,
94
+ ElementRef,
95
+ EventEmitter,
96
+ Input,
97
+ OnChanges,
98
+ OnDestroy,
99
+ Output,
100
+ ViewChild,
101
+ } from "@angular/core";
102
+ import { createElement, type MouseEvent as ReactMouseEvent, type ReactElement } from "react";
103
+ import { createRoot, Root } from "react-dom/client";
104
+ import {
105
+ Button,
106
+ type ButtonProps,
107
+ type ButtonVariant,
108
+ type Size,
109
+ type Tone,
110
+ } from "@vjdesigns/vjdesigns-ui";
111
+
112
+ // Button has overloaded types (for `href` and `as`). Narrowing it here lets createElement type-check.
113
+ const ReactButton = Button as (props: ButtonProps) => ReactElement;
114
+
115
+ @Component({
116
+ selector: "vj-button",
117
+ standalone: true,
118
+ template: `<span #host></span>`,
119
+ })
120
+ export class VjButtonComponent implements AfterViewInit, OnChanges, OnDestroy {
121
+ @ViewChild("host", { static: true }) host!: ElementRef<HTMLElement>;
122
+
123
+ @Input() label = "";
124
+ @Input() variant: ButtonVariant = "solid";
125
+ @Input() tone: Tone = "primary";
126
+ @Input() size: Size = "md";
127
+ @Input() disabled = false;
128
+ @Output() clicked = new EventEmitter<MouseEvent>();
129
+
130
+ private root?: Root;
131
+
132
+ ngAfterViewInit() {
133
+ this.root = createRoot(this.host.nativeElement);
134
+ this.render();
135
+ }
136
+
137
+ ngOnChanges() {
138
+ this.render();
139
+ }
140
+
141
+ ngOnDestroy() {
142
+ this.root?.unmount();
143
+ }
144
+
145
+ private render() {
146
+ this.root?.render(
147
+ createElement(
148
+ ReactButton,
149
+ {
150
+ variant: this.variant,
151
+ tone: this.tone,
152
+ size: this.size,
153
+ disabled: this.disabled,
154
+ onClick: (event: ReactMouseEvent) => this.clicked.emit(event.nativeEvent),
155
+ },
156
+ this.label // the button's text is passed as children
157
+ )
158
+ );
159
+ }
160
+ }
161
+ ```
162
+
163
+ Import the wrapper wherever you need it:
164
+
165
+ ```ts
166
+ // app.component.ts
167
+ import { Component } from "@angular/core";
168
+ import { VjButtonComponent } from "./vj-button.component";
169
+
170
+ @Component({
171
+ selector: "app-root",
172
+ standalone: true,
173
+ imports: [VjButtonComponent],
174
+ template: `<vj-button label="Submit" tone="primary" (clicked)="onSubmit()" />`,
175
+ })
176
+ export class AppComponent {
177
+ onSubmit() {
178
+ console.log("Submitted");
179
+ }
180
+ }
181
+ ```
182
+
183
+ ### Vue 3
184
+
185
+ Make a wrapper component that mounts the React component when it loads and re-renders it whenever its props change.
186
+
187
+ ```vue
188
+ <!-- VjButton.vue -->
189
+ <script setup lang="ts">
190
+ import { onBeforeUnmount, onMounted, ref, watch } from "vue";
191
+ import { createElement, type MouseEvent as ReactMouseEvent, type ReactElement } from "react";
192
+ import { createRoot, type Root } from "react-dom/client";
193
+ import {
194
+ Button,
195
+ type ButtonProps,
196
+ type ButtonVariant,
197
+ type Size,
198
+ type Tone,
199
+ } from "@vjdesigns/vjdesigns-ui";
200
+
201
+ // Button has overloaded types (for `href` and `as`). Narrowing it here lets createElement type-check.
202
+ const ReactButton = Button as (props: ButtonProps) => ReactElement;
203
+
204
+ const props = withDefaults(
205
+ defineProps<{
206
+ label: string;
207
+ variant?: ButtonVariant;
208
+ tone?: Tone;
209
+ size?: Size;
210
+ disabled?: boolean;
211
+ }>(),
212
+ { variant: "solid", tone: "primary", size: "md", disabled: false }
213
+ );
214
+
215
+ const emit = defineEmits<{ click: [event: MouseEvent] }>();
216
+
217
+ const host = ref<HTMLElement | null>(null);
218
+ let root: Root | undefined;
219
+
220
+ function render() {
221
+ root?.render(
222
+ createElement(
223
+ ReactButton,
224
+ {
225
+ variant: props.variant,
226
+ tone: props.tone,
227
+ size: props.size,
228
+ disabled: props.disabled,
229
+ onClick: (event: ReactMouseEvent) => emit("click", event.nativeEvent),
230
+ },
231
+ props.label // the button's text is passed as children
232
+ )
233
+ );
234
+ }
235
+
236
+ onMounted(() => {
237
+ root = createRoot(host.value!);
238
+ render();
239
+ });
240
+
241
+ watch(() => ({ ...props }), render);
242
+
243
+ onBeforeUnmount(() => root?.unmount());
244
+ </script>
245
+
246
+ <template>
247
+ <span ref="host" />
248
+ </template>
249
+ ```
250
+
251
+ Use it like any other Vue component:
252
+
253
+ ```vue
254
+ <script setup lang="ts">
255
+ import VjButton from "./VjButton.vue";
256
+
257
+ const onSubmit = () => console.log("Submitted");
258
+ </script>
259
+
260
+ <template>
261
+ <VjButton label="Submit" tone="primary" @click="onSubmit" />
262
+ </template>
263
+ ```
264
+
265
+ > **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.
266
+
267
+ ---
268
+
269
+ ## Theming
270
+
271
+ Wrap any part of your app in `Theme` to set the color mode, brand palette, and accent for everything inside it:
272
+
273
+ ```tsx
274
+ import { Theme, Button } from "@vjdesigns/vjdesigns-ui";
275
+
276
+ <Theme mode="dark" brand="purple" fill>
277
+ <Button>Dark, purple button</Button>
278
+ </Theme>
279
+ ```
280
+
281
+ | Prop | Type | Default | Description |
282
+ | --- | --- | --- | --- |
283
+ | `mode` | `"light" \| "dark"` | inherited | Color mode for everything inside. |
284
+ | `brand` | `"black" \| "blue" \| "gold" \| "green" \| "purple"`, a hex color, or `{ light, dark }` | inherited | Brand palette for buttons, links, focus rings and indicators. |
285
+ | `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. |
286
+ | `fill` | `boolean` | `false` | Paints the theme's background and text color on the wrapper. |
287
+
288
+ Outside React (or to theme the whole page), set the attributes on `<html>` instead:
289
+
290
+ ```html
291
+ <html data-theme="dark" data-brand="purple" data-accent="lime">
292
+ ```
293
+
294
+ ### Accent color
295
+
296
+ 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:
297
+
298
+ | Brand | Default accent |
299
+ | --- | --- |
300
+ | `blue` (default) | sky blue `#76c7ff` |
301
+ | `purple` | lavender `#c0b0ff` |
302
+ | `gold` | amber `#f5ad5f` |
303
+ | `green` | mint `#72d4ad` |
304
+ | `black` | lime `#c8f031` (black has no hue to build on) |
305
+ | a custom hex | generated from that color |
306
+
307
+ To choose a different one:
308
+
309
+ ```tsx
310
+ <Theme accent="lime">…</Theme> // the original bright yellow-green ("volt", its former name, still works)
311
+ <Theme accent="#ff6b00">…</Theme> // any color
312
+ <Theme accent="brand">…</Theme> // back to the brand's own, inside an area with another accent
313
+ ```
314
+
315
+ 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.
316
+
317
+ ### Custom brand colors
318
+
319
+ Pass any hex color as `brand` and a full palette is generated for both modes:
320
+
321
+ ```tsx
322
+ <Theme brand="#e11d48">…</Theme>
323
+ <Theme brand={{ light: "#4338ca", dark: "#f472b6" }}>…</Theme>
324
+ ```
325
+
326
+ 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.
327
+
328
+ To define a brand in CSS instead (for the whole page, applied before any JavaScript runs), generate a stylesheet rule with `brandCss`:
329
+
330
+ ```ts
331
+ import { brandCss } from "@vjdesigns/vjdesigns-ui";
332
+
333
+ brandCss("acme", "#e11d48"); // '[data-brand="acme"] { --brand-light-primary: … }'
334
+ ```
335
+
336
+ 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.
337
+
338
+ ---
339
+
340
+ ## Languages
341
+
342
+ 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:
343
+
344
+ ```tsx
345
+ import { LocaleProvider } from "@vjdesigns/vjdesigns-ui";
346
+ import es from "@vjdesigns/vjdesigns-ui/locales/es";
347
+
348
+ <LocaleProvider locale="es" messages={es}>
349
+ <App />
350
+ </LocaleProvider>
351
+ ```
352
+
353
+ `LocaleProvider` sets:
354
+
355
+ - **Built-in text** for every component, from `messages`.
356
+ - **Dates and numbers** in the locale's format (`23/10/2026`, `1.234,5`), and plurals that follow the language's rules.
357
+ - **Writing direction**: Arabic, Hebrew, Persian, and Urdu get `dir="rtl"`, and layouts and arrow keys mirror.
358
+ - **`lang`** on its wrapper, so screen readers pronounce the text correctly. Popups (menus, dialogs, tooltips) keep the language of the area that opened them.
359
+
360
+ 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.
361
+
362
+ 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).
363
+
364
+ ### What gets translated
365
+
366
+ 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}>`:
367
+
368
+ | | Translated? |
369
+ | --- | --- |
370
+ | Text your app passes in, like `<Button>Save</Button>` | No: your app translates it |
371
+ | Built-in names, like "Close", "Next page", "Clear search" | Yes: "Schließen", "Nächste Seite", "Suche löschen" |
372
+ | Screen-reader announcements, like "3 results available" | Yes: "3 Ergebnisse verfügbar" |
373
+ | Validation messages, empty states, file-upload prompts | Yes |
374
+ | Dates and numbers | Yes: "23.10.2026", "4,5" |
375
+
376
+ 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)).
377
+
378
+ ### Bundled languages
379
+
380
+ `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).
381
+
382
+ 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).)
383
+
384
+ ### Changing a few strings
385
+
386
+ Pass only the strings you want to change. Everything else falls back to the outer provider's language (English at the top):
387
+
388
+ ```tsx
389
+ <LocaleProvider locale="en-GB" messages={{ pagination: { next: "Onwards" } }}>
390
+ ```
391
+
392
+ 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.
393
+
394
+ ### Other languages
395
+
396
+ For a language that isn't bundled, write a complete messages object. TypeScript flags any missing string. Use English as the template:
397
+
398
+ ```ts
399
+ import { en, plural, type Messages } from "@vjdesigns/vjdesigns-ui";
400
+
401
+ const p = plural("sw");
402
+ export const sw: Messages = {
403
+ ...en,
404
+ pagination: { ...en.pagination, next: "Ukurasa unaofuata", previous: "Ukurasa uliopita" },
405
+ searchInput: {
406
+ ...en.searchInput,
407
+ results: (count) => p(count, { "=0": "Hakuna matokeo", one: "Tokeo #", other: "Matokeo #" }),
408
+ },
409
+ // ...
410
+ };
411
+ ```
412
+
413
+ `plural(locale)` picks the right plural form for any language (`#` is the formatted number).
414
+
415
+ ### Switching language at runtime
416
+
417
+ List the languages your app offers as loaders, so each one is downloaded only when picked:
418
+
419
+ ```tsx
420
+ import { LocaleProvider, type Messages } from "@vjdesigns/vjdesigns-ui";
421
+
422
+ const loaders: Record<string, () => Promise<{ default: Messages }>> = {
423
+ es: () => import("@vjdesigns/vjdesigns-ui/locales/es"),
424
+ fr: () => import("@vjdesigns/vjdesigns-ui/locales/fr"),
425
+ ar: () => import("@vjdesigns/vjdesigns-ui/locales/ar"),
426
+ };
427
+
428
+ function App({ lang }: { lang: string }) {
429
+ const [messages, setMessages] = useState<Messages>();
430
+ useEffect(() => {
431
+ const load = loaders[lang];
432
+ if (load) load().then((mod) => setMessages(mod.default));
433
+ else setMessages(undefined); // English
434
+ }, [lang]);
435
+ // Until a language loads, the previous one (or English) shows.
436
+ return <LocaleProvider locale={lang} messages={messages}>…</LocaleProvider>;
437
+ }
438
+ ```
439
+
440
+ For server rendering, also set `<html lang="…" dir="…">` on the page. `getLocaleDirection(locale)` returns `"ltr"` or `"rtl"`.
441
+
442
+ ---
443
+
444
+ ## Form validation
445
+
446
+ > **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.
447
+
448
+ `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.
449
+
450
+ ```tsx
451
+ import {
452
+ useFormValidation, rules, ErrorSummary, Field, Input, PasswordInput, Button,
453
+ } from "@vjdesigns/vjdesigns-ui";
454
+
455
+ function SignUp() {
456
+ const form = useFormValidation({
457
+ rules: {
458
+ confirm: rules.matches("password", "The passwords don't match."),
459
+ serverIp: rules.ip(), // IPv4 or IPv6
460
+ },
461
+ // Runs only when everything is valid. Return the server's errors to show them on their fields.
462
+ onValid: async (values) => {
463
+ const response = await api.signUp(values);
464
+ return response.errors; // e.g. { username: "That username is taken." }
465
+ },
466
+ });
467
+
468
+ return (
469
+ <form {...form.formProps}>
470
+ <ErrorSummary ref={form.summaryRef} errors={form.errorList} />
471
+ <Field label="Email" required error={form.errors.email}>
472
+ <Input name="email" type="email" />
473
+ </Field>
474
+ <Field label="Password" required error={form.errors.password}>
475
+ <PasswordInput name="password" minLength={12} />
476
+ </Field>
477
+ <Field label="Confirm password" required error={form.errors.confirm}>
478
+ <PasswordInput name="confirm" />
479
+ </Field>
480
+ <Field label="Server IP" error={form.errors.serverIp}>
481
+ <Input name="serverIp" />
482
+ </Field>
483
+ <Button type="submit" loading={form.isSubmitting}>Create account</Button>
484
+ </form>
485
+ );
486
+ }
487
+ ```
488
+
489
+ How it behaves:
490
+
491
+ - **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.
492
+ - **Errors are keyed by the field's `name`.** Pass `form.errors.<name>` to each `Field`'s `error`.
493
+ - **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.
494
+ - **Server errors** (returned from `onValid`, or set with `form.setErrors({...})`) show on their fields and stay until that field is edited.
495
+ - **Emails** must have a real domain (`name@example.com`). The browser's own check also accepts `name@localhost`.
496
+
497
+ ### Rules and format checks
498
+
499
+ | Rule | Checks |
500
+ | --- | --- |
501
+ | `rules.required()` | Not empty (for controls without a native `required`) |
502
+ | `rules.email()` | An email address in the usual format, with a top-level domain |
503
+ | `rules.ip()` / `rules.ip({ version: 4 })` / `rules.ip({ version: 6 })` | An IPv4 or IPv6 address |
504
+ | `rules.minLength(n)` / `rules.maxLength(n)` | Length in characters |
505
+ | `rules.pattern(/regex/, "message")` | The whole value matches |
506
+ | `rules.matches("otherField")` | Equals another field (confirm password) |
507
+ | your own `(value, { values }) => message` | Anything else |
508
+
509
+ Each rule takes an optional custom message. The checks behind them are exported for use anywhere, including your server:
510
+
511
+ - `isEmail(value)`
512
+ - `isIPv4(value)`: four numbers 0–255, no leading zeros
513
+ - `isIPv6(value)`: including `::` shorthand and IPv4 tails like `::ffff:192.168.0.1`
514
+ - `isIP(value, version?)`
515
+
516
+ ---
517
+
518
+ ## Hooks
519
+
520
+ Tested hooks for everyday UI work, exported from the package root (`import { useDisclosure } from "@vjdesigns/vjdesigns-ui"`). All are safe in server rendering and tree-shakable: each costs only itself, mostly well under 1 KB. Live demos are in Storybook under Hooks.
521
+
522
+ | Hook | What it does |
523
+ | --- | --- |
524
+ | `useDisclosure(initial?)` | Open/closed state with stable `open`, `close`, `toggle` handlers (for Modal, Drawer, Popover) |
525
+ | `useToggle(values?)` | Cycles through values: a boolean, or a list like `["list", "grid"]` |
526
+ | `usePrevious(value)` | The value from before its last change |
527
+ | `useDebouncedValue(value, ms)` | The value, updated after it stops changing (search as you type) |
528
+ | `useDebouncedCallback(fn, ms)` | A function that runs once after calls stop, with `cancel` and `flush` |
529
+ | `useThrottledCallback(fn, ms)` | A function that runs at most once per interval (scroll, resize) |
530
+ | `useInterval(fn, ms \| null)` / `useTimeout(fn, ms \| null)` | Timers that always call the latest function; `null` pauses |
531
+ | `useLocalStorage(key, default)` / `useSessionStorage` | State saved in storage, synced across components and tabs, and never throws when storage is blocked |
532
+ | `useClipboard()` | `copy(text)` with a `copied` flag, and a fallback when the Clipboard API is blocked |
533
+ | `useHotkeys([[keys, fn]])` | Page shortcuts. `mod` is ⌘ or Ctrl. Plain keys don't fire while typing. `getHotkeyHandler` does the same for one element |
534
+ | `useEventListener(type, fn, target?)` | An event listener that always calls the latest handler |
535
+ | `useClickOutside(fn)` | Calls `fn` on a press or focus move outside the element |
536
+ | `useIntersection(options)` | Whether an element is in view (lazy loading, entrance effects) |
537
+ | `useElementSize()` | An element's size as it resizes |
538
+ | `useVirtualizer(options)` | Renders only the visible items of a long list (what `VirtualList` uses), with variable heights and `scrollToIndex` |
539
+ | `useMediaQuery(query)` / `useResponsiveValue(value)` | A synced media query, or a responsive value resolved for the viewport |
540
+ | `useHydrated()` | False during server rendering, true after (avoids hydration mismatches) |
541
+ | `useReducedMotion()` / `useColorScheme()` | The user's motion and color-scheme preferences |
542
+ | `useOnline()` / `useDocumentVisibility()` / `useViewportSize()` | Connection, tab visibility, and viewport size |
543
+ | `useControllableState`, `useFormValidation`, `useFocusTrap`, `useScrollLock`, … | The building blocks the components use, for your own components |
544
+
545
+ ---
546
+
547
+ ## AI assistants
548
+
549
+ The package ships documentation written for AI coding assistants, generated from the source on every build, so it always matches the version you have installed:
550
+
551
+ | File | What it is |
552
+ | --- | --- |
553
+ | `@vjdesigns/vjdesigns-ui/llms.txt` | A short index in the [llms.txt](https://llmstxt.org) format: setup essentials and one line per component |
554
+ | `@vjdesigns/vjdesigns-ui/llms-full.txt` | The full reference in one file: guides, and every component's props (type, default, description), subcomponents, and examples |
555
+ | `@vjdesigns/vjdesigns-ui/registry.json` | The same, as structured JSON |
556
+
557
+ The files are in `node_modules/@vjdesigns/vjdesigns-ui/dist/ai/`. Point your assistant's rules or context at `llms-full.txt`.
558
+
559
+ ### MCP server
560
+
561
+ For assistants that support the [Model Context Protocol](https://modelcontextprotocol.io), the package includes a small MCP server with four tools:
562
+
563
+ - `list_components`
564
+ - `get_component`, for exact props
565
+ - `search_docs`
566
+ - `get_guide`
567
+
568
+ It reads the docs of your installed version and needs no network access.
569
+
570
+ Claude Code:
571
+
572
+ ```bash
573
+ claude mcp add vjdesigns-ui -- npx -p @vjdesigns/vjdesigns-ui vjdesigns-ui-mcp
574
+ ```
575
+
576
+ Cursor (`.cursor/mcp.json`), Claude Desktop, and other clients (VS Code's `.vscode/mcp.json` uses `"servers"` instead of `"mcpServers"`):
577
+
578
+ ```json
579
+ {
580
+ "mcpServers": {
581
+ "vjdesigns-ui": {
582
+ "command": "npx",
583
+ "args": ["-p", "@vjdesigns/vjdesigns-ui", "vjdesigns-ui-mcp"]
584
+ }
585
+ }
586
+ }
587
+ ```
588
+
589
+ ---
590
+
591
+ ## Components
592
+
593
+ Every component below is exported from the package root. Full prop docs and live examples are in [Storybook](https://component.vjdesigns.org).
594
+
595
+ | Category | Components |
596
+ | --- | --- |
597
+ | **Primitives** | Button, IconButton, ButtonGroup, Icon, Kbd, Typography |
598
+ | **Layout** | Box, Container, Divider, Flex, Grid, ResizablePanels, ScrollArea, Stack |
599
+ | **Forms** | CheckBox, CheckBoxGroup, ColorPicker, Combobox, DatePicker, DateRangePicker, DateTimePicker, Field, FileUpload, Input, NumberInput, PasswordInput, PinInput, Radio, Rating, SearchInput, SegmentedControl, Select, Slider, Switch, TagInput, Textarea, TimePicker, ToggleGroup |
600
+ | **Navigation** | Breadcrumbs, ContextMenu, Link, Menu, Navbar, Pagination, Sidebar, SkipLink, Stepper, Tabs |
601
+ | **Overlays** | CommandPalette, Drawer, HoverCard, Modal, Popover, Tooltip |
602
+ | **Feedback** | Alert, Banner, ErrorSummary, Progress, Skeleton, Spinner, Toast |
603
+ | **Data display** | Accordion, Avatar, Badge, Card, Carousel, Chat, Chip, CodeBlock, DataGrid, EmptyState, Image, List, Stat, Table, Timeline, TreeView, VirtualList |
604
+ | **Utilities** | Portal, Theme, VisuallyHidden |
605
+
606
+ ### Button
607
+
608
+ ```tsx
609
+ <Button variant="solid" tone="primary">Save</Button>
610
+ <Button variant="outline" tone="danger" startIcon={<TrashIcon />}>Delete</Button>
611
+ <Button href="/pricing" variant="link">See pricing</Button>
612
+ <Button loading loadingText="Saving…">Save</Button>
613
+ ```
614
+
615
+ | Prop | Type | Default | Description |
616
+ | --- | --- | --- | --- |
617
+ | `children` | `ReactNode` | — | The visible label. |
618
+ | `variant` | `"solid" \| "soft" \| "outline" \| "ghost" \| "link" \| "inverse"` | `"solid"` | Visual style. |
619
+ | `tone` | `"neutral" \| "primary" \| "accent" \| "success" \| "warning" \| "danger" \| "info"` | `"primary"` | Semantic color. |
620
+ | `size` | `"sm" \| "md" \| "lg"` | `"md"` | Height, padding and type size. |
621
+ | `radius` | `"none" \| "xs" \| "sm" \| "md" \| "lg" \| "xl" \| "2xl" \| "pill"` | `"pill"` | Corner rounding. |
622
+ | `startIcon` / `endIcon` | `ReactNode` | — | Decorative icon before or after the label. Hidden from screen readers. |
623
+ | `loading` | `boolean` | `false` | Shows a spinner, sets `aria-busy` and ignores clicks. |
624
+ | `loadingText` | `ReactNode` | — | Label shown next to the spinner while loading. |
625
+ | `fullWidth` | `boolean` | `false` | Stretches the button to fill its container. |
626
+ | `compact` | `boolean` | `false` | Tighter side padding for short labels like "OK". |
627
+ | `disabled` | `boolean` | `false` | Disables the button. |
628
+ | `href` | `string` | — | Renders the button as an `<a>` link with the same styling. |
629
+ | `as` | `ElementType` | — | Renders as your own component, such as a React Router or Next.js `Link`. |
630
+
631
+ Also accepts all standard `<button>` attributes (or `<a>` attributes when `href` is set), such as `onClick` and `type`.
632
+
633
+ For icon-only buttons, use `IconButton`. `aria-label` is required:
634
+
635
+ ```tsx
636
+ <IconButton aria-label="Close" variant="ghost"><CloseIcon /></IconButton>
637
+ ```
638
+
639
+ To group related buttons, use `ButtonGroup`. It passes `variant`, `tone`, `size`, `radius` and `disabled` down to every button inside.
640
+
641
+ ### CheckBox
642
+
643
+ ```tsx
644
+ <CheckBox label="Email me updates" description="About once a month." />
645
+ <CheckBox label="Remember me" labelPosition="start" defaultChecked />
646
+ ```
647
+
648
+ | Prop | Type | Default | Description |
649
+ | --- | --- | --- | --- |
650
+ | `label` | `ReactNode` | — | Visible label. Clicking it toggles the box. Without it, pass `aria-label`. |
651
+ | `description` | `ReactNode` | — | Supporting text below the label, linked with `aria-describedby`. |
652
+ | `checked` | `boolean` | — | Checked state (controlled). |
653
+ | `defaultChecked` | `boolean` | — | Initial checked state (uncontrolled). |
654
+ | `onCheckedChange` | `(checked: boolean) => void` | — | Called with the new state when the user toggles the box. |
655
+ | `indeterminate` | `boolean` | `false` | Shows a "mixed" dash, for a "select all" box whose options are partly checked. |
656
+ | `labelPosition` | `"start" \| "end"` | `"end"` | Puts the label before (`start`) or after (`end`) the box. |
657
+ | `size` | `"sm" \| "md" \| "lg"` | `"md"` | Box and text size. |
658
+ | `tone` | `Tone` | `"primary"` | Fill color when checked. |
659
+ | `invalid` | `boolean` | `false` | Danger styling plus `aria-invalid`. |
660
+ | `readOnly` | `boolean` | `false` | Keeps the box focusable but prevents toggling. |
661
+
662
+ Also accepts native `<input type="checkbox">` attributes, such as `name`, `required` and `disabled`.
663
+
664
+ To manage several options as one `string[]` value, use `CheckBoxGroup`:
665
+
666
+ ```tsx
667
+ <CheckBoxGroup label="Toppings" defaultValue={["cheese"]} onValueChange={setToppings}>
668
+ <CheckBox value="cheese" label="Cheese" />
669
+ <CheckBox value="peppers" label="Peppers" />
670
+ </CheckBoxGroup>
671
+ ```
672
+
673
+ ---
674
+
675
+ ## Pro
676
+
677
+ Everything above is free. `@vjdesigns/vjdesigns-ui-pro` is a commercial add-on for teams that need more from their data grids:
678
+
679
+ - **DataGridPro:** everything `DataGrid` does, plus resizable columns (pointer and keyboard), saved column layouts, and columns pinned to either edge.
680
+ - **CSV export:** export exactly what a grid shows, safe to open in a spreadsheet.
681
+ - **Charts:** accessible `BarChart` and `LineChart`: keyboard-navigable points with spoken values, a data table view, color-blind-safe series colors, and High Contrast support.
682
+ - **Account forms:** ready-made sign-in, sign-up, forgot-password, reset-password, and verification-code forms that you connect to your own backend, with validation, autofill, passkey support, and safe error messages built in.
683
+ - **Figma kit:** a plugin that creates the library's variables (brands, light and dark themes, spacing, type) and shadow styles in Figma, generated from the same tokens as the code, plus W3C design token files.
684
+
685
+ It uses the same tokens, themes, languages, and accessibility standard as the free package.
686
+
687
+ ---
688
+
689
+ ## Development
690
+
691
+ ```bash
692
+ npm run storybook # run Storybook locally at http://localhost:6006
693
+ npm run build # type-check and build the library to dist/
694
+ npm run build-storybook # build a static Storybook site
695
+ ```
696
+
697
+ Components live under `src/components/<category>/<Name>/`, with each component's implementation, CSS module and Storybook stories kept together. Shared design tokens live in `src/styles/tokens.css`. The build bundles all styles into `dist/vjdesigns-ui.css`.
698
+
699
+ ### Translating built-in text
700
+
701
+ You write each built-in message once, in English, and a script translates it into every bundled language with the Claude API.
702
+
703
+ **1. Edit the English.** Add, reword, or remove the message in [`src/i18n/en.ts`](src/i18n/en.ts). For a new message, also add it to the `Messages` type in [`src/i18n/types.ts`](src/i18n/types.ts) with a short comment saying where it appears. The translator reads that comment, so it helps to name the screen or the control.
704
+
705
+ ```ts
706
+ // src/i18n/types.ts
707
+ chat: {
708
+ /** Button that stops a reply while it streams. */
709
+ stop: string;
710
+ };
711
+
712
+ // src/i18n/en.ts
713
+ chat: {
714
+ stop: "Stop response",
715
+ },
716
+ ```
717
+
718
+ **2. Translate.**
719
+
720
+ ```bash
721
+ npm run i18n:translate
722
+ ```
723
+
724
+ The script finds the English messages that are new or reworded since the last run, translates only those into all 31 locales, and removes keys English no longer has. Then it runs `check-locales.mjs` and the type-check. It never touches messages you didn't change, so hand-corrected translations are kept.
725
+
726
+ **3. Review and commit.** Look over `git diff src/i18n`, then commit the locale files together with `src/i18n/en.snapshot.json`. The snapshot records which English text has been translated. It's how the next run knows what changed, so commit it with the translations.
727
+
728
+ **Setup (once).** Run `npm install`. The script uses `@anthropic-ai/sdk`, a dev dependency that the published package doesn't include. You also need a Claude API key from [platform.claude.com](https://platform.claude.com), set in your shell:
729
+
730
+ ```bash
731
+ export ANTHROPIC_API_KEY=sk-ant-... # macOS, Linux, Git Bash
732
+ $env:ANTHROPIC_API_KEY = "sk-ant-..." # PowerShell
733
+ ```
734
+
735
+ Don't commit the key or put it in a `.env` file that ships with the library. The script only runs on your machine, and the translations ship as plain files, so the library never calls the API at runtime. Each run sends only the changed messages, so a typical run costs a few cents.
736
+
737
+ **Options**
738
+
739
+ | Command | What it does |
740
+ | --- | --- |
741
+ | `npm run i18n:translate -- --dry-run` | Lists what would be translated in each locale, without calling the API |
742
+ | `npm run i18n:translate -- --locales de,fr` | Translates only these locales (the snapshot isn't updated, so a later full run still covers the rest) |
743
+ | `npm run i18n:translate -- --accept` | Records the current English as translated without translating, for edits that don't change the meaning (a typo or punctuation fix) |
744
+ | `npm run i18n:translate -- --concurrency 1` | Translates one locale at a time, if you hit rate limits (default: 4) |
745
+ | `npm run i18n:translate -- --effort medium` | Uses less model effort, which is cheaper and faster (default: `high`) |
746
+ | `npm run i18n:translate -- --skip-checks` | Skips the locale check and type-check afterwards |
747
+
748
+ **If English changes without a translation run**, `node scripts/check-locales.mjs` fails and names the messages, so other languages can't silently keep the old meaning. Run `npm run i18n:translate`, or use `--accept` if the change doesn't affect translations.
749
+
750
+ **Adding a language.** Copy an existing locale file to `src/i18n/locales/<code>.ts`, keep its header (the imports and the `p` and `n` helpers, with the new code), and empty the messages object: `export const messages: Messages = {};`. Then run `npm run i18n:translate -- --locales <code>`. It translates every message the new file is missing. The build picks up the new file automatically (`@vjdesigns/vjdesigns-ui/locales/<code>`). Add the language to [Bundled languages](#bundled-languages) too.
751
+
752
+ **How it works.** The script ([`scripts/translate-locales.mjs`](scripts/translate-locales.mjs)) reads the catalogs as TypeScript source and sends Claude each message's English source, its type and comment from `types.ts`, the locale's current translation if there is one, and the locale's existing translations in the same section, so terminology stays consistent. Claude returns TypeScript for each message. Plurals go through the locale's `p()` helper with that language's own plural forms (Russian has four, Arabic six). The script checks that every answer is valid TypeScript of the right kind (text, a function with the same parameters, or an array of the same length), asks again once for any that aren't, then writes the answers into the locale files in English order. Messages that still fail are listed and left unchanged, and the snapshot isn't updated, so re-running retries them.
753
+
754
+ The translations are machine-made. Corrections from native speakers are welcome: edit the locale file directly. The script won't overwrite your fix unless the English for that message changes.
755
+
756
+ ### Releasing
757
+
758
+ ```bash
759
+ npm whoami # check you're logged in (if not: npm login)
760
+ npm run build
761
+ npm version patch # or minor / major
762
+ npm publish
763
+ git push --follow-tags
764
+ ```
765
+
766
+ See [RELEASING.md](RELEASING.md) for the full process, including how to update the projects that use the library and fixes for common errors.
767
+
768
+ ---
769
+
770
+ ## Access to the production Storybook
771
+
772
+ 1. Go to [component.vjdesigns.org](https://component.vjdesigns.org/).
773
+ 2. Enter your email in the Cloudflare prompt to receive a 6-digit access code.
774
+
775
+ <img width="1917" height="927" alt="Cloudflare access prompt for component.vjdesigns.org" src="https://github.com/user-attachments/assets/bfe58737-52d2-4bf3-b7f7-72ba5ee54969" />
776
+
777
+ ---
778
+
779
+ ## License
780
+
781
+ ISC