@godxjp/ui 23.2.1 → 23.4.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 (101) hide show
  1. package/dist/app/timezones.js +2 -1
  2. package/dist/components/charts/chart-cartesian.js +2 -4
  3. package/dist/components/charts/chart-frame.js +2 -1
  4. package/dist/components/data-display/card.d.ts +87 -1
  5. package/dist/components/data-display/card.js +88 -15
  6. package/dist/components/data-display/index.d.ts +5 -1
  7. package/dist/components/data-display/index.js +5 -0
  8. package/dist/components/data-display/thought-chain.d.ts +42 -0
  9. package/dist/components/data-display/thought-chain.js +177 -0
  10. package/dist/components/data-display/welcome.d.ts +26 -0
  11. package/dist/components/data-display/welcome.js +37 -0
  12. package/dist/components/data-entry/attachments.d.ts +19 -0
  13. package/dist/components/data-entry/attachments.js +322 -0
  14. package/dist/components/data-entry/date-picker.js +12 -14
  15. package/dist/components/data-entry/index.d.ts +2 -0
  16. package/dist/components/data-entry/index.js +2 -0
  17. package/dist/components/data-entry/number-input.js +2 -1
  18. package/dist/components/feedback/dialog.d.ts +10 -7
  19. package/dist/components/feedback/dialog.js +40 -17
  20. package/dist/components/feedback/index.d.ts +3 -3
  21. package/dist/components/feedback/index.js +2 -0
  22. package/dist/components/feedback/skeleton.d.ts +11 -2
  23. package/dist/components/feedback/skeleton.js +8 -0
  24. package/dist/components/general/actions.d.ts +59 -0
  25. package/dist/components/general/actions.js +253 -0
  26. package/dist/components/general/activity.js +1 -0
  27. package/dist/components/general/button.js +2 -1
  28. package/dist/components/general/float-button.d.ts +41 -0
  29. package/dist/components/general/float-button.js +336 -0
  30. package/dist/components/general/index.d.ts +6 -2
  31. package/dist/components/general/index.js +12 -1
  32. package/dist/components/general/typography.d.ts +79 -4
  33. package/dist/components/general/typography.js +598 -54
  34. package/dist/components/layout/draggable-panel.d.ts +18 -0
  35. package/dist/components/layout/draggable-panel.js +189 -0
  36. package/dist/components/layout/error-surface.js +3 -2
  37. package/dist/components/layout/index.d.ts +2 -0
  38. package/dist/components/layout/index.js +2 -0
  39. package/dist/components/navigation/conversations.d.ts +39 -0
  40. package/dist/components/navigation/conversations.js +371 -0
  41. package/dist/components/navigation/index.d.ts +2 -0
  42. package/dist/components/navigation/index.js +2 -0
  43. package/dist/components/ui/card.js +1 -0
  44. package/dist/components/ui/toggle.js +2 -1
  45. package/dist/i18n/messages/en.json +53 -0
  46. package/dist/i18n/messages/ja.json +53 -0
  47. package/dist/i18n/messages/vi.json +53 -0
  48. package/dist/i18n/translate.js +3 -2
  49. package/dist/lib/datetime/picker-format.js +2 -1
  50. package/dist/lib/format.js +3 -2
  51. package/dist/lib/intl-cache.d.ts +32 -0
  52. package/dist/lib/intl-cache.js +29 -0
  53. package/dist/props/components/data-display.prop.d.ts +143 -1
  54. package/dist/props/components/data-entry.prop.d.ts +87 -0
  55. package/dist/props/components/feedback.prop.d.ts +53 -1
  56. package/dist/props/components/general.prop.d.ts +440 -3
  57. package/dist/props/components/index.d.ts +2 -2
  58. package/dist/props/components/layout.prop.d.ts +63 -1
  59. package/dist/props/components/navigation.prop.d.ts +150 -0
  60. package/dist/props/registry.d.ts +431 -0
  61. package/dist/props/registry.js +551 -0
  62. package/dist/props/vocabulary/index.d.ts +1 -1
  63. package/dist/props/vocabulary/interaction.prop.d.ts +113 -0
  64. package/dist/styles/alert-layout.css +22 -0
  65. package/dist/styles/card-layout.css +34 -0
  66. package/dist/styles/core.css +1 -0
  67. package/dist/styles/data-display-layout.css +236 -0
  68. package/dist/styles/float-button-layout.css +168 -0
  69. package/dist/styles/form-layout.css +11 -1
  70. package/dist/styles/index.css +1 -0
  71. package/dist/styles/layout.css +196 -0
  72. package/dist/styles/navigation-layout.css +139 -0
  73. package/dist/styles/text-layout.css +156 -0
  74. package/dist/tokens/axes.css +5 -0
  75. package/dist/tokens/base.css +7 -0
  76. package/dist/tokens/components/actions.css +14 -0
  77. package/dist/tokens/components/attachments.css +15 -0
  78. package/dist/tokens/components/conversations.css +28 -0
  79. package/dist/tokens/components/draggable-panel.css +25 -0
  80. package/dist/tokens/components/float-button.css +39 -0
  81. package/dist/tokens/components/text.css +13 -0
  82. package/dist/tokens/components/thought-chain.css +31 -0
  83. package/dist/tokens/components/welcome.css +19 -0
  84. package/dist/tokens/foundation.css +6 -4
  85. package/docs/DESIGN-AUTHORITY.md +133 -20
  86. package/docs/FRAME-COVERAGE-REPORT.md +5 -2
  87. package/docs/data-display/card/examples/tab-list.tsx +97 -0
  88. package/docs/data-display/thought-chain.tsx +176 -0
  89. package/docs/data-display/welcome.tsx +115 -0
  90. package/docs/data-entry/attachments.tsx +160 -0
  91. package/docs/feedback/dialog.tsx +50 -0
  92. package/docs/general/actions.tsx +175 -0
  93. package/docs/general/float-button.tsx +133 -0
  94. package/docs/general/typography.tsx +63 -1
  95. package/docs/layout/draggable-panel.tsx +127 -0
  96. package/docs/navigation/conversations.tsx +176 -0
  97. package/docs/roadmap/parity-audit-layout-navigation-general.md +17 -16
  98. package/package.json +20 -4
  99. package/scripts/_agent-setup.mjs +36 -5
  100. package/scripts/guinea-pig-skill.md +14 -0
  101. package/scripts/ui-audit.mjs +69 -9
