@flikk/ui 1.0.0-beta.32 → 1.0.0-beta.33

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.
Files changed (54) hide show
  1. package/CHANGELOG.md +202 -0
  2. package/dist/components/ai/AgentRequest/AgentRequest.theme.js +1 -1
  3. package/dist/components/core/Button/Button.theme.js +3 -3
  4. package/dist/components/core/Pill/Pill.js +32 -17
  5. package/dist/components/core/Pill/Pill.types.d.ts +14 -1
  6. package/dist/components/core/SlidingNumber/SlidingNumber.d.ts +8 -0
  7. package/dist/components/core/SlidingNumber/SlidingNumber.js +12 -3
  8. package/dist/components/forms/ColorPicker/ColorPicker.js +45 -3
  9. package/dist/components/forms/ColorPicker/ColorPickerBody.js +8 -4
  10. package/dist/components/forms/Combobox/Combobox.js +8 -4
  11. package/dist/components/forms/Combobox/Combobox.theme.js +9 -0
  12. package/dist/components/forms/Combobox/Combobox.types.d.ts +13 -0
  13. package/dist/components/forms/DatePicker/DatePicker.js +19 -12
  14. package/dist/components/forms/DatePicker/DatePicker.theme.js +5 -0
  15. package/dist/components/forms/DatePicker/DatePicker.types.d.ts +8 -0
  16. package/dist/components/forms/DatePicker/DatePickerContext.d.ts +0 -1
  17. package/dist/components/forms/DatePicker/DatePickerTrigger.js +5 -3
  18. package/dist/components/forms/DateRangePicker/DateRangePicker.theme.js +5 -0
  19. package/dist/components/forms/DateRangePicker/DateRangePicker.types.d.ts +8 -0
  20. package/dist/components/forms/DateRangePicker/DateRangePickerTrigger.js +5 -3
  21. package/dist/components/forms/Input/Input.js +17 -12
  22. package/dist/components/forms/Input/Input.theme.js +10 -0
  23. package/dist/components/forms/Input/Input.types.d.ts +14 -0
  24. package/dist/components/forms/InputAddress/InputAddress.js +5 -5
  25. package/dist/components/forms/InputAddress/InputAddress.types.d.ts +7 -0
  26. package/dist/components/forms/InputCounter/InputCounter.js +194 -17
  27. package/dist/components/forms/InputCounter/InputCounter.theme.d.ts +12 -0
  28. package/dist/components/forms/InputCounter/InputCounter.theme.js +92 -2
  29. package/dist/components/forms/InputCounter/InputCounter.types.d.ts +47 -4
  30. package/dist/components/forms/InputCreditCard/InputCreditCard.js +4 -4
  31. package/dist/components/forms/InputCreditCard/InputCreditCard.types.d.ts +7 -0
  32. package/dist/components/forms/Mention/Mention.js +13 -11
  33. package/dist/components/forms/RichTextEditor/RichTextEditor.js +12 -3
  34. package/dist/components/forms/Select/Select.js +15 -8
  35. package/dist/components/forms/Select/Select.theme.js +9 -1
  36. package/dist/components/forms/Select/Select.types.d.ts +16 -1
  37. package/dist/components/forms/Textarea/Textarea.js +14 -25
  38. package/dist/components/forms/TimePicker/TimePicker.theme.js +9 -0
  39. package/dist/components/forms/TimePicker/TimePicker.types.d.ts +11 -0
  40. package/dist/components/forms/TimePicker/TimePickerTrigger.js +7 -5
  41. package/dist/components/forms/forms.theme.d.ts +57 -0
  42. package/dist/components/forms/forms.theme.js +83 -8
  43. package/dist/components/generative/registry.js +1 -0
  44. package/dist/components/generative/schema.generated.js +21 -1
  45. package/dist/generative.schema.json +21 -1
  46. package/dist/registry.json +108 -17
  47. package/dist/styles.css +1 -1
  48. package/dist/tools.json +21 -1
  49. package/dist/utils/composeEventHandlers.d.ts +19 -0
  50. package/dist/utils/composeEventHandlers.js +26 -0
  51. package/dist/utils/composeEventHandlers.test.d.ts +1 -0
  52. package/dist/utils/index.d.ts +1 -0
  53. package/package.json +1 -1
  54. package/src/styles/theme.css +33 -9
@@ -5,6 +5,7 @@ import { createPortal } from 'react-dom';
5
5
  import { AnimatePresence, motion } from 'motion/react';
6
6
  import { mentionTheme } from './Mention.theme.js';
7
7
  import { cn } from '../../../utils/cn.js';
8
+ import { composeEventHandlers } from '../../../utils/composeEventHandlers.js';
8
9
  import { Avatar } from '../../core/Avatar/Avatar.js';
9
10
  import { Tag } from '../../core/Tag/Tag.js';
10
11
  import { MenuItem } from '../../core/MenuItem/MenuItem.js';
