baldur 0.1.6 → 0.2.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.
@@ -1,4 +1,48 @@
1
1
  @layer components {
2
+ .sidebar-shell {
3
+ min-height: 100vh;
4
+ background: var(--color-surface-low);
5
+ }
6
+
7
+ .sidebar-shell__main {
8
+ min-width: 0;
9
+ flex: 1 1 auto;
10
+ display: flex;
11
+ flex-direction: column;
12
+ }
13
+
14
+ .sidebar-shell__desktop {
15
+ display: none;
16
+ flex-shrink: 0;
17
+ border-right: 1px solid var(--color-outline-variant);
18
+ background: var(--color-surface);
19
+ padding: var(--space-3);
20
+ flex-direction: column;
21
+ }
22
+
23
+ .sidebar__brand-link {
24
+ display: inline-flex;
25
+ text-decoration: none;
26
+ }
27
+
28
+ .sidebar__brand-link .brand-lockup {
29
+ display: inline-flex;
30
+ align-items: center;
31
+ gap: var(--space-2);
32
+ color: var(--color-on-surface);
33
+ }
34
+
35
+ .sidebar__brand-link .brand-lockup__logo {
36
+ width: 2.25rem;
37
+ height: 2.25rem;
38
+ border-radius: var(--radius-xl);
39
+ object-fit: cover;
40
+ }
41
+
42
+ .sidebar__brand-link .brand-lockup__wordmark {
43
+ color: var(--color-on-surface);
44
+ }
45
+
2
46
  .sidebar {
3
47
  position: sticky;
4
48
  top: 0;
@@ -21,6 +65,196 @@
21
65
  font-weight: 500;
22
66
  }
23
67
 
68
+ .sidebar__header {
69
+ display: flex;
70
+ align-items: center;
71
+ justify-content: space-between;
72
+ gap: var(--space-2);
73
+ }
74
+
75
+ .sidebar__slot--header {
76
+ margin-top: var(--space-4);
77
+ }
78
+
79
+ .sidebar__nav {
80
+ margin-top: var(--space-6);
81
+ display: flex;
82
+ flex: 1 1 auto;
83
+ flex-direction: column;
84
+ gap: var(--space-1);
85
+ }
86
+
87
+ .sidebar__section-divider {
88
+ margin-block: var(--space-6);
89
+ border-top: 1px solid var(--color-outline-variant);
90
+ }
91
+
92
+ .sidebar__section-label {
93
+ margin: 0 0 var(--space-2);
94
+ padding-inline: var(--space-3);
95
+ color: var(--color-on-surface-variant);
96
+ font-size: 0.75rem;
97
+ font-weight: 600;
98
+ letter-spacing: 0.08em;
99
+ text-transform: uppercase;
100
+ }
101
+
102
+ .sidebar__footer {
103
+ margin-top: auto;
104
+ border-top: 1px solid var(--color-outline-variant);
105
+ padding-top: var(--space-4);
106
+ }
107
+
108
+ .sidebar__footer-inner {
109
+ display: flex;
110
+ flex-direction: column;
111
+ align-items: flex-start;
112
+ gap: var(--space-3);
113
+ }
114
+
115
+ .sidebar-mobile {
116
+ display: block;
117
+ }
118
+
119
+ .sidebar-mobile__topbar {
120
+ position: sticky;
121
+ top: 0;
122
+ z-index: 10;
123
+ display: flex;
124
+ align-items: center;
125
+ justify-content: space-between;
126
+ gap: var(--space-3);
127
+ border-bottom: 1px solid var(--color-outline-variant);
128
+ background: var(--color-surface-highest);
129
+ padding: var(--space-3) var(--space-4);
130
+ }
131
+
132
+ .sidebar-mobile__brand .brand-lockup__logo {
133
+ width: 1.75rem;
134
+ height: 1.75rem;
135
+ border-radius: var(--radius-lg);
136
+ object-fit: cover;
137
+ }
138
+
139
+ .sidebar-mobile__brand .brand-lockup__wordmark {
140
+ color: var(--color-on-surface);
141
+ font-size: 1.125rem;
142
+ font-weight: 700;
143
+ }
144
+
145
+ .sidebar-mobile__panel {
146
+ position: fixed;
147
+ inset: 0;
148
+ z-index: 40;
149
+ display: flex;
150
+ }
151
+
152
+ .sidebar-mobile__scrim {
153
+ position: fixed;
154
+ inset: 0;
155
+ }
156
+
157
+ .sidebar-mobile__surface {
158
+ position: relative;
159
+ display: flex;
160
+ flex: 1 1 auto;
161
+ width: 100%;
162
+ max-width: 20rem;
163
+ flex-direction: column;
164
+ background: var(--color-surface-highest);
165
+ }
166
+
167
+ .sidebar-mobile__close-shell {
168
+ position: absolute;
169
+ top: 0;
170
+ right: 0;
171
+ margin-right: -3rem;
172
+ padding-top: var(--space-2);
173
+ }
174
+
175
+ .sidebar-mobile__close {
176
+ color: var(--color-inverse-on-surface);
177
+ }
178
+
179
+ .sidebar-mobile__close-icon {
180
+ width: 1.5rem;
181
+ height: 1.5rem;
182
+ }
183
+
184
+ .sidebar-mobile__content {
185
+ flex: 1 1 auto;
186
+ overflow-y: auto;
187
+ padding: var(--space-5) var(--space-4) var(--space-4);
188
+ }
189
+
190
+ .sidebar-mobile__slot--header {
191
+ margin-bottom: var(--space-5);
192
+ }
193
+
194
+ .sidebar-mobile__slot--footer {
195
+ border-top: 1px solid var(--color-outline-variant);
196
+ padding: var(--space-4);
197
+ }
198
+
199
+ .sidebar-mobile__nav {
200
+ display: flex;
201
+ flex-direction: column;
202
+ gap: var(--space-1);
203
+ }
204
+
205
+ .sidebar-mobile__link {
206
+ display: flex;
207
+ align-items: center;
208
+ gap: var(--space-3);
209
+ border-radius: var(--radius-lg);
210
+ padding: 0.625rem 0.75rem;
211
+ color: var(--color-text-soft);
212
+ font-size: 0.875rem;
213
+ font-weight: 500;
214
+ text-decoration: none;
215
+ }
216
+
217
+ .sidebar-mobile__link:hover,
218
+ .sidebar-mobile__link--active {
219
+ color: var(--color-primary);
220
+ background: color-mix(in srgb, var(--color-primary) 10%, transparent);
221
+ }
222
+
223
+ .sidebar-mobile__icon {
224
+ width: 1.25rem;
225
+ height: 1.25rem;
226
+ flex-shrink: 0;
227
+ }
228
+
229
+ .sidebar-mobile__section-divider {
230
+ margin-block: var(--space-5);
231
+ border-top: 1px solid var(--color-outline-variant);
232
+ }
233
+
234
+ .sidebar-mobile__section-label {
235
+ margin: 0 0 var(--space-2);
236
+ padding-inline: var(--space-3);
237
+ color: var(--color-on-surface-variant);
238
+ font-size: 0.75rem;
239
+ font-weight: 600;
240
+ letter-spacing: 0.08em;
241
+ text-transform: uppercase;
242
+ }
243
+
244
+ @media (min-width: 48rem) {
245
+ .sidebar-shell {
246
+ display: flex;
247
+ }
248
+
249
+ .sidebar-shell__desktop {
250
+ display: flex;
251
+ }
252
+
253
+ .sidebar-mobile {
254
+ display: none;
255
+ }
256
+ }
257
+
24
258
  .sidebar__toggle {
25
259
  cursor: pointer;
26
260
  pointer-events: auto;
@@ -6,6 +6,7 @@
6
6
  @import "./baldur/application/components/utilities.css";
7
7
  @import "./baldur/application/marketing/layout.css";
8
8
  @import "./baldur/application/components/app_bar.css";
9
+ @import "./baldur/application/components/auth-page.css";
9
10
  @import "./baldur/application/components/alert.css";
10
11
  @import "./baldur/application/components/button.css";
11
12
  @import "./baldur/application/components/card.css";
@@ -24,4 +25,5 @@
24
25
  @import "./baldur/application/components/stepper.css";
25
26
  @import "./baldur/application/components/switch.css";
26
27
  @import "./baldur/application/components/table.css";
28
+ @import "./baldur/application/components/confirmation.css";
27
29
  @import "./baldur/application/components/timeline.css";
@@ -4,6 +4,7 @@ module Baldur
4
4
  module UiHelper
5
5
  include Baldur::RenderHelper
6
6
  include Baldur::UiHelperFeedback
7
+ include Baldur::UiHelperSidebar
7
8
  include Baldur::UiHelperUnavailable
8
9
  include Baldur::UiHelperForms
9
10
 
@@ -164,6 +165,31 @@ module Baldur
164
165
  }.merge(options)
165
166
  end
166
167
 
168
+ def ui_modal_host(id:, classes: nil, &block)
169
+ body = block_given? ? capture(&block) : nil
170
+ baldur_render "baldur/components/modal_host",
171
+ id: id,
172
+ classes: classes,
173
+ body: body
174
+ end
175
+
176
+ def ui_confirmation_modal(host_id:, dialog_id:, title:, description: nil, tone: :default, confirm_label: "Confirm", cancel_label: "Cancel", confirm_button_options: {}, cancel_button_options: {}, type_to_confirm: nil, body: nil, &block)
177
+ body = block_given? ? capture(&block) : body
178
+ type_to_confirm = nil unless type_to_confirm.is_a?(Hash)
179
+ baldur_render "baldur/components/confirmation_modal",
180
+ host_id: host_id,
181
+ dialog_id: dialog_id,
182
+ title: title,
183
+ description: description,
184
+ tone: tone,
185
+ confirm_label: confirm_label,
186
+ cancel_label: cancel_label,
187
+ confirm_button_options: confirm_button_options,
188
+ cancel_button_options: cancel_button_options,
189
+ type_to_confirm: type_to_confirm,
190
+ body: body
191
+ end
192
+
167
193
  def ui_badge(text:, variant: :default, size: :sm, html_options: {})
168
194
  baldur_render "baldur/components/badge", text: text, variant: variant, size: size, html_options: html_options
169
195
  end
@@ -0,0 +1,104 @@
1
+ module Baldur
2
+ module UiHelperSidebar
3
+ include Baldur::RenderHelper
4
+ include Baldur::MarketingHelper
5
+
6
+ class SidebarBuilder
7
+ attr_reader :header_content, :footer_content, :mobile_header_content, :mobile_footer_content
8
+
9
+ def initialize(view_context)
10
+ @view_context = view_context
11
+ end
12
+
13
+ def with_header(&block)
14
+ @header_content = capture_slot(&block)
15
+ nil
16
+ end
17
+
18
+ def with_footer(&block)
19
+ @footer_content = capture_slot(&block)
20
+ nil
21
+ end
22
+
23
+ def with_mobile_header(&block)
24
+ @mobile_header_content = capture_slot(&block)
25
+ nil
26
+ end
27
+
28
+ def with_mobile_footer(&block)
29
+ @mobile_footer_content = capture_slot(&block)
30
+ nil
31
+ end
32
+
33
+ private
34
+
35
+ def capture_slot(&block)
36
+ return unless block_given?
37
+
38
+ @view_context.capture(&block)
39
+ end
40
+ end
41
+
42
+ def ui_sidebar(primary_links:, secondary_links: [], secondary_label: nil, brand_path: nil, brand_name: nil, brand_wordmark: nil, brand_logo: nil, header_content: nil, footer_content: nil, mobile_header_content: nil, mobile_footer_content: nil, shell_class: nil, &block)
43
+ builder = SidebarBuilder.new(self)
44
+ body_content = capture(builder, &block) if block_given?
45
+
46
+ baldur_render "baldur/components/sidebar",
47
+ brand: ui_sidebar_resolve_brand(
48
+ brand_path: brand_path,
49
+ brand_name: brand_name,
50
+ brand_wordmark: brand_wordmark,
51
+ brand_logo: brand_logo
52
+ ),
53
+ primary_links: ui_sidebar_normalize_links(primary_links),
54
+ secondary_links: ui_sidebar_normalize_links(secondary_links),
55
+ secondary_label: secondary_label,
56
+ header_content: builder.header_content.presence || header_content,
57
+ footer_content: builder.footer_content.presence || footer_content,
58
+ mobile_header_content: builder.mobile_header_content.presence || mobile_header_content || builder.header_content.presence || header_content,
59
+ mobile_footer_content: builder.mobile_footer_content.presence || mobile_footer_content || builder.footer_content.presence || footer_content,
60
+ shell_class: shell_class,
61
+ collapsed: ui_sidebar_collapsed?,
62
+ body_content: body_content
63
+ end
64
+
65
+ private
66
+
67
+ def ui_sidebar_resolve_brand(brand_path:, brand_name:, brand_wordmark:, brand_logo:)
68
+ overrides = {
69
+ name: brand_name,
70
+ wordmark: brand_wordmark,
71
+ logo_src: brand_logo,
72
+ href: brand_path.presence || "#"
73
+ }.compact
74
+
75
+ ui_marketing_resolve_brand(overrides)
76
+ end
77
+
78
+ def ui_sidebar_normalize_links(links)
79
+ Array(links).filter_map do |link|
80
+ next if link.blank?
81
+
82
+ normalized = link.to_h.symbolize_keys
83
+ next if normalized[:name].blank? || normalized[:path].blank?
84
+
85
+ {
86
+ name: normalized[:name].to_s,
87
+ path: normalized[:path],
88
+ icon: normalized[:icon].presence || "circle",
89
+ active: !!normalized[:active],
90
+ title: normalized[:title].presence || normalized[:name].to_s,
91
+ method: normalized[:method],
92
+ data: normalized[:data].respond_to?(:to_h) ? normalized[:data].to_h : nil,
93
+ html_options: normalized[:html_options].respond_to?(:to_h) ? normalized[:html_options].to_h.symbolize_keys : {}
94
+ }
95
+ end
96
+ end
97
+
98
+ def ui_sidebar_collapsed?
99
+ return false unless respond_to?(:cookies)
100
+
101
+ cookies["baldur-sidebar-collapsed"] == "true"
102
+ end
103
+ end
104
+ end
@@ -0,0 +1,99 @@
1
+ <%
2
+ host_id = local_assigns.fetch(:host_id)
3
+ dialog_id = local_assigns.fetch(:dialog_id)
4
+ title = local_assigns.fetch(:title)
5
+ description = local_assigns[:description]
6
+ tone = (local_assigns[:tone] || :default).to_sym
7
+ confirm_label = local_assigns.fetch(:confirm_label, "Confirm")
8
+ cancel_label = local_assigns.fetch(:cancel_label, "Cancel")
9
+ confirm_button_options = local_assigns.fetch(:confirm_button_options, {})
10
+ cancel_button_options = local_assigns.fetch(:cancel_button_options, {})
11
+ type_to_confirm = local_assigns[:type_to_confirm]
12
+ body = local_assigns.fetch(:body, nil) unless defined?(body)
13
+
14
+ confirm_variant = tone == :danger ? :danger : :primary
15
+ confirm_btn_opts = { label: confirm_label, variant: confirm_variant, type: :submit }.merge(confirm_button_options.except(:data))
16
+ if confirm_button_options[:data].present?
17
+ confirm_btn_opts[:data] = confirm_button_options[:data]
18
+ end
19
+
20
+ cancel_btn_opts = { label: cancel_label, variant: :ghost }.merge(cancel_button_options.except(:data))
21
+ cancel_btn_opts[:data] = { modal_close: true }.merge(cancel_button_options[:data] || {})
22
+
23
+ has_type_to_confirm = type_to_confirm.is_a?(Hash)
24
+ expected_text = has_type_to_confirm ? type_to_confirm[:expected_text].to_s : nil
25
+ confirm_input_id = has_type_to_confirm ? "#{dialog_id}-confirm-input" : nil
26
+ confirm_input_label = has_type_to_confirm ? type_to_confirm[:label].to_s : nil
27
+ confirm_input_placeholder = has_type_to_confirm ? (type_to_confirm[:placeholder].to_s.presence || expected_text) : nil
28
+ confirm_input_hint = has_type_to_confirm ? type_to_confirm[:hint] : nil
29
+ case_sensitive = has_type_to_confirm ? (type_to_confirm[:case_sensitive] != false) : true
30
+
31
+ if has_type_to_confirm
32
+ confirm_btn_opts[:disabled] = true
33
+ confirm_btn_opts[:data] = (confirm_btn_opts[:data] || {}).merge(confirmation_target: "submit")
34
+ end
35
+ %>
36
+ <div id="<%= host_id %>"
37
+ class="fixed inset-0 z-50 hidden flex items-center justify-center bg-black/40 p-4"
38
+ data-controller="modal<%= has_type_to_confirm ? " confirmation" : "" %>"
39
+ <%= has_type_to_confirm ? 'data-confirmation-case-sensitive-value="' + case_sensitive.to_s + '"' : '' %>
40
+ data-modal-selector-value="#<%= host_id %>"
41
+ data-modal="true"
42
+ aria-hidden="true">
43
+ <div id="<%= dialog_id %>" class="dialog motion-fade-scale">
44
+ <div class="flex items-start justify-between gap-4">
45
+ <div>
46
+ <h3 class="text-xl font-semibold text-[color:var(--color-on-surface)]">
47
+ <% if tone == :danger %>
48
+ <span class="inline-flex items-center gap-2">
49
+ <%= ui_icon("triangle-alert", class_name: "h-5 w-5 text-[color:var(--color-error)]") %>
50
+ <%= title %>
51
+ </span>
52
+ <% else %>
53
+ <%= title %>
54
+ <% end %>
55
+ </h3>
56
+ <% if description.present? %>
57
+ <p class="text-sm text-[color:color-mix(in srgb,var(--color-on-surface) 75%,transparent)]"><%= description %></p>
58
+ <% end %>
59
+ </div>
60
+ <button type="button" class="icon-button" aria-label="Close modal" data-modal-close="true">
61
+ <%= ui_icon("x", class_name: "h-5 w-5") %>
62
+ </button>
63
+ </div>
64
+ <% if body.present? %>
65
+ <div class="mt-4 space-y-3 text-sm text-[color:var(--color-on-surface)]">
66
+ <%= body %>
67
+ </div>
68
+ <% end %>
69
+ <% if has_type_to_confirm %>
70
+ <div class="mt-4 space-y-2">
71
+ <% if confirm_input_label.present? %>
72
+ <label for="<%= confirm_input_id %>" class="field__label text-sm font-medium text-[color:var(--color-on-surface)]"><%= confirm_input_label %></label>
73
+ <% end %>
74
+ <div class="text-field__input">
75
+ <input id="<%= confirm_input_id %>"
76
+ type="text"
77
+ autocomplete="off"
78
+ class="text-field__control"
79
+ data-confirmation-target="input"
80
+ data-action="input->confirmation#validate"
81
+ <%= confirm_input_placeholder.present? ? "placeholder=\"#{confirm_input_placeholder}\"".html_safe : "" %>
82
+ data-modal-autofocus="true" />
83
+ </div>
84
+ <% if confirm_input_hint.present? %>
85
+ <p class="text-sm text-muted"><%= confirm_input_hint %></p>
86
+ <% end %>
87
+ <% if expected_text.present? %>
88
+ <span hidden data-confirmation-target="expectedText"><%= expected_text %></span>
89
+ <% end %>
90
+ </div>
91
+ <% end %>
92
+ <div class="mt-6 flex flex-wrap items-center gap-3">
93
+ <div class="ml-auto flex flex-wrap gap-3 justify-end">
94
+ <%= render "baldur/components/button", **cancel_btn_opts %>
95
+ <%= render "baldur/components/button", **confirm_btn_opts %>
96
+ </div>
97
+ </div>
98
+ </div>
99
+ </div>
@@ -0,0 +1,10 @@
1
+ <% classes = local_assigns[:classes] %>
2
+ <% body = local_assigns.fetch(:body, nil) unless defined?(body) %>
3
+ <div id="<%= id %>"
4
+ class="fixed inset-0 z-50 hidden flex items-center justify-center bg-black/40 p-4 <%= classes %>"
5
+ data-controller="modal"
6
+ data-modal-selector-value="#<%= id %>"
7
+ data-modal="true"
8
+ aria-hidden="true">
9
+ <%= body %>
10
+ </div>
@@ -0,0 +1,158 @@
1
+ <%
2
+ resolved_brand = local_assigns.fetch(:brand).symbolize_keys
3
+ primary_links = Array(local_assigns[:primary_links]).compact
4
+ secondary_links = Array(local_assigns[:secondary_links]).compact
5
+ secondary_label = local_assigns[:secondary_label]
6
+ header_content = local_assigns[:header_content]
7
+ footer_content = local_assigns[:footer_content]
8
+ mobile_header_content = local_assigns[:mobile_header_content]
9
+ mobile_footer_content = local_assigns[:mobile_footer_content]
10
+ body_content = local_assigns[:body_content]
11
+ shell_class = local_assigns[:shell_class]
12
+ collapsed = !!local_assigns[:collapsed]
13
+
14
+ brand_markup = content_tag(:span, class: "brand-lockup inline-flex items-center gap-2 text-[color:var(--color-on-surface)]") do
15
+ safe_join(
16
+ [
17
+ image_tag(resolved_brand[:logo_src], alt: resolved_brand[:logo_alt], class: "brand-lockup__logo h-9 w-9 rounded-xl"),
18
+ content_tag(:span, resolved_brand[:wordmark], class: "brand-lockup__wordmark sidebar__brand-name text-lg font-semibold text-[color:var(--color-on-surface)]")
19
+ ]
20
+ )
21
+ end
22
+ brand_markup = link_to(brand_markup, resolved_brand[:href], class: "sidebar__brand-link") if resolved_brand[:href].present?
23
+
24
+ render_nav_link = lambda do |link, mobile: false|
25
+ options = link[:html_options].deep_dup
26
+ options[:title] = link[:title] if link[:title].present?
27
+ options[:data] = (options[:data] || {}).merge(link[:data] || {}) if link[:data].present?
28
+ options[:method] = link[:method] if link[:method].present?
29
+ options[:aria] = (options[:aria] || {}).merge(current: (link[:active] ? "page" : nil))
30
+
31
+ if mobile
32
+ mobile_class = link[:active] ? "sidebar-mobile__link sidebar-mobile__link--active" : "sidebar-mobile__link"
33
+ options[:class] = [mobile_class, options[:class]].compact.join(" ")
34
+ link_to(link[:path], **options) do
35
+ safe_join(
36
+ [
37
+ ui_icon(link[:icon], class_name: "sidebar-mobile__icon"),
38
+ content_tag(:span, link[:name])
39
+ ]
40
+ )
41
+ end
42
+ else
43
+ options[:class] = ["sidebar__link", options[:class]].compact.join(" ")
44
+ link_to(link[:path], **options) do
45
+ safe_join(
46
+ [
47
+ ui_icon(link[:icon], class_name: "sidebar__icon"),
48
+ content_tag(:span, link[:name], class: "sidebar__label")
49
+ ]
50
+ )
51
+ end
52
+ end
53
+ end
54
+ %>
55
+
56
+ <div class="<%= ["sidebar-shell", shell_class].compact.join(" ") %>"
57
+ data-controller="sidebar"
58
+ data-sidebar-storage-key-value="baldur-sidebar-collapsed"
59
+ data-sidebar-collapsed-value="<%= collapsed %>">
60
+ <aside class="sidebar sidebar-shell__desktop"
61
+ aria-label="Primary navigation">
62
+ <div class="sidebar__header">
63
+ <%= brand_markup %>
64
+ <button type="button"
65
+ class="icon-button sidebar__toggle"
66
+ aria-label="Toggle navigation"
67
+ aria-expanded="<%= (!collapsed).to_s %>"
68
+ data-sidebar-target="toggleButton"
69
+ data-action="sidebar#toggle">
70
+ <span aria-hidden="true" data-sidebar-target="toggleIcon"><%= ui_icon(collapsed ? "chevron-right" : "chevron-left", class_name: "h-5 w-5") %></span>
71
+ </button>
72
+ </div>
73
+
74
+ <% if header_content.present? %>
75
+ <div class="sidebar__menu sidebar__slot sidebar__slot--header">
76
+ <%= header_content %>
77
+ </div>
78
+ <% end %>
79
+
80
+ <nav class="sidebar__nav" aria-label="Primary navigation links">
81
+ <% primary_links.each do |link| %>
82
+ <%= render_nav_link.call(link) %>
83
+ <% end %>
84
+
85
+ <% if secondary_links.any? %>
86
+ <div class="sidebar__section-divider"></div>
87
+ <% if secondary_label.present? %>
88
+ <p class="sidebar__section-label"><%= secondary_label %></p>
89
+ <% end %>
90
+ <% secondary_links.each do |link| %>
91
+ <%= render_nav_link.call(link) %>
92
+ <% end %>
93
+ <% end %>
94
+ </nav>
95
+
96
+ <% if footer_content.present? %>
97
+ <div class="sidebar__footer">
98
+ <div class="sidebar__footer-inner">
99
+ <%= footer_content %>
100
+ </div>
101
+ </div>
102
+ <% end %>
103
+
104
+ <button type="button" class="sidebar__toggle-hit-area" aria-label="Toggle navigation" data-action="click->sidebar#toggle"></button>
105
+ </aside>
106
+
107
+ <div class="sidebar-mobile" data-controller="mobile-sidebar">
108
+ <div class="sidebar-mobile__topbar">
109
+ <div class="sidebar-mobile__brand"><%= brand_markup %></div>
110
+ <button type="button" class="icon-button" aria-label="Toggle navigation" data-action="mobile-sidebar#toggle">
111
+ <%= ui_icon("menu", class_name: "h-6 w-6") %>
112
+ </button>
113
+ </div>
114
+
115
+ <div id="mobile-sidebar" class="sidebar-mobile__panel hidden" data-mobile-sidebar-target="panel">
116
+ <div class="bg-scrim sidebar-mobile__scrim" data-action="click->mobile-sidebar#close"></div>
117
+ <div class="sidebar-mobile__surface">
118
+ <div class="sidebar-mobile__close-shell">
119
+ <button type="button" class="icon-button sidebar-mobile__close" aria-label="Close navigation" data-action="mobile-sidebar#close">
120
+ <%= ui_icon("x", class_name: "sidebar-mobile__close-icon") %>
121
+ </button>
122
+ </div>
123
+
124
+ <div class="sidebar-mobile__content">
125
+ <% if mobile_header_content.present? %>
126
+ <div class="sidebar-mobile__slot sidebar-mobile__slot--header"><%= mobile_header_content %></div>
127
+ <% end %>
128
+
129
+ <nav class="sidebar-mobile__nav" aria-label="Mobile primary navigation links">
130
+ <% primary_links.each do |link| %>
131
+ <%= render_nav_link.call(link, mobile: true) %>
132
+ <% end %>
133
+
134
+ <% if secondary_links.any? %>
135
+ <div class="sidebar-mobile__section-divider"></div>
136
+ <% if secondary_label.present? %>
137
+ <p class="sidebar-mobile__section-label"><%= secondary_label %></p>
138
+ <% end %>
139
+ <% secondary_links.each do |link| %>
140
+ <%= render_nav_link.call(link, mobile: true) %>
141
+ <% end %>
142
+ <% end %>
143
+ </nav>
144
+ </div>
145
+
146
+ <% if mobile_footer_content.present? %>
147
+ <div class="sidebar-mobile__slot sidebar-mobile__slot--footer">
148
+ <%= mobile_footer_content %>
149
+ </div>
150
+ <% end %>
151
+ </div>
152
+ </div>
153
+ </div>
154
+
155
+ <div class="sidebar-shell__main">
156
+ <%= body_content %>
157
+ </div>
158
+ </div>
@@ -1,12 +1,12 @@
1
1
  <%
2
2
  resolved_brand_path = local_assigns[:brand_path].presence || "#"
3
- auth_shell_classes = ["auth-page", "flex min-h-screen items-center justify-center bg-surface-low px-6 py-12", local_assigns[:shell_class]].compact.join(" ")
3
+ auth_shell_classes = ["auth-page", local_assigns[:shell_class]].compact.join(" ")
4
4
  auth_card_class = local_assigns[:card_class]
5
5
  %>
6
6
 
7
7
  <div class="<%= auth_shell_classes %>">
8
- <div class="mx-auto max-w-lg">
9
- <div class="mb-6 flex justify-center">
8
+ <div class="auth-page__container">
9
+ <div class="auth-page__brand">
10
10
  <%= link_to resolved_brand_path, class: "inline-flex items-center justify-center no-underline" do %>
11
11
  <%= render "shared/brand_lockup",
12
12
  logo_class: "h-10 w-10 rounded-xl",