twico-ui 1.46.0 → 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,13 @@
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
+
5
12
  # [1.46.0](https://github.com/EncryptionL/twico-ui/compare/v1.45.1...v1.46.0) (2026-10-21)
6
13
 
7
14
 
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: [
@@ -14850,6 +14934,17 @@ function DiffTable({
14850
14934
  }
14851
14935
  var FIELD_CSS3 = `
14852
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; }
14853
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; }
14854
14949
  .twc-field__req { color: var(--color-danger); }
14855
14950
  .twc-field__hint { font-size: var(--text-xs); color: var(--color-text-muted); }
@@ -14982,6 +15077,7 @@ function DatePicker({
14982
15077
  onChange,
14983
15078
  id,
14984
15079
  className = "",
15080
+ reserveFooter = false,
14985
15081
  ...rest
14986
15082
  }) {
14987
15083
  const __twcStyles0 = useScopedStyles("twc-field-styles", FIELD_CSS3);
@@ -15184,7 +15280,7 @@ function DatePicker({
15184
15280
  setFocusDate(next);
15185
15281
  if (next.getFullYear() !== y || next.getMonth() !== m) setView(new Date(next.getFullYear(), next.getMonth(), 1));
15186
15282
  };
15187
- 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: [
15188
15284
  __twcStyles0,
15189
15285
  __twcStyles1,
15190
15286
  label ? /* @__PURE__ */ jsxRuntime.jsxs("label", { className: "twc-field__label", id: `${fieldId}-label`, children: [
@@ -15408,6 +15504,17 @@ function DatePicker({
15408
15504
  }
15409
15505
  var FIELD_CSS4 = `
15410
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; }
15411
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; }
15412
15519
  .twc-field__req { color: var(--color-danger); }
15413
15520
  .twc-field__hint { font-size: var(--text-xs); color: var(--color-text-muted); }
