@eifi1/ui-kit 0.51.3 → 0.51.4

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.
Files changed (96) hide show
  1. package/README.md +5 -5
  2. package/dist/components/combobox-core.js +4 -5
  3. package/dist/components/combobox-core.js.map +1 -1
  4. package/dist/components/currency-option-list.js +3 -3
  5. package/dist/components/currency-option-list.js.map +1 -1
  6. package/dist/components/data-table-fit.js +7 -5
  7. package/dist/components/data-table-fit.js.map +1 -1
  8. package/dist/components/data-table-head.js +5 -1
  9. package/dist/components/data-table-head.js.map +1 -1
  10. package/dist/components/data-table.d.ts +8 -0
  11. package/dist/components/data-table.js.map +1 -1
  12. package/dist/components/grouped-picker.js +5 -4
  13. package/dist/components/grouped-picker.js.map +1 -1
  14. package/dist/components/icon-picker.js +5 -4
  15. package/dist/components/icon-picker.js.map +1 -1
  16. package/dist/components/inline-entity-combobox.js +4 -4
  17. package/dist/components/inline-entity-combobox.js.map +1 -1
  18. package/dist/components/legal-labels.js +8 -2
  19. package/dist/components/legal-labels.js.map +1 -1
  20. package/dist/components/legal-page.d.ts +18 -1
  21. package/dist/components/legal-page.js +13 -0
  22. package/dist/components/legal-page.js.map +1 -1
  23. package/dist/components/legal.d.ts +40 -5
  24. package/dist/components/legal.js +29 -3
  25. package/dist/components/legal.js.map +1 -1
  26. package/dist/components/list-labels.js +3 -1
  27. package/dist/components/list-labels.js.map +1 -1
  28. package/dist/components/list.d.ts +18 -1
  29. package/dist/components/list.js +32 -3
  30. package/dist/components/list.js.map +1 -1
  31. package/dist/eslint/index.js +1 -1
  32. package/dist/feedback/feedback-table.d.ts +3 -1
  33. package/dist/feedback/feedback-table.js +18 -4
  34. package/dist/feedback/feedback-table.js.map +1 -1
  35. package/dist/i18n/german.js +11 -3
  36. package/dist/i18n/german.js.map +1 -1
  37. package/dist/i18n/locales/en.js +15 -3
  38. package/dist/i18n/locales/en.js.map +2 -2
  39. package/dist/i18n/locales/es.js +11 -3
  40. package/dist/i18n/locales/es.js.map +1 -1
  41. package/dist/i18n/locales/fr.js +11 -3
  42. package/dist/i18n/locales/fr.js.map +1 -1
  43. package/dist/i18n/locales/hu.js +11 -3
  44. package/dist/i18n/locales/hu.js.map +1 -1
  45. package/dist/i18n/locales/it.js +11 -3
  46. package/dist/i18n/locales/it.js.map +1 -1
  47. package/dist/i18n/locales/zh.js +11 -3
  48. package/dist/i18n/locales/zh.js.map +1 -1
  49. package/dist/i18n/review.js +2 -0
  50. package/dist/i18n/review.js.map +1 -1
  51. package/dist/index.d.ts +1 -1
  52. package/dist/index.js.map +1 -1
  53. package/dist/lib/text-match.d.ts +16 -0
  54. package/dist/lib/text-match.js +11 -1
  55. package/dist/lib/text-match.js.map +1 -1
  56. package/dist/settings/use-settings-focus.d.ts +7 -0
  57. package/dist/settings/use-settings-focus.js +2 -0
  58. package/dist/settings/use-settings-focus.js.map +1 -1
  59. package/dist/shell/dev-menu.d.ts +5 -0
  60. package/dist/shell/dev-menu.js +2 -1
  61. package/dist/shell/dev-menu.js.map +1 -1
  62. package/dist/shell/topbar-action-menu.d.ts +14 -1
  63. package/dist/shell/topbar-action-menu.js +85 -34
  64. package/dist/shell/topbar-action-menu.js.map +1 -1
  65. package/dist/shell.d.ts +1 -1
  66. package/dist/shell.js +2 -1
  67. package/dist/shell.js.map +1 -1
  68. package/package.json +1 -1
  69. package/src/components/combobox-core.tsx +5 -5
  70. package/src/components/currency-option-list.tsx +4 -5
  71. package/src/components/data-table-fit.ts +7 -5
  72. package/src/components/data-table-head.tsx +5 -1
  73. package/src/components/data-table.tsx +8 -0
  74. package/src/components/grouped-picker.tsx +15 -8
  75. package/src/components/icon-picker.tsx +6 -4
  76. package/src/components/inline-entity-combobox.tsx +5 -4
  77. package/src/components/legal-labels.ts +7 -1
  78. package/src/components/legal-page.tsx +39 -0
  79. package/src/components/legal.tsx +69 -5
  80. package/src/components/list-labels.ts +2 -0
  81. package/src/components/list.tsx +52 -2
  82. package/src/feedback/feedback-table.tsx +17 -1
  83. package/src/i18n/german.ts +9 -1
  84. package/src/i18n/locales/en.ts +4 -0
  85. package/src/i18n/locales/es.ts +9 -1
  86. package/src/i18n/locales/fr.ts +9 -1
  87. package/src/i18n/locales/hu.ts +9 -1
  88. package/src/i18n/locales/it.ts +9 -1
  89. package/src/i18n/locales/zh.ts +9 -1
  90. package/src/i18n/review.ts +2 -0
  91. package/src/index.ts +1 -0
  92. package/src/lib/text-match.ts +30 -0
  93. package/src/settings/use-settings-focus.ts +8 -0
  94. package/src/shell/dev-menu.tsx +6 -0
  95. package/src/shell/topbar-action-menu.tsx +122 -38
  96. package/src/shell.ts +1 -1
@@ -9,6 +9,12 @@ export interface ListLabels {
9
9
  unread: string;
10
10
  /** Read after an external row's title: the row leaves the app in a new tab. */
11
11
  opensInNewTab: string;
12
+ /** 0.51.4: the row under a {@link ListProps.limit}ed list that opens the rest, with
13
+ * the WHOLE list's count — "Show all (12)". Optional, as every key added to a
14
+ * namespace an app may annotate: left out, the provider's, then English. */
15
+ showAll?: (count: number) => string;
16
+ /** 0.51.4: the same row once the list is open, folding it back. */
17
+ showFewer?: string;
12
18
  }
13
19
  export { DEFAULT_LIST_LABELS };
14
20
  /**
@@ -29,6 +35,17 @@ export interface ListProps extends Omit<ComponentPropsWithoutRef<"ul">, "role">
29
35
  /** The `<ul>` (or `<ol>`). A React 19 ref prop since 0.34 (MOD-4, refactor audit
30
36
  * 2026-10); it was `forwardRef`. */
31
37
  ref?: Ref<HTMLUListElement>;
38
+ /**
39
+ * Show the first `limit` rows, then a "Show all (n)" row that opens the rest and turns
40
+ * into "Show fewer" (0.51.4, keksdose run 89: a long "Placed only roughly" list on its
41
+ * Matching report could only fold as a whole, so it opened as nothing or everything).
42
+ * A list of `limit` rows or fewer draws them all, with no such row. Rows are counted as
43
+ * `Children` counts them: one per `ListItem`, a fragment as one.
44
+ *
45
+ * The row is a button with `aria-expanded`, controlling the list; it sits in the list
46
+ * as `role="none"`, so a reader still hears "list, 5 items" for five rows.
47
+ */
48
+ limit?: number;
32
49
  }
33
50
  /**
34
51
  * The list of {@link ListItem}s.
@@ -37,7 +54,7 @@ export interface ListProps extends Omit<ComponentPropsWithoutRef<"ul">, "role">
37
54
  * bullets are styled away, and "list, 7 items" is exactly what a screen-reader user
38
55
  * wants to hear before the first row.
39
56
  */
40
- export declare function List({ ref, as, density, separator, className, children, ...rest }: ListProps): import("react").JSX.Element;
57
+ export declare function List({ ref, as, density, separator, limit, className, children, ...rest }: ListProps): import("react").JSX.Element;
41
58
  /** What {@link ListItemProps.renderLink} is handed. Spread it onto your router's link,
42
59
  * mapping `href` to what it calls it — `({ href, ...p }) => <Link to={href} {...p} />`. */
