rails-ai-context 1.1.1 → 1.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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: dc7244d020376cc416232a1cbbc0a287866dc4243b9566e4fa2d50d556e5a1c6
4
- data.tar.gz: 5251eb33ca4d3f72803e3acc77d272a4ec038c9a367390752db463195126675a
3
+ metadata.gz: 6e7442b6a28d089867479cba4a99d6f0fcf64e34f692d8c47b576de8e7ec4ffd
4
+ data.tar.gz: 0b8b21e8c1581a056afbb03e3ef6b3c45a62c7fbb61d345bbec49d370c4fd191
5
5
  SHA512:
6
- metadata.gz: ddda0c23e18344f9b95d4ff2b0e5ce9dba9c247a2915a1337226f98fb1ef2ab83b07370e2baf26bbb25261241baf75ccf40c952dd03cae4d6b6004c93398c14f
7
- data.tar.gz: 790dc32dffed8275469eaebae74491dcaa0dbc2b979e53fc8e959b889c69c2eec6982304a1d0d39e22900e79a0214743cf616a8f4e70bea8a4797be679be9c9c
6
+ metadata.gz: e1bd3b280678917a3b94576cef271669fc6892ca32cfd522b88a4c7cee93d73d4301b6d995bd75e78fd64bb448f5e226013b8678411bfd56e68de0a59ccba35b
7
+ data.tar.gz: a57fed3f44c4e1599854caf8243a35f62476f487e8b14a41489bcaee1efb4e8398935211ea9073ca70f9a1e34c16a6fa13d79cdaa5cf943d6f0d5d2e82ea100c
data/CHANGELOG.md CHANGED
@@ -5,6 +5,20 @@ All notable changes to this project will be documented in this file.
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
6
6
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
7
 
8
+ ## [1.2.0] - 2026-03-23
9
+
10
+ ### Added
11
+
12
+ - **Design system extraction** — ViewTemplateIntrospector now extracts canonical page examples (real HTML/ERB snippets from actual views), full color palette with semantic roles (primary/danger/success/warning), typography scale (sizes, weights, heading styles), layout patterns (containers, grids, spacing scale), responsive breakpoint usage, interactive state patterns (hover/focus/active/disabled), dark mode detection, and icon system identification.
13
+ - **New MCP tool: `rails_get_design_system`** — dedicated tool (15th) returns the app's design system: color palette, component patterns with real HTML examples, typography, layout conventions, responsive breakpoints. Supports `detail` parameter (summary/standard/full). Total MCP tools: 15.
14
+ - **DesignSystemHelper serializer module** — replaces flat component listings with actionable design guidance across all output formats (Claude, Cursor, Windsurf, Copilot, OpenCode). Shows components with semantic roles, canonical page examples in split rules, and explicit design rules.
15
+ - **DesignTokenIntrospector semantic categorization** — tokens now grouped into colors/typography/spacing/sizing/borders/shadows. Enhanced Tailwind v3 parsing for fontSize, spacing, borderRadius, and screens.
16
+
17
+ ### Changed
18
+
19
+ - **"UI Patterns" section renamed to "Design System"** — richer content with color palette, typography, components, spacing conventions, interactive states, and design rules.
20
+ - **Design tokens consumed for the first time** — `context[:design_tokens]` data was previously extracted but never rendered. Now merged into design system output in all serializers and the new MCP tool.
21
+
8
22
  ## [1.1.1] - 2026-03-23
9
23
 
10
24
  ### Added
data/CLAUDE.md CHANGED
@@ -9,7 +9,7 @@ structure to AI assistants via the Model Context Protocol (MCP).
9
9
  - `lib/rails_ai_context/configuration.rb` — User-facing config with presets (:standard, :full)
10
10
  - `lib/rails_ai_context/introspector.rb` — Orchestrates sub-introspectors
11
11
  - `lib/rails_ai_context/introspectors/` — 29 introspectors (schema, models, routes, jobs, gems, conventions, stimulus, database_stats, controllers, views, view_templates, design_tokens, turbo, i18n, config, active_storage, action_text, auth, api, tests, rake_tasks, assets, devops, action_mailbox, migrations, seeds, middleware, engines, multi_database)
