@tristan2828/ui-foundation 4.34.0 → 4.36.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.
|
@@ -3,19 +3,20 @@
|
|
|
3
3
|
<!-- index: written by `npm run docs:index` in the package from the headings below; never edit it by hand -->
|
|
4
4
|
## Index
|
|
5
5
|
|
|
6
|
-
- [Semantic tones](#semantic-tones) · lines
|
|
7
|
-
- [The three badge styles](#the-three-badge-styles) · lines
|
|
8
|
-
- [Mapping an enum to tones](#mapping-an-enum-to-tones) · lines
|
|
9
|
-
- [Glyphs and icons: 3:1](#glyphs-and-icons-31) · lines
|
|
10
|
-
- [Logos and brand marks](#logos-and-brand-marks) · lines
|
|
11
|
-
- [Categorical colour](#categorical-colour) · lines
|
|
12
|
-
- [Typography](#typography) · lines
|
|
13
|
-
- [Written text (notes)](#written-text-notes) · lines
|
|
14
|
-
- [Density](#density) · lines
|
|
15
|
-
- [Widget cards (hub pages)](#widget-cards-hub-pages) · lines
|
|
16
|
-
- [Module cards (a hub's places)](#module-cards-a-hubs-places) · lines
|
|
17
|
-
- [Notices](#notices) · lines
|
|
18
|
-
- [A value over time](#a-value-over-time) · lines
|
|
6
|
+
- [Semantic tones](#semantic-tones) · lines 29–57
|
|
7
|
+
- [The three badge styles](#the-three-badge-styles) · lines 59–76
|
|
8
|
+
- [Mapping an enum to tones](#mapping-an-enum-to-tones) · lines 78–107
|
|
9
|
+
- [Glyphs and icons: 3:1](#glyphs-and-icons-31) · lines 109–122
|
|
10
|
+
- [Logos and brand marks](#logos-and-brand-marks) · lines 124–168
|
|
11
|
+
- [Categorical colour](#categorical-colour) · lines 170–240
|
|
12
|
+
- [Typography](#typography) · lines 242–324
|
|
13
|
+
- [Written text (notes)](#written-text-notes) · lines 273–324
|
|
14
|
+
- [Density](#density) · lines 326–383
|
|
15
|
+
- [Widget cards (hub pages)](#widget-cards-hub-pages) · lines 385–460
|
|
16
|
+
- [Module cards (a hub's places)](#module-cards-a-hubs-places) · lines 462–520
|
|
17
|
+
- [Notices](#notices) · lines 522–550
|
|
18
|
+
- [A value over time](#a-value-over-time) · lines 552–585
|
|
19
|
+
- [A list as a board](#a-list-as-a-board) · lines 587–638
|
|
19
20
|
|
|
20
21
|
<!-- /index -->
|
|
21
22
|
|
|
@@ -582,3 +583,56 @@ or `'last'`) with its value.
|
|
|
582
583
|
`formatX` the page uses for those values elsewhere.
|
|
583
584
|
- One series and a few dozen points. Several series, zoom or tooltips
|
|
584
585
|
are a request for the foundation, not a hand-drawn chart in the app.
|
|
586
|
+
|
|
587
|
+
## A list as a board
|
|
588
|
+
|
|
589
|
+
`Board` (4.36) shows a list as a column per value of one choice field (a
|
|
590
|
+
status, a stage), a card per record, and moves a record by moving its
|
|
591
|
+
card. It is a second view of a list, on its own page beside the
|
|
592
|
+
`DataTable`, never a replacement: filters, sorting and saved views stay
|
|
593
|
+
the table's.
|
|
594
|
+
|
|
595
|
+
```tsx
|
|
596
|
+
<Board
|
|
597
|
+
aria-label="Projects by stage"
|
|
598
|
+
columns={STAGES.map((stage) => ({ value: stage, name: STAGE_NAMES[stage], label: <StageLabel value={stage} />, count: totals[stage] }))}
|
|
599
|
+
items={projects}
|
|
600
|
+
getId={(project) => project.id}
|
|
601
|
+
getColumn={(project) => project.stage}
|
|
602
|
+
getName={(project) => project.name}
|
|
603
|
+
renderCard={(project) => <ProjectCard project={project} />}
|
|
604
|
+
onMove={(project, stage) => saves.get(project.id)?.({ stage })}
|
|
605
|
+
moveTo={[...STAGE_TARGETS, { value: 'archived', label: 'Archived' }]}
|
|
606
|
+
isLoading={isLoading}
|
|
607
|
+
error={error}
|
|
608
|
+
onRetry={refetch}
|
|
609
|
+
emptyTitle="No projects yet"
|
|
610
|
+
/>
|
|
611
|
+
```
|
|
612
|
+
|
|
613
|
+
- **A card moves two ways:** dragged to another column, and its Move
|
|
614
|
+
button, a menu of `moveTo` (the columns by default) for the keyboard
|
|
615
|
+
and a phone, where there is no drag. A value with no column (Archived,
|
|
616
|
+
Dropped) is only in the menu, and takes the card off the board. After
|
|
617
|
+
a move from the menu the focus is back on the card's Move button in
|
|
618
|
+
its new column (on the board, if the card left it), and a status line
|
|
619
|
+
tells a screen reader "Moved <name> to <column>".
|
|
620
|
+
- **A column has a name in words** for the menu and that line: its
|
|
621
|
+
`label` when that is a string, else give `name` (a label with a glyph
|
|
622
|
+
in it is not plain text). The columns are headings, h2 by default;
|
|
623
|
+
`headingLevel` puts them under a section's heading.
|
|
624
|
+
- **Saving is the app's**, through `useRecordUpdate`, optimistic: cards
|
|
625
|
+
are placed by `getColumn` on the record, so a move shows at once and a
|
|
626
|
+
refusal puts the card back with the toast. Without `onMove` the board
|
|
627
|
+
is read-only, for a computed field.
|
|
628
|
+
- **A column is its own query** when a list can outgrow one page: give
|
|
629
|
+
it `count` (the total) and a `footer` linking the table filtered to it
|
|
630
|
+
when it holds more than it shows.
|
|
631
|
+
- **The card is short:** the title (a link to the record, cell pattern
|
|
632
|
+
16) and three or four values as glyphs, the same cells the table uses.
|
|
633
|
+
An empty value is left out, not shown as a dash.
|
|
634
|
+
- It owns its states: skeleton columns, `ErrorState`, and an empty
|
|
635
|
+
board; an empty column is a muted line, not an empty state. The
|
|
636
|
+
columns share the width, at least 16rem each, and scroll sideways with
|
|
637
|
+
snap points; a phone shows one column at a time, with the edge of the
|
|
638
|
+
next one showing.
|
|
@@ -13,8 +13,8 @@ to the device, where the app's service worker shows it.
|
|
|
13
13
|
| `usePushPermission()` | the package | Whether this device gets reminders, and `enable`/`disable` |
|
|
14
14
|
| `AllowReminders` | the package | The control: says what reminders are, then asks, on a click |
|
|
15
15
|
| `/push/public-key`, `/push/subscriptions` | `openapi.yaml`, the foundation's part (optional) | The VAPID key to subscribe with; this device's subscription saved or dropped |
|
|
16
|
-
| `
|
|
17
|
-
| `
|
|
16
|
+
| `foundation_backend.push` | `ui-foundation-backend` | `send_push(session, user_id, title, body, url)`: encrypted (RFC 8291), signed (VAPID), to every device the user allowed |
|
|
17
|
+
| `foundation_backend.reminders`, `reminders` | `ui-foundation-backend` | A reminder row (when, which days, the text, the path), and the `send-reminders` job that sends each one when it's due |
|
|
18
18
|
|
|
19
19
|
Off until the backend turns it on (`PUSH_ENABLED`, `docs/deploy.md`
|
|
20
20
|
"Reminders (Web Push)"). An app without reminders leaves `/push/*` out of
|
|
@@ -82,4 +82,4 @@ reads `mock` and never subscribes: screens show the `mock` state, and a
|
|
|
82
82
|
Playwright spec checks it the same way. The rest is the backend's: its
|
|
83
83
|
tests decrypt every message as a browser would. To try it end to end, run
|
|
84
84
|
the real backend with push on, allow reminders in the browser, then
|
|
85
|
-
`
|
|
85
|
+
`python -m foundation_backend.scripts.send_test_push <email>` (from `backend/`).
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { type ReactNode } from 'react';
|
|
2
|
+
import type { AppError } from '../../api/contracts.js';
|
|
3
|
+
export type BoardColumn<V extends string> = {
|
|
4
|
+
/** The field's value this column holds. */
|
|
5
|
+
value: V;
|
|
6
|
+
/** The column's heading: a word, or a glyph and a word. */
|
|
7
|
+
label: ReactNode;
|
|
8
|
+
/**
|
|
9
|
+
* The column's name as plain text: its entry in the default Move menu and
|
|
10
|
+
* in what a screen reader hears after a move. Omitted: `label` when it is
|
|
11
|
+
* a string, else `value`.
|
|
12
|
+
*/
|
|
13
|
+
name?: string;
|
|
14
|
+
/** The column's total, beside its heading. Omitted: the cards on the board are counted. */
|
|
15
|
+
count?: number;
|
|
16
|
+
/** The line an empty column shows. Default "Nothing here". */
|
|
17
|
+
empty?: ReactNode;
|
|
18
|
+
/** Under the cards: "Showing 100 of 240", a link to the whole list. */
|
|
19
|
+
footer?: ReactNode;
|
|
20
|
+
};
|
|
21
|
+
/** A value a card can be moved to from its Move menu. */
|
|
22
|
+
export type BoardMoveTarget<V extends string> = {
|
|
23
|
+
value: V;
|
|
24
|
+
label: string;
|
|
25
|
+
};
|
|
26
|
+
export type BoardProps<T, V extends string> = {
|
|
27
|
+
/** Names the board as a whole ("Tasks by status"). */
|
|
28
|
+
'aria-label': string;
|
|
29
|
+
columns: readonly BoardColumn<V>[];
|
|
30
|
+
items: readonly T[];
|
|
31
|
+
getId: (item: T) => string | number;
|
|
32
|
+
/** The record's value of the field: which column its card is in. */
|
|
33
|
+
getColumn: (item: T) => V;
|
|
34
|
+
/** What the Move button calls the record: "Move <name>". */
|
|
35
|
+
getName: (item: T) => string;
|
|
36
|
+
renderCard: (item: T) => ReactNode;
|
|
37
|
+
/** Saves the record's new value. Omitted: a read-only board. */
|
|
38
|
+
onMove?: (item: T, value: V) => void;
|
|
39
|
+
/** The Move menu's choices. Default: the columns, by their `name`. */
|
|
40
|
+
moveTo?: readonly BoardMoveTarget<V>[];
|
|
41
|
+
/** The columns' heading level: 2 under the page's h1 (the default), deeper under a section. */
|
|
42
|
+
headingLevel?: 2 | 3 | 4 | 5 | 6;
|
|
43
|
+
isLoading?: boolean;
|
|
44
|
+
error?: AppError | null;
|
|
45
|
+
onRetry?: () => void;
|
|
46
|
+
emptyTitle?: ReactNode;
|
|
47
|
+
emptyDescription?: ReactNode;
|
|
48
|
+
emptyAction?: ReactNode;
|
|
49
|
+
};
|
|
50
|
+
export declare function Board<T, V extends string>({ 'aria-label': ariaLabel, columns, items, getId, getColumn, getName, renderCard, onMove, moveTo, headingLevel, isLoading, error, onRetry, emptyTitle, emptyDescription, emptyAction, }: BoardProps<T, V>): import("react").JSX.Element;
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
// A board: a column per value of one choice field (a status, a stage), a
|
|
3
|
+
// card per record, and a record moved by moving its card. A second way to
|
|
4
|
+
// look at a list, beside its DataTable, never instead of it: filters, sort
|
|
5
|
+
// and saved views stay the table's.
|
|
6
|
+
//
|
|
7
|
+
// - Columns: `columns`, in order, each a `value` with its `label` (a stage
|
|
8
|
+
// circle and a word, say), an optional `count` (the column's total,
|
|
9
|
+
// which may be more than the cards loaded), an `empty` line, and a
|
|
10
|
+
// `footer` ("Showing 100 of 240", a link to the filtered list). A
|
|
11
|
+
// column whose `label` isn't plain text gives its `name` for the Move
|
|
12
|
+
// menu and what a screen reader hears after a move.
|
|
13
|
+
// - Cards: every record in `items` whose `getColumn` is one of the
|
|
14
|
+
// columns' values, in `items`' order; a record whose value has no column
|
|
15
|
+
// isn't shown. `renderCard` is the card's content: the title (a link to
|
|
16
|
+
// the record's page) and a few values.
|
|
17
|
+
// - Moving: with `onMove`, a card drags to another column (a pointer) and
|
|
18
|
+
// has a Move button (a menu of `moveTo`, by default the columns: the
|
|
19
|
+
// keyboard and a phone, where there is no drag). `moveTo` may add values
|
|
20
|
+
// with no column, such as Dropped, which take the card off the board.
|
|
21
|
+
// After a move from the menu the focus goes back to the card's Move
|
|
22
|
+
// button in its new column (to the board, if the card left it), and a
|
|
23
|
+
// status line says where the card went.
|
|
24
|
+
// Without `onMove` the board is read-only: a computed field.
|
|
25
|
+
// `onMove(item, value)` is the save; the app makes it optimistic
|
|
26
|
+
// (useRecordUpdate), so placing cards by `getColumn` on the record shows
|
|
27
|
+
// a move at once, and a refusal puts the card back.
|
|
28
|
+
// - States: `isLoading` (skeleton columns), `error` (ErrorState with
|
|
29
|
+
// `onRetry`), and an empty board (`emptyTitle`, `emptyDescription`,
|
|
30
|
+
// `emptyAction`), each for the board as a whole; an empty column is not
|
|
31
|
+
// an empty state, only a muted line.
|
|
32
|
+
//
|
|
33
|
+
// Columns share the width, at least 16rem each, and scroll sideways with
|
|
34
|
+
// snap points when they don't fit. A phone shows one column at a time,
|
|
35
|
+
// with the edge of the next one showing.
|
|
36
|
+
import { useEffect, useId, useRef, useState } from 'react';
|
|
37
|
+
import { ArrowRightLeftIcon } from 'lucide-react';
|
|
38
|
+
import { cn } from 'cn';
|
|
39
|
+
import { ErrorState } from './error-state.js';
|
|
40
|
+
import { Button } from '../ui/button.js';
|
|
41
|
+
import { Card } from '../ui/card.js';
|
|
42
|
+
import { DropdownMenu, DropdownMenuContent, DropdownMenuGroup, DropdownMenuLabel, DropdownMenuRadioGroup, DropdownMenuRadioItem, DropdownMenuTrigger, } from '../ui/dropdown-menu.js';
|
|
43
|
+
import { Empty, EmptyContent, EmptyDescription, EmptyHeader, EmptyTitle } from '../ui/empty.js';
|
|
44
|
+
import { Skeleton } from '../ui/skeleton.js';
|
|
45
|
+
// The drag's data type: a card of this board, and nothing a drop from
|
|
46
|
+
// elsewhere (a file, a link) carries.
|
|
47
|
+
const DRAG_TYPE = 'application/x-ui-foundation-board-card';
|
|
48
|
+
// At least 16rem, sharing the width; on a phone the board's width less 2rem,
|
|
49
|
+
// so the next column's edge shows there is more to scroll to.
|
|
50
|
+
const COLUMN_WIDTH = 'min-w-[calc(100%-2rem)] flex-1 basis-0 sm:min-w-64';
|
|
51
|
+
const columnName = (column) => column.name ?? (typeof column.label === 'string' ? column.label : column.value);
|
|
52
|
+
export function Board({ 'aria-label': ariaLabel, columns, items, getId, getColumn, getName, renderCard, onMove, moveTo, headingLevel = 2, isLoading = false, error = null, onRetry, emptyTitle = 'Nothing here yet', emptyDescription, emptyAction, }) {
|
|
53
|
+
const boardRef = useRef(null);
|
|
54
|
+
const [announcement, setAnnouncement] = useState('');
|
|
55
|
+
// A move from the Move menu, until its card is in its new column or off
|
|
56
|
+
// the board. The card is remounted there, so the Move button that had
|
|
57
|
+
// the focus is gone: its new one takes the focus, or the board does.
|
|
58
|
+
const moved = useRef(null);
|
|
59
|
+
useEffect(() => {
|
|
60
|
+
const pending = moved.current;
|
|
61
|
+
if (pending === null)
|
|
62
|
+
return;
|
|
63
|
+
const item = items.find((candidate) => String(getId(candidate)) === pending.id);
|
|
64
|
+
if (item !== undefined && getColumn(item) !== pending.value)
|
|
65
|
+
return;
|
|
66
|
+
moved.current = null;
|
|
67
|
+
const board = boardRef.current;
|
|
68
|
+
// Only when the focus was lost with the old button, never taken from
|
|
69
|
+
// wherever the user has gone since.
|
|
70
|
+
if (board === null || (document.activeElement !== null && document.activeElement !== document.body))
|
|
71
|
+
return;
|
|
72
|
+
const button = Array.from(board.querySelectorAll('[data-board-move]')).find((candidate) => candidate.dataset.boardMove === pending.id);
|
|
73
|
+
(button ?? board).focus();
|
|
74
|
+
}, [items, getId, getColumn]);
|
|
75
|
+
if (error) {
|
|
76
|
+
return (_jsx("div", { "data-slot": "board", "data-state": "error", children: _jsx(ErrorState, { error: error, onRetry: onRetry }) }));
|
|
77
|
+
}
|
|
78
|
+
if (isLoading) {
|
|
79
|
+
return (_jsx("div", { "data-slot": "board", "data-state": "loading", role: "group", "aria-busy": "true", "aria-label": ariaLabel, className: "flex gap-3 overflow-x-auto pb-2", children: columns.map((column) => (_jsxs("div", { className: cn('flex flex-col gap-2 rounded-lg bg-muted p-2', COLUMN_WIDTH), children: [_jsx(Skeleton, { className: "h-5 w-24" }), _jsx(Skeleton, { className: "h-20 w-full" }), _jsx(Skeleton, { className: "h-20 w-full" })] }, column.value))) }));
|
|
80
|
+
}
|
|
81
|
+
const shown = items.filter((item) => columns.some((column) => column.value === getColumn(item)));
|
|
82
|
+
if (shown.length === 0) {
|
|
83
|
+
return (_jsx("div", { "data-slot": "board", "data-state": "empty", children: _jsxs(Empty, { children: [_jsxs(EmptyHeader, { children: [_jsx(EmptyTitle, { children: emptyTitle }), emptyDescription && _jsx(EmptyDescription, { children: emptyDescription })] }), emptyAction && _jsx(EmptyContent, { children: emptyAction })] }) }));
|
|
84
|
+
}
|
|
85
|
+
const targets = moveTo ?? columns.map((column) => ({ value: column.value, label: columnName(column) }));
|
|
86
|
+
const move = (id, value, from) => {
|
|
87
|
+
const item = items.find((candidate) => String(getId(candidate)) === id);
|
|
88
|
+
if (onMove === undefined || item === undefined || getColumn(item) === value)
|
|
89
|
+
return;
|
|
90
|
+
const column = columns.find((candidate) => candidate.value === value);
|
|
91
|
+
const where = targets.find((target) => target.value === value)?.label ?? (column ? columnName(column) : value);
|
|
92
|
+
setAnnouncement(`Moved ${getName(item)} to ${where}`);
|
|
93
|
+
if (from === 'menu')
|
|
94
|
+
moved.current = { id, value };
|
|
95
|
+
onMove(item, value);
|
|
96
|
+
};
|
|
97
|
+
return (_jsxs("div", { ref: boardRef, "data-slot": "board", "data-state": "success", role: "group", "aria-label": ariaLabel, tabIndex: -1, className: "flex snap-x snap-mandatory gap-3 overflow-x-auto pb-2 outline-none", children: [columns.map((column) => {
|
|
98
|
+
const cards = shown.filter((item) => getColumn(item) === column.value);
|
|
99
|
+
return (_jsxs(BoardColumnView, { column: column, count: column.count ?? cards.length, headingLevel: headingLevel, onDrop: onMove ? (id) => move(id, column.value, 'drag') : undefined, children: [cards.map((item) => (_jsx(Card, { size: "sm", "data-slot": "board-card", draggable: onMove ? true : undefined, onDragStart: onMove
|
|
100
|
+
? (event) => {
|
|
101
|
+
event.dataTransfer.setData(DRAG_TYPE, String(getId(item)));
|
|
102
|
+
event.dataTransfer.effectAllowed = 'move';
|
|
103
|
+
}
|
|
104
|
+
: undefined, className: cn('gap-2 px-3 py-3', onMove && 'cursor-grab active:cursor-grabbing'), children: _jsxs("div", { className: "flex items-start gap-2", children: [_jsx("div", { className: "flex min-w-0 flex-1 flex-col gap-2", children: renderCard(item) }), onMove && (_jsx(MoveMenu, { id: String(getId(item)), name: getName(item), value: getColumn(item), targets: targets, onMove: (value) => move(String(getId(item)), value, 'menu') }))] }) }, getId(item)))), cards.length === 0 && (_jsx("p", { className: "px-1 py-6 text-center type-caption text-muted-foreground", children: column.empty ?? 'Nothing here' })), column.footer && _jsx("div", { className: "px-1 type-caption text-muted-foreground", children: column.footer })] }, column.value));
|
|
105
|
+
}), _jsx("span", { role: "status", className: "sr-only", children: announcement })] }));
|
|
106
|
+
}
|
|
107
|
+
function BoardColumnView({ column, count, headingLevel, onDrop, children, }) {
|
|
108
|
+
const [over, setOver] = useState(false);
|
|
109
|
+
const headingId = useId();
|
|
110
|
+
const Heading = `h${headingLevel}`;
|
|
111
|
+
const accepts = (event) => onDrop !== undefined && event.dataTransfer.types.includes(DRAG_TYPE);
|
|
112
|
+
return (_jsxs("section", { "aria-labelledby": headingId, "data-slot": "board-column", "data-over": over || undefined, className: cn('flex snap-start flex-col gap-2 rounded-lg bg-muted p-2 outline-2 outline-offset-2 outline-transparent data-over:outline-ring', COLUMN_WIDTH), onDragOver: (event) => {
|
|
113
|
+
if (!accepts(event))
|
|
114
|
+
return;
|
|
115
|
+
event.preventDefault();
|
|
116
|
+
event.dataTransfer.dropEffect = 'move';
|
|
117
|
+
setOver(true);
|
|
118
|
+
}, onDragLeave: (event) => {
|
|
119
|
+
if (!event.currentTarget.contains(event.relatedTarget))
|
|
120
|
+
setOver(false);
|
|
121
|
+
}, onDrop: (event) => {
|
|
122
|
+
setOver(false);
|
|
123
|
+
if (!accepts(event))
|
|
124
|
+
return;
|
|
125
|
+
event.preventDefault();
|
|
126
|
+
onDrop?.(event.dataTransfer.getData(DRAG_TYPE));
|
|
127
|
+
}, children: [_jsxs(Heading, { id: headingId, className: "flex items-center gap-2 px-1 pt-0.5 type-label", children: [column.label, _jsx("span", { className: "font-normal tabular-nums text-muted-foreground", children: count })] }), children] }));
|
|
128
|
+
}
|
|
129
|
+
function MoveMenu({ id, name, value, targets, onMove, }) {
|
|
130
|
+
return (_jsxs(DropdownMenu, { children: [_jsx(DropdownMenuTrigger, { render: _jsx(Button, { variant: "ghost", size: "icon-xs", "aria-label": `Move ${name}`, title: "Move to\u2026", "data-board-move": id }), children: _jsx(ArrowRightLeftIcon, { "aria-hidden": "true" }) }), _jsx(DropdownMenuContent, { align: "end", className: "w-44", children: _jsxs(DropdownMenuGroup, { children: [_jsx(DropdownMenuLabel, { children: "Move to" }), _jsx(DropdownMenuRadioGroup, { value: value, onValueChange: (next) => onMove(next), children: targets.map((target) => (_jsx(DropdownMenuRadioItem, { value: target.value, closeOnClick: true, children: target.label }, target.value))) })] }) })] }));
|
|
131
|
+
}
|
package/dist/index.d.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
export { AppShell, type AppShellProps, type AppShellVersion, type BottomBarItem, type NavEntry, type NavItem, type NavSection, } from './components/app/app-shell.js';
|
|
2
2
|
export type { IconComponent } from './components/app/icon-component.js';
|
|
3
|
+
export { Board, type BoardColumn, type BoardMoveTarget, type BoardProps } from './components/app/board.js';
|
|
3
4
|
export { CountLink, type CountLinkProps } from './components/app/count-link.js';
|
|
4
5
|
export { DataTable, type DataTableProps, type DataTableTotals, type DataTableWidth, type SortingState, } from './components/app/data-table.js';
|
|
5
6
|
export { type RowGroup } from './components/app/table-groups.js';
|
package/dist/index.js
CHANGED
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
// points (`/gateway`, `/mocks`, `/testing`) so each stays behind its boundary.
|
|
5
5
|
// Composites
|
|
6
6
|
export { AppShell, } from './components/app/app-shell.js';
|
|
7
|
+
export { Board } from './components/app/board.js';
|
|
7
8
|
export { CountLink } from './components/app/count-link.js';
|
|
8
9
|
export { DataTable, } from './components/app/data-table.js';
|
|
9
10
|
export {} from './components/app/table-groups.js';
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tristan2828/ui-foundation",
|
|
3
|
-
"version": "4.
|
|
3
|
+
"version": "4.36.0",
|
|
4
4
|
"description": "Shared UI foundation for contract-first, database-backed React apps: shadcn primitives, DataTable/EntityForm composites, auth, the gateway error seam, design tokens, lint rules and the AI playbook.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|