@godxjp/ui 16.10.0 → 17.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +30 -5
- package/dist/components/charts/chart-cartesian.js +1 -6
- package/dist/components/charts/pie-chart.js +1 -8
- package/dist/components/data-display/empty-state.d.ts +1 -1
- package/dist/components/data-display/empty-state.js +25 -7
- package/dist/components/data-entry/form-field.js +13 -4
- package/dist/components/data-entry/search-select.d.ts +1 -1
- package/dist/components/data-entry/search-select.js +97 -19
- package/dist/components/data-entry/select.js +15 -4
- package/dist/components/feedback/alert.d.ts +11 -2
- package/dist/components/feedback/alert.js +51 -9
- package/dist/components/general/button.js +2 -0
- package/dist/components/general/index.d.ts +2 -0
- package/dist/components/general/index.js +2 -0
- package/dist/components/general/reveal.d.ts +14 -0
- package/dist/components/general/reveal.js +21 -0
- package/dist/components/layout/auth-shell.d.ts +11 -0
- package/dist/components/layout/auth-shell.js +15 -0
- package/dist/components/layout/flex.js +1 -1
- package/dist/components/layout/index.d.ts +2 -0
- package/dist/components/layout/index.js +2 -0
- package/dist/components/layout/page-container.js +4 -4
- package/dist/components/navigation/app-setting-picker.js +19 -4
- package/dist/components/navigation/pagination.d.ts +1 -1
- package/dist/components/navigation/pagination.js +2 -1
- package/dist/components/query/data-state.d.ts +1 -1
- package/dist/components/query/data-state.js +27 -4
- package/dist/components/query/index.d.ts +2 -0
- package/dist/components/query/index.js +4 -1
- package/dist/i18n/messages/en.json +30 -2
- package/dist/i18n/messages/ja.json +30 -2
- package/dist/i18n/messages/vi.json +30 -2
- package/dist/lib/query-error.d.ts +17 -0
- package/dist/lib/query-error.js +58 -0
- package/dist/props/components/app.prop.d.ts +9 -1
- package/dist/props/components/data-display.prop.d.ts +15 -0
- package/dist/props/components/data-entry.prop.d.ts +8 -0
- package/dist/props/components/feedback.prop.d.ts +6 -0
- package/dist/props/components/general.prop.d.ts +23 -1
- package/dist/props/components/layout.prop.d.ts +17 -0
- package/dist/props/components/query.prop.d.ts +7 -1
- package/dist/props/registry.d.ts +31 -2
- package/dist/props/registry.js +44 -2
- package/dist/props/vocabulary/index.d.ts +1 -1
- package/dist/props/vocabulary/interaction.prop.d.ts +16 -0
- package/dist/styles/alert-layout.css +1 -2
- package/dist/styles/card-layout.css +6 -6
- package/dist/styles/index.css +1 -0
- package/dist/styles/layout.css +28 -0
- package/dist/styles/motion.css +52 -0
- package/dist/styles/shell-layout.css +42 -0
- package/dist/theme/famgia.service.css +15 -14
- package/dist/tokens/components/feedback.css +4 -0
- package/dist/tokens/components/shell.css +10 -0
- package/dist/tokens/foundation.css +8 -2
- package/package.json +7 -3
- package/scripts/visual-audit.mjs +293 -126
package/README.md
CHANGED
|
@@ -139,9 +139,9 @@ bundled fonts). To ship only what you render, import the foundation plus the
|
|
|
139
139
|
per-layer files you need (mirrors the JS subpaths — the CSS tree-shakes too):
|
|
140
140
|
|
|
141
141
|
```css
|
|
142
|
-
@import "@godxjp/ui/styles/base";
|
|
143
|
-
@import "@godxjp/ui/styles/control";
|
|
144
|
-
@import "@godxjp/ui/styles/form-layout";
|
|
142
|
+
@import "@godxjp/ui/styles/base"; /* required: tokens + tailwind + base layer */
|
|
143
|
+
@import "@godxjp/ui/styles/control"; /* Button, Input, Select, Textarea, toggles */
|
|
144
|
+
@import "@godxjp/ui/styles/form-layout"; /* FormField */
|
|
145
145
|
@import "@godxjp/ui/styles/dialog-layout"; /* Dialog */
|
|
146
146
|
/* …only the layers you use. Layer files need `base` first (they use @layer/@apply). */
|
|
147
147
|
```
|
|
@@ -158,8 +158,8 @@ one face everywhere, or per-language (no `[lang]` selectors to write):
|
|
|
158
158
|
```css
|
|
159
159
|
:root {
|
|
160
160
|
--font-sans-base: var(--my-latin), system-ui, sans-serif; /* default face */
|
|
161
|
-
--font-sans-ja: "Noto Sans JP", var(--font-sans-base);
|
|
162
|
-
--font-sans-vi: "Montserrat", var(--font-sans-base);
|
|
161
|
+
--font-sans-ja: "Noto Sans JP", var(--font-sans-base); /* lang="ja" */
|
|
162
|
+
--font-sans-vi: "Montserrat", var(--font-sans-base); /* lang="vi" */
|
|
163
163
|
/* also: --font-sans-ko, --font-sans-zh-hans, --font-sans-zh-hant */
|
|
164
164
|
}
|
|
165
165
|
```
|
|
@@ -221,6 +221,31 @@ pnpm release --ui <patch|minor|major> --mcp <…|skip> # publish lib + MCP in
|
|
|
221
221
|
| `check:mcp-orphans` | every public component HAS a catalog entry (catalog can't rot) |
|
|
222
222
|
| `check:core-isolation` | the root export pulls no foreign runtime (adapters stay on subpaths) |
|
|
223
223
|
|
|
224
|
+
### Runtime visual audit (Playwright + axe-core)
|
|
225
|
+
|
|
226
|
+
`scripts/visual-audit.mjs` drives a **real browser** over a running app and runs axe-core plus
|
|
227
|
+
computed-style heuristics (target size, OKLCH accent chroma, rendered emoji, mis-laid-out alerts) —
|
|
228
|
+
catching what the static `pnpm audit` (source regexes) can't see. Playwright + `@axe-core/playwright`
|
|
229
|
+
are **optional peers**, installed only by apps that run the audit.
|
|
230
|
+
|
|
231
|
+
```bash
|
|
232
|
+
# from a consumer, against its running dev/preview server:
|
|
233
|
+
node node_modules/@godxjp/ui/scripts/visual-audit.mjs http://localhost:5173 /invoices /settings
|
|
234
|
+
node node_modules/@godxjp/ui/scripts/visual-audit.mjs http://localhost:5173 --format json # machine-readable
|
|
235
|
+
node node_modules/@godxjp/ui/scripts/visual-audit.mjs --strict http://localhost:5173 # CI gate
|
|
236
|
+
```
|
|
237
|
+
|
|
238
|
+
**Tested peer range** (pin one of these): `playwright >=1.55 <2` (tested 1.61.1) ·
|
|
239
|
+
`@axe-core/playwright >=4.10 <5` (tested 4.12.1) · `axe-core >=4.10 <5` (tested 4.12.1). Playwright
|
|
240
|
+
1.55+ is required for the `browser.newContext()` → `context.newPage()` flow axe expects; older
|
|
241
|
+
`browser.newPage()` throws _"Please use browser.newContext()"_.
|
|
242
|
+
|
|
243
|
+
`--format json` **always** emits valid JSON — even on bootstrap failure — with a `status`
|
|
244
|
+
(`ok` · `partial` · `error`) that separates **infrastructure errors** (missing peers, page won't
|
|
245
|
+
load, axe won't inject → `errors[]`, `summary: null`/flagged) from **product findings** (`findings[]`).
|
|
246
|
+
A tool failure can therefore never be misread as "zero violations". `pnpm check:visual-audit` is the
|
|
247
|
+
CI smoke test: it serves a fixture page tripping all five rule families and asserts each one fires.
|
|
248
|
+
|
|
224
249
|
This repo ships two packages — `@godxjp/ui` (this dir) and `@godxjp/ui-mcp` (`mcp/`). They keep
|
|
225
250
|
separate version lines but release together via `pnpm release`; see DEVELOPMENT.md §6.
|
|
226
251
|
The **preview app auto-deploys to [GitHub Pages](https://godx-jp.github.io/godxjp-ui/)** on every push to `main`
|
|
@@ -17,12 +17,7 @@ import {
|
|
|
17
17
|
} from "recharts";
|
|
18
18
|
import { EmptyState } from "../data-display/empty-state.js";
|
|
19
19
|
import { useTranslation } from "../../i18n/use-translation.js";
|
|
20
|
-
import {
|
|
21
|
-
ChartFrame,
|
|
22
|
-
chartColor,
|
|
23
|
-
chartHeight,
|
|
24
|
-
useChartNumberFormat
|
|
25
|
-
} from "./chart-frame.js";
|
|
20
|
+
import { ChartFrame, chartColor, chartHeight, useChartNumberFormat } from "./chart-frame.js";
|
|
26
21
|
import { buildCartesianSummary } from "./chart-summary.js";
|
|
27
22
|
function CartesianChart({
|
|
28
23
|
kind,
|
|
@@ -1,13 +1,6 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
import { jsx, jsxs } from "react/jsx-runtime";
|
|
3
|
-
import {
|
|
4
|
-
Cell,
|
|
5
|
-
Legend,
|
|
6
|
-
Pie,
|
|
7
|
-
PieChart as RPieChart,
|
|
8
|
-
ResponsiveContainer,
|
|
9
|
-
Tooltip
|
|
10
|
-
} from "recharts";
|
|
3
|
+
import { Cell, Legend, Pie, PieChart as RPieChart, ResponsiveContainer, Tooltip } from "recharts";
|
|
11
4
|
import { EmptyState } from "../data-display/empty-state.js";
|
|
12
5
|
import { useTranslation } from "../../i18n/use-translation.js";
|
|
13
6
|
import { ChartFrame, chartColor, chartHeight, useChartNumberFormat } from "./chart-frame.js";
|
|
@@ -1,3 +1,3 @@
|
|
|
1
1
|
import type { EmptyStateProp } from "../../props/components/data-display.prop.js";
|
|
2
2
|
export type { EmptyStateProp, EmptyStateProp as EmptyStateProps, } from "../../props/components/data-display.prop.js";
|
|
3
|
-
export declare function EmptyState({ icon: Icon, title, description, action, className }: EmptyStateProp): import("react/jsx-runtime").JSX.Element;
|
|
3
|
+
export declare function EmptyState({ icon: Icon, title, description, action, variant, tone, className, }: EmptyStateProp): import("react/jsx-runtime").JSX.Element;
|
|
@@ -1,12 +1,30 @@
|
|
|
1
1
|
import { jsx, jsxs } from "react/jsx-runtime";
|
|
2
2
|
import { cn } from "../../lib/utils.js";
|
|
3
|
-
function EmptyState({
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
3
|
+
function EmptyState({
|
|
4
|
+
icon: Icon,
|
|
5
|
+
title,
|
|
6
|
+
description,
|
|
7
|
+
action,
|
|
8
|
+
variant = "page",
|
|
9
|
+
tone = "muted",
|
|
10
|
+
className
|
|
11
|
+
}) {
|
|
12
|
+
return /* @__PURE__ */ jsxs(
|
|
13
|
+
"div",
|
|
14
|
+
{
|
|
15
|
+
"data-slot": "empty-state",
|
|
16
|
+
"data-variant": variant,
|
|
17
|
+
"data-tone": tone,
|
|
18
|
+
role: "status",
|
|
19
|
+
className: cn("ui-empty-state", className),
|
|
20
|
+
children: [
|
|
21
|
+
Icon && variant !== "compact" && /* @__PURE__ */ jsx("div", { className: "ui-empty-state-icon", children: /* @__PURE__ */ jsx(Icon, { className: "text-muted-foreground size-6", "aria-hidden": "true" }) }),
|
|
22
|
+
/* @__PURE__ */ jsx("h3", { className: "ui-empty-state-title", children: title }),
|
|
23
|
+
description && /* @__PURE__ */ jsx("p", { className: "ui-empty-state-description", children: description }),
|
|
24
|
+
action && /* @__PURE__ */ jsx("div", { children: action })
|
|
25
|
+
]
|
|
26
|
+
}
|
|
27
|
+
);
|
|
10
28
|
}
|
|
11
29
|
export {
|
|
12
30
|
EmptyState
|
|
@@ -37,6 +37,9 @@ function FormField({
|
|
|
37
37
|
);
|
|
38
38
|
}
|
|
39
39
|
const childProps = React.isValidElement(children) ? children.props : void 0;
|
|
40
|
+
const mergeIds = (...values) => Array.from(new Set(values.flatMap((value) => value?.split(/\s+/).filter(Boolean) ?? []))).join(
|
|
41
|
+
" "
|
|
42
|
+
) || void 0;
|
|
40
43
|
const childWithA11y = React.isValidElement(children) ? React.cloneElement(children, {
|
|
41
44
|
// The label is associated via aria-labelledby (not <label for>): composite
|
|
42
45
|
// controls (Radio.Group, checkbox lists, range pairs) have no labelable root,
|
|
@@ -45,10 +48,16 @@ function FormField({
|
|
|
45
48
|
"aria-labelledby": childProps?.["aria-labelledby"] ?? labelId,
|
|
46
49
|
// Helper and error can coexist: helper stays on aria-describedby, the error on
|
|
47
50
|
// aria-errormessage (surfaced when aria-invalid is true).
|
|
48
|
-
"aria-describedby":
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
51
|
+
"aria-describedby": mergeIds(
|
|
52
|
+
childProps?.["aria-describedby"],
|
|
53
|
+
helperId
|
|
54
|
+
),
|
|
55
|
+
"aria-errormessage": mergeIds(
|
|
56
|
+
childProps?.["aria-errormessage"],
|
|
57
|
+
errorId
|
|
58
|
+
),
|
|
59
|
+
"aria-required": required ? true : childProps?.["aria-required"],
|
|
60
|
+
"aria-invalid": error ? true : childProps?.["aria-invalid"]
|
|
52
61
|
}) : children;
|
|
53
62
|
const style = {};
|
|
54
63
|
if (labelWidth != null)
|
|
@@ -8,4 +8,4 @@ export type { SearchSelectProp, SearchSelectProp as SearchSelectProps, SearchSel
|
|
|
8
8
|
* Custom per-option rendering via `renderOption` (Ant-Design style). Form-submittable via
|
|
9
9
|
* `name`; e2e-testable by the trigger's `data-testid` + each option's `${data-testid}-option-${value}`.
|
|
10
10
|
*/
|
|
11
|
-
export declare function SearchSelect({ value: valueProp, defaultValue, onValueChange, options: staticOptions, loadOptions, renderOption, labelRender, selectedLabel, selectedIcon, placeholder, searchPlaceholder, emptyMessage, loadingMessage, clearLabel, clearable, disabled, name, id, className, "data-testid": dataTestId, }: SearchSelectProp): import("react/jsx-runtime").JSX.Element;
|
|
11
|
+
export declare function SearchSelect({ value: valueProp, defaultValue, onValueChange, options: staticOptions, loadOptions, renderOption, labelRender, selectedLabel, selectedIcon, placeholder, searchPlaceholder, emptyMessage, loadingMessage, errorMessage, clearLabel, clearable, disabled, name, id, className, "data-testid": dataTestId, "aria-label": ariaLabel, "aria-labelledby": ariaLabelledby, "aria-describedby": ariaDescribedby, "aria-errormessage": ariaErrorMessage, "aria-invalid": ariaInvalid, "aria-required": ariaRequired, }: SearchSelectProp): import("react/jsx-runtime").JSX.Element;
|
|
@@ -7,7 +7,7 @@ import { cn } from "../../lib/utils.js";
|
|
|
7
7
|
import { controlOpenRingClass } from "../../lib/control-styles.js";
|
|
8
8
|
import { Button } from "../general/button.js";
|
|
9
9
|
import { Popover, PopoverContent, PopoverTrigger } from "../data-display/popover.js";
|
|
10
|
-
import { Command, CommandGroup
|
|
10
|
+
import { Command, CommandGroup } from "./command.js";
|
|
11
11
|
import { Input } from "./input.js";
|
|
12
12
|
const DEBOUNCE_MS = 250;
|
|
13
13
|
function SearchSelect({
|
|
@@ -24,13 +24,20 @@ function SearchSelect({
|
|
|
24
24
|
searchPlaceholder,
|
|
25
25
|
emptyMessage,
|
|
26
26
|
loadingMessage,
|
|
27
|
+
errorMessage,
|
|
27
28
|
clearLabel,
|
|
28
29
|
clearable = true,
|
|
29
30
|
disabled = false,
|
|
30
31
|
name,
|
|
31
32
|
id,
|
|
32
33
|
className,
|
|
33
|
-
"data-testid": dataTestId
|
|
34
|
+
"data-testid": dataTestId,
|
|
35
|
+
"aria-label": ariaLabel,
|
|
36
|
+
"aria-labelledby": ariaLabelledby,
|
|
37
|
+
"aria-describedby": ariaDescribedby,
|
|
38
|
+
"aria-errormessage": ariaErrorMessage,
|
|
39
|
+
"aria-invalid": ariaInvalid,
|
|
40
|
+
"aria-required": ariaRequired
|
|
34
41
|
}) {
|
|
35
42
|
const { t } = useTranslation();
|
|
36
43
|
const reactId = React.useId();
|
|
@@ -43,6 +50,7 @@ function SearchSelect({
|
|
|
43
50
|
const [page, setPage] = React.useState(1);
|
|
44
51
|
const [hasMore, setHasMore] = React.useState(false);
|
|
45
52
|
const [loading, setLoading] = React.useState(false);
|
|
53
|
+
const [error, setError] = React.useState(false);
|
|
46
54
|
const [activeIndex, setActiveIndex] = React.useState(0);
|
|
47
55
|
const [picked, setPicked] = React.useState(null);
|
|
48
56
|
const [internalValue, setInternalValue] = React.useState(defaultValue ?? "");
|
|
@@ -70,12 +78,22 @@ function SearchSelect({
|
|
|
70
78
|
async (nextPage, search, append) => {
|
|
71
79
|
const ticket = ++reqId.current;
|
|
72
80
|
setLoading(true);
|
|
81
|
+
if (!append) setError(false);
|
|
73
82
|
try {
|
|
74
83
|
const result = await resolvedLoad({ query: search, page: nextPage });
|
|
75
84
|
if (ticket !== reqId.current) return;
|
|
76
85
|
setLoaded((prev) => append ? [...prev, ...result.options] : result.options);
|
|
86
|
+
if (!append) {
|
|
87
|
+
const firstEnabled = result.options.findIndex((option) => !option.disabled);
|
|
88
|
+
setActiveIndex(firstEnabled >= 0 ? firstEnabled : 0);
|
|
89
|
+
}
|
|
77
90
|
setHasMore(Boolean(result.hasMore));
|
|
78
91
|
setPage(nextPage);
|
|
92
|
+
} catch {
|
|
93
|
+
if (ticket !== reqId.current) return;
|
|
94
|
+
if (!append) setLoaded([]);
|
|
95
|
+
setHasMore(false);
|
|
96
|
+
setError(true);
|
|
79
97
|
} finally {
|
|
80
98
|
if (ticket === reqId.current) setLoading(false);
|
|
81
99
|
}
|
|
@@ -131,13 +149,36 @@ function SearchSelect({
|
|
|
131
149
|
void fetchPage(page + 1, debouncedQuery, true);
|
|
132
150
|
}
|
|
133
151
|
};
|
|
152
|
+
const moveActive = (direction) => {
|
|
153
|
+
if (flatOrdered.length === 0) return;
|
|
154
|
+
let next = activeIndex;
|
|
155
|
+
for (let checked = 0; checked < flatOrdered.length; checked += 1) {
|
|
156
|
+
next = (next + direction + flatOrdered.length) % flatOrdered.length;
|
|
157
|
+
if (!flatOrdered[next]?.disabled) {
|
|
158
|
+
setActiveIndex(next);
|
|
159
|
+
return;
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
};
|
|
163
|
+
const moveToEdge = (edge) => {
|
|
164
|
+
const indices = flatOrdered.map((_, index) => index);
|
|
165
|
+
if (edge === "end") indices.reverse();
|
|
166
|
+
const next = indices.find((index) => !flatOrdered[index]?.disabled);
|
|
167
|
+
if (next !== void 0) setActiveIndex(next);
|
|
168
|
+
};
|
|
134
169
|
const onKeyDown = (event) => {
|
|
135
170
|
if (event.key === "ArrowDown") {
|
|
136
171
|
event.preventDefault();
|
|
137
|
-
|
|
172
|
+
moveActive(1);
|
|
138
173
|
} else if (event.key === "ArrowUp") {
|
|
139
174
|
event.preventDefault();
|
|
140
|
-
|
|
175
|
+
moveActive(-1);
|
|
176
|
+
} else if (event.key === "Home") {
|
|
177
|
+
event.preventDefault();
|
|
178
|
+
moveToEdge("start");
|
|
179
|
+
} else if (event.key === "End") {
|
|
180
|
+
event.preventDefault();
|
|
181
|
+
moveToEdge("end");
|
|
141
182
|
} else if (event.key === "Enter" && flatOrdered[activeIndex]) {
|
|
142
183
|
event.preventDefault();
|
|
143
184
|
select(flatOrdered[activeIndex]);
|
|
@@ -168,6 +209,13 @@ function SearchSelect({
|
|
|
168
209
|
variant: "outline",
|
|
169
210
|
role: "combobox",
|
|
170
211
|
"aria-expanded": open,
|
|
212
|
+
"aria-controls": open ? listId : void 0,
|
|
213
|
+
"aria-label": ariaLabel,
|
|
214
|
+
"aria-labelledby": ariaLabelledby,
|
|
215
|
+
"aria-describedby": ariaDescribedby,
|
|
216
|
+
"aria-errormessage": ariaErrorMessage,
|
|
217
|
+
"aria-invalid": ariaInvalid,
|
|
218
|
+
"aria-required": ariaRequired,
|
|
171
219
|
disabled,
|
|
172
220
|
"data-testid": dataTestId,
|
|
173
221
|
className: cn(
|
|
@@ -195,11 +243,13 @@ function SearchSelect({
|
|
|
195
243
|
/* @__PURE__ */ jsx(
|
|
196
244
|
PopoverContent,
|
|
197
245
|
{
|
|
246
|
+
"aria-label": ariaLabelledby ? void 0 : ariaLabel ?? resolvedPlaceholder,
|
|
247
|
+
"aria-labelledby": ariaLabelledby,
|
|
198
248
|
align: "start",
|
|
199
249
|
sideOffset: 4,
|
|
200
250
|
collisionPadding: 12,
|
|
201
251
|
className: "flex max-h-[var(--radix-popover-content-available-height)] w-max max-w-[min(32rem,calc(100vw-1.5rem))] min-w-[var(--radix-popover-trigger-width)] flex-col p-0",
|
|
202
|
-
children: /* @__PURE__ */ jsxs(Command, { shouldFilter: false, className: "flex min-h-0 flex-col", children: [
|
|
252
|
+
children: /* @__PURE__ */ jsxs(Command, { value, shouldFilter: false, className: "flex min-h-0 flex-col", children: [
|
|
203
253
|
/* @__PURE__ */ jsx("div", { className: "border-border shrink-0 border-b", children: /* @__PURE__ */ jsx(
|
|
204
254
|
Input,
|
|
205
255
|
{
|
|
@@ -207,6 +257,7 @@ function SearchSelect({
|
|
|
207
257
|
"aria-controls": listId,
|
|
208
258
|
"aria-autocomplete": "list",
|
|
209
259
|
"aria-activedescendant": activeOptionId,
|
|
260
|
+
"aria-label": searchPlaceholder ?? t("dataEntry.searchSelect.search"),
|
|
210
261
|
value: query,
|
|
211
262
|
onChange: (event) => setQuery(event.target.value),
|
|
212
263
|
onKeyDown,
|
|
@@ -215,32 +266,35 @@ function SearchSelect({
|
|
|
215
266
|
}
|
|
216
267
|
) }),
|
|
217
268
|
/* @__PURE__ */ jsxs(
|
|
218
|
-
|
|
269
|
+
"div",
|
|
219
270
|
{
|
|
220
271
|
id: listId,
|
|
221
272
|
role: "listbox",
|
|
273
|
+
"aria-busy": loading,
|
|
222
274
|
className: "max-h-none min-h-0 flex-1 overflow-y-auto p-1",
|
|
223
275
|
onScroll,
|
|
224
276
|
children: [
|
|
225
277
|
grouped.map((group) => {
|
|
226
278
|
const rows = group.items.map(({ option, index }) => /* @__PURE__ */ jsx(
|
|
227
|
-
|
|
279
|
+
"div",
|
|
228
280
|
{
|
|
229
281
|
id: optionDomId(option.value),
|
|
230
282
|
role: "option",
|
|
231
|
-
value: option.value,
|
|
232
283
|
"data-testid": optionTestId(option.value),
|
|
233
|
-
"aria-selected":
|
|
234
|
-
disabled: option.disabled,
|
|
284
|
+
"aria-selected": value === option.value,
|
|
285
|
+
"aria-disabled": option.disabled || void 0,
|
|
286
|
+
"data-disabled": option.disabled || void 0,
|
|
235
287
|
className: cn(
|
|
288
|
+
"ui-command-item",
|
|
236
289
|
// Selected = persistent bg-accent + medium weight (NO check icon — saves width),
|
|
237
290
|
// matching the plain SelectItem's `data-[state=checked]` convention; active =
|
|
238
291
|
// hover/keyboard accent. Same bg so selection stays coherent across both Selects.
|
|
239
292
|
value === option.value && "bg-accent text-foreground font-medium",
|
|
240
293
|
activeIndex === index && "bg-accent text-accent-foreground"
|
|
241
294
|
),
|
|
242
|
-
onMouseEnter: () => setActiveIndex(index),
|
|
243
|
-
|
|
295
|
+
onMouseEnter: () => !option.disabled && setActiveIndex(index),
|
|
296
|
+
onMouseDown: (event) => event.preventDefault(),
|
|
297
|
+
onClick: () => select(option),
|
|
244
298
|
children: renderOption ? /* @__PURE__ */ jsx("div", { className: "min-w-0 flex-1", children: renderOption(option) }) : /* @__PURE__ */ jsxs("div", { className: "flex min-w-0 flex-1 items-center gap-2", children: [
|
|
245
299
|
option.icon ? /* @__PURE__ */ jsx("span", { className: "flex shrink-0 items-center", "aria-hidden": "true", children: option.icon }) : null,
|
|
246
300
|
/* @__PURE__ */ jsxs("div", { className: "flex min-w-0 flex-col", children: [
|
|
@@ -253,11 +307,35 @@ function SearchSelect({
|
|
|
253
307
|
));
|
|
254
308
|
return group.heading ? /* @__PURE__ */ jsx(CommandGroup, { heading: group.heading, children: rows }, group.heading) : /* @__PURE__ */ jsx(React.Fragment, { children: rows }, "__ungrouped");
|
|
255
309
|
}),
|
|
256
|
-
loading ? /* @__PURE__ */ jsxs(
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
310
|
+
loading ? /* @__PURE__ */ jsxs(
|
|
311
|
+
"div",
|
|
312
|
+
{
|
|
313
|
+
role: "status",
|
|
314
|
+
className: "text-muted-foreground flex items-center gap-2 px-2 py-3 text-sm",
|
|
315
|
+
children: [
|
|
316
|
+
/* @__PURE__ */ jsx(Loader2, { className: "size-4 animate-spin", "aria-hidden": "true" }),
|
|
317
|
+
loadingMessage ?? t("dataEntry.searchSelect.loading")
|
|
318
|
+
]
|
|
319
|
+
}
|
|
320
|
+
) : error ? /* @__PURE__ */ jsx(
|
|
321
|
+
"div",
|
|
322
|
+
{
|
|
323
|
+
role: "option",
|
|
324
|
+
"aria-disabled": "true",
|
|
325
|
+
"aria-selected": false,
|
|
326
|
+
className: "text-destructive px-2 py-6 text-center text-sm",
|
|
327
|
+
children: errorMessage ?? t("dataEntry.searchSelect.error")
|
|
328
|
+
}
|
|
329
|
+
) : loaded.length === 0 ? /* @__PURE__ */ jsx(
|
|
330
|
+
"div",
|
|
331
|
+
{
|
|
332
|
+
role: "option",
|
|
333
|
+
"aria-disabled": "true",
|
|
334
|
+
"aria-selected": false,
|
|
335
|
+
className: "text-muted-foreground px-2 py-6 text-center text-sm",
|
|
336
|
+
children: emptyMessage ?? t("dataEntry.searchSelect.empty")
|
|
337
|
+
}
|
|
338
|
+
) : null
|
|
261
339
|
]
|
|
262
340
|
}
|
|
263
341
|
)
|
|
@@ -267,13 +345,13 @@ function SearchSelect({
|
|
|
267
345
|
]
|
|
268
346
|
}
|
|
269
347
|
),
|
|
270
|
-
/* @__PURE__ */ jsx("div", { className: "pointer-events-none absolute inset-y-0 end-
|
|
348
|
+
/* @__PURE__ */ jsx("div", { className: "pointer-events-none absolute inset-y-0 end-2 flex items-center", children: showClear ? /* @__PURE__ */ jsx(
|
|
271
349
|
"button",
|
|
272
350
|
{
|
|
273
351
|
type: "button",
|
|
274
352
|
"aria-label": clearLabel ?? t("dataEntry.searchSelect.clear"),
|
|
275
353
|
"data-testid": optionTestId("clear"),
|
|
276
|
-
className: "pointer-events-auto flex size-
|
|
354
|
+
className: "pointer-events-auto flex size-6 items-center justify-center rounded-sm opacity-50 hover:opacity-100 focus-visible:opacity-100",
|
|
277
355
|
onClick: clear,
|
|
278
356
|
children: /* @__PURE__ */ jsx(X, { className: "size-4", "aria-hidden": "true" })
|
|
279
357
|
}
|
|
@@ -35,7 +35,14 @@ const SelectTrigger = React.forwardRef(({ className, children, size = "md", ...p
|
|
|
35
35
|
...props,
|
|
36
36
|
children: [
|
|
37
37
|
children,
|
|
38
|
-
/* @__PURE__ */ jsx(SelectPrimitive.Icon, { asChild: true, children: /* @__PURE__ */ jsx(
|
|
38
|
+
/* @__PURE__ */ jsx(SelectPrimitive.Icon, { asChild: true, children: /* @__PURE__ */ jsx(
|
|
39
|
+
ChevronDown,
|
|
40
|
+
{
|
|
41
|
+
"data-slot": "select-chevron",
|
|
42
|
+
className: "size-4 shrink-0 opacity-50",
|
|
43
|
+
"aria-hidden": "true"
|
|
44
|
+
}
|
|
45
|
+
) })
|
|
39
46
|
]
|
|
40
47
|
}
|
|
41
48
|
));
|
|
@@ -154,6 +161,7 @@ function DataSelect({
|
|
|
154
161
|
searchPlaceholder,
|
|
155
162
|
emptyMessage,
|
|
156
163
|
loadingMessage,
|
|
164
|
+
errorMessage,
|
|
157
165
|
clearLabel,
|
|
158
166
|
clearable,
|
|
159
167
|
disabled,
|
|
@@ -167,6 +175,7 @@ function DataSelect({
|
|
|
167
175
|
Object.entries(rest).filter(([key]) => key.startsWith("aria-"))
|
|
168
176
|
);
|
|
169
177
|
const searchable = showSearch ?? Boolean(loadOptions);
|
|
178
|
+
const hasOptions = options.length > 0;
|
|
170
179
|
if (searchable) {
|
|
171
180
|
return /* @__PURE__ */ jsx(
|
|
172
181
|
SearchSelect,
|
|
@@ -184,13 +193,15 @@ function DataSelect({
|
|
|
184
193
|
searchPlaceholder,
|
|
185
194
|
emptyMessage,
|
|
186
195
|
loadingMessage,
|
|
196
|
+
errorMessage,
|
|
187
197
|
clearLabel,
|
|
188
198
|
clearable,
|
|
189
|
-
disabled,
|
|
199
|
+
disabled: disabled || !loadOptions && !hasOptions,
|
|
190
200
|
name,
|
|
191
201
|
id,
|
|
192
202
|
className,
|
|
193
|
-
"data-testid": dataTestId
|
|
203
|
+
"data-testid": dataTestId,
|
|
204
|
+
...ariaProps
|
|
194
205
|
}
|
|
195
206
|
);
|
|
196
207
|
}
|
|
@@ -216,7 +227,7 @@ function DataSelect({
|
|
|
216
227
|
next,
|
|
217
228
|
options.find((option) => option.value === next)
|
|
218
229
|
),
|
|
219
|
-
disabled,
|
|
230
|
+
disabled: disabled || !hasOptions,
|
|
220
231
|
name,
|
|
221
232
|
children: [
|
|
222
233
|
/* @__PURE__ */ jsx(SelectTrigger, { id, "data-testid": dataTestId, className, ...ariaProps, children: /* @__PURE__ */ jsx(SelectValue, { placeholder }) }),
|
|
@@ -26,8 +26,17 @@ export declare const AlertActions: React.ForwardRefExoticComponent<React.HTMLAtt
|
|
|
26
26
|
className?: import("../../props/index.js").ClassNameProp;
|
|
27
27
|
children?: import("../../props/index.js").ChildrenProp;
|
|
28
28
|
} & React.RefAttributes<HTMLDivElement>>;
|
|
29
|
-
/**
|
|
30
|
-
|
|
29
|
+
/**
|
|
30
|
+
* TanStack Query / API failure preset used by `DataState` (@godxjp/ui/query).
|
|
31
|
+
*
|
|
32
|
+
* - **Cause-aware mode** (pass `category`, as `DataState` does): presents a safe, localized message
|
|
33
|
+
* — never the raw backend/token/stack text — with a cause-appropriate action. `auth`
|
|
34
|
+
* (401/expired) offers session renewal (`onAuthAction`) instead of Retry; transient/network/5xx
|
|
35
|
+
* offer Retry (`onRetry`); permission/not-found/validation offer neither by default.
|
|
36
|
+
* - **Legacy mode** (no `category`, e.g. mutation/infinite feedback): shows the cleaned domain
|
|
37
|
+
* message (`humanError`) + optional Retry — form-submit corrective guidance stays visible.
|
|
38
|
+
*/
|
|
39
|
+
export declare function AlertQueryError({ error, category, onRetry, onAuthAction, className, }: AlertQueryErrorProp): import("react/jsx-runtime").JSX.Element;
|
|
31
40
|
export declare const Alert: React.ForwardRefExoticComponent<React.HTMLAttributes<HTMLDivElement> & {
|
|
32
41
|
variant?: import("../../props/index.js").AlertVariantProp;
|
|
33
42
|
tone?: ToneProp;
|
|
@@ -5,6 +5,7 @@ import {
|
|
|
5
5
|
AlertCircle,
|
|
6
6
|
CheckCircle2,
|
|
7
7
|
Info,
|
|
8
|
+
LogIn,
|
|
8
9
|
RefreshCw,
|
|
9
10
|
TriangleAlert,
|
|
10
11
|
X
|
|
@@ -89,25 +90,66 @@ const AlertActions = React.forwardRef(
|
|
|
89
90
|
({ className, ...props }, ref) => /* @__PURE__ */ jsx("div", { ref, "data-slot": "alert-actions", className, ...props })
|
|
90
91
|
);
|
|
91
92
|
AlertActions.displayName = "AlertActions";
|
|
92
|
-
|
|
93
|
+
const WARNING_CATEGORIES = /* @__PURE__ */ new Set([
|
|
94
|
+
"forbidden",
|
|
95
|
+
"notFound",
|
|
96
|
+
"validation"
|
|
97
|
+
]);
|
|
98
|
+
const RETRYABLE_CATEGORIES = /* @__PURE__ */ new Set([
|
|
99
|
+
"transient",
|
|
100
|
+
"unknown"
|
|
101
|
+
]);
|
|
102
|
+
function RetryButton({ onRetry }) {
|
|
93
103
|
const { t } = useTranslation();
|
|
94
|
-
return /* @__PURE__ */
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
104
|
+
return /* @__PURE__ */ jsx(AlertActions, { children: /* @__PURE__ */ jsx(
|
|
105
|
+
Button,
|
|
106
|
+
{
|
|
107
|
+
variant: "outline",
|
|
108
|
+
size: "sm",
|
|
109
|
+
onClick: () => {
|
|
110
|
+
void onRetry();
|
|
111
|
+
},
|
|
112
|
+
children: /* @__PURE__ */ jsxs(Flex, { direction: "row", wrap: true, align: "center", gap: "xs", children: [
|
|
113
|
+
/* @__PURE__ */ jsx(RefreshCw, { className: "size-4", "aria-hidden": "true" }),
|
|
114
|
+
t("common.retry")
|
|
115
|
+
] })
|
|
116
|
+
}
|
|
117
|
+
) });
|
|
118
|
+
}
|
|
119
|
+
function AlertQueryError({
|
|
120
|
+
error,
|
|
121
|
+
category,
|
|
122
|
+
onRetry,
|
|
123
|
+
onAuthAction,
|
|
124
|
+
className
|
|
125
|
+
}) {
|
|
126
|
+
const { t } = useTranslation();
|
|
127
|
+
if (!category) {
|
|
128
|
+
return /* @__PURE__ */ jsxs(Alert, { tone: "destructive", className, children: [
|
|
129
|
+
/* @__PURE__ */ jsx(AlertTitle, { children: t("common.error") }),
|
|
130
|
+
/* @__PURE__ */ jsx(AlertDescription, { children: humanError(error) }),
|
|
131
|
+
onRetry && /* @__PURE__ */ jsx(RetryButton, { onRetry })
|
|
132
|
+
] });
|
|
133
|
+
}
|
|
134
|
+
const tone = WARNING_CATEGORIES.has(category) ? "warning" : "destructive";
|
|
135
|
+
return /* @__PURE__ */ jsxs(Alert, { tone, className, children: [
|
|
136
|
+
/* @__PURE__ */ jsx(AlertTitle, { children: t(`query.error.title.${category}`) }),
|
|
137
|
+
/* @__PURE__ */ jsx(AlertDescription, { children: t(`query.error.description.${category}`) }),
|
|
138
|
+
category === "auth" && onAuthAction && /* @__PURE__ */ jsx(AlertActions, { children: /* @__PURE__ */ jsx(
|
|
98
139
|
Button,
|
|
99
140
|
{
|
|
100
141
|
variant: "outline",
|
|
101
142
|
size: "sm",
|
|
102
143
|
onClick: () => {
|
|
103
|
-
void
|
|
144
|
+
void onAuthAction();
|
|
104
145
|
},
|
|
105
146
|
children: /* @__PURE__ */ jsxs(Flex, { direction: "row", wrap: true, align: "center", gap: "xs", children: [
|
|
106
|
-
/* @__PURE__ */ jsx(
|
|
107
|
-
t("
|
|
147
|
+
/* @__PURE__ */ jsx(LogIn, { className: "size-4", "aria-hidden": "true" }),
|
|
148
|
+
t("query.error.action.signIn")
|
|
108
149
|
] })
|
|
109
150
|
}
|
|
110
|
-
) })
|
|
151
|
+
) }),
|
|
152
|
+
RETRYABLE_CATEGORIES.has(category) && onRetry && /* @__PURE__ */ jsx(RetryButton, { onRetry })
|
|
111
153
|
] });
|
|
112
154
|
}
|
|
113
155
|
const Alert = Object.assign(AlertBase, {
|
|
@@ -62,6 +62,7 @@ const Button = React.forwardRef(
|
|
|
62
62
|
overflowCount = 99,
|
|
63
63
|
showZero = true,
|
|
64
64
|
disabled,
|
|
65
|
+
type,
|
|
65
66
|
children,
|
|
66
67
|
...props
|
|
67
68
|
}, ref) => {
|
|
@@ -96,6 +97,7 @@ const Button = React.forwardRef(
|
|
|
96
97
|
"data-loading": isLoading ? "" : void 0,
|
|
97
98
|
"aria-busy": isLoading || void 0,
|
|
98
99
|
disabled: isLoading || disabled,
|
|
100
|
+
type: asChild ? void 0 : type ?? "button",
|
|
99
101
|
className: cn(
|
|
100
102
|
"inline-flex shrink-0 items-center justify-center gap-2 text-sm font-medium whitespace-nowrap transition-all outline-none",
|
|
101
103
|
fullWidth && "w-full",
|
|
@@ -4,3 +4,5 @@ export { Text, Heading } from "./typography.js";
|
|
|
4
4
|
export type { TextProps, HeadingProps, TextProp, HeadingProp } from "./typography.js";
|
|
5
5
|
export { Logo } from "./logo.js";
|
|
6
6
|
export type { LogoProps, LogoSize } from "./logo.js";
|
|
7
|
+
export { Reveal } from "./reveal.js";
|
|
8
|
+
export type { RevealProp, RevealProps } from "./reveal.js";
|
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
import { Button, buttonVariants } from "./button.js";
|
|
2
2
|
import { Text, Heading } from "./typography.js";
|
|
3
3
|
import { Logo } from "./logo.js";
|
|
4
|
+
import { Reveal } from "./reveal.js";
|
|
4
5
|
export {
|
|
5
6
|
Button,
|
|
6
7
|
Heading,
|
|
7
8
|
Logo,
|
|
9
|
+
Reveal,
|
|
8
10
|
Text,
|
|
9
11
|
buttonVariants
|
|
10
12
|
};
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { RevealProp } from "../../props/components/general.prop.js";
|
|
2
|
+
export type { RevealProp, RevealProp as RevealProps } from "../../props/components/general.prop.js";
|
|
3
|
+
/**
|
|
4
|
+
* Reveal — the official entrance-motion primitive (staggered fade-up). Content animates in on mount
|
|
5
|
+
* reading the DS motion tokens (`--duration-slow`, `--ease-emphasized`, `--reveal-distance`), so a
|
|
6
|
+
* consumer never hand-rolls `@keyframes` + `.app-reveal`/`.d1..d6`. `delay` is a stagger ordinal
|
|
7
|
+
* (`0..6`) — an index into the `--reveal-stagger-step` ladder, never a raw ms — so a column of rows
|
|
8
|
+
* cascades in. Under `prefers-reduced-motion` the animation is dropped entirely: content renders in
|
|
9
|
+
* its final position, fully visible, with no layout shift (CSS `@media` guard in motion.css).
|
|
10
|
+
*
|
|
11
|
+
* Pure/server-safe: no hooks, no effects. Pass `asChild` to merge the reveal onto the single child
|
|
12
|
+
* element (no wrapper `<div>`) when an extra box would break a grid/flex layout.
|
|
13
|
+
*/
|
|
14
|
+
export declare function Reveal({ children, delay, asChild, className, ...props }: RevealProp): import("react/jsx-runtime").JSX.Element;
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { jsx } from "react/jsx-runtime";
|
|
2
|
+
import { cloneElement, isValidElement } from "react";
|
|
3
|
+
import { cn } from "../../lib/utils.js";
|
|
4
|
+
function Reveal({ children, delay = 0, asChild = false, className, ...props }) {
|
|
5
|
+
const shared = {
|
|
6
|
+
"data-slot": "reveal",
|
|
7
|
+
"data-reveal-delay": delay > 0 ? String(delay) : void 0
|
|
8
|
+
};
|
|
9
|
+
if (asChild && isValidElement(children)) {
|
|
10
|
+
const child = children;
|
|
11
|
+
return cloneElement(child, {
|
|
12
|
+
...shared,
|
|
13
|
+
...props,
|
|
14
|
+
className: cn("ui-reveal", child.props.className, className)
|
|
15
|
+
});
|
|
16
|
+
}
|
|
17
|
+
return /* @__PURE__ */ jsx("div", { ...shared, ...props, className: cn("ui-reveal", className), children });
|
|
18
|
+
}
|
|
19
|
+
export {
|
|
20
|
+
Reveal
|
|
21
|
+
};
|