@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.
Files changed (58) hide show
  1. package/agent/START-HERE.md +6 -6
  2. package/agent/components/AuthExpiryProvider.json +40 -0
  3. package/agent/components/DataState.json +3 -1
  4. package/agent/components/InfiniteQueryState.json +1 -1
  5. package/agent/components/Tabs.json +5 -0
  6. package/agent/components-index.json +5 -0
  7. package/agent/components.json +49 -2
  8. package/agent/index.json +7 -7
  9. package/agent/llms.txt +6 -6
  10. package/agent/patterns/async-data-state.json +1 -1
  11. package/agent/patterns.json +1 -1
  12. package/agent/tokens.json +39 -3
  13. package/dist/components/admin/index.d.ts +2 -0
  14. package/dist/components/admin/index.js +2 -0
  15. package/dist/components/data-entry/upload.js +12 -1
  16. package/dist/components/feedback/alert.d.ts +4 -0
  17. package/dist/components/feedback/alert.js +29 -8
  18. package/dist/components/feedback/auth-expiry.d.ts +30 -0
  19. package/dist/components/feedback/auth-expiry.js +64 -0
  20. package/dist/components/feedback/index.d.ts +2 -0
  21. package/dist/components/feedback/index.js +2 -0
  22. package/dist/components/navigation/tabs.d.ts +2 -2
  23. package/dist/components/navigation/tabs.js +57 -10
  24. package/dist/components/query/data-state.js +9 -0
  25. package/dist/components/query/index.d.ts +2 -0
  26. package/dist/components/query/index.js +2 -0
  27. package/dist/components/query/infinite-query-state.js +9 -0
  28. package/dist/contracts/measurement.json +1 -1
  29. package/dist/i18n/messages/en.json +3 -0
  30. package/dist/i18n/messages/ja.json +3 -0
  31. package/dist/i18n/messages/vi.json +3 -0
  32. package/dist/props/components/feedback.prop.d.ts +13 -0
  33. package/dist/props/components/index.d.ts +1 -1
  34. package/dist/props/components/navigation.prop.d.ts +19 -1
  35. package/dist/props/registry.d.ts +11 -2
  36. package/dist/props/registry.js +13 -0
  37. package/dist/styles/alert-layout.css +16 -12
  38. package/dist/styles/control.css +18 -2
  39. package/dist/styles/data-entry-layout.css +4 -0
  40. package/dist/styles/form-layout.css +47 -5
  41. package/dist/styles/layers.json +35 -1
  42. package/dist/styles/layout.css +2 -1
  43. package/dist/tokens/components/feedback.css +2 -0
  44. package/dist/tokens/components/segmented.css +4 -0
  45. package/dist/tokens/semantic/layout.css +2 -0
  46. package/docs/FRAME-COVERAGE-REPORT.md +4 -2
  47. package/docs/data-entry/segmented.tsx +2 -0
  48. package/docs/feedback/auth-expiry.tsx +104 -0
  49. package/docs/i18n/messages/en.json +6 -0
  50. package/docs/i18n/messages/ja.json +6 -0
  51. package/docs/i18n/messages/vi.json +6 -0
  52. package/docs/navigation/tabs.tsx +43 -0
  53. package/docs/query/data-state.tsx +84 -12
  54. package/docs/showcase/table-view-tabs.tsx +15 -13
  55. package/docs/themes/flat.css +4 -0
  56. package/docs/themes/glassmorphism.css +2 -0
  57. package/docs/themes/neubrutalism.css +6 -0
  58. 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
- if (!category) {
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(category) ? "warning" : "destructive";
132
- return /* @__PURE__ */ jsxs(Alert, { tone, className, children: [
133
- /* @__PURE__ */ jsx(AlertTitle, { children: t(`query.error.title.${category}`) }),
134
- /* @__PURE__ */ jsx(AlertDescription, { children: t(`query.error.description.${category}`) }),
135
- category === "auth" && onAuthAction && /* @__PURE__ */ jsx(AlertActions, { children: /* @__PURE__ */ jsx(
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 onAuthAction();
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(category) && onRetry && /* @__PURE__ */ jsx(RetryButton, { onRetry })
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
- () => ({ orientation: resolvedOrientation, selectionSuppressed, size, panels }),
156
- [resolvedOrientation, selectionSuppressed, size, panels]
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
- ({ className, value, disabled, children, onKeyDown, onClick, ...props }, ref) => {
516
- const { orientation, selectionSuppressed, size, panels } = React.useContext(TabsFrameContext);
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__ */ jsx(
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
- "aria-controls": controlsAPanel ? domProps["aria-controls"] : void 0,
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.2.0",
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,
@@ -635,6 +635,9 @@
635
635
  "action": {
636
636
  "signIn": "Sign in again"
637
637
  }
638
+ },
639
+ "authExpiry": {
640
+ "redirecting": "Your session has ended. Redirecting to sign-in…"
638
641
  }
639
642
  },
640
643
  "chart": {
@@ -622,6 +622,9 @@
622
622
  "action": {
623
623
  "signIn": "再度サインイン"
624
624
  }
625
+ },
626
+ "authExpiry": {
627
+ "redirecting": "セッションの有効期限が切れました。サインイン画面へ移動しています…"
625
628
  }
626
629
  },
627
630
  "chart": {
@@ -623,6 +623,9 @@
623
623
  "action": {
624
624
  "signIn": "Đăng nhập lại"
625
625
  }
626
+ },
627
+ "authExpiry": {
628
+ "redirecting": "Phiên đăng nhập đã hết hạn. Đang chuyển đến trang đăng nhập…"
626
629
  }
627
630
  },
628
631
  "chart": {
@@ -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
- content: React.ReactNode;
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;
@@ -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: "antd's boolean spelling of orientation=\"vertical\"; orientation wins over it.";
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.";
@@ -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);