@charcuterie/ui 3.34.0 → 4.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.
Files changed (87) hide show
  1. package/dist/ActionTiles/ActionTiles.d.ts +66 -8
  2. package/dist/ActionTiles/ActionTiles.d.ts.map +1 -1
  3. package/dist/ActionTiles/ActionTiles.js +88 -11
  4. package/dist/ActionTiles/ActionTiles.js.map +1 -1
  5. package/dist/Main/Main.d.ts +18 -19
  6. package/dist/Main/Main.d.ts.map +1 -1
  7. package/dist/Main/Main.js +21 -10
  8. package/dist/Main/Main.js.map +1 -1
  9. package/dist/Main/ScrollMemoryProvider.d.ts +38 -0
  10. package/dist/Main/ScrollMemoryProvider.d.ts.map +1 -0
  11. package/dist/Main/ScrollMemoryProvider.js +30 -0
  12. package/dist/Main/ScrollMemoryProvider.js.map +1 -0
  13. package/dist/Main/scrollMemory.d.ts +54 -28
  14. package/dist/Main/scrollMemory.d.ts.map +1 -1
  15. package/dist/Main/scrollMemory.js +81 -40
  16. package/dist/Main/scrollMemory.js.map +1 -1
  17. package/dist/Main/scrollMemoryContext.d.ts +18 -0
  18. package/dist/Main/scrollMemoryContext.d.ts.map +1 -0
  19. package/dist/Main/scrollMemoryContext.js +18 -0
  20. package/dist/Main/scrollMemoryContext.js.map +1 -0
  21. package/dist/Main/useScrollMemory.d.ts +20 -18
  22. package/dist/Main/useScrollMemory.d.ts.map +1 -1
  23. package/dist/Main/useScrollMemory.js +69 -42
  24. package/dist/Main/useScrollMemory.js.map +1 -1
  25. package/dist/PortraitTiles/PortraitAvatar.d.ts +21 -0
  26. package/dist/PortraitTiles/PortraitAvatar.d.ts.map +1 -0
  27. package/dist/PortraitTiles/PortraitAvatar.js +29 -0
  28. package/dist/PortraitTiles/PortraitAvatar.js.map +1 -0
  29. package/dist/PortraitTiles/PortraitTiles.d.ts +162 -0
  30. package/dist/PortraitTiles/PortraitTiles.d.ts.map +1 -0
  31. package/dist/PortraitTiles/PortraitTiles.js +263 -0
  32. package/dist/PortraitTiles/PortraitTiles.js.map +1 -0
  33. package/dist/PortraitTiles/portraitColour.d.ts +49 -0
  34. package/dist/PortraitTiles/portraitColour.d.ts.map +1 -0
  35. package/dist/PortraitTiles/portraitColour.js +55 -0
  36. package/dist/PortraitTiles/portraitColour.js.map +1 -0
  37. package/dist/categoricalStyles.d.ts +27 -0
  38. package/dist/categoricalStyles.d.ts.map +1 -1
  39. package/dist/categoricalStyles.js +49 -0
  40. package/dist/categoricalStyles.js.map +1 -1
  41. package/dist/index.d.ts +7 -2
  42. package/dist/index.d.ts.map +1 -1
  43. package/dist/index.js +3 -1
  44. package/dist/index.js.map +1 -1
  45. package/dist/reactRouter/ReactRouterAdapter.d.ts +43 -0
  46. package/dist/reactRouter/ReactRouterAdapter.d.ts.map +1 -0
  47. package/dist/reactRouter/ReactRouterAdapter.js +50 -0
  48. package/dist/reactRouter/ReactRouterAdapter.js.map +1 -0
  49. package/dist/reactRouter/index.d.ts +14 -8
  50. package/dist/reactRouter/index.d.ts.map +1 -1
  51. package/dist/reactRouter/index.js +13 -8
  52. package/dist/reactRouter/index.js.map +1 -1
  53. package/dist/tileStyles.d.ts +15 -0
  54. package/dist/tileStyles.d.ts.map +1 -1
  55. package/dist/tileStyles.js +26 -1
  56. package/dist/tileStyles.js.map +1 -1
  57. package/package.json +2 -2
  58. package/src/ActionTiles/ActionTiles.mdx +64 -6
  59. package/src/ActionTiles/ActionTiles.stories.tsx +107 -2
  60. package/src/ActionTiles/ActionTiles.test.tsx +205 -5
  61. package/src/ActionTiles/ActionTiles.tsx +274 -110
  62. package/src/Main/Main.mdx +35 -7
  63. package/src/Main/Main.stories.tsx +129 -64
  64. package/src/Main/Main.test.tsx +58 -0
  65. package/src/Main/Main.tsx +20 -20
  66. package/src/Main/ScrollMemoryProvider.tsx +48 -0
  67. package/src/Main/scrollMemory.test.ts +70 -16
  68. package/src/Main/scrollMemory.ts +98 -40
  69. package/src/Main/scrollMemoryContext.ts +21 -0
  70. package/src/Main/useScrollMemory.ts +83 -43
  71. package/src/PortraitTiles/PortraitAvatar.tsx +49 -0
  72. package/src/PortraitTiles/PortraitTiles.mdx +153 -0
  73. package/src/PortraitTiles/PortraitTiles.stories.tsx +403 -0
  74. package/src/PortraitTiles/PortraitTiles.test.tsx +399 -0
  75. package/src/PortraitTiles/PortraitTiles.tsx +577 -0
  76. package/src/PortraitTiles/portraitColour.test.ts +39 -0
  77. package/src/PortraitTiles/portraitColour.ts +67 -0
  78. package/src/RadioGroup/RadioGroup.stories.tsx +39 -0
  79. package/src/RouterLink/RouterLink.mdx +53 -16
  80. package/src/categoricalStyles.ts +57 -0
  81. package/src/index.ts +12 -0
  82. package/src/reactRouter/ReactRouterAdapter.stories.tsx +120 -0
  83. package/src/reactRouter/ReactRouterAdapter.test.tsx +92 -0
  84. package/src/reactRouter/ReactRouterAdapter.tsx +66 -0
  85. package/src/reactRouter/index.ts +14 -8
  86. package/src/sourceRules.test.ts +19 -3
  87. package/src/tileStyles.ts +30 -1
