@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
@@ -3,7 +3,7 @@
3
3
  You are about to write code against a design system you did not author. This file is the whole
4
4
  contract. Read it before you write JSX.
5
5
 
6
- **This catalog describes `@godxjp/ui` 31.2.0.** If the project you are editing has a different
6
+ **This catalog describes `@godxjp/ui` 31.4.0.** If the project you are editing has a different
7
7
  version in its `package.json`, read the pinned catalog for THAT version instead
8
8
  (`…/v<their-version>/agent/…`). A catalog newer than the installed package describes props that do
9
9
  not exist yet; older, and it hides props that do. Neither failure announces itself.
@@ -47,7 +47,7 @@ Then ask it: `search_components`, `get_component`, `get_tokens`, `get_rule`, `li
47
47
  task is a task** — "build a settings page", "confirm a destructive delete", "a list page with
48
48
  filters" — start HERE, not at the components. Then fetch `patterns/<name>.json` for complete,
49
49
  copy-paste-ready code. A component index answers "does X exist"; it cannot answer "build Y".
50
- 1. `components-index.json` — 46 KB, all 175 components as name + group +
50
+ 1. `components-index.json` — 47 KB, all 176 components as name + group +
51
51
  tagline. Read this when you already know the SHAPE you need. Each entry may carry `absorbed`:
52
52
  names that **do not exist** and map to it — `Combobox`, `Autocomplete`, `CountrySelect` and
53
53
  `SearchSelect` are all `Select`. If you are about to hand-roll something, search this field
@@ -56,10 +56,10 @@ Then ask it: `search_components`, `get_component`, `get_tokens`, `get_rule`, `li
56
56
  its `importPath`, and its examples. Fetch only the handful you picked in step 1.
57
57
  3. `rules.json` — 50 cardinal rules. The ones about raw HTML and hardcoded colour are not
58
58
  style advice.
59
- 4. `tokens.json` — 2074 design tokens, each tagged with its `tier`. **If you were handed a
59
+ 4. `tokens.json` — 2080 design tokens, each tagged with its `tier`. **If you were handed a
60
60
  brand, read the 211 `foundation` entries first** — `--primary`, `--background`,
61
61
  `--radius`, `--font-size-base` are the handful everything else derives from. The
62
- 1760 `component` entries are per-part knobs; reach for one only when a role is
62
+ 1765 `component` entries are per-part knobs; reach for one only when a role is
63
63
  right everywhere except one component.
64
64
  5. `anti-ai-tells.json` — 26 shapes that make generated UI look generated, each with the
65
65
  fix. Read before you reach for a gradient hero or a wall of coloured chips.
@@ -144,8 +144,8 @@ has stopped following the brand.
144
144
  | `tier` | count | what it is | set it? |
145
145
  |---|---|---|---|
146
146
  | `foundation` | 211 | the seeds — `--primary`, `--background`, `--foreground`, `--radius`, `--font-size-base`, `--shadow-color`. Everything below is derived from these | **yes — this is the main road.** Handed a brand colour, this is where it goes: `:root { --primary: <H> <S>% <L>%; }` (HSL components, no `hsl()` wrapper) |
147
- | `semantic` | 103 | named roles that follow the seeds — `--ring`, `--text-link`, `--primary-hover`, `--overlay-background` | only when the seed is right and ONE role must differ. That role then stops following a later brand change |
148
- | `component` | 1760 | per-part knobs, `--{component}-{part}-{property}` | rarely. Most are declared `initial` with the real default at the call site — deliberate, so a scoped override re-resolves instead of freezing at `:root` |
147
+ | `semantic` | 104 | named roles that follow the seeds — `--ring`, `--text-link`, `--primary-hover`, `--overlay-background` | only when the seed is right and ONE role must differ. That role then stops following a later brand change |
148
+ | `component` | 1765 | per-part knobs, `--{component}-{part}-{property}` | rarely. Most are declared `initial` with the real default at the call site — deliberate, so a scoped override re-resolves instead of freezing at `:root` |
149
149
 
150
150
  A token whose `value` is `initial` is not empty and not broken: `initial` is the guaranteed-invalid
151
151
  value, so the real default is computed where the element paints it. Set it and yours wins.
@@ -0,0 +1,40 @@
1
+ {
2
+ "example": "import { AuthExpiryProvider } from \"@godxjp/ui/query\";\n\nexport function Providers({ children }: { children: React.ReactNode }) {\n return (\n <AuthExpiryProvider\n onAuthExpired={() => {\n const returnTo = encodeURIComponent(window.location.href);\n window.location.assign(`/auth/login?return_to=${returnTo}`);\n }}\n >\n {children}\n </AuthExpiryProvider>\n );\n}",
3
+ "group": "feedback",
4
+ "importPath": "@godxjp/ui/query",
5
+ "name": "AuthExpiryProvider",
6
+ "props": [
7
+ {
8
+ "description": "Called ONCE per expiry — any number of simultaneous 401s share one call; re-armed after every auth-errored view has gone. Typically `window.location.assign(loginUrl(returnTo = location.href))`. Return a promise for a silent refresh (then invalidate queries); a throw/rejection brings back the sign-in alert, whose button calls this again.",
9
+ "name": "onAuthExpired",
10
+ "required": true,
11
+ "type": "(context: { error: unknown }) => void | Promise<void>"
12
+ },
13
+ {
14
+ "description": "The app.",
15
+ "name": "children",
16
+ "type": "ReactNode"
17
+ }
18
+ ],
19
+ "related": [
20
+ "DataState / InfiniteQueryState / AlertMutationFeedback / AlertQueryError — the surfaces that consult this provider.",
21
+ "classifyQueryError — decides what is auth-class (401, or a status-less 'unauthenticated / access token invalid / token expired' message).",
22
+ "ErrorSurface — the whole-page error surface; a session expiry is neither a page error nor an inline one."
23
+ ],
24
+ "rules": [],
25
+ "storyPath": "query/DataState.stories.tsx",
26
+ "tagline": "App-root handler for an expired session (401 / invalid or expired token): every DataState / InfiniteQueryState / AlertMutationFeedback / AlertQueryError calls it automatically, once, and shows a neutral pending state instead of an error alert. Also exported from @godxjp/ui.",
27
+ "usage": [
28
+ "DO: mount it ONCE near the root, inside the QueryClientProvider/AppProvider, and redirect to the IdP with the current URL as the return target. The IdP returns immediately while its own session is alive — the SSO norm (Google/Microsoft apps, OIDC SPA guidance): no page-level error for an expired session.",
29
+ "DO: expect the surfaces to keep their `skeleton` (DataState/InfiniteQueryState) or a small muted status line (AlertMutationFeedback/AlertQueryError) with `role=\"status\"` announcing 'redirecting to sign-in' — never a destructive alert.",
30
+ "DON'T: wire `onAuthError` on every DataState as the session strategy — that renders an alert and waits for a click. It is the no-provider fallback only.",
31
+ "DON'T: use it for 403 — forbidden is a permission state, not an expired session; it still renders the access-guidance alert.",
32
+ "DO: when there is no provider the 401 fallback is a neutral (tone=default, role=status) sign-in alert capped at `--query-auth-alert-max-inline-size` (36rem, `none` = full width) — backward compatible, but proportionate."
33
+ ],
34
+ "useCases": [
35
+ "An SSO app (GoDX ID / OIDC) whose access token expired while the tab was idle: the first 401 redirects to the IdP and back to the same URL, no error panel flashes.",
36
+ "A dashboard with five DataStates that all 401 in the same tick: exactly one redirect.",
37
+ "A form submit (`AlertMutationFeedback`) that 401s: a muted 'redirecting to sign-in' line instead of a red alert.",
38
+ "A silent token refresh: return a promise that refreshes and then `queryClient.invalidateQueries()`; the views recover from skeleton without ever showing an error."
39
+ ]
40
+ }
@@ -28,7 +28,7 @@
28
28
  "type": "boolean"
