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/dist/index.mjs CHANGED
@@ -5880,6 +5880,17 @@ var Checkbox = /* @__PURE__ */ React55__default.forwardRef(function Checkbox2({
5880
5880
  .twc-field__req { color: var(--color-danger); }
5881
5881
  .twc-check__desc { font-size: var(--text-xs); color: var(--color-text-muted); }
5882
5882
  .twc-field { display: flex; flex-direction: column; gap: var(--space-1-5); font-family: var(--font-sans); }
5883
+ /* #494: reserve the footer lane. A row of fields aligned with align="flex-end" splits in two the
5884
+ moment ONE of them has a hint, because that field grows a footer and the others do not - and the
5885
+ caption then moves a DIFFERENT control, which nothing in the JSX suggests. align= cannot fix it:
5886
+ flex-start lines up the labels instead, so the controls part company as soon as one label wraps.
5887
+ The lane is held open by generated content rather than a fixed height so it matches the real
5888
+ footer by construction - same font metrics, same flex gap - instead of by a number that drifts.
5889
+ Whitespace-only generated content is not announced, and :has() lets the CSS tell on its own
5890
+ whether a footer is already there, so no component has to report it. */
5891
+ .twc-field[data-reserve-footer]:not(:has(> .twc-field__footer, > .twc-field__hint, > .twc-field__error))::after,
5892
+ [data-reserve-footer] .twc-field:not(:has(> .twc-field__footer, > .twc-field__hint, > .twc-field__error))::after {
5893
+ content: "\\00a0"; font-size: var(--text-xs); user-select: none; }
5883
5894
  .twc-field__error { font-size: var(--text-xs); color: var(--color-danger-subtle-fg); font-weight: var(--font-medium); }
5884
5895
  `);
5885
5896
  const autoId = React55__default.useId();
@@ -5934,6 +5945,17 @@ var Checkbox = /* @__PURE__ */ React55__default.forwardRef(function Checkbox2({
5934
5945
  });
5935
5946
  var FIELD_CSS = `
5936
5947
  .twc-field { display: flex; flex-direction: column; gap: var(--space-1-5); font-family: var(--font-sans); }
5948
+ /* #494: reserve the footer lane. A row of fields aligned with align="flex-end" splits in two the
5949
+ moment ONE of them has a hint, because that field grows a footer and the others do not - and the
5950
+ caption then moves a DIFFERENT control, which nothing in the JSX suggests. align= cannot fix it:
5951
+ flex-start lines up the labels instead, so the controls part company as soon as one label wraps.
5952
+ The lane is held open by generated content rather than a fixed height so it matches the real
5953
+ footer by construction - same font metrics, same flex gap - instead of by a number that drifts.
5954
+ Whitespace-only generated content is not announced, and :has() lets the CSS tell on its own
5955
+ whether a footer is already there, so no component has to report it. */
5956
+ .twc-field[data-reserve-footer]:not(:has(> .twc-field__footer, > .twc-field__hint, > .twc-field__error))::after,
5957
+ [data-reserve-footer] .twc-field:not(:has(> .twc-field__footer, > .twc-field__hint, > .twc-field__error))::after {
5958
+ content: "\\00a0"; font-size: var(--text-xs); user-select: none; }
5937
5959
  .twc-field__label { font-size: var(--text-sm); font-weight: var(--font-semibold); color: var(--color-text); display: flex; gap: 4px; align-items: center; }
5938
5960
  .twc-field__req { color: var(--color-danger); }
5939
5961
  .twc-field__hint { font-size: var(--text-xs); color: var(--color-text-muted); }
@@ -6045,6 +6067,7 @@ function ColorPicker({
6045
6067
  tone = "primary",
6046
6068
  onChange,
6047
6069
  className = "",
6070
+ reserveFooter = false,
6048
6071
  ...rest
6049
6072
  }) {
6050
6073
  const __twcStyles0 = useScopedStyles("twc-field-styles", FIELD_CSS);
@@ -6184,7 +6207,7 @@ function ColorPicker({
6184
6207
  e.preventDefault();
6185
6208
  commit({ ...hsv, a: Math.round(a * 100) / 100 });
6186
6209
  };
6187
- return /* @__PURE__ */ jsxs("div", { className: `twc-cp twc-field ${className}`, ref: wrapRef, ...rest, children: [
6210
+ return /* @__PURE__ */ jsxs("div", { className: `twc-cp twc-field ${className}`, ref: wrapRef, ...rest, "data-reserve-footer": reserveFooter || void 0, children: [
6188
6211
  __twcStyles0,
6189
6212
  __twcStyles1,
6190
6213
  label ? /* @__PURE__ */ jsxs("span", { className: "twc-field__label", id: labelId, children: [
@@ -6347,6 +6370,17 @@ function ColorPicker({
6347
6370
  }
6348
6371
  var COMBO_CSS = `
6349
6372
  .twc-field { display: flex; flex-direction: column; gap: var(--space-1-5); font-family: var(--font-sans); }
6373
+ /* #494: reserve the footer lane. A row of fields aligned with align="flex-end" splits in two the
6374
+ moment ONE of them has a hint, because that field grows a footer and the others do not - and the
6375
+ caption then moves a DIFFERENT control, which nothing in the JSX suggests. align= cannot fix it:
6376
+ flex-start lines up the labels instead, so the controls part company as soon as one label wraps.
6377
+ The lane is held open by generated content rather than a fixed height so it matches the real
6378
+ footer by construction - same font metrics, same flex gap - instead of by a number that drifts.
6379
+ Whitespace-only generated content is not announced, and :has() lets the CSS tell on its own
6380
+ whether a footer is already there, so no component has to report it. */
6381
+ .twc-field[data-reserve-footer]:not(:has(> .twc-field__footer, > .twc-field__hint, > .twc-field__error))::after,
6382
+ [data-reserve-footer] .twc-field:not(:has(> .twc-field__footer, > .twc-field__hint, > .twc-field__error))::after {
6383
+ content: "\\00a0"; font-size: var(--text-xs); user-select: none; }
6350
6384
  .twc-field__label { font-size: var(--text-sm); font-weight: var(--font-semibold); color: var(--color-text); display: flex; gap: 4px; align-items: center; }
6351
6385
  .twc-field__req { color: var(--color-danger); }
6352
6386
  .twc-field__hint { font-size: var(--text-xs); color: var(--color-text-muted); }
@@ -6485,6 +6519,7 @@ var Combobox = /* @__PURE__ */ React55__default.forwardRef(function Combobox2({
6485
6519
  className = "",
6486
6520
  onFocus,
6487
6521
  onKeyDown,
6522
+ reserveFooter = false,
6488
6523
  ...rest
6489
6524
  }, ref) {
6490
6525
  const __twcStyles = useScopedStyles("twc-combo-styles", COMBO_CSS);
@@ -6772,7 +6807,7 @@ var Combobox = /* @__PURE__ */ React55__default.forwardRef(function Combobox2({
6772
6807
  popEl = /* @__PURE__ */ jsx("div", { className: "twc-pop", ref: popRef, "data-placement": placement === "top" ? "top" : void 0, children: popInner });
6773
6808
  }
6774
6809
  }
6775
- return /* @__PURE__ */ jsxs("div", { className: `twc-field ${className}`, ref: wrapRef, children: [
6810
+ return /* @__PURE__ */ jsxs("div", { className: `twc-field ${className}`, ref: wrapRef, "data-reserve-footer": reserveFooter || void 0, children: [
6776
6811
  __twcStyles,
6777
6812
  name ? /* @__PURE__ */ jsx("input", { type: "hidden", name, value: current ?? "" }) : null,
6778
6813
  label ? /* @__PURE__ */ jsxs("label", { className: "twc-field__label", htmlFor: fieldId, children: [
@@ -7092,6 +7127,17 @@ function clampPrecision(str, precision) {
7092
7127
  }
7093
7128
  var CUR_CSS = `
7094
7129
  .twc-field { display: flex; flex-direction: column; gap: var(--space-1-5); font-family: var(--font-sans); }
7130
+ /* #494: reserve the footer lane. A row of fields aligned with align="flex-end" splits in two the
7131
+ moment ONE of them has a hint, because that field grows a footer and the others do not - and the
7132
+ caption then moves a DIFFERENT control, which nothing in the JSX suggests. align= cannot fix it:
7133
+ flex-start lines up the labels instead, so the controls part company as soon as one label wraps.
7134
+ The lane is held open by generated content rather than a fixed height so it matches the real
7135
+ footer by construction - same font metrics, same flex gap - instead of by a number that drifts.
7136
+ Whitespace-only generated content is not announced, and :has() lets the CSS tell on its own
7137
+ whether a footer is already there, so no component has to report it. */
7138
+ .twc-field[data-reserve-footer]:not(:has(> .twc-field__footer, > .twc-field__hint, > .twc-field__error))::after,
7139
+ [data-reserve-footer] .twc-field:not(:has(> .twc-field__footer, > .twc-field__hint, > .twc-field__error))::after {
7140
+ content: "\\00a0"; font-size: var(--text-xs); user-select: none; }
7095
7141
  .twc-field__label { font-size: var(--text-sm); font-weight: var(--font-semibold); color: var(--color-text); display: flex; gap: 4px; align-items: center; }
7096
7142
  .twc-field__req { color: var(--color-danger); }
7097
7143
  .twc-field__hint { font-size: var(--text-xs); color: var(--color-text-muted); }
@@ -7161,6 +7207,7 @@ function Currency({
7161
7207
  id,
7162
7208
  placeholder = "0.00",
7163
7209
  className = "",
7210
+ reserveFooter = false,
7164
7211
  ...rest
7165
7212
  }) {
7166
7213
  const __twcStyles = useScopedStyles("twc-cur-styles", CUR_CSS);
@@ -7209,7 +7256,7 @@ function Currency({
7209
7256
  if (!controlled) setInternal(fixed);
7210
7257
  onValueChange?.(Number(fixed), fixed);
7211
7258
  }
7212
- return /* @__PURE__ */ jsxs("div", { className: `twc-field ${className}`, children: [
7259
+ return /* @__PURE__ */ jsxs("div", { className: `twc-field ${className}`, "data-reserve-footer": reserveFooter || void 0, children: [
7213
7260
  __twcStyles,
7214
7261
  label ? /* @__PURE__ */ jsxs("label", { className: "twc-field__label", htmlFor: fieldId, children: [
7215
7262
  label,
@@ -7261,6 +7308,17 @@ function useFallbackName(ref, hasOwnName) {
7261
7308
  }
7262
7309
  var SELECT_CSS = `
7263
7310
  .twc-field { display: flex; flex-direction: column; gap: var(--space-1-5); font-family: var(--font-sans); }
7311
+ /* #494: reserve the footer lane. A row of fields aligned with align="flex-end" splits in two the
7312
+ moment ONE of them has a hint, because that field grows a footer and the others do not - and the
7313
+ caption then moves a DIFFERENT control, which nothing in the JSX suggests. align= cannot fix it:
7314
+ flex-start lines up the labels instead, so the controls part company as soon as one label wraps.
7315
+ The lane is held open by generated content rather than a fixed height so it matches the real
7316
+ footer by construction - same font metrics, same flex gap - instead of by a number that drifts.
7317
+ Whitespace-only generated content is not announced, and :has() lets the CSS tell on its own
7318
+ whether a footer is already there, so no component has to report it. */
7319
+ .twc-field[data-reserve-footer]:not(:has(> .twc-field__footer, > .twc-field__hint, > .twc-field__error))::after,
7320
+ [data-reserve-footer] .twc-field:not(:has(> .twc-field__footer, > .twc-field__hint, > .twc-field__error))::after {
7321
+ content: "\\00a0"; font-size: var(--text-xs); user-select: none; }
7264
7322
  .twc-field__label { font-size: var(--text-sm); font-weight: var(--font-semibold); color: var(--color-text); display: flex; gap: 4px; align-items: center; }
7265
7323
  .twc-field__req { color: var(--color-danger); }
7266
7324
  .twc-field__hint { font-size: var(--text-xs); color: var(--color-text-muted); }
@@ -7397,6 +7455,7 @@ var Select = /* @__PURE__ */ React55__default.forwardRef(function Select2({
7397
7455
  renderOption,
7398
7456
  id,
7399
7457
  className = "",
7458
+ reserveFooter = false,
7400
7459
  onClick,
7401
7460
  onKeyDown,
7402
7461
  ...rest
@@ -7742,7 +7801,7 @@ var Select = /* @__PURE__ */ React55__default.forwardRef(function Select2({
7742
7801
  popEl = /* @__PURE__ */ jsx("div", { className: "twc-pop", ref: popRef, "data-state": popState, "data-placement": placement === "top" ? "top" : "bottom", children: popInner });
7743
7802
  }
7744
7803
  }
7745
- return /* @__PURE__ */ jsxs("div", { className: `twc-field ${className}`, ref: wrapRef, children: [
7804
+ return /* @__PURE__ */ jsxs("div", { className: `twc-field ${className}`, ref: wrapRef, "data-reserve-footer": reserveFooter || void 0, children: [
7746
7805
  __twcStyles,
7747
7806
  name ? /* @__PURE__ */ jsx("input", { type: "hidden", name, value: current ?? "" }) : null,
7748
7807
  label ? /* @__PURE__ */ jsxs("label", { className: "twc-field__label", htmlFor: fieldId, children: [
@@ -7867,6 +7926,7 @@ function CurrencyField({
7867
7926
  id,
7868
7927
  placeholder = "0.00",
7869
7928
  className = "",
7929
+ reserveFooter = false,
7870
7930
  ...rest
7871
7931
  }) {
7872
7932
  const __twcStyles = useScopedStyles("twc-curf-styles", CURF_CSS);
@@ -7929,7 +7989,7 @@ function CurrencyField({
7929
7989
  onValueChange?.(Number(fixed), fixed, cur);
7930
7990
  }
7931
7991
  const curLabel = typeof label === "string" && label ? `${label} currency` : "Currency";
7932
- return /* @__PURE__ */ jsxs("div", { className: `twc-field ${className}`, children: [
7992
+ return /* @__PURE__ */ jsxs("div", { className: `twc-field ${className}`, "data-reserve-footer": reserveFooter || void 0, children: [
7933
7993
  __twcStyles,
7934
7994
  label ? /* @__PURE__ */ jsxs("label", { className: "twc-field__label", htmlFor: fieldId, children: [
7935
7995
  label,
@@ -7976,6 +8036,17 @@ function CurrencyField({
7976
8036
  }
7977
8037
  var FIELD_CSS2 = `
7978
8038
  .twc-field { display: flex; flex-direction: column; gap: var(--space-1-5); font-family: var(--font-sans); }
8039
+ /* #494: reserve the footer lane. A row of fields aligned with align="flex-end" splits in two the
8040
+ moment ONE of them has a hint, because that field grows a footer and the others do not - and the
8041
+ caption then moves a DIFFERENT control, which nothing in the JSX suggests. align= cannot fix it:
8042
+ flex-start lines up the labels instead, so the controls part company as soon as one label wraps.
8043
+ The lane is held open by generated content rather than a fixed height so it matches the real
8044
+ footer by construction - same font metrics, same flex gap - instead of by a number that drifts.
8045
+ Whitespace-only generated content is not announced, and :has() lets the CSS tell on its own
8046
+ whether a footer is already there, so no component has to report it. */
8047
+ .twc-field[data-reserve-footer]:not(:has(> .twc-field__footer, > .twc-field__hint, > .twc-field__error))::after,
8048
+ [data-reserve-footer] .twc-field:not(:has(> .twc-field__footer, > .twc-field__hint, > .twc-field__error))::after {
8049
+ content: "\\00a0"; font-size: var(--text-xs); user-select: none; }
7979
8050
  .twc-field__label { font-size: var(--text-sm); font-weight: var(--font-semibold); color: var(--color-text); display: flex; gap: 4px; align-items: center; }
7980
8051
  .twc-field__req { color: var(--color-danger); }
7981
8052
  .twc-field__hint { font-size: var(--text-xs); color: var(--color-text-muted); }
@@ -8065,6 +8136,7 @@ var Input = /* @__PURE__ */ React55__default.forwardRef(function Input2({
8065
8136
  disabled = false,
8066
8137
  id,
8067
8138
  className = "",
8139
+ reserveFooter = false,
8068
8140
  ...rest
8069
8141
  }, ref) {
8070
8142
  const __twcStyles = useScopedStyles("twc-field-styles", FIELD_CSS2);
@@ -8134,7 +8206,7 @@ var Input = /* @__PURE__ */ React55__default.forwardRef(function Input2({
8134
8206
  ] })
8135
8207
  }
8136
8208
  ) : null;
8137
- return /* @__PURE__ */ jsxs("div", { className: `twc-field ${className}`, children: [
8209
+ return /* @__PURE__ */ jsxs("div", { className: `twc-field ${className}`, "data-reserve-footer": reserveFooter || void 0, children: [
8138
8210
  __twcStyles,
8139
8211
  __twcExtra,
8140
8212
  label ? /* @__PURE__ */ jsxs("label", { className: "twc-field__label", htmlFor: fieldId, children: [
@@ -8179,6 +8251,17 @@ var Input = /* @__PURE__ */ React55__default.forwardRef(function Input2({
8179
8251
  });
8180
8252
  var MULTI_CSS = `
8181
8253
  .twc-field { display: flex; flex-direction: column; gap: var(--space-1-5); font-family: var(--font-sans); }
8254
+ /* #494: reserve the footer lane. A row of fields aligned with align="flex-end" splits in two the
8255
+ moment ONE of them has a hint, because that field grows a footer and the others do not - and the
8256
+ caption then moves a DIFFERENT control, which nothing in the JSX suggests. align= cannot fix it:
8257
+ flex-start lines up the labels instead, so the controls part company as soon as one label wraps.
8258
+ The lane is held open by generated content rather than a fixed height so it matches the real
8259
+ footer by construction - same font metrics, same flex gap - instead of by a number that drifts.
8260
+ Whitespace-only generated content is not announced, and :has() lets the CSS tell on its own
8261
+ whether a footer is already there, so no component has to report it. */
8262
+ .twc-field[data-reserve-footer]:not(:has(> .twc-field__footer, > .twc-field__hint, > .twc-field__error))::after,
8263
+ [data-reserve-footer] .twc-field:not(:has(> .twc-field__footer, > .twc-field__hint, > .twc-field__error))::after {
8264
+ content: "\\00a0"; font-size: var(--text-xs); user-select: none; }
8182
8265
  .twc-field__label { font-size: var(--text-sm); font-weight: var(--font-semibold); color: var(--color-text); display: flex; gap: 4px; align-items: center; }
8183
8266
  .twc-field__req { color: var(--color-danger); }
8184
8267
  .twc-field__hint { font-size: var(--text-xs); color: var(--color-text-muted); }
@@ -8324,6 +8407,7 @@ var MultiSelect = /* @__PURE__ */ React55__default.forwardRef(function MultiSele
8324
8407
  className = "",
8325
8408
  onFocus,
8326
8409
  onKeyDown,
8410
+ reserveFooter = false,
8327
8411
  ...rest
8328
8412
  }, ref) {
8329
8413
  const __twcStyles = useScopedStyles("twc-ms-styles", MULTI_CSS);
@@ -8597,7 +8681,7 @@ var MultiSelect = /* @__PURE__ */ React55__default.forwardRef(function MultiSele
8597
8681
  }
8598
8682
  }
8599
8683
  const msFallbackName = useFallbackName(inputRef, Boolean(label || rest["aria-label"] || rest["aria-labelledby"])) ? placeholder : void 0;
8600
- return /* @__PURE__ */ jsxs("div", { className: `twc-field ${className}`, ref: wrapRef, children: [
8684
+ return /* @__PURE__ */ jsxs("div", { className: `twc-field ${className}`, ref: wrapRef, "data-reserve-footer": reserveFooter || void 0, children: [
8601
8685
  __twcStyles,
8602
8686
  name ? selected.map((v, i) => /* @__PURE__ */ jsx("input", { type: "hidden", name, value: v }, i)) : null,
8603
8687
  label ? /* @__PURE__ */ jsxs("label", { className: "twc-field__label", htmlFor: fieldId, children: [
@@ -9273,9 +9357,20 @@ var DT_CSS = `
9273
9357
  font-size: var(--text-sm); color: var(--color-text); background: var(--twc-dt-row-bg, var(--color-surface));
9274
9358
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis; max-width: 0; }
9275
9359
  /* Wrap-text columns: content flows onto multiple lines (the row grows down) instead of
9276
- clipping to one line. height stays a per-row minimum, so single-line cells are unchanged. */
9360
+ clipping to one line. height stays a per-row minimum, so single-line cells are unchanged.
9361
+ The top alignment here is only a DEFAULT for wrapped cells - an explicit verticalAlign at any
9362
+ level resolves to data-valign below, which is more specific and wins. Before that existed, a row
9363
+ mixing a wrapped column with normal ones was silently inconsistent: the wrapped cell sat top and
9364
+ its neighbours sat middle, with no way to say otherwise. */
9277
9365
  .twc-dt__td[data-wrap="true"] { white-space: normal; overflow: hidden; text-overflow: clip;
9278
9366
  word-break: break-word; overflow-wrap: anywhere; vertical-align: top; padding-block: 10px; line-height: 1.45; }
9367
+ /* Vertical alignment. One attribute, already resolved: column > row > table default, computed in
9368
+ renderLeaf rather than left to three competing selectors, so the precedence is readable in the
9369
+ code and cannot be reordered by an unrelated CSS edit. Matters whenever rows vary in height -
9370
+ wrapText, a multi-line renderCell, or a drag-resized row. */
9371
+ .twc-dt__td[data-valign="top"] { vertical-align: top; }
9372
+ .twc-dt__td[data-valign="middle"] { vertical-align: middle; }
9373
+ .twc-dt__td[data-valign="bottom"] { vertical-align: bottom; }
9279
9374
  /* #338: "combined" column cell \u2014 one column showing several source columns' values. */
9280
9375
  .twc-dt__combine--stack { display: flex; flex-direction: column; gap: 1px; }
9281
9376
  .twc-dt__combine-sep { color: var(--color-text-subtle); padding-inline: 3px; }
@@ -10044,6 +10139,8 @@ function Datatable({
10044
10139
  batchActions = [],
10045
10140
  onRowSelectionChange,
10046
10141
  selectionSummary,
10142
+ verticalAlign,
10143
+ getRowVerticalAlign,
10047
10144
  getRowStyle,
10048
10145
  getRowClassName,
10049
10146
  showExport = false,
@@ -12623,6 +12720,7 @@ function Datatable({
12623
12720
  const h = rowHeights[k];
12624
12721
  const userRowStyle = getRowStyle ? getRowStyle(row, ri) : void 0;
12625
12722
  const userRowClass = getRowClassName ? getRowClassName(row, ri) : void 0;
12723
+ const rowValign = getRowVerticalAlign ? getRowVerticalAlign(row, ri) : void 0;
12626
12724
  const { background: _rowBgShorthand, backgroundColor: _rowBgColor, ...restRowStyle } = userRowStyle || {};
12627
12725
  const usable = (v) => typeof v === "string" && v.trim() !== "" ? v : void 0;
12628
12726
  const rowBg = usable(_rowBgShorthand) ?? usable(_rowBgColor);
@@ -12733,6 +12831,7 @@ function Datatable({
12733
12831
  "data-num": c.type === "number" || void 0,
12734
12832
  "data-actions": isActions || void 0,
12735
12833
  "data-align": c.align !== dtNaturalAlign(c) ? c.align : void 0,
12834
+ "data-valign": c.verticalAlign ?? rowValign ?? verticalAlign,
12736
12835
  "data-editable": editable && !isEditing || void 0,
12737
12836
  "data-editing": isEditing || void 0,
12738
12837
  "data-cell-active": cellActive || void 0,
@@ -14829,6 +14928,17 @@ function DiffTable({
14829
14928
  }
14830
14929
  var FIELD_CSS3 = `
14831
14930
  .twc-field { display: flex; flex-direction: column; gap: var(--space-1-5); font-family: var(--font-sans); }
14931
+ /* #494: reserve the footer lane. A row of fields aligned with align="flex-end" splits in two the
14932
+ moment ONE of them has a hint, because that field grows a footer and the others do not - and the
14933
+ caption then moves a DIFFERENT control, which nothing in the JSX suggests. align= cannot fix it:
14934
+ flex-start lines up the labels instead, so the controls part company as soon as one label wraps.
14935
+ The lane is held open by generated content rather than a fixed height so it matches the real
14936
+ footer by construction - same font metrics, same flex gap - instead of by a number that drifts.
14937
+ Whitespace-only generated content is not announced, and :has() lets the CSS tell on its own
14938
+ whether a footer is already there, so no component has to report it. */
14939
+ .twc-field[data-reserve-footer]:not(:has(> .twc-field__footer, > .twc-field__hint, > .twc-field__error))::after,
14940
+ [data-reserve-footer] .twc-field:not(:has(> .twc-field__footer, > .twc-field__hint, > .twc-field__error))::after {
14941
+ content: "\\00a0"; font-size: var(--text-xs); user-select: none; }
14832
14942
  .twc-field__label { font-size: var(--text-sm); font-weight: var(--font-semibold); color: var(--color-text); display: flex; gap: 4px; align-items: center; }
14833
14943
  .twc-field__req { color: var(--color-danger); }
14834
14944
  .twc-field__hint { font-size: var(--text-xs); color: var(--color-text-muted); }
@@ -14961,6 +15071,7 @@ function DatePicker({
14961
15071
  onChange,
14962
15072
  id,
14963
15073
  className = "",
15074
+ reserveFooter = false,
14964
15075
  ...rest
14965
15076
  }) {
14966
15077
  const __twcStyles0 = useScopedStyles("twc-field-styles", FIELD_CSS3);
@@ -15163,7 +15274,7 @@ function DatePicker({
15163
15274
  setFocusDate(next);
15164
15275
  if (next.getFullYear() !== y || next.getMonth() !== m) setView(new Date(next.getFullYear(), next.getMonth(), 1));
15165
15276
  };
15166
- return /* @__PURE__ */ jsxs("div", { className: `twc-dp twc-field ${className}`, ref: wrapRef, ...rest, children: [
15277
+ return /* @__PURE__ */ jsxs("div", { className: `twc-dp twc-field ${className}`, ref: wrapRef, ...rest, "data-reserve-footer": reserveFooter || void 0, children: [
15167
15278
  __twcStyles0,
15168
15279
  __twcStyles1,
15169
15280
  label ? /* @__PURE__ */ jsxs("label", { className: "twc-field__label", id: `${fieldId}-label`, children: [
@@ -15387,6 +15498,17 @@ function DatePicker({
15387
15498
  }
15388
15499
  var FIELD_CSS4 = `
15389
15500
  .twc-field { display: flex; flex-direction: column; gap: var(--space-1-5); font-family: var(--font-sans); }
15501
+ /* #494: reserve the footer lane. A row of fields aligned with align="flex-end" splits in two the
15502
+ moment ONE of them has a hint, because that field grows a footer and the others do not - and the
15503
+ caption then moves a DIFFERENT control, which nothing in the JSX suggests. align= cannot fix it:
15504
+ flex-start lines up the labels instead, so the controls part company as soon as one label wraps.
15505
+ The lane is held open by generated content rather than a fixed height so it matches the real
15506
+ footer by construction - same font metrics, same flex gap - instead of by a number that drifts.
15507
+ Whitespace-only generated content is not announced, and :has() lets the CSS tell on its own
15508
+ whether a footer is already there, so no component has to report it. */
15509
+ .twc-field[data-reserve-footer]:not(:has(> .twc-field__footer, > .twc-field__hint, > .twc-field__error))::after,
15510
+ [data-reserve-footer] .twc-field:not(:has(> .twc-field__footer, > .twc-field__hint, > .twc-field__error))::after {
15511
+ content: "\\00a0"; font-size: var(--text-xs); user-select: none; }
15390
15512
  .twc-field__label { font-size: var(--text-sm); font-weight: var(--font-semibold); color: var(--color-text); display: flex; gap: 4px; align-items: center; }
15391
15513
  .twc-field__req { color: var(--color-danger); }
15392
15514
  .twc-field__hint { font-size: var(--text-xs); color: var(--color-text-muted); }
@@ -15522,6 +15644,7 @@ function DateRangePicker({
15522
15644
  parse,
15523
15645
  onChange,
15524
15646
  className = "",
15647
+ reserveFooter = false,
15525
15648
  ...rest
15526
15649
  }) {
15527
15650
  const __twcStyles0 = useScopedStyles("twc-field-styles", FIELD_CSS4);
@@ -15766,7 +15889,7 @@ function DateRangePicker({
15766
15889
  return t > Math.min(s, e) && t < Math.max(s, e);
15767
15890
  };
15768
15891
  const text = range2.start ? format ? format(range2) : `${fmtD(range2.start, locale)} \u2013 ${range2.end ? fmtD(range2.end, locale) : "\u2026"}` : "";
15769
- return /* @__PURE__ */ jsxs("div", { className: `twc-drp twc-field ${className}`, ref: wrapRef, ...rest, children: [
15892
+ return /* @__PURE__ */ jsxs("div", { className: `twc-drp twc-field ${className}`, ref: wrapRef, ...rest, "data-reserve-footer": reserveFooter || void 0, children: [
15770
15893
  __twcStyles0,
15771
15894
  __twcStyles1,
15772
15895
  label ? /* @__PURE__ */ jsxs("span", { className: "twc-field__label", id: labelId, children: [
@@ -15994,6 +16117,17 @@ function DateRangePicker({
15994
16117
  }
15995
16118
  var FIELD_CSS5 = `
15996
16119
  .twc-field { display: flex; flex-direction: column; gap: var(--space-1-5); font-family: var(--font-sans); }
16120
+ /* #494: reserve the footer lane. A row of fields aligned with align="flex-end" splits in two the
16121
+ moment ONE of them has a hint, because that field grows a footer and the others do not - and the
16122
+ caption then moves a DIFFERENT control, which nothing in the JSX suggests. align= cannot fix it:
16123
+ flex-start lines up the labels instead, so the controls part company as soon as one label wraps.
16124
+ The lane is held open by generated content rather than a fixed height so it matches the real
16125
+ footer by construction - same font metrics, same flex gap - instead of by a number that drifts.
16126
+ Whitespace-only generated content is not announced, and :has() lets the CSS tell on its own
16127
+ whether a footer is already there, so no component has to report it. */
16128
+ .twc-field[data-reserve-footer]:not(:has(> .twc-field__footer, > .twc-field__hint, > .twc-field__error))::after,
16129
+ [data-reserve-footer] .twc-field:not(:has(> .twc-field__footer, > .twc-field__hint, > .twc-field__error))::after {
16130
+ content: "\\00a0"; font-size: var(--text-xs); user-select: none; }
15997
16131
  .twc-field__label { font-size: var(--text-sm); font-weight: var(--font-semibold); color: var(--color-text); display: flex; gap: 4px; align-items: center; }
15998
16132
  .twc-field__req { color: var(--color-danger); }
15999
16133
  .twc-field__hint { font-size: var(--text-xs); color: var(--color-text-muted); }
@@ -16092,6 +16226,7 @@ function TimePicker({
16092
16226
  onChange,
16093
16227
  id,
16094
16228
  className = "",
16229
+ reserveFooter = false,
16095
16230
  ...rest
16096
16231
  }) {
16097
16232
  const __twcStyles0 = useScopedStyles("twc-field-styles", FIELD_CSS5);
@@ -16306,7 +16441,7 @@ function TimePicker({
16306
16441
  /* @__PURE__ */ jsx("circle", { cx: "12", cy: "12", r: "9" }),
16307
16442
  /* @__PURE__ */ jsx("path", { d: "M12 7v5l3 2" })
16308
16443
  ] });
16309
- return /* @__PURE__ */ jsxs("div", { className: `twc-tp twc-field ${className}`, ref: wrapRef, ...rest, children: [
16444
+ return /* @__PURE__ */ jsxs("div", { className: `twc-tp twc-field ${className}`, ref: wrapRef, ...rest, "data-reserve-footer": reserveFooter || void 0, children: [
16310
16445
  __twcStyles0,
16311
16446
  __twcStyles1,
16312
16447
  label ? /* @__PURE__ */ jsxs("label", { className: "twc-field__label", id: `${fieldId}-label`, children: [
@@ -16463,6 +16598,17 @@ function TimePicker({
16463
16598
  }
16464
16599
  var FIELD_CSS6 = `
16465
16600
  .twc-field { display: flex; flex-direction: column; gap: var(--space-1-5); font-family: var(--font-sans); }
16601
+ /* #494: reserve the footer lane. A row of fields aligned with align="flex-end" splits in two the
16602
+ moment ONE of them has a hint, because that field grows a footer and the others do not - and the
16603
+ caption then moves a DIFFERENT control, which nothing in the JSX suggests. align= cannot fix it:
16604
+ flex-start lines up the labels instead, so the controls part company as soon as one label wraps.
16605
+ The lane is held open by generated content rather than a fixed height so it matches the real
16606
+ footer by construction - same font metrics, same flex gap - instead of by a number that drifts.
16607
+ Whitespace-only generated content is not announced, and :has() lets the CSS tell on its own
16608
+ whether a footer is already there, so no component has to report it. */
16609
+ .twc-field[data-reserve-footer]:not(:has(> .twc-field__footer, > .twc-field__hint, > .twc-field__error))::after,
16610
+ [data-reserve-footer] .twc-field:not(:has(> .twc-field__footer, > .twc-field__hint, > .twc-field__error))::after {
16611
+ content: "\\00a0"; font-size: var(--text-xs); user-select: none; }
16466
16612
  .twc-field__label { font-size: var(--text-sm); font-weight: var(--font-semibold); color: var(--color-text); display: flex; gap: 4px; align-items: center; }
16467
16613
  .twc-field__req { color: var(--color-danger); }
16468
16614
  .twc-field__hint { font-size: var(--text-xs); color: var(--color-text-muted); }
@@ -16501,6 +16647,7 @@ function DateTimePicker({
16501
16647
  onChange,
16502
16648
  id,
16503
16649
  className = "",
16650
+ reserveFooter = false,
16504
16651
  ...rest
16505
16652
  }) {
16506
16653
  const __twcStyles0 = useScopedStyles("twc-field-styles", FIELD_CSS6);
@@ -16525,7 +16672,7 @@ function DateTimePicker({
16525
16672
  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));
16526
16673
  };
16527
16674
  const childTone = error ? "danger" : tone;
16528
- return /* @__PURE__ */ jsxs("div", { className: `twc-dtp twc-field ${className}`, id: fieldId, ...rest, children: [
16675
+ return /* @__PURE__ */ jsxs("div", { className: `twc-dtp twc-field ${className}`, id: fieldId, ...rest, "data-reserve-footer": reserveFooter || void 0, children: [
16529
16676
  __twcStyles0,
16530
16677
  __twcStyles1,
16531
16678
  label ? /* @__PURE__ */ jsxs("label", { className: "twc-field__label", id: `${fieldId}-label`, children: [
@@ -17103,6 +17250,17 @@ function EmptyState({
17103
17250
  }
17104
17251
  var FIELD_CSS7 = `
17105
17252
  .twc-field { display: flex; flex-direction: column; gap: var(--space-1-5); font-family: var(--font-sans); }
17253
+ /* #494: reserve the footer lane. A row of fields aligned with align="flex-end" splits in two the
17254
+ moment ONE of them has a hint, because that field grows a footer and the others do not - and the
17255
+ caption then moves a DIFFERENT control, which nothing in the JSX suggests. align= cannot fix it:
17256
+ flex-start lines up the labels instead, so the controls part company as soon as one label wraps.
17257
+ The lane is held open by generated content rather than a fixed height so it matches the real
17258
+ footer by construction - same font metrics, same flex gap - instead of by a number that drifts.
17259
+ Whitespace-only generated content is not announced, and :has() lets the CSS tell on its own
17260
+ whether a footer is already there, so no component has to report it. */
17261
+ .twc-field[data-reserve-footer]:not(:has(> .twc-field__footer, > .twc-field__hint, > .twc-field__error))::after,
17262
+ [data-reserve-footer] .twc-field:not(:has(> .twc-field__footer, > .twc-field__hint, > .twc-field__error))::after {
17263
+ content: "\\00a0"; font-size: var(--text-xs); user-select: none; }
17106
17264
  .twc-field__label { font-size: var(--text-sm); font-weight: var(--font-semibold); color: var(--color-text); display: flex; gap: 4px; align-items: center; }
17107
17265
  .twc-field__req { color: var(--color-danger); }
17108
17266
  .twc-field__hint { font-size: var(--text-xs); color: var(--color-text-muted); }
@@ -17117,6 +17275,7 @@ function Field({
17117
17275
  size = "md",
17118
17276
  id,
17119
17277
  className = "",
17278
+ reserveFooter = false,
17120
17279
  children,
17121
17280
  ...rest
17122
17281
  }) {
@@ -17129,7 +17288,7 @@ function Field({
17129
17288
  "aria-describedby": [children.props["aria-describedby"], hasMsg ? descId : null].filter(Boolean).join(" ") || void 0,
17130
17289
  "aria-invalid": children.props["aria-invalid"] !== void 0 ? children.props["aria-invalid"] : error ? true : void 0
17131
17290
  }) : children;
17132
- return /* @__PURE__ */ jsxs("div", { className: `twc-field ${className}`, "data-size": size, ...rest, children: [
17291
+ return /* @__PURE__ */ jsxs("div", { className: `twc-field ${className}`, "data-size": size, ...rest, "data-reserve-footer": reserveFooter || void 0, children: [
17133
17292
  __twcStyles,
17134
17293
  label ? /* @__PURE__ */ jsxs("label", { className: "twc-field__label", htmlFor, children: [
17135
17294
  label,
@@ -18185,6 +18344,17 @@ var Radio = /* @__PURE__ */ React55__default.forwardRef(function Radio2({
18185
18344
  .twc-field__req { color: var(--color-danger); }
18186
18345
  .twc-radio__desc { font-size: var(--text-xs); color: var(--color-text-muted); }
18187
18346
  .twc-field { display: flex; flex-direction: column; gap: var(--space-1-5); font-family: var(--font-sans); }
18347
+ /* #494: reserve the footer lane. A row of fields aligned with align="flex-end" splits in two the
18348
+ moment ONE of them has a hint, because that field grows a footer and the others do not - and the
18349
+ caption then moves a DIFFERENT control, which nothing in the JSX suggests. align= cannot fix it:
18350
+ flex-start lines up the labels instead, so the controls part company as soon as one label wraps.
18351
+ The lane is held open by generated content rather than a fixed height so it matches the real
18352
+ footer by construction - same font metrics, same flex gap - instead of by a number that drifts.
18353
+ Whitespace-only generated content is not announced, and :has() lets the CSS tell on its own
18354
+ whether a footer is already there, so no component has to report it. */
18355
+ .twc-field[data-reserve-footer]:not(:has(> .twc-field__footer, > .twc-field__hint, > .twc-field__error))::after,
18356
+ [data-reserve-footer] .twc-field:not(:has(> .twc-field__footer, > .twc-field__hint, > .twc-field__error))::after {
18357
+ content: "\\00a0"; font-size: var(--text-xs); user-select: none; }
18188
18358
  .twc-field__error { font-size: var(--text-xs); color: var(--color-danger-subtle-fg); font-weight: var(--font-medium); }
18189
18359
  `);
18190
18360
  const autoId = React55__default.useId();
@@ -18234,6 +18404,17 @@ var Radio = /* @__PURE__ */ React55__default.forwardRef(function Radio2({
18234
18404
  });
18235
18405
  var RADIO_GROUP_CSS = `
18236
18406
  .twc-field { display: flex; flex-direction: column; gap: var(--space-1-5); font-family: var(--font-sans); }
18407
+ /* #494: reserve the footer lane. A row of fields aligned with align="flex-end" splits in two the
18408
+ moment ONE of them has a hint, because that field grows a footer and the others do not - and the
18409
+ caption then moves a DIFFERENT control, which nothing in the JSX suggests. align= cannot fix it:
18410
+ flex-start lines up the labels instead, so the controls part company as soon as one label wraps.
18411
+ The lane is held open by generated content rather than a fixed height so it matches the real
18412
+ footer by construction - same font metrics, same flex gap - instead of by a number that drifts.
18413
+ Whitespace-only generated content is not announced, and :has() lets the CSS tell on its own
18414
+ whether a footer is already there, so no component has to report it. */
18415
+ .twc-field[data-reserve-footer]:not(:has(> .twc-field__footer, > .twc-field__hint, > .twc-field__error))::after,
18416
+ [data-reserve-footer] .twc-field:not(:has(> .twc-field__footer, > .twc-field__hint, > .twc-field__error))::after {
18417
+ content: "\\00a0"; font-size: var(--text-xs); user-select: none; }
18237
18418
  .twc-field__label { font-size: var(--text-sm); font-weight: var(--font-semibold); color: var(--color-text); display: flex; gap: 4px; align-items: center; }
18238
18419
  .twc-field__req { color: var(--color-danger); }
18239
18420
  .twc-field__hint { font-size: var(--text-xs); color: var(--color-text-muted); }
@@ -18258,6 +18439,7 @@ function RadioGroup({
18258
18439
  tone = "primary",
18259
18440
  id,
18260
18441
  className = "",
18442
+ reserveFooter = false,
18261
18443
  ...rest
18262
18444
  }) {
18263
18445
  const __twcStyles = useScopedStyles("twc-radio-group-styles", RADIO_GROUP_CSS);
@@ -18305,6 +18487,7 @@ function RadioGroup({
18305
18487
  "div",
18306
18488
  {
18307
18489
  className: `twc-field ${className}`,
18490
+ "data-reserve-footer": reserveFooter || void 0,
18308
18491
  role: "radiogroup",
18309
18492
  "aria-labelledby": label ? labelId : void 0,
18310
18493
  "aria-describedby": error || hint ? descId : void 0,
@@ -19329,6 +19512,17 @@ var Switch = /* @__PURE__ */ React55__default.forwardRef(function Switch2({
19329
19512
  .twc-field__req { color: var(--color-danger); }
19330
19513
  .twc-switch__desc { font-size: var(--text-xs); color: var(--color-text-muted); }
19331
19514
  .twc-field { display: flex; flex-direction: column; gap: var(--space-1-5); font-family: var(--font-sans); }
19515
+ /* #494: reserve the footer lane. A row of fields aligned with align="flex-end" splits in two the
19516
+ moment ONE of them has a hint, because that field grows a footer and the others do not - and the
19517
+ caption then moves a DIFFERENT control, which nothing in the JSX suggests. align= cannot fix it:
19518
+ flex-start lines up the labels instead, so the controls part company as soon as one label wraps.
19519
+ The lane is held open by generated content rather than a fixed height so it matches the real
19520
+ footer by construction - same font metrics, same flex gap - instead of by a number that drifts.
19521
+ Whitespace-only generated content is not announced, and :has() lets the CSS tell on its own
19522
+ whether a footer is already there, so no component has to report it. */
19523
+ .twc-field[data-reserve-footer]:not(:has(> .twc-field__footer, > .twc-field__hint, > .twc-field__error))::after,
19524
+ [data-reserve-footer] .twc-field:not(:has(> .twc-field__footer, > .twc-field__hint, > .twc-field__error))::after {
19525
+ content: "\\00a0"; font-size: var(--text-xs); user-select: none; }
19332
19526
  .twc-field__error { font-size: var(--text-xs); color: var(--color-danger-subtle-fg); font-weight: var(--font-medium); }
19333
19527
  `);
19334
19528
  const autoId = React55__default.useId();
@@ -19768,6 +19962,17 @@ function Tag({
19768
19962
  var useIso = typeof document !== "undefined" ? React55__default.useLayoutEffect : React55__default.useEffect;
19769
19963
  var TEXTAREA_CSS = `
19770
19964
  .twc-field { display: flex; flex-direction: column; gap: var(--space-1-5); font-family: var(--font-sans); }
19965
+ /* #494: reserve the footer lane. A row of fields aligned with align="flex-end" splits in two the
19966
+ moment ONE of them has a hint, because that field grows a footer and the others do not - and the
19967
+ caption then moves a DIFFERENT control, which nothing in the JSX suggests. align= cannot fix it:
19968
+ flex-start lines up the labels instead, so the controls part company as soon as one label wraps.
19969
+ The lane is held open by generated content rather than a fixed height so it matches the real
19970
+ footer by construction - same font metrics, same flex gap - instead of by a number that drifts.
19971
+ Whitespace-only generated content is not announced, and :has() lets the CSS tell on its own
19972
+ whether a footer is already there, so no component has to report it. */
19973
+ .twc-field[data-reserve-footer]:not(:has(> .twc-field__footer, > .twc-field__hint, > .twc-field__error))::after,
19974
+ [data-reserve-footer] .twc-field:not(:has(> .twc-field__footer, > .twc-field__hint, > .twc-field__error))::after {
19975
+ content: "\\00a0"; font-size: var(--text-xs); user-select: none; }
19771
19976
  .twc-field__label { font-size: var(--text-sm); font-weight: var(--font-semibold); color: var(--color-text); display: flex; gap: 4px; align-items: center; }
19772
19977
  .twc-field__req { color: var(--color-danger); }
19773
19978
  .twc-field__hint { font-size: var(--text-xs); color: var(--color-text-muted); }
@@ -19830,6 +20035,7 @@ var Textarea = /* @__PURE__ */ React55__default.forwardRef(function Textarea2({
19830
20035
  disabled = false,
19831
20036
  id,
19832
20037
  className = "",
20038
+ reserveFooter = false,
19833
20039
  ...rest
19834
20040
  }, ref) {
19835
20041
  const __twcStyles = useScopedStyles("twc-textarea-styles", TEXTAREA_CSS);
@@ -19887,7 +20093,7 @@ var Textarea = /* @__PURE__ */ React55__default.forwardRef(function Textarea2({
19887
20093
  const showClear = clearable && count > 0 && !disabled && !rest.readOnly;
19888
20094
  const ariaInvalid = invalid ? true : rest["aria-invalid"] ?? void 0;
19889
20095
  const describedBy = [error || hint ? descId : null, showCounter ? countId : null, rest["aria-describedby"]].filter(Boolean).join(" ") || void 0;
19890
- return /* @__PURE__ */ jsxs("div", { className: `twc-field ${className}`, children: [
20096
+ return /* @__PURE__ */ jsxs("div", { className: `twc-field ${className}`, "data-reserve-footer": reserveFooter || void 0, children: [
19891
20097
  __twcStyles,
19892
20098
  label ? /* @__PURE__ */ jsxs("label", { className: "twc-field__label", htmlFor: fieldId, children: [
19893
20099
  label,