twico-ui 1.29.0 → 1.30.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.d.cts CHANGED
@@ -1536,6 +1536,10 @@ interface Option {
1536
1536
  label: string;
1537
1537
  /** Optional second line shown under the label. */
1538
1538
  description?: string;
1539
+ /** #326: leading icon/swatch node, rendered before the label (mirrors a Command item's icon). */
1540
+ icon?: React.ReactNode;
1541
+ /** #326: trailing hint node, right-aligned after the label (e.g. a code, shortcut, or badge). */
1542
+ hint?: React.ReactNode;
1539
1543
  /** Disable this option — not selectable, skipped by keyboard navigation. */
1540
1544
  disabled?: boolean;
1541
1545
  }
@@ -1601,6 +1605,10 @@ interface ComboboxProps extends Omit<React.InputHTMLAttributes<HTMLInputElement>
1601
1605
  * ellipsis — useful when options share a long prefix and differ only in a truncated tail. Takes precedence
1602
1606
  * over `virtualized` (wrapped rows aren't a fixed height). @default false */
1603
1607
  wrapOptions?: boolean;
1608
+ /** #326: render a custom option row body (twico keeps the checkmark, keyboard nav, and ARIA wiring). For
1609
+ * lightweight cases prefer the additive `icon`/`hint` fields on an option instead. Disables `virtualized`
1610
+ * (custom rows aren't a fixed height). */
1611
+ renderOption?: (option: ComboboxOption, state: { selected: boolean; active: boolean }) => React.ReactNode;
1604
1612
  /** Composed with the field's open-on-focus behavior — your handler runs first. */
1605
1613
  onFocus?: React.FocusEventHandler<HTMLInputElement>;
1606
1614
  /** Composed with the field's keyboard navigation (Arrows/Enter/Escape) — your handler runs first; call `event.preventDefault()` to suppress it. */
@@ -2704,6 +2712,10 @@ interface MultiSelectProps extends Omit<React.InputHTMLAttributes<HTMLInputEleme
2704
2712
  virtualized?: boolean;
2705
2713
  /** Extra option rows rendered above/below the viewport when `virtualized`, to smooth fast scrolling. @default 8 */
2706
2714
  overscan?: number;
2715
+ /** #326: render a custom option row body (twico keeps the checkbox, keyboard nav, and ARIA wiring). For
2716
+ * lightweight cases prefer the additive `icon`/`hint` fields on an option instead. Disables `virtualized`
2717
+ * (custom rows aren't a fixed height). */
2718
+ renderOption?: (option: MultiSelectOption, state: { selected: boolean; active: boolean }) => React.ReactNode;
2707
2719
  /** Composed with the field's open-on-focus behavior — your handler runs first. */
2708
2720
  onFocus?: React.FocusEventHandler<HTMLInputElement>;
2709
2721
  /** Composed with the field's keyboard navigation (Arrows/Enter/Escape/Backspace) — your handler runs first; call `event.preventDefault()` to suppress it. */
@@ -3011,6 +3023,10 @@ interface InputProps extends Omit<React.InputHTMLAttributes<HTMLInputElement>, "
3011
3023
  rightAddon?: React.ReactNode;
3012
3024
  /** With `maxLength`, render a live `current / max` character counter on the hint/error line; danger-toned near the limit. @default false */
3013
3025
  showCount?: boolean;
3026
+ /** #328: show a trailing clear (X) button when the field is non-empty; clicking it empties the field and
3027
+ * fires `onChange` with the empty value (controlled callers update). Composes with `rightIcon` and, for
3028
+ * `type="password"`, sits alongside the reveal-eye toggle. @default false */
3029
+ clearable?: boolean;
3014
3030
  }
3015
3031
 
3016
3032
  declare function Input(props: InputProps): React.JSX.Element;
@@ -3383,6 +3399,10 @@ interface SelectProps extends Omit<React.HTMLAttributes<HTMLButtonElement>, "onC
3383
3399
  virtualized?: boolean;
3384
3400
  /** Extra option rows rendered above/below the viewport when `virtualized`, to smooth fast scrolling. @default 8 */
3385
3401
  overscan?: number;
3402
+ /** #326: render a custom option row body (twico keeps the checkmark, keyboard nav, and ARIA wiring). For
3403
+ * lightweight cases prefer the additive `icon`/`hint` fields on an option instead. Disables `virtualized`
3404
+ * (custom rows aren't a fixed height). */
3405
+ renderOption?: (option: SelectOption, state: { selected: boolean; active: boolean }) => React.ReactNode;
3386
3406
  }
3387
3407
 
3388
3408
  /** Alias of the shared {@link Option} type (kept for backward-compatible imports). */
@@ -3761,6 +3781,9 @@ interface TextareaProps extends React.TextareaHTMLAttributes<HTMLTextAreaElement
3761
3781
  maxRows?: number;
3762
3782
  /** With `maxLength`, render a live `current / max` character counter; danger-toned near the limit. @default false */
3763
3783
  showCount?: boolean;
3784
+ /** #328: show a trailing clear (X) button (top-inline-end, clear of the resize handle) when non-empty;
3785
+ * clicking it empties the field and fires `onChange` with the empty value. @default false */
3786
+ clearable?: boolean;
3764
3787
  }
3765
3788
 
3766
3789
  /** Forwards `ref` to the inner `<textarea>`. */
package/dist/index.d.ts CHANGED
@@ -1536,6 +1536,10 @@ interface Option {
1536
1536
  label: string;
1537
1537
  /** Optional second line shown under the label. */
1538
1538
  description?: string;
1539
+ /** #326: leading icon/swatch node, rendered before the label (mirrors a Command item's icon). */
1540
+ icon?: React.ReactNode;
1541
+ /** #326: trailing hint node, right-aligned after the label (e.g. a code, shortcut, or badge). */
1542
+ hint?: React.ReactNode;
1539
1543
  /** Disable this option — not selectable, skipped by keyboard navigation. */
1540
1544
  disabled?: boolean;
1541
1545
  }
