@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
package/README.md ADDED
@@ -0,0 +1,100 @@
1
+ # Tangible UI
2
+
3
+ Design system for Tangible WordPress plugins. React components + CSS tokens + utility classes.
4
+
5
+ **Live Storybook:** https://storybook-tangible-ui.pages.dev
6
+
7
+ ## Components
8
+
9
+ - **Primitives:** Button, Chip, Icon, IconButton, Progress, Rating, Tooltip
10
+ - **Layout:** Accordion, Card, Modal, Notice, DataTable
11
+ - **Composites:** Chips (multi-select chip group), StepIndicator
12
+ - **Form Inputs:** Text, textarea, select, checkbox, radio, toggle, file (CSS-only)
13
+
14
+ ## Getting Started
15
+
16
+ ### Install
17
+
18
+ ```bash
19
+ npm install @tangible/ui
20
+ ```
21
+
22
+ ### Import styles
23
+
24
+ ```tsx
25
+ // In your app entry point
26
+ import '@tangible/ui/styles';
27
+ ```
28
+
29
+ Or in SCSS:
30
+ ```scss
31
+ @use '@tangible/ui/styles/scss';
32
+ ```
33
+
34
+ ### Wrap your app
35
+
36
+ Components require the `.tui-interface` wrapper to access design tokens:
37
+
38
+ ```tsx
39
+ function App() {
40
+ return (
41
+ <div className="tui-interface">
42
+ {/* Your UI here */}
43
+ </div>
44
+ );
45
+ }
46
+ ```
47
+
48
+ ### Use components
49
+
50
+ ```tsx
51
+ import { Button, Card, Tooltip, IconButton } from '@tangible/ui';
52
+
53
+ function Example() {
54
+ return (
55
+ <Card>
56
+ <Card.Body>
57
+ <Button label="Click me" theme="primary" />
58
+
59
+ <IconButton icon="system/settings" label="Settings" showTooltip />
60
+
61
+ <Tooltip>
62
+ <Tooltip.Trigger asChild>
63
+ <Button label="Hover me" variant="outline" />
64
+ </Tooltip.Trigger>
65
+ <Tooltip.Content>Hello!</Tooltip.Content>
66
+ </Tooltip>
67
+ </Card.Body>
68
+ </Card>
69
+ );
70
+ }
71
+ ```
72
+
73
+ See the [Storybook](https://storybook-tangible-ui.pages.dev) for full component documentation and examples.
74
+
75
+ ## Development
76
+
77
+ ```bash
78
+ npm install
79
+ npm run storybook # Dev server on port 6006
80
+ ```
81
+
82
+ ## Commands
83
+
84
+ ```bash
85
+ npm run storybook # Dev server
86
+ npm run build:lib # Build library (outputs to publish/)
87
+ npm run lint # ESLint
88
+ npm run test # Unit tests
89
+ npm run test:storybook # Story tests (requires Playwright)
90
+ ```
91
+
92
+ ## Documentation
93
+
94
+ - `CLAUDE.md` — Development guide (architecture, patterns, conventions)
95
+ - `CONTEXT.md` — Project background and LMS requirements
96
+ - `TIMELINE.md` — Development roadmap (Jan–Mar 2026)
97
+
98
+ ## Status
99
+
100
+ Under active development for Course Viewer (LMS) and Quiz modules. Component APIs are stabilising but may change.
@@ -0,0 +1,22 @@
1
+ import React from 'react';
2
+ import type { AccordionProps, AccordionItemProps, AccordionTriggerProps, AccordionPanelProps } from './types';
3
+ type AccordionCompound = {
4
+ (props: AccordionProps): React.JSX.Element;
5
+ Item: typeof AccordionItem;
6
+ Trigger: typeof AccordionTrigger;
7
+ Panel: typeof AccordionPanel;
8
+ };
9
+ declare function AccordionItem({ value, disabled, headingLevel, children, className, }: AccordionItemProps): import("react/jsx-runtime").JSX.Element;
10
+ declare namespace AccordionItem {
11
+ var displayName: string;
12
+ }
13
+ declare function AccordionTrigger({ children, className }: AccordionTriggerProps): import("react/jsx-runtime").JSX.Element;
14
+ declare namespace AccordionTrigger {
15
+ var displayName: string;
16
+ }
17
+ declare function AccordionPanel({ children, className }: AccordionPanelProps): import("react/jsx-runtime").JSX.Element;
18
+ declare namespace AccordionPanel {
19
+ var displayName: string;
20
+ }
21
+ export declare const Accordion: AccordionCompound;
22
+ export {};
@@ -0,0 +1,192 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import React, { useCallback, useId, useMemo, useRef, useState } from 'react';
3
+ import { cx } from '../../utils/cx.js';
4
+ import { Icon } from '../Icon/index.js';
5
+ import { AccordionContext, AccordionItemContext, useAccordionContext, useAccordionItemContext } from './AccordionContext.js';
6
+ function AccordionRoot(props) {
7
+ const { type, children, className } = props;
8
+ // Track trigger refs for keyboard navigation
9
+ const triggersRef = useRef(new Map());
10
+ const registerTrigger = useCallback((value, element) => {
11
+ if (element) {
12
+ triggersRef.current.set(value, element);
13
+ }
14
+ else {
15
+ triggersRef.current.delete(value);
16
+ }
17
+ }, []);
18
+ const getTriggers = useCallback(() => triggersRef.current, []);
19
+ // State management differs by type
20
+ if (type === 'single') {
21
+ return (_jsx(AccordionSingle, { ...props, registerTrigger: registerTrigger, getTriggers: getTriggers, className: className, children: children }));
22
+ }
23
+ return (_jsx(AccordionMultiple, { ...props, registerTrigger: registerTrigger, getTriggers: getTriggers, className: className, children: children }));
24
+ }
25
+ function AccordionSingle({ value: controlledValue, defaultValue, onValueChange, collapsible = false, children, className, registerTrigger, getTriggers, }) {
26
+ const [internalValue, setInternalValue] = useState(defaultValue);
27
+ const isControlled = controlledValue !== undefined;
28
+ const currentValue = isControlled ? controlledValue : internalValue;
29
+ const isOpen = useCallback((itemValue) => currentValue === itemValue, [currentValue]);
30
+ const toggle = useCallback((itemValue) => {
31
+ let newValue;
32
+ if (currentValue === itemValue) {
33
+ // Closing current item
34
+ newValue = collapsible ? undefined : itemValue;
35
+ }
36
+ else {
37
+ // Opening new item
38
+ newValue = itemValue;
39
+ }
40
+ if (!isControlled) {
41
+ setInternalValue(newValue);
42
+ }
43
+ onValueChange?.(newValue);
44
+ }, [currentValue, collapsible, isControlled, onValueChange]);
45
+ const contextValue = useMemo(() => ({
46
+ type: 'single',
47
+ collapsible,
48
+ isOpen,
49
+ toggle,
50
+ registerTrigger,
51
+ getTriggers,
52
+ }), [collapsible, isOpen, toggle, registerTrigger, getTriggers]);
53
+ return (_jsx(AccordionContext.Provider, { value: contextValue, children: _jsx("div", { className: cx('tui-accordion', className), "data-type": "single", children: children }) }));
54
+ }
55
+ function AccordionMultiple({ value: controlledValue, defaultValue, onValueChange, children, className, registerTrigger, getTriggers, }) {
56
+ const [internalValue, setInternalValue] = useState(defaultValue ?? []);
57
+ const isControlled = controlledValue !== undefined;
58
+ const currentValue = isControlled ? controlledValue : internalValue;
59
+ const isOpen = useCallback((itemValue) => currentValue.includes(itemValue), [currentValue]);
60
+ const toggle = useCallback((itemValue) => {
61
+ const newValue = currentValue.includes(itemValue)
62
+ ? currentValue.filter((v) => v !== itemValue)
63
+ : [...currentValue, itemValue];
64
+ if (!isControlled) {
65
+ setInternalValue(newValue);
66
+ }
67
+ onValueChange?.(newValue);
68
+ }, [currentValue, isControlled, onValueChange]);
69
+ const contextValue = useMemo(() => ({
70
+ type: 'multiple',
71
+ collapsible: true, // Multiple is always collapsible
72
+ isOpen,
73
+ toggle,
74
+ registerTrigger,
75
+ getTriggers,
76
+ }), [isOpen, toggle, registerTrigger, getTriggers]);
77
+ return (_jsx(AccordionContext.Provider, { value: contextValue, children: _jsx("div", { className: cx('tui-accordion', className), "data-type": "multiple", children: children }) }));
78
+ }
79
+ // =============================================================================
80
+ // Accordion Item
81
+ // =============================================================================
82
+ function AccordionItem({ value, disabled = false, headingLevel, children, className, }) {
83
+ const baseId = useId();
84
+ const triggerId = `${baseId}-trigger`;
85
+ const panelId = `${baseId}-panel`;
86
+ const { isOpen } = useAccordionContext();
87
+ const open = isOpen(value);
88
+ const itemContext = useMemo(() => ({
89
+ value,
90
+ disabled,
91
+ headingLevel,
92
+ triggerId,
93
+ panelId,
94
+ isOpen: open,
95
+ }), [value, disabled, headingLevel, triggerId, panelId, open]);
96
+ const state = open ? 'open' : 'closed';
97
+ return (_jsx(AccordionItemContext.Provider, { value: itemContext, children: _jsx("div", { className: cx('tui-accordion__item', className), "data-state": state, "data-disabled": disabled || undefined, children: children }) }));
98
+ }
99
+ // =============================================================================
100
+ // Accordion Trigger
101
+ // =============================================================================
102
+ function AccordionTrigger({ children, className }) {
103
+ const { toggle, registerTrigger, getTriggers } = useAccordionContext();
104
+ const { value, disabled, headingLevel, triggerId, panelId, isOpen } = useAccordionItemContext();
105
+ const buttonRef = useRef(null);
106
+ // Register trigger for keyboard navigation
107
+ React.useEffect(() => {
108
+ registerTrigger(value, buttonRef.current);
109
+ return () => registerTrigger(value, null);
110
+ }, [value, registerTrigger]);
111
+ const handleClick = () => {
112
+ if (!disabled) {
113
+ toggle(value);
114
+ }
115
+ };
116
+ const handleKeyDown = (event) => {
117
+ const triggers = getTriggers();
118
+ // Sort triggers by DOM order (not Map insertion order) to handle
119
+ // conditional rendering, async mounting, or reordered items
120
+ const sortedTriggers = Array.from(triggers.entries()).sort(([, a], [, b]) => {
121
+ if (a.compareDocumentPosition(b) & Node.DOCUMENT_POSITION_FOLLOWING) {
122
+ return -1;
123
+ }
124
+ return 1;
125
+ });
126
+ const currentIndex = sortedTriggers.findIndex(([v]) => v === value);
127
+ // Find next/prev non-disabled trigger
128
+ const findNextIndex = (start, direction) => {
129
+ let index = start;
130
+ const len = sortedTriggers.length;
131
+ for (let i = 0; i < len; i++) {
132
+ index = (index + direction + len) % len;
133
+ const [, triggerElement] = sortedTriggers[index];
134
+ if (triggerElement && !triggerElement.disabled) {
135
+ return index;
136
+ }
137
+ }
138
+ return start; // No non-disabled trigger found
139
+ };
140
+ let targetIndex = null;
141
+ switch (event.key) {
142
+ case 'ArrowDown':
143
+ event.preventDefault();
144
+ targetIndex = findNextIndex(currentIndex, 1);
145
+ break;
146
+ case 'ArrowUp':
147
+ event.preventDefault();
148
+ targetIndex = findNextIndex(currentIndex, -1);
149
+ break;
150
+ case 'Home':
151
+ event.preventDefault();
152
+ targetIndex = findNextIndex(sortedTriggers.length - 1, 1); // Start from end, go forward
153
+ break;
154
+ case 'End':
155
+ event.preventDefault();
156
+ targetIndex = findNextIndex(0, -1); // Start from beginning, go backward
157
+ break;
158
+ }
159
+ if (targetIndex !== null && targetIndex !== currentIndex) {
160
+ const [, targetElement] = sortedTriggers[targetIndex];
161
+ targetElement?.focus();
162
+ }
163
+ };
164
+ const state = isOpen ? 'open' : 'closed';
165
+ const button = (_jsxs("button", { ref: buttonRef, type: "button", id: triggerId, className: cx('tui-accordion__trigger', className), "aria-expanded": isOpen, "aria-controls": panelId, disabled: disabled, "data-state": state, "data-disabled": disabled || undefined, onClick: handleClick, onKeyDown: handleKeyDown, children: [_jsx("span", { className: "tui-accordion__trigger-content", children: children }), _jsx(Icon, { name: "system/chevron-down", size: "sm", className: "tui-accordion__indicator", "aria-hidden": "true" })] }));
166
+ // Wrap in heading if headingLevel is specified
167
+ if (headingLevel) {
168
+ const Heading = `h${headingLevel}`;
169
+ return _jsx(Heading, { className: "tui-accordion__heading", children: button });
170
+ }
171
+ return button;
172
+ }
173
+ // =============================================================================
174
+ // Accordion Panel
175
+ // =============================================================================
176
+ function AccordionPanel({ children, className }) {
177
+ const { triggerId, panelId, isOpen } = useAccordionItemContext();
178
+ const state = isOpen ? 'open' : 'closed';
179
+ return (_jsx("div", { id: panelId, role: "region", "aria-labelledby": triggerId, className: cx('tui-accordion__panel', className), "data-state": state, "aria-hidden": !isOpen,
180
+ // Prevent keyboard focus into collapsed panels
181
+ inert: !isOpen || undefined, children: _jsx("div", { className: "tui-accordion__panel-content", children: children }) }));
182
+ }
183
+ // =============================================================================
184
+ // Export Compound Component
185
+ // =============================================================================
186
+ AccordionItem.displayName = 'Accordion.Item';
187
+ AccordionTrigger.displayName = 'Accordion.Trigger';
188
+ AccordionPanel.displayName = 'Accordion.Panel';
189
+ export const Accordion = AccordionRoot;
190
+ Accordion.Item = AccordionItem;
191
+ Accordion.Trigger = AccordionTrigger;
192
+ Accordion.Panel = AccordionPanel;
@@ -0,0 +1,5 @@
1
+ import type { AccordionContextValue, AccordionItemContextValue } from './types';
2
+ export declare const AccordionContext: import("react").Context<AccordionContextValue | null>;
3
+ export declare function useAccordionContext(): AccordionContextValue;
4
+ export declare const AccordionItemContext: import("react").Context<AccordionItemContextValue | null>;
5
+ export declare function useAccordionItemContext(): AccordionItemContextValue;
@@ -0,0 +1,23 @@
1
+ import { createContext, useContext } from 'react';
2
+ // =============================================================================
3
+ // Accordion Root Context
4
+ // =============================================================================
5
+ export const AccordionContext = createContext(null);
6
+ export function useAccordionContext() {
7
+ const context = useContext(AccordionContext);
8
+ if (!context) {
9
+ throw new Error('Accordion compound components must be used within an Accordion');
10
+ }
11
+ return context;
12
+ }
13
+ // =============================================================================
14
+ // Accordion Item Context
15
+ // =============================================================================
16
+ export const AccordionItemContext = createContext(null);
17
+ export function useAccordionItemContext() {
18
+ const context = useContext(AccordionItemContext);
19
+ if (!context) {
20
+ throw new Error('Accordion.Trigger and Accordion.Panel must be used within an Accordion.Item');
21
+ }
22
+ return context;
23
+ }
@@ -0,0 +1,2 @@
1
+ export { Accordion } from './Accordion';
2
+ export type { AccordionProps, AccordionSingleProps, AccordionMultipleProps, AccordionItemProps, AccordionTriggerProps, AccordionPanelProps, } from './types';
@@ -0,0 +1 @@
1
+ export { Accordion } from './Accordion.js';
@@ -0,0 +1,61 @@
1
+ import type { ReactNode } from 'react';
2
+ type AccordionBaseProps = {
3
+ children: ReactNode;
4
+ className?: string;
5
+ };
6
+ export type AccordionSingleProps = AccordionBaseProps & {
7
+ type: 'single';
8
+ /** Controlled: currently open item value */
9
+ value?: string;
10
+ /** Uncontrolled: initially open item */
11
+ defaultValue?: string;
12
+ /** Callback when open item changes (undefined when all closed in collapsible mode) */
13
+ onValueChange?: (value: string | undefined) => void;
14
+ /** Allow closing all items (otherwise one stays open) */
15
+ collapsible?: boolean;
16
+ };
17
+ export type AccordionMultipleProps = AccordionBaseProps & {
18
+ type: 'multiple';
19
+ /** Controlled: currently open item values */
20
+ value?: string[];
21
+ /** Uncontrolled: initially open items */
22
+ defaultValue?: string[];
23
+ /** Callback when open items change */
24
+ onValueChange?: (value: string[]) => void;
25
+ };
26
+ export type AccordionProps = AccordionSingleProps | AccordionMultipleProps;
27
+ export type AccordionItemProps = {
28
+ /** Unique identifier for this item (required) */
29
+ value: string;
30
+ /** Prevent interaction */
31
+ disabled?: boolean;
32
+ /** Wrap trigger in heading element */
33
+ headingLevel?: 2 | 3 | 4 | 5 | 6;
34
+ children: ReactNode;
35
+ className?: string;
36
+ };
37
+ export type AccordionTriggerProps = {
38
+ children: ReactNode;
39
+ className?: string;
40
+ };
41
+ export type AccordionPanelProps = {
42
+ children: ReactNode;
43
+ className?: string;
44
+ };
45
+ export type AccordionContextValue = {
46
+ type: 'single' | 'multiple';
47
+ collapsible: boolean;
48
+ isOpen: (value: string) => boolean;
49
+ toggle: (value: string) => void;
50
+ registerTrigger: (value: string, element: HTMLButtonElement | null) => void;
51
+ getTriggers: () => Map<string, HTMLButtonElement>;
52
+ };
53
+ export type AccordionItemContextValue = {
54
+ value: string;
55
+ disabled: boolean;
56
+ headingLevel?: 2 | 3 | 4 | 5 | 6;
57
+ triggerId: string;
58
+ panelId: string;
59
+ isOpen: boolean;
60
+ };
61
+ export {};
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,11 @@
1
+ import React from 'react';
2
+ import type { AvatarProps } from './types';
3
+ /**
4
+ * Avatar displays a user's profile image, initials, or a placeholder icon.
5
+ *
6
+ * - Loads image from `src` prop
7
+ * - Falls back to initials from `name` if image fails or isn't provided
8
+ * - Shows placeholder icon if neither `src` nor `name` provided
9
+ * - Colors for initials are derived from the name hash for consistency
10
+ */
11
+ export declare const Avatar: React.ForwardRefExoticComponent<AvatarProps & React.RefAttributes<HTMLSpanElement>>;
@@ -0,0 +1,67 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import React, { useState, useMemo } from 'react';
3
+ import { cx } from '../../utils/cx.js';
4
+ import { Icon } from '../Icon/index.js';
5
+ import { AVATAR_COLORS } from './types.js';
6
+ /**
7
+ * Generate initials from a name.
8
+ * "Mary Ghen" → "MG", "Bob" → "B", "Jean-Luc Picard" → "JP"
9
+ */
10
+ function getInitials(name) {
11
+ const parts = name.trim().split(/[\s-]+/).filter(Boolean);
12
+ if (parts.length === 0)
13
+ return '';
14
+ if (parts.length === 1)
15
+ return parts[0].charAt(0).toUpperCase();
16
+ return (parts[0].charAt(0) + parts[parts.length - 1].charAt(0)).toUpperCase();
17
+ }
18
+ /**
19
+ * Simple hash function for consistent color assignment.
20
+ * Same name always gets the same color.
21
+ */
22
+ function hashString(str) {
23
+ if (!str)
24
+ return 0;
25
+ let hash = 0;
26
+ for (let i = 0; i < str.length; i++) {
27
+ const char = str.charCodeAt(i);
28
+ hash = ((hash << 5) - hash) + char;
29
+ hash = hash & hash; // Convert to 32-bit integer
30
+ }
31
+ return Math.abs(hash);
32
+ }
33
+ /**
34
+ * Get a consistent color for a name.
35
+ */
36
+ function getColorFromName(name, colors) {
37
+ const hash = hashString(name.toLowerCase());
38
+ return colors[hash % colors.length];
39
+ }
40
+ /**
41
+ * Avatar displays a user's profile image, initials, or a placeholder icon.
42
+ *
43
+ * - Loads image from `src` prop
44
+ * - Falls back to initials from `name` if image fails or isn't provided
45
+ * - Shows placeholder icon if neither `src` nor `name` provided
46
+ * - Colors for initials are derived from the name hash for consistency
47
+ */
48
+ export const Avatar = React.forwardRef(({ src, name, size = 'md', shape = 'circle', color, indicator, indicatorLabel, indicatorPosition = 'bottom-right', className, }, ref) => {
49
+ const [imgError, setImgError] = useState(false);
50
+ // Reset error state when src changes
51
+ React.useEffect(() => {
52
+ setImgError(false);
53
+ }, [src]);
54
+ const initials = useMemo(() => (name ? getInitials(name) : ''), [name]);
55
+ const derivedColor = useMemo(() => color ?? (name ? getColorFromName(name, AVATAR_COLORS) : 'slate'), [color, name]);
56
+ const showImage = src && !imgError;
57
+ const showInitials = !showImage && initials;
58
+ const showPlaceholder = !showImage && !initials;
59
+ // Placeholder avatars are decorative (no meaningful identity)
60
+ const isDecorative = showPlaceholder;
61
+ return (_jsxs("span", { ref: ref, className: cx('tui-avatar', `is-size-${size}`, `is-shape-${shape}`, !showImage && `is-color-${derivedColor}`, className), ...(isDecorative
62
+ ? { 'aria-hidden': true }
63
+ : { role: 'img', 'aria-label': name }), children: [_jsxs("span", { className: "tui-avatar__content", children: [showImage && (_jsx("img", { src: src, alt: "", className: "tui-avatar__image", onError: () => setImgError(true) })), showInitials && (_jsx("span", { className: "tui-avatar__initials", "aria-hidden": "true", children: initials })), showPlaceholder && (_jsx(Icon, { name: "system/user-circle-outline", className: "tui-avatar__placeholder" }))] }), indicator && (_jsx("span", { className: cx('tui-avatar__indicator', `is-position-${indicatorPosition}`), ...(indicatorLabel
64
+ ? { role: 'img', 'aria-label': indicatorLabel }
65
+ : { 'aria-hidden': true }), children: indicator }))] }));
66
+ });
67
+ Avatar.displayName = 'Avatar';
@@ -0,0 +1,11 @@
1
+ import React from 'react';
2
+ import type { AvatarGroupProps } from './types';
3
+ /**
4
+ * AvatarGroup displays multiple avatars with optional overlap.
5
+ *
6
+ * - Shows up to `max` avatars, with "+N" overflow indicator
7
+ * - Overlap mode (default) uses OverlapStack with avatar-styled overflow
8
+ * - Non-overlap mode uses flex with gap
9
+ * - Override overlap amount via `--tui-avatar-group-overlap` CSS property
10
+ */
11
+ export declare const AvatarGroup: React.ForwardRefExoticComponent<AvatarGroupProps & React.RefAttributes<HTMLDivElement>>;
@@ -0,0 +1,45 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import React, { Children, isValidElement, cloneElement } from 'react';
3
+ import { cx } from '../../utils/cx.js';
4
+ import { OverlapStack } from '../OverlapStack/index.js';
5
+ /**
6
+ * AvatarGroup displays multiple avatars with optional overlap.
7
+ *
8
+ * - Shows up to `max` avatars, with "+N" overflow indicator
9
+ * - Overlap mode (default) uses OverlapStack with avatar-styled overflow
10
+ * - Non-overlap mode uses flex with gap
11
+ * - Override overlap amount via `--tui-avatar-group-overlap` CSS property
12
+ */
13
+ export const AvatarGroup = React.forwardRef(({ max, size, shape, overlap = true, children, className }, ref) => {
14
+ const childArray = Children.toArray(children).filter(isValidElement);
15
+ const total = childArray.length;
16
+ // Clone children to inject size/shape props if provided at group level
17
+ const clonedChildren = childArray.map((child) => {
18
+ if (isValidElement(child)) {
19
+ return cloneElement(child, {
20
+ size: size ?? child.props.size,
21
+ shape: shape ?? child.props.shape,
22
+ });
23
+ }
24
+ return child;
25
+ });
26
+ // Descriptive label for the group
27
+ const hasOverflow = max !== undefined && total > max;
28
+ const visibleCount = hasOverflow ? max : total;
29
+ const groupLabel = hasOverflow
30
+ ? `${total} users, showing ${visibleCount}`
31
+ : `${total} users`;
32
+ // Non-overlap mode: simple flex layout
33
+ if (!overlap) {
34
+ return (_jsx("div", { ref: ref, className: cx('tui-avatar-group', className), role: "group", "aria-label": groupLabel, children: clonedChildren }));
35
+ }
36
+ // Overlap mode: delegate to OverlapStack
37
+ // Note: We don't use OverlapStack's `frame` prop because its border-radius
38
+ // wouldn't match each avatar's shape. Instead, we apply ring styles via CSS
39
+ // directly to avatars inside .is-overlap groups.
40
+ return (_jsx(OverlapStack, { ref: ref, className: cx('tui-avatar-group', 'is-overlap', className), max: max, renderOverflow: (count) => (_jsx(AvatarOverflow, { count: count, size: size, shape: shape })), overflowLabel: (count) => `${count} more users`, "aria-label": groupLabel, children: clonedChildren }));
41
+ });
42
+ AvatarGroup.displayName = 'AvatarGroup';
43
+ function AvatarOverflow({ count, size, shape }) {
44
+ return (_jsx("span", { className: cx('tui-avatar', 'tui-avatar--overflow', size && `is-size-${size}`, shape && `is-shape-${shape}`), role: "img", "aria-hidden": "true", children: _jsx("span", { className: "tui-avatar__content", children: _jsxs("span", { className: "tui-avatar__initials", children: ["+", count] }) }) }));
45
+ }
@@ -0,0 +1,9 @@
1
+ import { Avatar as AvatarBase } from './Avatar';
2
+ import { AvatarGroup } from './AvatarGroup';
3
+ export type { AvatarProps, AvatarGroupProps, AvatarSize, AvatarShape, AvatarColor, IndicatorPosition, } from './types';
4
+ export { AVATAR_COLORS } from './types';
5
+ type AvatarCompound = typeof AvatarBase & {
6
+ Group: typeof AvatarGroup;
7
+ };
8
+ export declare const Avatar: AvatarCompound;
9
+ export { AvatarGroup };
@@ -0,0 +1,7 @@
1
+ import { Avatar as AvatarBase } from './Avatar.js';
2
+ import { AvatarGroup } from './AvatarGroup.js';
3
+ export { AVATAR_COLORS } from './types.js';
4
+ export const Avatar = AvatarBase;
5
+ Avatar.Group = AvatarGroup;
6
+ // Named export for direct import
7
+ export { AvatarGroup };
@@ -0,0 +1,44 @@
1
+ import type { SizeExtended } from '../../types';
2
+ export type AvatarSize = SizeExtended;
3
+ export type AvatarShape = 'circle' | 'square';
4
+ export type IndicatorPosition = 'top-left' | 'top-right' | 'bottom-left' | 'bottom-right';
5
+ /**
6
+ * Avatar palette colors for initials backgrounds.
7
+ * These are not tied to semantic themes - they provide visual variety.
8
+ */
9
+ export type AvatarColor = 'coral' | 'amber' | 'lime' | 'teal' | 'cyan' | 'blue' | 'violet' | 'pink' | 'slate' | 'emerald';
10
+ export declare const AVATAR_COLORS: AvatarColor[];
11
+ export type AvatarProps = {
12
+ /** Image source URL */
13
+ src?: string;
14
+ /** User's name - used for alt text and generating initials fallback */
15
+ name?: string;
16
+ /** Size of the avatar */
17
+ size?: AvatarSize;
18
+ /** Shape of the avatar */
19
+ shape?: AvatarShape;
20
+ /** Explicit color for initials background (otherwise derived from name) */
21
+ color?: AvatarColor;
22
+ /** Status indicator element (icon, badge, etc.) */
23
+ indicator?: React.ReactNode;
24
+ /** Accessible label for the indicator (e.g., "Online", "Verified", "3 notifications") */
25
+ indicatorLabel?: string;
26
+ /** Position of the indicator */
27
+ indicatorPosition?: IndicatorPosition;
28
+ /** Additional CSS class */
29
+ className?: string;
30
+ };
31
+ export type AvatarGroupProps = {
32
+ /** Maximum avatars to show before "+N" overflow */
33
+ max?: number;
34
+ /** Size for all avatars in the group */
35
+ size?: AvatarSize;
36
+ /** Shape for all avatars in the group */
37
+ shape?: AvatarShape;
38
+ /** Whether avatars overlap (default: true) */
39
+ overlap?: boolean;
40
+ /** Children (Avatar components) */
41
+ children: React.ReactNode;
42
+ /** Additional CSS class */
43
+ className?: string;
44
+ };
@@ -0,0 +1,12 @@
1
+ export const AVATAR_COLORS = [
2
+ 'coral',
3
+ 'amber',
4
+ 'lime',
5
+ 'teal',
6
+ 'cyan',
7
+ 'blue',
8
+ 'violet',
9
+ 'pink',
10
+ 'slate',
11
+ 'emerald',
12
+ ];
@@ -0,0 +1,4 @@
1
+ import React from 'react';
2
+ import type { ButtonProps } from './types';
3
+ export type { ButtonProps } from './types';
4
+ export declare const Button: React.ForwardRefExoticComponent<ButtonProps & React.RefAttributes<HTMLButtonElement | HTMLAnchorElement>>;