@godxjp/ui 30.3.1 → 30.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -47,7 +47,7 @@ Then ask it: `search_components`, `get_component`, `get_tokens`, `get_rule`, `li
47
47
  task is a task** — "build a settings page", "confirm a destructive delete", "a list page with
48
48
  filters" — start HERE, not at the components. Then fetch `patterns/<name>.json` for complete,
49
49
  copy-paste-ready code. A component index answers "does X exist"; it cannot answer "build Y".
50
- 1. `components-index.json` — 45 KB, all 171 components as name + group +
50
+ 1. `components-index.json` — 45 KB, all 172 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
@@ -76,14 +76,14 @@
76
76
  "type": "React.ReactNode"
77
77
  },
78
78
  {
79
- "description": "Slot BELOW the draft row — a hint line, a token counter.",
79
+ "description": "Slot BELOW the draft row — a hint line, a token counter, OR (Ant Design X Sender NodeRender) a function that receives the real SubmitButton/CancelButton components, pre-wired, so they can be rendered in the footer instead of beside the draft box. Pair with actions={false} to hide the inline pair.",
80
80
  "name": "footer",
81
- "type": "React.ReactNode"
81
+ "type": "React.ReactNode | ((info: { components: ChatComposerActionComponents }) => React.ReactNode)"
82
82
  },
83
83
  {
84
- "description": "Extra trailing actions, rendered BEFORE the send/cancel action.",
84
+ "description": "Extra trailing actions, rendered BEFORE the send/cancel action — or false (Ant Design X Sender suffix semantics) to hide the ENTIRE inline trailing-action cell, built-in send/cancel button included, once footer has taken it over. Omitted renders exactly as before.",
85
85
  "name": "actions",
86
- "type": "React.ReactNode"
86
+ "type": "React.ReactNode | false"
87
87
  },
88
88
  {
89
89
  "defaultValue": "\"md\"",
@@ -148,11 +148,13 @@
148
148
  "DON'T render your own stop button beside the send button — set `loading` and the trailing action becomes cancel. Exactly one trailing action exists at a time (the picker trailing-action discipline).",
149
149
  "DO put a hint in `footer` (t('dataEntry.chatComposer.hintEnter') / 'hintShiftEnter') when you flip `submitType` — the keystroke contract is invisible otherwise. For submitType=\"modEnter\" use t('dataEntry.chatComposer.hintModEnter', { modifier: isApplePlatform() ? '⌘' : 'Ctrl' }) with isApplePlatform from @godxjp/ui/lib/utils — the same platform test the composer uses to pick metaKey vs ctrlKey.",
150
150
  "DO set `allowEmptySubmit` (not a hidden fake draft) when the composer also submits field changes from `header`/`footer`; your onSubmit receives \"\" and decides whether anything changed.",
151
- "DON'T size it with a className height: the box grows between --chat-composer-min-height and --chat-composer-max-height, both derived from the --control-height tier. Use `size`, or re-tune the two tokens in your theme."
151
+ "DON'T size it with a className height: the box grows between --chat-composer-min-height and --chat-composer-max-height, both derived from the --control-height tier. Use `size`, or re-tune the two tokens in your theme.",
152
+ "DO move the toolbar BELOW a full-width draft box with `actions={false}` + a function `footer` — the function receives `{ components: { SubmitButton, CancelButton } }`, both already wired to onSubmit/onCancel/disabled/the loading swap, so nothing about the send contract is reimplemented at the call site. Put the attach control and a keyboard hint on the START side of the footer row and `<SubmitButton />` on the END side (a `Flex justify=\"between\"`), exactly like the record-comment box case. Leaving `actions` unset keeps today's inline row — this is opt-in, not a breaking change."
152
153
  ],
