@tristan2828/ui-foundation 3.22.0 → 3.23.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.
@@ -10,14 +10,14 @@ alternatives ([`column-options.md`](column-options.md)), not designed in the abs
10
10
  Patterns 12 (boolean), 15 (yes/no flipped in the row), 16 (title linking
11
11
  to its view), 17 (quick action on the view) and 18 (editable value) are
12
12
  the template's own references: Widget's In Stock and Name columns and its
13
- view render them, and their specs pin them. Pattern 19 (stage circle)
14
- and the multi-value variant of 4, the dependency list in 10 and the
15
- pressed icon in 15 were each built for a real app's screen first; the
16
- package's Storybook renders them (`app/StageCircle`,
17
- `patterns/CellPatterns`), and its checks pin their accessibility and
18
- contrast in both themes. The other patterns from 11 on are
19
- marked **Unproven**: written ahead of a real
20
- column, so no app has tested them on real data yet. Prefer a proven
13
+ view render them, and their specs pin them. Patterns 19 (stage circle)
14
+ and 20 (count linking to the related records), the multi-value variant
15
+ of 4, the dependency list in 10 and the pressed icon in 15 were each
16
+ built for a real app's screen first; the package's Storybook renders
17
+ them (`app/StageCircle`, `app/CountLink`, `patterns/CellPatterns`), and
18
+ its checks pin their accessibility and contrast in both themes. The
19
+ other patterns from 11 on are marked **Unproven**: written ahead of a
20
+ real column, so no app has tested them on real data yet. Prefer a proven
21
21
  pattern when one fits. When you use an unproven one, say so in the
22
22
  entity's plan, and raise what you learned for the foundation, so the
23
23
  entry can lose its label. A pattern that proves generic is worth raising
