@charcuterie/ui 4.24.0 → 4.25.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.
@@ -1,3 +1,4 @@
1
+ import type { IntentName } from "@charcuterie/tokens";
1
2
  import type { ReactNode } from "react";
2
3
  export type GroupedListHeadingLevel = 2 | 3 | 4 | 5 | 6;
3
4
  export type GroupedListSize = "md" | "sm";
@@ -10,24 +11,86 @@ export type GroupedListItem = {
10
11
  * **Name a repeated action after its row.** Ten rows of "Open" are
11
12
  * ten buttons an agent cannot tell apart; `aria-label="Open Lobby"`
12
13
  * on each is one `getByRole` away.
14
+ *
15
+ * In a row with `onSelect` these stay their own targets: they sit
16
+ * above the whole-row button, so pressing one never selects the
17
+ * row.
13
18
  */
14
19
  actions?: ReactNode;
20
+ /**
21
+ * A figure that belongs beside the text rather than under it — a
22
+ * payout, a total, a schedule. It sits between the text and the
23
+ * actions in a wide list and drops under the text in a narrow one,
24
+ * the same content-decided wrap the actions use: it asks for at
25
+ * least 11rem beside the text column's 12rem. Never interactive:
26
+ * in a row with `onSelect` the whole-row button sits over it.
27
+ */
28
+ aside?: ReactNode;
15
29
  /** `Badge`s, drawn after the label and wrapping onto their own line when they run out of room. */
16
30
  badges?: ReactNode;
17
31
  /**
18
32
  * Anything richer than one line of help — a payout, a rule list.
19
33
  * Drawn under the description, inside the text column, and never
20
- * truncated.
34
+ * truncated. Never interactive in a row with `onSelect`; a control
35
+ * belongs in `actions`.
21
36
  */
22
37
  content?: ReactNode;
23
38
  /** One muted line under the label: a path, a key, a count. Truncated like the label. */
24
39
  description?: string;
40
+ /**
41
+ * The intent `isHighlighted` rings the row in. `accent` by default;
42
+ * an app whose highlight means "this just paid out" passes
43
+ * `success`.
44
+ */
45
+ highlightIntent?: IntentName;
46
+ /**
47
+ * `true` rings the row and pulses the ring twice — "this one just
48
+ * changed", for a row a scan, a save or an arrival has touched.
49
+ * The ring stays while the flag does, so the app decides how long
50
+ * it lasts, usually by clearing it on a timer.
51
+ *
52
+ * **To pulse a row that is already highlighted again, change its
53
+ * `key`.** A new key is a new row, and a new row starts the pulse
54
+ * from the beginning; `` `${id}-${sequence}` `` is the usual shape.
55
+ *
56
+ * Under `prefers-reduced-motion` and on ePaper the ring does not
57
+ * pulse; it is simply there.
58
+ */
59
+ isHighlighted?: boolean;
60
+ /**
61
+ * `true` draws the row receded — a sunken surface and a quieter
62
+ * label — for an entry that exists but is switched off. It stays
63
+ * fully operable and every colour on it still clears AA, which is
64
+ * why it is a surface and a text role rather than an opacity: a
65
+ * faded row is not a disabled one, and only a disabled control may
66
+ * drop below the contrast floor. Say WHY in a badge too ("off").
67
+ */
68
+ isMuted?: boolean;
25
69
  key: string;
26
70
  /**
27
71
  * Plain text, because it is what the row is called — the thing a
28
72
  * search matched and the text a clipped row's tooltip repeats.
29
73
  */
30
74
  label: string;
75
+ /**
76
+ * Makes the whole row one native `<button>` — "open this one" for a
77
+ * row whose primary action is the row itself. Hover tints the row
78
+ * and rings its edge, a press sinks it (with motion only when the
79
+ * user has not asked for less), and keyboard focus draws the focus
80
+ * ring inside the row's edge. `actions` stay separate targets
81
+ * above it.
82
+ *
83
+ * The button is drawn UNDER the row's text and the text lets the
84
+ * pointer through, so nothing interactive is ever nested inside a
85
+ * button: keep `badges`, `content` and `aside` free of controls.
86
+ */
87
+ onSelect?: () => void;
88
+ /**
89
+ * The whole-row button's accessible name. Defaults to `label`.
90
+ * Name the verb when there is one — `"Edit Water the ferns"` —
91
+ * the same rule as a repeated action.
92
+ */
93
+ selectLabel?: string;
31
94
  };
