@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.
- package/README.md +3 -2
- package/dist/ActionTiles/ActionTiles.d.ts +66 -8
- package/dist/ActionTiles/ActionTiles.d.ts.map +1 -1
- package/dist/ActionTiles/ActionTiles.js +88 -11
- package/dist/ActionTiles/ActionTiles.js.map +1 -1
- package/dist/PortraitTiles/PortraitAvatar.d.ts +21 -0
- package/dist/PortraitTiles/PortraitAvatar.d.ts.map +1 -0
- package/dist/PortraitTiles/PortraitAvatar.js +29 -0
- package/dist/PortraitTiles/PortraitAvatar.js.map +1 -0
- package/dist/PortraitTiles/PortraitTiles.d.ts +162 -0
- package/dist/PortraitTiles/PortraitTiles.d.ts.map +1 -0
- package/dist/PortraitTiles/PortraitTiles.js +263 -0
- package/dist/PortraitTiles/PortraitTiles.js.map +1 -0
- package/dist/PortraitTiles/portraitColour.d.ts +49 -0
- package/dist/PortraitTiles/portraitColour.d.ts.map +1 -0
- package/dist/PortraitTiles/portraitColour.js +55 -0
- package/dist/PortraitTiles/portraitColour.js.map +1 -0
- package/dist/RouterLink/UnstyledLink.d.ts +17 -0
- package/dist/RouterLink/UnstyledLink.d.ts.map +1 -0
- package/dist/RouterLink/UnstyledLink.js +24 -0
- package/dist/RouterLink/UnstyledLink.js.map +1 -0
- package/dist/categoricalStyles.d.ts +27 -0
- package/dist/categoricalStyles.d.ts.map +1 -1
- package/dist/categoricalStyles.js +49 -0
- package/dist/categoricalStyles.js.map +1 -1
- package/dist/index.d.ts +6 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -1
- package/dist/index.js.map +1 -1
- package/dist/tileStyles.d.ts +15 -0
- package/dist/tileStyles.d.ts.map +1 -1
- package/dist/tileStyles.js +26 -1
- package/dist/tileStyles.js.map +1 -1
- package/package.json +2 -2
- package/src/ActionTiles/ActionTiles.mdx +82 -8
- package/src/ActionTiles/ActionTiles.stories.tsx +107 -2
- package/src/ActionTiles/ActionTiles.test.tsx +205 -5
- package/src/ActionTiles/ActionTiles.tsx +274 -110
- package/src/PortraitTiles/PortraitAvatar.tsx +49 -0
- package/src/PortraitTiles/PortraitTiles.mdx +153 -0
- package/src/PortraitTiles/PortraitTiles.stories.tsx +403 -0
- package/src/PortraitTiles/PortraitTiles.test.tsx +399 -0
- package/src/PortraitTiles/PortraitTiles.tsx +577 -0
- package/src/PortraitTiles/portraitColour.test.ts +39 -0
- package/src/PortraitTiles/portraitColour.ts +67 -0
- package/src/RadioGroup/RadioGroup.stories.tsx +39 -0
- package/src/RouterLink/RouterLink.mdx +25 -4
- package/src/RouterLink/UnstyledLink.stories.tsx +57 -0
- package/src/RouterLink/UnstyledLink.test.tsx +40 -0
- package/src/RouterLink/UnstyledLink.tsx +34 -0
- package/src/categoricalStyles.ts +57 -0
- package/src/index.ts +11 -0
- package/src/sourceRules.test.ts +17 -4
- 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
|
+
}
|