@godxjp/ui 31.8.0 → 31.10.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.
@@ -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.8.0.** If the project you are editing has a different
6
+ **This catalog describes `@godxjp/ui` 31.10.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.
@@ -246,7 +246,7 @@
246
246
  "DO use DataTable.Toolbar as the immediate child that wraps search/filter controls on the left and DataTable.DensityToggle/action buttons on the right. DataTable.BulkActions inside the toolbar auto-hides when selection count is 0; it accepts either plain ReactNode children (built-in 'N selected' status bar) or a (count)=>node render-prop (you own the whole bar).",
247
247
  "DO reach for the grid chrome (DataTable.Search, DataTable.ViewOptions, DataTable.Pagination pageSizeOptions) when you need global search, a column 'set view' picker, or numbered pagination — these are the merged former-DataGrid features, now on the one DataTable. Drive them client-side by default; pass the matching state + manual* flag for a server query.",
248
248
  "DataTable.Pagination OWNS ITS OWN INSET. The footer is a self-contained slot: it declares `padding-block` + `padding-inline` from `--table-pagination-padding-{y,x}` (block default = `--space-stack-sm`, inline default = `--table-cell-space-x`, so the 'rows per page' label lands on the same optical axis as the first column's text). Before the fix it declared `padding-top` only, so inside the documented flush container (`<Card><CardContent flush><DataTable/>`) the label and page-size Select sat flush against the container edge and border. DON'T ship a local `.ui-data-table-pagination { padding: … }` override in an app — retune the two tokens in your theme instead.",
249
- "RESPONSIVE APPROVAL / ACTION QUEUE: reach for `preset=\"action-collection\"` when a dense five-column queue (requester · target · reason · requested date · row actions) must stay readable at 390px, and give every column a `priority` on its ColumnDef — `primary` (the row subject), `secondary` (its target), `meta` (a timestamp/id), `actions` (the row-action affordance, whose measure is reserved FIRST so it can never be pushed off-screen). Leave the free-text column unmarked; it takes the remaining space. This is the SAME contract, the SAME `--table-action-collection-*` tokens and the SAME container query as `Table preset=\"action-collection\"` — there is no separate DataTable family. Never add a consumer width, a hidden column, `hiddenOnMobile` or a page-local breakpoint to make a table fit: retune the tokens instead.",
249
+ "RESPONSIVE APPROVAL / ACTION QUEUE: reach for `preset=\"action-collection\"` when a dense five-column queue (requester · target · reason · requested date · row actions) must stay readable at 390px, and give every column a `priority` on its ColumnDef — `primary` (the row subject), `secondary` (its target), `meta` (a timestamp/id), `actions` (the row-action affordance, whose measure is reserved FIRST so it can never be pushed off-screen, and which grows to its content when a text action + `…` menu does not fit the icon-sized token — gh#1067). Leave the free-text column unmarked; it takes the remaining space. This is the SAME contract, the SAME `--table-action-collection-*` tokens and the SAME container query as `Table preset=\"action-collection\"` — there is no separate DataTable family. Never add a consumer width, a hidden column, `hiddenOnMobile` or a page-local breakpoint to make a table fit: retune the tokens instead.",
250
250
  "DON'T set `width` on a column that also has a `priority` — an explicit width utility wins the cascade over the priority measure and re-opens the horizontal scroll. Under the preset, `pin: 'end'` is also redundant: nothing scrolls sideways, so the actions column is already in frame.",
251
251
  "The DataTable surface's narrow-viewport width floor is `--table-surface-min-inline-size` (default 640px, released at the `sm` viewport step). It used to be a hard-coded `min-w-[640px] sm:min-w-0` utility pair on the surface — the literal that forced the horizontal scroll at 390. Retune (or zero) the token in your theme; `preset=\"action-collection\"` already opts out of it.",
252
252
  "DO use ColumnDef.render for custom cell content (Badge, Link, RowActions). For plain string/number fields render can be omitted — DataTable falls back to String(row[key]).",
@@ -13,7 +13,7 @@
13
13
  "name": "Select",
14
14
  "props": [
15
15
  {
16
- "description": "Select several options; value/defaultValue become string arrays. `tags` additionally ACCEPTS what was typed (a value that is not in the list), which is antd's own split between the two. The popup list of either mode renders as `Command split` (rows ruled, list padding 0, gh#699); retune it with --command-item-divider-color / --command-item-divider-width.",
16
+ "description": "Select several options; value/defaultValue become string arrays. `tags` additionally ACCEPTS what was typed (a value that is not in the list), which is antd's own split between the two. `tags` needs no `options` (`<Select mode=\"tags\" />` works) and only an explicit `disabled` disables it (gh#1063). While text is typed the tags list reads create row, then an exact (case-folded) match, then options, then held values — the first enabled row is the Enter target, never a held tag (gh#1064). The popup list of either mode renders as `Command split` (rows ruled, list padding 0, gh#699); retune it with --command-item-divider-color / --command-item-divider-width.",
17
17
  "name": "mode",
18
18
  "type": "\"multiple\" | \"tags\""
19
19
  },
@@ -18,7 +18,7 @@
18
18
  },
19
19
  {
20
20
  "defaultValue": "false",
21
- "description": "The row fills its parent's inline size (antd `block`, renamed to match Button.fullWidth — the same rename, the same reason: this library's controlled vocabulary wins on values).",
21
+ "description": "The row fills its parent's inline size (antd `block`, renamed to match Button.fullWidth — the same rename, the same reason: this library's controlled vocabulary wins on values). Field children grow to fill it; a Button keeps its content width.",
22
22
  "name": "fullWidth",
23
23
  "type": "boolean"
24
24
  },
@@ -50,7 +50,7 @@
50
50
  "DON'T reach for `className=\"rounded-none\"` / `[&>*:not(:first-child)]` utilities to join controls yourself — `ui-audit`'s `no-utility-layout` rule blocks exactly that, and it is precisely the corner-radius seam this component owns.",
51
51
  "DO set `size` on each CHILD individually, not on SpaceCompact — the row has no size prop of its own by design (see `density`); every control already owns its own `size` axis (`xs|sm|md|lg`).",
52
52
  "DO use `fullWidth` inside a narrow form card so the joined row spans the field column, exactly like a lone Input would.",
53
- "`fullWidth` gives EVERY child an equal share of the row — each item carries `flex: 1 1 0%`, so the split is even no matter what the children would size to on their own. Measured at a 1169px row: NumberInput+Select 585/585, and Select+Button ALSO 585/585, i.e. a Button under `fullWidth` is stretched to half the row, not left at its content width. Want the Button at content width and the field absorbing the rest? Omit `fullWidth` — the row is then `inline-flex` and sizes to content (measured 223/40). Do NOT remove the per-child `flex` to chase antd's block style: without it the items collapse to their content and the row stops filling at all (measured 203/73.6 in a 1169px column), which defeats `fullWidth`.",
53
+ "`fullWidth` is antd `Space.Compact block` (gh#1062): in a row, a FIELD child (Input and its affix/Password/Search forms, NumberInput, Textarea, TagInput, the Date/Time pickers, and the Select/Cascader/TreeSelect/SearchSelect trigger) grows to fill the row, and every other child — a Button, an icon button — keeps its content width. So `[Input][+ Add]` is a full-width field with its submit at content width, and two fields (NumberInput + Select) share the row evenly between them. Deviation from antd, written down: antd grows only its Input family (`width: 100%`); Select/InputNumber/DatePicker keep their own width there. Here every field grows, so two fields never leave an empty tail. `orientation=\"vertical\"` + `fullWidth` stacks full-width children as before.",
54
54
  "DON'T expect corner-radius welding on `orientation=\"vertical\"` yet — the shared border still collapses, but each child keeps all four of its own corners rounded until a block-axis radius knob exists on the Input/trigger families (documented gap, not a silent one)."
55
55
  ],
56
56
  "useCases": [
@@ -32,6 +32,12 @@
32
32
  "description": "Take the remaining height of the parent instead of growing with the content. Default false keeps the pane content-height, which is right inside a scrolling page. Set it for an app-shell surface whose columns own their own scrolling — a chat transcript with a pinned composer, a full-height table beside a detail rail: the pane and both columns get a DEFINITE height, so overflow inside a column scrolls that column instead of pushing the page taller. The parent still decides how much height there is to fill: inside a flex column, give the wrapper `flex: 1; min-height: 0`.",
33
33
  "name": "fill",
34
34
  "type": "boolean"
35
+ },
36
+ {
37
+ "defaultValue": "\"main-first\"",
38
+ "description": "Which pane comes first when the pane is too narrow to split and the columns stack. `aside-first` puts the aside on top — a detail page whose properties card must not fall below a long comment thread on a phone. It moves the `<aside>` first in the DOM (not a CSS `order` swap), so in the stacked layout reading and Tab order match the screen (WCAG 1.3.2 / 2.4.3); once the pane splits, grid placement keeps main in the leading column and the aside inline-end, geometry identical to the default. No antd equivalent (antd's closest tool is Grid `Col order`, a visual-only reorder).",
39
+ "name": "stackOrder",
40
+ "type": "\"main-first\" | \"aside-first\""
35
41
  }
