@waveinno/ui 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/CHANGELOG.md +35 -0
- package/LICENSE +5 -0
- package/README.md +64 -2
- package/dist/brand/waveinno-logo.svg +1 -0
- package/dist/components/brand/index.d.ts +3 -0
- package/dist/components/brand/index.js +1 -0
- package/dist/components/brand/logo.d.ts +89 -0
- package/dist/components/brand/logo.js +1 -0
- package/dist/components/brand/waveinno-logo.d.ts +52 -0
- package/dist/components/brand/waveinno-logo.js +1 -0
- package/dist/components/charts/bar-chart.d.ts +20 -0
- package/dist/components/charts/bar-chart.js +1 -0
- package/dist/components/charts/bar-list.d.ts +27 -0
- package/dist/components/charts/bar-list.js +1 -0
- package/dist/components/charts/chart-card.d.ts +32 -0
- package/dist/components/charts/chart-card.js +1 -0
- package/dist/components/charts/donut-chart.d.ts +27 -0
- package/dist/components/charts/donut-chart.js +1 -0
- package/dist/components/charts/gauge-chart.d.ts +21 -0
- package/dist/components/charts/gauge-chart.js +1 -0
- package/dist/components/charts/heatmap.d.ts +23 -0
- package/dist/components/charts/heatmap.js +1 -0
- package/dist/components/charts/index.d.ts +10 -0
- package/dist/components/charts/index.js +1 -0
- package/dist/components/charts/line-chart.d.ts +24 -0
- package/dist/components/charts/line-chart.js +1 -0
- package/dist/components/charts/palette.d.ts +39 -0
- package/dist/components/charts/palette.js +1 -0
- package/dist/components/charts/trend-chart.d.ts +22 -0
- package/dist/components/charts/trend-chart.js +1 -0
- package/dist/components/patterns/activity-feed.d.ts +28 -0
- package/dist/components/patterns/activity-feed.js +1 -0
- package/dist/components/patterns/data-table-cells.d.ts +39 -0
- package/dist/components/patterns/data-table-cells.js +1 -0
- package/dist/components/patterns/data-table-edit.d.ts +56 -0
- package/dist/components/patterns/data-table-edit.js +1 -0
- package/dist/components/patterns/data-table-filters.d.ts +84 -0
- package/dist/components/patterns/data-table-filters.js +1 -0
- package/dist/components/patterns/data-table.d.ts +76 -0
- package/dist/components/patterns/data-table.js +2 -0
- package/dist/components/patterns/index.d.ts +16 -0
- package/dist/components/patterns/index.js +1 -0
- package/dist/components/patterns/kanban.d.ts +28 -0
- package/dist/components/patterns/kanban.js +1 -0
- package/dist/components/patterns/kpi-card.d.ts +22 -0
- package/dist/components/patterns/kpi-card.js +1 -0
- package/dist/components/patterns/money.d.ts +28 -0
- package/dist/components/patterns/money.js +1 -0
- package/dist/components/patterns/page-header.d.ts +38 -0
- package/dist/components/patterns/page-header.js +1 -0
- package/dist/components/patterns/page-hero.d.ts +50 -0
- package/dist/components/patterns/page-hero.js +1 -0
- package/dist/components/patterns/page-shell.d.ts +27 -0
- package/dist/components/patterns/page-shell.js +1 -0
- package/dist/components/patterns/stat-card.d.ts +56 -0
- package/dist/components/patterns/stat-card.js +1 -0
- package/dist/components/patterns/states.d.ts +27 -0
- package/dist/components/patterns/states.js +1 -0
- package/dist/components/patterns/timeline.d.ts +100 -0
- package/dist/components/patterns/timeline.js +1 -0
- package/dist/components/ui/avatar.d.ts +37 -0
- package/dist/components/ui/avatar.js +1 -0
- package/dist/components/ui/badge.d.ts +41 -0
- package/dist/components/ui/badge.js +1 -0
- package/dist/components/ui/button.d.ts +25 -0
- package/dist/components/ui/button.js +1 -0
- package/dist/components/ui/calendar.d.ts +42 -0
- package/dist/components/ui/calendar.js +1 -0
- package/dist/components/ui/card.d.ts +11 -0
- package/dist/components/ui/card.js +1 -0
- package/dist/components/ui/checkbox.d.ts +3 -0
- package/dist/components/ui/checkbox.js +1 -0
- package/dist/components/ui/combobox.d.ts +47 -0
- package/dist/components/ui/combobox.js +1 -0
- package/dist/components/ui/confirm-dialog.d.ts +34 -0
- package/dist/components/ui/confirm-dialog.js +1 -0
- package/dist/components/ui/date-picker.d.ts +80 -0
- package/dist/components/ui/date-picker.js +1 -0
- package/dist/components/ui/dialog.d.ts +54 -0
- package/dist/components/ui/dialog.js +1 -0
- package/dist/components/ui/dropdown-menu.d.ts +44 -0
- package/dist/components/ui/dropdown-menu.js +1 -0
- package/dist/components/ui/form-field.d.ts +27 -0
- package/dist/components/ui/form-field.js +1 -0
- package/dist/components/ui/index.d.ts +26 -0
- package/dist/components/ui/index.js +1 -0
- package/dist/components/ui/input.d.ts +21 -0
- package/dist/components/ui/input.js +1 -0
- package/dist/components/ui/label.d.ts +5 -0
- package/dist/components/ui/label.js +1 -0
- package/dist/components/ui/popover.d.ts +22 -0
- package/dist/components/ui/popover.js +1 -0
- package/dist/components/ui/progress.d.ts +43 -0
- package/dist/components/ui/progress.js +1 -0
- package/dist/components/ui/segmented.d.ts +42 -0
- package/dist/components/ui/segmented.js +1 -0
- package/dist/components/ui/select.d.ts +10 -0
- package/dist/components/ui/select.js +1 -0
- package/dist/components/ui/separator.d.ts +3 -0
- package/dist/components/ui/separator.js +1 -0
- package/dist/components/ui/sheet.d.ts +19 -0
- package/dist/components/ui/sheet.js +1 -0
- package/dist/components/ui/skeleton.d.ts +10 -0
- package/dist/components/ui/skeleton.js +1 -0
- package/dist/components/ui/switch.d.ts +3 -0
- package/dist/components/ui/switch.js +1 -0
- package/dist/components/ui/tabs.d.ts +19 -0
- package/dist/components/ui/tabs.js +1 -0
- package/dist/components/ui/time-picker.d.ts +52 -0
- package/dist/components/ui/time-picker.js +1 -0
- package/dist/components/ui/tooltip.d.ts +13 -0
- package/dist/components/ui/tooltip.js +1 -0
- package/dist/hooks/index.d.ts +4 -0
- package/dist/hooks/index.js +1 -0
- package/dist/hooks/use-hotkey.d.ts +16 -0
- package/dist/hooks/use-hotkey.js +1 -0
- package/dist/hooks/use-media-query.d.ts +4 -0
- package/dist/hooks/use-media-query.js +1 -0
- package/dist/hooks/use-mounted.d.ts +2 -0
- package/dist/hooks/use-mounted.js +1 -0
- package/dist/index.d.ts +17 -0
- package/dist/index.js +1 -0
- package/dist/lib/boot-script.d.ts +15 -0
- package/dist/lib/boot-script.js +12 -0
- package/dist/lib/currency.d.ts +55 -0
- package/dist/lib/currency.js +1 -0
- package/dist/lib/format.d.ts +28 -0
- package/dist/lib/format.js +1 -0
- package/dist/lib/index.d.ts +5 -0
- package/dist/lib/index.js +1 -0
- package/dist/lib/utils.d.ts +7 -0
- package/dist/lib/utils.js +1 -0
- package/dist/shell/app-shell.d.ts +18 -0
- package/dist/shell/app-shell.js +1 -0
- package/dist/shell/app-switcher.d.ts +9 -0
- package/dist/shell/app-switcher.js +1 -0
- package/dist/shell/appearance-controls.d.ts +13 -0
- package/dist/shell/appearance-controls.js +1 -0
- package/dist/shell/command-palette.d.ts +9 -0
- package/dist/shell/command-palette.js +1 -0
- package/dist/shell/config.d.ts +66 -0
- package/dist/shell/config.js +1 -0
- package/dist/shell/icon-rail.d.ts +2 -0
- package/dist/shell/icon-rail.js +1 -0
- package/dist/shell/index.d.ts +7 -0
- package/dist/shell/index.js +1 -0
- package/dist/shell/mobile-nav.d.ts +7 -0
- package/dist/shell/mobile-nav.js +1 -0
- package/dist/shell/module-sidebar.d.ts +9 -0
- package/dist/shell/module-sidebar.js +1 -0
- package/dist/shell/notifications.d.ts +5 -0
- package/dist/shell/notifications.js +1 -0
- package/dist/shell/placeholder-screen.d.ts +11 -0
- package/dist/shell/placeholder-screen.js +1 -0
- package/dist/shell/record-drawer.d.ts +32 -0
- package/dist/shell/record-drawer.js +1 -0
- package/dist/shell/registry.d.ts +24 -0
- package/dist/shell/registry.js +1 -0
- package/dist/shell/topbar.d.ts +9 -0
- package/dist/shell/topbar.js +1 -0
- package/dist/shell/use-active.d.ts +21 -0
- package/dist/shell/use-active.js +1 -0
- package/dist/shell/user-menu.d.ts +5 -0
- package/dist/shell/user-menu.js +1 -0
- package/dist/store/app-store.d.ts +10 -0
- package/dist/store/app-store.js +1 -0
- package/dist/store/drawer-store.d.ts +22 -0
- package/dist/store/drawer-store.js +1 -0
- package/dist/store/index.d.ts +5 -0
- package/dist/store/index.js +1 -0
- package/dist/store/settings-store.d.ts +22 -0
- package/dist/store/settings-store.js +1 -0
- package/dist/store/ui-store.d.ts +28 -0
- package/dist/store/ui-store.js +1 -0
- package/dist/styles.css +658 -0
- package/dist/types.d.ts +140 -0
- package/dist/types.js +0 -0
- package/package.json +98 -4
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
import { type SeriesMeta } from "./chart-card";
|
|
3
|
+
export interface TrendSeries extends SeriesMeta {
|
|
4
|
+
key: string;
|
|
5
|
+
}
|
|
6
|
+
/**
|
|
7
|
+
* Change over time — area chart.
|
|
8
|
+
*
|
|
9
|
+
* One y-axis, always. Two measures of different scale get two charts or an
|
|
10
|
+
* index to a common base; a second axis invites the reader to compare slopes
|
|
11
|
+
* that have no shared unit, which is the most common chart mistake there is.
|
|
12
|
+
*
|
|
13
|
+
* Marks are thin: 2px strokes, no dots at rest, a faint fill that carries
|
|
14
|
+
* magnitude without competing with the line.
|
|
15
|
+
*/
|
|
16
|
+
export declare function TrendChart({ data, xKey, series, format, stacked, }: {
|
|
17
|
+
data: Record<string, string | number>[];
|
|
18
|
+
xKey: string;
|
|
19
|
+
series: TrendSeries[];
|
|
20
|
+
format?: (v: number) => string;
|
|
21
|
+
stacked?: boolean;
|
|
22
|
+
}): React.JSX.Element;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"use client";import{jsx as t,jsxs as s}from"react/jsx-runtime";import{Area as m,AreaChart as u,CartesianGrid as b,ResponsiveContainer as g,Tooltip as h,XAxis as v,YAxis as S}from"recharts";import{ChartTooltip as x}from"./chart-card.js";import{AXIS as i}from"./palette.js";function w({data:d,xKey:c,series:a,format:o=e=>String(e),stacked:k}){return t(g,{width:"100%",height:"100%",children:s(u,{data:d,margin:{top:6,right:8,bottom:0,left:-18},children:[t("defs",{children:a.map(e=>s("linearGradient",{id:`fill-${e.key}`,x1:"0",y1:"0",x2:"0",y2:"1",children:[t("stop",{offset:"0%",stopColor:e.color,stopOpacity:.22}),t("stop",{offset:"100%",stopColor:e.color,stopOpacity:.02})]},e.key))}),t(b,{vertical:!1,stroke:i.grid}),t(v,{dataKey:c,tickLine:!1,axisLine:!1,tickMargin:8,tick:{fill:i.tick,fontSize:i.tickSize}}),t(S,{tickLine:!1,axisLine:!1,width:64,tickFormatter:e=>o(e),tick:{fill:i.tick,fontSize:i.tickSize}}),t(h,{cursor:{stroke:"var(--border2)",strokeWidth:1},content:e=>{const{active:f,payload:n,label:y}=e;return!f||!n?.length?null:t(x,{label:y,rows:n.map(r=>{const l=a.find(p=>p.key===r.dataKey);return{id:r.dataKey,label:l?.label??r.dataKey,value:o(r.value),color:l?.color??i.tick}})})}}),a.map(e=>t(m,{type:"monotone",dataKey:e.key,name:e.label,stackId:k?"1":void 0,stroke:e.color,strokeWidth:2,fill:`url(#fill-${e.key})`,dot:!1,activeDot:{r:4,strokeWidth:2,stroke:"var(--raised)"},isAnimationActive:!0,animationDuration:600},e.key))]})})}export{w as TrendChart};
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
import type { Tone } from "../../types";
|
|
3
|
+
export interface FeedItem {
|
|
4
|
+
id: string;
|
|
5
|
+
tone: Tone;
|
|
6
|
+
title: string;
|
|
7
|
+
detail: React.ReactNode;
|
|
8
|
+
action?: React.ReactNode;
|
|
9
|
+
/** "6 min ago" — shown on the right in the cards and compact variants. */
|
|
10
|
+
time?: string;
|
|
11
|
+
icon?: React.ReactNode;
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* Attention feed — the prototype's "Needs attention" card.
|
|
15
|
+
*
|
|
16
|
+
* Each row is a decision waiting on a human: a status shape, what happened,
|
|
17
|
+
* what it costs, and the one button that resolves it. Anything without an
|
|
18
|
+
* action belongs in the activity log (Timeline), not here.
|
|
19
|
+
*
|
|
20
|
+
* Variants: `list` (default, inside a card), `cards` (each item its own card
|
|
21
|
+
* with a tone edge and an icon tile — a dashboard's alert column), `compact`
|
|
22
|
+
* (one line each, for a sidebar or a phone).
|
|
23
|
+
*/
|
|
24
|
+
export declare function ActivityFeed({ items, variant, className, }: {
|
|
25
|
+
items: FeedItem[];
|
|
26
|
+
variant?: "list" | "cards" | "compact";
|
|
27
|
+
className?: string;
|
|
28
|
+
}): React.JSX.Element;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{jsx as t,jsxs as a}from"react/jsx-runtime";import{cn as s}from"../../lib/utils.js";import{Badge as o}from"../ui/badge.js";const i={neutral:"bg-hover text-muted",info:"bg-primary-subtle text-primary",success:"bg-success-subtle text-success",warning:"bg-warning-subtle text-warning",danger:"bg-danger-subtle text-danger",accent:"bg-primary-subtle text-accent"},c={neutral:"before:bg-border2",info:"before:bg-primary",success:"before:bg-success",warning:"before:bg-warning",danger:"before:bg-danger",accent:"before:bg-accent"};function x({items:n,variant:l="list",className:r}){return l==="cards"?t("ul",{className:s("space-y-2",r),children:n.map(e=>a("li",{className:s("relative flex items-center gap-3 overflow-hidden rounded-[12px] border border-border bg-raised py-3 pl-4 pr-3 shadow-[var(--lit)] transition-[transform,box-shadow] hover:-translate-y-px hover:shadow-[var(--lit),0_8px_20px_-12px_rgba(5,8,22,.3)]","before:absolute before:inset-y-0 before:left-0 before:w-[3px]",c[e.tone]),children:[t("span",{className:s("grid size-9 shrink-0 place-items-center rounded-[10px] [&_svg]:size-[18px]",i[e.tone]),children:e.icon??t("i",{className:"size-2 rounded-full bg-current"})}),a("div",{className:"min-w-0 flex-1",children:[a("div",{className:"flex items-center gap-2",children:[t("p",{className:"truncate text-[13px] font-semibold text-text",children:e.title}),e.time?t("span",{className:"ml-auto shrink-0 text-[11px] text-muted",children:e.time}):null]}),t("p",{className:"mt-0.5 text-[12px] text-muted",children:e.detail})]}),e.action?t("div",{className:"shrink-0",children:e.action}):null]},e.id))}):l==="compact"?t("ul",{className:s("divide-y divide-border",r),children:n.map(e=>a("li",{className:"flex items-center gap-2.5 py-2 text-[12.5px]",children:[t(o,{tone:e.tone,className:"h-4 shrink-0 px-1",children:t("span",{className:"sr-only",children:e.tone})}),a("span",{className:"min-w-0 flex-1 truncate",children:[t("span",{className:"font-medium text-text",children:e.title})," ",a("span",{className:"text-muted",children:["\xB7 ",e.detail]})]}),e.time?t("span",{className:"shrink-0 text-[11px] text-muted",children:e.time}):null,e.action?t("div",{className:"shrink-0",children:e.action}):null]},e.id))}):t("ul",{className:s("",r),children:n.map(e=>a("li",{className:"flex items-center gap-3 border-b border-border py-3 last:border-0",children:[t(o,{tone:e.tone,className:"h-[18px] shrink-0 px-1.5",children:t("span",{className:"sr-only",children:e.tone})}),a("div",{className:"min-w-0 flex-1",children:[t("p",{className:"text-[13px] font-medium leading-[18px] text-text",children:e.title}),t("p",{className:"mt-0.5 text-xs text-muted",children:e.detail})]}),e.action?t("div",{className:"shrink-0",children:e.action}):null]},e.id))})}export{x as ActivityFeed};
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
import type { Tone } from "../../types";
|
|
3
|
+
/**
|
|
4
|
+
* Ready-made cells for DataTable columns.
|
|
5
|
+
*
|
|
6
|
+
* UserCell — a person in one cell: avatar (colour from the name, optional
|
|
7
|
+
* status dot), name, and a second line (email, phone, role). The name is the
|
|
8
|
+
* row's identity, so it is the strong text and it stays on one line.
|
|
9
|
+
*
|
|
10
|
+
* RowActions — the actions column: one to three quick icon buttons that appear
|
|
11
|
+
* on row hover (always visible on touch screens and when focused), and a ⋯
|
|
12
|
+
* menu with everything else. Destructive items sit last, after a divider.
|
|
13
|
+
*/
|
|
14
|
+
export declare function UserCell({ name, secondary, src, status, badge, verified, }: {
|
|
15
|
+
name: string;
|
|
16
|
+
/** Second line: email, phone or role. */
|
|
17
|
+
secondary?: React.ReactNode;
|
|
18
|
+
src?: string | null;
|
|
19
|
+
status?: "online" | "away" | "busy" | "offline";
|
|
20
|
+
badge?: {
|
|
21
|
+
label: string;
|
|
22
|
+
tone?: Tone;
|
|
23
|
+
};
|
|
24
|
+
verified?: boolean;
|
|
25
|
+
}): React.JSX.Element;
|
|
26
|
+
export interface RowAction {
|
|
27
|
+
label: string;
|
|
28
|
+
icon: React.ElementType;
|
|
29
|
+
onSelect: () => void;
|
|
30
|
+
/** Shown as an icon button on hover; the rest go in the ⋯ menu. */
|
|
31
|
+
quick?: boolean;
|
|
32
|
+
danger?: boolean;
|
|
33
|
+
shortcut?: string;
|
|
34
|
+
disabled?: boolean;
|
|
35
|
+
}
|
|
36
|
+
export declare function RowActions({ actions, label }: {
|
|
37
|
+
actions: RowAction[];
|
|
38
|
+
label?: string;
|
|
39
|
+
}): React.JSX.Element;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"use client";import{jsx as t,jsxs as o}from"react/jsx-runtime";import{MoreHorizontal as m}from"lucide-react";import{cn as p}from"../../lib/utils.js";import{Avatar as u}from"../ui/avatar.js";import{StatusBadge as b}from"../ui/badge.js";import{DropdownMenu as g,DropdownMenuContent as v,DropdownMenuItem as d,DropdownMenuSeparator as h,DropdownMenuTrigger as f}from"../ui/dropdown-menu.js";import{Tooltip as x}from"../ui/tooltip.js";function S({name:i,secondary:l,src:s,status:r,badge:n,verified:a}){return o("div",{className:"flex min-w-0 items-center gap-3",children:[t(u,{name:i,src:s,size:"md",status:r,verified:a}),o("div",{className:"min-w-0 leading-tight",children:[o("div",{className:"flex items-center gap-1.5",children:[t("span",{className:"truncate font-medium text-text",children:i}),n?t(b,{label:n.label,tone:n.tone??"neutral",variant:"outline"}):null]}),l?t("div",{className:"mt-0.5 truncate text-[12px] text-muted",children:l}):null]})]})}function T({actions:i,label:l="Row actions"}){const s=i.filter(e=>e.quick).slice(0,3),r=i.filter(e=>!e.quick),n=r.filter(e=>!e.danger),a=r.filter(e=>e.danger),c=e=>e.stopPropagation();return o("div",{className:"flex items-center justify-end gap-0.5",onClick:c,onKeyDown:c,children:[t("div",{className:"flex items-center gap-0.5 transition-opacity [@media(hover:hover)]:opacity-0 [@media(hover:hover)]:group-hover/row:opacity-100 [@media(hover:hover)]:focus-within:opacity-100",children:s.map(e=>t(x,{label:e.label,shortcut:e.shortcut,children:t("button",{type:"button","aria-label":e.label,disabled:e.disabled,onClick:e.onSelect,className:p("grid size-8 cursor-pointer place-items-center rounded-ctl text-muted transition-colors disabled:pointer-events-none disabled:opacity-40",e.danger?"hover:bg-danger-subtle hover:text-danger":"hover:bg-hover hover:text-text"),children:t(e.icon,{className:"size-4"})})},e.label))}),r.length?o(g,{children:[t(f,{asChild:!0,children:t("button",{type:"button","aria-label":l,className:"grid size-8 cursor-pointer place-items-center rounded-ctl text-muted transition-colors hover:bg-hover hover:text-text data-[state=open]:bg-hover data-[state=open]:text-text",children:t(m,{className:"size-4"})})}),o(v,{align:"end",className:"min-w-44",children:[n.map(e=>o(d,{disabled:e.disabled,onSelect:e.onSelect,children:[t(e.icon,{}),e.label,e.shortcut?t("span",{className:"ml-auto text-[11px] text-muted",children:e.shortcut}):null]},e.label)),n.length&&a.length?t(h,{}):null,a.map(e=>o(d,{danger:!0,disabled:e.disabled,onSelect:e.onSelect,children:[t(e.icon,{}),e.label]},e.label))]})]}):null]})}export{T as RowActions,S as UserCell};
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
/**
|
|
3
|
+
* Inline cell editing for DataTable.
|
|
4
|
+
*
|
|
5
|
+
* A column opts in with `meta: { edit: { type: "number" } }` and the table gets
|
|
6
|
+
* `onCellEdit`. Click a cell (or press Enter on it) to edit; Enter or leaving
|
|
7
|
+
* the cell saves, Escape cancels. `validate` returns a message to refuse a
|
|
8
|
+
* value — shown under the field, nothing saved. If `onCellEdit` returns a
|
|
9
|
+
* promise, the cell shows a spinner until it settles and stays in edit mode
|
|
10
|
+
* if it rejects. A saved cell flashes once, so the change is seen.
|
|
11
|
+
*/
|
|
12
|
+
export interface ColumnEditMeta {
|
|
13
|
+
type: "text" | "number" | "select" | "date";
|
|
14
|
+
options?: {
|
|
15
|
+
value: string;
|
|
16
|
+
label: string;
|
|
17
|
+
}[];
|
|
18
|
+
/** Return a message to refuse the value, or null to accept it. */
|
|
19
|
+
validate?: (value: string) => string | null;
|
|
20
|
+
/** Let the cell be cleared. Off by default: an empty save is usually a slip (or a half-typed date). */
|
|
21
|
+
allowEmpty?: boolean;
|
|
22
|
+
/** Message when an empty value is refused. */
|
|
23
|
+
requiredMessage?: string;
|
|
24
|
+
}
|
|
25
|
+
export declare function EditableCell({ value, display, edit, onSave, align, }: {
|
|
26
|
+
value: unknown;
|
|
27
|
+
/** What the cell normally shows (the column's own cell renderer). */
|
|
28
|
+
display: React.ReactNode;
|
|
29
|
+
edit: ColumnEditMeta;
|
|
30
|
+
onSave: (value: string) => void | Promise<void>;
|
|
31
|
+
align?: "left" | "right";
|
|
32
|
+
}): React.JSX.Element;
|
|
33
|
+
/**
|
|
34
|
+
* The bulk bar's Edit: one column, one value, every selected row. Uses the
|
|
35
|
+
* column's own edit rules (type, options, validate, allowEmpty), so a value
|
|
36
|
+
* refused in a cell is refused here too.
|
|
37
|
+
*/
|
|
38
|
+
export declare function BulkEdit({ count, columns, onApply, }: {
|
|
39
|
+
count: number;
|
|
40
|
+
columns: {
|
|
41
|
+
id: string;
|
|
42
|
+
label: string;
|
|
43
|
+
edit: ColumnEditMeta;
|
|
44
|
+
}[];
|
|
45
|
+
onApply: (columnId: string, value: string) => Promise<void>;
|
|
46
|
+
}): React.JSX.Element | null;
|
|
47
|
+
/**
|
|
48
|
+
* The bulk bar's Delete, through ConfirmDialog: the rows that will go are
|
|
49
|
+
* listed by name, and from `typeAbove` rows on the person types DELETE first.
|
|
50
|
+
*/
|
|
51
|
+
export declare function BulkDelete({ count, names, typeAbove, onConfirm, }: {
|
|
52
|
+
count: number;
|
|
53
|
+
names?: string[];
|
|
54
|
+
typeAbove?: number;
|
|
55
|
+
onConfirm: () => Promise<void>;
|
|
56
|
+
}): React.JSX.Element;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"use client";import{Fragment as P,jsx as e,jsxs as a}from"react/jsx-runtime";import{Check as z,Loader2 as I,Pencil as R,PenLine as E,Trash2 as H}from"lucide-react";import*as i from"react";import{cn as D}from"../../lib/utils.js";import{Button as w}from"../ui/button.js";import{Dialog as K,DialogBody as O,DialogContent as V,DialogDescription as $,DialogFooter as q,DialogHeader as A,DialogTitle as F}from"../ui/dialog.js";import{Input as W}from"../ui/input.js";import{Label as k}from"../ui/label.js";import{ConfirmDialog as G}from"../ui/confirm-dialog.js";import{Select as N,SelectContent as T,SelectItem as B,SelectTrigger as L,SelectValue as M}from"../ui/select.js";function ee({value:l,display:m,edit:r,onSave:x,align:c}){const[g,y]=i.useState(!1),[o,f]=i.useState(""),[p,u]=i.useState(null),[C,v]=i.useState("idle"),s=i.useRef(null),d=()=>{f(l==null?"":String(l)),u(null),y(!0)};i.useEffect(()=>{g&&(s.current?.focus(),s.current&&"select"in s.current&&r.type!=="select"&&s.current.select())},[g,r.type]);const b=async()=>{if(o===String(l??""))return y(!1);const n=o.trim()===""&&!r.allowEmpty?r.requiredMessage??"Can't be empty":r.validate?.(o)??null;if(n)return u(n);try{v("saving"),await x(o),y(!1),v("saved"),window.setTimeout(()=>v("idle"),900)}catch(S){v("idle"),u(S instanceof Error?S.message:String(S))}};if(!g)return a("button",{type:"button",onClick:n=>{n.stopPropagation(),d()},className:D("group/edit -mx-1.5 flex w-[calc(100%+12px)] min-w-0 cursor-text items-center gap-1.5 rounded-[6px] px-1.5 py-0.5 text-left outline-none transition-colors","hover:bg-primary-subtle/60 focus-visible:ring-2 focus-visible:ring-primary/40",c==="right"&&"flex-row-reverse text-right",C==="saved"&&"wm-saved"),children:[e("span",{className:"min-w-0 flex-1 truncate",children:m}),C==="saved"?e(z,{className:"size-3.5 shrink-0 text-success"}):e(R,{className:"size-3 shrink-0 text-muted opacity-0 transition-opacity group-hover/edit:opacity-70"})]});const t="h-8 w-full rounded-[7px] border border-primary bg-raised px-2 text-[13px] text-text outline-none ring-2 ring-primary/20 "+(p?"!border-danger !ring-danger/20 ":"")+(c==="right"?"text-right ":""),h=n=>{n.stopPropagation(),n.key==="Enter"?(n.preventDefault(),b()):n.key==="Escape"&&(n.preventDefault(),y(!1))};return a("div",{className:"relative -my-1",onClick:n=>n.stopPropagation(),children:[r.type==="select"?e("select",{ref:s,value:o,onChange:n=>f(n.target.value),onKeyDown:h,onBlur:()=>{b()},className:D(t,"cursor-pointer"),children:(r.options??[]).map(n=>e("option",{value:n.value,children:n.label},n.value))}):e("input",{ref:s,type:r.type==="number"?"number":r.type==="date"?"date":"text",value:o,onChange:n=>{f(n.target.value),u(null)},onKeyDown:h,onBlur:()=>{b()},"aria-invalid":!!p,className:D(t,r.type==="number"&&"num")}),C==="saving"?e(I,{className:"absolute right-2 top-1/2 size-3.5 -translate-y-1/2 animate-spin text-primary"}):null,p?e("p",{role:"alert",className:"absolute left-0 top-full z-20 mt-1 whitespace-nowrap rounded-[6px] bg-danger px-2 py-1 text-[11.5px] font-medium text-white shadow-elev2",children:p}):null]})}function te({count:l,columns:m,onApply:r}){const[x,c]=i.useState(!1),[g,y]=i.useState(m[0]?.id??""),[o,f]=i.useState(""),[p,u]=i.useState(null),[C,v]=i.useState(!1),s=m.find(t=>t.id===g)??m[0];if(!s)return null;const d=s.edit,b=async()=>{const t=o.trim()===""&&!d.allowEmpty?d.requiredMessage??"Can't be empty":d.validate?.(o)??null;if(t)return u(t);v(!0);try{await r(s.id,o),c(!1),f("")}catch(h){u(h instanceof Error?h.message:String(h))}finally{v(!1)}};return a(P,{children:[a(w,{variant:"ghost",size:"sm",onClick:()=>c(!0),children:[e(E,{}),"Edit"]}),e(K,{open:x,onOpenChange:c,children:a(V,{variant:"center",size:"sm",children:[a(A,{icon:e(E,{}),eyebrow:`${l} selected`,children:[a(F,{children:["Edit ",l," rows"]}),e($,{children:"The value replaces what each selected row has in that column."})]}),a(O,{className:"space-y-4",children:[a("div",{className:"space-y-1.5",children:[e(k,{children:"Column"}),a(N,{value:s.id,onValueChange:t=>{y(t),f(""),u(null)},children:[e(L,{children:e(M,{})}),e(T,{children:m.map(t=>e(B,{value:t.id,children:t.label},t.id))})]})]}),a("div",{className:"space-y-1.5",children:[e(k,{children:"New value"}),d.type==="select"?a(N,{value:o||void 0,onValueChange:t=>{f(t),u(null)},children:[e(L,{"aria-invalid":!!p,children:e(M,{placeholder:"Choose\u2026"})}),e(T,{children:(d.options??[]).map(t=>e(B,{value:t.value,children:t.label},t.value))})]}):e(W,{type:d.type==="number"?"number":d.type==="date"?"date":"text",value:o,state:p?"error":"default",onChange:t=>{f(t.target.value),u(null)},onKeyDown:t=>t.key==="Enter"&&void b()}),p?e("p",{role:"alert",className:"text-[12px] text-danger",children:p}):null]})]}),a(q,{children:[e(w,{onClick:()=>c(!1),children:"Cancel"}),a(w,{variant:"primary",loading:C,onClick:()=>{b()},children:["Apply to ",l," rows"]})]})]})})]})}function ne({count:l,names:m,typeAbove:r=5,onConfirm:x}){const[c,g]=i.useState(!1);return a(P,{children:[a(w,{variant:"ghost",size:"sm",className:"text-danger hover:bg-danger-subtle hover:text-danger",onClick:()=>g(!0),children:[e(H,{}),"Delete"]}),e(G,{open:c,onOpenChange:g,title:`Delete ${l} ${l===1?"row":"rows"}?`,description:"They will be removed for everyone. This cannot be undone.",items:m,confirmWord:l>=r?"DELETE":void 0,confirmLabel:`Delete ${l}`,onConfirm:x})]})}export{ne as BulkDelete,te as BulkEdit,ee as EditableCell};
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import type { ColumnDef } from "@tanstack/react-table";
|
|
2
|
+
import * as React from "react";
|
|
3
|
+
/**
|
|
4
|
+
* Advanced filters for DataTable — rules of column · operator · value, joined
|
|
5
|
+
* by "all" or "any", shown as chips under the toolbar.
|
|
6
|
+
*
|
|
7
|
+
* A column opts in through its meta: `meta: { filter: { type: "select" } }`.
|
|
8
|
+
* The type decides the operators and the value control: text (contains, is,
|
|
9
|
+
* starts with…), select (is any of / none of, options from the data unless
|
|
10
|
+
* given), number (=, ≠, >, ≥, <, ≤, between) and date (on, before, after,
|
|
11
|
+
* between, with the date pickers). Rules are plain JSON, so they can be saved
|
|
12
|
+
* in a view, put in a URL or sent to an API that filters on the server.
|
|
13
|
+
*/
|
|
14
|
+
export type FilterType = "text" | "select" | "number" | "date";
|
|
15
|
+
export interface ColumnFilterMeta {
|
|
16
|
+
type: FilterType;
|
|
17
|
+
/** Choices for a select filter; collected from the data when left out. */
|
|
18
|
+
options?: {
|
|
19
|
+
value: string;
|
|
20
|
+
label: string;
|
|
21
|
+
}[];
|
|
22
|
+
/** Name in the builder when the header is not plain text. */
|
|
23
|
+
label?: string;
|
|
24
|
+
}
|
|
25
|
+
export type FilterOp = "contains" | "not_contains" | "is" | "is_not" | "starts_with" | "any_of" | "none_of" | "eq" | "neq" | "gt" | "gte" | "lt" | "lte" | "between" | "on" | "before" | "after" | "empty" | "not_empty";
|
|
26
|
+
export interface FilterRule {
|
|
27
|
+
id: string;
|
|
28
|
+
column: string;
|
|
29
|
+
op: FilterOp;
|
|
30
|
+
/** text/number/date: one value; between: [from, to]; any_of/none_of: the picked values. */
|
|
31
|
+
value: string | string[];
|
|
32
|
+
}
|
|
33
|
+
export interface FilterState {
|
|
34
|
+
match: "and" | "or";
|
|
35
|
+
rules: FilterRule[];
|
|
36
|
+
}
|
|
37
|
+
export declare const EMPTY_FILTERS: FilterState;
|
|
38
|
+
export interface FilterField {
|
|
39
|
+
id: string;
|
|
40
|
+
label: string;
|
|
41
|
+
type: FilterType;
|
|
42
|
+
options: {
|
|
43
|
+
value: string;
|
|
44
|
+
label: string;
|
|
45
|
+
}[];
|
|
46
|
+
get: (row: never, index: number) => unknown;
|
|
47
|
+
}
|
|
48
|
+
/** Every word the filters show. Pass the ones your product translates. */
|
|
49
|
+
export declare const FILTER_LABELS: {
|
|
50
|
+
filter: string;
|
|
51
|
+
filters: string;
|
|
52
|
+
addFilter: string;
|
|
53
|
+
clearAll: string;
|
|
54
|
+
matchAll: string;
|
|
55
|
+
matchAny: string;
|
|
56
|
+
where: string;
|
|
57
|
+
and: string;
|
|
58
|
+
or: string;
|
|
59
|
+
value: string;
|
|
60
|
+
none: string;
|
|
61
|
+
ops: Record<FilterOp, string>;
|
|
62
|
+
};
|
|
63
|
+
export type FilterLabels = typeof FILTER_LABELS;
|
|
64
|
+
/** The filterable fields of a column list: id, label, type, how to read a row. */
|
|
65
|
+
export declare function filterFields<T>(columns: ColumnDef<T, unknown>[], data: T[]): FilterField[];
|
|
66
|
+
/** Keep the rows that pass the rules — all of them, or any, as `match` says. */
|
|
67
|
+
export declare function applyFilters<T>(data: T[], fields: FilterField[], state: FilterState): T[];
|
|
68
|
+
/** The Filter button and its rule builder. */
|
|
69
|
+
export declare function FilterBuilder({ fields, value, onChange, labels, open, onOpenChange, }: {
|
|
70
|
+
fields: FilterField[];
|
|
71
|
+
value: FilterState;
|
|
72
|
+
onChange: (v: FilterState) => void;
|
|
73
|
+
labels?: Partial<FilterLabels>;
|
|
74
|
+
open?: boolean;
|
|
75
|
+
onOpenChange?: (o: boolean) => void;
|
|
76
|
+
}): React.JSX.Element;
|
|
77
|
+
/** The active rules as chips: what is applied, at a glance, each removable. */
|
|
78
|
+
export declare function FilterChips({ fields, value, onChange, onEdit, labels, }: {
|
|
79
|
+
fields: FilterField[];
|
|
80
|
+
value: FilterState;
|
|
81
|
+
onChange: (v: FilterState) => void;
|
|
82
|
+
onEdit?: () => void;
|
|
83
|
+
labels?: Partial<FilterLabels>;
|
|
84
|
+
}): React.JSX.Element | null;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"use client";import{jsx as o,jsxs as f}from"react/jsx-runtime";import{ListFilter as T,Plus as L,Trash2 as P,X as q}from"lucide-react";import*as B from"react";import{cn as D}from"../../lib/utils.js";import{Button as g}from"../ui/button.js";import{parseIsoDay as I,toIsoDay as h}from"../ui/calendar.js";import{Checkbox as O}from"../ui/checkbox.js";import{DatePicker as z,DateRangePicker as E}from"../ui/date-picker.js";import{Input as v}from"../ui/input.js";import{Popover as V,PopoverContent as j,PopoverTrigger as M}from"../ui/popover.js";import{Segmented as $}from"../ui/segmented.js";import{Select as F,SelectContent as N,SelectItem as A,SelectTrigger as _,SelectValue as w}from"../ui/select.js";const ie={match:"and",rules:[]},S={text:["contains","not_contains","is","is_not","starts_with","empty","not_empty"],select:["any_of","none_of","empty","not_empty"],number:["eq","neq","gt","gte","lt","lte","between","empty","not_empty"],date:["on","before","after","between","empty","not_empty"]},b={filter:"Filter",filters:"Filters",addFilter:"Add filter",clearAll:"Clear all",matchAll:"All rules",matchAny:"Any rule",where:"Where",and:"and",or:"or",value:"Value",none:"No filters yet. Add one to narrow the rows.",ops:{contains:"contains",not_contains:"does not contain",is:"is",is_not:"is not",starts_with:"starts with",any_of:"is any of",none_of:"is none of",eq:"=",neq:"\u2260",gt:">",gte:"\u2265",lt:"<",lte:"\u2264",between:"is between",on:"is on",before:"is before",after:"is after",empty:"is empty",not_empty:"is not empty"}},x=e=>e==="empty"||e==="not_empty";function ae(e,n){const p=[];for(const c of e){const r=c.meta?.filter;if(!r)continue;const i="accessorKey"in c?String(c.accessorKey):void 0,t="accessorFn"in c?c.accessorFn:void 0,a=c.id??i;if(!a||!i&&!t)continue;const l=(y,u)=>t?t(y,u):y[i];let d=r.options??[];r.type==="select"&&!r.options&&(d=[...new Set(n.map((u,m)=>l(u,m)).filter(u=>u!=null&&u!=="").map(String))].sort().map(u=>({value:u,label:u})));const s=r.label??(typeof c.header=="string"?c.header:a);p.push({id:a,label:s,type:r.type,options:d,get:l})}return p}function K(e){return e instanceof Date?h(e):typeof e=="number"?h(new Date(e)):typeof e=="string"&&I(e)?e.slice(0,10):null}function W(e,n,p){const c=p==null||p==="";if(e.op==="empty")return c;if(e.op==="not_empty")return!c;const r=e.value,i=Array.isArray(r)?r[0]??"":r;if(n.type==="text"){if(i==="")return!0;const t=String(p??"").toLowerCase(),a=i.toLowerCase();if(e.op==="contains")return t.includes(a);if(e.op==="not_contains")return!t.includes(a);if(e.op==="is")return t===a;if(e.op==="is_not")return t!==a;if(e.op==="starts_with")return t.startsWith(a)}if(n.type==="select"){const t=Array.isArray(r)?r:[r];if(!t.length)return!0;const a=t.includes(String(p??""));return e.op==="none_of"?!a:a}if(n.type==="number"){const t=typeof p=="number"?p:Number(p);if(e.op==="between"){const[l,d]=(Array.isArray(r)?r:[r,""]).map(s=>s===""?null:Number(s));return(l===null||t>=l)&&(d===null||t<=d)}if(i==="")return!0;const a=Number(i);return Number.isNaN(t)?!1:{eq:t===a,neq:t!==a,gt:t>a,gte:t>=a,lt:t<a,lte:t<=a}[e.op]??!0}if(n.type==="date"){const t=K(p);if(e.op==="between"){const[a,l]=Array.isArray(r)?r:[r,""];return!a&&!l?!0:!!t&&(!a||t>=a)&&(!l||t<=l)}return i===""?!0:t?e.op==="on"?t===i:e.op==="before"?t<i:t>i:!1}return!0}function se(e,n,p){const c=p.rules.filter(r=>n.some(i=>i.id===r.column));return c.length?e.filter((r,i)=>{const t=c.map(a=>{const l=n.find(d=>d.id===a.column);return W(a,l,l.get(r,i))});return p.match==="and"?t.every(Boolean):t.some(Boolean)}):e}const C=e=>x(e.op)||(Array.isArray(e.value)?e.value.some(n=>n!==""):e.value!=="");let X=0;const Y=()=>`f${Date.now().toString(36)}${(X++).toString(36)}`;function k(e){const n=S[e.type][0];return{id:Y(),column:e.id,op:n,value:e.type==="select"?[]:n==="between"?["",""]:""}}function G({rule:e,field:n,onChange:p,L:c}){if(x(e.op))return o("div",{className:"h-ctl"});if(n.type==="select"){const r=Array.isArray(e.value)?e.value:[];return o("div",{className:"max-h-40 space-y-0.5 overflow-y-auto rounded-ctl border border-border2 bg-raised p-1.5",children:n.options.map(i=>f("label",{className:"flex cursor-pointer items-center gap-2 rounded-[6px] px-1.5 py-1 text-[13px] text-text hover:bg-hover",children:[o(O,{checked:r.includes(i.value),onCheckedChange:t=>p(t?[...r,i.value]:r.filter(a=>a!==i.value))}),o("span",{className:"truncate",children:i.label})]},i.value))})}if(n.type==="date"){if(e.op==="between"){const[r="",i=""]=Array.isArray(e.value)?e.value:[];return o(E,{value:{from:r,to:i},onChange:t=>p([t.from,t.to]),numberOfMonths:1,presets:[]})}return o(z,{value:typeof e.value=="string"?e.value:"",onChange:p})}if(e.op==="between"){const[r="",i=""]=Array.isArray(e.value)?e.value:[];return f("div",{className:"flex items-center gap-1.5",children:[o(v,{type:"number",value:r,onChange:t=>p([t.target.value,i]),placeholder:"min",className:"num"}),o("span",{className:"text-muted",children:"\u2013"}),o(v,{type:"number",value:i,onChange:t=>p([r,t.target.value]),placeholder:"max",className:"num"})]})}return o(v,{type:n.type==="number"?"number":"text",value:typeof e.value=="string"?e.value:"",onChange:r=>p(r.target.value),placeholder:c.value,className:D(n.type==="number"&&"num")})}function le({fields:e,value:n,onChange:p,labels:c,open:r,onOpenChange:i}){const t={...b,...c,ops:{...b.ops,...c?.ops}},a=n.rules.filter(C).length,l=s=>p({...n,rules:s}),d=(s,y)=>l(n.rules.map(u=>u.id===s?{...u,...y}:u));return f(V,{open:r,onOpenChange:i,children:[o(M,{asChild:!0,children:f(g,{variant:a?"subtle":"secondary",size:"sm",children:[o(T,{}),t.filter,a?o("span",{className:"num grid h-[18px] min-w-[18px] place-items-center rounded-full bg-primary px-1 text-[10.5px] font-semibold text-white",children:a}):null]})}),f(j,{align:"start",className:"w-[min(640px,calc(100vw-24px))] p-0",children:[f("div",{className:"flex items-center justify-between gap-3 border-b border-border px-4 py-3",children:[o("p",{className:"disp text-[14px] font-semibold text-text",children:t.filters}),n.rules.length>1?o($,{ariaLabel:"Match",value:n.match,onChange:s=>p({...n,match:s}),options:[{value:"and",label:t.matchAll},{value:"or",label:t.matchAny}]}):null]}),f("div",{className:"max-h-[52vh] space-y-2.5 overflow-y-auto px-4 py-3",children:[n.rules.length===0?o("p",{className:"py-4 text-center text-[13px] text-muted",children:t.none}):null,n.rules.map((s,y)=>{const u=e.find(m=>m.id===s.column)??e[0];return f("div",{className:"grid grid-cols-[44px_1fr_auto] items-start gap-2 sm:grid-cols-[44px_150px_150px_1fr_auto]",children:[o("span",{className:"pt-2 text-right text-[12px] font-medium text-muted",children:y===0?t.where:n.match==="and"?t.and:t.or}),f(F,{value:u.id,onValueChange:m=>d(s.id,{...k(e.find(R=>R.id===m)),id:s.id}),children:[o(_,{className:"max-sm:col-span-1",children:o(w,{})}),o(N,{children:e.map(m=>o(A,{value:m.id,children:m.label},m.id))})]}),o("button",{type:"button","aria-label":"Remove rule",onClick:()=>l(n.rules.filter(m=>m.id!==s.id)),className:"grid size-ctl cursor-pointer place-items-center rounded-ctl text-muted transition-colors hover:bg-danger-subtle hover:text-danger sm:order-last",children:o(P,{className:"size-4"})}),o("div",{className:"col-start-2 sm:col-start-auto",children:f(F,{value:s.op,onValueChange:m=>d(s.id,{op:m,value:u.type==="select"?Array.isArray(s.value)?s.value:[]:m==="between"?["",""]:Array.isArray(s.value)?"":s.value}),children:[o(_,{children:o(w,{})}),o(N,{children:S[u.type].map(m=>o(A,{value:m,children:t.ops[m]},m))})]})}),o("div",{className:"col-span-2 col-start-2 min-w-0 sm:col-span-1 sm:col-start-auto",children:o(G,{rule:s,field:u,onChange:m=>d(s.id,{value:m}),L:t})})]},s.id)})]}),f("div",{className:"flex items-center justify-between border-t border-border bg-surface px-4 py-2.5",children:[f(g,{size:"sm",variant:"ghost",disabled:!e.length,onClick:()=>l([...n.rules,k(e[0])]),children:[o(L,{})," ",t.addFilter]}),n.rules.length?o(g,{size:"sm",variant:"ghost",onClick:()=>p({...n,rules:[]}),children:t.clearAll}):null]})]})]})}function pe({fields:e,value:n,onChange:p,onEdit:c,labels:r}){const i={...b,...r,ops:{...b.ops,...r?.ops}},t=n.rules.filter(C);if(!t.length)return null;const a=(l,d)=>{if(x(l.op))return"";if(d.type==="select"){const s=(Array.isArray(l.value)?l.value:[l.value]).map(y=>d.options.find(u=>u.value===y)?.label??y);return s.length>2?`${s.slice(0,2).join(", ")} +${s.length-2}`:s.join(", ")}return Array.isArray(l.value)?l.value.filter(Boolean).join(" \u2013 "):l.value};return f("div",{className:"mb-2.5 flex flex-wrap items-center gap-1.5",children:[t.map((l,d)=>{const s=e.find(y=>y.id===l.column);return s?f(B.Fragment,{children:[d>0?o("span",{className:"text-[11px] font-medium uppercase text-muted",children:n.match==="and"?i.and:i.or}):null,f("span",{className:"inline-flex h-7 items-center gap-1 rounded-full border border-primary/20 bg-primary-subtle pl-2.5 pr-1 text-[12px] text-primary",children:[f("button",{type:"button",onClick:c,className:"cursor-pointer",children:[o("span",{className:"font-semibold",children:s.label})," ",o("span",{className:"opacity-80",children:i.ops[l.op]})," ",o("span",{className:"num font-medium",children:a(l,s)})]}),o("button",{type:"button","aria-label":"Remove filter",onClick:()=>p({...n,rules:n.rules.filter(y=>y.id!==l.id)}),className:"grid size-5 cursor-pointer place-items-center rounded-full transition-colors hover:bg-primary/15",children:o(q,{className:"size-3"})})]})]},l.id):null}),o("button",{type:"button",onClick:()=>p({...n,rules:[]}),className:"ml-1 cursor-pointer text-[12px] text-muted underline-offset-2 hover:text-text hover:underline",children:i.clearAll})]})}export{ie as EMPTY_FILTERS,b as FILTER_LABELS,le as FilterBuilder,pe as FilterChips,se as applyFilters,ae as filterFields};
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import { type ColumnDef, type ColumnPinningState, type RowData, type SortingState, type VisibilityState } from "@tanstack/react-table";
|
|
2
|
+
import * as React from "react";
|
|
3
|
+
import { type ColumnFilterMeta, type FilterLabels, type FilterState } from "./data-table-filters";
|
|
4
|
+
import { type ColumnEditMeta } from "./data-table-edit";
|
|
5
|
+
declare module "@tanstack/react-table" {
|
|
6
|
+
interface ColumnMeta<TData extends RowData, TValue> {
|
|
7
|
+
/** Numbers go right. Everything else goes left. §8. */
|
|
8
|
+
align?: "left" | "right";
|
|
9
|
+
mono?: boolean;
|
|
10
|
+
/** Tabular figures — mandatory for anything countable. */
|
|
11
|
+
num?: boolean;
|
|
12
|
+
/** Emphasised cell: identifiers and primary amounts. */
|
|
13
|
+
strong?: boolean;
|
|
14
|
+
/** Makes the column filterable in the advanced filters. */
|
|
15
|
+
filter?: ColumnFilterMeta;
|
|
16
|
+
/** Makes the column's cells editable in place (needs `onCellEdit`). */
|
|
17
|
+
edit?: ColumnEditMeta;
|
|
18
|
+
/** Keep the column in view while the table scrolls sideways — for actions. */
|
|
19
|
+
sticky?: "right";
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
export interface SavedView {
|
|
23
|
+
id: string;
|
|
24
|
+
name: string;
|
|
25
|
+
sorting?: SortingState;
|
|
26
|
+
visibility?: VisibilityState;
|
|
27
|
+
pinning?: ColumnPinningState;
|
|
28
|
+
filters?: FilterState;
|
|
29
|
+
}
|
|
30
|
+
interface DataTableProps<T> {
|
|
31
|
+
columns: ColumnDef<T, unknown>[];
|
|
32
|
+
data: T[];
|
|
33
|
+
getRowId: (row: T) => string;
|
|
34
|
+
onRowClick?: (row: T) => void;
|
|
35
|
+
/** Row currently shown in the drawer — highlighted, not navigated to. */
|
|
36
|
+
activeId?: string | null;
|
|
37
|
+
searchPlaceholder?: string;
|
|
38
|
+
pageSize?: number;
|
|
39
|
+
/** Rendered in <tfoot>: totals turn a table into a report (§8). */
|
|
40
|
+
footer?: React.ReactNode;
|
|
41
|
+
emptyState?: React.ReactNode;
|
|
42
|
+
savedViews?: SavedView[];
|
|
43
|
+
toolbarExtra?: React.ReactNode;
|
|
44
|
+
exportName?: string;
|
|
45
|
+
/** Filters to start with (e.g. from the URL). */
|
|
46
|
+
defaultFilters?: FilterState;
|
|
47
|
+
/** Told every time the filters change — save them, or send them to the API. */
|
|
48
|
+
onFiltersChange?: (filters: FilterState) => void;
|
|
49
|
+
/** The server filters: rules are shown and reported but rows are not filtered here. */
|
|
50
|
+
manualFiltering?: boolean;
|
|
51
|
+
filterLabels?: Partial<FilterLabels>;
|
|
52
|
+
/**
|
|
53
|
+
* The look: `default` (hairline rows), `striped` (zebra, for long scans),
|
|
54
|
+
* `bordered` (a spreadsheet grid — the one for editing), `cards` (every row a
|
|
55
|
+
* floating card, for few, rich rows), `minimal` (no frame, inside cards and
|
|
56
|
+
* drawers).
|
|
57
|
+
*/
|
|
58
|
+
variant?: DataTableVariant;
|
|
59
|
+
/** Save an edited cell. Return a promise to show a spinner; reject to keep the editor open. */
|
|
60
|
+
onCellEdit?: (row: T, columnId: string, value: string) => void | Promise<void>;
|
|
61
|
+
/**
|
|
62
|
+
* Save one value into many rows (the bulk bar's Edit). Without it, the table
|
|
63
|
+
* calls `onCellEdit` once per selected row.
|
|
64
|
+
*/
|
|
65
|
+
onBulkEdit?: (rows: T[], columnId: string, value: string) => void | Promise<void>;
|
|
66
|
+
/** Name of a row in confirmations ("Delete these 3: …"). Defaults to the row id. */
|
|
67
|
+
getRowLabel?: (row: T) => string;
|
|
68
|
+
/** Delete the selected rows (the bulk bar's Delete, after a confirmation). */
|
|
69
|
+
onBulkDelete?: (rows: T[]) => void | Promise<void>;
|
|
70
|
+
/** Extra buttons in the bulk bar, e.g. Assign or Archive. `clear` empties the selection. */
|
|
71
|
+
bulkActions?: (rows: T[], clear: () => void) => React.ReactNode;
|
|
72
|
+
className?: string;
|
|
73
|
+
}
|
|
74
|
+
export type DataTableVariant = "default" | "striped" | "bordered" | "cards" | "minimal";
|
|
75
|
+
export declare function DataTable<T>({ columns, data, getRowId, onRowClick, activeId, searchPlaceholder, pageSize, footer, emptyState, savedViews, toolbarExtra, exportName, defaultFilters, onFiltersChange, manualFiltering, filterLabels, variant, onCellEdit, onBulkEdit, onBulkDelete, getRowLabel, bulkActions, className, }: DataTableProps<T>): React.JSX.Element;
|
|
76
|
+
export {};
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
"use client";import{jsx as t,jsxs as n}from"react/jsx-runtime";import{flexRender as U,getCoreRowModel as Me,getFilteredRowModel as Re,getPaginationRowModel as Ne,getSortedRowModel as ke,useReactTable as Fe}from"@tanstack/react-table";import{ArrowDown as Pe,ArrowUp as ze,ArrowUpDown as Te,ChevronLeft as _e,ChevronRight as Ve,Columns3 as Ie,Download as te,FileDown as oe,FileSpreadsheet as Le,ListFilter as Ee,Pin as Ae,Search as Be,Star as Ue,X as $}from"lucide-react";import*as l from"react";import{toast as N}from"sonner";import{Button as m}from"../ui/button.js";import{Checkbox as ne}from"../ui/checkbox.js";import{DropdownMenu as K,DropdownMenuCheckboxItem as $e,DropdownMenuContent as O,DropdownMenuItem as S,DropdownMenuLabel as re,DropdownMenuSeparator as Ke,DropdownMenuTrigger as j}from"../ui/dropdown-menu.js";import{Input as Oe}from"../ui/input.js";import{cn as g}from"../../lib/utils.js";import{applyFilters as je,EMPTY_FILTERS as ae,FilterBuilder as He,FilterChips as qe,filterFields as Ge}from"./data-table-filters.js";import{BulkDelete as Qe,BulkEdit as Xe,EditableCell as Ye}from"./data-table-edit.js";const Je={default:"rounded-card border border-border bg-surface",striped:"rounded-card border border-border bg-surface",bordered:"rounded-card border border-border2 bg-raised",cards:"",minimal:""};function st({columns:b,data:f,getRowId:k,onRowClick:y,activeId:ie,searchPlaceholder:le="Search\u2026",pageSize:se=12,footer:H,emptyState:de,savedViews:F=[],toolbarExtra:ce,exportName:ge="waveinno-export",defaultFilters:ue=ae,onFiltersChange:pe,manualFiltering:q=!1,filterLabels:G,variant:d="default",onCellEdit:v,onBulkEdit:P,onBulkDelete:Q,getRowLabel:X,bulkActions:me,className:be}){const[fe,z]=l.useState([]),[we,T]=l.useState({}),[he,_]=l.useState({left:[],right:[]}),[V,w]=l.useState({}),[I,L]=l.useState(""),[E,A]=l.useState(-1),[Y,J]=l.useState(null),[C,xe]=l.useState(ue),[Se,W]=l.useState(!1),D=e=>{xe(e),pe?.(e)},h=l.useMemo(()=>Ge(b,f),[b,f]),Z=l.useMemo(()=>b.filter(e=>e.meta?.edit).map(e=>({id:e.id??("accessorKey"in e?String(e.accessorKey):""),label:typeof e.header=="string"?e.header:e.id??"",edit:e.meta.edit})).filter(e=>e.id),[b]),ye=l.useMemo(()=>q?f:je(f,h,C),[f,h,C,q]),ee=l.useMemo(()=>({id:"__select",size:36,enableSorting:!1,enableHiding:!1,header:({table:e})=>t(ne,{"aria-label":"Select all rows on this page",checked:e.getIsAllPageRowsSelected()?!0:e.getIsSomePageRowsSelected()?"indeterminate":!1,onCheckedChange:o=>e.toggleAllPageRowsSelected(!!o)}),cell:({row:e})=>t(ne,{"aria-label":"Select row",checked:e.getIsSelected(),onCheckedChange:o=>e.toggleSelected(!!o),onClick:o=>o.stopPropagation()})}),[]),ve=l.useMemo(()=>[ee,...b],[b,ee]),r=Fe({data:ye,columns:ve,state:{sorting:fe,columnVisibility:we,columnPinning:he,rowSelection:V,globalFilter:I},getRowId:e=>k(e),onSortingChange:z,onColumnVisibilityChange:T,onColumnPinningChange:_,onRowSelectionChange:w,onGlobalFilterChange:L,enableMultiSort:!0,enableColumnResizing:!0,columnResizeMode:"onChange",getCoreRowModel:Me(),getSortedRowModel:ke(),getFilteredRowModel:Re(),getPaginationRowModel:Ne(),initialState:{pagination:{pageSize:se}}}),M=r.getRowModel().rows,R=Object.keys(V).filter(e=>V[e]).length,Ce=e=>{if(e.key==="ArrowDown")e.preventDefault(),A(o=>Math.min(o+1,M.length-1));else if(e.key==="ArrowUp")e.preventDefault(),A(o=>Math.max(o-1,0));else if(e.key==="Enter"&&E>=0){e.preventDefault();const o=M[E];o&&y&&y(o.original)}},B=()=>{const e=r.getVisibleLeafColumns().filter(a=>a.id!=="__select"),o=e.map(a=>{const u=a.columnDef.header;return typeof u=="string"?u:a.id}),c=r.getFilteredRowModel().rows.map(a=>e.map(u=>{const x=a.getValue(u.id);return x==null?"":String(x)})),s=[o,...c].map(a=>a.map(u=>`"${u.replace(/"/g,'""')}"`).join(",")).join(`
|
|
2
|
+
`),p=URL.createObjectURL(new Blob([`\uFEFF${s}`],{type:"text/csv"})),i=document.createElement("a");i.href=p,i.download=`${ge}.csv`,i.click(),URL.revokeObjectURL(p),N.success(`Exported ${c.length} rows`)},De=e=>{J(e.id),z(e.sorting??[]),T(e.visibility??{}),_(e.pinning??{left:[],right:[]}),e.filters&&D(e.filters)};return n("div",{className:g("relative",be),children:[n("div",{className:"mb-2.5 flex flex-wrap items-center gap-2",children:[n("div",{className:"relative min-w-[200px] flex-1 sm:max-w-xs",children:[t(Be,{className:"pointer-events-none absolute left-2.5 top-1/2 size-3.5 -translate-y-1/2 text-muted"}),t(Oe,{value:I,onChange:e=>L(e.target.value),placeholder:le,className:"pl-8"}),I?t("button",{type:"button",onClick:()=>L(""),className:"absolute right-2 top-1/2 -translate-y-1/2 cursor-pointer text-muted hover:text-text","aria-label":"Clear search",children:t($,{className:"size-3.5"})}):null]}),h.length?t(He,{fields:h,value:C,onChange:D,labels:G,open:Se,onOpenChange:W}):null,ce,t("div",{className:"flex-1"}),F.length?n(K,{children:[t(j,{asChild:!0,children:n(m,{variant:"secondary",size:"sm",children:[t(Ue,{}),Y?F.find(e=>e.id===Y)?.name:"Saved views"]})}),n(O,{align:"end",children:[t(re,{children:"Saved views"}),F.map(e=>n(S,{onSelect:()=>De(e),children:[t(Ee,{}),e.name]},e.id)),t(Ke,{}),n(S,{onSelect:()=>{J(null),z([]),T({}),_({left:[],right:[]}),D(ae)},children:[t($,{}),"Reset to default"]})]})]}):null,n(K,{children:[t(j,{asChild:!0,children:n(m,{variant:"secondary",size:"sm",children:[t(Ie,{}),"Columns"]})}),n(O,{align:"end",className:"max-h-80 overflow-y-auto",children:[t(re,{children:"Visible columns"}),r.getAllLeafColumns().filter(e=>e.getCanHide()&&e.id!=="__select").map(e=>t($e,{checked:e.getIsVisible(),onCheckedChange:o=>e.toggleVisibility(!!o),onSelect:o=>o.preventDefault(),children:typeof e.columnDef.header=="string"?e.columnDef.header:e.id},e.id))]})]}),n(K,{children:[t(j,{asChild:!0,children:n(m,{variant:"secondary",size:"sm",children:[t(te,{}),"Export"]})}),n(O,{align:"end",children:[n(S,{onSelect:B,children:[t(oe,{}),"CSV"]}),n(S,{onSelect:B,children:[t(Le,{}),"Excel (.xlsx)"]}),n(S,{onSelect:()=>N.info("PDF export queued"),children:[t(oe,{}),"PDF"]})]})]})]}),t(qe,{fields:h,value:C,onChange:D,onEdit:()=>W(!0),labels:G}),t("div",{tabIndex:0,onKeyDown:Ce,role:"region","aria-label":"Data table",className:g("overflow-auto outline-none",Je[d],d==="cards"&&"-mx-1 px-1"),children:n("table",{className:g("w-full text-[13px]",d==="cards"?"border-separate border-spacing-y-1.5":"border-collapse"),children:[t("thead",{children:r.getHeaderGroups().map(e=>t("tr",{children:e.headers.map(o=>{const c=o.column.columnDef.meta,s=o.column.getIsSorted(),p=o.column.getCanSort(),i=o.column.getIsPinned();return t("th",{style:{width:o.getSize()},className:g("ov sticky top-0 z-10 whitespace-nowrap px-cellx py-2 text-left",d==="cards"?"bg-canvas":d==="minimal"?"border-b border-border bg-canvas":"border-b border-border bg-surface",d==="bordered"&&"border-r border-border bg-hover last:border-r-0",c?.sticky==="right"&&"right-0 z-20 shadow-[-8px_0_12px_-10px_rgba(5,8,22,.25)]",c?.align==="right"&&"text-right",i==="left"&&"left-0 z-20"),children:o.isPlaceholder?null:n("div",{className:g("group/th flex items-center gap-1",c?.align==="right"&&"justify-end"),children:[p?n("button",{type:"button",onClick:a=>o.column.toggleSorting(void 0,a.shiftKey),className:g("ov flex items-center gap-1",p&&"cursor-pointer hover:text-text",s&&"!text-primary"),children:[U(o.column.columnDef.header,o.getContext()),p?s==="asc"?t(ze,{className:"size-3"}):s==="desc"?t(Pe,{className:"size-3"}):t(Te,{className:"size-3 opacity-0 transition-opacity group-hover/th:opacity-60"}):null]}):t("span",{className:"ov flex items-center gap-1",children:U(o.column.columnDef.header,o.getContext())}),o.column.getCanPin()&&o.column.id!=="__select"?t("button",{type:"button","aria-label":i?"Unpin column":"Pin column left",onClick:()=>o.column.pin(i==="left"?!1:"left"),className:g("cursor-pointer transition-opacity",i?"text-primary":"opacity-0 group-hover/th:opacity-60 hover:!opacity-100"),children:t(Ae,{className:"size-3"})}):null,o.column.getCanResize()?t("span",{onMouseDown:o.getResizeHandler(),onTouchStart:o.getResizeHandler(),className:"absolute right-0 top-0 h-full w-1 cursor-col-resize select-none bg-transparent hover:bg-primary-muted","aria-hidden":!0}):null]})},o.id)})},e.id))}),t("tbody",{children:M.length===0?t("tr",{children:t("td",{colSpan:r.getVisibleLeafColumns().length,className:"p-0",children:de??t("div",{className:"px-6 py-14 text-center text-[13px] text-muted",children:"No rows match the current filters."})})}):M.map((e,o)=>{const c=k(e.original),s=ie===c;return t("tr",{onClick:()=>{A(o),y?.(e.original)},"aria-selected":s,className:g("group/row transition-[background-color,box-shadow,transform] duration-150",y&&"cursor-pointer",d==="striped"&&"even:bg-hover/45",d==="cards"?g("[&>td]:border-y [&>td]:border-border [&>td]:bg-raised [&>td:first-child]:rounded-l-[11px] [&>td:first-child]:border-l [&>td:last-child]:rounded-r-[11px] [&>td:last-child]:border-r","shadow-[0_1px_2px_rgba(5,8,22,.04)] hover:-translate-y-px hover:shadow-[0_6px_18px_-8px_rgba(5,8,22,.22)]",s&&"[&>td]:border-primary/40 [&>td]:bg-selected"):s?"bg-selected shadow-[inset_2px_0_var(--primary)]":E===o?"bg-hover":"hover:bg-hover"),children:e.getVisibleCells().map(i=>{const a=i.column.columnDef.meta,u=U(i.column.columnDef.cell,i.getContext());return t("td",{style:{height:d==="cards"?"calc(var(--row-h) + 10px)":"var(--row-h)"},className:g("whitespace-nowrap px-cellx py-celly",d==="cards"?"":d==="minimal"?"border-b border-dashed border-border":"border-b border-border",d==="bordered"&&"border-r border-border last:border-r-0",a?.sticky==="right"&&"sticky right-0 z-[1] bg-surface shadow-[-8px_0_12px_-10px_rgba(5,8,22,.25)] group-hover/row:bg-hover group-even/row:bg-inherit",a?.align==="right"&&"text-right",a?.mono&&"mono",a?.num&&"num",a?.strong&&"font-medium text-text"),children:a?.edit&&v?t(Ye,{value:i.getValue(),display:u,edit:a.edit,align:a.align,onSave:x=>v(e.original,i.column.id,x)}):u},i.id)})},e.id)})}),H?t("tfoot",{children:t("tr",{className:"border-t-2 border-border2 font-semibold text-text",children:H})}):null]})}),n("div",{className:"mt-2.5 flex flex-wrap items-center gap-3 text-xs text-muted",children:[t("span",{className:"num",children:r.getFilteredRowModel().rows.length===0?"0 rows":`${r.getState().pagination.pageIndex*r.getState().pagination.pageSize+1}\u2013${Math.min((r.getState().pagination.pageIndex+1)*r.getState().pagination.pageSize,r.getFilteredRowModel().rows.length)} of ${r.getFilteredRowModel().rows.length}`}),n("div",{className:"ml-auto flex items-center gap-1",children:[t(m,{variant:"ghost",size:"iconSm","aria-label":"Previous page",disabled:!r.getCanPreviousPage(),onClick:()=>r.previousPage(),children:t(_e,{})}),n("span",{className:"num px-1",children:[r.getState().pagination.pageIndex+1," /"," ",r.getPageCount()||1]}),t(m,{variant:"ghost",size:"iconSm","aria-label":"Next page",disabled:!r.getCanNextPage(),onClick:()=>r.nextPage(),children:t(Ve,{})})]})]}),R>0?n("div",{className:"anim-in fixed bottom-6 left-1/2 z-30 flex -translate-x-1/2 items-center gap-2 rounded-full border border-border bg-raised px-3 py-2 shadow-elev3",children:[n("span",{className:"num px-1 text-[13px] font-medium text-text",children:[R," selected"]}),t("span",{className:"h-4 w-px bg-border"}),n(m,{variant:"ghost",size:"sm",onClick:B,children:[t(te,{}),"Export"]}),Z.length&&(P||v)?t(Xe,{count:R,columns:Z,onApply:async(e,o)=>{const c=r.getSelectedRowModel().rows.map(s=>s.original);if(P)await P(c,e,o);else for(const s of c)await v(s,e,o);N.success(`Updated ${c.length} rows`),w({})}}):null,Q?t(Qe,{count:R,names:r.getSelectedRowModel().rows.map(e=>X?X(e.original):k(e.original)),onConfirm:async()=>{const e=r.getSelectedRowModel().rows.map(o=>o.original);await Q(e),N.success(`Deleted ${e.length} rows`),w({})}}):null,me?.(r.getSelectedRowModel().rows.map(e=>e.original),()=>w({})),t(m,{variant:"ghost",size:"iconSm","aria-label":"Clear selection",onClick:()=>w({}),children:t($,{})})]}):null]})}export{st as DataTable};
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/** Patterns: compositions of primitives that whole screens are built from. */
|
|
2
|
+
export * from "./activity-feed";
|
|
3
|
+
export * from "./data-table";
|
|
4
|
+
export * from "./data-table-cells";
|
|
5
|
+
export * from "./data-table-edit";
|
|
6
|
+
export * from "./data-table-filters";
|
|
7
|
+
export type { ColumnDef } from "@tanstack/react-table";
|
|
8
|
+
export * from "./kanban";
|
|
9
|
+
export * from "./kpi-card";
|
|
10
|
+
export * from "./money";
|
|
11
|
+
export * from "./page-header";
|
|
12
|
+
export * from "./page-hero";
|
|
13
|
+
export * from "./page-shell";
|
|
14
|
+
export * from "./stat-card";
|
|
15
|
+
export * from "./states";
|
|
16
|
+
export * from "./timeline";
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export*from"./activity-feed.js";export*from"./data-table.js";export*from"./data-table-cells.js";export*from"./data-table-edit.js";export*from"./data-table-filters.js";export*from"./kanban.js";export*from"./kpi-card.js";export*from"./money.js";export*from"./page-header.js";export*from"./page-hero.js";export*from"./page-shell.js";export*from"./stat-card.js";export*from"./states.js";export*from"./timeline.js";
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
export interface KanbanColumn {
|
|
3
|
+
id: string;
|
|
4
|
+
label: string;
|
|
5
|
+
/** Accent for the column rule and the count chip. */
|
|
6
|
+
color: string;
|
|
7
|
+
}
|
|
8
|
+
/**
|
|
9
|
+
* Kanban board — the pipeline surface from docs/crm-module.md §3.
|
|
10
|
+
*
|
|
11
|
+
* Native HTML5 drag and drop, with a keyboard path that does the same job:
|
|
12
|
+
* focus a card and press ← / → to move it a stage. A board that can only be
|
|
13
|
+
* operated with a mouse is a board half the organisation cannot use, and in a
|
|
14
|
+
* sales tool the stage change *is* the record update.
|
|
15
|
+
*
|
|
16
|
+
* Columns keep a fixed width and scroll horizontally. Making them flex would
|
|
17
|
+
* squeeze seven stages into unreadable slivers on a laptop.
|
|
18
|
+
*/
|
|
19
|
+
export declare function KanbanBoard<T>({ columns, items, getId, getColumn, onMove, renderCard, renderColumnSummary, className, }: {
|
|
20
|
+
columns: KanbanColumn[];
|
|
21
|
+
items: T[];
|
|
22
|
+
getId: (item: T) => string;
|
|
23
|
+
getColumn: (item: T) => string;
|
|
24
|
+
onMove: (id: string, toColumn: string) => void;
|
|
25
|
+
renderCard: (item: T) => React.ReactNode;
|
|
26
|
+
renderColumnSummary?: (column: KanbanColumn, items: T[]) => React.ReactNode;
|
|
27
|
+
className?: string;
|
|
28
|
+
}): React.JSX.Element;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"use client";import{jsx as a,jsxs as g}from"react/jsx-runtime";import*as h from"react";import{cn as c}from"../../lib/utils.js";function k({columns:l,items:m,getId:b,getColumn:p,onMove:f,renderCard:N,renderColumnSummary:x,className:y}){const[d,u]=h.useState(null),[D,i]=h.useState(null),v=(e,o)=>{const s=m.find(n=>b(n)===e);if(!s)return;const r=l.findIndex(n=>n.id===p(s)),t=l[r+o];t&&f(e,t.id)};return a("div",{className:c("flex gap-3 overflow-x-auto pb-3",y),children:l.map(e=>{const o=m.filter(r=>p(r)===e.id),s=D===e.id;return g("section",{"aria-label":e.label,onDragOver:r=>{r.preventDefault(),i(e.id)},onDragLeave:()=>i(r=>r===e.id?null:r),onDrop:r=>{r.preventDefault(),d&&f(d,e.id),u(null),i(null)},className:c("flex w-[268px] shrink-0 flex-col rounded-card border bg-surface transition-colors",s?"border-primary bg-selected":"border-border"),children:[g("header",{className:"border-b border-border px-3 py-2.5",children:[g("div",{className:"flex items-center gap-2",children:[a("span",{className:"h-3.5 w-1 shrink-0 rounded-full",style:{background:e.color},"aria-hidden":!0}),a("h3",{className:"flex-1 truncate text-[13px] font-semibold text-text",children:e.label}),a("span",{className:"num rounded-full bg-hover px-1.5 text-[11px] font-medium leading-[18px] text-muted",children:o.length})]}),x?a("div",{className:"mt-1.5",children:x(e,o)}):null]}),a("div",{className:"flex min-h-24 flex-1 flex-col gap-2 p-2",children:o.length===0?a("p",{className:"px-2 py-6 text-center text-xs text-muted",children:"Nothing at this stage"}):o.map(r=>{const t=b(r);return a("div",{tabIndex:0,role:"button",draggable:!0,onDragStart:()=>u(t),onDragEnd:()=>{u(null),i(null)},onKeyDown:n=>{n.key==="ArrowRight"?(n.preventDefault(),v(t,1)):n.key==="ArrowLeft"&&(n.preventDefault(),v(t,-1))},className:c("cursor-grab rounded-ctl border border-border bg-raised p-2.5 text-left","transition-[box-shadow,border-color,opacity] duration-150","hover:border-border2 hover:shadow-elev2 active:cursor-grabbing",d===t&&"opacity-40"),children:N(r)},t)})})]},e.id)})})}export{k as KanbanBoard};
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
import type { KpiDef } from "../../types";
|
|
3
|
+
/**
|
|
4
|
+
* Bar sparkline — the prototype's `.spark`.
|
|
5
|
+
*
|
|
6
|
+
* Deliberately not a line chart. At 20px tall a line is noise; bars read as
|
|
7
|
+
* "six periods, this is the last one" at a glance, and the final bar is
|
|
8
|
+
* emphasised so the eye lands on now rather than on the trend's peak.
|
|
9
|
+
*/
|
|
10
|
+
export declare function Sparkline({ values, className, tone, }: {
|
|
11
|
+
values: number[];
|
|
12
|
+
className?: string;
|
|
13
|
+
tone?: "primary" | "success" | "danger";
|
|
14
|
+
}): React.JSX.Element;
|
|
15
|
+
export declare function KpiCard({ kpi, className }: {
|
|
16
|
+
kpi: KpiDef;
|
|
17
|
+
className?: string;
|
|
18
|
+
}): React.JSX.Element;
|
|
19
|
+
/** KPI row — 2 up on phones, 3 at laptop, up to 5 on wide screens (§5). */
|
|
20
|
+
export declare function KpiRow({ items }: {
|
|
21
|
+
items: KpiDef[];
|
|
22
|
+
}): React.JSX.Element;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"use client";import{jsx as e,jsxs as o}from"react/jsx-runtime";import{ArrowDown as g,ArrowUp as f}from"lucide-react";import{Card as x}from"../ui/card.js";import{useCurrency as N}from"../../store/settings-store.js";import{cn as r}from"../../lib/utils.js";function y({values:a,className:n,tone:l="primary"}){const c=Math.max(...a,1),t=l==="success"?"bg-success":l==="danger"?"bg-danger":"bg-primary";return e("div",{className:r("mt-2 flex h-5 items-end gap-0.5",n),"aria-hidden":!0,children:a.map((i,s)=>e("i",{className:r("flex-1 rounded-[1px]",s===a.length-1?t:"bg-primary-muted"),style:{height:`${Math.max(8,i/c*100)}%`}},s))})}function b({kpi:a,className:n}){const{label:l,value:c,currency:t,hint:i,delta:s,invertDelta:u,spark:d}=a,m=N(),p=s===void 0?null:u?s<0:s>0;return o(x,{className:r("flex flex-col",n),children:[e("div",{className:"ov",children:l}),o("div",{className:"num disp mt-[5px] text-[28px] font-semibold leading-[34px]",children:[t&&m.position==="before"?e("span",{className:"cur",children:m.symbol}):null,c,t&&m.position==="after"?e("span",{className:"cur ml-1",children:m.symbol}):null]}),o("div",{className:"mt-0.5 flex flex-wrap items-center gap-x-1.5 text-xs text-muted",children:[s!==void 0?o("span",{className:r("num inline-flex items-center gap-0.5 font-medium",p?"text-success":"text-danger"),children:[s>0?e(f,{className:"size-3"}):e(g,{className:"size-3"}),Math.abs(s).toFixed(1),"%"]}):null,i?e("span",{className:"min-w-0",children:i}):null]}),d?.length?e(y,{values:d,tone:p===null?"primary":p?"success":"danger"}):null]})}function D({items:a}){return e("div",{className:r("stagger mb-4 grid gap-3 grid-cols-2 lg:grid-cols-3",a.length>=5?"xl:grid-cols-5":"xl:grid-cols-4"),children:a.map(n=>e(b,{kpi:n},n.label))})}export{b as KpiCard,D as KpiRow,y as Sparkline};
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Money — docs/DESIGN-SYSTEM.md §2.5.
|
|
3
|
+
*
|
|
4
|
+
* The symbol comes from the active company's currency and the reader's format
|
|
5
|
+
* preferences (see store/settings-store.ts), never from a hard-coded glyph. It
|
|
6
|
+
* renders in its own muted span so the magnitude leads the eye.
|
|
7
|
+
*
|
|
8
|
+
* Zero renders as an em dash with no symbol at all, because "৳ —" reads as a
|
|
9
|
+
* broken amount while "—" correctly reads as "nothing here".
|
|
10
|
+
*/
|
|
11
|
+
export declare function Money({ value, compact, className, symbol: symbolOverride, decimals, }: {
|
|
12
|
+
value: number | null | undefined;
|
|
13
|
+
compact?: boolean;
|
|
14
|
+
className?: string;
|
|
15
|
+
/** Override the currency's decimals — whole-number prices read cleaner. */
|
|
16
|
+
decimals?: number;
|
|
17
|
+
/** Escape hatch for a figure explicitly stated in another currency. */
|
|
18
|
+
symbol?: string;
|
|
19
|
+
}): import("react").JSX.Element;
|
|
20
|
+
/** The currency glyph on its own — for KPI tiles and table headers. */
|
|
21
|
+
export declare function CurrencySymbol({ className }: {
|
|
22
|
+
className?: string;
|
|
23
|
+
}): import("react").JSX.Element;
|
|
24
|
+
export declare function Qty({ value, unit, className, }: {
|
|
25
|
+
value: number | null | undefined;
|
|
26
|
+
unit?: string;
|
|
27
|
+
className?: string;
|
|
28
|
+
}): import("react").JSX.Element;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"use client";import{Fragment as b,jsx as e,jsxs as c}from"react/jsx-runtime";import{DASH as a,fmtNumber as N}from"../../lib/format.js";import{compact as d,groupDigits as g}from"../../lib/currency.js";import{cn as m}from"../../lib/utils.js";import{useCurrency as f}from"../../store/settings-store.js";function h({value:n,compact:s,className:o,symbol:t,decimals:u}){const i=f(),r=u===void 0?i:{...i,decimals:u};if(n==null||Number.isNaN(n)||n===0)return e("span",{className:m("num text-muted",o),children:a});const p=s?d(n,r):g(n,r),l=e("span",{className:"cur",children:t??r.symbol});return e("span",{className:m("num whitespace-nowrap",o),children:r.position==="before"?c(b,{children:[l,p]}):c(b,{children:[p," ",l]})})}function C({className:n}){const s=f();return e("span",{className:m("cur",n),children:s.symbol})}function S({value:n,unit:s,className:o}){const t=N(n);return c("span",{className:m("num whitespace-nowrap",t===a&&"text-muted",o),children:[t,s&&t!==a?e("span",{className:"cur ml-1",children:s}):null]})}export{C as CurrencySymbol,h as Money,S as Qty};
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
import type { Crumb, Tone } from "../../types";
|
|
3
|
+
export declare function Breadcrumbs({ items }: {
|
|
4
|
+
items: Crumb[];
|
|
5
|
+
}): React.JSX.Element;
|
|
6
|
+
/**
|
|
7
|
+
* Universal page header — §7.
|
|
8
|
+
*
|
|
9
|
+
* `primary` is a single slot, not an array. One primary action per screen is a
|
|
10
|
+
* structural rule of this system, and the type signature is where it gets
|
|
11
|
+
* enforced: a second high-contrast button cannot be passed without noticing.
|
|
12
|
+
*/
|
|
13
|
+
export declare function PageHeader({ crumbs, title, description, secondary, primary, className, }: {
|
|
14
|
+
crumbs?: Crumb[];
|
|
15
|
+
title: string;
|
|
16
|
+
description?: string;
|
|
17
|
+
secondary?: React.ReactNode;
|
|
18
|
+
primary?: React.ReactNode;
|
|
19
|
+
className?: string;
|
|
20
|
+
}): React.JSX.Element;
|
|
21
|
+
/**
|
|
22
|
+
* Callout — a rule, a warning or a tip stated next to the thing it governs.
|
|
23
|
+
*
|
|
24
|
+
* The default (no props) is the accent bar the prototype uses for design laws.
|
|
25
|
+
* Add `tone`, a `title`, an `icon` (true for the tone's own) and an `action`;
|
|
26
|
+
* `variant` picks `soft` (default), `outline` (quiet, inside cards) or
|
|
27
|
+
* `gradient` (the brand: announcements, an upgrade).
|
|
28
|
+
*/
|
|
29
|
+
export declare function Note({ children, className, tone, title, icon, action, variant, }: {
|
|
30
|
+
children: React.ReactNode;
|
|
31
|
+
className?: string;
|
|
32
|
+
tone?: Tone;
|
|
33
|
+
title?: React.ReactNode;
|
|
34
|
+
/** true = the tone's icon; or pass your own. */
|
|
35
|
+
icon?: boolean | React.ReactNode;
|
|
36
|
+
action?: React.ReactNode;
|
|
37
|
+
variant?: "soft" | "outline" | "gradient";
|
|
38
|
+
}): React.JSX.Element;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{jsx as e,jsxs as a}from"react/jsx-runtime";import{CircleAlert as b,CircleCheck as x,Info as u,Sparkles as g,TriangleAlert as f}from"lucide-react";import N from"next/link";import{cn as s}from"../../lib/utils.js";function h({items:n}){return e("nav",{"aria-label":"Breadcrumb",className:"mb-3 text-xs text-muted",children:e("ol",{className:"flex flex-wrap items-center",children:n.map((r,t)=>a("li",{className:"flex items-center",children:[t>0?e("span",{className:"mx-1.5 text-border2","aria-hidden":!0,children:"\u203A"}):null,r.href&&t<n.length-1?e(N,{href:r.href,className:"text-primary hover:underline",children:r.label}):e("span",{"aria-current":t===n.length-1?"page":void 0,children:r.label})]},`${r.label}-${t}`))})})}function k({crumbs:n,title:r,description:t,secondary:o,primary:l,className:c}){return a("div",{className:s("mb-5",c),children:[n?.length?e(h,{items:n}):null,a("div",{className:"flex flex-col items-start gap-3 sm:flex-row sm:flex-wrap sm:gap-4",children:[a("div",{className:"min-w-0 w-full flex-1",children:[e("h1",{className:"disp text-2xl font-semibold leading-[30px]",children:r}),t?e("p",{className:"mt-1 max-w-[70ch] text-[13px] text-muted",children:t}):null]}),o||l?a("div",{className:"flex w-full shrink-0 items-center gap-2 sm:w-auto",children:[o,l]}):null]})]})}const w={accent:{soft:"border-accent bg-primary-subtle",outline:"border-accent/40",icon:g},info:{soft:"border-primary bg-primary-subtle",outline:"border-primary/40",icon:u},success:{soft:"border-success bg-success-subtle",outline:"border-success/40",icon:x},warning:{soft:"border-warning bg-warning-subtle",outline:"border-warning/40",icon:f},danger:{soft:"border-danger bg-danger-subtle",outline:"border-danger/40",icon:b},neutral:{soft:"border-border2 bg-hover",outline:"border-border2",icon:u}},v={accent:"text-accent",info:"text-primary",success:"text-success",warning:"text-warning",danger:"text-danger",neutral:"text-muted"};function C({children:n,className:r,tone:t="accent",title:o,icon:l,action:c,variant:i="soft"}){const d=w[t],p=d.icon,m=l===!0?e(p,{className:"size-4"}):l||null;return a("div",{className:s("mb-4 flex items-start gap-3 text-[13px] leading-5",i==="soft"&&s("rounded-r-ctl border-l-[3px] px-3.5 py-2.5 text-body",d.soft),i==="outline"&&s("rounded-ctl border bg-raised px-3.5 py-3 text-body",d.outline),i==="gradient"&&"rounded-[12px] bg-[linear-gradient(135deg,#1437d3,#7560fd)] px-4 py-3 text-white/90 shadow-[inset_0_1px_0_rgba(255,255,255,.2),0_8px_20px_-10px_rgba(20,55,211,.6)]",r),children:[m?e("span",{className:s("mt-0.5 shrink-0 [&_svg]:size-4",i==="gradient"?"text-white":v[t]),children:m}):null,a("div",{className:"min-w-0 flex-1",children:[o?e("p",{className:s("mb-0.5 font-semibold",i==="gradient"?"text-white":"text-text"),children:o}):null,n]}),c?e("div",{className:"shrink-0 self-center",children:c}):null]})}export{h as Breadcrumbs,C as Note,k as PageHeader};
|