@charcuterie/ui 4.0.0 → 4.2.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 (54) hide show
  1. package/README.md +3 -2
  2. package/dist/ActionTiles/ActionTiles.d.ts +66 -8
  3. package/dist/ActionTiles/ActionTiles.d.ts.map +1 -1
  4. package/dist/ActionTiles/ActionTiles.js +88 -11
  5. package/dist/ActionTiles/ActionTiles.js.map +1 -1
  6. package/dist/PortraitTiles/PortraitAvatar.d.ts +21 -0
  7. package/dist/PortraitTiles/PortraitAvatar.d.ts.map +1 -0
  8. package/dist/PortraitTiles/PortraitAvatar.js +29 -0
  9. package/dist/PortraitTiles/PortraitAvatar.js.map +1 -0
  10. package/dist/PortraitTiles/PortraitTiles.d.ts +162 -0
  11. package/dist/PortraitTiles/PortraitTiles.d.ts.map +1 -0
  12. package/dist/PortraitTiles/PortraitTiles.js +263 -0
  13. package/dist/PortraitTiles/PortraitTiles.js.map +1 -0
  14. package/dist/PortraitTiles/portraitColour.d.ts +49 -0
  15. package/dist/PortraitTiles/portraitColour.d.ts.map +1 -0
  16. package/dist/PortraitTiles/portraitColour.js +55 -0
  17. package/dist/PortraitTiles/portraitColour.js.map +1 -0
  18. package/dist/RouterLink/UnstyledLink.d.ts +17 -0
  19. package/dist/RouterLink/UnstyledLink.d.ts.map +1 -0
  20. package/dist/RouterLink/UnstyledLink.js +24 -0
  21. package/dist/RouterLink/UnstyledLink.js.map +1 -0
  22. package/dist/categoricalStyles.d.ts +27 -0
  23. package/dist/categoricalStyles.d.ts.map +1 -1
  24. package/dist/categoricalStyles.js +49 -0
  25. package/dist/categoricalStyles.js.map +1 -1
  26. package/dist/index.d.ts +6 -2
  27. package/dist/index.d.ts.map +1 -1
  28. package/dist/index.js +3 -1
  29. package/dist/index.js.map +1 -1
  30. package/dist/tileStyles.d.ts +15 -0
  31. package/dist/tileStyles.d.ts.map +1 -1
  32. package/dist/tileStyles.js +26 -1
  33. package/dist/tileStyles.js.map +1 -1
  34. package/package.json +2 -2
  35. package/src/ActionTiles/ActionTiles.mdx +82 -8
  36. package/src/ActionTiles/ActionTiles.stories.tsx +107 -2
  37. package/src/ActionTiles/ActionTiles.test.tsx +205 -5
  38. package/src/ActionTiles/ActionTiles.tsx +274 -110
  39. package/src/PortraitTiles/PortraitAvatar.tsx +49 -0
  40. package/src/PortraitTiles/PortraitTiles.mdx +153 -0
  41. package/src/PortraitTiles/PortraitTiles.stories.tsx +403 -0
  42. package/src/PortraitTiles/PortraitTiles.test.tsx +399 -0
  43. package/src/PortraitTiles/PortraitTiles.tsx +577 -0
  44. package/src/PortraitTiles/portraitColour.test.ts +39 -0
  45. package/src/PortraitTiles/portraitColour.ts +67 -0
  46. package/src/RadioGroup/RadioGroup.stories.tsx +39 -0
  47. package/src/RouterLink/RouterLink.mdx +25 -4
  48. package/src/RouterLink/UnstyledLink.stories.tsx +57 -0
  49. package/src/RouterLink/UnstyledLink.test.tsx +40 -0
  50. package/src/RouterLink/UnstyledLink.tsx +34 -0
  51. package/src/categoricalStyles.ts +57 -0
  52. package/src/index.ts +11 -0
  53. package/src/sourceRules.test.ts +17 -4
  54. package/src/tileStyles.ts +30 -1
