@eifi1/ui-kit 0.15.5 → 0.16.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.
Files changed (140) hide show
  1. package/README.md +17 -12
  2. package/dist/chart.d.ts +3 -3
  3. package/dist/components/amount-input.d.ts +10 -3
  4. package/dist/components/amount-input.js +165 -156
  5. package/dist/components/amount-input.js.map +1 -1
  6. package/dist/components/authed-image.d.ts +14 -3
  7. package/dist/components/authed-image.js +32 -23
  8. package/dist/components/authed-image.js.map +1 -1
  9. package/dist/components/button-group.d.ts +3 -3
  10. package/dist/components/calculator.d.ts +3 -3
  11. package/dist/components/copy-button.d.ts +3 -3
  12. package/dist/components/facing-pair.d.ts +3 -3
  13. package/dist/components/file-button.d.ts +3 -3
  14. package/dist/components/file-button.js +64 -31
  15. package/dist/components/file-button.js.map +1 -1
  16. package/dist/components/file-dropzone.d.ts +3 -3
  17. package/dist/components/form-actions.d.ts +4 -4
  18. package/dist/components/form-actions.js +18 -6
  19. package/dist/components/form-actions.js.map +1 -1
  20. package/dist/components/line-items.d.ts +53 -2
  21. package/dist/components/line-items.js +73 -7
  22. package/dist/components/line-items.js.map +1 -1
  23. package/dist/components/list.d.ts +99 -5
  24. package/dist/components/list.js +103 -38
  25. package/dist/components/list.js.map +1 -1
  26. package/dist/components/loading-state.d.ts +19 -3
  27. package/dist/components/loading-state.js +14 -3
  28. package/dist/components/loading-state.js.map +1 -1
  29. package/dist/components/money-field.d.ts +3 -3
  30. package/dist/components/number-field.d.ts +3 -3
  31. package/dist/components/number-input.d.ts +3 -3
  32. package/dist/components/number-input.js +4 -2
  33. package/dist/components/number-input.js.map +1 -1
  34. package/dist/components/numpad-sheet.d.ts +3 -3
  35. package/dist/components/passkeys-setting.d.ts +30 -10
  36. package/dist/components/passkeys-setting.js +19 -11
  37. package/dist/components/passkeys-setting.js.map +1 -1
  38. package/dist/components/progress-bar.d.ts +12 -3
  39. package/dist/components/progress-bar.js +21 -11
  40. package/dist/components/progress-bar.js.map +1 -1
  41. package/dist/components/series-chart.d.ts +3 -3
  42. package/dist/components/settings-fields.d.ts +3 -3
  43. package/dist/components/signed-amount.d.ts +28 -4
  44. package/dist/components/signed-amount.js +17 -8
  45. package/dist/components/signed-amount.js.map +1 -1
  46. package/dist/components/stat-tile.d.ts +31 -2
  47. package/dist/components/stat-tile.js +37 -6
  48. package/dist/components/stat-tile.js.map +1 -1
  49. package/dist/components/status-dot.d.ts +4 -1
  50. package/dist/components/status-dot.js +20 -1
  51. package/dist/components/status-dot.js.map +1 -1
  52. package/dist/components/swatch-picker.d.ts +3 -1
  53. package/dist/components/swatch-picker.js.map +1 -1
  54. package/dist/components/table.d.ts +18 -13
  55. package/dist/components/table.js +20 -5
  56. package/dist/components/table.js.map +1 -1
  57. package/dist/components/text-link.d.ts +3 -3
  58. package/dist/components/time-input.d.ts +3 -3
  59. package/dist/components/toggle-legend.d.ts +12 -1
  60. package/dist/components/toggle-legend.js +6 -2
  61. package/dist/components/toggle-legend.js.map +1 -1
  62. package/dist/components/ui.d.ts +3 -3
  63. package/dist/components/ui.js +149 -45
  64. package/dist/components/ui.js.map +1 -1
  65. package/dist/feedback/feedback-attachment.d.ts +1 -1
  66. package/dist/feedback/feedback-attachment.js +24 -11
  67. package/dist/feedback/feedback-attachment.js.map +1 -1
  68. package/dist/feedback/feedback-dialog.d.ts +1 -1
  69. package/dist/feedback/feedback-dialog.js +6 -5
  70. package/dist/feedback/feedback-dialog.js.map +1 -1
  71. package/dist/feedback/feedback-inbox.d.ts +1 -1
  72. package/dist/feedback/feedback-thread.d.ts +1 -1
  73. package/dist/{feedback-attachment-BYAHJMOr.d.ts → feedback-attachment-WFiXJ8We.d.ts} +21 -8
  74. package/dist/feedback.d.ts +1 -1
  75. package/dist/hooks/use-file-drop.d.ts +3 -3
  76. package/dist/i18n/defaults.d.ts +3 -3
  77. package/dist/i18n/kit-labels.d.ts +2 -2
  78. package/dist/i18n/locales/de-CH-informal.d.ts +3 -3
  79. package/dist/i18n/locales/de-CH.d.ts +3 -3
  80. package/dist/i18n/locales/de-informal.d.ts +3 -3
  81. package/dist/i18n/locales/de.d.ts +3 -3
  82. package/dist/i18n/locales/de.js +1 -1
  83. package/dist/i18n/locales/de.js.map +1 -1
  84. package/dist/i18n/locales/es.d.ts +3 -3
  85. package/dist/i18n/locales/es.js +1 -1
  86. package/dist/i18n/locales/es.js.map +1 -1
  87. package/dist/i18n/locales/fr.d.ts +3 -3
  88. package/dist/i18n/locales/fr.js +1 -1
  89. package/dist/i18n/locales/fr.js.map +1 -1
  90. package/dist/i18n/locales/hu.d.ts +3 -3
  91. package/dist/i18n/locales/hu.js +1 -1
  92. package/dist/i18n/locales/hu.js.map +1 -1
  93. package/dist/i18n/locales/it.d.ts +3 -3
  94. package/dist/i18n/locales/it.js +1 -1
  95. package/dist/i18n/locales/it.js.map +1 -1
  96. package/dist/i18n/locales/zh.d.ts +3 -3
  97. package/dist/i18n/locales/zh.js +1 -1
  98. package/dist/i18n/locales/zh.js.map +1 -1
  99. package/dist/index.d.ts +10 -10
  100. package/dist/{kit-labels-BD2F7sOz.d.ts → kit-labels-DE--LmCk.d.ts} +153 -12
  101. package/dist/lib/format.d.ts +18 -3
  102. package/dist/lib/format.js +2 -1
  103. package/dist/lib/format.js.map +1 -1
  104. package/dist/rhf/fields.d.ts +3 -3
  105. package/dist/rhf/form.d.ts +3 -3
  106. package/dist/rhf/line-items.js.map +1 -1
  107. package/dist/rhf.d.ts +3 -3
  108. package/dist/shell/app-shell.d.ts +3 -3
  109. package/dist/shell/top-bar-brand.d.ts +3 -3
  110. package/dist/shell.d.ts +3 -3
  111. package/dist/wizard/stepper-nav.d.ts +3 -3
  112. package/dist/wizard.d.ts +3 -3
  113. package/package.json +1 -1
  114. package/src/components/amount-input.tsx +39 -15
  115. package/src/components/authed-image.tsx +20 -2
  116. package/src/components/file-button.tsx +77 -27
  117. package/src/components/form-actions.tsx +86 -8
  118. package/src/components/line-items.tsx +131 -14
  119. package/src/components/list.tsx +240 -50
  120. package/src/components/loading-state.tsx +35 -4
  121. package/src/components/number-input.tsx +18 -2
  122. package/src/components/passkeys-setting.tsx +45 -18
  123. package/src/components/progress-bar.tsx +46 -21
  124. package/src/components/signed-amount.tsx +47 -8
  125. package/src/components/stat-tile.tsx +76 -6
  126. package/src/components/status-dot.tsx +31 -0
  127. package/src/components/swatch-picker.tsx +3 -1
  128. package/src/components/table.tsx +42 -15
  129. package/src/components/toggle-legend.tsx +21 -4
  130. package/src/components/ui.tsx +273 -63
  131. package/src/feedback/feedback-attachment.tsx +45 -14
  132. package/src/feedback/feedback-dialog.tsx +31 -16
  133. package/src/i18n/locales/de.ts +1 -1
  134. package/src/i18n/locales/es.ts +1 -1
  135. package/src/i18n/locales/fr.ts +1 -1
  136. package/src/i18n/locales/hu.ts +1 -1
  137. package/src/i18n/locales/it.ts +1 -1
  138. package/src/i18n/locales/zh.ts +1 -1
  139. package/src/lib/format.ts +24 -3
  140. package/src/rhf/line-items.tsx +6 -0