@@ -0,0 +1,162 @@
1
+ import type { CategoricalIndex, ControlSize } from "@charcuterie/tokens";
2
+ import type { ReactNode } from "react";
3
+ /**
4
+ * Where one portrait's colour comes from. Two arms, and the fleet
5
+ * has both — the same split
6
+ * [`cardAccentEdge.ts`](../Card/cardAccentEdge.ts) already draws,
7
+ * for the same reason.
8
+ *
9
+ * - **`categorical`** — one of the ten audited hues. A colour the
10
+ * library owns, re-themed by `data-variant`, gated in both
11
+ * schemes. Right whenever the colour is a *choice from what we
12
+ * offered*.
13
+ * - **`color`** — any CSS colour, from data. Right when the colour
14
+ * **arrived from the world** and re-theming it would be lying
15
+ * about a physical object. points-market is exactly this: each
16
+ * kid's colour matches the NFC card they tap, by the workspace's
17
+ * 2026-07-20 `kid-identity-colors-match-nfc-cards` record. Ten
18
+ * palette hues cannot represent that, and swapping them in would
19
+ * have made the app disagree with the cards on the fridge.
20
+ *
21
+ * Neither is a default. A portrait that names no colour takes the
22
+ * next `categorical` hue by position.
23
+ */
24
+ type PortraitTileColour = {
25
+ categorical?: CategoricalIndex;
26
+ color?: never;
27
+ } | {
28
+ categorical?: never;
29
+ color: string;
30
+ };
31
+ export type PortraitTileItem = PortraitTileColour & {
32
+ /**
33
+ * The quiet line under the number, naming its unit — "points",
34
+ * "episodes left", "due today".
35
+ *
36
+ * Inside the tile, so it is part of the accessible name. "1,240"
37
+ * on its own tells a screen-reader user nothing.
38
+ */
39
+ hint?: ReactNode;
40
+ /** Makes the portrait a real `<a href>`. Same rules as `ActionTiles`. */
41
+ href?: string;
42
+ /**
43
+ * A picture of the subject. Falls back to {@link initials} when
44
+ * absent, and **also** when the file 404s — a broken portrait grid
45
+ * is the one failure this shape cannot absorb.
46
+ */
47
+ imageSrc?: string;
48
+ /**
49
+ * Drawn in the hue's solid fill when there is no picture. One or
50
+ * two characters; longer strings are the caller's problem to
51
+ * shorten, because "which letters" is a naming question this
52
+ * component cannot answer (a mononym, a family that shares an
53
+ * initial).
54
+ */
55
+ initials?: string;
56
+ isDisabled?: boolean;
57
+ /** Opens in a new tab, and says so to a screen reader. `href` only. */
58
+ isExternal?: boolean;
59
+ /** The name. */
60
+ label: ReactNode;
61
+ /**
62
+ * The one number this portrait is chosen by — a balance, a count,
63
+ * a streak. Optional: a picker that is only ever "who are you"
64
+ * passes none and the tile closes up around the name.
65
+ */
66
+ stat?: ReactNode;
67
+ /** Identity. The React key, and what `onChoose` is handed. */
68
+ value: string;
69
+ };
70
+ /**
71
+ * How a portrait arranges itself.
72
+ *
73
+ * - `auto` — a **row** while the set is narrow, a **column** once
74
+ * it has room. The switch is a container query on the set, not a
75
+ * media query: a picker in a 320px sidebar is a list of rows on a
76
+ * 2560px monitor, and the same picker filling a phone is a column
77
+ * per screenful.
78
+ * - `row` / `column` — fixed, for a layout that has already decided.
79
+ */
80
+ export type PortraitTilesLayout = "auto" | "column" | "row";
81
+ export type PortraitTilesProps = {
82
+ className?: string;
83
+ items: readonly PortraitTileItem[];
84
+ /** The set's accessible name. Required. */
85
+ label: string;
86
+ layout?: PortraitTilesLayout;
87
+ /** The narrowest a portrait track may be, in CSS px. */
88
+ minTileInlineSize?: number;
89
+ /** What `isExternal` announces. Not shown. */
90
+ newTabLabel?: string;
91
+ /** Fired by a portrait that carries no `href`. */
92
+ onChoose?: (value: string) => void;
93
+ size?: ControlSize;
94
+ };
95
+ /**
96
+ * A set of PEOPLE — or of anything else picked by its face — drawn
97
+ * as round pictures with a name and one big number.
98
+ *
99
+ * points-market's "Who's shopping?" is the shape this is taken from,
100
+ * and it is deliberately **not** an `ActionTiles` variant. The two
101
+ * answer different questions:
102
+ *
103
+ * - `ActionTiles` asks *what do you want to do* — a name, a line of
104
+ * help, an icon that illustrates the verb.
105
+ * - This asks *who is this* — a face, and one number that decides
106
+ * it. There is no description, because a person is not explained.
107
+ *
108
+ * Collapsing them would mean one component whose icon is sometimes a
109
+ * 20px glyph beside the title and sometimes a 144px circle above it,
110
+ * with `hint` meaning "what this does" in one mode and "points" in
111
+ * the other. That is two components wearing one name, which is the
112
+ * mistake the
113
+ * [2026-08-25 record](../../../../docs/decisions/2026-08-25-a-choice-tile-is-a-radiogroup-shape-not-a-third-component.md)
114
+ * warns about from the other direction.
115
+ *
116
+ * What they DO share is the box — `TILE_BOX_CLASS` and the same
117
+ * `auto-fill` grid — so a page carrying both does not show two
118
+ * different cards.
119
+ *
120
+ * ### It scales, and the scaling is the point
121
+ *
122
+ * The owner's one requirement when this moved into the library:
123
+ * *"that one is meant to scale. I don't wanna lose that
124
+ * functionality."* Two axes carry it.
125
+ *
126
+ * 1. **The container.** `layout="auto"` is a row while the set is
127
+ * narrow and a column once it has room, off a `cq-sm` query on
128
+ * the set itself. Nothing reads the viewport, so the same picker
129
+ * works in a sidebar and on a wall.
130
+ * 2. **The density.** Every length is a token, so `data-density="kiosk"`
131
+ * grows the type ramp and the control heights under it. points-market
132
+ * runs `kiosk` today; that is where its big numbers come from,
133
+ * and it keeps them.
134
+ *
135
+ * ### The colour is the person's, not the position's
136
+ *
137
+ * A positional hue is the default because it costs a call site
138
+ * nothing. It is also the wrong default for a household: add a
139
+ * fifth member and the first four change colour, which is exactly
140
+ * the identity the picker was using to be fast. Any set whose
141
+ * subjects persist should name its colour per subject.
142
+ *
143
+ * There are two ways to name one, and the difference is where the
144
+ * colour came from:
145
+ *
146
+ * - **`categorical`** — an index into the ten audited hues. A
147
+ * choice from what the library offered, so the library keeps
148
+ * owning it: re-themed by `data-variant`, contrast-gated in both
149
+ * schemes. The same call Docket makes for a project.
150
+ * - **`color`** — any CSS colour, straight from data. For a colour
151
+ * the system does **not** own and cannot re-theme without lying:
152
+ * points-market's kids are coloured to match the NFC cards they
153
+ * tap, so the picker and the cards on the fridge have to agree,
154
+ * and ten palette hues cannot promise that.
155
+ *
156
+ * The second arm is the narrow one. Reach for it only when the
157
+ * colour is a fact about the subject rather than a decision about
158
+ * the design — the same line `Swatch` draws, for the same reason.
159
+ */
160
+ export declare const PortraitTiles: ({ className, items, label, layout, minTileInlineSize, newTabLabel, onChoose, size, }: PortraitTilesProps) => ReactNode;
161
+ export {};
162
+ //# sourceMappingURL=PortraitTiles.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"PortraitTiles.d.ts","sourceRoot":"","sources":["../../src/PortraitTiles/PortraitTiles.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,gBAAgB,EAChB,WAAW,EACZ,MAAM,qBAAqB,CAAA;AAK5B,OAAO,KAAK,EAAiB,SAAS,EAAE,MAAM,OAAO,CAAA;AAqBrD;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,KAAK,kBAAkB,GACnB;IAAE,WAAW,CAAC,EAAE,gBAAgB,CAAC;IAAC,KAAK,CAAC,EAAE,KAAK,CAAA;CAAE,GACjD;IAAE,WAAW,CAAC,EAAE,KAAK,CAAC;IAAC,KAAK,EAAE,MAAM,CAAA;CAAE,CAAA;AAE1C,MAAM,MAAM,gBAAgB,GAAG,kBAAkB,GAAG;IAClD;;;;;;OAMG;IACH,IAAI,CAAC,EAAE,SAAS,CAAA;IAChB,yEAAyE;IACzE,IAAI,CAAC,EAAE,MAAM,CAAA;IACb;;;;OAIG;IACH,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB;;;;;;OAMG;IACH,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,UAAU,CAAC,EAAE,OAAO,CAAA;IACpB,uEAAuE;IACvE,UAAU,CAAC,EAAE,OAAO,CAAA;IACpB,gBAAgB;IAChB,KAAK,EAAE,SAAS,CAAA;IAChB;;;;OAIG;IACH,IAAI,CAAC,EAAE,SAAS,CAAA;IAChB,8DAA8D;IAC9D,KAAK,EAAE,MAAM,CAAA;CACd,CAAA;AAED;;;;;;;;;GASG;AACH,MAAM,MAAM,mBAAmB,GAAG,MAAM,GAAG,QAAQ,GAAG,KAAK,CAAA;AAE3D,MAAM,MAAM,kBAAkB,GAAG;IAC/B,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,KAAK,EAAE,SAAS,gBAAgB,EAAE,CAAA;IAClC,2CAA2C;IAC3C,KAAK,EAAE,MAAM,CAAA;IACb,MAAM,CAAC,EAAE,mBAAmB,CAAA;IAC5B,wDAAwD;IACxD,iBAAiB,CAAC,EAAE,MAAM,CAAA;IAC1B,8CAA8C;IAC9C,WAAW,CAAC,EAAE,MAAM,CAAA;IACpB,kDAAkD;IAClD,QAAQ,CAAC,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,CAAA;IAClC,IAAI,CAAC,EAAE,WAAW,CAAA;CACnB,CAAA;AAiLD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAgEG;AACH,eAAO,MAAM,aAAa,GAAI,sFAS3B,kBAAkB,KAAG,SA8MvB,CAAA"}
@@ -0,0 +1,263 @@
1
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
+ import { CATEGORICAL_INDEX_COUNT, CATEGORICAL_INDEXES, } from "@charcuterie/tokens";
3
+ import { CATEGORICAL_APPEARANCE_CLASS, CATEGORICAL_CONTENT_CLASS, CATEGORICAL_HOVER_BORDER_CLASS, CATEGORICAL_RING_CLASS, } from "../categoricalStyles.js";
4
+ import { FOCUS_RING_CLASS } from "../intentStyles.js";
5
+ import { useRouterLink } from "../RouterLink/RouterLinkProvider.js";
6
+ import { getIsRoutedHref } from "../RouterLink/routerLink.js";
7
+ import { TILE_BOX_CLASS, TILE_COLUMNS_CLASS, TILE_MIN_INLINE_SIZE_PROPERTY, } from "../tileStyles.js";
8
+ import { toClassName } from "../toClassName.js";
9
+ import { VisuallyHidden } from "../VisuallyHidden/VisuallyHidden.js";
10
+ import { PortraitAvatar } from "./PortraitAvatar.js";
11
+ import { getPortraitColourProperties } from "./portraitColour.js";
12
+ /**
13
+ * The two forms, spelled as complete class strings rather than
14
+ * assembled.
15
+ *
16
+ * `cq-sm` is 24rem. Below it the set is one narrow column — a phone,
17
+ * a sidebar — and a 144px face would push the number off the bottom
18
+ * of the fold; above it there is room for the shape points-market
19
+ * built, which is what its kiosk screen shows today.
20
+ *
21
+ * The `auto` strings carry BOTH forms, because a container query
22
+ * variant is a media query in the stylesheet and cannot be chosen in
23
+ * JavaScript. `layout="row"` and `layout="column"` exist so a caller
24
+ * that already knows can skip the query rather than fight it.
25
+ */
26
+ const TILE_LAYOUT_CLASS = {
27
+ auto: "flex-row items-center text-start cq-sm:flex-col cq-sm:items-center cq-sm:text-center",
28
+ column: "flex-col items-center text-center",
29
+ row: "flex-row items-center text-start",
30
+ };
31
+ const TEXT_LAYOUT_CLASS = {
32
+ auto: "items-start cq-sm:items-center",
33
+ column: "items-center",
34
+ row: "items-start",
35
+ };
36
+ const TILE_PADDING_CLASS = {
37
+ auto: "p-4 cq-sm:p-6",
38
+ column: "p-6",
39
+ row: "p-4",
40
+ };
41
+ /**
42
+ * Every length here moves together, and none of them is a pixel
43
+ * count in an app.
44
+ *
45
+ * The face grows when the set does, and the type ramp does the rest
46
+ * on its own — `text-2xl` is 30px at `comfortable` and 38px at
47
+ * `kiosk`, which is how points-market's wall-mounted picker gets its
48
+ * size back without a single override. That the density axis already
49
+ * carries this is the reason the owner's "it is meant to scale" does
50
+ * not need a fourth size here.
51
+ */
52
+ const AVATAR_SIZE_CLASS = {
53
+ sm: {
54
+ auto: "size-12 cq-sm:size-20",
55
+ column: "size-20",
56
+ row: "size-12",
57
+ },
58
+ md: {
59
+ auto: "size-16 cq-sm:size-28",
60
+ column: "size-28",
61
+ row: "size-16",
62
+ },
63
+ lg: {
64
+ auto: "size-20 cq-sm:size-36",
65
+ column: "size-36",
66
+ row: "size-20",
67
+ },
68
+ };
69
+ /**
70
+ * The initials, sized to the circle they sit in.
71
+ *
72
+ * Its own map rather than a `calc()` off the avatar size, because
73
+ * the size arrives as a Tailwind class and there is no number here
74
+ * to compute from. Inheriting the tile's text size — which is what
75
+ * the first version did — put a 17px letter in the middle of a 144px
76
+ * circle and looked like a bug in the picture loader.
77
+ *
78
+ * The ramp stops at `2xl`, so the largest column avatar is
79
+ * deliberately a letter with air around it rather than a letter that
80
+ * fills the disc. Going past the ramp for one glyph would be the
81
+ * hardcoded length every other size in this file avoids.
82
+ */
83
+ const AVATAR_TEXT_CLASS = {
84
+ sm: {
85
+ auto: "text-md cq-sm:text-xl",
86
+ column: "text-xl",
87
+ row: "text-md",
88
+ },
89
+ md: {
90
+ auto: "text-lg cq-sm:text-2xl",
91
+ column: "text-2xl",
92
+ row: "text-lg",
93
+ },
94
+ lg: {
95
+ auto: "text-xl cq-sm:text-2xl",
96
+ column: "text-2xl",
97
+ row: "text-xl",
98
+ },
99
+ };
100
+ const NAME_TEXT_CLASS = {
101
+ sm: "text-sm",
102
+ md: "text-md",
103
+ lg: "text-lg",
104
+ };
105
+ const STAT_TEXT_CLASS = {
106
+ sm: {
107
+ auto: "text-lg cq-sm:text-xl",
108
+ column: "text-xl",
109
+ row: "text-lg",
110
+ },
111
+ md: {
112
+ auto: "text-xl cq-sm:text-2xl",
113
+ column: "text-2xl",
114
+ row: "text-xl",
115
+ },
116
+ lg: {
117
+ auto: "text-2xl cq-sm:text-2xl",
118
+ column: "text-2xl",
119
+ row: "text-2xl",
120
+ },
121
+ };
122
+ const GAP_CLASS = {
123
+ sm: "gap-3",
124
+ md: "gap-4",
125
+ lg: "gap-5",
126
+ };
127
+ /**
128
+ * Positional hue, wrapping at ten. Same rule `ActionTiles` uses.
129
+ *
130
+ * `null` when the portrait brought its own `color` — there is no
131
+ * palette index to fall back to in that arm, and picking one anyway
132
+ * would paint a second, unrelated hue onto the same tile.
133
+ */
134
+ const getPortraitCategorical = (item, position) => item.color === undefined
135
+ ? (item.categorical ??
136
+ CATEGORICAL_INDEXES[position % CATEGORICAL_INDEX_COUNT])
137
+ : null;
138
+ /**
139
+ * The classes that read the four custom properties, spelled whole.
140
+ *
141
+ * Tailwind scans SOURCE TEXT for complete class strings, so these
142
+ * cannot be built from a variable — and the failure is silent CSS
143
+ * that never gets generated rather than an error anywhere.
144
+ */
145
+ const COLOUR_TILE_HOVER_CLASS = "hover:border-(--charcuterie-portrait-fill)";
146
+ const COLOUR_AVATAR_CLASS = "bg-(--charcuterie-portrait-fill) text-(--charcuterie-portrait-initials) ring-(--charcuterie-portrait-halo) inset-ring-1 inset-ring-black/10";
147
+ const COLOUR_STAT_CLASS = "text-(--charcuterie-portrait-stat)";
148
+ /**
149
+ * A set of PEOPLE — or of anything else picked by its face — drawn
150
+ * as round pictures with a name and one big number.
151
+ *
152
+ * points-market's "Who's shopping?" is the shape this is taken from,
153
+ * and it is deliberately **not** an `ActionTiles` variant. The two
154
+ * answer different questions:
155
+ *
156
+ * - `ActionTiles` asks *what do you want to do* — a name, a line of
157
+ * help, an icon that illustrates the verb.
158
+ * - This asks *who is this* — a face, and one number that decides
159
+ * it. There is no description, because a person is not explained.
160
+ *
161
+ * Collapsing them would mean one component whose icon is sometimes a
162
+ * 20px glyph beside the title and sometimes a 144px circle above it,
163
+ * with `hint` meaning "what this does" in one mode and "points" in
164
+ * the other. That is two components wearing one name, which is the
165
+ * mistake the
166
+ * [2026-08-25 record](../../../../docs/decisions/2026-08-25-a-choice-tile-is-a-radiogroup-shape-not-a-third-component.md)
167
+ * warns about from the other direction.
168
+ *
169
+ * What they DO share is the box — `TILE_BOX_CLASS` and the same
170
+ * `auto-fill` grid — so a page carrying both does not show two
171
+ * different cards.
172
+ *
173
+ * ### It scales, and the scaling is the point
174
+ *
175
+ * The owner's one requirement when this moved into the library:
176
+ * *"that one is meant to scale. I don't wanna lose that
177
+ * functionality."* Two axes carry it.
178
+ *
179
+ * 1. **The container.** `layout="auto"` is a row while the set is
180
+ * narrow and a column once it has room, off a `cq-sm` query on
181
+ * the set itself. Nothing reads the viewport, so the same picker
182
+ * works in a sidebar and on a wall.
183
+ * 2. **The density.** Every length is a token, so `data-density="kiosk"`
184
+ * grows the type ramp and the control heights under it. points-market
185
+ * runs `kiosk` today; that is where its big numbers come from,
186
+ * and it keeps them.
187
+ *
188
+ * ### The colour is the person's, not the position's
189
+ *
190
+ * A positional hue is the default because it costs a call site
191
+ * nothing. It is also the wrong default for a household: add a
192
+ * fifth member and the first four change colour, which is exactly
193
+ * the identity the picker was using to be fast. Any set whose
194
+ * subjects persist should name its colour per subject.
195
+ *
196
+ * There are two ways to name one, and the difference is where the
197
+ * colour came from:
198
+ *
199
+ * - **`categorical`** — an index into the ten audited hues. A
200
+ * choice from what the library offered, so the library keeps
201
+ * owning it: re-themed by `data-variant`, contrast-gated in both
202
+ * schemes. The same call Docket makes for a project.
203
+ * - **`color`** — any CSS colour, straight from data. For a colour
204
+ * the system does **not** own and cannot re-theme without lying:
205
+ * points-market's kids are coloured to match the NFC cards they
206
+ * tap, so the picker and the cards on the fridge have to agree,
207
+ * and ten palette hues cannot promise that.
208
+ *
209
+ * The second arm is the narrow one. Reach for it only when the
210
+ * colour is a fact about the subject rather than a decision about
211
+ * the design — the same line `Swatch` draws, for the same reason.
212
+ */
213
+ export const PortraitTiles = ({ className, items, label, layout = "auto", minTileInlineSize = 200, newTabLabel = "(opens in a new tab)", onChoose, size = "md", }) => {
214
+ const RouterLink = useRouterLink();
215
+ return (_jsx("div", { className: toClassName("@container", className), children: _jsx("div", { "aria-label": label, className: toClassName("grid", TILE_COLUMNS_CLASS, GAP_CLASS[size]), role: "group", style: {
216
+ [TILE_MIN_INLINE_SIZE_PROPERTY]: `${minTileInlineSize}px`,
217
+ }, children: items.map((item, position) => {
218
+ const { hint, href, imageSrc, initials, isDisabled = false, isExternal = false, label: tileLabel, stat, value, } = item;
219
+ const categorical = getPortraitCategorical(item, position);
220
+ const { color } = item;
221
+ const tileClassName = toClassName("flex min-w-0 wrap-anywhere transition-[background-color,border-color,box-shadow,transform] duration-(--duration-fast) ease-standard", TILE_BOX_CLASS, TILE_LAYOUT_CLASS[layout], TILE_PADDING_CLASS[layout], GAP_CLASS[size], isDisabled
222
+ ? "cursor-not-allowed opacity-60"
223
+ : toClassName("cursor-pointer hover:shadow-medium hover:-translate-y-1",
224
+ // The lift is the one piece of motion in this
225
+ // library, kept because it is what the owner
226
+ // pointed at in points-market — and turned off
227
+ // for anyone who has asked the OS for less. The
228
+ // shadow and the border stay, so the hover is
229
+ // still legible without it.
230
+ "motion-reduce:hover:translate-y-0", categorical === null
231
+ ? COLOUR_TILE_HOVER_CLASS
232
+ : CATEGORICAL_HOVER_BORDER_CLASS[categorical]), FOCUS_RING_CLASS);
233
+ const avatarClassName = toClassName("flex shrink-0 items-center justify-center overflow-hidden rounded-full object-cover font-display font-semibold ring-4", AVATAR_SIZE_CLASS[size][layout], AVATAR_TEXT_CLASS[size][layout], categorical === null
234
+ ? COLOUR_AVATAR_CLASS
235
+ : toClassName(CATEGORICAL_APPEARANCE_CLASS[categorical]
236
+ .solid, CATEGORICAL_RING_CLASS[categorical]));
237
+ // The one sanctioned escape hatch for a runtime value,
238
+ // and the same one `Swatch` uses: a colour out of a
239
+ // database cannot be a class, because Tailwind generates
240
+ // its CSS at build time from source text.
241
+ const tileStyle = color === undefined
242
+ ? undefined
243
+ : getPortraitColourProperties(color);
244
+ const content = (_jsxs(_Fragment, { children: [_jsx(PortraitAvatar, { className: avatarClassName, imageSrc: imageSrc, initials: initials }), _jsxs("span", { className: toClassName("flex min-w-0 flex-col", TEXT_LAYOUT_CLASS[layout]), children: [_jsx("span", { className: toClassName("font-semibold text-content-primary", NAME_TEXT_CLASS[size]), children: tileLabel }), stat === undefined ? null : (_jsx("span", { className: toClassName("font-bold tabular-nums", STAT_TEXT_CLASS[size][layout], categorical === null
245
+ ? COLOUR_STAT_CLASS
246
+ : CATEGORICAL_CONTENT_CLASS[categorical]), children: stat })), hint === undefined ? null : (_jsx("span", { className: "text-xs text-content-muted", children: hint }))] }), isExternal && href !== undefined ? (_jsx(VisuallyHidden, { children: newTabLabel })) : null] }));
247
+ if (href !== undefined && !isDisabled) {
248
+ const LinkElement = isExternal || !getIsRoutedHref(href)
249
+ ? "a"
250
+ : RouterLink;
251
+ return (_jsx(LinkElement, { className: tileClassName, href: href, rel: isExternal
252
+ ? "noopener noreferrer"
253
+ : undefined, style: tileStyle, target: isExternal ? "_blank" : undefined, children: content }, value));
254
+ }
255
+ if (href !== undefined) {
256
+ return (_jsx("a", { "aria-disabled": "true", className: tileClassName, role: "link", style: tileStyle, children: content }, value));
257
+ }
258
+ return (_jsx("button", { className: tileClassName, disabled: isDisabled, onClick: () => {
259
+ onChoose?.(value);
260
+ }, style: tileStyle, type: "button", children: content }, value));
261
+ }) }) }));
262
+ };
263
+ //# sourceMappingURL=PortraitTiles.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"PortraitTiles.js","sourceRoot":"","sources":["../../src/PortraitTiles/PortraitTiles.tsx"],"names":[],"mappings":"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"}
@@ -0,0 +1,49 @@
1
+ /**
2
+ * The paint for a portrait whose colour came from DATA rather than
3
+ * from the palette.
4
+ *
5
+ * `Swatch` already argues the case this file serves: a colour that
6
+ * "arrived from the world" — a sticker, an extracted album accent,
7
+ * the NFC card a child taps — is not a token, so it cannot be
8
+ * re-themed and cannot be a Tailwind class. It reaches the paint as
9
+ * custom properties on the tile, and the classes read those
10
+ * properties.
11
+ *
12
+ * Four properties rather than one, because the same hue has to do
13
+ * four jobs and only one of them can use it raw. Each is named for
14
+ * the PART it paints, following `--charcuterie-accent-edge` — a
15
+ * property called `-colour` would say only what type it holds, which
16
+ * is the one thing already obvious from where it is used:
17
+ *
18
+ * - the **fill** behind the initials — the colour itself, exactly;
19
+ * - the **initials** on top of it — black or white, whichever the
20
+ * fill can actually carry;
21
+ * - the **halo** around the face — a transparent wash of the hue,
22
+ * standing in for what `categorical-N-surface` is on the palette
23
+ * side;
24
+ * - the **number** — the hue pulled toward the scheme's own text
25
+ * colour, so a pale card colour stays readable on a pale surface
26
+ * and a dark one stays readable on a dark surface. Mixing toward
27
+ * `content-primary` moves lightness and keeps hue, which is the
28
+ * smallest change that keeps the number *this person's colour*
29
+ * while remaining a number somebody can read.
30
+ */
31
+ export declare const PORTRAIT_FILL_PROPERTY = "--charcuterie-portrait-fill";
32
+ export declare const PORTRAIT_INITIALS_PROPERTY = "--charcuterie-portrait-initials";
33
+ export declare const PORTRAIT_HALO_PROPERTY = "--charcuterie-portrait-halo";
34
+ export declare const PORTRAIT_STAT_PROPERTY = "--charcuterie-portrait-stat";
35
+ /**
36
+ * The four properties for one portrait.
37
+ *
38
+ * `srgb` rather than `oklab` for both mixes, matching `Swatch`'s
39
+ * existing `color-mix(in srgb, …)` — one interpolation space across
40
+ * the library, so two components handed the same colour do not
41
+ * produce two different washes of it.
42
+ *
43
+ * The initials colour is `getReadableTextColour`, which lives in
44
+ * `@charcuterie/tokens` beside the contrast maths rather than here.
45
+ * It is total on purpose: it runs inside a render, on a colour out
46
+ * of a database, and a throw would blank the whole picker.
47
+ */
48
+ export declare const getPortraitColourProperties: (color: string) => Record<string, string>;
49
+ //# sourceMappingURL=portraitColour.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"portraitColour.d.ts","sourceRoot":"","sources":["../../src/PortraitTiles/portraitColour.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA6BG;AAIH,eAAO,MAAM,sBAAsB,gCACJ,CAAA;AAE/B,eAAO,MAAM,0BAA0B,oCACJ,CAAA;AAEnC,eAAO,MAAM,sBAAsB,gCACJ,CAAA;AAE/B,eAAO,MAAM,sBAAsB,gCACJ,CAAA;AAE/B;;;;;;;;;;;;GAYG;AACH,eAAO,MAAM,2BAA2B,GACtC,OAAO,MAAM,KACZ,MAAM,CAAC,MAAM,EAAE,MAAM,CAMtB,CAAA"}
@@ -0,0 +1,55 @@
1
+ /**
2
+ * The paint for a portrait whose colour came from DATA rather than
3
+ * from the palette.
4
+ *
5
+ * `Swatch` already argues the case this file serves: a colour that
6
+ * "arrived from the world" — a sticker, an extracted album accent,
7
+ * the NFC card a child taps — is not a token, so it cannot be
8
+ * re-themed and cannot be a Tailwind class. It reaches the paint as
9
+ * custom properties on the tile, and the classes read those
10
+ * properties.
11
+ *
12
+ * Four properties rather than one, because the same hue has to do
13
+ * four jobs and only one of them can use it raw. Each is named for
14
+ * the PART it paints, following `--charcuterie-accent-edge` — a
15
+ * property called `-colour` would say only what type it holds, which
16
+ * is the one thing already obvious from where it is used:
17
+ *
18
+ * - the **fill** behind the initials — the colour itself, exactly;
19
+ * - the **initials** on top of it — black or white, whichever the
20
+ * fill can actually carry;
21
+ * - the **halo** around the face — a transparent wash of the hue,
22
+ * standing in for what `categorical-N-surface` is on the palette
23
+ * side;
24
+ * - the **number** — the hue pulled toward the scheme's own text
25
+ * colour, so a pale card colour stays readable on a pale surface
26
+ * and a dark one stays readable on a dark surface. Mixing toward
27
+ * `content-primary` moves lightness and keeps hue, which is the
28
+ * smallest change that keeps the number *this person's colour*
29
+ * while remaining a number somebody can read.
30
+ */
31
+ import { getReadableTextColour } from "@charcuterie/tokens";
32
+ export const PORTRAIT_FILL_PROPERTY = "--charcuterie-portrait-fill";
33
+ export const PORTRAIT_INITIALS_PROPERTY = "--charcuterie-portrait-initials";
34
+ export const PORTRAIT_HALO_PROPERTY = "--charcuterie-portrait-halo";
35
+ export const PORTRAIT_STAT_PROPERTY = "--charcuterie-portrait-stat";
36
+ /**
37
+ * The four properties for one portrait.
38
+ *
39
+ * `srgb` rather than `oklab` for both mixes, matching `Swatch`'s
40
+ * existing `color-mix(in srgb, …)` — one interpolation space across
41
+ * the library, so two components handed the same colour do not
42
+ * produce two different washes of it.
43
+ *
44
+ * The initials colour is `getReadableTextColour`, which lives in
45
+ * `@charcuterie/tokens` beside the contrast maths rather than here.
46
+ * It is total on purpose: it runs inside a render, on a colour out
47
+ * of a database, and a throw would blank the whole picker.
48
+ */
49
+ export const getPortraitColourProperties = (color) => ({
50
+ [PORTRAIT_FILL_PROPERTY]: color,
51
+ [PORTRAIT_HALO_PROPERTY]: `color-mix(in srgb, ${color} 28%, transparent)`,
52
+ [PORTRAIT_INITIALS_PROPERTY]: getReadableTextColour(color),
53
+ [PORTRAIT_STAT_PROPERTY]: `color-mix(in srgb, ${color} 62%, var(--color-content-primary))`,
54
+ });
55
+ //# sourceMappingURL=portraitColour.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"portraitColour.js","sourceRoot":"","sources":["../../src/PortraitTiles/portraitColour.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA6BG;AAEH,OAAO,EAAE,qBAAqB,EAAE,MAAM,qBAAqB,CAAA;AAE3D,MAAM,CAAC,MAAM,sBAAsB,GACjC,6BAA6B,CAAA;AAE/B,MAAM,CAAC,MAAM,0BAA0B,GACrC,iCAAiC,CAAA;AAEnC,MAAM,CAAC,MAAM,sBAAsB,GACjC,6BAA6B,CAAA;AAE/B,MAAM,CAAC,MAAM,sBAAsB,GACjC,6BAA6B,CAAA;AAE/B;;;;;;;;;;;;GAYG;AACH,MAAM,CAAC,MAAM,2BAA2B,GAAG,CACzC,KAAa,EACW,EAAE,CAAC,CAAC;IAC5B,CAAC,sBAAsB,CAAC,EAAE,KAAK;IAC/B,CAAC,sBAAsB,CAAC,EAAE,sBAAsB,KAAK,oBAAoB;IACzE,CAAC,0BAA0B,CAAC,EAC1B,qBAAqB,CAAC,KAAK,CAAC;IAC9B,CAAC,sBAAsB,CAAC,EAAE,sBAAsB,KAAK,qCAAqC;CAC3F,CAAC,CAAA"}
@@ -50,4 +50,31 @@ export declare const CATEGORICAL_HOVER_CLASS: Record<CategoricalIndex, Record<In
50
50
  export declare const CATEGORICAL_SOLID_FILL_CLASS: Record<CategoricalIndex, string>;
