@godxjp/ui 20.2.0 → 21.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.
Files changed (60) hide show
  1. package/dist/components/data-display/chat-bubble.d.ts +30 -0
  2. package/dist/components/data-display/chat-bubble.js +229 -0
  3. package/dist/components/data-display/descriptions.js +4 -1
  4. package/dist/components/data-display/index.d.ts +4 -2
  5. package/dist/components/data-display/index.js +5 -2
  6. package/dist/components/data-display/popover.js +3 -2
  7. package/dist/components/data-display/scroll-area.js +26 -1
  8. package/dist/components/data-display/tree.d.ts +8 -0
  9. package/dist/components/data-display/tree.js +426 -0
  10. package/dist/components/data-entry/chat-composer.d.ts +50 -0
  11. package/dist/components/data-entry/chat-composer.js +163 -0
  12. package/dist/components/data-entry/chat-suggestion.d.ts +28 -0
  13. package/dist/components/data-entry/chat-suggestion.js +285 -0
  14. package/dist/components/data-entry/index.d.ts +4 -0
  15. package/dist/components/data-entry/index.js +4 -0
  16. package/dist/components/data-entry/textarea.js +3 -1
  17. package/dist/components/data-entry/tree-utils.d.ts +10 -48
  18. package/dist/components/data-entry/tree-utils.js +1 -154
  19. package/dist/components/layout/org-switcher.js +22 -2
  20. package/dist/i18n/messages/en.json +50 -0
  21. package/dist/i18n/messages/ja.json +48 -0
  22. package/dist/i18n/messages/vi.json +49 -0
  23. package/dist/lib/tree.d.ts +53 -0
  24. package/dist/lib/tree.js +155 -0
  25. package/dist/props/components/data-display.prop.d.ts +187 -1
  26. package/dist/props/components/data-entry.prop.d.ts +127 -0
  27. package/dist/props/registry.d.ts +140 -3
  28. package/dist/props/registry.js +202 -3
  29. package/dist/styles/control.css +7 -0
  30. package/dist/styles/data-display-layout.css +309 -55
  31. package/dist/styles/data-entry-layout.css +64 -0
  32. package/dist/styles/shell-layout.css +41 -11
  33. package/dist/tokens/base.css +3 -0
  34. package/dist/tokens/components/chat-bubble.css +36 -0
  35. package/dist/tokens/components/chat-composer.css +19 -0
  36. package/dist/tokens/components/data-display.css +0 -6
  37. package/dist/tokens/components/descriptions.css +4 -0
  38. package/dist/tokens/components/shell.css +13 -3
  39. package/dist/tokens/components/tree.css +27 -0
  40. package/docs/FRAME-COVERAGE-LEDGER.md +1 -1
  41. package/docs/FRAME-COVERAGE-REPORT.md +8 -3
  42. package/docs/data-display/chat-bubble.tsx +397 -0
  43. package/docs/data-display/timeline.tsx +46 -0
  44. package/docs/data-display/tree.tsx +394 -0
  45. package/docs/data-entry/chat-composer.tsx +464 -0
  46. package/docs/data-entry/chat-suggestion.tsx +301 -0
  47. package/docs/roadmap/ai-chat-components.md +207 -0
  48. package/docs/roadmap/antd-parity.md +154 -0
  49. package/docs/roadmap/badge-tag-chip-count.md +172 -0
  50. package/docs/roadmap/list-masonry.md +159 -0
  51. package/docs/roadmap/parity-audit-data-display-feedback.md +567 -0
  52. package/docs/roadmap/parity-audit-data-entry.md +344 -0
  53. package/docs/roadmap/parity-audit-layout-navigation-general.md +464 -0
  54. package/docs/roadmap/parity-backlog.md +79 -0
  55. package/docs/roadmap/tree-components.md +151 -0
  56. package/docs/showcase/table-tree-rows.tsx +4 -4
  57. package/package.json +5 -3
  58. package/dist/components/data-display/tree-list.d.ts +0 -13
  59. package/dist/components/data-display/tree-list.js +0 -26
  60. package/docs/data-display/tree-list.tsx +0 -107
