@charcuterie/ui 4.12.0 → 4.13.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/dist/MediaTile/MediaTile.d.ts +8 -1
- package/dist/MediaTile/MediaTile.d.ts.map +1 -1
- package/dist/MediaTile/MediaTile.js +20 -6
- package/dist/MediaTile/MediaTile.js.map +1 -1
- 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/MediaTile/MediaTile.mdx +11 -0
- package/src/MediaTile/MediaTile.stories.tsx +38 -4
- package/src/MediaTile/MediaTile.test.tsx +37 -1
- package/src/MediaTile/MediaTile.tsx +58 -10
- package/src/PortraitTiles/PortraitTiles.stories.tsx +21 -0
- package/src/PortraitTiles/PortraitTiles.test.tsx +44 -0
- package/src/PortraitTiles/PortraitTiles.tsx +60 -8
|
@@ -12,6 +12,13 @@ export type MediaTileProps = Omit<ComponentPropsWithRef<"figure">, "children"> &
|
|
|
12
12
|
badge?: ReactNode;
|
|
13
13
|
/** Shown in place of the image when it fails. */
|
|
14
14
|
fallback?: ReactNode;
|
|
15
|
+
/** Untruncated content beneath the title and subtitle. No nested controls in an interactive tile. */
|
|
16
|
+
details?: ReactNode;
|
|
17
|
+
/** Keep the entire object visible, or fill the frame. */
|
|
18
|
+
imageFit?: "cover" | "contain";
|
|
19
|
+
/** Put a thumbnail beside the caption for a compact list. */
|
|
20
|
+
layout?: "vertical" | "horizontal";
|
|
21
|
+
isTitleTruncated?: boolean;
|
|
15
22
|
href?: string;
|
|
16
23
|
/**
|
|
17
24
|
* Makes the tile a button. Hover, focus-visible and
|
|
@@ -50,5 +57,5 @@ export type MediaTileProps = Omit<ComponentPropsWithRef<"figure">, "children"> &
|
|
|
50
57
|
* `useState<string>`, which is what makes `loaded → error`
|
|
51
58
|
* impossible instead of merely unlikely — see `mediaStatus.ts`.
|
|
52
59
|
*/
|
|
53
|
-
export declare const MediaTile: ({ alt, aspectRatio, badge, className, fallback, href, isDisabled, onClick, src, subtitle, title, ...figureProps }: MediaTileProps) => ReactNode;
|
|
60
|
+
export declare const MediaTile: ({ alt, aspectRatio, badge, className, fallback, details, imageFit, layout, isTitleTruncated, href, isDisabled, onClick, src, subtitle, title, ...figureProps }: MediaTileProps) => ReactNode;
|
|
54
61
|
//# sourceMappingURL=MediaTile.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"MediaTile.d.ts","sourceRoot":"","sources":["../../src/MediaTile/MediaTile.tsx"],"names":[],"mappings":"AACA,OAAO,KAAK,EACV,qBAAqB,EACrB,SAAS,EACV,MAAM,OAAO,CAAA;
|
|
1
|
+
{"version":3,"file":"MediaTile.d.ts","sourceRoot":"","sources":["../../src/MediaTile/MediaTile.tsx"],"names":[],"mappings":"AACA,OAAO,KAAK,EACV,qBAAqB,EACrB,SAAS,EACV,MAAM,OAAO,CAAA;AAQd,MAAM,MAAM,cAAc,GAAG,QAAQ,GAAG,QAAQ,GAAG,OAAO,CAAA;AAE1D,MAAM,MAAM,cAAc,GAAG,IAAI,CAC/B,qBAAqB,CAAC,QAAQ,CAAC,EAC/B,UAAU,CACX,GAAG;IACF;;;;OAIG;IACH,GAAG,EAAE,MAAM,CAAA;IACX,WAAW,CAAC,EAAE,cAAc,CAAA;IAC5B,uDAAuD;IACvD,KAAK,CAAC,EAAE,SAAS,CAAA;IACjB,iDAAiD;IACjD,QAAQ,CAAC,EAAE,SAAS,CAAA;IACpB,qGAAqG;IACrG,OAAO,CAAC,EAAE,SAAS,CAAA;IACnB,yDAAyD;IACzD,QAAQ,CAAC,EAAE,OAAO,GAAG,SAAS,CAAA;IAC9B,6DAA6D;IAC7D,MAAM,CAAC,EAAE,UAAU,GAAG,YAAY,CAAA;IAClC,gBAAgB,CAAC,EAAE,OAAO,CAAA;IAC1B,IAAI,CAAC,EAAE,MAAM,CAAA;IACb;;;;;OAKG;IACH,UAAU,CAAC,EAAE,OAAO,CAAA;IACpB,OAAO,CAAC,EAAE,qBAAqB,CAAC,QAAQ,CAAC,CAAC,SAAS,CAAC,CAAA;IACpD,GAAG,CAAC,EAAE,MAAM,CAAA;IACZ,QAAQ,CAAC,EAAE,SAAS,CAAA;IACpB,KAAK,EAAE,MAAM,CAAA;CACd,CAAA;AAkBD;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AACH,eAAO,MAAM,SAAS,GAAI,gKAiBvB,cAAc,KAAG,SA+MnB,CAAA"}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
import { useStatus } from "@charcuterie/logic";
|
|
3
3
|
import { useEffect, useRef } from "react";
|
|
4
|
+
import { UnstyledLink } from "../RouterLink/UnstyledLink.js";
|
|
4
5
|
import { Skeleton } from "../Skeleton/Skeleton.js";
|
|
5
6
|
import { toClassName } from "../toClassName.js";
|
|
6
7
|
import { mediaTransitions } from "./mediaStatus.js";
|
|
@@ -9,7 +10,7 @@ import { mediaTransitions } from "./mediaStatus.js";
|
|
|
9
10
|
* className="block">` does not get these, which is why a
|
|
10
11
|
* Collection thumbnail looked inert under the pointer.
|
|
11
12
|
*/
|
|
12
|
-
const INTERACTIVE_CLASS = "group flex cursor-pointer
|
|
13
|
+
const INTERACTIVE_CLASS = "group flex cursor-pointer rounded-md outline-offset-2 hover:opacity-90 focus-visible:outline-solid focus-visible:outline-(length:--focus-ring-width) focus-visible:outline-focus-ring";
|
|
13
14
|
const RATIO_CLASS = {
|
|
14
15
|
// 2:3 is the standard poster trim, which is what Plex, Kavita, and
|
|
15
16
|
// every physical sleeve in the collection actually are.
|
|
@@ -42,7 +43,7 @@ const RATIO_CLASS = {
|
|
|
42
43
|
* `useState<string>`, which is what makes `loaded → error`
|
|
43
44
|
* impossible instead of merely unlikely — see `mediaStatus.ts`.
|
|
44
45
|
*/
|
|
45
|
-
export const MediaTile = ({ alt, aspectRatio = "poster", badge, className, fallback, href, isDisabled = false, onClick, src, subtitle, title, ...figureProps }) => {
|
|
46
|
+
export const MediaTile = ({ alt, aspectRatio = "poster", badge, className, fallback, details, imageFit = "cover", layout = "vertical", isTitleTruncated = true, href, isDisabled = false, onClick, src, subtitle, title, ...figureProps }) => {
|
|
46
47
|
const imageRef = useRef(null);
|
|
47
48
|
const { can, reset, status, transitionTo } = useStatus({
|
|
48
49
|
initialState: "loading",
|
|
@@ -51,6 +52,8 @@ export const MediaTile = ({ alt, aspectRatio = "poster", badge, className, fallb
|
|
|
51
52
|
useEffect(() => {
|
|
52
53
|
// A new `src` is a new load, including after a failure.
|
|
53
54
|
reset();
|
|
55
|
+
if (!src)
|
|
56
|
+
return;
|
|
54
57
|
const image = imageRef.current;
|
|
55
58
|
if (!image?.complete) {
|
|
56
59
|
return;
|
|
@@ -67,8 +70,11 @@ export const MediaTile = ({ alt, aspectRatio = "poster", badge, className, fallb
|
|
|
67
70
|
if (can("error")) {
|
|
68
71
|
transitionTo("error");
|
|
69
72
|
}
|
|
70
|
-
}, [can, reset, transitionTo]);
|
|
71
|
-
const media = (_jsxs("div", { className: toClassName("relative overflow-hidden rounded-md bg-surface-sunken", RATIO_CLASS[aspectRatio]
|
|
73
|
+
}, [can, reset, src, transitionTo]);
|
|
74
|
+
const media = (_jsxs("div", { className: toClassName("relative overflow-hidden rounded-md bg-surface-sunken", RATIO_CLASS[aspectRatio], layout === "horizontal" &&
|
|
75
|
+
"w-24 shrink-0 self-start cq-sm:w-32"), children: [!src || status === "error" ? (_jsx("div", { "aria-label": alt, className: "flex size-full flex-col items-center justify-center gap-1 p-2 text-center text-content-muted text-xs", role: "img", children: fallback ?? _jsx("span", { children: "Image unavailable" }) })) : (_jsxs(_Fragment, { children: [status === "loading" ? (_jsx(Skeleton, { className: "absolute inset-0 size-full" })) : null, _jsx("img", { alt: href || onClick ? "" : alt, className: toClassName("size-full transition-opacity duration-(--duration-normal) ease-standard", imageFit === "contain"
|
|
76
|
+
? "object-contain"
|
|
77
|
+
: "object-cover", status === "loaded"
|
|
72
78
|
? "opacity-100"
|
|
73
79
|
: "opacity-0"), decoding: "async", loading: "lazy", onError: () => {
|
|
74
80
|
if (can("error")) {
|
|
@@ -79,13 +85,21 @@ export const MediaTile = ({ alt, aspectRatio = "poster", badge, className, fallb
|
|
|
79
85
|
transitionTo("loaded");
|
|
80
86
|
}
|
|
81
87
|
}, ref: imageRef, src: src })] })), badge ? (_jsx("div", { className: "absolute top-2 start-2", children: badge })) : null] }));
|
|
82
|
-
const caption = title === "" ? null : (_jsxs("figcaption", { className: "flex flex-col gap-
|
|
88
|
+
const caption = title === "" ? null : (_jsxs("figcaption", { className: toClassName("min-w-0 flex-1 flex flex-col gap-1", layout === "vertical" && "pt-2"), children: [_jsx("span", { className: toClassName("font-medium text-content-primary text-sm cq-sm:text-md", isTitleTruncated ? "truncate" : "wrap-anywhere"), title: isTitleTruncated ? title : undefined, children: title }), subtitle ? (_jsx("span", { className: "truncate text-content-muted text-xs", children: subtitle })) : null, details ? (_jsx("div", { className: "min-w-0 wrap-anywhere text-sm text-content-secondary", children: details })) : null] }));
|
|
83
89
|
// An empty `title` is how a tile sits next to a name the
|
|
84
90
|
// parent already printed. The control still needs a name, so
|
|
85
91
|
// the alt is the fallback — wrapping it in a nameless
|
|
86
92
|
// `<button>` was how Collection's cover thumbnail became
|
|
87
93
|
// unfindable as well as un-hoverable.
|
|
88
94
|
const accessibleName = title === "" ? alt : title;
|
|
89
|
-
return (_jsx("figure", { ...figureProps, className: toClassName("@container flex
|
|
95
|
+
return (_jsx("figure", { ...figureProps, className: toClassName("@container flex", !href && !onClick
|
|
96
|
+
? layout === "horizontal"
|
|
97
|
+
? "flex-row gap-4"
|
|
98
|
+
: "flex-col"
|
|
99
|
+
: "flex-col", className), children: href ? (_jsxs(UnstyledLink, { "aria-label": accessibleName, className: toClassName(INTERACTIVE_CLASS, layout === "horizontal"
|
|
100
|
+
? "flex-row gap-4"
|
|
101
|
+
: "flex-col"), href: href, children: [media, caption] })) : onClick ? (_jsxs("button", { "aria-label": accessibleName, className: toClassName(INTERACTIVE_CLASS, layout === "horizontal"
|
|
102
|
+
? "flex-row gap-4"
|
|
103
|
+
: "flex-col", "w-full border-0 bg-transparent p-0 text-start disabled:cursor-not-allowed disabled:opacity-50 disabled:hover:opacity-50"), disabled: isDisabled, onClick: onClick, type: "button", children: [media, caption] })) : (_jsxs(_Fragment, { children: [media, caption] })) }));
|
|
90
104
|
};
|
|
91
105
|
//# sourceMappingURL=MediaTile.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"MediaTile.js","sourceRoot":"","sources":["../../src/MediaTile/MediaTile.tsx"],"names":[],"mappings":";AAAA,OAAO,EAAE,SAAS,EAAE,MAAM,oBAAoB,CAAA;AAK9C,OAAO,EAAE,SAAS,EAAE,MAAM,EAAE,MAAM,OAAO,CAAA;AAEzC,OAAO,EAAE,QAAQ,EAAE,MAAM,yBAA0B,CAAA;AACnD,OAAO,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAA;AAC/C,OAAO,EAAE,gBAAgB,EAAE,MAAM,kBAAkB,CAAA;
|
|
1
|
+
{"version":3,"file":"MediaTile.js","sourceRoot":"","sources":["../../src/MediaTile/MediaTile.tsx"],"names":[],"mappings":";AAAA,OAAO,EAAE,SAAS,EAAE,MAAM,oBAAoB,CAAA;AAK9C,OAAO,EAAE,SAAS,EAAE,MAAM,EAAE,MAAM,OAAO,CAAA;AAEzC,OAAO,EAAE,YAAY,EAAE,MAAM,+BAAgC,CAAA;AAC7D,OAAO,EAAE,QAAQ,EAAE,MAAM,yBAA0B,CAAA;AACnD,OAAO,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAA;AAC/C,OAAO,EAAE,gBAAgB,EAAE,MAAM,kBAAkB,CAAA;AAwCnD;;;;GAIG;AACH,MAAM,iBAAiB,GACrB,uLAAuL,CAAA;AAEzL,MAAM,WAAW,GAAmC;IAClD,mEAAmE;IACnE,wDAAwD;IACxD,MAAM,EAAE,cAAc;IACtB,MAAM,EAAE,eAAe;IACvB,KAAK,EAAE,cAAc;CACtB,CAAA;AAED;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AACH,MAAM,CAAC,MAAM,SAAS,GAAG,CAAC,EACxB,GAAG,EACH,WAAW,GAAG,QAAQ,EACtB,KAAK,EACL,SAAS,EACT,QAAQ,EACR,OAAO,EACP,QAAQ,GAAG,OAAO,EAClB,MAAM,GAAG,UAAU,EACnB,gBAAgB,GAAG,IAAI,EACvB,IAAI,EACJ,UAAU,GAAG,KAAK,EAClB,OAAO,EACP,GAAG,EACH,QAAQ,EACR,KAAK,EACL,GAAG,WAAW,EACC,EAAa,EAAE;IAC9B,MAAM,QAAQ,GAAG,MAAM,CAAmB,IAAI,CAAC,CAAA;IAE/C,MAAM,EAAE,GAAG,EAAE,KAAK,EAAE,MAAM,EAAE,YAAY,EAAE,GAAG,SAAS,CAAC;QACrD,YAAY,EAAE,SAAS;QACvB,WAAW,EAAE,gBAAgB;KAC9B,CAAC,CAAA;IAEF,SAAS,CAAC,GAAG,EAAE;QACb,wDAAwD;QACxD,KAAK,EAAE,CAAA;QAEP,IAAI,CAAC,GAAG;YAAE,OAAM;QAEhB,MAAM,KAAK,GAAG,QAAQ,CAAC,OAAO,CAAA;QAE9B,IAAI,CAAC,KAAK,EAAE,QAAQ,EAAE,CAAC;YACrB,OAAM;QACR,CAAC;QAED,0DAA0D;QAC1D,6DAA6D;QAC7D,mBAAmB;QACnB,IAAI,KAAK,CAAC,YAAY,GAAG,CAAC,EAAE,CAAC;YAC3B,IAAI,GAAG,CAAC,QAAQ,CAAC,EAAE,CAAC;gBAClB,YAAY,CAAC,QAAQ,CAAC,CAAA;YACxB,CAAC;YAED,OAAM;QACR,CAAC;QAED,IAAI,GAAG,CAAC,OAAO,CAAC,EAAE,CAAC;YACjB,YAAY,CAAC,OAAO,CAAC,CAAA;QACvB,CAAC;IACH,CAAC,EAAE,CAAC,GAAG,EAAE,KAAK,EAAE,GAAG,EAAE,YAAY,CAAC,CAAC,CAAA;IAEnC,MAAM,KAAK,GAAG,CACZ,eACE,SAAS,EAAE,WAAW,CACpB,uDAAuD,EACvD,WAAW,CAAC,WAAW,CAAC,EACxB,MAAM,KAAK,YAAY;YACrB,qCAAqC,CACxC,aAEA,CAAC,GAAG,IAAI,MAAM,KAAK,OAAO,CAAC,CAAC,CAAC,CAC5B,4BAOc,GAAG,EACf,SAAS,EAAC,sGAAsG,EAChH,IAAI,EAAC,KAAK,YAQT,QAAQ,IAAI,+CAA8B,GACvC,CACP,CAAC,CAAC,CAAC,CACF,8BACG,MAAM,KAAK,SAAS,CAAC,CAAC,CAAC,CACtB,KAAC,QAAQ,IAAC,SAAS,EAAC,4BAA4B,GAAG,CACpD,CAAC,CAAC,CAAC,IAAI,EAER,cACE,GAAG,EAAE,IAAI,IAAI,OAAO,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,GAAG,EAC/B,SAAS,EAAE,WAAW,CACpB,yEAAyE,EACzE,QAAQ,KAAK,SAAS;4BACpB,CAAC,CAAC,gBAAgB;4BAClB,CAAC,CAAC,cAAc,EAClB,MAAM,KAAK,QAAQ;4BACjB,CAAC,CAAC,aAAa;4BACf,CAAC,CAAC,WAAW,CAChB,EACD,QAAQ,EAAC,OAAO,EAChB,OAAO,EAAC,MAAM,EACd,OAAO,EAAE,GAAG,EAAE;4BACZ,IAAI,GAAG,CAAC,OAAO,CAAC,EAAE,CAAC;gCACjB,YAAY,CAAC,OAAO,CAAC,CAAA;4BACvB,CAAC;wBACH,CAAC,EACD,MAAM,EAAE,GAAG,EAAE;4BACX,IAAI,GAAG,CAAC,QAAQ,CAAC,EAAE,CAAC;gCAClB,YAAY,CAAC,QAAQ,CAAC,CAAA;4BACxB,CAAC;wBACH,CAAC,EACD,GAAG,EAAE,QAAQ,EACb,GAAG,EAAE,GAAG,GACR,IACD,CACJ,EAEA,KAAK,CAAC,CAAC,CAAC,CACP,cAAK,SAAS,EAAC,wBAAwB,YACpC,KAAK,GACF,CACP,CAAC,CAAC,CAAC,IAAI,IACJ,CACP,CAAA;IAED,MAAM,OAAO,GACX,KAAK,KAAK,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,CACpB,sBACE,SAAS,EAAE,WAAW,CACpB,oCAAoC,EACpC,MAAM,KAAK,UAAU,IAAI,MAAM,CAChC,aAED,eACE,SAAS,EAAE,WAAW,CACpB,wDAAwD,EACxD,gBAAgB,CAAC,CAAC,CAAC,UAAU,CAAC,CAAC,CAAC,eAAe,CAChD,EACD,KAAK,EAAE,gBAAgB,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,SAAS,YAE1C,KAAK,GACD,EAEN,QAAQ,CAAC,CAAC,CAAC,CACV,eAAM,SAAS,EAAC,qCAAqC,YAClD,QAAQ,GACJ,CACR,CAAC,CAAC,CAAC,IAAI,EACP,OAAO,CAAC,CAAC,CAAC,CACT,cAAK,SAAS,EAAC,sDAAsD,YAClE,OAAO,GACJ,CACP,CAAC,CAAC,CAAC,IAAI,IACG,CACd,CAAA;IAEH,yDAAyD;IACzD,6DAA6D;IAC7D,sDAAsD;IACtD,yDAAyD;IACzD,sCAAsC;IACtC,MAAM,cAAc,GAAG,KAAK,KAAK,EAAE,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,KAAK,CAAA;IAEjD,OAAO,CACL,oBACM,WAAW,EACf,SAAS,EAAE,WAAW,CACpB,iBAAiB,EACjB,CAAC,IAAI,IAAI,CAAC,OAAO;YACf,CAAC,CAAC,MAAM,KAAK,YAAY;gBACvB,CAAC,CAAC,gBAAgB;gBAClB,CAAC,CAAC,UAAU;YACd,CAAC,CAAC,UAAU,EACd,SAAS,CACV,YAEA,IAAI,CAAC,CAAC,CAAC,CAON,MAAC,YAAY,kBACC,cAAc,EAC1B,SAAS,EAAE,WAAW,CACpB,iBAAiB,EACjB,MAAM,KAAK,YAAY;gBACrB,CAAC,CAAC,gBAAgB;gBAClB,CAAC,CAAC,UAAU,CACf,EACD,IAAI,EAAE,IAAI,aAET,KAAK,EAEL,OAAO,IACK,CAChB,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,CACZ,gCACc,cAAc,EAC1B,SAAS,EAAE,WAAW,CACpB,iBAAiB,EACjB,MAAM,KAAK,YAAY;gBACrB,CAAC,CAAC,gBAAgB;gBAClB,CAAC,CAAC,UAAU,EACd,yHAAyH,CAC1H,EACD,QAAQ,EAAE,UAAU,EACpB,OAAO,EAAE,OAAO,EAChB,IAAI,EAAC,QAAQ,aAEZ,KAAK,EAEL,OAAO,IACD,CACV,CAAC,CAAC,CAAC,CACF,8BACG,KAAK,EAEL,OAAO,IACP,CACJ,GACM,CACV,CAAA;AACH,CAAC,CAAA"}
|
|
@@ -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
|
@@ -54,3 +54,14 @@ The caption's type steps up past `--cq-sm`, so a tile in a dense 8-across grid a
|
|
|
54
54
|
tile in a 3-across one are both legible.
|
|
55
55
|
|
|
56
56
|
<Canvas of={MediaTileStories.Responsive} />
|
|
57
|
+
|
|
58
|
+
## Cards and compact rows
|
|
59
|
+
|
|
60
|
+
`layout="horizontal"` places a bounded thumbnail beside the caption. Use
|
|
61
|
+
`imageFit="contain"` for product images that must remain uncropped, and
|
|
62
|
+
`isTitleTruncated={false}` to wrap distinguishing model names. `details` accepts
|
|
63
|
+
wrapped metadata beneath the caption; it must not contain controls when the tile is a link
|
|
64
|
+
or button. Links use the shared router adapter. An absent source shows the fallback
|
|
65
|
+
immediately, while a supplied source retains the loading and failure states.
|
|
66
|
+
|
|
67
|
+
<Canvas of={MediaTileStories.Compact} />
|
|
@@ -103,12 +103,12 @@ export const AllStates: Story = {
|
|
|
103
103
|
|
|
104
104
|
<StoryCell
|
|
105
105
|
align="stretch"
|
|
106
|
-
label="
|
|
106
|
+
label="missing — honest fallback"
|
|
107
107
|
>
|
|
108
108
|
<MediaTile
|
|
109
|
-
alt="
|
|
110
|
-
subtitle="
|
|
111
|
-
title="
|
|
109
|
+
alt="No cover added"
|
|
110
|
+
subtitle="Add a cover when one is available"
|
|
111
|
+
title="No cover"
|
|
112
112
|
/>
|
|
113
113
|
</StoryCell>
|
|
114
114
|
|
|
@@ -225,3 +225,37 @@ export const InteractiveButton: Story = {
|
|
|
225
225
|
title: "",
|
|
226
226
|
},
|
|
227
227
|
}
|
|
228
|
+
|
|
229
|
+
export const Compact: Story = {
|
|
230
|
+
args: {
|
|
231
|
+
alt: "Portable field recorder",
|
|
232
|
+
aspectRatio: "square",
|
|
233
|
+
details: (
|
|
234
|
+
<>
|
|
235
|
+
<p>3 here</p>
|
|
236
|
+
<p>Audio · Portable</p>
|
|
237
|
+
</>
|
|
238
|
+
),
|
|
239
|
+
href: "#recorder",
|
|
240
|
+
imageFit: "contain",
|
|
241
|
+
isTitleTruncated: false,
|
|
242
|
+
layout: "horizontal",
|
|
243
|
+
src: toPosterSrc("Recorder"),
|
|
244
|
+
title:
|
|
245
|
+
"Portable field recorder with interchangeable microphones",
|
|
246
|
+
},
|
|
247
|
+
render: (args) => (
|
|
248
|
+
<ContainerBoard>
|
|
249
|
+
<MediaTile {...args} />
|
|
250
|
+
</ContainerBoard>
|
|
251
|
+
),
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
export const MissingImage: Story = {
|
|
255
|
+
args: {
|
|
256
|
+
alt: "No photo of field recorder",
|
|
257
|
+
fallback: "No photo yet",
|
|
258
|
+
title: "Field recorder",
|
|
259
|
+
aspectRatio: "video",
|
|
260
|
+
},
|
|
261
|
+
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { composeStories } from "@storybook/react"
|
|
2
|
-
import { expect, userEvent } from "storybook/test"
|
|
2
|
+
import { expect, userEvent, within } from "storybook/test"
|
|
3
3
|
import { test } from "vitest"
|
|
4
4
|
|
|
5
5
|
import { mountStory } from "../mountStory.testHelpers.ts"
|
|
@@ -55,3 +55,39 @@ test("a clickable tile is one button with a pointer cursor", async () => {
|
|
|
55
55
|
|
|
56
56
|
await expect(button).toHaveFocus()
|
|
57
57
|
})
|
|
58
|
+
|
|
59
|
+
test("compact tiles contain the whole image and keep long names readable", async () => {
|
|
60
|
+
const { canvas } = await mountStory(
|
|
61
|
+
composeStories(stories).Compact,
|
|
62
|
+
)
|
|
63
|
+
const links = canvas.getAllByRole("link", {
|
|
64
|
+
name: "Portable field recorder with interchangeable microphones",
|
|
65
|
+
})
|
|
66
|
+
for (const link of links) {
|
|
67
|
+
expectAgentDrivable(
|
|
68
|
+
within(link.parentElement as HTMLElement),
|
|
69
|
+
{
|
|
70
|
+
role: "link",
|
|
71
|
+
name: "Portable field recorder with interchangeable microphones",
|
|
72
|
+
},
|
|
73
|
+
)
|
|
74
|
+
const image = link.querySelector("img")
|
|
75
|
+
await expect(image).toHaveStyle({
|
|
76
|
+
objectFit: "contain",
|
|
77
|
+
})
|
|
78
|
+
expect(link.scrollWidth).toBeLessThanOrEqual(
|
|
79
|
+
link.clientWidth,
|
|
80
|
+
)
|
|
81
|
+
}
|
|
82
|
+
})
|
|
83
|
+
|
|
84
|
+
test("an absent image shows the fallback without waiting for a failed request", async () => {
|
|
85
|
+
const { canvas } = await mountStory(
|
|
86
|
+
composeStories(stories).MissingImage,
|
|
87
|
+
)
|
|
88
|
+
const fallback = expectAgentDrivable(canvas, {
|
|
89
|
+
role: "img",
|
|
90
|
+
name: "No photo of field recorder",
|
|
91
|
+
})
|
|
92
|
+
await expect(fallback).toHaveTextContent("No photo yet")
|
|
93
|
+
})
|
|
@@ -5,6 +5,7 @@ import type {
|
|
|
5
5
|
} from "react"
|
|
6
6
|
import { useEffect, useRef } from "react"
|
|
7
7
|
|
|
8
|
+
import { UnstyledLink } from "../RouterLink/UnstyledLink.tsx"
|
|
8
9
|
import { Skeleton } from "../Skeleton/Skeleton.tsx"
|
|
9
10
|
import { toClassName } from "../toClassName.ts"
|
|
10
11
|
import { mediaTransitions } from "./mediaStatus.ts"
|
|
@@ -26,6 +27,13 @@ export type MediaTileProps = Omit<
|
|
|
26
27
|
badge?: ReactNode
|
|
27
28
|
/** Shown in place of the image when it fails. */
|
|
28
29
|
fallback?: ReactNode
|
|
30
|
+
/** Untruncated content beneath the title and subtitle. No nested controls in an interactive tile. */
|
|
31
|
+
details?: ReactNode
|
|
32
|
+
/** Keep the entire object visible, or fill the frame. */
|
|
33
|
+
imageFit?: "cover" | "contain"
|
|
34
|
+
/** Put a thumbnail beside the caption for a compact list. */
|
|
35
|
+
layout?: "vertical" | "horizontal"
|
|
36
|
+
isTitleTruncated?: boolean
|
|
29
37
|
href?: string
|
|
30
38
|
/**
|
|
31
39
|
* Makes the tile a button. Hover, focus-visible and
|
|
@@ -46,7 +54,7 @@ export type MediaTileProps = Omit<
|
|
|
46
54
|
* Collection thumbnail looked inert under the pointer.
|
|
47
55
|
*/
|
|
48
56
|
const INTERACTIVE_CLASS =
|
|
49
|
-
"group flex cursor-pointer
|
|
57
|
+
"group flex cursor-pointer rounded-md outline-offset-2 hover:opacity-90 focus-visible:outline-solid focus-visible:outline-(length:--focus-ring-width) focus-visible:outline-focus-ring"
|
|
50
58
|
|
|
51
59
|
const RATIO_CLASS: Record<MediaTileRatio, string> = {
|
|
52
60
|
// 2:3 is the standard poster trim, which is what Plex, Kavita, and
|
|
@@ -87,6 +95,10 @@ export const MediaTile = ({
|
|
|
87
95
|
badge,
|
|
88
96
|
className,
|
|
89
97
|
fallback,
|
|
98
|
+
details,
|
|
99
|
+
imageFit = "cover",
|
|
100
|
+
layout = "vertical",
|
|
101
|
+
isTitleTruncated = true,
|
|
90
102
|
href,
|
|
91
103
|
isDisabled = false,
|
|
92
104
|
onClick,
|
|
@@ -106,6 +118,8 @@ export const MediaTile = ({
|
|
|
106
118
|
// A new `src` is a new load, including after a failure.
|
|
107
119
|
reset()
|
|
108
120
|
|
|
121
|
+
if (!src) return
|
|
122
|
+
|
|
109
123
|
const image = imageRef.current
|
|
110
124
|
|
|
111
125
|
if (!image?.complete) {
|
|
@@ -126,16 +140,18 @@ export const MediaTile = ({
|
|
|
126
140
|
if (can("error")) {
|
|
127
141
|
transitionTo("error")
|
|
128
142
|
}
|
|
129
|
-
}, [can, reset, transitionTo])
|
|
143
|
+
}, [can, reset, src, transitionTo])
|
|
130
144
|
|
|
131
145
|
const media = (
|
|
132
146
|
<div
|
|
133
147
|
className={toClassName(
|
|
134
148
|
"relative overflow-hidden rounded-md bg-surface-sunken",
|
|
135
149
|
RATIO_CLASS[aspectRatio],
|
|
150
|
+
layout === "horizontal" &&
|
|
151
|
+
"w-24 shrink-0 self-start cq-sm:w-32",
|
|
136
152
|
)}
|
|
137
153
|
>
|
|
138
|
-
{status === "error" ? (
|
|
154
|
+
{!src || status === "error" ? (
|
|
139
155
|
<div
|
|
140
156
|
// Unconditionally an `img` carrying the same `alt`, even
|
|
141
157
|
// inside a link. The link's own `aria-label` is a name from
|
|
@@ -164,7 +180,10 @@ export const MediaTile = ({
|
|
|
164
180
|
<img
|
|
165
181
|
alt={href || onClick ? "" : alt}
|
|
166
182
|
className={toClassName(
|
|
167
|
-
"size-full
|
|
183
|
+
"size-full transition-opacity duration-(--duration-normal) ease-standard",
|
|
184
|
+
imageFit === "contain"
|
|
185
|
+
? "object-contain"
|
|
186
|
+
: "object-cover",
|
|
168
187
|
status === "loaded"
|
|
169
188
|
? "opacity-100"
|
|
170
189
|
: "opacity-0",
|
|
@@ -197,8 +216,19 @@ export const MediaTile = ({
|
|
|
197
216
|
|
|
198
217
|
const caption =
|
|
199
218
|
title === "" ? null : (
|
|
200
|
-
<figcaption
|
|
201
|
-
|
|
219
|
+
<figcaption
|
|
220
|
+
className={toClassName(
|
|
221
|
+
"min-w-0 flex-1 flex flex-col gap-1",
|
|
222
|
+
layout === "vertical" && "pt-2",
|
|
223
|
+
)}
|
|
224
|
+
>
|
|
225
|
+
<span
|
|
226
|
+
className={toClassName(
|
|
227
|
+
"font-medium text-content-primary text-sm cq-sm:text-md",
|
|
228
|
+
isTitleTruncated ? "truncate" : "wrap-anywhere",
|
|
229
|
+
)}
|
|
230
|
+
title={isTitleTruncated ? title : undefined}
|
|
231
|
+
>
|
|
202
232
|
{title}
|
|
203
233
|
</span>
|
|
204
234
|
|
|
@@ -207,6 +237,11 @@ export const MediaTile = ({
|
|
|
207
237
|
{subtitle}
|
|
208
238
|
</span>
|
|
209
239
|
) : null}
|
|
240
|
+
{details ? (
|
|
241
|
+
<div className="min-w-0 wrap-anywhere text-sm text-content-secondary">
|
|
242
|
+
{details}
|
|
243
|
+
</div>
|
|
244
|
+
) : null}
|
|
210
245
|
</figcaption>
|
|
211
246
|
)
|
|
212
247
|
|
|
@@ -221,7 +256,12 @@ export const MediaTile = ({
|
|
|
221
256
|
<figure
|
|
222
257
|
{...figureProps}
|
|
223
258
|
className={toClassName(
|
|
224
|
-
"@container flex
|
|
259
|
+
"@container flex",
|
|
260
|
+
!href && !onClick
|
|
261
|
+
? layout === "horizontal"
|
|
262
|
+
? "flex-row gap-4"
|
|
263
|
+
: "flex-col"
|
|
264
|
+
: "flex-col",
|
|
225
265
|
className,
|
|
226
266
|
)}
|
|
227
267
|
>
|
|
@@ -232,20 +272,28 @@ export const MediaTile = ({
|
|
|
232
272
|
// have to know whether a subtitle happened to be rendered.
|
|
233
273
|
// The `<img>` inside goes `alt=""` for the same reason: two
|
|
234
274
|
// names for one link is a screen reader reading it twice.
|
|
235
|
-
<
|
|
275
|
+
<UnstyledLink
|
|
236
276
|
aria-label={accessibleName}
|
|
237
|
-
className={
|
|
277
|
+
className={toClassName(
|
|
278
|
+
INTERACTIVE_CLASS,
|
|
279
|
+
layout === "horizontal"
|
|
280
|
+
? "flex-row gap-4"
|
|
281
|
+
: "flex-col",
|
|
282
|
+
)}
|
|
238
283
|
href={href}
|
|
239
284
|
>
|
|
240
285
|
{media}
|
|
241
286
|
|
|
242
287
|
{caption}
|
|
243
|
-
</
|
|
288
|
+
</UnstyledLink>
|
|
244
289
|
) : onClick ? (
|
|
245
290
|
<button
|
|
246
291
|
aria-label={accessibleName}
|
|
247
292
|
className={toClassName(
|
|
248
293
|
INTERACTIVE_CLASS,
|
|
294
|
+
layout === "horizontal"
|
|
295
|
+
? "flex-row gap-4"
|
|
296
|
+
: "flex-col",
|
|
249
297
|
"w-full border-0 bg-transparent p-0 text-start disabled:cursor-not-allowed disabled:opacity-50 disabled:hover:opacity-50",
|
|
250
298
|
)}
|
|
251
299
|
disabled={isDisabled}
|
|
@@ -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
|
<>
|