@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
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { AuthShellProp } from "../../props/components/layout.prop.js";
|
|
2
|
+
export type { AuthShellProp, AuthShellProp as AuthShellProps, } from "../../props/components/layout.prop.js";
|
|
3
|
+
/**
|
|
4
|
+
* AuthShell — centred auth/login page shell (login · mfa · passkey · device · reset). A top brand
|
|
5
|
+
* bar (banner), a centred `main` that holds the auth `Card`, and an optional footer (contentinfo),
|
|
6
|
+
* over a `min-h-dvh` surface. Control density is scoped to the comfortable tier (44px, the WCAG
|
|
7
|
+
* touch floor) and the auth heading is bumped up, so forms read at the right size — replacing a
|
|
8
|
+
* consumer's `.auth-shell-*` / `.ui-auth-scope` classes. Motion is delegated to `Reveal` (wrap the
|
|
9
|
+
* card) so `prefers-reduced-motion` is honoured in one place.
|
|
10
|
+
*/
|
|
11
|
+
export declare function AuthShell({ brand, footer, children, className }: AuthShellProp): import("react/jsx-runtime").JSX.Element;
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
3
|
+
import { useTranslation } from "../../i18n/use-translation.js";
|
|
4
|
+
import { cn } from "../../lib/utils.js";
|
|
5
|
+
function AuthShell({ brand, footer, children, className }) {
|
|
6
|
+
const { t } = useTranslation();
|
|
7
|
+
return /* @__PURE__ */ jsxs("div", { "data-slot": "auth-shell", className: cn("ui-auth-shell", className), children: [
|
|
8
|
+
brand !== void 0 && /* @__PURE__ */ jsx("header", { className: "ui-auth-shell-bar", "aria-label": t("layout.authShell.brandLabel"), children: brand }),
|
|
9
|
+
/* @__PURE__ */ jsx("main", { className: "ui-auth-shell-main", "aria-label": t("layout.authShell.mainLabel"), children: /* @__PURE__ */ jsx("div", { className: "ui-auth-shell-card", children }) }),
|
|
10
|
+
footer !== void 0 && /* @__PURE__ */ jsx("footer", { className: "ui-auth-shell-footer", "aria-label": t("layout.authShell.footerLabel"), children: footer })
|
|
11
|
+
] });
|
|
12
|
+
}
|
|
13
|
+
export {
|
|
14
|
+
AuthShell
|
|
15
|
+
};
|
|
@@ -5,6 +5,8 @@ export type { FlexAlignProp, FlexDirectionProp, FlexJustifyProp, FlexProp, FlexP
|
|
|
5
5
|
export { ResizablePanel, ResizablePanelGroup, ResizableHandle } from "./resizable.js";
|
|
6
6
|
export { AppShell } from "./app-shell.js";
|
|
7
7
|
export type { AppShellProps } from "./app-shell.js";
|
|
8
|
+
export { AuthShell } from "./auth-shell.js";
|
|
9
|
+
export type { AuthShellProp, AuthShellProps } from "./auth-shell.js";
|
|
8
10
|
export { Breadcrumb } from "./breadcrumb.js";
|
|
9
11
|
export type { BreadcrumbProps } from "./breadcrumb.js";
|
|
10
12
|
export { Sidebar, SidebarHeader, SidebarItem, SidebarSection } from "./sidebar.js";
|
|
@@ -2,6 +2,7 @@ import { PageContainer } from "./page-container.js";
|
|
|
2
2
|
import { Flex } from "./flex.js";
|
|
3
3
|
import { ResizablePanel, ResizablePanelGroup, ResizableHandle } from "./resizable.js";
|
|
4
4
|
import { AppShell } from "./app-shell.js";
|
|
5
|
+
import { AuthShell } from "./auth-shell.js";
|
|
5
6
|
import { Breadcrumb } from "./breadcrumb.js";
|
|
6
7
|
import { Sidebar, SidebarHeader, SidebarItem, SidebarSection } from "./sidebar.js";
|
|
7
8
|
import { Topbar } from "./topbar.js";
|
|
@@ -12,6 +13,7 @@ import { AspectRatio } from "./aspect-ratio.js";
|
|
|
12
13
|
export {
|
|
13
14
|
AppShell,
|
|
14
15
|
AspectRatio,
|
|
16
|
+
AuthShell,
|
|
15
17
|
Breadcrumb,
|
|
16
18
|
Flex,
|
|
17
19
|
PageContainer,
|
|
@@ -20,10 +20,10 @@ function useFooterReveal(enabled) {
|
|
|
20
20
|
if (!enabled) return;
|
|
21
21
|
const el = headerRef.current;
|
|
22
22
|
if (!el || typeof IntersectionObserver === "undefined") return;
|
|
23
|
-
const observer = new IntersectionObserver(
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
);
|
|
23
|
+
const observer = new IntersectionObserver(([entry]) => setRevealed(!entry.isIntersecting), {
|
|
24
|
+
root: scrollParent(el),
|
|
25
|
+
threshold: 0
|
|
26
|
+
});
|
|
27
27
|
observer.observe(el);
|
|
28
28
|
return () => observer.disconnect();
|
|
29
29
|
}, [enabled]);
|
|
@@ -63,7 +63,7 @@ const ARIA_KEY = {
|
|
|
63
63
|
};
|
|
64
64
|
const BRAND_NONE = "__app__";
|
|
65
65
|
const AppSettingPicker = React.forwardRef(
|
|
66
|
-
function AppSettingPicker2({ kind, className, disabled, id, name, value, onValueChange }, ref) {
|
|
66
|
+
function AppSettingPicker2({ kind, appearance = "labeled", className, disabled, id, name, value, onValueChange }, ref) {
|
|
67
67
|
const ctx = useOptionalAppContext();
|
|
68
68
|
const { t, locale, fallbackLocale } = useTranslation();
|
|
69
69
|
const raw = value ?? ctx?.[kind];
|
|
@@ -119,6 +119,7 @@ const AppSettingPicker = React.forwardRef(
|
|
|
119
119
|
}, [kind, ctx?.timezoneOptions, current, t, locale, fallbackLocale]);
|
|
120
120
|
const unbound = current === void 0 || !handleChange;
|
|
121
121
|
const Icon = ICON[kind];
|
|
122
|
+
const iconOnly = appearance === "icon";
|
|
122
123
|
return /* @__PURE__ */ jsxs(
|
|
123
124
|
Select,
|
|
124
125
|
{
|
|
@@ -133,11 +134,25 @@ const AppSettingPicker = React.forwardRef(
|
|
|
133
134
|
{
|
|
134
135
|
ref,
|
|
135
136
|
id,
|
|
136
|
-
className: cn(
|
|
137
|
+
className: cn(
|
|
138
|
+
iconOnly ? (
|
|
139
|
+
// Structurally icon-only: drop the owned width + value spacing, square the box to
|
|
140
|
+
// the density-aware --control-height tap target, centre the icon and hide the
|
|
141
|
+
// trailing chevron — no consumer descendant-selector overrides required.
|
|
142
|
+
"w-[length:var(--control-height)] justify-center ps-0 pe-0 [&_[data-slot=select-chevron]]:hidden"
|
|
143
|
+
) : cn("w-full", TRIGGER_WIDTH[kind]),
|
|
144
|
+
className
|
|
145
|
+
),
|
|
137
146
|
"aria-label": t(ARIA_KEY[kind]),
|
|
138
147
|
children: [
|
|
139
|
-
/* @__PURE__ */ jsx(
|
|
140
|
-
|
|
148
|
+
/* @__PURE__ */ jsx(
|
|
149
|
+
Icon,
|
|
150
|
+
{
|
|
151
|
+
className: cn("size-4 shrink-0 opacity-70", !iconOnly && "me-2"),
|
|
152
|
+
"aria-hidden": "true"
|
|
153
|
+
}
|
|
154
|
+
),
|
|
155
|
+
iconOnly ? null : /* @__PURE__ */ jsx(SelectValue, {})
|
|
141
156
|
]
|
|
142
157
|
}
|
|
143
158
|
),
|
|
@@ -22,5 +22,5 @@ declare const PaginationNext: React.ForwardRefExoticComponent<Omit<Omit<React.De
|
|
|
22
22
|
disabled?: boolean;
|
|
23
23
|
href?: string;
|
|
24
24
|
} & React.RefAttributes<HTMLButtonElement>>;
|
|
25
|
-
export declare function Pagination({ value, total, pageSize, pageSizeOptions, showSizeChanger, showTotal, simple, disabled, className, onValueChange, }: PaginationProp): import("react/jsx-runtime").JSX.Element;
|
|
25
|
+
export declare function Pagination({ value, total, pageSize, pageSizeOptions, showSizeChanger, showTotal, simple, disabled, className, onValueChange, }: PaginationProp): import("react/jsx-runtime").JSX.Element | null;
|
|
26
26
|
export { PaginationContent, PaginationItem, PaginationLink, PaginationEllipsis, PaginationPrevious, PaginationNext, };
|
|
@@ -103,9 +103,10 @@ function Pagination({
|
|
|
103
103
|
onValueChange?.(nextPage, size);
|
|
104
104
|
};
|
|
105
105
|
const totalLabel = typeof showTotal === "function" ? showTotal(total, [
|
|
106
|
-
(safeCurrent - 1) * pageSize + 1,
|
|
106
|
+
total === 0 ? 0 : (safeCurrent - 1) * pageSize + 1,
|
|
107
107
|
Math.min(safeCurrent * pageSize, total)
|
|
108
108
|
]) : showTotal ? t("navigation.pagination.total", { total }) : null;
|
|
109
|
+
if (total <= pageSize) return null;
|
|
109
110
|
if (simple) {
|
|
110
111
|
return /* @__PURE__ */ jsxs(
|
|
111
112
|
"nav",
|
|
@@ -4,4 +4,4 @@ export type { DataStateProp, DataStateProp as DataStateProps, } from "../../prop
|
|
|
4
4
|
* Query lifecycle widget — orchestrates skeleton / error / empty / success for one `useQuery` block.
|
|
5
5
|
* Not a visual component; prefer `@godxjp/ui/query`. Apps may also import via `@godxjp/ui/admin`.
|
|
6
6
|
*/
|
|
7
|
-
export declare function DataState<T>({ query, skeleton, empty, isEmpty, errorRenderer, showRetry, onRetry, children, }: DataStateProp<T>): import("react/jsx-runtime").JSX.Element;
|
|
7
|
+
export declare function DataState<T>({ query, skeleton, prerequisite, empty, isEmpty, errorRenderer, showRetry, onRetry, onAuthError, children, }: DataStateProp<T>): import("react/jsx-runtime").JSX.Element;
|
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
"use client";
|
|
2
|
-
import { Fragment, jsx } from "react/jsx-runtime";
|
|
2
|
+
import { Fragment, jsx, jsxs } from "react/jsx-runtime";
|
|
3
3
|
import * as React from "react";
|
|
4
4
|
import { AlertQueryError } from "../feedback/alert.js";
|
|
5
|
+
import { useTranslation } from "../../i18n/use-translation.js";
|
|
6
|
+
import { classifyQueryError } from "../../lib/query-error.js";
|
|
5
7
|
function defaultIsEmpty(data) {
|
|
6
8
|
if (!data) return true;
|
|
7
9
|
if (Array.isArray(data)) return data.length === 0;
|
|
@@ -15,13 +17,16 @@ function defaultIsEmpty(data) {
|
|
|
15
17
|
function DataState({
|
|
16
18
|
query,
|
|
17
19
|
skeleton,
|
|
20
|
+
prerequisite = null,
|
|
18
21
|
empty,
|
|
19
22
|
isEmpty = defaultIsEmpty,
|
|
20
23
|
errorRenderer,
|
|
21
|
-
showRetry =
|
|
24
|
+
showRetry = false,
|
|
22
25
|
onRetry,
|
|
26
|
+
onAuthError,
|
|
23
27
|
children
|
|
24
28
|
}) {
|
|
29
|
+
const { t } = useTranslation();
|
|
25
30
|
const retry = React.useCallback(() => {
|
|
26
31
|
if (onRetry) {
|
|
27
32
|
void onRetry();
|
|
@@ -29,16 +34,34 @@ function DataState({
|
|
|
29
34
|
}
|
|
30
35
|
void query.refetch();
|
|
31
36
|
}, [onRetry, query]);
|
|
37
|
+
if (query.isPending && query.fetchStatus === "idle") return /* @__PURE__ */ jsx(Fragment, { children: prerequisite });
|
|
32
38
|
if (query.isPending) return /* @__PURE__ */ jsx(Fragment, { children: skeleton });
|
|
33
39
|
if (query.isError) {
|
|
34
40
|
if (query.isFetching) return /* @__PURE__ */ jsx(Fragment, { children: skeleton });
|
|
35
41
|
if (errorRenderer) return /* @__PURE__ */ jsx(Fragment, { children: errorRenderer(query.error, retry) });
|
|
36
|
-
|
|
42
|
+
const info = classifyQueryError(query.error);
|
|
43
|
+
const canRetry = (info.category === "transient" || info.category === "unknown") && (info.retryable || showRetry || Boolean(onRetry));
|
|
44
|
+
return /* @__PURE__ */ jsx(
|
|
45
|
+
AlertQueryError,
|
|
46
|
+
{
|
|
47
|
+
error: query.error,
|
|
48
|
+
category: info.category,
|
|
49
|
+
onRetry: canRetry ? retry : void 0,
|
|
50
|
+
onAuthAction: info.category === "auth" ? onAuthError : void 0
|
|
51
|
+
}
|
|
52
|
+
);
|
|
37
53
|
}
|
|
38
54
|
const data = query.data;
|
|
39
55
|
if (data === void 0) return /* @__PURE__ */ jsx(Fragment, { children: skeleton });
|
|
40
56
|
if (empty && (data === null || isEmpty(data))) return /* @__PURE__ */ jsx(Fragment, { children: empty });
|
|
41
|
-
|
|
57
|
+
const content = children(data);
|
|
58
|
+
if (query.isFetching) {
|
|
59
|
+
return /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
60
|
+
content,
|
|
61
|
+
/* @__PURE__ */ jsx("span", { role: "status", "aria-live": "polite", className: "sr-only", children: t("query.refreshing") })
|
|
62
|
+
] });
|
|
63
|
+
}
|
|
64
|
+
return /* @__PURE__ */ jsx(Fragment, { children: content });
|
|
42
65
|
}
|
|
43
66
|
export {
|
|
44
67
|
DataState
|
|
@@ -8,3 +8,5 @@ export { InfiniteQueryState, flattenItemPages } from "./infinite-query-state.js"
|
|
|
8
8
|
export type { InfiniteQueryStateProp, InfiniteQueryStateProps } from "./infinite-query-state.js";
|
|
9
9
|
export { PrefetchLink } from "./prefetch-link.js";
|
|
10
10
|
export type { PrefetchLinkProp, PrefetchLinkProps } from "./prefetch-link.js";
|
|
11
|
+
export { classifyQueryError, isRetryableQueryError } from "../../lib/query-error.js";
|
|
12
|
+
export type { QueryErrorCategory, QueryErrorInfo } from "../../lib/query-error.js";
|
|
@@ -3,11 +3,14 @@ import { AlertMutationFeedback } from "./mutation-feedback.js";
|
|
|
3
3
|
import { ButtonRefetch } from "./query-refetch-button.js";
|
|
4
4
|
import { InfiniteQueryState, flattenItemPages } from "./infinite-query-state.js";
|
|
5
5
|
import { PrefetchLink } from "./prefetch-link.js";
|
|
6
|
+
import { classifyQueryError, isRetryableQueryError } from "../../lib/query-error.js";
|
|
6
7
|
export {
|
|
7
8
|
AlertMutationFeedback,
|
|
8
9
|
ButtonRefetch,
|
|
9
10
|
DataState,
|
|
10
11
|
InfiniteQueryState,
|
|
11
12
|
PrefetchLink,
|
|
12
|
-
|
|
13
|
+
classifyQueryError,
|
|
14
|
+
flattenItemPages,
|
|
15
|
+
isRetryableQueryError
|
|
13
16
|
};
|
|
@@ -102,7 +102,8 @@
|
|
|
102
102
|
"search": "Search",
|
|
103
103
|
"clear": "Clear selection",
|
|
104
104
|
"loading": "Loading…",
|
|
105
|
-
"empty": "No results"
|
|
105
|
+
"empty": "No results",
|
|
106
|
+
"error": "Couldn’t load options"
|
|
106
107
|
},
|
|
107
108
|
"monthPicker": {
|
|
108
109
|
"openGrid": "Open month grid",
|
|
@@ -130,6 +131,11 @@
|
|
|
130
131
|
"mainLabel": "Main content",
|
|
131
132
|
"footerLabel": "Footer"
|
|
132
133
|
},
|
|
134
|
+
"authShell": {
|
|
135
|
+
"brandLabel": "Brand",
|
|
136
|
+
"mainLabel": "Main content",
|
|
137
|
+
"footerLabel": "Footer"
|
|
138
|
+
},
|
|
133
139
|
"topbar": {
|
|
134
140
|
"toggleSidebar": "Toggle sidebar",
|
|
135
141
|
"search": "Search",
|
|
@@ -300,7 +306,29 @@
|
|
|
300
306
|
"ASSIGNMENT_STATUS_TERMINATED": "Terminated"
|
|
301
307
|
},
|
|
302
308
|
"query": {
|
|
303
|
-
"loadMore": "Load more"
|
|
309
|
+
"loadMore": "Load more",
|
|
310
|
+
"refreshing": "Updating…",
|
|
311
|
+
"error": {
|
|
312
|
+
"title": {
|
|
313
|
+
"auth": "Session expired",
|
|
314
|
+
"forbidden": "Access denied",
|
|
315
|
+
"notFound": "Not found",
|
|
316
|
+
"validation": "Check your input",
|
|
317
|
+
"transient": "Something went wrong",
|
|
318
|
+
"unknown": "Something went wrong"
|
|
319
|
+
},
|
|
320
|
+
"description": {
|
|
321
|
+
"auth": "Your session has ended. Sign in again to continue.",
|
|
322
|
+
"forbidden": "You don't have permission to view this. Contact an administrator if you need access.",
|
|
323
|
+
"notFound": "We couldn't find what you were looking for. It may have been moved or removed.",
|
|
324
|
+
"validation": "The request couldn't be completed. Review your input and try again.",
|
|
325
|
+
"transient": "A temporary problem occurred. Please try again in a moment.",
|
|
326
|
+
"unknown": "An unexpected error occurred. Please try again later."
|
|
327
|
+
},
|
|
328
|
+
"action": {
|
|
329
|
+
"signIn": "Sign in again"
|
|
330
|
+
}
|
|
331
|
+
}
|
|
304
332
|
},
|
|
305
333
|
"chart": {
|
|
306
334
|
"empty": "No data to display",
|
|
@@ -102,7 +102,8 @@
|
|
|
102
102
|
"search": "検索",
|
|
103
103
|
"clear": "選択をクリア",
|
|
104
104
|
"loading": "読み込み中…",
|
|
105
|
-
"empty": "該当なし"
|
|
105
|
+
"empty": "該当なし",
|
|
106
|
+
"error": "選択肢を読み込めませんでした"
|
|
106
107
|
},
|
|
107
108
|
"monthPicker": {
|
|
108
109
|
"openGrid": "月選択を開く",
|
|
@@ -130,6 +131,11 @@
|
|
|
130
131
|
"mainLabel": "メインコンテンツ",
|
|
131
132
|
"footerLabel": "フッター"
|
|
132
133
|
},
|
|
134
|
+
"authShell": {
|
|
135
|
+
"brandLabel": "ブランド",
|
|
136
|
+
"mainLabel": "メインコンテンツ",
|
|
137
|
+
"footerLabel": "フッター"
|
|
138
|
+
},
|
|
133
139
|
"topbar": {
|
|
134
140
|
"toggleSidebar": "サイドバーの切り替え",
|
|
135
141
|
"search": "検索",
|
|
@@ -297,7 +303,29 @@
|
|
|
297
303
|
"ASSIGNMENT_STATUS_TERMINATED": "終了"
|
|
298
304
|
},
|
|
299
305
|
"query": {
|
|
300
|
-
"loadMore": "さらに読み込む"
|
|
306
|
+
"loadMore": "さらに読み込む",
|
|
307
|
+
"refreshing": "更新中…",
|
|
308
|
+
"error": {
|
|
309
|
+
"title": {
|
|
310
|
+
"auth": "セッションの有効期限が切れました",
|
|
311
|
+
"forbidden": "アクセスできません",
|
|
312
|
+
"notFound": "見つかりません",
|
|
313
|
+
"validation": "入力内容をご確認ください",
|
|
314
|
+
"transient": "問題が発生しました",
|
|
315
|
+
"unknown": "問題が発生しました"
|
|
316
|
+
},
|
|
317
|
+
"description": {
|
|
318
|
+
"auth": "セッションが終了しました。続行するには再度サインインしてください。",
|
|
319
|
+
"forbidden": "この内容を表示する権限がありません。アクセスが必要な場合は管理者にお問い合わせください。",
|
|
320
|
+
"notFound": "お探しの内容が見つかりませんでした。移動または削除された可能性があります。",
|
|
321
|
+
"validation": "リクエストを完了できませんでした。入力内容を確認してもう一度お試しください。",
|
|
322
|
+
"transient": "一時的な問題が発生しました。しばらくしてからもう一度お試しください。",
|
|
323
|
+
"unknown": "予期しないエラーが発生しました。しばらくしてからもう一度お試しください。"
|
|
324
|
+
},
|
|
325
|
+
"action": {
|
|
326
|
+
"signIn": "再度サインイン"
|
|
327
|
+
}
|
|
328
|
+
}
|
|
301
329
|
},
|
|
302
330
|
"chart": {
|
|
303
331
|
"empty": "データがありません",
|
|
@@ -102,7 +102,8 @@
|
|
|
102
102
|
"search": "Tìm kiếm",
|
|
103
103
|
"clear": "Xóa lựa chọn",
|
|
104
104
|
"loading": "Đang tải…",
|
|
105
|
-
"empty": "Không có kết quả"
|
|
105
|
+
"empty": "Không có kết quả",
|
|
106
|
+
"error": "Không tải được lựa chọn"
|
|
106
107
|
},
|
|
107
108
|
"monthPicker": {
|
|
108
109
|
"openGrid": "Mở chọn tháng",
|
|
@@ -130,6 +131,11 @@
|
|
|
130
131
|
"mainLabel": "Nội dung chính",
|
|
131
132
|
"footerLabel": "Chân trang"
|
|
132
133
|
},
|
|
134
|
+
"authShell": {
|
|
135
|
+
"brandLabel": "Thương hiệu",
|
|
136
|
+
"mainLabel": "Nội dung chính",
|
|
137
|
+
"footerLabel": "Chân trang"
|
|
138
|
+
},
|
|
133
139
|
"topbar": {
|
|
134
140
|
"toggleSidebar": "Bật/tắt thanh bên",
|
|
135
141
|
"search": "Tìm kiếm",
|
|
@@ -297,7 +303,29 @@
|
|
|
297
303
|
"ASSIGNMENT_STATUS_TERMINATED": "Chấm dứt"
|
|
298
304
|
},
|
|
299
305
|
"query": {
|
|
300
|
-
"loadMore": "Tải thêm"
|
|
306
|
+
"loadMore": "Tải thêm",
|
|
307
|
+
"refreshing": "Đang cập nhật…",
|
|
308
|
+
"error": {
|
|
309
|
+
"title": {
|
|
310
|
+
"auth": "Phiên đăng nhập đã hết hạn",
|
|
311
|
+
"forbidden": "Không có quyền truy cập",
|
|
312
|
+
"notFound": "Không tìm thấy",
|
|
313
|
+
"validation": "Kiểm tra lại dữ liệu",
|
|
314
|
+
"transient": "Đã xảy ra lỗi",
|
|
315
|
+
"unknown": "Đã xảy ra lỗi"
|
|
316
|
+
},
|
|
317
|
+
"description": {
|
|
318
|
+
"auth": "Phiên của bạn đã kết thúc. Vui lòng đăng nhập lại để tiếp tục.",
|
|
319
|
+
"forbidden": "Bạn không có quyền xem nội dung này. Liên hệ quản trị viên nếu cần quyền truy cập.",
|
|
320
|
+
"notFound": "Không tìm thấy nội dung bạn cần. Có thể nội dung đã được di chuyển hoặc xóa.",
|
|
321
|
+
"validation": "Không thể hoàn tất yêu cầu. Vui lòng kiểm tra lại dữ liệu và thử lại.",
|
|
322
|
+
"transient": "Đã xảy ra sự cố tạm thời. Vui lòng thử lại sau giây lát.",
|
|
323
|
+
"unknown": "Đã xảy ra lỗi không mong muốn. Vui lòng thử lại sau."
|
|
324
|
+
},
|
|
325
|
+
"action": {
|
|
326
|
+
"signIn": "Đăng nhập lại"
|
|
327
|
+
}
|
|
328
|
+
}
|
|
301
329
|
},
|
|
302
330
|
"chart": {
|
|
303
331
|
"empty": "Không có dữ liệu",
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/** Recovery-relevant error classes. Retry is only meaningful for `transient`. */
|
|
2
|
+
export type QueryErrorCategory = "auth" | "forbidden" | "notFound" | "validation" | "transient" | "unknown";
|
|
3
|
+
export type QueryErrorInfo = {
|
|
4
|
+
category: QueryErrorCategory;
|
|
5
|
+
/** HTTP status when one could be derived from the error, else `undefined`. */
|
|
6
|
+
status?: number;
|
|
7
|
+
/** `true` only when repeating the same request can plausibly resolve the cause. */
|
|
8
|
+
retryable: boolean;
|
|
9
|
+
};
|
|
10
|
+
/**
|
|
11
|
+
* Classify a query/API error into a recovery-relevant category. Status codes win; message
|
|
12
|
+
* keywords are the fallback for transport layers that only surface a string (e.g. an SDK that
|
|
13
|
+
* throws `new Error("Access token invalid")` with no status). Never exposes the raw message.
|
|
14
|
+
*/
|
|
15
|
+
export declare function classifyQueryError(error: unknown): QueryErrorInfo;
|
|
16
|
+
/** `true` when repeating the same request can plausibly resolve the error. */
|
|
17
|
+
export declare function isRetryableQueryError(error: unknown): boolean;
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
function readStatus(error) {
|
|
2
|
+
if (error && typeof error === "object") {
|
|
3
|
+
const e = error;
|
|
4
|
+
for (const value of [e.status, e.statusCode, e.code]) {
|
|
5
|
+
if (typeof value === "number" && value >= 100 && value <= 599) return value;
|
|
6
|
+
}
|
|
7
|
+
const response = e.response;
|
|
8
|
+
if (response && typeof response.status === "number") return response.status;
|
|
9
|
+
}
|
|
10
|
+
const match = readMessage(error).match(/\b([1-5]\d{2})\b/);
|
|
11
|
+
if (match) {
|
|
12
|
+
const parsed = Number(match[1]);
|
|
13
|
+
if (parsed >= 400 && parsed <= 599) return parsed;
|
|
14
|
+
}
|
|
15
|
+
return void 0;
|
|
16
|
+
}
|
|
17
|
+
function readMessage(error) {
|
|
18
|
+
if (typeof error === "string") return error;
|
|
19
|
+
if (error instanceof Error) return error.message;
|
|
20
|
+
if (error && typeof error === "object" && typeof error.message === "string") {
|
|
21
|
+
return error.message;
|
|
22
|
+
}
|
|
23
|
+
return "";
|
|
24
|
+
}
|
|
25
|
+
const AUTH_RE = /unauthenticated|unauthorized|access[ _-]?token|invalid token|token (?:is )?(?:expired|invalid)|(?:session|token) (?:has )?expired/i;
|
|
26
|
+
const FORBIDDEN_RE = /forbidden|permission denied|not allowed|access denied|insufficient (?:scope|permission)/i;
|
|
27
|
+
const NOT_FOUND_RE = /not found|does not exist|no such/i;
|
|
28
|
+
const TRANSIENT_RE = /network|failed to fetch|timeout|timed out|connection|econn(?:refused|reset|aborted)|temporarily|unavailable|gateway/i;
|
|
29
|
+
const VALIDATION_RE = /validation|invalid input|bad request|must be|is required/i;
|
|
30
|
+
function classifyQueryError(error) {
|
|
31
|
+
const status = readStatus(error);
|
|
32
|
+
const message = readMessage(error);
|
|
33
|
+
const of = (category, retryable) => ({
|
|
34
|
+
category,
|
|
35
|
+
status,
|
|
36
|
+
retryable
|
|
37
|
+
});
|
|
38
|
+
if (status !== void 0) {
|
|
39
|
+
if (status === 401) return of("auth", false);
|
|
40
|
+
if (status === 403) return of("forbidden", false);
|
|
41
|
+
if (status === 404) return of("notFound", false);
|
|
42
|
+
if (status === 408 || status === 429 || status >= 500) return of("transient", true);
|
|
43
|
+
if (status === 400 || status === 422) return of("validation", false);
|
|
44
|
+
}
|
|
45
|
+
if (AUTH_RE.test(message)) return of("auth", false);
|
|
46
|
+
if (FORBIDDEN_RE.test(message)) return of("forbidden", false);
|
|
47
|
+
if (NOT_FOUND_RE.test(message)) return of("notFound", false);
|
|
48
|
+
if (TRANSIENT_RE.test(message)) return of("transient", true);
|
|
49
|
+
if (VALIDATION_RE.test(message)) return of("validation", false);
|
|
50
|
+
return of("unknown", false);
|
|
51
|
+
}
|
|
52
|
+
function isRetryableQueryError(error) {
|
|
53
|
+
return classifyQueryError(error).retryable;
|
|
54
|
+
}
|
|
55
|
+
export {
|
|
56
|
+
classifyQueryError,
|
|
57
|
+
isRetryableQueryError
|
|
58
|
+
};
|
|
@@ -3,7 +3,7 @@ import type { Locale } from "date-fns";
|
|
|
3
3
|
import type { DayPickerProps } from "react-day-picker";
|
|
4
4
|
import type { AppLocale, AppRequestHeaders, AppTimeFormat, AppTimezone, AppTimezoneDefault, AppDateFormat } from "../../app/types.js";
|
|
5
5
|
import type { AppBrand, AppDensity, AppFontSize, AppTheme } from "../../app/theme-axes.js";
|
|
6
|
-
import type { ChildrenProp, ClassNameProp, DisabledProp, IdProp, NameProp, OnValueChangeProp, ValueProp } from "../vocabulary/index.js";
|
|
6
|
+
import type { AppSettingPickerAppearanceProp, ChildrenProp, ClassNameProp, DisabledProp, IdProp, NameProp, OnValueChangeProp, ValueProp } from "../vocabulary/index.js";
|
|
7
7
|
/** @see AppProvider */
|
|
8
8
|
export type AppProviderProp = {
|
|
9
9
|
children: ChildrenProp;
|
|
@@ -68,6 +68,14 @@ export type AppSettingKind = "locale" | "timezone" | "dateFormat" | "timeFormat"
|
|
|
68
68
|
*/
|
|
69
69
|
export type AppSettingPickerProp = {
|
|
70
70
|
kind: AppSettingKind;
|
|
71
|
+
/**
|
|
72
|
+
* Trigger presentation. `"labeled"` (default) shows the leading icon + the selected value in a
|
|
73
|
+
* full-width control. `"icon"` renders a supported square, icon-only topbar trigger (e.g. a globe
|
|
74
|
+
* language switcher): it structurally drops the value text and the picker's owned width — no
|
|
75
|
+
* descendant-selector CSS overrides needed — while always keeping the localized `aria-label`, so
|
|
76
|
+
* an icon-only trigger can never ship without an accessible name.
|
|
77
|
+
*/
|
|
78
|
+
appearance?: AppSettingPickerAppearanceProp;
|
|
71
79
|
className?: ClassNameProp;
|
|
72
80
|
disabled?: DisabledProp;
|
|
73
81
|
id?: IdProp;
|
|
@@ -2,11 +2,26 @@
|
|
|
2
2
|
import type * as React from "react";
|
|
3
3
|
import type { ActionProp, ClassNameProp, DescriptionProp, IconProp, TitleProp, ColumnDefProp, GetRowIdProp, OnRowClickProp, OnSelectChangeProp, OnSortChangeProp, OnTableDensityChangeProp, SelectedIdsProp, SortStateProp, TableDensityProp, ChildrenProp, ToneProp } from "../vocabulary/index.js";
|
|
4
4
|
/** @see EmptyState */
|
|
5
|
+
/**
|
|
6
|
+
* Semantic intent of the EmptyState icon medallion — a subset of the shared `ToneProp` vocabulary
|
|
7
|
+
* (no `default`/`neutral`; `destructive` is the DS name for a "danger" state). Drives the
|
|
8
|
+
* `--empty-state-icon-foreground` / `--empty-state-icon-tint` role tokens.
|
|
9
|
+
*/
|
|
10
|
+
export type EmptyStateToneProp = Extract<ToneProp, "muted" | "success" | "warning" | "destructive" | "info">;
|
|
5
11
|
export type EmptyStateProp = {
|
|
6
12
|
icon?: IconProp;
|
|
7
13
|
title: TitleProp;
|
|
8
14
|
description?: DescriptionProp;
|
|
9
15
|
action?: ActionProp;
|
|
16
|
+
/** Visual weight appropriate to the empty condition. Default `page`. */
|
|
17
|
+
variant?: "page" | "section" | "compact";
|
|
18
|
+
/**
|
|
19
|
+
* Medallion colour intent. Default `muted` (the neutral placeholder look). Set `success` for a
|
|
20
|
+
* confirmation zero-state (e.g. device approved), or `warning`/`destructive`/`info` to match the
|
|
21
|
+
* condition — tints the icon foreground + medallion fill from the matching role token, so a
|
|
22
|
+
* consumer never hand-rolls a `.ui-success-state` class to recolour it.
|
|
23
|
+
*/
|
|
24
|
+
tone?: EmptyStateToneProp;
|
|
10
25
|
className?: ClassNameProp;
|
|
11
26
|
};
|
|
12
27
|
/** @see Descriptions */
|
|
@@ -320,6 +320,8 @@ export type SearchSelectProp = {
|
|
|
320
320
|
searchPlaceholder?: PlaceholderProp;
|
|
321
321
|
emptyMessage?: EmptyMessageProp;
|
|
322
322
|
loadingMessage?: string;
|
|
323
|
+
/** Message shown when an async `loadOptions` rejects — a distinct state from empty/loading. */
|
|
324
|
+
errorMessage?: string;
|
|
323
325
|
clearLabel?: string;
|
|
324
326
|
/** Show a "clear" row when a value is selected (default true). */
|
|
325
327
|
clearable?: boolean;
|
|
@@ -328,6 +330,12 @@ export type SearchSelectProp = {
|
|
|
328
330
|
name?: NameProp;
|
|
329
331
|
id?: IdProp;
|
|
330
332
|
className?: ClassNameProp;
|
|
333
|
+
"aria-label"?: string;
|
|
334
|
+
"aria-labelledby"?: string;
|
|
335
|
+
"aria-describedby"?: string;
|
|
336
|
+
"aria-errormessage"?: string;
|
|
337
|
+
"aria-invalid"?: boolean | "true" | "false";
|
|
338
|
+
"aria-required"?: boolean | "true" | "false";
|
|
331
339
|
"data-testid"?: string;
|
|
332
340
|
};
|
|
333
341
|
/**
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
/** Feedback component prop types — @see docs/COMPONENTS.md#feedback */
|
|
2
2
|
import type * as React from "react";
|
|
3
|
+
import type { QueryErrorCategory } from "../../lib/query-error.js";
|
|
3
4
|
import type { AlertVariantProp, CancelLabelProp, ChildrenProp, ClassNameProp, ConfirmLabelProp, ConfirmVariantProp, DescriptionProp, HandlerProp, IconProp, OpenProp, OnOpenChangeProp, PendingProp, ToneProp, TitleProp } from "../vocabulary/index.js";
|
|
4
5
|
/** @see AlertDialog */
|
|
5
6
|
export type AlertDialogProp = {
|
|
@@ -19,7 +20,12 @@ export type AlertDialogProp = {
|
|
|
19
20
|
/** @see Alert */
|
|
20
21
|
export type AlertQueryErrorProp = {
|
|
21
22
|
error: unknown;
|
|
23
|
+
/** Override the auto-classified cause. When omitted, the error is classified via `classifyQueryError`. */
|
|
24
|
+
category?: QueryErrorCategory;
|
|
25
|
+
/** Retry affordance — only shown for causes where retrying is meaningful (transient/network/5xx). */
|
|
22
26
|
onRetry?: HandlerProp;
|
|
27
|
+
/** Recovery for `auth` (401/expired token): renew the session or sign in again. Replaces Retry. */
|
|
28
|
+
onAuthAction?: HandlerProp;
|
|
23
29
|
className?: ClassNameProp;
|
|
24
30
|
};
|
|
25
31
|
/** @see Alert */
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/** Foundation component prop types — @see docs/COMPONENTS.md#foundation */
|
|
2
2
|
import type * as React from "react";
|
|
3
|
-
import type { AsChildProp, ButtonSizeProp, ButtonVariantProp, DisabledProp, FontWeightProp, HeadingLevelProp, OnClickProp, PendingProp, ShapeProp, TextAlignProp, TextSizeProp, TextToneProp } from "../vocabulary/index.js";
|
|
3
|
+
import type { AsChildProp, ButtonSizeProp, ButtonVariantProp, ChildrenProp, ClassNameProp, DisabledProp, FontWeightProp, HeadingLevelProp, OnClickProp, PendingProp, RevealDelayProp, ShapeProp, TextAlignProp, TextSizeProp, TextToneProp } from "../vocabulary/index.js";
|
|
4
4
|
/** @see Text — typographic primitive; replaces hand-rolled `<span className="text-[13px] …">`. */
|
|
5
5
|
export type TextProp = Omit<React.HTMLAttributes<HTMLElement>, "color"> & {
|
|
6
6
|
/** Render element. Default `span`. Covers inline/block text + description-list + code/caption. */
|
|
@@ -71,3 +71,25 @@ export type ButtonProp = React.ButtonHTMLAttributes<HTMLButtonElement> & {
|
|
|
71
71
|
*/
|
|
72
72
|
showZero?: boolean;
|
|
73
73
|
};
|
|
74
|
+
/**
|
|
75
|
+
* @see Reveal — entrance-motion primitive (staggered fade-up). Wraps content in a real element
|
|
76
|
+
* that animates in on mount reading the DS motion tokens (`--duration-slow`, `--ease-emphasized`,
|
|
77
|
+
* `--reveal-distance`), replacing hand-rolled `@keyframes` + `.app-reveal`/`.d1..d6` classes.
|
|
78
|
+
* Honours `prefers-reduced-motion` — the animation is dropped and content stays fully visible with
|
|
79
|
+
* no layout shift.
|
|
80
|
+
*/
|
|
81
|
+
export type RevealProp = React.HTMLAttributes<HTMLDivElement> & {
|
|
82
|
+
/** Child content to reveal on enter. */
|
|
83
|
+
children?: ChildrenProp;
|
|
84
|
+
/**
|
|
85
|
+
* Stagger ordinal — an INDEX into the motion ladder (`0..6`), never a raw ms. Each step adds one
|
|
86
|
+
* `--reveal-stagger-step` of delay so sibling reveals cascade. Default `0` (enter immediately).
|
|
87
|
+
*/
|
|
88
|
+
delay?: RevealDelayProp;
|
|
89
|
+
/**
|
|
90
|
+
* Merge the reveal behaviour onto the single child element (Radix `Slot`) instead of rendering a
|
|
91
|
+
* wrapper `<div>` — use when an extra box would break a grid/flex layout. Default `false`.
|
|
92
|
+
*/
|
|
93
|
+
asChild?: AsChildProp;
|
|
94
|
+
className?: ClassNameProp;
|
|
95
|
+
};
|
|
@@ -67,6 +67,23 @@ export type AppShellProp = {
|
|
|
67
67
|
footer?: ReactNode;
|
|
68
68
|
sidebarCollapsed?: boolean;
|
|
69
69
|
};
|
|
70
|
+
/**
|
|
71
|
+
* @see AuthShell — centred auth/login page shell (login · mfa · passkey · device · reset). A
|
|
72
|
+
* top brand bar, a centred `main` that holds the auth `Card`, and an optional footer, over a
|
|
73
|
+
* `min-h-dvh` surface. The shell scopes `--control-height` to the comfortable tier (44px, the WCAG
|
|
74
|
+
* touch floor) and bumps the auth heading size so forms read at the right density — replacing
|
|
75
|
+
* consumers' hand-rolled `.auth-shell-*` / `.ui-auth-scope` classes. Motion is delegated to
|
|
76
|
+
* `Reveal` (wrap the card) so `prefers-reduced-motion` is honoured at one place.
|
|
77
|
+
*/
|
|
78
|
+
export type AuthShellProp = {
|
|
79
|
+
/** Centred content — typically a single auth `<Card>` with the form. */
|
|
80
|
+
children: ReactNode;
|
|
81
|
+
/** Brand bar slot pinned to the top (e.g. a `<Logo>` / product mark). */
|
|
82
|
+
brand?: ReactNode;
|
|
83
|
+
/** Footer slot pinned to the bottom (legal links, locale switch, support). */
|
|
84
|
+
footer?: ReactNode;
|
|
85
|
+
className?: ClassNameProp;
|
|
86
|
+
};
|
|
70
87
|
/** @see Sidebar */
|
|
71
88
|
export type SidebarProductProp = {
|
|
72
89
|
name: string;
|