@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.
@@ -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;AAUD;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AACH,eAAO,MAAM,SAAS,GAAI,8FAWvB,cAAc,KAAG,SAoJnB,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
- return (_jsx("figure", { ...figureProps, className: toClassName("@container flex flex-col", className), children: href ? (_jsxs("a", { "aria-label": title, className: "group flex 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", href: href, children: [media, caption] })) : (_jsxs(_Fragment, { children: [media, caption] })) }));
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;AAyBnD,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,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,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,GAAG,EACpB,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,GAAG,CACd,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;IAED,OAAO,CACL,oBACM,WAAW,EACf,SAAS,EAAE,WAAW,CACpB,0BAA0B,EAC1B,SAAS,CACV,YAEA,IAAI,CAAC,CAAC,CAAC,CAON,2BACc,KAAK,EACjB,SAAS,EAAC,iLAAiL,EAC3L,IAAI,EAAE,IAAI,aAET,KAAK,EAEL,OAAO,IACN,CACL,CAAC,CAAC,CAAC,CACF,8BACG,KAAK,EAEL,OAAO,IACP,CACJ,GACM,CACV,CAAA;AACH,CAAC,CAAA"}
1
+ {"version":3,"file":"MediaTile.js","sourceRoot":"","sources":["../../src/MediaTile/MediaTile.tsx"],"names":[],"mappings":"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"}
@@ -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;;;;;;;;;;;;;;;;;;;;;;;;CAoI3B,CAAA"}
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
- const triggerId = referenceProps.id ??
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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"}
@@ -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` does not survive; `data-testid` does
73
+ * ### `id` survives — it did not always
74
74
  *
75
- * `useAnchoredOverlay` overwrites the trigger's `id` with a generated
76
- * one, so the portalled listbox can point `aria-labelledby` across at
77
- * it. An `id` passed here is therefore silently replaced — queuepilot
78
- * found this the hard way and switched its e2e handles to
79
- * `data-testid`, which nothing injects. Everything else on the button
80
- * (`data-*`, `onClick`, `form`, …) passes straight through.
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;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAgDG;AACH,eAAO,MAAM,MAAM,GAAI,uIAcpB,WAAW,KAAG,SAoDhB,CAAA"}
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"}
@@ -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` does not survive; `data-testid` does
51
+ * ### `id` survives — it did not always
52
52
  *
53
- * `useAnchoredOverlay` overwrites the trigger's `id` with a generated
54
- * one, so the portalled listbox can point `aria-labelledby` across at
55
- * it. An `id` passed here is therefore silently replaced — queuepilot
56
- * found this the hard way and switched its e2e handles to
57
- * `data-testid`, which nothing injects. Everything else on the button
58
- * (`data-*`, `onClick`, `form`, …) passes straight through.
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;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAgDG;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"}
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"}
@@ -1,12 +1,52 @@
1
1
  /**
2
- * Filter-and-join. Deliberately not `clsx`.
3
- *
4
- * Every call site in this package passes a flat list of strings and
5
- * conditionals, which is four lines of work — and a published
6
- * component library's dependency list is a thing consumers inherit,
7
- * so it earns its entries. `clsx` also accepts objects and nested
8
- * arrays, both of which make a className harder for the Tailwind
9
- * scanner and for `tailwindCandidates.test.ts` to read statically.
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":"AAAA;;;;;;;;;GASG;AACH,eAAO,MAAM,WAAW,GACtB,GAAG,OAAO,CAAC,KAAK,GAAG,MAAM,GAAG,IAAI,GAAG,SAAS,CAAC,EAAE,WACb,CAAA"}
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"}
@@ -1,12 +1,71 @@
1
+ import { extendTailwindMerge } from "tailwind-merge";
1
2
  /**
2
- * Filter-and-join. Deliberately not `clsx`.
3
- *
4
- * Every call site in this package passes a flat list of strings and
5
- * conditionals, which is four lines of work — and a published
6
- * component library's dependency list is a thing consumers inherit,
7
- * so it earns its entries. `clsx` also accepts objects and nested
8
- * arrays, both of which make a className harder for the Tailwind
9
- * scanner and for `tailwindCandidates.test.ts` to read statically.
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
@@ -1 +1 @@
1
- {"version":3,"file":"toClassName.js","sourceRoot":"","sources":["../src/toClassName.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AACH,MAAM,CAAC,MAAM,WAAW,GAAG,CACzB,GAAG,KAA4C,EAC/C,EAAE,CAAC,KAAK,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,CAAA"}
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.15.1",
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",
@@ -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`,
@@ -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 link carries `aria-label={title}` and the image
44
- goes `alt=""` — an agent writing `getByRole("link", { name: "Blade Runner" })` should not
45
- have to know whether a subtitle happened to be rendered, and two names for one link is a
46
- screen reader reading it twice.
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 } = composeStories(stories)
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
+ })