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.
- checksums.yaml +4 -4
- data/.idea/hotcdn.iml +30 -0
- data/.mise.toml +2 -2
- data/app/assets/config/manifest.js +4 -0
- data/app/assets/images/icons/activity.svg +3 -0
- data/app/assets/images/icons/bell.svg +4 -0
- data/app/assets/images/icons/book.svg +4 -0
- data/app/assets/images/icons/chevron-down.svg +3 -0
- data/app/assets/images/icons/chevron-left.svg +3 -0
- data/app/assets/images/icons/chevron-right.svg +3 -0
- data/app/assets/images/icons/credit-card.svg +4 -0
- data/app/assets/images/icons/dollar-sign.svg +3 -0
- data/app/assets/images/icons/edit.svg +4 -0
- data/app/assets/images/icons/github.svg +3 -0
- data/app/assets/images/icons/home.svg +4 -0
- data/app/assets/images/icons/info.svg +5 -0
- data/app/assets/images/icons/layout.svg +6 -0
- data/app/assets/images/icons/logout.svg +5 -0
- data/app/assets/images/icons/menu.svg +5 -0
- data/app/assets/images/icons/moon.svg +3 -0
- data/app/assets/images/icons/paintbrush.svg +6 -0
- data/app/assets/images/icons/search.svg +4 -0
- data/app/assets/images/icons/settings.svg +4 -0
- data/app/assets/images/icons/sun.svg +11 -0
- data/app/assets/images/icons/user.svg +4 -0
- data/app/assets/images/icons/users.svg +5 -0
- data/app/assets/stylesheets/tailwind.css +1180 -0
- data/app/components/backdrop_component.rb +103 -0
- data/app/components/docs/code_block_component.rb +56 -0
- data/app/components/docs/component_api_component.rb +16 -0
- data/app/components/docs/component_examples_component.rb +16 -0
- data/app/components/docs/component_header_component.html.erb +8 -0
- data/app/components/docs/component_header_component.rb +14 -0
- data/app/components/docs/component_installation_component.html.erb +15 -0
- data/app/components/docs/component_installation_component.rb +13 -0
- data/app/components/docs/component_page_component.html.erb +9 -0
- data/app/components/docs/component_page_component.rb +19 -0
- data/app/components/docs/component_preview_component.rb +318 -0
- data/app/components/docs/component_usage_component.rb +18 -0
- data/app/components/docs/prop_table_component.rb +64 -0
- data/app/controllers/application_controller.rb +3 -0
- data/app/controllers/blocks_controller.rb +51 -0
- data/app/controllers/docs_controller.rb +162 -0
- data/app/controllers/showcase_controller.rb +42 -0
- data/app/helpers/blocks_helper.rb +343 -0
- data/app/helpers/docs_helper.rb +3807 -0
- data/app/helpers/m9sh/toast_helper.rb +46 -0
- data/app/helpers/m9sh_helper.rb +343 -0
- data/app/javascript/application.js +3 -0
- data/app/javascript/controllers/application.js +9 -0
- data/app/javascript/controllers/backdrop_controller.js +137 -0
- data/app/javascript/controllers/color_customizer_controller.js +569 -0
- data/app/javascript/controllers/color_theme_controller.js +120 -0
- data/app/javascript/controllers/docs/component_preview_controller.js +149 -0
- data/app/javascript/controllers/docs/copy_button_controller.js +20 -0
- data/app/javascript/controllers/index.js +6 -0
- data/app/javascript/controllers/theme_controller.js +23 -0
- data/app/views/blocks/_sidebar.html.erb +31 -0
- data/app/views/blocks/_toc.html.erb +29 -0
- data/app/views/blocks/examples/dashboard-01.html.erb +180 -0
- data/app/views/blocks/examples/dashboard-02.html.erb +190 -0
- data/app/views/blocks/examples/dashboard-03.html.erb +210 -0
- data/app/views/blocks/examples/settings-01.html.erb +220 -0
- data/app/views/blocks/examples/settings-02.html.erb +231 -0
- data/app/views/blocks/examples/settings-03.html.erb +340 -0
- data/app/views/blocks/index.html.erb +65 -0
- data/app/views/docs/_sidebar.html.erb +47 -0
- data/app/views/docs/_toc.html.erb +19 -0
- data/app/views/docs/about.html.erb +68 -0
- data/app/views/docs/components/accordion.html.erb +196 -0
- data/app/views/docs/components/alert.html.erb +272 -0
- data/app/views/docs/components/alert_dialog.html.erb +232 -0
- data/app/views/docs/components/avatar.html.erb +207 -0
- data/app/views/docs/components/badge.html.erb +145 -0
- data/app/views/docs/components/breadcrumb.html.erb +264 -0
- data/app/views/docs/components/button.html.erb +229 -0
- data/app/views/docs/components/card.html.erb +378 -0
- data/app/views/docs/components/checkbox.html.erb +212 -0
- data/app/views/docs/components/collapsible.html.erb +252 -0
- data/app/views/docs/components/dialog.html.erb +323 -0
- data/app/views/docs/components/dropdown_menu.html.erb +289 -0
- data/app/views/docs/components/hover_card.html.erb +220 -0
- data/app/views/docs/components/input.html.erb +254 -0
- data/app/views/docs/components/label.html.erb +128 -0
- data/app/views/docs/components/main.html.erb +352 -0
- data/app/views/docs/components/navbar.html.erb +394 -0
- data/app/views/docs/components/navigation_menu.html.erb +226 -0
- data/app/views/docs/components/popover.html.erb +267 -0
- data/app/views/docs/components/progress.html.erb +107 -0
- data/app/views/docs/components/radio_group.html.erb +209 -0
- data/app/views/docs/components/select.html.erb +260 -0
- data/app/views/docs/components/separator.html.erb +162 -0
- data/app/views/docs/components/sheet.html.erb +270 -0
- data/app/views/docs/components/sidebar.html.erb +597 -0
- data/app/views/docs/components/skeleton.html.erb +150 -0
- data/app/views/docs/components/slider.html.erb +218 -0
- data/app/views/docs/components/spinner.html.erb +132 -0
- data/app/views/docs/components/switch.html.erb +148 -0
- data/app/views/docs/components/table.html.erb +259 -0
- data/app/views/docs/components/tabs.html.erb +225 -0
- data/app/views/docs/components/textarea.html.erb +239 -0
- data/app/views/docs/components/theme_toggle.html.erb +135 -0
- data/app/views/docs/components/toast.html.erb +205 -0
- data/app/views/docs/components/toaster.html.erb +227 -0
- data/app/views/docs/components/toggle.html.erb +154 -0
- data/app/views/docs/components/tooltip.html.erb +216 -0
- data/app/views/docs/components/typography.html.erb +180 -0
- data/app/views/docs/index.html.erb +143 -0
- data/app/views/docs/installation.html.erb +155 -0
- data/app/views/docs/simple_test.html.erb +13 -0
- data/app/views/docs/test_accordion.html.erb +14 -0
- data/app/views/docs/usage.html.erb +272 -0
- data/app/views/layouts/application.html.erb +107 -0
- data/app/views/layouts/backdrop.html.erb +77 -0
- data/app/views/shared/_app_navbar.html.erb +240 -0
- data/app/views/shared/_navbar.html.erb +69 -0
- data/app/views/showcase/v2/_components_grid.html.erb +38 -0
- data/app/views/showcase/v2/_features.html.erb +59 -0
- data/app/views/showcase/v2/_forms.html.erb +195 -0
- data/app/views/showcase/v2/_hero.html.erb +55 -0
- data/app/views/showcase/v2/_metrics.html.erb +107 -0
- data/app/views/showcase/v2.html.erb +18 -0
- data/lib/m9sh/version.rb +1 -1
- data/m9sh.gemspec +1 -1
- 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
|
+
}
|