29
29
  },
30
30
  {
31
- "description": "Recovery for 401 / expired-token errors: renew the session or sign in again. A 401 renders this action instead of Retry.",
31
+ "description": "Per-instance sign-in button for 401 / expired-token errors when NO `AuthExpiryProvider` is mounted (clicked by the user, never auto-invoked). Prefer `AuthExpiryProvider onAuthExpired` at the app root, which handles every 401 automatically and once (gh#1022).",
32
32
  "name": "onAuthError",
33
33
  "type": "() => void"
34
34
  },
@@ -64,6 +64,8 @@
64
64
  "DO: provide `empty` + `isEmpty` together when the data can legitimately return 0 items — e.g. `isEmpty={(d) => d.items.length === 0}` paired with `empty={<EmptyState title=\"…\" />}`. Omitting `empty` means an empty array still falls through to `children`, silently rendering a blank table.",
65
65
  "DON'T: wrap DataState in your own conditional — e.g. `{query.isSuccess && <DataState …>}`. DataState IS the conditional; the outer guard is redundant and breaks the retry/refetch skeleton.",
66
66
  "DON'T: use DataState for `useInfiniteQuery` results. The `query` prop type is `UseQueryResult<T>`, not `UseInfiniteQueryResult`. Use `InfiniteQueryState` (from `@godxjp/ui/query`) instead, which accepts `flatten` and renders a load-more footer.",
67
+ "DO: mount `AuthExpiryProvider onAuthExpired={…}` once at the app root (SSO apps: redirect to the IdP with the current URL as return target). A 401 then never paints an alert: the handler runs automatically, once for any number of simultaneous 401s, and DataState keeps its `skeleton` with a polite live region announcing the redirect (gh#1022).",
68
+ "DON'T: rely on per-page `onAuthError` for session expiry — it is only a click-to-sign-in button in the fallback alert (no provider). An expired session is an app-wide concern, not a page error.",
67
69
  "DO: classify errors by cause. Use session renewal/sign-in for 401, access guidance for 403, contextual correction for domain errors, and opt into showRetry only for transient network/5xx errors.",
68
70
  "DO: pass prerequisite for enabled:false queries. Pending + fetchStatus idle is unstarted, not loading, and never renders a skeleton.",
69
71
  "DO: rely on the localized, cause-specific error message — the raw backend/token/stack text is never shown. For a domain-specific message (e.g. a 422 field error) pass a custom errorRenderer.",
@@ -42,7 +42,7 @@
42
42
  "DO: Import from `@godxjp/ui/query` (not `@godxjp/ui`). Use the bundled `flattenItemPages` helper for any API that returns `{ items: T[] }` pages — it handles `undefined` data safely. Custom page shapes require a custom `flatten` function.",
43
43
  "DO: Always pass `skeleton` (e.g. `<SkeletonTable />` or `<SkeletonStat />`). It shows on initial `isPending`, on refetch-after-error, and whenever `data` is absent. Never show a blank area while loading.",
44
44
  "DO: Pass `empty` (an `<EmptyState>` node) to handle the zero-results case — without it the children render-prop is called with an empty array and you get a silent blank screen. Provide a custom `isEmpty` only when `TFlat` is not an array.",
45
- "DO: Let errors remain cause-aware. Retry is automatic only for classified transient/network/5xx failures. Unknown errors do not get a blind retry unless `showRetry` or `onRetry` is explicitly supplied; 401 routes to `onAuthError`, and raw backend/token text is never rendered.",
45
+ "DO: Let errors remain cause-aware. Retry is automatic only for classified transient/network/5xx failures. Unknown errors do not get a blind retry unless `showRetry` or `onRetry` is explicitly supplied; a 401 is handled by the app-root `AuthExpiryProvider` (auto, once, skeleton + live region) or, without one, by the `onAuthError` sign-in button; raw backend/token text is never rendered.",
46
46
  "DON'T: Hand-roll a load-more button. The component renders a default centered outline Button when `hasNextPage` is true. Override only via `loadMore` (custom node) or `showLoadMore={false}` (hide entirely). Never call `query.fetchNextPage()` outside the component for pagination.",
47
47
  "DON'T: Use `InfiniteQueryState` for a `useQuery` result — it expects `UseInfiniteQueryResult` shape (`pages`, `hasNextPage`, `fetchNextPage`, `isFetchingNextPage`). For regular `useQuery` use `DataState` instead.",
48
48
  "DON'T: Confuse the two generics: `TPage` is the raw page shape from the API, `TFlat` is what `flatten` returns (usually `TItem[]`). The `children` render-prop receives `TFlat`, not `TPage`. Pass `isEmpty` if `TFlat` is not a plain array so empty detection works correctly."
@@ -24,6 +24,11 @@
24
24
  "name": "onValueChange",
25
25
  "type": "(value: string) => void"
26
26
  },
27
+ {
28
+ "description": "Strip-only mode (gh#1021): the tabs drive content OUTSIDE the strip. No tabpanel is rendered, and the selected tab's `aria-controls` points at this id, which is the consumer's own region (give it `role=\"region\"` and a name). Content there is never remounted on a tab switch, so a typed draft survives. With the compound form it applies to triggers without a declared `TabsContent` (a declared panel wins). A pure filter with NO region to point at is not a tab strip; use `Segmented`. The root's `aria-label`/`aria-labelledby` name the `role=\"tablist\"` (gh#1020), and a compound `TabsTrigger` takes `count`/`overflowCount`/`showZero`/`countLabel`, drawn exactly like `items[].count`.",
29
+ "name": "controls",
30
+ "type": "IdProp"
31
+ },
27
32
  {
28
33
  "defaultValue": "\"default\"",
29
34
  "description": "Trigger-strip appearance — this is Ant Design's `type` under the library's own `variant` vocabulary. `default` is the pill strip (antd has no equivalent). `line` is UNDERLINE-ONLY: the selected trigger gets no ring or card border at all — only the token-owned 2px primary bar (--tabs-indicator-{background,size,offset}) — so the `:focus-visible` keyboard ring stays visible and clearly distinct from selection. `card` gives each tab a boxed face on a rail (--tabs-card-*). `editable-card` is `card` plus the add button and per-tab remove shortcut, and needs `onEdit` to do anything. With `items`, the variant is forwarded to the list; when composing manually, pass the same value to `<TabsList variant=\"line\">`.",
@@ -321,6 +321,11 @@
321
321
  "name": "DataState",
322
322
  "tagline": "TanStack Query lifecycle widget — skeleton / error / empty / success for one useQuery block. Import from @godxjp/ui/query."
323
323
  },
324
+ {
325
+ "group": "feedback",
326
+ "name": "AuthExpiryProvider",
327
+ "tagline": "App-root handler for an expired session (401 / invalid or expired token): every DataState / InfiniteQueryState / AlertMutationFeedback / AlertQueryError calls it automatically, once, and shows a neutral pending state instead of an error alert. Also exported from @godxjp/ui."
328
+ },
324
329
  {
325
330
  "group": "data-display",
326
331
  "name": "InfiniteQueryState",
@@ -5220,7 +5220,7 @@
5220
5220
  "type": "boolean"
5221
5221
  },
