twico-ui 1.45.1 → 1.47.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -2,6 +2,20 @@
2
2
 
3
3
  All notable changes to Twico UI are documented here (newest first). This file is generated automatically on each release by [semantic-release](https://semantic-release.gitbook.io/) from [Conventional Commits](https://www.conventionalcommits.org/) — do not edit it by hand. The same notes appear on each [GitHub Release](https://github.com/EncryptionL/twico-ui/releases).
4
4
 
5
+ # [1.47.0](https://github.com/EncryptionL/twico-ui/compare/v1.46.0...v1.47.0) (2026-10-23)
6
+
7
+
8
+ ### Features
9
+
10
+ * **inputs:** reserveFooter — keep a field's footer lane open so rows stay aligned ([223ef88](https://github.com/EncryptionL/twico-ui/commit/223ef8841b41b8fc10e6224530c392ceda2b9a98)), closes [#471](https://github.com/EncryptionL/twico-ui/issues/471)
11
+
12
+ # [1.46.0](https://github.com/EncryptionL/twico-ui/compare/v1.45.1...v1.46.0) (2026-10-21)
13
+
14
+
15
+ ### Features
16
+
17
+ * **datatable,cardgrid:** selectionSummary slot, and move toolbarActions after the built-ins ([8ddbad9](https://github.com/EncryptionL/twico-ui/commit/8ddbad9c3d3321e585d895115ed4bb0b1a9c0c27)), closes [#492](https://github.com/EncryptionL/twico-ui/issues/492) [#478](https://github.com/EncryptionL/twico-ui/issues/478) [#286](https://github.com/EncryptionL/twico-ui/issues/286)
18
+
5
19
  ## [1.45.1](https://github.com/EncryptionL/twico-ui/compare/v1.45.0...v1.45.1) (2026-10-21)
6
20
 
7
21
 
package/dist/index.cjs CHANGED
@@ -5901,6 +5901,17 @@ var Checkbox = /* @__PURE__ */ React55__namespace.default.forwardRef(function Ch
5901
5901
  .twc-field__req { color: var(--color-danger); }
5902
5902
  .twc-check__desc { font-size: var(--text-xs); color: var(--color-text-muted); }
5903
5903
  .twc-field { display: flex; flex-direction: column; gap: var(--space-1-5); font-family: var(--font-sans); }
5904
+ /* #494: reserve the footer lane. A row of fields aligned with align="flex-end" splits in two the
5905
+ moment ONE of them has a hint, because that field grows a footer and the others do not - and the
5906
+ caption then moves a DIFFERENT control, which nothing in the JSX suggests. align= cannot fix it:
5907
+ flex-start lines up the labels instead, so the controls part company as soon as one label wraps.
5908
+ The lane is held open by generated content rather than a fixed height so it matches the real
5909
+ footer by construction - same font metrics, same flex gap - instead of by a number that drifts.
5910
+ Whitespace-only generated content is not announced, and :has() lets the CSS tell on its own
5911
+ whether a footer is already there, so no component has to report it. */
5912
+ .twc-field[data-reserve-footer]:not(:has(> .twc-field__footer, > .twc-field__hint, > .twc-field__error))::after,
5913
+ [data-reserve-footer] .twc-field:not(:has(> .twc-field__footer, > .twc-field__hint, > .twc-field__error))::after {
5914
+ content: "\\00a0"; font-size: var(--text-xs); user-select: none; }
5904
5915
  .twc-field__error { font-size: var(--text-xs); color: var(--color-danger-subtle-fg); font-weight: var(--font-medium); }
5905
5916
  `);
5906
5917
  const autoId = React55__namespace.default.useId();
@@ -5955,6 +5966,17 @@ var Checkbox = /* @__PURE__ */ React55__namespace.default.forwardRef(function Ch
5955
5966
  });
5956
5967
  var FIELD_CSS = `
5957
5968
  .twc-field { display: flex; flex-direction: column; gap: var(--space-1-5); font-family: var(--font-sans); }
5969
+ /* #494: reserve the footer lane. A row of fields aligned with align="flex-end" splits in two the
5970
+ moment ONE of them has a hint, because that field grows a footer and the others do not - and the
5971
+ caption then moves a DIFFERENT control, which nothing in the JSX suggests. align= cannot fix it:
5972
+ flex-start lines up the labels instead, so the controls part company as soon as one label wraps.
5973
+ The lane is held open by generated content rather than a fixed height so it matches the real
5974
+ footer by construction - same font metrics, same flex gap - instead of by a number that drifts.
5975
+ Whitespace-only generated content is not announced, and :has() lets the CSS tell on its own
5976
+ whether a footer is already there, so no component has to report it. */
5977
+ .twc-field[data-reserve-footer]:not(:has(> .twc-field__footer, > .twc-field__hint, > .twc-field__error))::after,
5978
+ [data-reserve-footer] .twc-field:not(:has(> .twc-field__footer, > .twc-field__hint, > .twc-field__error))::after {
5979
+ content: "\\00a0"; font-size: var(--text-xs); user-select: none; }
5958
5980
  .twc-field__label { font-size: var(--text-sm); font-weight: var(--font-semibold); color: var(--color-text); display: flex; gap: 4px; align-items: center; }
5959
5981
  .twc-field__req { color: var(--color-danger); }
5960
5982
  .twc-field__hint { font-size: var(--text-xs); color: var(--color-text-muted); }
@@ -6066,6 +6088,7 @@ function ColorPicker({
6066
6088
  tone = "primary",
6067
6089
  onChange,
6068
6090
  className = "",
6091
+ reserveFooter = false,
6069
6092
  ...rest
6070
6093
  }) {
6071
6094
  const __twcStyles0 = useScopedStyles("twc-field-styles", FIELD_CSS);
@@ -6205,7 +6228,7 @@ function ColorPicker({
6205
6228
  e.preventDefault();
6206
6229
  commit({ ...hsv, a: Math.round(a * 100) / 100 });
6207
6230
  };
6208
- return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: `twc-cp twc-field ${className}`, ref: wrapRef, ...rest, children: [
6231
+ return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: `twc-cp twc-field ${className}`, ref: wrapRef, ...rest, "data-reserve-footer": reserveFooter || void 0, children: [
6209
6232
  __twcStyles0,
6210
6233
  __twcStyles1,
6211
6234
  label ? /* @__PURE__ */ jsxRuntime.jsxs("span", { className: "twc-field__label", id: labelId, children: [
@@ -6368,6 +6391,17 @@ function ColorPicker({
6368
6391
  }
6369
6392
  var COMBO_CSS = `
6370
6393
  .twc-field { display: flex; flex-direction: column; gap: var(--space-1-5); font-family: var(--font-sans); }
6394
+ /* #494: reserve the footer lane. A row of fields aligned with align="flex-end" splits in two the
6395
+ moment ONE of them has a hint, because that field grows a footer and the others do not - and the
6396
+ caption then moves a DIFFERENT control, which nothing in the JSX suggests. align= cannot fix it:
6397
+ flex-start lines up the labels instead, so the controls part company as soon as one label wraps.
6398
+ The lane is held open by generated content rather than a fixed height so it matches the real
6399
+ footer by construction - same font metrics, same flex gap - instead of by a number that drifts.
6400
+ Whitespace-only generated content is not announced, and :has() lets the CSS tell on its own
6401
+ whether a footer is already there, so no component has to report it. */
6402
+ .twc-field[data-reserve-footer]:not(:has(> .twc-field__footer, > .twc-field__hint, > .twc-field__error))::after,
6403
+ [data-reserve-footer] .twc-field:not(:has(> .twc-field__footer, > .twc-field__hint, > .twc-field__error))::after {
6404
+ content: "\\00a0"; font-size: var(--text-xs); user-select: none; }
6371
6405
  .twc-field__label { font-size: var(--text-sm); font-weight: var(--font-semibold); color: var(--color-text); display: flex; gap: 4px; align-items: center; }
6372
6406
  .twc-field__req { color: var(--color-danger); }
6373
6407
  .twc-field__hint { font-size: var(--text-xs); color: var(--color-text-muted); }
@@ -6506,6 +6540,7 @@ var Combobox = /* @__PURE__ */ React55__namespace.default.forwardRef(function Co
6506
6540
  className = "",
6507
6541
  onFocus,
6508
6542
  onKeyDown,
6543
+ reserveFooter = false,
6509
6544
  ...rest
6510
6545
  }, ref) {
6511
6546
  const __twcStyles = useScopedStyles("twc-combo-styles", COMBO_CSS);
@@ -6793,7 +6828,7 @@ var Combobox = /* @__PURE__ */ React55__namespace.default.forwardRef(function Co
6793
6828
  popEl = /* @__PURE__ */ jsxRuntime.jsx("div", { className: "twc-pop", ref: popRef, "data-placement": placement === "top" ? "top" : void 0, children: popInner });
6794
6829
  }
6795
6830
  }
6796
- return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: `twc-field ${className}`, ref: wrapRef, children: [
6831
+ return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: `twc-field ${className}`, ref: wrapRef, "data-reserve-footer": reserveFooter || void 0, children: [
6797
6832
  __twcStyles,
6798
6833
  name ? /* @__PURE__ */ jsxRuntime.jsx("input", { type: "hidden", name, value: current ?? "" }) : null,
6799
6834
  label ? /* @__PURE__ */ jsxRuntime.jsxs("label", { className: "twc-field__label", htmlFor: fieldId, children: [
@@ -7113,6 +7148,17 @@ function clampPrecision(str, precision) {
7113
7148
  }
7114
7149
  var CUR_CSS = `
7115
7150
  .twc-field { display: flex; flex-direction: column; gap: var(--space-1-5); font-family: var(--font-sans); }
7151
+ /* #494: reserve the footer lane. A row of fields aligned with align="flex-end" splits in two the
7152
+ moment ONE of them has a hint, because that field grows a footer and the others do not - and the
7153
+ caption then moves a DIFFERENT control, which nothing in the JSX suggests. align= cannot fix it:
7154
+ flex-start lines up the labels instead, so the controls part company as soon as one label wraps.
7155
+ The lane is held open by generated content rather than a fixed height so it matches the real
7156
+ footer by construction - same font metrics, same flex gap - instead of by a number that drifts.
7157
+ Whitespace-only generated content is not announced, and :has() lets the CSS tell on its own
7158
+ whether a footer is already there, so no component has to report it. */
7159
+ .twc-field[data-reserve-footer]:not(:has(> .twc-field__footer, > .twc-field__hint, > .twc-field__error))::after,
7160
+ [data-reserve-footer] .twc-field:not(:has(> .twc-field__footer, > .twc-field__hint, > .twc-field__error))::after {
7161
+ content: "\\00a0"; font-size: var(--text-xs); user-select: none; }
7116
7162
  .twc-field__label { font-size: var(--text-sm); font-weight: var(--font-semibold); color: var(--color-text); display: flex; gap: 4px; align-items: center; }
7117
7163
  .twc-field__req { color: var(--color-danger); }
7118
7164
  .twc-field__hint { font-size: var(--text-xs); color: var(--color-text-muted); }
@@ -7182,6 +7228,7 @@ function Currency({
7182
7228
  id,
7183
7229
  placeholder = "0.00",
7184
7230
  className = "",
7231
+ reserveFooter = false,
7185
7232
  ...rest
7186
7233
  }) {
7187
7234
  const __twcStyles = useScopedStyles("twc-cur-styles", CUR_CSS);
@@ -7230,7 +7277,7 @@ function Currency({
7230
7277
  if (!controlled) setInternal(fixed);
7231
7278
  onValueChange?.(Number(fixed), fixed);
7232
7279
  }
7233
- return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: `twc-field ${className}`, children: [
7280
+ return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: `twc-field ${className}`, "data-reserve-footer": reserveFooter || void 0, children: [
7234
7281
  __twcStyles,
7235
7282
  label ? /* @__PURE__ */ jsxRuntime.jsxs("label", { className: "twc-field__label", htmlFor: fieldId, children: [
7236
7283
  label,
@@ -7282,6 +7329,17 @@ function useFallbackName(ref, hasOwnName) {
7282
7329
  }
7283
7330
  var SELECT_CSS = `
7284
7331
  .twc-field { display: flex; flex-direction: column; gap: var(--space-1-5); font-family: var(--font-sans); }
7332
+ /* #494: reserve the footer lane. A row of fields aligned with align="flex-end" splits in two the
7333
+ moment ONE of them has a hint, because that field grows a footer and the others do not - and the
7334
+ caption then moves a DIFFERENT control, which nothing in the JSX suggests. align= cannot fix it:
7335
+ flex-start lines up the labels instead, so the controls part company as soon as one label wraps.
7336
+ The lane is held open by generated content rather than a fixed height so it matches the real
7337
+ footer by construction - same font metrics, same flex gap - instead of by a number that drifts.
7338
+ Whitespace-only generated content is not announced, and :has() lets the CSS tell on its own
7339
+ whether a footer is already there, so no component has to report it. */
7340
+ .twc-field[data-reserve-footer]:not(:has(> .twc-field__footer, > .twc-field__hint, > .twc-field__error))::after,
7341
+ [data-reserve-footer] .twc-field:not(:has(> .twc-field__footer, > .twc-field__hint, > .twc-field__error))::after {
7342
+ content: "\\00a0"; font-size: var(--text-xs); user-select: none; }
7285
7343
  .twc-field__label { font-size: var(--text-sm); font-weight: var(--font-semibold); color: var(--color-text); display: flex; gap: 4px; align-items: center; }
7286
7344
  .twc-field__req { color: var(--color-danger); }
7287
7345
  .twc-field__hint { font-size: var(--text-xs); color: var(--color-text-muted); }
@@ -7418,6 +7476,7 @@ var Select = /* @__PURE__ */ React55__namespace.default.forwardRef(function Sele
7418
7476
  renderOption,
7419
7477
  id,
7420
7478
  className = "",
7479
+ reserveFooter = false,
7421
7480
  onClick,
7422
7481
  onKeyDown,
7423
7482
  ...rest
@@ -7763,7 +7822,7 @@ var Select = /* @__PURE__ */ React55__namespace.default.forwardRef(function Sele
7763
7822
  popEl = /* @__PURE__ */ jsxRuntime.jsx("div", { className: "twc-pop", ref: popRef, "data-state": popState, "data-placement": placement === "top" ? "top" : "bottom", children: popInner });
7764
7823
  }
7765
7824
  }
7766
- return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: `twc-field ${className}`, ref: wrapRef, children: [
7825
+ return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: `twc-field ${className}`, ref: wrapRef, "data-reserve-footer": reserveFooter || void 0, children: [
7767
7826
  __twcStyles,
7768
7827
  name ? /* @__PURE__ */ jsxRuntime.jsx("input", { type: "hidden", name, value: current ?? "" }) : null,
7769
7828
  label ? /* @__PURE__ */ jsxRuntime.jsxs("label", { className: "twc-field__label", htmlFor: fieldId, children: [
@@ -7888,6 +7947,7 @@ function CurrencyField({
7888
7947
  id,
7889
7948
  placeholder = "0.00",
7890
7949
  className = "",
7950
+ reserveFooter = false,
7891
7951
  ...rest
7892
7952
  }) {
7893
7953
  const __twcStyles = useScopedStyles("twc-curf-styles", CURF_CSS);
@@ -7950,7 +8010,7 @@ function CurrencyField({
7950
8010
  onValueChange?.(Number(fixed), fixed, cur);
7951
8011
  }
7952
8012
  const curLabel = typeof label === "string" && label ? `${label} currency` : "Currency";
7953
- return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: `twc-field ${className}`, children: [
8013
+ return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: `twc-field ${className}`, "data-reserve-footer": reserveFooter || void 0, children: [
7954
8014
  __twcStyles,
7955
8015
  label ? /* @__PURE__ */ jsxRuntime.jsxs("label", { className: "twc-field__label", htmlFor: fieldId, children: [
7956
8016
  label,
@@ -7997,6 +8057,17 @@ function CurrencyField({
7997
8057
  }
7998
8058
  var FIELD_CSS2 = `
7999
8059
  .twc-field { display: flex; flex-direction: column; gap: var(--space-1-5); font-family: var(--font-sans); }
8060
+ /* #494: reserve the footer lane. A row of fields aligned with align="flex-end" splits in two the
8061
+ moment ONE of them has a hint, because that field grows a footer and the others do not - and the
8062
+ caption then moves a DIFFERENT control, which nothing in the JSX suggests. align= cannot fix it:
8063
+ flex-start lines up the labels instead, so the controls part company as soon as one label wraps.
8064
+ The lane is held open by generated content rather than a fixed height so it matches the real
8065
+ footer by construction - same font metrics, same flex gap - instead of by a number that drifts.
8066
+ Whitespace-only generated content is not announced, and :has() lets the CSS tell on its own
8067
+ whether a footer is already there, so no component has to report it. */
8068
+ .twc-field[data-reserve-footer]:not(:has(> .twc-field__footer, > .twc-field__hint, > .twc-field__error))::after,
8069
+ [data-reserve-footer] .twc-field:not(:has(> .twc-field__footer, > .twc-field__hint, > .twc-field__error))::after {
8070
+ content: "\\00a0"; font-size: var(--text-xs); user-select: none; }
8000
8071
  .twc-field__label { font-size: var(--text-sm); font-weight: var(--font-semibold); color: var(--color-text); display: flex; gap: 4px; align-items: center; }
8001
8072
  .twc-field__req { color: var(--color-danger); }
8002
8073
  .twc-field__hint { font-size: var(--text-xs); color: var(--color-text-muted); }
@@ -8086,6 +8157,7 @@ var Input = /* @__PURE__ */ React55__namespace.default.forwardRef(function Input
8086
8157
  disabled = false,
8087
8158
  id,
8088
8159
  className = "",
8160
+ reserveFooter = false,
8089
8161
  ...rest
8090
8162
  }, ref) {
8091
8163
  const __twcStyles = useScopedStyles("twc-field-styles", FIELD_CSS2);
@@ -8155,7 +8227,7 @@ var Input = /* @__PURE__ */ React55__namespace.default.forwardRef(function Input
8155
8227
  ] })
8156
8228
  }
8157
8229
  ) : null;
8158
- return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: `twc-field ${className}`, children: [
8230
+ return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: `twc-field ${className}`, "data-reserve-footer": reserveFooter || void 0, children: [
8159
8231
  __twcStyles,
8160
8232
  __twcExtra,
8161
8233
  label ? /* @__PURE__ */ jsxRuntime.jsxs("label", { className: "twc-field__label", htmlFor: fieldId, children: [
@@ -8200,6 +8272,17 @@ var Input = /* @__PURE__ */ React55__namespace.default.forwardRef(function Input
8200
8272
  });
8201
8273
  var MULTI_CSS = `
8202
8274
  .twc-field { display: flex; flex-direction: column; gap: var(--space-1-5); font-family: var(--font-sans); }
8275
+ /* #494: reserve the footer lane. A row of fields aligned with align="flex-end" splits in two the
8276
+ moment ONE of them has a hint, because that field grows a footer and the others do not - and the
8277
+ caption then moves a DIFFERENT control, which nothing in the JSX suggests. align= cannot fix it:
8278
+ flex-start lines up the labels instead, so the controls part company as soon as one label wraps.
8279
+ The lane is held open by generated content rather than a fixed height so it matches the real
8280
+ footer by construction - same font metrics, same flex gap - instead of by a number that drifts.
8281
+ Whitespace-only generated content is not announced, and :has() lets the CSS tell on its own
8282
+ whether a footer is already there, so no component has to report it. */
8283
+ .twc-field[data-reserve-footer]:not(:has(> .twc-field__footer, > .twc-field__hint, > .twc-field__error))::after,
8284
+ [data-reserve-footer] .twc-field:not(:has(> .twc-field__footer, > .twc-field__hint, > .twc-field__error))::after {
8285
+ content: "\\00a0"; font-size: var(--text-xs); user-select: none; }
8203
8286
  .twc-field__label { font-size: var(--text-sm); font-weight: var(--font-semibold); color: var(--color-text); display: flex; gap: 4px; align-items: center; }
8204
8287
  .twc-field__req { color: var(--color-danger); }
8205
8288
  .twc-field__hint { font-size: var(--text-xs); color: var(--color-text-muted); }
@@ -8345,6 +8428,7 @@ var MultiSelect = /* @__PURE__ */ React55__namespace.default.forwardRef(function
8345
8428
  className = "",
8346
8429
  onFocus,
8347
8430
  onKeyDown,
8431
+ reserveFooter = false,
8348
8432
  ...rest
8349
8433
  }, ref) {
8350
8434
  const __twcStyles = useScopedStyles("twc-ms-styles", MULTI_CSS);
@@ -8618,7 +8702,7 @@ var MultiSelect = /* @__PURE__ */ React55__namespace.default.forwardRef(function
8618
8702
  }
8619
8703
  }
8620
8704
  const msFallbackName = useFallbackName(inputRef, Boolean(label || rest["aria-label"] || rest["aria-labelledby"])) ? placeholder : void 0;
8621
- return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: `twc-field ${className}`, ref: wrapRef, children: [
8705
+ return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: `twc-field ${className}`, ref: wrapRef, "data-reserve-footer": reserveFooter || void 0, children: [
8622
8706
  __twcStyles,
8623
8707
  name ? selected.map((v, i) => /* @__PURE__ */ jsxRuntime.jsx("input", { type: "hidden", name, value: v }, i)) : null,
8624
8708
  label ? /* @__PURE__ */ jsxRuntime.jsxs("label", { className: "twc-field__label", htmlFor: fieldId, children: [
@@ -9558,6 +9642,12 @@ th.twc-dt__rownum .twc-dt__th-inner { padding-inline: 8px; gap: 2px; justify-con
9558
9642
  .twc-dt__batch-x:hover { background: color-mix(in srgb, var(--color-primary) 20%, transparent); }
9559
9643
  .twc-dt__batch-x svg { width: 16px; height: 16px; }
9560
9644
  .twc-dt__batch-count { font-size: var(--text-sm); font-weight: var(--font-bold); color: var(--color-primary-subtle-fg); white-space: nowrap; }
9645
+ /* #492: the host's running total, sitting with the count it qualifies. Same colour as the count,
9646
+ differentiated by WEIGHT rather than by a lighter token: this bar is --color-primary-subtle, and
9647
+ --color-text-subtle on that is 4.26:1, under the 4.5:1 needed for normal text. */
9648
+ .twc-dt__batch-summary { display: inline-flex; align-items: center; gap: 6px; font-size: var(--text-sm);
9649
+ color: var(--color-primary-subtle-fg); white-space: nowrap; }
9650
+ .twc-dt__batch-summary::before { content: ""; width: var(--border-thin); height: 14px; background: currentColor; opacity: 0.35; }
9561
9651
  .twc-dt__batch-actions { margin-inline-start: auto; display: flex; align-items: center; gap: 6px; flex-wrap: wrap; }
9562
9652
  .twc-dt__batch-btn { display: inline-flex; align-items: center; gap: 6px; height: 32px; padding: 0 12px;
9563
9653
  border: var(--border-thin) solid var(--color-border); border-radius: var(--radius-md); background: var(--color-surface);
@@ -10058,6 +10148,7 @@ function Datatable({
10058
10148
  onColumnVisibilityChange,
10059
10149
  batchActions = [],
10060
10150
  onRowSelectionChange,
10151
+ selectionSummary,
10061
10152
  getRowStyle,
10062
10153
  getRowClassName,
10063
10154
  showExport = false,
@@ -12927,13 +13018,13 @@ function Datatable({
12927
13018
  return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: `twc-dt ${className}`, ref: rootRef, "data-density": density, "data-resizing": resizing || void 0, "data-editing": editing ? "" : void 0, ...rest, children: [
12928
13019
  __twcStyles,
12929
13020
  /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "twc-dt__toolbar", "data-compact": compact || void 0, children: [
12930
- toolbarActions != null ? /* @__PURE__ */ jsxRuntime.jsx("div", { className: "twc-dt__toolbar-actions", children: toolbarActions }) : null,
12931
13021
  (batchActions.length || hasBatchEditor) && selected.size > 0 ? /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "twc-dt__batch", children: [
12932
13022
  /* @__PURE__ */ jsxRuntime.jsx("button", { type: "button", className: "twc-dt__batch-x", onClick: clearSelection, "aria-label": "Clear selection", children: /* @__PURE__ */ jsxRuntime.jsx(Svg, { d: I.x }) }),
12933
13023
  /* @__PURE__ */ jsxRuntime.jsxs("span", { className: "twc-dt__batch-count", children: [
12934
13024
  selected.size,
12935
13025
  " selected"
12936
13026
  ] }),
13027
+ selectionSummary != null ? /* @__PURE__ */ jsxRuntime.jsx("span", { className: "twc-dt__batch-summary", children: typeof selectionSummary === "function" ? selectionSummary(selKeys, selectedRows) : selectionSummary }) : null,
12937
13028
  /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "twc-dt__batch-actions", children: [
12938
13029
  hasBatchEditor ? /* @__PURE__ */ jsxRuntime.jsxs("button", { type: "button", className: "twc-dt__batch-btn", onClick: (e) => openBatchEditor(e.currentTarget), children: [
12939
13030
  /* @__PURE__ */ jsxRuntime.jsx(Svg, { d: I.pencil }),
@@ -13128,6 +13219,7 @@ function Datatable({
13128
13219
  }
13129
13220
  )
13130
13221
  ] }) : null,
13222
+ toolbarActions != null ? /* @__PURE__ */ jsxRuntime.jsx("div", { className: "twc-dt__toolbar-actions", children: toolbarActions }) : null,
13131
13223
  searchable ? /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "twc-dt__search", children: [
13132
13224
  /* @__PURE__ */ jsxRuntime.jsx(Svg, { d: I.search }),
13133
13225
  /* @__PURE__ */ jsxRuntime.jsx("input", { ref: quickSearchRef, placeholder: "Search\u2026", "aria-label": "Search rows", value: quick, onChange: (e) => commitQuick(e.target.value) }),
@@ -14416,6 +14508,11 @@ var CARDGRID_CSS = `
14416
14508
  padding: var(--space-2) var(--space-3); border: var(--border-thin) solid var(--color-primary-border);
14417
14509
  background: var(--color-primary-subtle); border-radius: var(--radius-md); }
14418
14510
  .twc-cardgrid__batch-count { font-size: var(--text-sm); font-weight: var(--font-semibold); color: var(--color-primary-subtle-fg); }
14511
+ /* #492: same slot, same contract as Datatable. Weight, not a lighter token - this bar is
14512
+ --color-primary-subtle, where --color-text-subtle is 4.26:1 and fails normal-text contrast. */
14513
+ .twc-cardgrid__batch-summary { display: inline-flex; align-items: center; gap: var(--space-2);
14514
+ font-size: var(--text-sm); color: var(--color-primary-subtle-fg); }
14515
+ .twc-cardgrid__batch-summary::before { content: ""; width: var(--border-thin); height: 14px; background: currentColor; opacity: 0.35; }
14419
14516
  .twc-cardgrid__batch-actions { display: inline-flex; flex-wrap: wrap; align-items: center; gap: var(--space-2); margin-inline-start: auto; }
14420
14517
  .twc-cardgrid__batch-btn { display: inline-flex; align-items: center; gap: var(--space-2); height: var(--control-h-sm);
14421
14518
  padding-inline: var(--space-3); border: var(--border-thin) solid var(--color-border); background: var(--color-surface);
@@ -14471,6 +14568,7 @@ function CardGrid({
14471
14568
  // the whole point, and it was the only Datatable affordance CardGrid lacked.
14472
14569
  checkboxSelection = false,
14473
14570
  batchActions = [],
14571
+ selectionSummary,
14474
14572
  onRowSelectionChange,
14475
14573
  selectionPlacement = "top-start",
14476
14574
  selectionLabel,
@@ -14678,6 +14776,7 @@ function CardGrid({
14678
14776
  selected.size,
14679
14777
  " selected"
14680
14778
  ] }),
14779
+ selectionSummary != null ? /* @__PURE__ */ jsxRuntime.jsx("span", { className: "twc-cardgrid__batch-summary", children: typeof selectionSummary === "function" ? selectionSummary(selKeys, selectedRows) : selectionSummary }) : null,
14681
14780
  showSelectAll && pageRows.length > 0 ? /* @__PURE__ */ jsxRuntime.jsxs("label", { className: "twc-cardgrid__selall", children: [
14682
14781
  /* @__PURE__ */ jsxRuntime.jsx(
14683
14782
  Checkbox,
@@ -14835,6 +14934,17 @@ function DiffTable({
14835
14934
  }
14836
14935
  var FIELD_CSS3 = `
14837
14936
  .twc-field { display: flex; flex-direction: column; gap: var(--space-1-5); font-family: var(--font-sans); }
14937
+ /* #494: reserve the footer lane. A row of fields aligned with align="flex-end" splits in two the
14938
+ moment ONE of them has a hint, because that field grows a footer and the others do not - and the
14939
+ caption then moves a DIFFERENT control, which nothing in the JSX suggests. align= cannot fix it:
14940
+ flex-start lines up the labels instead, so the controls part company as soon as one label wraps.
14941
+ The lane is held open by generated content rather than a fixed height so it matches the real
14942
+ footer by construction - same font metrics, same flex gap - instead of by a number that drifts.
14943
+ Whitespace-only generated content is not announced, and :has() lets the CSS tell on its own
14944
+ whether a footer is already there, so no component has to report it. */
14945
+ .twc-field[data-reserve-footer]:not(:has(> .twc-field__footer, > .twc-field__hint, > .twc-field__error))::after,
14946
+ [data-reserve-footer] .twc-field:not(:has(> .twc-field__footer, > .twc-field__hint, > .twc-field__error))::after {
14947
+ content: "\\00a0"; font-size: var(--text-xs); user-select: none; }
14838
14948
  .twc-field__label { font-size: var(--text-sm); font-weight: var(--font-semibold); color: var(--color-text); display: flex; gap: 4px; align-items: center; }
14839
14949
  .twc-field__req { color: var(--color-danger); }
14840
14950
  .twc-field__hint { font-size: var(--text-xs); color: var(--color-text-muted); }
@@ -14967,6 +15077,7 @@ function DatePicker({
14967
15077
  onChange,
14968
15078
  id,
14969
15079
  className = "",
15080
+ reserveFooter = false,
14970
15081
  ...rest
14971
15082
  }) {
14972
15083
  const __twcStyles0 = useScopedStyles("twc-field-styles", FIELD_CSS3);
@@ -15169,7 +15280,7 @@ function DatePicker({
15169
15280
  setFocusDate(next);
15170
15281
  if (next.getFullYear() !== y || next.getMonth() !== m) setView(new Date(next.getFullYear(), next.getMonth(), 1));
15171
15282
  };
15172
- return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: `twc-dp twc-field ${className}`, ref: wrapRef, ...rest, children: [
15283
+ return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: `twc-dp twc-field ${className}`, ref: wrapRef, ...rest, "data-reserve-footer": reserveFooter || void 0, children: [
15173
15284
  __twcStyles0,
15174
15285
  __twcStyles1,
15175
15286
  label ? /* @__PURE__ */ jsxRuntime.jsxs("label", { className: "twc-field__label", id: `${fieldId}-label`, children: [
@@ -15393,6 +15504,17 @@ function DatePicker({
15393
15504
  }
15394
15505
  var FIELD_CSS4 = `
15395
15506
  .twc-field { display: flex; flex-direction: column; gap: var(--space-1-5); font-family: var(--font-sans); }
15507
+ /* #494: reserve the footer lane. A row of fields aligned with align="flex-end" splits in two the
15508
+ moment ONE of them has a hint, because that field grows a footer and the others do not - and the
15509
+ caption then moves a DIFFERENT control, which nothing in the JSX suggests. align= cannot fix it:
15510
+ flex-start lines up the labels instead, so the controls part company as soon as one label wraps.
15511
+ The lane is held open by generated content rather than a fixed height so it matches the real
15512
+ footer by construction - same font metrics, same flex gap - instead of by a number that drifts.
15513
+ Whitespace-only generated content is not announced, and :has() lets the CSS tell on its own
15514
+ whether a footer is already there, so no component has to report it. */
15515
+ .twc-field[data-reserve-footer]:not(:has(> .twc-field__footer, > .twc-field__hint, > .twc-field__error))::after,
15516
+ [data-reserve-footer] .twc-field:not(:has(> .twc-field__footer, > .twc-field__hint, > .twc-field__error))::after {
15517
+ content: "\\00a0"; font-size: var(--text-xs); user-select: none; }
15396
15518
  .twc-field__label { font-size: var(--text-sm); font-weight: var(--font-semibold); color: var(--color-text); display: flex; gap: 4px; align-items: center; }
15397
15519
  .twc-field__req { color: var(--color-danger); }
15398
15520
  .twc-field__hint { font-size: var(--text-xs); color: var(--color-text-muted); }
@@ -15528,6 +15650,7 @@ function DateRangePicker({
15528
15650
  parse,
15529
15651
  onChange,
15530
15652
  className = "",
15653
+ reserveFooter = false,
15531
15654
  ...rest
15532
15655
  }) {
15533
15656
  const __twcStyles0 = useScopedStyles("twc-field-styles", FIELD_CSS4);
@@ -15772,7 +15895,7 @@ function DateRangePicker({
15772
15895
  return t > Math.min(s, e) && t < Math.max(s, e);
15773
15896
  };
15774
15897
  const text = range2.start ? format ? format(range2) : `${fmtD(range2.start, locale)} \u2013 ${range2.end ? fmtD(range2.end, locale) : "\u2026"}` : "";
15775
- return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: `twc-drp twc-field ${className}`, ref: wrapRef, ...rest, children: [
15898
+ return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: `twc-drp twc-field ${className}`, ref: wrapRef, ...rest, "data-reserve-footer": reserveFooter || void 0, children: [
15776
15899
  __twcStyles0,
15777
15900
  __twcStyles1,
15778
15901
  label ? /* @__PURE__ */ jsxRuntime.jsxs("span", { className: "twc-field__label", id: labelId, children: [
@@ -16000,6 +16123,17 @@ function DateRangePicker({
16000
16123
  }
16001
16124
  var FIELD_CSS5 = `
16002
16125
  .twc-field { display: flex; flex-direction: column; gap: var(--space-1-5); font-family: var(--font-sans); }
16126
+ /* #494: reserve the footer lane. A row of fields aligned with align="flex-end" splits in two the
16127
+ moment ONE of them has a hint, because that field grows a footer and the others do not - and the
16128
+ caption then moves a DIFFERENT control, which nothing in the JSX suggests. align= cannot fix it:
16129
+ flex-start lines up the labels instead, so the controls part company as soon as one label wraps.
16130
+ The lane is held open by generated content rather than a fixed height so it matches the real
16131
+ footer by construction - same font metrics, same flex gap - instead of by a number that drifts.
16132
+ Whitespace-only generated content is not announced, and :has() lets the CSS tell on its own
16133
+ whether a footer is already there, so no component has to report it. */
16134
+ .twc-field[data-reserve-footer]:not(:has(> .twc-field__footer, > .twc-field__hint, > .twc-field__error))::after,
16135
+ [data-reserve-footer] .twc-field:not(:has(> .twc-field__footer, > .twc-field__hint, > .twc-field__error))::after {
16136
+ content: "\\00a0"; font-size: var(--text-xs); user-select: none; }
16003
16137
  .twc-field__label { font-size: var(--text-sm); font-weight: var(--font-semibold); color: var(--color-text); display: flex; gap: 4px; align-items: center; }
16004
16138
  .twc-field__req { color: var(--color-danger); }
16005
16139
  .twc-field__hint { font-size: var(--text-xs); color: var(--color-text-muted); }
@@ -16098,6 +16232,7 @@ function TimePicker({
16098
16232
  onChange,
16099
16233
  id,
16100
16234
  className = "",
16235
+ reserveFooter = false,
16101
16236
  ...rest
16102
16237
  }) {
16103
16238
  const __twcStyles0 = useScopedStyles("twc-field-styles", FIELD_CSS5);
@@ -16312,7 +16447,7 @@ function TimePicker({
16312
16447
  /* @__PURE__ */ jsxRuntime.jsx("circle", { cx: "12", cy: "12", r: "9" }),
16313
16448
  /* @__PURE__ */ jsxRuntime.jsx("path", { d: "M12 7v5l3 2" })
16314
16449
  ] });
16315
- return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: `twc-tp twc-field ${className}`, ref: wrapRef, ...rest, children: [
16450
+ return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: `twc-tp twc-field ${className}`, ref: wrapRef, ...rest, "data-reserve-footer": reserveFooter || void 0, children: [
16316
16451
  __twcStyles0,
16317
16452
  __twcStyles1,
16318
16453
  label ? /* @__PURE__ */ jsxRuntime.jsxs("label", { className: "twc-field__label", id: `${fieldId}-label`, children: [
@@ -16469,6 +16604,17 @@ function TimePicker({
16469
16604
  }
16470
16605
  var FIELD_CSS6 = `
16471
16606
  .twc-field { display: flex; flex-direction: column; gap: var(--space-1-5); font-family: var(--font-sans); }
16607
+ /* #494: reserve the footer lane. A row of fields aligned with align="flex-end" splits in two the
16608
+ moment ONE of them has a hint, because that field grows a footer and the others do not - and the
16609
+ caption then moves a DIFFERENT control, which nothing in the JSX suggests. align= cannot fix it:
16610
+ flex-start lines up the labels instead, so the controls part company as soon as one label wraps.
16611
+ The lane is held open by generated content rather than a fixed height so it matches the real
16612
+ footer by construction - same font metrics, same flex gap - instead of by a number that drifts.
16613
+ Whitespace-only generated content is not announced, and :has() lets the CSS tell on its own
16614
+ whether a footer is already there, so no component has to report it. */
16615
+ .twc-field[data-reserve-footer]:not(:has(> .twc-field__footer, > .twc-field__hint, > .twc-field__error))::after,
16616
+ [data-reserve-footer] .twc-field:not(:has(> .twc-field__footer, > .twc-field__hint, > .twc-field__error))::after {
16617
+ content: "\\00a0"; font-size: var(--text-xs); user-select: none; }
16472
16618
  .twc-field__label { font-size: var(--text-sm); font-weight: var(--font-semibold); color: var(--color-text); display: flex; gap: 4px; align-items: center; }
16473
16619
  .twc-field__req { color: var(--color-danger); }
16474
16620
  .twc-field__hint { font-size: var(--text-xs); color: var(--color-text-muted); }
@@ -16507,6 +16653,7 @@ function DateTimePicker({
16507
16653
  onChange,
16508
16654
  id,
16509
16655
  className = "",
16656
+ reserveFooter = false,
16510
16657
  ...rest
16511
16658
  }) {
16512
16659
  const __twcStyles0 = useScopedStyles("twc-field-styles", FIELD_CSS6);
@@ -16531,7 +16678,7 @@ function DateTimePicker({
16531
16678
  commitVal(new Date(b ? b.getFullYear() : t.getFullYear(), b ? b.getMonth() : t.getMonth(), b ? b.getDate() : t.getDate(), t.getHours(), t.getMinutes(), t.getSeconds(), 0));
16532
16679
  };
16533
16680
  const childTone = error ? "danger" : tone;
16534
- return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: `twc-dtp twc-field ${className}`, id: fieldId, ...rest, children: [
16681
+ return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: `twc-dtp twc-field ${className}`, id: fieldId, ...rest, "data-reserve-footer": reserveFooter || void 0, children: [
16535
16682
  __twcStyles0,
16536
16683
  __twcStyles1,
16537
16684
  label ? /* @__PURE__ */ jsxRuntime.jsxs("label", { className: "twc-field__label", id: `${fieldId}-label`, children: [
@@ -17109,6 +17256,17 @@ function EmptyState({
17109
17256
  }
17110
17257
  var FIELD_CSS7 = `
17111
17258
  .twc-field { display: flex; flex-direction: column; gap: var(--space-1-5); font-family: var(--font-sans); }
17259
+ /* #494: reserve the footer lane. A row of fields aligned with align="flex-end" splits in two the
17260
+ moment ONE of them has a hint, because that field grows a footer and the others do not - and the
17261
+ caption then moves a DIFFERENT control, which nothing in the JSX suggests. align= cannot fix it:
17262
+ flex-start lines up the labels instead, so the controls part company as soon as one label wraps.
17263
+ The lane is held open by generated content rather than a fixed height so it matches the real
17264
+ footer by construction - same font metrics, same flex gap - instead of by a number that drifts.
17265
+ Whitespace-only generated content is not announced, and :has() lets the CSS tell on its own
17266
+ whether a footer is already there, so no component has to report it. */
17267
+ .twc-field[data-reserve-footer]:not(:has(> .twc-field__footer, > .twc-field__hint, > .twc-field__error))::after,
17268
+ [data-reserve-footer] .twc-field:not(:has(> .twc-field__footer, > .twc-field__hint, > .twc-field__error))::after {
17269
+ content: "\\00a0"; font-size: var(--text-xs); user-select: none; }
17112
17270
  .twc-field__label { font-size: var(--text-sm); font-weight: var(--font-semibold); color: var(--color-text); display: flex; gap: 4px; align-items: center; }
17113
17271
  .twc-field__req { color: var(--color-danger); }
17114
17272
  .twc-field__hint { font-size: var(--text-xs); color: var(--color-text-muted); }
@@ -17123,6 +17281,7 @@ function Field({
17123
17281
  size = "md",
17124
17282
  id,
17125
17283
  className = "",
17284
+ reserveFooter = false,
17126
17285
  children,
17127
17286
  ...rest
17128
17287
  }) {
@@ -17135,7 +17294,7 @@ function Field({
17135
17294
  "aria-describedby": [children.props["aria-describedby"], hasMsg ? descId : null].filter(Boolean).join(" ") || void 0,
17136
17295
  "aria-invalid": children.props["aria-invalid"] !== void 0 ? children.props["aria-invalid"] : error ? true : void 0
17137
17296
  }) : children;
17138
- return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: `twc-field ${className}`, "data-size": size, ...rest, children: [
17297
+ return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: `twc-field ${className}`, "data-size": size, ...rest, "data-reserve-footer": reserveFooter || void 0, children: [
17139
17298
  __twcStyles,
17140
17299
  label ? /* @__PURE__ */ jsxRuntime.jsxs("label", { className: "twc-field__label", htmlFor, children: [
17141
17300
  label,
@@ -18191,6 +18350,17 @@ var Radio = /* @__PURE__ */ React55__namespace.default.forwardRef(function Radio
18191
18350
  .twc-field__req { color: var(--color-danger); }
18192
18351
  .twc-radio__desc { font-size: var(--text-xs); color: var(--color-text-muted); }
18193
18352
  .twc-field { display: flex; flex-direction: column; gap: var(--space-1-5); font-family: var(--font-sans); }
18353
+ /* #494: reserve the footer lane. A row of fields aligned with align="flex-end" splits in two the
18354
+ moment ONE of them has a hint, because that field grows a footer and the others do not - and the
18355
+ caption then moves a DIFFERENT control, which nothing in the JSX suggests. align= cannot fix it:
18356
+ flex-start lines up the labels instead, so the controls part company as soon as one label wraps.
18357
+ The lane is held open by generated content rather than a fixed height so it matches the real
18358
+ footer by construction - same font metrics, same flex gap - instead of by a number that drifts.
18359
+ Whitespace-only generated content is not announced, and :has() lets the CSS tell on its own
18360
+ whether a footer is already there, so no component has to report it. */
18361
+ .twc-field[data-reserve-footer]:not(:has(> .twc-field__footer, > .twc-field__hint, > .twc-field__error))::after,
18362
+ [data-reserve-footer] .twc-field:not(:has(> .twc-field__footer, > .twc-field__hint, > .twc-field__error))::after {
18363
+ content: "\\00a0"; font-size: var(--text-xs); user-select: none; }
18194
18364
  .twc-field__error { font-size: var(--text-xs); color: var(--color-danger-subtle-fg); font-weight: var(--font-medium); }
18195
18365
  `);
18196
18366
  const autoId = React55__namespace.default.useId();
@@ -18240,6 +18410,17 @@ var Radio = /* @__PURE__ */ React55__namespace.default.forwardRef(function Radio
18240
18410
  });
18241
18411
  var RADIO_GROUP_CSS = `
18242
18412
  .twc-field { display: flex; flex-direction: column; gap: var(--space-1-5); font-family: var(--font-sans); }
18413
+ /* #494: reserve the footer lane. A row of fields aligned with align="flex-end" splits in two the
18414
+ moment ONE of them has a hint, because that field grows a footer and the others do not - and the
18415
+ caption then moves a DIFFERENT control, which nothing in the JSX suggests. align= cannot fix it:
18416
+ flex-start lines up the labels instead, so the controls part company as soon as one label wraps.
18417
+ The lane is held open by generated content rather than a fixed height so it matches the real
18418
+ footer by construction - same font metrics, same flex gap - instead of by a number that drifts.
18419
+ Whitespace-only generated content is not announced, and :has() lets the CSS tell on its own
18420
+ whether a footer is already there, so no component has to report it. */
18421
+ .twc-field[data-reserve-footer]:not(:has(> .twc-field__footer, > .twc-field__hint, > .twc-field__error))::after,
18422
+ [data-reserve-footer] .twc-field:not(:has(> .twc-field__footer, > .twc-field__hint, > .twc-field__error))::after {
18423
+ content: "\\00a0"; font-size: var(--text-xs); user-select: none; }
18243
18424
  .twc-field__label { font-size: var(--text-sm); font-weight: var(--font-semibold); color: var(--color-text); display: flex; gap: 4px; align-items: center; }
18244
18425
  .twc-field__req { color: var(--color-danger); }
18245
18426
  .twc-field__hint { font-size: var(--text-xs); color: var(--color-text-muted); }
@@ -18264,6 +18445,7 @@ function RadioGroup({
18264
18445
  tone = "primary",
18265
18446
  id,
18266
18447
  className = "",
18448
+ reserveFooter = false,
18267
18449
  ...rest
18268
18450
  }) {
18269
18451
  const __twcStyles = useScopedStyles("twc-radio-group-styles", RADIO_GROUP_CSS);
@@ -18311,6 +18493,7 @@ function RadioGroup({
18311
18493
  "div",
18312
18494
  {
18313
18495
  className: `twc-field ${className}`,
18496
+ "data-reserve-footer": reserveFooter || void 0,
18314
18497
  role: "radiogroup",
18315
18498
  "aria-labelledby": label ? labelId : void 0,
18316
18499
  "aria-describedby": error || hint ? descId : void 0,
@@ -19335,6 +19518,17 @@ var Switch = /* @__PURE__ */ React55__namespace.default.forwardRef(function Swit
19335
19518
  .twc-field__req { color: var(--color-danger); }
19336
19519
  .twc-switch__desc { font-size: var(--text-xs); color: var(--color-text-muted); }
19337
19520
  .twc-field { display: flex; flex-direction: column; gap: var(--space-1-5); font-family: var(--font-sans); }
19521
+ /* #494: reserve the footer lane. A row of fields aligned with align="flex-end" splits in two the
19522
+ moment ONE of them has a hint, because that field grows a footer and the others do not - and the
19523
+ caption then moves a DIFFERENT control, which nothing in the JSX suggests. align= cannot fix it:
19524
+ flex-start lines up the labels instead, so the controls part company as soon as one label wraps.
19525
+ The lane is held open by generated content rather than a fixed height so it matches the real
19526
+ footer by construction - same font metrics, same flex gap - instead of by a number that drifts.
19527
+ Whitespace-only generated content is not announced, and :has() lets the CSS tell on its own
19528
+ whether a footer is already there, so no component has to report it. */
19529
+ .twc-field[data-reserve-footer]:not(:has(> .twc-field__footer, > .twc-field__hint, > .twc-field__error))::after,
19530
+ [data-reserve-footer] .twc-field:not(:has(> .twc-field__footer, > .twc-field__hint, > .twc-field__error))::after {
19531
+ content: "\\00a0"; font-size: var(--text-xs); user-select: none; }
19338
19532
  .twc-field__error { font-size: var(--text-xs); color: var(--color-danger-subtle-fg); font-weight: var(--font-medium); }
19339
19533
  `);
19340
19534
  const autoId = React55__namespace.default.useId();
@@ -19774,6 +19968,17 @@ function Tag({
19774
19968
  var useIso = typeof document !== "undefined" ? React55__namespace.default.useLayoutEffect : React55__namespace.default.useEffect;
19775
19969
  var TEXTAREA_CSS = `
19776
19970
  .twc-field { display: flex; flex-direction: column; gap: var(--space-1-5); font-family: var(--font-sans); }
19971
+ /* #494: reserve the footer lane. A row of fields aligned with align="flex-end" splits in two the
19972
+ moment ONE of them has a hint, because that field grows a footer and the others do not - and the
19973
+ caption then moves a DIFFERENT control, which nothing in the JSX suggests. align= cannot fix it:
19974
+ flex-start lines up the labels instead, so the controls part company as soon as one label wraps.
19975
+ The lane is held open by generated content rather than a fixed height so it matches the real
19976
+ footer by construction - same font metrics, same flex gap - instead of by a number that drifts.
19977
+ Whitespace-only generated content is not announced, and :has() lets the CSS tell on its own
19978
+ whether a footer is already there, so no component has to report it. */
19979
+ .twc-field[data-reserve-footer]:not(:has(> .twc-field__footer, > .twc-field__hint, > .twc-field__error))::after,
19980
+ [data-reserve-footer] .twc-field:not(:has(> .twc-field__footer, > .twc-field__hint, > .twc-field__error))::after {
19981
+ content: "\\00a0"; font-size: var(--text-xs); user-select: none; }
19777
19982
  .twc-field__label { font-size: var(--text-sm); font-weight: var(--font-semibold); color: var(--color-text); display: flex; gap: 4px; align-items: center; }
19778
19983
  .twc-field__req { color: var(--color-danger); }
19779
19984
  .twc-field__hint { font-size: var(--text-xs); color: var(--color-text-muted); }
@@ -19836,6 +20041,7 @@ var Textarea = /* @__PURE__ */ React55__namespace.default.forwardRef(function Te
19836
20041
  disabled = false,
19837
20042
  id,
19838
20043
  className = "",
20044
+ reserveFooter = false,
19839
20045
  ...rest
19840
20046
  }, ref) {
19841
20047
  const __twcStyles = useScopedStyles("twc-textarea-styles", TEXTAREA_CSS);
@@ -19893,7 +20099,7 @@ var Textarea = /* @__PURE__ */ React55__namespace.default.forwardRef(function Te
19893
20099
  const showClear = clearable && count > 0 && !disabled && !rest.readOnly;
19894
20100
  const ariaInvalid = invalid ? true : rest["aria-invalid"] ?? void 0;
19895
20101
  const describedBy = [error || hint ? descId : null, showCounter ? countId : null, rest["aria-describedby"]].filter(Boolean).join(" ") || void 0;
19896
- return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: `twc-field ${className}`, children: [
20102
+ return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: `twc-field ${className}`, "data-reserve-footer": reserveFooter || void 0, children: [
19897
20103
  __twcStyles,
19898
20104
  label ? /* @__PURE__ */ jsxRuntime.jsxs("label", { className: "twc-field__label", htmlFor: fieldId, children: [
19899
20105
  label,