m9sh 0.1.0 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/Dockerfile +2 -1
- data/GEM_README.md +284 -0
- data/LICENSE.txt +21 -0
- data/M9SH_CLI.md +453 -0
- data/PUBLISHING.md +331 -0
- data/README.md +120 -52
- data/app/components/m9sh/accordion_component.rb +3 -3
- data/app/components/m9sh/alert_component.rb +7 -9
- data/app/components/m9sh/base_component.rb +1 -0
- data/app/components/m9sh/button_component.rb +3 -2
- data/app/components/m9sh/color_customizer_component.rb +624 -0
- data/app/components/m9sh/dialog_close_component.rb +30 -0
- data/app/components/m9sh/dialog_component.rb +11 -99
- data/app/components/m9sh/dialog_content_component.rb +102 -0
- data/app/components/m9sh/dialog_description_component.rb +14 -0
- data/app/components/m9sh/dialog_footer_component.rb +14 -0
- data/app/components/m9sh/dialog_header_component.rb +27 -0
- data/app/components/m9sh/dialog_title_component.rb +14 -0
- data/app/components/m9sh/dialog_trigger_component.rb +23 -0
- data/app/components/m9sh/dropdown_menu_content_component.rb +1 -1
- data/app/components/m9sh/dropdown_menu_item_component.rb +1 -1
- data/app/components/m9sh/dropdown_menu_trigger_component.rb +1 -1
- data/app/components/m9sh/icon_component.rb +78 -0
- data/app/components/m9sh/main_component.rb +1 -1
- data/app/components/m9sh/menu_component.rb +85 -0
- data/app/components/m9sh/navbar_component.rb +186 -0
- data/app/components/m9sh/navigation_menu_component.rb +2 -2
- data/app/components/m9sh/popover_component.rb +12 -7
- data/app/components/m9sh/radio_group_component.rb +45 -13
- data/app/components/m9sh/sheet_component.rb +6 -6
- data/app/components/m9sh/sidebar_component.rb +6 -1
- data/app/components/m9sh/skeleton_component.rb +7 -1
- data/app/components/m9sh/tabs_component.rb +76 -48
- data/app/components/m9sh/textarea_component.rb +1 -1
- data/app/components/m9sh/theme_toggle_component.rb +1 -0
- data/app/javascript/controllers/m9sh/popover_controller.js +24 -18
- data/app/javascript/controllers/m9sh/sidebar_controller.js +29 -7
- data/lib/m9sh/config.rb +5 -5
- data/lib/m9sh/registry.rb +2 -2
- data/lib/m9sh/registry.yml +37 -0
- data/lib/m9sh/version.rb +1 -1
- data/lib/tasks/tailwindcss.rake +15 -0
- data/m9sh.gemspec +48 -0
- data/publish.sh +48 -0
- metadata +20 -3
- data/fix_namespaces.py +0 -32
@@ -0,0 +1,624 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module M9sh
|
4
|
+
class ColorCustomizerComponent < BaseComponent
|
5
|
+
include Utilities
|
6
|
+
|
7
|
+
SEMANTIC_COLORS = {
|
8
|
+
'Core' => %w[background foreground],
|
9
|
+
'Components' => %w[card card-foreground popover popover-foreground],
|
10
|
+
'Interactive' => %w[primary primary-foreground secondary secondary-foreground],
|
11
|
+
'State' => %w[muted muted-foreground accent accent-foreground],
|
12
|
+
'Feedback' => %w[destructive destructive-foreground],
|
13
|
+
'Form' => %w[border input ring],
|
14
|
+
'Sidebar' => %w[sidebar-background sidebar-foreground sidebar-primary sidebar-primary-foreground
|
15
|
+
sidebar-accent sidebar-accent-foreground sidebar-border sidebar-ring]
|
16
|
+
}.freeze
|
17
|
+
|
18
|
+
def initialize(theme_name: 'neutral', **extra_attrs)
|
19
|
+
@theme_name = theme_name
|
20
|
+
super(**extra_attrs)
|
21
|
+
end
|
22
|
+
|
23
|
+
def call
|
24
|
+
tag.div(
|
25
|
+
**component_attrs("h-full flex flex-col overflow-hidden"),
|
26
|
+
data: {
|
27
|
+
controller: "color-customizer m9sh--tabs",
|
28
|
+
color_customizer_theme_value: @theme_name,
|
29
|
+
m9sh__tabs_default_value: "preview"
|
30
|
+
}
|
31
|
+
) do
|
32
|
+
safe_join([
|
33
|
+
render_header,
|
34
|
+
render_content,
|
35
|
+
render_footer
|
36
|
+
])
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
private
|
41
|
+
|
42
|
+
def render_header
|
43
|
+
tag.div(class: "border-b border-border p-6") do
|
44
|
+
safe_join([
|
45
|
+
tag.h2(class: "text-2xl font-semibold") do
|
46
|
+
"Theme Customizer"
|
47
|
+
end,
|
48
|
+
tag.p(class: "text-sm text-muted-foreground mt-2") do
|
49
|
+
"Preview colors, adjust values, and export your custom theme configuration"
|
50
|
+
end,
|
51
|
+
tag.div(class: "mt-4") do
|
52
|
+
render_tab_list
|
53
|
+
end
|
54
|
+
])
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
def render_content
|
59
|
+
tag.div(class: "flex-1 overflow-hidden p-6 flex flex-col") do
|
60
|
+
render_tab_panels
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
def render_tab_list
|
65
|
+
tag.div(
|
66
|
+
class: "inline-flex h-9 items-center justify-center rounded-lg bg-muted p-1 text-muted-foreground",
|
67
|
+
role: "tablist",
|
68
|
+
data: { m9sh__tabs_target: "list" }
|
69
|
+
) do
|
70
|
+
safe_join([
|
71
|
+
render_tab_button("preview", "Color Preview", true),
|
72
|
+
render_tab_button("export", "Export Config", false)
|
73
|
+
])
|
74
|
+
end
|
75
|
+
end
|
76
|
+
|
77
|
+
def render_tab_button(value, label, selected)
|
78
|
+
tag.button(
|
79
|
+
label,
|
80
|
+
type: "button",
|
81
|
+
role: "tab",
|
82
|
+
class: "inline-flex items-center justify-center whitespace-nowrap rounded-md px-2.5 py-1 text-xs font-medium ring-offset-background transition-all focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 data-[state=active]:bg-background data-[state=active]:text-foreground data-[state=active]:shadow",
|
83
|
+
data: {
|
84
|
+
m9sh__tabs_target: "trigger",
|
85
|
+
action: "click->m9sh--tabs#selectTab",
|
86
|
+
value: value
|
87
|
+
},
|
88
|
+
"aria-selected": selected ? "true" : "false"
|
89
|
+
)
|
90
|
+
end
|
91
|
+
|
92
|
+
def render_tab_panels
|
93
|
+
tag.div(class: "flex-1 overflow-hidden w-full mx-1") do
|
94
|
+
safe_join([
|
95
|
+
render_tab_panel("preview", render_color_preview, true),
|
96
|
+
render_tab_panel("export", render_export_section, false)
|
97
|
+
])
|
98
|
+
end
|
99
|
+
end
|
100
|
+
|
101
|
+
def render_tab_panel(value, content, visible)
|
102
|
+
tag.div(
|
103
|
+
content,
|
104
|
+
role: "tabpanel",
|
105
|
+
class: "ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 overflow-visible h-full",
|
106
|
+
data: {
|
107
|
+
m9sh__tabs_target: "panel",
|
108
|
+
value: value
|
109
|
+
},
|
110
|
+
style: visible ? "" : "display: none;"
|
111
|
+
)
|
112
|
+
end
|
113
|
+
|
114
|
+
def render_color_preview
|
115
|
+
tag.div(class: "grid md:grid-cols-2 gap-8 h-full") do
|
116
|
+
safe_join([
|
117
|
+
# Left column: Component preview
|
118
|
+
tag.div(class: "space-y-4 overflow-y-auto overflow-x-visible pr-2") do
|
119
|
+
safe_join([
|
120
|
+
tag.h3(class: "text-sm font-semibold text-muted-foreground uppercase tracking-wide sticky top-0 bg-background pb-3 -mx-2 px-2 z-10") { "Component Preview" },
|
121
|
+
render_component_preview
|
122
|
+
])
|
123
|
+
end,
|
124
|
+
# Right column: Color swatches
|
125
|
+
tag.div(class: "space-y-6 overflow-y-auto overflow-x-visible pr-2") do
|
126
|
+
safe_join([
|
127
|
+
tag.h3(class: "text-sm font-semibold text-muted-foreground uppercase tracking-wide sticky top-0 bg-background pb-3 -mx-2 px-2 z-10 mb-4") { "Color Palette" },
|
128
|
+
*SEMANTIC_COLORS.map do |category, colors|
|
129
|
+
render_color_category(category, colors)
|
130
|
+
end
|
131
|
+
])
|
132
|
+
end
|
133
|
+
])
|
134
|
+
end
|
135
|
+
end
|
136
|
+
|
137
|
+
def render_component_preview
|
138
|
+
tag.div(class: "space-y-6") do
|
139
|
+
safe_join([
|
140
|
+
# Buttons
|
141
|
+
tag.div(class: "space-y-2") do
|
142
|
+
safe_join([
|
143
|
+
tag.h4(class: "text-xs font-semibold text-muted-foreground") { "Buttons" },
|
144
|
+
tag.div(class: "flex flex-wrap gap-2") do
|
145
|
+
safe_join([
|
146
|
+
render(M9sh::ButtonComponent.new(variant: :default, size: :sm)) { "Primary" },
|
147
|
+
render(M9sh::ButtonComponent.new(variant: :secondary, size: :sm)) { "Secondary" },
|
148
|
+
render(M9sh::ButtonComponent.new(variant: :outline, size: :sm)) { "Outline" },
|
149
|
+
render(M9sh::ButtonComponent.new(variant: :destructive, size: :sm)) { "Destructive" },
|
150
|
+
render(M9sh::ButtonComponent.new(variant: :ghost, size: :sm)) { "Ghost" }
|
151
|
+
])
|
152
|
+
end
|
153
|
+
])
|
154
|
+
end,
|
155
|
+
# Card
|
156
|
+
tag.div(class: "space-y-2") do
|
157
|
+
safe_join([
|
158
|
+
tag.h4(class: "text-xs font-semibold text-muted-foreground") { "Card" },
|
159
|
+
tag.div(class: "p-4 rounded-lg border border-border bg-card") do
|
160
|
+
safe_join([
|
161
|
+
tag.h3(class: "font-semibold text-card-foreground") { "Card Title" },
|
162
|
+
tag.p(class: "text-sm text-muted-foreground mt-1") { "Card description text" }
|
163
|
+
])
|
164
|
+
end
|
165
|
+
])
|
166
|
+
end,
|
167
|
+
# Alerts
|
168
|
+
tag.div(class: "space-y-2") do
|
169
|
+
safe_join([
|
170
|
+
tag.h4(class: "text-xs font-semibold text-muted-foreground") { "Alerts" },
|
171
|
+
tag.div(class: "rounded-lg border border-border bg-background p-3") do
|
172
|
+
safe_join([
|
173
|
+
tag.h5(class: "font-medium text-sm") { "Information" },
|
174
|
+
tag.p(class: "text-sm text-muted-foreground mt-1") { "This is an informational alert" }
|
175
|
+
])
|
176
|
+
end,
|
177
|
+
tag.div(class: "rounded-lg border border-destructive/50 bg-destructive/10 p-3") do
|
178
|
+
safe_join([
|
179
|
+
tag.h5(class: "font-medium text-sm text-destructive") { "Error" },
|
180
|
+
tag.p(class: "text-sm text-destructive/90 mt-1") { "This is an error alert" }
|
181
|
+
])
|
182
|
+
end
|
183
|
+
])
|
184
|
+
end,
|
185
|
+
# Form elements
|
186
|
+
tag.div(class: "space-y-2") do
|
187
|
+
safe_join([
|
188
|
+
tag.h4(class: "text-xs font-semibold text-muted-foreground") { "Form Elements" },
|
189
|
+
tag.input(
|
190
|
+
type: "text",
|
191
|
+
placeholder: "Input field",
|
192
|
+
class: "flex h-9 w-full rounded-md border border-input bg-background px-3 py-1 text-sm ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
|
193
|
+
),
|
194
|
+
tag.div(class: "flex items-center space-x-2 mt-2") do
|
195
|
+
safe_join([
|
196
|
+
tag.input(
|
197
|
+
type: "checkbox",
|
198
|
+
id: "preview-checkbox",
|
199
|
+
class: "h-4 w-4 rounded border-input accent-primary"
|
200
|
+
),
|
201
|
+
tag.label(
|
202
|
+
for: "preview-checkbox",
|
203
|
+
class: "text-sm font-medium leading-none"
|
204
|
+
) { "Checkbox option" }
|
205
|
+
])
|
206
|
+
end
|
207
|
+
])
|
208
|
+
end,
|
209
|
+
# Badges
|
210
|
+
tag.div(class: "space-y-2") do
|
211
|
+
safe_join([
|
212
|
+
tag.h4(class: "text-xs font-semibold text-muted-foreground") { "Badges" },
|
213
|
+
tag.div(class: "flex flex-wrap gap-2") do
|
214
|
+
safe_join([
|
215
|
+
tag.span(class: "inline-flex items-center rounded-full bg-primary px-2.5 py-0.5 text-xs font-semibold text-primary-foreground") { "Primary" },
|
216
|
+
tag.span(class: "inline-flex items-center rounded-full bg-secondary px-2.5 py-0.5 text-xs font-semibold text-secondary-foreground") { "Secondary" },
|
217
|
+
tag.span(class: "inline-flex items-center rounded-full bg-muted px-2.5 py-0.5 text-xs font-semibold text-muted-foreground") { "Muted" },
|
218
|
+
tag.span(class: "inline-flex items-center rounded-full bg-accent px-2.5 py-0.5 text-xs font-semibold text-accent-foreground") { "Accent" }
|
219
|
+
])
|
220
|
+
end
|
221
|
+
])
|
222
|
+
end,
|
223
|
+
# Progress & Status
|
224
|
+
tag.div(class: "space-y-2") do
|
225
|
+
safe_join([
|
226
|
+
tag.h4(class: "text-xs font-semibold text-muted-foreground") { "Progress" },
|
227
|
+
tag.div(class: "w-full bg-secondary rounded-full h-2") do
|
228
|
+
tag.div(class: "bg-primary h-2 rounded-full", style: "width: 60%")
|
229
|
+
end
|
230
|
+
])
|
231
|
+
end,
|
232
|
+
# Navigation
|
233
|
+
tag.div(class: "space-y-2") do
|
234
|
+
safe_join([
|
235
|
+
tag.h4(class: "text-xs font-semibold text-muted-foreground") { "Navigation" },
|
236
|
+
tag.nav(class: "flex space-x-4") do
|
237
|
+
safe_join([
|
238
|
+
tag.a(href: "#", class: "text-sm font-medium text-foreground hover:text-primary") { "Active" },
|
239
|
+
tag.a(href: "#", class: "text-sm font-medium text-muted-foreground hover:text-foreground") { "Inactive" },
|
240
|
+
tag.a(href: "#", class: "text-sm font-medium text-muted-foreground hover:text-foreground") { "Link" }
|
241
|
+
])
|
242
|
+
end
|
243
|
+
])
|
244
|
+
end,
|
245
|
+
# Popover/Dialog preview
|
246
|
+
tag.div(class: "space-y-2") do
|
247
|
+
safe_join([
|
248
|
+
tag.h4(class: "text-xs font-semibold text-muted-foreground") { "Popover & Dialog" },
|
249
|
+
tag.div(class: "p-4 rounded-lg border border-border bg-popover shadow-md") do
|
250
|
+
safe_join([
|
251
|
+
tag.h4(class: "font-medium text-sm text-popover-foreground") { "Popover Title" },
|
252
|
+
tag.p(class: "text-sm text-muted-foreground mt-1") { "Popover content with popover colors" }
|
253
|
+
])
|
254
|
+
end
|
255
|
+
])
|
256
|
+
end,
|
257
|
+
# Table preview
|
258
|
+
tag.div(class: "space-y-2") do
|
259
|
+
safe_join([
|
260
|
+
tag.h4(class: "text-xs font-semibold text-muted-foreground") { "Table" },
|
261
|
+
tag.div(class: "rounded-lg border border-border overflow-hidden") do
|
262
|
+
tag.table(class: "w-full text-sm") do
|
263
|
+
safe_join([
|
264
|
+
tag.thead(class: "bg-muted/50") do
|
265
|
+
tag.tr do
|
266
|
+
safe_join([
|
267
|
+
tag.th(class: "px-3 py-2 text-left font-medium text-muted-foreground") { "Name" },
|
268
|
+
tag.th(class: "px-3 py-2 text-left font-medium text-muted-foreground") { "Status" }
|
269
|
+
])
|
270
|
+
end
|
271
|
+
end,
|
272
|
+
tag.tbody do
|
273
|
+
safe_join([
|
274
|
+
tag.tr(class: "border-t border-border") do
|
275
|
+
safe_join([
|
276
|
+
tag.td(class: "px-3 py-2") { "Item 1" },
|
277
|
+
tag.td(class: "px-3 py-2") do
|
278
|
+
tag.span(class: "text-xs bg-primary/10 text-primary px-2 py-0.5 rounded") { "Active" }
|
279
|
+
end
|
280
|
+
])
|
281
|
+
end,
|
282
|
+
tag.tr(class: "border-t border-border bg-muted/20") do
|
283
|
+
safe_join([
|
284
|
+
tag.td(class: "px-3 py-2") { "Item 2" },
|
285
|
+
tag.td(class: "px-3 py-2") do
|
286
|
+
tag.span(class: "text-xs bg-secondary/50 text-secondary-foreground px-2 py-0.5 rounded") { "Pending" }
|
287
|
+
end
|
288
|
+
])
|
289
|
+
end
|
290
|
+
])
|
291
|
+
end
|
292
|
+
])
|
293
|
+
end
|
294
|
+
end
|
295
|
+
])
|
296
|
+
end,
|
297
|
+
# Select/Dropdown preview
|
298
|
+
tag.div(class: "space-y-2") do
|
299
|
+
safe_join([
|
300
|
+
tag.h4(class: "text-xs font-semibold text-muted-foreground") { "Select" },
|
301
|
+
tag.select(
|
302
|
+
class: "flex h-9 w-full rounded-md border border-input bg-background px-3 py-1 text-sm ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
|
303
|
+
) do
|
304
|
+
safe_join([
|
305
|
+
tag.option { "Option 1" },
|
306
|
+
tag.option { "Option 2" },
|
307
|
+
tag.option { "Option 3" }
|
308
|
+
])
|
309
|
+
end
|
310
|
+
])
|
311
|
+
end,
|
312
|
+
# Textarea preview
|
313
|
+
tag.div(class: "space-y-2") do
|
314
|
+
safe_join([
|
315
|
+
tag.h4(class: "text-xs font-semibold text-muted-foreground") { "Textarea" },
|
316
|
+
tag.textarea(
|
317
|
+
placeholder: "Enter your message...",
|
318
|
+
rows: 3,
|
319
|
+
class: "flex w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
|
320
|
+
)
|
321
|
+
])
|
322
|
+
end,
|
323
|
+
# Switch/Toggle preview
|
324
|
+
tag.div(class: "space-y-2") do
|
325
|
+
safe_join([
|
326
|
+
tag.h4(class: "text-xs font-semibold text-muted-foreground") { "Toggle" },
|
327
|
+
tag.div(class: "flex items-center space-x-2") do
|
328
|
+
safe_join([
|
329
|
+
tag.button(
|
330
|
+
role: "switch",
|
331
|
+
class: "relative inline-flex h-5 w-9 shrink-0 cursor-pointer rounded-full border-2 border-transparent bg-primary transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2"
|
332
|
+
) do
|
333
|
+
tag.span(class: "pointer-events-none block h-4 w-4 rounded-full bg-background shadow-lg transform translate-x-4 transition-transform")
|
334
|
+
end,
|
335
|
+
tag.label(class: "text-sm font-medium") { "Enable feature" }
|
336
|
+
])
|
337
|
+
end
|
338
|
+
])
|
339
|
+
end,
|
340
|
+
# Code block preview
|
341
|
+
tag.div(class: "space-y-2") do
|
342
|
+
safe_join([
|
343
|
+
tag.h4(class: "text-xs font-semibold text-muted-foreground") { "Code Block" },
|
344
|
+
tag.pre(class: "p-3 rounded-lg bg-muted text-sm font-mono overflow-x-auto") do
|
345
|
+
tag.code(class: "text-foreground") { "const theme = \"oklch\";\nconsole.log(theme);" }
|
346
|
+
end
|
347
|
+
])
|
348
|
+
end
|
349
|
+
])
|
350
|
+
end
|
351
|
+
end
|
352
|
+
|
353
|
+
def render_color_category(category, colors)
|
354
|
+
tag.div(class: "space-y-3") do
|
355
|
+
safe_join([
|
356
|
+
tag.h3(class: "text-sm font-semibold text-muted-foreground uppercase tracking-wide") { category },
|
357
|
+
tag.div(class: "grid grid-cols-2 gap-3") do
|
358
|
+
safe_join(
|
359
|
+
colors.map do |color|
|
360
|
+
render_color_swatch(color)
|
361
|
+
end
|
362
|
+
)
|
363
|
+
end
|
364
|
+
])
|
365
|
+
end
|
366
|
+
end
|
367
|
+
|
368
|
+
def render_color_swatch(color)
|
369
|
+
render(M9sh::PopoverComponent.new(width: "w-80", align: "end", side: "bottom")) do |popover|
|
370
|
+
popover.with_trigger do
|
371
|
+
tag.div(
|
372
|
+
class: "flex items-center gap-3 p-3 rounded-lg border border-border hover:bg-accent/50 cursor-pointer transition-colors",
|
373
|
+
data: {
|
374
|
+
action: "click->color-customizer#selectColor",
|
375
|
+
color_customizer_color_param: color
|
376
|
+
}
|
377
|
+
) do
|
378
|
+
safe_join([
|
379
|
+
tag.div(
|
380
|
+
class: "w-12 h-12 rounded-md border border-border",
|
381
|
+
style: "background-color: var(--color-#{color})",
|
382
|
+
data: { color_customizer_target: "swatch", color: color }
|
383
|
+
),
|
384
|
+
tag.div(class: "flex-1 min-w-0") do
|
385
|
+
safe_join([
|
386
|
+
tag.p(class: "text-sm font-medium truncate") { color },
|
387
|
+
tag.p(
|
388
|
+
class: "text-xs text-muted-foreground font-mono truncate",
|
389
|
+
data: { color_customizer_target: "colorValue", color: color }
|
390
|
+
) { "Loading..." }
|
391
|
+
])
|
392
|
+
end
|
393
|
+
])
|
394
|
+
end
|
395
|
+
end
|
396
|
+
|
397
|
+
popover.with_popover_content do
|
398
|
+
tag.div(class: "space-y-3") do
|
399
|
+
safe_join([
|
400
|
+
tag.div(class: "space-y-1") do
|
401
|
+
safe_join([
|
402
|
+
tag.h4(class: "font-semibold text-sm") { color },
|
403
|
+
tag.p(
|
404
|
+
class: "text-xs text-muted-foreground font-mono",
|
405
|
+
data: { color_customizer_target: "selectedValue" }
|
406
|
+
) { "Loading..." }
|
407
|
+
])
|
408
|
+
end,
|
409
|
+
tag.div(class: "space-y-2", data: { color_customizer_target: "sliders" }) do
|
410
|
+
safe_join([
|
411
|
+
render_slider_control("Lightness", "lightness", 0, 100, 50),
|
412
|
+
render_slider_control("Chroma", "chroma", 0, 40, 20),
|
413
|
+
render_slider_control("Hue", "hue", 0, 360, 180)
|
414
|
+
])
|
415
|
+
end,
|
416
|
+
tag.div(class: "pt-1") do
|
417
|
+
render(M9sh::ButtonComponent.new(
|
418
|
+
variant: :outline,
|
419
|
+
size: :sm,
|
420
|
+
class: "w-full h-8",
|
421
|
+
data: { action: "click->color-customizer#resetColor" }
|
422
|
+
)) { "Reset to Default" }
|
423
|
+
end
|
424
|
+
])
|
425
|
+
end
|
426
|
+
end
|
427
|
+
end
|
428
|
+
end
|
429
|
+
|
430
|
+
def render_color_adjuster
|
431
|
+
tag.div(class: "space-y-6") do
|
432
|
+
safe_join([
|
433
|
+
render_selected_color_display,
|
434
|
+
render_adjustment_sliders,
|
435
|
+
render_adjustment_actions
|
436
|
+
])
|
437
|
+
end
|
438
|
+
end
|
439
|
+
|
440
|
+
def render_selected_color_display
|
441
|
+
tag.div(class: "p-4 rounded-lg border border-border bg-card") do
|
442
|
+
safe_join([
|
443
|
+
tag.div(class: "flex items-center gap-4 mb-4") do
|
444
|
+
safe_join([
|
445
|
+
tag.div(
|
446
|
+
class: "w-20 h-20 rounded-lg border-2 border-border",
|
447
|
+
data: { color_customizer_target: "selectedSwatch" }
|
448
|
+
),
|
449
|
+
tag.div(class: "flex-1") do
|
450
|
+
safe_join([
|
451
|
+
tag.h3(
|
452
|
+
class: "text-lg font-semibold",
|
453
|
+
data: { color_customizer_target: "selectedName" }
|
454
|
+
) { "Select a color" },
|
455
|
+
tag.p(
|
456
|
+
class: "text-sm text-muted-foreground font-mono",
|
457
|
+
data: { color_customizer_target: "selectedValue" }
|
458
|
+
) { "Click on a color swatch to adjust" }
|
459
|
+
])
|
460
|
+
end
|
461
|
+
])
|
462
|
+
end
|
463
|
+
])
|
464
|
+
end
|
465
|
+
end
|
466
|
+
|
467
|
+
def render_adjustment_sliders
|
468
|
+
tag.div(class: "space-y-4", data: { color_customizer_target: "sliders" }) do
|
469
|
+
safe_join([
|
470
|
+
render_slider_control("Lightness", "lightness", 0, 100, 50),
|
471
|
+
render_slider_control("Chroma", "chroma", 0, 40, 20),
|
472
|
+
render_slider_control("Hue", "hue", 0, 360, 180)
|
473
|
+
])
|
474
|
+
end
|
475
|
+
end
|
476
|
+
|
477
|
+
def render_slider_control(label, param, min, max, value)
|
478
|
+
tag.div(class: "space-y-1") do
|
479
|
+
safe_join([
|
480
|
+
tag.div(class: "flex items-center justify-between") do
|
481
|
+
safe_join([
|
482
|
+
tag.label(class: "text-xs font-medium") { label },
|
483
|
+
tag.span(
|
484
|
+
class: "text-xs font-mono text-muted-foreground",
|
485
|
+
data: { color_customizer_target: "#{param}Value" }
|
486
|
+
) { value.to_s }
|
487
|
+
])
|
488
|
+
end,
|
489
|
+
tag.div(
|
490
|
+
data: {
|
491
|
+
color_customizer_target: "#{param}Slider",
|
492
|
+
action: "slider-change->color-customizer#adjustColor"
|
493
|
+
}
|
494
|
+
) do
|
495
|
+
render(M9sh::SliderComponent.new(
|
496
|
+
min: min,
|
497
|
+
max: max,
|
498
|
+
value: value,
|
499
|
+
step: param == "lightness" ? 1 : (param == "chroma" ? 1 : 1)
|
500
|
+
))
|
501
|
+
end
|
502
|
+
])
|
503
|
+
end
|
504
|
+
end
|
505
|
+
|
506
|
+
def render_adjustment_actions
|
507
|
+
tag.div(class: "flex gap-3") do
|
508
|
+
safe_join([
|
509
|
+
render(M9sh::ButtonComponent.new(
|
510
|
+
variant: :outline,
|
511
|
+
size: :sm,
|
512
|
+
data: { action: "click->color-customizer#resetColor" }
|
513
|
+
)) { "Reset to Default" },
|
514
|
+
render(M9sh::ButtonComponent.new(
|
515
|
+
variant: :default,
|
516
|
+
size: :sm,
|
517
|
+
data: { action: "click->color-customizer#applyColor" }
|
518
|
+
)) { "Apply Changes" }
|
519
|
+
])
|
520
|
+
end
|
521
|
+
end
|
522
|
+
|
523
|
+
def render_export_section
|
524
|
+
tag.div(class: "h-full overflow-y-auto pr-2 flex flex-col gap-6") do
|
525
|
+
safe_join([
|
526
|
+
render_export_format_selector,
|
527
|
+
render_config_preview,
|
528
|
+
render_export_actions
|
529
|
+
])
|
530
|
+
end
|
531
|
+
end
|
532
|
+
|
533
|
+
def render_export_format_selector
|
534
|
+
tag.div(class: "space-y-3") do
|
535
|
+
safe_join([
|
536
|
+
tag.h3(class: "text-sm font-semibold") { "Export Format" },
|
537
|
+
tag.div(class: "flex gap-2") do
|
538
|
+
safe_join([
|
539
|
+
render(M9sh::ButtonComponent.new(
|
540
|
+
variant: :outline,
|
541
|
+
size: :sm,
|
542
|
+
data: {
|
543
|
+
action: "click->color-customizer#setExportFormat",
|
544
|
+
color_customizer_format_param: "tailwind"
|
545
|
+
}
|
546
|
+
)) { "Tailwind v4" },
|
547
|
+
render(M9sh::ButtonComponent.new(
|
548
|
+
variant: :outline,
|
549
|
+
size: :sm,
|
550
|
+
data: {
|
551
|
+
action: "click->color-customizer#setExportFormat",
|
552
|
+
color_customizer_format_param: "css"
|
553
|
+
}
|
554
|
+
)) { "CSS Variables" },
|
555
|
+
render(M9sh::ButtonComponent.new(
|
556
|
+
variant: :outline,
|
557
|
+
size: :sm,
|
558
|
+
data: {
|
559
|
+
action: "click->color-customizer#setExportFormat",
|
560
|
+
color_customizer_format_param: "json"
|
561
|
+
}
|
562
|
+
)) { "JSON" }
|
563
|
+
])
|
564
|
+
end
|
565
|
+
])
|
566
|
+
end
|
567
|
+
end
|
568
|
+
|
569
|
+
def render_config_preview
|
570
|
+
tag.div(class: "space-y-3 flex-1 flex flex-col min-h-0") do
|
571
|
+
safe_join([
|
572
|
+
tag.h3(class: "text-sm font-semibold") { "Configuration" },
|
573
|
+
tag.textarea(
|
574
|
+
class: "flex-1 p-4 rounded-lg bg-muted text-sm font-mono overflow-auto resize-none border border-border focus:outline-none focus:ring-2 focus:ring-ring",
|
575
|
+
data: { color_customizer_target: "configPreview" },
|
576
|
+
placeholder: "// Configuration will appear here"
|
577
|
+
)
|
578
|
+
])
|
579
|
+
end
|
580
|
+
end
|
581
|
+
|
582
|
+
def render_export_actions
|
583
|
+
tag.div(class: "flex gap-3", data: { color_customizer_target: "exportActions" }) do
|
584
|
+
safe_join([
|
585
|
+
render(M9sh::ButtonComponent.new(
|
586
|
+
variant: :default,
|
587
|
+
class: "flex-1 hidden",
|
588
|
+
data: {
|
589
|
+
action: "click->color-customizer#applyConfigChanges",
|
590
|
+
color_customizer_target: "applyButton"
|
591
|
+
}
|
592
|
+
)) { "Apply to UI" },
|
593
|
+
render(M9sh::ButtonComponent.new(
|
594
|
+
variant: :outline,
|
595
|
+
class: "flex-1",
|
596
|
+
data: { action: "click->color-customizer#copyConfig" }
|
597
|
+
)) { "Copy to Clipboard" },
|
598
|
+
render(M9sh::ButtonComponent.new(
|
599
|
+
variant: :outline,
|
600
|
+
class: "flex-1",
|
601
|
+
data: { action: "click->color-customizer#downloadConfig" }
|
602
|
+
)) { "Download File" }
|
603
|
+
])
|
604
|
+
end
|
605
|
+
end
|
606
|
+
|
607
|
+
def render_footer
|
608
|
+
tag.div(class: "border-t border-border p-6") do
|
609
|
+
tag.div(class: "flex items-center justify-between") do
|
610
|
+
safe_join([
|
611
|
+
tag.p(class: "text-sm text-muted-foreground") do
|
612
|
+
"Color changes are saved automatically and persist across sessions"
|
613
|
+
end,
|
614
|
+
render(M9sh::ButtonComponent.new(
|
615
|
+
variant: :outline,
|
616
|
+
size: :sm,
|
617
|
+
data: { action: "click->color-customizer#resetAll" }
|
618
|
+
)) { "Reset All Colors" }
|
619
|
+
])
|
620
|
+
end
|
621
|
+
end
|
622
|
+
end
|
623
|
+
end
|
624
|
+
end
|
@@ -0,0 +1,30 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module M9sh
|
4
|
+
class DialogCloseComponent < BaseComponent
|
5
|
+
include Utilities
|
6
|
+
|
7
|
+
def initialize(as_child: false, **extra_attrs)
|
8
|
+
@as_child = as_child
|
9
|
+
super(**extra_attrs)
|
10
|
+
end
|
11
|
+
|
12
|
+
def call
|
13
|
+
if content.present?
|
14
|
+
# Wrap the child element with data attributes (as_child behavior)
|
15
|
+
tag.span(
|
16
|
+
content,
|
17
|
+
data: { action: "click->m9sh--dialog#close" }
|
18
|
+
)
|
19
|
+
else
|
20
|
+
# Render as a button if no content is provided
|
21
|
+
tag.button(
|
22
|
+
"Close",
|
23
|
+
**component_attrs("inline-flex items-center justify-center rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 border border-input bg-background hover:bg-accent hover:text-accent-foreground h-10 px-4 py-2"),
|
24
|
+
type: "button",
|
25
|
+
data: { action: "click->m9sh--dialog#close" }
|
26
|
+
)
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|