m9sh 0.2.1 → 0.2.2

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 (125) hide show
  1. checksums.yaml +4 -4
  2. data/.idea/hotcdn.iml +30 -0
  3. data/.mise.toml +2 -2
  4. data/app/assets/config/manifest.js +4 -0
  5. data/app/assets/images/icons/activity.svg +3 -0
  6. data/app/assets/images/icons/bell.svg +4 -0
  7. data/app/assets/images/icons/book.svg +4 -0
  8. data/app/assets/images/icons/chevron-down.svg +3 -0
  9. data/app/assets/images/icons/chevron-left.svg +3 -0
  10. data/app/assets/images/icons/chevron-right.svg +3 -0
  11. data/app/assets/images/icons/credit-card.svg +4 -0
  12. data/app/assets/images/icons/dollar-sign.svg +3 -0
  13. data/app/assets/images/icons/edit.svg +4 -0
  14. data/app/assets/images/icons/github.svg +3 -0
  15. data/app/assets/images/icons/home.svg +4 -0
  16. data/app/assets/images/icons/info.svg +5 -0
  17. data/app/assets/images/icons/layout.svg +6 -0
  18. data/app/assets/images/icons/logout.svg +5 -0
  19. data/app/assets/images/icons/menu.svg +5 -0
  20. data/app/assets/images/icons/moon.svg +3 -0
  21. data/app/assets/images/icons/paintbrush.svg +6 -0
  22. data/app/assets/images/icons/search.svg +4 -0
  23. data/app/assets/images/icons/settings.svg +4 -0
  24. data/app/assets/images/icons/sun.svg +11 -0
  25. data/app/assets/images/icons/user.svg +4 -0
  26. data/app/assets/images/icons/users.svg +5 -0
  27. data/app/assets/stylesheets/tailwind.css +1180 -0
  28. data/app/components/backdrop_component.rb +103 -0
  29. data/app/components/docs/code_block_component.rb +56 -0
  30. data/app/components/docs/component_api_component.rb +16 -0
  31. data/app/components/docs/component_examples_component.rb +16 -0
  32. data/app/components/docs/component_header_component.html.erb +8 -0
  33. data/app/components/docs/component_header_component.rb +14 -0
  34. data/app/components/docs/component_installation_component.html.erb +15 -0
  35. data/app/components/docs/component_installation_component.rb +13 -0
  36. data/app/components/docs/component_page_component.html.erb +9 -0
  37. data/app/components/docs/component_page_component.rb +19 -0
  38. data/app/components/docs/component_preview_component.rb +318 -0
  39. data/app/components/docs/component_usage_component.rb +18 -0
  40. data/app/components/docs/prop_table_component.rb +64 -0
  41. data/app/controllers/application_controller.rb +3 -0
  42. data/app/controllers/blocks_controller.rb +51 -0
  43. data/app/controllers/docs_controller.rb +162 -0
  44. data/app/controllers/showcase_controller.rb +42 -0
  45. data/app/helpers/blocks_helper.rb +343 -0
  46. data/app/helpers/docs_helper.rb +3807 -0
  47. data/app/helpers/m9sh/toast_helper.rb +46 -0
  48. data/app/helpers/m9sh_helper.rb +343 -0
  49. data/app/javascript/application.js +3 -0
  50. data/app/javascript/controllers/application.js +9 -0
  51. data/app/javascript/controllers/backdrop_controller.js +137 -0
  52. data/app/javascript/controllers/color_customizer_controller.js +569 -0
  53. data/app/javascript/controllers/color_theme_controller.js +120 -0
  54. data/app/javascript/controllers/docs/component_preview_controller.js +149 -0
  55. data/app/javascript/controllers/docs/copy_button_controller.js +20 -0
  56. data/app/javascript/controllers/index.js +6 -0
  57. data/app/javascript/controllers/theme_controller.js +23 -0
  58. data/app/views/blocks/_sidebar.html.erb +31 -0
  59. data/app/views/blocks/_toc.html.erb +29 -0
  60. data/app/views/blocks/examples/dashboard-01.html.erb +180 -0
  61. data/app/views/blocks/examples/dashboard-02.html.erb +190 -0
  62. data/app/views/blocks/examples/dashboard-03.html.erb +210 -0
  63. data/app/views/blocks/examples/settings-01.html.erb +220 -0
  64. data/app/views/blocks/examples/settings-02.html.erb +231 -0
  65. data/app/views/blocks/examples/settings-03.html.erb +340 -0
  66. data/app/views/blocks/index.html.erb +65 -0
  67. data/app/views/docs/_sidebar.html.erb +47 -0
  68. data/app/views/docs/_toc.html.erb +19 -0
  69. data/app/views/docs/about.html.erb +68 -0
  70. data/app/views/docs/components/accordion.html.erb +196 -0
  71. data/app/views/docs/components/alert.html.erb +272 -0
  72. data/app/views/docs/components/alert_dialog.html.erb +232 -0
  73. data/app/views/docs/components/avatar.html.erb +207 -0
  74. data/app/views/docs/components/badge.html.erb +145 -0
  75. data/app/views/docs/components/breadcrumb.html.erb +264 -0
  76. data/app/views/docs/components/button.html.erb +229 -0
  77. data/app/views/docs/components/card.html.erb +378 -0
  78. data/app/views/docs/components/checkbox.html.erb +212 -0
  79. data/app/views/docs/components/collapsible.html.erb +252 -0
  80. data/app/views/docs/components/dialog.html.erb +323 -0
  81. data/app/views/docs/components/dropdown_menu.html.erb +289 -0
  82. data/app/views/docs/components/hover_card.html.erb +220 -0
  83. data/app/views/docs/components/input.html.erb +254 -0
  84. data/app/views/docs/components/label.html.erb +128 -0
  85. data/app/views/docs/components/main.html.erb +352 -0
  86. data/app/views/docs/components/navbar.html.erb +394 -0
  87. data/app/views/docs/components/navigation_menu.html.erb +226 -0
  88. data/app/views/docs/components/popover.html.erb +267 -0
  89. data/app/views/docs/components/progress.html.erb +107 -0
  90. data/app/views/docs/components/radio_group.html.erb +209 -0
  91. data/app/views/docs/components/select.html.erb +260 -0
  92. data/app/views/docs/components/separator.html.erb +162 -0
  93. data/app/views/docs/components/sheet.html.erb +270 -0
  94. data/app/views/docs/components/sidebar.html.erb +597 -0
  95. data/app/views/docs/components/skeleton.html.erb +150 -0
  96. data/app/views/docs/components/slider.html.erb +218 -0
  97. data/app/views/docs/components/spinner.html.erb +132 -0
  98. data/app/views/docs/components/switch.html.erb +148 -0
  99. data/app/views/docs/components/table.html.erb +259 -0
  100. data/app/views/docs/components/tabs.html.erb +225 -0
  101. data/app/views/docs/components/textarea.html.erb +239 -0
  102. data/app/views/docs/components/theme_toggle.html.erb +135 -0
  103. data/app/views/docs/components/toast.html.erb +205 -0
  104. data/app/views/docs/components/toaster.html.erb +227 -0
  105. data/app/views/docs/components/toggle.html.erb +154 -0
  106. data/app/views/docs/components/tooltip.html.erb +216 -0
  107. data/app/views/docs/components/typography.html.erb +180 -0
  108. data/app/views/docs/index.html.erb +143 -0
  109. data/app/views/docs/installation.html.erb +155 -0
  110. data/app/views/docs/simple_test.html.erb +13 -0
  111. data/app/views/docs/test_accordion.html.erb +14 -0
  112. data/app/views/docs/usage.html.erb +272 -0
  113. data/app/views/layouts/application.html.erb +107 -0
  114. data/app/views/layouts/backdrop.html.erb +77 -0
  115. data/app/views/shared/_app_navbar.html.erb +240 -0
  116. data/app/views/shared/_navbar.html.erb +69 -0
  117. data/app/views/showcase/v2/_components_grid.html.erb +38 -0
  118. data/app/views/showcase/v2/_features.html.erb +59 -0
  119. data/app/views/showcase/v2/_forms.html.erb +195 -0
  120. data/app/views/showcase/v2/_hero.html.erb +55 -0
  121. data/app/views/showcase/v2/_metrics.html.erb +107 -0
  122. data/app/views/showcase/v2.html.erb +18 -0
  123. data/lib/m9sh/version.rb +1 -1
  124. data/m9sh.gemspec +1 -1
  125. metadata +120 -1
