stimulus_plumbers_mcp 0.4.4 → 0.4.8

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 (46) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +120 -0
  3. data/bin/bump-version +26 -0
  4. data/bin/mcp-query +107 -0
  5. data/bin/server +8 -0
  6. data/bin/stimulus-plumbers-mcp +7 -0
  7. data/lib/stimulus_plumbers/mcp/cli.rb +13 -0
  8. data/lib/stimulus_plumbers/mcp/loaders/aria_loader.rb +31 -0
  9. data/lib/stimulus_plumbers/mcp/loaders/component_docs_loader.rb +38 -0
  10. data/lib/stimulus_plumbers/mcp/loaders/component_requirements.rb +41 -0
  11. data/lib/stimulus_plumbers/mcp/loaders/component_schema_loader.rb +49 -0
  12. data/lib/stimulus_plumbers/mcp/loaders/component_theme_loader.rb +41 -0
  13. data/lib/stimulus_plumbers/mcp/loaders/controller_docs_loader.rb +31 -0
  14. data/lib/stimulus_plumbers/mcp/loaders/controller_schema_loader.rb +49 -0
  15. data/lib/stimulus_plumbers/mcp/loaders/guide.md +48 -0
  16. data/lib/stimulus_plumbers/mcp/loaders/guide_loader.rb +40 -4
  17. data/lib/stimulus_plumbers/mcp/loaders/icons_loader.rb +34 -0
  18. data/lib/stimulus_plumbers/mcp/loaders/support/docs_table_parser.rb +112 -0
  19. data/lib/stimulus_plumbers/mcp/loaders/support/gem_vendor_path.rb +18 -0
  20. data/lib/stimulus_plumbers/mcp/loaders/tailwind_loader.rb +35 -0
  21. data/lib/stimulus_plumbers/mcp/loaders/versions_loader.rb +110 -0
  22. data/lib/stimulus_plumbers/mcp/plugins/aria.rb +34 -0
  23. data/lib/stimulus_plumbers/mcp/plugins/base.rb +40 -31
  24. data/lib/stimulus_plumbers/mcp/plugins/component_docs.rb +96 -0
  25. data/lib/stimulus_plumbers/mcp/plugins/component_schema.rb +133 -0
  26. data/lib/stimulus_plumbers/mcp/plugins/component_theme.rb +90 -0
  27. data/lib/stimulus_plumbers/mcp/plugins/controller_docs.rb +91 -0
  28. data/lib/stimulus_plumbers/mcp/plugins/controller_schema.rb +81 -0
  29. data/lib/stimulus_plumbers/mcp/plugins/guide.rb +70 -16
  30. data/lib/stimulus_plumbers/mcp/plugins/icons.rb +42 -0
  31. data/lib/stimulus_plumbers/mcp/plugins/tailwind.rb +69 -45
  32. data/lib/stimulus_plumbers/mcp/plugins/versions.rb +44 -0
  33. data/lib/stimulus_plumbers/mcp/server.rb +26 -9
  34. data/lib/stimulus_plumbers/mcp/version.rb +1 -1
  35. data/lib/stimulus_plumbers_mcp.rb +32 -11
  36. metadata +36 -13
  37. data/lib/stimulus_plumbers/mcp/loaders/component_controller_map.rb +0 -38
  38. data/lib/stimulus_plumbers/mcp/loaders/docs_loader.rb +0 -122
  39. data/lib/stimulus_plumbers/mcp/loaders/schema_loader.rb +0 -45
  40. data/lib/stimulus_plumbers/mcp/loaders/stimulus_manifest.rb +0 -23
  41. data/lib/stimulus_plumbers/mcp/loaders/tailwind_theme_loader.rb +0 -29
  42. data/lib/stimulus_plumbers/mcp/loaders/theme_loader.rb +0 -97
  43. data/lib/stimulus_plumbers/mcp/plugins/docs.rb +0 -82
  44. data/lib/stimulus_plumbers/mcp/plugins/schema.rb +0 -110
  45. data/lib/stimulus_plumbers/mcp/plugins/stimulus.rb +0 -66
  46. data/lib/stimulus_plumbers/mcp/plugins/theme.rb +0 -66
