baldur 0.2.5 → 0.3.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 (38) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +10 -0
  3. data/TODO.md +27 -7
  4. data/app/assets/javascripts/baldur/controllers/segmented_tabs_controller.js +53 -2
  5. data/app/assets/javascripts/baldur/controllers/theme_controller.js +4 -3
  6. data/app/assets/stylesheets/baldur/application/components/auth-page.css +5 -6
  7. data/app/assets/stylesheets/baldur/application/components/table.css +17 -7
  8. data/app/helpers/baldur/ui_helper.rb +11 -2
  9. data/app/helpers/baldur/ui_helper_feedback.rb +19 -2
  10. data/app/views/baldur/components/_button.html.erb +4 -0
  11. data/app/views/baldur/components/_segmented_buttons.html.erb +14 -7
  12. data/app/views/baldur/components/_snackbar_stack.html.erb +10 -6
  13. data/app/views/baldur/components/_table.html.erb +6 -4
  14. data/app/views/baldur/optional/_auth_page.html.erb +2 -2
  15. data/baldur.gemspec +4 -1
  16. data/context7.json +17 -0
  17. data/docs/alerts-and-snackbars.md +72 -0
  18. data/docs/auth.md +66 -0
  19. data/docs/forms.md +267 -0
  20. data/docs/installation.md +63 -0
  21. data/docs/marketing.md +77 -0
  22. data/docs/modals-and-panels.md +55 -0
  23. data/docs/security.md +11 -0
  24. data/docs/sidebar.md +105 -0
  25. data/docs/styling.md +34 -0
  26. data/docs/tables.md +173 -0
  27. data/docs/tabs-and-segmented-controls.md +509 -0
  28. data/docs/theme.md +118 -0
  29. data/lib/baldur/version.rb +1 -1
  30. data/llms-full.txt +179 -0
  31. data/llms.txt +35 -0
  32. data/test/hidden_field_helper_test.rb +23 -0
  33. data/test/segmented_buttons_helper_test.rb +85 -0
  34. data/test/snackbar_stack_helper_test.rb +121 -0
  35. data/test/table_helper_test.rb +118 -0
  36. data/test/text_field_helper_test.rb +40 -0
  37. data/test/theme_toggle_helper_test.rb +2 -0
  38. metadata +22 -2
