@godxjp/ui 5.0.1 → 6.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/README.md +101 -142
- package/package.json +124 -128
- package/scripts/ui-audit.mjs +179 -0
- package/src/app/__tests__/app-provider.test.tsx +232 -0
- package/src/app/__tests__/date-format-labels.test.ts +36 -0
- package/src/app/__tests__/date-formats.test.ts +44 -0
- package/src/app/__tests__/timezones.test.ts +65 -0
- package/src/app/app-provider.tsx +227 -0
- package/src/app/date-format-labels.ts +21 -0
- package/src/app/date-formats.ts +30 -0
- package/src/app/index.ts +40 -0
- package/src/app/locales.ts +32 -0
- package/src/app/request-headers.ts +31 -0
- package/src/app/storage.ts +44 -0
- package/src/app/time-format-labels.ts +19 -0
- package/src/app/time-formats.ts +15 -0
- package/src/app/timezones.ts +208 -0
- package/src/app/types.ts +39 -0
- package/src/app/use-formatting.ts +47 -0
- package/src/components/__tests__/accessibility-primitives.test.tsx +65 -0
- package/src/components/__tests__/docs-parity.test.ts +41 -0
- package/src/components/__tests__/shadcn-release-guardrails.test.ts +71 -0
- package/src/components/__tests__/theme-axes-integration.test.tsx +242 -0
- package/src/components/admin/index.ts +76 -0
- package/src/components/data-display/__tests__/card-table.test.tsx +328 -0
- package/src/components/data-display/__tests__/data-display.test.tsx +73 -0
- package/src/components/data-display/__tests__/data-table.test.tsx +84 -0
- package/src/components/data-display/__tests__/popover.test.tsx +92 -0
- package/src/components/data-display/__tests__/scroll-area-collapsible.test.tsx +66 -0
- package/src/components/data-display/badge.tsx +27 -0
- package/src/components/data-display/card.tsx +194 -0
- package/src/components/data-display/code-badge.tsx +28 -0
- package/src/components/data-display/collapsible.tsx +5 -0
- package/src/components/data-display/data-table.tsx +476 -0
- package/src/components/data-display/empty-state.tsx +22 -0
- package/src/components/data-display/index.ts +41 -0
- package/src/components/data-display/key-value-grid.tsx +46 -0
- package/src/components/data-display/popover.tsx +62 -0
- package/src/components/data-display/progress-meter.tsx +20 -0
- package/src/components/data-display/scan-panel.tsx +16 -0
- package/src/components/data-display/scroll-area.tsx +42 -0
- package/src/components/data-display/status-badge.tsx +83 -0
- package/src/components/data-display/table.tsx +59 -0
- package/src/components/data-display/timeline.tsx +42 -0
- package/src/components/data-display/tree-list.tsx +42 -0
- package/src/components/data-entry/__fixtures__/tree-options.ts +80 -0
- package/src/components/data-entry/__tests__/cascader-tree-transfer.test.tsx +417 -0
- package/src/components/data-entry/__tests__/checkbox-group.test.tsx +40 -0
- package/src/components/data-entry/__tests__/checkbox.test.tsx +20 -0
- package/src/components/data-entry/__tests__/date-autocomplete.test.tsx +94 -0
- package/src/components/data-entry/__tests__/form-field.test.tsx +49 -0
- package/src/components/data-entry/__tests__/input-textarea.test.tsx +38 -0
- package/src/components/data-entry/__tests__/label-select.test.tsx +62 -0
- package/src/components/data-entry/__tests__/pickers.test.tsx +74 -0
- package/src/components/data-entry/__tests__/radio.test.tsx +46 -0
- package/src/components/data-entry/__tests__/search-input.test.tsx +32 -0
- package/src/components/data-entry/__tests__/switch-field.test.tsx +52 -0
- package/src/components/data-entry/__tests__/upload.test.tsx +125 -0
- package/src/components/data-entry/autocomplete.tsx +91 -0
- package/src/components/data-entry/calendar.tsx +90 -0
- package/src/components/data-entry/cascader.tsx +305 -0
- package/src/components/data-entry/checkbox-group.tsx +90 -0
- package/src/components/data-entry/checkbox.tsx +30 -0
- package/src/components/data-entry/choice-field.tsx +27 -0
- package/src/components/data-entry/choice-option.ts +20 -0
- package/src/components/data-entry/color-picker.tsx +75 -0
- package/src/components/data-entry/command.tsx +56 -0
- package/src/components/data-entry/country-select.tsx +88 -0
- package/src/components/data-entry/date-picker.tsx +69 -0
- package/src/components/data-entry/date-range-picker.tsx +75 -0
- package/src/components/data-entry/form-field.tsx +59 -0
- package/src/components/data-entry/index.ts +62 -0
- package/src/components/data-entry/input.tsx +26 -0
- package/src/components/data-entry/label.tsx +25 -0
- package/src/components/data-entry/radio.tsx +109 -0
- package/src/components/data-entry/search-input.tsx +103 -0
- package/src/components/data-entry/select.tsx +149 -0
- package/src/components/data-entry/slider.tsx +38 -0
- package/src/components/data-entry/switch-field.tsx +91 -0
- package/src/components/data-entry/switch.tsx +24 -0
- package/src/components/data-entry/textarea.tsx +12 -0
- package/src/components/data-entry/time-picker.tsx +214 -0
- package/src/components/data-entry/transfer.tsx +231 -0
- package/src/components/data-entry/tree-select-strategy.ts +6 -0
- package/src/components/data-entry/tree-select.tsx +279 -0
- package/src/components/data-entry/tree-utils.ts +221 -0
- package/src/components/data-entry/upload-crop-dialog.tsx +109 -0
- package/src/components/data-entry/upload-types.ts +86 -0
- package/src/components/data-entry/upload.tsx +498 -0
- package/src/components/data-entry/use-upload-draft.ts +93 -0
- package/src/components/feedback/__tests__/alert.test.tsx +127 -0
- package/src/components/feedback/__tests__/dialog.test.tsx +290 -0
- package/src/components/feedback/__tests__/sheet.test.tsx +94 -0
- package/src/components/feedback/__tests__/skeleton.test.tsx +25 -0
- package/src/components/feedback/__tests__/toast.test.tsx +52 -0
- package/src/components/feedback/alert.tsx +167 -0
- package/src/components/feedback/dialog.tsx +325 -0
- package/src/components/feedback/index.ts +53 -0
- package/src/components/feedback/sheet.tsx +130 -0
- package/src/components/feedback/skeleton.tsx +95 -0
- package/src/components/feedback/sonner.tsx +54 -0
- package/src/components/feedback/toaster.tsx +1 -0
- package/src/components/feedback/use-toast.ts +62 -0
- package/src/components/general/__tests__/button.test.tsx +71 -0
- package/src/components/general/button.tsx +61 -0
- package/src/components/general/index.ts +2 -0
- package/src/components/layout/__tests__/page-container.test.tsx +69 -0
- package/src/components/layout/__tests__/page-inset.test.tsx +14 -0
- package/src/components/layout/__tests__/stack-inline.test.tsx +39 -0
- package/src/components/layout/app-shell.tsx +42 -0
- package/src/components/layout/breadcrumb.tsx +35 -0
- package/src/components/layout/index.ts +31 -0
- package/src/components/layout/inline.tsx +13 -0
- package/src/components/layout/menu.tsx +34 -0
- package/src/components/layout/mobile-frame.tsx +57 -0
- package/src/components/layout/page-container.tsx +81 -0
- package/src/components/layout/page-inset.tsx +16 -0
- package/src/components/layout/responsive-grid.tsx +14 -0
- package/src/components/layout/shell-app.tsx +30 -0
- package/src/components/layout/sidebar.tsx +98 -0
- package/src/components/layout/split-pane.tsx +16 -0
- package/src/components/layout/stack.tsx +13 -0
- package/src/components/layout/topbar.tsx +108 -0
- package/src/components/navigation/__tests__/app-pickers.test.tsx +118 -0
- package/src/components/navigation/__tests__/dropdown-menu.test.tsx +104 -0
- package/src/components/navigation/__tests__/navigation.test.tsx +61 -0
- package/src/components/navigation/__tests__/pagination-steps-tabs.test.tsx +76 -0
- package/src/components/navigation/date-format-picker.tsx +55 -0
- package/src/components/navigation/dropdown-menu.tsx +190 -0
- package/src/components/navigation/filter-bar.tsx +38 -0
- package/src/components/navigation/index.ts +28 -0
- package/src/components/navigation/locale-picker.tsx +49 -0
- package/src/components/navigation/page-header.tsx +50 -0
- package/src/components/navigation/pagination-utils.ts +35 -0
- package/src/components/navigation/pagination.tsx +168 -0
- package/src/components/navigation/steps.tsx +163 -0
- package/src/components/navigation/tabs-items.tsx +69 -0
- package/src/components/navigation/tabs.tsx +67 -0
- package/src/components/navigation/time-format-picker.tsx +55 -0
- package/src/components/navigation/timezone-picker.tsx +63 -0
- package/src/components/query/__tests__/data-state.test.tsx +214 -0
- package/src/components/query/__tests__/infinite-prefetch.test.tsx +105 -0
- package/src/components/query/__tests__/query-helpers.test.tsx +61 -0
- package/src/components/query/data-state.tsx +58 -0
- package/src/components/query/index.ts +10 -0
- package/src/components/query/infinite-query-state.tsx +99 -0
- package/src/components/query/mutation-feedback.tsx +31 -0
- package/src/components/query/prefetch-link.tsx +45 -0
- package/src/components/query/query-refetch-button.tsx +41 -0
- package/src/components/ui/alert-dialog.tsx +1 -0
- package/src/components/ui/alert.tsx +1 -0
- package/src/components/ui/autocomplete.tsx +1 -0
- package/src/components/ui/badge.tsx +1 -0
- package/src/components/ui/button.tsx +1 -0
- package/src/components/ui/calendar.tsx +1 -0
- package/src/components/ui/card.tsx +1 -0
- package/src/components/ui/checkbox.tsx +1 -0
- package/src/components/ui/color-picker.tsx +1 -0
- package/src/components/ui/command.tsx +1 -0
- package/src/components/ui/date-picker.tsx +1 -0
- package/src/components/ui/date-range-picker.tsx +1 -0
- package/src/components/ui/dialog.tsx +1 -0
- package/src/components/ui/dropdown-menu.tsx +1 -0
- package/src/components/ui/index.tsx +31 -0
- package/src/components/ui/input.tsx +1 -0
- package/src/components/ui/label.tsx +1 -0
- package/src/components/ui/pagination.tsx +1 -0
- package/src/components/ui/popover.tsx +1 -0
- package/src/components/ui/radio.tsx +1 -0
- package/src/components/ui/scroll-area.tsx +1 -0
- package/src/components/ui/select.tsx +1 -0
- package/src/components/ui/sheet.tsx +1 -0
- package/src/components/ui/slider.tsx +1 -0
- package/src/components/ui/sonner.tsx +1 -0
- package/src/components/ui/switch.tsx +1 -0
- package/src/components/ui/table.tsx +1 -0
- package/src/components/ui/tabs-items.tsx +1 -0
- package/src/components/ui/tabs.tsx +1 -0
- package/src/components/ui/textarea.tsx +1 -0
- package/src/components/ui/time-picker.tsx +1 -0
- package/src/components/ui/upload.tsx +1 -0
- package/src/form/__tests__/use-zod-form.test.tsx +97 -0
- package/src/form/form-field-control.tsx +44 -0
- package/src/form/form-root.tsx +29 -0
- package/src/form/index.ts +7 -0
- package/src/form/use-zod-form.ts +29 -0
- package/src/i18n/__tests__/translate.test.ts +23 -0
- package/src/i18n/index.ts +9 -0
- package/src/i18n/messages/en.json +171 -0
- package/src/i18n/messages/ja.json +171 -0
- package/src/i18n/messages/vi.json +171 -0
- package/src/i18n/translate.ts +74 -0
- package/src/i18n/use-translation.ts +53 -0
- package/src/index.ts +3 -0
- package/src/lib/__tests__/control-styles.test.ts +78 -0
- package/src/lib/__tests__/datetime.test.ts +77 -0
- package/src/lib/__tests__/format-date.test.ts +97 -0
- package/src/lib/__tests__/format.test.ts +62 -0
- package/src/lib/__tests__/theme-tokens-audit.test.ts +176 -0
- package/src/lib/__tests__/theme-tokens-css.test.ts +118 -0
- package/src/lib/__tests__/token-governance.test.ts +191 -0
- package/src/lib/__tests__/variants.test.ts +18 -0
- package/src/lib/control-styles.ts +33 -0
- package/src/lib/datetime/detect.ts +25 -0
- package/src/lib/datetime/format-date.ts +100 -0
- package/src/lib/datetime/format.ts +140 -0
- package/src/lib/datetime/index.ts +25 -0
- package/src/lib/datetime/parse.ts +51 -0
- package/src/lib/datetime/sync.ts +48 -0
- package/src/lib/format.ts +114 -0
- package/src/lib/hooks.ts +54 -0
- package/src/lib/utils.ts +6 -0
- package/src/lib/variants.ts +40 -0
- package/src/props/components/app.prop.ts +99 -0
- package/src/props/components/data-display.prop.ts +73 -0
- package/src/props/components/data-entry.prop.ts +334 -0
- package/src/props/components/feedback.prop.ts +80 -0
- package/src/props/components/form.prop.ts +46 -0
- package/src/props/components/general.prop.ts +18 -0
- package/src/props/components/index.ts +99 -0
- package/src/props/components/layout.prop.ts +130 -0
- package/src/props/components/navigation.prop.ts +88 -0
- package/src/props/components/query.prop.ts +94 -0
- package/src/props/index.ts +17 -0
- package/src/props/registry.ts +603 -0
- package/src/props/vocabulary/content.prop.ts +35 -0
- package/src/props/vocabulary/data.prop.ts +46 -0
- package/src/props/vocabulary/index.ts +73 -0
- package/src/props/vocabulary/interaction.prop.ts +42 -0
- package/src/props/vocabulary/layout.prop.ts +25 -0
- package/src/props/vocabulary/navigation.prop.ts +19 -0
- package/src/props/vocabulary/shared.prop.ts +59 -0
- package/src/styles/alert-layout.css +191 -0
- package/src/styles/badge-layout.css +22 -0
- package/src/styles/card-layout.css +373 -0
- package/src/styles/control.css +504 -0
- package/src/styles/data-display-layout.css +246 -0
- package/src/styles/density.css +43 -0
- package/src/styles/dialog-layout.css +84 -0
- package/src/styles/index.css +105 -0
- package/src/styles/layout.css +479 -0
- package/src/styles/shell-layout.css +604 -0
- package/src/styles/table-layout.css +109 -0
- package/src/test/__tests__/render-loop-guard.test.tsx +38 -0
- package/src/test/jest-dom.d.ts +4 -0
- package/src/test/render-loop-guard.tsx +50 -0
- package/src/test/render.tsx +29 -0
- package/src/test/theme-globals.test.ts +77 -0
- package/src/test/theme-globals.ts +134 -0
- package/src/test/theme-test-utils.tsx +67 -0
- package/src/theme/example.service.css +37 -0
- package/src/tokens/base.css +13 -0
- package/src/tokens/foundation.css +151 -0
- package/src/tokens/primitives/badge.css +13 -0
- package/src/tokens/primitives/card.css +29 -0
- package/src/tokens/primitives/control.css +55 -0
- package/src/tokens/primitives/feedback.css +17 -0
- package/src/tokens/primitives/layout.css +20 -0
- package/src/tokens/primitives/navigation.css +13 -0
- package/src/tokens/primitives/table.css +10 -0
- package/BRAND.md +0 -296
- package/CHANGELOG.md +0 -650
- package/config/eslint.js +0 -54
- package/config/prettier.cjs +0 -20
- package/config/tsconfig.base.json +0 -22
- package/config/vitest.base.ts +0 -26
- package/dist/MiniMonth-YAmPGEpC.d.ts +0 -143
- package/dist/Table.types-BbsxoIYE.d.ts +0 -352
- package/dist/color-DO0qqUAb.d.ts +0 -38
- package/dist/components/composites.d.ts +0 -963
- package/dist/components/composites.js +0 -7340
- package/dist/components/composites.js.map +0 -1
- package/dist/components/primitives.d.ts +0 -2736
- package/dist/components/primitives.js +0 -7353
- package/dist/components/primitives.js.map +0 -1
- package/dist/components/shell.d.ts +0 -182
- package/dist/components/shell.js +0 -774
- package/dist/components/shell.js.map +0 -1
- package/dist/hooks.d.ts +0 -100
- package/dist/hooks.js +0 -558
- package/dist/hooks.js.map +0 -1
- package/dist/i18n.d.ts +0 -61
- package/dist/i18n.js +0 -860
- package/dist/i18n.js.map +0 -1
- package/dist/index.d.ts +0 -33
- package/dist/index.js +0 -13059
- package/dist/index.js.map +0 -1
- package/dist/padding-DY0JV5Ja.d.ts +0 -16
- package/dist/preferences.d.ts +0 -132
- package/dist/preferences.js +0 -262
- package/dist/preferences.js.map +0 -1
- package/dist/props.d.ts +0 -86
- package/dist/props.js +0 -16
- package/dist/props.js.map +0 -1
- package/dist/size-CQwNvOWd.d.ts +0 -19
- package/dist/types-LTj-2bl-.d.ts +0 -30
- package/dist/useTableViews-D5NIAJ7h.d.ts +0 -154
- package/src/tokens/tailwind.css +0 -158
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
// Skeleton family — shaped placeholders for loading states. Always pick the
|
|
2
|
+
// shape closest to the final layout; spinner-overlay is forbidden.
|
|
3
|
+
import * as React from "react";
|
|
4
|
+
|
|
5
|
+
import { cn } from "../../lib/utils";
|
|
6
|
+
import { tableCellPaddingClass, tableRowHeightClass } from "../../lib/control-styles";
|
|
7
|
+
|
|
8
|
+
function SkeletonBlock({ className, ...props }: React.HTMLAttributes<HTMLDivElement>) {
|
|
9
|
+
return (
|
|
10
|
+
<div
|
|
11
|
+
aria-busy="true"
|
|
12
|
+
aria-live="polite"
|
|
13
|
+
className={cn("ui-skeleton-block", className)}
|
|
14
|
+
{...props}
|
|
15
|
+
/>
|
|
16
|
+
);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
interface SkeletonRowsProps {
|
|
20
|
+
rows?: number;
|
|
21
|
+
columns?: number;
|
|
22
|
+
className?: string;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/** Skeleton for a flat list of rows (use inside a Card or section). */
|
|
26
|
+
export function SkeletonRows({ rows = 6, columns = 4, className }: SkeletonRowsProps) {
|
|
27
|
+
return (
|
|
28
|
+
<div className={cn("ui-skeleton-rows", className)} aria-busy="true">
|
|
29
|
+
{Array.from({ length: rows }).map((_, i) => (
|
|
30
|
+
<div key={i} className="ui-skeleton-row">
|
|
31
|
+
{Array.from({ length: columns }).map((_, j) => (
|
|
32
|
+
<SkeletonBlock
|
|
33
|
+
key={j}
|
|
34
|
+
className={cn("h-4", j === 0 ? "w-1/4" : j === columns - 1 ? "w-1/6" : "flex-1")}
|
|
35
|
+
/>
|
|
36
|
+
))}
|
|
37
|
+
</div>
|
|
38
|
+
))}
|
|
39
|
+
</div>
|
|
40
|
+
);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/** Skeleton matching the DataTable layout — header row + N body rows. */
|
|
44
|
+
export function SkeletonTable({ rows = 8, columns = 5 }: SkeletonRowsProps) {
|
|
45
|
+
return (
|
|
46
|
+
<div className="ui-skeleton-table" aria-busy="true">
|
|
47
|
+
<div className={cn("ui-skeleton-table-head", tableCellPaddingClass, tableRowHeightClass)}>
|
|
48
|
+
{Array.from({ length: columns }).map((_, j) => (
|
|
49
|
+
<SkeletonBlock key={j} className={cn("h-3", j === 0 ? "w-1/5" : "flex-1")} />
|
|
50
|
+
))}
|
|
51
|
+
</div>
|
|
52
|
+
<div className="ui-skeleton-table-body">
|
|
53
|
+
{Array.from({ length: rows }).map((_, i) => (
|
|
54
|
+
<div
|
|
55
|
+
key={i}
|
|
56
|
+
className={cn("ui-skeleton-table-row", tableCellPaddingClass, tableRowHeightClass)}
|
|
57
|
+
>
|
|
58
|
+
{Array.from({ length: columns }).map((_, j) => (
|
|
59
|
+
<SkeletonBlock key={j} className={cn("h-4", j === 0 ? "w-1/5" : "flex-1")} />
|
|
60
|
+
))}
|
|
61
|
+
</div>
|
|
62
|
+
))}
|
|
63
|
+
</div>
|
|
64
|
+
</div>
|
|
65
|
+
);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/** Skeleton matching a Card detail layout — title + 6 metadata rows. */
|
|
69
|
+
export function SkeletonDetail() {
|
|
70
|
+
return (
|
|
71
|
+
<div className="ui-skeleton-detail ui-skeleton-detail-stack" aria-busy="true">
|
|
72
|
+
<SkeletonBlock className="h-7 w-1/3" />
|
|
73
|
+
<SkeletonBlock className="h-4 w-1/2" />
|
|
74
|
+
<div className="ui-skeleton-detail-box ui-skeleton-detail-stack">
|
|
75
|
+
{Array.from({ length: 6 }).map((_, i) => (
|
|
76
|
+
<div key={i} className="ui-skeleton-detail-stack">
|
|
77
|
+
<SkeletonBlock className="h-3 w-24" />
|
|
78
|
+
<SkeletonBlock className="h-4 w-full max-w-md" />
|
|
79
|
+
</div>
|
|
80
|
+
))}
|
|
81
|
+
</div>
|
|
82
|
+
</div>
|
|
83
|
+
);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/** Skeleton matching a stat card / dashboard tile. */
|
|
87
|
+
export function SkeletonCard() {
|
|
88
|
+
return (
|
|
89
|
+
<div className="ui-skeleton-card" aria-busy="true">
|
|
90
|
+
<SkeletonBlock className="h-3 w-24" />
|
|
91
|
+
<SkeletonBlock className="h-[length:var(--control-height)] w-32" />
|
|
92
|
+
<SkeletonBlock className="h-3 w-20" />
|
|
93
|
+
</div>
|
|
94
|
+
);
|
|
95
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
// shadcn/ui Sonner — recommended toast (replaces deprecated Radix Toast).
|
|
2
|
+
// @see https://ui.shadcn.com/docs/components/sonner
|
|
3
|
+
import * as React from "react";
|
|
4
|
+
import { CheckCircle2, Info, Loader2, OctagonX, TriangleAlert } from "lucide-react";
|
|
5
|
+
import { Toaster as Sonner, type ToasterProps } from "sonner";
|
|
6
|
+
|
|
7
|
+
function useDocumentTheme(): ToasterProps["theme"] {
|
|
8
|
+
return React.useSyncExternalStore(
|
|
9
|
+
(onStoreChange) => {
|
|
10
|
+
if (typeof document === "undefined") return () => undefined;
|
|
11
|
+
const mq = window.matchMedia("(prefers-color-scheme: dark)");
|
|
12
|
+
mq.addEventListener("change", onStoreChange);
|
|
13
|
+
const obs = new MutationObserver(onStoreChange);
|
|
14
|
+
obs.observe(document.documentElement, { attributes: true, attributeFilter: ["class"] });
|
|
15
|
+
return () => {
|
|
16
|
+
mq.removeEventListener("change", onStoreChange);
|
|
17
|
+
obs.disconnect();
|
|
18
|
+
};
|
|
19
|
+
},
|
|
20
|
+
() => (document.documentElement.classList.contains("dark") ? "dark" : "light"),
|
|
21
|
+
() => "light",
|
|
22
|
+
);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function Toaster({ ...props }: ToasterProps) {
|
|
26
|
+
const theme = useDocumentTheme();
|
|
27
|
+
|
|
28
|
+
return (
|
|
29
|
+
<Sonner
|
|
30
|
+
theme={theme}
|
|
31
|
+
className="toaster group"
|
|
32
|
+
icons={{
|
|
33
|
+
success: <CheckCircle2 className="size-4" aria-hidden="true" />,
|
|
34
|
+
info: <Info className="size-4" aria-hidden="true" />,
|
|
35
|
+
warning: <TriangleAlert className="size-4" aria-hidden="true" />,
|
|
36
|
+
error: <OctagonX className="size-4" aria-hidden="true" />,
|
|
37
|
+
loading: <Loader2 className="size-4 animate-spin" aria-hidden="true" />,
|
|
38
|
+
}}
|
|
39
|
+
style={
|
|
40
|
+
{
|
|
41
|
+
"--normal-bg": "var(--popover)",
|
|
42
|
+
"--normal-text": "var(--popover-foreground)",
|
|
43
|
+
"--normal-border": "var(--border)",
|
|
44
|
+
"--border-radius": "var(--radius)",
|
|
45
|
+
} as React.CSSProperties
|
|
46
|
+
}
|
|
47
|
+
position="bottom-right"
|
|
48
|
+
mobileOffset={{ bottom: "16px", right: "16px" }}
|
|
49
|
+
{...props}
|
|
50
|
+
/>
|
|
51
|
+
);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export { Toaster };
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { Toaster } from "./sonner";
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
// Toast API — Sonner (shadcn recommended).
|
|
2
|
+
//
|
|
3
|
+
// `toast("msg")` / `toast.success("msg")` — Sonner native API (preferred).
|
|
4
|
+
// `toast({ title, description, variant })` — legacy admin compat.
|
|
5
|
+
import type * as React from "react";
|
|
6
|
+
import { toast as sonnerToast, type ExternalToast } from "sonner";
|
|
7
|
+
|
|
8
|
+
export type { ExternalToast } from "sonner";
|
|
9
|
+
export { sonnerToast };
|
|
10
|
+
|
|
11
|
+
export type LegacyToastOptions = ExternalToast & {
|
|
12
|
+
title?: React.ReactNode;
|
|
13
|
+
description?: React.ReactNode;
|
|
14
|
+
variant?: "default" | "destructive" | "success";
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
function nodeText(value: React.ReactNode): string {
|
|
18
|
+
if (value == null) return "";
|
|
19
|
+
if (typeof value === "string") return value;
|
|
20
|
+
if (typeof value === "number" || typeof value === "boolean") return String(value);
|
|
21
|
+
return "";
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function legacyToast(options: LegacyToastOptions) {
|
|
25
|
+
const { title, description, variant, ...rest } = options;
|
|
26
|
+
const titleText = nodeText(title);
|
|
27
|
+
const descText = nodeText(description);
|
|
28
|
+
const message = titleText || descText;
|
|
29
|
+
const desc = titleText && descText ? descText : undefined;
|
|
30
|
+
const sonnerOptions: ExternalToast = { ...rest, description: desc };
|
|
31
|
+
|
|
32
|
+
switch (variant) {
|
|
33
|
+
case "destructive":
|
|
34
|
+
return sonnerToast.error(message, sonnerOptions);
|
|
35
|
+
case "success":
|
|
36
|
+
return sonnerToast.success(message, sonnerOptions);
|
|
37
|
+
default:
|
|
38
|
+
return sonnerToast(message, sonnerOptions);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
type ToastFn = typeof sonnerToast &
|
|
43
|
+
((options: LegacyToastOptions) => ReturnType<typeof sonnerToast>);
|
|
44
|
+
|
|
45
|
+
/** Sonner toast + legacy `{ title, variant }` object form. */
|
|
46
|
+
const toast = Object.assign((messageOrOptions: string | LegacyToastOptions) => {
|
|
47
|
+
if (typeof messageOrOptions === "string") {
|
|
48
|
+
return sonnerToast(messageOrOptions);
|
|
49
|
+
}
|
|
50
|
+
return legacyToast(messageOrOptions);
|
|
51
|
+
}, sonnerToast) as ToastFn;
|
|
52
|
+
|
|
53
|
+
/** Legacy hook — prefer `toast` import directly; kept for existing admin pages. */
|
|
54
|
+
function useToast() {
|
|
55
|
+
return {
|
|
56
|
+
toast: (options: LegacyToastOptions) => legacyToast(options),
|
|
57
|
+
dismiss: sonnerToast.dismiss,
|
|
58
|
+
toasts: [] as const,
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export { toast, legacyToast, useToast };
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { describe, expect, it, vi } from "vitest";
|
|
2
|
+
import { renderWithUi, screen, userEvent } from "@/test/render";
|
|
3
|
+
import { Button } from "../button";
|
|
4
|
+
|
|
5
|
+
describe("Button", () => {
|
|
6
|
+
it("renders children and default type=submit implicit button", () => {
|
|
7
|
+
renderWithUi(<Button>Click me</Button>);
|
|
8
|
+
const btn = screen.getByRole("button", { name: "Click me" });
|
|
9
|
+
expect(btn).toBeInTheDocument();
|
|
10
|
+
expect(btn.tagName).toBe("BUTTON");
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
it("calls onClick when clicked", async () => {
|
|
14
|
+
const user = userEvent.setup();
|
|
15
|
+
const onClick = vi.fn();
|
|
16
|
+
renderWithUi(<Button onClick={onClick}>Go</Button>);
|
|
17
|
+
await user.click(screen.getByRole("button", { name: "Go" }));
|
|
18
|
+
expect(onClick).toHaveBeenCalledOnce();
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
it("does not fire onClick when disabled", async () => {
|
|
22
|
+
const user = userEvent.setup();
|
|
23
|
+
const onClick = vi.fn();
|
|
24
|
+
renderWithUi(
|
|
25
|
+
<Button disabled onClick={onClick}>
|
|
26
|
+
Blocked
|
|
27
|
+
</Button>,
|
|
28
|
+
);
|
|
29
|
+
await user.click(screen.getByRole("button", { name: "Blocked" }));
|
|
30
|
+
expect(onClick).not.toHaveBeenCalled();
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
it.each(["destructive", "outline", "secondary", "ghost", "link"] as const)(
|
|
34
|
+
"renders variant=%s",
|
|
35
|
+
(variant) => {
|
|
36
|
+
renderWithUi(<Button variant={variant}>V</Button>);
|
|
37
|
+
expect(screen.getByRole("button", { name: "V" })).toBeInTheDocument();
|
|
38
|
+
},
|
|
39
|
+
);
|
|
40
|
+
|
|
41
|
+
it.each(["xs", "sm", "lg", "icon", "icon-xs", "icon-sm", "icon-lg"] as const)(
|
|
42
|
+
"renders size=%s",
|
|
43
|
+
(size) => {
|
|
44
|
+
renderWithUi(<Button size={size}>S</Button>);
|
|
45
|
+
expect(screen.getByRole("button", { name: "S" })).toBeInTheDocument();
|
|
46
|
+
},
|
|
47
|
+
);
|
|
48
|
+
|
|
49
|
+
it("exposes shadcn data-slot and state attributes", () => {
|
|
50
|
+
renderWithUi(
|
|
51
|
+
<Button variant="outline" size="sm" aria-invalid>
|
|
52
|
+
State
|
|
53
|
+
</Button>,
|
|
54
|
+
);
|
|
55
|
+
const btn = screen.getByRole("button", { name: "State" });
|
|
56
|
+
expect(btn).toHaveAttribute("data-slot", "button");
|
|
57
|
+
expect(btn).toHaveAttribute("data-variant", "outline");
|
|
58
|
+
expect(btn).toHaveAttribute("data-size", "sm");
|
|
59
|
+
expect(btn).toHaveClass("aria-invalid:border-destructive");
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
it("default size applies ui-button size token binding", () => {
|
|
63
|
+
renderWithUi(<Button>Density</Button>);
|
|
64
|
+
expect(screen.getByRole("button", { name: "Density" })).toHaveClass("ui-button--default-size");
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
it("default variant uses semantic button token class", () => {
|
|
68
|
+
renderWithUi(<Button>Primary</Button>);
|
|
69
|
+
expect(screen.getByRole("button", { name: "Primary" })).toHaveClass("ui-button--default");
|
|
70
|
+
});
|
|
71
|
+
});
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
import { Slot } from "@radix-ui/react-slot";
|
|
3
|
+
import { cva } from "class-variance-authority";
|
|
4
|
+
import { cn } from "../../lib/utils";
|
|
5
|
+
import type { ButtonProp } from "../../props/components/general.prop";
|
|
6
|
+
|
|
7
|
+
const buttonVariants = cva("ui-button", {
|
|
8
|
+
variants: {
|
|
9
|
+
variant: {
|
|
10
|
+
default: "ui-button--default bg-primary text-primary-foreground hover:bg-primary/90",
|
|
11
|
+
destructive:
|
|
12
|
+
"ui-button--destructive bg-destructive text-destructive-foreground hover:bg-destructive/90 focus-visible:ring-destructive/20",
|
|
13
|
+
outline:
|
|
14
|
+
"ui-button--outline border bg-background shadow-xs hover:bg-accent hover:text-accent-foreground",
|
|
15
|
+
secondary:
|
|
16
|
+
"ui-button--secondary bg-secondary text-secondary-foreground hover:bg-secondary/80",
|
|
17
|
+
ghost: "ui-button--ghost hover:bg-accent hover:text-accent-foreground",
|
|
18
|
+
link: "ui-button--link text-primary underline-offset-4 hover:underline",
|
|
19
|
+
},
|
|
20
|
+
size: {
|
|
21
|
+
default: "ui-button--default-size py-2 has-[>svg]:px-3",
|
|
22
|
+
xs: "h-[calc(var(--control-height)-0.75rem)] gap-1 rounded-md px-2 text-xs has-[>svg]:px-1.5 [&_svg:not([class*='size-'])]:size-3",
|
|
23
|
+
sm: "ui-button--sm gap-1.5 rounded-md has-[>svg]:px-2.5",
|
|
24
|
+
lg: "ui-button--lg rounded-md has-[>svg]:px-4",
|
|
25
|
+
icon: "ui-button--icon",
|
|
26
|
+
"icon-xs":
|
|
27
|
+
"size-[calc(var(--control-height)-0.75rem)] rounded-md [&_svg:not([class*='size-'])]:size-3",
|
|
28
|
+
"icon-sm": "size-[calc(var(--control-height)-0.5rem)]",
|
|
29
|
+
"icon-lg": "size-[calc(var(--control-height)+0.25rem)]",
|
|
30
|
+
},
|
|
31
|
+
},
|
|
32
|
+
defaultVariants: { variant: "default", size: "default" },
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
export type { ButtonProp, ButtonProp as ButtonProps } from "../../props/components/general.prop";
|
|
36
|
+
|
|
37
|
+
export const Button = React.forwardRef<HTMLButtonElement, ButtonProp>(
|
|
38
|
+
({ className, variant, size, asChild = false, ...props }, ref) => {
|
|
39
|
+
const Comp = asChild ? Slot : "button";
|
|
40
|
+
return (
|
|
41
|
+
<Comp
|
|
42
|
+
data-slot="button"
|
|
43
|
+
data-variant={variant ?? "default"}
|
|
44
|
+
data-size={size ?? "default"}
|
|
45
|
+
className={cn(
|
|
46
|
+
"inline-flex shrink-0 items-center justify-center gap-2 rounded-md text-sm font-medium whitespace-nowrap transition-all outline-none",
|
|
47
|
+
"focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px]",
|
|
48
|
+
"disabled:pointer-events-none disabled:opacity-50",
|
|
49
|
+
"aria-invalid:border-destructive aria-invalid:ring-destructive/20",
|
|
50
|
+
"[&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
|
|
51
|
+
buttonVariants({ variant, size, className }),
|
|
52
|
+
)}
|
|
53
|
+
ref={ref}
|
|
54
|
+
{...props}
|
|
55
|
+
/>
|
|
56
|
+
);
|
|
57
|
+
},
|
|
58
|
+
);
|
|
59
|
+
Button.displayName = "Button";
|
|
60
|
+
|
|
61
|
+
export { buttonVariants };
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import { renderWithUi, screen } from "@/test/render";
|
|
3
|
+
import { PageContainer } from "../page-container";
|
|
4
|
+
import { Button } from "../../general/button";
|
|
5
|
+
|
|
6
|
+
describe("PageContainer", () => {
|
|
7
|
+
it("renders title as h1", () => {
|
|
8
|
+
renderWithUi(<PageContainer title="Customers" />);
|
|
9
|
+
expect(screen.getByRole("heading", { level: 1, name: "Customers" })).toBeInTheDocument();
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
it("renders subtitle when provided", () => {
|
|
13
|
+
renderWithUi(<PageContainer title="Customers" subtitle="CRM list" />);
|
|
14
|
+
expect(screen.getByText("CRM list")).toHaveClass("ui-page-subtitle");
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
it("renders extra slot in header row", () => {
|
|
18
|
+
renderWithUi(<PageContainer title="Customers" extra={<Button>Create</Button>} />);
|
|
19
|
+
expect(screen.getByRole("button", { name: "Create" })).toBeInTheDocument();
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
it("renders footer slot", () => {
|
|
23
|
+
renderWithUi(<PageContainer title="Edit" footer={<Button>Save</Button>} />);
|
|
24
|
+
expect(screen.getByRole("contentinfo")).toContainElement(
|
|
25
|
+
screen.getByRole("button", { name: "Save" }),
|
|
26
|
+
);
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
it("renders breadcrumb trail with links", () => {
|
|
30
|
+
renderWithUi(
|
|
31
|
+
<PageContainer
|
|
32
|
+
title="Detail"
|
|
33
|
+
breadcrumb={[
|
|
34
|
+
{ label: "CRM", to: "/crm" },
|
|
35
|
+
{ label: "Customers", to: "/crm/customers" },
|
|
36
|
+
{ label: "Detail" },
|
|
37
|
+
]}
|
|
38
|
+
/>,
|
|
39
|
+
);
|
|
40
|
+
const nav = screen.getByRole("navigation", { name: "Breadcrumb" });
|
|
41
|
+
expect(nav).toBeInTheDocument();
|
|
42
|
+
expect(screen.getByRole("link", { name: "CRM" })).toHaveAttribute("href", "/crm");
|
|
43
|
+
expect(nav).toHaveTextContent("Detail");
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
it("applies density class on root", () => {
|
|
47
|
+
const { container } = renderWithUi(<PageContainer title="Compact" density="compact" />);
|
|
48
|
+
expect(container.firstChild).toHaveClass("ui-density-compact");
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
it("applies variant modifier class", () => {
|
|
52
|
+
const { container } = renderWithUi(<PageContainer title="List" variant="flush" />);
|
|
53
|
+
expect(container.firstChild).toHaveClass("ui-page-container--flush");
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
it("applies sticky footer modifier when enabled", () => {
|
|
57
|
+
const { container } = renderWithUi(<PageContainer title="Form" stickyFooter />);
|
|
58
|
+
expect(container.firstChild).toHaveClass("ui-page-container--sticky-footer");
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
it("renders children in page body", () => {
|
|
62
|
+
renderWithUi(
|
|
63
|
+
<PageContainer title="Page">
|
|
64
|
+
<p>Body content</p>
|
|
65
|
+
</PageContainer>,
|
|
66
|
+
);
|
|
67
|
+
expect(screen.getByText("Body content")).toBeInTheDocument();
|
|
68
|
+
});
|
|
69
|
+
});
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import { renderWithUi, screen } from "@/test/render";
|
|
3
|
+
import { PageInset } from "../page-inset";
|
|
4
|
+
|
|
5
|
+
describe("PageInset", () => {
|
|
6
|
+
it("renders children with inset class", () => {
|
|
7
|
+
renderWithUi(
|
|
8
|
+
<PageInset>
|
|
9
|
+
<p>Filter zone</p>
|
|
10
|
+
</PageInset>,
|
|
11
|
+
);
|
|
12
|
+
expect(screen.getByText("Filter zone").parentElement).toHaveClass("ui-page-inset");
|
|
13
|
+
});
|
|
14
|
+
});
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import { renderWithUi } from "@/test/render";
|
|
3
|
+
import { Stack } from "../stack";
|
|
4
|
+
import { Inline } from "../inline";
|
|
5
|
+
|
|
6
|
+
describe("Stack", () => {
|
|
7
|
+
it.each([
|
|
8
|
+
["xs", "ui-stack-xs"],
|
|
9
|
+
["sm", "ui-stack-sm"],
|
|
10
|
+
["md", "ui-stack-md"],
|
|
11
|
+
["lg", "ui-stack-lg"],
|
|
12
|
+
["xl", "ui-stack-xl"],
|
|
13
|
+
] as const)("applies gap=%s → %s", (gap, cls) => {
|
|
14
|
+
const { container } = renderWithUi(
|
|
15
|
+
<Stack gap={gap}>
|
|
16
|
+
<span>a</span>
|
|
17
|
+
<span>b</span>
|
|
18
|
+
</Stack>,
|
|
19
|
+
);
|
|
20
|
+
expect(container.firstChild).toHaveClass(cls);
|
|
21
|
+
});
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
describe("Inline", () => {
|
|
25
|
+
it.each([
|
|
26
|
+
["xs", "ui-inline-xs"],
|
|
27
|
+
["sm", "ui-inline-sm"],
|
|
28
|
+
["md", "ui-inline-md"],
|
|
29
|
+
["lg", "ui-inline-lg"],
|
|
30
|
+
] as const)("applies gap=%s → %s", (gap, cls) => {
|
|
31
|
+
const { container } = renderWithUi(
|
|
32
|
+
<Inline gap={gap}>
|
|
33
|
+
<span>a</span>
|
|
34
|
+
<span>b</span>
|
|
35
|
+
</Inline>,
|
|
36
|
+
);
|
|
37
|
+
expect(container.firstChild).toHaveClass(cls);
|
|
38
|
+
});
|
|
39
|
+
});
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import type { AppShellProp } from "../../props/components/layout.prop";
|
|
2
|
+
|
|
3
|
+
export type {
|
|
4
|
+
AppShellProp,
|
|
5
|
+
AppShellProp as AppShellProps,
|
|
6
|
+
} from "../../props/components/layout.prop";
|
|
7
|
+
|
|
8
|
+
export function AppShell({
|
|
9
|
+
sidebar,
|
|
10
|
+
topbar,
|
|
11
|
+
topbarLeft,
|
|
12
|
+
topbarRight,
|
|
13
|
+
logo,
|
|
14
|
+
breadcrumb,
|
|
15
|
+
footer,
|
|
16
|
+
children,
|
|
17
|
+
sidebarCollapsed = false,
|
|
18
|
+
}: AppShellProp) {
|
|
19
|
+
const resolvedTopbar =
|
|
20
|
+
topbar !== undefined ? (
|
|
21
|
+
topbar
|
|
22
|
+
) : (
|
|
23
|
+
<div className="app-topbar-rail">
|
|
24
|
+
{logo !== undefined && <div className="app-topbar-logo">{logo}</div>}
|
|
25
|
+
{topbarLeft !== undefined && <div className="app-topbar-left">{topbarLeft}</div>}
|
|
26
|
+
<div className="app-topbar-spacer" />
|
|
27
|
+
{topbarRight !== undefined && <div className="app-topbar-right">{topbarRight}</div>}
|
|
28
|
+
</div>
|
|
29
|
+
);
|
|
30
|
+
|
|
31
|
+
return (
|
|
32
|
+
<div className="app-root" data-collapsed={sidebarCollapsed ? "true" : undefined}>
|
|
33
|
+
<aside className="app-sidebar">{sidebar}</aside>
|
|
34
|
+
<header className="app-topbar">{resolvedTopbar}</header>
|
|
35
|
+
<main className="app-main">
|
|
36
|
+
{breadcrumb !== undefined && <div className="app-breadcrumb">{breadcrumb}</div>}
|
|
37
|
+
{children}
|
|
38
|
+
</main>
|
|
39
|
+
{footer !== undefined && <footer className="app-footer">{footer}</footer>}
|
|
40
|
+
</div>
|
|
41
|
+
);
|
|
42
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { ChevronRight } from "lucide-react";
|
|
2
|
+
import { Link } from "react-router-dom";
|
|
3
|
+
|
|
4
|
+
import type { BreadcrumbProp } from "../../props/vocabulary/navigation.prop";
|
|
5
|
+
|
|
6
|
+
export type BreadcrumbProps = {
|
|
7
|
+
items: BreadcrumbProp;
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
export function Breadcrumb({ items }: BreadcrumbProps) {
|
|
11
|
+
return (
|
|
12
|
+
<nav aria-label="Breadcrumb" className="ui-breadcrumb">
|
|
13
|
+
<ol className="ui-breadcrumb-list">
|
|
14
|
+
{items.map((item, index) => {
|
|
15
|
+
const isLast = index === items.length - 1;
|
|
16
|
+
|
|
17
|
+
return (
|
|
18
|
+
<li key={item.to ?? index} className="ui-breadcrumb-item">
|
|
19
|
+
{item.to && !isLast ? (
|
|
20
|
+
<Link to={item.to} className="ui-breadcrumb-link">
|
|
21
|
+
{item.label}
|
|
22
|
+
</Link>
|
|
23
|
+
) : (
|
|
24
|
+
<span className="ui-breadcrumb-current" aria-current={isLast ? "page" : undefined}>
|
|
25
|
+
{item.label}
|
|
26
|
+
</span>
|
|
27
|
+
)}
|
|
28
|
+
{!isLast ? <ChevronRight aria-hidden="true" /> : null}
|
|
29
|
+
</li>
|
|
30
|
+
);
|
|
31
|
+
})}
|
|
32
|
+
</ol>
|
|
33
|
+
</nav>
|
|
34
|
+
);
|
|
35
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
export { PageContainer } from "./page-container";
|
|
2
|
+
export type {
|
|
3
|
+
PageContainerProp,
|
|
4
|
+
PageContainerProps,
|
|
5
|
+
BreadcrumbItem,
|
|
6
|
+
BreadcrumbItemProp,
|
|
7
|
+
} from "./page-container";
|
|
8
|
+
export { PageInset } from "./page-inset";
|
|
9
|
+
export type { PageInsetProp, PageInsetProps } from "./page-inset";
|
|
10
|
+
export { Stack } from "./stack";
|
|
11
|
+
export type { StackProp, StackProps } from "./stack";
|
|
12
|
+
export { Inline } from "./inline";
|
|
13
|
+
export type { InlineProp, InlineProps } from "./inline";
|
|
14
|
+
export { AppShell } from "./app-shell";
|
|
15
|
+
export type { AppShellProps } from "./app-shell";
|
|
16
|
+
export { ShellApp } from "./shell-app";
|
|
17
|
+
export type { ShellAppProps } from "./shell-app";
|
|
18
|
+
export { Menu } from "./menu";
|
|
19
|
+
export type { MenuItem, MenuProps, MenuSection } from "./menu";
|
|
20
|
+
export { Breadcrumb } from "./breadcrumb";
|
|
21
|
+
export type { BreadcrumbProps } from "./breadcrumb";
|
|
22
|
+
export { Sidebar } from "./sidebar";
|
|
23
|
+
export type { SidebarItem, SidebarProduct, SidebarProps, SidebarSection } from "./sidebar";
|
|
24
|
+
export { Topbar } from "./topbar";
|
|
25
|
+
export type { TopbarProduct, TopbarProject, TopbarProps } from "./topbar";
|
|
26
|
+
export { ResponsiveGrid } from "./responsive-grid";
|
|
27
|
+
export type { ResponsiveGridProps } from "./responsive-grid";
|
|
28
|
+
export { SplitPane } from "./split-pane";
|
|
29
|
+
export type { SplitPaneProps } from "./split-pane";
|
|
30
|
+
export { MobileFrame } from "./mobile-frame";
|
|
31
|
+
export type { MobileFrameNavItem, MobileFrameProps } from "./mobile-frame";
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { cn } from "../../lib/utils";
|
|
2
|
+
import { inlineGapClass } from "../../lib/variants";
|
|
3
|
+
import type { InlineProp } from "../../props/components/layout.prop";
|
|
4
|
+
|
|
5
|
+
export type { InlineProp, InlineProp as InlineProps } from "../../props/components/layout.prop";
|
|
6
|
+
|
|
7
|
+
export function Inline({ gap = "sm", className, children, ...props }: InlineProp) {
|
|
8
|
+
return (
|
|
9
|
+
<div className={cn(inlineGapClass[gap], className)} {...props}>
|
|
10
|
+
{children}
|
|
11
|
+
</div>
|
|
12
|
+
);
|
|
13
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import type { SidebarItem, SidebarSection } from "./sidebar";
|
|
2
|
+
import { Sidebar } from "./sidebar";
|
|
3
|
+
|
|
4
|
+
export type MenuItem = SidebarItem & {
|
|
5
|
+
active?: boolean;
|
|
6
|
+
};
|
|
7
|
+
|
|
8
|
+
export type MenuSection = {
|
|
9
|
+
label?: string;
|
|
10
|
+
items: MenuItem[];
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
export type MenuProps = {
|
|
14
|
+
items: MenuSection[];
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
export function Menu({ items }: MenuProps) {
|
|
18
|
+
const sections: SidebarSection[] = items.map((section) => ({
|
|
19
|
+
label: section.label,
|
|
20
|
+
items: section.items.map(({ active: _active, ...item }) => item),
|
|
21
|
+
}));
|
|
22
|
+
const activeId =
|
|
23
|
+
items.flatMap((section) => section.items).find((item) => item.active)?.id ??
|
|
24
|
+
items[0]?.items[0]?.id ??
|
|
25
|
+
"";
|
|
26
|
+
|
|
27
|
+
return (
|
|
28
|
+
<Sidebar
|
|
29
|
+
activeId={activeId}
|
|
30
|
+
sections={sections}
|
|
31
|
+
product={{ name: "Acme Console", role: "Workspace", color: "hsl(var(--attention))" }}
|
|
32
|
+
/>
|
|
33
|
+
);
|
|
34
|
+
}
|