@@ -0,0 +1,569 @@
1
+ import { Controller } from "@hotwired/stimulus"
2
+
3
+ export default class extends Controller {
4
+ static targets = [
5
+ "swatch", "colorValue", "selectedSwatch", "selectedName", "selectedValue",
6
+ "sliders", "lightnessSlider", "chromaSlider", "hueSlider",
7
+ "lightnessValue", "chromaValue", "hueValue",
8
+ "configPreview", "applyButton"
9
+ ]
10
+
11
+ static values = { theme: String }
12
+
13
+ connect() {
14
+ this.selectedColor = null
15
+ this.customColors = this.loadCustomColors()
16
+ this.exportFormat = "tailwind"
17
+
18
+ // Apply saved custom colors
19
+ this.applyCustomColors()
20
+
21
+ // Load current color values
22
+ this.loadColorValues()
23
+
24
+ // Generate initial config
25
+ this.generateConfig()
26
+
27
+ // Show apply button initially for tailwind format
28
+ if (this.hasApplyButtonTarget && this.exportFormat === "tailwind") {
29
+ this.applyButtonTarget.classList.remove('hidden')
30
+ }
31
+ }
32
+
33
+ loadColorValues() {
34
+ // Get computed styles for all color swatches
35
+ this.swatchTargets.forEach(swatch => {
36
+ const colorName = swatch.dataset.color
37
+ const computedStyle = getComputedStyle(document.documentElement)
38
+ const colorValue = computedStyle.getPropertyValue(`--color-${colorName}`).trim()
39
+
40
+ // Find the corresponding value display
41
+ const valueDisplay = this.colorValueTargets.find(el => el.dataset.color === colorName)
42
+ if (valueDisplay) {
43
+ valueDisplay.textContent = colorValue || "undefined"
44
+ }
45
+ })
46
+ }
47
+
48
+ closeAllPopovers() {
49
+ // Find all popover controllers within this element
50
+ const popovers = this.element.querySelectorAll('[data-controller*="m9sh--popover"]')
51
+ popovers.forEach(popoverEl => {
52
+ const controller = this.application.getControllerForElementAndIdentifier(popoverEl, 'm9sh--popover')
53
+ if (controller && controller.isOpen) {
54
+ controller.close()
55
+ }
56
+ })
57
+ }
58
+
59
+ selectColor(event) {
60
+ const colorName = event.params.color
61
+ this.selectedColor = colorName
62
+
63
+ // Close all other popovers first
64
+ this.closeAllPopovers()
65
+
66
+ // Find the popover that contains this color
67
+ const popover = event.currentTarget.closest('[data-controller*="m9sh--popover"]')
68
+ if (!popover) return
69
+
70
+ // Wait for popover to be visible before accessing its content
71
+ setTimeout(() => {
72
+ // Find targets within this specific popover
73
+ const popoverContent = popover.querySelector('[data-m9sh--popover-target="content"]')
74
+ if (!popoverContent) return
75
+
76
+ const selectedValueEl = popoverContent.querySelector('[data-color-customizer-target="selectedValue"]')
77
+ const lightnessSliderEl = popoverContent.querySelector('[data-color-customizer-target="lightnessSlider"]')
78
+ const chromaSliderEl = popoverContent.querySelector('[data-color-customizer-target="chromaSlider"]')
79
+ const hueSliderEl = popoverContent.querySelector('[data-color-customizer-target="hueSlider"]')
80
+ const lightnessValueEl = popoverContent.querySelector('[data-color-customizer-target="lightnessValue"]')
81
+ const chromaValueEl = popoverContent.querySelector('[data-color-customizer-target="chromaValue"]')
82
+ const hueValueEl = popoverContent.querySelector('[data-color-customizer-target="hueValue"]')
83
+
84
+ // Get current color value
85
+ const computedStyle = getComputedStyle(document.documentElement)
86
+ const colorValue = computedStyle.getPropertyValue(`--color-${colorName}`).trim()
87
+
88
+ // Update the value display in this popover
89
+ if (selectedValueEl) {
90
+ selectedValueEl.textContent = colorValue
91
+ }
92
+
93
+ // Parse OKLCH values and update sliders
94
+ if (colorValue && colorValue.startsWith('oklch')) {
95
+ const values = this.parseOklch(colorValue)
96
+ if (values) {
97
+ this.updateSlidersInPopover(values, lightnessSliderEl, chromaSliderEl, hueSliderEl, lightnessValueEl, chromaValueEl, hueValueEl)
98
+ }
99
+ }
100
+ }, 100)
101
+ }
102
+
103
+ parseOklch(oklchString) {
104
+ // Parse oklch(0.55 0.20 260) format
105
+ const match = oklchString.match(/oklch\(([\d.]+)\s+([\d.]+)\s+([\d.]+)(?:\s*\/\s*[\d.]+)?\)/)
106
+ if (match) {
107
+ return {
108
+ lightness: parseFloat(match[1]) * 100, // Convert to percentage
109
+ chroma: parseFloat(match[2]) * 100, // Convert to percentage (0-0.4 → 0-40)
110
+ hue: parseFloat(match[3])
111
+ }
112
+ }
113
+ return null
114
+ }
115
+
116
+ updateSliders(values) {
117
+ // Update slider values
118
+ const lightnessSlider = this.lightnessSliderTarget.querySelector('[data-controller="m9sh--slider"]')
119
+ const chromaSlider = this.chromaSliderTarget.querySelector('[data-controller="m9sh--slider"]')
120
+ const hueSlider = this.hueSliderTarget.querySelector('[data-controller="m9sh--slider"]')
121
+
122
+ if (lightnessSlider) {
123
+ const controller = this.application.getControllerForElementAndIdentifier(lightnessSlider, 'm9sh--slider')
124
+ if (controller) controller.setValue(values.lightness)
125
+ }
126
+
127
+ if (chromaSlider) {
128
+ const controller = this.application.getControllerForElementAndIdentifier(chromaSlider, 'm9sh--slider')
129
+ if (controller) controller.setValue(values.chroma)
130
+ }
131
+
132
+ if (hueSlider) {
133
+ const controller = this.application.getControllerForElementAndIdentifier(hueSlider, 'm9sh--slider')
134
+ if (controller) controller.setValue(values.hue)
135
+ }
136
+
137
+ // Update value displays
138
+ this.lightnessValueTarget.textContent = values.lightness.toFixed(0)
139
+ this.chromaValueTarget.textContent = values.chroma.toFixed(0)
140
+ this.hueValueTarget.textContent = values.hue.toFixed(0)
141
+ }
142
+
143
+ updateSlidersInPopover(values, lightnessSliderEl, chromaSliderEl, hueSliderEl, lightnessValueEl, chromaValueEl, hueValueEl) {
144
+ // Update slider values in the specific popover
145
+ if (lightnessSliderEl) {
146
+ const lightnessSlider = lightnessSliderEl.querySelector('[data-controller="m9sh--slider"]')
147
+ if (lightnessSlider) {
148
+ const controller = this.application.getControllerForElementAndIdentifier(lightnessSlider, 'm9sh--slider')
149
+ if (controller) controller.valueValue = values.lightness
150
+ }
151
+ }
152
+
153
+ if (chromaSliderEl) {
154
+ const chromaSlider = chromaSliderEl.querySelector('[data-controller="m9sh--slider"]')
155
+ if (chromaSlider) {
156
+ const controller = this.application.getControllerForElementAndIdentifier(chromaSlider, 'm9sh--slider')
157
+ if (controller) controller.valueValue = values.chroma
158
+ }
159
+ }
160
+
161
+ if (hueSliderEl) {
162
+ const hueSlider = hueSliderEl.querySelector('[data-controller="m9sh--slider"]')
163
+ if (hueSlider) {
164
+ const controller = this.application.getControllerForElementAndIdentifier(hueSlider, 'm9sh--slider')
165
+ if (controller) controller.valueValue = values.hue
166
+ }
167
+ }
168
+
169
+ // Update value displays
170
+ if (lightnessValueEl) lightnessValueEl.textContent = values.lightness.toFixed(0)
171
+ if (chromaValueEl) chromaValueEl.textContent = values.chroma.toFixed(0)
172
+ if (hueValueEl) hueValueEl.textContent = values.hue.toFixed(0)
173
+ }
174
+
175
+ adjustColor(event) {
176
+ if (!this.selectedColor) return
177
+
178
+ // Find the popover containing the slider
179
+ const popover = event.currentTarget.closest('[data-controller*="m9sh--popover"]')
180
+ if (!popover) return
181
+
182
+ const popoverContent = popover.querySelector('[data-m9sh--popover-target="content"]')
183
+ if (!popoverContent) return
184
+
185
+ const lightnessSliderEl = popoverContent.querySelector('[data-color-customizer-target="lightnessSlider"]')
186
+ const chromaSliderEl = popoverContent.querySelector('[data-color-customizer-target="chromaSlider"]')
187
+ const hueSliderEl = popoverContent.querySelector('[data-color-customizer-target="hueSlider"]')
188
+ const selectedValueEl = popoverContent.querySelector('[data-color-customizer-target="selectedValue"]')
189
+ const lightnessValueEl = popoverContent.querySelector('[data-color-customizer-target="lightnessValue"]')
190
+ const chromaValueEl = popoverContent.querySelector('[data-color-customizer-target="chromaValue"]')
191
+ const hueValueEl = popoverContent.querySelector('[data-color-customizer-target="hueValue"]')
192
+
193
+ // Get current slider values
194
+ const lightness = this.getSliderValue(lightnessSliderEl) / 100
195
+ const chroma = this.getSliderValue(chromaSliderEl) / 100
196
+ const hue = this.getSliderValue(hueSliderEl)
197
+
198
+ // Create new OKLCH value
199
+ const newColor = `oklch(${lightness.toFixed(2)} ${chroma.toFixed(2)} ${hue})`
200
+
201
+ // Preview the change
202
+ this.previewColorChange(this.selectedColor, newColor)
203
+
204
+ // Auto-save to localStorage
205
+ if (!this.customColors[this.themeValue]) {
206
+ this.customColors[this.themeValue] = {}
207
+ }
208
+ this.customColors[this.themeValue][this.selectedColor] = newColor
209
+ this.saveCustomColors()
210
+
211
+ // Regenerate config
212
+ this.generateConfig()
213
+
214
+ // Update value display in popover
215
+ if (selectedValueEl) {
216
+ selectedValueEl.textContent = newColor
217
+ }
218
+
219
+ // Update value displays
220
+ if (lightnessValueEl) lightnessValueEl.textContent = (lightness * 100).toFixed(0)
221
+ if (chromaValueEl) chromaValueEl.textContent = (chroma * 100).toFixed(0)
222
+ if (hueValueEl) hueValueEl.textContent = hue.toFixed(0)
223
+ }
224
+
225
+ getSliderValue(sliderContainer) {
226
+ if (!sliderContainer) return 0
227
+
228
+ const slider = sliderContainer.querySelector('[data-controller="m9sh--slider"]')
229
+ if (slider) {
230
+ const controller = this.application.getControllerForElementAndIdentifier(slider, 'm9sh--slider')
231
+ if (controller) {
232
+ return controller.valueValue
233
+ }
234
+ }
235
+ return 0
236
+ }
237
+
238
+ previewColorChange(colorName, colorValue) {
239
+ // Apply to document for live preview
240
+ document.documentElement.style.setProperty(`--color-${colorName}`, colorValue)
241
+
242
+ // Update swatch displays
243
+ const swatch = this.swatchTargets.find(s => s.dataset.color === colorName)
244
+ if (swatch) {
245
+ swatch.style.backgroundColor = colorValue
246
+ }
247
+
248
+ const valueDisplay = this.colorValueTargets.find(el => el.dataset.color === colorName)
249
+ if (valueDisplay) {
250
+ valueDisplay.textContent = colorValue
251
+ }
252
+ }
253
+
254
+ applyColor() {
255
+ if (!this.selectedColor) return
256
+
257
+ // Get current value
258
+ const computedStyle = getComputedStyle(document.documentElement)
259
+ const colorValue = computedStyle.getPropertyValue(`--color-${this.selectedColor}`).trim()
260
+
261
+ // Save to custom colors
262
+ if (!this.customColors[this.themeValue]) {
263
+ this.customColors[this.themeValue] = {}
264
+ }
265
+ this.customColors[this.themeValue][this.selectedColor] = colorValue
266
+
267
+ // Save to localStorage
268
+ this.saveCustomColors()
269
+
270
+ // Regenerate config
271
+ this.generateConfig()
272
+
273
+ // Show success feedback
274
+ this.showToast("Color applied successfully")
275
+ }
276
+
277
+ resetColor(event) {
278
+ if (!this.selectedColor) return
279
+
280
+ // Remove custom color
281
+ if (this.customColors[this.themeValue]) {
282
+ delete this.customColors[this.themeValue][this.selectedColor]
283
+ }
284
+
285
+ // Remove the custom property to revert to default
286
+ document.documentElement.style.removeProperty(`--color-${this.selectedColor}`)
287
+
288
+ // Save changes
289
+ this.saveCustomColors()
290
+
291
+ // Reload color values
292
+ this.loadColorValues()
293
+
294
+ // Regenerate config
295
+ this.generateConfig()
296
+
297
+ // Update sliders in the open popover
298
+ const popover = event.currentTarget.closest('[data-controller*="m9sh--popover"]')
299
+ if (popover) {
300
+ const popoverContent = popover.querySelector('[data-m9sh--popover-target="content"]')
301
+ if (popoverContent) {
302
+ const selectedValueEl = popoverContent.querySelector('[data-color-customizer-target="selectedValue"]')
303
+ const lightnessSliderEl = popoverContent.querySelector('[data-color-customizer-target="lightnessSlider"]')
304
+ const chromaSliderEl = popoverContent.querySelector('[data-color-customizer-target="chromaSlider"]')
305
+ const hueSliderEl = popoverContent.querySelector('[data-color-customizer-target="hueSlider"]')
306
+ const lightnessValueEl = popoverContent.querySelector('[data-color-customizer-target="lightnessValue"]')
307
+ const chromaValueEl = popoverContent.querySelector('[data-color-customizer-target="chromaValue"]')
308
+ const hueValueEl = popoverContent.querySelector('[data-color-customizer-target="hueValue"]')
309
+
310
+ // Get the default color value
311
+ const computedStyle = getComputedStyle(document.documentElement)
312
+ const defaultColor = computedStyle.getPropertyValue(`--color-${this.selectedColor}`).trim()
313
+
314
+ // Update value display
315
+ if (selectedValueEl) {
316
+ selectedValueEl.textContent = defaultColor
317
+ }
318
+
319
+ // Parse and update sliders
320
+ if (defaultColor && defaultColor.startsWith('oklch')) {
321
+ const values = this.parseOklch(defaultColor)
322
+ if (values) {
323
+ this.updateSlidersInPopover(values, lightnessSliderEl, chromaSliderEl, hueSliderEl, lightnessValueEl, chromaValueEl, hueValueEl)
324
+ }
325
+ }
326
+ }
327
+ }
328
+
329
+ // Show feedback
330
+ this.showToast("Color reset to default")
331
+ }
332
+
333
+ resetAll() {
334
+ // Clear all custom colors for this theme
335
+ delete this.customColors[this.themeValue]
336
+
337
+ // Remove all custom properties
338
+ const colorNames = this.swatchTargets.map(s => s.dataset.color)
339
+ colorNames.forEach(color => {
340
+ document.documentElement.style.removeProperty(`--color-${color}`)
341
+ })
342
+
343
+ // Save changes
344
+ this.saveCustomColors()
345
+
346
+ // Reload color values
347
+ this.loadColorValues()
348
+
349
+ // Regenerate config
350
+ this.generateConfig()
351
+
352
+ // Show feedback
353
+ this.showToast("All colors reset to defaults")
354
+ }
355
+
356
+ setExportFormat(event) {
357
+ this.exportFormat = event.params.format
358
+ this.generateConfig()
359
+
360
+ // Show/hide apply button based on format
361
+ if (this.hasApplyButtonTarget) {
362
+ if (this.exportFormat === "tailwind") {
363
+ this.applyButtonTarget.classList.remove('hidden')
364
+ } else {
365
+ this.applyButtonTarget.classList.add('hidden')
366
+ }
367
+ }
368
+
369
+ // Update button styles
370
+ event.currentTarget.parentElement.querySelectorAll('button').forEach(btn => {
371
+ btn.classList.remove('bg-primary', 'text-primary-foreground')
372
+ btn.classList.add('bg-background')
373
+ })
374
+ event.currentTarget.classList.add('bg-primary', 'text-primary-foreground')
375
+ event.currentTarget.classList.remove('bg-background')
376
+ }
377
+
378
+ generateConfig() {
379
+ const colors = this.getAllColors()
380
+ let config = ""
381
+
382
+ switch (this.exportFormat) {
383
+ case "tailwind":
384
+ config = this.generateTailwindConfig(colors)
385
+ break
386
+ case "css":
387
+ config = this.generateCSSConfig(colors)
388
+ break
389
+ case "json":
390
+ config = this.generateJSONConfig(colors)
391
+ break
392
+ }
393
+
394
+ this.configPreviewTarget.value = config
395
+ }
396
+
397
+ getAllColors() {
398
+ const colors = {}
399
+ const computedStyle = getComputedStyle(document.documentElement)
400
+
401
+ this.swatchTargets.forEach(swatch => {
402
+ const colorName = swatch.dataset.color
403
+ const colorValue = computedStyle.getPropertyValue(`--color-${colorName}`).trim()
404
+ if (colorValue) {
405
+ colors[colorName] = colorValue
406
+ }
407
+ })
408
+
409
+ return colors
410
+ }
411
+
412
+ generateTailwindConfig(colors) {
413
+ let config = `/* Tailwind CSS v4 Theme Configuration */\n`
414
+ config += `/* ${this.themeValue.charAt(0).toUpperCase() + this.themeValue.slice(1)} Theme */\n\n`
415
+ config += `@theme {\n`
416
+
417
+ Object.entries(colors).forEach(([name, value]) => {
418
+ config += ` --color-${name}: ${value};\n`
419
+ })
420
+
421
+ config += `}`
422
+
423
+ return config
424
+ }
425
+
426
+ generateCSSConfig(colors) {
427
+ let config = `/* CSS Custom Properties */\n`
428
+ config += `/* ${this.themeValue.charAt(0).toUpperCase() + this.themeValue.slice(1)} Theme */\n\n`
429
+
430
+ // Light mode
431
+ config += `[data-theme="${this.themeValue}"] {\n`
432
+ Object.entries(colors).forEach(([name, value]) => {
433
+ if (!name.includes('dark')) {
434
+ config += ` --color-${name}: ${value};\n`
435
+ }
436
+ })
437
+ config += `}\n\n`
438
+
439
+ // Dark mode if applicable
440
+ const hasDarkColors = Object.keys(colors).some(name => name.includes('dark'))
441
+ if (hasDarkColors || document.documentElement.classList.contains('dark')) {
442
+ config += `[data-theme="${this.themeValue}"].dark {\n`
443
+ Object.entries(colors).forEach(([name, value]) => {
444
+ config += ` --color-${name}: ${value};\n`
445
+ })
446
+ config += `}`
447
+ }
448
+
449
+ return config
450
+ }
451
+
452
+ generateJSONConfig(colors) {
453
+ const config = {
454
+ theme: this.themeValue,
455
+ colors: colors,
456
+ format: "oklch",
457
+ generated: new Date().toISOString()
458
+ }
459
+
460
+ return JSON.stringify(config, null, 2)
461
+ }
462
+
463
+ async copyConfig() {
464
+ const config = this.configPreviewTarget.value
465
+
466
+ try {
467
+ await navigator.clipboard.writeText(config)
468
+ this.showToast("Configuration copied to clipboard!")
469
+ } catch (err) {
470
+ this.showToast("Failed to copy configuration", "error")
471
+ }
472
+ }
473
+
474
+ downloadConfig() {
475
+ const config = this.configPreviewTarget.value
476
+ const filename = `${this.themeValue}-theme.${this.getFileExtension()}`
477
+
478
+ const blob = new Blob([config], { type: 'text/plain' })
479
+ const url = URL.createObjectURL(blob)
480
+ const a = document.createElement('a')
481
+ a.href = url
482
+ a.download = filename
483
+ a.click()
484
+ URL.revokeObjectURL(url)
485
+
486
+ this.showToast(`Downloaded ${filename}`)
487
+ }
488
+
489
+ getFileExtension() {
490
+ switch (this.exportFormat) {
491
+ case "tailwind":
492
+ case "css":
493
+ return "css"
494
+ case "json":
495
+ return "json"
496
+ default:
497
+ return "txt"
498
+ }
499
+ }
500
+
501
+ loadCustomColors() {
502
+ const stored = localStorage.getItem('m9sh-custom-colors')
503
+ return stored ? JSON.parse(stored) : {}
504
+ }
505
+
506
+ applyCustomColors() {
507
+ if (!this.customColors[this.themeValue]) return
508
+
509
+ // Apply all saved custom colors for this theme
510
+ Object.entries(this.customColors[this.themeValue]).forEach(([colorName, colorValue]) => {
511
+ document.documentElement.style.setProperty(`--color-${colorName}`, colorValue)
512
+ })
513
+ }
514
+
515
+ saveCustomColors() {
516
+ localStorage.setItem('m9sh-custom-colors', JSON.stringify(this.customColors))
517
+ }
518
+
519
+ applyConfigChanges() {
520
+ const config = this.configPreviewTarget.value
521
+
522
+ try {
523
+ let colors = {}
524
+
525
+ // Parse based on current export format
526
+ if (this.exportFormat === "json") {
527
+ const parsed = JSON.parse(config)
528
+ colors = parsed.colors || {}
529
+ } else if (this.exportFormat === "tailwind" || this.exportFormat === "css") {
530
+ // Parse CSS custom properties
531
+ const matches = config.matchAll(/--color-([^:]+):\s*([^;]+);/g)
532
+ for (const match of matches) {
533
+ colors[match[1].trim()] = match[2].trim()
534
+ }
535
+ }
536
+
537
+ // Apply colors to UI
538
+ Object.entries(colors).forEach(([name, value]) => {
539
+ document.documentElement.style.setProperty(`--color-${name}`, value)
540
+
541
+ // Update custom colors storage
542
+ if (!this.customColors[this.themeValue]) {
543
+ this.customColors[this.themeValue] = {}
544
+ }
545
+ this.customColors[this.themeValue][name] = value
546
+ })
547
+
548
+ // Save to localStorage
549
+ this.saveCustomColors()
550
+
551
+ // Reload color values to update displays
552
+ this.loadColorValues()
553
+
554
+ this.showToast("Configuration applied successfully!")
555
+ } catch (err) {
556
+ console.error("Failed to parse config:", err)
557
+ this.showToast("Failed to parse configuration. Please check the format.", "error")
558
+ }
559
+ }
560
+
561
+ showToast(message, type = "success") {
562
+ // Dispatch custom event for toast notification
563
+ const event = new CustomEvent('m9sh:toast', {
564
+ detail: { message, type },
565
+ bubbles: true
566
+ })
567
+ this.element.dispatchEvent(event)
568
+ }
569
+ }
@@ -0,0 +1,120 @@
1
+ import { Controller } from "@hotwired/stimulus"
2
+
3
+ export default class extends Controller {
4
+ static themes = ["neutral", "indigo", "party", "sunset", "ocean", "forest", "cyberpunk", "thunder", "rose", "miami", "lavender", "mint", "coral", "arctic"]
5
+ static targets = ["dropdown", "currentTheme", "customizer"]
6
+
7
+ connect() {
8
+ // Initialize color theme on connect
9
+ this.updateColorTheme()
10
+ this.updateDisplay()
11
+ this.updateCustomizer()
12
+
13
+ // Close dropdown when clicking outside
14
+ this.boundCloseDropdown = this.closeDropdown.bind(this)
15
+ document.addEventListener('click', this.boundCloseDropdown)
16
+ }
17
+
18
+ disconnect() {
19
+ document.removeEventListener('click', this.boundCloseDropdown)
20
+ }
21
+
22
+ previous(event) {
23
+ event?.stopPropagation()
24
+ const currentTheme = this.getCurrentTheme()
25
+ const currentIndex = this.constructor.themes.indexOf(currentTheme)
26
+ const previousIndex = (currentIndex - 1 + this.constructor.themes.length) % this.constructor.themes.length
27
+ const newTheme = this.constructor.themes[previousIndex]
28
+
29
+ this.setColorTheme(newTheme)
30
+ this.updateDisplay()
31
+ }
32
+
33
+ next(event) {
34
+ event?.stopPropagation()
35
+ const currentTheme = this.getCurrentTheme()
36
+ const currentIndex = this.constructor.themes.indexOf(currentTheme)
37
+ const nextIndex = (currentIndex + 1) % this.constructor.themes.length
38
+ const newTheme = this.constructor.themes[nextIndex]
39
+
40
+ this.setColorTheme(newTheme)
41
+ this.updateDisplay()
42
+ }
43
+
44
+ toggleDropdown(event) {
45
+ event.stopPropagation()
46
+ if (this.hasDropdownTarget) {
47
+ this.dropdownTarget.classList.toggle('hidden')
48
+ }
49
+ }
50
+
51
+ closeDropdown(event) {
52
+ if (this.hasDropdownTarget && !this.element.contains(event.target)) {
53
+ this.dropdownTarget.classList.add('hidden')
54
+ }
55
+ }
56
+
57
+ selectTheme(event) {
58
+ event.stopPropagation()
59
+ const theme = event.currentTarget.dataset.theme
60
+ this.setColorTheme(theme)
61
+ this.updateDisplay()
62
+ this.dropdownTarget.classList.add('hidden')
63
+ }
64
+
65
+ updateDisplay() {
66
+ const currentTheme = this.getCurrentTheme()
67
+ if (this.hasCurrentThemeTarget) {
68
+ // Capitalize first letter
69
+ const displayName = currentTheme.charAt(0).toUpperCase() + currentTheme.slice(1)
70
+ this.currentThemeTarget.textContent = displayName
71
+ }
72
+ }
73
+
74
+ getCurrentTheme() {
75
+ return localStorage.getItem('colorTheme') || 'neutral'
76
+ }
77
+
78
+ setColorTheme(theme) {
79
+ localStorage.setItem('colorTheme', theme)
80
+ this.applyColorTheme(theme)
81
+ this.updateCustomizer()
82
+ }
83
+
84
+ updateColorTheme() {
85
+ const theme = this.getCurrentTheme()
86
+ this.applyColorTheme(theme)
87
+ }
88
+
89
+ updateCustomizer() {
90
+ if (this.hasCustomizerTarget) {
91
+ const currentTheme = this.getCurrentTheme()
92
+ const customizerElement = this.customizerTarget.querySelector('[data-controller="color-customizer"]')
93
+
94
+ if (customizerElement) {
95
+ const controller = this.application.getControllerForElementAndIdentifier(customizerElement, 'color-customizer')
96
+ if (controller) {
97
+ controller.themeValue = currentTheme
98
+ controller.loadColorValues()
99
+ controller.generateConfig()
100
+ }
101
+ }
102
+ }
103
+ }
104
+
105
+ applyColorTheme(theme) {
106
+ const html = document.documentElement
107
+
108
+ // Remove all theme data attributes
109
+ this.constructor.themes.forEach(t => {
110
+ if (t !== 'neutral') {
111
+ html.removeAttribute('data-theme')
112
+ }
113
+ })
114
+
115
+ // Apply new theme (neutral has no data attribute)
116
+ if (theme !== 'neutral') {
117
+ html.setAttribute('data-theme', theme)
118
+ }
119
+ }
120
+ }