@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,61 @@
|
|
|
1
|
+
import { describe, expect, it, vi } from "vitest";
|
|
2
|
+
import { renderWithUi, screen, userEvent } from "@/test/render";
|
|
3
|
+
import { FilterBar, FilterGroup } from "../filter-bar";
|
|
4
|
+
import { PageHeader } from "../page-header";
|
|
5
|
+
import { Tabs, TabsContent, TabsList, TabsTrigger } from "../tabs";
|
|
6
|
+
|
|
7
|
+
describe("FilterBar", () => {
|
|
8
|
+
it("shows clear button when filters active", async () => {
|
|
9
|
+
const user = userEvent.setup();
|
|
10
|
+
const onClear = vi.fn();
|
|
11
|
+
renderWithUi(
|
|
12
|
+
<FilterBar onClear={onClear} hasActiveFilters>
|
|
13
|
+
<FilterGroup label="Status">
|
|
14
|
+
<span>Active</span>
|
|
15
|
+
</FilterGroup>
|
|
16
|
+
</FilterBar>,
|
|
17
|
+
);
|
|
18
|
+
await user.click(screen.getByRole("button", { name: /xóa bộ lọc/i }));
|
|
19
|
+
expect(onClear).toHaveBeenCalledOnce();
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
it("hides clear when hasActiveFilters=false", () => {
|
|
23
|
+
renderWithUi(
|
|
24
|
+
<FilterBar onClear={() => undefined} hasActiveFilters={false}>
|
|
25
|
+
<span>f</span>
|
|
26
|
+
</FilterBar>,
|
|
27
|
+
);
|
|
28
|
+
expect(screen.queryByRole("button", { name: /xóa bộ lọc/i })).not.toBeInTheDocument();
|
|
29
|
+
});
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
describe("PageHeader", () => {
|
|
33
|
+
it("renders title and description (legacy)", () => {
|
|
34
|
+
renderWithUi(
|
|
35
|
+
<PageHeader title="Legacy" description="Sub" actions={<button type="button">Go</button>} />,
|
|
36
|
+
);
|
|
37
|
+
expect(screen.getByRole("heading", { name: "Legacy" })).toBeInTheDocument();
|
|
38
|
+
expect(screen.getByText("Sub")).toBeInTheDocument();
|
|
39
|
+
});
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
describe("Tabs", () => {
|
|
43
|
+
it("switches tab panels", async () => {
|
|
44
|
+
const user = userEvent.setup();
|
|
45
|
+
renderWithUi(
|
|
46
|
+
<Tabs defaultValue="a">
|
|
47
|
+
<TabsList>
|
|
48
|
+
<TabsTrigger value="a">Tab A</TabsTrigger>
|
|
49
|
+
<TabsTrigger value="b">Tab B</TabsTrigger>
|
|
50
|
+
</TabsList>
|
|
51
|
+
<TabsContent value="a">Content A</TabsContent>
|
|
52
|
+
<TabsContent value="b">Content B</TabsContent>
|
|
53
|
+
</Tabs>,
|
|
54
|
+
);
|
|
55
|
+
expect(screen.getByText("Content A")).toBeInTheDocument();
|
|
56
|
+
expect(screen.getByRole("tablist")).toHaveAttribute("data-slot", "tabs-list");
|
|
57
|
+
await user.click(screen.getByRole("tab", { name: "Tab B" }));
|
|
58
|
+
expect(screen.getByRole("tab", { name: "Tab B" })).toHaveAttribute("data-slot", "tabs-trigger");
|
|
59
|
+
expect(screen.getByText("Content B")).toBeInTheDocument();
|
|
60
|
+
});
|
|
61
|
+
});
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import { describe, expect, it, vi } from "vitest";
|
|
2
|
+
import { renderWithUi, screen, userEvent } from "@/test/render";
|
|
3
|
+
import { Pagination } from "../pagination";
|
|
4
|
+
import { buildPageRange } from "../pagination-utils";
|
|
5
|
+
import { Steps } from "../steps";
|
|
6
|
+
import { TabsItems } from "../tabs-items";
|
|
7
|
+
|
|
8
|
+
describe("buildPageRange", () => {
|
|
9
|
+
it("returns all pages when total is small", () => {
|
|
10
|
+
expect(buildPageRange(2, 5)).toEqual([1, 2, 3, 4, 5]);
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
it("inserts ellipsis for large totals", () => {
|
|
14
|
+
expect(buildPageRange(5, 20)).toContain("ellipsis");
|
|
15
|
+
expect(buildPageRange(5, 20)[0]).toBe(1);
|
|
16
|
+
expect(buildPageRange(5, 20).at(-1)).toBe(20);
|
|
17
|
+
});
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
describe("Pagination", () => {
|
|
21
|
+
it("changes page on click", async () => {
|
|
22
|
+
const user = userEvent.setup();
|
|
23
|
+
const onChange = vi.fn();
|
|
24
|
+
|
|
25
|
+
renderWithUi(<Pagination current={1} total={100} pageSize={10} onChange={onChange} />);
|
|
26
|
+
|
|
27
|
+
await user.click(screen.getByRole("button", { name: /trang 2/i }));
|
|
28
|
+
expect(onChange).toHaveBeenCalledWith(2, 10);
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
it("renders simple mode", () => {
|
|
32
|
+
renderWithUi(<Pagination simple current={2} total={50} pageSize={10} />);
|
|
33
|
+
expect(screen.getByText("2 / 5")).toBeInTheDocument();
|
|
34
|
+
});
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
describe("Steps", () => {
|
|
38
|
+
it("renders step titles", () => {
|
|
39
|
+
renderWithUi(
|
|
40
|
+
<Steps
|
|
41
|
+
current={1}
|
|
42
|
+
items={[{ title: "Tạo đơn" }, { title: "Thanh toán" }, { title: "Giao hàng" }]}
|
|
43
|
+
/>,
|
|
44
|
+
);
|
|
45
|
+
expect(screen.getByText("Tạo đơn")).toBeInTheDocument();
|
|
46
|
+
expect(screen.getByText("Thanh toán")).toBeInTheDocument();
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
it("calls onChange when clickable", async () => {
|
|
50
|
+
const user = userEvent.setup();
|
|
51
|
+
const onChange = vi.fn();
|
|
52
|
+
renderWithUi(
|
|
53
|
+
<Steps current={0} onChange={onChange} items={[{ title: "A" }, { title: "B" }]} />,
|
|
54
|
+
);
|
|
55
|
+
await user.click(screen.getByText("B"));
|
|
56
|
+
expect(onChange).toHaveBeenCalledWith(1);
|
|
57
|
+
});
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
describe("TabsItems", () => {
|
|
61
|
+
it("switches panels via items API", async () => {
|
|
62
|
+
const user = userEvent.setup();
|
|
63
|
+
renderWithUi(
|
|
64
|
+
<TabsItems
|
|
65
|
+
defaultValue="orders"
|
|
66
|
+
items={[
|
|
67
|
+
{ key: "orders", label: "Đơn hàng", children: "Panel đơn" },
|
|
68
|
+
{ key: "shipments", label: "Kiện hàng", children: "Panel kiện" },
|
|
69
|
+
]}
|
|
70
|
+
/>,
|
|
71
|
+
);
|
|
72
|
+
expect(screen.getByText("Panel đơn")).toBeInTheDocument();
|
|
73
|
+
await user.click(screen.getByRole("tab", { name: "Kiện hàng" }));
|
|
74
|
+
expect(screen.getByText("Panel kiện")).toBeInTheDocument();
|
|
75
|
+
});
|
|
76
|
+
});
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { CalendarDays } from "lucide-react";
|
|
2
|
+
import { APP_DATE_FORMAT_OPTIONS, getDateFormatLabel } from "../../app/date-format-labels";
|
|
3
|
+
import { useOptionalAppContext } from "../../app/app-provider";
|
|
4
|
+
import { useTranslation } from "../../i18n/use-translation";
|
|
5
|
+
import { cn } from "../../lib/utils";
|
|
6
|
+
import type { DateFormatPickerProp } from "../../props/components/app.prop";
|
|
7
|
+
import {
|
|
8
|
+
Select,
|
|
9
|
+
SelectContent,
|
|
10
|
+
SelectItem,
|
|
11
|
+
SelectTrigger,
|
|
12
|
+
SelectValue,
|
|
13
|
+
} from "../data-entry/select";
|
|
14
|
+
|
|
15
|
+
export type {
|
|
16
|
+
DateFormatPickerProp,
|
|
17
|
+
DateFormatPickerProp as DateFormatPickerProps,
|
|
18
|
+
} from "../../props/components/app.prop";
|
|
19
|
+
|
|
20
|
+
export function DateFormatPicker({
|
|
21
|
+
className,
|
|
22
|
+
disabled,
|
|
23
|
+
id,
|
|
24
|
+
value,
|
|
25
|
+
onChange,
|
|
26
|
+
}: DateFormatPickerProp) {
|
|
27
|
+
const ctx = useOptionalAppContext();
|
|
28
|
+
const { t, locale, fallbackLocale } = useTranslation();
|
|
29
|
+
const current = value ?? ctx?.dateFormat;
|
|
30
|
+
const handleChange = onChange ?? ctx?.setDateFormat;
|
|
31
|
+
|
|
32
|
+
if (current === undefined || !handleChange) {
|
|
33
|
+
throw new Error("DateFormatPicker requires <AppProvider> or controlled value + onChange");
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
return (
|
|
37
|
+
<Select value={current} onValueChange={handleChange} disabled={disabled}>
|
|
38
|
+
<SelectTrigger
|
|
39
|
+
id={id}
|
|
40
|
+
className={cn("w-full sm:w-44", className)}
|
|
41
|
+
aria-label={t("navigation.dateFormatPicker.ariaLabel")}
|
|
42
|
+
>
|
|
43
|
+
<CalendarDays className="mr-2 size-4 shrink-0 opacity-70" aria-hidden="true" />
|
|
44
|
+
<SelectValue />
|
|
45
|
+
</SelectTrigger>
|
|
46
|
+
<SelectContent>
|
|
47
|
+
{APP_DATE_FORMAT_OPTIONS.map((option) => (
|
|
48
|
+
<SelectItem key={option.value} value={option.value}>
|
|
49
|
+
{getDateFormatLabel(option.value, locale, fallbackLocale)}
|
|
50
|
+
</SelectItem>
|
|
51
|
+
))}
|
|
52
|
+
</SelectContent>
|
|
53
|
+
</Select>
|
|
54
|
+
);
|
|
55
|
+
}
|
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu";
|
|
3
|
+
import { Check, ChevronRight } from "lucide-react";
|
|
4
|
+
import { cn } from "../../lib/utils";
|
|
5
|
+
|
|
6
|
+
export function DropdownMenu(props: React.ComponentProps<typeof DropdownMenuPrimitive.Root>) {
|
|
7
|
+
return <DropdownMenuPrimitive.Root data-slot="dropdown-menu" {...props} />;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export function DropdownMenuTrigger(
|
|
11
|
+
props: React.ComponentProps<typeof DropdownMenuPrimitive.Trigger>,
|
|
12
|
+
) {
|
|
13
|
+
return <DropdownMenuPrimitive.Trigger data-slot="dropdown-menu-trigger" {...props} />;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function DropdownMenuPortal(
|
|
17
|
+
props: React.ComponentProps<typeof DropdownMenuPrimitive.Portal>,
|
|
18
|
+
) {
|
|
19
|
+
return <DropdownMenuPrimitive.Portal data-slot="dropdown-menu-portal" {...props} />;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function DropdownMenuGroup(props: React.ComponentProps<typeof DropdownMenuPrimitive.Group>) {
|
|
23
|
+
return <DropdownMenuPrimitive.Group data-slot="dropdown-menu-group" {...props} />;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function DropdownMenuRadioGroup(
|
|
27
|
+
props: React.ComponentProps<typeof DropdownMenuPrimitive.RadioGroup>,
|
|
28
|
+
) {
|
|
29
|
+
return <DropdownMenuPrimitive.RadioGroup data-slot="dropdown-menu-radio-group" {...props} />;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function DropdownMenuSub(props: React.ComponentProps<typeof DropdownMenuPrimitive.Sub>) {
|
|
33
|
+
return <DropdownMenuPrimitive.Sub data-slot="dropdown-menu-sub" {...props} />;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export const DropdownMenuContent = React.forwardRef<
|
|
37
|
+
React.ComponentRef<typeof DropdownMenuPrimitive.Content>,
|
|
38
|
+
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Content>
|
|
39
|
+
>(({ className, sideOffset = 4, ...props }, ref) => (
|
|
40
|
+
<DropdownMenuPortal>
|
|
41
|
+
<DropdownMenuPrimitive.Content
|
|
42
|
+
ref={ref}
|
|
43
|
+
data-slot="dropdown-menu-content"
|
|
44
|
+
sideOffset={sideOffset}
|
|
45
|
+
className={cn(
|
|
46
|
+
"bg-popover text-popover-foreground data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[state=open]:animate-in data-[state=open]:fade-in-0 data-[state=open]:zoom-in-95 z-50 max-h-[var(--radix-dropdown-menu-content-available-height)] min-w-32 origin-[var(--radix-dropdown-menu-content-transform-origin)] overflow-x-hidden overflow-y-auto rounded-md border p-1 shadow-md",
|
|
47
|
+
className,
|
|
48
|
+
)}
|
|
49
|
+
{...props}
|
|
50
|
+
/>
|
|
51
|
+
</DropdownMenuPortal>
|
|
52
|
+
));
|
|
53
|
+
DropdownMenuContent.displayName = DropdownMenuPrimitive.Content.displayName;
|
|
54
|
+
|
|
55
|
+
export const DropdownMenuItem = React.forwardRef<
|
|
56
|
+
React.ComponentRef<typeof DropdownMenuPrimitive.Item>,
|
|
57
|
+
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Item> & {
|
|
58
|
+
inset?: boolean;
|
|
59
|
+
variant?: "default" | "destructive";
|
|
60
|
+
}
|
|
61
|
+
>(({ className, inset, variant = "default", ...props }, ref) => (
|
|
62
|
+
<DropdownMenuPrimitive.Item
|
|
63
|
+
ref={ref}
|
|
64
|
+
data-slot="dropdown-menu-item"
|
|
65
|
+
data-inset={inset}
|
|
66
|
+
data-variant={variant}
|
|
67
|
+
className={cn(
|
|
68
|
+
"focus:bg-accent focus:text-accent-foreground data-[variant=destructive]:text-destructive data-[variant=destructive]:focus:bg-destructive/10 data-[variant=destructive]:focus:text-destructive [&_svg:not([class*='text-'])]:text-muted-foreground relative flex cursor-default items-center gap-2 rounded-sm px-2 py-1.5 text-sm transition-colors outline-none select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 data-[inset=true]:ps-8 [&_svg]:pointer-events-none [&_svg]:shrink-0",
|
|
69
|
+
className,
|
|
70
|
+
)}
|
|
71
|
+
{...props}
|
|
72
|
+
/>
|
|
73
|
+
));
|
|
74
|
+
DropdownMenuItem.displayName = DropdownMenuPrimitive.Item.displayName;
|
|
75
|
+
|
|
76
|
+
export const DropdownMenuLabel = React.forwardRef<
|
|
77
|
+
React.ComponentRef<typeof DropdownMenuPrimitive.Label>,
|
|
78
|
+
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Label> & { inset?: boolean }
|
|
79
|
+
>(({ className, inset, ...props }, ref) => (
|
|
80
|
+
<DropdownMenuPrimitive.Label
|
|
81
|
+
ref={ref}
|
|
82
|
+
data-slot="dropdown-menu-label"
|
|
83
|
+
data-inset={inset}
|
|
84
|
+
className={cn("px-2 py-1.5 text-sm font-semibold data-[inset=true]:ps-8", className)}
|
|
85
|
+
{...props}
|
|
86
|
+
/>
|
|
87
|
+
));
|
|
88
|
+
DropdownMenuLabel.displayName = DropdownMenuPrimitive.Label.displayName;
|
|
89
|
+
|
|
90
|
+
export const DropdownMenuSeparator = React.forwardRef<
|
|
91
|
+
React.ComponentRef<typeof DropdownMenuPrimitive.Separator>,
|
|
92
|
+
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Separator>
|
|
93
|
+
>(({ className, ...props }, ref) => (
|
|
94
|
+
<DropdownMenuPrimitive.Separator
|
|
95
|
+
ref={ref}
|
|
96
|
+
data-slot="dropdown-menu-separator"
|
|
97
|
+
className={cn("bg-border -mx-1 my-1 h-px", className)}
|
|
98
|
+
{...props}
|
|
99
|
+
/>
|
|
100
|
+
));
|
|
101
|
+
DropdownMenuSeparator.displayName = DropdownMenuPrimitive.Separator.displayName;
|
|
102
|
+
|
|
103
|
+
export const DropdownMenuCheckboxItem = React.forwardRef<
|
|
104
|
+
React.ComponentRef<typeof DropdownMenuPrimitive.CheckboxItem>,
|
|
105
|
+
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.CheckboxItem>
|
|
106
|
+
>(({ className, children, checked, ...props }, ref) => (
|
|
107
|
+
<DropdownMenuPrimitive.CheckboxItem
|
|
108
|
+
ref={ref}
|
|
109
|
+
data-slot="dropdown-menu-checkbox-item"
|
|
110
|
+
className={cn(
|
|
111
|
+
"focus:bg-accent focus:text-accent-foreground relative flex cursor-default items-center gap-2 rounded-sm py-1.5 ps-8 pe-2 text-sm transition-colors outline-none select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0",
|
|
112
|
+
className,
|
|
113
|
+
)}
|
|
114
|
+
checked={checked}
|
|
115
|
+
{...props}
|
|
116
|
+
>
|
|
117
|
+
<span className="pointer-events-none absolute start-2 flex size-3.5 items-center justify-center">
|
|
118
|
+
<DropdownMenuPrimitive.ItemIndicator>
|
|
119
|
+
<Check className="size-4" aria-hidden="true" />
|
|
120
|
+
</DropdownMenuPrimitive.ItemIndicator>
|
|
121
|
+
</span>
|
|
122
|
+
{children}
|
|
123
|
+
</DropdownMenuPrimitive.CheckboxItem>
|
|
124
|
+
));
|
|
125
|
+
DropdownMenuCheckboxItem.displayName = DropdownMenuPrimitive.CheckboxItem.displayName;
|
|
126
|
+
|
|
127
|
+
export const DropdownMenuRadioItem = React.forwardRef<
|
|
128
|
+
React.ComponentRef<typeof DropdownMenuPrimitive.RadioItem>,
|
|
129
|
+
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.RadioItem>
|
|
130
|
+
>(({ className, children, ...props }, ref) => (
|
|
131
|
+
<DropdownMenuPrimitive.RadioItem
|
|
132
|
+
ref={ref}
|
|
133
|
+
data-slot="dropdown-menu-radio-item"
|
|
134
|
+
className={cn(
|
|
135
|
+
"focus:bg-accent focus:text-accent-foreground data-[state=checked]:bg-primary/10 data-[state=checked]:text-primary relative flex cursor-default items-center gap-2 rounded-sm px-2.5 py-1.5 text-sm transition-colors outline-none select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 data-[state=checked]:font-medium [&_svg]:pointer-events-none [&_svg]:shrink-0",
|
|
136
|
+
className,
|
|
137
|
+
)}
|
|
138
|
+
{...props}
|
|
139
|
+
>
|
|
140
|
+
{children}
|
|
141
|
+
</DropdownMenuPrimitive.RadioItem>
|
|
142
|
+
));
|
|
143
|
+
DropdownMenuRadioItem.displayName = DropdownMenuPrimitive.RadioItem.displayName;
|
|
144
|
+
|
|
145
|
+
export const DropdownMenuSubTrigger = React.forwardRef<
|
|
146
|
+
React.ComponentRef<typeof DropdownMenuPrimitive.SubTrigger>,
|
|
147
|
+
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.SubTrigger> & { inset?: boolean }
|
|
148
|
+
>(({ className, inset, children, ...props }, ref) => (
|
|
149
|
+
<DropdownMenuPrimitive.SubTrigger
|
|
150
|
+
ref={ref}
|
|
151
|
+
data-slot="dropdown-menu-sub-trigger"
|
|
152
|
+
data-inset={inset}
|
|
153
|
+
className={cn(
|
|
154
|
+
"focus:bg-accent focus:text-accent-foreground data-[state=open]:bg-accent data-[state=open]:text-accent-foreground [&_svg:not([class*='text-'])]:text-muted-foreground flex cursor-default items-center gap-2 rounded-sm px-2 py-1.5 text-sm transition-colors outline-none select-none data-[inset=true]:ps-8 [&_svg]:pointer-events-none [&_svg]:shrink-0",
|
|
155
|
+
className,
|
|
156
|
+
)}
|
|
157
|
+
{...props}
|
|
158
|
+
>
|
|
159
|
+
{children}
|
|
160
|
+
<ChevronRight className="ms-auto size-4" aria-hidden="true" />
|
|
161
|
+
</DropdownMenuPrimitive.SubTrigger>
|
|
162
|
+
));
|
|
163
|
+
DropdownMenuSubTrigger.displayName = DropdownMenuPrimitive.SubTrigger.displayName;
|
|
164
|
+
|
|
165
|
+
export const DropdownMenuSubContent = React.forwardRef<
|
|
166
|
+
React.ComponentRef<typeof DropdownMenuPrimitive.SubContent>,
|
|
167
|
+
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.SubContent>
|
|
168
|
+
>(({ className, ...props }, ref) => (
|
|
169
|
+
<DropdownMenuPrimitive.SubContent
|
|
170
|
+
ref={ref}
|
|
171
|
+
data-slot="dropdown-menu-sub-content"
|
|
172
|
+
className={cn(
|
|
173
|
+
"bg-popover text-popover-foreground data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[state=open]:animate-in data-[state=open]:fade-in-0 data-[state=open]:zoom-in-95 z-50 min-w-32 origin-[var(--radix-dropdown-menu-content-transform-origin)] overflow-hidden rounded-md border p-1 shadow-lg",
|
|
174
|
+
className,
|
|
175
|
+
)}
|
|
176
|
+
{...props}
|
|
177
|
+
/>
|
|
178
|
+
));
|
|
179
|
+
DropdownMenuSubContent.displayName = DropdownMenuPrimitive.SubContent.displayName;
|
|
180
|
+
|
|
181
|
+
export const DropdownMenuShortcut = ({
|
|
182
|
+
className,
|
|
183
|
+
...props
|
|
184
|
+
}: React.HTMLAttributes<HTMLSpanElement>) => (
|
|
185
|
+
<span
|
|
186
|
+
data-slot="dropdown-menu-shortcut"
|
|
187
|
+
className={cn("text-muted-foreground ms-auto text-xs tracking-widest", className)}
|
|
188
|
+
{...props}
|
|
189
|
+
/>
|
|
190
|
+
);
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { X } from "lucide-react";
|
|
2
|
+
|
|
3
|
+
import { useTranslation } from "../../i18n/use-translation";
|
|
4
|
+
import { Button } from "../general/button";
|
|
5
|
+
import { cn } from "../../lib/utils";
|
|
6
|
+
import type { FilterBarProp, FilterGroupProp } from "../../props/components/navigation.prop";
|
|
7
|
+
|
|
8
|
+
export type { FilterBarProp, FilterGroupProp } from "../../props/components/navigation.prop";
|
|
9
|
+
|
|
10
|
+
export function FilterBar({
|
|
11
|
+
onClear,
|
|
12
|
+
hasActiveFilters = true,
|
|
13
|
+
className,
|
|
14
|
+
children,
|
|
15
|
+
}: FilterBarProp) {
|
|
16
|
+
const { t } = useTranslation();
|
|
17
|
+
|
|
18
|
+
return (
|
|
19
|
+
<div className={cn("ui-filter-bar", className)}>
|
|
20
|
+
{children}
|
|
21
|
+
{onClear && hasActiveFilters && (
|
|
22
|
+
<Button variant="ghost" size="sm" onClick={onClear} className="ui-filter-clear">
|
|
23
|
+
<X aria-hidden="true" />
|
|
24
|
+
{t("common.clearFilters")}
|
|
25
|
+
</Button>
|
|
26
|
+
)}
|
|
27
|
+
</div>
|
|
28
|
+
);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function FilterGroup({ label, className, children }: FilterGroupProp) {
|
|
32
|
+
return (
|
|
33
|
+
<div className={cn("ui-stack-xs ui-filter-group", className)}>
|
|
34
|
+
<div className="ui-filter-label">{label}</div>
|
|
35
|
+
{children}
|
|
36
|
+
</div>
|
|
37
|
+
);
|
|
38
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
export { Tabs, TabsContent, TabsList, TabsTrigger } from "./tabs";
|
|
2
|
+
export { TabsItems } from "./tabs-items";
|
|
3
|
+
export { Pagination } from "./pagination";
|
|
4
|
+
export { Steps } from "./steps";
|
|
5
|
+
export { FilterBar, FilterGroup } from "./filter-bar";
|
|
6
|
+
export { PageHeader } from "./page-header";
|
|
7
|
+
export { LocalePicker } from "./locale-picker";
|
|
8
|
+
export { TimezonePicker } from "./timezone-picker";
|
|
9
|
+
export { TimeFormatPicker } from "./time-format-picker";
|
|
10
|
+
export { DateFormatPicker } from "./date-format-picker";
|
|
11
|
+
export type { BreadcrumbItem } from "./page-header";
|
|
12
|
+
export {
|
|
13
|
+
DropdownMenu,
|
|
14
|
+
DropdownMenuCheckboxItem,
|
|
15
|
+
DropdownMenuContent,
|
|
16
|
+
DropdownMenuGroup,
|
|
17
|
+
DropdownMenuItem,
|
|
18
|
+
DropdownMenuLabel,
|
|
19
|
+
DropdownMenuPortal,
|
|
20
|
+
DropdownMenuRadioGroup,
|
|
21
|
+
DropdownMenuRadioItem,
|
|
22
|
+
DropdownMenuSeparator,
|
|
23
|
+
DropdownMenuShortcut,
|
|
24
|
+
DropdownMenuSub,
|
|
25
|
+
DropdownMenuSubContent,
|
|
26
|
+
DropdownMenuSubTrigger,
|
|
27
|
+
DropdownMenuTrigger,
|
|
28
|
+
} from "./dropdown-menu";
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { Languages } from "lucide-react";
|
|
2
|
+
import { APP_LOCALES } from "../../app/types";
|
|
3
|
+
import { useOptionalAppContext } from "../../app/app-provider";
|
|
4
|
+
import { useTranslation } from "../../i18n/use-translation";
|
|
5
|
+
import { cn } from "../../lib/utils";
|
|
6
|
+
import type { LocalePickerProp } from "../../props/components/app.prop";
|
|
7
|
+
import {
|
|
8
|
+
Select,
|
|
9
|
+
SelectContent,
|
|
10
|
+
SelectItem,
|
|
11
|
+
SelectTrigger,
|
|
12
|
+
SelectValue,
|
|
13
|
+
} from "../data-entry/select";
|
|
14
|
+
|
|
15
|
+
export type {
|
|
16
|
+
LocalePickerProp,
|
|
17
|
+
LocalePickerProp as LocalePickerProps,
|
|
18
|
+
} from "../../props/components/app.prop";
|
|
19
|
+
|
|
20
|
+
export function LocalePicker({ className, disabled, id, value, onChange }: LocalePickerProp) {
|
|
21
|
+
const ctx = useOptionalAppContext();
|
|
22
|
+
const { t } = useTranslation();
|
|
23
|
+
const current = value ?? ctx?.locale;
|
|
24
|
+
const handleChange = onChange ?? ctx?.setLocale;
|
|
25
|
+
|
|
26
|
+
if (current === undefined || !handleChange) {
|
|
27
|
+
throw new Error("LocalePicker requires <AppProvider> or controlled value + onChange");
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
return (
|
|
31
|
+
<Select value={current} onValueChange={handleChange} disabled={disabled}>
|
|
32
|
+
<SelectTrigger
|
|
33
|
+
id={id}
|
|
34
|
+
className={cn("w-full sm:w-40", className)}
|
|
35
|
+
aria-label={t("navigation.localePicker.ariaLabel")}
|
|
36
|
+
>
|
|
37
|
+
<Languages className="mr-2 size-4 shrink-0 opacity-70" aria-hidden="true" />
|
|
38
|
+
<SelectValue />
|
|
39
|
+
</SelectTrigger>
|
|
40
|
+
<SelectContent>
|
|
41
|
+
{APP_LOCALES.map((code) => (
|
|
42
|
+
<SelectItem key={code} value={code}>
|
|
43
|
+
{t(`locale.${code}`)}
|
|
44
|
+
</SelectItem>
|
|
45
|
+
))}
|
|
46
|
+
</SelectContent>
|
|
47
|
+
</Select>
|
|
48
|
+
);
|
|
49
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
/** @deprecated Prefer PageContainer. Header-only legacy shell. */
|
|
2
|
+
/* eslint-disable @typescript-eslint/no-deprecated -- legacy component intentionally uses PageHeaderProp */
|
|
3
|
+
import { Link } from "react-router-dom";
|
|
4
|
+
import { ChevronRight } from "lucide-react";
|
|
5
|
+
|
|
6
|
+
import { cn } from "../../lib/utils";
|
|
7
|
+
import type { PageHeaderProp } from "../../props/components/layout.prop";
|
|
8
|
+
|
|
9
|
+
export type { PageHeaderProp } from "../../props/components/layout.prop";
|
|
10
|
+
export type { BreadcrumbItemProp as BreadcrumbItem } from "../../props/vocabulary/navigation.prop";
|
|
11
|
+
|
|
12
|
+
export function PageHeader({ title, description, breadcrumb, actions, className }: PageHeaderProp) {
|
|
13
|
+
return (
|
|
14
|
+
<header className={cn("ui-page-header", className)}>
|
|
15
|
+
{breadcrumb && breadcrumb.length > 0 && (
|
|
16
|
+
<nav aria-label="Breadcrumb" className="ui-breadcrumb">
|
|
17
|
+
<ol className="ui-breadcrumb-list">
|
|
18
|
+
{breadcrumb.map((item, i) => {
|
|
19
|
+
const isLast = i === breadcrumb.length - 1;
|
|
20
|
+
return (
|
|
21
|
+
<li key={i} className="ui-inline-xs">
|
|
22
|
+
{item.to && !isLast ? (
|
|
23
|
+
<Link to={item.to} className="hover:text-foreground hover:underline">
|
|
24
|
+
{item.label}
|
|
25
|
+
</Link>
|
|
26
|
+
) : (
|
|
27
|
+
<span
|
|
28
|
+
className={isLast ? "text-foreground" : ""}
|
|
29
|
+
aria-current={isLast ? "page" : undefined}
|
|
30
|
+
>
|
|
31
|
+
{item.label}
|
|
32
|
+
</span>
|
|
33
|
+
)}
|
|
34
|
+
{!isLast && <ChevronRight className="size-3" aria-hidden="true" />}
|
|
35
|
+
</li>
|
|
36
|
+
);
|
|
37
|
+
})}
|
|
38
|
+
</ol>
|
|
39
|
+
</nav>
|
|
40
|
+
)}
|
|
41
|
+
<div className="ui-page-header-row">
|
|
42
|
+
<div className="min-w-0">
|
|
43
|
+
<h1 className="ui-page-title">{title}</h1>
|
|
44
|
+
{description && <p className="ui-page-subtitle">{description}</p>}
|
|
45
|
+
</div>
|
|
46
|
+
{actions && <div className="ui-inline-sm shrink-0">{actions}</div>}
|
|
47
|
+
</div>
|
|
48
|
+
</header>
|
|
49
|
+
);
|
|
50
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/** Build visible page numbers with ellipsis — Ant Design pagination style. */
|
|
2
|
+
export function buildPageRange(
|
|
3
|
+
current: number,
|
|
4
|
+
totalPages: number,
|
|
5
|
+
siblingCount = 1,
|
|
6
|
+
): (number | "ellipsis")[] {
|
|
7
|
+
if (totalPages <= 1) return totalPages === 1 ? [1] : [];
|
|
8
|
+
|
|
9
|
+
const totalNumbers = siblingCount * 2 + 5;
|
|
10
|
+
if (totalPages <= totalNumbers) {
|
|
11
|
+
return Array.from({ length: totalPages }, (_, i) => i + 1);
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
const left = Math.max(current - siblingCount, 1);
|
|
15
|
+
const right = Math.min(current + siblingCount, totalPages);
|
|
16
|
+
const showLeftEllipsis = left > 2;
|
|
17
|
+
const showRightEllipsis = right < totalPages - 1;
|
|
18
|
+
|
|
19
|
+
const range: (number | "ellipsis")[] = [];
|
|
20
|
+
range.push(1);
|
|
21
|
+
|
|
22
|
+
if (showLeftEllipsis) range.push("ellipsis");
|
|
23
|
+
else for (let i = 2; i < left; i++) range.push(i);
|
|
24
|
+
|
|
25
|
+
for (let i = left; i <= right; i++) {
|
|
26
|
+
if (i !== 1 && i !== totalPages) range.push(i);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
if (showRightEllipsis) range.push("ellipsis");
|
|
30
|
+
else for (let i = right + 1; i < totalPages; i++) range.push(i);
|
|
31
|
+
|
|
32
|
+
if (totalPages > 1) range.push(totalPages);
|
|
33
|
+
|
|
34
|
+
return range;
|
|
35
|
+
}
|