@unabridged/midwest 0.22.0 → 0.23.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 (25) hide show
  1. package/README.md +64 -1
  2. package/app/assets/javascript/midwest/index.ts +8 -0
  3. package/app/assets/javascript/midwest/keyboard_navigation.ts +68 -0
  4. package/app/assets/javascript/midwest.js +476 -37
  5. package/app/assets/javascript/midwest.js.map +1 -1
  6. package/app/assets/stylesheets/midwest.css +1 -1
  7. package/app/assets/stylesheets/midwest.tailwind.css +13 -1
  8. package/dist/css/midwest.css +1 -1
  9. package/dist/javascript/collection/app/assets/javascript/midwest/index.js +8 -0
  10. package/dist/javascript/collection/app/assets/javascript/midwest/index.js.map +1 -1
  11. package/dist/javascript/collection/app/assets/javascript/midwest/keyboard_navigation.js +42 -0
  12. package/dist/javascript/collection/app/assets/javascript/midwest/keyboard_navigation.js.map +1 -0
  13. package/dist/javascript/collection/app/components/midwest/dropdown_component/dropdown_component_controller.js +8 -37
  14. package/dist/javascript/collection/app/components/midwest/dropdown_component/dropdown_component_controller.js.map +1 -1
  15. package/dist/javascript/collection/app/components/midwest/focus_trap_component/focus_trap_component_controller.js +83 -0
  16. package/dist/javascript/collection/app/components/midwest/focus_trap_component/focus_trap_component_controller.js.map +1 -0
  17. package/dist/javascript/collection/app/components/midwest/form/switch_component/theme_switch_controller.js +42 -0
  18. package/dist/javascript/collection/app/components/midwest/form/switch_component/theme_switch_controller.js.map +1 -0
  19. package/dist/javascript/collection/app/components/midwest/intersection_observer_component/intersection_observer_component_controller.js +43 -0
  20. package/dist/javascript/collection/app/components/midwest/intersection_observer_component/intersection_observer_component_controller.js.map +1 -0
  21. package/dist/javascript/collection/app/components/midwest/onboarding_component/onboarding_component_controller.js +273 -0
  22. package/dist/javascript/collection/app/components/midwest/onboarding_component/onboarding_component_controller.js.map +1 -0
  23. package/dist/javascript/midwest.js +476 -37
  24. package/dist/javascript/midwest.js.map +1 -1
  25. package/package.json +1 -1