12
- - `lib/rails_ai_context/tools/` — 14 MCP tools using the official mcp SDK
12
+ - `lib/rails_ai_context/tools/` — 15 MCP tools using the official mcp SDK
13
13
  - `lib/rails_ai_context/serializers/` — Output formatters (claude, claude_rules, opencode, opencode_rules, cursor_rules, windsurf, windsurf_rules, copilot, copilot_instructions, rules, markdown, JSON, context_file_serializer, test_command_detection)
14
14
  - `lib/rails_ai_context/resources.rb` — MCP resources (static data AI clients read directly)
15
15
  - `lib/rails_ai_context/server.rb` — MCP server configuration (stdio + HTTP transports)
@@ -39,7 +39,8 @@ structure to AI assistants via the Model Context Protocol (MCP).
39
39
  13. **Per-tool split rules** — `.claude/rules/`, `.cursor/rules/`, `.windsurf/rules/`, `.github/instructions/`
40
40
  14. **Section markers** — root file content wrapped in `<!-- BEGIN/END rails-ai-context -->` to preserve user content
41
41
  15. **generate_root_files toggle** — when false, skip root files (CLAUDE.md, etc.), only generate split rules
42
- 16. **custom_tools API** — `config.custom_tools` array lets users register additional MCP::Tool subclasses alongside the 14 built-in tools
42
+ 16. **custom_tools API** — `config.custom_tools` array lets users register additional MCP::Tool subclasses alongside the 15 built-in tools
43
+ 17. **Design system extraction** — view templates analyzed for canonical examples, color palette, typography, responsive patterns, interactive states, dark mode
43
44
 
44
45
  ## Testing
45
46
 
data/SECURITY.md CHANGED
@@ -4,6 +4,7 @@
4
4
 
5
5
  | Version | Supported |
6
6
  |---------|--------------------|
7
+ | 1.2.x | :white_check_mark: |
7
8
  | 1.1.x | :white_check_mark: |
8
9
  | 1.0.x | :white_check_mark: |
9
10
  | < 1.0 | :x: |
@@ -37,7 +37,8 @@ module RailsAiContext
37
37
 
38
38
  {
39
39
  framework: detect_framework(root),
40
- tokens: tokens
40
+ tokens: tokens,
41
+ categorized: categorize_tokens(tokens)
41
42
  }
42
43
  rescue => e
43
44
  { error: e.message }