@@ -0,0 +1,44 @@
1
+ # frozen_string_literal: true
2
+
3
+ module StimulusPlumbers
4
+ module MCP
5
+ module Plugins
6
+ class Versions < Base
7
+ class << self
8
+ def loader_key = :versions
9
+
10
+ def loader = VersionsLoader
11
+
12
+ def static_resources
13
+ [
14
+ ::MCP::Resource.new(
15
+ uri: "versions://sources",
16
+ name: "source-versions",
17
+ description: "Resolved version (and resolution path, for stimulus) of each gem/package " \
18
+ "backing this server's data — use to spot version drift between sources",
19
+ mime_type: "application/json"
20
+ )
21
+ ].freeze
22
+ end
23
+
24
+ def read(uri, store)
25
+ return unless uri == "versions://sources"
26
+
27
+ json_resource(uri, store[:versions])
28
+ end
29
+
30
+ def register_tools(server, store)
31
+ text_tool(
32
+ server,
33
+ name: "get_source_versions",
34
+ description: "Returns the resolved version (and resolution path, for stimulus) of each " \
35
+ "gem/package backing this server's data — use to spot version drift between sources"
36
+ ) do
37
+ JSON.generate(store[:versions])
38
+ end
39
+ end
40
+ end
41
+ end
42
+ end
43
+ end
44
+ end
@@ -4,12 +4,19 @@ module StimulusPlumbers
4
4
  module MCP
5
5
  module Server
6
6
  PLUGINS = [
7
+ Plugins::ComponentDocs,
8
+ Plugins::ComponentSchema,
9
+ Plugins::ComponentTheme,
10
+
11
+ Plugins::ControllerDocs,
12
+ Plugins::ControllerSchema,
13
+
14
+ Plugins::Icons,
15
+ Plugins::Tailwind,
16
+
17
+ Plugins::Aria,
7
18
  Plugins::Guide,
8
- Plugins::Schema,
9
- Plugins::Docs,
10
- Plugins::Stimulus,
11
- Plugins::Theme,
12
- Plugins::Tailwind
19
+ Plugins::Versions
13
20
  ].freeze
14
21
 
15
22
  INSTRUCTIONS = "Use these resources and tools for accurate API references when " \
@@ -17,24 +24,34 @@ module StimulusPlumbers
17
24
  "Read guide://overview first for a map of the form/view/stimulus API."
18
25
 
19
26
  def self.build
20
- store = PLUGINS.to_h { |plugin| [plugin::LOADER_KEY, plugin::LOADER.call] }
27
+ store = build_store
21
28
  report_sources(store)
22
29
 
23
30
  server = new_server
24
31
  server.resources_read_handler do |params|
25
- PLUGINS.lazy.filter_map { |plugin| plugin.read(params[:uri], store) }.first || []
32
+ PLUGINS.lazy.filter_map { |plugin| plugin.read(params[:uri], store) }.first ||
33
+ unknown_resource(params[:uri])
26
34
  end
27
35
  PLUGINS.each { |plugin| plugin.register_tools(server, store) }
28
36
  server
29
37
  end
30
38
 
39
+ def self.build_store
40
+ PLUGINS.to_h { |plugin| [plugin.loader_key, plugin.loader.call] }
41
+ end
42
+
43
+ def self.unknown_resource(uri)
44
+ message = "unknown resource: #{uri} — read guide://overview for a map of available resources"
45
+ [{ uri: uri, mimeType: "application/json", text: JSON.generate(error: message) }]
46
+ end
47
+
31
48
  def self.new_server
32
49
  ::MCP::Server.new(
33
50
  name: "stimulus-plumbers",
34
51
  version: StimulusPlumbers::MCP::VERSION,
35
52
  instructions: INSTRUCTIONS,
36
- resources: PLUGINS.flat_map { |p| p::STATIC_RESOURCES },
37
- resource_templates: PLUGINS.flat_map { |p| p::DYNAMIC_RESOURCE_TEMPLATES }
53
+ resources: PLUGINS.flat_map(&:static_resources),
54
+ resource_templates: PLUGINS.flat_map(&:dynamic_resource_templates)
38
55
  )
