@godxjp/ui 28.6.0 → 28.8.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/dist/components/data-display/data-table.d.ts +10 -2
- package/dist/components/data-display/data-table.js +12 -5
- package/dist/components/data-display/index.d.ts +2 -0
- package/dist/components/data-display/index.js +2 -0
- package/dist/components/data-display/marquee.d.ts +16 -0
- package/dist/components/data-display/marquee.js +155 -0
- package/dist/components/data-display/scroll-area.js +16 -2
- package/dist/components/data-display/service-launcher-card.d.ts +25 -0
- package/dist/components/data-display/table.d.ts +38 -1
- package/dist/components/data-display/table.js +57 -39
- package/dist/components/data-entry/form-field.js +29 -12
- package/dist/components/general/reveal.d.ts +23 -2
- package/dist/components/general/reveal.js +37 -7
- package/dist/components/general/typography.d.ts +4 -1
- package/dist/components/general/typography.js +14 -1
- package/dist/components/layout/affix.d.ts +86 -0
- package/dist/components/layout/affix.js +187 -0
- package/dist/components/layout/index.d.ts +4 -0
- package/dist/components/layout/index.js +4 -0
- package/dist/components/layout/legal-document-shell.js +4 -3
- package/dist/components/layout/masonry.d.ts +74 -0
- package/dist/components/layout/masonry.js +214 -0
- package/dist/components/layout/page-container.js +5 -20
- package/dist/components/navigation/anchor.d.ts +64 -0
- package/dist/components/navigation/anchor.js +284 -0
- package/dist/components/navigation/app-setting-picker.js +20 -3
- package/dist/components/navigation/index.d.ts +4 -0
- package/dist/components/navigation/index.js +4 -0
- package/dist/components/navigation/mega-menu.d.ts +21 -0
- package/dist/components/navigation/mega-menu.js +526 -0
- package/dist/components/ui/table.js +1 -0
- package/dist/contracts/measurement.json +1 -1
- package/dist/i18n/messages/en.json +522 -1
- package/dist/i18n/messages/ja.json +518 -1
- package/dist/i18n/messages/vi.json +518 -1
- package/dist/lib/hooks.d.ts +91 -0
- package/dist/lib/hooks.js +79 -0
- package/dist/lib/platform.d.ts +14 -0
- package/dist/lib/platform.js +10 -1
- package/dist/lib/utils.d.ts +1 -1
- package/dist/lib/utils.js +3 -2
- package/dist/props/components/data-display.prop.d.ts +109 -1
- package/dist/props/components/general.prop.d.ts +47 -3
- package/dist/props/components/layout.prop.d.ts +194 -0
- package/dist/props/components/navigation.prop.d.ts +263 -0
- package/dist/props/registry.d.ts +360 -5
- package/dist/props/registry.js +474 -4
- package/dist/props/vocabulary/index.d.ts +1 -1
- package/dist/props/vocabulary/interaction.prop.d.ts +39 -2
- package/dist/styles/control.css +5 -6
- package/dist/styles/data-display-layout.css +2 -1
- package/dist/styles/density.css +4 -0
- package/dist/styles/form-layout.css +22 -0
- package/dist/styles/layout.css +79 -1
- package/dist/styles/motion.css +121 -1
- package/dist/styles/navigation-layout.css +415 -20
- package/dist/styles/shell-layout.css +3 -0
- package/dist/styles/text-layout.css +52 -4
- package/dist/tokens/base.css +5 -0
- package/dist/tokens/components/affix.css +7 -0
- package/dist/tokens/components/anchor.css +17 -0
- package/dist/tokens/components/control.css +3 -3
- package/dist/tokens/components/form.css +3 -1
- package/dist/tokens/components/marquee.css +7 -0
- package/dist/tokens/components/masonry.css +6 -0
- package/dist/tokens/components/mega-menu.css +62 -0
- package/dist/tokens/components/navigation.css +2 -0
- package/dist/tokens/components/shell.css +3 -0
- package/dist/tokens/foundation.css +11 -0
- package/dist/tokens/semantic/layout.css +7 -0
- package/docs/COMPOSITION-VS-COMPONENT.md +21 -0
- package/docs/DESIGN-AUTHORITY.md +124 -23
- package/docs/FRAME-COVERAGE-REPORT.md +7 -2
- package/docs/data-display/marquee.tsx +254 -0
- package/docs/data-display/scroll-area.tsx +31 -0
- package/docs/data-display/service-launcher-card.tsx +275 -115
- package/docs/foundation/_theme-editor-scope.ts +222 -0
- package/docs/foundation/density.tsx +12 -2
- package/docs/foundation/spacing.tsx +5 -0
- package/docs/foundation/theme-editor.tsx +645 -0
- package/docs/general/activity.tsx +65 -0
- package/docs/general/reveal.tsx +290 -22
- package/docs/general/typography.tsx +91 -1
- package/docs/layout/affix.tsx +209 -0
- package/docs/layout/masonry.tsx +291 -0
- package/docs/navigation/anchor.tsx +285 -0
- package/docs/navigation/mega-menu-panel.tsx +86 -0
- package/docs/navigation/mega-menu.tsx +254 -0
- package/docs/roadmap/website-components.md +779 -0
- package/docs/showcase/acme-website.tsx +75 -39
- package/docs/showcase/futurelastic-web.tsx +91 -49
- package/docs/showcase/marketing-page.tsx +885 -0
- package/docs/showcase/table-footer-totals.tsx +12 -2
- package/docs/showcase/theme-customization.tsx +1259 -0
- package/package.json +8 -5
- package/scripts/brand-accent.generated.mjs +27 -0
- package/scripts/ui-audit.mjs +66 -0
- package/scripts/visual-audit-rules.mjs +46 -2
- package/scripts/visual-audit.mjs +29 -1
|
@@ -1,10 +1,15 @@
|
|
|
1
1
|
import {
|
|
2
|
-
|
|
2
|
+
BookOpen,
|
|
3
|
+
Boxes,
|
|
4
|
+
Calendar,
|
|
3
5
|
Clock3,
|
|
4
6
|
Database,
|
|
7
|
+
FileText,
|
|
5
8
|
LayoutGrid,
|
|
9
|
+
Mail,
|
|
6
10
|
PlugZap,
|
|
7
11
|
ReceiptText,
|
|
12
|
+
Server,
|
|
8
13
|
ShieldCheck,
|
|
9
14
|
} from "lucide-react";
|
|
10
15
|
|
|
@@ -13,132 +18,287 @@ import {
|
|
|
13
18
|
ServiceLauncherCard,
|
|
14
19
|
ServiceLauncherCardSkeleton,
|
|
15
20
|
} from "@godxjp/ui/data-display";
|
|
16
|
-
import { Button } from "@godxjp/ui/general";
|
|
21
|
+
import { Button, Text } from "@godxjp/ui/general";
|
|
17
22
|
import { Flex, PageContainer, ResponsiveGrid } from "@godxjp/ui/layout";
|
|
18
23
|
|
|
19
24
|
/**
|
|
20
|
-
* ServiceLauncherCard —
|
|
21
|
-
*
|
|
25
|
+
* ServiceLauncherCard — one tile for one application in an organization's launcher.
|
|
26
|
+
*
|
|
27
|
+
* WHY THIS PAGE IS GENERIC. It used to render this product's own service catalogue —
|
|
28
|
+
* `attendance.godx.jp`, plans `Standard/Enterprise/Free`, a 「カタログを見る」 link — which made a
|
|
29
|
+
* reader ask whether the component belonged in a UI framework at all. A fair question: a page that
|
|
30
|
+
* demonstrates a product cannot demonstrate a component. The data here is therefore ANY
|
|
31
|
+
* organization's internal tools, on RFC 2606 reserved domains, with no plan names of ours.
|
|
32
|
+
*
|
|
33
|
+
* WHAT IS NOT NEGOTIABLE is the state set — available / restricted / suspended / archived /
|
|
34
|
+
* loading / catalog entry. Those ARE the component's API surface, because the component infers
|
|
35
|
+
* nothing: `statusLabel`, `statusTone`, `disabledReason` and `action` all come from the consumer's
|
|
36
|
+
* own entitlement contract. There is no `href`, no `available`, no `entitlement` prop to find.
|
|
37
|
+
*
|
|
38
|
+
* ResponsiveGrid owns the 3 → 2 → 1 ladder. The page writes no grid tracks and no page-local CSS.
|
|
22
39
|
*/
|
|
40
|
+
|
|
41
|
+
/** A section heading. Headings are headings, not Cards — a Card here would nest borders. */
|
|
42
|
+
function Section({
|
|
43
|
+
title,
|
|
44
|
+
why,
|
|
45
|
+
children,
|
|
46
|
+
}: {
|
|
47
|
+
title: string;
|
|
48
|
+
why: string;
|
|
49
|
+
children: React.ReactNode;
|
|
50
|
+
}) {
|
|
51
|
+
return (
|
|
52
|
+
<Flex direction="col" gap="sm">
|
|
53
|
+
<Text as="div" weight="medium">
|
|
54
|
+
{title}
|
|
55
|
+
</Text>
|
|
56
|
+
<Text as="div" size="sm" tone="muted">
|
|
57
|
+
{why}
|
|
58
|
+
</Text>
|
|
59
|
+
{children}
|
|
60
|
+
</Flex>
|
|
61
|
+
);
|
|
62
|
+
}
|
|
63
|
+
|
|
23
64
|
export default function Demo() {
|
|
24
65
|
return (
|
|
25
66
|
<PageContainer
|
|
26
67
|
title="ServiceLauncherCard"
|
|
27
|
-
subtitle="
|
|
68
|
+
subtitle="社内アプリケーション一覧のタイル — 利用可能・制限・停止・アーカイブ・読み込み中・カタログ導線"
|
|
28
69
|
>
|
|
29
70
|
<Flex direction="col" gap="lg">
|
|
30
|
-
<
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
71
|
+
<Section
|
|
72
|
+
title="1 · 基本 · 利用できるサービス"
|
|
73
|
+
why="アイコン・名称・ステータス・説明・識別子・操作。ステータスの文言は常に利用側のコピーで、コンポーネントが決めるのは tone に対応する Badge の意味づけだけです。"
|
|
74
|
+
>
|
|
75
|
+
<ResponsiveGrid columns={{ sm: 1, md: 2, lg: 3 }}>
|
|
76
|
+
<ServiceLauncherCard
|
|
77
|
+
icon={Clock3}
|
|
78
|
+
title="勤怠管理"
|
|
79
|
+
statusLabel="利用可能"
|
|
80
|
+
statusTone="success"
|
|
81
|
+
description="打刻、シフト、休暇申請を一つのワークスペースで管理します。"
|
|
82
|
+
metadata="attend.corp.example.jp · v4.2"
|
|
83
|
+
action={<Button>サービスを開く</Button>}
|
|
84
|
+
/>
|
|
85
|
+
<ServiceLauncherCard
|
|
86
|
+
icon={ReceiptText}
|
|
87
|
+
title="経費精算"
|
|
88
|
+
statusLabel="利用可能"
|
|
89
|
+
statusTone="success"
|
|
90
|
+
description="領収書の提出から承認、仕訳連携までを扱います。"
|
|
91
|
+
metadata="keihi.corp.example.jp · v2.8"
|
|
92
|
+
action={<Button>サービスを開く</Button>}
|
|
93
|
+
/>
|
|
94
|
+
<ServiceLauncherCard
|
|
95
|
+
icon={Calendar}
|
|
96
|
+
title="会議室予約"
|
|
97
|
+
statusLabel="利用可能"
|
|
98
|
+
statusTone="success"
|
|
99
|
+
description="拠点ごとの会議室と備品を予約します。"
|
|
100
|
+
metadata="rooms.corp.example.jp · v1.9"
|
|
101
|
+
action={<Button>サービスを開く</Button>}
|
|
102
|
+
/>
|
|
103
|
+
</ResponsiveGrid>
|
|
104
|
+
</Section>
|
|
105
|
+
|
|
106
|
+
<Section
|
|
107
|
+
title="2 · statusTone の 6 値すべて"
|
|
108
|
+
why="success / warning / destructive / info / neutral / muted。tone は Badge の意味づけだけを決め、文言・判定・操作の可否はすべて利用側が渡します。"
|
|
109
|
+
>
|
|
110
|
+
<ResponsiveGrid columns={{ sm: 1, md: 2, lg: 3 }}>
|
|
111
|
+
<ServiceLauncherCard
|
|
112
|
+
icon={Mail}
|
|
113
|
+
titleLevel={3}
|
|
114
|
+
title="社内メール"
|
|
115
|
+
statusLabel="稼働中"
|
|
116
|
+
statusTone="success"
|
|
117
|
+
description="メールボックス、配布リスト、共有署名を管理します。"
|
|
118
|
+
metadata="mail.corp.example.jp"
|
|
119
|
+
action={<Button>サービスを開く</Button>}
|
|
120
|
+
/>
|
|
121
|
+
<ServiceLauncherCard
|
|
122
|
+
icon={Server}
|
|
123
|
+
titleLevel={3}
|
|
124
|
+
title="ファイル共有"
|
|
125
|
+
statusLabel="メンテナンス中"
|
|
126
|
+
statusTone="warning"
|
|
127
|
+
description="部門フォルダと外部共有リンクを扱います。"
|
|
128
|
+
metadata="files.corp.example.jp"
|
|
129
|
+
action={<Button variant="outline">状況を見る</Button>}
|
|
130
|
+
/>
|
|
131
|
+
<ServiceLauncherCard
|
|
132
|
+
icon={Database}
|
|
133
|
+
titleLevel={3}
|
|
134
|
+
title="データ連携基盤"
|
|
135
|
+
statusLabel="停止中"
|
|
136
|
+
statusTone="destructive"
|
|
137
|
+
description="外部システムとのデータ同期ジョブを管理します。"
|
|
138
|
+
metadata="sync.corp.example.jp"
|
|
139
|
+
action={<Button variant="outline">履歴を見る</Button>}
|
|
140
|
+
/>
|
|
141
|
+
<ServiceLauncherCard
|
|
142
|
+
icon={ShieldCheck}
|
|
143
|
+
titleLevel={3}
|
|
144
|
+
title="権限管理"
|
|
145
|
+
statusLabel="管理者限定"
|
|
146
|
+
statusTone="info"
|
|
147
|
+
description="ロール、二要素認証ポリシー、監査イベントを確認します。"
|
|
148
|
+
metadata="iam.corp.example.jp"
|
|
149
|
+
action={<Button variant="outline">権限を確認</Button>}
|
|
150
|
+
/>
|
|
151
|
+
<ServiceLauncherCard
|
|
152
|
+
icon={PlugZap}
|
|
153
|
+
titleLevel={3}
|
|
154
|
+
title="連携アプリ"
|
|
155
|
+
statusLabel="準備中"
|
|
156
|
+
statusTone="neutral"
|
|
157
|
+
description="サードパーティ連携の接続とスコープを設定します。"
|
|
158
|
+
metadata="connect.corp.example.jp"
|
|
159
|
+
action={<Button variant="outline">設定を開く</Button>}
|
|
160
|
+
/>
|
|
161
|
+
<ServiceLauncherCard
|
|
162
|
+
icon={BookOpen}
|
|
163
|
+
titleLevel={3}
|
|
164
|
+
title="旧ナレッジベース"
|
|
165
|
+
statusLabel="アーカイブ"
|
|
166
|
+
statusTone="muted"
|
|
167
|
+
description="2024 年度までの運用手順を読み取り専用で保管しています。"
|
|
168
|
+
metadata="kb-archive.corp.example.jp"
|
|
169
|
+
action={<Button variant="outline">アーカイブを見る</Button>}
|
|
170
|
+
/>
|
|
171
|
+
</ResponsiveGrid>
|
|
172
|
+
</Section>
|
|
173
|
+
|
|
174
|
+
<Section
|
|
175
|
+
title="3 · disabledReason · 操作が効かない理由は操作より先に置かれる"
|
|
176
|
+
why="無効化されたボタンは「なぜ」を一切伝えません。だから理由は DOM 順で操作より先に置かれ(WCAG 2.2 · 1.3.2)、タイルは data-unavailable を帯びてメダリオンが沈みます。disabledReason はボタンを無効化しません — それは利用側の Button prop のままです。"
|
|
177
|
+
>
|
|
178
|
+
<ResponsiveGrid columns={{ sm: 1, md: 2, lg: 3 }}>
|
|
179
|
+
<ServiceLauncherCard
|
|
180
|
+
icon={Boxes}
|
|
181
|
+
titleLevel={3}
|
|
182
|
+
title="在庫管理"
|
|
183
|
+
statusLabel="未申請"
|
|
184
|
+
statusTone="warning"
|
|
185
|
+
description="拠点別の在庫、入出庫、棚卸を管理します。"
|
|
186
|
+
metadata="zaiko.corp.example.jp"
|
|
187
|
+
disabledReason="この部門ではまだ利用申請が承認されていません。"
|
|
188
|
+
action={<Button disabled>サービスを開く</Button>}
|
|
189
|
+
/>
|
|
190
|
+
<ServiceLauncherCard
|
|
191
|
+
icon={FileText}
|
|
192
|
+
titleLevel={3}
|
|
193
|
+
title="契約書管理"
|
|
194
|
+
statusLabel="権限なし"
|
|
195
|
+
statusTone="info"
|
|
196
|
+
description="契約書の保管、期限通知、電子署名の依頼を扱います。"
|
|
197
|
+
metadata="keiyaku.corp.example.jp"
|
|
198
|
+
disabledReason="閲覧には法務部門のロールが必要です。IT ヘルプデスクへ申請してください。"
|
|
199
|
+
action={<Button disabled>サービスを開く</Button>}
|
|
200
|
+
/>
|
|
201
|
+
<ServiceLauncherCard
|
|
202
|
+
icon={Database}
|
|
203
|
+
titleLevel={3}
|
|
204
|
+
title="分析基盤"
|
|
205
|
+
statusLabel="停止中"
|
|
206
|
+
statusTone="destructive"
|
|
207
|
+
description="全社ダッシュボードとデータマートを提供します。"
|
|
208
|
+
metadata="bi.corp.example.jp"
|
|
209
|
+
disabledReason="このサービスは組織の管理者によって停止されています。"
|
|
210
|
+
action={<Button disabled>サービスを開く</Button>}
|
|
211
|
+
/>
|
|
212
|
+
</ResponsiveGrid>
|
|
213
|
+
</Section>
|
|
214
|
+
|
|
215
|
+
<Section
|
|
216
|
+
title="4 · 読み込み中とカタログ導線"
|
|
217
|
+
why="ServiceLauncherCardSkeleton は同じ骨格の初期プレースホルダーで、label が必須・aria-busy を持ち、ライブリージョンは意図的に開きません。ServiceCatalogCta は実在する追加導線がある場合にだけ並べる相棒タイルで、既定の Plus 以外のグリフも受け取ります。"
|
|
218
|
+
>
|
|
219
|
+
<ResponsiveGrid columns={{ sm: 1, md: 2, lg: 3 }}>
|
|
220
|
+
<ServiceLauncherCardSkeleton label="サービスを読み込み中" />
|
|
221
|
+
<ServiceCatalogCta
|
|
222
|
+
title="アプリを追加"
|
|
223
|
+
action={<Button variant="outline">申請フォームを開く</Button>}
|
|
224
|
+
/>
|
|
225
|
+
<ServiceCatalogCta
|
|
226
|
+
icon={LayoutGrid}
|
|
227
|
+
title="すべての社内アプリを見る"
|
|
228
|
+
action={<Button variant="ghost">一覧を開く</Button>}
|
|
229
|
+
/>
|
|
230
|
+
</ResponsiveGrid>
|
|
231
|
+
</Section>
|
|
65
232
|
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
action={<Button variant="ghost">カタログ一覧</Button>}
|
|
104
|
-
/>
|
|
105
|
-
</ResponsiveGrid>
|
|
233
|
+
<Section
|
|
234
|
+
title="5 · 端に触れる · 3 行の名称、説明なし、切れない長いホスト名"
|
|
235
|
+
why="整った例はタイルが崩れる場所を何も見せません。ここは崩れる側です:3 行になる名称と 2 語の名称が同じ行に並び、説明のないタイルが説明のあるタイルの隣に置かれ、空白のないホスト名が列幅を広げようとします。見苦しく見えるなら、このセクションは仕事をしています — 耐えるのはサンプルデータではなくコンポーネントの側です。"
|
|
236
|
+
>
|
|
237
|
+
<ResponsiveGrid columns={{ sm: 1, md: 2, lg: 3 }}>
|
|
238
|
+
<ServiceLauncherCard
|
|
239
|
+
icon={ShieldCheck}
|
|
240
|
+
titleLevel={3}
|
|
241
|
+
title="グローバル人事情報基盤・従業員セルフサービスポータル(アジア太平洋地域)"
|
|
242
|
+
statusLabel="利用可能"
|
|
243
|
+
statusTone="success"
|
|
244
|
+
description="人事マスタ、勤怠、給与、評価を一元管理する統合ワークスペースです。"
|
|
245
|
+
metadata="workforce-identity-administration.ap-northeast-1.corp.example.jp · v11.0"
|
|
246
|
+
action={<Button>サービスを開く</Button>}
|
|
247
|
+
/>
|
|
248
|
+
<ServiceLauncherCard
|
|
249
|
+
icon={Clock3}
|
|
250
|
+
titleLevel={3}
|
|
251
|
+
title="日報"
|
|
252
|
+
statusLabel="利用可能"
|
|
253
|
+
statusTone="success"
|
|
254
|
+
metadata="nippo.corp.example.jp"
|
|
255
|
+
action={<Button>サービスを開く</Button>}
|
|
256
|
+
/>
|
|
257
|
+
<ServiceLauncherCard
|
|
258
|
+
icon={Server}
|
|
259
|
+
titleLevel={3}
|
|
260
|
+
title="Global Workforce Identity & Entitlement Administration Console"
|
|
261
|
+
statusLabel="Attention required"
|
|
262
|
+
statusTone="warning"
|
|
263
|
+
description="Review pending entitlement requests before the next access certification."
|
|
264
|
+
metadata="workforce-identity-administration.ap-southeast-1.corp.example.com"
|
|
265
|
+
disabledReason="Your organization has no approved request for this service yet."
|
|
266
|
+
action={<Button disabled>Open service</Button>}
|
|
267
|
+
/>
|
|
268
|
+
</ResponsiveGrid>
|
|
269
|
+
</Section>
|
|
106
270
|
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
metadata="workforce-identity-administration.ap-southeast-1.godx.jp · Enterprise"
|
|
139
|
-
action={<Button variant="outline">Xem quyền truy cập</Button>}
|
|
140
|
-
/>
|
|
141
|
-
</ResponsiveGrid>
|
|
271
|
+
<Section
|
|
272
|
+
title="6 · 最小のタイル"
|
|
273
|
+
why="必須は icon・title・action の 3 つだけです。statusLabel を省けば Badge の行ごと消え、description と metadata を省いても骨格は保たれます — 隣の満載のタイルと高さを比べてください。"
|
|
274
|
+
>
|
|
275
|
+
<ResponsiveGrid columns={{ sm: 1, md: 2, lg: 3 }}>
|
|
276
|
+
<ServiceLauncherCard
|
|
277
|
+
icon={BookOpen}
|
|
278
|
+
titleLevel={3}
|
|
279
|
+
title="社内 Wiki"
|
|
280
|
+
action={<Button variant="outline">開く</Button>}
|
|
281
|
+
/>
|
|
282
|
+
<ServiceLauncherCard
|
|
283
|
+
icon={Mail}
|
|
284
|
+
titleLevel={3}
|
|
285
|
+
title="問い合わせ窓口"
|
|
286
|
+
description="社内のシステム相談はこちらで受け付けています。"
|
|
287
|
+
action={<Button variant="outline">開く</Button>}
|
|
288
|
+
/>
|
|
289
|
+
<ServiceLauncherCard
|
|
290
|
+
icon={Calendar}
|
|
291
|
+
titleLevel={3}
|
|
292
|
+
title="全社カレンダー"
|
|
293
|
+
statusLabel="利用可能"
|
|
294
|
+
statusTone="success"
|
|
295
|
+
description="全社行事、休業日、拠点別の予定を共有します。"
|
|
296
|
+
metadata="calendar.corp.example.jp · v3.1"
|
|
297
|
+
disabledReason="読み取り専用で公開されています。"
|
|
298
|
+
action={<Button variant="outline">開く</Button>}
|
|
299
|
+
/>
|
|
300
|
+
</ResponsiveGrid>
|
|
301
|
+
</Section>
|
|
142
302
|
</Flex>
|
|
143
303
|
</PageContainer>
|
|
144
304
|
);
|
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What the Theme Editor page needs that is NOT React: the repo's own brand derivation, the two
|
|
3
|
+
* stylesheet facts it takes as arguments, and the ONE style object that makes a previewed seed
|
|
4
|
+
* actually reach the components under it.
|
|
5
|
+
*
|
|
6
|
+
* `deriveBrand` is `scripts/gen-brand.mjs`'s body (src/tokens/__tests__/brand-derivation.ts).
|
|
7
|
+
* Importing it — rather than porting it — is why the page's export and `pnpm gen:brand` emit the
|
|
8
|
+
* same bytes for the same hex. `wcag-contrast.ts`'s own opening line says why there is one copy of
|
|
9
|
+
* the luminance formula; this is the same argument one level up.
|
|
10
|
+
*/
|
|
11
|
+
import type { CSSProperties } from "react";
|
|
12
|
+
|
|
13
|
+
import {
|
|
14
|
+
asTriplet,
|
|
15
|
+
deriveBrand,
|
|
16
|
+
triplet,
|
|
17
|
+
type BrandChannels,
|
|
18
|
+
type BrandTheme,
|
|
19
|
+
type Hsl,
|
|
20
|
+
} from "../../src/tokens/__tests__/brand-derivation";
|
|
21
|
+
|
|
22
|
+
export {
|
|
23
|
+
AA_TEXT,
|
|
24
|
+
NON_TEXT,
|
|
25
|
+
asTriplet,
|
|
26
|
+
contrast,
|
|
27
|
+
deriveBrand,
|
|
28
|
+
hslToRgb,
|
|
29
|
+
parseHex,
|
|
30
|
+
toHex,
|
|
31
|
+
toHsl,
|
|
32
|
+
round,
|
|
33
|
+
} from "../../src/tokens/__tests__/brand-derivation";
|
|
34
|
+
export type {
|
|
35
|
+
Brand,
|
|
36
|
+
BrandChannels,
|
|
37
|
+
BrandTheme,
|
|
38
|
+
ContrastRow,
|
|
39
|
+
Hsl,
|
|
40
|
+
} from "../../src/tokens/__tests__/brand-derivation";
|
|
41
|
+
|
|
42
|
+
/* ── what the stylesheet says, asked of the browser rather than copied here ─────────────────── */
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* `gen:brand` reads `--background` and the three `-channels` expressions out of `foundation.css`
|
|
46
|
+
* and `derived.css` with `node:fs`. A page has no filesystem, and a literal copied into this file
|
|
47
|
+
* would be a third place the canvas colour lives — stale the first time someone retunes the spine.
|
|
48
|
+
* So the page asks the DOCUMENT, which is the same declarations after the same cascade.
|
|
49
|
+
*
|
|
50
|
+
* The CANVASES come from the CSSOM rather than `getComputedStyle`, because the page needs BOTH at
|
|
51
|
+
* once and an element can only be in one theme at a time: read live, a page rendered under
|
|
52
|
+
* `data-theme="dark"` would report the dark canvas as the light one and the whole dark-seed search
|
|
53
|
+
* would target the wrong number, silently. The CSSOM carries both declarations side by side no
|
|
54
|
+
* matter which theme is on screen.
|
|
55
|
+
*
|
|
56
|
+
* The CHANNELS are the opposite case: `--primary-hover-darken-channels` and its three siblings are
|
|
57
|
+
* declared once at `:root` and never re-scoped (`.dark` only repoints `--primary-hover-channels`
|
|
58
|
+
* AT one of them), so a computed read is correct under any theme and is one line instead of a walk.
|
|
59
|
+
*/
|
|
60
|
+
export type ThemeSpine = { lightCanvas: Hsl; darkCanvas: Hsl; channels: BrandChannels };
|
|
61
|
+
|
|
62
|
+
/** Every style rule in the document, walking into `@layer` / `@media` / `@supports` and nesting. */
|
|
63
|
+
function* styleRules(rules: CSSRuleList): Generator<CSSStyleRule> {
|
|
64
|
+
for (const rule of Array.from(rules)) {
|
|
65
|
+
if ("selectorText" in rule) yield rule as CSSStyleRule;
|
|
66
|
+
const nested = (rule as CSSGroupingRule).cssRules;
|
|
67
|
+
if (nested) yield* styleRules(nested);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/** The LAST `--background` declared by a rule this selector test accepts — cascade order. */
|
|
72
|
+
function canvasFrom(accepts: (selector: string) => boolean): Hsl | null {
|
|
73
|
+
let found: Hsl | null = null;
|
|
74
|
+
for (const sheet of Array.from(document.styleSheets)) {
|
|
75
|
+
let rules: CSSRuleList;
|
|
76
|
+
try {
|
|
77
|
+
rules = sheet.cssRules;
|
|
78
|
+
} catch {
|
|
79
|
+
/* A cross-origin sheet refuses `cssRules`. Ours are same-origin; skip anything else. */
|
|
80
|
+
continue;
|
|
81
|
+
}
|
|
82
|
+
for (const rule of styleRules(rules)) {
|
|
83
|
+
if (!accepts(rule.selectorText)) continue;
|
|
84
|
+
const value = rule.style.getPropertyValue("--background").trim();
|
|
85
|
+
if (!value) continue;
|
|
86
|
+
try {
|
|
87
|
+
found = triplet(value);
|
|
88
|
+
} catch {
|
|
89
|
+
/* Not a plain `H S% L%` triple — not a canvas this derivation can use. */
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
return found;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* The probe fallback, for the one case the CSSOM cannot answer: a sheet the document can paint from
|
|
98
|
+
* but not read. It costs a layout, so it only runs when the walk came back empty.
|
|
99
|
+
*/
|
|
100
|
+
function canvasByProbe(dark: boolean): Hsl | null {
|
|
101
|
+
const probe = document.createElement("div");
|
|
102
|
+
if (dark) probe.className = "dark";
|
|
103
|
+
document.body.append(probe);
|
|
104
|
+
try {
|
|
105
|
+
const value = getComputedStyle(probe).getPropertyValue("--background").trim();
|
|
106
|
+
return value ? triplet(value) : null;
|
|
107
|
+
} catch {
|
|
108
|
+
return null;
|
|
109
|
+
} finally {
|
|
110
|
+
probe.remove();
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
const channel = (name: string) =>
|
|
115
|
+
getComputedStyle(document.documentElement).getPropertyValue(name).trim();
|
|
116
|
+
|
|
117
|
+
/** The spine this document is actually painting, or `null` when it cannot be read at all. */
|
|
118
|
+
export function readThemeSpine(): ThemeSpine | null {
|
|
119
|
+
const lightCanvas = canvasFrom((s) => s.trim() === ":root") ?? canvasByProbe(false);
|
|
120
|
+
const darkCanvas = canvasFrom((s) => /\[data-theme=["']?dark/.test(s)) ?? canvasByProbe(true);
|
|
121
|
+
const channels: BrandChannels = {
|
|
122
|
+
hoverDarken: channel("--primary-hover-darken-channels"),
|
|
123
|
+
activeDarken: channel("--primary-active-darken-channels"),
|
|
124
|
+
hoverLighten: channel("--primary-hover-lighten-channels"),
|
|
125
|
+
};
|
|
126
|
+
if (!lightCanvas || !darkCanvas) return null;
|
|
127
|
+
if (!channels.hoverDarken || !channels.activeDarken || !channels.hoverLighten) return null;
|
|
128
|
+
return { lightCanvas, darkCanvas, channels };
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/* ── the scope a preview pane declares ──────────────────────────────────────────────────────── */
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* THE THREE TOKENS `gen:brand` EMITS ARE THE THREE A SCOPE MUST DECLARE, and that is not a
|
|
135
|
+
* coincidence — it is the freeze rule (docs/TOKENS.md) read from the other end.
|
|
136
|
+
*
|
|
137
|
+
* `--primary` and `--primary-foreground` are ordinary roles: declare them on the pane and every
|
|
138
|
+
* descendant paints from them, including `--primary-hover` and `--text-link`, which are `initial`
|
|
139
|
+
* knobs whose defaults re-resolve at the element that paints (gh#678, gh#664). Writing THOSE here
|
|
140
|
+
* is the drift `gen:brand` refuses to emit, so this does not write them either.
|
|
141
|
+
*
|
|
142
|
+
* `--ring` is the one that bites. `derived.css` declares `--ring: var(--primary)` — and a `var()`
|
|
143
|
+
* substitutes where it is DECLARED, so at `:root` it computed against the package's own seed and
|
|
144
|
+
* inherits that frozen answer into every subtree. The dark pane escapes by accident (it carries
|
|
145
|
+
* `.dark`, and `derived.css` re-declares `--ring: var(--primary)` in that block too, which
|
|
146
|
+
* re-substitutes on the pane); the LIGHT pane does not, and a focus ring in the old brand colour
|
|
147
|
+
* on a re-themed button is exactly the defect. `derived.css` says so in as many words: "a
|
|
148
|
+
* `--primary` scoped BELOW `<html>` does not reach `--ring` … a nested scope sets `--ring`
|
|
149
|
+
* itself". So both panes set it, which is also what a consumer's `:root` gets from `gen:brand`.
|
|
150
|
+
*
|
|
151
|
+
* The `-channels` PAIR is written only when the seed's label runs against the theme's default,
|
|
152
|
+
* because that is the one thing CSS cannot infer: the label decides which way a state steps, and
|
|
153
|
+
* only the author knows the label. It is a role-mirror, so it is written as `var(--…-channels)`
|
|
154
|
+
* and never as a resolved literal.
|
|
155
|
+
*/
|
|
156
|
+
export function previewScope(theme: BrandTheme): CSSProperties {
|
|
157
|
+
const scope: Record<string, string> = {
|
|
158
|
+
"--primary": asTriplet(theme.seed),
|
|
159
|
+
"--primary-foreground": asTriplet(theme.label),
|
|
160
|
+
"--ring": asTriplet(theme.seed),
|
|
161
|
+
};
|
|
162
|
+
if (theme.repointsChannels) {
|
|
163
|
+
scope["--primary-hover-channels"] = `var(--primary-hover-${theme.polarity}-channels)`;
|
|
164
|
+
scope["--primary-active-channels"] = `var(--primary-active-${theme.polarity}-channels)`;
|
|
165
|
+
}
|
|
166
|
+
return scope as CSSProperties;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/* ── the seeds a demo must not avoid ────────────────────────────────────────────────────────── */
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* AN EDITOR THAT ONLY DEMOS A NICE BLUE PROVES NOTHING. Each of these is here because it produces
|
|
173
|
+
* an answer the happy path hides; `hex` is the only input, everything else is measured.
|
|
174
|
+
*/
|
|
175
|
+
export type SeedPreset = {
|
|
176
|
+
id: "godx" | "blue" | "very-light" | "very-dark" | "sunflower";
|
|
177
|
+
hex: string;
|
|
178
|
+
};
|
|
179
|
+
|
|
180
|
+
export const SEED_PRESETS: SeedPreset[] = [
|
|
181
|
+
/* The package's own `--primary`: the export must reproduce foundation.css's light block. */
|
|
182
|
+
{ id: "godx", hex: "#7A00FF" },
|
|
183
|
+
/* An ordinary, well-behaved brand — every row clears, so a passing readout is visible too. */
|
|
184
|
+
{ id: "blue", hex: "#2563EB" },
|
|
185
|
+
/* Near-white: the label flips to black and the fill cannot clear 3:1 on a near-white canvas. */
|
|
186
|
+
{ id: "very-light", hex: "#FFF9C4" },
|
|
187
|
+
/* Near-black: the dark-seed search runs all the way to pure white to hold parity. */
|
|
188
|
+
{ id: "very-dark", hex: "#0B0B14" },
|
|
189
|
+
/* A dark label in light and a light label in dark — the seed that repoints the `-channels`
|
|
190
|
+
* pair in BOTH theme blocks, which nothing else here exercises. */
|
|
191
|
+
{ id: "sunflower", hex: "#F5D60A" },
|
|
192
|
+
];
|
|
193
|
+
|
|
194
|
+
/** One place that knows how a hex becomes a brand, so the page never calls `deriveBrand` twice. */
|
|
195
|
+
export function brandFor(
|
|
196
|
+
spine: ThemeSpine,
|
|
197
|
+
hex: string,
|
|
198
|
+
name: string,
|
|
199
|
+
foreground: string | null,
|
|
200
|
+
darkLightness: number | null,
|
|
201
|
+
) {
|
|
202
|
+
return deriveBrand({ hex, name, foreground, darkLightness, ...spine });
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/**
|
|
206
|
+
* THE COMMAND THAT REPRODUCES THIS EXPORT, or `null` when nothing does.
|
|
207
|
+
*
|
|
208
|
+
* The seed and the forced label are both `gen:brand` flags, so those exports are byte-identical to
|
|
209
|
+
* what the CLI writes and the page can say which command to run. An AUTHORED dark lightness is
|
|
210
|
+
* not: the generator searches for parity and has no flag to override it, so the page has to stop
|
|
211
|
+
* claiming agreement rather than print a command that emits something else.
|
|
212
|
+
*/
|
|
213
|
+
export function genBrandCommand(
|
|
214
|
+
hex: string,
|
|
215
|
+
name: string,
|
|
216
|
+
foreground: string | null,
|
|
217
|
+
darkLightnessOverridden: boolean,
|
|
218
|
+
): string | null {
|
|
219
|
+
if (darkLightnessOverridden) return null;
|
|
220
|
+
const flags = foreground ? ` --foreground '${foreground}'` : "";
|
|
221
|
+
return `pnpm gen:brand '${hex}' --name ${name}${flags}`;
|
|
222
|
+
}
|