@godxjp/ui 5.0.2 → 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 -668
- 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 -7343
- package/dist/components/composites.js.map +0 -1
- package/dist/components/primitives.d.ts +0 -2744
- package/dist/components/primitives.js +0 -7356
- 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 -13062
- 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,194 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
import { cva, type VariantProps } from "class-variance-authority";
|
|
3
|
+
|
|
4
|
+
import { cn } from "../../lib/utils";
|
|
5
|
+
|
|
6
|
+
type CardSize = "default" | "compact";
|
|
7
|
+
/** Semantic 3px left-edge accent stripe. */
|
|
8
|
+
type CardAccent = "primary" | "success" | "warning" | "info" | "attention" | "destructive";
|
|
9
|
+
/** Surface fill — plain card, muted band, borderless outline, or emphasized featured ring. */
|
|
10
|
+
type CardVariant = "default" | "muted" | "outline" | "featured";
|
|
11
|
+
/** Padding density — base 16px · tight 12px · cozy 20px. */
|
|
12
|
+
type CardDensity = "tight" | "cozy";
|
|
13
|
+
|
|
14
|
+
const cardVariants = cva("group/card border", {
|
|
15
|
+
variants: {
|
|
16
|
+
size: {
|
|
17
|
+
default: "",
|
|
18
|
+
compact: "",
|
|
19
|
+
},
|
|
20
|
+
},
|
|
21
|
+
defaultVariants: { size: "default" },
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
export type CardProps = React.HTMLAttributes<HTMLDivElement> &
|
|
25
|
+
VariantProps<typeof cardVariants> & {
|
|
26
|
+
size?: CardSize;
|
|
27
|
+
accent?: CardAccent;
|
|
28
|
+
variant?: CardVariant;
|
|
29
|
+
density?: CardDensity;
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
export const Card = React.forwardRef<HTMLDivElement, CardProps>(
|
|
33
|
+
({ className, size = "default", accent, variant, density, ...props }, ref) => (
|
|
34
|
+
<div
|
|
35
|
+
ref={ref}
|
|
36
|
+
className={cn(cardVariants({ size }), className)}
|
|
37
|
+
data-slot="card"
|
|
38
|
+
data-size={size === "compact" ? "compact" : undefined}
|
|
39
|
+
data-accent={accent}
|
|
40
|
+
data-variant={variant && variant !== "default" ? variant : undefined}
|
|
41
|
+
data-density={density}
|
|
42
|
+
{...props}
|
|
43
|
+
/>
|
|
44
|
+
),
|
|
45
|
+
);
|
|
46
|
+
Card.displayName = "Card";
|
|
47
|
+
|
|
48
|
+
/** Full-bleed cover media — first child; header below uses section top (φ⁰), not shell. */
|
|
49
|
+
export type CardCoverProps = React.HTMLAttributes<HTMLDivElement>;
|
|
50
|
+
|
|
51
|
+
export const CardCover = React.forwardRef<HTMLDivElement, CardCoverProps>(
|
|
52
|
+
({ className, ...props }, ref) => (
|
|
53
|
+
<div ref={ref} data-slot="card-cover" className={cn("ui-card-cover", className)} {...props} />
|
|
54
|
+
),
|
|
55
|
+
);
|
|
56
|
+
CardCover.displayName = "CardCover";
|
|
57
|
+
|
|
58
|
+
export type CardHeaderProps = React.HTMLAttributes<HTMLDivElement> & {
|
|
59
|
+
/** Muted background + border-bottom — section band (mirror footer `separated`). */
|
|
60
|
+
banded?: boolean;
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
export const CardHeader = React.forwardRef<HTMLDivElement, CardHeaderProps>(
|
|
64
|
+
({ className, banded, ...props }, ref) => (
|
|
65
|
+
<div
|
|
66
|
+
ref={ref}
|
|
67
|
+
data-slot="card-header"
|
|
68
|
+
data-banded={banded ? "" : undefined}
|
|
69
|
+
className={cn(banded && "ui-card-header--banded", className)}
|
|
70
|
+
{...props}
|
|
71
|
+
/>
|
|
72
|
+
),
|
|
73
|
+
);
|
|
74
|
+
CardHeader.displayName = "CardHeader";
|
|
75
|
+
|
|
76
|
+
export const CardTitle = React.forwardRef<
|
|
77
|
+
HTMLHeadingElement,
|
|
78
|
+
React.HTMLAttributes<HTMLHeadingElement>
|
|
79
|
+
>(({ className, children, ...props }, ref) => (
|
|
80
|
+
<h3 ref={ref} data-slot="card-title" className={className} {...props}>
|
|
81
|
+
{children}
|
|
82
|
+
</h3>
|
|
83
|
+
));
|
|
84
|
+
CardTitle.displayName = "CardTitle";
|
|
85
|
+
|
|
86
|
+
export const CardDescription = React.forwardRef<
|
|
87
|
+
HTMLParagraphElement,
|
|
88
|
+
React.HTMLAttributes<HTMLParagraphElement>
|
|
89
|
+
>(({ className, ...props }, ref) => (
|
|
90
|
+
<p ref={ref} data-slot="card-description" className={className} {...props} />
|
|
91
|
+
));
|
|
92
|
+
CardDescription.displayName = "CardDescription";
|
|
93
|
+
|
|
94
|
+
export type CardContentProps = React.HTMLAttributes<HTMLDivElement> & {
|
|
95
|
+
/** Edge-to-edge body (tables, tabs list). Horizontal padding removed. */
|
|
96
|
+
flush?: boolean;
|
|
97
|
+
/** No gap after header — pair with tabs / flush toolbar. */
|
|
98
|
+
tight?: boolean;
|
|
99
|
+
/** No header above — top padding matches card shell. */
|
|
100
|
+
solo?: boolean;
|
|
101
|
+
};
|
|
102
|
+
|
|
103
|
+
export const CardContent = React.forwardRef<HTMLDivElement, CardContentProps>(
|
|
104
|
+
({ className, flush, tight, solo, ...props }, ref) => (
|
|
105
|
+
<div
|
|
106
|
+
ref={ref}
|
|
107
|
+
data-slot="card-content"
|
|
108
|
+
data-flush={flush ? "" : undefined}
|
|
109
|
+
data-tight={tight ? "" : undefined}
|
|
110
|
+
data-solo={solo ? "" : undefined}
|
|
111
|
+
className={className}
|
|
112
|
+
{...props}
|
|
113
|
+
/>
|
|
114
|
+
),
|
|
115
|
+
);
|
|
116
|
+
CardContent.displayName = "CardContent";
|
|
117
|
+
|
|
118
|
+
export type CardFooterProps = React.HTMLAttributes<HTMLDivElement> & {
|
|
119
|
+
/** Top border + symmetric action band — form Save/Cancel, table summary. */
|
|
120
|
+
separated?: boolean;
|
|
121
|
+
/** Full-bleed footer (Ant Design `actions` bar). */
|
|
122
|
+
flush?: boolean;
|
|
123
|
+
};
|
|
124
|
+
|
|
125
|
+
export const CardFooter = React.forwardRef<HTMLDivElement, CardFooterProps>(
|
|
126
|
+
({ className, separated, flush, ...props }, ref) => (
|
|
127
|
+
<div
|
|
128
|
+
ref={ref}
|
|
129
|
+
data-slot="card-footer"
|
|
130
|
+
data-separated={separated ? "" : undefined}
|
|
131
|
+
data-flush={flush ? "" : undefined}
|
|
132
|
+
className={className}
|
|
133
|
+
{...props}
|
|
134
|
+
/>
|
|
135
|
+
),
|
|
136
|
+
);
|
|
137
|
+
CardFooter.displayName = "CardFooter";
|
|
138
|
+
|
|
139
|
+
export type CardStatProps = React.HTMLAttributes<HTMLDivElement> &
|
|
140
|
+
VariantProps<typeof cardVariants> & {
|
|
141
|
+
label: React.ReactNode;
|
|
142
|
+
value: React.ReactNode;
|
|
143
|
+
hint?: React.ReactNode;
|
|
144
|
+
/** Optional compact trend text beside the value. Avoid badge-like deltas. */
|
|
145
|
+
delta?: React.ReactNode;
|
|
146
|
+
/** KPI layout: stacked = design default, inline = label left / value right. */
|
|
147
|
+
layout?: "stacked" | "inline";
|
|
148
|
+
/** Align the metric group. */
|
|
149
|
+
align?: "start" | "end";
|
|
150
|
+
};
|
|
151
|
+
|
|
152
|
+
/** KPI / stat tile — token-driven layout aligned to dashboard KPI cards. */
|
|
153
|
+
export function CardStat({
|
|
154
|
+
label,
|
|
155
|
+
value,
|
|
156
|
+
hint,
|
|
157
|
+
delta,
|
|
158
|
+
layout = "stacked",
|
|
159
|
+
align = "start",
|
|
160
|
+
className,
|
|
161
|
+
size = "compact",
|
|
162
|
+
...props
|
|
163
|
+
}: CardStatProps) {
|
|
164
|
+
return (
|
|
165
|
+
<Card
|
|
166
|
+
size={size ?? "compact"}
|
|
167
|
+
className={className}
|
|
168
|
+
data-stat-card=""
|
|
169
|
+
data-stat-layout={layout}
|
|
170
|
+
data-stat-align={align}
|
|
171
|
+
{...props}
|
|
172
|
+
>
|
|
173
|
+
<div data-slot="card-stat-body">
|
|
174
|
+
<div data-slot="card-stat-label">{label}</div>
|
|
175
|
+
{hint && layout === "inline" ? <div data-slot="card-stat-hint">{hint}</div> : null}
|
|
176
|
+
</div>
|
|
177
|
+
<div>
|
|
178
|
+
<div data-slot="card-stat-value-row">
|
|
179
|
+
<span data-slot="card-stat-value">{value}</span>
|
|
180
|
+
{delta ? <span data-slot="card-stat-delta">{delta}</span> : null}
|
|
181
|
+
</div>
|
|
182
|
+
{hint && layout !== "inline" ? <div data-slot="card-stat-hint">{hint}</div> : null}
|
|
183
|
+
</div>
|
|
184
|
+
</Card>
|
|
185
|
+
);
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/** Header actions slot — pair with `CardHeader className="flex flex-row …"`. */
|
|
189
|
+
export const CardAction = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(
|
|
190
|
+
({ className, ...props }, ref) => (
|
|
191
|
+
<div ref={ref} data-slot="card-action" className={className} {...props} />
|
|
192
|
+
),
|
|
193
|
+
);
|
|
194
|
+
CardAction.displayName = "CardAction";
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import type { ComponentType, SVGProps } from "react";
|
|
2
|
+
import { Hash, ShoppingBag, Truck } from "lucide-react";
|
|
3
|
+
|
|
4
|
+
export type CodeBadgeKind = "internal" | "seller" | "yamato";
|
|
5
|
+
|
|
6
|
+
export type CodeBadgeProps = {
|
|
7
|
+
kind: CodeBadgeKind;
|
|
8
|
+
value: string;
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
const codeBadgeConfig = {
|
|
12
|
+
internal: { label: "INT", icon: Hash },
|
|
13
|
+
seller: { label: "SLR", icon: ShoppingBag },
|
|
14
|
+
yamato: { label: "YMT", icon: Truck },
|
|
15
|
+
} satisfies Record<CodeBadgeKind, { label: string; icon: ComponentType<SVGProps<SVGSVGElement>> }>;
|
|
16
|
+
|
|
17
|
+
export function CodeBadge({ kind, value }: CodeBadgeProps) {
|
|
18
|
+
const config = codeBadgeConfig[kind] ?? codeBadgeConfig.internal;
|
|
19
|
+
const Icon = config.icon;
|
|
20
|
+
|
|
21
|
+
return (
|
|
22
|
+
<span className="ui-code-badge" data-kind={kind}>
|
|
23
|
+
<span className="ui-code-badge-label">{config.label}</span>
|
|
24
|
+
<Icon aria-hidden="true" />
|
|
25
|
+
<span className="ui-code-badge-value">{value}</span>
|
|
26
|
+
</span>
|
|
27
|
+
);
|
|
28
|
+
}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import * as CollapsiblePrimitive from "@radix-ui/react-collapsible";
|
|
2
|
+
|
|
3
|
+
export const Collapsible = CollapsiblePrimitive.Root;
|
|
4
|
+
export const CollapsibleTrigger = CollapsiblePrimitive.CollapsibleTrigger;
|
|
5
|
+
export const CollapsibleContent = CollapsiblePrimitive.CollapsibleContent;
|
|
@@ -0,0 +1,476 @@
|
|
|
1
|
+
// DataTable — compound component for admin lists.
|
|
2
|
+
//
|
|
3
|
+
// Encapsulates: sticky header, density toggle, per-row click navigation, bulk
|
|
4
|
+
// selection, empty/loading states, cursor pagination. Use this everywhere
|
|
5
|
+
// instead of raw <Table> markup.
|
|
6
|
+
//
|
|
7
|
+
// Compound API (drop these as children of <DataTable>):
|
|
8
|
+
// <DataTable.Toolbar> — left-aligned status / right-aligned controls
|
|
9
|
+
// <DataTable.SelectAll> — header checkbox bound to selection state
|
|
10
|
+
// <DataTable.BulkActions> — only rendered when count > 0; sits in the toolbar
|
|
11
|
+
// <DataTable.DensityToggle> — compact ↔ comfortable
|
|
12
|
+
// <DataTable.Content> — the actual table body (auto-included when omitted)
|
|
13
|
+
// <DataTable.Pagination> — cursor pagination footer
|
|
14
|
+
import * as React from "react";
|
|
15
|
+
import { ArrowDown, ArrowUp, ChevronsUpDown, Layers, Layers2, MoreHorizontal } from "lucide-react";
|
|
16
|
+
|
|
17
|
+
import { useTranslation } from "../../i18n/use-translation";
|
|
18
|
+
import { Inline } from "../layout/inline";
|
|
19
|
+
import { Button } from "../general/button";
|
|
20
|
+
import {
|
|
21
|
+
Table,
|
|
22
|
+
TableBody,
|
|
23
|
+
TableCell,
|
|
24
|
+
TableHead,
|
|
25
|
+
TableHeader,
|
|
26
|
+
TableRow,
|
|
27
|
+
} from "../data-display/table";
|
|
28
|
+
import { cn } from "../../lib/utils";
|
|
29
|
+
import { densityClass } from "../../lib/variants";
|
|
30
|
+
import {
|
|
31
|
+
controlIconSmClass,
|
|
32
|
+
tableCellPaddingClass,
|
|
33
|
+
tableRowHeightClass,
|
|
34
|
+
} from "../../lib/control-styles";
|
|
35
|
+
import type { ColumnDefProp, TableDensityProp, SortStateProp } from "../../props/vocabulary";
|
|
36
|
+
|
|
37
|
+
export type Density = TableDensityProp;
|
|
38
|
+
export type ColumnDef<T> = ColumnDefProp<T>;
|
|
39
|
+
|
|
40
|
+
interface DataTableContextValue<T = unknown> {
|
|
41
|
+
data: T[];
|
|
42
|
+
columns: ColumnDef<T>[];
|
|
43
|
+
density: Density;
|
|
44
|
+
setDensity: (d: Density) => void;
|
|
45
|
+
selected: Set<string>;
|
|
46
|
+
toggleSelect: (id: string) => void;
|
|
47
|
+
toggleSelectAll: () => void;
|
|
48
|
+
allSelected: boolean;
|
|
49
|
+
someSelected: boolean;
|
|
50
|
+
selectable: boolean;
|
|
51
|
+
getRowId: (row: T) => string;
|
|
52
|
+
onRowClick?: (row: T) => void;
|
|
53
|
+
sort?: SortStateProp;
|
|
54
|
+
onSortChange?: (sort: SortStateProp | undefined) => void;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const DataTableContext = React.createContext<DataTableContextValue | null>(null);
|
|
58
|
+
|
|
59
|
+
function useDataTableContext<T>() {
|
|
60
|
+
const ctx = React.useContext(DataTableContext);
|
|
61
|
+
if (!ctx) throw new Error("DataTable subcomponents must be used inside <DataTable>");
|
|
62
|
+
return ctx as unknown as DataTableContextValue<T>;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function useOptionalDataTableContext<T>() {
|
|
66
|
+
return React.useContext(DataTableContext) as unknown as DataTableContextValue<T> | null;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
interface DataTableProps<T> {
|
|
70
|
+
data: T[];
|
|
71
|
+
columns: ColumnDef<T>[];
|
|
72
|
+
/** Required when `selectable` is true. Default: assume row.id (typed as any). */
|
|
73
|
+
getRowId?: (row: T) => string;
|
|
74
|
+
selectable?: boolean;
|
|
75
|
+
selected?: Set<string>;
|
|
76
|
+
onSelectChange?: (next: Set<string>) => void;
|
|
77
|
+
onRowClick?: (row: T) => void;
|
|
78
|
+
density?: Density;
|
|
79
|
+
onDensityChange?: (d: Density) => void;
|
|
80
|
+
sort?: SortStateProp;
|
|
81
|
+
onSortChange?: (sort: SortStateProp | undefined) => void;
|
|
82
|
+
className?: string;
|
|
83
|
+
children?: React.ReactNode;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const noopGetRowId = <T,>(row: T): string => {
|
|
87
|
+
const id = (row as { id?: unknown }).id;
|
|
88
|
+
if (typeof id === "string") return id;
|
|
89
|
+
if (typeof id === "number") return String(id);
|
|
90
|
+
return "";
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
export function DataTable<T>({
|
|
94
|
+
data,
|
|
95
|
+
columns,
|
|
96
|
+
getRowId = noopGetRowId,
|
|
97
|
+
selectable = false,
|
|
98
|
+
selected: controlledSelected,
|
|
99
|
+
onSelectChange,
|
|
100
|
+
onRowClick,
|
|
101
|
+
density: controlledDensity,
|
|
102
|
+
onDensityChange,
|
|
103
|
+
sort,
|
|
104
|
+
onSortChange,
|
|
105
|
+
className,
|
|
106
|
+
children,
|
|
107
|
+
}: DataTableProps<T>) {
|
|
108
|
+
const [internalDensity, setInternalDensity] = React.useState<Density>("compact");
|
|
109
|
+
const density = controlledDensity ?? internalDensity;
|
|
110
|
+
const setDensity = (d: Density) => {
|
|
111
|
+
setInternalDensity(d);
|
|
112
|
+
onDensityChange?.(d);
|
|
113
|
+
};
|
|
114
|
+
|
|
115
|
+
const [internalSelected, setInternalSelected] = React.useState<Set<string>>(new Set());
|
|
116
|
+
const selected = controlledSelected ?? internalSelected;
|
|
117
|
+
const setSelected = (next: Set<string>) => {
|
|
118
|
+
setInternalSelected(next);
|
|
119
|
+
onSelectChange?.(next);
|
|
120
|
+
};
|
|
121
|
+
|
|
122
|
+
const toggleSelect = (id: string) => {
|
|
123
|
+
const next = new Set(selected);
|
|
124
|
+
if (next.has(id)) next.delete(id);
|
|
125
|
+
else next.add(id);
|
|
126
|
+
setSelected(next);
|
|
127
|
+
};
|
|
128
|
+
const allSelected = data.length > 0 && data.every((r) => selected.has(getRowId(r)));
|
|
129
|
+
const someSelected = !allSelected && data.some((r) => selected.has(getRowId(r)));
|
|
130
|
+
const toggleSelectAll = () => {
|
|
131
|
+
if (allSelected) setSelected(new Set());
|
|
132
|
+
else setSelected(new Set(data.map(getRowId)));
|
|
133
|
+
};
|
|
134
|
+
|
|
135
|
+
const ctx: DataTableContextValue<T> = {
|
|
136
|
+
data,
|
|
137
|
+
columns,
|
|
138
|
+
density,
|
|
139
|
+
setDensity,
|
|
140
|
+
selected,
|
|
141
|
+
toggleSelect,
|
|
142
|
+
toggleSelectAll,
|
|
143
|
+
allSelected,
|
|
144
|
+
someSelected,
|
|
145
|
+
selectable,
|
|
146
|
+
getRowId,
|
|
147
|
+
onRowClick,
|
|
148
|
+
sort,
|
|
149
|
+
onSortChange,
|
|
150
|
+
};
|
|
151
|
+
|
|
152
|
+
// Determine if children include a Content slot — if not, render default.
|
|
153
|
+
const hasContent = React.Children.toArray(children).some(
|
|
154
|
+
(c) =>
|
|
155
|
+
React.isValidElement(c) &&
|
|
156
|
+
(c.type as { displayName?: string }).displayName === "DataTable.Content",
|
|
157
|
+
);
|
|
158
|
+
|
|
159
|
+
return (
|
|
160
|
+
<DataTableContext.Provider value={ctx as DataTableContextValue}>
|
|
161
|
+
<div
|
|
162
|
+
className={cn(
|
|
163
|
+
"ui-data-table-root",
|
|
164
|
+
densityClass[density === "compact" ? "compact" : "comfortable"],
|
|
165
|
+
className,
|
|
166
|
+
)}
|
|
167
|
+
>
|
|
168
|
+
{children}
|
|
169
|
+
{!hasContent && <DataTable.Content />}
|
|
170
|
+
</div>
|
|
171
|
+
</DataTableContext.Provider>
|
|
172
|
+
);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
// ── Toolbar ────────────────────────────────────────────────────────────
|
|
176
|
+
|
|
177
|
+
DataTable.Toolbar = function DataTableToolbar({
|
|
178
|
+
children,
|
|
179
|
+
className,
|
|
180
|
+
}: {
|
|
181
|
+
children?: React.ReactNode;
|
|
182
|
+
className?: string;
|
|
183
|
+
}) {
|
|
184
|
+
return <div className={cn("ui-data-table-toolbar", className)}>{children}</div>;
|
|
185
|
+
};
|
|
186
|
+
(DataTable.Toolbar as React.FC).displayName = "DataTable.Toolbar";
|
|
187
|
+
|
|
188
|
+
// ── SelectAll header checkbox ──────────────────────────────────────────
|
|
189
|
+
|
|
190
|
+
DataTable.SelectAll = function DataTableSelectAll() {
|
|
191
|
+
const { allSelected, someSelected, toggleSelectAll, selectable } = useDataTableContext();
|
|
192
|
+
const { t } = useTranslation();
|
|
193
|
+
if (!selectable) return null;
|
|
194
|
+
return (
|
|
195
|
+
<input
|
|
196
|
+
type="checkbox"
|
|
197
|
+
checked={allSelected}
|
|
198
|
+
ref={(el) => {
|
|
199
|
+
if (el) el.indeterminate = someSelected;
|
|
200
|
+
}}
|
|
201
|
+
onChange={toggleSelectAll}
|
|
202
|
+
aria-label={t("dataTable.selectAll")}
|
|
203
|
+
/>
|
|
204
|
+
);
|
|
205
|
+
};
|
|
206
|
+
(DataTable.SelectAll as React.FC).displayName = "DataTable.SelectAll";
|
|
207
|
+
|
|
208
|
+
// ── BulkActions — visible when selection > 0 ───────────────────────────
|
|
209
|
+
|
|
210
|
+
interface BulkActionsProps {
|
|
211
|
+
count?: number;
|
|
212
|
+
children?: React.ReactNode;
|
|
213
|
+
className?: string;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
DataTable.BulkActions = function DataTableBulkActions({
|
|
217
|
+
count,
|
|
218
|
+
children,
|
|
219
|
+
className,
|
|
220
|
+
}: BulkActionsProps) {
|
|
221
|
+
const ctx = useOptionalDataTableContext();
|
|
222
|
+
const { t } = useTranslation();
|
|
223
|
+
const c = count ?? ctx?.selected.size ?? 0;
|
|
224
|
+
if (c === 0) return null;
|
|
225
|
+
return (
|
|
226
|
+
<div
|
|
227
|
+
role="region"
|
|
228
|
+
aria-label={t("dataTable.bulkActions")}
|
|
229
|
+
className={cn("ui-data-table-bulk", className)}
|
|
230
|
+
>
|
|
231
|
+
<span className="text-muted-foreground">
|
|
232
|
+
<strong className="text-foreground">{t("common.selectedCount", { count: c })}</strong>
|
|
233
|
+
</span>
|
|
234
|
+
<div className="ui-data-table-bulk-actions">{children}</div>
|
|
235
|
+
</div>
|
|
236
|
+
);
|
|
237
|
+
};
|
|
238
|
+
(DataTable.BulkActions as React.FC).displayName = "DataTable.BulkActions";
|
|
239
|
+
|
|
240
|
+
// ── Density toggle ────────────────────────────────────────────────────
|
|
241
|
+
|
|
242
|
+
DataTable.DensityToggle = function DataTableDensityToggle() {
|
|
243
|
+
const { density, setDensity } = useDataTableContext();
|
|
244
|
+
const { t } = useTranslation();
|
|
245
|
+
const next: Density = density === "compact" ? "comfortable" : "compact";
|
|
246
|
+
const Icon = density === "compact" ? Layers : Layers2;
|
|
247
|
+
const nextLabel =
|
|
248
|
+
next === "compact" ? t("dataTable.densityCompact") : t("dataTable.densityComfortable");
|
|
249
|
+
return (
|
|
250
|
+
<Button
|
|
251
|
+
variant="ghost"
|
|
252
|
+
size="sm"
|
|
253
|
+
onClick={() => {
|
|
254
|
+
setDensity(next);
|
|
255
|
+
}}
|
|
256
|
+
aria-label={t("dataTable.densitySwitch", { density: nextLabel })}
|
|
257
|
+
>
|
|
258
|
+
<Inline gap="xs">
|
|
259
|
+
<Icon className="size-4" aria-hidden="true" />
|
|
260
|
+
{density === "compact" ? t("dataTable.densityCompact") : t("dataTable.densityComfortable")}
|
|
261
|
+
</Inline>
|
|
262
|
+
</Button>
|
|
263
|
+
);
|
|
264
|
+
};
|
|
265
|
+
(DataTable.DensityToggle as React.FC).displayName = "DataTable.DensityToggle";
|
|
266
|
+
|
|
267
|
+
// ── Content (the actual table) ─────────────────────────────────────────
|
|
268
|
+
|
|
269
|
+
DataTable.Content = function DataTableContent() {
|
|
270
|
+
const {
|
|
271
|
+
data,
|
|
272
|
+
columns,
|
|
273
|
+
density: _density,
|
|
274
|
+
selectable,
|
|
275
|
+
selected,
|
|
276
|
+
toggleSelect,
|
|
277
|
+
getRowId,
|
|
278
|
+
onRowClick,
|
|
279
|
+
sort,
|
|
280
|
+
onSortChange,
|
|
281
|
+
} = useDataTableContext();
|
|
282
|
+
const { t } = useTranslation();
|
|
283
|
+
|
|
284
|
+
const rowPadding = tableRowHeightClass;
|
|
285
|
+
const cellPadding = tableCellPaddingClass;
|
|
286
|
+
|
|
287
|
+
const onHeaderClick = (col: ColumnDef<unknown>) => {
|
|
288
|
+
if (!col.sortable || !onSortChange) return;
|
|
289
|
+
if (sort?.key !== col.key) {
|
|
290
|
+
onSortChange({ key: col.key, direction: "asc" });
|
|
291
|
+
} else if (sort.direction === "asc") {
|
|
292
|
+
onSortChange({ key: col.key, direction: "desc" });
|
|
293
|
+
} else {
|
|
294
|
+
onSortChange(undefined);
|
|
295
|
+
}
|
|
296
|
+
};
|
|
297
|
+
|
|
298
|
+
return (
|
|
299
|
+
<div className="ui-data-table-scroll">
|
|
300
|
+
<div className="ui-data-table-surface min-w-[640px] sm:min-w-0">
|
|
301
|
+
<Table>
|
|
302
|
+
<TableHeader className="bg-secondary sticky top-0 z-10">
|
|
303
|
+
<TableRow>
|
|
304
|
+
{selectable && (
|
|
305
|
+
<TableHead className="w-10">
|
|
306
|
+
<DataTable.SelectAll />
|
|
307
|
+
</TableHead>
|
|
308
|
+
)}
|
|
309
|
+
{columns.map((col) => (
|
|
310
|
+
<TableHead
|
|
311
|
+
key={col.key}
|
|
312
|
+
className={cn(
|
|
313
|
+
col.width,
|
|
314
|
+
col.align === "right" && "text-right",
|
|
315
|
+
col.align === "center" && "text-center",
|
|
316
|
+
col.sortable && onSortChange && "cursor-pointer select-none",
|
|
317
|
+
)}
|
|
318
|
+
onClick={() => {
|
|
319
|
+
onHeaderClick(col);
|
|
320
|
+
}}
|
|
321
|
+
>
|
|
322
|
+
<span className="ui-data-table-sort-label">
|
|
323
|
+
{col.header}
|
|
324
|
+
{col.sortable &&
|
|
325
|
+
onSortChange &&
|
|
326
|
+
(sort?.key === col.key ? (
|
|
327
|
+
sort.direction === "asc" ? (
|
|
328
|
+
<ArrowUp className="size-3" aria-hidden="true" />
|
|
329
|
+
) : (
|
|
330
|
+
<ArrowDown className="size-3" aria-hidden="true" />
|
|
331
|
+
)
|
|
332
|
+
) : (
|
|
333
|
+
<ChevronsUpDown
|
|
334
|
+
className="text-muted-foreground size-3"
|
|
335
|
+
aria-hidden="true"
|
|
336
|
+
/>
|
|
337
|
+
))}
|
|
338
|
+
</span>
|
|
339
|
+
</TableHead>
|
|
340
|
+
))}
|
|
341
|
+
</TableRow>
|
|
342
|
+
</TableHeader>
|
|
343
|
+
<TableBody>
|
|
344
|
+
{data.map((row) => {
|
|
345
|
+
const id = getRowId(row);
|
|
346
|
+
const isSelected = selected.has(id);
|
|
347
|
+
return (
|
|
348
|
+
<TableRow
|
|
349
|
+
key={id}
|
|
350
|
+
data-state={isSelected ? "selected" : undefined}
|
|
351
|
+
onClick={(e) => {
|
|
352
|
+
// Don't trigger row click if user clicked on an interactive child.
|
|
353
|
+
const target = e.target as HTMLElement;
|
|
354
|
+
if (target.closest("button, a, input, select, textarea, [role=menuitem]"))
|
|
355
|
+
return;
|
|
356
|
+
onRowClick?.(row);
|
|
357
|
+
}}
|
|
358
|
+
className={cn(
|
|
359
|
+
rowPadding,
|
|
360
|
+
onRowClick && "hover:bg-muted/50 cursor-pointer",
|
|
361
|
+
isSelected && "bg-muted/30",
|
|
362
|
+
)}
|
|
363
|
+
>
|
|
364
|
+
{selectable && (
|
|
365
|
+
<TableCell className={cellPadding}>
|
|
366
|
+
<input
|
|
367
|
+
type="checkbox"
|
|
368
|
+
checked={isSelected}
|
|
369
|
+
onChange={() => {
|
|
370
|
+
toggleSelect(id);
|
|
371
|
+
}}
|
|
372
|
+
aria-label={t("dataTable.selectRow", { id })}
|
|
373
|
+
onClick={(e) => {
|
|
374
|
+
e.stopPropagation();
|
|
375
|
+
}}
|
|
376
|
+
/>
|
|
377
|
+
</TableCell>
|
|
378
|
+
)}
|
|
379
|
+
{columns.map((col) => (
|
|
380
|
+
<TableCell
|
|
381
|
+
key={col.key}
|
|
382
|
+
className={cn(
|
|
383
|
+
cellPadding,
|
|
384
|
+
col.width,
|
|
385
|
+
col.align === "right" && "text-right",
|
|
386
|
+
col.align === "center" && "text-center",
|
|
387
|
+
)}
|
|
388
|
+
>
|
|
389
|
+
{col.render
|
|
390
|
+
? col.render(row)
|
|
391
|
+
: (() => {
|
|
392
|
+
const v = (row as Record<string, unknown>)[col.key];
|
|
393
|
+
if (v == null) return "—";
|
|
394
|
+
if (typeof v === "string" || typeof v === "number") return String(v);
|
|
395
|
+
return "—";
|
|
396
|
+
})()}
|
|
397
|
+
</TableCell>
|
|
398
|
+
))}
|
|
399
|
+
</TableRow>
|
|
400
|
+
);
|
|
401
|
+
})}
|
|
402
|
+
</TableBody>
|
|
403
|
+
</Table>
|
|
404
|
+
</div>
|
|
405
|
+
</div>
|
|
406
|
+
);
|
|
407
|
+
};
|
|
408
|
+
(DataTable.Content as React.FC).displayName = "DataTable.Content";
|
|
409
|
+
|
|
410
|
+
// ── Pagination ─────────────────────────────────────────────────────────
|
|
411
|
+
|
|
412
|
+
interface PaginationProps {
|
|
413
|
+
cursor?: string;
|
|
414
|
+
hasMore: boolean;
|
|
415
|
+
onChange: (cursor: string | undefined) => void;
|
|
416
|
+
className?: string;
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
DataTable.Pagination = function DataTablePagination({
|
|
420
|
+
cursor,
|
|
421
|
+
hasMore,
|
|
422
|
+
onChange,
|
|
423
|
+
className,
|
|
424
|
+
}: PaginationProps) {
|
|
425
|
+
const { t } = useTranslation();
|
|
426
|
+
return (
|
|
427
|
+
<div className={cn("ui-data-table-pagination", className)}>
|
|
428
|
+
<Button
|
|
429
|
+
variant="outline"
|
|
430
|
+
size="sm"
|
|
431
|
+
disabled={!cursor}
|
|
432
|
+
onClick={() => {
|
|
433
|
+
onChange(undefined);
|
|
434
|
+
}}
|
|
435
|
+
>
|
|
436
|
+
{t("common.first")}
|
|
437
|
+
</Button>
|
|
438
|
+
<Button
|
|
439
|
+
variant="outline"
|
|
440
|
+
size="sm"
|
|
441
|
+
disabled={!hasMore}
|
|
442
|
+
onClick={() => {
|
|
443
|
+
onChange(cursor);
|
|
444
|
+
}}
|
|
445
|
+
>
|
|
446
|
+
{t("common.next")}
|
|
447
|
+
</Button>
|
|
448
|
+
</div>
|
|
449
|
+
);
|
|
450
|
+
};
|
|
451
|
+
(DataTable.Pagination as React.FC).displayName = "DataTable.Pagination";
|
|
452
|
+
|
|
453
|
+
// ── More-actions dropdown trigger (kebab) ──────────────────────────────
|
|
454
|
+
|
|
455
|
+
interface RowActionsProps {
|
|
456
|
+
ariaLabel?: string;
|
|
457
|
+
children: React.ReactNode;
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
/** Kebab menu trigger for per-row actions. Wrap children in a DropdownMenu in
|
|
461
|
+
* the consumer — this is just the trigger button shape. */
|
|
462
|
+
DataTable.RowActions = function DataTableRowActions({ ariaLabel, children }: RowActionsProps) {
|
|
463
|
+
const { t } = useTranslation();
|
|
464
|
+
return (
|
|
465
|
+
<Button
|
|
466
|
+
variant="ghost"
|
|
467
|
+
size="icon"
|
|
468
|
+
aria-label={ariaLabel ?? t("dataTable.rowActions")}
|
|
469
|
+
className={controlIconSmClass}
|
|
470
|
+
>
|
|
471
|
+
<MoreHorizontal className="size-4" aria-hidden="true" />
|
|
472
|
+
{children}
|
|
473
|
+
</Button>
|
|
474
|
+
);
|
|
475
|
+
};
|
|
476
|
+
(DataTable.RowActions as React.FC).displayName = "DataTable.RowActions";
|