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.
- checksums.yaml +7 -0
- data/.do/app.yaml +25 -0
- data/.dockerignore +51 -0
- data/.idea/.gitignore +8 -0
- data/.idea/aws.xml +17 -0
- data/.idea/hotcdn.iml +189 -0
- data/.idea/jsLibraryMappings.xml +6 -0
- data/.idea/misc.xml +4 -0
- data/.idea/modules.xml +8 -0
- data/.idea/vcs.xml +6 -0
- data/.mise.toml +3 -0
- data/.node-version +1 -0
- data/Dockerfile +84 -0
- data/README.md +230 -0
- data/Rakefile +6 -0
- data/app/components/m9sh/accordion_component.rb +122 -0
- data/app/components/m9sh/alert_component.rb +72 -0
- data/app/components/m9sh/alert_dialog_component.rb +100 -0
- data/app/components/m9sh/avatar_component.rb +65 -0
- data/app/components/m9sh/badge_component.rb +37 -0
- data/app/components/m9sh/base_component.rb +21 -0
- data/app/components/m9sh/breadcrumb_component.rb +100 -0
- data/app/components/m9sh/button_component.rb +54 -0
- data/app/components/m9sh/card_component.rb +90 -0
- data/app/components/m9sh/checkbox_component.rb +36 -0
- data/app/components/m9sh/collapsible_component.rb +47 -0
- data/app/components/m9sh/dialog_component.rb +123 -0
- data/app/components/m9sh/dropdown_menu_component.rb +27 -0
- data/app/components/m9sh/dropdown_menu_content_component.rb +24 -0
- data/app/components/m9sh/dropdown_menu_item_component.rb +36 -0
- data/app/components/m9sh/dropdown_menu_separator_component.rb +9 -0
- data/app/components/m9sh/dropdown_menu_trigger_component.rb +19 -0
- data/app/components/m9sh/hover_card_component.rb +48 -0
- data/app/components/m9sh/input_component.rb +33 -0
- data/app/components/m9sh/label_component.rb +27 -0
- data/app/components/m9sh/main_component.rb +16 -0
- data/app/components/m9sh/navigation_menu_component.rb +95 -0
- data/app/components/m9sh/popover_component.rb +47 -0
- data/app/components/m9sh/progress_component.rb +46 -0
- data/app/components/m9sh/radio_group_component.rb +88 -0
- data/app/components/m9sh/select_component.rb +51 -0
- data/app/components/m9sh/separator_component.rb +40 -0
- data/app/components/m9sh/sheet_component.rb +123 -0
- data/app/components/m9sh/sidebar_component.rb +126 -0
- data/app/components/m9sh/sidebar_group_component.rb +51 -0
- data/app/components/m9sh/sidebar_inset_component.rb +16 -0
- data/app/components/m9sh/sidebar_menu_button_component.rb +56 -0
- data/app/components/m9sh/sidebar_menu_component.rb +16 -0
- data/app/components/m9sh/sidebar_menu_item_component.rb +16 -0
- data/app/components/m9sh/sidebar_provider_component.rb +29 -0
- data/app/components/m9sh/sidebar_trigger_component.rb +44 -0
- data/app/components/m9sh/skeleton_component.rb +32 -0
- data/app/components/m9sh/slider_component.rb +83 -0
- data/app/components/m9sh/spinner_component.rb +46 -0
- data/app/components/m9sh/switch_component.rb +47 -0
- data/app/components/m9sh/table_component.rb +111 -0
- data/app/components/m9sh/tabs_component.rb +92 -0
- data/app/components/m9sh/textarea_component.rb +44 -0
- data/app/components/m9sh/theme_toggle_component.rb +88 -0
- data/app/components/m9sh/toast_component.rb +86 -0
- data/app/components/m9sh/toaster_component.rb +20 -0
- data/app/components/m9sh/toggle_component.rb +64 -0
- data/app/components/m9sh/tooltip_component.rb +48 -0
- data/app/components/m9sh/typography_component.rb +56 -0
- data/app/components/m9sh/utilities.rb +26 -0
- data/app/javascript/controllers/m9sh/accordion_controller.js +110 -0
- data/app/javascript/controllers/m9sh/alert_dialog_controller.js +47 -0
- data/app/javascript/controllers/m9sh/collapsible_controller.js +57 -0
- data/app/javascript/controllers/m9sh/dialog_controller.js +119 -0
- data/app/javascript/controllers/m9sh/dropdown_menu_controller.js +103 -0
- data/app/javascript/controllers/m9sh/hover_card_controller.js +66 -0
- data/app/javascript/controllers/m9sh/navigation_menu_controller.js +219 -0
- data/app/javascript/controllers/m9sh/popover_controller.js +113 -0
- data/app/javascript/controllers/m9sh/radio_controller.js +59 -0
- data/app/javascript/controllers/m9sh/sheet_controller.js +46 -0
- data/app/javascript/controllers/m9sh/sidebar_controller.js +114 -0
- data/app/javascript/controllers/m9sh/sidebar_provider_controller.js +12 -0
- data/app/javascript/controllers/m9sh/slider_controller.js +90 -0
- data/app/javascript/controllers/m9sh/switch_controller.js +33 -0
- data/app/javascript/controllers/m9sh/tabs_controller.js +51 -0
- data/app/javascript/controllers/m9sh/theme_controller.js +50 -0
- data/app/javascript/controllers/m9sh/toast_controller.js +46 -0
- data/app/javascript/controllers/m9sh/toaster_controller.js +70 -0
- data/app/javascript/controllers/m9sh/toggle_controller.js +27 -0
- data/app/javascript/controllers/m9sh/tooltip_controller.js +86 -0
- data/components.json +21 -0
- data/config.ru +6 -0
- data/exe/m9sh +12 -0
- data/fix_namespaces.py +32 -0
- data/fly.toml +30 -0
- data/koyeb.yaml +26 -0
- data/lib/m9sh/cli.rb +234 -0
- data/lib/m9sh/config.rb +114 -0
- data/lib/m9sh/generator.rb +183 -0
- data/lib/m9sh/registry.rb +107 -0
- data/lib/m9sh/registry.yml +384 -0
- data/lib/m9sh/version.rb +5 -0
- data/lib/m9sh.rb +11 -0
- data/package-lock.json +99 -0
- data/package.json +28 -0
- data/pnpm-lock.yaml +75 -0
- data/tailwind.config.js +93 -0
- data/update_namespace.py +73 -0
- 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
|
data/lib/m9sh/config.rb
ADDED
|
@@ -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
|