39
56
  end
40
57
 
@@ -2,6 +2,6 @@
2
2
 
3
3
  module StimulusPlumbers
4
4
  module MCP
5
- VERSION = "0.4.4"
5
+ VERSION = "0.4.8"
6
6
  end
7
7
  end
@@ -9,18 +9,39 @@ require "stimulus_plumbers"
9
9
  require "stimulus_plumbers_tailwind"
10
10
 
11
11
  require_relative "stimulus_plumbers/mcp/version"
12
+ require_relative "stimulus_plumbers/mcp/loaders/support/docs_table_parser"
13
+ require_relative "stimulus_plumbers/mcp/loaders/support/gem_vendor_path"
14
+
15
+ require_relative "stimulus_plumbers/mcp/loaders/component_docs_loader"
16
+ require_relative "stimulus_plumbers/mcp/loaders/component_requirements"
17
+ require_relative "stimulus_plumbers/mcp/loaders/component_schema_loader"
18
+ require_relative "stimulus_plumbers/mcp/loaders/component_theme_loader"
19
+
20
+ require_relative "stimulus_plumbers/mcp/loaders/controller_docs_loader"
21
+ require_relative "stimulus_plumbers/mcp/loaders/controller_schema_loader"
22
+
23
+ require_relative "stimulus_plumbers/mcp/loaders/icons_loader"
24
+ require_relative "stimulus_plumbers/mcp/loaders/tailwind_loader"
25
+
26
+ require_relative "stimulus_plumbers/mcp/loaders/aria_loader"
12
27
  require_relative "stimulus_plumbers/mcp/loaders/guide_loader"
13
- require_relative "stimulus_plumbers/mcp/loaders/schema_loader"
14
- require_relative "stimulus_plumbers/mcp/loaders/docs_loader"
15
- require_relative "stimulus_plumbers/mcp/loaders/stimulus_manifest"
16
- require_relative "stimulus_plumbers/mcp/loaders/component_controller_map"
17
- require_relative "stimulus_plumbers/mcp/loaders/theme_loader"
18
- require_relative "stimulus_plumbers/mcp/loaders/tailwind_theme_loader"
28
+ require_relative "stimulus_plumbers/mcp/loaders/versions_loader"
29
+
19
30
  require_relative "stimulus_plumbers/mcp/plugins/base"
20
- require_relative "stimulus_plumbers/mcp/plugins/guide"
21
- require_relative "stimulus_plumbers/mcp/plugins/schema"
22
- require_relative "stimulus_plumbers/mcp/plugins/docs"
23
- require_relative "stimulus_plumbers/mcp/plugins/stimulus"
24
- require_relative "stimulus_plumbers/mcp/plugins/theme"
31
+
32
+ require_relative "stimulus_plumbers/mcp/plugins/component_docs"
33
+ require_relative "stimulus_plumbers/mcp/plugins/component_schema"
34
+ require_relative "stimulus_plumbers/mcp/plugins/component_theme"
35
+
36
+ require_relative "stimulus_plumbers/mcp/plugins/controller_docs"
37
+ require_relative "stimulus_plumbers/mcp/plugins/controller_schema"
38
+
39
+ require_relative "stimulus_plumbers/mcp/plugins/icons"
25
40
  require_relative "stimulus_plumbers/mcp/plugins/tailwind"
41
+
42
+ require_relative "stimulus_plumbers/mcp/plugins/aria"
43
+ require_relative "stimulus_plumbers/mcp/plugins/guide"
44
+ require_relative "stimulus_plumbers/mcp/plugins/versions"
45
+
26
46
  require_relative "stimulus_plumbers/mcp/server"
47
+ require_relative "stimulus_plumbers/mcp/cli"
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: stimulus_plumbers_mcp
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.4.4
4
+ version: 0.4.8
5
5
  platform: ruby
6
6
  authors:
7
7
  - Ryan Chang
@@ -69,31 +69,54 @@ description: A local MCP server that exposes the stimulus-plumbers API schema an
69
69
  documentation to LLM-powered IDEs
70
70
  email:
71
71
  - ryancyq@gmail.com
