@umbra.ui/core 0.1.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 (272) hide show
  1. package/dist/components/controls/Dropdown/types.d.ts +5 -0
  2. package/dist/components/controls/Dropdown/types.d.ts.map +1 -0
  3. package/dist/components/controls/Dropdown/types.js +1 -0
  4. package/dist/components/controls/SegmentedControl/types.d.ts +6 -0
  5. package/dist/components/controls/SegmentedControl/types.d.ts.map +1 -0
  6. package/dist/components/controls/SegmentedControl/types.js +1 -0
  7. package/dist/components/dialogs/Alert/types.d.ts +7 -0
  8. package/dist/components/dialogs/Alert/types.d.ts.map +1 -0
  9. package/dist/components/dialogs/Alert/types.js +1 -0
  10. package/dist/components/dialogs/Toast/types.d.ts +34 -0
  11. package/dist/components/dialogs/Toast/types.d.ts.map +1 -0
  12. package/dist/components/dialogs/Toast/types.js +10 -0
  13. package/dist/components/dialogs/Toast/useToast.d.ts +36 -0
  14. package/dist/components/dialogs/Toast/useToast.d.ts.map +1 -0
  15. package/dist/components/dialogs/Toast/useToast.js +90 -0
  16. package/dist/components/indicators/Tooltip/tooltip.d.ts +3 -0
  17. package/dist/components/indicators/Tooltip/tooltip.d.ts.map +1 -0
  18. package/dist/components/indicators/Tooltip/tooltip.js +33 -0
  19. package/dist/components/indicators/Tooltip/types.d.ts +14 -0
  20. package/dist/components/indicators/Tooltip/types.d.ts.map +1 -0
  21. package/dist/components/indicators/Tooltip/types.js +1 -0
  22. package/dist/components/indicators/Tooltip/useTooltip.d.ts +18 -0
  23. package/dist/components/indicators/Tooltip/useTooltip.d.ts.map +1 -0
  24. package/dist/components/indicators/Tooltip/useTooltip.js +57 -0
  25. package/dist/components/inputs/Tags/tag-bar-styles.d.ts +14 -0
  26. package/dist/components/inputs/Tags/tag-bar-styles.d.ts.map +1 -0
  27. package/dist/components/inputs/Tags/tag-bar-styles.js +313 -0
  28. package/dist/components/inputs/Tags/types.d.ts +93 -0
  29. package/dist/components/inputs/Tags/types.d.ts.map +1 -0
  30. package/dist/components/inputs/Tags/types.js +216 -0
  31. package/dist/components/inputs/search/types.d.ts +9 -0
  32. package/dist/components/inputs/search/types.d.ts.map +1 -0
  33. package/dist/components/inputs/search/types.js +1 -0
  34. package/dist/components/navigation/adaptive/types.d.ts +16 -0
  35. package/dist/components/navigation/adaptive/types.d.ts.map +1 -0
  36. package/dist/components/navigation/adaptive/types.js +1 -0
  37. package/dist/components/navigation/adaptive/useAdaptiveLayout.d.ts +27 -0
  38. package/dist/components/navigation/adaptive/useAdaptiveLayout.d.ts.map +1 -0
  39. package/dist/components/navigation/adaptive/useAdaptiveLayout.js +40 -0
  40. package/dist/components/navigation/adaptive/useBreakpoints.d.ts +6 -0
  41. package/dist/components/navigation/adaptive/useBreakpoints.d.ts.map +1 -0
  42. package/dist/components/navigation/adaptive/useBreakpoints.js +37 -0
  43. package/dist/components/navigation/adaptive/useContainerMonitor.d.ts +93 -0
  44. package/dist/components/navigation/adaptive/useContainerMonitor.d.ts.map +1 -0
  45. package/dist/components/navigation/adaptive/useContainerMonitor.js +145 -0
  46. package/dist/components/navigation/adaptive/useViewAnimation.d.ts +31 -0
  47. package/dist/components/navigation/adaptive/useViewAnimation.d.ts.map +1 -0
  48. package/dist/components/navigation/adaptive/useViewAnimation.js +591 -0
  49. package/dist/components/navigation/adaptive/useViewResize.d.ts +52 -0
  50. package/dist/components/navigation/adaptive/useViewResize.d.ts.map +1 -0
  51. package/dist/components/navigation/adaptive/useViewResize.js +146 -0
  52. package/dist/components/navigation/navstack/useNavigationStack.d.ts +25 -0
  53. package/dist/components/navigation/navstack/useNavigationStack.d.ts.map +1 -0
  54. package/dist/components/navigation/navstack/useNavigationStack.js +133 -0
  55. package/dist/components/navigation/slideover/useSlideoverController.d.ts +20 -0
  56. package/dist/components/navigation/slideover/useSlideoverController.d.ts.map +1 -0
  57. package/dist/components/navigation/slideover/useSlideoverController.js +267 -0
  58. package/dist/components/navigation/splitview/useSplitViewController.d.ts +20 -0
  59. package/dist/components/navigation/splitview/useSplitViewController.d.ts.map +1 -0
  60. package/dist/components/navigation/splitview/useSplitViewController.js +325 -0
  61. package/dist/components/navigation/tabcontroller/types.d.ts +21 -0
  62. package/dist/components/navigation/tabcontroller/types.d.ts.map +1 -0
  63. package/dist/components/navigation/tabcontroller/types.js +1 -0
  64. package/dist/components/navigation/tabcontroller/useTabController.d.ts +5 -0
  65. package/dist/components/navigation/tabcontroller/useTabController.d.ts.map +1 -0
  66. package/dist/components/navigation/tabcontroller/useTabController.js +10 -0
  67. package/dist/components/navigation/types.d.ts +8 -0
  68. package/dist/components/navigation/types.d.ts.map +1 -0
  69. package/dist/components/navigation/types.js +1 -0
  70. package/dist/components/pickers/CollectionPicker/types.d.ts +11 -0
  71. package/dist/components/pickers/CollectionPicker/types.d.ts.map +1 -0
  72. package/dist/components/pickers/CollectionPicker/types.js +1 -0
  73. package/dist/components/pickers/ColorPicker/colors.d.ts +13 -0
  74. package/dist/components/pickers/ColorPicker/colors.d.ts.map +1 -0
  75. package/dist/components/pickers/ColorPicker/colors.js +266 -0
  76. package/dist/components/pickers/FilePicker/types.d.ts +10 -0
  77. package/dist/components/pickers/FilePicker/types.d.ts.map +1 -0
  78. package/dist/components/pickers/FilePicker/types.js +1 -0
  79. package/dist/index.d.ts +91 -0
  80. package/dist/index.d.ts.map +1 -0
  81. package/dist/index.js +196 -0
  82. package/dist/theme.d.ts +73 -0
  83. package/dist/theme.d.ts.map +1 -0
  84. package/dist/theme.js +279 -0
  85. package/dist/themes/blank.d.ts +7 -0
  86. package/dist/themes/blank.d.ts.map +1 -0
  87. package/dist/themes/blank.js +543 -0
  88. package/dist/themes/crimson-dark.d.ts +4 -0
  89. package/dist/themes/crimson-dark.d.ts.map +1 -0
  90. package/dist/themes/crimson-dark.js +552 -0
  91. package/dist/themes/cyan-light.d.ts +4 -0
  92. package/dist/themes/cyan-light.d.ts.map +1 -0
  93. package/dist/themes/cyan-light.js +552 -0
  94. package/dist/themes/dark.d.ts +4 -0
  95. package/dist/themes/dark.d.ts.map +1 -0
  96. package/dist/themes/dark.js +551 -0
  97. package/dist/themes/gold-dark.d.ts +4 -0
  98. package/dist/themes/gold-dark.d.ts.map +1 -0
  99. package/dist/themes/gold-dark.js +552 -0
  100. package/dist/themes/grass-dark.d.ts +4 -0
  101. package/dist/themes/grass-dark.d.ts.map +1 -0
  102. package/dist/themes/grass-dark.js +552 -0
  103. package/dist/themes/indigo.d.ts +4 -0
  104. package/dist/themes/indigo.d.ts.map +1 -0
  105. package/dist/themes/indigo.js +552 -0
  106. package/dist/themes/light.d.ts +4 -0
  107. package/dist/themes/light.d.ts.map +1 -0
  108. package/dist/themes/light.js +551 -0
  109. package/dist/themes/orange-dark.d.ts +4 -0
  110. package/dist/themes/orange-dark.d.ts.map +1 -0
  111. package/dist/themes/orange-dark.js +551 -0
  112. package/dist/themes/orange-light.d.ts +4 -0
  113. package/dist/themes/orange-light.d.ts.map +1 -0
  114. package/dist/themes/orange-light.js +551 -0
  115. package/package.json +62 -0
  116. package/src/components/controls/Button/Button.vue +417 -0
  117. package/src/components/controls/Button/README.md +348 -0
  118. package/src/components/controls/Button/theme.css +200 -0
  119. package/src/components/controls/Checkbox/Checkbox.vue +164 -0
  120. package/src/components/controls/Checkbox/README.md +441 -0
  121. package/src/components/controls/Checkbox/theme.css +36 -0
  122. package/src/components/controls/Dropdown/Dropdown.vue +476 -0
  123. package/src/components/controls/Dropdown/README.md +370 -0
  124. package/src/components/controls/Dropdown/theme.css +50 -0
  125. package/src/components/controls/Dropdown/types.ts +6 -0
  126. package/src/components/controls/IconButton/IconButton.vue +267 -0
  127. package/src/components/controls/IconButton/README.md +502 -0
  128. package/src/components/controls/IconButton/theme.css +89 -0
  129. package/src/components/controls/Radio/README.md +591 -0
  130. package/src/components/controls/Radio/Radio.vue +89 -0
  131. package/src/components/controls/Radio/theme.css +14 -0
  132. package/src/components/controls/RangeSlider/README.md +608 -0
  133. package/src/components/controls/RangeSlider/RangeSlider.vue +535 -0
  134. package/src/components/controls/RangeSlider/theme.css +80 -0
  135. package/src/components/controls/SegmentedControl/README.md +587 -0
  136. package/src/components/controls/SegmentedControl/SegmentedControl.vue +284 -0
  137. package/src/components/controls/SegmentedControl/theme.css +60 -0
  138. package/src/components/controls/SegmentedControl/types.ts +5 -0
  139. package/src/components/controls/Slider/README.md +627 -0
  140. package/src/components/controls/Slider/Slider.vue +260 -0
  141. package/src/components/controls/Slider/theme.css +74 -0
  142. package/src/components/controls/Stepper/README.md +601 -0
  143. package/src/components/controls/Stepper/Stepper.vue +103 -0
  144. package/src/components/controls/Stepper/theme.css +53 -0
  145. package/src/components/controls/Switch/README.md +667 -0
  146. package/src/components/controls/Switch/Switch.vue +127 -0
  147. package/src/components/controls/Switch/theme.css +42 -0
  148. package/src/components/dialogs/Alert/Alert.vue +218 -0
  149. package/src/components/dialogs/Alert/README.md +450 -0
  150. package/src/components/dialogs/Alert/theme.css +44 -0
  151. package/src/components/dialogs/Alert/types.ts +11 -0
  152. package/src/components/dialogs/Toast/README.md +522 -0
  153. package/src/components/dialogs/Toast/Toast.vue +296 -0
  154. package/src/components/dialogs/Toast/ToastContainer.vue +330 -0
  155. package/src/components/dialogs/Toast/theme.css +44 -0
  156. package/src/components/dialogs/Toast/types.ts +46 -0
  157. package/src/components/dialogs/Toast/useToast.ts +127 -0
  158. package/src/components/indicators/ProgressBar/ProgressBar.vue +98 -0
  159. package/src/components/indicators/ProgressBar/README.md +744 -0
  160. package/src/components/indicators/ProgressBar/theme.css +36 -0
  161. package/src/components/indicators/Tooltip/README.md +723 -0
  162. package/src/components/indicators/Tooltip/TooltipProvider.vue +142 -0
  163. package/src/components/indicators/Tooltip/theme.css +18 -0
  164. package/src/components/indicators/Tooltip/tooltip.ts +48 -0
  165. package/src/components/indicators/Tooltip/types.ts +15 -0
  166. package/src/components/indicators/Tooltip/useTooltip.ts +71 -0
  167. package/src/components/inputs/AutogrowTextView/AutogrowTextView.vue +110 -0
  168. package/src/components/inputs/AutogrowTextView/README.md +643 -0
  169. package/src/components/inputs/AutogrowTextView/theme.css +28 -0
  170. package/src/components/inputs/InputCard/InputCard.vue +600 -0
  171. package/src/components/inputs/InputCard/README.md +636 -0
  172. package/src/components/inputs/InputEmail/InputEmail.vue +698 -0
  173. package/src/components/inputs/InputEmail/README.md +764 -0
  174. package/src/components/inputs/InputNumber/InputNumber.vue +300 -0
  175. package/src/components/inputs/InputNumber/README.md +749 -0
  176. package/src/components/inputs/InputPhone/InputPhone.vue +645 -0
  177. package/src/components/inputs/InputPhone/README.md +636 -0
  178. package/src/components/inputs/InputSecure/InputSecure.vue +646 -0
  179. package/src/components/inputs/InputSecure/README.md +771 -0
  180. package/src/components/inputs/InputText/InputText.vue +225 -0
  181. package/src/components/inputs/InputText/README.md +844 -0
  182. package/src/components/inputs/OTP/OTP.vue +349 -0
  183. package/src/components/inputs/OTP/README.md +736 -0
  184. package/src/components/inputs/OTP/theme.css +50 -0
  185. package/src/components/inputs/StringCapture/README.md +718 -0
  186. package/src/components/inputs/StringCapture/StringCapture.vue +315 -0
  187. package/src/components/inputs/StringCapture/theme.css +86 -0
  188. package/src/components/inputs/Tags/README.md +897 -0
  189. package/src/components/inputs/Tags/TagBar.vue +793 -0
  190. package/src/components/inputs/Tags/TagCreation.vue +219 -0
  191. package/src/components/inputs/Tags/TagPicker.vue +380 -0
  192. package/src/components/inputs/Tags/tag-bar-styles.ts +354 -0
  193. package/src/components/inputs/Tags/theme.css +121 -0
  194. package/src/components/inputs/Tags/types.ts +346 -0
  195. package/src/components/inputs/search/README.md +759 -0
  196. package/src/components/inputs/search/SearchBar.vue +394 -0
  197. package/src/components/inputs/search/SearchResults.vue +310 -0
  198. package/src/components/inputs/search/theme.css +187 -0
  199. package/src/components/inputs/search/types.ts +8 -0
  200. package/src/components/inputs/theme.css +102 -0
  201. package/src/components/menus/ActionMenu/ActionMenu.vue +383 -0
  202. package/src/components/menus/ActionMenu/README.md +825 -0
  203. package/src/components/menus/ActionMenu/theme.css +93 -0
  204. package/src/components/models/Popover/Popover.vue +551 -0
  205. package/src/components/models/Popover/README.md +885 -0
  206. package/src/components/models/Popover/theme.css +52 -0
  207. package/src/components/models/Sheet/README.md +1159 -0
  208. package/src/components/models/Sheet/Sheet.vue +465 -0
  209. package/src/components/models/Sheet/theme.css +72 -0
  210. package/src/components/models/Sidebar/README.md +1228 -0
  211. package/src/components/models/Sidebar/Sidebar.vue +480 -0
  212. package/src/components/models/Sidebar/theme.css +90 -0
  213. package/src/components/navigation/adaptive/AdaptiveLayout.vue +779 -0
  214. package/src/components/navigation/adaptive/AdaptiveLayoutBreadcrumbs.vue +192 -0
  215. package/src/components/navigation/adaptive/AdaptiveLayoutMenuButton.vue +149 -0
  216. package/src/components/navigation/adaptive/README.md +768 -0
  217. package/src/components/navigation/adaptive/types.ts +19 -0
  218. package/src/components/navigation/adaptive/useAdaptiveLayout.ts +89 -0
  219. package/src/components/navigation/adaptive/useBreakpoints.ts +41 -0
  220. package/src/components/navigation/adaptive/useContainerMonitor.ts +214 -0
  221. package/src/components/navigation/adaptive/useViewAnimation.ts +721 -0
  222. package/src/components/navigation/adaptive/useViewResize.ts +211 -0
  223. package/src/components/navigation/navstack/NavigationStack.vue +180 -0
  224. package/src/components/navigation/navstack/README.md +994 -0
  225. package/src/components/navigation/navstack/useNavigationStack.ts +164 -0
  226. package/src/components/navigation/slideover/README.md +1275 -0
  227. package/src/components/navigation/slideover/SlideoverController.vue +287 -0
  228. package/src/components/navigation/slideover/useSlideoverController.ts +320 -0
  229. package/src/components/navigation/splitview/README.md +1115 -0
  230. package/src/components/navigation/splitview/SplitViewController.vue +176 -0
  231. package/src/components/navigation/splitview/useSplitViewController.ts +388 -0
  232. package/src/components/navigation/tabcontroller/README.md +919 -0
  233. package/src/components/navigation/tabcontroller/TabController.vue +307 -0
  234. package/src/components/navigation/tabcontroller/TabItem.vue +57 -0
  235. package/src/components/navigation/tabcontroller/types.ts +24 -0
  236. package/src/components/navigation/tabcontroller/useTabController.ts +18 -0
  237. package/src/components/navigation/theme.css +91 -0
  238. package/src/components/navigation/types.ts +7 -0
  239. package/src/components/pickers/CollectionPicker/CollectionPicker.vue +398 -0
  240. package/src/components/pickers/CollectionPicker/README.md +1115 -0
  241. package/src/components/pickers/CollectionPicker/theme.css +14 -0
  242. package/src/components/pickers/CollectionPicker/types.ts +11 -0
  243. package/src/components/pickers/ColorPicker/ColorPicker.vue +376 -0
  244. package/src/components/pickers/ColorPicker/README.md +1439 -0
  245. package/src/components/pickers/ColorPicker/colors.ts +299 -0
  246. package/src/components/pickers/ColorPicker/theme.css +32 -0
  247. package/src/components/pickers/DatePicker/DatePicker.vue +660 -0
  248. package/src/components/pickers/DatePicker/README.md +1195 -0
  249. package/src/components/pickers/DatePicker/theme.css +22 -0
  250. package/src/components/pickers/FilePicker/FilePicker.vue +534 -0
  251. package/src/components/pickers/FilePicker/README.md +1542 -0
  252. package/src/components/pickers/FilePicker/theme.css +48 -0
  253. package/src/components/pickers/FilePicker/types.ts +10 -0
  254. package/src/components/pickers/IconPicker/IconPicker.vue +327 -0
  255. package/src/components/pickers/IconPicker/README.md +1161 -0
  256. package/src/components/pickers/IconPicker/theme.css +28 -0
  257. package/src/components/pickers/theme.css +82 -0
  258. package/src/components/views/MarkdownViewer/MarkdownViewer.vue +442 -0
  259. package/src/components/views/MarkdownViewer/README.md +833 -0
  260. package/src/components/views/MarkdownViewer/theme.css +130 -0
  261. package/src/index.ts +263 -0
  262. package/src/theme.ts +378 -0
  263. package/src/themes/crimson-dark.ts +556 -0
  264. package/src/themes/cyan-light.ts +556 -0
  265. package/src/themes/dark.ts +557 -0
  266. package/src/themes/gold-dark.ts +556 -0
  267. package/src/themes/grass-dark.ts +556 -0
  268. package/src/themes/indigo.ts +556 -0
  269. package/src/themes/light.ts +557 -0
  270. package/src/themes/orange-dark.ts +557 -0
  271. package/src/themes/orange-light.ts +557 -0
  272. package/src/vue.d.ts +45 -0
