@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,15 @@
1
+ /**
2
+ * Compose two event handlers into one.
3
+ * Internal handler runs first, then user handler (if not defaultPrevented).
4
+ *
5
+ * Use this instead of relying on spread order when combining handlers
6
+ * from different sources (e.g., Floating UI props + our handlers).
7
+ *
8
+ * @example
9
+ * const props = getReferenceProps({
10
+ * onPointerDown: composeEventHandlers(ourHandler, floatingHandler),
11
+ * });
12
+ */
13
+ export declare function composeEventHandlers<E extends {
14
+ defaultPrevented?: boolean;
15
+ }>(internalHandler: ((event: E) => void) | undefined, userHandler: ((event: E) => void) | undefined): ((event: E) => void) | undefined;
@@ -0,0 +1,27 @@
1
+ /**
2
+ * Compose two event handlers into one.
3
+ * Internal handler runs first, then user handler (if not defaultPrevented).
4
+ *
5
+ * Use this instead of relying on spread order when combining handlers
6
+ * from different sources (e.g., Floating UI props + our handlers).
7
+ *
8
+ * @example
9
+ * const props = getReferenceProps({
10
+ * onPointerDown: composeEventHandlers(ourHandler, floatingHandler),
11
+ * });
12
+ */
13
+ export function composeEventHandlers(internalHandler, userHandler) {
14
+ if (!internalHandler && !userHandler)
15
+ return undefined;
16
+ if (!internalHandler)
17
+ return userHandler;
18
+ if (!userHandler)
19
+ return internalHandler;
20
+ return (event) => {
21
+ internalHandler(event);
22
+ // Only call user handler if internal didn't prevent default
23
+ if (!event.defaultPrevented) {
24
+ userHandler(event);
25
+ }
26
+ };
27
+ }
@@ -0,0 +1,15 @@
1
+ /**
2
+ * FNV-1a 32-bit hash function.
3
+ * Fast, deterministic, low collision risk for typical string inputs.
4
+ * Used for generating stable DOM IDs from arbitrary values.
5
+ */
6
+ /**
7
+ * Generate a short, stable hash string from any value.
8
+ * Returns base36-encoded FNV-1a hash (6-7 chars).
9
+ *
10
+ * @example
11
+ * hashForId('apple') // 'k7x2m1'
12
+ * hashForId('foo/bar') // 'a3b4c5'
13
+ * hashForId('foo:bar') // 'd6e7f8' (different from foo/bar)
14
+ */
15
+ export declare function hashForId(value: string | number): string;
package/utils/hash.js ADDED
@@ -0,0 +1,32 @@
1
+ /**
2
+ * FNV-1a 32-bit hash function.
3
+ * Fast, deterministic, low collision risk for typical string inputs.
4
+ * Used for generating stable DOM IDs from arbitrary values.
5
+ */
6
+ const FNV_OFFSET_BASIS = 2166136261;
7
+ const FNV_PRIME = 16777619;
8
+ /**
9
+ * Compute FNV-1a 32-bit hash of a string.
10
+ */
11
+ function fnv1a32(str) {
12
+ let hash = FNV_OFFSET_BASIS;
13
+ for (let i = 0; i < str.length; i++) {
14
+ hash ^= str.charCodeAt(i);
15
+ hash = Math.imul(hash, FNV_PRIME);
16
+ }
17
+ // Convert to unsigned 32-bit
18
+ return hash >>> 0;
19
+ }
20
+ /**
21
+ * Generate a short, stable hash string from any value.
22
+ * Returns base36-encoded FNV-1a hash (6-7 chars).
23
+ *
24
+ * @example
25
+ * hashForId('apple') // 'k7x2m1'
26
+ * hashForId('foo/bar') // 'a3b4c5'
27
+ * hashForId('foo:bar') // 'd6e7f8' (different from foo/bar)
28
+ */
29
+ export function hashForId(value) {
30
+ const str = typeof value === 'string' ? value : String(value);
31
+ return fnv1a32(str).toString(36);
32
+ }
package/utils/index.d.ts CHANGED
@@ -8,3 +8,6 @@ export { mergeProps } from './merge-props';
8
8
  export { cx } from './cx';
9
9
  export { getContrastTone, getContrastForeground } from './color/contrast';
10
10
  export type { ContrastTone } from './color/contrast';
11
+ export { hashForId } from './hash';
12
+ export { composeEventHandlers } from './compose-events';
13
+ export { useControllableState } from './use-controllable-state';
package/utils/index.js CHANGED
@@ -14,3 +14,9 @@ export { mergeProps } from './merge-props.js';
14
14
  export { cx } from './cx.js';