@@ -1601,6 +1605,10 @@ interface ComboboxProps extends Omit<React.InputHTMLAttributes<HTMLInputElement>
1601
1605
  * ellipsis — useful when options share a long prefix and differ only in a truncated tail. Takes precedence
1602
1606
  * over `virtualized` (wrapped rows aren't a fixed height). @default false */
1603
1607
  wrapOptions?: boolean;
1608
+ /** #326: render a custom option row body (twico keeps the checkmark, keyboard nav, and ARIA wiring). For
1609
+ * lightweight cases prefer the additive `icon`/`hint` fields on an option instead. Disables `virtualized`
1610
+ * (custom rows aren't a fixed height). */
1611
+ renderOption?: (option: ComboboxOption, state: { selected: boolean; active: boolean }) => React.ReactNode;
1604
1612
  /** Composed with the field's open-on-focus behavior — your handler runs first. */
1605
1613
  onFocus?: React.FocusEventHandler<HTMLInputElement>;
1606
1614
  /** Composed with the field's keyboard navigation (Arrows/Enter/Escape) — your handler runs first; call `event.preventDefault()` to suppress it. */
@@ -2704,6 +2712,10 @@ interface MultiSelectProps extends Omit<React.InputHTMLAttributes<HTMLInputEleme
2704
2712
  virtualized?: boolean;
2705
2713
  /** Extra option rows rendered above/below the viewport when `virtualized`, to smooth fast scrolling. @default 8 */
2706
2714
  overscan?: number;
2715
+ /** #326: render a custom option row body (twico keeps the checkbox, keyboard nav, and ARIA wiring). For
2716
+ * lightweight cases prefer the additive `icon`/`hint` fields on an option instead. Disables `virtualized`
2717
+ * (custom rows aren't a fixed height). */
2718
+ renderOption?: (option: MultiSelectOption, state: { selected: boolean; active: boolean }) => React.ReactNode;
2707
2719
  /** Composed with the field's open-on-focus behavior — your handler runs first. */
2708
2720
  onFocus?: React.FocusEventHandler<HTMLInputElement>;
2709
2721
  /** Composed with the field's keyboard navigation (Arrows/Enter/Escape/Backspace) — your handler runs first; call `event.preventDefault()` to suppress it. */
@@ -3011,6 +3023,10 @@ interface InputProps extends Omit<React.InputHTMLAttributes<HTMLInputElement>, "
3011
3023
  rightAddon?: React.ReactNode;
3012
3024
  /** With `maxLength`, render a live `current / max` character counter on the hint/error line; danger-toned near the limit. @default false */
3013
3025
  showCount?: boolean;
3026
+ /** #328: show a trailing clear (X) button when the field is non-empty; clicking it empties the field and
3027
+ * fires `onChange` with the empty value (controlled callers update). Composes with `rightIcon` and, for
3028
+ * `type="password"`, sits alongside the reveal-eye toggle. @default false */
3029
+ clearable?: boolean;
3014
3030
  }
3015
3031
 
3016
3032
  declare function Input(props: InputProps): React.JSX.Element;
@@ -3383,6 +3399,10 @@ interface SelectProps extends Omit<React.HTMLAttributes<HTMLButtonElement>, "onC
3383
3399
  virtualized?: boolean;
3384
3400
  /** Extra option rows rendered above/below the viewport when `virtualized`, to smooth fast scrolling. @default 8 */
3385
3401
  overscan?: number;
3402
+ /** #326: render a custom option row body (twico keeps the checkmark, keyboard nav, and ARIA wiring). For
3403
+ * lightweight cases prefer the additive `icon`/`hint` fields on an option instead. Disables `virtualized`
3404
+ * (custom rows aren't a fixed height). */
3405
+ renderOption?: (option: SelectOption, state: { selected: boolean; active: boolean }) => React.ReactNode;
3386
3406
  }
3387
3407
 
3388
3408
  /** Alias of the shared {@link Option} type (kept for backward-compatible imports). */
@@ -3761,6 +3781,9 @@ interface TextareaProps extends React.TextareaHTMLAttributes<HTMLTextAreaElement
3761
3781
  maxRows?: number;
3762
3782
  /** With `maxLength`, render a live `current / max` character counter; danger-toned near the limit. @default false */
3763
3783
  showCount?: boolean;
3784
+ /** #328: show a trailing clear (X) button (top-inline-end, clear of the resize handle) when non-empty;
3785
+ * clicking it empties the field and fires `onChange` with the empty value. @default false */
3786
+ clearable?: boolean;
3764
3787
  }
3765
3788
 
3766
3789
  /** Forwards `ref` to the inner `<textarea>`. */
