twico-ui 1.45.1 → 1.47.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/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: [
@@ -9537,6 +9621,12 @@ th.twc-dt__rownum .twc-dt__th-inner { padding-inline: 8px; gap: 2px; justify-con
9537
9621
  .twc-dt__batch-x:hover { background: color-mix(in srgb, var(--color-primary) 20%, transparent); }
9538
9622
  .twc-dt__batch-x svg { width: 16px; height: 16px; }
9539
9623
  .twc-dt__batch-count { font-size: var(--text-sm); font-weight: var(--font-bold); color: var(--color-primary-subtle-fg); white-space: nowrap; }
9624
+ /* #492: the host's running total, sitting with the count it qualifies. Same colour as the count,
9625
+ differentiated by WEIGHT rather than by a lighter token: this bar is --color-primary-subtle, and
9626
+ --color-text-subtle on that is 4.26:1, under the 4.5:1 needed for normal text. */
9627
+ .twc-dt__batch-summary { display: inline-flex; align-items: center; gap: 6px; font-size: var(--text-sm);
9628
+ color: var(--color-primary-subtle-fg); white-space: nowrap; }
9629
+ .twc-dt__batch-summary::before { content: ""; width: var(--border-thin); height: 14px; background: currentColor; opacity: 0.35; }
9540
9630
  .twc-dt__batch-actions { margin-inline-start: auto; display: flex; align-items: center; gap: 6px; flex-wrap: wrap; }
9541
9631
  .twc-dt__batch-btn { display: inline-flex; align-items: center; gap: 6px; height: 32px; padding: 0 12px;
9542
9632
  border: var(--border-thin) solid var(--color-border); border-radius: var(--radius-md); background: var(--color-surface);
@@ -10037,6 +10127,7 @@ function Datatable({
10037
10127
  onColumnVisibilityChange,
10038
10128
  batchActions = [],
10039
10129
  onRowSelectionChange,
10130
+ selectionSummary,
10040
10131
  getRowStyle,
10041
10132
  getRowClassName,
10042
10133
  showExport = false,
@@ -12906,13 +12997,13 @@ function Datatable({
12906
12997
  return /* @__PURE__ */ jsxs("div", { className: `twc-dt ${className}`, ref: rootRef, "data-density": density, "data-resizing": resizing || void 0, "data-editing": editing ? "" : void 0, ...rest, children: [
12907
12998
  __twcStyles,
12908
12999
  /* @__PURE__ */ jsxs("div", { className: "twc-dt__toolbar", "data-compact": compact || void 0, children: [
12909
- toolbarActions != null ? /* @__PURE__ */ jsx("div", { className: "twc-dt__toolbar-actions", children: toolbarActions }) : null,
12910
13000
  (batchActions.length || hasBatchEditor) && selected.size > 0 ? /* @__PURE__ */ jsxs("div", { className: "twc-dt__batch", children: [
12911
13001
  /* @__PURE__ */ jsx("button", { type: "button", className: "twc-dt__batch-x", onClick: clearSelection, "aria-label": "Clear selection", children: /* @__PURE__ */ jsx(Svg, { d: I.x }) }),
12912
13002
  /* @__PURE__ */ jsxs("span", { className: "twc-dt__batch-count", children: [
12913
13003
  selected.size,
12914
13004
  " selected"
12915
13005
  ] }),
13006
+ selectionSummary != null ? /* @__PURE__ */ jsx("span", { className: "twc-dt__batch-summary", children: typeof selectionSummary === "function" ? selectionSummary(selKeys, selectedRows) : selectionSummary }) : null,
12916
13007
  /* @__PURE__ */ jsxs("div", { className: "twc-dt__batch-actions", children: [
12917
13008
  hasBatchEditor ? /* @__PURE__ */ jsxs("button", { type: "button", className: "twc-dt__batch-btn", onClick: (e) => openBatchEditor(e.currentTarget), children: [
12918
13009
  /* @__PURE__ */ jsx(Svg, { d: I.pencil }),
@@ -13107,6 +13198,7 @@ function Datatable({
13107
13198
  }
13108
13199
  )
13109
13200
  ] }) : null,
13201
+ toolbarActions != null ? /* @__PURE__ */ jsx("div", { className: "twc-dt__toolbar-actions", children: toolbarActions }) : null,
13110
13202
  searchable ? /* @__PURE__ */ jsxs("div", { className: "twc-dt__search", children: [
13111
13203
  /* @__PURE__ */ jsx(Svg, { d: I.search }),
13112
13204
  /* @__PURE__ */ jsx("input", { ref: quickSearchRef, placeholder: "Search\u2026", "aria-label": "Search rows", value: quick, onChange: (e) => commitQuick(e.target.value) }),
@@ -14395,6 +14487,11 @@ var CARDGRID_CSS = `
14395
14487
  padding: var(--space-2) var(--space-3); border: var(--border-thin) solid var(--color-primary-border);
14396
14488
  background: var(--color-primary-subtle); border-radius: var(--radius-md); }
14397
14489
  .twc-cardgrid__batch-count { font-size: var(--text-sm); font-weight: var(--font-semibold); color: var(--color-primary-subtle-fg); }
14490
+ /* #492: same slot, same contract as Datatable. Weight, not a lighter token - this bar is
14491
+ --color-primary-subtle, where --color-text-subtle is 4.26:1 and fails normal-text contrast. */
14492
+ .twc-cardgrid__batch-summary { display: inline-flex; align-items: center; gap: var(--space-2);
14493
+ font-size: var(--text-sm); color: var(--color-primary-subtle-fg); }
14494
+ .twc-cardgrid__batch-summary::before { content: ""; width: var(--border-thin); height: 14px; background: currentColor; opacity: 0.35; }
14398
14495
  .twc-cardgrid__batch-actions { display: inline-flex; flex-wrap: wrap; align-items: center; gap: var(--space-2); margin-inline-start: auto; }
14399
14496
  .twc-cardgrid__batch-btn { display: inline-flex; align-items: center; gap: var(--space-2); height: var(--control-h-sm);
14400
14497
  padding-inline: var(--space-3); border: var(--border-thin) solid var(--color-border); background: var(--color-surface);
@@ -14450,6 +14547,7 @@ function CardGrid({
14450
14547
  // the whole point, and it was the only Datatable affordance CardGrid lacked.
14451
14548
  checkboxSelection = false,
14452
14549
  batchActions = [],
14550
+ selectionSummary,
14453
14551
  onRowSelectionChange,
14454
14552
  selectionPlacement = "top-start",
14455
14553
  selectionLabel,
@@ -14657,6 +14755,7 @@ function CardGrid({
14657
14755
  selected.size,
14658
14756
  " selected"
14659
14757
  ] }),
14758
+ selectionSummary != null ? /* @__PURE__ */ jsx("span", { className: "twc-cardgrid__batch-summary", children: typeof selectionSummary === "function" ? selectionSummary(selKeys, selectedRows) : selectionSummary }) : null,
14660
14759
  showSelectAll && pageRows.length > 0 ? /* @__PURE__ */ jsxs("label", { className: "twc-cardgrid__selall", children: [
14661
14760
  /* @__PURE__ */ jsx(
14662
14761
  Checkbox,
@@ -14814,6 +14913,17 @@ function DiffTable({
14814
14913
  }
14815
14914
  var FIELD_CSS3 = `
14816
14915
  .twc-field { display: flex; flex-direction: column; gap: var(--space-1-5); font-family: var(--font-sans); }
14916
+ /* #494: reserve the footer lane. A row of fields aligned with align="flex-end" splits in two the
14917
+ moment ONE of them has a hint, because that field grows a footer and the others do not - and the
14918
+ caption then moves a DIFFERENT control, which nothing in the JSX suggests. align= cannot fix it:
14919
+ flex-start lines up the labels instead, so the controls part company as soon as one label wraps.
14920
+ The lane is held open by generated content rather than a fixed height so it matches the real
14921
+ footer by construction - same font metrics, same flex gap - instead of by a number that drifts.
14922
+ Whitespace-only generated content is not announced, and :has() lets the CSS tell on its own
14923
+ whether a footer is already there, so no component has to report it. */
14924
+ .twc-field[data-reserve-footer]:not(:has(> .twc-field__footer, > .twc-field__hint, > .twc-field__error))::after,
14925
+ [data-reserve-footer] .twc-field:not(:has(> .twc-field__footer, > .twc-field__hint, > .twc-field__error))::after {
14926
+ content: "\\00a0"; font-size: var(--text-xs); user-select: none; }
14817
14927
  .twc-field__label { font-size: var(--text-sm); font-weight: var(--font-semibold); color: var(--color-text); display: flex; gap: 4px; align-items: center; }
14818
14928
  .twc-field__req { color: var(--color-danger); }
14819
14929
  .twc-field__hint { font-size: var(--text-xs); color: var(--color-text-muted); }
@@ -14946,6 +15056,7 @@ function DatePicker({
14946
15056
  onChange,
14947
15057
  id,
14948
15058
  className = "",
15059
+ reserveFooter = false,
14949
15060
  ...rest
14950
15061
  }) {
14951
15062
  const __twcStyles0 = useScopedStyles("twc-field-styles", FIELD_CSS3);
@@ -15148,7 +15259,7 @@ function DatePicker({
15148
15259
  setFocusDate(next);
15149
15260
  if (next.getFullYear() !== y || next.getMonth() !== m) setView(new Date(next.getFullYear(), next.getMonth(), 1));
15150
15261
  };
15151
- return /* @__PURE__ */ jsxs("div", { className: `twc-dp twc-field ${className}`, ref: wrapRef, ...rest, children: [
15262
+ return /* @__PURE__ */ jsxs("div", { className: `twc-dp twc-field ${className}`, ref: wrapRef, ...rest, "data-reserve-footer": reserveFooter || void 0, children: [
15152
15263
  __twcStyles0,
15153
15264
  __twcStyles1,
15154
15265
  label ? /* @__PURE__ */ jsxs("label", { className: "twc-field__label", id: `${fieldId}-label`, children: [
@@ -15372,6 +15483,17 @@ function DatePicker({
15372
15483
  }
15373
15484
  var FIELD_CSS4 = `
15374
15485
  .twc-field { display: flex; flex-direction: column; gap: var(--space-1-5); font-family: var(--font-sans); }
15486
+ /* #494: reserve the footer lane. A row of fields aligned with align="flex-end" splits in two the
15487
+ moment ONE of them has a hint, because that field grows a footer and the others do not - and the
15488
+ caption then moves a DIFFERENT control, which nothing in the JSX suggests. align= cannot fix it:
15489
+ flex-start lines up the labels instead, so the controls part company as soon as one label wraps.
15490
+ The lane is held open by generated content rather than a fixed height so it matches the real
15491
+ footer by construction - same font metrics, same flex gap - instead of by a number that drifts.
15492
+ Whitespace-only generated content is not announced, and :has() lets the CSS tell on its own
15493
+ whether a footer is already there, so no component has to report it. */
15494
+ .twc-field[data-reserve-footer]:not(:has(> .twc-field__footer, > .twc-field__hint, > .twc-field__error))::after,
15495
+ [data-reserve-footer] .twc-field:not(:has(> .twc-field__footer, > .twc-field__hint, > .twc-field__error))::after {
15496
+ content: "\\00a0"; font-size: var(--text-xs); user-select: none; }
15375
15497
  .twc-field__label { font-size: var(--text-sm); font-weight: var(--font-semibold); color: var(--color-text); display: flex; gap: 4px; align-items: center; }
15376
15498
  .twc-field__req { color: var(--color-danger); }
15377
15499
  .twc-field__hint { font-size: var(--text-xs); color: var(--color-text-muted); }
@@ -15507,6 +15629,7 @@ function DateRangePicker({
15507
15629
  parse,
15508
15630
  onChange,
15509
15631
  className = "",
15632
+ reserveFooter = false,
15510
15633
  ...rest
15511
15634
  }) {
15512
15635
  const __twcStyles0 = useScopedStyles("twc-field-styles", FIELD_CSS4);
@@ -15751,7 +15874,7 @@ function DateRangePicker({
15751
15874
  return t > Math.min(s, e) && t < Math.max(s, e);
15752
15875
  };
15753
15876
  const text = range2.start ? format ? format(range2) : `${fmtD(range2.start, locale)} \u2013 ${range2.end ? fmtD(range2.end, locale) : "\u2026"}` : "";
15754
- return /* @__PURE__ */ jsxs("div", { className: `twc-drp twc-field ${className}`, ref: wrapRef, ...rest, children: [
15877
+ return /* @__PURE__ */ jsxs("div", { className: `twc-drp twc-field ${className}`, ref: wrapRef, ...rest, "data-reserve-footer": reserveFooter || void 0, children: [
15755
15878
  __twcStyles0,
15756
15879
  __twcStyles1,
15757
15880
  label ? /* @__PURE__ */ jsxs("span", { className: "twc-field__label", id: labelId, children: [
@@ -15979,6 +16102,17 @@ function DateRangePicker({
15979
16102
  }
15980
16103
  var FIELD_CSS5 = `
15981
16104
  .twc-field { display: flex; flex-direction: column; gap: var(--space-1-5); font-family: var(--font-sans); }
16105
+ /* #494: reserve the footer lane. A row of fields aligned with align="flex-end" splits in two the
16106
+ moment ONE of them has a hint, because that field grows a footer and the others do not - and the
16107
+ caption then moves a DIFFERENT control, which nothing in the JSX suggests. align= cannot fix it:
16108
+ flex-start lines up the labels instead, so the controls part company as soon as one label wraps.
16109
+ The lane is held open by generated content rather than a fixed height so it matches the real
16110
+ footer by construction - same font metrics, same flex gap - instead of by a number that drifts.
16111
+ Whitespace-only generated content is not announced, and :has() lets the CSS tell on its own
16112
+ whether a footer is already there, so no component has to report it. */
16113
+ .twc-field[data-reserve-footer]:not(:has(> .twc-field__footer, > .twc-field__hint, > .twc-field__error))::after,
16114
+ [data-reserve-footer] .twc-field:not(:has(> .twc-field__footer, > .twc-field__hint, > .twc-field__error))::after {
16115
+ content: "\\00a0"; font-size: var(--text-xs); user-select: none; }
15982
16116
  .twc-field__label { font-size: var(--text-sm); font-weight: var(--font-semibold); color: var(--color-text); display: flex; gap: 4px; align-items: center; }
15983
16117
  .twc-field__req { color: var(--color-danger); }
15984
16118
  .twc-field__hint { font-size: var(--text-xs); color: var(--color-text-muted); }
@@ -16077,6 +16211,7 @@ function TimePicker({
16077
16211
  onChange,
16078
16212
  id,
16079
16213
  className = "",
16214
+ reserveFooter = false,
16080
16215
  ...rest
16081
16216
  }) {
16082
16217
  const __twcStyles0 = useScopedStyles("twc-field-styles", FIELD_CSS5);
@@ -16291,7 +16426,7 @@ function TimePicker({
16291
16426
  /* @__PURE__ */ jsx("circle", { cx: "12", cy: "12", r: "9" }),
16292
16427
  /* @__PURE__ */ jsx("path", { d: "M12 7v5l3 2" })
16293
16428
  ] });
16294
- return /* @__PURE__ */ jsxs("div", { className: `twc-tp twc-field ${className}`, ref: wrapRef, ...rest, children: [
16429
+ return /* @__PURE__ */ jsxs("div", { className: `twc-tp twc-field ${className}`, ref: wrapRef, ...rest, "data-reserve-footer": reserveFooter || void 0, children: [
16295
16430
  __twcStyles0,
16296
16431
  __twcStyles1,
16297
16432
  label ? /* @__PURE__ */ jsxs("label", { className: "twc-field__label", id: `${fieldId}-label`, children: [
@@ -16448,6 +16583,17 @@ function TimePicker({
16448
16583
  }
16449
16584
  var FIELD_CSS6 = `
16450
16585
  .twc-field { display: flex; flex-direction: column; gap: var(--space-1-5); font-family: var(--font-sans); }
16586
+ /* #494: reserve the footer lane. A row of fields aligned with align="flex-end" splits in two the
16587
+ moment ONE of them has a hint, because that field grows a footer and the others do not - and the
16588
+ caption then moves a DIFFERENT control, which nothing in the JSX suggests. align= cannot fix it:
16589
+ flex-start lines up the labels instead, so the controls part company as soon as one label wraps.
16590
+ The lane is held open by generated content rather than a fixed height so it matches the real
16591
+ footer by construction - same font metrics, same flex gap - instead of by a number that drifts.
16592
+ Whitespace-only generated content is not announced, and :has() lets the CSS tell on its own
16593
+ whether a footer is already there, so no component has to report it. */
16594
+ .twc-field[data-reserve-footer]:not(:has(> .twc-field__footer, > .twc-field__hint, > .twc-field__error))::after,
16595
+ [data-reserve-footer] .twc-field:not(:has(> .twc-field__footer, > .twc-field__hint, > .twc-field__error))::after {
16596
+ content: "\\00a0"; font-size: var(--text-xs); user-select: none; }
16451
16597
  .twc-field__label { font-size: var(--text-sm); font-weight: var(--font-semibold); color: var(--color-text); display: flex; gap: 4px; align-items: center; }
16452
16598
  .twc-field__req { color: var(--color-danger); }
16453
16599
  .twc-field__hint { font-size: var(--text-xs); color: var(--color-text-muted); }
@@ -16486,6 +16632,7 @@ function DateTimePicker({
16486
16632
  onChange,
16487
16633
  id,
16488
16634
  className = "",
16635
+ reserveFooter = false,
16489
16636
  ...rest
16490
16637
  }) {
16491
16638
  const __twcStyles0 = useScopedStyles("twc-field-styles", FIELD_CSS6);
@@ -16510,7 +16657,7 @@ function DateTimePicker({
16510
16657
  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));
16511
16658
  };
16512
16659
  const childTone = error ? "danger" : tone;
16513
- return /* @__PURE__ */ jsxs("div", { className: `twc-dtp twc-field ${className}`, id: fieldId, ...rest, children: [
16660
+ return /* @__PURE__ */ jsxs("div", { className: `twc-dtp twc-field ${className}`, id: fieldId, ...rest, "data-reserve-footer": reserveFooter || void 0, children: [
16514
16661
  __twcStyles0,
16515
16662
  __twcStyles1,
16516
16663
  label ? /* @__PURE__ */ jsxs("label", { className: "twc-field__label", id: `${fieldId}-label`, children: [
@@ -17088,6 +17235,17 @@ function EmptyState({
17088
17235
  }
17089
17236
  var FIELD_CSS7 = `
17090
17237
  .twc-field { display: flex; flex-direction: column; gap: var(--space-1-5); font-family: var(--font-sans); }
17238
+ /* #494: reserve the footer lane. A row of fields aligned with align="flex-end" splits in two the
17239
+ moment ONE of them has a hint, because that field grows a footer and the others do not - and the
17240
+ caption then moves a DIFFERENT control, which nothing in the JSX suggests. align= cannot fix it:
17241
+ flex-start lines up the labels instead, so the controls part company as soon as one label wraps.
17242
+ The lane is held open by generated content rather than a fixed height so it matches the real
17243
+ footer by construction - same font metrics, same flex gap - instead of by a number that drifts.
17244
+ Whitespace-only generated content is not announced, and :has() lets the CSS tell on its own
17245
+ whether a footer is already there, so no component has to report it. */
17246
+ .twc-field[data-reserve-footer]:not(:has(> .twc-field__footer, > .twc-field__hint, > .twc-field__error))::after,
17247
+ [data-reserve-footer] .twc-field:not(:has(> .twc-field__footer, > .twc-field__hint, > .twc-field__error))::after {
17248
+ content: "\\00a0"; font-size: var(--text-xs); user-select: none; }
17091
17249
  .twc-field__label { font-size: var(--text-sm); font-weight: var(--font-semibold); color: var(--color-text); display: flex; gap: 4px; align-items: center; }
17092
17250
  .twc-field__req { color: var(--color-danger); }
17093
17251
  .twc-field__hint { font-size: var(--text-xs); color: var(--color-text-muted); }
@@ -17102,6 +17260,7 @@ function Field({
17102
17260
  size = "md",
17103
17261
  id,
17104
17262
  className = "",
17263
+ reserveFooter = false,
17105
17264
  children,
17106
17265
  ...rest
17107
17266
  }) {
@@ -17114,7 +17273,7 @@ function Field({
17114
17273
  "aria-describedby": [children.props["aria-describedby"], hasMsg ? descId : null].filter(Boolean).join(" ") || void 0,
17115
17274
  "aria-invalid": children.props["aria-invalid"] !== void 0 ? children.props["aria-invalid"] : error ? true : void 0
17116
17275
  }) : children;
17117
- return /* @__PURE__ */ jsxs("div", { className: `twc-field ${className}`, "data-size": size, ...rest, children: [
17276
+ return /* @__PURE__ */ jsxs("div", { className: `twc-field ${className}`, "data-size": size, ...rest, "data-reserve-footer": reserveFooter || void 0, children: [
17118
17277
  __twcStyles,
17119
17278
  label ? /* @__PURE__ */ jsxs("label", { className: "twc-field__label", htmlFor, children: [
17120
17279
  label,
@@ -18170,6 +18329,17 @@ var Radio = /* @__PURE__ */ React55__default.forwardRef(function Radio2({
18170
18329
  .twc-field__req { color: var(--color-danger); }
18171
18330
  .twc-radio__desc { font-size: var(--text-xs); color: var(--color-text-muted); }
18172
18331
  .twc-field { display: flex; flex-direction: column; gap: var(--space-1-5); font-family: var(--font-sans); }
18332
+ /* #494: reserve the footer lane. A row of fields aligned with align="flex-end" splits in two the
18333
+ moment ONE of them has a hint, because that field grows a footer and the others do not - and the
18334
+ caption then moves a DIFFERENT control, which nothing in the JSX suggests. align= cannot fix it:
18335
+ flex-start lines up the labels instead, so the controls part company as soon as one label wraps.
18336
+ The lane is held open by generated content rather than a fixed height so it matches the real
18337
+ footer by construction - same font metrics, same flex gap - instead of by a number that drifts.
18338
+ Whitespace-only generated content is not announced, and :has() lets the CSS tell on its own
18339
+ whether a footer is already there, so no component has to report it. */
18340
+ .twc-field[data-reserve-footer]:not(:has(> .twc-field__footer, > .twc-field__hint, > .twc-field__error))::after,
18341
+ [data-reserve-footer] .twc-field:not(:has(> .twc-field__footer, > .twc-field__hint, > .twc-field__error))::after {
18342
+ content: "\\00a0"; font-size: var(--text-xs); user-select: none; }
18173
18343
  .twc-field__error { font-size: var(--text-xs); color: var(--color-danger-subtle-fg); font-weight: var(--font-medium); }
18174
18344
  `);
18175
18345
  const autoId = React55__default.useId();
@@ -18219,6 +18389,17 @@ var Radio = /* @__PURE__ */ React55__default.forwardRef(function Radio2({
18219
18389
  });
18220
18390
  var RADIO_GROUP_CSS = `
18221
18391
  .twc-field { display: flex; flex-direction: column; gap: var(--space-1-5); font-family: var(--font-sans); }
18392
+ /* #494: reserve the footer lane. A row of fields aligned with align="flex-end" splits in two the
18393
+ moment ONE of them has a hint, because that field grows a footer and the others do not - and the
18394
+ caption then moves a DIFFERENT control, which nothing in the JSX suggests. align= cannot fix it:
18395
+ flex-start lines up the labels instead, so the controls part company as soon as one label wraps.
18396
+ The lane is held open by generated content rather than a fixed height so it matches the real
18397
+ footer by construction - same font metrics, same flex gap - instead of by a number that drifts.
18398
+ Whitespace-only generated content is not announced, and :has() lets the CSS tell on its own
18399
+ whether a footer is already there, so no component has to report it. */
18400
+ .twc-field[data-reserve-footer]:not(:has(> .twc-field__footer, > .twc-field__hint, > .twc-field__error))::after,
18401
+ [data-reserve-footer] .twc-field:not(:has(> .twc-field__footer, > .twc-field__hint, > .twc-field__error))::after {
18402
+ content: "\\00a0"; font-size: var(--text-xs); user-select: none; }
18222
18403
  .twc-field__label { font-size: var(--text-sm); font-weight: var(--font-semibold); color: var(--color-text); display: flex; gap: 4px; align-items: center; }
18223
18404
  .twc-field__req { color: var(--color-danger); }
18224
18405
  .twc-field__hint { font-size: var(--text-xs); color: var(--color-text-muted); }
@@ -18243,6 +18424,7 @@ function RadioGroup({
18243
18424
  tone = "primary",
18244
18425
  id,
18245
18426
  className = "",
18427
+ reserveFooter = false,
18246
18428
  ...rest
18247
18429
  }) {
18248
18430
  const __twcStyles = useScopedStyles("twc-radio-group-styles", RADIO_GROUP_CSS);
@@ -18290,6 +18472,7 @@ function RadioGroup({
18290
18472
  "div",
18291
18473
  {
18292
18474
  className: `twc-field ${className}`,
18475
+ "data-reserve-footer": reserveFooter || void 0,
18293
18476
  role: "radiogroup",
18294
18477
  "aria-labelledby": label ? labelId : void 0,
18295
18478
  "aria-describedby": error || hint ? descId : void 0,
@@ -19314,6 +19497,17 @@ var Switch = /* @__PURE__ */ React55__default.forwardRef(function Switch2({
19314
19497
  .twc-field__req { color: var(--color-danger); }
19315
19498
  .twc-switch__desc { font-size: var(--text-xs); color: var(--color-text-muted); }
19316
19499
  .twc-field { display: flex; flex-direction: column; gap: var(--space-1-5); font-family: var(--font-sans); }
19500
+ /* #494: reserve the footer lane. A row of fields aligned with align="flex-end" splits in two the
19501
+ moment ONE of them has a hint, because that field grows a footer and the others do not - and the
19502
+ caption then moves a DIFFERENT control, which nothing in the JSX suggests. align= cannot fix it:
19503
+ flex-start lines up the labels instead, so the controls part company as soon as one label wraps.
19504
+ The lane is held open by generated content rather than a fixed height so it matches the real
19505
+ footer by construction - same font metrics, same flex gap - instead of by a number that drifts.
19506
+ Whitespace-only generated content is not announced, and :has() lets the CSS tell on its own
19507
+ whether a footer is already there, so no component has to report it. */
19508
+ .twc-field[data-reserve-footer]:not(:has(> .twc-field__footer, > .twc-field__hint, > .twc-field__error))::after,
19509
+ [data-reserve-footer] .twc-field:not(:has(> .twc-field__footer, > .twc-field__hint, > .twc-field__error))::after {
19510
+ content: "\\00a0"; font-size: var(--text-xs); user-select: none; }
19317
19511
  .twc-field__error { font-size: var(--text-xs); color: var(--color-danger-subtle-fg); font-weight: var(--font-medium); }
19318
19512
  `);
19319
19513
  const autoId = React55__default.useId();
@@ -19753,6 +19947,17 @@ function Tag({
19753
19947
  var useIso = typeof document !== "undefined" ? React55__default.useLayoutEffect : React55__default.useEffect;
19754
19948
  var TEXTAREA_CSS = `
19755
19949
  .twc-field { display: flex; flex-direction: column; gap: var(--space-1-5); font-family: var(--font-sans); }
19950
+ /* #494: reserve the footer lane. A row of fields aligned with align="flex-end" splits in two the
19951
+ moment ONE of them has a hint, because that field grows a footer and the others do not - and the
19952
+ caption then moves a DIFFERENT control, which nothing in the JSX suggests. align= cannot fix it:
19953
+ flex-start lines up the labels instead, so the controls part company as soon as one label wraps.
19954
+ The lane is held open by generated content rather than a fixed height so it matches the real
19955
+ footer by construction - same font metrics, same flex gap - instead of by a number that drifts.
19956
+ Whitespace-only generated content is not announced, and :has() lets the CSS tell on its own
19957
+ whether a footer is already there, so no component has to report it. */
19958
+ .twc-field[data-reserve-footer]:not(:has(> .twc-field__footer, > .twc-field__hint, > .twc-field__error))::after,
19959
+ [data-reserve-footer] .twc-field:not(:has(> .twc-field__footer, > .twc-field__hint, > .twc-field__error))::after {
19960
+ content: "\\00a0"; font-size: var(--text-xs); user-select: none; }
19756
19961
  .twc-field__label { font-size: var(--text-sm); font-weight: var(--font-semibold); color: var(--color-text); display: flex; gap: 4px; align-items: center; }
19757
19962
  .twc-field__req { color: var(--color-danger); }
19758
19963
  .twc-field__hint { font-size: var(--text-xs); color: var(--color-text-muted); }
@@ -19815,6 +20020,7 @@ var Textarea = /* @__PURE__ */ React55__default.forwardRef(function Textarea2({
19815
20020
  disabled = false,
19816
20021
  id,
19817
20022
  className = "",
20023
+ reserveFooter = false,
19818
20024
  ...rest
19819
20025
  }, ref) {
19820
20026
  const __twcStyles = useScopedStyles("twc-textarea-styles", TEXTAREA_CSS);
@@ -19872,7 +20078,7 @@ var Textarea = /* @__PURE__ */ React55__default.forwardRef(function Textarea2({
19872
20078
  const showClear = clearable && count > 0 && !disabled && !rest.readOnly;
19873
20079
  const ariaInvalid = invalid ? true : rest["aria-invalid"] ?? void 0;
19874
20080
  const describedBy = [error || hint ? descId : null, showCounter ? countId : null, rest["aria-describedby"]].filter(Boolean).join(" ") || void 0;
19875
- return /* @__PURE__ */ jsxs("div", { className: `twc-field ${className}`, children: [
20081
+ return /* @__PURE__ */ jsxs("div", { className: `twc-field ${className}`, "data-reserve-footer": reserveFooter || void 0, children: [
19876
20082
  __twcStyles,
19877
20083
  label ? /* @__PURE__ */ jsxs("label", { className: "twc-field__label", htmlFor: fieldId, children: [
19878
20084
  label,