51
51
  /** The same ten as text, for a glyph or a dot's ring. */
52
52
  export declare const CATEGORICAL_CONTENT_CLASS: Record<CategoricalIndex, string>;
53
+ /**
54
+ * The hover BORDER, which `CATEGORICAL_HOVER_CLASS` deliberately
55
+ * does not carry.
56
+ *
57
+ * That map answers "what does this surface do under the pointer",
58
+ * and a `Badge` has no border state to change. A tile does: it is a
59
+ * bordered card, and the whole point of colouring one is that the
60
+ * box reacts in its own hue rather than in the neutral
61
+ * `border-strong` every other box uses. Kept separate rather than
62
+ * added as a fifth appearance key, because an appearance is a claim
63
+ * about the fill and this is not one.
64
+ *
65
+ * Written out in full for the reason this file opens with.
66
+ */
67
+ export declare const CATEGORICAL_HOVER_BORDER_CLASS: Record<CategoricalIndex, string>;
68
+ /**
69
+ * A halo the width of a ring, in the hue's SURFACE.
70
+ *
71
+ * `surface` and not `border`: this sits directly against the solid
72
+ * fill it surrounds, so it is a soft step out of the colour rather
73
+ * than a second line around it. `PortraitTiles` draws it around a
74
+ * face, where a hard outline reads as a frame and this reads as
75
+ * light.
76
+ *
77
+ * Written out in full for the reason this file opens with.
78
+ */
79
+ export declare const CATEGORICAL_RING_CLASS: Record<CategoricalIndex, string>;
53
80
  //# sourceMappingURL=categoricalStyles.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"categoricalStyles.d.ts","sourceRoot":"","sources":["../src/categoricalStyles.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AAEH,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,qBAAqB,CAAA;AAE3D,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,mBAAmB,CAAA;AAEzD,eAAO,MAAM,4BAA4B,EAAE,MAAM,CAC/C,gBAAgB,EAChB,MAAM,CAAC,gBAAgB,EAAE,MAAM,CAAC,CA4FjC,CAAA;AAED;;;;;;;;;;;GAWG;AACH,eAAO,MAAM,uBAAuB,EAAE,MAAM,CAC1C,gBAAgB,EAChB,MAAM,CAAC,gBAAgB,EAAE,MAAM,CAAC,CA8DjC,CAAA;AAED;;;;;GAKG;AACH,eAAO,MAAM,4BAA4B,EAAE,MAAM,CAC/C,gBAAgB,EAChB,MAAM,CAYP,CAAA;AAED,yDAAyD;AACzD,eAAO,MAAM,yBAAyB,EAAE,MAAM,CAC5C,gBAAgB,EAChB,MAAM,CAYP,CAAA"}