@@ -0,0 +1,115 @@
1
+ import { useState } from "react";
2
+
3
+ import { Welcome } from "@godxjp/ui/data-display";
4
+ import { Button, Text } from "@godxjp/ui/general";
5
+ import {
6
+ AppShell,
7
+ Flex,
8
+ PageContainer,
9
+ Sidebar,
10
+ type SidebarSectionProp,
11
+ Topbar,
12
+ } from "@godxjp/ui/layout";
13
+ import { Bot, MessageSquare, Settings, Sparkles, Users } from "lucide-react";
14
+
15
+ /**
16
+ * Welcome — the greeting block at the head of an empty conversation (Ant Design X `Welcome`).
17
+ *
18
+ * It is the first thing an assistant says, so every slot on this page is one Ant X slot at rest:
19
+ * the full block, the two variants, the block with no glyph, and the `icon`-as-URL rule that Ant
20
+ * ports from a string beginning with `http`.
21
+ *
22
+ * Composed only from real @godxjp/ui components.
23
+ */
24
+ const sections: SidebarSectionProp[] = [
25
+ {
26
+ label: "アシスタント",
27
+ items: [
28
+ { id: "chat", label: "チャット", icon: MessageSquare },
29
+ { id: "agents", label: "エージェント", icon: Bot },
30
+ { id: "members", label: "メンバー", icon: Users },
31
+ ],
32
+ },
33
+ { label: "管理", items: [{ id: "settings", label: "設定", icon: Settings }] },
34
+ ];
35
+
36
+ export default function WelcomeDoc() {
37
+ const [dismissed, setDismissed] = useState(false);
38
+
39
+ return (
40
+ <AppShell
41
+ sidebar={
42
+ <Sidebar
43
+ activeId="chat"
44
+ sections={sections}
45
+ onSelect={() => {}}
46
+ product={{ name: "CoreDesk", role: "アシスタント", color: "hsl(var(--primary))" }}
47
+ />
48
+ }
49
+ topbar={<Topbar />}
50
+ >
51
+ <PageContainer
52
+ title="あいさつ"
53
+ subtitle="Welcome · 会話がまだ一件もない画面の先頭に置く四つのスロット"
54
+ >
55
+ {/* THE WHOLE BLOCK. `extra` sits on the title row, which is the placement a hand-roll
56
+ usually gets wrong (it lands under the description instead). */}
57
+ <Flex direction="col" gap="sm">
58
+ <Text size="sm" weight="medium">
59
+ icon · title · description · extra
60
+ </Text>
61
+ {dismissed ? (
62
+ <Text size="xs" tone="muted">
63
+ 閉じました。
64
+ </Text>
65
+ ) : (
66
+ <Welcome
67
+ icon={<Sparkles />}
68
+ title="こんにちは、何からはじめますか"
69
+ description="請求、経費、勤怠のことならお手伝いできます。左の履歴から続きを開くこともできます。"
70
+ extra={
71
+ <Button variant="ghost" size="sm" onClick={() => setDismissed(true)}>
72
+ 閉じる
73
+ </Button>
74
+ }
75
+ />
76
+ )}
77
+ </Flex>
78
+
79
+ {/* THE TWO VARIANTS, side by side. */}
80
+ <Flex direction="col" gap="sm">
81
+ <Text size="sm" weight="medium">
82
+ variant · filled と borderless
83
+ </Text>
84
+ <Welcome
85
+ variant="filled"
86
+ icon={<Bot />}
87
+ title="filled"
88
+ description="自分の地色と髪の毛一本分の境界を持つ。カードの中に置いても沈まない。"
89
+ />
90
+ <Welcome
91
+ variant="borderless"
92
+ icon={<Bot />}
93
+ title="borderless"
94
+ description="ページの地の上にそのまま置く。上下に他の要素が続くときはこちら。"
95
+ />
96
+ </Flex>
97
+
98
+ {/* NO GLYPH, AND AN IMAGE GLYPH. */}
99
+ <Flex direction="col" gap="sm">
100
+ <Text size="sm" weight="medium">
101
+ icon · 省略と URL
102
+ </Text>
103
+ <Welcome
104
+ title="アイコンなし"
105
+ description="スロットを渡さなければ、その列自体が出ない。"
106
+ />
107
+ <Text size="xs" tone="muted">
108
+ http で始まる文字列は装飾画像として描かれる(Ant Design X と同じ規則、ただし alt
109
+ は空)。
110
+ </Text>
111
+ </Flex>
112
+ </PageContainer>
113
+ </AppShell>
114
+ );
115
+ }
@@ -0,0 +1,160 @@
1
+ import { useState } from "react";
2
+
3
+ import { Attachments, type AttachmentsItemProp } from "@godxjp/ui/data-entry";
4
+ import { Text } from "@godxjp/ui/general";
5
+ import {
6
+ AppShell,
7
+ Flex,
8
+ PageContainer,
9
+ Sidebar,
10
+ type SidebarSectionProp,
11
+ Topbar,
12
+ } from "@godxjp/ui/layout";
13
+ import { Bot, MessageSquare, Paperclip, Settings, Users } from "lucide-react";
14
+
15
+ /**
16
+ * Attachments — the file collection that rides a chat surface (Ant Design X `Attachments`).
17
+ *
18
+ * Last of the #559 series. Every slot on this page is one Ant X axis at rest: the empty
19
+ * placeholder, a settled list, the three `overflow` modes, an upload in flight, a failed upload,
20
+ * and the disabled strip.
21
+ *
22
+ * The field names are antd's (`uid`, `name`, `size`, `status`, `percent`, `thumbUrl`,
23
+ * `originFileObj`) so an Ant X call site compiles here unchanged.
24
+ *
25
+ * Composed only from real @godxjp/ui components.
26
+ */
27
+ const sections: SidebarSectionProp[] = [
28
+ {
29
+ label: "アシスタント",
30
+ items: [
31
+ { id: "chat", label: "チャット", icon: MessageSquare },
32
+ { id: "agents", label: "エージェント", icon: Bot },
33
+ { id: "members", label: "メンバー", icon: Users },
34
+ ],
35
+ },
36
+ { label: "管理", items: [{ id: "settings", label: "設定", icon: Settings }] },
37
+ ];
38
+
39
+ const settled: AttachmentsItemProp[] = [
40
+ { uid: "1", name: "請求書_2026-03.pdf", size: 182_400, status: "done" },
41
+ { uid: "2", name: "経費明細.xlsx", size: 44_120, status: "done" },
42
+ ];
43
+
44
+ const inFlight: AttachmentsItemProp[] = [
45
+ { uid: "3", name: "会議録画.mp4", size: 78_300_000, status: "uploading", percent: 42 },
46
+ ];
47
+
48
+ const failed: AttachmentsItemProp[] = [
49
+ { uid: "4", name: "契約書_原本.pdf", size: 2_400_000, status: "error" },
50
+ ];
51
+
52
+ const many: AttachmentsItemProp[] = Array.from({ length: 6 }, (_, i) => ({
53
+ uid: `m${i}`,
54
+ name: `添付_${i + 1}.pdf`,
55
+ size: 12_000 * (i + 1),
56
+ status: "done" as const,
57
+ }));
58
+
59
+ export default function AttachmentsDoc() {
60
+ const [items, setItems] = useState<AttachmentsItemProp[]>(settled);
61
+
62
+ return (
63
+ <AppShell
64
+ sidebar={
65
+ <Sidebar
66
+ activeId="chat"
67
+ sections={sections}
68
+ onSelect={() => {}}
69
+ product={{ name: "CoreDesk", role: "アシスタント", color: "hsl(var(--primary))" }}
70
+ />
71
+ }
72
+ topbar={<Topbar />}
73
+ >
74
+ <PageContainer
75
+ title="添付ファイル"
76
+ subtitle="Attachments · チャットに載せるファイルの束(Ant Design X)"
77
+ >
78
+ {/* EMPTY. The placeholder is the whole control when there is nothing yet: one button,
79
+ one accessible name, and a drop target that is the same box. */}
80
+ <Flex direction="col" gap="sm">
81
+ <Text size="sm" weight="medium">
82
+ 空のとき · placeholder
83
+ </Text>
84
+ <Attachments items={[]} />
85
+ </Flex>
86
+
87
+ {/* THE CONTROLLED LIST. `onChange` carries antd's `{ file, fileList }`, so the removed
88
+ row arrives as `file` with status "removed" and `fileList` is what remains. */}
89
+ <Flex direction="col" gap="sm">
90
+ <Text size="sm" weight="medium">
91
+ items · onChange · onRemove
92
+ </Text>
93
+ <Attachments
94
+ items={items}
95
+ onRemove={() => true}
96
+ onChange={({ fileList }) => setItems(fileList)}
97
+ />
98
+ <Text size="xs" tone="muted">
99
+ 残り {items.length} 件。× を押すと `onChange` が `{"{ file, fileList }"}` で届く。
100
+ </Text>
101
+ </Flex>
102
+
103
+ {/* STATUS. Uploading carries a determinate Progress; error states the failure in words,
104
+ not only in tint. */}
105
+ <Flex direction="col" gap="sm">
106
+ <Text size="sm" weight="medium">
107
+ status · uploading と error
108
+ </Text>
109
+ <Attachments items={inFlight} />
110
+ <Attachments items={failed} />
111
+ </Flex>
112
+
113
+ {/* OVERFLOW. Three modes, same six items — what happens when the row runs out of box. */}
114
+ <Flex direction="col" gap="sm">
115
+ <Text size="sm" weight="medium">
116
+ overflow · wrap
117
+ </Text>
118
+ <Attachments items={many} overflow="wrap" />
119
+ </Flex>
120
+
121
+ <Flex direction="col" gap="sm">
122
+ <Text size="sm" weight="medium">
123
+ overflow · scrollX
124
+ </Text>
125
+ <Attachments items={many} overflow="scrollX" />
126
+ </Flex>
127
+
128
+ <Flex direction="col" gap="sm">
129
+ <Text size="sm" weight="medium">
130
+ overflow · scrollY
131
+ </Text>
132
+ <Attachments items={many} overflow="scrollY" />
133
+ </Flex>
134
+
135
+ {/* PLACEHOLDER, OVERRIDDEN. An object, or a function of "inline" | "drop". */}
136
+ <Flex direction="col" gap="sm">
137
+ <Text size="sm" weight="medium">
138
+ placeholder · 自分の文言
139
+ </Text>
140
+ <Attachments
141
+ items={[]}
142
+ placeholder={{
143
+ icon: <Paperclip />,
144
+ title: "証憑を添付",
145
+ description: "領収書・請求書・契約書。PDF と画像に対応します。",
146
+ }}
147
+ />
148
+ </Flex>
149
+
150
+ {/* DISABLED. The strip stays readable; nothing in it is reachable. */}
151
+ <Flex direction="col" gap="sm">
152
+ <Text size="sm" weight="medium">
153
+ disabled
154
+ </Text>
155
+ <Attachments items={settled} disabled />
156
+ </Flex>
157
+ </PageContainer>
158
+ </AppShell>
159
+ );
160
+ }
@@ -1,6 +1,8 @@
1
1
  import { useState } from "react";
