@tangible/ui 0.0.1

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 (212) hide show
  1. package/README.md +100 -0
  2. package/components/Accordion/Accordion.d.ts +22 -0
  3. package/components/Accordion/Accordion.js +192 -0
  4. package/components/Accordion/AccordionContext.d.ts +5 -0
  5. package/components/Accordion/AccordionContext.js +23 -0
  6. package/components/Accordion/index.d.ts +2 -0
  7. package/components/Accordion/index.js +1 -0
  8. package/components/Accordion/types.d.ts +61 -0
  9. package/components/Accordion/types.js +1 -0
  10. package/components/Avatar/Avatar.d.ts +11 -0
  11. package/components/Avatar/Avatar.js +67 -0
  12. package/components/Avatar/AvatarGroup.d.ts +11 -0
  13. package/components/Avatar/AvatarGroup.js +45 -0
  14. package/components/Avatar/index.d.ts +9 -0
  15. package/components/Avatar/index.js +7 -0
  16. package/components/Avatar/types.d.ts +44 -0
  17. package/components/Avatar/types.js +12 -0
  18. package/components/Button/Button.d.ts +4 -0
  19. package/components/Button/Button.js +33 -0
  20. package/components/Button/index.d.ts +2 -0
  21. package/components/Button/index.js +1 -0
  22. package/components/Button/types.d.ts +127 -0
  23. package/components/Button/types.js +1 -0
  24. package/components/Card/Card.d.ts +29 -0
  25. package/components/Card/Card.js +47 -0
  26. package/components/Card/index.d.ts +2 -0
  27. package/components/Card/index.js +1 -0
  28. package/components/Chip/Chip.d.ts +24 -0
  29. package/components/Chip/Chip.js +37 -0
  30. package/components/Chip/index.d.ts +2 -0
  31. package/components/Chip/index.js +1 -0
  32. package/components/Chips/Chips.d.ts +31 -0
  33. package/components/Chips/Chips.js +21 -0
  34. package/components/Chips/index.d.ts +2 -0
  35. package/components/Chips/index.js +1 -0
  36. package/components/ContentIndicator/ContentIndicator.d.ts +2 -0
  37. package/components/ContentIndicator/ContentIndicator.js +21 -0
  38. package/components/ContentIndicator/index.d.ts +2 -0
  39. package/components/ContentIndicator/index.js +1 -0
  40. package/components/ContentIndicator/types.d.ts +57 -0
  41. package/components/ContentIndicator/types.js +1 -0
  42. package/components/Dropdown/Dropdown.d.ts +31 -0
  43. package/components/Dropdown/Dropdown.js +219 -0
  44. package/components/Dropdown/DropdownContext.d.ts +3 -0
  45. package/components/Dropdown/DropdownContext.js +9 -0
  46. package/components/Dropdown/index.d.ts +2 -0
  47. package/components/Dropdown/index.js +1 -0
  48. package/components/Dropdown/types.d.ts +102 -0
  49. package/components/Dropdown/types.js +8 -0
  50. package/components/Icon/Icon.d.ts +22 -0
  51. package/components/Icon/Icon.js +24 -0
  52. package/components/Icon/index.d.ts +2 -0
  53. package/components/Icon/index.js +1 -0
  54. package/components/IconButton/IconButton.d.ts +2 -0
  55. package/components/IconButton/IconButton.js +50 -0
  56. package/components/IconButton/index.d.ts +2 -0
  57. package/components/IconButton/index.js +1 -0
  58. package/components/IconButton/types.d.ts +79 -0
  59. package/components/IconButton/types.js +1 -0
  60. package/components/Modal/Modal.d.ts +52 -0
  61. package/components/Modal/Modal.js +133 -0
  62. package/components/Modal/context.d.ts +6 -0
  63. package/components/Modal/context.js +9 -0
  64. package/components/Modal/index.d.ts +2 -0
  65. package/components/Modal/index.js +1 -0
  66. package/components/Notice/Notice.d.ts +93 -0
  67. package/components/Notice/Notice.js +144 -0
  68. package/components/Notice/index.d.ts +2 -0
  69. package/components/Notice/index.js +1 -0
  70. package/components/OverlapStack/OverlapStack.d.ts +44 -0
  71. package/components/OverlapStack/OverlapStack.js +41 -0
  72. package/components/OverlapStack/index.d.ts +2 -0
  73. package/components/OverlapStack/index.js +1 -0
  74. package/components/Pager/Pager.d.ts +26 -0
  75. package/components/Pager/Pager.js +151 -0
  76. package/components/Pager/index.d.ts +2 -0
  77. package/components/Pager/index.js +1 -0
  78. package/components/Progress/Progress.d.ts +2 -0
  79. package/components/Progress/Progress.js +100 -0
  80. package/components/Progress/index.d.ts +4 -0
  81. package/components/Progress/index.js +2 -0
  82. package/components/Progress/types.d.ts +251 -0
  83. package/components/Progress/types.js +1 -0
  84. package/components/Progress/useProgressSegments.d.ts +40 -0
  85. package/components/Progress/useProgressSegments.js +42 -0
  86. package/components/Rating/Rating.d.ts +32 -0
  87. package/components/Rating/Rating.js +74 -0
  88. package/components/Rating/index.d.ts +2 -0
  89. package/components/Rating/index.js +1 -0
  90. package/components/SegmentedControl/SegmentedControl.d.ts +10 -0
  91. package/components/SegmentedControl/SegmentedControl.js +183 -0
  92. package/components/SegmentedControl/SegmentedControlContext.d.ts +3 -0
  93. package/components/SegmentedControl/SegmentedControlContext.js +9 -0
  94. package/components/SegmentedControl/index.d.ts +2 -0
  95. package/components/SegmentedControl/index.js +1 -0
  96. package/components/SegmentedControl/types.d.ts +63 -0
  97. package/components/SegmentedControl/types.js +1 -0
  98. package/components/Sidebar/Sidebar.d.ts +17 -0
  99. package/components/Sidebar/Sidebar.js +107 -0
  100. package/components/Sidebar/index.d.ts +2 -0
  101. package/components/Sidebar/index.js +1 -0
  102. package/components/Sidebar/types.d.ts +65 -0
  103. package/components/Sidebar/types.js +4 -0
  104. package/components/StepIndicator/StepIndicator.d.ts +2 -0
  105. package/components/StepIndicator/StepIndicator.js +64 -0
  106. package/components/StepIndicator/index.d.ts +2 -0
  107. package/components/StepIndicator/index.js +1 -0
  108. package/components/StepIndicator/types.d.ts +68 -0
  109. package/components/StepIndicator/types.js +1 -0
  110. package/components/StepList/StepList.d.ts +12 -0
  111. package/components/StepList/StepList.js +59 -0
  112. package/components/StepList/StepListContext.d.ts +3 -0
  113. package/components/StepList/StepListContext.js +9 -0
  114. package/components/StepList/index.d.ts +2 -0
  115. package/components/StepList/index.js +1 -0
  116. package/components/StepList/types.d.ts +91 -0
  117. package/components/StepList/types.js +4 -0
  118. package/components/Table/BulkActionsBar.d.ts +12 -0
  119. package/components/Table/BulkActionsBar.js +9 -0
  120. package/components/Table/DataTable.d.ts +35 -0
  121. package/components/Table/DataTable.js +184 -0
  122. package/components/Table/Pagination.d.ts +13 -0
  123. package/components/Table/Pagination.js +13 -0
  124. package/components/Table/index.d.ts +2 -0
  125. package/components/Table/index.js +1 -0
  126. package/components/Tabs/Tabs.d.ts +23 -0
  127. package/components/Tabs/Tabs.js +309 -0
  128. package/components/Tabs/TabsContext.d.ts +3 -0
  129. package/components/Tabs/TabsContext.js +12 -0
  130. package/components/Tabs/index.d.ts +2 -0
  131. package/components/Tabs/index.js +1 -0
  132. package/components/Tabs/types.d.ts +75 -0
  133. package/components/Tabs/types.js +1 -0
  134. package/components/Toolbar/Toolbar.d.ts +18 -0
  135. package/components/Toolbar/Toolbar.js +241 -0
  136. package/components/Toolbar/index.d.ts +2 -0
  137. package/components/Toolbar/index.js +1 -0
  138. package/components/Toolbar/types.d.ts +28 -0
  139. package/components/Toolbar/types.js +1 -0
  140. package/components/Tooltip/Tooltip.d.ts +15 -0
  141. package/components/Tooltip/Tooltip.js +166 -0
  142. package/components/Tooltip/TooltipContext.d.ts +15 -0
  143. package/components/Tooltip/TooltipContext.js +25 -0
  144. package/components/Tooltip/index.d.ts +2 -0
  145. package/components/Tooltip/index.js +1 -0
  146. package/components/Tooltip/types.d.ts +85 -0
  147. package/components/Tooltip/types.js +8 -0
  148. package/components/index.d.ts +52 -0
  149. package/components/index.js +26 -0
  150. package/constants.d.ts +16 -0
  151. package/constants.js +16 -0
  152. package/icons/cred/index.d.ts +31 -0
  153. package/icons/cred/index.js +136 -0
  154. package/icons/icons.svg +155 -0
  155. package/icons/lms/index.d.ts +21 -0
  156. package/icons/lms/index.js +81 -0
  157. package/icons/manifest.json +1226 -0
  158. package/icons/player/index.d.ts +55 -0
  159. package/icons/player/index.js +268 -0
  160. package/icons/reaction/index.d.ts +79 -0
  161. package/icons/reaction/index.js +400 -0
  162. package/icons/registry.d.ts +316 -0
  163. package/icons/registry.js +163 -0
  164. package/icons/system/index.d.ts +155 -0
  165. package/icons/system/index.js +818 -0
  166. package/package.json +121 -0
  167. package/styles/all.css +1 -0
  168. package/styles/all.expanded.css +4137 -0
  169. package/styles/all.expanded.unlayered.css +4137 -0
  170. package/styles/all.unlayered.css +1 -0
  171. package/styles/components/_bundle.scss +51 -0
  172. package/styles/components/index.scss +1 -0
  173. package/styles/components/input/index.scss +248 -0
  174. package/styles/index.scss +71 -0
  175. package/styles/system/_constants.scss +12 -0
  176. package/styles/system/_motion.scss +47 -0
  177. package/styles/system/_palette-fns.scss +10 -0
  178. package/styles/system/_palettes.scss +80 -0
  179. package/styles/system/_tokens.scss +249 -0
  180. package/styles/system/index.scss +4 -0
  181. package/styles/utilities/_index.scss +373 -0
  182. package/tui-manifest.json +1858 -0
  183. package/types/index.d.ts +2 -0
  184. package/types/index.js +1 -0
  185. package/types/index.ts +2 -0
  186. package/types/sizes.d.ts +17 -0
  187. package/types/sizes.js +10 -0
  188. package/types/sizes.ts +21 -0
  189. package/types/svg.d.ts +5 -0
  190. package/types/themes.d.ts +14 -0
  191. package/types/themes.js +9 -0
  192. package/types/themes.ts +17 -0
  193. package/utils/color/contrast.d.ts +33 -0
  194. package/utils/color/contrast.js +88 -0
  195. package/utils/color-scheme.d.ts +25 -0
  196. package/utils/color-scheme.js +55 -0
  197. package/utils/compose-refs.d.ts +17 -0
  198. package/utils/compose-refs.js +38 -0
  199. package/utils/cx.d.ts +12 -0
  200. package/utils/cx.js +14 -0
  201. package/utils/focus-trap.d.ts +40 -0
  202. package/utils/focus-trap.js +93 -0
  203. package/utils/index.d.ts +10 -0
  204. package/utils/index.js +16 -0
  205. package/utils/math.d.ts +4 -0
  206. package/utils/math.js +19 -0
  207. package/utils/merge-props.d.ts +25 -0
  208. package/utils/merge-props.js +60 -0
  209. package/utils/polymorphic.d.ts +28 -0
  210. package/utils/polymorphic.js +44 -0
  211. package/utils/portal.d.ts +11 -0
  212. package/utils/portal.js +105 -0
