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.
- checksums.yaml +4 -4
- data/README.md +10 -0
- data/TODO.md +27 -7
- data/app/assets/javascripts/baldur/controllers/segmented_tabs_controller.js +53 -2
- data/app/assets/javascripts/baldur/controllers/theme_controller.js +4 -3
- data/app/assets/stylesheets/baldur/application/components/auth-page.css +5 -6
- data/app/assets/stylesheets/baldur/application/components/table.css +17 -7
- data/app/helpers/baldur/ui_helper.rb +11 -2
- data/app/helpers/baldur/ui_helper_feedback.rb +19 -2
- data/app/views/baldur/components/_button.html.erb +4 -0
- data/app/views/baldur/components/_segmented_buttons.html.erb +14 -7
- data/app/views/baldur/components/_snackbar_stack.html.erb +10 -6
- data/app/views/baldur/components/_table.html.erb +6 -4
- data/app/views/baldur/optional/_auth_page.html.erb +2 -2
- data/baldur.gemspec +4 -1
- data/context7.json +17 -0
- data/docs/alerts-and-snackbars.md +72 -0
- data/docs/auth.md +66 -0
- data/docs/forms.md +267 -0
- data/docs/installation.md +63 -0
- data/docs/marketing.md +77 -0
- data/docs/modals-and-panels.md +55 -0
- data/docs/security.md +11 -0
- data/docs/sidebar.md +105 -0
- data/docs/styling.md +34 -0
- data/docs/tables.md +173 -0
- data/docs/tabs-and-segmented-controls.md +509 -0
- data/docs/theme.md +118 -0
- data/lib/baldur/version.rb +1 -1
- data/llms-full.txt +179 -0
- data/llms.txt +35 -0
- data/test/hidden_field_helper_test.rb +23 -0
- data/test/segmented_buttons_helper_test.rb +85 -0
- data/test/snackbar_stack_helper_test.rb +121 -0
- data/test/table_helper_test.rb +118 -0
- data/test/text_field_helper_test.rb +40 -0
- data/test/theme_toggle_helper_test.rb +2 -0
- metadata +22 -2
data/docs/theme.md
ADDED
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
# Theme
|
|
2
|
+
|
|
3
|
+
Baldur ships a light/dark theme system backed by CSS custom properties and a Stimulus controller.
|
|
4
|
+
|
|
5
|
+
## Quick Setup
|
|
6
|
+
|
|
7
|
+
1. Mount the theme controller on a high-level element (typically `<body>`):
|
|
8
|
+
|
|
9
|
+
```erb
|
|
10
|
+
<body data-controller="theme">
|
|
11
|
+
<%= yield %>
|
|
12
|
+
</body>
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
2. Render a theme toggle where needed:
|
|
16
|
+
|
|
17
|
+
```erb
|
|
18
|
+
<%= ui_theme_toggle %>
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
That is all — the controller initializes on connect, reads any stored preference, and applies the class.
|
|
22
|
+
|
|
23
|
+
## Theme Controller
|
|
24
|
+
|
|
25
|
+
`baldur/controllers/theme_controller` is included in the base install shim (`app/javascript/controllers/theme_controller.js`).
|
|
26
|
+
|
|
27
|
+
### How It Works
|
|
28
|
+
|
|
29
|
+
| Step | Behavior |
|
|
30
|
+
|------|----------|
|
|
31
|
+
| Connect | Reads stored preference from `localStorage` (`baldur.theme` by default). Falls back to system preference (`prefers-color-scheme`), then the first configured theme. Once the user toggles, the stored choice wins over system preference. |
|
|
32
|
+
| Apply | Adds the theme class (`"light"` or `"dark"`) to `<html>`. Sets `data-theme` attribute. |
|
|
33
|
+
| Toggle | Checkbox inputs with `data-theme-target="toggle"` sync checked state. Compact icon buttons use `data-action="click->theme#toggle"`. |
|
|
34
|
+
| Persist | Stores chosen theme to `localStorage` under `baldur.theme`. |
|
|
35
|
+
|
|
36
|
+
### Values
|
|
37
|
+
|
|
38
|
+
| Value | Type | Default | Purpose |
|
|
39
|
+
|-------|------|---------|---------|
|
|
40
|
+
| `storageKey` | String | `"baldur.theme"` | localStorage key for persistence |
|
|
41
|
+
| `themes` | Array | `["light", "dark"]` | Theme class names to cycle |
|
|
42
|
+
|
|
43
|
+
### Targets
|
|
44
|
+
|
|
45
|
+
| Target | Element | Purpose |
|
|
46
|
+
|--------|---------|---------|
|
|
47
|
+
| `toggle` | Checkbox `<input>` | Syncs checked state (`checked` = dark) |
|
|
48
|
+
|
|
49
|
+
### Public API
|
|
50
|
+
|
|
51
|
+
The controller exposes methods for programmatic use:
|
|
52
|
+
|
|
53
|
+
```js
|
|
54
|
+
// Read current theme
|
|
55
|
+
this.application.getControllerForElementAndContainer(document.body, "theme").getTheme()
|
|
56
|
+
|
|
57
|
+
// Set theme explicitly
|
|
58
|
+
controller.setTheme("dark") // animated (default)
|
|
59
|
+
controller.setTheme("dark", { animate: false })
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
### Animation
|
|
63
|
+
|
|
64
|
+
When theme changes, the controller briefly adds `theme-transition` to `<html>` (800ms). Define this class in your CSS to control the transition:
|
|
65
|
+
|
|
66
|
+
```css
|
|
67
|
+
.theme-transition,
|
|
68
|
+
.theme-transition *,
|
|
69
|
+
.theme-transition *::before,
|
|
70
|
+
.theme-transition *::after {
|
|
71
|
+
transition: background-color 300ms ease, color 300ms ease !important;
|
|
72
|
+
}
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
## Brand Token Customization
|
|
76
|
+
|
|
77
|
+
Baldur derives all semantic colors from four brand input tokens. Override only these in your host `theme.css`:
|
|
78
|
+
|
|
79
|
+
| Token | Default (oklch) | Purpose |
|
|
80
|
+
|-------|-----------------|---------|
|
|
81
|
+
| `--_primary-base` | `0.682 0.157 27.1` | Primary actions, CTA |
|
|
82
|
+
| `--_secondary-base` | `0.2968 0.0383 195.29` | Sidebar, nav, subdued surfaces |
|
|
83
|
+
| `--_accent-base` | `0.482 0.067 91.7` | Highlights, badges, tags |
|
|
84
|
+
| `--_neutral-base` | `0.8672 0 0` | Surfaces, text, borders |
|
|
85
|
+
|
|
86
|
+
### Do
|
|
87
|
+
|
|
88
|
+
```css
|
|
89
|
+
:root {
|
|
90
|
+
--_primary-base: oklch(0.55 0.22 260);
|
|
91
|
+
}
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
### Do Not
|
|
95
|
+
|
|
96
|
+
```css
|
|
97
|
+
:root {
|
|
98
|
+
--color-primary: oklch(0.55 0.22 260); /* Baldur owns semantic outputs */
|
|
99
|
+
--color-surface: white; /* Breaks dark-mode mapping */
|
|
100
|
+
}
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
Baldur automatically maps brand inputs through scale/tone stops and resolves them into light and dark semantic roles. Overriding a semantic token directly breaks that mapping.
|
|
104
|
+
|
|
105
|
+
## Font Mapping
|
|
106
|
+
|
|
107
|
+
1. Load web fonts in `app/assets/stylesheets/fonts.css` (before Tailwind).
|
|
108
|
+
2. Map loaded families to font tokens in `app/assets/stylesheets/theme.css` (after Baldur build):
|
|
109
|
+
|
|
110
|
+
```css
|
|
111
|
+
:root {
|
|
112
|
+
--font-body: "Geist", "Inter", system-ui, sans-serif;
|
|
113
|
+
--font-heading: var(--font-body);
|
|
114
|
+
--font-ui: var(--font-body);
|
|
115
|
+
}
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
Baldur uses `--font-body` as the default for body, heading, and UI text. Override `--font-heading` or `--font-ui` separately only when you need a different family for those roles.
|
data/lib/baldur/version.rb
CHANGED
data/llms-full.txt
ADDED
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
# Baldur Full Agent Guide
|
|
2
|
+
|
|
3
|
+
Baldur is an opinionated Rails UI engine for the Propshaft + importmap + Stimulus + Tailwind stack. It ships install generators, reusable `ui_*` helpers, partial-backed components, Tailwind styles, and Stimulus controllers so host apps can compose polished UI without rebuilding common primitives.
|
|
4
|
+
|
|
5
|
+
## Supported Stack
|
|
6
|
+
|
|
7
|
+
- Ruby `>= 3.3`
|
|
8
|
+
- Rails `>= 7.0`
|
|
9
|
+
- Propshaft
|
|
10
|
+
- `importmap-rails`
|
|
11
|
+
- `stimulus-rails`
|
|
12
|
+
- `tailwindcss-rails >= 4.3.0`
|
|
13
|
+
|
|
14
|
+
## Install Flow
|
|
15
|
+
|
|
16
|
+
Add to the host `Gemfile`:
|
|
17
|
+
|
|
18
|
+
```ruby
|
|
19
|
+
gem "baldur", ">= 0.1.3"
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
Run:
|
|
23
|
+
|
|
24
|
+
```sh
|
|
25
|
+
bundle install
|
|
26
|
+
bundle exec rails tailwindcss:engines
|
|
27
|
+
bundle exec rails generate baldur:install
|
|
28
|
+
bundle exec rails tailwindcss:build
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
Optional generators:
|
|
32
|
+
|
|
33
|
+
```sh
|
|
34
|
+
bundle exec rails generate baldur:install_panel_secondary
|
|
35
|
+
bundle exec rails generate baldur:install_google_auth
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
## Host App Assumptions
|
|
39
|
+
|
|
40
|
+
- Tailwind entrypoint exists at `app/assets/tailwind/application.css`.
|
|
41
|
+
- Host app uses importmap Stimulus boot with `app/javascript/controllers`.
|
|
42
|
+
- Host app has `app/assets/stylesheets/fonts.css` and `app/assets/stylesheets/theme.css`.
|
|
43
|
+
- Host app can import `app/assets/builds/tailwind/baldur.css` from `app/assets/tailwind/application.css`.
|
|
44
|
+
|
|
45
|
+
After installation, smoke-check with:
|
|
46
|
+
|
|
47
|
+
```sh
|
|
48
|
+
bundle exec rails tailwindcss:build
|
|
49
|
+
bundle exec ruby "$(bundle show baldur)/script/verify_host_install"
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
## Core Helper Families
|
|
53
|
+
|
|
54
|
+
### App shell and navigation
|
|
55
|
+
|
|
56
|
+
- `ui_sidebar`: authenticated application shell with desktop + mobile sidebar behavior.
|
|
57
|
+
- Host app owns routes, active-state logic, and app-specific slot content.
|
|
58
|
+
- Baldur owns sidebar shell markup, toggle behavior, and default nav rendering.
|
|
59
|
+
|
|
60
|
+
See: `docs/sidebar.md`
|
|
61
|
+
|
|
62
|
+
### Auth
|
|
63
|
+
|
|
64
|
+
- `ui_auth_page`: branded auth layout shell.
|
|
65
|
+
- Default base install already includes auth page helper.
|
|
66
|
+
|
|
67
|
+
See: `docs/auth.md`
|
|
68
|
+
|
|
69
|
+
### Forms
|
|
70
|
+
|
|
71
|
+
- `ui_text_field_tag` accepts Baldur-owned named parameters for labels, support text, wrappers, prefix/suffix, and multiline behavior.
|
|
72
|
+
- Pass raw HTML5 input attributes such as `step`, `min`, `max`, `pattern`, `inputmode`, and `autocomplete` through `input_options:`.
|
|
73
|
+
- `ui_hidden_field_tag` is a thin wrapper over Rails `hidden_field_tag` for hidden state fields.
|
|
74
|
+
- Use hidden fields to preserve interactive state such as active tabs, filters, stepped form state, and selected panels across submits or Turbo rerenders.
|
|
75
|
+
- Prefer `ui_hidden_field_tag` for documented Baldur interaction patterns; plain `hidden_field_tag` remains acceptable for simple Rails glue.
|
|
76
|
+
|
|
77
|
+
See: `docs/forms.md`
|
|
78
|
+
|
|
79
|
+
### Feedback
|
|
80
|
+
|
|
81
|
+
- `ui_alert`: inline status surface with optional actions and collapsible state.
|
|
82
|
+
- `ui_snackbar_stack`: global snackbar wrapper. Accepts `snackbars:`, `id:`, `class_name:`, `data:`.
|
|
83
|
+
- `snackbar_flash_payloads(flash)`: maps Rails flash into snackbar payloads.
|
|
84
|
+
- `ui_snackbar_turbo_stream(flash, target: "snackbar-stack")`: opt-in Turbo Stream update helper. Requires `turbo-rails`.
|
|
85
|
+
|
|
86
|
+
Recommended layout pattern:
|
|
87
|
+
|
|
88
|
+
```erb
|
|
89
|
+
<%= ui_snackbar_stack(
|
|
90
|
+
snackbars: snackbar_flash_payloads(flash),
|
|
91
|
+
id: "snackbar-stack"
|
|
92
|
+
) %>
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
Recommended Turbo Stream pattern:
|
|
96
|
+
|
|
97
|
+
```erb
|
|
98
|
+
<%= ui_snackbar_turbo_stream(flash) %>
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
See: `docs/alerts-and-snackbars.md`
|
|
102
|
+
|
|
103
|
+
### Dialogs and side surfaces
|
|
104
|
+
|
|
105
|
+
- `ui_modal`
|
|
106
|
+
- `ui_modal_host`
|
|
107
|
+
- `ui_confirmation_modal`
|
|
108
|
+
- optional panel helpers via generators
|
|
109
|
+
|
|
110
|
+
See: `docs/modals-and-panels.md`
|
|
111
|
+
|
|
112
|
+
### Tabs and segmented controls
|
|
113
|
+
|
|
114
|
+
- `ui_segmented_buttons` should be treated as Baldur's tabs trigger primitive.
|
|
115
|
+
- It renders `role="tablist"`, `role="tab"`, `aria-selected`, and roving `tabindex`, and now accepts wrapper `id` / `data` plus per-item `id` / `aria` for panel association.
|
|
116
|
+
- Host apps still own panel markup, URL state, Turbo flows, and form-state persistence.
|
|
117
|
+
- `baldur:install` ships `segmented_tabs_controller.js` for click selection, keyboard navigation, panel hide/show, and optional hidden-input syncing.
|
|
118
|
+
- `ui_hidden_field_tag` is available as a thin wrapper over Rails `hidden_field_tag` for preserving selected tab state across form submits.
|
|
119
|
+
|
|
120
|
+
See: `docs/tabs-and-segmented-controls.md`
|
|
121
|
+
|
|
122
|
+
### Data display
|
|
123
|
+
|
|
124
|
+
- `ui_table`
|
|
125
|
+
- table card composition helpers
|
|
126
|
+
- pagination and supporting surfaces
|
|
127
|
+
|
|
128
|
+
See: `docs/tables.md`
|
|
129
|
+
|
|
130
|
+
### Theming and styling
|
|
131
|
+
|
|
132
|
+
- Theme and brand tokens flow through `theme.css` and shared CSS variables.
|
|
133
|
+
- `ui_theme_toggle` exposes first-class theme switching UI.
|
|
134
|
+
- Host apps should customize semantic tokens and font mappings instead of rewriting component styles first.
|
|
135
|
+
|
|
136
|
+
See: `docs/styling.md`, `docs/theme.md`
|
|
137
|
+
|
|
138
|
+
### Marketing surfaces
|
|
139
|
+
|
|
140
|
+
- marketing hero, features, testimonials, FAQ, pricing, CTA, footer helpers
|
|
141
|
+
|
|
142
|
+
See: `docs/marketing.md`
|
|
143
|
+
|
|
144
|
+
## Ownership Boundaries
|
|
145
|
+
|
|
146
|
+
Prefer these boundaries when integrating or extending Baldur:
|
|
147
|
+
|
|
148
|
+
- Baldur owns reusable shell markup, shared class structure, Stimulus hooks, and component-level interaction wiring.
|
|
149
|
+
- Host apps own routes, persistence, controller actions, authorization, data loading, and app-specific page composition.
|
|
150
|
+
- Host apps should prefer `ui_*` helpers over duplicating Baldur component HTML directly.
|
|
151
|
+
- If a host app needs a missing HTML pass-through or repeated composition pattern, add it upstream rather than forking markup locally.
|
|
152
|
+
|
|
153
|
+
## Turbo Guidance
|
|
154
|
+
|
|
155
|
+
- Baldur does not require Turbo globally.
|
|
156
|
+
- For Turbo-enabled apps, treat snackbar stack as a stable server target with explicit `id`.
|
|
157
|
+
- `ui_snackbar_turbo_stream` raises `ArgumentError` if used outside a Turbo Stream-capable context.
|
|
158
|
+
|
|
159
|
+
## Security and Contribution
|
|
160
|
+
|
|
161
|
+
- Security reporting and artifact verification: `docs/security.md`
|
|
162
|
+
- Release history: `CHANGELOG.md`
|
|
163
|
+
- Contribution guide: `CONTRIBUTING.md`
|
|
164
|
+
|
|
165
|
+
## Doc Index
|
|
166
|
+
|
|
167
|
+
- `README.md`
|
|
168
|
+
- `docs/installation.md`
|
|
169
|
+
- `docs/styling.md`
|
|
170
|
+
- `docs/theme.md`
|
|
171
|
+
- `docs/sidebar.md`
|
|
172
|
+
- `docs/auth.md`
|
|
173
|
+
- `docs/forms.md`
|
|
174
|
+
- `docs/modals-and-panels.md`
|
|
175
|
+
- `docs/alerts-and-snackbars.md`
|
|
176
|
+
- `docs/tabs-and-segmented-controls.md`
|
|
177
|
+
- `docs/tables.md`
|
|
178
|
+
- `docs/marketing.md`
|
|
179
|
+
- `docs/security.md`
|
data/llms.txt
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
# Baldur
|
|
2
|
+
|
|
3
|
+
Baldur is an opinionated Rails UI engine for apps using Propshaft, importmap-rails, stimulus-rails, and tailwindcss-rails. It provides reusable `ui_*` helpers, Tailwind-backed components, install generators, and Stimulus wiring for common application surfaces.
|
|
4
|
+
|
|
5
|
+
## Project Links
|
|
6
|
+
|
|
7
|
+
- [README](README.md): Overview, install summary, supported stack, and roadmap.
|
|
8
|
+
- [Installation](docs/installation.md): Required install flow, host assumptions, optional generators, and smoke checks.
|
|
9
|
+
- [Styling](docs/styling.md): Theme tokens, CSS variable conventions, and host customization boundaries.
|
|
10
|
+
- [Theme](docs/theme.md): Theme controller behavior and `ui_theme_toggle` usage.
|
|
11
|
+
- [Sidebar](docs/sidebar.md): `ui_sidebar` usage, link options, slot patterns, and ownership boundaries.
|
|
12
|
+
- [Auth](docs/auth.md): `ui_auth_page` usage and auth-page composition guidance.
|
|
13
|
+
- [Forms](docs/forms.md): `ui_text_field_tag` guidance, HTML5 attribute pass-through via `input_options`, and `ui_hidden_field_tag` usage.
|
|
14
|
+
- [Modals and Panels](docs/modals-and-panels.md): `ui_modal`, confirmation modal, panel helpers, and interaction wiring.
|
|
15
|
+
- [Alerts and Snackbars](docs/alerts-and-snackbars.md): `ui_alert`, `ui_snackbar_stack`, `snackbar_flash_payloads`, and Turbo Stream snackbar updates.
|
|
16
|
+
- [Tabs and Segmented Controls](docs/tabs-and-segmented-controls.md): `ui_segmented_buttons` as a tabs trigger primitive, with local, Turbo-backed, and form-state cookbook examples.
|
|
17
|
+
- [Tables](docs/tables.md): Table helper APIs, table card composition, and rendering guidance.
|
|
18
|
+
- [Marketing](docs/marketing.md): Marketing page helpers and composition patterns.
|
|
19
|
+
- [Security](docs/security.md): Artifact verification, MFA requirements, and vulnerability reporting.
|
|
20
|
+
|
|
21
|
+
## Key Constraints
|
|
22
|
+
|
|
23
|
+
- Supported stack: Ruby `>= 3.3`, Rails `>= 7.0`, Propshaft, `importmap-rails`, `stimulus-rails`, `tailwindcss-rails >= 4.3.0`.
|
|
24
|
+
- Base install assumes host app has `app/assets/tailwind/application.css`, `app/javascript/controllers`, `app/assets/stylesheets/fonts.css`, and `app/assets/stylesheets/theme.css`.
|
|
25
|
+
- Prefer Baldur `ui_*` helpers over ad hoc markup when a helper exists.
|
|
26
|
+
- Optional surfaces may require generators such as `baldur:install_panel_secondary` and `baldur:install_google_auth`.
|
|
27
|
+
- Turbo support is opt-in. `ui_snackbar_turbo_stream` requires `turbo-rails` in host app.
|
|
28
|
+
|
|
29
|
+
## Agent Starting Points
|
|
30
|
+
|
|
31
|
+
- For first-time adoption, start with [Installation](docs/installation.md).
|
|
32
|
+
- For app-shell composition, read [Sidebar](docs/sidebar.md), [Auth](docs/auth.md), and [Modals and Panels](docs/modals-and-panels.md).
|
|
33
|
+
- For tabs or segmented controls, read [Tabs and Segmented Controls](docs/tabs-and-segmented-controls.md).
|
|
34
|
+
- For feedback surfaces, read [Alerts and Snackbars](docs/alerts-and-snackbars.md).
|
|
35
|
+
- For host theming or appearance changes, read [Styling](docs/styling.md) and [Theme](docs/theme.md).
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
require_relative 'test_helper'
|
|
2
|
+
|
|
3
|
+
require 'action_controller'
|
|
4
|
+
|
|
5
|
+
class BaldurHiddenFieldHelperTest < Minitest::Test
|
|
6
|
+
class TestController < ActionController::Base
|
|
7
|
+
append_view_path File.expand_path('../app/views', __dir__)
|
|
8
|
+
helper Baldur::UiHelper
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def test_ui_hidden_field_tag_renders_rails_hidden_field_markup
|
|
12
|
+
html = TestController.render(
|
|
13
|
+
inline: '<%= ui_hidden_field_tag(:planner_tab, "targets", data: { planner_tabs_target: "hiddenInput" }) %>',
|
|
14
|
+
formats: [:html]
|
|
15
|
+
)
|
|
16
|
+
|
|
17
|
+
assert_includes html, 'type="hidden"'
|
|
18
|
+
assert_includes html, 'name="planner_tab"'
|
|
19
|
+
assert_includes html, 'id="planner_tab"'
|
|
20
|
+
assert_includes html, 'value="targets"'
|
|
21
|
+
assert_includes html, 'data-planner-tabs-target="hiddenInput"'
|
|
22
|
+
end
|
|
23
|
+
end
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
require_relative 'test_helper'
|
|
2
|
+
|
|
3
|
+
require 'action_controller'
|
|
4
|
+
|
|
5
|
+
class BaldurSegmentedButtonsHelperTest < Minitest::Test
|
|
6
|
+
class TestController < ActionController::Base
|
|
7
|
+
append_view_path File.expand_path('../app/views', __dir__)
|
|
8
|
+
helper Baldur::UiHelper
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def test_ui_segmented_buttons_renders_wrapper_and_tab_aria_wiring
|
|
12
|
+
html = TestController.render(
|
|
13
|
+
inline: <<~ERB,
|
|
14
|
+
<%= ui_segmented_buttons(
|
|
15
|
+
id: "catalog-tabs",
|
|
16
|
+
aria_label: "Catalog tabs",
|
|
17
|
+
data: { controller: "segmented-tabs" },
|
|
18
|
+
items: [
|
|
19
|
+
{
|
|
20
|
+
id: "catalog-tab-overview",
|
|
21
|
+
label: "Overview",
|
|
22
|
+
value: "overview",
|
|
23
|
+
selected: true,
|
|
24
|
+
aria: { controls: "catalog-panel-overview" },
|
|
25
|
+
data: { segmented_tabs_target: "tab", tab_value: "overview" }
|
|
26
|
+
},
|
|
27
|
+
{
|
|
28
|
+
id: "catalog-tab-settings",
|
|
29
|
+
label: "Settings",
|
|
30
|
+
value: "settings",
|
|
31
|
+
aria: { controls: "catalog-panel-settings" },
|
|
32
|
+
data: { segmented_tabs_target: "tab", tab_value: "settings" }
|
|
33
|
+
}
|
|
34
|
+
]
|
|
35
|
+
) %>
|
|
36
|
+
ERB
|
|
37
|
+
formats: [:html]
|
|
38
|
+
)
|
|
39
|
+
|
|
40
|
+
assert_includes html, 'id="catalog-tabs"'
|
|
41
|
+
assert_includes html, 'role="tablist"'
|
|
42
|
+
assert_includes html, 'aria-label="Catalog tabs"'
|
|
43
|
+
assert_includes html, 'data-controller="segmented-tabs"'
|
|
44
|
+
assert_includes html, 'id="catalog-tab-overview"'
|
|
45
|
+
assert_includes html, 'aria-controls="catalog-panel-overview"'
|
|
46
|
+
assert_includes html, 'aria-selected="true"'
|
|
47
|
+
assert_includes html, 'data-segmented-tabs-target="tab"'
|
|
48
|
+
assert_includes html, 'data-tab-value="overview"'
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def test_ui_segmented_buttons_keeps_unselected_tabs_focusable_only_via_roving_tabindex
|
|
52
|
+
html = TestController.render(
|
|
53
|
+
inline: <<~ERB,
|
|
54
|
+
<%= ui_segmented_buttons(
|
|
55
|
+
items: [
|
|
56
|
+
{ label: "First", value: "first", selected: true },
|
|
57
|
+
{ label: "Second", value: "second" }
|
|
58
|
+
]
|
|
59
|
+
) %>
|
|
60
|
+
ERB
|
|
61
|
+
formats: [:html]
|
|
62
|
+
)
|
|
63
|
+
|
|
64
|
+
assert_includes html, 'tabindex="0"'
|
|
65
|
+
assert_includes html, 'tabindex="-1"'
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def test_ui_segmented_buttons_renders_role_tab_and_aria_selected_false_on_unselected
|
|
69
|
+
html = TestController.render(
|
|
70
|
+
inline: <<~ERB,
|
|
71
|
+
<%= ui_segmented_buttons(
|
|
72
|
+
items: [
|
|
73
|
+
{ label: "First", value: "first", selected: true },
|
|
74
|
+
{ label: "Second", value: "second" }
|
|
75
|
+
]
|
|
76
|
+
) %>
|
|
77
|
+
ERB
|
|
78
|
+
formats: [:html]
|
|
79
|
+
)
|
|
80
|
+
|
|
81
|
+
assert_includes html, 'role="tab"'
|
|
82
|
+
assert_includes html, 'aria-selected="true"'
|
|
83
|
+
assert_includes html, 'aria-selected="false"'
|
|
84
|
+
end
|
|
85
|
+
end
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
require_relative 'test_helper'
|
|
2
|
+
|
|
3
|
+
require 'action_controller'
|
|
4
|
+
|
|
5
|
+
class BaldurSnackbarStackHelperTest < Minitest::Test
|
|
6
|
+
class TestController < ActionController::Base
|
|
7
|
+
append_view_path File.expand_path('../app/views', __dir__)
|
|
8
|
+
helper Baldur::UiHelper
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def test_renders_stack_with_required_defaults
|
|
12
|
+
html = TestController.render(
|
|
13
|
+
inline: '<%= ui_snackbar_stack(snackbars: []) %>',
|
|
14
|
+
formats: [:html]
|
|
15
|
+
)
|
|
16
|
+
|
|
17
|
+
assert_includes html, 'class="snackbar-stack"'
|
|
18
|
+
assert_includes html, 'data-baldur-snackbar-stack="true"'
|
|
19
|
+
assert_includes html, 'role="status"'
|
|
20
|
+
assert_includes html, 'aria-live="polite"'
|
|
21
|
+
assert_includes html, 'aria-atomic="true"'
|
|
22
|
+
assert_includes html, '<template data-baldur-snackbar-template>'
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def test_renders_stack_with_custom_id
|
|
26
|
+
html = TestController.render(
|
|
27
|
+
inline: '<%= ui_snackbar_stack(snackbars: [], id: "snackbar-stack") %>',
|
|
28
|
+
formats: [:html]
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
assert_includes html, 'id="snackbar-stack"'
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def test_renders_stack_with_custom_class_and_data
|
|
35
|
+
html = TestController.render(
|
|
36
|
+
inline: '<%= ui_snackbar_stack(snackbars: [], class_name: "my-stack", data: { controller: "toast" }) %>',
|
|
37
|
+
formats: [:html]
|
|
38
|
+
)
|
|
39
|
+
|
|
40
|
+
assert_includes html, 'class="snackbar-stack my-stack"'
|
|
41
|
+
assert_includes html, 'data-controller="toast"'
|
|
42
|
+
assert_includes html, 'data-baldur-snackbar-stack="true"'
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def test_renders_stack_with_string_keyed_data_without_duplicate_attributes
|
|
46
|
+
html = TestController.render(
|
|
47
|
+
inline: '<%= ui_snackbar_stack(snackbars: [], data: { "controller" => "toast", "baldur-snackbar-stack" => "override" }) %>',
|
|
48
|
+
formats: [:html]
|
|
49
|
+
)
|
|
50
|
+
|
|
51
|
+
assert_includes html, 'data-controller="toast"'
|
|
52
|
+
refute_includes html, 'data-baldur-snackbar-stack="true"'
|
|
53
|
+
assert_includes html, 'data-baldur-snackbar-stack="override"'
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def test_renders_snackbars_inside_stack
|
|
57
|
+
html = TestController.render(
|
|
58
|
+
inline: '<%= ui_snackbar_stack(snackbars: [{ variant: :success, message: "Saved." }]) %>',
|
|
59
|
+
formats: [:html]
|
|
60
|
+
)
|
|
61
|
+
|
|
62
|
+
assert_includes html, 'class="snackbar snackbar--success"'
|
|
63
|
+
assert_includes html, 'Saved.'
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def test_turbo_stream_helper_requires_turbo_context
|
|
67
|
+
refute TestController._helpers.instance_methods.include?(:turbo_stream)
|
|
68
|
+
|
|
69
|
+
error = assert_raises(ActionView::Template::Error) do
|
|
70
|
+
TestController.render(
|
|
71
|
+
inline: '<%= ui_snackbar_turbo_stream({ notice: "Hello" }) %>',
|
|
72
|
+
formats: [:html]
|
|
73
|
+
)
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
assert_match(/requires turbo-rails/, error.message)
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
def test_turbo_stream_helper_produces_update_with_target_id
|
|
80
|
+
turbo_module = install_turbo_stream_helper
|
|
81
|
+
|
|
82
|
+
html = TestController.render(
|
|
83
|
+
inline: '<%= ui_snackbar_turbo_stream({ notice: "Hello" }) %>',
|
|
84
|
+
formats: [:html]
|
|
85
|
+
)
|
|
86
|
+
|
|
87
|
+
assert_includes html, '<turbo-stream action="update" target="snackbar-stack">'
|
|
88
|
+
assert_includes html, 'id="snackbar-stack"'
|
|
89
|
+
assert_includes html, 'Hello'
|
|
90
|
+
ensure
|
|
91
|
+
uninstall_turbo_stream_helper(turbo_module)
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
private
|
|
95
|
+
|
|
96
|
+
def install_turbo_stream_helper
|
|
97
|
+
return TestController.instance_variable_get(:@turbo_test_module) if TestController.instance_variable_get(:@turbo_test_module)
|
|
98
|
+
|
|
99
|
+
turbo_test_module = Module.new do
|
|
100
|
+
def turbo_stream
|
|
101
|
+
@turbo_stream ||= Object.new.tap do |obj|
|
|
102
|
+
obj.define_singleton_method(:update) do |target, html:|
|
|
103
|
+
"<turbo-stream action=\"update\" target=\"#{target}\">#{html}</turbo-stream>".html_safe
|
|
104
|
+
end
|
|
105
|
+
end
|
|
106
|
+
end
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
TestController.instance_variable_set(:@turbo_test_module, turbo_test_module)
|
|
110
|
+
TestController.helper turbo_test_module
|
|
111
|
+
turbo_test_module
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
def uninstall_turbo_stream_helper(turbo_module)
|
|
115
|
+
turbo_module.module_eval do
|
|
116
|
+
remove_method :turbo_stream if instance_methods(false).include?(:turbo_stream)
|
|
117
|
+
end
|
|
118
|
+
TestController.instance_variable_set(:@turbo_test_module, nil)
|
|
119
|
+
end
|
|
120
|
+
end
|
|
121
|
+
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
require_relative 'test_helper'
|
|
2
|
+
|
|
3
|
+
require 'action_controller'
|
|
4
|
+
|
|
5
|
+
class BaldurTableHelperTest < Minitest::Test
|
|
6
|
+
class TestController < ActionController::Base
|
|
7
|
+
append_view_path File.expand_path('../app/views', __dir__)
|
|
8
|
+
helper Baldur::UiHelper
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def test_numeric_column_right_aligns_header_and_cell
|
|
12
|
+
html = TestController.render(
|
|
13
|
+
inline: <<~ERB,
|
|
14
|
+
<%= ui_table(
|
|
15
|
+
columns: [
|
|
16
|
+
{ label: "SKU", key: :sku },
|
|
17
|
+
{ label: "Revenue", key: :revenue, numeric: true }
|
|
18
|
+
],
|
|
19
|
+
rows: [
|
|
20
|
+
{ sku: "SKU-001", revenue: "$12,500" }
|
|
21
|
+
],
|
|
22
|
+
empty_state: "No records"
|
|
23
|
+
) %>
|
|
24
|
+
ERB
|
|
25
|
+
formats: [:html]
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
assert_includes html, '<th scope="col" class="px-6 py-4 text-xs font-semibold uppercase tracking-wide text-[color:var(--color-on-surface-variant)] text-right">'
|
|
29
|
+
assert_includes html, '<td class="px-6 py-4 text-sm text-[color:var(--color-on-surface)] text-right">'
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def test_non_numeric_column_stays_left_aligned
|
|
33
|
+
html = TestController.render(
|
|
34
|
+
inline: <<~ERB,
|
|
35
|
+
<%= ui_table(
|
|
36
|
+
columns: [
|
|
37
|
+
{ label: "SKU", key: :sku },
|
|
38
|
+
{ label: "Status", key: :status }
|
|
39
|
+
],
|
|
40
|
+
rows: [
|
|
41
|
+
{ sku: "SKU-001", status: "Active" }
|
|
42
|
+
],
|
|
43
|
+
empty_state: "No records"
|
|
44
|
+
) %>
|
|
45
|
+
ERB
|
|
46
|
+
formats: [:html]
|
|
47
|
+
)
|
|
48
|
+
|
|
49
|
+
refute_includes html, 'text-right'
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def test_numeric_middle_column_right_aligns
|
|
53
|
+
html = TestController.render(
|
|
54
|
+
inline: <<~ERB,
|
|
55
|
+
<%= ui_table(
|
|
56
|
+
columns: [
|
|
57
|
+
{ label: "SKU", key: :sku },
|
|
58
|
+
{ label: "Revenue", key: :revenue, numeric: true },
|
|
59
|
+
{ label: "Status", key: :status }
|
|
60
|
+
],
|
|
61
|
+
rows: [
|
|
62
|
+
{ sku: "SKU-001", revenue: "$12,500", status: "Active" }
|
|
63
|
+
],
|
|
64
|
+
empty_state: "No records"
|
|
65
|
+
) %>
|
|
66
|
+
ERB
|
|
67
|
+
formats: [:html]
|
|
68
|
+
)
|
|
69
|
+
|
|
70
|
+
assert_includes html, '<th scope="col" class="px-6 py-4 text-xs font-semibold uppercase tracking-wide text-[color:var(--color-on-surface-variant)] text-right">'
|
|
71
|
+
assert_includes html, '<td class="px-6 py-4 text-sm text-[color:var(--color-on-surface)] text-right">'
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def test_numeric_sortable_header_keeps_right_aligned_layout
|
|
75
|
+
sort_builder = ->(k, d) { "/products?sort=#{k}&direction=#{d}" }
|
|
76
|
+
html = TestController.render(
|
|
77
|
+
inline: <<~ERB,
|
|
78
|
+
<%= ui_table(
|
|
79
|
+
sort: { key: "revenue", direction: "desc" },
|
|
80
|
+
sort_path_builder: sort_builder,
|
|
81
|
+
columns: [
|
|
82
|
+
{ label: "SKU", key: :sku },
|
|
83
|
+
{ label: "Revenue", key: :revenue, numeric: true, sortable: true, sort_key: "revenue" }
|
|
84
|
+
],
|
|
85
|
+
rows: [
|
|
86
|
+
{ sku: "SKU-001", revenue: "$12,500" }
|
|
87
|
+
],
|
|
88
|
+
empty_state: "No records"
|
|
89
|
+
) %>
|
|
90
|
+
ERB
|
|
91
|
+
formats: [:html],
|
|
92
|
+
locals: { sort_builder: sort_builder }
|
|
93
|
+
)
|
|
94
|
+
|
|
95
|
+
assert_includes html, 'text-right'
|
|
96
|
+
assert_includes html, 'justify-end'
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
def test_explicit_cell_class_overrides_numeric
|
|
100
|
+
html = TestController.render(
|
|
101
|
+
inline: <<~ERB,
|
|
102
|
+
<%= ui_table(
|
|
103
|
+
columns: [
|
|
104
|
+
{ label: "SKU", key: :sku },
|
|
105
|
+
{ label: "Revenue", key: :revenue, numeric: true, cell_class: "font-mono" }
|
|
106
|
+
],
|
|
107
|
+
rows: [
|
|
108
|
+
{ sku: "SKU-001", revenue: "$12,500" }
|
|
109
|
+
],
|
|
110
|
+
empty_state: "No records"
|
|
111
|
+
) %>
|
|
112
|
+
ERB
|
|
113
|
+
formats: [:html]
|
|
114
|
+
)
|
|
115
|
+
|
|
116
|
+
assert_includes html, '<td class="px-6 py-4 text-sm text-[color:var(--color-on-surface)] text-right font-mono">'
|
|
117
|
+
end
|
|
118
|
+
end
|