@tcn/ui 0.0.4 → 0.2.0

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 (198) hide show
  1. package/README.md +38 -3
  2. package/dist/actions/button/base_button/base_button.d.ts.map +1 -1
  3. package/dist/actions/button/base_button/base_button.js +17 -12
  4. package/dist/actions/button/base_button/base_button.js.map +1 -1
  5. package/dist/actions/button/button/button.d.ts.map +1 -1
  6. package/dist/actions/button/button/button.js +7 -7
  7. package/dist/actions/button/button/button.js.map +1 -1
  8. package/dist/actions/button/slim_button/slim_button.js +2 -2
  9. package/dist/actions/button/slim_button/slim_button.js.map +1 -1
  10. package/dist/button.css +1 -1
  11. package/dist/draggable.css +1 -0
  12. package/dist/feedback/progress/progress_bar.js +1 -1
  13. package/dist/footer.css +1 -1
  14. package/dist/form/field/common/field_description.js +1 -1
  15. package/dist/form/field/common/field_error.js +4 -3
  16. package/dist/form/field/common/field_error.js.map +1 -1
  17. package/dist/form/field/common/field_label.js +1 -1
  18. package/dist/inputs/date_picker/date_picker_date.js +1 -1
  19. package/dist/inputs/date_picker/date_picker_day.js +1 -1
  20. package/dist/inputs/date_picker/date_picker_time_selector.js +1 -1
  21. package/dist/inputs/date_picker/date_picker_year_selector.js +1 -1
  22. package/dist/inputs/input/input.d.ts +2 -2
  23. package/dist/inputs/input/input.d.ts.map +1 -1
  24. package/dist/inputs/input/input.js.map +1 -1
  25. package/dist/inputs/options/option.d.ts +1 -0
  26. package/dist/inputs/options/option.d.ts.map +1 -1
  27. package/dist/inputs/options/option.js.map +1 -1
  28. package/dist/inputs/phone_number_input/phone_number_input.d.ts +8 -1
  29. package/dist/inputs/phone_number_input/phone_number_input.d.ts.map +1 -1
  30. package/dist/inputs/phone_number_input/phone_number_input.js +187 -137
  31. package/dist/inputs/phone_number_input/phone_number_input.js.map +1 -1
  32. package/dist/inputs/suggestions/suggestion_item.d.ts +1 -1
  33. package/dist/inputs/suggestions/suggestion_item.d.ts.map +1 -1
  34. package/dist/inputs/suggestions/suggestion_item.js +23 -18
  35. package/dist/inputs/suggestions/suggestion_item.js.map +1 -1
  36. package/dist/inputs/suggestions/suggestion_list.d.ts +1 -1
  37. package/dist/inputs/suggestions/suggestion_list.d.ts.map +1 -1
  38. package/dist/inputs/suggestions/suggestion_list.js +106 -96
  39. package/dist/inputs/suggestions/suggestion_list.js.map +1 -1
  40. package/dist/inputs/textarea/textarea.d.ts +2 -2
  41. package/dist/inputs/textarea/textarea.d.ts.map +1 -1
  42. package/dist/inputs/textarea/textarea.js.map +1 -1
  43. package/dist/layouts/footer/footer.js +5 -5
  44. package/dist/layouts/footer/footer.js.map +1 -1
  45. package/dist/layouts/header/header.d.ts.map +1 -1
  46. package/dist/layouts/header/header.js.map +1 -1
  47. package/dist/layouts/index.d.ts +3 -2
  48. package/dist/layouts/index.d.ts.map +1 -1
  49. package/dist/layouts/index.js +26 -24
  50. package/dist/layouts/index.js.map +1 -1
  51. package/dist/layouts/list/item.d.ts +1 -0
  52. package/dist/layouts/list/item.d.ts.map +1 -1
  53. package/dist/layouts/list/item.js +17 -6
  54. package/dist/layouts/list/item.js.map +1 -1
  55. package/dist/layouts/list/list.js +10 -10
  56. package/dist/layouts/list/list.js.map +1 -1
  57. package/dist/overlay/context_menu/context_menu.js +4 -4
  58. package/dist/overlay/frame/frame.d.ts +11 -0
  59. package/dist/overlay/frame/frame.d.ts.map +1 -0
  60. package/dist/overlay/frame/frame.js +18 -0
  61. package/dist/overlay/frame/frame.js.map +1 -0
  62. package/dist/overlay/index.d.ts +1 -0
  63. package/dist/overlay/index.d.ts.map +1 -1
  64. package/dist/overlay/index.js +5 -3
  65. package/dist/overlay/index.js.map +1 -1
  66. package/dist/overlay/popper/popper.js +12 -12
  67. package/dist/{portal-qqIp4SIl.js → overlay/portal/portal.js} +3 -3
  68. package/dist/overlay/portal/portal.js.map +1 -0
  69. package/dist/overlay/portal/portal_platform.js +3 -4
  70. package/dist/overlay/portal/portal_platform.js.map +1 -1
  71. package/dist/phone_number_input.css +1 -1
  72. package/dist/slim_button.css +1 -1
  73. package/dist/stacks/box/box.d.ts +1 -1
  74. package/dist/stacks/box/box.d.ts.map +1 -1
  75. package/dist/stacks/box/box.js.map +1 -1
  76. package/dist/surfaces/card/card.d.ts.map +1 -1
  77. package/dist/surfaces/card/card.js +7 -16
  78. package/dist/surfaces/card/card.js.map +1 -1
  79. package/dist/surfaces/confirm/confirm.js +4 -4
  80. package/dist/surfaces/index.d.ts +2 -2
  81. package/dist/surfaces/index.d.ts.map +1 -1
  82. package/dist/surfaces/index.js +22 -22
  83. package/dist/surfaces/modal/modal.d.ts +3 -2
  84. package/dist/surfaces/modal/modal.d.ts.map +1 -1
  85. package/dist/surfaces/modal/modal.js +14 -13
  86. package/dist/surfaces/modal/modal.js.map +1 -1
  87. package/dist/surfaces/window/window.d.ts +3 -2
  88. package/dist/surfaces/window/window.d.ts.map +1 -1
  89. package/dist/surfaces/window/window.js +17 -7
  90. package/dist/surfaces/window/window.js.map +1 -1
  91. package/dist/themes/index.js +6 -141
  92. package/dist/themes/index.js.map +1 -1
  93. package/dist/themes/stylesheets/reset.js +140 -0
  94. package/dist/themes/stylesheets/reset.js.map +1 -0
  95. package/dist/themes/themes/ergo/ergo_theme.js +664 -0
  96. package/dist/themes/themes/ergo/ergo_theme.js.map +1 -0
  97. package/dist/tokens/bubble/bubble.js +17 -16
  98. package/dist/tokens/bubble/bubble.js.map +1 -1
  99. package/dist/tokens/chip/chip.js +9 -8
  100. package/dist/tokens/chip/chip.js.map +1 -1
  101. package/dist/typography/title/title.d.ts +2 -1
  102. package/dist/typography/title/title.d.ts.map +1 -1
  103. package/dist/typography/title/title.js +24 -23
  104. package/dist/typography/title/title.js.map +1 -1
  105. package/dist/utils/dnd/context.d.ts +4 -0
  106. package/dist/utils/dnd/context.d.ts.map +1 -0
  107. package/dist/utils/dnd/context.js +20 -0
  108. package/dist/utils/dnd/context.js.map +1 -0
  109. package/dist/utils/dnd/draggable/draggable.d.ts +7 -0
  110. package/dist/utils/dnd/draggable/draggable.d.ts.map +1 -0
  111. package/dist/utils/dnd/draggable/draggable.js +27 -0
  112. package/dist/utils/dnd/draggable/draggable.js.map +1 -0
  113. package/dist/utils/dnd/handle.d.ts +6 -0
  114. package/dist/utils/dnd/handle.d.ts.map +1 -0
  115. package/dist/utils/dnd/handle.js +22 -0
  116. package/dist/utils/dnd/handle.js.map +1 -0
  117. package/dist/utils/dnd/hooks/use_drag_container.d.ts +7 -0
  118. package/dist/utils/dnd/hooks/use_drag_container.d.ts.map +1 -0
  119. package/dist/utils/dnd/hooks/use_drag_container.js +30 -0
  120. package/dist/utils/dnd/hooks/use_drag_container.js.map +1 -0
  121. package/dist/utils/{hooks → dnd/hooks}/use_draggable.d.ts +3 -3
  122. package/dist/utils/dnd/hooks/use_draggable.d.ts.map +1 -0
  123. package/dist/utils/dnd/hooks/use_draggable.js +41 -0
  124. package/dist/utils/dnd/hooks/use_draggable.js.map +1 -0
  125. package/dist/utils/dnd/types.d.ts +10 -0
  126. package/dist/utils/dnd/types.d.ts.map +1 -0
  127. package/dist/utils/dnd/types.js +2 -0
  128. package/dist/utils/dnd/types.js.map +1 -0
  129. package/dist/utils/index.d.ts +1 -1
  130. package/dist/utils/index.d.ts.map +1 -1
  131. package/dist/utils/index.js +1 -1
  132. package/package.json +9 -3
  133. package/src/actions/button/base_button/base_button.tsx +7 -2
  134. package/src/actions/button/button/button.module.css +0 -78
  135. package/src/actions/button/button/button.tsx +2 -4
  136. package/src/actions/button/slim_button/slim_button.module.css +0 -26
  137. package/src/actions/button/slim_button/slim_button.tsx +1 -1
  138. package/src/inputs/input/input.tsx +3 -2
  139. package/src/inputs/options/option.tsx +1 -0
  140. package/src/inputs/phone_number_input/phone_number_input.module.css +12 -0
  141. package/src/inputs/phone_number_input/phone_number_input.stories.tsx +8 -0
  142. package/src/inputs/phone_number_input/phone_number_input.tsx +107 -21
  143. package/src/inputs/suggestions/suggestion_item.tsx +12 -2
  144. package/src/inputs/suggestions/suggestion_list.tsx +22 -3
  145. package/src/inputs/textarea/textarea.tsx +2 -2
  146. package/src/layouts/footer/footer.module.css +0 -1
  147. package/src/layouts/footer/footer.tsx +1 -1
  148. package/src/layouts/header/header.tsx +0 -1
  149. package/src/layouts/index.ts +3 -2
  150. package/src/layouts/list/item.tsx +10 -2
  151. package/src/layouts/list/list.tsx +2 -2
  152. package/src/overlay/frame/frame.stories.tsx +40 -0
  153. package/src/overlay/frame/frame.tsx +34 -0
  154. package/src/overlay/frame/frame_stories.module.css +14 -0
  155. package/src/overlay/index.ts +1 -0
  156. package/src/stacks/box/box.tsx +8 -2
  157. package/src/surfaces/card/card.tsx +2 -8
  158. package/src/surfaces/index.ts +2 -2
  159. package/src/surfaces/modal/__stories__/modal.stories.tsx +19 -27
  160. package/src/surfaces/modal/modal.tsx +13 -10
  161. package/src/surfaces/panel/__stories__/panel.stories.tsx +13 -12
  162. package/src/surfaces/window/window.stories.tsx +37 -4
  163. package/src/surfaces/window/window.tsx +14 -6
  164. package/src/themes/themes/ergo/__stories__/components/material_picker/sb_inverted_materials.module.css +34 -0
  165. package/src/themes/themes/ergo/__stories__/components/material_picker/sb_material_picker.tsx +52 -0
  166. package/src/themes/themes/ergo/__stories__/components/tone_picker/sb_card.module.css +5 -0
  167. package/src/themes/themes/ergo/__stories__/components/tone_picker/sb_tone_card.tsx +40 -0
  168. package/src/themes/themes/ergo/__stories__/components/tone_picker/sb_tone_picker.tsx +83 -0
  169. package/src/themes/themes/ergo/__stories__/components/tone_picker/types.ts +7 -0
  170. package/src/themes/themes/ergo/__stories__/material.stories.tsx +154 -0
  171. package/src/themes/themes/ergo/__stories__/sb_materials.module.css +110 -0
  172. package/src/themes/themes/ergo/__stories__/utils.ts +92 -0
  173. package/src/themes/themes/ergo/ergo_theme.css +358 -26
  174. package/src/typography/title/title.tsx +23 -19
  175. package/src/utils/dnd/__stories__/draggable.stories.tsx +48 -0
  176. package/src/utils/dnd/__stories__/draggable_stories.module.css +21 -0
  177. package/src/utils/{__stories__ → dnd/__stories__}/use_draggable.stories.tsx +15 -10
  178. package/src/utils/dnd/context.ts +24 -0
  179. package/src/utils/dnd/draggable/draggable.module.css +8 -0
  180. package/src/utils/dnd/draggable/draggable.tsx +42 -0
  181. package/src/utils/dnd/handle.tsx +32 -0
  182. package/src/utils/dnd/hooks/use_drag_container.ts +42 -0
  183. package/src/utils/{hooks → dnd/hooks}/use_draggable.ts +23 -17
  184. package/src/utils/dnd/types.ts +6 -0
  185. package/src/utils/index.ts +1 -1
  186. package/tsconfig.json +0 -3
  187. package/dist/card.css +0 -1
  188. package/dist/portal-qqIp4SIl.js.map +0 -1
  189. package/dist/themes/stylesheets/reset.css +0 -1
  190. package/dist/themes/themes/ergo/ergo_theme.css +0 -1
  191. package/dist/themes/themes/windows_98/windows_98.css +0 -1
  192. package/dist/title.module-B16de2jd.js +0 -5
  193. package/dist/title.module-B16de2jd.js.map +0 -1
  194. package/dist/utils/hooks/use_draggable.d.ts.map +0 -1
  195. package/dist/utils/hooks/use_draggable.js +0 -30
  196. package/dist/utils/hooks/use_draggable.js.map +0 -1
  197. package/src/surfaces/card/card.module.css +0 -5
  198. /package/dist/{overlay/portal/portal.css → portal_platform.css} +0 -0