5222
5222
  {
5223
- "description": "Recovery for 401 / expired-token errors: renew the session or sign in again. A 401 renders this action instead of Retry.",
5223
+ "description": "Per-instance sign-in button for 401 / expired-token errors when NO `AuthExpiryProvider` is mounted (clicked by the user, never auto-invoked). Prefer `AuthExpiryProvider onAuthExpired` at the app root, which handles every 401 automatically and once (gh#1022).",
5224
5224
  "name": "onAuthError",
5225
5225
  "type": "() => void"
5226
5226
  },
@@ -5256,6 +5256,8 @@
5256
5256
  "DO: provide `empty` + `isEmpty` together when the data can legitimately return 0 items — e.g. `isEmpty={(d) => d.items.length === 0}` paired with `empty={<EmptyState title=\"…\" />}`. Omitting `empty` means an empty array still falls through to `children`, silently rendering a blank table.",
5257
5257
  "DON'T: wrap DataState in your own conditional — e.g. `{query.isSuccess && <DataState …>}`. DataState IS the conditional; the outer guard is redundant and breaks the retry/refetch skeleton.",
5258
5258
  "DON'T: use DataState for `useInfiniteQuery` results. The `query` prop type is `UseQueryResult<T>`, not `UseInfiniteQueryResult`. Use `InfiniteQueryState` (from `@godxjp/ui/query`) instead, which accepts `flatten` and renders a load-more footer.",
5259
+ "DO: mount `AuthExpiryProvider onAuthExpired={…}` once at the app root (SSO apps: redirect to the IdP with the current URL as return target). A 401 then never paints an alert: the handler runs automatically, once for any number of simultaneous 401s, and DataState keeps its `skeleton` with a polite live region announcing the redirect (gh#1022).",
5260
+ "DON'T: rely on per-page `onAuthError` for session expiry — it is only a click-to-sign-in button in the fallback alert (no provider). An expired session is an app-wide concern, not a page error.",
5259
5261
  "DO: classify errors by cause. Use session renewal/sign-in for 401, access guidance for 403, contextual correction for domain errors, and opt into showRetry only for transient network/5xx errors.",
5260
5262
  "DO: pass prerequisite for enabled:false queries. Pending + fetchStatus idle is unstarted, not loading, and never renders a skeleton.",
5261
5263
  "DO: rely on the localized, cause-specific error message — the raw backend/token/stack text is never shown. For a domain-specific message (e.g. a 422 field error) pass a custom errorRenderer.",
@@ -5269,6 +5271,46 @@
5269
5271
  "Any page using `useQuery` where the empty state and loading state are visually different — DataState enforces the correct visual for each phase without scattered `if` statements across the component tree."
5270
5272
  ]
5271
5273
  },
5274
+ {
5275
+ "example": "import { AuthExpiryProvider } from \"@godxjp/ui/query\";\n\nexport function Providers({ children }: { children: React.ReactNode }) {\n return (\n <AuthExpiryProvider\n onAuthExpired={() => {\n const returnTo = encodeURIComponent(window.location.href);\n window.location.assign(`/auth/login?return_to=${returnTo}`);\n }}\n >\n {children}\n </AuthExpiryProvider>\n );\n}",
5276
+ "group": "feedback",
5277
+ "importPath": "@godxjp/ui/query",
5278
+ "name": "AuthExpiryProvider",
5279
+ "props": [
5280
+ {
5281
+ "description": "Called ONCE per expiry — any number of simultaneous 401s share one call; re-armed after every auth-errored view has gone. Typically `window.location.assign(loginUrl(returnTo = location.href))`. Return a promise for a silent refresh (then invalidate queries); a throw/rejection brings back the sign-in alert, whose button calls this again.",
5282
+ "name": "onAuthExpired",
5283
+ "required": true,
5284
+ "type": "(context: { error: unknown }) => void | Promise<void>"
5285
+ },
5286
+ {
5287
+ "description": "The app.",
5288
+ "name": "children",
5289
+ "type": "ReactNode"
5290
+ }
5291
+ ],
5292
+ "related": [
5293
+ "DataState / InfiniteQueryState / AlertMutationFeedback / AlertQueryError — the surfaces that consult this provider.",
5294
+ "classifyQueryError — decides what is auth-class (401, or a status-less 'unauthenticated / access token invalid / token expired' message).",
5295
+ "ErrorSurface — the whole-page error surface; a session expiry is neither a page error nor an inline one."
5296
+ ],
5297
+ "rules": [],
5298
+ "storyPath": "query/DataState.stories.tsx",
5299
+ "tagline": "App-root handler for an expired session (401 / invalid or expired token): every DataState / InfiniteQueryState / AlertMutationFeedback / AlertQueryError calls it automatically, once, and shows a neutral pending state instead of an error alert. Also exported from @godxjp/ui.",
5300
+ "usage": [
5301
+ "DO: mount it ONCE near the root, inside the QueryClientProvider/AppProvider, and redirect to the IdP with the current URL as the return target. The IdP returns immediately while its own session is alive — the SSO norm (Google/Microsoft apps, OIDC SPA guidance): no page-level error for an expired session.",
5302
+ "DO: expect the surfaces to keep their `skeleton` (DataState/InfiniteQueryState) or a small muted status line (AlertMutationFeedback/AlertQueryError) with `role=\"status\"` announcing 'redirecting to sign-in' — never a destructive alert.",
5303
+ "DON'T: wire `onAuthError` on every DataState as the session strategy — that renders an alert and waits for a click. It is the no-provider fallback only.",
5304
+ "DON'T: use it for 403 — forbidden is a permission state, not an expired session; it still renders the access-guidance alert.",
5305
+ "DO: when there is no provider the 401 fallback is a neutral (tone=default, role=status) sign-in alert capped at `--query-auth-alert-max-inline-size` (36rem, `none` = full width) — backward compatible, but proportionate."
5306
+ ],
5307
+ "useCases": [
5308
+ "An SSO app (GoDX ID / OIDC) whose access token expired while the tab was idle: the first 401 redirects to the IdP and back to the same URL, no error panel flashes.",
5309
+ "A dashboard with five DataStates that all 401 in the same tick: exactly one redirect.",
5310
+ "A form submit (`AlertMutationFeedback`) that 401s: a muted 'redirecting to sign-in' line instead of a red alert.",
5311
+ "A silent token refresh: return a promise that refreshes and then `queryClient.invalidateQueries()`; the views recover from skeleton without ever showing an error."
5312
+ ]
5313
+ },
5272
5314
  {
5273
5315
  "example": "import { useInfiniteQuery } from \"@tanstack/react-query\";\nimport { InfiniteQueryState, flattenItemPages } from \"@godxjp/ui/query\";\n\ntype Activity = { id: string; label: string };\n\n// `flattenItemPages` constrains the page to `{ items: TItem[] }`, so the query must be typed:\n// an untyped one makes the page `unknown`, which cannot satisfy that constraint.\nconst q = useInfiniteQuery<{ items: Activity[]; cursor?: string }>({\n queryKey: [\"activity\"],\n queryFn: fetchActivityPage,\n initialPageParam: undefined,\n getNextPageParam: (last) => last.cursor,\n});\n\n<InfiniteQueryState query={q} skeleton={<SkeletonRows />} flatten={flattenItemPages} isEmpty={(it) => it.length === 0}>\n {(items) => items.map((a) => <ActivityRow key={a.id} activity={a} />)}\n</InfiniteQueryState>",
5274
5316
  "group": "data-display",
@@ -5313,7 +5355,7 @@
5313
5355
  "DO: Import from `@godxjp/ui/query` (not `@godxjp/ui`). Use the bundled `flattenItemPages` helper for any API that returns `{ items: T[] }` pages — it handles `undefined` data safely. Custom page shapes require a custom `flatten` function.",
5314
5356
  "DO: Always pass `skeleton` (e.g. `<SkeletonTable />` or `<SkeletonStat />`). It shows on initial `isPending`, on refetch-after-error, and whenever `data` is absent. Never show a blank area while loading.",
5315
5357
  "DO: Pass `empty` (an `<EmptyState>` node) to handle the zero-results case — without it the children render-prop is called with an empty array and you get a silent blank screen. Provide a custom `isEmpty` only when `TFlat` is not an array.",
5316
- "DO: Let errors remain cause-aware. Retry is automatic only for classified transient/network/5xx failures. Unknown errors do not get a blind retry unless `showRetry` or `onRetry` is explicitly supplied; 401 routes to `onAuthError`, and raw backend/token text is never rendered.",
5358
+ "DO: Let errors remain cause-aware. Retry is automatic only for classified transient/network/5xx failures. Unknown errors do not get a blind retry unless `showRetry` or `onRetry` is explicitly supplied; a 401 is handled by the app-root `AuthExpiryProvider` (auto, once, skeleton + live region) or, without one, by the `onAuthError` sign-in button; raw backend/token text is never rendered.",
5317
5359
  "DON'T: Hand-roll a load-more button. The component renders a default centered outline Button when `hasNextPage` is true. Override only via `loadMore` (custom node) or `showLoadMore={false}` (hide entirely). Never call `query.fetchNextPage()` outside the component for pagination.",
5318
5360
  "DON'T: Use `InfiniteQueryState` for a `useQuery` result — it expects `UseInfiniteQueryResult` shape (`pages`, `hasNextPage`, `fetchNextPage`, `isFetchingNextPage`). For regular `useQuery` use `DataState` instead.",
5319
5361
  "DON'T: Confuse the two generics: `TPage` is the raw page shape from the API, `TFlat` is what `flatten` returns (usually `TItem[]`). The `children` render-prop receives `TFlat`, not `TPage`. Pass `isEmpty` if `TFlat` is not a plain array so empty detection works correctly."
@@ -7838,6 +7880,11 @@
7838
7880
  "name": "onValueChange",
7839
7881
  "type": "(value: string) => void"
7840
7882
  },