153
154
  "useCases": [
154
155
  "The message box of an AI assistant or support chat, under a ChatBubbleList feed.",
155
156
  "A comment composer on a record detail screen (prefix = attach Button, footer = character counter).",
157
+ "A record-detail comment box with a full-width textarea and a toolbar row BELOW it — attach on the start side, a keyboard hint in the middle, clear/send on the end side: `actions={false}` + `footer={({ components }) => <Flex justify=\"between\">…<components.SubmitButton /></Flex>}`.",
156
158
  "A long-form reply box where Enter must break the line: submitType=\"shiftEnter\".",
157
159
  "A comment bar on an issue/record where Enter breaks the line and ⌘/Ctrl+Enter posts, and a status change may be posted without text: submitType=\"modEnter\" + allowEmptySubmit.",
158
160
  "A streaming answer the user can stop: loading + onCancel."
@@ -0,0 +1,60 @@
1
+ {
2
+ "docPath": "layout/space-compact.tsx",
3
+ "example": "import { SpaceCompact } from \"@godxjp/ui/layout\";\nimport { FormField, NumberInput, Select } from \"@godxjp/ui/data-entry\";\n\n<FormField label=\"繰り返し間隔\">\n <SpaceCompact>\n <NumberInput aria-label=\"間隔の数\" min={1} defaultValue={2} />\n <Select\n aria-label=\"単位\"\n defaultValue=\"week\"\n options={[\n { value: \"day\", label: \"日\" },\n { value: \"week\", label: \"週\" },\n { value: \"month\", label: \"月\" },\n ]}\n />\n </SpaceCompact>\n</FormField>",
4
+ "group": "layout",
5
+ "importPath": "@godxjp/ui/layout",
6
+ "name": "SpaceCompact",
7
+ "props": [
8
+ {
9
+ "defaultValue": "\"horizontal\"",
10
+ "description": "Layout axis (antd `orientation`; the installed antd `direction` prop is itself @deprecated in favour of this, so only `orientation` is ported).",
11
+ "name": "orientation",
12
+ "type": "\"horizontal\" | \"vertical\""
13
+ },
14
+ {
15
+ "description": "antd's boolean spelling of orientation=\"vertical\". `orientation` wins when both are given.",
16
+ "name": "vertical",
17
+ "type": "boolean"
18
+ },
19
+ {
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).",
22
+ "name": "fullWidth",
23
+ "type": "boolean"
24
+ },
25
+ {
26
+ "description": "Scoped control density for the whole row (antd Space.Compact `size`, small|middle|large — the ConfigProvider ambient-size cascade this library already owns under `density`; see `Form.density`). Each child still carries its OWN `size` prop.",
27
+ "name": "density",
28
+ "type": "\"compact\" | \"default\" | \"comfortable\""
29
+ },
30
+ {
31
+ "description": "DOM id of the row.",
32
+ "name": "id",
33
+ "type": "string"
34
+ }
35
+ ],
36
+ "related": [
37
+ "Flex `gap` — siblings that stay visually SEPARATE (a normal control row with spacing between items). SpaceCompact is the opposite case: controls that must read as ONE box.",
38
+ "ResponsiveGrid — equal-width form CELLS; SpaceCompact welds controls WITHIN one cell (or one row) into a single unit.",
39
+ "FormField — wraps the whole SpaceCompact with one label when the pair is one field.",
40
+ "NumberInput, Select — the two controls the shipping use case welds; either composes cleanly inside SpaceCompact because both already expose the radius knob it reads (`--input-radius-*`, `--control-trigger-radius-*`)."
41
+ ],
42
+ "rules": [
43
+ 2
44
+ ],
45
+ "storyPath": "layout/SpaceCompact.stories.tsx",
46
+ "tagline": "Weld a row (or column) of controls into one visual unit — antd Space.Compact. Inner corner radii are zeroed and the shared border collapses to one hairline; each child keeps its own focus ring and its own size.",
47
+ "usage": [
48
+ "DO reach for it when two or more controls must read as ONE field — a number + a unit (\"毎 [2] [週 ▾] ごと\"), a currency Select welded to an amount Input, a search field with an attached filter Select. `ResponsiveGrid columns={2}` puts them in separate cells (which collapses to ONE column on a narrow form card); SpaceCompact keeps them fused on one line regardless of the grid.",
49
+ "ONE label for the pair vs per-control: wrap the WHOLE `SpaceCompact` in a single `FormField label=\"…\"` when the pair reads as one field (\"繰り返し間隔\" over a number+unit row) — a role-less `<div>` cannot carry a cloned `aria-label`/`aria-labelledby` (axe `aria-allowed-attr`), so a NAMED SpaceCompact with no explicit `role` promotes itself to `role=\"group\"` automatically, the same contract `Flex` already honours for a range/年月 pair. Give each CHILD its own `aria-label` instead only when the two controls are independently meaningful outside the row (rare — prefer the one-label pattern for a welded control).",
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
+ "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
+ "DO use `fullWidth` inside a narrow form card so the joined row spans the field column, exactly like a lone Input would.",
53
+ "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)."
54
+ ],
55
+ "useCases": [
56
+ "定期課題 (recurring issue) interval row: 毎 [NumberInput] [Select 週/月/日 ▾] ごと, fused on one line inside a FormField, inside a 2-column ResponsiveGrid form card that collapses to one column.",
57
+ "A currency amount: [Select 通貨 ▾][NumberInput 金額] welded so the currency reads as part of the amount field, not a separate control beside it.",
58
+ "A filtered search bar: [Select scope][SearchInput query] as one visual field, `fullWidth` inside a page toolbar."
59
+ ]
60
+ }
@@ -54,6 +54,11 @@
54
54
  "name": "Flex",
55
55
  "tagline": "Token-spaced flex primitive with explicit direction, alignment, justification, and wrapping controls."
56
56
  },
57
+ {
58
+ "group": "layout",
59
+ "name": "SpaceCompact",
60
+ "tagline": "Weld a row (or column) of controls into one visual unit — antd Space.Compact. Inner corner radii are zeroed and the shared border collapses to one hairline; each child keeps its own focus ring and its own size."
61
+ },
57
62
  {
58
63
  "group": "layout",
59
64
  "name": "ResponsiveGrid",
@@ -938,6 +938,66 @@
938
938
  "Low-level layout composition inside custom components where raw flex classes would duplicate the primitive."
939
939
  ]
940
940
  },
