baldur 0.1.1
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 +7 -0
- data/Gemfile +3 -0
- data/LICENSE +21 -0
- data/README.md +318 -0
- data/TODO.md +6 -0
- data/app/assets/javascripts/baldur/controllers/accordion_controller.js +148 -0
- data/app/assets/javascripts/baldur/controllers/alert_controller.js +209 -0
- data/app/assets/javascripts/baldur/controllers/date_field_controller.js +558 -0
- data/app/assets/javascripts/baldur/controllers/details_menu_controller.js +30 -0
- data/app/assets/javascripts/baldur/controllers/form_submit_controller.js +7 -0
- data/app/assets/javascripts/baldur/controllers/marketing_pricing_controller.js +47 -0
- data/app/assets/javascripts/baldur/controllers/marketing_tabs_controller.js +118 -0
- data/app/assets/javascripts/baldur/controllers/menu_select_controller.js +401 -0
- data/app/assets/javascripts/baldur/controllers/mobile_sidebar_controller.js +13 -0
- data/app/assets/javascripts/baldur/controllers/modal_controller.js +149 -0
- data/app/assets/javascripts/baldur/controllers/panel_right_controller.js +1 -0
- data/app/assets/javascripts/baldur/controllers/panel_secondary_controller.js +129 -0
- data/app/assets/javascripts/baldur/controllers/segmented_tabs_controller.js +38 -0
- data/app/assets/javascripts/baldur/controllers/sidebar_controller.js +77 -0
- data/app/assets/javascripts/baldur/controllers/smooth_scroll_controller.js +29 -0
- data/app/assets/javascripts/baldur/controllers/snackbar_controller.js +158 -0
- data/app/assets/javascripts/baldur/controllers/table_disclosure_controller.js +46 -0
- data/app/assets/javascripts/baldur/controllers/theme_controller.js +90 -0
- data/app/assets/javascripts/baldur/controllers/tooltip_controller.js +136 -0
- data/app/assets/javascripts/baldur/lib/animation-helpers.js +56 -0
- data/app/assets/javascripts/baldur/lib/dom-helpers.js +80 -0
- data/app/assets/javascripts/baldur/lib/field-validation-helpers.js +36 -0
- data/app/assets/javascripts/baldur/lib/focus-management.js +89 -0
- data/app/assets/javascripts/baldur/lib/formatting-helpers.js +100 -0
- data/app/assets/javascripts/baldur/lib/lucide.js +20 -0
- data/app/assets/javascripts/baldur/lib/snackbar.js +50 -0
- data/app/assets/javascripts/baldur/lib/storage-helpers.js +50 -0
- data/app/assets/stylesheets/baldur/application/components/alert.css +226 -0
- data/app/assets/stylesheets/baldur/application/components/app_bar.css +41 -0
- data/app/assets/stylesheets/baldur/application/components/button.css +173 -0
- data/app/assets/stylesheets/baldur/application/components/card.css +63 -0
- data/app/assets/stylesheets/baldur/application/components/chart.css +40 -0
- data/app/assets/stylesheets/baldur/application/components/chip.css +51 -0
- data/app/assets/stylesheets/baldur/application/components/dialog.css +81 -0
- data/app/assets/stylesheets/baldur/application/components/forms.css +624 -0
- data/app/assets/stylesheets/baldur/application/components/layout.css +2 -0
- data/app/assets/stylesheets/baldur/application/components/list.css +15 -0
- data/app/assets/stylesheets/baldur/application/components/menu.css +300 -0
- data/app/assets/stylesheets/baldur/application/components/panel-right.css +1 -0
- data/app/assets/stylesheets/baldur/application/components/panel-secondary.css +71 -0
- data/app/assets/stylesheets/baldur/application/components/progress.css +84 -0
- data/app/assets/stylesheets/baldur/application/components/segmented-buttons.css +117 -0
- data/app/assets/stylesheets/baldur/application/components/settings-nav.css +84 -0
- data/app/assets/stylesheets/baldur/application/components/sidebar.css +123 -0
- data/app/assets/stylesheets/baldur/application/components/snackbar.css +179 -0
- data/app/assets/stylesheets/baldur/application/components/stepper.css +124 -0
- data/app/assets/stylesheets/baldur/application/components/switch.css +105 -0
- data/app/assets/stylesheets/baldur/application/components/table.css +331 -0
- data/app/assets/stylesheets/baldur/application/components/timeline.css +184 -0
- data/app/assets/stylesheets/baldur/application/components/utilities.css +180 -0
- data/app/assets/stylesheets/baldur/application/global.css +125 -0
- data/app/assets/stylesheets/baldur/application/marketing/layout.css +36 -0
- data/app/assets/stylesheets/baldur/application/motion.css +125 -0
- data/app/assets/stylesheets/baldur/application/theme.css +329 -0
- data/app/assets/stylesheets/baldur/theme/dark.css +90 -0
- data/app/assets/stylesheets/baldur/theme/light.css +82 -0
- data/app/assets/stylesheets/baldur.css +27 -0
- data/app/assets/stylesheets/baldur_panel_right.css +1 -0
- data/app/assets/stylesheets/baldur_panel_secondary.css +1 -0
- data/app/assets/tailwind/baldur/engine.css +5 -0
- data/app/helpers/baldur/compatibility/ui_aliases.rb +7 -0
- data/app/helpers/baldur/marketing_helper.rb +121 -0
- data/app/helpers/baldur/optional/auth_page_helper.rb +17 -0
- data/app/helpers/baldur/optional/google_auth_helper.rb +16 -0
- data/app/helpers/baldur/optional/panel_right_helper.rb +7 -0
- data/app/helpers/baldur/optional/panel_secondary_helper.rb +26 -0
- data/app/helpers/baldur/render_helper.rb +13 -0
- data/app/helpers/baldur/ui_helper.rb +217 -0
- data/app/helpers/baldur/ui_helper_feedback.rb +93 -0
- data/app/helpers/baldur/ui_helper_forms.rb +230 -0
- data/app/helpers/baldur/ui_helper_unavailable.rb +98 -0
- data/app/views/baldur/components/_accordion.html.erb +30 -0
- data/app/views/baldur/components/_action_row.html.erb +6 -0
- data/app/views/baldur/components/_alert.html.erb +61 -0
- data/app/views/baldur/components/_badge.html.erb +25 -0
- data/app/views/baldur/components/_button.html.erb +81 -0
- data/app/views/baldur/components/_card.html.erb +40 -0
- data/app/views/baldur/components/_chart_card.html.erb +42 -0
- data/app/views/baldur/components/_checkbox.html.erb +27 -0
- data/app/views/baldur/components/_date_field.html.erb +43 -0
- data/app/views/baldur/components/_google_sign_in_button.html.erb +1 -0
- data/app/views/baldur/components/_kebab_menu.html.erb +36 -0
- data/app/views/baldur/components/_kpi.html.erb +45 -0
- data/app/views/baldur/components/_menu_select.html.erb +78 -0
- data/app/views/baldur/components/_modal.html.erb +54 -0
- data/app/views/baldur/components/_pagination.html.erb +61 -0
- data/app/views/baldur/components/_segmented_buttons.html.erb +51 -0
- data/app/views/baldur/components/_settings_nav.html.erb +41 -0
- data/app/views/baldur/components/_snackbar.html.erb +42 -0
- data/app/views/baldur/components/_snackbar_stack.html.erb +13 -0
- data/app/views/baldur/components/_stepper.html.erb +39 -0
- data/app/views/baldur/components/_table.html.erb +117 -0
- data/app/views/baldur/components/_table_card.html.erb +86 -0
- data/app/views/baldur/components/_table_footer.html.erb +68 -0
- data/app/views/baldur/components/_text_field.html.erb +33 -0
- data/app/views/baldur/components/_tooltip.html.erb +73 -0
- data/app/views/baldur/marketing/_cta_banner.html.erb +20 -0
- data/app/views/baldur/marketing/_faq_section.html.erb +37 -0
- data/app/views/baldur/marketing/_features_section.html.erb +67 -0
- data/app/views/baldur/marketing/_footer.html.erb +38 -0
- data/app/views/baldur/marketing/_hero_section.html.erb +259 -0
- data/app/views/baldur/marketing/_pricing_tables.html.erb +99 -0
- data/app/views/baldur/marketing/_testimonials_section.html.erb +80 -0
- data/app/views/baldur/marketing/_top_nav.html.erb +28 -0
- data/app/views/baldur/optional/_auth_page.html.erb +21 -0
- data/app/views/baldur/optional/_google_sign_in_button.html.erb +19 -0
- data/app/views/baldur/optional/_panel_right.html.erb +1 -0
- data/app/views/baldur/optional/_panel_secondary.html.erb +34 -0
- data/baldur.gemspec +30 -0
- data/config/importmap.rb +2 -0
- data/lib/baldur/configuration.rb +24 -0
- data/lib/baldur/engine.rb +10 -0
- data/lib/baldur/version.rb +3 -0
- data/lib/baldur.rb +17 -0
- data/lib/generators/baldur/install/install_generator.rb +113 -0
- data/lib/generators/baldur/install/templates/baldur_initializer.rb +19 -0
- data/lib/generators/baldur/install/templates/fonts.css +14 -0
- data/lib/generators/baldur/install/templates/theme.css +27 -0
- data/lib/generators/baldur/install/templates/ui_helper.rb +4 -0
- data/lib/generators/baldur/install_google_auth/install_google_auth_generator.rb +15 -0
- data/lib/generators/baldur/install_panel_right/install_panel_right_generator.rb +9 -0
- data/lib/generators/baldur/install_panel_secondary/install_panel_secondary_generator.rb +21 -0
- data/script/verify_host_install +111 -0
- data/test/gemspec_test.rb +11 -0
- data/test/install_generator_test.rb +35 -0
- data/test/install_panel_secondary_generator_test.rb +21 -0
- data/test/marketing_helper_test.rb +38 -0
- data/test/run_all.rb +3 -0
- data/test/test_helper.rb +9 -0
- data/test/tmp/install_generator/app/assets/stylesheets/fonts.css +14 -0
- data/test/tmp/install_generator/app/assets/stylesheets/theme.css +27 -0
- data/test/tmp/install_generator/app/assets/tailwind/application.css +4 -0
- data/test/tmp/install_generator/app/helpers/ui_helper.rb +4 -0
- data/test/tmp/install_generator/app/javascript/controllers/accordion_controller.js +1 -0
- data/test/tmp/install_generator/app/javascript/controllers/date_field_controller.js +1 -0
- data/test/tmp/install_generator/app/javascript/controllers/details_menu_controller.js +1 -0
- data/test/tmp/install_generator/app/javascript/controllers/form_submit_controller.js +1 -0
- data/test/tmp/install_generator/app/javascript/controllers/marketing_pricing_controller.js +1 -0
- data/test/tmp/install_generator/app/javascript/controllers/marketing_tabs_controller.js +1 -0
- data/test/tmp/install_generator/app/javascript/controllers/menu_select_controller.js +1 -0
- data/test/tmp/install_generator/app/javascript/controllers/modal_controller.js +1 -0
- data/test/tmp/install_generator/app/javascript/controllers/segmented_tabs_controller.js +1 -0
- data/test/tmp/install_generator/app/javascript/controllers/sidebar_controller.js +1 -0
- data/test/tmp/install_generator/app/javascript/controllers/smooth_scroll_controller.js +1 -0
- data/test/tmp/install_generator/app/javascript/controllers/snackbar_controller.js +1 -0
- data/test/tmp/install_generator/app/javascript/controllers/theme_controller.js +1 -0
- data/test/tmp/install_generator/app/javascript/controllers/tooltip_controller.js +1 -0
- data/test/tmp/install_generator/app/javascript/lib/animation-helpers.js +1 -0
- data/test/tmp/install_generator/app/javascript/lib/dom-helpers.js +1 -0
- data/test/tmp/install_generator/app/javascript/lib/field-validation-helpers.js +1 -0
- data/test/tmp/install_generator/app/javascript/lib/focus-management.js +1 -0
- data/test/tmp/install_generator/app/javascript/lib/formatting-helpers.js +1 -0
- data/test/tmp/install_generator/app/javascript/lib/snackbar.js +1 -0
- data/test/tmp/install_generator/app/javascript/lib/storage-helpers.js +1 -0
- data/test/tmp/install_generator/config/initializers/baldur.rb +19 -0
- data/test/tmp/install_panel_secondary_generator/app/assets/tailwind/application.css +2 -0
- data/test/tmp/install_panel_secondary_generator/app/helpers/panel_secondary_helper.rb +3 -0
- data/test/tmp/install_panel_secondary_generator/app/javascript/controllers/panel_secondary_controller.js +1 -0
- metadata +259 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: c530dc1592f68c70aea978df00320e2b5c60c3bdf1438fed868858b13808e19c
|
|
4
|
+
data.tar.gz: '0827b0d90a5520b0af7fcc92fc4d6cae262274d54a7bfe6c8ba92c8beacc1d45'
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: aeeda062066859904828caecb85e2dc4599274138b6d2aeaa3172a9edfd358c673579b1e32b4864e19422b306811e7f774d7e0c2208c6817d8f94438b2998bb0
|
|
7
|
+
data.tar.gz: 21f0c38a4047ab20ae7bd75c2001f8a4e1f013f27ddb930852c915cf39627017e318399cd204038b77e538686c05bce2323fdbd82652cda316056c5b06400bc3
|
data/Gemfile
ADDED
data/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Varun Murkar
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
data/README.md
ADDED
|
@@ -0,0 +1,318 @@
|
|
|
1
|
+
# Baldur
|
|
2
|
+
|
|
3
|
+
Baldur is a reusable Rails UI engine for apps using the same frontend stack as this repository:
|
|
4
|
+
|
|
5
|
+
- Rails 8
|
|
6
|
+
- Propshaft
|
|
7
|
+
- `importmap-rails`
|
|
8
|
+
- `stimulus-rails`
|
|
9
|
+
- `tailwindcss-rails`
|
|
10
|
+
|
|
11
|
+
## Install Into Another App
|
|
12
|
+
|
|
13
|
+
1. Add Baldur to the target app `Gemfile`:
|
|
14
|
+
|
|
15
|
+
Baldur now declares `tailwindcss-rails` as a gem dependency, so hosts do not need to add that gem separately unless they want to pin a specific version.
|
|
16
|
+
|
|
17
|
+
```ruby
|
|
18
|
+
gem "baldur"
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
2. Run:
|
|
22
|
+
|
|
23
|
+
```sh
|
|
24
|
+
bundle install
|
|
25
|
+
bundle exec rails tailwindcss:engines
|
|
26
|
+
bundle exec rails generate baldur:install
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
3. Rebuild Tailwind:
|
|
30
|
+
|
|
31
|
+
```sh
|
|
32
|
+
bundle exec rails tailwindcss:build
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
`tailwindcss:engines` creates `app/assets/builds/tailwind/baldur.css` from the engine-owned Tailwind entrypoint. `baldur:install` then imports that generated build into the host Tailwind entrypoint.
|
|
36
|
+
|
|
37
|
+
If the host app already runs `tailwindcss:build` or `tailwindcss:watch`, those commands will also create the engine build automatically once the engine entrypoint exists.
|
|
38
|
+
|
|
39
|
+
4. Install optional surfaces as needed:
|
|
40
|
+
|
|
41
|
+
```sh
|
|
42
|
+
bundle exec rails generate baldur:install_panel_secondary
|
|
43
|
+
bundle exec rails generate baldur:install_google_auth
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
Default install behavior keeps Geist loaded through the host `fonts.css` scaffold. If a host app wants a different stack, it should update `fonts.css` and then map the loaded families in `theme.css`.
|
|
47
|
+
|
|
48
|
+
## Styling Ownership
|
|
49
|
+
|
|
50
|
+
Tailwind provides the utility/base layer. Baldur is the source of truth for shared design-system primitives.
|
|
51
|
+
|
|
52
|
+
- Import host `fonts.css` before Tailwind so the host controls what font families get loaded.
|
|
53
|
+
- Import the generated Baldur Tailwind engine build into the host Tailwind entrypoint.
|
|
54
|
+
- Import host `theme.css` after the Baldur build to override only the base palette inputs and font-token mapping.
|
|
55
|
+
- Treat host `fonts.css` as the project-specific font loading layer.
|
|
56
|
+
- Treat host `theme.css` as the project-specific override layer for palette, font-token mapping, and other brand inputs, not as a place to fork Baldur-owned primitive styles.
|
|
57
|
+
- Add host-app styles only for app-specific surfaces after Baldur.
|
|
58
|
+
- Do not re-import or override host-local copies of Baldur-owned primitives such as buttons, forms, snackbars, or tables.
|
|
59
|
+
- Keep shared elevation semantics in Baldur-owned `--elev-*` tokens. If a host app needs softer or stronger shared shadows, change the Baldur token source instead of swapping raw Tailwind shadow utilities into Baldur-owned primitives.
|
|
60
|
+
- Do not keep duplicate copies of Baldur-owned primitives under `app/assets/stylesheets/application/`; leave only app-specific files there.
|
|
61
|
+
- Do not keep host copies of Baldur semantic theme files such as `theme/light.css` or `theme/dark.css`.
|
|
62
|
+
|
|
63
|
+
## What The Installer Assumes
|
|
64
|
+
|
|
65
|
+
- Tailwind entrypoint exists at `app/assets/tailwind/application.css`
|
|
66
|
+
- Host app gets `tailwindcss-rails` through Baldur or its own Gemfile and uses engine builds
|
|
67
|
+
- Host app uses importmap Stimulus boot with `app/javascript/controllers`
|
|
68
|
+
- Host app gets `app/assets/stylesheets/fonts.css` for font loading and `app/assets/stylesheets/theme.css` for brand and font-token overrides
|
|
69
|
+
- Host app can import `app/assets/builds/tailwind/baldur.css` from `app/assets/tailwind/application.css`
|
|
70
|
+
|
|
71
|
+
## Building UI
|
|
72
|
+
|
|
73
|
+
Canonical Ruby internals live under `Baldur::*`, but the default DX is `ui_*` helpers through the generated `UiHelper` include.
|
|
74
|
+
|
|
75
|
+
Examples:
|
|
76
|
+
|
|
77
|
+
```erb
|
|
78
|
+
<%= ui_button(label: "Save", href: "#") %>
|
|
79
|
+
|
|
80
|
+
<%= ui_card(title: "Revenue") do %>
|
|
81
|
+
<p>Content</p>
|
|
82
|
+
<% end %>
|
|
83
|
+
|
|
84
|
+
<%= ui_panel_secondary(id: "assistant", title: "Assistant", trigger_label: "Open") do %>
|
|
85
|
+
<p>Panel content</p>
|
|
86
|
+
<% end %>
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
External triggers can open a Baldur panel declaratively:
|
|
90
|
+
|
|
91
|
+
```erb
|
|
92
|
+
<button
|
|
93
|
+
type="button"
|
|
94
|
+
data-open-panel="#assistant"
|
|
95
|
+
data-panel-payload="<%= json_escape({ source: "dashboard" }.to_json) %>">
|
|
96
|
+
Open assistant
|
|
97
|
+
</button>
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
`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`.
|
|
101
|
+
|
|
102
|
+
For modals, prefer `ui_modal` directly:
|
|
103
|
+
|
|
104
|
+
```erb
|
|
105
|
+
<%= ui_modal(id: "confirm-delete", title: "Delete item") do %>
|
|
106
|
+
<p>This action cannot be undone.</p>
|
|
107
|
+
<% end %>
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
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.
|
|
111
|
+
|
|
112
|
+
For horizontal primary/secondary CTA groups, prefer `ui_action_row`:
|
|
113
|
+
|
|
114
|
+
```erb
|
|
115
|
+
<%= ui_action_row(
|
|
116
|
+
secondary_button: { label: "Back", variant: :outline, href: settings_path },
|
|
117
|
+
primary_button: { label: "Save", variant: :primary, type: :submit }
|
|
118
|
+
) %>
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
The action row owns the responsive layout and keeps the primary CTA last on the right.
|
|
122
|
+
|
|
123
|
+
Use `ui_alert` for inline status surfaces. Alerts support optional inline actions and opt-in collapsed state:
|
|
124
|
+
|
|
125
|
+
```erb
|
|
126
|
+
<%= ui_alert(
|
|
127
|
+
variant: :warning,
|
|
128
|
+
title: "Data freshness warning",
|
|
129
|
+
actions: ui_button(label: "Upload Latest Data", href: new_ecommerce_import_path, variant: :primary, size: :sm),
|
|
130
|
+
collapsible: true,
|
|
131
|
+
collapse_key: "tenant-#{current_tenant.id}-executive-pulse-stale-data"
|
|
132
|
+
) do %>
|
|
133
|
+
<p>Latest available data is 10 days old.</p>
|
|
134
|
+
<% end %>
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
Collapsed alerts stay inline and can be re-expanded with the built-in `More` summary action.
|
|
138
|
+
|
|
139
|
+
## Tables
|
|
140
|
+
|
|
141
|
+
Use the table helpers as a small composition system:
|
|
142
|
+
|
|
143
|
+
- `ui_table` is the table atom.
|
|
144
|
+
- `ui_table_card` is the card shell for title, controls, table body, and footer.
|
|
145
|
+
- `ui_table_footer` owns the `Show [x] items per page` control and the `Showing x-y of z` status line.
|
|
146
|
+
- `ui_pagination` is the page-navigation atom and is usually composed through `ui_table_footer`.
|
|
147
|
+
|
|
148
|
+
If a table has title, controls, rows, and pagination, render them inside the same `ui_table_card`.
|
|
149
|
+
|
|
150
|
+
Use `ui_table` directly for embedded or simple tables:
|
|
151
|
+
|
|
152
|
+
```erb
|
|
153
|
+
<%= ui_table(
|
|
154
|
+
columns: [
|
|
155
|
+
{ label: "SKU", key: :sku },
|
|
156
|
+
{ label: "Status", key: :status },
|
|
157
|
+
{ label: "Revenue", key: :revenue, header_class: "text-right", cell_class: "text-right" }
|
|
158
|
+
],
|
|
159
|
+
rows: [
|
|
160
|
+
{ sku: "SKU-001", status: "Active", revenue: number_to_currency(12_500) },
|
|
161
|
+
{ sku: "SKU-002", status: "Draft", revenue: number_to_currency(3_800) }
|
|
162
|
+
],
|
|
163
|
+
empty_state: "No SKUs found"
|
|
164
|
+
) %>
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
Use `ui_table_card` when the table is a standalone surface:
|
|
168
|
+
|
|
169
|
+
```erb
|
|
170
|
+
<% table_controls = capture do %>
|
|
171
|
+
<div class="flex items-end gap-3">
|
|
172
|
+
<%= ui_menu_select_tag :status,
|
|
173
|
+
options: [
|
|
174
|
+
{ label: "All", value: "all" },
|
|
175
|
+
{ label: "Active", value: "active" },
|
|
176
|
+
{ label: "Draft", value: "draft" }
|
|
177
|
+
],
|
|
178
|
+
selected: params[:status].presence || "all",
|
|
179
|
+
label: "Status" %>
|
|
180
|
+
</div>
|
|
181
|
+
<% end %>
|
|
182
|
+
|
|
183
|
+
<%= ui_table_card(
|
|
184
|
+
title: "Products",
|
|
185
|
+
description: "Track inventory and performance in one place.",
|
|
186
|
+
controls: table_controls,
|
|
187
|
+
controls_position: :header,
|
|
188
|
+
footer: ui_table_footer(
|
|
189
|
+
current_page: @pagination[:current_page],
|
|
190
|
+
total_pages: @pagination[:total_pages],
|
|
191
|
+
total_count: @pagination[:total_count],
|
|
192
|
+
per_page: @pagination[:per_page],
|
|
193
|
+
path_builder: ->(page) { products_path(request.query_parameters.merge(page: page, per_page: @pagination[:per_page])) },
|
|
194
|
+
rows_per_page_param: "per_page",
|
|
195
|
+
rows_per_page_options: [10, 20, 50],
|
|
196
|
+
rows_per_page_selected: @pagination[:per_page]
|
|
197
|
+
)
|
|
198
|
+
) do %>
|
|
199
|
+
<%= ui_table(
|
|
200
|
+
sort: { key: params[:sort], direction: params[:direction] },
|
|
201
|
+
sort_path_builder: ->(sort_key, direction) { products_path(request.query_parameters.merge(sort: sort_key, direction: direction, page: 1)) },
|
|
202
|
+
columns: [
|
|
203
|
+
{ label: "SKU", key: :sku, sortable: true, sort_key: "sku" },
|
|
204
|
+
{ label: "Status", key: :status },
|
|
205
|
+
{
|
|
206
|
+
label: "Revenue",
|
|
207
|
+
key: :revenue,
|
|
208
|
+
sortable: true,
|
|
209
|
+
sort_key: "revenue",
|
|
210
|
+
header_class: "text-right",
|
|
211
|
+
cell_class: "text-right",
|
|
212
|
+
header_tooltip: "Total revenue attributed to the current filter window."
|
|
213
|
+
}
|
|
214
|
+
],
|
|
215
|
+
rows: @rows,
|
|
216
|
+
empty_state: "No products found"
|
|
217
|
+
) %>
|
|
218
|
+
<% end %>
|
|
219
|
+
```
|
|
220
|
+
|
|
221
|
+
Use `controls_position: :header` for compact data-view controls that belong in the top-right header zone. Keep the default `:row` placement for wider filter bars. You can also pass `title_meta:` to render subdued inline metadata beside the title, for example `title_meta: "24 rows"`.
|
|
222
|
+
|
|
223
|
+
Sorting is opt-in: header sort controls render only when a column is marked `sortable: true` and the table receives `sort:` plus `sort_path_builder:`.
|
|
224
|
+
|
|
225
|
+
Use `ui_pagination` directly only when you need bare page navigation without the table-footer composition.
|
|
226
|
+
|
|
227
|
+
## Marketing Templates
|
|
228
|
+
|
|
229
|
+
Keep marketing-page templates separate from app UI primitives. Baldur’s low-level atoms still live under the shared `ui_*` component layer, while full-page marketing surfaces use dedicated `ui_marketing_*` helpers.
|
|
230
|
+
|
|
231
|
+
Current canonical marketing helpers:
|
|
232
|
+
|
|
233
|
+
- `ui_marketing_top_nav`
|
|
234
|
+
- `ui_marketing_hero_section(variant: :solar_system, ...)`
|
|
235
|
+
- `ui_marketing_features_section`
|
|
236
|
+
- `ui_marketing_testimonials_section(variant: :bento, ...)`
|
|
237
|
+
- `ui_marketing_faq_section`
|
|
238
|
+
- `ui_marketing_cta_banner`
|
|
239
|
+
- `ui_marketing_pricing_tables`
|
|
240
|
+
- `ui_marketing_footer`
|
|
241
|
+
|
|
242
|
+
Use the existing landing and pricing templates as the canonical v1 variants. Future hero or testimonial layouts should be added as new variants, not folded into the existing default markup.
|
|
243
|
+
|
|
244
|
+
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`. If a future 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.
|
|
245
|
+
|
|
246
|
+
Interactive marketing templates ship with Baldur-owned Stimulus controllers. `baldur:install` now 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.
|
|
247
|
+
|
|
248
|
+
`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.
|
|
249
|
+
|
|
250
|
+
Example hero usage:
|
|
251
|
+
|
|
252
|
+
```erb
|
|
253
|
+
<%= ui_marketing_hero_section(
|
|
254
|
+
variant: :solar_system,
|
|
255
|
+
headline: "Turn Every Data Point Into ROI",
|
|
256
|
+
body: "Connect fragmented data into one decision engine.",
|
|
257
|
+
primary_action: { label: "Book a Demo", variant: :primary, href: dashboard_path },
|
|
258
|
+
secondary_action: { label: "See Use Cases", variant: :outline, href: "#use-cases" },
|
|
259
|
+
supporting_action: { href: "#", label: "Watch walkthrough", data: { open_modal: "#walkthrough-modal" } },
|
|
260
|
+
callouts: [
|
|
261
|
+
{ label: "Unified decision context" },
|
|
262
|
+
{ label: "Prioritized recommendations" },
|
|
263
|
+
{ label: "Impact-aware next actions" }
|
|
264
|
+
],
|
|
265
|
+
orbit_sources: [
|
|
266
|
+
{ name: "Shopify", asset_path: "/landing/source-logos/shopify.svg" },
|
|
267
|
+
{ name: "HubSpot", asset_path: "/landing/source-logos/hubspot.svg" }
|
|
268
|
+
],
|
|
269
|
+
centerpiece_image: { src: "/branding/logo.png", alt: "Acme logo" }
|
|
270
|
+
) %>
|
|
271
|
+
```
|
|
272
|
+
|
|
273
|
+
Example features section usage:
|
|
274
|
+
|
|
275
|
+
```erb
|
|
276
|
+
<%= ui_marketing_features_section(
|
|
277
|
+
title: "What Mimir unlocks for your teams",
|
|
278
|
+
description: "Tailored to your business model and decision priorities.",
|
|
279
|
+
tabs: [
|
|
280
|
+
{
|
|
281
|
+
value: "ecommerce",
|
|
282
|
+
label: "E-commerce",
|
|
283
|
+
selected: true,
|
|
284
|
+
panel_title: "E-commerce",
|
|
285
|
+
panel_body: "Priority ROI plays for commerce teams.",
|
|
286
|
+
cards: [
|
|
287
|
+
{ title: "Which products should I run campaigns for?", body: "Rank SKUs by incremental margin potential." }
|
|
288
|
+
]
|
|
289
|
+
}
|
|
290
|
+
],
|
|
291
|
+
cta: { label: "Get a demo tailored for you", variant: :primary, href: dashboard_path }
|
|
292
|
+
) %>
|
|
293
|
+
```
|
|
294
|
+
|
|
295
|
+
## Snackbars
|
|
296
|
+
|
|
297
|
+
Use semantic snackbar tones:
|
|
298
|
+
|
|
299
|
+
- `:success` for green success states
|
|
300
|
+
- `:notice` for blue notice/info states
|
|
301
|
+
- `:warning` for amber warning states
|
|
302
|
+
- `:error` for red error states
|
|
303
|
+
|
|
304
|
+
Host apps should map `flash[:notice]` to `:notice` and `flash[:alert]` to `:error` unless they have a stronger semantic signal available.
|
|
305
|
+
|
|
306
|
+
## Smoke Check
|
|
307
|
+
|
|
308
|
+
Run this from the host app root after installation:
|
|
309
|
+
|
|
310
|
+
```sh
|
|
311
|
+
bundle exec ruby "$(bundle show baldur)/script/verify_host_install"
|
|
312
|
+
```
|
|
313
|
+
|
|
314
|
+
That verifies the host can render core helpers and confirms the Tailwind entrypoint contains the required Baldur imports.
|
|
315
|
+
|
|
316
|
+
## Deferred Work
|
|
317
|
+
|
|
318
|
+
See `TODO.md` for work intentionally deferred until the dedicated gem repo exists.
|
data/TODO.md
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
# Baldur TODO
|
|
2
|
+
|
|
3
|
+
- Add a dedicated dummy app in the extracted gem repo for visual smoke checks.
|
|
4
|
+
- Add a component inventory/showcase page in that dummy app.
|
|
5
|
+
- Add a few interaction-specific showcase pages for modal, sidebar, menu select, snackbar, and `panel_secondary`.
|
|
6
|
+
- Add release automation in the extracted gem repo once publication workflow is defined.
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
import { Controller } from "@hotwired/stimulus";
|
|
2
|
+
import { updateAriaExpanded } from "baldur/lib/focus-management";
|
|
3
|
+
|
|
4
|
+
export default class AccordionController extends Controller {
|
|
5
|
+
static targets = ["item", "content", "icon"];
|
|
6
|
+
static values = {
|
|
7
|
+
allowMultiple: { type: Boolean, default: false },
|
|
8
|
+
transitionDuration: { type: Number, default: 300 }
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
connect() {
|
|
12
|
+
this.setupInitialState();
|
|
13
|
+
this.handleTurboFrameLoad = this.handleTurboFrameLoad.bind(this);
|
|
14
|
+
this.element.addEventListener("turbo:frame-load", this.handleTurboFrameLoad);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
disconnect() {
|
|
18
|
+
this.element.removeEventListener("turbo:frame-load", this.handleTurboFrameLoad);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
setupInitialState() {
|
|
22
|
+
this.itemTargets.forEach((item) => {
|
|
23
|
+
const isOpen = item.dataset.open === "true";
|
|
24
|
+
const content = item.querySelector('[data-accordion-target="content"]');
|
|
25
|
+
const icon = item.querySelector('[data-accordion-target="icon"]');
|
|
26
|
+
const button = item.querySelector("button");
|
|
27
|
+
|
|
28
|
+
if (!content) return;
|
|
29
|
+
|
|
30
|
+
content.style.transition = `max-height ${this.transitionDurationValue}ms var(--motion-easing-standard, ease), opacity ${this.transitionDurationValue}ms var(--motion-easing-standard, ease)`;
|
|
31
|
+
content.style.overflow = "hidden";
|
|
32
|
+
|
|
33
|
+
if (isOpen) {
|
|
34
|
+
content.style.maxHeight = "200px";
|
|
35
|
+
content.style.opacity = "1";
|
|
36
|
+
|
|
37
|
+
setTimeout(() => {
|
|
38
|
+
content.style.maxHeight = `${content.scrollHeight}px`;
|
|
39
|
+
|
|
40
|
+
setTimeout(() => {
|
|
41
|
+
if (item.dataset.open === "true") {
|
|
42
|
+
content.style.maxHeight = "none";
|
|
43
|
+
}
|
|
44
|
+
}, this.transitionDurationValue);
|
|
45
|
+
}, 100);
|
|
46
|
+
} else {
|
|
47
|
+
content.style.maxHeight = "0px";
|
|
48
|
+
content.style.opacity = "0";
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
this.updateIcon(icon, isOpen);
|
|
52
|
+
this.updateAriaState(button, content, isOpen);
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
handleTurboFrameLoad() {
|
|
57
|
+
this.itemTargets.forEach((item) => {
|
|
58
|
+
if (item.dataset.open !== "true") return;
|
|
59
|
+
const content = item.querySelector('[data-accordion-target="content"]');
|
|
60
|
+
if (!content) return;
|
|
61
|
+
|
|
62
|
+
content.style.maxHeight = "none";
|
|
63
|
+
const newHeight = content.scrollHeight;
|
|
64
|
+
content.style.maxHeight = `${newHeight}px`;
|
|
65
|
+
|
|
66
|
+
setTimeout(() => {
|
|
67
|
+
if (item.dataset.open === "true") {
|
|
68
|
+
content.style.maxHeight = "none";
|
|
69
|
+
}
|
|
70
|
+
}, 50);
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
toggle(event) {
|
|
75
|
+
const button = event.currentTarget;
|
|
76
|
+
const item = button.closest('[data-accordion-target="item"]');
|
|
77
|
+
const content = item?.querySelector('[data-accordion-target="content"]');
|
|
78
|
+
const icon = item?.querySelector('[data-accordion-target="icon"]');
|
|
79
|
+
|
|
80
|
+
if (!item || !content) return;
|
|
81
|
+
|
|
82
|
+
const isOpen = content.style.maxHeight !== "0px" && content.style.maxHeight !== "";
|
|
83
|
+
|
|
84
|
+
if (isOpen) {
|
|
85
|
+
this.closeItem(item, content, icon, button);
|
|
86
|
+
} else {
|
|
87
|
+
if (!this.allowMultipleValue) {
|
|
88
|
+
this.closeAllItems(item);
|
|
89
|
+
}
|
|
90
|
+
this.openItem(item, content, icon, button);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
openItem(item, content, icon, button) {
|
|
95
|
+
content.style.maxHeight = `${content.scrollHeight}px`;
|
|
96
|
+
content.style.opacity = "1";
|
|
97
|
+
item.dataset.open = "true";
|
|
98
|
+
|
|
99
|
+
this.updateIcon(icon, true);
|
|
100
|
+
this.updateAriaState(button, content, true);
|
|
101
|
+
|
|
102
|
+
setTimeout(() => {
|
|
103
|
+
if (item.dataset.open === "true") {
|
|
104
|
+
content.style.maxHeight = "none";
|
|
105
|
+
}
|
|
106
|
+
}, this.transitionDurationValue);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
closeItem(item, content, icon, button) {
|
|
110
|
+
content.style.maxHeight = `${content.scrollHeight}px`;
|
|
111
|
+
content.offsetHeight;
|
|
112
|
+
content.style.maxHeight = "0px";
|
|
113
|
+
content.style.opacity = "0";
|
|
114
|
+
item.dataset.open = "false";
|
|
115
|
+
|
|
116
|
+
this.updateIcon(icon, false);
|
|
117
|
+
this.updateAriaState(button, content, false);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
closeAllItems(exceptItem) {
|
|
121
|
+
this.itemTargets.forEach((item) => {
|
|
122
|
+
if (item === exceptItem) return;
|
|
123
|
+
const content = item.querySelector('[data-accordion-target="content"]');
|
|
124
|
+
const icon = item.querySelector('[data-accordion-target="icon"]');
|
|
125
|
+
const button = item.querySelector("button");
|
|
126
|
+
|
|
127
|
+
if (!content) return;
|
|
128
|
+
if (content.style.maxHeight === "0px" || content.style.maxHeight === "") return;
|
|
129
|
+
|
|
130
|
+
this.closeItem(item, content, icon, button);
|
|
131
|
+
});
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
updateIcon(icon, isOpen) {
|
|
135
|
+
if (!icon) return;
|
|
136
|
+
icon.style.transition = `transform ${this.transitionDurationValue}ms var(--motion-easing-standard, ease)`;
|
|
137
|
+
icon.style.transform = isOpen ? "rotate(90deg)" : "rotate(0deg)";
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
updateAriaState(button, content, isOpen) {
|
|
141
|
+
if (button) {
|
|
142
|
+
updateAriaExpanded(button, isOpen);
|
|
143
|
+
}
|
|
144
|
+
if (content) {
|
|
145
|
+
content.setAttribute("aria-hidden", isOpen ? "false" : "true");
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
}
|