@titan-design/react-ui 0.1.0 → 0.2.5

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 (236) hide show
  1. package/README.md +47 -1
  2. package/dist/{chunk-MGW37MPO.mjs → chunk-UXVRPOME.mjs} +173 -3
  3. package/dist/chunk-UXVRPOME.mjs.map +1 -0
  4. package/dist/index.d.mts +101 -12
  5. package/dist/index.d.ts +101 -12
  6. package/dist/index.js +1267 -468
  7. package/dist/index.js.map +1 -1
  8. package/dist/index.mjs +734 -112
  9. package/dist/index.mjs.map +1 -1
  10. package/dist/theme/index.d.mts +86 -1
  11. package/dist/theme/index.d.ts +86 -1
  12. package/dist/theme/index.js +166 -0
  13. package/dist/theme/index.js.map +1 -1
  14. package/dist/theme/index.mjs +1 -1
  15. package/package.json +22 -6
  16. package/src/components/custom/DateTime/DateTime.stories.tsx +180 -0
  17. package/src/components/custom/DateTime/DateTime.test.tsx +152 -0
  18. package/src/components/custom/DateTime/DateTime.tsx +235 -0
  19. package/src/components/custom/DateTime/index.ts +2 -0
  20. package/src/components/custom/EmptyState/EmptyState.stories.tsx +99 -0
  21. package/src/components/custom/EmptyState/EmptyState.test.tsx +106 -0
  22. package/src/components/custom/EmptyState/EmptyState.tsx +68 -0
  23. package/src/components/custom/EmptyState/index.ts +2 -0
  24. package/src/components/custom/Metric/Metric.stories.tsx +133 -0
  25. package/src/components/custom/Metric/Metric.test.tsx +120 -0
  26. package/src/components/custom/Metric/Metric.tsx +92 -0
  27. package/src/components/custom/Metric/index.ts +2 -0
  28. package/src/components/custom/Sidebar/Sidebar.stories.tsx +227 -0
  29. package/src/components/custom/Sidebar/Sidebar.test.tsx +185 -0
  30. package/src/components/custom/Sidebar/Sidebar.tsx +257 -0
  31. package/src/components/custom/Sidebar/index.ts +18 -0
  32. package/src/components/custom/Table/Table.stories.tsx +481 -0
  33. package/src/components/custom/Table/Table.test.tsx +531 -0
  34. package/src/components/custom/Table/Table.tsx +696 -0
  35. package/src/components/custom/Table/index.ts +22 -0
  36. package/src/components/custom/Typography/Typography.stories.tsx +153 -0
  37. package/src/components/custom/Typography/Typography.test.tsx +107 -0
  38. package/src/components/custom/Typography/Typography.tsx +218 -0
  39. package/src/components/custom/Typography/index.ts +19 -0
  40. package/src/components/custom/Workout/RestTimer.stories.tsx +71 -0
  41. package/src/components/custom/Workout/RestTimer.test.tsx +145 -0
  42. package/src/components/custom/Workout/RestTimer.tsx +172 -0
  43. package/src/components/custom/Workout/SupersetWrapper.stories.tsx +144 -0
  44. package/src/components/custom/Workout/SupersetWrapper.test.tsx +130 -0
  45. package/src/components/custom/Workout/SupersetWrapper.tsx +63 -0
  46. package/src/components/custom/Workout/index.ts +2 -0
  47. package/src/components/custom/index.ts +9 -0
  48. package/src/components/custom/stepper/Stepper.stories.tsx +245 -0
  49. package/src/components/custom/stepper/Stepper.test.tsx +230 -0
  50. package/src/components/custom/stepper/Stepper.tsx +250 -0
  51. package/src/components/custom/stepper/index.ts +10 -0
  52. package/src/components/index.ts +5 -0
  53. package/src/components/ui/alert/Alert.stories.tsx +154 -0
  54. package/src/components/ui/alert/Alert.test.tsx +202 -0
  55. package/src/components/ui/alert/Alert.tsx +170 -0
  56. package/src/components/ui/alert/index.ts +8 -0
  57. package/src/components/ui/autocomplete/Autocomplete.stories.tsx +299 -0
  58. package/src/components/ui/autocomplete/Autocomplete.test.tsx +247 -0
  59. package/src/components/ui/autocomplete/Autocomplete.tsx +332 -0
  60. package/src/components/ui/autocomplete/index.ts +2 -0
  61. package/src/components/ui/avatar/Avatar.stories.tsx +105 -0
  62. package/src/components/ui/avatar/Avatar.test.tsx +152 -0
  63. package/src/components/ui/avatar/Avatar.tsx +176 -0
  64. package/src/components/ui/avatar/index.ts +2 -0
  65. package/src/components/ui/badge/Badge.stories.tsx +100 -0
  66. package/src/components/ui/badge/Badge.test.tsx +103 -0
  67. package/src/components/ui/badge/Badge.tsx +119 -0
  68. package/src/components/ui/badge/index.ts +3 -0
  69. package/src/components/ui/breadcrumbs/Breadcrumbs.stories.tsx +70 -0
  70. package/src/components/ui/breadcrumbs/Breadcrumbs.test.tsx +156 -0
  71. package/src/components/ui/breadcrumbs/Breadcrumbs.tsx +127 -0
  72. package/src/components/ui/breadcrumbs/index.ts +2 -0
  73. package/src/components/ui/button/Button.stories.tsx +216 -0
  74. package/src/components/ui/button/Button.test.tsx +132 -0
  75. package/src/components/ui/button/Button.tsx +297 -0
  76. package/src/components/ui/button/index.ts +10 -0
  77. package/src/components/ui/card/Card.stories.tsx +443 -0
  78. package/src/components/ui/card/Card.test.tsx +294 -0
  79. package/src/components/ui/card/Card.tsx +401 -0
  80. package/src/components/ui/card/index.ts +20 -0
  81. package/src/components/ui/checkbox/Checkbox.stories.tsx +126 -0
  82. package/src/components/ui/checkbox/Checkbox.test.tsx +152 -0
  83. package/src/components/ui/checkbox/Checkbox.tsx +163 -0
  84. package/src/components/ui/checkbox/index.ts +2 -0
  85. package/src/components/ui/chip/Chip.stories.tsx +112 -0
  86. package/src/components/ui/chip/Chip.test.tsx +119 -0
  87. package/src/components/ui/chip/Chip.tsx +151 -0
  88. package/src/components/ui/chip/index.ts +2 -0
  89. package/src/components/ui/collapse/Collapse.stories.tsx +313 -0
  90. package/src/components/ui/collapse/Collapse.test.tsx +298 -0
  91. package/src/components/ui/collapse/Collapse.tsx +275 -0
  92. package/src/components/ui/collapse/index.ts +18 -0
  93. package/src/components/ui/data-row/DataRow.stories.tsx +78 -0
  94. package/src/components/ui/data-row/DataRow.test.tsx +78 -0
  95. package/src/components/ui/data-row/DataRow.tsx +29 -0
  96. package/src/components/ui/data-row/index.ts +2 -0
  97. package/src/components/ui/divider/Divider.stories.tsx +71 -0
  98. package/src/components/ui/divider/Divider.test.tsx +55 -0
  99. package/src/components/ui/divider/Divider.tsx +37 -0
  100. package/src/components/ui/divider/index.ts +2 -0
  101. package/src/components/ui/drawer/Drawer.stories.tsx +228 -0
  102. package/src/components/ui/drawer/Drawer.test.tsx +142 -0
  103. package/src/components/ui/drawer/Drawer.tsx +228 -0
  104. package/src/components/ui/drawer/index.ts +9 -0
  105. package/src/components/ui/form-field/FormField.stories.tsx +260 -0
  106. package/src/components/ui/form-field/FormField.test.tsx +280 -0
  107. package/src/components/ui/form-field/FormField.tsx +308 -0
  108. package/src/components/ui/form-field/index.ts +2 -0
  109. package/src/components/ui/help-tip/HelpTip.stories.tsx +250 -0
  110. package/src/components/ui/help-tip/HelpTip.test.tsx +198 -0
  111. package/src/components/ui/help-tip/HelpTip.tsx +229 -0
  112. package/src/components/ui/help-tip/index.ts +2 -0
  113. package/src/components/ui/icon-box/IconBox.stories.tsx +139 -0
  114. package/src/components/ui/icon-box/IconBox.test.tsx +120 -0
  115. package/src/components/ui/icon-box/IconBox.tsx +57 -0
  116. package/src/components/ui/icon-box/index.ts +1 -0
  117. package/src/components/ui/index.ts +38 -0
  118. package/src/components/ui/indicator/Indicator.stories.tsx +74 -0
  119. package/src/components/ui/indicator/Indicator.test.tsx +55 -0
  120. package/src/components/ui/indicator/Indicator.tsx +73 -0
  121. package/src/components/ui/indicator/index.ts +2 -0
  122. package/src/components/ui/input/Input.stories.tsx +192 -0
  123. package/src/components/ui/input/Input.test.tsx +86 -0
  124. package/src/components/ui/input/Input.tsx +273 -0
  125. package/src/components/ui/input/index.ts +2 -0
  126. package/src/components/ui/link/Link.stories.tsx +78 -0
  127. package/src/components/ui/link/Link.test.tsx +94 -0
  128. package/src/components/ui/link/Link.tsx +86 -0
  129. package/src/components/ui/link/index.ts +2 -0
  130. package/src/components/ui/list-item/ListItem.stories.tsx +146 -0
  131. package/src/components/ui/list-item/ListItem.test.tsx +128 -0
  132. package/src/components/ui/list-item/ListItem.tsx +89 -0
  133. package/src/components/ui/list-item/index.ts +14 -0
  134. package/src/components/ui/menu/Menu.stories.tsx +180 -0
  135. package/src/components/ui/menu/Menu.test.tsx +353 -0
  136. package/src/components/ui/menu/Menu.tsx +222 -0
  137. package/src/components/ui/menu/index.ts +2 -0
  138. package/src/components/ui/modal/Modal.stories.tsx +350 -0
  139. package/src/components/ui/modal/Modal.test.tsx +159 -0
  140. package/src/components/ui/modal/Modal.tsx +254 -0
  141. package/src/components/ui/modal/index.ts +19 -0
  142. package/src/components/ui/pill/Pill.stories.tsx +71 -0
  143. package/src/components/ui/pill/Pill.test.tsx +69 -0
  144. package/src/components/ui/pill/Pill.tsx +104 -0
  145. package/src/components/ui/pill/index.ts +2 -0
  146. package/src/components/ui/popover/Popover.stories.tsx +190 -0
  147. package/src/components/ui/popover/Popover.test.tsx +419 -0
  148. package/src/components/ui/popover/Popover.tsx +237 -0
  149. package/src/components/ui/popover/index.ts +2 -0
  150. package/src/components/ui/progress/Progress.stories.tsx +190 -0
  151. package/src/components/ui/progress/Progress.test.tsx +210 -0
  152. package/src/components/ui/progress/Progress.tsx +339 -0
  153. package/src/components/ui/progress/index.ts +9 -0
  154. package/src/components/ui/radio/Radio.stories.tsx +199 -0
  155. package/src/components/ui/radio/Radio.test.tsx +166 -0
  156. package/src/components/ui/radio/Radio.tsx +200 -0
  157. package/src/components/ui/radio/index.ts +2 -0
  158. package/src/components/ui/section/Section.stories.tsx +111 -0
  159. package/src/components/ui/section/Section.test.tsx +121 -0
  160. package/src/components/ui/section/Section.tsx +52 -0
  161. package/src/components/ui/section/index.ts +6 -0
  162. package/src/components/ui/select/Select.stories.tsx +286 -0
  163. package/src/components/ui/select/Select.test.tsx +200 -0
  164. package/src/components/ui/select/Select.tsx +243 -0
  165. package/src/components/ui/select/index.ts +2 -0
  166. package/src/components/ui/skeleton/Skeleton.stories.tsx +153 -0
  167. package/src/components/ui/skeleton/Skeleton.test.tsx +162 -0
  168. package/src/components/ui/skeleton/Skeleton.tsx +229 -0
  169. package/src/components/ui/skeleton/index.ts +16 -0
  170. package/src/components/ui/spinner/Spinner.stories.tsx +67 -0
  171. package/src/components/ui/spinner/Spinner.test.tsx +67 -0
  172. package/src/components/ui/spinner/Spinner.tsx +72 -0
  173. package/src/components/ui/spinner/index.ts +2 -0
  174. package/src/components/ui/stack/Stack.stories.tsx +127 -0
  175. package/src/components/ui/stack/Stack.test.tsx +184 -0
  176. package/src/components/ui/stack/Stack.tsx +76 -0
  177. package/src/components/ui/stack/index.ts +2 -0
  178. package/src/components/ui/surface/Surface.stories.tsx +91 -0
  179. package/src/components/ui/surface/Surface.test.tsx +63 -0
  180. package/src/components/ui/surface/Surface.tsx +51 -0
  181. package/src/components/ui/surface/index.ts +1 -0
  182. package/src/components/ui/switch/Switch.stories.tsx +111 -0
  183. package/src/components/ui/switch/Switch.test.tsx +112 -0
  184. package/src/components/ui/switch/Switch.tsx +103 -0
  185. package/src/components/ui/switch/index.ts +2 -0
  186. package/src/components/ui/tabs/Tabs.stories.tsx +213 -0
  187. package/src/components/ui/tabs/Tabs.test.tsx +192 -0
  188. package/src/components/ui/tabs/Tabs.tsx +253 -0
  189. package/src/components/ui/tabs/index.ts +16 -0
  190. package/src/components/ui/toast/Toast.stories.tsx +255 -0
  191. package/src/components/ui/toast/Toast.test.tsx +207 -0
  192. package/src/components/ui/toast/Toast.tsx +327 -0
  193. package/src/components/ui/toast/index.ts +9 -0
  194. package/src/components/ui/toolbar-button/ToolbarButton.stories.tsx +415 -0
  195. package/src/components/ui/toolbar-button/ToolbarButton.test.tsx +224 -0
  196. package/src/components/ui/toolbar-button/ToolbarButton.tsx +307 -0
  197. package/src/components/ui/toolbar-button/index.ts +7 -0
  198. package/src/components/ui/tooltip/Tooltip.stories.tsx +126 -0
  199. package/src/components/ui/tooltip/Tooltip.test.tsx +262 -0
  200. package/src/components/ui/tooltip/Tooltip.tsx +234 -0
  201. package/src/components/ui/tooltip/index.ts +2 -0
  202. package/src/index.ts +10 -0
  203. package/src/stories/PresetShowcase.stories.tsx +462 -0
  204. package/src/stories/ThemePresets.stories.tsx +85 -0
  205. package/src/test/setup.ts +19 -0
  206. package/src/theme/Colors.stories.tsx +258 -0
  207. package/src/theme/config.ts +193 -0
  208. package/src/theme/elevation.stories.tsx +346 -0
  209. package/src/theme/elevation.test.ts +27 -0
  210. package/src/theme/elevation.ts +567 -0
  211. package/src/theme/global.css +129 -1
  212. package/src/theme/index.ts +40 -0
  213. package/src/theme/presets/apply.ts +97 -0
  214. package/src/theme/presets/audiobook.ts +93 -0
  215. package/src/theme/presets/default.ts +6 -0
  216. package/src/theme/presets/index.ts +4 -0
  217. package/src/theme/presets/presets.test.ts +51 -0
  218. package/src/theme/presets/types.ts +76 -0
  219. package/src/theme/shadows.stories.tsx +523 -0
  220. package/src/theme/shadows.ts +439 -0
  221. package/src/theme/tokens/index.ts +2 -0
  222. package/src/theme/tokens/primitives.ts +354 -0
  223. package/src/theme/tokens/semantic.ts +381 -0
  224. package/src/types/jest-axe.d.ts +16 -0
  225. package/src/types/nativewind.d.ts +54 -0
  226. package/src/utils/avatar-color.test.ts +30 -0
  227. package/src/utils/avatar-color.ts +20 -0
  228. package/src/utils/cn.ts +14 -0
  229. package/src/utils/colors.ts +140 -0
  230. package/src/utils/form.ts +188 -0
  231. package/src/utils/index.ts +29 -0
  232. package/src/utils/useTheme.ts +72 -0
  233. package/src/web-jsx/jsx-dev-runtime.ts +28 -0
  234. package/src/web-jsx/jsx-runtime.ts +41 -0
  235. package/tailwind.config.js +53 -3
  236. package/dist/chunk-MGW37MPO.mjs.map +0 -1
