@tangible/ui 0.0.1 → 0.0.3

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 (135) hide show
  1. package/components/Card/Card.d.ts +1 -0
  2. package/components/Card/Card.js +17 -20
  3. package/components/Checkbox/Checkbox.d.ts +9 -0
  4. package/components/Checkbox/Checkbox.js +92 -0
  5. package/components/Checkbox/index.d.ts +2 -0
  6. package/components/Checkbox/index.js +1 -0
  7. package/components/Checkbox/types.d.ts +10 -0
  8. package/components/Checkbox/types.js +1 -0
  9. package/components/Chip/Chip.d.ts +4 -1
  10. package/components/Chip/Chip.js +32 -7
  11. package/components/ChipGroup/ChipGroup.d.ts +5 -0
  12. package/components/ChipGroup/ChipGroup.js +68 -0
  13. package/components/ChipGroup/ChipGroupContext.d.ts +3 -0
  14. package/components/ChipGroup/ChipGroupContext.js +5 -0
  15. package/components/ChipGroup/index.d.ts +3 -0
  16. package/components/ChipGroup/index.js +2 -0
  17. package/components/ChipGroup/types.d.ts +36 -0
  18. package/components/ChipGroup/types.js +1 -0
  19. package/components/Chips/Chips.d.ts +2 -0
  20. package/components/Chips/Chips.js +1 -1
  21. package/components/Combobox/Combobox.d.ts +33 -0
  22. package/components/Combobox/Combobox.js +466 -0
  23. package/components/Combobox/ComboboxContext.d.ts +8 -0
  24. package/components/Combobox/ComboboxContext.js +36 -0
  25. package/components/Combobox/index.d.ts +2 -0
  26. package/components/Combobox/index.js +1 -0
  27. package/components/Combobox/types.d.ts +204 -0
  28. package/components/Combobox/types.js +1 -0
  29. package/components/Dropdown/Dropdown.js +2 -1
  30. package/components/Field/Field.d.ts +39 -0
  31. package/components/Field/Field.js +92 -0
  32. package/components/Field/FieldContext.d.ts +16 -0
  33. package/components/Field/FieldContext.js +10 -0
  34. package/components/Field/index.d.ts +2 -0
  35. package/components/Field/index.js +1 -0
  36. package/components/Modal/Modal.d.ts +4 -4
  37. package/components/Modal/Modal.js +14 -12
  38. package/components/MoveHandle/MoveHandle.d.ts +2 -0
  39. package/components/MoveHandle/MoveHandle.js +84 -0
  40. package/components/MoveHandle/index.d.ts +2 -0
  41. package/components/MoveHandle/index.js +1 -0
  42. package/components/MoveHandle/types.d.ts +43 -0
  43. package/components/MoveHandle/types.js +1 -0
  44. package/components/MultiSelect/MultiSelect.d.ts +39 -0
  45. package/components/MultiSelect/MultiSelect.js +623 -0
  46. package/components/MultiSelect/MultiSelectContext.d.ts +20 -0
  47. package/components/MultiSelect/MultiSelectContext.js +56 -0
  48. package/components/MultiSelect/index.d.ts +2 -0
  49. package/components/MultiSelect/index.js +1 -0
  50. package/components/MultiSelect/types.d.ts +218 -0
  51. package/components/MultiSelect/types.js +3 -0
  52. package/components/Notice/Notice.d.ts +1 -1
  53. package/components/Notice/Notice.js +1 -1
  54. package/components/Progress/Progress.js +1 -1
  55. package/components/Progress/types.d.ts +7 -7
  56. package/components/Radio/Radio.d.ts +2 -0
  57. package/components/Radio/Radio.js +50 -0
  58. package/components/Radio/RadioGroup.d.ts +2 -0
  59. package/components/Radio/RadioGroup.js +54 -0
  60. package/components/Radio/RadioGroupContext.d.ts +3 -0
  61. package/components/Radio/RadioGroupContext.js +9 -0
  62. package/components/Radio/index.d.ts +8 -0
  63. package/components/Radio/index.js +6 -0
  64. package/components/Radio/types.d.ts +32 -0
  65. package/components/Radio/types.js +1 -0
  66. package/components/Rating/Rating.d.ts +5 -5
  67. package/components/Rating/Rating.js +2 -2
  68. package/components/SegmentedControl/SegmentedControl.js +20 -104
  69. package/components/SegmentedControl/types.d.ts +4 -8
  70. package/components/Select/Select.d.ts +39 -0
  71. package/components/Select/Select.js +497 -0
  72. package/components/Select/SelectContext.d.ts +20 -0
  73. package/components/Select/SelectContext.js +56 -0
  74. package/components/Select/index.d.ts +3 -0
  75. package/components/Select/index.js +1 -0
  76. package/components/Select/types.d.ts +216 -0
  77. package/components/Select/types.js +11 -0
  78. package/components/Sidebar/Sidebar.js +12 -12
  79. package/components/Sidebar/types.d.ts +5 -5
  80. package/components/StepIndicator/StepIndicator.js +1 -1
  81. package/components/StepList/StepList.js +2 -2
  82. package/components/StepList/types.d.ts +4 -4
  83. package/components/Switch/Switch.d.ts +9 -0
  84. package/components/Switch/Switch.js +91 -0
  85. package/components/Switch/index.d.ts +2 -0
  86. package/components/Switch/index.js +1 -0
  87. package/components/Switch/types.d.ts +11 -0
  88. package/components/Switch/types.js +1 -0
  89. package/components/TextInput/TextInput.d.ts +8 -0
  90. package/components/TextInput/TextInput.js +25 -0
  91. package/components/TextInput/index.d.ts +2 -0
  92. package/components/TextInput/index.js +1 -0
  93. package/components/TextInput/types.d.ts +32 -0
  94. package/components/TextInput/types.js +1 -0
  95. package/components/Textarea/Textarea.d.ts +6 -0
  96. package/components/Textarea/Textarea.js +49 -0
  97. package/components/Textarea/index.d.ts +2 -0
  98. package/components/Textarea/index.js +1 -0
  99. package/components/Textarea/types.d.ts +25 -0
  100. package/components/Textarea/types.js +1 -0
  101. package/components/index.d.ts +22 -0
  102. package/components/index.js +11 -0
  103. package/icons/icons.svg +2 -0
  104. package/icons/manifest.json +16 -0
  105. package/icons/registry.d.ts +4 -0
  106. package/icons/registry.js +2 -0
  107. package/icons/system/index.d.ts +4 -0
  108. package/icons/system/index.js +22 -0
  109. package/package.json +1 -1
  110. package/styles/all.css +1 -1
  111. package/styles/all.expanded.css +1838 -136
  112. package/styles/all.expanded.unlayered.css +1838 -136
  113. package/styles/all.unlayered.css +1 -1
  114. package/styles/components/_bundle.scss +22 -0
  115. package/styles/components/input/index.scss +5 -20
  116. package/styles/index.scss +21 -0
  117. package/styles/system/_control.scss +49 -0
  118. package/styles/system/_tokens.scss +124 -2
  119. package/styles/system/index.scss +2 -1
  120. package/styles/utilities/_index.scss +50 -0
  121. package/tui-manifest.json +907 -112
  122. package/utils/compose-events.d.ts +15 -0
  123. package/utils/compose-events.js +27 -0
  124. package/utils/hash.d.ts +15 -0
  125. package/utils/hash.js +32 -0
  126. package/utils/index.d.ts +3 -0
  127. package/utils/index.js +6 -0
  128. package/utils/is-dev.d.ts +5 -0
  129. package/utils/is-dev.js +7 -0
  130. package/utils/use-controllable-state.d.ts +19 -0
  131. package/utils/use-controllable-state.js +59 -0
  132. package/utils/use-roving-group.d.ts +33 -0
  133. package/utils/use-roving-group.js +123 -0
  134. package/utils/value-key.d.ts +16 -0
  135. package/utils/value-key.js +14 -0
