m9sh 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (104) hide show
  1. checksums.yaml +7 -0
  2. data/.do/app.yaml +25 -0
  3. data/.dockerignore +51 -0
  4. data/.idea/.gitignore +8 -0
  5. data/.idea/aws.xml +17 -0
  6. data/.idea/hotcdn.iml +189 -0
  7. data/.idea/jsLibraryMappings.xml +6 -0
  8. data/.idea/misc.xml +4 -0
  9. data/.idea/modules.xml +8 -0
  10. data/.idea/vcs.xml +6 -0
  11. data/.mise.toml +3 -0
  12. data/.node-version +1 -0
  13. data/Dockerfile +84 -0
  14. data/README.md +230 -0
  15. data/Rakefile +6 -0
  16. data/app/components/m9sh/accordion_component.rb +122 -0
  17. data/app/components/m9sh/alert_component.rb +72 -0
  18. data/app/components/m9sh/alert_dialog_component.rb +100 -0
  19. data/app/components/m9sh/avatar_component.rb +65 -0
  20. data/app/components/m9sh/badge_component.rb +37 -0
  21. data/app/components/m9sh/base_component.rb +21 -0
  22. data/app/components/m9sh/breadcrumb_component.rb +100 -0
  23. data/app/components/m9sh/button_component.rb +54 -0
  24. data/app/components/m9sh/card_component.rb +90 -0
  25. data/app/components/m9sh/checkbox_component.rb +36 -0
  26. data/app/components/m9sh/collapsible_component.rb +47 -0
  27. data/app/components/m9sh/dialog_component.rb +123 -0
  28. data/app/components/m9sh/dropdown_menu_component.rb +27 -0
  29. data/app/components/m9sh/dropdown_menu_content_component.rb +24 -0
  30. data/app/components/m9sh/dropdown_menu_item_component.rb +36 -0
  31. data/app/components/m9sh/dropdown_menu_separator_component.rb +9 -0
  32. data/app/components/m9sh/dropdown_menu_trigger_component.rb +19 -0
  33. data/app/components/m9sh/hover_card_component.rb +48 -0
  34. data/app/components/m9sh/input_component.rb +33 -0
  35. data/app/components/m9sh/label_component.rb +27 -0
  36. data/app/components/m9sh/main_component.rb +16 -0
  37. data/app/components/m9sh/navigation_menu_component.rb +95 -0
  38. data/app/components/m9sh/popover_component.rb +47 -0
  39. data/app/components/m9sh/progress_component.rb +46 -0
  40. data/app/components/m9sh/radio_group_component.rb +88 -0
  41. data/app/components/m9sh/select_component.rb +51 -0
  42. data/app/components/m9sh/separator_component.rb +40 -0
  43. data/app/components/m9sh/sheet_component.rb +123 -0
  44. data/app/components/m9sh/sidebar_component.rb +126 -0
  45. data/app/components/m9sh/sidebar_group_component.rb +51 -0
  46. data/app/components/m9sh/sidebar_inset_component.rb +16 -0
  47. data/app/components/m9sh/sidebar_menu_button_component.rb +56 -0
  48. data/app/components/m9sh/sidebar_menu_component.rb +16 -0
  49. data/app/components/m9sh/sidebar_menu_item_component.rb +16 -0
  50. data/app/components/m9sh/sidebar_provider_component.rb +29 -0
  51. data/app/components/m9sh/sidebar_trigger_component.rb +44 -0
  52. data/app/components/m9sh/skeleton_component.rb +32 -0
  53. data/app/components/m9sh/slider_component.rb +83 -0
  54. data/app/components/m9sh/spinner_component.rb +46 -0
  55. data/app/components/m9sh/switch_component.rb +47 -0
  56. data/app/components/m9sh/table_component.rb +111 -0
  57. data/app/components/m9sh/tabs_component.rb +92 -0
  58. data/app/components/m9sh/textarea_component.rb +44 -0
  59. data/app/components/m9sh/theme_toggle_component.rb +88 -0
  60. data/app/components/m9sh/toast_component.rb +86 -0
  61. data/app/components/m9sh/toaster_component.rb +20 -0
  62. data/app/components/m9sh/toggle_component.rb +64 -0
  63. data/app/components/m9sh/tooltip_component.rb +48 -0
  64. data/app/components/m9sh/typography_component.rb +56 -0
  65. data/app/components/m9sh/utilities.rb +26 -0
  66. data/app/javascript/controllers/m9sh/accordion_controller.js +110 -0
  67. data/app/javascript/controllers/m9sh/alert_dialog_controller.js +47 -0
  68. data/app/javascript/controllers/m9sh/collapsible_controller.js +57 -0
  69. data/app/javascript/controllers/m9sh/dialog_controller.js +119 -0
  70. data/app/javascript/controllers/m9sh/dropdown_menu_controller.js +103 -0
  71. data/app/javascript/controllers/m9sh/hover_card_controller.js +66 -0
  72. data/app/javascript/controllers/m9sh/navigation_menu_controller.js +219 -0
  73. data/app/javascript/controllers/m9sh/popover_controller.js +113 -0
  74. data/app/javascript/controllers/m9sh/radio_controller.js +59 -0
  75. data/app/javascript/controllers/m9sh/sheet_controller.js +46 -0
  76. data/app/javascript/controllers/m9sh/sidebar_controller.js +114 -0
  77. data/app/javascript/controllers/m9sh/sidebar_provider_controller.js +12 -0
  78. data/app/javascript/controllers/m9sh/slider_controller.js +90 -0
  79. data/app/javascript/controllers/m9sh/switch_controller.js +33 -0
  80. data/app/javascript/controllers/m9sh/tabs_controller.js +51 -0
  81. data/app/javascript/controllers/m9sh/theme_controller.js +50 -0
  82. data/app/javascript/controllers/m9sh/toast_controller.js +46 -0
  83. data/app/javascript/controllers/m9sh/toaster_controller.js +70 -0
  84. data/app/javascript/controllers/m9sh/toggle_controller.js +27 -0
  85. data/app/javascript/controllers/m9sh/tooltip_controller.js +86 -0
  86. data/components.json +21 -0
  87. data/config.ru +6 -0
  88. data/exe/m9sh +12 -0
  89. data/fix_namespaces.py +32 -0
  90. data/fly.toml +30 -0
  91. data/koyeb.yaml +26 -0
  92. data/lib/m9sh/cli.rb +234 -0
  93. data/lib/m9sh/config.rb +114 -0
  94. data/lib/m9sh/generator.rb +183 -0
  95. data/lib/m9sh/registry.rb +107 -0
  96. data/lib/m9sh/registry.yml +384 -0
  97. data/lib/m9sh/version.rb +5 -0
  98. data/lib/m9sh.rb +11 -0
  99. data/package-lock.json +99 -0
  100. data/package.json +28 -0
  101. data/pnpm-lock.yaml +75 -0
  102. data/tailwind.config.js +93 -0
  103. data/update_namespace.py +73 -0
  104. metadata +208 -0
