keystone_ui 0.6.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 (131) hide show
  1. checksums.yaml +7 -0
  2. data/app/assets/javascripts/keystone_ui/accordion_controller.js +21 -0
  3. data/app/assets/javascripts/keystone_ui/auto_submit_controller.js +7 -0
  4. data/app/assets/javascripts/keystone_ui/chartjs.js +24 -0
  5. data/app/assets/javascripts/keystone_ui/clipboard_controller.js +25 -0
  6. data/app/assets/javascripts/keystone_ui/color_picker_controller.js +158 -0
  7. data/app/assets/javascripts/keystone_ui/column_picker_controller.js +47 -0
  8. data/app/assets/javascripts/keystone_ui/dismiss_controller.js +7 -0
  9. data/app/assets/javascripts/keystone_ui/dropdown_controller.js +16 -0
  10. data/app/assets/javascripts/keystone_ui/file_upload_controller.js +61 -0
  11. data/app/assets/javascripts/keystone_ui/index.js +33 -0
  12. data/app/assets/javascripts/keystone_ui/line_chart_controller.js +38 -0
  13. data/app/assets/javascripts/keystone_ui/modal_controller.js +19 -0
  14. data/app/assets/javascripts/keystone_ui/multi_select_controller.js +35 -0
  15. data/app/assets/javascripts/keystone_ui/stat_card_info_controller.js +9 -0
  16. data/app/assets/javascripts/keystone_ui/swipe_deck_controller.js +123 -0
  17. data/app/assets/javascripts/keystone_ui/tab_switcher_controller.js +19 -0
  18. data/app/assets/tailwind/keystone_ui_engine/color_picker.css +31 -0
  19. data/app/assets/tailwind/keystone_ui_engine/engine.css +7 -0
  20. data/app/assets/tailwind/keystone_ui_engine/nav.css +175 -0
  21. data/app/assets/tailwind/keystone_ui_engine/theme.css +32 -0
  22. data/app/components/keystone/ui/accordion_component.html.erb +19 -0
  23. data/app/components/keystone/ui/accordion_component.rb +51 -0
  24. data/app/components/keystone/ui/alert_component.html.erb +15 -0
  25. data/app/components/keystone/ui/alert_component.rb +58 -0
  26. data/app/components/keystone/ui/badge_component.html.erb +1 -0
  27. data/app/components/keystone/ui/badge_component.rb +28 -0
  28. data/app/components/keystone/ui/bottom_nav_component.html.erb +3 -0
  29. data/app/components/keystone/ui/bottom_nav_component.rb +13 -0
  30. data/app/components/keystone/ui/bottom_nav_item_component.html.erb +4 -0
  31. data/app/components/keystone/ui/bottom_nav_item_component.rb +24 -0
  32. data/app/components/keystone/ui/button_component.html.erb +1 -0
  33. data/app/components/keystone/ui/button_component.rb +57 -0
  34. data/app/components/keystone/ui/card_component.html.erb +9 -0
  35. data/app/components/keystone/ui/card_component.rb +33 -0
  36. data/app/components/keystone/ui/card_link_component.html.erb +3 -0
  37. data/app/components/keystone/ui/card_link_component.rb +25 -0
  38. data/app/components/keystone/ui/chart_card_component.html.erb +6 -0
  39. data/app/components/keystone/ui/chart_card_component.rb +35 -0
  40. data/app/components/keystone/ui/code_component.html.erb +6 -0
  41. data/app/components/keystone/ui/code_component.rb +40 -0
  42. data/app/components/keystone/ui/color_picker_component.html.erb +37 -0
  43. data/app/components/keystone/ui/color_picker_component.rb +31 -0
  44. data/app/components/keystone/ui/column.rb +21 -0
  45. data/app/components/keystone/ui/column_picker_component.html.erb +20 -0
  46. data/app/components/keystone/ui/column_picker_component.rb +35 -0
  47. data/app/components/keystone/ui/copy_button_component.html.erb +11 -0
  48. data/app/components/keystone/ui/copy_button_component.rb +39 -0
  49. data/app/components/keystone/ui/cta_banner_component.html.erb +9 -0
  50. data/app/components/keystone/ui/cta_banner_component.rb +39 -0
  51. data/app/components/keystone/ui/data_table_component.html.erb +54 -0
  52. data/app/components/keystone/ui/data_table_component.rb +173 -0
  53. data/app/components/keystone/ui/disclosure_component.html.erb +9 -0
  54. data/app/components/keystone/ui/disclosure_component.rb +46 -0
  55. data/app/components/keystone/ui/feature_grid_component.html.erb +17 -0
  56. data/app/components/keystone/ui/feature_grid_component.rb +55 -0
  57. data/app/components/keystone/ui/file_upload_component.html.erb +18 -0
  58. data/app/components/keystone/ui/file_upload_component.rb +85 -0
  59. data/app/components/keystone/ui/form_component.html.erb +3 -0
  60. data/app/components/keystone/ui/form_component.rb +27 -0
  61. data/app/components/keystone/ui/form_field_component.html.erb +36 -0
  62. data/app/components/keystone/ui/form_field_component.rb +83 -0
  63. data/app/components/keystone/ui/form_page_component.html.erb +10 -0
  64. data/app/components/keystone/ui/form_page_component.rb +21 -0
  65. data/app/components/keystone/ui/funnel_component.html.erb +14 -0
  66. data/app/components/keystone/ui/funnel_component.rb +85 -0
  67. data/app/components/keystone/ui/grid_component.html.erb +3 -0
  68. data/app/components/keystone/ui/grid_component.rb +38 -0
  69. data/app/components/keystone/ui/hero_component.html.erb +27 -0
  70. data/app/components/keystone/ui/hero_component.rb +74 -0
  71. data/app/components/keystone/ui/input_component.html.erb +1 -0
  72. data/app/components/keystone/ui/input_component.rb +57 -0
  73. data/app/components/keystone/ui/line_chart_component.html.erb +3 -0
  74. data/app/components/keystone/ui/line_chart_component.rb +48 -0
  75. data/app/components/keystone/ui/mobile_actions_component.html.erb +11 -0
  76. data/app/components/keystone/ui/mobile_actions_component.rb +21 -0
  77. data/app/components/keystone/ui/mobile_header_component.html.erb +11 -0
  78. data/app/components/keystone/ui/mobile_header_component.rb +38 -0
  79. data/app/components/keystone/ui/modal_component.html.erb +13 -0
  80. data/app/components/keystone/ui/modal_component.rb +68 -0
  81. data/app/components/keystone/ui/multi_select_component.html.erb +21 -0
  82. data/app/components/keystone/ui/multi_select_component.rb +40 -0
  83. data/app/components/keystone/ui/nav_dropdown_component.html.erb +11 -0
  84. data/app/components/keystone/ui/nav_dropdown_component.rb +33 -0
  85. data/app/components/keystone/ui/nav_item_component.html.erb +1 -0
  86. data/app/components/keystone/ui/nav_item_component.rb +21 -0
  87. data/app/components/keystone/ui/navbar_component.html.erb +34 -0
  88. data/app/components/keystone/ui/navbar_component.rb +32 -0
  89. data/app/components/keystone/ui/option_card_component.html.erb +9 -0
  90. data/app/components/keystone/ui/option_card_component.rb +28 -0
  91. data/app/components/keystone/ui/page_component.html.erb +3 -0
  92. data/app/components/keystone/ui/page_component.rb +40 -0
  93. data/app/components/keystone/ui/page_header_component.html.erb +17 -0
  94. data/app/components/keystone/ui/page_header_component.rb +42 -0
  95. data/app/components/keystone/ui/panel_component.html.erb +3 -0
  96. data/app/components/keystone/ui/panel_component.rb +25 -0
  97. data/app/components/keystone/ui/pipeline_component.html.erb +34 -0
  98. data/app/components/keystone/ui/pipeline_component.rb +48 -0
  99. data/app/components/keystone/ui/progress_component.html.erb +8 -0
  100. data/app/components/keystone/ui/progress_component.rb +35 -0
  101. data/app/components/keystone/ui/radio_card_component.html.erb +9 -0
  102. data/app/components/keystone/ui/radio_card_component.rb +42 -0
  103. data/app/components/keystone/ui/section_component.html.erb +18 -0
  104. data/app/components/keystone/ui/section_component.rb +32 -0
  105. data/app/components/keystone/ui/select_component.html.erb +8 -0
  106. data/app/components/keystone/ui/select_component.rb +38 -0
  107. data/app/components/keystone/ui/settings_link_component.html.erb +4 -0
  108. data/app/components/keystone/ui/settings_link_component.rb +26 -0
  109. data/app/components/keystone/ui/show_page_component.html.erb +4 -0
  110. data/app/components/keystone/ui/show_page_component.rb +21 -0
  111. data/app/components/keystone/ui/stat_card_component.html.erb +22 -0
  112. data/app/components/keystone/ui/stat_card_component.rb +92 -0
  113. data/app/components/keystone/ui/swipe_deck_component.html.erb +39 -0
  114. data/app/components/keystone/ui/swipe_deck_component.rb +52 -0
  115. data/app/components/keystone/ui/tab_switcher_component.html.erb +14 -0
  116. data/app/components/keystone/ui/tab_switcher_component.rb +33 -0
  117. data/app/components/keystone/ui/textarea_component.html.erb +1 -0
  118. data/app/components/keystone/ui/textarea_component.rb +38 -0
  119. data/app/helpers/keystone_ui_helper.rb +195 -0
  120. data/config/importmap.rb +19 -0
  121. data/lib/generators/keystone/install_generator.rb +90 -0
  122. data/lib/keystone_ui/configuration.rb +24 -0
  123. data/lib/keystone_ui/engine.rb +47 -0
  124. data/lib/keystone_ui/safelist.rb +106 -0
  125. data/lib/keystone_ui/version.rb +5 -0
  126. data/lib/keystone_ui.rb +9 -0
  127. data/the_local/agents/keystone_ui-develop.md +297 -0
  128. data/the_local/agents/keystone_ui-info.md +81 -0
  129. data/the_local/agents/keystone_ui-install.md +137 -0
  130. data/the_local/interface.yml +110 -0
  131. metadata +203 -0