@@ -0,0 +1,216 @@
1
+ import type { Placement, FloatingContext, ReferenceType } from '@floating-ui/react';
2
+ import type { RefObject, CSSProperties, MutableRefObject } from 'react';
3
+ import type { SizeStandard } from '../../types/sizes';
4
+ import type { OptionValue } from '../../utils/value-key';
5
+ /**
6
+ * Placement sides for the dropdown content.
7
+ */
8
+ export type Side = 'top' | 'bottom';
9
+ /**
10
+ * Alignment relative to trigger.
11
+ */
12
+ export type Align = 'start' | 'center' | 'end';
13
+ export type SelectProps = {
14
+ /**
15
+ * Control size.
16
+ * @default 'md'
17
+ */
18
+ size?: SizeStandard;
19
+ /**
20
+ * Controlled selected value.
21
+ */
22
+ value?: OptionValue;
23
+ /**
24
+ * Default value for uncontrolled usage.
25
+ */
26
+ defaultValue?: OptionValue;
27
+ /**
28
+ * Callback when selection changes.
29
+ * Called with undefined when selection is cleared.
30
+ */
31
+ onValueChange?: (value: OptionValue | undefined) => void;
32
+ /**
33
+ * Controlled open state.
34
+ */
35
+ open?: boolean;
36
+ /**
37
+ * Default open state for uncontrolled usage.
38
+ * @default false
39
+ */
40
+ defaultOpen?: boolean;
41
+ /**
42
+ * Callback when open state changes.
43
+ */
44
+ onOpenChange?: (open: boolean) => void;
45
+ /**
46
+ * Whether the entire select is disabled.
47
+ * @default false
48
+ */
49
+ disabled?: boolean;
50
+ /**
51
+ * Placeholder text when no value is selected.
52
+ */
53
+ placeholder?: string;
54
+ /**
55
+ * Preferred side of the trigger to place the listbox.
56
+ * @default 'bottom'
57
+ */
58
+ side?: Side;
59
+ /**
60
+ * Alignment along the side.
61
+ * @default 'start'
62
+ */
63
+ align?: Align;
64
+ /**
65
+ * Distance from the trigger in pixels.
66
+ * @default 4
67
+ */
68
+ sideOffset?: number;
69
+ /**
70
+ * Whether the selection can be cleared with a clear button.
71
+ * @default false
72
+ */
73
+ clearable?: boolean;
74
+ /**
75
+ * Accessible name for the select.
76
+ */
77
+ 'aria-label'?: string;
78
+ /**
79
+ * ID of element that labels this select.
80
+ */
81
+ 'aria-labelledby'?: string;
82
+ /**
83
+ * ID of element(s) that describe this select.
84
+ */
85
+ 'aria-describedby'?: string;
86
+ /**
87
+ * Optional trigger ID override.
88
+ * Useful for Field integration via htmlFor.
89
+ */
90
+ id?: string;
91
+ children: React.ReactNode;
92
+ };
93
+ export type SelectTriggerProps = {
94
+ /**
95
+ * When true, merges props onto the child element instead of wrapping.
96
+ * Child must be a single React element that accepts ref and event handlers.
97
+ * @default false
98
+ */
99
+ asChild?: boolean;
100
+ /**
101
+ * Additional CSS class names.
102
+ */
103
+ className?: string;
104
+ children?: React.ReactNode;
105
+ };
106
+ export type SelectContentProps = {
107
+ /**
108
+ * Additional CSS class names.
109
+ */
110
+ className?: string;
111
+ children: React.ReactNode;
112
+ };
113
+ export type SelectOptionProps = {
114
+ /**
115
+ * The value for this option. Required and must be unique.
116
+ * Can be string or number.
117
+ */
118
+ value: OptionValue;
119
+ /**
120
+ * Whether this option is disabled.
121
+ * @default false
122
+ */
123
+ disabled?: boolean;
124
+ /**
125
+ * Text value for typeahead. Required when children is not a string.
126
+ * If children is a string, defaults to that.
127
+ */
128
+ textValue?: string;
129
+ /**
130
+ * Additional CSS class names.
131
+ */
132
+ className?: string;
133
+ children: React.ReactNode;
134
+ };
135
+ export type SelectGroupProps = {
136
+ /**
137
+ * Additional CSS class names.
138
+ */
139
+ className?: string;
140
+ children: React.ReactNode;
141
+ };
142
+ export type SelectLabelProps = {
143
+ /**
144
+ * Additional CSS class names.
145
+ */
146
+ className?: string;
147
+ children: React.ReactNode;
148
+ };
149
+ /**
150
+ * Registration record for an option.
151
+ */
152
+ export type RegisteredOption = {
153
+ value: OptionValue;
154
+ ref: RefObject<HTMLElement | null>;
155
+ disabled: boolean;
156
+ textValue: string;
157
+ };
158
+ /**
159
+ * Stable context: config, IDs, refs, and stable callbacks.
160
+ * Rarely changes — safe to subscribe without rerender concerns.
161
+ */
162
+ export type SelectActionsContextValue = {
163
+ disabled: boolean;
164
+ placeholder: string;
165
+ clearable: boolean;
166
+ size: SizeStandard;
167
+ triggerId: string;
168
+ listboxId: string;
169
+ ariaLabel?: string;
170
+ ariaLabelledBy?: string;
171
+ ariaDescribedBy?: string;
172
+ setOpen: (open: boolean) => void;
173
+ setValue: (value: OptionValue | undefined, textValue?: string) => void;
174
+ registerOption: (option: RegisteredOption) => void;
175
+ unregisterOption: (value: OptionValue) => void;
176
+ handleSelect: (index: number | null) => void;
177
+ refs: {
178
+ reference: React.RefObject<ReferenceType | null>;
179
+ floating: React.RefObject<HTMLElement | null>;
180
+ setReference: (node: ReferenceType | null) => void;
181
+ setFloating: (node: HTMLElement | null) => void;
182
+ };
183
+ listRef: MutableRefObject<(HTMLElement | null)[]>;
184
+ getReferenceProps: (userProps?: React.HTMLProps<Element>) => Record<string, unknown>;
185
+ getFloatingProps: (userProps?: React.HTMLProps<HTMLElement>) => Record<string, unknown>;
186
+ getItemProps: (userProps?: React.HTMLProps<HTMLElement>) => Record<string, unknown>;
187
+ };
188
+ /**
189
+ * State context: values that change during interaction.
190
+ * Subscribe only when you need reactive updates.
191
+ */
192
+ export type SelectStateContextValue = {
193
+ open: boolean;
194
+ value: OptionValue | undefined;
195
+ getSelectedTextValue: () => string | undefined;
196
+ activeIndex: number | null;
197
+ highlightedValue: OptionValue | null;
198
+ orderedOptions: RegisteredOption[];
199
+ floatingStyles: CSSProperties;
200
+ floatingContext: FloatingContext;
201
+ };
202
+ /**
203
+ * Combined context value (for backwards compat and convenience hooks).
204
+ * @deprecated Prefer using useSelectActions + useSelectState separately.
205
+ */
206
+ export type SelectContextValue = SelectActionsContextValue & SelectStateContextValue;
207
+ export type SelectContentContextValue = {
208
+ listRef: MutableRefObject<(HTMLElement | null)[]>;
209
+ activeIndex: number | null;
210
+ handleSelect: (index: number | null) => void;
211
+ orderedOptions: RegisteredOption[];
212
+ };
213
+ /**
214
+ * Convert side + align to Floating UI placement.
215
+ */
216
+ export declare function toPlacement(side: Side, align: Align): Placement;
@@ -0,0 +1,11 @@
1
+ // -----------------------------------------------------------------------------
2
+ // Utility
3
+ // -----------------------------------------------------------------------------
4
+ /**
5
+ * Convert side + align to Floating UI placement.
6
+ */
7
+ export function toPlacement(side, align) {
8
+ if (align === 'center')
9
+ return side;
10
+ return `${side}-${align}`;
11
+ }
@@ -6,7 +6,7 @@ const isBrowser = typeof document !== 'undefined';
6
6
  // Sidebar (Root)
