@jcsoftdev/ui 0.1.5 → 0.1.6
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 +85 -2
- package/dist/atoms/button/button.d.ts +8 -4
- package/dist/atoms/button/button.js +16 -6
- package/dist/atoms/checkbox/checkbox.js +4 -1
- package/dist/atoms/chip/chip.d.ts +7 -3
- package/dist/atoms/chip/chip.js +22 -4
- package/dist/atoms/internal/status-icons.d.ts +4 -0
- package/dist/atoms/internal/status-icons.js +15 -0
- package/dist/atoms/radio-group/radio-group.js +1 -1
- package/dist/atoms/switch/switch.js +4 -1
- package/dist/containers/banner/banner.d.ts +5 -3
- package/dist/containers/banner/banner.js +11 -5
- package/dist/containers/confirm-action/confirm-action.d.ts +17 -0
- package/dist/containers/confirm-action/confirm-action.js +71 -0
- package/dist/containers/confirm-action/index.d.ts +2 -0
- package/dist/containers/confirm-action/index.js +1 -0
- package/dist/containers/confirm-action/model.d.ts +14 -0
- package/dist/containers/confirm-action/model.js +12 -0
- package/dist/containers/index.d.ts +2 -0
- package/dist/containers/index.js +2 -0
- package/dist/containers/responsive-list/index.d.ts +1 -0
- package/dist/containers/responsive-list/index.js +1 -0
- package/dist/containers/responsive-list/responsive-list.d.ts +24 -0
- package/dist/containers/responsive-list/responsive-list.js +69 -0
- package/dist/forms/field/field.d.ts +5 -2
- package/dist/forms/field/field.js +11 -2
- package/dist/i18n/index.d.ts +2 -0
- package/dist/i18n/index.js +2 -0
- package/dist/i18n/strings.d.ts +42 -0
- package/dist/i18n/strings.js +31 -0
- package/dist/i18n/ui-strings-provider.d.ts +11 -0
- package/dist/i18n/ui-strings-provider.js +20 -0
- package/dist/overlays/dialog/dialog.js +5 -3
- package/dist/overlays/drawer/drawer.js +5 -3
- package/dist/states/error-state/error-state.d.ts +8 -3
- package/dist/states/error-state/error-state.js +19 -5
- package/dist/states/skeleton/skeleton.js +5 -2
- package/package.json +1 -1
- package/scripts/build-theme.mjs +11 -5
- package/src/atoms/button/button.tsx +27 -5
- package/src/atoms/checkbox/checkbox.tsx +4 -1
- package/src/atoms/chip/chip.tsx +28 -3
- package/src/atoms/internal/status-icons.tsx +17 -0
- package/src/atoms/radio-group/radio-group.tsx +1 -1
- package/src/atoms/switch/switch.tsx +4 -1
- package/src/containers/banner/banner.tsx +18 -10
- package/src/containers/confirm-action/confirm-action.tsx +92 -0
- package/src/containers/confirm-action/index.ts +2 -0
- package/src/containers/confirm-action/model.ts +17 -0
- package/src/containers/index.ts +2 -0
- package/src/containers/responsive-list/index.ts +1 -0
- package/src/containers/responsive-list/responsive-list.tsx +127 -0
- package/src/forms/field/field.tsx +11 -3
- package/src/i18n/index.ts +2 -0
- package/src/i18n/strings.ts +44 -0
- package/src/i18n/ui-strings-provider.tsx +27 -0
- package/src/overlays/dialog/dialog.tsx +5 -3
- package/src/overlays/drawer/drawer.tsx +5 -3
- package/src/states/error-state/error-state.tsx +30 -7
- package/src/states/skeleton/skeleton.tsx +6 -2
- package/styles/Theme.kt +7 -0
- package/styles/theme.css +4 -0
- package/styles/tokens.css +24 -0
- package/tokens/default.json +34 -0
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
3
|
+
import { useUiStrings } from "../../i18n/index.js";
|
|
4
|
+
import { cn } from "../../utils/cn.js";
|
|
5
|
+
import {
|
|
6
|
+
Table,
|
|
7
|
+
TableBody,
|
|
8
|
+
TableCell,
|
|
9
|
+
TableHead,
|
|
10
|
+
TableHeaderCell,
|
|
11
|
+
TableRow
|
|
12
|
+
} from "../table/table.js";
|
|
13
|
+
function ResponsiveList({
|
|
14
|
+
caption,
|
|
15
|
+
items,
|
|
16
|
+
columns,
|
|
17
|
+
rowKey,
|
|
18
|
+
renderActions,
|
|
19
|
+
actionsHeader,
|
|
20
|
+
rowClassName,
|
|
21
|
+
className
|
|
22
|
+
}) {
|
|
23
|
+
const strings = useUiStrings();
|
|
24
|
+
const primary = columns.find((column) => column.primary) ?? columns[0];
|
|
25
|
+
const details = columns.filter((column) => column !== primary);
|
|
26
|
+
return /* @__PURE__ */ jsxs("div", { className: cn("min-w-0", className), children: [
|
|
27
|
+
/* @__PURE__ */ jsx("div", { className: "hidden rounded-card border-card shadow-card surface-card md:block", children: /* @__PURE__ */ jsxs(Table, { children: [
|
|
28
|
+
/* @__PURE__ */ jsx("caption", { className: "sr-only", children: caption }),
|
|
29
|
+
/* @__PURE__ */ jsx(TableHead, { children: /* @__PURE__ */ jsxs(TableRow, { className: "hover:bg-transparent", children: [
|
|
30
|
+
columns.map((column) => /* @__PURE__ */ jsx(TableHeaderCell, { scope: "col", alignRight: column.align === "end", children: column.header }, column.key)),
|
|
31
|
+
renderActions ? /* @__PURE__ */ jsx(TableHeaderCell, { scope: "col", alignRight: true, children: actionsHeader ?? strings.actions }) : null
|
|
32
|
+
] }) }),
|
|
33
|
+
/* @__PURE__ */ jsx(TableBody, { children: items.map((item) => /* @__PURE__ */ jsxs(
|
|
34
|
+
TableRow,
|
|
35
|
+
{
|
|
36
|
+
"data-row-key": rowKey(item),
|
|
37
|
+
className: rowClassName?.(item),
|
|
38
|
+
children: [
|
|
39
|
+
columns.map((column) => /* @__PURE__ */ jsx(TableCell, { alignRight: column.align === "end", children: column.cell(item) }, column.key)),
|
|
40
|
+
renderActions ? /* @__PURE__ */ jsx(TableCell, { alignRight: true, children: /* @__PURE__ */ jsx("div", { className: "flex flex-wrap justify-end gap-2", children: renderActions(item) }) }) : null
|
|
41
|
+
]
|
|
42
|
+
},
|
|
43
|
+
rowKey(item)
|
|
44
|
+
)) })
|
|
45
|
+
] }) }),
|
|
46
|
+
/* @__PURE__ */ jsx("ul", { "aria-label": caption, className: "m-0 flex list-none flex-col gap-density p-0 md:hidden", children: items.map((item) => /* @__PURE__ */ jsxs(
|
|
47
|
+
"li",
|
|
48
|
+
{
|
|
49
|
+
"data-row-key": rowKey(item),
|
|
50
|
+
className: cn(
|
|
51
|
+
"flex min-w-0 flex-col gap-3 rounded-card border-card p-card shadow-card surface-card",
|
|
52
|
+
rowClassName?.(item)
|
|
53
|
+
),
|
|
54
|
+
children: [
|
|
55
|
+
/* @__PURE__ */ jsx("div", { className: "min-w-0 font-semibold text-[15px] text-ink [overflow-wrap:anywhere]", children: primary?.cell(item) }),
|
|
56
|
+
details.length > 0 ? /* @__PURE__ */ jsx("dl", { className: "m-0 flex flex-col gap-2", children: details.map((column) => /* @__PURE__ */ jsxs("div", { className: "flex items-baseline justify-between gap-4", children: [
|
|
57
|
+
/* @__PURE__ */ jsx("dt", { className: "shrink-0 text-ink-3 text-meta", children: column.header }),
|
|
58
|
+
/* @__PURE__ */ jsx("dd", { className: "m-0 min-w-0 text-right text-body text-ink [overflow-wrap:anywhere]", children: column.cell(item) })
|
|
59
|
+
] }, column.key)) }) : null,
|
|
60
|
+
renderActions ? /* @__PURE__ */ jsx("div", { className: "flex flex-wrap gap-2", children: renderActions(item) }) : null
|
|
61
|
+
]
|
|
62
|
+
},
|
|
63
|
+
rowKey(item)
|
|
64
|
+
)) })
|
|
65
|
+
] });
|
|
66
|
+
}
|
|
67
|
+
export {
|
|
68
|
+
ResponsiveList
|
|
69
|
+
};
|
|
@@ -7,10 +7,13 @@ interface ControlProps {
|
|
|
7
7
|
export interface FieldProps {
|
|
8
8
|
label: string;
|
|
9
9
|
hint?: string;
|
|
10
|
-
error
|
|
10
|
+
/** Message under the control; `null` and `undefined` mean no error. */
|
|
11
|
+
error?: string | null;
|
|
12
|
+
/** Marks the field as optional by appending the localized marker to the label. */
|
|
13
|
+
optional?: boolean;
|
|
11
14
|
id?: string;
|
|
12
15
|
children: ReactElement<ControlProps>;
|
|
13
16
|
className?: string;
|
|
14
17
|
}
|
|
15
|
-
export declare function Field({ label, hint, error, id, children, className }: FieldProps): import("react").JSX.Element;
|
|
18
|
+
export declare function Field({ label, hint, error, optional, id, children, className }: FieldProps): import("react").JSX.Element;
|
|
16
19
|
export {};
|
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
import { jsx, jsxs } from "react/jsx-runtime";
|
|
3
3
|
import { cloneElement, isValidElement, useId } from "react";
|
|
4
|
+
import { useUiStrings } from "../../i18n/index.js";
|
|
4
5
|
import { cn } from "../../utils/cn.js";
|
|
5
6
|
import { Label } from "../label/index.js";
|
|
6
|
-
function Field({ label, hint, error, id, children, className }) {
|
|
7
|
+
function Field({ label, hint, error, optional, id, children, className }) {
|
|
8
|
+
const strings = useUiStrings();
|
|
7
9
|
const generatedId = useId();
|
|
8
10
|
const controlId = id ?? generatedId;
|
|
9
11
|
const hintId = hint ? `${controlId}-hint` : void 0;
|
|
@@ -15,7 +17,14 @@ function Field({ label, hint, error, id, children, className }) {
|
|
|
15
17
|
"aria-invalid": error ? true : void 0
|
|
16
18
|
}) : children;
|
|
17
19
|
return /* @__PURE__ */ jsxs("div", { className: cn("flex min-w-0 flex-col gap-1.25", className), children: [
|
|
18
|
-
/* @__PURE__ */
|
|
20
|
+
/* @__PURE__ */ jsxs(Label, { htmlFor: controlId, children: [
|
|
21
|
+
label,
|
|
22
|
+
optional ? /* @__PURE__ */ jsxs("span", { className: "font-normal text-ink-3", children: [
|
|
23
|
+
" (",
|
|
24
|
+
strings.optional,
|
|
25
|
+
")"
|
|
26
|
+
] }) : null
|
|
27
|
+
] }),
|
|
19
28
|
control,
|
|
20
29
|
hint ? /* @__PURE__ */ jsx("p", { id: hintId, className: "text-meta text-ink-3", children: hint }) : null,
|
|
21
30
|
error ? /* @__PURE__ */ jsx("p", { id: errorId, className: "font-semibold text-danger text-meta", role: "alert", children: error }) : null
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
export interface UiStrings {
|
|
2
|
+
retry: string;
|
|
3
|
+
close: string;
|
|
4
|
+
loading: string;
|
|
5
|
+
optional: string;
|
|
6
|
+
cancel: string;
|
|
7
|
+
confirm: string;
|
|
8
|
+
processing: string;
|
|
9
|
+
correlationPrefix: string;
|
|
10
|
+
actionFailed: string;
|
|
11
|
+
somethingWrong: string;
|
|
12
|
+
actions: string;
|
|
13
|
+
}
|
|
14
|
+
export type UiLocale = "en" | "es";
|
|
15
|
+
export declare const uiStrings: {
|
|
16
|
+
readonly en: {
|
|
17
|
+
readonly retry: "Retry";
|
|
18
|
+
readonly close: "Close";
|
|
19
|
+
readonly loading: "Loading";
|
|
20
|
+
readonly optional: "optional";
|
|
21
|
+
readonly cancel: "Cancel";
|
|
22
|
+
readonly confirm: "Confirm";
|
|
23
|
+
readonly processing: "Processing…";
|
|
24
|
+
readonly correlationPrefix: "ID:";
|
|
25
|
+
readonly actionFailed: "The action could not be completed. Try again.";
|
|
26
|
+
readonly somethingWrong: "Something went wrong. Try again.";
|
|
27
|
+
readonly actions: "Actions";
|
|
28
|
+
};
|
|
29
|
+
readonly es: {
|
|
30
|
+
readonly retry: "Reintentar";
|
|
31
|
+
readonly close: "Cerrar";
|
|
32
|
+
readonly loading: "Cargando";
|
|
33
|
+
readonly optional: "opcional";
|
|
34
|
+
readonly cancel: "Cancelar";
|
|
35
|
+
readonly confirm: "Confirmar";
|
|
36
|
+
readonly processing: "Procesando…";
|
|
37
|
+
readonly correlationPrefix: "ID:";
|
|
38
|
+
readonly actionFailed: "No se pudo completar la acción. Inténtalo de nuevo.";
|
|
39
|
+
readonly somethingWrong: "Algo salió mal. Inténtalo de nuevo.";
|
|
40
|
+
readonly actions: "Acciones";
|
|
41
|
+
};
|
|
42
|
+
};
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
const uiStrings = {
|
|
2
|
+
en: {
|
|
3
|
+
retry: "Retry",
|
|
4
|
+
close: "Close",
|
|
5
|
+
loading: "Loading",
|
|
6
|
+
optional: "optional",
|
|
7
|
+
cancel: "Cancel",
|
|
8
|
+
confirm: "Confirm",
|
|
9
|
+
processing: "Processing\u2026",
|
|
10
|
+
correlationPrefix: "ID:",
|
|
11
|
+
actionFailed: "The action could not be completed. Try again.",
|
|
12
|
+
somethingWrong: "Something went wrong. Try again.",
|
|
13
|
+
actions: "Actions"
|
|
14
|
+
},
|
|
15
|
+
es: {
|
|
16
|
+
retry: "Reintentar",
|
|
17
|
+
close: "Cerrar",
|
|
18
|
+
loading: "Cargando",
|
|
19
|
+
optional: "opcional",
|
|
20
|
+
cancel: "Cancelar",
|
|
21
|
+
confirm: "Confirmar",
|
|
22
|
+
processing: "Procesando\u2026",
|
|
23
|
+
correlationPrefix: "ID:",
|
|
24
|
+
actionFailed: "No se pudo completar la acci\xF3n. Int\xE9ntalo de nuevo.",
|
|
25
|
+
somethingWrong: "Algo sali\xF3 mal. Int\xE9ntalo de nuevo.",
|
|
26
|
+
actions: "Acciones"
|
|
27
|
+
}
|
|
28
|
+
};
|
|
29
|
+
export {
|
|
30
|
+
uiStrings
|
|
31
|
+
};
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { type ReactNode } from "react";
|
|
2
|
+
import { type UiLocale, type UiStrings } from "./strings.js";
|
|
3
|
+
export interface UiStringsProviderProps {
|
|
4
|
+
/** Preset to start from. Without it the provider keeps the strings of its parent (default `en`). */
|
|
5
|
+
locale?: UiLocale;
|
|
6
|
+
/** Overrides applied on top of the preset. */
|
|
7
|
+
strings?: Partial<UiStrings>;
|
|
8
|
+
children: ReactNode;
|
|
9
|
+
}
|
|
10
|
+
export declare function UiStringsProvider({ locale, strings, children }: UiStringsProviderProps): import("react").JSX.Element;
|
|
11
|
+
export declare function useUiStrings(): UiStrings;
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx } from "react/jsx-runtime";
|
|
3
|
+
import { createContext, useContext, useMemo } from "react";
|
|
4
|
+
import { uiStrings } from "./strings.js";
|
|
5
|
+
const UiStringsContext = createContext(uiStrings.en);
|
|
6
|
+
function UiStringsProvider({ locale, strings, children }) {
|
|
7
|
+
const parent = useContext(UiStringsContext);
|
|
8
|
+
const value = useMemo(
|
|
9
|
+
() => ({ ...locale ? uiStrings[locale] : parent, ...strings }),
|
|
10
|
+
[locale, strings, parent]
|
|
11
|
+
);
|
|
12
|
+
return /* @__PURE__ */ jsx(UiStringsContext.Provider, { value, children });
|
|
13
|
+
}
|
|
14
|
+
function useUiStrings() {
|
|
15
|
+
return useContext(UiStringsContext);
|
|
16
|
+
}
|
|
17
|
+
export {
|
|
18
|
+
UiStringsProvider,
|
|
19
|
+
useUiStrings
|
|
20
|
+
};
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
import { jsx, jsxs } from "react/jsx-runtime";
|
|
3
3
|
import * as DialogPrimitive from "@radix-ui/react-dialog";
|
|
4
4
|
import { X } from "lucide-react";
|
|
5
|
+
import { useUiStrings } from "../../i18n/index.js";
|
|
5
6
|
import { cn } from "../../utils/cn.js";
|
|
6
7
|
import {
|
|
7
8
|
PreviousFocusProvider,
|
|
@@ -27,7 +28,7 @@ function DialogContent({
|
|
|
27
28
|
className,
|
|
28
29
|
width = "md",
|
|
29
30
|
blocking = false,
|
|
30
|
-
closeLabel
|
|
31
|
+
closeLabel,
|
|
31
32
|
container,
|
|
32
33
|
children,
|
|
33
34
|
onEscapeKeyDown,
|
|
@@ -37,6 +38,7 @@ function DialogContent({
|
|
|
37
38
|
...props
|
|
38
39
|
}) {
|
|
39
40
|
const previousFocusRef = usePreviousFocus();
|
|
41
|
+
const strings = useUiStrings();
|
|
40
42
|
return /* @__PURE__ */ jsxs(DialogPrimitive.Portal, { container, children: [
|
|
41
43
|
/* @__PURE__ */ jsx(DialogPrimitive.Overlay, { className: "fixed inset-0 z-70 bg-scrim" }),
|
|
42
44
|
/* @__PURE__ */ jsxs(
|
|
@@ -69,8 +71,8 @@ function DialogContent({
|
|
|
69
71
|
!blocking && /* @__PURE__ */ jsx(
|
|
70
72
|
DialogPrimitive.Close,
|
|
71
73
|
{
|
|
72
|
-
"aria-label": closeLabel,
|
|
73
|
-
className: "absolute top-3 right-3 rounded-event p-1 text-ink-3 hover:text-ink",
|
|
74
|
+
"aria-label": closeLabel ?? strings.close,
|
|
75
|
+
className: "absolute top-3 right-3 rounded-event p-1 text-ink-3 hover:text-ink in-data-[density=touch]:top-0.5 in-data-[density=touch]:right-0.5 in-data-[density=touch]:p-3.5",
|
|
74
76
|
children: /* @__PURE__ */ jsx(X, { "aria-hidden": "true", className: "size-4" })
|
|
75
77
|
}
|
|
76
78
|
)
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
import { jsx, jsxs } from "react/jsx-runtime";
|
|
3
3
|
import * as DialogPrimitive from "@radix-ui/react-dialog";
|
|
4
4
|
import { X } from "lucide-react";
|
|
5
|
+
import { useUiStrings } from "../../i18n/index.js";
|
|
5
6
|
import { cn } from "../../utils/cn.js";
|
|
6
7
|
import {
|
|
7
8
|
PreviousFocusProvider,
|
|
@@ -23,7 +24,7 @@ const DrawerTitle = DialogPrimitive.Title;
|
|
|
23
24
|
function DrawerContent({
|
|
24
25
|
className,
|
|
25
26
|
blocking = false,
|
|
26
|
-
closeLabel
|
|
27
|
+
closeLabel,
|
|
27
28
|
children,
|
|
28
29
|
onEscapeKeyDown,
|
|
29
30
|
onPointerDownOutside,
|
|
@@ -32,6 +33,7 @@ function DrawerContent({
|
|
|
32
33
|
...props
|
|
33
34
|
}) {
|
|
34
35
|
const previousFocusRef = usePreviousFocus();
|
|
36
|
+
const strings = useUiStrings();
|
|
35
37
|
return /* @__PURE__ */ jsxs(DialogPrimitive.Portal, { children: [
|
|
36
38
|
/* @__PURE__ */ jsx(DialogPrimitive.Overlay, { className: "fixed inset-0 z-70 bg-scrim" }),
|
|
37
39
|
/* @__PURE__ */ jsxs(
|
|
@@ -63,8 +65,8 @@ function DrawerContent({
|
|
|
63
65
|
!blocking && /* @__PURE__ */ jsx(
|
|
64
66
|
DialogPrimitive.Close,
|
|
65
67
|
{
|
|
66
|
-
"aria-label": closeLabel,
|
|
67
|
-
className: "absolute top-3 right-3 rounded-event p-1 text-ink-3 hover:text-ink",
|
|
68
|
+
"aria-label": closeLabel ?? strings.close,
|
|
69
|
+
className: "absolute top-3 right-3 rounded-event p-1 text-ink-3 hover:text-ink in-data-[density=touch]:top-0.5 in-data-[density=touch]:right-0.5 in-data-[density=touch]:p-3.5",
|
|
68
70
|
children: /* @__PURE__ */ jsx(X, { "aria-hidden": "true", className: "size-4" })
|
|
69
71
|
}
|
|
70
72
|
)
|
|
@@ -1,6 +1,11 @@
|
|
|
1
|
-
import type
|
|
1
|
+
import { type ReactNode } from "react";
|
|
2
2
|
export interface ErrorStateProps {
|
|
3
|
-
message
|
|
3
|
+
/** Short headline. Wins over `message` as the headline when both are given. */
|
|
4
|
+
title?: ReactNode;
|
|
5
|
+
/** Explanation under the headline, in plain language. */
|
|
6
|
+
description?: ReactNode;
|
|
7
|
+
/** Headline when `title` is absent (kept for source compatibility). */
|
|
8
|
+
message?: ReactNode;
|
|
4
9
|
/** Correlation code for support; never a raw exception. */
|
|
5
10
|
correlationId?: ReactNode;
|
|
6
11
|
/** Prefix shown before the correlation id. */
|
|
@@ -9,4 +14,4 @@ export interface ErrorStateProps {
|
|
|
9
14
|
retryLabel?: string;
|
|
10
15
|
className?: string;
|
|
11
16
|
}
|
|
12
|
-
export declare function ErrorState({ message, correlationId, correlationLabel, onRetry, retryLabel, className, }: ErrorStateProps): import("react").JSX.Element;
|
|
17
|
+
export declare function ErrorState({ title, description, message, correlationId, correlationLabel, onRetry, retryLabel, className, }: ErrorStateProps): import("react").JSX.Element;
|
|
@@ -1,14 +1,21 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
import { jsx, jsxs } from "react/jsx-runtime";
|
|
3
|
+
import { isValidElement } from "react";
|
|
4
|
+
import { useUiStrings } from "../../i18n/index.js";
|
|
3
5
|
import { cn } from "../../utils/cn.js";
|
|
4
6
|
function ErrorState({
|
|
7
|
+
title,
|
|
8
|
+
description,
|
|
5
9
|
message,
|
|
6
10
|
correlationId,
|
|
7
|
-
correlationLabel
|
|
11
|
+
correlationLabel,
|
|
8
12
|
onRetry,
|
|
9
|
-
retryLabel
|
|
13
|
+
retryLabel,
|
|
10
14
|
className
|
|
11
15
|
}) {
|
|
16
|
+
const strings = useUiStrings();
|
|
17
|
+
const headline = displayable(title) ?? displayable(message) ?? strings.somethingWrong;
|
|
18
|
+
const detail = displayable(description) ?? (title === void 0 ? void 0 : displayable(message));
|
|
12
19
|
return /* @__PURE__ */ jsxs(
|
|
13
20
|
"div",
|
|
14
21
|
{
|
|
@@ -18,9 +25,10 @@ function ErrorState({
|
|
|
18
25
|
className
|
|
19
26
|
),
|
|
20
27
|
children: [
|
|
21
|
-
/* @__PURE__ */ jsx("p", { className: "max-w-full font-semibold text-[14px] text-ink [overflow-wrap:anywhere]", children:
|
|
28
|
+
/* @__PURE__ */ jsx("p", { className: "max-w-full font-semibold text-[14px] text-ink [overflow-wrap:anywhere]", children: headline }),
|
|
29
|
+
detail ? /* @__PURE__ */ jsx("p", { className: "max-w-[42ch] text-[13px] text-ink-3 [overflow-wrap:anywhere]", children: detail }) : null,
|
|
22
30
|
correlationId ? /* @__PURE__ */ jsxs("p", { className: "max-w-full text-[11.5px] text-ink-3 tabular-nums [overflow-wrap:anywhere]", children: [
|
|
23
|
-
correlationLabel,
|
|
31
|
+
correlationLabel ?? strings.correlationPrefix,
|
|
24
32
|
" ",
|
|
25
33
|
correlationId
|
|
26
34
|
] }) : null,
|
|
@@ -30,13 +38,19 @@ function ErrorState({
|
|
|
30
38
|
type: "button",
|
|
31
39
|
onClick: onRetry,
|
|
32
40
|
className: "mt-2 inline-flex h-control-sm cursor-pointer items-center rounded-input border-control bg-surface-control px-3 font-semibold text-[13px] text-ink hover:bg-surface-2",
|
|
33
|
-
children: retryLabel
|
|
41
|
+
children: retryLabel ?? strings.retry
|
|
34
42
|
}
|
|
35
43
|
) : null
|
|
36
44
|
]
|
|
37
45
|
}
|
|
38
46
|
);
|
|
39
47
|
}
|
|
48
|
+
function displayable(value) {
|
|
49
|
+
if (value === null || value === void 0 || typeof value === "boolean") return void 0;
|
|
50
|
+
if (typeof value === "string" || typeof value === "number") return value;
|
|
51
|
+
if (isValidElement(value) || Array.isArray(value)) return value;
|
|
52
|
+
return void 0;
|
|
53
|
+
}
|
|
40
54
|
export {
|
|
41
55
|
ErrorState
|
|
42
56
|
};
|
|
@@ -1,19 +1,22 @@
|
|
|
1
|
+
"use client";
|
|
1
2
|
import { jsx, jsxs } from "react/jsx-runtime";
|
|
3
|
+
import { useUiStrings } from "../../i18n/index.js";
|
|
2
4
|
import { cn } from "../../utils/cn.js";
|
|
3
5
|
const PLACEHOLDER_IDS = ["a", "b", "c", "d", "e", "f", "g", "h", "i", "j", "k", "l"];
|
|
4
6
|
function Skeleton({
|
|
5
7
|
variant = "lines",
|
|
6
8
|
lines = 3,
|
|
7
9
|
rows = 4,
|
|
8
|
-
label
|
|
10
|
+
label,
|
|
9
11
|
className
|
|
10
12
|
}) {
|
|
13
|
+
const strings = useUiStrings();
|
|
11
14
|
return /* @__PURE__ */ jsxs(
|
|
12
15
|
"div",
|
|
13
16
|
{
|
|
14
17
|
role: "status",
|
|
15
18
|
"aria-busy": "true",
|
|
16
|
-
"aria-label": label,
|
|
19
|
+
"aria-label": label ?? strings.loading,
|
|
17
20
|
className: cn("animate-pulse", className),
|
|
18
21
|
children: [
|
|
19
22
|
variant === "lines" ? /* @__PURE__ */ jsx("div", { className: "flex flex-col gap-2", children: PLACEHOLDER_IDS.slice(0, lines).map((id, index) => /* @__PURE__ */ jsx(
|
package/package.json
CHANGED
package/scripts/build-theme.mjs
CHANGED
|
@@ -116,6 +116,13 @@ export function stylesOf(tokens) {
|
|
|
116
116
|
throw new Error(`styles: density token "${name}" is themed`);
|
|
117
117
|
}
|
|
118
118
|
|
|
119
|
+
for (const level of density.levels) {
|
|
120
|
+
for (const name of Object.keys(level.values ?? {})) {
|
|
121
|
+
if (!densityGoverned.has(name) || name === "density")
|
|
122
|
+
throw new Error(`styles: density level "${level.id}" sets "${name}", which is not scaled`);
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
119
126
|
const radiusLevels = styles.radius?.levels ?? [];
|
|
120
127
|
const radiusGoverned = new Set(radiusLevels.flatMap((l) => Object.keys(l.values ?? {})));
|
|
121
128
|
for (const name of radiusGoverned) known(name, "radius levels");
|
|
@@ -170,11 +177,10 @@ function buildStyleBlocks(tokens) {
|
|
|
170
177
|
}
|
|
171
178
|
|
|
172
179
|
for (const level of s.density.levels) {
|
|
173
|
-
const entries = s.densityGoverned.map((name) =>
|
|
174
|
-
name === "density"
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
);
|
|
180
|
+
const entries = s.densityGoverned.map((name) => {
|
|
181
|
+
if (name === "density") return [name, String(level.density)];
|
|
182
|
+
return [name, level.values?.[name] ?? `calc(${s.byName.get(name).value} * var(--density))`];
|
|
183
|
+
});
|
|
178
184
|
blocks.push(...variantBlocks("data-density", level.id, entries, themes));
|
|
179
185
|
}
|
|
180
186
|
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
import { Slot } from "@radix-ui/react-slot";
|
|
4
4
|
import {
|
|
5
5
|
Children,
|
|
6
|
-
type
|
|
6
|
+
type ComponentPropsWithRef,
|
|
7
7
|
Fragment,
|
|
8
8
|
isValidElement,
|
|
9
9
|
type ReactNode,
|
|
@@ -11,7 +11,7 @@ import {
|
|
|
11
11
|
import { cn } from "../../utils/cn.js";
|
|
12
12
|
|
|
13
13
|
export type ButtonVariant = "primary" | "secondary" | "ghost" | "danger";
|
|
14
|
-
export type ButtonSize = "sm" | "md";
|
|
14
|
+
export type ButtonSize = "sm" | "md" | "lg";
|
|
15
15
|
|
|
16
16
|
const variantClasses: Record<ButtonVariant, string> = {
|
|
17
17
|
primary: "bg-accent text-on-accent font-bold hover:bg-accent-ink",
|
|
@@ -23,12 +23,17 @@ const variantClasses: Record<ButtonVariant, string> = {
|
|
|
23
23
|
const sizeClasses: Record<ButtonSize, string> = {
|
|
24
24
|
sm: "h-control-sm px-3 text-[12.5px] [&_svg]:size-3.5",
|
|
25
25
|
md: "h-control px-control-x text-[13px]",
|
|
26
|
+
lg: "h-control-lg px-control-x text-[14px]",
|
|
26
27
|
};
|
|
27
28
|
|
|
28
|
-
export interface ButtonProps extends
|
|
29
|
+
export interface ButtonProps extends ComponentPropsWithRef<"button"> {
|
|
29
30
|
variant?: ButtonVariant;
|
|
30
31
|
size?: ButtonSize;
|
|
31
32
|
loading?: boolean;
|
|
33
|
+
/** Replaces the children while `loading` and is announced to screen readers. */
|
|
34
|
+
loadingLabel?: string;
|
|
35
|
+
/** Stretch to the full width of the container. */
|
|
36
|
+
block?: boolean;
|
|
32
37
|
/** Render the child element (via Radix Slot) instead of a `<button>`, e.g. a link. */
|
|
33
38
|
asChild?: boolean;
|
|
34
39
|
}
|
|
@@ -37,6 +42,8 @@ export function Button({
|
|
|
37
42
|
variant = "secondary",
|
|
38
43
|
size = "md",
|
|
39
44
|
loading,
|
|
45
|
+
loadingLabel,
|
|
46
|
+
block,
|
|
40
47
|
disabled,
|
|
41
48
|
asChild,
|
|
42
49
|
className,
|
|
@@ -46,8 +53,13 @@ export function Button({
|
|
|
46
53
|
const Comp = asChild ? Slot : "button";
|
|
47
54
|
// Text runs get their own truncating span so a long label ends in "…" inside a narrow
|
|
48
55
|
// container instead of overflowing; icons keep their size. Slot needs its single child as is.
|
|
49
|
-
const
|
|
50
|
-
|
|
56
|
+
const showLoadingLabel = loading && loadingLabel !== undefined && !asChild;
|
|
57
|
+
const content = showLoadingLabel
|
|
58
|
+
? labelRun(loadingLabel)
|
|
59
|
+
: asChild
|
|
60
|
+
? children
|
|
61
|
+
: Children.map(children, labelRun);
|
|
62
|
+
const button = (
|
|
51
63
|
<Comp
|
|
52
64
|
{...(asChild ? {} : { type: "button" })}
|
|
53
65
|
aria-busy={loading ? "true" : undefined}
|
|
@@ -56,6 +68,7 @@ export function Button({
|
|
|
56
68
|
"inline-flex min-w-0 max-w-full cursor-pointer items-center justify-center gap-1.5 whitespace-nowrap rounded-button pressable focus-visible:focus-ring font-[650] no-underline disabled:cursor-not-allowed disabled:opacity-45 [&_svg]:shrink-0",
|
|
57
69
|
variantClasses[variant],
|
|
58
70
|
sizeClasses[size],
|
|
71
|
+
block && "flex w-full",
|
|
59
72
|
className,
|
|
60
73
|
)}
|
|
61
74
|
{...props}
|
|
@@ -63,6 +76,15 @@ export function Button({
|
|
|
63
76
|
{content}
|
|
64
77
|
</Comp>
|
|
65
78
|
);
|
|
79
|
+
if (loadingLabel === undefined) return button;
|
|
80
|
+
return (
|
|
81
|
+
<>
|
|
82
|
+
{button}
|
|
83
|
+
<span role="status" className="sr-only">
|
|
84
|
+
{loading ? loadingLabel : null}
|
|
85
|
+
</span>
|
|
86
|
+
</>
|
|
87
|
+
);
|
|
66
88
|
}
|
|
67
89
|
|
|
68
90
|
function labelRun(child: ReactNode): ReactNode {
|
|
@@ -10,7 +10,10 @@ export interface CheckboxProps extends ComponentPropsWithoutRef<typeof CheckboxP
|
|
|
10
10
|
export function Checkbox({ className, ...props }: CheckboxProps) {
|
|
11
11
|
return (
|
|
12
12
|
<CheckboxPrimitive.Root
|
|
13
|
-
className={cn(
|
|
13
|
+
className={cn(
|
|
14
|
+
"inline-flex size-10 shrink-0 items-center justify-center in-data-[density=touch]:size-11",
|
|
15
|
+
className,
|
|
16
|
+
)}
|
|
14
17
|
{...props}
|
|
15
18
|
>
|
|
16
19
|
<span className="grid size-[18px] place-items-center rounded-pill border-[1.5px] border-line-strong data-[state=checked]:border-success data-[state=checked]:bg-success">
|
package/src/atoms/chip/chip.tsx
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
import type { ComponentPropsWithoutRef } from "react";
|
|
1
|
+
import type { ComponentPropsWithoutRef, ReactNode } from "react";
|
|
2
2
|
import { cn } from "../../utils/cn.js";
|
|
3
|
+
import { type StatusTone, statusIcon } from "../internal/status-icons.js";
|
|
3
4
|
|
|
4
5
|
export type ChipTone =
|
|
5
6
|
| "neutral"
|
|
@@ -7,6 +8,7 @@ export type ChipTone =
|
|
|
7
8
|
| "warning"
|
|
8
9
|
| "success"
|
|
9
10
|
| "accent"
|
|
11
|
+
| "info"
|
|
10
12
|
| "solid"
|
|
11
13
|
| "outline";
|
|
12
14
|
|
|
@@ -16,6 +18,7 @@ const toneClasses: Record<ChipTone, string> = {
|
|
|
16
18
|
warning: "bg-warning-soft text-warning",
|
|
17
19
|
success: "bg-success-soft text-success",
|
|
18
20
|
accent: "bg-accent-soft text-accent-ink",
|
|
21
|
+
info: "bg-info-soft text-info",
|
|
19
22
|
solid: "bg-accent text-on-accent",
|
|
20
23
|
outline: "bg-transparent text-ink-2 shadow-[inset_0_0_0_1px_var(--line-strong)]",
|
|
21
24
|
};
|
|
@@ -24,18 +27,40 @@ export interface ChipProps extends ComponentPropsWithoutRef<"span"> {
|
|
|
24
27
|
tone?: ChipTone;
|
|
25
28
|
/** Use the smaller chip radius instead of a full pill. */
|
|
26
29
|
square?: boolean;
|
|
30
|
+
/** Leading decorative icon; wins over `statusIcon`. */
|
|
31
|
+
icon?: ReactNode;
|
|
32
|
+
/** Prefix `info`, `success`, `warning` and `danger` chips with their status glyph. */
|
|
33
|
+
statusIcon?: boolean;
|
|
27
34
|
}
|
|
28
35
|
|
|
29
|
-
|
|
36
|
+
const statusTones = new Set<ChipTone>(["info", "success", "warning", "danger"]);
|
|
37
|
+
|
|
38
|
+
export function Chip({
|
|
39
|
+
tone = "neutral",
|
|
40
|
+
square,
|
|
41
|
+
icon,
|
|
42
|
+
statusIcon: withStatusIcon,
|
|
43
|
+
className,
|
|
44
|
+
children,
|
|
45
|
+
...props
|
|
46
|
+
}: ChipProps) {
|
|
47
|
+
const leading =
|
|
48
|
+
icon ??
|
|
49
|
+
(withStatusIcon && statusTones.has(tone) ? statusIcon(tone as StatusTone, "size-3") : null);
|
|
50
|
+
const interactive = props.onClick !== undefined || props.role === "button";
|
|
30
51
|
return (
|
|
31
52
|
<span
|
|
32
53
|
className={cn(
|
|
33
54
|
"inline-flex h-6 max-w-full items-center gap-1.5 overflow-hidden whitespace-nowrap px-2.5 font-[650] text-[12px] leading-none [&_svg]:size-3",
|
|
34
55
|
square ? "rounded-chip" : "rounded-badge",
|
|
35
56
|
toneClasses[tone],
|
|
57
|
+
interactive && "in-data-[density=touch]:min-h-11",
|
|
36
58
|
className,
|
|
37
59
|
)}
|
|
38
60
|
{...props}
|
|
39
|
-
|
|
61
|
+
>
|
|
62
|
+
{leading}
|
|
63
|
+
{children}
|
|
64
|
+
</span>
|
|
40
65
|
);
|
|
41
66
|
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { CircleAlert, CircleCheck, Info, TriangleAlert } from "lucide-react";
|
|
2
|
+
import type { ReactNode } from "react";
|
|
3
|
+
|
|
4
|
+
export type StatusTone = "info" | "success" | "warning" | "danger";
|
|
5
|
+
|
|
6
|
+
const icons = {
|
|
7
|
+
info: Info,
|
|
8
|
+
success: CircleCheck,
|
|
9
|
+
warning: TriangleAlert,
|
|
10
|
+
danger: CircleAlert,
|
|
11
|
+
} as const;
|
|
12
|
+
|
|
13
|
+
/** Decorative glyph that pairs a status tone with a shape, so status never relies on color alone. */
|
|
14
|
+
export function statusIcon(tone: StatusTone, className = "size-4"): ReactNode {
|
|
15
|
+
const Icon = icons[tone];
|
|
16
|
+
return <Icon aria-hidden="true" className={className} />;
|
|
17
|
+
}
|
|
@@ -31,7 +31,7 @@ export function RadioGroup({ options, className, ...props }: RadioGroupProps) {
|
|
|
31
31
|
<label
|
|
32
32
|
key={option.value}
|
|
33
33
|
htmlFor={id}
|
|
34
|
-
className="flex cursor-pointer items-start gap-2 rounded-control border-divider p-2.5 text-body has-[[data-state=checked]]:border-accent has-[[data-state=checked]]:shadow-[inset_0_0_0_1px_var(--accent)]"
|
|
34
|
+
className="flex cursor-pointer items-start gap-2 rounded-control border-divider p-2.5 text-body in-data-[density=touch]:min-h-11 has-[[data-state=checked]]:border-accent has-[[data-state=checked]]:shadow-[inset_0_0_0_1px_var(--accent)]"
|
|
35
35
|
>
|
|
36
36
|
<RadioGroupPrimitive.Item
|
|
37
37
|
id={id}
|
|
@@ -9,7 +9,10 @@ export interface SwitchProps extends ComponentPropsWithoutRef<typeof SwitchPrimi
|
|
|
9
9
|
export function Switch({ className, ...props }: SwitchProps) {
|
|
10
10
|
return (
|
|
11
11
|
<SwitchPrimitive.Root
|
|
12
|
-
className={cn(
|
|
12
|
+
className={cn(
|
|
13
|
+
"inline-flex size-10 shrink-0 items-center justify-center in-data-[density=touch]:size-11",
|
|
14
|
+
className,
|
|
15
|
+
)}
|
|
13
16
|
{...props}
|
|
14
17
|
>
|
|
15
18
|
<span className="relative block h-5 w-[34px] rounded-pill bg-line-strong transition-colors data-[state=checked]:bg-accent">
|