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/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.
@@ -1,3 +1,3 @@
1
1
  module Baldur
2
- VERSION = '0.2.5'.freeze
2
+ VERSION = '0.3.0'.freeze
3
3
  end
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