7
7
  // =============================================================================
8
8
  function SidebarRoot(props) {
9
- const { position = 'left', drawer = false, isOpen = false, onClose, ariaLabel, children, className, } = props;
9
+ const { position = 'left', drawer = false, open = false, onClose, 'aria-label': ariaLabel, children, className, } = props;
10
10
  const sidebarRef = useRef(null);
11
11
  const restoreRef = useRef(null);
12
12
  const rootClassName = ['tui-sidebar', className].filter(Boolean).join(' ');
@@ -16,7 +16,7 @@ function SidebarRoot(props) {
16
16
  useEffect(() => {
17
17
  if (!drawer || !isBrowser)
18
18
  return;
19
- if (isOpen) {
19
+ if (open) {
20
20
  // Capture focus target when opening
21
21
  restoreRef.current = document.activeElement;
22
22
  }
@@ -28,41 +28,41 @@ function SidebarRoot(props) {
28
28
  }
29
29
  restoreRef.current = null;
30
30
  }
31
- }, [drawer, isOpen]);
31
+ }, [drawer, open]);
32
32
  // ---------------------------------------------------------------------------
33
33
  // Drawer mode: body scroll lock
34
34
  // ---------------------------------------------------------------------------
35
35
  useEffect(() => {
36
- if (!drawer || !isOpen)
36
+ if (!drawer || !open)
37
37
  return;
38
38
  document.body.classList.add('tui-sidebar-drawer-open');
39
39
  return () => {
40
40
  document.body.classList.remove('tui-sidebar-drawer-open');
41
41
  };
42
- }, [drawer, isOpen]);
42
+ }, [drawer, open]);
43
43
  // ---------------------------------------------------------------------------
