@charcuterie/ui 4.12.0 → 4.12.1
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/dist/PortraitTiles/PortraitTiles.d.ts +2 -2
- package/dist/PortraitTiles/PortraitTiles.d.ts.map +1 -1
- package/dist/PortraitTiles/PortraitTiles.js +51 -4
- package/dist/PortraitTiles/PortraitTiles.js.map +1 -1
- package/package.json +1 -1
- package/src/PortraitTiles/PortraitTiles.stories.tsx +21 -0
- package/src/PortraitTiles/PortraitTiles.test.tsx +44 -0
- package/src/PortraitTiles/PortraitTiles.tsx +60 -8
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { CategoricalIndex, ControlSize } from "@charcuterie/tokens";
|
|
2
|
-
import type
|
|
2
|
+
import { type ReactNode } from "react";
|
|
3
3
|
/**
|
|
4
4
|
* Where one portrait's colour comes from. Two arms, and the fleet
|
|
5
5
|
* has both — the same split
|
|
@@ -88,7 +88,7 @@ export type PortraitTilesProps = {
|
|
|
88
88
|
layout?: PortraitTilesLayout;
|
|
89
89
|
/** The narrowest a portrait track may be, in CSS px. */
|
|
90
90
|
minTileInlineSize?: number;
|
|
91
|
-
/**
|
|
91
|
+
/** Fill available rows and center an incomplete final row without widening its tiles. */
|
|
92
92
|
isFillingRow?: boolean;
|
|
93
93
|
/** What `isExternal` announces. Not shown. */
|
|
94
94
|
newTabLabel?: string;
|
|
@@ -1 +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,
|
|
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,EAEL,KAAK,SAAS,EAIf,MAAM,OAAO,CAAA;AAqBd;;;;;;;;;;;;;;;;;;;;;;GAsBG;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,yFAAyF;IACzF,YAAY,CAAC,EAAE,OAAO,CAAA;IACtB,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,oGAU3B,kBAAkB,KAAG,SA8PvB,CAAA"}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
2
|
import { CATEGORICAL_INDEX_COUNT, CATEGORICAL_SEQUENCE, } from "@charcuterie/tokens";
|
|
3
|
+
import { useLayoutEffect, useRef, useState, } from "react";
|
|
3
4
|
import { CATEGORICAL_APPEARANCE_CLASS, CATEGORICAL_CONTENT_CLASS, CATEGORICAL_HOVER_BORDER_CLASS, CATEGORICAL_RING_CLASS, } from "../categoricalStyles.js";
|
|
4
5
|
import { FOCUS_RING_CLASS } from "../intentStyles.js";
|
|
5
6
|
import { useRouterLink } from "../RouterLink/RouterLinkProvider.js";
|
|
@@ -212,7 +213,43 @@ const COLOUR_STAT_CLASS = "text-(--charcuterie-portrait-stat)";
|
|
|
212
213
|
*/
|
|
213
214
|
export const PortraitTiles = ({ className, items, label, layout = "auto", minTileInlineSize = 200, isFillingRow = false, newTabLabel = "(opens in a new tab)", onChoose, size = "md", }) => {
|
|
214
215
|
const RouterLink = useRouterLink();
|
|
215
|
-
|
|
216
|
+
const gridRef = useRef(null);
|
|
217
|
+
const [lastRow, setLastRow] = useState({
|
|
218
|
+
start: 0,
|
|
219
|
+
offset: 0,
|
|
220
|
+
});
|
|
221
|
+
const itemCount = items.length;
|
|
222
|
+
useLayoutEffect(() => {
|
|
223
|
+
const grid = gridRef.current;
|
|
224
|
+
if (!isFillingRow || !grid)
|
|
225
|
+
return;
|
|
226
|
+
// CSS still chooses the tracks. Read its result so spacing, density,
|
|
227
|
+
// container resizing, and caller-supplied minimums stay in agreement.
|
|
228
|
+
const measure = () => {
|
|
229
|
+
const style = getComputedStyle(grid);
|
|
230
|
+
const tracks = style.gridTemplateColumns
|
|
231
|
+
.split(" ")
|
|
232
|
+
.map(Number.parseFloat)
|
|
233
|
+
.filter((width) => width > 0);
|
|
234
|
+
const remainder = itemCount % tracks.length;
|
|
235
|
+
const gap = Number.parseFloat(style.columnGap) || 0;
|
|
236
|
+
const offset = remainder
|
|
237
|
+
? ((tracks.length - remainder) *
|
|
238
|
+
((tracks[0] ?? 0) + gap)) /
|
|
239
|
+
2
|
|
240
|
+
: 0;
|
|
241
|
+
const start = itemCount - (remainder || 0);
|
|
242
|
+
setLastRow((previous) => previous.start === start &&
|
|
243
|
+
previous.offset === offset
|
|
244
|
+
? previous
|
|
245
|
+
: { start, offset });
|
|
246
|
+
};
|
|
247
|
+
measure();
|
|
248
|
+
const observer = new ResizeObserver(measure);
|
|
249
|
+
observer.observe(grid);
|
|
250
|
+
return () => observer.disconnect();
|
|
251
|
+
});
|
|
252
|
+
return (_jsx("div", { className: toClassName("@container", className), children: _jsx("div", { "aria-label": label, ref: gridRef, className: toClassName("grid", isFillingRow
|
|
216
253
|
? "grid-cols-[repeat(auto-fit,minmax(min(var(--charcuterie-tile-min-inline-size),100%),1fr))]"
|
|
217
254
|
: TILE_COLUMNS_CLASS, GAP_CLASS[size]), role: "group", style: {
|
|
218
255
|
[TILE_MIN_INLINE_SIZE_PROPERTY]: `${minTileInlineSize}px`,
|
|
@@ -240,9 +277,19 @@ export const PortraitTiles = ({ className, items, label, layout = "auto", minTil
|
|
|
240
277
|
// and the same one `Swatch` uses: a colour out of a
|
|
241
278
|
// database cannot be a class, because Tailwind generates
|
|
242
279
|
// its CSS at build time from source text.
|
|
243
|
-
const tileStyle =
|
|
244
|
-
|
|
245
|
-
|
|
280
|
+
const tileStyle = {
|
|
281
|
+
...(color === undefined
|
|
282
|
+
? {}
|
|
283
|
+
: getPortraitColourProperties(color)),
|
|
284
|
+
...(isFillingRow &&
|
|
285
|
+
position >= lastRow.start &&
|
|
286
|
+
lastRow.offset > 0
|
|
287
|
+
? {
|
|
288
|
+
position: "relative",
|
|
289
|
+
insetInlineStart: lastRow.offset,
|
|
290
|
+
}
|
|
291
|
+
: {}),
|
|
292
|
+
};
|
|
246
293
|
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
|
|
247
294
|
? COLOUR_STAT_CLASS
|
|
248
295
|
: 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] }));
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"PortraitTiles.js","sourceRoot":"","sources":["../../src/PortraitTiles/PortraitTiles.tsx"],"names":[],"mappings":";AAIA,OAAO,EACL,uBAAuB,EACvB,oBAAoB,GACrB,MAAM,qBAAqB,CAAA;
|
|
1
|
+
{"version":3,"file":"PortraitTiles.js","sourceRoot":"","sources":["../../src/PortraitTiles/PortraitTiles.tsx"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -417,3 +417,24 @@ export const BoundedSet: Story = {
|
|
|
417
417
|
</div>
|
|
418
418
|
),
|
|
419
419
|
}
|
|
420
|
+
|
|
421
|
+
/** An incomplete row keeps the full row's tile widths and centers as a group. */
|
|
422
|
+
export const CenteredLastRow: Story = {
|
|
423
|
+
args: {
|
|
424
|
+
...BoundedSet.args,
|
|
425
|
+
items: [
|
|
426
|
+
...PINNED_ITEMS,
|
|
427
|
+
{
|
|
428
|
+
value: "admin",
|
|
429
|
+
label: "Admin",
|
|
430
|
+
initials: "A",
|
|
431
|
+
hint: "Manage",
|
|
432
|
+
},
|
|
433
|
+
],
|
|
434
|
+
},
|
|
435
|
+
render: (props) => (
|
|
436
|
+
<div style={{ width: 780 }}>
|
|
437
|
+
<PortraitTiles {...props} />
|
|
438
|
+
</div>
|
|
439
|
+
),
|
|
440
|
+
}
|
|
@@ -411,3 +411,47 @@ test("a bounded set keeps three portraits beside each other without an empty fou
|
|
|
411
411
|
await expect(rectangles[0]?.top).toBe(rectangles[2]?.top)
|
|
412
412
|
await expect(rectangles[0]?.width).toBeGreaterThan(240)
|
|
413
413
|
})
|
|
414
|
+
|
|
415
|
+
test("a wrapping bounded set centers its final row and responds to container resizing", async () => {
|
|
416
|
+
const { CenteredLastRow } = composeStories(stories)
|
|
417
|
+
const { canvas, canvasElement } =
|
|
418
|
+
await mountStory(CenteredLastRow)
|
|
419
|
+
const group = expectAgentDrivable(canvas, {
|
|
420
|
+
name: "People",
|
|
421
|
+
role: "group",
|
|
422
|
+
})
|
|
423
|
+
const tiles = Array.from(group.querySelectorAll("button"))
|
|
424
|
+
const wrapper = group.parentElement?.parentElement
|
|
425
|
+
if (!wrapper) throw new Error("Missing story wrapper")
|
|
426
|
+
for (const width of [780, 540, 1100, 200, 780]) {
|
|
427
|
+
wrapper.style.width = `${width}px`
|
|
428
|
+
await waitFor(() => {
|
|
429
|
+
const bounds = group.getBoundingClientRect()
|
|
430
|
+
const rects = tiles.map((tile) =>
|
|
431
|
+
tile.getBoundingClientRect(),
|
|
432
|
+
)
|
|
433
|
+
const lastTop = rects.at(-1)?.top
|
|
434
|
+
const last = rects.filter(
|
|
435
|
+
(rect) => rect.top === lastTop,
|
|
436
|
+
)
|
|
437
|
+
expect(
|
|
438
|
+
Math.abs(
|
|
439
|
+
((last[0]?.left ?? 0) +
|
|
440
|
+
(last.at(-1)?.right ?? 0)) /
|
|
441
|
+
2 -
|
|
442
|
+
(bounds.left + bounds.right) / 2,
|
|
443
|
+
),
|
|
444
|
+
).toBeLessThan(1)
|
|
445
|
+
expect(
|
|
446
|
+
Math.abs(
|
|
447
|
+
(rects[0]?.width ?? 0) -
|
|
448
|
+
(rects.at(-1)?.width ?? 0),
|
|
449
|
+
),
|
|
450
|
+
).toBeLessThan(1)
|
|
451
|
+
expect(group.scrollWidth).toBeLessThanOrEqual(
|
|
452
|
+
group.clientWidth,
|
|
453
|
+
)
|
|
454
|
+
})
|
|
455
|
+
}
|
|
456
|
+
await expectNoAxeViolations(canvasElement)
|
|
457
|
+
})
|
|
@@ -6,7 +6,13 @@ import {
|
|
|
6
6
|
CATEGORICAL_INDEX_COUNT,
|
|
7
7
|
CATEGORICAL_SEQUENCE,
|
|
8
8
|
} from "@charcuterie/tokens"
|
|
9
|
-
import
|
|
9
|
+
import {
|
|
10
|
+
type CSSProperties,
|
|
11
|
+
type ReactNode,
|
|
12
|
+
useLayoutEffect,
|
|
13
|
+
useRef,
|
|
14
|
+
useState,
|
|
15
|
+
} from "react"
|
|
10
16
|
|
|
11
17
|
import {
|
|
12
18
|
CATEGORICAL_APPEARANCE_CLASS,
|
|
@@ -114,7 +120,7 @@ export type PortraitTilesProps = {
|
|
|
114
120
|
layout?: PortraitTilesLayout
|
|
115
121
|
/** The narrowest a portrait track may be, in CSS px. */
|
|
116
122
|
minTileInlineSize?: number
|
|
117
|
-
/**
|
|
123
|
+
/** Fill available rows and center an incomplete final row without widening its tiles. */
|
|
118
124
|
isFillingRow?: boolean
|
|
119
125
|
/** What `isExternal` announces. Not shown. */
|
|
120
126
|
newTabLabel?: string
|
|
@@ -375,6 +381,44 @@ export const PortraitTiles = ({
|
|
|
375
381
|
size = "md",
|
|
376
382
|
}: PortraitTilesProps): ReactNode => {
|
|
377
383
|
const RouterLink = useRouterLink()
|
|
384
|
+
const gridRef = useRef<HTMLDivElement>(null)
|
|
385
|
+
const [lastRow, setLastRow] = useState({
|
|
386
|
+
start: 0,
|
|
387
|
+
offset: 0,
|
|
388
|
+
})
|
|
389
|
+
const itemCount = items.length
|
|
390
|
+
|
|
391
|
+
useLayoutEffect(() => {
|
|
392
|
+
const grid = gridRef.current
|
|
393
|
+
if (!isFillingRow || !grid) return
|
|
394
|
+
// CSS still chooses the tracks. Read its result so spacing, density,
|
|
395
|
+
// container resizing, and caller-supplied minimums stay in agreement.
|
|
396
|
+
const measure = () => {
|
|
397
|
+
const style = getComputedStyle(grid)
|
|
398
|
+
const tracks = style.gridTemplateColumns
|
|
399
|
+
.split(" ")
|
|
400
|
+
.map(Number.parseFloat)
|
|
401
|
+
.filter((width) => width > 0)
|
|
402
|
+
const remainder = itemCount % tracks.length
|
|
403
|
+
const gap = Number.parseFloat(style.columnGap) || 0
|
|
404
|
+
const offset = remainder
|
|
405
|
+
? ((tracks.length - remainder) *
|
|
406
|
+
((tracks[0] ?? 0) + gap)) /
|
|
407
|
+
2
|
|
408
|
+
: 0
|
|
409
|
+
const start = itemCount - (remainder || 0)
|
|
410
|
+
setLastRow((previous) =>
|
|
411
|
+
previous.start === start &&
|
|
412
|
+
previous.offset === offset
|
|
413
|
+
? previous
|
|
414
|
+
: { start, offset },
|
|
415
|
+
)
|
|
416
|
+
}
|
|
417
|
+
measure()
|
|
418
|
+
const observer = new ResizeObserver(measure)
|
|
419
|
+
observer.observe(grid)
|
|
420
|
+
return () => observer.disconnect()
|
|
421
|
+
})
|
|
378
422
|
|
|
379
423
|
return (
|
|
380
424
|
// The container and the thing querying it cannot be the same
|
|
@@ -386,6 +430,7 @@ export const PortraitTiles = ({
|
|
|
386
430
|
{/* biome-ignore lint/a11y/useSemanticElements: the semantic element for `group` is `<fieldset>`, which is a form-control grouping — it drags `<legend>` semantics, a border and form-reset behaviour onto a set of links that is not a form. Same call `ActionTiles` and `Menu`'s group already made. */}
|
|
387
431
|
<div
|
|
388
432
|
aria-label={label}
|
|
433
|
+
ref={gridRef}
|
|
389
434
|
className={toClassName(
|
|
390
435
|
"grid",
|
|
391
436
|
isFillingRow
|
|
@@ -463,12 +508,19 @@ export const PortraitTiles = ({
|
|
|
463
508
|
// and the same one `Swatch` uses: a colour out of a
|
|
464
509
|
// database cannot be a class, because Tailwind generates
|
|
465
510
|
// its CSS at build time from source text.
|
|
466
|
-
const tileStyle =
|
|
467
|
-
color === undefined
|
|
468
|
-
?
|
|
469
|
-
:
|
|
470
|
-
|
|
471
|
-
|
|
511
|
+
const tileStyle: CSSProperties = {
|
|
512
|
+
...(color === undefined
|
|
513
|
+
? {}
|
|
514
|
+
: getPortraitColourProperties(color)),
|
|
515
|
+
...(isFillingRow &&
|
|
516
|
+
position >= lastRow.start &&
|
|
517
|
+
lastRow.offset > 0
|
|
518
|
+
? {
|
|
519
|
+
position: "relative",
|
|
520
|
+
insetInlineStart: lastRow.offset,
|
|
521
|
+
}
|
|
522
|
+
: {}),
|
|
523
|
+
}
|
|
472
524
|
|
|
473
525
|
const content = (
|
|
474
526
|
<>
|