@@ -836,3 +836,69 @@ cell: ({ getValue }) => {
836
836
  `<EditableValue>` like a badge.
837
837
  - **Cost:** a reader learns the shapes once. Until then the word does the
838
838
  work, which is why it stays.
839
+
840
+ ## 20. Count linking to the related records
841
+
842
+ A count of the records in another table that point at this row (open
843
+ Tasks per Project, items per Tag), which opens that table already filtered
844
+ to them. The count is a `computed` integer (the entity plan's type).
845
+ The package ships it: `CountLink`, rendered in its Storybook
846
+ (`app/CountLink`).
847
+
848
+ ```tsx
849
+ import { CountLink } from '@tristan2828/ui-foundation'
850
+
851
+ cell: ({ row }) => {
852
+ const { id, name, openTaskCount: count } = row.original
853
+ return (
854
+ <CountLink
855
+ count={count}
856
+ to={`/tasks?project=${id}`}
857
+ label={`open ${count === 1 ? 'task' : 'tasks'} in ${name}`}
858
+ />
859
+ )
860
+ }
861
+ ```
862
+
863
+ What the component decides, so a cell doesn't have to:
864
+
865
+ - **Underlined at rest**, not only on hover as in pattern 16. A title
866
+ reads as a name to open; a bare number doesn't look like it goes
867
+ anywhere until it's underlined. Hover thickens the line. Foreground,
868
+ not `text-link`: `--link` is for links in written text, and a column of
869
+ blue numbers pulls the eye from every other column.
870
+ - **Zero is a plain `0`**, not a link (a link to an empty list is a dead
871
+ end) and not an em dash: zero is a value (pattern 11). Same
872
+ `tabular-nums`, so it lines up with the links.
873
+ - **The accessible name is the count, then `label`**: `3 open tasks in
874
+ Kitchen remodel`. Every row's link text is just a number, so `3` alone
875
+ isn't distinct, and starting with it keeps the link findable by what
876
+ it shows (speech input, `getByRole('link', { name: /^3 / })`).
877
+
878
+ What the cell decides:
879
+
880
+ - **`label` says what's counted and which row**, singular for one
881
+ (`open task in Garden`).
882
+ - **The target is the other entity's list, filtered by the URL**:
883
+ `/<other>?<filterName>=<id>`, the same query string `useTableUrlState`
884
+ reads (a multi-value filter takes the id the same way). Back, forward,
885
+ a refresh and saved views keep working, and the other list needs a
886
+ filter by this reference (`Filter: yes` in its plan).
887
+ - **The count and the list it opens must agree.** If the target list
888
+ narrows by default (hides finished records unless asked), the count
889
+ applies the same condition, and the word in the name says so (`open`
890
+ tasks). Where the list has a filter for that condition, prefer putting
891
+ it in the link too (`?project=7&status=open`), so the list shows what
892
+ was counted even if its default changes. Say the rule in the field's
893
+ `description` in `openapi.yaml` (`Tasks in this project that aren't
894
+ done; what /tasks?project=<id> lists`), so the backend, the mocks and
895
+ the next agent count the same thing.
896
+ - **Sorted on the server, as a number**, like any computed field: the
897
+ scalar subquery is the sort expression, with a stable tiebreak (the
898
+ name, then the id) so rows with the same count don't change places
899
+ between pages.
900
+ - **Cost:** one correlated subquery per row read. Fine at an app's scale;
901
+ give the other table's foreign key column an index, since every row's
902
+ count reads it.
903
+ - **On the record's view**, the same `CountLink` beside its label (`Open
904
+ tasks: 3`), with the record's name in `label` like the table's.
@@ -194,6 +194,10 @@ shade: note `info`, tip `success`, important category slot 2, warning
194
194
  `warning`, caution `destructive`. The package draws all of it; an app
195
195
  only renders `<Markdown>` or the editor.
196
196
 
197
+ `--link` is for written text only. A link in a table cell is
198
+ `text-foreground` and underlined, never blue: a column of coloured text
199
+ pulls the eye from every other column (cell patterns 16 and 20).
200
+
197
201
  ## Density
198
202
 
199
203
  Tables have three densities, set with `data-density` on **any ancestor**
@@ -43,7 +43,8 @@ One or two sentences: what one record is, and who uses the screens.
43
43
  every read and never stores it — give its rule, and if it's a choice,
44
44
  its values in sort order with a label for each; a read-only column, a filter and a sort the
45
45
  server evaluates, never on the form). A count of related records is a
46
- `computed` integer.
46
+ `computed` integer; say whether its column links to those records
47
+ (cell pattern 20) and which of them it counts (`open tasks`).
47
48
  - *Close variants* (built by a small, stated change to the nearest
48
49
  pattern): `integer` (from decimal), `url` (from email — a format
49
50
  check), `date` (from date-time — no time part).
@@ -0,0 +1,14 @@
1
+ import { type To } from 'react-router';
2
+ export interface CountLinkProps {
3
+ /** How many related records there are. Count what `to` lists. */
4
+ count: number;
5
+ /** The other entity's list, filtered to this row: `/tasks?project=7`. */
6
+ to: To;
7
+ /**
8
+ * What's counted and for which row, read after the count: `open tasks in
9
+ * Kitchen remodel` (`open task` for one).
10
+ */
11
+ label: string;
12
+ className?: string;
13
+ }
14
+ export declare function CountLink({ count, to, label, className }: CountLinkProps): import("react").JSX.Element;
@@ -0,0 +1,20 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ // A count linking to the related records (cell pattern 20): how many
3
+ // records of another entity point at this row, as a link to that entity's
4
+ // list filtered to them (`/tasks?project=7`, the query string
5
+ // useTableUrlState reads).
6
+ //
7
+ // - Underlined at rest: a bare number doesn't look like it goes anywhere.
8
+ // Foreground, not text-link: --link is for written text.
9
+ // - Zero is a plain 0, not a link (a list of nothing is a dead end) and
10
+ // not an em dash (zero is a value).
11
+ // - The accessible name is the count, then `label`, so every row's link is
12
+ // distinct ("3 open tasks in Kitchen remodel") and still starts with
13
+ // what it shows.
14
+ import { Link } from 'react-router';
15
+ import { cn } from 'cn';
16
+ export function CountLink({ count, to, label, className }) {
17
+ if (count === 0)
18
+ return _jsx("span", { className: cn('tabular-nums', className), children: "0" });
19
+ return (_jsx(Link, { to: to, "aria-label": `${count} ${label}`, className: cn('rounded-sm tabular-nums text-foreground underline underline-offset-4 outline-none', 'hover:decoration-2 focus-visible:ring-3 focus-visible:ring-ring/50', className), children: count }));
20
+ }
package/dist/index.d.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  export { AppShell, type AppShellProps, type NavItem } from './components/app/app-shell.js';
2
+ export { CountLink, type CountLinkProps } from './components/app/count-link.js';
2
3
  export { DataTable, type DataTableProps, type SortingState } from './components/app/data-table.js';
3
4
  export { EntityForm, type EntityFormProps } from './components/app/entity-form.js';
4
5
  export { EntityView, type EntityViewField, type EntityViewProps, type EntityViewSection, } from './components/app/entity-view.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 { CountLink } from './components/app/count-link.js';
7
8
  export { DataTable } from './components/app/data-table.js';
8
9
  export { EntityForm } from './components/app/entity-form.js';
9
10
  export { EntityView, } from './components/app/entity-view.js';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tristan2828/ui-foundation",
3
- "version": "3.22.0",
3
+ "version": "3.23.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": {