m9sh 0.2.1 → 0.2.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (125) hide show
  1. checksums.yaml +4 -4
  2. data/.idea/hotcdn.iml +30 -0
  3. data/.mise.toml +2 -2
  4. data/app/assets/config/manifest.js +4 -0
  5. data/app/assets/images/icons/activity.svg +3 -0
  6. data/app/assets/images/icons/bell.svg +4 -0
  7. data/app/assets/images/icons/book.svg +4 -0
  8. data/app/assets/images/icons/chevron-down.svg +3 -0
  9. data/app/assets/images/icons/chevron-left.svg +3 -0
  10. data/app/assets/images/icons/chevron-right.svg +3 -0
  11. data/app/assets/images/icons/credit-card.svg +4 -0
  12. data/app/assets/images/icons/dollar-sign.svg +3 -0
  13. data/app/assets/images/icons/edit.svg +4 -0
  14. data/app/assets/images/icons/github.svg +3 -0
  15. data/app/assets/images/icons/home.svg +4 -0
  16. data/app/assets/images/icons/info.svg +5 -0
  17. data/app/assets/images/icons/layout.svg +6 -0
  18. data/app/assets/images/icons/logout.svg +5 -0
  19. data/app/assets/images/icons/menu.svg +5 -0
  20. data/app/assets/images/icons/moon.svg +3 -0
  21. data/app/assets/images/icons/paintbrush.svg +6 -0
  22. data/app/assets/images/icons/search.svg +4 -0
  23. data/app/assets/images/icons/settings.svg +4 -0
  24. data/app/assets/images/icons/sun.svg +11 -0
  25. data/app/assets/images/icons/user.svg +4 -0
  26. data/app/assets/images/icons/users.svg +5 -0
  27. data/app/assets/stylesheets/tailwind.css +1180 -0
  28. data/app/components/backdrop_component.rb +103 -0
  29. data/app/components/docs/code_block_component.rb +56 -0
  30. data/app/components/docs/component_api_component.rb +16 -0
  31. data/app/components/docs/component_examples_component.rb +16 -0
  32. data/app/components/docs/component_header_component.html.erb +8 -0
  33. data/app/components/docs/component_header_component.rb +14 -0
  34. data/app/components/docs/component_installation_component.html.erb +15 -0
  35. data/app/components/docs/component_installation_component.rb +13 -0
  36. data/app/components/docs/component_page_component.html.erb +9 -0
  37. data/app/components/docs/component_page_component.rb +19 -0
  38. data/app/components/docs/component_preview_component.rb +318 -0
  39. data/app/components/docs/component_usage_component.rb +18 -0
  40. data/app/components/docs/prop_table_component.rb +64 -0
  41. data/app/controllers/application_controller.rb +3 -0
  42. data/app/controllers/blocks_controller.rb +51 -0
  43. data/app/controllers/docs_controller.rb +162 -0
  44. data/app/controllers/showcase_controller.rb +42 -0
  45. data/app/helpers/blocks_helper.rb +343 -0
  46. data/app/helpers/docs_helper.rb +3807 -0
  47. data/app/helpers/m9sh/toast_helper.rb +46 -0
  48. data/app/helpers/m9sh_helper.rb +343 -0
  49. data/app/javascript/application.js +3 -0
  50. data/app/javascript/controllers/application.js +9 -0
  51. data/app/javascript/controllers/backdrop_controller.js +137 -0
  52. data/app/javascript/controllers/color_customizer_controller.js +569 -0
  53. data/app/javascript/controllers/color_theme_controller.js +120 -0
  54. data/app/javascript/controllers/docs/component_preview_controller.js +149 -0
  55. data/app/javascript/controllers/docs/copy_button_controller.js +20 -0
  56. data/app/javascript/controllers/index.js +6 -0
  57. data/app/javascript/controllers/theme_controller.js +23 -0
  58. data/app/views/blocks/_sidebar.html.erb +31 -0
  59. data/app/views/blocks/_toc.html.erb +29 -0
  60. data/app/views/blocks/examples/dashboard-01.html.erb +180 -0
  61. data/app/views/blocks/examples/dashboard-02.html.erb +190 -0
  62. data/app/views/blocks/examples/dashboard-03.html.erb +210 -0
  63. data/app/views/blocks/examples/settings-01.html.erb +220 -0
  64. data/app/views/blocks/examples/settings-02.html.erb +231 -0
  65. data/app/views/blocks/examples/settings-03.html.erb +340 -0
  66. data/app/views/blocks/index.html.erb +65 -0
  67. data/app/views/docs/_sidebar.html.erb +47 -0
  68. data/app/views/docs/_toc.html.erb +19 -0
  69. data/app/views/docs/about.html.erb +68 -0
  70. data/app/views/docs/components/accordion.html.erb +196 -0
  71. data/app/views/docs/components/alert.html.erb +272 -0
  72. data/app/views/docs/components/alert_dialog.html.erb +232 -0
  73. data/app/views/docs/components/avatar.html.erb +207 -0
  74. data/app/views/docs/components/badge.html.erb +145 -0
  75. data/app/views/docs/components/breadcrumb.html.erb +264 -0
  76. data/app/views/docs/components/button.html.erb +229 -0
  77. data/app/views/docs/components/card.html.erb +378 -0
  78. data/app/views/docs/components/checkbox.html.erb +212 -0
  79. data/app/views/docs/components/collapsible.html.erb +252 -0
  80. data/app/views/docs/components/dialog.html.erb +323 -0
  81. data/app/views/docs/components/dropdown_menu.html.erb +289 -0
  82. data/app/views/docs/components/hover_card.html.erb +220 -0
  83. data/app/views/docs/components/input.html.erb +254 -0
  84. data/app/views/docs/components/label.html.erb +128 -0
  85. data/app/views/docs/components/main.html.erb +352 -0
  86. data/app/views/docs/components/navbar.html.erb +394 -0
  87. data/app/views/docs/components/navigation_menu.html.erb +226 -0
  88. data/app/views/docs/components/popover.html.erb +267 -0
  89. data/app/views/docs/components/progress.html.erb +107 -0
  90. data/app/views/docs/components/radio_group.html.erb +209 -0
  91. data/app/views/docs/components/select.html.erb +260 -0
  92. data/app/views/docs/components/separator.html.erb +162 -0
  93. data/app/views/docs/components/sheet.html.erb +270 -0
  94. data/app/views/docs/components/sidebar.html.erb +597 -0
  95. data/app/views/docs/components/skeleton.html.erb +150 -0
  96. data/app/views/docs/components/slider.html.erb +218 -0
  97. data/app/views/docs/components/spinner.html.erb +132 -0
  98. data/app/views/docs/components/switch.html.erb +148 -0
  99. data/app/views/docs/components/table.html.erb +259 -0
  100. data/app/views/docs/components/tabs.html.erb +225 -0
  101. data/app/views/docs/components/textarea.html.erb +239 -0
  102. data/app/views/docs/components/theme_toggle.html.erb +135 -0
  103. data/app/views/docs/components/toast.html.erb +205 -0
  104. data/app/views/docs/components/toaster.html.erb +227 -0
  105. data/app/views/docs/components/toggle.html.erb +154 -0
  106. data/app/views/docs/components/tooltip.html.erb +216 -0
  107. data/app/views/docs/components/typography.html.erb +180 -0
  108. data/app/views/docs/index.html.erb +143 -0
  109. data/app/views/docs/installation.html.erb +155 -0
  110. data/app/views/docs/simple_test.html.erb +13 -0
  111. data/app/views/docs/test_accordion.html.erb +14 -0
  112. data/app/views/docs/usage.html.erb +272 -0
  113. data/app/views/layouts/application.html.erb +107 -0
  114. data/app/views/layouts/backdrop.html.erb +77 -0
  115. data/app/views/shared/_app_navbar.html.erb +240 -0
  116. data/app/views/shared/_navbar.html.erb +69 -0
  117. data/app/views/showcase/v2/_components_grid.html.erb +38 -0
  118. data/app/views/showcase/v2/_features.html.erb +59 -0
  119. data/app/views/showcase/v2/_forms.html.erb +195 -0
  120. data/app/views/showcase/v2/_hero.html.erb +55 -0
  121. data/app/views/showcase/v2/_metrics.html.erb +107 -0
  122. data/app/views/showcase/v2.html.erb +18 -0
  123. data/lib/m9sh/version.rb +1 -1
  124. data/m9sh.gemspec +1 -1
  125. metadata +120 -1