941
+ {
942
+ "docPath": "layout/space-compact.tsx",
943
+ "example": "import { SpaceCompact } from \"@godxjp/ui/layout\";\nimport { FormField, NumberInput, Select } from \"@godxjp/ui/data-entry\";\n\n<FormField label=\"繰り返し間隔\">\n <SpaceCompact>\n <NumberInput aria-label=\"間隔の数\" min={1} defaultValue={2} />\n <Select\n aria-label=\"単位\"\n defaultValue=\"week\"\n options={[\n { value: \"day\", label: \"日\" },\n { value: \"week\", label: \"週\" },\n { value: \"month\", label: \"月\" },\n ]}\n />\n </SpaceCompact>\n</FormField>",
944
+ "group": "layout",
945
+ "importPath": "@godxjp/ui/layout",
946
+ "name": "SpaceCompact",
947
+ "props": [
948
+ {
949
+ "defaultValue": "\"horizontal\"",
950
+ "description": "Layout axis (antd `orientation`; the installed antd `direction` prop is itself @deprecated in favour of this, so only `orientation` is ported).",
951
+ "name": "orientation",
952
+ "type": "\"horizontal\" | \"vertical\""
953
+ },
954
+ {
955
+ "description": "antd's boolean spelling of orientation=\"vertical\". `orientation` wins when both are given.",
956
+ "name": "vertical",
957
+ "type": "boolean"
958
+ },
959
+ {
960
+ "defaultValue": "false",
961
+ "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).",
962
+ "name": "fullWidth",
963
+ "type": "boolean"
964
+ },
965
+ {
966
+ "description": "Scoped control density for the whole row (antd Space.Compact `size`, small|middle|large — the ConfigProvider ambient-size cascade this library already owns under `density`; see `Form.density`). Each child still carries its OWN `size` prop.",
967
+ "name": "density",
968
+ "type": "\"compact\" | \"default\" | \"comfortable\""
969
+ },
970
+ {
971
+ "description": "DOM id of the row.",
972
+ "name": "id",
973
+ "type": "string"
974
+ }
975
+ ],
976
+ "related": [
977
+ "Flex `gap` — siblings that stay visually SEPARATE (a normal control row with spacing between items). SpaceCompact is the opposite case: controls that must read as ONE box.",
978
+ "ResponsiveGrid — equal-width form CELLS; SpaceCompact welds controls WITHIN one cell (or one row) into a single unit.",
979
+ "FormField — wraps the whole SpaceCompact with one label when the pair is one field.",
980
+ "NumberInput, Select — the two controls the shipping use case welds; either composes cleanly inside SpaceCompact because both already expose the radius knob it reads (`--input-radius-*`, `--control-trigger-radius-*`)."
981
+ ],
982
+ "rules": [
983
+ 2
984
+ ],
985
+ "storyPath": "layout/SpaceCompact.stories.tsx",
986
+ "tagline": "Weld a row (or column) of controls into one visual unit — antd Space.Compact. Inner corner radii are zeroed and the shared border collapses to one hairline; each child keeps its own focus ring and its own size.",
987
+ "usage": [
988
+ "DO reach for it when two or more controls must read as ONE field — a number + a unit (\"毎 [2] [週 ▾] ごと\"), a currency Select welded to an amount Input, a search field with an attached filter Select. `ResponsiveGrid columns={2}` puts them in separate cells (which collapses to ONE column on a narrow form card); SpaceCompact keeps them fused on one line regardless of the grid.",
989
+ "ONE label for the pair vs per-control: wrap the WHOLE `SpaceCompact` in a single `FormField label=\"…\"` when the pair reads as one field (\"繰り返し間隔\" over a number+unit row) — a role-less `<div>` cannot carry a cloned `aria-label`/`aria-labelledby` (axe `aria-allowed-attr`), so a NAMED SpaceCompact with no explicit `role` promotes itself to `role=\"group\"` automatically, the same contract `Flex` already honours for a range/年月 pair. Give each CHILD its own `aria-label` instead only when the two controls are independently meaningful outside the row (rare — prefer the one-label pattern for a welded control).",
990
+ "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.",
991
+ "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`).",
992
+ "DO use `fullWidth` inside a narrow form card so the joined row spans the field column, exactly like a lone Input would.",
993
+ "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)."
994
+ ],
995
+ "useCases": [
996
+ "定期課題 (recurring issue) interval row: 毎 [NumberInput] [Select 週/月/日 ▾] ごと, fused on one line inside a FormField, inside a 2-column ResponsiveGrid form card that collapses to one column.",
997
+ "A currency amount: [Select 通貨 ▾][NumberInput 金額] welded so the currency reads as part of the amount field, not a separate control beside it.",
998
+ "A filtered search bar: [Select scope][SearchInput query] as one visual field, `fullWidth` inside a page toolbar."
999
+ ]
1000
+ },
941
1001
  {
942
1002
  "example": "import { ResponsiveGrid } from \"@godxjp/ui/layout\";\nimport { StatCard } from \"@godxjp/ui/data-display\";\n\n<ResponsiveGrid columns={4}>\n <StatCard label=\"総会員数\" value=\"12,400\" />\n <StatCard label=\"公開中クーポン\" value=\"8\" />\n <StatCard label=\"月間利用数\" value=\"3,210\" />\n <StatCard label=\"割引総額\" value=\"¥480,000\" />\n</ResponsiveGrid>",
943
1003
  "group": "layout",
@@ -14713,14 +14773,14 @@
14713
14773
  "type": "React.ReactNode"
14714
14774
  },
14715
14775
  {
14716
- "description": "Slot BELOW the draft row — a hint line, a token counter.",
14776
+ "description": "Slot BELOW the draft row — a hint line, a token counter, OR (Ant Design X Sender NodeRender) a function that receives the real SubmitButton/CancelButton components, pre-wired, so they can be rendered in the footer instead of beside the draft box. Pair with actions={false} to hide the inline pair.",
14717
14777
  "name": "footer",
14718
- "type": "React.ReactNode"
14778
+ "type": "React.ReactNode | ((info: { components: ChatComposerActionComponents }) => React.ReactNode)"
14719
14779
  },
14720
14780
  {
14721
- "description": "Extra trailing actions, rendered BEFORE the send/cancel action.",
14781
+ "description": "Extra trailing actions, rendered BEFORE the send/cancel action — or false (Ant Design X Sender suffix semantics) to hide the ENTIRE inline trailing-action cell, built-in send/cancel button included, once footer has taken it over. Omitted renders exactly as before.",
14722
14782
  "name": "actions",
14723
- "type": "React.ReactNode"
14783
+ "type": "React.ReactNode | false"
14724
14784
  },
