@godxjp/ui 18.15.4 → 19.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (154) hide show
  1. package/dist/app/app-provider.d.ts +1 -1
  2. package/dist/app/app-provider.js +3 -0
  3. package/dist/app/date-format-labels.d.ts +1 -1
  4. package/dist/app/time-format-labels.d.ts +1 -1
  5. package/dist/components/data-display/badge.d.ts +1 -1
  6. package/dist/components/data-display/card.d.ts +4 -13
  7. package/dist/components/data-display/card.js +2 -15
  8. package/dist/components/data-display/data-table.d.ts +3 -8
  9. package/dist/components/data-display/data-table.js +106 -68
  10. package/dist/components/data-display/descriptions.js +34 -5
  11. package/dist/components/data-display/empty-state.js +1 -1
  12. package/dist/components/data-display/permission-matrix.js +7 -7
  13. package/dist/components/data-display/popover.js +2 -9
  14. package/dist/components/data-display/qr-code.d.ts +1 -1
  15. package/dist/components/data-display/scroll-area.d.ts +3 -1
  16. package/dist/components/data-display/scroll-area.js +150 -16
  17. package/dist/components/data-display/table.js +12 -3
  18. package/dist/components/data-entry/branch-scope-picker.js +4 -4
  19. package/dist/components/data-entry/calendar.js +15 -24
  20. package/dist/components/data-entry/cascader.js +70 -72
  21. package/dist/components/data-entry/checkbox-group.js +12 -5
  22. package/dist/components/data-entry/checkbox.js +30 -13
  23. package/dist/components/data-entry/command.d.ts +6 -6
  24. package/dist/components/data-entry/date-picker.js +34 -17
  25. package/dist/components/data-entry/date-range-picker.js +19 -13
  26. package/dist/components/data-entry/form-field.d.ts +1 -1
  27. package/dist/components/data-entry/form-field.js +22 -5
  28. package/dist/components/data-entry/index.d.ts +2 -1
  29. package/dist/components/data-entry/input.js +18 -22
  30. package/dist/components/data-entry/label.js +4 -2
  31. package/dist/components/data-entry/month-picker.js +20 -17
  32. package/dist/components/data-entry/month-range-picker.js +26 -19
  33. package/dist/components/data-entry/radio.js +25 -6
  34. package/dist/components/data-entry/search-select.d.ts +1 -1
  35. package/dist/components/data-entry/search-select.js +37 -31
  36. package/dist/components/data-entry/select.d.ts +8 -0
  37. package/dist/components/data-entry/select.js +68 -26
  38. package/dist/components/data-entry/switch.js +16 -2
  39. package/dist/components/data-entry/textarea.d.ts +56 -0
  40. package/dist/components/data-entry/textarea.js +110 -40
  41. package/dist/components/data-entry/time-picker.js +52 -52
  42. package/dist/components/data-entry/toggle-group.d.ts +1 -0
  43. package/dist/components/data-entry/toggle.d.ts +1 -1
  44. package/dist/components/data-entry/transfer.js +14 -15
  45. package/dist/components/data-entry/tree-select.js +19 -23
  46. package/dist/components/data-entry/upload-crop-dialog.js +4 -4
  47. package/dist/components/data-entry/upload.d.ts +1 -1
  48. package/dist/components/data-entry/upload.js +46 -46
  49. package/dist/components/feedback/alert.js +7 -14
  50. package/dist/components/feedback/dialog.d.ts +23 -32
  51. package/dist/components/feedback/dialog.js +16 -13
  52. package/dist/components/feedback/sheet.d.ts +1 -1
  53. package/dist/components/feedback/sheet.js +14 -14
  54. package/dist/components/feedback/skeleton.js +13 -10
  55. package/dist/components/feedback/sonner.d.ts +1 -1
  56. package/dist/components/feedback/sonner.js +17 -8
  57. package/dist/components/feedback/tooltip.js +1 -1
  58. package/dist/components/general/activity.d.ts +44 -0
  59. package/dist/components/general/activity.js +65 -0
  60. package/dist/components/general/button.d.ts +2 -2
  61. package/dist/components/general/button.js +11 -24
  62. package/dist/components/general/index.d.ts +2 -0
  63. package/dist/components/general/index.js +2 -0
  64. package/dist/components/layout/app-shell.js +5 -3
  65. package/dist/components/layout/auth-divider.d.ts +9 -2
  66. package/dist/components/layout/auth-divider.js +7 -11
  67. package/dist/components/layout/index.d.ts +1 -0
  68. package/dist/components/layout/page-container.d.ts +1 -1
  69. package/dist/components/layout/page-container.js +32 -20
  70. package/dist/components/layout/resizable.d.ts +12 -12
  71. package/dist/components/layout/separator.d.ts +1 -0
  72. package/dist/components/layout/service-role-panel.js +47 -42
  73. package/dist/components/layout/sidebar.js +13 -1
  74. package/dist/components/layout/split-pane.d.ts +12 -1
  75. package/dist/components/layout/split-pane.js +13 -4
  76. package/dist/components/navigation/app-setting-picker.js +5 -14
  77. package/dist/components/navigation/dropdown-menu.js +10 -16
  78. package/dist/components/navigation/menubar.d.ts +3 -6
  79. package/dist/components/navigation/pagination-utils.d.ts +15 -2
  80. package/dist/components/navigation/pagination-utils.js +27 -21
  81. package/dist/components/navigation/pagination.d.ts +4 -4
  82. package/dist/components/navigation/pagination.js +2 -2
  83. package/dist/components/navigation/steps.js +31 -67
  84. package/dist/components/navigation/tabs.js +17 -5
  85. package/dist/components/query/infinite-query-state.js +2 -2
  86. package/dist/components/ui/accordion.d.ts +1 -1
  87. package/dist/components/ui/avatar.d.ts +1 -1
  88. package/dist/components/ui/avatar.js +29 -10
  89. package/dist/components/ui/input-otp.d.ts +8 -6
  90. package/dist/components/ui/rating.js +1 -1
  91. package/dist/components/ui/separator.d.ts +27 -2
  92. package/dist/components/ui/separator.js +42 -11
  93. package/dist/components/ui/toggle-group.d.ts +15 -5
  94. package/dist/components/ui/toggle-group.js +43 -18
  95. package/dist/components/ui/toggle.d.ts +66 -5
  96. package/dist/components/ui/toggle.js +64 -9
  97. package/dist/i18n/messages/en.json +8 -0
  98. package/dist/i18n/messages/ja.json +8 -0
  99. package/dist/i18n/messages/vi.json +8 -0
  100. package/dist/lib/control-styles.d.ts +14 -0
  101. package/dist/lib/control-styles.js +2 -0
  102. package/dist/lib/field-a11y.d.ts +52 -0
  103. package/dist/lib/field-a11y.js +17 -2
  104. package/dist/props/components/app.prop.d.ts +15 -0
  105. package/dist/props/components/data-display.prop.d.ts +85 -3
  106. package/dist/props/components/data-entry.prop.d.ts +35 -2
  107. package/dist/props/components/general.prop.d.ts +52 -1
  108. package/dist/props/components/layout.prop.d.ts +137 -2
  109. package/dist/props/registry.d.ts +116 -5
  110. package/dist/props/registry.js +165 -4
  111. package/dist/props/vocabulary/index.d.ts +2 -2
  112. package/dist/props/vocabulary/interaction.prop.d.ts +15 -0
  113. package/dist/props/vocabulary/layout.prop.d.ts +8 -0
  114. package/dist/styles/alert-layout.css +69 -3
  115. package/dist/styles/badge-layout.css +2 -2
  116. package/dist/styles/base.css +6 -0
  117. package/dist/styles/card-layout.css +25 -8
  118. package/dist/styles/control.css +1150 -29
  119. package/dist/styles/data-display-layout.css +185 -11
  120. package/dist/styles/data-entry-layout.css +352 -1
  121. package/dist/styles/density.css +2 -2
  122. package/dist/styles/dialog-layout.css +203 -4
  123. package/dist/styles/focus-ring.css +155 -0
  124. package/dist/styles/form-layout.css +50 -5
  125. package/dist/styles/index.css +1 -0
  126. package/dist/styles/layout.css +412 -23
  127. package/dist/styles/motion.css +178 -0
  128. package/dist/styles/navigation-layout.css +398 -22
  129. package/dist/styles/shell-layout.css +137 -39
  130. package/dist/styles/table-layout.css +153 -22
  131. package/dist/styles/toggle.css +59 -0
  132. package/dist/tokens/base.css +5 -0
  133. package/dist/tokens/components/activity.css +64 -0
  134. package/dist/tokens/components/badge.css +7 -0
  135. package/dist/tokens/components/card.css +14 -10
  136. package/dist/tokens/components/chart.css +1 -1
  137. package/dist/tokens/components/control.css +358 -8
  138. package/dist/tokens/components/data-display.css +75 -0
  139. package/dist/tokens/components/data-entry.css +25 -0
  140. package/dist/tokens/components/descriptions.css +22 -0
  141. package/dist/tokens/components/feedback.css +143 -4
  142. package/dist/tokens/components/form.css +16 -0
  143. package/dist/tokens/components/legal-document.css +5 -1
  144. package/dist/tokens/components/navigation.css +109 -0
  145. package/dist/tokens/components/scroll-area.css +21 -0
  146. package/dist/tokens/components/separator.css +52 -0
  147. package/dist/tokens/components/shell.css +132 -20
  148. package/dist/tokens/components/table.css +52 -3
  149. package/dist/tokens/components/toggle.css +82 -0
  150. package/dist/tokens/components/upload.css +107 -0
  151. package/dist/tokens/foundation.css +171 -10
  152. package/dist/tokens/semantic/layout.css +120 -1
  153. package/package.json +85 -68
  154. package/scripts/ui-audit.mjs +51 -2
