@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
|
@@ -1,11 +1,11 @@
|
|
|
1
|
-
# k8ordo
|
|
1
|
+
# @k8ordo/ui AI chat components
|
|
2
2
|
|
|
3
|
-
AI
|
|
3
|
+
Components for building an AI chat UI: the conversation log, messages, the input box, reasoning display, and tool-invocation display. Import them from their dedicated subpaths rather than from the root.
|
|
4
4
|
|
|
5
|
-
##
|
|
5
|
+
## Importing
|
|
6
6
|
|
|
7
7
|
```tsx
|
|
8
|
-
//
|
|
8
|
+
// Core components (no extra dependencies)
|
|
9
9
|
import {
|
|
10
10
|
Conversation,
|
|
11
11
|
Message,
|
|
@@ -15,47 +15,47 @@ import {
|
|
|
15
15
|
ToolInvocation,
|
|
16
16
|
} from '@k8ordo/ui/ai';
|
|
17
17
|
|
|
18
|
-
//
|
|
18
|
+
// Streaming-aware Markdown rendering (optional peer: streamdown)
|
|
19
19
|
import { Response } from '@k8ordo/ui/ai/response';
|
|
20
20
|
|
|
21
|
-
// AI SDK
|
|
21
|
+
// AI SDK integration (optional peer: ai)
|
|
22
22
|
import { mapMessageParts } from '@k8ordo/ui/ai-sdk';
|
|
23
23
|
```
|
|
24
24
|
|
|
25
|
-
|
|
25
|
+
The types are exported from `@k8ordo/ui/ai` too:
|
|
26
26
|
|
|
27
|
-
- `ChatStatus`: `'ready' | 'submitted' | 'streaming' | 'error'
|
|
28
|
-
- `ToolState`: `'input-streaming' | 'input-available' | 'approval-requested' | 'approval-responded' | 'output-available' | 'output-error' | 'output-denied'
|
|
27
|
+
- `ChatStatus`: `'ready' | 'submitted' | 'streaming' | 'error'` (compatible with the AI SDK's `status`)
|
|
28
|
+
- `ToolState`: `'input-streaming' | 'input-available' | 'approval-requested' | 'approval-responded' | 'output-available' | 'output-error' | 'output-denied'` (1:1 with the AI SDK v7 tool states)
|
|
29
29
|
|
|
30
|
-
### optional
|
|
30
|
+
### Setting up the optional peers
|
|
31
31
|
|
|
32
|
-
|
|
32
|
+
Install only what the subpaths you use require. `@k8ordo/ui/ai` itself needs neither.
|
|
33
33
|
|
|
34
34
|
```bash
|
|
35
|
-
# @k8ordo/ui/ai/response
|
|
35
|
+
# If you use @k8ordo/ui/ai/response (Response)
|
|
36
36
|
pnpm add streamdown
|
|
37
|
-
# @k8ordo/ui/ai-sdk
|
|
37
|
+
# If you use @k8ordo/ui/ai-sdk (mapMessageParts)
|
|
38
38
|
pnpm add ai
|
|
39
39
|
```
|
|
40
40
|
|
|
41
|
-
`Response`
|
|
41
|
+
Using `Response` also means loading streamdown's stylesheet and adding a Tailwind `@source` entry. streamdown's styles are not part of the prebuilt `styles.css`, so `Response` alone requires a Tailwind CSS 4 build (the `@k8ordo/ui/tailwind.css` entry):
|
|
42
42
|
|
|
43
43
|
```tsx
|
|
44
44
|
import 'streamdown/styles.css';
|
|
45
45
|
```
|
|
46
46
|
|
|
47
47
|
```css
|
|
48
|
-
/*
|
|
48
|
+
/* Add to your app's CSS entry (the path is relative from the CSS file to node_modules) */
|
|
49
49
|
@source '../node_modules/streamdown/dist/*.js';
|
|
50
50
|
```
|
|
51
51
|
|
|
52
|
-
|
|
52
|
+
The `useChat` used in the examples below comes from the AI SDK's React bindings (`pnpm add @ai-sdk/react`).
|
|
53
53
|
|
|
54
|
-
|
|
54
|
+
The "default" shown for `label`, `sendLabel`, and similar props below is the message dictionary's default (Japanese). Swap the whole dictionary with `<UIProvider messages={en}>` (see [i18n](components.md)). Passing the prop directly wins over the dictionary.
|
|
55
55
|
|
|
56
|
-
##
|
|
56
|
+
## The whole picture
|
|
57
57
|
|
|
58
|
-
`Conversation
|
|
58
|
+
`Conversation` (the log), `Message` (a bubble), and `PromptInput` (the input box) form the skeleton. `Response`, `Reasoning`, and `ToolInvocation` can be added later.
|
|
59
59
|
|
|
60
60
|
```tsx
|
|
61
61
|
'use client';
|
|
@@ -99,7 +99,7 @@ export function Chat() {
|
|
|
99
99
|
onSubmit={(text) => sendMessage({ text })}
|
|
100
100
|
status={status}
|
|
101
101
|
>
|
|
102
|
-
<PromptInput.Textarea placeholder="
|
|
102
|
+
<PromptInput.Textarea placeholder="Type a message" />
|
|
103
103
|
<PromptInput.Submit />
|
|
104
104
|
</PromptInput.Root>
|
|
105
105
|
</div>
|
|
@@ -109,7 +109,7 @@ export function Chat() {
|
|
|
109
109
|
|
|
110
110
|
## Conversation
|
|
111
111
|
|
|
112
|
-
|
|
112
|
+
The scrolling region for the conversation log, as a compound component. It sticks to the bottom and auto-scrolls on a new message. Once you scroll away from the bottom, a `ScrollButton` appears.
|
|
113
113
|
|
|
114
114
|
```tsx
|
|
115
115
|
import { Conversation } from '@k8ordo/ui/ai';
|
|
@@ -124,40 +124,40 @@ import { Conversation } from '@k8ordo/ui/ai';
|
|
|
124
124
|
|
|
125
125
|
Props (Conversation.Messages):
|
|
126
126
|
|
|
127
|
-
- `label`: string
|
|
128
|
-
- `isStreaming`: boolean
|
|
127
|
+
- `label`: string (default: `'チャット'`, used as the aria-label)
|
|
128
|
+
- `isStreaming`: boolean (reflected in `aria-busy`)
|
|
129
129
|
|
|
130
130
|
Props (Conversation.ScrollButton):
|
|
131
131
|
|
|
132
|
-
- `label`: string
|
|
132
|
+
- `label`: string (default: `'最新のメッセージへ移動'`)
|
|
133
133
|
|
|
134
134
|
## Message
|
|
135
135
|
|
|
136
|
-
|
|
136
|
+
A single message. `from="user"` renders as a right-aligned bubble; `from="assistant"` renders as running text. An avatar or anything else can sit freely as a child of `Message.Root`.
|
|
137
137
|
|
|
138
138
|
```tsx
|
|
139
139
|
import { Message } from '@k8ordo/ui/ai';
|
|
140
140
|
|
|
141
141
|
<Message.Root from="user">
|
|
142
|
-
<Message.Content
|
|
142
|
+
<Message.Content>Hello</Message.Content>
|
|
143
143
|
</Message.Root>
|
|
144
144
|
|
|
145
145
|
<Message.Root from="assistant">
|
|
146
|
-
<Message.Content isStreaming
|
|
146
|
+
<Message.Content isStreaming>Text still streaming…</Message.Content>
|
|
147
147
|
</Message.Root>
|
|
148
148
|
```
|
|
149
149
|
|
|
150
150
|
Props (Message.Root):
|
|
151
151
|
|
|
152
|
-
- `from`: `'user'` | `'assistant'
|
|
152
|
+
- `from`: `'user'` | `'assistant'` (required)
|
|
153
153
|
|
|
154
154
|
Props (Message.Content):
|
|
155
155
|
|
|
156
|
-
- `isStreaming`: boolean
|
|
156
|
+
- `isStreaming`: boolean (shows the streaming cursor when true)
|
|
157
157
|
|
|
158
158
|
## PromptInput
|
|
159
159
|
|
|
160
|
-
|
|
160
|
+
The submit form. Enter sends; Shift+Enter inserts a newline; the Enter that commits an IME conversion does not send. While `status` is `'submitted'` or `'streaming'`, `Submit` turns into a stop button. The body is trimmed, and an empty string is never sent.
|
|
161
161
|
|
|
162
162
|
```tsx
|
|
163
163
|
import { PromptInput } from '@k8ordo/ui/ai';
|
|
@@ -167,30 +167,30 @@ import { PromptInput } from '@k8ordo/ui/ai';
|
|
|
167
167
|
onSubmit={(message) => send(message)}
|
|
168
168
|
onStop={stop}
|
|
169
169
|
>
|
|
170
|
-
<PromptInput.Textarea placeholder="
|
|
170
|
+
<PromptInput.Textarea placeholder="Type a message" />
|
|
171
171
|
<PromptInput.Submit />
|
|
172
172
|
</PromptInput.Root>;
|
|
173
173
|
```
|
|
174
174
|
|
|
175
175
|
Props (PromptInput.Root):
|
|
176
176
|
|
|
177
|
-
- `status`: ChatStatus
|
|
178
|
-
- `value` / `defaultValue` / `onChange`: controlled
|
|
179
|
-
- `onSubmit`: `(message: string) => void
|
|
180
|
-
- `onStop`: `() => void
|
|
177
|
+
- `status`: ChatStatus (default: `'ready'`)
|
|
178
|
+
- `value` / `defaultValue` / `onChange`: supports both controlled and uncontrolled use (`onChange` is `(value: string) => void`)
|
|
179
|
+
- `onSubmit`: `(message: string) => void` (the trimmed body)
|
|
180
|
+
- `onStop`: `() => void` (when the stop button is pressed mid-send)
|
|
181
181
|
|
|
182
182
|
Props (PromptInput.Textarea):
|
|
183
183
|
|
|
184
|
-
- `placeholder`: string
|
|
184
|
+
- `placeholder`: string, plus the other textarea attributes
|
|
185
185
|
|
|
186
186
|
Props (PromptInput.Submit):
|
|
187
187
|
|
|
188
|
-
- `sendLabel`: string
|
|
189
|
-
- `stopLabel`: string
|
|
188
|
+
- `sendLabel`: string (default: `'送信'`)
|
|
189
|
+
- `stopLabel`: string (default: `'停止'`)
|
|
190
190
|
|
|
191
191
|
## Reasoning
|
|
192
192
|
|
|
193
|
-
|
|
193
|
+
A collapsible view of the model's reasoning. While streaming, the label reads 「思考中…」.
|
|
194
194
|
|
|
195
195
|
```tsx
|
|
196
196
|
import { Reasoning } from '@k8ordo/ui/ai';
|
|
@@ -201,11 +201,11 @@ import { Reasoning } from '@k8ordo/ui/ai';
|
|
|
201
201
|
Props:
|
|
202
202
|
|
|
203
203
|
- `isStreaming`: boolean
|
|
204
|
-
- `isOpen` / `defaultOpen` / `onChange`:
|
|
204
|
+
- `isOpen` / `defaultOpen` / `onChange`: open state, controlled or uncontrolled (`onChange` is `(isOpen: boolean) => void`)
|
|
205
205
|
|
|
206
206
|
## Response
|
|
207
207
|
|
|
208
|
-
|
|
208
|
+
A streaming-aware Markdown renderer. It renders half-finished Markdown — an unclosed code block, say — without breaking. Requires the optional peer `streamdown` (see [setup](#setting-up-the-optional-peers)).
|
|
209
209
|
|
|
210
210
|
```tsx
|
|
211
211
|
import { Response } from '@k8ordo/ui/ai/response';
|
|
@@ -218,47 +218,47 @@ import 'streamdown/styles.css';
|
|
|
218
218
|
|
|
219
219
|
Props:
|
|
220
220
|
|
|
221
|
-
- `children`: string
|
|
221
|
+
- `children`: string (the Markdown, required)
|
|
222
222
|
- `isStreaming`: boolean
|
|
223
|
-
-
|
|
223
|
+
- Every other streamdown prop (`translations`, `controls`, `linkSafety`, `plugins`, `components`, `urlTransform`, `dir`, …) passes straight through. The library owns `className` and `mode` (`mode` is derived from `isStreaming`)
|
|
224
224
|
|
|
225
|
-
|
|
225
|
+
The wording comes from the i18n dictionary, so by default strings such as 「コードをコピー」 and 「表をダウンロード」 appear in Japanese. Pass `translations` to change individual strings (prop > dictionary > streamdown's own default).
|
|
226
226
|
|
|
227
|
-
`linkSafety`
|
|
227
|
+
The library **defaults `linkSafety` to off**. With streamdown's own default (on), links render as `<button>` rather than `<a>`, which loses ⌘-click, middle-click, copying the link address, and the link role for assistive technology. Turn it on explicitly if you want the confirmation dialog:
|
|
228
228
|
|
|
229
229
|
```tsx
|
|
230
230
|
<Response linkSafety={{ enabled: true }}>{markdown}</Response>
|
|
231
231
|
```
|
|
232
232
|
|
|
233
|
-
|
|
233
|
+
Dangerous schemes such as `javascript:` are neutralized by rehype-harden regardless of `linkSafety`, so they never become live links even with the default.
|
|
234
234
|
|
|
235
235
|
## Suggestion
|
|
236
236
|
|
|
237
|
-
|
|
237
|
+
Suggested prompts shown as chips. Clicking one passes its `value` to `onSelect`.
|
|
238
238
|
|
|
239
239
|
```tsx
|
|
240
240
|
import { Suggestion } from '@k8ordo/ui/ai';
|
|
241
241
|
|
|
242
242
|
<Suggestion.List>
|
|
243
|
-
<Suggestion.Item onSelect={send} value="IME
|
|
243
|
+
<Suggestion.Item onSelect={send} value="Tell me about IME support" />
|
|
244
244
|
<Suggestion.Item onSelect={send} value="streaming">
|
|
245
|
-
|
|
245
|
+
How does streaming display work?
|
|
246
246
|
</Suggestion.Item>
|
|
247
247
|
</Suggestion.List>;
|
|
248
248
|
```
|
|
249
249
|
|
|
250
250
|
Props (Suggestion.List):
|
|
251
251
|
|
|
252
|
-
- `label`: string
|
|
252
|
+
- `label`: string (default: `'候補'`, used as the aria-label)
|
|
253
253
|
|
|
254
254
|
Props (Suggestion.Item):
|
|
255
255
|
|
|
256
|
-
- `value`: string
|
|
256
|
+
- `value`: string (required; also serves as the visible text when children is omitted)
|
|
257
257
|
- `onSelect`: `(value: string) => void`
|
|
258
258
|
|
|
259
259
|
## ToolInvocation
|
|
260
260
|
|
|
261
|
-
|
|
261
|
+
A collapsible view of a tool call. The icon switches between spinner, success, error, and denied according to `state` (awaiting approval, approval responded, and input streaming all count as in-progress and show the spinner).
|
|
262
262
|
|
|
263
263
|
```tsx
|
|
264
264
|
import { ToolInvocation } from '@k8ordo/ui/ai';
|
|
@@ -267,23 +267,23 @@ import { ToolInvocation } from '@k8ordo/ui/ai';
|
|
|
267
267
|
name="search_web"
|
|
268
268
|
state="output-available"
|
|
269
269
|
input={{ query: 'k8ordo UI' }}
|
|
270
|
-
output="
|
|
270
|
+
output="Search results…"
|
|
271
271
|
/>;
|
|
272
272
|
```
|
|
273
273
|
|
|
274
274
|
Props:
|
|
275
275
|
|
|
276
|
-
- `name`: string
|
|
277
|
-
- `state`: ToolState
|
|
278
|
-
- `input`: unknown
|
|
279
|
-
- `output`: ReactNode
|
|
280
|
-
- `errorText`: string
|
|
281
|
-
- `deniedReason`: string
|
|
282
|
-
- `isOpen` / `defaultOpen` / `onChange`:
|
|
276
|
+
- `name`: string (required, the tool name)
|
|
277
|
+
- `state`: ToolState (required)
|
|
278
|
+
- `input`: unknown (anything other than a string is shown as JSON)
|
|
279
|
+
- `output`: ReactNode (a string is shown in a `pre`; an element renders as-is)
|
|
280
|
+
- `errorText`: string (shown when `state="output-error"`; falls back to the default wording)
|
|
281
|
+
- `deniedReason`: string (shown when `state="output-denied"`; falls back to the default wording)
|
|
282
|
+
- `isOpen` / `defaultOpen` / `onChange`: open state, controlled or uncontrolled
|
|
283
283
|
|
|
284
|
-
## AI SDK
|
|
284
|
+
## AI SDK integration (mapMessageParts)
|
|
285
285
|
|
|
286
|
-
`@k8ordo/ui/ai-sdk`
|
|
286
|
+
`mapMessageParts` from `@k8ordo/ui/ai-sdk` converts the AI SDK's `UIMessage.parts` into a `MappedPart[]` that is easy to render. It has no React dependency and requires the optional peer `ai` (v7).
|
|
287
287
|
|
|
288
288
|
```ts
|
|
289
289
|
type MappedPart =
|
|
@@ -301,7 +301,7 @@ type MappedPart =
|
|
|
301
301
|
};
|
|
302
302
|
```
|
|
303
303
|
|
|
304
|
-
|
|
304
|
+
The caller switches on `kind` and renders `Response`, `Reasoning`, or `ToolInvocation`:
|
|
305
305
|
|
|
306
306
|
```tsx
|
|
307
307
|
import { mapMessageParts } from '@k8ordo/ui/ai-sdk';
|
|
@@ -313,7 +313,7 @@ import { Response } from '@k8ordo/ui/ai/response';
|
|
|
313
313
|
if (part.kind === 'reasoning')
|
|
314
314
|
return <Reasoning key={i}>{part.text}</Reasoning>;
|
|
315
315
|
if (part.kind === 'tool') {
|
|
316
|
-
// output
|
|
316
|
+
// output is unknown, so convert it to a ReactNode before passing it on
|
|
317
317
|
return (
|
|
318
318
|
<ToolInvocation
|
|
319
319
|
key={part.toolCallId}
|
|
@@ -335,11 +335,11 @@ import { Response } from '@k8ordo/ui/ai/response';
|
|
|
335
335
|
</Message.Content>;
|
|
336
336
|
```
|
|
337
337
|
|
|
338
|
-
`ChatStatus`
|
|
338
|
+
The `ChatStatus` and `ToolState` types are re-exported from `@k8ordo/ui/ai-sdk` as well.
|
|
339
339
|
|
|
340
|
-
##
|
|
340
|
+
## Combining with generative UI
|
|
341
341
|
|
|
342
|
-
|
|
342
|
+
A UI spec returned by an LLM as a tool result can be rendered inside a chat bubble. See the [generative UI reference](generative-ui.md) for details.
|
|
343
343
|
|
|
344
344
|
```tsx
|
|
345
345
|
'use client';
|
package/docs/references/color.md
CHANGED
|
@@ -1,121 +1,122 @@
|
|
|
1
|
-
#
|
|
2
|
-
|
|
3
|
-
k8ordo
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
-
|
|
9
|
-
- **
|
|
10
|
-
- **
|
|
11
|
-
-
|
|
12
|
-
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
|
20
|
-
|
|
|
21
|
-
|
|
|
22
|
-
|
|
|
23
|
-
|
|
|
24
|
-
|
|
|
25
|
-
|
|
|
26
|
-
|
|
|
27
|
-
|
|
|
28
|
-
|
|
|
29
|
-
|
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
|
37
|
-
|
|
|
38
|
-
| `text-fg-
|
|
39
|
-
| `text-fg-
|
|
40
|
-
| `text-fg-
|
|
41
|
-
| `text-fg-
|
|
42
|
-
| `text-fg-
|
|
43
|
-
| `text-fg-
|
|
44
|
-
| `text-fg-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
|
50
|
-
|
|
|
51
|
-
| `bg-bg-
|
|
52
|
-
| `bg-bg-
|
|
53
|
-
| `bg-bg-
|
|
54
|
-
| `bg-bg-
|
|
55
|
-
| `bg-bg-
|
|
56
|
-
| `bg-bg-
|
|
57
|
-
| `bg-bg-
|
|
58
|
-
| `bg-bg-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
|
64
|
-
|
|
|
65
|
-
| `border-border-
|
|
66
|
-
| `border-border-
|
|
67
|
-
| `border-border-
|
|
68
|
-
| `border-border-
|
|
69
|
-
| `border-border-
|
|
70
|
-
| `border-border-
|
|
71
|
-
| `border-border-
|
|
72
|
-
| `border-border-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
|
78
|
-
|
|
|
79
|
-
| `
|
|
80
|
-
| `bg-primary-bg
|
|
81
|
-
| `bg-primary-bg-
|
|
82
|
-
| `bg-primary-bg-
|
|
83
|
-
| `
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
|
89
|
-
|
|
|
90
|
-
| `
|
|
91
|
-
| `bg-secondary-bg
|
|
92
|
-
| `
|
|
93
|
-
|
|
94
|
-
|
|
1
|
+
# Color system
|
|
2
|
+
|
|
3
|
+
The colors in `@k8ordo/ui` are defined in the OKLCH color space and reach the UI
|
|
4
|
+
in a calm tone.
|
|
5
|
+
|
|
6
|
+
## Design intent
|
|
7
|
+
|
|
8
|
+
- **An OKLCH palette**: every color is defined in OKLCH. Lightness (L) is shared across hues, so the same step number gives matching contrast whatever the hue
|
|
9
|
+
- **A vivid palette, calm tokens**: the palette itself stays vivid; the semantic tokens map it down to a restrained tone
|
|
10
|
+
- **WCAG AAA**: fg/bg pairs hold a contrast ratio of 7:1 or better
|
|
11
|
+
- **The 60-30-10 rule**: 60% neutral (greys), 30% supporting (`bg-subtle`, …), 10% accent (primary)
|
|
12
|
+
- Primary is teal (H:180), secondary is cyan (H:210)
|
|
13
|
+
- Dark mode is designed as its own tone, not as an inversion of light mode
|
|
14
|
+
|
|
15
|
+
## OKLCH palette design
|
|
16
|
+
|
|
17
|
+
The lightness scale (shared by every hue):
|
|
18
|
+
|
|
19
|
+
| Step | L | Use |
|
|
20
|
+
| ---- | ----- | ----------------------- |
|
|
21
|
+
| 50 | 0.975 | The lightest background |
|
|
22
|
+
| 100 | 0.945 | A light background |
|
|
23
|
+
| 200 | 0.900 | A restrained background |
|
|
24
|
+
| 300 | 0.840 | Supporting color |
|
|
25
|
+
| 400 | 0.750 | Mid tone |
|
|
26
|
+
| 500 | 0.660 | The core color |
|
|
27
|
+
| 600 | 0.575 | Slightly dark |
|
|
28
|
+
| 700 | 0.490 | Dark tone |
|
|
29
|
+
| 800 | 0.410 | For text (AAA on white) |
|
|
30
|
+
| 900 | 0.370 | The darkest |
|
|
31
|
+
|
|
32
|
+
Chroma is optimized per hue (maximized within gamut).
|
|
33
|
+
|
|
34
|
+
## Semantic colors (foreground)
|
|
35
|
+
|
|
36
|
+
| Tailwind class | Light | Dark | Use |
|
|
37
|
+
| ----------------- | ---------- | ---------- | --------------- |
|
|
38
|
+
| `text-fg-base` | gray-900 | gray-50 | Body text |
|
|
39
|
+
| `text-fg-subtle` | gray-400 | gray-500 | Placeholders |
|
|
40
|
+
| `text-fg-mute` | gray-700 | gray-300 | Supporting text |
|
|
41
|
+
| `text-fg-inverse` | gray-50 | gray-900 | Inverted text |
|
|
42
|
+
| `text-fg-info` | blue-800 | blue-200 | Information |
|
|
43
|
+
| `text-fg-success` | green-800 | green-200 | Success |
|
|
44
|
+
| `text-fg-warning` | yellow-800 | yellow-200 | Warning |
|
|
45
|
+
| `text-fg-error` | red-800 | red-200 | Error |
|
|
46
|
+
|
|
47
|
+
## Semantic colors (background)
|
|
48
|
+
|
|
49
|
+
| Tailwind class | Light | Dark | Use |
|
|
50
|
+
| ----------------- | ---------- | ---------- | -------------------- |
|
|
51
|
+
| `bg-bg-base` | white | gray-900 | Card background |
|
|
52
|
+
| `bg-bg-subtle` | gray-100 | gray-800 | Page background |
|
|
53
|
+
| `bg-bg-mute` | gray-200 | gray-700 | For the hover state |
|
|
54
|
+
| `bg-bg-emphasize` | gray-300 | gray-600 | For the active state |
|
|
55
|
+
| `bg-bg-inverse` | gray-900 | white | Inverted background |
|
|
56
|
+
| `bg-bg-info` | blue-100 | blue-900 | Information ground |
|
|
57
|
+
| `bg-bg-success` | green-100 | green-900 | Success ground |
|
|
58
|
+
| `bg-bg-warning` | yellow-100 | yellow-900 | Warning ground |
|
|
59
|
+
| `bg-bg-error` | red-100 | red-900 | Error ground |
|
|
60
|
+
|
|
61
|
+
## Semantic colors (border)
|
|
62
|
+
|
|
63
|
+
| Tailwind class | Light | Dark | Use |
|
|
64
|
+
| ------------------------- | ---------- | ---------- | ------------------ |
|
|
65
|
+
| `border-border-base` | gray-400 | gray-600 | Standard border |
|
|
66
|
+
| `border-border-subtle` | gray-100 | gray-900 | Light border |
|
|
67
|
+
| `border-border-mute` | gray-200 | gray-800 | Restrained border |
|
|
68
|
+
| `border-border-emphasize` | gray-500 | gray-500 | Emphasized border |
|
|
69
|
+
| `border-border-inverse` | gray-700 | gray-300 | Inverted border |
|
|
70
|
+
| `border-border-info` | blue-500 | blue-500 | Information border |
|
|
71
|
+
| `border-border-success` | green-500 | green-500 | Success border |
|
|
72
|
+
| `border-border-warning` | yellow-500 | yellow-500 | Warning border |
|
|
73
|
+
| `border-border-error` | red-500 | red-500 | Error border |
|
|
74
|
+
|
|
75
|
+
## Brand color (primary: teal H:180)
|
|
76
|
+
|
|
77
|
+
| Tailwind class | Light | Dark | Use |
|
|
78
|
+
| ------------------------- | -------- | -------- | ------------------------- |
|
|
79
|
+
| `text-primary-fg` | teal-800 | teal-100 | Primary text |
|
|
80
|
+
| `bg-primary-bg` | teal-200 | teal-800 | Primary background |
|
|
81
|
+
| `bg-primary-bg-subtle` | teal-50 | teal-900 | Light primary background |
|
|
82
|
+
| `bg-primary-bg-mute` | teal-100 | teal-800 | Restrained primary ground |
|
|
83
|
+
| `bg-primary-bg-emphasize` | teal-200 | teal-700 | Emphasized primary ground |
|
|
84
|
+
| `border-primary-border` | teal-500 | teal-500 | Primary border |
|
|
85
|
+
|
|
86
|
+
## Brand color (secondary: cyan H:210)
|
|
87
|
+
|
|
88
|
+
| Tailwind class | Light | Dark | Use |
|
|
89
|
+
| ------------------------- | -------- | -------- | -------------------------- |
|
|
90
|
+
| `text-secondary-fg` | cyan-700 | cyan-300 | Secondary text |
|
|
91
|
+
| `bg-secondary-bg` | cyan-300 | cyan-700 | Secondary background |
|
|
92
|
+
| `bg-secondary-bg-subtle` | cyan-100 | cyan-900 | Light secondary background |
|
|
93
|
+
| `border-secondary-border` | cyan-600 | cyan-600 | Secondary border |
|
|
94
|
+
|
|
95
|
+
## Usage examples
|
|
95
96
|
|
|
96
97
|
```tsx
|
|
97
|
-
//
|
|
98
|
+
// Float a card on the page background
|
|
98
99
|
<div className="bg-bg-subtle min-h-screen">
|
|
99
100
|
<Card appearance="shadow">
|
|
100
101
|
<div className="p-8">
|
|
101
|
-
<h2 className="text-fg-base font-bold"
|
|
102
|
-
<p className="text-fg-mute mt-2"
|
|
103
|
-
<span className="text-primary-fg"
|
|
102
|
+
<h2 className="text-fg-base font-bold">Title</h2>
|
|
103
|
+
<p className="text-fg-mute mt-2">Supporting text</p>
|
|
104
|
+
<span className="text-primary-fg">Accent text</span>
|
|
104
105
|
</div>
|
|
105
106
|
</Card>
|
|
106
107
|
</div>
|
|
107
108
|
|
|
108
|
-
//
|
|
109
|
+
// Hover state
|
|
109
110
|
<button className="bg-bg-base hover:bg-bg-mute transition-colors">
|
|
110
|
-
|
|
111
|
+
Button
|
|
111
112
|
</button>
|
|
112
113
|
```
|
|
113
114
|
|
|
114
|
-
##
|
|
115
|
+
## What not to do
|
|
115
116
|
|
|
116
|
-
-
|
|
117
|
-
-
|
|
118
|
-
-
|
|
119
|
-
-
|
|
120
|
-
-
|
|
121
|
-
-
|
|
117
|
+
- Gradient backgrounds (`bg-gradient-to-*`)
|
|
118
|
+
- Saturated color over large areas (keep accents small)
|
|
119
|
+
- Expressing state with opacity (`/90`, `/80`, …) — use the dedicated semantic color
|
|
120
|
+
- Using `bg-primary-bg` on hover — prefer `bg-bg-mute`
|
|
121
|
+
- Using a raw palette color (`bg-teal-500`) directly — use a semantic token
|
|
122
|
+
- Simply inverting the colors for dark mode
|