@butinapp/ui 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.
Files changed (112) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +21 -0
  3. package/dist/components/badge.d.ts +7 -0
  4. package/dist/components/badge.js +34 -0
  5. package/dist/components/button.d.ts +11 -0
  6. package/dist/components/button.js +30 -0
  7. package/dist/components/card.d.ts +8 -0
  8. package/dist/components/card.js +10 -0
  9. package/dist/components/checkbox.d.ts +3 -0
  10. package/dist/components/checkbox.js +7 -0
  11. package/dist/components/combobox.d.ts +14 -0
  12. package/dist/components/combobox.js +53 -0
  13. package/dist/components/data-table-model.d.ts +40 -0
  14. package/dist/components/data-table-model.js +105 -0
  15. package/dist/components/data-table.d.ts +33 -0
  16. package/dist/components/data-table.js +252 -0
  17. package/dist/components/dialog.d.ts +11 -0
  18. package/dist/components/dialog.js +15 -0
  19. package/dist/components/dropdown-menu.d.ts +7 -0
  20. package/dist/components/dropdown-menu.js +15 -0
  21. package/dist/components/echart.d.ts +5 -0
  22. package/dist/components/echart.js +27 -0
  23. package/dist/components/input.d.ts +3 -0
  24. package/dist/components/input.js +5 -0
  25. package/dist/components/label.d.ts +3 -0
  26. package/dist/components/label.js +5 -0
  27. package/dist/components/progress.d.ts +4 -0
  28. package/dist/components/progress.js +8 -0
  29. package/dist/components/select.d.ts +11 -0
  30. package/dist/components/select.js +5 -0
  31. package/dist/components/separator.d.ts +3 -0
  32. package/dist/components/separator.js +5 -0
  33. package/dist/components/service-icon.d.ts +19 -0
  34. package/dist/components/service-icon.js +49 -0
  35. package/dist/components/sheet.d.ts +10 -0
  36. package/dist/components/sheet.js +11 -0
  37. package/dist/components/skeleton.d.ts +2 -0
  38. package/dist/components/skeleton.js +4 -0
  39. package/dist/components/sparkline.d.ts +7 -0
  40. package/dist/components/sparkline.js +22 -0
  41. package/dist/components/switch.d.ts +3 -0
  42. package/dist/components/switch.js +5 -0
  43. package/dist/components/tabs.d.ts +8 -0
  44. package/dist/components/tabs.js +15 -0
  45. package/dist/components/tooltip.d.ts +6 -0
  46. package/dist/components/tooltip.js +8 -0
  47. package/dist/components/truncated-cell.d.ts +3 -0
  48. package/dist/components/truncated-cell.js +22 -0
  49. package/dist/dashboard.d.ts +12 -0
  50. package/dist/dashboard.js +13 -0
  51. package/dist/features/charts.d.ts +7 -0
  52. package/dist/features/charts.js +4 -0
  53. package/dist/features/dashboard/dashboard-renderer.d.ts +32 -0
  54. package/dist/features/dashboard/dashboard-renderer.js +322 -0
  55. package/dist/features/dashboard/format-role.d.ts +2 -0
  56. package/dist/features/dashboard/format-role.js +26 -0
  57. package/dist/features/dashboard/overview-model.d.ts +59 -0
  58. package/dist/features/dashboard/overview-model.js +170 -0
  59. package/dist/features/dashboard/overview.d.ts +8 -0
  60. package/dist/features/dashboard/overview.js +136 -0
  61. package/dist/features/dashboard/plan-views.d.ts +7 -0
  62. package/dist/features/dashboard/plan-views.js +29 -0
  63. package/dist/features/dashboard/rollup.d.ts +31 -0
  64. package/dist/features/dashboard/rollup.js +45 -0
  65. package/dist/features/dashboard/timeseries-chart.d.ts +11 -0
  66. package/dist/features/dashboard/timeseries-chart.js +191 -0
  67. package/dist/features/dashboard/view-models.d.ts +72 -0
  68. package/dist/features/dashboard/view-models.js +291 -0
  69. package/dist/features/documents/doc-models.d.ts +1 -0
  70. package/dist/features/documents/doc-models.js +17 -0
  71. package/dist/features/documents/open-file-button.d.ts +6 -0
  72. package/dist/features/documents/open-file-button.js +16 -0
  73. package/dist/features/shell/app-shell.d.ts +13 -0
  74. package/dist/features/shell/app-shell.js +22 -0
  75. package/dist/features/shell/conn-state.d.ts +10 -0
  76. package/dist/features/shell/conn-state.js +29 -0
  77. package/dist/features/shell/service-tabs.d.ts +13 -0
  78. package/dist/features/shell/service-tabs.js +20 -0
  79. package/dist/features/shell/sidebar.d.ts +31 -0
  80. package/dist/features/shell/sidebar.js +31 -0
  81. package/dist/features/shell/theme-toggle.d.ts +5 -0
  82. package/dist/features/shell/theme-toggle.js +12 -0
  83. package/dist/i18n/context.d.ts +16 -0
  84. package/dist/i18n/context.js +19 -0
  85. package/dist/i18n/format-context.d.ts +7 -0
  86. package/dist/i18n/format-context.js +6 -0
  87. package/dist/i18n/format.d.ts +10 -0
  88. package/dist/i18n/format.js +31 -0
  89. package/dist/i18n/index.d.ts +9 -0
  90. package/dist/i18n/index.js +8 -0
  91. package/dist/i18n/labels.d.ts +400 -0
  92. package/dist/i18n/labels.js +828 -0
  93. package/dist/i18n/plugin-text.d.ts +4 -0
  94. package/dist/i18n/plugin-text.js +150 -0
  95. package/dist/index.d.ts +6 -0
  96. package/dist/index.js +15 -0
  97. package/dist/lib/echarts-theme.d.ts +38 -0
  98. package/dist/lib/echarts-theme.js +59 -0
  99. package/dist/lib/format.d.ts +4 -0
  100. package/dist/lib/format.js +18 -0
  101. package/dist/lib/monthly-buckets.d.ts +9 -0
  102. package/dist/lib/monthly-buckets.js +32 -0
  103. package/dist/lib/utils.d.ts +2 -0
  104. package/dist/lib/utils.js +3 -0
  105. package/dist/primitives.d.ts +28 -0
  106. package/dist/primitives.js +26 -0
  107. package/dist/shell.d.ts +8 -0
  108. package/dist/shell.js +10 -0
  109. package/dist/theme.css +137 -0
  110. package/dist/types.d.ts +11 -0
  111. package/dist/types.js +6 -0
  112. package/package.json +78 -0
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Yann Allard
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,21 @@
1
+ # @butinapp/ui
2
+
3
+ The embeddable, theme-portable design system + generic data-view renderer for [Butin](https://butin.app). Pure, prop-driven React — no Electron, no data-fetching.
4
+
5
+ - `@butinapp/ui/primitives` — `button`, `card`, `input`, `badge`, `sheet`, … + `cn`
6
+ - `@butinapp/ui/dashboard` — `DashboardRenderer` · `Overview` · view-models · charts
7
+ - `@butinapp/ui/shell` — embeddable app scaffolding (`AppShell`, `Sidebar`, `ServiceTabs`, `ThemeToggle`)
8
+ - `@butinapp/ui/i18n` — en/fr label contract + formatters
9
+ - `@butinapp/ui/theme.css` — the shared brand tokens + `@theme` + dark variant
10
+
11
+ ## CSS (Tailwind host)
12
+
13
+ `@butinapp/ui` ships **no compiled CSS**. Your app's single Tailwind build generates the utilities and imports the tokens:
14
+
15
+ ```css
16
+ @import 'tailwindcss';
17
+ @import '@butinapp/ui/theme.css';
18
+ @source '../node_modules/@butinapp/ui/dist/**/*.js';
19
+ ```
20
+
21
+ `react` / `react-dom` are peer dependencies. MIT licensed.
@@ -0,0 +1,7 @@
1
+ import { type VariantProps } from 'class-variance-authority';
2
+ import * as React from 'react';
3
+ declare const badgeVariants: (props?: ({
4
+ variant?: "cyan" | "danger" | "default" | "destructive" | "fuchsia" | "indigo" | "info" | "neutral" | "outline" | "pink" | "purple" | "rose" | "secondary" | "success" | "teal" | "violet" | "warning" | null | undefined;
5
+ } & import("class-variance-authority/types").ClassProp) | undefined) => string;
6
+ declare const Badge: ({ className, variant, ...props }: React.ComponentProps<'span'> & VariantProps<typeof badgeVariants>) => React.JSX.Element;
7
+ export { Badge, badgeVariants };
@@ -0,0 +1,34 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import { cva } from 'class-variance-authority';
3
+ import * as React from 'react';
4
+ import { cn } from '../lib/utils.js';
5
+ const badgeVariants = cva('inline-flex items-center justify-center rounded-md border px-2 py-0.5 text-xs font-medium whitespace-nowrap shrink-0 gap-1 [&>svg]:size-3 transition-[color,box-shadow] overflow-hidden', {
6
+ variants: {
7
+ variant: {
8
+ default: 'border-transparent bg-primary text-primary-foreground',
9
+ secondary: 'border-transparent bg-secondary text-secondary-foreground',
10
+ destructive: 'border-transparent bg-destructive text-white',
11
+ outline: 'text-foreground',
12
+ // Soft-fill tones: low-alpha fill + colored text, dark/light aware. The sentiment set
13
+ // (neutral/success/warning/danger/info) colors a role:'status' value. The categorical set below sits
14
+ // in the purple→pink→blue arc — deliberately clear of the sentiment hues — so a role:'category' badge
15
+ // never reads as good/bad; the renderer assigns one per value.
16
+ neutral: 'border-transparent bg-muted text-muted-foreground',
17
+ success: 'border-transparent bg-emerald-500/15 text-emerald-700 dark:text-emerald-300',
18
+ warning: 'border-transparent bg-amber-500/15 text-amber-700 dark:text-amber-300',
19
+ danger: 'border-transparent bg-red-500/15 text-red-700 dark:text-red-300',
20
+ info: 'border-transparent bg-sky-500/15 text-sky-700 dark:text-sky-300',
21
+ indigo: 'border-transparent bg-indigo-500/15 text-indigo-700 dark:text-indigo-300',
22
+ violet: 'border-transparent bg-violet-500/15 text-violet-700 dark:text-violet-300',
23
+ purple: 'border-transparent bg-purple-500/15 text-purple-700 dark:text-purple-300',
24
+ fuchsia: 'border-transparent bg-fuchsia-500/15 text-fuchsia-700 dark:text-fuchsia-300',
25
+ pink: 'border-transparent bg-pink-500/15 text-pink-700 dark:text-pink-300',
26
+ cyan: 'border-transparent bg-cyan-500/15 text-cyan-700 dark:text-cyan-300',
27
+ teal: 'border-transparent bg-teal-500/15 text-teal-700 dark:text-teal-300',
28
+ rose: 'border-transparent bg-rose-500/15 text-rose-700 dark:text-rose-300'
29
+ }
30
+ },
31
+ defaultVariants: { variant: 'default' }
32
+ });
33
+ const Badge = ({ className, variant, ...props }) => (_jsx("span", { "data-slot": "badge", className: cn(badgeVariants({ variant }), className), ...props }));
34
+ export { Badge, badgeVariants };
@@ -0,0 +1,11 @@
1
+ import { type VariantProps } from 'class-variance-authority';
2
+ import * as React from 'react';
3
+ declare const buttonVariants: (props?: ({
4
+ variant?: "default" | "destructive" | "ghost" | "link" | "outline" | "secondary" | null | undefined;
5
+ size?: "default" | "icon" | "lg" | "sm" | "xs" | null | undefined;
6
+ } & import("class-variance-authority/types").ClassProp) | undefined) => string;
7
+ type ButtonProps = React.ComponentProps<'button'> & VariantProps<typeof buttonVariants> & {
8
+ asChild?: boolean;
9
+ };
10
+ declare const Button: ({ className, variant, size, asChild, ref, ...props }: ButtonProps) => React.JSX.Element;
11
+ export { Button, buttonVariants };
@@ -0,0 +1,30 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import { cva } from 'class-variance-authority';
3
+ import { Slot } from 'radix-ui';
4
+ import * as React from 'react';
5
+ import { cn } from '../lib/utils.js';
6
+ const buttonVariants = cva("inline-flex shrink-0 cursor-pointer items-center justify-center gap-2 rounded-md text-sm font-medium whitespace-nowrap transition-all outline-none focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 disabled:pointer-events-none disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4", {
7
+ variants: {
8
+ variant: {
9
+ default: 'bg-primary text-primary-foreground hover:bg-primary/90',
10
+ destructive: 'bg-destructive text-white hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:bg-destructive/60 dark:focus-visible:ring-destructive/40',
11
+ outline: 'border bg-background shadow-xs hover:bg-accent hover:text-accent-foreground dark:border-input dark:bg-input/30 dark:hover:bg-input/50',
12
+ secondary: 'bg-secondary text-secondary-foreground hover:bg-secondary/80',
13
+ ghost: 'hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50',
14
+ link: 'text-primary underline-offset-4 hover:underline'
15
+ },
16
+ size: {
17
+ default: 'h-9 px-4 py-2 has-[>svg]:px-3',
18
+ xs: "h-6 gap-1 rounded-md px-2 text-xs has-[>svg]:px-1.5 [&_svg:not([class*='size-'])]:size-3",
19
+ sm: 'h-8 gap-1.5 rounded-md px-3 has-[>svg]:px-2.5',
20
+ lg: 'h-10 rounded-md px-6 has-[>svg]:px-4',
21
+ icon: 'size-9'
22
+ }
23
+ },
24
+ defaultVariants: { variant: 'default', size: 'default' }
25
+ });
26
+ const Button = ({ className, variant = 'default', size = 'default', asChild = false, ref, ...props }) => {
27
+ const Comp = asChild ? Slot.Root : 'button';
28
+ return _jsx(Comp, { ref: ref, "data-slot": "button", className: cn(buttonVariants({ variant, size, className })), ...props });
29
+ };
30
+ export { Button, buttonVariants };
@@ -0,0 +1,8 @@
1
+ import * as React from 'react';
2
+ declare const Card: ({ className, ...props }: React.ComponentProps<'div'>) => React.JSX.Element;
3
+ declare const CardHeader: ({ className, ...props }: React.ComponentProps<'div'>) => React.JSX.Element;
4
+ declare const CardTitle: ({ className, ...props }: React.ComponentProps<'div'>) => React.JSX.Element;
5
+ declare const CardDescription: ({ className, ...props }: React.ComponentProps<'div'>) => React.JSX.Element;
6
+ declare const CardContent: ({ className, ...props }: React.ComponentProps<'div'>) => React.JSX.Element;
7
+ declare const CardFooter: ({ className, ...props }: React.ComponentProps<'div'>) => React.JSX.Element;
8
+ export { Card, CardHeader, CardTitle, CardDescription, CardContent, CardFooter };
@@ -0,0 +1,10 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import * as React from 'react';
3
+ import { cn } from '../lib/utils.js';
4
+ const Card = ({ className, ...props }) => (_jsx("div", { "data-slot": "card", className: cn('bg-card text-card-foreground flex flex-col gap-6 rounded-xl border py-6 shadow-sm', className), ...props }));
5
+ const CardHeader = ({ className, ...props }) => (_jsx("div", { "data-slot": "card-header", className: cn('flex flex-col gap-1.5 px-6', className), ...props }));
6
+ const CardTitle = ({ className, ...props }) => (_jsx("div", { "data-slot": "card-title", className: cn('leading-none font-semibold', className), ...props }));
7
+ const CardDescription = ({ className, ...props }) => (_jsx("div", { "data-slot": "card-description", className: cn('text-muted-foreground text-sm', className), ...props }));
8
+ const CardContent = ({ className, ...props }) => (_jsx("div", { "data-slot": "card-content", className: cn('px-6', className), ...props }));
9
+ const CardFooter = ({ className, ...props }) => (_jsx("div", { "data-slot": "card-footer", className: cn('flex items-center px-6', className), ...props }));
10
+ export { Card, CardHeader, CardTitle, CardDescription, CardContent, CardFooter };
@@ -0,0 +1,3 @@
1
+ import { Checkbox as CheckboxPrimitive } from 'radix-ui';
2
+ import type { ComponentProps } from 'react';
3
+ export declare const Checkbox: ({ className, ...props }: ComponentProps<typeof CheckboxPrimitive.Root>) => import("react").JSX.Element;
@@ -0,0 +1,7 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import { Check, Minus } from 'lucide-react';
3
+ import { Checkbox as CheckboxPrimitive } from 'radix-ui';
4
+ import { cn } from '../lib/utils.js';
5
+ // Themed checkbox (radix). Semantic tokens only, so it inherits the host theme.
6
+ // `checked="indeterminate"` renders a dash (partial selection — e.g. a header that selects all rows).
7
+ export const Checkbox = ({ className, ...props }) => (_jsx(CheckboxPrimitive.Root, { "data-slot": "checkbox", className: cn('peer border-input data-[state=checked]:bg-primary data-[state=checked]:border-primary data-[state=checked]:text-primary-foreground data-[state=indeterminate]:bg-primary data-[state=indeterminate]:border-primary data-[state=indeterminate]:text-primary-foreground focus-visible:ring-ring/50 size-4 shrink-0 cursor-pointer rounded-[4px] border shadow-sm outline-none transition-colors focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50', className), ...props, children: _jsx(CheckboxPrimitive.Indicator, { className: "flex items-center justify-center text-current", children: props.checked === 'indeterminate' ? (_jsx(Minus, { className: "size-3.5", strokeWidth: 3 })) : (_jsx(Check, { className: "size-3.5", strokeWidth: 3 })) }) }));
@@ -0,0 +1,14 @@
1
+ import type { ConfigOption } from '@butinapp/sdk';
2
+ export interface ComboboxProps {
3
+ id?: string;
4
+ value: string;
5
+ onValueChange: (value: string) => void;
6
+ options: ConfigOption[];
7
+ loading?: boolean;
8
+ disabled?: boolean;
9
+ placeholder?: string;
10
+ emptyHint?: string;
11
+ onOpen?: () => void;
12
+ allowFreeText?: boolean;
13
+ }
14
+ export declare const Combobox: ({ id, value, onValueChange, options, loading, disabled, placeholder, emptyHint, onOpen, allowFreeText }: ComboboxProps) => import("react").JSX.Element;
@@ -0,0 +1,53 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { Check, ChevronsUpDown, Loader2, Search, Sparkles } from 'lucide-react';
3
+ import { Popover as PopoverPrimitive } from 'radix-ui';
4
+ import { useMemo, useRef, useState } from 'react';
5
+ import { cn } from '../lib/utils.js';
6
+ export const Combobox = ({ id, value, onValueChange, options, loading, disabled, placeholder, emptyHint, onOpen, allowFreeText = true }) => {
7
+ const [open, setOpen] = useState(false);
8
+ const [query, setQuery] = useState('');
9
+ const inputRef = useRef(null);
10
+ const selected = options.find((o) => o.value === value);
11
+ // What the trigger shows: the matched option's label, else the raw stored value (a cold relaunch shows the
12
+ // id until the list loads), else the placeholder.
13
+ const triggerText = selected?.label ?? (value || placeholder);
14
+ const filtered = useMemo(() => {
15
+ const q = query.trim().toLowerCase();
16
+ if (!q) {
17
+ return options;
18
+ }
19
+ return options.filter((o) => o.label.toLowerCase().includes(q) || o.value.toLowerCase().includes(q));
20
+ }, [options, query]);
21
+ // The typed query is usable as a literal value (paste-an-id fallback) when it matches no option's value.
22
+ const freeText = query.trim();
23
+ const showFreeText = allowFreeText && freeText.length > 0 && !options.some((o) => o.value === freeText);
24
+ const pick = (v) => {
25
+ onValueChange(v);
26
+ setQuery('');
27
+ setOpen(false);
28
+ };
29
+ const onOpenChange = (next) => {
30
+ setOpen(next);
31
+ if (next) {
32
+ onOpen?.();
33
+ }
34
+ else {
35
+ setQuery('');
36
+ }
37
+ };
38
+ return (_jsxs(PopoverPrimitive.Root, { open: open, onOpenChange: onOpenChange, children: [_jsxs(PopoverPrimitive.Trigger, { id: id, disabled: disabled, className: cn('border-input dark:bg-input/30 flex h-8 w-full items-center justify-between gap-2 rounded-md border bg-transparent px-3 py-1 text-sm shadow-xs outline-none', 'focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] disabled:pointer-events-none disabled:opacity-50'), children: [_jsx("span", { className: cn('truncate', !selected && !value && 'text-muted-foreground'), children: triggerText }), _jsx(ChevronsUpDown, { className: "size-4 shrink-0 opacity-60" })] }), _jsx(PopoverPrimitive.Portal, { children: _jsxs(PopoverPrimitive.Content, { align: "start", sideOffset: 4, onOpenAutoFocus: (e) => {
39
+ e.preventDefault();
40
+ inputRef.current?.focus();
41
+ }, className: cn('bg-popover text-popover-foreground relative z-50 w-[var(--radix-popover-trigger-width)] overflow-hidden rounded-md border shadow-md', 'data-[state=open]:animate-in data-[state=open]:fade-in-0 data-[state=closed]:animate-out data-[state=closed]:fade-out-0'), children: [_jsxs("div", { className: "flex items-center gap-2 border-b px-2.5", children: [_jsx(Search, { className: "size-3.5 shrink-0 opacity-60" }), _jsx("input", { ref: inputRef, value: query, onChange: (e) => setQuery(e.target.value), onKeyDown: (e) => {
42
+ if (e.key === 'Enter') {
43
+ e.preventDefault();
44
+ // Enter takes the first match, else the free-text literal — so "type an id, hit Enter" works.
45
+ if (filtered[0]) {
46
+ pick(filtered[0].value);
47
+ }
48
+ else if (showFreeText) {
49
+ pick(freeText);
50
+ }
51
+ }
52
+ }, placeholder: placeholder ?? 'Search…', className: "placeholder:text-muted-foreground h-9 w-full bg-transparent text-sm outline-none" })] }), _jsxs("div", { className: "max-h-64 overflow-auto p-1", children: [loading ? (_jsxs("div", { className: "text-muted-foreground flex items-center gap-2 px-2 py-3 text-xs", children: [_jsx(Loader2, { className: "size-3.5 animate-spin" }), " Loading\u2026"] })) : filtered.length === 0 && !showFreeText ? (_jsx("div", { className: "text-muted-foreground px-2 py-3 text-xs", children: emptyHint ?? 'No matches.' })) : null, filtered.map((o) => (_jsxs("button", { type: "button", onClick: () => pick(o.value), className: "hover:bg-accent hover:text-accent-foreground flex w-full items-start gap-2 rounded-sm px-2 py-1.5 text-left text-sm outline-none", children: [_jsx(Check, { className: cn('mt-0.5 size-4 shrink-0', o.value === value ? 'opacity-100' : 'opacity-0') }), _jsxs("span", { className: "min-w-0 flex-1", children: [_jsxs("span", { className: "flex items-center gap-1.5", children: [_jsx("span", { className: "truncate", children: o.label }), o.recommended ? (_jsxs("span", { className: "text-primary inline-flex items-center gap-0.5 text-[10px] font-medium", children: [_jsx(Sparkles, { className: "size-3" }), " suggested"] })) : null] }), o.description ? (_jsx("span", { className: "text-muted-foreground block truncate text-xs", children: o.description })) : null] })] }, o.value))), showFreeText ? (_jsxs("button", { type: "button", onClick: () => pick(freeText), className: "hover:bg-accent hover:text-accent-foreground flex w-full items-center gap-2 rounded-sm px-2 py-1.5 text-left text-sm outline-none", children: [_jsx(Check, { className: "size-4 shrink-0 opacity-0" }), _jsxs("span", { className: "truncate", children: ["Use ", _jsx("span", { className: "font-mono", children: freeText })] })] })) : null] })] }) })] }));
53
+ };
@@ -0,0 +1,40 @@
1
+ import type { ReactNode } from 'react';
2
+ export type DataTableColumn<Row> = {
3
+ key: string;
4
+ label: string;
5
+ align?: 'left' | 'right';
6
+ noWrap?: boolean;
7
+ grow?: boolean;
8
+ minWidth?: string;
9
+ sortable?: boolean;
10
+ hideable?: boolean;
11
+ cell?: (row: Row) => ReactNode;
12
+ sortValue?: (row: Row) => string | number | undefined;
13
+ exportValue?: (row: Row) => string | number | undefined;
14
+ };
15
+ export type DataTableState = {
16
+ sort?: {
17
+ key: string;
18
+ dir: 'asc' | 'desc';
19
+ };
20
+ columnOrder?: string[];
21
+ columnVisibility?: Record<string, boolean>;
22
+ pageSize?: number;
23
+ };
24
+ export type ExportFormat = 'csv' | 'json';
25
+ export type ExportBlob = {
26
+ text: string;
27
+ mime: string;
28
+ ext: ExportFormat;
29
+ };
30
+ export declare const formatRows: <Row>(columns: DataTableColumn<Row>[], rows: Row[], format: ExportFormat) => ExportBlob;
31
+ export declare const sortRows: <Row>(rows: Row[], sort: {
32
+ key: string;
33
+ dir: 'asc' | 'desc';
34
+ }, columns: DataTableColumn<Row>[]) => Row[];
35
+ export declare const selection: {
36
+ toggle: (set: Set<string>, id: string) => Set<string>;
37
+ setGroup: (set: Set<string>, ids: string[], on: boolean) => Set<string>;
38
+ allOn: (set: Set<string>, ids: string[]) => boolean;
39
+ };
40
+ export declare const normalizeTablePrefs: (prefs: DataTableState | undefined, columnKeys: string[]) => DataTableState;
@@ -0,0 +1,105 @@
1
+ const exportValueOf = (col, row) => col.exportValue ? col.exportValue(row) : row[col.key];
2
+ // One field of a CSV record. Quote when it contains a comma, quote, or newline; double embedded quotes.
3
+ const csvCell = (value) => {
4
+ if (value === undefined || value === null) {
5
+ return '';
6
+ }
7
+ const s = String(value);
8
+ return /[",\n]/.test(s) ? `"${s.replace(/"/g, '""')}"` : s;
9
+ };
10
+ // Format the GIVEN columns + rows to a downloadable blob. The caller passes only the columns it wants
11
+ // exported (visible, in display order) and only the rows it wants (current sort/filter, all pages).
12
+ export const formatRows = (columns, rows, format) => {
13
+ if (format === 'json') {
14
+ const objects = rows.map((row) => {
15
+ const o = {};
16
+ for (const col of columns) {
17
+ const v = exportValueOf(col, row);
18
+ o[col.key] = v === undefined ? null : v;
19
+ }
20
+ return o;
21
+ });
22
+ return { text: JSON.stringify(objects, null, 2), mime: 'application/json', ext: 'json' };
23
+ }
24
+ const header = columns.map((c) => csvCell(c.label)).join(',');
25
+ const body = rows.map((row) => columns.map((c) => csvCell(exportValueOf(c, row))).join(','));
26
+ return { text: [header, ...body].join('\n'), mime: 'text/csv', ext: 'csv' };
27
+ };
28
+ const sortValueOf = (col, row) => {
29
+ if (!col) {
30
+ return undefined;
31
+ }
32
+ return col.sortValue ? col.sortValue(row) : row[col.key];
33
+ };
34
+ // Stable, type-aware sort. Numbers compare numerically; everything else compares as a case-insensitive
35
+ // string. undefined/null always sort last regardless of direction. Returns a new array.
36
+ export const sortRows = (rows, sort, columns) => {
37
+ const col = columns.find((c) => c.key === sort.key);
38
+ const factor = sort.dir === 'desc' ? -1 : 1;
39
+ return rows
40
+ .map((row, i) => ({ row, i }))
41
+ .sort((a, b) => {
42
+ const av = sortValueOf(col, a.row);
43
+ const bv = sortValueOf(col, b.row);
44
+ if (av === undefined || av === null) {
45
+ return bv === undefined || bv === null ? a.i - b.i : 1;
46
+ }
47
+ if (bv === undefined || bv === null) {
48
+ return -1;
49
+ }
50
+ const cmp = typeof av === 'number' && typeof bv === 'number'
51
+ ? av - bv
52
+ : String(av).localeCompare(String(bv), undefined, { sensitivity: 'accent' });
53
+ return cmp === 0 ? a.i - b.i : cmp * factor;
54
+ })
55
+ .map((x) => x.row);
56
+ };
57
+ export const selection = {
58
+ toggle: (set, id) => {
59
+ const next = new Set(set);
60
+ if (next.has(id)) {
61
+ next.delete(id);
62
+ }
63
+ else {
64
+ next.add(id);
65
+ }
66
+ return next;
67
+ },
68
+ setGroup: (set, ids, on) => {
69
+ const next = new Set(set);
70
+ ids.forEach((id) => (on ? next.add(id) : next.delete(id)));
71
+ return next;
72
+ },
73
+ allOn: (set, ids) => ids.length > 0 && ids.every((id) => set.has(id))
74
+ };
75
+ // Sanitize persisted prefs against the table's CURRENT column keys. A plugin may add/remove/rename
76
+ // columns between releases; stale references are silently dropped so a saved pref never throws or
77
+ // hides/sorts by a column that no longer exists. Never returns undefined fields.
78
+ export const normalizeTablePrefs = (prefs, columnKeys) => {
79
+ const known = new Set(columnKeys);
80
+ const out = {};
81
+ if (prefs?.sort && known.has(prefs.sort.key)) {
82
+ out.sort = prefs.sort;
83
+ }
84
+ if (prefs?.columnOrder) {
85
+ const order = prefs.columnOrder.filter((k) => known.has(k));
86
+ if (order.length > 0) {
87
+ out.columnOrder = order;
88
+ }
89
+ }
90
+ if (prefs?.columnVisibility) {
91
+ const vis = {};
92
+ for (const [k, v] of Object.entries(prefs.columnVisibility)) {
93
+ if (known.has(k)) {
94
+ vis[k] = v;
95
+ }
96
+ }
97
+ if (Object.keys(vis).length > 0) {
98
+ out.columnVisibility = vis;
99
+ }
100
+ }
101
+ if (typeof prefs?.pageSize === 'number' && prefs.pageSize > 0) {
102
+ out.pageSize = prefs.pageSize;
103
+ }
104
+ return out;
105
+ };
@@ -0,0 +1,33 @@
1
+ import { type ReactNode } from 'react';
2
+ import { type DataTableColumn, type DataTableState } from './data-table-model.js';
3
+ export type DataTableProps<Row> = {
4
+ columns: DataTableColumn<Row>[];
5
+ rows: Row[];
6
+ getRowId: (row: Row) => string;
7
+ selection?: Set<string>;
8
+ onSelectionChange?: (next: Set<string>) => void;
9
+ grouped?: {
10
+ by: (row: Row) => string;
11
+ order?: string[];
12
+ groupAction?: (key: string, ids: string[], selectedIds: string[]) => ReactNode;
13
+ };
14
+ expandable?: (row: Row) => ReactNode | null;
15
+ paginated?: boolean;
16
+ pageSizes?: number[];
17
+ initialState?: DataTableState;
18
+ onStateChange?: (next: DataTableState) => void;
19
+ onExport?: (blob: {
20
+ text: string;
21
+ mime: string;
22
+ }, filename: string) => void;
23
+ exportName?: string;
24
+ toolbar?: {
25
+ search?: boolean;
26
+ columns?: boolean;
27
+ };
28
+ toolbarExtra?: ReactNode;
29
+ actionBar?: ReactNode;
30
+ emptyLabel?: string;
31
+ searchFloating?: boolean;
32
+ };
33
+ export declare const DataTable: <Row>({ columns, rows, getRowId, selection, onSelectionChange, grouped, expandable, paginated, pageSizes, initialState, onStateChange, onExport, exportName, toolbar, toolbarExtra, actionBar, emptyLabel, searchFloating }: DataTableProps<Row>) => import("react").JSX.Element;