@godxjp/ui 31.1.0 → 31.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/agent/START-HERE.md +6 -6
- package/agent/components/AuthExpiryProvider.json +40 -0
- package/agent/components/DataState.json +3 -1
- package/agent/components/DataTable.json +1 -1
- package/agent/components/InfiniteQueryState.json +1 -1
- package/agent/components/ListRow.json +1 -0
- package/agent/components-index.json +5 -0
- package/agent/components.json +46 -3
- package/agent/index.json +7 -7
- package/agent/llms.txt +7 -7
- package/agent/patterns/async-data-state.json +1 -1
- package/agent/patterns.json +1 -1
- package/agent/tokens.json +6 -0
- package/dist/components/admin/index.d.ts +2 -0
- package/dist/components/admin/index.js +2 -0
- package/dist/components/data-display/data-table.js +2 -1
- package/dist/components/feedback/alert.d.ts +4 -0
- package/dist/components/feedback/alert.js +29 -8
- package/dist/components/feedback/auth-expiry.d.ts +30 -0
- package/dist/components/feedback/auth-expiry.js +64 -0
- package/dist/components/feedback/index.d.ts +2 -0
- package/dist/components/feedback/index.js +2 -0
- package/dist/components/query/data-state.js +9 -0
- package/dist/components/query/index.d.ts +2 -0
- package/dist/components/query/index.js +2 -0
- package/dist/components/query/infinite-query-state.js +9 -0
- package/dist/contracts/measurement.json +1 -1
- package/dist/i18n/messages/en.json +3 -0
- package/dist/i18n/messages/ja.json +3 -0
- package/dist/i18n/messages/vi.json +3 -0
- package/dist/props/components/feedback.prop.d.ts +13 -0
- package/dist/props/components/index.d.ts +1 -1
- package/dist/props/registry.d.ts +10 -1
- package/dist/props/registry.js +12 -0
- package/dist/props/vocabulary/data.prop.d.ts +8 -0
- package/dist/styles/alert-layout.css +4 -0
- package/dist/styles/layers.json +35 -1
- package/dist/styles/table-layout.css +6 -2
- package/dist/tokens/components/feedback.css +2 -0
- package/docs/FRAME-COVERAGE-REPORT.md +4 -2
- package/docs/data-display/data-table/examples/selectable-inbox.tsx +113 -0
- package/docs/data-display/data-table/index.md +2 -0
- package/docs/feedback/auth-expiry.tsx +104 -0
- package/docs/i18n/messages/en.json +6 -0
- package/docs/i18n/messages/ja.json +6 -0
- package/docs/i18n/messages/vi.json +6 -0
- package/docs/query/data-state.tsx +53 -5
- package/package.json +2 -2
package/agent/START-HERE.md
CHANGED
|
@@ -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.
|
|
6
|
+
**This catalog describes `@godxjp/ui` 31.3.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,19 +47,19 @@ 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` —
|
|
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
|
|
54
54
|
first; it exists because that is the mistake.
|
|
55
|
-
2. `components/<Name>.json` — one file per component (1 KB–
|
|
55
|
+
2. `components/<Name>.json` — one file per component (1 KB–35 KB, median 6 KB), carrying its props,
|
|
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` —
|
|
59
|
+
4. `tokens.json` — 2075 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
|
-
|
|
62
|
+
1761 `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.
|
|
@@ -145,7 +145,7 @@ has stopped following the brand.
|
|
|
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
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` |
|
|
148
|
+
| `component` | 1761 | 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": "
|
|
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.",
|
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
"type": "T[]"
|
|
15
15
|
},
|
|
16
16
|
{
|
|
17
|
-
"description": "Lean column definitions (adapted to TanStack internally — `meta.lean` is the declared home for every custom column option, so `priority` needs no second TanStack channel). Each column: { key: string; header: ReactNode; ariaLabel?: string; render?: (row: T) => ReactNode; sortable?: boolean; width?: string; align?: 'left'|'center'|'right'; hideBelow?: 'sm'|'md'|'lg'|'xl' (same contract as Flex hideBelow — stamped as data-hide-below on th/td); hiddenOnMobile?: boolean (alias for hideBelow:'md'); enableHiding?: boolean; pin?: 'end'; priority?: 'primary'|'secondary'|'meta'|'actions' }. priority is the column-priority contract read by preset=\"action-collection\" — DataTable stamps it as data-priority on the <th> AND every <td> of the column, so the preset can allocate the narrow-frame measure; leave the free-text column unmarked (it takes the remaining space), and prefer priority over width under the preset because an explicit width utility wins the cascade and defeats the measure. If render is omitted, the raw value at row[key] is rendered as a string. sortable opts the column into the sort cycle (client-side by default, or server-side via sort+onSortChange). enableHiding (default true) lists the column in DataTable.ViewOptions; set false to keep a key/actions column always visible. pin:'end' sticks the column (typically row actions) to the inline-end edge on horizontal scroll with a separating shadow — pin at most one column. ariaLabel gives a VISUALLY-EMPTY header (header='' — an action or selection column) a screen-reader name (e.g. 'Actions'/'Select'): it renders as an sr-only label inside the <th> so the column is never nameless (axe: empty-table-header). ANT DESIGN PARITY on the same column: fixed:'start'|'end' freezes the column against a scroll edge (logical, so it mirrors in RTL; the stacking offsets are MEASURED from the rendered header, so several adjacent frozen columns are correct at any width — pin:'end' is the older spelling of fixed:'end'). ellipsis holds the cell to one line and keeps the full value as its title (it also switches the table to table-layout: fixed, without which no ellipsis truncates anything). sorter is antd's richer `sortable`: true | (a, b) => number | { compare, multiple }, where multiple is the MULTI-column sort priority (highest sorts first). sortOrder / defaultSortOrder / sortDirections control and shape the cycle per column, and showSorterTooltip explains the next step. filters + onFilter + filteredValue / defaultFilteredValue / filterMultiple add a real filter menu to the header (filterMultiple: false makes it single-choice); omit onFilter for a server filter and drive it from the table's onFilterChange.",
|
|
17
|
+
"description": "Lean column definitions (adapted to TanStack internally — `meta.lean` is the declared home for every custom column option, so `priority` needs no second TanStack channel). Each column: { key: string; header: ReactNode; ariaLabel?: string; render?: (row: T) => ReactNode; sortable?: boolean; width?: string; align?: 'left'|'center'|'right'; hideBelow?: 'sm'|'md'|'lg'|'xl' (same contract as Flex hideBelow — stamped as data-hide-below on th/td); hiddenOnMobile?: boolean (alias for hideBelow:'md'); enableHiding?: boolean; pin?: 'end'; priority?: 'primary'|'secondary'|'meta'|'actions'; flush?: boolean }. flush (gh#1016 — the same word as TableCell flush / CardContent flush) drops the column's BODY cell padding so a self-padded row primitive owns the inset: a selectable list whose single column renders a `ListRow` writes `{ key: 'row', header: '', ariaLabel: …, flush: true, render: (m) => <ListRow asChild …><a/></ListRow> }` — the ListRow then sits 16px from the checkbox (not 32px) and the row is ListRow's own height; an unread ListRow in a flush cell paints `--list-row-unread-background` across the whole table row (checkbox cell included). The header keeps its padding, so the header label sits on the ListRow's inset. Never zero the cell with a className instead. priority is the column-priority contract read by preset=\"action-collection\" — DataTable stamps it as data-priority on the <th> AND every <td> of the column, so the preset can allocate the narrow-frame measure; leave the free-text column unmarked (it takes the remaining space), and prefer priority over width under the preset because an explicit width utility wins the cascade and defeats the measure. If render is omitted, the raw value at row[key] is rendered as a string. sortable opts the column into the sort cycle (client-side by default, or server-side via sort+onSortChange). enableHiding (default true) lists the column in DataTable.ViewOptions; set false to keep a key/actions column always visible. pin:'end' sticks the column (typically row actions) to the inline-end edge on horizontal scroll with a separating shadow — pin at most one column. ariaLabel gives a VISUALLY-EMPTY header (header='' — an action or selection column) a screen-reader name (e.g. 'Actions'/'Select'): it renders as an sr-only label inside the <th> so the column is never nameless (axe: empty-table-header). ANT DESIGN PARITY on the same column: fixed:'start'|'end' freezes the column against a scroll edge (logical, so it mirrors in RTL; the stacking offsets are MEASURED from the rendered header, so several adjacent frozen columns are correct at any width — pin:'end' is the older spelling of fixed:'end'). ellipsis holds the cell to one line and keeps the full value as its title (it also switches the table to table-layout: fixed, without which no ellipsis truncates anything). sorter is antd's richer `sortable`: true | (a, b) => number | { compare, multiple }, where multiple is the MULTI-column sort priority (highest sorts first). sortOrder / defaultSortOrder / sortDirections control and shape the cycle per column, and showSorterTooltip explains the next step. filters + onFilter + filteredValue / defaultFilteredValue / filterMultiple add a real filter menu to the header (filterMultiple: false makes it single-choice); omit onFilter for a server filter and drive it from the table's onFilterChange.",
|
|
18
18
|
"name": "columns",
|
|
19
19
|
"required": true,
|
|
20
20
|
"type": "ColumnDef<T>[]"
|
|
@@ -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
|
|
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."
|
|
@@ -75,6 +75,7 @@
|
|
|
75
75
|
"usage": [
|
|
76
76
|
"DO use ListRow for a SHORT (≈2–8 item) list of entities inside a Card where each row is one line with an action — account sessions, API keys, linked identities, passkeys. Stack rows in a `<Card><CardContent flush>` so the rows draw their own quiet dividers edge-to-edge.",
|
|
77
77
|
"DON'T reach for DataTable here — it carries sorting/selection/pagination chrome that a 3-item list doesn't need. DON'T nest a Card per row either (card-in-card). ListRow is the in-between surface.",
|
|
78
|
+
"DO host ListRow in a DataTable column when the list needs ROW SELECTION (a mail/notification inbox with bulk actions) — DataTable is the only primitive that selects rows. Mark that column `flush: true` so the cell drops its own padding and the ListRow owns the inset (gh#1016); without it the cell padding stacks on the row's (a 32px dead gap after the checkbox, taller rows) and the unread band stops at the checkbox cell. DON'T zero the cell with a className.",
|
|
78
79
|
"DO write a list of LINKS as `<Flex as=\"ul\" marker=\"none\" direction=\"col\" gap=\"none\">` + `<ListRow as=\"li\" asChild><Link/></ListRow>` — one call gives the list item, the whole-row link and the divider. DON'T wrap the row in your own `<li>` or `role=\"listitem\"` element to get list semantics back: the divider rule reads `:not(:last-child)` among the rows themselves, so a wrapper per row makes each one an only child and EVERY divider disappears — silently, which is why `ui-audit` now flags it as `no-hand-rolled-list`.",
|
|
79
80
|
"DON'T hand-roll `<div className=\"flex items-center justify-between border-b py-3\">` — that is exactly the repeated pattern ListRow replaces (border/radius/padding are tokenized via `--list-row-*`).",
|
|
80
81
|
"DO put the row's action in `trailing` (a `ghost`/`outline` Button, a DropdownMenu trigger, a Switch, or a status Badge). DO pass `as=\"li\"` when the rows live inside a semantic list, and build that list as `<Flex as=\"ul\" marker=\"none\">` — `ui-audit` flags a raw `<ul>`/`role=\"list\"` as `no-hand-rolled-list`.",
|
|
@@ -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",
|
package/agent/components.json
CHANGED
|
@@ -3380,7 +3380,7 @@
|
|
|
3380
3380
|
"type": "T[]"
|
|
3381
3381
|
},
|
|
3382
3382
|
{
|
|
3383
|
-
"description": "Lean column definitions (adapted to TanStack internally — `meta.lean` is the declared home for every custom column option, so `priority` needs no second TanStack channel). Each column: { key: string; header: ReactNode; ariaLabel?: string; render?: (row: T) => ReactNode; sortable?: boolean; width?: string; align?: 'left'|'center'|'right'; hideBelow?: 'sm'|'md'|'lg'|'xl' (same contract as Flex hideBelow — stamped as data-hide-below on th/td); hiddenOnMobile?: boolean (alias for hideBelow:'md'); enableHiding?: boolean; pin?: 'end'; priority?: 'primary'|'secondary'|'meta'|'actions' }. priority is the column-priority contract read by preset=\"action-collection\" — DataTable stamps it as data-priority on the <th> AND every <td> of the column, so the preset can allocate the narrow-frame measure; leave the free-text column unmarked (it takes the remaining space), and prefer priority over width under the preset because an explicit width utility wins the cascade and defeats the measure. If render is omitted, the raw value at row[key] is rendered as a string. sortable opts the column into the sort cycle (client-side by default, or server-side via sort+onSortChange). enableHiding (default true) lists the column in DataTable.ViewOptions; set false to keep a key/actions column always visible. pin:'end' sticks the column (typically row actions) to the inline-end edge on horizontal scroll with a separating shadow — pin at most one column. ariaLabel gives a VISUALLY-EMPTY header (header='' — an action or selection column) a screen-reader name (e.g. 'Actions'/'Select'): it renders as an sr-only label inside the <th> so the column is never nameless (axe: empty-table-header). ANT DESIGN PARITY on the same column: fixed:'start'|'end' freezes the column against a scroll edge (logical, so it mirrors in RTL; the stacking offsets are MEASURED from the rendered header, so several adjacent frozen columns are correct at any width — pin:'end' is the older spelling of fixed:'end'). ellipsis holds the cell to one line and keeps the full value as its title (it also switches the table to table-layout: fixed, without which no ellipsis truncates anything). sorter is antd's richer `sortable`: true | (a, b) => number | { compare, multiple }, where multiple is the MULTI-column sort priority (highest sorts first). sortOrder / defaultSortOrder / sortDirections control and shape the cycle per column, and showSorterTooltip explains the next step. filters + onFilter + filteredValue / defaultFilteredValue / filterMultiple add a real filter menu to the header (filterMultiple: false makes it single-choice); omit onFilter for a server filter and drive it from the table's onFilterChange.",
|
|
3383
|
+
"description": "Lean column definitions (adapted to TanStack internally — `meta.lean` is the declared home for every custom column option, so `priority` needs no second TanStack channel). Each column: { key: string; header: ReactNode; ariaLabel?: string; render?: (row: T) => ReactNode; sortable?: boolean; width?: string; align?: 'left'|'center'|'right'; hideBelow?: 'sm'|'md'|'lg'|'xl' (same contract as Flex hideBelow — stamped as data-hide-below on th/td); hiddenOnMobile?: boolean (alias for hideBelow:'md'); enableHiding?: boolean; pin?: 'end'; priority?: 'primary'|'secondary'|'meta'|'actions'; flush?: boolean }. flush (gh#1016 — the same word as TableCell flush / CardContent flush) drops the column's BODY cell padding so a self-padded row primitive owns the inset: a selectable list whose single column renders a `ListRow` writes `{ key: 'row', header: '', ariaLabel: …, flush: true, render: (m) => <ListRow asChild …><a/></ListRow> }` — the ListRow then sits 16px from the checkbox (not 32px) and the row is ListRow's own height; an unread ListRow in a flush cell paints `--list-row-unread-background` across the whole table row (checkbox cell included). The header keeps its padding, so the header label sits on the ListRow's inset. Never zero the cell with a className instead. priority is the column-priority contract read by preset=\"action-collection\" — DataTable stamps it as data-priority on the <th> AND every <td> of the column, so the preset can allocate the narrow-frame measure; leave the free-text column unmarked (it takes the remaining space), and prefer priority over width under the preset because an explicit width utility wins the cascade and defeats the measure. If render is omitted, the raw value at row[key] is rendered as a string. sortable opts the column into the sort cycle (client-side by default, or server-side via sort+onSortChange). enableHiding (default true) lists the column in DataTable.ViewOptions; set false to keep a key/actions column always visible. pin:'end' sticks the column (typically row actions) to the inline-end edge on horizontal scroll with a separating shadow — pin at most one column. ariaLabel gives a VISUALLY-EMPTY header (header='' — an action or selection column) a screen-reader name (e.g. 'Actions'/'Select'): it renders as an sr-only label inside the <th> so the column is never nameless (axe: empty-table-header). ANT DESIGN PARITY on the same column: fixed:'start'|'end' freezes the column against a scroll edge (logical, so it mirrors in RTL; the stacking offsets are MEASURED from the rendered header, so several adjacent frozen columns are correct at any width — pin:'end' is the older spelling of fixed:'end'). ellipsis holds the cell to one line and keeps the full value as its title (it also switches the table to table-layout: fixed, without which no ellipsis truncates anything). sorter is antd's richer `sortable`: true | (a, b) => number | { compare, multiple }, where multiple is the MULTI-column sort priority (highest sorts first). sortOrder / defaultSortOrder / sortDirections control and shape the cycle per column, and showSorterTooltip explains the next step. filters + onFilter + filteredValue / defaultFilteredValue / filterMultiple add a real filter menu to the header (filterMultiple: false makes it single-choice); omit onFilter for a server filter and drive it from the table's onFilterChange.",
|
|
3384
3384
|
"name": "columns",
|
|
3385
3385
|
"required": true,
|
|
3386
3386
|
"type": "ColumnDef<T>[]"
|
|
@@ -4386,6 +4386,7 @@
|
|
|
4386
4386
|
"usage": [
|
|
4387
4387
|
"DO use ListRow for a SHORT (≈2–8 item) list of entities inside a Card where each row is one line with an action — account sessions, API keys, linked identities, passkeys. Stack rows in a `<Card><CardContent flush>` so the rows draw their own quiet dividers edge-to-edge.",
|
|
4388
4388
|
"DON'T reach for DataTable here — it carries sorting/selection/pagination chrome that a 3-item list doesn't need. DON'T nest a Card per row either (card-in-card). ListRow is the in-between surface.",
|
|
4389
|
+
"DO host ListRow in a DataTable column when the list needs ROW SELECTION (a mail/notification inbox with bulk actions) — DataTable is the only primitive that selects rows. Mark that column `flush: true` so the cell drops its own padding and the ListRow owns the inset (gh#1016); without it the cell padding stacks on the row's (a 32px dead gap after the checkbox, taller rows) and the unread band stops at the checkbox cell. DON'T zero the cell with a className.",
|
|
4389
4390
|
"DO write a list of LINKS as `<Flex as=\"ul\" marker=\"none\" direction=\"col\" gap=\"none\">` + `<ListRow as=\"li\" asChild><Link/></ListRow>` — one call gives the list item, the whole-row link and the divider. DON'T wrap the row in your own `<li>` or `role=\"listitem\"` element to get list semantics back: the divider rule reads `:not(:last-child)` among the rows themselves, so a wrapper per row makes each one an only child and EVERY divider disappears — silently, which is why `ui-audit` now flags it as `no-hand-rolled-list`.",
|
|
4390
4391
|
"DON'T hand-roll `<div className=\"flex items-center justify-between border-b py-3\">` — that is exactly the repeated pattern ListRow replaces (border/radius/padding are tokenized via `--list-row-*`).",
|
|
4391
4392
|
"DO put the row's action in `trailing` (a `ghost`/`outline` Button, a DropdownMenu trigger, a Switch, or a status Badge). DO pass `as=\"li\"` when the rows live inside a semantic list, and build that list as `<Flex as=\"ul\" marker=\"none\">` — `ui-audit` flags a raw `<ul>`/`role=\"list\"` as `no-hand-rolled-list`.",
|
|
@@ -5219,7 +5220,7 @@
|
|
|
5219
5220
|
"type": "boolean"
|
|
5220
5221
|
},
|
|
5221
5222
|
{
|
|
5222
|
-
"description": "
|
|
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).",
|
|
5223
5224
|
"name": "onAuthError",
|
|
5224
5225
|
"type": "() => void"
|
|
5225
5226
|
},
|
|
@@ -5255,6 +5256,8 @@
|
|
|
5255
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.",
|
|
5256
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.",
|
|
5257
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.",
|
|
5258
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.",
|
|
5259
5262
|
"DO: pass prerequisite for enabled:false queries. Pending + fetchStatus idle is unstarted, not loading, and never renders a skeleton.",
|
|
5260
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.",
|
|
@@ -5268,6 +5271,46 @@
|
|
|
5268
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."
|
|
5269
5272
|
]
|
|
5270
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
|
+
},
|
|
5271
5314
|
{
|
|
5272
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>",
|
|
5273
5316
|
"group": "data-display",
|
|
@@ -5312,7 +5355,7 @@
|
|
|
5312
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.",
|
|
5313
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.",
|
|
5314
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.",
|
|
5315
|
-
"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
|
|
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.",
|
|
5316
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.",
|
|
5317
5360
|
"DON'T: Use `InfiniteQueryState` for a `useQuery` result — it expects `UseInfiniteQueryResult` shape (`pages`, `hasNextPage`, `fetchNextPage`, `isFetchingNextPage`). For regular `useQuery` use `DataState` instead.",
|
|
5318
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."
|
package/agent/index.json
CHANGED
|
@@ -1,21 +1,21 @@
|
|
|
1
1
|
{
|
|
2
2
|
"counts": {
|
|
3
3
|
"anti-ai-tells": 26,
|
|
4
|
-
"components":
|
|
4
|
+
"components": 176,
|
|
5
5
|
"patterns": 21,
|
|
6
6
|
"rules": 50,
|
|
7
|
-
"tokens":
|
|
7
|
+
"tokens": 2075,
|
|
8
8
|
"vocabulary": 14
|
|
9
9
|
},
|
|
10
10
|
"files": [
|
|
11
11
|
{
|
|
12
12
|
"file": "components-index.json",
|
|
13
|
-
"note": "
|
|
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
|
{
|
|
17
17
|
"file": "components/<Name>.json",
|
|
18
|
-
"note": "One file per component (1 KB–
|
|
18
|
+
"note": "One file per component (1 KB–35 KB, median 6 KB), each carrying its importPath. This is the selective route: read the index, then fetch only what you need instead of the 1.2 MB blob.",
|
|
19
19
|
"url": "https://raw.githubusercontent.com/godx-jp/godxjp-ui/main/agent/components/<Name>.json"
|
|
20
20
|
},
|
|
21
21
|
{
|
|
@@ -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.
|
|
51
|
+
"pinned": "https://raw.githubusercontent.com/godx-jp/godxjp-ui/v31.3.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":
|
|
58
|
+
"component": 1761,
|
|
59
59
|
"foundation": 211,
|
|
60
60
|
"semantic": 103
|
|
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.
|
|
65
|
+
"version": "31.3.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:
|
|
4
|
-
> 50 cardinal rules. This file is the entry point for AI agents. Catalog version 31.
|
|
3
|
+
> A Japanese-enterprise React design system: 176 components, 2075 design tokens,
|
|
4
|
+
> 50 cardinal rules. This file is the entry point for AI agents. Catalog version 31.3.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.
|
|
7
|
+
(`npx @godxjp/ui-mcp@31.3.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):
|
|
19
|
-
- [components/<Name>.json](https://raw.githubusercontent.com/godx-jp/godxjp-ui/main/agent/components/Select.json): one file per component (1 KB–
|
|
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
|
+
- [components/<Name>.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,
|
|
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, 1761 `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.
|
|
29
|
+
the tag: `.../godx-jp/godxjp-ui/v31.3.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) →
|
|
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": [
|
package/agent/patterns.json
CHANGED
|
@@ -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) →
|
|
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
|
@@ -6983,6 +6983,12 @@
|
|
|
6983
6983
|
"tier": "component",
|
|
6984
6984
|
"value": "var(--space-stack-sm)"
|
|
6985
6985
|
},
|
|
6986
|
+
{
|
|
6987
|
+
"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.",
|
|
6988
|
+
"name": "--query-auth-alert-max-inline-size",
|
|
6989
|
+
"tier": "component",
|
|
6990
|
+
"value": "36rem"
|
|
6991
|
+
},
|
|
6986
6992
|
{
|
|
6987
6993
|
"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
6994
|
"name": "--tooltip-max-width",
|
|
@@ -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,
|
|
@@ -1296,7 +1296,8 @@ DataTable.Content = function DataTableContent() {
|
|
|
1296
1296
|
...fixedCellProps(col.key, fixedEdge(col)),
|
|
1297
1297
|
...columnHideBelowProps(col),
|
|
1298
1298
|
style: columnCellStyle(col),
|
|
1299
|
-
|
|
1299
|
+
flush: col.flush,
|
|
1300
|
+
className: cn(!col.flush && cellPadding, columnCellClass(col)),
|
|
1300
1301
|
children: rendered
|
|
1301
1302
|
},
|
|
1302
1303
|
col.key
|
|
@@ -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> & {
|
|
@@ -12,8 +12,11 @@ import {
|
|
|
12
12
|
} from "lucide-react";
|
|
13
13
|
import { useTranslation } from "../../i18n/use-translation.js";
|
|
14
14
|
import { humanError } from "../../lib/format.js";
|
|
15
|
+
import { classifyQueryError } from "../../lib/query-error.js";
|
|
15
16
|
import { Flex } from "../layout/flex.js";
|
|
16
17
|
import { Button } from "../general/button.js";
|
|
18
|
+
import { Text } from "../general/typography.js";
|
|
19
|
+
import { useAuthExpiry } from "./auth-expiry.js";
|
|
17
20
|
const AlertContext = React.createContext("default");
|
|
18
21
|
const ASSERTIVE_TONES = /* @__PURE__ */ new Set(["destructive", "warning"]);
|
|
19
22
|
const roleFor = (variant, tone) => {
|
|
@@ -121,24 +124,42 @@ function AlertQueryError({
|
|
|
121
124
|
className
|
|
122
125
|
}) {
|
|
123
126
|
const { t } = useTranslation();
|
|
124
|
-
|
|
127
|
+
const resolved = category ?? classifyQueryError(error).category;
|
|
128
|
+
const expiry = useAuthExpiry(resolved === "auth", error);
|
|
129
|
+
if (expiry?.status === "redirecting") {
|
|
130
|
+
return /* @__PURE__ */ jsx(
|
|
131
|
+
Text,
|
|
132
|
+
{
|
|
133
|
+
as: "p",
|
|
134
|
+
size: "sm",
|
|
135
|
+
tone: "muted",
|
|
136
|
+
role: "status",
|
|
137
|
+
"aria-live": "polite",
|
|
138
|
+
"data-slot": "alert-query-auth-pending",
|
|
139
|
+
className,
|
|
140
|
+
children: t("query.authExpiry.redirecting")
|
|
141
|
+
}
|
|
142
|
+
);
|
|
143
|
+
}
|
|
144
|
+
const authAction = expiry ? expiry.retry : onAuthAction;
|
|
145
|
+
if (!category && !expiry) {
|
|
125
146
|
return /* @__PURE__ */ jsxs(Alert, { tone: "destructive", className, children: [
|
|
126
147
|
/* @__PURE__ */ jsx(AlertTitle, { children: t("common.error") }),
|
|
127
148
|
/* @__PURE__ */ jsx(AlertDescription, { children: humanError(error) }),
|
|
128
149
|
onRetry && /* @__PURE__ */ jsx(RetryButton, { onRetry })
|
|
129
150
|
] });
|
|
130
151
|
}
|
|
131
|
-
const tone = WARNING_CATEGORIES.has(
|
|
132
|
-
return /* @__PURE__ */ jsxs(Alert, { tone, className, children: [
|
|
133
|
-
/* @__PURE__ */ jsx(AlertTitle, { children: t(`query.error.title.${
|
|
134
|
-
/* @__PURE__ */ jsx(AlertDescription, { children: t(`query.error.description.${
|
|
135
|
-
|
|
152
|
+
const tone = resolved === "auth" ? "default" : WARNING_CATEGORIES.has(resolved) ? "warning" : "destructive";
|
|
153
|
+
return /* @__PURE__ */ jsxs(Alert, { tone, className, "data-query-category": resolved, children: [
|
|
154
|
+
/* @__PURE__ */ jsx(AlertTitle, { children: t(`query.error.title.${resolved}`) }),
|
|
155
|
+
/* @__PURE__ */ jsx(AlertDescription, { children: t(`query.error.description.${resolved}`) }),
|
|
156
|
+
resolved === "auth" && authAction && /* @__PURE__ */ jsx(AlertActions, { children: /* @__PURE__ */ jsx(
|
|
136
157
|
Button,
|
|
137
158
|
{
|
|
138
159
|
variant: "outline",
|
|
139
160
|
size: "sm",
|
|
140
161
|
onClick: () => {
|
|
141
|
-
void
|
|
162
|
+
void authAction();
|
|
142
163
|
},
|
|
143
164
|
children: /* @__PURE__ */ jsxs(Flex, { direction: "row", wrap: true, align: "center", gap: "xs", children: [
|
|
144
165
|
/* @__PURE__ */ jsx(LogIn, { "aria-hidden": "true" }),
|
|
@@ -146,7 +167,7 @@ function AlertQueryError({
|
|
|
146
167
|
] })
|
|
147
168
|
}
|
|
148
169
|
) }),
|
|
149
|
-
RETRYABLE_CATEGORIES.has(
|
|
170
|
+
RETRYABLE_CATEGORIES.has(resolved) && onRetry && /* @__PURE__ */ jsx(RetryButton, { onRetry })
|
|
150
171
|
] });
|
|
151
172
|
}
|
|
152
173
|
const Alert = Object.assign(AlertBase, {
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
import type { AuthExpiryProviderProp } from "../../props/components/feedback.prop.js";
|
|
3
|
+
export type { AuthExpiryProviderProp, AuthExpiryProviderProp as AuthExpiryProviderProps, } from "../../props/components/feedback.prop.js";
|
|
4
|
+
/**
|
|
5
|
+
* `"redirecting"` — the handler has been invoked (or is about to be); render a neutral pending state.
|
|
6
|
+
* `"failed"` — the handler threw or rejected; fall back to the sign-in alert so the user is not left
|
|
7
|
+
* on an endless spinner.
|
|
8
|
+
*/
|
|
9
|
+
export type AuthExpiryStatus = "redirecting" | "failed";
|
|
10
|
+
/**
|
|
11
|
+
* Central handling for an expired session (401 / invalid or expired token) — the SSO norm: the app
|
|
12
|
+
* sends the user back through the IdP, which returns immediately while the IdP session is alive, so
|
|
13
|
+
* no page-level error is ever painted.
|
|
14
|
+
*
|
|
15
|
+
* Every `DataState`, `InfiniteQueryState`, `AlertMutationFeedback` and `AlertQueryError` under this
|
|
16
|
+
* provider consults it. On an auth-class error it calls `onAuthExpired` **once** — any number of
|
|
17
|
+
* simultaneous 401s share one call — and renders a neutral pending state with a polite live region
|
|
18
|
+
* instead of an alert. The call is re-armed once every auth-errored view has gone (navigated away,
|
|
19
|
+
* or the queries recovered after a silent refresh), so a later expiry fires again.
|
|
20
|
+
*/
|
|
21
|
+
export declare function AuthExpiryProvider({ onAuthExpired, children }: AuthExpiryProviderProp): React.JSX.Element;
|
|
22
|
+
/**
|
|
23
|
+
* Internal hook for the query surfaces. `active` = this view is currently showing an auth-class
|
|
24
|
+
* error. Returns `null` when there is no provider (the caller keeps its legacy alert), otherwise the
|
|
25
|
+
* status to render plus a `retry` for the fallback alert's sign-in button.
|
|
26
|
+
*/
|
|
27
|
+
export declare function useAuthExpiry(active: boolean, error: unknown): {
|
|
28
|
+
status: AuthExpiryStatus;
|
|
29
|
+
retry: () => void;
|
|
30
|
+
} | null;
|