@@ -0,0 +1,297 @@
1
+ ---
2
+ name: keystone_ui-develop
3
+ description: Use PROACTIVELY for building or editing screens in a Rails app that has Keystone UI — pages, forms, tables, navigation, dashboards, charts, marketing sections — MUST BE USED instead of hand-writing ERB and Tailwind for UI.
4
+ tools: Read, Write, Edit, Grep
5
+ scope: UI — pages, forms, tables, navigation, dashboards
6
+ ---
7
+
8
+ This local builds screens by composing Keystone UI's `ui_*` view helpers in ERB.
9
+ It always works the same way: pick the page shell, fill it with the helpers that
10
+ match the content, and write no Tailwind classes of its own.
11
+
12
+ ## What Keystone UI is
13
+
14
+ Keystone UI is a Rails engine that supplies an app's visual layer as a library of
15
+ view helpers. Each helper renders one named piece — a page shell, a section, a
16
+ form field, a data table, a navigation bar, a stat card, a chart — with all
17
+ styling and dark-mode treatment owned inside the gem. Two screens built from the
18
+ same helpers cannot drift apart, and a change to a piece updates every screen at
19
+ once. It is mobile-first: several helpers ship distinct mobile and desktop
20
+ treatments, which matters because these apps are often viewed in a native
21
+ webview.
22
+
23
+ Fire on any request to build or change a screen, view, form, table, navigation,
24
+ or dashboard in an app that has Keystone UI installed. If the `ui_*` helpers are
25
+ not available in the app yet, that is the `keystone_ui-install` local's job, not
26
+ this one's.
27
+
28
+ ## Interface
29
+
30
+ Every entry point is a view helper called from ERB. Keywords with defaults are
31
+ optional; the rest are required. Symbol options are validated — an unrecognized
32
+ one raises at render time.
33
+
34
+ ### Page shells and layout
35
+
36
+ - `ui_page(max_width: :full, padding: :standard, top_offset: nil)` — takes a
37
+ block. The outer wrapper for a screen. `max_width:` `:sm` `:md` `:lg` `:xl`
38
+ `:full` (anything but `:full` also centers); `padding:` `:standard` or `:none`;
39
+ `top_offset:` `:sm` `:md` `:lg` `:xl` to clear a fixed navbar.
40
+ - `ui_section(title: nil, subtitle: nil, action: nil, spacing: :md)` — takes a
41
+ block. A titled block of content with an optional right-aligned link.
42
+ `action:` is `{ label:, href: }`; `spacing:` `:sm` `:md` `:lg`.
43
+ - `ui_panel(padding: :md, radius: :lg, shadow: true)` — takes a block. A bordered
44
+ card surface. `padding:` `:sm` `:md` `:lg`; `radius:` `:md` `:lg` `:xl`.
45
+ - `ui_grid(cols: { default: 1 }, gap: :md, gap_x: nil, gap_y: nil)` — takes a
46
+ block. A responsive grid. `cols:` maps breakpoints `:default` `:sm` `:md` `:lg`
47
+ to a column count 1–12, e.g. `{ default: 1, md: 3 }`. `gap:` `:sm` `:md` `:lg`
48
+ `:xl`; passing `gap_x:`/`gap_y:` replaces `gap:` entirely.
49
+ - `ui_card_link(href:, padding: :md, shadow: true)` — takes a block. A whole
50
+ panel that is one link. `padding:` `:sm` `:md` `:lg`.
51
+ - `ui_card(title:, summary:, link:, cta: "Read more", edge_to_edge: false)` — a
52
+ fixed title/summary/CTA card. `edge_to_edge: true` drops the side border and
53
+ corner rounding below `sm:` so it spans the full width on mobile.
54
+ - `ui_page_header(title:, subtitle: nil, action_url: nil, action_label: "Add new")`
55
+ — takes a block yielding the header. Desktop-only page title (hidden below
56
+ `sm:`). Call `header.action { ... }` in the block to place a custom control on
57
+ the right; only what `action` receives is rendered. Passing `action_url:`
58
+ publishes that URL and label for a mobile navbar to pick up.
59
+ - `ui_form_page(title:, back_url:, subtitle: nil)` — the shell marker for a form
60
+ screen. Renders the desktop title block and publishes the title and back URL so
61
+ the navbar can render mobile header context.
62
+ - `ui_show_page(title:, back_url:, subtitle: nil)` — the shell marker for a
63
+ detail screen. Renders nothing itself; only publishes the title, subtitle, and
64
+ back URL for the navbar.
65
+
66
+ ### Navigation
67
+
68
+ - `ui_navbar(sticky: true)` — takes a block yielding the navbar. Fill named slots
69
+ on it: `logo`, `desktop_links`, `desktop_right`, `mobile_left`,
70
+ `mobile_center`, `mobile_right`. Desktop slots are hidden below `lg:` and the
71
+ mobile slots above it. `desktop_right` renders only when `desktop_links` is
72
+ also filled.
73
+ - `ui_nav_item(label:, href:, active: false)` — one desktop navigation link.
74
+ - `ui_nav_dropdown(title:, area:, active: false)` — takes a block. A navbar
75
+ dropdown; the block holds the menu links.
76
+ - `ui_bottom_nav` — no keywords, takes a block. The mobile bottom tab bar; hidden
77
+ above `lg:` and inside a Hotwire Native webview.
78
+ - `ui_bottom_nav_item(label:, href:, icon:, active: false)` — one bottom tab.
79
+ `icon:` is a raw SVG string.
80
+ - `ui_mobile_header(title:, back_url:, subtitle: nil)` — a back chevron plus
81
+ centered title for mobile; hidden above `lg:`. Place it in the navbar's
82
+ `mobile_left` slot.
83
+ - `ui_mobile_actions` — no keywords, takes a block. An ellipsis dropdown for
84
+ mobile actions; the block holds the menu items. Hidden above `lg:`.
85
+ - `ui_settings_link(label:, href:)` — a full-width settings row with a chevron.
86
+
87
+ ### Forms
88
+
89
+ - `ui_form(action:, method: :post, multipart: false, data: nil)` — takes a block.
90
+ The `<form>` wrapper. `method:` may be `:patch`/`:put`/`:delete` and is
91
+ translated for Rails. Set `multipart: true` when the form contains a file
92
+ upload.
93
+ - `ui_form_field(attribute:, label: nil, type: :text, required: false, hint: nil, placeholder: nil, min: nil, max: nil, step: nil, value: nil, options: [], errors: [])`
94
+ — a labeled field with hint and error text. This is the default way to render
95
+ an input. `type:` `:text` `:number` `:email` `:password` `:date` `:textarea`
96
+ `:checkbox` `:select`. `label:` defaults to the humanized attribute name.
97
+ `options:` is for `:select` and takes `[[label, value], ...]`; a non-required
98
+ select gets a leading blank option. A `:checkbox` submits `"0"` when unchecked
99
+ and `"1"` when checked, and pre-checks when `value:` is `"1"`. `errors:` is an
100
+ array of message strings.
101
+ - `ui_input(name:, type: :text, value: nil, placeholder: nil, disabled: false, min: nil, max: nil, step: nil)`
102
+ — a bare styled input with no label. `type:` `:text` `:number` `:email`
103
+ `:password` `:date`.
104
+ - `ui_textarea(name:, value: nil, rows: 3, placeholder: nil, disabled: false)` —
105
+ a bare styled textarea.
106
+ - `ui_select(name:, options: [], selected: nil, include_blank: nil, disabled: false)`
107
+ — a bare styled select. `options:` is `[[label, value], ...]`;
108
+ `include_blank:` is the text of a leading empty option.
109
+ - `ui_multi_select(name:, label:, options:, selected: [])` — a dropdown of
110
+ checkboxes all posting under `name`. `options:` is `[[label, value], ...]`;
111
+ the trigger reads "All <label>" when nothing is checked and "N selected"
112
+ otherwise.
113
+ - `ui_file_upload(name:, label: nil, accept: nil, multiple: false, hint: nil)` —
114
+ a drop zone with drag-and-drop and selected-file feedback. Requires the
115
+ enclosing form to be multipart.
116
+ - `ui_color_picker(name:, value: "#000000", label: nil)` — a swatch that opens a
117
+ hue/saturation panel and writes the hex into a hidden input named `name`.
118
+ - `ui_radio_card(name:, value:, label:, hint: nil, checked: false)` — a
119
+ selectable card backed by a real radio input; selection styling is pure CSS.
120
+ - `ui_option_card(name:, value:, selected: false, input_data: {}, label_data: {})`
121
+ — takes a block. A radio whose visible body is whatever the block renders.
122
+ `input_data:`/`label_data:` become `data-*` attributes on the input and label.
123
+
124
+ ### Tables
125
+
126
+ - `ui_data_table(items:, columns:, empty_message: nil, sort: nil, sort_direction: nil, sort_url: nil, hidden_columns: [])`
127
+ — takes a block yielding the table. `items:` are records or hashes; each cell
128
+ value is read by calling the column key on the item, falling back to `item[key]`.
129
+ `columns:` accepts plain `{ key: "Label" }` hashes or `Keystone::Ui::Column`
130
+ objects. In the block, `table.link(:column_key) { |item| url }` turns that
131
+ column's cells into links and `table.actions { |item| ... }` appends a
132
+ right-aligned actions column. Sorting requires all three of `sort:` (the
133
+ current column key), `sort_direction:` (`:asc`/`:desc`), and `sort_url:` (a
134
+ lambda taking `(column_key, direction)` and returning a URL); headers then
135
+ render as links that flip direction. `hidden_columns:` drops columns
136
+ server-side and only affects columns declared `hideable: true`.
137
+ - `Keystone::Ui::Column.new(key, header_text, mobile_hidden: false, sortable: false, hideable: false)`
138
+ — a column with per-column options, for when a `{ key: "Label" }` hash is not
139
+ enough. `mobile_hidden:` hides the column below `sm:`; `sortable:` opts it into
140
+ sort headers; `hideable:` lets the column picker hide it.
141
+ - `ui_column_picker(columns:, hidden_columns: [], save_url: nil)` — a "Columns"
142
+ dropdown of checkboxes for every `hideable` column. Pass it the same columns
143
+ and hidden keys as the table. On toggle it sends `PATCH save_url` with JSON
144
+ `{ "hidden_columns": ["key", ...] }` and a `X-CSRF-Token` header, then reloads
145
+ the page. The app must provide that endpoint and persist the list.
146
+
147
+ ### Content and status
148
+
149
+ - `ui_button(label:, href: nil, variant: :primary, size: :md, type: :submit, data: nil)`
150
+ — renders an `<a>` when `href:` is given and a `<button>` otherwise. `variant:`
151
+ `:primary` `:secondary` `:danger`; `size:` `:sm` `:md` `:lg`; `type:` applies
152
+ only to the button form.
153
+ - `ui_badge(label:, variant: :neutral)` — a pill. `variant:` `:neutral`
154
+ `:success` `:danger` `:warning` `:info`.
155
+ - `ui_alert(message:, type: :info, title: nil, dismissible: false)` — a banner.
156
+ `type:` `:info` `:success` `:warning` `:error`. `dismissible: true` adds a
157
+ close control.
158
+ - `ui_progress(value:, max:, label: nil)` — a labeled progress bar. The percent
159
+ is `value / max`, rounded and clamped at 100.
160
+ - `ui_stat_card(label:, value:, variant: :neutral, suffix: nil, definition: nil, calculation: nil, change: nil)`
161
+ — a single metric tile. `variant:` `:neutral` `:success` `:danger` `:warning`
162
+ `:info` colors the value. `change:` is a signed number rendered as `▲ 4.2%` in
163
+ green when positive, `▼` in red when negative, plain when zero. Passing
164
+ `definition:` and/or `calculation:` adds an info button that reveals them.
165
+ - `ui_copy_button(text:, label: "Copy", success_message: "Copied!", error_message: "Failed!")`
166
+ — copies `text:` to the clipboard.
167
+ - `ui_code(language: nil, caption: nil)` — takes a block holding the code. The
168
+ caption is a header strip above the block; `language:` sets the `language-*`
169
+ class for a highlighter.
170
+ - `ui_disclosure(open: false)` — takes a block yielding the component. Fill its
171
+ `summary` slot with the clickable header; the rest of the block is the body.
172
+ Native `<details>` — no JavaScript.
173
+ - `ui_accordion(items: [])` — a stack of independently expandable rows. `items:`
174
+ is `[{ question:, answer: }, ...]`.
175
+ - `ui_tab_switcher(tabs:)` — takes a block. `tabs:` is an array of label strings;
176
+ the first is active on load. The block renders below the tab bar. Selecting a
177
+ tab dispatches a `tab-switcher:change` event carrying the clicked index —
178
+ showing and hiding the matching panels is the app's job.
179
+ - `ui_modal(title:, size: :md)` — takes a block holding the body. `size:` `:sm`
180
+ `:md` `:lg` `:xl`. Renders hidden; it closes on its own close button and on a
181
+ backdrop click, but the app must supply the control that opens it by targeting
182
+ the modal controller's `open` action.
183
+ - `ui_swipe_deck(items:, empty_title: "All done!", empty_subtitle: nil)` — takes
184
+ a block yielding the deck. Call `deck.item { |item| ... }` in the block to
185
+ render one card's face. Accepting a card dispatches `swipe-deck:complete` and
186
+ rejecting dispatches `swipe-deck:skip`, both carrying the item's id — the app
187
+ must listen and persist the outcome.
188
+
189
+ ### Charts and analytics
190
+
191
+ - `ui_chart_card(title:, height: :md)` — takes a block holding a chart. `height:`
192
+ `:sm` `:md` `:lg`.
193
+ - `ui_line_chart(series:, labels:, height: :md)` — a line chart. `labels:` is the
194
+ x-axis labels; `series:` is `[{ name:, data:, color:, dashed: }, ...]` where
195
+ `color:` and `dashed:` are optional. `height:` `:sm` `:md` `:lg`.
196
+ - `ui_funnel(steps:)` — a conversion funnel. `steps:` is `[{ label:, value: }, ...]`
197
+ in order. Bar widths are relative to the first step; the caption between two
198
+ layers is the step-to-step conversion. Divide-by-zero safe, no JavaScript.
199
+ - `ui_pipeline(title:, boxes:, links:, subtitle: nil)` — a staged flow diagram
200
+ for event flows, approval chains, or state machines. `boxes:` is
201
+ `[{ label:, count:, accent:, action: }, ...]` where `count:` and `accent:`
202
+ (`:amber` `:emerald` `:danger` `:muted`) are optional, and `action:` is
203
+ `{ url:, label:, params:, variant: }` rendering a button that POSTs `params`
204
+ to `url`. `links:` has exactly one fewer entry than `boxes:`, each
205
+ `{ url:, params:, broken: }`, rendering a ✓/✗ toggle between two boxes that
206
+ POSTs to flip its state. The app owns every endpoint these post to.
207
+
208
+ ### Marketing sections
209
+
210
+ - `ui_hero(title:, subtitle: nil, badge: nil, layout: :split)` — takes a block
211
+ holding the call-to-action buttons, and yields the component so its `aside`
212
+ slot can hold an image or panel. `layout:` `:split` (content beside the aside)
213
+ or `:centered`.
214
+ - `ui_cta_banner(title:, subtitle: nil)` — takes a block holding the
215
+ call-to-action buttons.
216
+ - `ui_feature_grid(title:, features:, subtitle: nil)` — a responsive grid of
217
+ feature cards. `features:` is `[{ icon:, title:, description: }, ...]` where
218
+ `icon:` is a raw SVG or HTML string.
219
+
220
+ ## How to use it
221
+
222
+ 1. Confirm the helpers are available in the app. If they are not, stop and hand
223
+ off to `keystone_ui-install` — do not hand-roll the markup in the meantime.
224
+
225
+ 2. Find the closest existing screen in `app/views/` and read it. Match its
226
+ composition before inventing one; that screen is the house style.
227
+
228
+ 3. Pick the page shell for what you are building:
229
+ - a form screen → `ui_form_page`
230
+ - a detail screen → `ui_show_page`
231
+ - anything else → `ui_page`, with `ui_page_header` for the desktop title.
232
+
233
+ `ui_form_page` and `ui_show_page` publish their title and back URL for the
234
+ navbar to render as a mobile header. Check the app's layout: if it does not
235
+ already render `ui_mobile_header` from that published context, ask the
236
+ developer whether to wire it before adding more screens that depend on it.
237
+
238
+ 4. Lay out the body with `ui_section` for each titled group, `ui_grid` for
239
+ multi-column arrangements, and `ui_panel` or `ui_card_link` for card
240
+ surfaces. Nest them; each takes a block.
241
+
242
+ 5. Fill the body with the leaf helpers from the Interface above. Reach for the
243
+ most specific one that fits — `ui_form_field` over `ui_input`,
244
+ `ui_data_table` over a hand-built `<table>`, `ui_stat_card` over a panel with
245
+ text in it.
246
+
247
+ 6. For a table, decide how columns are declared. Use `{ key: "Label" }` hashes
248
+ when every column is plain. Switch the whole set to `Keystone::Ui::Column`
249
+ objects as soon as one column needs `mobile_hidden:`, `sortable:`, or
250
+ `hideable:`.
251
+
252
+ 7. Wire up anything that posts back. Several helpers render controls whose
253
+ endpoints the app must own — the column picker's save URL, the pipeline's box
254
+ and link URLs, the swipe deck's outcome events, the modal's open trigger, the
255
+ tab switcher's panel visibility. Each is named in the Interface. These are
256
+ real decisions about the app's domain: where a preference is persisted, what
257
+ a stage's action does, what happens when a card is accepted. Do not invent
258
+ routes or a persistence strategy — put the choice to the developer, then
259
+ implement what they pick.
260
+
261
+ 8. Read back what you wrote and delete every Tailwind class and inline `style`
262
+ you added. If the result still needs one, that is a signal the wrong helper
263
+ was chosen — go back to step 5. Bring it to the developer only if no helper
264
+ fits.
265
+
266
+ ## Conventions
267
+
268
+ - **Helpers only.** Call `ui_*` helpers from ERB. Never name a component class
269
+ directly in an app. `Keystone::Ui::Column` is the one exception — it is a value
270
+ object passed as an argument, not a thing that renders.
271
+ - **Never hand-write Tailwind for something a helper covers.** Utility classes
272
+ layered onto a helper's output fight the component and drift the moment the gem
273
+ updates. The gem owns spacing, color, borders, radius, shadow, and dark mode.
274
+ - **Never restyle a helper from the outside** — no wrapper div that overrides its
275
+ padding or width, no `class:` smuggled through, no CSS targeting its markup.
276
+ Choose a different option symbol instead, or say the helper does not fit.
277
+ - **Semantic color only.** Themed color is `accent-*` (the brand hue) and
278
+ `surface-*` (the neutral family). Never write a literal color into a view;
279
+ changing the palette is install-local territory.
280
+ - **Options are per-helper.** `variant:`, `size:`, `padding:`, and `spacing:` do
281
+ not share one vocabulary — a button's `variant:` and a badge's `variant:`
282
+ accept different symbols. Use the values listed above; a wrong symbol raises at
283
+ render time rather than degrading quietly.
284
+ - **Containers take blocks, leaves take keywords.** Helpers that wrap content
285
+ yield; helpers that render one thing are configured entirely by keywords.
286
+ Composite helpers (the navbar, the hero, the disclosure) yield the component so
287
+ named slots can be filled. Helpers that yield a receiver for registration —
288
+ the data table, page header, and swipe deck — only render what was registered
289
+ through it, so anything else emitted inside their block is discarded.
290
+ - **Mobile is not an afterthought.** Several helpers render only on one side of
291
+ the `lg:` (or `sm:`) breakpoint — page headers, mobile headers, mobile actions,
292
+ bottom navigation. A screen needs both treatments; check the small viewport
293
+ before calling it done.
294
+ - Out of scope for this local: installing or upgrading the gem, changing the
295
+ palette or theme defaults, and editing the components themselves. Building a
296
+ new UI primitive belongs in the gem, not in a host app's views — raise it with
297
+ the developer rather than approximating it locally.
@@ -0,0 +1,81 @@
1
+ ---
2
+ name: keystone_ui-info
3
+ description: Use to learn what Keystone UI offers — what the component system covers, its layout and color model, and the vocabulary the install and develop locals assume.
4
+ tools: Read
5
+ scope: UI — pages, forms, tables, navigation, dashboards
6
+ ---
7
+
8
+ This local explains Keystone UI and hands you off to the local that does the
9
+ work. It changes nothing and gives no steps.
10
+
11
+ ## What Keystone UI is
12
+
13
+ Keystone UI is a Rails engine gem that supplies a host app's entire visual
14
+ layer as a library of view helpers built on ViewComponent. Screens are composed
15
+ out of named pieces — page shells, sections, panels, grids, form fields, data
16
+ tables, navigation bars, cards, charts, banners — instead of hand-written ERB
17
+ and Tailwind. Every class the UI renders lives inside the gem, in frozen
18
+ constants, so the look is owned in one place.
19
+
20
+ Reach for it whenever you are building or changing a screen in an app that has
21
+ it installed. The point is to stop UI drift: two pages built from the same
22
+ helpers cannot disagree about spacing, color, or dark-mode treatment, and a
23
+ change to a component updates every page at once. It is mobile-first by
24
+ construction — components ship distinct mobile and desktop treatments (a bottom
25
+ tab bar and mobile header on small screens, a full navigation bar above the
26
+ `lg:` breakpoint), which matters because these apps are often viewed in a native
27
+ webview.
28
+
29
+ ## Interface
30
+
31
+ This local declares no commands. The two working surfaces belong elsewhere:
32
+
33
+ - **Getting the gem into a host app** — adding it, wiring Tailwind and Stimulus,
34
+ setting the palette, refreshing the generated reference → **`keystone_ui-install`**.
35
+ - **Building UI with it** — which helper renders what, what keywords it takes,
36
+ how helpers nest → **`keystone_ui-develop`**.
37
+
38
+ ## How to use it
39
+
40
+ One decision: is the app already wearing Keystone UI?
41
+
42
+ - No, or it is out of date → **`keystone_ui-install`**.
43
+ - Yes, and you have a screen to build or edit → **`keystone_ui-develop`**. Do not
44
+ hand-write ERB or Tailwind for UI it covers.
45
+
46
+ Questions about which piece fits a scenario are also answered by the develop
47
+ local — it owns the catalog.
48
+
49
+ ## Conventions
50
+
51
+ - **Helpers, not classes.** Every entry point is a view helper prefixed `ui_`,
52
+ called from ERB. Components live under the `Keystone::Ui` namespace, but a
53
+ host app should not name a component class directly.
54
+ - **Containers take blocks; leaves take keywords.** Helpers that wrap content
55
+ (page shells, sections, panels, grids, forms, tables) yield a block; helpers
56
+ that render one thing (a button, a badge, a field, a stat) are configured
57
+ entirely by keyword arguments. Composite pieces such as the navigation bar
58
+ expose named slots rather than a single block.
59
+ - **Options are symbols, and the accepted set is per-component.** Appearance is
60
+ chosen by name — `variant:`, `size:`, `type:`, `padding:`, `spacing:`,
61
+ `max_width:`, `radius:` — on a small t-shirt scale (`:sm` … `:xl`) or a short
62
+ semantic list. Do not assume one global vocabulary: a button's `variant:` and
63
+ a badge's `variant:` accept different symbols, and `padding:` means different
64
+ things on a page shell than on a panel. The develop local carries the real
65
+ values. An unrecognized symbol raises rather than degrading silently, so a
66
+ wrong guess fails loudly at render time.
67
+ - **Semantic color, not literal color.** The themed hue is `accent-*` and the
68
+ themed neutral family is `surface-*` — Tailwind v4 CSS custom properties
69
+ defaulting to blue and zinc, so retheming an app is a token change, not a
70
+ component change. Older components still reach for Tailwind's stock
71
+ `gray-*`/`zinc-*` neutrals directly; the accent hue is themed throughout.
72
+ Dark-mode variants are already built into every component. Changing the
73
+ defaults is install-local territory.
74
+ - **Tailwind classes are static strings.** Class names are never interpolated,
75
+ so Tailwind's scanner can see them; the host needs tailwindcss-rails v4+ and
76
+ the engine tells Tailwind where to look.
77
+ - **Interactivity is Stimulus.** Dropdowns, modals, dismissible alerts, file
78
+ uploads, tab switchers, column pickers, clipboard copy and similar behavior
79
+ ship with the gem as Stimulus controllers registered in one call at install —
80
+ a host writes no JavaScript to use them.
81
+ - Ruby >= 3.2. ViewComponent >= 2.0.
@@ -0,0 +1,137 @@
1
+ ---
2
+ name: keystone_ui-install
3
+ description: Use to hook Keystone UI into a project — adding the gem, running the install generator to wire Tailwind and Stimulus, and setting the accent/surface palette.
4
+ tools: Bash, Read, Edit
5
+ scope: UI — pages, forms, tables, navigation, dashboards
6
+ ---
7
+
8
+ This local carries the steps for wiring Keystone UI into a host app. Follow them
9
+ in order, exactly as written, and invent none.
10
+
11
+ ## What Keystone UI is
12
+
13
+ A Rails engine gem that supplies an app's visual layer as `ui_*` view helpers
14
+ built on ViewComponent; hook it in before building any screen with those helpers.
15
+
16
+ ## Interface
17
+
18
+ - `bin/rails generate keystone:install` — sets up the host's Tailwind entry point
19
+ and registers the gem's Stimulus controllers. Idempotent, and safe to re-run.
20
+ - `KeystoneUi.configure` — a block yielding `accent` and `surface`, the names of
21
+ the palettes the app wants (`:blue` and `:zinc` unless set).
22
+
23
+ ## How to use it
24
+
25
+ 1. Confirm the prerequisites: Ruby >= 3.2 and **tailwindcss-rails v4+** in the
26
+ host app. The gem brings ViewComponent with it. Tailwind does not have to be
27
+ initialized first — the generator creates the stylesheet if it is missing.
28
+
29
+ 2. Add the gem to the Gemfile and install it:
30
+
31
+ ```ruby
32
+ gem "keystone_ui"
33
+ ```
34
+
35
+ ```bash
36
+ bundle install
37
+ ```
38
+
39
+ If the project sources its own gems from somewhere other than RubyGems (a git
40
+ or path reference), ask the developer which to use rather than choosing.
41
+
42
+ 3. Run the generator:
43
+
44
+ ```bash
45
+ bin/rails generate keystone:install
46
+ ```
47
+
48
+ It touches two host files:
49
+
50
+ - `app/assets/tailwind/application.css` — creates it with
51
+ `@import "tailwindcss";` and `@import "./keystone_source.css";` if absent;
52
+ otherwise injects the Keystone import after the Tailwind one and strips
53
+ superseded Keystone lines from older installs.
54
+ - `app/javascript/controllers/index.js` — appends
55
+ `import { registerControllers } from "keystone_ui/index"` and
56
+ `registerControllers(application)`.
57
+
58
+ Read its output. If it reports that `app/javascript/controllers/index.js` was
59
+ not found, ask the developer where the Stimulus application is set up and add
60
+ those two lines there — without them, dropdowns, modals, file uploads, the
61
+ column picker and the other interactive components do nothing.
62
+
63
+ 4. Restart the app (or rebuild assets). On boot the engine writes
64
+ `app/assets/tailwind/keystone_source.css`, which points Tailwind at the
65
+ component files and pulls in the gem's theme and component CSS. It is written
66
+ only when `application.css` exists **and** contains
67
+ `@import "./keystone_source.css";` — if the file never appears, that import is
68
+ missing.
69
+
70
+ 5. Keep the generated file out of git. It holds absolute paths to the gem on the
71
+ machine that booted the app, and is rewritten on every boot — dev server, CI,
72
+ and `assets:precompile` alike. Add to `.gitignore`:
73
+
74
+ ```
75
+ app/assets/tailwind/keystone_source.css
76
+ ```
77
+
78
+ Only the `@import` line in `application.css` belongs in the repo.
79
+
80
+ 6. Settle the palette. Out of the box the accent scale is blue and the surface
81
+ scale is zinc, with no configuration required. To change them statically, add
82
+ an `@theme` block to `application.css` **after** the two imports and override
83
+ only the shades the app uses:
84
+
85
+ ```css
86
+ @import "tailwindcss";
87
+ @import "./keystone_source.css";
88
+
89
+ @theme {
90
+ --color-accent-500: #6366f1;
91
+ --color-accent-600: #4f46e5;
92
+ }
93
+ ```
94
+
95
+ Both scales run 50 through 950. Every component picks the values up with no
96
+ component changes.
97
+
98
+ This is a decision, not a default to assume: fixed app-wide colors set in CSS,
99
+ or per-user colors generated at runtime by a companion theming gem. Ask the
100
+ developer which the app wants before wiring either.
101
+
102
+ 7. Only if a companion gem or engine reads the palette choice, declare it in
103
+ `config/initializers/keystone_ui.rb`:
104
+
105
+ ```ruby
106
+ require "keystone_ui"
107
+
108
+ KeystoneUi.configure do |config|
109
+ config.accent = :emerald
110
+ config.surface = :slate
111
+ end
112
+ ```
113
+
114
+ Keystone UI itself stores these names and renders from the CSS custom
115
+ properties regardless — setting them changes no color on its own. Colors move
116
+ when the custom properties in step 6 move. The engine reads host configuration
117
+ after initializers have run, so the initializer is the correct home for the
118
+ block.
119
+
120
+ ## Conventions
121
+
122
+ - **Verify the install before building anything on it.** `application.css` holds
123
+ both imports; `app/assets/tailwind/keystone_source.css` exists after a boot;
124
+ the Stimulus setup calls `registerControllers(application)`. Then load one page
125
+ that renders a `ui_*` helper and confirm it is styled and that an interactive
126
+ piece (a dropdown, a dismissible alert) responds.
127
+ - **Importmap is the supported JS path.** For apps configured with importmap the
128
+ gem pins its own controllers — and the charting library they need — on boot, so
129
+ the host pins nothing. If the app bundles JavaScript instead (esbuild, bun,
130
+ webpack), the appended `keystone_ui/index` import has no pin behind it — surface
131
+ this to the developer rather than guessing at a bundler configuration.
132
+ - **Re-run the generator after upgrading the gem.** It clears out superseded
133
+ install lines and reports "already up to date" when there is nothing to do.
134
+ - **New components need no re-run.** Tailwind rescans the gem on each build, so
135
+ components added by a later version are styled on the next boot.
136
+ - Building UI with the helpers — which helper to use, what keywords it takes — is
137
+ out of scope here and belongs to `keystone_ui-develop`.
@@ -0,0 +1,110 @@
1
+ scope: UI — pages, forms, tables, navigation, dashboards
2
+
3
+ install:
4
+ - bin/rails generate keystone:install
5
+ - KeystoneUi.configure
6
+
7
+ develop:
8
+ - ui_accordion
9
+ - ui_tab_switcher
10
+ - ui_stat_card
11
+ - ui_chart_card
12
+ - ui_line_chart
13
+ - ui_cta_banner
14
+ - ui_feature_grid
15
+ - ui_hero
16
+ - ui_modal
17
+ - ui_badge
18
+ - ui_copy_button
19
+ - ui_card
20
+ - ui_button
21
+ - ui_data_table
22
+ - ui_page
23
+ - ui_section
24
+ - ui_grid
25
+ - ui_panel
26
+ - ui_card_link
27
+ - ui_input
28
+ - ui_textarea
29
+ - ui_select
30
+ - ui_form_field
31
+ - ui_page_header
32
+ - ui_alert
33
+ - ui_form_page
34
+ - ui_show_page
35
+ - ui_mobile_header
36
+ - ui_mobile_actions
37
+ - ui_navbar
38
+ - ui_nav_item
39
+ - ui_nav_dropdown
40
+ - ui_bottom_nav
41
+ - ui_bottom_nav_item
42
+ - ui_option_card
43
+ - ui_radio_card
44
+ - ui_progress
45
+ - ui_settings_link
46
+ - ui_color_picker
47
+ - ui_multi_select
48
+ - ui_swipe_deck
49
+ - ui_column_picker
50
+ - ui_form
51
+ - ui_file_upload
52
+ - ui_funnel
53
+ - ui_pipeline
54
+ - ui_code
55
+ - ui_disclosure
56
+ - Keystone::Ui::Column
57
+
58
+ sources:
59
+ - lib/generators/keystone/install_generator.rb
60
+ - lib/keystone_ui/configuration.rb
61
+ - app/helpers/keystone_ui_helper.rb
62
+ - app/components/keystone/ui/accordion_component.rb
63
+ - app/components/keystone/ui/alert_component.rb
64
+ - app/components/keystone/ui/badge_component.rb
65
+ - app/components/keystone/ui/bottom_nav_component.rb
66
+ - app/components/keystone/ui/bottom_nav_item_component.rb
67
+ - app/components/keystone/ui/button_component.rb
68
+ - app/components/keystone/ui/card_component.rb
69
+ - app/components/keystone/ui/card_link_component.rb
70
+ - app/components/keystone/ui/chart_card_component.rb
71
+ - app/components/keystone/ui/code_component.rb
72
+ - app/components/keystone/ui/color_picker_component.rb
73
+ - app/components/keystone/ui/column_picker_component.rb
74
+ - app/components/keystone/ui/column.rb
75
+ - app/components/keystone/ui/copy_button_component.rb
76
+ - app/components/keystone/ui/cta_banner_component.rb
77
+ - app/components/keystone/ui/data_table_component.rb
78
+ - app/components/keystone/ui/disclosure_component.rb
79
+ - app/components/keystone/ui/feature_grid_component.rb
80
+ - app/components/keystone/ui/file_upload_component.rb
81
+ - app/components/keystone/ui/form_component.rb
82
+ - app/components/keystone/ui/form_field_component.rb
83
+ - app/components/keystone/ui/form_page_component.rb
84
+ - app/components/keystone/ui/funnel_component.rb
85
+ - app/components/keystone/ui/grid_component.rb
86
+ - app/components/keystone/ui/hero_component.rb
87
+ - app/components/keystone/ui/input_component.rb
88
+ - app/components/keystone/ui/line_chart_component.rb
89
+ - app/components/keystone/ui/mobile_actions_component.rb
90
+ - app/components/keystone/ui/mobile_header_component.rb
91
+ - app/components/keystone/ui/modal_component.rb
92
+ - app/components/keystone/ui/multi_select_component.rb
93
+ - app/components/keystone/ui/nav_dropdown_component.rb
94
+ - app/components/keystone/ui/nav_item_component.rb
95
+ - app/components/keystone/ui/navbar_component.rb
96
+ - app/components/keystone/ui/option_card_component.rb
97
+ - app/components/keystone/ui/page_component.rb
98
+ - app/components/keystone/ui/page_header_component.rb
99
+ - app/components/keystone/ui/panel_component.rb
100
+ - app/components/keystone/ui/pipeline_component.rb
101
+ - app/components/keystone/ui/progress_component.rb
102
+ - app/components/keystone/ui/radio_card_component.rb
103
+ - app/components/keystone/ui/section_component.rb
104
+ - app/components/keystone/ui/select_component.rb
105
+ - app/components/keystone/ui/settings_link_component.rb
106
+ - app/components/keystone/ui/show_page_component.rb
107
+ - app/components/keystone/ui/stat_card_component.rb
108
+ - app/components/keystone/ui/swipe_deck_component.rb
109
+ - app/components/keystone/ui/tab_switcher_component.rb
110
+ - app/components/keystone/ui/textarea_component.rb