@charcuterie/ui 2.15.1 → 2.17.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 +9 -1
- package/dist/MediaTile/MediaTile.d.ts.map +1 -1
- package/dist/MediaTile/MediaTile.js +16 -4
- package/dist/MediaTile/MediaTile.js.map +1 -1
- package/dist/Overlay/useAnchoredOverlay.d.ts.map +1 -1
- package/dist/Overlay/useAnchoredOverlay.js +30 -1
- package/dist/Overlay/useAnchoredOverlay.js.map +1 -1
- package/dist/Picker/Picker.d.ts +13 -7
- package/dist/Picker/Picker.d.ts.map +1 -1
- package/dist/Picker/Picker.js +13 -7
- package/dist/Picker/Picker.js.map +1 -1
- package/dist/toClassName.d.ts +48 -8
- package/dist/toClassName.d.ts.map +1 -1
- package/dist/toClassName.js +68 -9
- package/dist/toClassName.js.map +1 -1
- package/package.json +3 -2
- package/src/Field/Field.mdx +6 -0
- package/src/Field/Field.stories.tsx +28 -0
- package/src/Field/Field.test.tsx +38 -0
- package/src/MediaTile/MediaTile.mdx +9 -4
- package/src/MediaTile/MediaTile.stories.tsx +14 -0
- package/src/MediaTile/MediaTile.test.tsx +17 -1
- package/src/MediaTile/MediaTile.tsx +56 -15
- package/src/Overlay/useAnchoredOverlay.ts +32 -0
- package/src/Picker/Picker.mdx +31 -0
- package/src/Picker/Picker.stories.tsx +74 -0
- package/src/Picker/Picker.test.tsx +13 -1
- package/src/Picker/Picker.tsx +13 -7
- package/src/sourceRules.test.ts +10 -0
- package/src/toClassName.test.ts +229 -0
- package/src/toClassName.ts +70 -9
|
@@ -13,6 +13,14 @@ export type MediaTileProps = Omit<ComponentPropsWithRef<"figure">, "children"> &
|
|
|
13
13
|
/** Shown in place of the image when it fails. */
|
|
14
14
|
fallback?: ReactNode;
|
|
15
15
|
href?: string;
|
|
16
|
+
/**
|
|
17
|
+
* Makes the tile a button. Hover, focus-visible and
|
|
18
|
+
* `cursor-pointer` come with it — wrapping `MediaTile` in a
|
|
19
|
+
* bare `<button>` is how a poster ends up with a text cursor
|
|
20
|
+
* and no hover at all. `href` wins if both are set.
|
|
21
|
+
*/
|
|
22
|
+
isDisabled?: boolean;
|
|
23
|
+
onClick?: ComponentPropsWithRef<"button">["onClick"];
|
|
16
24
|
src?: string;
|
|
17
25
|
subtitle?: ReactNode;
|
|
18
26
|
title: string;
|
|
@@ -42,5 +50,5 @@ export type MediaTileProps = Omit<ComponentPropsWithRef<"figure">, "children"> &
|
|
|
42
50
|
* `useState<string>`, which is what makes `loaded → error`
|
|
43
51
|
* impossible instead of merely unlikely — see `mediaStatus.ts`.
|
|
44
52
|
*/
|
|
45
|
-
export declare const MediaTile: ({ alt, aspectRatio, badge, className, fallback, href, src, subtitle, title, ...figureProps }: MediaTileProps) => ReactNode;
|
|
53
|
+
export declare const MediaTile: ({ alt, aspectRatio, badge, className, fallback, href, isDisabled, onClick, src, subtitle, title, ...figureProps }: MediaTileProps) => ReactNode;
|
|
46
54
|
//# 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;AAOd,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,IAAI,CAAC,EAAE,MAAM,CAAA;IACb,GAAG,CAAC,EAAE,MAAM,CAAA;IACZ,QAAQ,CAAC,EAAE,SAAS,CAAA;IACpB,KAAK,EAAE,MAAM,CAAA;CACd,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;AAOd,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,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,mHAavB,cAAc,KAAG,SA2KnB,CAAA"}
|
|
@@ -4,6 +4,12 @@ import { useEffect, useRef } from "react";
|
|
|
4
4
|
import { Skeleton } from "../Skeleton/Skeleton.js";
|
|
5
5
|
import { toClassName } from "../toClassName.js";
|
|
6
6
|
import { mediaTransitions } from "./mediaStatus.js";
|
|
7
|
+
/**
|
|
8
|
+
* Shared by the link and the button. A wrapped `<button
|
|
9
|
+
* className="block">` does not get these, which is why a
|
|
10
|
+
* Collection thumbnail looked inert under the pointer.
|
|
11
|
+
*/
|
|
12
|
+
const INTERACTIVE_CLASS = "group flex cursor-pointer flex-col rounded-md outline-offset-2 hover:opacity-90 focus-visible:outline-solid focus-visible:outline-(length:--focus-ring-width) focus-visible:outline-focus-ring";
|
|
7
13
|
const RATIO_CLASS = {
|
|
8
14
|
// 2:3 is the standard poster trim, which is what Plex, Kavita, and
|
|
9
15
|
// every physical sleeve in the collection actually are.
|
|
@@ -36,7 +42,7 @@ const RATIO_CLASS = {
|
|
|
36
42
|
* `useState<string>`, which is what makes `loaded → error`
|
|
37
43
|
* impossible instead of merely unlikely — see `mediaStatus.ts`.
|
|
38
44
|
*/
|
|
39
|
-
export const MediaTile = ({ alt, aspectRatio = "poster", badge, className, fallback, href, src, subtitle, title, ...figureProps }) => {
|
|
45
|
+
export const MediaTile = ({ alt, aspectRatio = "poster", badge, className, fallback, href, isDisabled = false, onClick, src, subtitle, title, ...figureProps }) => {
|
|
40
46
|
const imageRef = useRef(null);
|
|
41
47
|
const { can, reset, status, transitionTo } = useStatus({
|
|
42
48
|
initialState: "loading",
|
|
@@ -62,7 +68,7 @@ export const MediaTile = ({ alt, aspectRatio = "poster", badge, className, fallb
|
|
|
62
68
|
transitionTo("error");
|
|
63
69
|
}
|
|
64
70
|
}, [can, reset, transitionTo]);
|
|
65
|
-
const media = (_jsxs("div", { className: toClassName("relative overflow-hidden rounded-md bg-surface-sunken", RATIO_CLASS[aspectRatio]), children: [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 ? "" : alt, className: toClassName("size-full object-cover transition-opacity duration-(--duration-normal) ease-standard", status === "loaded"
|
|
71
|
+
const media = (_jsxs("div", { className: toClassName("relative overflow-hidden rounded-md bg-surface-sunken", RATIO_CLASS[aspectRatio]), children: [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 object-cover transition-opacity duration-(--duration-normal) ease-standard", status === "loaded"
|
|
66
72
|
? "opacity-100"
|
|
67
73
|
: "opacity-0"), decoding: "async", loading: "lazy", onError: () => {
|
|
68
74
|
if (can("error")) {
|
|
@@ -73,7 +79,13 @@ export const MediaTile = ({ alt, aspectRatio = "poster", badge, className, fallb
|
|
|
73
79
|
transitionTo("loaded");
|
|
74
80
|
}
|
|
75
81
|
}, ref: imageRef, src: src })] })), badge ? (_jsx("div", { className: "absolute top-2 start-2", children: badge })) : null] }));
|
|
76
|
-
const caption = (_jsxs("figcaption", { className: "flex flex-col gap-0.5 pt-2", children: [_jsx("span", { className: "truncate font-medium text-content-primary text-sm cq-sm:text-md", children: title }), subtitle ? (_jsx("span", { className: "truncate text-content-muted text-xs", children: subtitle })) : null] }));
|
|
77
|
-
|
|
82
|
+
const caption = title === "" ? null : (_jsxs("figcaption", { className: "flex flex-col gap-0.5 pt-2", children: [_jsx("span", { className: "truncate font-medium text-content-primary text-sm cq-sm:text-md", children: title }), subtitle ? (_jsx("span", { className: "truncate text-content-muted text-xs", children: subtitle })) : null] }));
|
|
83
|
+
// An empty `title` is how a tile sits next to a name the
|
|
84
|
+
// parent already printed. The control still needs a name, so
|
|
85
|
+
// the alt is the fallback — wrapping it in a nameless
|
|
86
|
+
// `<button>` was how Collection's cover thumbnail became
|
|
87
|
+
// unfindable as well as un-hoverable.
|
|
88
|
+
const accessibleName = title === "" ? alt : title;
|
|
89
|
+
return (_jsx("figure", { ...figureProps, className: toClassName("@container flex flex-col", className), children: href ? (_jsxs("a", { "aria-label": accessibleName, className: INTERACTIVE_CLASS, href: href, children: [media, caption] })) : onClick ? (_jsxs("button", { "aria-label": accessibleName, className: toClassName(INTERACTIVE_CLASS, "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] })) }));
|
|
78
90
|
};
|
|
79
91
|
//# 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,QAAQ,EAAE,MAAM,yBAA0B,CAAA;AACnD,OAAO,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAA;AAC/C,OAAO,EAAE,gBAAgB,EAAE,MAAM,kBAAkB,CAAA;AAiCnD;;;;GAIG;AACH,MAAM,iBAAiB,GACrB,gMAAgM,CAAA;AAElM,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,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,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,YAAY,CAAC,CAAC,CAAA;IAE9B,MAAM,KAAK,GAAG,CACZ,eACE,SAAS,EAAE,WAAW,CACpB,uDAAuD,EACvD,WAAW,CAAC,WAAW,CAAC,CACzB,aAEA,MAAM,KAAK,OAAO,CAAC,CAAC,CAAC,CACpB,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,sFAAsF,EACtF,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,sBAAY,SAAS,EAAC,4BAA4B,aAChD,eAAM,SAAS,EAAC,iEAAiE,YAC9E,KAAK,GACD,EAEN,QAAQ,CAAC,CAAC,CAAC,CACV,eAAM,SAAS,EAAC,qCAAqC,YAClD,QAAQ,GACJ,CACR,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,0BAA0B,EAC1B,SAAS,CACV,YAEA,IAAI,CAAC,CAAC,CAAC,CAON,2BACc,cAAc,EAC1B,SAAS,EAAE,iBAAiB,EAC5B,IAAI,EAAE,IAAI,aAET,KAAK,EAEL,OAAO,IACN,CACL,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,CACZ,gCACc,cAAc,EAC1B,SAAS,EAAE,WAAW,CACpB,iBAAiB,EACjB,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 +1 @@
|
|
|
1
|
-
{"version":3,"file":"useAnchoredOverlay.d.ts","sourceRoot":"","sources":["../../src/Overlay/useAnchoredOverlay.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,oBAAoB,CAAA;AAYnD,OAAO,KAAK,EAAE,YAAY,EAAE,SAAS,EAAE,MAAM,OAAO,CAAA;AAGpD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA4CG;AACH,MAAM,MAAM,mBAAmB,GAC3B,QAAQ,GACR,SAAS,GACT,MAAM,CAAA;AAEV,MAAM,MAAM,yBAAyB,GAAG;IACtC;;;;;;;OAOG;IACH,SAAS,CAAC,EAAE,SAAS,CAAC,WAAW,GAAG,IAAI,CAAC,CAAA;IACzC;;;;;;OAMG;IACH,mBAAmB,CAAC,EAAE,OAAO,CAAA;IAC7B,yBAAyB,CAAC,EAAE,OAAO,CAAA;IACnC,+DAA+D;IAC/D,qBAAqB,CAAC,EAAE,OAAO,CAAA;IAC/B,SAAS,EAAE,OAAO,CAAA;IAClB,iEAAiE;IACjE,WAAW,CAAC,EAAE,MAAM,CAAA;IACpB;;;;;;OAMG;IACH,UAAU,CAAC,EAAE,MAAM,CAAA;IACnB,0CAA0C;IAC1C,WAAW,CAAC,EAAE,MAAM,CAAA;IACpB,SAAS,EAAE,MAAM,IAAI,CAAA;IACrB,SAAS,CAAC,EAAE,SAAS,CAAA;IACrB,IAAI,EAAE,mBAAmB,CAAA;IACzB;;;OAGG;IACH,OAAO,CAAC,EAAE,YAAY,CAAA;CACvB,CAAA;AAQD,eAAO,MAAM,kBAAkB,GAAI,6KAahC,yBAAyB;;;;;;;;;;;;;;;;;;;;;;;;
|
|
1
|
+
{"version":3,"file":"useAnchoredOverlay.d.ts","sourceRoot":"","sources":["../../src/Overlay/useAnchoredOverlay.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,oBAAoB,CAAA;AAYnD,OAAO,KAAK,EAAE,YAAY,EAAE,SAAS,EAAE,MAAM,OAAO,CAAA;AAGpD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA4CG;AACH,MAAM,MAAM,mBAAmB,GAC3B,QAAQ,GACR,SAAS,GACT,MAAM,CAAA;AAEV,MAAM,MAAM,yBAAyB,GAAG;IACtC;;;;;;;OAOG;IACH,SAAS,CAAC,EAAE,SAAS,CAAC,WAAW,GAAG,IAAI,CAAC,CAAA;IACzC;;;;;;OAMG;IACH,mBAAmB,CAAC,EAAE,OAAO,CAAA;IAC7B,yBAAyB,CAAC,EAAE,OAAO,CAAA;IACnC,+DAA+D;IAC/D,qBAAqB,CAAC,EAAE,OAAO,CAAA;IAC/B,SAAS,EAAE,OAAO,CAAA;IAClB,iEAAiE;IACjE,WAAW,CAAC,EAAE,MAAM,CAAA;IACpB;;;;;;OAMG;IACH,UAAU,CAAC,EAAE,MAAM,CAAA;IACnB,0CAA0C;IAC1C,WAAW,CAAC,EAAE,MAAM,CAAA;IACpB,SAAS,EAAE,MAAM,IAAI,CAAA;IACrB,SAAS,CAAC,EAAE,SAAS,CAAA;IACrB,IAAI,EAAE,mBAAmB,CAAA;IACzB;;;OAGG;IACH,OAAO,CAAC,EAAE,YAAY,CAAA;CACvB,CAAA;AAQD,eAAO,MAAM,kBAAkB,GAAI,6KAahC,yBAAyB;;;;;;;;;;;;;;;;;;;;;;;;CAoK3B,CAAA"}
|
|
@@ -75,7 +75,36 @@ export const useAnchoredOverlay = ({ anchorRef, isEscapeDismissable = true, isOu
|
|
|
75
75
|
// a `listbox` panel needs, since a bare `role="listbox"` is an ARIA
|
|
76
76
|
// input field and must be named.
|
|
77
77
|
const generatedTriggerId = useUniqueId();
|
|
78
|
-
|
|
78
|
+
/**
|
|
79
|
+
* The trigger's OWN id wins over both.
|
|
80
|
+
*
|
|
81
|
+
* This hook used to mint an id and clone it over whatever the
|
|
82
|
+
* trigger already had, on the reasoning that the panel needs a
|
|
83
|
+
* stable target for `aria-labelledby`. It does — but any id serves
|
|
84
|
+
* that purpose, including the caller's, and overwriting had a
|
|
85
|
+
* consequence nobody traced: `Field` clones a `controlId` onto its
|
|
86
|
+
* child and renders `<label htmlFor={controlId}>`, so for every
|
|
87
|
+
* `Picker`/`Listbox`/`Combobox`/`Menu` inside a `Field` the label
|
|
88
|
+
* pointed at an element that did not exist. Measured in a real
|
|
89
|
+
* browser: zero nodes carried the id the `<label>` named.
|
|
90
|
+
*
|
|
91
|
+
* That is the exact defect `Field`'s docstring calls "precisely the
|
|
92
|
+
* defect this component was built to make impossible" — arriving by
|
|
93
|
+
* a different route, and silent, because a dangling `htmlFor`
|
|
94
|
+
* throws nothing and shows nothing.
|
|
95
|
+
*
|
|
96
|
+
* Preferring the child's id fixes the label without weakening the
|
|
97
|
+
* panel: `aria-labelledby` now points at the caller's id instead of
|
|
98
|
+
* a generated one, which is the same element either way.
|
|
99
|
+
*
|
|
100
|
+
* Two rediscoveries of the overwrite are already in the fleet —
|
|
101
|
+
* queuepilot and mux-magic both moved their e2e handles to
|
|
102
|
+
* `data-testid` because `id` "did not survive". `data-testid` is
|
|
103
|
+
* still the more robust handle, but `id` is no longer a trap.
|
|
104
|
+
*/
|
|
105
|
+
const triggerOwnId = trigger?.props?.id;
|
|
106
|
+
const triggerId = triggerOwnId ??
|
|
107
|
+
referenceProps.id ??
|
|
79
108
|
generatedTriggerId;
|
|
80
109
|
// In anchor mode floating-ui's reference is the consumer's element, set
|
|
81
110
|
// imperatively — there is no cloned trigger to carry `refs.setReference`.
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"useAnchoredOverlay.js","sourceRoot":"","sources":["../../src/Overlay/useAnchoredOverlay.ts"],"names":[],"mappings":"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
|
|
1
|
+
{"version":3,"file":"useAnchoredOverlay.js","sourceRoot":"","sources":["../../src/Overlay/useAnchoredOverlay.ts"],"names":[],"mappings":"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"}
|
package/dist/Picker/Picker.d.ts
CHANGED
|
@@ -70,14 +70,20 @@ export type PickerProps = Omit<ComponentPropsWithRef<"button">, "disabled" | "on
|
|
|
70
70
|
*
|
|
71
71
|
* Query it as `getByRole("button", { name: /^Language: / })`.
|
|
72
72
|
*
|
|
73
|
-
* ### `id`
|
|
73
|
+
* ### `id` survives — it did not always
|
|
74
74
|
*
|
|
75
|
-
* `useAnchoredOverlay`
|
|
76
|
-
* one
|
|
77
|
-
* it.
|
|
78
|
-
*
|
|
79
|
-
*
|
|
80
|
-
*
|
|
75
|
+
* `useAnchoredOverlay` used to overwrite the trigger's `id` with a
|
|
76
|
+
* generated one so the portalled listbox could point `aria-labelledby`
|
|
77
|
+
* across at it. That silently broke `Field`, which renders
|
|
78
|
+
* `<label htmlFor>` against an id it clones onto its child: the label
|
|
79
|
+
* pointed at nothing for every overlay control in a `Field`. The hook
|
|
80
|
+
* now prefers the trigger's own id and mints one only when absent, so
|
|
81
|
+
* both the label and the panel name the same element.
|
|
82
|
+
*
|
|
83
|
+
* `data-testid` remains the sturdier e2e handle — queuepilot and
|
|
84
|
+
* mux-magic both moved to it while `id` was still being replaced — but
|
|
85
|
+
* `id` is no longer a trap. Everything else on the button (`data-*`,
|
|
86
|
+
* `onClick`, `form`, …) passes straight through.
|
|
81
87
|
*/
|
|
82
88
|
export declare const Picker: ({ appearance, className, iconEnd, intent, isDisabled, label, onChange, options, placement, placeholder, size, value, ...buttonProps }: PickerProps) => ReactNode;
|
|
83
89
|
//# sourceMappingURL=Picker.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"Picker.d.ts","sourceRoot":"","sources":["../../src/Picker/Picker.tsx"],"names":[],"mappings":"AACA,OAAO,KAAK,EACV,WAAW,EACX,UAAU,EACX,MAAM,qBAAqB,CAAA;AAC5B,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,oBAAoB,CAAA;AACnD,OAAO,KAAK,EACV,qBAAqB,EACrB,SAAS,EACV,MAAM,OAAO,CAAA;AAGd,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,oBAAoB,CAAA;AAC1D,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,wBAAwB,CAAA;AAGzD,MAAM,MAAM,YAAY,GAAG,WAAW,CAAA;AAwBtC,MAAM,MAAM,WAAW,GAAG,IAAI,CAC5B,qBAAqB,CAAC,QAAQ,CAAC,EAC/B,UAAU,GAAG,UAAU,GAAG,OAAO,CAClC,GAAG;IACF,UAAU,CAAC,EAAE,gBAAgB,CAAA;IAC7B,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,MAAM,CAAC,EAAE,UAAU,CAAA;IACnB,UAAU,CAAC,EAAE,OAAO,CAAA;IACpB;;;OAGG;IACH,KAAK,EAAE,MAAM,CAAA;IACb;;;;;;OAMG;IACH,OAAO,CAAC,EAAE,SAAS,CAAA;IACnB,QAAQ,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,CAAA;IACjC,OAAO,EAAE,SAAS,YAAY,EAAE,CAAA;IAChC,SAAS,CAAC,EAAE,SAAS,CAAA;IACrB,0DAA0D;IAC1D,WAAW,CAAC,EAAE,MAAM,CAAA;IACpB,IAAI,CAAC,EAAE,WAAW,CAAA;IAClB,KAAK,CAAC,EAAE,MAAM,CAAA;CACf,CAAA;AAED
|
|
1
|
+
{"version":3,"file":"Picker.d.ts","sourceRoot":"","sources":["../../src/Picker/Picker.tsx"],"names":[],"mappings":"AACA,OAAO,KAAK,EACV,WAAW,EACX,UAAU,EACX,MAAM,qBAAqB,CAAA;AAC5B,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,oBAAoB,CAAA;AACnD,OAAO,KAAK,EACV,qBAAqB,EACrB,SAAS,EACV,MAAM,OAAO,CAAA;AAGd,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,oBAAoB,CAAA;AAC1D,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,wBAAwB,CAAA;AAGzD,MAAM,MAAM,YAAY,GAAG,WAAW,CAAA;AAwBtC,MAAM,MAAM,WAAW,GAAG,IAAI,CAC5B,qBAAqB,CAAC,QAAQ,CAAC,EAC/B,UAAU,GAAG,UAAU,GAAG,OAAO,CAClC,GAAG;IACF,UAAU,CAAC,EAAE,gBAAgB,CAAA;IAC7B,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,MAAM,CAAC,EAAE,UAAU,CAAA;IACnB,UAAU,CAAC,EAAE,OAAO,CAAA;IACpB;;;OAGG;IACH,KAAK,EAAE,MAAM,CAAA;IACb;;;;;;OAMG;IACH,OAAO,CAAC,EAAE,SAAS,CAAA;IACnB,QAAQ,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,CAAA;IACjC,OAAO,EAAE,SAAS,YAAY,EAAE,CAAA;IAChC,SAAS,CAAC,EAAE,SAAS,CAAA;IACrB,0DAA0D;IAC1D,WAAW,CAAC,EAAE,MAAM,CAAA;IACpB,IAAI,CAAC,EAAE,WAAW,CAAA;IAClB,KAAK,CAAC,EAAE,MAAM,CAAA;CACf,CAAA;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAsDG;AACH,eAAO,MAAM,MAAM,GAAI,uIAcpB,WAAW,KAAG,SAoDhB,CAAA"}
|
package/dist/Picker/Picker.js
CHANGED
|
@@ -48,14 +48,20 @@ const CHEVRON_DOWN = (_jsx("svg", { "aria-hidden": "true", className: "size-4",
|
|
|
48
48
|
*
|
|
49
49
|
* Query it as `getByRole("button", { name: /^Language: / })`.
|
|
50
50
|
*
|
|
51
|
-
* ### `id`
|
|
51
|
+
* ### `id` survives — it did not always
|
|
52
52
|
*
|
|
53
|
-
* `useAnchoredOverlay`
|
|
54
|
-
* one
|
|
55
|
-
* it.
|
|
56
|
-
*
|
|
57
|
-
*
|
|
58
|
-
*
|
|
53
|
+
* `useAnchoredOverlay` used to overwrite the trigger's `id` with a
|
|
54
|
+
* generated one so the portalled listbox could point `aria-labelledby`
|
|
55
|
+
* across at it. That silently broke `Field`, which renders
|
|
56
|
+
* `<label htmlFor>` against an id it clones onto its child: the label
|
|
57
|
+
* pointed at nothing for every overlay control in a `Field`. The hook
|
|
58
|
+
* now prefers the trigger's own id and mints one only when absent, so
|
|
59
|
+
* both the label and the panel name the same element.
|
|
60
|
+
*
|
|
61
|
+
* `data-testid` remains the sturdier e2e handle — queuepilot and
|
|
62
|
+
* mux-magic both moved to it while `id` was still being replaced — but
|
|
63
|
+
* `id` is no longer a trap. Everything else on the button (`data-*`,
|
|
64
|
+
* `onClick`, `form`, …) passes straight through.
|
|
59
65
|
*/
|
|
60
66
|
export const Picker = ({ appearance = "outline", className, iconEnd = CHEVRON_DOWN, intent = "neutral", isDisabled = false, label, onChange, options, placement, placeholder, size, value, ...buttonProps }) => {
|
|
61
67
|
const { hide, isVisible, toggle } = useVisibility();
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"Picker.js","sourceRoot":"","sources":["../../src/Picker/Picker.tsx"],"names":[],"mappings":";AAAA,OAAO,EAAE,aAAa,EAAE,MAAM,oBAAoB,CAAA;AAWlD,OAAO,EAAE,MAAM,EAAE,MAAM,qBAAsB,CAAA;AAG7C,OAAO,EAAE,OAAO,EAAE,MAAM,uBAAwB,CAAA;AAIhD;;;;GAIG;AACH,MAAM,YAAY,GAAG,CACnB,6BACc,MAAM,EAClB,SAAS,EAAC,QAAQ,EAClB,IAAI,EAAC,MAAM,EACX,SAAS,EAAE,KAAK,EAChB,MAAM,EAAC,cAAc,EACrB,aAAa,EAAC,OAAO,EACrB,cAAc,EAAC,OAAO,EACtB,WAAW,EAAE,IAAI,EACjB,OAAO,EAAC,WAAW,EACnB,KAAK,EAAC,4BAA4B,YAElC,eAAM,CAAC,EAAC,cAAc,GAAG,GACrB,CACP,CAAA;AAgCD
|
|
1
|
+
{"version":3,"file":"Picker.js","sourceRoot":"","sources":["../../src/Picker/Picker.tsx"],"names":[],"mappings":";AAAA,OAAO,EAAE,aAAa,EAAE,MAAM,oBAAoB,CAAA;AAWlD,OAAO,EAAE,MAAM,EAAE,MAAM,qBAAsB,CAAA;AAG7C,OAAO,EAAE,OAAO,EAAE,MAAM,uBAAwB,CAAA;AAIhD;;;;GAIG;AACH,MAAM,YAAY,GAAG,CACnB,6BACc,MAAM,EAClB,SAAS,EAAC,QAAQ,EAClB,IAAI,EAAC,MAAM,EACX,SAAS,EAAE,KAAK,EAChB,MAAM,EAAC,cAAc,EACrB,aAAa,EAAC,OAAO,EACrB,cAAc,EAAC,OAAO,EACtB,WAAW,EAAE,IAAI,EACjB,OAAO,EAAC,WAAW,EACnB,KAAK,EAAC,4BAA4B,YAElC,eAAM,CAAC,EAAC,cAAc,GAAG,GACrB,CACP,CAAA;AAgCD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAsDG;AACH,MAAM,CAAC,MAAM,MAAM,GAAG,CAAC,EACrB,UAAU,GAAG,SAAS,EACtB,SAAS,EACT,OAAO,GAAG,YAAY,EACtB,MAAM,GAAG,SAAS,EAClB,UAAU,GAAG,KAAK,EAClB,KAAK,EACL,QAAQ,EACR,OAAO,EACP,SAAS,EACT,WAAW,EACX,IAAI,EACJ,KAAK,EACL,GAAG,WAAW,EACF,EAAa,EAAE;IAC3B,MAAM,EAAE,IAAI,EAAE,SAAS,EAAE,MAAM,EAAE,GAAG,aAAa,EAAE,CAAA;IAEnD,MAAM,aAAa,GAAG,OAAO,CAAC,IAAI,CAChC,CAAC,MAAM,EAAE,EAAE,CAAC,MAAM,CAAC,KAAK,KAAK,KAAK,CACnC,CAAA;IAED,MAAM,WAAW,GACf,aAAa,KAAK,SAAS;QACzB,CAAC,CAAC,WAAW;QACb,CAAC,CAAC,CAAC,aAAa,CAAC,SAAS;YACxB,CAAC,OAAO,aAAa,CAAC,KAAK,KAAK,QAAQ;gBACtC,CAAC,CAAC,aAAa,CAAC,KAAK;gBACrB,CAAC,CAAC,SAAS,CAAC,CAAC,CAAA;IAErB,MAAM,WAAW,GAAG,WAAW,IAAI,WAAW,IAAI,EAAE,CAAA;IAEpD,OAAO,CACL,KAAC,OAAO;IACN,8DAA8D;IAC9D,kEAAkE;IAClE,2DAA2D;;QAF3D,8DAA8D;QAC9D,kEAAkE;QAClE,2DAA2D;QAC3D,SAAS,EAAE,SAAS,IAAI,CAAC,UAAU,EACnC,SAAS,EAAE,IAAI,EACf,QAAQ,EAAE,QAAQ,EAClB,OAAO,EAAE,OAAO,EAChB,SAAS,EAAE,SAAS,EACpB,aAAa,EAAE,KAAK,EACpB,OAAO,EACL,KAAC,MAAM,OACD,WAAW,EACf,UAAU,EAAE,UAAU,gBACV,GAAG,KAAK,KAAK,WAAW,EAAE,EACtC,SAAS,EAAE,SAAS,EACpB,OAAO,EAAE,OAAO,EAChB,MAAM,EAAE,MAAM,EACd,UAAU,EAAE,UAAU,EACtB,OAAO,EAAE,CAAC,UAAU,EAAE,EAAE;gBACtB,WAAW,CAAC,OAAO,EAAE,CAAC,UAAU,CAAC,CAAA;gBAEjC,MAAM,EAAE,CAAA;YACV,CAAC,EACD,IAAI,EAAE,IAAI,EACV,IAAI,EAAC,QAAQ,YAEZ,aAAa,KAAK,SAAS;gBAC1B,CAAC,CAAC,WAAW;gBACb,CAAC,CAAC,aAAa,CAAC,KAAK,GAChB,GAEX,CACH,CAAA;AACH,CAAC,CAAA"}
|
package/dist/toClassName.d.ts
CHANGED
|
@@ -1,12 +1,52 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
2
|
+
* Join a class list, and let a later class beat an earlier one that
|
|
3
|
+
* targets the same CSS property.
|
|
4
|
+
*
|
|
5
|
+
* ### Why this is not a plain join any more
|
|
6
|
+
*
|
|
7
|
+
* It was, and that was a bug. `getControlClassName` assembles
|
|
8
|
+
* `toClassName(CONTROL_BASE_CLASS, …, className)`, and
|
|
9
|
+
* `CONTROL_BASE_CLASS` opens with `inline-flex`. A caller writing
|
|
10
|
+
*
|
|
11
|
+
* ```tsx
|
|
12
|
+
* <Button className="hidden lg:inline-flex" />
|
|
13
|
+
* ```
|
|
14
|
+
*
|
|
15
|
+
* emitted `class="inline-flex … hidden lg:inline-flex"`. Both are
|
|
16
|
+
* display utilities at equal specificity, so the winner is decided by
|
|
17
|
+
* **source order in the generated stylesheet**, not by order in the
|
|
18
|
+
* class attribute — and the caller has no way to win. `className` is a
|
|
19
|
+
* documented escape hatch, and it did not work for the most common
|
|
20
|
+
* thing an escape hatch is used for.
|
|
21
|
+
*
|
|
22
|
+
* It failed *silently and invisibly*. mail-sifter's header used exactly
|
|
23
|
+
* that class to hide a duplicate button below `lg`; it never hid, and
|
|
24
|
+
* nobody noticed for weeks because at 390px the header happened to fit
|
|
25
|
+
* anyway. It only surfaced when an unrelated type-ramp change pushed the
|
|
26
|
+
* width to 427px and scrolled the page sideways. Two more repos hit the
|
|
27
|
+
* same shape independently.
|
|
28
|
+
*
|
|
29
|
+
* ### Why `tailwind-merge` and not `clsx`
|
|
30
|
+
*
|
|
31
|
+
* The old docblock argued against taking a dependency, on the grounds
|
|
32
|
+
* that a published component library's dependency list is inherited by
|
|
33
|
+
* consumers and that `clsx`'s object and nested-array forms make a
|
|
34
|
+
* className harder for the Tailwind scanner and for
|
|
35
|
+
* `tailwindCandidates.test.ts` to read statically.
|
|
36
|
+
*
|
|
37
|
+
* The first half still stands and this entry earns its place; the
|
|
38
|
+
* second half does not transfer. `tailwind-merge` does not change the
|
|
39
|
+
* *input* shape at all — this still takes a flat list of strings and
|
|
40
|
+
* conditionals, and every call site is still statically scannable. It
|
|
41
|
+
* changes only which of two conflicting classes survives.
|
|
42
|
+
*
|
|
43
|
+
* ### It is a no-op on everything this package emits
|
|
44
|
+
*
|
|
45
|
+
* Merging could in principle drop one of our own classes. It does not:
|
|
46
|
+
* the test file checks all 358 class-string literals in `src` and all
|
|
47
|
+
* 288 strings `getControlClassName` composes, and the merge changes
|
|
48
|
+
* none of them. So this only ever acts on a genuine caller conflict —
|
|
49
|
+
* which is the entire point.
|
|
10
50
|
*/
|
|
11
51
|
export declare const toClassName: (...parts: (false | string | null | undefined)[]) => string;
|
|
12
52
|
//# sourceMappingURL=toClassName.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"toClassName.d.ts","sourceRoot":"","sources":["../src/toClassName.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"toClassName.d.ts","sourceRoot":"","sources":["../src/toClassName.ts"],"names":[],"mappings":"AAqBA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAiDG;AACH,eAAO,MAAM,WAAW,GACtB,GAAG,OAAO,CAAC,KAAK,GAAG,MAAM,GAAG,IAAI,GAAG,SAAS,CAAC,EAAE,WACI,CAAA"}
|
package/dist/toClassName.js
CHANGED
|
@@ -1,12 +1,71 @@
|
|
|
1
|
+
import { extendTailwindMerge } from "tailwind-merge";
|
|
1
2
|
/**
|
|
2
|
-
*
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
3
|
+
* `ease-standard` is ours, from the motion tokens — Tailwind ships
|
|
4
|
+
* `ease-linear`/`ease-in`/`ease-out`/`ease-in-out` and knows nothing
|
|
5
|
+
* about it. Without this it lands in no class group, so a caller
|
|
6
|
+
* passing `ease-linear` would get BOTH and the winner would be decided
|
|
7
|
+
* by stylesheet source order — exactly the bug this file now exists to
|
|
8
|
+
* prevent, one utility further along.
|
|
9
|
+
*
|
|
10
|
+
* Nothing else needs registering. Verified empirically rather than
|
|
11
|
+
* assumed: `toClassName.test.ts` asserts the merge is a no-op across
|
|
12
|
+
* every class string in this package and every string
|
|
13
|
+
* `getControlClassName` composes.
|
|
14
|
+
*/
|
|
15
|
+
const mergeClassNames = extendTailwindMerge({
|
|
16
|
+
extend: {
|
|
17
|
+
classGroups: { ease: [{ ease: ["standard"] }] },
|
|
18
|
+
},
|
|
19
|
+
});
|
|
20
|
+
/**
|
|
21
|
+
* Join a class list, and let a later class beat an earlier one that
|
|
22
|
+
* targets the same CSS property.
|
|
23
|
+
*
|
|
24
|
+
* ### Why this is not a plain join any more
|
|
25
|
+
*
|
|
26
|
+
* It was, and that was a bug. `getControlClassName` assembles
|
|
27
|
+
* `toClassName(CONTROL_BASE_CLASS, …, className)`, and
|
|
28
|
+
* `CONTROL_BASE_CLASS` opens with `inline-flex`. A caller writing
|
|
29
|
+
*
|
|
30
|
+
* ```tsx
|
|
31
|
+
* <Button className="hidden lg:inline-flex" />
|
|
32
|
+
* ```
|
|
33
|
+
*
|
|
34
|
+
* emitted `class="inline-flex … hidden lg:inline-flex"`. Both are
|
|
35
|
+
* display utilities at equal specificity, so the winner is decided by
|
|
36
|
+
* **source order in the generated stylesheet**, not by order in the
|
|
37
|
+
* class attribute — and the caller has no way to win. `className` is a
|
|
38
|
+
* documented escape hatch, and it did not work for the most common
|
|
39
|
+
* thing an escape hatch is used for.
|
|
40
|
+
*
|
|
41
|
+
* It failed *silently and invisibly*. mail-sifter's header used exactly
|
|
42
|
+
* that class to hide a duplicate button below `lg`; it never hid, and
|
|
43
|
+
* nobody noticed for weeks because at 390px the header happened to fit
|
|
44
|
+
* anyway. It only surfaced when an unrelated type-ramp change pushed the
|
|
45
|
+
* width to 427px and scrolled the page sideways. Two more repos hit the
|
|
46
|
+
* same shape independently.
|
|
47
|
+
*
|
|
48
|
+
* ### Why `tailwind-merge` and not `clsx`
|
|
49
|
+
*
|
|
50
|
+
* The old docblock argued against taking a dependency, on the grounds
|
|
51
|
+
* that a published component library's dependency list is inherited by
|
|
52
|
+
* consumers and that `clsx`'s object and nested-array forms make a
|
|
53
|
+
* className harder for the Tailwind scanner and for
|
|
54
|
+
* `tailwindCandidates.test.ts` to read statically.
|
|
55
|
+
*
|
|
56
|
+
* The first half still stands and this entry earns its place; the
|
|
57
|
+
* second half does not transfer. `tailwind-merge` does not change the
|
|
58
|
+
* *input* shape at all — this still takes a flat list of strings and
|
|
59
|
+
* conditionals, and every call site is still statically scannable. It
|
|
60
|
+
* changes only which of two conflicting classes survives.
|
|
61
|
+
*
|
|
62
|
+
* ### It is a no-op on everything this package emits
|
|
63
|
+
*
|
|
64
|
+
* Merging could in principle drop one of our own classes. It does not:
|
|
65
|
+
* the test file checks all 358 class-string literals in `src` and all
|
|
66
|
+
* 288 strings `getControlClassName` composes, and the merge changes
|
|
67
|
+
* none of them. So this only ever acts on a genuine caller conflict —
|
|
68
|
+
* which is the entire point.
|
|
10
69
|
*/
|
|
11
|
-
export const toClassName = (...parts) => parts.filter(Boolean).join(" ");
|
|
70
|
+
export const toClassName = (...parts) => mergeClassNames(parts.filter(Boolean).join(" "));
|
|
12
71
|
//# sourceMappingURL=toClassName.js.map
|
package/dist/toClassName.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"toClassName.js","sourceRoot":"","sources":["../src/toClassName.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"toClassName.js","sourceRoot":"","sources":["../src/toClassName.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,mBAAmB,EAAE,MAAM,gBAAgB,CAAA;AAEpD;;;;;;;;;;;;GAYG;AACH,MAAM,eAAe,GAAG,mBAAmB,CAAC;IAC1C,MAAM,EAAE;QACN,WAAW,EAAE,EAAE,IAAI,EAAE,CAAC,EAAE,IAAI,EAAE,CAAC,UAAU,CAAC,EAAE,CAAC,EAAE;KAChD;CACF,CAAC,CAAA;AAEF;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAiDG;AACH,MAAM,CAAC,MAAM,WAAW,GAAG,CACzB,GAAG,KAA4C,EAC/C,EAAE,CAAC,eAAe,CAAC,KAAK,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,CAAA"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@charcuterie/ui",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.17.0",
|
|
4
4
|
"description": "The Charcuterie components. Tokens for look, @charcuterie/logic for state, ARIA that agents can actually drive.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -51,7 +51,8 @@
|
|
|
51
51
|
"@charcuterie/logic": "2.0.0",
|
|
52
52
|
"@charcuterie/tokens": "1.5.0",
|
|
53
53
|
"@floating-ui/react": "^0.27.20",
|
|
54
|
-
"@tanstack/react-virtual": "^3.13.12"
|
|
54
|
+
"@tanstack/react-virtual": "^3.13.12",
|
|
55
|
+
"tailwind-merge": "^3.6.0"
|
|
55
56
|
},
|
|
56
57
|
"peerDependencies": {
|
|
57
58
|
"react": ">=19",
|
package/src/Field/Field.mdx
CHANGED
|
@@ -27,6 +27,12 @@ in one place and cannot be spelled twice.
|
|
|
27
27
|
|
|
28
28
|
<Canvas of={FieldStories.Default} />
|
|
29
29
|
|
|
30
|
+
`Field` is also how a `Picker` (or `Button`, or `Select`) sits next to
|
|
31
|
+
its label **without butting against the last word**. Those controls are
|
|
32
|
+
flush — no outer margin — on purpose. The stacked `gap-1.5` here is the
|
|
33
|
+
gutter. An inline label + trigger is a row with `gap-*`, not a margin
|
|
34
|
+
on the control. See [Picker](../Picker/Picker.mdx#the-trigger-is-flush--the-parent-owns-the-gap).
|
|
35
|
+
|
|
30
36
|
## Props
|
|
31
37
|
|
|
32
38
|
<Controls />
|
|
@@ -4,6 +4,7 @@ import {
|
|
|
4
4
|
StoryCell,
|
|
5
5
|
StoryGrid,
|
|
6
6
|
} from "../board.storyHelpers.tsx"
|
|
7
|
+
import { Picker } from "../Picker/Picker.tsx"
|
|
7
8
|
import { Select } from "../Select/Select.tsx"
|
|
8
9
|
import { Tooltip } from "../Tooltip/Tooltip.tsx"
|
|
9
10
|
import { Field } from "./Field.tsx"
|
|
@@ -55,6 +56,33 @@ export const AdoptsChildId: Story = {
|
|
|
55
56
|
},
|
|
56
57
|
}
|
|
57
58
|
|
|
59
|
+
/**
|
|
60
|
+
* An overlay-triggered control — `Picker`, and by the same route
|
|
61
|
+
* `Listbox`, `Combobox` and `Menu` — is the case that used to break.
|
|
62
|
+
*
|
|
63
|
+
* `useAnchoredOverlay` minted its own trigger id and cloned it over
|
|
64
|
+
* whatever was there, so the `<label htmlFor>` above pointed at an
|
|
65
|
+
* element that did not exist. It failed silently: a dangling `htmlFor`
|
|
66
|
+
* throws nothing, renders nothing, and only shows up if you go looking
|
|
67
|
+
* for the id in the DOM. The hook now prefers the trigger's own id.
|
|
68
|
+
*/
|
|
69
|
+
export const AdoptsOverlayTriggerId: Story = {
|
|
70
|
+
args: {
|
|
71
|
+
children: (
|
|
72
|
+
<Picker
|
|
73
|
+
label="Language"
|
|
74
|
+
onChange={() => {}}
|
|
75
|
+
options={[
|
|
76
|
+
{ label: "English", value: "en" },
|
|
77
|
+
{ label: "Japanese", value: "ja" },
|
|
78
|
+
]}
|
|
79
|
+
value="en"
|
|
80
|
+
/>
|
|
81
|
+
),
|
|
82
|
+
label: "Language",
|
|
83
|
+
},
|
|
84
|
+
}
|
|
85
|
+
|
|
58
86
|
/**
|
|
59
87
|
* The control is a slot, not a hardcoded `<input>` — `Select` goes
|
|
60
88
|
* in the same hole, and so does an app's own control. The `id`,
|
package/src/Field/Field.test.tsx
CHANGED
|
@@ -9,6 +9,7 @@ import * as stories from "./Field.stories.tsx"
|
|
|
9
9
|
|
|
10
10
|
const {
|
|
11
11
|
AdoptsChildId,
|
|
12
|
+
AdoptsOverlayTriggerId,
|
|
12
13
|
AllStates,
|
|
13
14
|
AllVariants,
|
|
14
15
|
Default,
|
|
@@ -38,6 +39,43 @@ test("a control's own id is adopted, not overwritten", async () => {
|
|
|
38
39
|
await expectNoAxeViolations(canvasElement)
|
|
39
40
|
})
|
|
40
41
|
|
|
42
|
+
test("an overlay trigger keeps the id the label points at", async () => {
|
|
43
|
+
const { canvas, canvasElement } = await mountStory(
|
|
44
|
+
AdoptsOverlayTriggerId,
|
|
45
|
+
)
|
|
46
|
+
|
|
47
|
+
// The regression this exists for: `useAnchoredOverlay` minted its own
|
|
48
|
+
// trigger id and cloned it over the Field's, so the `<label htmlFor>`
|
|
49
|
+
// named an element that was not in the document. Nothing threw and
|
|
50
|
+
// nothing looked wrong — the only way to see it was to go looking for
|
|
51
|
+
// the id.
|
|
52
|
+
const label = canvasElement.querySelector("label")
|
|
53
|
+
|
|
54
|
+
await expect(label).not.toBeNull()
|
|
55
|
+
|
|
56
|
+
const htmlFor = label?.getAttribute("for")
|
|
57
|
+
|
|
58
|
+
await expect(htmlFor).toBeTruthy()
|
|
59
|
+
|
|
60
|
+
// The assertion that fails on the old behaviour: the id the label
|
|
61
|
+
// names must actually exist, and must BE the trigger.
|
|
62
|
+
const target = canvasElement.ownerDocument.getElementById(
|
|
63
|
+
htmlFor as string,
|
|
64
|
+
)
|
|
65
|
+
|
|
66
|
+
await expect(target).not.toBeNull()
|
|
67
|
+
await expect(target?.tagName).toBe("BUTTON")
|
|
68
|
+
|
|
69
|
+
// And the round trip callers actually rely on: findable by the
|
|
70
|
+
// label's own text.
|
|
71
|
+
expectAgentDrivable(canvas, {
|
|
72
|
+
name: /^Language: /,
|
|
73
|
+
role: "button",
|
|
74
|
+
})
|
|
75
|
+
|
|
76
|
+
await expectNoAxeViolations(canvasElement)
|
|
77
|
+
})
|
|
78
|
+
|
|
41
79
|
test("the label names the control it points at", async () => {
|
|
42
80
|
const { canvas, canvasElement } =
|
|
43
81
|
await mountStory(Default)
|
|
@@ -40,10 +40,15 @@ carries, and for the same reason.
|
|
|
40
40
|
|
|
41
41
|
## Names
|
|
42
42
|
|
|
43
|
-
`alt` is required. When `href` is set, the
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
43
|
+
`alt` is required. When `href` or `onClick` is set, the control carries
|
|
44
|
+
`aria-label={title}` (or `alt` if `title` is empty) and the image goes `alt=""` — an agent
|
|
45
|
+
writing `getByRole("link", { name: "Blade Runner" })` or `getByRole("button", { name: "Change cover for Blade Runner" })`
|
|
46
|
+
should not have to know whether a subtitle happened to be rendered, and two names for one
|
|
47
|
+
control is a screen reader reading it twice.
|
|
48
|
+
|
|
49
|
+
`onClick` is the button form of that same chrome: pointer cursor, hover fade, focus-visible
|
|
50
|
+
ring. Do not wrap `MediaTile` in a bare `<button>` — Tailwind's preflight leaves a text
|
|
51
|
+
cursor and no hover, which is how a Collection thumbnail looked inert.
|
|
47
52
|
|
|
48
53
|
The caption's type steps up past `--cq-sm`, so a tile in a dense 8-across grid and the same
|
|
49
54
|
tile in a 3-across one are both legible.
|
|
@@ -209,3 +209,17 @@ export const Interactive: Story = {
|
|
|
209
209
|
title: "Blade Runner",
|
|
210
210
|
},
|
|
211
211
|
}
|
|
212
|
+
|
|
213
|
+
/**
|
|
214
|
+
* Same chrome as the link — pointer, hover fade, focus ring —
|
|
215
|
+
* as a button. Wrapping the tile in a bare `<button>` is how a
|
|
216
|
+
* Collection thumbnail ended up with a text cursor.
|
|
217
|
+
*/
|
|
218
|
+
export const InteractiveButton: Story = {
|
|
219
|
+
args: {
|
|
220
|
+
alt: "Change cover for Blade Runner",
|
|
221
|
+
onClick: () => undefined,
|
|
222
|
+
src: toPosterSrc("Blade Runner"),
|
|
223
|
+
title: "",
|
|
224
|
+
},
|
|
225
|
+
}
|
|
@@ -6,7 +6,8 @@ import { mountStory } from "../mountStory.testHelpers.ts"
|
|
|
6
6
|
import { expectAgentDrivable } from "../testing/index.ts"
|
|
7
7
|
import * as stories from "./MediaTile.stories.tsx"
|
|
8
8
|
|
|
9
|
-
const { Default, Interactive } =
|
|
9
|
+
const { Default, Interactive, InteractiveButton } =
|
|
10
|
+
composeStories(stories)
|
|
10
11
|
|
|
11
12
|
test("an unlinked tile's poster carries the name", async () => {
|
|
12
13
|
const { canvas } = await mountStory(Default)
|
|
@@ -39,3 +40,18 @@ test("a linked tile is one link named for its title", async () => {
|
|
|
39
40
|
// for one link is a screen reader reading it twice.
|
|
40
41
|
await expect(canvas.queryAllByRole("img")).toHaveLength(0)
|
|
41
42
|
})
|
|
43
|
+
|
|
44
|
+
test("a clickable tile is one button with a pointer cursor", async () => {
|
|
45
|
+
const { canvas } = await mountStory(InteractiveButton)
|
|
46
|
+
|
|
47
|
+
const button = expectAgentDrivable(canvas, {
|
|
48
|
+
name: "Change cover for Blade Runner",
|
|
49
|
+
role: "button",
|
|
50
|
+
})
|
|
51
|
+
|
|
52
|
+
await expect(button).toHaveStyle({ cursor: "pointer" })
|
|
53
|
+
|
|
54
|
+
await userEvent.tab()
|
|
55
|
+
|
|
56
|
+
await expect(button).toHaveFocus()
|
|
57
|
+
})
|