@godxjp/ui 28.7.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/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/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/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/contracts/measurement.json +1 -1
- package/dist/i18n/messages/en.json +517 -0
- package/dist/i18n/messages/ja.json +513 -0
- package/dist/i18n/messages/vi.json +513 -0
- package/dist/lib/hooks.d.ts +68 -0
- package/dist/lib/hooks.js +52 -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 +95 -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 +359 -4
- package/dist/props/registry.js +472 -3
- 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/layout.css +79 -0
- package/dist/styles/motion.css +121 -1
- package/dist/styles/navigation-layout.css +397 -1
- 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 +1 -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/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 +19 -1
- package/docs/DESIGN-AUTHORITY.md +99 -18
- package/docs/FRAME-COVERAGE-REPORT.md +7 -2
- package/docs/data-display/marquee.tsx +254 -0
- 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 +5 -3
- package/scripts/brand-accent.generated.mjs +27 -0
- package/scripts/ui-audit.mjs +66 -0
- package/scripts/visual-audit-rules.mjs +46 -2
|
@@ -0,0 +1,285 @@
|
|
|
1
|
+
import { useRef, useState } from "react";
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
Badge,
|
|
5
|
+
Card,
|
|
6
|
+
CardContent,
|
|
7
|
+
CardDescription,
|
|
8
|
+
CardHeader,
|
|
9
|
+
CardTitle,
|
|
10
|
+
Descriptions,
|
|
11
|
+
ScrollArea,
|
|
12
|
+
} from "@godxjp/ui/data-display";
|
|
13
|
+
import { Heading, Paragraph, Text } from "@godxjp/ui/general";
|
|
14
|
+
import { Flex, PageContainer } from "@godxjp/ui/layout";
|
|
15
|
+
import { Anchor, type AnchorItemProp } from "@godxjp/ui/navigation";
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Anchor — Ant Design `Anchor` (6.6.5). The in-page section navigation, and the only thing in the
|
|
19
|
+
* library that COMPUTES which section is current.
|
|
20
|
+
*
|
|
21
|
+
* This page is built out of the cases that break a table of contents, not the one that flatters it:
|
|
22
|
+
*
|
|
23
|
+
* — a FORTY-entry rail beside a TWO-entry one, in the same screen, because the rail that works at
|
|
24
|
+
* two entries is not the rail that works at forty (it scrolls, and the ink rail has to keep up);
|
|
25
|
+
* — a 71-character UNBREAKABLE label, which is a real section title (a config key, a German
|
|
26
|
+
* compound, a URL) and the shape that widens a rail until the page grows a scrollbar;
|
|
27
|
+
* — CJK beside Latin in the same list, where the same nominal size is a taller line box;
|
|
28
|
+
* — a section TALLER than its scrollport, which is where an IntersectionObserver band goes blank
|
|
29
|
+
* and this component keeps answering;
|
|
30
|
+
* — the HORIZONTAL direction, which is what a narrow viewport collapses to;
|
|
31
|
+
* — and a bounded `getContainer`, because a page is very often not the thing that scrolls.
|
|
32
|
+
*
|
|
33
|
+
* Composed only from real @godxjp/ui components.
|
|
34
|
+
*/
|
|
35
|
+
|
|
36
|
+
/** A real section title that cannot be broken anywhere. Exactly 71 characters. */
|
|
37
|
+
const UNBREAKABLE = "RegionalDeploymentConfigurationParameterNamespaceIdentifierOverrides123";
|
|
38
|
+
|
|
39
|
+
const PRIMER: AnchorItemProp[] = [
|
|
40
|
+
{ key: "p1", href: "#primer-what", title: "What it resolves" },
|
|
41
|
+
{ key: "p2", href: "#primer-why", title: "Why not sticky" },
|
|
42
|
+
];
|
|
43
|
+
|
|
44
|
+
const CHAPTERS = [
|
|
45
|
+
{ id: "ch-intro", latin: "Introduction", cjk: "はじめに" },
|
|
46
|
+
{ id: "ch-scope", latin: "Scope and audience", cjk: "対象範囲と読者" },
|
|
47
|
+
{ id: "ch-terms", latin: "Defined terms", cjk: "用語の定義" },
|
|
48
|
+
{ id: "ch-accounts", latin: "Accounts", cjk: "アカウント" },
|
|
49
|
+
{ id: "ch-identity", latin: "Identity verification", cjk: "本人確認" },
|
|
50
|
+
{ id: "ch-billing", latin: "Billing", cjk: "請求" },
|
|
51
|
+
{ id: "ch-invoices", latin: "Invoices and receipts", cjk: "請求書と領収書" },
|
|
52
|
+
{ id: "ch-taxes", latin: "Taxes", cjk: "税" },
|
|
53
|
+
{ id: "ch-refunds", latin: "Refunds", cjk: "返金" },
|
|
54
|
+
{ id: "ch-plans", latin: "Plans", cjk: "プラン" },
|
|
55
|
+
{ id: "ch-limits", latin: "Usage limits", cjk: "利用上限" },
|
|
56
|
+
{ id: "ch-fair-use", latin: "Fair use", cjk: "公正利用" },
|
|
57
|
+
{ id: "ch-data", latin: "Data ownership", cjk: "データの帰属" },
|
|
58
|
+
{ id: "ch-retention", latin: "Retention", cjk: "保持期間" },
|
|
59
|
+
{ id: "ch-deletion", latin: "Deletion", cjk: "削除" },
|
|
60
|
+
{ id: "ch-export", latin: "Export", cjk: "エクスポート" },
|
|
61
|
+
{ id: "ch-security", latin: "Security", cjk: "セキュリティ" },
|
|
62
|
+
{ id: "ch-incident", latin: "Incident response", cjk: "インシデント対応" },
|
|
63
|
+
{ id: "ch-subprocessors", latin: "Sub-processors", cjk: "再委託先" },
|
|
64
|
+
{ id: "ch-transfers", latin: "International transfers", cjk: "国際移転" },
|
|
65
|
+
{ id: "ch-availability", latin: "Availability", cjk: "可用性" },
|
|
66
|
+
{ id: "ch-maintenance", latin: "Maintenance windows", cjk: "メンテナンス" },
|
|
67
|
+
{ id: "ch-support", latin: "Support", cjk: "サポート" },
|
|
68
|
+
{ id: "ch-escalation", latin: "Escalation", cjk: "エスカレーション" },
|
|
69
|
+
{ id: "ch-credits", latin: "Service credits", cjk: "サービスクレジット" },
|
|
70
|
+
{ id: "ch-acceptable", latin: "Acceptable use", cjk: "禁止事項" },
|
|
71
|
+
{ id: "ch-content", latin: "Customer content", cjk: "顧客コンテンツ" },
|
|
72
|
+
{ id: "ch-ip", latin: "Intellectual property", cjk: "知的財産" },
|
|
73
|
+
{ id: "ch-feedback", latin: "Feedback", cjk: "フィードバック" },
|
|
74
|
+
{ id: "ch-publicity", latin: "Publicity", cjk: "公表" },
|
|
75
|
+
{ id: "ch-warranty", latin: "Warranties", cjk: "保証" },
|
|
76
|
+
{ id: "ch-liability", latin: "Limitation of liability", cjk: "責任の制限" },
|
|
77
|
+
{ id: "ch-indemnity", latin: "Indemnity", cjk: "補償" },
|
|
78
|
+
{ id: "ch-term", latin: "Term", cjk: "契約期間" },
|
|
79
|
+
{ id: "ch-suspension", latin: "Suspension", cjk: "利用停止" },
|
|
80
|
+
{ id: "ch-termination", latin: "Termination", cjk: "解約" },
|
|
81
|
+
{ id: "ch-survival", latin: "Survival", cjk: "存続条項" },
|
|
82
|
+
{ id: "ch-law", latin: "Governing law", cjk: "準拠法" },
|
|
83
|
+
{ id: "ch-disputes", latin: "Disputes", cjk: "紛争解決" },
|
|
84
|
+
{ id: "ch-namespace", latin: UNBREAKABLE, cjk: "名前空間の上書き" },
|
|
85
|
+
{ id: "ch-changes", latin: "Changes to these terms", cjk: "本規約の変更" },
|
|
86
|
+
{ id: "ch-contact", latin: "Contact", cjk: "お問い合わせ" },
|
|
87
|
+
];
|
|
88
|
+
|
|
89
|
+
const CHAPTER_ITEMS: AnchorItemProp[] = CHAPTERS.map((chapter) => ({
|
|
90
|
+
key: chapter.id,
|
|
91
|
+
href: `#${chapter.id}`,
|
|
92
|
+
// CJK beside Latin on ONE row: the same nominal size, a taller line box, and the rail has to
|
|
93
|
+
// hold both without the ink rail losing its alignment.
|
|
94
|
+
title: (
|
|
95
|
+
<>
|
|
96
|
+
{chapter.cjk} · {chapter.latin}
|
|
97
|
+
</>
|
|
98
|
+
),
|
|
99
|
+
}));
|
|
100
|
+
|
|
101
|
+
const HORIZONTAL: AnchorItemProp[] = [
|
|
102
|
+
{ key: "h-overview", href: "#bar-overview", title: "概要 · Overview" },
|
|
103
|
+
{ key: "h-pricing", href: "#bar-pricing", title: "価格 · Pricing" },
|
|
104
|
+
{ key: "h-security", href: "#bar-security", title: "セキュリティ · Security" },
|
|
105
|
+
{ key: "h-faq", href: "#bar-faq", title: "よくある質問 · FAQ" },
|
|
106
|
+
];
|
|
107
|
+
|
|
108
|
+
function Chapter({ id, title, lines }: { id: string; title: string; lines: number }) {
|
|
109
|
+
return (
|
|
110
|
+
<section id={id} aria-labelledby={`${id}-heading`}>
|
|
111
|
+
<Heading level={3} id={`${id}-heading`}>
|
|
112
|
+
{title}
|
|
113
|
+
</Heading>
|
|
114
|
+
{Array.from({ length: lines }, (_, line) => (
|
|
115
|
+
<Paragraph key={line}>
|
|
116
|
+
本条は{title}について定めます。The clause is deliberately long enough that a reader spends
|
|
117
|
+
several scroll gestures inside it, which is the case an IntersectionObserver band cannot
|
|
118
|
+
answer and a decision line can.
|
|
119
|
+
</Paragraph>
|
|
120
|
+
))}
|
|
121
|
+
</section>
|
|
122
|
+
);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
export default function Demo() {
|
|
126
|
+
const primerRef = useRef<HTMLDivElement>(null);
|
|
127
|
+
const chaptersRef = useRef<HTMLDivElement>(null);
|
|
128
|
+
const barRef = useRef<HTMLDivElement>(null);
|
|
129
|
+
const [reading, setReading] = useState<string>("");
|
|
130
|
+
|
|
131
|
+
return (
|
|
132
|
+
<PageContainer
|
|
133
|
+
title="Anchor"
|
|
134
|
+
subtitle="antd Anchor · the decision line, the suppressed click, the landing hash, aria-current=location"
|
|
135
|
+
>
|
|
136
|
+
<Flex direction="col" gap="lg">
|
|
137
|
+
<Card>
|
|
138
|
+
<CardHeader>
|
|
139
|
+
<CardTitle>1 · Two entries, and what the component reports</CardTitle>
|
|
140
|
+
<CardDescription>
|
|
141
|
+
The smallest rail there is, bounded by its own `getContainer` rather than the page.
|
|
142
|
+
`onValueChange` hands back the href the SCROLL POSITION resolved, which is the value
|
|
143
|
+
printed below it.
|
|
144
|
+
</CardDescription>
|
|
145
|
+
</CardHeader>
|
|
146
|
+
<CardContent>
|
|
147
|
+
<Flex gap="lg" align="start">
|
|
148
|
+
<Anchor
|
|
149
|
+
affix={false}
|
|
150
|
+
showInkInFixed
|
|
151
|
+
label="Primer sections"
|
|
152
|
+
items={PRIMER}
|
|
153
|
+
getContainer={() => primerRef.current ?? window}
|
|
154
|
+
onValueChange={setReading}
|
|
155
|
+
/>
|
|
156
|
+
<ScrollArea viewportRef={primerRef} className="h-64 flex-1" label="Primer">
|
|
157
|
+
<Flex direction="col" gap="md">
|
|
158
|
+
<Chapter id="primer-what" title="What it resolves" lines={4} />
|
|
159
|
+
<Chapter id="primer-why" title="Why not sticky" lines={4} />
|
|
160
|
+
</Flex>
|
|
161
|
+
</ScrollArea>
|
|
162
|
+
</Flex>
|
|
163
|
+
<Descriptions
|
|
164
|
+
items={[
|
|
165
|
+
{ label: "onValueChange", children: <Text>{reading || "—"}</Text> },
|
|
166
|
+
{
|
|
167
|
+
label: "aria-current",
|
|
168
|
+
children: <Badge variant="secondary">location</Badge>,
|
|
169
|
+
},
|
|
170
|
+
]}
|
|
171
|
+
/>
|
|
172
|
+
</CardContent>
|
|
173
|
+
</Card>
|
|
174
|
+
|
|
175
|
+
<Card>
|
|
176
|
+
<CardHeader>
|
|
177
|
+
<CardTitle>2 · Forty-two entries, CJK beside Latin, and a 71-character label</CardTitle>
|
|
178
|
+
<CardDescription>
|
|
179
|
+
The rail that works at two entries is not the rail that works at forty-two: this one
|
|
180
|
+
scrolls, the ink rail keeps up, and the `{UNBREAKABLE.length}`-character unbreakable
|
|
181
|
+
title wraps instead of widening the column until the page grows a scrollbar.
|
|
182
|
+
</CardDescription>
|
|
183
|
+
</CardHeader>
|
|
184
|
+
<CardContent>
|
|
185
|
+
<Flex gap="lg" align="start">
|
|
186
|
+
<ScrollArea className="h-96 w-64 shrink-0" label="Contents">
|
|
187
|
+
<Anchor
|
|
188
|
+
affix={false}
|
|
189
|
+
showInkInFixed
|
|
190
|
+
label="Agreement contents"
|
|
191
|
+
items={CHAPTER_ITEMS}
|
|
192
|
+
getContainer={() => chaptersRef.current ?? window}
|
|
193
|
+
/>
|
|
194
|
+
</ScrollArea>
|
|
195
|
+
<ScrollArea viewportRef={chaptersRef} className="h-96 flex-1" label="Agreement">
|
|
196
|
+
<Flex direction="col" gap="md">
|
|
197
|
+
{CHAPTERS.map((chapter) => (
|
|
198
|
+
<Chapter
|
|
199
|
+
key={chapter.id}
|
|
200
|
+
id={chapter.id}
|
|
201
|
+
title={`${chapter.cjk} · ${chapter.latin}`}
|
|
202
|
+
// One chapter is far taller than the scrollport on purpose — the case a
|
|
203
|
+
// band-based scrollspy cannot answer and a decision line can.
|
|
204
|
+
lines={chapter.id === "ch-billing" ? 24 : 3}
|
|
205
|
+
/>
|
|
206
|
+
))}
|
|
207
|
+
</Flex>
|
|
208
|
+
</ScrollArea>
|
|
209
|
+
</Flex>
|
|
210
|
+
</CardContent>
|
|
211
|
+
</Card>
|
|
212
|
+
|
|
213
|
+
<Card>
|
|
214
|
+
<CardHeader>
|
|
215
|
+
<CardTitle>3 · direction="horizontal" — the narrow case</CardTitle>
|
|
216
|
+
<CardDescription>
|
|
217
|
+
One row that scrolls inline rather than wrapping onto a second, because the ink rail
|
|
218
|
+
has to stay beside the label it belongs to. Nesting is not available here, and Ant
|
|
219
|
+
Design says the same.
|
|
220
|
+
</CardDescription>
|
|
221
|
+
</CardHeader>
|
|
222
|
+
<CardContent>
|
|
223
|
+
<Anchor
|
|
224
|
+
affix={false}
|
|
225
|
+
showInkInFixed
|
|
226
|
+
direction="horizontal"
|
|
227
|
+
label="Page sections"
|
|
228
|
+
items={HORIZONTAL}
|
|
229
|
+
getContainer={() => barRef.current ?? window}
|
|
230
|
+
/>
|
|
231
|
+
<ScrollArea viewportRef={barRef} className="h-64" label="Sections">
|
|
232
|
+
<Flex direction="col" gap="md">
|
|
233
|
+
<Chapter id="bar-overview" title="概要 · Overview" lines={3} />
|
|
234
|
+
<Chapter id="bar-pricing" title="価格 · Pricing" lines={3} />
|
|
235
|
+
<Chapter id="bar-security" title="セキュリティ · Security" lines={3} />
|
|
236
|
+
<Chapter id="bar-faq" title="よくある質問 · FAQ" lines={3} />
|
|
237
|
+
</Flex>
|
|
238
|
+
</ScrollArea>
|
|
239
|
+
</CardContent>
|
|
240
|
+
</Card>
|
|
241
|
+
|
|
242
|
+
<Card>
|
|
243
|
+
<CardHeader>
|
|
244
|
+
<CardTitle>4 · Against the page, pinned by Affix</CardTitle>
|
|
245
|
+
<CardDescription>
|
|
246
|
+
The default. `affix` is `true`, so the rail pins itself once it reaches the line;
|
|
247
|
+
`targetOffsetBlockStart` is the room a clicked section leaves for whatever is pinned
|
|
248
|
+
above it. Landing on one of these hashes selects the entry with no scroll event at
|
|
249
|
+
all, and the click moves FOCUS to the section, not just the scroll position.
|
|
250
|
+
</CardDescription>
|
|
251
|
+
</CardHeader>
|
|
252
|
+
<CardContent>
|
|
253
|
+
<Flex gap="lg" align="start">
|
|
254
|
+
<Anchor
|
|
255
|
+
offsetBlockStart={16}
|
|
256
|
+
targetOffsetBlockStart={24}
|
|
257
|
+
label="On this page"
|
|
258
|
+
items={[
|
|
259
|
+
{ key: "page-a", href: "#page-alpha", title: "Alpha · 第一章" },
|
|
260
|
+
{
|
|
261
|
+
key: "page-b",
|
|
262
|
+
href: "#page-beta",
|
|
263
|
+
title: "Beta · 第二章",
|
|
264
|
+
children: [
|
|
265
|
+
{ key: "page-b1", href: "#page-beta-one", title: "Beta, part one" },
|
|
266
|
+
{ key: "page-b2", href: "#page-beta-two", title: "Beta, part two" },
|
|
267
|
+
],
|
|
268
|
+
},
|
|
269
|
+
{ key: "page-c", href: "#page-gamma", title: "Gamma · 第三章" },
|
|
270
|
+
]}
|
|
271
|
+
/>
|
|
272
|
+
<Flex direction="col" gap="md" className="flex-1">
|
|
273
|
+
<Chapter id="page-alpha" title="Alpha · 第一章" lines={6} />
|
|
274
|
+
<Chapter id="page-beta" title="Beta · 第二章" lines={2} />
|
|
275
|
+
<Chapter id="page-beta-one" title="Beta, part one" lines={6} />
|
|
276
|
+
<Chapter id="page-beta-two" title="Beta, part two" lines={6} />
|
|
277
|
+
<Chapter id="page-gamma" title="Gamma · 第三章" lines={8} />
|
|
278
|
+
</Flex>
|
|
279
|
+
</Flex>
|
|
280
|
+
</CardContent>
|
|
281
|
+
</Card>
|
|
282
|
+
</Flex>
|
|
283
|
+
</PageContainer>
|
|
284
|
+
);
|
|
285
|
+
}
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import { Text } from "@godxjp/ui/general";
|
|
2
|
+
import { Flex, PageContainer } from "@godxjp/ui/layout";
|
|
3
|
+
import { MegaMenu, type MegaMenuItemProp } from "@godxjp/ui/navigation";
|
|
4
|
+
import { ShieldCheck, Users } from "lucide-react";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* MegaMenu — ONE panel, open, and nothing else on the page.
|
|
8
|
+
*
|
|
9
|
+
* A separate frame from `mega-menu.tsx` on purpose. An open panel is an OVERLAY: it is
|
|
10
|
+
* `position: fixed` so that `Topbar`'s `overflow: clip` slots and `Card`'s `overflow: hidden` do
|
|
11
|
+
* not clip it away, which means an always-open panel paints over whatever is beneath it. On a
|
|
12
|
+
* page of demo cards that is a measurement hazard — the open panel covers the next card and every
|
|
13
|
+
* sweep of that card reads the panel instead. So the open state lives here, alone, where it is
|
|
14
|
+
* the subject rather than the obstruction.
|
|
15
|
+
*
|
|
16
|
+
* This is the edge case page: a column with an icon and a description beside a bare one, a
|
|
17
|
+
* disabled row, a 71-character unbreakable identifier, and CJK beside Latin in the same column.
|
|
18
|
+
*/
|
|
19
|
+
const ITEMS: MegaMenuItemProp[] = [
|
|
20
|
+
{
|
|
21
|
+
key: "products",
|
|
22
|
+
label: "製品",
|
|
23
|
+
panel: {
|
|
24
|
+
groups: [
|
|
25
|
+
{
|
|
26
|
+
key: "core",
|
|
27
|
+
label: "コア業務",
|
|
28
|
+
icon: <Users />,
|
|
29
|
+
description: "毎日使う基幹アプリ。人事と給与はひとつの従業員台帳を共有します。",
|
|
30
|
+
links: [
|
|
31
|
+
{
|
|
32
|
+
key: "hr",
|
|
33
|
+
label: "人事管理 / HR",
|
|
34
|
+
href: "/hr",
|
|
35
|
+
icon: <Users />,
|
|
36
|
+
description: "従業員台帳、異動履歴、組織図",
|
|
37
|
+
},
|
|
38
|
+
{ key: "payroll", label: "給与計算 / Payroll", href: "/payroll" },
|
|
39
|
+
],
|
|
40
|
+
},
|
|
41
|
+
{
|
|
42
|
+
key: "addons",
|
|
43
|
+
label: "アドオン",
|
|
44
|
+
links: [
|
|
45
|
+
{ key: "attendance", label: "勤怠管理", href: "/attendance" },
|
|
46
|
+
{ key: "workflow", label: "ワークフロー", href: "/workflow" },
|
|
47
|
+
{ key: "eol", label: "提供終了予定のモジュール", href: "/eol", disabled: true },
|
|
48
|
+
],
|
|
49
|
+
},
|
|
50
|
+
{
|
|
51
|
+
key: "platform",
|
|
52
|
+
label: "Platform",
|
|
53
|
+
icon: <ShieldCheck />,
|
|
54
|
+
links: [
|
|
55
|
+
{ key: "sso", label: "SSO / SAML", href: "/sso" },
|
|
56
|
+
{
|
|
57
|
+
key: "webhook",
|
|
58
|
+
label: "webhookEndpointConfigurationForOutboundPayrollNotificationsV2",
|
|
59
|
+
href: "/webhooks",
|
|
60
|
+
description: "エンドポイント設定",
|
|
61
|
+
},
|
|
62
|
+
],
|
|
63
|
+
},
|
|
64
|
+
],
|
|
65
|
+
footer: (
|
|
66
|
+
<Text tone="muted" size="sm">
|
|
67
|
+
すべての製品と価格は製品一覧ページでご覧いただけます。
|
|
68
|
+
</Text>
|
|
69
|
+
),
|
|
70
|
+
},
|
|
71
|
+
},
|
|
72
|
+
{ key: "pricing", label: "料金", href: "/pricing" },
|
|
73
|
+
];
|
|
74
|
+
|
|
75
|
+
export default function MegaMenuPanelDoc() {
|
|
76
|
+
return (
|
|
77
|
+
<PageContainer
|
|
78
|
+
title="MegaMenu · 開いたパネル"
|
|
79
|
+
subtitle="1 つの nav、パネルは開いたまま · アイコンと説明のある列 / 素の列 / 無効な行 / 71 文字の識別子"
|
|
80
|
+
>
|
|
81
|
+
<Flex direction="col" gap="lg">
|
|
82
|
+
<MegaMenu label="開いたパネルの例" items={ITEMS} defaultOpen="products" value="payroll" />
|
|
83
|
+
</Flex>
|
|
84
|
+
</PageContainer>
|
|
85
|
+
);
|
|
86
|
+
}
|
|
@@ -0,0 +1,254 @@
|
|
|
1
|
+
import { useState } from "react";
|
|
2
|
+
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@godxjp/ui/data-display";
|
|
3
|
+
import { Text } from "@godxjp/ui/general";
|
|
4
|
+
import { Flex, PageContainer } from "@godxjp/ui/layout";
|
|
5
|
+
import { MegaMenu, type MegaMenuItemProp } from "@godxjp/ui/navigation";
|
|
6
|
+
import {
|
|
7
|
+
BookOpen,
|
|
8
|
+
Building2,
|
|
9
|
+
Calculator,
|
|
10
|
+
CreditCard,
|
|
11
|
+
FileText,
|
|
12
|
+
LifeBuoy,
|
|
13
|
+
ShieldCheck,
|
|
14
|
+
Users,
|
|
15
|
+
} from "lucide-react";
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* MegaMenu — the primary site navigation whose top-level items disclose a full-width panel.
|
|
19
|
+
*
|
|
20
|
+
* WAI-ARIA APG **Disclosure Navigation with Top-Level Links**, not Menubar: a bar of links to
|
|
21
|
+
* PLACES is not a menu of COMMANDS, and `role="menu"` would promise assistive technology an
|
|
22
|
+
* application menu. That is also why this is not `DropdownMenu` plus a token — `DropdownMenu` is
|
|
23
|
+
* `role="menu"` by construction.
|
|
24
|
+
*
|
|
25
|
+
* The cards below are deliberately NOT tidy. Each one is an edge a real site hits on day one and a
|
|
26
|
+
* demo page usually hides: a panel with forty links beside a panel with one, a 71-character
|
|
27
|
+
* unbreakable label, CJK beside Latin, a group carrying an icon and a description beside a bare
|
|
28
|
+
* column, and the narrow layout where the panel has nowhere to go.
|
|
29
|
+
*/
|
|
30
|
+
|
|
31
|
+
/** Forty links, which is the size at which a panel stops being a list and starts being a screen. */
|
|
32
|
+
const FORTY = Array.from({ length: 40 }, (_, index) => ({
|
|
33
|
+
key: `report-${index + 1}`,
|
|
34
|
+
label: `レポート ${String(index + 1).padStart(2, "0")}`,
|
|
35
|
+
href: `/reports/${index + 1}`,
|
|
36
|
+
}));
|
|
37
|
+
|
|
38
|
+
/** The bar every other card varies. CJK labels beside Latin ones, on purpose. */
|
|
39
|
+
const ITEMS: MegaMenuItemProp[] = [
|
|
40
|
+
{
|
|
41
|
+
key: "products",
|
|
42
|
+
label: "製品",
|
|
43
|
+
icon: <Building2 />,
|
|
44
|
+
panel: {
|
|
45
|
+
groups: [
|
|
46
|
+
{
|
|
47
|
+
key: "core",
|
|
48
|
+
label: "コア業務",
|
|
49
|
+
icon: <Users />,
|
|
50
|
+
description: "毎日使う基幹アプリ。人事と給与はひとつの従業員台帳を共有します。",
|
|
51
|
+
links: [
|
|
52
|
+
{
|
|
53
|
+
key: "hr",
|
|
54
|
+
label: "人事管理 / HR",
|
|
55
|
+
href: "/hr",
|
|
56
|
+
icon: <Users />,
|
|
57
|
+
description: "従業員台帳、異動履歴、組織図",
|
|
58
|
+
},
|
|
59
|
+
{
|
|
60
|
+
key: "payroll",
|
|
61
|
+
label: "給与計算 / Payroll",
|
|
62
|
+
href: "/payroll",
|
|
63
|
+
icon: <Calculator />,
|
|
64
|
+
description: "月次締め、賞与、年末調整",
|
|
65
|
+
},
|
|
66
|
+
{
|
|
67
|
+
key: "expense",
|
|
68
|
+
label: "経費精算 / Expenses",
|
|
69
|
+
href: "/expense",
|
|
70
|
+
icon: <CreditCard />,
|
|
71
|
+
description: "申請から振込まで",
|
|
72
|
+
},
|
|
73
|
+
],
|
|
74
|
+
},
|
|
75
|
+
{
|
|
76
|
+
// A BARE column beside the decorated one above: no icon, no description, no heading
|
|
77
|
+
// beyond the label. Both shapes have to look deliberate side by side.
|
|
78
|
+
key: "addons",
|
|
79
|
+
label: "アドオン",
|
|
80
|
+
links: [
|
|
81
|
+
{ key: "attendance", label: "勤怠管理", href: "/attendance" },
|
|
82
|
+
{ key: "workflow", label: "ワークフロー", href: "/workflow" },
|
|
83
|
+
{ key: "mynumber", label: "マイナンバー管理", href: "/mynumber" },
|
|
84
|
+
{ key: "eol", label: "提供終了予定のモジュール", href: "/eol", disabled: true },
|
|
85
|
+
],
|
|
86
|
+
},
|
|
87
|
+
{
|
|
88
|
+
key: "platform",
|
|
89
|
+
label: "Platform",
|
|
90
|
+
icon: <ShieldCheck />,
|
|
91
|
+
links: [
|
|
92
|
+
{ key: "sso", label: "SSO / SAML", href: "/sso" },
|
|
93
|
+
{ key: "audit", label: "Audit log", href: "/audit" },
|
|
94
|
+
{
|
|
95
|
+
// 71 characters, no space, no hyphen — a real webhook identifier pasted into a nav.
|
|
96
|
+
// It must WRAP inside its column, never widen the panel past the viewport.
|
|
97
|
+
key: "webhook",
|
|
98
|
+
label: "webhookEndpointConfigurationForOutboundPayrollNotificationsV2",
|
|
99
|
+
href: "/webhooks",
|
|
100
|
+
description: "エンドポイント設定",
|
|
101
|
+
},
|
|
102
|
+
],
|
|
103
|
+
},
|
|
104
|
+
],
|
|
105
|
+
footer: (
|
|
106
|
+
<Text tone="muted" size="sm">
|
|
107
|
+
すべての製品と価格は製品一覧ページでご覧いただけます。
|
|
108
|
+
</Text>
|
|
109
|
+
),
|
|
110
|
+
},
|
|
111
|
+
},
|
|
112
|
+
{
|
|
113
|
+
key: "reports",
|
|
114
|
+
label: "レポート",
|
|
115
|
+
icon: <FileText />,
|
|
116
|
+
panel: {
|
|
117
|
+
groups: [
|
|
118
|
+
{ key: "all", label: "標準レポート (40)", links: FORTY.slice(0, 20) },
|
|
119
|
+
{ key: "more", label: "続き", links: FORTY.slice(20) },
|
|
120
|
+
],
|
|
121
|
+
},
|
|
122
|
+
},
|
|
123
|
+
{
|
|
124
|
+
key: "support",
|
|
125
|
+
label: "Support",
|
|
126
|
+
icon: <LifeBuoy />,
|
|
127
|
+
panel: {
|
|
128
|
+
// ONE link. The narrowest panel a consumer will ever build, and it must not look broken
|
|
129
|
+
// beside the forty-link one above.
|
|
130
|
+
groups: [
|
|
131
|
+
{
|
|
132
|
+
key: "contact",
|
|
133
|
+
label: "お問い合わせ",
|
|
134
|
+
links: [{ key: "contact-us", label: "サポートに連絡する", href: "/support" }],
|
|
135
|
+
},
|
|
136
|
+
],
|
|
137
|
+
},
|
|
138
|
+
},
|
|
139
|
+
{ key: "docs", label: "ドキュメント", href: "/docs", icon: <BookOpen /> },
|
|
140
|
+
{ key: "pricing", label: "料金", href: "/pricing" },
|
|
141
|
+
];
|
|
142
|
+
|
|
143
|
+
export default function MegaMenuDoc() {
|
|
144
|
+
const [route, setRoute] = useState("payroll");
|
|
145
|
+
|
|
146
|
+
return (
|
|
147
|
+
<PageContainer
|
|
148
|
+
title="MegaMenu"
|
|
149
|
+
subtitle="APG Disclosure Navigation · 全幅パネル · roving tabindex · hover intent · 狭い画面ではアコーディオン"
|
|
150
|
+
>
|
|
151
|
+
<Flex direction="col" gap="lg">
|
|
152
|
+
{/* 1 · IN A REAL BAR — the component where it actually lives. */}
|
|
153
|
+
<Card>
|
|
154
|
+
<CardHeader>
|
|
155
|
+
<CardTitle level={2}>サイトのヘッダー · ロゴの隣に置く</CardTitle>
|
|
156
|
+
<CardDescription>
|
|
157
|
+
バーのふるまい(roving tabindex・aria-expanded・Escape で戻る)はすべて `MegaMenu`
|
|
158
|
+
が持ちます。現在のルートは `value` で渡し、`aria-current="page"`
|
|
159
|
+
としてバーとパネルの両方に出ます。
|
|
160
|
+
<br />
|
|
161
|
+
`Topbar` のスロットには入れないでください。実測した理由が 2
|
|
162
|
+
つあります。`topbar-center` は約 1280px 未満で `display: none` になるのでノート PC
|
|
163
|
+
の幅でナビゲーションごと消え、`topbar-start` は `overflow: clip` かつ `flex-wrap:
|
|
164
|
+
nowrap` の 1 行なので、狭い画面で縦に開く アコーディオンが行からはみ出します(375px で
|
|
165
|
+
58 要素がビューポート外)。電話幅のサイトナビは バーの中ではなく `Sheet`
|
|
166
|
+
の中に置く、というのが実サイトの答えです。
|
|
167
|
+
</CardDescription>
|
|
168
|
+
</CardHeader>
|
|
169
|
+
<CardContent>
|
|
170
|
+
<Flex align="center" gap="md">
|
|
171
|
+
<Text weight="semibold">GodX</Text>
|
|
172
|
+
<MegaMenu
|
|
173
|
+
label="メインナビゲーション"
|
|
174
|
+
items={ITEMS}
|
|
175
|
+
value={route}
|
|
176
|
+
onValueChange={setRoute}
|
|
177
|
+
/>
|
|
178
|
+
</Flex>
|
|
179
|
+
</CardContent>
|
|
180
|
+
</Card>
|
|
181
|
+
|
|
182
|
+
{/* 2 · THE EDGES — one link beside forty, an unbreakable token, CJK beside Latin. */}
|
|
183
|
+
<Card>
|
|
184
|
+
<CardHeader>
|
|
185
|
+
<CardTitle level={2}>端 · 1 件のパネルと 40 件のパネル</CardTitle>
|
|
186
|
+
<CardDescription>
|
|
187
|
+
「レポート」は 40 件、「Support」は 1 件。40
|
|
188
|
+
件のパネルは列に折り返して高さを抑え、それでも入り切らなければブロック方向にスクロールします。「Platform」列の
|
|
189
|
+
71 文字の webhook
|
|
190
|
+
識別子は列の中で折り返し、バーやパネルをビューポートの外へ広げません。
|
|
191
|
+
</CardDescription>
|
|
192
|
+
</CardHeader>
|
|
193
|
+
<CardContent>
|
|
194
|
+
<MegaMenu label="端の例" items={ITEMS} />
|
|
195
|
+
</CardContent>
|
|
196
|
+
</Card>
|
|
197
|
+
|
|
198
|
+
{/* 3 · HOVER INTENT — the mode antd defaults to, and the diagonal it has to survive. */}
|
|
199
|
+
<Card>
|
|
200
|
+
<CardHeader>
|
|
201
|
+
<CardTitle level={2}>triggerAction="hover" · 斜めに動くマウス</CardTitle>
|
|
202
|
+
<CardDescription>
|
|
203
|
+
Ant Design の `triggerSubMenuAction`。トリガーから離れただけでは閉じません —
|
|
204
|
+
閉じるのは nav 全体からポインタが出たときだけで、それも `closeDelay`(既定 100ms、antd
|
|
205
|
+
の `subMenuCloseDelay`
|
|
206
|
+
と同じ)の猶予があります。だから「製品」から斜め下のリンクへ一直線に動けます。クリックでも開くので、タッチ端末で行き止まりになりません。
|
|
207
|
+
</CardDescription>
|
|
208
|
+
</CardHeader>
|
|
209
|
+
<CardContent>
|
|
210
|
+
<MegaMenu label="ホバーの例" items={ITEMS} triggerAction="hover" size="sm" />
|
|
211
|
+
</CardContent>
|
|
212
|
+
</Card>
|
|
213
|
+
|
|
214
|
+
{/* 4 · NARROW — the same disclosure, in flow. */}
|
|
215
|
+
<Card>
|
|
216
|
+
<CardHeader>
|
|
217
|
+
<CardTitle level={2}>
|
|
218
|
+
狭い画面 · 同じ disclosure がそのままアコーディオンになる
|
|
219
|
+
</CardTitle>
|
|
220
|
+
<CardDescription>
|
|
221
|
+
全幅パネルは 375px では行き場がありません。768px
|
|
222
|
+
未満ではバーが縦に積み、パネルは自分のトリガーの真下にインフローで開きます。DOM もARIA
|
|
223
|
+
も同じ(`aria-expanded` / `aria-controls`)で、Escape もそのまま効きます。Menubar
|
|
224
|
+
ではなく Disclosure を選んだ配当がこれです。ブラウザを 768px
|
|
225
|
+
より狭くして確かめてください。
|
|
226
|
+
</CardDescription>
|
|
227
|
+
</CardHeader>
|
|
228
|
+
<CardContent>
|
|
229
|
+
<MegaMenu label="狭い画面の例" items={ITEMS} size="lg" />
|
|
230
|
+
</CardContent>
|
|
231
|
+
</Card>
|
|
232
|
+
|
|
233
|
+
{/* 5 · WHAT IT IS NOT. */}
|
|
234
|
+
<Card>
|
|
235
|
+
<CardHeader>
|
|
236
|
+
<CardTitle level={2}>これは DropdownMenu ではない</CardTitle>
|
|
237
|
+
<CardDescription>
|
|
238
|
+
`DropdownMenu` は `role="menu"` /
|
|
239
|
+
`role="menuitem"`(コマンドのメニュー)です。リンクの集まりにこれを使うと、スクリーンリーダーはアプリケーションメニューとして読み上げ、Tab
|
|
240
|
+
はウィジェット全体から出てしまいます。これがメガメニューの典型的な a11y
|
|
241
|
+
欠陥で、このコンポーネントが存在する理由です。操作(削除・名前の変更)には
|
|
242
|
+
`DropdownMenu`、場所への移動には `MegaMenu` を使ってください。
|
|
243
|
+
</CardDescription>
|
|
244
|
+
</CardHeader>
|
|
245
|
+
<CardContent>
|
|
246
|
+
<Text tone="muted" size="sm">
|
|
247
|
+
現在のルート: {route}
|
|
248
|
+
</Text>
|
|
249
|
+
</CardContent>
|
|
250
|
+
</Card>
|
|
251
|
+
</Flex>
|
|
252
|
+
</PageContainer>
|
|
253
|
+
);
|
|
254
|
+
}
|