@@ -116,6 +117,34 @@ module RailsAiContext
116
117
  content.scan(/(\w+)\s*:\s*\[['"]([^'"]+)['"]/).each do |name, font|
117
118
  tokens["tw3-font-#{name}"] = font if name.match?(/font|sans|serif|mono|display|heading/)
118
119
  end
120
+
121
+ # Extract fontSize configuration
122
+ content.scan(/fontSize\s*:\s*\{([^}]+)\}/m).each do |match|
123
+ match[0].scan(/['"]([\w-]+)['"]\s*:\s*['"]([^'"]+)['"]/).each do |name, value|
124
+ tokens["tw3-fontSize-#{name}"] = value
125
+ end
126
+ end
127
+
128
+ # Extract screens (breakpoints)
129
+ content.scan(/screens\s*:\s*\{([^}]+)\}/m).each do |match|
130
+ match[0].scan(/['"]([\w-]+)['"]\s*:\s*['"]([^'"]+)['"]/).each do |name, value|
131
+ tokens["tw3-screen-#{name}"] = value
132
+ end
133
+ end
134
+
135
+ # Extract spacing overrides
136
+ content.scan(/spacing\s*:\s*\{([^}]+)\}/m).each do |match|
137
+ match[0].scan(/['"]([\w.-]+)['"]\s*:\s*['"]([^'"]+)['"]/).each do |name, value|
138
+ tokens["tw3-spacing-#{name}"] = value
139
+ end
140
+ end
141
+
142
+ # Extract borderRadius overrides
143
+ content.scan(/borderRadius\s*:\s*\{([^}]+)\}/m).each do |match|
144
+ match[0].scan(/['"]([\w-]+)['"]\s*:\s*['"]([^'"]+)['"]/).each do |name, value|
145
+ tokens["tw3-radius-#{name}"] = value
146
+ end
147
+ end
119
148
  end
120
149
 
121
150
  # 4. Bootstrap/Sass variable definitions
@@ -174,6 +203,41 @@ module RailsAiContext
174
203
  end
175
204
  end
176
205
 
206
+ def categorize_tokens(tokens)
207
+ categories = {
208
+ colors: {},
209
+ typography: {},
210
+ spacing: {},
211
+ sizing: {},
212
+ borders: {},
213
+ shadows: {},
214
+ other: {}
215
+ }
216
+
217
+ tokens.each do |name, value|
218
+ category = case name
219
+ when /color|brand|primary|secondary|danger|success|warning|accent|neutral|bg|surface/i
220
+ :colors
221
+ when /font|text-size|leading|tracking|letter-spacing|line-height/i
222
+ :typography
223
+ when /spacing|gap|margin|padding|space|inset/i
224
+ :spacing
225
+ when /width|height|size|radius|rounded|screen|breakpoint/i
226
+ :sizing
227
+ when /border|ring|outline|divide/i
228
+ :borders
229
+ when /shadow/i
230
+ :shadows
231
+ else
232
+ name.match?(/shade-\d+|#[\da-fA-F]/) ? :colors : :other
233
+ end
234
+
235
+ categories[category][name] = value
236
+ end
237
+
238
+ categories.reject { |_, v| v.empty? }
239
+ end
240
+
177
241
  # Helper: extract :root { --var: value } from CSS content
178
242
  def extract_root_vars(content, tokens)
179
243
  content.scan(/:root\s*(?:,\s*:host)?\s*\{([^}]+)\}/m).each do |match|
@@ -20,7 +20,9 @@ module RailsAiContext
20
20
  {
21
21
  templates: scan_templates(views_dir),
22
22
  partials: scan_partials(views_dir),
23
- ui_patterns: extract_ui_patterns(all_content)
23
+ ui_patterns: extract_ui_patterns(all_content).merge(
24
+ canonical_examples: extract_canonical_examples(views_dir)
25
+ )
24
26
  }
25
27
  rescue => e
26
28
  { error: e.message }
@@ -106,7 +108,13 @@ module RailsAiContext
106
108
  color_scheme: color_scheme,
107
109
  radius: radius_convention,
108
110
  form_layout: form_layout,
109
- components: components
111
+ components: components,
112
+ typography: extract_typography(all_content),
113
+ layout: extract_layout_patterns(all_content),
114
+ responsive: extract_responsive_patterns(all_content),
115
+ interactive_states: extract_interactive_states(all_content),
116
+ dark_mode: extract_dark_mode_patterns(all_content),
117
+ icons: extract_icon_system(all_content)
110
118
  }
111
119
  end
112
120
 
@@ -149,12 +157,19 @@ module RailsAiContext
149
157
  "primary"
150
158
  end
151
159
  next unless role # skip unclassified buttons
152
- classified[role] = { classes: c, count: count } if !classified[role] || count > classified[role][:count]
160
+ classified[role] ||= { classes: c, count: count, variants: [] }
161
+ classified[role][:variants] << { classes: c, count: count }
162
+ if count > classified[role][:count]
163
+ classified[role][:classes] = c
164
+ classified[role][:count] = count
165
+ end
153
166
  end
154
167
 
155
168
  classified.each do |role, data|
156
169
  label = role == "default" ? "Button" : "Button (#{role})"
157
- components << { type: :button, label: label, classes: data[:classes] }
170
+ entry = { type: :button, label: label, classes: data[:classes] }
171
+ entry[:variants] = data[:variants] if data[:variants]&.size&.> 1
172
+ components << entry
158
173
  used << data[:classes]
159
174
  end
160
175
  end
@@ -287,7 +302,7 @@ module RailsAiContext
287
302
  components << { type: :alert, label: "Alert", classes: best[0] }
288
303
  end
289
304
 
290
- def extract_color_scheme(_content, groups)
305
+ def extract_color_scheme(content, groups)
291
306
  # Find primary color from button backgrounds
292
307
  primary_colors = Hash.new(0)
293
308
  groups.each do |c, count|
@@ -306,6 +321,27 @@ module RailsAiContext
306
321
  scheme = {}
307
322
  scheme[:primary] = primary if primary
308
323
  scheme[:text] = text_colors.join("/") if text_colors.any?
324
+
325
+ # Full palette: background colors used
326
+ bg_colors = Hash.new(0)
327
+ content.scan(/bg-(\w+)-(\d+)/).each { |color, shade| bg_colors["#{color}-#{shade}"] += 1 }
328
+ scheme[:background_palette] = bg_colors.sort_by { |_, c| -c }.first(10).map(&:first) if bg_colors.any?
329
+
330
+ # Text color palette
331
+ text_palette = Hash.new(0)
332
+ content.scan(/text-(\w+)-(\d+)/).each { |color, shade| text_palette["#{color}-#{shade}"] += 1 }
333
+ scheme[:text_palette] = text_palette.sort_by { |_, c| -c }.first(8).map(&:first) if text_palette.any?
334
+
335
+ # Border color palette
336
+ border_colors = Hash.new(0)
337
+ content.scan(/border-(\w+)-(\d+)/).each { |color, shade| border_colors["#{color}-#{shade}"] += 1 }
338
+ scheme[:border_palette] = border_colors.sort_by { |_, c| -c }.first(5).map(&:first) if border_colors.any?
339
+
340
+ # Semantic roles inferred from usage
341
+ scheme[:danger] = "red" if bg_colors.any? { |k, _| k.start_with?("red-") }
342
+ scheme[:success] = "green" if bg_colors.any? { |k, _| k.start_with?("green-") }
343
+ scheme[:warning] = "yellow" if bg_colors.any? { |k, _| k.start_with?("yellow-") || k.start_with?("amber-") }
344
+
309
345
  scheme
310
346
  end
311
347
 
@@ -332,6 +368,179 @@ module RailsAiContext
332
368
  layout
333
369
  end
334
370
 
371
+ def extract_responsive_patterns(content)
372
+ breakpoints = {}
373
+ %w[sm md lg xl 2xl].each do |bp|
374
+ matches = content.scan(/#{bp}:[\w-]+/).map { |m| m.sub("#{bp}:", "") }
375
+ next if matches.empty?
376
+ breakpoints[bp] = matches.tally.sort_by { |_, c| -c }.first(5).to_h
377
+ end
378
+ breakpoints
379
+ end
380
+
381
+ def extract_interactive_states(content)
382
+ states = {}
383
+ %w[hover focus active disabled group-hover focus-within focus-visible].each do |state|
384
+ matches = content.scan(/#{state}:[\w-]+/)
385
+ next if matches.empty?
386
+ states[state] = matches.tally.sort_by { |_, c| -c }.first(5).to_h
387
+ end
388
+ states
389
+ end
390
+
391
+ def extract_dark_mode_patterns(content)
392
+ dark_classes = content.scan(/dark:[\w-]+/)
393
+ return {} if dark_classes.empty?
394
+ { used: true, patterns: dark_classes.tally.sort_by { |_, c| -c }.first(10).to_h }
395
+ end
396
+
397
+ def extract_layout_patterns(content)
398
+ layout = {}
399
+
400
+ containers = content.scan(/(?:max-w-\w+|container)\b/).tally
401
+ layout[:containers] = containers.sort_by { |_, c| -c }.first(3).to_h unless containers.empty?
402
+
403
+ flex = content.scan(/(?:flex-(?:row|col|wrap)|items-\w+|justify-\w+)\b/).tally
404
+ layout[:flex] = flex.sort_by { |_, c| -c }.first(5).to_h unless flex.empty?
405
+
406
+ grids = content.scan(/grid-cols-\d+/).tally
407
+ layout[:grid] = grids.sort_by { |_, c| -c }.first(3).to_h unless grids.empty?
408
+
409
+ spacing = content.scan(/(?:space-[xy]-|gap-|p-|px-|py-|m-|mx-|my-|mt-|mb-)\d+/).tally
410
+ layout[:spacing_scale] = spacing.sort_by { |_, c| -c }.first(8).to_h unless spacing.empty?
411
+
412
+ layout
413
+ end
414
+
415
+ def extract_typography(content)
416
+ typo = {}
417
+
418
+ sizes = content.scan(/text-(?:xs|sm|base|lg|xl|2xl|3xl|4xl|5xl|6xl)/).tally
419
+ typo[:sizes] = sizes.sort_by { |_, c| -c }.to_h unless sizes.empty?
420
+
421
+ weights = content.scan(/font-(?:thin|extralight|light|normal|medium|semibold|bold|extrabold|black)/).tally
422
+ typo[:weights] = weights.sort_by { |_, c| -c }.to_h unless weights.empty?
423
+
424
+ headings = {}
425
+ %w[h1 h2 h3 h4].each do |tag|
426
+ classes = content.scan(/<#{tag}[^>]*class=["']([^"']+)["']/i).map { |m| m[0].gsub(/<%=.*?%>/, "").strip }
427
+ next if classes.empty?
428
+ headings[tag] = classes.tally.max_by { |_, c| c }[0]
429
+ end
430
+ typo[:heading_styles] = headings unless headings.empty?
431
+
432
+ leading = content.scan(/leading-\w+/).tally
433
+ typo[:line_height] = leading.sort_by { |_, c| -c }.first(3).to_h unless leading.empty?
434
+
435
+ typo
436
+ end
437
+
438
+ def extract_icon_system(content)
439
+ icons = {}
440
+
441
+ icons[:library] = "heroicons" if content.match?(/heroicon|hero_icon/)
442
+ icons[:library] = "lucide" if content.match?(/lucide|data-lucide/)
443
+ icons[:library] = "font-awesome" if content.match?(/fa-\w+|font-awesome/)
444
+ icons[:library] = "bootstrap-icons" if content.match?(/bi-\w+/)
445
+
446
+ svg_count = content.scan(/<svg\b/).size
447
+ icons[:inline_svg_count] = svg_count if svg_count > 0
448
+
449
+ icon_sizes = content.scan(/(?:w-\d+\s+h-\d+|size-\d+)/).tally
450
+ icons[:sizes] = icon_sizes.sort_by { |_, c| -c }.first(3).to_h unless icon_sizes.empty?
451
+
452
+ icons.empty? ? nil : icons
453
+ end
454
+
455
+ # Analyzes individual templates to find canonical examples of common page types.
456
+ # Returns up to 5 representative ERB snippets that AI can copy.
457
+ def extract_canonical_examples(views_dir) # rubocop:disable Metrics
458
+ max_snippet = 80 # lines per example
459
+ max_file = RailsAiContext.configuration.max_view_file_size
460
+ examples = {}
461
+
462
+ Dir.glob(File.join(views_dir, "**", "*.{erb,haml,slim}")).each do |path|
463
+ next if File.directory?(path)
464
+ next if File.basename(path).start_with?("_") # skip partials
465
+ next if path.include?("/layouts/")
466
+ next if File.size(path) > max_file
467
+
468
+ relative = path.sub("#{views_dir}/", "")
469
+ content = File.read(path, encoding: "UTF-8", invalid: :replace, undef: :replace) rescue next
470
+
471
+ page_type = classify_template(content)
472
+ next unless page_type
473
+
474
+ score = score_template(content)
475
+ existing = examples[page_type]
476
+
477
+ if !existing || score > existing[:score]
478
+ snippet = content.lines.first(max_snippet).join
479
+ # Strip large SVG blocks
480
+ snippet = snippet.gsub(/<svg[^>]*>.*?<\/svg>/m, "<!-- svg icon -->")
481
+ components_used = detect_components_in_template(content)
482
+
483
+ examples[page_type] = {
484
+ type: page_type,
485
+ template: relative,
486
+ snippet: snippet,
487
+ components_used: components_used,
488
+ score: score
489
+ }
490
+ end
491
+ end
492
+
493
+ examples.values.map { |e| e.except(:score) }.first(5)
494
+ rescue
495
+ []
496
+ end
497
+
498
+ def classify_template(content)
499
+ has_form = content.match?(/form_with|form_for|<form\b/i)
500
+ has_collection = content.match?(/\.each\s+do\b|render\s+collection:|render\s+@\w+/)
501
+ has_grid = content.match?(/grid-cols-|grid\s+grid-/)
502
+ has_show = content.match?(/\A(?:(?!\.each).)*@\w+\.\w+/m) && !has_collection
503
+
504
+ if has_form && !has_collection
505
+ :form_page
506
+ elsif has_collection || has_grid
507
+ :list_page
508
+ elsif has_show
509
+ :show_page
510
+ end
511
+ end
512
+
513
+ def score_template(content)
514
+ score = 0
515
+ # Prefer templates with more design patterns
516
+ score += content.scan(/class=["'][^"']+["']/).size.clamp(0, 20)
517
+ # Responsive classes
518
+ score += 5 if content.match?(/(?:sm|md|lg|xl):/)
519
+ # Interactive states
520
+ score += 3 if content.match?(/hover:|focus:/)
521
+ # Component variety
522
+ score += 2 if content.match?(/<button|btn/i)
523
+ score += 2 if content.match?(/shadow.*rounded|card/i)
524
+ score += 2 if content.match?(/<input|form_with|form_for/i)
525
+ # Not too short, not too long (sweet spot: 30-100 lines)
526
+ lines = content.lines.size
527
+ score += 5 if lines.between?(30, 100)
528
+ score -= 3 if lines < 10
529
+ score
530
+ end
531
+
532
+ def detect_components_in_template(content)
533
+ used = []
534
+ used << :button if content.match?(/bg-\w+-[5-9]00.*text-white|btn-|<button/i)
535
+ used << :card if content.match?(/shadow.*rounded|card/i)
536
+ used << :input if content.match?(/<input|text_field|email_field|password_field/i)
537
+ used << :form if content.match?(/form_with|form_for|<form/i)
538
+ used << :link if content.match?(/link_to|<a\b/i)
539
+ used << :badge if content.match?(/rounded-full.*text-(?:xs|sm)|badge/i)
540
+ used << :grid if content.match?(/grid-cols-|grid\s+grid-/)
541
+ used
542
+ end
543
+
335
544
  EXCLUDED_METHODS = %w[
336
545
  each map select reject first last size count any? empty? present? blank?
337
546
  new build create find where order limit nil? join class html_safe
@@ -6,6 +6,7 @@ module RailsAiContext
6
6
  # These provide quick-reference lists without bloating CLAUDE.md.
7
7
  class ClaudeRulesSerializer
8
8
  include StackOverviewHelper
9
+ include DesignSystemHelper
9
10
 
10
11
  attr_reader :context
11
12
 
@@ -250,31 +251,10 @@ module RailsAiContext
250
251
  components = patterns[:components] || []
251
252
  return nil if components.empty?
252
253
 
253
- lines = [ "# UI Patterns", "" ]
254
+ lines = [ "# Design System", "" ]
254
255
 
255
- scheme = patterns[:color_scheme] || {}
256
- parts = []
257
- parts << "Primary: #{scheme[:primary]}" if scheme[:primary]
258
- parts << "Text: #{scheme[:text]}" if scheme[:text]
259
- lines << parts.join(" | ") if parts.any?
260
-
261
- radius = patterns[:radius] || {}
262
- if radius.any?
263
- # Group by radius value to avoid repetition
264
- by_radius = radius.group_by { |_, r| r }.map { |r, types| "#{r} (#{types.map(&:first).join(', ')})" }
265
- lines << "Radius: #{by_radius.join(', ')}"
266
- end
267
- lines << ""
268
-
269
- lines << "## Components"
270
- components.first(20).each { |c| next unless c[:label] && c[:classes]; lines << "- #{c[:label]}: `#{c[:classes]}`" }
271
-
272
- fl = patterns[:form_layout] || {}
273
- if fl.any?
274
- lines << "" << "## Form layout"
275
- lines << "- Spacing: #{fl[:spacing]}" if fl[:spacing]
276
- lines << "- Grid: #{fl[:grid]}" if fl[:grid]
277
- end
256
+ # Full design system with canonical examples
257
+ lines.concat(render_design_system_full(context))
278
258
 
279
259
  # Shared partials — so agents reuse them instead of recreating
280
260
  begin
@@ -346,7 +326,7 @@ module RailsAiContext
346
326
  "- app/javascript/controllers/index.js — Stimulus auto-registers controllers. No need to read.",
347
327
  "- Test files — use `rails_get_test_info(detail:\"full\")` for patterns.",
348
328
  "",
349
- "## Tools (14)",
329
+ "## Tools (15)",
350
330
  "",
351
331
  "**rails_get_schema** — database tables, columns, indexes, foreign keys",
352
332
  "- `rails_get_schema(detail:\"summary\")` — all tables with column counts",
@@ -387,6 +367,10 @@ module RailsAiContext
387
367
  "**rails_analyze_feature** — combined schema + models + controllers + routes for a feature area",
388
368
  "- `rails_analyze_feature(feature:\"authentication\")` — one call gets everything related to a feature",
389
369
  "",
370
+ "**rails_get_design_system** — color palette, component patterns, canonical page examples",
371
+ "- `rails_get_design_system(detail:\"standard\")` — colors + components + real HTML examples + design rules",
372
+ "- `rails_get_design_system(detail:\"full\")` — + typography, responsive, dark mode, layout, spacing",
373
+ "",
390
374
  "**rails_get_config** — cache store, session, timezone, middleware, initializers",
391
375
  "**rails_get_gems** — notable gems categorized by function",
392
376
  "**rails_get_conventions** — architecture patterns, directory structure",
@@ -8,6 +8,7 @@ module RailsAiContext
8
8
  class ClaudeSerializer
9
9
  include TestCommandDetection
10
10
  include StackOverviewHelper
11
+ include DesignSystemHelper
11
12
 
12
13
  attr_reader :context
13
14
 
@@ -192,40 +193,12 @@ module RailsAiContext
192
193
  end
193
194
 
194
195
  def render_ui_patterns
195
- vt = context[:view_templates]
196
- return [] unless vt.is_a?(Hash) && !vt[:error]
197
- patterns = vt[:ui_patterns] || {}
198
- components = patterns[:components] || []
199
- return [] if components.empty?
200
-
201
- lines = [ "## UI Patterns" ]
202
-
203
- # Color scheme summary
204
- scheme = patterns[:color_scheme] || {}
205
- if scheme.any?
206
- parts = []
207
- parts << "Primary: #{scheme[:primary]}" if scheme[:primary]
208
- parts << "Text: #{scheme[:text]}" if scheme[:text]
209
- lines << parts.join(" | ") if parts.any?
210
- end
211
-
212
- lines << ""
213
- components.first(15).each do |comp|
214
- lines << "- #{comp[:label]}: `#{comp[:classes]}`"
215
- end
216
-
217
- # Form layout
218
- fl = patterns[:form_layout] || {}
219
- if fl.any?
220
- lines << "" << "Form: #{fl.values.join(', ')}"
221
- end
222
- lines << ""
223
- lines
196
+ render_design_system(context, max_lines: 30)
224
197
  end
225
198
 
226
199
  def render_mcp_guide # rubocop:disable Metrics/MethodLength
227
200
  [
228
- "## MCP Tools (14) — ALWAYS Use These First",
201
+ "## MCP Tools (15) — ALWAYS Use These First",
229
202
  "",
230
203
  "Use MCP for reference files (schema, routes, tests). Read directly if you'll edit.",
231
204
  "MCP tools return line numbers. Start with `detail:\"summary\"`.",
@@ -238,6 +211,7 @@ module RailsAiContext
238
211
  "- `rails_get_stimulus(detail:\"summary\")` → `(controller:\"name\")` — targets, actions, values",
239
212
  "- `rails_get_test_info(detail:\"full\")` — fixtures, factories, helpers; `(model:\"Cook\")` — tests",
240
213
  "- `rails_analyze_feature(feature:\"auth\")` — schema + models + controllers + routes for a feature",
214
+ "- `rails_get_design_system` — color palette, component patterns, canonical page examples",
241
215
  "- `rails_get_config` | `rails_get_gems` | `rails_get_conventions` | `rails_search_code`",
242
216
  "- `rails_validate(files:[\"path/to/file.rb\"])` — validate Ruby, ERB, JS syntax in one call",
243
217
  ""
@@ -6,6 +6,7 @@ module RailsAiContext
6
6
  # for GitHub Copilot path-specific instructions.
7
7
  class CopilotInstructionsSerializer
8
8
  include StackOverviewHelper
9
+ include DesignSystemHelper
9
10
 
10
11
  attr_reader :context
11
12
 
@@ -193,37 +194,10 @@ module RailsAiContext
193
194
  "---",
194
195
  "applyTo: \"app/views/**/*.erb\"",
195
196
  "---",
196
- "",
197
- "# UI Patterns",
198
197
  ""
199
198
  ]
200
- components.first(15).each { |c| next unless c[:label] && c[:classes]; lines << "- #{c[:label]}: `#{c[:classes]}`" }
201
-
202
- # Shared partials
203
- begin
204
- root = defined?(Rails) ? Rails.root.to_s : Dir.pwd
205
- shared_dir = File.join(root, "app", "views", "shared")
206
- if Dir.exist?(shared_dir)
207
- partials = Dir.glob(File.join(shared_dir, "_*.html.erb")).map { |f| File.basename(f) }.sort
208
- if partials.any?
209
- lines << "" << "## Shared partials"
210
- partials.each { |p| lines << "- #{p}" }
211
- end
212
- end
213
- rescue; end
214
199
 
215
- # Helpers
216
- begin
217
- root = defined?(Rails) ? Rails.root.to_s : Dir.pwd
218
- helper_file = File.join(root, "app", "helpers", "application_helper.rb")
219
- if File.exist?(helper_file)
220
- helper_methods = File.read(helper_file).scan(/def\s+(\w+)/).flatten
221
- if helper_methods.any?
222
- lines << "" << "## Helpers (ApplicationHelper)"
223
- helper_methods.each { |m| lines << "- #{m}" }
224
- end
225
- end
226
- rescue; end
200
+ lines.concat(render_design_system_full(context))
227
201
 
228
202
  # Stimulus controllers
229
203
  stim = context[:stimulus]
@@ -245,7 +219,7 @@ module RailsAiContext
245
219
  "applyTo: \"**/*\"",
246
220
  "---",
247
221
  "",
248
- "# Rails MCP Tools (14) — Use These First",
222
+ "# Rails MCP Tools (15) — Use These First",
249
223
  "",
250
224
  "Use MCP for reference files (schema, routes, tests). Read directly if you'll edit.",
251
225
  "MCP tools return line numbers for surgical edits.",
@@ -259,6 +233,7 @@ module RailsAiContext
259
233
  "- `rails_get_stimulus(detail:\"summary\")` → `(controller:\"name\")` — targets, actions, values",
260
234
  "- `rails_get_test_info(detail:\"full\")` — fixtures, factories, helpers; `(model:\"Cook\")` — existing tests",
261
235
  "- `rails_analyze_feature(feature:\"auth\")` — schema + models + controllers + routes for a feature",
236
+ "- `rails_get_design_system` — color palette, components, canonical page examples",
262
237
  "- `rails_get_config` | `rails_get_gems` | `rails_get_conventions` | `rails_search_code`",
263
238
  "- `rails_get_edit_context(file:\"path\", near:\"keyword\")` — surgical edit context with line numbers",
264
239
  "- `rails_validate(files:[\"path\"])` — validate Ruby, ERB, JS syntax in one call",
@@ -8,6 +8,7 @@ module RailsAiContext
8
8
  class CopilotSerializer
9
9
  include TestCommandDetection
10
10
  include StackOverviewHelper
11
+ include DesignSystemHelper
11
12
 
12
13
  attr_reader :context
13
14
 
@@ -87,16 +88,8 @@ module RailsAiContext
87
88
  end
88
89
  end
89
90
 
90
- # UI Patterns
91
- vt = context[:view_templates]
92
- if vt.is_a?(Hash) && !vt[:error]
93
- components = vt.dig(:ui_patterns, :components) || []
94
- if components.any?
95
- lines << "## UI Patterns"
96
- components.first(15).each { |c| next unless c[:label] && c[:classes]; lines << "- #{c[:label]}: `#{c[:classes]}`" }
97
- lines << ""
98
- end
99
- end
91
+ # Design System
92
+ lines.concat(render_design_system(context, max_lines: 35))
100
93
 
101
94
  # MCP tools
102
95
  lines << "## MCP Tool Reference"