@@ -1,10 +1,12 @@
1
1
  "use client";
2
2
  import { Fragment, jsx, jsxs } from "react/jsx-runtime";
3
- import { createContext, forwardRef, useContext, useId } from "react";
4
- import { ExternalLink } from "lucide-react";
3
+ import { createContext, forwardRef, useContext, useId, useState } from "react";
4
+ import { ChevronDown, ExternalLink } from "lucide-react";
5
5
  import { cn } from "../lib/cn.js";
6
6
  import { Spinner } from "./ui.js";
7
7
  import { StatusDot } from "./status-dot.js";
8
+ import { Collapse } from "./disclosure.js";
9
+ import { toneTextClass } from "./signed-amount.js";
8
10
  import { useKitLabels, useKitLink } from "../i18n/kit-labels.js";
9
11
  import { pickLinkRenderer } from "./text-link.js";
10
12
  const DEFAULT_LIST_LABELS = {
@@ -33,21 +35,26 @@ const List = forwardRef(function List2({ as = "ul", density = "default", separat
33
35
  ) });
34
36
  });
35
37
  const PAD = {
36
- compact: { target: "gap-2 px-2 py-1.5", actions: "pe-1.5", gap: "gap-0.5", body: "px-2 pb-1.5" },
37
- default: { target: "gap-3 px-3 py-2.5", actions: "pe-2", gap: "gap-1", body: "px-3 pb-2.5" },
38
- comfortable: { target: "gap-3 px-4 py-3", actions: "pe-3", gap: "gap-1", body: "px-4 pb-3" }
38
+ 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" },
39
+ default: { target: "gap-3 px-3 py-2.5", actions: "pe-2", leadingActions: "ps-2", gap: "gap-1", body: "px-3 pb-2.5" },
40
+ comfortable: { target: "gap-3 px-4 py-3", actions: "pe-3", leadingActions: "ps-3", gap: "gap-1", body: "px-4 pb-3" }
39
41
  };
40
42
  const TARGET_RING = "focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-[var(--brand)]";
