@eifi1/ui-kit 0.9.0 → 0.11.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 (243) hide show
  1. package/README.md +37 -23
  2. package/dist/components/alert-banner.d.ts +57 -5
  3. package/dist/components/alert-banner.js +63 -17
  4. package/dist/components/alert-banner.js.map +1 -1
  5. package/dist/components/amount-input.d.ts +6 -0
  6. package/dist/components/breadcrumbs.d.ts +60 -0
  7. package/dist/components/breadcrumbs.js +88 -0
  8. package/dist/components/breadcrumbs.js.map +1 -0
  9. package/dist/components/bulk-action-bar.d.ts +112 -0
  10. package/dist/components/bulk-action-bar.js +196 -0
  11. package/dist/components/bulk-action-bar.js.map +1 -0
  12. package/dist/components/button-group.d.ts +58 -3
  13. package/dist/components/button-group.js +56 -5
  14. package/dist/components/button-group.js.map +1 -1
  15. package/dist/components/calculator.d.ts +6 -0
  16. package/dist/components/calendar-heatmap.d.ts +124 -0
  17. package/dist/components/calendar-heatmap.js +295 -0
  18. package/dist/components/calendar-heatmap.js.map +1 -0
  19. package/dist/components/chip.d.ts +31 -5
  20. package/dist/components/chip.js +85 -5
  21. package/dist/components/chip.js.map +1 -1
  22. package/dist/components/choice-card.d.ts +34 -2
  23. package/dist/components/choice-card.js +49 -0
  24. package/dist/components/choice-card.js.map +1 -1
  25. package/dist/components/copy-button.d.ts +14 -4
  26. package/dist/components/copy-button.js +17 -4
  27. package/dist/components/copy-button.js.map +1 -1
  28. package/dist/components/data-table.js +2 -0
  29. package/dist/components/data-table.js.map +1 -1
  30. package/dist/components/date-picker.d.ts +22 -2
  31. package/dist/components/date-picker.js +11 -3
  32. package/dist/components/date-picker.js.map +1 -1
  33. package/dist/components/description-list.d.ts +43 -6
  34. package/dist/components/description-list.js +91 -11
  35. package/dist/components/description-list.js.map +1 -1
  36. package/dist/components/dialog-frame.d.ts +7 -0
  37. package/dist/components/dialog-frame.js.map +1 -1
  38. package/dist/components/disclosure.d.ts +57 -2
  39. package/dist/components/disclosure.js +24 -12
  40. package/dist/components/disclosure.js.map +1 -1
  41. package/dist/components/field.d.ts +43 -0
  42. package/dist/components/field.js +52 -0
  43. package/dist/components/field.js.map +1 -0
  44. package/dist/components/file-button.d.ts +1 -0
  45. package/dist/components/file-dropzone.d.ts +4 -4
  46. package/dist/components/file-dropzone.js +2 -1
  47. package/dist/components/file-dropzone.js.map +1 -1
  48. package/dist/components/floating-panel.d.ts +147 -4
  49. package/dist/components/floating-panel.js +188 -28
  50. package/dist/components/floating-panel.js.map +1 -1
  51. package/dist/components/full-bleed-dialog.d.ts +23 -4
  52. package/dist/components/full-bleed-dialog.js +11 -2
  53. package/dist/components/full-bleed-dialog.js.map +1 -1
  54. package/dist/components/list.d.ts +187 -0
  55. package/dist/components/list.js +221 -0
  56. package/dist/components/list.js.map +1 -0
  57. package/dist/components/menu-item.d.ts +109 -0
  58. package/dist/components/menu-item.js +88 -0
  59. package/dist/components/menu-item.js.map +1 -0
  60. package/dist/components/mini-calendar.d.ts +70 -5
  61. package/dist/components/mini-calendar.js +166 -62
  62. package/dist/components/mini-calendar.js.map +1 -1
  63. package/dist/components/modal.d.ts +23 -1
  64. package/dist/components/modal.js +36 -10
  65. package/dist/components/modal.js.map +1 -1
  66. package/dist/components/nav-pills.d.ts +66 -0
  67. package/dist/components/nav-pills.js +65 -0
  68. package/dist/components/nav-pills.js.map +1 -0
  69. package/dist/components/number-field.d.ts +6 -0
  70. package/dist/components/number-input.d.ts +6 -0
  71. package/dist/components/numpad-sheet.d.ts +6 -0
  72. package/dist/components/page-contents.js +1 -2
  73. package/dist/components/page-contents.js.map +1 -1
  74. package/dist/components/page-header.d.ts +37 -0
  75. package/dist/components/page-header.js +36 -0
  76. package/dist/components/page-header.js.map +1 -0
  77. package/dist/components/progress-bar.d.ts +31 -2
  78. package/dist/components/progress-bar.js +73 -7
  79. package/dist/components/progress-bar.js.map +1 -1
  80. package/dist/components/series-chart-labels.d.ts +3 -0
  81. package/dist/components/series-chart-labels.js +2 -1
  82. package/dist/components/series-chart-labels.js.map +1 -1
  83. package/dist/components/series-chart.d.ts +32 -0
  84. package/dist/components/series-chart.js +141 -4
  85. package/dist/components/series-chart.js.map +1 -1
  86. package/dist/components/settings-fields.d.ts +1 -0
  87. package/dist/components/status-dot.d.ts +53 -0
  88. package/dist/components/status-dot.js +57 -0
  89. package/dist/components/status-dot.js.map +1 -0
  90. package/dist/components/table.d.ts +78 -13
  91. package/dist/components/table.js +84 -11
  92. package/dist/components/table.js.map +1 -1
  93. package/dist/components/text.d.ts +70 -0
  94. package/dist/components/text.js +24 -0
  95. package/dist/components/text.js.map +1 -0
  96. package/dist/components/time-input.d.ts +1 -0
  97. package/dist/components/toast.d.ts +167 -0
  98. package/dist/components/toast.js +226 -0
  99. package/dist/components/toast.js.map +1 -0
  100. package/dist/components/toggle-group.d.ts +44 -1
  101. package/dist/components/toggle-group.js +73 -4
  102. package/dist/components/toggle-group.js.map +1 -1
  103. package/dist/components/tooltip.d.ts +46 -15
  104. package/dist/components/tooltip.js +87 -47
  105. package/dist/components/tooltip.js.map +1 -1
  106. package/dist/components/ui.d.ts +158 -7
  107. package/dist/components/ui.js +191 -10
  108. package/dist/components/ui.js.map +1 -1
  109. package/dist/components/user-avatar.d.ts +25 -3
  110. package/dist/components/user-avatar.js +32 -3
  111. package/dist/components/user-avatar.js.map +1 -1
  112. package/dist/hooks/use-close-transition.d.ts +7 -1
  113. package/dist/hooks/use-close-transition.js +5 -2
  114. package/dist/hooks/use-close-transition.js.map +1 -1
  115. package/dist/hooks/use-copy-to-clipboard.js +1 -1
  116. package/dist/hooks/use-copy-to-clipboard.js.map +1 -1
  117. package/dist/hooks/use-file-drop.d.ts +1 -0
  118. package/dist/i18n/defaults.d.ts +6 -0
  119. package/dist/i18n/defaults.js +11 -1
  120. package/dist/i18n/defaults.js.map +1 -1
  121. package/dist/i18n/kit-labels.d.ts +11 -0
  122. package/dist/i18n/kit-labels.js.map +1 -1
  123. package/dist/i18n/locales/de-CH-informal.d.ts +6 -0
  124. package/dist/i18n/locales/de-CH.d.ts +6 -0
  125. package/dist/i18n/locales/de-informal.d.ts +6 -0
  126. package/dist/i18n/locales/de.d.ts +6 -0
  127. package/dist/i18n/locales/de.js +30 -2
  128. package/dist/i18n/locales/de.js.map +1 -1
  129. package/dist/i18n/locales/es.d.ts +6 -0
  130. package/dist/i18n/locales/es.js +30 -2
  131. package/dist/i18n/locales/es.js.map +1 -1
  132. package/dist/i18n/locales/fr.d.ts +6 -0
  133. package/dist/i18n/locales/fr.js +30 -2
  134. package/dist/i18n/locales/fr.js.map +1 -1
  135. package/dist/i18n/locales/hu.d.ts +6 -0
  136. package/dist/i18n/locales/hu.js +30 -2
  137. package/dist/i18n/locales/hu.js.map +1 -1
  138. package/dist/i18n/locales/it.d.ts +6 -0
  139. package/dist/i18n/locales/it.js +30 -2
  140. package/dist/i18n/locales/it.js.map +1 -1
  141. package/dist/i18n/locales/zh.d.ts +6 -0
  142. package/dist/i18n/locales/zh.js +30 -2
  143. package/dist/i18n/locales/zh.js.map +1 -1
  144. package/dist/index.d.ts +27 -15
  145. package/dist/index.js +15 -0
  146. package/dist/index.js.map +1 -1
  147. package/dist/lib/clipping.d.ts +25 -0
  148. package/dist/lib/clipping.js +17 -0
  149. package/dist/lib/clipping.js.map +1 -0
  150. package/dist/lib/dates.d.ts +17 -1
  151. package/dist/lib/dates.js +19 -0
  152. package/dist/lib/dates.js.map +1 -1
  153. package/dist/rhf/form.d.ts +1 -0
  154. package/dist/rhf/use-rhf-wizard-step.d.ts +22 -0
  155. package/dist/rhf/use-rhf-wizard-step.js +38 -0
  156. package/dist/rhf/use-rhf-wizard-step.js.map +1 -0
  157. package/dist/rhf.d.ts +2 -0
  158. package/dist/rhf.js +1 -0
  159. package/dist/rhf.js.map +1 -1
  160. package/dist/search/command-palette.d.ts +19 -2
  161. package/dist/search/command-palette.js +16 -5
  162. package/dist/search/command-palette.js.map +1 -1
  163. package/dist/search/global-search.d.ts +26 -1
  164. package/dist/search/global-search.js +29 -13
  165. package/dist/search/global-search.js.map +1 -1
  166. package/dist/search.d.ts +1 -1
  167. package/dist/shell/topbar-action-menu.d.ts +73 -3
  168. package/dist/shell/topbar-action-menu.js +100 -27
  169. package/dist/shell/topbar-action-menu.js.map +1 -1
  170. package/dist/shell.d.ts +2 -1
  171. package/dist/wizard/stepper-nav.d.ts +1 -0
  172. package/dist/wizard/stepper-nav.js +1 -1
  173. package/dist/wizard/stepper-nav.js.map +1 -1
  174. package/dist/wizard/types.d.ts +2 -2
  175. package/dist/wizard/types.js.map +1 -1
  176. package/dist/wizard/use-wizard.js +1 -1
  177. package/dist/wizard/use-wizard.js.map +1 -1
  178. package/dist/wizard/wizard-context.d.ts +7 -1
  179. package/dist/wizard/wizard-context.js +4 -0
  180. package/dist/wizard/wizard-context.js.map +1 -1
  181. package/dist/wizard/wizard-summary.js +3 -3
  182. package/dist/wizard/wizard-summary.js.map +1 -1
  183. package/dist/wizard.d.ts +2 -1
  184. package/package.json +16 -12
  185. package/src/components/alert-banner.tsx +149 -21
  186. package/src/components/breadcrumbs.tsx +168 -0
  187. package/src/components/bulk-action-bar.tsx +362 -0
  188. package/src/components/button-group.tsx +124 -4
  189. package/src/components/calendar-heatmap.tsx +504 -0
  190. package/src/components/chip.tsx +128 -7
  191. package/src/components/choice-card.tsx +97 -1
  192. package/src/components/copy-button.tsx +29 -7
  193. package/src/components/data-table.tsx +7 -0
  194. package/src/components/date-picker.tsx +49 -1
  195. package/src/components/description-list.tsx +168 -15
  196. package/src/components/dialog-frame.tsx +7 -0
  197. package/src/components/disclosure.tsx +93 -21
  198. package/src/components/field.tsx +137 -0
  199. package/src/components/file-dropzone.tsx +9 -10
  200. package/src/components/floating-panel.tsx +366 -15
  201. package/src/components/full-bleed-dialog.tsx +42 -5
  202. package/src/components/list.tsx +443 -0
  203. package/src/components/menu-item.tsx +235 -0
  204. package/src/components/mini-calendar.tsx +245 -54
  205. package/src/components/modal.tsx +80 -17
  206. package/src/components/nav-pills.tsx +145 -0
  207. package/src/components/page-contents.tsx +4 -4
  208. package/src/components/page-header.tsx +68 -0
  209. package/src/components/progress-bar.tsx +129 -11
  210. package/src/components/series-chart-labels.ts +4 -0
  211. package/src/components/series-chart.tsx +274 -3
  212. package/src/components/status-dot.tsx +107 -0
  213. package/src/components/table.tsx +176 -17
  214. package/src/components/text.tsx +97 -0
  215. package/src/components/toast.tsx +441 -0
  216. package/src/components/toggle-group.tsx +128 -4
  217. package/src/components/tooltip.tsx +200 -100
  218. package/src/components/ui.tsx +381 -14
  219. package/src/components/user-avatar.tsx +57 -3
  220. package/src/hooks/use-close-transition.ts +14 -5
  221. package/src/hooks/use-copy-to-clipboard.ts +1 -1
  222. package/src/i18n/defaults.ts +10 -0
  223. package/src/i18n/kit-labels.tsx +10 -0
  224. package/src/i18n/locales/de.ts +29 -0
  225. package/src/i18n/locales/es.ts +29 -0
  226. package/src/i18n/locales/fr.ts +29 -0
  227. package/src/i18n/locales/hu.ts +29 -0
  228. package/src/i18n/locales/it.ts +29 -0
  229. package/src/i18n/locales/zh.ts +29 -0
  230. package/src/index.ts +31 -0
  231. package/src/lib/clipping.ts +34 -0
  232. package/src/lib/dates.ts +37 -0
  233. package/src/rhf/use-rhf-wizard-step.ts +113 -0
  234. package/src/rhf.ts +2 -0
  235. package/src/search/command-palette.tsx +42 -5
  236. package/src/search/global-search.tsx +59 -18
  237. package/src/shell/topbar-action-menu.tsx +239 -49
  238. package/src/wizard/stepper-nav.tsx +2 -1
  239. package/src/wizard/types.ts +2 -2
  240. package/src/wizard/use-wizard.ts +3 -3
  241. package/src/wizard/wizard-context.tsx +9 -0
  242. package/src/wizard/wizard-summary.tsx +14 -10
  243. package/tokens.css +97 -0
