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.
Files changed (47) hide show
  1. checksums.yaml +4 -4
  2. data/Dockerfile +2 -1
  3. data/GEM_README.md +284 -0
  4. data/LICENSE.txt +21 -0
  5. data/M9SH_CLI.md +453 -0
  6. data/PUBLISHING.md +331 -0
  7. data/README.md +120 -52
  8. data/app/components/m9sh/accordion_component.rb +3 -3
  9. data/app/components/m9sh/alert_component.rb +7 -9
  10. data/app/components/m9sh/base_component.rb +1 -0
  11. data/app/components/m9sh/button_component.rb +3 -2
  12. data/app/components/m9sh/color_customizer_component.rb +624 -0
  13. data/app/components/m9sh/dialog_close_component.rb +30 -0
  14. data/app/components/m9sh/dialog_component.rb +11 -99
  15. data/app/components/m9sh/dialog_content_component.rb +102 -0
  16. data/app/components/m9sh/dialog_description_component.rb +14 -0
  17. data/app/components/m9sh/dialog_footer_component.rb +14 -0
  18. data/app/components/m9sh/dialog_header_component.rb +27 -0
  19. data/app/components/m9sh/dialog_title_component.rb +14 -0
  20. data/app/components/m9sh/dialog_trigger_component.rb +23 -0
  21. data/app/components/m9sh/dropdown_menu_content_component.rb +1 -1
  22. data/app/components/m9sh/dropdown_menu_item_component.rb +1 -1
  23. data/app/components/m9sh/dropdown_menu_trigger_component.rb +1 -1
  24. data/app/components/m9sh/icon_component.rb +78 -0
  25. data/app/components/m9sh/main_component.rb +1 -1
  26. data/app/components/m9sh/menu_component.rb +85 -0
  27. data/app/components/m9sh/navbar_component.rb +186 -0
  28. data/app/components/m9sh/navigation_menu_component.rb +2 -2
  29. data/app/components/m9sh/popover_component.rb +12 -7
  30. data/app/components/m9sh/radio_group_component.rb +45 -13
  31. data/app/components/m9sh/sheet_component.rb +6 -6
  32. data/app/components/m9sh/sidebar_component.rb +6 -1
  33. data/app/components/m9sh/skeleton_component.rb +7 -1
  34. data/app/components/m9sh/tabs_component.rb +76 -48
  35. data/app/components/m9sh/textarea_component.rb +1 -1
  36. data/app/components/m9sh/theme_toggle_component.rb +1 -0
  37. data/app/javascript/controllers/m9sh/popover_controller.js +24 -18
  38. data/app/javascript/controllers/m9sh/sidebar_controller.js +29 -7
  39. data/lib/m9sh/config.rb +5 -5
  40. data/lib/m9sh/registry.rb +2 -2
  41. data/lib/m9sh/registry.yml +37 -0
  42. data/lib/m9sh/version.rb +1 -1
  43. data/lib/tasks/tailwindcss.rake +15 -0
  44. data/m9sh.gemspec +48 -0
  45. data/publish.sh +48 -0
  46. metadata +20 -3
  47. 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