@@ -116,6 +116,29 @@
116
116
 
117
117
  --color-avatar-background: #4B5563;
118
118
  --color-avatar-text: #FFFFFF;
119
+
120
+ /* RGB decomposed values for glow shadows */
121
+ --color-brand-primary-rgb: 255, 121, 0;
122
+ --color-brand-secondary-rgb: 64, 109, 135;
123
+ --color-status-success-rgb: 20, 184, 166;
124
+ --color-status-error-rgb: 209, 67, 67;
125
+ --color-status-warning-rgb: 255, 176, 32;
126
+ --color-status-info-rgb: 33, 150, 243;
127
+ --color-background-base-rgb: 16, 16, 16;
128
+
129
+ /* Font families */
130
+ --font-family-sans: 'Inter';
131
+ --font-family-body: 'Nunito Sans';
132
+ --font-family-heading: 'Space Grotesk';
133
+ --font-family-mono: 'SFMono-Regular', Consolas, 'Liberation Mono', Menlo, monospace;
134
+
135
+ /* Animation tokens */
136
+ --ease-out: cubic-bezier(0.22, 1, 0.36, 1);
137
+ --ease-in-out: cubic-bezier(0.65, 0, 0.35, 1);
138
+ --ease-spring: cubic-bezier(0.34, 1.56, 0.64, 1);
139
+ --duration-fast: 150ms;
140
+ --duration-normal: 250ms;
141
+ --duration-slow: 400ms;
119
142
  }