14725
14785
  {
14726
14786
  "defaultValue": "\"md\"",
@@ -14785,11 +14845,13 @@
14785
14845
  "DON'T render your own stop button beside the send button — set `loading` and the trailing action becomes cancel. Exactly one trailing action exists at a time (the picker trailing-action discipline).",
14786
14846
  "DO put a hint in `footer` (t('dataEntry.chatComposer.hintEnter') / 'hintShiftEnter') when you flip `submitType` — the keystroke contract is invisible otherwise. For submitType=\"modEnter\" use t('dataEntry.chatComposer.hintModEnter', { modifier: isApplePlatform() ? '⌘' : 'Ctrl' }) with isApplePlatform from @godxjp/ui/lib/utils — the same platform test the composer uses to pick metaKey vs ctrlKey.",
14787
14847
  "DO set `allowEmptySubmit` (not a hidden fake draft) when the composer also submits field changes from `header`/`footer`; your onSubmit receives \"\" and decides whether anything changed.",
14788
- "DON'T size it with a className height: the box grows between --chat-composer-min-height and --chat-composer-max-height, both derived from the --control-height tier. Use `size`, or re-tune the two tokens in your theme."
14848
+ "DON'T size it with a className height: the box grows between --chat-composer-min-height and --chat-composer-max-height, both derived from the --control-height tier. Use `size`, or re-tune the two tokens in your theme.",
14849
+ "DO move the toolbar BELOW a full-width draft box with `actions={false}` + a function `footer` — the function receives `{ components: { SubmitButton, CancelButton } }`, both already wired to onSubmit/onCancel/disabled/the loading swap, so nothing about the send contract is reimplemented at the call site. Put the attach control and a keyboard hint on the START side of the footer row and `<SubmitButton />` on the END side (a `Flex justify=\"between\"`), exactly like the record-comment box case. Leaving `actions` unset keeps today's inline row — this is opt-in, not a breaking change."
14789
14850
  ],
