@tangible/ui 0.0.1 → 0.0.2

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 (129) 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/MultiSelect/MultiSelect.d.ts +39 -0
  39. package/components/MultiSelect/MultiSelect.js +623 -0
  40. package/components/MultiSelect/MultiSelectContext.d.ts +20 -0
  41. package/components/MultiSelect/MultiSelectContext.js +56 -0
  42. package/components/MultiSelect/index.d.ts +2 -0
  43. package/components/MultiSelect/index.js +1 -0
  44. package/components/MultiSelect/types.d.ts +218 -0
  45. package/components/MultiSelect/types.js +3 -0
  46. package/components/Notice/Notice.d.ts +1 -1
  47. package/components/Notice/Notice.js +1 -1
  48. package/components/Progress/Progress.js +1 -1
  49. package/components/Progress/types.d.ts +7 -7
  50. package/components/Radio/Radio.d.ts +2 -0
  51. package/components/Radio/Radio.js +50 -0
  52. package/components/Radio/RadioGroup.d.ts +2 -0
  53. package/components/Radio/RadioGroup.js +54 -0
  54. package/components/Radio/RadioGroupContext.d.ts +3 -0
  55. package/components/Radio/RadioGroupContext.js +9 -0
  56. package/components/Radio/index.d.ts +8 -0
  57. package/components/Radio/index.js +6 -0
  58. package/components/Radio/types.d.ts +32 -0
  59. package/components/Radio/types.js +1 -0
  60. package/components/Rating/Rating.d.ts +5 -5
  61. package/components/Rating/Rating.js +2 -2
  62. package/components/SegmentedControl/SegmentedControl.js +20 -104
  63. package/components/SegmentedControl/types.d.ts +4 -8
  64. package/components/Select/Select.d.ts +39 -0
  65. package/components/Select/Select.js +497 -0
  66. package/components/Select/SelectContext.d.ts +20 -0
  67. package/components/Select/SelectContext.js +56 -0
  68. package/components/Select/index.d.ts +3 -0
  69. package/components/Select/index.js +1 -0
  70. package/components/Select/types.d.ts +216 -0
  71. package/components/Select/types.js +11 -0
  72. package/components/Sidebar/Sidebar.js +12 -12
  73. package/components/Sidebar/types.d.ts +5 -5
  74. package/components/StepIndicator/StepIndicator.js +1 -1
  75. package/components/StepList/StepList.js +2 -2
  76. package/components/StepList/types.d.ts +4 -4
  77. package/components/Switch/Switch.d.ts +9 -0
  78. package/components/Switch/Switch.js +91 -0
  79. package/components/Switch/index.d.ts +2 -0
  80. package/components/Switch/index.js +1 -0
  81. package/components/Switch/types.d.ts +11 -0
  82. package/components/Switch/types.js +1 -0
  83. package/components/TextInput/TextInput.d.ts +8 -0
  84. package/components/TextInput/TextInput.js +25 -0
  85. package/components/TextInput/index.d.ts +2 -0
  86. package/components/TextInput/index.js +1 -0
  87. package/components/TextInput/types.d.ts +32 -0
  88. package/components/TextInput/types.js +1 -0
  89. package/components/Textarea/Textarea.d.ts +6 -0
  90. package/components/Textarea/Textarea.js +49 -0
  91. package/components/Textarea/index.d.ts +2 -0
  92. package/components/Textarea/index.js +1 -0
  93. package/components/Textarea/types.d.ts +25 -0
  94. package/components/Textarea/types.js +1 -0
  95. package/components/index.d.ts +20 -0
  96. package/components/index.js +10 -0
  97. package/icons/icons.svg +1 -0
  98. package/icons/manifest.json +8 -0
  99. package/icons/registry.d.ts +2 -0
  100. package/icons/registry.js +1 -0
  101. package/icons/system/index.d.ts +2 -0
  102. package/icons/system/index.js +11 -0
  103. package/package.json +1 -1
  104. package/styles/all.css +1 -1
  105. package/styles/all.expanded.css +1187 -96
  106. package/styles/all.expanded.unlayered.css +1187 -96
  107. package/styles/all.unlayered.css +1 -1
  108. package/styles/components/_bundle.scss +20 -0
  109. package/styles/components/input/index.scss +5 -20
  110. package/styles/index.scss +16 -0
  111. package/styles/system/_control.scss +34 -0
  112. package/styles/system/_tokens.scss +8 -0
  113. package/styles/system/index.scss +2 -1
  114. package/styles/utilities/_index.scss +50 -0
  115. package/tui-manifest.json +632 -61
  116. package/utils/compose-events.d.ts +15 -0
  117. package/utils/compose-events.js +27 -0
  118. package/utils/hash.d.ts +15 -0
  119. package/utils/hash.js +32 -0
  120. package/utils/index.d.ts +3 -0
  121. package/utils/index.js +6 -0
  122. package/utils/is-dev.d.ts +5 -0
  123. package/utils/is-dev.js +7 -0
  124. package/utils/use-controllable-state.d.ts +19 -0
  125. package/utils/use-controllable-state.js +59 -0
  126. package/utils/use-roving-group.d.ts +33 -0
  127. package/utils/use-roving-group.js +123 -0
  128. package/utils/value-key.d.ts +16 -0
  129. package/utils/value-key.js +14 -0