120
143
 
121
144
  /* Light mode */
@@ -222,11 +245,34 @@
222
245
 
223
246
  --color-avatar-background: #6B7280;
224
247
  --color-avatar-text: #FFFFFF;
248
+
249
+ /* RGB decomposed values for glow shadows */
250
+ --color-brand-primary-rgb: 255, 121, 0;
251
+ --color-brand-secondary-rgb: 64, 109, 135;
252
+ --color-status-success-rgb: 20, 184, 166;
253
+ --color-status-error-rgb: 209, 67, 67;
254
+ --color-status-warning-rgb: 255, 176, 32;
255
+ --color-status-info-rgb: 33, 150, 243;
256
+ --color-background-base-rgb: 16, 16, 16;
257
+
258
+ /* Font families */
259
+ --font-family-sans: 'Inter';
260
+ --font-family-body: 'Nunito Sans';
261
+ --font-family-heading: 'Space Grotesk';
262
+ --font-family-mono: 'SFMono-Regular', Consolas, 'Liberation Mono', Menlo, monospace;
263
+
264
+ /* Animation tokens */
265
+ --ease-out: cubic-bezier(0.22, 1, 0.36, 1);
266
+ --ease-in-out: cubic-bezier(0.65, 0, 0.35, 1);
267
+ --ease-spring: cubic-bezier(0.34, 1.56, 0.64, 1);
268
+ --duration-fast: 150ms;
269
+ --duration-normal: 250ms;
270
+ --duration-slow: 400ms;
225
271
  }