@@ -36,6 +36,7 @@
36
36
  --sidebar-gradient: none;
37
37
  --topbar-gradient: none;
38
38
 
39
+ --app-shell-viewport-inset: 0px;
39
40
  --app-shell-bar-height: var(--band-height-2xl);
40
41
 
41
42
  --app-shell-bar-inset: var(--space-page-x);
@@ -132,6 +133,10 @@
132
133
 
133
134
  --org-switcher-list-offset: 0px;
134
135
 
136
+ --org-switcher-legend-space-gap: var(--space-4);
137
+ --org-switcher-legend-font-size: var(--font-size-2xs);
138
+ --org-switcher-hint-space-gap: var(--space-1);
139
+
135
140
  --org-switcher-dialog-width: 26rem;
136
141
 
137
142
  --org-switcher-search-space-block: var(--space-3);
@@ -164,9 +169,14 @@
164
169
  --app-launcher-group-label-color: var(--muted-foreground);
165
170
  --app-launcher-state-min-height: 8rem;
166
171
 
167
- --app-launcher-launchpad-backdrop-blur-size: 28px;
168
- --app-launcher-launchpad-backdrop-alpha: 42%;
172
+ --app-launcher-launchpad-backdrop-background: initial;
173
+ --app-launcher-launchpad-backdrop-blur-size: 16px;
174
+ --app-launcher-launchpad-backdrop-alpha: 55%;
175
+
176
+ --app-launcher-launchpad-backdrop-color-saturate: 180%;
169
177
  --app-launcher-launchpad-space-inset: var(--space-8);
178
+
179
+ --app-launcher-launchpad-space-safe-area: 0;
170
180
  --app-launcher-launchpad-panel-max-width: 60rem;
171
181
  --app-launcher-launchpad-panel-gap: var(--space-8);
172
182
  --app-launcher-launchpad-grid-gap: var(--space-6);
@@ -184,7 +194,7 @@
184
194
 
185
195
  --app-launcher-launchpad-mark-background: color-mix(
186
196
  in srgb,
187
- hsl(var(--card)) 62%,
197
+ hsl(var(--card)) 78%,
188
198
  transparent
189
199
  );