14790
14851
  "useCases": [
14791
14852
  "The message box of an AI assistant or support chat, under a ChatBubbleList feed.",
14792
14853
  "A comment composer on a record detail screen (prefix = attach Button, footer = character counter).",
14854
+ "A record-detail comment box with a full-width textarea and a toolbar row BELOW it — attach on the start side, a keyboard hint in the middle, clear/send on the end side: `actions={false}` + `footer={({ components }) => <Flex justify=\"between\">…<components.SubmitButton /></Flex>}`.",
14793
14855
  "A long-form reply box where Enter must break the line: submitType=\"shiftEnter\".",
14794
14856
  "A comment bar on an issue/record where Enter breaks the line and ⌘/Ctrl+Enter posts, and a status change may be posted without text: submitType=\"modEnter\" + allowEmptySubmit.",
14795
14857
  "A streaming answer the user can stop: loading + onCancel."
package/agent/index.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "counts": {
3
3
  "anti-ai-tells": 26,
4
- "components": 171,
4
+ "components": 172,
5
5
  "patterns": 20,
6
6
  "rules": 50,
7
7
  "tokens": 2071,
@@ -10,7 +10,7 @@
10
10
  "files": [
11
11
  {
12
12
  "file": "components-index.json",
13
- "note": "45 KB — name + group + tagline for all 171. FETCH THIS FIRST, then fetch only the components you chose.",
13
+ "note": "45 KB — name + group + tagline for all 172. 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
  {
package/agent/llms.txt CHANGED
@@ -1,6 +1,6 @@
1
1
  # @godxjp/ui
2
2
 
3
- > A Japanese-enterprise React design system: 171 components, 2071 design tokens,
3
+ > A Japanese-enterprise React design system: 172 components, 2071 design tokens,
4
4
  > 50 cardinal rules. This file is the entry point for AI agents. Catalog version 30.3.1.
5
5
 
6
6
  If your client can run a process, do not read these files — run the MCP server instead
@@ -15,7 +15,7 @@ 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): 20 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): 45 KB — all 171 components as name, group, tagline, plus `absorbed`: the names that do NOT exist and map to it (`Combobox` → `Select`).
18
+ - [components-index.json](https://raw.githubusercontent.com/godx-jp/godxjp-ui/main/agent/components-index.json): 45 KB — all 172 components as name, group, tagline, plus `absorbed`: the names that do NOT exist and map to it (`Combobox` → `Select`).
19
19
  - [components/&lt;Name&gt;.json](https://raw.githubusercontent.com/godx-jp/godxjp-ui/main/agent/components/Select.json): one file per component (1 KB–34 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
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, 1757 `component` knobs.
@@ -1,5 +1,5 @@
1
1
  import * as React from "react";
2
- export type { ChatComposerProp, ChatComposerProp as ChatComposerProps, ChatComposerSubmitTypeProp, } from "../../props/components/data-entry.prop.js";
2
+ export type { ChatComposerProp, ChatComposerProp as ChatComposerProps, ChatComposerSubmitTypeProp, ChatComposerActionComponents, ChatComposerFooterProp, } from "../../props/components/data-entry.prop.js";
3
3
  /**
4
4
  * ChatComposer — the message input of a conversation (Ant Design X `Sender`).
5
5
  *
@@ -37,8 +37,8 @@ export declare const ChatComposer: React.ForwardRefExoticComponent<Omit<React.HT
37
37
  readOnly?: boolean;
38
38
  header?: React.ReactNode;
39
39
  prefix?: React.ReactNode;
40
- footer?: React.ReactNode;
41
- actions?: React.ReactNode;
40
+ footer?: import("./chat-composer.js").ChatComposerFooterProp;
41
+ actions?: React.ReactNode | false;
42
42
  size?: import("../../props/index.js").SizeProp;
43
43
  maxLength?: number;
44
44
  status?: import("../../props/index.js").ControlStatusProp;
@@ -85,6 +85,50 @@ const ChatComposer = React.forwardRef(
85
85
  const sendName = submitLabel ?? t("dataEntry.chatComposer.send");
86
86
  const cancelName = cancelLabel ?? t("dataEntry.chatComposer.cancel");
87
87
  const actionSize = size === "xs" ? "icon-xs" : size === "sm" ? "icon-sm" : size === "lg" ? "icon-lg" : "icon";
88
+ const liveRef = React.useRef({ actionSize, sendName, cancelName, submit, onCancel, canSubmit, disabled });
89
+ liveRef.current = { actionSize, sendName, cancelName, submit, onCancel, canSubmit, disabled };
90
+ const SubmitButton = React.useMemo(
91
+ () => function SubmitButton2(buttonProps) {
92
+ const live = liveRef.current;
93
+ return /* @__PURE__ */ jsx(
94
+ Button,
95
+ {
96
+ type: "button",
97
+ size: live.actionSize,
98
+ "aria-label": live.sendName,
99
+ onClick: live.submit,
100
+ disabled: !live.canSubmit,
101
+ ...buttonProps,
102
+ children: buttonProps.children ?? /* @__PURE__ */ jsx(SendHorizontal, { "aria-hidden": "true" })
103
+ }
104
+ );
105
+ },
106
+ []
107
+ );
108
+ const CancelButton = React.useMemo(
109
+ () => function CancelButton2(buttonProps) {
110
+ const live = liveRef.current;
111
+ return /* @__PURE__ */ jsx(
112
+ Button,
113
+ {
114
+ type: "button",
115
+ size: live.actionSize,
116
+ variant: "secondary",
117
+ "aria-label": live.cancelName,
118
+ onClick: live.onCancel,
119
+ disabled: live.disabled,
120
+ ...buttonProps,
121
+ children: buttonProps.children ?? /* @__PURE__ */ jsx(Square, { "aria-hidden": "true" })
122
+ }
123
+ );
124
+ },
125
+ []
126
+ );
127
+ const actionComponents = React.useMemo(
128
+ () => ({ SubmitButton, CancelButton }),
129
+ [SubmitButton, CancelButton]
130
+ );
131
+ const footerContent = typeof footer === "function" ? footer({ components: actionComponents }) : footer;
88
132
  return /* @__PURE__ */ jsxs(
89
133
  "div",
90
134
  {
@@ -132,33 +176,12 @@ const ChatComposer = React.forwardRef(
132
176
  ...identity
133
177
  }
134
178
  ),
135
- /* @__PURE__ */ jsxs("div", { "data-slot": "chat-composer-actions", className: "ui-chat-composer-actions", children: [
179
+ actions === false ? null : /* @__PURE__ */ jsxs("div", { "data-slot": "chat-composer-actions", className: "ui-chat-composer-actions", children: [
136
180
  actions,
137
- loading ? /* @__PURE__ */ jsx(
138
- Button,
139
- {
140
- type: "button",
141
- size: actionSize,
142
- variant: "secondary",
143
- "aria-label": cancelName,
144
- onClick: onCancel,
145
- disabled,
146
- children: /* @__PURE__ */ jsx(Square, { "aria-hidden": "true" })
147
- }
148
- ) : /* @__PURE__ */ jsx(
149
- Button,
150
- {
151
- type: "button",
152
- size: actionSize,
153
- "aria-label": sendName,
154
- onClick: submit,
155
- disabled: !canSubmit,
156
- children: /* @__PURE__ */ jsx(SendHorizontal, { "aria-hidden": "true" })
157
- }
158
- )
181
+ loading ? /* @__PURE__ */ jsx(CancelButton, {}) : /* @__PURE__ */ jsx(SubmitButton, {})
159
182
  ] })
160
183
  ] }),
161
- footer ? /* @__PURE__ */ jsx("div", { "data-slot": "chat-composer-footer", className: "ui-chat-composer-footer", children: footer }) : null
184
+ footerContent ? /* @__PURE__ */ jsx("div", { "data-slot": "chat-composer-footer", className: "ui-chat-composer-footer", children: footerContent }) : null
162
185
  ]
163
186
  }
164
187
  );
@@ -2,6 +2,8 @@ export { PageContainer } from "./page-container.js";
2
2
  export type { PageContainerProp, PageContainerProps, BreadcrumbItem, BreadcrumbItemProp, } from "./page-container.js";
3
3
  export { Flex } from "./flex.js";
4
4
  export type { FlexAlignProp, FlexDirectionProp, FlexJustifyProp, FlexProp, FlexProps, } from "./flex.js";
5
+ export { SpaceCompact } from "./space-compact.js";
6
+ export type { SpaceCompactProp, SpaceCompactProps } from "./space-compact.js";
5
7
  export { ResizablePanel, ResizablePanelGroup, ResizableHandle } from "./resizable.js";
6
8
  export { NavList } from "./nav-list.js";
7
9
  export type { NavListProp, NavListProps } from "./nav-list.js";
@@ -1,5 +1,6 @@
1
1
  import { PageContainer } from "./page-container.js";
2
2
  import { Flex } from "./flex.js";
3
+ import { SpaceCompact } from "./space-compact.js";
3
4
  import { ResizablePanel, ResizablePanelGroup, ResizableHandle } from "./resizable.js";
4
5
  import { NavList } from "./nav-list.js";
5
6
  import { AppShell } from "./app-shell.js";
@@ -64,6 +65,7 @@ export {
64
65
  SidebarHeader,
65
66
  SidebarItem,
66
67
  SidebarSection,
68
+ SpaceCompact,
67
69
  SplitPane,
68
70
  Topbar,
69
71
  TopbarItem,
@@ -0,0 +1,43 @@
1
+ import type { JSX } from "react";
2
+ import type { SpaceCompactProp } from "../../props/components/layout.prop.js";
3
+ export type { SpaceCompactProp, SpaceCompactProp as SpaceCompactProps, } from "../../props/components/layout.prop.js";
4
+ /**
5
+ * SpaceCompact — antd `Space.Compact`: weld a row of controls into one visual unit.
6
+ *
7
+ * A plain `<div>`, matching antd's own DOM (no ARIA role by default — each child keeps its own
8
+ * accessible name; this is a visual join, not a semantic group, exactly like antd's
9
+ * `Space.Compact`). The corner radii and shared border seam are zeroed/collapsed in CSS — the
10
+ * "not expressible from primitives + tokens" case `docs/roadmap/parity-audit-layout-navigation-
11
+ * general.md` §4.1 describes: doing this at the call site would need `[&>*:not(:first-child)]`
12
+ * utilities, which `ui-audit`'s `no-utility-layout` rule blocks.
13
+ *
14
+ * ## ONE label for the pair (`FormField` wrapping a `SpaceCompact`)
15
+ *
16
+ * `FormField` lands its label contract on its single direct child (`docs/FORMS.md`). Cloned
17
+ * `aria-label`/`aria-labelledby` onto a role-less `<div>` would be inert (axe `aria-allowed-attr`
18
+ * disagrees, and no AT reads a name off a generic element) — so, like `Flex` (which `FormField`
19
+ * already lands on for a range/`年/月` pair), a NAMED `SpaceCompact` with no explicit `role`
20
+ * becomes `role="group"` and keeps only the aria a group allows: `aria-errormessage` folds
21
+ * into `aria-describedby`, and the widget-only `aria-required`/`aria-invalid` are dropped. Pass an
22
+ * explicit `role` to opt out and own the attribute set yourself.
23
+ *
24
+ * NOT quite "exactly like `Flex`" any more: since gh#916 `Flex` skips that promotion when its `as`
25
+ * renders an element that already has a role (`ul`, `ol`, `li`), because a name should improve
26
+ * such a role rather than replace it. This component always renders a `<div>`, which has no role
27
+ * to lose, so the promotion is unconditional here and the two agree on every case `Flex` still
28
+ * promotes.
29
+ *
30
+ * ## `orientation="vertical"` — a written-down partial port
31
+ *
32
+ * Layout (column stacking, border collapse) works on both axes. Corner-zeroing currently only
33
+ * covers the INLINE seam (`orientation="horizontal"`, the default and the only axis the shipping
34
+ * use case needs): it reads the same `--input-radius-start`/`--input-radius-end` and
35
+ * `--control-trigger-radius-start`/`--control-trigger-radius-end` knobs the Input and trigger
36
+ * families already expose, and both pairs are inline-only. A BLOCK-axis pair does not exist on
37
+ * either family yet, so a vertical stack collapses its shared border but each child keeps all
38
+ * four of its own corners rounded. Follow-up, not a silent gap.
39
+ */
40
+ export declare function SpaceCompact({ children, orientation, vertical, fullWidth, density, className, ...props }: SpaceCompactProp): JSX.Element;
41
+ export declare namespace SpaceCompact {
42
+ var displayName: string;
43
+ }
@@ -0,0 +1,44 @@
1
+ "use client";
2
+ import { jsx } from "react/jsx-runtime";
3
+ import { mergeAriaIds } from "../../lib/field-a11y.js";
4
+ import { cn } from "../../lib/utils.js";
5
+ function SpaceCompact({
6
+ children,
7
+ orientation,
8
+ vertical,
9
+ fullWidth,
10
+ density,
11
+ className,
12
+ ...props
13
+ }) {
14
+ const resolvedOrientation = orientation ?? (vertical ? "vertical" : "horizontal");
15
+ let domProps = props;
16
+ if (props.role === void 0 && (props["aria-label"] !== void 0 || props["aria-labelledby"] !== void 0)) {
17
+ const {
18
+ "aria-required": _ariaRequired,
19
+ "aria-invalid": _ariaInvalid,
20
+ "aria-errormessage": ariaErrorMessage,
21
+ ...allowed
22
+ } = props;
23
+ domProps = {
24
+ ...allowed,
25
+ role: "group",
26
+ "aria-describedby": mergeAriaIds(props["aria-describedby"], ariaErrorMessage)
27
+ };
28
+ }
29
+ return /* @__PURE__ */ jsx(
30
+ "div",
31
+ {
32
+ "data-slot": "space-compact",
33
+ "data-orientation": resolvedOrientation,
34
+ "data-full-width": fullWidth ? "true" : void 0,
35
+ className: cn("ui-space-compact", density && `ui-density-${density}`, className),
36
+ ...domProps,
37
+ children
38
+ }
39
+ );
40
+ }
41
+ SpaceCompact.displayName = "SpaceCompact";
42
+ export {
43
+ SpaceCompact
44
+ };
@@ -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": "30.3.0",
3
+ "version": "30.3.1",
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,
@@ -46,7 +46,7 @@ export declare const controlOpenRingClass = "ui-control-trigger";
46
46
  *
47
47
  * The clamp keeps it to one line, so a wrappable value cannot grow the control.
48
48
  */
49
- export declare const controlTriggerBaseClass = "ui-control ui-control-trigger flex items-center justify-between gap-2 whitespace-nowrap rounded-[var(--control-radius)] transition-[color,box-shadow] [&>[data-slot=select-value]]:line-clamp-1 [&>[data-slot=select-value]]:whitespace-normal [&>[data-slot=select-value]]:text-ellipsis";
49
+ export declare const controlTriggerBaseClass = "ui-control ui-control-trigger flex items-center justify-between gap-2 whitespace-nowrap rounded-s-[var(--control-trigger-radius-start,var(--control-radius))] rounded-e-[var(--control-trigger-radius-end,var(--control-radius))] transition-[color,box-shadow] [&>[data-slot=select-value]]:line-clamp-1 [&>[data-slot=select-value]]:whitespace-normal [&>[data-slot=select-value]]:text-ellipsis";
50
50
  /**
51
51
  * The trigger WITH the resting surface — `.ui-control-outlined-surface`, which paints the identical
52
52
  * border and fill from `--control-surface-border-color` / `--control-surface-background`.
@@ -55,9 +55,9 @@ export declare const controlTriggerBaseClass = "ui-control ui-control-trigger fl
55
55
  * outranks `@layer components`, so those two tokens could not reach it (gh#880). The painted result
56
56
  * is unchanged; what changed is that a theme can now reach it.
57
57
  */
58
- export declare const controlTriggerClass = "ui-control ui-control-trigger flex items-center justify-between gap-2 whitespace-nowrap rounded-[var(--control-radius)] transition-[color,box-shadow] [&>[data-slot=select-value]]:line-clamp-1 [&>[data-slot=select-value]]:whitespace-normal [&>[data-slot=select-value]]:text-ellipsis ui-control-outlined-surface";
58
+ export declare const controlTriggerClass = "ui-control ui-control-trigger flex items-center justify-between gap-2 whitespace-nowrap rounded-s-[var(--control-trigger-radius-start,var(--control-radius))] rounded-e-[var(--control-trigger-radius-end,var(--control-radius))] transition-[color,box-shadow] [&>[data-slot=select-value]]:line-clamp-1 [&>[data-slot=select-value]]:whitespace-normal [&>[data-slot=select-value]]:text-ellipsis ui-control-outlined-surface";
59
59
  /** `controlTriggerBaseClass` + the token-driven surface — the select-family trigger. */
60
- export declare const controlSurfaceTriggerClass = "ui-control ui-control-trigger flex items-center justify-between gap-2 whitespace-nowrap rounded-[var(--control-radius)] transition-[color,box-shadow] [&>[data-slot=select-value]]:line-clamp-1 [&>[data-slot=select-value]]:whitespace-normal [&>[data-slot=select-value]]:text-ellipsis ui-control-surface";
60
+ export declare const controlSurfaceTriggerClass = "ui-control ui-control-trigger flex items-center justify-between gap-2 whitespace-nowrap rounded-s-[var(--control-trigger-radius-start,var(--control-radius))] rounded-e-[var(--control-trigger-radius-end,var(--control-radius))] transition-[color,box-shadow] [&>[data-slot=select-value]]:line-clamp-1 [&>[data-slot=select-value]]:whitespace-normal [&>[data-slot=select-value]]:text-ellipsis ui-control-surface";
61
61
  export declare const controlIconClass = "size-[length:var(--control-height)] shrink-0";
62
62
  export declare const controlIconSmClass = "size-[calc(var(--control-height)-0.5rem)] shrink-0";
63
63
  /** Leading/affix icon inside an input row (search, command) — sized to `--control-icon-size`. */
@@ -2,7 +2,14 @@ const controlMultilineClass = "ui-control-multiline ui-control-outlined-surface
2
2
  const controlMultilineGhostClass = "ui-control-multiline data-[status=error]:border-destructive data-[status=warning]:border-warning w-full min-h-0 border-0 bg-transparent shadow-none placeholder:text-muted-foreground focus-visible:ring-0";
3
3
  const controlMultilineFilledClass = "ui-control-multiline ui-control--filled aria-invalid:border-destructive data-[status=error]:border-destructive data-[status=warning]:border-warning w-full rounded-[var(--control-radius)] ring-offset-background placeholder:text-muted-foreground";
4
4
  const controlOpenRingClass = "ui-control-trigger";
5
- const controlTriggerBaseClass = "ui-control ui-control-trigger flex items-center justify-between gap-2 whitespace-nowrap rounded-[var(--control-radius)] transition-[color,box-shadow] [&>[data-slot=select-value]]:line-clamp-1 [&>[data-slot=select-value]]:whitespace-normal [&>[data-slot=select-value]]:text-ellipsis";
5
+ const controlTriggerBaseClass = (
6
+ // PER-EDGE RADIUS THROUGH A KNOB, same repair as Input's `--input-radius-start`/`-end`
7
+ // (gh#841): a plain `rounded-[var(--control-radius)]` is a utility, and `@layer utilities`
8
+ // outranks `@layer components`, so a joined-control seam (SpaceCompact) can never zero one
9
+ // side of it from a components-layer rule. Unset, both knobs fall back to `--control-radius`
10
+ // exactly as before — no existing trigger changes.
11
+ "ui-control ui-control-trigger flex items-center justify-between gap-2 whitespace-nowrap rounded-s-[var(--control-trigger-radius-start,var(--control-radius))] rounded-e-[var(--control-trigger-radius-end,var(--control-radius))] transition-[color,box-shadow] [&>[data-slot=select-value]]:line-clamp-1 [&>[data-slot=select-value]]:whitespace-normal [&>[data-slot=select-value]]:text-ellipsis"
12
+ );
6
13
  const controlTriggerClass = `${controlTriggerBaseClass} ui-control-outlined-surface`;
7
14
  const controlSurfaceTriggerClass = `${controlTriggerBaseClass} ui-control-surface`;
8
15
  const controlIconClass = "size-[length:var(--control-height)] shrink-0";
@@ -5,6 +5,7 @@ import type { DateRange } from "react-day-picker";
5
5
  import type * as React from "react";
6
6
  import type { UploadFileItem } from "../../components/data-entry/upload-types.js";
7
7
  import type { FieldA11yProps } from "../../lib/field-a11y.js";
8
+ import type { ButtonProp } from "./general.prop.js";
8
9
  import type { ClassNameProp, ControlWidthProp, DisabledProp, EmptyMessageProp, ErrorBagProp, ErrorProp, HelperProp, IdProp, LabelProp, NameProp, OnValueChangeProp, OnSearchChangeProp, OpenProp, OnOpenChangeProp, PlaceholderProp, RequiredProp, ValueProp, DefaultValueProp, FormLayoutProp, WidthProp, BreakpointProp, DensityProp, SizeProp, TitleProp, DefaultOpenProp, ControlStatusProp, ControlVariantProp, AllowClearProp, MaxTagCountProp, MaxTagPlaceholderProp, NotFoundContentProp, PopupMatchWidthProp, PendingProp, PadProp, PadRawProp } from "../vocabulary/index.js";
9
10
  import type { ResponsiveGridColumnsProp } from "./layout.prop.js";
10
11
  /** One-outline-per-group appearance for the compound InputOTP control. */
@@ -1923,6 +1924,33 @@ export type BranchScopePickerProp = FieldA11yProps & {
1923
1924
  * @see ChatComposer
1924
1925
  */
1925
1926
  export type ChatComposerSubmitTypeProp = "enter" | "shiftEnter" | "modEnter";
1927
+ /**
1928
+ * @see ChatComposer `footer` — the built-in trailing action, pre-wired (onClick/disabled/
1929
+ * accessible name already resolved) and handed to a `footer` render function so it can be
1930
+ * RELOCATED instead of duplicated.
1931
+ *
1932
+ * Ant Design X `Sender.ActionsComponents` ships four (`SendButton`/`ClearButton`/`LoadingButton`/
1933
+ * `SpeechButton`) because `Sender` has a clear affordance and speech input this composer does not
1934
+ * — porting those two as inert components would ship dead API (the same "not ported" discipline
1935
+ * `docs/DESIGN-AUTHORITY.md` applies to antd fields nothing here reads). `LoadingButton` is
1936
+ * `CancelButton` here — this composer already names the streaming state's trailing action `cancel`
1937
+ * (`onCancel`, `cancelLabel`), so the antd-x name would be a second spelling of the same button.
1938
+ */
1939
+ export type ChatComposerActionComponents = {
1940
+ /** The send action — disabled while empty/disabled/readOnly/loading, exactly like the inline one. */
1941
+ SubmitButton: React.ComponentType<ButtonProp>;
1942
+ /** The stop action — only meaningful while `loading`; renders antd X's `LoadingButton` slot. */
1943
+ CancelButton: React.ComponentType<ButtonProp>;
1944
+ };
1945
+ /**
1946
+ * @see ChatComposer `footer` — either a plain node (unchanged default behaviour) or Ant Design X
1947
+ * `Sender`'s `NodeRender` shape: a function receiving `{ components }` so a consumer can render
1948
+ * the real send/cancel buttons BELOW the draft box instead of beside it. Combine with `actions=
1949
+ * {false}` to hide the inline pair and avoid rendering both.
1950
+ */
1951
+ export type ChatComposerFooterProp = React.ReactNode | ((info: {
1952
+ components: ChatComposerActionComponents;
1953
+ }) => React.ReactNode);
1926
1954
  /**
1927
1955
  * @see ChatComposer — the message input of a conversation (Ant Design X `Sender`; the industry
1928
1956
  * calls the control a *composer*, so that is what it is named).
@@ -1972,10 +2000,20 @@ export type ChatComposerProp = Omit<React.HTMLAttributes<HTMLDivElement>, "onSub
1972
2000
  header?: React.ReactNode;
1973
2001
  /** Slot at the inline START of the draft row — an attach button, an avatar. */
1974
2002
  prefix?: React.ReactNode;
1975
- /** Slot BELOW the draft row — a hint line, a token counter. */
1976
- footer?: React.ReactNode;
1977
- /** Extra trailing actions, rendered BEFORE the send/cancel action. */
1978
- actions?: React.ReactNode;
2003
+ /**
2004
+ * Slot BELOW the draft row — a hint line, a token counter, or (Ant Design X `Sender`
2005
+ * `NodeRender`) a function `({ components }) => ReactNode` that renders the real send/cancel
2006
+ * buttons in the footer instead of beside the draft box. Pair with `actions={false}` to hide
2007
+ * the inline pair and avoid rendering both — see the `usage` note on relocating actions.
2008
+ */
2009
+ footer?: ChatComposerFooterProp;
2010
+ /**
2011
+ * Extra trailing actions, rendered BEFORE the send/cancel action — or `false` (Ant Design X
2012
+ * `Sender`'s `suffix` semantics: `BaseNode = ReactNode | false`) to hide the ENTIRE inline
2013
+ * trailing-action cell, built-in send/cancel button included, when `footer` has taken over
2014
+ * rendering it. Omitted (`undefined`) renders the cell exactly as before — no breaking change.
2015
+ */
2016
+ actions?: React.ReactNode | false;
1979
2017
  /** Control height tier on the shared `--control-height` ladder. Default `md`. */
1980
2018
  size?: SizeProp;
1981
2019
  /** Hard ceiling on the draft length, forwarded to the textarea. */