36
42
  ],
37
43
  "related": [
@@ -48,11 +54,12 @@
48
54
  "usage": [
49
55
  "DO: pass all right-panel content via the `aside` prop — it renders inside a semantic `<aside>` element at a fixed rem width (sm=20rem, md=22rem). The `children` prop fills the main `1fr` column. Both accept any ReactNode.",
50
56
  "DO: reach for `fill` when the pane is an app-shell surface rather than a block in a scrolling page — a chat transcript whose composer stays pinned to the bottom, a full-height table beside a detail rail. It is the supported way to say \"be as tall as what is left\"; a consumer that instead styles the pane's wrapper divs from outside (`[&>*]`, `[&>*>*]`) is targeting this component's internal DOM by position and will break the day another wrapper appears.",
57
+ "DO: set `stackOrder=\"aside-first\"` when the aside is what a phone user needs first — an issue's properties / agent card above its comment thread. DON'T render the aside twice (once above, once beside) or reorder with a CSS `order` override: the prop already puts it first in the DOM, so focus order follows the stacked screen, and restores the side-by-side placement once the pane splits.",
51
58
  "DO: choose `asideWidth=\"sm\"` for compact detail panels (filters, quick stats, key-value summaries) and the default `asideWidth=\"md\"` for richer panels (forms, timelines, long metadata lists).",
52
59
  "DO: wrap SplitPane inside `PageContainer` or `PageContainer.Inset` — SplitPane provides no page padding of its own. It is a grid primitive, not a page scaffold.",
53
60
  "DO: close a collapsible rail with `aside={null}` — a Slack-style thread, a Linear-style detail panel — rather than swapping `<SplitPane aside={<Thread />}>{page}</SplitPane>` for a bare `{page}`. Both look the same on screen; only the prop keeps `children` mounted. The conditional swap changes the DEPTH of `{page}` in the React tree, so React remounts it: measured in a consumer as a message list jumping from scrollTop 400 to the bottom the instant a thread opened, losing the reader's place and any component state below it.",
54
61
  "DON'T: leave `SplitPane` mounted with an empty `aside={<div />}` to avoid that remount — an empty aside still reserves its rail, leaving a blank 20-30rem column. `aside={null}` removes the element AND the column (the pane sets `data-aside=\"closed\"` on itself, so the collapse is one CSS rule, not a call-site width override).",
55
- "DON'T: expect two columns below 48rem OF THE PANE'S OWN WIDTH (64rem for `asideWidth=\"lg\"`). The split is a CSS container query on the pane, not a viewport media query, so a narrow embedded pane on a large screen correctly stays single-column and a wide pane on a small screen still splits. Below the threshold it stacks (main on top, aside below); for layouts that must stay side-by-side at any width use CSS Grid or `ResponsiveGrid`.",
62
+ "DON'T: expect two columns below 48rem OF THE PANE'S OWN WIDTH (64rem for `asideWidth=\"lg\"`). The split is a CSS container query on the pane, not a viewport media query, so a narrow embedded pane on a large screen correctly stays single-column and a wide pane on a small screen still splits. Below the threshold it stacks (main on top, aside below — `stackOrder=\"aside-first\"` flips that); for layouts that must stay side-by-side at any width use CSS Grid or `ResponsiveGrid`.",
56
63
  "DON'T: add a CSS `overflow: hidden` or fixed height on the SplitPane wrapper; both columns carry `min-width: 0` to handle overflow correctly, and the grid uses `minmax(0, 1fr)` — adding external constraints will break the overflow contract.",
57
64
  "DON'T: hand-roll a two-column div layout with flexbox or CSS Grid when SplitPane already ships — that duplicates the responsive breakpoint logic and the semantic `<aside>` element."
58
65
  ],
@@ -108,7 +108,7 @@
108
108
  "DO use `striped` on dense list tables so a row is easy to follow across its columns; to turn it on for EVERY Table and DataTable at once, set `:root { --table-row-striped-alpha: 100%; }` in the theme rather than passing the prop everywhere (`striped={false}` opts one table out). A detail row under a record is `<TableRow data-expanded-row=\"\">` so the stripe counts records, not DOM rows — never stripe with `:nth-child` page CSS or row utilities.",
109
109
  "DO NOT hand-roll empty-state handling inside a Table composition. When data can be empty, switch to `DataTable` (which has a built-in empty state) or wrap the `<Table>` with a conditional that renders `<EmptyState>` — never leave a table with only a header and zero rows.",
110
110
  "DO NOT use Table for lists that need sorting, filtering, pagination, or row selection — those features are only in `DataTable`. Table is intentionally stateless: it owns no TanStack Table instance, no column definitions, and no toolbar.",
111
- "DO reach for `preset=\"action-collection\"` for a dense approval / action queue (requester · target · reason · requested date · row actions) that must stay readable at 390px, and mark every column with `priority` on BOTH its `TableHead` and its `TableCell`: `primary` (the row subject), `secondary` (its target), `meta` (a timestamp/id), `actions` (the row-action affordance, whose measure is reserved first so it can never be pushed off-screen). Leave the free-text column unmarked — it takes the remaining space. For text actions such as 対応する, set ColumnDef.width (or TableHead width) to reserve the label measure; leave a content column fluid. The default actions token is sized for icon actions. Do not add a hidden column or page-local breakpoint to make a table fit. The IDENTICAL preset exists on `DataTable` (`preset` + `collapseBelow` on the table, `priority` on the `ColumnDef`) sharing these same tokens — use DataTable when the queue is data-driven and needs sorting/selection/pagination, and reach for the raw `Table` only for a hand-authored queue.",
111
+ "DO reach for `preset=\"action-collection\"` for a dense approval / action queue (requester · target · reason · requested date · row actions) that must stay readable at 390px, and mark every column with `priority` on BOTH its `TableHead` and its `TableCell`: `primary` (the row subject), `secondary` (its target), `meta` (a timestamp/id), `actions` (the row-action affordance, whose measure is reserved first so it can never be pushed off-screen). Leave the free-text column unmarked — it takes the remaining space. The `actions` token measure is a FLOOR sized for an icon trigger: a text action (対応する, Retry) plus a `…` menu grows the column to its content on its own (gh#1067), so it never paints over the previous column and needs no ColumnDef.width; an explicit width still wins. Leave a content column fluid. Do not add a hidden column or page-local breakpoint to make a table fit. The IDENTICAL preset exists on `DataTable` (`preset` + `collapseBelow` on the table, `priority` on the `ColumnDef`) sharing these same tokens — use DataTable when the queue is data-driven and needs sorting/selection/pagination, and reach for the raw `Table` only for a hand-authored queue.",
112
112
  "DO reach for `<TableCell flush>` when the cell's CONTENT owns its inset — an expanded detail panel under a row (`<TableCell flush colSpan={n}>`), a nested table, a full-bleed media strip. It drops the cell's own padding so the child spans the whole cell; without it the panel is indented by `--table-cell-space-x` and the only route was a `p-0` utility, which no service theme can reach.",
113
113
  "DO express hierarchy with `<TableCell indent={depth}>` — a grouped table's detail rows under their subtotal header, or a tree row under its parent. The measure is `--table-cell-space-x + depth x --table-cell-indent-space-step`, so level 0 sits on the column's own text axis and a service retunes (or flattens) the step in one token. Never hand-roll `style={{ paddingInlineStart }}` at the call site — that is a per-page constant no theme can reach, and it breaks in RTL unless you remember the logical property.",
114
114
  "DO place `<Table>` inside a `<CardContent flush>` (or `p-0` card) when embedding in a Card, so the built-in `overflow-auto` wrapper sits flush to the card edges. Wrapping with plain `<CardContent>` adds padding that clips the horizontal scroll shadow."
@@ -964,7 +964,7 @@
964
964
  },
965
965
  {
966
966
  "defaultValue": "false",
967
- "description": "The row fills its parent's inline size (antd `block`, renamed to match Button.fullWidth — the same rename, the same reason: this library's controlled vocabulary wins on values).",
967
+ "description": "The row fills its parent's inline size (antd `block`, renamed to match Button.fullWidth — the same rename, the same reason: this library's controlled vocabulary wins on values). Field children grow to fill it; a Button keeps its content width.",
968
968
  "name": "fullWidth",
969
969
  "type": "boolean"
970
970
  },