43
60
  export interface ListItemLinkProps {
@@ -1,6 +1,6 @@
1
1
  "use client";
2
2
  import { Fragment, jsx, jsxs } from "react/jsx-runtime";
3
- import { createContext, useContext, useId, useState } from "react";
3
+ import { Children, createContext, useContext, useId, useState } from "react";
4
4
  import { ChevronDown, ExternalLink } from "lucide-react";
5
5
  import { cn } from "../lib/cn.js";
6
6
  import { FOCUS_RING } from "./focus-ring.js";
@@ -12,24 +12,38 @@ import { useKitLabels, useKitLink } from "../i18n/kit-labels.js";
12
12
  import { pickLinkRenderer } from "./text-link.js";
13
13
  import { LockedReason, mergeDescribedBy, useLockReason } from "./field-parts.js";
14
14
  import { DEFAULT_LIST_LABELS } from "./list-labels.js";
15
+ import { Button } from "./button.js";
15
16
  const ListContext = createContext({
16
17
  density: "default",
17
18
  separator: "gap"
18
19
  });
20
+ const MORE_PAD = {
21
+ compact: "px-2 py-1",
22
+ default: "px-3 py-1.5",
23
+ comfortable: "px-4 py-2"
24
+ };
19
25
  function List({
20
26
  ref,
21
27
  as = "ul",
22
28
  density = "default",
23
29
  separator = "gap",
30
+ limit,
24
31
  className,
25
32
  children,
26
33
  ...rest
27
34
  }) {
28
35
  const Tag = as;
29
- return /* @__PURE__ */ jsx(ListContext.Provider, { value: { density, separator }, children: /* @__PURE__ */ jsx(
36
+ const labels = useKitLabels("list", DEFAULT_LIST_LABELS);
37
+ const [open, setOpen] = useState(false);
38
+ const generatedId = useId();
39
+ const id = rest.id ?? generatedId;
40
+ const rows = limit === void 0 ? null : Children.toArray(children);
41
+ const folds = rows !== null && rows.length > Math.max(0, limit);
42
+ return /* @__PURE__ */ jsx(ListContext.Provider, { value: { density, separator }, children: /* @__PURE__ */ jsxs(
30
43
  Tag,
31
44
  {
32
45
  ...rest,
46
+ id: folds ? id : rest.id,
33
47
  ref,
34
48
  role: "list",
35
49
  className: cn(
@@ -37,7 +51,22 @@ function List({
37
51
  separator === "gap" && (density === "compact" ? "gap-0.5" : "gap-1"),
38
52
  className
39
53
  ),
40
- children
54
+ children: [
55
+ folds && !open ? rows.slice(0, Math.max(0, limit)) : children,
56
+ folds && /* @__PURE__ */ jsx("li", { role: "none", className: MORE_PAD[density], children: /* @__PURE__ */ jsx(
57
+ Button,
58
+ {
59
+ type: "button",
60
+ variant: "link",
61
+ size: "sm",
62
+ className: "h-auto p-0",
63
+ "aria-expanded": open,
64
+ "aria-controls": id,
65
+ onClick: () => setOpen((was) => !was),
66
+ children: open ? labels.showFewer ?? DEFAULT_LIST_LABELS.showFewer : (labels.showAll ?? DEFAULT_LIST_LABELS.showAll)(rows.length)
67
+ }
68
+ ) })
69
+ ]
41
70
  }
42
71
  ) });
43
72
  }
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/components/list.tsx"],"sourcesContent":["import { createContext, useContext, useId, useState } from \"react\";\nimport type { ComponentPropsWithoutRef, HTMLAttributes, MouseEvent, ReactElement, ReactNode, Ref } from \"react\";\nimport { ChevronDown, ExternalLink } from \"lucide-react\";\nimport type { LucideIcon } from \"lucide-react\";\nimport { cn } from \"../lib/cn\";\nimport { FOCUS_RING } from \"./focus-ring\";\nimport { Spinner } from \"./spinner\";\nimport { StatusDot } from \"./status-dot\";\nimport { Collapse } from \"./disclosure\";\nimport { toneTextClass } from \"./signed-amount\";\nimport type { TextTone } from \"./signed-amount\";\nimport { useKitLabels, useKitLink } from \"../i18n/kit-labels\";\nimport { pickLinkRenderer } from \"./text-link\";\nimport { LockedReason, mergeDescribedBy, useLockReason, type DisabledReasonDisplay } from \"./field-parts\";\nimport type { CommitScope } from \"./write-lock\";\nimport { DEFAULT_LIST_LABELS } from \"./list-labels\";\n\n/* ── Labels ───────────────────────────────────────────────────────────────── */\n\nexport interface ListLabels {\n /** Read after an unread row's title — the dot alone says nothing to a screen reader. */\n unread: string;\n /** Read after an external row's title: the row leaves the app in a new tab. */\n opensInNewTab: string;\n}\n\n// Defined in ./list-labels, which holds no component code (0.34, KS U8).\nexport { DEFAULT_LIST_LABELS };\n\n/* ── List ─────────────────────────────────────────────────────────────────── */\n\n/**\n * `compact` 36px-ish rows for a dense panel (an inbox in a menu), `default` the ~44px\n * row every site draws with `py-2.5`, `comfortable` for a list that is the page.\n */\nexport type ListDensity = \"compact\" | \"default\" | \"comfortable\";\n/** `divider`: a rule between rows (kastlan's `divide-y` lists); `gap`: a small space\n * between rows (lenkbank's `space-y-2`, kastlan's `space-y-1`); `none`: flush. */\nexport type ListSeparator = \"divider\" | \"gap\" | \"none\";\n\nconst ListContext = createContext<{ density: ListDensity; separator: ListSeparator }>({\n density: \"default\",\n separator: \"gap\",\n});\n\nexport interface ListProps extends Omit<ComponentPropsWithoutRef<\"ul\">, \"role\"> {\n /** `ol` when the order means something (a ranked list, numbered segments). */\n as?: \"ul\" | \"ol\";\n /** Every row's density, unless a row says otherwise. Default `default`. */\n density?: ListDensity;\n /** Default `gap`. */\n separator?: ListSeparator;\n /** The `<ul>` (or `<ol>`). A React 19 ref prop since 0.34 (MOD-4, refactor audit\n * 2026-10); it was `forwardRef`. */\n ref?: Ref<HTMLUListElement>;\n}\n\n/**\n * The list of {@link ListItem}s.\n *\n * A `<ul>` with an explicit `role=\"list\"`: Safari drops a list's semantics once its\n * bullets are styled away, and \"list, 7 items\" is exactly what a screen-reader user\n * wants to hear before the first row.\n */\nexport function List({\n ref,\n as = \"ul\",\n density = \"default\",\n separator = \"gap\",\n className,\n children,\n ...rest\n}: ListProps) {\n const Tag = as;\n return (\n <ListContext.Provider value={{ density, separator }}>\n <Tag\n {...rest}\n ref={ref as Ref<HTMLUListElement & HTMLOListElement>}\n role=\"list\"\n className={cn(\n \"m-0 flex list-none flex-col p-0\",\n separator === \"gap\" && (density === \"compact\" ? \"gap-0.5\" : \"gap-1\"),\n className,\n )}\n >\n {children}\n </Tag>\n </ListContext.Provider>\n );\n}\n\n/* ── ListItem ─────────────────────────────────────────────────────────────── */\n\n/** What {@link ListItemProps.renderLink} is handed. Spread it onto your router's link,\n * mapping `href` to what it calls it — `({ href, ...p }) => <Link to={href} {...p} />`. */\nexport interface ListItemLinkProps {\n href: string;\n /** The row's target look — keep it, or the row's padding and focus ring go. */\n className: string;\n children: ReactNode;\n ref?: Ref<HTMLAnchorElement>;\n onClick?: (event: MouseEvent<HTMLAnchorElement>) => void;\n onAuxClick?: (event: MouseEvent<HTMLAnchorElement>) => void;\n \"aria-current\"?: \"true\";\n \"aria-busy\"?: true;\n id?: string;\n [key: `aria-${string}`]: string | boolean | number | undefined;\n [key: `data-${string}`]: unknown;\n}\n\nconst PAD: Record<\n ListDensity,\n { target: string; actions: string; leadingActions: string; gap: string; body: string }\n> = {\n compact: { target: \"gap-2 px-2 py-1.5\", actions: \"pe-1.5\", leadingActions: \"ps-1.5\", gap: \"gap-0.5\", body: \"px-2 pb-1.5\" },\n default: { target: \"gap-3 px-3 py-2.5\", actions: \"pe-2\", leadingActions: \"ps-2\", gap: \"gap-1\", body: \"px-3 pb-2.5\" },\n comfortable: { target: \"gap-3 px-4 py-3\", actions: \"pe-3\", leadingActions: \"ps-3\", gap: \"gap-1\", body: \"px-4 pb-3\" },\n};\n\n/** {@link ListItemBaseProps.trailingTone}: a {@link TextTone}, or `inherit` — no\n * colour of the slot's own, so an element's colour (or the row's) stands. */\nexport type ListItemTrailingTone = TextTone | \"inherit\";\n\n// The kit's focus frame (§5), inset so the row's own border and its neighbours do not\n// clip it.\nconst TARGET_RING = cn(FOCUS_RING, \"focus-visible:ring-inset\");\n\n/**\n * §4 \"ListItem: title, subtitle and meta wrap instead of truncating\" at Large and Extra\n * large (docs/text-size-harmonization.md). `large:` variants over the Normal look, so\n * a list keeps its one-height rows at Normal and costs no render to switch: a one-line\n * line is `truncate-until-large` (tokens.css, 0.33 §10.17: `truncate`, and at Large a\n * wrap that breaks a word too long for the row — the kit's one name for what this file\n * spelt as `WRAP_AT_LARGE`), a `line-clamp-2` lets go. An ellipsis hides the end of a\n * name, which is the part a reader who needs big type cannot recover by squinting.\n */\nconst UNCLAMP_AT_LARGE = \"large:line-clamp-none\";\n\ninterface ListItemBaseProps {\n /** The row's name: one line, truncated — unless {@link titleLines} says otherwise. */\n title: ReactNode;\n /**\n * How many lines the title may take. `1` (default) truncates with an ellipsis, which\n * is right for a name the app chose — a unit number, a user, a file — where the\n * cut-off end is still recognisable and every row keeps one height.\n *\n * `2` clamps at two lines; `\"all\"` shows every line, breaking inside a long word\n * rather than overflowing. For rows whose only name is free text a person typed\n * (kastlan 44): a defect or a note titled by its description, where the first 30\n * characters on a phone say nothing and there is no other field to tell two rows\n * apart. Pair it with `align=\"start\"` so the icon stays by the first line.\n */\n titleLines?: 1 | 2 | \"all\";\n /** The line under it: one line, truncated — or two, with `subtitleLines={2}`. */\n subtitle?: ReactNode;\n /** `2` for a line that is a body rather than a label (keksdose's notification text). */\n subtitleLines?: 1 | 2;\n /**\n * A small line ABOVE the title, in the caption type: the group a row belongs to, a\n * date heading, a category (\"Groceries · weekly\"). Part of the target and of its\n * name, read before the title. One line, truncated.\n */\n overline?: ReactNode;\n /** A third line in the caption type: a timestamp, \"Updated …\" (lenkbank's projects). */\n meta?: ReactNode;\n /**\n * Let {@link meta} wrap: it becomes a wrapping row (`flex-wrap`, a small gap)\n * instead of one truncated line — for a run of chips (keksdose's payee-backfill\n * tags), which otherwise had nowhere to go but the body, outside the target. Still\n * inside the target, so still no controls: chips that only show, not ones that\n * remove themselves.\n */\n metaWrap?: boolean;\n /**\n * Content INSIDE the main target, under the title, subtitle and meta: a progress or\n * budget bar, a sparkline, a strip of figures (keksdose's food-group bars). Unlike\n * {@link children}, which sits outside the target, a click on it is a click on the\n * row, and its hover fill covers it.\n *\n * Because it is inside the button or link it obeys the {@link trailing} rule: shown,\n * never operated — no buttons, links or inputs (invalid HTML in a button, and folded\n * into its name). Its text joins the row's accessible name, so give a bar that only\n * repeats a figure already in the row `aria-hidden`. Rendered in a block `<span>`,\n * full width of the TEXT COLUMN only — the column the title and subtitle are in, not\n * the whole row: it starts after the {@link icon} / {@link leading} and stops where\n * {@link trailing} begins, so a meter under a row with a trailing figure is as wide as\n * the title, not run on under the figure (keksdose's budget rows). For a bar across\n * the full row, put it in {@link children} (outside the target) or leave `trailing`\n * off and show the figure in `meta`. Strictly a button holds phrasing content only; the\n * kit's `ProgressBar` draws `<div>`s, which every browser lays out and reads fine\n * there but an HTML validator flags.\n */\n content?: ReactNode;\n /** A Lucide icon before the text, muted, at 16px. */\n icon?: LucideIcon;\n /** Anything else before the text — an avatar, a file-type badge, a drag handle. */\n leading?: ReactNode;\n /**\n * Inside the main target, at its end: a status chip, a file size, a count. It is part\n * of what the row's click does and part of its accessible name — so never a control.\n * Controls go in {@link actions}.\n */\n trailing?: ReactNode;\n /**\n * The colour of the {@link trailing} slot. Default `muted` — today's look, right for\n * a size or a count in plain text. `inherit` sets none, so an element's own colour\n * (an amount coloured by its sign) stands without a second override; any\n * {@link TextTone} colours a plain-text figure (`\"danger\"` for an overdue count).\n */\n trailingTone?: ListItemTrailingTone;\n /**\n * Controls BESIDE the main target, never inside it: copy, delete, \"Open\".\n *\n * A button inside a button (or a link) is invalid HTML, and a screen reader folds the\n * inner one into the outer one's name where it cannot be reached. lenkbank found this\n * three times (profile-bar.tsx:110, projects-page.tsx:61, segment-list.tsx:165) and\n * solved it the same way each time: the row's target and its actions are siblings in\n * one bordered box. That is this slot — and being siblings is also what stops a click\n * on \"Copy\" from selecting the row.\n */\n actions?: ReactNode;\n /**\n * Controls BEFORE the main target, never inside it: a selection checkbox, a drag\n * handle that is a button. The mirror of {@link actions} at the start edge — a\n * sibling of the target, so checking the box does not activate (or expand) the row\n * and the checkbox keeps its own name. Give the control an `aria-label` naming the\n * row (\"Select Groceries\"), since it is not inside the row's name. Anything that only\n * shows goes in {@link leading}, inside the target.\n */\n leadingActions?: ReactNode;\n /**\n * Rich content UNDER the title row and outside its target: photos, per-row buttons,\n * form fields — kastlan's handover room inspector (room-inspector.tsx), the handover\n * detail page and the wizard's defects step (wizard/defects-step.tsx), which draw\n * each room or defect as a `Card variant=\"inset\"` because a row could not hold a\n * photo strip with its own buttons.\n *\n * The same rule as {@link actions}, one row down: the title area stays ONE named\n * target (the button or link, with the title and subtitle as its name), and the body\n * is its sibling, never nested in it — a thumbnail's \"Remove\" inside the row's button\n * would be invalid HTML and unreachable by a screen reader. The body is a\n * `role=\"group\"` named by the row's title (`aria-labelledby`), so tabbing into the\n * third photo's button of the second room is heard as \"Kitchen, group\" first rather\n * than as a button floating free of any row. Inside the row's box, so a `selected`\n * border and a `divider` hold both; the hover fill stays on the target alone, since\n * the body is not what a click on the row does.\n */\n children?: ReactNode;\n /** Classes on the {@link children} body. */\n bodyClassName?: string;\n /**\n * The unread mark: a brand dot at the row's end, the title in semibold, and \"Unread\"\n * read after the title (keksdose notification-inbox.tsx:215). For another status,\n * pass your own mark in {@link status}.\n */\n unread?: boolean;\n /** A mark in the unread dot's place — a `StatusDot` of another tone, a count. */\n status?: ReactNode;\n /**\n * The row is the chosen one — a brand border on the raised surface (lenkbank's\n * profile bar and segment list). `aria-current=\"true\"` on the target: the row is the\n * current one of a set, which is what those lists mean, not a pressed toggle.\n */\n selected?: boolean;\n disabled?: boolean;\n /**\n * Work under way on the row — keksdose's attachment download (support-attachments\n * .tsx:91). A spinner takes the trailing edge, the target is `aria-busy`, and a\n * second click is ignored. The row keeps focus: `aria-disabled`, not `disabled`, so\n * a keyboard user is not dropped back to the top of the page mid-download.\n */\n loading?: boolean;\n /** Overrides the {@link List}'s density for this row. */\n density?: ListDensity;\n /** `start` for rows whose text runs to two or three lines, so the icon sits by the\n * title rather than the middle of the paragraph. Default `center`. */\n align?: \"center\" | \"start\";\n /** Classes on the visible row (the bordered box). */\n className?: string;\n /** Classes on the main target (the button, link or static block). */\n targetClassName?: string;\n /**\n * Extra attributes and handlers for the main target — e.g. HTML5 drag-and-drop\n * (`draggable`, `onDragStart`, `onDragEnd`): lenkbank's segment list drags the WHOLE\n * 44px row, and a grip inside the button cannot start a drag in Firefox. Spread first,\n * so the row's own role, name, state and click handling always win.\n */\n targetProps?: HTMLAttributes<HTMLElement>;\n /**\n * A visible border while unselected, instead of the transparent one. For rows laid out\n * as a wrapping strip (lenkbank's profile bar), where borderless rows read as loose\n * text. The selected row's brand border is unchanged.\n */\n bordered?: boolean;\n /** `div` for a single row outside a {@link List}; an `<li>` must sit in a list. */\n as?: \"li\" | \"div\";\n /**\n * Wraps the visible row (the bordered box: target, actions and body) in an element\n * of your own, INSIDE the `<li>` — a `<Tooltip>` explaining why the row is disabled,\n * an app's save guard: `renderRow={(row) => <Tooltip label=\"Locked\">{row}</Tooltip>}`.\n *\n * The wrapper otherwise had to go round the whole `ListItem`, which put a `<span>`\n * between the `<ul>` and its `<li>` (invalid; the list stops counting items), so\n * callers wrote `<li><Wrapper><ListItem as=\"div\" /></Wrapper></li>` — and lost the\n * divider and its spacing, which sit on the row's own `<li>`. Here the `<li>` stays\n * the list's child and keeps both. The `<li>` becomes a column and the row takes the\n * full width, so an inline wrapper (the tooltip's `inline-flex` span) still stretches\n * the row across the list.\n *\n * `row` is the row BOX (a `<div>`), not the target: a wrapper that forces a prop onto\n * its child with `cloneElement` — a save guard that injects `disabled` while a form is\n * dirty (keksdose's SaveGuard) — sets it on that div, where it does nothing, and the\n * button inside stays live. Pass `disabled` (or `loading`) to the ListItem yourself,\n * from the same state the guard reads, and let the wrapper only explain it:\n * `<ListItem disabled={dirty} renderRow={(row) => <Tooltip label=\"Save first\">{row}</Tooltip>} />`.\n */\n renderRow?: (row: ReactElement) => ReactNode;\n /** Reaches the main target — the element with the row's name and action. */\n id?: string;\n [key: `aria-${string}`]: string | boolean | number | undefined;\n [key: `data-${string}`]: unknown;\n /** The main target — the row's `<a>`, `<button>` or `<div>`, where `id` goes. A React\n * 19 ref prop since 0.34 (MOD-4, refactor audit 2026-10); it was `forwardRef`. */\n ref?: Ref<HTMLElement>;\n}\n\n/**\n * The row is ONE thing to activate, so the types allow one: a button, an in-app link,\n * an external link, or nothing (a static row, like kastlan's passkey and user lists).\n */\nexport type ListItemProps = ListItemBaseProps &\n (\n | ({\n /** Makes the row a button — select, open, mark read. */\n onClick: (event: MouseEvent<HTMLButtonElement>) => void;\n /**\n * Why the row's click is not available — {@link Button}'s `disabledReason`, for\n * a row whose click WRITES (mark read, assign, toggle a grant). keksdose K3: such\n * rows sat behind a `SaveGuard` that forced a native `disabled`, out of the tab\n * order, so the reason never reached a keyboard; `renderRow` with a Tooltip could\n * explain it but not keep the row reachable.\n *\n * With a reason the row's button is `aria-disabled` instead: still focusable, a\n * click does nothing, the reason is a line under the row at Large and on a touch\n * screen, else in the kit {@link Tooltip} over the row (`disabledReasonDisplay`),\n * and on its `aria-describedby`, and the row wears the disabled look. It wins over\n * `disabled`. The row's `actions` are untouched — each control there is its own.\n */\n disabledReason?: ReactNode;\n /** Where the reason shows — {@link Button}'s `disabledReasonDisplay`. A long list\n * of locked rows under a banner that already says why can keep it to the\n * tooltip with `\"tooltip\"` (DUP-3). */\n disabledReasonDisplay?: DisabledReasonDisplay;\n /**\n * The row's click COMMITS. Under a locked {@link WriteLockProvider} it is locked\n * the `disabledReason` way with the lock's reason (which wins over its own). No\n * provider, or an unlocked one: no effect. Button's `commit`, for keksdose K3.\n */\n commit?: CommitScope;\n href?: never;\n renderLink?: never;\n external?: never;\n onAuxClick?: never;\n } & NotExpandable)\n | ({\n /**\n * Makes the row a link. A real `<a href>`, so a middle click opens it in a\n * background tab (keksdose feedback #451) — which a button calling `navigate`\n * cannot do.\n */\n href: string;\n /** Your router's link for an in-app `href`. Default: the `<UiKitProvider\n * linkComponent>`, then `<a>`. The API `Chip`, `StatTile` and `MenuItem`\n * share. Ignored for an `external` row. */\n renderLink?: (props: ListItemLinkProps) => ReactElement;\n /**\n * The link leaves the app: a plain `<a target=\"_blank\" rel=\"noopener\n * noreferrer\">` with an external-link mark, and \"opens in a new tab\" read\n * after the title. keksdose's inbox rows whose URL is absolute.\n */\n external?: boolean;\n /** Runs on a click, before the navigation (mark the row read). */\n onClick?: (event: MouseEvent<HTMLAnchorElement>) => void;\n /** Runs on a middle click, which fires no `click` — keksdose marks a\n * notification read here too, so a row opened into a background tab does not\n * stay unread. */\n onAuxClick?: (event: MouseEvent<HTMLAnchorElement>) => void;\n disabledReason?: never;\n disabledReasonDisplay?: never;\n commit?: never;\n } & NotExpandable)\n | ({\n onClick?: never;\n href?: never;\n renderLink?: never;\n external?: never;\n onAuxClick?: never;\n disabledReason?: never;\n disabledReasonDisplay?: never;\n commit?: never;\n } & NotExpandable)\n | {\n /**\n * Makes the row a disclosure: the target is a button with `aria-expanded` and a\n * turning chevron at its end, and this opens under the row, outside the target\n * (so it may hold controls), animated by {@link Collapse}. keksdose's collapsible\n * groups — a group row with a select-all checkbox before it\n * ({@link ListItemBaseProps.leadingActions}) and its members inside.\n *\n * The row's ONE action is the toggle, so it takes no `onClick` or `href`. The\n * content is a `role=\"group\"` named by the title, unmounted while shut (as a\n * `Disclosure`'s body is).\n */\n expandedContent: ReactNode;\n /** Controlled open state. Leave it out for the row to keep its own. */\n expanded?: boolean;\n /** The uncontrolled row's first state. Default `false`. */\n defaultExpanded?: boolean;\n /** Runs on every toggle with the new state. */\n onExpandedChange?: (expanded: boolean) => void;\n onClick?: never;\n href?: never;\n renderLink?: never;\n external?: never;\n onAuxClick?: never;\n disabledReason?: never;\n disabledReasonDisplay?: never;\n commit?: never;\n }\n );\n\ninterface NotExpandable {\n expandedContent?: never;\n expanded?: never;\n defaultExpanded?: never;\n onExpandedChange?: never;\n}\n\n/**\n * One row of a {@link List}: leading icon or avatar, title and subtitle (both\n * truncating by default), a trailing slot, and — beside the row, not inside it — its actions.\n *\n * At Large and Extra large (0.32, docs/text-size-harmonization.md §4) nothing in it\n * truncates: the overline, title, subtitle and meta wrap, a two-line clamp lets go, and\n * a trailing slot drops under the text when the two no longer fit side by side. The\n * rows lose their one height there; that is the trade the reader asked for.\n *\n * All three apps draw this row by hand, a dozen times: kastlan's units\n * (building-detail-page.tsx:402), open tickets (group-overview-page.tsx:160), activity\n * feed (activity-feed.tsx:53), company users (platform-company-detail-page.tsx:140)\n * and passkeys (passkeys-card.tsx:75); keksdose's notification inbox and attachment\n * downloads; lenkbank's profiles, projects and segments. The copies differ in the\n * details that are easy to get wrong — which of them truncate, whether the actions\n * are nested in the button, whether a link row is a link at all.\n */\nexport function ListItem({\n ref,\n title,\n titleLines = 1,\n overline,\n subtitle,\n subtitleLines = 1,\n meta,\n metaWrap = false,\n content,\n icon: Icon,\n leading,\n trailing,\n trailingTone = \"muted\",\n actions,\n leadingActions,\n unread = false,\n status,\n selected = false,\n disabled = false,\n loading = false,\n density: densityProp,\n align = \"center\",\n className,\n targetClassName,\n targetProps,\n bordered = false,\n as = \"li\",\n renderRow,\n children,\n bodyClassName,\n href,\n renderLink,\n external = false,\n onClick,\n onAuxClick,\n disabledReason,\n disabledReasonDisplay,\n commit,\n expandedContent,\n expanded: expandedProp,\n defaultExpanded = false,\n onExpandedChange,\n ...rest\n}: ListItemProps) {\n const list = useContext(ListContext);\n const labels = useKitLabels(\"list\", DEFAULT_LIST_LABELS);\n const kitLink = useKitLink();\n const [ownExpanded, setOwnExpanded] = useState(defaultExpanded);\n const density = densityProp ?? list.density;\n const pad = PAD[density];\n const expandable = expandedContent !== undefined;\n const expanded = expandable && (expandedProp ?? ownExpanded);\n const isLink = href !== undefined && !disabled;\n const isButton = !isLink && (onClick !== undefined || expandable) && href === undefined;\n const interactive = isLink || isButton;\n // Only an onClick row takes a reason (the types allow no other); the hook runs on\n // every row so the hook order never depends on which kind of row this is.\n const lock = useLockReason(commit, disabledReason);\n const locked = lock.locked && isButton && !expandable;\n const inert = disabled || loading || locked;\n const hasBody = children !== undefined && children !== null && children !== false;\n // Anything under the row's own line: the box turns into a column, and the target\n // takes its own corners and hover fill (see `children`).\n const stacked = hasBody || expandable;\n const titleId = useId();\n const expandedId = useId();\n\n const toggle = () => {\n const next = !expanded;\n if (expandedProp === undefined) setOwnExpanded(next);\n onExpandedChange?.(next);\n };\n\n const mark =\n status ??\n (unread ? <StatusDot tone=\"brand\" size=\"sm\" className={align === \"start\" ? \"mt-1.5\" : undefined} /> : null);\n\n const body = (\n <>\n {Icon && (\n <Icon\n className={cn(\"size-4 shrink-0 text-muted\", align === \"start\" && \"mt-0.5\")}\n aria-hidden\n />\n )}\n {leading}\n {/* With a trailing slot, a basis at Large: below it the trailing chip or figure\n wraps under the text (the target wraps, see `target`) rather than squeezing the\n now-wrapping title into a column one word wide (§4, 240 px effective width). */}\n <span className={cn(\"flex min-w-0 flex-1 flex-col\", trailing != null && \"large:basis-40\")}>\n {overline != null && (\n <span className=\"block truncate-until-large text-caption font-medium leading-snug text-muted\">\n {overline}\n </span>\n )}\n <span\n // Names the body's and the expanded content's group; harmless without one.\n id={stacked ? titleId : undefined}\n className={cn(\n \"block text-sm text-primary\",\n titleLines === 1\n ? \"truncate-until-large\"\n : titleLines === 2\n ? cn(\"line-clamp-2 break-words\", UNCLAMP_AT_LARGE)\n : \"break-words\",\n unread ? \"font-semibold\" : \"font-medium\",\n )}\n >\n {title}\n {unread && <span className=\"sr-only\"> ({labels.unread})</span>}\n {external && href !== undefined && <span className=\"sr-only\"> ({labels.opensInNewTab})</span>}\n </span>\n {subtitle != null && (\n <span\n className={cn(\n \"block text-xs text-muted\",\n subtitleLines === 2 ? cn(\"line-clamp-2 break-words\", UNCLAMP_AT_LARGE) : \"truncate-until-large\",\n )}\n >\n {subtitle}\n </span>\n )}\n {meta != null && (\n <span\n className={cn(\n \"mt-0.5 text-caption leading-snug text-muted\",\n metaWrap ? \"flex flex-wrap items-center gap-1\" : \"block truncate-until-large\",\n )}\n >\n {meta}\n </span>\n )}\n {content != null && <span className=\"mt-1.5 block min-w-0\">{content}</span>}\n </span>\n {trailing != null && (\n <span\n className={cn(\n \"flex shrink-0 items-center gap-2 text-xs large:ms-auto\",\n trailingTone !== \"inherit\" && toneTextClass(trailingTone),\n )}\n >\n {trailing}\n </span>\n )}\n {loading ? (\n <Spinner label={null} className=\"size-3.5 shrink-0\" />\n ) : (\n external && href !== undefined && (\n <ExternalLink className=\"size-3.5 shrink-0 text-muted rtl:-scale-x-100\" aria-hidden />\n )\n )}\n {mark}\n {expandable && (\n // Turned, not swapped — the Disclosure card's convention: down while shut, up\n // while open. A rotation is its own mirror, so RTL needs nothing.\n <ChevronDown\n aria-hidden\n className={cn(\n \"size-4 shrink-0 text-muted transition-transform duration-200 ease-out motion-reduce:transition-none\",\n align === \"start\" && \"mt-0.5\",\n expanded && \"rotate-180\",\n )}\n />\n )}\n </>\n );\n\n const target = cn(\n \"flex min-w-0 flex-1 text-start\",\n // §4 at Large: the trailing slot drops under the text when both no longer fit.\n trailing != null && \"large:flex-wrap\",\n align === \"start\" ? \"items-start\" : \"items-center\",\n pad.target,\n // When stacked the target is the top of the box, not all of it, so it takes its\n // own corners and its own hover fill (see `children`).\n stacked ? \"rounded-md\" : \"rounded-[inherit]\",\n stacked && interactive && !inert && !selected && \"transition-colors hover:bg-hover\",\n interactive && TARGET_RING,\n interactive && !inert && \"cursor-pointer\",\n loading && \"cursor-progress\",\n locked && !loading && \"cursor-not-allowed\",\n targetClassName,\n );\n\n // `role` is the row's own (a button, a link, or nothing on a static block): a\n // targetProps role would silently turn the row into something else.\n const { role: _ignoredRole, ...targetExtras } = targetProps ?? {};\n\n let main: ReactNode;\n if (isLink) {\n const linkProps: ListItemLinkProps = {\n ...(targetExtras as Partial<ListItemLinkProps>),\n ...rest,\n ref: ref as Ref<HTMLAnchorElement>,\n href,\n className: target,\n \"aria-current\": selected ? \"true\" : undefined,\n \"aria-busy\": loading || undefined,\n onClick: loading\n ? (e) => e.preventDefault()\n : (onClick as ListItemLinkProps[\"onClick\"]),\n onAuxClick,\n children: body,\n };\n const { children: linkBody, ...anchorProps } = linkProps;\n const render = external ? undefined : pickLinkRenderer(renderLink, kitLink, href);\n main = render ? (\n <RenderedLink render={render} {...linkProps} />\n ) : (\n <a {...anchorProps} {...(external ? { target: \"_blank\", rel: \"noopener noreferrer\" } : null)}>\n {linkBody}\n </a>\n );\n } else if (isButton) {\n main = (\n <button\n {...(targetExtras as HTMLAttributes<HTMLButtonElement>)}\n {...rest}\n ref={ref as Ref<HTMLButtonElement>}\n type=\"button\"\n // A disabled row is `disabled`: nothing to do there, and nothing to reach. A\n // loading row is only `aria-disabled` — see `loading` — and so is a locked one:\n // a reason wins over `disabled`, so the row stays reachable to say it.\n disabled={locked ? undefined : disabled}\n aria-disabled={loading || locked || undefined}\n aria-busy={loading || undefined}\n aria-current={selected ? \"true\" : undefined}\n aria-expanded={expandable ? expanded : undefined}\n aria-controls={expandable ? expandedId : undefined}\n aria-describedby={\n locked\n ? mergeDescribedBy(rest[\"aria-describedby\"] as string | undefined, lock.reasonId)\n : (rest[\"aria-describedby\"] as string | undefined)\n }\n onClick={\n loading || locked\n ? undefined\n : expandable\n ? toggle\n : (onClick as (event: MouseEvent<HTMLButtonElement>) => void)\n }\n className={target}\n >\n {body}\n </button>\n );\n if (locked) {\n // The frame takes the target's place in the row, so it takes its flex sizing too;\n // the button fills it (`items-stretch` in the line's column). The line takes the\n // row's body padding, so its words start where the title's do.\n main = (\n <LockedReason\n lock={lock}\n display={disabledReasonDisplay}\n className=\"flex min-w-0 flex-1 items-stretch\"\n lineClassName={pad.body}\n >\n {main}\n </LockedReason>\n );\n }\n } else {\n main = (\n <div\n {...(targetExtras as HTMLAttributes<HTMLDivElement>)}\n {...rest}\n ref={ref as Ref<HTMLDivElement>}\n aria-disabled={disabled || undefined}\n aria-busy={loading || undefined}\n className={target}\n >\n {body}\n </div>\n );\n }\n\n const leadingActionsEl =\n leadingActions != null ? (\n <div className={cn(\"flex shrink-0 items-center\", pad.gap, pad.leadingActions)}>{leadingActions}</div>\n ) : null;\n const actionsEl =\n actions != null ? <div className={cn(\"flex shrink-0 items-center\", pad.gap, pad.actions)}>{actions}</div> : null;\n\n const row = (\n <div\n className={cn(\n \"flex min-w-0 rounded-md border transition-colors\",\n stacked ? \"flex-col\" : \"items-center\",\n // See `renderRow`: the wrapper may be inline, the row is not.\n renderRow && \"w-full\",\n selected\n ? \"border-brand bg-surface-2\"\n : bordered\n ? \"border-subtle\"\n : \"border-transparent\",\n !stacked && interactive && !inert && !selected && \"hover:bg-hover\",\n (disabled || locked) && \"opacity-50\",\n className,\n )}\n >\n {stacked ? (\n <>\n <div className=\"flex min-w-0 items-center\">\n {leadingActionsEl}\n {main}\n {actionsEl}\n </div>\n {expandable && (\n <Collapse open={expanded}>\n <div id={expandedId} role=\"group\" aria-labelledby={titleId} className={cn(\"min-w-0\", pad.body)}>\n {expandedContent}\n </div>\n </Collapse>\n )}\n {hasBody && (\n <div role=\"group\" aria-labelledby={titleId} className={cn(\"min-w-0\", pad.body, bodyClassName)}>\n {children}\n </div>\n )}\n </>\n ) : (\n <>\n {leadingActionsEl}\n {main}\n {actionsEl}\n </>\n )}\n </div>\n );\n\n const Tag = as;\n return (\n <Tag\n className={cn(\n list.separator === \"divider\" && \"border-b border-subtle last:border-b-0\",\n list.separator === \"divider\" && as === \"li\" && \"py-0.5\",\n renderRow && \"flex flex-col\",\n )}\n >\n {renderRow ? renderRow(row) : row}\n </Tag>\n );\n}\n\nfunction RenderedLink({\n render,\n ...props\n}: ListItemLinkProps & { render: (props: ListItemLinkProps) => ReactElement }) {\n return render(props);\n}\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../../src/components/list.tsx"],"sourcesContent":["import { Children, createContext, useContext, useId, useState } from \"react\";\nimport type { ComponentPropsWithoutRef, HTMLAttributes, MouseEvent, ReactElement, ReactNode, Ref } from \"react\";\nimport { ChevronDown, ExternalLink } from \"lucide-react\";\nimport type { LucideIcon } from \"lucide-react\";\nimport { cn } from \"../lib/cn\";\nimport { FOCUS_RING } from \"./focus-ring\";\nimport { Spinner } from \"./spinner\";\nimport { StatusDot } from \"./status-dot\";\nimport { Collapse } from \"./disclosure\";\nimport { toneTextClass } from \"./signed-amount\";\nimport type { TextTone } from \"./signed-amount\";\nimport { useKitLabels, useKitLink } from \"../i18n/kit-labels\";\nimport { pickLinkRenderer } from \"./text-link\";\nimport { LockedReason, mergeDescribedBy, useLockReason, type DisabledReasonDisplay } from \"./field-parts\";\nimport type { CommitScope } from \"./write-lock\";\nimport { DEFAULT_LIST_LABELS } from \"./list-labels\";\nimport { Button } from \"./button\";\n\n/* ── Labels ───────────────────────────────────────────────────────────────── */\n\nexport interface ListLabels {\n /** Read after an unread row's title — the dot alone says nothing to a screen reader. */\n unread: string;\n /** Read after an external row's title: the row leaves the app in a new tab. */\n opensInNewTab: string;\n /** 0.51.4: the row under a {@link ListProps.limit}ed list that opens the rest, with\n * the WHOLE list's count — \"Show all (12)\". Optional, as every key added to a\n * namespace an app may annotate: left out, the provider's, then English. */\n showAll?: (count: number) => string;\n /** 0.51.4: the same row once the list is open, folding it back. */\n showFewer?: string;\n}\n\n// Defined in ./list-labels, which holds no component code (0.34, KS U8).\nexport { DEFAULT_LIST_LABELS };\n\n/* ── List ─────────────────────────────────────────────────────────────────── */\n\n/**\n * `compact` 36px-ish rows for a dense panel (an inbox in a menu), `default` the ~44px\n * row every site draws with `py-2.5`, `comfortable` for a list that is the page.\n */\nexport type ListDensity = \"compact\" | \"default\" | \"comfortable\";\n/** `divider`: a rule between rows (kastlan's `divide-y` lists); `gap`: a small space\n * between rows (lenkbank's `space-y-2`, kastlan's `space-y-1`); `none`: flush. */\nexport type ListSeparator = \"divider\" | \"gap\" | \"none\";\n\nconst ListContext = createContext<{ density: ListDensity; separator: ListSeparator }>({\n density: \"default\",\n separator: \"gap\",\n});\n\nexport interface ListProps extends Omit<ComponentPropsWithoutRef<\"ul\">, \"role\"> {\n /** `ol` when the order means something (a ranked list, numbered segments). */\n as?: \"ul\" | \"ol\";\n /** Every row's density, unless a row says otherwise. Default `default`. */\n density?: ListDensity;\n /** Default `gap`. */\n separator?: ListSeparator;\n /** The `<ul>` (or `<ol>`). A React 19 ref prop since 0.34 (MOD-4, refactor audit\n * 2026-10); it was `forwardRef`. */\n ref?: Ref<HTMLUListElement>;\n /**\n * Show the first `limit` rows, then a \"Show all (n)\" row that opens the rest and turns\n * into \"Show fewer\" (0.51.4, keksdose run 89: a long \"Placed only roughly\" list on its\n * Matching report could only fold as a whole, so it opened as nothing or everything).\n * A list of `limit` rows or fewer draws them all, with no such row. Rows are counted as\n * `Children` counts them: one per `ListItem`, a fragment as one.\n *\n * The row is a button with `aria-expanded`, controlling the list; it sits in the list\n * as `role=\"none\"`, so a reader still hears \"list, 5 items\" for five rows.\n */\n limit?: number;\n}\n\n/** The show-all row's inset, in step with each density's row padding (`PAD`). */\nconst MORE_PAD: Record<ListDensity, string> = {\n compact: \"px-2 py-1\",\n default: \"px-3 py-1.5\",\n comfortable: \"px-4 py-2\",\n};\n\n/**\n * The list of {@link ListItem}s.\n *\n * A `<ul>` with an explicit `role=\"list\"`: Safari drops a list's semantics once its\n * bullets are styled away, and \"list, 7 items\" is exactly what a screen-reader user\n * wants to hear before the first row.\n */\nexport function List({\n ref,\n as = \"ul\",\n density = \"default\",\n separator = \"gap\",\n limit,\n className,\n children,\n ...rest\n}: ListProps) {\n const Tag = as;\n const labels = useKitLabels(\"list\", DEFAULT_LIST_LABELS);\n const [open, setOpen] = useState(false);\n const generatedId = useId();\n const id = rest.id ?? generatedId;\n const rows = limit === undefined ? null : Children.toArray(children);\n const folds = rows !== null && rows.length > Math.max(0, limit!);\n return (\n <ListContext.Provider value={{ density, separator }}>\n <Tag\n {...rest}\n id={folds ? id : rest.id}\n ref={ref as Ref<HTMLUListElement & HTMLOListElement>}\n role=\"list\"\n className={cn(\n \"m-0 flex list-none flex-col p-0\",\n separator === \"gap\" && (density === \"compact\" ? \"gap-0.5\" : \"gap-1\"),\n className,\n )}\n >\n {folds && !open ? rows.slice(0, Math.max(0, limit!)) : children}\n {folds && (\n <li role=\"none\" className={MORE_PAD[density]}>\n <Button\n type=\"button\"\n variant=\"link\"\n size=\"sm\"\n className=\"h-auto p-0\"\n aria-expanded={open}\n aria-controls={id}\n onClick={() => setOpen((was) => !was)}\n >\n {open\n ? (labels.showFewer ?? DEFAULT_LIST_LABELS.showFewer)\n : (labels.showAll ?? DEFAULT_LIST_LABELS.showAll!)(rows.length)}\n </Button>\n </li>\n )}\n </Tag>\n </ListContext.Provider>\n );\n}\n\n/* ── ListItem ─────────────────────────────────────────────────────────────── */\n\n/** What {@link ListItemProps.renderLink} is handed. Spread it onto your router's link,\n * mapping `href` to what it calls it — `({ href, ...p }) => <Link to={href} {...p} />`. */\nexport interface ListItemLinkProps {\n href: string;\n /** The row's target look — keep it, or the row's padding and focus ring go. */\n className: string;\n children: ReactNode;\n ref?: Ref<HTMLAnchorElement>;\n onClick?: (event: MouseEvent<HTMLAnchorElement>) => void;\n onAuxClick?: (event: MouseEvent<HTMLAnchorElement>) => void;\n \"aria-current\"?: \"true\";\n \"aria-busy\"?: true;\n id?: string;\n [key: `aria-${string}`]: string | boolean | number | undefined;\n [key: `data-${string}`]: unknown;\n}\n\nconst PAD: Record<\n ListDensity,\n { target: string; actions: string; leadingActions: string; gap: string; body: string }\n> = {\n compact: { target: \"gap-2 px-2 py-1.5\", actions: \"pe-1.5\", leadingActions: \"ps-1.5\", gap: \"gap-0.5\", body: \"px-2 pb-1.5\" },\n default: { target: \"gap-3 px-3 py-2.5\", actions: \"pe-2\", leadingActions: \"ps-2\", gap: \"gap-1\", body: \"px-3 pb-2.5\" },\n comfortable: { target: \"gap-3 px-4 py-3\", actions: \"pe-3\", leadingActions: \"ps-3\", gap: \"gap-1\", body: \"px-4 pb-3\" },\n};\n\n/** {@link ListItemBaseProps.trailingTone}: a {@link TextTone}, or `inherit` — no\n * colour of the slot's own, so an element's colour (or the row's) stands. */\nexport type ListItemTrailingTone = TextTone | \"inherit\";\n\n// The kit's focus frame (§5), inset so the row's own border and its neighbours do not\n// clip it.\nconst TARGET_RING = cn(FOCUS_RING, \"focus-visible:ring-inset\");\n\n/**\n * §4 \"ListItem: title, subtitle and meta wrap instead of truncating\" at Large and Extra\n * large (docs/text-size-harmonization.md). `large:` variants over the Normal look, so\n * a list keeps its one-height rows at Normal and costs no render to switch: a one-line\n * line is `truncate-until-large` (tokens.css, 0.33 §10.17: `truncate`, and at Large a\n * wrap that breaks a word too long for the row — the kit's one name for what this file\n * spelt as `WRAP_AT_LARGE`), a `line-clamp-2` lets go. An ellipsis hides the end of a\n * name, which is the part a reader who needs big type cannot recover by squinting.\n */\nconst UNCLAMP_AT_LARGE = \"large:line-clamp-none\";\n\ninterface ListItemBaseProps {\n /** The row's name: one line, truncated — unless {@link titleLines} says otherwise. */\n title: ReactNode;\n /**\n * How many lines the title may take. `1` (default) truncates with an ellipsis, which\n * is right for a name the app chose — a unit number, a user, a file — where the\n * cut-off end is still recognisable and every row keeps one height.\n *\n * `2` clamps at two lines; `\"all\"` shows every line, breaking inside a long word\n * rather than overflowing. For rows whose only name is free text a person typed\n * (kastlan 44): a defect or a note titled by its description, where the first 30\n * characters on a phone say nothing and there is no other field to tell two rows\n * apart. Pair it with `align=\"start\"` so the icon stays by the first line.\n */\n titleLines?: 1 | 2 | \"all\";\n /** The line under it: one line, truncated — or two, with `subtitleLines={2}`. */\n subtitle?: ReactNode;\n /** `2` for a line that is a body rather than a label (keksdose's notification text). */\n subtitleLines?: 1 | 2;\n /**\n * A small line ABOVE the title, in the caption type: the group a row belongs to, a\n * date heading, a category (\"Groceries · weekly\"). Part of the target and of its\n * name, read before the title. One line, truncated.\n */\n overline?: ReactNode;\n /** A third line in the caption type: a timestamp, \"Updated …\" (lenkbank's projects). */\n meta?: ReactNode;\n /**\n * Let {@link meta} wrap: it becomes a wrapping row (`flex-wrap`, a small gap)\n * instead of one truncated line — for a run of chips (keksdose's payee-backfill\n * tags), which otherwise had nowhere to go but the body, outside the target. Still\n * inside the target, so still no controls: chips that only show, not ones that\n * remove themselves.\n */\n metaWrap?: boolean;\n /**\n * Content INSIDE the main target, under the title, subtitle and meta: a progress or\n * budget bar, a sparkline, a strip of figures (keksdose's food-group bars). Unlike\n * {@link children}, which sits outside the target, a click on it is a click on the\n * row, and its hover fill covers it.\n *\n * Because it is inside the button or link it obeys the {@link trailing} rule: shown,\n * never operated — no buttons, links or inputs (invalid HTML in a button, and folded\n * into its name). Its text joins the row's accessible name, so give a bar that only\n * repeats a figure already in the row `aria-hidden`. Rendered in a block `<span>`,\n * full width of the TEXT COLUMN only — the column the title and subtitle are in, not\n * the whole row: it starts after the {@link icon} / {@link leading} and stops where\n * {@link trailing} begins, so a meter under a row with a trailing figure is as wide as\n * the title, not run on under the figure (keksdose's budget rows). For a bar across\n * the full row, put it in {@link children} (outside the target) or leave `trailing`\n * off and show the figure in `meta`. Strictly a button holds phrasing content only; the\n * kit's `ProgressBar` draws `<div>`s, which every browser lays out and reads fine\n * there but an HTML validator flags.\n */\n content?: ReactNode;\n /** A Lucide icon before the text, muted, at 16px. */\n icon?: LucideIcon;\n /** Anything else before the text — an avatar, a file-type badge, a drag handle. */\n leading?: ReactNode;\n /**\n * Inside the main target, at its end: a status chip, a file size, a count. It is part\n * of what the row's click does and part of its accessible name — so never a control.\n * Controls go in {@link actions}.\n */\n trailing?: ReactNode;\n /**\n * The colour of the {@link trailing} slot. Default `muted` — today's look, right for\n * a size or a count in plain text. `inherit` sets none, so an element's own colour\n * (an amount coloured by its sign) stands without a second override; any\n * {@link TextTone} colours a plain-text figure (`\"danger\"` for an overdue count).\n */\n trailingTone?: ListItemTrailingTone;\n /**\n * Controls BESIDE the main target, never inside it: copy, delete, \"Open\".\n *\n * A button inside a button (or a link) is invalid HTML, and a screen reader folds the\n * inner one into the outer one's name where it cannot be reached. lenkbank found this\n * three times (profile-bar.tsx:110, projects-page.tsx:61, segment-list.tsx:165) and\n * solved it the same way each time: the row's target and its actions are siblings in\n * one bordered box. That is this slot — and being siblings is also what stops a click\n * on \"Copy\" from selecting the row.\n */\n actions?: ReactNode;\n /**\n * Controls BEFORE the main target, never inside it: a selection checkbox, a drag\n * handle that is a button. The mirror of {@link actions} at the start edge — a\n * sibling of the target, so checking the box does not activate (or expand) the row\n * and the checkbox keeps its own name. Give the control an `aria-label` naming the\n * row (\"Select Groceries\"), since it is not inside the row's name. Anything that only\n * shows goes in {@link leading}, inside the target.\n */\n leadingActions?: ReactNode;\n /**\n * Rich content UNDER the title row and outside its target: photos, per-row buttons,\n * form fields — kastlan's handover room inspector (room-inspector.tsx), the handover\n * detail page and the wizard's defects step (wizard/defects-step.tsx), which draw\n * each room or defect as a `Card variant=\"inset\"` because a row could not hold a\n * photo strip with its own buttons.\n *\n * The same rule as {@link actions}, one row down: the title area stays ONE named\n * target (the button or link, with the title and subtitle as its name), and the body\n * is its sibling, never nested in it — a thumbnail's \"Remove\" inside the row's button\n * would be invalid HTML and unreachable by a screen reader. The body is a\n * `role=\"group\"` named by the row's title (`aria-labelledby`), so tabbing into the\n * third photo's button of the second room is heard as \"Kitchen, group\" first rather\n * than as a button floating free of any row. Inside the row's box, so a `selected`\n * border and a `divider` hold both; the hover fill stays on the target alone, since\n * the body is not what a click on the row does.\n */\n children?: ReactNode;\n /** Classes on the {@link children} body. */\n bodyClassName?: string;\n /**\n * The unread mark: a brand dot at the row's end, the title in semibold, and \"Unread\"\n * read after the title (keksdose notification-inbox.tsx:215). For another status,\n * pass your own mark in {@link status}.\n */\n unread?: boolean;\n /** A mark in the unread dot's place — a `StatusDot` of another tone, a count. */\n status?: ReactNode;\n /**\n * The row is the chosen one — a brand border on the raised surface (lenkbank's\n * profile bar and segment list). `aria-current=\"true\"` on the target: the row is the\n * current one of a set, which is what those lists mean, not a pressed toggle.\n */\n selected?: boolean;\n disabled?: boolean;\n /**\n * Work under way on the row — keksdose's attachment download (support-attachments\n * .tsx:91). A spinner takes the trailing edge, the target is `aria-busy`, and a\n * second click is ignored. The row keeps focus: `aria-disabled`, not `disabled`, so\n * a keyboard user is not dropped back to the top of the page mid-download.\n */\n loading?: boolean;\n /** Overrides the {@link List}'s density for this row. */\n density?: ListDensity;\n /** `start` for rows whose text runs to two or three lines, so the icon sits by the\n * title rather than the middle of the paragraph. Default `center`. */\n align?: \"center\" | \"start\";\n /** Classes on the visible row (the bordered box). */\n className?: string;\n /** Classes on the main target (the button, link or static block). */\n targetClassName?: string;\n /**\n * Extra attributes and handlers for the main target — e.g. HTML5 drag-and-drop\n * (`draggable`, `onDragStart`, `onDragEnd`): lenkbank's segment list drags the WHOLE\n * 44px row, and a grip inside the button cannot start a drag in Firefox. Spread first,\n * so the row's own role, name, state and click handling always win.\n */\n targetProps?: HTMLAttributes<HTMLElement>;\n /**\n * A visible border while unselected, instead of the transparent one. For rows laid out\n * as a wrapping strip (lenkbank's profile bar), where borderless rows read as loose\n * text. The selected row's brand border is unchanged.\n */\n bordered?: boolean;\n /** `div` for a single row outside a {@link List}; an `<li>` must sit in a list. */\n as?: \"li\" | \"div\";\n /**\n * Wraps the visible row (the bordered box: target, actions and body) in an element\n * of your own, INSIDE the `<li>` — a `<Tooltip>` explaining why the row is disabled,\n * an app's save guard: `renderRow={(row) => <Tooltip label=\"Locked\">{row}</Tooltip>}`.\n *\n * The wrapper otherwise had to go round the whole `ListItem`, which put a `<span>`\n * between the `<ul>` and its `<li>` (invalid; the list stops counting items), so\n * callers wrote `<li><Wrapper><ListItem as=\"div\" /></Wrapper></li>` — and lost the\n * divider and its spacing, which sit on the row's own `<li>`. Here the `<li>` stays\n * the list's child and keeps both. The `<li>` becomes a column and the row takes the\n * full width, so an inline wrapper (the tooltip's `inline-flex` span) still stretches\n * the row across the list.\n *\n * `row` is the row BOX (a `<div>`), not the target: a wrapper that forces a prop onto\n * its child with `cloneElement` — a save guard that injects `disabled` while a form is\n * dirty (keksdose's SaveGuard) — sets it on that div, where it does nothing, and the\n * button inside stays live. Pass `disabled` (or `loading`) to the ListItem yourself,\n * from the same state the guard reads, and let the wrapper only explain it:\n * `<ListItem disabled={dirty} renderRow={(row) => <Tooltip label=\"Save first\">{row}</Tooltip>} />`.\n */\n renderRow?: (row: ReactElement) => ReactNode;\n /** Reaches the main target — the element with the row's name and action. */\n id?: string;\n [key: `aria-${string}`]: string | boolean | number | undefined;\n [key: `data-${string}`]: unknown;\n /** The main target — the row's `<a>`, `<button>` or `<div>`, where `id` goes. A React\n * 19 ref prop since 0.34 (MOD-4, refactor audit 2026-10); it was `forwardRef`. */\n ref?: Ref<HTMLElement>;\n}\n\n/**\n * The row is ONE thing to activate, so the types allow one: a button, an in-app link,\n * an external link, or nothing (a static row, like kastlan's passkey and user lists).\n */\nexport type ListItemProps = ListItemBaseProps &\n (\n | ({\n /** Makes the row a button — select, open, mark read. */\n onClick: (event: MouseEvent<HTMLButtonElement>) => void;\n /**\n * Why the row's click is not available — {@link Button}'s `disabledReason`, for\n * a row whose click WRITES (mark read, assign, toggle a grant). keksdose K3: such\n * rows sat behind a `SaveGuard` that forced a native `disabled`, out of the tab\n * order, so the reason never reached a keyboard; `renderRow` with a Tooltip could\n * explain it but not keep the row reachable.\n *\n * With a reason the row's button is `aria-disabled` instead: still focusable, a\n * click does nothing, the reason is a line under the row at Large and on a touch\n * screen, else in the kit {@link Tooltip} over the row (`disabledReasonDisplay`),\n * and on its `aria-describedby`, and the row wears the disabled look. It wins over\n * `disabled`. The row's `actions` are untouched — each control there is its own.\n */\n disabledReason?: ReactNode;\n /** Where the reason shows — {@link Button}'s `disabledReasonDisplay`. A long list\n * of locked rows under a banner that already says why can keep it to the\n * tooltip with `\"tooltip\"` (DUP-3). */\n disabledReasonDisplay?: DisabledReasonDisplay;\n /**\n * The row's click COMMITS. Under a locked {@link WriteLockProvider} it is locked\n * the `disabledReason` way with the lock's reason (which wins over its own). No\n * provider, or an unlocked one: no effect. Button's `commit`, for keksdose K3.\n */\n commit?: CommitScope;\n href?: never;\n renderLink?: never;\n external?: never;\n onAuxClick?: never;\n } & NotExpandable)\n | ({\n /**\n * Makes the row a link. A real `<a href>`, so a middle click opens it in a\n * background tab (keksdose feedback #451) — which a button calling `navigate`\n * cannot do.\n */\n href: string;\n /** Your router's link for an in-app `href`. Default: the `<UiKitProvider\n * linkComponent>`, then `<a>`. The API `Chip`, `StatTile` and `MenuItem`\n * share. Ignored for an `external` row. */\n renderLink?: (props: ListItemLinkProps) => ReactElement;\n /**\n * The link leaves the app: a plain `<a target=\"_blank\" rel=\"noopener\n * noreferrer\">` with an external-link mark, and \"opens in a new tab\" read\n * after the title. keksdose's inbox rows whose URL is absolute.\n */\n external?: boolean;\n /** Runs on a click, before the navigation (mark the row read). */\n onClick?: (event: MouseEvent<HTMLAnchorElement>) => void;\n /** Runs on a middle click, which fires no `click` — keksdose marks a\n * notification read here too, so a row opened into a background tab does not\n * stay unread. */\n onAuxClick?: (event: MouseEvent<HTMLAnchorElement>) => void;\n disabledReason?: never;\n disabledReasonDisplay?: never;\n commit?: never;\n } & NotExpandable)\n | ({\n onClick?: never;\n href?: never;\n renderLink?: never;\n external?: never;\n onAuxClick?: never;\n disabledReason?: never;\n disabledReasonDisplay?: never;\n commit?: never;\n } & NotExpandable)\n | {\n /**\n * Makes the row a disclosure: the target is a button with `aria-expanded` and a\n * turning chevron at its end, and this opens under the row, outside the target\n * (so it may hold controls), animated by {@link Collapse}. keksdose's collapsible\n * groups — a group row with a select-all checkbox before it\n * ({@link ListItemBaseProps.leadingActions}) and its members inside.\n *\n * The row's ONE action is the toggle, so it takes no `onClick` or `href`. The\n * content is a `role=\"group\"` named by the title, unmounted while shut (as a\n * `Disclosure`'s body is).\n */\n expandedContent: ReactNode;\n /** Controlled open state. Leave it out for the row to keep its own. */\n expanded?: boolean;\n /** The uncontrolled row's first state. Default `false`. */\n defaultExpanded?: boolean;\n /** Runs on every toggle with the new state. */\n onExpandedChange?: (expanded: boolean) => void;\n onClick?: never;\n href?: never;\n renderLink?: never;\n external?: never;\n onAuxClick?: never;\n disabledReason?: never;\n disabledReasonDisplay?: never;\n commit?: never;\n }\n );\n\ninterface NotExpandable {\n expandedContent?: never;\n expanded?: never;\n defaultExpanded?: never;\n onExpandedChange?: never;\n}\n\n/**\n * One row of a {@link List}: leading icon or avatar, title and subtitle (both\n * truncating by default), a trailing slot, and — beside the row, not inside it — its actions.\n *\n * At Large and Extra large (0.32, docs/text-size-harmonization.md §4) nothing in it\n * truncates: the overline, title, subtitle and meta wrap, a two-line clamp lets go, and\n * a trailing slot drops under the text when the two no longer fit side by side. The\n * rows lose their one height there; that is the trade the reader asked for.\n *\n * All three apps draw this row by hand, a dozen times: kastlan's units\n * (building-detail-page.tsx:402), open tickets (group-overview-page.tsx:160), activity\n * feed (activity-feed.tsx:53), company users (platform-company-detail-page.tsx:140)\n * and passkeys (passkeys-card.tsx:75); keksdose's notification inbox and attachment\n * downloads; lenkbank's profiles, projects and segments. The copies differ in the\n * details that are easy to get wrong — which of them truncate, whether the actions\n * are nested in the button, whether a link row is a link at all.\n */\nexport function ListItem({\n ref,\n title,\n titleLines = 1,\n overline,\n subtitle,\n subtitleLines = 1,\n meta,\n metaWrap = false,\n content,\n icon: Icon,\n leading,\n trailing,\n trailingTone = \"muted\",\n actions,\n leadingActions,\n unread = false,\n status,\n selected = false,\n disabled = false,\n loading = false,\n density: densityProp,\n align = \"center\",\n className,\n targetClassName,\n targetProps,\n bordered = false,\n as = \"li\",\n renderRow,\n children,\n bodyClassName,\n href,\n renderLink,\n external = false,\n onClick,\n onAuxClick,\n disabledReason,\n disabledReasonDisplay,\n commit,\n expandedContent,\n expanded: expandedProp,\n defaultExpanded = false,\n onExpandedChange,\n ...rest\n}: ListItemProps) {\n const list = useContext(ListContext);\n const labels = useKitLabels(\"list\", DEFAULT_LIST_LABELS);\n const kitLink = useKitLink();\n const [ownExpanded, setOwnExpanded] = useState(defaultExpanded);\n const density = densityProp ?? list.density;\n const pad = PAD[density];\n const expandable = expandedContent !== undefined;\n const expanded = expandable && (expandedProp ?? ownExpanded);\n const isLink = href !== undefined && !disabled;\n const isButton = !isLink && (onClick !== undefined || expandable) && href === undefined;\n const interactive = isLink || isButton;\n // Only an onClick row takes a reason (the types allow no other); the hook runs on\n // every row so the hook order never depends on which kind of row this is.\n const lock = useLockReason(commit, disabledReason);\n const locked = lock.locked && isButton && !expandable;\n const inert = disabled || loading || locked;\n const hasBody = children !== undefined && children !== null && children !== false;\n // Anything under the row's own line: the box turns into a column, and the target\n // takes its own corners and hover fill (see `children`).\n const stacked = hasBody || expandable;\n const titleId = useId();\n const expandedId = useId();\n\n const toggle = () => {\n const next = !expanded;\n if (expandedProp === undefined) setOwnExpanded(next);\n onExpandedChange?.(next);\n };\n\n const mark =\n status ??\n (unread ? <StatusDot tone=\"brand\" size=\"sm\" className={align === \"start\" ? \"mt-1.5\" : undefined} /> : null);\n\n const body = (\n <>\n {Icon && (\n <Icon\n className={cn(\"size-4 shrink-0 text-muted\", align === \"start\" && \"mt-0.5\")}\n aria-hidden\n />\n )}\n {leading}\n {/* With a trailing slot, a basis at Large: below it the trailing chip or figure\n wraps under the text (the target wraps, see `target`) rather than squeezing the\n now-wrapping title into a column one word wide (§4, 240 px effective width). */}\n <span className={cn(\"flex min-w-0 flex-1 flex-col\", trailing != null && \"large:basis-40\")}>\n {overline != null && (\n <span className=\"block truncate-until-large text-caption font-medium leading-snug text-muted\">\n {overline}\n </span>\n )}\n <span\n // Names the body's and the expanded content's group; harmless without one.\n id={stacked ? titleId : undefined}\n className={cn(\n \"block text-sm text-primary\",\n titleLines === 1\n ? \"truncate-until-large\"\n : titleLines === 2\n ? cn(\"line-clamp-2 break-words\", UNCLAMP_AT_LARGE)\n : \"break-words\",\n unread ? \"font-semibold\" : \"font-medium\",\n )}\n >\n {title}\n {unread && <span className=\"sr-only\"> ({labels.unread})</span>}\n {external && href !== undefined && <span className=\"sr-only\"> ({labels.opensInNewTab})</span>}\n </span>\n {subtitle != null && (\n <span\n className={cn(\n \"block text-xs text-muted\",\n subtitleLines === 2 ? cn(\"line-clamp-2 break-words\", UNCLAMP_AT_LARGE) : \"truncate-until-large\",\n )}\n >\n {subtitle}\n </span>\n )}\n {meta != null && (\n <span\n className={cn(\n \"mt-0.5 text-caption leading-snug text-muted\",\n metaWrap ? \"flex flex-wrap items-center gap-1\" : \"block truncate-until-large\",\n )}\n >\n {meta}\n </span>\n )}\n {content != null && <span className=\"mt-1.5 block min-w-0\">{content}</span>}\n </span>\n {trailing != null && (\n <span\n className={cn(\n \"flex shrink-0 items-center gap-2 text-xs large:ms-auto\",\n trailingTone !== \"inherit\" && toneTextClass(trailingTone),\n )}\n >\n {trailing}\n </span>\n )}\n {loading ? (\n <Spinner label={null} className=\"size-3.5 shrink-0\" />\n ) : (\n external && href !== undefined && (\n <ExternalLink className=\"size-3.5 shrink-0 text-muted rtl:-scale-x-100\" aria-hidden />\n )\n )}\n {mark}\n {expandable && (\n // Turned, not swapped — the Disclosure card's convention: down while shut, up\n // while open. A rotation is its own mirror, so RTL needs nothing.\n <ChevronDown\n aria-hidden\n className={cn(\n \"size-4 shrink-0 text-muted transition-transform duration-200 ease-out motion-reduce:transition-none\",\n align === \"start\" && \"mt-0.5\",\n expanded && \"rotate-180\",\n )}\n />\n )}\n </>\n );\n\n const target = cn(\n \"flex min-w-0 flex-1 text-start\",\n // §4 at Large: the trailing slot drops under the text when both no longer fit.\n trailing != null && \"large:flex-wrap\",\n align === \"start\" ? \"items-start\" : \"items-center\",\n pad.target,\n // When stacked the target is the top of the box, not all of it, so it takes its\n // own corners and its own hover fill (see `children`).\n stacked ? \"rounded-md\" : \"rounded-[inherit]\",\n stacked && interactive && !inert && !selected && \"transition-colors hover:bg-hover\",\n interactive && TARGET_RING,\n interactive && !inert && \"cursor-pointer\",\n loading && \"cursor-progress\",\n locked && !loading && \"cursor-not-allowed\",\n targetClassName,\n );\n\n // `role` is the row's own (a button, a link, or nothing on a static block): a\n // targetProps role would silently turn the row into something else.\n const { role: _ignoredRole, ...targetExtras } = targetProps ?? {};\n\n let main: ReactNode;\n if (isLink) {\n const linkProps: ListItemLinkProps = {\n ...(targetExtras as Partial<ListItemLinkProps>),\n ...rest,\n ref: ref as Ref<HTMLAnchorElement>,\n href,\n className: target,\n \"aria-current\": selected ? \"true\" : undefined,\n \"aria-busy\": loading || undefined,\n onClick: loading\n ? (e) => e.preventDefault()\n : (onClick as ListItemLinkProps[\"onClick\"]),\n onAuxClick,\n children: body,\n };\n const { children: linkBody, ...anchorProps } = linkProps;\n const render = external ? undefined : pickLinkRenderer(renderLink, kitLink, href);\n main = render ? (\n <RenderedLink render={render} {...linkProps} />\n ) : (\n <a {...anchorProps} {...(external ? { target: \"_blank\", rel: \"noopener noreferrer\" } : null)}>\n {linkBody}\n </a>\n );\n } else if (isButton) {\n main = (\n <button\n {...(targetExtras as HTMLAttributes<HTMLButtonElement>)}\n {...rest}\n ref={ref as Ref<HTMLButtonElement>}\n type=\"button\"\n // A disabled row is `disabled`: nothing to do there, and nothing to reach. A\n // loading row is only `aria-disabled` — see `loading` — and so is a locked one:\n // a reason wins over `disabled`, so the row stays reachable to say it.\n disabled={locked ? undefined : disabled}\n aria-disabled={loading || locked || undefined}\n aria-busy={loading || undefined}\n aria-current={selected ? \"true\" : undefined}\n aria-expanded={expandable ? expanded : undefined}\n aria-controls={expandable ? expandedId : undefined}\n aria-describedby={\n locked\n ? mergeDescribedBy(rest[\"aria-describedby\"] as string | undefined, lock.reasonId)\n : (rest[\"aria-describedby\"] as string | undefined)\n }\n onClick={\n loading || locked\n ? undefined\n : expandable\n ? toggle\n : (onClick as (event: MouseEvent<HTMLButtonElement>) => void)\n }\n className={target}\n >\n {body}\n </button>\n );\n if (locked) {\n // The frame takes the target's place in the row, so it takes its flex sizing too;\n // the button fills it (`items-stretch` in the line's column). The line takes the\n // row's body padding, so its words start where the title's do.\n main = (\n <LockedReason\n lock={lock}\n display={disabledReasonDisplay}\n className=\"flex min-w-0 flex-1 items-stretch\"\n lineClassName={pad.body}\n >\n {main}\n </LockedReason>\n );\n }\n } else {\n main = (\n <div\n {...(targetExtras as HTMLAttributes<HTMLDivElement>)}\n {...rest}\n ref={ref as Ref<HTMLDivElement>}\n aria-disabled={disabled || undefined}\n aria-busy={loading || undefined}\n className={target}\n >\n {body}\n </div>\n );\n }\n\n const leadingActionsEl =\n leadingActions != null ? (\n <div className={cn(\"flex shrink-0 items-center\", pad.gap, pad.leadingActions)}>{leadingActions}</div>\n ) : null;\n const actionsEl =\n actions != null ? <div className={cn(\"flex shrink-0 items-center\", pad.gap, pad.actions)}>{actions}</div> : null;\n\n const row = (\n <div\n className={cn(\n \"flex min-w-0 rounded-md border transition-colors\",\n stacked ? \"flex-col\" : \"items-center\",\n // See `renderRow`: the wrapper may be inline, the row is not.\n renderRow && \"w-full\",\n selected\n ? \"border-brand bg-surface-2\"\n : bordered\n ? \"border-subtle\"\n : \"border-transparent\",\n !stacked && interactive && !inert && !selected && \"hover:bg-hover\",\n (disabled || locked) && \"opacity-50\",\n className,\n )}\n >\n {stacked ? (\n <>\n <div className=\"flex min-w-0 items-center\">\n {leadingActionsEl}\n {main}\n {actionsEl}\n </div>\n {expandable && (\n <Collapse open={expanded}>\n <div id={expandedId} role=\"group\" aria-labelledby={titleId} className={cn(\"min-w-0\", pad.body)}>\n {expandedContent}\n </div>\n </Collapse>\n )}\n {hasBody && (\n <div role=\"group\" aria-labelledby={titleId} className={cn(\"min-w-0\", pad.body, bodyClassName)}>\n {children}\n </div>\n )}\n </>\n ) : (\n <>\n {leadingActionsEl}\n {main}\n {actionsEl}\n </>\n )}\n </div>\n );\n\n const Tag = as;\n return (\n <Tag\n className={cn(\n list.separator === \"divider\" && \"border-b border-subtle last:border-b-0\",\n list.separator === \"divider\" && as === \"li\" && \"py-0.5\",\n renderRow && \"flex flex-col\",\n )}\n >\n {renderRow ? renderRow(row) : row}\n </Tag>\n );\n}\n\nfunction RenderedLink({\n render,\n ...props\n}: ListItemLinkProps & { render: (props: ListItemLinkProps) => ReactElement }) {\n return render(props);\n}\n"],"mappings":"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","names":[]}
@@ -47,7 +47,7 @@ const roleUtilities = {
47
47
  };
48
48
  }
49
49
  };
50
- const VERSION = true ? "0.51.3" : "0.0.0-source";
50
+ const VERSION = true ? "0.51.4" : "0.0.0-source";
51
51
  const recommended = {
52
52
  name: "@eifi1/ui-kit/recommended",
53
53
  // Not the tests, as check:tokens skips the kit's: a test names the old class to assert it
@@ -221,7 +221,9 @@ export interface FeedbackColumnsOptions {
221
221
  * - Fitted to the table's width (0.48, keksdose live #451): Subject `grow`s and
222
222
  * ellipsises, and a narrow table leaves out URL, then Email, then Resolved
223
223
  * (`fitPriority` 1–3) — the open report repeats all three — then Votes and Category
224
- * (4, 5) before Subject would drop below 10rem. Drop the two keys with
224
+ * (4, 5), then User, Date and # (6–8, 0.51.4) before Subject would drop below 8rem
225
+ * (`growMin`, 0.51.4; the table's default floor is 10).
226
+ * Drop the two keys with
225
227
  * `{ ...col, grow: undefined, fitPriority: undefined }` for the 0.47 layout.
226
228
  * - `/my-feedback` shows the same nine: the Email cell's context fallback exists for it
227
229
  * (the admin list's join is absent there, §3.1), and its phone card drops the submitter
@@ -159,14 +159,19 @@ function feedbackColumns({
159
159
  cell: (row) => /* @__PURE__ */ jsx("span", { className: "font-mono text-xs text-muted", children: row.id }),
160
160
  sortBy: (row) => row.id,
161
161
  filter: { type: "number", getValue: (row) => row.id },
162
- mobileHidden: true
162
+ mobileHidden: true,
163
+ // The very last to go (0.51.4, Kurvenschmiede): replies and the feedback loop name a
164
+ // report by its number, so it stays while anything else can give way.
165
+ fitPriority: 8
163
166
  },
164
167
  {
165
168
  key: "date",
166
169
  header: labels.columnDate,
167
170
  cell: (row) => /* @__PURE__ */ jsx(FeedbackDate, { value: row.created_at, renderDate }),
168
171
  sortBy: (row) => instant(row.created_at),
169
- filter: { type: "date", getValue: (row) => row.created_at }
172
+ filter: { type: "date", getValue: (row) => row.created_at },
173
+ // Seventh to go (0.51.4): the open report shows it.
174
+ fitPriority: 7
170
175
  },
171
176
  {
172
177
  key: "category",
@@ -196,7 +201,12 @@ function feedbackColumns({
196
201
  mobilePrimary: true,
197
202
  // The rest of the row, one line and an ellipsis (0.48, keksdose live #451: one long
198
203
  // crash title pushed Status and the row actions off the edge).
199
- grow: true
204
+ grow: true,
205
+ // 8rem, not the table's 10 (0.51.4, Kurvenschmiede): at 768px beside a sidebar only
206
+ // Subject, Status (a fixed 13.5rem) and the row actions are left, and they were up
207
+ // to 20px too wide (Hungarian). A subject's first words carry it; the open report
208
+ // has the rest.
209
+ growMin: 8
200
210
  },
201
211
  {
202
212
  key: "user",
@@ -212,7 +222,11 @@ function feedbackColumns({
212
222
  sortBy: (row) => feedbackSubmitter(row, labels),
213
223
  // Name AND email, so either finds the person; the erased ones by the placeholder
214
224
  // (their context holds no name any more).
215
- filterBy: (row) => row.user_id == null ? labels.deletedUser : `${feedbackSubmitter(row, labels)} ${feedbackSubmitterEmail(row)}`
225
+ filterBy: (row) => row.user_id == null ? labels.deletedUser : `${feedbackSubmitter(row, labels)} ${feedbackSubmitterEmail(row)}`,
226
+ // Sixth to go (0.51.4, Kurvenschmiede): with only 1–5, Subject reached its floor while
227
+ // #, Date and User were still drawn, and the inbox scrolled sideways from 768 to
228
+ // ~1115px beside an open sidebar. The open report names the submitter.
229
+ fitPriority: 6
216
230
  },
217
231
  {
218
232
  key: "email",