@@ -77,12 +78,15 @@ const dropdownVariants = {
77
78
  // ---------------------------------------------------------------------------
78
79
  // Mention component
79
80
  // ---------------------------------------------------------------------------
80
- const Mention = React__default.forwardRef(({ triggers: triggersProp, suggestions: suggestionsProp, onSearch: onSearchProp, value: controlledValue, defaultValue = "", onChange, onMentionSelect, onMentionsChange, placeholder, size = "md", maxSuggestions = 8, className, wrapperClassName, dropdownClassName, theme: themeOverrides, state = "default", disabled = false, helperText, required, label, renderSuggestion, emptyMessage = "No results found", loading = false, mentionTagSize = "sm", mentionTagClassName, mentionTagRemovable = true, rows = 3, style: styleProp, elevation, lift, ...props }, ref) => {
81
+ const Mention = React__default.forwardRef(({ triggers: triggersProp, suggestions: suggestionsProp, onSearch: onSearchProp, value: controlledValue, defaultValue = "", onChange, onMentionSelect, onMentionsChange, placeholder, size = "md", maxSuggestions = 8, className, wrapperClassName, dropdownClassName, theme: themeOverrides, state = "default", disabled = false, helperText, required, label, renderSuggestion, emptyMessage = "No results found", loading = false, mentionTagSize = "sm", mentionTagClassName, mentionTagRemovable = true, rows = 3, style: styleProp, id: idProp, onInput: onInputProp, onKeyDown: onKeyDownProp, onClick: onClickProp, onFocus: onFocusProp, onBlur: onBlurProp, onPaste: onPasteProp, onSelect: onSelectProp, onCompositionStart: onCompositionStartProp, onCompositionEnd: onCompositionEndProp, elevation, lift, ...props }, ref) => {
81
82
  // Relative elevation: the suggestion dropdown lifts above its host surface
82
83
  // (overlay base = 5) so a Mention inside a Popover/Modal still reads higher.
83
84
  const surfaceLevel = useSurfaceElevation({ baseRung: 5, offset: 2, elevation, lift });
84
85
  const theme = useMemo(() => ({ ...mentionTheme, ...(themeOverrides || {}) }), [themeOverrides]);
85
- const generatedId = useId();
86
+ const autoId = useId();
87
+ // `id` is destructured rather than left to `...props`: the spread lands
88
+ // BEFORE this element's own `id`, so a consumer value would be overwritten.
89
+ const generatedId = idProp !== null && idProp !== void 0 ? idProp : autoId;
86
90
  const listboxId = `${generatedId}-listbox`;
87
91
  const isDisabled = state === "disabled" || disabled;
88
92
  const isInvalid = state === "invalid";
@@ -583,17 +587,13 @@ const Mention = React__default.forwardRef(({ triggers: triggersProp, suggestions
583
587
  const text = e.clipboardData.getData("text/plain");
584
588
  document.execCommand("insertText", false, text);
585
589
  }, []);
586
- const handleFocus = useCallback((e) => {
587
- var _a;
590
+ const handleFocus = useCallback((_e) => {
588
591
  setIsFocused(true);
589
- (_a = props.onFocus) === null || _a === void 0 ? void 0 : _a.call(props, e);
590
- }, [props.onFocus]);
591
- const handleBlur = useCallback((e) => {
592
- var _a;
592
+ }, []);
593
+ const handleBlur = useCallback((_e) => {
593
594
  setSelectedMentionIndex(null);
594
595
  setIsFocused(false);
595
- (_a = props.onBlur) === null || _a === void 0 ? void 0 : _a.call(props, e);
596
- }, [props.onBlur]);
596
+ }, []);
597
597
  const handleCompositionStart = useCallback(() => {
598
598
  composingRef.current = true;
599
599
  }, []);
@@ -702,7 +702,9 @@ const Mention = React__default.forwardRef(({ triggers: triggersProp, suggestions
702
702
  const helperId = `${generatedId}-helper`;
703
703
  const labelId = `${generatedId}-label`;
704
704
  return (jsxs("div", { className: cn(theme.wrapperStyle, wrapperClassName), children: [label && (jsx(FormLabel, { id: labelId, htmlFor: generatedId, state: isDisabled ? "disabled" : isInvalid ? "invalid" : "default", required: required, className: "mb-1.5", children: label })), jsx("div", { ref: wrapperRef, className: cn(theme.inputGroupStyle, isInvalid && theme.invalidStyle, isFocused &&
705
- (isInvalid ? theme.invalidFocusStyle : theme.inputGroupFocusStyle)), children: createElement("div", { ...props, key: editorKey, ref: setEditableRef, id: generatedId, contentEditable: !isDisabled, suppressContentEditableWarning: true, onInput: handleInput, onKeyDown: handleKeyDown, onClick: handleClick, onFocus: handleFocus, onBlur: handleBlur, onPaste: handlePaste, onSelect: handleSelect, onCompositionStart: handleCompositionStart, onCompositionEnd: handleCompositionEnd, role: "combobox", "aria-autocomplete": "list", "aria-expanded": isDropdownOpen, "aria-haspopup": "listbox", "aria-controls": listboxId, "aria-placeholder": placeholder, "aria-required": required || undefined, "data-placeholder": placeholder, "aria-disabled": isDisabled || undefined, "aria-invalid": isInvalid || undefined, "aria-describedby": helperText ? helperId : undefined, "aria-labelledby": label ? labelId : undefined, className: cn(theme.editableStyle, theme.sizes[size], isContentEmpty &&
705
+ (isInvalid ? theme.invalidFocusStyle : theme.inputGroupFocusStyle)), children: createElement("div", { ...props, key: editorKey, ref: setEditableRef, id: generatedId, contentEditable: !isDisabled, suppressContentEditableWarning: true,
706
+ // 🔴 §9: composed, not clobbered — ours first, then the consumer's.
707
+ onInput: composeEventHandlers(handleInput, onInputProp), onKeyDown: composeEventHandlers(handleKeyDown, onKeyDownProp), onClick: composeEventHandlers(handleClick, onClickProp), onFocus: composeEventHandlers(handleFocus, onFocusProp), onBlur: composeEventHandlers(handleBlur, onBlurProp), onPaste: composeEventHandlers(handlePaste, onPasteProp), onSelect: composeEventHandlers(handleSelect, onSelectProp), onCompositionStart: composeEventHandlers(handleCompositionStart, onCompositionStartProp), onCompositionEnd: composeEventHandlers(handleCompositionEnd, onCompositionEndProp), role: "combobox", "aria-autocomplete": "list", "aria-expanded": isDropdownOpen, "aria-haspopup": "listbox", "aria-controls": listboxId, "aria-placeholder": placeholder, "aria-required": required || undefined, "data-placeholder": placeholder, "aria-disabled": isDisabled || undefined, "aria-invalid": isInvalid || undefined, "aria-describedby": helperText ? helperId : undefined, "aria-labelledby": label ? labelId : undefined, className: cn(theme.editableStyle, theme.sizes[size], isContentEmpty &&
706
708
  "before:content-[attr(data-placeholder)] before:text-[var(--color-text-placeholder)] before:pointer-events-none before:absolute", isDisabled && "cursor-not-allowed opacity-60", className), style: {
707
709
  minHeight: rows ? `${rows * 1.5}em` : undefined,
708
710
  ...styleProp,
@@ -3,6 +3,7 @@ import { jsxs, jsx } from 'react/jsx-runtime';
3
3
  import React__default, { useRef, useId, useState, useCallback, useEffect } from 'react';
4
4
  import { FormLabel } from '../FormLabel/FormLabel.js';
5
5
  import { cn } from '../../../utils/cn.js';
6
+ import { composeEventHandlers } from '../../../utils/composeEventHandlers.js';
6
7
  import { richTextEditorTheme } from './RichTextEditor.theme.js';
7
8
  import { DEFAULT_TOOLBAR_TOOLS, RichTextToolbar } from './RichTextToolbar.js';
8
9
  import { RichTextLinkField } from './RichTextLinkField.js';
@@ -24,11 +25,15 @@ import { redo, undo, clearFormatting, toggleCodeBlock, toggleBlockquote, toggleO
24
25
  *
25
26
  * 🔴 Per §9, `className`, `ref` and `...rest` target the contentEditable surface.
26
27
  */
27
- const RichTextEditor = React__default.forwardRef(({ size = "md", state = "default", label, helperText, errorMessage, value, onChange, placeholder, className, wrapperClassName, containerClassName, toolbarClassName, labelClassName, helperTextClassName, required = false, minHeight = 160, maxHeight, toolbar, theme: themeOverrides, sanitize, ...rest }, ref) => {
28
+ const RichTextEditor = React__default.forwardRef(({ size = "md", state = "default", label, helperText, errorMessage, value, onChange, placeholder, className, wrapperClassName, containerClassName, toolbarClassName, labelClassName, helperTextClassName, required = false, minHeight = 160, maxHeight, toolbar, theme: themeOverrides, sanitize, id: idProp, style: styleProp, onInput: onInputProp, onPaste: onPasteProp, onDrop: onDropProp, onKeyUp: onKeyUpProp, onMouseUp: onMouseUpProp, onBlur: onBlurProp, ...rest }, ref) => {
28
29
  var _a;
29
30
  const theme = { ...richTextEditorTheme, ...themeOverrides };
30
31
  const editorRef = useRef(null);
31
- const generatedId = useId();
32
+ const autoId = useId();
33
+ // 🔴 `id` and `style` are destructured rather than left to `...rest`: the
34
+ // spread lands BEFORE this element's own attributes, so anything the
35
+ // component also sets would silently overwrite the consumer's value.
36
+ const generatedId = idProp !== null && idProp !== void 0 ? idProp : autoId;
32
37
  const describedById = `${generatedId}-description`;
33
38
  const isDisabled = state === "disabled";
34
39
  const isInvalid = state === "invalid";
@@ -193,7 +198,11 @@ const RichTextEditor = React__default.forwardRef(({ size = "md", state = "defaul
193
198
  const message = isInvalid && errorMessage ? errorMessage : helperText;
194
199
  const hasMessage = Boolean(message);
195
200
  const restAriaLabel = rest["aria-label"];
196
- return (jsxs("div", { className: cn(theme.wrapperStyle, wrapperClassName), children: [label && (jsx(FormLabel, { htmlFor: generatedId, state: state, required: required, className: labelClassName, children: label })), jsxs("div", { className: cn(theme.containerStyle, theme.containerStates[state], containerClassName), children: [showToolbar && (jsx(RichTextToolbar, { tools: tools, formatting: formatting, disabled: isDisabled, size: size, className: cn(toolbar === null || toolbar === void 0 ? void 0 : toolbar.className, toolbarClassName), onCommand: handleToolCommand, onBlockChange: handleBlockChange })), isLinkFieldOpen && !isDisabled && (jsx(RichTextLinkField, { initialHref: formatting.link, onApply: applyLink, onRemove: removeLink$1, onCancel: cancelLink })), jsx("div", { ...rest, id: generatedId, ref: setRefs, role: "textbox", "aria-multiline": "true", "aria-label": typeof label === "string" ? label : restAriaLabel !== null && restAriaLabel !== void 0 ? restAriaLabel : "Rich text editor", "aria-required": required || undefined, "aria-invalid": isInvalid || undefined, "aria-describedby": hasMessage ? describedById : undefined, "aria-disabled": isDisabled || undefined, contentEditable: !isDisabled, suppressContentEditableWarning: true, "data-placeholder": placeholder, "data-empty": isEmpty || undefined, tabIndex: isDisabled ? -1 : 0, style: { minHeight, maxHeight }, className: cn(theme.editorStyle, theme.editorSizes[size], isEmpty && placeholder && theme.placeholderStyle, className), onInput: emitChange, onPaste: handlePaste, onDrop: handleDrop, onKeyUp: syncFormatting, onMouseUp: syncFormatting, onBlur: saveSelection })] }), hasMessage && (jsx("div", { id: describedById, className: cn(isInvalid && errorMessage ? theme.errorTextStyle : theme.helperTextStyle, helperTextClassName), children: message }))] }));
201
+ return (jsxs("div", { className: cn(theme.wrapperStyle, wrapperClassName), children: [label && (jsx(FormLabel, { htmlFor: generatedId, state: state, required: required, className: labelClassName, children: label })), jsxs("div", { className: cn(theme.containerStyle, theme.containerStates[state], containerClassName), children: [showToolbar && (jsx(RichTextToolbar, { tools: tools, formatting: formatting, disabled: isDisabled, size: size, className: cn(toolbar === null || toolbar === void 0 ? void 0 : toolbar.className, toolbarClassName), onCommand: handleToolCommand, onBlockChange: handleBlockChange })), isLinkFieldOpen && !isDisabled && (jsx(RichTextLinkField, { initialHref: formatting.link, onApply: applyLink, onRemove: removeLink$1, onCancel: cancelLink })), jsx("div", { ...rest, id: generatedId, ref: setRefs, role: "textbox", "aria-multiline": "true", "aria-label": typeof label === "string" ? label : restAriaLabel !== null && restAriaLabel !== void 0 ? restAriaLabel : "Rich text editor", "aria-required": required || undefined, "aria-invalid": isInvalid || undefined, "aria-describedby": hasMessage ? describedById : undefined, "aria-disabled": isDisabled || undefined, contentEditable: !isDisabled, suppressContentEditableWarning: true, "data-placeholder": placeholder, "data-empty": isEmpty || undefined, tabIndex: isDisabled ? -1 : 0, style: { minHeight, maxHeight, ...styleProp }, className: cn(theme.editorStyle, theme.editorSizes[size], isEmpty && placeholder && theme.placeholderStyle, className),
202
+ // 🔴 §9: composed, not clobbered. These handlers are load-bearing
203
+ // (selection tracking, sanitize-on-paste), so a consumer's must run
204
+ // ALONGSIDE ours — ours first — never instead of them.
205
+ onInput: composeEventHandlers(emitChange, onInputProp), onPaste: composeEventHandlers(handlePaste, onPasteProp), onDrop: composeEventHandlers(handleDrop, onDropProp), onKeyUp: composeEventHandlers(syncFormatting, onKeyUpProp), onMouseUp: composeEventHandlers(syncFormatting, onMouseUpProp), onBlur: composeEventHandlers(saveSelection, onBlurProp) })] }), hasMessage && (jsx("div", { id: describedById, className: cn(isInvalid && errorMessage ? theme.errorTextStyle : theme.helperTextStyle, helperTextClassName), children: message }))] }));
197
206
  });
198
207
  RichTextEditor.displayName = "RichTextEditor";
199
208
 
@@ -77,7 +77,7 @@ function SelectProvider({ children, selectState, size = "md", componentState = "
77
77
  * Select Button Component
78
78
  */
79
79
  const SelectButton = ({ id, disabled, isOpen, state = "default", size = "md", iconStart, iconEnd, contentStart, contentEnd, className = "", onClick, onFocus, onBlur, onKeyDown, children, "aria-invalid": ariaInvalid, "aria-describedby": ariaDescribedby, "aria-labelledby": ariaLabelledby, "aria-label": ariaLabel, }) => {
80
- var _a, _b, _c, _d, _e, _f;
80
+ var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k;
81
81
  const { theme, buttonRef } = useSelectContext();
82
82
  const baseStyle = theme.baseStyle || "";
83
83
  const sizeStyle = ((_a = theme.sizes) === null || _a === void 0 ? void 0 : _a[size]) || "";
@@ -86,10 +86,16 @@ const SelectButton = ({ id, disabled, isOpen, state = "default", size = "md", ic
86
86
  const focusStateStyle = ((_d = theme.focusStates) === null || _d === void 0 ? void 0 : _d[state]) || "";
87
87
  const buttonWrapperStyle = theme.buttonWrapperStyle || "";
88
88
  const buttonStyle = theme.buttonStyle || "";
89
- const iconStartStyle = theme.iconStartStyle || "";
90
- const iconEndStyle = theme.iconEndStyle || "";
91
- const iconStartPadding = iconStart ? ((_e = theme.iconPadding) === null || _e === void 0 ? void 0 : _e.left) || "" : "";
92
- const iconEndPadding = iconEnd ? ((_f = theme.iconPadding) === null || _f === void 0 ? void 0 : _f.right) || "" : "";
89
+ const iconStartStyle = ((_e = theme.iconStartStyles) === null || _e === void 0 ? void 0 : _e[size]) || theme.iconStartStyle || "";
90
+ // Size-aware slot/runway, falling back to the flat keys for a consumer theme
91
+ // that only overrode those.
92
+ const iconEndStyle = ((_f = theme.iconEndStyles) === null || _f === void 0 ? void 0 : _f[size]) || theme.iconEndStyle || "";
93
+ const iconStartPadding = iconStart
94
+ ? ((_g = theme.iconStartPaddings) === null || _g === void 0 ? void 0 : _g[size]) || ((_h = theme.iconPadding) === null || _h === void 0 ? void 0 : _h.left) || ""
95
+ : "";
96
+ const iconEndPadding = iconEnd
97
+ ? ((_j = theme.iconEndPaddings) === null || _j === void 0 ? void 0 : _j[size]) || ((_k = theme.iconPadding) === null || _k === void 0 ? void 0 : _k.right) || ""
98
+ : "";
93
99
  // Size-aware content slot style (mirrors Input.theme contentStyle)
94
100
  const contentSlotStyle = `flex items-center px-3 ${size === "sm"
95
101
  ? "text-sm"
@@ -368,7 +374,7 @@ const SelectOption = ({ value, isSelected, disabled, className = "", onSelect, c
368
374
  * ```
369
375
  */
370
376
  const SelectInner = ({ id, name, size = "md", state = "default", value, displayValue, placeholder = "Select an option", disabled = false, required = false, options = [], label, helperText, onChange, onFocus, onBlur, iconStart, iconEnd, contentStart, contentEnd, theme: themeOverrides, className = "", wrapperClassName = "", searchable = false, searchPlaceholder = "Search...", portal = true, placement = "bottom-start", offset = 8, enableTypeahead = true, creatable = false, onCreateOption, createLabel, multiple = false, maxDisplayedChips = 3, elevation, lift, "aria-label": ariaLabel, children, }, ref) => {
371
- var _a;
377
+ var _a, _b;
372
378
  // Merge theme with overrides
373
379
  const theme = useMemo(() => ({ ...selectTheme, ...(themeOverrides || {}) }), [themeOverrides]);
374
380
  // Use the extracted state hook
@@ -426,8 +432,9 @@ const SelectInner = ({ id, name, size = "md", state = "default", value, displayV
426
432
  const helperTextStyle = theme.helperTextStyle || "";
427
433
  // Container style
428
434
  const containerStyle = theme.containerStyle || "";
429
- // Default chevron icon if no iconEnd provided
430
- const defaultChevronIcon = jsx(CaretUpDown, { className: "size-4" });
435
+ // Default chevron icon if no iconEnd provided. Its size ramps with the
436
+ // control a flat size-4 out-weighed the 14px text at `sm`.
437
+ const defaultChevronIcon = (jsx(CaretUpDown, { className: ((_b = theme.iconEndSizes) === null || _b === void 0 ? void 0 : _b[size]) || "size-4" }));
431
438
  // Generate a unique ID if not provided
432
439
  const generatedId = React__default.useId();
433
440
  const selectId = id || generatedId;
@@ -51,8 +51,16 @@ const selectTheme = {
51
51
  helperTextStates: formsBaseTheme.helperTextStates,
52
52
  // Left icon style - inherited from common form theme
53
53
  iconStartStyle: formsBaseTheme.iconStyles.left,
54
- // Right icon style - inherited from common form theme
54
+ iconStartStyles: formsBaseTheme.iconStyles.leftSizes,
55
+ iconStartPaddings: formsBaseTheme.iconStyles.paddingLeftSizes,
56
+ // Right icon (the chevron) — size-aware: the slot insets from the control's
57
+ // own --form-px-*, the glyph ramps 12/14/16, and the text runway is reserved
58
+ // to match. Flat `pr-3` + `size-4` at every size left the sm chevron both
59
+ // oversized against 14px text and further from the edge than it looked.
55
60
  iconEndStyle: formsBaseTheme.iconStyles.right,
61
+ iconEndStyles: formsBaseTheme.iconStyles.rightSizes,
62
+ iconEndSizes: formsBaseTheme.iconStyles.sizes,
63
+ iconEndPaddings: formsBaseTheme.iconStyles.paddingRightSizes,
56
64
  // Check icon style for selected options
57
65
  checkIconStyle: "absolute inset-y-0 right-0 flex items-center pr-3 text-[var(--color-primary)] " +
58
66
  "dark:text-[var(--color-primary-400)]",
@@ -303,8 +303,23 @@ export interface SelectThemeOverrides {
303
303
  helperTextStates?: Partial<Record<SelectState, string>>;
304
304
  /** Left icon style */
305
305
  iconStartStyle?: string;
306
- /** Right icon style */
306
+ /** Per-size left icon slot style (position + inset) */
307
+ iconStartStyles?: Partial<Record<SelectSize, string>>;
308
+ /** Per-size text runway reserved for the left icon */
309
+ iconStartPaddings?: Partial<Record<SelectSize, string>>;
310
+ /**
311
+ * Right icon slot style.
312
+ * @deprecated superseded by `iconEndStyles`, which insets the slot from the
313
+ * control's own `--form-px-*` instead of a flat `pr-3` at every size. Still
314
+ * honoured as a fallback when `iconEndStyles` has no entry for the size.
315
+ */
307
316
  iconEndStyle?: string;
317
+ /** Per-size right icon slot style (position + inset) */
318
+ iconEndStyles?: Partial<Record<SelectSize, string>>;
319
+ /** Per-size sizing for the built-in chevron */
320
+ iconEndSizes?: Partial<Record<SelectSize, string>>;
321
+ /** Per-size text runway reserved for the right icon */
322
+ iconEndPaddings?: Partial<Record<SelectSize, string>>;
308
323
  /** Style for the check icon in selected options */
309
324
  checkIconStyle?: string;
310
325
  /** Icon padding styles */
@@ -1,13 +1,12 @@
1
1
  "use client";
2
2
  import { jsx, jsxs } from 'react/jsx-runtime';
3
- import React__default, { useState } from 'react';
3
+ import React__default from 'react';
4
4
  import { FormLabel } from '../FormLabel/FormLabel.js';
5
5
  import { textareaTheme } from './Textarea.theme.js';
6
6
  import { cn } from '../../../utils/cn.js';
7
7
 
8
8
  const Textarea = React__default.forwardRef(({ size = "md", state = "default", className = "", label, labelClassName = "", helperText, errorMessage, helperTextClassName = "", wrapperClassName = "", inputGroupClassName = "", id, theme = {}, required, rows = 3, cols, unstyled = false, onValueChange, ...props }, ref) => {
9
9
  var _a, _b, _c, _d;
10
- const [isFocused, setIsFocused] = useState(false);
11
10
  const generatedId = React__default.useId();
12
11
  const inputId = id || generatedId;
13
12
  // Derived state flags for internal use
@@ -16,17 +15,20 @@ const Textarea = React__default.forwardRef(({ size = "md", state = "default", cl
16
15
  // errorMessage takes precedence over helperText while invalid; helperText
17
16
  // behavior is unchanged otherwise.
18
17
  const displayedMessage = isInvalid && errorMessage ? errorMessage : helperText;
18
+ // Pull onChange out of props so the explicit handler below isn't overwritten
19
+ // by the spread, and wire it ONCE — 🔴 both render paths must use it. The
20
+ // `unstyled` early return below used to drop `onValueChange` entirely, so
21
+ // `value` + `onValueChange` + `unstyled` produced a permanently read-only
22
+ // textarea: React saw a controlled value with no onChange, warned, and
23
+ // nothing the user typed ever reached the consumer.
24
+ const { onChange: propsOnChange, ...restProps } = props;
25
+ const handleChange = (event) => {
26
+ propsOnChange === null || propsOnChange === void 0 ? void 0 : propsOnChange(event);
27
+ onValueChange === null || onValueChange === void 0 ? void 0 : onValueChange(event.target.value);
28
+ };
19
29
  // If unstyled, render only the textarea element
20
30
  if (unstyled) {
21
- return (jsx("textarea", { ref: ref, id: inputId, className: className, disabled: isDisabled, "aria-invalid": isInvalid, required: required, rows: rows, cols: cols, onFocus: (e) => {
22
- var _a;
23
- setIsFocused(true);
24
- (_a = props.onFocus) === null || _a === void 0 ? void 0 : _a.call(props, e);
25
- }, onBlur: (e) => {
26
- var _a;
27
- setIsFocused(false);
28
- (_a = props.onBlur) === null || _a === void 0 ? void 0 : _a.call(props, e);
29
- }, ...props }));
31
+ return (jsx("textarea", { ref: ref, id: inputId, className: className, disabled: isDisabled, "aria-invalid": isInvalid, required: required, rows: rows, cols: cols, onChange: handleChange, ...restProps }));
30
32
  }
31
33
  // State class for CSS selectors
32
34
  const stateClass = isInvalid
@@ -34,8 +36,6 @@ const Textarea = React__default.forwardRef(({ size = "md", state = "default", cl
34
36
  : isDisabled
35
37
  ? "state-disabled"
36
38
  : "state-default";
37
- // Pull onChange out of props so the explicit handler below isn't overwritten by the spread
38
- const { onChange: propsOnChange, ...restProps } = props;
39
39
  // Get theme classes
40
40
  const wrapperClasses = theme.wrapperStyle || textareaTheme.wrapperStyle || "";
41
41
  const inputGroupClasses = theme.inputGroupStyle || textareaTheme.inputGroupStyle || "";
@@ -43,18 +43,7 @@ const Textarea = React__default.forwardRef(({ size = "md", state = "default", cl
43
43
  const sizeClasses = ((_a = theme.sizes) === null || _a === void 0 ? void 0 : _a[size]) || ((_b = textareaTheme.sizes) === null || _b === void 0 ? void 0 : _b[size]) || "";
44
44
  const stateClasses = ((_c = theme.states) === null || _c === void 0 ? void 0 : _c[state]) || ((_d = textareaTheme.states) === null || _d === void 0 ? void 0 : _d[state]) || "";
45
45
  const helperTextClasses = theme.helperText || textareaTheme.helperText || "";
46
- return (jsxs("div", { className: cn(wrapperClasses, stateClass, wrapperClassName), children: [label && (typeof label === 'string' ? (jsx("div", { className: "mb-1", children: jsx(FormLabel, { htmlFor: inputId, className: labelClassName, state: state, required: required, children: label }) })) : (label)), jsx("div", { children: jsx("div", { className: cn(inputGroupClasses, stateClasses, inputGroupClassName), children: jsx("div", { className: "relative flex-1", children: jsx("textarea", { ref: ref, id: inputId, className: cn(baseClasses, sizeClasses, className), disabled: isDisabled, "aria-invalid": isInvalid, "aria-describedby": displayedMessage ? `${inputId}-helper` : undefined, required: required, rows: rows, cols: cols, onFocus: (e) => {
47
- var _a;
48
- setIsFocused(true);
49
- (_a = props.onFocus) === null || _a === void 0 ? void 0 : _a.call(props, e);
50
- }, onBlur: (e) => {
51
- var _a;
52
- setIsFocused(false);
53
- (_a = props.onBlur) === null || _a === void 0 ? void 0 : _a.call(props, e);
54
- }, onChange: (e) => {
55
- propsOnChange === null || propsOnChange === void 0 ? void 0 : propsOnChange(e);
56
- onValueChange === null || onValueChange === void 0 ? void 0 : onValueChange(e.target.value);
57
- }, ...restProps }) }) }) }), displayedMessage && (typeof displayedMessage === 'string' ? (jsx("div", { id: `${inputId}-helper`, role: isInvalid ? "alert" : undefined, className: cn(helperTextClasses, helperTextClassName), children: displayedMessage })) : (displayedMessage))] }));
46
+ return (jsxs("div", { className: cn(wrapperClasses, stateClass, wrapperClassName), children: [label && (typeof label === 'string' ? (jsx("div", { className: "mb-1", children: jsx(FormLabel, { htmlFor: inputId, className: labelClassName, state: state, required: required, children: label }) })) : (label)), jsx("div", { children: jsx("div", { className: cn(inputGroupClasses, stateClasses, inputGroupClassName), children: jsx("div", { className: "relative flex-1", children: jsx("textarea", { ref: ref, id: inputId, className: cn(baseClasses, sizeClasses, className), disabled: isDisabled, "aria-invalid": isInvalid, "aria-describedby": displayedMessage ? `${inputId}-helper` : undefined, required: required, rows: rows, cols: cols, onChange: handleChange, ...restProps }) }) }) }), displayedMessage && (typeof displayedMessage === 'string' ? (jsx("div", { id: `${inputId}-helper`, role: isInvalid ? "alert" : undefined, className: cn(helperTextClasses, helperTextClassName), children: displayedMessage })) : (displayedMessage))] }));
58
47
  });
59
48
  Textarea.displayName = "Textarea";
60
49
 
@@ -30,6 +30,15 @@ const timePickerTheme = {
30
30
  // Icon padding styles - inherited from common form theme
31
31
  iconStartPadding: formsBaseTheme.iconStyles.padding.left,
32
32
  iconEndPadding: formsBaseTheme.iconStyles.padding.right,
33
+ // Size-aware icon geometry: slot inset from --form-px-*, glyph from
34
+ // --form-icon-size-*. Flat keys above stay as the fallback.
35
+ iconStartStyles: formsBaseTheme.iconStyles.leftSizes,
36
+ iconEndStyles: formsBaseTheme.iconStyles.rightSizes,
37
+ iconSizes: formsBaseTheme.iconStyles.sizes,
38
+ iconPaddings: {
39
+ left: formsBaseTheme.iconStyles.paddingLeftSizes,
40
+ right: formsBaseTheme.iconStyles.paddingRightSizes,
41
+ },
33
42
  // Placeholder text style - matches Select/Combobox placeholder styling
34
43
  placeholderTextStyle: formsBaseTheme.placeholderStyle,
35
44
  // Selected value text style
@@ -141,6 +141,17 @@ export interface TimePickerThemeOverrides {
141
141
  iconStartPadding?: string;
142
142
  /** End icon padding */
143
143
  iconEndPadding?: string;
144
+ /** Per-size leading icon slot (position + inset) */
145
+ iconStartStyles?: Partial<Record<TimePickerSize, string>>;
146
+ /** Per-size trailing icon slot (position + inset) */
147
+ iconEndStyles?: Partial<Record<TimePickerSize, string>>;
148
+ /** Per-size sizing for the built-in clock/caret/clear icons */
149
+ iconSizes?: Record<TimePickerSize, string>;
150
+ /** Per-size text runway reserved for each icon slot */
151
+ iconPaddings?: {
152
+ left?: Partial<Record<TimePickerSize, string>>;
153
+ right?: Partial<Record<TimePickerSize, string>>;
154
+ };
144
155
  /** Placeholder text style */
145
156
  placeholderTextStyle?: string;
146
157
  /** Selected value text style */
@@ -11,7 +11,7 @@ import { TimePickerContext } from './TimePickerContext.js';
11
11
  * Follows the same visual pattern as DatePickerTrigger and SelectButton
12
12
  */
13
13
  const TimePickerTrigger = ({ id, disabled, isOpen, state = 'default', size = 'md', clearable, onClear, iconStart, iconEnd, className, inputValue, onInputChange, onInputFocus, onInputKeyDown, format, period, onPeriodToggle, placeholder, 'aria-invalid': ariaInvalid, 'aria-describedby': ariaDescribedby, }) => {
14
- var _a, _b, _c, _d;
14
+ var _a, _b, _c, _d, _e, _f;
15
15
  const context = useContext(TimePickerContext);
16
16
  if (!context) {
17
17
  throw new Error('TimePickerTrigger must be used within a TimePicker');
@@ -21,12 +21,14 @@ const TimePickerTrigger = ({ id, disabled, isOpen, state = 'default', size = 'md
21
21
  const sizeStyle = ((_a = theme.triggerSizes) === null || _a === void 0 ? void 0 : _a[size]) || "";
22
22
  const stateStyle = ((_b = theme.triggerStates) === null || _b === void 0 ? void 0 : _b[state]) || "";
23
23
  const hoverStateStyle = ((_c = theme.triggerHoverStates) === null || _c === void 0 ? void 0 : _c[state]) || "";
24
- const iconStartStyle = theme.iconStartStyle || "";
24
+ const iconStartStyle = ((_d = theme.iconStartStyles) === null || _d === void 0 ? void 0 : _d[size]) || theme.iconStartStyle || "";
25
+ // Glyph ramps with the control (--form-icon-size-*), not a flat h-4 w-4.
26
+ const iconSizeStyle = ((_e = theme.iconSizes) === null || _e === void 0 ? void 0 : _e[size]) || "size-4";
25
27
  const inputStyle = theme.inputStyle || "";
26
28
  const periodToggleStyle = theme.periodToggleStyle || "";
27
29
  // Apply focused styles when open — same pattern as SelectButton and DatePickerTrigger
28
30
  const activeFocusState = isOpen
29
- ? ((_d = theme.triggerFocusStates) === null || _d === void 0 ? void 0 : _d[state]) || "shadow-[inset_0_0_0_1px_var(--color-primary-600)] ring-4 ring-[var(--color-primary)]/10 dark:shadow-[inset_0_0_0_1px_var(--color-primary-500)] dark:ring-[var(--color-primary-500)]/20"
31
+ ? ((_f = theme.triggerFocusStates) === null || _f === void 0 ? void 0 : _f[state]) || "shadow-[inset_0_0_0_1px_var(--color-primary-600)] ring-4 ring-[var(--color-primary)]/10 dark:shadow-[inset_0_0_0_1px_var(--color-primary-500)] dark:ring-[var(--color-primary-500)]/20"
30
32
  : "";
31
33
  // Determine whether start icon is present (for input padding)
32
34
  const hasStartIcon = iconStart !== undefined ? Boolean(iconStart) : true;
@@ -45,10 +47,10 @@ const TimePickerTrigger = ({ id, disabled, isOpen, state = 'default', size = 'md
45
47
  (_a = inputRef.current) === null || _a === void 0 ? void 0 : _a.focus();
46
48
  setIsOpen(true);
47
49
  }, className: cn(baseStyle, stateStyle, hoverStateStyle, activeFocusState, 'w-full items-center', className // User overrides take highest precedence
48
- ), children: [jsxs("div", { className: "relative flex-1", children: [jsx("input", { ref: inputRef, id: id, type: "text", inputMode: "numeric", value: inputValue, onChange: (e) => onInputChange(e.target.value), onFocus: onInputFocus, onKeyDown: onInputKeyDown, placeholder: placeholder, maxLength: 5, disabled: disabled, className: cn(inputStyle, sizeStyle, hasStartIcon && iconStartPadding), role: "combobox", "aria-expanded": isOpen, "aria-haspopup": "dialog", "aria-controls": dialogId, "aria-label": "Enter time", "aria-invalid": ariaInvalid, "aria-describedby": ariaDescribedby, autoComplete: "off" }), iconStart !== undefined ? (iconStart && jsx("div", { className: iconStartStyle, children: iconStart })) : (jsx("div", { className: iconStartStyle, children: jsx(Clock, { className: "h-4 w-4" }) }))] }), format === '12h' && (jsx("button", { type: "button", onClick: onPeriodToggle, disabled: disabled, className: cn(periodToggleStyle, "shrink-0"), "aria-label": `Toggle AM/PM, current: ${period}`, tabIndex: -1, children: period || 'AM' })), clearable && hasValue && !disabled && (jsx("button", { type: "button", onClick: (e) => {
50
+ ), children: [jsxs("div", { className: "relative flex-1", children: [jsx("input", { ref: inputRef, id: id, type: "text", inputMode: "numeric", value: inputValue, onChange: (e) => onInputChange(e.target.value), onFocus: onInputFocus, onKeyDown: onInputKeyDown, placeholder: placeholder, maxLength: 5, disabled: disabled, className: cn(inputStyle, sizeStyle, hasStartIcon && iconStartPadding), role: "combobox", "aria-expanded": isOpen, "aria-haspopup": "dialog", "aria-controls": dialogId, "aria-label": "Enter time", "aria-invalid": ariaInvalid, "aria-describedby": ariaDescribedby, autoComplete: "off" }), iconStart !== undefined ? (iconStart && jsx("div", { className: iconStartStyle, children: iconStart })) : (jsx("div", { className: iconStartStyle, children: jsx(Clock, { className: iconSizeStyle }) }))] }), format === '12h' && (jsx("button", { type: "button", onClick: onPeriodToggle, disabled: disabled, className: cn(periodToggleStyle, "shrink-0"), "aria-label": `Toggle AM/PM, current: ${period}`, tabIndex: -1, children: period || 'AM' })), clearable && hasValue && !disabled && (jsx("button", { type: "button", onClick: (e) => {
49
51
  e.stopPropagation();
50
52
  onClear === null || onClear === void 0 ? void 0 : onClear();
51
- }, className: "shrink-0 p-0.5 rounded text-[var(--color-text-muted)] hover:text-[var(--color-text-primary)] hover:bg-[var(--color-surface)] transition-colors", "aria-label": "Clear time", tabIndex: -1, children: jsx(X, { className: "w-4 h-4" }) })), iconEnd !== undefined ? (iconEnd && (jsx("span", { className: "shrink-0 flex items-center pr-3 pointer-events-none text-[var(--color-text-muted)]", children: iconEnd }))) : (jsx("span", { className: "shrink-0 flex items-center pr-3 pointer-events-none text-[var(--color-text-muted)]", children: jsx(CaretUpDown, { className: "h-4 w-4" }) }))] }) }));
53
+ }, className: "shrink-0 p-0.5 rounded text-[var(--color-text-muted)] hover:text-[var(--color-text-primary)] hover:bg-[var(--color-surface)] transition-colors", "aria-label": "Clear time", tabIndex: -1, children: jsx(X, { className: iconSizeStyle }) })), iconEnd !== undefined ? (iconEnd && (jsx("span", { className: "shrink-0 flex items-center pr-3 pointer-events-none text-[var(--color-text-muted)]", children: iconEnd }))) : (jsx("span", { className: "shrink-0 flex items-center pr-3 pointer-events-none text-[var(--color-text-muted)]", children: jsx(CaretUpDown, { className: iconSizeStyle }) }))] }) }));
52
54
  };
53
55
 
54
56
  export { TimePickerTrigger };
@@ -5,6 +5,8 @@
5
5
  */
6
6
  export declare const formsBaseTheme: {
7
7
  wrapperStyle: string;
8
+ /** The field edge alone — for controls that must supply their own transition/fill. */
9
+ inputGroupBorderStyle: string;
8
10
  inputGroupBaseStyle: string;
9
11
  controlBaseStyle: string;
10
12
  sizes: {
@@ -51,6 +53,61 @@ export declare const formsBaseTheme: {
51
53
  left: string;
52
54
  right: string;
53
55
  };
56
+ /**
57
+ * The leading-icon slot, inset by the control's own horizontal padding —
58
+ * EXACTLY, not the trailing slot's `− 4px`. A leading icon stands where the
59
+ * text would have started, so matching `--form-px-*` keeps one constant left
60
+ * edge for a field's contents whether or not it has an icon. The trailing
61
+ * slot is chrome rather than content, which is why it insets tighter.
62
+ */
63
+ leftSizes: {
64
+ sm: string;
65
+ md: string;
66
+ lg: string;
67
+ };
68
+ /** Text runway for the leading slot: inset + icon + an 8px gap (32/36/40). */
69
+ paddingLeftSizes: {
70
+ sm: string;
71
+ md: string;
72
+ lg: string;
73
+ };
74
+ /**
75
+ * The trailing-icon slot, inset from the right edge by the control's own
76
+ * horizontal padding MINUS 4px (8 / 10 / 12 at the default theme). Tighter
77
+ * than the text inset on purpose: a chevron is mostly whitespace, so at the
78
+ * same metric distance it reads as further from the edge than a glyph does.
79
+ * Derived from `--form-px-*` rather than a literal, so retuning a theme's
80
+ * control padding moves the icon with it.
81
+ */
82
+ rightSizes: {
83
+ sm: string;
84
+ md: string;
85
+ lg: string;
86
+ };
87
+ /**
88
+ * Text runway reserved for that slot: inset + icon + an 8px gap, i.e.
89
+ * `--form-px-*` + 16/18/20 (28 / 32 / 36). It replaces a flat `pr-10` that
90
+ * over-reserved at every size.
91
+ */
92
+ paddingRightSizes: {
93
+ sm: string;
94
+ md: string;
95
+ lg: string;
96
+ };
97
+ /**
98
+ * The glyph itself, ramping with the control (12 / 14 / 16px) instead of a
99
+ * flat 16 — at `sm` a 16px icon out-weighed the 14px text beside it.
100
+ *
101
+ * 🔴 Reads --form-icon-size-*, not a Tailwind literal: icon size was the one
102
+ * axis of a field's geometry a theme could not retune, so a preset that
103
+ * scaled height/padding/type left every decorator at its hardcoded size.
104
+ * Not to be confused with --button-icon-square-*, which is a BUTTON's box.
105
+ */
106
+ sizes: {
107
+ sm: string;
108
+ md: string;
109
+ lg: string;
110
+ };
54
111
  };
55
112
  dropdownStyles: {
56
113
  container: string;
@@ -3,19 +3,34 @@
3
3
  * This file defines shared styling patterns used across all form components
4
4
  * Individual component themes should extend these base styles
5
5
  */
6
+ /**
7
+ * The inset-shadow field edge on its own — radius, resting border, hover edge and
8
+ * focus border+ring — with NO layout/paint extras (no `flex`, no fill, no
9
+ * `transition-all`).
10
+ *
11
+ * Split out of `inputGroupBaseStyle` (which is just this plus those extras) for the
12
+ * one control that cannot take `transition-all`: `InputCounter`'s root is a
13
+ * motion element carrying `layout`, and a CSS transition covering `transform`
14
+ * re-interpolates motion's per-frame writes (§12). It needs the same edge as every
15
+ * other control, but has to name its own transitioned properties — so the edge is
16
+ * defined once here and both consumers compose it.
17
+ */
18
+ const inputGroupBorderStyle = "rounded-[var(--form-radius)] outline-none shadow-[inset_0_0_0_1px_var(--color-border)] " +
19
+ "focus-within:shadow-[inset_0_0_0_1px_var(--color-primary-600)] " +
20
+ "focus-within:ring-4 focus-within:ring-[var(--color-primary)]/10 " +
21
+ // Hover edge: strengthen the border to --color-border-secondary on hover. Scoped with
22
+ // not-focus-within so focus (primary border) always wins; the token flips for dark mode.
23
+ "not-focus-within:hover:shadow-[inset_0_0_0_1px_var(--color-border-secondary)] " +
24
+ // Dark focus mirror (fill comes from --color-surface, which flips automatically)
25
+ "dark:focus-within:shadow-[inset_0_0_0_1px_var(--color-primary-500)] dark:focus-within:ring-[var(--color-primary-500)]/20";
6
26
  // Common form theme configuration
7
27
  const formsBaseTheme = {
8
28
  // Common wrapper style
9
29
  wrapperStyle: "relative w-full",
30
+ /** The field edge alone — for controls that must supply their own transition/fill. */
31
+ inputGroupBorderStyle,
10
32
  // Common input group style with focus, hover, and state variations
11
- inputGroupBaseStyle: "relative flex rounded-[var(--form-radius)] outline-none shadow-[inset_0_0_0_1px_var(--color-border)] " +
12
- "focus-within:shadow-[inset_0_0_0_1px_var(--color-primary-600)] " +
13
- "focus-within:ring-4 focus-within:ring-[var(--color-primary)]/10 bg-[var(--field-fill)] transition-all " +
14
- // Hover edge: strengthen the border to --color-border-secondary on hover. Scoped with
15
- // not-focus-within so focus (primary border) always wins; the token flips for dark mode.
16
- "not-focus-within:hover:shadow-[inset_0_0_0_1px_var(--color-border-secondary)] " +
17
- // Dark focus mirror (fill comes from --color-surface, which flips automatically)
18
- "dark:focus-within:shadow-[inset_0_0_0_1px_var(--color-primary-500)] dark:focus-within:ring-[var(--color-primary-500)]/20",
33
+ inputGroupBaseStyle: `relative flex bg-[var(--field-fill)] transition-all ${inputGroupBorderStyle}`,
19
34
  // Common base input/control style
20
35
  controlBaseStyle: "w-full bg-transparent text-sm text-[var(--color-text-primary)] " +
21
36
  "placeholder:text-[var(--color-text-placeholder)] focus:outline-none",
@@ -103,6 +118,11 @@ const formsBaseTheme = {
103
118
  helperText: "text-xs text-[var(--color-text-muted)] mt-1.5 [.state-disabled_&]:text-[var(--color-text-disabled)] [.state-invalid_&]:text-[var(--color-danger-500)] " +
104
119
  "dark:[.state-invalid_&]:text-[var(--color-danger-400)]",
105
120
  // Common icon styles
121
+ //
122
+ // `left`/`right`/`padding` are the size-BLIND originals (flat pr-3 / pr-10 at
123
+ // every size); the `*Sizes` maps below are the size-aware replacements. Adopt
124
+ // the maps in new work — Select already has — and treat the flat keys as the
125
+ // not-yet-migrated path for Input/Combobox/DatePicker/DateRangePicker/TimePicker.
106
126
  iconStyles: {
107
127
  left: "absolute inset-y-0 left-0 flex items-center pl-3 pointer-events-none text-[var(--color-text-muted)]",
108
128
  right: "absolute inset-y-0 right-0 flex items-center pr-3 pointer-events-none text-[var(--color-text-muted)]",
@@ -110,6 +130,61 @@ const formsBaseTheme = {
110
130
  left: "pl-10",
111
131
  right: "pr-10",
112
132
  },
133
+ /**
134
+ * The leading-icon slot, inset by the control's own horizontal padding —
135
+ * EXACTLY, not the trailing slot's `− 4px`. A leading icon stands where the
136
+ * text would have started, so matching `--form-px-*` keeps one constant left
137
+ * edge for a field's contents whether or not it has an icon. The trailing
138
+ * slot is chrome rather than content, which is why it insets tighter.
139
+ */
140
+ leftSizes: {
141
+ sm: "absolute inset-y-0 left-0 flex items-center pl-[var(--form-px-sm)] pointer-events-none text-[var(--color-text-muted)]",
142
+ md: "absolute inset-y-0 left-0 flex items-center pl-[var(--form-px-md)] pointer-events-none text-[var(--color-text-muted)]",
143
+ lg: "absolute inset-y-0 left-0 flex items-center pl-[var(--form-px-lg)] pointer-events-none text-[var(--color-text-muted)]",
144
+ },
145
+ /** Text runway for the leading slot: inset + icon + an 8px gap (32/36/40). */
146
+ paddingLeftSizes: {
147
+ sm: "pl-[calc(var(--form-px-sm)_+_20px)]",
148
+ md: "pl-[calc(var(--form-px-md)_+_22px)]",
149
+ lg: "pl-[calc(var(--form-px-lg)_+_24px)]",
150
+ },
151
+ /**
152
+ * The trailing-icon slot, inset from the right edge by the control's own
153
+ * horizontal padding MINUS 4px (8 / 10 / 12 at the default theme). Tighter
154
+ * than the text inset on purpose: a chevron is mostly whitespace, so at the
155
+ * same metric distance it reads as further from the edge than a glyph does.
156
+ * Derived from `--form-px-*` rather than a literal, so retuning a theme's
157
+ * control padding moves the icon with it.
158
+ */
159
+ rightSizes: {
160
+ sm: "absolute inset-y-0 right-0 flex items-center pr-[calc(var(--form-px-sm)_-_4px)] pointer-events-none text-[var(--color-text-muted)]",
161
+ md: "absolute inset-y-0 right-0 flex items-center pr-[calc(var(--form-px-md)_-_4px)] pointer-events-none text-[var(--color-text-muted)]",
162
+ lg: "absolute inset-y-0 right-0 flex items-center pr-[calc(var(--form-px-lg)_-_4px)] pointer-events-none text-[var(--color-text-muted)]",
163
+ },
164
+ /**
165
+ * Text runway reserved for that slot: inset + icon + an 8px gap, i.e.
166
+ * `--form-px-*` + 16/18/20 (28 / 32 / 36). It replaces a flat `pr-10` that
167
+ * over-reserved at every size.
168
+ */
169
+ paddingRightSizes: {
170
+ sm: "pr-[calc(var(--form-px-sm)_+_16px)]",
171
+ md: "pr-[calc(var(--form-px-md)_+_18px)]",
172
+ lg: "pr-[calc(var(--form-px-lg)_+_20px)]",
173
+ },
174
+ /**
175
+ * The glyph itself, ramping with the control (12 / 14 / 16px) instead of a
176
+ * flat 16 — at `sm` a 16px icon out-weighed the 14px text beside it.
177
+ *
178
+ * 🔴 Reads --form-icon-size-*, not a Tailwind literal: icon size was the one
179
+ * axis of a field's geometry a theme could not retune, so a preset that
180
+ * scaled height/padding/type left every decorator at its hardcoded size.
181
+ * Not to be confused with --button-icon-square-*, which is a BUTTON's box.
182
+ */
183
+ sizes: {
184
+ sm: "size-[var(--form-icon-size-sm)]",
185
+ md: "size-[var(--form-icon-size-md)]",
186
+ lg: "size-[var(--form-icon-size-lg)]",
187
+ },
113
188
  },
114
189
  // Dropdown/listbox styles for Select, Combobox, and similar components
115
190
  dropdownStyles: {
@@ -92,6 +92,7 @@ import '../forms/InputAddress/InputAddress.js';
92
92
  import '../forms/InputAddress/InputAddress.theme.js';
93
93
  import { InputDate } from '../forms/InputDate/InputDate.js';
94
94
  import { InputCounter } from '../forms/InputCounter/InputCounter.js';
95
+ import '../forms/InputCounter/InputCounter.theme.js';
95
96
  import { InputTag } from '../forms/InputTag/InputTag.js';
96
97
  import '../forms/InputTag/InputTag.theme.js';
97
98
  import { Combobox } from '../forms/Combobox/Combobox.js';
@@ -1762,6 +1762,21 @@ const GENERATIVE_SCHEMA = {
1762
1762
  "required": true,
1763
1763
  "description": "Current counter value (controlled)"
1764
1764
  },
1765
+ "size": {
1766
+ "kind": "enum",
1767
+ "options": [
1768
+ "sm",
1769
+ "md",
1770
+ "lg"
1771
+ ],
1772
+ "default": "md",
1773
+ "description": "Control size, drawn from the shared `--form-*` scale so a counter lines up"
1774
+ },
1775
+ "editable": {
1776
+ "kind": "boolean",
1777
+ "default": true,
1778
+ "description": "Whether clicking the number swaps it for a text field so the value can be"
1779
+ },
1765
1780
  "state": {
1766
1781
  "kind": "enum",
1767
1782
  "options": [
@@ -2548,7 +2563,12 @@ const GENERATIVE_SCHEMA = {
2548
2563
  },
2549
2564
  "selected": {
2550
2565
  "kind": "boolean",
2551
- "description": "Whether the pill is selected (controlled state)"
2566
+ "description": "Whether the pill is selected CONTROLLED. Passing it makes the prop"
2567
+ },
2568
+ "defaultSelected": {
2569
+ "kind": "boolean",
2570
+ "default": false,
2571
+ "description": "Initial selection for an UNCONTROLLED pill, which then owns its own state."
2552
2572
  },
2553
2573
  "readOnly": {
2554
2574
  "kind": "boolean",