@k8ordo/ui 1.0.0 → 2.0.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 +37 -12
- package/dist/components/ai/conversation/conversation.d.mts +8 -10
- package/dist/components/ai/conversation/conversation.mjs +1 -6
- package/dist/components/ai/conversation/index.d.mts +15 -1
- package/dist/components/ai/conversation/index.mjs +8 -1
- package/dist/components/ai/index.d.mts +5 -9
- package/dist/components/ai/index.mjs +4 -4
- package/dist/components/ai/message/index.d.mts +12 -1
- package/dist/components/ai/message/index.mjs +7 -1
- package/dist/components/ai/message/message.d.mts +3 -5
- package/dist/components/ai/message/message.mjs +1 -5
- package/dist/components/ai/prompt-input/index.d.mts +23 -1
- package/dist/components/ai/prompt-input/index.mjs +8 -1
- package/dist/components/ai/prompt-input/prompt-input.d.mts +4 -6
- package/dist/components/ai/prompt-input/prompt-input.mjs +1 -6
- package/dist/components/ai/suggestion/index.d.mts +13 -1
- package/dist/components/ai/suggestion/index.mjs +7 -1
- package/dist/components/ai/suggestion/suggestion.d.mts +3 -5
- package/dist/components/ai/suggestion/suggestion.mjs +1 -5
- package/dist/components/buttons/icon-button/icon-button.mjs +1 -1
- package/dist/components/form/checkbox/checkbox.d.mts +1 -0
- package/dist/components/form/checkbox/checkbox.mjs +3 -2
- package/dist/components/form/checkbox-group/index.d.mts +1 -0
- package/dist/components/form/file-field/file-field.d.mts +12 -15
- package/dist/components/form/file-field/file-field.mjs +1 -6
- package/dist/components/form/file-field/index.d.mts +17 -1
- package/dist/components/form/file-field/index.mjs +8 -1
- package/dist/components/form/radio/radio.d.mts +1 -0
- package/dist/components/form/radio/radio.mjs +3 -2
- package/dist/components/index.d.mts +15 -21
- package/dist/components/index.mjs +7 -7
- package/dist/components/navigation/anchor/anchor.mjs +1 -1
- package/dist/components/navigation/tabs/index.d.mts +19 -1
- package/dist/components/navigation/tabs/index.mjs +9 -1
- package/dist/components/navigation/tabs/tabs.d.mts +16 -18
- package/dist/components/navigation/tabs/tabs.mjs +1 -7
- package/dist/components/overlays/dialog/dialog.d.mts +12 -16
- package/dist/components/overlays/dialog/dialog.mjs +1 -6
- package/dist/components/overlays/dialog/index.d.mts +17 -1
- package/dist/components/overlays/dialog/index.mjs +8 -1
- package/dist/components/overlays/dropdown-menu/dropdown-menu.d.mts +20 -24
- package/dist/components/overlays/dropdown-menu/dropdown-menu.mjs +2 -11
- package/dist/components/overlays/dropdown-menu/index.d.mts +31 -1
- package/dist/components/overlays/dropdown-menu/index.mjs +12 -1
- package/dist/components/overlays/dropdown-menu/sub-menu.mjs +1 -1
- package/dist/components/overlays/list-box/index.d.mts +27 -1
- package/dist/components/overlays/list-box/index.mjs +9 -1
- package/dist/components/overlays/list-box/list-box.d.mts +20 -22
- package/dist/components/overlays/list-box/list-box.mjs +2 -8
- package/dist/components/overlays/popover/index.d.mts +24 -1
- package/dist/components/overlays/popover/index.mjs +8 -1
- package/dist/components/overlays/popover/popover.d.mts +18 -20
- package/dist/components/overlays/popover/popover.mjs +2 -7
- package/dist/components/overlays/tooltip/index.d.mts +21 -1
- package/dist/components/overlays/tooltip/index.mjs +8 -1
- package/dist/components/overlays/tooltip/tooltip.d.mts +18 -22
- package/dist/components/overlays/tooltip/tooltip.mjs +2 -7
- package/dist/hooks/index.d.mts +1 -4
- package/dist/hooks/index.mjs +2 -5
- package/dist/index.d.mts +13 -15
- package/dist/index.mjs +9 -12
- package/dist/integrations/_shared/renderers.mjs +7 -7
- package/dist/integrations/_shared/schemas.d.mts +1 -1
- package/dist/{hooks/focus-trap/index.d.mts → internal/focus-trap.d.mts} +1 -1
- package/dist/{hooks/focus-trap/index.mjs → internal/focus-trap.mjs} +1 -1
- package/dist/styles/index.css +0 -8
- package/docs/GUIDE.md +148 -133
- package/docs/llms.txt +11 -7
- package/docs/props.generated.json +18 -0
- package/docs/references/ai-chat.md +67 -67
- package/docs/references/color.md +108 -107
- package/docs/references/components.md +318 -310
- package/docs/references/generative-ui.md +51 -30
- package/docs/references/helpers.md +155 -52
- package/docs/references/hooks.md +61 -74
- package/docs/references/interaction-design.md +47 -47
- package/docs/references/spatial-design.md +83 -81
- package/docs/references/typography.md +68 -67
- package/package.json +32 -33
- package/dist/hooks/hash/index.d.mts +0 -4
- package/dist/hooks/hash/index.mjs +0 -25
- package/dist/hooks/local-storage/index.d.mts +0 -4
- package/dist/hooks/local-storage/index.mjs +0 -46
- package/dist/hooks/session-storage/index.d.mts +0 -4
- package/dist/hooks/session-storage/index.mjs +0 -46
package/docs/references/hooks.md
CHANGED
|
@@ -1,16 +1,22 @@
|
|
|
1
1
|
# Hooks
|
|
2
2
|
|
|
3
|
-
k8ordo
|
|
3
|
+
The custom hooks `@k8ordo/ui` provides.
|
|
4
4
|
|
|
5
5
|
```tsx
|
|
6
6
|
import { useDisclosure, useDeferredDebounce } from '@k8ordo/ui';
|
|
7
7
|
```
|
|
8
8
|
|
|
9
|
-
|
|
9
|
+
These hooks cover UI mechanics: disclosure, timing, DOM observation, and the
|
|
10
|
+
like. State that lives in a place — the URL, a history entry, localStorage,
|
|
11
|
+
memory — is `@k8ordo/state`'s job (`defineLocalState` and friends). There is
|
|
12
|
+
deliberately no `useLocalStorage`, `useSessionStorage`, or `useHash` here: a
|
|
13
|
+
package that owned that state twice would give an app two answers.
|
|
14
|
+
|
|
15
|
+
## State
|
|
10
16
|
|
|
11
17
|
### useDisclosure
|
|
12
18
|
|
|
13
|
-
|
|
19
|
+
Toggles open/closed state.
|
|
14
20
|
|
|
15
21
|
```tsx
|
|
16
22
|
const { isOpen, open, close, toggle } = useDisclosure();
|
|
@@ -18,7 +24,7 @@ const { isOpen, open, close, toggle } = useDisclosure();
|
|
|
18
24
|
|
|
19
25
|
### useControllableState
|
|
20
26
|
|
|
21
|
-
|
|
27
|
+
Handles controlled and uncontrolled use transparently.
|
|
22
28
|
|
|
23
29
|
```tsx
|
|
24
30
|
const [value, setValue] = useControllableState({
|
|
@@ -30,7 +36,7 @@ const [value, setValue] = useControllableState({
|
|
|
30
36
|
|
|
31
37
|
### useStep
|
|
32
38
|
|
|
33
|
-
|
|
39
|
+
A step counter with forward/back control.
|
|
34
40
|
|
|
35
41
|
```tsx
|
|
36
42
|
const { count, next, back, isDisabledBack, isDisabledNext } = useStep({
|
|
@@ -39,86 +45,65 @@ const { count, next, back, isDisabledBack, isDisabledNext } = useStep({
|
|
|
39
45
|
});
|
|
40
46
|
```
|
|
41
47
|
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
URL ハッシュの読み取り(読み取り専用)。
|
|
45
|
-
|
|
46
|
-
```tsx
|
|
47
|
-
const hash = useHash(); // string | null
|
|
48
|
-
```
|
|
49
|
-
|
|
50
|
-
## ストレージ
|
|
51
|
-
|
|
52
|
-
### useLocalStorage
|
|
53
|
-
|
|
54
|
-
LocalStorage と同期した状態。`remove` で削除も可能。
|
|
55
|
-
|
|
56
|
-
```tsx
|
|
57
|
-
const [value, setValue, remove] = useLocalStorage<string>('key', 'default');
|
|
58
|
-
```
|
|
59
|
-
|
|
60
|
-
### useSessionStorage
|
|
61
|
-
|
|
62
|
-
SessionStorage と同期した状態。`remove` で削除も可能。
|
|
63
|
-
|
|
64
|
-
```tsx
|
|
65
|
-
const [value, setValue, remove] = useSessionStorage<string>('key', 'default');
|
|
66
|
-
```
|
|
67
|
-
|
|
68
|
-
## イベント
|
|
48
|
+
## Events
|
|
69
49
|
|
|
70
50
|
### useClickAway
|
|
71
51
|
|
|
72
|
-
|
|
52
|
+
Detects a click outside an element. Takes a ref and a callback.
|
|
73
53
|
|
|
74
54
|
```tsx
|
|
75
55
|
const ref = useRef<HTMLDivElement>(null);
|
|
76
56
|
|
|
77
57
|
useClickAway(ref, (e) => {
|
|
78
|
-
console.log('
|
|
58
|
+
console.log('clicked outside');
|
|
79
59
|
});
|
|
80
60
|
```
|
|
81
61
|
|
|
82
|
-
|
|
62
|
+
Arguments:
|
|
83
63
|
|
|
84
64
|
- `ref`: `RefObject<T | null>`
|
|
85
65
|
- `callback`: `(e: Event) => void`
|
|
86
|
-
- `enabled`: boolean
|
|
66
|
+
- `enabled`: boolean (default: `true`)
|
|
87
67
|
|
|
88
68
|
### useHover
|
|
89
69
|
|
|
90
|
-
|
|
70
|
+
Detects the hover state. Spread `hoverProps` onto the element.
|
|
91
71
|
|
|
92
72
|
```tsx
|
|
93
73
|
const { isHovered, hoverProps } = useHover();
|
|
94
74
|
|
|
95
|
-
<div {...hoverProps}>{isHovered ? '
|
|
75
|
+
<div {...hoverProps}>{isHovered ? 'hovered' : 'idle'}</div>;
|
|
96
76
|
```
|
|
97
77
|
|
|
98
|
-
`hoverProps`
|
|
78
|
+
`hoverProps` contains `{ onPointerEnter, onPointerLeave }`.
|
|
99
79
|
|
|
100
|
-
##
|
|
80
|
+
## Timing
|
|
101
81
|
|
|
102
|
-
> `useDeferredDebounce`
|
|
82
|
+
> `useDeferredDebounce` and `useDebouncedTransition` do different jobs.
|
|
103
83
|
>
|
|
104
|
-
> - **`useDeferredDebounce`**:
|
|
105
|
-
> - **`useDebouncedTransition`**:
|
|
84
|
+
> - **`useDeferredDebounce`**: **defers rendering**. The React scheduler simply postpones an expensive re-render; there is no guarantee about how long it waits. It cannot be used to throttle side effects (fetch, external APIs).
|
|
85
|
+
> - **`useDebouncedTransition`**: **rate-limits side effects**. It waits for the given `delay` before running the action, and aborts the previous `AbortSignal` when called again. Use it for work you do not want fired in rapid succession, such as fetch.
|
|
106
86
|
|
|
107
87
|
### useDeferredDebounce
|
|
108
88
|
|
|
109
|
-
`useDeferredValue`
|
|
89
|
+
Wraps `useDeferredValue` and returns the value plus a pending flag for when it
|
|
90
|
+
has not caught up. **For pure UI purposes only**, such as filtering a list as
|
|
91
|
+
the user types.
|
|
110
92
|
|
|
111
93
|
```tsx
|
|
112
94
|
const [deferredValue, isPending] = useDeferredDebounce(inputValue);
|
|
113
95
|
```
|
|
114
96
|
|
|
115
|
-
|
|
97
|
+
Returns:
|
|
116
98
|
|
|
117
99
|
- `[T, boolean]` — `[deferredValue, isPending]`
|
|
118
100
|
|
|
119
101
|
### useDebouncedTransition
|
|
120
102
|
|
|
121
|
-
`startTransition(async)`
|
|
103
|
+
Combines `startTransition(async)` with an `AbortController` and runs the action
|
|
104
|
+
once the delay has elapsed. On a repeat call it aborts the signal passed to the
|
|
105
|
+
previous action, so an `AbortError` thrown inside the action by, say,
|
|
106
|
+
`fetch({ signal })` does not become an unhandled rejection.
|
|
122
107
|
|
|
123
108
|
```tsx
|
|
124
109
|
const [isPending, run] = useDebouncedTransition(300);
|
|
@@ -133,13 +118,13 @@ const handleChange = (e: ChangeEvent<HTMLInputElement>) => {
|
|
|
133
118
|
};
|
|
134
119
|
```
|
|
135
120
|
|
|
136
|
-
|
|
121
|
+
Returns:
|
|
137
122
|
|
|
138
123
|
- `[boolean, (action: (signal: AbortSignal) => void | Promise<void>) => void]`
|
|
139
124
|
|
|
140
125
|
### useInterval
|
|
141
126
|
|
|
142
|
-
|
|
127
|
+
Runs something on an interval.
|
|
143
128
|
|
|
144
129
|
```tsx
|
|
145
130
|
useInterval(() => {
|
|
@@ -149,7 +134,7 @@ useInterval(() => {
|
|
|
149
134
|
|
|
150
135
|
### useTimeout
|
|
151
136
|
|
|
152
|
-
|
|
137
|
+
Runs something after a delay.
|
|
153
138
|
|
|
154
139
|
```tsx
|
|
155
140
|
useTimeout(() => {
|
|
@@ -157,11 +142,11 @@ useTimeout(() => {
|
|
|
157
142
|
}, 3000);
|
|
158
143
|
```
|
|
159
144
|
|
|
160
|
-
## DOM
|
|
145
|
+
## DOM and browser
|
|
161
146
|
|
|
162
147
|
### useWindowSize
|
|
163
148
|
|
|
164
|
-
|
|
149
|
+
Reads the window size.
|
|
165
150
|
|
|
166
151
|
```tsx
|
|
167
152
|
const { width, height } = useWindowSize();
|
|
@@ -169,7 +154,7 @@ const { width, height } = useWindowSize();
|
|
|
169
154
|
|
|
170
155
|
### useWindowResize
|
|
171
156
|
|
|
172
|
-
|
|
157
|
+
Listens for window resizes. The callback receives `{ width, height }`.
|
|
173
158
|
|
|
174
159
|
```tsx
|
|
175
160
|
useWindowResize(
|
|
@@ -180,14 +165,14 @@ useWindowResize(
|
|
|
180
165
|
);
|
|
181
166
|
```
|
|
182
167
|
|
|
183
|
-
|
|
168
|
+
Arguments:
|
|
184
169
|
|
|
185
170
|
- `callback`: `(size: { width: number; height: number }) => void`
|
|
186
171
|
- `options`: `{ enabled?: boolean }`
|
|
187
172
|
|
|
188
173
|
### useResize
|
|
189
174
|
|
|
190
|
-
|
|
175
|
+
Observes an element's size (ResizeObserver). Takes a ref.
|
|
191
176
|
|
|
192
177
|
```tsx
|
|
193
178
|
const ref = useRef<HTMLDivElement>(null);
|
|
@@ -201,7 +186,7 @@ useResize(
|
|
|
201
186
|
);
|
|
202
187
|
```
|
|
203
188
|
|
|
204
|
-
|
|
189
|
+
Arguments:
|
|
205
190
|
|
|
206
191
|
- `ref`: `RefObject<T | null>`
|
|
207
192
|
- `callback`: `(entry: ResizeObserverEntry) => void`
|
|
@@ -209,7 +194,7 @@ useResize(
|
|
|
209
194
|
|
|
210
195
|
### useIntersectionObserver
|
|
211
196
|
|
|
212
|
-
|
|
197
|
+
An intersection observer. Takes a ref.
|
|
213
198
|
|
|
214
199
|
```tsx
|
|
215
200
|
const ref = useRef<HTMLDivElement>(null);
|
|
@@ -223,29 +208,29 @@ useIntersectionObserver(
|
|
|
223
208
|
);
|
|
224
209
|
```
|
|
225
210
|
|
|
226
|
-
|
|
211
|
+
Arguments:
|
|
227
212
|
|
|
228
213
|
- `ref`: `RefObject<T | null>`
|
|
229
214
|
- `callback`: `(entry: IntersectionObserverEntry) => void`
|
|
230
|
-
- `options`: IntersectionObserver
|
|
215
|
+
- `options`: IntersectionObserver options
|
|
231
216
|
|
|
232
217
|
### useInView
|
|
233
218
|
|
|
234
|
-
|
|
219
|
+
Whether an element is in view. Takes a ref and returns a boolean.
|
|
235
220
|
|
|
236
221
|
```tsx
|
|
237
222
|
const ref = useRef<HTMLDivElement>(null);
|
|
238
223
|
const isInView = useInView(ref, { threshold: 0.1 });
|
|
239
224
|
```
|
|
240
225
|
|
|
241
|
-
|
|
226
|
+
Arguments:
|
|
242
227
|
|
|
243
228
|
- `ref`: `RefObject<T | null>`
|
|
244
|
-
- `options`: IntersectionObserver
|
|
229
|
+
- `options`: IntersectionObserver options
|
|
245
230
|
|
|
246
231
|
### useScrollDirection
|
|
247
232
|
|
|
248
|
-
|
|
233
|
+
Detects scroll direction, on both axes.
|
|
249
234
|
|
|
250
235
|
```tsx
|
|
251
236
|
const { x, y } = useScrollDirection();
|
|
@@ -255,48 +240,50 @@ const { x, y } = useScrollDirection();
|
|
|
255
240
|
|
|
256
241
|
### useScrollLock
|
|
257
242
|
|
|
258
|
-
body
|
|
243
|
+
Locks scrolling on the body. Returns lock and unlock functions.
|
|
259
244
|
|
|
260
245
|
```tsx
|
|
261
246
|
const { lock, unlock } = useScrollLock();
|
|
262
247
|
|
|
263
|
-
//
|
|
248
|
+
// when opening a modal
|
|
264
249
|
lock();
|
|
265
250
|
|
|
266
|
-
//
|
|
251
|
+
// when closing it
|
|
267
252
|
unlock();
|
|
268
253
|
```
|
|
269
254
|
|
|
270
255
|
### useWritingMode
|
|
271
256
|
|
|
272
|
-
|
|
257
|
+
Detects an element's `writing-mode` and returns `'horizontal'` or `'vertical'`.
|
|
258
|
+
Every `vertical-*` and `sideways-*` value normalizes to `'vertical'`. It
|
|
259
|
+
observes through a ResizeObserver, and returns `'horizontal'` during SSR.
|
|
273
260
|
|
|
274
261
|
```tsx
|
|
275
262
|
const ref = useRef<HTMLDivElement>(null);
|
|
276
263
|
const writingMode = useWritingMode(ref); // 'horizontal' | 'vertical'
|
|
277
264
|
```
|
|
278
265
|
|
|
279
|
-
|
|
266
|
+
Arguments:
|
|
280
267
|
|
|
281
268
|
- `ref`: `RefObject<Element | null>`
|
|
282
269
|
|
|
283
|
-
|
|
270
|
+
Returns:
|
|
284
271
|
|
|
285
272
|
- `'horizontal' | 'vertical'`
|
|
286
273
|
|
|
287
274
|
### useBreakpoint
|
|
288
275
|
|
|
289
|
-
Tailwind
|
|
276
|
+
Tests a Tailwind breakpoint.
|
|
290
277
|
|
|
291
278
|
```tsx
|
|
292
|
-
const isMd = useBreakpoint('md'); // 768px
|
|
279
|
+
const isMd = useBreakpoint('md'); // whether the viewport is 768px or wider
|
|
293
280
|
```
|
|
294
281
|
|
|
295
|
-
##
|
|
282
|
+
## Utilities
|
|
296
283
|
|
|
297
284
|
### useClient
|
|
298
285
|
|
|
299
|
-
|
|
286
|
+
Whether we are on the client.
|
|
300
287
|
|
|
301
288
|
```tsx
|
|
302
289
|
const isClient = useClient();
|
|
@@ -304,11 +291,11 @@ const isClient = useClient();
|
|
|
304
291
|
|
|
305
292
|
### useClipboard
|
|
306
293
|
|
|
307
|
-
|
|
294
|
+
Reads from and writes to the clipboard.
|
|
308
295
|
|
|
309
296
|
```tsx
|
|
310
297
|
const { writeClipboard, readClipboard } = useClipboard();
|
|
311
298
|
|
|
312
|
-
await writeClipboard('
|
|
299
|
+
await writeClipboard('the text to copy');
|
|
313
300
|
const text = await readClipboard();
|
|
314
301
|
```
|
|
@@ -1,77 +1,77 @@
|
|
|
1
|
-
#
|
|
1
|
+
# Interaction design
|
|
2
2
|
|
|
3
|
-
k8ordo
|
|
3
|
+
Interaction in `@k8ordo/ui` follows one principle: quiet change.
|
|
4
4
|
|
|
5
|
-
##
|
|
5
|
+
## Interactive states
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
Design with these eight states in mind.
|
|
8
8
|
|
|
9
|
-
|
|
|
9
|
+
| State | Style |
|
|
10
10
|
| -------- | ----------------------------------------------------- |
|
|
11
|
-
| Default |
|
|
12
|
-
| Hover | `hover:bg-bg-mute` —
|
|
11
|
+
| Default | The base style |
|
|
12
|
+
| Hover | `hover:bg-bg-mute` — a gentle color shift |
|
|
13
13
|
| Focus | `focus-visible:ring-2 focus-visible:ring-border-info` |
|
|
14
14
|
| Active | `active:bg-bg-emphasize` |
|
|
15
15
|
| Disabled | `opacity-50 cursor-not-allowed` |
|
|
16
|
-
| Loading |
|
|
16
|
+
| Loading | A spinner or a skeleton |
|
|
17
17
|
| Selected | `bg-primary-bg-subtle` |
|
|
18
18
|
| Error | `border-border-error` + `text-fg-error` |
|
|
19
19
|
|
|
20
|
-
##
|
|
20
|
+
## Transitions
|
|
21
21
|
|
|
22
|
-
|
|
22
|
+
Restrained, natural motion.
|
|
23
23
|
|
|
24
|
-
|
|
|
25
|
-
|
|
|
26
|
-
|
|
|
27
|
-
|
|
|
28
|
-
|
|
|
24
|
+
| Purpose | Recommended setting |
|
|
25
|
+
| ------------------------------ | ------------------------------------------ |
|
|
26
|
+
| Hover color change | `transition-colors duration-150 ease-out` |
|
|
27
|
+
| Opacity change | `transition-opacity duration-200 ease-out` |
|
|
28
|
+
| When size changes are involved | `transition-all duration-150 ease-out` |
|
|
29
29
|
|
|
30
|
-
###
|
|
30
|
+
### Timing principles
|
|
31
31
|
|
|
32
|
-
- **100ms**:
|
|
33
|
-
- **150–200ms**:
|
|
34
|
-
- **300ms**:
|
|
35
|
-
- **300ms
|
|
32
|
+
- **100ms**: immediate feedback (a button press)
|
|
33
|
+
- **150–200ms**: the standard transition (hover, focus)
|
|
34
|
+
- **300ms**: open/close animation (the limit for Accordion, Drawer)
|
|
35
|
+
- **Never exceed 300ms** — it starts to feel heavy
|
|
36
36
|
|
|
37
|
-
###
|
|
37
|
+
### Choosing an animation
|
|
38
38
|
|
|
39
39
|
```tsx
|
|
40
|
-
// Good: transition-colors
|
|
40
|
+
// Good: transition-colors (only the color changes)
|
|
41
41
|
className = 'transition-colors hover:bg-bg-mute';
|
|
42
42
|
|
|
43
|
-
// OK: transition-all
|
|
43
|
+
// OK: transition-all (only when several properties change)
|
|
44
44
|
className = 'transition-all hover:bg-bg-mute hover:scale-[1.02]';
|
|
45
45
|
|
|
46
|
-
// Bad: bounce
|
|
46
|
+
// Bad: bounce or spring easing
|
|
47
47
|
className = 'animate-bounce';
|
|
48
48
|
```
|
|
49
49
|
|
|
50
|
-
##
|
|
50
|
+
## Focus management
|
|
51
51
|
|
|
52
|
-
- `focus-visible`
|
|
53
|
-
-
|
|
54
|
-
- `outline-hidden`
|
|
52
|
+
- Use `focus-visible`, not `focus` — no ring appears on a mouse click
|
|
53
|
+
- Keep the focus ring consistent with `ring-border-info`
|
|
54
|
+
- Clear the default outline with `outline-hidden` before applying the ring
|
|
55
55
|
|
|
56
56
|
```tsx
|
|
57
57
|
className =
|
|
58
58
|
'focus-visible:outline-hidden focus-visible:ring-2 focus-visible:ring-border-info';
|
|
59
59
|
```
|
|
60
60
|
|
|
61
|
-
##
|
|
61
|
+
## Form design
|
|
62
62
|
|
|
63
|
-
-
|
|
64
|
-
-
|
|
65
|
-
-
|
|
66
|
-
-
|
|
67
|
-
- `FormControl`
|
|
63
|
+
- Put the label above the input (a left-aligned label reads badly in Japanese)
|
|
64
|
+
- Put the error message directly under the input, as `text-fg-error text-sm`
|
|
65
|
+
- Mark required fields with `*` after the label
|
|
66
|
+
- Validate on submit; keep real-time validation to a minimum
|
|
67
|
+
- Use the `FormControl` component so labels and errors stay consistent
|
|
68
68
|
|
|
69
69
|
```tsx
|
|
70
70
|
import { FormControl, TextField } from '@k8ordo/ui';
|
|
71
71
|
|
|
72
72
|
<FormControl
|
|
73
|
-
label="
|
|
74
|
-
errorText="
|
|
73
|
+
label="Email address"
|
|
74
|
+
errorText="This field is required"
|
|
75
75
|
required
|
|
76
76
|
renderInput={(props) => (
|
|
77
77
|
<TextField {...props} placeholder="example@mail.com" />
|
|
@@ -79,17 +79,17 @@ import { FormControl, TextField } from '@k8ordo/ui';
|
|
|
79
79
|
/>;
|
|
80
80
|
```
|
|
81
81
|
|
|
82
|
-
##
|
|
82
|
+
## Accessibility
|
|
83
83
|
|
|
84
|
-
- `aria-label` / `aria-describedby`
|
|
85
|
-
-
|
|
86
|
-
- `prefers-reduced-motion`
|
|
87
|
-
-
|
|
84
|
+
- Set `aria-label` / `aria-describedby` where they belong
|
|
85
|
+
- Guarantee keyboard navigation (Tab, Enter, Escape, arrow keys)
|
|
86
|
+
- Respect `prefers-reduced-motion` — the motion library handles it for you
|
|
87
|
+
- Never signal state with color alone; pair it with an icon or text
|
|
88
88
|
|
|
89
|
-
##
|
|
89
|
+
## What not to do
|
|
90
90
|
|
|
91
|
-
-
|
|
92
|
-
- 300ms
|
|
93
|
-
-
|
|
94
|
-
- `cursor-pointer`
|
|
95
|
-
-
|
|
91
|
+
- Bounce or spring easing
|
|
92
|
+
- Animation longer than 300ms
|
|
93
|
+
- A strong primary color (`bg-primary-bg`) on hover
|
|
94
|
+
- `cursor-pointer` on anything that is not a button (links do not need it either)
|
|
95
|
+
- A submit button with no double-click guard
|