package/src/theme.ts ADDED
@@ -0,0 +1,378 @@
1
+ import { ref, computed, readonly, type Ref, type ComputedRef } from "vue";
2
+
3
+ export type Theme = "light" | "dark" | "custom";
4
+
5
+ export interface ThemeConfig {
6
+ theme: Theme;
7
+ autoDetect: boolean;
8
+ customThemeId?: string;
9
+ customThemeColorMode?: "light" | "dark";
10
+ }
11
+
12
+ // Track registered themes to avoid duplicates
13
+ const registeredThemes = new Set<string>();
14
+
15
+ class ThemeManager {
16
+ private currentTheme: Ref<Theme> = ref("light");
17
+ private autoDetect: Ref<boolean> = ref(true);
18
+ private mediaQuery: MediaQueryList | null = null;
19
+ private systemThemeListener: ((e: MediaQueryListEvent) => void) | null = null;
20
+ private customThemeId: Ref<string | undefined> = ref(undefined);
21
+ private customThemeColorMode: Ref<"light" | "dark" | undefined> =
22
+ ref(undefined);
23
+
24
+ constructor() {
25
+ this.initialize();
26
+ }
27
+
28
+ private initialize(): void {
29
+ // Load saved configuration
30
+ const savedConfig = this.loadConfig();
31
+
32
+ if (savedConfig) {
33
+ this.autoDetect.value = savedConfig.autoDetect;
34
+ this.customThemeId.value = savedConfig.customThemeId;
35
+ this.customThemeColorMode.value = savedConfig.customThemeColorMode;
36
+ // If auto-detect is enabled, use system theme regardless of saved theme
37
+ if (this.autoDetect.value) {
38
+ this.setupSystemThemeDetection();
39
+ } else {
40
+ // Use saved theme only if auto-detect is disabled
41
+ if (
42
+ savedConfig.theme === "custom" &&
43
+ this.customThemeId.value &&
44
+ this.customThemeColorMode.value
45
+ ) {
46
+ // Properly restore custom theme with both ID and color mode
47
+ this.setCustomTheme(
48
+ this.customThemeId.value,
49
+ this.customThemeColorMode.value,
50
+ false
51
+ );
52
+ } else {
53
+ this.setTheme(savedConfig.theme, false);
54
+ }
55
+ }
56
+ } else {
57
+ // First time setup - default to auto-detect
58
+ this.setupSystemThemeDetection();
59
+ }
60
+ }
61
+
62
+ private loadConfig(): ThemeConfig | null {
63
+ try {
64
+ const saved = localStorage.getItem("umbra-theme-config");
65
+ return saved ? JSON.parse(saved) : null;
66
+ } catch {
67
+ return null;
68
+ }
69
+ }
70
+
71
+ private saveConfig(): void {
72
+ const config: ThemeConfig = {
73
+ theme: this.currentTheme.value,
74
+ autoDetect: this.autoDetect.value,
75
+ customThemeId: this.customThemeId.value,
76
+ customThemeColorMode: this.customThemeColorMode.value,
77
+ };
78
+
79
+ try {
80
+ localStorage.setItem("umbra-theme-config", JSON.stringify(config));
81
+ } catch (e) {
82
+ console.warn("Failed to save theme configuration:", e);
83
+ }
84
+ }
85
+
86
+ private setupSystemThemeDetection(): void {
87
+ // Clean up existing listener if any
88
+ this.removeSystemThemeListener();
89
+
90
+ this.mediaQuery = window.matchMedia("(prefers-color-scheme: dark)");
91
+ const systemTheme: Theme = this.mediaQuery.matches ? "dark" : "light";
92
+ this.setTheme(systemTheme, false);
93
+
94
+ // Create and store the listener
95
+ this.systemThemeListener = (e: MediaQueryListEvent) => {
96
+ if (this.autoDetect.value) {
97
+ this.setTheme(e.matches ? "dark" : "light", false);
98
+ }
99
+ };
100
+
101
+ this.mediaQuery.addEventListener("change", this.systemThemeListener);
102
+ }
103
+
104
+ private removeSystemThemeListener(): void {
105
+ if (this.mediaQuery && this.systemThemeListener) {
106
+ this.mediaQuery.removeEventListener("change", this.systemThemeListener);
107
+ this.systemThemeListener = null;
108
+ }
109
+ }
110
+
111
+ public setTheme(theme: Theme, save: boolean = true): void {
112
+ this.currentTheme.value = theme;
113
+
114
+ // Remove all theme-related classes and attributes
115
+ document.documentElement.classList.remove(
116
+ "light",
117
+ "light-theme",
118
+ "dark",
119
+ "dark-theme"
120
+ );
121
+ document.documentElement.removeAttribute("data-theme");
122
+ document.documentElement.removeAttribute("data-custom-theme");
123
+
124
+ // Apply theme using CSS classes
125
+ if (theme === "custom" && this.customThemeId.value) {
126
+ document.documentElement.setAttribute("data-theme", "custom");
127
+ document.documentElement.setAttribute(
128
+ "data-custom-theme",
129
+ this.customThemeId.value
130
+ );
131
+
132
+ // Also set the color mode class for custom themes
133
+ if (this.customThemeColorMode.value === "dark") {
134
+ document.documentElement.classList.add("dark", "dark-theme");
135
+ } else {
136
+ document.documentElement.classList.add("light", "light-theme");
137
+ }
138
+ } else if (theme === "dark") {
139
+ document.documentElement.classList.add("dark", "dark-theme");
140
+ } else {
141
+ document.documentElement.classList.add("light", "light-theme");
142
+ }
143
+
144
+ if (save) {
145
+ this.saveConfig();
146
+ }
147
+
148
+ // Dispatch theme change event
149
+ window.dispatchEvent(
150
+ new CustomEvent("theme-change", {
151
+ detail: {
152
+ theme,
153
+ customThemeId:
154
+ theme === "custom" ? this.customThemeId.value : undefined,
155
+ customThemeColorMode:
156
+ theme === "custom" ? this.customThemeColorMode.value : undefined,
157
+ },
158
+ })
159
+ );
160
+ }
161
+
162
+ public setCustomTheme(
163
+ customThemeId: string,
164
+ customThemeColorMode: "light" | "dark",
165
+ save: boolean = true
166
+ ): void {
167
+ this.customThemeId.value = customThemeId;
168
+ this.customThemeColorMode.value = customThemeColorMode;
169
+ this.setTheme("custom", save);
170
+ }
171
+
172
+ public getTheme(): Theme {
173
+ return this.currentTheme.value;
174
+ }
175
+
176
+ public getConfig(): ThemeConfig {
177
+ return {
178
+ theme: this.currentTheme.value,
179
+ autoDetect: this.autoDetect.value,
180
+ customThemeId: this.customThemeId.value,
181
+ customThemeColorMode: this.customThemeColorMode.value,
182
+ };
183
+ }
184
+
185
+ public setAutoDetect(enabled: boolean): void {
186
+ this.autoDetect.value = enabled;
187
+
188
+ if (enabled) {
189
+ this.setupSystemThemeDetection();
190
+ } else {
191
+ this.removeSystemThemeListener();
192
+ }
193
+
194
+ this.saveConfig();
195
+ }
196
+
197
+ public isAutoDetectEnabled(): boolean {
198
+ return this.autoDetect.value;
199
+ }
200
+
201
+ public toggleTheme(): void {
202
+ // Disable auto-detect when manually toggling
203
+ if (this.autoDetect.value) {
204
+ this.setAutoDetect(false);
205
+ }
206
+
207
+ // Toggle between light and dark only
208
+ // If custom theme is active, switch to light
209
+ let newTheme: Theme;
210
+ switch (this.currentTheme.value) {
211
+ case "light":
212
+ newTheme = "dark";
213
+ break;
214
+ case "dark":
215
+ newTheme = "light";
216
+ break;
217
+ case "custom":
218
+ newTheme = "light";
219
+ break;
220
+ default:
221
+ newTheme = "light";
222
+ }
223
+
224
+ this.setTheme(newTheme);
225
+ }
226
+
227
+ public destroy(): void {
228
+ this.removeSystemThemeListener();
229
+ }
230
+
231
+ // Expose reactive references as readonly for components
232
+ public get theme(): Readonly<Ref<Theme>> {
233
+ return readonly(this.currentTheme);
234
+ }
235
+
236
+ public get customThemeIdRef(): Readonly<Ref<string | undefined>> {
237
+ return readonly(this.customThemeId);
238
+ }
239
+
240
+ public get customThemeColorModeRef(): Readonly<
241
+ Ref<"light" | "dark" | undefined>
242
+ > {
243
+ return readonly(this.customThemeColorMode);
244
+ }
245
+
246
+ public get autoDetectRef(): Readonly<Ref<boolean>> {
247
+ return readonly(this.autoDetect);
248
+ }
249
+
250
+ // Computed property for the full config
251
+ public get configRef(): ComputedRef<ThemeConfig> {
252
+ return computed(() => ({
253
+ theme: this.currentTheme.value,
254
+ autoDetect: this.autoDetect.value,
255
+ customThemeId: this.customThemeId.value,
256
+ customThemeColorMode: this.customThemeColorMode.value,
257
+ }));
258
+ }
259
+ }
260
+
261
+ // Create singleton instance
262
+ export const themeManager = new ThemeManager();
263
+
264
+ // Export reactive references for direct use in components
265
+ export const currentTheme = themeManager.theme;
266
+ export const customThemeId = themeManager.customThemeIdRef;
267
+ export const customThemeColorMode = themeManager.customThemeColorModeRef;
268
+ export const autoDetect = themeManager.autoDetectRef;
269
+ export const themeConfig = themeManager.configRef;
270
+
271
+ // Export convenience functions (keep existing API)
272
+ export const setTheme = (theme: Theme, save?: boolean): void =>
273
+ themeManager.setTheme(theme, save);
274
+
275
+ export const setCustomTheme = (
276
+ customThemeId: string,
277
+ customThemeColorMode: "light" | "dark",
278
+ save?: boolean
279
+ ): void =>
280
+ themeManager.setCustomTheme(customThemeId, customThemeColorMode, save);
281
+
282
+ export const getTheme = (): Theme => themeManager.getTheme();
283
+
284
+ export const getThemeConfig = (): ThemeConfig => themeManager.getConfig();
285
+
286
+ export const toggleTheme = (): void => themeManager.toggleTheme();
287
+
288
+ export const setAutoDetect = (enabled: boolean): void =>
289
+ themeManager.setAutoDetect(enabled);
290
+
291
+ export const isAutoDetectEnabled = (): boolean =>
292
+ themeManager.isAutoDetectEnabled();
293
+
294
+ // Cleanup function for SPA navigation
295
+ export const destroyThemeManager = (): void => themeManager.destroy();
296
+
297
+ // Custom theme registration utilities
298
+ export const registerCustomTheme = (
299
+ themeId: string,
300
+ cssVariables: Record<string, string>
301
+ ): void => {
302
+ // Check if already registered
303
+ if (registeredThemes.has(themeId)) {
304
+ console.warn(`Theme "${themeId}" is already registered`);
305
+ return;
306
+ }
307
+
308
+ // Check if style element already exists (e.g., from hot reload)
309
+ const existing = document.querySelector(
310
+ `style[data-umbra-custom-theme="${themeId}"]`
311
+ );
312
+ if (existing) {
313
+ existing.remove();
314
+ }
315
+
316
+ const style = document.createElement("style");
317
+ style.setAttribute("data-umbra-custom-theme", themeId);
318
+
319
+ const cssVars = Object.entries(cssVariables)
320
+ .map(([key, value]) => ` ${key}: ${value};`)
321
+ .join("\n");
322
+
323
+ style.textContent = `
324
+ [data-theme="custom"][data-custom-theme="${themeId}"] {
325
+ ${cssVars}
326
+ }`;
327
+
328
+ document.head.appendChild(style);
329
+ registeredThemes.add(themeId);
330
+ };
331
+
332
+ export const unregisterCustomTheme = (themeId: string): void => {
333
+ const styleElement = document.querySelector(
334
+ `style[data-umbra-custom-theme="${themeId}"]`
335
+ );
336
+
337
+ if (styleElement) {
338
+ styleElement.remove();
339
+ registeredThemes.delete(themeId);
340
+ }
341
+ };
342
+
343
+ export const isThemeRegistered = (themeId: string): boolean => {
344
+ return registeredThemes.has(themeId);
345
+ };
346
+
347
+ export const getRegisteredThemes = (): string[] => {
348
+ return Array.from(registeredThemes);
349
+ };
350
+
351
+ // Composable for use in Vue components
352
+ export const useTheme = () => {
353
+ return {
354
+ // Reactive refs
355
+ theme: currentTheme,
356
+ customThemeId,
357
+ customThemeColorMode,
358
+ autoDetect,
359
+ config: themeConfig,
360
+
361
+ // Methods
362
+ setTheme,
363
+ setCustomTheme,
364
+ toggleTheme,
365
+ setAutoDetect,
366
+
367
+ // Custom theme registration (for advanced usage)
368
+ registerCustomTheme,
369
+ unregisterCustomTheme,
370
+ isThemeRegistered,
371
+ getRegisteredThemes,
372
+
373
+ // Non-reactive getters (for backward compatibility)
374
+ getTheme,
375
+ getThemeConfig,
376
+ isAutoDetectEnabled,
377
+ };
378
+ };