@godxjp/ui 28.12.0 → 29.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/agent/START-HERE.md +29 -10
- package/agent/components/Anchor.json +6 -1
- package/agent/components/AppLauncher.json +10 -0
- package/agent/components/AppShell.json +1 -1
- package/agent/components/AreaChart.json +19 -1
- package/agent/components/Attachments.json +26 -1
- package/agent/components/BarChart.json +11 -1
- package/agent/components/BranchScopePicker.json +10 -0
- package/agent/components/Cascader.json +6 -1
- package/agent/components/Checkbox.json +6 -0
- package/agent/components/CompactBarTrend.json +1 -1
- package/agent/components/CredentialReveal.json +21 -0
- package/agent/components/DataState.json +1 -1
- package/agent/components/DataTable.json +4 -4
- package/agent/components/FormField.json +1 -1
- package/agent/components/InfiniteQueryState.json +1 -1
- package/agent/components/Input.json +1 -1
- package/agent/components/InputOTP.json +35 -0
- package/agent/components/LineChart.json +20 -2
- package/agent/components/ListRow.json +1 -1
- package/agent/components/Masonry.json +1 -1
- package/agent/components/MasterDetail.json +1 -1
- package/agent/components/PasswordStrength.json +1 -1
- package/agent/components/PermissionMatrix.json +6 -1
- package/agent/components/SearchInput.json +5 -0
- package/agent/components/Select.json +1 -0
- package/agent/components/ServiceRolePanel.json +5 -0
- package/agent/components/Sidebar.json +1 -1
- package/agent/components/Switch.json +6 -0
- package/agent/components/Table.json +8 -3
- package/agent/components/Tabs.json +10 -0
- package/agent/components/ThemeScope.json +49 -0
- package/agent/components/TimeRangePicker.json +5 -0
- package/agent/components/Topbar.json +1 -0
- package/agent/components/TopbarItem.json +2 -1
- package/agent/components/Transfer.json +6 -1
- package/agent/components/TreeSelect.json +1 -1
- package/agent/components/Upload.json +5 -0
- package/agent/components/UploadCropDialog.json +1 -1
- package/agent/components/formatDate.json +1 -1
- package/agent/components-index.json +5 -0
- package/agent/components.json +297 -31
- package/agent/index.json +19 -9
- package/agent/llms.txt +10 -10
- package/agent/patterns/tenant-brand-color.json +28 -0
- package/agent/patterns-index.json +27 -0
- package/agent/patterns.json +28 -0
- package/agent/rules.json +15 -0
- package/agent/tokens.json +4965 -970
- package/dist/app/index.d.ts +3 -0
- package/dist/app/index.js +3 -0
- package/dist/app/tenant-theme.d.ts +80 -0
- package/dist/app/tenant-theme.js +154 -0
- package/dist/app/theme-axes.d.ts +14 -1
- package/dist/app/theme-axes.js +24 -31
- package/dist/components/charts/chart-cartesian.d.ts +5 -1
- package/dist/components/charts/chart-cartesian.js +15 -8
- package/dist/components/data-display/badge.d.ts +1 -1
- package/dist/components/data-display/badge.js +20 -2
- package/dist/components/data-display/carousel.js +4 -4
- package/dist/components/data-display/data-table.js +13 -2
- package/dist/components/data-display/permission-matrix.js +1 -1
- package/dist/components/data-display/table.d.ts +11 -2
- package/dist/components/data-display/table.js +18 -2
- package/dist/components/data-entry/control-appearance.d.ts +12 -6
- package/dist/components/data-entry/control-appearance.js +1 -1
- package/dist/components/data-entry/select.js +4 -3
- package/dist/components/feedback/dialog.js +6 -3
- package/dist/components/feedback/overlay-header-tone.d.ts +7 -0
- package/dist/components/feedback/overlay-header-tone.js +4 -4
- package/dist/components/feedback/sheet.d.ts +1 -1
- package/dist/components/feedback/sheet.js +6 -9
- package/dist/components/feedback/sonner.js +16 -3
- package/dist/components/general/button.js +22 -5
- package/dist/components/layout/affix.js +15 -1
- package/dist/components/layout/sidebar.js +7 -1
- package/dist/components/navigation/anchor.d.ts +1 -1
- package/dist/components/navigation/anchor.js +5 -4
- package/dist/components/navigation/app-setting-picker.js +1 -1
- package/dist/components/navigation/pagination.js +1 -1
- package/dist/components/navigation/tabs.js +15 -2
- package/dist/components/query/infinite-query-state.d.ts +22 -6
- package/dist/contracts/measurement.json +1 -1
- package/dist/i18n/messages/en.json +0 -697
- package/dist/i18n/messages/ja.json +0 -691
- package/dist/i18n/messages/vi.json +0 -691
- package/dist/lib/control-styles.d.ts +31 -11
- package/dist/lib/control-styles.js +6 -6
- package/dist/lib/overlay-portal.d.ts +20 -0
- package/dist/lib/overlay-portal.js +93 -0
- package/dist/props/components/app.prop.d.ts +12 -0
- package/dist/props/components/charts.prop.d.ts +30 -0
- package/dist/props/components/index.d.ts +1 -1
- package/dist/props/components/navigation.prop.d.ts +21 -2
- package/dist/props/components/query.prop.d.ts +36 -2
- package/dist/props/registry.d.ts +46 -1
- package/dist/props/registry.js +38 -3
- package/dist/styles/alert-layout.css +34 -14
- package/dist/styles/badge-layout.css +10 -6
- package/dist/styles/base.css +14 -5
- package/dist/styles/card-layout.css +19 -8
- package/dist/styles/chart-layout.css +22 -3
- package/dist/styles/control.css +165 -59
- package/dist/styles/data-display-layout.css +129 -36
- package/dist/styles/data-entry-layout.css +23 -87
- package/dist/styles/dialog-layout.css +49 -19
- package/dist/styles/float-button-layout.css +5 -5
- package/dist/styles/focus-ring.css +9 -5
- package/dist/styles/layout.css +42 -15
- package/dist/styles/logo-layout.css +1 -1
- package/dist/styles/motion.css +1 -1
- package/dist/styles/navigation-layout.css +90 -29
- package/dist/styles/shell-layout.css +63 -40
- package/dist/styles/table-layout.css +56 -17
- package/dist/styles/text-layout.css +13 -4
- package/dist/styles/toggle.css +8 -2
- package/dist/tokens/components/actions.css +1 -1
- package/dist/tokens/components/attachments.css +4 -4
- package/dist/tokens/components/badge.css +4 -4
- package/dist/tokens/components/callout.css +1 -1
- package/dist/tokens/components/card.css +9 -4
- package/dist/tokens/components/chart.css +10 -1
- package/dist/tokens/components/chat-bubble.css +1 -1
- package/dist/tokens/components/control.css +28 -10
- package/dist/tokens/components/conversations.css +2 -1
- package/dist/tokens/components/data-display.css +12 -7
- package/dist/tokens/components/descriptions.css +1 -1
- package/dist/tokens/components/draggable-panel.css +1 -1
- package/dist/tokens/components/feedback.css +28 -8
- package/dist/tokens/components/float-button.css +1 -1
- package/dist/tokens/components/legal-document.css +1 -1
- package/dist/tokens/components/logo.css +1 -1
- package/dist/tokens/components/mega-menu.css +5 -3
- package/dist/tokens/components/navigation.css +21 -7
- package/dist/tokens/components/segmented.css +8 -3
- package/dist/tokens/components/shell.css +19 -5
- package/dist/tokens/components/table.css +9 -1
- package/dist/tokens/components/thought-chain.css +1 -1
- package/dist/tokens/components/toggle.css +2 -0
- package/dist/tokens/components/tree.css +3 -1
- package/dist/tokens/components/upload.css +6 -6
- package/dist/tokens/components/welcome.css +1 -1
- package/dist/tokens/foundation.css +28 -1
- package/docs/COMPOSITION-VS-COMPONENT.md +31 -0
- package/docs/CUSTOMER-THEMING.md +637 -1
- package/docs/DESIGN-AUTHORITY.md +13 -0
- package/docs/FRAME-COVERAGE-REPORT.md +3 -2
- package/docs/GLASSMORPHISM-STANDARD.md +196 -0
- package/docs/THEME-API-COVERAGE.md +538 -0
- package/docs/TOKEN-RESOLUTION.md +195 -0
- package/docs/TOKENS.md +63 -24
- package/docs/asset-modules.d.ts +7 -0
- package/docs/data-display/charts.tsx +80 -0
- package/docs/data-display/data-table/index.tsx +30 -0
- package/docs/data-display/popover.tsx +1 -1
- package/docs/data-display/table.tsx +52 -0
- package/docs/feedback/sheet.tsx +10 -10
- package/docs/foundation/density.tsx +4 -4
- package/docs/i18n/messages/en.json +1201 -0
- package/docs/i18n/messages/ja.json +1195 -0
- package/docs/i18n/messages/vi.json +1195 -0
- package/docs/layout/account-chip.tsx +2 -2
- package/docs/layout/responsive-grid.tsx +1 -1
- package/docs/navigation/toolbar.tsx +20 -12
- package/docs/providers/theme-scope.tsx +186 -0
- package/docs/showcase/caimono-price-comparison.tsx +911 -0
- package/docs/showcase/case4-login.tsx +2 -2
- package/docs/showcase/marketing-page.tsx +3 -2
- package/docs/showcase/permission-matrix.tsx +13 -5
- package/docs/showcase/table-pagination.tsx +2 -1
- package/docs/showcase/tenant-brand-color.tsx +338 -0
- package/docs/showcase/theme-customization.tsx +2 -1
- package/docs/showcase/theme-lab.tsx +2125 -0
- package/docs/themes/flat.css +462 -0
- package/docs/themes/glassmorphism.css +958 -0
- package/docs/themes/index.ts +200 -0
- package/package.json +4 -3
- package/scripts/explain-token.mjs +382 -0
|
@@ -0,0 +1,2125 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Showcase · theme-lab — CAN THIS COMPONENT SET WEAR A COMPLEX THEME? One page, every component
|
|
3
|
+
* that can be rendered, several themes, several seeds, switchable, measured.
|
|
4
|
+
*
|
|
5
|
+
* It replaces `/showcase/glassmorphism`, which gh#882 diagnosed correctly: that page proved ONE
|
|
6
|
+
* theme against a DOZEN components, and the question is neither "does glassmorphism work" nor
|
|
7
|
+
* "does Card survive it". A single theme cannot answer the question, and twelve components cannot
|
|
8
|
+
* either — three defects that week were found only because something happened to be on the page
|
|
9
|
+
* (an opaque Tabs strip inside a glass Card, a table header that kept `--muted`, a Sidebar over
|
|
10
|
+
* the wrong canvas). **Coverage is the instrument.**
|
|
11
|
+
*
|
|
12
|
+
* ── THE RULES THIS PAGE IS MEASURED UNDER ────────────────────────────────────────────────────
|
|
13
|
+
* 1. `docs/themes/*.css` may contain NOTHING but `--custom-property` declarations. No
|
|
14
|
+
* `backdrop-filter:`, no `background:`, no selector into a `.ui-*` class.
|
|
15
|
+
* 2. This page carries no `className` and no inline `style` for theming. The ONE inline style is
|
|
16
|
+
* `tenantTheme(hex).vars` on the scope — the library's own published channel for a seed, and
|
|
17
|
+
* the thing gh#882 asks to be used "rather than hand-picking".
|
|
18
|
+
* 3. `ThemeScope`, never a bare wrapper. `src/styles/base.css` sets `color: hsl(var(--foreground))`
|
|
19
|
+
* on `body`, above every scope a page can make, so that `var()` substitutes against the ROOT
|
|
20
|
+
* once and every descendant inherits the RESOLVED colour. A plain `<div>` therefore retints
|
|
21
|
+
* every surface and no text — measured at 1.34:1 before gh#881. `ThemeScope` re-states `color`
|
|
22
|
+
* on itself AND on the body-level overlay host it creates, which is also what carries the
|
|
23
|
+
* theme to the portalled panels: Dialog, Sheet, Select, DropdownMenu, Popover, Tooltip and
|
|
24
|
+
* Toast all portal out of this subtree, and a custom property only inherits through the DOM.
|
|
25
|
+
* The previous page re-declared `data-theme-style` on each `*Content` by hand; that is no
|
|
26
|
+
* longer needed and its absence here is part of what this page checks.
|
|
27
|
+
* 4. Where something cannot be expressed, the RESULT is written down — on the page, in the
|
|
28
|
+
* "could not be rendered" table at the bottom and the "could not be reached" notes in each
|
|
29
|
+
* theme file — never worked around.
|
|
30
|
+
*
|
|
31
|
+
* ── ADDRESSING, FOR THE INSTRUMENT ───────────────────────────────────────────────────────────
|
|
32
|
+
* `?theme=<id>&seed=<id>` drives both switches, so `scripts/measure-glass.mjs` navigates straight
|
|
33
|
+
* to a cell of the matrix instead of clicking through a Segmented whose labels are localised (the
|
|
34
|
+
* preview's default locale is `vi`). Overlay triggers carry `data-probe="…"`, because the
|
|
35
|
+
* instrument has had four of its own selectors wrong — `[data-slot="table"]` when the blur is on
|
|
36
|
+
* `.ui-data-table-surface`, `[role="listbox"]` instead of `.ui-select-content`, `borderTopWidth`
|
|
37
|
+
* alone, and a backdrop sampled inside the Sidebar — every one of them making the library look
|
|
38
|
+
* worse than it was. A `data-*` hook is not styling and not a theme; it is an address.
|
|
39
|
+
*
|
|
40
|
+
* ── WHAT IS NOT HERE, AND WHY ────────────────────────────────────────────────────────────────
|
|
41
|
+
* See `ABSENT` at the bottom of this file: it is rendered on the page as a real table, because an
|
|
42
|
+
* absence nobody can see is an absence nobody accounts for. Every row is about RENDERING — a
|
|
43
|
+
* provider, a hook, a shell that owns the document — never about whether a token exists.
|
|
44
|
+
*
|
|
45
|
+
* ── AND THE ONE THING THIS PAGE DELIBERATELY DOES NOT SAY ────────────────────────────────────
|
|
46
|
+
* **No sentence here asserts that a surface is unreachable by a token.** The page it replaces
|
|
47
|
+
* carried a per-component gap table, and that table rotted twice — most recently inside the Sheet
|
|
48
|
+
* itself, whose body text read "sheet.tsx bakes `bg-background` into its className, there is no
|
|
49
|
+
* `--sheet-background` to set" while the panel it was printed on rendered `rgba(29, 33, 53, 0.92)`
|
|
50
|
+
* through the `--sheet-surface-background` gh#880 had added hours earlier. A reachability claim is
|
|
51
|
+
* true on the day it is written, false the day someone adds a knob, and worst of all in a page
|
|
52
|
+
* whose entire claim is to be a measurement.
|
|
53
|
+
*
|
|
54
|
+
* A derived table was considered and rejected, and the reason is small but decisive: the only
|
|
55
|
+
* honest source is `agent/tokens.json`, `docs/**` may not import it (`check:example-imports`
|
|
56
|
+
* allows relative `.ts` data modules and committed assets, not a JSON reach outside the tree), and
|
|
57
|
+
* a generated snapshot committed beside the page is the same rot with an extra step. So the prose
|
|
58
|
+
* here is limited to what the theme files DEMONSTRATE by setting a token, and the authority on
|
|
59
|
+
* reachability is `scripts/measure-glass.mjs`, which reads the computed value of every surface and
|
|
60
|
+
* every opened overlay across the whole theme × seed matrix. Run it; do not trust a paragraph.
|
|
61
|
+
*/
|
|
62
|
+
import * as React from "react";
|
|
63
|
+
import {
|
|
64
|
+
Activity as ActivityIcon,
|
|
65
|
+
BarChart3,
|
|
66
|
+
Bell,
|
|
67
|
+
Bot,
|
|
68
|
+
Building2,
|
|
69
|
+
Check,
|
|
70
|
+
ChevronDown,
|
|
71
|
+
Clock,
|
|
72
|
+
CreditCard,
|
|
73
|
+
FileText,
|
|
74
|
+
LayoutGrid,
|
|
75
|
+
LogOut,
|
|
76
|
+
Palette,
|
|
77
|
+
RefreshCw,
|
|
78
|
+
Search,
|
|
79
|
+
Settings,
|
|
80
|
+
Share2,
|
|
81
|
+
Shield,
|
|
82
|
+
Smartphone,
|
|
83
|
+
Users,
|
|
84
|
+
} from "lucide-react";
|
|
85
|
+
|
|
86
|
+
import { ThemeScope, tenantTheme, useOptionalAppContext } from "@godxjp/ui/app";
|
|
87
|
+
import { AreaChart, BarChart, LineChart, PieChart } from "@godxjp/ui/charts";
|
|
88
|
+
import { CompactBarTrend } from "@godxjp/ui/charts/compact-bar-trend";
|
|
89
|
+
import {
|
|
90
|
+
Accordion,
|
|
91
|
+
AccordionContent,
|
|
92
|
+
AccordionItem,
|
|
93
|
+
AccordionTrigger,
|
|
94
|
+
Avatar,
|
|
95
|
+
AvatarFallback,
|
|
96
|
+
Badge,
|
|
97
|
+
Card,
|
|
98
|
+
CardAction,
|
|
99
|
+
CardBar,
|
|
100
|
+
CardContent,
|
|
101
|
+
CardDescription,
|
|
102
|
+
CardFooter,
|
|
103
|
+
CardHeader,
|
|
104
|
+
CardTitle,
|
|
105
|
+
Carousel,
|
|
106
|
+
CarouselContent,
|
|
107
|
+
CarouselDots,
|
|
108
|
+
CarouselItem,
|
|
109
|
+
CarouselNext,
|
|
110
|
+
CarouselPrevious,
|
|
111
|
+
ChatBubble,
|
|
112
|
+
ChatBubbleList,
|
|
113
|
+
CodeBlock,
|
|
114
|
+
Collapsible,
|
|
115
|
+
CollapsibleContent,
|
|
116
|
+
CollapsibleTrigger,
|
|
117
|
+
CredentialReveal,
|
|
118
|
+
DataTable,
|
|
119
|
+
Descriptions,
|
|
120
|
+
EmptyState,
|
|
121
|
+
FeatureList,
|
|
122
|
+
HoverCard,
|
|
123
|
+
HoverCardContent,
|
|
124
|
+
HoverCardTrigger,
|
|
125
|
+
Legend,
|
|
126
|
+
ListRow,
|
|
127
|
+
Marquee,
|
|
128
|
+
PermissionMatrix,
|
|
129
|
+
Popover,
|
|
130
|
+
PopoverContent,
|
|
131
|
+
PopoverDescription,
|
|
132
|
+
PopoverHeader,
|
|
133
|
+
PopoverTitle,
|
|
134
|
+
PopoverTrigger,
|
|
135
|
+
Progress,
|
|
136
|
+
Prose,
|
|
137
|
+
QrCode,
|
|
138
|
+
RangeTimeline,
|
|
139
|
+
ScrollArea,
|
|
140
|
+
ServiceCatalogCta,
|
|
141
|
+
ServiceLauncherCard,
|
|
142
|
+
StatCard,
|
|
143
|
+
StatusBadge,
|
|
144
|
+
Swatch,
|
|
145
|
+
Table,
|
|
146
|
+
TableBody,
|
|
147
|
+
TableCell,
|
|
148
|
+
TableHead,
|
|
149
|
+
TableHeader,
|
|
150
|
+
TableRow,
|
|
151
|
+
ThoughtChain,
|
|
152
|
+
Thumbnail,
|
|
153
|
+
Timeline,
|
|
154
|
+
TimelineGrid,
|
|
155
|
+
Tree,
|
|
156
|
+
Welcome,
|
|
157
|
+
type ChatMessageProp,
|
|
158
|
+
type ColumnDef,
|
|
159
|
+
} from "@godxjp/ui/data-display";
|
|
160
|
+
import {
|
|
161
|
+
Attachments,
|
|
162
|
+
Calendar,
|
|
163
|
+
Cascader,
|
|
164
|
+
ChatComposer,
|
|
165
|
+
ChatSuggestion,
|
|
166
|
+
Checkbox,
|
|
167
|
+
CheckboxGroup,
|
|
168
|
+
ColorPicker,
|
|
169
|
+
Command,
|
|
170
|
+
CommandEmpty,
|
|
171
|
+
CommandGroup,
|
|
172
|
+
CommandInput,
|
|
173
|
+
CommandItem,
|
|
174
|
+
CommandList,
|
|
175
|
+
CommandPalette,
|
|
176
|
+
DatePicker,
|
|
177
|
+
BranchScopePicker,
|
|
178
|
+
Field,
|
|
179
|
+
Form,
|
|
180
|
+
FormErrors,
|
|
181
|
+
FormField,
|
|
182
|
+
Input,
|
|
183
|
+
InputOTP,
|
|
184
|
+
InputOTPGroup,
|
|
185
|
+
InputOTPSlot,
|
|
186
|
+
Label,
|
|
187
|
+
NumberInput,
|
|
188
|
+
PasswordInput,
|
|
189
|
+
PasswordStrength,
|
|
190
|
+
Radio,
|
|
191
|
+
RadioGroup,
|
|
192
|
+
Rating,
|
|
193
|
+
SearchInput,
|
|
194
|
+
Segmented,
|
|
195
|
+
Select,
|
|
196
|
+
Slider,
|
|
197
|
+
Switch,
|
|
198
|
+
TagInput,
|
|
199
|
+
Textarea,
|
|
200
|
+
TimePicker,
|
|
201
|
+
TimeRangePicker,
|
|
202
|
+
Toggle,
|
|
203
|
+
ToggleGroup,
|
|
204
|
+
ToggleGroupItem,
|
|
205
|
+
Transfer,
|
|
206
|
+
TreeSelect,
|
|
207
|
+
Upload,
|
|
208
|
+
type UploadFileItem,
|
|
209
|
+
} from "@godxjp/ui/data-entry";
|
|
210
|
+
import {
|
|
211
|
+
Alert,
|
|
212
|
+
AlertActions,
|
|
213
|
+
AlertDescription,
|
|
214
|
+
AlertDialog,
|
|
215
|
+
AlertTitle,
|
|
216
|
+
Banner,
|
|
217
|
+
Callout,
|
|
218
|
+
Dialog,
|
|
219
|
+
DialogBody,
|
|
220
|
+
DialogClose,
|
|
221
|
+
DialogContent,
|
|
222
|
+
DialogDescription,
|
|
223
|
+
DialogFooter,
|
|
224
|
+
DialogHeader,
|
|
225
|
+
DialogTitle,
|
|
226
|
+
Sheet,
|
|
227
|
+
SheetBody,
|
|
228
|
+
SheetClose,
|
|
229
|
+
SheetContent,
|
|
230
|
+
SheetDescription,
|
|
231
|
+
SheetFooter,
|
|
232
|
+
SheetHeader,
|
|
233
|
+
SheetTitle,
|
|
234
|
+
Skeleton,
|
|
235
|
+
SkeletonArticle,
|
|
236
|
+
SkeletonAvatar,
|
|
237
|
+
SkeletonButton,
|
|
238
|
+
SkeletonForm,
|
|
239
|
+
SkeletonImage,
|
|
240
|
+
SkeletonInput,
|
|
241
|
+
SkeletonNode,
|
|
242
|
+
SkeletonRows,
|
|
243
|
+
SkeletonTable,
|
|
244
|
+
Toaster,
|
|
245
|
+
Tooltip,
|
|
246
|
+
TooltipContent,
|
|
247
|
+
TooltipTrigger,
|
|
248
|
+
TwoFactorSetup,
|
|
249
|
+
toast,
|
|
250
|
+
} from "@godxjp/ui/feedback";
|
|
251
|
+
import {
|
|
252
|
+
Actions,
|
|
253
|
+
ActionsCopy,
|
|
254
|
+
ActionsFeedback,
|
|
255
|
+
Activity,
|
|
256
|
+
Button,
|
|
257
|
+
FloatButton,
|
|
258
|
+
Heading,
|
|
259
|
+
Icon,
|
|
260
|
+
Link,
|
|
261
|
+
Logo,
|
|
262
|
+
Paragraph,
|
|
263
|
+
Reveal,
|
|
264
|
+
Text,
|
|
265
|
+
Title,
|
|
266
|
+
Typography,
|
|
267
|
+
VisuallyHidden,
|
|
268
|
+
} from "@godxjp/ui/general";
|
|
269
|
+
import {
|
|
270
|
+
AccountChip,
|
|
271
|
+
Affix,
|
|
272
|
+
AppLauncher,
|
|
273
|
+
AppShell,
|
|
274
|
+
AspectRatio,
|
|
275
|
+
AuthAccountSummary,
|
|
276
|
+
AuthDivider,
|
|
277
|
+
AuthFooter,
|
|
278
|
+
AuthIdentity,
|
|
279
|
+
AuthStack,
|
|
280
|
+
Breadcrumb,
|
|
281
|
+
DraggablePanel,
|
|
282
|
+
ErrorSurface,
|
|
283
|
+
Flex,
|
|
284
|
+
Masonry,
|
|
285
|
+
MasterDetail,
|
|
286
|
+
NavList,
|
|
287
|
+
OrgSwitcher,
|
|
288
|
+
PageContainer,
|
|
289
|
+
ResizableHandle,
|
|
290
|
+
ResizablePanel,
|
|
291
|
+
ResizablePanelGroup,
|
|
292
|
+
ResponsiveGrid,
|
|
293
|
+
Separator,
|
|
294
|
+
ServiceRolePanel,
|
|
295
|
+
Sidebar,
|
|
296
|
+
SplitPane,
|
|
297
|
+
Topbar,
|
|
298
|
+
TopbarItem,
|
|
299
|
+
} from "@godxjp/ui/layout";
|
|
300
|
+
import {
|
|
301
|
+
Anchor,
|
|
302
|
+
AppSettingPicker,
|
|
303
|
+
AppSettingToggle,
|
|
304
|
+
Conversations,
|
|
305
|
+
DropdownMenu,
|
|
306
|
+
DropdownMenuContent,
|
|
307
|
+
DropdownMenuItem,
|
|
308
|
+
DropdownMenuLabel,
|
|
309
|
+
DropdownMenuSeparator,
|
|
310
|
+
DropdownMenuTrigger,
|
|
311
|
+
FilterBar,
|
|
312
|
+
MegaMenu,
|
|
313
|
+
Pagination,
|
|
314
|
+
Steps,
|
|
315
|
+
Tabs,
|
|
316
|
+
Toolbar,
|
|
317
|
+
ToolbarGroup,
|
|
318
|
+
} from "@godxjp/ui/navigation";
|
|
319
|
+
import { useTranslation } from "@godxjp/ui/i18n";
|
|
320
|
+
|
|
321
|
+
import shotLandscape from "../assets/shot-landscape.svg";
|
|
322
|
+
import {
|
|
323
|
+
MODES,
|
|
324
|
+
SEEDS,
|
|
325
|
+
THEMES,
|
|
326
|
+
modeFromQuery,
|
|
327
|
+
seedFromQuery,
|
|
328
|
+
themeFromQuery,
|
|
329
|
+
themeQueryValue,
|
|
330
|
+
type ModeRow,
|
|
331
|
+
type SeedRow,
|
|
332
|
+
type ThemeRow,
|
|
333
|
+
} from "../themes";
|
|
334
|
+
|
|
335
|
+
/* ── The components that CANNOT appear here, and the reason for each ─────────────────────────
|
|
336
|
+
* Rendered on the page as a table. An absence nobody can see is an absence nobody accounts for,
|
|
337
|
+
* and gh#882 asks for the reason in the page itself, not in a commit message. */
|
|
338
|
+
const ABSENT: ReadonlyArray<{ name: string; reasonKey: string }> = [
|
|
339
|
+
{ name: "AppProvider", reasonKey: "themeLab.absent.provider" },
|
|
340
|
+
{ name: "OverlayPortalProvider", reasonKey: "themeLab.absent.overlayPortal" },
|
|
341
|
+
{ name: "ThemeScope", reasonKey: "themeLab.absent.themeScope" },
|
|
342
|
+
{ name: "AuthShell", reasonKey: "themeLab.absent.ownsPage" },
|
|
343
|
+
{ name: "CenteredShell", reasonKey: "themeLab.absent.ownsPage" },
|
|
344
|
+
{ name: "MobileShell", reasonKey: "themeLab.absent.ownsPage" },
|
|
345
|
+
{ name: "LegalDocumentShell", reasonKey: "themeLab.absent.ownsPage" },
|
|
346
|
+
{ name: "useZodForm", reasonKey: "themeLab.absent.needsSchema" },
|
|
347
|
+
{ name: "FormRoot", reasonKey: "themeLab.absent.needsSchema" },
|
|
348
|
+
{ name: "FormFieldControl", reasonKey: "themeLab.absent.needsSchema" },
|
|
349
|
+
{ name: "FormFieldArray", reasonKey: "themeLab.absent.needsSchema" },
|
|
350
|
+
{ name: "AlertDialogRoot", reasonKey: "themeLab.absent.sameSurface" },
|
|
351
|
+
{ name: "useFormAdapter", reasonKey: "themeLab.absent.hook" },
|
|
352
|
+
{ name: "useCarousel", reasonKey: "themeLab.absent.hook" },
|
|
353
|
+
{ name: "usePasswordStrength", reasonKey: "themeLab.absent.hook" },
|
|
354
|
+
{ name: "useUploadDraft", reasonKey: "themeLab.absent.hook" },
|
|
355
|
+
{ name: "formatDate", reasonKey: "themeLab.absent.function" },
|
|
356
|
+
{ name: "flexRender", reasonKey: "themeLab.absent.function" },
|
|
357
|
+
{ name: "buttonVariants", reasonKey: "themeLab.absent.function" },
|
|
358
|
+
{ name: "collectUploadCommitActions", reasonKey: "themeLab.absent.function" },
|
|
359
|
+
{ name: "createSidebarLink", reasonKey: "themeLab.absent.function" },
|
|
360
|
+
{ name: "inertiaUpload", reasonKey: "themeLab.absent.inertia" },
|
|
361
|
+
{ name: "UploadCropDialog", reasonKey: "themeLab.absent.needsFile" },
|
|
362
|
+
{ name: "AvatarImage", reasonKey: "themeLab.absent.needsNetwork" },
|
|
363
|
+
{ name: "PrefetchLink", reasonKey: "themeLab.absent.needsRouter" },
|
|
364
|
+
{ name: "DataState", reasonKey: "themeLab.absent.needsQuery" },
|
|
365
|
+
{ name: "InfiniteQueryState", reasonKey: "themeLab.absent.needsQuery" },
|
|
366
|
+
{ name: "AlertQueryError", reasonKey: "themeLab.absent.needsQuery" },
|
|
367
|
+
{ name: "ButtonRefetch", reasonKey: "themeLab.absent.needsQuery" },
|
|
368
|
+
{ name: "VisuallyHidden", reasonKey: "themeLab.absent.invisible" },
|
|
369
|
+
];
|
|
370
|
+
|
|
371
|
+
const UNBREAKABLE = "Betriebsstaettengewinnabgrenzungsaufzeichnungsverordnung-2026-A";
|
|
372
|
+
|
|
373
|
+
type Row = {
|
|
374
|
+
id: string;
|
|
375
|
+
nameKey: string;
|
|
376
|
+
ownerKey: string;
|
|
377
|
+
tone: "success" | "warning" | "destructive";
|
|
378
|
+
};
|
|
379
|
+
|
|
380
|
+
const ROWS: Row[] = [
|
|
381
|
+
{ id: "r1", nameKey: "alpha", ownerKey: "one", tone: "success" },
|
|
382
|
+
{ id: "r2", nameKey: "beta", ownerKey: "two", tone: "warning" },
|
|
383
|
+
{ id: "r3", nameKey: "gamma", ownerKey: "three", tone: "destructive" },
|
|
384
|
+
];
|
|
385
|
+
|
|
386
|
+
const TREND = [
|
|
387
|
+
{ date: "2026-09-16", count: 4 },
|
|
388
|
+
{ date: "2026-09-17", count: 9 },
|
|
389
|
+
{ date: "2026-09-18", count: 6 },
|
|
390
|
+
{ date: "2026-09-19", count: 12 },
|
|
391
|
+
{ date: "2026-09-20", count: 8 },
|
|
392
|
+
{ date: "2026-09-21", count: 15 },
|
|
393
|
+
{ date: "2026-09-22", count: 11 },
|
|
394
|
+
];
|
|
395
|
+
|
|
396
|
+
const SERIES = [
|
|
397
|
+
{ month: "2026-04", plan: 120, actual: 98 },
|
|
398
|
+
{ month: "2026-05", plan: 130, actual: 141 },
|
|
399
|
+
{ month: "2026-06", plan: 128, actual: 119 },
|
|
400
|
+
{ month: "2026-07", plan: 150, actual: 162 },
|
|
401
|
+
];
|
|
402
|
+
|
|
403
|
+
/** A titled block. Local composition, not a component: it owns no behaviour a Card does not. */
|
|
404
|
+
function Section({
|
|
405
|
+
id,
|
|
406
|
+
title,
|
|
407
|
+
note,
|
|
408
|
+
children,
|
|
409
|
+
}: {
|
|
410
|
+
id: string;
|
|
411
|
+
title: string;
|
|
412
|
+
note?: string;
|
|
413
|
+
children: React.ReactNode;
|
|
414
|
+
}) {
|
|
415
|
+
return (
|
|
416
|
+
<Card id={id}>
|
|
417
|
+
<CardHeader>
|
|
418
|
+
<CardTitle level={2}>{title}</CardTitle>
|
|
419
|
+
{note ? <CardDescription>{note}</CardDescription> : null}
|
|
420
|
+
</CardHeader>
|
|
421
|
+
<CardContent>
|
|
422
|
+
<Flex direction="col" gap="lg">
|
|
423
|
+
{children}
|
|
424
|
+
</Flex>
|
|
425
|
+
</CardContent>
|
|
426
|
+
</Card>
|
|
427
|
+
);
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
/** A labelled row of specimens. */
|
|
431
|
+
function Row2({ label, children }: { label: string; children: React.ReactNode }) {
|
|
432
|
+
return (
|
|
433
|
+
<Flex direction="col" gap="xs">
|
|
434
|
+
<Text size="xs" tone="muted">
|
|
435
|
+
{label}
|
|
436
|
+
</Text>
|
|
437
|
+
<Flex direction="row" gap="sm" wrap align="center">
|
|
438
|
+
{children}
|
|
439
|
+
</Flex>
|
|
440
|
+
</Flex>
|
|
441
|
+
);
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
export default function ThemeLabShowcase() {
|
|
445
|
+
const { t } = useTranslation();
|
|
446
|
+
|
|
447
|
+
const initial = React.useMemo(() => {
|
|
448
|
+
const params = new URLSearchParams(typeof window === "undefined" ? "" : window.location.search);
|
|
449
|
+
return {
|
|
450
|
+
theme: themeFromQuery(params.get("theme")),
|
|
451
|
+
seed: seedFromQuery(params.get("seed")),
|
|
452
|
+
mode: modeFromQuery(params.get("mode")),
|
|
453
|
+
};
|
|
454
|
+
}, []);
|
|
455
|
+
|
|
456
|
+
// The bounded scroller Affix and Anchor pin against — see the note at their call site.
|
|
457
|
+
const affixScrollerRef = React.useRef<HTMLDivElement>(null);
|
|
458
|
+
const [theme, setTheme] = React.useState<ThemeRow>(initial.theme);
|
|
459
|
+
const [seed, setSeed] = React.useState<SeedRow>(initial.seed);
|
|
460
|
+
const [mode, setMode] = React.useState<ModeRow>(initial.mode);
|
|
461
|
+
|
|
462
|
+
/*
|
|
463
|
+
* The URL is the page's own state, so a measurement or a screenshot can address one cell of the
|
|
464
|
+
* matrix directly. `replaceState` rather than `pushState`: flipping a switch is not navigation,
|
|
465
|
+
* and a back button that walks a colour history is the kind of cleverness nobody asked for.
|
|
466
|
+
*/
|
|
467
|
+
React.useEffect(() => {
|
|
468
|
+
if (typeof window === "undefined") return;
|
|
469
|
+
const params = new URLSearchParams(window.location.search);
|
|
470
|
+
params.set("theme", themeQueryValue(theme));
|
|
471
|
+
params.set("seed", seed.id);
|
|
472
|
+
params.set("mode", mode.id);
|
|
473
|
+
window.history.replaceState(null, "", `${window.location.pathname}?${params.toString()}`);
|
|
474
|
+
}, [theme, seed, mode]);
|
|
475
|
+
|
|
476
|
+
/*
|
|
477
|
+
* Polarity, applied THE WAY THE LIBRARY ITSELF APPLIES IT: through `AppProvider`'s own `theme`
|
|
478
|
+
* axis (`src/app/theme-axes.ts`'s `applyThemeAxes`, which is what `AppProvider` calls on
|
|
479
|
+
* `<html data-theme>`), never by writing `document.documentElement` from this page. Going
|
|
480
|
+
* through `ctx.setTheme` — the exact setter `AppSettingToggle kind="theme"` already uses above —
|
|
481
|
+
* sets `AppProvider`'s OWN state, so ITS OWN effect is what reaches the DOM; this page never
|
|
482
|
+
* touches `document.documentElement` for polarity.
|
|
483
|
+
*
|
|
484
|
+
* `mounted`, not a plain effect on `[mode.id]` — MEASURED, not assumed. `AppProvider` re-derives
|
|
485
|
+
* `theme` from `persist`/storage on ITS OWN mount effect (`app-provider.tsx`'s "hydrate stored
|
|
486
|
+
* preferences" effect), unconditionally, from `initialTheme` when nothing is stored — and the
|
|
487
|
+
* preview harness resolves `?theme=` (base/glass/flat here) as a SEPARATE polarity switch, so
|
|
488
|
+
* that initial value is always "light" on this page. Calling `ctx.setTheme("dark")` from a plain
|
|
489
|
+
* mount effect here was overwritten a moment later: React commits a descendant's passive effects
|
|
490
|
+
* before its ancestor's, so `AppProvider`'s own hydration effect always runs AFTER this page's —
|
|
491
|
+
* confirmed with a `MutationObserver` on `data-theme`, which recorded ZERO mutations and settled
|
|
492
|
+
* on "light" for `?mode=dark`. Deferring our own call by one render pass (`setMounted(true)` in
|
|
493
|
+
* an effect, then applying `mode` only once `mounted`) pushes it into a SECOND commit, which
|
|
494
|
+
* starts only after the FIRST commit's entire effect list — `AppProvider`'s hydration effect
|
|
495
|
+
* included — has already run and settled; reconfirmed with the same observer, now showing
|
|
496
|
+
* `data-theme` reaching "dark" reliably.
|
|
497
|
+
*
|
|
498
|
+
* `ctx` is read via a ref kept fresh every render, and the apply effect depends only on
|
|
499
|
+
* `[mounted, mode.id]`, so it does not re-fire every time some OTHER axis (locale, density, …)
|
|
500
|
+
* changes the context's identity — only once mounted, and again when this switch changes. The
|
|
501
|
+
* cleanup restores whatever the axis was immediately before this effect's own change, so leaving
|
|
502
|
+
* the page (or flipping the switch back) does not leak a forced polarity into the rest of the
|
|
503
|
+
* showcase.
|
|
504
|
+
*/
|
|
505
|
+
const appContext = useOptionalAppContext();
|
|
506
|
+
const appContextRef = React.useRef(appContext);
|
|
507
|
+
appContextRef.current = appContext;
|
|
508
|
+
const [mounted, setMounted] = React.useState(false);
|
|
509
|
+
React.useEffect(() => setMounted(true), []);
|
|
510
|
+
React.useEffect(() => {
|
|
511
|
+
if (!mounted) return;
|
|
512
|
+
const ctx = appContextRef.current;
|
|
513
|
+
if (!ctx) return;
|
|
514
|
+
const previous = ctx.theme;
|
|
515
|
+
ctx.setTheme(mode.id);
|
|
516
|
+
return () => {
|
|
517
|
+
ctx.setTheme(previous);
|
|
518
|
+
};
|
|
519
|
+
}, [mounted, mode.id]);
|
|
520
|
+
|
|
521
|
+
/* The seed is walked against THIS THEME's surface for THIS POLARITY, not the package's (see
|
|
522
|
+
* `inkSurface`). A theme declares its own `--text-link`, but `tenantTheme` returns the brand
|
|
523
|
+
* inks as literals in `style`, and an inline literal outranks any selector — so the theme's
|
|
524
|
+
* declaration is only reachable if the value handed to it was computed on the right ground. */
|
|
525
|
+
const brand = React.useMemo(
|
|
526
|
+
() => tenantTheme(seed.hex, { surface: theme.inkSurface[mode.id] }),
|
|
527
|
+
[seed.hex, theme.inkSurface, mode.id],
|
|
528
|
+
);
|
|
529
|
+
|
|
530
|
+
const [dialogOpen, setDialogOpen] = React.useState(false);
|
|
531
|
+
const [sheetOpen, setSheetOpen] = React.useState(false);
|
|
532
|
+
const [alertOpen, setAlertOpen] = React.useState(false);
|
|
533
|
+
const [twoFactorOpen, setTwoFactorOpen] = React.useState(false);
|
|
534
|
+
const [role, setRole] = React.useState("viewer");
|
|
535
|
+
const [checked, setChecked] = React.useState(true);
|
|
536
|
+
const [switched, setSwitched] = React.useState(true);
|
|
537
|
+
const [volume, setVolume] = React.useState(64);
|
|
538
|
+
const [colour, setColour] = React.useState("#2563EB");
|
|
539
|
+
const [tags, setTags] = React.useState<string[]>(["a11y", "rtl"]);
|
|
540
|
+
const [password, setPassword] = React.useState("Glass-2026!");
|
|
541
|
+
const [query, setQuery] = React.useState("");
|
|
542
|
+
const [page, setPage] = React.useState(1);
|
|
543
|
+
const [step] = React.useState(1);
|
|
544
|
+
const [date, setDate] = React.useState<Date | undefined>(new Date(2026, 8, 23));
|
|
545
|
+
const [time, setTime] = React.useState("09:30");
|
|
546
|
+
const [uploads, setUploads] = React.useState<UploadFileItem[]>([]);
|
|
547
|
+
const [draft, setDraft] = React.useState("");
|
|
548
|
+
const [otp, setOtp] = React.useState("");
|
|
549
|
+
const [conversation, setConversation] = React.useState("c1");
|
|
550
|
+
const [route, setRoute] = React.useState("products");
|
|
551
|
+
const [org, setOrg] = React.useState("dxs");
|
|
552
|
+
const [panelPosition, setPanelPosition] = React.useState({ x: 0, y: 0 });
|
|
553
|
+
const [treeChecked, setTreeChecked] = React.useState<string[]>(["billing.read"]);
|
|
554
|
+
const [transferred, setTransferred] = React.useState<string[]>(["1010"]);
|
|
555
|
+
const [cascade, setCascade] = React.useState<string[]>([]);
|
|
556
|
+
const [account, setAccount] = React.useState<string | undefined>(undefined);
|
|
557
|
+
const [selectedRole, setSelectedRole] = React.useState("admin");
|
|
558
|
+
const [scope, setScope] = React.useState<{
|
|
559
|
+
mode: "all" | "selected";
|
|
560
|
+
branchIds?: readonly string[];
|
|
561
|
+
}>({ mode: "selected", branchIds: ["b1"] });
|
|
562
|
+
|
|
563
|
+
const toneLabel = React.useCallback((tone: Row["tone"]) => t(`themeLab.tone.${tone}`), [t]);
|
|
564
|
+
|
|
565
|
+
const columns: ColumnDef<Row>[] = [
|
|
566
|
+
{
|
|
567
|
+
key: "nameKey",
|
|
568
|
+
header: t("themeLab.table.name"),
|
|
569
|
+
render: (row) => <Text as="span">{t(`themeLab.sample.${row.nameKey}`)}</Text>,
|
|
570
|
+
},
|
|
571
|
+
{
|
|
572
|
+
key: "ownerKey",
|
|
573
|
+
header: t("themeLab.table.owner"),
|
|
574
|
+
hiddenOnMobile: true,
|
|
575
|
+
render: (row) => <Text as="span">{t(`themeLab.sample.${row.ownerKey}`)}</Text>,
|
|
576
|
+
},
|
|
577
|
+
{
|
|
578
|
+
key: "tone",
|
|
579
|
+
header: t("themeLab.table.status"),
|
|
580
|
+
align: "right",
|
|
581
|
+
render: (row) => <Badge tone={row.tone}>{toneLabel(row.tone)}</Badge>,
|
|
582
|
+
},
|
|
583
|
+
];
|
|
584
|
+
|
|
585
|
+
const chat: ChatMessageProp[] = [
|
|
586
|
+
{ id: "m1", role: "assistant", content: t("themeLab.chat.assistant") },
|
|
587
|
+
{ id: "m2", role: "user", content: t("themeLab.chat.user") },
|
|
588
|
+
];
|
|
589
|
+
|
|
590
|
+
const sidebar = (
|
|
591
|
+
<Sidebar
|
|
592
|
+
activeId="overview"
|
|
593
|
+
onSelect={() => {}}
|
|
594
|
+
sections={[
|
|
595
|
+
{
|
|
596
|
+
label: t("themeLab.nav.section"),
|
|
597
|
+
items: [
|
|
598
|
+
{ id: "overview", label: t("themeLab.nav.overview"), icon: LayoutGrid },
|
|
599
|
+
{ id: "reports", label: t("themeLab.nav.reports"), icon: BarChart3 },
|
|
600
|
+
{ id: "people", label: t("themeLab.nav.people"), icon: Users },
|
|
601
|
+
{ id: "settings", label: t("themeLab.nav.settings"), icon: Settings },
|
|
602
|
+
],
|
|
603
|
+
},
|
|
604
|
+
]}
|
|
605
|
+
product={{ name: t("themeLab.product"), role: t("themeLab.nav.role") }}
|
|
606
|
+
footer={
|
|
607
|
+
<Text size="xs" tone="muted">
|
|
608
|
+
{t("themeLab.nav.footer")}
|
|
609
|
+
</Text>
|
|
610
|
+
}
|
|
611
|
+
/>
|
|
612
|
+
);
|
|
613
|
+
|
|
614
|
+
const topbar = (
|
|
615
|
+
<Topbar
|
|
616
|
+
start={
|
|
617
|
+
<>
|
|
618
|
+
{/* `Logo`, not `TopbarItem asChild` + `Text`: `Text` does not forward a ref to a DOM
|
|
619
|
+
node, and the bar's `Focusable` wrapper warned about exactly that at runtime. The
|
|
620
|
+
brand cell is a lockup, not a control, so `Logo` is also the right component. */}
|
|
621
|
+
<Logo glyph="g" wordmark={t("themeLab.product")} />
|
|
622
|
+
<AppLauncher
|
|
623
|
+
apps={[
|
|
624
|
+
{
|
|
625
|
+
id: "console",
|
|
626
|
+
name: t("themeLab.app.console"),
|
|
627
|
+
href: "#console",
|
|
628
|
+
icon: <BarChart3 aria-hidden="true" />,
|
|
629
|
+
current: true,
|
|
630
|
+
},
|
|
631
|
+
{
|
|
632
|
+
id: "billing",
|
|
633
|
+
name: t("themeLab.app.billing"),
|
|
634
|
+
href: "#billing",
|
|
635
|
+
icon: <CreditCard aria-hidden="true" />,
|
|
636
|
+
},
|
|
637
|
+
]}
|
|
638
|
+
labels={{
|
|
639
|
+
trigger: t("themeLab.launcher.trigger"),
|
|
640
|
+
title: t("themeLab.launcher.title"),
|
|
641
|
+
empty: t("themeLab.label.noResults"),
|
|
642
|
+
loading: t("themeLab.label.loading"),
|
|
643
|
+
}}
|
|
644
|
+
/>
|
|
645
|
+
</>
|
|
646
|
+
}
|
|
647
|
+
end={
|
|
648
|
+
<Flex direction="row" gap="sm" align="center">
|
|
649
|
+
<AppSettingPicker kind="locale" appearance="bar" />
|
|
650
|
+
<AppSettingToggle kind="theme" />
|
|
651
|
+
<Popover>
|
|
652
|
+
<PopoverTrigger asChild>
|
|
653
|
+
<TopbarItem
|
|
654
|
+
data-probe="popover"
|
|
655
|
+
icon={<Bell aria-hidden="true" />}
|
|
656
|
+
aria-label={t("themeLab.topbar.notifications")}
|
|
657
|
+
/>
|
|
658
|
+
</PopoverTrigger>
|
|
659
|
+
<PopoverContent align="end">
|
|
660
|
+
<PopoverHeader>
|
|
661
|
+
<PopoverTitle>{t("themeLab.popover.title")}</PopoverTitle>
|
|
662
|
+
<PopoverDescription>{t("themeLab.popover.body")}</PopoverDescription>
|
|
663
|
+
</PopoverHeader>
|
|
664
|
+
<Button size="sm" variant="outline">
|
|
665
|
+
<Check aria-hidden="true" />
|
|
666
|
+
{t("themeLab.popover.action")}
|
|
667
|
+
</Button>
|
|
668
|
+
</PopoverContent>
|
|
669
|
+
</Popover>
|
|
670
|
+
<DropdownMenu>
|
|
671
|
+
<DropdownMenuTrigger asChild>
|
|
672
|
+
<TopbarItem
|
|
673
|
+
data-probe="dropdown"
|
|
674
|
+
aria-label={t("themeLab.topbar.account")}
|
|
675
|
+
icon={
|
|
676
|
+
<Avatar>
|
|
677
|
+
<AvatarFallback>DP</AvatarFallback>
|
|
678
|
+
</Avatar>
|
|
679
|
+
}
|
|
680
|
+
/>
|
|
681
|
+
</DropdownMenuTrigger>
|
|
682
|
+
<DropdownMenuContent align="end">
|
|
683
|
+
<DropdownMenuLabel>{t("themeLab.product")}</DropdownMenuLabel>
|
|
684
|
+
<DropdownMenuSeparator />
|
|
685
|
+
<DropdownMenuItem>{t("themeLab.menu.profile")}</DropdownMenuItem>
|
|
686
|
+
<DropdownMenuItem>{t("themeLab.menu.billing")}</DropdownMenuItem>
|
|
687
|
+
<DropdownMenuSeparator />
|
|
688
|
+
<DropdownMenuItem>
|
|
689
|
+
<LogOut aria-hidden="true" />
|
|
690
|
+
{t("themeLab.menu.logout")}
|
|
691
|
+
</DropdownMenuItem>
|
|
692
|
+
</DropdownMenuContent>
|
|
693
|
+
</DropdownMenu>
|
|
694
|
+
</Flex>
|
|
695
|
+
}
|
|
696
|
+
/>
|
|
697
|
+
);
|
|
698
|
+
|
|
699
|
+
return (
|
|
700
|
+
/*
|
|
701
|
+
* The attribute is ALWAYS written, "base" included. `docs/themes/` has no `base.css`, so
|
|
702
|
+
* `[data-theme-style="base"]` matches no rule and the region keeps the library's own theme —
|
|
703
|
+
* but the scope stays ADDRESSABLE, and that is not cosmetic: `measure-glass.mjs` scopes its
|
|
704
|
+
* contrast walk to `[data-theme-style]`, and with the attribute omitted the default theme
|
|
705
|
+
* measured 22 strings instead of 530 while reporting a clean sweep.
|
|
706
|
+
*/
|
|
707
|
+
<ThemeScope data-theme-style={themeQueryValue(theme)} style={brand.vars}>
|
|
708
|
+
<AppShell sidebar={sidebar} topbar={topbar}>
|
|
709
|
+
<PageContainer
|
|
710
|
+
title={t("themeLab.page.title")}
|
|
711
|
+
subtitle={t("themeLab.page.subtitle")}
|
|
712
|
+
breadcrumb={[
|
|
713
|
+
{ label: t("themeLab.nav.overview"), to: "#overview" },
|
|
714
|
+
{ label: t("themeLab.page.title") },
|
|
715
|
+
]}
|
|
716
|
+
extra={
|
|
717
|
+
<AccountChip
|
|
718
|
+
name={t("themeLab.sample.one")}
|
|
719
|
+
email="duong@example.test"
|
|
720
|
+
actionLabel={t("themeLab.menu.logout")}
|
|
721
|
+
onAction={() => {}}
|
|
722
|
+
/>
|
|
723
|
+
}
|
|
724
|
+
>
|
|
725
|
+
<Flex direction="col" gap="lg">
|
|
726
|
+
{/* ── 1 · the switches themselves ───────────────────────────────────────────── */}
|
|
727
|
+
<Card accent="primary">
|
|
728
|
+
<CardHeader>
|
|
729
|
+
<CardTitle level={2}>{t("themeLab.switch.title")}</CardTitle>
|
|
730
|
+
<CardDescription>{t("themeLab.switch.note")}</CardDescription>
|
|
731
|
+
<CardAction>
|
|
732
|
+
<Legend
|
|
733
|
+
items={[
|
|
734
|
+
{ tone: "success", label: t("themeLab.switch.legendPass") },
|
|
735
|
+
{ tone: "destructive", label: t("themeLab.switch.legendFail") },
|
|
736
|
+
]}
|
|
737
|
+
/>
|
|
738
|
+
</CardAction>
|
|
739
|
+
</CardHeader>
|
|
740
|
+
<CardContent>
|
|
741
|
+
<Flex direction="col" gap="md">
|
|
742
|
+
<Field id="theme-switch" label={t("themeLab.switch.themeLabel")}>
|
|
743
|
+
<Segmented
|
|
744
|
+
id="theme-switch"
|
|
745
|
+
aria-label={t("themeLab.switch.themeLabel")}
|
|
746
|
+
value={themeQueryValue(theme)}
|
|
747
|
+
onValueChange={(next) => setTheme(themeFromQuery(next))}
|
|
748
|
+
options={THEMES.map((row) => ({
|
|
749
|
+
value: themeQueryValue(row),
|
|
750
|
+
label: t(row.nameKey),
|
|
751
|
+
}))}
|
|
752
|
+
/>
|
|
753
|
+
</Field>
|
|
754
|
+
<Text size="sm" tone="muted">
|
|
755
|
+
{t(theme.noteKey)}
|
|
756
|
+
</Text>
|
|
757
|
+
<Field id="seed-switch" label={t("themeLab.switch.seedLabel")}>
|
|
758
|
+
<Segmented
|
|
759
|
+
id="seed-switch"
|
|
760
|
+
aria-label={t("themeLab.switch.seedLabel")}
|
|
761
|
+
value={seed.id}
|
|
762
|
+
onValueChange={(next) => setSeed(seedFromQuery(next))}
|
|
763
|
+
options={SEEDS.map((row) => ({
|
|
764
|
+
value: row.id,
|
|
765
|
+
label: t(row.nameKey),
|
|
766
|
+
}))}
|
|
767
|
+
/>
|
|
768
|
+
</Field>
|
|
769
|
+
<Field id="mode-switch" label={t("themeLab.switch.modeLabel")}>
|
|
770
|
+
<Segmented
|
|
771
|
+
id="mode-switch"
|
|
772
|
+
aria-label={t("themeLab.switch.modeLabel")}
|
|
773
|
+
value={mode.id}
|
|
774
|
+
onValueChange={(next) => setMode(modeFromQuery(next))}
|
|
775
|
+
options={MODES.map((row) => ({
|
|
776
|
+
value: row.id,
|
|
777
|
+
label: t(row.nameKey),
|
|
778
|
+
}))}
|
|
779
|
+
/>
|
|
780
|
+
</Field>
|
|
781
|
+
<Flex direction="row" gap="sm" wrap align="center">
|
|
782
|
+
{SEEDS.map((row) => (
|
|
783
|
+
<Swatch
|
|
784
|
+
key={row.id}
|
|
785
|
+
color={row.hex}
|
|
786
|
+
aria-label={`${t(row.nameKey)} ${row.hex}`}
|
|
787
|
+
/>
|
|
788
|
+
))}
|
|
789
|
+
</Flex>
|
|
790
|
+
<Descriptions columns={2}>
|
|
791
|
+
<Descriptions.Item label={t("themeLab.switch.seedHex")} mono>
|
|
792
|
+
{brand.primary ?? "—"}
|
|
793
|
+
</Descriptions.Item>
|
|
794
|
+
<Descriptions.Item label={t("themeLab.switch.seedInk")} mono>
|
|
795
|
+
{brand.foreground ?? "—"}
|
|
796
|
+
</Descriptions.Item>
|
|
797
|
+
<Descriptions.Item label={t("themeLab.switch.seedContrast")} span={2}>
|
|
798
|
+
<Badge tone={brand.meetsAA ? "success" : "destructive"}>
|
|
799
|
+
{brand.contrast.toFixed(2)}:1
|
|
800
|
+
</Badge>
|
|
801
|
+
</Descriptions.Item>
|
|
802
|
+
</Descriptions>
|
|
803
|
+
</Flex>
|
|
804
|
+
</CardContent>
|
|
805
|
+
</Card>
|
|
806
|
+
|
|
807
|
+
{/* ── 2 · type ─────────────────────────────────────────────────────────────── */}
|
|
808
|
+
<Section
|
|
809
|
+
id="type"
|
|
810
|
+
title={t("themeLab.section.type")}
|
|
811
|
+
note={t("themeLab.section.typeNote")}
|
|
812
|
+
>
|
|
813
|
+
<Flex direction="col" gap="sm">
|
|
814
|
+
<Heading level={3}>{t("themeLab.sample.headline")}</Heading>
|
|
815
|
+
<Title level={4}>{t("themeLab.sample.alpha")}</Title>
|
|
816
|
+
<Paragraph>{t("themeLab.sample.long")}</Paragraph>
|
|
817
|
+
<Text size="sm" tone="muted">
|
|
818
|
+
{t("themeLab.sample.short")}
|
|
819
|
+
</Text>
|
|
820
|
+
<Text size="xs" mono tone="muted">
|
|
821
|
+
{UNBREAKABLE}
|
|
822
|
+
</Text>
|
|
823
|
+
<Text weight="medium" tabular>
|
|
824
|
+
{new Intl.NumberFormat(undefined, { notation: "standard" }).format(1240000)}
|
|
825
|
+
</Text>
|
|
826
|
+
<Link href="#type">{t("themeLab.sample.linkLabel")}</Link>
|
|
827
|
+
<Typography>
|
|
828
|
+
<Typography.Title level={5}>{t("themeLab.sample.beta")}</Typography.Title>
|
|
829
|
+
<Typography.Paragraph>{t("themeLab.sample.short")}</Typography.Paragraph>
|
|
830
|
+
</Typography>
|
|
831
|
+
<Prose size="sm">
|
|
832
|
+
<Typography.Paragraph>{t("themeLab.sample.long")}</Typography.Paragraph>
|
|
833
|
+
</Prose>
|
|
834
|
+
<CodeBlock size="xs" maxHeight="sm" aria-label={t("themeLab.sample.codeLabel")}>
|
|
835
|
+
{`[data-theme-style="flat"] {\n --card-radius: 0;\n}`}
|
|
836
|
+
</CodeBlock>
|
|
837
|
+
<Flex direction="row" gap="sm" align="center" wrap>
|
|
838
|
+
<Logo glyph="g" wordmark={t("themeLab.product")} />
|
|
839
|
+
<Icon as={Shield} size="md" tone="success" label={t("themeLab.sample.secure")} />
|
|
840
|
+
<Icon as={Clock} size="sm" tone="muted" />
|
|
841
|
+
<Activity label={t("themeLab.sample.syncing")} />
|
|
842
|
+
<Activity variant="bar" tone="info" label={t("themeLab.sample.syncing")} />
|
|
843
|
+
</Flex>
|
|
844
|
+
<Separator label={t("themeLab.sample.short")} labelAlign="start" />
|
|
845
|
+
<Marquee fade pauseOnHover label={t("themeLab.sample.marqueeLabel")}>
|
|
846
|
+
{["alpha", "beta", "gamma", "delta", "epsilon"].map((key) => (
|
|
847
|
+
<Text key={key} size="sm" tone="muted">
|
|
848
|
+
{t(`themeLab.sample.${key}`)}
|
|
849
|
+
</Text>
|
|
850
|
+
))}
|
|
851
|
+
</Marquee>
|
|
852
|
+
<AspectRatio ratio={16 / 5}>
|
|
853
|
+
<EmptyState
|
|
854
|
+
title={t("themeLab.sample.aspectTitle")}
|
|
855
|
+
description={t("themeLab.sample.short")}
|
|
856
|
+
/>
|
|
857
|
+
</AspectRatio>
|
|
858
|
+
<VisuallyHidden>{t("themeLab.sample.short")}</VisuallyHidden>
|
|
859
|
+
</Flex>
|
|
860
|
+
</Section>
|
|
861
|
+
|
|
862
|
+
{/* ── 3 · actions ──────────────────────────────────────────────────────────── */}
|
|
863
|
+
<Section id="actions" title={t("themeLab.section.actions")}>
|
|
864
|
+
<Row2 label={t("themeLab.label.variants")}>
|
|
865
|
+
<Button>{t("themeLab.action.save")}</Button>
|
|
866
|
+
<Button variant="secondary">{t("themeLab.action.secondary")}</Button>
|
|
867
|
+
<Button variant="outline">{t("themeLab.action.outline")}</Button>
|
|
868
|
+
<Button variant="ghost">{t("themeLab.action.ghost")}</Button>
|
|
869
|
+
<Button variant="dashed">{t("themeLab.action.dashed")}</Button>
|
|
870
|
+
<Button variant="destructive">{t("themeLab.action.destructive")}</Button>
|
|
871
|
+
<Button variant="link">{t("themeLab.action.link")}</Button>
|
|
872
|
+
<Button disabled>{t("themeLab.action.disabled")}</Button>
|
|
873
|
+
</Row2>
|
|
874
|
+
<Row2 label={t("themeLab.label.sizes")}>
|
|
875
|
+
<Button size="xs">xs</Button>
|
|
876
|
+
<Button size="sm">sm</Button>
|
|
877
|
+
<Button size="md">md</Button>
|
|
878
|
+
<Button size="lg">lg</Button>
|
|
879
|
+
<Button size="icon" aria-label={t("themeLab.action.search")}>
|
|
880
|
+
<Search aria-hidden="true" />
|
|
881
|
+
</Button>
|
|
882
|
+
</Row2>
|
|
883
|
+
<Row2 label={t("themeLab.label.toggles")}>
|
|
884
|
+
<Toggle aria-label={t("themeLab.action.bold")}>B</Toggle>
|
|
885
|
+
<Toggle
|
|
886
|
+
variant="soft"
|
|
887
|
+
shape="pill"
|
|
888
|
+
count={12}
|
|
889
|
+
countLabel={t("themeLab.label.items")}
|
|
890
|
+
>
|
|
891
|
+
{t("themeLab.action.unread")}
|
|
892
|
+
</Toggle>
|
|
893
|
+
<ToggleGroup
|
|
894
|
+
type="single"
|
|
895
|
+
defaultValue="left"
|
|
896
|
+
aria-label={t("themeLab.label.align")}
|
|
897
|
+
>
|
|
898
|
+
<ToggleGroupItem value="left">{t("themeLab.align.start")}</ToggleGroupItem>
|
|
899
|
+
<ToggleGroupItem value="center">{t("themeLab.align.center")}</ToggleGroupItem>
|
|
900
|
+
<ToggleGroupItem value="right">{t("themeLab.align.end")}</ToggleGroupItem>
|
|
901
|
+
</ToggleGroup>
|
|
902
|
+
<Rating name="score" aria-label={t("themeLab.label.rating")} defaultValue={4} />
|
|
903
|
+
</Row2>
|
|
904
|
+
<Row2 label={t("themeLab.label.actionsBar")}>
|
|
905
|
+
<Actions
|
|
906
|
+
label={t("themeLab.label.actionsBar")}
|
|
907
|
+
items={[
|
|
908
|
+
{ key: "retry", label: t("themeLab.action.retry"), icon: <RefreshCw /> },
|
|
909
|
+
{
|
|
910
|
+
key: "more",
|
|
911
|
+
label: t("themeLab.action.more"),
|
|
912
|
+
subItems: [
|
|
913
|
+
{ key: "share", label: t("themeLab.action.share"), icon: <Share2 /> },
|
|
914
|
+
],
|
|
915
|
+
},
|
|
916
|
+
{ key: "copy", actionRender: <ActionsCopy text={UNBREAKABLE} /> },
|
|
917
|
+
{ key: "feedback", actionRender: <ActionsFeedback /> },
|
|
918
|
+
]}
|
|
919
|
+
/>
|
|
920
|
+
</Row2>
|
|
921
|
+
</Section>
|
|
922
|
+
|
|
923
|
+
{/* ── 4 · status and data display ──────────────────────────────────────────── */}
|
|
924
|
+
<Section id="display" title={t("themeLab.section.display")}>
|
|
925
|
+
<Row2 label={t("themeLab.label.badges")}>
|
|
926
|
+
<Badge>{t("themeLab.tone.neutral")}</Badge>
|
|
927
|
+
<Badge tone="success">{t("themeLab.tone.success")}</Badge>
|
|
928
|
+
<Badge tone="warning">{t("themeLab.tone.warning")}</Badge>
|
|
929
|
+
<Badge tone="destructive">{t("themeLab.tone.destructive")}</Badge>
|
|
930
|
+
<Badge tone="info">{t("themeLab.tone.info")}</Badge>
|
|
931
|
+
<Badge variant="secondary">{t("themeLab.tone.neutral")}</Badge>
|
|
932
|
+
<StatusBadge status="active">{t("themeLab.tone.success")}</StatusBadge>
|
|
933
|
+
<Swatch color={seed.hex} aria-label={t(seed.nameKey)} />
|
|
934
|
+
</Row2>
|
|
935
|
+
<ResponsiveGrid columns={{ sm: 1, md: 2, lg: 4 }}>
|
|
936
|
+
<StatCard
|
|
937
|
+
label={t("themeLab.stat.members")}
|
|
938
|
+
value="12,450"
|
|
939
|
+
hint={t("themeLab.stat.hint")}
|
|
940
|
+
/>
|
|
941
|
+
<StatCard label={t("themeLab.stat.revenue")} value="8.2M" delta="+12%" />
|
|
942
|
+
<StatCard label={t("themeLab.stat.usage")} value="68.4%" />
|
|
943
|
+
<StatCard label={t("themeLab.stat.open")} value="3" />
|
|
944
|
+
</ResponsiveGrid>
|
|
945
|
+
<Flex direction="col" gap="sm">
|
|
946
|
+
<Progress value={68} label={t("themeLab.label.progress")} />
|
|
947
|
+
<Progress value={92} tone="warning" label={t("themeLab.label.progress")} />
|
|
948
|
+
<Flex direction="row" gap="xs" aria-label={t("themeLab.label.people")}>
|
|
949
|
+
<Avatar>
|
|
950
|
+
<AvatarFallback>MS</AvatarFallback>
|
|
951
|
+
</Avatar>
|
|
952
|
+
<Avatar>
|
|
953
|
+
<AvatarFallback>DC</AvatarFallback>
|
|
954
|
+
</Avatar>
|
|
955
|
+
<Avatar>
|
|
956
|
+
<AvatarFallback>PN</AvatarFallback>
|
|
957
|
+
</Avatar>
|
|
958
|
+
</Flex>
|
|
959
|
+
</Flex>
|
|
960
|
+
<ResponsiveGrid columns={{ sm: 1, md: 2 }}>
|
|
961
|
+
<Card>
|
|
962
|
+
<CardBar border="block-end">{t("themeLab.label.tools")}</CardBar>
|
|
963
|
+
<CardContent flush>
|
|
964
|
+
<ListRow
|
|
965
|
+
leading={<Smartphone aria-hidden="true" />}
|
|
966
|
+
title={t("themeLab.sample.alpha")}
|
|
967
|
+
description={t("themeLab.sample.short")}
|
|
968
|
+
trailing={<Badge tone="success">{t("themeLab.tone.success")}</Badge>}
|
|
969
|
+
/>
|
|
970
|
+
<ListRow
|
|
971
|
+
unread
|
|
972
|
+
overflow="wrap"
|
|
973
|
+
title={t("themeLab.sample.long")}
|
|
974
|
+
description={t("themeLab.sample.short")}
|
|
975
|
+
trailing={
|
|
976
|
+
<Button size="xs" variant="ghost">
|
|
977
|
+
{t("themeLab.action.more")}
|
|
978
|
+
</Button>
|
|
979
|
+
}
|
|
980
|
+
/>
|
|
981
|
+
</CardContent>
|
|
982
|
+
<CardFooter>
|
|
983
|
+
<Text size="xs" tone="muted">
|
|
984
|
+
{t("themeLab.sample.short")}
|
|
985
|
+
</Text>
|
|
986
|
+
</CardFooter>
|
|
987
|
+
</Card>
|
|
988
|
+
<Flex direction="col" gap="md">
|
|
989
|
+
<FeatureList
|
|
990
|
+
items={[
|
|
991
|
+
{ state: "included", label: "SSO", description: t("themeLab.sample.short") },
|
|
992
|
+
{ state: "limited", label: "API", description: t("themeLab.sample.short") },
|
|
993
|
+
{ state: "excluded", label: t("themeLab.sample.gamma") },
|
|
994
|
+
]}
|
|
995
|
+
/>
|
|
996
|
+
<Descriptions columns={2}>
|
|
997
|
+
<Descriptions.Item label={t("themeLab.table.name")} mono>
|
|
998
|
+
{UNBREAKABLE}
|
|
999
|
+
</Descriptions.Item>
|
|
1000
|
+
<Descriptions.Item label={t("themeLab.table.owner")}>
|
|
1001
|
+
{t("themeLab.sample.one")}
|
|
1002
|
+
</Descriptions.Item>
|
|
1003
|
+
<Descriptions.Item label={t("themeLab.table.status")} span={2}>
|
|
1004
|
+
{t("themeLab.sample.long")}
|
|
1005
|
+
</Descriptions.Item>
|
|
1006
|
+
</Descriptions>
|
|
1007
|
+
</Flex>
|
|
1008
|
+
</ResponsiveGrid>
|
|
1009
|
+
<ResponsiveGrid columns={{ sm: 1, md: 2 }}>
|
|
1010
|
+
<Timeline
|
|
1011
|
+
items={[
|
|
1012
|
+
{ title: t("themeLab.sample.alpha"), time: "2026-09-21 10:00" },
|
|
1013
|
+
{ title: t("themeLab.sample.beta"), time: "2026-09-22 14:00" },
|
|
1014
|
+
{ title: t("themeLab.sample.gamma"), current: true },
|
|
1015
|
+
]}
|
|
1016
|
+
/>
|
|
1017
|
+
<RangeTimeline
|
|
1018
|
+
label={t("themeLab.label.schedule")}
|
|
1019
|
+
columns={[{ label: t("themeLab.label.week"), units: 7 }]}
|
|
1020
|
+
rows={[
|
|
1021
|
+
{
|
|
1022
|
+
id: "task",
|
|
1023
|
+
label: t("themeLab.sample.alpha"),
|
|
1024
|
+
start: 0,
|
|
1025
|
+
end: 4,
|
|
1026
|
+
startLabel: t("themeLab.label.start"),
|
|
1027
|
+
endLabel: t("themeLab.label.end"),
|
|
1028
|
+
},
|
|
1029
|
+
]}
|
|
1030
|
+
/>
|
|
1031
|
+
</ResponsiveGrid>
|
|
1032
|
+
<TimelineGrid
|
|
1033
|
+
label={t("themeLab.label.schedule")}
|
|
1034
|
+
start="08:00"
|
|
1035
|
+
end="20:00"
|
|
1036
|
+
interval={2}
|
|
1037
|
+
now="14:35"
|
|
1038
|
+
columns={[
|
|
1039
|
+
{ id: "d1", label: t("themeLab.sample.alpha") },
|
|
1040
|
+
{ id: "d2", label: t("themeLab.sample.beta"), current: true },
|
|
1041
|
+
]}
|
|
1042
|
+
events={[
|
|
1043
|
+
{
|
|
1044
|
+
id: "e1",
|
|
1045
|
+
columnId: "d2",
|
|
1046
|
+
start: "09:00",
|
|
1047
|
+
end: "17:30",
|
|
1048
|
+
title: t("themeLab.sample.gamma"),
|
|
1049
|
+
description: t("themeLab.sample.one"),
|
|
1050
|
+
},
|
|
1051
|
+
]}
|
|
1052
|
+
/>
|
|
1053
|
+
<ResponsiveGrid columns={{ sm: 1, md: 2 }}>
|
|
1054
|
+
<Accordion type="single" collapsible>
|
|
1055
|
+
<AccordionItem value="a">
|
|
1056
|
+
<AccordionTrigger>{t("themeLab.sample.alpha")}</AccordionTrigger>
|
|
1057
|
+
<AccordionContent>{t("themeLab.sample.long")}</AccordionContent>
|
|
1058
|
+
</AccordionItem>
|
|
1059
|
+
<AccordionItem value="b">
|
|
1060
|
+
<AccordionTrigger>{t("themeLab.sample.beta")}</AccordionTrigger>
|
|
1061
|
+
<AccordionContent>{t("themeLab.sample.short")}</AccordionContent>
|
|
1062
|
+
</AccordionItem>
|
|
1063
|
+
</Accordion>
|
|
1064
|
+
<Flex direction="col" gap="sm" align="start">
|
|
1065
|
+
<Collapsible>
|
|
1066
|
+
<Flex direction="col" align="start" gap="sm">
|
|
1067
|
+
<CollapsibleTrigger asChild>
|
|
1068
|
+
<Button variant="ghost" size="sm">
|
|
1069
|
+
<ChevronDown aria-hidden="true" />
|
|
1070
|
+
{t("themeLab.action.more")}
|
|
1071
|
+
</Button>
|
|
1072
|
+
</CollapsibleTrigger>
|
|
1073
|
+
<CollapsibleContent>
|
|
1074
|
+
<Text size="sm">{t("themeLab.sample.long")}</Text>
|
|
1075
|
+
</CollapsibleContent>
|
|
1076
|
+
</Flex>
|
|
1077
|
+
</Collapsible>
|
|
1078
|
+
{/* No `asChild`. HoverCardTrigger is a react-aria `Focusable`, and a `Button`
|
|
1079
|
+
handed to it through `asChild` warned "<Focusable> child must forward its ref
|
|
1080
|
+
to a DOM element" on every render. The catalogue's own example is a bare
|
|
1081
|
+
trigger, and that is what this uses. */}
|
|
1082
|
+
<HoverCard>
|
|
1083
|
+
<HoverCardTrigger>{t("themeLab.sample.one")}</HoverCardTrigger>
|
|
1084
|
+
<HoverCardContent>{t("themeLab.sample.short")}</HoverCardContent>
|
|
1085
|
+
</HoverCard>
|
|
1086
|
+
</Flex>
|
|
1087
|
+
</ResponsiveGrid>
|
|
1088
|
+
<Card>
|
|
1089
|
+
<CardContent flush>
|
|
1090
|
+
<ScrollArea>
|
|
1091
|
+
<Flex direction="col" gap="sm">
|
|
1092
|
+
{["alpha", "beta", "gamma", "delta", "epsilon"].map((key) => (
|
|
1093
|
+
<Text key={key} size="sm">
|
|
1094
|
+
{t(`themeLab.sample.${key}`)}
|
|
1095
|
+
</Text>
|
|
1096
|
+
))}
|
|
1097
|
+
</Flex>
|
|
1098
|
+
</ScrollArea>
|
|
1099
|
+
</CardContent>
|
|
1100
|
+
</Card>
|
|
1101
|
+
<Tree
|
|
1102
|
+
aria-label={t("themeLab.label.permissions")}
|
|
1103
|
+
treeData={[
|
|
1104
|
+
{
|
|
1105
|
+
value: "billing",
|
|
1106
|
+
label: t("themeLab.sample.alpha"),
|
|
1107
|
+
children: [
|
|
1108
|
+
{ value: "billing.read", label: t("themeLab.sample.beta") },
|
|
1109
|
+
{ value: "billing.write", label: t("themeLab.sample.gamma") },
|
|
1110
|
+
],
|
|
1111
|
+
},
|
|
1112
|
+
]}
|
|
1113
|
+
checkable
|
|
1114
|
+
defaultExpandAll
|
|
1115
|
+
checkedValues={treeChecked}
|
|
1116
|
+
onCheckedValuesChange={setTreeChecked}
|
|
1117
|
+
/>
|
|
1118
|
+
<Carousel opts={{ loop: true }}>
|
|
1119
|
+
<CarouselContent>
|
|
1120
|
+
{["alpha", "beta", "gamma"].map((key) => (
|
|
1121
|
+
<CarouselItem key={key}>
|
|
1122
|
+
<Card>
|
|
1123
|
+
<CardContent>
|
|
1124
|
+
<Text>{t(`themeLab.sample.${key}`)}</Text>
|
|
1125
|
+
</CardContent>
|
|
1126
|
+
</Card>
|
|
1127
|
+
</CarouselItem>
|
|
1128
|
+
))}
|
|
1129
|
+
</CarouselContent>
|
|
1130
|
+
<CarouselPrevious />
|
|
1131
|
+
<CarouselNext />
|
|
1132
|
+
<CarouselDots />
|
|
1133
|
+
</Carousel>
|
|
1134
|
+
<Masonry
|
|
1135
|
+
columns={{ base: 1, sm: 2, lg: 3 }}
|
|
1136
|
+
gap="md"
|
|
1137
|
+
items={["alpha", "beta", "gamma", "delta"].map((key) => ({ key, data: key }))}
|
|
1138
|
+
itemRender={({ data }) => (
|
|
1139
|
+
<Card>
|
|
1140
|
+
<CardContent>
|
|
1141
|
+
<Text>{t(`themeLab.sample.${data}`)}</Text>
|
|
1142
|
+
</CardContent>
|
|
1143
|
+
</Card>
|
|
1144
|
+
)}
|
|
1145
|
+
/>
|
|
1146
|
+
<Flex direction="row" gap="md" wrap align="start">
|
|
1147
|
+
<QrCode
|
|
1148
|
+
value="https://example.test/enrol"
|
|
1149
|
+
label={t("themeLab.label.qr")}
|
|
1150
|
+
size="md"
|
|
1151
|
+
/>
|
|
1152
|
+
<Thumbnail
|
|
1153
|
+
src={shotLandscape}
|
|
1154
|
+
width={960}
|
|
1155
|
+
height={540}
|
|
1156
|
+
alt={t("themeLab.label.thumbnail")}
|
|
1157
|
+
size="lg"
|
|
1158
|
+
/>
|
|
1159
|
+
<CredentialReveal
|
|
1160
|
+
label={t("themeLab.label.credential")}
|
|
1161
|
+
secret="gxp_live_8Fh2kQ9wR7nZ1xV4bT6mL0cD"
|
|
1162
|
+
defaultRevealed
|
|
1163
|
+
/>
|
|
1164
|
+
</Flex>
|
|
1165
|
+
</Section>
|
|
1166
|
+
|
|
1167
|
+
{/* ── 5 · tables ───────────────────────────────────────────────────────────── */}
|
|
1168
|
+
<Section id="tables" title={t("themeLab.section.tables")}>
|
|
1169
|
+
<Card>
|
|
1170
|
+
<CardContent flush>
|
|
1171
|
+
<DataTable
|
|
1172
|
+
columns={columns}
|
|
1173
|
+
data={ROWS}
|
|
1174
|
+
getRowId={(row) => row.id}
|
|
1175
|
+
label={t("themeLab.section.tables")}
|
|
1176
|
+
/>
|
|
1177
|
+
</CardContent>
|
|
1178
|
+
</Card>
|
|
1179
|
+
<Table>
|
|
1180
|
+
<TableHeader>
|
|
1181
|
+
<TableRow>
|
|
1182
|
+
<TableHead>{t("themeLab.table.name")}</TableHead>
|
|
1183
|
+
<TableHead numeric>{t("themeLab.table.amount")}</TableHead>
|
|
1184
|
+
</TableRow>
|
|
1185
|
+
</TableHeader>
|
|
1186
|
+
<TableBody>
|
|
1187
|
+
<TableRow>
|
|
1188
|
+
<TableCell>{t("themeLab.sample.alpha")}</TableCell>
|
|
1189
|
+
<TableCell numeric>1,200</TableCell>
|
|
1190
|
+
</TableRow>
|
|
1191
|
+
<TableRow>
|
|
1192
|
+
<TableCell>{t("themeLab.sample.beta")}</TableCell>
|
|
1193
|
+
<TableCell numeric>860</TableCell>
|
|
1194
|
+
</TableRow>
|
|
1195
|
+
</TableBody>
|
|
1196
|
+
</Table>
|
|
1197
|
+
<Card>
|
|
1198
|
+
<CardContent flush>
|
|
1199
|
+
<PermissionMatrix
|
|
1200
|
+
roles={[
|
|
1201
|
+
{ id: "admin", name: t("themeLab.role.admin") },
|
|
1202
|
+
{ id: "viewer", name: t("themeLab.role.viewer") },
|
|
1203
|
+
]}
|
|
1204
|
+
permissions={[
|
|
1205
|
+
{ id: "read", name: t("themeLab.perm.read") },
|
|
1206
|
+
{ id: "write", name: t("themeLab.perm.write") },
|
|
1207
|
+
]}
|
|
1208
|
+
grants={new Set(["admin:read", "admin:write", "viewer:read"])}
|
|
1209
|
+
readOnly
|
|
1210
|
+
/>
|
|
1211
|
+
</CardContent>
|
|
1212
|
+
</Card>
|
|
1213
|
+
<SkeletonTable rows={3} columns={4} />
|
|
1214
|
+
<SkeletonRows rows={2} columns={3} />
|
|
1215
|
+
</Section>
|
|
1216
|
+
|
|
1217
|
+
{/* ── 6 · charts ───────────────────────────────────────────────────────────── */}
|
|
1218
|
+
<Section id="charts" title={t("themeLab.section.charts")}>
|
|
1219
|
+
<ResponsiveGrid columns={{ sm: 1, lg: 2 }}>
|
|
1220
|
+
<LineChart
|
|
1221
|
+
label={t("themeLab.chart.trend")}
|
|
1222
|
+
data={SERIES}
|
|
1223
|
+
categoryKey="month"
|
|
1224
|
+
series={[
|
|
1225
|
+
{ dataKey: "plan", label: t("themeLab.chart.plan") },
|
|
1226
|
+
{ dataKey: "actual", label: t("themeLab.chart.actual") },
|
|
1227
|
+
]}
|
|
1228
|
+
/>
|
|
1229
|
+
<BarChart
|
|
1230
|
+
label={t("themeLab.chart.byMonth")}
|
|
1231
|
+
data={SERIES}
|
|
1232
|
+
categoryKey="month"
|
|
1233
|
+
series={[{ dataKey: "actual", label: t("themeLab.chart.actual") }]}
|
|
1234
|
+
/>
|
|
1235
|
+
<AreaChart
|
|
1236
|
+
label={t("themeLab.chart.stacked")}
|
|
1237
|
+
data={SERIES}
|
|
1238
|
+
categoryKey="month"
|
|
1239
|
+
series={[
|
|
1240
|
+
{ dataKey: "plan", label: t("themeLab.chart.plan") },
|
|
1241
|
+
{ dataKey: "actual", label: t("themeLab.chart.actual") },
|
|
1242
|
+
]}
|
|
1243
|
+
stacked
|
|
1244
|
+
/>
|
|
1245
|
+
<PieChart
|
|
1246
|
+
label={t("themeLab.chart.split")}
|
|
1247
|
+
data={[
|
|
1248
|
+
{ category: t("themeLab.sample.alpha"), amount: 42 },
|
|
1249
|
+
{ category: t("themeLab.sample.beta"), amount: 31 },
|
|
1250
|
+
{ category: t("themeLab.sample.gamma"), amount: 27 },
|
|
1251
|
+
]}
|
|
1252
|
+
dataKey="amount"
|
|
1253
|
+
nameKey="category"
|
|
1254
|
+
donut
|
|
1255
|
+
/>
|
|
1256
|
+
</ResponsiveGrid>
|
|
1257
|
+
<CompactBarTrend
|
|
1258
|
+
label={t("themeLab.chart.weekly")}
|
|
1259
|
+
description={t("themeLab.sample.short")}
|
|
1260
|
+
data={TREND}
|
|
1261
|
+
categoryKey="date"
|
|
1262
|
+
valueKey="count"
|
|
1263
|
+
size="xs"
|
|
1264
|
+
/>
|
|
1265
|
+
</Section>
|
|
1266
|
+
|
|
1267
|
+
{/* ── 7 · form controls ────────────────────────────────────────────────────── */}
|
|
1268
|
+
<Section
|
|
1269
|
+
id="fields"
|
|
1270
|
+
title={t("themeLab.section.fields")}
|
|
1271
|
+
note={t("themeLab.section.fieldsNote")}
|
|
1272
|
+
>
|
|
1273
|
+
<Form
|
|
1274
|
+
layout="vertical"
|
|
1275
|
+
columns={2}
|
|
1276
|
+
/* A server error map, so `FormErrors` has something to summarise: with none it
|
|
1277
|
+
renders nothing at all, which on a coverage page reads as a missing component
|
|
1278
|
+
rather than as an empty state. */
|
|
1279
|
+
errors={{ email: t("themeLab.field.emailError") }}
|
|
1280
|
+
>
|
|
1281
|
+
<FormErrors />
|
|
1282
|
+
<FormField
|
|
1283
|
+
id="f-name"
|
|
1284
|
+
label={t("themeLab.field.name")}
|
|
1285
|
+
required
|
|
1286
|
+
helper={t("themeLab.sample.short")}
|
|
1287
|
+
>
|
|
1288
|
+
<Input
|
|
1289
|
+
id="f-name"
|
|
1290
|
+
name="name"
|
|
1291
|
+
placeholder={t("themeLab.field.namePlaceholder")}
|
|
1292
|
+
/>
|
|
1293
|
+
</FormField>
|
|
1294
|
+
<FormField
|
|
1295
|
+
id="f-error"
|
|
1296
|
+
label={t("themeLab.field.email")}
|
|
1297
|
+
error={t("themeLab.field.emailError")}
|
|
1298
|
+
>
|
|
1299
|
+
<Input id="f-error" name="email" defaultValue="not-an-email" />
|
|
1300
|
+
</FormField>
|
|
1301
|
+
<FormField id="f-search" label={t("themeLab.field.search")}>
|
|
1302
|
+
<SearchInput
|
|
1303
|
+
id="f-search"
|
|
1304
|
+
value={query}
|
|
1305
|
+
onSearch={setQuery}
|
|
1306
|
+
placeholder={t("themeLab.field.searchPlaceholder")}
|
|
1307
|
+
/>
|
|
1308
|
+
</FormField>
|
|
1309
|
+
<FormField id="f-qty" label={t("themeLab.field.quantity")}>
|
|
1310
|
+
<NumberInput
|
|
1311
|
+
id="f-qty"
|
|
1312
|
+
aria-label={t("themeLab.field.quantity")}
|
|
1313
|
+
defaultValue={3}
|
|
1314
|
+
min={0}
|
|
1315
|
+
max={99}
|
|
1316
|
+
/>
|
|
1317
|
+
</FormField>
|
|
1318
|
+
<FormField id="f-pw" label={t("themeLab.field.password")}>
|
|
1319
|
+
<PasswordInput
|
|
1320
|
+
id="f-pw"
|
|
1321
|
+
value={password}
|
|
1322
|
+
onChange={(event) => setPassword(event.target.value)}
|
|
1323
|
+
autoComplete="new-password"
|
|
1324
|
+
/>
|
|
1325
|
+
</FormField>
|
|
1326
|
+
<FormField id="f-notes" label={t("themeLab.field.notes")} colSpan={2}>
|
|
1327
|
+
<Textarea id="f-notes" rows={3} placeholder={t("themeLab.sample.short")} />
|
|
1328
|
+
</FormField>
|
|
1329
|
+
<FormField id="f-tags" label={t("themeLab.field.tags")}>
|
|
1330
|
+
<TagInput
|
|
1331
|
+
id="f-tags"
|
|
1332
|
+
aria-label={t("themeLab.field.tags")}
|
|
1333
|
+
value={tags}
|
|
1334
|
+
onValueChange={setTags}
|
|
1335
|
+
/>
|
|
1336
|
+
</FormField>
|
|
1337
|
+
<FormField id="f-role" label={t("themeLab.field.role")}>
|
|
1338
|
+
<Select
|
|
1339
|
+
id="f-role"
|
|
1340
|
+
value={role}
|
|
1341
|
+
onValueChange={setRole}
|
|
1342
|
+
options={[
|
|
1343
|
+
{ value: "viewer", label: t("themeLab.role.viewer") },
|
|
1344
|
+
{ value: "editor", label: t("themeLab.role.editor") },
|
|
1345
|
+
{ value: "admin", label: t("themeLab.role.admin") },
|
|
1346
|
+
]}
|
|
1347
|
+
/>
|
|
1348
|
+
</FormField>
|
|
1349
|
+
<FormField id="f-date" label={t("themeLab.field.date")}>
|
|
1350
|
+
<DatePicker id="f-date" value={date} onValueChange={setDate} />
|
|
1351
|
+
</FormField>
|
|
1352
|
+
<FormField id="f-time" label={t("themeLab.field.time")}>
|
|
1353
|
+
<TimePicker id="f-time" value={time} onValueChange={setTime} minuteStep={15} />
|
|
1354
|
+
</FormField>
|
|
1355
|
+
<FormField id="f-range" label={t("themeLab.field.timeRange")}>
|
|
1356
|
+
<TimeRangePicker
|
|
1357
|
+
aria-label={t("themeLab.field.timeRange")}
|
|
1358
|
+
defaultValue={["09:00", "18:00"]}
|
|
1359
|
+
/>
|
|
1360
|
+
</FormField>
|
|
1361
|
+
<FormField id="f-colour" label={t("themeLab.field.colour")}>
|
|
1362
|
+
<ColorPicker
|
|
1363
|
+
id="f-colour"
|
|
1364
|
+
aria-label={t("themeLab.field.colour")}
|
|
1365
|
+
value={colour}
|
|
1366
|
+
onValueChange={setColour}
|
|
1367
|
+
/>
|
|
1368
|
+
</FormField>
|
|
1369
|
+
<FormField id="f-volume" label={t("themeLab.field.volume")}>
|
|
1370
|
+
<Slider
|
|
1371
|
+
id="f-volume"
|
|
1372
|
+
aria-label={t("themeLab.field.volume")}
|
|
1373
|
+
value={volume}
|
|
1374
|
+
onChange={(next) => setVolume(next as number)}
|
|
1375
|
+
/>
|
|
1376
|
+
</FormField>
|
|
1377
|
+
<FormField id="f-cascade" label={t("themeLab.field.region")}>
|
|
1378
|
+
<Cascader
|
|
1379
|
+
id="f-cascade"
|
|
1380
|
+
options={[
|
|
1381
|
+
{
|
|
1382
|
+
value: "north",
|
|
1383
|
+
label: t("themeLab.sample.alpha"),
|
|
1384
|
+
children: [
|
|
1385
|
+
{ value: "n1", label: t("themeLab.sample.beta") },
|
|
1386
|
+
{ value: "n2", label: t("themeLab.sample.gamma") },
|
|
1387
|
+
],
|
|
1388
|
+
},
|
|
1389
|
+
]}
|
|
1390
|
+
value={cascade}
|
|
1391
|
+
onValueChange={(next) => setCascade(next as string[])}
|
|
1392
|
+
showSearch
|
|
1393
|
+
/>
|
|
1394
|
+
</FormField>
|
|
1395
|
+
<FormField id="f-scope" label={t("themeLab.field.scope")} colSpan={2}>
|
|
1396
|
+
<BranchScopePicker
|
|
1397
|
+
id="f-scope"
|
|
1398
|
+
branches={[
|
|
1399
|
+
{ id: "b1", name: t("themeLab.sample.alpha") },
|
|
1400
|
+
{
|
|
1401
|
+
id: "b2",
|
|
1402
|
+
name: t("themeLab.sample.beta"),
|
|
1403
|
+
description: t("themeLab.sample.short"),
|
|
1404
|
+
},
|
|
1405
|
+
{ id: "b3", name: t("themeLab.sample.gamma"), disabled: true },
|
|
1406
|
+
]}
|
|
1407
|
+
value={scope}
|
|
1408
|
+
onValueChange={setScope}
|
|
1409
|
+
searchable
|
|
1410
|
+
/>
|
|
1411
|
+
</FormField>
|
|
1412
|
+
<FormField id="f-account" label={t("themeLab.field.account")}>
|
|
1413
|
+
<TreeSelect
|
|
1414
|
+
id="f-account"
|
|
1415
|
+
/* `children`, not `content`: Cascader's option type nests through `content`
|
|
1416
|
+
* and TreeSelect's `TreeOptionProp` through `children`. The MCP catalogue's
|
|
1417
|
+
* own TreeSelect example writes `content` and does not typecheck — recorded
|
|
1418
|
+
* here rather than quietly corrected, because the catalogue is what a
|
|
1419
|
+
* consumer copies. */
|
|
1420
|
+
treeData={[
|
|
1421
|
+
{
|
|
1422
|
+
value: "assets",
|
|
1423
|
+
label: t("themeLab.sample.alpha"),
|
|
1424
|
+
children: [
|
|
1425
|
+
{ value: "cash", label: t("themeLab.sample.beta") },
|
|
1426
|
+
{ value: "ar", label: t("themeLab.sample.gamma") },
|
|
1427
|
+
],
|
|
1428
|
+
},
|
|
1429
|
+
]}
|
|
1430
|
+
value={account}
|
|
1431
|
+
onValueChange={(next) => setAccount(next as string | undefined)}
|
|
1432
|
+
showSearch
|
|
1433
|
+
treeDefaultExpandAll
|
|
1434
|
+
allowClear
|
|
1435
|
+
/>
|
|
1436
|
+
</FormField>
|
|
1437
|
+
</Form>
|
|
1438
|
+
<Flex direction="col" gap="md">
|
|
1439
|
+
<Field
|
|
1440
|
+
id="f-switch"
|
|
1441
|
+
label={t("themeLab.field.notify")}
|
|
1442
|
+
description={t("themeLab.sample.short")}
|
|
1443
|
+
>
|
|
1444
|
+
<Switch
|
|
1445
|
+
id="f-switch"
|
|
1446
|
+
aria-label={t("themeLab.field.notify")}
|
|
1447
|
+
checked={switched}
|
|
1448
|
+
onCheckedChange={setSwitched}
|
|
1449
|
+
/>
|
|
1450
|
+
</Field>
|
|
1451
|
+
<Label htmlFor="f-agree">{t("themeLab.field.agree")}</Label>
|
|
1452
|
+
<Checkbox
|
|
1453
|
+
id="f-agree"
|
|
1454
|
+
checked={checked}
|
|
1455
|
+
onCheckedChange={(next) => setChecked(next === true)}
|
|
1456
|
+
>
|
|
1457
|
+
{t("themeLab.field.agree")}
|
|
1458
|
+
</Checkbox>
|
|
1459
|
+
<CheckboxGroup
|
|
1460
|
+
name="perms"
|
|
1461
|
+
aria-label={t("themeLab.label.permissions")}
|
|
1462
|
+
options={[
|
|
1463
|
+
{ label: t("themeLab.perm.read"), value: "read" },
|
|
1464
|
+
{
|
|
1465
|
+
label: t("themeLab.perm.write"),
|
|
1466
|
+
value: "write",
|
|
1467
|
+
description: t("themeLab.sample.short"),
|
|
1468
|
+
},
|
|
1469
|
+
{ label: t("themeLab.perm.admin"), value: "admin", disabled: true },
|
|
1470
|
+
]}
|
|
1471
|
+
defaultValue={["read"]}
|
|
1472
|
+
orientation="vertical"
|
|
1473
|
+
/>
|
|
1474
|
+
<RadioGroup
|
|
1475
|
+
name="tier"
|
|
1476
|
+
aria-label={t("themeLab.label.variants")}
|
|
1477
|
+
defaultValue="a"
|
|
1478
|
+
orientation="horizontal"
|
|
1479
|
+
options={[
|
|
1480
|
+
{ label: t("themeLab.sample.alpha"), value: "a" },
|
|
1481
|
+
{ label: t("themeLab.sample.beta"), value: "b" },
|
|
1482
|
+
]}
|
|
1483
|
+
/>
|
|
1484
|
+
<Radio.Group
|
|
1485
|
+
name="method"
|
|
1486
|
+
aria-label={t("themeLab.label.sizes")}
|
|
1487
|
+
defaultValue="card"
|
|
1488
|
+
orientation="vertical"
|
|
1489
|
+
options={[
|
|
1490
|
+
{
|
|
1491
|
+
label: t("themeLab.sample.gamma"),
|
|
1492
|
+
value: "card",
|
|
1493
|
+
description: t("themeLab.sample.short"),
|
|
1494
|
+
},
|
|
1495
|
+
{ label: t("themeLab.sample.delta"), value: "bank" },
|
|
1496
|
+
]}
|
|
1497
|
+
/>
|
|
1498
|
+
<PasswordStrength value={password} />
|
|
1499
|
+
<InputOTP maxLength={6} value={otp} onChange={setOtp}>
|
|
1500
|
+
<InputOTPGroup>
|
|
1501
|
+
{[0, 1, 2, 3, 4, 5].map((index) => (
|
|
1502
|
+
<InputOTPSlot key={index} index={index} />
|
|
1503
|
+
))}
|
|
1504
|
+
</InputOTPGroup>
|
|
1505
|
+
</InputOTP>
|
|
1506
|
+
<Upload variant="dropzone" value={uploads} onValueChange={setUploads} />
|
|
1507
|
+
<Attachments items={[]} overflow="scrollX" />
|
|
1508
|
+
<Command label={t("themeLab.label.quickPick")} loop>
|
|
1509
|
+
<CommandInput placeholder={t("themeLab.field.searchPlaceholder")} />
|
|
1510
|
+
<CommandList>
|
|
1511
|
+
<CommandEmpty>{t("themeLab.label.noResults")}</CommandEmpty>
|
|
1512
|
+
<CommandGroup heading={t("themeLab.sample.alpha")}>
|
|
1513
|
+
<CommandItem value="one">{t("themeLab.sample.one")}</CommandItem>
|
|
1514
|
+
<CommandItem value="two">{t("themeLab.sample.two")}</CommandItem>
|
|
1515
|
+
</CommandGroup>
|
|
1516
|
+
</CommandList>
|
|
1517
|
+
</Command>
|
|
1518
|
+
<Transfer
|
|
1519
|
+
/* `key`, not `value` — the MCP catalogue's Transfer example writes `value` and
|
|
1520
|
+
* does not typecheck either. Same note as TreeSelect above. */
|
|
1521
|
+
dataSource={[
|
|
1522
|
+
{ key: "1010", title: t("themeLab.sample.alpha") },
|
|
1523
|
+
{ key: "1020", title: t("themeLab.sample.beta") },
|
|
1524
|
+
{ key: "2010", title: t("themeLab.sample.gamma") },
|
|
1525
|
+
]}
|
|
1526
|
+
targetKeys={transferred}
|
|
1527
|
+
onValueChange={setTransferred}
|
|
1528
|
+
titles={[t("themeLab.label.available"), t("themeLab.label.selected")]}
|
|
1529
|
+
showSearch
|
|
1530
|
+
/>
|
|
1531
|
+
<Calendar
|
|
1532
|
+
mode="single"
|
|
1533
|
+
selected={date}
|
|
1534
|
+
onSelect={setDate}
|
|
1535
|
+
aria-label={t("themeLab.field.date")}
|
|
1536
|
+
/>
|
|
1537
|
+
</Flex>
|
|
1538
|
+
</Section>
|
|
1539
|
+
|
|
1540
|
+
{/* ── 8 · navigation ───────────────────────────────────────────────────────── */}
|
|
1541
|
+
<Section id="nav" title={t("themeLab.section.nav")}>
|
|
1542
|
+
<Breadcrumb
|
|
1543
|
+
items={[
|
|
1544
|
+
/* Distinct hrefs, because Breadcrumb keys its rows by `to` and two "#" rows
|
|
1545
|
+
* produced a duplicate-key error in React. */
|
|
1546
|
+
{ label: t("themeLab.nav.overview"), to: "#overview" },
|
|
1547
|
+
{ label: t("themeLab.nav.reports"), to: "#nav" },
|
|
1548
|
+
{ label: t("themeLab.sample.alpha") },
|
|
1549
|
+
]}
|
|
1550
|
+
/>
|
|
1551
|
+
<Tabs
|
|
1552
|
+
defaultValue="overview"
|
|
1553
|
+
items={[
|
|
1554
|
+
{
|
|
1555
|
+
value: "overview",
|
|
1556
|
+
label: t("themeLab.nav.overview"),
|
|
1557
|
+
content: t("themeLab.sample.long"),
|
|
1558
|
+
},
|
|
1559
|
+
{
|
|
1560
|
+
value: "reports",
|
|
1561
|
+
label: t("themeLab.nav.reports"),
|
|
1562
|
+
content: t("themeLab.sample.short"),
|
|
1563
|
+
},
|
|
1564
|
+
]}
|
|
1565
|
+
/>
|
|
1566
|
+
<Steps
|
|
1567
|
+
value={step}
|
|
1568
|
+
items={[
|
|
1569
|
+
{ title: t("themeLab.sample.alpha") },
|
|
1570
|
+
{ title: t("themeLab.sample.beta") },
|
|
1571
|
+
{ title: t("themeLab.sample.gamma") },
|
|
1572
|
+
]}
|
|
1573
|
+
/>
|
|
1574
|
+
<Pagination value={page} total={96} pageSize={10} showTotal onValueChange={setPage} />
|
|
1575
|
+
<Toolbar hasActiveFilters onClear={() => setQuery("")}>
|
|
1576
|
+
<SearchInput
|
|
1577
|
+
value={query}
|
|
1578
|
+
onSearch={setQuery}
|
|
1579
|
+
placeholder={t("themeLab.field.searchPlaceholder")}
|
|
1580
|
+
/>
|
|
1581
|
+
<ToolbarGroup label={t("themeLab.table.status")}>
|
|
1582
|
+
<Select
|
|
1583
|
+
aria-label={t("themeLab.table.status")}
|
|
1584
|
+
value={role}
|
|
1585
|
+
onValueChange={setRole}
|
|
1586
|
+
options={[
|
|
1587
|
+
{ value: "viewer", label: t("themeLab.role.viewer") },
|
|
1588
|
+
{ value: "admin", label: t("themeLab.role.admin") },
|
|
1589
|
+
]}
|
|
1590
|
+
/>
|
|
1591
|
+
</ToolbarGroup>
|
|
1592
|
+
</Toolbar>
|
|
1593
|
+
<FilterBar
|
|
1594
|
+
search={{
|
|
1595
|
+
value: query,
|
|
1596
|
+
onValueChange: setQuery,
|
|
1597
|
+
placeholder: t("themeLab.field.searchPlaceholder"),
|
|
1598
|
+
}}
|
|
1599
|
+
resultCount={ROWS.length}
|
|
1600
|
+
actions={<Button size="sm">{t("themeLab.action.save")}</Button>}
|
|
1601
|
+
/>
|
|
1602
|
+
<MegaMenu
|
|
1603
|
+
label={t("themeLab.label.mainNav")}
|
|
1604
|
+
value={route}
|
|
1605
|
+
onValueChange={setRoute}
|
|
1606
|
+
items={[
|
|
1607
|
+
{
|
|
1608
|
+
key: "products",
|
|
1609
|
+
label: t("themeLab.sample.alpha"),
|
|
1610
|
+
panel: {
|
|
1611
|
+
groups: [
|
|
1612
|
+
{
|
|
1613
|
+
key: "core",
|
|
1614
|
+
label: t("themeLab.sample.beta"),
|
|
1615
|
+
links: [
|
|
1616
|
+
{ key: "hr", label: t("themeLab.sample.gamma"), href: "#hr" },
|
|
1617
|
+
{ key: "pay", label: t("themeLab.sample.delta"), href: "#pay" },
|
|
1618
|
+
],
|
|
1619
|
+
},
|
|
1620
|
+
],
|
|
1621
|
+
},
|
|
1622
|
+
},
|
|
1623
|
+
{ key: "pricing", label: t("themeLab.sample.epsilon"), href: "#pricing" },
|
|
1624
|
+
]}
|
|
1625
|
+
/>
|
|
1626
|
+
<NavList
|
|
1627
|
+
label={t("themeLab.nav.section")}
|
|
1628
|
+
activeId="overview"
|
|
1629
|
+
items={[
|
|
1630
|
+
{
|
|
1631
|
+
id: "account",
|
|
1632
|
+
label: t("themeLab.sample.alpha"),
|
|
1633
|
+
icon: Users,
|
|
1634
|
+
children: [
|
|
1635
|
+
{ id: "overview", label: t("themeLab.nav.overview"), href: "#overview" },
|
|
1636
|
+
{ id: "people", label: t("themeLab.nav.people"), href: "#people" },
|
|
1637
|
+
],
|
|
1638
|
+
},
|
|
1639
|
+
{
|
|
1640
|
+
id: "appearance",
|
|
1641
|
+
label: t("themeLab.nav.settings"),
|
|
1642
|
+
icon: Palette,
|
|
1643
|
+
href: "#settings",
|
|
1644
|
+
},
|
|
1645
|
+
]}
|
|
1646
|
+
/>
|
|
1647
|
+
{/*
|
|
1648
|
+
* `affix={false}`, because Anchor has NO `target` prop — unlike Affix, which does.
|
|
1649
|
+
* So an affixed Anchor can only pin to the viewport, and on this page that put it
|
|
1650
|
+
* on top of the PageContainer header alongside the Affix. Turning the pinning off
|
|
1651
|
+
* is the only thing the API offers here; that asymmetry between two components
|
|
1652
|
+
* whose whole job is the same behaviour is filed as its own issue.
|
|
1653
|
+
*/}
|
|
1654
|
+
<Anchor
|
|
1655
|
+
affix={false}
|
|
1656
|
+
label={t("themeLab.label.onThisPage")}
|
|
1657
|
+
items={[
|
|
1658
|
+
{ key: "type", href: "#type", title: t("themeLab.section.type") },
|
|
1659
|
+
{ key: "actions", href: "#actions", title: t("themeLab.section.actions") },
|
|
1660
|
+
{ key: "tables", href: "#tables", title: t("themeLab.section.tables") },
|
|
1661
|
+
]}
|
|
1662
|
+
/>
|
|
1663
|
+
{/*
|
|
1664
|
+
* Affix AND Anchor pin to their SCROLL TARGET, and without one that target is the
|
|
1665
|
+
* viewport — so on this page both landed on top of the PageContainer header, three
|
|
1666
|
+
* `position: fixed` bars stacked over the title. Measured: `.ui-affix-content` at
|
|
1667
|
+
* `inset-block-start: 0` and `64px`, `z-index: 20`, over a header at z-index 0.
|
|
1668
|
+
*
|
|
1669
|
+
* That is the components doing exactly what they were told, which is why it is the
|
|
1670
|
+
* PAGE that is wrong: a demo of a pinning component has no business pinning to the
|
|
1671
|
+
* document it is being demonstrated in. Each gets its own bounded scroller, which is
|
|
1672
|
+
* also the only way to SHOW the behaviour — a bar pinned to a viewport you are not
|
|
1673
|
+
* scrolling never moves, so the old version demonstrated nothing while breaking the
|
|
1674
|
+
* page it sat on.
|
|
1675
|
+
*/}
|
|
1676
|
+
<div
|
|
1677
|
+
ref={affixScrollerRef}
|
|
1678
|
+
className="ui-scroll-area"
|
|
1679
|
+
style={{ blockSize: "12rem", overflowY: "auto" }}
|
|
1680
|
+
>
|
|
1681
|
+
<div style={{ blockSize: "30rem" }}>
|
|
1682
|
+
<Affix offsetBlockStart={8} target={() => affixScrollerRef.current}>
|
|
1683
|
+
<Flex align="center" justify="between" gap="sm">
|
|
1684
|
+
<Text weight="medium">{t("themeLab.label.pinned")}</Text>
|
|
1685
|
+
<Button size="sm">{t("themeLab.action.save")}</Button>
|
|
1686
|
+
</Flex>
|
|
1687
|
+
</Affix>
|
|
1688
|
+
</div>
|
|
1689
|
+
</div>
|
|
1690
|
+
</Section>
|
|
1691
|
+
|
|
1692
|
+
{/* ── 9 · feedback ─────────────────────────────────────────────────────────── */}
|
|
1693
|
+
<Section id="feedback" title={t("themeLab.section.feedback")}>
|
|
1694
|
+
<Alert>
|
|
1695
|
+
<AlertTitle>{t("themeLab.tone.neutral")}</AlertTitle>
|
|
1696
|
+
<AlertDescription>{t("themeLab.sample.long")}</AlertDescription>
|
|
1697
|
+
</Alert>
|
|
1698
|
+
<Alert tone="info">
|
|
1699
|
+
<AlertTitle>{t("themeLab.tone.info")}</AlertTitle>
|
|
1700
|
+
<AlertDescription>{t("themeLab.sample.short")}</AlertDescription>
|
|
1701
|
+
<AlertActions>
|
|
1702
|
+
<Button size="xs" variant="outline">
|
|
1703
|
+
{t("themeLab.action.more")}
|
|
1704
|
+
</Button>
|
|
1705
|
+
</AlertActions>
|
|
1706
|
+
</Alert>
|
|
1707
|
+
<Alert tone="success">
|
|
1708
|
+
<AlertTitle>{t("themeLab.tone.success")}</AlertTitle>
|
|
1709
|
+
<AlertDescription>{t("themeLab.sample.short")}</AlertDescription>
|
|
1710
|
+
</Alert>
|
|
1711
|
+
<Alert tone="warning">
|
|
1712
|
+
<AlertTitle>{t("themeLab.tone.warning")}</AlertTitle>
|
|
1713
|
+
<AlertDescription>{t("themeLab.sample.short")}</AlertDescription>
|
|
1714
|
+
</Alert>
|
|
1715
|
+
<Alert tone="destructive">
|
|
1716
|
+
<AlertTitle>{t("themeLab.tone.destructive")}</AlertTitle>
|
|
1717
|
+
<AlertDescription>{t("themeLab.sample.short")}</AlertDescription>
|
|
1718
|
+
</Alert>
|
|
1719
|
+
<Banner tone="warning">
|
|
1720
|
+
<Banner.Content>
|
|
1721
|
+
<Banner.Title>{t("themeLab.tone.warning")}</Banner.Title>
|
|
1722
|
+
<Banner.Description>{t("themeLab.sample.short")}</Banner.Description>
|
|
1723
|
+
</Banner.Content>
|
|
1724
|
+
</Banner>
|
|
1725
|
+
<Callout kind="warning">
|
|
1726
|
+
<Callout.Title>{t("themeLab.tone.warning")}</Callout.Title>
|
|
1727
|
+
<Callout.Description>{t("themeLab.sample.long")}</Callout.Description>
|
|
1728
|
+
</Callout>
|
|
1729
|
+
<EmptyState
|
|
1730
|
+
title={t("themeLab.label.noResults")}
|
|
1731
|
+
description={t("themeLab.sample.short")}
|
|
1732
|
+
/>
|
|
1733
|
+
<ErrorSurface
|
|
1734
|
+
mode="application"
|
|
1735
|
+
status={403}
|
|
1736
|
+
title={t("themeLab.error.title")}
|
|
1737
|
+
description={t("themeLab.sample.long")}
|
|
1738
|
+
action={<Button>{t("themeLab.action.retry")}</Button>}
|
|
1739
|
+
/>
|
|
1740
|
+
<Row2 label={t("themeLab.label.skeletons")}>
|
|
1741
|
+
<Skeleton />
|
|
1742
|
+
<SkeletonAvatar size="lg" />
|
|
1743
|
+
<SkeletonButton size="sm" />
|
|
1744
|
+
<SkeletonInput />
|
|
1745
|
+
<SkeletonNode />
|
|
1746
|
+
<SkeletonImage active />
|
|
1747
|
+
</Row2>
|
|
1748
|
+
<SkeletonArticle avatar active paragraph={{ rows: 2 }} />
|
|
1749
|
+
<SkeletonForm columns={2} fields={4} />
|
|
1750
|
+
</Section>
|
|
1751
|
+
|
|
1752
|
+
{/* ── 10 · overlays. OPENED by the instrument, not merely present. ──────────── */}
|
|
1753
|
+
<Section
|
|
1754
|
+
id="overlays"
|
|
1755
|
+
title={t("themeLab.section.overlays")}
|
|
1756
|
+
note={t("themeLab.section.overlaysNote")}
|
|
1757
|
+
>
|
|
1758
|
+
<Flex direction="row" gap="sm" wrap align="center">
|
|
1759
|
+
<Button data-probe="dialog" onClick={() => setDialogOpen(true)}>
|
|
1760
|
+
{t("themeLab.overlay.dialog")}
|
|
1761
|
+
</Button>
|
|
1762
|
+
<Button data-probe="sheet" variant="outline" onClick={() => setSheetOpen(true)}>
|
|
1763
|
+
{t("themeLab.overlay.sheet")}
|
|
1764
|
+
</Button>
|
|
1765
|
+
<Button
|
|
1766
|
+
data-probe="alert-dialog"
|
|
1767
|
+
variant="destructive"
|
|
1768
|
+
onClick={() => setAlertOpen(true)}
|
|
1769
|
+
>
|
|
1770
|
+
{t("themeLab.overlay.alertDialog")}
|
|
1771
|
+
</Button>
|
|
1772
|
+
<Tooltip>
|
|
1773
|
+
<TooltipTrigger asChild>
|
|
1774
|
+
<Button data-probe="tooltip" variant="outline">
|
|
1775
|
+
{t("themeLab.overlay.tooltip")}
|
|
1776
|
+
</Button>
|
|
1777
|
+
</TooltipTrigger>
|
|
1778
|
+
<TooltipContent>{t("themeLab.sample.short")}</TooltipContent>
|
|
1779
|
+
</Tooltip>
|
|
1780
|
+
<Button
|
|
1781
|
+
data-probe="toast"
|
|
1782
|
+
variant="secondary"
|
|
1783
|
+
onClick={() => toast(t("themeLab.overlay.toastMessage"))}
|
|
1784
|
+
>
|
|
1785
|
+
{t("themeLab.overlay.toast")}
|
|
1786
|
+
</Button>
|
|
1787
|
+
<Button
|
|
1788
|
+
data-probe="two-factor"
|
|
1789
|
+
variant="outline"
|
|
1790
|
+
onClick={() => setTwoFactorOpen(true)}
|
|
1791
|
+
>
|
|
1792
|
+
{t("themeLab.overlay.twoFactor")}
|
|
1793
|
+
</Button>
|
|
1794
|
+
<CommandPalette
|
|
1795
|
+
groups={[
|
|
1796
|
+
{
|
|
1797
|
+
id: "pages",
|
|
1798
|
+
label: t("themeLab.nav.section"),
|
|
1799
|
+
items: [
|
|
1800
|
+
{ id: "overview", label: t("themeLab.nav.overview") },
|
|
1801
|
+
{ id: "reports", label: t("themeLab.nav.reports") },
|
|
1802
|
+
],
|
|
1803
|
+
},
|
|
1804
|
+
]}
|
|
1805
|
+
labels={{
|
|
1806
|
+
open: t("themeLab.label.quickPick"),
|
|
1807
|
+
title: t("themeLab.label.quickPick"),
|
|
1808
|
+
description: t("themeLab.sample.short"),
|
|
1809
|
+
placeholder: t("themeLab.field.searchPlaceholder"),
|
|
1810
|
+
empty: t("themeLab.label.noResults"),
|
|
1811
|
+
}}
|
|
1812
|
+
onSelect={() => {}}
|
|
1813
|
+
/>
|
|
1814
|
+
</Flex>
|
|
1815
|
+
<Flex direction="col" gap="sm" align="start">
|
|
1816
|
+
<Text size="xs" tone="muted">
|
|
1817
|
+
{t("themeLab.overlay.inlineNote")}
|
|
1818
|
+
</Text>
|
|
1819
|
+
{/* `id`, not `data-probe`: Select is a closed component and does not spread
|
|
1820
|
+
arbitrary `data-*` onto its trigger — measured, the attribute never reached the
|
|
1821
|
+
DOM. The instrument addresses this one by id. */}
|
|
1822
|
+
<Select
|
|
1823
|
+
id="overlay-select"
|
|
1824
|
+
aria-label={t("themeLab.field.role")}
|
|
1825
|
+
value={role}
|
|
1826
|
+
onValueChange={setRole}
|
|
1827
|
+
options={[
|
|
1828
|
+
{ value: "viewer", label: t("themeLab.role.viewer") },
|
|
1829
|
+
{ value: "editor", label: t("themeLab.role.editor") },
|
|
1830
|
+
{ value: "admin", label: t("themeLab.role.admin") },
|
|
1831
|
+
]}
|
|
1832
|
+
/>
|
|
1833
|
+
</Flex>
|
|
1834
|
+
</Section>
|
|
1835
|
+
|
|
1836
|
+
{/* ── 11 · regions and shells that nest ────────────────────────────────────── */}
|
|
1837
|
+
<Section id="regions" title={t("themeLab.section.regions")}>
|
|
1838
|
+
<SplitPane
|
|
1839
|
+
asideLabel={t("themeLab.label.detail")}
|
|
1840
|
+
aside={<Text size="sm">{t("themeLab.sample.short")}</Text>}
|
|
1841
|
+
>
|
|
1842
|
+
<Text size="sm">{t("themeLab.sample.long")}</Text>
|
|
1843
|
+
</SplitPane>
|
|
1844
|
+
<MasterDetail
|
|
1845
|
+
masterLabel={t("themeLab.label.master")}
|
|
1846
|
+
detailLabel={t("themeLab.label.detail")}
|
|
1847
|
+
master={<Text size="sm">{t("themeLab.sample.alpha")}</Text>}
|
|
1848
|
+
>
|
|
1849
|
+
<Text size="sm">{t("themeLab.sample.beta")}</Text>
|
|
1850
|
+
</MasterDetail>
|
|
1851
|
+
<ResizablePanelGroup orientation="horizontal">
|
|
1852
|
+
<ResizablePanel id="list" defaultSize="40%" minSize="20%">
|
|
1853
|
+
<Text size="sm">{t("themeLab.sample.alpha")}</Text>
|
|
1854
|
+
</ResizablePanel>
|
|
1855
|
+
<ResizableHandle />
|
|
1856
|
+
<ResizablePanel id="detail" defaultSize="60%" minSize="30%">
|
|
1857
|
+
<Text size="sm">{t("themeLab.sample.beta")}</Text>
|
|
1858
|
+
</ResizablePanel>
|
|
1859
|
+
</ResizablePanelGroup>
|
|
1860
|
+
<ResponsiveGrid columns={{ sm: 1, md: 2, lg: 3 }}>
|
|
1861
|
+
<ServiceLauncherCard
|
|
1862
|
+
icon={Clock}
|
|
1863
|
+
title={t("themeLab.sample.alpha")}
|
|
1864
|
+
statusLabel={t("themeLab.tone.success")}
|
|
1865
|
+
statusTone="success"
|
|
1866
|
+
description={t("themeLab.sample.short")}
|
|
1867
|
+
action={<Button>{t("themeLab.action.open")}</Button>}
|
|
1868
|
+
/>
|
|
1869
|
+
<ServiceLauncherCard
|
|
1870
|
+
icon={Building2}
|
|
1871
|
+
title={t("themeLab.sample.beta")}
|
|
1872
|
+
statusLabel={t("themeLab.tone.warning")}
|
|
1873
|
+
statusTone="warning"
|
|
1874
|
+
description={t("themeLab.sample.short")}
|
|
1875
|
+
disabledReason={t("themeLab.sample.short")}
|
|
1876
|
+
action={<Button disabled>{t("themeLab.action.open")}</Button>}
|
|
1877
|
+
/>
|
|
1878
|
+
<ServiceCatalogCta
|
|
1879
|
+
title={t("themeLab.label.catalog")}
|
|
1880
|
+
action={<Button variant="outline">{t("themeLab.action.more")}</Button>}
|
|
1881
|
+
/>
|
|
1882
|
+
</ResponsiveGrid>
|
|
1883
|
+
<ServiceRolePanel
|
|
1884
|
+
roles={[
|
|
1885
|
+
{ id: "admin", name: t("themeLab.role.admin") },
|
|
1886
|
+
{ id: "viewer", name: t("themeLab.role.viewer") },
|
|
1887
|
+
]}
|
|
1888
|
+
value={selectedRole}
|
|
1889
|
+
onValueChange={setSelectedRole}
|
|
1890
|
+
>
|
|
1891
|
+
{(selected) => (
|
|
1892
|
+
<Text size="sm">{selected ? selected.name : t("themeLab.label.noResults")}</Text>
|
|
1893
|
+
)}
|
|
1894
|
+
</ServiceRolePanel>
|
|
1895
|
+
<OrgSwitcher
|
|
1896
|
+
organizations={[
|
|
1897
|
+
{ id: "dxs", name: t("themeLab.sample.alpha"), meta: t("themeLab.role.admin") },
|
|
1898
|
+
{ id: "acme", name: t("themeLab.sample.beta"), meta: t("themeLab.role.viewer") },
|
|
1899
|
+
]}
|
|
1900
|
+
value={org}
|
|
1901
|
+
onValueChange={setOrg}
|
|
1902
|
+
labels={{
|
|
1903
|
+
trigger: (name) => `${t("themeLab.org.trigger")}: ${name}`,
|
|
1904
|
+
title: t("themeLab.org.trigger"),
|
|
1905
|
+
search: t("themeLab.field.searchPlaceholder"),
|
|
1906
|
+
empty: t("themeLab.label.noResults"),
|
|
1907
|
+
loading: t("themeLab.label.loading"),
|
|
1908
|
+
}}
|
|
1909
|
+
/>
|
|
1910
|
+
<AuthStack>
|
|
1911
|
+
<AuthIdentity
|
|
1912
|
+
title={t("themeLab.auth.title")}
|
|
1913
|
+
requester={t("themeLab.auth.requester")}
|
|
1914
|
+
/>
|
|
1915
|
+
<AuthAccountSummary
|
|
1916
|
+
email="duong@example.test"
|
|
1917
|
+
actionLabel={t("themeLab.auth.switch")}
|
|
1918
|
+
onAction={() => {}}
|
|
1919
|
+
/>
|
|
1920
|
+
<AuthDivider label={t("themeLab.auth.or")} />
|
|
1921
|
+
<AuthFooter
|
|
1922
|
+
product={t("themeLab.product")}
|
|
1923
|
+
terms={t("themeLab.auth.terms")}
|
|
1924
|
+
privacy={t("themeLab.auth.privacy")}
|
|
1925
|
+
/>
|
|
1926
|
+
</AuthStack>
|
|
1927
|
+
</Section>
|
|
1928
|
+
|
|
1929
|
+
{/* ── 12 · conversational ──────────────────────────────────────────────────── */}
|
|
1930
|
+
<Section id="chat" title={t("themeLab.section.chat")}>
|
|
1931
|
+
<Welcome
|
|
1932
|
+
icon={<Bot aria-hidden="true" />}
|
|
1933
|
+
title={t("themeLab.chat.welcomeTitle")}
|
|
1934
|
+
description={t("themeLab.chat.welcomeBody")}
|
|
1935
|
+
/>
|
|
1936
|
+
<ChatBubbleList
|
|
1937
|
+
label={t("themeLab.section.chat")}
|
|
1938
|
+
items={chat}
|
|
1939
|
+
roles={{
|
|
1940
|
+
assistant: { placement: "start", variant: "filled" },
|
|
1941
|
+
user: { placement: "end", variant: "outlined" },
|
|
1942
|
+
}}
|
|
1943
|
+
/>
|
|
1944
|
+
<ChatBubble placement="start" header={t("themeLab.chat.assistantName")} loading />
|
|
1945
|
+
<ChatBubble placement="end" tone="destructive" header={t("themeLab.chat.userName")}>
|
|
1946
|
+
{t("themeLab.sample.short")}
|
|
1947
|
+
</ChatBubble>
|
|
1948
|
+
<ThoughtChain
|
|
1949
|
+
label={t("themeLab.chat.thoughts")}
|
|
1950
|
+
defaultExpandedKeys={["search"]}
|
|
1951
|
+
items={[
|
|
1952
|
+
{ key: "read", title: t("themeLab.sample.alpha"), status: "success" },
|
|
1953
|
+
{
|
|
1954
|
+
key: "search",
|
|
1955
|
+
title: t("themeLab.sample.beta"),
|
|
1956
|
+
status: "loading",
|
|
1957
|
+
collapsible: true,
|
|
1958
|
+
},
|
|
1959
|
+
{ key: "write", title: t("themeLab.sample.gamma"), status: "abort" },
|
|
1960
|
+
]}
|
|
1961
|
+
/>
|
|
1962
|
+
<Conversations
|
|
1963
|
+
activeKey={conversation}
|
|
1964
|
+
onActiveChange={setConversation}
|
|
1965
|
+
items={[
|
|
1966
|
+
{ key: "c1", label: t("themeLab.sample.alpha") },
|
|
1967
|
+
{ key: "c2", label: t("themeLab.sample.beta") },
|
|
1968
|
+
]}
|
|
1969
|
+
/>
|
|
1970
|
+
<ChatSuggestion
|
|
1971
|
+
items={[
|
|
1972
|
+
{ value: "summarize", label: t("themeLab.sample.gamma") },
|
|
1973
|
+
{ value: "translate", label: t("themeLab.sample.delta") },
|
|
1974
|
+
]}
|
|
1975
|
+
>
|
|
1976
|
+
{({ onTrigger, onKeyDown }) => (
|
|
1977
|
+
<ChatComposer
|
|
1978
|
+
value={draft}
|
|
1979
|
+
onValueChange={(next) => {
|
|
1980
|
+
setDraft(next);
|
|
1981
|
+
onTrigger(next);
|
|
1982
|
+
}}
|
|
1983
|
+
onKeyDown={onKeyDown}
|
|
1984
|
+
onSubmit={() => setDraft("")}
|
|
1985
|
+
placeholder={t("themeLab.chat.placeholder")}
|
|
1986
|
+
/>
|
|
1987
|
+
)}
|
|
1988
|
+
</ChatSuggestion>
|
|
1989
|
+
</Section>
|
|
1990
|
+
|
|
1991
|
+
{/* ── 13 · what could NOT be rendered, and why ──────────────────────────────── */}
|
|
1992
|
+
<Section
|
|
1993
|
+
id="absent"
|
|
1994
|
+
title={t("themeLab.section.absent")}
|
|
1995
|
+
note={t("themeLab.section.absentNote")}
|
|
1996
|
+
>
|
|
1997
|
+
<Table>
|
|
1998
|
+
<TableHeader>
|
|
1999
|
+
<TableRow>
|
|
2000
|
+
<TableHead>{t("themeLab.absent.export")}</TableHead>
|
|
2001
|
+
<TableHead>{t("themeLab.absent.reason")}</TableHead>
|
|
2002
|
+
</TableRow>
|
|
2003
|
+
</TableHeader>
|
|
2004
|
+
<TableBody>
|
|
2005
|
+
{ABSENT.map((entry) => (
|
|
2006
|
+
<TableRow key={`${entry.name}-${entry.reasonKey}`}>
|
|
2007
|
+
<TableCell>
|
|
2008
|
+
<Text as="span" size="sm" mono>
|
|
2009
|
+
{entry.name}
|
|
2010
|
+
</Text>
|
|
2011
|
+
</TableCell>
|
|
2012
|
+
<TableCell>
|
|
2013
|
+
<Text as="span" size="sm">
|
|
2014
|
+
{t(entry.reasonKey)}
|
|
2015
|
+
</Text>
|
|
2016
|
+
</TableCell>
|
|
2017
|
+
</TableRow>
|
|
2018
|
+
))}
|
|
2019
|
+
</TableBody>
|
|
2020
|
+
</Table>
|
|
2021
|
+
</Section>
|
|
2022
|
+
|
|
2023
|
+
<Reveal>
|
|
2024
|
+
<Text size="xs" tone="muted">
|
|
2025
|
+
{t("themeLab.page.footer")}
|
|
2026
|
+
</Text>
|
|
2027
|
+
</Reveal>
|
|
2028
|
+
</Flex>
|
|
2029
|
+
</PageContainer>
|
|
2030
|
+
</AppShell>
|
|
2031
|
+
|
|
2032
|
+
<Dialog open={dialogOpen} onOpenChange={setDialogOpen}>
|
|
2033
|
+
<DialogContent>
|
|
2034
|
+
<DialogHeader>
|
|
2035
|
+
<DialogTitle>{t("themeLab.overlay.dialog")}</DialogTitle>
|
|
2036
|
+
<DialogDescription>{t("themeLab.sample.short")}</DialogDescription>
|
|
2037
|
+
</DialogHeader>
|
|
2038
|
+
<DialogBody>
|
|
2039
|
+
<Text as="p" size="sm">
|
|
2040
|
+
{t("themeLab.sample.long")}
|
|
2041
|
+
</Text>
|
|
2042
|
+
</DialogBody>
|
|
2043
|
+
<DialogFooter>
|
|
2044
|
+
<DialogClose asChild>
|
|
2045
|
+
<Button variant="outline">{t("themeLab.action.cancel")}</Button>
|
|
2046
|
+
</DialogClose>
|
|
2047
|
+
<Button onClick={() => setDialogOpen(false)}>{t("themeLab.action.save")}</Button>
|
|
2048
|
+
</DialogFooter>
|
|
2049
|
+
</DialogContent>
|
|
2050
|
+
</Dialog>
|
|
2051
|
+
|
|
2052
|
+
<Sheet open={sheetOpen} onOpenChange={setSheetOpen}>
|
|
2053
|
+
<SheetContent>
|
|
2054
|
+
<SheetHeader>
|
|
2055
|
+
<SheetTitle>{t("themeLab.overlay.sheet")}</SheetTitle>
|
|
2056
|
+
<SheetDescription>{t("themeLab.sample.short")}</SheetDescription>
|
|
2057
|
+
</SheetHeader>
|
|
2058
|
+
<SheetBody>
|
|
2059
|
+
<Text as="p" size="sm">
|
|
2060
|
+
{t("themeLab.sample.long")}
|
|
2061
|
+
</Text>
|
|
2062
|
+
</SheetBody>
|
|
2063
|
+
<SheetFooter>
|
|
2064
|
+
<SheetClose asChild>
|
|
2065
|
+
<Button variant="outline">{t("themeLab.action.cancel")}</Button>
|
|
2066
|
+
</SheetClose>
|
|
2067
|
+
</SheetFooter>
|
|
2068
|
+
</SheetContent>
|
|
2069
|
+
</Sheet>
|
|
2070
|
+
|
|
2071
|
+
<AlertDialog
|
|
2072
|
+
open={alertOpen}
|
|
2073
|
+
onOpenChange={setAlertOpen}
|
|
2074
|
+
title={t("themeLab.overlay.alertDialog")}
|
|
2075
|
+
description={t("themeLab.sample.long")}
|
|
2076
|
+
confirmLabel={t("themeLab.action.save")}
|
|
2077
|
+
cancelLabel={t("themeLab.action.cancel")}
|
|
2078
|
+
onConfirm={() => setAlertOpen(false)}
|
|
2079
|
+
/>
|
|
2080
|
+
|
|
2081
|
+
<TwoFactorSetup
|
|
2082
|
+
open={twoFactorOpen}
|
|
2083
|
+
onOpenChange={setTwoFactorOpen}
|
|
2084
|
+
qrValue="otpauth://totp/theme-lab?secret=JBSWY3DPEHPK3PXP"
|
|
2085
|
+
manualKey="JBSWY3DPEHPK3PXP"
|
|
2086
|
+
code={otp}
|
|
2087
|
+
onCodeChange={setOtp}
|
|
2088
|
+
onConfirm={() => setTwoFactorOpen(false)}
|
|
2089
|
+
onAcknowledge={() => setTwoFactorOpen(false)}
|
|
2090
|
+
labels={{
|
|
2091
|
+
title: t("themeLab.twoFactor.title"),
|
|
2092
|
+
description: t("themeLab.twoFactor.description"),
|
|
2093
|
+
qrLabel: t("themeLab.label.qr"),
|
|
2094
|
+
manualKeyLabel: t("themeLab.twoFactor.manualKey"),
|
|
2095
|
+
codeLabel: t("themeLab.twoFactor.code"),
|
|
2096
|
+
recoveryNotice: t("themeLab.twoFactor.recovery"),
|
|
2097
|
+
cancel: t("themeLab.action.cancel"),
|
|
2098
|
+
confirm: t("themeLab.action.save"),
|
|
2099
|
+
acknowledge: t("themeLab.twoFactor.acknowledge"),
|
|
2100
|
+
}}
|
|
2101
|
+
/>
|
|
2102
|
+
|
|
2103
|
+
<DraggablePanel
|
|
2104
|
+
title={t("themeLab.label.assistant")}
|
|
2105
|
+
placement="bottom-end"
|
|
2106
|
+
position={panelPosition}
|
|
2107
|
+
onPositionChange={setPanelPosition}
|
|
2108
|
+
>
|
|
2109
|
+
<Text size="sm">{t("themeLab.sample.short")}</Text>
|
|
2110
|
+
</DraggablePanel>
|
|
2111
|
+
|
|
2112
|
+
<FloatButton.Group
|
|
2113
|
+
trigger="click"
|
|
2114
|
+
icon={<ActivityIcon />}
|
|
2115
|
+
aria-label={t("themeLab.label.assistant")}
|
|
2116
|
+
>
|
|
2117
|
+
<FloatButton tooltip={t("themeLab.action.share")} icon={<Share2 />} />
|
|
2118
|
+
<FloatButton tooltip={t("themeLab.action.more")} icon={<FileText />} />
|
|
2119
|
+
<FloatButton.BackTop />
|
|
2120
|
+
</FloatButton.Group>
|
|
2121
|
+
|
|
2122
|
+
<Toaster />
|
|
2123
|
+
</ThemeScope>
|
|
2124
|
+
);
|
|
2125
|
+
}
|