190
200
  --app-launcher-launchpad-mark-border-color: color-mix(
@@ -0,0 +1,27 @@
1
+
2
+
3
+ :root {
4
+
5
+ --tree-node-height: initial;
6
+
7
+ --tree-indent-width: var(--space-5);
8
+
9
+ --tree-node-padding-inline: var(--space-2);
10
+ --tree-node-gap: var(--space-2);
11
+ --tree-node-radius: var(--radius-sm);
12
+ --tree-node-font-size: var(--font-size-sm);
13
+
14
+ --tree-switcher-size: 1.25rem;
15
+
16
+ --tree-switcher-color: initial;
17
+
18
+ --tree-icon-size: var(--icon-size-md);
19
+
20
+ --tree-line-color: initial;
21
+
22
+ --tree-node-hover-background: initial;
23
+ --tree-node-selected-background: initial;
24
+ --tree-node-selected-foreground: initial;
25
+
26
+ --tree-loading-bar-inline-size: 40%;
27
+ }
@@ -112,7 +112,7 @@ The executed geometry sweep (`pnpm check:frame-geometry`, `scripts/frame-geometr
112
112
 
113
113
  ## Known gaps
114
114
 
115
- The issue's "Initial known gaps to encode" are locked in code (`KNOWN_GAPS` in `scripts/frame-coverage-contract.mjs`) and mirrored into `policy.knownGaps`: Button, Card, Card subcomponents, EmptyState, Progress, Carousel, DataTable, Charts and the Timeline/TreeList/PasswordStrength locale stress. They cannot be deleted, and they cannot be waived through `policy.notApplicable` — they must be **covered** or stay **UNTESTED**.
115
+ The issue's "Initial known gaps to encode" are locked in code (`KNOWN_GAPS` in `scripts/frame-coverage-contract.mjs`) and mirrored into `policy.knownGaps`: Button, Card, Card subcomponents, EmptyState, Progress, Carousel, DataTable, Charts and the Timeline/PasswordStrength locale stress. They cannot be deleted, and they cannot be waived through `policy.notApplicable` — they must be **covered** or stay **UNTESTED**.
116
116
 
117
117
  ## Current state — read this before quoting coverage
118
118
 
@@ -3,8 +3,8 @@
3
3
  > Generated by `scripts/frame-coverage.mjs` (issue #163). Do not edit by hand — run `pnpm check:frame-coverage`.
4
4
  > Standard: [FRAME-COVERAGE-STANDARD.md](./FRAME-COVERAGE-STANDARD.md). A missing dimension is **UNTESTED**, never a pass.
5
5
 
6
- - Public components: **137**
7
- - With ≥1 frame: **133** · zero-frame: **4**
6
+ - Public components: **141**
7
+ - With ≥1 frame: **136** · zero-frame: **5**
8
8
  - Fully covered (every contract axis declared): **0**
9
9
 
10
10
  ## UNTESTED — no `/frame/**` route
@@ -13,6 +13,7 @@
13
13
  - [ ] `CardBar`
14
14
  - [ ] `VisuallyHidden`
15
15
  - [ ] `RangeTimeline`
16
+ - [ ] `ChatBubbleList`
16
17
 
17
18
  ## Per-component contract axes
18
19
 
@@ -113,7 +114,7 @@ Legend: ✓ covered · N/A reasoned skip · · UNTESTED (blank cell = UNTESTED).
113
114
  | Popover | ✓ | N/A | N/A | · | · | · | · | · | · | · |
114
115
  | ScrollArea | ✓ | N/A | N/A | N/A | · | · | · | · | · | · |
115
116
  | Collapsible | ✓ | N/A | · | · | · | · | · | · | · | · |
116
- | TreeList | ✓ | N/A | N/A | N/A | · | · | · | · | · | · |
117
+ | Tree | ✓ | · | · | · | · | · | · | · | · | · |
117
118
  | Tooltip | ✓ | N/A | N/A | · | · | · | · | · | · | · |
118
119
  | PrefetchLink | ✓ | N/A | N/A | N/A | · | · | · | · | · | · |
119
120
  | Avatar | ✓ | ✓ | N/A | N/A | · | · | · | · | · | · |
@@ -157,4 +158,8 @@ Legend: ✓ covered · N/A reasoned skip · · UNTESTED (blank cell = UNTESTED).
157
158
  | PermissionMatrix | ✓ | N/A | · | N/A | · | · | · | · | · | · |
158
159
  | BranchScopePicker | ✓ | N/A | · | · | · | · | · | · | · | · |
159
160
  | ServiceRolePanel | ✓ | N/A | · | · | · | · | · | · | · | · |
161
+ | ChatBubble | ✓ | · | · | N/A | · | · | · | · | · | · |
162
+ | ChatBubbleList | **✗** | N/A | N/A | N/A | · | · | · | · | · | · |
163
+ | ChatComposer | ✓ | · | · | · | · | · | · | · | · | · |
164
+ | ChatSuggestion | ✓ | N/A | N/A | · | · | · | · | · | · | · |
160
165
 
@@ -0,0 +1,397 @@
1
+ import * as React from "react";
2
+ import { Bot, RotateCcw, User } from "lucide-react";
3
+ import {
4
+ Avatar,
5
+ Card,
6
+ CardContent,
7
+ CardDescription,
8
+ CardFooter,
9
+ CardHeader,
10
+ CardTitle,
11
+ ChatBubble,
12
+ ChatBubbleList,
13
+ type ChatBubbleProp,
14
+ type ChatMessageProp,
15
+ } from "@godxjp/ui/data-display";
16
+ import { formatAppTime } from "@godxjp/ui/datetime";
17
+ import { Button, Text } from "@godxjp/ui/general";
18
+ import { Flex, PageContainer, ResponsiveGrid } from "@godxjp/ui/layout";
19
+
20
+ /**
21
+ * ChatBubble · ChatBubbleList — 会話の1発言と、その発言が流れるフィード。
22
+ *
23
+ * ChatBubbleList が持っているのは「一番下に貼りつく」挙動です。読み手がすでに最下部にいる間だけ
24
+ * 新着に追従し、履歴を読み返そうとスクロールを上げた瞬間に追従をやめて、戻るためのボタンを出しま
25
+ * す。読んでいる途中で最下部へ引き戻すのは、機械が勝手に文脈を変える操作(WCAG 3.2.5)です。
26
+ *
27
+ * 入力欄は ChatComposer(data-entry)が担当します。このページはフィード側だけを扱います。
28
+ */
29
+
30
+ const ASSISTANT_AVATAR = (
31
+ <Avatar aria-hidden="true" appearance="tinted" shape="square">
32
+ <Bot />
33
+ </Avatar>
34
+ );
35
+
36
+ const USER_AVATAR = (
37
+ <Avatar aria-hidden="true" shape="square">
38
+ <User />
39
+ </Avatar>
40
+ );
41
+
42
+ /** 1つのフィードの見た目を1か所で決める。各メッセージ側の指定が常に優先されます。 */
43
+ const ROLES: Record<string, Partial<ChatBubbleProp>> = {
44
+ assistant: {
45
+ placement: "start",
46
+ variant: "filled",
47
+ header: "アシスタント",
48
+ avatar: ASSISTANT_AVATAR,
49
+ },
50
+ user: {
51
+ placement: "end",
52
+ variant: "outlined",
53
+ header: "自分",
54
+ avatar: USER_AVATAR,
55
+ },
56
+ system: {
57
+ placement: "start",
58
+ variant: "borderless",
59
+ tone: "info",
60
+ size: "sm",
61
+ header: "システム",
62
+ },
63
+ };
64
+
65
+ /**
66
+ * Times are formatted INSIDE the component, not at module scope: formatAppTime reads the
67
+ * AppProvider's locale and IANA timezone, and at module-evaluation time that context has not been
68
+ * synced yet — a module-level call silently formats against the pre-provider default and drifts
69
+ * from every other time on the page.
70
+ */
71
+ const AT = (hhmm: string) => formatAppTime(`2026-09-10T${hhmm}:00+07:00`);
72
+
73
+ const conversation = (): ChatMessageProp[] => [
74
+ {
75
+ id: "c1",
76
+ role: "assistant",
77
+ content: "おはようございます。今日は何をお手伝いしましょうか。",
78
+ footer: AT("09:02"),
79
+ },
80
+ {
81
+ id: "c2",
82
+ role: "user",
83
+ content: "先週受領した請求書のうち、まだ承認されていないものを一覧にしてください。",
84
+ footer: AT("09:03"),
85
+ },
86
+ {
87
+ id: "c3",
88
+ role: "system",
89
+ content: "検索ツールを実行しました(対象 128 件 / 該当 7 件)。",
90
+ },
91
+ {
92
+ id: "c4",
93
+ role: "assistant",
94
+ content:
95
+ "未承認の請求書は 7 件です。合計 1,284,000 円。\n最も古いものは 9月1日受領の株式会社青葉商事 分で、支払期限まであと 3 日です。",
96
+ footer: AT("09:03"),
97
+ typing: { step: 2, interval: 40 },
98
+ },
99
+ {
100
+ id: "c5",
101
+ role: "user",
102
+ content: "その 7 件を承認待ちのまま経理へ回してください。",
103
+ footer: AT("09:05"),
104
+ },
105
+ {
106
+ id: "c6",
107
+ role: "assistant",
108
+ loading: true,
109
+ },
110
+ ];
111
+
112
+ /** 上へ戻って読み返せるだけの長さがあるフィード。追従が切れる様子を実際に確かめる用。 */
113
+ const LONG_THREAD: ChatMessageProp[] = Array.from({ length: 24 }, (_, index) => ({
114
+ id: `t${String(index)}`,
115
+ role: index % 2 === 0 ? "assistant" : "user",
116
+ content:
117
+ index % 2 === 0
118
+ ? `${String(index + 1)} 件目の集計が終わりました。差異は見つかりません。`
119
+ : `${String(index + 1)} 件目もお願いします。`,
120
+ }));
121
+
122
+ const RTL_THREAD: ChatMessageProp[] = [
123
+ { id: "r1", role: "assistant", content: "كيف يمكنني المساعدة؟" },
124
+ { id: "r2", role: "user", content: "لخّص فواتير الأسبوع الماضي." },
125
+ ];
126
+
127
+ const LTR_THREAD: ChatMessageProp[] = [
128
+ { id: "l1", role: "assistant", content: "How can I help?" },
129
+ { id: "l2", role: "user", content: "Summarise last week's invoices." },
130
+ ];
131
+
132
+ const ARCHIVE: ChatMessageProp[] = Array.from({ length: 12 }, (_, index) => ({
133
+ id: `a${String(index)}`,
134
+ role: index % 2 === 0 ? "user" : "assistant",
135
+ content:
136
+ index % 2 === 0
137
+ ? `${String(index + 1)} 番目の質問です。`
138
+ : `${String(index + 1)} 番目の回答です。記録として保存されています。`,
139
+ }));
140
+
141
+ export default function Demo() {
142
+ // 「新しい会話」を押すと typing がもう一度先頭から流れる。id が変わる = 別の発言。
143
+ const [run, setRun] = React.useState(0);
144
+ const live = React.useMemo(
145
+ () => conversation().map((m) => (m.id === "c4" ? { ...m, id: `c4-${String(run)}` } : m)),
146
+ [run],
147
+ );
148
+
149
+ return (
150
+ <PageContainer
151
+ title="ChatBubble · ChatBubbleList"
152
+ subtitle="会話の1発言とフィード · 最下部への貼りつき · typing / loading · placement は論理軸"
153
+ >
154
+ <Flex direction="col" gap="lg">
155
+ <Card>
156
+ <CardHeader>
157
+ <CardTitle level={2}>アシスタントの会話</CardTitle>
158
+ <CardDescription>
159
+ roles で assistant / user / system の既定値を1か所に書き、items は中身だけを持ちま
160
+ す。最後の発言は loading(Skeleton と aria-busy、読み上げ用の文言つき)、その1つ前は
161
+ typing で1文字ずつ流れます。prefers-reduced-motion: reduce の環境では、アニメーショ
162
+ ンは動かず全文がそのまま表示されます。時刻は formatAppTime(Intl.DateTimeFormat と
163
+ AppProvider のタイムゾーン)で整形しています。
164
+ </CardDescription>
165
+ </CardHeader>
166
+ <CardContent>
167
+ <Card variant="outline">
168
+ <CardContent flush>
169
+ <ChatBubbleList
170
+ className="h-96"
171
+ label="アシスタントとの会話"
172
+ items={live}
173
+ roles={ROLES}
174
+ />
175
+ </CardContent>
176
+ </Card>
177
+ </CardContent>
178
+ <CardFooter>
179
+ <Flex gap="sm" wrap>
180
+ <Button type="button" variant="outline" onClick={() => setRun((n) => n + 1)}>
181
+ <RotateCcw aria-hidden="true" />
182
+ 返答をもう一度流す
183
+ </Button>
184
+ <Text size="xs" tone="muted">
185
+ 入力欄は ChatComposer(data-entry)が担当します。
186
+ </Text>
187
+ </Flex>
188
+ </CardFooter>
189
+ </Card>
190
+
191
+ <Card>
192
+ <CardHeader>
193
+ <CardTitle level={2}>最下部への貼りつきと、戻るためのボタン</CardTitle>
194
+ <CardDescription>
195
+ 下のフィードを少しだけ上へスクロールしてください。追従がその場で止まり、
196
+ フィードの下端に「最新へ移動」ボタンが出ます。離れている間に届いた件数は
197
+ Intl.PluralRules で数えて文言に入ります。ボタンはフォーカスできる本物のボタンなの
198
+ で、キーボードだけでも最新へ戻れます。自動で引き戻すことは一度もありません。
199
+ </CardDescription>
200
+ </CardHeader>
201
+ <CardContent>
202
+ <Card variant="outline">
203
+ <CardContent flush>
204
+ <ChatBubbleList
205
+ className="h-64"
206
+ label="集計の進捗"
207
+ items={LONG_THREAD}
208
+ roles={ROLES}
209
+ />
210
+ </CardContent>
211
+ </Card>
212
+ </CardContent>
213
+ </Card>
214
+
215
+ <Card>
216
+ <CardHeader>
217
+ <CardTitle level={2}>
218
+ placement は論理軸(dir=&quot;rtl&quot; で自動的に反転)
219
+ </CardTitle>
220
+ <CardDescription>
221
+ 同じ items と同じ roles を、左が dir=&quot;ltr&quot;、右が dir=&quot;rtl&quot; で描
222
+ いています。placement=&quot;end&quot; の自分の発言は、ltr では右、rtl では左に寄り
223
+ ます。ロケールで分岐したコードは1行もありません。
224
+ </CardDescription>
225
+ </CardHeader>
226
+ <CardContent>
227
+ <ResponsiveGrid columns={{ base: 1, md: 2 }} gap="md">
228
+ <Card variant="outline">
229
+ <CardContent flush>
230
+ <div dir="ltr">
231
+ <ChatBubbleList
232
+ className="h-48"
233
+ label="ltr conversation"
234
+ items={LTR_THREAD}
235
+ roles={ROLES}
236
+ />
237
+ </div>
238
+ </CardContent>
239
+ </Card>
240
+ <Card variant="outline">
241
+ <CardContent flush>
242
+ <div dir="rtl">
243
+ <ChatBubbleList
244
+ className="h-48"
245
+ label="محادثة"
246
+ items={RTL_THREAD}
247
+ roles={ROLES}
248
+ />
249
+ </div>
250
+ </CardContent>
251
+ </Card>
252
+ </ResponsiveGrid>
253
+ </CardContent>
254
+ </Card>
255
+
256
+ <Card>
257
+ <CardHeader>
258
+ <CardTitle level={2}>variant · 3つすべて</CardTitle>
259
+ <CardDescription>
260
+ variant は構造の軸です。filled は既定の面、outlined は 1px の枠だけ、borderless は
261
+ 面も枠も持たず本文としてそのまま読ませます。色で状態を示すのは tone の役目で、 variant
262
+ には状態を表す値がありません。Ant Design X の shadow は、このデザインシステ
263
+ ムが影を使わないため意図的にありません。
264
+ </CardDescription>
265
+ </CardHeader>
266
+ <CardContent>
267
+ <Flex direction="col" gap="md">
268
+ <ChatBubble variant="filled" header="アシスタント" avatar={ASSISTANT_AVATAR}>
269
+ filled · 既定の面。相手の発言に使います。
270
+ </ChatBubble>
271
+ <ChatBubble variant="outlined" header="アシスタント" avatar={ASSISTANT_AVATAR}>
272
+ outlined · 面を持たず 1px の枠だけ。
273
+ </ChatBubble>
274
+ <ChatBubble variant="borderless" header="アシスタント" avatar={ASSISTANT_AVATAR}>
275
+ borderless · 長い回答を本文として読ませたいとき。面も枠もありません。
276
+ </ChatBubble>
277
+ <ChatBubble variant="filled" placement="end" header="自分" avatar={USER_AVATAR}>
278
+ filled · placement=&quot;end&quot; は自分の発言用の色を持ちます。
279
+ </ChatBubble>
280
+ </Flex>
281
+ </CardContent>
282
+ </Card>
283
+
284
+ <Card>
285
+ <CardHeader>
286
+ <CardTitle level={2}>size · xs / sm / md / lg</CardTitle>
287
+ <CardDescription>
288
+ size は文字の段と内側の余白を同時に動かします。既定は md。&quot;default&quot; という
289
+ 値はありません。
290
+ </CardDescription>
291
+ </CardHeader>
292
+ <CardContent>
293
+ <Flex direction="col" gap="md">
294
+ <ChatBubble size="xs" header="アシスタント" avatar={ASSISTANT_AVATAR}>
295
+ xs · 監査ログのように密度を上げたいとき。
296
+ </ChatBubble>
297
+ <ChatBubble size="sm" header="アシスタント" avatar={ASSISTANT_AVATAR}>
298
+ sm · システムの注記など、会話の脇に置くもの。
299
+ </ChatBubble>
300
+ <ChatBubble size="md" header="アシスタント" avatar={ASSISTANT_AVATAR}>
301
+ md · 既定。ふつうの会話。
302
+ </ChatBubble>
303
+ <ChatBubble size="lg" header="アシスタント" avatar={ASSISTANT_AVATAR}>
304
+ lg · 画面いっぱいのアシスタント面で、長文を読ませるとき。
305
+ </ChatBubble>
306
+ </Flex>
307
+ </CardContent>
308
+ </Card>
309
+
310
+ <Card>
311
+ <CardHeader>
312
+ <CardTitle level={2}>tone · 色だけで状態を伝えない</CardTitle>
313
+ <CardDescription>
314
+ tone は会話そのものではない発言(送信失敗、上限の警告、ツールの結果)のための状態
315
+ です。Alert と同じ役割色を薄く敷き、同時に読み上げ用の状態語(情報 / 成功 / 警告 /
316
+ エラー)を必ず入れます。色を見分けられない読み手にも状態が届きます。
317
+ </CardDescription>
318
+ </CardHeader>
319
+ <CardContent>
320
+ <Flex direction="col" gap="md">
321
+ <ChatBubble header="アシスタント" avatar={ASSISTANT_AVATAR}>
322
+ default · 状態なし。ふつうの発言です。
323
+ </ChatBubble>
324
+ <ChatBubble tone="info" variant="outlined" size="sm" header="システム">
325
+ info · モデルを gpt-5 に切り替えました。
326
+ </ChatBubble>
327
+ <ChatBubble tone="success" variant="outlined" size="sm" header="システム">
328
+ success · 7 件の請求書を経理へ回しました。
329
+ </ChatBubble>
330
+ <ChatBubble tone="warning" variant="outlined" size="sm" header="システム">
331
+ warning · 今月の利用上限の 90% に達しています。
332
+ </ChatBubble>
333
+ <ChatBubble tone="destructive" placement="end" header="自分" avatar={USER_AVATAR}>
334
+ destructive · 送信できませんでした。もう一度お試しください。
335
+ </ChatBubble>
336
+ </Flex>
337
+ </CardContent>
338
+ </Card>
339
+
340
+ <Card>
341
+ <CardHeader>
342
+ <CardTitle level={2}>loading と typing</CardTitle>
343
+ <CardDescription>
344
+ loading は「返答を頼んだが、まだ届いていない」状態です。Skeleton の棒は装飾なので
345
+ 支援技術からは隠し、代わりに aria-busy と読み上げ用の1行を置きます。typing は文字列
346
+ の本文だけを流します(ReactNode には流す単位がないので、そのまま全部出ます)。
347
+ 流れている間の途中の文字は読み上げ対象から外れ、完成した本文だけが1回だけ読み上げ
348
+ られます。
349
+ </CardDescription>
350
+ </CardHeader>
351
+ <CardContent>
352
+ <Flex direction="col" gap="md">
353
+ <ChatBubble header="アシスタント" avatar={ASSISTANT_AVATAR} loading />
354
+ <ChatBubble
355
+ key={`typing-${String(run)}`}
356
+ header="アシスタント"
357
+ avatar={ASSISTANT_AVATAR}
358
+ typing={{ step: 1, interval: 60 }}
359
+ footer={AT("09:12")}
360
+ >
361
+ typing · 1文字ずつ流れます。上の「返答をもう一度流す」でやり直せます。
362
+ </ChatBubble>
363
+ <ChatBubble header="アシスタント" avatar={ASSISTANT_AVATAR} typing>
364
+ <Text size="sm">
365
+ ReactNode の本文。typing を渡しても流れず、そのまま表示されます。
366
+ </Text>
367
+ </ChatBubble>
368
+ </Flex>
369
+ </CardContent>
370
+ </Card>
371
+
372
+ <Card>
373
+ <CardHeader>
374
+ <CardTitle level={2}>autoScroll=&#123;false&#125; · 読み終えた記録</CardTitle>
375
+ <CardDescription>
376
+ 保管された記録を引用から開くときは、最新の発言に飛ばされたくありません。autoScroll
377
+ を切ると、フィードは先頭のまま開き、新しい要素が増えても動きません。
378
+ </CardDescription>
379
+ </CardHeader>
380
+ <CardContent>
381
+ <Card variant="outline">
382
+ <CardContent flush>
383
+ <ChatBubbleList
384
+ className="h-64"
385
+ label="保管された会話"
386
+ items={ARCHIVE}
387
+ roles={ROLES}
388
+ autoScroll={false}
389
+ />
390
+ </CardContent>
391
+ </Card>
392
+ </CardContent>
393
+ </Card>
394
+ </Flex>
395
+ </PageContainer>
396
+ );
397
+ }
@@ -9,6 +9,7 @@ import {
9
9
  RangeTimeline,
10
10
  type TimelineItem,
11
11
  } from "@godxjp/ui/data-display";
12
+ import { Text } from "@godxjp/ui/general";
12
13
  import { Flex, PageContainer } from "@godxjp/ui/layout";
13
14
 
14
15
  /**
@@ -53,6 +54,37 @@ const approvalItems: TimelineItem[] = [
53
54
  { title: "消費税を計上", location: "経理", status: "pending" },
54
55
  ];
55
56
 
57
+ // Fourth shape: `title` is a NODE WITH A WIDTH OF ITS OWN, which is the only shape that can
58
+ // show whether the title actually spans the row. Every case above passes a bare string, and a
59
+ // string is already narrower than the row, so a title that shrink-wraps looks identical to one
60
+ // that fills — the defect hides in plain sight. A Card as the title makes the width visible; the
61
+ // truncating id is the other half, a title whose intrinsic width EXCEEDS the row, which must
62
+ // shrink rather than push `time` off the end. Measured by check:timeline-title-fill.
63
+ const richItems: TimelineItem[] = [
64
+ {
65
+ title: (
66
+ <Card>
67
+ <CardContent>
68
+ <Text weight="medium">請求書 INV-2026-0912 を承認しました</Text>
69
+ </CardContent>
70
+ </Card>
71
+ ),
72
+ time: "11:05",
73
+ status: "done",
74
+ },
75
+ {
76
+ title: (
77
+ <Text truncate mono>
78
+ INV-2026-09-0001-APPROVAL-CHAIN-SEGMENT-0007-RECONCILIATION
79
+ </Text>
80
+ ),
81
+ // A full CJK timestamp on purpose: it breaks between any two glyphs, so nothing floors its
82
+ // width and a title with an `auto` flex-basis would squeeze it onto three lines.
83
+ time: "2026年9月12日 11:18",
84
+ status: "current",
85
+ },
86
+ ];
87
+
56
88
  export default function Demo() {
57
89
  const { t, locale } = useTranslation();
58
90
  const dayLabel = (day: number) =>
@@ -113,6 +145,20 @@ export default function Demo() {
113
145
  <Timeline variant="status" items={approvalItems} />
114
146
  </CardContent>
115
147
  </Card>
148
+ <Card>
149
+ <CardHeader>
150
+ <CardTitle level={2}>タイトルにノードを渡す</CardTitle>
151
+ <CardDescription>
152
+ title は ReactNode です。自身の幅を持つノード(Card
153
+ など)を渡しても行いっぱいに広がり、内容が行より広い場合は time
154
+ を押し出さずに切り詰められます。
155
+ </CardDescription>
156
+ </CardHeader>
157
+ <CardContent>
158
+ <Timeline variant="status" items={richItems} />
159
+ </CardContent>
160
+ </Card>
161
+
116
162
  <Card>
117
163
  <CardHeader>
118
164
  <CardTitle level={2}>RangeTimeline</CardTitle>