@@ -0,0 +1,403 @@
1
+ import type { Meta, StoryObj } from "@storybook/react"
2
+
3
+ import { controlSizeArgType } from "../argTypes.storyHelpers.ts"
4
+ import {
5
+ ContainerBoard,
6
+ StoryCell,
7
+ StoryGrid,
8
+ StorySection,
9
+ } from "../board.storyHelpers.tsx"
10
+ import type { PortraitTileItem } from "./PortraitTiles.tsx"
11
+ import { PortraitTiles } from "./PortraitTiles.tsx"
12
+
13
+ /**
14
+ * The shape this was taken from: points-market's "Who's shopping?".
15
+ *
16
+ * Placeholder names on purpose. A component library's fixtures are
17
+ * read by everyone who opens Storybook and are the one place a
18
+ * household's real names would leak into a published repo — which
19
+ * the fleet has a standing rule about, and which a PNG in a pull
20
+ * request makes ungreppable afterwards.
21
+ */
22
+ const SHOPPER_ITEMS = [
23
+ {
24
+ hint: "points",
25
+ initials: "A",
26
+ label: "Avery",
27
+ stat: "1,240",
28
+ value: "avery",
29
+ },
30
+ {
31
+ hint: "points",
32
+ initials: "B",
33
+ label: "Bailey",
34
+ stat: "860",
35
+ value: "bailey",
36
+ },
37
+ {
38
+ hint: "points",
39
+ initials: "C",
40
+ label: "Casey",
41
+ stat: "2,015",
42
+ value: "casey",
43
+ },
44
+ {
45
+ hint: "points",
46
+ initials: "D",
47
+ label: "Devon",
48
+ stat: "430",
49
+ value: "devon",
50
+ },
51
+ // `satisfies` rather than an annotation, so each fixture keeps its
52
+ // OWN type. `PortraitTileItem` is a union — a palette hue or a
53
+ // colour out of data, never both — and annotating the array widens
54
+ // every element to that union, after which `{ ...item,
55
+ // categorical: 3 }` below is an object that TypeScript reads as
56
+ // possibly carrying a `color` too, and rejects.
57
+ ] satisfies PortraitTileItem[]
58
+
59
+ /** One fixture, with its colour arm still known. */
60
+ type ShopperItem = (typeof SHOPPER_ITEMS)[number]
61
+
62
+ /**
63
+ * A set whose subjects persist, so each one owns its hue rather than
64
+ * borrowing the one its position happened to give it.
65
+ *
66
+ * This is the arrangement any real household picker wants: add a
67
+ * fifth member and the first four keep their colours.
68
+ */
69
+ const PINNED_ITEMS: PortraitTileItem[] = [
70
+ {
71
+ ...(SHOPPER_ITEMS[0] as ShopperItem),
72
+ categorical: 3,
73
+ },
74
+ {
75
+ ...(SHOPPER_ITEMS[1] as ShopperItem),
76
+ categorical: 8,
77
+ },
78
+ {
79
+ ...(SHOPPER_ITEMS[2] as ShopperItem),
80
+ categorical: 5,
81
+ },
82
+ ]
83
+
84
+ /**
85
+ * Colours that did **not** come from the palette.
86
+ *
87
+ * The shape this component was taken from is coloured this way: each
88
+ * subject's colour matches a physical card they tap, so the picker
89
+ * and the object in their hand have to agree. Ten audited hues
90
+ * cannot make that promise, and re-theming the colour would be
91
+ * re-theming the card.
92
+ *
93
+ * Hex on purpose — the contrast pick that decides whether the
94
+ * initials come out black or white reads six-digit hex and nothing
95
+ * else.
96
+ *
97
+ * Invented colours, like the names above them. An app's real values
98
+ * would be its data sitting in a published library, and a fixture
99
+ * that is somebody's actual configuration is a fixture nobody can
100
+ * change later. Two pale and one dark, so the story shows the
101
+ * initials flipping rather than describing it.
102
+ */
103
+ const CARD_COLOUR_ITEMS: PortraitTileItem[] = [
104
+ {
105
+ color: "#8FD3F4",
106
+ hint: "points",
107
+ initials: "A",
108
+ label: "Avery",
109
+ stat: "1,240",
110
+ value: "avery",
111
+ },
112
+ {
113
+ color: "#D9D2C5",
114
+ hint: "points",
115
+ initials: "B",
116
+ label: "Bailey",
117
+ stat: "860",
118
+ value: "bailey",
119
+ },
120
+ {
121
+ color: "#3B4A6B",
122
+ hint: "points",
123
+ initials: "C",
124
+ label: "Casey",
125
+ stat: "2,015",
126
+ value: "casey",
127
+ },
128
+ ]
129
+
130
+ /** No number at all — a picker that is only ever "who are you". */
131
+ const NAME_ONLY_ITEMS: PortraitTileItem[] =
132
+ SHOPPER_ITEMS.map(({ initials, label, value }) => ({
133
+ initials,
134
+ label,
135
+ value,
136
+ }))
137
+
138
+ const meta = {
139
+ title: "Components/Actions/PortraitTiles",
140
+ component: PortraitTiles,
141
+ parameters: { layout: "padded" },
142
+ argTypes: { size: controlSizeArgType },
143
+ // The component's own defaults, restated — Storybook does not seed
144
+ // `args` from docgen, so an unstated default shows in the props
145
+ // table with nothing selected in its control.
146
+ args: {
147
+ layout: "auto",
148
+ minTileInlineSize: 200,
149
+ size: "md",
150
+ },
151
+ } satisfies Meta<typeof PortraitTiles>
152
+
153
+ export default meta
154
+
155
+ type Story = StoryObj<typeof meta>
156
+
157
+ export const Default: Story = {
158
+ args: {
159
+ items: SHOPPER_ITEMS,
160
+ label: "Who's shopping?",
161
+ },
162
+ }
163
+
164
+ /**
165
+ * The two forms, side by side, forced rather than queried — so the
166
+ * difference is visible without resizing anything.
167
+ *
168
+ * `auto` picks between them off the SET's width, which is why the
169
+ * board below this one is the story that actually documents the
170
+ * behaviour.
171
+ */
172
+ export const AllVariants: Story = {
173
+ args: {
174
+ items: SHOPPER_ITEMS,
175
+ label: "Who's shopping?",
176
+ },
177
+ render: (controlProps) => (
178
+ <StorySection title="A row while the set is narrow, a column once it has room. Both are the same component and the same tokens; only the arrangement changes.">
179
+ <StoryGrid columns={2}>
180
+ <StoryCell align="stretch" label="row">
181
+ <PortraitTiles
182
+ {...controlProps}
183
+ label="Who's shopping, as rows"
184
+ layout="row"
185
+ minTileInlineSize={260}
186
+ />
187
+ </StoryCell>
188
+
189
+ <StoryCell align="stretch" label="column">
190
+ <PortraitTiles
191
+ {...controlProps}
192
+ label="Who's shopping, as columns"
193
+ layout="column"
194
+ />
195
+ </StoryCell>
196
+ </StoryGrid>
197
+ </StorySection>
198
+ ),
199
+ }
200
+
201
+ /**
202
+ * Three sizes, each moving the face and the number together. The
203
+ * type ramp does the rest on its own: `text-2xl` is 30px at
204
+ * `comfortable` and 38px at `kiosk`, which is where points-market's
205
+ * wall-mounted picker gets its size from without a single override.
206
+ */
207
+ export const AllSizes: Story = {
208
+ args: {
209
+ items: SHOPPER_ITEMS.slice(0, 2),
210
+ label: "Who's shopping?",
211
+ },
212
+ render: (controlProps) => (
213
+ <StorySection title="One size axis, and it moves the picture, the name and the number together.">
214
+ <StoryGrid columns={3}>
215
+ {(["sm", "md", "lg"] as const).map((size) => (
216
+ <StoryCell
217
+ align="stretch"
218
+ key={size}
219
+ label={size}
220
+ >
221
+ <PortraitTiles
222
+ {...controlProps}
223
+ label={`Who's shopping at ${size}`}
224
+ layout="column"
225
+ size={size}
226
+ />
227
+ </StoryCell>
228
+ ))}
229
+ </StoryGrid>
230
+ </StorySection>
231
+ ),
232
+ }
233
+
234
+ /**
235
+ * `layout="auto"` reads the **container**, never the window.
236
+ *
237
+ * At 15rem the set is one narrow column and each portrait is a row —
238
+ * a face beside a name, which is what fits. Past `cq-sm` (24rem) it
239
+ * has room to stack, and every portrait becomes the tall column
240
+ * points-market shows on its kiosk.
241
+ *
242
+ * Every panel here is the same window, which is the whole point: a
243
+ * picker in a 320px sidebar on a 2560px monitor gets the row form,
244
+ * and a media query would have given it the column.
245
+ */
246
+ export const Responsive: Story = {
247
+ args: {
248
+ items: SHOPPER_ITEMS.slice(0, 3),
249
+ label: "Who's shopping?",
250
+ },
251
+ render: (controlProps) => (
252
+ <ContainerBoard>
253
+ {(width) => (
254
+ <PortraitTiles
255
+ {...controlProps}
256
+ label={`Who's shopping at ${width}`}
257
+ />
258
+ )}
259
+ </ContainerBoard>
260
+ ),
261
+ }
262
+
263
+ /**
264
+ * A picture instead of initials. It is `alt=""` on purpose — the
265
+ * name is beside it inside the same tile, so an alt here is the name
266
+ * announced twice.
267
+ *
268
+ * The broken source is not a mistake in this story. It is the case
269
+ * the component handles: a 404 falls back to the initials rather
270
+ * than leaving a torn hole where a face was, next to three that
271
+ * loaded.
272
+ */
273
+ export const WithPictures: Story = {
274
+ args: {
275
+ items: [
276
+ {
277
+ ...(SHOPPER_ITEMS[0] as ShopperItem),
278
+ imageSrc:
279
+ "data:image/svg+xml;utf8," +
280
+ encodeURIComponent(
281
+ '<svg xmlns="http://www.w3.org/2000/svg" width="160" height="160"><rect width="160" height="160" fill="%23c9d3e0"/><circle cx="80" cy="62" r="28" fill="%238b9bb4"/><path d="M20 160c0-33 27-52 60-52s60 19 60 52Z" fill="%238b9bb4"/></svg>',
282
+ ),
283
+ },
284
+ {
285
+ ...(SHOPPER_ITEMS[1] as ShopperItem),
286
+ // The same URL `Avatar`'s fallback story points at, and
287
+ // that is not a coincidence: `smokeStorybook.ts` keeps a
288
+ // short allowlist of resources that are MEANT to 404, so a
289
+ // real missing chunk still fails the run. A new spelling of
290
+ // "this file is absent" is an unlisted 404 and fails the
291
+ // storybook gate — which is exactly what it did here.
292
+ imageSrc: "/charcuterie-missing-portrait.png",
293
+ },
294
+ SHOPPER_ITEMS[2] as PortraitTileItem,
295
+ ],
296
+ label: "Who's shopping?",
297
+ },
298
+ }
299
+
300
+ /**
301
+ * A hue named per subject rather than taken from the position.
302
+ *
303
+ * The default is positional because it costs a call site nothing. It
304
+ * is also the wrong default for a household: adding a fifth member
305
+ * re-colours the first four, and their colour was the thing the
306
+ * picker was being read by.
307
+ */
308
+ export const NamedHues: Story = {
309
+ args: {
310
+ items: PINNED_ITEMS,
311
+ label: "Who's shopping, with pinned colours",
312
+ },
313
+ }
314
+
315
+ /**
316
+ * A colour out of data rather than out of the palette.
317
+ *
318
+ * Two pale fills and one dark one, which is the case the arm has to
319
+ * survive: the initials go black where white would vanish and white
320
+ * where black would, and the number is the same hue pulled toward
321
+ * the scheme's own text colour so it stays readable on a pale
322
+ * surface — and, in the dark scheme, pulled the other way. Compare
323
+ * it with `NamedHues` above, where the library owns the colour and
324
+ * can simply state both ends.
325
+ *
326
+ * Use this arm only when the colour is a **fact about the subject**.
327
+ * A colour that is merely a design choice is a `categorical` index,
328
+ * and it gets re-theming and a contrast guarantee for free.
329
+ */
330
+ export const DataColours: Story = {
331
+ args: {
332
+ items: CARD_COLOUR_ITEMS,
333
+ label: "Who's shopping, coloured by their cards",
334
+ },
335
+ }
336
+
337
+ export const AllStates: Story = {
338
+ args: {
339
+ items: SHOPPER_ITEMS,
340
+ label: "Who's shopping?",
341
+ },
342
+ render: (controlProps) => (
343
+ <StorySection title="What varies is what a portrait carries and whether it can be pressed.">
344
+ <StoryGrid columns={2}>
345
+ <StoryCell align="stretch" label="name and number">
346
+ <PortraitTiles
347
+ {...controlProps}
348
+ items={SHOPPER_ITEMS.slice(0, 2)}
349
+ label="Who's shopping?"
350
+ />
351
+ </StoryCell>
352
+
353
+ <StoryCell align="stretch" label="name only">
354
+ <PortraitTiles
355
+ {...controlProps}
356
+ items={NAME_ONLY_ITEMS.slice(0, 2)}
357
+ label="Who's here?"
358
+ />
359
+ </StoryCell>
360
+
361
+ <StoryCell align="stretch" label="links">
362
+ <PortraitTiles
363
+ {...controlProps}
364
+ items={SHOPPER_ITEMS.slice(0, 2).map(
365
+ (item) => ({
366
+ ...item,
367
+ href: `/${item.value}`,
368
+ }),
369
+ )}
370
+ label="Who's shopping, as links"
371
+ />
372
+ </StoryCell>
373
+
374
+ <StoryCell align="stretch" label="one unavailable">
375
+ <PortraitTiles
376
+ {...controlProps}
377
+ items={[
378
+ SHOPPER_ITEMS[0] as ShopperItem,
379
+ {
380
+ ...(SHOPPER_ITEMS[1] as ShopperItem),
381
+ isDisabled: true,
382
+ },
383
+ ]}
384
+ label="Who's shopping, with Bailey unavailable"
385
+ />
386
+ </StoryCell>
387
+ </StoryGrid>
388
+ </StorySection>
389
+ ),
390
+ }
391
+
392
+ /**
393
+ * The keyboard path is the platform's, the same as `ActionTiles`:
394
+ * Tab reaches each portrait, Enter and Space press a button one,
395
+ * Enter follows a link one. Nothing stays selected, because nothing
396
+ * was chosen — the page moved on.
397
+ */
398
+ export const Interactive: Story = {
399
+ args: {
400
+ items: SHOPPER_ITEMS.slice(0, 2),
401
+ label: "Who's shopping?",
402
+ },
403
+ }