72
- executables: []
72
+ executables:
73
+ - stimulus-plumbers-mcp
73
74
  extensions: []
74
75
  extra_rdoc_files: []
75
76
  files:
76
- - lib/stimulus_plumbers/mcp/loaders/component_controller_map.rb
77
- - lib/stimulus_plumbers/mcp/loaders/docs_loader.rb
77
+ - README.md
78
+ - bin/bump-version
79
+ - bin/mcp-query
80
+ - bin/server
81
+ - bin/stimulus-plumbers-mcp
82
+ - lib/stimulus_plumbers/mcp/cli.rb
83
+ - lib/stimulus_plumbers/mcp/loaders/aria_loader.rb
84
+ - lib/stimulus_plumbers/mcp/loaders/component_docs_loader.rb
85
+ - lib/stimulus_plumbers/mcp/loaders/component_requirements.rb
86
+ - lib/stimulus_plumbers/mcp/loaders/component_schema_loader.rb
87
+ - lib/stimulus_plumbers/mcp/loaders/component_theme_loader.rb
88
+ - lib/stimulus_plumbers/mcp/loaders/controller_docs_loader.rb
89
+ - lib/stimulus_plumbers/mcp/loaders/controller_schema_loader.rb
90
+ - lib/stimulus_plumbers/mcp/loaders/guide.md
78
91
  - lib/stimulus_plumbers/mcp/loaders/guide_loader.rb
79
- - lib/stimulus_plumbers/mcp/loaders/schema_loader.rb
80
- - lib/stimulus_plumbers/mcp/loaders/stimulus_manifest.rb
81
- - lib/stimulus_plumbers/mcp/loaders/tailwind_theme_loader.rb
82
- - lib/stimulus_plumbers/mcp/loaders/theme_loader.rb
92
+ - lib/stimulus_plumbers/mcp/loaders/icons_loader.rb
93
+ - lib/stimulus_plumbers/mcp/loaders/support/docs_table_parser.rb
94
+ - lib/stimulus_plumbers/mcp/loaders/support/gem_vendor_path.rb
95
+ - lib/stimulus_plumbers/mcp/loaders/tailwind_loader.rb
96
+ - lib/stimulus_plumbers/mcp/loaders/versions_loader.rb
97
+ - lib/stimulus_plumbers/mcp/plugins/aria.rb
83
98
  - lib/stimulus_plumbers/mcp/plugins/base.rb
84
- - lib/stimulus_plumbers/mcp/plugins/docs.rb
99
+ - lib/stimulus_plumbers/mcp/plugins/component_docs.rb
100
+ - lib/stimulus_plumbers/mcp/plugins/component_schema.rb
101
+ - lib/stimulus_plumbers/mcp/plugins/component_theme.rb
102
+ - lib/stimulus_plumbers/mcp/plugins/controller_docs.rb
103
+ - lib/stimulus_plumbers/mcp/plugins/controller_schema.rb
85
104
  - lib/stimulus_plumbers/mcp/plugins/guide.rb
86
- - lib/stimulus_plumbers/mcp/plugins/schema.rb
87
- - lib/stimulus_plumbers/mcp/plugins/stimulus.rb
105
+ - lib/stimulus_plumbers/mcp/plugins/icons.rb
88
106
  - lib/stimulus_plumbers/mcp/plugins/tailwind.rb
89
- - lib/stimulus_plumbers/mcp/plugins/theme.rb
107
+ - lib/stimulus_plumbers/mcp/plugins/versions.rb
90
108
  - lib/stimulus_plumbers/mcp/server.rb
91
109
  - lib/stimulus_plumbers/mcp/version.rb
92
110
  - lib/stimulus_plumbers_mcp.rb
93
111
  homepage: https://github.com/ryancyq/stimulus-plumbers
94
112
  licenses:
95
113
  - MIT
96
- metadata: {}
114
+ metadata:
115
+ rubygems_mfa_required: 'true'
116
+ allowed_push_host: https://rubygems.org
117
+ changelog_uri: https://github.com/ryancyq/stimulus-plumbers/blob/main/CHANGELOG.md
118
+ homepage_uri: https://github.com/ryancyq/stimulus-plumbers
119
+ source_code_uri: https://github.com/ryancyq/stimulus-plumbers/tree/main/stimulus-plumbers-mcp
97
120
  rdoc_options: []