41
43
  const ListItem = forwardRef(function ListItem2({
42
44
  title,
43
45
  titleLines = 1,
46
+ overline,
44
47
  subtitle,
45
48
  subtitleLines = 1,
46
49
  meta,
50
+ metaWrap = false,
51
+ content,
47
52
  icon: Icon,
48
53
  leading,
49
54
  trailing,
55
+ trailingTone = "muted",
50
56
  actions,
57
+ leadingActions,
51
58
  unread = false,
52
59
  status,
53
60
  selected = false,
@@ -60,6 +67,7 @@ const ListItem = forwardRef(function ListItem2({
60
67
  targetProps,
61
68
  bordered = false,
62
69
  as = "li",
70
+ renderRow,
63
71
  children,
64
72
  bodyClassName,
65
73
  href,
@@ -67,19 +75,33 @@ const ListItem = forwardRef(function ListItem2({
67
75
  external = false,
68
76
  onClick,
69
77
  onAuxClick,
78
+ expandedContent,
79
+ expanded: expandedProp,
80
+ defaultExpanded = false,
81
+ onExpandedChange,
70
82
  ...rest
71
83
  }, ref) {
72
84
  const list = useContext(ListContext);
73
85
  const labels = useKitLabels("list", DEFAULT_LIST_LABELS);
74
86
  const kitLink = useKitLink();
87
+ const [ownExpanded, setOwnExpanded] = useState(defaultExpanded);
75
88
  const density = densityProp ?? list.density;
76
89
  const pad = PAD[density];
90
+ const expandable = expandedContent !== void 0;
91
+ const expanded = expandable && (expandedProp ?? ownExpanded);
77
92
  const isLink = href !== void 0 && !disabled;
78
- const isButton = !isLink && onClick !== void 0 && href === void 0;
93
+ const isButton = !isLink && (onClick !== void 0 || expandable) && href === void 0;
79
94
  const interactive = isLink || isButton;
80
95
  const inert = disabled || loading;
81
96
  const hasBody = children !== void 0 && children !== null && children !== false;
97
+ const stacked = hasBody || expandable;
82
98
  const titleId = useId();
99
+ const expandedId = useId();
100
+ const toggle = () => {
101
+ const next = !expanded;
102
+ if (expandedProp === void 0) setOwnExpanded(next);
103
+ onExpandedChange?.(next);
104
+ };
83
105
  const mark = status ?? (unread ? /* @__PURE__ */ jsx(StatusDot, { tone: "brand", size: "sm", className: align === "start" ? "mt-1.5" : void 0 }) : null);
84
106
  const body = /* @__PURE__ */ jsxs(Fragment, { children: [
85
107
  Icon && /* @__PURE__ */ jsx(
@@ -91,10 +113,11 @@ const ListItem = forwardRef(function ListItem2({
91
113
  ),
92
114
  leading,
93
115
  /* @__PURE__ */ jsxs("span", { className: "flex min-w-0 flex-1 flex-col", children: [
116
+ overline != null && /* @__PURE__ */ jsx("span", { className: "block truncate text-[11px] font-medium leading-snug text-[var(--text-muted)]", children: overline }),
94
117
  /* @__PURE__ */ jsxs(
95
118
  "span",
96
119
  {
97
- id: hasBody ? titleId : void 0,
120
+ id: stacked ? titleId : void 0,
98
121
  className: cn(
99
122
  "block text-sm text-[var(--text-primary)]",
100
123
  titleLines === 1 ? "truncate" : titleLines === 2 ? "line-clamp-2 break-words" : "break-words",
@@ -125,20 +148,52 @@ const ListItem = forwardRef(function ListItem2({
125
148
  children: subtitle
126
149
  }
127
150
  ),
128
- meta != null && /* @__PURE__ */ jsx("span", { className: "mt-0.5 block truncate text-[11px] leading-snug text-[var(--text-muted)]", children: meta })
151
+ meta != null && /* @__PURE__ */ jsx(
152
+ "span",
153
+ {
154
+ className: cn(
155
+ "mt-0.5 text-[11px] leading-snug text-[var(--text-muted)]",
156
+ metaWrap ? "flex flex-wrap items-center gap-1" : "block truncate"
157
+ ),
158
+ children: meta
159
+ }
160
+ ),
161
+ content != null && /* @__PURE__ */ jsx("span", { className: "mt-1.5 block min-w-0", children: content })
129
162
  ] }),
130
- trailing != null && /* @__PURE__ */ jsx("span", { className: "flex shrink-0 items-center gap-2 text-xs text-[var(--text-muted)]", children: trailing }),
163
+ trailing != null && /* @__PURE__ */ jsx(
164
+ "span",
165
+ {
166
+ className: cn(
167
+ "flex shrink-0 items-center gap-2 text-xs",
168
+ trailingTone !== "inherit" && toneTextClass(trailingTone)
169
+ ),
170
+ children: trailing
171
+ }
172
+ ),
131
173
  loading ? /* @__PURE__ */ jsx(Spinner, { label: null, className: "size-3.5 shrink-0" }) : external && href !== void 0 && /* @__PURE__ */ jsx(ExternalLink, { className: "size-3.5 shrink-0 text-[var(--text-muted)] rtl:-scale-x-100", "aria-hidden": true }),
132
- mark
174
+ mark,
175
+ expandable && // Turned, not swapped — the Disclosure card's convention: down while shut, up
176
+ // while open. A rotation is its own mirror, so RTL needs nothing.
177
+ /* @__PURE__ */ jsx(
178
+ ChevronDown,
179
+ {
180
+ "aria-hidden": true,
181
+ className: cn(
182
+ "size-4 shrink-0 text-[var(--text-muted)] transition-transform duration-200 ease-out motion-reduce:transition-none",
183
+ align === "start" && "mt-0.5",
184
+ expanded && "rotate-180"
185
+ )
186
+ }
187
+ )
133
188
  ] });
134
189
  const target = cn(
135
190
  "flex min-w-0 flex-1 text-start",
136
191
  align === "start" ? "items-start" : "items-center",
137
192
  pad.target,
138
- // With a body the target is the top of the box, not all of it, so it takes its
193
+ // When stacked the target is the top of the box, not all of it, so it takes its
139
194
  // own corners and its own hover fill (see `children`).
140
- hasBody ? "rounded-md" : "rounded-[inherit]",
141
- hasBody && interactive && !inert && !selected && "transition-colors hover:bg-[var(--bg-hover)]",
195
+ stacked ? "rounded-md" : "rounded-[inherit]",
196
+ stacked && interactive && !inert && !selected && "transition-colors hover:bg-[var(--bg-hover)]",
142
197
  interactive && TARGET_RING,
143
198
  interactive && !inert && "cursor-pointer",
144
199
  loading && "cursor-progress",
@@ -174,7 +229,9 @@ const ListItem = forwardRef(function ListItem2({
174
229
  "aria-disabled": loading || void 0,
175
230
  "aria-busy": loading || void 0,
176
231
  "aria-current": selected ? "true" : void 0,
177
- onClick: loading ? void 0 : onClick,
232
+ "aria-expanded": expandable ? expanded : void 0,
233
+ "aria-controls": expandable ? expandedId : void 0,
234
+ onClick: loading ? void 0 : expandable ? toggle : onClick,
178
235
  className: target,
179
236
  children: body
180
237
  }
@@ -193,38 +250,46 @@ const ListItem = forwardRef(function ListItem2({
193
250
  }
194
251
  );
195
252
  }
253
+ const leadingActionsEl = leadingActions != null ? /* @__PURE__ */ jsx("div", { className: cn("flex shrink-0 items-center", pad.gap, pad.leadingActions), children: leadingActions }) : null;
196
254
  const actionsEl = actions != null ? /* @__PURE__ */ jsx("div", { className: cn("flex shrink-0 items-center", pad.gap, pad.actions), children: actions }) : null;
255
+ const row = /* @__PURE__ */ jsx(
256
+ "div",
257
+ {
258
+ className: cn(
259
+ "flex min-w-0 rounded-md border transition-colors",
260
+ stacked ? "flex-col" : "items-center",
261
+ // See `renderRow`: the wrapper may be inline, the row is not.
262
+ renderRow && "w-full",
263
+ selected ? "border-[var(--brand)] bg-[var(--bg-surface-2)]" : bordered ? "border-[var(--border)]" : "border-transparent",
264
+ !stacked && interactive && !inert && !selected && "hover:bg-[var(--bg-hover)]",
265
+ disabled && "opacity-50",
266
+ className
267
+ ),
268
+ children: stacked ? /* @__PURE__ */ jsxs(Fragment, { children: [
269
+ /* @__PURE__ */ jsxs("div", { className: "flex min-w-0 items-center", children: [
270
+ leadingActionsEl,
271
+ main,
272
+ actionsEl
273
+ ] }),
274
+ expandable && /* @__PURE__ */ jsx(Collapse, { open: expanded, children: /* @__PURE__ */ jsx("div", { id: expandedId, role: "group", "aria-labelledby": titleId, className: cn("min-w-0", pad.body), children: expandedContent }) }),
275
+ hasBody && /* @__PURE__ */ jsx("div", { role: "group", "aria-labelledby": titleId, className: cn("min-w-0", pad.body, bodyClassName), children })
276
+ ] }) : /* @__PURE__ */ jsxs(Fragment, { children: [
277
+ leadingActionsEl,
278
+ main,
279
+ actionsEl
280
+ ] })
281
+ }
282
+ );
197
283
  const Tag = as;
198
284
  return /* @__PURE__ */ jsx(
199
285
  Tag,
200
286
  {
201
287
  className: cn(
202
288
  list.separator === "divider" && "border-b border-[var(--border)] last:border-b-0",
203
- list.separator === "divider" && as === "li" && "py-0.5"
289
+ list.separator === "divider" && as === "li" && "py-0.5",
290
+ renderRow && "flex flex-col"
204
291
  ),
205
- children: /* @__PURE__ */ jsx(
206
- "div",
207
- {
208
- className: cn(
209
- "flex min-w-0 rounded-md border transition-colors",
210
- hasBody ? "flex-col" : "items-center",
211
- selected ? "border-[var(--brand)] bg-[var(--bg-surface-2)]" : bordered ? "border-[var(--border)]" : "border-transparent",
212
- !hasBody && interactive && !inert && !selected && "hover:bg-[var(--bg-hover)]",
213
- disabled && "opacity-50",
214
- className
215
- ),
216
- children: hasBody ? /* @__PURE__ */ jsxs(Fragment, { children: [
217
- /* @__PURE__ */ jsxs("div", { className: "flex min-w-0 items-center", children: [
218
- main,
219
- actionsEl
220
- ] }),
221
- /* @__PURE__ */ jsx("div", { role: "group", "aria-labelledby": titleId, className: cn("min-w-0", pad.body, bodyClassName), children })
222
- ] }) : /* @__PURE__ */ jsxs(Fragment, { children: [
223
- main,
224
- actionsEl
225
- ] })
226
- }
227
- )
292
+ children: renderRow ? renderRow(row) : row
228
293
  }
229
294
  );
230
295
  });
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/components/list.tsx"],"sourcesContent":["import { createContext, forwardRef, useContext, useId } from \"react\";\nimport type { ComponentPropsWithoutRef, HTMLAttributes, MouseEvent, ReactElement, ReactNode, Ref } from \"react\";\nimport { ExternalLink } from \"lucide-react\";\nimport type { LucideIcon } from \"lucide-react\";\nimport { cn } from \"../lib/cn\";\nimport { Spinner } from \"./ui\";\nimport { StatusDot } from \"./status-dot\";\nimport { useKitLabels, useKitLink } from \"../i18n/kit-labels\";\nimport { pickLinkRenderer } from \"./text-link\";\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\nexport const DEFAULT_LIST_LABELS: ListLabels = {\n unread: \"Unread\",\n opensInNewTab: \"opens in a new tab\",\n};\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}\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 const List = forwardRef<HTMLUListElement, ListProps>(function List(\n { as = \"ul\", density = \"default\", separator = \"gap\", className, children, ...rest },\n ref,\n) {\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<ListDensity, { target: string; actions: string; gap: string; body: string }> = {\n compact: { target: \"gap-2 px-2 py-1.5\", actions: \"pe-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\", gap: \"gap-1\", body: \"px-3 pb-2.5\" },\n comfortable: { target: \"gap-3 px-4 py-3\", actions: \"pe-3\", gap: \"gap-1\", body: \"px-4 pb-3\" },\n};\n\nconst TARGET_RING =\n \"focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-[var(--brand)]\";\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 /** A third line in the caption type: a timestamp, \"Updated …\" (lenkbank's projects). */\n meta?: 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 * 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 * 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 /** 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}\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 href?: never;\n renderLink?: never;\n external?: never;\n onAuxClick?: never;\n }\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 }\n | {\n onClick?: never;\n href?: never;\n renderLink?: never;\n external?: never;\n onAuxClick?: never;\n }\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 * 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 const ListItem = forwardRef<HTMLElement, ListItemProps>(function ListItem(\n {\n title,\n titleLines = 1,\n subtitle,\n subtitleLines = 1,\n meta,\n icon: Icon,\n leading,\n trailing,\n actions,\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 children,\n bodyClassName,\n href,\n renderLink,\n external = false,\n onClick,\n onAuxClick,\n ...rest\n },\n ref,\n) {\n const list = useContext(ListContext);\n const labels = useKitLabels(\"list\", DEFAULT_LIST_LABELS);\n const kitLink = useKitLink();\n const density = densityProp ?? list.density;\n const pad = PAD[density];\n const isLink = href !== undefined && !disabled;\n const isButton = !isLink && onClick !== undefined && href === undefined;\n const interactive = isLink || isButton;\n const inert = disabled || loading;\n const hasBody = children !== undefined && children !== null && children !== false;\n const titleId = useId();\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-[var(--text-muted)]\", align === \"start\" && \"mt-0.5\")}\n aria-hidden\n />\n )}\n {leading}\n <span className=\"flex min-w-0 flex-1 flex-col\">\n <span\n // Names the body's group; harmless without one.\n id={hasBody ? titleId : undefined}\n className={cn(\n \"block text-sm text-[var(--text-primary)]\",\n titleLines === 1 ? \"truncate\" : titleLines === 2 ? \"line-clamp-2 break-words\" : \"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-[var(--text-muted)]\",\n subtitleLines === 2 ? \"line-clamp-2\" : \"truncate\",\n )}\n >\n {subtitle}\n </span>\n )}\n {meta != null && <span className=\"mt-0.5 block truncate text-[11px] leading-snug text-[var(--text-muted)]\">{meta}</span>}\n </span>\n {trailing != null && <span className=\"flex shrink-0 items-center gap-2 text-xs text-[var(--text-muted)]\">{trailing}</span>}\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-[var(--text-muted)] rtl:-scale-x-100\" aria-hidden />\n )\n )}\n {mark}\n </>\n );\n\n const target = cn(\n \"flex min-w-0 flex-1 text-start\",\n align === \"start\" ? \"items-start\" : \"items-center\",\n pad.target,\n // With a body 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 hasBody ? \"rounded-md\" : \"rounded-[inherit]\",\n hasBody && interactive && !inert && !selected && \"transition-colors hover:bg-[var(--bg-hover)]\",\n interactive && TARGET_RING,\n interactive && !inert && \"cursor-pointer\",\n loading && \"cursor-progress\",\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`.\n disabled={disabled}\n aria-disabled={loading || undefined}\n aria-busy={loading || undefined}\n aria-current={selected ? \"true\" : undefined}\n onClick={loading ? undefined : (onClick as (event: MouseEvent<HTMLButtonElement>) => void)}\n className={target}\n >\n {body}\n </button>\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 actionsEl =\n actions != null ? <div className={cn(\"flex shrink-0 items-center\", pad.gap, pad.actions)}>{actions}</div> : null;\n\n const Tag = as;\n return (\n <Tag\n className={cn(\n list.separator === \"divider\" && \"border-b border-[var(--border)] last:border-b-0\",\n list.separator === \"divider\" && as === \"li\" && \"py-0.5\",\n )}\n >\n <div\n className={cn(\n \"flex min-w-0 rounded-md border transition-colors\",\n hasBody ? \"flex-col\" : \"items-center\",\n selected\n ? \"border-[var(--brand)] bg-[var(--bg-surface-2)]\"\n : bordered\n ? \"border-[var(--border)]\"\n : \"border-transparent\",\n !hasBody && interactive && !inert && !selected && \"hover:bg-[var(--bg-hover)]\",\n disabled && \"opacity-50\",\n className,\n )}\n >\n {hasBody ? (\n <>\n <div className=\"flex min-w-0 items-center\">\n {main}\n {actionsEl}\n </div>\n <div role=\"group\" aria-labelledby={titleId} className={cn(\"min-w-0\", pad.body, bodyClassName)}>\n {children}\n </div>\n </>\n ) : (\n <>\n {main}\n {actionsEl}\n </>\n )}\n </div>\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":["List","ListItem"]}
1
+ {"version":3,"sources":["../../src/components/list.tsx"],"sourcesContent":["import { createContext, forwardRef, 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 { Spinner } from \"./ui\";\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\";\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\nexport const DEFAULT_LIST_LABELS: ListLabels = {\n unread: \"Unread\",\n opensInNewTab: \"opens in a new tab\",\n};\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}\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 const List = forwardRef<HTMLUListElement, ListProps>(function List(\n { as = \"ul\", density = \"default\", separator = \"gap\", className, children, ...rest },\n ref,\n) {\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\nconst TARGET_RING =\n \"focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-[var(--brand)]\";\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. 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 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}\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 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 } & NotExpandable)\n | ({\n onClick?: never;\n href?: never;\n renderLink?: never;\n external?: never;\n onAuxClick?: 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 }\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 * 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 const ListItem = forwardRef<HTMLElement, ListItemProps>(function ListItem(\n {\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 expandedContent,\n expanded: expandedProp,\n defaultExpanded = false,\n onExpandedChange,\n ...rest\n },\n ref,\n) {\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 const inert = disabled || loading;\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-[var(--text-muted)]\", align === \"start\" && \"mt-0.5\")}\n aria-hidden\n />\n )}\n {leading}\n <span className=\"flex min-w-0 flex-1 flex-col\">\n {overline != null && (\n <span className=\"block truncate text-[11px] font-medium leading-snug text-[var(--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-[var(--text-primary)]\",\n titleLines === 1 ? \"truncate\" : titleLines === 2 ? \"line-clamp-2 break-words\" : \"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-[var(--text-muted)]\",\n subtitleLines === 2 ? \"line-clamp-2\" : \"truncate\",\n )}\n >\n {subtitle}\n </span>\n )}\n {meta != null && (\n <span\n className={cn(\n \"mt-0.5 text-[11px] leading-snug text-[var(--text-muted)]\",\n metaWrap ? \"flex flex-wrap items-center gap-1\" : \"block truncate\",\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\",\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-[var(--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-[var(--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 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-[var(--bg-hover)]\",\n interactive && TARGET_RING,\n interactive && !inert && \"cursor-pointer\",\n loading && \"cursor-progress\",\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`.\n disabled={disabled}\n aria-disabled={loading || 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 onClick={\n loading\n ? undefined\n : expandable\n ? toggle\n : (onClick as (event: MouseEvent<HTMLButtonElement>) => void)\n }\n className={target}\n >\n {body}\n </button>\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-[var(--brand)] bg-[var(--bg-surface-2)]\"\n : bordered\n ? \"border-[var(--border)]\"\n : \"border-transparent\",\n !stacked && interactive && !inert && !selected && \"hover:bg-[var(--bg-hover)]\",\n disabled && \"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-[var(--border)] 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":["List","ListItem"]}
@@ -2,12 +2,28 @@ import * as react from 'react';
2
2
  import { ComponentPropsWithoutRef, ReactNode } from 'react';
3
3
 
4
4
  type LoadingStateSize = "sm" | "md" | "lg";
5
+ /** Whether {@link LoadingStateProps.label} shows, or is only read out. */
6
+ type LoadingStateLabelVisibility = "visible" | "sr-only";
5
7
  interface LoadingStateProps extends Omit<ComponentPropsWithoutRef<"div">, "children" | "role"> {
6
8
  /** The visible words under the spinner, and what the status region announces.
7
- * Default: `common.loading` ("Loading…"). */
9
+ * Default: `common.loading` ("Loading…"). `null` (or `""`) hides the words — the
10
+ * same as `labelVisibility="sr-only"` with the default text, so the region still
11
+ * announces "Loading…" rather than nothing. */
8
12
  label?: ReactNode;
13
+ /**
14
+ * `sr-only`: the spinner alone shows, the label is still the status region's words
15
+ * for a screen reader — a busy row or a bubble where "Loading…" in print is noise.
16
+ * Default `visible`.
17
+ */
18
+ labelVisibility?: LoadingStateLabelVisibility;
9
19
  /** `sm` for a panel or a table body, `md` (default) for a section, `lg` for a page. */
10
20
  size?: LoadingStateSize;
21
+ /**
22
+ * Less room above and below, the size's spinner and text unchanged — `md` at `py-8`
23
+ * instead of `py-12` (`sm` `py-2`, `lg` `py-12`): the step between a panel and a
24
+ * section, for a card body that `md` makes look empty rather than loading.
25
+ */
26
+ compact?: boolean;
11
27
  /** Spinner beside the text on one line, rather than above it. */
12
28
  inline?: boolean;
13
29
  }
@@ -19,6 +35,6 @@ interface LoadingStateProps extends Omit<ComponentPropsWithoutRef<"div">, "child
19
35
  * visible text inside it, so the spinner is decorative (`label={null}`) — otherwise
20
36
  * the reader hears "Loading… Loading…", one from each.
21
37
  */
22
- declare function LoadingState({ label, size, inline, className, ...rest }: LoadingStateProps): react.JSX.Element;
38
+ declare function LoadingState({ label, labelVisibility, size, compact, inline, className, ...rest }: LoadingStateProps): react.JSX.Element;
23
39
 
24
- export { LoadingState, type LoadingStateProps, type LoadingStateSize };
40
+ export { LoadingState, type LoadingStateLabelVisibility, type LoadingStateProps, type LoadingStateSize };
@@ -6,8 +6,19 @@ import { Spinner } from "./ui.js";
6
6
  const SPINNER = { sm: "h-4 w-4", md: "h-8 w-8", lg: "h-10 w-10" };
7
7
  const TEXT = { sm: "text-xs", md: "text-sm", lg: "text-base" };
8
8
  const PAD = { sm: "py-4", md: "py-12", lg: "py-20" };
9
- function LoadingState({ label, size = "md", inline = false, className, ...rest }) {
9
+ const PAD_COMPACT = { sm: "py-2", md: "py-8", lg: "py-12" };
10
+ function LoadingState({
11
+ label,
12
+ labelVisibility = "visible",
13
+ size = "md",
14
+ compact = false,
15
+ inline = false,
16
+ className,
17
+ ...rest
18
+ }) {
10
19
  const common = useKitLabels("common", DEFAULT_COMMON_LABELS);
20
+ const hidden = label === null || label === "" || labelVisibility === "sr-only";
21
+ const words = label === null || label === void 0 || label === "" ? common.loading : label;
11
22
  return /* @__PURE__ */ jsxs(
12
23
  "div",
13
24
  {
@@ -16,12 +27,12 @@ function LoadingState({ label, size = "md", inline = false, className, ...rest }
16
27
  className: cn(
17
28
  "flex items-center justify-center text-center text-[var(--text-muted)]",
18
29
  inline ? "gap-2" : "flex-col gap-2",
19
- PAD[size],
30
+ (compact ? PAD_COMPACT : PAD)[size],
20
31
  className
21
32
  ),
22
33
  children: [
23
34
  /* @__PURE__ */ jsx(Spinner, { label: null, className: SPINNER[size] }),
24
- /* @__PURE__ */ jsx("span", { className: TEXT[size], children: label ?? common.loading })
35
+ /* @__PURE__ */ jsx("span", { className: hidden ? "sr-only" : TEXT[size], children: words })
25
36
  ]
26
37
  }
27
38
  );
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/components/loading-state.tsx"],"sourcesContent":["import type { ComponentPropsWithoutRef, ReactNode } from \"react\";\nimport { cn } from \"../lib/cn\";\nimport { DEFAULT_COMMON_LABELS, useKitLabels } from \"../i18n/kit-labels\";\nimport { Spinner } from \"./ui\";\n\nexport type LoadingStateSize = \"sm\" | \"md\" | \"lg\";\n\nexport interface LoadingStateProps extends Omit<ComponentPropsWithoutRef<\"div\">, \"children\" | \"role\"> {\n /** The visible words under the spinner, and what the status region announces.\n * Default: `common.loading` (\"Loading…\"). */\n label?: ReactNode;\n /** `sm` for a panel or a table body, `md` (default) for a section, `lg` for a page. */\n size?: LoadingStateSize;\n /** Spinner beside the text on one line, rather than above it. */\n inline?: boolean;\n}\n\nconst SPINNER: Record<LoadingStateSize, string> = { sm: \"h-4 w-4\", md: \"h-8 w-8\", lg: \"h-10 w-10\" };\nconst TEXT: Record<LoadingStateSize, string> = { sm: \"text-xs\", md: \"text-sm\", lg: \"text-base\" };\nconst PAD: Record<LoadingStateSize, string> = { sm: \"py-4\", md: \"py-12\", lg: \"py-20\" };\n\n/**\n * A spinner and the words \"Loading…\", centred where the content will be.\n *\n * kastlan's `LoadingState` (feedback/loading-state.tsx:14), which got the reading\n * right the second time: the WRAPPER is the `role=\"status\"` region and the words are\n * visible text inside it, so the spinner is decorative (`label={null}`) — otherwise\n * the reader hears \"Loading… Loading…\", one from each.\n */\nexport function LoadingState({ label, size = \"md\", inline = false, className, ...rest }: LoadingStateProps) {\n const common = useKitLabels(\"common\", DEFAULT_COMMON_LABELS);\n return (\n <div\n {...rest}\n role=\"status\"\n className={cn(\n \"flex items-center justify-center text-center text-[var(--text-muted)]\",\n inline ? \"gap-2\" : \"flex-col gap-2\",\n PAD[size],\n className,\n )}\n >\n <Spinner label={null} className={SPINNER[size]} />\n <span className={TEXT[size]}>{label ?? common.loading}</span>\n </div>\n );\n}\n"],"mappings":";AAgCI,SAUE,KAVF;AA/BJ,SAAS,UAAU;AACnB,SAAS,uBAAuB,oBAAoB;AACpD,SAAS,eAAe;AAcxB,MAAM,UAA4C,EAAE,IAAI,WAAW,IAAI,WAAW,IAAI,YAAY;AAClG,MAAM,OAAyC,EAAE,IAAI,WAAW,IAAI,WAAW,IAAI,YAAY;AAC/F,MAAM,MAAwC,EAAE,IAAI,QAAQ,IAAI,SAAS,IAAI,QAAQ;AAU9E,SAAS,aAAa,EAAE,OAAO,OAAO,MAAM,SAAS,OAAO,WAAW,GAAG,KAAK,GAAsB;AAC1G,QAAM,SAAS,aAAa,UAAU,qBAAqB;AAC3D,SACE;AAAA,IAAC;AAAA;AAAA,MACE,GAAG;AAAA,MACJ,MAAK;AAAA,MACL,WAAW;AAAA,QACT;AAAA,QACA,SAAS,UAAU;AAAA,QACnB,IAAI,IAAI;AAAA,QACR;AAAA,MACF;AAAA,MAEA;AAAA,4BAAC,WAAQ,OAAO,MAAM,WAAW,QAAQ,IAAI,GAAG;AAAA,QAChD,oBAAC,UAAK,WAAW,KAAK,IAAI,GAAI,mBAAS,OAAO,SAAQ;AAAA;AAAA;AAAA,EACxD;AAEJ;","names":[]}
1
+ {"version":3,"sources":["../../src/components/loading-state.tsx"],"sourcesContent":["import type { ComponentPropsWithoutRef, ReactNode } from \"react\";\nimport { cn } from \"../lib/cn\";\nimport { DEFAULT_COMMON_LABELS, useKitLabels } from \"../i18n/kit-labels\";\nimport { Spinner } from \"./ui\";\n\nexport type LoadingStateSize = \"sm\" | \"md\" | \"lg\";\n\n/** Whether {@link LoadingStateProps.label} shows, or is only read out. */\nexport type LoadingStateLabelVisibility = \"visible\" | \"sr-only\";\n\nexport interface LoadingStateProps extends Omit<ComponentPropsWithoutRef<\"div\">, \"children\" | \"role\"> {\n /** The visible words under the spinner, and what the status region announces.\n * Default: `common.loading` (\"Loading…\"). `null` (or `\"\"`) hides the words — the\n * same as `labelVisibility=\"sr-only\"` with the default text, so the region still\n * announces \"Loading…\" rather than nothing. */\n label?: ReactNode;\n /**\n * `sr-only`: the spinner alone shows, the label is still the status region's words\n * for a screen reader — a busy row or a bubble where \"Loading…\" in print is noise.\n * Default `visible`.\n */\n labelVisibility?: LoadingStateLabelVisibility;\n /** `sm` for a panel or a table body, `md` (default) for a section, `lg` for a page. */\n size?: LoadingStateSize;\n /**\n * Less room above and below, the size's spinner and text unchanged — `md` at `py-8`\n * instead of `py-12` (`sm` `py-2`, `lg` `py-12`): the step between a panel and a\n * section, for a card body that `md` makes look empty rather than loading.\n */\n compact?: boolean;\n /** Spinner beside the text on one line, rather than above it. */\n inline?: boolean;\n}\n\nconst SPINNER: Record<LoadingStateSize, string> = { sm: \"h-4 w-4\", md: \"h-8 w-8\", lg: \"h-10 w-10\" };\nconst TEXT: Record<LoadingStateSize, string> = { sm: \"text-xs\", md: \"text-sm\", lg: \"text-base\" };\nconst PAD: Record<LoadingStateSize, string> = { sm: \"py-4\", md: \"py-12\", lg: \"py-20\" };\nconst PAD_COMPACT: Record<LoadingStateSize, string> = { sm: \"py-2\", md: \"py-8\", lg: \"py-12\" };\n\n/**\n * A spinner and the words \"Loading…\", centred where the content will be.\n *\n * kastlan's `LoadingState` (feedback/loading-state.tsx:14), which got the reading\n * right the second time: the WRAPPER is the `role=\"status\"` region and the words are\n * visible text inside it, so the spinner is decorative (`label={null}`) — otherwise\n * the reader hears \"Loading… Loading…\", one from each.\n */\nexport function LoadingState({\n label,\n labelVisibility = \"visible\",\n size = \"md\",\n compact = false,\n inline = false,\n className,\n ...rest\n}: LoadingStateProps) {\n const common = useKitLabels(\"common\", DEFAULT_COMMON_LABELS);\n // `null` / `\"\"` used to fall through `??` to a printed \"Loading…\" (null) or leave a\n // silent region (\"\"): both now mean \"no words on screen\", and the region keeps the\n // default words to announce — a status region with nothing in it says nothing.\n const hidden = label === null || label === \"\" || labelVisibility === \"sr-only\";\n const words = label === null || label === undefined || label === \"\" ? common.loading : label;\n return (\n <div\n {...rest}\n role=\"status\"\n className={cn(\n \"flex items-center justify-center text-center text-[var(--text-muted)]\",\n inline ? \"gap-2\" : \"flex-col gap-2\",\n (compact ? PAD_COMPACT : PAD)[size],\n className,\n )}\n >\n <Spinner label={null} className={SPINNER[size]} />\n <span className={hidden ? \"sr-only\" : TEXT[size]}>{words}</span>\n </div>\n );\n}\n"],"mappings":";AA+DI,SAUE,KAVF;AA9DJ,SAAS,UAAU;AACnB,SAAS,uBAAuB,oBAAoB;AACpD,SAAS,eAAe;AA+BxB,MAAM,UAA4C,EAAE,IAAI,WAAW,IAAI,WAAW,IAAI,YAAY;AAClG,MAAM,OAAyC,EAAE,IAAI,WAAW,IAAI,WAAW,IAAI,YAAY;AAC/F,MAAM,MAAwC,EAAE,IAAI,QAAQ,IAAI,SAAS,IAAI,QAAQ;AACrF,MAAM,cAAgD,EAAE,IAAI,QAAQ,IAAI,QAAQ,IAAI,QAAQ;AAUrF,SAAS,aAAa;AAAA,EAC3B;AAAA,EACA,kBAAkB;AAAA,EAClB,OAAO;AAAA,EACP,UAAU;AAAA,EACV,SAAS;AAAA,EACT;AAAA,EACA,GAAG;AACL,GAAsB;AACpB,QAAM,SAAS,aAAa,UAAU,qBAAqB;AAI3D,QAAM,SAAS,UAAU,QAAQ,UAAU,MAAM,oBAAoB;AACrE,QAAM,QAAQ,UAAU,QAAQ,UAAU,UAAa,UAAU,KAAK,OAAO,UAAU;AACvF,SACE;AAAA,IAAC;AAAA;AAAA,MACE,GAAG;AAAA,MACJ,MAAK;AAAA,MACL,WAAW;AAAA,QACT;AAAA,QACA,SAAS,UAAU;AAAA,SAClB,UAAU,cAAc,KAAK,IAAI;AAAA,QAClC;AAAA,MACF;AAAA,MAEA;AAAA,4BAAC,WAAQ,OAAO,MAAM,WAAW,QAAQ,IAAI,GAAG;AAAA,QAChD,oBAAC,UAAK,WAAW,SAAS,YAAY,KAAK,IAAI,GAAI,iBAAM;AAAA;AAAA;AAAA,EAC3D;AAEJ;","names":[]}
@@ -2,7 +2,7 @@ import * as react from 'react';
2
2
  import { ComponentProps, Ref } from 'react';
3
3
  import { AmountInput } from './amount-input.js';
4
4
  import './calculator.js';
5
- import '../kit-labels-BD2F7sOz.js';
5
+ import '../kit-labels-DE--LmCk.js';
6
6
  import './data-table-labels.js';
7
7
  import './mini-calendar.js';
8
8
  import './calendar-heatmap.js';
@@ -34,7 +34,7 @@ import './dialog-frame.js';
34
34
  import './modal.js';
35
35
  import '../hooks/use-search-param-state.js';
36
36
  import './measured-grid.js';
37
- import '../feedback-attachment-BYAHJMOr.js';
37
+ import '../feedback-attachment-WFiXJ8We.js';
38
38
  import '../feedback/feedback-thread.js';
39
39
  import '../feedback/feedback-inbox.js';
40
40
  import './account-settings-labels.js';
@@ -42,12 +42,12 @@ import './confirm-dialog.js';
42
42
  import './floating-panel.js';
43
43
  import './bulk-action-bar.js';
44
44
  import './list.js';
45
+ import './signed-amount.js';
45
46
  import './breadcrumbs.js';
46
47
  import './toast.js';
47
48
  import './description-list.js';
48
49
  import './line-items.js';
49
50
  import './progress-bar.js';
50
- import './signed-amount.js';
51
51
  import './error-boundary.js';
52
52
  import './authed-image.js';
53
53
  import '../hooks/use-authed-src.js';
@@ -2,7 +2,7 @@ import * as react from 'react';
2
2
  import { ComponentProps, ReactNode } from 'react';
3
3
  import { NumberInput } from './number-input.js';
4
4
  import './calculator.js';
5
- import '../kit-labels-BD2F7sOz.js';
5
+ import '../kit-labels-DE--LmCk.js';
6
6
  import './data-table-labels.js';
7
7
  import './mini-calendar.js';
8
8
  import './calendar-heatmap.js';
@@ -34,7 +34,7 @@ import './dialog-frame.js';
34
34
  import './modal.js';
35
35
  import '../hooks/use-search-param-state.js';
36
36
  import './measured-grid.js';
37
- import '../feedback-attachment-BYAHJMOr.js';
37
+ import '../feedback-attachment-WFiXJ8We.js';
38
38
  import '../feedback/feedback-thread.js';
39
39
  import '../feedback/feedback-inbox.js';
40
40
  import './account-settings-labels.js';
@@ -42,12 +42,12 @@ import './confirm-dialog.js';
42
42
  import './floating-panel.js';
43
43
  import './bulk-action-bar.js';
44
44
  import './list.js';
45
+ import './signed-amount.js';
45
46
  import './breadcrumbs.js';
46
47
  import './toast.js';
47
48
  import './description-list.js';
48
49
  import './line-items.js';
49
50
  import './progress-bar.js';
50
- import './signed-amount.js';
51
51
  import './error-boundary.js';
52
52
  import './authed-image.js';
53
53
  import '../hooks/use-authed-src.js';
@@ -2,7 +2,7 @@ import * as react from 'react';
2
2
  import { ReactNode } from 'react';
3
3
  import { CalculatorButtonLabels } from './calculator.js';
4
4
  import { NumberPadSheetLabels } from './numpad-sheet.js';
5
- import '../kit-labels-BD2F7sOz.js';
5
+ import '../kit-labels-DE--LmCk.js';
6
6
  import './data-table-labels.js';
7
7
  import './mini-calendar.js';
8
8
  import './calendar-heatmap.js';
@@ -34,7 +34,7 @@ import './dialog-frame.js';
34
34
  import './modal.js';
35
35
  import '../hooks/use-search-param-state.js';
36
36
  import './measured-grid.js';
37
- import '../feedback-attachment-BYAHJMOr.js';
37
+ import '../feedback-attachment-WFiXJ8We.js';
38
38
  import '../feedback/feedback-thread.js';
39
39
  import '../feedback/feedback-inbox.js';
40
40
  import './account-settings-labels.js';
@@ -42,12 +42,12 @@ import './confirm-dialog.js';
42
42
  import './floating-panel.js';
43
43
  import './bulk-action-bar.js';
44
44
  import './list.js';
45
+ import './signed-amount.js';
45
46
  import './breadcrumbs.js';
46
47
  import './toast.js';
47
48
  import './description-list.js';
48
49
  import './line-items.js';
49
50
  import './progress-bar.js';
50
- import './signed-amount.js';
51
51
  import './error-boundary.js';
52
52
  import './authed-image.js';
53
53
  import '../hooks/use-authed-src.js';
@@ -62,6 +62,8 @@ function NumberInput({
62
62
  const endRef = useRef(null);
63
63
  const [endWidth, setEndWidth] = useState(null);
64
64
  const hasEnd = showCalc || suffix !== void 0;
65
+ const suffixChars = typeof suffix === "string" || typeof suffix === "number" ? String(suffix).length : null;
66
+ const estimatedEnd = suffixChars !== null ? `calc(${suffixChars}ch + 0.75rem${showCalc ? " + 2.25rem" : ""})` : void 0;
65
67
  useLayoutEffect(() => {
66
68
  const el = endRef.current;
67
69
  if (!hasEnd || !el || typeof ResizeObserver === "undefined") return;
@@ -131,7 +133,7 @@ function NumberInput({
131
133
  showCalc && suffix !== void 0 ? "pe-16" : showCalc ? "pe-9" : suffix !== void 0 ? "pe-8" : void 0,
132
134
  invalid && FIELD_INVALID
133
135
  ),
134
- style: hasEnd && endWidth != null ? { paddingInlineEnd: endWidth + 4 } : void 0
136
+ style: hasEnd && endWidth != null ? { paddingInlineEnd: endWidth + 4 } : estimatedEnd !== void 0 ? { paddingInlineEnd: estimatedEnd } : void 0
135
137
  }
136
138
  ),
137
139
  (showCalc || suffix !== void 0) && // One flex track for both, so the unit and the calculator sit side by side
@@ -152,7 +154,7 @@ function NumberInput({
152
154
  "span",
153
155
  {
154
156
  "aria-hidden": true,
155
- className: "pointer-events-none pe-3 text-xs font-medium text-[var(--text-muted)]",
157
+ className: "pointer-events-none pe-2 text-xs font-medium text-[var(--text-muted)]",
156
158
  children: suffix
157
159
  }
158
160
  )