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,47 @@
1
+ # frozen_string_literal: true
2
+
3
+ module M9sh
4
+ class CollapsibleComponent < BaseComponent
5
+ include Utilities
6
+
7
+ renders_one :trigger, lambda { |&block|
8
+ tag.div(
9
+ data: { action: "click->m9sh--collapsible#toggle" }
10
+ ) { block.call }
11
+ }
12
+
13
+ renders_one :collapsible_content, lambda { |&block|
14
+ tag.div(
15
+ class: "overflow-hidden transition-all duration-300 data-[state=closed]:animate-accordion-up data-[state=open]:animate-accordion-down",
16
+ data: {
17
+ m9sh__collapsible_target: "content"
18
+ }
19
+ ) do
20
+ tag.div(
21
+ class: "pt-0",
22
+ data: { m9sh__collapsible_target: "inner" }
23
+ ) { block.call }
24
+ end
25
+ }
26
+
27
+ def initialize(open: false, **extra_attrs)
28
+ @open = open
29
+ super(**extra_attrs)
30
+ end
31
+
32
+ def call
33
+ tag.div(
34
+ **component_attrs(""),
35
+ data: {
36
+ controller: "m9sh--collapsible",
37
+ m9sh__collapsible_open_value: @open
38
+ }
39
+ ) do
40
+ safe_join([
41
+ trigger,
42
+ collapsible_content
43
+ ].compact)
44
+ end
45
+ end
46
+ end
47
+ end
@@ -0,0 +1,123 @@
1
+ # frozen_string_literal: true
2
+
3
+ module M9sh
4
+ class DialogComponent < BaseComponent
5
+ include Utilities
6
+
7
+ renders_one :trigger
8
+ renders_one :header
9
+ renders_one :footer
10
+ renders_one :title
11
+ renders_one :description
12
+
13
+ def initialize(open: false, **extra_attrs)
14
+ @open = open
15
+ super(**extra_attrs)
16
+ end
17
+
18
+ def call
19
+ tag.div(
20
+ data: {
21
+ controller: "m9sh--dialog",
22
+ m9sh__dialog_open_value: @open.to_s
23
+ }
24
+ ) do
25
+ safe_join([
26
+ render_trigger,
27
+ render_dialog
28
+ ])
29
+ end
30
+ end
31
+
32
+ private
33
+
34
+ def render_trigger
35
+ return unless trigger?
36
+
37
+ tag.div(
38
+ trigger,
39
+ data: {
40
+ action: "click->m9sh--dialog#open",
41
+ m9sh__dialog_target: "trigger"
42
+ }
43
+ )
44
+ end
45
+
46
+ def render_dialog
47
+ tag.div(
48
+ class: overlay_classes,
49
+ data: {
50
+ m9sh__dialog_target: "overlay",
51
+ action: "click->m9sh--dialog#handleBackdropClick"
52
+ },
53
+ style: @open ? "" : "display: none;",
54
+ role: "dialog",
55
+ "aria-modal": "true"
56
+ ) do
57
+ tag.div(
58
+ class: content_classes,
59
+ data: { m9sh__dialog_target: "content" }
60
+ ) do
61
+ safe_join([
62
+ render_close_button,
63
+ render_header,
64
+ render_body,
65
+ render_footer
66
+ ].compact)
67
+ end
68
+ end
69
+ end
70
+
71
+ def render_close_button
72
+ tag.button(
73
+ type: "button",
74
+ class: "absolute right-4 top-4 rounded-sm opacity-70 ring-offset-background transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none data-[state=open]:bg-accent data-[state=open]:text-muted-foreground",
75
+ data: { action: "click->m9sh--dialog#close" }
76
+ ) do
77
+ tag.svg(
78
+ class: "h-4 w-4",
79
+ xmlns: "http://www.w3.org/2000/svg",
80
+ viewBox: "0 0 24 24",
81
+ fill: "none",
82
+ stroke: "currentColor",
83
+ "stroke-width": "2",
84
+ "stroke-linecap": "round",
85
+ "stroke-linejoin": "round"
86
+ ) do
87
+ tag.line(x1: "18", y1: "6", x2: "6", y2: "18") +
88
+ tag.line(x1: "6", y1: "6", x2: "18", y2: "18")
89
+ end
90
+ end
91
+ end
92
+
93
+ def render_header
94
+ return unless header? || title? || description?
95
+
96
+ tag.div(class: "flex flex-col space-y-1.5 text-center sm:text-left") do
97
+ safe_join([
98
+ title? ? tag.h2(title, class: "text-lg font-semibold leading-none tracking-tight") : nil,
99
+ description? ? tag.p(description, class: "text-sm text-muted-foreground") : nil,
100
+ header
101
+ ].compact)
102
+ end
103
+ end
104
+
105
+ def render_body
106
+ tag.div(class: "py-4") { content }
107
+ end
108
+
109
+ def render_footer
110
+ return unless footer?
111
+
112
+ tag.div(class: "flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2") { footer }
113
+ end
114
+
115
+ def overlay_classes
116
+ "fixed inset-0 z-50 bg-black/80 data-[state=open]:animate-fade-in data-[state=closed]:animate-fade-out"
117
+ end
118
+
119
+ def content_classes
120
+ "fixed left-[50%] top-[50%] z-50 grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 border bg-background p-6 shadow-lg duration-200 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%] sm:rounded-lg"
121
+ end
122
+ end
123
+ end
@@ -0,0 +1,27 @@
1
+ # frozen_string_literal: true
2
+
3
+ module M9sh
4
+ class DropdownMenuComponent < BaseComponent
5
+ attr_reader :align, :side
6
+
7
+ def initialize(align: "end", side: "bottom", **extra_attrs)
8
+ @align = align
9
+ @side = side
10
+ super(**extra_attrs)
11
+ end
12
+
13
+ def call
14
+ tag.div(
15
+ data: {
16
+ controller: "m9sh--dropdown-menu",
17
+ m9sh__dropdown_menu_align_value: align,
18
+ m9sh__dropdown_menu_side_value: side
19
+ },
20
+ class: "relative inline-block text-left",
21
+ **(@extra_attrs || {})
22
+ ) do
23
+ content
24
+ end
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,24 @@
1
+ # frozen_string_literal: true
2
+
3
+ module M9sh
4
+ class DropdownMenuContentComponent < BaseComponent
5
+ def call
6
+ tag.div(
7
+ data: {
8
+ m9sh__dropdown_menu_target: "content",
9
+ transition_enter: "transition ease-out duration-100",
10
+ transition_enter_start: "transform opacity-0 scale-95",
11
+ transition_enter_end: "transform opacity-100 scale-100",
12
+ transition_leave: "transition ease-in duration-75",
13
+ transition_leave_start: "transform opacity-100 scale-100",
14
+ transition_leave_end: "transform opacity-0 scale-95"
15
+ },
16
+ class: "absolute z-50 min-w-[8rem] overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-lg hidden",
17
+ style: "position: absolute;",
18
+ **(@extra_attrs || {})
19
+ ) do
20
+ content
21
+ end
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,36 @@
1
+ # frozen_string_literal: true
2
+
3
+ module M9sh
4
+ class DropdownMenuItemComponent < BaseComponent
5
+ attr_reader :href, :method
6
+
7
+ def initialize(href: nil, method: nil, **extra_attrs)
8
+ @href = href
9
+ @method = method
10
+ super(**extra_attrs)
11
+ end
12
+
13
+ def call
14
+ if href
15
+ link_to href, method: method, class: item_classes, **(@extra_attrs || {}) do
16
+ content
17
+ end
18
+ else
19
+ tag.button(
20
+ type: "button",
21
+ class: item_classes,
22
+ data: { action: "click->m9sh--dropdown-menu#hide" },
23
+ **(@extra_attrs || {})
24
+ ) do
25
+ content
26
+ end
27
+ end
28
+ end
29
+
30
+ private
31
+
32
+ def item_classes
33
+ "relative flex cursor-pointer select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none transition-colors hover:bg-accent hover:text-accent-foreground focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50"
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ module M9sh
4
+ class DropdownMenuSeparatorComponent < BaseComponent
5
+ def call
6
+ tag.div(class: "-mx-1 my-1 h-px bg-border", **(@extra_attrs || {}))
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,19 @@
1
+ # frozen_string_literal: true
2
+
3
+ module M9sh
4
+ class DropdownMenuTriggerComponent < BaseComponent
5
+ def call
6
+ tag.button(
7
+ data: {
8
+ action: "click->m9sh--dropdown-menu#toggle click@window->m9sh--dropdown-menu#hide",
9
+ m9sh__dropdown_menu_target: "trigger"
10
+ },
11
+ type: "button",
12
+ class: "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",
13
+ **(@extra_attrs || {})
14
+ ) do
15
+ content
16
+ end
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,48 @@
1
+ # frozen_string_literal: true
2
+
3
+ module M9sh
4
+ class HoverCardComponent < BaseComponent
5
+ include Utilities
6
+
7
+ renders_one :trigger, lambda { |&block|
8
+ tag.div(
9
+ data: {
10
+ action: "mouseenter->m9sh--hover-card#show mouseleave->m9sh--hover-card#hide",
11
+ m9sh__hover_card_target: "trigger"
12
+ }
13
+ ) { block.call }
14
+ }
15
+
16
+ renders_one :hover_content, lambda { |&block|
17
+ tag.div(
18
+ class: "absolute z-50 w-64 rounded-lg border border-border bg-popover p-4 text-popover-foreground shadow-md outline-none hidden data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95",
19
+ data: {
20
+ m9sh__hover_card_target: "content",
21
+ action: "mouseenter->m9sh--hover-card#cancelHide mouseleave->m9sh--hover-card#hide"
22
+ }
23
+ ) { block.call }
24
+ }
25
+
26
+ def initialize(open_delay: 200, close_delay: 300, **extra_attrs)
27
+ @open_delay = open_delay
28
+ @close_delay = close_delay
29
+ super(**extra_attrs)
30
+ end
31
+
32
+ def call
33
+ tag.div(
34
+ **component_attrs("relative inline-block"),
35
+ data: {
36
+ controller: "m9sh--hover-card",
37
+ m9sh__hover_card_open_delay_value: @open_delay,
38
+ m9sh__hover_card_close_delay_value: @close_delay
39
+ }
40
+ ) do
41
+ safe_join([
42
+ trigger,
43
+ hover_content
44
+ ].compact)
45
+ end
46
+ end
47
+ end
48
+ end
@@ -0,0 +1,33 @@
1
+ # frozen_string_literal: true
2
+
3
+ module M9sh
4
+ class InputComponent < BaseComponent
5
+ include Utilities
6
+
7
+ def initialize(type: "text", name: nil, value: nil, placeholder: nil, disabled: false, **extra_attrs)
8
+ @type = type
9
+ @name = name
10
+ @value = value
11
+ @placeholder = placeholder
12
+ @disabled = disabled
13
+ super(**extra_attrs)
14
+ end
15
+
16
+ def call
17
+ tag.input(
18
+ **component_attrs(base_classes),
19
+ type: @type,
20
+ name: @name,
21
+ value: @value,
22
+ placeholder: @placeholder,
23
+ disabled: @disabled
24
+ )
25
+ end
26
+
27
+ private
28
+
29
+ def base_classes
30
+ "flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-base ring-offset-background file:border-0 file:bg-transparent file:text-sm file:font-medium file:text-foreground placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 md:text-sm"
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,27 @@
1
+ # frozen_string_literal: true
2
+
3
+ module M9sh
4
+ class LabelComponent < BaseComponent
5
+ include Utilities
6
+
7
+ def initialize(for_id: nil, **extra_attrs)
8
+ @for_id = for_id
9
+ super(**extra_attrs)
10
+ end
11
+
12
+ def call
13
+ tag.label(
14
+ content,
15
+ **component_attrs(base_classes),
16
+ for: @for_id,
17
+ data: { slot: "label" }
18
+ )
19
+ end
20
+
21
+ private
22
+
23
+ def base_classes
24
+ "flex items-center gap-2 text-sm leading-none font-medium text-foreground select-none group-data-[disabled=true]:pointer-events-none group-data-[disabled=true]:opacity-50 peer-disabled:cursor-not-allowed peer-disabled:opacity-50"
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,16 @@
1
+ # frozen_string_literal: true
2
+
3
+ module M9sh
4
+ class MainComponent < BaseComponent
5
+ include Utilities
6
+
7
+ def call
8
+ tag.div(
9
+ **component_attrs("flex-1 flex flex-col m-4 rounded-lg shadow-xl bg-card overflow-hidden border border-border"),
10
+ data: { slot: "main" }
11
+ ) do
12
+ content
13
+ end
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,95 @@
1
+ # frozen_string_literal: true
2
+
3
+ module M9sh
4
+ class NavigationMenuComponent < BaseComponent
5
+ include Utilities
6
+
7
+ renders_many :items, "ItemComponent"
8
+
9
+ def call
10
+ tag.nav(
11
+ **component_attrs("relative z-10 flex max-w-max flex-1 items-center justify-center"),
12
+ data: { controller: "m9sh--navigation-menu" }
13
+ ) do
14
+ tag.div(
15
+ class: "group flex flex-1 list-none items-center justify-center space-x-1",
16
+ role: "group"
17
+ ) do
18
+ safe_join(items)
19
+ end
20
+ end
21
+ end
22
+
23
+ class ItemComponent < BaseComponent
24
+ include Utilities
25
+
26
+ renders_one :trigger, lambda { |text = nil, &block|
27
+ content = block ? block.call : text
28
+ tag.button(
29
+ type: "button",
30
+ class: "group inline-flex h-10 w-max items-center justify-center rounded-md bg-background px-4 py-2 text-sm font-medium transition-colors hover:bg-accent hover:text-accent-foreground focus:bg-accent focus:text-accent-foreground focus:outline-none disabled:pointer-events-none disabled:opacity-50 data-[state=open]:bg-accent/50",
31
+ data: {
32
+ action: "mouseenter->m9sh--navigation-menu#onTriggerEnter mouseleave->m9sh--navigation-menu#onTriggerLeave click->m9sh--navigation-menu#toggle",
33
+ m9sh__navigation_menu_target: "trigger",
34
+ state: "closed"
35
+ }
36
+ ) do
37
+ safe_join([
38
+ content,
39
+ tag.svg(
40
+ class: "relative top-[1px] ml-1 h-3 w-3 transition duration-300 group-data-[state=open]:rotate-180",
41
+ xmlns: "http://www.w3.org/2000/svg",
42
+ width: "24",
43
+ height: "24",
44
+ viewBox: "0 0 24 24",
45
+ fill: "none",
46
+ stroke: "currentColor",
47
+ stroke_width: "2",
48
+ stroke_linecap: "round",
49
+ stroke_linejoin: "round",
50
+ aria: { hidden: "true" }
51
+ ) do
52
+ tag.path(d: "m6 9 6 6 6-6")
53
+ end
54
+ ])
55
+ end
56
+ }
57
+
58
+ renders_one :menu_content, lambda { |&block|
59
+ tag.div(
60
+ class: "absolute left-1/2 top-full -translate-x-1/2 w-full md:w-auto",
61
+ style: "display: none;",
62
+ data: {
63
+ action: "mouseenter->m9sh--navigation-menu#onContentEnter mouseleave->m9sh--navigation-menu#onContentLeave",
64
+ m9sh__navigation_menu_target: "content",
65
+ state: "closed"
66
+ }
67
+ ) do
68
+ tag.div(
69
+ class: "relative mt-1.5 overflow-hidden rounded-lg border border-border bg-popover text-popover-foreground shadow-md data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95",
70
+ data: { state: "closed" }
71
+ ) do
72
+ block.call
73
+ end
74
+ end
75
+ }
76
+
77
+ renders_one :link, lambda { |href:, &block|
78
+ tag.a(
79
+ href: href,
80
+ class: "group inline-flex h-10 w-max items-center justify-center rounded-md bg-background px-4 py-2 text-sm font-medium text-foreground transition-colors hover:bg-accent hover:text-accent-foreground focus:bg-accent focus:text-accent-foreground focus:outline-none disabled:pointer-events-none disabled:opacity-50"
81
+ ) { block.call }
82
+ }
83
+
84
+ def call
85
+ tag.div(class: "relative", data: { m9sh__navigation_menu_target: "item" }) do
86
+ if link?
87
+ link
88
+ else
89
+ safe_join([trigger, menu_content].compact)
90
+ end
91
+ end
92
+ end
93
+ end
94
+ end
95
+ end
@@ -0,0 +1,47 @@
1
+ # frozen_string_literal: true
2
+
3
+ module M9sh
4
+ class PopoverComponent < BaseComponent
5
+ include Utilities
6
+
7
+ renders_one :trigger, lambda { |&block|
8
+ tag.div(
9
+ data: {
10
+ action: "click->m9sh--popover#toggle",
11
+ m9sh__popover_target: "trigger"
12
+ }
13
+ ) { block.call }
14
+ }
15
+
16
+ renders_one :popover_content, lambda { |&block|
17
+ tag.div(
18
+ class: "absolute z-50 w-72 rounded-lg border border-border bg-popover p-4 text-popover-foreground shadow-md outline-none hidden data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
19
+ data: {
20
+ m9sh__popover_target: "content"
21
+ }
22
+ ) { block.call }
23
+ }
24
+
25
+ def initialize(align: "center", side: "bottom", **extra_attrs)
26
+ @align = align
27
+ @side = side
28
+ super(**extra_attrs)
29
+ end
30
+
31
+ def call
32
+ tag.div(
33
+ **component_attrs("relative inline-block"),
34
+ data: {
35
+ controller: "m9sh--popover",
36
+ m9sh__popover_align_value: @align,
37
+ m9sh__popover_side_value: @side
38
+ }
39
+ ) do
40
+ safe_join([
41
+ trigger,
42
+ popover_content
43
+ ].compact)
44
+ end
45
+ end
46
+ end
47
+ end
@@ -0,0 +1,46 @@
1
+ # frozen_string_literal: true
2
+
3
+ module M9sh
4
+ class ProgressComponent < BaseComponent
5
+ include Utilities
6
+
7
+ def initialize(value: 0, max: 100, **extra_attrs)
8
+ @value = value.to_f
9
+ @max = max.to_f
10
+ super(**extra_attrs)
11
+ end
12
+
13
+ def call
14
+ tag.div(
15
+ **component_attrs(base_classes),
16
+ role: "progressbar",
17
+ "aria-valuemin": "0",
18
+ "aria-valuemax": @max,
19
+ "aria-valuenow": @value,
20
+ data: { slot: "progress" }
21
+ ) do
22
+ tag.div(
23
+ class: indicator_classes,
24
+ style: "transform: translateX(-#{100 - percentage}%)",
25
+ data: { slot: "progress-indicator" }
26
+ )
27
+ end
28
+ end
29
+
30
+ private
31
+
32
+ def base_classes
33
+ "bg-primary/20 relative h-2 w-full overflow-hidden rounded-full"
34
+ end
35
+
36
+ def indicator_classes
37
+ "bg-primary h-full w-full flex-1 transition-all"
38
+ end
39
+
40
+ def percentage
41
+ return 0 if @max.zero?
42
+
43
+ [(@value / @max * 100).round(2), 100].min
44
+ end
45
+ end
46
+ end