7883
+ {
7884
+ "description": "Strip-only mode (gh#1021): the tabs drive content OUTSIDE the strip. No tabpanel is rendered, and the selected tab's `aria-controls` points at this id, which is the consumer's own region (give it `role=\"region\"` and a name). Content there is never remounted on a tab switch, so a typed draft survives. With the compound form it applies to triggers without a declared `TabsContent` (a declared panel wins). A pure filter with NO region to point at is not a tab strip; use `Segmented`. The root's `aria-label`/`aria-labelledby` name the `role=\"tablist\"` (gh#1020), and a compound `TabsTrigger` takes `count`/`overflowCount`/`showZero`/`countLabel`, drawn exactly like `items[].count`.",
7885
+ "name": "controls",
7886
+ "type": "IdProp"
7887
+ },
7841
7888
  {
7842
7889
  "defaultValue": "\"default\"",
7843
7890
  "description": "Trigger-strip appearance — this is Ant Design's `type` under the library's own `variant` vocabulary. `default` is the pill strip (antd has no equivalent). `line` is UNDERLINE-ONLY: the selected trigger gets no ring or card border at all — only the token-owned 2px primary bar (--tabs-indicator-{background,size,offset}) — so the `:focus-visible` keyboard ring stays visible and clearly distinct from selection. `card` gives each tab a boxed face on a rail (--tabs-card-*). `editable-card` is `card` plus the add button and per-tab remove shortcut, and needs `onEdit` to do anything. With `items`, the variant is forwarded to the list; when composing manually, pass the same value to `<TabsList variant=\"line\">`.",
package/agent/index.json CHANGED
@@ -1,16 +1,16 @@
1
1
  {
2
2
  "counts": {
3
3
  "anti-ai-tells": 26,
4
- "components": 175,
4
+ "components": 176,
5
5
  "patterns": 21,
6
6
  "rules": 50,
7
- "tokens": 2074,
7
+ "tokens": 2080,
8
8
  "vocabulary": 14
9
9
  },
10
10
  "files": [
11
11
  {
12
12
  "file": "components-index.json",
13
- "note": "46 KB — name + group + tagline for all 175. FETCH THIS FIRST, then fetch only the components you chose.",
13
+ "note": "47 KB — name + group + tagline for all 176. FETCH THIS FIRST, then fetch only the components you chose.",
14
14
  "url": "https://raw.githubusercontent.com/godx-jp/godxjp-ui/main/agent/components-index.json"
15
15
  },
16
16
  {
@@ -48,19 +48,19 @@
48
48
  "note": "Pin to the tag that matches the @godxjp/ui version you installed. A catalog newer than your package describes props you do not have; older, and it hides props you do.",
49
49
  "read": {
50
50
  "live": "https://raw.githubusercontent.com/godx-jp/godxjp-ui/main/agent/index.json",
51
- "pinned": "https://raw.githubusercontent.com/godx-jp/godxjp-ui/v31.2.0/agent/index.json"
51
+ "pinned": "https://raw.githubusercontent.com/godx-jp/godxjp-ui/v31.4.0/agent/index.json"
52
52
  },
53
53
  "source": "mcp/src/data — the same data @godxjp/ui-mcp serves — plus the foundation and semantic token tiers, read from src/tokens/*.css",
54
54
  "start": "https://raw.githubusercontent.com/godx-jp/godxjp-ui/main/agent/START-HERE.md",
55
55
  "tokenTiers": {
56
56
  "component": "per-part knobs, --{component}-{part}-{property}; usually leave these alone",
57
57
  "counts": {
58
- "component": 1760,
58
+ "component": 1765,
59
59
  "foundation": 211,
60
- "semantic": 103
60
+ "semantic": 104
61
61
  },
62
62
  "foundation": "the seeds a consumer is invited to set — --primary, --background, --radius",
63
63
  "semantic": "named roles that follow the seeds — --ring, --text-link, --overlay-background"
64
64
  },
65
- "version": "31.2.0"
65
+ "version": "31.4.0"
66
66
  }
package/agent/llms.txt CHANGED
@@ -1,10 +1,10 @@
1
1
  # @godxjp/ui
2
2
 
3
- > A Japanese-enterprise React design system: 175 components, 2074 design tokens,
4
- > 50 cardinal rules. This file is the entry point for AI agents. Catalog version 31.2.0.
3
+ > A Japanese-enterprise React design system: 176 components, 2080 design tokens,
4
+ > 50 cardinal rules. This file is the entry point for AI agents. Catalog version 31.4.0.
5
5
 
6
6
  If your client can run a process, do not read these files — run the MCP server instead
7
- (`npx @godxjp/ui-mcp@31.2.0`). It is searchable and version-locked. These files exist for agents
7
+ (`npx @godxjp/ui-mcp@31.4.0`). It is searchable and version-locked. These files exist for agents
8
8
  that can only fetch URLs.
9
9
 
10
10
  ## Start
@@ -15,10 +15,10 @@ that can only fetch URLs.
15
15
  ## Catalog
16
16
 
17
17
  - [patterns-index.json](https://raw.githubusercontent.com/godx-jp/godxjp-ui/main/agent/patterns-index.json): 21 whole-task patterns (name, tagline, tags). Start here when the task is a TASK — "build a settings page" — then fetch `patterns/<name>.json` for complete code.
18
- - [components-index.json](https://raw.githubusercontent.com/godx-jp/godxjp-ui/main/agent/components-index.json): 46 KB — all 175 components as name, group, tagline, plus `absorbed`: the names that do NOT exist and map to it (`Combobox` → `Select`).
18
+ - [components-index.json](https://raw.githubusercontent.com/godx-jp/godxjp-ui/main/agent/components-index.json): 47 KB — all 176 components as name, group, tagline, plus `absorbed`: the names that do NOT exist and map to it (`Combobox` → `Select`).
19
19
  - [components/&lt;Name&gt;.json](https://raw.githubusercontent.com/godx-jp/godxjp-ui/main/agent/components/Select.json): one file per component (1 KB–35 KB, median 6 KB). Read the index, then fetch only the ones you chose — this is the selective route, and the reason you do not need the blob.
20
20
  - [components.json](https://raw.githubusercontent.com/godx-jp/godxjp-ui/main/agent/components.json): 1.2 MB — every entry in one file. Most URL fetchers truncate a response this size without saying so; prefer the per-component files.
21
- - [tokens.json](https://raw.githubusercontent.com/godx-jp/godxjp-ui/main/agent/tokens.json): every design token with its value, the reason it exists, and its `tier` — 211 `foundation` seeds (`--primary`, `--background`, `--radius`: set these when you are handed a brand), 103 `semantic` roles, 1760 `component` knobs.
21
+ - [tokens.json](https://raw.githubusercontent.com/godx-jp/godxjp-ui/main/agent/tokens.json): every design token with its value, the reason it exists, and its `tier` — 211 `foundation` seeds (`--primary`, `--background`, `--radius`: set these when you are handed a brand), 104 `semantic` roles, 1765 `component` knobs.
22
22
  - [vocabulary.json](https://raw.githubusercontent.com/godx-jp/godxjp-ui/main/agent/vocabulary.json): the controlled prop vocabulary — which prop name means what, across every component.
23
23
  - [rules.json](https://raw.githubusercontent.com/godx-jp/godxjp-ui/main/agent/rules.json): 50 cardinal rules.
24
24
  - [anti-ai-tells.json](https://raw.githubusercontent.com/godx-jp/godxjp-ui/main/agent/anti-ai-tells.json): 26 shapes that make generated UI look generated, each with its fix.
@@ -26,7 +26,7 @@ that can only fetch URLs.
26
26
  ## Pinning
27
27
 
28
28
  Every URL above tracks `main`. To pin to the release a project actually installed, swap `main` for
29
- the tag: `.../godx-jp/godxjp-ui/v31.2.0/agent/...`. A catalog that does not match the installed
29
+ the tag: `.../godx-jp/godxjp-ui/v31.4.0/agent/...`. A catalog that does not match the installed
30
30
  package describes props that are absent, or hides props that are present, and says nothing either way.
31
31
 
32
32
  Pinned catalogs only exist for releases whose tag actually contains `agent/`. If `…/v<version>/agent/index.json` returns 404, that release predates this catalog: read `…/main/…` instead and compare `index.json` → `version` against the package you have, so you at least know which way it drifted.
@@ -4,7 +4,7 @@
4
4
  "data-state",
5
5
  "query-states"
6
6
  ],
7
- "code": "import { DataState, classifyQueryError } from \"@godxjp/ui/query\";\nimport { EmptyState } from \"@godxjp/ui/data-display\";\nimport { SkeletonTable } from \"@godxjp/ui/feedback\";\nimport { Building2, Inbox } from \"lucide-react\";\n\n// ONE primary state renders at a time; DataState makes them mutually exclusive:\n// prerequisite → skeleton(loading) → data → empty → error (never two at once).\n//\n// enabled:false is PREREQUISITE/idle, NOT loading. TanStack reports isPending while a query is\n// disabled, but fetchStatus stays \"idle\" (no request in flight) — DataState renders the prerequisite\n// slot, so a disabled query shows an instruction, NOT an endless skeleton.\n// const query = useQuery({ queryKey: [\"members\", orgId], queryFn, enabled: Boolean(orgId) });\n//\n// A background refetch over EXISTING data keeps the content on screen (no skeleton flash) and\n// announces the busy state politely — stale/placeholder refresh is handled for you.\n//\n// Errors are classified by CAUSE, not blanket-retried:\n// auth (401) → onAuthError: renew session / sign in again (NOT a retry)\n// forbidden (403) → permission message + access path (no retry)\n// notFound (404) → contextual not-found (no retry)\n// validation (400/422)→ corrective guidance (no retry)\n// transient (408/429/5xx/network) → Retry offered automatically\n// unknown → neutral; opt into Retry via showRetry/onRetry only if it can help\nexport function MembersPanel({ query, orgId, onSignIn }: {\n query: any; orgId?: string; onSignIn: () => void;\n}) {\n return (\n <DataState\n query={query}\n prerequisite={<EmptyState icon={Building2} variant=\"section\" title=\"組織を選択してください\"\n description=\"メンバーを表示するには、上のセレクタで組織を選びます。\" />}\n skeleton={<SkeletonTable rows={8} columns={4} />}\n empty={<EmptyState icon={Inbox} variant=\"section\" title=\"メンバーがいません\"\n description=\"この組織にはまだメンバーが登録されていません。\" />}\n isEmpty={(data) => data.items.length === 0}\n onAuthError={onSignIn}\n >\n {(data) => <MemberTable items={data.items} />}\n </DataState>\n );\n}\n\n// Need a bespoke error surface? Pass errorRenderer and branch on the classified category — the\n// default detail is always a localized message (never raw token / endpoint / stack text):\n// errorRenderer={(error, retry) => {\n// const { category } = classifyQueryError(error);\n// if (category === \"auth\") return <SessionExpired onRenew={onSignIn} />;\n// if (category === \"forbidden\") return <NoAccess />;\n// if (category === \"transient\") return <Retryable onRetry={retry} />;\n// return <GenericError />;\n// }}\n// RULE: pagination/footer chrome NEVER renders outside the populated-data branch (see data-table-page).",
7
+ "code": "import { DataState, classifyQueryError } from \"@godxjp/ui/query\";\nimport { EmptyState } from \"@godxjp/ui/data-display\";\nimport { SkeletonTable } from \"@godxjp/ui/feedback\";\nimport { Building2, Inbox } from \"lucide-react\";\n\n// ONE primary state renders at a time; DataState makes them mutually exclusive:\n// prerequisite → skeleton(loading) → data → empty → error (never two at once).\n//\n// enabled:false is PREREQUISITE/idle, NOT loading. TanStack reports isPending while a query is\n// disabled, but fetchStatus stays \"idle\" (no request in flight) — DataState renders the prerequisite\n// slot, so a disabled query shows an instruction, NOT an endless skeleton.\n// const query = useQuery({ queryKey: [\"members\", orgId], queryFn, enabled: Boolean(orgId) });\n//\n// A background refetch over EXISTING data keeps the content on screen (no skeleton flash) and\n// announces the busy state politely — stale/placeholder refresh is handled for you.\n//\n// Errors are classified by CAUSE, not blanket-retried:\n// auth (401) → handled app-wide by <AuthExpiryProvider onAuthExpired> at the root:\n// auto-redirect to sign-in ONCE, skeleton stays (NOT a retry, NOT an alert)\n// forbidden (403) → permission message + access path (no retry)\n// notFound (404) → contextual not-found (no retry)\n// validation (400/422)→ corrective guidance (no retry)\n// transient (408/429/5xx/network) → Retry offered automatically\n// unknown → neutral; opt into Retry via showRetry/onRetry only if it can help\n// App root (once): <AuthExpiryProvider onAuthExpired={() => redirectToSignIn(location.href)}>\nexport function MembersPanel({ query, orgId }: { query: any; orgId?: string }) {\n return (\n <DataState\n query={query}\n prerequisite={<EmptyState icon={Building2} variant=\"section\" title=\"組織を選択してください\"\n description=\"メンバーを表示するには、上のセレクタで組織を選びます。\" />}\n skeleton={<SkeletonTable rows={8} columns={4} />}\n empty={<EmptyState icon={Inbox} variant=\"section\" title=\"メンバーがいません\"\n description=\"この組織にはまだメンバーが登録されていません。\" />}\n isEmpty={(data) => data.items.length === 0}\n >\n {(data) => <MemberTable items={data.items} />}\n </DataState>\n );\n}\n\n// Need a bespoke error surface? Pass errorRenderer and branch on the classified category — the\n// default detail is always a localized message (never raw token / endpoint / stack text):\n// errorRenderer={(error, retry) => {\n// const { category } = classifyQueryError(error);\n// if (category === \"forbidden\") return <NoAccess />;\n// if (category === \"transient\") return <Retryable onRetry={retry} />;\n// return <GenericError />;\n// }}\n// RULE: pagination/footer chrome NEVER renders outside the populated-data branch (see data-table-page).",
8
8
  "name": "async-data-state",
9
9
  "tagline": "The full async state machine — ONE primary state at a time: prerequisite, disabled-vs-loading, stale refresh, populated, real-empty, and cause-aware error (401/403/404/422/transient) with correct recovery.",
10
10
  "tags": [
@@ -182,7 +182,7 @@
182
182
  "data-state",
183
183
  "query-states"
184
184
  ],
185
- "code": "import { DataState, classifyQueryError } from \"@godxjp/ui/query\";\nimport { EmptyState } from \"@godxjp/ui/data-display\";\nimport { SkeletonTable } from \"@godxjp/ui/feedback\";\nimport { Building2, Inbox } from \"lucide-react\";\n\n// ONE primary state renders at a time; DataState makes them mutually exclusive:\n// prerequisite → skeleton(loading) → data → empty → error (never two at once).\n//\n// enabled:false is PREREQUISITE/idle, NOT loading. TanStack reports isPending while a query is\n// disabled, but fetchStatus stays \"idle\" (no request in flight) — DataState renders the prerequisite\n// slot, so a disabled query shows an instruction, NOT an endless skeleton.\n// const query = useQuery({ queryKey: [\"members\", orgId], queryFn, enabled: Boolean(orgId) });\n//\n// A background refetch over EXISTING data keeps the content on screen (no skeleton flash) and\n// announces the busy state politely — stale/placeholder refresh is handled for you.\n//\n// Errors are classified by CAUSE, not blanket-retried:\n// auth (401) → onAuthError: renew session / sign in again (NOT a retry)\n// forbidden (403) → permission message + access path (no retry)\n// notFound (404) → contextual not-found (no retry)\n// validation (400/422)→ corrective guidance (no retry)\n// transient (408/429/5xx/network) → Retry offered automatically\n// unknown → neutral; opt into Retry via showRetry/onRetry only if it can help\nexport function MembersPanel({ query, orgId, onSignIn }: {\n query: any; orgId?: string; onSignIn: () => void;\n}) {\n return (\n <DataState\n query={query}\n prerequisite={<EmptyState icon={Building2} variant=\"section\" title=\"組織を選択してください\"\n description=\"メンバーを表示するには、上のセレクタで組織を選びます。\" />}\n skeleton={<SkeletonTable rows={8} columns={4} />}\n empty={<EmptyState icon={Inbox} variant=\"section\" title=\"メンバーがいません\"\n description=\"この組織にはまだメンバーが登録されていません。\" />}\n isEmpty={(data) => data.items.length === 0}\n onAuthError={onSignIn}\n >\n {(data) => <MemberTable items={data.items} />}\n </DataState>\n );\n}\n\n// Need a bespoke error surface? Pass errorRenderer and branch on the classified category — the\n// default detail is always a localized message (never raw token / endpoint / stack text):\n// errorRenderer={(error, retry) => {\n// const { category } = classifyQueryError(error);\n// if (category === \"auth\") return <SessionExpired onRenew={onSignIn} />;\n// if (category === \"forbidden\") return <NoAccess />;\n// if (category === \"transient\") return <Retryable onRetry={retry} />;\n// return <GenericError />;\n// }}\n// RULE: pagination/footer chrome NEVER renders outside the populated-data branch (see data-table-page).",
185
+ "code": "import { DataState, classifyQueryError } from \"@godxjp/ui/query\";\nimport { EmptyState } from \"@godxjp/ui/data-display\";\nimport { SkeletonTable } from \"@godxjp/ui/feedback\";\nimport { Building2, Inbox } from \"lucide-react\";\n\n// ONE primary state renders at a time; DataState makes them mutually exclusive:\n// prerequisite → skeleton(loading) → data → empty → error (never two at once).\n//\n// enabled:false is PREREQUISITE/idle, NOT loading. TanStack reports isPending while a query is\n// disabled, but fetchStatus stays \"idle\" (no request in flight) — DataState renders the prerequisite\n// slot, so a disabled query shows an instruction, NOT an endless skeleton.\n// const query = useQuery({ queryKey: [\"members\", orgId], queryFn, enabled: Boolean(orgId) });\n//\n// A background refetch over EXISTING data keeps the content on screen (no skeleton flash) and\n// announces the busy state politely — stale/placeholder refresh is handled for you.\n//\n// Errors are classified by CAUSE, not blanket-retried:\n// auth (401) → handled app-wide by <AuthExpiryProvider onAuthExpired> at the root:\n// auto-redirect to sign-in ONCE, skeleton stays (NOT a retry, NOT an alert)\n// forbidden (403) → permission message + access path (no retry)\n// notFound (404) → contextual not-found (no retry)\n// validation (400/422)→ corrective guidance (no retry)\n// transient (408/429/5xx/network) → Retry offered automatically\n// unknown → neutral; opt into Retry via showRetry/onRetry only if it can help\n// App root (once): <AuthExpiryProvider onAuthExpired={() => redirectToSignIn(location.href)}>\nexport function MembersPanel({ query, orgId }: { query: any; orgId?: string }) {\n return (\n <DataState\n query={query}\n prerequisite={<EmptyState icon={Building2} variant=\"section\" title=\"組織を選択してください\"\n description=\"メンバーを表示するには、上のセレクタで組織を選びます。\" />}\n skeleton={<SkeletonTable rows={8} columns={4} />}\n empty={<EmptyState icon={Inbox} variant=\"section\" title=\"メンバーがいません\"\n description=\"この組織にはまだメンバーが登録されていません。\" />}\n isEmpty={(data) => data.items.length === 0}\n >\n {(data) => <MemberTable items={data.items} />}\n </DataState>\n );\n}\n\n// Need a bespoke error surface? Pass errorRenderer and branch on the classified category — the\n// default detail is always a localized message (never raw token / endpoint / stack text):\n// errorRenderer={(error, retry) => {\n// const { category } = classifyQueryError(error);\n// if (category === \"forbidden\") return <NoAccess />;\n// if (category === \"transient\") return <Retryable onRetry={retry} />;\n// return <GenericError />;\n// }}\n// RULE: pagination/footer chrome NEVER renders outside the populated-data branch (see data-table-page).",
186
186
  "name": "async-data-state",
187
187
  "tagline": "The full async state machine — ONE primary state at a time: prerequisite, disabled-vs-loading, stale refresh, populated, real-empty, and cause-aware error (401/403/404/422/transient) with correct recovery.",
188
188
  "tags": [
package/agent/tokens.json CHANGED
@@ -1625,6 +1625,12 @@
1625
1625
  "tier": "semantic",
1626
1626
  "value": "none"
1627
1627
  },
1628
+ {
1629
+ "description": "The widest share of the header row the actions (`extra`) keep on ONE row before they wrap (gh#1025); the title keeps the rest. `initial` — default 60% at the call site.",
1630
+ "name": "--page-header-extra-max-measure",
1631
+ "tier": "semantic",
1632
+ "value": "initial"
1633
+ },
1628
1634
  {
1629
1635
  "description": "PageContainer `toolbar` — the fixed chrome band between the header and the (scrolling) body. ALL THREE knobs are quiet by default (rule #44): background — `transparent`, i.e. the band shows the page ground exactly as it did before the knob existed. It is bound at `:root` and NOT `initial`, unlike the divider below, because the default is a plain CSS keyword rather than another role token: there is nothing for a scoped [data-tenant]/.dark override to re-resolve, so the `initial` + call-site-fallback dance would buy nothing and only hide the default from anyone reading this file. A service that wants the band to read as its own surface — the design that asked for this knob puts a chat channel's workflow rail on the card ground — sets `--page-toolbar-background: hsl(var(--card));` ONCE in its theme. Never `className=\"bg-card\"` at the call site: that is hand-laid page chrome, it paints only under `variant=\"flush\"` gutters correctly by accident, and it cannot be re-themed per tenant. pad-block 0 — the band's ONLY breathing room, and the only one it should ever have. The band is chrome, so it sits FLUSH against the header above and the body below (src/styles/layout.css cancels the container's --page-band-gap from the band itself); there is deliberately no outside space to tune, because a ruled, painted band floating in a void divides nothing. A service that opts into the background or the divider gives the band its inset HERE (`--page-toolbar-pad-block: var(--space-2)`) instead of padding the strip at the call site. Still 0 by default: a transparent band is not a surface and has no inside to breathe, and under `fill` every pixel of it comes straight off the scroll viewport the slot exists to protect. divider — declared `initial` so the CALL SITE can fall back to --page-header-divider (`var(--page-toolbar-divider, var(--page-header-divider))`). One theme declaration therefore rules the whole page chrome consistently, and a scoped [data-tenant]/.dark override of the header divider still reaches the band — a `:root` binding would freeze it at the :root value (docs/TOKENS.md · \"Role-mirror knobs MUST be `initial`\"). Set `--page-toolbar-divider: none` to silence just this band.",
1630
1636
  "name": "--page-toolbar-background",
@@ -6983,6 +6989,12 @@
6983
6989
  "tier": "component",
6984
6990
  "value": "var(--space-stack-sm)"
6985
6991
  },
6992
+ {
6993
+ "description": "The sign-in fallback a query surface paints for an expired session when no AuthExpiryProvider handles it (gh#1022). An expired session is an expected, recoverable condition, so it is capped at a reading measure instead of spanning the page body its DataState wraps. `none` restores the full-width alert.",
6994
+ "name": "--query-auth-alert-max-inline-size",
6995
+ "tier": "component",
6996
+ "value": "36rem"
6997
+ },
6986
6998
  {
6987
6999
  "description": "TOOLTIP — the transient label surface. Every constant here was a Tailwind literal baked into the component (`max-w-xs px-2 py-1 rounded-md text-xs shadow-md`), so a service could not retune tooltip density or measure without forking the component (rule #45). Defaults reproduce the previous look exactly, so adopting this changes nothing until a theme opts in.",
6988
7000
  "name": "--tooltip-max-width",
@@ -9378,7 +9390,7 @@
9378
9390
  "value": "initial"
9379
9391
  },
9380
9392
  {
9381
- "description": "Counter pill beside the label (gh#602). Geometry matches Button/Toggle count pills; colour does not. A Badge `secondary` fill is `--muted`, which is byte-identical to `--segmented-track-background`, so a consumer-nested Badge is 1.00:1 on unselected items. The pill therefore wears an opaque `--primary` / `--primary-foreground` pair that clears WCAG 2.2 SC 1.4.11 against both the recessed track (`--muted`) and the lifted slab (`--background`) in light and dark — measured in segmented-count-contrast.test.ts. antd has no `count` on SegmentedItemType; this knob exists because every faceted filter in the wild needs one and nesting Badge in `label` was the only API until now.",
9393
+ "description": "Counter pill beside the label (gh#602). Geometry matches Button/Toggle count pills; COLOUR follows `Tabs` (gh#1019): resting = `--background` / `--muted-foreground`, byte-identical to a resting `.ui-tabs-count` on a CARD tab — the Tabs rule for a pill whose host surface is `--muted`, which the Segmented track is (a `--muted` pill there is 1.00:1, shape gone). The selected segment's pill = `--primary` / `--primary-foreground`, identical to the active tab's. gh#602 had painted EVERY pill primary — four identical blobs that dropped the one thing a count pill in a one-of-N control can say, \"this is the chosen one\". The resting number is held to SC 1.4.3 (≥ 4.5:1 in both themes, measured in segmented-count-tabs-convention-1019.test.ts); it is not the selection indicator, so its fill only has to stand off the track, not reach 3:1. antd has no `count` on SegmentedItemType; this knob exists because every faceted filter in the wild needs one and nesting Badge in `label` was the only API until now.",
9382
9394
  "name": "--segmented-count-min-width",
9383
9395
  "tier": "component",
9384
9396
  "value": "var(--button-count-min-width)"
@@ -9408,17 +9420,41 @@
9408
9420
  "value": "var(--space-1)"
9409
9421
  },
9410
9422
  {
9411
- "description": "Segmented (one-of-N control) component tokens. EVERY VALUE BELOW IS A PORT OF AN ESTABLISHED SEGMENTED SPEC, NOT A CHOICE. The geometry and the role assignments come from the widely-implemented enterprise Segmented control, transcribed as ratios rather than as pixels: track padding: the bold line width → 2px track background: the page's recessed neutral item colour: the label ink · hover / selected colour: the body ink item hover fill: the lighter neutral · item active fill: the heavier neutral item selected fill: the elevated surface label height = control height − track padding × 2 → 32 − 4 = 28 label padding-inline = control padding-x − border width → 12 − 1 = 11 icon gap = the small margin step / 2 → 6 THE ROLE PORTS ARE THE ONES `Tabs` ALREADY MADE. TabsList is `bg-muted` and an active TabsTrigger is `bg-background` + `shadow-sm` — the same track-and-slab pair, so a Segmented and a default Tabs strip read as one control family in both themes rather than two near-misses. That matters more than reproducing the source dark ramp literally: that ramp's dark track is BLACK with a lighter slab on it, while this palette's dark `--muted` sits above `--background`, so a literal port would invert the pairing relative to every Tabs strip on the same page. NOT ONE OF THEM READS THE BRAND SEED. The spec's own derivation produces identical Segmented values for its default seed and for this system's `#0071bd` — the control is neutral by construction, and the only brand ink it can carry is the focus mark, which styles/focus-ring.css owns. So the ports below are ROLE references (the same neutrals this system already names), never copied hex. The disabled state is the ONE place this departs. The source recolours to a disabled ink and changes nothing else; this system disables every control with the one `--disabled-opacity` knob, and a single library-wide answer outranks a per-component one. The other thing NOT ported is the sliding thumb: it exists to animate between items, and the implementations that have one remove the `-item-selected` class while it runs, which means the selected state lives in two places at once. A static selected slab reads identically at rest and cannot desynchronise.",
9423
+ "description": "default = hsl(var(--background)) at the call site",
9412
9424
  "name": "--segmented-count-background",
9413
9425
  "tier": "component",
9414
9426
  "value": "initial"
9415
9427
  },
9416
9428
  {
9417
- "description": "Segmented (one-of-N control) component tokens. EVERY VALUE BELOW IS A PORT OF AN ESTABLISHED SEGMENTED SPEC, NOT A CHOICE. The geometry and the role assignments come from the widely-implemented enterprise Segmented control, transcribed as ratios rather than as pixels: track padding: the bold line width → 2px track background: the page's recessed neutral item colour: the label ink · hover / selected colour: the body ink item hover fill: the lighter neutral · item active fill: the heavier neutral item selected fill: the elevated surface label height = control height − track padding × 2 → 32 − 4 = 28 label padding-inline = control padding-x − border width → 12 − 1 = 11 icon gap = the small margin step / 2 → 6 THE ROLE PORTS ARE THE ONES `Tabs` ALREADY MADE. TabsList is `bg-muted` and an active TabsTrigger is `bg-background` + `shadow-sm` — the same track-and-slab pair, so a Segmented and a default Tabs strip read as one control family in both themes rather than two near-misses. That matters more than reproducing the source dark ramp literally: that ramp's dark track is BLACK with a lighter slab on it, while this palette's dark `--muted` sits above `--background`, so a literal port would invert the pairing relative to every Tabs strip on the same page. NOT ONE OF THEM READS THE BRAND SEED. The spec's own derivation produces identical Segmented values for its default seed and for this system's `#0071bd` — the control is neutral by construction, and the only brand ink it can carry is the focus mark, which styles/focus-ring.css owns. So the ports below are ROLE references (the same neutrals this system already names), never copied hex. The disabled state is the ONE place this departs. The source recolours to a disabled ink and changes nothing else; this system disables every control with the one `--disabled-opacity` knob, and a single library-wide answer outranks a per-component one. The other thing NOT ported is the sliding thumb: it exists to animate between items, and the implementations that have one remove the `-item-selected` class while it runs, which means the selected state lives in two places at once. A static selected slab reads identically at rest and cannot desynchronise.",
9429
+ "description": "default = hsl(var(--muted-foreground)) at the call site",
9418
9430
  "name": "--segmented-count-color",
9419
9431
  "tier": "component",
9420
9432
  "value": "initial"
9421
9433
  },
9434
+ {
9435
+ "description": "default = hsl(var(--primary)) at the call site",
9436
+ "name": "--segmented-count-selected-background",
9437
+ "tier": "component",
9438
+ "value": "initial"
9439
+ },
9440
+ {
9441
+ "description": "default = 100% at the call site",
9442
+ "name": "--segmented-count-selected-background-alpha",
9443
+ "tier": "component",
9444
+ "value": "initial"
9445
+ },
9446
+ {
9447
+ "description": "default = hsl(var(--primary-foreground)) at the call site",
9448
+ "name": "--segmented-count-selected-color",
9449
+ "tier": "component",
9450
+ "value": "initial"
9451
+ },
9452
+ {
9453
+ "description": "default = var(--stroke-hairline) at the call site",
9454
+ "name": "--segmented-count-forced-outline-width",
9455
+ "tier": "component",
9456
+ "value": "initial"
9457
+ },
9422
9458
  {
9423
9459
  "description": "Rule weight, both orientations and both halves of a labelled rule. default = var(--stroke-hairline), resolved at the call site (gh#906)",
9424
9460
  "name": "--separator-rule-size",
@@ -10,6 +10,8 @@ export { Field } from "../data-entry/field.js";
10
10
  export { Descriptions } from "../data-display/descriptions.js";
11
11
  export { SkeletonRows, SkeletonTable, SkeletonDetail, SkeletonStat } from "../feedback/skeleton.js";
12
12
  export { Alert, AlertTitle, AlertContent, AlertDescription, AlertActions, AlertQueryError, } from "../feedback/alert.js";
13
+ export { AuthExpiryProvider } from "../feedback/auth-expiry.js";
14
+ export type { AuthExpiryProviderProps } from "../feedback/auth-expiry.js";
13
15
  export { SearchInput } from "../data-entry/search-input.js";
14
16
  export { Upload, collectUploadCommitActions, createUploadItem, useUploadDraft, } from "../data-entry/upload.js";
15
17
  export { Cascader } from "../data-entry/cascader.js";
@@ -14,6 +14,7 @@ import {
14
14
  AlertActions,
15
15
  AlertQueryError
16
16
  } from "../feedback/alert.js";
17
+ import { AuthExpiryProvider } from "../feedback/auth-expiry.js";
17
18
  import { SearchInput } from "../data-entry/search-input.js";
18
19
  import {
19
20
  Upload,
@@ -48,6 +49,7 @@ export {
48
49
  AlertDialog,
49
50
  AlertQueryError,
50
51
  AlertTitle,
52
+ AuthExpiryProvider,
51
53
  Badge,
52
54
  Cascader,
53
55
  Descriptions,
@@ -497,6 +497,7 @@ function Upload({
497
497
  disabled,
498
498
  onClick: openPicker,
499
499
  "aria-label": triggerAriaLabel,
500
+ className: "ui-upload-trigger",
500
501
  children: [
501
502
  /* @__PURE__ */ jsx(
502
503
  TriggerIcon,
@@ -553,7 +554,17 @@ function Upload({
553
554
  hiddenInput,
554
555
  liveRegion,
555
556
  rejection && /* @__PURE__ */ jsx("p", { role: "alert", className: "text-error-strong", children: rejection }),
556
- /* @__PURE__ */ jsx(Button, { type: "button", disabled, variant: triggerVariant, onClick: openPicker, children: children ?? t("dataEntry.upload.addImage") }),
557
+ /* @__PURE__ */ jsx(
558
+ Button,
559
+ {
560
+ type: "button",
561
+ disabled,
562
+ variant: triggerVariant,
563
+ onClick: openPicker,
564
+ className: "ui-upload-trigger",
565
+ children: children ?? t("dataEntry.upload.addImage")
566
+ }
567
+ ),
557
568
  list
558
569
  ] });
559
570
  }
@@ -35,6 +35,10 @@ export declare const AlertActions: React.ForwardRefExoticComponent<React.HTMLAtt
35
35
  * offer Retry (`onRetry`); permission/not-found/validation offer neither by default.
36
36
  * - **Legacy mode** (no `category`, e.g. mutation/infinite feedback): shows the cleaned domain
37
37
  * message (`humanError`) + optional Retry — form-submit corrective guidance stays visible.
38
+ * - **Under an `AuthExpiryProvider`** an auth-class error (either mode) is never painted as an
39
+ * alert: the provider's `onAuthExpired` runs once and this renders a small polite status
40
+ * ("redirecting to sign-in") instead. If the handler fails, the sign-in alert comes back with its
41
+ * button wired to the provider (gh#1022).
38
42
  */
39
43
  export declare function AlertQueryError({ error, category, onRetry, onAuthAction, className, }: AlertQueryErrorProp): React.JSX.Element;
40
44
  export declare const Alert: React.ForwardRefExoticComponent<React.HTMLAttributes<HTMLDivElement> & {