@tea-ui/core 0.1.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/LICENSE +21 -0
- package/README.md +33 -0
- package/dist/card.d.ts +60 -0
- package/dist/card.js +116 -0
- package/dist/card.js.map +1 -0
- package/dist/feedback/alert.d.ts +57 -0
- package/dist/feedback/alert.js +104 -0
- package/dist/feedback/alert.js.map +1 -0
- package/dist/feedback/badge.d.ts +34 -0
- package/dist/feedback/badge.js +62 -0
- package/dist/feedback/badge.js.map +1 -0
- package/dist/feedback/index.d.ts +13 -0
- package/dist/feedback/index.js +62 -0
- package/dist/feedback/index.js.map +1 -0
- package/dist/feedback/progress.d.ts +25 -0
- package/dist/feedback/progress.js +49 -0
- package/dist/feedback/progress.js.map +1 -0
- package/dist/feedback/skeleton.d.ts +38 -0
- package/dist/feedback/skeleton.js +49 -0
- package/dist/feedback/skeleton.js.map +1 -0
- package/dist/feedback/spinner.d.ts +31 -0
- package/dist/feedback/spinner.js +55 -0
- package/dist/feedback/spinner.js.map +1 -0
- package/dist/feedback/status.d.ts +63 -0
- package/dist/feedback/status.js +156 -0
- package/dist/feedback/status.js.map +1 -0
- package/dist/feedback/toast.d.ts +72 -0
- package/dist/feedback/toast.js +211 -0
- package/dist/feedback/toast.js.map +1 -0
- package/dist/format/index.d.ts +63 -0
- package/dist/format/index.js +84 -0
- package/dist/format/index.js.map +1 -0
- package/dist/index.d.ts +43 -0
- package/dist/index.js +163 -0
- package/dist/index.js.map +1 -0
- package/dist/inputs/button-group.d.ts +70 -0
- package/dist/inputs/button-group.js +59 -0
- package/dist/inputs/button-group.js.map +1 -0
- package/dist/inputs/button.d.ts +52 -0
- package/dist/inputs/button.js +84 -0
- package/dist/inputs/button.js.map +1 -0
- package/dist/inputs/checkbox.d.ts +48 -0
- package/dist/inputs/checkbox.js +74 -0
- package/dist/inputs/checkbox.js.map +1 -0
- package/dist/inputs/combobox.d.ts +77 -0
- package/dist/inputs/combobox.js +273 -0
- package/dist/inputs/combobox.js.map +1 -0
- package/dist/inputs/field.d.ts +220 -0
- package/dist/inputs/field.js +183 -0
- package/dist/inputs/field.js.map +1 -0
- package/dist/inputs/icon-button.d.ts +44 -0
- package/dist/inputs/icon-button.js +30 -0
- package/dist/inputs/icon-button.js.map +1 -0
- package/dist/inputs/index.d.ts +22 -0
- package/dist/inputs/index.js +92 -0
- package/dist/inputs/index.js.map +1 -0
- package/dist/inputs/input-group.d.ts +77 -0
- package/dist/inputs/input-group.js +76 -0
- package/dist/inputs/input-group.js.map +1 -0
- package/dist/inputs/input.d.ts +58 -0
- package/dist/inputs/input.js +68 -0
- package/dist/inputs/input.js.map +1 -0
- package/dist/inputs/label.d.ts +47 -0
- package/dist/inputs/label.js +29 -0
- package/dist/inputs/label.js.map +1 -0
- package/dist/inputs/number-input.d.ts +62 -0
- package/dist/inputs/number-input.js +86 -0
- package/dist/inputs/number-input.js.map +1 -0
- package/dist/inputs/password-input.d.ts +66 -0
- package/dist/inputs/password-input.js +73 -0
- package/dist/inputs/password-input.js.map +1 -0
- package/dist/inputs/radio.d.ts +66 -0
- package/dist/inputs/radio.js +96 -0
- package/dist/inputs/radio.js.map +1 -0
- package/dist/inputs/search-input.d.ts +66 -0
- package/dist/inputs/search-input.js +76 -0
- package/dist/inputs/search-input.js.map +1 -0
- package/dist/inputs/select.d.ts +65 -0
- package/dist/inputs/select.js +139 -0
- package/dist/inputs/select.js.map +1 -0
- package/dist/inputs/slider.d.ts +67 -0
- package/dist/inputs/slider.js +107 -0
- package/dist/inputs/slider.js.map +1 -0
- package/dist/inputs/switch.d.ts +42 -0
- package/dist/inputs/switch.js +69 -0
- package/dist/inputs/switch.js.map +1 -0
- package/dist/inputs/textarea.d.ts +51 -0
- package/dist/inputs/textarea.js +82 -0
- package/dist/inputs/textarea.js.map +1 -0
- package/dist/inputs/toggle.d.ts +57 -0
- package/dist/inputs/toggle.js +91 -0
- package/dist/inputs/toggle.js.map +1 -0
- package/dist/internal/index.d.ts +5 -0
- package/dist/internal/index.js +19 -0
- package/dist/internal/index.js.map +1 -0
- package/dist/internal/refs.d.ts +31 -0
- package/dist/internal/refs.js +34 -0
- package/dist/internal/refs.js.map +1 -0
- package/dist/internal/slot.d.ts +76 -0
- package/dist/internal/slot.js +40 -0
- package/dist/internal/slot.js.map +1 -0
- package/dist/internal/use-controllable-state.d.ts +25 -0
- package/dist/internal/use-controllable-state.js +38 -0
- package/dist/internal/use-controllable-state.js.map +1 -0
- package/dist/layout/index.d.ts +131 -0
- package/dist/layout/index.js +212 -0
- package/dist/layout/index.js.map +1 -0
- package/dist/navigation/index.d.ts +114 -0
- package/dist/navigation/index.js +523 -0
- package/dist/navigation/index.js.map +1 -0
- package/dist/overlays/alert-dialog.d.ts +75 -0
- package/dist/overlays/alert-dialog.js +163 -0
- package/dist/overlays/alert-dialog.js.map +1 -0
- package/dist/overlays/dialog.d.ts +44 -0
- package/dist/overlays/dialog.js +121 -0
- package/dist/overlays/dialog.js.map +1 -0
- package/dist/overlays/drawer.d.ts +49 -0
- package/dist/overlays/drawer.js +125 -0
- package/dist/overlays/drawer.js.map +1 -0
- package/dist/overlays/dropdown-menu.d.ts +53 -0
- package/dist/overlays/dropdown-menu.js +192 -0
- package/dist/overlays/dropdown-menu.js.map +1 -0
- package/dist/overlays/index.d.ts +10 -0
- package/dist/overlays/index.js +169 -0
- package/dist/overlays/index.js.map +1 -0
- package/dist/overlays/popover.d.ts +58 -0
- package/dist/overlays/popover.js +105 -0
- package/dist/overlays/popover.js.map +1 -0
- package/dist/styles.css +2 -0
- package/dist/typography/index.d.ts +79 -0
- package/dist/typography/index.js +280 -0
- package/dist/typography/index.js.map +1 -0
- package/package.json +60 -0
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import * as React from 'react';
|
|
2
|
+
import { Progress as Progress$1 } from 'radix-ui';
|
|
3
|
+
import { Tone } from '@tea-ui/tokens';
|
|
4
|
+
|
|
5
|
+
interface ProgressProps extends Omit<React.ComponentProps<typeof Progress$1.Root>, "children" | "value"> {
|
|
6
|
+
/** Current value. Omit together with `indeterminate` for unknown progress. */
|
|
7
|
+
value?: number | null | undefined;
|
|
8
|
+
/** The maximum. Defaults to 100. */
|
|
9
|
+
max?: number | undefined;
|
|
10
|
+
/** The total is not known. */
|
|
11
|
+
indeterminate?: boolean | undefined;
|
|
12
|
+
/** Semantic tone of the filled portion. */
|
|
13
|
+
tone?: Tone | undefined;
|
|
14
|
+
/** The accessible name. Required. */
|
|
15
|
+
label: string;
|
|
16
|
+
/**
|
|
17
|
+
* Human-readable value, e.g. "3 von 8". Without it a screen reader announces
|
|
18
|
+
* a bare percentage, which is rarely the thing the user needs to know.
|
|
19
|
+
*/
|
|
20
|
+
valueText?: string | undefined;
|
|
21
|
+
className?: string | undefined;
|
|
22
|
+
}
|
|
23
|
+
declare const Progress: React.ForwardRefExoticComponent<Omit<ProgressProps, "ref"> & React.RefAttributes<HTMLDivElement>>;
|
|
24
|
+
|
|
25
|
+
export { Progress, type ProgressProps };
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
2
|
+
import * as React from "react";
|
|
3
|
+
import { Progress as ProgressPrimitive } from "radix-ui";
|
|
4
|
+
import { cn } from "@tea-ui/utils";
|
|
5
|
+
import { dataSlot, stateAttributes } from "../internal/index.js";
|
|
6
|
+
const TONE_FILL = {
|
|
7
|
+
positive: "bg-positive",
|
|
8
|
+
info: "bg-info",
|
|
9
|
+
caution: "bg-caution",
|
|
10
|
+
critical: "bg-critical",
|
|
11
|
+
neutral: "bg-fg-muted"
|
|
12
|
+
};
|
|
13
|
+
const Progress = React.forwardRef(function Progress2({ value, max = 100, indeterminate = false, tone = "info", label, valueText, className, ...props }, ref) {
|
|
14
|
+
const isIndeterminate = indeterminate || value === null || value === void 0;
|
|
15
|
+
const percent = isIndeterminate ? 0 : Math.min(100, Math.max(0, (value ?? 0) / max * 100));
|
|
16
|
+
return /* @__PURE__ */ jsxs("div", { className: cn("w-full", className), ...dataSlot("progress", "root"), children: [
|
|
17
|
+
/* @__PURE__ */ jsx(
|
|
18
|
+
ProgressPrimitive.Root,
|
|
19
|
+
{
|
|
20
|
+
ref,
|
|
21
|
+
value: isIndeterminate ? null : value ?? 0,
|
|
22
|
+
max,
|
|
23
|
+
"aria-label": label,
|
|
24
|
+
"aria-valuetext": valueText,
|
|
25
|
+
className: cn("relative h-2 w-full overflow-hidden bg-surface-3", "rounded-pill"),
|
|
26
|
+
...dataSlot("progress", "track"),
|
|
27
|
+
...stateAttributes({ loading: isIndeterminate }),
|
|
28
|
+
...props,
|
|
29
|
+
children: /* @__PURE__ */ jsx(
|
|
30
|
+
ProgressPrimitive.Indicator,
|
|
31
|
+
{
|
|
32
|
+
className: cn(
|
|
33
|
+
"h-full transition-[width] duration-slow ease-standard",
|
|
34
|
+
TONE_FILL[tone],
|
|
35
|
+
isIndeterminate && "w-1/3 animate-pulse"
|
|
36
|
+
),
|
|
37
|
+
style: isIndeterminate ? void 0 : { width: `${percent}%` },
|
|
38
|
+
...dataSlot("progress", "indicator")
|
|
39
|
+
}
|
|
40
|
+
)
|
|
41
|
+
}
|
|
42
|
+
),
|
|
43
|
+
isIndeterminate ? /* @__PURE__ */ jsx("span", { className: "sr-only", children: "Wird verarbeitet" }) : null
|
|
44
|
+
] });
|
|
45
|
+
});
|
|
46
|
+
export {
|
|
47
|
+
Progress
|
|
48
|
+
};
|
|
49
|
+
//# sourceMappingURL=progress.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/feedback/progress.tsx"],"sourcesContent":["import * as React from \"react\";\nimport { Progress as ProgressPrimitive } from \"radix-ui\";\nimport type { Tone } from \"@tea-ui/tokens\";\nimport { cn } from \"@tea-ui/utils\";\n\nimport { dataSlot, stateAttributes } from \"../internal\";\n\n/**\n * TEA UI — Progress.\n *\n * Determinate and indeterminate must be distinguishable **without colour**. A\n * moving bar reads as \"working, no total known\"; a bar with a filled portion\n * reads as \"N of M\". Colour is the third signal, never the only one.\n *\n * Under `prefers-reduced-motion` the token stylesheet neutralises the movement,\n * so the indeterminate state would become a static, meaningless bar. It\n * therefore also carries a text label, which is the signal that survives.\n */\nconst TONE_FILL: Record<Tone, string> = {\n positive: \"bg-positive\",\n info: \"bg-info\",\n caution: \"bg-caution\",\n critical: \"bg-critical\",\n neutral: \"bg-fg-muted\",\n};\n\nexport interface ProgressProps\n extends Omit<React.ComponentProps<typeof ProgressPrimitive.Root>, \"children\" | \"value\"> {\n /** Current value. Omit together with `indeterminate` for unknown progress. */\n value?: number | null | undefined;\n /** The maximum. Defaults to 100. */\n max?: number | undefined;\n /** The total is not known. */\n indeterminate?: boolean | undefined;\n /** Semantic tone of the filled portion. */\n tone?: Tone | undefined;\n /** The accessible name. Required. */\n label: string;\n /**\n * Human-readable value, e.g. \"3 von 8\". Without it a screen reader announces\n * a bare percentage, which is rarely the thing the user needs to know.\n */\n valueText?: string | undefined;\n className?: string | undefined;\n}\n\nexport const Progress = React.forwardRef<HTMLDivElement, ProgressProps>(function Progress(\n { value, max = 100, indeterminate = false, tone = \"info\", label, valueText, className, ...props },\n ref,\n) {\n const isIndeterminate = indeterminate || value === null || value === undefined;\n const percent = isIndeterminate ? 0 : Math.min(100, Math.max(0, ((value ?? 0) / max) * 100));\n\n return (\n <div className={cn(\"w-full\", className)} {...dataSlot(\"progress\", \"root\")}>\n <ProgressPrimitive.Root\n ref={ref}\n value={isIndeterminate ? null : (value ?? 0)}\n max={max}\n aria-label={label}\n aria-valuetext={valueText}\n className={cn(\"relative h-2 w-full overflow-hidden bg-surface-3\", \"rounded-pill\")}\n {...dataSlot(\"progress\", \"track\")}\n {...stateAttributes({ loading: isIndeterminate })}\n {...props}\n >\n <ProgressPrimitive.Indicator\n className={cn(\n \"h-full transition-[width] duration-slow ease-standard\",\n TONE_FILL[tone],\n isIndeterminate && \"w-1/3 animate-pulse\",\n )}\n style={isIndeterminate ? undefined : { width: `${percent}%` }}\n {...dataSlot(\"progress\", \"indicator\")}\n />\n </ProgressPrimitive.Root>\n {isIndeterminate ? <span className=\"sr-only\">Wird verarbeitet</span> : null}\n </div>\n );\n});\n"],"mappings":"AAsDI,SAYI,KAZJ;AAtDJ,YAAY,WAAW;AACvB,SAAS,YAAY,yBAAyB;AAE9C,SAAS,UAAU;AAEnB,SAAS,UAAU,uBAAuB;AAa1C,MAAM,YAAkC;AAAA,EACtC,UAAU;AAAA,EACV,MAAM;AAAA,EACN,SAAS;AAAA,EACT,UAAU;AAAA,EACV,SAAS;AACX;AAsBO,MAAM,WAAW,MAAM,WAA0C,SAASA,UAC/E,EAAE,OAAO,MAAM,KAAK,gBAAgB,OAAO,OAAO,QAAQ,OAAO,WAAW,WAAW,GAAG,MAAM,GAChG,KACA;AACA,QAAM,kBAAkB,iBAAiB,UAAU,QAAQ,UAAU;AACrE,QAAM,UAAU,kBAAkB,IAAI,KAAK,IAAI,KAAK,KAAK,IAAI,IAAK,SAAS,KAAK,MAAO,GAAG,CAAC;AAE3F,SACE,qBAAC,SAAI,WAAW,GAAG,UAAU,SAAS,GAAI,GAAG,SAAS,YAAY,MAAM,GACtE;AAAA;AAAA,MAAC,kBAAkB;AAAA,MAAlB;AAAA,QACC;AAAA,QACA,OAAO,kBAAkB,OAAQ,SAAS;AAAA,QAC1C;AAAA,QACA,cAAY;AAAA,QACZ,kBAAgB;AAAA,QAChB,WAAW,GAAG,oDAAoD,cAAc;AAAA,QAC/E,GAAG,SAAS,YAAY,OAAO;AAAA,QAC/B,GAAG,gBAAgB,EAAE,SAAS,gBAAgB,CAAC;AAAA,QAC/C,GAAG;AAAA,QAEJ;AAAA,UAAC,kBAAkB;AAAA,UAAlB;AAAA,YACC,WAAW;AAAA,cACT;AAAA,cACA,UAAU,IAAI;AAAA,cACd,mBAAmB;AAAA,YACrB;AAAA,YACA,OAAO,kBAAkB,SAAY,EAAE,OAAO,GAAG,OAAO,IAAI;AAAA,YAC3D,GAAG,SAAS,YAAY,WAAW;AAAA;AAAA,QACtC;AAAA;AAAA,IACF;AAAA,IACC,kBAAkB,oBAAC,UAAK,WAAU,WAAU,8BAAgB,IAAU;AAAA,KACzE;AAEJ,CAAC;","names":["Progress"]}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import * as class_variance_authority_types from 'class-variance-authority/types';
|
|
2
|
+
import * as React from 'react';
|
|
3
|
+
import { VariantProps } from '@tea-ui/utils';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* TEA UI — Skeleton.
|
|
7
|
+
*
|
|
8
|
+
* The *default* loading affordance in TEA UI, per the performance UX standard.
|
|
9
|
+
* A skeleton that mirrors the real layout is strictly better than a spinner: it
|
|
10
|
+
* tells the user what is arriving, it reserves the space so the page does not
|
|
11
|
+
* jump, and it never replaces content the user is already reading.
|
|
12
|
+
*
|
|
13
|
+
* It is decorative by definition — it depicts nothing that exists — so it is
|
|
14
|
+
* `aria-hidden`. The loading *message* is a separate, real element; `label`
|
|
15
|
+
* renders it as visually hidden text, or you pass the message to whatever
|
|
16
|
+
* announces the region. The audit found eight hand-written "Lade …" loaders in
|
|
17
|
+
* one product and a fully built, completely unused `Skeleton` beside them.
|
|
18
|
+
*/
|
|
19
|
+
declare const skeletonVariants: (props?: ({
|
|
20
|
+
shape?: "circle" | "rect" | "pill" | null | undefined;
|
|
21
|
+
} & class_variance_authority_types.ClassProp) | undefined) => string;
|
|
22
|
+
interface SkeletonProps extends Omit<React.ComponentProps<"div">, "children">, VariantProps<typeof skeletonVariants> {
|
|
23
|
+
/**
|
|
24
|
+
* Accessible text. Omit when the surrounding region already announces that it
|
|
25
|
+
* is loading — never leave a bare animated block with no context anywhere.
|
|
26
|
+
*/
|
|
27
|
+
label?: string | undefined;
|
|
28
|
+
}
|
|
29
|
+
declare const Skeleton: React.ForwardRefExoticComponent<Omit<SkeletonProps, "ref"> & React.RefAttributes<HTMLDivElement>>;
|
|
30
|
+
interface SkeletonTextProps extends React.ComponentProps<"div"> {
|
|
31
|
+
/** How many lines to reserve. */
|
|
32
|
+
lines?: number | undefined;
|
|
33
|
+
className?: string | undefined;
|
|
34
|
+
}
|
|
35
|
+
/** A paragraph-shaped skeleton, for text blocks of unknown length. */
|
|
36
|
+
declare const SkeletonText: React.ForwardRefExoticComponent<Omit<SkeletonTextProps, "ref"> & React.RefAttributes<HTMLDivElement>>;
|
|
37
|
+
|
|
38
|
+
export { Skeleton, type SkeletonProps, SkeletonText, type SkeletonTextProps, skeletonVariants };
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { Fragment, jsx, jsxs } from "react/jsx-runtime";
|
|
2
|
+
import * as React from "react";
|
|
3
|
+
import { cn, cva } from "@tea-ui/utils";
|
|
4
|
+
import { COPY } from "@tea-ui/ux-standards";
|
|
5
|
+
import { dataSlot } from "../internal/index.js";
|
|
6
|
+
const skeletonVariants = cva("animate-pulse bg-surface-3", {
|
|
7
|
+
variants: {
|
|
8
|
+
shape: {
|
|
9
|
+
rect: "rounded-none",
|
|
10
|
+
pill: "rounded-pill",
|
|
11
|
+
circle: "rounded-pill"
|
|
12
|
+
}
|
|
13
|
+
},
|
|
14
|
+
defaultVariants: { shape: "rect" }
|
|
15
|
+
});
|
|
16
|
+
const Skeleton = React.forwardRef(function Skeleton2({ className, shape, label, style, ...props }, ref) {
|
|
17
|
+
return /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
18
|
+
/* @__PURE__ */ jsx(
|
|
19
|
+
"div",
|
|
20
|
+
{
|
|
21
|
+
ref,
|
|
22
|
+
"aria-hidden": label ? void 0 : true,
|
|
23
|
+
className: cn(skeletonVariants({ shape }), className),
|
|
24
|
+
"data-tea-skeleton": true,
|
|
25
|
+
"data-tea-motion": "decorative",
|
|
26
|
+
style: shape === "circle" ? { borderRadius: "9999px", ...style } : style,
|
|
27
|
+
...dataSlot("skeleton"),
|
|
28
|
+
...props
|
|
29
|
+
}
|
|
30
|
+
),
|
|
31
|
+
label ? /* @__PURE__ */ jsx("span", { className: "sr-only", children: label || COPY.states.loading }) : null
|
|
32
|
+
] });
|
|
33
|
+
});
|
|
34
|
+
const SkeletonText = React.forwardRef(function SkeletonText2({ lines = 3, className, ...props }, ref) {
|
|
35
|
+
return /* @__PURE__ */ jsx("div", { ref, className: cn("flex flex-col gap-2", className), ...dataSlot("skeleton", "text"), ...props, children: Array.from({ length: Math.max(1, lines) }, (_, index) => /* @__PURE__ */ jsx(
|
|
36
|
+
Skeleton,
|
|
37
|
+
{
|
|
38
|
+
className: cn("h-3", index === lines - 1 ? "w-2/3" : "w-full"),
|
|
39
|
+
...dataSlot("skeleton", "line")
|
|
40
|
+
},
|
|
41
|
+
index
|
|
42
|
+
)) });
|
|
43
|
+
});
|
|
44
|
+
export {
|
|
45
|
+
Skeleton,
|
|
46
|
+
SkeletonText,
|
|
47
|
+
skeletonVariants
|
|
48
|
+
};
|
|
49
|
+
//# sourceMappingURL=skeleton.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/feedback/skeleton.tsx"],"sourcesContent":["import * as React from \"react\";\nimport { cn, cva, type VariantProps } from \"@tea-ui/utils\";\nimport { COPY } from \"@tea-ui/ux-standards\";\n\nimport { dataSlot } from \"../internal\";\n\n/**\n * TEA UI — Skeleton.\n *\n * The *default* loading affordance in TEA UI, per the performance UX standard.\n * A skeleton that mirrors the real layout is strictly better than a spinner: it\n * tells the user what is arriving, it reserves the space so the page does not\n * jump, and it never replaces content the user is already reading.\n *\n * It is decorative by definition — it depicts nothing that exists — so it is\n * `aria-hidden`. The loading *message* is a separate, real element; `label`\n * renders it as visually hidden text, or you pass the message to whatever\n * announces the region. The audit found eight hand-written \"Lade …\" loaders in\n * one product and a fully built, completely unused `Skeleton` beside them.\n */\nexport const skeletonVariants = cva(\"animate-pulse bg-surface-3\", {\n variants: {\n shape: {\n rect: \"rounded-none\",\n pill: \"rounded-pill\",\n circle: \"rounded-pill\",\n },\n },\n defaultVariants: { shape: \"rect\" },\n});\n\nexport interface SkeletonProps\n extends Omit<React.ComponentProps<\"div\">, \"children\">,\n VariantProps<typeof skeletonVariants> {\n /**\n * Accessible text. Omit when the surrounding region already announces that it\n * is loading — never leave a bare animated block with no context anywhere.\n */\n label?: string | undefined;\n}\n\nexport const Skeleton = React.forwardRef<HTMLDivElement, SkeletonProps>(function Skeleton(\n { className, shape, label, style, ...props },\n ref,\n) {\n return (\n <>\n <div\n ref={ref}\n aria-hidden={label ? undefined : true}\n className={cn(skeletonVariants({ shape }), className)}\n data-tea-skeleton\n data-tea-motion=\"decorative\"\n style={shape === \"circle\" ? { borderRadius: \"9999px\", ...style } : style}\n {...dataSlot(\"skeleton\")}\n {...props}\n />\n {label ? <span className=\"sr-only\">{label || COPY.states.loading}</span> : null}\n </>\n );\n});\n\nexport interface SkeletonTextProps extends React.ComponentProps<\"div\"> {\n /** How many lines to reserve. */\n lines?: number | undefined;\n className?: string | undefined;\n}\n\n/** A paragraph-shaped skeleton, for text blocks of unknown length. */\nexport const SkeletonText = React.forwardRef<HTMLDivElement, SkeletonTextProps>(function SkeletonText(\n { lines = 3, className, ...props },\n ref,\n) {\n return (\n <div ref={ref} className={cn(\"flex flex-col gap-2\", className)} {...dataSlot(\"skeleton\", \"text\")} {...props}>\n {Array.from({ length: Math.max(1, lines) }, (_, index) => (\n <Skeleton\n key={index}\n className={cn(\"h-3\", index === lines - 1 ? \"w-2/3\" : \"w-full\")}\n {...dataSlot(\"skeleton\", \"line\")}\n />\n ))}\n </div>\n );\n});\n"],"mappings":"AA8CI,mBACE,KADF;AA9CJ,YAAY,WAAW;AACvB,SAAS,IAAI,WAA8B;AAC3C,SAAS,YAAY;AAErB,SAAS,gBAAgB;AAgBlB,MAAM,mBAAmB,IAAI,8BAA8B;AAAA,EAChE,UAAU;AAAA,IACR,OAAO;AAAA,MACL,MAAM;AAAA,MACN,MAAM;AAAA,MACN,QAAQ;AAAA,IACV;AAAA,EACF;AAAA,EACA,iBAAiB,EAAE,OAAO,OAAO;AACnC,CAAC;AAYM,MAAM,WAAW,MAAM,WAA0C,SAASA,UAC/E,EAAE,WAAW,OAAO,OAAO,OAAO,GAAG,MAAM,GAC3C,KACA;AACA,SACE,iCACE;AAAA;AAAA,MAAC;AAAA;AAAA,QACC;AAAA,QACA,eAAa,QAAQ,SAAY;AAAA,QACjC,WAAW,GAAG,iBAAiB,EAAE,MAAM,CAAC,GAAG,SAAS;AAAA,QACpD,qBAAiB;AAAA,QACjB,mBAAgB;AAAA,QAChB,OAAO,UAAU,WAAW,EAAE,cAAc,UAAU,GAAG,MAAM,IAAI;AAAA,QAClE,GAAG,SAAS,UAAU;AAAA,QACtB,GAAG;AAAA;AAAA,IACN;AAAA,IACC,QAAQ,oBAAC,UAAK,WAAU,WAAW,mBAAS,KAAK,OAAO,SAAQ,IAAU;AAAA,KAC7E;AAEJ,CAAC;AASM,MAAM,eAAe,MAAM,WAA8C,SAASC,cACvF,EAAE,QAAQ,GAAG,WAAW,GAAG,MAAM,GACjC,KACA;AACA,SACE,oBAAC,SAAI,KAAU,WAAW,GAAG,uBAAuB,SAAS,GAAI,GAAG,SAAS,YAAY,MAAM,GAAI,GAAG,OACnG,gBAAM,KAAK,EAAE,QAAQ,KAAK,IAAI,GAAG,KAAK,EAAE,GAAG,CAAC,GAAG,UAC9C;AAAA,IAAC;AAAA;AAAA,MAEC,WAAW,GAAG,OAAO,UAAU,QAAQ,IAAI,UAAU,QAAQ;AAAA,MAC5D,GAAG,SAAS,YAAY,MAAM;AAAA;AAAA,IAF1B;AAAA,EAGP,CACD,GACH;AAEJ,CAAC;","names":["Skeleton","SkeletonText"]}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import * as React from 'react';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* TEA UI — Spinner.
|
|
5
|
+
*
|
|
6
|
+
* The spinner is the *least* preferred loading affordance in TEA UI: it says
|
|
7
|
+
* "something is happening" without saying what or how much. Use a skeleton when
|
|
8
|
+
* there is a layout to mirror, determinate progress when the total is known,
|
|
9
|
+
* and a spinner only when neither applies.
|
|
10
|
+
*
|
|
11
|
+
* Two things it must get right, both of which the audit found broken:
|
|
12
|
+
* - It announces itself. `role="status"` plus a text label, so a screen reader
|
|
13
|
+
* user learns that the wait started. A bare spinning SVG is silent.
|
|
14
|
+
* - It survives reduced motion. Rotation is the one animation that carries
|
|
15
|
+
* meaning, so rather than freezing it, `data-tea-motion="essential"` swaps it
|
|
16
|
+
* for a non-rotating pulse in the token stylesheet.
|
|
17
|
+
*/
|
|
18
|
+
type SpinnerSize = "sm" | "md" | "lg";
|
|
19
|
+
interface SpinnerProps extends Omit<React.ComponentProps<"svg">, "children"> {
|
|
20
|
+
/** Visible size. */
|
|
21
|
+
size?: SpinnerSize;
|
|
22
|
+
/**
|
|
23
|
+
* Accessible name. Defaults to "Wird geladen". Pass an empty string only if
|
|
24
|
+
* the surrounding surface already announces the wait — never pass nothing.
|
|
25
|
+
*/
|
|
26
|
+
label?: string;
|
|
27
|
+
className?: string | undefined;
|
|
28
|
+
}
|
|
29
|
+
declare const Spinner: React.ForwardRefExoticComponent<Omit<SpinnerProps, "ref"> & React.RefAttributes<SVGSVGElement>>;
|
|
30
|
+
|
|
31
|
+
export { Spinner, type SpinnerProps, type SpinnerSize };
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
2
|
+
import * as React from "react";
|
|
3
|
+
import { cn } from "@tea-ui/utils";
|
|
4
|
+
import { COPY } from "@tea-ui/ux-standards";
|
|
5
|
+
import { dataSlot } from "../internal/index.js";
|
|
6
|
+
const SIZES = {
|
|
7
|
+
sm: "size-3.5",
|
|
8
|
+
md: "size-4",
|
|
9
|
+
lg: "size-6"
|
|
10
|
+
};
|
|
11
|
+
const Spinner = React.forwardRef(function Spinner2({ size = "md", label = COPY.states.loading, className, ...props }, ref) {
|
|
12
|
+
return /* @__PURE__ */ jsxs(
|
|
13
|
+
"span",
|
|
14
|
+
{
|
|
15
|
+
role: "status",
|
|
16
|
+
className: "inline-flex items-center gap-2",
|
|
17
|
+
...dataSlot("spinner", "root"),
|
|
18
|
+
children: [
|
|
19
|
+
/* @__PURE__ */ jsxs(
|
|
20
|
+
"svg",
|
|
21
|
+
{
|
|
22
|
+
ref,
|
|
23
|
+
viewBox: "0 0 24 24",
|
|
24
|
+
fill: "none",
|
|
25
|
+
"aria-hidden": label ? void 0 : true,
|
|
26
|
+
className: cn(
|
|
27
|
+
"shrink-0 animate-spin text-current",
|
|
28
|
+
SIZES[size],
|
|
29
|
+
className
|
|
30
|
+
),
|
|
31
|
+
"data-tea-motion": "essential",
|
|
32
|
+
...props,
|
|
33
|
+
children: [
|
|
34
|
+
/* @__PURE__ */ jsx("circle", { cx: "12", cy: "12", r: "9", stroke: "currentColor", strokeWidth: "2.5", opacity: "0.25" }),
|
|
35
|
+
/* @__PURE__ */ jsx(
|
|
36
|
+
"path",
|
|
37
|
+
{
|
|
38
|
+
d: "M21 12a9 9 0 0 0-9-9",
|
|
39
|
+
stroke: "currentColor",
|
|
40
|
+
strokeWidth: "2.5",
|
|
41
|
+
strokeLinecap: "butt"
|
|
42
|
+
}
|
|
43
|
+
)
|
|
44
|
+
]
|
|
45
|
+
}
|
|
46
|
+
),
|
|
47
|
+
label ? /* @__PURE__ */ jsx("span", { className: "sr-only", children: label }) : null
|
|
48
|
+
]
|
|
49
|
+
}
|
|
50
|
+
);
|
|
51
|
+
});
|
|
52
|
+
export {
|
|
53
|
+
Spinner
|
|
54
|
+
};
|
|
55
|
+
//# sourceMappingURL=spinner.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/feedback/spinner.tsx"],"sourcesContent":["import * as React from \"react\";\nimport { cn } from \"@tea-ui/utils\";\nimport { COPY } from \"@tea-ui/ux-standards\";\n\nimport { dataSlot } from \"../internal\";\n\n/**\n * TEA UI — Spinner.\n *\n * The spinner is the *least* preferred loading affordance in TEA UI: it says\n * \"something is happening\" without saying what or how much. Use a skeleton when\n * there is a layout to mirror, determinate progress when the total is known,\n * and a spinner only when neither applies.\n *\n * Two things it must get right, both of which the audit found broken:\n * - It announces itself. `role=\"status\"` plus a text label, so a screen reader\n * user learns that the wait started. A bare spinning SVG is silent.\n * - It survives reduced motion. Rotation is the one animation that carries\n * meaning, so rather than freezing it, `data-tea-motion=\"essential\"` swaps it\n * for a non-rotating pulse in the token stylesheet.\n */\nexport type SpinnerSize = \"sm\" | \"md\" | \"lg\";\n\nexport interface SpinnerProps extends Omit<React.ComponentProps<\"svg\">, \"children\"> {\n /** Visible size. */\n size?: SpinnerSize;\n /**\n * Accessible name. Defaults to \"Wird geladen\". Pass an empty string only if\n * the surrounding surface already announces the wait — never pass nothing.\n */\n label?: string;\n className?: string | undefined;\n}\n\nconst SIZES: Record<SpinnerSize, string> = {\n sm: \"size-3.5\",\n md: \"size-4\",\n lg: \"size-6\",\n};\n\nexport const Spinner = React.forwardRef<SVGSVGElement, SpinnerProps>(function Spinner(\n { size = \"md\", label = COPY.states.loading, className, ...props },\n ref,\n) {\n return (\n <span\n role=\"status\"\n className=\"inline-flex items-center gap-2\"\n {...dataSlot(\"spinner\", \"root\")}\n >\n <svg\n ref={ref}\n viewBox=\"0 0 24 24\"\n fill=\"none\"\n aria-hidden={label ? undefined : true}\n className={cn(\n \"shrink-0 animate-spin text-current\",\n SIZES[size],\n className,\n )}\n data-tea-motion=\"essential\"\n {...props}\n >\n <circle cx=\"12\" cy=\"12\" r=\"9\" stroke=\"currentColor\" strokeWidth=\"2.5\" opacity=\"0.25\" />\n <path\n d=\"M21 12a9 9 0 0 0-9-9\"\n stroke=\"currentColor\"\n strokeWidth=\"2.5\"\n strokeLinecap=\"butt\"\n />\n </svg>\n {label ? <span className=\"sr-only\">{label}</span> : null}\n </span>\n );\n});\n"],"mappings":"AAkDM,SAaE,KAbF;AAlDL,YAAY,WAAW;AACxB,SAAS,UAAU;AACnB,SAAS,YAAY;AAErB,SAAS,gBAAgB;AA8BzB,MAAM,QAAqC;AAAA,EACzC,IAAI;AAAA,EACJ,IAAI;AAAA,EACJ,IAAI;AACN;AAEO,MAAM,UAAU,MAAM,WAAwC,SAASA,SAC5E,EAAE,OAAO,MAAM,QAAQ,KAAK,OAAO,SAAS,WAAW,GAAG,MAAM,GAChE,KACA;AACA,SACE;AAAA,IAAC;AAAA;AAAA,MACC,MAAK;AAAA,MACL,WAAU;AAAA,MACT,GAAG,SAAS,WAAW,MAAM;AAAA,MAE9B;AAAA;AAAA,UAAC;AAAA;AAAA,YACC;AAAA,YACA,SAAQ;AAAA,YACR,MAAK;AAAA,YACL,eAAa,QAAQ,SAAY;AAAA,YACjC,WAAW;AAAA,cACT;AAAA,cACA,MAAM,IAAI;AAAA,cACV;AAAA,YACF;AAAA,YACA,mBAAgB;AAAA,YACf,GAAG;AAAA,YAEJ;AAAA,kCAAC,YAAO,IAAG,MAAK,IAAG,MAAK,GAAE,KAAI,QAAO,gBAAe,aAAY,OAAM,SAAQ,QAAO;AAAA,cACrF;AAAA,gBAAC;AAAA;AAAA,kBACC,GAAE;AAAA,kBACF,QAAO;AAAA,kBACP,aAAY;AAAA,kBACZ,eAAc;AAAA;AAAA,cAChB;AAAA;AAAA;AAAA,QACF;AAAA,QACC,QAAQ,oBAAC,UAAK,WAAU,WAAW,iBAAM,IAAU;AAAA;AAAA;AAAA,EACtD;AAEJ,CAAC;","names":["Spinner"]}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import * as React from 'react';
|
|
2
|
+
import { StatusDomain, StatusKey } from '@tea-ui/ux-standards';
|
|
3
|
+
|
|
4
|
+
type StatusDomainKey<D extends StatusDomain> = StatusKey<D>;
|
|
5
|
+
interface StatusBaseProps<D extends StatusDomain> {
|
|
6
|
+
/** Which status vocabulary the key belongs to. */
|
|
7
|
+
domain: D;
|
|
8
|
+
/** The wire value. Type-checked against the domain. */
|
|
9
|
+
status: StatusKey<D>;
|
|
10
|
+
className?: string | undefined;
|
|
11
|
+
}
|
|
12
|
+
interface StatusDotProps<D extends StatusDomain> extends StatusBaseProps<D> {
|
|
13
|
+
size?: "sm" | "md" | undefined;
|
|
14
|
+
/**
|
|
15
|
+
* Hide the visually hidden text. Only for a dot inside a row that already
|
|
16
|
+
* states the same status as visible text — never as a shortcut.
|
|
17
|
+
*/
|
|
18
|
+
hideLabel?: boolean | undefined;
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* A dot plus a word. `rounded-full` here is one of the five documented
|
|
22
|
+
* geometric exceptions: a status indicator is a *mark*, not a surface, and the
|
|
23
|
+
* design language's square rule applies to surfaces.
|
|
24
|
+
*/
|
|
25
|
+
declare function StatusDot<D extends StatusDomain>({ domain, status, size, hideLabel, className, }: StatusDotProps<D>): React.ReactElement;
|
|
26
|
+
interface StatusBadgeProps<D extends StatusDomain> extends StatusBaseProps<D> {
|
|
27
|
+
/** Show the registry description as a secondary line. */
|
|
28
|
+
showDescription?: boolean | undefined;
|
|
29
|
+
title?: string | undefined;
|
|
30
|
+
}
|
|
31
|
+
/** A bordered chip with a dot and the registry label. The default choice. */
|
|
32
|
+
declare function StatusBadge<D extends StatusDomain>({ domain, status, showDescription, title, className, }: StatusBadgeProps<D>): React.ReactElement;
|
|
33
|
+
interface StatusTextProps<D extends StatusDomain> extends StatusBaseProps<D> {
|
|
34
|
+
title?: string | undefined;
|
|
35
|
+
}
|
|
36
|
+
/** The label alone. For dense rows where a chip would be too heavy. */
|
|
37
|
+
declare function StatusText<D extends StatusDomain>({ domain, status, title, className, }: StatusTextProps<D>): React.ReactElement;
|
|
38
|
+
interface StatusSelectProps<D extends StatusDomain> {
|
|
39
|
+
domain: D;
|
|
40
|
+
/** Controlled value. */
|
|
41
|
+
value?: StatusKey<D> | undefined;
|
|
42
|
+
/** Uncontrolled initial value. */
|
|
43
|
+
defaultValue?: StatusKey<D> | undefined;
|
|
44
|
+
/** Called with the chosen wire value. Never a colour. */
|
|
45
|
+
onValueChange?: ((value: StatusKey<D>) => void) | undefined;
|
|
46
|
+
/** Accessible name for the group. */
|
|
47
|
+
label: string;
|
|
48
|
+
disabled?: boolean | undefined;
|
|
49
|
+
className?: string | undefined;
|
|
50
|
+
/** Restrict the choices. Defaults to every key in the domain. */
|
|
51
|
+
statuses?: readonly StatusKey<D>[] | undefined;
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* The interactive picker over one domain's keys. A radiogroup, because choosing
|
|
55
|
+
* one status *is* choosing one value from a small set — a listbox here would
|
|
56
|
+
* add a level of interaction for no gain.
|
|
57
|
+
*
|
|
58
|
+
* It emits a wire value. The caller then decides what to do with it, which is
|
|
59
|
+
* what keeps this component free of application logic.
|
|
60
|
+
*/
|
|
61
|
+
declare function StatusSelect<D extends StatusDomain>({ domain, value, defaultValue, onValueChange, label, disabled, className, statuses, }: StatusSelectProps<D>): React.ReactElement;
|
|
62
|
+
|
|
63
|
+
export { StatusBadge, type StatusBadgeProps, type StatusBaseProps, type StatusDomainKey, StatusDot, type StatusDotProps, StatusSelect, type StatusSelectProps, StatusText, type StatusTextProps };
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
2
|
+
import * as React from "react";
|
|
3
|
+
import { statusEntries, statusMeta } from "@tea-ui/ux-standards";
|
|
4
|
+
import { cn } from "@tea-ui/utils";
|
|
5
|
+
import { dataSlot, stateAttributes } from "../internal/index.js";
|
|
6
|
+
const TONE_DOT = {
|
|
7
|
+
positive: "bg-positive",
|
|
8
|
+
info: "bg-info",
|
|
9
|
+
caution: "bg-caution",
|
|
10
|
+
critical: "bg-critical",
|
|
11
|
+
neutral: "bg-fg-muted"
|
|
12
|
+
};
|
|
13
|
+
const TONE_TEXT = {
|
|
14
|
+
positive: "text-positive",
|
|
15
|
+
info: "text-info",
|
|
16
|
+
caution: "text-caution",
|
|
17
|
+
critical: "text-critical",
|
|
18
|
+
neutral: "text-fg-muted"
|
|
19
|
+
};
|
|
20
|
+
const TONE_CHIP = {
|
|
21
|
+
positive: "bg-positive-subtle text-positive border-positive-border",
|
|
22
|
+
info: "bg-info-subtle text-info border-info-border",
|
|
23
|
+
caution: "bg-caution-subtle text-caution border-caution-border",
|
|
24
|
+
critical: "bg-critical-subtle text-critical border-critical-border",
|
|
25
|
+
neutral: "bg-neutral-subtle text-fg-muted border-neutral-border"
|
|
26
|
+
};
|
|
27
|
+
function StatusDot({
|
|
28
|
+
domain,
|
|
29
|
+
status,
|
|
30
|
+
size = "md",
|
|
31
|
+
hideLabel = false,
|
|
32
|
+
className
|
|
33
|
+
}) {
|
|
34
|
+
const meta = statusMeta(domain, status);
|
|
35
|
+
return /* @__PURE__ */ jsxs(
|
|
36
|
+
"span",
|
|
37
|
+
{
|
|
38
|
+
className: cn("inline-flex items-center gap-1.5", className),
|
|
39
|
+
title: meta.description,
|
|
40
|
+
...dataSlot("status", "dot"),
|
|
41
|
+
...stateAttributes({ checked: true }),
|
|
42
|
+
children: [
|
|
43
|
+
/* @__PURE__ */ jsx(
|
|
44
|
+
"span",
|
|
45
|
+
{
|
|
46
|
+
"aria-hidden": "true",
|
|
47
|
+
className: cn(
|
|
48
|
+
"shrink-0 rounded-pill",
|
|
49
|
+
size === "sm" ? "size-1.5" : "size-2",
|
|
50
|
+
TONE_DOT[meta.tone]
|
|
51
|
+
)
|
|
52
|
+
}
|
|
53
|
+
),
|
|
54
|
+
hideLabel ? null : /* @__PURE__ */ jsx("span", { className: "sr-only", children: meta.label })
|
|
55
|
+
]
|
|
56
|
+
}
|
|
57
|
+
);
|
|
58
|
+
}
|
|
59
|
+
function StatusBadge({
|
|
60
|
+
domain,
|
|
61
|
+
status,
|
|
62
|
+
showDescription = false,
|
|
63
|
+
title,
|
|
64
|
+
className
|
|
65
|
+
}) {
|
|
66
|
+
const meta = statusMeta(domain, status);
|
|
67
|
+
return /* @__PURE__ */ jsxs(
|
|
68
|
+
"span",
|
|
69
|
+
{
|
|
70
|
+
title: title ?? meta.description,
|
|
71
|
+
className: cn(
|
|
72
|
+
"inline-flex items-center gap-1.5 border px-2 py-0.5 text-micro font-medium leading-none",
|
|
73
|
+
TONE_CHIP[meta.tone],
|
|
74
|
+
className
|
|
75
|
+
),
|
|
76
|
+
...dataSlot("status", "badge"),
|
|
77
|
+
children: [
|
|
78
|
+
/* @__PURE__ */ jsx("span", { "aria-hidden": "true", className: cn("size-1.5 shrink-0 rounded-pill", TONE_DOT[meta.tone]) }),
|
|
79
|
+
meta.label,
|
|
80
|
+
showDescription ? /* @__PURE__ */ jsx("span", { className: "font-normal text-fg-muted", children: meta.description }) : null
|
|
81
|
+
]
|
|
82
|
+
}
|
|
83
|
+
);
|
|
84
|
+
}
|
|
85
|
+
function StatusText({
|
|
86
|
+
domain,
|
|
87
|
+
status,
|
|
88
|
+
title,
|
|
89
|
+
className
|
|
90
|
+
}) {
|
|
91
|
+
const meta = statusMeta(domain, status);
|
|
92
|
+
return /* @__PURE__ */ jsx("span", { title: title ?? meta.description, className: cn("text-ui", TONE_TEXT[meta.tone], className), ...dataSlot("status", "text"), children: meta.label });
|
|
93
|
+
}
|
|
94
|
+
function StatusSelect({
|
|
95
|
+
domain,
|
|
96
|
+
value,
|
|
97
|
+
defaultValue,
|
|
98
|
+
onValueChange,
|
|
99
|
+
label,
|
|
100
|
+
disabled = false,
|
|
101
|
+
className,
|
|
102
|
+
statuses
|
|
103
|
+
}) {
|
|
104
|
+
const [internal, setInternal] = React.useState(
|
|
105
|
+
defaultValue ?? statuses?.[0] ?? statusEntries(domain)[0]?.[0]
|
|
106
|
+
);
|
|
107
|
+
const current = value ?? internal;
|
|
108
|
+
const options = statuses ?? statusEntries(domain).map(([key]) => key);
|
|
109
|
+
return /* @__PURE__ */ jsx(
|
|
110
|
+
"div",
|
|
111
|
+
{
|
|
112
|
+
role: "radiogroup",
|
|
113
|
+
"aria-label": label,
|
|
114
|
+
className: cn("flex flex-wrap items-center gap-1.5", className),
|
|
115
|
+
...dataSlot("status", "select"),
|
|
116
|
+
...stateAttributes({ disabled }),
|
|
117
|
+
children: options.map((option) => {
|
|
118
|
+
const meta = statusMeta(domain, option);
|
|
119
|
+
const selected = option === current;
|
|
120
|
+
return /* @__PURE__ */ jsxs(
|
|
121
|
+
"button",
|
|
122
|
+
{
|
|
123
|
+
type: "button",
|
|
124
|
+
role: "radio",
|
|
125
|
+
"aria-checked": selected,
|
|
126
|
+
title: meta.description,
|
|
127
|
+
disabled,
|
|
128
|
+
"data-tea-touch": true,
|
|
129
|
+
className: cn(
|
|
130
|
+
"inline-flex items-center gap-1.5 border px-2 py-1 text-micro font-medium leading-none",
|
|
131
|
+
"transition-colors duration-fast ease-standard",
|
|
132
|
+
"disabled:pointer-events-none disabled:opacity-50",
|
|
133
|
+
selected ? TONE_CHIP[meta.tone] : "border-line bg-transparent text-fg-muted hover:border-line-strong hover:text-fg"
|
|
134
|
+
),
|
|
135
|
+
onClick: () => {
|
|
136
|
+
setInternal(option);
|
|
137
|
+
onValueChange?.(option);
|
|
138
|
+
},
|
|
139
|
+
children: [
|
|
140
|
+
/* @__PURE__ */ jsx("span", { "aria-hidden": "true", className: cn("size-1.5 shrink-0 rounded-pill", TONE_DOT[meta.tone]) }),
|
|
141
|
+
meta.label
|
|
142
|
+
]
|
|
143
|
+
},
|
|
144
|
+
option
|
|
145
|
+
);
|
|
146
|
+
})
|
|
147
|
+
}
|
|
148
|
+
);
|
|
149
|
+
}
|
|
150
|
+
export {
|
|
151
|
+
StatusBadge,
|
|
152
|
+
StatusDot,
|
|
153
|
+
StatusSelect,
|
|
154
|
+
StatusText
|
|
155
|
+
};
|
|
156
|
+
//# sourceMappingURL=status.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/feedback/status.tsx"],"sourcesContent":["import * as React from \"react\";\nimport type { Tone } from \"@tea-ui/tokens\";\nimport { statusEntries, statusMeta, type StatusDomain, type StatusKey } from \"@tea-ui/ux-standards\";\nimport { cn } from \"@tea-ui/utils\";\n\nimport { dataSlot, stateAttributes } from \"../internal\";\n\n/**\n * TEA UI — the status surface.\n *\n * **The only supported way to render a status in TEA UI.** A component takes a\n * `domain` and a `key`, and this family derives the label, the tone and the\n * description from the shared registry in `@tea-ui/ux-standards`. Nothing here\n * accepts a colour or a hand-written word.\n *\n * That constraint is the fix. The audit found, across two products, five\n * hand-written status-to-label tables that did not agree — one said \"Online\"\n * where another said \"Läuft\" for the same wire value, and one used \"Gültig\"\n * for a certificate where another used \"Online\". Keying on the wire value makes\n * that class of divergence a type error instead of a review comment.\n *\n * Colour is never the only signal. Every renderer pairs the tone with a word;\n * `StatusDot` additionally carries a title and visually hidden text.\n */\n\nconst TONE_DOT: Record<Tone, string> = {\n positive: \"bg-positive\",\n info: \"bg-info\",\n caution: \"bg-caution\",\n critical: \"bg-critical\",\n neutral: \"bg-fg-muted\",\n};\n\nconst TONE_TEXT: Record<Tone, string> = {\n positive: \"text-positive\",\n info: \"text-info\",\n caution: \"text-caution\",\n critical: \"text-critical\",\n neutral: \"text-fg-muted\",\n};\n\nconst TONE_CHIP: Record<Tone, string> = {\n positive: \"bg-positive-subtle text-positive border-positive-border\",\n info: \"bg-info-subtle text-info border-info-border\",\n caution: \"bg-caution-subtle text-caution border-caution-border\",\n critical: \"bg-critical-subtle text-critical border-critical-border\",\n neutral: \"bg-neutral-subtle text-fg-muted border-neutral-border\",\n};\n\nexport type StatusDomainKey<D extends StatusDomain> = StatusKey<D>;\n\nexport interface StatusBaseProps<D extends StatusDomain> {\n /** Which status vocabulary the key belongs to. */\n domain: D;\n /** The wire value. Type-checked against the domain. */\n status: StatusKey<D>;\n className?: string | undefined;\n}\n\nexport interface StatusDotProps<D extends StatusDomain> extends StatusBaseProps<D> {\n size?: \"sm\" | \"md\" | undefined;\n /**\n * Hide the visually hidden text. Only for a dot inside a row that already\n * states the same status as visible text — never as a shortcut.\n */\n hideLabel?: boolean | undefined;\n}\n\n/**\n * A dot plus a word. `rounded-full` here is one of the five documented\n * geometric exceptions: a status indicator is a *mark*, not a surface, and the\n * design language's square rule applies to surfaces.\n */\nexport function StatusDot<D extends StatusDomain>({\n domain,\n status,\n size = \"md\",\n hideLabel = false,\n className,\n}: StatusDotProps<D>): React.ReactElement {\n const meta = statusMeta(domain, status);\n return (\n <span\n className={cn(\"inline-flex items-center gap-1.5\", className)}\n title={meta.description}\n {...dataSlot(\"status\", \"dot\")}\n {...stateAttributes({ checked: true })}\n >\n <span\n aria-hidden=\"true\"\n className={cn(\n \"shrink-0 rounded-pill\",\n size === \"sm\" ? \"size-1.5\" : \"size-2\",\n TONE_DOT[meta.tone],\n )}\n />\n {hideLabel ? null : <span className=\"sr-only\">{meta.label}</span>}\n </span>\n );\n}\n\nexport interface StatusBadgeProps<D extends StatusDomain> extends StatusBaseProps<D> {\n /** Show the registry description as a secondary line. */\n showDescription?: boolean | undefined;\n title?: string | undefined;\n}\n\n/** A bordered chip with a dot and the registry label. The default choice. */\nexport function StatusBadge<D extends StatusDomain>({\n domain,\n status,\n showDescription = false,\n title,\n className,\n}: StatusBadgeProps<D>): React.ReactElement {\n const meta = statusMeta(domain, status);\n return (\n <span\n title={title ?? meta.description}\n className={cn(\n \"inline-flex items-center gap-1.5 border px-2 py-0.5 text-micro font-medium leading-none\",\n TONE_CHIP[meta.tone],\n className,\n )}\n {...dataSlot(\"status\", \"badge\")}\n >\n <span aria-hidden=\"true\" className={cn(\"size-1.5 shrink-0 rounded-pill\", TONE_DOT[meta.tone])} />\n {meta.label}\n {showDescription ? <span className=\"font-normal text-fg-muted\">{meta.description}</span> : null}\n </span>\n );\n}\n\nexport interface StatusTextProps<D extends StatusDomain> extends StatusBaseProps<D> {\n title?: string | undefined;\n}\n\n/** The label alone. For dense rows where a chip would be too heavy. */\nexport function StatusText<D extends StatusDomain>({\n domain,\n status,\n title,\n className,\n}: StatusTextProps<D>): React.ReactElement {\n const meta = statusMeta(domain, status);\n return (\n <span title={title ?? meta.description} className={cn(\"text-ui\", TONE_TEXT[meta.tone], className)} {...dataSlot(\"status\", \"text\")}>\n {meta.label}\n </span>\n );\n}\n\nexport interface StatusSelectProps<D extends StatusDomain> {\n domain: D;\n /** Controlled value. */\n value?: StatusKey<D> | undefined;\n /** Uncontrolled initial value. */\n defaultValue?: StatusKey<D> | undefined;\n /** Called with the chosen wire value. Never a colour. */\n onValueChange?: ((value: StatusKey<D>) => void) | undefined;\n /** Accessible name for the group. */\n label: string;\n disabled?: boolean | undefined;\n className?: string | undefined;\n /** Restrict the choices. Defaults to every key in the domain. */\n statuses?: readonly StatusKey<D>[] | undefined;\n}\n\n/**\n * The interactive picker over one domain's keys. A radiogroup, because choosing\n * one status *is* choosing one value from a small set — a listbox here would\n * add a level of interaction for no gain.\n *\n * It emits a wire value. The caller then decides what to do with it, which is\n * what keeps this component free of application logic.\n */\nexport function StatusSelect<D extends StatusDomain>({\n domain,\n value,\n defaultValue,\n onValueChange,\n label,\n disabled = false,\n className,\n statuses,\n}: StatusSelectProps<D>): React.ReactElement {\n const [internal, setInternal] = React.useState<StatusKey<D>>(\n (defaultValue ?? statuses?.[0] ?? (statusEntries(domain)[0]?.[0] as StatusKey<D>)) as StatusKey<D>,\n );\n const current = (value ?? internal) as StatusKey<D>;\n const options = (statuses ?? statusEntries(domain).map(([key]) => key)) as Array<StatusKey<D>>;\n\n return (\n <div\n role=\"radiogroup\"\n aria-label={label}\n className={cn(\"flex flex-wrap items-center gap-1.5\", className)}\n {...dataSlot(\"status\", \"select\")}\n {...stateAttributes({ disabled })}\n >\n {options.map((option) => {\n const meta = statusMeta(domain, option);\n const selected = option === current;\n return (\n <button\n key={option}\n type=\"button\"\n role=\"radio\"\n aria-checked={selected}\n title={meta.description}\n disabled={disabled}\n data-tea-touch\n className={cn(\n \"inline-flex items-center gap-1.5 border px-2 py-1 text-micro font-medium leading-none\",\n \"transition-colors duration-fast ease-standard\",\n \"disabled:pointer-events-none disabled:opacity-50\",\n selected\n ? TONE_CHIP[meta.tone]\n : \"border-line bg-transparent text-fg-muted hover:border-line-strong hover:text-fg\",\n )}\n onClick={() => {\n setInternal(option);\n onValueChange?.(option);\n }}\n >\n <span aria-hidden=\"true\" className={cn(\"size-1.5 shrink-0 rounded-pill\", TONE_DOT[meta.tone])} />\n {meta.label}\n </button>\n );\n })}\n </div>\n );\n}\n"],"mappings":"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","names":[]}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import * as React from 'react';
|
|
2
|
+
import { Toast } from 'radix-ui';
|
|
3
|
+
import { Tone } from '@tea-ui/tokens';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* TEA UI — toasts.
|
|
7
|
+
*
|
|
8
|
+
* The audit found a toast implementation that was entirely inert: a
|
|
9
|
+
* `ToastProvider` was mounted, but the `Toast` component rendered a plain `<div>`
|
|
10
|
+
* instead of Radix's root, so nothing ever registered with the provider and
|
|
11
|
+
* nothing was ever announced. Alongside it sat a store with `TOAST_LIMIT = 1`,
|
|
12
|
+
* so a second toast silently replaced the first — the user never learned the
|
|
13
|
+
* second operation happened.
|
|
14
|
+
*
|
|
15
|
+
* Three decisions fix that class of problem:
|
|
16
|
+
* - The store is a `useSyncExternalStore` store with an immutable snapshot. The
|
|
17
|
+
* audit's version was a module-level mutable object with a hand-managed
|
|
18
|
+
* listener array and a side effect inside the reducer.
|
|
19
|
+
* - The limit is three, and the overflow is **counted and shown** rather than
|
|
20
|
+
* swallowed. A user who triggered four operations is told that three are
|
|
21
|
+
* running and one is queued.
|
|
22
|
+
* - The live region belongs to a permanently mounted `Toaster`, which is the
|
|
23
|
+
* only way a region that is added and removed on every toast can work.
|
|
24
|
+
*/
|
|
25
|
+
interface TeaToast {
|
|
26
|
+
readonly id: string;
|
|
27
|
+
readonly title: string;
|
|
28
|
+
readonly description?: string | undefined;
|
|
29
|
+
readonly tone: Tone;
|
|
30
|
+
readonly duration: number;
|
|
31
|
+
readonly action?: {
|
|
32
|
+
readonly label: string;
|
|
33
|
+
readonly onClick: () => void;
|
|
34
|
+
} | undefined;
|
|
35
|
+
readonly createdAt: number;
|
|
36
|
+
}
|
|
37
|
+
type ToastInput = {
|
|
38
|
+
title: string;
|
|
39
|
+
tone?: Tone;
|
|
40
|
+
} & Omit<Partial<TeaToast>, "id" | "createdAt" | "tone" | "title">;
|
|
41
|
+
/** Three at once. Beyond that, the user cannot read them anyway. */
|
|
42
|
+
declare const TOAST_LIMIT = 3;
|
|
43
|
+
/** Imperative API, for use outside React. */
|
|
44
|
+
declare const toast: ((input: ToastInput) => string) & {
|
|
45
|
+
dismiss(id: string): void;
|
|
46
|
+
clear(): void;
|
|
47
|
+
};
|
|
48
|
+
interface UseToastResult {
|
|
49
|
+
toasts: readonly TeaToast[];
|
|
50
|
+
dismiss: (id: string) => void;
|
|
51
|
+
clear: () => void;
|
|
52
|
+
}
|
|
53
|
+
/** Subscribe to the toast store. */
|
|
54
|
+
declare function useToast(): UseToastResult;
|
|
55
|
+
interface ToastSurfaceProps extends React.ComponentProps<typeof Toast.Root> {
|
|
56
|
+
tone?: Tone | undefined;
|
|
57
|
+
/** Adds a coloured left rule so tone survives without reading the text. */
|
|
58
|
+
showTone?: boolean | undefined;
|
|
59
|
+
}
|
|
60
|
+
declare const ToastRoot: React.ForwardRefExoticComponent<Omit<ToastSurfaceProps, "ref"> & React.RefAttributes<HTMLLIElement>>;
|
|
61
|
+
declare const ToastTitle: React.ForwardRefExoticComponent<Omit<React.DetailedHTMLProps<React.HTMLAttributes<HTMLParagraphElement>, HTMLParagraphElement>, "ref"> & React.RefAttributes<HTMLParagraphElement>>;
|
|
62
|
+
declare const ToastDescription: React.ForwardRefExoticComponent<Omit<React.DetailedHTMLProps<React.HTMLAttributes<HTMLParagraphElement>, HTMLParagraphElement>, "ref"> & React.RefAttributes<HTMLParagraphElement>>;
|
|
63
|
+
declare const ToastClose: React.ForwardRefExoticComponent<Omit<React.DetailedHTMLProps<React.ButtonHTMLAttributes<HTMLButtonElement>, HTMLButtonElement>, "ref"> & React.RefAttributes<HTMLButtonElement>>;
|
|
64
|
+
type ToasterProps = React.ComponentProps<typeof Toast.Viewport>;
|
|
65
|
+
/**
|
|
66
|
+
* The live region. Mount this ONCE, near the root of the application, and
|
|
67
|
+
* never conditionally — a region that is created at the moment it needs to
|
|
68
|
+
* announce is a region that is too late.
|
|
69
|
+
*/
|
|
70
|
+
declare const Toaster: React.ForwardRefExoticComponent<Omit<Toast.ToastViewportProps & React.RefAttributes<HTMLOListElement>, "ref"> & React.RefAttributes<HTMLOListElement>>;
|
|
71
|
+
|
|
72
|
+
export { TOAST_LIMIT, type TeaToast, ToastClose, ToastDescription, type ToastInput, ToastRoot, type ToastSurfaceProps, ToastTitle, Toaster, type ToasterProps, type UseToastResult, toast, useToast };
|