@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,2 @@
1
+ export type { Size, SizeExtended, SizeCompact, SizeStandard } from './sizes';
2
+ export type { Theme, ThemeIntent, ThemeStatus } from './themes';
package/types/index.js ADDED
@@ -0,0 +1 @@
1
+ export {};
package/types/index.ts ADDED
@@ -0,0 +1,2 @@
1
+ export type { Size, SizeExtended, SizeCompact, SizeStandard } from './sizes';
2
+ export type { Theme, ThemeIntent, ThemeStatus } from './themes';
@@ -0,0 +1,17 @@
1
+ /**
2
+ * Shared size type definitions for components.
3
+ *
4
+ * Components use different subsets of the size scale based on their use case:
5
+ * - Size: Standard range (xs→lg) for buttons, progress indicators
6
+ * - SizeExtended: Full range (xs→xxl) for icons and typography
7
+ * - SizeCompact: Smaller range (xs→md) for inline elements like chips
8
+ * - SizeStandard: Medium range (sm→lg) for containers like modals
9
+ */
10
+ /** Standard size scale: xs, sm, md, lg */
11
+ export type Size = 'xs' | 'sm' | 'md' | 'lg';
12
+ /** Extended size scale for icons/typography: xs, sm, md, lg, xl, xxl */
13
+ export type SizeExtended = 'xs' | 'sm' | 'md' | 'lg' | 'xl' | 'xxl';
14
+ /** Compact size scale for inline elements: xs, sm, md */
15
+ export type SizeCompact = 'xs' | 'sm' | 'md';
16
+ /** Standard size scale for containers: sm, md, lg */
17
+ export type SizeStandard = 'sm' | 'md' | 'lg';
package/types/sizes.js ADDED
@@ -0,0 +1,10 @@
1
+ /**
2
+ * Shared size type definitions for components.
3
+ *
4
+ * Components use different subsets of the size scale based on their use case:
5
+ * - Size: Standard range (xs→lg) for buttons, progress indicators
6
+ * - SizeExtended: Full range (xs→xxl) for icons and typography
7
+ * - SizeCompact: Smaller range (xs→md) for inline elements like chips
8
+ * - SizeStandard: Medium range (sm→lg) for containers like modals
9
+ */
10
+ export {};
package/types/sizes.ts ADDED
@@ -0,0 +1,21 @@
1
+ /**
2
+ * Shared size type definitions for components.
3
+ *
4
+ * Components use different subsets of the size scale based on their use case:
5
+ * - Size: Standard range (xs→lg) for buttons, progress indicators
6
+ * - SizeExtended: Full range (xs→xxl) for icons and typography
7
+ * - SizeCompact: Smaller range (xs→md) for inline elements like chips
8
+ * - SizeStandard: Medium range (sm→lg) for containers like modals
9
+ */
10
+
11
+ /** Standard size scale: xs, sm, md, lg */
12
+ export type Size = 'xs' | 'sm' | 'md' | 'lg';
13
+
14
+ /** Extended size scale for icons/typography: xs, sm, md, lg, xl, xxl */
15
+ export type SizeExtended = 'xs' | 'sm' | 'md' | 'lg' | 'xl' | 'xxl';
16
+
17
+ /** Compact size scale for inline elements: xs, sm, md */
18
+ export type SizeCompact = 'xs' | 'sm' | 'md';
19
+
20
+ /** Standard size scale for containers: sm, md, lg */
21
+ export type SizeStandard = 'sm' | 'md' | 'lg';
package/types/svg.d.ts ADDED
@@ -0,0 +1,5 @@
1
+ declare module '*.svg?react' {
2
+ import * as React from 'react';
3
+ const ReactComponent: React.FC<React.SVGProps<SVGSVGElement>>;
4
+ export default ReactComponent;
5
+ }
@@ -0,0 +1,14 @@
1
+ /**
2
+ * Shared theme type definitions for components.
3
+ *
4
+ * Components use different subsets of the theme palette based on their purpose:
5
+ * - ThemeIntent: Core action themes (primary, secondary, danger) for buttons
6
+ * - Theme: Full palette including status colors for general use
7
+ * - ThemeStatus: Semantic status themes for notices and alerts
8
+ */
9
+ /** Core action themes for buttons and interactive elements */
10
+ export type ThemeIntent = 'primary' | 'secondary' | 'danger';
11
+ /** Full theme palette including status colors */
12
+ export type Theme = 'primary' | 'secondary' | 'success' | 'warning' | 'danger';
13
+ /** Semantic status themes for notices, alerts, feedback */
14
+ export type ThemeStatus = 'info' | 'success' | 'warning' | 'danger';
@@ -0,0 +1,9 @@
1
+ /**
2
+ * Shared theme type definitions for components.
3
+ *
4
+ * Components use different subsets of the theme palette based on their purpose:
5
+ * - ThemeIntent: Core action themes (primary, secondary, danger) for buttons
6
+ * - Theme: Full palette including status colors for general use
7
+ * - ThemeStatus: Semantic status themes for notices and alerts
8
+ */
9
+ export {};
@@ -0,0 +1,17 @@
1
+ /**
2
+ * Shared theme type definitions for components.
3
+ *
4
+ * Components use different subsets of the theme palette based on their purpose:
5
+ * - ThemeIntent: Core action themes (primary, secondary, danger) for buttons
6
+ * - Theme: Full palette including status colors for general use
7
+ * - ThemeStatus: Semantic status themes for notices and alerts
8
+ */
9
+
10
+ /** Core action themes for buttons and interactive elements */
11
+ export type ThemeIntent = 'primary' | 'secondary' | 'danger';
12
+
13
+ /** Full theme palette including status colors */
14
+ export type Theme = 'primary' | 'secondary' | 'success' | 'warning' | 'danger';
15
+
16
+ /** Semantic status themes for notices, alerts, feedback */
17
+ export type ThemeStatus = 'info' | 'success' | 'warning' | 'danger';
@@ -0,0 +1,33 @@
1
+ export type ContrastTone = 'light' | 'dark';
2
+ /**
3
+ * Determine whether light or dark foreground provides better contrast.
4
+ * Uses WCAG relative luminance with 0.179 threshold.
5
+ *
6
+ * @param hex - Background color in #rgb or #rrggbb format
7
+ * @returns 'light' for dark backgrounds, 'dark' for light backgrounds
8
+ *
9
+ * Invalid input returns 'dark' (assumes light background).
10
+ * Relies on tests to catch invalid usage - no runtime throws.
11
+ *
12
+ * Approved use cases:
13
+ * - Avatar initials / fallback glyphs
14
+ * - User-selected background chips (reactions, labels)
15
+ * - One-off inline visuals where no semantic token exists
16
+ *
17
+ * NOT for core component theming - use semantic tokens instead.
18
+ */
19
+ export declare function getContrastTone(hex: string): ContrastTone;
20
+ /**
21
+ * Get a concrete foreground color value for a given background.
22
+ *
23
+ * @param hex - Background color in #rgb or #rrggbb format
24
+ * @param options.light - Color for dark backgrounds (default '#fff')
25
+ * @param options.dark - Color for light backgrounds (default '#000')
26
+ * @returns The appropriate foreground color
27
+ *
28
+ * Invalid input returns options.dark (assumes light background).
29
+ */
30
+ export declare function getContrastForeground(hex: string, options?: {
31
+ light?: string;
32
+ dark?: string;
33
+ }): string;
@@ -0,0 +1,88 @@
1
+ // Supports #rgb, #rrggbb, #rgba, #rrggbbaa (alpha is ignored for contrast calc)
2
+ const HEX_PATTERN = /^#([0-9a-f]{3}|[0-9a-f]{4}|[0-9a-f]{6}|[0-9a-f]{8})$/i;
3
+ const LUMINANCE_THRESHOLD = 0.179;
4
+ /**
5
+ * Parse hex color to RGB components (0-255).
6
+ * Supports #rgb, #rrggbb, #rgba, #rrggbbaa formats.
7
+ * Alpha channel is ignored (contrast is calculated on opaque color).
8
+ * Trims whitespace (common in user input).
9
+ * Returns null for invalid input.
10
+ */
11
+ function parseHex(hex) {
12
+ const match = hex.trim().match(HEX_PATTERN);
13
+ if (!match)
14
+ return null;
15
+ const value = match[1];
16
+ if (value.length === 3 || value.length === 4) {
17
+ // #rgb or #rgba -> extract RGB only
18
+ return [
19
+ parseInt(value[0] + value[0], 16),
20
+ parseInt(value[1] + value[1], 16),
21
+ parseInt(value[2] + value[2], 16),
22
+ ];
23
+ }
24
+ // #rrggbb or #rrggbbaa -> extract RGB only
25
+ return [
26
+ parseInt(value.slice(0, 2), 16),
27
+ parseInt(value.slice(2, 4), 16),
28
+ parseInt(value.slice(4, 6), 16),
29
+ ];
30
+ }
31
+ /**
32
+ * Apply sRGB gamma correction to a channel value.
33
+ * Uses 0.04045 threshold (correct sRGB spec, not the common 0.03928 bug).
34
+ */
35
+ function gammaCorrect(channel) {
36
+ const srgb = channel / 255;
37
+ return srgb <= 0.04045
38
+ ? srgb / 12.92
39
+ : Math.pow((srgb + 0.055) / 1.055, 2.4);
40
+ }
41
+ /**
42
+ * Calculate WCAG relative luminance.
43
+ */
44
+ function getRelativeLuminance(r, g, b) {
45
+ return (0.2126 * gammaCorrect(r) +
46
+ 0.7152 * gammaCorrect(g) +
47
+ 0.0722 * gammaCorrect(b));
48
+ }
49
+ /**
50
+ * Determine whether light or dark foreground provides better contrast.
51
+ * Uses WCAG relative luminance with 0.179 threshold.
52
+ *
53
+ * @param hex - Background color in #rgb or #rrggbb format
54
+ * @returns 'light' for dark backgrounds, 'dark' for light backgrounds
55
+ *
56
+ * Invalid input returns 'dark' (assumes light background).
57
+ * Relies on tests to catch invalid usage - no runtime throws.
58
+ *
59
+ * Approved use cases:
60
+ * - Avatar initials / fallback glyphs
61
+ * - User-selected background chips (reactions, labels)
62
+ * - One-off inline visuals where no semantic token exists
63
+ *
64
+ * NOT for core component theming - use semantic tokens instead.
65
+ */
66
+ export function getContrastTone(hex) {
67
+ const rgb = parseHex(hex);
68
+ // Invalid input: return 'dark' fallback (assumes light background)
69
+ if (!rgb)
70
+ return 'dark';
71
+ const luminance = getRelativeLuminance(...rgb);
72
+ return luminance <= LUMINANCE_THRESHOLD ? 'light' : 'dark';
73
+ }
74
+ /**
75
+ * Get a concrete foreground color value for a given background.
76
+ *
77
+ * @param hex - Background color in #rgb or #rrggbb format
78
+ * @param options.light - Color for dark backgrounds (default '#fff')
79
+ * @param options.dark - Color for light backgrounds (default '#000')
80
+ * @returns The appropriate foreground color
81
+ *
82
+ * Invalid input returns options.dark (assumes light background).
83
+ */
84
+ export function getContrastForeground(hex, options = {}) {
85
+ const { light = '#fff', dark = '#000' } = options;
86
+ const tone = getContrastTone(hex);
87
+ return tone === 'light' ? light : dark;
88
+ }
@@ -0,0 +1,25 @@
1
+ export type ColorScheme = 'light' | 'dark' | 'auto';
2
+ /**
3
+ * Set color scheme on a .tui-interface element.
4
+ *
5
+ * - 'light': Removes data-theme attribute (default state)
6
+ * - 'dark': Sets data-theme="dark"
7
+ * - 'auto': Sets data-theme="auto" (respects prefers-color-scheme)
8
+ *
9
+ * Note: 'auto' falls back to light if matchMedia is unsupported.
10
+ */
11
+ export declare function setColorScheme(scheme: ColorScheme, root?: Element | null): void;
12
+ /**
13
+ * Get current color scheme from a .tui-interface element.
14
+ * Returns 'light' if no data-theme attribute present.
15
+ */
16
+ export declare function getColorScheme(root?: Element | null): ColorScheme;
17
+ /**
18
+ * Get the resolved color scheme (what's actually displayed).
19
+ * For 'auto', checks prefers-color-scheme media query.
20
+ */
21
+ export declare function getResolvedColorScheme(root?: Element | null): 'light' | 'dark';
22
+ /**
23
+ * Toggle between light and dark (skips auto).
24
+ */
25
+ export declare function toggleColorScheme(root?: Element | null): ColorScheme;
@@ -0,0 +1,55 @@
1
+ import { INTERFACE_SELECTOR } from '../constants.js';
2
+ const ATTR = 'data-theme';
3
+ /**
4
+ * Set color scheme on a .tui-interface element.
5
+ *
6
+ * - 'light': Removes data-theme attribute (default state)
7
+ * - 'dark': Sets data-theme="dark"
8
+ * - 'auto': Sets data-theme="auto" (respects prefers-color-scheme)
9
+ *
10
+ * Note: 'auto' falls back to light if matchMedia is unsupported.
11
+ */
12
+ export function setColorScheme(scheme, root) {
13
+ const el = root ?? document.querySelector(INTERFACE_SELECTOR);
14
+ if (!el)
15
+ return;
16
+ if (scheme === 'light') {
17
+ el.removeAttribute(ATTR);
18
+ }
19
+ else {
20
+ el.setAttribute(ATTR, scheme);
21
+ }
22
+ }
23
+ /**
24
+ * Get current color scheme from a .tui-interface element.
25
+ * Returns 'light' if no data-theme attribute present.
26
+ */
27
+ export function getColorScheme(root) {
28
+ const el = root ?? document.querySelector(INTERFACE_SELECTOR);
29
+ const attr = el?.getAttribute(ATTR);
30
+ return attr === 'dark' || attr === 'auto' ? attr : 'light';
31
+ }
32
+ /**
33
+ * Get the resolved color scheme (what's actually displayed).
34
+ * For 'auto', checks prefers-color-scheme media query.
35
+ */
36
+ export function getResolvedColorScheme(root) {
37
+ const scheme = getColorScheme(root);
38
+ if (scheme === 'auto') {
39
+ // Fallback to light if matchMedia unavailable
40
+ if (typeof window === 'undefined' || typeof window.matchMedia !== 'function') {
41
+ return 'light';
42
+ }
43
+ return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light';
44
+ }
45
+ return scheme;
46
+ }
47
+ /**
48
+ * Toggle between light and dark (skips auto).
49
+ */
50
+ export function toggleColorScheme(root) {
51
+ const current = getColorScheme(root);
52
+ const next = current === 'dark' ? 'light' : 'dark';
53
+ setColorScheme(next, root);
54
+ return next;
55
+ }
@@ -0,0 +1,17 @@
1
+ import type { Ref, RefCallback } from 'react';
2
+ /**
3
+ * Combines multiple refs into a single ref callback.
4
+ * Useful for components that need to forward refs while also maintaining
5
+ * an internal ref, or when using asChild pattern with ref composition.
6
+ *
7
+ * @example
8
+ * const MyComponent = forwardRef((props, forwardedRef) => {
9
+ * const internalRef = useRef(null);
10
+ * return <div ref={composeRefs(forwardedRef, internalRef)} />;
11
+ * });
12
+ */
13
+ export declare function composeRefs<T>(...refs: (Ref<T> | undefined | null)[]): RefCallback<T>;
14
+ /**
15
+ * Sets a single ref value. Handles both callback refs and ref objects.
16
+ */
17
+ export declare function setRef<T>(ref: Ref<T> | undefined | null, value: T | null): void;
@@ -0,0 +1,38 @@
1
+ /**
2
+ * Combines multiple refs into a single ref callback.
3
+ * Useful for components that need to forward refs while also maintaining
4
+ * an internal ref, or when using asChild pattern with ref composition.
5
+ *
6
+ * @example
7
+ * const MyComponent = forwardRef((props, forwardedRef) => {
8
+ * const internalRef = useRef(null);
9
+ * return <div ref={composeRefs(forwardedRef, internalRef)} />;
10
+ * });
11
+ */
12
+ export function composeRefs(...refs) {
13
+ return (node) => {
14
+ refs.forEach((ref) => {
15
+ if (!ref)
16
+ return;
17
+ if (typeof ref === 'function') {
18
+ ref(node);
19
+ }
20
+ else {
21
+ ref.current = node;
22
+ }
23
+ });
24
+ };
25
+ }
26
+ /**
27
+ * Sets a single ref value. Handles both callback refs and ref objects.
28
+ */
29
+ export function setRef(ref, value) {
30
+ if (!ref)
31
+ return;
32
+ if (typeof ref === 'function') {
33
+ ref(value);
34
+ }
35
+ else {
36
+ ref.current = value;
37
+ }
38
+ }
package/utils/cx.d.ts ADDED
@@ -0,0 +1,12 @@
1
+ /**
2
+ * Simple class name joiner - replaces clsx dependency
3
+ *
4
+ * Joins class name arguments, filtering out falsy values.
5
+ * Supports string, boolean, undefined, and null arguments.
6
+ *
7
+ * @example
8
+ * cx('base', 'modifier') // 'base modifier'
9
+ * cx('base', false && 'hidden') // 'base'
10
+ * cx('base', isActive && 'is-active', className) // 'base is-active custom'
11
+ */
12
+ export declare function cx(...args: (string | boolean | undefined | null)[]): string;
package/utils/cx.js ADDED
@@ -0,0 +1,14 @@
1
+ /**
2
+ * Simple class name joiner - replaces clsx dependency
3
+ *
4
+ * Joins class name arguments, filtering out falsy values.
5
+ * Supports string, boolean, undefined, and null arguments.
6
+ *
7
+ * @example
8
+ * cx('base', 'modifier') // 'base modifier'
9
+ * cx('base', false && 'hidden') // 'base'
10
+ * cx('base', isActive && 'is-active', className) // 'base is-active custom'
11
+ */
12
+ export function cx(...args) {
13
+ return args.filter(Boolean).join(' ');
14
+ }
@@ -0,0 +1,40 @@
1
+ /**
2
+ * CSS selector for focusable elements.
3
+ * Used by focus trap and initial focus logic.
4
+ */
5
+ export declare const FOCUSABLE_SELECTOR: string;
6
+ /**
7
+ * Options for the focus trap hook.
8
+ */
9
+ export type UseFocusTrapOptions = {
10
+ /** Whether the trap is currently active */
11
+ isActive: boolean;
12
+ /** Optional callback when Escape is pressed */
13
+ onEscape?: () => void;
14
+ /** Whether Escape should trigger onEscape (default: true) */
15
+ escapeDeactivates?: boolean;
16
+ };
17
+ /**
18
+ * Traps focus within a container element.
19
+ * Tab and Shift+Tab cycle through focusable elements inside the container.
20
+ * If no focusable elements exist, focus returns to the container itself.
21
+ *
22
+ * @param containerRef - Ref to the container element
23
+ * @param options - Configuration options
24
+ *
25
+ * @example
26
+ * ```tsx
27
+ * const dialogRef = useRef<HTMLDivElement>(null);
28
+ * useFocusTrap(dialogRef, { isActive: isOpen, onEscape: onClose });
29
+ * ```
30
+ */
31
+ export declare function useFocusTrap(containerRef: React.RefObject<HTMLElement | null>, options: UseFocusTrapOptions): void;
32
+ /**
33
+ * Finds the first focusable element within a container.
34
+ * Optionally accepts a custom selector to try first.
35
+ *
36
+ * @param container - The container element to search within
37
+ * @param preferredSelector - Optional selector to try before fallback
38
+ * @returns The found element, or the container if nothing found
39
+ */
40
+ export declare function getInitialFocus(container: HTMLElement, preferredSelector?: string): HTMLElement;
@@ -0,0 +1,93 @@
1
+ import { useEffect } from 'react';
2
+ /**
3
+ * CSS selector for focusable elements.
4
+ * Used by focus trap and initial focus logic.
5
+ */
6
+ export const FOCUSABLE_SELECTOR = [
7
+ '[data-autofocus]',
8
+ 'a[href]',
9
+ 'area[href]',
10
+ 'button:not([disabled])',
11
+ 'input:not([disabled]):not([type="hidden"])',
12
+ 'select:not([disabled])',
13
+ 'textarea:not([disabled])',
14
+ 'iframe',
15
+ 'audio[controls]',
16
+ 'video[controls]',
17
+ '[contenteditable]:not([contenteditable="false"])',
18
+ '[tabindex]:not([tabindex="-1"])',
19
+ ].join(',');
20
+ /**
21
+ * Traps focus within a container element.
22
+ * Tab and Shift+Tab cycle through focusable elements inside the container.
23
+ * If no focusable elements exist, focus returns to the container itself.
24
+ *
25
+ * @param containerRef - Ref to the container element
26
+ * @param options - Configuration options
27
+ *
28
+ * @example
29
+ * ```tsx
30
+ * const dialogRef = useRef<HTMLDivElement>(null);
31
+ * useFocusTrap(dialogRef, { isActive: isOpen, onEscape: onClose });
32
+ * ```
33
+ */
34
+ export function useFocusTrap(containerRef, options) {
35
+ const { isActive, onEscape, escapeDeactivates = true } = options;
36
+ useEffect(() => {
37
+ if (!isActive)
38
+ return;
39
+ const container = containerRef.current;
40
+ if (!container)
41
+ return;
42
+ const handleKeyDown = (e) => {
43
+ // Escape handling
44
+ if (e.key === 'Escape' && escapeDeactivates && onEscape) {
45
+ onEscape();
46
+ return;
47
+ }
48
+ // Tab trap
49
+ if (e.key !== 'Tab')
50
+ return;
51
+ const nodes = Array.from(container.querySelectorAll(FOCUSABLE_SELECTOR)).filter((n) => n.offsetParent !== null || n === document.activeElement);
52
+ if (nodes.length === 0) {
53
+ e.preventDefault();
54
+ container.focus();
55
+ return;
56
+ }
57
+ const first = nodes[0];
58
+ const last = nodes[nodes.length - 1];
59
+ const active = document.activeElement;
60
+ if (e.shiftKey) {
61
+ if (active === first || !container.contains(active)) {
62
+ e.preventDefault();
63
+ last.focus();
64
+ }
65
+ }
66
+ else {
67
+ if (active === last || !container.contains(active)) {
68
+ e.preventDefault();
69
+ first.focus();
70
+ }
71
+ }
72
+ };
73
+ document.addEventListener('keydown', handleKeyDown);
74
+ return () => document.removeEventListener('keydown', handleKeyDown);
75
+ }, [isActive, containerRef, onEscape, escapeDeactivates]);
76
+ }
77
+ /**
78
+ * Finds the first focusable element within a container.
79
+ * Optionally accepts a custom selector to try first.
80
+ *
81
+ * @param container - The container element to search within
82
+ * @param preferredSelector - Optional selector to try before fallback
83
+ * @returns The found element, or the container if nothing found
84
+ */
85
+ export function getInitialFocus(container, preferredSelector) {
86
+ if (preferredSelector) {
87
+ const preferred = container.querySelector(preferredSelector);
88
+ if (preferred)
89
+ return preferred;
90
+ }
91
+ const firstFocusable = container.querySelector(FOCUSABLE_SELECTOR);
92
+ return firstFocusable ?? container;
93
+ }
@@ -0,0 +1,10 @@
1
+ export { setColorScheme, getColorScheme, getResolvedColorScheme, toggleColorScheme } from './color-scheme';
2
+ export type { ColorScheme } from './color-scheme';
3
+ export { getPortalRoot, getPortalRootFor } from './portal';
4
+ export { useFocusTrap, getInitialFocus, FOCUSABLE_SELECTOR } from './focus-trap';
5
+ export { isAnchor, getSafeRel, getDisabledAnchorProps, getDisabledButtonProps, } from './polymorphic';
6
+ export { composeRefs } from './compose-refs';
7
+ export { mergeProps } from './merge-props';
8
+ export { cx } from './cx';
9
+ export { getContrastTone, getContrastForeground } from './color/contrast';
10
+ export type { ContrastTone } from './color/contrast';
package/utils/index.js ADDED
@@ -0,0 +1,16 @@
1
+ // Color scheme utilities
2
+ export { setColorScheme, getColorScheme, getResolvedColorScheme, toggleColorScheme } from './color-scheme.js';
3
+ // Portal utilities
4
+ export { getPortalRoot, getPortalRootFor } from './portal.js';
5
+ // Focus trap utilities
6
+ export { useFocusTrap, getInitialFocus, FOCUSABLE_SELECTOR } from './focus-trap.js';
7
+ // Polymorphic component utilities
8
+ export { isAnchor, getSafeRel, getDisabledAnchorProps, getDisabledButtonProps, } from './polymorphic.js';
9
+ // Ref composition
10
+ export { composeRefs } from './compose-refs.js';
11
+ // Prop merging
12
+ export { mergeProps } from './merge-props.js';
13
+ // Class name joining (replaces clsx)
14
+ export { cx } from './cx.js';
15
+ // Contrast utilities (for user-selected colors)
16
+ export { getContrastTone, getContrastForeground } from './color/contrast.js';
@@ -0,0 +1,4 @@
1
+ export declare const clamp: (value: number, min?: number, max?: number) => number;
2
+ export declare const normalize: (value: number, currentScaleMin: number, currentScaleMax: number, newScaleMin?: number, newScaleMax?: number) => number;
3
+ export declare const clampedNormalize: (value: number, currentScaleMin: number, currentScaleMax: number, newScaleMin?: number, newScaleMax?: number) => number;
4
+ export declare const exponentialNormalize: (value: number, currentScaleMin: number, currentScaleMax: number, newScaleMin?: number, newScaleMax?: number, exponent?: number) => number;
package/utils/math.js ADDED
@@ -0,0 +1,19 @@
1
+ export const clamp = (value, min = 0, max = 1) => {
2
+ if (min > max) {
3
+ [min, max] = [max, min];
4
+ }
5
+ return Math.max(min, Math.min(max, value));
6
+ };
7
+ export const normalize = (value, currentScaleMin, currentScaleMax, newScaleMin = 0, newScaleMax = 1) => {
8
+ const standardNormalization = (value - currentScaleMin) / (currentScaleMax - currentScaleMin);
9
+ return ((newScaleMax - newScaleMin) * standardNormalization + newScaleMin);
10
+ };
11
+ export const clampedNormalize = (value, currentScaleMin, currentScaleMax, newScaleMin = 0, newScaleMax = 1) => {
12
+ return clamp(normalize(value, currentScaleMin, currentScaleMax, newScaleMin, newScaleMax), newScaleMin, newScaleMax);
13
+ };
14
+ // Exponential interpolation, input is mapped onto a curved line rather than a straight one
15
+ export const exponentialNormalize = (value, currentScaleMin, currentScaleMax, newScaleMin = 0, newScaleMax = 1, exponent = 2) => {
16
+ const normalizedInput = (value - currentScaleMin) / (currentScaleMax - currentScaleMin);
17
+ const exponentialOutput = Math.pow(normalizedInput, exponent);
18
+ return (newScaleMin + (newScaleMax - newScaleMin) * exponentialOutput);
19
+ };
@@ -0,0 +1,25 @@
1
+ type AnyProps = Record<string, unknown>;
2
+ /**
3
+ * Merges two sets of props, composing event handlers so both fire.
4
+ * Useful for extending component props without clobbering existing values.
5
+ *
6
+ * Merge rules:
7
+ * - Event handlers (on*): Both called in sequence (slotProps first, then overrideProps)
8
+ * - className: Concatenated with a space
9
+ * - style: Shallow merged (overrideProps wins conflicts)
10
+ * - Other props: overrideProps takes precedence
11
+ *
12
+ * Note: For asChild patterns where parent props must not be overridden
13
+ * (e.g., data-* attributes for registration), spread them after the merge:
14
+ *
15
+ * @example
16
+ * // Event handler composition
17
+ * mergeProps({ onClick: logClick }, { onClick: doAction })
18
+ * // Result: { onClick: [calls logClick, then doAction] }
19
+ *
20
+ * @example
21
+ * // Ensuring parent attribute wins
22
+ * cloneElement(child, { ...child.props, 'data-toolbar-item': '' })
23
+ */
24
+ export declare function mergeProps<T extends AnyProps, U extends AnyProps>(slotProps: T, overrideProps: U): T & U;
25
+ export {};