@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.
- package/README.md +64 -1
- package/app/assets/javascript/midwest/index.ts +8 -0
- package/app/assets/javascript/midwest/keyboard_navigation.ts +68 -0
- package/app/assets/javascript/midwest.js +476 -37
- package/app/assets/javascript/midwest.js.map +1 -1
- package/app/assets/stylesheets/midwest.css +1 -1
- package/app/assets/stylesheets/midwest.tailwind.css +13 -1
- package/dist/css/midwest.css +1 -1
- package/dist/javascript/collection/app/assets/javascript/midwest/index.js +8 -0
- package/dist/javascript/collection/app/assets/javascript/midwest/index.js.map +1 -1
- package/dist/javascript/collection/app/assets/javascript/midwest/keyboard_navigation.js +42 -0
- package/dist/javascript/collection/app/assets/javascript/midwest/keyboard_navigation.js.map +1 -0
- package/dist/javascript/collection/app/components/midwest/dropdown_component/dropdown_component_controller.js +8 -37
- package/dist/javascript/collection/app/components/midwest/dropdown_component/dropdown_component_controller.js.map +1 -1
- package/dist/javascript/collection/app/components/midwest/focus_trap_component/focus_trap_component_controller.js +83 -0
- package/dist/javascript/collection/app/components/midwest/focus_trap_component/focus_trap_component_controller.js.map +1 -0
- package/dist/javascript/collection/app/components/midwest/form/switch_component/theme_switch_controller.js +42 -0
- package/dist/javascript/collection/app/components/midwest/form/switch_component/theme_switch_controller.js.map +1 -0
- package/dist/javascript/collection/app/components/midwest/intersection_observer_component/intersection_observer_component_controller.js +43 -0
- package/dist/javascript/collection/app/components/midwest/intersection_observer_component/intersection_observer_component_controller.js.map +1 -0
- package/dist/javascript/collection/app/components/midwest/onboarding_component/onboarding_component_controller.js +273 -0
- package/dist/javascript/collection/app/components/midwest/onboarding_component/onboarding_component_controller.js.map +1 -0
- package/dist/javascript/midwest.js +476 -37
- package/dist/javascript/midwest.js.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
[](https://github.com/unabridged/midwest/actions/workflows/rails-tests.yml)
|
|
4
4
|
[](https://github.com/unabridged/midwest/actions/workflows/bridgetown-tests.yml)
|
|
5
5
|
[](https://github.com/unabridged/midwest/actions/workflows/ci.yml)
|
|
6
|
-
[](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
|
+
}
|