@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.
- package/CHANGELOG.md +202 -0
- package/dist/components/ai/AgentRequest/AgentRequest.theme.js +1 -1
- package/dist/components/core/Button/Button.theme.js +3 -3
- package/dist/components/core/Pill/Pill.js +32 -17
- package/dist/components/core/Pill/Pill.types.d.ts +14 -1
- package/dist/components/core/SlidingNumber/SlidingNumber.d.ts +8 -0
- package/dist/components/core/SlidingNumber/SlidingNumber.js +12 -3
- package/dist/components/forms/ColorPicker/ColorPicker.js +45 -3
- package/dist/components/forms/ColorPicker/ColorPickerBody.js +8 -4
- package/dist/components/forms/Combobox/Combobox.js +8 -4
- package/dist/components/forms/Combobox/Combobox.theme.js +9 -0
- package/dist/components/forms/Combobox/Combobox.types.d.ts +13 -0
- package/dist/components/forms/DatePicker/DatePicker.js +19 -12
- package/dist/components/forms/DatePicker/DatePicker.theme.js +5 -0
- package/dist/components/forms/DatePicker/DatePicker.types.d.ts +8 -0
- package/dist/components/forms/DatePicker/DatePickerContext.d.ts +0 -1
- package/dist/components/forms/DatePicker/DatePickerTrigger.js +5 -3
- package/dist/components/forms/DateRangePicker/DateRangePicker.theme.js +5 -0
- package/dist/components/forms/DateRangePicker/DateRangePicker.types.d.ts +8 -0
- package/dist/components/forms/DateRangePicker/DateRangePickerTrigger.js +5 -3
- package/dist/components/forms/Input/Input.js +17 -12
- package/dist/components/forms/Input/Input.theme.js +10 -0
- package/dist/components/forms/Input/Input.types.d.ts +14 -0
- package/dist/components/forms/InputAddress/InputAddress.js +5 -5
- package/dist/components/forms/InputAddress/InputAddress.types.d.ts +7 -0
- package/dist/components/forms/InputCounter/InputCounter.js +194 -17
- package/dist/components/forms/InputCounter/InputCounter.theme.d.ts +12 -0
- package/dist/components/forms/InputCounter/InputCounter.theme.js +92 -2
- package/dist/components/forms/InputCounter/InputCounter.types.d.ts +47 -4
- package/dist/components/forms/InputCreditCard/InputCreditCard.js +4 -4
- package/dist/components/forms/InputCreditCard/InputCreditCard.types.d.ts +7 -0
- package/dist/components/forms/Mention/Mention.js +13 -11
- package/dist/components/forms/RichTextEditor/RichTextEditor.js +12 -3
- package/dist/components/forms/Select/Select.js +15 -8
- package/dist/components/forms/Select/Select.theme.js +9 -1
- package/dist/components/forms/Select/Select.types.d.ts +16 -1
- package/dist/components/forms/Textarea/Textarea.js +14 -25
- package/dist/components/forms/TimePicker/TimePicker.theme.js +9 -0
- package/dist/components/forms/TimePicker/TimePicker.types.d.ts +11 -0
- package/dist/components/forms/TimePicker/TimePickerTrigger.js +7 -5
- package/dist/components/forms/forms.theme.d.ts +57 -0
- package/dist/components/forms/forms.theme.js +83 -8
- package/dist/components/generative/registry.js +1 -0
- package/dist/components/generative/schema.generated.js +21 -1
- package/dist/generative.schema.json +21 -1
- package/dist/registry.json +108 -17
- package/dist/styles.css +1 -1
- package/dist/tools.json +21 -1
- package/dist/utils/composeEventHandlers.d.ts +19 -0
- package/dist/utils/composeEventHandlers.js +26 -0
- package/dist/utils/composeEventHandlers.test.d.ts +1 -0
- package/dist/utils/index.d.ts +1 -0
- package/package.json +1 -1
- 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
|
|
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((
|
|
587
|
-
var _a;
|
|
590
|
+
const handleFocus = useCallback((_e) => {
|
|
588
591
|
setIsFocused(true);
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
const handleBlur = useCallback((e) => {
|
|
592
|
-
var _a;
|
|
592
|
+
}, []);
|
|
593
|
+
const handleBlur = useCallback((_e) => {
|
|
593
594
|
setSelectedMentionIndex(null);
|
|
594
595
|
setIsFocused(false);
|
|
595
|
-
|
|
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,
|
|
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
|
|
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),
|
|
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
|
-
|
|
91
|
-
|
|
92
|
-
const
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
/**
|
|
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
|
|
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,
|
|
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,
|
|
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
|
-
? ((
|
|
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:
|
|
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:
|
|
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:
|
|
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
|
|
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",
|