32
95
  export type GroupedListGroup = {
33
96
  /** Plain text. It is the accessible name of the group's list, so `getByRole("list", { name })` finds it. */
@@ -121,6 +184,20 @@ export type GroupedListProps = {
121
184
  * know that surface, so `isHeadingSticky` reads it from a custom
122
185
  * property the host can set, falling back to `surface-base`.
123
186
  *
187
+ * ### A row can be the target
188
+ *
189
+ * tally-marks' card list opens a card's editor from anywhere on the
190
+ * card, and the owner asked for that to feel like an app: hover,
191
+ * press, focus, touch. `onSelect` is that, as one real `<button>`
192
+ * stretched under the row's text — not a click handler on an
193
+ * `<li>`, which no keyboard reaches and no screen reader announces.
194
+ * The text lets the pointer through to it; the actions sit above it
195
+ * and keep their own targets, so "Delete" never opens the editor.
196
+ *
197
+ * A row can also be muted (switched off but still there) and
198
+ * highlighted (just touched by a scan or a save) — the two other
199
+ * per-row states the same list drew by hand.
200
+ *
124
201
  * ### Not collapsible
125
202
  *
126
203
  * A group you can close is `Accordion`. This is for a list that is
@@ -1 +1 @@
1
- {"version":3,"file":"GroupedList.d.ts","sourceRoot":"","sources":["../../src/GroupedList/GroupedList.tsx"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,OAAO,CAAA;AAMtC,MAAM,MAAM,uBAAuB,GAAG,CAAC,GAAG,CAAC,GAAG,CAAC,GAAG,CAAC,GAAG,CAAC,CAAA;AAEvD,MAAM,MAAM,eAAe,GAAG,IAAI,GAAG,IAAI,CAAA;AAEzC,MAAM,MAAM,eAAe,GAAG;IAC5B;;;;;;;;OAQG;IACH,OAAO,CAAC,EAAE,SAAS,CAAA;IACnB,kGAAkG;IAClG,MAAM,CAAC,EAAE,SAAS,CAAA;IAClB;;;;OAIG;IACH,OAAO,CAAC,EAAE,SAAS,CAAA;IACnB,wFAAwF;IACxF,WAAW,CAAC,EAAE,MAAM,CAAA;IACpB,GAAG,EAAE,MAAM,CAAA;IACX;;;OAGG;IACH,KAAK,EAAE,MAAM,CAAA;CACd,CAAA;AAED,MAAM,MAAM,gBAAgB,GAAG;IAC7B,4GAA4G;IAC5G,OAAO,EAAE,MAAM,CAAA;IACf,KAAK,EAAE,SAAS,eAAe,EAAE,CAAA;IACjC,GAAG,EAAE,MAAM,CAAA;IACX;;;;OAIG;IACH,OAAO,CAAC,EAAE,SAAS,CAAA;CACpB,CAAA;AAED,MAAM,MAAM,gBAAgB,GAAG;IAC7B,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB;;;;OAIG;IACH,UAAU,CAAC,EAAE,SAAS,CAAA;IACtB,MAAM,EAAE,SAAS,gBAAgB,EAAE,CAAA;IACnC;;;;OAIG;IACH,YAAY,CAAC,EAAE,uBAAuB,CAAA;IACtC;;;;;;;;;;;;;;;;OAgBG;IACH,eAAe,CAAC,EAAE,OAAO,CAAA;IACzB;;;;;OAKG;IACH,eAAe,CAAC,EAAE,OAAO,CAAA;IACzB,8DAA8D;IAC9D,IAAI,CAAC,EAAE,eAAe,CAAA;CACvB,CAAA;AAkBD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAyCG;AACH,eAAO,MAAM,WAAW,GAAI,0FAQzB,gBAAgB,KAAG,SAuErB,CAAA"}
1
+ {"version":3,"file":"GroupedList.d.ts","sourceRoot":"","sources":["../../src/GroupedList/GroupedList.tsx"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,qBAAqB,CAAA;AACrD,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,OAAO,CAAA;AAMtC,MAAM,MAAM,uBAAuB,GAAG,CAAC,GAAG,CAAC,GAAG,CAAC,GAAG,CAAC,GAAG,CAAC,CAAA;AAEvD,MAAM,MAAM,eAAe,GAAG,IAAI,GAAG,IAAI,CAAA;AAEzC,MAAM,MAAM,eAAe,GAAG;IAC5B;;;;;;;;;;;;OAYG;IACH,OAAO,CAAC,EAAE,SAAS,CAAA;IACnB;;;;;;;OAOG;IACH,KAAK,CAAC,EAAE,SAAS,CAAA;IACjB,kGAAkG;IAClG,MAAM,CAAC,EAAE,SAAS,CAAA;IAClB;;;;;OAKG;IACH,OAAO,CAAC,EAAE,SAAS,CAAA;IACnB,wFAAwF;IACxF,WAAW,CAAC,EAAE,MAAM,CAAA;IACpB;;;;OAIG;IACH,eAAe,CAAC,EAAE,UAAU,CAAA;IAC5B;;;;;;;;;;;;OAYG;IACH,aAAa,CAAC,EAAE,OAAO,CAAA;IACvB;;;;;;;OAOG;IACH,OAAO,CAAC,EAAE,OAAO,CAAA;IACjB,GAAG,EAAE,MAAM,CAAA;IACX;;;OAGG;IACH,KAAK,EAAE,MAAM,CAAA;IACb;;;;;;;;;;;OAWG;IACH,QAAQ,CAAC,EAAE,MAAM,IAAI,CAAA;IACrB;;;;OAIG;IACH,WAAW,CAAC,EAAE,MAAM,CAAA;CACrB,CAAA;AAED,MAAM,MAAM,gBAAgB,GAAG;IAC7B,4GAA4G;IAC5G,OAAO,EAAE,MAAM,CAAA;IACf,KAAK,EAAE,SAAS,eAAe,EAAE,CAAA;IACjC,GAAG,EAAE,MAAM,CAAA;IACX;;;;OAIG;IACH,OAAO,CAAC,EAAE,SAAS,CAAA;CACpB,CAAA;AAED,MAAM,MAAM,gBAAgB,GAAG;IAC7B,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB;;;;OAIG;IACH,UAAU,CAAC,EAAE,SAAS,CAAA;IACtB,MAAM,EAAE,SAAS,gBAAgB,EAAE,CAAA;IACnC;;;;OAIG;IACH,YAAY,CAAC,EAAE,uBAAuB,CAAA;IACtC;;;;;;;;;;;;;;;;OAgBG;IACH,eAAe,CAAC,EAAE,OAAO,CAAA;IACzB;;;;;OAKG;IACH,eAAe,CAAC,EAAE,OAAO,CAAA;IACzB,8DAA8D;IAC9D,IAAI,CAAC,EAAE,eAAe,CAAA;CACvB,CAAA;AAkBD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAuDG;AACH,eAAO,MAAM,WAAW,GAAI,0FAQzB,gBAAgB,KAAG,SAuErB,CAAA"}
@@ -52,6 +52,20 @@ const HEADING_TAG = {
52
52
  * know that surface, so `isHeadingSticky` reads it from a custom
53
53
  * property the host can set, falling back to `surface-base`.
54
54
  *
55
+ * ### A row can be the target
56
+ *
57
+ * tally-marks' card list opens a card's editor from anywhere on the
58
+ * card, and the owner asked for that to feel like an app: hover,
59
+ * press, focus, touch. `onSelect` is that, as one real `<button>`
60
+ * stretched under the row's text — not a click handler on an
61
+ * `<li>`, which no keyboard reaches and no screen reader announces.
62
+ * The text lets the pointer through to it; the actions sit above it
63
+ * and keep their own targets, so "Delete" never opens the editor.
64
+ *
65
+ * A row can also be muted (switched off but still there) and
66
+ * highlighted (just touched by a scan or a save) — the two other
67
+ * per-row states the same list drew by hand.
68
+ *
55
69
  * ### Not collapsible
56
70
  *
57
71
  * A group you can close is `Accordion`. This is for a list that is
@@ -1 +1 @@
1
- {"version":3,"file":"GroupedList.js","sourceRoot":"","sources":["../../src/GroupedList/GroupedList.tsx"],"names":[],"mappings":";AAAA,OAAO,EAAE,WAAW,EAAE,MAAM,oBAAoB,CAAA;AAGhD,OAAO,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAA;AAC/C,OAAO,EAAE,cAAc,EAAE,MAAM,qBAAsB,CAAA;AACrD,OAAO,EAAE,oBAAoB,EAAE,MAAM,+BAA+B,CAAA;AA4FpE;;;;GAIG;AACH,MAAM,WAAW,GAGb;IACF,CAAC,EAAE,IAAI;IACP,CAAC,EAAE,IAAI;IACP,CAAC,EAAE,IAAI;IACP,CAAC,EAAE,IAAI;IACP,CAAC,EAAE,IAAI;CACR,CAAA;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAyCG;AACH,MAAM,CAAC,MAAM,WAAW,GAAG,CAAC,EAC1B,SAAS,EACT,UAAU,EACV,MAAM,EACN,YAAY,GAAG,CAAC,EAChB,eAAe,GAAG,KAAK,EACvB,eAAe,GAAG,IAAI,EACtB,IAAI,GAAG,IAAI,GACM,EAAa,EAAE;IAChC,MAAM,MAAM,GAAG,WAAW,EAAE,CAAA;IAE5B,MAAM,WAAW,GAAG,MAAM,CAAC,MAAM,CAC/B,CAAC,KAAK,EAAE,EAAE,CAAC,KAAK,CAAC,KAAK,CAAC,MAAM,GAAG,CAAC,CAClC,CAAA;IAED,MAAM,OAAO,GAAG,WAAW,CAAC,YAAY,CAAC,CAAA;IAEzC,OAAO,CACL,cACE,SAAS,EAAE,WAAW,CACpB,eAAe,EACf,IAAI,KAAK,IAAI,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,OAAO,EACjC,SAAS,CACV,YAEA,WAAW,CAAC,MAAM,KAAK,CAAC;YACvB,CAAC,CAAC,CAAC,UAAU,IAAI,IAAI,CAAC;YACtB,CAAC,CAAC,WAAW,CAAC,GAAG,CAAC,CAAC,KAAK,EAAE,KAAK,EAAE,EAAE;gBAC/B,MAAM,SAAS,GAAG,GAAG,MAAM,YAAY,KAAK,EAAE,CAAA;gBAE9C,OAAO,CACL,eACE,SAAS,EAAE,WAAW,CACpB,eAAe,EACf,eAAe,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,OAAO,CACpC,aAGD,eACE,SAAS,EAAE,WAAW,CACpB,iDAAiD,EACjD,eAAe,IAAI,oBAAoB,CACxC,aAED,KAAC,OAAO,IACN,SAAS,EAAE,WAAW,CACpB,8DAA8D,EAC9D,IAAI,KAAK,IAAI,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,SAAS,CACtC,EACD,EAAE,EAAE,SAAS,YAEZ,KAAK,CAAC,OAAO,GACN,EAET,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,CACf,eAAM,SAAS,EAAC,sDAAsD,YACnE,KAAK,CAAC,OAAO,GACT,CACR,CAAC,CAAC,CAAC,IAAI,IACJ,EAEN,gCACmB,SAAS,EAC1B,SAAS,EAAC,wIAAwI,YAEjJ,KAAK,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,CACzB,KAAC,cAAc,IACb,eAAe,EAAE,eAAe,EAChC,IAAI,EAAE,IAAI,EAEV,IAAI,EAAE,IAAI,IADL,IAAI,CAAC,GAAG,CAEb,CACH,CAAC,GACC,KArCA,KAAK,CAAC,GAAG,CAsCV,CACP,CAAA;YACH,CAAC,CAAC,GACF,CACP,CAAA;AACH,CAAC,CAAA"}
1
+ {"version":3,"file":"GroupedList.js","sourceRoot":"","sources":["../../src/GroupedList/GroupedList.tsx"],"names":[],"mappings":";AAAA,OAAO,EAAE,WAAW,EAAE,MAAM,oBAAoB,CAAA;AAIhD,OAAO,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAA;AAC/C,OAAO,EAAE,cAAc,EAAE,MAAM,qBAAsB,CAAA;AACrD,OAAO,EAAE,oBAAoB,EAAE,MAAM,+BAA+B,CAAA;AA0JpE;;;;GAIG;AACH,MAAM,WAAW,GAGb;IACF,CAAC,EAAE,IAAI;IACP,CAAC,EAAE,IAAI;IACP,CAAC,EAAE,IAAI;IACP,CAAC,EAAE,IAAI;IACP,CAAC,EAAE,IAAI;CACR,CAAA;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAuDG;AACH,MAAM,CAAC,MAAM,WAAW,GAAG,CAAC,EAC1B,SAAS,EACT,UAAU,EACV,MAAM,EACN,YAAY,GAAG,CAAC,EAChB,eAAe,GAAG,KAAK,EACvB,eAAe,GAAG,IAAI,EACtB,IAAI,GAAG,IAAI,GACM,EAAa,EAAE;IAChC,MAAM,MAAM,GAAG,WAAW,EAAE,CAAA;IAE5B,MAAM,WAAW,GAAG,MAAM,CAAC,MAAM,CAC/B,CAAC,KAAK,EAAE,EAAE,CAAC,KAAK,CAAC,KAAK,CAAC,MAAM,GAAG,CAAC,CAClC,CAAA;IAED,MAAM,OAAO,GAAG,WAAW,CAAC,YAAY,CAAC,CAAA;IAEzC,OAAO,CACL,cACE,SAAS,EAAE,WAAW,CACpB,eAAe,EACf,IAAI,KAAK,IAAI,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,OAAO,EACjC,SAAS,CACV,YAEA,WAAW,CAAC,MAAM,KAAK,CAAC;YACvB,CAAC,CAAC,CAAC,UAAU,IAAI,IAAI,CAAC;YACtB,CAAC,CAAC,WAAW,CAAC,GAAG,CAAC,CAAC,KAAK,EAAE,KAAK,EAAE,EAAE;gBAC/B,MAAM,SAAS,GAAG,GAAG,MAAM,YAAY,KAAK,EAAE,CAAA;gBAE9C,OAAO,CACL,eACE,SAAS,EAAE,WAAW,CACpB,eAAe,EACf,eAAe,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,OAAO,CACpC,aAGD,eACE,SAAS,EAAE,WAAW,CACpB,iDAAiD,EACjD,eAAe,IAAI,oBAAoB,CACxC,aAED,KAAC,OAAO,IACN,SAAS,EAAE,WAAW,CACpB,8DAA8D,EAC9D,IAAI,KAAK,IAAI,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,SAAS,CACtC,EACD,EAAE,EAAE,SAAS,YAEZ,KAAK,CAAC,OAAO,GACN,EAET,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,CACf,eAAM,SAAS,EAAC,sDAAsD,YACnE,KAAK,CAAC,OAAO,GACT,CACR,CAAC,CAAC,CAAC,IAAI,IACJ,EAEN,gCACmB,SAAS,EAC1B,SAAS,EAAC,wIAAwI,YAEjJ,KAAK,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,CACzB,KAAC,cAAc,IACb,eAAe,EAAE,eAAe,EAChC,IAAI,EAAE,IAAI,EAEV,IAAI,EAAE,IAAI,IADL,IAAI,CAAC,GAAG,CAEb,CACH,CAAC,GACC,KArCA,KAAK,CAAC,GAAG,CAsCV,CACP,CAAA;YACH,CAAC,CAAC,GACF,CACP,CAAA;AACH,CAAC,CAAA"}
@@ -14,16 +14,27 @@ export type GroupedListRowProps = {
14
14
  * `useIsTextClipped` is `Badge`'s: an ellipsis leaves no trace in the
15
15
  * DOM, so the only way to know a label is cut off is to measure it.
16
16
  * A `title` on every row would put a tooltip on forty short names to
17
- * help the one long one.
17
+ * help the one long one. In a selectable row the text lets the
18
+ * pointer through, so the clipped label's tooltip moves to the button
19
+ * the pointer is actually over.
18
20
  *
19
21
  * ### The actions wrap; they never squeeze the text
20
22
  *
21
23
  * The row is a wrapping flex line. The text column asks for 12rem
22
- * and grows into the rest, so in a wide list the actions sit at the
23
- * inline end of the same line, and in a list narrower than the text
24
- * plus the actions they drop under the text instead of crushing the
25
- * label to an ellipsis and nothing. No container query: the wrap is
26
- * decided by the content itself.
24
+ * and grows into the rest, and the aside asks for at least 11rem, so
25
+ * in a wide list the aside and the actions sit at the inline end of
26
+ * the same line, and in a list narrower than all of them they drop
27
+ * under the text instead of crushing the label to an ellipsis and
28
+ * nothing. No container query: the wrap is decided by the content
29
+ * itself.
30
+ *
31
+ * ### The layers of a selectable row
32
+ *
33
+ * In tree order, and every one of them positioned, so they paint in
34
+ * that order with no `z-index` to keep in step: the button (bottom),
35
+ * the text and the aside (pointer passes through), the actions
36
+ * (their own targets), and the highlight ring (top, pointer passes
37
+ * through) — the ring is last so a hovered row cannot paint over it.
27
38
  */
28
39
  export declare const GroupedListRow: ({ isTextTruncated, item, size, }: GroupedListRowProps) => ReactNode;
29
40
  //# sourceMappingURL=GroupedListRow.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"GroupedListRow.d.ts","sourceRoot":"","sources":["../../src/GroupedList/GroupedListRow.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,OAAO,CAAA;AAItC,OAAO,KAAK,EACV,eAAe,EACf,eAAe,EAChB,MAAM,mBAAmB,CAAA;AAE1B,MAAM,MAAM,mBAAmB,GAAG;IAChC,eAAe,EAAE,OAAO,CAAA;IACxB,IAAI,EAAE,eAAe,CAAA;IACrB,IAAI,EAAE,eAAe,CAAA;CACtB,CAAA;AAOD;;;;;;;;;;;;;;;;;;;GAmBG;AACH,eAAO,MAAM,cAAc,GAAI,kCAI5B,mBAAmB,KAAG,SAwExB,CAAA"}
1
+ {"version":3,"file":"GroupedListRow.d.ts","sourceRoot":"","sources":["../../src/GroupedList/GroupedListRow.tsx"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,OAAO,CAAA;AAItC,OAAO,KAAK,EACV,eAAe,EACf,eAAe,EAChB,MAAM,mBAAmB,CAAA;AAE1B,MAAM,MAAM,mBAAmB,GAAG;IAChC,eAAe,EAAE,OAAO,CAAA;IACxB,IAAI,EAAE,eAAe,CAAA;IACrB,IAAI,EAAE,eAAe,CAAA;CACtB,CAAA;AAmDD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA8BG;AACH,eAAO,MAAM,cAAc,GAAI,kCAI5B,mBAAmB,KAAG,SA6IxB,CAAA"}
@@ -5,6 +5,45 @@ const ROW_SIZE_CLASS = {
5
5
  md: "px-4 py-3",
6
6
  sm: "px-3 py-2",
7
7
  };
8
+ /**
9
+ * Written out per intent, because Tailwind scans source text for
10
+ * whole class strings: `inset-ring-intent-${intent}-content`
11
+ * generates nothing. `-content` rather than `-border`, because a
12
+ * ring that announces "this just changed" has to clear the 3:1
13
+ * non-text contrast floor against the row, and a border role is
14
+ * tuned to sit quietly under it.
15
+ */
16
+ const HIGHLIGHT_RING_CLASS = {
17
+ accent: "inset-ring-intent-accent-content",
18
+ danger: "inset-ring-intent-danger-content",
19
+ info: "inset-ring-intent-info-content",
20
+ neutral: "inset-ring-intent-neutral-content",
21
+ success: "inset-ring-intent-success-content",
22
+ warning: "inset-ring-intent-warning-content",
23
+ };
24
+ /**
25
+ * The whole-row button. It is the bottom layer of the row: the text
26
+ * columns above it are `pointer-events-none`, so a click on the label
27
+ * lands here, and the actions above it keep their own targets.
28
+ *
29
+ * Every state is a SURFACE plus an edge, never colour alone. Hover is
30
+ * scoped by Tailwind's own `hover:` to devices that hover, so a tap on
31
+ * a phone does not leave a row lit. The focus ring is drawn INSIDE the
32
+ * row's edge (a negative offset) because the group's list clips its
33
+ * rounded corners, and an outside ring would be cut off on the first
34
+ * and last row. It sits one ring-width in from the edge, so it never
35
+ * shares pixels with the hover edge or the highlight ring, which both
36
+ * hug the edge — a highlighted row that has focus shows both.
37
+ */
38
+ const SELECT_BUTTON_CLASS = "absolute inset-0 cursor-pointer appearance-none border-0 bg-transparent p-0 transition-[background-color,box-shadow] duration-(--duration-fast) ease-standard hover:bg-intent-neutral-surface-hover hover:inset-ring hover:inset-ring-intent-accent-border active:bg-intent-neutral-surface-hover focus-visible:outline-solid focus-visible:outline-(length:--focus-ring-width) focus-visible:outline-offset-[calc(var(--focus-ring-width)*-2)] focus-visible:outline-focus-ring";
39
+ /**
40
+ * The press. The row sinks a little while its button is held — the
41
+ * motion the owner asked for on tally-marks' cards — and only when the
42
+ * user has not asked the OS for less; the surface change on the button
43
+ * carries the press without it. Keyed on the row's OWN button, so
44
+ * holding an action does not press the row.
45
+ */
46
+ const SELECTABLE_ROW_CLASS = "relative transition-transform duration-(--duration-fast) ease-standard motion-safe:has-[>button:active]:scale-[0.99]";
8
47
  /**
9
48
  * Its own file because each row measures its own text, and a hook
10
49
  * cannot run in a loop.
@@ -14,16 +53,27 @@ const ROW_SIZE_CLASS = {
14
53
  * `useIsTextClipped` is `Badge`'s: an ellipsis leaves no trace in the
15
54
  * DOM, so the only way to know a label is cut off is to measure it.
16
55
  * A `title` on every row would put a tooltip on forty short names to
17
- * help the one long one.
56
+ * help the one long one. In a selectable row the text lets the
57
+ * pointer through, so the clipped label's tooltip moves to the button
58
+ * the pointer is actually over.
18
59
  *
19
60
  * ### The actions wrap; they never squeeze the text
20
61
  *
21
62
  * The row is a wrapping flex line. The text column asks for 12rem
22
- * and grows into the rest, so in a wide list the actions sit at the
23
- * inline end of the same line, and in a list narrower than the text
24
- * plus the actions they drop under the text instead of crushing the
25
- * label to an ellipsis and nothing. No container query: the wrap is
26
- * decided by the content itself.
63
+ * and grows into the rest, and the aside asks for at least 11rem, so
64
+ * in a wide list the aside and the actions sit at the inline end of
65
+ * the same line, and in a list narrower than all of them they drop
66
+ * under the text instead of crushing the label to an ellipsis and
67
+ * nothing. No container query: the wrap is decided by the content
68
+ * itself.
69
+ *
70
+ * ### The layers of a selectable row
71
+ *
72
+ * In tree order, and every one of them positioned, so they paint in
73
+ * that order with no `z-index` to keep in step: the button (bottom),
74
+ * the text and the aside (pointer passes through), the actions
75
+ * (their own targets), and the highlight ring (top, pointer passes
76
+ * through) — the ring is last so a hovered row cannot paint over it.
27
77
  */
28
78
  export const GroupedListRow = ({ isTextTruncated, item, size, }) => {
29
79
  const [labelRef, clippedLabel] = useIsTextClipped();
@@ -31,8 +81,25 @@ export const GroupedListRow = ({ isTextTruncated, item, size, }) => {
31
81
  const overflowClass = isTextTruncated
32
82
  ? "truncate"
33
83
  : "wrap-anywhere";
34
- return (_jsxs("li", { className: toClassName("flex flex-wrap items-center gap-x-3 gap-y-2", ROW_SIZE_CLASS[size]), children: [_jsxs("div", { className: "flex min-w-0 flex-[1_1_12rem] flex-col gap-0.5", children: [_jsxs("div", { className: "flex min-w-0 flex-wrap items-center gap-x-2 gap-y-1", children: [_jsx("span", { className: toClassName("min-w-0 max-w-full font-medium text-content-primary", size === "sm" ? "text-sm" : "text-md", overflowClass), ref: labelRef, title: isTextTruncated ? clippedLabel : undefined, children: item.label }), item.badges ? (_jsx("span", { className: "flex min-w-0 flex-wrap items-center gap-1", children: item.badges })) : null] }), item.description ? (_jsx("span", { className: toClassName("block min-w-0 text-content-secondary", size === "sm" ? "text-xs" : "text-sm", overflowClass), ref: descriptionRef, title: isTextTruncated
84
+ const { highlightIntent = "accent", isHighlighted = false, isMuted = false, onSelect, } = item;
85
+ const isSelectable = onSelect !== undefined;
86
+ // A layer that sits above the button and must let the pointer
87
+ // through to it. Without a button it is an ordinary column.
88
+ const passThroughClass = isSelectable
89
+ ? "pointer-events-none relative"
90
+ : null;
91
+ return (_jsxs("li", { className: toClassName("flex flex-wrap items-center gap-x-3 gap-y-2", ROW_SIZE_CLASS[size], isSelectable && SELECTABLE_ROW_CLASS, isHighlighted && "relative", isMuted && "bg-surface-sunken"), children: [isSelectable ? (_jsx("button", { "aria-label": item.selectLabel ?? item.label, className: SELECT_BUTTON_CLASS, onClick: onSelect, title: isTextTruncated ? clippedLabel : undefined, type: "button" })) : null, _jsxs("div", { className: toClassName("flex min-w-0 flex-[1_1_12rem] flex-col gap-0.5", passThroughClass), children: [_jsxs("div", { className: "flex min-w-0 flex-wrap items-center gap-x-2 gap-y-1", children: [_jsx("span", { className: toClassName("min-w-0 max-w-full font-medium", isMuted
92
+ ? "text-content-secondary"
93
+ : "text-content-primary", size === "sm" ? "text-sm" : "text-md", overflowClass), ref: labelRef, title: isTextTruncated && !isSelectable
94
+ ? clippedLabel
95
+ : undefined, children: item.label }), item.badges ? (_jsx("span", { className: "flex min-w-0 flex-wrap items-center gap-1", children: item.badges })) : null] }), item.description ? (_jsx("span", { className: toClassName("block min-w-0 text-content-secondary", size === "sm" ? "text-xs" : "text-sm", overflowClass), ref: descriptionRef, title: isTextTruncated
35
96
  ? clippedDescription
36
- : undefined, children: item.description })) : null, item.content ? (_jsx("div", { className: "mt-1 min-w-0 wrap-anywhere text-content-secondary text-sm", children: item.content })) : null] }), item.actions ? (_jsx("div", { className: "flex shrink-0 items-center gap-2", children: item.actions })) : null] }));
97
+ : undefined, children: item.description })) : null, item.content ? (_jsx("div", { className: "mt-1 min-w-0 wrap-anywhere text-content-secondary text-sm", children: item.content })) : null] }), item.aside ? (_jsx("div", { className: toClassName(
98
+ // An 11rem floor, so a short figure does not squeeze in
99
+ // beside a phone-width label: it sits beside the text only
100
+ // when there is room for a real column, and drops under
101
+ // it otherwise. `min(…, 100%)` keeps the floor from
102
+ // overflowing a list narrower than the floor itself.
103
+ "flex min-w-[min(11rem,100%)] max-w-full flex-[0_1_auto] wrap-anywhere", passThroughClass), children: item.aside })) : null, item.actions ? (_jsx("div", { className: toClassName("flex shrink-0 items-center gap-2", isSelectable && "relative"), children: item.actions })) : null, isHighlighted ? (_jsx("span", { "aria-hidden": true, className: toClassName("charcuterie-highlight pointer-events-none absolute inset-0 inset-ring-2", HIGHLIGHT_RING_CLASS[highlightIntent]) })) : null] }));
37
104
  };
38
105
  //# sourceMappingURL=GroupedListRow.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"GroupedListRow.js","sourceRoot":"","sources":["../../src/GroupedList/GroupedListRow.tsx"],"names":[],"mappings":";AAEA,OAAO,EAAE,gBAAgB,EAAE,MAAM,8BAA8B,CAAA;AAC/D,OAAO,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAA;AAY/C,MAAM,cAAc,GAAoC;IACtD,EAAE,EAAE,WAAW;IACf,EAAE,EAAE,WAAW;CAChB,CAAA;AAED;;;;;;;;;;;;;;;;;;;GAmBG;AACH,MAAM,CAAC,MAAM,cAAc,GAAG,CAAC,EAC7B,eAAe,EACf,IAAI,EACJ,IAAI,GACgB,EAAa,EAAE;IACnC,MAAM,CAAC,QAAQ,EAAE,YAAY,CAAC,GAAG,gBAAgB,EAAE,CAAA;IAEnD,MAAM,CAAC,cAAc,EAAE,kBAAkB,CAAC,GACxC,gBAAgB,EAAE,CAAA;IAEpB,MAAM,aAAa,GAAG,eAAe;QACnC,CAAC,CAAC,UAAU;QACZ,CAAC,CAAC,eAAe,CAAA;IAEnB,OAAO,CACL,cACE,SAAS,EAAE,WAAW,CACpB,6CAA6C,EAC7C,cAAc,CAAC,IAAI,CAAC,CACrB,aAED,eAAK,SAAS,EAAC,gDAAgD,aAC7D,eAAK,SAAS,EAAC,qDAAqD,aAClE,eACE,SAAS,EAAE,WAAW,CACpB,qDAAqD,EACrD,IAAI,KAAK,IAAI,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,SAAS,EACrC,aAAa,CACd,EACD,GAAG,EAAE,QAAQ,EACb,KAAK,EACH,eAAe,CAAC,CAAC,CAAC,YAAY,CAAC,CAAC,CAAC,SAAS,YAG3C,IAAI,CAAC,KAAK,GACN,EAEN,IAAI,CAAC,MAAM,CAAC,CAAC,CAAC,CACb,eAAM,SAAS,EAAC,2CAA2C,YACxD,IAAI,CAAC,MAAM,GACP,CACR,CAAC,CAAC,CAAC,IAAI,IACJ,EAEL,IAAI,CAAC,WAAW,CAAC,CAAC,CAAC,CAClB,eACE,SAAS,EAAE,WAAW,CACpB,sCAAsC,EACtC,IAAI,KAAK,IAAI,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,SAAS,EACrC,aAAa,CACd,EACD,GAAG,EAAE,cAAc,EACnB,KAAK,EACH,eAAe;4BACb,CAAC,CAAC,kBAAkB;4BACpB,CAAC,CAAC,SAAS,YAGd,IAAI,CAAC,WAAW,GACZ,CACR,CAAC,CAAC,CAAC,IAAI,EAEP,IAAI,CAAC,OAAO,CAAC,CAAC,CAAC,CACd,cAAK,SAAS,EAAC,2DAA2D,YACvE,IAAI,CAAC,OAAO,GACT,CACP,CAAC,CAAC,CAAC,IAAI,IACJ,EAEL,IAAI,CAAC,OAAO,CAAC,CAAC,CAAC,CACd,cAAK,SAAS,EAAC,kCAAkC,YAC9C,IAAI,CAAC,OAAO,GACT,CACP,CAAC,CAAC,CAAC,IAAI,IACL,CACN,CAAA;AACH,CAAC,CAAA"}
1
+ {"version":3,"file":"GroupedListRow.js","sourceRoot":"","sources":["../../src/GroupedList/GroupedListRow.tsx"],"names":[],"mappings":"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"}
package/dist/styles.css CHANGED
@@ -1,6 +1,7 @@
1
- /* `@charcuterie/ui/styles.css` — the four looping affordances and
2
- * the token-tinted scrollbar: the visuals a Tailwind utility cannot
3
- * express against our tokens, and nothing else.
1
+ /* `@charcuterie/ui/styles.css` — the four looping affordances, the
2
+ * one-shot highlight pulse and the token-tinted scrollbar: the
3
+ * visuals a Tailwind utility cannot express against our tokens, and
4
+ * nothing else.
4
5
  *
5
6
  * Every other visual in this package is a Tailwind utility reading
6
7
  * a token, which is what makes a scheme/density/variant flip a
@@ -74,6 +75,36 @@
74
75
  }
75
76
  }
76
77
 
78
+ /* The highlight pulse: "this row just changed". Two blinks, then
79
+ * steady. Only OPACITY moves, and the last keyframe is fully shown,
80
+ * so the element's own resting style IS the static fallback — under
81
+ * reduced motion the animation is switched off below and the ring is
82
+ * simply there, and on ePaper a 0ms run ends on the same frame. That
83
+ * is the "every moving affordance owes a static fallback" rule met
84
+ * by construction rather than by a second rule. */
85
+ @keyframes charcuterie-highlight {
86
+ 0% {
87
+ opacity: 0;
88
+ }
89
+
90
+ 25% {
91
+ opacity: 1;
92
+ }
93
+
94
+ 55% {
95
+ opacity: 0.3;
96
+ }
97
+
98
+ 100% {
99
+ opacity: 1;
100
+ }
101
+ }
102
+
103
+ .charcuterie-highlight {
104
+ animation: charcuterie-highlight var(--duration-loop-slow)
105
+ var(--easing-standard) 2;
106
+ }
107
+
77
108
  .charcuterie-spin {
78
109
  animation: charcuterie-spin var(--duration-loop-fast)
79
110
  linear infinite;
@@ -105,7 +136,8 @@
105
136
  .charcuterie-spin,
106
137
  .charcuterie-shimmer,
107
138
  .charcuterie-sweep,
108
- .charcuterie-pulse {
139
+ .charcuterie-pulse,
140
+ .charcuterie-highlight {
109
141
  animation: none;
110
142
  }
111
143
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@charcuterie/ui",
3
- "version": "4.24.0",
3
+ "version": "4.25.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",
@@ -7,7 +7,9 @@ import * as GroupedListStories from "./GroupedList.stories";
7
7
  # GroupedList
8
8
 
9
9
  An always-open list whose rows sit under group headings. Each row is a label, an optional
10
- muted description, optional badges, optional richer content, and optional trailing actions.
10
+ muted description, optional badges, optional richer content, an optional figure beside the
11
+ text, and optional trailing actions. A row can also be the target itself, muted, or
12
+ highlighted.
11
13
 
12
14
  Three apps drew this shape by hand: castkit's page picker (pages under the screen that shows
13
15
  them), tally-marks' cards (cards under the team that earns them) and whereabouts' nested
@@ -84,6 +86,45 @@ that sets them through a `style` object.
84
86
 
85
87
  <Canvas of={GroupedListStories.StickyHeadings} />
86
88
 
89
+ ## A row can be the target
90
+
91
+ Give an item `onSelect` and the whole row becomes one native `<button>` — "open this card"
92
+ for a list whose primary action is the row itself. `selectLabel` names it (it defaults to
93
+ `label`); name the verb, as with any repeated action: `selectLabel="Edit Water the ferns"`.
94
+
95
+ - **Every state is drawn.** Hover tints the row and rings its edge (only on devices that
96
+ hover, so a tap does not leave a row lit). A press sinks the row slightly, with motion only
97
+ when the user has not asked the OS for less. Keyboard focus draws the focus ring inside the
98
+ row's edge, where the list's rounded corners cannot clip it, and one ring-width in from it,
99
+ so a highlighted row that has focus shows both rings.
100
+ - **The actions stay their own targets.** The button is drawn under the row's text and the
101
+ text lets the pointer through to it; `actions` sit above it. Pressing "Delete" never opens
102
+ the row, and Tab reaches the row, then its actions, then the next row.
103
+ - **Nothing is nested in the button.** It is empty and named by `aria-label`, so `badges`,
104
+ `content` and `aside` must not hold controls — a control belongs in `actions`.
105
+
106
+ `aside` is a figure that belongs **beside** the text — a payout, a total. It asks for at
107
+ least 11rem next to the text column's 12rem, so it sits between the text and the actions in a
108
+ wide list and drops under the text in a narrow one.
109
+
110
+ <Canvas of={GroupedListStories.SelectableRows} />
111
+
112
+ ## Muted and highlighted rows
113
+
114
+ - `isMuted` recedes a row that exists but is switched off: a sunken surface and a quieter
115
+ label. It stays fully operable, and it is a surface and a text role rather than an opacity,
116
+ because a faded row would drop below the AA contrast floor and only a disabled control may.
117
+ Say why in a badge as well ("off").
118
+ - `isHighlighted` rings the row and pulses the ring twice — "this one just changed", after a
119
+ scan or a save. `highlightIntent` picks the colour (`accent` by default). The ring stays
120
+ while the flag does, so the app decides how long it lasts, usually with a timer. **To pulse
121
+ an already-highlighted row again, change its `key`**: a new key is a new row, and a new row
122
+ starts the pulse from the beginning. Under `prefers-reduced-motion` and on ePaper the ring
123
+ does not pulse; it is simply there. The pulse lives in `@charcuterie/ui/styles.css`, which
124
+ every app already imports.
125
+
126
+ <Canvas of={GroupedListStories.RowStates} />
127
+
87
128
  ## Name a repeated action after its row
88
129
 
89
130
  Ten rows of "Open" are ten buttons an agent cannot tell apart. Give each one a name that