15
15
  // Contrast utilities (for user-selected colors)
16
16
  export { getContrastTone, getContrastForeground } from './color/contrast.js';
17
+ // Hash utilities (for stable DOM IDs)
18
+ export { hashForId } from './hash.js';
19
+ // Event composition
20
+ export { composeEventHandlers } from './compose-events.js';
21
+ // Controllable state hook
22
+ export { useControllableState } from './use-controllable-state.js';
@@ -0,0 +1,5 @@
1
+ /**
2
+ * Check if we're in a development environment.
3
+ * Uses Vite's import.meta.env.DEV — safe for SSR and production builds.
4
+ */
5
+ export declare function isDev(): boolean;
@@ -0,0 +1,7 @@
1
+ /**
2
+ * Check if we're in a development environment.
3
+ * Uses Vite's import.meta.env.DEV — safe for SSR and production builds.
4
+ */
5
+ export function isDev() {
6
+ return typeof import.meta !== 'undefined' && import.meta.env?.DEV === true;
7
+ }
@@ -0,0 +1,19 @@
1
+ /**
2
+ * Shared controlled/uncontrolled state hook.
3
+ *
4
+ * - When `value` is provided (not `undefined`): controlled mode. Setter only fires `onChange`.
5
+ * - When `value` is `undefined`: uncontrolled mode. Setter updates internal state AND fires `onChange`.
6
+ * - `isControlled` override: for components where `undefined` is a valid controlled value
7
+ * (e.g. ChipGroup single-select deselection). Pass `true` to force controlled mode
8
+ * even when value is `undefined`.
9
+ * - Supports functional updates: `setValue(prev => next)`.
10
+ * - `defaultValue` seeds the internal state (callers decide their own fallback).
11
+ * - The returned setter is stable across renders.
12
+ */
13
+ export declare function useControllableState<T>({ value, defaultValue, onChange, isControlled: isControlledOverride, }: {
14
+ value?: T;
15
+ defaultValue?: T;
16
+ onChange?: (value: T) => void;
17
+ /** Force controlled mode even when value is undefined. */
18
+ isControlled?: boolean;
19
+ }): [T | undefined, (next: T | ((prev: T | undefined) => T)) => void];
@@ -0,0 +1,59 @@
1
+ import { useState, useCallback, useRef, useEffect } from 'react';
2
+ import { isDev as checkIsDev } from './is-dev.js';
3
+ /**
4
+ * Shared controlled/uncontrolled state hook.
5
+ *
6
+ * - When `value` is provided (not `undefined`): controlled mode. Setter only fires `onChange`.
7
+ * - When `value` is `undefined`: uncontrolled mode. Setter updates internal state AND fires `onChange`.
8
+ * - `isControlled` override: for components where `undefined` is a valid controlled value
9
+ * (e.g. ChipGroup single-select deselection). Pass `true` to force controlled mode
10
+ * even when value is `undefined`.
11
+ * - Supports functional updates: `setValue(prev => next)`.
12
+ * - `defaultValue` seeds the internal state (callers decide their own fallback).
13
+ * - The returned setter is stable across renders.
14
+ */
15
+ export function useControllableState({ value, defaultValue, onChange, isControlled: isControlledOverride, }) {
16
+ const [internalValue, setInternalValue] = useState(defaultValue);
17
+ // Keep refs so the setter callback stays stable
18
+ const onChangeRef = useRef(onChange);
19
+ onChangeRef.current = onChange;
20
+ const internalRef = useRef(internalValue);
21
+ internalRef.current = internalValue;
22
+ const isControlled = isControlledOverride ?? value !== undefined;
23
+ const currentValue = isControlled ? value : internalValue;
24
+ // Ref for stable setter — tracks controlled state
25
+ const isControlledRef = useRef(isControlled);
26
+ isControlledRef.current = isControlled;
27
+ const controlledValueRef = useRef(value);
28
+ controlledValueRef.current = value;
29
+ // Dev-only: warn if component switches between controlled and uncontrolled
30
+ const wasControlledRef = useRef(isControlled);
31
+ useEffect(() => {
32
+ if (checkIsDev()) {
33
+ if (wasControlledRef.current && !isControlled) {
34
+ console.warn('useControllableState: Switching from controlled to uncontrolled. This is likely a bug. Decide between using a controlled or uncontrolled value for the lifetime of the component.');
35
+ }
36
+ if (!wasControlledRef.current && isControlled) {
37
+ console.warn('useControllableState: Switching from uncontrolled to controlled. This is likely a bug. Decide between using a controlled or uncontrolled value for the lifetime of the component.');
38
+ }
39
+ wasControlledRef.current = isControlled;
40
+ }
41
+ }, [isControlled]);
42
+ const setValue = useCallback((next) => {
43
+ // Resolve functional updates using the appropriate source of truth
44
+ const prev = isControlledRef.current
45
+ ? controlledValueRef.current
46
+ : internalRef.current;
47
+ const resolvedNext = typeof next === 'function'
48
+ ? next(prev)
49
+ : next;
50
+ if (!isControlledRef.current) {
51
+ // Uncontrolled — update internal state
52
+ setInternalValue(resolvedNext);
53
+ internalRef.current = resolvedNext;
54
+ }
55
+ onChangeRef.current?.(resolvedNext);
56
+ }, [] // Stable — reads everything from refs
57
+ );
58
+ return [currentValue, setValue];
59
+ }
@@ -0,0 +1,33 @@
1
+ import type { OptionValue } from './value-key';
2
+ export type RovingItemRecord = {
3
+ value: OptionValue;
4
+ element: HTMLElement;
5
+ disabled: boolean;
6
+ /** Internal — assigned by registry on first mount */
7
+ mountIndex: number;
8
+ };
9
+ type UseRovingGroupOptions = {
10
+ selectedValue: OptionValue | undefined;
11
+ onSelect: (value: OptionValue) => void;
12
+ /** Group-level disabled. Default false. */
13
+ disabled?: boolean;
14
+ /** Wrap around at boundaries. Default true. */
15
+ loop?: boolean;
16
+ /** Layout orientation. Default 'horizontal'. */
17
+ orientation?: 'horizontal' | 'vertical';
18
+ /**
19
+ * Whether only orientation-matching arrow keys navigate.
20
+ *
21
+ * - `false` (default): All 4 arrows work — WAI-ARIA APG Radio Group pattern.
22
+ * - `true`: Only orientation-matching arrows work — Tabs/Toolbar pattern.
23
+ */
24
+ orientationKeyboard?: boolean;
25
+ };
26
+ export declare function useRovingGroup({ selectedValue, onSelect, disabled, loop, orientation, orientationKeyboard, }: UseRovingGroupOptions): {
27
+ registerItem: (record: Omit<RovingItemRecord, "mountIndex">) => void;
28
+ unregisterItem: (value: OptionValue) => void;
29
+ getOrderedItems: () => readonly RovingItemRecord[];
30
+ focusableValue: OptionValue | undefined;
31
+ handleKeyDown: (event: React.KeyboardEvent) => void;
32
+ };
33
+ export {};
@@ -0,0 +1,123 @@
1
+ import { useCallback, useMemo, useRef, useState } from 'react';
2
+ import { toKey } from './value-key.js';
3
+ export function useRovingGroup({ selectedValue, onSelect, disabled = false, loop = true, orientation = 'horizontal', orientationKeyboard = false, }) {
4
+ const [registryVersion, setRegistryVersion] = useState(0);
5
+ const itemsRef = useRef(new Map());
6
+ const mountCounterRef = useRef(0);
7
+ // ── Item registration ──────────────────────────────────────────────────
8
+ const registerItem = useCallback((record) => {
9
+ const key = toKey(record.value);
10
+ const existing = itemsRef.current.get(key);
11
+ itemsRef.current.set(key, {
12
+ ...record,
13
+ mountIndex: existing?.mountIndex ?? mountCounterRef.current++,
14
+ });
15
+ setRegistryVersion((v) => v + 1);
16
+ }, []);
17
+ const unregisterItem = useCallback((value) => {
18
+ itemsRef.current.delete(toKey(value));
19
+ setRegistryVersion((v) => v + 1);
20
+ }, []);
21
+ // ── DOM-order sort ─────────────────────────────────────────────────────
22
+ const getOrderedItems = useCallback(() => {
23
+ const items = [...itemsRef.current.values()];
24
+ return items.sort((a, b) => {
25
+ const position = a.element.compareDocumentPosition(b.element);
26
+ if (position & Node.DOCUMENT_POSITION_FOLLOWING)
27
+ return -1;
28
+ if (position & Node.DOCUMENT_POSITION_PRECEDING)
29
+ return 1;
30
+ return a.mountIndex - b.mountIndex;
31
+ });
32
+ }, []);
33
+ // ── Focusable value (which item gets tabIndex={0}) ─────────────────────
34
+ const focusableValue = useMemo(() => {
35
+ const items = getOrderedItems();
36
+ const enabledItems = items.filter((item) => !item.disabled && !disabled);
37
+ if (enabledItems.length === 0)
38
+ return undefined;
39
+ // Selected + enabled item gets focus
40
+ if (selectedValue !== undefined) {
41
+ const selectedItem = items.find((item) => toKey(item.value) === toKey(selectedValue));
42
+ if (selectedItem && !selectedItem.disabled && !disabled) {
43
+ return selectedValue;
44
+ }
45
+ }
46
+ // Otherwise first enabled item
47
+ return enabledItems[0].value;
48
+ // eslint-disable-next-line react-hooks/exhaustive-deps
49
+ }, [selectedValue, disabled, getOrderedItems, registryVersion]);
50
+ // ── Keyboard navigation ────────────────────────────────────────────────
51
+ const handleKeyDown = useCallback((event) => {
52
+ const items = getOrderedItems().filter((item) => !item.disabled && !disabled);
53
+ if (items.length === 0)
54
+ return;
55
+ let currentIndex = selectedValue !== undefined
56
+ ? items.findIndex((item) => toKey(item.value) === toKey(selectedValue))
57
+ : -1;
58
+ if (currentIndex === -1)
59
+ currentIndex = 0;
60
+ // Determine which keys map to next/prev
61
+ let nextKeys;
62
+ let prevKeys;
63
+ if (orientationKeyboard) {
64
+ // Only orientation-matching arrows (Tabs/Toolbar pattern)
65
+ nextKeys =
66
+ orientation === 'horizontal' ? ['ArrowRight'] : ['ArrowDown'];
67
+ prevKeys =
68
+ orientation === 'horizontal' ? ['ArrowLeft'] : ['ArrowUp'];
69
+ }
70
+ else {
71
+ // All 4 arrows (Radio Group pattern)
72
+ nextKeys = ['ArrowRight', 'ArrowDown'];
73
+ prevKeys = ['ArrowLeft', 'ArrowUp'];
74
+ }
75
+ let targetIndex = null;
76
+ if (nextKeys.includes(event.key)) {
77
+ event.preventDefault();
78
+ if (loop) {
79
+ targetIndex = (currentIndex + 1) % items.length;
80
+ }
81
+ else {
82
+ targetIndex = Math.min(currentIndex + 1, items.length - 1);
83
+ }
84
+ }
85
+ else if (prevKeys.includes(event.key)) {
86
+ event.preventDefault();
87
+ if (loop) {
88
+ targetIndex = (currentIndex - 1 + items.length) % items.length;
89
+ }
90
+ else {
91
+ targetIndex = Math.max(currentIndex - 1, 0);
92
+ }
93
+ }
94
+ else if (event.key === 'Home') {
95
+ event.preventDefault();
96
+ targetIndex = 0;
97
+ }
98
+ else if (event.key === 'End') {
99
+ event.preventDefault();
100
+ targetIndex = items.length - 1;
101
+ }
102
+ if (targetIndex !== null && targetIndex !== currentIndex) {
103
+ const targetItem = items[targetIndex];
104
+ targetItem.element.focus();
105
+ onSelect(targetItem.value);
106
+ }
107
+ }, [
108
+ getOrderedItems,
109
+ disabled,
110
+ selectedValue,
111
+ orientationKeyboard,
112
+ orientation,
113
+ loop,
114
+ onSelect,
115
+ ]);
116
+ return {
117
+ registerItem,
118
+ unregisterItem,
119
+ getOrderedItems,
120
+ focusableValue,
121
+ handleKeyDown,
122
+ };
123
+ }
@@ -0,0 +1,16 @@
1
+ /**
2
+ * Shared value-key utility for selection components.
3
+ *
4
+ * All components that track selection by value (Select, MultiSelect, Combobox,
5
+ * Radio, ChipGroup) use this to normalise string | number values into string
6
+ * keys for Map/Set lookups and equality comparisons.
7
+ */
8
+ /**
9
+ * Value type accepted by selection components.
10
+ */
11
+ export type OptionValue = string | number;
12
+ /**
13
+ * Normalise a value to a string key for Map/Set lookups.
14
+ * Prefixes with type indicator to avoid collisions between 1 (number) and "1" (string).
15
+ */
16
+ export declare function toKey(value: OptionValue): string;
@@ -0,0 +1,14 @@
1
+ /**
2
+ * Shared value-key utility for selection components.
3
+ *
4
+ * All components that track selection by value (Select, MultiSelect, Combobox,
5
+ * Radio, ChipGroup) use this to normalise string | number values into string
6
+ * keys for Map/Set lookups and equality comparisons.
7
+ */
8
+ /**
9
+ * Normalise a value to a string key for Map/Set lookups.
10
+ * Prefixes with type indicator to avoid collisions between 1 (number) and "1" (string).
11
+ */
12
+ export function toKey(value) {
13
+ return typeof value === 'number' ? `n:${value}` : `s:${value}`;
14
+ }