@@ -0,0 +1,56 @@
1
+ import { createContext, useContext } from 'react';
2
+ // =============================================================================
3
+ // Split Contexts for Performance
4
+ // =============================================================================
5
+ // Actions context: stable config, IDs, refs, callbacks — rarely changes
6
+ // State context: open, value, activeIndex, etc. — changes on interaction
7
+ //
8
+ // Components subscribe to what they need:
9
+ // - Options: actions (register, toggle) + minimal state (isSelected check)
10
+ // - Trigger: both (needs state for display, actions for handlers)
11
+ // - Content: both (needs floating styles, refs)
12
+ // =============================================================================
13
+ export const MultiSelectActionsContext = createContext(null);
14
+ export const MultiSelectStateContext = createContext(null);
15
+ /**
16
+ * Access stable config, IDs, refs, and callbacks.
17
+ * Safe to use without causing rerenders on navigation changes.
18
+ */
19
+ export function useMultiSelectActions() {
20
+ const context = useContext(MultiSelectActionsContext);
21
+ if (!context) {
22
+ throw new Error('MultiSelect components must be used within a MultiSelect');
23
+ }
24
+ return context;
25
+ }
26
+ /**
27
+ * Access reactive state: open, value, activeIndex, orderedOptions.
28
+ * Subscribing causes rerender when these values change.
29
+ */
30
+ export function useMultiSelectState() {
31
+ const context = useContext(MultiSelectStateContext);
32
+ if (!context) {
33
+ throw new Error('MultiSelect components must be used within a MultiSelect');
34
+ }
35
+ return context;
36
+ }
37
+ /**
38
+ * Combined hook for components that need both.
39
+ * Convenience for Trigger/Content that need everything.
40
+ */
41
+ export function useMultiSelectContext() {
42
+ const actions = useMultiSelectActions();
43
+ const state = useMultiSelectState();
44
+ return { ...actions, ...state };
45
+ }
46
+ // =============================================================================
47
+ // Content Context (for Option registration)
48
+ // =============================================================================
49
+ export const MultiSelectContentContext = createContext(null);
50
+ export function useMultiSelectContentContext() {
51
+ const context = useContext(MultiSelectContentContext);
52
+ if (!context) {
53
+ throw new Error('MultiSelect.Option must be used within MultiSelect.Content');
54
+ }
55
+ return context;
56
+ }
@@ -0,0 +1,2 @@
1
+ export { MultiSelect, MultiSelectTrigger, MultiSelectContent, MultiSelectOption, MultiSelectGroup, MultiSelectLabel, useMultiSelect, } from './MultiSelect';
2
+ export type { MultiSelectProps, MultiSelectTriggerProps, MultiSelectContentProps, MultiSelectOptionProps, MultiSelectGroupProps, MultiSelectLabelProps, MultiSelectValue, OptionValue, DisplayMode, RegisteredOption as MultiSelectRegisteredOption, } from './types';
@@ -0,0 +1 @@
1
+ export { MultiSelect, MultiSelectTrigger, MultiSelectContent, MultiSelectOption, MultiSelectGroup, MultiSelectLabel, useMultiSelect, } from './MultiSelect.js';
@@ -0,0 +1,218 @@
1
+ import type { FloatingContext, ReferenceType } from '@floating-ui/react';
2
+ import type { RefObject, CSSProperties, MutableRefObject } from 'react';
3
+ import type { SizeStandard } from '../../types/sizes';
4
+ import { toKey, type OptionValue } from '../../utils/value-key';
5
+ export { toKey, type OptionValue };
6
+ /**
7
+ * Value type for MultiSelect - arrays of strings or numbers.
8
+ */
9
+ export type MultiSelectValue = Array<string | number>;
10
+ /**
11
+ * Display mode for the trigger.
12
+ */
13
+ export type DisplayMode = 'count' | 'chips';
14
+ export type MultiSelectProps = {
15
+ /**
16
+ * Control size.
17
+ * @default 'md'
18
+ */
19
+ size?: SizeStandard;
20
+ /**
21
+ * Controlled selected values.
22
+ */
23
+ value?: MultiSelectValue;
24
+ /**
25
+ * Default values for uncontrolled usage.
26
+ */
27
+ defaultValue?: MultiSelectValue;
28
+ /**
29
+ * Callback when selection changes.
30
+ */
31
+ onValueChange?: (value: MultiSelectValue) => 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 values are selected.
52
+ */
53
+ placeholder?: string;
54
+ /**
55
+ * Display mode for the trigger.
56
+ * - 'count': Shows "N selected"
57
+ * - 'chips': Shows visual chips for selected items
58
+ * @default 'count'
59
+ */
60
+ display?: DisplayMode;
61
+ /**
62
+ * Maximum number of chips to show before "+N more" badge.
63
+ * Only applies when display='chips'.
64
+ * @default 3
65
+ */
66
+ maxChips?: number;
67
+ /**
68
+ * Maximum number of selections allowed.
69
+ * When reached, unselected options become disabled.
70
+ */
71
+ max?: number;
72
+ /**
73
+ * Callback when max selections is reached.
74
+ */
75
+ onMaxReached?: () => void;
76
+ /**
77
+ * Accessible name for the select.
78
+ */
79
+ 'aria-label'?: string;
80
+ /**
81
+ * ID of element that labels this select.
82
+ */
83
+ 'aria-labelledby'?: string;
84
+ /**
85
+ * ID of element(s) that describe this select.
86
+ */
87
+ 'aria-describedby'?: string;
88
+ /**
89
+ * Optional trigger ID override.
90
+ * Useful for Field integration via htmlFor.
91
+ */
92
+ id?: string;
93
+ children: React.ReactNode;
94
+ };
95
+ export type MultiSelectTriggerProps = {
96
+ /**
97
+ * When true, merges props onto the child element instead of wrapping.
98
+ * Child must be a single React element that accepts ref and event handlers.
99
+ * @default false
100
+ */
101
+ asChild?: boolean;
102
+ /**
103
+ * Additional CSS class names.
104
+ */
105
+ className?: string;
106
+ children?: React.ReactNode;
107
+ };
108
+ export type MultiSelectContentProps = {
109
+ /**
110
+ * Additional CSS class names.
111
+ */
112
+ className?: string;
113
+ children: React.ReactNode;
114
+ };
115
+ export type MultiSelectOptionProps = {
116
+ /**
117
+ * The value for this option. Required and must be unique.
118
+ * Can be string or number.
119
+ */
120
+ value: OptionValue;
121
+ /**
122
+ * Whether this option is disabled.
123
+ * @default false
124
+ */
125
+ disabled?: boolean;
126
+ /**
127
+ * Text value for typeahead and chip labels.
128
+ * Required when children is not a string.
129
+ * If children is a string, defaults to that.
130
+ */
131
+ textValue?: string;
132
+ /**
133
+ * Additional CSS class names.
134
+ */
135
+ className?: string;
136
+ children: React.ReactNode;
137
+ };
138
+ export type MultiSelectGroupProps = {
139
+ /**
140
+ * Additional CSS class names.
141
+ */
142
+ className?: string;
143
+ children: React.ReactNode;
144
+ };
145
+ export type MultiSelectLabelProps = {
146
+ /**
147
+ * Additional CSS class names.
148
+ */
149
+ className?: string;
150
+ children: React.ReactNode;
151
+ };
152
+ /**
153
+ * Registration record for an option.
154
+ */
155
+ export type RegisteredOption = {
156
+ value: OptionValue;
157
+ ref: RefObject<HTMLElement | null>;
158
+ disabled: boolean;
159
+ textValue: string;
160
+ };
161
+ /**
162
+ * Stable context: config, IDs, refs, and stable callbacks.
163
+ * Rarely changes — safe to subscribe without rerender concerns.
164
+ */
165
+ export type MultiSelectActionsContextValue = {
166
+ disabled: boolean;
167
+ placeholder: string;
168
+ display: DisplayMode;
169
+ maxChips: number;
170
+ max: number | undefined;
171
+ size: SizeStandard;
172
+ triggerId: string;
173
+ listboxId: string;
174
+ ariaLabel?: string;
175
+ ariaLabelledBy?: string;
176
+ ariaDescribedBy?: string;
177
+ setOpen: (open: boolean) => void;
178
+ toggleOption: (optionValue: OptionValue, textValue: string) => void;
179
+ clearAll: () => void;
180
+ registerOption: (option: RegisteredOption) => void;
181
+ unregisterOption: (value: OptionValue) => void;
182
+ refs: {
183
+ reference: React.RefObject<ReferenceType | null>;
184
+ floating: React.RefObject<HTMLElement | null>;
185
+ setReference: (node: ReferenceType | null) => void;
186
+ setFloating: (node: HTMLElement | null) => void;
187
+ };
188
+ listRef: MutableRefObject<(HTMLElement | null)[]>;
189
+ getReferenceProps: (userProps?: React.HTMLProps<Element>) => Record<string, unknown>;
190
+ getFloatingProps: (userProps?: React.HTMLProps<HTMLElement>) => Record<string, unknown>;
191
+ getItemProps: (userProps?: React.HTMLProps<HTMLElement>) => Record<string, unknown>;
192
+ };
193
+ /**
194
+ * State context: values that change during interaction.
195
+ * Subscribe only when you need reactive updates.
196
+ */
197
+ export type MultiSelectStateContextValue = {
198
+ open: boolean;
199
+ value: MultiSelectValue;
200
+ maxReached: boolean;
201
+ isSelected: (optionValue: OptionValue) => boolean;
202
+ getSelectedOptions: () => RegisteredOption[];
203
+ activeIndex: number | null;
204
+ highlightedValue: OptionValue | null;
205
+ orderedOptions: RegisteredOption[];
206
+ floatingStyles: CSSProperties;
207
+ floatingContext: FloatingContext;
208
+ };
209
+ /**
210
+ * Combined context value (for backwards compat and convenience hooks).
211
+ * @deprecated Prefer using useMultiSelectActions + useMultiSelectState separately.
212
+ */
213
+ export type MultiSelectContextValue = MultiSelectActionsContextValue & MultiSelectStateContextValue;
214
+ export type MultiSelectContentContextValue = {
215
+ listRef: MutableRefObject<(HTMLElement | null)[]>;
216
+ activeIndex: number | null;
217
+ orderedOptions: RegisteredOption[];
218
+ };
@@ -0,0 +1,3 @@
1
+ import { toKey } from '../../utils/value-key.js';
2
+ // Re-export shared value-key types so existing consumers don't break
3
+ export { toKey };
@@ -38,7 +38,7 @@ export type NoticeProps<TAs extends RootAs = 'section'> = CommonProps & {
38
38
  * Accessible name when there is no visible title in Notice.Head.
39
39
  * Only use when Notice.Head has no title prop - if both exist, the visible title takes precedence.
40
40
  */
41
- ariaLabel?: string;
41
+ 'aria-label'?: string;
42
42
  /** Renders a dismiss button */
43
43
  dismissible?: boolean;
44
44
  onDismiss?: () => void;
@@ -9,7 +9,7 @@ function prefersReducedMotion() {
9
9
  return false;
10
10
  return window.matchMedia?.('(prefers-reduced-motion: reduce)').matches ?? false;
11
11
  }
12
- export const Notice = forwardRef(function Notice({ as = 'section', inline, elevated, interactive, disabled, stripe, className, style, onClick, theme = 'info', announce = 'off', ariaLabel, dismissible, onDismiss, dismissLabel = 'Dismiss', exitAnimation = 'none', focusable, children, ...rest }, ref) {
12
+ export const Notice = forwardRef(function Notice({ as = 'section', inline, elevated, interactive, disabled, stripe, className, style, onClick, theme = 'info', announce = 'off', 'aria-label': ariaLabel, dismissible, onDismiss, dismissLabel = 'Dismiss', exitAnimation = 'none', focusable, children, ...rest }, ref) {
13
13
  const Tag = as;
14
14
  const innerRef = useRef(null);
15
15
  const [isExiting, setIsExiting] = useState(false);
@@ -6,7 +6,7 @@ import { cx } from '../../utils/cx.js';
6
6
  // COMPONENT
7
7
  // =============================================================================
8
8
  export function Progress(props) {
9
- const { children, mode = 'line', size = 'md', max = 100, showLabels = true, labelledBy, ariaLabel, defaultLabel, className, } = props;
9
+ const { children, mode = 'line', size = 'md', max = 100, showLabels = true, 'aria-labelledby': labelledBy, 'aria-label': ariaLabel, defaultLabel, className, } = props;
10
10
  // Determine mode
11
11
  const isSegmented = 'segments' in props && Array.isArray(props.segments);
12
12
  // Standard mode props
@@ -60,17 +60,17 @@ export type BaseProgressProps = {
60
60
  showLabels?: boolean;
61
61
  /**
62
62
  * ARIA: ID of an element that labels this progress bar.
63
- * Takes precedence over `ariaLabel` and `defaultLabel`.
63
+ * Takes precedence over `aria-label` and `defaultLabel`.
64
64
  */
65
- labelledBy?: string;
65
+ 'aria-labelledby'?: string;
66
66
  /**
67
67
  * ARIA: Accessible name for the progress bar.
68
68
  * Use when no visible label exists. Takes precedence over `defaultLabel`.
69
69
  */
70
- ariaLabel?: string;
70
+ 'aria-label'?: string;
71
71
  /**
72
72
  * Fallback accessible name, rendered as visually-hidden text.
73
- * Only used if neither `labelledBy` nor `ariaLabel` is provided.
73
+ * Only used if neither `aria-labelledby` nor `aria-label` is provided.
74
74
  */
75
75
  defaultLabel?: string;
76
76
  /**
@@ -97,7 +97,7 @@ export type BaseProgressProps = {
97
97
  * { value: 24, label: 'Progress' },
98
98
  * { value: 38, label: 'Expected' },
99
99
  * ]}
100
- * ariaLabel="Student progress"
100
+ * aria-label="Student progress"
101
101
  * />
102
102
  * ```
103
103
  */
@@ -129,7 +129,7 @@ export type SegmentedProgressProps = BaseProgressProps & {
129
129
  * @example
130
130
  * ```tsx
131
131
  * // Basic bar with no label
132
- * <Progress value={42} ariaLabel="Upload progress" />
132
+ * <Progress value={42} aria-label="Upload progress" />
133
133
  *
134
134
  * // With labels above the bar (split left/right)
135
135
  * <Progress
@@ -140,7 +140,7 @@ export type SegmentedProgressProps = BaseProgressProps & {
140
140
  * />
141
141
  *
142
142
  * // Indeterminate (loading)
143
- * <Progress indeterminate ariaLabel="Loading" />
143
+ * <Progress indeterminate aria-label="Loading" />
144
144
  * ```
145
145
  */
146
146
  export type StandardProgressProps = BaseProgressProps & {
@@ -0,0 +1,2 @@
1
+ import type { RadioProps } from './types';
2
+ export declare const Radio: import("react").ForwardRefExoticComponent<RadioProps & import("react").RefAttributes<HTMLButtonElement>>;
@@ -0,0 +1,50 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } 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
+ import { toKey } from '../../utils/value-key.js';
6
+ import { isDev } from '../../utils/is-dev.js';
7
+ import { useRadioGroupContext } from './RadioGroupContext.js';
8
+ // =============================================================================
9
+ // Radio Component
10
+ // =============================================================================
11
+ //
12
+ // <button role="radio"> inside a RadioGroup. Not a native <input>.
13
+ // Uses roving tabindex — only the selected (or first enabled) item gets tabIndex={0}.
14
+ // Arrow keys in the group move focus AND select.
15
+ //
16
+ // =============================================================================
17
+ export const Radio = forwardRef(function Radio({ value, label, disabled = false, className }, externalRef) {
18
+ const { selectedValue, focusableValue, rootDisabled, registerItem, unregisterItem, onSelect, } = useRadioGroupContext();
19
+ const isSelected = selectedValue !== undefined && toKey(selectedValue) === toKey(value);
20
+ const isDisabled = rootDisabled || disabled;
21
+ const isFocusable = focusableValue !== undefined && toKey(focusableValue) === toKey(value);
22
+ // Dev-only: warn if Radio has no accessible name (fire once)
23
+ const hasWarnedRef = useRef(false);
24
+ useEffect(() => {
25
+ if (hasWarnedRef.current)
26
+ return;
27
+ if (isDev() && !label) {
28
+ console.warn('Radio: Missing accessible name. Provide a label prop.');
29
+ hasWarnedRef.current = true;
30
+ }
31
+ // eslint-disable-next-line react-hooks/exhaustive-deps
32
+ }, []);
33
+ // Callback ref for registration
34
+ const callbackRef = useCallback((node) => {
35
+ if (node) {
36
+ registerItem({ value, element: node, disabled });
37
+ }
38
+ else {
39
+ unregisterItem(value);
40
+ }
41
+ }, [value, disabled, registerItem, unregisterItem]);
42
+ // Click handler
43
+ const handleClick = () => {
44
+ if (isDisabled)
45
+ return;
46
+ onSelect(value);
47
+ };
48
+ return (_jsxs("button", { ref: composeRefs(callbackRef, externalRef), type: "button", role: "radio", className: cx('tui-radio', className), "aria-checked": isSelected, disabled: isDisabled, tabIndex: isFocusable ? 0 : -1, onClick: handleClick, children: [_jsx("span", { className: "tui-radio__indicator", "aria-hidden": "true" }), label && _jsx("span", { className: "tui-radio__label", children: label })] }));
49
+ });
50
+ Radio.displayName = 'Radio';
@@ -0,0 +1,2 @@
1
+ import type { RadioGroupProps } from './types';
2
+ export declare function RadioGroup({ value: controlledValue, defaultValue, onValueChange, disabled, orientation, loop, 'aria-label': ariaLabel, 'aria-labelledby': ariaLabelledBy, className, children, }: RadioGroupProps): import("react/jsx-runtime").JSX.Element;
@@ -0,0 +1,54 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import { useCallback, useEffect, useMemo } from 'react';
3
+ import { cx } from '../../utils/cx.js';
4
+ import { useControllableState } from '../../utils/use-controllable-state.js';
5
+ import { useRovingGroup } from '../../utils/use-roving-group.js';
6
+ import { RadioGroupContext } from './RadioGroupContext.js';
7
+ import { isDev } from '../../utils/is-dev.js';
8
+ // =============================================================================
9
+ // RadioGroup Component
10
+ // =============================================================================
11
+ //
12
+ // <div role="radiogroup"> with roving tabindex and arrow key navigation.
13
+ // Radio items are <button role="radio"> — not native inputs.
14
+ // Arrow keys move focus AND select (WAI-ARIA APG pattern).
15
+ //
16
+ // CSS token API:
17
+ // --tui-radio-accent Accent color for selected state
18
+ //
19
+ // =============================================================================
20
+ export function RadioGroup({ value: controlledValue, defaultValue, onValueChange, disabled = false, orientation = 'vertical', loop = true, 'aria-label': ariaLabel, 'aria-labelledby': ariaLabelledBy, className, children, }) {
21
+ const [selectedValue, setSelectedValue] = useControllableState({
22
+ value: controlledValue,
23
+ defaultValue,
24
+ onChange: onValueChange,
25
+ });
26
+ // Selection handler
27
+ const onSelect = useCallback((newValue) => {
28
+ setSelectedValue(newValue);
29
+ }, [setSelectedValue]);
30
+ const { registerItem, unregisterItem, focusableValue, handleKeyDown, } = useRovingGroup({
31
+ selectedValue,
32
+ onSelect,
33
+ disabled,
34
+ loop,
35
+ orientation,
36
+ orientationKeyboard: false,
37
+ });
38
+ // Dev-only: Warn if missing accessible name
39
+ useEffect(() => {
40
+ if (isDev() && !ariaLabel && !ariaLabelledBy) {
41
+ console.warn('RadioGroup: Missing accessible name. Provide aria-label or aria-labelledby.');
42
+ }
43
+ }, [ariaLabel, ariaLabelledBy]);
44
+ const contextValue = useMemo(() => ({
45
+ selectedValue,
46
+ focusableValue,
47
+ rootDisabled: disabled,
48
+ orientation,
49
+ registerItem,
50
+ unregisterItem,
51
+ onSelect,
52
+ }), [selectedValue, focusableValue, disabled, orientation, registerItem, unregisterItem, onSelect]);
53
+ return (_jsx(RadioGroupContext.Provider, { value: contextValue, children: _jsx("div", { role: "radiogroup", className: cx('tui-radio-group', orientation === 'horizontal' && 'is-horizontal', className), "aria-label": ariaLabel, "aria-labelledby": ariaLabelledBy, "aria-disabled": disabled || undefined, "aria-orientation": orientation === 'horizontal' ? 'horizontal' : undefined, onKeyDown: handleKeyDown, children: children }) }));
54
+ }
@@ -0,0 +1,3 @@
1
+ import type { RadioGroupContextValue } from './types';
2
+ export declare const RadioGroupContext: import("react").Context<RadioGroupContextValue | null>;
3
+ export declare function useRadioGroupContext(): RadioGroupContextValue;
@@ -0,0 +1,9 @@
1
+ import { createContext, useContext } from 'react';
2
+ export const RadioGroupContext = createContext(null);
3
+ export function useRadioGroupContext() {
4
+ const context = useContext(RadioGroupContext);
5
+ if (!context) {
6
+ throw new Error('Radio must be used within a RadioGroup');
7
+ }
8
+ return context;
9
+ }
@@ -0,0 +1,8 @@
1
+ import { RadioGroup as RadioGroupRoot } from './RadioGroup';
2
+ import { Radio as RadioItem } from './Radio';
3
+ type RadioGroupCompound = typeof RadioGroupRoot & {
4
+ Item: typeof RadioItem;
5
+ };
6
+ export declare const RadioGroup: RadioGroupCompound;
7
+ export { Radio } from './Radio';
8
+ export type { RadioGroupProps, RadioProps, } from './types';
@@ -0,0 +1,6 @@
1
+ import { RadioGroup as RadioGroupRoot } from './RadioGroup.js';
2
+ import { Radio as RadioItem } from './Radio.js';
3
+ export const RadioGroup = RadioGroupRoot;
4
+ RadioGroup.Item = RadioItem;
5
+ // Named exports
6
+ export { Radio } from './Radio.js';
@@ -0,0 +1,32 @@
1
+ import type { ReactNode } from 'react';
2
+ import type { OptionValue } from '../../utils/value-key';
3
+ import type { RovingItemRecord } from '../../utils/use-roving-group';
4
+ export type RadioGroupProps = {
5
+ value?: OptionValue;
6
+ defaultValue?: OptionValue;
7
+ onValueChange?: (value: OptionValue) => void;
8
+ disabled?: boolean;
9
+ orientation?: 'horizontal' | 'vertical';
10
+ loop?: boolean;
11
+ 'aria-label'?: string;
12
+ 'aria-labelledby'?: string;
13
+ className?: string;
14
+ children: ReactNode;
15
+ };
16
+ export type RadioProps = {
17
+ value: OptionValue;
18
+ label?: ReactNode;
19
+ disabled?: boolean;
20
+ className?: string;
21
+ };
22
+ export type RadioItemRecord = RovingItemRecord;
23
+ export type RadioGroupContextValue = {
24
+ selectedValue: OptionValue | undefined;
25
+ /** The value of the item that should receive tabIndex={0} */
26
+ focusableValue: OptionValue | undefined;
27
+ rootDisabled: boolean;
28
+ orientation: 'horizontal' | 'vertical';
29
+ registerItem: (record: Omit<RadioItemRecord, 'mountIndex'>) => void;
30
+ unregisterItem: (value: OptionValue) => void;
31
+ onSelect: (value: OptionValue) => void;
32
+ };
@@ -0,0 +1 @@
1
+ export {};
@@ -2,7 +2,7 @@ import type { SizeStandard, Theme as ThemeFull } from '../../types';
2
2
  type Size = SizeStandard;
3
3
  type Theme = ThemeFull;
4
4
  export type RatingProps = {
5
- /** Controlled value (1..max). Use with onChange */
5
+ /** Controlled value (1..max). Use with onValueChange */
6
6
  value?: number;
7
7
  /** Uncontrolled initial value */
8
8
  defaultValue?: number;
@@ -18,15 +18,15 @@ export type RatingProps = {
18
18
  size?: Size;
19
19
  /** Theme feeds foreground color tokens */
20
20
  theme?: Theme;
21
- /** Called on change (controlled or uncontrolled) */
22
- onChange?: (value: number) => void;
21
+ /** Called when the value changes */
22
+ onValueChange?: (value: number) => void;
23
23
  /** Allow clicking the current selection to clear back to 0 */
24
24
  allowClear?: boolean;
25
25
  className?: string;
26
26
  /** Gap override (e.g. '0.25rem') – otherwise uses density utilities */
27
27
  gap?: string;
28
28
  /** Accessible label for the rating group. Defaults to "Rating: X of Y" */
29
- ariaLabel?: string;
29
+ 'aria-label'?: string;
30
30
  };
31
- export declare function Rating({ value, defaultValue, max, disabled, readOnly, name, size, theme, onChange, allowClear, className, gap, ariaLabel, }: RatingProps): import("react/jsx-runtime").JSX.Element;
31
+ export declare function Rating({ value, defaultValue, max, disabled, readOnly, name, size, theme, onValueChange, allowClear, className, gap, 'aria-label': ariaLabel, }: RatingProps): import("react/jsx-runtime").JSX.Element;
32
32
  export {};
@@ -2,7 +2,7 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import React from 'react';
3
3
  import { cx } from '../../utils/cx.js';
4
4
  import { Icon } from '../Icon/index.js';
5
- export function Rating({ value, defaultValue = 0, max = 5, disabled, readOnly, name, size = 'lg', theme = 'secondary', onChange, allowClear, className, gap, ariaLabel, }) {
5
+ export function Rating({ value, defaultValue = 0, max = 5, disabled, readOnly, name, size = 'lg', theme = 'secondary', onValueChange, allowClear, className, gap, 'aria-label': ariaLabel, }) {
6
6
  const isControlled = value != null;
7
7
  const [internal, setInternal] = React.useState(defaultValue);
8
8
  const current = isControlled ? value : internal;
@@ -13,7 +13,7 @@ export function Rating({ value, defaultValue = 0, max = 5, disabled, readOnly, n
13
13
  return;
14
14
  if (!isControlled)
15
15
  setInternal(v);
16
- onChange?.(v);
16
+ onValueChange?.(v);
17
17
  };
18
18
  const handleSelect = (n) => {
19
19
  if (allowClear && current === n)