@@ -0,0 +1,103 @@
1
+ # frozen_string_literal: true
2
+
3
+ class BackdropComponent < ViewComponent::Base
4
+ renders_one :navbar
5
+ renders_one :left_sidebar
6
+ renders_one :right_sidebar
7
+ renders_one :main_content
8
+
9
+ def initialize(
10
+ show_left_sidebar: true,
11
+ show_right_sidebar: true,
12
+ left_sidebar_width: "w-[300px]",
13
+ right_sidebar_width: "w-[300px]",
14
+ **extra_attrs
15
+ )
16
+ @show_left_sidebar = show_left_sidebar
17
+ @show_right_sidebar = show_right_sidebar
18
+ @left_sidebar_width = left_sidebar_width
19
+ @right_sidebar_width = right_sidebar_width
20
+ @extra_attrs = extra_attrs
21
+ super()
22
+ end
23
+
24
+ def call
25
+ tag.div(**component_attrs("h-screen w-screen flex flex-col overflow-hidden"), data: { controller: "backdrop" }) do
26
+ safe_join([
27
+ render_navbar_section,
28
+ render_overlay,
29
+ render_content_area
30
+ ].compact)
31
+ end
32
+ end
33
+
34
+ private
35
+
36
+ def component_attrs(default_classes)
37
+ classes = [default_classes, @extra_attrs[:class]].compact.join(" ")
38
+ @extra_attrs.merge(class: classes)
39
+ end
40
+
41
+ def render_navbar_section
42
+ return unless navbar?
43
+
44
+ # Render navbar with toggle buttons passed as slots
45
+ navbar.to_s.html_safe
46
+ end
47
+
48
+ def render_content_area
49
+ tag.div(class: "flex-1 flex overflow-hidden") do
50
+ safe_join([
51
+ render_left_sidebar_section,
52
+ render_main_content_section,
53
+ render_right_sidebar_section
54
+ ].compact)
55
+ end
56
+ end
57
+
58
+ def render_left_sidebar_section
59
+ return unless @show_left_sidebar && left_sidebar?
60
+
61
+ # Convert width to responsive: w-[300px] -> w-screen md:w-[300px]
62
+ desktop_width = @left_sidebar_width.start_with?('w-') ? "md:#{@left_sidebar_width}" : "md:w-[300px]"
63
+
64
+ tag.div(
65
+ class: "pt-16 w-screen #{desktop_width} flex-shrink-0 overflow-y-auto fixed md:relative left-0 md:left-auto -translate-x-full md:translate-x-0 top-0 bottom-0 md:bottom-auto z-40 md:z-auto transition-all duration-300 ease-in-out bg-transparent md:bg-background shadow-xl md:shadow-none md:border-r md:border-transparent",
66
+ data: { backdrop_target: "leftSidebar" }
67
+ ) do
68
+ left_sidebar.to_s.html_safe
69
+ end
70
+ end
71
+
72
+ def render_main_content_section
73
+ return unless main_content?
74
+
75
+ tag.div(class: "pt-16 flex-1 overflow-auto") do
76
+ main_content.to_s.html_safe
77
+ end
78
+ end
79
+
80
+ def render_right_sidebar_section
81
+ return unless @show_right_sidebar && right_sidebar?
82
+
83
+ # Convert width to responsive: w-[300px] -> w-screen md:w-[300px]
84
+ desktop_width = @right_sidebar_width.start_with?('w-') ? "md:#{@right_sidebar_width}" : "md:w-[300px]"
85
+
86
+ tag.div(
87
+ class: "pt-16 w-screen #{desktop_width} flex-shrink-0 overflow-y-auto fixed md:relative right-0 md:right-auto translate-x-full md:translate-x-0 top-0 bottom-0 md:bottom-auto z-40 md:z-auto transition-all duration-300 ease-in-out bg-transparent md:bg-background shadow-xl md:shadow-none md:border-l md:border-transparent",
88
+ data: { backdrop_target: "rightSidebar" }
89
+ ) do
90
+ right_sidebar.to_s.html_safe
91
+ end
92
+ end
93
+
94
+ def render_overlay
95
+ tag.div(
96
+ class: "hidden fixed top-16 left-0 right-0 bottom-0 bg-background/80 backdrop-blur-sm z-30 md:hidden",
97
+ data: {
98
+ backdrop_target: "overlay",
99
+ action: "click->backdrop#closeAll"
100
+ }
101
+ )
102
+ end
103
+ end
@@ -0,0 +1,56 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "rouge"
4
+
5
+ module Docs
6
+ class CodeBlockComponent < ViewComponent::Base
7
+ attr_reader :code, :language, :show_copy
8
+
9
+ def initialize(code:, language: "ruby", show_copy: true)
10
+ @code = code.strip
11
+ @language = language
12
+ @show_copy = show_copy
13
+ end
14
+
15
+ def call
16
+ tag.div(class: "relative my-4") do
17
+ safe_join([
18
+ copy_button,
19
+ highlighted_code
20
+ ])
21
+ end
22
+ end
23
+
24
+ private
25
+
26
+ def copy_button
27
+ return unless @show_copy
28
+
29
+ tag.div(class: "absolute right-2 top-2") do
30
+ render M9sh::ButtonComponent.new(
31
+ variant: :ghost,
32
+ size: :sm,
33
+ data: {
34
+ controller: "docs--copy-button",
35
+ action: "click->docs--copy-button#copy",
36
+ docs__copy_button_text_value: @code
37
+ }
38
+ ) do
39
+ "Copy"
40
+ end
41
+ end
42
+ end
43
+
44
+ def highlighted_code
45
+ formatter = Rouge::Formatters::HTML.new
46
+ lexer = Rouge::Lexer.find(@language) || Rouge::Lexers::PlainText.new
47
+ highlighted = formatter.format(lexer.lex(@code))
48
+
49
+ tag.div(class: "overflow-x-auto rounded-lg border border-border bg-muted") do
50
+ tag.pre(class: "p-4 text-sm") do
51
+ tag.code(highlighted.html_safe, class: "language-#{@language}")
52
+ end
53
+ end
54
+ end
55
+ end
56
+ end
@@ -0,0 +1,16 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Docs
4
+ class ComponentApiComponent < ViewComponent::Base
5
+ def initialize
6
+ # No parameters needed for API section wrapper
7
+ end
8
+
9
+ def call
10
+ tag.div(id: "api-reference", class: "space-y-4") do
11
+ concat(tag.h2("API Reference", class: "text-2xl font-semibold border-b border-border pb-2"))
12
+ concat(tag.div(class: "space-y-6") { content })
13
+ end
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,16 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Docs
4
+ class ComponentExamplesComponent < ViewComponent::Base
5
+ def initialize
6
+ # No parameters needed for examples section wrapper
7
+ end
8
+
9
+ def call
10
+ tag.div(id: "examples", class: "space-y-6") do
11
+ concat(tag.h2("Examples", class: "text-2xl font-semibold border-b border-border pb-2"))
12
+ concat(tag.div(class: "space-y-6") { content })
13
+ end
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,8 @@
1
+ <div class="space-y-4">
2
+ <h1 class="scroll-m-20 text-4xl font-bold tracking-tight lg:text-5xl">
3
+ <%= name %>
4
+ </h1>
5
+ <p class="text-lg text-muted-foreground">
6
+ <%= description %>
7
+ </p>
8
+ </div>
@@ -0,0 +1,14 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Docs
4
+ class ComponentHeaderComponent < ViewComponent::Base
5
+ def initialize(name:, description:)
6
+ @name = name
7
+ @description = description
8
+ end
9
+
10
+ private
11
+
12
+ attr_reader :name, :description
13
+ end
14
+ end
@@ -0,0 +1,15 @@
1
+ <div id="installation" class="space-y-4">
2
+ <h2 class="text-2xl font-semibold border-b border-border pb-2">Installation</h2>
3
+ <div class="space-y-3">
4
+ <p class="text-sm text-muted-foreground">
5
+ Install the component using the M9sh CLI (with automatic dependency resolution) or by copying the files manually.
6
+ </p>
7
+ <%= render Docs::CodeBlockComponent.new(
8
+ code: "m9sh add #{component_name}",
9
+ language: "bash"
10
+ ) %>
11
+ <p class="text-sm text-muted-foreground mt-4">
12
+ Or copy the component files manually from the documentation into your project's <%= render M9sh::BadgeComponent.new(variant: :secondary) do %>app/components/m9sh<% end %> directory.
13
+ </p>
14
+ </div>
15
+ </div>
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Docs
4
+ class ComponentInstallationComponent < ViewComponent::Base
5
+ def initialize(component_name:)
6
+ @component_name = component_name
7
+ end
8
+
9
+ private
10
+
11
+ attr_reader :component_name
12
+ end
13
+ end
@@ -0,0 +1,9 @@
1
+ <% content_for :title, "#{title} - m9sh Components" %>
2
+
3
+ <div class="space-y-8">
4
+ <%= header if header? %>
5
+ <%= installation if installation? %>
6
+ <%= usage if usage? %>
7
+ <%= examples if examples? %>
8
+ <%= api if api? %>
9
+ </div>
@@ -0,0 +1,19 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Docs
4
+ class ComponentPageComponent < ViewComponent::Base
5
+ renders_one :header, Docs::ComponentHeaderComponent
6
+ renders_one :installation, Docs::ComponentInstallationComponent
7
+ renders_one :usage, Docs::ComponentUsageComponent
8
+ renders_one :examples, Docs::ComponentExamplesComponent
9
+ renders_one :api, Docs::ComponentApiComponent
10
+
11
+ def initialize(title:)
12
+ @title = title
13
+ end
14
+
15
+ private
16
+
17
+ attr_reader :title
18
+ end
19
+ end
@@ -0,0 +1,318 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Docs
4
+ class ComponentPreviewComponent < ViewComponent::Base
5
+ attr_reader :title, :preview_content, :code, :ai_command, :mobile_optimized
6
+
7
+ def initialize(title: nil, preview_content: nil, code: nil, ai_command: nil, mobile_optimized: true)
8
+ @title = title
9
+ @preview_content = preview_content
10
+ @code = code
11
+ @ai_command = ai_command
12
+ @mobile_optimized = mobile_optimized
13
+ end
14
+
15
+ def call
16
+ tag.div(
17
+ class: "my-6",
18
+ data: {
19
+ controller: "docs--component-preview",
20
+ docs__component_preview_default_tab_value: "preview"
21
+ }
22
+ ) do
23
+ safe_join([
24
+ header_section,
25
+ preview_container
26
+ ].compact)
27
+ end
28
+ end
29
+
30
+ private
31
+
32
+ def header_section
33
+ return unless @title
34
+
35
+ tag.div(class: "flex items-center justify-between mb-4") do
36
+ tag.h3(@title, class: "text-lg font-semibold")
37
+ end
38
+ end
39
+
40
+ def preview_container
41
+ tag.div(class: "rounded-lg border border-border bg-card overflow-hidden") do
42
+ safe_join([
43
+ tabs_header,
44
+ tabs_content
45
+ ])
46
+ end
47
+ end
48
+
49
+ def tabs_header
50
+ tag.div(class: "flex items-center justify-between border-b border-border bg-muted") do
51
+ safe_join([
52
+ tabs_list,
53
+ controls_section
54
+ ])
55
+ end
56
+ end
57
+
58
+ def tabs_list
59
+ tag.div(
60
+ class: "flex items-center gap-1 p-1",
61
+ data: { docs__component_preview_target: "tabList" }
62
+ ) do
63
+ safe_join([
64
+ tab_trigger("Preview", "preview", true),
65
+ (@mobile_optimized ? tab_trigger("iOS", "ios") : nil),
66
+ (@mobile_optimized ? tab_trigger("Android", "android") : nil),
67
+ (@code ? tab_trigger("Code", "code") : nil),
68
+ (@ai_command ? tab_trigger("AI Commands", "ai") : nil)
69
+ ].compact)
70
+ end
71
+ end
72
+
73
+ def tab_trigger(label, value, active = false)
74
+ tag.button(
75
+ label,
76
+ class: "px-3 py-1.5 text-sm font-medium rounded-md transition-colors hover:bg-accent " \
77
+ "#{active ? 'bg-background text-foreground shadow-sm' : 'text-muted-foreground'}",
78
+ data: {
79
+ docs__component_preview_target: "tab",
80
+ action: "click->docs--component-preview#selectTab",
81
+ tab_value: value,
82
+ state: active ? "active" : "inactive"
83
+ },
84
+ aria: { selected: active.to_s }
85
+ )
86
+ end
87
+
88
+ def controls_section
89
+ tag.div(class: "flex items-center gap-1 p-1") do
90
+ safe_join([
91
+ copy_button,
92
+ theme_toggle_button,
93
+ fullscreen_button
94
+ ])
95
+ end
96
+ end
97
+
98
+ def copy_button
99
+ return unless @code
100
+
101
+ tag.button(
102
+ class: "p-2 rounded-md hover:bg-accent transition-colors text-muted-foreground hover:text-foreground",
103
+ data: {
104
+ action: "click->docs--component-preview#copyCode",
105
+ docs__component_preview_target: "copyButton"
106
+ },
107
+ title: "Copy code"
108
+ ) do
109
+ copy_icon
110
+ end
111
+ end
112
+
113
+ def theme_toggle_button
114
+ tag.button(
115
+ class: "p-2 rounded-md hover:bg-accent transition-colors text-muted-foreground hover:text-foreground",
116
+ data: {
117
+ action: "click->docs--component-preview#toggleTheme",
118
+ docs__component_preview_target: "themeButton"
119
+ },
120
+ title: "Toggle theme"
121
+ ) do
122
+ theme_icon
123
+ end
124
+ end
125
+
126
+ def fullscreen_button
127
+ tag.button(
128
+ class: "p-2 rounded-md hover:bg-accent transition-colors text-muted-foreground hover:text-foreground",
129
+ data: {
130
+ action: "click->docs--component-preview#toggleFullscreen",
131
+ docs__component_preview_target: "fullscreenButton"
132
+ },
133
+ title: "Toggle fullscreen"
134
+ ) do
135
+ fullscreen_icon
136
+ end
137
+ end
138
+
139
+ def tabs_content
140
+ tag.div(
141
+ class: "relative",
142
+ data: { docs__component_preview_target: "previewContainer" }
143
+ ) do
144
+ safe_join([
145
+ preview_panel,
146
+ (@mobile_optimized ? ios_panel : nil),
147
+ (@mobile_optimized ? android_panel : nil),
148
+ (@code ? code_panel : nil),
149
+ (@ai_command ? ai_panel : nil)
150
+ ].compact)
151
+ end
152
+ end
153
+
154
+ def preview_panel
155
+ tab_panel("preview", true) do
156
+ tag.div(class: "p-8 min-h-[300px] flex items-center justify-center") do
157
+ preview_content_to_render
158
+ end
159
+ end
160
+ end
161
+
162
+ def ios_panel
163
+ tab_panel("ios") do
164
+ tag.div(class: "p-8 flex items-center justify-center bg-muted") do
165
+ tag.div(class: "ios-frame relative bg-background rounded-[3rem] shadow-2xl border-[14px] border-gray-800 overflow-hidden") do
166
+ safe_join([
167
+ ios_notch,
168
+ tag.div(class: "h-[812px] w-[375px] overflow-y-auto p-4 flex items-center justify-center") do
169
+ preview_content_to_render
170
+ end
171
+ ])
172
+ end
173
+ end
174
+ end
175
+ end
176
+
177
+ def android_panel
178
+ tab_panel("android") do
179
+ tag.div(class: "p-8 flex items-center justify-center bg-muted") do
180
+ tag.div(class: "android-frame relative bg-background rounded-[2rem] shadow-2xl border-[12px] border-gray-900 overflow-hidden") do
181
+ safe_join([
182
+ tag.div(class: "h-[800px] w-[360px] overflow-y-auto p-4 flex items-center justify-center") do
183
+ preview_content_to_render
184
+ end,
185
+ android_nav_bar
186
+ ])
187
+ end
188
+ end
189
+ end
190
+ end
191
+
192
+ def preview_content_to_render
193
+ # Use block content if provided, otherwise use the preview_content parameter
194
+ content.present? ? content : @preview_content
195
+ end
196
+
197
+ def code_panel
198
+ tab_panel("code") do
199
+ tag.div(class: "p-4") do
200
+ render Docs::CodeBlockComponent.new(
201
+ code: @code,
202
+ language: "erb",
203
+ show_copy: false
204
+ )
205
+ end
206
+ end
207
+ end
208
+
209
+ def ai_panel
210
+ tab_panel("ai") do
211
+ tag.div(class: "p-6 space-y-4") do
212
+ safe_join([
213
+ tag.p("Use this component with the following command:", class: "text-sm text-muted-foreground mb-4"),
214
+ render(Docs::CodeBlockComponent.new(
215
+ code: @ai_command,
216
+ language: "erb",
217
+ show_copy: true
218
+ ))
219
+ ])
220
+ end
221
+ end
222
+ end
223
+
224
+ def tab_panel(value, active = false)
225
+ tag.div(
226
+ class: "#{active ? '' : 'hidden'}",
227
+ data: {
228
+ docs__component_preview_target: "panel",
229
+ panel_value: value
230
+ },
231
+ aria: { hidden: (!active).to_s }
232
+ ) do
233
+ yield
234
+ end
235
+ end
236
+
237
+ # SVG Icons
238
+ def copy_icon
239
+ tag.svg(
240
+ xmlns: "http://www.w3.org/2000/svg",
241
+ width: "16",
242
+ height: "16",
243
+ viewBox: "0 0 24 24",
244
+ fill: "none",
245
+ stroke: "currentColor",
246
+ stroke_width: "2",
247
+ stroke_linecap: "round",
248
+ stroke_linejoin: "round"
249
+ ) do
250
+ safe_join([
251
+ tag.rect(x: "9", y: "9", width: "13", height: "13", rx: "2", ry: "2"),
252
+ tag.path(d: "M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1")
253
+ ])
254
+ end
255
+ end
256
+
257
+ def theme_icon
258
+ tag.svg(
259
+ xmlns: "http://www.w3.org/2000/svg",
260
+ width: "16",
261
+ height: "16",
262
+ viewBox: "0 0 24 24",
263
+ fill: "none",
264
+ stroke: "currentColor",
265
+ stroke_width: "2",
266
+ stroke_linecap: "round",
267
+ stroke_linejoin: "round"
268
+ ) do
269
+ safe_join([
270
+ tag.circle(cx: "12", cy: "12", r: "5"),
271
+ tag.line(x1: "12", y1: "1", x2: "12", y2: "3"),
272
+ tag.line(x1: "12", y1: "21", x2: "12", y2: "23"),
273
+ tag.line(x1: "4.22", y1: "4.22", x2: "5.64", y2: "5.64"),
274
+ tag.line(x1: "18.36", y1: "18.36", x2: "19.78", y2: "19.78"),
275
+ tag.line(x1: "1", y1: "12", x2: "3", y2: "12"),
276
+ tag.line(x1: "21", y1: "12", x2: "23", y2: "12"),
277
+ tag.line(x1: "4.22", y1: "19.78", x2: "5.64", y2: "18.36"),
278
+ tag.line(x1: "18.36", y1: "5.64", x2: "19.78", y2: "4.22")
279
+ ])
280
+ end
281
+ end
282
+
283
+ def fullscreen_icon
284
+ tag.svg(
285
+ xmlns: "http://www.w3.org/2000/svg",
286
+ width: "16",
287
+ height: "16",
288
+ viewBox: "0 0 24 24",
289
+ fill: "none",
290
+ stroke: "currentColor",
291
+ stroke_width: "2",
292
+ stroke_linecap: "round",
293
+ stroke_linejoin: "round"
294
+ ) do
295
+ safe_join([
296
+ tag.path(d: "M8 3H5a2 2 0 0 0-2 2v3"),
297
+ tag.path(d: "M21 8V5a2 2 0 0 0-2-2h-3"),
298
+ tag.path(d: "M3 16v3a2 2 0 0 0 2 2h3"),
299
+ tag.path(d: "M16 21h3a2 2 0 0 0 2-2v-3")
300
+ ])
301
+ end
302
+ end
303
+
304
+ def ios_notch
305
+ tag.div(class: "absolute top-0 left-1/2 -translate-x-1/2 w-[200px] h-[30px] bg-gray-800 rounded-b-3xl z-10")
306
+ end
307
+
308
+ def android_nav_bar
309
+ tag.div(class: "absolute bottom-0 left-0 right-0 h-[40px] bg-gray-900 flex items-center justify-center gap-8") do
310
+ safe_join([
311
+ tag.div(class: "w-2 h-2 rounded-full bg-gray-500"),
312
+ tag.div(class: "w-2 h-2 rounded-full bg-gray-500"),
313
+ tag.div(class: "w-2 h-2 rounded-full bg-gray-500")
314
+ ])
315
+ end
316
+ end
317
+ end
318
+ end
@@ -0,0 +1,18 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Docs
4
+ class ComponentUsageComponent < ViewComponent::Base
5
+ renders_many :code_examples
6
+
7
+ def initialize
8
+ # No parameters needed for usage section wrapper
9
+ end
10
+
11
+ def call
12
+ tag.div(id: "usage", class: "space-y-4") do
13
+ concat(tag.h2("Usage", class: "text-2xl font-semibold border-b border-border pb-2"))
14
+ concat(tag.div(class: "space-y-4") { content })
15
+ end
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,64 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Docs
4
+ class PropTableComponent < ViewComponent::Base
5
+ attr_reader :props
6
+
7
+ def initialize(props:)
8
+ @props = props
9
+ end
10
+
11
+ def call
12
+ tag.div(class: "my-6 overflow-x-auto") do
13
+ tag.table(class: "w-full border-collapse") do
14
+ safe_join([
15
+ table_header,
16
+ table_body
17
+ ])
18
+ end
19
+ end
20
+ end
21
+
22
+ private
23
+
24
+ def table_header
25
+ tag.thead(class: "border-b border-border") do
26
+ tag.tr do
27
+ safe_join([
28
+ tag.th("Prop", class: "px-4 py-2 text-left text-sm font-semibold"),
29
+ tag.th("Type", class: "px-4 py-2 text-left text-sm font-semibold"),
30
+ tag.th("Default", class: "px-4 py-2 text-left text-sm font-semibold"),
31
+ tag.th("Description", class: "px-4 py-2 text-left text-sm font-semibold")
32
+ ])
33
+ end
34
+ end
35
+ end
36
+
37
+ def table_body
38
+ tag.tbody do
39
+ safe_join(
40
+ @props.map do |prop|
41
+ tag.tr(class: "border-b border-border hover:bg-muted") do
42
+ safe_join([
43
+ tag.td(class: "px-4 py-3") do
44
+ render(M9sh::BadgeComponent.new(variant: :secondary)) { prop[:name] }
45
+ end,
46
+ tag.td(class: "px-4 py-3") do
47
+ render(M9sh::BadgeComponent.new(variant: :outline)) { prop[:type] }
48
+ end,
49
+ tag.td(class: "px-4 py-3") do
50
+ if prop[:default]
51
+ render(M9sh::BadgeComponent.new(variant: :secondary)) { prop[:default] }
52
+ else
53
+ tag.span("-", class: "text-muted-foreground")
54
+ end
55
+ end,
56
+ tag.td(prop[:description], class: "px-4 py-3 text-sm text-muted-foreground")
57
+ ])
58
+ end
59
+ end
60
+ )
61
+ end
62
+ end
63
+ end
64
+ end
@@ -0,0 +1,3 @@
1
+ class ApplicationController < ActionController::Base
2
+ protect_from_forgery with: :exception
3
+ end