@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/dist/ai/llms.txt CHANGED
@@ -25,6 +25,7 @@
25
25
  - [BadgeAnchor](llms-full.txt#badgeanchor): Pins a count, dot, or short label to a corner of its child.
26
26
  - [Card](llms-full.txt#card): A soft, rounded surface that groups related content.
27
27
  - [Carousel](llms-full.txt#carousel): A row of slides that scroll horizontally with CSS scroll snap (so touch, trackpad, and swipe work natively), plus Previous / Next buttons, optional slide picker indicators, and optional autoplay with a Pause / Play button.
28
+ - [Chat](llms-full.txt#chat): A chat interface for an AI assistant (or a person), with no backend of its own: `onSend` connects it to any API and can stream the reply.
28
29
  - [Chip](llms-full.txt#chip): A compact pill for tags, filters, and selections.
29
30
  - [ChipGroup](llms-full.txt#chipgroup): Lays out chips with a gap and wraps them.
30
31
  - [Code](llms-full.txt#code): Inline code inside running text, e.g. a prop name or a command.
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vjdesigns/vjdesigns-ui",
3
- "version": "1.5.0",
3
+ "version": "2.0.0",
4
4
  "summary": "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.",
5
5
  "install": "npm install @vjdesigns/vjdesigns-ui",
6
6
  "stylesheet": "@vjdesigns/vjdesigns-ui/dist/vjdesigns-ui.css",
@@ -17,27 +17,27 @@
17
17
  {
18
18
  "id": "setup",
19
19
  "title": "Installation",
20
- "markdown": "Install the package along with React and React DOM (version 18 or 19), which the components need in order to render:\r\n\r\n```bash\r\nnpm install @vjdesigns/vjdesigns-ui react react-dom\r\n```\r\n\r\nIf your project already uses React, you only need the first package:\r\n\r\n```bash\r\nnpm install @vjdesigns/vjdesigns-ui\r\n```\r\n\r\n### Import the styles\r\n\r\nThe 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.\r\n\r\n**React:** in `main.tsx` (or `App.tsx`)\r\n\r\n```ts\r\nimport \"@vjdesigns/vjdesigns-ui/dist/vjdesigns-ui.css\";\r\n```\r\n\r\n**Angular:** add it to the `styles` array in `angular.json`\r\n\r\n```json\r\n\"styles\": [\r\n \"node_modules/@vjdesigns/vjdesigns-ui/dist/vjdesigns-ui.css\",\r\n \"src/styles.css\"\r\n]\r\n```\r\n\r\n**Vue:** in `main.ts`\r\n\r\n```ts\r\nimport \"@vjdesigns/vjdesigns-ui/dist/vjdesigns-ui.css\";\r\n```"
20
+ "markdown": "Install the package along with React and React DOM (version 18 or 19), which the components need in order to render:\n\n```bash\nnpm install @vjdesigns/vjdesigns-ui react react-dom\n```\n\nIf your project already uses React, you only need the first package:\n\n```bash\nnpm install @vjdesigns/vjdesigns-ui\n```\n\n### Import the styles\n\nThe 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.\n\n**React:** in `main.tsx` (or `App.tsx`)\n\n```ts\nimport \"@vjdesigns/vjdesigns-ui/dist/vjdesigns-ui.css\";\n```\n\n**Angular:** add it to the `styles` array in `angular.json`\n\n```json\n\"styles\": [\n \"node_modules/@vjdesigns/vjdesigns-ui/dist/vjdesigns-ui.css\",\n \"src/styles.css\"\n]\n```\n\n**Vue:** in `main.ts`\n\n```ts\nimport \"@vjdesigns/vjdesigns-ui/dist/vjdesigns-ui.css\";\n```"
21
21
  },
22
22
  {
23
23
  "id": "usage",
24
24
  "title": "Usage",
25
- "markdown": "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.\r\n\r\n### React\r\n\r\n```tsx\r\nimport { useState } from \"react\";\r\nimport { Button, CheckBox } from \"@vjdesigns/vjdesigns-ui\";\r\n\r\nexport function SignupForm() {\r\n const [accepted, setAccepted] = useState(false);\r\n\r\n return (\r\n <form>\r\n <CheckBox\r\n label=\"Accept terms\"\r\n checked={accepted}\r\n onCheckedChange={setAccepted}\r\n />\r\n <Button\r\n variant=\"solid\"\r\n tone=\"primary\"\r\n size=\"md\"\r\n disabled={!accepted}\r\n onClick={() => console.log(\"Submitted\")}\r\n >\r\n Submit\r\n </Button>\r\n </form>\r\n );\r\n}\r\n```\r\n\r\n### Angular\r\n\r\nMake 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.\r\n\r\n```ts\r\n// vj-button.component.ts\r\nimport {\r\n AfterViewInit,\r\n Component,\r\n ElementRef,\r\n EventEmitter,\r\n Input,\r\n OnChanges,\r\n OnDestroy,\r\n Output,\r\n ViewChild,\r\n} from \"@angular/core\";\r\nimport { createElement, type MouseEvent as ReactMouseEvent, type ReactElement } from \"react\";\r\nimport { createRoot, Root } from \"react-dom/client\";\r\nimport {\r\n Button,\r\n type ButtonProps,\r\n type ButtonVariant,\r\n type Size,\r\n type Tone,\r\n} from \"@vjdesigns/vjdesigns-ui\";\r\n\r\n// Button has overloaded types (for `href` and `as`). Narrowing it here lets createElement type-check.\r\nconst ReactButton = Button as (props: ButtonProps) => ReactElement;\r\n\r\n@Component({\r\n selector: \"vj-button\",\r\n standalone: true,\r\n template: `<span #host></span>`,\r\n})\r\nexport class VjButtonComponent implements AfterViewInit, OnChanges, OnDestroy {\r\n @ViewChild(\"host\", { static: true }) host!: ElementRef<HTMLElement>;\r\n\r\n @Input() label = \"\";\r\n @Input() variant: ButtonVariant = \"solid\";\r\n @Input() tone: Tone = \"primary\";\r\n @Input() size: Size = \"md\";\r\n @Input() disabled = false;\r\n @Output() clicked = new EventEmitter<MouseEvent>();\r\n\r\n private root?: Root;\r\n\r\n ngAfterViewInit() {\r\n this.root = createRoot(this.host.nativeElement);\r\n this.render();\r\n }\r\n\r\n ngOnChanges() {\r\n this.render();\r\n }\r\n\r\n ngOnDestroy() {\r\n this.root?.unmount();\r\n }\r\n\r\n private render() {\r\n this.root?.render(\r\n createElement(\r\n ReactButton,\r\n {\r\n variant: this.variant,\r\n tone: this.tone,\r\n size: this.size,\r\n disabled: this.disabled,\r\n onClick: (event: ReactMouseEvent) => this.clicked.emit(event.nativeEvent),\r\n },\r\n this.label // the button's text is passed as children\r\n )\r\n );\r\n }\r\n}\r\n```\r\n\r\nImport the wrapper wherever you need it:\r\n\r\n```ts\r\n// app.component.ts\r\nimport { Component } from \"@angular/core\";\r\nimport { VjButtonComponent } from \"./vj-button.component\";\r\n\r\n@Component({\r\n selector: \"app-root\",\r\n standalone: true,\r\n imports: [VjButtonComponent],\r\n template: `<vj-button label=\"Submit\" tone=\"primary\" (clicked)=\"onSubmit()\" />`,\r\n})\r\nexport class AppComponent {\r\n onSubmit() {\r\n console.log(\"Submitted\");\r\n }\r\n}\r\n```\r\n\r\n### Vue 3\r\n\r\nMake a wrapper component that mounts the React component when it loads and re-renders it whenever its props change.\r\n\r\n```vue\r\n<!-- VjButton.vue -->\r\n<script setup lang=\"ts\">\r\nimport { onBeforeUnmount, onMounted, ref, watch } from \"vue\";\r\nimport { createElement, type MouseEvent as ReactMouseEvent, type ReactElement } from \"react\";\r\nimport { createRoot, type Root } from \"react-dom/client\";\r\nimport {\r\n Button,\r\n type ButtonProps,\r\n type ButtonVariant,\r\n type Size,\r\n type Tone,\r\n} from \"@vjdesigns/vjdesigns-ui\";\r\n\r\n// Button has overloaded types (for `href` and `as`). Narrowing it here lets createElement type-check.\r\nconst ReactButton = Button as (props: ButtonProps) => ReactElement;\r\n\r\nconst props = withDefaults(\r\n defineProps<{\r\n label: string;\r\n variant?: ButtonVariant;\r\n tone?: Tone;\r\n size?: Size;\r\n disabled?: boolean;\r\n }>(),\r\n { variant: \"solid\", tone: \"primary\", size: \"md\", disabled: false }\r\n);\r\n\r\nconst emit = defineEmits<{ click: [event: MouseEvent] }>();\r\n\r\nconst host = ref<HTMLElement | null>(null);\r\nlet root: Root | undefined;\r\n\r\nfunction render() {\r\n root?.render(\r\n createElement(\r\n ReactButton,\r\n {\r\n variant: props.variant,\r\n tone: props.tone,\r\n size: props.size,\r\n disabled: props.disabled,\r\n onClick: (event: ReactMouseEvent) => emit(\"click\", event.nativeEvent),\r\n },\r\n props.label // the button's text is passed as children\r\n )\r\n );\r\n}\r\n\r\nonMounted(() => {\r\n root = createRoot(host.value!);\r\n render();\r\n});\r\n\r\nwatch(() => ({ ...props }), render);\r\n\r\nonBeforeUnmount(() => root?.unmount());\r\n</script>\r\n\r\n<template>\r\n <span ref=\"host\" />\r\n</template>\r\n```\r\n\r\nUse it like any other Vue component:\r\n\r\n```vue\r\n<script setup lang=\"ts\">\r\nimport VjButton from \"./VjButton.vue\";\r\n\r\nconst onSubmit = () => console.log(\"Submitted\");\r\n</script>\r\n\r\n<template>\r\n <VjButton label=\"Submit\" tone=\"primary\" @click=\"onSubmit\" />\r\n</template>\r\n```\r\n\r\n> **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."
25
+ "markdown": "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.\n\n### React\n\n```tsx\nimport { useState } from \"react\";\nimport { Button, CheckBox } from \"@vjdesigns/vjdesigns-ui\";\n\nexport function SignupForm() {\n const [accepted, setAccepted] = useState(false);\n\n return (\n <form>\n <CheckBox\n label=\"Accept terms\"\n checked={accepted}\n onCheckedChange={setAccepted}\n />\n <Button\n variant=\"solid\"\n tone=\"primary\"\n size=\"md\"\n disabled={!accepted}\n onClick={() => console.log(\"Submitted\")}\n >\n Submit\n </Button>\n </form>\n );\n}\n```\n\n### Angular\n\nMake 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.\n\n```ts\n// vj-button.component.ts\nimport {\n AfterViewInit,\n Component,\n ElementRef,\n EventEmitter,\n Input,\n OnChanges,\n OnDestroy,\n Output,\n ViewChild,\n} from \"@angular/core\";\nimport { createElement, type MouseEvent as ReactMouseEvent, type ReactElement } from \"react\";\nimport { createRoot, Root } from \"react-dom/client\";\nimport {\n Button,\n type ButtonProps,\n type ButtonVariant,\n type Size,\n type Tone,\n} from \"@vjdesigns/vjdesigns-ui\";\n\n// Button has overloaded types (for `href` and `as`). Narrowing it here lets createElement type-check.\nconst ReactButton = Button as (props: ButtonProps) => ReactElement;\n\n@Component({\n selector: \"vj-button\",\n standalone: true,\n template: `<span #host></span>`,\n})\nexport class VjButtonComponent implements AfterViewInit, OnChanges, OnDestroy {\n @ViewChild(\"host\", { static: true }) host!: ElementRef<HTMLElement>;\n\n @Input() label = \"\";\n @Input() variant: ButtonVariant = \"solid\";\n @Input() tone: Tone = \"primary\";\n @Input() size: Size = \"md\";\n @Input() disabled = false;\n @Output() clicked = new EventEmitter<MouseEvent>();\n\n private root?: Root;\n\n ngAfterViewInit() {\n this.root = createRoot(this.host.nativeElement);\n this.render();\n }\n\n ngOnChanges() {\n this.render();\n }\n\n ngOnDestroy() {\n this.root?.unmount();\n }\n\n private render() {\n this.root?.render(\n createElement(\n ReactButton,\n {\n variant: this.variant,\n tone: this.tone,\n size: this.size,\n disabled: this.disabled,\n onClick: (event: ReactMouseEvent) => this.clicked.emit(event.nativeEvent),\n },\n this.label // the button's text is passed as children\n )\n );\n }\n}\n```\n\nImport the wrapper wherever you need it:\n\n```ts\n// app.component.ts\nimport { Component } from \"@angular/core\";\nimport { VjButtonComponent } from \"./vj-button.component\";\n\n@Component({\n selector: \"app-root\",\n standalone: true,\n imports: [VjButtonComponent],\n template: `<vj-button label=\"Submit\" tone=\"primary\" (clicked)=\"onSubmit()\" />`,\n})\nexport class AppComponent {\n onSubmit() {\n console.log(\"Submitted\");\n }\n}\n```\n\n### Vue 3\n\nMake a wrapper component that mounts the React component when it loads and re-renders it whenever its props change.\n\n```vue\n<!-- VjButton.vue -->\n<script setup lang=\"ts\">\nimport { onBeforeUnmount, onMounted, ref, watch } from \"vue\";\nimport { createElement, type MouseEvent as ReactMouseEvent, type ReactElement } from \"react\";\nimport { createRoot, type Root } from \"react-dom/client\";\nimport {\n Button,\n type ButtonProps,\n type ButtonVariant,\n type Size,\n type Tone,\n} from \"@vjdesigns/vjdesigns-ui\";\n\n// Button has overloaded types (for `href` and `as`). Narrowing it here lets createElement type-check.\nconst ReactButton = Button as (props: ButtonProps) => ReactElement;\n\nconst props = withDefaults(\n defineProps<{\n label: string;\n variant?: ButtonVariant;\n tone?: Tone;\n size?: Size;\n disabled?: boolean;\n }>(),\n { variant: \"solid\", tone: \"primary\", size: \"md\", disabled: false }\n);\n\nconst emit = defineEmits<{ click: [event: MouseEvent] }>();\n\nconst host = ref<HTMLElement | null>(null);\nlet root: Root | undefined;\n\nfunction render() {\n root?.render(\n createElement(\n ReactButton,\n {\n variant: props.variant,\n tone: props.tone,\n size: props.size,\n disabled: props.disabled,\n onClick: (event: ReactMouseEvent) => emit(\"click\", event.nativeEvent),\n },\n props.label // the button's text is passed as children\n )\n );\n}\n\nonMounted(() => {\n root = createRoot(host.value!);\n render();\n});\n\nwatch(() => ({ ...props }), render);\n\nonBeforeUnmount(() => root?.unmount());\n</script>\n\n<template>\n <span ref=\"host\" />\n</template>\n```\n\nUse it like any other Vue component:\n\n```vue\n<script setup lang=\"ts\">\nimport VjButton from \"./VjButton.vue\";\n\nconst onSubmit = () => console.log(\"Submitted\");\n</script>\n\n<template>\n <VjButton label=\"Submit\" tone=\"primary\" @click=\"onSubmit\" />\n</template>\n```\n\n> **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."
26
26
  },
27
27
  {
28
28
  "id": "theming",
29
29
  "title": "Theming",
30
- "markdown": "Wrap any part of your app in `Theme` to set the color mode, brand palette, and accent for everything inside it:\r\n\r\n```tsx\r\nimport { Theme, Button } from \"@vjdesigns/vjdesigns-ui\";\r\n\r\n<Theme mode=\"dark\" brand=\"purple\" fill>\r\n <Button>Dark, purple button</Button>\r\n</Theme>\r\n```\r\n\r\n| Prop | Type | Default | Description |\r\n| --- | --- | --- | --- |\r\n| `mode` | `\"light\" \\| \"dark\"` | inherited | Color mode for everything inside. |\r\n| `brand` | `\"black\" \\| \"blue\" \\| \"gold\" \\| \"green\" \\| \"purple\"`, a hex color, or `{ light, dark }` | inherited | Brand palette for buttons, links, focus rings and indicators. |\r\n| `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. |\r\n| `fill` | `boolean` | `false` | Paints the theme's background and text color on the wrapper. |\r\n\r\nOutside React (or to theme the whole page), set the attributes on `<html>` instead:\r\n\r\n```html\r\n<html data-theme=\"dark\" data-brand=\"purple\" data-accent=\"lime\">\r\n```\r\n\r\n### Accent color\r\n\r\nThe 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:\r\n\r\n| Brand | Default accent |\r\n| --- | --- |\r\n| `blue` (default) | sky blue `#76c7ff` |\r\n| `purple` | lavender `#c0b0ff` |\r\n| `gold` | amber `#f5ad5f` |\r\n| `green` | mint `#72d4ad` |\r\n| `black` | lime `#c8f031` (black has no hue to build on) |\r\n| a custom hex | generated from that color |\r\n\r\nTo choose a different one:\r\n\r\n```tsx\r\n<Theme accent=\"lime\">…</Theme> // the original bright yellow-green (\"volt\", its former name, still works)\r\n<Theme accent=\"#ff6b00\">…</Theme> // any color\r\n<Theme accent=\"brand\">…</Theme> // back to the brand's own, inside an area with another accent\r\n```\r\n\r\nAccents 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.\r\n\r\n### Custom brand colors\r\n\r\nPass any hex color as `brand` and a full palette is generated for both modes:\r\n\r\n```tsx\r\n<Theme brand=\"#e11d48\">…</Theme>\r\n<Theme brand={{ light: \"#4338ca\", dark: \"#f472b6\" }}>…</Theme>\r\n```\r\n\r\nEvery 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.\r\n\r\nTo define a brand in CSS instead (for the whole page, applied before any JavaScript runs), generate a stylesheet rule with `brandCss`:\r\n\r\n```ts\r\nimport { brandCss } from \"@vjdesigns/vjdesigns-ui\";\r\n\r\nbrandCss(\"acme\", \"#e11d48\"); // '[data-brand=\"acme\"] { --brand-light-primary: … }'\r\n```\r\n\r\nAdd 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."
30
+ "markdown": "Wrap any part of your app in `Theme` to set the color mode, brand palette, and accent for everything inside it:\n\n```tsx\nimport { Theme, Button } from \"@vjdesigns/vjdesigns-ui\";\n\n<Theme mode=\"dark\" brand=\"purple\" fill>\n <Button>Dark, purple button</Button>\n</Theme>\n```\n\n| Prop | Type | Default | Description |\n| --- | --- | --- | --- |\n| `mode` | `\"light\" \\| \"dark\"` | inherited | Color mode for everything inside. |\n| `brand` | `\"black\" \\| \"blue\" \\| \"gold\" \\| \"green\" \\| \"purple\"`, a hex color, or `{ light, dark }` | inherited | Brand palette for buttons, links, focus rings and indicators. |\n| `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. |\n| `fill` | `boolean` | `false` | Paints the theme's background and text color on the wrapper. |\n\nOutside React (or to theme the whole page), set the attributes on `<html>` instead:\n\n```html\n<html data-theme=\"dark\" data-brand=\"purple\" data-accent=\"lime\">\n```\n\n### Accent color\n\nThe 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:\n\n| Brand | Default accent |\n| --- | --- |\n| `blue` (default) | sky blue `#76c7ff` |\n| `purple` | lavender `#c0b0ff` |\n| `gold` | amber `#f5ad5f` |\n| `green` | mint `#72d4ad` |\n| `black` | lime `#c8f031` (black has no hue to build on) |\n| a custom hex | generated from that color |\n\nTo choose a different one:\n\n```tsx\n<Theme accent=\"lime\">…</Theme> // the original bright yellow-green (\"volt\", its former name, still works)\n<Theme accent=\"#ff6b00\">…</Theme> // any color\n<Theme accent=\"brand\">…</Theme> // back to the brand's own, inside an area with another accent\n```\n\nAccents 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.\n\n### Custom brand colors\n\nPass any hex color as `brand` and a full palette is generated for both modes:\n\n```tsx\n<Theme brand=\"#e11d48\">…</Theme>\n<Theme brand={{ light: \"#4338ca\", dark: \"#f472b6\" }}>…</Theme>\n```\n\nEvery 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.\n\nTo define a brand in CSS instead (for the whole page, applied before any JavaScript runs), generate a stylesheet rule with `brandCss`:\n\n```ts\nimport { brandCss } from \"@vjdesigns/vjdesigns-ui\";\n\nbrandCss(\"acme\", \"#e11d48\"); // '[data-brand=\"acme\"] { --brand-light-primary: … }'\n```\n\nAdd 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."
31
31
  },
32
32
  {
33
33
  "id": "languages",
34
34
  "title": "Languages",
35
- "markdown": "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:\r\n\r\n```tsx\r\nimport { LocaleProvider } from \"@vjdesigns/vjdesigns-ui\";\r\nimport es from \"@vjdesigns/vjdesigns-ui/locales/es\";\r\n\r\n<LocaleProvider locale=\"es\" messages={es}>\r\n <App />\r\n</LocaleProvider>\r\n```\r\n\r\n`LocaleProvider` sets:\r\n\r\n- **Built-in text** for every component, from `messages`.\r\n- **Dates and numbers** in the locale's format (`23/10/2026`, `1.234,5`), and plurals that follow the language's rules.\r\n- **Writing direction**: Arabic, Hebrew, Persian, and Urdu get `dir=\"rtl\"`, and layouts and arrow keys mirror.\r\n- **`lang`** on its wrapper, so screen readers pronounce the text correctly. Popups (menus, dialogs, tooltips) keep the language of the area that opened them.\r\n\r\nEach 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.\r\n\r\nThis 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).\r\n\r\n### What gets translated\r\n\r\nThe 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}>`:\r\n\r\n| | Translated? |\r\n| --- | --- |\r\n| Text your app passes in, like `<Button>Save</Button>` | No: your app translates it |\r\n| Built-in names, like \"Close\", \"Next page\", \"Clear search\" | Yes: \"Schließen\", \"Nächste Seite\", \"Suche löschen\" |\r\n| Screen-reader announcements, like \"3 results available\" | Yes: \"3 Ergebnisse verfügbar\" |\r\n| Validation messages, empty states, file-upload prompts | Yes |\r\n| Dates and numbers | Yes: \"23.10.2026\", \"4,5\" |\r\n\r\nTo 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)).\r\n\r\n### Bundled languages\r\n\r\n`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).\r\n\r\nThe 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.\r\n\r\n### Changing a few strings\r\n\r\nPass only the strings you want to change. Everything else falls back to the outer provider's language (English at the top):\r\n\r\n```tsx\r\n<LocaleProvider locale=\"en-GB\" messages={{ pagination: { next: \"Onwards\" } }}>\r\n```\r\n\r\nProviders 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.\r\n\r\n### Other languages\r\n\r\nFor a language that isn't bundled, write a complete messages object. TypeScript flags any missing string. Use English as the template:\r\n\r\n```ts\r\nimport { en, plural, type Messages } from \"@vjdesigns/vjdesigns-ui\";\r\n\r\nconst p = plural(\"sw\");\r\nexport const sw: Messages = {\r\n ...en,\r\n pagination: { ...en.pagination, next: \"Ukurasa unaofuata\", previous: \"Ukurasa uliopita\" },\r\n searchInput: {\r\n ...en.searchInput,\r\n results: (count) => p(count, { \"=0\": \"Hakuna matokeo\", one: \"Tokeo #\", other: \"Matokeo #\" }),\r\n },\r\n // ...\r\n};\r\n```\r\n\r\n`plural(locale)` picks the right plural form for any language (`#` is the formatted number).\r\n\r\n### Switching language at runtime\r\n\r\nList the languages your app offers as loaders, so each one is downloaded only when picked:\r\n\r\n```tsx\r\nimport { LocaleProvider, type Messages } from \"@vjdesigns/vjdesigns-ui\";\r\n\r\nconst loaders: Record<string, () => Promise<{ default: Messages }>> = {\r\n es: () => import(\"@vjdesigns/vjdesigns-ui/locales/es\"),\r\n fr: () => import(\"@vjdesigns/vjdesigns-ui/locales/fr\"),\r\n ar: () => import(\"@vjdesigns/vjdesigns-ui/locales/ar\"),\r\n};\r\n\r\nfunction App({ lang }: { lang: string }) {\r\n const [messages, setMessages] = useState<Messages>();\r\n useEffect(() => {\r\n const load = loaders[lang];\r\n if (load) load().then((mod) => setMessages(mod.default));\r\n else setMessages(undefined); // English\r\n }, [lang]);\r\n // Until a language loads, the previous one (or English) shows.\r\n return <LocaleProvider locale={lang} messages={messages}>…</LocaleProvider>;\r\n}\r\n```\r\n\r\nFor server rendering, also set `<html lang=\"…\" dir=\"…\">` on the page. `getLocaleDirection(locale)` returns `\"ltr\"` or `\"rtl\"`."
35
+ "markdown": "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:\n\n```tsx\nimport { LocaleProvider } from \"@vjdesigns/vjdesigns-ui\";\nimport es from \"@vjdesigns/vjdesigns-ui/locales/es\";\n\n<LocaleProvider locale=\"es\" messages={es}>\n <App />\n</LocaleProvider>\n```\n\n`LocaleProvider` sets:\n\n- **Built-in text** for every component, from `messages`.\n- **Dates and numbers** in the locale's format (`23/10/2026`, `1.234,5`), and plurals that follow the language's rules.\n- **Writing direction**: Arabic, Hebrew, Persian, and Urdu get `dir=\"rtl\"`, and layouts and arrow keys mirror.\n- **`lang`** on its wrapper, so screen readers pronounce the text correctly. Popups (menus, dialogs, tooltips) keep the language of the area that opened them.\n\nEach 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.\n\nThis 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).\n\n### What gets translated\n\nThe 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}>`:\n\n| | Translated? |\n| --- | --- |\n| Text your app passes in, like `<Button>Save</Button>` | No: your app translates it |\n| Built-in names, like \"Close\", \"Next page\", \"Clear search\" | Yes: \"Schließen\", \"Nächste Seite\", \"Suche löschen\" |\n| Screen-reader announcements, like \"3 results available\" | Yes: \"3 Ergebnisse verfügbar\" |\n| Validation messages, empty states, file-upload prompts | Yes |\n| Dates and numbers | Yes: \"23.10.2026\", \"4,5\" |\n\nTo 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)).\n\n### Bundled languages\n\n`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).\n\nThe 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).)\n\n### Changing a few strings\n\nPass only the strings you want to change. Everything else falls back to the outer provider's language (English at the top):\n\n```tsx\n<LocaleProvider locale=\"en-GB\" messages={{ pagination: { next: \"Onwards\" } }}>\n```\n\nProviders 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.\n\n### Other languages\n\nFor a language that isn't bundled, write a complete messages object. TypeScript flags any missing string. Use English as the template:\n\n```ts\nimport { en, plural, type Messages } from \"@vjdesigns/vjdesigns-ui\";\n\nconst p = plural(\"sw\");\nexport const sw: Messages = {\n ...en,\n pagination: { ...en.pagination, next: \"Ukurasa unaofuata\", previous: \"Ukurasa uliopita\" },\n searchInput: {\n ...en.searchInput,\n results: (count) => p(count, { \"=0\": \"Hakuna matokeo\", one: \"Tokeo #\", other: \"Matokeo #\" }),\n },\n // ...\n};\n```\n\n`plural(locale)` picks the right plural form for any language (`#` is the formatted number).\n\n### Switching language at runtime\n\nList the languages your app offers as loaders, so each one is downloaded only when picked:\n\n```tsx\nimport { LocaleProvider, type Messages } from \"@vjdesigns/vjdesigns-ui\";\n\nconst loaders: Record<string, () => Promise<{ default: Messages }>> = {\n es: () => import(\"@vjdesigns/vjdesigns-ui/locales/es\"),\n fr: () => import(\"@vjdesigns/vjdesigns-ui/locales/fr\"),\n ar: () => import(\"@vjdesigns/vjdesigns-ui/locales/ar\"),\n};\n\nfunction App({ lang }: { lang: string }) {\n const [messages, setMessages] = useState<Messages>();\n useEffect(() => {\n const load = loaders[lang];\n if (load) load().then((mod) => setMessages(mod.default));\n else setMessages(undefined); // English\n }, [lang]);\n // Until a language loads, the previous one (or English) shows.\n return <LocaleProvider locale={lang} messages={messages}>…</LocaleProvider>;\n}\n```\n\nFor server rendering, also set `<html lang=\"…\" dir=\"…\">` on the page. `getLocaleDirection(locale)` returns `\"ltr\"` or `\"rtl\"`."
36
36
  },
37
37
  {
38
38
  "id": "validation",
39
39
  "title": "Form validation",
40
- "markdown": "> **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.\r\n\r\n`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.\r\n\r\n```tsx\r\nimport {\r\n useFormValidation, rules, ErrorSummary, Field, Input, PasswordInput, Button,\r\n} from \"@vjdesigns/vjdesigns-ui\";\r\n\r\nfunction SignUp() {\r\n const form = useFormValidation({\r\n rules: {\r\n confirm: rules.matches(\"password\", \"The passwords don't match.\"),\r\n serverIp: rules.ip(), // IPv4 or IPv6\r\n },\r\n // Runs only when everything is valid. Return the server's errors to show them on their fields.\r\n onValid: async (values) => {\r\n const response = await api.signUp(values);\r\n return response.errors; // e.g. { username: \"That username is taken.\" }\r\n },\r\n });\r\n\r\n return (\r\n <form {...form.formProps}>\r\n <ErrorSummary ref={form.summaryRef} errors={form.errorList} />\r\n <Field label=\"Email\" required error={form.errors.email}>\r\n <Input name=\"email\" type=\"email\" />\r\n </Field>\r\n <Field label=\"Password\" required error={form.errors.password}>\r\n <PasswordInput name=\"password\" minLength={12} />\r\n </Field>\r\n <Field label=\"Confirm password\" required error={form.errors.confirm}>\r\n <PasswordInput name=\"confirm\" />\r\n </Field>\r\n <Field label=\"Server IP\" error={form.errors.serverIp}>\r\n <Input name=\"serverIp\" />\r\n </Field>\r\n <Button type=\"submit\" loading={form.isSubmitting}>Create account</Button>\r\n </form>\r\n );\r\n}\r\n```\r\n\r\nHow it behaves:\r\n\r\n- **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.\r\n- **Errors are keyed by the field's `name`.** Pass `form.errors.<name>` to each `Field`'s `error`.\r\n- **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.\r\n- **Server errors** (returned from `onValid`, or set with `form.setErrors({...})`) show on their fields and stay until that field is edited.\r\n- **Emails** must have a real domain (`name@example.com`). The browser's own check also accepts `name@localhost`.\r\n\r\n### Rules and format checks\r\n\r\n| Rule | Checks |\r\n| --- | --- |\r\n| `rules.required()` | Not empty (for controls without a native `required`) |\r\n| `rules.email()` | An email address in the usual format, with a top-level domain |\r\n| `rules.ip()` / `rules.ip({ version: 4 })` / `rules.ip({ version: 6 })` | An IPv4 or IPv6 address |\r\n| `rules.minLength(n)` / `rules.maxLength(n)` | Length in characters |\r\n| `rules.pattern(/regex/, \"message\")` | The whole value matches |\r\n| `rules.matches(\"otherField\")` | Equals another field (confirm password) |\r\n| your own `(value, { values }) => message` | Anything else |\r\n\r\nEach rule takes an optional custom message. The checks behind them are exported for use anywhere, including your server:\r\n\r\n- `isEmail(value)`\r\n- `isIPv4(value)`: four numbers 0–255, no leading zeros\r\n- `isIPv6(value)`: including `::` shorthand and IPv4 tails like `::ffff:192.168.0.1`\r\n- `isIP(value, version?)`"
40
+ "markdown": "> **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.\n\n`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.\n\n```tsx\nimport {\n useFormValidation, rules, ErrorSummary, Field, Input, PasswordInput, Button,\n} from \"@vjdesigns/vjdesigns-ui\";\n\nfunction SignUp() {\n const form = useFormValidation({\n rules: {\n confirm: rules.matches(\"password\", \"The passwords don't match.\"),\n serverIp: rules.ip(), // IPv4 or IPv6\n },\n // Runs only when everything is valid. Return the server's errors to show them on their fields.\n onValid: async (values) => {\n const response = await api.signUp(values);\n return response.errors; // e.g. { username: \"That username is taken.\" }\n },\n });\n\n return (\n <form {...form.formProps}>\n <ErrorSummary ref={form.summaryRef} errors={form.errorList} />\n <Field label=\"Email\" required error={form.errors.email}>\n <Input name=\"email\" type=\"email\" />\n </Field>\n <Field label=\"Password\" required error={form.errors.password}>\n <PasswordInput name=\"password\" minLength={12} />\n </Field>\n <Field label=\"Confirm password\" required error={form.errors.confirm}>\n <PasswordInput name=\"confirm\" />\n </Field>\n <Field label=\"Server IP\" error={form.errors.serverIp}>\n <Input name=\"serverIp\" />\n </Field>\n <Button type=\"submit\" loading={form.isSubmitting}>Create account</Button>\n </form>\n );\n}\n```\n\nHow it behaves:\n\n- **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.\n- **Errors are keyed by the field's `name`.** Pass `form.errors.<name>` to each `Field`'s `error`.\n- **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.\n- **Server errors** (returned from `onValid`, or set with `form.setErrors({...})`) show on their fields and stay until that field is edited.\n- **Emails** must have a real domain (`name@example.com`). The browser's own check also accepts `name@localhost`.\n\n### Rules and format checks\n\n| Rule | Checks |\n| --- | --- |\n| `rules.required()` | Not empty (for controls without a native `required`) |\n| `rules.email()` | An email address in the usual format, with a top-level domain |\n| `rules.ip()` / `rules.ip({ version: 4 })` / `rules.ip({ version: 6 })` | An IPv4 or IPv6 address |\n| `rules.minLength(n)` / `rules.maxLength(n)` | Length in characters |\n| `rules.pattern(/regex/, \"message\")` | The whole value matches |\n| `rules.matches(\"otherField\")` | Equals another field (confirm password) |\n| your own `(value, { values }) => message` | Anything else |\n\nEach rule takes an optional custom message. The checks behind them are exported for use anywhere, including your server:\n\n- `isEmail(value)`\n- `isIPv4(value)`: four numbers 0–255, no leading zeros\n- `isIPv6(value)`: including `::` shorthand and IPv4 tails like `::ffff:192.168.0.1`\n- `isIP(value, version?)`"
41
41
  }
42
42
  ],
43
43
  "components": [
@@ -1264,6 +1264,215 @@
1264
1264
  }
1265
1265
  ]
1266
1266
  },
1267
+ {
1268
+ "name": "Chat",
1269
+ "category": "data-display",
1270
+ "folder": "Chat",
1271
+ "import": "import { Chat } from \"@vjdesigns/vjdesigns-ui\";",
1272
+ "description": "A chat interface for an AI assistant (or a person), with no backend of its\nown: `onSend` connects it to any API and can stream the reply. Includes a\nscrolling conversation that follows new messages, an auto-growing message\nbox (Enter sends, Shift+Enter adds a line), Stop and Retry, copy buttons,\nsuggested prompts, and screen reader announcements of each reply.\n\n```tsx\n<Chat\n style={{ blockSize: 560 }}\n onSend={async function* (text, { messages, signal }) {\n const res = await fetch(\"/api/chat\", {\n method: \"POST\",\n body: JSON.stringify({ messages }),\n signal,\n });\n yield* readTextStream(res);\n }}\n/>\n```",
1273
+ "nativeElement": "section",
1274
+ "props": [
1275
+ {
1276
+ "name": "messages",
1277
+ "type": "ChatMessage[]",
1278
+ "required": false,
1279
+ "description": "Controlled conversation."
1280
+ },
1281
+ {
1282
+ "name": "defaultMessages",
1283
+ "type": "ChatMessage[]",
1284
+ "required": false,
1285
+ "description": "Initial conversation when uncontrolled."
1286
+ },
1287
+ {
1288
+ "name": "onMessagesChange",
1289
+ "type": "(messages: ChatMessage[]) => void",
1290
+ "required": false,
1291
+ "description": "Called with the whole conversation whenever it changes (new messages, every streamed chunk, status changes)."
1292
+ },
1293
+ {
1294
+ "name": "onSend",
1295
+ "type": "( text: string, context: ChatSendContext, ) => ChatSendResult | Promise<ChatSendResult>",
1296
+ "required": false,
1297
+ "description": "Connects the Chat to your backend. Called with the user's text when they\nsend a message (or retry). Return the reply as a string, stream it as an\n`AsyncIterable<string>`, or return nothing and add it yourself through\ncontrolled `messages`. Throwing (or rejecting) turns the reply into an\nerror with a Retry button."
1298
+ },
1299
+ {
1300
+ "name": "onStop",
1301
+ "type": "() => void",
1302
+ "required": false,
1303
+ "description": "Called when the user stops a reply, after its `signal` is aborted."
1304
+ },
1305
+ {
1306
+ "name": "onError",
1307
+ "type": "(error: unknown) => void",
1308
+ "required": false,
1309
+ "description": "Called with whatever `onSend` threw, for logging. The user sees `labels.error` (or the message's `error`)."
1310
+ },
1311
+ {
1312
+ "name": "draft",
1313
+ "type": "string",
1314
+ "required": false,
1315
+ "description": "Controlled text of the message box."
1316
+ },
1317
+ {
1318
+ "name": "defaultDraft",
1319
+ "type": "string",
1320
+ "required": false,
1321
+ "description": "Initial text of the message box when uncontrolled."
1322
+ },
1323
+ {
1324
+ "name": "onDraftChange",
1325
+ "type": "(draft: string) => void",
1326
+ "required": false,
1327
+ "description": "Called with the message box text on every edit."
1328
+ },
1329
+ {
1330
+ "name": "suggestions",
1331
+ "type": "string[]",
1332
+ "required": false,
1333
+ "description": "Prompts offered as buttons while the conversation is empty. Pressing one sends it."
1334
+ },
1335
+ {
1336
+ "name": "emptyState",
1337
+ "type": "ReactNode",
1338
+ "required": false,
1339
+ "description": "Shown while the conversation is empty, above the suggestions. Defaults to a short greeting (`labels.emptyTitle`)."
1340
+ },
1341
+ {
1342
+ "name": "renderContent",
1343
+ "type": "(message: ChatMessage) => ReactNode",
1344
+ "required": false,
1345
+ "description": "Renders a message's content, e.g. through your Markdown renderer.\nDefaults to plain text with line breaks kept. Never render\nunsanitized HTML from a model or a user."
1346
+ },
1347
+ {
1348
+ "name": "assistantName",
1349
+ "type": "string",
1350
+ "required": false,
1351
+ "description": "Name of the assistant, read to screen readers (\"Coach said:\"). Defaults to `labels.assistant`."
1352
+ },
1353
+ {
1354
+ "name": "assistantAvatar",
1355
+ "type": "ReactNode",
1356
+ "required": false,
1357
+ "description": "Shown beside assistant messages (e.g. an `Avatar` or `Icon`). Decorative: hidden from assistive technology."
1358
+ },
1359
+ {
1360
+ "name": "userAvatar",
1361
+ "type": "ReactNode",
1362
+ "required": false,
1363
+ "description": "Shown beside user messages. Decorative: hidden from assistive technology."
1364
+ },
1365
+ {
1366
+ "name": "copyable",
1367
+ "type": "boolean",
1368
+ "required": false,
1369
+ "default": "true",
1370
+ "description": "Show a Copy button under finished assistant messages."
1371
+ },
1372
+ {
1373
+ "name": "disclaimer",
1374
+ "type": "ReactNode",
1375
+ "required": false,
1376
+ "description": "Small print under the message box, e.g. \"AI can make mistakes.\""
1377
+ },
1378
+ {
1379
+ "name": "placeholder",
1380
+ "type": "string",
1381
+ "required": false,
1382
+ "description": "Placeholder of the message box. Defaults to `labels.placeholder`."
1383
+ },
1384
+ {
1385
+ "name": "maxLength",
1386
+ "type": "number",
1387
+ "required": false,
1388
+ "description": "Maximum length of a message, in characters. Shows a counter near the limit."
1389
+ },
1390
+ {
1391
+ "name": "disabled",
1392
+ "type": "boolean",
1393
+ "required": false,
1394
+ "description": "Disables the message box and the suggestions."
1395
+ },
1396
+ {
1397
+ "name": "labels",
1398
+ "type": "Partial<ChatLabels>",
1399
+ "required": false,
1400
+ "description": "Overrides for the built-in text."
1401
+ }
1402
+ ],
1403
+ "subcomponents": [],
1404
+ "storybook": "Data Display/Chat",
1405
+ "stories": [
1406
+ {
1407
+ "name": "Playground",
1408
+ "description": ""
1409
+ },
1410
+ {
1411
+ "name": "Send And Stream",
1412
+ "description": "Type a message and press Enter: it is sent, and the streamed reply is announced when done."
1413
+ },
1414
+ {
1415
+ "name": "Shift Enter Adds Line",
1416
+ "description": "Shift+Enter adds a line instead of sending."
1417
+ },
1418
+ {
1419
+ "name": "String Reply",
1420
+ "description": "`onSend` can return the whole reply as a string (or a promise of one)."
1421
+ },
1422
+ {
1423
+ "name": "With Suggestions",
1424
+ "description": "While the conversation is empty, `emptyState` and `suggestions` show. Pressing a suggestion sends it."
1425
+ },
1426
+ {
1427
+ "name": "With Avatars",
1428
+ "description": "Avatars beside each speaker. They're decorative; the speaker is read from hidden text."
1429
+ },
1430
+ {
1431
+ "name": "Long Conversation",
1432
+ "description": "A long conversation. Scroll up, and a button jumps back to the latest message."
1433
+ },
1434
+ {
1435
+ "name": "Long Content",
1436
+ "description": "A long unbroken string (a URL, say) wraps instead of overflowing the bubble."
1437
+ },
1438
+ {
1439
+ "name": "Error And Retry",
1440
+ "description": "When `onSend` throws, the reply shows an error with a Retry button. This backend fails the first time."
1441
+ },
1442
+ {
1443
+ "name": "Stop Streaming",
1444
+ "description": "While a reply streams, the send button becomes Stop (Escape in the message box works too)."
1445
+ },
1446
+ {
1447
+ "name": "Disabled",
1448
+ "description": ""
1449
+ },
1450
+ {
1451
+ "name": "With Max Length",
1452
+ "description": "`maxLength` limits a message and shows a counter near the limit."
1453
+ },
1454
+ {
1455
+ "name": "System Messages",
1456
+ "description": "System messages (joins, handoffs, context notes) are centered notes."
1457
+ },
1458
+ {
1459
+ "name": "Custom Rendering",
1460
+ "description": "`renderContent` renders messages your way, e.g. through a Markdown\nrenderer. This one turns `**bold**` into `<strong>` and keeps paragraphs.\nNever render unsanitized HTML from a model."
1461
+ },
1462
+ {
1463
+ "name": "Controlled",
1464
+ "description": "Controlled: the parent owns the messages, so it can save, clear, or seed\nthem. Here, `onSend` returns nothing and the parent adds the reply itself\n(the way a WebSocket or a human agent would)."
1465
+ },
1466
+ {
1467
+ "name": "Connecting ABackend",
1468
+ "description": "How to connect a real backend: stream server-sent events from your API\nroute with `readEventStream`, passing `signal` so Stop cancels the request.\n\n```tsx\nimport { Chat, readEventStream } from \"@vjdesigns/vjdesigns-ui\";\n\n<Chat\n onSend={async function* (_text, { messages, signal }) {\n const res = await fetch(\"/api/chat\", {\n method: \"POST\",\n headers: { \"Content-Type\": \"application/json\" },\n body: JSON.stringify({\n messages: messages.map(({ role, content }) => ({ role, content })),\n }),\n signal,\n });\n if (!res.ok) throw new Error(`Chat request failed: ${res.status}`);\n for await (const data of readEventStream(res)) {\n if (data === \"[DONE]\") break;\n yield JSON.parse(data).text ?? \"\";\n }\n }}\n/>\n```\n\nKeep API keys on your server: the browser should call your own route,\nwhich calls the model provider. This story fakes the server with an\nin-memory event stream."
1469
+ },
1470
+ {
1471
+ "name": "Right To Left",
1472
+ "description": "Right-to-left: user messages sit on the inline end, which is the left in RTL."
1473
+ }
1474
+ ]
1475
+ },
1267
1476
  {
1268
1477
  "name": "Chip",
1269
1478
  "category": "data-display",
@@ -0,0 +1 @@
1
+ const e=require("../../../hooks/useControllableState.cjs.js"),t=require("../../utilities/LocaleProvider/LocaleProvider.cjs.js"),n=require("../../../hooks/useClipboard.cjs.js"),r=require("../../../hooks/useEnvironment.cjs.js"),i=require("../../../utils/cx.cjs.js"),a=require("../../../utils/announce.cjs.js"),o=require("../../utilities/VisuallyHidden/VisuallyHidden.cjs.js"),s=require("../../primitives/Button/Button.cjs.js"),c=require("../../primitives/Icon/Icon.cjs.js"),l=require("../../layout/ScrollArea/ScrollArea.cjs.js"),ee=require("../../forms/Textarea/Textarea.cjs.js"),u=require("./Chat.module.css.cjs.js");let d=require("react"),f=require("react/jsx-runtime");var te=48,ne=typeof window<`u`?d.useLayoutEffect:d.useEffect;function re(e){return e!=null&&typeof e[Symbol.asyncIterator]==`function`}var ie=()=>(0,f.jsx)(c.Icon,{children:(0,f.jsx)(`rect`,{x:`7`,y:`7`,width:`10`,height:`10`,rx:`2`,fill:`currentColor`,stroke:`none`})}),p=({messages:p,defaultMessages:ae,onMessagesChange:oe,onSend:m,onStop:se,onError:h,draft:ce,defaultDraft:le,onDraftChange:ue,suggestions:g,emptyState:de,renderContent:_,assistantName:fe,assistantAvatar:pe,userAvatar:v,copyable:y=!0,disclaimer:b,placeholder:me,maxLength:x,disabled:S=!1,labels:he,className:ge,"aria-label":_e,...C})=>{let w={...t.useMessages().chat,...he},T=fe??w.assistant,ve=r.useReducedMotion(),E=(0,d.useId)(),D=`${E}-hint`,[O,k]=e.useControllableState({value:p,defaultValue:ae??[],onChange:oe}),A=(0,d.useRef)(O),j=(0,d.useRef)(O);O!==j.current&&(j.current=O,A.current=O);let M=(0,d.useCallback)(e=>{let t=e(A.current);A.current=t,k(t)},[k]),N=(0,d.useCallback)((e,t)=>M(n=>n.map(n=>n.id===e?{...n,...t}:n)),[M]),P=(0,d.useRef)(0),F=()=>`${E}-${++P.current}`,[I,L]=e.useControllableState({value:ce,defaultValue:le??``,onChange:ue}),[R,z]=(0,d.useState)(!1),B=(0,d.useRef)(null),V=(0,d.useRef)(null),H=(0,d.useRef)(m);H.current=m;let U=(0,d.useRef)(h);U.current=h,(0,d.useEffect)(()=>()=>B.current?.abort(),[]);let W=(0,d.useRef)(null),G=(0,d.useRef)(!0),[ye,K]=(0,d.useState)(!0),be=()=>{let e=W.current;if(!e)return;let t=e.scrollHeight-e.scrollTop-e.clientHeight<=te;G.current=t,K(t)};ne(()=>{let e=W.current;e&&G.current&&(e.scrollTop=e.scrollHeight)},[O]);let xe=()=>{let e=W.current;e&&(G.current=!0,K(!0),e.scrollTo({top:e.scrollHeight,behavior:ve?`auto`:`smooth`}),V.current?.focus())},q=async(e,t)=>{let n=F();M(()=>[...e,{id:n,role:`assistant`,content:``,status:`streaming`}]),G.current=!0,K(!0),z(!0),a.announce(w.typing(T));let r=new AbortController;B.current=r;let i=``;try{let o=await H.current?.(t,{messages:e,signal:r.signal});if(typeof o==`string`)i=o;else if(re(o))for await(let e of o){if(r.signal.aborted)break;i+=e,N(n,{content:i})}else if(!r.signal.aborted){M(e=>e.filter(e=>e.id!==n));return}if(r.signal.aborted)throw r.signal.reason;N(n,{content:i,status:`done`}),a.announce(`${w.assistantSaid(T)} ${i}`)}catch(e){r.signal.aborted?(N(n,{content:i,status:`stopped`}),a.announce(w.stopped)):(U.current?.(e),N(n,{content:i,status:`error`}),a.announce(w.error))}finally{B.current===r&&(B.current=null),z(!1)}},J=e=>{let t=e.trim();!t||R||S||(L(``),q([...A.current,{id:F(),role:`user`,content:t,status:`done`}],t))},Y=()=>{B.current&&(B.current.abort(),se?.(),V.current?.focus())},Se=e=>{let t=A.current,n=t.findIndex(t=>t.id===e)-1;for(;n>=0&&t[n].role!==`user`;)n--;n<0||R||(q(t.slice(0,n+1),t[n].content),V.current?.focus())},X=e=>{e.preventDefault(),R?Y():J(I)},Ce=e=>{if(e.key===`Escape`&&R){e.preventDefault(),Y();return}e.key===`Enter`&&!e.shiftKey&&!e.nativeEvent.isComposing&&(e.preventDefault(),J(e.currentTarget.value))},{copy:we}=n.useClipboard(),[Z,Q]=(0,d.useState)(null);(0,d.useEffect)(()=>{if(Z===null)return;let e=window.setTimeout(()=>Q(null),2e3);return()=>window.clearTimeout(e)},[Z]);let Te=async e=>{await we(e.content)&&(Q(e.id),a.announce(w.copied))},$=O.filter(e=>e.role!==`system`).length===0,Ee=O[O.length-1],De=e=>{if(e.role===`system`)return(0,f.jsx)(`p`,{className:u.default.system,children:e.content},e.id);let t=e.role===`user`,n=e.status??`done`,r=n===`streaming`&&e.content===``,a=t?v:pe,l=!t&&!R&&e===Ee&&(n===`error`||n===`stopped`);return(0,f.jsxs)(`article`,{className:i.cx(u.default.message,t?u.default.user:u.default.assistant),"data-role":e.role,"data-status":n,"aria-busy":n===`streaming`||void 0,children:[a!=null&&(0,f.jsx)(`div`,{className:u.default.avatar,"aria-hidden":`true`,children:a}),(0,f.jsxs)(`div`,{className:u.default.body,children:[r?(0,f.jsxs)(`div`,{className:u.default.bubble,children:[(0,f.jsxs)(`span`,{className:u.default.dots,"aria-hidden":`true`,children:[(0,f.jsx)(`span`,{}),(0,f.jsx)(`span`,{}),(0,f.jsx)(`span`,{})]}),(0,f.jsx)(o.VisuallyHidden,{children:w.typing(T)})]}):e.content!==``&&(0,f.jsxs)(`div`,{className:u.default.bubble,children:[(0,f.jsxs)(o.VisuallyHidden,{children:[t?w.youSaid:w.assistantSaid(T),` `]}),_?_(e):(0,f.jsx)(`p`,{className:u.default.text,children:e.content})]}),n===`error`&&(0,f.jsxs)(`p`,{className:u.default.error,children:[(0,f.jsx)(c.Icon,{name:`error`,size:`sm`}),(0,f.jsx)(`span`,{children:e.error??w.error})]}),n===`stopped`&&(0,f.jsx)(`p`,{className:u.default.note,children:w.stopped}),!t&&(l||y&&n===`done`&&e.content!==``)&&(0,f.jsxs)(`div`,{className:u.default.actions,children:[y&&n===`done`&&e.content!==``&&(0,f.jsx)(s.IconButton,{variant:`ghost`,tone:`neutral`,size:`sm`,"aria-label":Z===e.id?w.copied:w.copy,onClick:()=>void Te(e),children:(0,f.jsx)(c.Icon,{name:Z===e.id?`check`:`copy`})}),l&&(0,f.jsx)(s.Button,{variant:`ghost`,tone:`neutral`,size:`sm`,startIcon:(0,f.jsx)(c.Icon,{name:`refresh`}),onClick:()=>Se(e.id),children:w.retry})]})]})]},e.id)};return(0,f.jsxs)(`section`,{...C,"aria-label":_e??(C[`aria-labelledby`]?void 0:w.region),className:i.cx(u.default.root,ge),children:[(0,f.jsxs)(`div`,{className:u.default.logWrap,children:[(0,f.jsx)(l.ScrollArea,{className:u.default.log,viewportRef:W,onViewportScroll:be,"aria-label":w.log,"aria-busy":R||void 0,tabIndex:0,children:(0,f.jsxs)(`div`,{className:u.default.messages,children:[$&&(0,f.jsxs)(`div`,{className:u.default.empty,children:[de??(0,f.jsx)(`p`,{className:u.default.emptyTitle,children:w.emptyTitle}),g&&g.length>0&&(0,f.jsx)(`div`,{role:`group`,"aria-label":w.suggestions,className:u.default.suggestions,children:g.map(e=>(0,f.jsx)(s.Button,{variant:`outline`,tone:`neutral`,size:`sm`,disabled:S,className:u.default.suggestion,onClick:()=>J(e),children:e},e))})]}),O.map(De)]})}),!ye&&!$&&(0,f.jsx)(s.IconButton,{variant:`soft`,tone:`neutral`,size:`sm`,className:u.default.jump,"aria-label":w.scrollToLatest,onClick:xe,children:(0,f.jsx)(c.Icon,{name:`arrow-down`})})]}),(0,f.jsxs)(`form`,{className:u.default.composer,onSubmit:X,children:[(0,f.jsxs)(`div`,{className:u.default.inputRow,children:[(0,f.jsx)(ee.Textarea,{ref:V,className:u.default.input,"aria-label":w.input,"aria-describedby":D,placeholder:me??w.placeholder,value:I,onValueChange:L,onKeyDown:Ce,autoResize:!0,rows:1,maxRows:6,maxLength:x,showCount:x!==void 0,disabled:S,enterKeyHint:`send`}),R?(0,f.jsx)(s.IconButton,{type:`submit`,variant:`soft`,tone:`neutral`,"aria-label":w.stop,children:(0,f.jsx)(ie,{})}):(0,f.jsx)(s.IconButton,{type:`submit`,"aria-label":w.send,disabled:S||I.trim()===``,children:(0,f.jsx)(c.Icon,{name:`arrow-up`})})]}),(0,f.jsx)(o.VisuallyHidden,{id:D,children:w.hint}),b!=null&&(0,f.jsx)(`p`,{className:u.default.disclaimer,children:b})]})]})};exports.Chat=p;
@@ -0,0 +1,114 @@
1
+ import { ComponentPropsWithRef, ReactNode } from 'react';
2
+ import { Messages } from '../../../i18n/types';
3
+ /** Who wrote a message. `system` messages are shown as centered notes. */
4
+ export type ChatRole = "user" | "assistant" | "system";
5
+ /**
6
+ * Where a message is in its life. `streaming` shows a typing indicator while
7
+ * the content is empty; `error` shows the error and a Retry button;
8
+ * `stopped` marks a reply the user interrupted.
9
+ */
10
+ export type ChatMessageStatus = "streaming" | "done" | "stopped" | "error";
11
+ /** One message in the conversation. Plain data, so it can be stored or sent to a backend as is. */
12
+ export interface ChatMessage {
13
+ /** Unique, stable id. */
14
+ id: string;
15
+ /** Who wrote it. */
16
+ role: ChatRole;
17
+ /** The text. Shown with line breaks kept, or through `renderContent`. */
18
+ content: string;
19
+ /** Message state. Treated as `done` when omitted. */
20
+ status?: ChatMessageStatus;
21
+ /** Error text for an `error` message. Defaults to the built-in error message. */
22
+ error?: string;
23
+ }
24
+ /** Passed to `onSend` along with the text. */
25
+ export interface ChatSendContext {
26
+ /** The conversation so far, ending with the new user message. */
27
+ messages: ChatMessage[];
28
+ /** Aborted when the user presses Stop or the Chat unmounts. Pass it to `fetch`. */
29
+ signal: AbortSignal;
30
+ }
31
+ /**
32
+ * What `onSend` returns: the whole reply as a string, an async iterable of
33
+ * text chunks to stream it in (an `async function*`, or `readTextStream`),
34
+ * or nothing when you add the reply yourself through controlled `messages`.
35
+ */
36
+ export type ChatSendResult = string | AsyncIterable<string> | void;
37
+ /** Built-in text of a Chat. Every key can be overridden with the `labels` prop. */
38
+ export type ChatLabels = Messages["chat"];
39
+ export interface ChatProps extends Omit<ComponentPropsWithRef<"section">, "children" | "onError"> {
40
+ /** Controlled conversation. */
41
+ messages?: ChatMessage[];
42
+ /** Initial conversation when uncontrolled. */
43
+ defaultMessages?: ChatMessage[];
44
+ /** Called with the whole conversation whenever it changes (new messages, every streamed chunk, status changes). */
45
+ onMessagesChange?: (messages: ChatMessage[]) => void;
46
+ /**
47
+ * Connects the Chat to your backend. Called with the user's text when they
48
+ * send a message (or retry). Return the reply as a string, stream it as an
49
+ * `AsyncIterable<string>`, or return nothing and add it yourself through
50
+ * controlled `messages`. Throwing (or rejecting) turns the reply into an
51
+ * error with a Retry button.
52
+ */
53
+ onSend?: (text: string, context: ChatSendContext) => ChatSendResult | Promise<ChatSendResult>;
54
+ /** Called when the user stops a reply, after its `signal` is aborted. */
55
+ onStop?: () => void;
56
+ /** Called with whatever `onSend` threw, for logging. The user sees `labels.error` (or the message's `error`). */
57
+ onError?: (error: unknown) => void;
58
+ /** Controlled text of the message box. */
59
+ draft?: string;
60
+ /** Initial text of the message box when uncontrolled. */
61
+ defaultDraft?: string;
62
+ /** Called with the message box text on every edit. */
63
+ onDraftChange?: (draft: string) => void;
64
+ /** Prompts offered as buttons while the conversation is empty. Pressing one sends it. */
65
+ suggestions?: string[];
66
+ /** Shown while the conversation is empty, above the suggestions. Defaults to a short greeting (`labels.emptyTitle`). */
67
+ emptyState?: ReactNode;
68
+ /**
69
+ * Renders a message's content, e.g. through your Markdown renderer.
70
+ * Defaults to plain text with line breaks kept. Never render
71
+ * unsanitized HTML from a model or a user.
72
+ */
73
+ renderContent?: (message: ChatMessage) => ReactNode;
74
+ /** Name of the assistant, read to screen readers ("Coach said:"). Defaults to `labels.assistant`. */
75
+ assistantName?: string;
76
+ /** Shown beside assistant messages (e.g. an `Avatar` or `Icon`). Decorative: hidden from assistive technology. */
77
+ assistantAvatar?: ReactNode;
78
+ /** Shown beside user messages. Decorative: hidden from assistive technology. */
79
+ userAvatar?: ReactNode;
80
+ /** Show a Copy button under finished assistant messages. @default true */
81
+ copyable?: boolean;
82
+ /** Small print under the message box, e.g. "AI can make mistakes." */
83
+ disclaimer?: ReactNode;
84
+ /** Placeholder of the message box. Defaults to `labels.placeholder`. */
85
+ placeholder?: string;
86
+ /** Maximum length of a message, in characters. Shows a counter near the limit. */
87
+ maxLength?: number;
88
+ /** Disables the message box and the suggestions. */
89
+ disabled?: boolean;
90
+ /** Overrides for the built-in text. */
91
+ labels?: Partial<ChatLabels>;
92
+ }
93
+ /**
94
+ * A chat interface for an AI assistant (or a person), with no backend of its
95
+ * own: `onSend` connects it to any API and can stream the reply. Includes a
96
+ * scrolling conversation that follows new messages, an auto-growing message
97
+ * box (Enter sends, Shift+Enter adds a line), Stop and Retry, copy buttons,
98
+ * suggested prompts, and screen reader announcements of each reply.
99
+ *
100
+ * ```tsx
101
+ * <Chat
102
+ * style={{ blockSize: 560 }}
103
+ * onSend={async function* (text, { messages, signal }) {
104
+ * const res = await fetch("/api/chat", {
105
+ * method: "POST",
106
+ * body: JSON.stringify({ messages }),
107
+ * signal,
108
+ * });
109
+ * yield* readTextStream(res);
110
+ * }}
111
+ * />
112
+ * ```
113
+ */
114
+ export declare const Chat: ({ messages: messagesProp, defaultMessages, onMessagesChange, onSend, onStop, onError, draft: draftProp, defaultDraft, onDraftChange, suggestions, emptyState, renderContent, assistantName: assistantNameProp, assistantAvatar, userAvatar, copyable, disclaimer, placeholder, maxLength, disabled, labels: labelsProp, className, "aria-label": ariaLabel, ...rest }: ChatProps) => import("react").JSX.Element;