@@ -1,10 +1,33 @@
1
- import { createContext, useContext, useId, useLayoutEffect, useMemo, useRef, useState } from "react";
2
- import type { ComponentPropsWithoutRef } from "react";
1
+ import { Children, createContext, useContext, useId, useLayoutEffect, useMemo, useRef, useState } from "react";
2
+ import type { ComponentPropsWithoutRef, ReactNode } from "react";
3
3
  import { cn } from "../lib/cn";
4
4
  import { THIN_SCROLLBAR_CLASS, useScrollOverflow } from "./scroll-area";
5
5
 
6
- export type TableDensity = "comfortable" | "compact";
6
+ /** `none` (0.10.0): no cell padding at all — keksdose's VAT summary, a table set
7
+ * inside a card's own padding whose columns are spaced by hand (`pl-2` on exactly the
8
+ * columns that need it). Type stays at the body size, as `comfortable`. */
9
+ export type TableDensity = "comfortable" | "compact" | "none";
10
+ /** CSS `table-layout`. `auto` (the browser default) sizes columns to their content;
11
+ * `fixed` takes widths from the first row and ignores the rest — keksdose's VAT
12
+ * summary switches to it while editing so two inputs stop bidding against the
13
+ * figures for room. */
14
+ export type TableLayout = "auto" | "fixed";
7
15
  export type TableAlign = "start" | "center" | "end";