package/dist/index.mjs CHANGED
@@ -5650,6 +5650,10 @@ var COMBO_CSS = `
5650
5650
  .twc-opt__main { display: flex; flex-direction: column; gap: 1px; min-width: 0; flex: 1; }
5651
5651
  .twc-opt__label { line-height: 1.3; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
5652
5652
  .twc-opt__desc { font-size: var(--text-xs); color: var(--color-text-muted); line-height: 1.3; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
5653
+ /* #326: optional leading icon/swatch + trailing (right-aligned) hint on an option. */
5654
+ .twc-opt__icon { flex: none; display: inline-flex; align-items: center; color: var(--color-text-muted); }
5655
+ .twc-opt__icon svg { width: 16px; height: 16px; }
5656
+ .twc-opt__hint { flex: none; margin-inline-start: auto; padding-inline-start: var(--space-2); color: var(--color-text-muted); font-size: var(--text-xs); white-space: nowrap; }
5653
5657
  /* #300: wrapOptions \u2014 let long labels/descriptions wrap onto multiple lines instead of truncating.
5654
5658
  overflow-wrap: anywhere also breaks a single very long token so it can't overflow the popover edge. */
5655
5659
  .twc-opt[data-wrap="true"] { align-items: flex-start; }
@@ -5717,6 +5721,7 @@ function Combobox({
5717
5721
  virtualized: virtualizedProp = false,
5718
5722
  overscan = 8,
5719
5723
  wrapOptions = false,
5724
+ renderOption,
5720
5725
  id,
5721
5726
  className = "",
5722
5727
  onFocus,
@@ -5724,8 +5729,8 @@ function Combobox({
5724
5729
  ...rest
5725
5730
  }) {
5726
5731
  const __twcStyles = useScopedStyles("twc-combo-styles", COMBO_CSS);
5727
- if (virtualizedProp && wrapOptions) warnOnce("combobox-wrap-virtualized", "twico-ui Combobox: `wrapOptions` disables `virtualized` \u2014 wrapped option rows aren't a fixed height.");
5728
- const virtualized = virtualizedProp && !wrapOptions;
5732
+ if (virtualizedProp && (wrapOptions || renderOption)) warnOnce("combobox-var-height-virtualized", "twico-ui Combobox: `wrapOptions`/`renderOption` disable `virtualized` \u2014 those rows aren't a fixed height.");
5733
+ const virtualized = virtualizedProp && !wrapOptions && !renderOption;
5729
5734
  const groups = React51__default.useMemo(() => normalizeGroups(options), [options]);
5730
5735
  const flat = React51__default.useMemo(() => groups.flatMap((g) => g.options), [groups]);
5731
5736
  const autoId = React51__default.useId();
@@ -5896,7 +5901,7 @@ function Combobox({
5896
5901
  const activeId = open && visible[active] ? optionId(active) : void 0;
5897
5902
  const descId = `${fieldId}-desc`;
5898
5903
  const describedBy = error || hint ? descId : void 0;
5899
- const renderOption = (o, idx) => {
5904
+ const renderRow = (o, idx) => {
5900
5905
  const isSel = o.value === current;
5901
5906
  return /* @__PURE__ */ jsxs(
5902
5907
  "button",
@@ -5919,9 +5924,13 @@ function Combobox({
5919
5924
  onMouseDown: (e) => e.preventDefault(),
5920
5925
  onClick: () => commit(o.value),
5921
5926
  children: [
5922
- /* @__PURE__ */ jsxs("span", { className: "twc-opt__main", children: [
5923
- /* @__PURE__ */ jsx("span", { className: "twc-opt__label", children: o.label }),
5924
- o.description ? /* @__PURE__ */ jsx("span", { className: "twc-opt__desc", children: o.description }) : null
5927
+ renderOption ? /* @__PURE__ */ jsx("span", { className: "twc-opt__main", children: renderOption(o, { selected: isSel, active: idx === active }) }) : /* @__PURE__ */ jsxs(Fragment, { children: [
5928
+ o.icon != null ? /* @__PURE__ */ jsx("span", { className: "twc-opt__icon", "aria-hidden": "true", children: o.icon }) : null,
5929
+ /* @__PURE__ */ jsxs("span", { className: "twc-opt__main", children: [
5930
+ /* @__PURE__ */ jsx("span", { className: "twc-opt__label", children: o.label }),
5931
+ o.description ? /* @__PURE__ */ jsx("span", { className: "twc-opt__desc", children: o.description }) : null
5932
+ ] }),
5933
+ o.hint != null ? /* @__PURE__ */ jsx("span", { className: "twc-opt__hint", children: o.hint }) : null
5925
5934
  ] }),
5926
5935
  isSel ? /* @__PURE__ */ jsx("span", { className: "twc-opt__check", "aria-hidden": "true", children: /* @__PURE__ */ jsx("svg", { viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2.6", strokeLinecap: "round", strokeLinejoin: "round", children: /* @__PURE__ */ jsx("path", { d: "M20 6 9 17l-5-5" }) }) }) : null
5927
5936
  ]
@@ -5943,14 +5952,14 @@ function Combobox({
5943
5952
  "Loading\u2026"
5944
5953
  ] }) : visible.length === 0 ? /* @__PURE__ */ jsx("div", { className: "twc-pop__empty", children: emptyText }) : virtualized ? /* @__PURE__ */ jsxs(Fragment, { children: [
5945
5954
  vTop > 0 ? /* @__PURE__ */ jsx("div", { "aria-hidden": "true", style: { height: vTop } }) : null,
5946
- vShown.map((r2) => r2.kind === "group" ? /* @__PURE__ */ jsx("div", { className: "twc-pop__group", children: r2.label }, `g${r2.gi}`) : renderOption(r2.o, r2.idx)),
5955
+ vShown.map((r2) => r2.kind === "group" ? /* @__PURE__ */ jsx("div", { className: "twc-pop__group", children: r2.label }, `g${r2.gi}`) : renderRow(r2.o, r2.idx)),
5947
5956
  vBottom > 0 ? /* @__PURE__ */ jsx("div", { "aria-hidden": "true", style: { height: vBottom } }) : null
5948
5957
  ] }) : fGroups.map((g, gi) => /* @__PURE__ */ jsxs(React51__default.Fragment, { children: [
5949
5958
  g.group ? /* @__PURE__ */ jsx("div", { className: "twc-pop__group", children: g.group }) : null,
5950
5959
  g.options.map((o) => {
5951
5960
  counter += 1;
5952
5961
  const idx = counter;
5953
- return renderOption(o, idx);
5962
+ return renderRow(o, idx);
5954
5963
  })
5955
5964
  ] }, gi)) });
5956
5965
  const RD = { createPortal: typeof createPortal === "function" ? createPortal : typeof window !== "undefined" && window.ReactDOM && window.ReactDOM.createPortal };
@@ -6487,6 +6496,10 @@ var SELECT_CSS = `
6487
6496
  .twc-opt__main { display: flex; flex-direction: column; gap: 1px; min-width: 0; flex: 1; }
