open-ds 0.0.0-stage → 0.1.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/LICENSE +21 -0
- package/README.md +66 -2
- package/assets/schemes/accessible-first.json +3093 -0
- package/assets/schemes/brand-expressive.json +3124 -0
- package/assets/schemes/content-first.json +3110 -0
- package/assets/schemes/dense-data.json +3489 -0
- package/assets/schemes/grammar.json +3903 -0
- package/assets/schemes/multi-brand.json +4359 -0
- package/assets/skills/open-ds/SKILL.md +50 -0
- package/assets/skills/open-ds-build/SKILL.md +19 -0
- package/assets/skills/open-ds-change/SKILL.md +19 -0
- package/assets/skills/open-ds-choose/SKILL.md +18 -0
- package/assets/skills/open-ds-review/SKILL.md +22 -0
- package/assets/skills/open-ds-write/SKILL.md +13 -0
- package/assets/stories/accordion.stories.tsx +46 -0
- package/assets/stories/alert-dialog.stories.tsx +65 -0
- package/assets/stories/alert.stories.tsx +44 -0
- package/assets/stories/aspect-ratio.stories.tsx +37 -0
- package/assets/stories/avatar.stories.tsx +42 -0
- package/assets/stories/badge.stories.tsx +39 -0
- package/assets/stories/breadcrumb.stories.tsx +54 -0
- package/assets/stories/button.stories.tsx +46 -0
- package/assets/stories/calendar.stories.tsx +30 -0
- package/assets/stories/card.stories.tsx +50 -0
- package/assets/stories/carousel.stories.tsx +64 -0
- package/assets/stories/chart.stories.tsx +61 -0
- package/assets/stories/checkbox.stories.tsx +29 -0
- package/assets/stories/collapsible.stories.tsx +39 -0
- package/assets/stories/combobox.stories.tsx +57 -0
- package/assets/stories/command.stories.tsx +60 -0
- package/assets/stories/context-menu.stories.tsx +55 -0
- package/assets/stories/data-table.stories.tsx +93 -0
- package/assets/stories/date-picker.stories.tsx +27 -0
- package/assets/stories/dialog.stories.tsx +70 -0
- package/assets/stories/drawer.stories.tsx +62 -0
- package/assets/stories/dropdown-menu.stories.tsx +65 -0
- package/assets/stories/empty.stories.tsx +39 -0
- package/assets/stories/field.stories.tsx +42 -0
- package/assets/stories/form.stories.tsx +56 -0
- package/assets/stories/input-group.stories.tsx +41 -0
- package/assets/stories/input-otp.stories.tsx +48 -0
- package/assets/stories/input.stories.tsx +32 -0
- package/assets/stories/kbd.stories.tsx +30 -0
- package/assets/stories/label.stories.tsx +26 -0
- package/assets/stories/native-select.stories.tsx +39 -0
- package/assets/stories/navigation-menu.stories.tsx +41 -0
- package/assets/stories/pagination.stories.tsx +61 -0
- package/assets/stories/popover.stories.tsx +37 -0
- package/assets/stories/progress.stories.tsx +30 -0
- package/assets/stories/radio-group.stories.tsx +45 -0
- package/assets/stories/resizable.stories.tsx +54 -0
- package/assets/stories/scroll-area.stories.tsx +47 -0
- package/assets/stories/select.stories.tsx +57 -0
- package/assets/stories/separator.stories.tsx +28 -0
- package/assets/stories/sheet.stories.tsx +46 -0
- package/assets/stories/sidebar.stories.tsx +62 -0
- package/assets/stories/skeleton.stories.tsx +27 -0
- package/assets/stories/slider.stories.tsx +44 -0
- package/assets/stories/spinner.stories.tsx +25 -0
- package/assets/stories/switch.stories.tsx +29 -0
- package/assets/stories/table.stories.tsx +97 -0
- package/assets/stories/tabs.stories.tsx +55 -0
- package/assets/stories/textarea.stories.tsx +28 -0
- package/assets/stories/toast.stories.tsx +30 -0
- package/assets/stories/toggle-group.stories.tsx +41 -0
- package/assets/stories/toggle.stories.tsx +20 -0
- package/assets/stories/tooltip.stories.tsx +49 -0
- package/assets/tokens/accessible-first/content.json +32 -0
- package/assets/tokens/accessible-first/dark.tokens.json +581 -0
- package/assets/tokens/accessible-first/light.tokens.json +581 -0
- package/assets/tokens/accessible-first/shadcn.css +21 -0
- package/assets/tokens/accessible-first/theme.css +101 -0
- package/assets/tokens/accessible-first/tokens.css +331 -0
- package/assets/tokens/accessible-first/tokens.json +1616 -0
- package/assets/tokens/accessible-first/tokens.scoped.css +333 -0
- package/assets/tokens/brand-expressive/content.json +32 -0
- package/assets/tokens/brand-expressive/dark.tokens.json +585 -0
- package/assets/tokens/brand-expressive/light.tokens.json +585 -0
- package/assets/tokens/brand-expressive/shadcn.css +21 -0
- package/assets/tokens/brand-expressive/theme.css +101 -0
- package/assets/tokens/brand-expressive/tokens.css +331 -0
- package/assets/tokens/brand-expressive/tokens.json +1651 -0
- package/assets/tokens/brand-expressive/tokens.scoped.css +333 -0
- package/assets/tokens/content-first/content.json +32 -0
- package/assets/tokens/content-first/dark.tokens.json +582 -0
- package/assets/tokens/content-first/light.tokens.json +582 -0
- package/assets/tokens/content-first/shadcn.css +21 -0
- package/assets/tokens/content-first/theme.css +101 -0
- package/assets/tokens/content-first/tokens.css +331 -0
- package/assets/tokens/content-first/tokens.json +1631 -0
- package/assets/tokens/content-first/tokens.scoped.css +333 -0
- package/assets/tokens/dense-data/content.json +32 -0
- package/assets/tokens/dense-data/dark.tokens.json +580 -0
- package/assets/tokens/dense-data/light.tokens.json +580 -0
- package/assets/tokens/dense-data/shadcn.css +21 -0
- package/assets/tokens/dense-data/theme.css +101 -0
- package/assets/tokens/dense-data/tokens.css +331 -0
- package/assets/tokens/dense-data/tokens.json +1623 -0
- package/assets/tokens/dense-data/tokens.scoped.css +333 -0
- package/assets/tokens/index.json +143 -0
- package/assets/tokens/multi-brand/aster-dark.tokens.json +625 -0
- package/assets/tokens/multi-brand/aster-light.tokens.json +625 -0
- package/assets/tokens/multi-brand/birch-dark.tokens.json +625 -0
- package/assets/tokens/multi-brand/birch-light.tokens.json +625 -0
- package/assets/tokens/multi-brand/content.json +32 -0
- package/assets/tokens/multi-brand/coral-dark.tokens.json +625 -0
- package/assets/tokens/multi-brand/coral-light.tokens.json +625 -0
- package/assets/tokens/multi-brand/shadcn.css +21 -0
- package/assets/tokens/multi-brand/theme.css +101 -0
- package/assets/tokens/multi-brand/tokens.css +1002 -0
- package/assets/tokens/multi-brand/tokens.json +1754 -0
- package/assets/tokens/multi-brand/tokens.scoped.css +1022 -0
- package/assets/tokens/shadcn-map.json +34 -0
- package/assets/ui/components/accordion.tsx +60 -0
- package/assets/ui/components/alert-dialog.tsx +171 -0
- package/assets/ui/components/alert.tsx +56 -0
- package/assets/ui/components/aspect-ratio.tsx +17 -0
- package/assets/ui/components/avatar.tsx +96 -0
- package/assets/ui/components/badge.tsx +36 -0
- package/assets/ui/components/breadcrumb.tsx +99 -0
- package/assets/ui/components/button.tsx +53 -0
- package/assets/ui/components/calendar.tsx +169 -0
- package/assets/ui/components/card.tsx +72 -0
- package/assets/ui/components/carousel.tsx +224 -0
- package/assets/ui/components/chart.tsx +315 -0
- package/assets/ui/components/checkbox.tsx +29 -0
- package/assets/ui/components/collapsible.tsx +25 -0
- package/assets/ui/components/combobox.tsx +275 -0
- package/assets/ui/components/command.tsx +145 -0
- package/assets/ui/components/context-menu.tsx +218 -0
- package/assets/ui/components/data-table.tsx +307 -0
- package/assets/ui/components/date-picker.tsx +79 -0
- package/assets/ui/components/dialog.tsx +68 -0
- package/assets/ui/components/drawer.tsx +115 -0
- package/assets/ui/components/dropdown-menu.tsx +58 -0
- package/assets/ui/components/empty.tsx +92 -0
- package/assets/ui/components/field.tsx +234 -0
- package/assets/ui/components/form.tsx +146 -0
- package/assets/ui/components/input-group.tsx +154 -0
- package/assets/ui/components/input-otp.tsx +75 -0
- package/assets/ui/components/input.tsx +25 -0
- package/assets/ui/components/kbd.tsx +30 -0
- package/assets/ui/components/label.tsx +22 -0
- package/assets/ui/components/native-select.tsx +59 -0
- package/assets/ui/components/navigation-menu.tsx +149 -0
- package/assets/ui/components/pagination.tsx +108 -0
- package/assets/ui/components/popover.tsx +61 -0
- package/assets/ui/components/progress.tsx +31 -0
- package/assets/ui/components/radio-group.tsx +40 -0
- package/assets/ui/components/resizable.tsx +54 -0
- package/assets/ui/components/scroll-area.tsx +55 -0
- package/assets/ui/components/select.tsx +81 -0
- package/assets/ui/components/separator.tsx +35 -0
- package/assets/ui/components/sheet.tsx +114 -0
- package/assets/ui/components/sidebar.tsx +685 -0
- package/assets/ui/components/skeleton.tsx +15 -0
- package/assets/ui/components/slider.tsx +70 -0
- package/assets/ui/components/spinner.tsx +16 -0
- package/assets/ui/components/switch.tsx +32 -0
- package/assets/ui/components/table.tsx +78 -0
- package/assets/ui/components/tabs.tsx +49 -0
- package/assets/ui/components/textarea.tsx +25 -0
- package/assets/ui/components/toast.tsx +54 -0
- package/assets/ui/components/toggle-group.tsx +88 -0
- package/assets/ui/components/toggle.tsx +48 -0
- package/assets/ui/components/tooltip.tsx +34 -0
- package/assets/ui/lib/use-mobile.ts +20 -0
- package/assets/ui/lib/utils.ts +7 -0
- package/assets/ui/styles.css +27 -0
- package/bin/open-ds.js +6 -0
- package/package.json +56 -4
- package/src/agents.mjs +150 -0
- package/src/cli.mjs +445 -0
- package/src/components.mjs +69 -0
- package/src/context.mjs +143 -0
- package/src/create.mjs +377 -0
- package/src/docs.mjs +570 -0
- package/src/guidance.mjs +63 -0
- package/src/index.mjs +20 -0
- package/src/mcp.mjs +660 -0
- package/src/readmes.mjs +106 -0
- package/src/schemes.mjs +188 -0
- package/src/tokens.mjs +127 -0
- package/templates/project/.github/workflows/ci.yml +15 -0
- package/templates/project/.storybook/component-docs.mdx +26 -0
- package/templates/project/.storybook/main.ts +17 -0
- package/templates/project/.storybook/manager-head.html +3 -0
- package/templates/project/.storybook/manager.ts +37 -0
- package/templates/project/.storybook/preview-head.html +3 -0
- package/templates/project/.storybook/preview.tsx +82 -0
- package/templates/project/docs/docusaurus.config.ts +64 -0
- package/templates/project/docs/sidebars.ts +8 -0
- package/templates/project/docs/src/css/custom.css +275 -0
- package/templates/project/gitignore +6 -0
- package/templates/project/src/styles/index.css +16 -0
- package/templates/project/src/vite-env.d.ts +7 -0
- package/templates/project/stories/docs-blocks.tsx +190 -0
- package/templates/project/stories/samples.tsx +102 -0
- package/templates/project/stories/usage.tsx +31 -0
- package/templates/project/tokens/build.mjs +468 -0
- package/templates/project/tsconfig.json +15 -0
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Command. A search-first menu of actions, opened from the keyboard.
|
|
3
|
+
*
|
|
4
|
+
* From shadcn/ui (new-york-v4, MIT), carried by the schemes whose inventory names it.
|
|
5
|
+
* Reads the shadcn variables, which theme.css maps onto the scheme's semantic tokens, and the
|
|
6
|
+
* radius, shadow and text scale that shadcn.css maps onto the scheme's tokens. Dark mode comes
|
|
7
|
+
* from the tokens, so no dark: classes.
|
|
8
|
+
*/
|
|
9
|
+
"use client";
|
|
10
|
+
|
|
11
|
+
import * as React from "react";
|
|
12
|
+
import { Command as CommandPrimitive } from "cmdk";
|
|
13
|
+
import { cn } from "../lib/utils";
|
|
14
|
+
import { SearchIcon } from "lucide-react";
|
|
15
|
+
|
|
16
|
+
import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle } from "./dialog";
|
|
17
|
+
|
|
18
|
+
function Command({ className, ...props }: React.ComponentProps<typeof CommandPrimitive>) {
|
|
19
|
+
return (
|
|
20
|
+
<CommandPrimitive
|
|
21
|
+
data-slot="command"
|
|
22
|
+
className={cn(
|
|
23
|
+
"flex h-full w-full flex-col overflow-hidden rounded-md bg-popover text-popover-foreground",
|
|
24
|
+
className,
|
|
25
|
+
)}
|
|
26
|
+
{...props}
|
|
27
|
+
/>
|
|
28
|
+
);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function CommandDialog({
|
|
32
|
+
title = "Command Palette",
|
|
33
|
+
description = "Search for a command to run...",
|
|
34
|
+
children,
|
|
35
|
+
className,
|
|
36
|
+
showCloseButton = true,
|
|
37
|
+
...props
|
|
38
|
+
}: React.ComponentProps<typeof Dialog> & {
|
|
39
|
+
title?: string;
|
|
40
|
+
description?: string;
|
|
41
|
+
className?: string;
|
|
42
|
+
showCloseButton?: boolean;
|
|
43
|
+
}) {
|
|
44
|
+
return (
|
|
45
|
+
<Dialog {...props}>
|
|
46
|
+
<DialogHeader className="sr-only">
|
|
47
|
+
<DialogTitle>{title}</DialogTitle>
|
|
48
|
+
<DialogDescription>{description}</DialogDescription>
|
|
49
|
+
</DialogHeader>
|
|
50
|
+
<DialogContent className={cn("overflow-hidden p-0", className)}>
|
|
51
|
+
<Command className="**:data-[slot=command-input-wrapper]:h-12 [&_[cmdk-group-heading]]:px-2 [&_[cmdk-group-heading]]:font-medium [&_[cmdk-group-heading]]:text-muted-foreground [&_[cmdk-group]]:px-2 [&_[cmdk-group]:not([hidden])_~[cmdk-group]]:pt-0 [&_[cmdk-input-wrapper]_svg]:h-5 [&_[cmdk-input-wrapper]_svg]:w-5 [&_[cmdk-input]]:h-12 [&_[cmdk-item]]:px-2 [&_[cmdk-item]]:py-3 [&_[cmdk-item]_svg]:h-5 [&_[cmdk-item]_svg]:w-5">
|
|
52
|
+
{children}
|
|
53
|
+
</Command>
|
|
54
|
+
</DialogContent>
|
|
55
|
+
</Dialog>
|
|
56
|
+
);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function CommandInput({ className, ...props }: React.ComponentProps<typeof CommandPrimitive.Input>) {
|
|
60
|
+
return (
|
|
61
|
+
<div data-slot="command-input-wrapper" className="flex h-9 items-center gap-2 border-b px-3">
|
|
62
|
+
<SearchIcon className="size-4 shrink-0 opacity-50" />
|
|
63
|
+
<CommandPrimitive.Input
|
|
64
|
+
data-slot="command-input"
|
|
65
|
+
className={cn(
|
|
66
|
+
"flex h-10 w-full rounded-md bg-transparent py-3 text-sm outline-hidden placeholder:text-muted-foreground disabled:cursor-not-allowed disabled:opacity-50",
|
|
67
|
+
className,
|
|
68
|
+
)}
|
|
69
|
+
{...props}
|
|
70
|
+
/>
|
|
71
|
+
</div>
|
|
72
|
+
);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function CommandList({ className, ...props }: React.ComponentProps<typeof CommandPrimitive.List>) {
|
|
76
|
+
return (
|
|
77
|
+
<CommandPrimitive.List
|
|
78
|
+
data-slot="command-list"
|
|
79
|
+
className={cn("max-h-[300px] scroll-py-1 overflow-x-hidden overflow-y-auto", className)}
|
|
80
|
+
{...props}
|
|
81
|
+
/>
|
|
82
|
+
);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function CommandEmpty({ ...props }: React.ComponentProps<typeof CommandPrimitive.Empty>) {
|
|
86
|
+
return <CommandPrimitive.Empty data-slot="command-empty" className="py-6 text-center text-sm" {...props} />;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function CommandGroup({ className, ...props }: React.ComponentProps<typeof CommandPrimitive.Group>) {
|
|
90
|
+
return (
|
|
91
|
+
<CommandPrimitive.Group
|
|
92
|
+
data-slot="command-group"
|
|
93
|
+
className={cn(
|
|
94
|
+
"overflow-hidden p-1 text-foreground [&_[cmdk-group-heading]]:px-2 [&_[cmdk-group-heading]]:py-1.5 [&_[cmdk-group-heading]]:text-xs [&_[cmdk-group-heading]]:font-medium [&_[cmdk-group-heading]]:text-muted-foreground",
|
|
95
|
+
className,
|
|
96
|
+
)}
|
|
97
|
+
{...props}
|
|
98
|
+
/>
|
|
99
|
+
);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function CommandSeparator({ className, ...props }: React.ComponentProps<typeof CommandPrimitive.Separator>) {
|
|
103
|
+
return (
|
|
104
|
+
<CommandPrimitive.Separator
|
|
105
|
+
data-slot="command-separator"
|
|
106
|
+
className={cn("-mx-1 h-px bg-border", className)}
|
|
107
|
+
{...props}
|
|
108
|
+
/>
|
|
109
|
+
);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function CommandItem({ className, ...props }: React.ComponentProps<typeof CommandPrimitive.Item>) {
|
|
113
|
+
return (
|
|
114
|
+
<CommandPrimitive.Item
|
|
115
|
+
data-slot="command-item"
|
|
116
|
+
className={cn(
|
|
117
|
+
"relative flex cursor-default items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-hidden select-none data-[disabled=true]:pointer-events-none data-[disabled=true]:opacity-50 data-[selected=true]:bg-accent data-[selected=true]:text-accent-foreground [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4 [&_svg:not([class*='text-'])]:text-muted-foreground",
|
|
118
|
+
className,
|
|
119
|
+
)}
|
|
120
|
+
{...props}
|
|
121
|
+
/>
|
|
122
|
+
);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function CommandShortcut({ className, ...props }: React.ComponentProps<"span">) {
|
|
126
|
+
return (
|
|
127
|
+
<span
|
|
128
|
+
data-slot="command-shortcut"
|
|
129
|
+
className={cn("ml-auto text-xs tracking-widest text-muted-foreground", className)}
|
|
130
|
+
{...props}
|
|
131
|
+
/>
|
|
132
|
+
);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
export {
|
|
136
|
+
Command,
|
|
137
|
+
CommandDialog,
|
|
138
|
+
CommandInput,
|
|
139
|
+
CommandList,
|
|
140
|
+
CommandEmpty,
|
|
141
|
+
CommandGroup,
|
|
142
|
+
CommandItem,
|
|
143
|
+
CommandShortcut,
|
|
144
|
+
CommandSeparator,
|
|
145
|
+
};
|
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Context menu. A menu on right-click.
|
|
3
|
+
*
|
|
4
|
+
* From shadcn/ui (new-york-v4, MIT), carried by the schemes whose inventory names it.
|
|
5
|
+
* Reads the shadcn variables, which theme.css maps onto the scheme's semantic tokens, and the
|
|
6
|
+
* radius, shadow and text scale that shadcn.css maps onto the scheme's tokens. Dark mode comes
|
|
7
|
+
* from the tokens, so no dark: classes.
|
|
8
|
+
*/
|
|
9
|
+
"use client";
|
|
10
|
+
|
|
11
|
+
import * as React from "react";
|
|
12
|
+
import { cn } from "../lib/utils";
|
|
13
|
+
import { CheckIcon, ChevronRightIcon, CircleIcon } from "lucide-react";
|
|
14
|
+
import { ContextMenu as ContextMenuPrimitive } from "radix-ui";
|
|
15
|
+
|
|
16
|
+
function ContextMenu({ ...props }: React.ComponentProps<typeof ContextMenuPrimitive.Root>) {
|
|
17
|
+
return <ContextMenuPrimitive.Root data-slot="context-menu" {...props} />;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function ContextMenuTrigger({ ...props }: React.ComponentProps<typeof ContextMenuPrimitive.Trigger>) {
|
|
21
|
+
return <ContextMenuPrimitive.Trigger data-slot="context-menu-trigger" {...props} />;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function ContextMenuGroup({ ...props }: React.ComponentProps<typeof ContextMenuPrimitive.Group>) {
|
|
25
|
+
return <ContextMenuPrimitive.Group data-slot="context-menu-group" {...props} />;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function ContextMenuPortal({ ...props }: React.ComponentProps<typeof ContextMenuPrimitive.Portal>) {
|
|
29
|
+
return <ContextMenuPrimitive.Portal data-slot="context-menu-portal" {...props} />;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function ContextMenuSub({ ...props }: React.ComponentProps<typeof ContextMenuPrimitive.Sub>) {
|
|
33
|
+
return <ContextMenuPrimitive.Sub data-slot="context-menu-sub" {...props} />;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function ContextMenuRadioGroup({ ...props }: React.ComponentProps<typeof ContextMenuPrimitive.RadioGroup>) {
|
|
37
|
+
return <ContextMenuPrimitive.RadioGroup data-slot="context-menu-radio-group" {...props} />;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function ContextMenuSubTrigger({
|
|
41
|
+
className,
|
|
42
|
+
inset,
|
|
43
|
+
children,
|
|
44
|
+
...props
|
|
45
|
+
}: React.ComponentProps<typeof ContextMenuPrimitive.SubTrigger> & {
|
|
46
|
+
inset?: boolean;
|
|
47
|
+
}) {
|
|
48
|
+
return (
|
|
49
|
+
<ContextMenuPrimitive.SubTrigger
|
|
50
|
+
data-slot="context-menu-sub-trigger"
|
|
51
|
+
data-inset={inset}
|
|
52
|
+
className={cn(
|
|
53
|
+
"flex cursor-default items-center rounded-sm px-2 py-1.5 text-sm outline-hidden select-none focus:bg-accent focus:text-accent-foreground data-[inset]:pl-8 data-[state=open]:bg-accent data-[state=open]:text-accent-foreground [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4 [&_svg:not([class*='text-'])]:text-muted-foreground",
|
|
54
|
+
className,
|
|
55
|
+
)}
|
|
56
|
+
{...props}
|
|
57
|
+
>
|
|
58
|
+
{children}
|
|
59
|
+
<ChevronRightIcon className="ml-auto" />
|
|
60
|
+
</ContextMenuPrimitive.SubTrigger>
|
|
61
|
+
);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function ContextMenuSubContent({ className, ...props }: React.ComponentProps<typeof ContextMenuPrimitive.SubContent>) {
|
|
65
|
+
return (
|
|
66
|
+
<ContextMenuPrimitive.SubContent
|
|
67
|
+
data-slot="context-menu-sub-content"
|
|
68
|
+
className={cn(
|
|
69
|
+
"z-50 min-w-[8rem] origin-(--radix-context-menu-content-transform-origin) overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-lg 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",
|
|
70
|
+
className,
|
|
71
|
+
)}
|
|
72
|
+
{...props}
|
|
73
|
+
/>
|
|
74
|
+
);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function ContextMenuContent({ className, ...props }: React.ComponentProps<typeof ContextMenuPrimitive.Content>) {
|
|
78
|
+
return (
|
|
79
|
+
<ContextMenuPrimitive.Portal>
|
|
80
|
+
<ContextMenuPrimitive.Content
|
|
81
|
+
data-slot="context-menu-content"
|
|
82
|
+
className={cn(
|
|
83
|
+
"z-50 max-h-(--radix-context-menu-content-available-height) min-w-[8rem] origin-(--radix-context-menu-content-transform-origin) overflow-x-hidden overflow-y-auto rounded-md border bg-popover p-1 text-popover-foreground shadow-md 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",
|
|
84
|
+
className,
|
|
85
|
+
)}
|
|
86
|
+
{...props}
|
|
87
|
+
/>
|
|
88
|
+
</ContextMenuPrimitive.Portal>
|
|
89
|
+
);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function ContextMenuItem({
|
|
93
|
+
className,
|
|
94
|
+
inset,
|
|
95
|
+
variant = "default",
|
|
96
|
+
...props
|
|
97
|
+
}: React.ComponentProps<typeof ContextMenuPrimitive.Item> & {
|
|
98
|
+
inset?: boolean;
|
|
99
|
+
variant?: "default" | "destructive";
|
|
100
|
+
}) {
|
|
101
|
+
return (
|
|
102
|
+
<ContextMenuPrimitive.Item
|
|
103
|
+
data-slot="context-menu-item"
|
|
104
|
+
data-inset={inset}
|
|
105
|
+
data-variant={variant}
|
|
106
|
+
className={cn(
|
|
107
|
+
"relative flex cursor-default items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-hidden select-none focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50 data-[inset]:pl-8 data-[variant=destructive]:text-destructive data-[variant=destructive]:focus:bg-destructive/10 data-[variant=destructive]:focus:text-destructive [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4 [&_svg:not([class*='text-'])]:text-muted-foreground data-[variant=destructive]:*:[svg]:text-destructive!",
|
|
108
|
+
className,
|
|
109
|
+
)}
|
|
110
|
+
{...props}
|
|
111
|
+
/>
|
|
112
|
+
);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function ContextMenuCheckboxItem({
|
|
116
|
+
className,
|
|
117
|
+
children,
|
|
118
|
+
checked,
|
|
119
|
+
...props
|
|
120
|
+
}: React.ComponentProps<typeof ContextMenuPrimitive.CheckboxItem>) {
|
|
121
|
+
return (
|
|
122
|
+
<ContextMenuPrimitive.CheckboxItem
|
|
123
|
+
data-slot="context-menu-checkbox-item"
|
|
124
|
+
className={cn(
|
|
125
|
+
"relative flex cursor-default items-center gap-2 rounded-sm py-1.5 pr-2 pl-8 text-sm outline-hidden select-none focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
|
|
126
|
+
className,
|
|
127
|
+
)}
|
|
128
|
+
checked={checked}
|
|
129
|
+
{...props}
|
|
130
|
+
>
|
|
131
|
+
<span className="pointer-events-none absolute left-2 flex size-3.5 items-center justify-center">
|
|
132
|
+
<ContextMenuPrimitive.ItemIndicator>
|
|
133
|
+
<CheckIcon className="size-4" />
|
|
134
|
+
</ContextMenuPrimitive.ItemIndicator>
|
|
135
|
+
</span>
|
|
136
|
+
{children}
|
|
137
|
+
</ContextMenuPrimitive.CheckboxItem>
|
|
138
|
+
);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
function ContextMenuRadioItem({
|
|
142
|
+
className,
|
|
143
|
+
children,
|
|
144
|
+
...props
|
|
145
|
+
}: React.ComponentProps<typeof ContextMenuPrimitive.RadioItem>) {
|
|
146
|
+
return (
|
|
147
|
+
<ContextMenuPrimitive.RadioItem
|
|
148
|
+
data-slot="context-menu-radio-item"
|
|
149
|
+
className={cn(
|
|
150
|
+
"relative flex cursor-default items-center gap-2 rounded-sm py-1.5 pr-2 pl-8 text-sm outline-hidden select-none focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
|
|
151
|
+
className,
|
|
152
|
+
)}
|
|
153
|
+
{...props}
|
|
154
|
+
>
|
|
155
|
+
<span className="pointer-events-none absolute left-2 flex size-3.5 items-center justify-center">
|
|
156
|
+
<ContextMenuPrimitive.ItemIndicator>
|
|
157
|
+
<CircleIcon className="size-2 fill-current" />
|
|
158
|
+
</ContextMenuPrimitive.ItemIndicator>
|
|
159
|
+
</span>
|
|
160
|
+
{children}
|
|
161
|
+
</ContextMenuPrimitive.RadioItem>
|
|
162
|
+
);
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
function ContextMenuLabel({
|
|
166
|
+
className,
|
|
167
|
+
inset,
|
|
168
|
+
...props
|
|
169
|
+
}: React.ComponentProps<typeof ContextMenuPrimitive.Label> & {
|
|
170
|
+
inset?: boolean;
|
|
171
|
+
}) {
|
|
172
|
+
return (
|
|
173
|
+
<ContextMenuPrimitive.Label
|
|
174
|
+
data-slot="context-menu-label"
|
|
175
|
+
data-inset={inset}
|
|
176
|
+
className={cn("px-2 py-1.5 text-sm font-medium text-foreground data-[inset]:pl-8", className)}
|
|
177
|
+
{...props}
|
|
178
|
+
/>
|
|
179
|
+
);
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
function ContextMenuSeparator({ className, ...props }: React.ComponentProps<typeof ContextMenuPrimitive.Separator>) {
|
|
183
|
+
return (
|
|
184
|
+
<ContextMenuPrimitive.Separator
|
|
185
|
+
data-slot="context-menu-separator"
|
|
186
|
+
className={cn("-mx-1 my-1 h-px bg-border", className)}
|
|
187
|
+
{...props}
|
|
188
|
+
/>
|
|
189
|
+
);
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
function ContextMenuShortcut({ className, ...props }: React.ComponentProps<"span">) {
|
|
193
|
+
return (
|
|
194
|
+
<span
|
|
195
|
+
data-slot="context-menu-shortcut"
|
|
196
|
+
className={cn("ml-auto text-xs tracking-widest text-muted-foreground", className)}
|
|
197
|
+
{...props}
|
|
198
|
+
/>
|
|
199
|
+
);
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
export {
|
|
203
|
+
ContextMenu,
|
|
204
|
+
ContextMenuTrigger,
|
|
205
|
+
ContextMenuContent,
|
|
206
|
+
ContextMenuItem,
|
|
207
|
+
ContextMenuCheckboxItem,
|
|
208
|
+
ContextMenuRadioItem,
|
|
209
|
+
ContextMenuLabel,
|
|
210
|
+
ContextMenuSeparator,
|
|
211
|
+
ContextMenuShortcut,
|
|
212
|
+
ContextMenuGroup,
|
|
213
|
+
ContextMenuPortal,
|
|
214
|
+
ContextMenuSub,
|
|
215
|
+
ContextMenuSubContent,
|
|
216
|
+
ContextMenuSubTrigger,
|
|
217
|
+
ContextMenuRadioGroup,
|
|
218
|
+
};
|
|
@@ -0,0 +1,307 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Data table. A pattern for many records: search, faceted filters, sorting, column choice, row
|
|
3
|
+
* selection with bulk actions, pagination and a density switch.
|
|
4
|
+
*
|
|
5
|
+
* Built from the system's own components (table, input, button, checkbox, popover, toggle group)
|
|
6
|
+
* on TanStack Table, following shadcn/ui's Data Table guide (MIT). Reads only component keys and the
|
|
7
|
+
* shadcn variables that theme.css maps onto the scheme's semantics. Every label is a prop, so the
|
|
8
|
+
* words come from the scheme's voice.
|
|
9
|
+
*/
|
|
10
|
+
"use client";
|
|
11
|
+
|
|
12
|
+
import {
|
|
13
|
+
type ColumnDef,
|
|
14
|
+
type ColumnFiltersState,
|
|
15
|
+
type Row,
|
|
16
|
+
type SortingState,
|
|
17
|
+
type VisibilityState,
|
|
18
|
+
flexRender,
|
|
19
|
+
getCoreRowModel,
|
|
20
|
+
getFacetedUniqueValues,
|
|
21
|
+
getFilteredRowModel,
|
|
22
|
+
getPaginationRowModel,
|
|
23
|
+
getSortedRowModel,
|
|
24
|
+
useReactTable,
|
|
25
|
+
} from "@tanstack/react-table";
|
|
26
|
+
import { ArrowDown, ArrowUp, ChevronsUpDown, Columns3, ListFilter } from "lucide-react";
|
|
27
|
+
import * as React from "react";
|
|
28
|
+
import { cn } from "../lib/utils";
|
|
29
|
+
import { Button } from "./button";
|
|
30
|
+
import { Checkbox } from "./checkbox";
|
|
31
|
+
import { Input } from "./input";
|
|
32
|
+
import { Popover, PopoverContent, PopoverTrigger } from "./popover";
|
|
33
|
+
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "./table";
|
|
34
|
+
import { ToggleGroup, ToggleGroupItem } from "./toggle-group";
|
|
35
|
+
|
|
36
|
+
/** Column options read by the table: numbers align right in the data font. */
|
|
37
|
+
declare module "@tanstack/react-table" {
|
|
38
|
+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
39
|
+
interface ColumnMeta<TData, TValue> {
|
|
40
|
+
numeric?: boolean;
|
|
41
|
+
label?: string;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export interface DataTableFacet {
|
|
46
|
+
/** The column id to filter by. */
|
|
47
|
+
column: string;
|
|
48
|
+
/** The filter's button label. */
|
|
49
|
+
title: string;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export interface DataTableLabels {
|
|
53
|
+
search: string;
|
|
54
|
+
columns: string;
|
|
55
|
+
density: string;
|
|
56
|
+
compact: string;
|
|
57
|
+
comfortable: string;
|
|
58
|
+
selected: (count: number) => string;
|
|
59
|
+
noResults: string;
|
|
60
|
+
previous: string;
|
|
61
|
+
next: string;
|
|
62
|
+
page: (index: number, count: number) => string;
|
|
63
|
+
selectAll: string;
|
|
64
|
+
selectRow: string;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const LABELS: DataTableLabels = {
|
|
68
|
+
search: "Search",
|
|
69
|
+
columns: "Columns",
|
|
70
|
+
density: "Row density",
|
|
71
|
+
compact: "Compact",
|
|
72
|
+
comfortable: "Comfortable",
|
|
73
|
+
selected: (n) => `${n} selected`,
|
|
74
|
+
noResults: "No results.",
|
|
75
|
+
previous: "Previous",
|
|
76
|
+
next: "Next",
|
|
77
|
+
page: (i, n) => `Page ${i} of ${n}`,
|
|
78
|
+
selectAll: "Select all rows on this page",
|
|
79
|
+
selectRow: "Select row",
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
export interface DataTableProps<TData> {
|
|
83
|
+
columns: ColumnDef<TData, unknown>[];
|
|
84
|
+
data: TData[];
|
|
85
|
+
facets?: DataTableFacet[];
|
|
86
|
+
pageSize?: number;
|
|
87
|
+
/** Actions for the selected rows, shown when at least one row is selected. */
|
|
88
|
+
bulkActions?: (rows: TData[]) => React.ReactNode;
|
|
89
|
+
labels?: Partial<DataTableLabels>;
|
|
90
|
+
className?: string;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/** A checkbox column for row selection. Put it first. */
|
|
94
|
+
export function dataTableSelectColumn<TData>(labels: Partial<DataTableLabels> = {}): ColumnDef<TData, unknown> {
|
|
95
|
+
const l = { ...LABELS, ...labels };
|
|
96
|
+
return {
|
|
97
|
+
id: "select",
|
|
98
|
+
enableSorting: false,
|
|
99
|
+
enableHiding: false,
|
|
100
|
+
header: ({ table }) => (
|
|
101
|
+
<Checkbox
|
|
102
|
+
aria-label={l.selectAll}
|
|
103
|
+
checked={table.getIsAllPageRowsSelected() || (table.getIsSomePageRowsSelected() && "indeterminate")}
|
|
104
|
+
onCheckedChange={(v) => table.toggleAllPageRowsSelected(!!v)}
|
|
105
|
+
/>
|
|
106
|
+
),
|
|
107
|
+
cell: ({ row }) => (
|
|
108
|
+
<Checkbox aria-label={l.selectRow} checked={row.getIsSelected()} onCheckedChange={(v) => row.toggleSelected(!!v)} />
|
|
109
|
+
),
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/** A header that sorts its column on click and shows the direction. */
|
|
114
|
+
export function DataTableSortHeader({ label, sorted, onSort }: { label: string; sorted: false | "asc" | "desc"; onSort: () => void }) {
|
|
115
|
+
const Icon = sorted === "asc" ? ArrowUp : sorted === "desc" ? ArrowDown : ChevronsUpDown;
|
|
116
|
+
return (
|
|
117
|
+
<button type="button" onClick={onSort} className="inline-flex items-center gap-1 font-[inherit] hover:text-foreground">
|
|
118
|
+
{label}
|
|
119
|
+
<Icon className="size-3.5" aria-hidden />
|
|
120
|
+
</button>
|
|
121
|
+
);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function FacetFilter<TData>({ facet, table }: { facet: DataTableFacet; table: ReturnType<typeof useReactTable<TData>> }) {
|
|
125
|
+
const column = table.getColumn(facet.column);
|
|
126
|
+
if (!column) return null;
|
|
127
|
+
const counts = column.getFacetedUniqueValues();
|
|
128
|
+
const chosen = new Set((column.getFilterValue() as string[] | undefined) ?? []);
|
|
129
|
+
const toggle = (value: string) => {
|
|
130
|
+
const next = new Set(chosen);
|
|
131
|
+
if (next.has(value)) next.delete(value);
|
|
132
|
+
else next.add(value);
|
|
133
|
+
column.setFilterValue(next.size ? [...next] : undefined);
|
|
134
|
+
};
|
|
135
|
+
return (
|
|
136
|
+
<Popover>
|
|
137
|
+
<PopoverTrigger asChild>
|
|
138
|
+
<Button variant="secondary" size="compact">
|
|
139
|
+
<ListFilter /> {facet.title}
|
|
140
|
+
{chosen.size > 0 && <span className="tabular-nums">· {chosen.size}</span>}
|
|
141
|
+
</Button>
|
|
142
|
+
</PopoverTrigger>
|
|
143
|
+
<PopoverContent align="start" className="w-56 p-2">
|
|
144
|
+
<ul className="flex flex-col">
|
|
145
|
+
{[...counts.entries()]
|
|
146
|
+
.sort(([a], [b]) => String(a).localeCompare(String(b)))
|
|
147
|
+
.map(([value, count]) => (
|
|
148
|
+
<li key={String(value)}>
|
|
149
|
+
<label className="flex cursor-pointer items-center gap-2 rounded-sm px-2 py-1 hover:bg-accent">
|
|
150
|
+
<Checkbox checked={chosen.has(String(value))} onCheckedChange={() => toggle(String(value))} />
|
|
151
|
+
<span className="flex-1">{String(value)}</span>
|
|
152
|
+
<span className="text-muted-foreground tabular-nums">{count}</span>
|
|
153
|
+
</label>
|
|
154
|
+
</li>
|
|
155
|
+
))}
|
|
156
|
+
</ul>
|
|
157
|
+
</PopoverContent>
|
|
158
|
+
</Popover>
|
|
159
|
+
);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
const inFacet = <TData,>(row: Row<TData>, id: string, value: string[]) => value.includes(String(row.getValue(id)));
|
|
163
|
+
|
|
164
|
+
export function DataTable<TData>({ columns, data, facets = [], pageSize = 10, bulkActions, labels = {}, className }: DataTableProps<TData>) {
|
|
165
|
+
const l = { ...LABELS, ...labels };
|
|
166
|
+
const [sorting, setSorting] = React.useState<SortingState>([]);
|
|
167
|
+
const [filters, setFilters] = React.useState<ColumnFiltersState>([]);
|
|
168
|
+
const [visibility, setVisibility] = React.useState<VisibilityState>({});
|
|
169
|
+
const [selection, setSelection] = React.useState({});
|
|
170
|
+
const [search, setSearch] = React.useState("");
|
|
171
|
+
const [density, setDensity] = React.useState<"compact" | "comfortable">("compact");
|
|
172
|
+
|
|
173
|
+
const facetIds = new Set(facets.map((f) => f.column));
|
|
174
|
+
const table = useReactTable({
|
|
175
|
+
data,
|
|
176
|
+
columns: columns.map((c) => (c.id && facetIds.has(c.id)) || ("accessorKey" in c && facetIds.has(String(c.accessorKey))) ? { ...c, filterFn: inFacet } : c),
|
|
177
|
+
state: { sorting, columnFilters: filters, columnVisibility: visibility, rowSelection: selection, globalFilter: search },
|
|
178
|
+
onSortingChange: setSorting,
|
|
179
|
+
onColumnFiltersChange: setFilters,
|
|
180
|
+
onColumnVisibilityChange: setVisibility,
|
|
181
|
+
onRowSelectionChange: setSelection,
|
|
182
|
+
onGlobalFilterChange: setSearch,
|
|
183
|
+
getCoreRowModel: getCoreRowModel(),
|
|
184
|
+
getFilteredRowModel: getFilteredRowModel(),
|
|
185
|
+
getFacetedUniqueValues: getFacetedUniqueValues(),
|
|
186
|
+
getSortedRowModel: getSortedRowModel(),
|
|
187
|
+
getPaginationRowModel: getPaginationRowModel(),
|
|
188
|
+
initialState: { pagination: { pageSize } },
|
|
189
|
+
});
|
|
190
|
+
const selected = table.getFilteredSelectedRowModel().rows.map((r) => r.original);
|
|
191
|
+
const pages = Math.max(table.getPageCount(), 1);
|
|
192
|
+
|
|
193
|
+
return (
|
|
194
|
+
<div data-slot="data-table" className={cn("flex flex-col gap-[var(--space-gap-default)]", className)}>
|
|
195
|
+
<div className="flex flex-wrap items-center gap-[var(--space-gap-default)]">
|
|
196
|
+
<Input
|
|
197
|
+
aria-label={l.search}
|
|
198
|
+
placeholder={l.search}
|
|
199
|
+
value={search}
|
|
200
|
+
onChange={(e) => setSearch(e.target.value)}
|
|
201
|
+
className="w-56"
|
|
202
|
+
/>
|
|
203
|
+
{facets.map((f) => (
|
|
204
|
+
<FacetFilter key={f.column} facet={f} table={table} />
|
|
205
|
+
))}
|
|
206
|
+
{selected.length > 0 && bulkActions && (
|
|
207
|
+
<div className="flex items-center gap-[var(--space-gap-default)]">
|
|
208
|
+
<span className="text-muted-foreground tabular-nums">{l.selected(selected.length)}</span>
|
|
209
|
+
{bulkActions(selected)}
|
|
210
|
+
</div>
|
|
211
|
+
)}
|
|
212
|
+
<div className="ml-auto flex items-center gap-[var(--space-gap-default)]">
|
|
213
|
+
<ToggleGroup
|
|
214
|
+
type="single"
|
|
215
|
+
variant="outline"
|
|
216
|
+
size="sm"
|
|
217
|
+
aria-label={l.density}
|
|
218
|
+
value={density}
|
|
219
|
+
onValueChange={(v) => v && setDensity(v as "compact" | "comfortable")}
|
|
220
|
+
>
|
|
221
|
+
<ToggleGroupItem value="compact">{l.compact}</ToggleGroupItem>
|
|
222
|
+
<ToggleGroupItem value="comfortable">{l.comfortable}</ToggleGroupItem>
|
|
223
|
+
</ToggleGroup>
|
|
224
|
+
<Popover>
|
|
225
|
+
<PopoverTrigger asChild>
|
|
226
|
+
<Button variant="secondary" size="compact">
|
|
227
|
+
<Columns3 /> {l.columns}
|
|
228
|
+
</Button>
|
|
229
|
+
</PopoverTrigger>
|
|
230
|
+
<PopoverContent align="end" className="w-52 p-2">
|
|
231
|
+
<ul className="flex flex-col">
|
|
232
|
+
{table
|
|
233
|
+
.getAllLeafColumns()
|
|
234
|
+
.filter((c) => c.getCanHide())
|
|
235
|
+
.map((c) => (
|
|
236
|
+
<li key={c.id}>
|
|
237
|
+
<label className="flex cursor-pointer items-center gap-2 rounded-sm px-2 py-1 hover:bg-accent">
|
|
238
|
+
<Checkbox checked={c.getIsVisible()} onCheckedChange={(v) => c.toggleVisibility(!!v)} />
|
|
239
|
+
{c.columnDef.meta?.label ?? c.id}
|
|
240
|
+
</label>
|
|
241
|
+
</li>
|
|
242
|
+
))}
|
|
243
|
+
</ul>
|
|
244
|
+
</PopoverContent>
|
|
245
|
+
</Popover>
|
|
246
|
+
</div>
|
|
247
|
+
</div>
|
|
248
|
+
|
|
249
|
+
<div className="max-h-[32rem] overflow-auto rounded-[var(--card-radius)] border border-[var(--table-row-border)]">
|
|
250
|
+
<Table>
|
|
251
|
+
<TableHeader className="sticky top-0 z-10">
|
|
252
|
+
{table.getHeaderGroups().map((group) => (
|
|
253
|
+
<TableRow key={group.id}>
|
|
254
|
+
{group.headers.map((h) => (
|
|
255
|
+
<TableHead key={h.id} className={cn(h.column.columnDef.meta?.numeric && "text-right")}>
|
|
256
|
+
{h.isPlaceholder ? null : h.column.getCanSort() ? (
|
|
257
|
+
<DataTableSortHeader
|
|
258
|
+
label={h.column.columnDef.meta?.label ?? String(h.column.columnDef.header ?? h.id)}
|
|
259
|
+
sorted={h.column.getIsSorted()}
|
|
260
|
+
onSort={h.column.getToggleSortingHandler() as () => void}
|
|
261
|
+
/>
|
|
262
|
+
) : (
|
|
263
|
+
flexRender(h.column.columnDef.header, h.getContext())
|
|
264
|
+
)}
|
|
265
|
+
</TableHead>
|
|
266
|
+
))}
|
|
267
|
+
</TableRow>
|
|
268
|
+
))}
|
|
269
|
+
</TableHeader>
|
|
270
|
+
<TableBody>
|
|
271
|
+
{table.getRowModel().rows.length ? (
|
|
272
|
+
table.getRowModel().rows.map((row) => (
|
|
273
|
+
<TableRow
|
|
274
|
+
key={row.id}
|
|
275
|
+
data-state={row.getIsSelected() ? "selected" : undefined}
|
|
276
|
+
className={density === "comfortable" ? "h-[calc(var(--table-row-height)*1.5)]" : undefined}
|
|
277
|
+
>
|
|
278
|
+
{row.getVisibleCells().map((cell) => (
|
|
279
|
+
<TableCell key={cell.id} numeric={cell.column.columnDef.meta?.numeric}>
|
|
280
|
+
{flexRender(cell.column.columnDef.cell, cell.getContext())}
|
|
281
|
+
</TableCell>
|
|
282
|
+
))}
|
|
283
|
+
</TableRow>
|
|
284
|
+
))
|
|
285
|
+
) : (
|
|
286
|
+
<TableRow>
|
|
287
|
+
<TableCell colSpan={table.getVisibleLeafColumns().length} className="text-center text-muted-foreground">
|
|
288
|
+
{l.noResults}
|
|
289
|
+
</TableCell>
|
|
290
|
+
</TableRow>
|
|
291
|
+
)}
|
|
292
|
+
</TableBody>
|
|
293
|
+
</Table>
|
|
294
|
+
</div>
|
|
295
|
+
|
|
296
|
+
<div className="flex items-center justify-end gap-[var(--space-gap-default)]">
|
|
297
|
+
<span className="text-muted-foreground tabular-nums">{l.page(table.getState().pagination.pageIndex + 1, pages)}</span>
|
|
298
|
+
<Button variant="secondary" size="compact" onClick={() => table.previousPage()} disabled={!table.getCanPreviousPage()}>
|
|
299
|
+
{l.previous}
|
|
300
|
+
</Button>
|
|
301
|
+
<Button variant="secondary" size="compact" onClick={() => table.nextPage()} disabled={!table.getCanNextPage()}>
|
|
302
|
+
{l.next}
|
|
303
|
+
</Button>
|
|
304
|
+
</div>
|
|
305
|
+
</div>
|
|
306
|
+
);
|
|
307
|
+
}
|