2
2
  import {
3
3
  Dialog,
4
+ DialogAction,
5
+ DialogCancel,
4
6
  DialogContent,
5
7
  DialogDescription,
6
8
  DialogFooter,
@@ -8,6 +10,7 @@ import {
8
10
  DialogTitle,
9
11
  DialogTrigger,
10
12
  } from "@godxjp/ui/feedback";
13
+ import { FormField, Textarea } from "@godxjp/ui/data-entry";
11
14
  import {
12
15
  Badge,
13
16
  Card,
@@ -40,6 +43,8 @@ export default function Demo() {
40
43
  const [detailOpen, setDetailOpen] = useState(false);
41
44
  const [toneOpen, setToneOpen] = useState(false);
42
45
  const [headerTone, setHeaderTone] = useState<(typeof headerTones)[number]>("default");
46
+ const [terminateOpen, setTerminateOpen] = useState(false);
47
+ const [reason, setReason] = useState("");
43
48
 
44
49
  return (
45
50
  <PageContainer
@@ -187,6 +192,51 @@ export default function Demo() {
187
192
  </Flex>
188
193
  </CardContent>
189
194
  </Card>
195
+
196
+ <Card>
197
+ <CardHeader>
198
+ <CardTitle level={2}>Destructive confirmation with a required field</CardTitle>
199
+ <CardDescription>
200
+ variant is the one prop that decides three things that always travel together: the
201
+ ARIA role, whether an outside click dismisses, and the primary action emphasis.
202
+ variant=&quot;destructive&quot; renders role=&quot;alertdialog&quot;, ignores an
203
+ outside click and tints DialogAction, while the surface stays a plain Dialog, so a
204
+ required input can sit inside it. That combination used to need a rewrite: Dialog had
205
+ the form but lost the role, AlertDialog had the role but its parts assume a two-button
206
+ body. Escape still closes, exactly as the AlertDialog family already does.
207
+ </CardDescription>
208
+ </CardHeader>
209
+ <CardContent>
210
+ <Dialog open={terminateOpen} onOpenChange={setTerminateOpen} variant="destructive">
211
+ <DialogTrigger asChild>
212
+ <Button variant="destructive" size="sm">
213
+ 提携を終了
214
+ </Button>
215
+ </DialogTrigger>
216
+ <DialogContent className="max-w-md">
217
+ <DialogHeader
218
+ tone="destructive"
219
+ title="提携を終了しますか?"
220
+ subtitle="この操作は元に戻せません。終了理由は監査ログに残ります。"
221
+ />
222
+ <FormField id="termination-reason" label="終了理由" required>
223
+ <Textarea
224
+ id="termination-reason"
225
+ value={reason}
226
+ onChange={(event) => setReason(event.target.value)}
227
+ placeholder="契約違反の内容を記載してください"
228
+ />
229
+ </FormField>
230
+ <DialogFooter>
231
+ <DialogCancel asChild>
232
+ <Button variant="ghost">キャンセル</Button>
233
+ </DialogCancel>
234
+ <DialogAction disabled={reason.trim().length === 0}>終了する</DialogAction>
235
+ </DialogFooter>
236
+ </DialogContent>
237
+ </Dialog>
238
+ </CardContent>
239
+ </Card>
190
240
  </Flex>
191
241
  </PageContainer>
192
242
  );
@@ -0,0 +1,175 @@
1
+ import { useState } from "react";
2
+
3
+ import { ChatBubble } from "@godxjp/ui/data-display";
4
+ import {
5
+ Actions,
6
+ ActionsCopy,
7
+ ActionsFeedback,
8
+ ActionsItem,
9
+ type ActionsFeedbackValueProp,
10
+ Text,
11
+ } from "@godxjp/ui/general";
12
+ import {
13
+ AppShell,
14
+ Flex,
15
+ PageContainer,
16
+ Sidebar,
17
+ type SidebarSectionProp,
18
+ Topbar,
19
+ } from "@godxjp/ui/layout";
20
+ import {
21
+ Bot,
22
+ Flag,
23
+ MessageSquare,
24
+ RefreshCw,
25
+ Settings,
26
+ Share2,
27
+ Users,
28
+ Volume2,
29
+ } from "lucide-react";
30
+
31
+ /**
32
+ * Actions — the strip of actions under an assistant message (Ant Design X `Actions`).
33
+ *
34
+ * Every card is one axis at rest. The first shows the strip where it belongs, under a real
35
+ * `ChatBubble`; the rest are the three variants, the `subItems` menu, the four `ActionsItem`
36
+ * statuses, and the two ready-made actions (`ActionsCopy`, `ActionsFeedback`).
37
+ *
38
+ * The strip is a WAI-ARIA toolbar: one Tab reaches it, the arrows move inside it. Ant X's own
39
+ * strip is a row of `<div onClick>` and cannot be reached from the keyboard at all.
40
+ *
41
+ * Composed only from real @godxjp/ui components.
42
+ */
43
+ const sections: SidebarSectionProp[] = [
44
+ {
45
+ label: "アシスタント",
46
+ items: [
47
+ { id: "chat", label: "チャット", icon: MessageSquare },
48
+ { id: "agents", label: "エージェント", icon: Bot },
49
+ { id: "members", label: "メンバー", icon: Users },
50
+ ],
51
+ },
52
+ { label: "管理", items: [{ id: "settings", label: "設定", icon: Settings }] },
53
+ ];
54
+
55
+ const ANSWER = "差分は3件でした。金額、支払期日、宛名のうちどれから直しますか。";
56
+
57
+ export default function ActionsDoc() {
58
+ const [vote, setVote] = useState<ActionsFeedbackValueProp>("default");
59
+ const [log, setLog] = useState("まだ何も押されていません。");
60
+
61
+ return (
62
+ <AppShell
63
+ sidebar={
64
+ <Sidebar
65
+ activeId="chat"
66
+ sections={sections}
67
+ onSelect={() => {}}
68
+ product={{ name: "CoreDesk", role: "アシスタント", color: "hsl(var(--primary))" }}
69
+ />
70
+ }
71
+ topbar={<Topbar />}
72
+ >
73
+ <PageContainer
74
+ title="回答の操作"
75
+ subtitle="Actions · 一つの回答にぶら下がる操作の帯。Tab は一回、中は矢印キー。"
76
+ >
77
+ {/* WHERE IT BELONGS: under the answer it acts on. */}
78
+ <Flex direction="col" gap="sm">
79
+ <Text size="sm" weight="medium">
80
+ in place · 回答の下
81
+ </Text>
82
+ <ChatBubble placement="start" variant="filled">
83
+ {ANSWER}
84
+ </ChatBubble>
85
+ <Actions
86
+ label="回答の操作"
87
+ items={[
88
+ { key: "retry", label: "やり直す", icon: <RefreshCw /> },
89
+ { key: "copy", actionRender: <ActionsCopy text={ANSWER} /> },
90
+ {
91
+ key: "feedback",
92
+ actionRender: <ActionsFeedback value={vote} onChange={setVote} />,
93
+ },
94
+ {
95
+ key: "more",
96
+ label: "その他",
97
+ subItems: [
98
+ { key: "share", label: "共有", icon: <Share2 /> },
99
+ { key: "report", label: "報告", icon: <Flag />, danger: true },
100
+ ],
101
+ },
102
+ ]}
103
+ onClick={({ key, keyPath }) => setLog(`${key} — keyPath: ${keyPath.join(" ← ")}`)}
104
+ />
105
+ <Text size="xs" tone="muted">
106
+ {log}
107
+ </Text>
108
+ </Flex>
109
+
110
+ {/* THE THREE VARIANTS. */}
111
+ <Flex direction="col" gap="sm">
112
+ <Text size="sm" weight="medium">
113
+ variant · borderless · filled · outlined
114
+ </Text>
115
+ <Actions
116
+ label="borderless の例"
117
+ variant="borderless"
118
+ items={[
119
+ { key: "a", label: "やり直す", icon: <RefreshCw /> },
120
+ { key: "b", label: "共有", icon: <Share2 /> },
121
+ ]}
122
+ />
123
+ <Actions
124
+ label="filled の例"
125
+ variant="filled"
126
+ items={[
127
+ { key: "a", label: "やり直す", icon: <RefreshCw /> },
128
+ { key: "b", label: "共有", icon: <Share2 /> },
129
+ ]}
130
+ />
131
+ <Actions
132
+ label="outlined の例"
133
+ variant="outlined"
134
+ items={[
135
+ { key: "a", label: "やり直す", icon: <RefreshCw /> },
136
+ { key: "b", label: "共有", icon: <Share2 /> },
137
+ ]}
138
+ />
139
+ </Flex>
140
+
141
+ {/* ACTIONSITEM — the four statuses, side by side. */}
142
+ <Flex direction="col" gap="sm">
143
+ <Text size="sm" weight="medium">
144
+ ActionsItem · status
145
+ </Text>
146
+ <Text size="xs" tone="muted">
147
+ 状態は字にもなる。読み上げのボタン名に「実行中」「失敗」が入るので、
148
+ アイコンの差し替えだけに頼らない。
149
+ </Text>
150
+ <Flex gap="sm">
151
+ <ActionsItem defaultIcon={<Volume2 />} label="読み上げ" />
152
+ <ActionsItem defaultIcon={<Volume2 />} label="読み上げ" status="running" />
153
+ <ActionsItem defaultIcon={<Volume2 />} label="読み上げ" status="loading" />
154
+ <ActionsItem defaultIcon={<Volume2 />} label="読み上げ" status="error" />
155
+ </Flex>
156
+ </Flex>
157
+
158
+ {/* THE TWO READY-MADE ACTIONS, alone. */}
159
+ <Flex direction="col" gap="sm">
160
+ <Text size="sm" weight="medium">
161
+ ActionsCopy · ActionsFeedback
162
+ </Text>
163
+ <Text size="xs" tone="muted">
164
+ どちらも両方のボタンが画面に残る。押した側は aria-pressed
165
+ で示され、押していない側が消えることはない。
166
+ </Text>
167
+ <Flex gap="sm">
168
+ <ActionsCopy text={ANSWER} />
169
+ <ActionsFeedback value={vote} onChange={setVote} />
170
+ </Flex>
171
+ </Flex>
172
+ </PageContainer>
173
+ </AppShell>
174
+ );
175
+ }
@@ -0,0 +1,133 @@
1
+ import type { CSSProperties } from "react";
2
+
3
+ import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@godxjp/ui/data-display";
4
+ import { FloatButton, Text } from "@godxjp/ui/general";
5
+ import { Flex, PageContainer } from "@godxjp/ui/layout";
6
+ import { MessageCircle, Printer, Share2 } from "lucide-react";
7
+
8
+ /** One row up from the corner: the control's own height plus one stack step, both tokens. */
9
+ const STEP_UP = "calc(var(--space-stack-xl) + var(--control-height-lg) + var(--space-stack-md))";
10
+ /** Two rows up. */
11
+ const STEP_UP_TWICE =
12
+ "calc(var(--space-stack-xl) + (var(--control-height-lg) + var(--space-stack-md)) * 2)";
13
+
14
+ /**
15
+ * FloatButton — Ant Design のコーナーアクションを 100% 移植したもの(antd 6.6.3)。
16
+ *
17
+ * ページの内容ではないが常に手の届く場所にあるべき道具のための、ビューポートに固定された操作子。
18
+ * `Button` はレイアウトの流れの中にあるので、隅に留めるには呼び出し側が `position: fixed` を書く
19
+ * しかなく、それは `ui-audit` が止める(gh#558)。`Popover` はパネルの位置を決めるもので、
20
+ * トリガーの位置は決めない。`Banner` は全幅の帯であって隅の印ではない。
21
+ *
22
+ * 隅までの距離は `--float-button-offset-block-end` / `--float-button-offset-inline-end`。
23
+ * 下部に固定バーがあるサービスは、メディアクエリではなくトークン 1 つで印を動かす。
24
+ *
25
+ * このフレームは実際に固定表示される要素を含むため、画面の右下に印が重なって見える。
26
+ * それが正しい姿で、そこが FloatButton の唯一の居場所である。
27
+ */
28
+ export default function Demo() {
29
+ return (
30
+ <PageContainer
31
+ title="FloatButton"
32
+ subtitle="ビューポートの隅に固定される操作子 · Group と BackTop つき"
33
+ >
34
+ <Flex direction="col" gap="lg">
35
+ <Card>
36
+ <CardHeader>
37
+ <CardTitle level={2}>単体のボタン</CardTitle>
38
+ <CardDescription>
39
+ 画面右下に丸い印が 1 つ出ている。`tooltip` を文字列で渡すと、それがアクセシブル名にも
40
+ なる —— アイコンだけの操作子に名前がないのは WCAG 4.1.2 違反であり、ツールチップは
41
+ タッチの利用者には決して届かないため。
42
+ </CardDescription>
43
+ </CardHeader>
44
+ <CardContent>
45
+ <Flex direction="col" gap="sm">
46
+ <Text size="sm" tone="muted">
47
+ type=&quot;primary&quot; はブランド塗り、type=&quot;default&quot; は面とヘアライン。
48
+ antd の語をそのまま使っている(この Library の他の場所では variant と呼ぶもの)。
49
+ </Text>
50
+ <Text size="sm" tone="muted">
51
+ badge は count / dot / overflowCount / showZero を取る。count が overflowCount を
52
+ 超えると &quot;99+&quot; になる。
53
+ </Text>
54
+ </Flex>
55
+ </CardContent>
56
+ </Card>
57
+
58
+ <Card>
59
+ <CardHeader>
60
+ <CardTitle level={2}>Group · 積み重ねか、1 つのトリガーの後ろか</CardTitle>
61
+ <CardDescription>
62
+ `trigger` がその切り替えスイッチ。渡さなければ全部のボタンが見えたままの素の積み重ね、
63
+ 渡せば子はトリガーの後ろに畳まれ、click か hover で開く。`placement` は top / left /
64
+ right / bottom。left と right は `inset-inline-*` で解決されるので、
65
+ dir=&quot;rtl&quot; では画面上の向きが正しく反転する。
66
+ </CardDescription>
67
+ </CardHeader>
68
+ <CardContent>
69
+ <Text size="sm" tone="muted">
70
+ Group のルート要素は `pointer-events: none`、ボタンだけが `auto`。タッチ画面で、
71
+ ボタンとボタンの隙間に落ちたスワイプがページのスクロールを殺さないため。
72
+ </Text>
73
+ </CardContent>
74
+ </Card>
75
+
76
+ <Card>
77
+ <CardHeader>
78
+ <CardTitle level={2}>BackTop</CardTitle>
79
+ <CardDescription>
80
+ `visibilityHeight`(既定 400px)だけ下ったところで現れ、`duration`(既定 450ms)で
81
+ 上まで戻す。`prefers-reduced-motion` では瞬時に移動する(WCAG 2.3.3)。 `target`
82
+ に「その要素を返す関数」を渡せば、document ではなくその要素を見張る —— 自前の
83
+ スクロール領域を持つシェルの中でも使えるのはこのため。
84
+ </CardDescription>
85
+ </CardHeader>
86
+ <CardContent>
87
+ <Text size="sm" tone="muted">
88
+ `showProgress` を付けると、どこまで読んだかをリングで描く。1 フレームごとに変わる値は
89
+ カスタムプロパティ 1 つだけで、呼び出し側に数値は 1 つも出てこない。
90
+ </Text>
91
+ </CardContent>
92
+ </Card>
93
+ </Flex>
94
+
95
+ {/*
96
+ 実運用の画面に浮くボタンは 1 つである。このフレームは 3 つの形を同時に見せるため、
97
+ 2 つ目と 3 つ目を 1 段ずつ上にずらしてある —— そして、そのずらし方こそが gh#558 の求めた
98
+ ものである: メディアクエリでも呼び出し側の px でもなく、トークンを 1 つ上書きするだけ。
99
+ 段の高さはボタンの高さ + 段間で、どちらもトークンなので密度を変えても崩れない。
100
+ */}
101
+ <FloatButton
102
+ tooltip="アシスタントに聞く"
103
+ type="primary"
104
+ icon={<MessageCircle />}
105
+ badge={{ count: 3 }}
106
+ />
107
+
108
+ <FloatButton.Group
109
+ trigger="hover"
110
+ placement="left"
111
+ aria-label="その他の操作"
112
+ style={
113
+ {
114
+ "--float-button-offset-block-end": STEP_UP,
115
+ } as CSSProperties
116
+ }
117
+ >
118
+ <FloatButton tooltip="共有" icon={<Share2 />} />
119
+ <FloatButton tooltip="印刷" icon={<Printer />} />
120
+ </FloatButton.Group>
121
+
122
+ <FloatButton.BackTop
123
+ visibilityHeight={0}
124
+ showProgress
125
+ style={
126
+ {
127
+ "--float-button-offset-block-end": STEP_UP_TWICE,
128
+ } as CSSProperties
129
+ }
130
+ />
131
+ </PageContainer>
132
+ );
133
+ }