226
272
 
227
273
  /* Base styles */
228
274
  html {
229
- font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Helvetica, Arial, sans-serif;
275
+ font-family: var(--font-family-sans), -apple-system, BlinkMacSystemFont, 'Segoe UI', Helvetica, Arial, sans-serif;
230
276
  -webkit-font-smoothing: antialiased;
231
277
  -moz-osx-font-smoothing: grayscale;
232
278
  }
@@ -248,3 +294,85 @@
248
294
  color: var(--color-text-primary);
249
295
  }
250
296
  }
297
+
298
+ @layer utilities {
299
+ /* Opt-in styled scrollbar — apply to scrollable containers */
300
+ .scrollbar-styled::-webkit-scrollbar {
301
+ width: 8px;
302
+ height: 8px;
303
+ }
304
+
305
+ .scrollbar-styled::-webkit-scrollbar-track {
306
+ background: transparent;
307
+ }
308
+
309
+ .scrollbar-styled::-webkit-scrollbar-thumb {
310
+ background: var(--color-border-strong);
311
+ border-radius: 4px;
312
+ }
313
+
314
+ .scrollbar-styled::-webkit-scrollbar-thumb:hover {
315
+ background: var(--color-text-tertiary);
316
+ }
317
+
318
+ /* Thin variant */
319
+ .scrollbar-thin::-webkit-scrollbar {
320
+ width: 4px;
321
+ height: 4px;
322
+ }
323
+
324
+ .scrollbar-thin::-webkit-scrollbar-track {
325
+ background: transparent;
326
+ }
327
+
328
+ .scrollbar-thin::-webkit-scrollbar-thumb {
329
+ background: var(--color-border-strong);
330
+ border-radius: 2px;
331
+ }
332
+
333
+ .scrollbar-thin::-webkit-scrollbar-thumb:hover {
334
+ background: var(--color-text-tertiary);
335
+ }
336
+
337
+ /* Hide scrollbar but keep scrollable */
338
+ .scrollbar-none::-webkit-scrollbar {
339
+ display: none;
340
+ }
341
+
342
+ .scrollbar-none {
343
+ -ms-overflow-style: none;
344
+ scrollbar-width: none;
345
+ }
346
+
347
+ /* Atmosphere: subtle radial gradient washes for ambient color */
348
+ .atmosphere-warm {
349
+ position: relative;
350
+ }
351
+
352
+ .atmosphere-warm::before {
353
+ content: '';
354
+ position: fixed;
355
+ inset: 0;
356
+ pointer-events: none;
357
+ z-index: 0;
358
+ background:
359
+ radial-gradient(ellipse 140% 100% at 15% 5%, rgba(var(--color-brand-primary-rgb, 255, 121, 0), 0.06) 0%, transparent 45%),
360
+ radial-gradient(ellipse 120% 80% at 85% 95%, rgba(var(--color-brand-secondary-rgb, 64, 109, 135), 0.04) 0%, transparent 45%);
361
+ }
362
+
363
+ /* Film grain overlay for vintage texture */
364
+ .grain {
365
+ position: relative;
366
+ }
367
+
368
+ .grain::after {
369
+ content: '';
370
+ position: fixed;
371
+ inset: 0;
372
+ pointer-events: none;
373
+ z-index: 1;
374
+ opacity: 0.015;
375
+ mix-blend-mode: overlay;
376
+ background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='300' height='300'%3E%3Cfilter id='noise'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.85' numOctaves='4' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23noise)'/%3E%3C/svg%3E");
377
+ }
378
+ }
@@ -0,0 +1,40 @@
1
+ // Theme exports
2
+ export * from './tokens'
3
+ export * from './config'
4
+ export * from './shadows'
5
+ export * from './elevation'
6
+
7
+ // Re-export commonly used items
8
+ export { getSemanticColors, type ThemeMode } from './tokens/semantic'
9
+ export { getThemeCSSVars, gluestackConfig } from './config'
10
+ export {
11
+ neumorphicShadows,
12
+ // Color math utilities (HSV-based)
13
+ lighten,
14
+ darken,
15
+ getHoverColors,
16
+ isDark,
17
+ hexToRgb,
18
+ rgbToHsv,
19
+ // Shadow creation functions
20
+ createRaisedShadow,
21
+ createRaisedHoverShadow,
22
+ createPressedShadow,
23
+ createPressedHoverShadow,
24
+ createFlatShadow,
25
+ shadowColors,
26
+ type ShadowIntensity,
27
+ type ShadowSurface,
28
+ } from './shadows'
29
+ export {
30
+ getElevationConfig,
31
+ getElevationSurface,
32
+ getElevationShadow,
33
+ getBaseSurfaceColor,
34
+ getValidatedElevation,
35
+ componentElevationRanges,
36
+ type ElevationLevel,
37
+ type ElevationConfig,
38
+ type ComponentType,
39
+ } from './elevation'
40
+ export * from './presets'
@@ -0,0 +1,97 @@
1
+ import type { ThemePreset } from './types'
2
+
3
+ function colorKeyToVar(key: string): string {
4
+ return `--color-${key}`
5
+ }
6
+
7
+ function applyColorOverrides(colors: Record<string, string | undefined>, root: HTMLElement) {
8
+ for (const [key, value] of Object.entries(colors)) {
9
+ if (value !== undefined) {
10
+ root.style.setProperty(colorKeyToVar(key), value)
11
+ }
12
+ }
13
+ }
14
+
15
+ function applyFontOverrides(fonts: Record<string, string | undefined>, root: HTMLElement) {
16
+ const fontVarMap: Record<string, string> = {
17
+ sans: '--font-family-sans',
18
+ body: '--font-family-body',
19
+ heading: '--font-family-heading',
20
+ mono: '--font-family-mono',
21
+ }
22
+ for (const [key, value] of Object.entries(fonts)) {
23
+ if (value !== undefined && fontVarMap[key]) {
24
+ root.style.setProperty(fontVarMap[key], value)
25
+ }
26
+ }
27
+ }
28
+
29
+ function injectFontImport(url: string) {
30
+ if (typeof document === 'undefined') return
31
+ const existing = document.querySelector(`link[href="${url}"]`)
32
+ if (existing) return
33
+ const link = document.createElement('link')
34
+ link.rel = 'stylesheet'
35
+ link.href = url
36
+ document.head.appendChild(link)
37
+ }
38
+
39
+ export function applyThemePreset(preset: ThemePreset): () => void {
40
+ if (typeof document === 'undefined') {
41
+ return () => {}
42
+ }
43
+
44
+ const root = document.documentElement
45
+ const removals: (() => void)[] = []
46
+
47
+ // Inject font import if specified
48
+ if (preset.fontImport) {
49
+ injectFontImport(preset.fontImport)
50
+ }
51
+
52
+ // Determine current theme mode
53
+ const isLight = root.classList.contains('light')
54
+ const modeColors = isLight ? preset.colors?.light : preset.colors?.dark
55
+
56
+ // Apply color overrides
57
+ if (modeColors) {
58
+ const previousValues: Record<string, string | null> = {}
59
+ for (const key of Object.keys(modeColors)) {
60
+ previousValues[colorKeyToVar(key)] = root.style.getPropertyValue(colorKeyToVar(key)) || null
61
+ }
62
+ applyColorOverrides(modeColors, root)
63
+ removals.push(() => {
64
+ for (const [varName, prev] of Object.entries(previousValues)) {
65
+ if (prev === null) {
66
+ root.style.removeProperty(varName)
67
+ } else {
68
+ root.style.setProperty(varName, prev)
69
+ }
70
+ }
71
+ })
72
+ }
73
+
74
+ // Apply font overrides
75
+ if (preset.fonts) {
76
+ applyFontOverrides(preset.fonts as Record<string, string | undefined>, root)
77
+ }
78
+
79
+ // Apply root classes
80
+ if (preset.rootClasses) {
81
+ for (const cls of preset.rootClasses) {
82
+ root.classList.add(cls)
83
+ }
84
+ removals.push(() => {
85
+ for (const cls of preset.rootClasses!) {
86
+ root.classList.remove(cls)
87
+ }
88
+ })
89
+ }
90
+
91
+ // Return cleanup function
92
+ return () => {
93
+ for (const remove of removals) {
94
+ remove()
95
+ }
96
+ }
97
+ }
@@ -0,0 +1,93 @@
1
+ import type { ThemePreset } from './types'
2
+
3
+ /**
4
+ * Audiobook Studio preset.
5
+ *
6
+ * Warm copper/patina aesthetic inspired by vintage audio equipment
7
+ * and reading lounges. Dark-mode only design with editorial typography.
8
+ */
9
+ export const audiobookPreset: ThemePreset = {
10
+ name: 'audiobook',
11
+ description: 'Warm copper/patina theme with editorial typography',
12
+ fontImport:
13
+ "https://fonts.googleapis.com/css2?family=Newsreader:ital,wght@0,400;0,600;0,700;1,400&family=Outfit:wght@400;500;600;700&family=JetBrains+Mono:wght@400;500&display=swap",
14
+ fonts: {
15
+ sans: "'Outfit', 'Avenir', 'Avenir Next', system-ui, sans-serif",
16
+ body: "'Outfit', 'Avenir', 'Avenir Next', system-ui, sans-serif",
17
+ heading: "'Newsreader', 'Libre Baskerville', Georgia, serif",
18
+ mono: "'JetBrains Mono', 'SF Mono', 'Fira Code', monospace",
19
+ },
20
+ colors: {
21
+ dark: {
22
+ // Brand: Copper accent
23
+ 'brand-primary': '#d4782a',
24
+ 'brand-primary-light': '#e8944a',
25
+ 'brand-primary-dark': '#9a4a15',
26
+ 'brand-primary-subtle': 'rgba(212, 120, 42, 0.12)',
27
+ 'brand-primary-hover': '#e8944a',
28
+ 'brand-primary-active': '#f5b070',
29
+
30
+ // Brand secondary: Patina/verdigris
31
+ 'brand-secondary': '#3a9b8a',
32
+ 'brand-secondary-light': '#55baa7',
33
+ 'brand-secondary-dark': '#1f5c52',
34
+ 'brand-secondary-subtle': 'rgba(58, 155, 138, 0.12)',
35
+ 'brand-secondary-hover': '#55baa7',
36
+ 'brand-secondary-active': '#7ed4c4',
37
+
38
+ 'on-brand-primary': '#FFFFFF',
39
+ 'on-brand-secondary': '#FFFFFF',
40
+
41
+ // Status colors (audiobook's warmer status palette)
42
+ 'status-success': '#5fb870',
43
+ 'status-error': '#d85c4a',
44
+ 'status-warning': '#d4a43a',
45
+ 'status-info': '#5a8fd4',
46
+
47
+ // Text: warm off-whites
48
+ 'text-primary': '#f4f3f1',
49
+ 'text-secondary': '#b5b3af',
50
+ 'text-tertiary': '#7a7875',
51
+
52
+ // Surfaces: warm charcoals (not pure gray)
53
+ 'surface-base': '#101114',
54
+ 'surface-elevated': '#15171c',
55
+ 'surface-raised': '#1a1d24',
56
+ 'surface-overlay': '#15171c',
57
+ 'surface-input': '#15171c',
58
+
59
+ // Backgrounds
60
+ 'background-base': '#0a0b0d',
61
+ 'background-default': '#101114',
62
+ 'background-subtle': '#1a1d24',
63
+
64
+ // Borders
65
+ 'border-default': 'rgba(255, 255, 255, 0.06)',
66
+ 'border-subtle': 'rgba(255, 255, 255, 0.03)',
67
+ 'border-strong': '#2d323d',
68
+ 'border-focus': '#d4782a',
69
+ 'border-input': '#2d323d',
70
+ 'border-input-hover': '#454c5c',
71
+ 'border-input-focus': '#d4782a',
72
+ 'border-input-error': '#d85c4a',
73
+
74
+ // Interactive
75
+ 'interactive-hover': 'rgba(255, 255, 255, 0.04)',
76
+ 'interactive-focus': 'rgba(255, 255, 255, 0.12)',
77
+ 'interactive-active': 'rgba(255, 255, 255, 0.16)',
78
+ 'interactive-selected': 'rgba(255, 255, 255, 0.08)',
79
+
80
+ 'divider': 'rgba(255, 255, 255, 0.06)',
81
+
82
+ // Glow RGB values for shadow utilities
83
+ 'brand-primary-rgb': '212, 120, 42',
84
+ 'brand-secondary-rgb': '58, 155, 138',
85
+ 'status-success-rgb': '95, 184, 112',
86
+ 'status-error-rgb': '216, 92, 74',
87
+ 'status-warning-rgb': '212, 164, 58',
88
+ 'status-info-rgb': '90, 143, 212',
89
+ 'background-base-rgb': '10, 11, 13',
90
+ },
91
+ },
92
+ rootClasses: ['atmosphere-warm', 'grain'],
93
+ }
@@ -0,0 +1,6 @@
1
+ import type { ThemePreset } from './types'
2
+
3
+ export const defaultPreset: ThemePreset = {
4
+ name: 'default',
5
+ description: 'Titan Design System default theme — orange accent, steel secondary',
6
+ }
@@ -0,0 +1,4 @@
1
+ export type { ThemePreset, ThemePresetColors, ThemePresetFonts, ThemePresetRadii, ThemePresetShadows } from './types'
2
+ export { applyThemePreset } from './apply'
3
+ export { defaultPreset } from './default'
4
+ export { audiobookPreset } from './audiobook'
@@ -0,0 +1,51 @@
1
+ import { describe, it, expect, afterEach } from 'vitest'
2
+ import { applyThemePreset } from './apply'
3
+ import type { ThemePreset } from './types'
4
+
5
+ describe('applyThemePreset', () => {
6
+ let cleanup: () => void
7
+
8
+ afterEach(() => {
9
+ cleanup?.()
10
+ })
11
+
12
+ it('applies color overrides to document root', () => {
13
+ const preset: ThemePreset = {
14
+ name: 'test',
15
+ colors: {
16
+ dark: {
17
+ 'brand-primary': '#FF0000',
18
+ },
19
+ },
20
+ }
21
+ cleanup = applyThemePreset(preset)
22
+ const value = document.documentElement.style.getPropertyValue('--color-brand-primary')
23
+ expect(value).toBe('#FF0000')
24
+ })
25
+
26
+ it('returns cleanup function that removes overrides', () => {
27
+ const preset: ThemePreset = {
28
+ name: 'test',
29
+ colors: {
30
+ dark: {
31
+ 'brand-primary': '#FF0000',
32
+ },
33
+ },
34
+ }
35
+ cleanup = applyThemePreset(preset)
36
+ cleanup()
37
+ const value = document.documentElement.style.getPropertyValue('--color-brand-primary')
38
+ expect(value).toBe('')
39
+ })
40
+
41
+ it('adds root classes', () => {
42
+ const preset: ThemePreset = {
43
+ name: 'test',
44
+ rootClasses: ['atmosphere-warm'],
45
+ }
46
+ cleanup = applyThemePreset(preset)
47
+ expect(document.documentElement.classList.contains('atmosphere-warm')).toBe(true)
48
+ cleanup()
49
+ expect(document.documentElement.classList.contains('atmosphere-warm')).toBe(false)
50
+ })
51
+ })
@@ -0,0 +1,76 @@
1
+ export interface ThemePresetColors {
2
+ 'brand-primary'?: string
3
+ 'brand-primary-light'?: string
4
+ 'brand-primary-dark'?: string
5
+ 'brand-primary-subtle'?: string
6
+ 'brand-primary-hover'?: string
7
+ 'brand-primary-active'?: string
8
+ 'brand-secondary'?: string
9
+ 'brand-secondary-light'?: string
10
+ 'brand-secondary-dark'?: string
11
+ 'brand-secondary-subtle'?: string
12
+ 'brand-secondary-hover'?: string
13
+ 'brand-secondary-active'?: string
14
+ 'on-brand-primary'?: string
15
+ 'on-brand-secondary'?: string
16
+ 'status-success'?: string
17
+ 'status-error'?: string
18
+ 'status-warning'?: string
19
+ 'status-info'?: string
20
+ 'text-primary'?: string
21
+ 'text-secondary'?: string
22
+ 'text-tertiary'?: string
23
+ 'surface-base'?: string
24
+ 'surface-elevated'?: string
25
+ 'surface-raised'?: string
26
+ 'surface-overlay'?: string
27
+ 'surface-input'?: string
28
+ 'background-base'?: string
29
+ 'background-default'?: string
30
+ 'background-subtle'?: string
31
+ 'border-default'?: string
32
+ 'border-subtle'?: string
33
+ 'border-strong'?: string
34
+ 'border-focus'?: string
35
+ 'divider'?: string
36
+ [key: `${string}`]: string | undefined
37
+ }
38
+
39
+ export interface ThemePresetFonts {
40
+ sans?: string
41
+ body?: string
42
+ heading?: string
43
+ mono?: string
44
+ }
45
+
46
+ export interface ThemePresetRadii {
47
+ sm?: string
48
+ DEFAULT?: string
49
+ md?: string
50
+ lg?: string
51
+ xl?: string
52
+ }
53
+
54
+ export interface ThemePresetShadows {
55
+ sm?: string
56
+ DEFAULT?: string
57
+ md?: string
58
+ lg?: string
59
+ xl?: string
60
+ }
61
+
62
+ export interface ThemePreset {
63
+ name: string
64
+ description?: string
65
+ colors?: {
66
+ dark?: ThemePresetColors
67
+ light?: ThemePresetColors
68
+ }
69
+ fonts?: ThemePresetFonts
70
+ radii?: ThemePresetRadii
71
+ shadows?: ThemePresetShadows
72
+ /** Additional CSS classes to add to root element */
73
+ rootClasses?: string[]
74
+ /** Google Fonts import URL (appended to existing imports) */
75
+ fontImport?: string
76
+ }