@@ -15543,6 +15650,7 @@ function DateRangePicker({
15543
15650
  parse,
15544
15651
  onChange,
15545
15652
  className = "",
15653
+ reserveFooter = false,
15546
15654
  ...rest
15547
15655
  }) {
15548
15656
  const __twcStyles0 = useScopedStyles("twc-field-styles", FIELD_CSS4);
@@ -15787,7 +15895,7 @@ function DateRangePicker({
15787
15895
  return t > Math.min(s, e) && t < Math.max(s, e);
15788
15896
  };
15789
15897
  const text = range2.start ? format ? format(range2) : `${fmtD(range2.start, locale)} \u2013 ${range2.end ? fmtD(range2.end, locale) : "\u2026"}` : "";
15790
- 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: [
15791
15899
  __twcStyles0,
15792
15900
  __twcStyles1,
15793
15901
  label ? /* @__PURE__ */ jsxRuntime.jsxs("span", { className: "twc-field__label", id: labelId, children: [
@@ -16015,6 +16123,17 @@ function DateRangePicker({
16015
16123
  }
16016
16124
  var FIELD_CSS5 = `
16017
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; }
16018
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; }
16019
16138
  .twc-field__req { color: var(--color-danger); }
16020
16139
  .twc-field__hint { font-size: var(--text-xs); color: var(--color-text-muted); }
@@ -16113,6 +16232,7 @@ function TimePicker({
16113
16232
  onChange,
16114
16233
  id,
16115
16234
  className = "",
16235
+ reserveFooter = false,
16116
16236
  ...rest
16117
16237
  }) {
16118
16238
  const __twcStyles0 = useScopedStyles("twc-field-styles", FIELD_CSS5);
@@ -16327,7 +16447,7 @@ function TimePicker({
16327
16447
  /* @__PURE__ */ jsxRuntime.jsx("circle", { cx: "12", cy: "12", r: "9" }),
16328
16448
  /* @__PURE__ */ jsxRuntime.jsx("path", { d: "M12 7v5l3 2" })
16329
16449
  ] });
16330
- 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: [
16331
16451
  __twcStyles0,
16332
16452
  __twcStyles1,
16333
16453
  label ? /* @__PURE__ */ jsxRuntime.jsxs("label", { className: "twc-field__label", id: `${fieldId}-label`, children: [
@@ -16484,6 +16604,17 @@ function TimePicker({
16484
16604
  }
16485
16605
  var FIELD_CSS6 = `
16486
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; }
16487
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; }
16488
16619
  .twc-field__req { color: var(--color-danger); }
16489
16620
  .twc-field__hint { font-size: var(--text-xs); color: var(--color-text-muted); }
@@ -16522,6 +16653,7 @@ function DateTimePicker({
16522
16653
  onChange,
16523
16654
  id,
16524
16655
  className = "",
16656
+ reserveFooter = false,
16525
16657
  ...rest
16526
16658
  }) {
16527
16659
  const __twcStyles0 = useScopedStyles("twc-field-styles", FIELD_CSS6);
@@ -16546,7 +16678,7 @@ function DateTimePicker({
16546
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));
16547
16679
  };
16548
16680
  const childTone = error ? "danger" : tone;
16549
- 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: [
16550
16682
  __twcStyles0,
16551
16683
  __twcStyles1,
16552
16684
  label ? /* @__PURE__ */ jsxRuntime.jsxs("label", { className: "twc-field__label", id: `${fieldId}-label`, children: [
@@ -17124,6 +17256,17 @@ function EmptyState({
17124
17256
  }
17125
17257
  var FIELD_CSS7 = `
17126
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; }
17127
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; }
17128
17271
  .twc-field__req { color: var(--color-danger); }
17129
17272
  .twc-field__hint { font-size: var(--text-xs); color: var(--color-text-muted); }
@@ -17138,6 +17281,7 @@ function Field({
17138
17281
  size = "md",
17139
17282
  id,
17140
17283
  className = "",
17284
+ reserveFooter = false,
17141
17285
  children,
17142
17286
  ...rest
17143
17287
  }) {
@@ -17150,7 +17294,7 @@ function Field({
17150
17294
  "aria-describedby": [children.props["aria-describedby"], hasMsg ? descId : null].filter(Boolean).join(" ") || void 0,
17151
17295
  "aria-invalid": children.props["aria-invalid"] !== void 0 ? children.props["aria-invalid"] : error ? true : void 0
17152
17296
  }) : children;
17153
- 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: [
17154
17298
  __twcStyles,
17155
17299
  label ? /* @__PURE__ */ jsxRuntime.jsxs("label", { className: "twc-field__label", htmlFor, children: [
17156
17300
  label,
@@ -18206,6 +18350,17 @@ var Radio = /* @__PURE__ */ React55__namespace.default.forwardRef(function Radio
18206
18350
  .twc-field__req { color: var(--color-danger); }
18207
18351
  .twc-radio__desc { font-size: var(--text-xs); color: var(--color-text-muted); }
18208
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; }
18209
18364
  .twc-field__error { font-size: var(--text-xs); color: var(--color-danger-subtle-fg); font-weight: var(--font-medium); }
18210
18365
  `);
18211
18366
  const autoId = React55__namespace.default.useId();
@@ -18255,6 +18410,17 @@ var Radio = /* @__PURE__ */ React55__namespace.default.forwardRef(function Radio
18255
18410
  });
18256
18411
  var RADIO_GROUP_CSS = `
18257
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; }
18258
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; }
18259
18425
  .twc-field__req { color: var(--color-danger); }
18260
18426
  .twc-field__hint { font-size: var(--text-xs); color: var(--color-text-muted); }
@@ -18279,6 +18445,7 @@ function RadioGroup({
18279
18445
  tone = "primary",
18280
18446
  id,
18281
18447
  className = "",
18448
+ reserveFooter = false,
18282
18449
  ...rest
18283
18450
  }) {
18284
18451
  const __twcStyles = useScopedStyles("twc-radio-group-styles", RADIO_GROUP_CSS);
@@ -18326,6 +18493,7 @@ function RadioGroup({
18326
18493
  "div",
18327
18494
  {
18328
18495
  className: `twc-field ${className}`,
18496
+ "data-reserve-footer": reserveFooter || void 0,
18329
18497
  role: "radiogroup",
18330
18498
  "aria-labelledby": label ? labelId : void 0,
18331
18499
  "aria-describedby": error || hint ? descId : void 0,
@@ -19350,6 +19518,17 @@ var Switch = /* @__PURE__ */ React55__namespace.default.forwardRef(function Swit
19350
19518
  .twc-field__req { color: var(--color-danger); }
19351
19519
  .twc-switch__desc { font-size: var(--text-xs); color: var(--color-text-muted); }
19352
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; }
19353
19532
  .twc-field__error { font-size: var(--text-xs); color: var(--color-danger-subtle-fg); font-weight: var(--font-medium); }
19354
19533
  `);
19355
19534
  const autoId = React55__namespace.default.useId();
@@ -19789,6 +19968,17 @@ function Tag({
19789
19968
  var useIso = typeof document !== "undefined" ? React55__namespace.default.useLayoutEffect : React55__namespace.default.useEffect;
19790
19969
  var TEXTAREA_CSS = `
19791
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; }
19792
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; }
19793
19983
  .twc-field__req { color: var(--color-danger); }
19794
19984
  .twc-field__hint { font-size: var(--text-xs); color: var(--color-text-muted); }
@@ -19851,6 +20041,7 @@ var Textarea = /* @__PURE__ */ React55__namespace.default.forwardRef(function Te
19851
20041
  disabled = false,
19852
20042
  id,
19853
20043
  className = "",
20044
+ reserveFooter = false,
19854
20045
  ...rest
19855
20046
  }, ref) {
19856
20047
  const __twcStyles = useScopedStyles("twc-textarea-styles", TEXTAREA_CSS);
@@ -19908,7 +20099,7 @@ var Textarea = /* @__PURE__ */ React55__namespace.default.forwardRef(function Te
19908
20099
  const showClear = clearable && count > 0 && !disabled && !rest.readOnly;
19909
20100
  const ariaInvalid = invalid ? true : rest["aria-invalid"] ?? void 0;
19910
20101
  const describedBy = [error || hint ? descId : null, showCounter ? countId : null, rest["aria-describedby"]].filter(Boolean).join(" ") || void 0;
19911
- 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: [
19912
20103
  __twcStyles,
19913
20104
  label ? /* @__PURE__ */ jsxRuntime.jsxs("label", { className: "twc-field__label", htmlFor: fieldId, children: [
19914
20105
  label,