@@ -996,7 +996,7 @@
996
996
  "DON'T reach for `className=\"rounded-none\"` / `[&>*:not(:first-child)]` utilities to join controls yourself — `ui-audit`'s `no-utility-layout` rule blocks exactly that, and it is precisely the corner-radius seam this component owns.",
997
997
  "DO set `size` on each CHILD individually, not on SpaceCompact — the row has no size prop of its own by design (see `density`); every control already owns its own `size` axis (`xs|sm|md|lg`).",
998
998
  "DO use `fullWidth` inside a narrow form card so the joined row spans the field column, exactly like a lone Input would.",
999
- "`fullWidth` gives EVERY child an equal share of the row — each item carries `flex: 1 1 0%`, so the split is even no matter what the children would size to on their own. Measured at a 1169px row: NumberInput+Select 585/585, and Select+Button ALSO 585/585, i.e. a Button under `fullWidth` is stretched to half the row, not left at its content width. Want the Button at content width and the field absorbing the rest? Omit `fullWidth` — the row is then `inline-flex` and sizes to content (measured 223/40). Do NOT remove the per-child `flex` to chase antd's block style: without it the items collapse to their content and the row stops filling at all (measured 203/73.6 in a 1169px column), which defeats `fullWidth`.",
999
+ "`fullWidth` is antd `Space.Compact block` (gh#1062): in a row, a FIELD child (Input and its affix/Password/Search forms, NumberInput, Textarea, TagInput, the Date/Time pickers, and the Select/Cascader/TreeSelect/SearchSelect trigger) grows to fill the row, and every other child — a Button, an icon button — keeps its content width. So `[Input][+ Add]` is a full-width field with its submit at content width, and two fields (NumberInput + Select) share the row evenly between them. Deviation from antd, written down: antd grows only its Input family (`width: 100%`); Select/InputNumber/DatePicker keep their own width there. Here every field grows, so two fields never leave an empty tail. `orientation=\"vertical\"` + `fullWidth` stacks full-width children as before.",
1000
1000
  "DON'T expect corner-radius welding on `orientation=\"vertical\"` yet — the shared border still collapses, but each child keeps all four of its own corners rounded until a block-axis radius knob exists on the Input/trigger families (documented gap, not a silent one)."
1001
1001
  ],
1002
1002
  "useCases": [
@@ -2070,6 +2070,12 @@
2070
2070
  "description": "Take the remaining height of the parent instead of growing with the content. Default false keeps the pane content-height, which is right inside a scrolling page. Set it for an app-shell surface whose columns own their own scrolling — a chat transcript with a pinned composer, a full-height table beside a detail rail: the pane and both columns get a DEFINITE height, so overflow inside a column scrolls that column instead of pushing the page taller. The parent still decides how much height there is to fill: inside a flex column, give the wrapper `flex: 1; min-height: 0`.",
2071
2071
  "name": "fill",
2072
2072
  "type": "boolean"
2073
+ },
2074
+ {
2075
+ "defaultValue": "\"main-first\"",
2076
+ "description": "Which pane comes first when the pane is too narrow to split and the columns stack. `aside-first` puts the aside on top — a detail page whose properties card must not fall below a long comment thread on a phone. It moves the `<aside>` first in the DOM (not a CSS `order` swap), so in the stacked layout reading and Tab order match the screen (WCAG 1.3.2 / 2.4.3); once the pane splits, grid placement keeps main in the leading column and the aside inline-end, geometry identical to the default. No antd equivalent (antd's closest tool is Grid `Col order`, a visual-only reorder).",
2077
+ "name": "stackOrder",
2078
+ "type": "\"main-first\" | \"aside-first\""
2073
2079
  }
2074
2080
  ],