44
44
  // Drawer mode: focus trap (handles Tab cycling and ESC to close)
45
45
  // ---------------------------------------------------------------------------
46
46
  useFocusTrap(sidebarRef, {
47
- isActive: drawer && isOpen,
47
+ isActive: drawer && open,
48
48
  onEscape: onClose,
49
49
  });
50
50
  // ---------------------------------------------------------------------------
51
51
  // Drawer mode: initial focus
52
52
  // ---------------------------------------------------------------------------
53
53
  useEffect(() => {
54
- if (!drawer || !isOpen)
54
+ if (!drawer || !open)
55
55
  return;
56
56
  const sidebar = sidebarRef.current;
57
57
  if (!sidebar)
58
58
  return;
59
59
  const target = getInitialFocus(sidebar);
60
60
  target.focus({ preventScroll: true });
61
- }, [drawer, isOpen]);
61
+ }, [drawer, open]);
62
62
  // ---------------------------------------------------------------------------
63
63
  // Sidebar content (shared between static and drawer modes)
64
64
  // ---------------------------------------------------------------------------
65
- const sidebarContent = (_jsx("aside", { ref: sidebarRef, className: rootClassName, "data-position": position, "aria-label": ariaLabel, "aria-modal": drawer && isOpen ? 'true' : undefined, tabIndex: drawer ? -1 : undefined, children: children }));
65
+ const sidebarContent = (_jsx("aside", { ref: sidebarRef, className: rootClassName, "data-position": position, "aria-label": ariaLabel, "aria-modal": drawer && open ? 'true' : undefined, tabIndex: drawer ? -1 : undefined, children: children }));
66
66
  // ---------------------------------------------------------------------------
