@godxjp/ui 16.9.4 → 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-display/progress.d.ts +9 -2
- package/dist/components/data-display/progress.js +6 -3
- 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 +4 -0
- package/dist/components/general/index.js +4 -0
- package/dist/components/general/logo.d.ts +27 -0
- package/dist/components/general/logo.js +23 -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/data-display-layout.css +18 -0
- package/dist/styles/index.css +2 -0
- package/dist/styles/layout.css +28 -0
- package/dist/styles/logo-layout.css +41 -0
- package/dist/styles/motion.css +52 -0
- package/dist/styles/shell-layout.css +42 -0
- package/dist/theme/famgia.service.css +44 -0
- package/dist/tokens/base.css +1 -0
- package/dist/tokens/components/feedback.css +4 -0
- package/dist/tokens/components/logo.css +15 -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
|
@@ -1,8 +1,12 @@
|
|
|
1
1
|
import { Button, buttonVariants } from "./button.js";
|
|
2
2
|
import { Text, Heading } from "./typography.js";
|
|
3
|
+
import { Logo } from "./logo.js";
|
|
4
|
+
import { Reveal } from "./reveal.js";
|
|
3
5
|
export {
|
|
4
6
|
Button,
|
|
5
7
|
Heading,
|
|
8
|
+
Logo,
|
|
9
|
+
Reveal,
|
|
6
10
|
Text,
|
|
7
11
|
buttonVariants
|
|
8
12
|
};
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
export type LogoSize = "xs" | "sm" | "md" | "lg";
|
|
3
|
+
export interface LogoProps extends Omit<React.HTMLAttributes<HTMLSpanElement>, "children"> {
|
|
4
|
+
/**
|
|
5
|
+
* The brand glyph — a short mark (a letter/initials, default `"g"`) or a custom node such as an
|
|
6
|
+
* inline `<svg>`. Keep it to 1–2 glyphs; the box is square and centres its content.
|
|
7
|
+
*/
|
|
8
|
+
glyph?: React.ReactNode;
|
|
9
|
+
/** Box size tier (tokenised). Default `"md"` (1.75rem). */
|
|
10
|
+
size?: LogoSize;
|
|
11
|
+
/**
|
|
12
|
+
* Accessible name for the mark. When set, the logo is exposed to assistive tech as an image with
|
|
13
|
+
* this name; when omitted the mark is decorative (`aria-hidden`) — the correct default when a
|
|
14
|
+
* readable wordmark sits beside it.
|
|
15
|
+
*/
|
|
16
|
+
label?: string;
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* Logo — the product brand-mark box: a glyph on the primary fill. Use it INSTEAD of hand-rolling a
|
|
20
|
+
* bare span with a fixed square size, a literal radius, and `bg-primary` + type utilities in shell
|
|
21
|
+
* headers, auth screens, and topbars — that repeats literal size/radius and puts type utilities on
|
|
22
|
+
* a bare span (rules #45/#46). Size, radius, and per-tier font-size are tokens; the fill reads the
|
|
23
|
+
* primary role, so a re-themed `--primary` re-tints the mark automatically. Pair with a wordmark for
|
|
24
|
+
* the full lockup (leave `label` unset so the mark stays decorative and the wordmark carries the
|
|
25
|
+
* accessible name).
|
|
26
|
+
*/
|
|
27
|
+
export declare const Logo: React.ForwardRefExoticComponent<LogoProps & React.RefAttributes<HTMLSpanElement>>;
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { jsx } from "react/jsx-runtime";
|
|
2
|
+
import * as React from "react";
|
|
3
|
+
import { cn } from "../../lib/utils.js";
|
|
4
|
+
const Logo = React.forwardRef(
|
|
5
|
+
({ glyph = "g", size = "md", label, className, ...props }, ref) => /* @__PURE__ */ jsx(
|
|
6
|
+
"span",
|
|
7
|
+
{
|
|
8
|
+
ref,
|
|
9
|
+
"data-slot": "logo",
|
|
10
|
+
"data-size": size,
|
|
11
|
+
className: cn("ui-logo", className),
|
|
12
|
+
role: label ? "img" : void 0,
|
|
13
|
+
"aria-label": label,
|
|
14
|
+
"aria-hidden": label ? void 0 : true,
|
|
15
|
+
...props,
|
|
16
|
+
children: glyph
|
|
17
|
+
}
|
|
18
|
+
)
|
|
19
|
+
);
|
|
20
|
+
Logo.displayName = "Logo";
|
|
21
|
+
export {
|
|
22
|
+
Logo
|
|
23
|
+
};
|
|
@@ -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
|
+
};
|
|
@@ -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 */
|