@tristan2828/ui-foundation 4.36.0 → 4.37.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.
|
@@ -12,11 +12,11 @@
|
|
|
12
12
|
- [Typography](#typography) · lines 242–324
|
|
13
13
|
- [Written text (notes)](#written-text-notes) · lines 273–324
|
|
14
14
|
- [Density](#density) · lines 326–383
|
|
15
|
-
- [Widget cards (hub pages)](#widget-cards-hub-pages) · lines 385–
|
|
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
|
|
19
|
-
- [A list as a board](#a-list-as-a-board) · lines
|
|
15
|
+
- [Widget cards (hub pages)](#widget-cards-hub-pages) · lines 385–463
|
|
16
|
+
- [Module cards (a hub's places)](#module-cards-a-hubs-places) · lines 465–523
|
|
17
|
+
- [Notices](#notices) · lines 525–553
|
|
18
|
+
- [A value over time](#a-value-over-time) · lines 555–588
|
|
19
|
+
- [A list as a board](#a-list-as-a-board) · lines 590–641
|
|
20
20
|
|
|
21
21
|
<!-- /index -->
|
|
22
22
|
|
|
@@ -424,7 +424,10 @@ const { data, isLoading, error, refetch } = useReadyTasksQuery() // one page of
|
|
|
424
424
|
- **Three to six rows.** A widget is a sample, not the list: ask the API
|
|
425
425
|
for that many (the query's page size) and put the rest behind the
|
|
426
426
|
header `link` ("All", "All doing"), which goes to the full list
|
|
427
|
-
filtered the same way.
|
|
427
|
+
filtered the same way. A widget about a service outside the app links
|
|
428
|
+
its header there instead: `link={{ href: 'https://…', label: 'Open
|
|
429
|
+
<service>' }}` opens it in a new tab, with the external-link mark and
|
|
430
|
+
"(opens in a new tab)" for a screen reader. Set `skeletonRows` to the number it usually holds, so the card
|
|
428
431
|
keeps its height when the data arrives.
|
|
429
432
|
- **Each widget owns its four states.** Loading is a skeleton of rows,
|
|
430
433
|
empty is one quiet line the app writes ("Nothing waiting", not "No
|
|
@@ -68,6 +68,18 @@ Scope it to the signed-in user, as Widgets are.
|
|
|
68
68
|
sends a reminder when today is one of its days in its time zone and the
|
|
69
69
|
time has come, once a day at most. More than 15 minutes late (the
|
|
70
70
|
backend was down) and it's skipped for the day rather than sent late.
|
|
71
|
+
- **Reminders in a table of the app's own** (another schema, another
|
|
72
|
+
name, as an app with a schema per module keeps them): define its table
|
|
73
|
+
model with the same columns (`id`, `user_id`, `title`, `body`, `url`,
|
|
74
|
+
`time_of_day`, `days`, `time_zone`, `enabled`, `last_sent_on`) and hand
|
|
75
|
+
it to the job, `reminders.register(model=MyReminder)` in place of
|
|
76
|
+
`reminders.register()`. Left out, the job reads the package's
|
|
77
|
+
`reminders` table. Neither table is on `SQLModel.metadata` until the
|
|
78
|
+
app uses it: `push_subscriptions` arrives with `foundation_backend.push`
|
|
79
|
+
(and `foundation_backend.reminders`, which sends through it), the
|
|
80
|
+
package's `reminders` only with its `Reminder` model (imported, or the
|
|
81
|
+
job run without a model). So an app without push, or with its own
|
|
82
|
+
table, gets neither from `create_all` or autogenerate.
|
|
71
83
|
- **Something else to send?** Call `send_push` from the app's own code or
|
|
72
84
|
job: a message when a long task finishes, a daily summary. The `url` is
|
|
73
85
|
always the app's own path.
|
|
@@ -14,14 +14,12 @@ export interface WidgetCardProps {
|
|
|
14
14
|
headingLevel?: 2 | 3 | 4 | 5 | 6;
|
|
15
15
|
/**
|
|
16
16
|
* A link on the right of the header to everything the widget samples:
|
|
17
|
-
* `{ to: '/tasks?status=doing', label: 'All doing' }
|
|
18
|
-
*
|
|
19
|
-
*
|
|
17
|
+
* `{ to: '/tasks?status=doing', label: 'All doing' }`, or an outside
|
|
18
|
+
* address, opened in a new tab: `{ href: 'https://example.com', label:
|
|
19
|
+
* 'Open Example' }`. Its accessible name is `label`, described by the
|
|
20
|
+
* title, so several "All" links on one page stay distinct.
|
|
20
21
|
*/
|
|
21
|
-
link?:
|
|
22
|
-
to: To;
|
|
23
|
-
label: string;
|
|
24
|
-
};
|
|
22
|
+
link?: WidgetCardLink;
|
|
25
23
|
isLoading: boolean;
|
|
26
24
|
error?: AppError | null;
|
|
27
25
|
onRetry?: () => void;
|
|
@@ -44,6 +42,15 @@ export interface WidgetCardProps {
|
|
|
44
42
|
children?: ReactNode;
|
|
45
43
|
className?: string;
|
|
46
44
|
}
|
|
45
|
+
export type WidgetCardLink = {
|
|
46
|
+
to: To;
|
|
47
|
+
label: string;
|
|
48
|
+
href?: never;
|
|
49
|
+
} | {
|
|
50
|
+
href: string;
|
|
51
|
+
label: string;
|
|
52
|
+
to?: never;
|
|
53
|
+
};
|
|
47
54
|
export declare function WidgetCard({ title, icon: Icon, headingLevel, link, isLoading, error, onRetry, isEmpty, emptyMessage, skeletonRows, collapsible, defaultCollapsed, storageKey, children, className, }: WidgetCardProps): import("react").JSX.Element;
|
|
48
55
|
/** The rows of a WidgetCard: a list with a divider between rows. */
|
|
49
56
|
export declare function WidgetList({ children, className }: {
|
|
@@ -24,6 +24,11 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
|
24
24
|
// where that's blocked). The body is hidden, not unmounted, so whatever
|
|
25
25
|
// the app keeps in it stays put.
|
|
26
26
|
//
|
|
27
|
+
// The header's link is an app route (`to`) or, for a widget about a
|
|
28
|
+
// service outside the app, its address (`href`, #248): that one opens in a
|
|
29
|
+
// new tab, with the outside-link mark and a screen reader's "(opens in a
|
|
30
|
+
// new tab)", as WidgetListItem's `href` does.
|
|
31
|
+
//
|
|
27
32
|
// WidgetList and WidgetListItem are the common body: rows with dividers,
|
|
28
33
|
// each a primary link (or plain text) truncated to one line, an optional
|
|
29
34
|
// muted note ("2 h ago") and an optional trailing element (a badge, a
|
|
@@ -52,7 +57,7 @@ export function WidgetCard({ title, icon: Icon, headingLevel = 2, link, isLoadin
|
|
|
52
57
|
const state = error ? 'error' : isLoading ? 'loading' : isEmpty ? 'empty' : 'success';
|
|
53
58
|
const [folded, setFolded] = useRememberedFlag(collapsible && storageKey !== undefined ? `${storageKey}:collapsed` : undefined, defaultCollapsed);
|
|
54
59
|
const collapsed = collapsible && folded;
|
|
55
|
-
return (_jsx("section", { "aria-labelledby": headingId, "data-slot": "widget-card", "data-state": state, "data-collapsed": collapsed || undefined, className: cn('min-w-0 self-start', className), children: _jsxs(Card, { className: "density-compact:[--card-spacing:--spacing(3)]", children: [_jsxs(CardHeader, { className: "flex items-center justify-between gap-3", children: [_jsxs(Heading, { id: headingId, className: "flex min-w-0 items-center gap-2 type-section-title text-foreground", children: [Icon && _jsx(Icon, { "aria-hidden": "true", className: "size-4 shrink-0 text-muted-foreground" }), _jsx("span", { className: "truncate", children: title })] }), (link || collapsible) && (_jsxs("div", { className: "flex shrink-0 items-center gap-1", children: [link && (_jsx(Link, { to: link.to, "aria-describedby": headingId, className: HEADER_LINK_CLASS, children: link.label })), collapsible && (_jsx(Button, { variant: "ghost", size: "icon-sm", "aria-labelledby": headingId, "aria-expanded": !collapsed, "aria-controls": contentId, onClick: () => setFolded(!collapsed),
|
|
60
|
+
return (_jsx("section", { "aria-labelledby": headingId, "data-slot": "widget-card", "data-state": state, "data-collapsed": collapsed || undefined, className: cn('min-w-0 self-start', className), children: _jsxs(Card, { className: "density-compact:[--card-spacing:--spacing(3)]", children: [_jsxs(CardHeader, { className: "flex items-center justify-between gap-3", children: [_jsxs(Heading, { id: headingId, className: "flex min-w-0 items-center gap-2 type-section-title text-foreground", children: [Icon && _jsx(Icon, { "aria-hidden": "true", className: "size-4 shrink-0 text-muted-foreground" }), _jsx("span", { className: "truncate", children: title })] }), (link || collapsible) && (_jsxs("div", { className: "flex shrink-0 items-center gap-1", children: [link?.href !== undefined ? (_jsxs("a", { href: link.href, target: "_blank", rel: "noreferrer", "aria-describedby": headingId, className: cn(HEADER_LINK_CLASS, 'inline-flex items-center gap-1'), children: [link.label, _jsx(ExternalLinkIcon, { "aria-hidden": "true", className: "size-3 shrink-0 text-muted-foreground" }), _jsx("span", { className: "sr-only", children: " (opens in a new tab)" })] })) : (link && (_jsx(Link, { to: link.to, "aria-describedby": headingId, className: HEADER_LINK_CLASS, children: link.label }))), collapsible && (_jsx(Button, { variant: "ghost", size: "icon-sm", "aria-labelledby": headingId, "aria-expanded": !collapsed, "aria-controls": contentId, onClick: () => setFolded(!collapsed),
|
|
56
61
|
// The button's own open look (a filled background) would
|
|
57
62
|
// read as pressed: the chevron says it.
|
|
58
63
|
className: "-my-1 -mr-1.5 aria-expanded:not-hover:bg-transparent", children: _jsx(ChevronDownIcon, { "aria-hidden": "true", className: cn('transition-transform', collapsed && '-rotate-90') }) }))] }))] }), _jsx(CardContent, { id: contentId, hidden: collapsed, "aria-busy": state === 'loading' || undefined, children: error ? (_jsx(ErrorState, { error: error, onRetry: onRetry, size: "compact" })) : state === 'loading' ? (_jsx(WidgetListSkeleton, { rows: skeletonRows })) : state === 'empty' ? (_jsx("p", { "data-state": "empty", className: "py-2 type-body text-muted-foreground", children: emptyMessage })) : (children) })] }) }));
|
package/dist/index.d.ts
CHANGED
|
@@ -40,7 +40,7 @@ export { DEFAULT_IMAGE_TYPES, type ImageUpload, type ImageUploadResult } from '.
|
|
|
40
40
|
export { formatPlainDate, type PlainDate } from './lib/plain-date.js';
|
|
41
41
|
export { ChoiceCell, LinkCell, NameCell, type ChoiceCellProps, type LinkCellProps, type NameCellProps, } from './components/app/row-edit-cells.js';
|
|
42
42
|
export { ModuleCard, ModuleStrip, type CategorySlot, type ModuleCardLink, type ModuleCardPlace, type ModuleCardProps, type ModuleStripProps, } from './components/app/module-card.js';
|
|
43
|
-
export { WidgetCard, WidgetList, WidgetListItem, type WidgetCardProps, type WidgetListItemProps, } from './components/app/widget-card.js';
|
|
43
|
+
export { WidgetCard, WidgetList, WidgetListItem, type WidgetCardLink, type WidgetCardProps, type WidgetListItemProps, } from './components/app/widget-card.js';
|
|
44
44
|
export { ThemeProvider } from './components/theme-provider.js';
|
|
45
45
|
export { LoginRoute, type LoginRouteProps } from './routes/login.js';
|
|
46
46
|
export { RegisterRoute } from './routes/register.js';
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tristan2828/ui-foundation",
|
|
3
|
-
"version": "4.
|
|
3
|
+
"version": "4.37.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": {
|