@undevy-org/tablewright 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +262 -0
- package/dist/es/components/dashboard/ReportSummaryCard.js +67 -0
- package/dist/es/components/data-table/AppliedStateChips.js +113 -0
- package/dist/es/components/data-table/ColumnFilterPopover.js +548 -0
- package/dist/es/components/data-table/ColumnHeaderMenu.js +144 -0
- package/dist/es/components/data-table/CompoundFilterPopover.js +508 -0
- package/dist/es/components/data-table/CopyButton.js +35 -0
- package/dist/es/components/data-table/CopyableText.js +26 -0
- package/dist/es/components/data-table/DataTableShell.js +145 -0
- package/dist/es/components/data-table/DrawerActionGroup.js +59 -0
- package/dist/es/components/data-table/DrawerEntityCard.js +21 -0
- package/dist/es/components/data-table/DrawerField.js +10 -0
- package/dist/es/components/data-table/DrawerFlagItem.js +20 -0
- package/dist/es/components/data-table/DrawerSection.js +26 -0
- package/dist/es/components/data-table/DrawerShell.js +141 -0
- package/dist/es/components/data-table/DrawerSummaryCard.js +11 -0
- package/dist/es/components/data-table/EmptyState.js +14 -0
- package/dist/es/components/data-table/ExpiryTimestampCell.js +43 -0
- package/dist/es/components/data-table/FilterChip.js +47 -0
- package/dist/es/components/data-table/FilterFormPanel.js +357 -0
- package/dist/es/components/data-table/FilterFormToggle.js +77 -0
- package/dist/es/components/data-table/FilterPopoverFooter.js +19 -0
- package/dist/es/components/data-table/FilterToolbar.js +131 -0
- package/dist/es/components/data-table/FlatColumnToggle.js +46 -0
- package/dist/es/components/data-table/GapIndicator.js +29 -0
- package/dist/es/components/data-table/GroupedColumnToggle.js +112 -0
- package/dist/es/components/data-table/RowActionsMenu.js +74 -0
- package/dist/es/components/data-table/RowControlCell.js +104 -0
- package/dist/es/components/data-table/SortableHeader.js +102 -0
- package/dist/es/components/data-table/TableFooter.js +132 -0
- package/dist/es/components/data-table/TableLayout.js +14 -0
- package/dist/es/components/data-table/TimestampCell.js +16 -0
- package/dist/es/components/data-table/TwoPanelMenu.js +64 -0
- package/dist/es/components/data-table/ViewChip.js +25 -0
- package/dist/es/components/data-table/cells/AmountCell.js +127 -0
- package/dist/es/components/data-table/cells/ConfirmedAmountCell.js +115 -0
- package/dist/es/components/data-table/cells/CurrencyAmountCell.js +26 -0
- package/dist/es/components/data-table/cells/DualLineCell.js +59 -0
- package/dist/es/components/data-table/cells/EnumBadgeCell.js +9 -0
- package/dist/es/components/data-table/cells/MonoIdCell.js +31 -0
- package/dist/es/components/data-table/hooks/use-column-resize.js +39 -0
- package/dist/es/components/data-table/hooks/use-row-drag.js +40 -0
- package/dist/es/components/data-table/hooks/use-table-pagination.js +14 -0
- package/dist/es/components/data-table/hooks/use-table-sort.js +35 -0
- package/dist/es/components/data-table/managed-filters.js +34 -0
- package/dist/es/components/data-table/sticky-utils.js +25 -0
- package/dist/es/components/layout/AppShell.js +113 -0
- package/dist/es/components/layout/Sidebar.js +128 -0
- package/dist/es/components/layout/SidebarAccountMenu.js +129 -0
- package/dist/es/components/layout/SidebarUser.js +38 -0
- package/dist/es/components/layout/TopBar.js +25 -0
- package/dist/es/components/layout/app-shell-context.js +9 -0
- package/dist/es/components/ui/badge.js +31 -0
- package/dist/es/components/ui/button-footer.js +25 -0
- package/dist/es/components/ui/button.js +44 -0
- package/dist/es/components/ui/checkbox.js +24 -0
- package/dist/es/components/ui/combobox.js +179 -0
- package/dist/es/components/ui/dialog.js +80 -0
- package/dist/es/components/ui/dropdown-menu.js +66 -0
- package/dist/es/components/ui/input.js +24 -0
- package/dist/es/components/ui/popover.js +33 -0
- package/dist/es/components/ui/segmented-control.js +60 -0
- package/dist/es/components/ui/select.js +66 -0
- package/dist/es/components/ui/tabs.js +41 -0
- package/dist/es/components/ui/theme-switch.js +47 -0
- package/dist/es/constants/drawer.js +4 -0
- package/dist/es/context/drawer-expand-context.js +10 -0
- package/dist/es/context/portal-container-context.js +10 -0
- package/dist/es/hooks/table-orchestration-store.js +165 -0
- package/dist/es/hooks/use-scroll-activity.js +33 -0
- package/dist/es/hooks/useRowDragOverride.js +18 -0
- package/dist/es/hooks/useTableOrchestration.js +452 -0
- package/dist/es/index.js +199 -0
- package/dist/es/lib/column-gaps.js +23 -0
- package/dist/es/lib/filter-stats.js +67 -0
- package/dist/es/lib/format-enum.js +6 -0
- package/dist/es/lib/format-timestamp.js +37 -0
- package/dist/es/lib/style-scope.js +4 -0
- package/dist/es/lib/utils.js +8 -0
- package/dist/fonts.css +5 -0
- package/dist/preflight.css +502 -0
- package/dist/styles.css +1731 -0
- package/dist/tablewright.cjs +5473 -0
- package/dist/tablewright.css +1579 -0
- package/dist/tokens.css +150 -0
- package/dist/types/components/dashboard/ReportSummaryCard.d.ts +15 -0
- package/dist/types/components/data-table/AppliedStateChips.d.ts +28 -0
- package/dist/types/components/data-table/ColumnFilterPopover.d.ts +14 -0
- package/dist/types/components/data-table/ColumnHeaderMenu.d.ts +27 -0
- package/dist/types/components/data-table/CompoundFilterPopover.d.ts +14 -0
- package/dist/types/components/data-table/CopyButton.d.ts +8 -0
- package/dist/types/components/data-table/CopyableText.d.ts +10 -0
- package/dist/types/components/data-table/DataTableShell.d.ts +30 -0
- package/dist/types/components/data-table/DrawerActionGroup.d.ts +8 -0
- package/dist/types/components/data-table/DrawerEntityCard.d.ts +13 -0
- package/dist/types/components/data-table/DrawerField.d.ts +7 -0
- package/dist/types/components/data-table/DrawerFlagItem.d.ts +10 -0
- package/dist/types/components/data-table/DrawerSection.d.ts +8 -0
- package/dist/types/components/data-table/DrawerShell.d.ts +33 -0
- package/dist/types/components/data-table/DrawerSummaryCard.d.ts +7 -0
- package/dist/types/components/data-table/EmptyState.d.ts +6 -0
- package/dist/types/components/data-table/ExpiryTimestampCell.d.ts +6 -0
- package/dist/types/components/data-table/FilterChip.d.ts +8 -0
- package/dist/types/components/data-table/FilterFormPanel.d.ts +34 -0
- package/dist/types/components/data-table/FilterFormToggle.d.ts +18 -0
- package/dist/types/components/data-table/FilterPopoverFooter.d.ts +9 -0
- package/dist/types/components/data-table/FilterToolbar.d.ts +29 -0
- package/dist/types/components/data-table/FlatColumnToggle.d.ts +10 -0
- package/dist/types/components/data-table/GapIndicator.d.ts +11 -0
- package/dist/types/components/data-table/GroupedColumnToggle.d.ts +12 -0
- package/dist/types/components/data-table/RowActionsMenu.d.ts +10 -0
- package/dist/types/components/data-table/RowControlCell.d.ts +16 -0
- package/dist/types/components/data-table/SortableHeader.d.ts +16 -0
- package/dist/types/components/data-table/TableFooter.d.ts +31 -0
- package/dist/types/components/data-table/TableLayout.d.ts +12 -0
- package/dist/types/components/data-table/TimestampCell.d.ts +3 -0
- package/dist/types/components/data-table/TwoPanelMenu.d.ts +22 -0
- package/dist/types/components/data-table/ViewChip.d.ts +6 -0
- package/dist/types/components/data-table/cells/AmountCell.d.ts +11 -0
- package/dist/types/components/data-table/cells/ConfirmedAmountCell.d.ts +10 -0
- package/dist/types/components/data-table/cells/CurrencyAmountCell.d.ts +8 -0
- package/dist/types/components/data-table/cells/DualLineCell.d.ts +44 -0
- package/dist/types/components/data-table/cells/EnumBadgeCell.d.ts +9 -0
- package/dist/types/components/data-table/cells/MonoIdCell.d.ts +19 -0
- package/dist/types/components/data-table/filter-types.d.ts +79 -0
- package/dist/types/components/data-table/hooks/use-column-resize.d.ts +7 -0
- package/dist/types/components/data-table/hooks/use-row-drag.d.ts +14 -0
- package/dist/types/components/data-table/hooks/use-table-pagination.d.ts +10 -0
- package/dist/types/components/data-table/hooks/use-table-sort.d.ts +6 -0
- package/dist/types/components/data-table/managed-filters.d.ts +18 -0
- package/dist/types/components/data-table/sticky-utils.d.ts +4 -0
- package/dist/types/components/data-table/types.d.ts +35 -0
- package/dist/types/components/layout/AppShell.d.ts +23 -0
- package/dist/types/components/layout/Sidebar.d.ts +16 -0
- package/dist/types/components/layout/SidebarAccountMenu.d.ts +20 -0
- package/dist/types/components/layout/SidebarUser.d.ts +8 -0
- package/dist/types/components/layout/TopBar.d.ts +7 -0
- package/dist/types/components/layout/app-shell-context.d.ts +8 -0
- package/dist/types/components/layout/index.d.ts +10 -0
- package/dist/types/components/layout/types.d.ts +11 -0
- package/dist/types/components/ui/badge.d.ts +9 -0
- package/dist/types/components/ui/button-footer.d.ts +7 -0
- package/dist/types/components/ui/button.d.ts +11 -0
- package/dist/types/components/ui/checkbox.d.ts +4 -0
- package/dist/types/components/ui/combobox.d.ts +92 -0
- package/dist/types/components/ui/dialog.d.ts +15 -0
- package/dist/types/components/ui/dropdown-menu.d.ts +10 -0
- package/dist/types/components/ui/input.d.ts +6 -0
- package/dist/types/components/ui/popover.d.ts +7 -0
- package/dist/types/components/ui/segmented-control.d.ts +17 -0
- package/dist/types/components/ui/select.d.ts +8 -0
- package/dist/types/components/ui/tabs.d.ts +10 -0
- package/dist/types/components/ui/theme-switch.d.ts +12 -0
- package/dist/types/constants/drawer.d.ts +1 -0
- package/dist/types/context/drawer-expand-context.d.ts +7 -0
- package/dist/types/context/portal-container-context.d.ts +14 -0
- package/dist/types/hooks/table-orchestration-store.d.ts +75 -0
- package/dist/types/hooks/use-scroll-activity.d.ts +1 -0
- package/dist/types/hooks/useDemoTheme.d.ts +8 -0
- package/dist/types/hooks/useRowDragOverride.d.ts +6 -0
- package/dist/types/hooks/useTableOrchestration.d.ts +115 -0
- package/dist/types/index.d.ts +107 -0
- package/dist/types/lib/column-gaps.d.ts +3 -0
- package/dist/types/lib/filter-stats.d.ts +2 -0
- package/dist/types/lib/format-enum.d.ts +1 -0
- package/dist/types/lib/format-timestamp.d.ts +4 -0
- package/dist/types/lib/style-scope.d.ts +19 -0
- package/dist/types/lib/utils.d.ts +2 -0
- package/package.json +114 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 undevy-org
|
|
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,262 @@
|
|
|
1
|
+
# tablewright
|
|
2
|
+
|
|
3
|
+
Live Storybook: <https://undevy-org.github.io/tablewright/>
|
|
4
|
+
|
|
5
|
+
A React + TypeScript component library — buttons, inputs, dialogs, and a
|
|
6
|
+
data-table system (drawers, filters, row controls, sortable headers) built
|
|
7
|
+
on Radix UI, TanStack Table, and Tailwind CSS.
|
|
8
|
+
|
|
9
|
+
## Using it in another project
|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
npm i @undevy-org/tablewright
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
Peer dependencies you install yourself, so there is exactly one copy of each in
|
|
16
|
+
the final bundle:
|
|
17
|
+
|
|
18
|
+
```bash
|
|
19
|
+
npm i react react-dom @tanstack/react-table
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
| Peer | Range | |
|
|
23
|
+
| --- | --- | --- |
|
|
24
|
+
| `react`, `react-dom` | `>=18.0.0 <20.0.0` | required |
|
|
25
|
+
| `@tanstack/react-table` | `^8.21.3` | required — you construct the `Table` instance yourself |
|
|
26
|
+
| `@types/react`, `@types/react-dom` | `*` | optional; your copy is used, none is installed |
|
|
27
|
+
|
|
28
|
+
### Styles
|
|
29
|
+
|
|
30
|
+
Two stylesheets, imported once at the application entry:
|
|
31
|
+
|
|
32
|
+
```ts
|
|
33
|
+
import "@undevy-org/tablewright/tokens.css"; // CSS custom properties, both themes
|
|
34
|
+
import "@undevy-org/tablewright/tablewright.css"; // scoped reset + component styles
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
`@undevy-org/tablewright/styles.css` is the two of them concatenated, if you would
|
|
38
|
+
rather have a single import.
|
|
39
|
+
|
|
40
|
+
Then put the style scope on the element that wraps your use of the kit:
|
|
41
|
+
|
|
42
|
+
```tsx
|
|
43
|
+
import { STYLE_SCOPE } from "@undevy-org/tablewright";
|
|
44
|
+
|
|
45
|
+
<div className={STYLE_SCOPE} data-theme="light">
|
|
46
|
+
…
|
|
47
|
+
</div>;
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
The scope exists because the package must not restyle its host. Tailwind's
|
|
51
|
+
preflight is a global reset — it rewrites margins, list styles, heading sizes
|
|
52
|
+
and form-control appearance for every element on the page — so this package
|
|
53
|
+
ships that normalisation confined to `.tablewright` instead.
|
|
54
|
+
|
|
55
|
+
What is and is not isolated, precisely:
|
|
56
|
+
|
|
57
|
+
- **The element reset is scoped.** It never touches an element outside
|
|
58
|
+
`.tablewright`, and it sits at exactly one class of specificity, so it
|
|
59
|
+
outranks a host reset that names bare elements (`button, input { border: none }`
|
|
60
|
+
would otherwise erase every border in the kit) while still losing to the
|
|
61
|
+
package's own utilities.
|
|
62
|
+
- **The utility classes are global and unprefixed.** `tablewright.css` defines
|
|
63
|
+
`.flex`, `.rounded-md`, `.text-sm` and the rest at the top level. If your own
|
|
64
|
+
stylesheets define class names of the same shape, they collide, and source
|
|
65
|
+
order decides. CSS-module or component-scoped class names — what the first
|
|
66
|
+
consumer uses — cannot collide.
|
|
67
|
+
|
|
68
|
+
If your app has no reset of its own and you want the global one,
|
|
69
|
+
`@undevy-org/tablewright/preflight.css` is Tailwind's preflight compiled from a
|
|
70
|
+
stock config, so it carries Tailwind's default font stacks rather than this
|
|
71
|
+
package's.
|
|
72
|
+
|
|
73
|
+
Fonts are not loaded for you. Serve Inter and JetBrains Mono yourself, or
|
|
74
|
+
`import "@undevy-org/tablewright/fonts.css"` to pull them from Google Fonts.
|
|
75
|
+
|
|
76
|
+
You do not need Tailwind. `tablewright.css` is compiled; the package works the
|
|
77
|
+
same in a project that has never seen Tailwind, and adding the package to a
|
|
78
|
+
`content` glob is neither required nor useful.
|
|
79
|
+
|
|
80
|
+
### Theme
|
|
81
|
+
|
|
82
|
+
Tokens resolve from the nearest ancestor carrying `data-theme="light"` or
|
|
83
|
+
`data-theme="dark"`. Set it on `:root` for a whole-application theme, or on any
|
|
84
|
+
wrapper to theme a region.
|
|
85
|
+
|
|
86
|
+
Radix portals — dropdown, popover, dialog, select — mount outside the React tree
|
|
87
|
+
and default to `document.body`. Their content carries the style scope itself, so
|
|
88
|
+
it is always laid out and typeset correctly, but `document.body` sits outside
|
|
89
|
+
your `data-theme` wrapper. To make portalled content follow a theme that is not
|
|
90
|
+
on `:root`, give it a container inside the themed subtree:
|
|
91
|
+
|
|
92
|
+
```tsx
|
|
93
|
+
import { useState } from "react";
|
|
94
|
+
import { PortalContainerContext, STYLE_SCOPE } from "@undevy-org/tablewright";
|
|
95
|
+
|
|
96
|
+
function Page({ theme }: { theme: "light" | "dark" }) {
|
|
97
|
+
// Callback ref, not useRef: a ref object is still null on first render,
|
|
98
|
+
// and the portals would fall back to document.body.
|
|
99
|
+
const [portal, setPortal] = useState<HTMLDivElement | null>(null);
|
|
100
|
+
|
|
101
|
+
return (
|
|
102
|
+
<div ref={setPortal} className={STYLE_SCOPE} data-theme={theme}>
|
|
103
|
+
<PortalContainerContext.Provider value={portal}>
|
|
104
|
+
…
|
|
105
|
+
</PortalContainerContext.Provider>
|
|
106
|
+
</div>
|
|
107
|
+
);
|
|
108
|
+
}
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
### Example
|
|
112
|
+
|
|
113
|
+
```tsx
|
|
114
|
+
import { getCoreRowModel, useReactTable, type ColumnDef } from "@tanstack/react-table";
|
|
115
|
+
import { Badge, DataTableShell, STYLE_SCOPE, type ColumnMetaDef } from "@undevy-org/tablewright";
|
|
116
|
+
|
|
117
|
+
type Row = { id: string; status: string };
|
|
118
|
+
|
|
119
|
+
const columns: ColumnDef<Row>[] = [
|
|
120
|
+
{ id: "id", accessorKey: "id", header: "ID" },
|
|
121
|
+
{ id: "status", accessorKey: "status", header: "Status",
|
|
122
|
+
cell: ({ row }) => <Badge variant="success">{row.original.status}</Badge> },
|
|
123
|
+
];
|
|
124
|
+
|
|
125
|
+
const columnMeta: Record<string, ColumnMetaDef> = {
|
|
126
|
+
id: { minW: 140 },
|
|
127
|
+
status: { minW: 120 },
|
|
128
|
+
};
|
|
129
|
+
|
|
130
|
+
function Table({ rows }: { rows: Row[] }) {
|
|
131
|
+
const table = useReactTable({ data: rows, columns, getCoreRowModel: getCoreRowModel() });
|
|
132
|
+
return (
|
|
133
|
+
<div className={STYLE_SCOPE} style={{ height: 400, display: "flex" }}>
|
|
134
|
+
<DataTableShell
|
|
135
|
+
table={table}
|
|
136
|
+
dense={false}
|
|
137
|
+
emptyMessage="Nothing found"
|
|
138
|
+
columnMeta={columnMeta}
|
|
139
|
+
columnWidths={{}}
|
|
140
|
+
onColumnResizeStart={() => {}}
|
|
141
|
+
/>
|
|
142
|
+
</div>
|
|
143
|
+
);
|
|
144
|
+
}
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
### Known rough edges
|
|
148
|
+
|
|
149
|
+
- **User-visible strings are hardcoded English in eleven components** —
|
|
150
|
+
`Sidebar`, `SidebarAccountMenu`, `TableFooter`, `DrawerShell`,
|
|
151
|
+
`FilterFormPanel`, `RowActionsMenu`, `GroupedColumnToggle`,
|
|
152
|
+
`ReportSummaryCard`, `AmountCell`, `ConfirmedAmountCell` and the dialog close
|
|
153
|
+
button. `ColumnHeaderMenu` is the only component that takes a `labels` prop, so
|
|
154
|
+
everything else needs a fork or a patch to localise.
|
|
155
|
+
- **`animate-in` / `fade-in-0` / `zoom-in-95` compile to nothing.** `DrawerShell`
|
|
156
|
+
asks for a fade-and-zoom entrance on the expanded drawer, but no
|
|
157
|
+
`tailwindcss-animate` plugin is configured, so those three class names produce
|
|
158
|
+
no CSS in any build — the modal appears instantly, in Storybook and in the
|
|
159
|
+
package alike. Either add the plugin or drop the classes; today the source says
|
|
160
|
+
one thing and the artifact does another.
|
|
161
|
+
- **Opacity modifiers on `var()` colours compile to nothing.** Tailwind v3 cannot
|
|
162
|
+
extract an alpha channel from a raw `var()`, so
|
|
163
|
+
`border-[var(--tag-blue-text)]/20` and `text-[var(--tag-blue-text)]/60` emit no
|
|
164
|
+
rule at all. The blue filter chips in `ColumnFilterPopover` and
|
|
165
|
+
`CompoundFilterPopover` ship without their intended 20% border and 60% glyph
|
|
166
|
+
colour. `color-mix(in srgb, var(--tag-blue-text) 20%, transparent)` in an
|
|
167
|
+
arbitrary value compiles and is the closest faithful fix.
|
|
168
|
+
- **Portalled surfaces re-assert the typographic base.** A font size or colour
|
|
169
|
+
set on your `STYLE_SCOPE` wrapper reaches the tables and toolbars but not the
|
|
170
|
+
dropdowns, popovers, selects or dialogs, because those carry the scope
|
|
171
|
+
themselves.
|
|
172
|
+
- **No tests.** The gates are ESLint, `tsc`, the build's own assertions, the
|
|
173
|
+
render check. Nothing exercises component
|
|
174
|
+
behaviour or interaction.
|
|
175
|
+
|
|
176
|
+
## Development
|
|
177
|
+
|
|
178
|
+
```bash
|
|
179
|
+
pnpm install
|
|
180
|
+
pnpm storybook # dev server
|
|
181
|
+
pnpm build-storybook # static build
|
|
182
|
+
pnpm build # dist/ — library, stylesheets, types
|
|
183
|
+
pnpm check:render # renders dist in headless Chrome against a hostile host stylesheet
|
|
184
|
+
pnpm lint
|
|
185
|
+
pnpm typecheck
|
|
186
|
+
```
|
|
187
|
+
|
|
188
|
+
`check:render` is the one gate that resolves the CSS cascade rather than
|
|
189
|
+
inspecting it. It loads `dist/` — not `src` — into headless Chrome underneath a
|
|
190
|
+
deliberately hostile host stylesheet and asserts 27 computed styles: that the
|
|
191
|
+
kit's controls keep their borders and typography, that the utilities still beat
|
|
192
|
+
the reset, that portalled surfaces are styled while mounted on `document.body`,
|
|
193
|
+
and that the host's own elements are left exactly as the host styled them. Run it
|
|
194
|
+
after touching `src/css/`, the scope wiring, or the CSS build. It needs a Chrome
|
|
195
|
+
on the machine (`CHROME_PATH` overrides the search) and is deliberately not part
|
|
196
|
+
of `pnpm build`.
|
|
197
|
+
|
|
198
|
+
## Structure
|
|
199
|
+
|
|
200
|
+
- `src/components/ui` — primitives (Button, Input, Dialog, Select, Tabs, ...)
|
|
201
|
+
- `src/components/layout` — app shell, sidebar, top bar
|
|
202
|
+
- `src/components/data-table` — table shell, drawers, filters, row controls
|
|
203
|
+
- `src/components/dashboard` — summary cards
|
|
204
|
+
- `src/css` — token, reset, global and font layers assembled into `dist/`
|
|
205
|
+
- `scripts/render-check` — the fixture and runner behind `pnpm check:render`
|
|
206
|
+
- `design-tokens/manifest.json` — canonical token source
|
|
207
|
+
|
|
208
|
+
## Build
|
|
209
|
+
|
|
210
|
+
`pnpm build` runs four steps (`scripts/build.mjs`). `dist/` is cleared once at
|
|
211
|
+
the start, so the passes can write into the same tree:
|
|
212
|
+
|
|
213
|
+
| Step | Output |
|
|
214
|
+
| --- | --- |
|
|
215
|
+
| `TW_FORMAT=es vite build --config vite.lib.config.ts` | `dist/es/**` — one file per source module |
|
|
216
|
+
| `TW_FORMAT=cjs vite build --config vite.lib.config.ts` | `dist/tablewright.cjs` — one flattened file |
|
|
217
|
+
| `node scripts/build-css.mjs` | `dist/{tokens,tablewright,styles,preflight,fonts}.css` |
|
|
218
|
+
| `tsc -p tsconfig.build.json` | `dist/types/**` |
|
|
219
|
+
|
|
220
|
+
The ES output is per-module because a single flattened bundle defeats
|
|
221
|
+
tree-shaking here: component modules call `forwardRef(...)` and `cva(...)` at
|
|
222
|
+
module scope, which Rollup cannot prove side-effect free. Measured in a real
|
|
223
|
+
consumer with React, Radix and TanStack external — importing only `cn` costs
|
|
224
|
+
25.4 kB / 5.7 kB gzip against 171 kB / 34.3 kB for the whole surface. (The
|
|
225
|
+
`sideEffects` field changes none of this; it is declared because a webpack
|
|
226
|
+
consumer needs it to keep the CSS imports, and was measured to make no difference
|
|
227
|
+
to what Rollup drops.)
|
|
228
|
+
|
|
229
|
+
The CJS output is `.cjs`, not `.cjs.js`. This package is `"type": "module"`, so
|
|
230
|
+
Node reads any `.js` inside it as ESM — a CommonJS file named `.cjs.js` returns
|
|
231
|
+
an empty namespace from `require()` with no error at all.
|
|
232
|
+
|
|
233
|
+
The library build is deliberately not `vite.config.ts`: Storybook's
|
|
234
|
+
`@storybook/react-vite` auto-loads that filename and would inherit `build.lib`.
|
|
235
|
+
|
|
236
|
+
`tsconfig.build.json` sets `noEmit: false`. The base config sets it to `true`,
|
|
237
|
+
and a declaration build that inherits it exits 0 having written nothing — an
|
|
238
|
+
empty `dist` that no error reports.
|
|
239
|
+
|
|
240
|
+
Two further passes run after those steps: relative specifiers in the emitted
|
|
241
|
+
declarations get an explicit `.js` extension (without it, a consumer on
|
|
242
|
+
`node16`/`nodenext` types every export as `any` and sees no error), and every
|
|
243
|
+
path `package.json` advertises is asserted to exist and be non-trivial.
|
|
244
|
+
|
|
245
|
+
### Checks
|
|
246
|
+
|
|
247
|
+
The CSS build fails loudly on three mistakes it can otherwise make silently:
|
|
248
|
+
|
|
249
|
+
- a `content` glob that matches nothing — utilities would be missing and every
|
|
250
|
+
component would render unstyled;
|
|
251
|
+
- a rule that addresses elements directly instead of through a class — a leaked
|
|
252
|
+
preflight looks exactly like that and would restyle the host page;
|
|
253
|
+
- hand-authored CSS (`reset.css`, `scrollbar.css`) whose selector does not name
|
|
254
|
+
`.tablewright` outside a `:where()`. Inside `:where()` the scope contributes no
|
|
255
|
+
specificity, a host's plain `button, input { border: none }` beats the reset,
|
|
256
|
+
and every control in the kit loses its border. That one shipped, and no static
|
|
257
|
+
check caught it — `pnpm check:render` did, by rendering.
|
|
258
|
+
|
|
259
|
+
Each guard is verified by breaking the build on purpose and confirming it fails,
|
|
260
|
+
not merely by passing on a good build. The same applies to `check:render`: it was
|
|
261
|
+
confirmed to go red both for the specificity defect and for a portal that loses
|
|
262
|
+
the scope class.
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import { jsxs, jsx } from "react/jsx-runtime";
|
|
2
|
+
import { Badge } from "../ui/badge.js";
|
|
3
|
+
function ReportSummaryCard({
|
|
4
|
+
title,
|
|
5
|
+
count,
|
|
6
|
+
endpoint,
|
|
7
|
+
topActions,
|
|
8
|
+
filterNames,
|
|
9
|
+
columns,
|
|
10
|
+
sampleRows,
|
|
11
|
+
emptyStateMessage,
|
|
12
|
+
notes
|
|
13
|
+
}) {
|
|
14
|
+
return /* @__PURE__ */ jsxs("section", { className: "rounded-xl border border-[var(--border-subtle)] bg-[var(--bg-surface)]", children: [
|
|
15
|
+
/* @__PURE__ */ jsxs("div", { className: "flex flex-wrap items-center justify-between gap-3 border-b border-[var(--border-subtle)] px-6 py-4", children: [
|
|
16
|
+
/* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2", children: [
|
|
17
|
+
/* @__PURE__ */ jsx("h3", { className: "text-[15px] font-semibold text-[var(--text-primary)]", children: title }),
|
|
18
|
+
typeof count === "number" ? /* @__PURE__ */ jsx(Badge, { variant: "neutral", children: count }) : null
|
|
19
|
+
] }),
|
|
20
|
+
/* @__PURE__ */ jsx("code", { className: "rounded-md bg-[var(--bg-secondary)] px-2.5 py-1 font-mono text-[11px] text-[var(--text-secondary)]", children: endpoint })
|
|
21
|
+
] }),
|
|
22
|
+
/* @__PURE__ */ jsxs("div", { className: "grid gap-4 px-6 py-5 lg:grid-cols-[0.9fr_1.1fr]", children: [
|
|
23
|
+
/* @__PURE__ */ jsxs("div", { className: "space-y-4", children: [
|
|
24
|
+
/* @__PURE__ */ jsxs("div", { children: [
|
|
25
|
+
/* @__PURE__ */ jsx("div", { className: "mb-2 text-[11px] font-semibold uppercase tracking-[0.08em] text-[var(--text-secondary)]", children: "Top Actions" }),
|
|
26
|
+
/* @__PURE__ */ jsx("div", { className: "flex flex-wrap gap-2", children: topActions.length > 0 ? topActions.map((action) => /* @__PURE__ */ jsx(Badge, { variant: "neutral", className: "rounded-full px-3 py-1", children: action }, action)) : /* @__PURE__ */ jsx("span", { className: "text-[12px] text-[var(--text-tertiary)]", children: "No visible top actions" }) })
|
|
27
|
+
] }),
|
|
28
|
+
/* @__PURE__ */ jsxs("div", { children: [
|
|
29
|
+
/* @__PURE__ */ jsx("div", { className: "mb-2 text-[11px] font-semibold uppercase tracking-[0.08em] text-[var(--text-secondary)]", children: "Filters" }),
|
|
30
|
+
/* @__PURE__ */ jsx("div", { className: "flex flex-wrap gap-2", children: filterNames.length > 0 ? filterNames.map((filterName) => /* @__PURE__ */ jsx(Badge, { variant: "info", className: "rounded-full px-3 py-1", children: filterName }, filterName)) : /* @__PURE__ */ jsx("span", { className: "text-[12px] text-[var(--text-tertiary)]", children: "No visible filters" }) })
|
|
31
|
+
] }),
|
|
32
|
+
/* @__PURE__ */ jsxs("div", { children: [
|
|
33
|
+
/* @__PURE__ */ jsx("div", { className: "mb-2 text-[11px] font-semibold uppercase tracking-[0.08em] text-[var(--text-secondary)]", children: "Columns" }),
|
|
34
|
+
/* @__PURE__ */ jsx("div", { className: "flex flex-wrap gap-2", children: columns.map((column) => /* @__PURE__ */ jsx(Badge, { variant: "warning", className: "rounded-full px-3 py-1", children: column }, column)) })
|
|
35
|
+
] }),
|
|
36
|
+
notes && notes.length > 0 ? /* @__PURE__ */ jsxs("div", { children: [
|
|
37
|
+
/* @__PURE__ */ jsx("div", { className: "mb-2 text-[11px] font-semibold uppercase tracking-[0.08em] text-[var(--text-secondary)]", children: "Notes" }),
|
|
38
|
+
/* @__PURE__ */ jsx("div", { className: "space-y-2 text-[12px] text-[var(--text-secondary)]", children: notes.map((note) => /* @__PURE__ */ jsx("p", { children: note }, note)) })
|
|
39
|
+
] }) : null
|
|
40
|
+
] }),
|
|
41
|
+
/* @__PURE__ */ jsxs("div", { className: "rounded-xl border border-[var(--border-subtle)] bg-[var(--bg-hover)] p-4", children: [
|
|
42
|
+
/* @__PURE__ */ jsx("div", { className: "mb-3 text-[11px] font-semibold uppercase tracking-[0.08em] text-[var(--text-secondary)]", children: "Sample Rows" }),
|
|
43
|
+
sampleRows.length > 0 ? /* @__PURE__ */ jsx("div", { className: "space-y-3", children: sampleRows.map((row) => /* @__PURE__ */ jsxs(
|
|
44
|
+
"div",
|
|
45
|
+
{
|
|
46
|
+
className: "rounded-lg border border-[var(--border-subtle)] bg-[var(--bg-surface)] p-3",
|
|
47
|
+
children: [
|
|
48
|
+
/* @__PURE__ */ jsx("div", { className: "mb-2 text-[13px] font-medium text-[var(--text-primary)]", children: row.title }),
|
|
49
|
+
/* @__PURE__ */ jsx("div", { className: "flex flex-wrap gap-2", children: row.values.map((value) => /* @__PURE__ */ jsx(
|
|
50
|
+
"span",
|
|
51
|
+
{
|
|
52
|
+
className: "rounded-md bg-[var(--bg-secondary)] px-2 py-1 text-[11px] text-[var(--text-secondary)]",
|
|
53
|
+
children: value
|
|
54
|
+
},
|
|
55
|
+
`${row.title}-${value}`
|
|
56
|
+
)) })
|
|
57
|
+
]
|
|
58
|
+
},
|
|
59
|
+
`${title}-${row.title}`
|
|
60
|
+
)) }) : /* @__PURE__ */ jsx("div", { className: "rounded-lg border border-dashed border-[var(--border-subtle)] bg-[var(--bg-surface)] px-4 py-8 text-center text-[12px] text-[var(--text-secondary)]", children: emptyStateMessage ?? "No sample rows captured." })
|
|
61
|
+
] })
|
|
62
|
+
] })
|
|
63
|
+
] });
|
|
64
|
+
}
|
|
65
|
+
export {
|
|
66
|
+
ReportSummaryCard
|
|
67
|
+
};
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
import { jsxs, jsx } from "react/jsx-runtime";
|
|
2
|
+
import { X } from "lucide-react";
|
|
3
|
+
import { Button } from "../ui/button.js";
|
|
4
|
+
import { FilterChip } from "./FilterChip.js";
|
|
5
|
+
import { ColumnFilterPopover } from "./ColumnFilterPopover.js";
|
|
6
|
+
import { CompoundFilterPopover } from "./CompoundFilterPopover.js";
|
|
7
|
+
const DEFAULT_LABELS = {
|
|
8
|
+
clearAll: "Clear all",
|
|
9
|
+
columns: "Columns",
|
|
10
|
+
hiddenCount: (n) => `${n} hidden`
|
|
11
|
+
};
|
|
12
|
+
function isFilterValueEmpty(value) {
|
|
13
|
+
if (Array.isArray(value)) return value.length === 0;
|
|
14
|
+
if (typeof value === "string") return value.length === 0;
|
|
15
|
+
if (typeof value === "object" && value !== null) {
|
|
16
|
+
const keys = Object.keys(value);
|
|
17
|
+
if (keys.length === 0) return true;
|
|
18
|
+
return keys.every((k) => isFilterValueEmpty(value[k]));
|
|
19
|
+
}
|
|
20
|
+
return false;
|
|
21
|
+
}
|
|
22
|
+
function AppliedStateChips({
|
|
23
|
+
filters,
|
|
24
|
+
configs,
|
|
25
|
+
newestColumnId,
|
|
26
|
+
onRemoveFilter,
|
|
27
|
+
onChangeFilter,
|
|
28
|
+
currentSort,
|
|
29
|
+
columnLabels,
|
|
30
|
+
onRemoveSort,
|
|
31
|
+
hiddenColumnsCount,
|
|
32
|
+
onResetColumns,
|
|
33
|
+
onOpenColumnsMenu,
|
|
34
|
+
customFilterChip,
|
|
35
|
+
onRemoveCustomFilter,
|
|
36
|
+
onClearAll,
|
|
37
|
+
columnFilterStats,
|
|
38
|
+
requestOpenFilterId,
|
|
39
|
+
onRequestOpenHandled
|
|
40
|
+
}) {
|
|
41
|
+
const hasFilters = filters.length > 0;
|
|
42
|
+
const hasSort = currentSort !== null;
|
|
43
|
+
const hasHiddenColumns = hiddenColumnsCount > 0;
|
|
44
|
+
const hasCustomFilter = customFilterChip !== null;
|
|
45
|
+
if (!hasFilters && !hasSort && !hasHiddenColumns && !hasCustomFilter) return null;
|
|
46
|
+
const sortDirectionLabel = currentSort?.direction === "asc" ? "↑" : "↓";
|
|
47
|
+
const sortColumnLabel = currentSort ? columnLabels[currentSort.columnId] ?? currentSort.columnId : "";
|
|
48
|
+
return /* @__PURE__ */ jsxs("div", { className: "flex flex-wrap items-center gap-2 pb-3", children: [
|
|
49
|
+
/* @__PURE__ */ jsxs(Button, { variant: "secondary", onClick: onClearAll, className: "shrink-0", children: [
|
|
50
|
+
/* @__PURE__ */ jsx(X, { size: 14 }),
|
|
51
|
+
DEFAULT_LABELS.clearAll
|
|
52
|
+
] }),
|
|
53
|
+
hasHiddenColumns && /* @__PURE__ */ jsx(
|
|
54
|
+
FilterChip,
|
|
55
|
+
{
|
|
56
|
+
label: DEFAULT_LABELS.columns,
|
|
57
|
+
value: DEFAULT_LABELS.hiddenCount(hiddenColumnsCount),
|
|
58
|
+
onRemove: onResetColumns,
|
|
59
|
+
onClick: onOpenColumnsMenu,
|
|
60
|
+
className: "cursor-pointer"
|
|
61
|
+
}
|
|
62
|
+
),
|
|
63
|
+
hasSort && /* @__PURE__ */ jsx(FilterChip, { label: sortColumnLabel, value: sortDirectionLabel, onRemove: onRemoveSort }),
|
|
64
|
+
filters.map((filter) => {
|
|
65
|
+
const filterConfig = configs[filter.columnId];
|
|
66
|
+
if (!filterConfig) return null;
|
|
67
|
+
const isEmpty = isFilterValueEmpty(filter.value);
|
|
68
|
+
if (filterConfig.type === "compound") {
|
|
69
|
+
return /* @__PURE__ */ jsx(
|
|
70
|
+
CompoundFilterPopover,
|
|
71
|
+
{
|
|
72
|
+
config: filterConfig,
|
|
73
|
+
filter,
|
|
74
|
+
onChange: (value) => onChangeFilter(filter.columnId, value),
|
|
75
|
+
onRemove: () => onRemoveFilter(filter.columnId),
|
|
76
|
+
initialOpen: filter.columnId === newestColumnId,
|
|
77
|
+
chipClassName: isEmpty ? "border-dashed" : void 0,
|
|
78
|
+
columnFilterStats,
|
|
79
|
+
requestOpen: filter.columnId === requestOpenFilterId,
|
|
80
|
+
onRequestOpenHandled
|
|
81
|
+
},
|
|
82
|
+
filter.columnId
|
|
83
|
+
);
|
|
84
|
+
}
|
|
85
|
+
return /* @__PURE__ */ jsx(
|
|
86
|
+
ColumnFilterPopover,
|
|
87
|
+
{
|
|
88
|
+
config: filterConfig,
|
|
89
|
+
filter,
|
|
90
|
+
onChange: (value) => onChangeFilter(filter.columnId, value),
|
|
91
|
+
onRemove: () => onRemoveFilter(filter.columnId),
|
|
92
|
+
initialOpen: filter.columnId === newestColumnId,
|
|
93
|
+
chipClassName: isEmpty ? "border-dashed" : void 0,
|
|
94
|
+
stats: columnFilterStats?.[filter.columnId],
|
|
95
|
+
requestOpen: filter.columnId === requestOpenFilterId,
|
|
96
|
+
onRequestOpenHandled
|
|
97
|
+
},
|
|
98
|
+
filter.columnId
|
|
99
|
+
);
|
|
100
|
+
}),
|
|
101
|
+
hasCustomFilter && /* @__PURE__ */ jsx(
|
|
102
|
+
FilterChip,
|
|
103
|
+
{
|
|
104
|
+
label: customFilterChip.label,
|
|
105
|
+
value: customFilterChip.value,
|
|
106
|
+
onRemove: onRemoveCustomFilter
|
|
107
|
+
}
|
|
108
|
+
)
|
|
109
|
+
] });
|
|
110
|
+
}
|
|
111
|
+
export {
|
|
112
|
+
AppliedStateChips
|
|
113
|
+
};
|