@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,49 @@
|
|
|
1
|
+
import * as react from 'react';
|
|
2
|
+
import { ComponentPropsWithoutRef, ReactNode } from 'react';
|
|
3
|
+
import { ProgressBarTone } from './progress-bar.js';
|
|
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
|
+
type StatusDotTone = ProgressBarTone;
|
|
12
|
+
/** `sm` 8px (keksdose's unread dot on the support trigger), `md` 10px (the avatar's
|
|
13
|
+
* unread dot), `lg` 12px (kastlan's occupancy legend swatch). */
|
|
14
|
+
type StatusDotSize = "sm" | "md" | "lg";
|
|
15
|
+
interface StatusDotProps extends Omit<ComponentPropsWithoutRef<"span">, "children"> {
|
|
16
|
+
tone?: StatusDotTone;
|
|
17
|
+
size?: StatusDotSize;
|
|
18
|
+
/**
|
|
19
|
+
* Visible text beside the dot — a legend entry ("Rented") or a status ("Online"). It
|
|
20
|
+
* is also what a screen reader reads: the dot itself is decorative whenever there is
|
|
21
|
+
* a label.
|
|
22
|
+
*
|
|
23
|
+
* Without a label the dot is hidden from assistive technology, because a coloured
|
|
24
|
+
* circle says nothing to someone who cannot see it. Say the state where it IS read —
|
|
25
|
+
* keksdose's avatar trigger puts "3 unread" in its own `aria-label` — or give the dot
|
|
26
|
+
* an `aria-label` of its own, which makes it an image with that name.
|
|
27
|
+
*/
|
|
28
|
+
label?: ReactNode;
|
|
29
|
+
/**
|
|
30
|
+
* A ring in the surface colour round the dot, so a dot laid over the corner of an
|
|
31
|
+
* icon or an avatar reads as a separate mark and not a smudge (keksdose top-bar.tsx
|
|
32
|
+
* :152, account-menu.tsx ~385, both hand-drawing `ring-2 ring-white
|
|
33
|
+
* dark:ring-slate-900`). Position it with `className` (`absolute -end-0.5 -top-0.5`).
|
|
34
|
+
*/
|
|
35
|
+
ring?: boolean;
|
|
36
|
+
/** Extra classes for the dot itself; `className` goes on the outer element. */
|
|
37
|
+
dotClassName?: string;
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* A small token-coloured dot, with an optional label.
|
|
41
|
+
*
|
|
42
|
+
* kastlan's reporting dashboard (dashboard-tab.tsx:79) paints its occupancy legend
|
|
43
|
+
* with raw `bg-green-500` / `bg-red-500` / `bg-yellow-500`, which ignores the palette
|
|
44
|
+
* and the dark theme; keksdose draws two unread dots by hand in `rose-500`. Both are
|
|
45
|
+
* this.
|
|
46
|
+
*/
|
|
47
|
+
declare function StatusDot({ tone, size, label, ring, className, dotClassName, "aria-label": ariaLabel, ...rest }: StatusDotProps): react.JSX.Element;
|
|
48
|
+
|
|
49
|
+
export { StatusDot, type StatusDotProps, type StatusDotSize, type StatusDotTone };
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
3
|
+
import { cn } from "../lib/cn.js";
|
|
4
|
+
const FILL = {
|
|
5
|
+
brand: "bg-[var(--brand)]",
|
|
6
|
+
neutral: "bg-[var(--text-muted)]",
|
|
7
|
+
success: "bg-[var(--success)]",
|
|
8
|
+
warning: "bg-[var(--warning)]",
|
|
9
|
+
danger: "bg-[var(--danger)]",
|
|
10
|
+
info: "bg-[var(--info)]",
|
|
11
|
+
income: "bg-[var(--money-income)]",
|
|
12
|
+
expense: "bg-[var(--money-expense)]"
|
|
13
|
+
};
|
|
14
|
+
const SIZE = { sm: "size-2", md: "size-2.5", lg: "size-3" };
|
|
15
|
+
function StatusDot({
|
|
16
|
+
tone = "neutral",
|
|
17
|
+
size = "sm",
|
|
18
|
+
label,
|
|
19
|
+
ring = false,
|
|
20
|
+
className,
|
|
21
|
+
dotClassName,
|
|
22
|
+
"aria-label": ariaLabel,
|
|
23
|
+
...rest
|
|
24
|
+
}) {
|
|
25
|
+
const dot = /* @__PURE__ */ jsx(
|
|
26
|
+
"span",
|
|
27
|
+
{
|
|
28
|
+
className: cn(
|
|
29
|
+
"inline-block shrink-0 rounded-full",
|
|
30
|
+
SIZE[size],
|
|
31
|
+
FILL[tone],
|
|
32
|
+
ring && "ring-2 ring-[var(--bg-surface)]",
|
|
33
|
+
label == null && className,
|
|
34
|
+
dotClassName
|
|
35
|
+
),
|
|
36
|
+
...label == null ? ariaLabel ? { role: "img", "aria-label": ariaLabel, ...rest } : { "aria-hidden": true, ...rest } : { "aria-hidden": true }
|
|
37
|
+
}
|
|
38
|
+
);
|
|
39
|
+
if (label == null) return dot;
|
|
40
|
+
return (
|
|
41
|
+
// No `aria-label` here: the visible label is the text, and a name on a role-less
|
|
42
|
+
// span is one ARIA forbids and screen readers ignore.
|
|
43
|
+
/* @__PURE__ */ jsxs("span", { ...rest, className: cn("inline-flex min-w-0 items-center gap-1.5 text-sm", className), children: [
|
|
44
|
+
dot,
|
|
45
|
+
/* @__PURE__ */ jsx("span", { className: "min-w-0 truncate", children: label })
|
|
46
|
+
] })
|
|
47
|
+
);
|
|
48
|
+
}
|
|
49
|
+
export {
|
|
50
|
+
StatusDot
|
|
51
|
+
};
|
|
52
|
+
//# sourceMappingURL=status-dot.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/components/status-dot.tsx"],"sourcesContent":["import type { ComponentPropsWithoutRef, ReactNode } from \"react\";\nimport { cn } from \"../lib/cn\";\nimport type { ProgressBarTone } from \"./progress-bar\";\n\n/**\n * The tones are `ProgressBar`'s, name for name — which are `Chip`'s, less nothing — so a\n * dot in a legend, the bar it explains and the badge beside it can be given the same\n * word and agree. The money pair is here for the same reason it is there: a legend\n * swatch for income is money, not \"success\".\n */\nexport type StatusDotTone = ProgressBarTone;\n\n/** `sm` 8px (keksdose's unread dot on the support trigger), `md` 10px (the avatar's\n * unread dot), `lg` 12px (kastlan's occupancy legend swatch). */\nexport type StatusDotSize = \"sm\" | \"md\" | \"lg\";\n\nconst FILL: Record<StatusDotTone, string> = {\n brand: \"bg-[var(--brand)]\",\n neutral: \"bg-[var(--text-muted)]\",\n success: \"bg-[var(--success)]\",\n warning: \"bg-[var(--warning)]\",\n danger: \"bg-[var(--danger)]\",\n info: \"bg-[var(--info)]\",\n income: \"bg-[var(--money-income)]\",\n expense: \"bg-[var(--money-expense)]\",\n};\n\nconst SIZE: Record<StatusDotSize, string> = { sm: \"size-2\", md: \"size-2.5\", lg: \"size-3\" };\n\nexport interface StatusDotProps extends Omit<ComponentPropsWithoutRef<\"span\">, \"children\"> {\n tone?: StatusDotTone;\n size?: StatusDotSize;\n /**\n * Visible text beside the dot — a legend entry (\"Rented\") or a status (\"Online\"). It\n * is also what a screen reader reads: the dot itself is decorative whenever there is\n * a label.\n *\n * Without a label the dot is hidden from assistive technology, because a coloured\n * circle says nothing to someone who cannot see it. Say the state where it IS read —\n * keksdose's avatar trigger puts \"3 unread\" in its own `aria-label` — or give the dot\n * an `aria-label` of its own, which makes it an image with that name.\n */\n label?: ReactNode;\n /**\n * A ring in the surface colour round the dot, so a dot laid over the corner of an\n * icon or an avatar reads as a separate mark and not a smudge (keksdose top-bar.tsx\n * :152, account-menu.tsx ~385, both hand-drawing `ring-2 ring-white\n * dark:ring-slate-900`). Position it with `className` (`absolute -end-0.5 -top-0.5`).\n */\n ring?: boolean;\n /** Extra classes for the dot itself; `className` goes on the outer element. */\n dotClassName?: string;\n}\n\n/**\n * A small token-coloured dot, with an optional label.\n *\n * kastlan's reporting dashboard (dashboard-tab.tsx:79) paints its occupancy legend\n * with raw `bg-green-500` / `bg-red-500` / `bg-yellow-500`, which ignores the palette\n * and the dark theme; keksdose draws two unread dots by hand in `rose-500`. Both are\n * this.\n */\nexport function StatusDot({\n tone = \"neutral\",\n size = \"sm\",\n label,\n ring = false,\n className,\n dotClassName,\n \"aria-label\": ariaLabel,\n ...rest\n}: StatusDotProps) {\n const dot = (\n <span\n className={cn(\n \"inline-block shrink-0 rounded-full\",\n SIZE[size],\n FILL[tone],\n ring && \"ring-2 ring-[var(--bg-surface)]\",\n label == null && className,\n dotClassName,\n )}\n {...(label == null\n ? ariaLabel\n ? { role: \"img\", \"aria-label\": ariaLabel, ...rest }\n : { \"aria-hidden\": true, ...rest }\n : { \"aria-hidden\": true })}\n />\n );\n if (label == null) return dot;\n return (\n // No `aria-label` here: the visible label is the text, and a name on a role-less\n // span is one ARIA forbids and screen readers ignore.\n <span {...rest} className={cn(\"inline-flex min-w-0 items-center gap-1.5 text-sm\", className)}>\n {dot}\n <span className=\"min-w-0 truncate\">{label}</span>\n </span>\n );\n}\n"],"mappings":";AAyEI,cAoBA,YApBA;AAxEJ,SAAS,UAAU;AAenB,MAAM,OAAsC;AAAA,EAC1C,OAAO;AAAA,EACP,SAAS;AAAA,EACT,SAAS;AAAA,EACT,SAAS;AAAA,EACT,QAAQ;AAAA,EACR,MAAM;AAAA,EACN,QAAQ;AAAA,EACR,SAAS;AACX;AAEA,MAAM,OAAsC,EAAE,IAAI,UAAU,IAAI,YAAY,IAAI,SAAS;AAmClF,SAAS,UAAU;AAAA,EACxB,OAAO;AAAA,EACP,OAAO;AAAA,EACP;AAAA,EACA,OAAO;AAAA,EACP;AAAA,EACA;AAAA,EACA,cAAc;AAAA,EACd,GAAG;AACL,GAAmB;AACjB,QAAM,MACJ;AAAA,IAAC;AAAA;AAAA,MACC,WAAW;AAAA,QACT;AAAA,QACA,KAAK,IAAI;AAAA,QACT,KAAK,IAAI;AAAA,QACT,QAAQ;AAAA,QACR,SAAS,QAAQ;AAAA,QACjB;AAAA,MACF;AAAA,MACC,GAAI,SAAS,OACV,YACE,EAAE,MAAM,OAAO,cAAc,WAAW,GAAG,KAAK,IAChD,EAAE,eAAe,MAAM,GAAG,KAAK,IACjC,EAAE,eAAe,KAAK;AAAA;AAAA,EAC5B;AAEF,MAAI,SAAS,KAAM,QAAO;AAC1B;AAAA;AAAA;AAAA,IAGE,qBAAC,UAAM,GAAG,MAAM,WAAW,GAAG,oDAAoD,SAAS,GACxF;AAAA;AAAA,MACD,oBAAC,UAAK,WAAU,oBAAoB,iBAAM;AAAA,OAC5C;AAAA;AAEJ;","names":[]}
|
|
@@ -1,7 +1,15 @@
|
|
|
1
1
|
import * as react from 'react';
|
|
2
|
-
import { ComponentPropsWithoutRef } from 'react';
|
|
2
|
+
import { ComponentPropsWithoutRef, ReactNode } from 'react';
|
|
3
3
|
|
|
4
|
-
|
|
4
|
+
/** `none` (0.10.0): no cell padding at all — keksdose's VAT summary, a table set
|
|
5
|
+
* inside a card's own padding whose columns are spaced by hand (`pl-2` on exactly the
|
|
6
|
+
* columns that need it). Type stays at the body size, as `comfortable`. */
|
|
7
|
+
type TableDensity = "comfortable" | "compact" | "none";
|
|
8
|
+
/** CSS `table-layout`. `auto` (the browser default) sizes columns to their content;
|
|
9
|
+
* `fixed` takes widths from the first row and ignores the rest — keksdose's VAT
|
|
10
|
+
* summary switches to it while editing so two inputs stop bidding against the
|
|
11
|
+
* figures for room. */
|
|
12
|
+
type TableLayout = "auto" | "fixed";
|
|
5
13
|
type TableAlign = "start" | "center" | "end";
|
|
6
14
|
/**
|
|
7
15
|
* The alignment a figure column wants: end-aligned (so the units line up under each
|
|
@@ -20,6 +28,8 @@ interface TableProps extends ComponentPropsWithoutRef<"table"> {
|
|
|
20
28
|
hover?: boolean;
|
|
21
29
|
/** Classes for the overflow wrapper around the `<table>` (a max-height, a border). */
|
|
22
30
|
wrapperClassName?: string;
|
|
31
|
+
/** See {@link TableLayout}. Unset leaves the browser's `auto` and adds no class. */
|
|
32
|
+
layout?: TableLayout;
|
|
23
33
|
}
|
|
24
34
|
/**
|
|
25
35
|
* A plain, static HTML table in the kit's tokens — for the detail views that need
|
|
@@ -38,11 +48,34 @@ interface TableProps extends ComponentPropsWithoutRef<"table"> {
|
|
|
38
48
|
* Alignment is logical throughout (`text-start` / `text-end`), so a right-to-left
|
|
39
49
|
* page gets its first column on the right without a class changing.
|
|
40
50
|
*/
|
|
41
|
-
declare function Table({ density, zebra, hover, wrapperClassName, className, "aria-label": ariaLabel, ...rest }: TableProps): react.JSX.Element;
|
|
51
|
+
declare function Table({ density, zebra, hover, wrapperClassName, layout, className, "aria-label": ariaLabel, ...rest }: TableProps): react.JSX.Element;
|
|
42
52
|
type TableHeadProps = ComponentPropsWithoutRef<"thead">;
|
|
43
53
|
declare function TableHead({ className, ...rest }: TableHeadProps): react.JSX.Element;
|
|
44
|
-
|
|
45
|
-
|
|
54
|
+
interface TableBodyProps extends ComponentPropsWithoutRef<"tbody"> {
|
|
55
|
+
/**
|
|
56
|
+
* Shown as ONE full-width row (a {@link TableEmpty}) when the body has no rows —
|
|
57
|
+
* `children` that render nothing: an empty `.map`, a `false`, `null`. kastlan hand-
|
|
58
|
+
* writes that row with a counted `colSpan` in unit-values-editor, the meeting agenda
|
|
59
|
+
* and invitations tabs, the journal-entry and invoice detail pages, and a column
|
|
60
|
+
* added later leaves every one of those counts one short.
|
|
61
|
+
*/
|
|
62
|
+
empty?: ReactNode;
|
|
63
|
+
}
|
|
64
|
+
declare function TableBody({ className, empty, children, ...rest }: TableBodyProps): react.JSX.Element;
|
|
65
|
+
interface TableEmptyProps extends Omit<ComponentPropsWithoutRef<"tr">, "children"> {
|
|
66
|
+
children: ReactNode;
|
|
67
|
+
/** Columns to span. Default: MEASURED — the widest row of the table it renders in,
|
|
68
|
+
* counted after mount, so the row stays full width when a column is added. */
|
|
69
|
+
colSpan?: number;
|
|
70
|
+
/** Classes for the `<td>`. */
|
|
71
|
+
cellClassName?: string;
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* The "nothing here" row: one cell across every column, centred, muted. What
|
|
75
|
+
* {@link TableBodyProps.empty} renders, and usable on its own for a body whose rows
|
|
76
|
+
* are not its direct children.
|
|
77
|
+
*/
|
|
78
|
+
declare function TableEmpty({ children, colSpan, cellClassName, className, ...rest }: TableEmptyProps): react.JSX.Element;
|
|
46
79
|
type TableFootProps = ComponentPropsWithoutRef<"tfoot">;
|
|
47
80
|
/** The totals row. Not in the brief, but every ledger view has one and a `<tfoot>`
|
|
48
81
|
* is what tells a reader it is a total rather than one more row. */
|
|
@@ -80,4 +113,4 @@ type TableCaptionProps = ComponentPropsWithoutRef<"caption">;
|
|
|
80
113
|
*/
|
|
81
114
|
declare function TableCaption({ className, id, ...rest }: TableCaptionProps): react.JSX.Element;
|
|
82
115
|
|
|
83
|
-
export { NUMERIC_CELL_CLASS, Table, type TableAlign, TableBody, type TableBodyProps, TableCaption, type TableCaptionProps, TableCell, type TableCellProps, type TableDensity, TableFoot, type TableFootProps, TableHead, type TableHeadProps, TableHeaderCell, type TableHeaderCellProps, type TableProps, TableRow, type TableRowProps };
|
|
116
|
+
export { NUMERIC_CELL_CLASS, Table, type TableAlign, TableBody, type TableBodyProps, TableCaption, type TableCaptionProps, TableCell, type TableCellProps, type TableDensity, TableEmpty, type TableEmptyProps, TableFoot, type TableFootProps, TableHead, type TableHeadProps, TableHeaderCell, type TableHeaderCellProps, type TableLayout, type TableProps, TableRow, type TableRowProps };
|
package/dist/components/table.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
import { jsx } from "react/jsx-runtime";
|
|
3
|
-
import { createContext, useContext, useId, useLayoutEffect, useMemo, useRef, useState } from "react";
|
|
3
|
+
import { Children, createContext, useContext, useId, useLayoutEffect, useMemo, useRef, useState } from "react";
|
|
4
4
|
import { cn } from "../lib/cn.js";
|
|
5
5
|
import { THIN_SCROLLBAR_CLASS, useScrollOverflow } from "./scroll-area.js";
|
|
6
6
|
const TableContext = createContext({
|
|
@@ -14,7 +14,8 @@ const TableContext = createContext({
|
|
|
14
14
|
const SectionContext = createContext("body");
|
|
15
15
|
const CELL_PAD = {
|
|
16
16
|
comfortable: "px-3 py-2",
|
|
17
|
-
compact: "px-2 py-1"
|
|
17
|
+
compact: "px-2 py-1",
|
|
18
|
+
none: "p-0"
|
|
18
19
|
};
|
|
19
20
|
const ALIGN = {
|
|
20
21
|
start: "text-start",
|
|
@@ -27,6 +28,7 @@ function Table({
|
|
|
27
28
|
zebra = false,
|
|
28
29
|
hover = false,
|
|
29
30
|
wrapperClassName,
|
|
31
|
+
layout,
|
|
30
32
|
className,
|
|
31
33
|
"aria-label": ariaLabel,
|
|
32
34
|
...rest
|
|
@@ -72,6 +74,8 @@ function Table({
|
|
|
72
74
|
className: cn(
|
|
73
75
|
"w-full caption-bottom border-collapse text-sm text-[var(--text-primary)]",
|
|
74
76
|
density === "compact" && "text-xs",
|
|
77
|
+
layout === "fixed" && "table-fixed",
|
|
78
|
+
layout === "auto" && "table-auto",
|
|
75
79
|
className
|
|
76
80
|
)
|
|
77
81
|
}
|
|
@@ -82,8 +86,42 @@ function Table({
|
|
|
82
86
|
function TableHead({ className, ...rest }) {
|
|
83
87
|
return /* @__PURE__ */ jsx(SectionContext.Provider, { value: "head", children: /* @__PURE__ */ jsx("thead", { ...rest, className: cn("border-b border-[var(--border)]", className) }) });
|
|
84
88
|
}
|
|
85
|
-
function TableBody({ className, ...rest }) {
|
|
86
|
-
|
|
89
|
+
function TableBody({ className, empty, children, ...rest }) {
|
|
90
|
+
const hasRows = Children.toArray(children).length > 0;
|
|
91
|
+
return /* @__PURE__ */ jsx(SectionContext.Provider, { value: "body", children: /* @__PURE__ */ jsx("tbody", { ...rest, className: cn("[&>tr:last-child]:border-b-0", className), children: hasRows || empty === void 0 || empty === null || empty === false ? children : /* @__PURE__ */ jsx(TableEmpty, { children: empty }) }) });
|
|
92
|
+
}
|
|
93
|
+
function TableEmpty({ children, colSpan, cellClassName, className, ...rest }) {
|
|
94
|
+
const { density } = useContext(TableContext);
|
|
95
|
+
const cell = useRef(null);
|
|
96
|
+
const [measured, setMeasured] = useState(1);
|
|
97
|
+
useLayoutEffect(() => {
|
|
98
|
+
if (colSpan !== void 0) return;
|
|
99
|
+
const row = cell.current?.parentElement;
|
|
100
|
+
const table = cell.current?.closest("table");
|
|
101
|
+
if (!row || !table) return;
|
|
102
|
+
let widest = 1;
|
|
103
|
+
for (const r of Array.from(table.rows)) {
|
|
104
|
+
if (r === row) continue;
|
|
105
|
+
let n = 0;
|
|
106
|
+
for (const c of Array.from(r.cells)) n += c.colSpan || 1;
|
|
107
|
+
widest = Math.max(widest, n);
|
|
108
|
+
}
|
|
109
|
+
setMeasured(widest);
|
|
110
|
+
});
|
|
111
|
+
return /* @__PURE__ */ jsx("tr", { ...rest, "data-table-empty": "", className, children: /* @__PURE__ */ jsx(
|
|
112
|
+
"td",
|
|
113
|
+
{
|
|
114
|
+
ref: cell,
|
|
115
|
+
colSpan: colSpan ?? measured,
|
|
116
|
+
className: cn(
|
|
117
|
+
// `none` keeps the message off the edges regardless: it is text, not a figure.
|
|
118
|
+
density === "none" ? "py-2" : CELL_PAD[density],
|
|
119
|
+
"text-center text-[var(--text-muted)]",
|
|
120
|
+
cellClassName
|
|
121
|
+
),
|
|
122
|
+
children
|
|
123
|
+
}
|
|
124
|
+
) });
|
|
87
125
|
}
|
|
88
126
|
function TableFoot({ className, ...rest }) {
|
|
89
127
|
return /* @__PURE__ */ jsx(SectionContext.Provider, { value: "foot", children: /* @__PURE__ */ jsx("tfoot", { ...rest, className: cn("border-t border-[var(--border-strong)] font-medium", className) }) });
|
|
@@ -158,6 +196,7 @@ export {
|
|
|
158
196
|
TableBody,
|
|
159
197
|
TableCaption,
|
|
160
198
|
TableCell,
|
|
199
|
+
TableEmpty,
|
|
161
200
|
TableFoot,
|
|
162
201
|
TableHead,
|
|
163
202
|
TableHeaderCell,
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/components/table.tsx"],"sourcesContent":["import { createContext, useContext, useId, useLayoutEffect, useMemo, useRef, useState } from \"react\";\nimport type { ComponentPropsWithoutRef } from \"react\";\nimport { cn } from \"../lib/cn\";\nimport { THIN_SCROLLBAR_CLASS, useScrollOverflow } from \"./scroll-area\";\n\nexport type TableDensity = \"comfortable\" | \"compact\";\nexport type TableAlign = \"start\" | \"center\" | \"end\";\n\ninterface TableContextValue {\n density: TableDensity;\n zebra: boolean;\n hover: boolean;\n captionId: string;\n registerCaption: () => () => void;\n}\n\nconst TableContext = createContext<TableContextValue>({\n density: \"comfortable\",\n zebra: false,\n hover: false,\n captionId: \"\",\n registerCaption: () => () => {},\n});\n\n/** Which section a row sits in — zebra and hover apply to body rows only. */\nconst SectionContext = createContext<\"head\" | \"body\" | \"foot\">(\"body\");\n\nconst CELL_PAD: Record<TableDensity, string> = {\n comfortable: \"px-3 py-2\",\n compact: \"px-2 py-1\",\n};\n\nconst ALIGN: Record<TableAlign, string> = {\n start: \"text-start\",\n center: \"text-center\",\n end: \"text-end\",\n};\n\n/**\n * The alignment a figure column wants: end-aligned (so the units line up under each\n * other in either reading direction) with tabular digits (so they line up at all).\n * Exported for a cell the kit did not render — a DataTable column, a `<td>` of the\n * app's own.\n */\nexport const NUMERIC_CELL_CLASS = \"text-end tabular-nums\";\n\nexport interface TableProps extends ComponentPropsWithoutRef<\"table\"> {\n /** `compact` for a dense detail view (a ledger, a spec sheet). */\n density?: TableDensity;\n /** Tint every other body row. */\n zebra?: boolean;\n /** Tint the body row under the pointer. Off by default: a static table whose rows\n * light up promises a click that does nothing. */\n hover?: boolean;\n /** Classes for the overflow wrapper around the `<table>` (a max-height, a border). */\n wrapperClassName?: string;\n}\n\n/**\n * A plain, static HTML table in the kit's tokens — for the detail views that need\n * rows and columns and none of {@link DataTable}'s sorting, filtering or paging.\n *\n * kastlan renders about twenty of these on shadcn's Table parts; the part names here\n * are the same idea (`TableHead` is the `<thead>`, `TableHeaderCell` the `<th>`), so\n * the move is a rename rather than a rewrite.\n *\n * The table sits in an overflow-x wrapper, because a table cannot shrink below its\n * content and on a phone it would otherwise widen the page. While it overflows that\n * wrapper becomes a tab stop and a `role=\"region\"` named by the `TableCaption` (or the\n * table's own `aria-label`), so a keyboard user can scroll it and a reader can say\n * what it is — the same contract as {@link ScrollArea}.\n *\n * Alignment is logical throughout (`text-start` / `text-end`), so a right-to-left\n * page gets its first column on the right without a class changing.\n */\nexport function Table({\n density = \"comfortable\",\n zebra = false,\n hover = false,\n wrapperClassName,\n className,\n \"aria-label\": ariaLabel,\n ...rest\n}: TableProps) {\n const captionId = useId();\n const [captions, setCaptions] = useState(0);\n const wrapper = useRef<HTMLDivElement | null>(null);\n const overflowing = useScrollOverflow(wrapper);\n\n const ctx = useMemo<TableContextValue>(\n () => ({\n density,\n zebra,\n hover,\n captionId,\n registerCaption: () => {\n setCaptions((n) => n + 1);\n return () => setCaptions((n) => n - 1);\n },\n }),\n [density, zebra, hover, captionId],\n );\n\n const regionName = captions > 0 ? { \"aria-labelledby\": captionId } : ariaLabel ? { \"aria-label\": ariaLabel } : null;\n\n return (\n <TableContext.Provider value={ctx}>\n <div\n ref={wrapper}\n // Only a region when it scrolls AND has a name: a landmark for every short\n // table on a page would bury the real ones.\n role={overflowing && regionName ? \"region\" : undefined}\n {...(overflowing ? regionName : null)}\n tabIndex={overflowing ? 0 : undefined}\n data-overflowing={overflowing || undefined}\n className={cn(\n // `relative`: the containing block for an `sr-only` caption.\n \"relative w-full overflow-x-auto\",\n THIN_SCROLLBAR_CLASS,\n \"focus-visible:outline-2 focus-visible:-outline-offset-2 focus-visible:outline-[var(--brand)]\",\n wrapperClassName,\n )}\n >\n <table\n {...rest}\n aria-label={ariaLabel}\n className={cn(\n \"w-full caption-bottom border-collapse text-sm text-[var(--text-primary)]\",\n density === \"compact\" && \"text-xs\",\n className,\n )}\n />\n </div>\n </TableContext.Provider>\n );\n}\n\nexport type TableHeadProps = ComponentPropsWithoutRef<\"thead\">;\n\nexport function TableHead({ className, ...rest }: TableHeadProps) {\n return (\n <SectionContext.Provider value=\"head\">\n <thead {...rest} className={cn(\"border-b border-[var(--border)]\", className)} />\n </SectionContext.Provider>\n );\n}\n\nexport type TableBodyProps = ComponentPropsWithoutRef<\"tbody\">;\n\nexport function TableBody({ className, ...rest }: TableBodyProps) {\n return (\n <SectionContext.Provider value=\"body\">\n <tbody {...rest} className={cn(\"[&>tr:last-child]:border-b-0\", className)} />\n </SectionContext.Provider>\n );\n}\n\nexport type TableFootProps = ComponentPropsWithoutRef<\"tfoot\">;\n\n/** The totals row. Not in the brief, but every ledger view has one and a `<tfoot>`\n * is what tells a reader it is a total rather than one more row. */\nexport function TableFoot({ className, ...rest }: TableFootProps) {\n return (\n <SectionContext.Provider value=\"foot\">\n <tfoot {...rest} className={cn(\"border-t border-[var(--border-strong)] font-medium\", className)} />\n </SectionContext.Provider>\n );\n}\n\nexport type TableRowProps = ComponentPropsWithoutRef<\"tr\">;\n\nexport function TableRow({ className, ...rest }: TableRowProps) {\n const { zebra, hover } = useContext(TableContext);\n const section = useContext(SectionContext);\n const body = section === \"body\";\n return (\n <tr\n {...rest}\n className={cn(\n body && \"border-b border-[var(--border)]\",\n body && zebra && \"even:bg-[var(--bg-surface-2)]\",\n body && hover && \"transition-colors hover:bg-[var(--bg-hover)]\",\n className,\n )}\n />\n );\n}\n\ninterface CellAlignProps {\n /** Shorthand for a figure column: end-aligned, tabular digits. */\n numeric?: boolean;\n /** Logical alignment. Default `start` (`end` when `numeric`). */\n align?: TableAlign;\n}\n\n/** The deprecated HTML `align` attribute is replaced by a logical one. */\nexport interface TableHeaderCellProps extends Omit<ComponentPropsWithoutRef<\"th\">, \"align\">, CellAlignProps {}\n\n/**\n * A `<th>`. `scope` defaults to `col` in the head and `row` in the body — a header\n * cell in a body row is the row's label (\"Net rent\" in a key-value table), and a\n * reader needs the scope to read it with each cell beside it.\n */\nexport function TableHeaderCell({ numeric = false, align, scope, className, ...rest }: TableHeaderCellProps) {\n const { density } = useContext(TableContext);\n const section = useContext(SectionContext);\n return (\n <th\n {...rest}\n scope={scope ?? (section === \"head\" ? \"col\" : \"row\")}\n className={cn(\n CELL_PAD[density],\n ALIGN[align ?? (numeric ? \"end\" : \"start\")],\n numeric && \"tabular-nums\",\n \"align-bottom font-medium\",\n section === \"head\" ? \"text-xs text-[var(--text-muted)]\" : \"align-top text-[var(--text-secondary)]\",\n className,\n )}\n />\n );\n}\n\nexport interface TableCellProps extends Omit<ComponentPropsWithoutRef<\"td\">, \"align\">, CellAlignProps {}\n\nexport function TableCell({ numeric = false, align, className, ...rest }: TableCellProps) {\n const { density } = useContext(TableContext);\n return (\n <td\n {...rest}\n className={cn(\n CELL_PAD[density],\n \"align-top\",\n ALIGN[align ?? (numeric ? \"end\" : \"start\")],\n numeric && \"tabular-nums whitespace-nowrap\",\n className,\n )}\n />\n );\n}\n\nexport type TableCaptionProps = ComponentPropsWithoutRef<\"caption\">;\n\n/**\n * The table's title. Beneath the table by default (`caption-bottom`, the shadcn\n * convention kastlan's views already follow); `className=\"caption-top\"` moves it.\n * `className=\"sr-only\"` keeps it for readers only — the wrapper is `relative`, so\n * the hidden caption stays inside it.\n *\n * It also names the scroll region when the table overflows: it registers itself\n * with the {@link Table} so the wrapper points at an id that exists, and never at\n * one that does not.\n */\nexport function TableCaption({ className, id, ...rest }: TableCaptionProps) {\n const { captionId, registerCaption } = useContext(TableContext);\n useLayoutEffect(() => (id ? undefined : registerCaption()), [id, registerCaption]);\n return (\n <caption\n {...rest}\n id={id ?? captionId}\n className={cn(\"mt-2 text-start text-sm text-[var(--text-muted)]\", className)}\n />\n );\n}\n"],"mappings":"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","names":[]}
|
|
1
|
+
{"version":3,"sources":["../../src/components/table.tsx"],"sourcesContent":["import { Children, createContext, useContext, useId, useLayoutEffect, useMemo, useRef, useState } from \"react\";\nimport type { ComponentPropsWithoutRef, ReactNode } from \"react\";\nimport { cn } from \"../lib/cn\";\nimport { THIN_SCROLLBAR_CLASS, useScrollOverflow } from \"./scroll-area\";\n\n/** `none` (0.10.0): no cell padding at all — keksdose's VAT summary, a table set\n * inside a card's own padding whose columns are spaced by hand (`pl-2` on exactly the\n * columns that need it). Type stays at the body size, as `comfortable`. */\nexport type TableDensity = \"comfortable\" | \"compact\" | \"none\";\n/** CSS `table-layout`. `auto` (the browser default) sizes columns to their content;\n * `fixed` takes widths from the first row and ignores the rest — keksdose's VAT\n * summary switches to it while editing so two inputs stop bidding against the\n * figures for room. */\nexport type TableLayout = \"auto\" | \"fixed\";\nexport type TableAlign = \"start\" | \"center\" | \"end\";\n\ninterface TableContextValue {\n density: TableDensity;\n zebra: boolean;\n hover: boolean;\n captionId: string;\n registerCaption: () => () => void;\n}\n\nconst TableContext = createContext<TableContextValue>({\n density: \"comfortable\",\n zebra: false,\n hover: false,\n captionId: \"\",\n registerCaption: () => () => {},\n});\n\n/** Which section a row sits in — zebra and hover apply to body rows only. */\nconst SectionContext = createContext<\"head\" | \"body\" | \"foot\">(\"body\");\n\nconst CELL_PAD: Record<TableDensity, string> = {\n comfortable: \"px-3 py-2\",\n compact: \"px-2 py-1\",\n none: \"p-0\",\n};\n\nconst ALIGN: Record<TableAlign, string> = {\n start: \"text-start\",\n center: \"text-center\",\n end: \"text-end\",\n};\n\n/**\n * The alignment a figure column wants: end-aligned (so the units line up under each\n * other in either reading direction) with tabular digits (so they line up at all).\n * Exported for a cell the kit did not render — a DataTable column, a `<td>` of the\n * app's own.\n */\nexport const NUMERIC_CELL_CLASS = \"text-end tabular-nums\";\n\nexport interface TableProps extends ComponentPropsWithoutRef<\"table\"> {\n /** `compact` for a dense detail view (a ledger, a spec sheet). */\n density?: TableDensity;\n /** Tint every other body row. */\n zebra?: boolean;\n /** Tint the body row under the pointer. Off by default: a static table whose rows\n * light up promises a click that does nothing. */\n hover?: boolean;\n /** Classes for the overflow wrapper around the `<table>` (a max-height, a border). */\n wrapperClassName?: string;\n /** See {@link TableLayout}. Unset leaves the browser's `auto` and adds no class. */\n layout?: TableLayout;\n}\n\n/**\n * A plain, static HTML table in the kit's tokens — for the detail views that need\n * rows and columns and none of {@link DataTable}'s sorting, filtering or paging.\n *\n * kastlan renders about twenty of these on shadcn's Table parts; the part names here\n * are the same idea (`TableHead` is the `<thead>`, `TableHeaderCell` the `<th>`), so\n * the move is a rename rather than a rewrite.\n *\n * The table sits in an overflow-x wrapper, because a table cannot shrink below its\n * content and on a phone it would otherwise widen the page. While it overflows that\n * wrapper becomes a tab stop and a `role=\"region\"` named by the `TableCaption` (or the\n * table's own `aria-label`), so a keyboard user can scroll it and a reader can say\n * what it is — the same contract as {@link ScrollArea}.\n *\n * Alignment is logical throughout (`text-start` / `text-end`), so a right-to-left\n * page gets its first column on the right without a class changing.\n */\nexport function Table({\n density = \"comfortable\",\n zebra = false,\n hover = false,\n wrapperClassName,\n layout,\n className,\n \"aria-label\": ariaLabel,\n ...rest\n}: TableProps) {\n const captionId = useId();\n const [captions, setCaptions] = useState(0);\n const wrapper = useRef<HTMLDivElement | null>(null);\n const overflowing = useScrollOverflow(wrapper);\n\n const ctx = useMemo<TableContextValue>(\n () => ({\n density,\n zebra,\n hover,\n captionId,\n registerCaption: () => {\n setCaptions((n) => n + 1);\n return () => setCaptions((n) => n - 1);\n },\n }),\n [density, zebra, hover, captionId],\n );\n\n const regionName = captions > 0 ? { \"aria-labelledby\": captionId } : ariaLabel ? { \"aria-label\": ariaLabel } : null;\n\n return (\n <TableContext.Provider value={ctx}>\n <div\n ref={wrapper}\n // Only a region when it scrolls AND has a name: a landmark for every short\n // table on a page would bury the real ones.\n role={overflowing && regionName ? \"region\" : undefined}\n {...(overflowing ? regionName : null)}\n tabIndex={overflowing ? 0 : undefined}\n data-overflowing={overflowing || undefined}\n className={cn(\n // `relative`: the containing block for an `sr-only` caption.\n \"relative w-full overflow-x-auto\",\n THIN_SCROLLBAR_CLASS,\n \"focus-visible:outline-2 focus-visible:-outline-offset-2 focus-visible:outline-[var(--brand)]\",\n wrapperClassName,\n )}\n >\n <table\n {...rest}\n aria-label={ariaLabel}\n className={cn(\n \"w-full caption-bottom border-collapse text-sm text-[var(--text-primary)]\",\n density === \"compact\" && \"text-xs\",\n layout === \"fixed\" && \"table-fixed\",\n layout === \"auto\" && \"table-auto\",\n className,\n )}\n />\n </div>\n </TableContext.Provider>\n );\n}\n\nexport type TableHeadProps = ComponentPropsWithoutRef<\"thead\">;\n\nexport function TableHead({ className, ...rest }: TableHeadProps) {\n return (\n <SectionContext.Provider value=\"head\">\n <thead {...rest} className={cn(\"border-b border-[var(--border)]\", className)} />\n </SectionContext.Provider>\n );\n}\n\nexport interface TableBodyProps extends ComponentPropsWithoutRef<\"tbody\"> {\n /**\n * Shown as ONE full-width row (a {@link TableEmpty}) when the body has no rows —\n * `children` that render nothing: an empty `.map`, a `false`, `null`. kastlan hand-\n * writes that row with a counted `colSpan` in unit-values-editor, the meeting agenda\n * and invitations tabs, the journal-entry and invoice detail pages, and a column\n * added later leaves every one of those counts one short.\n */\n empty?: ReactNode;\n}\n\nexport function TableBody({ className, empty, children, ...rest }: TableBodyProps) {\n // `toArray` drops null/undefined/booleans and flattens arrays, so `[[], false]` —\n // an empty map beside a conditional row that is off — is no rows.\n const hasRows = Children.toArray(children).length > 0;\n return (\n <SectionContext.Provider value=\"body\">\n <tbody {...rest} className={cn(\"[&>tr:last-child]:border-b-0\", className)}>\n {hasRows || empty === undefined || empty === null || empty === false ? (\n children\n ) : (\n <TableEmpty>{empty}</TableEmpty>\n )}\n </tbody>\n </SectionContext.Provider>\n );\n}\n\nexport interface TableEmptyProps extends Omit<ComponentPropsWithoutRef<\"tr\">, \"children\"> {\n children: ReactNode;\n /** Columns to span. Default: MEASURED — the widest row of the table it renders in,\n * counted after mount, so the row stays full width when a column is added. */\n colSpan?: number;\n /** Classes for the `<td>`. */\n cellClassName?: string;\n}\n\n/**\n * The \"nothing here\" row: one cell across every column, centred, muted. What\n * {@link TableBodyProps.empty} renders, and usable on its own for a body whose rows\n * are not its direct children.\n */\nexport function TableEmpty({ children, colSpan, cellClassName, className, ...rest }: TableEmptyProps) {\n const { density } = useContext(TableContext);\n const cell = useRef<HTMLTableCellElement | null>(null);\n const [measured, setMeasured] = useState(1);\n // Every render, deliberately: a column can appear in the head without anything this\n // row depends on changing. It cannot loop — setting the same count is a bail-out.\n // eslint-disable-next-line react-hooks/exhaustive-deps\n useLayoutEffect(() => {\n if (colSpan !== undefined) return;\n const row = cell.current?.parentElement;\n const table = cell.current?.closest(\"table\");\n if (!row || !table) return;\n let widest = 1;\n for (const r of Array.from(table.rows)) {\n if (r === row) continue;\n let n = 0;\n for (const c of Array.from(r.cells)) n += c.colSpan || 1;\n widest = Math.max(widest, n);\n }\n setMeasured(widest);\n });\n return (\n <tr {...rest} data-table-empty=\"\" className={className}>\n <td\n ref={cell}\n colSpan={colSpan ?? measured}\n className={cn(\n // `none` keeps the message off the edges regardless: it is text, not a figure.\n density === \"none\" ? \"py-2\" : CELL_PAD[density],\n \"text-center text-[var(--text-muted)]\",\n cellClassName,\n )}\n >\n {children}\n </td>\n </tr>\n );\n}\n\nexport type TableFootProps = ComponentPropsWithoutRef<\"tfoot\">;\n\n/** The totals row. Not in the brief, but every ledger view has one and a `<tfoot>`\n * is what tells a reader it is a total rather than one more row. */\nexport function TableFoot({ className, ...rest }: TableFootProps) {\n return (\n <SectionContext.Provider value=\"foot\">\n <tfoot {...rest} className={cn(\"border-t border-[var(--border-strong)] font-medium\", className)} />\n </SectionContext.Provider>\n );\n}\n\nexport type TableRowProps = ComponentPropsWithoutRef<\"tr\">;\n\nexport function TableRow({ className, ...rest }: TableRowProps) {\n const { zebra, hover } = useContext(TableContext);\n const section = useContext(SectionContext);\n const body = section === \"body\";\n return (\n <tr\n {...rest}\n className={cn(\n body && \"border-b border-[var(--border)]\",\n body && zebra && \"even:bg-[var(--bg-surface-2)]\",\n body && hover && \"transition-colors hover:bg-[var(--bg-hover)]\",\n className,\n )}\n />\n );\n}\n\ninterface CellAlignProps {\n /** Shorthand for a figure column: end-aligned, tabular digits. */\n numeric?: boolean;\n /** Logical alignment. Default `start` (`end` when `numeric`). */\n align?: TableAlign;\n}\n\n/** The deprecated HTML `align` attribute is replaced by a logical one. */\nexport interface TableHeaderCellProps extends Omit<ComponentPropsWithoutRef<\"th\">, \"align\">, CellAlignProps {}\n\n/**\n * A `<th>`. `scope` defaults to `col` in the head and `row` in the body — a header\n * cell in a body row is the row's label (\"Net rent\" in a key-value table), and a\n * reader needs the scope to read it with each cell beside it.\n */\nexport function TableHeaderCell({ numeric = false, align, scope, className, ...rest }: TableHeaderCellProps) {\n const { density } = useContext(TableContext);\n const section = useContext(SectionContext);\n return (\n <th\n {...rest}\n scope={scope ?? (section === \"head\" ? \"col\" : \"row\")}\n className={cn(\n CELL_PAD[density],\n ALIGN[align ?? (numeric ? \"end\" : \"start\")],\n numeric && \"tabular-nums\",\n \"align-bottom font-medium\",\n section === \"head\" ? \"text-xs text-[var(--text-muted)]\" : \"align-top text-[var(--text-secondary)]\",\n className,\n )}\n />\n );\n}\n\nexport interface TableCellProps extends Omit<ComponentPropsWithoutRef<\"td\">, \"align\">, CellAlignProps {}\n\nexport function TableCell({ numeric = false, align, className, ...rest }: TableCellProps) {\n const { density } = useContext(TableContext);\n return (\n <td\n {...rest}\n className={cn(\n CELL_PAD[density],\n \"align-top\",\n ALIGN[align ?? (numeric ? \"end\" : \"start\")],\n numeric && \"tabular-nums whitespace-nowrap\",\n className,\n )}\n />\n );\n}\n\nexport type TableCaptionProps = ComponentPropsWithoutRef<\"caption\">;\n\n/**\n * The table's title. Beneath the table by default (`caption-bottom`, the shadcn\n * convention kastlan's views already follow); `className=\"caption-top\"` moves it.\n * `className=\"sr-only\"` keeps it for readers only — the wrapper is `relative`, so\n * the hidden caption stays inside it.\n *\n * It also names the scroll region when the table overflows: it registers itself\n * with the {@link Table} so the wrapper points at an id that exists, and never at\n * one that does not.\n */\nexport function TableCaption({ className, id, ...rest }: TableCaptionProps) {\n const { captionId, registerCaption } = useContext(TableContext);\n useLayoutEffect(() => (id ? undefined : registerCaption()), [id, registerCaption]);\n return (\n <caption\n {...rest}\n id={id ?? captionId}\n className={cn(\"mt-2 text-start text-sm text-[var(--text-muted)]\", className)}\n />\n );\n}\n"],"mappings":"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","names":[]}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import * as react from 'react';
|
|
2
|
+
import { ComponentPropsWithoutRef, ReactNode } from 'react';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* `xs` is 10px — the size `LegendGroup`'s title and the top-bar menus' heading row draw;
|
|
6
|
+
* `sm` (default) is 12px — the size `StatTile`'s label and lenkbank's section headings
|
|
7
|
+
* (control-page.tsx:119/123/127) draw. Two sizes because the label sits either inside a
|
|
8
|
+
* dense panel (a legend, a menu) or above a block of page content, and one size looked
|
|
9
|
+
* wrong in the other place.
|
|
10
|
+
*/
|
|
11
|
+
type SectionLabelSize = "xs" | "sm";
|
|
12
|
+
/**
|
|
13
|
+
* The small, uppercase, tracked, muted label — as a class string, per size.
|
|
14
|
+
*
|
|
15
|
+
* Exported because `LegendGroup` and `StatTile` already draw it inline, and a class a
|
|
16
|
+
* caller has to position itself (a `<th>`, a `<legend>`, a menu `<li>`) is better
|
|
17
|
+
* reached as a string than wrapped in a component that only takes a `className`. The
|
|
18
|
+
* uppercase is CSS, so a screen reader reads the text as written: write it in normal
|
|
19
|
+
* case.
|
|
20
|
+
*/
|
|
21
|
+
declare const SECTION_LABEL_CLASS: Record<SectionLabelSize, string>;
|
|
22
|
+
type SectionLabelElement = "h2" | "h3" | "h4" | "h5" | "h6" | "p" | "span" | "div" | "legend";
|
|
23
|
+
interface SectionLabelProps extends ComponentPropsWithoutRef<"h3"> {
|
|
24
|
+
/**
|
|
25
|
+
* The element — which is the heading LEVEL, and so is the caller's to pick: the kit
|
|
26
|
+
* cannot know where in the outline the label sits. Default `h3`, which is what
|
|
27
|
+
* lenkbank's control page writes under its `h2` card title. Pass `span` or `p` for a
|
|
28
|
+
* label that is not a heading at all (a legend group's title).
|
|
29
|
+
*/
|
|
30
|
+
as?: SectionLabelElement;
|
|
31
|
+
size?: SectionLabelSize;
|
|
32
|
+
children: ReactNode;
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* A section's small uppercase label. lenkbank spells
|
|
36
|
+
* `text-xs font-semibold uppercase tracking-wide text-slate-500 dark:text-slate-400`
|
|
37
|
+
* on every `<h3>` of its control page and more; keksdose's budget switcher writes the
|
|
38
|
+
* 10px version on a menu row. One component, the token colour, and the level chosen
|
|
39
|
+
* where the outline is known.
|
|
40
|
+
*/
|
|
41
|
+
declare function SectionLabel({ as, size, className, children, ...rest }: SectionLabelProps): react.JSX.Element;
|
|
42
|
+
/**
|
|
43
|
+
* The caption type as a class string: 11px, snug, muted. lenkbank's `CAPTION`
|
|
44
|
+
* (shared/ui/caption.ts:23) — written out 31 times before it was a constant — as a
|
|
45
|
+
* token colour rather than `slate-500 dark:slate-400`, so it follows the palette.
|
|
46
|
+
* Exported for the same reason lenkbank's is a string: every caller positions it
|
|
47
|
+
* differently (`mt-1`, `self-end pb-2`, `border-t`), and what must be one thing is the
|
|
48
|
+
* type, not the box.
|
|
49
|
+
*/
|
|
50
|
+
declare const CAPTION_CLASS = "text-[11px] leading-snug text-[var(--text-muted)]";
|
|
51
|
+
interface CaptionProps extends ComponentPropsWithoutRef<"p"> {
|
|
52
|
+
/** Default `p`; `span` for a caption inside a line of other content. */
|
|
53
|
+
as?: "p" | "span" | "div";
|
|
54
|
+
children: ReactNode;
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* The small grey sentence under a field, a table or a button — prose ABOUT the thing
|
|
58
|
+
* above it. Not `FieldHint`, which hides behind a field label's "?" so a paragraph does
|
|
59
|
+
* not make one field taller than its neighbour (lenkbank feedback #27).
|
|
60
|
+
*/
|
|
61
|
+
declare function Caption({ as, className, children, ...rest }: CaptionProps): react.JSX.Element;
|
|
62
|
+
|
|
63
|
+
export { CAPTION_CLASS, Caption, type CaptionProps, SECTION_LABEL_CLASS, SectionLabel, type SectionLabelProps, type SectionLabelSize };
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx } from "react/jsx-runtime";
|
|
3
|
+
import { cn } from "../lib/cn.js";
|
|
4
|
+
const SECTION_LABEL_CLASS = {
|
|
5
|
+
xs: "text-[10px] font-semibold uppercase tracking-wide text-[var(--text-muted)]",
|
|
6
|
+
sm: "text-xs font-semibold uppercase tracking-wide text-[var(--text-muted)]"
|
|
7
|
+
};
|
|
8
|
+
function SectionLabel({ as = "h3", size = "sm", className, children, ...rest }) {
|
|
9
|
+
const Tag = as;
|
|
10
|
+
return /* @__PURE__ */ jsx(Tag, { ...rest, className: cn(SECTION_LABEL_CLASS[size], className), children });
|
|
11
|
+
}
|
|
12
|
+
const CAPTION_CLASS = "text-[11px] leading-snug text-[var(--text-muted)]";
|
|
13
|
+
function Caption({ as = "p", className, children, ...rest }) {
|
|
14
|
+
const Tag = as;
|
|
15
|
+
return /* @__PURE__ */ jsx(Tag, { ...rest, className: cn(CAPTION_CLASS, className), children });
|
|
16
|
+
}
|
|
17
|
+
export {
|
|
18
|
+
CAPTION_CLASS,
|
|
19
|
+
Caption,
|
|
20
|
+
SECTION_LABEL_CLASS,
|
|
21
|
+
SectionLabel
|
|
22
|
+
};
|
|
23
|
+
//# sourceMappingURL=text.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/components/text.tsx"],"sourcesContent":["import type { ComponentPropsWithoutRef, ElementType, ReactNode } from \"react\";\nimport { cn } from \"../lib/cn\";\n\n/* ── SectionLabel ─────────────────────────────────────────────────────────── */\n\n/**\n * `xs` is 10px — the size `LegendGroup`'s title and the top-bar menus' heading row draw;\n * `sm` (default) is 12px — the size `StatTile`'s label and lenkbank's section headings\n * (control-page.tsx:119/123/127) draw. Two sizes because the label sits either inside a\n * dense panel (a legend, a menu) or above a block of page content, and one size looked\n * wrong in the other place.\n */\nexport type SectionLabelSize = \"xs\" | \"sm\";\n\n/**\n * The small, uppercase, tracked, muted label — as a class string, per size.\n *\n * Exported because `LegendGroup` and `StatTile` already draw it inline, and a class a\n * caller has to position itself (a `<th>`, a `<legend>`, a menu `<li>`) is better\n * reached as a string than wrapped in a component that only takes a `className`. The\n * uppercase is CSS, so a screen reader reads the text as written: write it in normal\n * case.\n */\nexport const SECTION_LABEL_CLASS: Record<SectionLabelSize, string> = {\n xs: \"text-[10px] font-semibold uppercase tracking-wide text-[var(--text-muted)]\",\n sm: \"text-xs font-semibold uppercase tracking-wide text-[var(--text-muted)]\",\n};\n\ntype SectionLabelElement = \"h2\" | \"h3\" | \"h4\" | \"h5\" | \"h6\" | \"p\" | \"span\" | \"div\" | \"legend\";\n\nexport interface SectionLabelProps extends ComponentPropsWithoutRef<\"h3\"> {\n /**\n * The element — which is the heading LEVEL, and so is the caller's to pick: the kit\n * cannot know where in the outline the label sits. Default `h3`, which is what\n * lenkbank's control page writes under its `h2` card title. Pass `span` or `p` for a\n * label that is not a heading at all (a legend group's title).\n */\n as?: SectionLabelElement;\n size?: SectionLabelSize;\n children: ReactNode;\n}\n\n/**\n * A section's small uppercase label. lenkbank spells\n * `text-xs font-semibold uppercase tracking-wide text-slate-500 dark:text-slate-400`\n * on every `<h3>` of its control page and more; keksdose's budget switcher writes the\n * 10px version on a menu row. One component, the token colour, and the level chosen\n * where the outline is known.\n */\nexport function SectionLabel({ as = \"h3\", size = \"sm\", className, children, ...rest }: SectionLabelProps) {\n const Tag = as as ElementType;\n return (\n <Tag {...rest} className={cn(SECTION_LABEL_CLASS[size], className)}>\n {children}\n </Tag>\n );\n}\n\n/* ── Caption ──────────────────────────────────────────────────────────────── */\n\n/**\n * The caption type as a class string: 11px, snug, muted. lenkbank's `CAPTION`\n * (shared/ui/caption.ts:23) — written out 31 times before it was a constant — as a\n * token colour rather than `slate-500 dark:slate-400`, so it follows the palette.\n * Exported for the same reason lenkbank's is a string: every caller positions it\n * differently (`mt-1`, `self-end pb-2`, `border-t`), and what must be one thing is the\n * type, not the box.\n */\nexport const CAPTION_CLASS = \"text-[11px] leading-snug text-[var(--text-muted)]\";\n\nexport interface CaptionProps extends ComponentPropsWithoutRef<\"p\"> {\n /** Default `p`; `span` for a caption inside a line of other content. */\n as?: \"p\" | \"span\" | \"div\";\n children: ReactNode;\n}\n\n/**\n * The small grey sentence under a field, a table or a button — prose ABOUT the thing\n * above it. Not `FieldHint`, which hides behind a field label's \"?\" so a paragraph does\n * not make one field taller than its neighbour (lenkbank feedback #27).\n */\nexport function Caption({ as = \"p\", className, children, ...rest }: CaptionProps) {\n const Tag = as as ElementType;\n return (\n <Tag {...rest} className={cn(CAPTION_CLASS, className)}>\n {children}\n </Tag>\n );\n}\n"],"mappings":";AAoDI;AAnDJ,SAAS,UAAU;AAsBZ,MAAM,sBAAwD;AAAA,EACnE,IAAI;AAAA,EACJ,IAAI;AACN;AAuBO,SAAS,aAAa,EAAE,KAAK,MAAM,OAAO,MAAM,WAAW,UAAU,GAAG,KAAK,GAAsB;AACxG,QAAM,MAAM;AACZ,SACE,oBAAC,OAAK,GAAG,MAAM,WAAW,GAAG,oBAAoB,IAAI,GAAG,SAAS,GAC9D,UACH;AAEJ;AAYO,MAAM,gBAAgB;AAatB,SAAS,QAAQ,EAAE,KAAK,KAAK,WAAW,UAAU,GAAG,KAAK,GAAiB;AAChF,QAAM,MAAM;AACZ,SACE,oBAAC,OAAK,GAAG,MAAM,WAAW,GAAG,eAAe,SAAS,GAClD,UACH;AAEJ;","names":[]}
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
import * as react from 'react';
|
|
2
|
+
import { ReactNode, MouseEvent, CSSProperties, ReactElement } from 'react';
|
|
3
|
+
|
|
4
|
+
/** The words the toast layer says. `close` and `notifications` are read by a screen
|
|
5
|
+
* reader (the close button's name, the toast region's name); `undo` and `redo` are
|
|
6
|
+
* the default action labels of {@link toast.undo} / {@link toast.redo}. */
|
|
7
|
+
interface ToastLabels {
|
|
8
|
+
close: string;
|
|
9
|
+
notifications: string;
|
|
10
|
+
undo: string;
|
|
11
|
+
redo: string;
|
|
12
|
+
}
|
|
13
|
+
declare const DEFAULT_TOAST_LABELS: ToastLabels;
|
|
14
|
+
type ToastId = string | number;
|
|
15
|
+
/** A button on the toast. Clicking it dismisses the toast, unless `onClick` calls
|
|
16
|
+
* `event.preventDefault()`. */
|
|
17
|
+
interface ToastAction {
|
|
18
|
+
label: ReactNode;
|
|
19
|
+
onClick: (event: MouseEvent<HTMLButtonElement>) => void;
|
|
20
|
+
}
|
|
21
|
+
interface ToastOptions {
|
|
22
|
+
/** Re-using an id REPLACES the toast on screen with that id (in place — it does not
|
|
23
|
+
* stack a second one), which is how a toast is updated. Default: a fresh id. */
|
|
24
|
+
id?: ToastId;
|
|
25
|
+
description?: ReactNode;
|
|
26
|
+
action?: ToastAction;
|
|
27
|
+
/** A second, quieter button (sonner's `cancel`). */
|
|
28
|
+
cancel?: ToastAction;
|
|
29
|
+
/** Milliseconds; `Infinity` keeps it until dismissed. Default 4s — or
|
|
30
|
+
* {@link TOAST_ACTION_DURATION} when there is an `action`, so there is time to
|
|
31
|
+
* read the message AND reach the button. */
|
|
32
|
+
duration?: number;
|
|
33
|
+
/** Called when the toast is closed by the user (close button, swipe, action). */
|
|
34
|
+
onDismiss?: (toast: {
|
|
35
|
+
id: ToastId;
|
|
36
|
+
}) => void;
|
|
37
|
+
/** Called when the toast times out. */
|
|
38
|
+
onAutoClose?: (toast: {
|
|
39
|
+
id: ToastId;
|
|
40
|
+
}) => void;
|
|
41
|
+
/** Wrap the message and the description in `data-private`, so an app's demo /
|
|
42
|
+
* privacy mode blurs them like every other sensitive value. */
|
|
43
|
+
redact?: boolean;
|
|
44
|
+
/** Replaces the tone's icon. */
|
|
45
|
+
icon?: ReactNode;
|
|
46
|
+
/** `false`: no close button, no swipe. */
|
|
47
|
+
dismissible?: boolean;
|
|
48
|
+
/** Per toast, over the `<Toaster>`'s `closeButton`. */
|
|
49
|
+
closeButton?: boolean;
|
|
50
|
+
className?: string;
|
|
51
|
+
style?: CSSProperties;
|
|
52
|
+
}
|
|
53
|
+
/** The default duration of a toast that carries an action (8s rather than 4s). */
|
|
54
|
+
declare const TOAST_ACTION_DURATION = 8000;
|
|
55
|
+
type Kind = "default" | "success" | "error" | "warning" | "info" | "loading";
|
|
56
|
+
interface ToastPromiseOptions<T> extends ToastOptions {
|
|
57
|
+
loading: ReactNode;
|
|
58
|
+
/** The success message, or a function of the resolved value. Omitted: the loading
|
|
59
|
+
* toast is dismissed. */
|
|
60
|
+
success?: ReactNode | ((value: T) => ReactNode);
|
|
61
|
+
/** The error message, or a function of the rejection. Omitted: dismissed. */
|
|
62
|
+
error?: ReactNode | ((error: unknown) => ReactNode);
|
|
63
|
+
finally?: () => void;
|
|
64
|
+
}
|
|
65
|
+
interface ToastUndoOptions extends Omit<ToastOptions, "action"> {
|
|
66
|
+
/** Called when the button is pressed. */
|
|
67
|
+
onUndo: () => void;
|
|
68
|
+
/** Default: `toast.undo` from `<UiKitProvider>` ("Undo"). */
|
|
69
|
+
label?: ReactNode;
|
|
70
|
+
/** Default `success` — the step the toast reports worked. */
|
|
71
|
+
tone?: Exclude<Kind, "loading">;
|
|
72
|
+
}
|
|
73
|
+
interface ToastRedoOptions extends Omit<ToastUndoOptions, "onUndo"> {
|
|
74
|
+
onRedo: () => void;
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* Show a toast. The same shape as sonner's `toast`, so an app migrates by changing the
|
|
78
|
+
* import: `toast(msg, opts)`, `toast.success` / `error` / `warning` / `info` /
|
|
79
|
+
* `loading`, `toast.promise`, `toast.dismiss`, `toast.custom`. Each returns the toast's
|
|
80
|
+
* id; pass it back as `id` to replace that toast in place.
|
|
81
|
+
*
|
|
82
|
+
* Needs a mounted {@link Toaster}. Messages may be any ReactNode.
|
|
83
|
+
*/
|
|
84
|
+
declare const toast: ((message: ReactNode, opts?: ToastOptions) => ToastId) & {
|
|
85
|
+
message: (message: ReactNode, opts?: ToastOptions) => ToastId;
|
|
86
|
+
success: (message: ReactNode, opts?: ToastOptions) => ToastId;
|
|
87
|
+
error: (message: ReactNode, opts?: ToastOptions) => ToastId;
|
|
88
|
+
warning: (message: ReactNode, opts?: ToastOptions) => ToastId;
|
|
89
|
+
info: (message: ReactNode, opts?: ToastOptions) => ToastId;
|
|
90
|
+
/** A spinner that does not time out — replace it (same `id`) or dismiss it. */
|
|
91
|
+
loading: (message: ReactNode, opts?: ToastOptions) => ToastId;
|
|
92
|
+
/**
|
|
93
|
+
* A loading toast that becomes the success or the error toast, in place, when
|
|
94
|
+
* `promise` settles. Returns the toast's id; the promise itself is the caller's.
|
|
95
|
+
*/
|
|
96
|
+
promise<T>(promise: Promise<T> | (() => Promise<T>), options: ToastPromiseOptions<T>): ToastId;
|
|
97
|
+
/** Close one toast, or every toast when `id` is omitted. */
|
|
98
|
+
dismiss(id?: ToastId): void;
|
|
99
|
+
/** Render your own node in a toast slot — `render` receives the id to dismiss it. */
|
|
100
|
+
custom(render: (id: ToastId) => ReactElement, opts?: Omit<ToastOptions, "redact">): ToastId;
|
|
101
|
+
/**
|
|
102
|
+
* "Deleted — Undo": a toast whose action takes the step back, labelled by the
|
|
103
|
+
* provider's `toast.undo`. The journal stays the app's; this is only its toast.
|
|
104
|
+
* Pair with {@link toast.redo} so the step back can itself be taken back.
|
|
105
|
+
*/
|
|
106
|
+
undo(message: ReactNode, { onUndo, ...opts }: ToastUndoOptions): ToastId;
|
|
107
|
+
/** {@link toast.undo}'s mirror: the action is labelled `toast.redo`. */
|
|
108
|
+
redo(message: ReactNode, { onRedo, ...opts }: ToastRedoOptions): ToastId;
|
|
109
|
+
};
|
|
110
|
+
type ToastPosition = "top-left" | "top-center" | "top-right" | "bottom-left" | "bottom-center" | "bottom-right";
|
|
111
|
+
type ToastSwipeDirection = "top" | "right" | "bottom" | "left";
|
|
112
|
+
/** sonner's offset: one value for every edge, or per edge. Numbers are px. */
|
|
113
|
+
type ToasterOffset = string | number | {
|
|
114
|
+
top?: string | number;
|
|
115
|
+
right?: string | number;
|
|
116
|
+
bottom?: string | number;
|
|
117
|
+
left?: string | number;
|
|
118
|
+
};
|
|
119
|
+
interface ToasterProps {
|
|
120
|
+
/** Default: `bottom-center` on a phone (so the toast covers neither the header's
|
|
121
|
+
* buttons nor the bottom nav's), `top-center` from `md` up. */
|
|
122
|
+
position?: ToastPosition;
|
|
123
|
+
/** The height to keep clear at the bottom — AppShell's bottom nav. Default: its
|
|
124
|
+
* measured `--app-nav-h`, so nothing has to be passed under an AppShell. The safe
|
|
125
|
+
* area is added either way. Numbers are px. */
|
|
126
|
+
navOffset?: string | number;
|
|
127
|
+
/** Default: follows the `.dark` class the kit's theme store (`useApplyTheme`) puts
|
|
128
|
+
* on `<html>` — whichever store the app built, with nothing passed. */
|
|
129
|
+
theme?: "light" | "dark" | "system";
|
|
130
|
+
labels?: Partial<ToastLabels>;
|
|
131
|
+
/** Default 4s (8s for a toast with an action). */
|
|
132
|
+
duration?: number;
|
|
133
|
+
visibleToasts?: number;
|
|
134
|
+
expand?: boolean;
|
|
135
|
+
/** Default true. */
|
|
136
|
+
closeButton?: boolean;
|
|
137
|
+
/** Tone-coloured toasts (the AlertBanner tokens). Default true. */
|
|
138
|
+
richColors?: boolean;
|
|
139
|
+
/** Default: every direction. */
|
|
140
|
+
swipeDirections?: ToastSwipeDirection[];
|
|
141
|
+
/** Overrides the computed offset outright (above the nav on a phone, below a 48px
|
|
142
|
+
* top bar on desktop). */
|
|
143
|
+
offset?: ToasterOffset;
|
|
144
|
+
/** Below 600px, where sonner switches to its full-width layout. Default: `offset`'s
|
|
145
|
+
* phone value. */
|
|
146
|
+
mobileOffset?: ToasterOffset;
|
|
147
|
+
/** Keyboard shortcut that moves focus into the toasts. Default Alt+T. */
|
|
148
|
+
hotkey?: string[];
|
|
149
|
+
gap?: number;
|
|
150
|
+
dir?: "ltr" | "rtl" | "auto";
|
|
151
|
+
className?: string;
|
|
152
|
+
style?: CSSProperties;
|
|
153
|
+
}
|
|
154
|
+
/**
|
|
155
|
+
* Where the toasts appear, with the kit's defaults — mount ONE, near the root and
|
|
156
|
+
* inside `<UiKitProvider>` (it reads the `toast` labels) and inside the router if a
|
|
157
|
+
* toast's content navigates.
|
|
158
|
+
*
|
|
159
|
+
* Accessible as sonner makes it: the toasts sit in a polite live region, so a message
|
|
160
|
+
* is announced with its description and its action's label; every toast and every
|
|
161
|
+
* button in it is focusable, the timer pauses while focus or the pointer is inside,
|
|
162
|
+
* and Alt+T ({@link ToasterProps.hotkey}) moves focus to the newest toast. A toast
|
|
163
|
+
* with an action stays up 8s by default, so the button can be reached.
|
|
164
|
+
*/
|
|
165
|
+
declare function Toaster({ position, navOffset, theme, labels: labelsProp, closeButton, richColors, swipeDirections, offset, mobileOffset, className, style, ...rest }: ToasterProps): react.JSX.Element | null;
|
|
166
|
+
|
|
167
|
+
export { DEFAULT_TOAST_LABELS, TOAST_ACTION_DURATION, type ToastAction, type ToastId, type ToastLabels, type ToastOptions, type ToastPosition, type ToastPromiseOptions, type ToastRedoOptions, type ToastSwipeDirection, type ToastUndoOptions, Toaster, type ToasterOffset, type ToasterProps, toast };
|