6488
6497
  .twc-opt__label { line-height: 1.3; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
6489
6498
  .twc-opt__desc { font-size: var(--text-xs); color: var(--color-text-muted); line-height: 1.3; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
6499
+ /* #326: optional leading icon/swatch + trailing (right-aligned) hint on an option. */
6500
+ .twc-opt__icon { flex: none; display: inline-flex; align-items: center; color: var(--color-text-muted); }
6501
+ .twc-opt__icon svg { width: 16px; height: 16px; }
6502
+ .twc-opt__hint { flex: none; margin-inline-start: auto; padding-inline-start: var(--space-2); color: var(--color-text-muted); font-size: var(--text-xs); white-space: nowrap; }
6490
6503
  .twc-opt__check { flex: none; color: var(--color-primary); display: inline-flex; }
6491
6504
  .twc-opt__check svg { width: 16px; height: 16px; }
6492
6505
  .twc-pop__empty { padding: 14px 12px; text-align: center; font-size: var(--text-sm); color: var(--color-text-subtle); }
@@ -6547,8 +6560,9 @@ function Select({
6547
6560
  loading = false,
6548
6561
  emptyText = "No results found",
6549
6562
  name,
6550
- virtualized = false,
6563
+ virtualized: virtualizedProp = false,
6551
6564
  overscan = 8,
6565
+ renderOption,
6552
6566
  id,
6553
6567
  className = "",
6554
6568
  onClick,
@@ -6556,6 +6570,8 @@ function Select({
6556
6570
  ...rest
6557
6571
  }) {
6558
6572
  const __twcStyles = useScopedStyles("twc-select-styles", SELECT_CSS);
6573
+ if (virtualizedProp && renderOption) warnOnce("select-renderoption-virtualized", "twico-ui Select: `renderOption` disables `virtualized` \u2014 custom rows aren't a fixed height.");
6574
+ const virtualized = virtualizedProp && !renderOption;
6559
6575
  const bufferRef = React51__default.useRef("");
6560
6576
  const bufferTimerRef = React51__default.useRef(null);
6561
6577
  const groups = React51__default.useMemo(() => normalizeGroups2(options), [options]);
@@ -6761,7 +6777,7 @@ function Select({
6761
6777
  const activeId = open && visible[active] ? optionId(active) : void 0;
6762
6778
  const descId = `${fieldId}-desc`;
6763
6779
  const describedBy = error || hint ? descId : void 0;
6764
- const renderOption = (o, idx) => {
6780
+ const renderRow = (o, idx) => {
6765
6781
  const isSel = o.value === current;
6766
6782
  return /* @__PURE__ */ jsxs(
6767
6783
  "button",
@@ -6783,9 +6799,13 @@ function Select({
6783
6799
  },
6784
6800
  onClick: () => commit(o.value),
6785
6801
  children: [
6786
- /* @__PURE__ */ jsxs("span", { className: "twc-opt__main", children: [
6787
- /* @__PURE__ */ jsx("span", { className: "twc-opt__label", children: o.label }),
6788
- o.description ? /* @__PURE__ */ jsx("span", { className: "twc-opt__desc", children: o.description }) : null
6802
+ renderOption ? /* @__PURE__ */ jsx("span", { className: "twc-opt__main", children: renderOption(o, { selected: isSel, active: idx === active }) }) : /* @__PURE__ */ jsxs(Fragment, { children: [
6803
+ o.icon != null ? /* @__PURE__ */ jsx("span", { className: "twc-opt__icon", "aria-hidden": "true", children: o.icon }) : null,
6804
+ /* @__PURE__ */ jsxs("span", { className: "twc-opt__main", children: [
6805
+ /* @__PURE__ */ jsx("span", { className: "twc-opt__label", children: o.label }),
6806
+ o.description ? /* @__PURE__ */ jsx("span", { className: "twc-opt__desc", children: o.description }) : null
6807
+ ] }),
6808
+ o.hint != null ? /* @__PURE__ */ jsx("span", { className: "twc-opt__hint", children: o.hint }) : null
6789
6809
  ] }),
6790
6810
  isSel ? /* @__PURE__ */ jsx("span", { className: "twc-opt__check", "aria-hidden": "true", children: /* @__PURE__ */ jsx("svg", { viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2.6", strokeLinecap: "round", strokeLinejoin: "round", children: /* @__PURE__ */ jsx("path", { d: "M20 6 9 17l-5-5" }) }) }) : null
6791
6811
  ]
@@ -6833,14 +6853,14 @@ function Select({
6833
6853
  "Loading\u2026"
6834
6854
  ] }) : visible.length === 0 ? /* @__PURE__ */ jsx("div", { className: "twc-pop__empty", children: emptyText }) : virtualized ? /* @__PURE__ */ jsxs(Fragment, { children: [
6835
6855
  vTop > 0 ? /* @__PURE__ */ jsx("div", { "aria-hidden": "true", style: { height: vTop } }) : null,
6836
- vShown.map((r2) => r2.kind === "group" ? /* @__PURE__ */ jsx("div", { className: "twc-pop__group", children: r2.label }, `g${r2.gi}`) : renderOption(r2.o, r2.idx)),
6856
+ vShown.map((r2) => r2.kind === "group" ? /* @__PURE__ */ jsx("div", { className: "twc-pop__group", children: r2.label }, `g${r2.gi}`) : renderRow(r2.o, r2.idx)),
6837
6857
  vBottom > 0 ? /* @__PURE__ */ jsx("div", { "aria-hidden": "true", style: { height: vBottom } }) : null
6838
6858
  ] }) : fGroups.map((g, gi) => /* @__PURE__ */ jsxs(React51__default.Fragment, { children: [
6839
6859
  g.group ? /* @__PURE__ */ jsx("div", { className: "twc-pop__group", children: g.group }) : null,
6840
6860
  g.options.map((o) => {
6841
6861
  counter += 1;
6842
6862
  const idx = counter;
6843
- return renderOption(o, idx);
6863
+ return renderRow(o, idx);
6844
6864
  })
6845
6865
  ] }, gi)) }),
6846
6866
  /* @__PURE__ */ jsx("div", { className: "twc-sel__sr", role: "status", "aria-live": "polite", children: loading ? "Loading\u2026" : visible.length === 0 ? emptyText : `${visible.length} result${visible.length === 1 ? "" : "s"}` })
@@ -7144,6 +7164,11 @@ var INPUT_EXTRA_CSS = `
7144
7164
  .twc-input__reveal:hover { background: var(--color-surface-sunken); color: var(--color-text); }
7145
7165
  .twc-input__reveal:focus-visible { outline: none; box-shadow: var(--ring); }
7146
7166
  .twc-input__reveal svg { width: 17px; height: 17px; }
7167
+ /* #328: trailing clear (X) button \u2014 mirrors the reveal toggle; sits alongside rightIcon / the password eye. */
7168
+ .twc-input__clear { display: inline-grid; place-items: center; width: 24px; height: 24px; padding: 0; border: none; background: transparent; color: var(--color-text-subtle); cursor: pointer; border-radius: var(--radius-sm); flex: none; transition: background-color var(--duration-fast) var(--ease-standard), color var(--duration-fast) var(--ease-standard); }
7169
+ .twc-input__clear:hover { background: var(--color-surface-sunken); color: var(--color-text); }
7170
+ .twc-input__clear:focus-visible { outline: none; box-shadow: var(--ring); }
7171
+ .twc-input__clear svg { width: 15px; height: 15px; }
7147
7172
  .twc-input__addon { align-self: stretch; display: inline-flex; align-items: center; padding-inline: var(--space-3);
7148
7173
  background: var(--color-surface-sunken); color: var(--color-text-muted); font-size: var(--text-sm);
7149
7174
  font-weight: var(--font-semibold); white-space: nowrap; flex: none; }
@@ -7165,6 +7190,7 @@ function Input({
7165
7190
  leftAddon,
7166
7191
  rightAddon,
7167
7192
  showCount = false,
7193
+ clearable = false,
7168
7194
  type = "text",
7169
7195
  disabled = false,
7170
7196
  id,
@@ -7186,8 +7212,25 @@ function Input({
7186
7212
  const near = showCounter && count >= Math.floor(max * 0.9);
7187
7213
  const handleChange = (e) => {
7188
7214
  rest.onChange?.(e);
7189
- if (showCount && !countControlled) setLen(e.target.value.length);
7215
+ if ((showCount || clearable) && !countControlled) setLen(e.target.value.length);
7190
7216
  };
7217
+ const inputRef = React51__default.useRef(null);
7218
+ const userRef = rest.ref;
7219
+ const setRef = React51__default.useCallback((node) => {
7220
+ inputRef.current = node;
7221
+ if (typeof userRef === "function") userRef(node);
7222
+ else if (userRef && typeof userRef === "object") userRef.current = node;
7223
+ }, [userRef]);
7224
+ const clearField = () => {
7225
+ const el = inputRef.current;
7226
+ if (!el) return;
7227
+ const setter = typeof HTMLInputElement !== "undefined" && Object.getOwnPropertyDescriptor(HTMLInputElement.prototype, "value")?.set;
7228
+ if (setter) setter.call(el, "");
7229
+ else el.value = "";
7230
+ el.dispatchEvent(new Event("input", { bubbles: true }));
7231
+ el.focus();
7232
+ };
7233
+ const showClear = clearable && count > 0 && !disabled && !rest.readOnly;
7191
7234
  const ariaInvalid = invalid ? true : rest["aria-invalid"] ?? void 0;
7192
7235
  const describedBy = [error || hint ? descId : null, showCounter ? countId : null, rest["aria-describedby"]].filter(Boolean).join(" ") || void 0;
7193
7236
  const isPassword = type === "password";
@@ -7234,11 +7277,16 @@ function Input({
7234
7277
  required: required || void 0,
7235
7278
  "aria-required": required || void 0,
7236
7279
  ...rest,
7280
+ ref: setRef,
7237
7281
  onChange: handleChange,
7238
7282
  "aria-invalid": ariaInvalid,
7239
7283
  "aria-describedby": describedBy
7240
7284
  }
7241
7285
  ),
7286
+ showClear ? /* @__PURE__ */ jsx("button", { type: "button", className: "twc-input__clear", "aria-label": "Clear", tabIndex: 0, onMouseDown: (e) => e.preventDefault(), onClick: clearField, children: /* @__PURE__ */ jsxs("svg", { viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", children: [
7287
+ /* @__PURE__ */ jsx("path", { d: "M18 6 6 18" }),
7288
+ /* @__PURE__ */ jsx("path", { d: "m6 6 12 12" })
7289
+ ] }) }) : null,
7242
7290
  suffix,
7243
7291
  rightAddon != null ? /* @__PURE__ */ jsx("span", { className: "twc-input__addon twc-input__addon--end", children: rightAddon }) : null
7244
7292
  ] }),
@@ -7326,6 +7374,10 @@ var MULTI_CSS = `
7326
7374
  .twc-opt__main { display: flex; flex-direction: column; gap: 1px; min-width: 0; flex: 1; }
7327
7375
  .twc-opt__label { line-height: 1.3; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
7328
7376
  .twc-opt__desc { font-size: var(--text-xs); color: var(--color-text-muted); line-height: 1.3; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
7377
+ /* #326: optional leading icon/swatch + trailing (right-aligned) hint on an option. */
7378
+ .twc-opt__icon { flex: none; display: inline-flex; align-items: center; color: var(--color-text-muted); }
7379
+ .twc-opt__icon svg { width: 16px; height: 16px; }
7380
+ .twc-opt__hint { flex: none; margin-inline-start: auto; padding-inline-start: var(--space-2); color: var(--color-text-muted); font-size: var(--text-xs); white-space: nowrap; }
7329
7381
  .twc-pop__empty { padding: 14px 12px; text-align: center; font-size: var(--text-sm); color: var(--color-text-subtle); }
7330
7382
  .twc-opt[data-disabled="true"] { opacity: 0.45; cursor: not-allowed; pointer-events: none; }
7331
7383
  .twc-pop__loading { display: flex; align-items: center; justify-content: center; gap: var(--space-2); padding: 14px 12px; font-size: var(--text-sm); color: var(--color-text-muted); }
@@ -7387,8 +7439,9 @@ function MultiSelect({
7387
7439
  maxTagCount,
7388
7440
  filter,
7389
7441
  onInputChange,
7390
- virtualized = false,
7442
+ virtualized: virtualizedProp = false,
7391
7443
  overscan = 8,
7444
+ renderOption,
7392
7445
  id,
7393
7446
  className = "",
7394
7447
  onFocus,
@@ -7396,6 +7449,8 @@ function MultiSelect({
7396
7449
  ...rest
7397
7450
  }) {
7398
7451
  const __twcStyles = useScopedStyles("twc-ms-styles", MULTI_CSS);
7452
+ if (virtualizedProp && renderOption) warnOnce("multiselect-renderoption-virtualized", "twico-ui MultiSelect: `renderOption` disables `virtualized` \u2014 custom rows aren't a fixed height.");
7453
+ const virtualized = virtualizedProp && !renderOption;
7399
7454
  const groups = React51__default.useMemo(() => normalizeGroups3(options), [options]);
7400
7455
  const flat = React51__default.useMemo(() => groups.flatMap((g) => g.options), [groups]);
7401
7456
  const autoId = React51__default.useId();
@@ -7555,7 +7610,7 @@ function MultiSelect({
7555
7610
  const activeId = open && visible[active] ? optionId(active) : void 0;
7556
7611
  const descId = `${fieldId}-desc`;
7557
7612
  const describedBy = error || hint ? descId : void 0;
7558
- const renderOption = (o, idx) => {
7613
+ const renderRow = (o, idx) => {
7559
7614
  const isSel = selected.includes(o.value);
7560
7615
  const optDisabled = isOptDisabled(o);
7561
7616
  return /* @__PURE__ */ jsxs(
@@ -7579,9 +7634,13 @@ function MultiSelect({
7579
7634
  onClick: () => toggle(o.value),
7580
7635
  children: [
7581
7636
  /* @__PURE__ */ jsx("span", { className: "twc-opt__box", "aria-hidden": "true", children: /* @__PURE__ */ jsx("svg", { viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "3.2", strokeLinecap: "round", strokeLinejoin: "round", children: /* @__PURE__ */ jsx("path", { d: "M20 6 9 17l-5-5" }) }) }),
7582
- /* @__PURE__ */ jsxs("span", { className: "twc-opt__main", children: [
7583
- /* @__PURE__ */ jsx("span", { className: "twc-opt__label", children: o.label }),
7584
- o.description ? /* @__PURE__ */ jsx("span", { className: "twc-opt__desc", children: o.description }) : null
7637
+ renderOption ? /* @__PURE__ */ jsx("span", { className: "twc-opt__main", children: renderOption(o, { selected: isSel, active: idx === active }) }) : /* @__PURE__ */ jsxs(Fragment, { children: [
7638
+ o.icon != null ? /* @__PURE__ */ jsx("span", { className: "twc-opt__icon", "aria-hidden": "true", children: o.icon }) : null,
7639
+ /* @__PURE__ */ jsxs("span", { className: "twc-opt__main", children: [
7640
+ /* @__PURE__ */ jsx("span", { className: "twc-opt__label", children: o.label }),
7641
+ o.description ? /* @__PURE__ */ jsx("span", { className: "twc-opt__desc", children: o.description }) : null
7642
+ ] }),
7643
+ o.hint != null ? /* @__PURE__ */ jsx("span", { className: "twc-opt__hint", children: o.hint }) : null
7585
7644
  ] })
7586
7645
  ]
7587
7646
  },
@@ -7611,14 +7670,14 @@ function MultiSelect({
7611
7670
  "Loading\u2026"
7612
7671
  ] }) : visible.length === 0 ? /* @__PURE__ */ jsx("div", { className: "twc-pop__empty", children: emptyText }) : virtualized ? /* @__PURE__ */ jsxs(Fragment, { children: [
7613
7672
  vTop > 0 ? /* @__PURE__ */ jsx("div", { "aria-hidden": "true", style: { height: vTop } }) : null,
7614
- vShown.map((r2) => r2.kind === "group" ? /* @__PURE__ */ jsx("div", { className: "twc-pop__group", children: r2.label }, `g${r2.gi}`) : renderOption(r2.o, r2.idx)),
7673
+ vShown.map((r2) => r2.kind === "group" ? /* @__PURE__ */ jsx("div", { className: "twc-pop__group", children: r2.label }, `g${r2.gi}`) : renderRow(r2.o, r2.idx)),
7615
7674
  vBottom > 0 ? /* @__PURE__ */ jsx("div", { "aria-hidden": "true", style: { height: vBottom } }) : null
7616
7675
  ] }) : fGroups.map((g, gi) => /* @__PURE__ */ jsxs(React51__default.Fragment, { children: [
7617
7676
  g.group ? /* @__PURE__ */ jsx("div", { className: "twc-pop__group", children: g.group }) : null,
7618
7677
  g.options.map((o) => {
7619
7678
  counter += 1;
7620
7679
  const idx = counter;
7621
- return renderOption(o, idx);
7680
+ return renderRow(o, idx);
7622
7681
  })
7623
7682
  ] }, gi))
7624
7683
  }
@@ -8202,6 +8261,11 @@ var DT_CSS = `
8202
8261
  font-size: var(--text-sm); color: var(--color-text); width: 150px; }
8203
8262
  .twc-dt__search input:focus, .twc-dt__search input:focus-visible { outline: none; box-shadow: none; }
8204
8263
  .twc-dt__search input::placeholder { color: var(--color-text-subtle); }
8264
+ /* #327: trailing clear (X) for the built-in search inputs (quick-search + column-menu search). */
8265
+ .twc-dt__search-clear { border: none; background: transparent; color: var(--color-text-subtle); cursor: pointer; display: inline-grid; place-items: center; width: 20px; height: 20px; padding: 0; border-radius: var(--radius-sm); flex: none; }
8266
+ .twc-dt__search-clear:hover { color: var(--color-text); background: var(--color-surface-sunken); }
8267
+ .twc-dt__search-clear:focus-visible { outline: none; box-shadow: var(--ring); }
8268
+ .twc-dt__search-clear svg { width: 14px; height: 14px; }
8205
8269
  .twc-dt__tlabel { display: inline; }
8206
8270
  /* Narrow grid: collapse the toolbar to icon-only buttons (labels survive as
8207
8271
  hover tooltips via data-tip) and let the search flex so nothing wraps. */
@@ -9570,6 +9634,8 @@ function Datatable({
9570
9634
  const colMenuRef = React51__default.useRef(null);
9571
9635
  const rowMenuRef = React51__default.useRef(null);
9572
9636
  const exportMenuRef = React51__default.useRef(null);
9637
+ const quickSearchRef = React51__default.useRef(null);
9638
+ const colSearchRef = React51__default.useRef(null);
9573
9639
  function focusFirstItem(container) {
9574
9640
  if (!container) return;
9575
9641
  const first = container.querySelector('[role="menuitem"]:not([disabled])');
@@ -11584,7 +11650,11 @@ function Datatable({
11584
11650
  ] }) : null,
11585
11651
  searchable ? /* @__PURE__ */ jsxs("div", { className: "twc-dt__search", children: [
11586
11652
  /* @__PURE__ */ jsx(Svg, { d: I.search }),
11587
- /* @__PURE__ */ jsx("input", { placeholder: "Search\u2026", "aria-label": "Search rows", value: quick, onChange: (e) => commitQuick(e.target.value) })
11653
+ /* @__PURE__ */ jsx("input", { ref: quickSearchRef, placeholder: "Search\u2026", "aria-label": "Search rows", value: quick, onChange: (e) => commitQuick(e.target.value) }),
11654
+ quick ? /* @__PURE__ */ jsx("button", { type: "button", className: "twc-dt__search-clear", "aria-label": "Clear search", onMouseDown: (e) => e.preventDefault(), onClick: () => {
11655
+ commitQuick("");
11656
+ quickSearchRef.current?.focus();
11657
+ }, children: /* @__PURE__ */ jsx(Svg, { d: I.x }) }) : null
11588
11658
  ] }) : null
11589
11659
  ] }),
11590
11660
  !pivotActive && activeGroupBy.length ? /* @__PURE__ */ jsxs("div", { className: "twc-dt__groupbar", children: [
@@ -12177,7 +12247,11 @@ function Datatable({
12177
12247
  ] }),
12178
12248
  /* @__PURE__ */ jsxs("div", { className: "twc-dt__col-search", children: [
12179
12249
  /* @__PURE__ */ jsx(Svg, { d: I.search }),
12180
- /* @__PURE__ */ jsx("input", { autoFocus: true, placeholder: "Find column\u2026", "aria-label": "Find column", value: colQuery, onChange: (e) => setColQuery(e.target.value) })
12250
+ /* @__PURE__ */ jsx("input", { ref: colSearchRef, autoFocus: true, placeholder: "Find column\u2026", "aria-label": "Find column", value: colQuery, onChange: (e) => setColQuery(e.target.value) }),
12251
+ colQuery ? /* @__PURE__ */ jsx("button", { type: "button", className: "twc-dt__search-clear", "aria-label": "Clear column search", onMouseDown: (e) => e.preventDefault(), onClick: () => {
12252
+ setColQuery("");
12253
+ colSearchRef.current?.focus();
12254
+ }, children: /* @__PURE__ */ jsx(Svg, { d: I.x }) }) : null
12181
12255
  ] }),
12182
12256
  /* @__PURE__ */ jsxs("div", { className: "twc-dt__col-list", children: [
12183
12257
  rowNumMatch ? /* @__PURE__ */ jsxs("div", { className: "twc-dt__col-row", onClick: () => setShowRowNum((v) => !v), children: [
@@ -12547,6 +12621,7 @@ function Datatable({
12547
12621
  Input,
12548
12622
  {
12549
12623
  size: "sm",
12624
+ clearable: true,
12550
12625
  type: filterTypeOf(col) === "number" ? "number" : "text",
12551
12626
  placeholder: "Value",
12552
12627
  value: f.value,
@@ -17635,6 +17710,12 @@ var TEXTAREA_CSS = `
17635
17710
  .twc-textarea__el:disabled { background: var(--color-surface-sunken); opacity: 0.7; cursor: not-allowed; }
17636
17711
  .twc-textarea__el[data-readonly] { background: var(--color-surface-sunken); }
17637
17712
  .twc-textarea__el[data-autosize="true"] { resize: none; overflow: hidden; }
17713
+ /* #328: clearable (X). Wrap positions the button top-inline-end so it dodges the bottom-right resize handle. */
17714
+ .twc-textarea__wrap { position: relative; display: block; }
17715
+ .twc-textarea__clear { position: absolute; top: 6px; inset-inline-end: 8px; display: inline-grid; place-items: center; width: 22px; height: 22px; padding: 0; border: none; background: var(--color-surface); color: var(--color-text-subtle); cursor: pointer; border-radius: var(--radius-sm); transition: background-color var(--duration-fast) var(--ease-standard), color var(--duration-fast) var(--ease-standard); }
17716
+ .twc-textarea__clear:hover { background: var(--color-surface-sunken); color: var(--color-text); }
17717
+ .twc-textarea__clear:focus-visible { outline: none; box-shadow: var(--ring); }
17718
+ .twc-textarea__clear svg { width: 15px; height: 15px; }
17638
17719
  .twc-field__footer { display: flex; align-items: center; gap: var(--space-2); }
17639
17720
  .twc-field__count { margin-inline-start: auto; font-size: var(--text-xs); color: var(--color-text-muted); font-variant-numeric: tabular-nums; flex: none; }
17640
17721
  .twc-field__count[data-danger="true"] { color: var(--color-danger-subtle-fg); }
@@ -17651,6 +17732,7 @@ var Textarea = React51__default.forwardRef(function Textarea2({
17651
17732
  minRows,
17652
17733
  maxRows,
17653
17734
  showCount = false,
17735
+ clearable = false,
17654
17736
  disabled = false,
17655
17737
  id,
17656
17738
  className = "",
@@ -17697,8 +17779,18 @@ var Textarea = React51__default.forwardRef(function Textarea2({
17697
17779
  };
17698
17780
  const handleChange = (e) => {
17699
17781
  rest.onChange?.(e);
17700
- if (showCount && !countControlled) setLen(e.target.value.length);
17782
+ if ((showCount || clearable) && !countControlled) setLen(e.target.value.length);
17783
+ };
17784
+ const clearField = () => {
17785
+ const el = innerRef.current;
17786
+ if (!el) return;
17787
+ const setter = typeof HTMLTextAreaElement !== "undefined" && Object.getOwnPropertyDescriptor(HTMLTextAreaElement.prototype, "value")?.set;
17788
+ if (setter) setter.call(el, "");
17789
+ else el.value = "";
17790
+ el.dispatchEvent(new Event("input", { bubbles: true }));
17791
+ el.focus();
17701
17792
  };
17793
+ const showClear = clearable && count > 0 && !disabled && !rest.readOnly;
17702
17794
  const ariaInvalid = invalid ? true : rest["aria-invalid"] ?? void 0;
17703
17795
  const describedBy = [error || hint ? descId : null, showCounter ? countId : null, rest["aria-describedby"]].filter(Boolean).join(" ") || void 0;
17704
17796
  return /* @__PURE__ */ jsxs("div", { className: `twc-field ${className}`, children: [
@@ -17707,27 +17799,33 @@ var Textarea = React51__default.forwardRef(function Textarea2({
17707
17799
  label,
17708
17800
  required ? /* @__PURE__ */ jsx("span", { className: "twc-field__req", children: "*" }) : null
17709
17801
  ] }) : null,
17710
- /* @__PURE__ */ jsx(
17711
- "textarea",
17712
- {
17713
- ref: setRef,
17714
- id: fieldId,
17715
- className: "twc-textarea__el",
17716
- "data-size": size,
17717
- "data-tone": tone,
17718
- "data-autosize": autosize || void 0,
17719
- "data-readonly": rest.readOnly || void 0,
17720
- rows: autosize ? minRows ?? rows : rows,
17721
- disabled,
17722
- required: required || void 0,
17723
- "aria-required": required || void 0,
17724
- ...rest,
17725
- onInput: handleInput,
17726
- onChange: handleChange,
17727
- "aria-invalid": ariaInvalid,
17728
- "aria-describedby": describedBy
17729
- }
17730
- ),
17802
+ /* @__PURE__ */ jsxs("div", { className: "twc-textarea__wrap", children: [
17803
+ /* @__PURE__ */ jsx(
17804
+ "textarea",
17805
+ {
17806
+ ref: setRef,
17807
+ id: fieldId,
17808
+ className: "twc-textarea__el",
17809
+ "data-size": size,
17810
+ "data-tone": tone,
17811
+ "data-autosize": autosize || void 0,
17812
+ "data-readonly": rest.readOnly || void 0,
17813
+ rows: autosize ? minRows ?? rows : rows,
17814
+ disabled,
17815
+ required: required || void 0,
17816
+ "aria-required": required || void 0,
17817
+ ...rest,
17818
+ onInput: handleInput,
17819
+ onChange: handleChange,
17820
+ "aria-invalid": ariaInvalid,
17821
+ "aria-describedby": describedBy
17822
+ }
17823
+ ),
17824
+ showClear ? /* @__PURE__ */ jsx("button", { type: "button", className: "twc-textarea__clear", "aria-label": "Clear", tabIndex: 0, onMouseDown: (e) => e.preventDefault(), onClick: clearField, children: /* @__PURE__ */ jsxs("svg", { viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", children: [
17825
+ /* @__PURE__ */ jsx("path", { d: "M18 6 6 18" }),
17826
+ /* @__PURE__ */ jsx("path", { d: "m6 6 12 12" })
17827
+ ] }) }) : null
17828
+ ] }),
17731
17829
  error || hint || showCounter ? /* @__PURE__ */ jsxs("div", { className: "twc-field__footer", children: [
17732
17830
  error ? /* @__PURE__ */ jsx("span", { id: descId, className: "twc-field__error", children: error }) : hint ? /* @__PURE__ */ jsx("span", { id: descId, className: "twc-field__hint", children: hint }) : null,
17733
17831
  showCounter ? /* @__PURE__ */ jsxs("span", { id: countId, className: "twc-field__count", "data-danger": near || void 0, "aria-live": "polite", children: [