package/README.md CHANGED
@@ -3,7 +3,7 @@
3
3
  [![Rails Tests](https://github.com/unabridged/midwest/actions/workflows/rails-tests.yml/badge.svg?branch=main)](https://github.com/unabridged/midwest/actions/workflows/rails-tests.yml)
4
4
  [![Bridgetown Tests](https://github.com/unabridged/midwest/actions/workflows/bridgetown-tests.yml/badge.svg?branch=main)](https://github.com/unabridged/midwest/actions/workflows/bridgetown-tests.yml)
5
5
  [![Ruby](https://img.shields.io/badge/Ruby-3.3_%7C_3.4-CC342D?logo=ruby&logoColor=white)](https://github.com/unabridged/midwest/actions/workflows/ci.yml)
6
- [![Coverage](https://img.shields.io/badge/coverage-95%25-brightgreen)](https://github.com/unabridged/midwest/actions/workflows/ci.yml)
6
+ [![Coverage](https://img.shields.io/badge/coverage-96%25-brightgreen)](https://github.com/unabridged/midwest/actions/workflows/ci.yml)
7
7
 
8
8
  Midwest Component Library is an implementation of the Midwest Design System using [ViewComponent](https://github.com/github/view_component).
9
9
 
@@ -109,6 +109,69 @@ bin/rails midwest:generate:component YourComponent
109
109
 
110
110
  The syntax follows [ViewComponent's generator](https://viewcomponent.org/guide/generators.html).
111
111
 
112
+ ### MCP Server
113
+
114
+ Midwest ships an [MCP](https://modelcontextprotocol.io) server that gives AI agents live access to the component API. To connect, add to `.mcp.json`:
115
+
116
+ ```json
117
+ {
118
+ "mcpServers": {
119
+ "midwest": {
120
+ "command": "bundle",
121
+ "args": ["exec", "midwest_mcp"]
122
+ }
123
+ }
124
+ }
125
+ ```
126
+
127
+ The server exposes 8 tools: `list_components`, `get_component_api`, `search_components`, `get_component_examples`, `get_helper_methods`, `get_form_builder_info`, `get_component_relationships`, and `validate_component_usage`.
128
+
129
+ ### Component Guidance
130
+
131
+ The MCP server includes a guidance system that helps agents avoid common composition mistakes. Guidance is stored in `lib/midwest/mcp/data/component_guidance.yml` and surfaced through MCP tools automatically.
132
+
133
+ Two data sources feed into guidance:
134
+
135
+ - **Auto-detected**: Template scanning discovers which components internally render other components (e.g., `StatComponent` renders a `CardComponent`). This powers redundant nesting warnings.
136
+ - **Curated**: Anti-patterns and best practices added to the YAML file from real agent mistakes observed in the field.
137
+
138
+ Use `bin/guidance` to manage guidance data:
139
+
140
+ ```bash
141
+ bin/guidance # List components with guidance and auto-detected internal renders
142
+ bin/guidance show stat # Show full guidance for a component
143
+ bin/guidance add # Describe a pattern in plain text; Claude formats and appends it
144
+ bin/guidance scan # Display the internally-renders graph for all components
145
+ bin/guidance check # Validate YAML keys against the component index
146
+ ```
147
+
148
+ The `add` command opens your `$EDITOR` for you to describe an anti-pattern or best practice in plain English. When you save and close, the Claude CLI processes your description into properly formatted YAML and appends it to the guidance file. This is the fastest way to capture a mistake you spotted an agent making in the field.
149
+
150
+ You can also edit `lib/midwest/mcp/data/component_guidance.yml` directly:
151
+
152
+ ```yaml
153
+ stat_component:
154
+ anti_patterns:
155
+ - title: "Wrapping stat in a card"
156
+ description: "StatComponent already renders a CardComponent internally."
157
+ bad: |
158
+ <%= midwest_card do %>
159
+ <%= midwest_stat value: "1,234", label: "Users" %>
160
+ <% end %>
161
+ good: |
162
+ <%= midwest_stat value: "1,234", label: "Users" %>
163
+ best_practices:
164
+ - title: "Group stats in a grid"
165
+ description: "Use midwest_grid to lay out multiple stats."
166
+ example: |
167
+ <%= midwest_grid columns: 3 do %>
168
+ <%= midwest_stat value: "1,234", label: "Users" %>
169
+ <%= midwest_stat value: "567", label: "Orders" %>
170
+ <% end %>
171
+ ```
172
+
173
+ Run `bin/guidance check` after editing to verify all component keys are valid.
174
+
112
175
  ### Deploying your application
113
176
 
114
177
  In order to deploy your application the deploy server will need access to the private repo. One way to accomplish that is to add the deploy key on this repo to the deploy server. You should be able to create the key from the values in the Shared 1Password vault entry "GitHub - Midwest"
@@ -32,6 +32,10 @@ import PasswordRequirements from '../../../components/midwest/password_requireme
32
32
  import CodeBlock from '../../../components/midwest/code_block_component/code_block_component_controller'
33
33
  import DatePicker from '../../../components/midwest/form/date_picker_component/date_picker_component_controller'
34
34
  import TimePicker from '../../../components/midwest/form/time_picker_component/time_picker_component_controller'
35
+ import IntersectionObserver from '../../../components/midwest/intersection_observer_component/intersection_observer_component_controller'
36
+ import FocusTrap from '../../../components/midwest/focus_trap_component/focus_trap_component_controller'
37
+ import Onboarding from '../../../components/midwest/onboarding_component/onboarding_component_controller'
38
+ import ThemeSwitch from '../../../components/midwest/form/switch_component/theme_switch_controller'
35
39
  /* IMPORTS */
36
40
 
37
41
  export { Card, Banner, CountdownTimer, Chart }
@@ -71,5 +75,9 @@ export function registerMidwestControllers (application: any) {
71
75
  application.register('midwest-code-block', CodeBlock)
72
76
  application.register('midwest-date-picker', DatePicker)
73
77
  application.register('midwest-time-picker', TimePicker)
78
+ application.register('midwest-intersection-observer', IntersectionObserver)
79
+ application.register('midwest-focus-trap', FocusTrap)
80
+ application.register('midwest-onboarding', Onboarding)
81
+ application.register('midwest-theme-switch', ThemeSwitch)
74
82
  /* EXPORTS */
75
83
  }
@@ -0,0 +1,68 @@
1
+ // Shared keyboard-navigation helpers for menu/list components.
2
+ //
3
+ // Implements the WAI-ARIA "roving focus" pattern: arrow keys move DOM focus
4
+ // between a flat list of items, Home/End jump to the ends, and Escape/Tab hand
5
+ // control back to the caller (typically to close a popover and restore focus).
6
+ // Components own *which* elements are navigable and *what* closing means; this
7
+ // module owns the key handling so the behavior stays consistent across the
8
+ // library.
9
+
10
+ const NAVIGATION_KEYS = ['ArrowDown', 'ArrowUp', 'Home', 'End', 'Escape', 'Tab']
11
+
12
+ export interface RovingFocusOptions {
13
+ // Called on Escape. Typically closes the menu and returns focus to the trigger.
14
+ onEscape?: () => void
15
+ // Called on Tab. The default Tab behavior is left intact so focus still leaves
16
+ // the menu; use this to close the menu alongside it.
17
+ onTab?: () => void
18
+ // When true (default) arrow navigation wraps from the last item to the first
19
+ // and vice versa. When false it stops at the ends.
20
+ loop?: boolean
21
+ }
22
+
23
+ // Handles a keydown against a roving-focus list. Returns true when the event
24
+ // was one this helper is responsible for, so callers can `stopPropagation()`
25
+ // only for keys that were actually handled.
26
+ export function handleRovingFocusKeydown (
27
+ event: KeyboardEvent,
28
+ items: HTMLElement[],
29
+ options: RovingFocusOptions = {}
30
+ ): boolean {
31
+ if (items.length === 0) return false
32
+ if (!NAVIGATION_KEYS.includes(event.key)) return false
33
+
34
+ const { onEscape, onTab, loop = true } = options
35
+ const current = items.findIndex(item => item === document.activeElement)
36
+
37
+ switch (event.key) {
38
+ case 'ArrowDown': {
39
+ event.preventDefault()
40
+ const next = current < items.length - 1 ? current + 1 : (loop ? 0 : items.length - 1)
41
+ items[next].focus()
42
+ break
43
+ }
44
+ case 'ArrowUp': {
45
+ event.preventDefault()
46
+ const prev = current > 0 ? current - 1 : (loop ? items.length - 1 : 0)
47
+ items[prev].focus()
48
+ break
49
+ }
50
+ case 'Home':
51
+ event.preventDefault()
52
+ items[0].focus()
53
+ break
54
+ case 'End':
55
+ event.preventDefault()
56
+ items[items.length - 1].focus()
57
+ break
58
+ case 'Escape':
59
+ event.preventDefault()
60
+ onEscape?.()
61
+ break
62
+ case 'Tab':
63
+ onTab?.()
64
+ break
65
+ }
66
+
67
+ return true
68
+ }