data/docs/forms.md ADDED
@@ -0,0 +1,267 @@
1
+ # Forms
2
+
3
+ ## Text Field Basics
4
+
5
+ Use `ui_text_field_tag` for Baldur-styled single-line inputs and textareas.
6
+
7
+ ```erb
8
+ <%= ui_text_field_tag(
9
+ :company_name,
10
+ "Acme",
11
+ label: "Company name",
12
+ supporting_text: "Shown in billing and exports."
13
+ ) %>
14
+ ```
15
+
16
+ Use Baldur-owned named parameters for common concerns:
17
+
18
+ - `label:`
19
+ - `supporting_text:`
20
+ - `type:`
21
+ - `required:`
22
+ - `disabled:`
23
+ - `wrapper_class:`
24
+ - `input_class:`
25
+ - `multiline:`
26
+ - `prefix:`
27
+ - `suffix:`
28
+
29
+ ## HTML5 Input Attributes via `input_options`
30
+
31
+ For raw HTML input attributes, pass them through `input_options:`.
32
+
33
+ This is the recommended way to set HTML5 attributes such as:
34
+
35
+ - `step`
36
+ - `min`
37
+ - `max`
38
+ - `inputmode`
39
+ - `autocomplete`
40
+ - `pattern`
41
+ - `maxlength`
42
+ - `readonly`
43
+ - `data`
44
+ - `aria`
45
+ - custom `id`
46
+
47
+ Example:
48
+
49
+ ```erb
50
+ <%= ui_text_field_tag(
51
+ :price,
52
+ 12.5,
53
+ label: "Price",
54
+ type: :number,
55
+ input_options: {
56
+ step: :any,
57
+ min: 0,
58
+ inputmode: "decimal",
59
+ autocomplete: "off"
60
+ }
61
+ ) %>
62
+ ```
63
+
64
+ Another example with pattern and max length:
65
+
66
+ ```erb
67
+ <%= ui_text_field_tag(
68
+ :sku,
69
+ nil,
70
+ label: "SKU",
71
+ input_options: {
72
+ pattern: "[A-Z0-9-]+",
73
+ maxlength: 32,
74
+ autocomplete: "off"
75
+ }
76
+ ) %>
77
+ ```
78
+
79
+ `ui_text_field_tag` forwards `input_options` directly to the underlying `<input>` or `<textarea>` after Baldur applies its own base classes, generated `id`, and accessibility wiring.
80
+
81
+ ## Hidden State Fields
82
+
83
+ Use hidden fields to preserve UI state that should survive submits, validation rerenders, or Turbo refreshes.
84
+
85
+ Common examples:
86
+
87
+ - active tab
88
+ - selected filter
89
+ - stepped form state
90
+ - selected panel
91
+
92
+ For Baldur-flavored app code, prefer `ui_hidden_field_tag`:
93
+
94
+ ```erb
95
+ <%= ui_hidden_field_tag :planner_tab, "targets" %>
96
+ ```
97
+
98
+ It is a thin wrapper around Rails `hidden_field_tag`, so both are valid.
99
+
100
+ ### When to Use `ui_hidden_field_tag`
101
+
102
+ Prefer `ui_hidden_field_tag` when:
103
+
104
+ - hidden state is part of a documented Baldur interaction pattern
105
+ - you want host app helper usage to stay consistently `ui_*`
106
+ - the hidden field sits beside other Baldur controls such as segmented buttons, menu selects, or submit flows
107
+
108
+ ### When Plain `hidden_field_tag` Is Acceptable
109
+
110
+ Plain Rails `hidden_field_tag` is still acceptable when:
111
+
112
+ - the field is simple Rails glue and Baldur adds no extra meaning
113
+ - you are already inside host-specific form builder or low-level Rails form code
114
+ - the hidden field is not part of a reusable Baldur interaction pattern
115
+
116
+ Rule of thumb: if the hidden field is helping a Baldur control preserve UI state, reach for `ui_hidden_field_tag`. If it is generic Rails plumbing, plain `hidden_field_tag` is fine.
117
+
118
+ ### URL Params vs Hidden State
119
+
120
+ Prefer URL params for state that should be shareable, bookmarkable, or navigable:
121
+
122
+ - GET-driven tabs
123
+ - filters in index screens
124
+ - state that should survive refresh and copy-pasted URLs
125
+
126
+ Prefer hidden fields for state that primarily belongs to an in-form workflow:
127
+
128
+ - current step in a stepped form
129
+ - active tab inside a POST form
130
+ - selected panel within a multi-surface editor
131
+
132
+ Avoid storing the same state in both params and hidden inputs unless the flow truly needs both.
133
+
134
+ ## Hidden State Cookbook
135
+
136
+ ### Active Tab
137
+
138
+ Inside a form, store the selected tab so rerenders return the user to the same panel:
139
+
140
+ ```erb
141
+ <% current_tab = params[:planner_tab].presence || "global" %>
142
+
143
+ <%= form_with url: planner_path, method: :post do %>
144
+ <%= ui_hidden_field_tag :planner_tab,
145
+ current_tab,
146
+ data: { planner_tabs_target: "hiddenInput" } %>
147
+
148
+ <%= ui_segmented_buttons(
149
+ aria_label: "Planner tabs",
150
+ items: [
151
+ {
152
+ label: "Global",
153
+ value: "global",
154
+ selected: current_tab == "global",
155
+ data: {
156
+ action: "click->planner-tabs#switch",
157
+ planner_tabs_target: "tab",
158
+ planner_tab_value: "global"
159
+ }
160
+ },
161
+ {
162
+ label: "Targets",
163
+ value: "targets",
164
+ selected: current_tab == "targets",
165
+ data: {
166
+ action: "click->planner-tabs#switch",
167
+ planner_tabs_target: "tab",
168
+ planner_tab_value: "targets"
169
+ }
170
+ }
171
+ ]
172
+ ) %>
173
+ <% end %>
174
+ ```
175
+
176
+ See `docs/tabs-and-segmented-controls.md` for the full tabs cookbook.
177
+
178
+ ### Filters
179
+
180
+ For POST-backed filter forms or mixed interactive controls, hidden state can hold the selected filter key:
181
+
182
+ ```erb
183
+ <%= form_with url: reports_path, method: :post do %>
184
+ <%= ui_hidden_field_tag :status_filter,
185
+ params[:status_filter].presence || "active",
186
+ data: { report_filters_target: "hiddenInput" } %>
187
+
188
+ <%= ui_segmented_buttons(
189
+ aria_label: "Status filters",
190
+ items: [
191
+ {
192
+ label: "Active",
193
+ value: "active",
194
+ selected: params[:status_filter] != "archived",
195
+ data: {
196
+ action: "click->report-filters#select",
197
+ report_filter_value: "active"
198
+ }
199
+ },
200
+ {
201
+ label: "Archived",
202
+ value: "archived",
203
+ selected: params[:status_filter] == "archived",
204
+ data: {
205
+ action: "click->report-filters#select",
206
+ report_filter_value: "archived"
207
+ }
208
+ }
209
+ ]
210
+ ) %>
211
+ <% end %>
212
+ ```
213
+
214
+ If the filter should be shareable in the URL, prefer GET params instead of hidden POST state.
215
+
216
+ ### Stepped Form State
217
+
218
+ For multi-step forms, hidden state can track current step across validation failures and submit cycles:
219
+
220
+ ```erb
221
+ <% current_step = params[:step].presence || "details" %>
222
+
223
+ <%= form_with url: onboarding_path, method: :post do %>
224
+ <%= ui_hidden_field_tag :step,
225
+ current_step,
226
+ data: { onboarding_target: "currentStep" } %>
227
+
228
+ <% if current_step == "details" %>
229
+ ...details fields...
230
+ <%= ui_button(label: "Continue", type: :submit, data: { action: "click->onboarding#setStep", onboarding_step_value: "billing" }) %>
231
+ <% elsif current_step == "billing" %>
232
+ ...billing fields...
233
+ <%= ui_button(label: "Review", type: :submit, data: { action: "click->onboarding#setStep", onboarding_step_value: "review" }) %>
234
+ <% end %>
235
+ <% end %>
236
+ ```
237
+
238
+ Server rerender should read `params[:step]` and send the same value back so the correct step remains visible.
239
+
240
+ ### Selected Panel
241
+
242
+ When a page has multiple host-owned panels, hidden state can preserve which panel was open when the form submitted:
243
+
244
+ ```erb
245
+ <%= form_with url: workspace_path, method: :post do %>
246
+ <%= ui_hidden_field_tag :selected_panel,
247
+ params[:selected_panel].presence || "summary",
248
+ data: { workspace_panels_target: "hiddenInput" } %>
249
+
250
+ <%= ui_button(label: "Show Details", type: :button, data: { action: "click->workspace-panels#select", workspace_panel_value: "details" }) %>
251
+ <%= ui_button(label: "Show Summary", type: :button, data: { action: "click->workspace-panels#select", workspace_panel_value: "summary" }) %>
252
+ <% end %>
253
+ ```
254
+
255
+ This is useful when visibility is host-owned but the selected panel should survive submit-driven rerenders.
256
+
257
+ ## When to Use Raw Rails Helpers
258
+
259
+ Prefer raw Rails helpers when Baldur is not adding meaningful value to the field shape.
260
+
261
+ Good examples:
262
+
263
+ - simple hidden fields with no Baldur interaction coupling
264
+ - low-level form builder integrations
265
+ - one-off HTML attributes that already fit naturally inside `input_options:`
266
+
267
+ Prefer Baldur helpers when you want shared label, support text, styling, component structure, or a documented interaction pattern.
@@ -0,0 +1,63 @@
1
+ # Installation
2
+
3
+ ## Add the Gem
4
+
5
+ Add to the host `Gemfile`:
6
+
7
+ ```ruby
8
+ gem "baldur", ">= 0.1.3"
9
+ ```
10
+
11
+ Baldur declares `tailwindcss-rails >= 4.3.0` as a dependency, so hosts do not need to add it separately unless they want to enforce their own minimum version.
12
+
13
+ ## Install and Generate
14
+
15
+ ```sh
16
+ bundle install
17
+ bundle exec rails tailwindcss:engines
18
+ bundle exec rails generate baldur:install
19
+ ```
20
+
21
+ `tailwindcss:engines` creates `app/assets/builds/tailwind/baldur.css` from the engine-owned Tailwind entrypoint.
22
+
23
+ `baldur:install` imports that generated build into the host Tailwind entrypoint.
24
+
25
+ If the host already runs `tailwindcss:build` or `tailwindcss:watch`, those commands will also create the engine build once the entrypoint exists.
26
+
27
+ ## Rebuild Tailwind
28
+
29
+ ```sh
30
+ bundle exec rails tailwindcss:build
31
+ ```
32
+
33
+ ## Optional Surfaces
34
+
35
+ Install only what you need:
36
+
37
+ ```sh
38
+ bundle exec rails generate baldur:install_panel_secondary
39
+ bundle exec rails generate baldur:install_google_auth
40
+ ```
41
+
42
+ `baldur:install` already includes `Baldur::Optional::AuthPageHelper` in the generated `UiHelper`, so `ui_auth_page` is available by default after base install. The generators above are only required when using those specific surfaces.
43
+
44
+ ## Installer Assumptions
45
+
46
+ - Tailwind entrypoint exists at `app/assets/tailwind/application.css`
47
+ - Host app gets `tailwindcss-rails` through Baldur or its own Gemfile and uses engine builds
48
+ - Host app uses importmap Stimulus boot with `app/javascript/controllers`
49
+ - Host app gets `app/assets/stylesheets/fonts.css` for font loading and `app/assets/stylesheets/theme.css` for brand and font-token overrides
50
+ - Host app can import `app/assets/builds/tailwind/baldur.css` from `app/assets/tailwind/application.css`
51
+
52
+ Default install behavior keeps Geist loaded through the host `fonts.css` scaffold. If the host wants a different stack, update `fonts.css` and then map the loaded families in `theme.css`.
53
+
54
+ ## Smoke Check
55
+
56
+ After installation, run from the host app root:
57
+
58
+ ```sh
59
+ bundle exec rails tailwindcss:build
60
+ bundle exec ruby "$(bundle show baldur)/script/verify_host_install"
61
+ ```
62
+
63
+ That verifies the host can render core helpers, confirms the Tailwind entrypoint contains the required Baldur imports, and checks that Baldur template utility classes (e.g. `px-6`, `py-4`, `uppercase`, `tracking-wide`) are present in the compiled CSS. If utility classes are missing, the engine `@source` paths in `engine.css` may be misconfigured — ensure they resolve correctly from `app/assets/tailwind/baldur/` to `app/helpers` and `app/views`.
data/docs/marketing.md ADDED
@@ -0,0 +1,77 @@
1
+ # Marketing
2
+
3
+ ## When to Use
4
+
5
+ Use marketing helpers for landing pages, pricing, and feature surfaces. Keep marketing-page templates separate from app UI primitives. Low-level atoms live under `ui_*`, while full-page marketing surfaces use dedicated `ui_marketing_*` helpers.
6
+
7
+ ## Helpers
8
+
9
+ - `ui_marketing_top_nav`
10
+ - `ui_marketing_hero_section(variant: :solar_system, ...)`
11
+ - `ui_marketing_features_section`
12
+ - `ui_marketing_testimonials_section(variant: :bento, ...)`
13
+ - `ui_marketing_faq_section`
14
+ - `ui_marketing_cta_banner`
15
+ - `ui_marketing_pricing_tables`
16
+ - `ui_marketing_footer`
17
+
18
+ Use the existing landing and pricing templates as canonical v1 variants. Future hero or testimonial layouts should be added as new variants, not folded into the existing default markup.
19
+
20
+ ## Branding
21
+
22
+ Marketing nav/footer branding comes from `config.marketing_brand` in the Baldur initializer. Hosts can override that deployment-level default per render by passing `brand:` into `ui_marketing_top_nav` or `ui_marketing_footer`.
23
+
24
+ If a host needs tenant-specific or whitelabel branding, resolve/cache that in the app and pass the resolved values through `brand:` rather than teaching Baldur about tenant lookup.
25
+
26
+ `config.marketing_brand` supports `name`, `wordmark`, `logo_src`, `logo_alt`, and optional `href`. Hosts should treat that config as the canonical branding contract instead of relying on helper-method coupling.
27
+
28
+ ## Controllers
29
+
30
+ Interactive marketing templates ship with Baldur-owned Stimulus controllers. `baldur:install` generates `marketing_tabs_controller.js` and `marketing_pricing_controller.js` shims so features tabs and pricing billing toggles do not depend on host-specific controller names.
31
+
32
+ ## Examples
33
+
34
+ Hero:
35
+
36
+ ```erb
37
+ <%= ui_marketing_hero_section(
38
+ variant: :solar_system,
39
+ headline: "Turn Every Data Point Into ROI",
40
+ body: "Connect fragmented data into one decision engine.",
41
+ primary_action: { label: "Book a Demo", variant: :primary, href: dashboard_path },
42
+ secondary_action: { label: "See Use Cases", variant: :outline, href: "#use-cases" },
43
+ supporting_action: { href: "#", label: "Watch walkthrough", data: { open_modal: "#walkthrough-modal" } },
44
+ callouts: [
45
+ { label: "Unified decision context" },
46
+ { label: "Prioritized recommendations" },
47
+ { label: "Impact-aware next actions" }
48
+ ],
49
+ orbit_sources: [
50
+ { name: "Shopify", asset_path: "/landing/source-logos/shopify.svg" },
51
+ { name: "HubSpot", asset_path: "/landing/source-logos/hubspot.svg" }
52
+ ],
53
+ centerpiece_image: { src: "/branding/logo.png", alt: "Acme logo" }
54
+ ) %>
55
+ ```
56
+
57
+ Features:
58
+
59
+ ```erb
60
+ <%= ui_marketing_features_section(
61
+ title: "What Mimir unlocks for your teams",
62
+ description: "Tailored to your business model and decision priorities.",
63
+ tabs: [
64
+ {
65
+ value: "ecommerce",
66
+ label: "E-commerce",
67
+ selected: true,
68
+ panel_title: "E-commerce",
69
+ panel_body: "Priority ROI plays for commerce teams.",
70
+ cards: [
71
+ { title: "Which products should I run campaigns for?", body: "Rank SKUs by incremental margin potential." }
72
+ ]
73
+ }
74
+ ],
75
+ cta: { label: "Get a demo tailored for you", variant: :primary, href: dashboard_path }
76
+ ) %>
77
+ ```
@@ -0,0 +1,55 @@
1
+ # Modals and Panels
2
+
3
+ ## Modals
4
+
5
+ Prefer `ui_modal` directly:
6
+
7
+ ```erb
8
+ <%= ui_modal(id: "confirm-delete", title: "Delete item") do %>
9
+ <p>This action cannot be undone.</p>
10
+ <% end %>
11
+ ```
12
+
13
+ If a host app keeps a shared wrapper partial around `ui_modal`, treat `modal_body:` as the wrapper-local input and let the wrapper pass that content into `ui_modal`. Avoid calling a wrapper with `body:` through `render`, since `body` collides with Rails render options.
14
+
15
+ ## Panels
16
+
17
+ `ui_panel_secondary` is part of the optional panel surface. Install it if needed:
18
+
19
+ ```sh
20
+ bundle exec rails generate baldur:install_panel_secondary
21
+ ```
22
+
23
+ Example usage:
24
+
25
+ ```erb
26
+ <%= ui_panel_secondary(id: "assistant", title: "Assistant", trigger_label: "Open") do %>
27
+ <p>Panel content</p>
28
+ <% end %>
29
+ ```
30
+
31
+ External triggers can open a Baldur panel declaratively:
32
+
33
+ ```erb
34
+ <button
35
+ type="button"
36
+ data-open-panel="#assistant"
37
+ data-panel-payload="<%= json_escape({ source: "dashboard" }.to_json) %>">
38
+ Open assistant
39
+ </button>
40
+ ```
41
+
42
+ `panel-secondary` emits `baldur:panel:opened` and `baldur:panel:closed` on the panel shell and `window`. The event detail includes `id`, `selector`, `trigger`, and parsed `payload`.
43
+
44
+ ## Action Rows
45
+
46
+ For horizontal primary/secondary CTA groups, prefer `ui_action_row`:
47
+
48
+ ```erb
49
+ <%= ui_action_row(
50
+ secondary_button: { label: "Back", variant: :outline, href: settings_path },
51
+ primary_button: { label: "Save", variant: :primary, type: :submit }
52
+ ) %>
53
+ ```
54
+
55
+ The action row owns the responsive layout and keeps the primary CTA last on the right.
data/docs/security.md ADDED
@@ -0,0 +1,11 @@
1
+ # Security
2
+
3
+ - New Baldur releases require MFA for RubyGems owners via gem metadata starting with `0.1.2`.
4
+ - Release artifacts should be installed from RubyGems or GitHub releases and can be verified with the published `.sha512` checksum file.
5
+ - Report vulnerabilities privately through GitHub Security Advisories.
6
+
7
+ ## Verify a Release Artifact
8
+
9
+ ```sh
10
+ sha512sum -c baldur-0.1.3.gem.sha512
11
+ ```
data/docs/sidebar.md ADDED
@@ -0,0 +1,105 @@
1
+ # Sidebar
2
+
3
+ ## When to Use
4
+
5
+ Use `ui_sidebar` for authenticated application shells. It is part of the base install and includes:
6
+
7
+ - desktop and mobile sidebar shell markup
8
+ - `sidebar` and `mobile-sidebar` controller wiring
9
+ - default brand rendering from `config.marketing_brand`
10
+ - default nav rendering for primary and secondary links
11
+
12
+ Host apps provide the route arrays, active-state logic, and any optional slot content.
13
+
14
+ ## Quick Example
15
+
16
+ ```erb
17
+ <%= ui_sidebar(
18
+ brand_path: root_path,
19
+ primary_links: [
20
+ { name: "Dashboard", path: root_path, icon: "layout-dashboard", active: current_page?(root_path) }
21
+ ]
22
+ ) do |_sidebar| %>
23
+ <main class="flex-1 p-6">
24
+ <h1>Dashboard</h1>
25
+ </main>
26
+ <% end %>
27
+ ```
28
+
29
+ ## Link Options
30
+
31
+ Each link accepts:
32
+
33
+ - `name` required label text
34
+ - `path` required destination
35
+ - `icon` optional Lucide icon name, defaults to `circle`
36
+ - `active` optional boolean for `aria-current="page"`
37
+ - `title` optional hover title
38
+ - `method` optional Rails link method
39
+ - `data` optional data attributes hash
40
+ - `html_options` optional extra HTML attributes hash
41
+
42
+ ## Brand Behavior
43
+
44
+ - `brand_name`, `brand_wordmark`, and `brand_logo` are optional
45
+ - when omitted, Baldur resolves them from `config.marketing_brand`
46
+ - `brand_path` defaults to `#` if the host app does not provide one
47
+
48
+ ## Mobile Behavior
49
+
50
+ - mobile nav mirrors desktop primary and secondary links automatically
51
+ - the install generator ships both `sidebar_controller.js` and `mobile_sidebar_controller.js`
52
+
53
+ ## Slots vs Params
54
+
55
+ You can adopt `ui_sidebar` with no slots. The block content becomes the main app surface beside the sidebar:
56
+
57
+ ```erb
58
+ <%= ui_sidebar(
59
+ brand_path: root_path,
60
+ primary_links: sidebar_primary_links,
61
+ secondary_links: sidebar_secondary_links,
62
+ secondary_label: "Admin"
63
+ ) do |_sidebar| %>
64
+ <main id="main-content" class="flex-1 p-6">
65
+ <%= yield %>
66
+ </main>
67
+ <% end %>
68
+ ```
69
+
70
+ For host-specific sidebar surfaces, prefer slots. `*_content` params are also supported for incremental adoption.
71
+
72
+ ```erb
73
+ <%= ui_sidebar(
74
+ brand_path: root_path,
75
+ primary_links: sidebar_primary_links,
76
+ secondary_links: sidebar_secondary_links,
77
+ secondary_label: "Admin"
78
+ ) do |sidebar| %>
79
+ <% sidebar.with_header do %>
80
+ <%= render "layouts/tenant_switcher" %>
81
+ <% end %>
82
+
83
+ <% sidebar.with_footer do %>
84
+ <div class="space-y-2">
85
+ <p class="text-sm text-muted"><%= current_user.email %></p>
86
+ <%= ui_button(label: "Sign out", href: logout_path, method: :delete, variant: :text, size: :sm, icon: "log-out") %>
87
+ </div>
88
+ <% end %>
89
+ <% end %>
90
+ ```
91
+
92
+ ## Ownership
93
+
94
+ Host apps own:
95
+
96
+ - route definitions
97
+ - active-state logic
98
+ - app-specific header/footer content
99
+
100
+ Baldur owns:
101
+
102
+ - desktop and mobile sidebar shell markup
103
+ - toggle behavior wiring
104
+ - default brand rendering
105
+ - default nav rendering
data/docs/styling.md ADDED
@@ -0,0 +1,34 @@
1
+ # Styling
2
+
3
+ ## Ownership
4
+
5
+ Tailwind provides the utility/base layer. Baldur is the source of truth for shared design-system primitives.
6
+
7
+ Host app responsibilities:
8
+
9
+ - Load `fonts.css` before Tailwind to control font families.
10
+ - Import the generated Baldur Tailwind engine build into the host Tailwind entrypoint.
11
+ - Override only base palette inputs and font-token mapping in `theme.css` after the Baldur build.
12
+ - Add host-app styles only for app-specific surfaces after Baldur.
13
+
14
+ Host app restrictions:
15
+
16
+ - Do not re-import or override host-local copies of Baldur-owned primitives such as buttons, forms, snackbars, or tables.
17
+ - Do not keep duplicate copies of Baldur-owned primitives under `app/assets/stylesheets/application/`; leave only app-specific files there.
18
+ - Do not keep host copies of Baldur semantic theme files such as `theme/light.css` or `theme/dark.css`.
19
+ - Keep shared elevation semantics in Baldur-owned `--elev-*` tokens. If a host needs softer or stronger shared shadows, change the Baldur token source instead of swapping raw Tailwind shadow utilities into Baldur-owned primitives.
20
+
21
+ ## Brand tokens
22
+
23
+ Baldur derives all semantic colors from four brand input tokens. Override only these in your host `theme.css` — see `docs/theme.md` for the full token reference and customization guide.
24
+
25
+ In short:
26
+
27
+ | Token | Purpose |
28
+ |-------|---------|
29
+ | `--_primary-base` | Primary actions, CTA |
30
+ | `--_secondary-base` | Sidebar, nav, subdued |
31
+ | `--_accent-base` | Highlights, badges |
32
+ | `--_neutral-base` | Surfaces, text, borders |
33
+
34
+ Do not override semantic `--color-*` tokens (e.g. `--color-primary`, `--color-surface`). Baldur maps brand inputs to semantic outputs automatically for both light and dark modes.