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
@@ -55,28 +55,32 @@ export default class extends Controller {
55
55
  if (!this.hasContentTarget) return
56
56
 
57
57
  const triggerRect = this.triggerTarget.getBoundingClientRect()
58
- const contentRect = this.contentTarget.getBoundingClientRect()
59
58
 
60
- // Position based on side
59
+ // Reset styles
60
+ this.contentTarget.style.top = ""
61
+ this.contentTarget.style.bottom = ""
62
+ this.contentTarget.style.left = ""
63
+ this.contentTarget.style.right = ""
64
+ this.contentTarget.style.transform = ""
65
+
66
+ // Position based on side using fixed positioning
61
67
  switch (this.sideValue) {
62
68
  case "top":
63
- this.contentTarget.style.bottom = "100%"
64
- this.contentTarget.style.marginBottom = "0.5rem"
69
+ this.contentTarget.style.top = `${triggerRect.top - 8}px`
70
+ this.contentTarget.style.transform = "translateY(-100%)"
65
71
  this.contentTarget.setAttribute("data-side", "top")
66
72
  break
67
73
  case "bottom":
68
- this.contentTarget.style.top = "100%"
69
- this.contentTarget.style.marginTop = "0.5rem"
74
+ this.contentTarget.style.top = `${triggerRect.bottom + 8}px`
70
75
  this.contentTarget.setAttribute("data-side", "bottom")
71
76
  break
72
77
  case "left":
73
- this.contentTarget.style.right = "100%"
74
- this.contentTarget.style.marginRight = "0.5rem"
78
+ this.contentTarget.style.left = `${triggerRect.left - 8}px`
79
+ this.contentTarget.style.transform = "translateX(-100%)"
75
80
  this.contentTarget.setAttribute("data-side", "left")
76
81
  break
77
82
  case "right":
78
- this.contentTarget.style.left = "100%"
79
- this.contentTarget.style.marginLeft = "0.5rem"
83
+ this.contentTarget.style.left = `${triggerRect.right + 8}px`
80
84
  this.contentTarget.setAttribute("data-side", "right")
81
85
  break
82
86
  }
@@ -85,27 +89,29 @@ export default class extends Controller {
85
89
  if (this.sideValue === "top" || this.sideValue === "bottom") {
86
90
  switch (this.alignValue) {
87
91
  case "start":
88
- this.contentTarget.style.left = "0"
92
+ this.contentTarget.style.left = `${triggerRect.left}px`
89
93
  break
90
94
  case "center":
91
- this.contentTarget.style.left = "50%"
92
- this.contentTarget.style.transform = "translateX(-50%)"
95
+ this.contentTarget.style.left = `${triggerRect.left + triggerRect.width / 2}px`
96
+ this.contentTarget.style.transform = (this.contentTarget.style.transform || "") + " translateX(-50%)"
93
97
  break
94
98
  case "end":
95
- this.contentTarget.style.right = "0"
99
+ this.contentTarget.style.left = `${triggerRect.right}px`
100
+ this.contentTarget.style.transform = (this.contentTarget.style.transform || "") + " translateX(-100%)"
96
101
  break
97
102
  }
98
103
  } else {
99
104
  switch (this.alignValue) {
100
105
  case "start":
101
- this.contentTarget.style.top = "0"
106
+ this.contentTarget.style.top = `${triggerRect.top}px`
102
107
  break
103
108
  case "center":
104
- this.contentTarget.style.top = "50%"
105
- this.contentTarget.style.transform = "translateY(-50%)"
109
+ this.contentTarget.style.top = `${triggerRect.top + triggerRect.height / 2}px`
110
+ this.contentTarget.style.transform = (this.contentTarget.style.transform || "") + " translateY(-50%)"
106
111
  break
107
112
  case "end":
108
- this.contentTarget.style.bottom = "0"
113
+ this.contentTarget.style.top = `${triggerRect.bottom}px`
114
+ this.contentTarget.style.transform = (this.contentTarget.style.transform || "") + " translateY(-100%)"
109
115
  break
110
116
  }
111
117
  }
@@ -8,14 +8,27 @@ export default class extends Controller {
8
8
  }
9
9
 
10
10
  connect() {
11
+ // Check if mobile first
12
+ this.isMobile = window.innerWidth < 768
13
+
11
14
  // Get initial state from localStorage or default
12
15
  const storageKey = this.storageKey
13
16
  const savedState = localStorage.getItem(storageKey)
14
- const defaultState = this.collapsibleValue === "none" ? "expanded" : "collapsed"
17
+
18
+ // For offcanvas sidebars: collapsed on mobile, expanded on desktop
19
+ // For other types: expanded if none, collapsed otherwise
20
+ let defaultState
21
+ if (this.collapsibleValue === "offcanvas") {
22
+ defaultState = this.isMobile ? "collapsed" : "expanded"
23
+ } else {
24
+ defaultState = this.collapsibleValue === "none" ? "expanded" : "collapsed"
25
+ }
26
+
15
27
  this.state = savedState || defaultState
16
28
 
17
29
  // Set initial state
18
30
  this.element.dataset.state = this.state
31
+ this.element.dataset.mobile = this.isMobile
19
32
 
20
33
  // Apply initial classes/transforms
21
34
  this.applyState()
@@ -25,7 +38,6 @@ export default class extends Controller {
25
38
  document.addEventListener("keydown", this.boundHandleKeyboard)
26
39
 
27
40
  // Handle mobile detection
28
- this.checkMobile()
29
41
  window.addEventListener("resize", this.checkMobile.bind(this))
30
42
  }
31
43
 
@@ -89,14 +101,24 @@ export default class extends Controller {
89
101
  }
90
102
 
91
103
  checkMobile() {
104
+ const wasMobile = this.isMobile
92
105
  this.isMobile = window.innerWidth < 768
93
106
  this.element.dataset.mobile = this.isMobile
94
107
 
95
- // On mobile, default to collapsed
96
- if (this.isMobile && this.state === "expanded" && this.collapsibleValue === "offcanvas") {
97
- this.state = "collapsed"
98
- this.element.dataset.state = this.state
99
- this.applyState()
108
+ // Only update state if mobile status changed and sidebar is offcanvas
109
+ if (this.collapsibleValue === "offcanvas" && wasMobile !== this.isMobile) {
110
+ // Transitioning to mobile: collapse
111
+ if (this.isMobile && this.state === "expanded") {
112
+ this.state = "collapsed"
113
+ this.element.dataset.state = this.state
114
+ this.applyState()
115
+ }
116
+ // Transitioning to desktop: expand
117
+ else if (!this.isMobile && this.state === "collapsed") {
118
+ this.state = "expanded"
119
+ this.element.dataset.state = this.state
120
+ this.applyState()
121
+ }
100
122
  }
101
123
  }
102
124
 
data/lib/m9sh/config.rb CHANGED
@@ -82,23 +82,23 @@ module M9sh
82
82
  config_hash = {}
83
83
 
84
84
  print "Component namespace (default: M9sh): "
85
- namespace = gets.chomp
85
+ namespace = $stdin.gets.chomp
86
86
  config_hash["namespace"] = namespace.empty? ? "M9sh" : namespace
87
87
 
88
88
  print "Components path (default: app/components/m9sh): "
89
- components_path = gets.chomp
89
+ components_path = $stdin.gets.chomp
90
90
  config_hash["components_path"] = components_path.empty? ? "app/components/m9sh" : components_path
91
91
 
92
92
  print "JavaScript controllers path (default: app/javascript/controllers/m9sh): "
93
- js_path = gets.chomp
93
+ js_path = $stdin.gets.chomp
94
94
  config_hash["javascript_path"] = js_path.empty? ? "app/javascript/controllers/m9sh" : js_path
95
95
 
96
96
  print "Tailwind config path (default: tailwind.config.js): "
97
- tailwind = gets.chomp
97
+ tailwind = $stdin.gets.chomp
98
98
  config_hash["tailwind_config"] = tailwind.empty? ? "tailwind.config.js" : tailwind
99
99
 
100
100
  print "CSS file path (default: app/assets/stylesheets/application.tailwind.css): "
101
- css = gets.chomp
101
+ css = $stdin.gets.chomp
102
102
  config_hash["css_file"] = css.empty? ? "app/assets/stylesheets/application.tailwind.css" : css
103
103
 
104
104
  config_hash["style"] = "default"
data/lib/m9sh/registry.rb CHANGED
@@ -91,11 +91,11 @@ module M9sh
91
91
  # Get components grouped by category (for listing)
92
92
  def components_by_category
93
93
  categories = {
94
- "Base" => ["base", "utilities"],
94
+ "Base" => ["base", "utilities", "icon"],
95
95
  "Form Components" => ["button", "input", "label", "checkbox", "textarea", "select", "switch", "slider", "radio_group"],
96
96
  "Layout Components" => ["card", "table", "separator", "main"],
97
97
  "Feedback Components" => ["alert", "toast", "toaster", "progress", "spinner", "skeleton"],
98
- "Navigation Components" => ["breadcrumb", "navigation_menu", "sidebar", "tabs"],
98
+ "Navigation Components" => ["breadcrumb", "navigation_menu", "navbar", "sidebar", "tabs", "menu"],
99
99
  "Display Components" => ["avatar", "badge", "typography"],
100
100
  "Interactive Components" => ["accordion", "dialog", "alert_dialog", "sheet", "tooltip", "popover", "hover_card", "collapsible", "dropdown_menu", "toggle"],
101
101
  "Theme Components" => ["theme_toggle"]
@@ -16,6 +16,15 @@ components:
16
16
  - app/components/m9sh/utilities.rb
17
17
  dependencies: []
18
18
 
19
+ icon:
20
+ name: "Icon"
21
+ description: "SVG icon component with dynamic sizing and styling"
22
+ files:
23
+ - app/components/m9sh/icon_component.rb
24
+ dependencies:
25
+ - base
26
+ - utilities
27
+
19
28
  button:
20
29
  name: "Button"
21
30
  description: "Button component with multiple variants"
@@ -224,6 +233,13 @@ components:
224
233
  description: "Modal dialog component with Stimulus"
225
234
  files:
226
235
  - app/components/m9sh/dialog_component.rb
236
+ - app/components/m9sh/dialog_trigger_component.rb
237
+ - app/components/m9sh/dialog_content_component.rb
238
+ - app/components/m9sh/dialog_header_component.rb
239
+ - app/components/m9sh/dialog_title_component.rb
240
+ - app/components/m9sh/dialog_description_component.rb
241
+ - app/components/m9sh/dialog_footer_component.rb
242
+ - app/components/m9sh/dialog_close_component.rb
227
243
  - app/javascript/controllers/m9sh/dialog_controller.js
228
244
  dependencies:
229
245
  - base
@@ -382,3 +398,24 @@ components:
382
398
  dependencies:
383
399
  - base
384
400
  - utilities
401
+
402
+ menu:
403
+ name: "Menu"
404
+ description: "Menu component with item and separator slots"
405
+ files:
406
+ - app/components/m9sh/menu_component.rb
407
+ dependencies:
408
+ - base
409
+ - utilities
410
+
411
+ navbar:
412
+ name: "Navbar"
413
+ description: "Navigation bar component with mobile menu support"
414
+ files:
415
+ - app/components/m9sh/navbar_component.rb
416
+ dependencies:
417
+ - base
418
+ - utilities
419
+ - icon
420
+ - button
421
+ - dropdown_menu
data/lib/m9sh/version.rb CHANGED
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module M9sh
4
- VERSION = "0.1.0"
4
+ VERSION = "0.2.0"
5
5
  end
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Override tailwindcss-rails tasks since we use npm to build CSS
4
+ # This prevents the gem from interfering with asset precompilation
5
+
6
+ # Make tailwindcss:build a no-op since we build with npm
7
+ Rake::Task["tailwindcss:build"].clear if Rake::Task.task_defined?("tailwindcss:build")
8
+
9
+ namespace :tailwindcss do
10
+ desc "Build Tailwind CSS (using npm)"
11
+ task :build do
12
+ # No-op: CSS is built via npm run build:css
13
+ puts "Skipping tailwindcss:build task (CSS built via npm run build:css)"
14
+ end
15
+ end
data/m9sh.gemspec ADDED
@@ -0,0 +1,48 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "lib/m9sh/version"
4
+
5
+ Gem::Specification.new do |spec|
6
+ spec.name = "m9sh"
7
+ spec.version = M9sh::VERSION
8
+ spec.authors = ["Marcin Urbanski"]
9
+ spec.email = ["marcinn.urbanski@gmail.com"]
10
+
11
+ spec.summary = "Beautiful, accessible UI components for Rails with Hotwire"
12
+ spec.description = "M9sh is a component library for Rails applications using ViewComponent and Hotwire, inspired by shadcn/ui. Generate beautiful, accessible UI components with a powerful CLI."
13
+ spec.homepage = "https://github.com/veas-org/m9sh"
14
+ spec.license = "MIT"
15
+ spec.required_ruby_version = ">= 3.0.0"
16
+
17
+ spec.metadata["homepage_uri"] = spec.homepage
18
+ spec.metadata["source_code_uri"] = spec.homepage
19
+ spec.metadata["changelog_uri"] = "#{spec.homepage}/blob/main/CHANGELOG.md"
20
+ spec.metadata["documentation_uri"] = "#{spec.homepage}/blob/main/M9SH_CLI.md"
21
+
22
+ # Specify which files should be added to the gem when it is released.
23
+ # The `git ls-files -z` loads the files in the RubyGem that have been added into git.
24
+ spec.files = Dir.chdir(__dir__) do
25
+ `git ls-files -z`.split("\x0").reject do |f|
26
+ (File.expand_path(f) == __FILE__) ||
27
+ f.start_with?(*%w[bin/ test/ spec/ features/ .git .github appveyor Gemfile config/ tmp/ app/ db/ public/ log/])
28
+ end
29
+ end
30
+
31
+ # Include lib files explicitly
32
+ spec.files += Dir["lib/**/*.rb"]
33
+ spec.files += Dir["lib/**/*.yml"]
34
+ spec.files += Dir["app/components/m9sh/**/*.rb"]
35
+ spec.files += Dir["app/javascript/controllers/m9sh/**/*.js"]
36
+
37
+ spec.bindir = "exe"
38
+ spec.executables = ["m9sh"]
39
+ spec.require_paths = ["lib"]
40
+
41
+ # Runtime dependencies
42
+ spec.add_dependency "thor", "~> 1.3"
43
+ spec.add_dependency "view_component", "~> 3.0"
44
+
45
+ # Development dependencies
46
+ spec.add_development_dependency "bundler", "~> 2.0"
47
+ spec.add_development_dependency "rake", "~> 13.0"
48
+ end
data/publish.sh ADDED
@@ -0,0 +1,48 @@
1
+ #!/bin/bash
2
+ set -e
3
+
4
+ echo "🚀 Publishing M9sh gem to RubyGems..."
5
+
6
+ # Check if BUNDLE_TOKEN is set
7
+ if [ -z "$BUNDLE_TOKEN" ]; then
8
+ echo "❌ Error: BUNDLE_TOKEN environment variable is not set"
9
+ echo ""
10
+ echo "Please set your RubyGems API token:"
11
+ echo " export BUNDLE_TOKEN=your_rubygems_api_token"
12
+ echo ""
13
+ echo "You can get your API token from:"
14
+ echo " https://rubygems.org/profile/edit"
15
+ exit 1
16
+ fi
17
+
18
+ # Create credentials directory
19
+ mkdir -p ~/.gem
20
+
21
+ # Write credentials file
22
+ echo "📝 Configuring RubyGems credentials..."
23
+ cat > ~/.gem/credentials << EOF
24
+ ---
25
+ :rubygems_api_key: ${BUNDLE_TOKEN}
26
+ EOF
27
+
28
+ # Set proper permissions
29
+ chmod 0600 ~/.gem/credentials
30
+
31
+ echo "✅ Credentials configured"
32
+
33
+ # Build gem if needed
34
+ if [ ! -f "m9sh-0.1.0.gem" ]; then
35
+ echo "📦 Building gem..."
36
+ gem build m9sh.gemspec
37
+ fi
38
+
39
+ # Push to RubyGems
40
+ echo "📤 Publishing to RubyGems..."
41
+ gem push m9sh-0.1.0.gem
42
+
43
+ echo ""
44
+ echo "🎉 Successfully published M9sh gem!"
45
+ echo " View at: https://rubygems.org/gems/m9sh"
46
+ echo ""
47
+ echo "Users can now install with:"
48
+ echo " gem install m9sh"
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: m9sh
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.0
4
+ version: 0.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Marcin Urbanski
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2025-10-13 00:00:00.000000000 Z
11
+ date: 2025-10-17 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: thor
@@ -88,6 +88,10 @@ files:
88
88
  - ".mise.toml"
89
89
  - ".node-version"
90
90
  - Dockerfile
91
+ - GEM_README.md
92
+ - LICENSE.txt
93
+ - M9SH_CLI.md
94
+ - PUBLISHING.md
91
95
  - README.md
92
96
  - Rakefile
93
97
  - app/components/m9sh/accordion_component.rb
@@ -101,16 +105,27 @@ files:
101
105
  - app/components/m9sh/card_component.rb
102
106
  - app/components/m9sh/checkbox_component.rb
103
107
  - app/components/m9sh/collapsible_component.rb
108
+ - app/components/m9sh/color_customizer_component.rb
109
+ - app/components/m9sh/dialog_close_component.rb
104
110
  - app/components/m9sh/dialog_component.rb
111
+ - app/components/m9sh/dialog_content_component.rb
112
+ - app/components/m9sh/dialog_description_component.rb
113
+ - app/components/m9sh/dialog_footer_component.rb
114
+ - app/components/m9sh/dialog_header_component.rb
115
+ - app/components/m9sh/dialog_title_component.rb
116
+ - app/components/m9sh/dialog_trigger_component.rb
105
117
  - app/components/m9sh/dropdown_menu_component.rb
106
118
  - app/components/m9sh/dropdown_menu_content_component.rb
107
119
  - app/components/m9sh/dropdown_menu_item_component.rb
108
120
  - app/components/m9sh/dropdown_menu_separator_component.rb
109
121
  - app/components/m9sh/dropdown_menu_trigger_component.rb
110
122
  - app/components/m9sh/hover_card_component.rb
123
+ - app/components/m9sh/icon_component.rb
111
124
  - app/components/m9sh/input_component.rb
112
125
  - app/components/m9sh/label_component.rb
113
126
  - app/components/m9sh/main_component.rb
127
+ - app/components/m9sh/menu_component.rb
128
+ - app/components/m9sh/navbar_component.rb
114
129
  - app/components/m9sh/navigation_menu_component.rb
115
130
  - app/components/m9sh/popover_component.rb
116
131
  - app/components/m9sh/progress_component.rb
@@ -163,7 +178,6 @@ files:
163
178
  - components.json
164
179
  - config.ru
165
180
  - exe/m9sh
166
- - fix_namespaces.py
167
181
  - fly.toml
168
182
  - koyeb.yaml
169
183
  - lib/m9sh.rb
@@ -173,9 +187,12 @@ files:
173
187
  - lib/m9sh/registry.rb
174
188
  - lib/m9sh/registry.yml
175
189
  - lib/m9sh/version.rb
190
+ - lib/tasks/tailwindcss.rake
191
+ - m9sh.gemspec
176
192
  - package-lock.json
177
193
  - package.json
178
194
  - pnpm-lock.yaml
195
+ - publish.sh
179
196
  - tailwind.config.js
180
197
  - update_namespace.py
181
198
  homepage: https://github.com/veas-org/m9sh
data/fix_namespaces.py DELETED
@@ -1,32 +0,0 @@
1
- #!/usr/bin/env python3
2
- import os
3
- import re
4
-
5
- # Path to the components directory
6
- components_dir = '/Users/marcin/Projects/veas/hotcdn/app/components/m9sh'
7
-
8
- # Function to replace hotcdn__ with m9sh__ in a file
9
- def fix_file(filepath):
10
- with open(filepath, 'r') as f:
11
- content = f.read()
12
-
13
- # Replace all hotcdn__ with m9sh__
14
- updated_content = re.sub(r'hotcdn__', 'm9sh__', content)
15
-
16
- if content != updated_content:
17
- with open(filepath, 'w') as f:
18
- f.write(updated_content)
19
- print(f"Fixed: {filepath}")
20
- return True
21
- return False
22
-
23
- # Process all .rb files in the components directory
24
- fixed_count = 0
25
- for root, dirs, files in os.walk(components_dir):
26
- for file in files:
27
- if file.endswith('.rb'):
28
- filepath = os.path.join(root, file)
29
- if fix_file(filepath):
30
- fixed_count += 1
31
-
32
- print(f"\nFixed {fixed_count} files")