98
121
  require_paths:
99
122
  - lib
@@ -1,38 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module StimulusPlumbers
4
- module MCP
5
- class ComponentControllerMap
6
- def self.call
7
- Components.constants
8
- .map { |c| Components.const_get(c) }
9
- .grep(Class)
10
- .to_h { |klass| [component_key(klass), controllers_for(klass).uniq] }
11
- end
12
-
13
- # Controllers from a component class plus its nested sub-components (e.g. Combobox::Date),
14
- # keyed to the top-level component. Skips references to sibling components.
15
- def self.controllers_for(mod)
16
- mod.constants(false).flat_map do |const|
17
- value = mod.const_get(const)
18
- if const.to_s.end_with?("CONTROLLER")
19
- Array(value).grep(String).flat_map(&:split)
20
- elsif nested?(mod, value)
21
- controllers_for(value)
22
- else
23
- []
24
- end
25
- end
26
- end
27
-
28
- def self.nested?(mod, value)
29
- value.is_a?(Module) && !value.name.nil? && value.name.start_with?("#{mod.name}::")
30
- end
31
-
32
- def self.component_key(klass)
33
- klass.name.demodulize.underscore.to_sym
34
- end
35
- private_class_method :controllers_for, :nested?, :component_key
36
- end
37
- end
38
- end
@@ -1,122 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module StimulusPlumbers
4
- module MCP
5
- class DocsLoader
6
- DOCS_DIR = File.expand_path(
7
- "../../../../../stimulus-plumbers-rails/docs/component",
8
- __dir__
9
- )
10
-
11
- def self.call
12
- Dir[File.join(DOCS_DIR, "*.md")].each_with_object({}) do |path, result|
13
- name = File.basename(path, ".md").to_sym
14
- content = File.read(path)
15
- result[name] = {
16
- content: content,
17
- examples: extract_erb_examples(content),
18
- signature: extract_signature(content)
19
- }
20
- end
21
- end
22
-
23
- def self.extract_erb_examples(content)
24
- content.scan(%r{```erb\n(.*?)```}m).map(&:first).map(&:strip)
25
- end
26
-
27
- # Parse `| Option |` / `| Slot method |` doc tables into the helper surface.
28
- # Options are grouped under the heading (sub-helper signature) above them.
29
- def self.extract_signature(content)
30
- tables = tables_with_headings(content)
31
- { helpers: option_helpers(tables), slots: slot_methods(tables) }
32
- end
33
-
34
- def self.option_helpers(tables)
35
- tables.select { |t| t[:header].first == "Option" }
36
- .filter_map do |t|
37
- options = t[:rows].map { |r| option_row(r) }
38
- { signature: t[:heading], options: options } unless options.empty?
39
- end
40
- end
41
-
42
- def self.slot_methods(tables)
43
- tables.select { |t| t[:header].first == "Slot method" }
44
- .flat_map { |t| t[:rows].map { |r| slot_row(r) } }
45
- end
46
-
47
- def self.option_row(cells)
48
- { option: clean(cells[0]), default: clean(cells[1]), description: cells[2].to_s }
49
- end
50
-
51
- def self.slot_row(cells)
52
- slot = clean(cells[0])
53
- description = cells[1].to_s
54
- { slot: slot, description: description, block: block_required?(slot, description) }
55
- end
56
-
57
- def self.block_required?(slot, description)
58
- slot.include?("{") || description.match?(%r{block required}i)
59
- end
60
-
61
- # Tag each table with the heading above it; skip fenced code (so ```ruby
62
- # comments aren't read as headings).
63
- def self.tables_with_headings(content)
64
- state = { heading: nil, fenced: false, buffer: [], tables: [] }
65
- content.each_line { |line| scan_line(line, state) }
66
- flush_table(state)
67
- state[:tables]
68
- end
69
-
70
- def self.scan_line(line, state)
71
- if line.start_with?("```")
72
- flush_table(state)
73
- state[:fenced] = !state[:fenced]
74
- elsif state[:fenced]
75
- nil
76
- elsif (heading = line[%r{\A#+\s+(.+)}, 1])
77
- flush_table(state)
78
- state[:heading] = clean(heading)
79
- elsif line.lstrip.start_with?("|")
80
- state[:buffer] << line
81
- else
82
- flush_table(state)
83
- end
84
- end
85
-
86
- def self.flush_table(state)
87
- return if state[:buffer].empty?
88
-
89
- state[:tables] << build_table(state[:buffer]).merge(heading: state[:heading])
90
- state[:buffer] = []
91
- end
92
-
93
- def self.build_table(lines)
94
- rows = lines.map { |l| split_row(l) }.reject { |cells| cells.all? { |c| c.match?(%r{\A:?-+:?\z}) } }
95
- { header: rows.first, rows: rows.drop(1) }
96
- end
97
-
98
- # Split a markdown table row, honouring escaped pipes (`\|`) inside cells.
99
- def self.split_row(line)
100
- line.strip.delete_prefix("|").delete_suffix("|")
101
- .split(%r{(?<!\\)\|})
102
- .map { |c| c.gsub('\|', "|").strip }
103
- end
104
-
105
- def self.clean(cell)
106
- cell.to_s.gsub(%r{[`*]}, "").sub(%r{:\z}, "").strip
107
- end
108
-
109
- private_class_method :option_helpers,
110
- :slot_methods,
111
- :option_row,
112
- :slot_row,
113
- :block_required?,
114
- :tables_with_headings,
115
- :scan_line,
116
- :flush_table,
117
- :build_table,
118
- :split_row,
119
- :clean
120
- end
121
- end
122
- end
@@ -1,45 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module StimulusPlumbers
4
- module MCP
5
- class SchemaLoader
6
- def self.call
7
- {
8
- components: extract_schema,
9
- field_as: extract_field_as,
10
- icons: extract_icons,
11
- stimulus: ComponentControllerMap.call
12
- }
13
- end
14
-
15
- def self.extract_schema
16
- Themes::Base::SCHEMA.transform_values do |param_schema|
17
- param_schema.transform_values do |meta|
18
- v = meta[:validate]
19
- { default: meta[:default], valid: v.respond_to?(:to_a) ? v.to_a : v.inspect }
20
- end
21
- end
22
- end
23
-
24
- def self.extract_field_as
25
- {
26
- field: Form::Fields::Renderer::FIELD.keys,
27
- collection_field: Form::Fields::Renderer::COLLECTION.keys,
28
- choice: Form::Fields::Renderer::CHOICE.keys
29
- }
30
- end
31
-
32
- def self.extract_icons
33
- heroicon_dir = Themes::Tailwind::Icons::Heroicon.send(:svg_dir)
34
- custom_dir = Themes::Tailwind::Icons::Custom.send(:svg_dir)
35
-
36
- outline = Dir[File.join(heroicon_dir, "outline", "*.svg")].map { |f| File.basename(f, ".svg") }
37
- solid = Dir[File.join(heroicon_dir, "solid", "*.svg")].map { |f| "#{File.basename(f, ".svg")}/solid" }
38
- customs = Dir[File.join(custom_dir, "*.svg")].map { |f| File.basename(f, ".svg") }
39
- aliases = Themes::Tailwind::Icon::ALIASES.keys
40
-
41
- (outline + solid + customs + aliases).uniq.sort
42
- end
43
- end
44
- end
45
- end
@@ -1,23 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module StimulusPlumbers
4
- module MCP
5
- class StimulusManifest
6
- MANIFEST_PATH = File.expand_path(
7
- File.join(__dir__, "../../../../..", "stimulus-plumbers", "dist", "controllers.manifest.json")
8
- ).freeze
9
-
10
- def self.call
11
- unless File.exist?(MANIFEST_PATH)
12
- StimulusPlumbers::Logger.warn(
13
- "controllers.manifest.json not found at #{MANIFEST_PATH}. " \
14
- "Run `node --run build:manifest` in stimulus-plumbers/ to generate it."
15
- )
16
- return {}
17
- end
18
-
19
- JSON.parse(File.read(MANIFEST_PATH))
20
- end
21
- end
22
- end
23
- end
@@ -1,29 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module StimulusPlumbers
4
- module MCP
5
- class TailwindThemeLoader
6
- def self.call
7
- theme = Themes::TailwindTheme.new
8
-
9
- Themes::Base::SCHEMA.each_with_object({}) do |(key, params), result|
10
- # Skip keys with no _classes method — calling resolve would trigger Logger.warn
11
- next unless theme.respond_to?(:"#{key}_classes", true)
12
-
13
- result[key] = {}
14
- result[key][:default] = theme.resolve(key)[:classes].to_s
15
-
16
- params.each do |param, meta|
17
- valid = meta[:validate]
18
- next unless valid.respond_to?(:to_a)
19
-
20
- valid.to_a.each do |val|
21
- classes = theme.resolve(key, param => val)[:classes].to_s
22
- result[key]["#{param}:#{val}"] = classes
23
- end
24
- end
25
- end
26
- end
27
- end
28
- end
29
- end
@@ -1,97 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module StimulusPlumbers
4
- module MCP
5
- class ThemeLoader
6
- BASE_DOC = <<~MARKDOWN
7
- # Custom Theme Implementation Guide
8
-
9
- A custom theme is a Ruby class that extends `StimulusPlumbers::Themes::Base` and defines
10
- `{component_key}_classes(**args)` methods for the components you want to style.
11
-
12
- ## Method Convention
13
-
14
- - **Name:** `{component_key}_classes` — e.g. `button_classes`, `form_group_classes`
15
- - **Params:** keyword arguments matching the schema params for that component
16
- - **Return:** a Hash with a `:classes` key containing a space-separated CSS class string
17
-
18
- ```ruby
19
- def button_classes(type: :default, variant: :default, size: :md)
20
- { classes: "..." }
21
- end
22
- ```
23
-
24
- Components with no params still receive empty kwargs:
25
-
26
- ```ruby
27
- def form_group_classes
28
- { classes: "..." }
29
- end
30
- ```
31
-
32
- ## Minimal Example
33
-
34
- ```ruby
35
- class MyTheme < StimulusPlumbers::Themes::Base
36
- private
37
-
38
- def button_classes(type: :default, variant: :default, size: :md)
39
- base = "inline-flex items-center gap-2 rounded px-3 py-2"
40
- variant_class = { primary: "bg-blue-600 text-white", destructive: "bg-red-600 text-white" }.fetch(variant, "bg-gray-100")
41
- { classes: [base, variant_class].join(" ") }
42
- end
43
-
44
- def link_classes(type: :default, variant: :default)
45
- { classes: "underline text-blue-600 hover:text-blue-800" }
46
- end
47
- end
48
- ```
49
-
50
- ## Registration
51
-
52
- ```ruby
53
- StimulusPlumbers.configure do |config|
54
- config.theme.register(:my_theme, MyTheme)
55
- config.theme = :my_theme
56
- end
57
- ```
58
-
59
- ## Component Keys
60
-
61
- See `theme://components` for the full list of component keys that can be themed.
62
- Use `theme://components/{name}` for the method signature and param details per component.
63
-
64
- ## Partial Implementation
65
-
66
- You only need to define methods for components you want to style — unimplemented keys
67
- return an empty hash, which renders the component with no CSS classes.
68
- MARKDOWN
69
-
70
- def self.call
71
- {
72
- base_doc: BASE_DOC,
73
- components: extract_interface
74
- }
75
- end
76
-
77
- def self.extract_interface
78
- Themes::Base::SCHEMA.each_with_object({}) do |(key, params), result|
79
- result[key] = {
80
- method: "#{key}_classes",
81
- params: params.transform_values { |meta| format_param(meta) },
82
- returns: "{ classes: String }"
83
- }
84
- end
85
- end
86
-
87
- def self.format_param(meta)
88
- valid = meta[:validate]
89
- entry = { default: meta[:default] }
90
- entry[:valid] = valid.to_a if valid.respond_to?(:to_a)
91
- entry
92
- end
93
-
94
- private_class_method :extract_interface, :format_param
95
- end
96
- end
97
- end