@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–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
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. Set `skeletonRows` to the number it usually holds, so the card
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' }`. Its accessible
18
- * name is `label`, described by the title, so several "All" links on one
19
- * page stay distinct.
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.36.0",
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": {