2075
2081
  "related": [
@@ -2086,11 +2092,12 @@
2086
2092
  "usage": [
2087
2093
  "DO: pass all right-panel content via the `aside` prop — it renders inside a semantic `<aside>` element at a fixed rem width (sm=20rem, md=22rem). The `children` prop fills the main `1fr` column. Both accept any ReactNode.",
2088
2094
  "DO: reach for `fill` when the pane is an app-shell surface rather than a block in a scrolling page — a chat transcript whose composer stays pinned to the bottom, a full-height table beside a detail rail. It is the supported way to say \"be as tall as what is left\"; a consumer that instead styles the pane's wrapper divs from outside (`[&>*]`, `[&>*>*]`) is targeting this component's internal DOM by position and will break the day another wrapper appears.",
2095
+ "DO: set `stackOrder=\"aside-first\"` when the aside is what a phone user needs first — an issue's properties / agent card above its comment thread. DON'T render the aside twice (once above, once beside) or reorder with a CSS `order` override: the prop already puts it first in the DOM, so focus order follows the stacked screen, and restores the side-by-side placement once the pane splits.",
2089
2096
  "DO: choose `asideWidth=\"sm\"` for compact detail panels (filters, quick stats, key-value summaries) and the default `asideWidth=\"md\"` for richer panels (forms, timelines, long metadata lists).",
2090
2097
  "DO: wrap SplitPane inside `PageContainer` or `PageContainer.Inset` — SplitPane provides no page padding of its own. It is a grid primitive, not a page scaffold.",
2091
2098
  "DO: close a collapsible rail with `aside={null}` — a Slack-style thread, a Linear-style detail panel — rather than swapping `<SplitPane aside={<Thread />}>{page}</SplitPane>` for a bare `{page}`. Both look the same on screen; only the prop keeps `children` mounted. The conditional swap changes the DEPTH of `{page}` in the React tree, so React remounts it: measured in a consumer as a message list jumping from scrollTop 400 to the bottom the instant a thread opened, losing the reader's place and any component state below it.",
2092
2099
  "DON'T: leave `SplitPane` mounted with an empty `aside={<div />}` to avoid that remount — an empty aside still reserves its rail, leaving a blank 20-30rem column. `aside={null}` removes the element AND the column (the pane sets `data-aside=\"closed\"` on itself, so the collapse is one CSS rule, not a call-site width override).",
2093
- "DON'T: expect two columns below 48rem OF THE PANE'S OWN WIDTH (64rem for `asideWidth=\"lg\"`). The split is a CSS container query on the pane, not a viewport media query, so a narrow embedded pane on a large screen correctly stays single-column and a wide pane on a small screen still splits. Below the threshold it stacks (main on top, aside below); for layouts that must stay side-by-side at any width use CSS Grid or `ResponsiveGrid`.",
2100
+ "DON'T: expect two columns below 48rem OF THE PANE'S OWN WIDTH (64rem for `asideWidth=\"lg\"`). The split is a CSS container query on the pane, not a viewport media query, so a narrow embedded pane on a large screen correctly stays single-column and a wide pane on a small screen still splits. Below the threshold it stacks (main on top, aside below — `stackOrder=\"aside-first\"` flips that); for layouts that must stay side-by-side at any width use CSS Grid or `ResponsiveGrid`.",
2094
2101
  "DON'T: add a CSS `overflow: hidden` or fixed height on the SplitPane wrapper; both columns carry `min-width: 0` to handle overflow correctly, and the grid uses `minmax(0, 1fr)` — adding external constraints will break the overflow contract.",
2095
2102
  "DON'T: hand-roll a two-column div layout with flexbox or CSS Grid when SplitPane already ships — that duplicates the responsive breakpoint logic and the semantic `<aside>` element."
2096
2103
  ],
@@ -3618,7 +3625,7 @@
3618
3625
  "DO use DataTable.Toolbar as the immediate child that wraps search/filter controls on the left and DataTable.DensityToggle/action buttons on the right. DataTable.BulkActions inside the toolbar auto-hides when selection count is 0; it accepts either plain ReactNode children (built-in 'N selected' status bar) or a (count)=>node render-prop (you own the whole bar).",
3619
3626
  "DO reach for the grid chrome (DataTable.Search, DataTable.ViewOptions, DataTable.Pagination pageSizeOptions) when you need global search, a column 'set view' picker, or numbered pagination — these are the merged former-DataGrid features, now on the one DataTable. Drive them client-side by default; pass the matching state + manual* flag for a server query.",
3620
3627
  "DataTable.Pagination OWNS ITS OWN INSET. The footer is a self-contained slot: it declares `padding-block` + `padding-inline` from `--table-pagination-padding-{y,x}` (block default = `--space-stack-sm`, inline default = `--table-cell-space-x`, so the 'rows per page' label lands on the same optical axis as the first column's text). Before the fix it declared `padding-top` only, so inside the documented flush container (`<Card><CardContent flush><DataTable/>`) the label and page-size Select sat flush against the container edge and border. DON'T ship a local `.ui-data-table-pagination { padding: … }` override in an app — retune the two tokens in your theme instead.",
3621
- "RESPONSIVE APPROVAL / ACTION QUEUE: reach for `preset=\"action-collection\"` when a dense five-column queue (requester · target · reason · requested date · row actions) must stay readable at 390px, and give every column a `priority` on its ColumnDef — `primary` (the row subject), `secondary` (its target), `meta` (a timestamp/id), `actions` (the row-action affordance, whose measure is reserved FIRST so it can never be pushed off-screen). Leave the free-text column unmarked; it takes the remaining space. This is the SAME contract, the SAME `--table-action-collection-*` tokens and the SAME container query as `Table preset=\"action-collection\"` — there is no separate DataTable family. Never add a consumer width, a hidden column, `hiddenOnMobile` or a page-local breakpoint to make a table fit: retune the tokens instead.",
3628
+ "RESPONSIVE APPROVAL / ACTION QUEUE: reach for `preset=\"action-collection\"` when a dense five-column queue (requester · target · reason · requested date · row actions) must stay readable at 390px, and give every column a `priority` on its ColumnDef — `primary` (the row subject), `secondary` (its target), `meta` (a timestamp/id), `actions` (the row-action affordance, whose measure is reserved FIRST so it can never be pushed off-screen, and which grows to its content when a text action + `…` menu does not fit the icon-sized token — gh#1067). Leave the free-text column unmarked; it takes the remaining space. This is the SAME contract, the SAME `--table-action-collection-*` tokens and the SAME container query as `Table preset=\"action-collection\"` — there is no separate DataTable family. Never add a consumer width, a hidden column, `hiddenOnMobile` or a page-local breakpoint to make a table fit: retune the tokens instead.",
3622
3629
  "DON'T set `width` on a column that also has a `priority` — an explicit width utility wins the cascade over the priority measure and re-opens the horizontal scroll. Under the preset, `pin: 'end'` is also redundant: nothing scrolls sideways, so the actions column is already in frame.",
3623
3630
  "The DataTable surface's narrow-viewport width floor is `--table-surface-min-inline-size` (default 640px, released at the `sm` viewport step). It used to be a hard-coded `min-w-[640px] sm:min-w-0` utility pair on the surface — the literal that forced the horizontal scroll at 390. Retune (or zero) the token in your theme; `preset=\"action-collection\"` already opts out of it.",
3624
3631
  "DO use ColumnDef.render for custom cell content (Badge, Link, RowActions). For plain string/number fields render can be omitted — DataTable falls back to String(row[key]).",
@@ -5200,7 +5207,7 @@
5200
5207
  "DO use `striped` on dense list tables so a row is easy to follow across its columns; to turn it on for EVERY Table and DataTable at once, set `:root { --table-row-striped-alpha: 100%; }` in the theme rather than passing the prop everywhere (`striped={false}` opts one table out). A detail row under a record is `<TableRow data-expanded-row=\"\">` so the stripe counts records, not DOM rows — never stripe with `:nth-child` page CSS or row utilities.",
5201
5208
  "DO NOT hand-roll empty-state handling inside a Table composition. When data can be empty, switch to `DataTable` (which has a built-in empty state) or wrap the `<Table>` with a conditional that renders `<EmptyState>` — never leave a table with only a header and zero rows.",
5202
5209
  "DO NOT use Table for lists that need sorting, filtering, pagination, or row selection — those features are only in `DataTable`. Table is intentionally stateless: it owns no TanStack Table instance, no column definitions, and no toolbar.",
5203
- "DO reach for `preset=\"action-collection\"` for a dense approval / action queue (requester · target · reason · requested date · row actions) that must stay readable at 390px, and mark every column with `priority` on BOTH its `TableHead` and its `TableCell`: `primary` (the row subject), `secondary` (its target), `meta` (a timestamp/id), `actions` (the row-action affordance, whose measure is reserved first so it can never be pushed off-screen). Leave the free-text column unmarked — it takes the remaining space. For text actions such as 対応する, set ColumnDef.width (or TableHead width) to reserve the label measure; leave a content column fluid. The default actions token is sized for icon actions. Do not add a hidden column or page-local breakpoint to make a table fit. The IDENTICAL preset exists on `DataTable` (`preset` + `collapseBelow` on the table, `priority` on the `ColumnDef`) sharing these same tokens — use DataTable when the queue is data-driven and needs sorting/selection/pagination, and reach for the raw `Table` only for a hand-authored queue.",
5210
+ "DO reach for `preset=\"action-collection\"` for a dense approval / action queue (requester · target · reason · requested date · row actions) that must stay readable at 390px, and mark every column with `priority` on BOTH its `TableHead` and its `TableCell`: `primary` (the row subject), `secondary` (its target), `meta` (a timestamp/id), `actions` (the row-action affordance, whose measure is reserved first so it can never be pushed off-screen). Leave the free-text column unmarked — it takes the remaining space. The `actions` token measure is a FLOOR sized for an icon trigger: a text action (対応する, Retry) plus a `…` menu grows the column to its content on its own (gh#1067), so it never paints over the previous column and needs no ColumnDef.width; an explicit width still wins. Leave a content column fluid. Do not add a hidden column or page-local breakpoint to make a table fit. The IDENTICAL preset exists on `DataTable` (`preset` + `collapseBelow` on the table, `priority` on the `ColumnDef`) sharing these same tokens — use DataTable when the queue is data-driven and needs sorting/selection/pagination, and reach for the raw `Table` only for a hand-authored queue.",
5204
5211
  "DO reach for `<TableCell flush>` when the cell's CONTENT owns its inset — an expanded detail panel under a row (`<TableCell flush colSpan={n}>`), a nested table, a full-bleed media strip. It drops the cell's own padding so the child spans the whole cell; without it the panel is indented by `--table-cell-space-x` and the only route was a `p-0` utility, which no service theme can reach.",
5205
5212
  "DO express hierarchy with `<TableCell indent={depth}>` — a grouped table's detail rows under their subtotal header, or a tree row under its parent. The measure is `--table-cell-space-x + depth x --table-cell-indent-space-step`, so level 0 sits on the column's own text axis and a service retunes (or flattens) the step in one token. Never hand-roll `style={{ paddingInlineStart }}` at the call site — that is a per-page constant no theme can reach, and it breaks in RTL unless you remember the logical property.",
5206
5213
  "DO place `<Table>` inside a `<CardContent flush>` (or `p-0` card) when embedding in a Card, so the built-in `overflow-auto` wrapper sits flush to the card edges. Wrapping with plain `<CardContent>` adds padding that clips the horizontal scroll shadow."
@@ -6194,7 +6201,7 @@
6194
6201
  "name": "Select",
6195
6202
  "props": [
6196
6203
  {
6197
- "description": "Select several options; value/defaultValue become string arrays. `tags` additionally ACCEPTS what was typed (a value that is not in the list), which is antd's own split between the two. The popup list of either mode renders as `Command split` (rows ruled, list padding 0, gh#699); retune it with --command-item-divider-color / --command-item-divider-width.",
6204
+ "description": "Select several options; value/defaultValue become string arrays. `tags` additionally ACCEPTS what was typed (a value that is not in the list), which is antd's own split between the two. `tags` needs no `options` (`<Select mode=\"tags\" />` works) and only an explicit `disabled` disables it (gh#1063). While text is typed the tags list reads create row, then an exact (case-folded) match, then options, then held values — the first enabled row is the Enter target, never a held tag (gh#1064). The popup list of either mode renders as `Command split` (rows ruled, list padding 0, gh#699); retune it with --command-item-divider-color / --command-item-divider-width.",
6198
6205
  "name": "mode",
6199
6206
  "type": "\"multiple\" | \"tags\""
6200
6207
  },
package/agent/index.json CHANGED
@@ -48,7 +48,7 @@
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.8.0/agent/index.json"
51
+ "pinned": "https://raw.githubusercontent.com/godx-jp/godxjp-ui/v31.10.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",
@@ -62,5 +62,5 @@
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.8.0"
65
+ "version": "31.10.0"
66
66
  }
package/agent/llms.txt CHANGED
@@ -1,10 +1,10 @@
1
1
  # @godxjp/ui
2
2
 
3
3
  > A Japanese-enterprise React design system: 177 components, 2104 design tokens,
4
- > 50 cardinal rules. This file is the entry point for AI agents. Catalog version 31.8.0.
4
+ > 50 cardinal rules. This file is the entry point for AI agents. Catalog version 31.10.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.8.0`). It is searchable and version-locked. These files exist for agents
7
+ (`npx @godxjp/ui-mcp@31.10.0`). It is searchable and version-locked. These files exist for agents
8
8
  that can only fetch URLs.
9
9
 
10
10
  ## Start
@@ -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.8.0/agent/...`. A catalog that does not match the installed
29
+ the tag: `.../godx-jp/godxjp-ui/v31.10.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.
package/agent/tokens.json CHANGED
@@ -11640,7 +11640,7 @@
11640
11640
  "value": "12%"
11641
11641
  },
11642
11642
  {
11643
- "description": "row-action affordance, never clipped",
11643
+ "description": "row-action affordance: a FLOOR, not a cap — Table grows the column to a wider actions cell content (a text button + `…` menu), gh#1067",
11644
11644
  "name": "--table-action-collection-actions-width",
11645
11645
  "tier": "component",
11646
11646
  "value": "3.5rem"
@@ -680,6 +680,7 @@ DataTable.BulkActions = function DataTableBulkActions({
680
680
  direction: "row",
681
681
  align: "center",
682
682
  gap: "sm",
683
+ wrap: true,
683
684
  role: "region",
684
685
  "aria-label": t("dataTable.bulkActions"),
685
686
  className,
@@ -8,6 +8,43 @@ import { cn } from "../../lib/utils.js";
8
8
  function scrollRegionLabel(label, t) {
9
9
  return typeof label === "string" && label.trim() !== "" ? label : t("dataTable.scrollRegion");
10
10
  }
11
+ const ACTIONS_CONTENT_WIDTH = "--table-action-collection-actions-content-width";
12
+ function useActionsColumnFit(ref, enabled) {
13
+ React.useLayoutEffect(() => {
14
+ const box = ref.current;
15
+ if (!enabled || !box || typeof ResizeObserver === "undefined") return void 0;
16
+ const measure = () => {
17
+ box.style.removeProperty(ACTIONS_CONTENT_WIDTH);
18
+ let needed = 0;
19
+ box.querySelectorAll(".ui-table-actions-content").forEach((content) => {
20
+ const cell = content.parentElement;
21
+ if (!cell) return;
22
+ const style = getComputedStyle(cell);
23
+ const inset = parseFloat(style.paddingInlineStart) + parseFloat(style.paddingInlineEnd);
24
+ const width = content.getBoundingClientRect().width;
25
+ if (width <= cell.clientWidth + 0.5) return;
26
+ const edges = style.boxSizing === "border-box" ? inset + parseFloat(style.borderInlineStartWidth) + parseFloat(style.borderInlineEndWidth) : 0;
27
+ needed = Math.max(needed, Math.ceil(width + edges));
28
+ });
29
+ if (needed > 0) box.style.setProperty(ACTIONS_CONTENT_WIDTH, `${needed}px`);
30
+ };
31
+ const resize = new ResizeObserver(measure);
32
+ const observeAll = () => {
33
+ resize.disconnect();
34
+ resize.observe(box);
35
+ box.querySelectorAll(".ui-table-actions-content").forEach((content) => resize.observe(content));
36
+ measure();
37
+ };
38
+ const mutations = new MutationObserver(observeAll);
39
+ mutations.observe(box, { childList: true, subtree: true });
40
+ observeAll();
41
+ return () => {
42
+ resize.disconnect();
43
+ mutations.disconnect();
44
+ box.style.removeProperty(ACTIONS_CONTENT_WIDTH);
45
+ };
46
+ }, [ref, enabled]);
47
+ }
11
48
  const Table = React.forwardRef(
12
49
  ({
13
50
  className,
@@ -23,6 +60,7 @@ const Table = React.forwardRef(
23
60
  const { t } = useTranslation();
24
61
  const scrollRef = React.useRef(null);
25
62
  const scrolls = useScrollsHorizontally(scrollRef, scrollable);
63
+ useActionsColumnFit(scrollRef, preset === "action-collection");
26
64
  return (
27
65
  // A table wider than its container scrolls horizontally in this wrapper; keep it
28
66
  // keyboard-reachable so it can be scrolled without a pointer (WCAG 2.1.1 / axe
@@ -162,7 +200,7 @@ const TableCell = React.forwardRef(
162
200
  ...props,
163
201
  children: [
164
202
  label !== void 0 ? /* @__PURE__ */ jsx("span", { className: "ui-table-stacked-collection-label", "aria-hidden": "true", children: label }) : null,
165
- children
203
+ priority === "actions" ? /* @__PURE__ */ jsx("span", { className: "ui-table-actions-content", children }) : children
166
204
  ]
167
205
  }
168
206
  )
@@ -161,22 +161,26 @@ function SearchSelect(props) {
161
161
  const isSelected = (optionValue) => multiple ? values.includes(optionValue) : value === optionValue;
162
162
  const hasSelection = multiple ? values.length > 0 : Boolean(value);
163
163
  const reqId = React.useRef(0);
164
+ const resetActiveRef = React.useRef(false);
165
+ const matchesQuery = React.useCallback(
166
+ (option, needle) => {
167
+ if (filterOption) return filterOption(option, needle);
168
+ const haystack = optionFilterProp ? [String(option[optionFilterProp] ?? "")] : [option.label, option.value];
169
+ return haystack.some((field) => fold(String(field)).includes(fold(needle)));
170
+ },
171
+ [filterOption, optionFilterProp, fold]
172
+ );
164
173
  const resolvedLoad = React.useMemo(
165
174
  () => loadOptions ?? (async ({ query: search }) => {
166
175
  const needle = search.trim();
167
176
  const list = staticOptions ?? [];
168
- const matches = (option) => {
169
- if (filterOption) return filterOption(option, needle);
170
- const haystack = optionFilterProp ? [String(option[optionFilterProp] ?? "")] : [option.label, option.value];
171
- return haystack.some((field) => fold(String(field)).includes(fold(needle)));
172
- };
173
- const kept = needle ? list.filter(matches) : list;
177
+ const kept = needle ? list.filter((option) => matchesQuery(option, needle)) : list;
174
178
  return {
175
179
  options: filterSort ? [...kept].sort((a, b) => filterSort(a, b, { searchValue: needle })) : kept,
176
180
  hasMore: false
177
181
  };
178
182
  }),
179
- [loadOptions, staticOptions, filterOption, optionFilterProp, filterSort, fold]
183
+ [loadOptions, staticOptions, matchesQuery, filterSort]
180
184
  );
181
185
  React.useEffect(() => {
182
186
  const handle = window.setTimeout(() => setDebouncedQuery(query.trim()), DEBOUNCE_MS);
@@ -191,10 +195,7 @@ function SearchSelect(props) {
191
195
  const result = await resolvedLoad({ query: search, page: nextPage });
192
196
  if (ticket !== reqId.current) return;
193
197
  setLoaded((prev) => append ? [...prev, ...result.options] : result.options);
194
- if (!append) {
195
- const firstEnabled = result.options.findIndex((option) => !option.disabled);
196
- setActiveIndex(firstEnabled >= 0 ? firstEnabled : 0);
197
- }
198
+ if (!append) resetActiveRef.current = true;
198
199
  setHasMore(Boolean(result.hasMore));
199
200
  setPage(nextPage);
200
201
  } catch {
@@ -221,18 +222,33 @@ function SearchSelect(props) {
221
222
  for (const entry of values) {
222
223
  if (known.has(entry)) continue;
223
224
  known.add(entry);
224
- extras.push(pickedOptions[entry] ?? { value: entry, label: entry });
225
+ const held = pickedOptions[entry] ?? { value: entry, label: entry };
226
+ if (!typedText || matchesQuery(held, typedText)) extras.push(held);
225
227
  }
226
228
  const typed = fold(typedText);
227
229
  const exists = [...known].some((entry) => fold(entry) === typed) || [...staticOptions ?? [], ...loaded].some(
228
230
  (option) => fold(option.value) === typed || fold(option.label) === typed
229
231
  );
230
232
  const create = typedText && !exists && canCreate(typedText) ? { value: typedText, label: typedText } : void 0;
233
+ const exact = typedText ? loaded.find(
234
+ (option) => !values.includes(option.value) && (fold(option.value) === typed || fold(option.label) === typed)
235
+ ) : void 0;
236
+ const rest = exact ? loaded.filter((option) => option !== exact) : loaded;
231
237
  return {
232
- displayOptions: create || extras.length ? [...create ? [create] : [], ...extras, ...loaded] : loaded,
238
+ displayOptions: create || exact || extras.length ? [...create ? [create] : [], ...exact ? [exact] : [], ...rest, ...extras] : loaded,
233
239
  createOption: create
234
240
  };
235
- }, [tagsMode, loaded, values, typedText, pickedOptions, staticOptions, fold, canCreate]);
241
+ }, [
242
+ tagsMode,
243
+ loaded,
244
+ values,
245
+ typedText,
246
+ pickedOptions,
247
+ staticOptions,
248
+ fold,
249
+ canCreate,
250
+ matchesQuery
251
+ ]);
236
252
  const grouped = React.useMemo(() => {
237
253
  const order = [];
238
254
  const buckets = /* @__PURE__ */ new Map();
@@ -254,6 +270,15 @@ function SearchSelect(props) {
254
270
  () => grouped.flatMap((group) => group.items.map((entry) => entry.option)),
255
271
  [grouped]
256
272
  );
273
+ const lastTypedRef = React.useRef(typedText);
274
+ React.useEffect(() => {
275
+ const typedChanged = lastTypedRef.current !== typedText;
276
+ lastTypedRef.current = typedText;
277
+ if (!typedChanged && !resetActiveRef.current) return;
278
+ resetActiveRef.current = false;
279
+ const firstEnabled = flatOrdered.findIndex((option) => !option.disabled);
280
+ setActiveIndex(firstEnabled >= 0 ? firstEnabled : 0);
281
+ }, [flatOrdered, typedText]);
257
282
  const resolvedPlaceholder = placeholder ?? t("dataEntry.searchSelect.placeholder");
258
283
  const optionFor = (optionValue) => pickedOptions[optionValue] ?? (staticOptions ?? []).find((option) => option.value === optionValue) ?? loaded.find((option) => option.value === optionValue) ?? null;
259
284
  const selectedOption = value ? optionFor(value) : null;
@@ -30,7 +30,7 @@ import { SearchSelect } from "./search-select.js";
30
30
  import { useTranslation } from "../../i18n/use-translation.js";
31
31
  import { normalizeSelectOptions } from "../../lib/select-options.js";
32
32
  function isDataSelect(props) {
33
- return "options" in props || "loadOptions" in props;
33
+ return "options" in props || "loadOptions" in props || props.mode !== void 0;
34
34
  }
35
35
  const SelectFieldA11yContext = React.createContext(null);
36
36
  function toKey(value) {
@@ -28,5 +28,20 @@ export type SplitPaneProps = {
28
28
  * not invent it. Inside a flex column give the wrapper `flex: 1; min-height: 0`.
29
29
  */
30
30
  fill?: boolean;
31
+ /**
32
+ * Which pane comes first when the pane is too narrow to split and the columns STACK.
33
+ *
34
+ * `main-first` (default) keeps today's order: main on top, aside below. `aside-first` puts the
35
+ * aside on top — for a detail page whose properties / agent card must not fall below a long
36
+ * comment thread on a phone.
37
+ *
38
+ * `aside-first` moves the `<aside>` FIRST IN THE DOM, not just visually, so in the stacked
39
+ * layout the reading and focus order match what is on screen (WCAG 1.3.2 / 2.4.3 — a CSS
40
+ * `order` swap would leave Tab going through the whole main column before reaching the aside
41
+ * drawn above it). Once the pane is wide enough to split, grid placement puts main back in the
42
+ * leading column and the aside in the trailing one (inline-end, so it flips under RTL); there
43
+ * the DOM order is aside → main, which reads as two independent regions, not a broken sequence.
44
+ */
45
+ stackOrder?: "main-first" | "aside-first";
31
46
  };
32
- export declare function SplitPane({ children, aside, asideWidth, asideLabel, fill, }: SplitPaneProps): import("react").JSX.Element;
47
+ export declare function SplitPane({ children, aside, asideWidth, asideLabel, fill, stackOrder, }: SplitPaneProps): import("react").JSX.Element;
@@ -4,9 +4,12 @@ function SplitPane({
4
4
  aside,
5
5
  asideWidth = "md",
6
6
  asideLabel,
7
- fill = false
7
+ fill = false,
8
+ stackOrder = "main-first"
8
9
  }) {
9
10
  const closed = aside === null || aside === void 0;
11
+ const asideFirst = stackOrder === "aside-first";
12
+ const asideElement = closed ? null : /* @__PURE__ */ jsx("aside", { className: "ui-split-pane-aside", "aria-label": asideLabel, children: aside });
10
13
  return (
11
14
  // `data-fill` is published on BOTH wrappers, not just the grid: the scope element is the one
12
15
  // the consumer's own box actually contains, so without it the height stops at the scope and
@@ -19,9 +22,11 @@ function SplitPane({
19
22
  "data-aside-width": asideWidth,
20
23
  "data-aside": closed ? "closed" : void 0,
21
24
  "data-fill": fill ? "true" : void 0,
25
+ "data-stack-order": asideFirst ? "aside-first" : void 0,
22
26
  children: [
27
+ asideFirst ? asideElement : null,
23
28
  /* @__PURE__ */ jsx("div", { className: "ui-split-pane-main", children }),
24
- closed ? null : /* @__PURE__ */ jsx("aside", { className: "ui-split-pane-aside", "aria-label": asideLabel, children: aside })
29
+ asideFirst ? null : asideElement
25
30
  ]
26
31
  }
27
32
  ) })
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "$comment": "AUTO-GENERATED by scripts/gen-measurement-contract.mjs — do not edit. Read this instead of guessing: docs/MEASUREMENT-CONTRACT.md.",
3
- "version": "31.8.0",
3
+ "version": "31.10.0",
4
4
  "targetSize": {
5
5
  "standard": "WCAG 2.2 SC 2.5.8 Target Size (Minimum), level AA — 24×24 CSS px",
6
6
  "min": 24,
@@ -1572,7 +1572,10 @@ export type SearchSelectSingleProp = {
1572
1572
  export type SearchSelectMultipleProp = {
1573
1573
  /**
1574
1574
  * `multiple` picks from the list; `tags` also ACCEPTS what was typed, so a value that is not in
1575
- * the list can still be committed (antd's own distinction between the two).
1575
+ * the list can still be committed (antd's own distinction between the two). A `tags` Select
1576
+ * needs no `options` at all and is never disabled for having none (gh#1063). While text is
1577
+ * typed its list reads create row → exact match → options → held values, and the first enabled
1578
+ * row is the active (Enter) row (gh#1064).
1576
1579
  */
1577
1580
  mode: "multiple" | "tags";
1578
1581
  /** @see SelectLabelInValueMultipleProp for the `{value,label}` dialect. */
@@ -378,7 +378,10 @@ export type SpaceCompactProp = Omit<React.HTMLAttributes<HTMLDivElement>, "child
378
378
  orientation?: OrientationProp;
379
379
  /** antd's boolean spelling of `orientation="vertical"`. `orientation` wins when both are set. */
380
380
  vertical?: boolean;
381
- /** Row fills its parent's inline size (antd `block`, renamed to match `Button.fullWidth`). */
381
+ /**
382
+ * Row fills its parent's inline size (antd `block`, renamed to match `Button.fullWidth`). Field
383
+ * children grow to fill it; a Button (any non-field child) keeps its content width (gh#1062).
384
+ */
382
385
  fullWidth?: boolean;
383
386
  /** Scoped control density for the whole row (antd `Space.Compact` `size`). */
384
387
  density?: DensityProp;
@@ -1,5 +1,5 @@
1
1
  {
2
- "version": "31.8.0",
2
+ "version": "31.10.0",
3
3
  "generatedBy": "scripts/gen-style-layers.mjs — do not edit by hand; run `pnpm gen:style-layers`",
4
4
  "base": "base.css",
5
5
  "fonts": "fonts.css",
@@ -138,6 +138,10 @@
138
138
  min-inline-size: 0;
139
139
  }
140
140
 
141
+ .ui-space-compact[data-orientation="horizontal"][data-full-width="true"] > [data-slot="space-compact-item"]:not(:has(.ui-control, .ui-control-multiline, .ui-tag-input)) {
142
+ flex: none;
143
+ }
144
+
141
145
  .ui-space-compact[data-orientation="vertical"] {
142
146
  flex-direction: column;
143
147
  }
@@ -608,6 +612,10 @@
608
612
  min-block-size: 0;
609
613
  }
610
614
 
615
+ .ui-split-pane[data-fill="true"][data-stack-order="aside-first"]:not([data-aside="closed"]) {
616
+ grid-template-rows: auto minmax(0, 1fr);
617
+ }
618
+
611
619
  .ui-resizable-panel-group {
612
620
  display: flex;
613
621
  min-height: 0;
@@ -662,12 +670,53 @@
662
670
  .ui-split-pane[data-aside-width="md"] {
663
671
  grid-template-columns: minmax(0, 1fr) 22rem;
664
672
  }
673
+
674
+ .ui-split-pane[data-stack-order="aside-first"]:is(
675
+ [data-aside-width="sm"],
676
+ [data-aside-width="md"]
677
+ )
678
+ > .ui-split-pane-main {
679
+ grid-column: 1;
680
+ grid-row: 1;
681
+ }
682
+
683
+ .ui-split-pane[data-stack-order="aside-first"]:is(
684
+ [data-aside-width="sm"],
685
+ [data-aside-width="md"]
686
+ )
687
+ > .ui-split-pane-aside {
688
+ grid-column: 2;
689
+ grid-row: 1;
690
+ }
691
+
692
+ .ui-split-pane[data-fill="true"][data-stack-order="aside-first"]:is(
693
+ [data-aside-width="sm"],
694
+ [data-aside-width="md"]
695
+ ):not([data-aside="closed"]) {
696
+ grid-template-rows: minmax(0, 1fr);
697
+ }
665
698
  }
666
699
 
667
700
  @container split-pane (min-width: 64rem) {
668
701
  .ui-split-pane[data-aside-width="lg"] {
669
702
  grid-template-columns: minmax(0, 1fr) 30rem;
670
703
  }
704
+
705
+ .ui-split-pane[data-stack-order="aside-first"][data-aside-width="lg"] > .ui-split-pane-main {
706
+ grid-column: 1;
707
+ grid-row: 1;
708
+ }
709
+
710
+ .ui-split-pane[data-stack-order="aside-first"][data-aside-width="lg"] > .ui-split-pane-aside {
711
+ grid-column: 2;
712
+ grid-row: 1;
713
+ }
714
+
715
+ .ui-split-pane[data-fill="true"][data-stack-order="aside-first"][data-aside-width="lg"]:not(
716
+ [data-aside="closed"]
717
+ ) {
718
+ grid-template-rows: minmax(0, 1fr);
719
+ }
671
720
  }
672
721
 
673
722
  .ui-split-pane-main,
@@ -231,6 +231,7 @@
231
231
 
232
232
  .ui-data-table-bulk {
233
233
  display: flex;
234
+ flex-wrap: wrap;
234
235
  align-items: center;
235
236
  gap: var(--space-inline-sm);
236
237
  font-size: var(--font-size-base);
@@ -238,6 +239,7 @@
238
239
 
239
240
  .ui-data-table-bulk-actions {
240
241
  display: flex;
242
+ flex-wrap: wrap;
241
243
  align-items: center;
242
244
  gap: var(--space-inline-sm);
243
245
  }
@@ -642,9 +644,23 @@
642
644
  }
643
645
 
644
646
  .ui-table-collection [data-priority="actions"] {
645
- --table-action-collection-column-width: var(--table-action-collection-actions-width);
647
+
648
+ --table-action-collection-column-width: var(
649
+ --table-action-collection-actions-content-width,
650
+ var(--table-action-collection-actions-width)
651
+ );
646
652
  text-align: end;
647
653
  }
654
+
655
+ .ui-table-actions-content {
656
+ display: contents;
657
+ }
658
+
659
+ .ui-table-collection .ui-table-actions-content {
660
+ display: inline-block;
661
+ white-space: nowrap;
662
+ vertical-align: top;
663
+ }
648
664
  }
649
665
 
650
666
  @layer components {
@@ -56,6 +56,7 @@
56
56
  --table-action-collection-primary-width: 18%;
57
57
  --table-action-collection-secondary-width: 22%;
58
58
  --table-action-collection-meta-width: 12%;
59
+
59
60
  --table-action-collection-actions-width: 3.5rem;
60
61
 
61
62
  --table-action-collection-primary-width-compact: 24%;
@@ -156,7 +156,7 @@ export default function Demo() {
156
156
  key: "action",
157
157
  header: "操作",
158
158
  priority: "actions",
159
- width: "104px",
159
+ // No width: the actions column grows to the text button on its own (gh#1067).
160
160
  render: () => (
161
161
  <Button size="sm" variant="ghost">
162
162
  対応する
@@ -546,7 +546,7 @@
546
546
  },
547
547
  "select": {
548
548
  "createTitle": "Tags: create row, onCreate and case-insensitive matching",
549
- "createDescription": "Typed text that matches no option is offered as an explicit “Create” row. onCreate reports only invented text, never an option id. Typing “bug” finds “Bug” instead of creating a duplicate (caseSensitive restores exact matching). Unlike antd, the create row is labelled and text longer than 12 characters is refused through allowCreate.",
549
+ "createDescription": "Typed text that matches no option is offered as an explicit “Create” row. onCreate reports only invented text, never an option id. Typing “bug” finds “Bug” instead of creating a duplicate (caseSensitive restores exact matching). Unlike antd, the create row is labelled and text longer than 12 characters is refused through allowCreate. While you type, the create row (or an option that exactly matches) is the first and active row, so Enter never takes off a tag you already picked; also unlike antd, that exact match is lifted to the top.",
550
550
  "createField": "Labels",
551
551
  "createdHelper": "Created in this session: {list}",
552
552
  "createdNone": "Nothing created yet"
@@ -561,6 +561,16 @@
561
561
  "presetsOnlyDescription": "panelRender returns only Presets — people pick from the palette and cannot type an arbitrary hex.",
562
562
  "tagColorLabel": "Tag colour",
563
563
  "tagColorHelper": "Arrow keys move between swatches; the selected one is announced."
564
+ },
565
+ "splitPaneStackOrder": {
566
+ "title": "stackOrder — the aside first when the panes stack",
567
+ "lead": "stackOrder=\"aside-first\" puts the aside above main once the pane is too narrow to split, and first in the DOM, so Tab reaches it first too. Wide enough to split, it sits in the trailing column exactly as it does by default. The embed below is narrower than the split width, so the properties card sits above the thread.",
568
+ "propertiesTitle": "Properties",
569
+ "status": "Status",
570
+ "statusValue": "In progress",
571
+ "assignee": "Assignee",
572
+ "threadTitle": "Comments",
573
+ "threadLead": "Long threads no longer push the properties below the fold on a phone."
564
574
  }
565
575
  },
566
576
  "serviceLauncherShowcase": {
@@ -1257,7 +1267,7 @@
1257
1267
  },
1258
1268
  "search": {
1259
1269
  "title": "Search and its button",
1260
- "body": "Typing and running are one continuous action, so the seam goes. The button is still a real Button — focus ring and disabled keep working.",
1270
+ "body": "Typing and running are one continuous action, so the seam goes. The button is still a real Button — focus ring and disabled keep working. With `fullWidth` the field fills the row and the button keeps its own width (antd `block`).",
1261
1271
  "row": "Search employees",
1262
1272
  "term": "Search term",
1263
1273
  "run": "Run the search",
@@ -540,7 +540,7 @@
540
540
  },
541
541
  "select": {
542
542
  "createTitle": "タグ: 作成行・onCreate・大文字小文字を区別しない照合",
543
- "createDescription": "どの選択肢にも一致しない入力は「作成」行として明示されます。onCreate は新しく作られたテキストだけを通知し、選択肢の ID では発火しません。「bug」と入力すると「Bug」が見つかり、重複は作られません(caseSensitive で完全一致に戻せます)。antd と異なり作成行にはラベルが付き、12 文字を超える入力は allowCreate で拒否しています。",
543
+ "createDescription": "どの選択肢にも一致しない入力は「作成」行として明示されます。onCreate は新しく作られたテキストだけを通知し、選択肢の ID では発火しません。「bug」と入力すると「Bug」が見つかり、重複は作られません(caseSensitive で完全一致に戻せます)。antd と異なり作成行にはラベルが付き、12 文字を超える入力は allowCreate で拒否しています。入力中は作成行(または完全一致する選択肢)が先頭のアクティブ行になるため、Enter で選択済みのタグが外れることはありません。完全一致を先頭に上げる点も antd と異なります。",
544
544
  "createField": "ラベル",
545
545
  "createdHelper": "このセッションで作成: {list}",
546
546
  "createdNone": "まだ作成されていません"
@@ -555,6 +555,16 @@
555
555
  "presetsOnlyDescription": "panelRender で Presets だけを返すと、パレットからの選択のみになり任意の HEX は入力できない。",
556
556
  "tagColorLabel": "タグの色",
557
557
  "tagColorHelper": "矢印キーでスウォッチ間を移動し、選択中の色が読み上げられる。"
558
+ },
559
+ "splitPaneStackOrder": {
560
+ "title": "stackOrder — 縦積み時にサイドを先頭へ",
561
+ "lead": "stackOrder=\"aside-first\" は、分割できない幅になったときにサイドをメインの上に置き、DOM でも先頭にするため Tab でも先に届きます。分割できる幅では既定と同じく後方の列に並びます。下の埋め込みは分割幅より狭いため、プロパティがスレッドの上に来ます。",
562
+ "propertiesTitle": "プロパティ",
563
+ "status": "ステータス",
564
+ "statusValue": "対応中",
565
+ "assignee": "担当者",
566
+ "threadTitle": "コメント",
567
+ "threadLead": "スマートフォンでも、長いスレッドがプロパティを画面外へ押し出しません。"
558
568
  }
559
569
  },
560
570
  "serviceLauncherShowcase": {
@@ -1251,7 +1261,7 @@
1251
1261
  },
1252
1262
  "search": {
1253
1263
  "title": "検索とボタン",
1254
- "body": "入力と実行は一続きの操作なので継ぎ目をなくします。ボタンは本物の Button のままなので、フォーカスリングも disabled もそのまま効きます。",
1264
+ "body": "入力と実行は一続きの操作なので継ぎ目をなくします。ボタンは本物の Button のままなので、フォーカスリングも disabled もそのまま効きます。`fullWidth` では入力欄が行の残りを埋め、ボタンは自分の幅のままです(antd の `block`)。",
1255
1265
  "row": "社員を検索",
1256
1266
  "term": "検索語",
1257
1267
  "run": "検索する",
@@ -540,7 +540,7 @@
540
540
  },
541
541
  "select": {
542
542
  "createTitle": "Tags: dòng tạo mới, onCreate và so khớp không phân biệt hoa thường",
543
- "createDescription": "Văn bản gõ vào không khớp tùy chọn nào được đưa ra thành dòng “Tạo” rõ ràng. onCreate chỉ báo văn bản mới tạo, không bao giờ báo id của tùy chọn. Gõ “bug” sẽ tìm thấy “Bug” thay vì tạo bản trùng (caseSensitive trả lại so khớp chính xác). Khác antd, dòng tạo có nhãn và văn bản dài hơn 12 ký tự bị allowCreate từ chối.",
543
+ "createDescription": "Văn bản gõ vào không khớp tùy chọn nào được đưa ra thành dòng “Tạo” rõ ràng. onCreate chỉ báo văn bản mới tạo, không bao giờ báo id của tùy chọn. Gõ “bug” sẽ tìm thấy “Bug” thay vì tạo bản trùng (caseSensitive trả lại so khớp chính xác). Khác antd, dòng tạo có nhãn và văn bản dài hơn 12 ký tự bị allowCreate từ chối. Khi đang gõ, dòng tạo (hoặc tùy chọn khớp chính xác) là dòng đầu tiên và đang được chọn, nên Enter không bao giờ gỡ một thẻ đã chọn; cũng khác antd, tùy chọn khớp chính xác được đưa lên đầu.",
544
544
  "createField": "Nhãn",
545
545
  "createdHelper": "Đã tạo trong phiên này: {list}",
546
546
  "createdNone": "Chưa tạo gì"
@@ -555,6 +555,16 @@
555
555
  "presetsOnlyDescription": "panelRender chỉ trả về Presets — người dùng chọn trong bảng màu, không gõ được mã hex tuỳ ý.",
556
556
  "tagColorLabel": "Màu thẻ",
557
557
  "tagColorHelper": "Phím mũi tên di chuyển giữa các ô màu; ô đang chọn được đọc lên."
558
+ },
559
+ "splitPaneStackOrder": {
560
+ "title": "stackOrder — đưa cột phụ lên trước khi xếp chồng",
561
+ "lead": "stackOrder=\"aside-first\" đặt cột phụ lên trên cột chính khi khung quá hẹp để chia đôi, và đặt nó trước trong DOM nên phím Tab cũng tới nó trước. Khi đủ rộng để chia, nó nằm ở cột cuối như mặc định. Khung nhúng bên dưới hẹp hơn ngưỡng chia đôi, nên thẻ thuộc tính nằm trên luồng bình luận.",
562
+ "propertiesTitle": "Thuộc tính",
563
+ "status": "Trạng thái",
564
+ "statusValue": "Đang xử lý",
565
+ "assignee": "Người phụ trách",
566
+ "threadTitle": "Bình luận",
567
+ "threadLead": "Trên điện thoại, luồng bình luận dài không còn đẩy phần thuộc tính xuống dưới."
558
568
  }
559
569
  },
560
570
  "serviceLauncherShowcase": {
@@ -1251,7 +1261,7 @@
1251
1261
  },
1252
1262
  "search": {
1253
1263
  "title": "Tìm kiếm và nút của nó",
1254
- "body": "Gõ và chạy là một thao tác liền mạch nên bỏ đường nối. Nút vẫn là Button thật — vòng focus và disabled vẫn hoạt động.",
1264
+ "body": "Gõ và chạy là một thao tác liền mạch nên bỏ đường nối. Nút vẫn là Button thật — vòng focus và disabled vẫn hoạt động. Với `fullWidth`, ô nhập lấp phần còn lại của hàng, nút giữ đúng bề rộng của nó (antd `block`).",
1255
1265
  "row": "Tìm nhân viên",
1256
1266
  "term": "Từ khoá",
1257
1267
  "run": "Chạy tìm kiếm",
@@ -110,7 +110,7 @@ export default function SpaceCompactShowcase() {
110
110
  <CardDescription>{t("spaceCompactDocs.search.body")}</CardDescription>
111
111
  </CardHeader>
112
112
  <CardContent>
113
- <SpaceCompact aria-label={t("spaceCompactDocs.search.row")}>
113
+ <SpaceCompact fullWidth aria-label={t("spaceCompactDocs.search.row")}>
114
114
  <SearchInput
115
115
  value={query}
116
116
  onValueChange={setQuery}
@@ -10,6 +10,7 @@ import {
10
10
  StatCard,
11
11
  } from "@godxjp/ui/data-display";
12
12
  import { Button, Text } from "@godxjp/ui/general";
13
+ import { useTranslation } from "@godxjp/ui/i18n";
13
14
  import { FileText, Building2, Calendar, CreditCard, ArrowRight } from "lucide-react";
14
15
 
15
16
  /**
@@ -98,6 +99,7 @@ const yen = (n: number) => yenFormat.format(Math.round(n));
98
99
  const toNumber = (amount: string) => Number(amount.replace(/[^0-9]/g, ""));
99
100
 
100
101
  export default function Demo() {
102
+ const { t } = useTranslation();
101
103
  const [selectedId, setSelectedId] = useState<string>("INV-0241");
102
104
  const [asideWidth, setAsideWidth] = useState<"sm" | "md">("md");
103
105
  const [threadOpen, setThreadOpen] = useState(true);
@@ -406,6 +408,69 @@ export default function Demo() {
406
408
  </CardContent>
407
409
  </Card>
408
410
 
411
+ {/* ── stackOrder="aside-first" (gh#1057) — the aside stacks ABOVE main when narrow ── */}
412
+ <Card>
413
+ <CardHeader>
414
+ <CardTitle level={2}>{t("showcase.splitPaneStackOrder.title")}</CardTitle>
415
+ <CardDescription>{t("showcase.splitPaneStackOrder.lead")}</CardDescription>
416
+ </CardHeader>
417
+ <CardContent>
418
+ <Card className="max-w-md">
419
+ <CardContent>
420
+ <SplitPane
421
+ stackOrder="aside-first"
422
+ asideWidth="sm"
423
+ asideLabel={t("showcase.splitPaneStackOrder.propertiesTitle")}
424
+ aside={
425
+ <Card variant="muted">
426
+ <CardHeader>
427
+ <CardTitle level={3} className="text-sm">
428
+ {t("showcase.splitPaneStackOrder.propertiesTitle")}
429
+ </CardTitle>
430
+ </CardHeader>
431
+ <CardContent>
432
+ <Flex direction="col" gap="xs">
433
+ <Flex direction="row" justify="between" gap="sm">
434
+ <Text tone="muted">{t("showcase.splitPaneStackOrder.status")}</Text>
435
+ <Badge tone="info">
436
+ {t("showcase.splitPaneStackOrder.statusValue")}
437
+ </Badge>
438
+ </Flex>
439
+ <Flex direction="row" justify="between" gap="sm">
440
+ <Text tone="muted">{t("showcase.splitPaneStackOrder.assignee")}</Text>
441
+ <Text>{THREAD_REPLIES[0].author}</Text>
442
+ </Flex>
443
+ </Flex>
444
+ </CardContent>
445
+ </Card>
446
+ }
447
+ >
448
+ <Card>
449
+ <CardHeader>
450
+ <CardTitle level={3} className="text-sm">
451
+ {t("showcase.splitPaneStackOrder.threadTitle")}
452
+ </CardTitle>
453
+ <CardDescription>
454
+ {t("showcase.splitPaneStackOrder.threadLead")}
455
+ </CardDescription>
456
+ </CardHeader>
457
+ <CardContent>
458
+ <Flex direction="col" gap="sm">
459
+ {CHANNEL_MESSAGES.map((message) => (
460
+ <Flex key={message.author} direction="col" gap="xs">
461
+ <Text weight="medium">{message.author}</Text>
462
+ <Text tone="muted">{message.body}</Text>
463
+ </Flex>
464
+ ))}
465
+ </Flex>
466
+ </CardContent>
467
+ </Card>
468
+ </SplitPane>
469
+ </CardContent>
470
+ </Card>
471
+ </CardContent>
472
+ </Card>
473
+
409
474
  <Card>
410
475
  <CardHeader>
411
476
  <CardTitle level={2}>Container stress · narrow vs wide embed</CardTitle>
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@godxjp/ui",
3
- "version": "31.8.0",
4
- "godxUiMcp": "31.8.0",
3
+ "version": "31.10.0",
4
+ "godxUiMcp": "31.10.0",
5
5
  "license": "Apache-2.0",
6
6
  "repository": {
7
7
  "type": "git",