@@ -0,0 +1,83 @@
1
+ # frozen_string_literal: true
2
+
3
+ module M9sh
4
+ class SliderComponent < BaseComponent
5
+ include Utilities
6
+
7
+ def initialize(min: 0, max: 100, step: 1, value: 50, name: nil, **extra_attrs)
8
+ @min = min
9
+ @max = max
10
+ @step = step
11
+ @value = value
12
+ @name = name
13
+ super(**extra_attrs)
14
+ end
15
+
16
+ def call
17
+ tag.div(
18
+ **component_attrs("relative flex w-full touch-none select-none items-center"),
19
+ data: {
20
+ controller: "m9sh--slider",
21
+ m9sh__slider_min_value: @min,
22
+ m9sh__slider_max_value: @max,
23
+ m9sh__slider_step_value: @step,
24
+ m9sh__slider_value_value: @value
25
+ }
26
+ ) do
27
+ safe_join([
28
+ render_track,
29
+ render_range,
30
+ render_thumb,
31
+ render_hidden_input
32
+ ])
33
+ end
34
+ end
35
+
36
+ private
37
+
38
+ def render_track
39
+ tag.span(
40
+ class: "relative h-2 w-full grow overflow-hidden rounded-full bg-secondary",
41
+ data: {
42
+ m9sh__slider_target: "track",
43
+ action: "click->m9sh--slider#handleTrackClick"
44
+ }
45
+ )
46
+ end
47
+
48
+ def render_range
49
+ tag.span(
50
+ class: "absolute h-full bg-primary",
51
+ data: { m9sh__slider_target: "range" }
52
+ )
53
+ end
54
+
55
+ def render_thumb
56
+ tag.span(
57
+ class: "block h-5 w-5 rounded-full border-2 border-primary bg-background 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 cursor-pointer absolute top-1/2 -translate-y-1/2 -translate-x-1/2",
58
+ data: {
59
+ m9sh__slider_target: "thumb",
60
+ action: "mousedown->m9sh--slider#startDrag touchstart->m9sh--slider#startDrag"
61
+ },
62
+ tabindex: "0",
63
+ role: "slider",
64
+ aria: {
65
+ valuenow: @value,
66
+ valuemin: @min,
67
+ valuemax: @max
68
+ }
69
+ )
70
+ end
71
+
72
+ def render_hidden_input
73
+ return unless @name
74
+
75
+ tag.input(
76
+ type: "hidden",
77
+ name: @name,
78
+ value: @value,
79
+ data: { m9sh__slider_target: "input" }
80
+ )
81
+ end
82
+ end
83
+ end
@@ -0,0 +1,46 @@
1
+ # frozen_string_literal: true
2
+
3
+ module M9sh
4
+ class SpinnerComponent < BaseComponent
5
+ include Utilities
6
+
7
+ SIZES = {
8
+ sm: "size-3",
9
+ default: "size-4",
10
+ md: "size-6",
11
+ lg: "size-8"
12
+ }.freeze
13
+
14
+ def initialize(size: :default, **extra_attrs)
15
+ @size = size
16
+ super(**extra_attrs)
17
+ end
18
+
19
+ def call
20
+ tag.svg(
21
+ **component_attrs(class_names("animate-spin", size_classes)),
22
+ xmlns: "http://www.w3.org/2000/svg",
23
+ width: "24",
24
+ height: "24",
25
+ viewBox: "0 0 24 24",
26
+ fill: "none",
27
+ stroke: "currentColor",
28
+ stroke_width: "2",
29
+ stroke_linecap: "round",
30
+ stroke_linejoin: "round",
31
+ role: "status",
32
+ aria: { label: "Loading" }
33
+ ) do
34
+ safe_join([
35
+ tag.path(d: "M21 12a9 9 0 1 1-6.219-8.56")
36
+ ])
37
+ end
38
+ end
39
+
40
+ private
41
+
42
+ def size_classes
43
+ SIZES[@size] || SIZES[:default]
44
+ end
45
+ end
46
+ end
@@ -0,0 +1,47 @@
1
+ # frozen_string_literal: true
2
+
3
+ module M9sh
4
+ class SwitchComponent < BaseComponent
5
+ include Utilities
6
+
7
+ def initialize(name: nil, checked: false, disabled: false, **extra_attrs)
8
+ @name = name
9
+ @checked = checked
10
+ @disabled = disabled
11
+ super(**extra_attrs)
12
+ end
13
+
14
+ def call
15
+ tag.button(
16
+ **component_attrs(base_classes),
17
+ type: "button",
18
+ role: "switch",
19
+ "aria-checked": @checked.to_s,
20
+ data: {
21
+ controller: "m9sh--switch",
22
+ action: "click->m9sh--switch#toggle",
23
+ m9sh__switch_checked_value: @checked.to_s
24
+ },
25
+ disabled: @disabled
26
+ ) do
27
+ tag.span(
28
+ class: thumb_classes,
29
+ data: { m9sh__switch_target: "thumb" }
30
+ )
31
+ end
32
+ end
33
+
34
+ private
35
+
36
+ def base_classes
37
+ "peer inline-flex h-5 w-9 shrink-0 cursor-pointer items-center rounded-full border-2 border-transparent shadow-sm transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background disabled:cursor-not-allowed disabled:opacity-50 data-[state=checked]:bg-primary data-[state=unchecked]:bg-input"
38
+ end
39
+
40
+ def thumb_classes
41
+ cn(
42
+ "pointer-events-none block h-4 w-4 rounded-full bg-background shadow-lg ring-0 transition-transform",
43
+ @checked ? "translate-x-4" : "translate-x-0"
44
+ )
45
+ end
46
+ end
47
+ end
@@ -0,0 +1,111 @@
1
+ # frozen_string_literal: true
2
+
3
+ module M9sh
4
+ class TableComponent < BaseComponent
5
+ include Utilities
6
+
7
+ renders_one :header, "M9sh::TableComponent::HeaderComponent"
8
+ renders_one :body, "M9sh::TableComponent::BodyComponent"
9
+ renders_one :footer, "M9sh::TableComponent::FooterComponent"
10
+ renders_one :caption, "M9sh::TableComponent::CaptionComponent"
11
+
12
+ def call
13
+ tag.div(
14
+ class: "relative w-full overflow-auto"
15
+ ) do
16
+ tag.table(
17
+ **component_attrs("w-full caption-bottom text-sm")
18
+ ) do
19
+ concat(header) if header?
20
+ concat(body) if body?
21
+ concat(footer) if footer?
22
+ concat(caption) if caption?
23
+ end
24
+ end
25
+ end
26
+
27
+ class HeaderComponent < BaseComponent
28
+ include Utilities
29
+
30
+ def call
31
+ tag.thead(
32
+ **component_attrs("[&_tr]:border-b")
33
+ ) do
34
+ content
35
+ end
36
+ end
37
+ end
38
+
39
+ class BodyComponent < BaseComponent
40
+ include Utilities
41
+
42
+ def call
43
+ tag.tbody(
44
+ **component_attrs("[&_tr:last-child]:border-0")
45
+ ) do
46
+ content
47
+ end
48
+ end
49
+ end
50
+
51
+ class FooterComponent < BaseComponent
52
+ include Utilities
53
+
54
+ def call
55
+ tag.tfoot(
56
+ **component_attrs("border-t bg-muted font-medium [&>tr]:last:border-b-0")
57
+ ) do
58
+ content
59
+ end
60
+ end
61
+ end
62
+
63
+ class RowComponent < BaseComponent
64
+ include Utilities
65
+
66
+ def call
67
+ tag.tr(
68
+ **component_attrs("border-b transition-colors hover:bg-muted data-[state=selected]:bg-muted")
69
+ ) do
70
+ content
71
+ end
72
+ end
73
+ end
74
+
75
+ class HeadComponent < BaseComponent
76
+ include Utilities
77
+
78
+ def call
79
+ tag.th(
80
+ **component_attrs("h-12 px-4 text-left align-middle font-medium text-muted-foreground [&:has([role=checkbox])]:pr-0")
81
+ ) do
82
+ content
83
+ end
84
+ end
85
+ end
86
+
87
+ class CellComponent < BaseComponent
88
+ include Utilities
89
+
90
+ def call
91
+ tag.td(
92
+ **component_attrs("p-4 align-middle [&:has([role=checkbox])]:pr-0")
93
+ ) do
94
+ content
95
+ end
96
+ end
97
+ end
98
+
99
+ class CaptionComponent < BaseComponent
100
+ include Utilities
101
+
102
+ def call
103
+ tag.caption(
104
+ **component_attrs("mt-4 text-sm text-muted-foreground")
105
+ ) do
106
+ content
107
+ end
108
+ end
109
+ end
110
+ end
111
+ end
@@ -0,0 +1,92 @@
1
+ # frozen_string_literal: true
2
+
3
+ module M9sh
4
+ class TabsComponent < BaseComponent
5
+ include Utilities
6
+
7
+ renders_many :tabs, "TabComponent"
8
+
9
+ def initialize(default_value: nil, **extra_attrs)
10
+ @default_value = default_value
11
+ super(**extra_attrs)
12
+ end
13
+
14
+ def call
15
+ tag.div(
16
+ **component_attrs(base_classes),
17
+ data: {
18
+ controller: "m9sh--tabs",
19
+ m9sh__tabs_default_value: @default_value
20
+ }
21
+ ) do
22
+ safe_join([
23
+ render_tab_list,
24
+ render_tab_panels
25
+ ])
26
+ end
27
+ end
28
+
29
+ private
30
+
31
+ def base_classes
32
+ "w-full"
33
+ end
34
+
35
+ def render_tab_list
36
+ tag.div(
37
+ class: "inline-flex h-9 items-center justify-center rounded-lg bg-muted p-1 text-muted-foreground",
38
+ role: "tablist",
39
+ data: { m9sh__tabs_target: "list" }
40
+ ) do
41
+ tabs.each_with_index.map do |tab, index|
42
+ tag.button(
43
+ tab.label,
44
+ type: "button",
45
+ role: "tab",
46
+ class: "inline-flex items-center justify-center whitespace-nowrap rounded-md px-3 py-1 text-sm 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",
47
+ data: {
48
+ m9sh__tabs_target: "trigger",
49
+ action: "click->m9sh--tabs#selectTab",
50
+ value: tab.value
51
+ },
52
+ "aria-selected": @default_value == tab.value ? "true" : "false"
53
+ )
54
+ end.join.html_safe
55
+ end
56
+ end
57
+
58
+ def render_tab_panels
59
+ tag.div(class: "mt-2") do
60
+ tabs.map do |tab|
61
+ tag.div(
62
+ tab.panel,
63
+ role: "tabpanel",
64
+ class: "ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2",
65
+ data: {
66
+ m9sh__tabs_target: "panel",
67
+ value: tab.value
68
+ },
69
+ style: @default_value == tab.value ? "" : "display: none;"
70
+ )
71
+ end.join.html_safe
72
+ end
73
+ end
74
+
75
+ class TabComponent < BaseComponent
76
+ attr_reader :value, :label
77
+
78
+ renders_one :panel
79
+
80
+ def initialize(value:, label:, **extra_attrs)
81
+ @value = value
82
+ @label = label
83
+ super(**extra_attrs)
84
+ end
85
+
86
+ def call
87
+ # Tabs are rendered by parent component
88
+ nil
89
+ end
90
+ end
91
+ end
92
+ end
@@ -0,0 +1,44 @@
1
+ # frozen_string_literal: true
2
+
3
+ module M9sh
4
+ class TextareaComponent < BaseComponent
5
+ include Utilities
6
+
7
+ def initialize(
8
+ name: nil,
9
+ value: nil,
10
+ placeholder: nil,
11
+ rows: 3,
12
+ disabled: false,
13
+ readonly: false,
14
+ **extra_attrs
15
+ )
16
+ @name = name
17
+ @value = value
18
+ @placeholder = placeholder
19
+ @rows = rows
20
+ @disabled = disabled
21
+ @readonly = readonly
22
+ super(**extra_attrs)
23
+ end
24
+
25
+ def call
26
+ tag.textarea(
27
+ @value,
28
+ **component_attrs(base_classes),
29
+ name: @name,
30
+ placeholder: @placeholder,
31
+ rows: @rows,
32
+ disabled: @disabled,
33
+ readonly: @readonly,
34
+ data: { slot: "textarea" }
35
+ )
36
+ end
37
+
38
+ private
39
+
40
+ def base_classes
41
+ "border-input placeholder:text-muted-foreground focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive dark:bg-input/30 flex min-h-16 w-full rounded-md border bg-transparent px-3 py-2 text-base text-foreground shadow-xs transition-[color,box-shadow] outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50 md:text-sm"
42
+ end
43
+ end
44
+ end
@@ -0,0 +1,88 @@
1
+ # frozen_string_literal: true
2
+
3
+ module M9sh
4
+ class ThemeToggleComponent < BaseComponent
5
+ include Utilities
6
+
7
+ def initialize(**extra_attrs)
8
+ super(**extra_attrs)
9
+ end
10
+
11
+ def call
12
+ tag.div(
13
+ **component_attrs("inline-flex items-center gap-2"),
14
+ data: { controller: "m9sh--theme" }
15
+ ) do
16
+ tag.button(
17
+ class: button_classes,
18
+ data: { action: "click->m9sh--theme#toggle" },
19
+ aria: { label: "Toggle theme" }
20
+ ) do
21
+ safe_join([
22
+ sun_icon,
23
+ moon_icon,
24
+ tag.span("Toggle theme", class: "sr-only")
25
+ ])
26
+ end
27
+ end
28
+ end
29
+
30
+ private
31
+
32
+ def button_classes
33
+ cn(
34
+ "inline-flex items-center justify-center",
35
+ "rounded-md text-sm font-medium",
36
+ "h-9 w-9",
37
+ "border border-input bg-background",
38
+ "hover:bg-accent hover:text-accent-foreground",
39
+ "focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring",
40
+ "transition-colors"
41
+ )
42
+ end
43
+
44
+ def sun_icon
45
+ tag.svg(
46
+ xmlns: "http://www.w3.org/2000/svg",
47
+ width: "16",
48
+ height: "16",
49
+ viewBox: "0 0 24 24",
50
+ fill: "none",
51
+ stroke: "currentColor",
52
+ stroke_width: "2",
53
+ stroke_linecap: "round",
54
+ stroke_linejoin: "round",
55
+ class: "h-4 w-4 rotate-0 scale-100 transition-all dark:-rotate-90 dark:scale-0"
56
+ ) do
57
+ safe_join([
58
+ tag.circle(cx: "12", cy: "12", r: "4"),
59
+ tag.path(d: "M12 2v2"),
60
+ tag.path(d: "M12 20v2"),
61
+ tag.path(d: "m4.93 4.93 1.41 1.41"),
62
+ tag.path(d: "m17.66 17.66 1.41 1.41"),
63
+ tag.path(d: "M2 12h2"),
64
+ tag.path(d: "M20 12h2"),
65
+ tag.path(d: "m6.34 17.66-1.41 1.41"),
66
+ tag.path(d: "m19.07 4.93-1.41 1.41")
67
+ ])
68
+ end
69
+ end
70
+
71
+ def moon_icon
72
+ tag.svg(
73
+ xmlns: "http://www.w3.org/2000/svg",
74
+ width: "16",
75
+ height: "16",
76
+ viewBox: "0 0 24 24",
77
+ fill: "none",
78
+ stroke: "currentColor",
79
+ stroke_width: "2",
80
+ stroke_linecap: "round",
81
+ stroke_linejoin: "round",
82
+ class: "absolute h-4 w-4 rotate-90 scale-0 transition-all dark:rotate-0 dark:scale-100"
83
+ ) do
84
+ tag.path(d: "M12 3a6 6 0 0 0 9 9 9 9 0 1 1-9-9Z")
85
+ end
86
+ end
87
+ end
88
+ end
@@ -0,0 +1,86 @@
1
+ # frozen_string_literal: true
2
+
3
+ module M9sh
4
+ class ToastComponent < BaseComponent
5
+ attr_reader :variant, :title, :description, :duration
6
+
7
+ VARIANTS = {
8
+ default: "bg-background text-foreground border",
9
+ success: "bg-green-50 text-green-900 border",
10
+ error: "bg-red-50 text-red-900 border",
11
+ warning: "bg-yellow-50 text-yellow-900 border",
12
+ info: "bg-blue-50 text-blue-900 border"
13
+ }.freeze
14
+
15
+ def initialize(title:, description: nil, variant: :default, duration: 5000, **extra_attrs)
16
+ @title = title
17
+ @description = description
18
+ @variant = variant.to_sym
19
+ @duration = duration
20
+ super(**extra_attrs)
21
+ end
22
+
23
+ def call
24
+ tag.div(
25
+ data: {
26
+ controller: "m9sh--toast",
27
+ m9sh__toast_duration_value: duration,
28
+ transition_enter: "transition ease-out duration-300",
29
+ transition_enter_start: "transform translate-x-full opacity-0",
30
+ transition_enter_end: "transform translate-x-0 opacity-100",
31
+ transition_leave: "transition ease-in duration-200",
32
+ transition_leave_start: "transform translate-x-0 opacity-100",
33
+ transition_leave_end: "transform translate-x-full opacity-0"
34
+ },
35
+ class: "pointer-events-auto w-full max-w-sm overflow-hidden rounded-lg shadow-lg ring-1 ring-black ring-opacity-5 #{variant_classes}",
36
+ **(@extra_attrs || {})
37
+ ) do
38
+ tag.div(class: "p-4") do
39
+ safe_join([
40
+ title_element,
41
+ description_element,
42
+ close_button
43
+ ].compact)
44
+ end
45
+ end
46
+ end
47
+
48
+ private
49
+
50
+ def variant_classes
51
+ VARIANTS[@variant] || VARIANTS[:default]
52
+ end
53
+
54
+ def title_element
55
+ return unless title
56
+
57
+ tag.p(class: "text-sm font-semibold") { title }
58
+ end
59
+
60
+ def description_element
61
+ return unless description
62
+
63
+ tag.p(class: "mt-1 text-sm opacity-90") { description }
64
+ end
65
+
66
+ def close_button
67
+ tag.button(
68
+ type: "button",
69
+ data: { action: "click->m9sh--toast#close" },
70
+ class: "absolute top-2 right-2 inline-flex rounded-md p-1.5 text-gray-400 hover:bg-gray-100 hover:text-gray-500 focus:outline-none focus:ring-2 focus:ring-gray-300"
71
+ ) do
72
+ tag.span(class: "sr-only") { "Dismiss" }
73
+ tag.svg(
74
+ class: "h-5 w-5",
75
+ xmlns: "http://www.w3.org/2000/svg",
76
+ viewBox: "0 0 20 20",
77
+ fill: "currentColor"
78
+ ) do
79
+ tag.path(
80
+ d: "M6.28 5.22a.75.75 0 00-1.06 1.06L8.94 10l-3.72 3.72a.75.75 0 101.06 1.06L10 11.06l3.72 3.72a.75.75 0 101.06-1.06L11.06 10l3.72-3.72a.75.75 0 00-1.06-1.06L10 8.94 6.28 5.22z"
81
+ )
82
+ end
83
+ end
84
+ end
85
+ end
86
+ end
@@ -0,0 +1,20 @@
1
+ # frozen_string_literal: true
2
+
3
+ module M9sh
4
+ class ToasterComponent < BaseComponent
5
+ def call
6
+ tag.div(
7
+ id: "toast-container",
8
+ data: { controller: "m9sh--toaster" },
9
+ class: "fixed inset-0 z-50 pointer-events-none",
10
+ "aria-live": "polite",
11
+ **(@extra_attrs || {})
12
+ ) do
13
+ tag.div(
14
+ class: "fixed bottom-0 right-0 flex flex-col gap-2 p-4 sm:bottom-4 sm:right-4 sm:flex-col sm:items-end",
15
+ data: { m9sh__toaster_target: "container" }
16
+ )
17
+ end
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,64 @@
1
+ # frozen_string_literal: true
2
+
3
+ module M9sh
4
+ class ToggleComponent < BaseComponent
5
+ include Utilities
6
+
7
+ VARIANTS = {
8
+ default: "bg-transparent",
9
+ outline: "border border-input bg-transparent shadow-xs hover:bg-accent hover:text-accent-foreground"
10
+ }.freeze
11
+
12
+ SIZES = {
13
+ sm: "h-8 px-1.5 min-w-8",
14
+ default: "h-9 px-2 min-w-9",
15
+ lg: "h-10 px-2.5 min-w-10"
16
+ }.freeze
17
+
18
+ def initialize(
19
+ variant: :default,
20
+ size: :default,
21
+ pressed: false,
22
+ disabled: false,
23
+ **extra_attrs
24
+ )
25
+ @variant = variant.to_sym
26
+ @size = size.to_sym
27
+ @pressed = pressed
28
+ @disabled = disabled
29
+ super(**extra_attrs)
30
+ end
31
+
32
+ def call
33
+ tag.button(
34
+ content,
35
+ **component_attrs(class_names(base_classes, variant_classes, size_classes)),
36
+ type: "button",
37
+ role: "button",
38
+ "aria-pressed": @pressed.to_s,
39
+ disabled: @disabled,
40
+ data: {
41
+ slot: "toggle",
42
+ state: @pressed ? "on" : "off",
43
+ controller: "m9sh--toggle",
44
+ action: "click->m9sh--toggle#toggle",
45
+ m9sh__toggle_pressed_value: @pressed.to_s
46
+ }
47
+ )
48
+ end
49
+
50
+ private
51
+
52
+ def base_classes
53
+ "inline-flex items-center justify-center gap-2 rounded-md text-sm font-medium hover:bg-muted hover:text-muted-foreground disabled:pointer-events-none disabled:opacity-50 data-[state=on]:bg-accent data-[state=on]:text-accent-foreground [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 [&_svg]:shrink-0 focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] outline-none transition-[color,box-shadow] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive whitespace-nowrap"
54
+ end
55
+
56
+ def variant_classes
57
+ VARIANTS[@variant] || VARIANTS[:default]
58
+ end
59
+
60
+ def size_classes
61
+ SIZES[@size] || SIZES[:default]
62
+ end
63
+ end
64
+ end