@vjdesigns/vjdesigns-ui 2.0.0 → 2.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +781 -724
- package/dist/ai/llms-full.txt +555 -480
- package/dist/ai/llms.txt +1 -0
- package/dist/ai/registry.json +220 -7
- package/dist/components/data-display/Chat/Chat.cjs.js +1 -0
- package/dist/components/data-display/Chat/Chat.d.ts +114 -0
- package/dist/components/data-display/Chat/Chat.mjs +301 -0
- package/dist/components/data-display/Chat/Chat.module.css.cjs.js +1 -0
- package/dist/components/data-display/Chat/Chat.module.css.mjs +32 -0
- package/dist/components/data-display/Chat/index.d.ts +2 -0
- package/dist/components/data-display/Chat/streams.cjs.js +4 -0
- package/dist/components/data-display/Chat/streams.d.ts +32 -0
- package/dist/components/data-display/Chat/streams.mjs +35 -0
- package/dist/components/feedback/Toast/ToastProvider.cjs.js +1 -1
- package/dist/components/feedback/Toast/ToastProvider.d.ts +4 -1
- package/dist/components/feedback/Toast/ToastProvider.mjs +44 -26
- package/dist/components/forms/Combobox/Combobox.cjs.js +1 -1
- package/dist/components/forms/Combobox/Combobox.mjs +5 -5
- package/dist/components/forms/TagInput/TagInput.cjs.js +1 -1
- package/dist/components/forms/TagInput/TagInput.mjs +5 -5
- package/dist/i18n/en.cjs.js +1 -1
- package/dist/i18n/en.mjs +21 -0
- package/dist/i18n/types.d.ts +35 -0
- package/dist/index.cjs.js +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.mjs +85 -83
- package/dist/locales/ar.cjs.js +1 -1
- package/dist/locales/ar.mjs +21 -0
- package/dist/locales/bn.cjs.js +1 -1
- package/dist/locales/bn.mjs +21 -0
- package/dist/locales/cs.cjs.js +1 -1
- package/dist/locales/cs.mjs +21 -0
- package/dist/locales/da.cjs.js +1 -1
- package/dist/locales/da.mjs +21 -0
- package/dist/locales/de.cjs.js +1 -1
- package/dist/locales/de.mjs +21 -0
- package/dist/locales/el.cjs.js +1 -1
- package/dist/locales/el.mjs +21 -0
- package/dist/locales/es.cjs.js +1 -1
- package/dist/locales/es.mjs +21 -0
- package/dist/locales/fa.cjs.js +1 -1
- package/dist/locales/fa.mjs +21 -0
- package/dist/locales/fi.cjs.js +1 -1
- package/dist/locales/fi.mjs +21 -0
- package/dist/locales/fr.cjs.js +1 -1
- package/dist/locales/fr.mjs +21 -0
- package/dist/locales/he.cjs.js +1 -1
- package/dist/locales/he.mjs +21 -0
- package/dist/locales/hi.cjs.js +1 -1
- package/dist/locales/hi.mjs +21 -0
- package/dist/locales/hu.cjs.js +1 -1
- package/dist/locales/hu.mjs +21 -0
- package/dist/locales/id.cjs.js +1 -1
- package/dist/locales/id.mjs +21 -0
- package/dist/locales/it.cjs.js +1 -1
- package/dist/locales/it.mjs +21 -0
- package/dist/locales/ja.cjs.js +1 -1
- package/dist/locales/ja.mjs +21 -0
- package/dist/locales/ko.cjs.js +1 -1
- package/dist/locales/ko.mjs +21 -0
- package/dist/locales/nb.cjs.js +1 -1
- package/dist/locales/nb.mjs +21 -0
- package/dist/locales/nl.cjs.js +1 -1
- package/dist/locales/nl.mjs +21 -0
- package/dist/locales/pl.cjs.js +1 -1
- package/dist/locales/pl.mjs +21 -0
- package/dist/locales/pt-BR.cjs.js +1 -1
- package/dist/locales/pt-BR.mjs +21 -0
- package/dist/locales/ro.cjs.js +1 -1
- package/dist/locales/ro.mjs +21 -0
- package/dist/locales/ru.cjs.js +1 -1
- package/dist/locales/ru.mjs +21 -0
- package/dist/locales/sv.cjs.js +1 -1
- package/dist/locales/sv.mjs +21 -0
- package/dist/locales/th.cjs.js +1 -1
- package/dist/locales/th.mjs +21 -0
- package/dist/locales/tr.cjs.js +1 -1
- package/dist/locales/tr.mjs +21 -0
- package/dist/locales/uk.cjs.js +1 -1
- package/dist/locales/uk.mjs +21 -0
- package/dist/locales/ur.cjs.js +1 -1
- package/dist/locales/ur.mjs +21 -0
- package/dist/locales/vi.cjs.js +1 -1
- package/dist/locales/vi.mjs +21 -0
- package/dist/locales/zh-Hans.cjs.js +1 -1
- package/dist/locales/zh-Hans.mjs +21 -0
- package/dist/locales/zh-Hant.cjs.js +1 -1
- package/dist/locales/zh-Hant.mjs +21 -0
- package/dist/vjdesigns-ui.css +1 -1
- package/package.json +90 -88
package/dist/ai/llms-full.txt
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
# @vjdesigns/vjdesigns-ui 1.
|
|
1
|
+
# @vjdesigns/vjdesigns-ui 2.1.0: full reference
|
|
2
2
|
|
|
3
3
|
> Accessible React 19 component library: WCAG 2.2 AA verified (axe on every story, a contrast check on every color pair), 31 languages with right-to-left support, themeable with any brand color (auto-adjusted to stay accessible), tree-shakable, no runtime dependencies besides React.
|
|
4
4
|
|
|
@@ -12,500 +12,500 @@
|
|
|
12
12
|
|
|
13
13
|
# Guide: Installation
|
|
14
14
|
|
|
15
|
-
Install the package along with React and React DOM (version 18 or 19), which the components need in order to render:
|
|
16
|
-
|
|
17
|
-
```bash
|
|
18
|
-
npm install @vjdesigns/vjdesigns-ui react react-dom
|
|
19
|
-
```
|
|
20
|
-
|
|
21
|
-
If your project already uses React, you only need the first package:
|
|
22
|
-
|
|
23
|
-
```bash
|
|
24
|
-
npm install @vjdesigns/vjdesigns-ui
|
|
25
|
-
```
|
|
26
|
-
|
|
27
|
-
### Import the styles
|
|
28
|
-
|
|
29
|
-
The component styles and shared design tokens ship as a separate stylesheet. Import it **once** at your app's entry point, or the components will render unstyled.
|
|
30
|
-
|
|
31
|
-
**React:** in `main.tsx` (or `App.tsx`)
|
|
32
|
-
|
|
33
|
-
```ts
|
|
34
|
-
import "@vjdesigns/vjdesigns-ui/dist/vjdesigns-ui.css";
|
|
35
|
-
```
|
|
36
|
-
|
|
37
|
-
**Angular:** add it to the `styles` array in `angular.json`
|
|
38
|
-
|
|
39
|
-
```json
|
|
40
|
-
"styles": [
|
|
41
|
-
"node_modules/@vjdesigns/vjdesigns-ui/dist/vjdesigns-ui.css",
|
|
42
|
-
"src/styles.css"
|
|
43
|
-
]
|
|
44
|
-
```
|
|
45
|
-
|
|
46
|
-
**Vue:** in `main.ts`
|
|
47
|
-
|
|
48
|
-
```ts
|
|
49
|
-
import "@vjdesigns/vjdesigns-ui/dist/vjdesigns-ui.css";
|
|
15
|
+
Install the package along with React and React DOM (version 18 or 19), which the components need in order to render:
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
npm install @vjdesigns/vjdesigns-ui react react-dom
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
If your project already uses React, you only need the first package:
|
|
22
|
+
|
|
23
|
+
```bash
|
|
24
|
+
npm install @vjdesigns/vjdesigns-ui
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
### Import the styles
|
|
28
|
+
|
|
29
|
+
The component styles and shared design tokens ship as a separate stylesheet. Import it **once** at your app's entry point, or the components will render unstyled.
|
|
30
|
+
|
|
31
|
+
**React:** in `main.tsx` (or `App.tsx`)
|
|
32
|
+
|
|
33
|
+
```ts
|
|
34
|
+
import "@vjdesigns/vjdesigns-ui/dist/vjdesigns-ui.css";
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
**Angular:** add it to the `styles` array in `angular.json`
|
|
38
|
+
|
|
39
|
+
```json
|
|
40
|
+
"styles": [
|
|
41
|
+
"node_modules/@vjdesigns/vjdesigns-ui/dist/vjdesigns-ui.css",
|
|
42
|
+
"src/styles.css"
|
|
43
|
+
]
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
**Vue:** in `main.ts`
|
|
47
|
+
|
|
48
|
+
```ts
|
|
49
|
+
import "@vjdesigns/vjdesigns-ui/dist/vjdesigns-ui.css";
|
|
50
50
|
```
|
|
51
51
|
|
|
52
52
|
# Guide: Usage
|
|
53
53
|
|
|
54
|
-
The components are built with React. You can use them directly in a React project. In Angular or Vue, you mount them inside a small wrapper component, as shown below.
|
|
55
|
-
|
|
56
|
-
### React
|
|
57
|
-
|
|
58
|
-
```tsx
|
|
59
|
-
import { useState } from "react";
|
|
60
|
-
import { Button, CheckBox } from "@vjdesigns/vjdesigns-ui";
|
|
61
|
-
|
|
62
|
-
export function SignupForm() {
|
|
63
|
-
const [accepted, setAccepted] = useState(false);
|
|
64
|
-
|
|
65
|
-
return (
|
|
66
|
-
<form>
|
|
67
|
-
<CheckBox
|
|
68
|
-
label="Accept terms"
|
|
69
|
-
checked={accepted}
|
|
70
|
-
onCheckedChange={setAccepted}
|
|
71
|
-
/>
|
|
72
|
-
<Button
|
|
73
|
-
variant="solid"
|
|
74
|
-
tone="primary"
|
|
75
|
-
size="md"
|
|
76
|
-
disabled={!accepted}
|
|
77
|
-
onClick={() => console.log("Submitted")}
|
|
78
|
-
>
|
|
79
|
-
Submit
|
|
80
|
-
</Button>
|
|
81
|
-
</form>
|
|
82
|
-
);
|
|
83
|
-
}
|
|
84
|
-
```
|
|
85
|
-
|
|
86
|
-
### Angular
|
|
87
|
-
|
|
88
|
-
Make a standalone wrapper that renders the React component into its host element. The wrapper uses `createElement`, so you don't need to change your JSX or TSX settings.
|
|
89
|
-
|
|
90
|
-
```ts
|
|
91
|
-
// vj-button.component.ts
|
|
92
|
-
import {
|
|
93
|
-
AfterViewInit,
|
|
94
|
-
Component,
|
|
95
|
-
ElementRef,
|
|
96
|
-
EventEmitter,
|
|
97
|
-
Input,
|
|
98
|
-
OnChanges,
|
|
99
|
-
OnDestroy,
|
|
100
|
-
Output,
|
|
101
|
-
ViewChild,
|
|
102
|
-
} from "@angular/core";
|
|
103
|
-
import { createElement, type MouseEvent as ReactMouseEvent, type ReactElement } from "react";
|
|
104
|
-
import { createRoot, Root } from "react-dom/client";
|
|
105
|
-
import {
|
|
106
|
-
Button,
|
|
107
|
-
type ButtonProps,
|
|
108
|
-
type ButtonVariant,
|
|
109
|
-
type Size,
|
|
110
|
-
type Tone,
|
|
111
|
-
} from "@vjdesigns/vjdesigns-ui";
|
|
112
|
-
|
|
113
|
-
// Button has overloaded types (for `href` and `as`). Narrowing it here lets createElement type-check.
|
|
114
|
-
const ReactButton = Button as (props: ButtonProps) => ReactElement;
|
|
115
|
-
|
|
116
|
-
@Component({
|
|
117
|
-
selector: "vj-button",
|
|
118
|
-
standalone: true,
|
|
119
|
-
template: `<span #host></span>`,
|
|
120
|
-
})
|
|
121
|
-
export class VjButtonComponent implements AfterViewInit, OnChanges, OnDestroy {
|
|
122
|
-
@ViewChild("host", { static: true }) host!: ElementRef<HTMLElement>;
|
|
123
|
-
|
|
124
|
-
@Input() label = "";
|
|
125
|
-
@Input() variant: ButtonVariant = "solid";
|
|
126
|
-
@Input() tone: Tone = "primary";
|
|
127
|
-
@Input() size: Size = "md";
|
|
128
|
-
@Input() disabled = false;
|
|
129
|
-
@Output() clicked = new EventEmitter<MouseEvent>();
|
|
130
|
-
|
|
131
|
-
private root?: Root;
|
|
132
|
-
|
|
133
|
-
ngAfterViewInit() {
|
|
134
|
-
this.root = createRoot(this.host.nativeElement);
|
|
135
|
-
this.render();
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
ngOnChanges() {
|
|
139
|
-
this.render();
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
ngOnDestroy() {
|
|
143
|
-
this.root?.unmount();
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
private render() {
|
|
147
|
-
this.root?.render(
|
|
148
|
-
createElement(
|
|
149
|
-
ReactButton,
|
|
150
|
-
{
|
|
151
|
-
variant: this.variant,
|
|
152
|
-
tone: this.tone,
|
|
153
|
-
size: this.size,
|
|
154
|
-
disabled: this.disabled,
|
|
155
|
-
onClick: (event: ReactMouseEvent) => this.clicked.emit(event.nativeEvent),
|
|
156
|
-
},
|
|
157
|
-
this.label // the button's text is passed as children
|
|
158
|
-
)
|
|
159
|
-
);
|
|
160
|
-
}
|
|
161
|
-
}
|
|
162
|
-
```
|
|
163
|
-
|
|
164
|
-
Import the wrapper wherever you need it:
|
|
165
|
-
|
|
166
|
-
```ts
|
|
167
|
-
// app.component.ts
|
|
168
|
-
import { Component } from "@angular/core";
|
|
169
|
-
import { VjButtonComponent } from "./vj-button.component";
|
|
170
|
-
|
|
171
|
-
@Component({
|
|
172
|
-
selector: "app-root",
|
|
173
|
-
standalone: true,
|
|
174
|
-
imports: [VjButtonComponent],
|
|
175
|
-
template: `<vj-button label="Submit" tone="primary" (clicked)="onSubmit()" />`,
|
|
176
|
-
})
|
|
177
|
-
export class AppComponent {
|
|
178
|
-
onSubmit() {
|
|
179
|
-
console.log("Submitted");
|
|
180
|
-
}
|
|
181
|
-
}
|
|
182
|
-
```
|
|
183
|
-
|
|
184
|
-
### Vue 3
|
|
185
|
-
|
|
186
|
-
Make a wrapper component that mounts the React component when it loads and re-renders it whenever its props change.
|
|
187
|
-
|
|
188
|
-
```vue
|
|
189
|
-
<!-- VjButton.vue -->
|
|
190
|
-
<script setup lang="ts">
|
|
191
|
-
import { onBeforeUnmount, onMounted, ref, watch } from "vue";
|
|
192
|
-
import { createElement, type MouseEvent as ReactMouseEvent, type ReactElement } from "react";
|
|
193
|
-
import { createRoot, type Root } from "react-dom/client";
|
|
194
|
-
import {
|
|
195
|
-
Button,
|
|
196
|
-
type ButtonProps,
|
|
197
|
-
type ButtonVariant,
|
|
198
|
-
type Size,
|
|
199
|
-
type Tone,
|
|
200
|
-
} from "@vjdesigns/vjdesigns-ui";
|
|
201
|
-
|
|
202
|
-
// Button has overloaded types (for `href` and `as`). Narrowing it here lets createElement type-check.
|
|
203
|
-
const ReactButton = Button as (props: ButtonProps) => ReactElement;
|
|
204
|
-
|
|
205
|
-
const props = withDefaults(
|
|
206
|
-
defineProps<{
|
|
207
|
-
label: string;
|
|
208
|
-
variant?: ButtonVariant;
|
|
209
|
-
tone?: Tone;
|
|
210
|
-
size?: Size;
|
|
211
|
-
disabled?: boolean;
|
|
212
|
-
}>(),
|
|
213
|
-
{ variant: "solid", tone: "primary", size: "md", disabled: false }
|
|
214
|
-
);
|
|
215
|
-
|
|
216
|
-
const emit = defineEmits<{ click: [event: MouseEvent] }>();
|
|
217
|
-
|
|
218
|
-
const host = ref<HTMLElement | null>(null);
|
|
219
|
-
let root: Root | undefined;
|
|
220
|
-
|
|
221
|
-
function render() {
|
|
222
|
-
root?.render(
|
|
223
|
-
createElement(
|
|
224
|
-
ReactButton,
|
|
225
|
-
{
|
|
226
|
-
variant: props.variant,
|
|
227
|
-
tone: props.tone,
|
|
228
|
-
size: props.size,
|
|
229
|
-
disabled: props.disabled,
|
|
230
|
-
onClick: (event: ReactMouseEvent) => emit("click", event.nativeEvent),
|
|
231
|
-
},
|
|
232
|
-
props.label // the button's text is passed as children
|
|
233
|
-
)
|
|
234
|
-
);
|
|
235
|
-
}
|
|
236
|
-
|
|
237
|
-
onMounted(() => {
|
|
238
|
-
root = createRoot(host.value!);
|
|
239
|
-
render();
|
|
240
|
-
});
|
|
241
|
-
|
|
242
|
-
watch(() => ({ ...props }), render);
|
|
243
|
-
|
|
244
|
-
onBeforeUnmount(() => root?.unmount());
|
|
245
|
-
</script>
|
|
246
|
-
|
|
247
|
-
<template>
|
|
248
|
-
<span ref="host" />
|
|
249
|
-
</template>
|
|
250
|
-
```
|
|
251
|
-
|
|
252
|
-
Use it like any other Vue component:
|
|
253
|
-
|
|
254
|
-
```vue
|
|
255
|
-
<script setup lang="ts">
|
|
256
|
-
import VjButton from "./VjButton.vue";
|
|
257
|
-
|
|
258
|
-
const onSubmit = () => console.log("Submitted");
|
|
259
|
-
</script>
|
|
260
|
-
|
|
261
|
-
<template>
|
|
262
|
-
<VjButton label="Submit" tone="primary" @click="onSubmit" />
|
|
263
|
-
</template>
|
|
264
|
-
```
|
|
265
|
-
|
|
54
|
+
The components are built with React. You can use them directly in a React project. In Angular or Vue, you mount them inside a small wrapper component, as shown below.
|
|
55
|
+
|
|
56
|
+
### React
|
|
57
|
+
|
|
58
|
+
```tsx
|
|
59
|
+
import { useState } from "react";
|
|
60
|
+
import { Button, CheckBox } from "@vjdesigns/vjdesigns-ui";
|
|
61
|
+
|
|
62
|
+
export function SignupForm() {
|
|
63
|
+
const [accepted, setAccepted] = useState(false);
|
|
64
|
+
|
|
65
|
+
return (
|
|
66
|
+
<form>
|
|
67
|
+
<CheckBox
|
|
68
|
+
label="Accept terms"
|
|
69
|
+
checked={accepted}
|
|
70
|
+
onCheckedChange={setAccepted}
|
|
71
|
+
/>
|
|
72
|
+
<Button
|
|
73
|
+
variant="solid"
|
|
74
|
+
tone="primary"
|
|
75
|
+
size="md"
|
|
76
|
+
disabled={!accepted}
|
|
77
|
+
onClick={() => console.log("Submitted")}
|
|
78
|
+
>
|
|
79
|
+
Submit
|
|
80
|
+
</Button>
|
|
81
|
+
</form>
|
|
82
|
+
);
|
|
83
|
+
}
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
### Angular
|
|
87
|
+
|
|
88
|
+
Make a standalone wrapper that renders the React component into its host element. The wrapper uses `createElement`, so you don't need to change your JSX or TSX settings.
|
|
89
|
+
|
|
90
|
+
```ts
|
|
91
|
+
// vj-button.component.ts
|
|
92
|
+
import {
|
|
93
|
+
AfterViewInit,
|
|
94
|
+
Component,
|
|
95
|
+
ElementRef,
|
|
96
|
+
EventEmitter,
|
|
97
|
+
Input,
|
|
98
|
+
OnChanges,
|
|
99
|
+
OnDestroy,
|
|
100
|
+
Output,
|
|
101
|
+
ViewChild,
|
|
102
|
+
} from "@angular/core";
|
|
103
|
+
import { createElement, type MouseEvent as ReactMouseEvent, type ReactElement } from "react";
|
|
104
|
+
import { createRoot, Root } from "react-dom/client";
|
|
105
|
+
import {
|
|
106
|
+
Button,
|
|
107
|
+
type ButtonProps,
|
|
108
|
+
type ButtonVariant,
|
|
109
|
+
type Size,
|
|
110
|
+
type Tone,
|
|
111
|
+
} from "@vjdesigns/vjdesigns-ui";
|
|
112
|
+
|
|
113
|
+
// Button has overloaded types (for `href` and `as`). Narrowing it here lets createElement type-check.
|
|
114
|
+
const ReactButton = Button as (props: ButtonProps) => ReactElement;
|
|
115
|
+
|
|
116
|
+
@Component({
|
|
117
|
+
selector: "vj-button",
|
|
118
|
+
standalone: true,
|
|
119
|
+
template: `<span #host></span>`,
|
|
120
|
+
})
|
|
121
|
+
export class VjButtonComponent implements AfterViewInit, OnChanges, OnDestroy {
|
|
122
|
+
@ViewChild("host", { static: true }) host!: ElementRef<HTMLElement>;
|
|
123
|
+
|
|
124
|
+
@Input() label = "";
|
|
125
|
+
@Input() variant: ButtonVariant = "solid";
|
|
126
|
+
@Input() tone: Tone = "primary";
|
|
127
|
+
@Input() size: Size = "md";
|
|
128
|
+
@Input() disabled = false;
|
|
129
|
+
@Output() clicked = new EventEmitter<MouseEvent>();
|
|
130
|
+
|
|
131
|
+
private root?: Root;
|
|
132
|
+
|
|
133
|
+
ngAfterViewInit() {
|
|
134
|
+
this.root = createRoot(this.host.nativeElement);
|
|
135
|
+
this.render();
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
ngOnChanges() {
|
|
139
|
+
this.render();
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
ngOnDestroy() {
|
|
143
|
+
this.root?.unmount();
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
private render() {
|
|
147
|
+
this.root?.render(
|
|
148
|
+
createElement(
|
|
149
|
+
ReactButton,
|
|
150
|
+
{
|
|
151
|
+
variant: this.variant,
|
|
152
|
+
tone: this.tone,
|
|
153
|
+
size: this.size,
|
|
154
|
+
disabled: this.disabled,
|
|
155
|
+
onClick: (event: ReactMouseEvent) => this.clicked.emit(event.nativeEvent),
|
|
156
|
+
},
|
|
157
|
+
this.label // the button's text is passed as children
|
|
158
|
+
)
|
|
159
|
+
);
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
Import the wrapper wherever you need it:
|
|
165
|
+
|
|
166
|
+
```ts
|
|
167
|
+
// app.component.ts
|
|
168
|
+
import { Component } from "@angular/core";
|
|
169
|
+
import { VjButtonComponent } from "./vj-button.component";
|
|
170
|
+
|
|
171
|
+
@Component({
|
|
172
|
+
selector: "app-root",
|
|
173
|
+
standalone: true,
|
|
174
|
+
imports: [VjButtonComponent],
|
|
175
|
+
template: `<vj-button label="Submit" tone="primary" (clicked)="onSubmit()" />`,
|
|
176
|
+
})
|
|
177
|
+
export class AppComponent {
|
|
178
|
+
onSubmit() {
|
|
179
|
+
console.log("Submitted");
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
```
|
|
183
|
+
|
|
184
|
+
### Vue 3
|
|
185
|
+
|
|
186
|
+
Make a wrapper component that mounts the React component when it loads and re-renders it whenever its props change.
|
|
187
|
+
|
|
188
|
+
```vue
|
|
189
|
+
<!-- VjButton.vue -->
|
|
190
|
+
<script setup lang="ts">
|
|
191
|
+
import { onBeforeUnmount, onMounted, ref, watch } from "vue";
|
|
192
|
+
import { createElement, type MouseEvent as ReactMouseEvent, type ReactElement } from "react";
|
|
193
|
+
import { createRoot, type Root } from "react-dom/client";
|
|
194
|
+
import {
|
|
195
|
+
Button,
|
|
196
|
+
type ButtonProps,
|
|
197
|
+
type ButtonVariant,
|
|
198
|
+
type Size,
|
|
199
|
+
type Tone,
|
|
200
|
+
} from "@vjdesigns/vjdesigns-ui";
|
|
201
|
+
|
|
202
|
+
// Button has overloaded types (for `href` and `as`). Narrowing it here lets createElement type-check.
|
|
203
|
+
const ReactButton = Button as (props: ButtonProps) => ReactElement;
|
|
204
|
+
|
|
205
|
+
const props = withDefaults(
|
|
206
|
+
defineProps<{
|
|
207
|
+
label: string;
|
|
208
|
+
variant?: ButtonVariant;
|
|
209
|
+
tone?: Tone;
|
|
210
|
+
size?: Size;
|
|
211
|
+
disabled?: boolean;
|
|
212
|
+
}>(),
|
|
213
|
+
{ variant: "solid", tone: "primary", size: "md", disabled: false }
|
|
214
|
+
);
|
|
215
|
+
|
|
216
|
+
const emit = defineEmits<{ click: [event: MouseEvent] }>();
|
|
217
|
+
|
|
218
|
+
const host = ref<HTMLElement | null>(null);
|
|
219
|
+
let root: Root | undefined;
|
|
220
|
+
|
|
221
|
+
function render() {
|
|
222
|
+
root?.render(
|
|
223
|
+
createElement(
|
|
224
|
+
ReactButton,
|
|
225
|
+
{
|
|
226
|
+
variant: props.variant,
|
|
227
|
+
tone: props.tone,
|
|
228
|
+
size: props.size,
|
|
229
|
+
disabled: props.disabled,
|
|
230
|
+
onClick: (event: ReactMouseEvent) => emit("click", event.nativeEvent),
|
|
231
|
+
},
|
|
232
|
+
props.label // the button's text is passed as children
|
|
233
|
+
)
|
|
234
|
+
);
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
onMounted(() => {
|
|
238
|
+
root = createRoot(host.value!);
|
|
239
|
+
render();
|
|
240
|
+
});
|
|
241
|
+
|
|
242
|
+
watch(() => ({ ...props }), render);
|
|
243
|
+
|
|
244
|
+
onBeforeUnmount(() => root?.unmount());
|
|
245
|
+
</script>
|
|
246
|
+
|
|
247
|
+
<template>
|
|
248
|
+
<span ref="host" />
|
|
249
|
+
</template>
|
|
250
|
+
```
|
|
251
|
+
|
|
252
|
+
Use it like any other Vue component:
|
|
253
|
+
|
|
254
|
+
```vue
|
|
255
|
+
<script setup lang="ts">
|
|
256
|
+
import VjButton from "./VjButton.vue";
|
|
257
|
+
|
|
258
|
+
const onSubmit = () => console.log("Submitted");
|
|
259
|
+
</script>
|
|
260
|
+
|
|
261
|
+
<template>
|
|
262
|
+
<VjButton label="Submit" tone="primary" @click="onSubmit" />
|
|
263
|
+
</template>
|
|
264
|
+
```
|
|
265
|
+
|
|
266
266
|
> **Tip:** Wrap `CheckBox` and any other component the same way. Pass its props into `createElement`, and send its callbacks back out as Angular outputs or Vue emits. For `CheckBox`, that means sending `onCheckedChange` back out.
|
|
267
267
|
|
|
268
268
|
# Guide: Theming
|
|
269
269
|
|
|
270
|
-
Wrap any part of your app in `Theme` to set the color mode, brand palette, and accent for everything inside it:
|
|
271
|
-
|
|
272
|
-
```tsx
|
|
273
|
-
import { Theme, Button } from "@vjdesigns/vjdesigns-ui";
|
|
274
|
-
|
|
275
|
-
<Theme mode="dark" brand="purple" fill>
|
|
276
|
-
<Button>Dark, purple button</Button>
|
|
277
|
-
</Theme>
|
|
278
|
-
```
|
|
279
|
-
|
|
280
|
-
| Prop | Type | Default | Description |
|
|
281
|
-
| --- | --- | --- | --- |
|
|
282
|
-
| `mode` | `"light" \| "dark"` | inherited | Color mode for everything inside. |
|
|
283
|
-
| `brand` | `"black" \| "blue" \| "gold" \| "green" \| "purple"`, a hex color, or `{ light, dark }` | inherited | Brand palette for buttons, links, focus rings and indicators. |
|
|
284
|
-
| `accent` | `"brand" | "lime"`, a hex color, or `{ light, dark }` | inherited (the brand's own) | The bright highlight on badges, switches, progress bars, sliders, steppers, and active navigation. |
|
|
285
|
-
| `fill` | `boolean` | `false` | Paints the theme's background and text color on the wrapper. |
|
|
286
|
-
|
|
287
|
-
Outside React (or to theme the whole page), set the attributes on `<html>` instead:
|
|
288
|
-
|
|
289
|
-
```html
|
|
290
|
-
<html data-theme="dark" data-brand="purple" data-accent="lime">
|
|
291
|
-
```
|
|
292
|
-
|
|
293
|
-
### Accent color
|
|
294
|
-
|
|
295
|
-
The accent is the energetic highlight: badges, switches, progress bars, sliders, steppers, and the active link in the Navbar. By default it follows the brand, as a bright shade of the brand's own color, so it always fits:
|
|
296
|
-
|
|
297
|
-
| Brand | Default accent |
|
|
298
|
-
| --- | --- |
|
|
299
|
-
| `blue` (default) | sky blue `#76c7ff` |
|
|
300
|
-
| `purple` | lavender `#c0b0ff` |
|
|
301
|
-
| `gold` | amber `#f5ad5f` |
|
|
302
|
-
| `green` | mint `#72d4ad` |
|
|
303
|
-
| `black` | lime `#c8f031` (black has no hue to build on) |
|
|
304
|
-
| a custom hex | generated from that color |
|
|
305
|
-
|
|
306
|
-
To choose a different one:
|
|
307
|
-
|
|
308
|
-
```tsx
|
|
309
|
-
<Theme accent="lime">…</Theme> // the original bright yellow-green ("volt", its former name, still works)
|
|
310
|
-
<Theme accent="#ff6b00">…</Theme> // any color
|
|
311
|
-
<Theme accent="brand">…</Theme> // back to the brand's own, inside an area with another accent
|
|
312
|
-
```
|
|
313
|
-
|
|
314
|
-
Accents always carry dark text. A color too dark for that is lightened (same hue) just enough to meet WCAG AA, with a warning in development. An accent you set also holds inside nested brands. In CSS, `accentCss("name", "#ff6b00")` generates a `[data-accent="name"]` rule, like `brandCss` below.
|
|
315
|
-
|
|
316
|
-
### Custom brand colors
|
|
317
|
-
|
|
318
|
-
Pass any hex color as `brand` and a full palette is generated for both modes:
|
|
319
|
-
|
|
320
|
-
```tsx
|
|
321
|
-
<Theme brand="#e11d48">…</Theme>
|
|
322
|
-
<Theme brand={{ light: "#4338ca", dark: "#f472b6" }}>…</Theme>
|
|
323
|
-
```
|
|
324
|
-
|
|
325
|
-
Every generated palette meets WCAG 2.2 AA. A single color is used exactly as given in the mode where it has enough contrast (dark colors in light mode, light colors in dark mode), and the other mode gets a matching shade of the same hue. If a color doesn't have enough contrast (for example, a mid-tone that fails in both modes), its lightness is shifted just enough to pass, and a warning is logged in development.
|
|
326
|
-
|
|
327
|
-
To define a brand in CSS instead (for the whole page, applied before any JavaScript runs), generate a stylesheet rule with `brandCss`:
|
|
328
|
-
|
|
329
|
-
```ts
|
|
330
|
-
import { brandCss } from "@vjdesigns/vjdesigns-ui";
|
|
331
|
-
|
|
332
|
-
brandCss("acme", "#e11d48"); // '[data-brand="acme"] { --brand-light-primary: … }'
|
|
333
|
-
```
|
|
334
|
-
|
|
270
|
+
Wrap any part of your app in `Theme` to set the color mode, brand palette, and accent for everything inside it:
|
|
271
|
+
|
|
272
|
+
```tsx
|
|
273
|
+
import { Theme, Button } from "@vjdesigns/vjdesigns-ui";
|
|
274
|
+
|
|
275
|
+
<Theme mode="dark" brand="purple" fill>
|
|
276
|
+
<Button>Dark, purple button</Button>
|
|
277
|
+
</Theme>
|
|
278
|
+
```
|
|
279
|
+
|
|
280
|
+
| Prop | Type | Default | Description |
|
|
281
|
+
| --- | --- | --- | --- |
|
|
282
|
+
| `mode` | `"light" \| "dark"` | inherited | Color mode for everything inside. |
|
|
283
|
+
| `brand` | `"black" \| "blue" \| "gold" \| "green" \| "purple"`, a hex color, or `{ light, dark }` | inherited | Brand palette for buttons, links, focus rings and indicators. |
|
|
284
|
+
| `accent` | `"brand" | "lime"`, a hex color, or `{ light, dark }` | inherited (the brand's own) | The bright highlight on badges, switches, progress bars, sliders, steppers, and active navigation. |
|
|
285
|
+
| `fill` | `boolean` | `false` | Paints the theme's background and text color on the wrapper. |
|
|
286
|
+
|
|
287
|
+
Outside React (or to theme the whole page), set the attributes on `<html>` instead:
|
|
288
|
+
|
|
289
|
+
```html
|
|
290
|
+
<html data-theme="dark" data-brand="purple" data-accent="lime">
|
|
291
|
+
```
|
|
292
|
+
|
|
293
|
+
### Accent color
|
|
294
|
+
|
|
295
|
+
The accent is the energetic highlight: badges, switches, progress bars, sliders, steppers, and the active link in the Navbar. By default it follows the brand, as a bright shade of the brand's own color, so it always fits:
|
|
296
|
+
|
|
297
|
+
| Brand | Default accent |
|
|
298
|
+
| --- | --- |
|
|
299
|
+
| `blue` (default) | sky blue `#76c7ff` |
|
|
300
|
+
| `purple` | lavender `#c0b0ff` |
|
|
301
|
+
| `gold` | amber `#f5ad5f` |
|
|
302
|
+
| `green` | mint `#72d4ad` |
|
|
303
|
+
| `black` | lime `#c8f031` (black has no hue to build on) |
|
|
304
|
+
| a custom hex | generated from that color |
|
|
305
|
+
|
|
306
|
+
To choose a different one:
|
|
307
|
+
|
|
308
|
+
```tsx
|
|
309
|
+
<Theme accent="lime">…</Theme> // the original bright yellow-green ("volt", its former name, still works)
|
|
310
|
+
<Theme accent="#ff6b00">…</Theme> // any color
|
|
311
|
+
<Theme accent="brand">…</Theme> // back to the brand's own, inside an area with another accent
|
|
312
|
+
```
|
|
313
|
+
|
|
314
|
+
Accents always carry dark text. A color too dark for that is lightened (same hue) just enough to meet WCAG AA, with a warning in development. An accent you set also holds inside nested brands. In CSS, `accentCss("name", "#ff6b00")` generates a `[data-accent="name"]` rule, like `brandCss` below.
|
|
315
|
+
|
|
316
|
+
### Custom brand colors
|
|
317
|
+
|
|
318
|
+
Pass any hex color as `brand` and a full palette is generated for both modes:
|
|
319
|
+
|
|
320
|
+
```tsx
|
|
321
|
+
<Theme brand="#e11d48">…</Theme>
|
|
322
|
+
<Theme brand={{ light: "#4338ca", dark: "#f472b6" }}>…</Theme>
|
|
323
|
+
```
|
|
324
|
+
|
|
325
|
+
Every generated palette meets WCAG 2.2 AA. A single color is used exactly as given in the mode where it has enough contrast (dark colors in light mode, light colors in dark mode), and the other mode gets a matching shade of the same hue. If a color doesn't have enough contrast (for example, a mid-tone that fails in both modes), its lightness is shifted just enough to pass, and a warning is logged in development.
|
|
326
|
+
|
|
327
|
+
To define a brand in CSS instead (for the whole page, applied before any JavaScript runs), generate a stylesheet rule with `brandCss`:
|
|
328
|
+
|
|
329
|
+
```ts
|
|
330
|
+
import { brandCss } from "@vjdesigns/vjdesigns-ui";
|
|
331
|
+
|
|
332
|
+
brandCss("acme", "#e11d48"); // '[data-brand="acme"] { --brand-light-primary: … }'
|
|
333
|
+
```
|
|
334
|
+
|
|
335
335
|
Add the rule to your stylesheet and use `<html data-brand="acme">`. `createBrandPalette(color)` returns the generated colors as an object, if you need them directly.
|
|
336
336
|
|
|
337
337
|
# Guide: Languages
|
|
338
338
|
|
|
339
|
-
Components have built-in text: button names like "Close" and "Next page", screen-reader announcements like "3 results available", validation messages, and date formats. Wrap your app in `LocaleProvider` to show all of it in another language:
|
|
340
|
-
|
|
341
|
-
```tsx
|
|
342
|
-
import { LocaleProvider } from "@vjdesigns/vjdesigns-ui";
|
|
343
|
-
import es from "@vjdesigns/vjdesigns-ui/locales/es";
|
|
344
|
-
|
|
345
|
-
<LocaleProvider locale="es" messages={es}>
|
|
346
|
-
<App />
|
|
347
|
-
</LocaleProvider>
|
|
348
|
-
```
|
|
349
|
-
|
|
350
|
-
`LocaleProvider` sets:
|
|
351
|
-
|
|
352
|
-
- **Built-in text** for every component, from `messages`.
|
|
353
|
-
- **Dates and numbers** in the locale's format (`23/10/2026`, `1.234,5`), and plurals that follow the language's rules.
|
|
354
|
-
- **Writing direction**: Arabic, Hebrew, Persian, and Urdu get `dir="rtl"`, and layouts and arrow keys mirror.
|
|
355
|
-
- **`lang`** on its wrapper, so screen readers pronounce the text correctly. Popups (menus, dialogs, tooltips) keep the language of the area that opened them.
|
|
356
|
-
|
|
357
|
-
Each locale is a separate import, so your app only downloads the languages it uses. Without a `LocaleProvider`, components use English text and the browser's date and number format.
|
|
358
|
-
|
|
359
|
-
This library only translates its own built-in text. Text your app passes in, like `<Button>Save</Button>`, is your app's content: translate it with your app's i18n library (react-i18next, FormatJS, and so on).
|
|
360
|
-
|
|
361
|
-
### What gets translated
|
|
362
|
-
|
|
363
|
-
The language is never picked automatically from where the user lives or their browser settings. Your app chooses it by rendering a `LocaleProvider`. For example, with `<LocaleProvider locale="de" messages={de}>`:
|
|
364
|
-
|
|
365
|
-
| | Translated? |
|
|
366
|
-
| --- | --- |
|
|
367
|
-
| Text your app passes in, like `<Button>Save</Button>` | No: your app translates it |
|
|
368
|
-
| Built-in names, like "Close", "Next page", "Clear search" | Yes: "Schließen", "Nächste Seite", "Suche löschen" |
|
|
369
|
-
| Screen-reader announcements, like "3 results available" | Yes: "3 Ergebnisse verfügbar" |
|
|
370
|
-
| Validation messages, empty states, file-upload prompts | Yes |
|
|
371
|
-
| Dates and numbers | Yes: "23.10.2026", "4,5" |
|
|
372
|
-
|
|
373
|
-
To follow the user's browser language, pass it yourself: `locale={navigator.language}` together with the matching `messages` (see [Switching language at runtime](#switching-language-at-runtime)).
|
|
374
|
-
|
|
375
|
-
### Bundled languages
|
|
376
|
-
|
|
377
|
-
`ar` Arabic · `bn` Bengali · `cs` Czech · `da` Danish · `de` German · `el` Greek · `es` Spanish · `fa` Persian · `fi` Finnish · `fr` French · `he` Hebrew · `hi` Hindi · `hu` Hungarian · `id` Indonesian · `it` Italian · `ja` Japanese · `ko` Korean · `nb` Norwegian Bokmål · `nl` Dutch · `pl` Polish · `pt-BR` Portuguese (Brazil) · `ro` Romanian · `ru` Russian · `sv` Swedish · `th` Thai · `tr` Turkish · `uk` Ukrainian · `ur` Urdu · `vi` Vietnamese · `zh-Hans` Chinese (Simplified) · `zh-Hant` Chinese (Traditional), plus English (the default).
|
|
378
|
-
|
|
379
|
-
The translations are machine-generated and haven't been reviewed by native speakers yet. If you find a wrong or awkward phrase, override it (below) and report it so the bundled locale can be fixed.
|
|
380
|
-
|
|
381
|
-
### Changing a few strings
|
|
382
|
-
|
|
383
|
-
Pass only the strings you want to change. Everything else falls back to the outer provider's language (English at the top):
|
|
384
|
-
|
|
385
|
-
```tsx
|
|
386
|
-
<LocaleProvider locale="en-GB" messages={{ pagination: { next: "Onwards" } }}>
|
|
387
|
-
```
|
|
388
|
-
|
|
389
|
-
Providers nest, so an inner provider can switch the language for one part of the page. Per-component props such as `clearLabel` or `labels` still win over the provider.
|
|
390
|
-
|
|
391
|
-
### Other languages
|
|
392
|
-
|
|
393
|
-
For a language that isn't bundled, write a complete messages object. TypeScript flags any missing string. Use English as the template:
|
|
394
|
-
|
|
395
|
-
```ts
|
|
396
|
-
import { en, plural, type Messages } from "@vjdesigns/vjdesigns-ui";
|
|
397
|
-
|
|
398
|
-
const p = plural("sw");
|
|
399
|
-
export const sw: Messages = {
|
|
400
|
-
...en,
|
|
401
|
-
pagination: { ...en.pagination, next: "Ukurasa unaofuata", previous: "Ukurasa uliopita" },
|
|
402
|
-
searchInput: {
|
|
403
|
-
...en.searchInput,
|
|
404
|
-
results: (count) => p(count, { "=0": "Hakuna matokeo", one: "Tokeo #", other: "Matokeo #" }),
|
|
405
|
-
},
|
|
406
|
-
// ...
|
|
407
|
-
};
|
|
408
|
-
```
|
|
409
|
-
|
|
410
|
-
`plural(locale)` picks the right plural form for any language (`#` is the formatted number).
|
|
411
|
-
|
|
412
|
-
### Switching language at runtime
|
|
413
|
-
|
|
414
|
-
List the languages your app offers as loaders, so each one is downloaded only when picked:
|
|
415
|
-
|
|
416
|
-
```tsx
|
|
417
|
-
import { LocaleProvider, type Messages } from "@vjdesigns/vjdesigns-ui";
|
|
418
|
-
|
|
419
|
-
const loaders: Record<string, () => Promise<{ default: Messages }>> = {
|
|
420
|
-
es: () => import("@vjdesigns/vjdesigns-ui/locales/es"),
|
|
421
|
-
fr: () => import("@vjdesigns/vjdesigns-ui/locales/fr"),
|
|
422
|
-
ar: () => import("@vjdesigns/vjdesigns-ui/locales/ar"),
|
|
423
|
-
};
|
|
424
|
-
|
|
425
|
-
function App({ lang }: { lang: string }) {
|
|
426
|
-
const [messages, setMessages] = useState<Messages>();
|
|
427
|
-
useEffect(() => {
|
|
428
|
-
const load = loaders[lang];
|
|
429
|
-
if (load) load().then((mod) => setMessages(mod.default));
|
|
430
|
-
else setMessages(undefined); // English
|
|
431
|
-
}, [lang]);
|
|
432
|
-
// Until a language loads, the previous one (or English) shows.
|
|
433
|
-
return <LocaleProvider locale={lang} messages={messages}>…</LocaleProvider>;
|
|
434
|
-
}
|
|
435
|
-
```
|
|
436
|
-
|
|
339
|
+
Components have built-in text: button names like "Close" and "Next page", screen-reader announcements like "3 results available", validation messages, and date formats. Wrap your app in `LocaleProvider` to show all of it in another language:
|
|
340
|
+
|
|
341
|
+
```tsx
|
|
342
|
+
import { LocaleProvider } from "@vjdesigns/vjdesigns-ui";
|
|
343
|
+
import es from "@vjdesigns/vjdesigns-ui/locales/es";
|
|
344
|
+
|
|
345
|
+
<LocaleProvider locale="es" messages={es}>
|
|
346
|
+
<App />
|
|
347
|
+
</LocaleProvider>
|
|
348
|
+
```
|
|
349
|
+
|
|
350
|
+
`LocaleProvider` sets:
|
|
351
|
+
|
|
352
|
+
- **Built-in text** for every component, from `messages`.
|
|
353
|
+
- **Dates and numbers** in the locale's format (`23/10/2026`, `1.234,5`), and plurals that follow the language's rules.
|
|
354
|
+
- **Writing direction**: Arabic, Hebrew, Persian, and Urdu get `dir="rtl"`, and layouts and arrow keys mirror.
|
|
355
|
+
- **`lang`** on its wrapper, so screen readers pronounce the text correctly. Popups (menus, dialogs, tooltips) keep the language of the area that opened them.
|
|
356
|
+
|
|
357
|
+
Each locale is a separate import, so your app only downloads the languages it uses. Without a `LocaleProvider`, components use English text and the browser's date and number format.
|
|
358
|
+
|
|
359
|
+
This library only translates its own built-in text. Text your app passes in, like `<Button>Save</Button>`, is your app's content: translate it with your app's i18n library (react-i18next, FormatJS, and so on).
|
|
360
|
+
|
|
361
|
+
### What gets translated
|
|
362
|
+
|
|
363
|
+
The language is never picked automatically from where the user lives or their browser settings. Your app chooses it by rendering a `LocaleProvider`. For example, with `<LocaleProvider locale="de" messages={de}>`:
|
|
364
|
+
|
|
365
|
+
| | Translated? |
|
|
366
|
+
| --- | --- |
|
|
367
|
+
| Text your app passes in, like `<Button>Save</Button>` | No: your app translates it |
|
|
368
|
+
| Built-in names, like "Close", "Next page", "Clear search" | Yes: "Schließen", "Nächste Seite", "Suche löschen" |
|
|
369
|
+
| Screen-reader announcements, like "3 results available" | Yes: "3 Ergebnisse verfügbar" |
|
|
370
|
+
| Validation messages, empty states, file-upload prompts | Yes |
|
|
371
|
+
| Dates and numbers | Yes: "23.10.2026", "4,5" |
|
|
372
|
+
|
|
373
|
+
To follow the user's browser language, pass it yourself: `locale={navigator.language}` together with the matching `messages` (see [Switching language at runtime](#switching-language-at-runtime)).
|
|
374
|
+
|
|
375
|
+
### Bundled languages
|
|
376
|
+
|
|
377
|
+
`ar` Arabic · `bn` Bengali · `cs` Czech · `da` Danish · `de` German · `el` Greek · `es` Spanish · `fa` Persian · `fi` Finnish · `fr` French · `he` Hebrew · `hi` Hindi · `hu` Hungarian · `id` Indonesian · `it` Italian · `ja` Japanese · `ko` Korean · `nb` Norwegian Bokmål · `nl` Dutch · `pl` Polish · `pt-BR` Portuguese (Brazil) · `ro` Romanian · `ru` Russian · `sv` Swedish · `th` Thai · `tr` Turkish · `uk` Ukrainian · `ur` Urdu · `vi` Vietnamese · `zh-Hans` Chinese (Simplified) · `zh-Hant` Chinese (Traditional), plus English (the default).
|
|
378
|
+
|
|
379
|
+
The translations are machine-generated and haven't been reviewed by native speakers yet. If you find a wrong or awkward phrase, override it (below) and report it so the bundled locale can be fixed. (Working on the library itself? See [Translating built-in text](#translating-built-in-text).)
|
|
380
|
+
|
|
381
|
+
### Changing a few strings
|
|
382
|
+
|
|
383
|
+
Pass only the strings you want to change. Everything else falls back to the outer provider's language (English at the top):
|
|
384
|
+
|
|
385
|
+
```tsx
|
|
386
|
+
<LocaleProvider locale="en-GB" messages={{ pagination: { next: "Onwards" } }}>
|
|
387
|
+
```
|
|
388
|
+
|
|
389
|
+
Providers nest, so an inner provider can switch the language for one part of the page. Per-component props such as `clearLabel` or `labels` still win over the provider.
|
|
390
|
+
|
|
391
|
+
### Other languages
|
|
392
|
+
|
|
393
|
+
For a language that isn't bundled, write a complete messages object. TypeScript flags any missing string. Use English as the template:
|
|
394
|
+
|
|
395
|
+
```ts
|
|
396
|
+
import { en, plural, type Messages } from "@vjdesigns/vjdesigns-ui";
|
|
397
|
+
|
|
398
|
+
const p = plural("sw");
|
|
399
|
+
export const sw: Messages = {
|
|
400
|
+
...en,
|
|
401
|
+
pagination: { ...en.pagination, next: "Ukurasa unaofuata", previous: "Ukurasa uliopita" },
|
|
402
|
+
searchInput: {
|
|
403
|
+
...en.searchInput,
|
|
404
|
+
results: (count) => p(count, { "=0": "Hakuna matokeo", one: "Tokeo #", other: "Matokeo #" }),
|
|
405
|
+
},
|
|
406
|
+
// ...
|
|
407
|
+
};
|
|
408
|
+
```
|
|
409
|
+
|
|
410
|
+
`plural(locale)` picks the right plural form for any language (`#` is the formatted number).
|
|
411
|
+
|
|
412
|
+
### Switching language at runtime
|
|
413
|
+
|
|
414
|
+
List the languages your app offers as loaders, so each one is downloaded only when picked:
|
|
415
|
+
|
|
416
|
+
```tsx
|
|
417
|
+
import { LocaleProvider, type Messages } from "@vjdesigns/vjdesigns-ui";
|
|
418
|
+
|
|
419
|
+
const loaders: Record<string, () => Promise<{ default: Messages }>> = {
|
|
420
|
+
es: () => import("@vjdesigns/vjdesigns-ui/locales/es"),
|
|
421
|
+
fr: () => import("@vjdesigns/vjdesigns-ui/locales/fr"),
|
|
422
|
+
ar: () => import("@vjdesigns/vjdesigns-ui/locales/ar"),
|
|
423
|
+
};
|
|
424
|
+
|
|
425
|
+
function App({ lang }: { lang: string }) {
|
|
426
|
+
const [messages, setMessages] = useState<Messages>();
|
|
427
|
+
useEffect(() => {
|
|
428
|
+
const load = loaders[lang];
|
|
429
|
+
if (load) load().then((mod) => setMessages(mod.default));
|
|
430
|
+
else setMessages(undefined); // English
|
|
431
|
+
}, [lang]);
|
|
432
|
+
// Until a language loads, the previous one (or English) shows.
|
|
433
|
+
return <LocaleProvider locale={lang} messages={messages}>…</LocaleProvider>;
|
|
434
|
+
}
|
|
435
|
+
```
|
|
436
|
+
|
|
437
437
|
For server rendering, also set `<html lang="…" dir="…">` on the page. `getLocaleDirection(locale)` returns `"ltr"` or `"rtl"`.
|
|
438
438
|
|
|
439
439
|
# Guide: Form validation
|
|
440
440
|
|
|
441
|
-
> **Front-end validation is not security.** It helps people fill in a form correctly, but anyone can turn off JavaScript or send requests straight to your server. Always validate again on the server. The format checks below (`isEmail`, `isIP`, …) are plain functions you can reuse there.
|
|
442
|
-
|
|
443
|
-
`useFormValidation` checks a form when it's submitted, using the browser's own rules on each input (`required`, `type="email"`, `minLength`, `pattern`, `min` / `max`) plus any rules you add. Messages follow the current language.
|
|
444
|
-
|
|
445
|
-
```tsx
|
|
446
|
-
import {
|
|
447
|
-
useFormValidation, rules, ErrorSummary, Field, Input, PasswordInput, Button,
|
|
448
|
-
} from "@vjdesigns/vjdesigns-ui";
|
|
449
|
-
|
|
450
|
-
function SignUp() {
|
|
451
|
-
const form = useFormValidation({
|
|
452
|
-
rules: {
|
|
453
|
-
confirm: rules.matches("password", "The passwords don't match."),
|
|
454
|
-
serverIp: rules.ip(), // IPv4 or IPv6
|
|
455
|
-
},
|
|
456
|
-
// Runs only when everything is valid. Return the server's errors to show them on their fields.
|
|
457
|
-
onValid: async (values) => {
|
|
458
|
-
const response = await api.signUp(values);
|
|
459
|
-
return response.errors; // e.g. { username: "That username is taken." }
|
|
460
|
-
},
|
|
461
|
-
});
|
|
462
|
-
|
|
463
|
-
return (
|
|
464
|
-
<form {...form.formProps}>
|
|
465
|
-
<ErrorSummary ref={form.summaryRef} errors={form.errorList} />
|
|
466
|
-
<Field label="Email" required error={form.errors.email}>
|
|
467
|
-
<Input name="email" type="email" />
|
|
468
|
-
</Field>
|
|
469
|
-
<Field label="Password" required error={form.errors.password}>
|
|
470
|
-
<PasswordInput name="password" minLength={12} />
|
|
471
|
-
</Field>
|
|
472
|
-
<Field label="Confirm password" required error={form.errors.confirm}>
|
|
473
|
-
<PasswordInput name="confirm" />
|
|
474
|
-
</Field>
|
|
475
|
-
<Field label="Server IP" error={form.errors.serverIp}>
|
|
476
|
-
<Input name="serverIp" />
|
|
477
|
-
</Field>
|
|
478
|
-
<Button type="submit" loading={form.isSubmitting}>Create account</Button>
|
|
479
|
-
</form>
|
|
480
|
-
);
|
|
481
|
-
}
|
|
482
|
-
```
|
|
483
|
-
|
|
484
|
-
How it behaves:
|
|
485
|
-
|
|
486
|
-
- **On submit**, every field is checked. If anything is wrong, the `ErrorSummary` at the top lists the problems and takes focus, so screen readers announce it. Each item links to its field. Without a summary, focus goes to the first invalid field.
|
|
487
|
-
- **Errors are keyed by the field's `name`.** Pass `form.errors.<name>` to each `Field`'s `error`.
|
|
488
|
-
- **After a failed submit**, an error disappears as soon as the field is fixed (while typing), and a field is fully re-checked when you leave it. It doesn't nag while you're still typing.
|
|
489
|
-
- **Server errors** (returned from `onValid`, or set with `form.setErrors({...})`) show on their fields and stay until that field is edited.
|
|
490
|
-
- **Emails** must have a real domain (`name@example.com`). The browser's own check also accepts `name@localhost`.
|
|
491
|
-
|
|
492
|
-
### Rules and format checks
|
|
493
|
-
|
|
494
|
-
| Rule | Checks |
|
|
495
|
-
| --- | --- |
|
|
496
|
-
| `rules.required()` | Not empty (for controls without a native `required`) |
|
|
497
|
-
| `rules.email()` | An email address in the usual format, with a top-level domain |
|
|
498
|
-
| `rules.ip()` / `rules.ip({ version: 4 })` / `rules.ip({ version: 6 })` | An IPv4 or IPv6 address |
|
|
499
|
-
| `rules.minLength(n)` / `rules.maxLength(n)` | Length in characters |
|
|
500
|
-
| `rules.pattern(/regex/, "message")` | The whole value matches |
|
|
501
|
-
| `rules.matches("otherField")` | Equals another field (confirm password) |
|
|
502
|
-
| your own `(value, { values }) => message` | Anything else |
|
|
503
|
-
|
|
504
|
-
Each rule takes an optional custom message. The checks behind them are exported for use anywhere, including your server:
|
|
505
|
-
|
|
506
|
-
- `isEmail(value)`
|
|
507
|
-
- `isIPv4(value)`: four numbers 0–255, no leading zeros
|
|
508
|
-
- `isIPv6(value)`: including `::` shorthand and IPv4 tails like `::ffff:192.168.0.1`
|
|
441
|
+
> **Front-end validation is not security.** It helps people fill in a form correctly, but anyone can turn off JavaScript or send requests straight to your server. Always validate again on the server. The format checks below (`isEmail`, `isIP`, …) are plain functions you can reuse there.
|
|
442
|
+
|
|
443
|
+
`useFormValidation` checks a form when it's submitted, using the browser's own rules on each input (`required`, `type="email"`, `minLength`, `pattern`, `min` / `max`) plus any rules you add. Messages follow the current language.
|
|
444
|
+
|
|
445
|
+
```tsx
|
|
446
|
+
import {
|
|
447
|
+
useFormValidation, rules, ErrorSummary, Field, Input, PasswordInput, Button,
|
|
448
|
+
} from "@vjdesigns/vjdesigns-ui";
|
|
449
|
+
|
|
450
|
+
function SignUp() {
|
|
451
|
+
const form = useFormValidation({
|
|
452
|
+
rules: {
|
|
453
|
+
confirm: rules.matches("password", "The passwords don't match."),
|
|
454
|
+
serverIp: rules.ip(), // IPv4 or IPv6
|
|
455
|
+
},
|
|
456
|
+
// Runs only when everything is valid. Return the server's errors to show them on their fields.
|
|
457
|
+
onValid: async (values) => {
|
|
458
|
+
const response = await api.signUp(values);
|
|
459
|
+
return response.errors; // e.g. { username: "That username is taken." }
|
|
460
|
+
},
|
|
461
|
+
});
|
|
462
|
+
|
|
463
|
+
return (
|
|
464
|
+
<form {...form.formProps}>
|
|
465
|
+
<ErrorSummary ref={form.summaryRef} errors={form.errorList} />
|
|
466
|
+
<Field label="Email" required error={form.errors.email}>
|
|
467
|
+
<Input name="email" type="email" />
|
|
468
|
+
</Field>
|
|
469
|
+
<Field label="Password" required error={form.errors.password}>
|
|
470
|
+
<PasswordInput name="password" minLength={12} />
|
|
471
|
+
</Field>
|
|
472
|
+
<Field label="Confirm password" required error={form.errors.confirm}>
|
|
473
|
+
<PasswordInput name="confirm" />
|
|
474
|
+
</Field>
|
|
475
|
+
<Field label="Server IP" error={form.errors.serverIp}>
|
|
476
|
+
<Input name="serverIp" />
|
|
477
|
+
</Field>
|
|
478
|
+
<Button type="submit" loading={form.isSubmitting}>Create account</Button>
|
|
479
|
+
</form>
|
|
480
|
+
);
|
|
481
|
+
}
|
|
482
|
+
```
|
|
483
|
+
|
|
484
|
+
How it behaves:
|
|
485
|
+
|
|
486
|
+
- **On submit**, every field is checked. If anything is wrong, the `ErrorSummary` at the top lists the problems and takes focus, so screen readers announce it. Each item links to its field. Without a summary, focus goes to the first invalid field.
|
|
487
|
+
- **Errors are keyed by the field's `name`.** Pass `form.errors.<name>` to each `Field`'s `error`.
|
|
488
|
+
- **After a failed submit**, an error disappears as soon as the field is fixed (while typing), and a field is fully re-checked when you leave it. It doesn't nag while you're still typing.
|
|
489
|
+
- **Server errors** (returned from `onValid`, or set with `form.setErrors({...})`) show on their fields and stay until that field is edited.
|
|
490
|
+
- **Emails** must have a real domain (`name@example.com`). The browser's own check also accepts `name@localhost`.
|
|
491
|
+
|
|
492
|
+
### Rules and format checks
|
|
493
|
+
|
|
494
|
+
| Rule | Checks |
|
|
495
|
+
| --- | --- |
|
|
496
|
+
| `rules.required()` | Not empty (for controls without a native `required`) |
|
|
497
|
+
| `rules.email()` | An email address in the usual format, with a top-level domain |
|
|
498
|
+
| `rules.ip()` / `rules.ip({ version: 4 })` / `rules.ip({ version: 6 })` | An IPv4 or IPv6 address |
|
|
499
|
+
| `rules.minLength(n)` / `rules.maxLength(n)` | Length in characters |
|
|
500
|
+
| `rules.pattern(/regex/, "message")` | The whole value matches |
|
|
501
|
+
| `rules.matches("otherField")` | Equals another field (confirm password) |
|
|
502
|
+
| your own `(value, { values }) => message` | Anything else |
|
|
503
|
+
|
|
504
|
+
Each rule takes an optional custom message. The checks behind them are exported for use anywhere, including your server:
|
|
505
|
+
|
|
506
|
+
- `isEmail(value)`
|
|
507
|
+
- `isIPv4(value)`: four numbers 0–255, no leading zeros
|
|
508
|
+
- `isIPv6(value)`: including `::` shorthand and IPv4 tails like `::ffff:192.168.0.1`
|
|
509
509
|
- `isIP(value, version?)`
|
|
510
510
|
|
|
511
511
|
# Components
|
|
@@ -927,6 +927,77 @@ view, so keyboard and screen reader users only reach what is shown.
|
|
|
927
927
|
- Controlled Edge Cases: Controlled: the viewport only moves when the parent accepts the new index. A parent that ignores `onIndexChange` keeps the slide (and dots) in place, and an out-of-range `index` is reported back clamped.
|
|
928
928
|
- Resize Keeps Slide: After a resize the same slide stays in view (slide widths change, so scroll is realigned).
|
|
929
929
|
|
|
930
|
+
## Chat
|
|
931
|
+
|
|
932
|
+
import { Chat } from "@vjdesigns/vjdesigns-ui";
|
|
933
|
+
|
|
934
|
+
Category: data-display · Storybook: Data Display/Chat
|
|
935
|
+
|
|
936
|
+
A chat interface for an AI assistant (or a person), with no backend of its
|
|
937
|
+
own: `onSend` connects it to any API and can stream the reply. Includes a
|
|
938
|
+
scrolling conversation that follows new messages, an auto-growing message
|
|
939
|
+
box (Enter sends, Shift+Enter adds a line), Stop and Retry, copy buttons,
|
|
940
|
+
suggested prompts, and screen reader announcements of each reply.
|
|
941
|
+
|
|
942
|
+
```tsx
|
|
943
|
+
<Chat
|
|
944
|
+
style={{ blockSize: 560 }}
|
|
945
|
+
onSend={async function* (text, { messages, signal }) {
|
|
946
|
+
const res = await fetch("/api/chat", {
|
|
947
|
+
method: "POST",
|
|
948
|
+
body: JSON.stringify({ messages }),
|
|
949
|
+
signal,
|
|
950
|
+
});
|
|
951
|
+
yield* readTextStream(res);
|
|
952
|
+
}}
|
|
953
|
+
/>
|
|
954
|
+
```
|
|
955
|
+
|
|
956
|
+
### Props
|
|
957
|
+
|
|
958
|
+
- `messages`: `ChatMessage[]`. Controlled conversation.
|
|
959
|
+
- `defaultMessages`: `ChatMessage[]`. Initial conversation when uncontrolled.
|
|
960
|
+
- `onMessagesChange`: `(messages: ChatMessage[]) => void`. Called with the whole conversation whenever it changes (new messages, every streamed chunk, status changes).
|
|
961
|
+
- `onSend`: `( text: string, context: ChatSendContext, ) => ChatSendResult | Promise<ChatSendResult>`. Connects the Chat to your backend. Called with the user's text when they send a message (or retry). Return the reply as a string, stream it as an `AsyncIterable<string>`, or return nothing and add it yourself through controlled `messages`. Throwing (or rejecting) turns the reply into an error with a Retry button.
|
|
962
|
+
- `onStop`: `() => void`. Called when the user stops a reply, after its `signal` is aborted.
|
|
963
|
+
- `onError`: `(error: unknown) => void`. Called with whatever `onSend` threw, for logging. The user sees `labels.error` (or the message's `error`).
|
|
964
|
+
- `draft`: `string`. Controlled text of the message box.
|
|
965
|
+
- `defaultDraft`: `string`. Initial text of the message box when uncontrolled.
|
|
966
|
+
- `onDraftChange`: `(draft: string) => void`. Called with the message box text on every edit.
|
|
967
|
+
- `suggestions`: `string[]`. Prompts offered as buttons while the conversation is empty. Pressing one sends it.
|
|
968
|
+
- `emptyState`: `ReactNode`. Shown while the conversation is empty, above the suggestions. Defaults to a short greeting (`labels.emptyTitle`).
|
|
969
|
+
- `renderContent`: `(message: ChatMessage) => ReactNode`. Renders a message's content, e.g. through your Markdown renderer. Defaults to plain text with line breaks kept. Never render unsanitized HTML from a model or a user.
|
|
970
|
+
- `assistantName`: `string`. Name of the assistant, read to screen readers ("Coach said:"). Defaults to `labels.assistant`.
|
|
971
|
+
- `assistantAvatar`: `ReactNode`. Shown beside assistant messages (e.g. an `Avatar` or `Icon`). Decorative: hidden from assistive technology.
|
|
972
|
+
- `userAvatar`: `ReactNode`. Shown beside user messages. Decorative: hidden from assistive technology.
|
|
973
|
+
- `copyable`: `boolean` (default true). Show a Copy button under finished assistant messages.
|
|
974
|
+
- `disclaimer`: `ReactNode`. Small print under the message box, e.g. "AI can make mistakes."
|
|
975
|
+
- `placeholder`: `string`. Placeholder of the message box. Defaults to `labels.placeholder`.
|
|
976
|
+
- `maxLength`: `number`. Maximum length of a message, in characters. Shows a counter near the limit.
|
|
977
|
+
- `disabled`: `boolean`. Disables the message box and the suggestions.
|
|
978
|
+
- `labels`: `Partial<ChatLabels>`. Overrides for the built-in text.
|
|
979
|
+
- Plus every native `<section>` attribute (className, style, id, aria-*, data-*, event handlers), spread onto the root.
|
|
980
|
+
|
|
981
|
+
### Examples (Storybook stories)
|
|
982
|
+
|
|
983
|
+
- Playground
|
|
984
|
+
- Send And Stream: Type a message and press Enter: it is sent, and the streamed reply is announced when done.
|
|
985
|
+
- Shift Enter Adds Line: Shift+Enter adds a line instead of sending.
|
|
986
|
+
- String Reply: `onSend` can return the whole reply as a string (or a promise of one).
|
|
987
|
+
- With Suggestions: While the conversation is empty, `emptyState` and `suggestions` show. Pressing a suggestion sends it.
|
|
988
|
+
- With Avatars: Avatars beside each speaker. They're decorative; the speaker is read from hidden text.
|
|
989
|
+
- Long Conversation: A long conversation. Scroll up, and a button jumps back to the latest message.
|
|
990
|
+
- Long Content: A long unbroken string (a URL, say) wraps instead of overflowing the bubble.
|
|
991
|
+
- Error And Retry: When `onSend` throws, the reply shows an error with a Retry button. This backend fails the first time.
|
|
992
|
+
- Stop Streaming: While a reply streams, the send button becomes Stop (Escape in the message box works too).
|
|
993
|
+
- Disabled
|
|
994
|
+
- With Max Length: `maxLength` limits a message and shows a counter near the limit.
|
|
995
|
+
- System Messages: System messages (joins, handoffs, context notes) are centered notes.
|
|
996
|
+
- Custom Rendering: `renderContent` renders messages your way, e.g. through a Markdown renderer. This one turns `**bold**` into `<strong>` and keeps paragraphs. Never render unsanitized HTML from a model.
|
|
997
|
+
- Controlled: Controlled: the parent owns the messages, so it can save, clear, or seed them. Here, `onSend` returns nothing and the parent adds the reply itself (the way a WebSocket or a human agent would).
|
|
998
|
+
- Connecting ABackend: How to connect a real backend: stream server-sent events from your API route with `readEventStream`, passing `signal` so Stop cancels the request. ```tsx import { Chat, readEventStream } from "@vjdesigns/vjdesigns-ui"; <Chat onSend={async function* (_text, { messages, signal }) { const res = await fetch("/api/chat", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ messages: messages.map(({ role, content }) => ({ role, content })), }), signal, }); if (!res.ok) throw new Error(`Chat request failed: ${res.status}`); for await (const data of readEventStream(res)) { if (data === "[DONE]") break; yield JSON.parse(data).text ?? ""; } }} /> ``` Keep API keys on your server: the browser should call your own route, which calls the model provider. This story fakes the server with an in-memory event stream.
|
|
999
|
+
- Right To Left: Right-to-left: user messages sit on the inline end, which is the left in RTL.
|
|
1000
|
+
|
|
930
1001
|
## Chip
|
|
931
1002
|
|
|
932
1003
|
import { Chip } from "@vjdesigns/vjdesigns-ui";
|
|
@@ -2340,6 +2411,7 @@ announce it yourself if needed.
|
|
|
2340
2411
|
- Update (progressive upload)
|
|
2341
2412
|
- Placements
|
|
2342
2413
|
- Queue and max
|
|
2414
|
+
- One viewport per store: Two providers on one store (here, one at the "app root" and one inside a Japanese `LocaleProvider`) share a single viewport: the first one mounted draws it, so toasts and the "+N more" note never appear twice. Use one provider near the root, inside your `LocaleProvider`, or give a provider its own `store={createToastStore()}` to keep separate toasts.
|
|
2343
2415
|
- Hotkey focus (F8)
|
|
2344
2416
|
- Pause on hover and focus
|
|
2345
2417
|
- Announcements: Announcements go through two persistent live regions inside the viewport: `role="status"` (polite) for most tones and `role="alert"` for errors, with the tone said in words ("Success:").
|
|
@@ -2354,7 +2426,10 @@ import { ToastProvider } from "@vjdesigns/vjdesigns-ui";
|
|
|
2354
2426
|
Category: feedback · Storybook: Feedback/Toast
|
|
2355
2427
|
|
|
2356
2428
|
Renders the toast viewport (in a portal) and provides `useToast()`.
|
|
2357
|
-
Place it once near the root of your app
|
|
2429
|
+
Place it once near the root of your app, inside your `LocaleProvider` so
|
|
2430
|
+
its built-in text (like "+2 more notifications") is in the app's language.
|
|
2431
|
+
Each store gets one viewport: a second provider on the same store (nested
|
|
2432
|
+
or not) only provides `useToast()`, so toasts are never drawn twice.
|
|
2358
2433
|
|
|
2359
2434
|
Accessibility:
|
|
2360
2435
|
- The viewport is a labelled region ("Notifications (F8)"). The hotkey
|