@@ -0,0 +1,154 @@
1
+ import React, { useState } from 'react';
2
+ import { VStack } from '../../../../stacks/v_stack.js';
3
+ import { clsx } from 'clsx';
4
+ import { PlusIcon } from '@tcn/icons/plus_icon.js';
5
+ import { Divider, Footer, Header, VBody } from '../../../../layouts/index.js';
6
+ import { VPanel } from '../../../../surfaces/index.js';
7
+ import { Button } from '../../../../actions/button/button/button.js';
8
+ import { Spacer } from '../../../../stacks/spacer.js';
9
+ import { SlimButton } from '../../../../actions/button/slim_button/slim_button.js';
10
+ import { SB_TonePicker } from './components/tone_picker/sb_tone_picker.js';
11
+ import { formatHSLString, getContrastText, parseHSL } from './utils.js';
12
+ import { SB_MaterialPicker } from './components/material_picker/sb_material_picker.js';
13
+ import { Title } from '../../../../typography/index.js';
14
+ import { theme } from '../../../theme_variables.js';
15
+
16
+ // Styles
17
+ import styles from './sb_materials.module.css';
18
+
19
+ export default {
20
+ title: 'Themes/Material',
21
+ tags: ['autodocs'],
22
+ argTypes: {
23
+ materialColor: {
24
+ control: { type: 'color', presetColors: [], disableAlpha: false },
25
+ label: 'Custom Material Color',
26
+ format: 'hsla',
27
+ },
28
+ accentColor: {
29
+ control: { type: 'color', presetColors: [], disableAlpha: false },
30
+ label: 'Custom Accent Color',
31
+ format: 'hsla',
32
+ },
33
+ },
34
+ args: {
35
+ materialColor: 'hsl(122, 100%, 20%)',
36
+ accentColor: 'hsl(122, 100%, 55%)',
37
+ },
38
+ };
39
+
40
+ function useCustomSurface(materialColor?: string, accentColor?: string) {
41
+ const mat = materialColor || 'hsl(122, 100%, 20%)';
42
+ const acc = accentColor || 'hsl(122, 100%, 55%)';
43
+
44
+ const material = parseHSL(mat);
45
+ const accent = parseHSL(acc);
46
+ const onMaterial = getContrastText(material);
47
+ const onAction = getContrastText(accent);
48
+
49
+ return {
50
+ material: formatHSLString(material),
51
+ accent: formatHSLString(accent),
52
+ onMaterial: formatHSLString(onMaterial),
53
+ onAction: formatHSLString(onAction),
54
+ };
55
+ }
56
+
57
+ export const DynamicSurface = (args: {
58
+ materialColor?: string;
59
+ accentColor?: string;
60
+ }) => {
61
+ const [surface, setSurface] = useState('light-primary');
62
+
63
+ const handleSurfaceClick = (surface: string) => () => {
64
+ setSurface(surface);
65
+ };
66
+
67
+ const custom = useCustomSurface(args.materialColor, args.accentColor);
68
+
69
+ return (
70
+ <VStack
71
+ className={styles['storybook-palette']}
72
+ height="100%"
73
+ gap={theme.gap.md}
74
+ padding={theme.padding.md}
75
+ growWeight={1}
76
+ style={
77
+ {
78
+ '--custom-action': custom.accent,
79
+ '--custom-on-action': custom.onAction,
80
+ '--custom-material': custom.material,
81
+ '--custom-on-material': custom.onMaterial,
82
+ } as React.CSSProperties
83
+ }
84
+ >
85
+ <Header>
86
+ <Title>Ergo Theme: Materials and Surfaces</Title>
87
+ </Header>
88
+ <SB_MaterialPicker handleSurfaceClick={handleSurfaceClick} />
89
+ <VPanel
90
+ // TODO: Find a better way to have typography inherit the on material color
91
+ style={{
92
+ color: 'inherit',
93
+ }}
94
+ minHeight="200px"
95
+ maxHeight="700px"
96
+ height="100%"
97
+ className={clsx(styles[`${surface}`], styles['surface'])}
98
+ >
99
+ <Header>
100
+ Enabled Utilities
101
+ <Spacer />
102
+ <SlimButton hierarchy="tertiary">
103
+ <PlusIcon />
104
+ </SlimButton>
105
+ <SlimButton hierarchy="secondary">
106
+ <PlusIcon />
107
+ </SlimButton>
108
+ <Divider vertical />
109
+ <SlimButton hierarchy="primary">
110
+ <PlusIcon />
111
+ </SlimButton>
112
+ </Header>
113
+ <Header>
114
+ Disabled Utilities
115
+ <Spacer />
116
+ <SlimButton hierarchy="tertiary" disabled>
117
+ <PlusIcon />
118
+ </SlimButton>
119
+ <SlimButton hierarchy="secondary" disabled>
120
+ <PlusIcon />
121
+ </SlimButton>
122
+ <Divider vertical />
123
+ <SlimButton hierarchy="primary" disabled>
124
+ <PlusIcon />
125
+ </SlimButton>
126
+ </Header>
127
+ <VBody>
128
+ <SB_TonePicker />
129
+ </VBody>
130
+ <Footer>
131
+ Disabled Buttons
132
+ <Spacer />
133
+ <Button hierarchy="tertiary" disabled>
134
+ Tertiary
135
+ </Button>
136
+ <Button hierarchy="secondary" disabled>
137
+ Secondary
138
+ </Button>
139
+ <Button hierarchy="primary" disabled>
140
+ Primary
141
+ </Button>
142
+ </Footer>
143
+ <Footer>
144
+ Enabled Buttons
145
+ <Spacer />
146
+ <Button hierarchy="tertiary">Tertiary</Button>
147
+ <Button hierarchy="secondary">Secondary</Button>
148
+ <Button hierarchy="primary">Primary</Button>
149
+ </Footer>
150
+ </VPanel>
151
+ <Header></Header>
152
+ </VStack>
153
+ );
154
+ };
@@ -0,0 +1,110 @@
1
+ .storybook-palette {
2
+ --background: #f1f1f1;
3
+ --primary: 23, 65%, 49%;
4
+ --navy: 213, 35.6%, 34.7%;
5
+ --white: 0, 0%, 100%;
6
+ --tan: 33, 22%, 84%;
7
+ --secondary: 197, 34.2%, 55.3%;
8
+ --secondary-light: 208 65.2% 95.5%;
9
+ --slate: 217, 32.6%, 17.5%;
10
+
11
+ --positive: 141, 71%, 48%; /* Green */
12
+ --notice: 202, 76%, 50%; /* Blue */
13
+ --neutral: 0, 0%, 100%; /* White */
14
+ --warning: 48, 100%, 50%; /* Yellow */
15
+ --danger: 0, 100%, 50%; /* Red */
16
+
17
+ background: var(--background);
18
+
19
+ /* This behavior can be moved to ergo theme once dialed and the global will be dropped */
20
+ :global(.tcn-card) {
21
+ :global(.tcn-header) {
22
+ background: hsl(var(--material));
23
+ color: hsl(var(--on-material));
24
+ }
25
+ }
26
+
27
+ :global(.tcn-card[data-tone="positive"]) {
28
+ --material: var(--positive);
29
+ --on-material: var(--white);
30
+ --action: var(--white);
31
+ --on-action: var(--positive);
32
+ }
33
+
34
+ :global(.tcn-card[data-tone="notice"]) {
35
+ --material: var(--notice);
36
+ --on-material: var(--white);
37
+ --action: var(--white);
38
+ --on-action: var(--notice);
39
+ }
40
+
41
+ /* :global(.tcn-card[data-tone="neutral"]) {} */
42
+
43
+ :global(.tcn-card[data-tone="warning"]) {
44
+ --material: var(--warning);
45
+ --on-material: var(--navy);
46
+ --action: var(--navy);
47
+ --on-action: var(--white);
48
+ }
49
+
50
+ :global(.tcn-card[data-tone="danger"]) {
51
+ --material: var(--danger);
52
+ --on-material: var(--white);
53
+ --action: var(--white);
54
+ --on-action: var(--danger);
55
+ }
56
+ }
57
+
58
+ .light-primary {
59
+ --action: var(--primary);
60
+ --on-action: var(--white);
61
+ --material: var(--white);
62
+ --on-material: var(--navy);
63
+ }
64
+
65
+ .light-secondary {
66
+ --action: var(--navy);
67
+ --on-action: var(--white);
68
+ --material: var(--tan);
69
+ --on-material: var(--navy);
70
+ }
71
+
72
+ .dark-primary {
73
+ --action: var(--secondary);
74
+ --on-action: var(--white);
75
+ --material: var(--slate);
76
+ --on-material: var(--white);
77
+ }
78
+
79
+ .dark-secondary {
80
+ --action: var(--tan);
81
+ --on-action: var(--navy);
82
+ --material: var(--navy);
83
+ --on-material: var(--white);
84
+ }
85
+
86
+ .light-tertiary {
87
+ --action: var(--navy);
88
+ --on-action: var(--white);
89
+ --material: var(--secondary-light);
90
+ --on-material: var(--navy);
91
+ }
92
+
93
+ .surface-custom {
94
+ --action: var(--custom-action);
95
+ --on-action: var(--custom-on-action);
96
+ --material: var(--custom-material);
97
+ --on-material: var(--custom-on-material);
98
+ }
99
+
100
+ .surface-custom-inverted {
101
+ --action: var(--custom-material);
102
+ --on-action: var(--custom-on-material);
103
+ --material: var(--navy);
104
+ --on-material: var(--white);
105
+ }
106
+
107
+ .surface {
108
+ background: hsl(var(--material));
109
+ color: hsl(var(--on-material));
110
+ }
@@ -0,0 +1,92 @@
1
+ interface HSL {
2
+ h: string;
3
+ s: string;
4
+ l: string;
5
+ a?: string;
6
+ }
7
+
8
+ export function parseHSL(color: string): HSL {
9
+ if (!color) return { h: '46', s: '46%', l: '89%', a: '100%' };
10
+
11
+ // Handle hsl() format: "hsl(46, 46%, 89%)"
12
+ if (color.startsWith('hsl(')) {
13
+ const match = color.match(
14
+ /hsl\((\d+(?:\.\d+)?),\s*(\d+(?:\.\d+)?)%?,\s*(\d+(?:\.\d+)?)%?\)/
15
+ );
16
+ if (match) {
17
+ return {
18
+ h: match[1],
19
+ s: match[2].includes('%') ? match[2] : `${match[2]}%`,
20
+ l: match[3].includes('%') ? match[3] : `${match[3]}%`,
21
+ };
22
+ }
23
+ }
24
+
25
+ if (color.startsWith('hsla(')) {
26
+ const match = color.match(
27
+ /hsla\((\d+(?:\.\d+)?),\s*(\d+(?:\.\d+)?)%?,\s*(\d+(?:\.\d+)?)%?,\s*(\d+(?:\.\d+)?)\)/
28
+ );
29
+ if (match) {
30
+ return {
31
+ h: match[1],
32
+ s: match[2].includes('%') ? match[2] : `${match[2]}%`,
33
+ l: match[3].includes('%') ? match[3] : `${match[3]}%`,
34
+ a: match[4].includes('%') ? match[4] : `${match[4]}%`,
35
+ };
36
+ }
37
+ }
38
+
39
+ // Handle hex format (Storybook color picker often returns hex)
40
+ if (color.startsWith('#')) {
41
+ const r = parseInt(color.slice(1, 3), 16) / 255;
42
+ const g = parseInt(color.slice(3, 5), 16) / 255;
43
+ const b = parseInt(color.slice(5, 7), 16) / 255;
44
+ const max = Math.max(r, g, b);
45
+ const min = Math.min(r, g, b);
46
+ let h = 0;
47
+ let s = 0;
48
+ const l = (max + min) / 2;
49
+ if (max !== min) {
50
+ const d = max - min;
51
+ s = l > 0.5 ? d / (2 - max - min) : d / (max + min);
52
+ switch (max) {
53
+ case r:
54
+ h = ((g - b) / d + (g < b ? 6 : 0)) / 6;
55
+ break;
56
+ case g:
57
+ h = ((b - r) / d + 2) / 6;
58
+ break;
59
+ case b:
60
+ h = ((r - g) / d + 4) / 6;
61
+ break;
62
+ }
63
+ }
64
+ return {
65
+ h: Math.round(h * 360).toString(),
66
+ s: `${Math.round(s * 100)}%`,
67
+ l: `${Math.round(l * 100)}%`,
68
+ a: '100%',
69
+ };
70
+ }
71
+
72
+ // Default fallback
73
+ return { h: '46', s: '46%', l: '89%', a: '100%' };
74
+ }
75
+
76
+ export function getContrastText(material: HSL) {
77
+ const l = parseFloat(material.l);
78
+
79
+ // If background is light, return off-black for contrast.
80
+ // If background is dark, return off-white for contrast.
81
+ if (l > 50) {
82
+ // dark text for light background: "off-black"
83
+ return { h: '0', s: '0%', l: '12%' }; // near black, but not pure black
84
+ } else {
85
+ // light text for dark background: "off-white"
86
+ return { h: '0', s: '0%', l: '95%' }; // near white, but not pure white
87
+ }
88
+ }
89
+
90
+ export function formatHSLString(hsl: HSL) {
91
+ return `${hsl.h}, ${hsl.s}, ${hsl.l}`;
92
+ }