@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.
- package/README.md +17 -12
- package/dist/chart.d.ts +3 -3
- package/dist/components/amount-input.d.ts +10 -3
- package/dist/components/amount-input.js +165 -156
- package/dist/components/amount-input.js.map +1 -1
- package/dist/components/authed-image.d.ts +14 -3
- package/dist/components/authed-image.js +32 -23
- package/dist/components/authed-image.js.map +1 -1
- package/dist/components/button-group.d.ts +3 -3
- package/dist/components/calculator.d.ts +3 -3
- package/dist/components/copy-button.d.ts +3 -3
- package/dist/components/facing-pair.d.ts +3 -3
- package/dist/components/file-button.d.ts +3 -3
- package/dist/components/file-button.js +64 -31
- package/dist/components/file-button.js.map +1 -1
- package/dist/components/file-dropzone.d.ts +3 -3
- package/dist/components/form-actions.d.ts +4 -4
- package/dist/components/form-actions.js +18 -6
- package/dist/components/form-actions.js.map +1 -1
- package/dist/components/line-items.d.ts +53 -2
- package/dist/components/line-items.js +73 -7
- package/dist/components/line-items.js.map +1 -1
- package/dist/components/list.d.ts +99 -5
- package/dist/components/list.js +103 -38
- package/dist/components/list.js.map +1 -1
- package/dist/components/loading-state.d.ts +19 -3
- package/dist/components/loading-state.js +14 -3
- package/dist/components/loading-state.js.map +1 -1
- package/dist/components/money-field.d.ts +3 -3
- package/dist/components/number-field.d.ts +3 -3
- package/dist/components/number-input.d.ts +3 -3
- package/dist/components/number-input.js +4 -2
- package/dist/components/number-input.js.map +1 -1
- package/dist/components/numpad-sheet.d.ts +3 -3
- package/dist/components/passkeys-setting.d.ts +30 -10
- package/dist/components/passkeys-setting.js +19 -11
- package/dist/components/passkeys-setting.js.map +1 -1
- package/dist/components/progress-bar.d.ts +12 -3
- package/dist/components/progress-bar.js +21 -11
- package/dist/components/progress-bar.js.map +1 -1
- package/dist/components/series-chart.d.ts +3 -3
- package/dist/components/settings-fields.d.ts +3 -3
- package/dist/components/signed-amount.d.ts +28 -4
- package/dist/components/signed-amount.js +17 -8
- package/dist/components/signed-amount.js.map +1 -1
- package/dist/components/stat-tile.d.ts +31 -2
- package/dist/components/stat-tile.js +37 -6
- package/dist/components/stat-tile.js.map +1 -1
- package/dist/components/status-dot.d.ts +4 -1
- package/dist/components/status-dot.js +20 -1
- package/dist/components/status-dot.js.map +1 -1
- package/dist/components/swatch-picker.d.ts +3 -1
- package/dist/components/swatch-picker.js.map +1 -1
- package/dist/components/table.d.ts +18 -13
- package/dist/components/table.js +20 -5
- package/dist/components/table.js.map +1 -1
- package/dist/components/text-link.d.ts +3 -3
- package/dist/components/time-input.d.ts +3 -3
- package/dist/components/toggle-legend.d.ts +12 -1
- package/dist/components/toggle-legend.js +6 -2
- package/dist/components/toggle-legend.js.map +1 -1
- package/dist/components/ui.d.ts +3 -3
- package/dist/components/ui.js +149 -45
- package/dist/components/ui.js.map +1 -1
- package/dist/feedback/feedback-attachment.d.ts +1 -1
- package/dist/feedback/feedback-attachment.js +24 -11
- package/dist/feedback/feedback-attachment.js.map +1 -1
- package/dist/feedback/feedback-dialog.d.ts +1 -1
- package/dist/feedback/feedback-dialog.js +6 -5
- package/dist/feedback/feedback-dialog.js.map +1 -1
- package/dist/feedback/feedback-inbox.d.ts +1 -1
- package/dist/feedback/feedback-thread.d.ts +1 -1
- package/dist/{feedback-attachment-BYAHJMOr.d.ts → feedback-attachment-WFiXJ8We.d.ts} +21 -8
- package/dist/feedback.d.ts +1 -1
- package/dist/hooks/use-file-drop.d.ts +3 -3
- package/dist/i18n/defaults.d.ts +3 -3
- package/dist/i18n/kit-labels.d.ts +2 -2
- package/dist/i18n/locales/de-CH-informal.d.ts +3 -3
- package/dist/i18n/locales/de-CH.d.ts +3 -3
- package/dist/i18n/locales/de-informal.d.ts +3 -3
- package/dist/i18n/locales/de.d.ts +3 -3
- package/dist/i18n/locales/de.js +1 -1
- package/dist/i18n/locales/de.js.map +1 -1
- package/dist/i18n/locales/es.d.ts +3 -3
- package/dist/i18n/locales/es.js +1 -1
- package/dist/i18n/locales/es.js.map +1 -1
- package/dist/i18n/locales/fr.d.ts +3 -3
- package/dist/i18n/locales/fr.js +1 -1
- package/dist/i18n/locales/fr.js.map +1 -1
- package/dist/i18n/locales/hu.d.ts +3 -3
- package/dist/i18n/locales/hu.js +1 -1
- package/dist/i18n/locales/hu.js.map +1 -1
- package/dist/i18n/locales/it.d.ts +3 -3
- package/dist/i18n/locales/it.js +1 -1
- package/dist/i18n/locales/it.js.map +1 -1
- package/dist/i18n/locales/zh.d.ts +3 -3
- package/dist/i18n/locales/zh.js +1 -1
- package/dist/i18n/locales/zh.js.map +1 -1
- package/dist/index.d.ts +10 -10
- package/dist/{kit-labels-BD2F7sOz.d.ts → kit-labels-DE--LmCk.d.ts} +153 -12
- package/dist/lib/format.d.ts +18 -3
- package/dist/lib/format.js +2 -1
- package/dist/lib/format.js.map +1 -1
- package/dist/rhf/fields.d.ts +3 -3
- package/dist/rhf/form.d.ts +3 -3
- package/dist/rhf/line-items.js.map +1 -1
- package/dist/rhf.d.ts +3 -3
- package/dist/shell/app-shell.d.ts +3 -3
- package/dist/shell/top-bar-brand.d.ts +3 -3
- package/dist/shell.d.ts +3 -3
- package/dist/wizard/stepper-nav.d.ts +3 -3
- package/dist/wizard.d.ts +3 -3
- package/package.json +1 -1
- package/src/components/amount-input.tsx +39 -15
- package/src/components/authed-image.tsx +20 -2
- package/src/components/file-button.tsx +77 -27
- package/src/components/form-actions.tsx +86 -8
- package/src/components/line-items.tsx +131 -14
- package/src/components/list.tsx +240 -50
- package/src/components/loading-state.tsx +35 -4
- package/src/components/number-input.tsx +18 -2
- package/src/components/passkeys-setting.tsx +45 -18
- package/src/components/progress-bar.tsx +46 -21
- package/src/components/signed-amount.tsx +47 -8
- package/src/components/stat-tile.tsx +76 -6
- package/src/components/status-dot.tsx +31 -0
- package/src/components/swatch-picker.tsx +3 -1
- package/src/components/table.tsx +42 -15
- package/src/components/toggle-legend.tsx +21 -4
- package/src/components/ui.tsx +273 -63
- package/src/feedback/feedback-attachment.tsx +45 -14
- package/src/feedback/feedback-dialog.tsx +31 -16
- package/src/i18n/locales/de.ts +1 -1
- package/src/i18n/locales/es.ts +1 -1
- package/src/i18n/locales/fr.ts +1 -1
- package/src/i18n/locales/hu.ts +1 -1
- package/src/i18n/locales/it.ts +1 -1
- package/src/i18n/locales/zh.ts +1 -1
- package/src/lib/format.ts +24 -3
- package/src/rhf/line-items.tsx +6 -0
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { isValidElement, useId, useLayoutEffect, useState } from "react";
|
|
1
|
+
import { createContext, isValidElement, useContext, useId, useLayoutEffect, useState } from "react";
|
|
2
2
|
import type { ComponentPropsWithoutRef, CSSProperties, ReactElement, ReactNode } from "react";
|
|
3
3
|
import { ArrowDown, ArrowUp, Minus } from "lucide-react";
|
|
4
4
|
import { cn } from "../lib/cn";
|
|
@@ -217,6 +217,16 @@ export interface StatTileProps
|
|
|
217
217
|
loading?: boolean;
|
|
218
218
|
/** `"sm"` for dense grids (a metrics wall); `"md"` the default KPI. */
|
|
219
219
|
size?: "sm" | "md";
|
|
220
|
+
/**
|
|
221
|
+
* The tile's frame. `"card"` (default) is the bordered, shadowed card on the page.
|
|
222
|
+
* `"inset"` is a tile INSIDE a card: {@link Card}'s `inset` — the raised
|
|
223
|
+
* `--bg-surface-2`, a small radius, no border or shadow, since the card round it
|
|
224
|
+
* already has them. `"plain"` drops the frame altogether — no fill and no padding —
|
|
225
|
+
* for figures set straight into a card's content (a summary row under a card title),
|
|
226
|
+
* where any box would be a box in a box. keksdose wrote both by hand over the card
|
|
227
|
+
* tile (`border-0 shadow-none bg-transparent p-0`) for its tiles inside a card.
|
|
228
|
+
*/
|
|
229
|
+
variant?: "card" | "inset" | "plain";
|
|
220
230
|
/** Makes the tile a link. See {@link renderLink} for a router. */
|
|
221
231
|
href?: string;
|
|
222
232
|
/** Renders the link for `href` — pass your router's `<Link>` here, since a plain
|
|
@@ -291,6 +301,7 @@ export function StatTile({
|
|
|
291
301
|
sensitive = false,
|
|
292
302
|
loading = false,
|
|
293
303
|
size = "md",
|
|
304
|
+
variant = "card",
|
|
294
305
|
href,
|
|
295
306
|
renderLink,
|
|
296
307
|
onClick,
|
|
@@ -299,6 +310,11 @@ export function StatTile({
|
|
|
299
310
|
...rest
|
|
300
311
|
}: StatTileProps) {
|
|
301
312
|
const text = useKitLabels("statTile", DEFAULT_STAT_TILE_LABELS, labels);
|
|
313
|
+
// A loading StatTileGrid puts every tile in it into the loading state and says
|
|
314
|
+
// "Loading…" ONCE for all of them; the tile then keeps its own sentence to itself
|
|
315
|
+
// (see StatTileGrid's `loading`).
|
|
316
|
+
const gridLoading = useContext(StatTileGridLoadingContext);
|
|
317
|
+
loading = loading || gridLoading;
|
|
302
318
|
const kitLink = useKitLink();
|
|
303
319
|
const [labelEl, setLabelEl] = useState<HTMLElement | null>(null);
|
|
304
320
|
const labelCut = useIsTruncated(truncateLabel ? labelEl : null);
|
|
@@ -398,7 +414,7 @@ export function StatTile({
|
|
|
398
414
|
const headline = loading ? (
|
|
399
415
|
<span id={valueId} className="relative block py-0.5">
|
|
400
416
|
<span aria-hidden className={cn(SKELETON_CLASS, "block h-6 w-24 max-w-full")} />
|
|
401
|
-
<span className="sr-only">{text.loading}</span>
|
|
417
|
+
{!gridLoading && <span className="sr-only">{text.loading}</span>}
|
|
402
418
|
</span>
|
|
403
419
|
) : hasValue ? (
|
|
404
420
|
<span id={valueId} data-private={priv} className={cn(valueClass, valueLabel != null && "ms-auto")}>
|
|
@@ -432,13 +448,18 @@ export function StatTile({
|
|
|
432
448
|
return (
|
|
433
449
|
<Card
|
|
434
450
|
{...rest}
|
|
451
|
+
variant={variant === "card" ? "default" : "inset"}
|
|
452
|
+
data-variant={variant === "card" ? undefined : variant}
|
|
435
453
|
aria-busy={loading || undefined}
|
|
436
454
|
className={cn(
|
|
437
455
|
// @container: the headline sizes read the tile's width. relative: the stretched
|
|
438
456
|
// link's overlay. overflow-hidden: at pathological widths a figure clips inside
|
|
439
457
|
// the border instead of painting over the neighbouring tile.
|
|
440
458
|
"@container relative flex min-w-0 flex-col overflow-hidden",
|
|
441
|
-
|
|
459
|
+
// `plain` is the inset card with its fill and padding taken away — the figures
|
|
460
|
+
// align with the content edge of the card they sit in. (Not `p-0` over the
|
|
461
|
+
// padding: the padding's `sm:p-4` would still win from `sm` up.)
|
|
462
|
+
variant === "plain" ? "rounded-none bg-transparent p-0" : PADDING[size],
|
|
442
463
|
interactive && "transition-colors hover:border-[var(--border-strong)] hover:bg-[var(--bg-hover)]",
|
|
443
464
|
className,
|
|
444
465
|
)}
|
|
@@ -577,8 +598,30 @@ export interface StatTileGridProps extends ComponentPropsWithoutRef<"div"> {
|
|
|
577
598
|
/** The narrowest a tile may get before the grid drops a column. The default fits
|
|
578
599
|
* two tiles on a 320px phone. */
|
|
579
600
|
minTileWidth?: string;
|
|
601
|
+
/**
|
|
602
|
+
* Let a row that is not full stretch its tiles across the whole width. Default
|
|
603
|
+
* `true` (CSS `auto-fit`): one or two tiles fill the row, as they always have.
|
|
604
|
+
* `false` (`auto-fill`): the empty columns are kept, so every tile is the width of
|
|
605
|
+
* a column whatever the count — a lone "Balance" tile in a card stays tile-sized
|
|
606
|
+
* instead of becoming a banner the width of the card, and the tiles of a short row
|
|
607
|
+
* line up with the columns of a full one above it.
|
|
608
|
+
*/
|
|
609
|
+
stretch?: boolean;
|
|
610
|
+
/**
|
|
611
|
+
* Every tile in the grid is loading: each shows its skeleton (as with its own
|
|
612
|
+
* `loading`), and the grid says "Loading…" ONCE, in a polite status region, instead
|
|
613
|
+
* of each tile carrying its own sr-only "Loading…" — a wall of eight tiles was read
|
|
614
|
+
* as eight "Loading…"s in a row. The grid is `aria-busy` meanwhile. A tile's own
|
|
615
|
+
* `loading` inside a grid that is not loading still speaks for itself.
|
|
616
|
+
*/
|
|
617
|
+
loading?: boolean;
|
|
618
|
+
/** The grid's one sentence — `loading` from {@link StatTileLabels}. */
|
|
619
|
+
labels?: Partial<Pick<StatTileLabels, "loading">>;
|
|
580
620
|
}
|
|
581
621
|
|
|
622
|
+
/** Set by a loading {@link StatTileGrid}; read by each tile in it. */
|
|
623
|
+
const StatTileGridLoadingContext = createContext(false);
|
|
624
|
+
|
|
582
625
|
/**
|
|
583
626
|
* The row of tiles every dashboard in the three apps lays out by hand, each with its
|
|
584
627
|
* own breakpoint ladder (`grid-cols-2 md:grid-cols-4 min-[2400px]:grid-cols-8` …).
|
|
@@ -586,9 +629,36 @@ export interface StatTileGridProps extends ComponentPropsWithoutRef<"div"> {
|
|
|
586
629
|
* the band where the sidebar was open. This fits as many columns as `minTileWidth`
|
|
587
630
|
* allows in the space the grid actually has, and needs no breakpoint at all.
|
|
588
631
|
*/
|
|
589
|
-
export function StatTileGrid({
|
|
632
|
+
export function StatTileGrid({
|
|
633
|
+
minTileWidth = "9rem",
|
|
634
|
+
stretch = true,
|
|
635
|
+
loading = false,
|
|
636
|
+
labels,
|
|
637
|
+
className,
|
|
638
|
+
style,
|
|
639
|
+
children,
|
|
640
|
+
...rest
|
|
641
|
+
}: StatTileGridProps) {
|
|
642
|
+
const text = useKitLabels("statTile", DEFAULT_STAT_TILE_LABELS, labels);
|
|
590
643
|
const columns: CSSProperties = {
|
|
591
|
-
gridTemplateColumns: `repeat(auto-fit, minmax(min(100%, ${minTileWidth}), 1fr))`,
|
|
644
|
+
gridTemplateColumns: `repeat(${stretch ? "auto-fit" : "auto-fill"}, minmax(min(100%, ${minTileWidth}), 1fr))`,
|
|
592
645
|
};
|
|
593
|
-
return
|
|
646
|
+
return (
|
|
647
|
+
<div
|
|
648
|
+
{...rest}
|
|
649
|
+
aria-busy={loading || rest["aria-busy"] || undefined}
|
|
650
|
+
// `relative`: the containing block for the sr-only status, which is absolutely
|
|
651
|
+
// positioned and so also takes no grid cell.
|
|
652
|
+
className={cn("relative grid gap-3", className)}
|
|
653
|
+
style={{ ...columns, ...style }}
|
|
654
|
+
>
|
|
655
|
+
{/* Always mounted, filled while loading: a live region announces a CHANGE of
|
|
656
|
+
its content, so one that appeared already holding the sentence might say
|
|
657
|
+
nothing. */}
|
|
658
|
+
<span role="status" className="sr-only">
|
|
659
|
+
{loading ? text.loading : ""}
|
|
660
|
+
</span>
|
|
661
|
+
<StatTileGridLoadingContext.Provider value={loading}>{children}</StatTileGridLoadingContext.Provider>
|
|
662
|
+
</div>
|
|
663
|
+
);
|
|
594
664
|
}
|
|
@@ -33,6 +33,37 @@ const FILL: Record<StatusDotTone, string> = {
|
|
|
33
33
|
orange: "bg-[var(--hue-orange)]",
|
|
34
34
|
};
|
|
35
35
|
|
|
36
|
+
/**
|
|
37
|
+
* The colour of each tone as a CSS value — the same `var(--…)` the dot's class paints
|
|
38
|
+
* with. For a mark the kit does not draw: a `SwatchPicker` option (`color`), a chart
|
|
39
|
+
* series, an inline `style`. keksdose's category picker spelled `var(--hue-teal)` out by
|
|
40
|
+
* hand to match the dot beside the category name; a renamed token would have parted
|
|
41
|
+
* them silently. (The class table above stays literal: Tailwind finds classes by
|
|
42
|
+
* reading the source, so `bg-[${…}]` would never be generated. A test keeps the two
|
|
43
|
+
* tables agreeing.)
|
|
44
|
+
*/
|
|
45
|
+
const COLOR: Record<StatusDotTone, string> = {
|
|
46
|
+
brand: "var(--brand)",
|
|
47
|
+
neutral: "var(--text-muted)",
|
|
48
|
+
success: "var(--success)",
|
|
49
|
+
warning: "var(--warning)",
|
|
50
|
+
danger: "var(--danger)",
|
|
51
|
+
info: "var(--info)",
|
|
52
|
+
income: "var(--money-income)",
|
|
53
|
+
expense: "var(--money-expense)",
|
|
54
|
+
blue: "var(--hue-blue)",
|
|
55
|
+
indigo: "var(--hue-indigo)",
|
|
56
|
+
purple: "var(--hue-purple)",
|
|
57
|
+
teal: "var(--hue-teal)",
|
|
58
|
+
orange: "var(--hue-orange)",
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
/** The CSS colour a {@link StatusDot} of `tone` is filled with — `statusDotColor("teal")`
|
|
62
|
+
* is `"var(--hue-teal)"`. See the table above for when to reach for it. */
|
|
63
|
+
export function statusDotColor(tone: StatusDotTone): string {
|
|
64
|
+
return COLOR[tone];
|
|
65
|
+
}
|
|
66
|
+
|
|
36
67
|
const SIZE: Record<StatusDotSize, string> = { sm: "size-2", md: "size-2.5", lg: "size-3" };
|
|
37
68
|
|
|
38
69
|
export interface StatusDotProps extends Omit<ComponentPropsWithoutRef<"span">, "children"> {
|
|
@@ -21,7 +21,9 @@ export interface SwatchOption<T extends string> {
|
|
|
21
21
|
value: T;
|
|
22
22
|
/** Any CSS colour — a token (`var(--chart-3)`), a hex, an `oklch()`. Painted as an
|
|
23
23
|
* inline background, so it does not have to be a class the kit's CSS knows.
|
|
24
|
-
* Leave it out when `swatchClassName` paints the dot instead.
|
|
24
|
+
* Leave it out when `swatchClassName` paints the dot instead. For a colour that
|
|
25
|
+
* must match a `StatusDot` / `Chip` hue elsewhere, `statusDotColor("teal")` rather
|
|
26
|
+
* than `"var(--hue-teal)"` written out — one table, so the two cannot drift. */
|
|
25
27
|
color?: string;
|
|
26
28
|
/** The colour's name, in the user's language. Shown in the bubble and read out;
|
|
27
29
|
* required, because a swatch with no name is a colour only some people can read. */
|
package/src/components/table.tsx
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
|
-
import { Children, createContext, useContext, useId, useLayoutEffect, useMemo, useRef, useState } from "react";
|
|
2
|
-
import type {
|
|
1
|
+
import { Children, createContext, useCallback, useContext, useId, useLayoutEffect, useMemo, useRef, useState } from "react";
|
|
2
|
+
import type { ComponentPropsWithRef, ReactNode, Ref } from "react";
|
|
3
3
|
import { cn } from "../lib/cn";
|
|
4
|
+
import { assignRef } from "./choice-parts";
|
|
4
5
|
import { THIN_SCROLLBAR_CLASS, useScrollOverflow } from "./scroll-area";
|
|
5
6
|
|
|
6
7
|
/** `none` (0.10.0): no cell padding at all — keksdose's VAT summary, a table set
|
|
@@ -91,6 +92,22 @@ function useSection<E extends HTMLElement>(): [TableSection, ((el: E | null) =>
|
|
|
91
92
|
return [fromContext ?? detected ?? "body", fromContext === undefined ? setNode : undefined];
|
|
92
93
|
}
|
|
93
94
|
|
|
95
|
+
/**
|
|
96
|
+
* One callback ref for the caller's `ref` and the part's own (0.16.0). Every part takes
|
|
97
|
+
* a `ref` to its DOM element — React 19 hands it to a function component as a prop —
|
|
98
|
+
* and the parts that hold the node themselves (the table, a row or a head cell reading
|
|
99
|
+
* its section off the DOM) must still get it when the caller passes one too.
|
|
100
|
+
*/
|
|
101
|
+
function useMergedRef<E>(own: ((el: E | null) => void) | { current: E | null } | undefined, ref: Ref<E> | undefined) {
|
|
102
|
+
return useCallback(
|
|
103
|
+
(node: E | null) => {
|
|
104
|
+
assignRef(own as Ref<E> | undefined, node);
|
|
105
|
+
assignRef(ref, node);
|
|
106
|
+
},
|
|
107
|
+
[own, ref],
|
|
108
|
+
);
|
|
109
|
+
}
|
|
110
|
+
|
|
94
111
|
/** A {@link TableRow}'s `valign`, which its cells take unless they set their own. It
|
|
95
112
|
* cannot be left to CSS inheritance: every cell states its own default alignment,
|
|
96
113
|
* and a class on the cell beats one on the row. */
|
|
@@ -133,7 +150,12 @@ const ALIGN: Record<TableAlign, string> = {
|
|
|
133
150
|
*/
|
|
134
151
|
export const NUMERIC_CELL_CLASS = "text-end tabular-nums";
|
|
135
152
|
|
|
136
|
-
|
|
153
|
+
/**
|
|
154
|
+
* Every part's props are `ComponentPropsWithRef` (0.16.0), so `ref` reaches the DOM
|
|
155
|
+
* element: keksdose scrolled a budget row into view through `useId` + `getElementById`
|
|
156
|
+
* because `<TableRow ref>` did not type-check.
|
|
157
|
+
*/
|
|
158
|
+
export interface TableProps extends ComponentPropsWithRef<"table"> {
|
|
137
159
|
/** `compact` for a dense detail view (a ledger, a spec sheet). */
|
|
138
160
|
density?: TableDensity;
|
|
139
161
|
/** Tint every other body row. */
|
|
@@ -216,6 +238,7 @@ export function Table({
|
|
|
216
238
|
stack,
|
|
217
239
|
className,
|
|
218
240
|
"aria-label": ariaLabel,
|
|
241
|
+
ref,
|
|
219
242
|
...rest
|
|
220
243
|
}: TableProps) {
|
|
221
244
|
const captionId = useId();
|
|
@@ -223,6 +246,7 @@ export function Table({
|
|
|
223
246
|
const wrapper = useRef<HTMLDivElement | null>(null);
|
|
224
247
|
const overflowing = useScrollOverflow(wrapper);
|
|
225
248
|
const tableRef = useRef<HTMLTableElement | null>(null);
|
|
249
|
+
const setTableRef = useMergedRef(tableRef, ref);
|
|
226
250
|
|
|
227
251
|
// `stack="phone"`: label every body cell with its column's header text, and keep the
|
|
228
252
|
// table's roles explicit — `display: block` on table parts drops their table
|
|
@@ -296,7 +320,7 @@ export function Table({
|
|
|
296
320
|
>
|
|
297
321
|
<table
|
|
298
322
|
{...rest}
|
|
299
|
-
ref={
|
|
323
|
+
ref={setTableRef}
|
|
300
324
|
aria-label={ariaLabel}
|
|
301
325
|
className={cn(
|
|
302
326
|
"w-full caption-bottom border-collapse text-sm text-[var(--text-primary)]",
|
|
@@ -312,7 +336,7 @@ export function Table({
|
|
|
312
336
|
);
|
|
313
337
|
}
|
|
314
338
|
|
|
315
|
-
export interface TableHeadProps extends
|
|
339
|
+
export interface TableHeadProps extends ComponentPropsWithRef<"thead"> {
|
|
316
340
|
/** The rule under the head. Default true; `false` for a quiet header over a small
|
|
317
341
|
* table (keksdose's VAT summary wrote `className="border-b-0"` to lose it). */
|
|
318
342
|
bordered?: boolean;
|
|
@@ -326,7 +350,7 @@ export function TableHead({ bordered = true, className, ...rest }: TableHeadProp
|
|
|
326
350
|
);
|
|
327
351
|
}
|
|
328
352
|
|
|
329
|
-
export interface TableBodyProps extends
|
|
353
|
+
export interface TableBodyProps extends ComponentPropsWithRef<"tbody"> {
|
|
330
354
|
/**
|
|
331
355
|
* Shown as ONE full-width row (a {@link TableEmpty}) when the body has no rows —
|
|
332
356
|
* `children` that render nothing: an empty `.map`, a `false`, `null`. kastlan hand-
|
|
@@ -354,7 +378,7 @@ export function TableBody({ className, empty, children, ...rest }: TableBodyProp
|
|
|
354
378
|
);
|
|
355
379
|
}
|
|
356
380
|
|
|
357
|
-
export interface TableEmptyProps extends Omit<
|
|
381
|
+
export interface TableEmptyProps extends Omit<ComponentPropsWithRef<"tr">, "children"> {
|
|
358
382
|
children: ReactNode;
|
|
359
383
|
/** Columns to span. Default: MEASURED — the widest row of the table it renders in,
|
|
360
384
|
* counted after mount, so the row stays full width when a column is added. */
|
|
@@ -407,7 +431,7 @@ export function TableEmpty({ children, colSpan, cellClassName, className, ...res
|
|
|
407
431
|
);
|
|
408
432
|
}
|
|
409
433
|
|
|
410
|
-
export type TableFootProps =
|
|
434
|
+
export type TableFootProps = ComponentPropsWithRef<"tfoot">;
|
|
411
435
|
|
|
412
436
|
/** The totals row. Not in the brief, but every ledger view has one and a `<tfoot>`
|
|
413
437
|
* is what tells a reader it is a total rather than one more row. */
|
|
@@ -429,7 +453,7 @@ const ROW_VARIANT: Record<TableRowVariant, string | false> = {
|
|
|
429
453
|
};
|
|
430
454
|
|
|
431
455
|
/** The deprecated HTML `valign` attribute is replaced by a class-backed one. */
|
|
432
|
-
export interface TableRowProps extends
|
|
456
|
+
export interface TableRowProps extends ComponentPropsWithRef<"tr"> {
|
|
433
457
|
/** Vertical alignment for every cell of the row that does not set its own. See
|
|
434
458
|
* {@link TableVAlign}. Left out, each cell keeps its default. */
|
|
435
459
|
valign?: TableVAlign;
|
|
@@ -440,9 +464,10 @@ export interface TableRowProps extends ComponentPropsWithoutRef<"tr"> {
|
|
|
440
464
|
bordered?: boolean;
|
|
441
465
|
}
|
|
442
466
|
|
|
443
|
-
export function TableRow({ valign, variant = "row", bordered, className, ...rest }: TableRowProps) {
|
|
467
|
+
export function TableRow({ valign, variant = "row", bordered, className, ref: refProp, ...rest }: TableRowProps) {
|
|
444
468
|
const { zebra, hover, rowDividers } = useContext(TableContext);
|
|
445
|
-
const [section,
|
|
469
|
+
const [section, sectionRef] = useSection<HTMLTableRowElement>();
|
|
470
|
+
const ref = useMergedRef(sectionRef, refProp);
|
|
446
471
|
const body = section === "body";
|
|
447
472
|
const record = body && variant === "row";
|
|
448
473
|
const row = (
|
|
@@ -475,7 +500,7 @@ interface CellAlignProps {
|
|
|
475
500
|
}
|
|
476
501
|
|
|
477
502
|
/** The deprecated HTML `align` and `valign` attributes are replaced by class-backed ones. */
|
|
478
|
-
export interface TableHeaderCellProps extends Omit<
|
|
503
|
+
export interface TableHeaderCellProps extends Omit<ComponentPropsWithRef<"th">, "align" | "valign">, CellAlignProps {
|
|
479
504
|
/**
|
|
480
505
|
* Type size. Left out: `xs` in the head, the table's own size in the body — as
|
|
481
506
|
* before. keksdose's VAT summary heads its columns at the body size and normal weight
|
|
@@ -500,10 +525,12 @@ export function TableHeaderCell({
|
|
|
500
525
|
weight = "medium",
|
|
501
526
|
scope,
|
|
502
527
|
className,
|
|
528
|
+
ref: refProp,
|
|
503
529
|
...rest
|
|
504
530
|
}: TableHeaderCellProps) {
|
|
505
531
|
const { density } = useContext(TableContext);
|
|
506
|
-
const [section,
|
|
532
|
+
const [section, sectionRef] = useSection<HTMLTableCellElement>();
|
|
533
|
+
const ref = useMergedRef(sectionRef, refProp);
|
|
507
534
|
const rowVAlign = useContext(RowVAlignContext);
|
|
508
535
|
const head = section === "head";
|
|
509
536
|
const textSize = size ?? (head ? "xs" : undefined);
|
|
@@ -526,7 +553,7 @@ export function TableHeaderCell({
|
|
|
526
553
|
);
|
|
527
554
|
}
|
|
528
555
|
|
|
529
|
-
export interface TableCellProps extends Omit<
|
|
556
|
+
export interface TableCellProps extends Omit<ComponentPropsWithRef<"td">, "align" | "valign">, CellAlignProps {}
|
|
530
557
|
|
|
531
558
|
export function TableCell({ numeric = false, align, valign, className, ...rest }: TableCellProps) {
|
|
532
559
|
const { density } = useContext(TableContext);
|
|
@@ -545,7 +572,7 @@ export function TableCell({ numeric = false, align, valign, className, ...rest }
|
|
|
545
572
|
);
|
|
546
573
|
}
|
|
547
574
|
|
|
548
|
-
export type TableCaptionProps =
|
|
575
|
+
export type TableCaptionProps = ComponentPropsWithRef<"caption">;
|
|
549
576
|
|
|
550
577
|
/**
|
|
551
578
|
* The table's title. Beneath the table by default (`caption-bottom`, the shadcn
|
|
@@ -194,6 +194,17 @@ export interface StaticLegendProps {
|
|
|
194
194
|
orientation?: "horizontal" | "vertical";
|
|
195
195
|
/** The list's accessible name. Default: the `seriesChart.legend` label. */
|
|
196
196
|
"aria-label"?: string;
|
|
197
|
+
/**
|
|
198
|
+
* `ul` (default): a list — "list, 4 items", walked with the list keys. Its entries are
|
|
199
|
+
* `listitem`s, so a host test that counts `getAllByRole("listitem")` on a page with
|
|
200
|
+
* the legend on it counts the legend too: scope it (`within(...)`) or use `div`.
|
|
201
|
+
*
|
|
202
|
+
* `div`: a named `role="group"` of plain entries, for a legend set inside something
|
|
203
|
+
* that is already a list or a table row, where a list in a list reads as nesting that
|
|
204
|
+
* is not there, or where the entries are too few for "list, 2 items" to help. The
|
|
205
|
+
* entries are read in order either way; only the list framing goes.
|
|
206
|
+
*/
|
|
207
|
+
as?: "ul" | "div";
|
|
197
208
|
className?: string;
|
|
198
209
|
/** Per-instance strings over `<UiKitProvider labels={{ seriesChart }}>`. */
|
|
199
210
|
labels?: Partial<SeriesChartLabels>;
|
|
@@ -218,13 +229,19 @@ export function StaticLegend({
|
|
|
218
229
|
entries,
|
|
219
230
|
orientation = "horizontal",
|
|
220
231
|
"aria-label": ariaLabel,
|
|
232
|
+
as = "ul",
|
|
221
233
|
className,
|
|
222
234
|
labels: labelsProp,
|
|
223
235
|
}: StaticLegendProps) {
|
|
224
236
|
const labels = useKitLabels("seriesChart", DEFAULT_SERIES_CHART_LABELS, labelsProp);
|
|
225
237
|
if (entries.length === 0) return null;
|
|
238
|
+
const List = as;
|
|
239
|
+
const Item = as === "ul" ? "li" : "div";
|
|
226
240
|
return (
|
|
227
|
-
<
|
|
241
|
+
<List
|
|
242
|
+
// A `div` takes `group`: a name on a role-less div is one ARIA forbids and
|
|
243
|
+
// screen readers drop, and the name is what says these marks are a key.
|
|
244
|
+
role={as === "div" ? "group" : undefined}
|
|
228
245
|
aria-label={ariaLabel ?? labels.legend}
|
|
229
246
|
className={cn(
|
|
230
247
|
"m-0 flex list-none gap-x-3 gap-y-1 p-0",
|
|
@@ -235,7 +252,7 @@ export function StaticLegend({
|
|
|
235
252
|
)}
|
|
236
253
|
>
|
|
237
254
|
{entries.map((entry) => (
|
|
238
|
-
<
|
|
255
|
+
<Item
|
|
239
256
|
key={entry.key}
|
|
240
257
|
className={cn(
|
|
241
258
|
"flex items-center gap-1.5 text-[11px] text-[var(--text-secondary)]",
|
|
@@ -244,9 +261,9 @@ export function StaticLegend({
|
|
|
244
261
|
>
|
|
245
262
|
<LegendMark entry={entry} off={false} />
|
|
246
263
|
{entry.label}
|
|
247
|
-
</
|
|
264
|
+
</Item>
|
|
248
265
|
))}
|
|
249
|
-
</
|
|
266
|
+
</List>
|
|
250
267
|
);
|
|
251
268
|
}
|
|
252
269
|
|