@godxjp/ui 31.2.0 → 31.4.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/agent/START-HERE.md +6 -6
- package/agent/components/AuthExpiryProvider.json +40 -0
- package/agent/components/DataState.json +3 -1
- package/agent/components/InfiniteQueryState.json +1 -1
- package/agent/components/Tabs.json +5 -0
- package/agent/components-index.json +5 -0
- package/agent/components.json +49 -2
- package/agent/index.json +7 -7
- package/agent/llms.txt +6 -6
- package/agent/patterns/async-data-state.json +1 -1
- package/agent/patterns.json +1 -1
- package/agent/tokens.json +39 -3
- package/dist/components/admin/index.d.ts +2 -0
- package/dist/components/admin/index.js +2 -0
- package/dist/components/data-entry/upload.js +12 -1
- package/dist/components/feedback/alert.d.ts +4 -0
- package/dist/components/feedback/alert.js +29 -8
- package/dist/components/feedback/auth-expiry.d.ts +30 -0
- package/dist/components/feedback/auth-expiry.js +64 -0
- package/dist/components/feedback/index.d.ts +2 -0
- package/dist/components/feedback/index.js +2 -0
- package/dist/components/navigation/tabs.d.ts +2 -2
- package/dist/components/navigation/tabs.js +57 -10
- package/dist/components/query/data-state.js +9 -0
- package/dist/components/query/index.d.ts +2 -0
- package/dist/components/query/index.js +2 -0
- package/dist/components/query/infinite-query-state.js +9 -0
- package/dist/contracts/measurement.json +1 -1
- package/dist/i18n/messages/en.json +3 -0
- package/dist/i18n/messages/ja.json +3 -0
- package/dist/i18n/messages/vi.json +3 -0
- package/dist/props/components/feedback.prop.d.ts +13 -0
- package/dist/props/components/index.d.ts +1 -1
- package/dist/props/components/navigation.prop.d.ts +19 -1
- package/dist/props/registry.d.ts +11 -2
- package/dist/props/registry.js +13 -0
- package/dist/styles/alert-layout.css +16 -12
- package/dist/styles/control.css +18 -2
- package/dist/styles/data-entry-layout.css +4 -0
- package/dist/styles/form-layout.css +47 -5
- package/dist/styles/layers.json +35 -1
- package/dist/styles/layout.css +2 -1
- package/dist/tokens/components/feedback.css +2 -0
- package/dist/tokens/components/segmented.css +4 -0
- package/dist/tokens/semantic/layout.css +2 -0
- package/docs/FRAME-COVERAGE-REPORT.md +4 -2
- package/docs/data-entry/segmented.tsx +2 -0
- package/docs/feedback/auth-expiry.tsx +104 -0
- package/docs/i18n/messages/en.json +6 -0
- package/docs/i18n/messages/ja.json +6 -0
- package/docs/i18n/messages/vi.json +6 -0
- package/docs/navigation/tabs.tsx +43 -0
- package/docs/query/data-state.tsx +84 -12
- package/docs/showcase/table-view-tabs.tsx +15 -13
- package/docs/themes/flat.css +4 -0
- package/docs/themes/glassmorphism.css +2 -0
- package/docs/themes/neubrutalism.css +6 -0
- package/package.json +2 -2
|
@@ -12,8 +12,11 @@ import {
|
|
|
12
12
|
} from "lucide-react";
|
|
13
13
|
import { useTranslation } from "../../i18n/use-translation.js";
|
|
14
14
|
import { humanError } from "../../lib/format.js";
|
|
15
|
+
import { classifyQueryError } from "../../lib/query-error.js";
|
|
15
16
|
import { Flex } from "../layout/flex.js";
|
|
16
17
|
import { Button } from "../general/button.js";
|
|
18
|
+
import { Text } from "../general/typography.js";
|
|
19
|
+
import { useAuthExpiry } from "./auth-expiry.js";
|
|
17
20
|
const AlertContext = React.createContext("default");
|
|
18
21
|
const ASSERTIVE_TONES = /* @__PURE__ */ new Set(["destructive", "warning"]);
|
|
19
22
|
const roleFor = (variant, tone) => {
|
|
@@ -121,24 +124,42 @@ function AlertQueryError({
|
|
|
121
124
|
className
|
|
122
125
|
}) {
|
|
123
126
|
const { t } = useTranslation();
|
|
124
|
-
|
|
127
|
+
const resolved = category ?? classifyQueryError(error).category;
|
|
128
|
+
const expiry = useAuthExpiry(resolved === "auth", error);
|
|
129
|
+
if (expiry?.status === "redirecting") {
|
|
130
|
+
return /* @__PURE__ */ jsx(
|
|
131
|
+
Text,
|
|
132
|
+
{
|
|
133
|
+
as: "p",
|
|
134
|
+
size: "sm",
|
|
135
|
+
tone: "muted",
|
|
136
|
+
role: "status",
|
|
137
|
+
"aria-live": "polite",
|
|
138
|
+
"data-slot": "alert-query-auth-pending",
|
|
139
|
+
className,
|
|
140
|
+
children: t("query.authExpiry.redirecting")
|
|
141
|
+
}
|
|
142
|
+
);
|
|
143
|
+
}
|
|
144
|
+
const authAction = expiry ? expiry.retry : onAuthAction;
|
|
145
|
+
if (!category && !expiry) {
|
|
125
146
|
return /* @__PURE__ */ jsxs(Alert, { tone: "destructive", className, children: [
|
|
126
147
|
/* @__PURE__ */ jsx(AlertTitle, { children: t("common.error") }),
|
|
127
148
|
/* @__PURE__ */ jsx(AlertDescription, { children: humanError(error) }),
|
|
128
149
|
onRetry && /* @__PURE__ */ jsx(RetryButton, { onRetry })
|
|
129
150
|
] });
|
|
130
151
|
}
|
|
131
|
-
const tone = WARNING_CATEGORIES.has(
|
|
132
|
-
return /* @__PURE__ */ jsxs(Alert, { tone, className, children: [
|
|
133
|
-
/* @__PURE__ */ jsx(AlertTitle, { children: t(`query.error.title.${
|
|
134
|
-
/* @__PURE__ */ jsx(AlertDescription, { children: t(`query.error.description.${
|
|
135
|
-
|
|
152
|
+
const tone = resolved === "auth" ? "default" : WARNING_CATEGORIES.has(resolved) ? "warning" : "destructive";
|
|
153
|
+
return /* @__PURE__ */ jsxs(Alert, { tone, className, "data-query-category": resolved, children: [
|
|
154
|
+
/* @__PURE__ */ jsx(AlertTitle, { children: t(`query.error.title.${resolved}`) }),
|
|
155
|
+
/* @__PURE__ */ jsx(AlertDescription, { children: t(`query.error.description.${resolved}`) }),
|
|
156
|
+
resolved === "auth" && authAction && /* @__PURE__ */ jsx(AlertActions, { children: /* @__PURE__ */ jsx(
|
|
136
157
|
Button,
|
|
137
158
|
{
|
|
138
159
|
variant: "outline",
|
|
139
160
|
size: "sm",
|
|
140
161
|
onClick: () => {
|
|
141
|
-
void
|
|
162
|
+
void authAction();
|
|
142
163
|
},
|
|
143
164
|
children: /* @__PURE__ */ jsxs(Flex, { direction: "row", wrap: true, align: "center", gap: "xs", children: [
|
|
144
165
|
/* @__PURE__ */ jsx(LogIn, { "aria-hidden": "true" }),
|
|
@@ -146,7 +167,7 @@ function AlertQueryError({
|
|
|
146
167
|
] })
|
|
147
168
|
}
|
|
148
169
|
) }),
|
|
149
|
-
RETRYABLE_CATEGORIES.has(
|
|
170
|
+
RETRYABLE_CATEGORIES.has(resolved) && onRetry && /* @__PURE__ */ jsx(RetryButton, { onRetry })
|
|
150
171
|
] });
|
|
151
172
|
}
|
|
152
173
|
const Alert = Object.assign(AlertBase, {
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
import type { AuthExpiryProviderProp } from "../../props/components/feedback.prop.js";
|
|
3
|
+
export type { AuthExpiryProviderProp, AuthExpiryProviderProp as AuthExpiryProviderProps, } from "../../props/components/feedback.prop.js";
|
|
4
|
+
/**
|
|
5
|
+
* `"redirecting"` — the handler has been invoked (or is about to be); render a neutral pending state.
|
|
6
|
+
* `"failed"` — the handler threw or rejected; fall back to the sign-in alert so the user is not left
|
|
7
|
+
* on an endless spinner.
|
|
8
|
+
*/
|
|
9
|
+
export type AuthExpiryStatus = "redirecting" | "failed";
|
|
10
|
+
/**
|
|
11
|
+
* Central handling for an expired session (401 / invalid or expired token) — the SSO norm: the app
|
|
12
|
+
* sends the user back through the IdP, which returns immediately while the IdP session is alive, so
|
|
13
|
+
* no page-level error is ever painted.
|
|
14
|
+
*
|
|
15
|
+
* Every `DataState`, `InfiniteQueryState`, `AlertMutationFeedback` and `AlertQueryError` under this
|
|
16
|
+
* provider consults it. On an auth-class error it calls `onAuthExpired` **once** — any number of
|
|
17
|
+
* simultaneous 401s share one call — and renders a neutral pending state with a polite live region
|
|
18
|
+
* instead of an alert. The call is re-armed once every auth-errored view has gone (navigated away,
|
|
19
|
+
* or the queries recovered after a silent refresh), so a later expiry fires again.
|
|
20
|
+
*/
|
|
21
|
+
export declare function AuthExpiryProvider({ onAuthExpired, children }: AuthExpiryProviderProp): React.JSX.Element;
|
|
22
|
+
/**
|
|
23
|
+
* Internal hook for the query surfaces. `active` = this view is currently showing an auth-class
|
|
24
|
+
* error. Returns `null` when there is no provider (the caller keeps its legacy alert), otherwise the
|
|
25
|
+
* status to render plus a `retry` for the fallback alert's sign-in button.
|
|
26
|
+
*/
|
|
27
|
+
export declare function useAuthExpiry(active: boolean, error: unknown): {
|
|
28
|
+
status: AuthExpiryStatus;
|
|
29
|
+
retry: () => void;
|
|
30
|
+
} | null;
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx } from "react/jsx-runtime";
|
|
3
|
+
import * as React from "react";
|
|
4
|
+
const AuthExpiryContext = React.createContext(null);
|
|
5
|
+
function AuthExpiryProvider({ onAuthExpired, children }) {
|
|
6
|
+
const handlerRef = React.useRef(onAuthExpired);
|
|
7
|
+
handlerRef.current = onAuthExpired;
|
|
8
|
+
const countRef = React.useRef(0);
|
|
9
|
+
const firedRef = React.useRef(false);
|
|
10
|
+
const [failed, setFailed] = React.useState(false);
|
|
11
|
+
const invoke = React.useCallback((error) => {
|
|
12
|
+
firedRef.current = true;
|
|
13
|
+
setFailed(false);
|
|
14
|
+
const fail = () => setFailed(true);
|
|
15
|
+
try {
|
|
16
|
+
const result = handlerRef.current({ error });
|
|
17
|
+
if (result && typeof result.then === "function") {
|
|
18
|
+
result.then(void 0, fail);
|
|
19
|
+
}
|
|
20
|
+
} catch {
|
|
21
|
+
fail();
|
|
22
|
+
}
|
|
23
|
+
}, []);
|
|
24
|
+
const register = React.useCallback(
|
|
25
|
+
(error) => {
|
|
26
|
+
countRef.current += 1;
|
|
27
|
+
if (!firedRef.current) invoke(error);
|
|
28
|
+
return () => {
|
|
29
|
+
countRef.current -= 1;
|
|
30
|
+
setTimeout(() => {
|
|
31
|
+
if (countRef.current === 0) {
|
|
32
|
+
firedRef.current = false;
|
|
33
|
+
setFailed(false);
|
|
34
|
+
}
|
|
35
|
+
}, 0);
|
|
36
|
+
};
|
|
37
|
+
},
|
|
38
|
+
[invoke]
|
|
39
|
+
);
|
|
40
|
+
const value = React.useMemo(
|
|
41
|
+
() => ({ register, retry: invoke, failed }),
|
|
42
|
+
[register, invoke, failed]
|
|
43
|
+
);
|
|
44
|
+
return /* @__PURE__ */ jsx(AuthExpiryContext.Provider, { value, children });
|
|
45
|
+
}
|
|
46
|
+
function useAuthExpiry(active, error) {
|
|
47
|
+
const ctx = React.useContext(AuthExpiryContext);
|
|
48
|
+
const errorRef = React.useRef(error);
|
|
49
|
+
errorRef.current = error;
|
|
50
|
+
const register = ctx?.register;
|
|
51
|
+
React.useEffect(() => {
|
|
52
|
+
if (!register || !active) return void 0;
|
|
53
|
+
return register(errorRef.current);
|
|
54
|
+
}, [register, active]);
|
|
55
|
+
if (!ctx || !active) return null;
|
|
56
|
+
return {
|
|
57
|
+
status: ctx.failed ? "failed" : "redirecting",
|
|
58
|
+
retry: () => ctx.retry(errorRef.current)
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
export {
|
|
62
|
+
AuthExpiryProvider,
|
|
63
|
+
useAuthExpiry
|
|
64
|
+
};
|
|
@@ -14,4 +14,6 @@ export { Callout } from "./callout.js";
|
|
|
14
14
|
export type { CalloutProp, CalloutProps } from "./callout.js";
|
|
15
15
|
export { Alert, AlertTitle, AlertContent, AlertDescription, AlertActions, AlertQueryError, } from "./alert.js";
|
|
16
16
|
export type { AlertProp, AlertProps, AlertTitleProp, AlertTitleProps, AlertContentProp, AlertContentProps, AlertDescriptionProp, AlertDescriptionProps, AlertActionsProp, AlertActionsProps, AlertQueryErrorProp, AlertQueryErrorProps, } from "./alert.js";
|
|
17
|
+
export { AuthExpiryProvider } from "./auth-expiry.js";
|
|
18
|
+
export type { AuthExpiryProviderProp, AuthExpiryProviderProps } from "./auth-expiry.js";
|
|
17
19
|
export { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider } from "./tooltip.js";
|
|
@@ -66,6 +66,7 @@ import {
|
|
|
66
66
|
AlertActions,
|
|
67
67
|
AlertQueryError
|
|
68
68
|
} from "./alert.js";
|
|
69
|
+
import { AuthExpiryProvider } from "./auth-expiry.js";
|
|
69
70
|
import { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider } from "./tooltip.js";
|
|
70
71
|
export {
|
|
71
72
|
Alert,
|
|
@@ -86,6 +87,7 @@ export {
|
|
|
86
87
|
AlertDialogTrigger,
|
|
87
88
|
AlertQueryError,
|
|
88
89
|
AlertTitle,
|
|
90
|
+
AuthExpiryProvider,
|
|
89
91
|
Banner,
|
|
90
92
|
Callout,
|
|
91
93
|
Dialog,
|
|
@@ -8,7 +8,7 @@ export type TabsProps = Omit<React.ComponentPropsWithoutRef<"div">, "defaultValu
|
|
|
8
8
|
dir?: "ltr" | "rtl";
|
|
9
9
|
activationMode?: "manual" | "automatic";
|
|
10
10
|
};
|
|
11
|
-
export declare function Tabs({ className, orientation, items, value, defaultValue, onValueChange, activationMode, dir, variant, tabPlacement, size, centered, bodied, extra, destroyOnHidden, onEdit, addIcon, hideAdd, closeIcon, onTabClick, overflow, animated, indicator, moreIcon, onTabScroll, listClassName, contentClassName, children, ...props }: TabsProps): React.JSX.Element;
|
|
11
|
+
export declare function Tabs({ className, orientation, items, value, defaultValue, onValueChange, activationMode, dir, variant, tabPlacement, size, centered, bodied, extra, destroyOnHidden, onEdit, addIcon, hideAdd, closeIcon, onTabClick, overflow, animated, indicator, moreIcon, onTabScroll, controls, listClassName, contentClassName, children, "aria-label": ariaLabel, "aria-labelledby": ariaLabelledby, ...props }: TabsProps): React.JSX.Element;
|
|
12
12
|
export declare const TabsList: React.ForwardRefExoticComponent<Omit<React.DetailedHTMLProps<React.HTMLAttributes<HTMLDivElement>, HTMLDivElement>, "ref"> & {
|
|
13
13
|
variant?: "default" | "line";
|
|
14
14
|
/**
|
|
@@ -24,7 +24,7 @@ export declare const TabsTrigger: React.ForwardRefExoticComponent<Omit<Omit<Reac
|
|
|
24
24
|
value: string;
|
|
25
25
|
/** RAC `isDisabled`; ALSO written to the button so `disabled:` utilities keep matching. */
|
|
26
26
|
disabled?: boolean;
|
|
27
|
-
} & React.RefAttributes<HTMLButtonElement>>;
|
|
27
|
+
} & Pick<TabItemProp, "count" | "countLabel" | "overflowCount" | "showZero"> & React.RefAttributes<HTMLButtonElement>>;
|
|
28
28
|
export declare const TabsContent: React.ForwardRefExoticComponent<Omit<Omit<React.DetailedHTMLProps<React.HTMLAttributes<HTMLDivElement>, HTMLDivElement>, "ref">, "value"> & {
|
|
29
29
|
/** The panel's key — must match a `TabsTrigger value`. RAC `id`. */
|
|
30
30
|
value: string;
|
|
@@ -41,11 +41,14 @@ function TabCount({
|
|
|
41
41
|
const { pill } = useCounterPill({ count, overflowCount, showZero, countLabel, slot: "tabs" });
|
|
42
42
|
return pill;
|
|
43
43
|
}
|
|
44
|
+
const NO_LIST_LABEL = {};
|
|
44
45
|
const TabsFrameContext = React.createContext({
|
|
45
46
|
orientation: "horizontal",
|
|
46
47
|
selectionSuppressed: false,
|
|
47
48
|
size: "md",
|
|
48
|
-
panels: null
|
|
49
|
+
panels: null,
|
|
50
|
+
controls: void 0,
|
|
51
|
+
listLabel: NO_LIST_LABEL
|
|
49
52
|
});
|
|
50
53
|
function resolveFallbackTabValue(items, requested) {
|
|
51
54
|
if (!items || items.length === 0) return requested;
|
|
@@ -122,9 +125,12 @@ function Tabs({
|
|
|
122
125
|
indicator,
|
|
123
126
|
moreIcon,
|
|
124
127
|
onTabScroll,
|
|
128
|
+
controls,
|
|
125
129
|
listClassName,
|
|
126
130
|
contentClassName,
|
|
127
131
|
children,
|
|
132
|
+
"aria-label": ariaLabel,
|
|
133
|
+
"aria-labelledby": ariaLabelledby,
|
|
128
134
|
...props
|
|
129
135
|
}) {
|
|
130
136
|
const { t } = useTranslation();
|
|
@@ -151,9 +157,20 @@ function Tabs({
|
|
|
151
157
|
return next;
|
|
152
158
|
});
|
|
153
159
|
}, []);
|
|
160
|
+
const listLabel = React.useMemo(
|
|
161
|
+
() => ariaLabel === void 0 && ariaLabelledby === void 0 ? NO_LIST_LABEL : { "aria-label": ariaLabel, "aria-labelledby": ariaLabelledby },
|
|
162
|
+
[ariaLabel, ariaLabelledby]
|
|
163
|
+
);
|
|
154
164
|
const frame = React.useMemo(
|
|
155
|
-
() => ({
|
|
156
|
-
|
|
165
|
+
() => ({
|
|
166
|
+
orientation: resolvedOrientation,
|
|
167
|
+
selectionSuppressed,
|
|
168
|
+
size,
|
|
169
|
+
panels,
|
|
170
|
+
controls,
|
|
171
|
+
listLabel
|
|
172
|
+
}),
|
|
173
|
+
[resolvedOrientation, selectionSuppressed, size, panels, controls, listLabel]
|
|
157
174
|
);
|
|
158
175
|
const editable = variant === "editable-card";
|
|
159
176
|
const collapsible = items != null && overflow === "menu";
|
|
@@ -444,7 +461,7 @@ function Tabs({
|
|
|
444
461
|
] }) : null,
|
|
445
462
|
extraEnd ? /* @__PURE__ */ jsx("div", { "data-slot": "tabs-extra", "data-side": "end", className: "ui-tabs-extra", children: extraEnd }) : null
|
|
446
463
|
] }) : list,
|
|
447
|
-
items.map((item) => {
|
|
464
|
+
controls !== void 0 ? null : items.map((item) => {
|
|
448
465
|
const keepMounted = !destroyOnHidden || item.forceRender === true;
|
|
449
466
|
return /* @__PURE__ */ jsx(
|
|
450
467
|
TabsContent,
|
|
@@ -465,6 +482,9 @@ function Tabs({
|
|
|
465
482
|
}
|
|
466
483
|
const TabsList = React.forwardRef(
|
|
467
484
|
({ className, variant = "default", loop: _loop, children, ...props }, ref) => {
|
|
485
|
+
const { listLabel } = React.useContext(TabsFrameContext);
|
|
486
|
+
const ownName = props["aria-label"] !== void 0 || props["aria-labelledby"] !== void 0;
|
|
487
|
+
const name = ownName ? { "aria-label": props["aria-label"], "aria-labelledby": props["aria-labelledby"] } : listLabel;
|
|
468
488
|
const listRef = React.useRef(null);
|
|
469
489
|
const setListRef = React.useCallback(
|
|
470
490
|
(node) => {
|
|
@@ -479,6 +499,8 @@ const TabsList = React.forwardRef(
|
|
|
479
499
|
AriaTabList,
|
|
480
500
|
{
|
|
481
501
|
ref: setListRef,
|
|
502
|
+
"aria-label": name["aria-label"],
|
|
503
|
+
"aria-labelledby": name["aria-labelledby"],
|
|
482
504
|
className: cn(
|
|
483
505
|
// `min-w-0 max-w-full` let the list shrink to (and never exceed) whatever width its
|
|
484
506
|
// ancestors actually give it instead of forcing them wider; horizontal orientation then
|
|
@@ -512,10 +534,22 @@ const TabsList = React.forwardRef(
|
|
|
512
534
|
);
|
|
513
535
|
TabsList.displayName = "TabsList";
|
|
514
536
|
const TabsTrigger = React.forwardRef(
|
|
515
|
-
({
|
|
516
|
-
|
|
537
|
+
({
|
|
538
|
+
className,
|
|
539
|
+
value,
|
|
540
|
+
disabled,
|
|
541
|
+
count,
|
|
542
|
+
overflowCount,
|
|
543
|
+
showZero,
|
|
544
|
+
countLabel,
|
|
545
|
+
children,
|
|
546
|
+
onKeyDown,
|
|
547
|
+
onClick,
|
|
548
|
+
...props
|
|
549
|
+
}, ref) => {
|
|
550
|
+
const { orientation, selectionSuppressed, size, panels, controls } = React.useContext(TabsFrameContext);
|
|
517
551
|
const controlsAPanel = panels === null || panels.has(value);
|
|
518
|
-
return /* @__PURE__ */
|
|
552
|
+
return /* @__PURE__ */ jsxs(
|
|
519
553
|
AriaTab,
|
|
520
554
|
{
|
|
521
555
|
ref,
|
|
@@ -565,8 +599,10 @@ const TabsTrigger = React.forwardRef(
|
|
|
565
599
|
// here for BOTH states, exactly as Radix did.
|
|
566
600
|
"data-state": renderProps.isSelected && !selectionSuppressed ? "active" : "inactive",
|
|
567
601
|
"aria-selected": renderProps.isSelected && !selectionSuppressed,
|
|
568
|
-
// RAC writes this unconditionally; see `TabsFrame.panels` (gh#643).
|
|
569
|
-
|
|
602
|
+
// RAC writes this unconditionally; see `TabsFrame.panels` (gh#643). With no panel
|
|
603
|
+
// declared, a strip-only root's `controls` region takes its place — on the SELECTED
|
|
604
|
+
// tab only, the same rule RAC applies to a real panel (gh#1021).
|
|
605
|
+
"aria-controls": controlsAPanel ? domProps["aria-controls"] : renderProps.isSelected && !selectionSuppressed ? controls : void 0,
|
|
570
606
|
"data-orientation": orientation,
|
|
571
607
|
disabled,
|
|
572
608
|
// CHAINED, not spread — the same reason `onKeyDown` below is. `withDomProps` lays the
|
|
@@ -591,7 +627,18 @@ const TabsTrigger = React.forwardRef(
|
|
|
591
627
|
},
|
|
592
628
|
tabIndex: props.tabIndex ?? domProps.tabIndex
|
|
593
629
|
}),
|
|
594
|
-
children
|
|
630
|
+
children: [
|
|
631
|
+
children,
|
|
632
|
+
/* @__PURE__ */ jsx(
|
|
633
|
+
TabCount,
|
|
634
|
+
{
|
|
635
|
+
count,
|
|
636
|
+
overflowCount,
|
|
637
|
+
showZero,
|
|
638
|
+
countLabel
|
|
639
|
+
}
|
|
640
|
+
)
|
|
641
|
+
]
|
|
595
642
|
}
|
|
596
643
|
);
|
|
597
644
|
}
|
|
@@ -2,6 +2,7 @@
|
|
|
2
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 { useAuthExpiry } from "../feedback/auth-expiry.js";
|
|
5
6
|
import { useTranslation } from "../../i18n/use-translation.js";
|
|
6
7
|
import { classifyQueryError } from "../../lib/query-error.js";
|
|
7
8
|
function defaultIsEmpty(data) {
|
|
@@ -34,10 +35,18 @@ function DataState({
|
|
|
34
35
|
}
|
|
35
36
|
void query.refetch();
|
|
36
37
|
}, [onRetry, query]);
|
|
38
|
+
const showingAuthError = query.isError && !query.isFetching && classifyQueryError(query.error).category === "auth";
|
|
39
|
+
const authExpiry = useAuthExpiry(showingAuthError, query.error);
|
|
37
40
|
if (query.isPending && query.fetchStatus === "idle") return /* @__PURE__ */ jsx(Fragment, { children: prerequisite });
|
|
38
41
|
if (query.isPending) return /* @__PURE__ */ jsx(Fragment, { children: skeleton });
|
|
39
42
|
if (query.isError) {
|
|
40
43
|
if (query.isFetching) return /* @__PURE__ */ jsx(Fragment, { children: skeleton });
|
|
44
|
+
if (authExpiry?.status === "redirecting") {
|
|
45
|
+
return /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
46
|
+
skeleton,
|
|
47
|
+
/* @__PURE__ */ jsx("span", { role: "status", "aria-live": "polite", className: "sr-only", children: t("query.authExpiry.redirecting") })
|
|
48
|
+
] });
|
|
49
|
+
}
|
|
41
50
|
if (errorRenderer) return /* @__PURE__ */ jsx(Fragment, { children: errorRenderer(query.error, retry) });
|
|
42
51
|
const info = classifyQueryError(query.error);
|
|
43
52
|
const canRetry = (info.category === "transient" || info.category === "unknown") && (info.retryable || showRetry || Boolean(onRetry));
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
export { DataState } from "./data-state.js";
|
|
2
2
|
export type { DataStateProp, DataStateProps } from "./data-state.js";
|
|
3
3
|
export { AlertMutationFeedback } from "./mutation-feedback.js";
|
|
4
|
+
export { AuthExpiryProvider } from "../feedback/auth-expiry.js";
|
|
5
|
+
export type { AuthExpiryProviderProp, AuthExpiryProviderProps } from "../feedback/auth-expiry.js";
|
|
4
6
|
export type { AlertMutationFeedbackProp, AlertMutationFeedbackProps } from "./mutation-feedback.js";
|
|
5
7
|
export { ButtonRefetch } from "./query-refetch-button.js";
|
|
6
8
|
export type { ButtonRefetchProp, ButtonRefetchProps } from "./query-refetch-button.js";
|
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
import { DataState } from "./data-state.js";
|
|
2
2
|
import { AlertMutationFeedback } from "./mutation-feedback.js";
|
|
3
|
+
import { AuthExpiryProvider } from "../feedback/auth-expiry.js";
|
|
3
4
|
import { ButtonRefetch } from "./query-refetch-button.js";
|
|
4
5
|
import { InfiniteQueryState, flattenItemPages } from "./infinite-query-state.js";
|
|
5
6
|
import { classifyQueryError, isRetryableQueryError } from "../../lib/query-error.js";
|
|
6
7
|
export {
|
|
7
8
|
AlertMutationFeedback,
|
|
9
|
+
AuthExpiryProvider,
|
|
8
10
|
ButtonRefetch,
|
|
9
11
|
DataState,
|
|
10
12
|
InfiniteQueryState,
|
|
@@ -2,6 +2,7 @@
|
|
|
2
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 { useAuthExpiry } from "../feedback/auth-expiry.js";
|
|
5
6
|
import { Button } from "../general/button.js";
|
|
6
7
|
import { useTranslation } from "../../i18n/use-translation.js";
|
|
7
8
|
import { classifyQueryError } from "../../lib/query-error.js";
|
|
@@ -36,9 +37,17 @@ function InfiniteQueryState({
|
|
|
36
37
|
}
|
|
37
38
|
void query.refetch();
|
|
38
39
|
}, [onRetry, query]);
|
|
40
|
+
const showingAuthError = query.isError && !(query.isFetching && !query.isFetchingNextPage) && classifyQueryError(query.error).category === "auth";
|
|
41
|
+
const authExpiry = useAuthExpiry(showingAuthError, query.error);
|
|
39
42
|
if (query.isPending) return /* @__PURE__ */ jsx(Fragment, { children: skeleton });
|
|
40
43
|
if (query.isError) {
|
|
41
44
|
if (query.isFetching && !query.isFetchingNextPage) return /* @__PURE__ */ jsx(Fragment, { children: skeleton });
|
|
45
|
+
if (authExpiry?.status === "redirecting") {
|
|
46
|
+
return /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
47
|
+
skeleton,
|
|
48
|
+
/* @__PURE__ */ jsx("span", { role: "status", "aria-live": "polite", className: "sr-only", children: t("query.authExpiry.redirecting") })
|
|
49
|
+
] });
|
|
50
|
+
}
|
|
42
51
|
if (errorRenderer) return /* @__PURE__ */ jsx(Fragment, { children: errorRenderer(query.error, retry) });
|
|
43
52
|
const info = classifyQueryError(query.error);
|
|
44
53
|
const canRetry = (info.category === "transient" || info.category === "unknown") && (info.retryable || showRetry || Boolean(onRetry));
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"$comment": "AUTO-GENERATED by scripts/gen-measurement-contract.mjs — do not edit. Read this instead of guessing: docs/MEASUREMENT-CONTRACT.md.",
|
|
3
|
-
"version": "31.
|
|
3
|
+
"version": "31.4.0",
|
|
4
4
|
"targetSize": {
|
|
5
5
|
"standard": "WCAG 2.2 SC 2.5.8 Target Size (Minimum), level AA — 24×24 CSS px",
|
|
6
6
|
"min": 24,
|
|
@@ -71,6 +71,19 @@ export type AlertQueryErrorProp = {
|
|
|
71
71
|
onAuthAction?: HandlerProp;
|
|
72
72
|
className?: ClassNameProp;
|
|
73
73
|
};
|
|
74
|
+
/** @see AuthExpiryProvider — central, deduplicated handling of an expired session (gh#1022). */
|
|
75
|
+
export type AuthExpiryProviderProp = {
|
|
76
|
+
/**
|
|
77
|
+
* Called ONCE when a query/mutation surface under the provider meets an auth-class error (401 /
|
|
78
|
+
* invalid or expired token); simultaneous 401s share one call, re-armed after every auth-errored
|
|
79
|
+
* view has gone. Typically redirects to the IdP with the current URL as the return target.
|
|
80
|
+
* Return a promise for a silent refresh: a rejection brings back the sign-in alert.
|
|
81
|
+
*/
|
|
82
|
+
onAuthExpired: (context: {
|
|
83
|
+
error: unknown;
|
|
84
|
+
}) => void | Promise<void>;
|
|
85
|
+
children?: ChildrenProp;
|
|
86
|
+
};
|
|
74
87
|
/** @see Alert */
|
|
75
88
|
export type AlertProp = React.HTMLAttributes<HTMLDivElement> & {
|
|
76
89
|
variant?: AlertVariantProp;
|
|
@@ -3,7 +3,7 @@ export type { ButtonProp } from "./general.prop.js";
|
|
|
3
3
|
export type { CommandProp, InputProp, TextareaProp, FormFieldProp, SearchInputProp, CheckboxProp, CheckboxGroupProp, ChoiceOptionProp, RadioProp, RadioGroupProp, SwitchProp, SliderProp, CalendarProp, DatePickerProp, TimePickerProp, ColorPickerProp, UploadProp, UploadFileItemProp, UploadVariantProp, TreeOptionProp, TreeFieldNamesProp, CascaderProp, TreeSelectProp, ShowCheckedStrategyProp, TransferProp, TransferItemProp, } from "./data-entry.prop.js";
|
|
4
4
|
export type { AvatarProp, EmptyStateProp, DescriptionsProp, DescriptionsItemProp, BadgeProp, DataTableProp, QrCodeProp, } from "./data-display.prop.js";
|
|
5
5
|
export type { ChartDatum, ChartSeriesProp, LineChartProp, BarChartProp, AreaChartProp, PieChartProp, } from "./charts.prop.js";
|
|
6
|
-
export type { AlertQueryErrorProp, AlertProp, DialogProp, DialogContentProp, AlertTitleProp, AlertContentProp, AlertDescriptionProp, AlertActionsProp, SheetResponsiveProp, SkeletonRowsProp, } from "./feedback.prop.js";
|
|
6
|
+
export type { AlertQueryErrorProp, AuthExpiryProviderProp, AlertProp, DialogProp, DialogContentProp, AlertTitleProp, AlertContentProp, AlertDescriptionProp, AlertActionsProp, SheetResponsiveProp, SkeletonRowsProp, } from "./feedback.prop.js";
|
|
7
7
|
export type { DataStateProp, InfiniteQueryStateProp, InfiniteQueryHelpers, PrefetchLinkProp, } from "./query.prop.js";
|
|
8
8
|
export type { DropdownMenuPlacementProp, DropdownMenuTriggerActionProp, PaginationProp, PaginationSizeProp, PaginationAlignProp, StepsProp, StepItemProp, StepStatusProp, StepsTypeProp, TabsProp, TabItemProp, TabsVariantProp, TabsPlacementProp, TabsExtraProp, TabsOnEditProp, } from "./navigation.prop.js";
|
|
9
9
|
export type { AppProviderProp, AppContextValue, AppSettingKind, AppSettingPickerProp, AppSettingToggleKind, AppSettingToggleProp, ThemeScopeProp, } from "./app.prop.js";
|
|
@@ -255,7 +255,11 @@ export type StepsSeparatorProp = "chevron" | "arrow";
|
|
|
255
255
|
export type TabItemProp = {
|
|
256
256
|
value: string;
|
|
257
257
|
label: React.ReactNode;
|
|
258
|
-
|
|
258
|
+
/**
|
|
259
|
+
* The panel body. Optional because a strip-only `Tabs` (`controls`, gh#1021) renders no panel
|
|
260
|
+
* at all — its content lives in the consumer's own region.
|
|
261
|
+
*/
|
|
262
|
+
content?: React.ReactNode;
|
|
259
263
|
disabled?: boolean;
|
|
260
264
|
/** Leading glyph inside the trigger (Ant Design `Tab.icon`). */
|
|
261
265
|
icon?: React.ReactNode;
|
|
@@ -514,6 +518,20 @@ export type TabsProp = {
|
|
|
514
518
|
moreIcon?: React.ReactNode;
|
|
515
519
|
/** Ant Design `onTabScroll`, on the logical axis. @see TabsOnScrollProp */
|
|
516
520
|
onTabScroll?: TabsOnScrollProp;
|
|
521
|
+
/**
|
|
522
|
+
* STRIP-ONLY MODE (gh#1021) — the `id` of the element OUTSIDE this `Tabs` that the strip
|
|
523
|
+
* drives: a status strip (未対応 / 対応中 / 完了) over a MasterDetail, a saved-view ribbon over a
|
|
524
|
+
* grid. With it set the `items` API renders the tablist ONLY — no `tabpanel`, empty or
|
|
525
|
+
* otherwise — and the selected tab's `aria-controls` points at this id, so the WAI-ARIA APG
|
|
526
|
+
* tab → controlled-region relationship stays real. The consumer's region is never mounted by
|
|
527
|
+
* the strip, so switching tabs cannot remount it (an unsent reply draft survives).
|
|
528
|
+
*
|
|
529
|
+
* Give the region an accessible name (`aria-label`/`aria-labelledby`, e.g. a `<section>`). In
|
|
530
|
+
* the compound form it applies to every trigger with no `TabsContent` declared for its value; a
|
|
531
|
+
* declared panel always wins. A pure FILTER with no region to point at is a `Segmented`
|
|
532
|
+
* (`radiogroup`), not a strip-only `Tabs`.
|
|
533
|
+
*/
|
|
534
|
+
controls?: IdProp;
|
|
517
535
|
className?: ClassNameProp;
|
|
518
536
|
listClassName?: ClassNameProp;
|
|
519
537
|
contentClassName?: ClassNameProp;
|
package/dist/props/registry.d.ts
CHANGED
|
@@ -848,7 +848,7 @@ export declare const COMPONENT_PROP_REGISTRY: {
|
|
|
848
848
|
readonly vocabulary: readonly ["OrientationProp", {
|
|
849
849
|
readonly field: "vertical";
|
|
850
850
|
readonly local: true;
|
|
851
|
-
readonly reason:
|
|
851
|
+
readonly reason: 'antd\'s boolean spelling of orientation="vertical"; orientation wins over it.';
|
|
852
852
|
}, {
|
|
853
853
|
readonly field: "fullWidth";
|
|
854
854
|
readonly local: true;
|
|
@@ -2415,6 +2415,15 @@ export declare const COMPONENT_PROP_REGISTRY: {
|
|
|
2415
2415
|
readonly file: "components/query.prop.ts";
|
|
2416
2416
|
readonly vocabulary: readonly [];
|
|
2417
2417
|
};
|
|
2418
|
+
readonly AuthExpiryProviderProp: {
|
|
2419
|
+
readonly group: "feedback";
|
|
2420
|
+
readonly file: "components/feedback.prop.ts";
|
|
2421
|
+
readonly vocabulary: readonly [{
|
|
2422
|
+
readonly field: "onAuthExpired";
|
|
2423
|
+
readonly local: true;
|
|
2424
|
+
readonly reason: "App-wide session-expiry callback receiving the triggering error; fired once per expiry (gh#1022).";
|
|
2425
|
+
}, "ChildrenProp"];
|
|
2426
|
+
};
|
|
2418
2427
|
readonly AlertQueryErrorProp: {
|
|
2419
2428
|
readonly group: "feedback";
|
|
2420
2429
|
readonly file: "components/feedback.prop.ts";
|
|
@@ -2617,7 +2626,7 @@ export declare const COMPONENT_PROP_REGISTRY: {
|
|
|
2617
2626
|
readonly TabsProp: {
|
|
2618
2627
|
readonly group: "navigation";
|
|
2619
2628
|
readonly file: "components/navigation.prop.ts";
|
|
2620
|
-
readonly vocabulary: readonly ["ValueProp", "DefaultValueProp", "OnValueChangeProp", "ClassNameProp", "SizeProp", "ExtraProp", {
|
|
2629
|
+
readonly vocabulary: readonly ["ValueProp", "DefaultValueProp", "OnValueChangeProp", "ClassNameProp", "SizeProp", "ExtraProp", "IdProp", {
|
|
2621
2630
|
readonly field: "bodied";
|
|
2622
2631
|
readonly local: true;
|
|
2623
2632
|
readonly reason: "A positive boolean for the card strip's PANEL BODY (gh#762) — an axis on Tabs rather than a `CardTabs` export, because the thing missing was a surface the existing card face already repaints its edge into, not a second component. No vocabulary type: no other component draws a body for its own panel.";
|
package/dist/props/registry.js
CHANGED
|
@@ -2833,6 +2833,18 @@ const COMPONENT_PROP_REGISTRY = {
|
|
|
2833
2833
|
vocabulary: ["HandlerProp"]
|
|
2834
2834
|
},
|
|
2835
2835
|
PrefetchLinkProp: { group: "query", file: "components/query.prop.ts", vocabulary: [] },
|
|
2836
|
+
AuthExpiryProviderProp: {
|
|
2837
|
+
group: "feedback",
|
|
2838
|
+
file: "components/feedback.prop.ts",
|
|
2839
|
+
vocabulary: [
|
|
2840
|
+
{
|
|
2841
|
+
field: "onAuthExpired",
|
|
2842
|
+
local: true,
|
|
2843
|
+
reason: "App-wide session-expiry callback receiving the triggering error; fired once per expiry (gh#1022)."
|
|
2844
|
+
},
|
|
2845
|
+
"ChildrenProp"
|
|
2846
|
+
]
|
|
2847
|
+
},
|
|
2836
2848
|
AlertQueryErrorProp: {
|
|
2837
2849
|
group: "feedback",
|
|
2838
2850
|
file: "components/feedback.prop.ts",
|
|
@@ -3051,6 +3063,7 @@ const COMPONENT_PROP_REGISTRY = {
|
|
|
3051
3063
|
"ClassNameProp",
|
|
3052
3064
|
"SizeProp",
|
|
3053
3065
|
"ExtraProp",
|
|
3066
|
+
"IdProp",
|
|
3054
3067
|
{
|
|
3055
3068
|
field: "bodied",
|
|
3056
3069
|
local: true,
|
|
@@ -50,6 +50,10 @@
|
|
|
50
50
|
color: hsl(var(--foreground));
|
|
51
51
|
}
|
|
52
52
|
|
|
53
|
+
[data-slot="alert"][data-query-category="auth"] {
|
|
54
|
+
max-inline-size: var(--query-auth-alert-max-inline-size);
|
|
55
|
+
}
|
|
56
|
+
|
|
53
57
|
[data-slot="alert"][data-variant="banner"] {
|
|
54
58
|
border-radius: var(--banner-radius);
|
|
55
59
|
border-block-start-width: 0;
|
|
@@ -285,6 +289,18 @@
|
|
|
285
289
|
background: var(--skeleton-background, hsl(var(--muted)));
|
|
286
290
|
}
|
|
287
291
|
|
|
292
|
+
.ui-skeleton-line {
|
|
293
|
+
block-size: var(--skeleton-block-height);
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
.ui-skeleton-caption {
|
|
297
|
+
block-size: var(--skeleton-caption-height);
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
.ui-skeleton-title {
|
|
301
|
+
block-size: var(--skeleton-title-height);
|
|
302
|
+
}
|
|
303
|
+
|
|
288
304
|
.ui-skeleton-table {
|
|
289
305
|
overflow: hidden;
|
|
290
306
|
border: var(--stroke-hairline) solid hsl(var(--border));
|
|
@@ -323,18 +339,6 @@
|
|
|
323
339
|
border-radius: var(--skeleton-radius, var(--radius));
|
|
324
340
|
padding: var(--skeleton-card-inset);
|
|
325
341
|
|
|
326
|
-
.ui-skeleton-line {
|
|
327
|
-
block-size: var(--skeleton-block-height);
|
|
328
|
-
}
|
|
329
|
-
|
|
330
|
-
.ui-skeleton-caption {
|
|
331
|
-
block-size: var(--skeleton-caption-height);
|
|
332
|
-
}
|
|
333
|
-
|
|
334
|
-
.ui-skeleton-title {
|
|
335
|
-
block-size: var(--skeleton-title-height);
|
|
336
|
-
}
|
|
337
|
-
|
|
338
342
|
.ui-skeleton-detail-label {
|
|
339
343
|
block-size: var(--skeleton-caption-height);
|
|
340
344
|
inline-size: var(--skeleton-label-width);
|