@@ -7,6 +7,7 @@ import { cn } from "../../lib/utils.js";
7
7
  import { controlTriggerClass } from "../../lib/control-styles.js";
8
8
  import {
9
9
  mergeAriaIds,
10
+ useFieldIdentity,
10
11
  omitFieldA11y,
11
12
  pickFieldA11y,
12
13
  useFieldNameFallback
@@ -26,7 +27,36 @@ function Select(props) {
26
27
  function CompoundSelect({ id, ...props }) {
27
28
  const fieldA11y = pickFieldA11y(props);
28
29
  const rootProps = omitFieldA11y(props);
29
- return /* @__PURE__ */ jsx(SelectFieldA11yContext.Provider, { value: { ...fieldA11y, id }, children: /* @__PURE__ */ jsx(SelectPrimitive.Root, { "data-slot": "select", ...rootProps }) });
30
+ const [uncontrolled, setUncontrolled] = React.useState(props.defaultValue);
31
+ const value = props.value ?? uncontrolled;
32
+ const identity = useFieldIdentity({
33
+ id,
34
+ name: props.name,
35
+ "data-field": fieldA11y["data-field"]
36
+ });
37
+ return /* @__PURE__ */ jsx(
38
+ SelectFieldA11yContext.Provider,
39
+ {
40
+ value: {
41
+ ...fieldA11y,
42
+ "data-field": identity["data-field"] ?? fieldA11y["data-field"],
43
+ id,
44
+ value
45
+ },
46
+ children: /* @__PURE__ */ jsx(
47
+ SelectPrimitive.Root,
48
+ {
49
+ "data-slot": "select",
50
+ ...rootProps,
51
+ name: props.name ?? identity.name,
52
+ onValueChange: (next) => {
53
+ setUncontrolled(next);
54
+ props.onValueChange?.(next);
55
+ }
56
+ }
57
+ )
58
+ }
59
+ );
30
60
  }
31
61
  function SelectGroup(props) {
32
62
  return /* @__PURE__ */ jsx(SelectPrimitive.Group, { "data-slot": "select-group", ...props });
@@ -54,8 +84,10 @@ const SelectTrigger = React.forwardRef(({ className, children, size = "md", show
54
84
  field?.["aria-errormessage"]
55
85
  ),
56
86
  "aria-required": props["aria-required"] ?? field?.["aria-required"],
57
- "aria-invalid": props["aria-invalid"] ?? field?.["aria-invalid"]
87
+ "aria-invalid": props["aria-invalid"] ?? field?.["aria-invalid"],
88
+ "data-field": props["data-field"] ?? field?.["data-field"]
58
89
  } : void 0;
90
+ const dataValue = (props["data-value"] ?? field?.value) || void 0;
59
91
  return /* @__PURE__ */ jsxs(
60
92
  SelectPrimitive.Trigger,
61
93
  {
@@ -64,18 +96,19 @@ const SelectTrigger = React.forwardRef(({ className, children, size = "md", show
64
96
  "data-size": size,
65
97
  className: cn(
66
98
  controlTriggerClass,
67
- "focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:border-destructive aria-invalid:ring-destructive/20 data-[placeholder]:text-muted-foreground [&_svg:not([class*='text-'])]:text-muted-foreground w-full gap-2 whitespace-nowrap shadow-xs transition-[color,box-shadow] outline-none focus-visible:ring-[3px] *:data-[slot=select-value]:line-clamp-1 *:data-[slot=select-value]:flex *:data-[slot=select-value]:items-center *:data-[slot=select-value]:gap-2",
99
+ "aria-invalid:border-destructive aria-invalid:ring-destructive/20 data-[placeholder]:text-muted-foreground [&_svg:not([class*='text-'])]:text-muted-foreground w-full whitespace-nowrap transition-[color,box-shadow] outline-none *:data-[slot=select-value]:line-clamp-1 *:data-[slot=select-value]:flex *:data-[slot=select-value]:items-center",
68
100
  className
69
101
  ),
70
102
  ...props,
71
103
  ...fieldA11y,
104
+ "data-value": dataValue,
72
105
  children: [
73
106
  children,
74
107
  showIndicator ? /* @__PURE__ */ jsx(SelectPrimitive.Icon, { asChild: true, children: /* @__PURE__ */ jsx(
75
108
  ChevronDown,
76
109
  {
77
110
  "data-slot": "select-chevron",
78
- className: "size-4 shrink-0 opacity-50",
111
+ className: "ui-select-chevron",
79
112
  "aria-hidden": "true"
80
113
  }
81
114
  ) }) : null
@@ -89,9 +122,9 @@ const SelectScrollUpButton = React.forwardRef(({ className, ...props }, ref) =>
89
122
  {
90
123
  ref,
91
124
  "data-slot": "select-scroll-up-button",
92
- className: cn("flex cursor-default items-center justify-center py-1", className),
125
+ className: cn("ui-select-scroll-button", className),
93
126
  ...props,
94
- children: /* @__PURE__ */ jsx(ChevronUp, { className: "size-4", "aria-hidden": "true" })
127
+ children: /* @__PURE__ */ jsx(ChevronUp, { className: "ui-select-scroll-icon", "aria-hidden": "true" })
95
128
  }
96
129
  ));
97
130
  SelectScrollUpButton.displayName = SelectPrimitive.ScrollUpButton.displayName;
@@ -100,9 +133,9 @@ const SelectScrollDownButton = React.forwardRef(({ className, ...props }, ref) =
100
133
  {
101
134
  ref,
102
135
  "data-slot": "select-scroll-down-button",
103
- className: cn("flex cursor-default items-center justify-center py-1", className),
136
+ className: cn("ui-select-scroll-button", className),
104
137
  ...props,
105
- children: /* @__PURE__ */ jsx(ChevronDown, { className: "size-4", "aria-hidden": "true" })
138
+ children: /* @__PURE__ */ jsx(ChevronDown, { className: "ui-select-scroll-icon", "aria-hidden": "true" })
106
139
  }
107
140
  ));
108
141
  SelectScrollDownButton.displayName = SelectPrimitive.ScrollDownButton.displayName;
@@ -112,7 +145,7 @@ const SelectContent = React.forwardRef(({ className, children, position = "poppe
112
145
  ref,
113
146
  "data-slot": "select-content",
114
147
  className: cn(
115
- "bg-popover text-popover-foreground data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 relative z-50 max-h-96 min-w-32 overflow-hidden rounded-md border shadow-md",
148
+ "ui-select-content data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
116
149
  position === "popper" && "translate-y-1",
117
150
  className
118
151
  ),
@@ -125,7 +158,7 @@ const SelectContent = React.forwardRef(({ className, children, position = "poppe
125
158
  {
126
159
  "data-slot": "select-viewport",
127
160
  className: cn(
128
- "p-1",
161
+ "ui-select-viewport",
129
162
  position === "popper" && "h-[var(--radix-select-trigger-height)] w-full min-w-[var(--radix-select-trigger-width)]"
130
163
  ),
131
164
  children
@@ -141,7 +174,7 @@ const SelectLabel = React.forwardRef(({ className, ...props }, ref) => /* @__PUR
141
174
  {
142
175
  ref,
143
176
  "data-slot": "select-label",
144
- className: cn("px-2 py-1.5 text-sm font-medium", className),
177
+ className: cn("ui-select-label", className),
145
178
  ...props
146
179
  }
147
180
  ));
@@ -151,10 +184,7 @@ const SelectItem = React.forwardRef(({ className, children, ...props }, ref) =>
151
184
  {
152
185
  ref,
153
186
  "data-slot": "select-item",
154
- className: cn(
155
- "focus:bg-accent focus:text-accent-foreground data-[state=checked]:bg-accent data-[state=checked]:text-foreground [&_svg:not([class*='text-'])]:text-muted-foreground relative flex w-full cursor-default items-center gap-2 rounded-sm px-2.5 py-1.5 text-sm outline-none select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 data-[state=checked]:font-medium [&_svg]:pointer-events-none [&_svg]:shrink-0",
156
- className
157
- ),
187
+ className: cn("ui-select-item [&_svg:not([class*='text-'])]:text-muted-foreground", className),
158
188
  ...props,
159
189
  children: /* @__PURE__ */ jsx(SelectPrimitive.ItemText, { children })
160
190
  }
@@ -165,7 +195,7 @@ const SelectSeparator = React.forwardRef(({ className, ...props }, ref) => /* @_
165
195
  {
166
196
  ref,
167
197
  "data-slot": "select-separator",
168
- className: cn("bg-border -mx-1 my-1 h-px", className),
198
+ className: cn("ui-select-separator", className),
169
199
  ...props
170
200
  }
171
201
  ));
@@ -215,12 +245,18 @@ function DataSelect({
215
245
  id,
216
246
  className,
217
247
  "data-testid": dataTestId,
248
+ "data-field": dataField,
218
249
  ...rest
219
250
  }) {
220
251
  const ariaProps = Object.fromEntries(
221
252
  Object.entries(rest).filter(([key]) => key.startsWith("aria-"))
222
253
  );
223
254
  const { t } = useTranslation();
255
+ const [uncontrolledValue, setUncontrolledValue] = React.useState(defaultValue);
256
+ const currentValue = value ?? uncontrolledValue;
257
+ const identity = useFieldIdentity({ id, name, "data-field": dataField });
258
+ const resolvedName = name ?? identity.name;
259
+ const resolvedField = dataField ?? identity["data-field"];
224
260
  const searchable = showSearch ?? Boolean(loadOptions);
225
261
  const hasOptions = options.length > 0;
226
262
  if (searchable) {
@@ -253,10 +289,11 @@ function DataSelect({
253
289
  filterOption,
254
290
  renderError,
255
291
  renderLoadMore,
256
- name,
292
+ name: resolvedName,
257
293
  id,
258
294
  className,
259
295
  "data-testid": dataTestId,
296
+ "data-field": resolvedField,
260
297
  ...ariaProps
261
298
  }
262
299
  );
@@ -281,19 +318,24 @@ function DataSelect({
281
318
  "data-slot": "select",
282
319
  value: isControlled ? value : void 0,
283
320
  defaultValue: isControlled ? void 0 : defaultValue || void 0,
284
- onValueChange: (next) => onValueChange?.(
285
- next,
286
- options.find((option) => option.value === next)
287
- ),
321
+ onValueChange: (next) => {
322
+ setUncontrolledValue(next);
323
+ onValueChange?.(
324
+ next,
325
+ options.find((option) => option.value === next)
326
+ );
327
+ },
288
328
  disabled: disabled || !hasOptions,
289
- name,
329
+ name: resolvedName,
290
330
  children: [
291
331
  /* @__PURE__ */ jsx(
292
332
  SelectTrigger,
293
333
  {
294
334
  id,
295
335
  "data-testid": dataTestId,
296
- className: cn(showClear && "pe-9", canClear ? void 0 : className),
336
+ "data-field": resolvedField,
337
+ "data-value": currentValue || void 0,
338
+ className: cn(showClear && "ui-control-trigger-affixed", canClear ? void 0 : className),
297
339
  showIndicator: !showClear,
298
340
  ...ariaProps,
299
341
  children: /* @__PURE__ */ jsx(SelectValue, { placeholder })
@@ -311,15 +353,15 @@ function DataSelect({
311
353
  if (!canClear) return select;
312
354
  return /* @__PURE__ */ jsxs("div", { className: cn("relative", className), children: [
313
355
  select,
314
- showClear ? /* @__PURE__ */ jsx("div", { className: "absolute inset-y-0 end-2 flex items-center", children: /* @__PURE__ */ jsx(
356
+ showClear ? /* @__PURE__ */ jsx("div", { className: "ui-control-affix", children: /* @__PURE__ */ jsx(
315
357
  "button",
316
358
  {
317
359
  type: "button",
318
360
  "aria-label": clearLabel ?? t("dataEntry.searchSelect.clear"),
319
361
  "data-testid": dataTestId ? `${dataTestId}-clear` : void 0,
320
- className: "flex size-6 items-center justify-center rounded-sm opacity-50 hover:opacity-100 focus-visible:opacity-100",
362
+ className: "ui-control-affix-action",
321
363
  onClick: () => onValueChange?.("", void 0),
322
- children: /* @__PURE__ */ jsx(X, { className: "size-4", "aria-hidden": "true" })
364
+ children: /* @__PURE__ */ jsx(X, { className: "ui-control-affix-icon", "aria-hidden": "true" })
323
365
  }
324
366
  ) }) : null
325
367
  ] });
@@ -3,8 +3,15 @@ import { Fragment, jsx, jsxs } from "react/jsx-runtime";
3
3
  import * as React from "react";
4
4
  import * as SwitchPrimitive from "@radix-ui/react-switch";
5
5
  import { cn } from "../../lib/utils.js";
6
+ import { useFieldIdentity } from "../../lib/field-a11y.js";
6
7
  const Switch = React.forwardRef(
7
8
  ({ className, size = "md", name, checked, defaultChecked = false, onCheckedChange, ...props }, ref) => {
9
+ const identity = useFieldIdentity({
10
+ id: props.id,
11
+ name,
12
+ "data-field": props["data-field"]
13
+ });
14
+ const resolvedName = name ?? identity.name;
8
15
  const [internalChecked, setInternalChecked] = React.useState(defaultChecked);
9
16
  const isControlled = checked !== void 0;
10
17
  const isChecked = isControlled ? checked : internalChecked;
@@ -15,7 +22,7 @@ const Switch = React.forwardRef(
15
22
  onCheckedChange?.(next);
16
23
  };
17
24
  return /* @__PURE__ */ jsxs(Fragment, { children: [
18
- name ? /* @__PURE__ */ jsx("input", { type: "hidden", name, value: isChecked ? "1" : "0", readOnly: true }) : null,
25
+ resolvedName ? /* @__PURE__ */ jsx("input", { type: "hidden", name: resolvedName, value: isChecked ? "1" : "0", readOnly: true }) : null,
19
26
  /* @__PURE__ */ jsx(
20
27
  SwitchPrimitive.Root,
21
28
  {
@@ -25,10 +32,17 @@ const Switch = React.forwardRef(
25
32
  checked: isChecked,
26
33
  onCheckedChange: handleCheckedChange,
27
34
  className: cn(
28
- "peer ui-switch focus-visible:border-ring focus-visible:ring-ring/50 shadow-xs transition-all outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50",
35
+ // `disabled:cursor-not-allowed disabled:opacity-50` DELETED, not moved (#319):
36
+ // `.ui-switch:disabled, .ui-switch[data-disabled]` in styles/control.css already
37
+ // declares both and reads --disabled-opacity. The utility was layered after
38
+ // components, so it silently outranked that token. Byte-identical: the token
39
+ // defaults to 0.5. (`shadow-xs` STAYS — .ui-switch declares --shadow-sm, so this
40
+ // utility is the switch's real resting elevation, not a duplicate.)
41
+ "peer ui-switch shadow-xs transition-all outline-none",
29
42
  className
30
43
  ),
31
44
  ...props,
45
+ "data-field": identity["data-field"] ?? props["data-field"],
32
46
  children: /* @__PURE__ */ jsx(SwitchPrimitive.Thumb, { "data-slot": "switch-thumb", className: "ui-switch-thumb" })
33
47
  }
34
48
  )
@@ -4,10 +4,66 @@ export type TextareaProps = React.TextareaHTMLAttributes<HTMLTextAreaElement> &
4
4
  allowClear?: boolean;
5
5
  /** Called after the field is cleared via the inline ✕. */
6
6
  onClear?: () => void;
7
+ /**
8
+ * `ghost` strips the field's own border, background and focus ring, for a textarea embedded
9
+ * in a surface that already draws the box (a chat composer inside a Card). The surface then
10
+ * owns focus — give it `focus-within`. Default keeps the standalone control chrome.
11
+ */
12
+ variant?: "default" | "ghost";
13
+ /**
14
+ * Grow the control with its content instead of holding a fixed `rows` height (default false —
15
+ * the inert default, so every existing `<Textarea>` keeps the geometry it has today).
16
+ *
17
+ * The floor is `minRows` (or `rows`, when that is the only one given) and never undercuts the
18
+ * `--control-height` tier; past `maxRows` the control stops growing and scrolls internally.
19
+ * Sizing is done in CSS by a hidden replica of the text, so it follows a paste, a programmatic
20
+ * value change, a font swap and a container resize — not just typing — and it never writes
21
+ * `style.height` or reads `scrollHeight`. Works controlled and uncontrolled.
22
+ */
23
+ autoGrow?: boolean;
24
+ /**
25
+ * Floor in text rows while `autoGrow` (theme default `--textarea-autogrow-min-height-rows`, 1).
26
+ * Ignored when `autoGrow` is false.
27
+ */
28
+ minRows?: number;
29
+ /**
30
+ * Ceiling in text rows while `autoGrow` (theme default `--textarea-autogrow-max-height-rows`,
31
+ * 8); beyond it the control scrolls internally rather than pushing the page. Pass `0` for no
32
+ * ceiling — only correct inside an owning scroll container. Ignored when `autoGrow` is false.
33
+ */
34
+ maxRows?: number;
7
35
  };
8
36
  export declare const Textarea: React.ForwardRefExoticComponent<React.TextareaHTMLAttributes<HTMLTextAreaElement> & {
9
37
  /** Show an inline ✕ (top-end) that clears the field while it holds text (default false). */
10
38
  allowClear?: boolean;
11
39
  /** Called after the field is cleared via the inline ✕. */
12
40
  onClear?: () => void;
41
+ /**
42
+ * `ghost` strips the field's own border, background and focus ring, for a textarea embedded
43
+ * in a surface that already draws the box (a chat composer inside a Card). The surface then
44
+ * owns focus — give it `focus-within`. Default keeps the standalone control chrome.
45
+ */
46
+ variant?: "default" | "ghost";
47
+ /**
48
+ * Grow the control with its content instead of holding a fixed `rows` height (default false —
49
+ * the inert default, so every existing `<Textarea>` keeps the geometry it has today).
50
+ *
51
+ * The floor is `minRows` (or `rows`, when that is the only one given) and never undercuts the
52
+ * `--control-height` tier; past `maxRows` the control stops growing and scrolls internally.
53
+ * Sizing is done in CSS by a hidden replica of the text, so it follows a paste, a programmatic
54
+ * value change, a font swap and a container resize — not just typing — and it never writes
55
+ * `style.height` or reads `scrollHeight`. Works controlled and uncontrolled.
56
+ */
57
+ autoGrow?: boolean;
58
+ /**
59
+ * Floor in text rows while `autoGrow` (theme default `--textarea-autogrow-min-height-rows`, 1).
60
+ * Ignored when `autoGrow` is false.
61
+ */
62
+ minRows?: number;
63
+ /**
64
+ * Ceiling in text rows while `autoGrow` (theme default `--textarea-autogrow-max-height-rows`,
65
+ * 8); beyond it the control scrolls internally rather than pushing the page. Pass `0` for no
66
+ * ceiling — only correct inside an owning scroll container. Ignored when `autoGrow` is false.
67
+ */
68
+ maxRows?: number;
13
69
  } & React.RefAttributes<HTMLTextAreaElement>>;
@@ -4,10 +4,34 @@ import * as React from "react";
4
4
  import { X } from "lucide-react";
5
5
  import { useTranslation } from "../../i18n/use-translation.js";
6
6
  import { cn } from "../../lib/utils.js";
7
- import { controlMultilineClass } from "../../lib/control-styles.js";
7
+ import { useFieldIdentity } from "../../lib/field-a11y.js";
8
+ import { controlMultilineClass, controlMultilineGhostClass } from "../../lib/control-styles.js";
9
+ const UNBOUNDED_ROWS = "infinity";
8
10
  const Textarea = React.forwardRef(
9
- ({ className, allowClear = false, onClear, value, defaultValue, onChange, ...props }, ref) => {
11
+ ({
12
+ className,
13
+ allowClear = false,
14
+ onClear,
15
+ variant = "default",
16
+ autoGrow = false,
17
+ minRows,
18
+ maxRows,
19
+ rows,
20
+ style,
21
+ value,
22
+ defaultValue,
23
+ onChange,
24
+ onCompositionStart,
25
+ onCompositionEnd,
26
+ ...props
27
+ }, ref) => {
10
28
  const { t } = useTranslation();
29
+ const base = variant === "ghost" ? controlMultilineGhostClass : controlMultilineClass;
30
+ const identity = useFieldIdentity({
31
+ id: props.id,
32
+ name: props.name,
33
+ "data-field": props["data-field"]
34
+ });
11
35
  const innerRef = React.useRef(null);
12
36
  const setRefs = React.useCallback(
13
37
  (node) => {
@@ -23,10 +47,39 @@ const Textarea = React.forwardRef(
23
47
  React.useEffect(() => {
24
48
  if (value !== void 0) setHasText(String(value).length > 0);
25
49
  }, [value]);
50
+ const [mirror, setMirror] = React.useState(() => String(value ?? defaultValue ?? ""));
51
+ const composing = React.useRef(false);
52
+ const syncMirror = React.useCallback(() => {
53
+ const el = innerRef.current;
54
+ if (!el || composing.current) return;
55
+ setMirror(el.value);
56
+ }, []);
57
+ React.useLayoutEffect(() => {
58
+ if (autoGrow) syncMirror();
59
+ });
60
+ React.useEffect(() => {
61
+ const form = innerRef.current?.form;
62
+ if (!autoGrow || !form) return;
63
+ const onReset = () => {
64
+ queueMicrotask(syncMirror);
65
+ };
66
+ form.addEventListener("reset", onReset);
67
+ return () => form.removeEventListener("reset", onReset);
68
+ }, [autoGrow, syncMirror]);
26
69
  const handleChange = (event) => {
27
70
  if (value === void 0) setHasText(event.target.value.length > 0);
71
+ if (autoGrow && !composing.current) setMirror(event.target.value);
28
72
  onChange?.(event);
29
73
  };
74
+ const handleCompositionStart = (event) => {
75
+ composing.current = true;
76
+ onCompositionStart?.(event);
77
+ };
78
+ const handleCompositionEnd = (event) => {
79
+ composing.current = false;
80
+ if (autoGrow) setMirror(event.currentTarget.value);
81
+ onCompositionEnd?.(event);
82
+ };
30
83
  const clear = () => {
31
84
  const el = innerRef.current;
32
85
  if (el) {
@@ -39,46 +92,63 @@ const Textarea = React.forwardRef(
39
92
  el.focus();
40
93
  }
41
94
  setHasText(false);
95
+ if (autoGrow) setMirror("");
42
96
  onClear?.();
43
97
  };
44
- if (!allowClear) {
45
- return /* @__PURE__ */ jsx(
46
- "textarea",
47
- {
48
- ref: setRefs,
49
- value,
50
- defaultValue,
51
- onChange,
52
- className: cn(controlMultilineClass, className),
53
- ...props
54
- }
55
- );
56
- }
57
- const showClear = hasText && !props.disabled && !props.readOnly;
58
- return /* @__PURE__ */ jsxs("span", { "data-slot": "textarea-affix-wrapper", className: "relative block w-full", children: [
59
- /* @__PURE__ */ jsx(
60
- "textarea",
61
- {
62
- ref: setRefs,
63
- value,
64
- defaultValue,
65
- onChange: handleChange,
66
- className: cn(controlMultilineClass, showClear && "pe-9", className),
67
- ...props
68
- }
69
- ),
70
- showClear ? /* @__PURE__ */ jsx(
71
- "button",
72
- {
73
- type: "button",
74
- tabIndex: -1,
75
- "aria-label": t("common.clear") ?? "Clear",
76
- onClick: clear,
77
- className: "text-muted-foreground hover:text-foreground absolute end-2 top-2 inline-flex size-5 items-center justify-center rounded-sm opacity-70 transition-opacity hover:opacity-100 focus-visible:opacity-100",
78
- children: /* @__PURE__ */ jsx(X, { className: "size-4", "aria-hidden": "true" })
79
- }
80
- ) : null
81
- ] });
98
+ const floorRows = minRows ?? (typeof rows === "number" ? rows : void 0);
99
+ const autoGrowVars = autoGrow ? {
100
+ ...floorRows === void 0 ? null : { "--textarea-autogrow-min-height-rows": floorRows },
101
+ ...maxRows === void 0 ? null : {
102
+ "--textarea-autogrow-max-height-rows": maxRows === 0 ? UNBOUNDED_ROWS : maxRows
103
+ }
104
+ } : void 0;
105
+ const showClear = allowClear && hasText && !props.disabled && !props.readOnly;
106
+ const needsWrapper = allowClear || autoGrow;
107
+ const field = /* @__PURE__ */ jsx(
108
+ "textarea",
109
+ {
110
+ ref: setRefs,
111
+ value,
112
+ defaultValue,
113
+ rows: autoGrow ? 1 : rows,
114
+ onChange: needsWrapper ? handleChange : onChange,
115
+ onCompositionStart: handleCompositionStart,
116
+ onCompositionEnd: handleCompositionEnd,
117
+ style,
118
+ className: cn(base, showClear && "ui-input--trailing-affix", className),
119
+ ...props,
120
+ ...identity
121
+ }
122
+ );
123
+ if (!needsWrapper) return field;
124
+ return /* @__PURE__ */ jsxs(
125
+ "span",
126
+ {
127
+ "data-slot": "textarea-affix-wrapper",
128
+ "data-autogrow-value": autoGrow ? mirror : void 0,
129
+ style: autoGrowVars,
130
+ className: cn(
131
+ "relative w-full",
132
+ autoGrow ? "grid" : "block",
133
+ autoGrow && "ui-textarea-autogrow",
134
+ autoGrow && variant === "ghost" && "ui-textarea-autogrow--ghost"
135
+ ),
136
+ children: [
137
+ field,
138
+ showClear ? /* @__PURE__ */ jsx(
139
+ "button",
140
+ {
141
+ type: "button",
142
+ tabIndex: -1,
143
+ "aria-label": t("common.clear") ?? "Clear",
144
+ onClick: clear,
145
+ className: "ui-control-inline-affix-action ui-textarea-clear",
146
+ children: /* @__PURE__ */ jsx(X, { className: "ui-control-inline-affix-icon", "aria-hidden": "true" })
147
+ }
148
+ ) : null
149
+ ]
150
+ }
151
+ );
82
152
  }
83
153
  );
84
154
  Textarea.displayName = "Textarea";