16
+ /**
17
+ * Vertical alignment of a cell's content (0.11.0). The defaults stay as they were — a
18
+ * head cell sits on the `bottom` (a wrapped heading ends on the line above the
19
+ * figures), a body cell at the `top` (a wrapped note starts level with its row). A
20
+ * table whose cells are INPUTS wants `middle`: keksdose's VAT summary
21
+ * (invoices/vat-summary.tsx) sets fields beside derived read-out figures in one row,
22
+ * and a figure at the top of its cell sat above the middle of the field next to it —
23
+ * so it writes `<tr className="align-middle">` over raw `<th>`/`<td>`, because the
24
+ * kit's cells each stated `align-top` and a class on the row could not reach them.
25
+ */
26
+ export type TableVAlign = "top" | "middle" | "bottom";
27
+ /** The head cell's type size: `xs` (the head's default) or `sm` (the body's). */
28
+ export type TableHeaderCellSize = "xs" | "sm";
29
+ /** The header cell's weight. Default `medium`. */
30
+ export type TableHeaderCellWeight = "normal" | "medium" | "semibold";
8
31
 
9
32
  interface TableContextValue {
10
33
  density: TableDensity;
@@ -25,9 +48,32 @@ const TableContext = createContext<TableContextValue>({
25
48
  /** Which section a row sits in — zebra and hover apply to body rows only. */
26
49
  const SectionContext = createContext<"head" | "body" | "foot">("body");
27
50
 
51
+ /** A {@link TableRow}'s `valign`, which its cells take unless they set their own. It
52
+ * cannot be left to CSS inheritance: every cell states its own default alignment,
53
+ * and a class on the cell beats one on the row. */
54
+ const RowVAlignContext = createContext<TableVAlign | undefined>(undefined);
55
+
56
+ const VALIGN: Record<TableVAlign, string> = {
57
+ top: "align-top",
58
+ middle: "align-middle",
59
+ bottom: "align-bottom",
60
+ };
61
+
62
+ const HEADER_SIZE: Record<TableHeaderCellSize, string> = {
63
+ xs: "text-xs",
64
+ sm: "text-sm",
65
+ };
66
+
67
+ const HEADER_WEIGHT: Record<TableHeaderCellWeight, string> = {
68
+ normal: "font-normal",
69
+ medium: "font-medium",
70
+ semibold: "font-semibold",
71
+ };
72
+
28
73
  const CELL_PAD: Record<TableDensity, string> = {
29
74
  comfortable: "px-3 py-2",
30
75
  compact: "px-2 py-1",
76
+ none: "p-0",
31
77
  };
32
78
 
33
79
  const ALIGN: Record<TableAlign, string> = {
@@ -54,6 +100,8 @@ export interface TableProps extends ComponentPropsWithoutRef<"table"> {
54
100
  hover?: boolean;
55
101
  /** Classes for the overflow wrapper around the `<table>` (a max-height, a border). */
56
102
  wrapperClassName?: string;
103
+ /** See {@link TableLayout}. Unset leaves the browser's `auto` and adds no class. */
104
+ layout?: TableLayout;
57
105
  }
58
106
 
59
107
  /**
@@ -78,6 +126,7 @@ export function Table({
78
126
  zebra = false,
79
127
  hover = false,
80
128
  wrapperClassName,
129
+ layout,
81
130
  className,
82
131
  "aria-label": ariaLabel,
83
132
  ...rest
@@ -113,6 +162,7 @@ export function Table({
113
162
  {...(overflowing ? regionName : null)}
114
163
  tabIndex={overflowing ? 0 : undefined}
115
164
  data-overflowing={overflowing || undefined}
165
+ data-clips=""
116
166
  className={cn(
117
167
  // `relative`: the containing block for an `sr-only` caption.
118
168
  "relative w-full overflow-x-auto",
@@ -127,6 +177,8 @@ export function Table({
127
177
  className={cn(
128
178
  "w-full caption-bottom border-collapse text-sm text-[var(--text-primary)]",
129
179
  density === "compact" && "text-xs",
180
+ layout === "fixed" && "table-fixed",
181
+ layout === "auto" && "table-auto",
130
182
  className,
131
183
  )}
132
184
  />
@@ -145,16 +197,87 @@ export function TableHead({ className, ...rest }: TableHeadProps) {
145
197
  );
146
198
  }
147
199
 
148
- export type TableBodyProps = ComponentPropsWithoutRef<"tbody">;
200
+ export interface TableBodyProps extends ComponentPropsWithoutRef<"tbody"> {
201
+ /**
202
+ * Shown as ONE full-width row (a {@link TableEmpty}) when the body has no rows —
203
+ * `children` that render nothing: an empty `.map`, a `false`, `null`. kastlan hand-
204
+ * writes that row with a counted `colSpan` in unit-values-editor, the meeting agenda
205
+ * and invitations tabs, the journal-entry and invoice detail pages, and a column
206
+ * added later leaves every one of those counts one short.
207
+ */
208
+ empty?: ReactNode;
209
+ }
149
210
 
150
- export function TableBody({ className, ...rest }: TableBodyProps) {
211
+ export function TableBody({ className, empty, children, ...rest }: TableBodyProps) {
212
+ // `toArray` drops null/undefined/booleans and flattens arrays, so `[[], false]` —
213
+ // an empty map beside a conditional row that is off — is no rows.
214
+ const hasRows = Children.toArray(children).length > 0;
151
215
  return (
152
216
  <SectionContext.Provider value="body">
153
- <tbody {...rest} className={cn("[&>tr:last-child]:border-b-0", className)} />
217
+ <tbody {...rest} className={cn("[&>tr:last-child]:border-b-0", className)}>
218
+ {hasRows || empty === undefined || empty === null || empty === false ? (
219
+ children
220
+ ) : (
221
+ <TableEmpty>{empty}</TableEmpty>
222
+ )}
223
+ </tbody>
154
224
  </SectionContext.Provider>
155
225
  );
156
226
  }
157
227
 
228
+ export interface TableEmptyProps extends Omit<ComponentPropsWithoutRef<"tr">, "children"> {
229
+ children: ReactNode;
230
+ /** Columns to span. Default: MEASURED — the widest row of the table it renders in,
231
+ * counted after mount, so the row stays full width when a column is added. */
232
+ colSpan?: number;
233
+ /** Classes for the `<td>`. */
234
+ cellClassName?: string;
235
+ }
236
+
237
+ /**
238
+ * The "nothing here" row: one cell across every column, centred, muted. What
239
+ * {@link TableBodyProps.empty} renders, and usable on its own for a body whose rows
240
+ * are not its direct children.
241
+ */
242
+ export function TableEmpty({ children, colSpan, cellClassName, className, ...rest }: TableEmptyProps) {
243
+ const { density } = useContext(TableContext);
244
+ const cell = useRef<HTMLTableCellElement | null>(null);
245
+ const [measured, setMeasured] = useState(1);
246
+ // Every render, deliberately: a column can appear in the head without anything this
247
+ // row depends on changing. It cannot loop — setting the same count is a bail-out.
248
+ // eslint-disable-next-line react-hooks/exhaustive-deps
249
+ useLayoutEffect(() => {
250
+ if (colSpan !== undefined) return;
251
+ const row = cell.current?.parentElement;
252
+ const table = cell.current?.closest("table");
253
+ if (!row || !table) return;
254
+ let widest = 1;
255
+ for (const r of Array.from(table.rows)) {
256
+ if (r === row) continue;
257
+ let n = 0;
258
+ for (const c of Array.from(r.cells)) n += c.colSpan || 1;
259
+ widest = Math.max(widest, n);
260
+ }
261
+ setMeasured(widest);
262
+ });
263
+ return (
264
+ <tr {...rest} data-table-empty="" className={className}>
265
+ <td
266
+ ref={cell}
267
+ colSpan={colSpan ?? measured}
268
+ className={cn(
269
+ // `none` keeps the message off the edges regardless: it is text, not a figure.
270
+ density === "none" ? "py-2" : CELL_PAD[density],
271
+ "text-center text-[var(--text-muted)]",
272
+ cellClassName,
273
+ )}
274
+ >
275
+ {children}
276
+ </td>
277
+ </tr>
278
+ );
279
+ }
280
+
158
281
  export type TableFootProps = ComponentPropsWithoutRef<"tfoot">;
159
282
 
160
283
  /** The totals row. Not in the brief, but every ledger view has one and a `<tfoot>`
@@ -167,23 +290,31 @@ export function TableFoot({ className, ...rest }: TableFootProps) {
167
290
  );
168
291
  }
169
292
 
170
- export type TableRowProps = ComponentPropsWithoutRef<"tr">;
293
+ /** The deprecated HTML `valign` attribute is replaced by a class-backed one. */
294
+ export interface TableRowProps extends ComponentPropsWithoutRef<"tr"> {
295
+ /** Vertical alignment for every cell of the row that does not set its own. See
296
+ * {@link TableVAlign}. Left out, each cell keeps its default. */
297
+ valign?: TableVAlign;
298
+ }
171
299
 
172
- export function TableRow({ className, ...rest }: TableRowProps) {
300
+ export function TableRow({ valign, className, ...rest }: TableRowProps) {
173
301
  const { zebra, hover } = useContext(TableContext);
174
302
  const section = useContext(SectionContext);
175
303
  const body = section === "body";
176
- return (
304
+ const row = (
177
305
  <tr
178
306
  {...rest}
179
307
  className={cn(
180
308
  body && "border-b border-[var(--border)]",
181
309
  body && zebra && "even:bg-[var(--bg-surface-2)]",
182
310
  body && hover && "transition-colors hover:bg-[var(--bg-hover)]",
311
+ // On the row as well, for a raw `<td>` of the caller's, which inherits it.
312
+ valign && VALIGN[valign],
183
313
  className,
184
314
  )}
185
315
  />
186
316
  );
317
+ return valign ? <RowVAlignContext.Provider value={valign}>{row}</RowVAlignContext.Provider> : row;
187
318
  }
188
319
 
189
320
  interface CellAlignProps {
@@ -191,19 +322,44 @@ interface CellAlignProps {
191
322
  numeric?: boolean;
192
323
  /** Logical alignment. Default `start` (`end` when `numeric`). */
193
324
  align?: TableAlign;
325
+ /** Vertical alignment. Default: the row's `valign`, else `bottom` for a head cell and
326
+ * `top` for a body cell. See {@link TableVAlign}. */
327
+ valign?: TableVAlign;
194
328
  }
195
329
 
196
- /** The deprecated HTML `align` attribute is replaced by a logical one. */
197
- export interface TableHeaderCellProps extends Omit<ComponentPropsWithoutRef<"th">, "align">, CellAlignProps {}
330
+ /** The deprecated HTML `align` and `valign` attributes are replaced by class-backed ones. */
331
+ export interface TableHeaderCellProps extends Omit<ComponentPropsWithoutRef<"th">, "align" | "valign">, CellAlignProps {
332
+ /**
333
+ * Type size. Left out: `xs` in the head, the table's own size in the body — as
334
+ * before. keksdose's VAT summary heads its columns at the body size and normal weight
335
+ * (vat-summary.tsx:158–168, `font-normal` on each `<th>`), a quiet header over a
336
+ * small table of figures, and could not say so without overriding classes.
337
+ */
338
+ size?: TableHeaderCellSize;
339
+ /** Default `medium`. `normal` for the quiet header above; see {@link size}. */
340
+ weight?: TableHeaderCellWeight;
341
+ }
198
342
 
199
343
  /**
200
344
  * A `<th>`. `scope` defaults to `col` in the head and `row` in the body — a header
201
345
  * cell in a body row is the row's label ("Net rent" in a key-value table), and a
202
346
  * reader needs the scope to read it with each cell beside it.
203
347
  */
204
- export function TableHeaderCell({ numeric = false, align, scope, className, ...rest }: TableHeaderCellProps) {
348
+ export function TableHeaderCell({
349
+ numeric = false,
350
+ align,
351
+ valign,
352
+ size,
353
+ weight = "medium",
354
+ scope,
355
+ className,
356
+ ...rest
357
+ }: TableHeaderCellProps) {
205
358
  const { density } = useContext(TableContext);
206
359
  const section = useContext(SectionContext);
360
+ const rowVAlign = useContext(RowVAlignContext);
361
+ const head = section === "head";
362
+ const textSize = size ?? (head ? "xs" : undefined);
207
363
  return (
208
364
  <th
209
365
  {...rest}
@@ -212,24 +368,27 @@ export function TableHeaderCell({ numeric = false, align, scope, className, ...r
212
368
  CELL_PAD[density],
213
369
  ALIGN[align ?? (numeric ? "end" : "start")],
214
370
  numeric && "tabular-nums",
215
- "align-bottom font-medium",
216
- section === "head" ? "text-xs text-[var(--text-muted)]" : "align-top text-[var(--text-secondary)]",
371
+ VALIGN[valign ?? rowVAlign ?? (head ? "bottom" : "top")],
372
+ HEADER_WEIGHT[weight],
373
+ head ? "text-[var(--text-muted)]" : "text-[var(--text-secondary)]",
374
+ textSize && HEADER_SIZE[textSize],
217
375
  className,
218
376
  )}
219
377
  />
220
378
  );
221
379
  }
222
380
 
223
- export interface TableCellProps extends Omit<ComponentPropsWithoutRef<"td">, "align">, CellAlignProps {}
381
+ export interface TableCellProps extends Omit<ComponentPropsWithoutRef<"td">, "align" | "valign">, CellAlignProps {}
224
382
 
225
- export function TableCell({ numeric = false, align, className, ...rest }: TableCellProps) {
383
+ export function TableCell({ numeric = false, align, valign, className, ...rest }: TableCellProps) {
226
384
  const { density } = useContext(TableContext);
385
+ const rowVAlign = useContext(RowVAlignContext);
227
386
  return (
228
387
  <td
229
388
  {...rest}
230
389
  className={cn(
231
390
  CELL_PAD[density],
232
- "align-top",
391
+ VALIGN[valign ?? rowVAlign ?? "top"],
233
392
  ALIGN[align ?? (numeric ? "end" : "start")],
234
393
  numeric && "tabular-nums whitespace-nowrap",
235
394
  className,
@@ -0,0 +1,97 @@
1
+ import type { ComponentPropsWithoutRef, ElementType, ReactNode } from "react";
2
+ import { cn } from "../lib/cn";
3
+
4
+ /* ── SectionLabel ─────────────────────────────────────────────────────────── */
5
+
6
+ /**
7
+ * `xs` is 10px — the size `LegendGroup`'s title and the top-bar menus' heading row draw;
8
+ * `sm` (default) is 12px — the size `StatTile`'s label and lenkbank's section headings
9
+ * (control-page.tsx:119/123/127) draw. Two sizes because the label sits either inside a
10
+ * dense panel (a legend, a menu) or above a block of page content, and one size looked
11
+ * wrong in the other place.
12
+ *
13
+ * `md` (0.11.0) is 11px, the rung between them: keksdose's budget-summary card labels
14
+ * its figures ("Assigned", "Activity", "Upcoming", "Available" —
15
+ * budget-summary-card:258/264/286/297) and its support thread its day divider and a message's meta line
16
+ * (support-thread:153/223) at `text-[11px]` by hand — 10px read as a footnote under a
17
+ * figure that size, 12px competed with it. Named `md` rather than slotted in order
18
+ * because `sm` was already the default and renaming it would move every caller.
19
+ */
20
+ export type SectionLabelSize = "xs" | "md" | "sm";
21
+
22
+ /**
23
+ * The small, uppercase, tracked, muted label — as a class string, per size.
24
+ *
25
+ * Exported because `LegendGroup` and `StatTile` already draw it inline, and a class a
26
+ * caller has to position itself (a `<th>`, a `<legend>`, a menu `<li>`) is better
27
+ * reached as a string than wrapped in a component that only takes a `className`. The
28
+ * uppercase is CSS, so a screen reader reads the text as written: write it in normal
29
+ * case.
30
+ */
31
+ export const SECTION_LABEL_CLASS: Record<SectionLabelSize, string> = {
32
+ xs: "text-[10px] font-semibold uppercase tracking-wide text-[var(--text-muted)]",
33
+ md: "text-[11px] font-semibold uppercase tracking-wide text-[var(--text-muted)]",
34
+ sm: "text-xs font-semibold uppercase tracking-wide text-[var(--text-muted)]",
35
+ };
36
+
37
+ type SectionLabelElement = "h2" | "h3" | "h4" | "h5" | "h6" | "p" | "span" | "div" | "legend";
38
+
39
+ export interface SectionLabelProps extends ComponentPropsWithoutRef<"h3"> {
40
+ /**
41
+ * The element — which is the heading LEVEL, and so is the caller's to pick: the kit
42
+ * cannot know where in the outline the label sits. Default `h3`, which is what
43
+ * lenkbank's control page writes under its `h2` card title. Pass `span` or `p` for a
44
+ * label that is not a heading at all (a legend group's title).
45
+ */
46
+ as?: SectionLabelElement;
47
+ size?: SectionLabelSize;
48
+ children: ReactNode;
49
+ }
50
+
51
+ /**
52
+ * A section's small uppercase label. lenkbank spells
53
+ * `text-xs font-semibold uppercase tracking-wide text-slate-500 dark:text-slate-400`
54
+ * on every `<h3>` of its control page and more; keksdose's budget switcher writes the
55
+ * 10px version on a menu row. One component, the token colour, and the level chosen
56
+ * where the outline is known.
57
+ */
58
+ export function SectionLabel({ as = "h3", size = "sm", className, children, ...rest }: SectionLabelProps) {
59
+ const Tag = as as ElementType;
60
+ return (
61
+ <Tag {...rest} className={cn(SECTION_LABEL_CLASS[size], className)}>
62
+ {children}
63
+ </Tag>
64
+ );
65
+ }
66
+
67
+ /* ── Caption ──────────────────────────────────────────────────────────────── */
68
+
69
+ /**
70
+ * The caption type as a class string: 11px, snug, muted. lenkbank's `CAPTION`
71
+ * (shared/ui/caption.ts:23) — written out 31 times before it was a constant — as a
72
+ * token colour rather than `slate-500 dark:slate-400`, so it follows the palette.
73
+ * Exported for the same reason lenkbank's is a string: every caller positions it
74
+ * differently (`mt-1`, `self-end pb-2`, `border-t`), and what must be one thing is the
75
+ * type, not the box.
76
+ */
77
+ export const CAPTION_CLASS = "text-[11px] leading-snug text-[var(--text-muted)]";
78
+
79
+ export interface CaptionProps extends ComponentPropsWithoutRef<"p"> {
80
+ /** Default `p`; `span` for a caption inside a line of other content. */
81
+ as?: "p" | "span" | "div";
82
+ children: ReactNode;
83
+ }
84
+
85
+ /**
86
+ * The small grey sentence under a field, a table or a button — prose ABOUT the thing
87
+ * above it. Not `FieldHint`, which hides behind a field label's "?" so a paragraph does
88
+ * not make one field taller than its neighbour (lenkbank feedback #27).
89
+ */
90
+ export function Caption({ as = "p", className, children, ...rest }: CaptionProps) {
91
+ const Tag = as as ElementType;
92
+ return (
93
+ <Tag {...rest} className={cn(CAPTION_CLASS, className)}>
94
+ {children}
95
+ </Tag>
96
+ );
97
+ }