1
+ {"version":3,"file":"categoricalStyles.d.ts","sourceRoot":"","sources":["../src/categoricalStyles.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AAEH,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,qBAAqB,CAAA;AAE3D,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,mBAAmB,CAAA;AAEzD,eAAO,MAAM,4BAA4B,EAAE,MAAM,CAC/C,gBAAgB,EAChB,MAAM,CAAC,gBAAgB,EAAE,MAAM,CAAC,CA4FjC,CAAA;AAED;;;;;;;;;;;GAWG;AACH,eAAO,MAAM,uBAAuB,EAAE,MAAM,CAC1C,gBAAgB,EAChB,MAAM,CAAC,gBAAgB,EAAE,MAAM,CAAC,CA8DjC,CAAA;AAED;;;;;GAKG;AACH,eAAO,MAAM,4BAA4B,EAAE,MAAM,CAC/C,gBAAgB,EAChB,MAAM,CAYP,CAAA;AAED,yDAAyD;AACzD,eAAO,MAAM,yBAAyB,EAAE,MAAM,CAC5C,gBAAgB,EAChB,MAAM,CAYP,CAAA;AAED;;;;;;;;;;;;;GAaG;AACH,eAAO,MAAM,8BAA8B,EAAE,MAAM,CACjD,gBAAgB,EAChB,MAAM,CAYP,CAAA;AAED;;;;;;;;;;GAUG;AACH,eAAO,MAAM,sBAAsB,EAAE,MAAM,CACzC,gBAAgB,EAChB,MAAM,CAYP,CAAA"}
@@ -192,4 +192,53 @@ export const CATEGORICAL_CONTENT_CLASS = {
192
192
  9: "text-categorical-9-content",
193
193
  10: "text-categorical-10-content",
194
194
  };
195
+ /**
196
+ * The hover BORDER, which `CATEGORICAL_HOVER_CLASS` deliberately
197
+ * does not carry.
198
+ *
199
+ * That map answers "what does this surface do under the pointer",
200
+ * and a `Badge` has no border state to change. A tile does: it is a
201
+ * bordered card, and the whole point of colouring one is that the
202
+ * box reacts in its own hue rather than in the neutral
203
+ * `border-strong` every other box uses. Kept separate rather than
204
+ * added as a fifth appearance key, because an appearance is a claim
205
+ * about the fill and this is not one.
206
+ *
207
+ * Written out in full for the reason this file opens with.
208
+ */
209
+ export const CATEGORICAL_HOVER_BORDER_CLASS = {
210
+ 1: "hover:border-categorical-1-border",
211
+ 2: "hover:border-categorical-2-border",
212
+ 3: "hover:border-categorical-3-border",
213
+ 4: "hover:border-categorical-4-border",
214
+ 5: "hover:border-categorical-5-border",
215
+ 6: "hover:border-categorical-6-border",
216
+ 7: "hover:border-categorical-7-border",
217
+ 8: "hover:border-categorical-8-border",
218
+ 9: "hover:border-categorical-9-border",
219
+ 10: "hover:border-categorical-10-border",
220
+ };
221
+ /**
222
+ * A halo the width of a ring, in the hue's SURFACE.
223
+ *
224
+ * `surface` and not `border`: this sits directly against the solid
225
+ * fill it surrounds, so it is a soft step out of the colour rather
226
+ * than a second line around it. `PortraitTiles` draws it around a
227
+ * face, where a hard outline reads as a frame and this reads as
228
+ * light.
229
+ *
230
+ * Written out in full for the reason this file opens with.
231
+ */
232
+ export const CATEGORICAL_RING_CLASS = {
233
+ 1: "ring-categorical-1-surface",
234
+ 2: "ring-categorical-2-surface",
235
+ 3: "ring-categorical-3-surface",
236
+ 4: "ring-categorical-4-surface",
237
+ 5: "ring-categorical-5-surface",
238
+ 6: "ring-categorical-6-surface",
239
+ 7: "ring-categorical-7-surface",
240
+ 8: "ring-categorical-8-surface",
241
+ 9: "ring-categorical-9-surface",
242
+ 10: "ring-categorical-10-surface",
243
+ };
195
244
  //# sourceMappingURL=categoricalStyles.js.map