67
67
  // Static mode: render directly
68
68
  // ---------------------------------------------------------------------------
@@ -72,7 +72,7 @@ function SidebarRoot(props) {
72
72
  // ---------------------------------------------------------------------------
73
73
  // Drawer mode: render inline with fixed positioning (no portal needed)
74
74
  // ---------------------------------------------------------------------------
75
- if (!isOpen) {
75
+ if (!open) {
76
76
  return null;
77
77
  }
78
78
  const drawerClassName = [
@@ -95,9 +95,9 @@ function SidebarHeader(props) {
95
95
  // Sidebar.Nav
96
96
  // =============================================================================
97
97
  function SidebarNav(props) {
98
- const { ariaLabel, ariaLabelledBy, children, className } = props;
98
+ const { 'aria-label': navAriaLabel, 'aria-labelledby': navAriaLabelledBy, children, className } = props;
99
99
  const navClassName = ['tui-sidebar__nav', className].filter(Boolean).join(' ');
100
- return (_jsx("nav", { className: navClassName, "aria-label": ariaLabel, "aria-labelledby": ariaLabelledBy, children: children }));
100
+ return (_jsx("nav", { className: navClassName, "aria-label": navAriaLabel, "aria-labelledby": navAriaLabelledBy, children: children }));
101
101
  }
102
102
  SidebarHeader.displayName = 'Sidebar.Header';
103
103
  SidebarNav.displayName = 'Sidebar.Nav';
@@ -14,7 +14,7 @@ export type SidebarProps = {
14
14
  /**
15
15
  * Drawer open state. Required when `drawer={true}`.
16
16
  */
17
- isOpen?: boolean;
17
+ open?: boolean;
18
18
  /**
19
19
  * Callback to close the drawer.
20
20
  * Called when ESC is pressed or backdrop is clicked.
@@ -22,9 +22,9 @@ export type SidebarProps = {
22
22
  onClose?: () => void;
23
23
  /**
24
24
  * Accessible label for the aside landmark.
25
- * Typically omit this if using `ariaLabel` on `Sidebar.Nav` instead.
25
+ * Typically omit this if using `aria-label` on `Sidebar.Nav` instead.
26
26
  */
27
- ariaLabel?: string;
27
+ 'aria-label'?: string;
28
28
  /**
29
29
  * Sidebar content (Header, Nav, etc.).
30
30
  */
@@ -49,11 +49,11 @@ export type SidebarNavProps = {
49
49
  * Accessible label for the nav landmark.
50
50
  * Use this for the primary label since nav is the actionable region.
51
51
  */
52
- ariaLabel?: string;
52
+ 'aria-label'?: string;
53
53
  /**
54
54
  * ID of a visible heading that labels the navigation.
55
55
  */
56
- ariaLabelledBy?: string;
56
+ 'aria-labelledby'?: string;
57
57
  /**
58
58
  * Nav content (typically Accordion with StepList).
59
59
  */
@@ -60,5 +60,5 @@ export function StepIndicator(props) {
60
60
  ]
61
61
  .filter(Boolean)
62
62
  .join(' ');
63
- return (_jsx("div", { className: rootClassName, children: _jsxs(Progress, { mode: "circle", variant: variant, size: size, value: displayValue, showLabels: showValue || hasIcon, labelPosition: "inside", ariaLabel: ariaLabel, children: [hasIcon && iconName && (_jsx(Icon, { name: iconName })), showValue && status === 'in-progress' && (_jsxs("span", { className: "tui-step-indicator__value", children: [Math.round(displayValue), "%"] }))] }) }));
63
+ return (_jsx("div", { className: rootClassName, children: _jsxs(Progress, { mode: "circle", variant: variant, size: size, value: displayValue, showLabels: showValue || hasIcon, labelPosition: "inside", "aria-label": ariaLabel, children: [hasIcon && iconName && (_jsx(Icon, { name: iconName })), showValue && status === 'in-progress' && (_jsxs("span", { className: "tui-step-indicator__value", children: [Math.round(displayValue), "%"] }))] }) }));
64
64
  }
@@ -7,11 +7,11 @@ import { StepIndicator } from '../StepIndicator/index.js';
7
7
  // StepList (Root)
8
8
  // =============================================================================
9
9
  function StepListRoot(props) {
10
- const { ariaLabel, ariaLabelledBy, ariaCurrent = 'step', current, onSelect, children, className, } = props;
10
+ const { 'aria-label': ariaLabel, 'aria-labelledby': ariaLabelledBy, ariaCurrent = 'step', current, onSelect, children, className, } = props;
11
11
  // Dev warning for missing nav label
12
12
  if (import.meta.env.DEV && !ariaLabel && !ariaLabelledBy) {
13
13
  console.warn('StepList: Navigation landmark requires a label. ' +
14
- 'Provide either `ariaLabel` or `ariaLabelledBy` prop for screen reader users.');
14
+ 'Provide either `aria-label` or `aria-labelledby` prop for screen reader users.');
15
15
  }
16
16
  const rootClassName = cx('tui-steplist', className);
17
17
  // Memoize context value to prevent unnecessary re-renders of all items
@@ -5,14 +5,14 @@ export type { StepStatus };
5
5
  export type StepListProps = {
6
6
  /**
7
7
  * Accessible label for the navigation landmark.
8
- * Use either `ariaLabel` or `ariaLabelledBy`, not both.
8
+ * Use either `aria-label` or `aria-labelledby`, not both.
9
9
  */
10
- ariaLabel?: string;
10
+ 'aria-label'?: string;
11
11
  /**
12
12
  * ID of a visible heading that labels the navigation.
13
- * Use either `ariaLabel` or `ariaLabelledBy`, not both.
13
+ * Use either `aria-label` or `aria-labelledby`, not both.
14
14
  */
15
- ariaLabelledBy?: string;
15
+ 'aria-labelledby'?: string;
16
16
  /**
17
17
  * What `aria-current` value to use for the current item.
18
18
  * - `'step'`: For step-by-step progressions (default)
@@ -0,0 +1,9 @@
1
+ export declare const Switch: import("react").ForwardRefExoticComponent<Omit<import("react").ButtonHTMLAttributes<HTMLButtonElement>, "role" | "aria-checked" | "onChange"> & {
2
+ checked?: boolean;
3
+ defaultChecked?: boolean;
4
+ onCheckedChange?: (checked: boolean) => void;
5
+ size?: import("..").SizeStandard;
6
+ label?: import("react").ReactNode;
7
+ disabled?: boolean;
8
+ className?: string;
9
+ } & import("react").RefAttributes<HTMLButtonElement>>;
@@ -0,0 +1,91 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { forwardRef, useEffect, useRef } from 'react';
3
+ import { cx } from '../../utils/cx.js';
4
+ import { composeRefs } from '../../utils/compose-refs.js';
5
+ import { useControllableState } from '../../utils/use-controllable-state.js';
6
+ import { isDev } from '../../utils/is-dev.js';
7
+ // =============================================================================
8
+ // Switch Component
9
+ // =============================================================================
10
+ //
11
+ // <button role="switch"> with animated thumb. Uses new tui-switch SCSS.
12
+ // Existing tui-toggle CSS untouched (stays for CSS-only usage).
13
+ //
14
+ // Bare (no label): returns <button> directly for Field.Control cloneElement.
15
+ // With label: wraps in <label>, native label click focuses the button.
16
+ //
17
+ // CSS token API:
18
+ // --tui-switch-accent Accent color for on state
19
+ // --tui-switch-track-off Track color when off
20
+ //
21
+ // =============================================================================
22
+ // Props that should route to the <button>, not the wrapper
23
+ const BUTTON_PROPS = new Set([
24
+ 'id',
25
+ 'aria-describedby',
26
+ 'aria-invalid',
27
+ 'aria-required',
28
+ 'aria-label',
29
+ 'aria-labelledby',
30
+ 'form',
31
+ 'tabIndex',
32
+ 'onFocus',
33
+ 'onBlur',
34
+ ]);
35
+ export const Switch = forwardRef(function Switch({ checked: controlledChecked, defaultChecked = false, onCheckedChange, size = 'md', label, disabled, className, ...rest }, externalRef) {
36
+ const internalRef = useRef(null);
37
+ const sizeClass = size !== 'md' ? `is-size-${size}` : undefined;
38
+ const [checked, setChecked] = useControllableState({
39
+ value: controlledChecked,
40
+ defaultValue: defaultChecked,
41
+ onChange: onCheckedChange,
42
+ });
43
+ // Dev-only: warn if bare switch has no accessible name (fire once)
44
+ const hasWarnedRef = useRef(false);
45
+ useEffect(() => {
46
+ if (hasWarnedRef.current)
47
+ return;
48
+ if (isDev() && !label) {
49
+ const hasName = 'aria-label' in rest || 'aria-labelledby' in rest;
50
+ if (!hasName) {
51
+ console.warn('Switch: Missing accessible name. Provide label prop, aria-label, or aria-labelledby.');
52
+ hasWarnedRef.current = true;
53
+ }
54
+ }
55
+ // eslint-disable-next-line react-hooks/exhaustive-deps
56
+ }, []);
57
+ // Extract onClick from rest so prop spreading can't override internal handler
58
+ const { onClick: onClickProp, ...restWithoutClick } = rest;
59
+ const handleClick = (e) => {
60
+ setChecked((prev) => !prev);
61
+ onClickProp?.(e);
62
+ };
63
+ const isChecked = checked ?? false;
64
+ // Bare: no label — Field.Control can inject id/aria-* directly
65
+ if (!label) {
66
+ return (_jsx("button", { ref: composeRefs(internalRef, externalRef), type: "button", role: "switch", "aria-checked": isChecked, disabled: disabled, className: cx('tui-switch__track', sizeClass, isChecked && 'is-checked', className), onClick: handleClick, ...restWithoutClick, children: _jsx("span", { className: "tui-switch__thumb" }) }));
67
+ }
68
+ // Split rest props: some go on button, some on wrapper
69
+ const buttonProps = {};
70
+ const wrapperProps = {};
71
+ for (const [key, val] of Object.entries(restWithoutClick)) {
72
+ if (BUTTON_PROPS.has(key)) {
73
+ buttonProps[key] = val;
74
+ }
75
+ else {
76
+ wrapperProps[key] = val;
77
+ }
78
+ }
79
+ // With label: <label> wrapper provides click-anywhere-to-toggle natively.
80
+ // We intercept the label click to toggle state and prevent the browser's
81
+ // default label→button activation (which would double-toggle).
82
+ const handleLabelClick = (e) => {
83
+ // Prevent native label click from also activating the button
84
+ e.preventDefault();
85
+ if (disabled)
86
+ return;
87
+ internalRef.current?.focus();
88
+ setChecked((prev) => !prev);
89
+ };
90
+ return (_jsxs("label", { className: cx('tui-switch', sizeClass, disabled && 'is-disabled', className), onClick: handleLabelClick, ...wrapperProps, children: [_jsx("button", { ref: composeRefs(internalRef, externalRef), type: "button", role: "switch", "aria-checked": isChecked, disabled: disabled, className: cx('tui-switch__track', isChecked && 'is-checked'), ...buttonProps, children: _jsx("span", { className: "tui-switch__thumb" }) }), _jsx("span", { className: "tui-switch__label", children: label })] }));
91
+ });
@@ -0,0 +1,2 @@
1
+ export { Switch } from './Switch';
2
+ export type { SwitchProps } from './types';
@@ -0,0 +1 @@
1
+ export { Switch } from './Switch.js';
@@ -0,0 +1,11 @@
1
+ import type { ButtonHTMLAttributes, ReactNode } from 'react';
2
+ import type { SizeStandard } from '../../types/sizes';
3
+ export type SwitchProps = Omit<ButtonHTMLAttributes<HTMLButtonElement>, 'role' | 'onChange' | 'aria-checked'> & {
4
+ checked?: boolean;
5
+ defaultChecked?: boolean;
6
+ onCheckedChange?: (checked: boolean) => void;
7
+ size?: SizeStandard;
8
+ label?: ReactNode;
9
+ disabled?: boolean;
10
+ className?: string;
11
+ };
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,8 @@
1
+ export declare const TextInput: import("react").ForwardRefExoticComponent<Omit<import("react").InputHTMLAttributes<HTMLInputElement>, "size" | "type" | "prefix"> & {
2
+ type?: import("./types").TextInputType;
3
+ size?: import("..").SizeStandard;
4
+ prefix?: import("react").ReactNode;
5
+ suffix?: import("react").ReactNode;
6
+ className?: string;
7
+ inputClassName?: string;
8
+ } & import("react").RefAttributes<HTMLInputElement>>;
@@ -0,0 +1,25 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { forwardRef } from 'react';
3
+ import { cx } from '../../utils/cx.js';
4
+ // =============================================================================
5
+ // TextInput Component
6
+ // =============================================================================
7
+ //
8
+ // A controlled text input with optional prefix/suffix slots.
9
+ // Works with Field.Control for label, helper text, and error wiring.
10
+ //
11
+ // CSS token API (inherited from input styles):
12
+ // --tui-input-bg Background color
13
+ // --tui-input-fg Foreground/text color
14
+ // --tui-input-border Border color
15
+ // --tui-input-border-focus Focus state border color
16
+ // --tui-input-border-invalid Invalid state border color
17
+ // --tui-input-radius Border radius
18
+ //
19
+ // =============================================================================
20
+ export const TextInput = forwardRef(function TextInput({ type = 'text', size = 'md', prefix, suffix, className, inputClassName, disabled, ...rest }, ref) {
21
+ const sizeClass = size !== 'md' ? `is-size-${size}` : undefined;
22
+ // Always wrap in tui-input-group so className consistently targets the root.
23
+ // ARIA/form props from Field.Control reach the <input> via ...rest.
24
+ return (_jsxs("div", { className: cx('tui-input-group', sizeClass, disabled && 'is-disabled', className), children: [prefix && _jsx("span", { className: "tui-input-group__prefix", children: prefix }), _jsx("input", { ref: ref, type: type, disabled: disabled, className: cx('tui-input', inputClassName), ...rest }), suffix && _jsx("span", { className: "tui-input-group__suffix", children: suffix })] }));
25
+ });
@@ -0,0 +1,2 @@
1
+ export { TextInput } from './TextInput';
2
+ export type { TextInputProps, TextInputType } from './types';
@@ -0,0 +1 @@
1
+ export { TextInput } from './TextInput.js';
@@ -0,0 +1,32 @@
1
+ import type { ReactNode, InputHTMLAttributes } from 'react';
2
+ import type { SizeStandard } from '../../types/sizes';
3
+ export type TextInputType = 'text' | 'email' | 'password' | 'url' | 'tel' | 'search' | 'number';
4
+ export type TextInputProps = Omit<InputHTMLAttributes<HTMLInputElement>, 'type' | 'prefix' | 'size'> & {
5
+ /**
6
+ * Input type.
7
+ * @default 'text'
8
+ */
9
+ type?: TextInputType;
10
+ /**
11
+ * Control size.
12
+ * @default 'md'
13
+ */
14
+ size?: SizeStandard;
15
+ /**
16
+ * Content to render before the input (icon, text).
17
+ */
18
+ prefix?: ReactNode;
19
+ /**
20
+ * Content to render after the input (icon, button).
21
+ */
22
+ suffix?: ReactNode;
23
+ /**
24
+ * Additional class name applied to the root wrapper element.
25
+ */
26
+ className?: string;
27
+ /**
28
+ * Class name applied directly to the `<input>` element.
29
+ * Use for utilities like `tui-input-reset` that must target the input itself.
30
+ */
31
+ inputClassName?: string;
32
+ };
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,6 @@
1
+ export declare const Textarea: import("react").ForwardRefExoticComponent<Omit<import("react").TextareaHTMLAttributes<HTMLTextAreaElement>, "size"> & {
2
+ size?: import("..").SizeStandard;
3
+ resize?: import("./types").TextareaResize;
4
+ autoResize?: boolean;
5
+ className?: string;
6
+ } & import("react").RefAttributes<HTMLTextAreaElement>>;
@@ -0,0 +1,49 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import { forwardRef, useCallback, useEffect, useRef } from 'react';
3
+ import { cx } from '../../utils/cx.js';
4
+ import { composeRefs } from '../../utils/compose-refs.js';
5
+ // =============================================================================
6
+ // Textarea Component
7
+ // =============================================================================
8
+ //
9
+ // A controlled textarea with optional auto-resize.
10
+ // Works with Field.Control for label, helper text, and error wiring.
11
+ //
12
+ // CSS token API (inherited from input styles):
13
+ // --tui-input-bg Background color
14
+ // --tui-input-fg Foreground/text color
15
+ // --tui-input-border Border color
16
+ // --tui-input-border-focus Focus state border color
17
+ // --tui-input-border-invalid Invalid state border color
18
+ // --tui-input-radius Border radius
19
+ //
20
+ // =============================================================================
21
+ export const Textarea = forwardRef(function Textarea({ size = 'md', resize = 'vertical', autoResize = false, className, onInput, ...rest }, externalRef) {
22
+ const internalRef = useRef(null);
23
+ const sizeClass = size !== 'md' ? `is-size-${size}` : undefined;
24
+ // Auto-resize: adjust height to fit content
25
+ const adjustHeight = useCallback(() => {
26
+ const el = internalRef.current;
27
+ if (!el)
28
+ return;
29
+ // Reset to auto so scrollHeight reflects actual content height
30
+ el.style.height = 'auto';
31
+ el.style.height = `${el.scrollHeight}px`;
32
+ }, []);
33
+ // Size on mount and when value changes externally (controlled).
34
+ // Track rest.value so we only re-measure when content actually changes,
35
+ // not on every render.
36
+ const value = rest.value;
37
+ useEffect(() => {
38
+ if (autoResize) {
39
+ adjustHeight();
40
+ }
41
+ }, [autoResize, adjustHeight, value]);
42
+ const handleInput = useCallback((e) => {
43
+ if (autoResize) {
44
+ adjustHeight();
45
+ }
46
+ onInput?.(e);
47
+ }, [autoResize, adjustHeight, onInput]);
48
+ return (_jsx("textarea", { ref: composeRefs(internalRef, externalRef), className: cx('tui-textarea', sizeClass, autoResize && 'is-autoresize', className), style: !autoResize ? { resize } : undefined, onInput: handleInput, ...rest }));
49
+ });
@@ -0,0 +1,2 @@
1
+ export { Textarea } from './Textarea';
2
+ export type { TextareaProps, TextareaResize } from './types';