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
data/lib/m9sh/cli.rb ADDED
@@ -0,0 +1,234 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "thor"
4
+ require_relative "version"
5
+ require_relative "config"
6
+ require_relative "registry"
7
+ require_relative "generator"
8
+
9
+ module M9sh
10
+ class CLI < Thor
11
+ def self.exit_on_failure?
12
+ true
13
+ end
14
+
15
+ desc "init", "Initialize M9sh configuration"
16
+ method_option :interactive, type: :boolean, default: true, aliases: "-i", desc: "Interactive setup"
17
+ method_option :namespace, type: :string, desc: "Component namespace (e.g., MyApp)"
18
+ method_option :components_path, type: :string, desc: "Path to components directory"
19
+ method_option :javascript_path, type: :string, desc: "Path to JavaScript controllers"
20
+ def init
21
+ if Config.new.exists?
22
+ puts "⚠️ m9sh.yml already exists. Use --force to overwrite."
23
+ return unless options[:force]
24
+ end
25
+
26
+ if options[:interactive]
27
+ config = Config.interactive_setup
28
+ else
29
+ config_hash = Config::DEFAULT_CONFIG.dup
30
+ config_hash["namespace"] = options[:namespace] if options[:namespace]
31
+ config_hash["components_path"] = options[:components_path] if options[:components_path]
32
+ config_hash["javascript_path"] = options[:javascript_path] if options[:javascript_path]
33
+
34
+ config = Config.new
35
+ config.save_config(config_hash)
36
+ # Reload config to pick up the saved values
37
+ config = Config.new
38
+ end
39
+
40
+ # Create directories
41
+ FileUtils.mkdir_p(config.full_components_path)
42
+ FileUtils.mkdir_p(config.full_javascript_path)
43
+
44
+ puts "\n✨ M9sh initialized successfully!"
45
+ puts " Configuration saved to m9sh.yml"
46
+ puts " Component path: #{config.components_path}"
47
+ puts " JavaScript path: #{config.javascript_path}"
48
+ puts "\nNext steps:"
49
+ puts " Run 'bin/m9sh list' to see available components"
50
+ puts " Run 'bin/m9sh add button' to add a component"
51
+ end
52
+
53
+ desc "add COMPONENT", "Add a component (with dependencies)"
54
+ method_option :namespace, type: :string, desc: "Override namespace from config"
55
+ method_option :components_path, type: :string, desc: "Override components path"
56
+ method_option :javascript_path, type: :string, desc: "Override JavaScript path"
57
+ method_option :force, type: :boolean, default: false, aliases: "-f", desc: "Overwrite existing files"
58
+ method_option :all, type: :boolean, default: false, desc: "Install all components"
59
+ def add(component_name = nil)
60
+ ensure_config_exists!
61
+
62
+ config = Config.new
63
+ generator = Generator.new(Dir.pwd, config: config)
64
+
65
+ if options[:all]
66
+ puts "📦 Installing all components...\n\n"
67
+ result = generator.generate_all(
68
+ namespace: options[:namespace],
69
+ components_path: options[:components_path],
70
+ javascript_path: options[:javascript_path],
71
+ force: options[:force]
72
+ )
73
+ else
74
+ unless component_name
75
+ puts "❌ Please specify a component name or use --all"
76
+ puts " Run 'bin/m9sh list' to see available components"
77
+ return
78
+ end
79
+
80
+ puts "📦 Installing component: #{component_name}\n\n"
81
+ result = generator.generate(
82
+ component_name,
83
+ namespace: options[:namespace],
84
+ components_path: options[:components_path],
85
+ javascript_path: options[:javascript_path],
86
+ force: options[:force]
87
+ )
88
+ end
89
+
90
+ display_result(result)
91
+ end
92
+
93
+ desc "list", "List all available components"
94
+ method_option :installed, type: :boolean, default: false, aliases: "-i", desc: "Show only installed components"
95
+ method_option :available, type: :boolean, default: false, aliases: "-a", desc: "Show only available components"
96
+ def list
97
+ registry = Registry.new
98
+ config = Config.new if Config.new.exists?
99
+
100
+ puts "\n🎨 M9sh Components\n\n"
101
+
102
+ categories = registry.components_by_category
103
+
104
+ categories.each do |category, components|
105
+ puts "#{category}:"
106
+ components.each do |comp_name|
107
+ next unless registry.exists?(comp_name)
108
+
109
+ installed = registry.installed?(comp_name)
110
+ status = installed ? "✅" : " "
111
+
112
+ next if options[:installed] && !installed
113
+ next if options[:available] && installed
114
+
115
+ description = registry.description_for(comp_name)
116
+ puts " #{status} #{comp_name.ljust(20)} - #{description}"
117
+ end
118
+ puts ""
119
+ end
120
+
121
+ puts "\nUsage:"
122
+ puts " bin/m9sh add <component> Install a component"
123
+ puts " bin/m9sh add --all Install all components"
124
+ puts " bin/m9sh list -i Show installed components"
125
+ puts " bin/m9sh list -a Show available components"
126
+ end
127
+
128
+ desc "sync COMPONENT", "Update a component to the latest version"
129
+ method_option :all, type: :boolean, default: false, desc: "Sync all components"
130
+ method_option :namespace, type: :string, desc: "Override namespace from config"
131
+ method_option :components_path, type: :string, desc: "Override components path"
132
+ method_option :javascript_path, type: :string, desc: "Override JavaScript path"
133
+ def sync(component_name = nil)
134
+ ensure_config_exists!
135
+
136
+ config = Config.new
137
+ generator = Generator.new(Dir.pwd, config: config)
138
+
139
+ if options[:all]
140
+ puts "🔄 Syncing all components...\n\n"
141
+ result = generator.sync_all(
142
+ namespace: options[:namespace],
143
+ components_path: options[:components_path],
144
+ javascript_path: options[:javascript_path]
145
+ )
146
+ else
147
+ unless component_name
148
+ puts "❌ Please specify a component name or use --all"
149
+ return
150
+ end
151
+
152
+ puts "🔄 Syncing component: #{component_name}\n\n"
153
+ result = generator.sync_component(
154
+ component_name,
155
+ namespace: options[:namespace],
156
+ components_path: options[:components_path],
157
+ javascript_path: options[:javascript_path]
158
+ )
159
+ end
160
+
161
+ display_result(result, sync: true)
162
+ end
163
+
164
+ desc "info COMPONENT", "Show information about a component"
165
+ def info(component_name)
166
+ registry = Registry.new
167
+
168
+ unless registry.exists?(component_name)
169
+ puts "❌ Component '#{component_name}' not found"
170
+ return
171
+ end
172
+
173
+ puts "\n📋 Component: #{registry.name_for(component_name)}\n"
174
+ puts "Description: #{registry.description_for(component_name)}"
175
+ puts "Installed: #{registry.installed?(component_name) ? '✅ Yes' : '❌ No'}"
176
+
177
+ dependencies = registry.dependencies_for(component_name)
178
+ if dependencies.any?
179
+ puts "\nDependencies:"
180
+ dependencies.each do |dep|
181
+ puts " - #{dep}"
182
+ end
183
+ end
184
+
185
+ files = registry.files_for(component_name)
186
+ if files.any?
187
+ puts "\nFiles:"
188
+ files.each do |file|
189
+ puts " - #{file}"
190
+ end
191
+ end
192
+
193
+ puts ""
194
+ end
195
+
196
+ desc "version", "Show M9sh CLI version"
197
+ def version
198
+ puts "M9sh CLI version #{M9sh::VERSION}"
199
+ end
200
+
201
+ private
202
+
203
+ def ensure_config_exists!
204
+ unless Config.new.exists?
205
+ puts "❌ Configuration not found. Run 'bin/m9sh init' first."
206
+ exit 1
207
+ end
208
+ end
209
+
210
+ def display_result(result, sync: false)
211
+ action = sync ? "synced" : "installed"
212
+
213
+ if result[:success]
214
+ if result[:installed].any?
215
+ puts "✅ Successfully #{action}:"
216
+ result[:installed].each { |comp| puts " - #{comp}" }
217
+ end
218
+
219
+ if result[:skipped].any?
220
+ puts "\n⏭️ Skipped (already exists):"
221
+ result[:skipped].each { |comp| puts " - #{comp}" }
222
+ puts "\n Use --force to overwrite existing files"
223
+ end
224
+
225
+ puts "\n🎉 Done!\n" if result[:installed].any?
226
+ else
227
+ puts "❌ Failed to #{action.delete_suffix('ed')}:"
228
+ result[:errors].each do |error|
229
+ puts " - #{error[:component]}: #{error[:error]}"
230
+ end
231
+ end
232
+ end
233
+ end
234
+ end
@@ -0,0 +1,114 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "yaml"
4
+ require "fileutils"
5
+
6
+ module M9sh
7
+ class Config
8
+ CONFIG_FILE = "m9sh.yml"
9
+
10
+ DEFAULT_CONFIG = {
11
+ "namespace" => "M9sh",
12
+ "components_path" => "app/components/m9sh",
13
+ "javascript_path" => "app/javascript/controllers/m9sh",
14
+ "tailwind_config" => "tailwind.config.js",
15
+ "css_file" => "app/assets/stylesheets/application.tailwind.css",
16
+ "style" => "default"
17
+ }.freeze
18
+
19
+ attr_reader :config
20
+
21
+ def initialize(root_path = Dir.pwd)
22
+ @root_path = root_path
23
+ @config_path = File.join(@root_path, CONFIG_FILE)
24
+ @config = load_config
25
+ end
26
+
27
+ def load_config
28
+ if File.exist?(@config_path)
29
+ YAML.load_file(@config_path)
30
+ else
31
+ DEFAULT_CONFIG.dup
32
+ end
33
+ end
34
+
35
+ def save_config(config_hash = @config)
36
+ File.write(@config_path, YAML.dump(config_hash))
37
+ end
38
+
39
+ def exists?
40
+ File.exist?(@config_path)
41
+ end
42
+
43
+ def get(key)
44
+ @config[key]
45
+ end
46
+
47
+ def set(key, value)
48
+ @config[key] = value
49
+ end
50
+
51
+ def namespace
52
+ get("namespace") || DEFAULT_CONFIG["namespace"]
53
+ end
54
+
55
+ def components_path
56
+ get("components_path") || DEFAULT_CONFIG["components_path"]
57
+ end
58
+
59
+ def javascript_path
60
+ get("javascript_path") || DEFAULT_CONFIG["javascript_path"]
61
+ end
62
+
63
+ def full_components_path
64
+ File.join(@root_path, components_path)
65
+ end
66
+
67
+ def full_javascript_path
68
+ File.join(@root_path, javascript_path)
69
+ end
70
+
71
+ def self.create_default(root_path = Dir.pwd)
72
+ config = new(root_path)
73
+ config.save_config(DEFAULT_CONFIG.dup)
74
+ config
75
+ end
76
+
77
+ def self.interactive_setup(root_path = Dir.pwd)
78
+ config = new(root_path)
79
+
80
+ puts "\n🎨 Welcome to M9sh component library setup!\n\n"
81
+
82
+ config_hash = {}
83
+
84
+ print "Component namespace (default: M9sh): "
85
+ namespace = gets.chomp
86
+ config_hash["namespace"] = namespace.empty? ? "M9sh" : namespace
87
+
88
+ print "Components path (default: app/components/m9sh): "
89
+ components_path = gets.chomp
90
+ config_hash["components_path"] = components_path.empty? ? "app/components/m9sh" : components_path
91
+
92
+ print "JavaScript controllers path (default: app/javascript/controllers/m9sh): "
93
+ js_path = gets.chomp
94
+ config_hash["javascript_path"] = js_path.empty? ? "app/javascript/controllers/m9sh" : js_path
95
+
96
+ print "Tailwind config path (default: tailwind.config.js): "
97
+ tailwind = gets.chomp
98
+ config_hash["tailwind_config"] = tailwind.empty? ? "tailwind.config.js" : tailwind
99
+
100
+ print "CSS file path (default: app/assets/stylesheets/application.tailwind.css): "
101
+ css = gets.chomp
102
+ config_hash["css_file"] = css.empty? ? "app/assets/stylesheets/application.tailwind.css" : css
103
+
104
+ config_hash["style"] = "default"
105
+
106
+ config.save_config(config_hash)
107
+ config.config.merge!(config_hash)
108
+
109
+ puts "\n✅ Configuration saved to m9sh.yml\n"
110
+
111
+ config
112
+ end
113
+ end
114
+ end
@@ -0,0 +1,183 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "fileutils"
4
+ require_relative "config"
5
+ require_relative "registry"
6
+
7
+ module M9sh
8
+ class Generator
9
+ attr_reader :config, :registry, :root_path
10
+
11
+ def initialize(root_path = Dir.pwd, config: nil)
12
+ @root_path = root_path
13
+ @config = config || Config.new(root_path)
14
+ @registry = Registry.new
15
+ end
16
+
17
+ # Generate a component with all its dependencies
18
+ def generate(component_name, options = {})
19
+ unless @registry.exists?(component_name)
20
+ return { success: false, error: "Component '#{component_name}' not found in registry" }
21
+ end
22
+
23
+ # Override config with options if provided
24
+ target_namespace = options[:namespace] || @config.namespace
25
+ target_components_path = options[:components_path] || @config.components_path
26
+ target_javascript_path = options[:javascript_path] || @config.javascript_path
27
+
28
+ # Resolve dependencies
29
+ components_to_install = @registry.resolve_dependencies(component_name)
30
+
31
+ results = {
32
+ success: true,
33
+ installed: [],
34
+ skipped: [],
35
+ errors: []
36
+ }
37
+
38
+ components_to_install.each do |comp_name|
39
+ result = install_component(
40
+ comp_name,
41
+ namespace: target_namespace,
42
+ components_path: target_components_path,
43
+ javascript_path: target_javascript_path,
44
+ force: options[:force]
45
+ )
46
+
47
+ if result[:success]
48
+ if result[:skipped]
49
+ results[:skipped] << comp_name
50
+ else
51
+ results[:installed] << comp_name
52
+ end
53
+ else
54
+ results[:errors] << { component: comp_name, error: result[:error] }
55
+ results[:success] = false
56
+ end
57
+ end
58
+
59
+ results
60
+ end
61
+
62
+ # Generate all components
63
+ def generate_all(options = {})
64
+ results = {
65
+ success: true,
66
+ installed: [],
67
+ skipped: [],
68
+ errors: []
69
+ }
70
+
71
+ @registry.all_component_names.each do |component_name|
72
+ result = generate(component_name, options)
73
+
74
+ results[:installed].concat(result[:installed])
75
+ results[:skipped].concat(result[:skipped])
76
+ results[:errors].concat(result[:errors])
77
+ results[:success] = false unless result[:success]
78
+ end
79
+
80
+ results
81
+ end
82
+
83
+ private
84
+
85
+ def install_component(component_name, namespace:, components_path:, javascript_path:, force: false)
86
+ files = @registry.files_for(component_name)
87
+
88
+ if files.empty?
89
+ return { success: false, error: "No files defined for component '#{component_name}'" }
90
+ end
91
+
92
+ # Check if already installed
93
+ if !force && @registry.installed?(component_name, @root_path)
94
+ return { success: true, skipped: true }
95
+ end
96
+
97
+ files.each do |source_file|
98
+ begin
99
+ copy_result = copy_file(source_file, namespace, components_path, javascript_path, force)
100
+ return copy_result unless copy_result[:success]
101
+ rescue StandardError => e
102
+ return { success: false, error: "Failed to copy #{source_file}: #{e.message}" }
103
+ end
104
+ end
105
+
106
+ { success: true }
107
+ end
108
+
109
+ def copy_file(source_file, namespace, components_path, javascript_path, force)
110
+ source_path = File.join(@root_path, source_file)
111
+
112
+ unless File.exist?(source_path)
113
+ return { success: false, error: "Source file not found: #{source_file}" }
114
+ end
115
+
116
+ # Determine target path
117
+ target_path = transform_path(source_file, components_path, javascript_path)
118
+ target_path = File.join(@root_path, target_path)
119
+
120
+ # Check if file exists and force is not set
121
+ if File.exist?(target_path) && !force
122
+ return { success: true, skipped: true }
123
+ end
124
+
125
+ # Create directory if needed
126
+ FileUtils.mkdir_p(File.dirname(target_path))
127
+
128
+ # Read and transform content
129
+ content = File.read(source_path)
130
+ transformed_content = transform_content(content, namespace)
131
+
132
+ # Write file
133
+ File.write(target_path, transformed_content)
134
+
135
+ { success: true, path: target_path }
136
+ end
137
+
138
+ def transform_path(original_path, components_path, javascript_path)
139
+ # Replace the default M9sh paths with custom paths
140
+ path = original_path.dup
141
+
142
+ if path.include?("app/components/m9sh")
143
+ path.sub("app/components/m9sh", components_path)
144
+ elsif path.include?("app/javascript/controllers/m9sh")
145
+ path.sub("app/javascript/controllers/m9sh", javascript_path)
146
+ else
147
+ path
148
+ end
149
+ end
150
+
151
+ def transform_content(content, target_namespace)
152
+ return content if target_namespace == "M9sh"
153
+
154
+ # Replace module declarations
155
+ content = content.gsub(/^module M9sh$/, "module #{target_namespace}")
156
+ content = content.gsub(/module M9sh\s*$/, "module #{target_namespace}")
157
+
158
+ # Replace class declarations
159
+ content = content.gsub(/class M9sh::/, "class #{target_namespace}::")
160
+
161
+ # Replace controller identifiers in data attributes (for JS)
162
+ content = content.gsub(/m9sh--/, "#{target_namespace.downcase}--")
163
+ content = content.gsub(/"m9sh--/, "\"#{target_namespace.downcase}--")
164
+ content = content.gsub(/'m9sh--/, "'#{target_namespace.downcase}--")
165
+
166
+ # Replace target names
167
+ content = content.gsub(/m9sh__/, "#{target_namespace.downcase}__")
168
+ content = content.gsub(/"m9sh__/, "\"#{target_namespace.downcase}__")
169
+ content = content.gsub(/'m9sh__/, "'#{target_namespace.downcase}__")
170
+
171
+ content
172
+ end
173
+
174
+ # Sync existing components (update them)
175
+ def sync_component(component_name, options = {})
176
+ generate(component_name, options.merge(force: true))
177
+ end
178
+
179
+ def sync_all(options = {})
180
+ generate_all(options.merge(force: true))
181
+ end
182
+ end
183
+ end
@@ -0,0 +1,107 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "yaml"
4
+
5
+ module M9sh
6
+ class Registry
7
+ REGISTRY_FILE = File.expand_path("../registry.yml", __FILE__)
8
+
9
+ attr_reader :components
10
+
11
+ def initialize
12
+ @registry_data = YAML.load_file(REGISTRY_FILE)
13
+ @components = @registry_data["components"] || {}
14
+ end
15
+
16
+ def all_component_names
17
+ @components.keys.sort
18
+ end
19
+
20
+ def get(component_name)
21
+ @components[component_name.to_s]
22
+ end
23
+
24
+ def exists?(component_name)
25
+ @components.key?(component_name.to_s)
26
+ end
27
+
28
+ def dependencies_for(component_name)
29
+ component = get(component_name)
30
+ return [] unless component
31
+
32
+ component["dependencies"] || []
33
+ end
34
+
35
+ def files_for(component_name)
36
+ component = get(component_name)
37
+ return [] unless component
38
+
39
+ component["files"] || []
40
+ end
41
+
42
+ def description_for(component_name)
43
+ component = get(component_name)
44
+ return "" unless component
45
+
46
+ component["description"] || ""
47
+ end
48
+
49
+ def name_for(component_name)
50
+ component = get(component_name)
51
+ return component_name unless component
52
+
53
+ component["name"] || component_name
54
+ end
55
+
56
+ # Resolve all dependencies recursively
57
+ def resolve_dependencies(component_name, resolved = [])
58
+ return resolved unless exists?(component_name)
59
+ return resolved if resolved.include?(component_name)
60
+
61
+ dependencies = dependencies_for(component_name)
62
+
63
+ dependencies.each do |dep|
64
+ resolve_dependencies(dep, resolved)
65
+ end
66
+
67
+ resolved << component_name unless resolved.include?(component_name)
68
+ resolved
69
+ end
70
+
71
+ # Get all files needed for a component including dependencies
72
+ def all_files_for(component_name)
73
+ deps = resolve_dependencies(component_name)
74
+ deps.flat_map { |dep| files_for(dep) }
75
+ end
76
+
77
+ # Check if a component has JavaScript files
78
+ def has_javascript?(component_name)
79
+ files = files_for(component_name)
80
+ files.any? { |file| file.include?("javascript") || file.end_with?(".js") }
81
+ end
82
+
83
+ # Check if component is already installed
84
+ def installed?(component_name, root_path = Dir.pwd)
85
+ files = files_for(component_name)
86
+ return false if files.empty?
87
+
88
+ files.all? { |file| File.exist?(File.join(root_path, file)) }
89
+ end
90
+
91
+ # Get components grouped by category (for listing)
92
+ def components_by_category
93
+ categories = {
94
+ "Base" => ["base", "utilities"],
95
+ "Form Components" => ["button", "input", "label", "checkbox", "textarea", "select", "switch", "slider", "radio_group"],
96
+ "Layout Components" => ["card", "table", "separator", "main"],
97
+ "Feedback Components" => ["alert", "toast", "toaster", "progress", "spinner", "skeleton"],
98
+ "Navigation Components" => ["breadcrumb", "navigation_menu", "sidebar", "tabs"],
99
+ "Display Components" => ["avatar", "badge", "typography"],
100
+ "Interactive Components" => ["accordion", "dialog", "alert_dialog", "sheet", "tooltip", "popover", "hover_card", "collapsible", "dropdown_menu", "toggle"],
101
+ "Theme Components" => ["theme_toggle"]
102
+ }
103
+
104
+ categories
105
+ end
106
+ end
107
+ end