@jcsoftdev/ui 0.1.5 → 0.1.7
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 +5 -2
- 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 +5 -2
- 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 +5 -2
- 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 +5 -2
- 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
package/README.md
CHANGED
|
@@ -22,7 +22,7 @@ Set these on `<html>` (or any element to restyle only its subtree); no rebuild i
|
|
|
22
22
|
| `data-theme="light\|dark"` | Color scheme (omit to follow the OS). |
|
|
23
23
|
| `data-accent="<palette-id>"` | Brand palette. |
|
|
24
24
|
| `data-style="<preset>"` | Style preset (radii, borders, shadows, glass, ...). See https://ui.jcsoftdev.com/docs/theming/. |
|
|
25
|
-
| `data-density="<level>"` | Control and spacing density
|
|
25
|
+
| `data-density="<level>"` | Control and spacing density: `compact`, `normal`, `comfortable`, `touch`. |
|
|
26
26
|
| `data-radius="<level>"` | Global corner radius scale. |
|
|
27
27
|
|
|
28
28
|
### Subpath exports
|
|
@@ -30,6 +30,7 @@ Set these on `<html>` (or any element to restyle only its subtree); no rebuild i
|
|
|
30
30
|
- Component groups: `@jcsoftdev/ui/atoms`, `forms`, `overlays`, `navigation`, `containers`, `states`, `shell`, `palette`, `calendar`, `charts`, `dashboards`, `utils`, each also per component (`@jcsoftdev/ui/atoms/button`).
|
|
31
31
|
- `@jcsoftdev/ui/ai`: chat, prompt input, markdown and code block building blocks for AI interfaces.
|
|
32
32
|
- `@jcsoftdev/ui/rich-text`: rich text editor with a sanitizer.
|
|
33
|
+
- `@jcsoftdev/ui/i18n`: `UiStringsProvider`, `useUiStrings` and the `en`/`es` string presets.
|
|
33
34
|
- `@jcsoftdev/ui/xlsx`: dependency-free `.xlsx` writer.
|
|
34
35
|
- `@jcsoftdev/ui/brand`: logo mark and logo forge utilities.
|
|
35
36
|
- `@jcsoftdev/ui/styles/*` and `@jcsoftdev/ui/tokens/*`: CSS and token JSON.
|
|
@@ -97,6 +98,88 @@ A custom tokens file must define the same names as `tokens/default.json`; copy i
|
|
|
97
98
|
|
|
98
99
|
Switching at runtime: set `data-theme="light|dark"` (remove it for "system") and `data-accent="<palette-id>"` on `<html>`.
|
|
99
100
|
|
|
101
|
+
## Touch density
|
|
102
|
+
|
|
103
|
+
`data-density="touch"` is the preset for phones operated with a thumb: controls and small buttons are
|
|
104
|
+
44px tall, large buttons 52px, text inputs 48px, and padding follows `comfortable`. Components with
|
|
105
|
+
fixed hit areas (Checkbox, Switch, RadioGroup rows, an interactive Chip, the Dialog and Drawer close
|
|
106
|
+
buttons) grow to at least 44px under it and are unchanged under every other level.
|
|
107
|
+
|
|
108
|
+
```html
|
|
109
|
+
<html data-density="touch"> <!-- or any subtree: <main data-density="touch"> -->
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
Portaled content (Dialog, Drawer) leaves the subtree, so set the attribute on `<html>` or pass the
|
|
113
|
+
`container` prop to keep it inside. Custom token files can declare absolute sizes per level:
|
|
114
|
+
`{ "id": "touch", "density": 1.125, "values": { "control-height": "44px" } }`; the keys must be
|
|
115
|
+
tokens listed in `styles.density.scaled`.
|
|
116
|
+
|
|
117
|
+
## Localized default strings
|
|
118
|
+
|
|
119
|
+
Default texts (Retry, Close, Loading, the optional marker, Cancel, Confirm, Processing…) come from
|
|
120
|
+
`UiStringsProvider`. `en` is the default and needs no provider; `es` is neutral Spanish (tú).
|
|
121
|
+
Explicit props such as `retryLabel` or `closeLabel` always win.
|
|
122
|
+
|
|
123
|
+
```tsx
|
|
124
|
+
import { UiStringsProvider } from "@jcsoftdev/ui/i18n";
|
|
125
|
+
|
|
126
|
+
<UiStringsProvider locale="es" strings={{ retry: "Probar otra vez" }}>
|
|
127
|
+
<App />
|
|
128
|
+
</UiStringsProvider>;
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
`uiStrings.en` and `uiStrings.es` export the presets; `useUiStrings()` reads the active set. A nested
|
|
132
|
+
provider inherits its parent and overrides only the keys it passes. Hard-coded Spanish labels that
|
|
133
|
+
older components already ship (NumberStepper, Carousel, AI set) are not routed through it yet.
|
|
134
|
+
|
|
135
|
+
## Status, forms and states (additive props)
|
|
136
|
+
|
|
137
|
+
| Component | Added |
|
|
138
|
+
| --- | --- |
|
|
139
|
+
| `Button` | `loadingLabel` (shown instead of the children while `loading`, announced through a status region), `block` (full width), `size="lg"` (52px at normal density, scaled like the other controls). |
|
|
140
|
+
| `Field` | `optional` appends the localized "(optional)" marker to the label; `error` also accepts `null`. |
|
|
141
|
+
| `Banner` | `title`; a default glyph per tone when `icon` is not passed (`icon={null}` hides it, so status never relies on color alone); only `danger` is `role="alert"`, the rest are `role="status"`; the `info` tone uses the `info` tokens. |
|
|
142
|
+
| `Chip` | `icon`, `statusIcon` (prefixes `info`, `success`, `warning`, `danger` chips with their glyph), and an `info` tone. |
|
|
143
|
+
| `ErrorState` | `title` and `description` (`message` keeps working as the headline); an `Error` or any non-renderable value is never printed, a generic localized sentence is shown instead. |
|
|
144
|
+
|
|
145
|
+
## ConfirmAction and ResponsiveList
|
|
146
|
+
|
|
147
|
+
```tsx
|
|
148
|
+
import { ConfirmAction } from "@jcsoftdev/ui/containers/confirm-action";
|
|
149
|
+
|
|
150
|
+
<ConfirmAction
|
|
151
|
+
label="Cancel order"
|
|
152
|
+
consequence="The order of Marco Salazar is cancelled and S/ 45 is refunded."
|
|
153
|
+
onConfirm={async () => {
|
|
154
|
+
const res = await cancelOrder(id);
|
|
155
|
+
return res.ok ? undefined : { ok: false, error: "The payment is already settled." };
|
|
156
|
+
}}
|
|
157
|
+
/>;
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
`ConfirmAction` swaps its trigger for the consequence, a danger button and a Cancel button that takes
|
|
161
|
+
focus. While `onConfirm` runs the button shows the localized "Processing…"; a returned
|
|
162
|
+
`{ ok: false, error }` or a thrown error keeps the step open with an inline message.
|
|
163
|
+
|
|
164
|
+
```tsx
|
|
165
|
+
import { ResponsiveList } from "@jcsoftdev/ui/containers/responsive-list";
|
|
166
|
+
|
|
167
|
+
<ResponsiveList
|
|
168
|
+
caption="Today's orders"
|
|
169
|
+
items={orders}
|
|
170
|
+
rowKey={(o) => o.id}
|
|
171
|
+
columns={[
|
|
172
|
+
{ key: "customer", header: "Customer", cell: (o) => o.customer, primary: true },
|
|
173
|
+
{ key: "total", header: "Total", cell: (o) => o.total, align: "end" },
|
|
174
|
+
]}
|
|
175
|
+
renderActions={(o) => <Button size="sm">Open</Button>}
|
|
176
|
+
/>;
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
A table from `md` up (library `Table` styling) and a stack of cards below it; the `primary` column
|
|
180
|
+
(default: the first) titles each card and the other columns become label/value rows. Every row
|
|
181
|
+
carries `data-row-key`; `rowClassName` applies to both layouts.
|
|
182
|
+
|
|
100
183
|
## Install a single component (tree shaking)
|
|
101
184
|
|
|
102
185
|
The package is unbundled ESM, `"sideEffects": ["**/*.css"]`, and every component has its own entry point:
|
|
@@ -128,6 +211,6 @@ The `source` export condition points at `src/*/index.ts` for consumers that want
|
|
|
128
211
|
|
|
129
212
|
## Conventions
|
|
130
213
|
|
|
131
|
-
- English identifiers; every visible string and aria label is a prop
|
|
214
|
+
- English identifiers; every visible string and aria label is a prop. Defaults are English and can be localized in one place with `UiStringsProvider`.
|
|
132
215
|
- Relative imports use the `.js` extension; components import specific component entries, never group barrels.
|
|
133
216
|
- No literal colors in components, no `dark:` variants; dark mode is a token concern.
|
|
@@ -1,11 +1,15 @@
|
|
|
1
|
-
import { type
|
|
1
|
+
import { type ComponentPropsWithRef } from "react";
|
|
2
2
|
export type ButtonVariant = "primary" | "secondary" | "ghost" | "danger";
|
|
3
|
-
export type ButtonSize = "sm" | "md";
|
|
4
|
-
export interface ButtonProps extends
|
|
3
|
+
export type ButtonSize = "sm" | "md" | "lg";
|
|
4
|
+
export interface ButtonProps extends ComponentPropsWithRef<"button"> {
|
|
5
5
|
variant?: ButtonVariant;
|
|
6
6
|
size?: ButtonSize;
|
|
7
7
|
loading?: boolean;
|
|
8
|
+
/** Replaces the children while `loading` and is announced to screen readers. */
|
|
9
|
+
loadingLabel?: string;
|
|
10
|
+
/** Stretch to the full width of the container. */
|
|
11
|
+
block?: boolean;
|
|
8
12
|
/** Render the child element (via Radix Slot) instead of a `<button>`, e.g. a link. */
|
|
9
13
|
asChild?: boolean;
|
|
10
14
|
}
|
|
11
|
-
export declare function Button({ variant, size, loading, disabled, asChild, className, children, ...props }: ButtonProps): import("react").JSX.Element;
|
|
15
|
+
export declare function Button({ variant, size, loading, loadingLabel, block, disabled, asChild, className, children, ...props }: ButtonProps): import("react").JSX.Element;
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
"use client";
|
|
2
|
-
import { jsx } from "react/jsx-runtime";
|
|
2
|
+
import { Fragment, jsx, jsxs } from "react/jsx-runtime";
|
|
3
3
|
import { Slot } from "@radix-ui/react-slot";
|
|
4
4
|
import {
|
|
5
5
|
Children,
|
|
6
|
-
Fragment,
|
|
6
|
+
Fragment as Fragment2,
|
|
7
7
|
isValidElement
|
|
8
8
|
} from "react";
|
|
9
9
|
import { cn } from "../../utils/cn.js";
|
|
@@ -15,12 +15,15 @@ const variantClasses = {
|
|
|
15
15
|
};
|
|
16
16
|
const sizeClasses = {
|
|
17
17
|
sm: "h-control-sm px-3 text-[12.5px] [&_svg]:size-3.5",
|
|
18
|
-
md: "h-control px-control-x text-[13px]"
|
|
18
|
+
md: "h-control px-control-x text-[13px]",
|
|
19
|
+
lg: "h-control-lg px-control-x text-[14px]"
|
|
19
20
|
};
|
|
20
21
|
function Button({
|
|
21
22
|
variant = "secondary",
|
|
22
23
|
size = "md",
|
|
23
24
|
loading,
|
|
25
|
+
loadingLabel,
|
|
26
|
+
block,
|
|
24
27
|
disabled,
|
|
25
28
|
asChild,
|
|
26
29
|
className,
|
|
@@ -28,8 +31,9 @@ function Button({
|
|
|
28
31
|
...props
|
|
29
32
|
}) {
|
|
30
33
|
const Comp = asChild ? Slot : "button";
|
|
31
|
-
const
|
|
32
|
-
|
|
34
|
+
const showLoadingLabel = loading && loadingLabel !== void 0 && !asChild;
|
|
35
|
+
const content = showLoadingLabel ? labelRun(loadingLabel) : asChild ? children : Children.map(children, labelRun);
|
|
36
|
+
const button = /* @__PURE__ */ jsx(
|
|
33
37
|
Comp,
|
|
34
38
|
{
|
|
35
39
|
...asChild ? {} : { type: "button" },
|
|
@@ -39,18 +43,24 @@ function Button({
|
|
|
39
43
|
"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",
|
|
40
44
|
variantClasses[variant],
|
|
41
45
|
sizeClasses[size],
|
|
46
|
+
block && "flex w-full",
|
|
42
47
|
className
|
|
43
48
|
),
|
|
44
49
|
...props,
|
|
45
50
|
children: content
|
|
46
51
|
}
|
|
47
52
|
);
|
|
53
|
+
if (loadingLabel === void 0) return button;
|
|
54
|
+
return /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
55
|
+
button,
|
|
56
|
+
/* @__PURE__ */ jsx("span", { role: "status", className: "sr-only", children: loading ? loadingLabel : null })
|
|
57
|
+
] });
|
|
48
58
|
}
|
|
49
59
|
function labelRun(child) {
|
|
50
60
|
if (typeof child === "string" || typeof child === "number") {
|
|
51
61
|
return /* @__PURE__ */ jsx("span", { className: "min-w-0 truncate", children: child });
|
|
52
62
|
}
|
|
53
|
-
if (isValidElement(child) && child.type ===
|
|
63
|
+
if (isValidElement(child) && child.type === Fragment2) {
|
|
54
64
|
return Children.map(child.props.children, labelRun);
|
|
55
65
|
}
|
|
56
66
|
return child;
|
|
@@ -7,9 +7,12 @@ function Checkbox({ className, ...props }) {
|
|
|
7
7
|
return /* @__PURE__ */ jsx(
|
|
8
8
|
CheckboxPrimitive.Root,
|
|
9
9
|
{
|
|
10
|
-
className: cn(
|
|
10
|
+
className: cn(
|
|
11
|
+
"group inline-flex size-10 shrink-0 items-center justify-center in-data-[density=touch]:size-11",
|
|
12
|
+
className
|
|
13
|
+
),
|
|
11
14
|
...props,
|
|
12
|
-
children: /* @__PURE__ */ jsx("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", children: /* @__PURE__ */ jsx(CheckboxPrimitive.Indicator, { children: /* @__PURE__ */ jsx(Check, { "aria-hidden": "true", className: "size-[11px] stroke-[3] text-on-accent" }) }) })
|
|
15
|
+
children: /* @__PURE__ */ jsx("span", { className: "grid size-[18px] place-items-center rounded-pill border-[1.5px] border-line-strong group-data-[state=checked]:border-success group-data-[state=checked]:bg-success group-data-[state=indeterminate]:border-success group-data-[state=indeterminate]:bg-success", children: /* @__PURE__ */ jsx(CheckboxPrimitive.Indicator, { children: /* @__PURE__ */ jsx(Check, { "aria-hidden": "true", className: "size-[11px] stroke-[3] text-on-accent" }) }) })
|
|
13
16
|
}
|
|
14
17
|
);
|
|
15
18
|
}
|
|
@@ -1,8 +1,12 @@
|
|
|
1
|
-
import type { ComponentPropsWithoutRef } from "react";
|
|
2
|
-
export type ChipTone = "neutral" | "danger" | "warning" | "success" | "accent" | "solid" | "outline";
|
|
1
|
+
import type { ComponentPropsWithoutRef, ReactNode } from "react";
|
|
2
|
+
export type ChipTone = "neutral" | "danger" | "warning" | "success" | "accent" | "info" | "solid" | "outline";
|
|
3
3
|
export interface ChipProps extends ComponentPropsWithoutRef<"span"> {
|
|
4
4
|
tone?: ChipTone;
|
|
5
5
|
/** Use the smaller chip radius instead of a full pill. */
|
|
6
6
|
square?: boolean;
|
|
7
|
+
/** Leading decorative icon; wins over `statusIcon`. */
|
|
8
|
+
icon?: ReactNode;
|
|
9
|
+
/** Prefix `info`, `success`, `warning` and `danger` chips with their status glyph. */
|
|
10
|
+
statusIcon?: boolean;
|
|
7
11
|
}
|
|
8
|
-
export declare function Chip({ tone, square, className, ...props }: ChipProps): import("react").JSX.Element;
|
|
12
|
+
export declare function Chip({ tone, square, icon, statusIcon: withStatusIcon, className, children, ...props }: ChipProps): import("react").JSX.Element;
|
package/dist/atoms/chip/chip.js
CHANGED
|
@@ -1,25 +1,43 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { jsxs } from "react/jsx-runtime";
|
|
2
2
|
import { cn } from "../../utils/cn.js";
|
|
3
|
+
import { statusIcon } from "../internal/status-icons.js";
|
|
3
4
|
const toneClasses = {
|
|
4
5
|
neutral: "bg-surface-2 text-ink-2",
|
|
5
6
|
danger: "bg-danger-soft text-danger",
|
|
6
7
|
warning: "bg-warning-soft text-warning",
|
|
7
8
|
success: "bg-success-soft text-success",
|
|
8
9
|
accent: "bg-accent-soft text-accent-ink",
|
|
10
|
+
info: "bg-info-soft text-info",
|
|
9
11
|
solid: "bg-accent text-on-accent",
|
|
10
12
|
outline: "bg-transparent text-ink-2 shadow-[inset_0_0_0_1px_var(--line-strong)]"
|
|
11
13
|
};
|
|
12
|
-
|
|
13
|
-
|
|
14
|
+
const statusTones = /* @__PURE__ */ new Set(["info", "success", "warning", "danger"]);
|
|
15
|
+
function Chip({
|
|
16
|
+
tone = "neutral",
|
|
17
|
+
square,
|
|
18
|
+
icon,
|
|
19
|
+
statusIcon: withStatusIcon,
|
|
20
|
+
className,
|
|
21
|
+
children,
|
|
22
|
+
...props
|
|
23
|
+
}) {
|
|
24
|
+
const leading = icon ?? (withStatusIcon && statusTones.has(tone) ? statusIcon(tone, "size-3") : null);
|
|
25
|
+
const interactive = props.onClick !== void 0 || props.role === "button";
|
|
26
|
+
return /* @__PURE__ */ jsxs(
|
|
14
27
|
"span",
|
|
15
28
|
{
|
|
16
29
|
className: cn(
|
|
17
30
|
"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",
|
|
18
31
|
square ? "rounded-chip" : "rounded-badge",
|
|
19
32
|
toneClasses[tone],
|
|
33
|
+
interactive && "in-data-[density=touch]:min-h-11",
|
|
20
34
|
className
|
|
21
35
|
),
|
|
22
|
-
...props
|
|
36
|
+
...props,
|
|
37
|
+
children: [
|
|
38
|
+
leading,
|
|
39
|
+
children
|
|
40
|
+
]
|
|
23
41
|
}
|
|
24
42
|
);
|
|
25
43
|
}
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import type { ReactNode } from "react";
|
|
2
|
+
export type StatusTone = "info" | "success" | "warning" | "danger";
|
|
3
|
+
/** Decorative glyph that pairs a status tone with a shape, so status never relies on color alone. */
|
|
4
|
+
export declare function statusIcon(tone: StatusTone, className?: string): ReactNode;
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { jsx } from "react/jsx-runtime";
|
|
2
|
+
import { CircleAlert, CircleCheck, Info, TriangleAlert } from "lucide-react";
|
|
3
|
+
const icons = {
|
|
4
|
+
info: Info,
|
|
5
|
+
success: CircleCheck,
|
|
6
|
+
warning: TriangleAlert,
|
|
7
|
+
danger: CircleAlert
|
|
8
|
+
};
|
|
9
|
+
function statusIcon(tone, className = "size-4") {
|
|
10
|
+
const Icon = icons[tone];
|
|
11
|
+
return /* @__PURE__ */ jsx(Icon, { "aria-hidden": "true", className });
|
|
12
|
+
}
|
|
13
|
+
export {
|
|
14
|
+
statusIcon
|
|
15
|
+
};
|
|
@@ -19,7 +19,7 @@ function RadioGroup({ options, className, ...props }) {
|
|
|
19
19
|
"label",
|
|
20
20
|
{
|
|
21
21
|
htmlFor: id,
|
|
22
|
-
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)]",
|
|
22
|
+
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)]",
|
|
23
23
|
children: [
|
|
24
24
|
/* @__PURE__ */ jsx(
|
|
25
25
|
RadioGroupPrimitive.Item,
|
|
@@ -6,9 +6,12 @@ function Switch({ className, ...props }) {
|
|
|
6
6
|
return /* @__PURE__ */ jsx(
|
|
7
7
|
SwitchPrimitive.Root,
|
|
8
8
|
{
|
|
9
|
-
className: cn(
|
|
9
|
+
className: cn(
|
|
10
|
+
"group inline-flex size-10 shrink-0 items-center justify-center in-data-[density=touch]:size-11",
|
|
11
|
+
className
|
|
12
|
+
),
|
|
10
13
|
...props,
|
|
11
|
-
children: /* @__PURE__ */ jsx("span", { className: "relative block h-5 w-[34px] rounded-pill bg-line-strong transition-colors data-[state=checked]:bg-accent", children: /* @__PURE__ */ jsx(SwitchPrimitive.Thumb, { className: "absolute top-[3px] left-[3px] block size-3.5 rounded-pill bg-surface transition-transform data-[state=checked]:translate-x-3.5" }) })
|
|
14
|
+
children: /* @__PURE__ */ jsx("span", { className: "relative block h-5 w-[34px] rounded-pill bg-line-strong transition-colors group-data-[state=checked]:bg-accent", children: /* @__PURE__ */ jsx(SwitchPrimitive.Thumb, { className: "absolute top-[3px] left-[3px] block size-3.5 rounded-pill bg-surface transition-transform data-[state=checked]:translate-x-3.5" }) })
|
|
12
15
|
}
|
|
13
16
|
);
|
|
14
17
|
}
|
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
import type { ComponentPropsWithoutRef, ReactNode } from "react";
|
|
2
2
|
export type BannerTone = "info" | "success" | "warning" | "danger";
|
|
3
|
-
export interface BannerProps extends Omit<ComponentPropsWithoutRef<"div">, "role"> {
|
|
3
|
+
export interface BannerProps extends Omit<ComponentPropsWithoutRef<"div">, "role" | "title"> {
|
|
4
4
|
tone?: BannerTone;
|
|
5
|
-
/**
|
|
5
|
+
/** Bold first line above the content. */
|
|
6
|
+
title?: ReactNode;
|
|
7
|
+
/** Decorative icon on the left; marked `aria-hidden`. Defaults to a glyph for the tone; `null` hides it. */
|
|
6
8
|
icon?: ReactNode;
|
|
7
9
|
/** Actions on the right, e.g. a retry button. */
|
|
8
10
|
action?: ReactNode;
|
|
9
11
|
}
|
|
10
|
-
export declare function Banner({ tone, icon, action, className, children, ...props }: BannerProps): import("react").JSX.Element;
|
|
12
|
+
export declare function Banner({ tone, title, icon, action, className, children, ...props }: BannerProps): import("react").JSX.Element;
|
|
@@ -1,13 +1,14 @@
|
|
|
1
1
|
import { jsx, jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { statusIcon } from "../../atoms/internal/status-icons.js";
|
|
2
3
|
import { cn } from "../../utils/cn.js";
|
|
3
4
|
const toneClasses = {
|
|
4
|
-
info: "bg-
|
|
5
|
+
info: "bg-info-soft",
|
|
5
6
|
success: "bg-success-soft",
|
|
6
7
|
warning: "bg-warning-soft",
|
|
7
8
|
danger: "bg-danger-soft"
|
|
8
9
|
};
|
|
9
10
|
const toneIconClasses = {
|
|
10
|
-
info: "text-
|
|
11
|
+
info: "text-info",
|
|
11
12
|
success: "text-success",
|
|
12
13
|
warning: "text-warning",
|
|
13
14
|
danger: "text-danger"
|
|
@@ -15,17 +16,19 @@ const toneIconClasses = {
|
|
|
15
16
|
const toneRole = {
|
|
16
17
|
info: "status",
|
|
17
18
|
success: "status",
|
|
18
|
-
warning: "
|
|
19
|
+
warning: "status",
|
|
19
20
|
danger: "alert"
|
|
20
21
|
};
|
|
21
22
|
function Banner({
|
|
22
23
|
tone = "info",
|
|
24
|
+
title,
|
|
23
25
|
icon,
|
|
24
26
|
action,
|
|
25
27
|
className,
|
|
26
28
|
children,
|
|
27
29
|
...props
|
|
28
30
|
}) {
|
|
31
|
+
const resolvedIcon = icon === void 0 ? statusIcon(tone) : icon;
|
|
29
32
|
return /* @__PURE__ */ jsxs(
|
|
30
33
|
"div",
|
|
31
34
|
{
|
|
@@ -37,8 +40,11 @@ function Banner({
|
|
|
37
40
|
),
|
|
38
41
|
...props,
|
|
39
42
|
children: [
|
|
40
|
-
|
|
41
|
-
/* @__PURE__ */
|
|
43
|
+
resolvedIcon ? /* @__PURE__ */ jsx("span", { "aria-hidden": "true", className: cn("mt-0.5 shrink-0", toneIconClasses[tone]), children: resolvedIcon }) : null,
|
|
44
|
+
/* @__PURE__ */ jsxs("div", { className: "min-w-0 flex-[1_1_14rem] break-words", children: [
|
|
45
|
+
title ? /* @__PURE__ */ jsx("p", { className: "font-semibold", children: title }) : null,
|
|
46
|
+
children
|
|
47
|
+
] }),
|
|
42
48
|
action ? /* @__PURE__ */ jsx("div", { className: "ml-auto flex shrink-0 flex-wrap items-center gap-1.5", children: action }) : null
|
|
43
49
|
]
|
|
44
50
|
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { type ButtonVariant } from "../../atoms/button/button.js";
|
|
2
|
+
import { type ConfirmResult } from "./model.js";
|
|
3
|
+
export interface ConfirmActionProps {
|
|
4
|
+
/** Trigger label, a verb in the user's words ("Cancel order"). */
|
|
5
|
+
label: string;
|
|
6
|
+
/** What will happen, said plainly, shown before the user commits. */
|
|
7
|
+
consequence: string;
|
|
8
|
+
/** Label of the destructive button. Defaults to the localized "Confirm". */
|
|
9
|
+
confirmLabel?: string;
|
|
10
|
+
cancelLabel?: string;
|
|
11
|
+
/** Runs on confirm. Return `{ ok: false, error }` or throw to keep the step open with a message. */
|
|
12
|
+
onConfirm: () => Promise<ConfirmResult> | Promise<void>;
|
|
13
|
+
triggerVariant?: ButtonVariant;
|
|
14
|
+
className?: string;
|
|
15
|
+
}
|
|
16
|
+
/** Inline two-step confirmation for a destructive action; no modal, no lost context. */
|
|
17
|
+
export declare function ConfirmAction({ label, consequence, confirmLabel, cancelLabel, onConfirm, triggerVariant, className, }: ConfirmActionProps): import("react").JSX.Element;
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
3
|
+
import { useEffect, useId, useRef, useState } from "react";
|
|
4
|
+
import { Button } from "../../atoms/button/button.js";
|
|
5
|
+
import { useUiStrings } from "../../i18n/index.js";
|
|
6
|
+
import { cn } from "../../utils/cn.js";
|
|
7
|
+
import { Banner } from "../banner/banner.js";
|
|
8
|
+
import { runConfirm } from "./model.js";
|
|
9
|
+
function ConfirmAction({
|
|
10
|
+
label,
|
|
11
|
+
consequence,
|
|
12
|
+
confirmLabel,
|
|
13
|
+
cancelLabel,
|
|
14
|
+
onConfirm,
|
|
15
|
+
triggerVariant = "secondary",
|
|
16
|
+
className
|
|
17
|
+
}) {
|
|
18
|
+
const strings = useUiStrings();
|
|
19
|
+
const consequenceId = useId();
|
|
20
|
+
const triggerRef = useRef(null);
|
|
21
|
+
const wasConfirming = useRef(false);
|
|
22
|
+
const [confirming, setConfirming] = useState(false);
|
|
23
|
+
const [busy, setBusy] = useState(false);
|
|
24
|
+
const [failure, setFailure] = useState(null);
|
|
25
|
+
useEffect(() => {
|
|
26
|
+
if (wasConfirming.current && !confirming) triggerRef.current?.focus();
|
|
27
|
+
wasConfirming.current = confirming;
|
|
28
|
+
}, [confirming]);
|
|
29
|
+
async function confirm() {
|
|
30
|
+
setBusy(true);
|
|
31
|
+
setFailure(null);
|
|
32
|
+
const outcome = await runConfirm(onConfirm, strings.actionFailed);
|
|
33
|
+
setBusy(false);
|
|
34
|
+
if (outcome.status === "failed") setFailure(outcome.message);
|
|
35
|
+
else setConfirming(false);
|
|
36
|
+
}
|
|
37
|
+
if (!confirming) {
|
|
38
|
+
return /* @__PURE__ */ jsxs("div", { className: cn("flex flex-col gap-2", className), children: [
|
|
39
|
+
failure === null ? null : /* @__PURE__ */ jsx(Banner, { tone: "danger", children: failure }),
|
|
40
|
+
/* @__PURE__ */ jsx(Button, { ref: triggerRef, variant: triggerVariant, onClick: () => setConfirming(true), children: label })
|
|
41
|
+
] });
|
|
42
|
+
}
|
|
43
|
+
return /* @__PURE__ */ jsxs(
|
|
44
|
+
"fieldset",
|
|
45
|
+
{
|
|
46
|
+
"aria-describedby": consequenceId,
|
|
47
|
+
className: cn("m-0 flex min-w-0 flex-col gap-3 border-0 p-0", className),
|
|
48
|
+
children: [
|
|
49
|
+
/* @__PURE__ */ jsx("legend", { className: "sr-only", children: label }),
|
|
50
|
+
/* @__PURE__ */ jsx("p", { id: consequenceId, className: "text-body text-ink", children: consequence }),
|
|
51
|
+
failure === null ? null : /* @__PURE__ */ jsx(Banner, { tone: "danger", children: failure }),
|
|
52
|
+
/* @__PURE__ */ jsxs("div", { className: "flex flex-wrap gap-2", children: [
|
|
53
|
+
/* @__PURE__ */ jsx(
|
|
54
|
+
Button,
|
|
55
|
+
{
|
|
56
|
+
variant: "danger",
|
|
57
|
+
loading: busy,
|
|
58
|
+
loadingLabel: strings.processing,
|
|
59
|
+
onClick: () => void confirm(),
|
|
60
|
+
children: confirmLabel ?? strings.confirm
|
|
61
|
+
}
|
|
62
|
+
),
|
|
63
|
+
/* @__PURE__ */ jsx(Button, { variant: "secondary", disabled: busy, autoFocus: true, onClick: () => setConfirming(false), children: cancelLabel ?? strings.cancel })
|
|
64
|
+
] })
|
|
65
|
+
]
|
|
66
|
+
}
|
|
67
|
+
);
|
|
68
|
+
}
|
|
69
|
+
export {
|
|
70
|
+
ConfirmAction
|
|
71
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from "./confirm-action.js";
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
export type ConfirmResult = {
|
|
2
|
+
ok: true;
|
|
3
|
+
} | {
|
|
4
|
+
ok: false;
|
|
5
|
+
error: string;
|
|
6
|
+
};
|
|
7
|
+
export type ConfirmOutcome = {
|
|
8
|
+
status: "done";
|
|
9
|
+
} | {
|
|
10
|
+
status: "failed";
|
|
11
|
+
message: string;
|
|
12
|
+
};
|
|
13
|
+
/** Runs the confirmed action and folds a returned failure or a thrown error into one outcome. */
|
|
14
|
+
export declare function runConfirm(onConfirm: () => Promise<ConfirmResult> | Promise<void>, fallbackMessage: string): Promise<ConfirmOutcome>;
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
async function runConfirm(onConfirm, fallbackMessage) {
|
|
2
|
+
try {
|
|
3
|
+
const result = await onConfirm();
|
|
4
|
+
if (result !== void 0 && !result.ok) return { status: "failed", message: result.error };
|
|
5
|
+
return { status: "done" };
|
|
6
|
+
} catch {
|
|
7
|
+
return { status: "failed", message: fallbackMessage };
|
|
8
|
+
}
|
|
9
|
+
}
|
|
10
|
+
export {
|
|
11
|
+
runConfirm
|
|
12
|
+
};
|
|
@@ -5,6 +5,7 @@ export * from "./carousel/index.js";
|
|
|
5
5
|
export * from "./cell/index.js";
|
|
6
6
|
export * from "./cols/index.js";
|
|
7
7
|
export * from "./comment/index.js";
|
|
8
|
+
export * from "./confirm-action/index.js";
|
|
8
9
|
export * from "./data-grid/index.js";
|
|
9
10
|
export * from "./description-list/index.js";
|
|
10
11
|
export * from "./facts/index.js";
|
|
@@ -18,6 +19,7 @@ export * from "./kpi/index.js";
|
|
|
18
19
|
export * from "./kpi-group/index.js";
|
|
19
20
|
export * from "./page-header/index.js";
|
|
20
21
|
export * from "./resizable/index.js";
|
|
22
|
+
export * from "./responsive-list/index.js";
|
|
21
23
|
export * from "./scrolly/index.js";
|
|
22
24
|
export * from "./settings-layout/index.js";
|
|
23
25
|
export * from "./settings-row/index.js";
|
package/dist/containers/index.js
CHANGED
|
@@ -5,6 +5,7 @@ export * from "./carousel/index.js";
|
|
|
5
5
|
export * from "./cell/index.js";
|
|
6
6
|
export * from "./cols/index.js";
|
|
7
7
|
export * from "./comment/index.js";
|
|
8
|
+
export * from "./confirm-action/index.js";
|
|
8
9
|
export * from "./data-grid/index.js";
|
|
9
10
|
export * from "./description-list/index.js";
|
|
10
11
|
export * from "./facts/index.js";
|
|
@@ -18,6 +19,7 @@ export * from "./kpi/index.js";
|
|
|
18
19
|
export * from "./kpi-group/index.js";
|
|
19
20
|
export * from "./page-header/index.js";
|
|
20
21
|
export * from "./resizable/index.js";
|
|
22
|
+
export * from "./responsive-list/index.js";
|
|
21
23
|
export * from "./scrolly/index.js";
|
|
22
24
|
export * from "./settings-layout/index.js";
|
|
23
25
|
export * from "./settings-row/index.js";
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from "./responsive-list.js";
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from "./responsive-list.js";
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import type { ReactNode } from "react";
|
|
2
|
+
export interface ResponsiveListColumn<T> {
|
|
3
|
+
key: string;
|
|
4
|
+
header: string;
|
|
5
|
+
cell: (item: T) => ReactNode;
|
|
6
|
+
/** Title of the stacked card. Defaults to the first column. */
|
|
7
|
+
primary?: boolean;
|
|
8
|
+
align?: "start" | "end";
|
|
9
|
+
}
|
|
10
|
+
export interface ResponsiveListProps<T> {
|
|
11
|
+
/** Accessible name of the table and of the card list. */
|
|
12
|
+
caption: string;
|
|
13
|
+
items: readonly T[];
|
|
14
|
+
columns: readonly ResponsiveListColumn<T>[];
|
|
15
|
+
rowKey: (item: T) => string;
|
|
16
|
+
/** Per-row actions: a last table column, and a footer row on each card. */
|
|
17
|
+
renderActions?: (item: T) => ReactNode;
|
|
18
|
+
/** Header of the actions column. Defaults to the localized "Actions". */
|
|
19
|
+
actionsHeader?: string;
|
|
20
|
+
rowClassName?: (item: T) => string | undefined;
|
|
21
|
+
className?: string;
|
|
22
|
+
}
|
|
23
|
+
/** A table from `md` up and a stack of cards below it, from one column definition. */
|
|
24
|
+
export declare function ResponsiveList<T>({ caption, items, columns, rowKey, renderActions, actionsHeader, rowClassName, className, }: ResponsiveListProps<T>): import("react").JSX.Element;
|