@@ -0,0 +1,74 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import React from 'react';
3
+ import { cx } from '../../utils/cx.js';
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, }) {
6
+ const isControlled = value != null;
7
+ const [internal, setInternal] = React.useState(defaultValue);
8
+ const current = isControlled ? value : internal;
9
+ const generatedId = React.useId();
10
+ const groupName = name ?? generatedId;
11
+ const setValue = (v) => {
12
+ if (disabled || readOnly)
13
+ return;
14
+ if (!isControlled)
15
+ setInternal(v);
16
+ onChange?.(v);
17
+ };
18
+ const handleSelect = (n) => {
19
+ if (allowClear && current === n)
20
+ setValue(0);
21
+ else
22
+ setValue(n);
23
+ };
24
+ // Keyboard navigation per WAI-ARIA APG Radio Group pattern
25
+ // Arrow keys auto-select (not just navigate), Space/Enter for allowClear toggle
26
+ const onKeyDown = (e) => {
27
+ if (disabled || readOnly)
28
+ return;
29
+ switch (e.key) {
30
+ case 'ArrowLeft':
31
+ case 'ArrowDown':
32
+ // Move and select previous, wrap to max or stop at 1 based on allowClear
33
+ setValue(current <= 1 ? (allowClear ? max : 1) : current - 1);
34
+ e.preventDefault();
35
+ break;
36
+ case 'ArrowRight':
37
+ case 'ArrowUp':
38
+ // Move and select next, wrap to 1 or stop at max
39
+ setValue(current >= max ? (allowClear ? 1 : max) : current + 1);
40
+ e.preventDefault();
41
+ break;
42
+ case 'Home':
43
+ setValue(1);
44
+ e.preventDefault();
45
+ break;
46
+ case 'End':
47
+ setValue(max);
48
+ e.preventDefault();
49
+ break;
50
+ case ' ':
51
+ case 'Enter':
52
+ // Toggle clear if allowClear and already have selection
53
+ if (allowClear && current > 0) {
54
+ setValue(0);
55
+ }
56
+ else if (current === 0) {
57
+ setValue(1);
58
+ }
59
+ e.preventDefault();
60
+ break;
61
+ default:
62
+ return;
63
+ }
64
+ };
65
+ const defaultAriaLabel = `Rating: ${current} of ${max}`;
66
+ // Roving tabindex: only the selected radio (or first if none) is in tab order
67
+ const focusableIndex = current > 0 ? current : 1;
68
+ return (_jsx("div", { className: cx('tui-rating', `is-size-${size}`, `is-theme-${theme}`, disabled && 'is-disabled', className), style: { gap }, role: readOnly ? 'img' : 'radiogroup', "aria-label": ariaLabel ?? defaultAriaLabel, onKeyDown: readOnly ? undefined : onKeyDown, children: Array.from({ length: max }).map((_, i) => {
69
+ const n = i + 1;
70
+ const checked = current >= n;
71
+ const id = `${groupName}__star-${n}`;
72
+ return (_jsxs("div", { className: "tui-rating__item", children: [!readOnly && (_jsx("input", { className: "tui-visually-hidden", type: "radio", id: id, name: groupName, value: n, checked: current === n, "aria-checked": current === n, tabIndex: n === focusableIndex ? 0 : -1, onChange: () => handleSelect(n), disabled: disabled })), readOnly ? (_jsx("span", { className: cx('tui-rating__star', checked && 'is-active'), children: _jsx(Icon, { name: checked ? 'system/star-fill' : 'system/star-outline' }) })) : (_jsxs("label", { className: cx('tui-rating__star', checked && 'is-active'), htmlFor: id, tabIndex: -1, children: [_jsx(Icon, { name: checked ? 'system/star-fill' : 'system/star-outline' }), _jsx("span", { className: "tui-visually-hidden", children: `${n} of ${max}` })] }))] }, n));
73
+ }) }));
74
+ }
@@ -0,0 +1,2 @@
1
+ export { Rating } from './Rating';
2
+ export type { RatingProps } from './Rating';
@@ -0,0 +1 @@
1
+ export { Rating } from './Rating.js';
@@ -0,0 +1,10 @@
1
+ import React from 'react';
2
+ import type { SegmentedControlProps, SegmentedControlItemProps } from './types';
3
+ type SegmentedControlCompound = {
4
+ (props: SegmentedControlProps): React.JSX.Element;
5
+ displayName?: string;
6
+ Item: typeof Item;
7
+ };
8
+ declare function Item({ value, disabled, icon, 'aria-label': ariaLabel, className, children, }: SegmentedControlItemProps): import("react/jsx-runtime").JSX.Element;
9
+ export declare const SegmentedControl: SegmentedControlCompound;
10
+ export {};
@@ -0,0 +1,183 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react';
3
+ import { cx } from '../../utils/cx.js';
4
+ import { SegmentedControlContext, useSegmentedControlContext } from './SegmentedControlContext.js';
5
+ // =============================================================================
6
+ // SegmentedControl Root
7
+ // =============================================================================
8
+ function SegmentedControlRoot({ value: controlledValue, defaultValue, onValueChange, variant = 'pill', size = 'md', orientation = 'horizontal', loop = true, disabled = false, 'aria-label': ariaLabel, 'aria-labelledby': ariaLabelledBy, className, children, }) {
9
+ const [internalValue, setInternalValue] = useState(defaultValue);
10
+ const [, setRegistryVersion] = useState(0);
11
+ const itemsRef = useRef(new Map());
12
+ const mountCounterRef = useRef(0);
13
+ const isControlled = controlledValue !== undefined;
14
+ const selectedValue = isControlled ? controlledValue : internalValue;
15
+ // Item registration
16
+ const registerItem = useCallback((record) => {
17
+ const existing = itemsRef.current.get(record.value);
18
+ itemsRef.current.set(record.value, {
19
+ ...record,
20
+ mountIndex: existing?.mountIndex ?? mountCounterRef.current++,
21
+ });
22
+ setRegistryVersion((v) => v + 1);
23
+ }, []);
24
+ const unregisterItem = useCallback((value) => {
25
+ itemsRef.current.delete(value);
26
+ setRegistryVersion((v) => v + 1);
27
+ }, []);
28
+ // Get items sorted by DOM order
29
+ const getOrderedItems = useCallback(() => {
30
+ const items = Array.from(itemsRef.current.values());
31
+ return items.sort((a, b) => {
32
+ const position = a.element.compareDocumentPosition(b.element);
33
+ if (position & Node.DOCUMENT_POSITION_FOLLOWING)
34
+ return -1;
35
+ if (position & Node.DOCUMENT_POSITION_PRECEDING)
36
+ return 1;
37
+ return a.mountIndex - b.mountIndex;
38
+ });
39
+ }, []);
40
+ // Selection handler
41
+ const onSelect = useCallback((newValue) => {
42
+ if (isControlled) {
43
+ onValueChange?.(newValue);
44
+ }
45
+ else {
46
+ setInternalValue(newValue);
47
+ onValueChange?.(newValue);
48
+ }
49
+ }, [isControlled, onValueChange]);
50
+ // Dev-only: Warn if missing accessible name
51
+ useEffect(() => {
52
+ // Safe check for dev mode - import.meta.env may not exist in all bundler contexts
53
+ const isDev = typeof import.meta !== 'undefined' && import.meta.env?.DEV;
54
+ if (isDev && !ariaLabel && !ariaLabelledBy) {
55
+ console.warn('SegmentedControl: Missing accessible name. Provide aria-label or aria-labelledby.');
56
+ }
57
+ }, [ariaLabel, ariaLabelledBy]);
58
+ // Keyboard navigation
59
+ const handleKeyDown = (event) => {
60
+ const items = getOrderedItems().filter((item) => !item.disabled && !disabled);
61
+ if (items.length === 0)
62
+ return;
63
+ let currentIndex = items.findIndex((item) => item.value === selectedValue);
64
+ if (currentIndex === -1)
65
+ currentIndex = 0;
66
+ const prevKey = orientation === 'horizontal' ? 'ArrowLeft' : 'ArrowUp';
67
+ const nextKey = orientation === 'horizontal' ? 'ArrowRight' : 'ArrowDown';
68
+ let targetIndex = null;
69
+ switch (event.key) {
70
+ case nextKey:
71
+ event.preventDefault();
72
+ if (loop) {
73
+ targetIndex = (currentIndex + 1) % items.length;
74
+ }
75
+ else {
76
+ targetIndex = Math.min(currentIndex + 1, items.length - 1);
77
+ }
78
+ break;
79
+ case prevKey:
80
+ event.preventDefault();
81
+ if (loop) {
82
+ targetIndex = (currentIndex - 1 + items.length) % items.length;
83
+ }
84
+ else {
85
+ targetIndex = Math.max(currentIndex - 1, 0);
86
+ }
87
+ break;
88
+ case 'Home':
89
+ event.preventDefault();
90
+ targetIndex = 0;
91
+ break;
92
+ case 'End':
93
+ event.preventDefault();
94
+ targetIndex = items.length - 1;
95
+ break;
96
+ }
97
+ if (targetIndex !== null && targetIndex !== currentIndex) {
98
+ const targetItem = items[targetIndex];
99
+ targetItem.element.focus();
100
+ // Radiogroup: arrow keys move focus AND select
101
+ onSelect(targetItem.value);
102
+ }
103
+ };
104
+ const contextValue = useMemo(() => ({
105
+ variant,
106
+ size,
107
+ orientation,
108
+ loop,
109
+ selectedValue,
110
+ rootDisabled: disabled,
111
+ registerItem,
112
+ unregisterItem,
113
+ getOrderedItems,
114
+ onSelect,
115
+ itemsRef,
116
+ }), [
117
+ variant,
118
+ size,
119
+ orientation,
120
+ loop,
121
+ selectedValue,
122
+ disabled,
123
+ registerItem,
124
+ unregisterItem,
125
+ getOrderedItems,
126
+ onSelect,
127
+ ]);
128
+ return (_jsx(SegmentedControlContext.Provider, { value: contextValue, children: _jsx("div", { role: "radiogroup", className: cx('tui-segmented', `is-variant-${variant}`, `is-size-${size}`, orientation === 'vertical' && 'is-vertical', className), "aria-label": ariaLabel, "aria-labelledby": ariaLabelledBy, "aria-disabled": disabled || undefined, "aria-orientation": orientation, onKeyDown: handleKeyDown, children: children }) }));
129
+ }
130
+ // =============================================================================
131
+ // SegmentedControl.Item
132
+ // =============================================================================
133
+ function Item({ value, disabled = false, icon, 'aria-label': ariaLabel, className, children, }) {
134
+ const { selectedValue, rootDisabled, registerItem, unregisterItem, onSelect, getOrderedItems, } = useSegmentedControlContext();
135
+ const isSelected = selectedValue === value;
136
+ const isDisabled = rootDisabled || disabled;
137
+ // Determine which item gets tabIndex={0}
138
+ const getIsFocusable = () => {
139
+ if (isDisabled)
140
+ return false;
141
+ if (isSelected)
142
+ return true;
143
+ // If no selection, first enabled item is focusable
144
+ const items = getOrderedItems();
145
+ const enabledItems = items.filter((item) => !item.disabled && !rootDisabled);
146
+ if (enabledItems.length === 0)
147
+ return false;
148
+ if (selectedValue === undefined) {
149
+ return enabledItems[0].value === value;
150
+ }
151
+ // If selected item is disabled, first enabled gets focus
152
+ const selectedItem = items.find((item) => item.value === selectedValue);
153
+ if (selectedItem?.disabled || rootDisabled) {
154
+ return enabledItems[0].value === value;
155
+ }
156
+ return false;
157
+ };
158
+ const isFocusable = getIsFocusable();
159
+ // Callback ref for registration
160
+ const callbackRef = useCallback((node) => {
161
+ if (node) {
162
+ registerItem({ value, element: node, disabled });
163
+ }
164
+ else {
165
+ unregisterItem(value);
166
+ }
167
+ }, [value, disabled, registerItem, unregisterItem]);
168
+ // Click handler
169
+ const handleClick = () => {
170
+ if (isDisabled)
171
+ return;
172
+ onSelect(value);
173
+ };
174
+ const hasIcon = icon !== undefined && icon !== null;
175
+ const hasChildren = children !== undefined && children !== null;
176
+ return (_jsxs("button", { ref: callbackRef, type: "button", role: "radio", className: cx('tui-segmented__item', className), "aria-checked": isSelected, "aria-disabled": isDisabled || undefined, "aria-label": ariaLabel, tabIndex: isDisabled ? -1 : isFocusable ? 0 : -1, onClick: handleClick, children: [hasIcon && _jsx("span", { className: "tui-segmented__item-icon", children: icon }), hasChildren && _jsx("span", { className: "tui-segmented__item-label", children: children })] }));
177
+ }
178
+ // =============================================================================
179
+ // Compound Component Export
180
+ // =============================================================================
181
+ export const SegmentedControl = SegmentedControlRoot;
182
+ SegmentedControl.Item = Item;
183
+ SegmentedControl.displayName = 'SegmentedControl';
@@ -0,0 +1,3 @@
1
+ import type { SegmentedControlContextValue } from './types';
2
+ export declare const SegmentedControlContext: import("react").Context<SegmentedControlContextValue | null>;
3
+ export declare function useSegmentedControlContext(): SegmentedControlContextValue;
@@ -0,0 +1,9 @@
1
+ import { createContext, useContext } from 'react';
2
+ export const SegmentedControlContext = createContext(null);
3
+ export function useSegmentedControlContext() {
4
+ const context = useContext(SegmentedControlContext);
5
+ if (!context) {
6
+ throw new Error('SegmentedControl compound components must be used within a SegmentedControl');
7
+ }
8
+ return context;
9
+ }
@@ -0,0 +1,2 @@
1
+ export { SegmentedControl } from './SegmentedControl';
2
+ export type { SegmentedControlProps, SegmentedControlItemProps, SegmentedControlValue, SegmentedControlVariant, SegmentedControlSize, SegmentedControlOrientation, } from './types';
@@ -0,0 +1 @@
1
+ export { SegmentedControl } from './SegmentedControl.js';
@@ -0,0 +1,63 @@
1
+ import type { ReactNode } from 'react';
2
+ export type SegmentedControlValue = string | number;
3
+ export type SegmentedControlVariant = 'pill' | 'outline' | 'underline';
4
+ export type SegmentedControlSize = 'sm' | 'md';
5
+ export type SegmentedControlOrientation = 'horizontal' | 'vertical';
6
+ export type SegmentedControlProps = {
7
+ /** Controlled selected value */
8
+ value?: SegmentedControlValue;
9
+ /** Uncontrolled initial value */
10
+ defaultValue?: SegmentedControlValue;
11
+ /** Callback when selection changes */
12
+ onValueChange?: (value: SegmentedControlValue) => void;
13
+ /** Visual style */
14
+ variant?: SegmentedControlVariant;
15
+ /** Size scale */
16
+ size?: SegmentedControlSize;
17
+ /** Layout direction */
18
+ orientation?: SegmentedControlOrientation;
19
+ /** Whether arrow keys wrap around */
20
+ loop?: boolean;
21
+ /** Disable all items */
22
+ disabled?: boolean;
23
+ /** Accessible label */
24
+ 'aria-label'?: string;
25
+ /** ID of element that labels this control */
26
+ 'aria-labelledby'?: string;
27
+ /** Additional classes */
28
+ className?: string;
29
+ children: ReactNode;
30
+ };
31
+ export type SegmentedControlItemProps = {
32
+ /** Unique identifier (required) */
33
+ value: SegmentedControlValue;
34
+ /** Disable this item */
35
+ disabled?: boolean;
36
+ /** Icon element */
37
+ icon?: ReactNode;
38
+ /** Accessible label for icon-only items */
39
+ 'aria-label'?: string;
40
+ /** Additional classes */
41
+ className?: string;
42
+ /** Label content */
43
+ children?: ReactNode;
44
+ };
45
+ export type ItemRecord = {
46
+ value: SegmentedControlValue;
47
+ element: HTMLButtonElement;
48
+ disabled: boolean;
49
+ mountIndex: number;
50
+ };
51
+ export type SegmentedControlContextValue = {
52
+ variant: SegmentedControlVariant;
53
+ size: SegmentedControlSize;
54
+ orientation: SegmentedControlOrientation;
55
+ loop: boolean;
56
+ selectedValue: SegmentedControlValue | undefined;
57
+ rootDisabled: boolean;
58
+ registerItem: (record: Omit<ItemRecord, 'mountIndex'>) => void;
59
+ unregisterItem: (value: SegmentedControlValue) => void;
60
+ getOrderedItems: () => ItemRecord[];
61
+ onSelect: (value: SegmentedControlValue) => void;
62
+ itemsRef: React.RefObject<Map<SegmentedControlValue, ItemRecord>>;
63
+ };
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,17 @@
1
+ import type { SidebarProps, SidebarHeaderProps, SidebarNavProps } from './types';
2
+ declare function SidebarHeader(props: SidebarHeaderProps): import("react/jsx-runtime").JSX.Element;
3
+ declare namespace SidebarHeader {
4
+ var displayName: string;
5
+ }
6
+ declare function SidebarNav(props: SidebarNavProps): import("react/jsx-runtime").JSX.Element;
7
+ declare namespace SidebarNav {
8
+ var displayName: string;
9
+ }
10
+ type SidebarCompound = {
11
+ (props: SidebarProps): React.JSX.Element | null;
12
+ displayName?: string;
13
+ Header: typeof SidebarHeader;
14
+ Nav: typeof SidebarNav;
15
+ };
16
+ export declare const Sidebar: SidebarCompound;
17
+ export {};
@@ -0,0 +1,107 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { useEffect, useRef } from 'react';
3
+ import { useFocusTrap, getInitialFocus } from '../../utils/focus-trap.js';
4
+ const isBrowser = typeof document !== 'undefined';
5
+ // =============================================================================
6
+ // Sidebar (Root)
7
+ // =============================================================================
8
+ function SidebarRoot(props) {
9
+ const { position = 'left', drawer = false, isOpen = false, onClose, ariaLabel, children, className, } = props;
10
+ const sidebarRef = useRef(null);
11
+ const restoreRef = useRef(null);
12
+ const rootClassName = ['tui-sidebar', className].filter(Boolean).join(' ');
13
+ // ---------------------------------------------------------------------------
14
+ // Drawer mode: capture trigger for focus restoration
15
+ // ---------------------------------------------------------------------------
16
+ useEffect(() => {
17
+ if (!drawer || !isBrowser)
18
+ return;
19
+ if (isOpen) {
20
+ // Capture focus target when opening
21
+ restoreRef.current = document.activeElement;
22
+ }
23
+ else {
24
+ // Restore focus when closing
25
+ const el = restoreRef.current;
26
+ if (el && typeof el.focus === 'function') {
27
+ el.focus();
28
+ }
29
+ restoreRef.current = null;
30
+ }
31
+ }, [drawer, isOpen]);
32
+ // ---------------------------------------------------------------------------
33
+ // Drawer mode: body scroll lock
34
+ // ---------------------------------------------------------------------------
35
+ useEffect(() => {
36
+ if (!drawer || !isOpen)
37
+ return;
38
+ document.body.classList.add('tui-sidebar-drawer-open');
39
+ return () => {
40
+ document.body.classList.remove('tui-sidebar-drawer-open');
41
+ };
42
+ }, [drawer, isOpen]);
43
+ // ---------------------------------------------------------------------------
44
+ // Drawer mode: focus trap (handles Tab cycling and ESC to close)
45
+ // ---------------------------------------------------------------------------
46
+ useFocusTrap(sidebarRef, {
47
+ isActive: drawer && isOpen,
48
+ onEscape: onClose,
49
+ });
50
+ // ---------------------------------------------------------------------------
51
+ // Drawer mode: initial focus
52
+ // ---------------------------------------------------------------------------
53
+ useEffect(() => {
54
+ if (!drawer || !isOpen)
55
+ return;
56
+ const sidebar = sidebarRef.current;
57
+ if (!sidebar)
58
+ return;
59
+ const target = getInitialFocus(sidebar);
60
+ target.focus({ preventScroll: true });
61
+ }, [drawer, isOpen]);
62
+ // ---------------------------------------------------------------------------
63
+ // Sidebar content (shared between static and drawer modes)
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 }));
66
+ // ---------------------------------------------------------------------------
67
+ // Static mode: render directly
68
+ // ---------------------------------------------------------------------------
69
+ if (!drawer) {
70
+ return sidebarContent;
71
+ }
72
+ // ---------------------------------------------------------------------------
73
+ // Drawer mode: render inline with fixed positioning (no portal needed)
74
+ // ---------------------------------------------------------------------------
75
+ if (!isOpen) {
76
+ return null;
77
+ }
78
+ const drawerClassName = [
79
+ 'tui-sidebar-drawer',
80
+ position === 'right' && 'is-position-right',
81
+ ]
82
+ .filter(Boolean)
83
+ .join(' ');
84
+ return (_jsxs("div", { className: drawerClassName, "data-position": position, "data-state": "open", children: [_jsx("div", { className: "tui-sidebar-drawer__backdrop", onClick: onClose, "aria-hidden": "true" }), _jsx("div", { className: "tui-sidebar-drawer__panel", children: sidebarContent })] }));
85
+ }
86
+ // =============================================================================
87
+ // Sidebar.Header
88
+ // =============================================================================
89
+ function SidebarHeader(props) {
90
+ const { children, className } = props;
91
+ const headerClassName = ['tui-sidebar__header', className].filter(Boolean).join(' ');
92
+ return _jsx("header", { className: headerClassName, children: children });
93
+ }
94
+ // =============================================================================
95
+ // Sidebar.Nav
96
+ // =============================================================================
97
+ function SidebarNav(props) {
98
+ const { ariaLabel, ariaLabelledBy, children, className } = props;
99
+ const navClassName = ['tui-sidebar__nav', className].filter(Boolean).join(' ');
100
+ return (_jsx("nav", { className: navClassName, "aria-label": ariaLabel, "aria-labelledby": ariaLabelledBy, children: children }));
101
+ }
102
+ SidebarHeader.displayName = 'Sidebar.Header';
103
+ SidebarNav.displayName = 'Sidebar.Nav';
104
+ export const Sidebar = SidebarRoot;
105
+ Sidebar.displayName = 'Sidebar';
106
+ Sidebar.Header = SidebarHeader;
107
+ Sidebar.Nav = SidebarNav;
@@ -0,0 +1,2 @@
1
+ export { Sidebar } from './Sidebar';
2
+ export type { SidebarProps, SidebarHeaderProps, SidebarNavProps, } from './types';
@@ -0,0 +1 @@
1
+ export { Sidebar } from './Sidebar.js';
@@ -0,0 +1,65 @@
1
+ import type { ReactNode } from 'react';
2
+ export type SidebarProps = {
3
+ /**
4
+ * Sidebar position. Affects border placement and drawer slide direction.
5
+ * @default 'left'
6
+ */
7
+ position?: 'left' | 'right';
8
+ /**
9
+ * Enable drawer mode (mobile overlay).
10
+ * When true, sidebar renders via portal with backdrop and focus trap.
11
+ * @default false
12
+ */
13
+ drawer?: boolean;
14
+ /**
15
+ * Drawer open state. Required when `drawer={true}`.
16
+ */
17
+ isOpen?: boolean;
18
+ /**
19
+ * Callback to close the drawer.
20
+ * Called when ESC is pressed or backdrop is clicked.
21
+ */
22
+ onClose?: () => void;
23
+ /**
24
+ * Accessible label for the aside landmark.
25
+ * Typically omit this if using `ariaLabel` on `Sidebar.Nav` instead.
26
+ */
27
+ ariaLabel?: string;
28
+ /**
29
+ * Sidebar content (Header, Nav, etc.).
30
+ */
31
+ children: ReactNode;
32
+ /**
33
+ * Additional CSS class names.
34
+ */
35
+ className?: string;
36
+ };
37
+ export type SidebarHeaderProps = {
38
+ /**
39
+ * Header content (back button, title, progress, CTAs).
40
+ */
41
+ children: ReactNode;
42
+ /**
43
+ * Additional CSS class names.
44
+ */
45
+ className?: string;
46
+ };
47
+ export type SidebarNavProps = {
48
+ /**
49
+ * Accessible label for the nav landmark.
50
+ * Use this for the primary label since nav is the actionable region.
51
+ */
52
+ ariaLabel?: string;
53
+ /**
54
+ * ID of a visible heading that labels the navigation.
55
+ */
56
+ ariaLabelledBy?: string;
57
+ /**
58
+ * Nav content (typically Accordion with StepList).
59
+ */
60
+ children: ReactNode;
61
+ /**
62
+ * Additional CSS class names.
63
+ */
64
+ className?: string;
65
+ };
@@ -0,0 +1,4 @@
1
+ // =============================================================================
2
+ // Sidebar Types
3
+ // =============================================================================
4
+ export {};
@@ -0,0 +1,2 @@
1
+ import type { StepIndicatorProps } from './types';
2
+ export declare function StepIndicator(props: StepIndicatorProps): import("react/jsx-runtime").JSX.Element;
@@ -0,0 +1,64 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { Progress } from '../Progress/index.js';
3
+ import { Icon } from '../Icon/index.js';
4
+ // =============================================================================
5
+ // Icon mapping for static states
6
+ // =============================================================================
7
+ const statusIcons = {
8
+ complete: 'lms/checkmark',
9
+ locked: 'lms/lock',
10
+ };
11
+ const statusLabels = {
12
+ 'not-started': 'Not started',
13
+ 'in-progress': 'In progress',
14
+ complete: 'Complete',
15
+ locked: 'Locked',
16
+ };
17
+ // =============================================================================
18
+ // Status inference
19
+ // =============================================================================
20
+ function inferStatus(value) {
21
+ if (value <= 0)
22
+ return 'not-started';
23
+ if (value >= 100)
24
+ return 'complete';
25
+ return 'in-progress';
26
+ }
27
+ // =============================================================================
28
+ // Component
29
+ // =============================================================================
30
+ export function StepIndicator(props) {
31
+ const { value = 0, status: statusOverride, icon: customIcon, size = 'sm', showValue = false, label, className, } = props;
32
+ // Infer status from value, allow override
33
+ const status = statusOverride ?? inferStatus(value);
34
+ // When status is explicit, the visual should match the status, not the value
35
+ // - not-started: always show empty (0%)
36
+ // - complete: always show full (100%)
37
+ // - in-progress: use actual value
38
+ // - locked: shows icon, value doesn't matter visually
39
+ const displayValue = statusOverride === 'not-started' ? 0
40
+ : statusOverride === 'complete' ? 100
41
+ : value;
42
+ // Determine which icon to show:
43
+ // - complete/locked: always use status icons (universal meaning)
44
+ // - not-started/in-progress: use custom icon if provided, otherwise no icon
45
+ const hasStatusIcon = status === 'complete' || status === 'locked';
46
+ const hasCustomIcon = !!customIcon && (status === 'not-started' || status === 'in-progress');
47
+ const hasIcon = hasStatusIcon || hasCustomIcon;
48
+ const iconName = hasStatusIcon ? statusIcons[status] : customIcon;
49
+ // Use solid variant for complete state (filled circle)
50
+ const variant = status === 'complete' ? 'solid' : 'ring';
51
+ // Accessible label
52
+ const ariaLabel = label ?? `Step status: ${statusLabels[status]}`;
53
+ // Build class names
54
+ const rootClassName = [
55
+ 'tui-step-indicator',
56
+ `is-size-${size}`,
57
+ `is-status-${status}`,
58
+ hasIcon && 'has-icon',
59
+ className,
60
+ ]
61
+ .filter(Boolean)
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), "%"] }))] }) }));
64
+ }
@@ -0,0 +1,2 @@
1
+ export { StepIndicator } from './StepIndicator';
2
+ export type { StepIndicatorProps, StepStatus } from './types';
@@ -0,0 +1 @@
1
+ export { StepIndicator } from './StepIndicator.js';