twico-ui 1.46.0 → 1.48.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.48.0](https://github.com/EncryptionL/twico-ui/compare/v1.47.0...v1.48.0) (2026-10-23)
6
+
7
+
8
+ ### Features
9
+
10
+ * **datatable:** vertical alignment at three levels — table, row, column ([221978b](https://github.com/EncryptionL/twico-ui/commit/221978b1db3527d97b93335a9b36b9274d2c8b10))
11
+
12
+ # [1.47.0](https://github.com/EncryptionL/twico-ui/compare/v1.46.0...v1.47.0) (2026-10-23)
13
+
14
+
15
+ ### Features
16
+
17
+ * **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)
18
+
5
19
  # [1.46.0](https://github.com/EncryptionL/twico-ui/compare/v1.45.1...v1.46.0) (2026-10-21)
6
20
 
7
21
 
package/dist/icons.d.cts CHANGED
@@ -56,4 +56,5 @@ declare const brandIcons: BrandIconMeta[];
56
56
  /** Just the exported component names of every brand icon. */
57
57
  declare const brandIconNames: string[];
58
58
 
59
- export { AppleBrandIcon, BlueskyIcon, type BrandIcon, type BrandIconMeta, type BrandIconProps, DiscordIcon, DockerIcon, DribbbleIcon, FacebookIcon, FigmaIcon, GithubIcon, GitlabIcon, GoogleIcon, InstagramIcon, MastodonIcon, MediumIcon, NextjsIcon, NodejsIcon, NpmIcon, PinterestIcon, ReactjsIcon, RedditIcon, SpotifyIcon, StackOverflowIcon, TailwindIcon, TelegramIcon, ThreadsIcon, TikTokIcon, TwitchIcon, TypeScriptIcon, VercelIcon, WhatsAppIcon, XTwitterIcon, YoutubeIcon, brandIconNames, brandIcons };
59
+ export { AppleBrandIcon, BlueskyIcon, DiscordIcon, DockerIcon, DribbbleIcon, FacebookIcon, FigmaIcon, GithubIcon, GitlabIcon, GoogleIcon, InstagramIcon, MastodonIcon, MediumIcon, NextjsIcon, NodejsIcon, NpmIcon, PinterestIcon, ReactjsIcon, RedditIcon, SpotifyIcon, StackOverflowIcon, TailwindIcon, TelegramIcon, ThreadsIcon, TikTokIcon, TwitchIcon, TypeScriptIcon, VercelIcon, WhatsAppIcon, XTwitterIcon, YoutubeIcon, brandIconNames, brandIcons };
60
+ export type { BrandIcon, BrandIconMeta, BrandIconProps };
package/dist/icons.d.ts CHANGED
@@ -56,4 +56,5 @@ declare const brandIcons: BrandIconMeta[];
56
56
  /** Just the exported component names of every brand icon. */
57
57
  declare const brandIconNames: string[];
58
58
 
59
- export { AppleBrandIcon, BlueskyIcon, type BrandIcon, type BrandIconMeta, type BrandIconProps, DiscordIcon, DockerIcon, DribbbleIcon, FacebookIcon, FigmaIcon, GithubIcon, GitlabIcon, GoogleIcon, InstagramIcon, MastodonIcon, MediumIcon, NextjsIcon, NodejsIcon, NpmIcon, PinterestIcon, ReactjsIcon, RedditIcon, SpotifyIcon, StackOverflowIcon, TailwindIcon, TelegramIcon, ThreadsIcon, TikTokIcon, TwitchIcon, TypeScriptIcon, VercelIcon, WhatsAppIcon, XTwitterIcon, YoutubeIcon, brandIconNames, brandIcons };
59
+ export { AppleBrandIcon, BlueskyIcon, DiscordIcon, DockerIcon, DribbbleIcon, FacebookIcon, FigmaIcon, GithubIcon, GitlabIcon, GoogleIcon, InstagramIcon, MastodonIcon, MediumIcon, NextjsIcon, NodejsIcon, NpmIcon, PinterestIcon, ReactjsIcon, RedditIcon, SpotifyIcon, StackOverflowIcon, TailwindIcon, TelegramIcon, ThreadsIcon, TikTokIcon, TwitchIcon, TypeScriptIcon, VercelIcon, WhatsAppIcon, XTwitterIcon, YoutubeIcon, brandIconNames, brandIcons };
60
+ export type { BrandIcon, BrandIconMeta, BrandIconProps };
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: [
@@ -9294,9 +9378,20 @@ var DT_CSS = `
9294
9378
  font-size: var(--text-sm); color: var(--color-text); background: var(--twc-dt-row-bg, var(--color-surface));
9295
9379
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis; max-width: 0; }
9296
9380
  /* Wrap-text columns: content flows onto multiple lines (the row grows down) instead of
9297
- clipping to one line. height stays a per-row minimum, so single-line cells are unchanged. */
9381
+ clipping to one line. height stays a per-row minimum, so single-line cells are unchanged.
9382
+ The top alignment here is only a DEFAULT for wrapped cells - an explicit verticalAlign at any
9383
+ level resolves to data-valign below, which is more specific and wins. Before that existed, a row
9384
+ mixing a wrapped column with normal ones was silently inconsistent: the wrapped cell sat top and
9385
+ its neighbours sat middle, with no way to say otherwise. */
9298
9386
  .twc-dt__td[data-wrap="true"] { white-space: normal; overflow: hidden; text-overflow: clip;
9299
9387
  word-break: break-word; overflow-wrap: anywhere; vertical-align: top; padding-block: 10px; line-height: 1.45; }
9388
+ /* Vertical alignment. One attribute, already resolved: column > row > table default, computed in
9389
+ renderLeaf rather than left to three competing selectors, so the precedence is readable in the
9390
+ code and cannot be reordered by an unrelated CSS edit. Matters whenever rows vary in height -
9391
+ wrapText, a multi-line renderCell, or a drag-resized row. */
9392
+ .twc-dt__td[data-valign="top"] { vertical-align: top; }
9393
+ .twc-dt__td[data-valign="middle"] { vertical-align: middle; }
9394
+ .twc-dt__td[data-valign="bottom"] { vertical-align: bottom; }
9300
9395
  /* #338: "combined" column cell \u2014 one column showing several source columns' values. */
9301
9396
  .twc-dt__combine--stack { display: flex; flex-direction: column; gap: 1px; }
9302
9397
  .twc-dt__combine-sep { color: var(--color-text-subtle); padding-inline: 3px; }
@@ -10065,6 +10160,8 @@ function Datatable({
10065
10160
  batchActions = [],
10066
10161
  onRowSelectionChange,
10067
10162
  selectionSummary,
10163
+ verticalAlign,
10164
+ getRowVerticalAlign,
10068
10165
  getRowStyle,
10069
10166
  getRowClassName,
10070
10167
  showExport = false,
@@ -12644,6 +12741,7 @@ function Datatable({
12644
12741
  const h = rowHeights[k];
12645
12742
  const userRowStyle = getRowStyle ? getRowStyle(row, ri) : void 0;
12646
12743
  const userRowClass = getRowClassName ? getRowClassName(row, ri) : void 0;
12744
+ const rowValign = getRowVerticalAlign ? getRowVerticalAlign(row, ri) : void 0;
12647
12745
  const { background: _rowBgShorthand, backgroundColor: _rowBgColor, ...restRowStyle } = userRowStyle || {};
12648
12746
  const usable = (v) => typeof v === "string" && v.trim() !== "" ? v : void 0;
12649
12747
  const rowBg = usable(_rowBgShorthand) ?? usable(_rowBgColor);
@@ -12754,6 +12852,7 @@ function Datatable({
12754
12852
  "data-num": c.type === "number" || void 0,
12755
12853
  "data-actions": isActions || void 0,
12756
12854
  "data-align": c.align !== dtNaturalAlign(c) ? c.align : void 0,
12855
+ "data-valign": c.verticalAlign ?? rowValign ?? verticalAlign,
12757
12856
  "data-editable": editable && !isEditing || void 0,
12758
12857
  "data-editing": isEditing || void 0,
12759
12858
  "data-cell-active": cellActive || void 0,
@@ -14850,6 +14949,17 @@ function DiffTable({
14850
14949
  }
14851
14950
  var FIELD_CSS3 = `
14852
14951
  .twc-field { display: flex; flex-direction: column; gap: var(--space-1-5); font-family: var(--font-sans); }
14952
+ /* #494: reserve the footer lane. A row of fields aligned with align="flex-end" splits in two the
14953
+ moment ONE of them has a hint, because that field grows a footer and the others do not - and the
14954
+ caption then moves a DIFFERENT control, which nothing in the JSX suggests. align= cannot fix it:
14955
+ flex-start lines up the labels instead, so the controls part company as soon as one label wraps.
14956
+ The lane is held open by generated content rather than a fixed height so it matches the real
14957
+ footer by construction - same font metrics, same flex gap - instead of by a number that drifts.
14958
+ Whitespace-only generated content is not announced, and :has() lets the CSS tell on its own
14959
+ whether a footer is already there, so no component has to report it. */
14960
+ .twc-field[data-reserve-footer]:not(:has(> .twc-field__footer, > .twc-field__hint, > .twc-field__error))::after,
14961
+ [data-reserve-footer] .twc-field:not(:has(> .twc-field__footer, > .twc-field__hint, > .twc-field__error))::after {
14962
+ content: "\\00a0"; font-size: var(--text-xs); user-select: none; }
14853
14963
  .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
14964
  .twc-field__req { color: var(--color-danger); }
14855
14965
  .twc-field__hint { font-size: var(--text-xs); color: var(--color-text-muted); }
@@ -14982,6 +15092,7 @@ function DatePicker({
14982
15092
  onChange,
14983
15093
  id,
14984
15094
  className = "",
15095
+ reserveFooter = false,
14985
15096
  ...rest
14986
15097
  }) {
14987
15098
  const __twcStyles0 = useScopedStyles("twc-field-styles", FIELD_CSS3);
@@ -15184,7 +15295,7 @@ function DatePicker({
15184
15295
  setFocusDate(next);
15185
15296
  if (next.getFullYear() !== y || next.getMonth() !== m) setView(new Date(next.getFullYear(), next.getMonth(), 1));
15186
15297
  };
15187
- return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: `twc-dp twc-field ${className}`, ref: wrapRef, ...rest, children: [
15298
+ return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: `twc-dp twc-field ${className}`, ref: wrapRef, ...rest, "data-reserve-footer": reserveFooter || void 0, children: [
15188
15299
  __twcStyles0,
15189
15300
  __twcStyles1,
15190
15301
  label ? /* @__PURE__ */ jsxRuntime.jsxs("label", { className: "twc-field__label", id: `${fieldId}-label`, children: [
@@ -15408,6 +15519,17 @@ function DatePicker({
15408
15519
  }
15409
15520
  var FIELD_CSS4 = `
15410
15521
  .twc-field { display: flex; flex-direction: column; gap: var(--space-1-5); font-family: var(--font-sans); }
15522
+ /* #494: reserve the footer lane. A row of fields aligned with align="flex-end" splits in two the
15523
+ moment ONE of them has a hint, because that field grows a footer and the others do not - and the
15524
+ caption then moves a DIFFERENT control, which nothing in the JSX suggests. align= cannot fix it:
15525
+ flex-start lines up the labels instead, so the controls part company as soon as one label wraps.
15526
+ The lane is held open by generated content rather than a fixed height so it matches the real
15527
+ footer by construction - same font metrics, same flex gap - instead of by a number that drifts.
15528
+ Whitespace-only generated content is not announced, and :has() lets the CSS tell on its own
15529
+ whether a footer is already there, so no component has to report it. */
15530
+ .twc-field[data-reserve-footer]:not(:has(> .twc-field__footer, > .twc-field__hint, > .twc-field__error))::after,
15531
+ [data-reserve-footer] .twc-field:not(:has(> .twc-field__footer, > .twc-field__hint, > .twc-field__error))::after {
15532
+ content: "\\00a0"; font-size: var(--text-xs); user-select: none; }
15411
15533
  .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
15534
  .twc-field__req { color: var(--color-danger); }
15413
15535
  .twc-field__hint { font-size: var(--text-xs); color: var(--color-text-muted); }
@@ -15543,6 +15665,7 @@ function DateRangePicker({
15543
15665
  parse,
15544
15666
  onChange,
15545
15667
  className = "",
15668
+ reserveFooter = false,
15546
15669
  ...rest
15547
15670
  }) {
15548
15671
  const __twcStyles0 = useScopedStyles("twc-field-styles", FIELD_CSS4);
@@ -15787,7 +15910,7 @@ function DateRangePicker({
15787
15910
  return t > Math.min(s, e) && t < Math.max(s, e);
15788
15911
  };
15789
15912
  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: [
15913
+ return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: `twc-drp twc-field ${className}`, ref: wrapRef, ...rest, "data-reserve-footer": reserveFooter || void 0, children: [
15791
15914
  __twcStyles0,
15792
15915
  __twcStyles1,
15793
15916
  label ? /* @__PURE__ */ jsxRuntime.jsxs("span", { className: "twc-field__label", id: labelId, children: [
@@ -16015,6 +16138,17 @@ function DateRangePicker({
16015
16138
  }
16016
16139
  var FIELD_CSS5 = `
16017
16140
  .twc-field { display: flex; flex-direction: column; gap: var(--space-1-5); font-family: var(--font-sans); }
16141
+ /* #494: reserve the footer lane. A row of fields aligned with align="flex-end" splits in two the
16142
+ moment ONE of them has a hint, because that field grows a footer and the others do not - and the
16143
+ caption then moves a DIFFERENT control, which nothing in the JSX suggests. align= cannot fix it:
16144
+ flex-start lines up the labels instead, so the controls part company as soon as one label wraps.
16145
+ The lane is held open by generated content rather than a fixed height so it matches the real
16146
+ footer by construction - same font metrics, same flex gap - instead of by a number that drifts.
16147
+ Whitespace-only generated content is not announced, and :has() lets the CSS tell on its own
16148
+ whether a footer is already there, so no component has to report it. */
16149
+ .twc-field[data-reserve-footer]:not(:has(> .twc-field__footer, > .twc-field__hint, > .twc-field__error))::after,
16150
+ [data-reserve-footer] .twc-field:not(:has(> .twc-field__footer, > .twc-field__hint, > .twc-field__error))::after {
16151
+ content: "\\00a0"; font-size: var(--text-xs); user-select: none; }
16018
16152
  .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
16153
  .twc-field__req { color: var(--color-danger); }
16020
16154
  .twc-field__hint { font-size: var(--text-xs); color: var(--color-text-muted); }
@@ -16113,6 +16247,7 @@ function TimePicker({
16113
16247
  onChange,
16114
16248
  id,
16115
16249
  className = "",
16250
+ reserveFooter = false,
16116
16251
  ...rest
16117
16252
  }) {
16118
16253
  const __twcStyles0 = useScopedStyles("twc-field-styles", FIELD_CSS5);
@@ -16327,7 +16462,7 @@ function TimePicker({
16327
16462
  /* @__PURE__ */ jsxRuntime.jsx("circle", { cx: "12", cy: "12", r: "9" }),
16328
16463
  /* @__PURE__ */ jsxRuntime.jsx("path", { d: "M12 7v5l3 2" })
16329
16464
  ] });
16330
- return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: `twc-tp twc-field ${className}`, ref: wrapRef, ...rest, children: [
16465
+ return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: `twc-tp twc-field ${className}`, ref: wrapRef, ...rest, "data-reserve-footer": reserveFooter || void 0, children: [
16331
16466
  __twcStyles0,
16332
16467
  __twcStyles1,
16333
16468
  label ? /* @__PURE__ */ jsxRuntime.jsxs("label", { className: "twc-field__label", id: `${fieldId}-label`, children: [
@@ -16484,6 +16619,17 @@ function TimePicker({
16484
16619
  }
16485
16620
  var FIELD_CSS6 = `
16486
16621
  .twc-field { display: flex; flex-direction: column; gap: var(--space-1-5); font-family: var(--font-sans); }
16622
+ /* #494: reserve the footer lane. A row of fields aligned with align="flex-end" splits in two the
16623
+ moment ONE of them has a hint, because that field grows a footer and the others do not - and the
16624
+ caption then moves a DIFFERENT control, which nothing in the JSX suggests. align= cannot fix it:
16625
+ flex-start lines up the labels instead, so the controls part company as soon as one label wraps.
16626
+ The lane is held open by generated content rather than a fixed height so it matches the real
16627
+ footer by construction - same font metrics, same flex gap - instead of by a number that drifts.
16628
+ Whitespace-only generated content is not announced, and :has() lets the CSS tell on its own
16629
+ whether a footer is already there, so no component has to report it. */
16630
+ .twc-field[data-reserve-footer]:not(:has(> .twc-field__footer, > .twc-field__hint, > .twc-field__error))::after,
16631
+ [data-reserve-footer] .twc-field:not(:has(> .twc-field__footer, > .twc-field__hint, > .twc-field__error))::after {
16632
+ content: "\\00a0"; font-size: var(--text-xs); user-select: none; }
16487
16633
  .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
16634
  .twc-field__req { color: var(--color-danger); }
16489
16635
  .twc-field__hint { font-size: var(--text-xs); color: var(--color-text-muted); }
@@ -16522,6 +16668,7 @@ function DateTimePicker({
16522
16668
  onChange,
16523
16669
  id,
16524
16670
  className = "",
16671
+ reserveFooter = false,
16525
16672
  ...rest
16526
16673
  }) {
16527
16674
  const __twcStyles0 = useScopedStyles("twc-field-styles", FIELD_CSS6);
@@ -16546,7 +16693,7 @@ function DateTimePicker({
16546
16693
  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
16694
  };
16548
16695
  const childTone = error ? "danger" : tone;
16549
- return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: `twc-dtp twc-field ${className}`, id: fieldId, ...rest, children: [
16696
+ return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: `twc-dtp twc-field ${className}`, id: fieldId, ...rest, "data-reserve-footer": reserveFooter || void 0, children: [
16550
16697
  __twcStyles0,
16551
16698
  __twcStyles1,
16552
16699
  label ? /* @__PURE__ */ jsxRuntime.jsxs("label", { className: "twc-field__label", id: `${fieldId}-label`, children: [
@@ -17124,6 +17271,17 @@ function EmptyState({
17124
17271
  }
17125
17272
  var FIELD_CSS7 = `
17126
17273
  .twc-field { display: flex; flex-direction: column; gap: var(--space-1-5); font-family: var(--font-sans); }
17274
+ /* #494: reserve the footer lane. A row of fields aligned with align="flex-end" splits in two the
17275
+ moment ONE of them has a hint, because that field grows a footer and the others do not - and the
17276
+ caption then moves a DIFFERENT control, which nothing in the JSX suggests. align= cannot fix it:
17277
+ flex-start lines up the labels instead, so the controls part company as soon as one label wraps.
17278
+ The lane is held open by generated content rather than a fixed height so it matches the real
17279
+ footer by construction - same font metrics, same flex gap - instead of by a number that drifts.
17280
+ Whitespace-only generated content is not announced, and :has() lets the CSS tell on its own
17281
+ whether a footer is already there, so no component has to report it. */
17282
+ .twc-field[data-reserve-footer]:not(:has(> .twc-field__footer, > .twc-field__hint, > .twc-field__error))::after,
17283
+ [data-reserve-footer] .twc-field:not(:has(> .twc-field__footer, > .twc-field__hint, > .twc-field__error))::after {
17284
+ content: "\\00a0"; font-size: var(--text-xs); user-select: none; }
17127
17285
  .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
17286
  .twc-field__req { color: var(--color-danger); }
17129
17287
  .twc-field__hint { font-size: var(--text-xs); color: var(--color-text-muted); }
@@ -17138,6 +17296,7 @@ function Field({
17138
17296
  size = "md",
17139
17297
  id,
17140
17298
  className = "",
17299
+ reserveFooter = false,
17141
17300
  children,
17142
17301
  ...rest
17143
17302
  }) {
@@ -17150,7 +17309,7 @@ function Field({
17150
17309
  "aria-describedby": [children.props["aria-describedby"], hasMsg ? descId : null].filter(Boolean).join(" ") || void 0,
17151
17310
  "aria-invalid": children.props["aria-invalid"] !== void 0 ? children.props["aria-invalid"] : error ? true : void 0
17152
17311
  }) : children;
17153
- return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: `twc-field ${className}`, "data-size": size, ...rest, children: [
17312
+ return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: `twc-field ${className}`, "data-size": size, ...rest, "data-reserve-footer": reserveFooter || void 0, children: [
17154
17313
  __twcStyles,
17155
17314
  label ? /* @__PURE__ */ jsxRuntime.jsxs("label", { className: "twc-field__label", htmlFor, children: [
17156
17315
  label,
@@ -18206,6 +18365,17 @@ var Radio = /* @__PURE__ */ React55__namespace.default.forwardRef(function Radio
18206
18365
  .twc-field__req { color: var(--color-danger); }
18207
18366
  .twc-radio__desc { font-size: var(--text-xs); color: var(--color-text-muted); }
18208
18367
  .twc-field { display: flex; flex-direction: column; gap: var(--space-1-5); font-family: var(--font-sans); }
18368
+ /* #494: reserve the footer lane. A row of fields aligned with align="flex-end" splits in two the
18369
+ moment ONE of them has a hint, because that field grows a footer and the others do not - and the
18370
+ caption then moves a DIFFERENT control, which nothing in the JSX suggests. align= cannot fix it:
18371
+ flex-start lines up the labels instead, so the controls part company as soon as one label wraps.
18372
+ The lane is held open by generated content rather than a fixed height so it matches the real
18373
+ footer by construction - same font metrics, same flex gap - instead of by a number that drifts.
18374
+ Whitespace-only generated content is not announced, and :has() lets the CSS tell on its own
18375
+ whether a footer is already there, so no component has to report it. */
18376
+ .twc-field[data-reserve-footer]:not(:has(> .twc-field__footer, > .twc-field__hint, > .twc-field__error))::after,
18377
+ [data-reserve-footer] .twc-field:not(:has(> .twc-field__footer, > .twc-field__hint, > .twc-field__error))::after {
18378
+ content: "\\00a0"; font-size: var(--text-xs); user-select: none; }
18209
18379
  .twc-field__error { font-size: var(--text-xs); color: var(--color-danger-subtle-fg); font-weight: var(--font-medium); }
18210
18380
  `);
18211
18381
  const autoId = React55__namespace.default.useId();
@@ -18255,6 +18425,17 @@ var Radio = /* @__PURE__ */ React55__namespace.default.forwardRef(function Radio
18255
18425
  });
18256
18426
  var RADIO_GROUP_CSS = `
18257
18427
  .twc-field { display: flex; flex-direction: column; gap: var(--space-1-5); font-family: var(--font-sans); }
18428
+ /* #494: reserve the footer lane. A row of fields aligned with align="flex-end" splits in two the
18429
+ moment ONE of them has a hint, because that field grows a footer and the others do not - and the
18430
+ caption then moves a DIFFERENT control, which nothing in the JSX suggests. align= cannot fix it:
18431
+ flex-start lines up the labels instead, so the controls part company as soon as one label wraps.
18432
+ The lane is held open by generated content rather than a fixed height so it matches the real
18433
+ footer by construction - same font metrics, same flex gap - instead of by a number that drifts.
18434
+ Whitespace-only generated content is not announced, and :has() lets the CSS tell on its own
18435
+ whether a footer is already there, so no component has to report it. */
18436
+ .twc-field[data-reserve-footer]:not(:has(> .twc-field__footer, > .twc-field__hint, > .twc-field__error))::after,
18437
+ [data-reserve-footer] .twc-field:not(:has(> .twc-field__footer, > .twc-field__hint, > .twc-field__error))::after {
18438
+ content: "\\00a0"; font-size: var(--text-xs); user-select: none; }
18258
18439
  .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
18440
  .twc-field__req { color: var(--color-danger); }
18260
18441
  .twc-field__hint { font-size: var(--text-xs); color: var(--color-text-muted); }
@@ -18279,6 +18460,7 @@ function RadioGroup({
18279
18460
  tone = "primary",
18280
18461
  id,
18281
18462
  className = "",
18463
+ reserveFooter = false,
18282
18464
  ...rest
18283
18465
  }) {
18284
18466
  const __twcStyles = useScopedStyles("twc-radio-group-styles", RADIO_GROUP_CSS);
@@ -18326,6 +18508,7 @@ function RadioGroup({
18326
18508
  "div",
18327
18509
  {
18328
18510
  className: `twc-field ${className}`,
18511
+ "data-reserve-footer": reserveFooter || void 0,
18329
18512
  role: "radiogroup",
18330
18513
  "aria-labelledby": label ? labelId : void 0,
18331
18514
  "aria-describedby": error || hint ? descId : void 0,
@@ -19350,6 +19533,17 @@ var Switch = /* @__PURE__ */ React55__namespace.default.forwardRef(function Swit
19350
19533
  .twc-field__req { color: var(--color-danger); }
19351
19534
  .twc-switch__desc { font-size: var(--text-xs); color: var(--color-text-muted); }
19352
19535
  .twc-field { display: flex; flex-direction: column; gap: var(--space-1-5); font-family: var(--font-sans); }
19536
+ /* #494: reserve the footer lane. A row of fields aligned with align="flex-end" splits in two the
19537
+ moment ONE of them has a hint, because that field grows a footer and the others do not - and the
19538
+ caption then moves a DIFFERENT control, which nothing in the JSX suggests. align= cannot fix it:
19539
+ flex-start lines up the labels instead, so the controls part company as soon as one label wraps.
19540
+ The lane is held open by generated content rather than a fixed height so it matches the real
19541
+ footer by construction - same font metrics, same flex gap - instead of by a number that drifts.
19542
+ Whitespace-only generated content is not announced, and :has() lets the CSS tell on its own
19543
+ whether a footer is already there, so no component has to report it. */
19544
+ .twc-field[data-reserve-footer]:not(:has(> .twc-field__footer, > .twc-field__hint, > .twc-field__error))::after,
19545
+ [data-reserve-footer] .twc-field:not(:has(> .twc-field__footer, > .twc-field__hint, > .twc-field__error))::after {
19546
+ content: "\\00a0"; font-size: var(--text-xs); user-select: none; }
19353
19547
  .twc-field__error { font-size: var(--text-xs); color: var(--color-danger-subtle-fg); font-weight: var(--font-medium); }
19354
19548
  `);
19355
19549
  const autoId = React55__namespace.default.useId();
@@ -19789,6 +19983,17 @@ function Tag({
19789
19983
  var useIso = typeof document !== "undefined" ? React55__namespace.default.useLayoutEffect : React55__namespace.default.useEffect;
19790
19984
  var TEXTAREA_CSS = `
19791
19985
  .twc-field { display: flex; flex-direction: column; gap: var(--space-1-5); font-family: var(--font-sans); }
19986
+ /* #494: reserve the footer lane. A row of fields aligned with align="flex-end" splits in two the
19987
+ moment ONE of them has a hint, because that field grows a footer and the others do not - and the
19988
+ caption then moves a DIFFERENT control, which nothing in the JSX suggests. align= cannot fix it:
19989
+ flex-start lines up the labels instead, so the controls part company as soon as one label wraps.
19990
+ The lane is held open by generated content rather than a fixed height so it matches the real
19991
+ footer by construction - same font metrics, same flex gap - instead of by a number that drifts.
19992
+ Whitespace-only generated content is not announced, and :has() lets the CSS tell on its own
19993
+ whether a footer is already there, so no component has to report it. */
19994
+ .twc-field[data-reserve-footer]:not(:has(> .twc-field__footer, > .twc-field__hint, > .twc-field__error))::after,
19995
+ [data-reserve-footer] .twc-field:not(:has(> .twc-field__footer, > .twc-field__hint, > .twc-field__error))::after {
19996
+ content: "\\00a0"; font-size: var(--text-xs); user-select: none; }
19792
19997
  .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
19998
  .twc-field__req { color: var(--color-danger); }
19794
19999
  .twc-field__hint { font-size: var(--text-xs); color: var(--color-text-muted); }
@@ -19851,6 +20056,7 @@ var Textarea = /* @__PURE__ */ React55__namespace.default.forwardRef(function Te
19851
20056
  disabled = false,
19852
20057
  id,
19853
20058
  className = "",
20059
+ reserveFooter = false,
19854
20060
  ...rest
19855
20061
  }, ref) {
19856
20062
  const __twcStyles = useScopedStyles("twc-textarea-styles", TEXTAREA_CSS);
@@ -19908,7 +20114,7 @@ var Textarea = /* @__PURE__ */ React55__namespace.default.forwardRef(function Te
19908
20114
  const showClear = clearable && count > 0 && !disabled && !rest.readOnly;
19909
20115
  const ariaInvalid = invalid ? true : rest["aria-invalid"] ?? void 0;
19910
20116
  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: [
20117
+ return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: `twc-field ${className}`, "data-reserve-footer": reserveFooter || void 0, children: [
19912
20118
  __twcStyles,
19913
20119
  label ? /* @__PURE__ */ jsxRuntime.jsxs("label", { className: "twc-field__label", htmlFor: fieldId, children: [
19914
20120
  label,