@eifi1/ui-kit 0.9.0 → 0.10.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.
- package/README.md +21 -11
- package/dist/components/alert-banner.d.ts +32 -5
- package/dist/components/alert-banner.js +57 -14
- package/dist/components/alert-banner.js.map +1 -1
- package/dist/components/amount-input.d.ts +4 -0
- package/dist/components/breadcrumbs.d.ts +60 -0
- package/dist/components/breadcrumbs.js +88 -0
- package/dist/components/breadcrumbs.js.map +1 -0
- package/dist/components/bulk-action-bar.d.ts +66 -0
- package/dist/components/bulk-action-bar.js +149 -0
- package/dist/components/bulk-action-bar.js.map +1 -0
- package/dist/components/calculator.d.ts +4 -0
- package/dist/components/chip.d.ts +31 -5
- package/dist/components/chip.js +85 -5
- package/dist/components/chip.js.map +1 -1
- package/dist/components/copy-button.d.ts +13 -4
- package/dist/components/copy-button.js +17 -4
- package/dist/components/copy-button.js.map +1 -1
- package/dist/components/date-picker.d.ts +22 -2
- package/dist/components/date-picker.js +11 -3
- package/dist/components/date-picker.js.map +1 -1
- package/dist/components/description-list.d.ts +43 -6
- package/dist/components/description-list.js +91 -11
- package/dist/components/description-list.js.map +1 -1
- package/dist/components/dialog-frame.d.ts +7 -0
- package/dist/components/dialog-frame.js.map +1 -1
- package/dist/components/disclosure.d.ts +57 -2
- package/dist/components/disclosure.js +24 -12
- package/dist/components/disclosure.js.map +1 -1
- package/dist/components/file-dropzone.d.ts +3 -4
- package/dist/components/file-dropzone.js +2 -1
- package/dist/components/file-dropzone.js.map +1 -1
- package/dist/components/full-bleed-dialog.d.ts +23 -4
- package/dist/components/full-bleed-dialog.js +11 -2
- package/dist/components/full-bleed-dialog.js.map +1 -1
- package/dist/components/list.d.ts +174 -0
- package/dist/components/list.js +215 -0
- package/dist/components/list.js.map +1 -0
- package/dist/components/menu-item.d.ts +99 -0
- package/dist/components/menu-item.js +83 -0
- package/dist/components/menu-item.js.map +1 -0
- package/dist/components/modal.d.ts +23 -1
- package/dist/components/modal.js +36 -10
- package/dist/components/modal.js.map +1 -1
- package/dist/components/number-field.d.ts +4 -0
- package/dist/components/number-input.d.ts +4 -0
- package/dist/components/numpad-sheet.d.ts +4 -0
- package/dist/components/page-contents.js +1 -2
- package/dist/components/page-contents.js.map +1 -1
- package/dist/components/page-header.d.ts +37 -0
- package/dist/components/page-header.js +36 -0
- package/dist/components/page-header.js.map +1 -0
- package/dist/components/series-chart-labels.d.ts +3 -0
- package/dist/components/series-chart-labels.js +2 -1
- package/dist/components/series-chart-labels.js.map +1 -1
- package/dist/components/series-chart.d.ts +16 -0
- package/dist/components/series-chart.js +137 -3
- package/dist/components/series-chart.js.map +1 -1
- package/dist/components/status-dot.d.ts +49 -0
- package/dist/components/status-dot.js +52 -0
- package/dist/components/status-dot.js.map +1 -0
- package/dist/components/table.d.ts +39 -6
- package/dist/components/table.js +43 -4
- package/dist/components/table.js.map +1 -1
- package/dist/components/text.d.ts +63 -0
- package/dist/components/text.js +23 -0
- package/dist/components/text.js.map +1 -0
- package/dist/components/toast.d.ts +167 -0
- package/dist/components/toast.js +226 -0
- package/dist/components/toast.js.map +1 -0
- package/dist/components/toggle-group.d.ts +29 -1
- package/dist/components/toggle-group.js +52 -3
- package/dist/components/toggle-group.js.map +1 -1
- package/dist/components/tooltip.d.ts +38 -15
- package/dist/components/tooltip.js +85 -47
- package/dist/components/tooltip.js.map +1 -1
- package/dist/components/ui.d.ts +104 -4
- package/dist/components/ui.js +112 -8
- package/dist/components/ui.js.map +1 -1
- package/dist/hooks/use-close-transition.d.ts +7 -1
- package/dist/hooks/use-close-transition.js +5 -2
- package/dist/hooks/use-close-transition.js.map +1 -1
- package/dist/hooks/use-copy-to-clipboard.js +1 -1
- package/dist/hooks/use-copy-to-clipboard.js.map +1 -1
- package/dist/i18n/defaults.d.ts +4 -0
- package/dist/i18n/defaults.js +9 -1
- package/dist/i18n/defaults.js.map +1 -1
- package/dist/i18n/kit-labels.d.ts +8 -0
- package/dist/i18n/kit-labels.js.map +1 -1
- package/dist/i18n/locales/de-CH-informal.d.ts +4 -0
- package/dist/i18n/locales/de-CH.d.ts +4 -0
- package/dist/i18n/locales/de-informal.d.ts +4 -0
- package/dist/i18n/locales/de.d.ts +4 -0
- package/dist/i18n/locales/de.js +21 -1
- package/dist/i18n/locales/de.js.map +1 -1
- package/dist/i18n/locales/es.d.ts +4 -0
- package/dist/i18n/locales/es.js +21 -1
- package/dist/i18n/locales/es.js.map +1 -1
- package/dist/i18n/locales/fr.d.ts +4 -0
- package/dist/i18n/locales/fr.js +21 -1
- package/dist/i18n/locales/fr.js.map +1 -1
- package/dist/i18n/locales/hu.d.ts +4 -0
- package/dist/i18n/locales/hu.js +21 -1
- package/dist/i18n/locales/hu.js.map +1 -1
- package/dist/i18n/locales/it.d.ts +4 -0
- package/dist/i18n/locales/it.js +21 -1
- package/dist/i18n/locales/it.js.map +1 -1
- package/dist/i18n/locales/zh.d.ts +4 -0
- package/dist/i18n/locales/zh.js +21 -1
- package/dist/i18n/locales/zh.js.map +1 -1
- package/dist/index.d.ts +15 -7
- package/dist/index.js +12 -0
- package/dist/index.js.map +1 -1
- package/dist/lib/clipping.d.ts +9 -0
- package/dist/lib/clipping.js +14 -0
- package/dist/lib/clipping.js.map +1 -0
- package/dist/search/command-palette.d.ts +19 -2
- package/dist/search/command-palette.js +16 -5
- package/dist/search/command-palette.js.map +1 -1
- package/dist/search/global-search.d.ts +26 -1
- package/dist/search/global-search.js +29 -13
- package/dist/search/global-search.js.map +1 -1
- package/dist/search.d.ts +1 -1
- package/dist/wizard/stepper-nav.js +1 -1
- package/dist/wizard/stepper-nav.js.map +1 -1
- package/dist/wizard/types.d.ts +2 -2
- package/dist/wizard/types.js.map +1 -1
- package/dist/wizard/use-wizard.js +1 -1
- package/dist/wizard/use-wizard.js.map +1 -1
- package/dist/wizard/wizard-summary.js +3 -3
- package/dist/wizard/wizard-summary.js.map +1 -1
- package/package.json +16 -12
- package/src/components/alert-banner.tsx +117 -18
- package/src/components/breadcrumbs.tsx +168 -0
- package/src/components/bulk-action-bar.tsx +256 -0
- package/src/components/chip.tsx +128 -7
- package/src/components/copy-button.tsx +29 -7
- package/src/components/date-picker.tsx +49 -1
- package/src/components/description-list.tsx +168 -15
- package/src/components/dialog-frame.tsx +7 -0
- package/src/components/disclosure.tsx +93 -21
- package/src/components/file-dropzone.tsx +9 -10
- package/src/components/full-bleed-dialog.tsx +42 -5
- package/src/components/list.tsx +417 -0
- package/src/components/menu-item.tsx +214 -0
- package/src/components/modal.tsx +80 -17
- package/src/components/page-contents.tsx +4 -4
- package/src/components/page-header.tsx +68 -0
- package/src/components/series-chart-labels.ts +4 -0
- package/src/components/series-chart.tsx +253 -3
- package/src/components/status-dot.tsx +99 -0
- package/src/components/table.tsx +91 -6
- package/src/components/text.tsx +89 -0
- package/src/components/toast.tsx +441 -0
- package/src/components/toggle-group.tsx +82 -3
- package/src/components/tooltip.tsx +186 -100
- package/src/components/ui.tsx +237 -8
- package/src/hooks/use-close-transition.ts +14 -5
- package/src/hooks/use-copy-to-clipboard.ts +1 -1
- package/src/i18n/defaults.ts +8 -0
- package/src/i18n/kit-labels.tsx +8 -0
- package/src/i18n/locales/de.ts +20 -0
- package/src/i18n/locales/es.ts +20 -0
- package/src/i18n/locales/fr.ts +20 -0
- package/src/i18n/locales/hu.ts +20 -0
- package/src/i18n/locales/it.ts +20 -0
- package/src/i18n/locales/zh.ts +20 -0
- package/src/index.ts +26 -0
- package/src/lib/clipping.ts +16 -0
- package/src/search/command-palette.tsx +42 -5
- package/src/search/global-search.tsx +59 -18
- package/src/wizard/stepper-nav.tsx +2 -1
- package/src/wizard/types.ts +2 -2
- package/src/wizard/use-wizard.ts +3 -3
- package/src/wizard/wizard-summary.tsx +14 -10
- package/tokens.css +97 -0
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import type { ComponentPropsWithoutRef, ReactNode } from "react";
|
|
2
|
+
import { cn } from "../lib/cn";
|
|
3
|
+
import type { ProgressBarTone } from "./progress-bar";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* The tones are `ProgressBar`'s, name for name — which are `Chip`'s, less nothing — so a
|
|
7
|
+
* dot in a legend, the bar it explains and the badge beside it can be given the same
|
|
8
|
+
* word and agree. The money pair is here for the same reason it is there: a legend
|
|
9
|
+
* swatch for income is money, not "success".
|
|
10
|
+
*/
|
|
11
|
+
export type StatusDotTone = ProgressBarTone;
|
|
12
|
+
|
|
13
|
+
/** `sm` 8px (keksdose's unread dot on the support trigger), `md` 10px (the avatar's
|
|
14
|
+
* unread dot), `lg` 12px (kastlan's occupancy legend swatch). */
|
|
15
|
+
export type StatusDotSize = "sm" | "md" | "lg";
|
|
16
|
+
|
|
17
|
+
const FILL: Record<StatusDotTone, string> = {
|
|
18
|
+
brand: "bg-[var(--brand)]",
|
|
19
|
+
neutral: "bg-[var(--text-muted)]",
|
|
20
|
+
success: "bg-[var(--success)]",
|
|
21
|
+
warning: "bg-[var(--warning)]",
|
|
22
|
+
danger: "bg-[var(--danger)]",
|
|
23
|
+
info: "bg-[var(--info)]",
|
|
24
|
+
income: "bg-[var(--money-income)]",
|
|
25
|
+
expense: "bg-[var(--money-expense)]",
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
const SIZE: Record<StatusDotSize, string> = { sm: "size-2", md: "size-2.5", lg: "size-3" };
|
|
29
|
+
|
|
30
|
+
export interface StatusDotProps extends Omit<ComponentPropsWithoutRef<"span">, "children"> {
|
|
31
|
+
tone?: StatusDotTone;
|
|
32
|
+
size?: StatusDotSize;
|
|
33
|
+
/**
|
|
34
|
+
* Visible text beside the dot — a legend entry ("Rented") or a status ("Online"). It
|
|
35
|
+
* is also what a screen reader reads: the dot itself is decorative whenever there is
|
|
36
|
+
* a label.
|
|
37
|
+
*
|
|
38
|
+
* Without a label the dot is hidden from assistive technology, because a coloured
|
|
39
|
+
* circle says nothing to someone who cannot see it. Say the state where it IS read —
|
|
40
|
+
* keksdose's avatar trigger puts "3 unread" in its own `aria-label` — or give the dot
|
|
41
|
+
* an `aria-label` of its own, which makes it an image with that name.
|
|
42
|
+
*/
|
|
43
|
+
label?: ReactNode;
|
|
44
|
+
/**
|
|
45
|
+
* A ring in the surface colour round the dot, so a dot laid over the corner of an
|
|
46
|
+
* icon or an avatar reads as a separate mark and not a smudge (keksdose top-bar.tsx
|
|
47
|
+
* :152, account-menu.tsx ~385, both hand-drawing `ring-2 ring-white
|
|
48
|
+
* dark:ring-slate-900`). Position it with `className` (`absolute -end-0.5 -top-0.5`).
|
|
49
|
+
*/
|
|
50
|
+
ring?: boolean;
|
|
51
|
+
/** Extra classes for the dot itself; `className` goes on the outer element. */
|
|
52
|
+
dotClassName?: string;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* A small token-coloured dot, with an optional label.
|
|
57
|
+
*
|
|
58
|
+
* kastlan's reporting dashboard (dashboard-tab.tsx:79) paints its occupancy legend
|
|
59
|
+
* with raw `bg-green-500` / `bg-red-500` / `bg-yellow-500`, which ignores the palette
|
|
60
|
+
* and the dark theme; keksdose draws two unread dots by hand in `rose-500`. Both are
|
|
61
|
+
* this.
|
|
62
|
+
*/
|
|
63
|
+
export function StatusDot({
|
|
64
|
+
tone = "neutral",
|
|
65
|
+
size = "sm",
|
|
66
|
+
label,
|
|
67
|
+
ring = false,
|
|
68
|
+
className,
|
|
69
|
+
dotClassName,
|
|
70
|
+
"aria-label": ariaLabel,
|
|
71
|
+
...rest
|
|
72
|
+
}: StatusDotProps) {
|
|
73
|
+
const dot = (
|
|
74
|
+
<span
|
|
75
|
+
className={cn(
|
|
76
|
+
"inline-block shrink-0 rounded-full",
|
|
77
|
+
SIZE[size],
|
|
78
|
+
FILL[tone],
|
|
79
|
+
ring && "ring-2 ring-[var(--bg-surface)]",
|
|
80
|
+
label == null && className,
|
|
81
|
+
dotClassName,
|
|
82
|
+
)}
|
|
83
|
+
{...(label == null
|
|
84
|
+
? ariaLabel
|
|
85
|
+
? { role: "img", "aria-label": ariaLabel, ...rest }
|
|
86
|
+
: { "aria-hidden": true, ...rest }
|
|
87
|
+
: { "aria-hidden": true })}
|
|
88
|
+
/>
|
|
89
|
+
);
|
|
90
|
+
if (label == null) return dot;
|
|
91
|
+
return (
|
|
92
|
+
// No `aria-label` here: the visible label is the text, and a name on a role-less
|
|
93
|
+
// span is one ARIA forbids and screen readers ignore.
|
|
94
|
+
<span {...rest} className={cn("inline-flex min-w-0 items-center gap-1.5 text-sm", className)}>
|
|
95
|
+
{dot}
|
|
96
|
+
<span className="min-w-0 truncate">{label}</span>
|
|
97
|
+
</span>
|
|
98
|
+
);
|
|
99
|
+
}
|
package/src/components/table.tsx
CHANGED
|
@@ -1,9 +1,17 @@
|
|
|
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
|
-
|
|
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";
|
|
8
16
|
|
|
9
17
|
interface TableContextValue {
|
|
@@ -28,6 +36,7 @@ const SectionContext = createContext<"head" | "body" | "foot">("body");
|
|
|
28
36
|
const CELL_PAD: Record<TableDensity, string> = {
|
|
29
37
|
comfortable: "px-3 py-2",
|
|
30
38
|
compact: "px-2 py-1",
|
|
39
|
+
none: "p-0",
|
|
31
40
|
};
|
|
32
41
|
|
|
33
42
|
const ALIGN: Record<TableAlign, string> = {
|
|
@@ -54,6 +63,8 @@ export interface TableProps extends ComponentPropsWithoutRef<"table"> {
|
|
|
54
63
|
hover?: boolean;
|
|
55
64
|
/** Classes for the overflow wrapper around the `<table>` (a max-height, a border). */
|
|
56
65
|
wrapperClassName?: string;
|
|
66
|
+
/** See {@link TableLayout}. Unset leaves the browser's `auto` and adds no class. */
|
|
67
|
+
layout?: TableLayout;
|
|
57
68
|
}
|
|
58
69
|
|
|
59
70
|
/**
|
|
@@ -78,6 +89,7 @@ export function Table({
|
|
|
78
89
|
zebra = false,
|
|
79
90
|
hover = false,
|
|
80
91
|
wrapperClassName,
|
|
92
|
+
layout,
|
|
81
93
|
className,
|
|
82
94
|
"aria-label": ariaLabel,
|
|
83
95
|
...rest
|
|
@@ -127,6 +139,8 @@ export function Table({
|
|
|
127
139
|
className={cn(
|
|
128
140
|
"w-full caption-bottom border-collapse text-sm text-[var(--text-primary)]",
|
|
129
141
|
density === "compact" && "text-xs",
|
|
142
|
+
layout === "fixed" && "table-fixed",
|
|
143
|
+
layout === "auto" && "table-auto",
|
|
130
144
|
className,
|
|
131
145
|
)}
|
|
132
146
|
/>
|
|
@@ -145,16 +159,87 @@ export function TableHead({ className, ...rest }: TableHeadProps) {
|
|
|
145
159
|
);
|
|
146
160
|
}
|
|
147
161
|
|
|
148
|
-
export
|
|
162
|
+
export interface TableBodyProps extends ComponentPropsWithoutRef<"tbody"> {
|
|
163
|
+
/**
|
|
164
|
+
* Shown as ONE full-width row (a {@link TableEmpty}) when the body has no rows —
|
|
165
|
+
* `children` that render nothing: an empty `.map`, a `false`, `null`. kastlan hand-
|
|
166
|
+
* writes that row with a counted `colSpan` in unit-values-editor, the meeting agenda
|
|
167
|
+
* and invitations tabs, the journal-entry and invoice detail pages, and a column
|
|
168
|
+
* added later leaves every one of those counts one short.
|
|
169
|
+
*/
|
|
170
|
+
empty?: ReactNode;
|
|
171
|
+
}
|
|
149
172
|
|
|
150
|
-
export function TableBody({ className, ...rest }: TableBodyProps) {
|
|
173
|
+
export function TableBody({ className, empty, children, ...rest }: TableBodyProps) {
|
|
174
|
+
// `toArray` drops null/undefined/booleans and flattens arrays, so `[[], false]` —
|
|
175
|
+
// an empty map beside a conditional row that is off — is no rows.
|
|
176
|
+
const hasRows = Children.toArray(children).length > 0;
|
|
151
177
|
return (
|
|
152
178
|
<SectionContext.Provider value="body">
|
|
153
|
-
<tbody {...rest} className={cn("[&>tr:last-child]:border-b-0", className)}
|
|
179
|
+
<tbody {...rest} className={cn("[&>tr:last-child]:border-b-0", className)}>
|
|
180
|
+
{hasRows || empty === undefined || empty === null || empty === false ? (
|
|
181
|
+
children
|
|
182
|
+
) : (
|
|
183
|
+
<TableEmpty>{empty}</TableEmpty>
|
|
184
|
+
)}
|
|
185
|
+
</tbody>
|
|
154
186
|
</SectionContext.Provider>
|
|
155
187
|
);
|
|
156
188
|
}
|
|
157
189
|
|
|
190
|
+
export interface TableEmptyProps extends Omit<ComponentPropsWithoutRef<"tr">, "children"> {
|
|
191
|
+
children: ReactNode;
|
|
192
|
+
/** Columns to span. Default: MEASURED — the widest row of the table it renders in,
|
|
193
|
+
* counted after mount, so the row stays full width when a column is added. */
|
|
194
|
+
colSpan?: number;
|
|
195
|
+
/** Classes for the `<td>`. */
|
|
196
|
+
cellClassName?: string;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/**
|
|
200
|
+
* The "nothing here" row: one cell across every column, centred, muted. What
|
|
201
|
+
* {@link TableBodyProps.empty} renders, and usable on its own for a body whose rows
|
|
202
|
+
* are not its direct children.
|
|
203
|
+
*/
|
|
204
|
+
export function TableEmpty({ children, colSpan, cellClassName, className, ...rest }: TableEmptyProps) {
|
|
205
|
+
const { density } = useContext(TableContext);
|
|
206
|
+
const cell = useRef<HTMLTableCellElement | null>(null);
|
|
207
|
+
const [measured, setMeasured] = useState(1);
|
|
208
|
+
// Every render, deliberately: a column can appear in the head without anything this
|
|
209
|
+
// row depends on changing. It cannot loop — setting the same count is a bail-out.
|
|
210
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
211
|
+
useLayoutEffect(() => {
|
|
212
|
+
if (colSpan !== undefined) return;
|
|
213
|
+
const row = cell.current?.parentElement;
|
|
214
|
+
const table = cell.current?.closest("table");
|
|
215
|
+
if (!row || !table) return;
|
|
216
|
+
let widest = 1;
|
|
217
|
+
for (const r of Array.from(table.rows)) {
|
|
218
|
+
if (r === row) continue;
|
|
219
|
+
let n = 0;
|
|
220
|
+
for (const c of Array.from(r.cells)) n += c.colSpan || 1;
|
|
221
|
+
widest = Math.max(widest, n);
|
|
222
|
+
}
|
|
223
|
+
setMeasured(widest);
|
|
224
|
+
});
|
|
225
|
+
return (
|
|
226
|
+
<tr {...rest} data-table-empty="" className={className}>
|
|
227
|
+
<td
|
|
228
|
+
ref={cell}
|
|
229
|
+
colSpan={colSpan ?? measured}
|
|
230
|
+
className={cn(
|
|
231
|
+
// `none` keeps the message off the edges regardless: it is text, not a figure.
|
|
232
|
+
density === "none" ? "py-2" : CELL_PAD[density],
|
|
233
|
+
"text-center text-[var(--text-muted)]",
|
|
234
|
+
cellClassName,
|
|
235
|
+
)}
|
|
236
|
+
>
|
|
237
|
+
{children}
|
|
238
|
+
</td>
|
|
239
|
+
</tr>
|
|
240
|
+
);
|
|
241
|
+
}
|
|
242
|
+
|
|
158
243
|
export type TableFootProps = ComponentPropsWithoutRef<"tfoot">;
|
|
159
244
|
|
|
160
245
|
/** The totals row. Not in the brief, but every ledger view has one and a `<tfoot>`
|
|
@@ -0,0 +1,89 @@
|
|
|
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
|
+
export type SectionLabelSize = "xs" | "sm";
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* The small, uppercase, tracked, muted label — as a class string, per size.
|
|
17
|
+
*
|
|
18
|
+
* Exported because `LegendGroup` and `StatTile` already draw it inline, and a class a
|
|
19
|
+
* caller has to position itself (a `<th>`, a `<legend>`, a menu `<li>`) is better
|
|
20
|
+
* reached as a string than wrapped in a component that only takes a `className`. The
|
|
21
|
+
* uppercase is CSS, so a screen reader reads the text as written: write it in normal
|
|
22
|
+
* case.
|
|
23
|
+
*/
|
|
24
|
+
export const SECTION_LABEL_CLASS: Record<SectionLabelSize, string> = {
|
|
25
|
+
xs: "text-[10px] font-semibold uppercase tracking-wide text-[var(--text-muted)]",
|
|
26
|
+
sm: "text-xs font-semibold uppercase tracking-wide text-[var(--text-muted)]",
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
type SectionLabelElement = "h2" | "h3" | "h4" | "h5" | "h6" | "p" | "span" | "div" | "legend";
|
|
30
|
+
|
|
31
|
+
export interface SectionLabelProps extends ComponentPropsWithoutRef<"h3"> {
|
|
32
|
+
/**
|
|
33
|
+
* The element — which is the heading LEVEL, and so is the caller's to pick: the kit
|
|
34
|
+
* cannot know where in the outline the label sits. Default `h3`, which is what
|
|
35
|
+
* lenkbank's control page writes under its `h2` card title. Pass `span` or `p` for a
|
|
36
|
+
* label that is not a heading at all (a legend group's title).
|
|
37
|
+
*/
|
|
38
|
+
as?: SectionLabelElement;
|
|
39
|
+
size?: SectionLabelSize;
|
|
40
|
+
children: ReactNode;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* A section's small uppercase label. lenkbank spells
|
|
45
|
+
* `text-xs font-semibold uppercase tracking-wide text-slate-500 dark:text-slate-400`
|
|
46
|
+
* on every `<h3>` of its control page and more; keksdose's budget switcher writes the
|
|
47
|
+
* 10px version on a menu row. One component, the token colour, and the level chosen
|
|
48
|
+
* where the outline is known.
|
|
49
|
+
*/
|
|
50
|
+
export function SectionLabel({ as = "h3", size = "sm", className, children, ...rest }: SectionLabelProps) {
|
|
51
|
+
const Tag = as as ElementType;
|
|
52
|
+
return (
|
|
53
|
+
<Tag {...rest} className={cn(SECTION_LABEL_CLASS[size], className)}>
|
|
54
|
+
{children}
|
|
55
|
+
</Tag>
|
|
56
|
+
);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/* ── Caption ──────────────────────────────────────────────────────────────── */
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* The caption type as a class string: 11px, snug, muted. lenkbank's `CAPTION`
|
|
63
|
+
* (shared/ui/caption.ts:23) — written out 31 times before it was a constant — as a
|
|
64
|
+
* token colour rather than `slate-500 dark:slate-400`, so it follows the palette.
|
|
65
|
+
* Exported for the same reason lenkbank's is a string: every caller positions it
|
|
66
|
+
* differently (`mt-1`, `self-end pb-2`, `border-t`), and what must be one thing is the
|
|
67
|
+
* type, not the box.
|
|
68
|
+
*/
|
|
69
|
+
export const CAPTION_CLASS = "text-[11px] leading-snug text-[var(--text-muted)]";
|
|
70
|
+
|
|
71
|
+
export interface CaptionProps extends ComponentPropsWithoutRef<"p"> {
|
|
72
|
+
/** Default `p`; `span` for a caption inside a line of other content. */
|
|
73
|
+
as?: "p" | "span" | "div";
|
|
74
|
+
children: ReactNode;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* The small grey sentence under a field, a table or a button — prose ABOUT the thing
|
|
79
|
+
* above it. Not `FieldHint`, which hides behind a field label's "?" so a paragraph does
|
|
80
|
+
* not make one field taller than its neighbour (lenkbank feedback #27).
|
|
81
|
+
*/
|
|
82
|
+
export function Caption({ as = "p", className, children, ...rest }: CaptionProps) {
|
|
83
|
+
const Tag = as as ElementType;
|
|
84
|
+
return (
|
|
85
|
+
<Tag {...rest} className={cn(CAPTION_CLASS, className)}>
|
|
86
|
+
{children}
|
|
87
|
+
</Tag>
|
|
88
|
+
);
|
|
89
|
+
}
|