kozenet_ui 0.1.4 → 0.1.6
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/CHANGELOG.md +11 -0
- data/README.md +79 -16
- data/app/assets/fonts/kozenet_ui/inter-latin.woff2 +0 -0
- data/app/assets/fonts/kozenet_ui/jetbrains-mono-latin.woff2 +0 -0
- data/app/assets/fonts/kozenet_ui/source-serif-4-latin.woff2 +0 -0
- data/app/assets/javascripts/kozenet_ui/index.js +226 -17
- data/app/assets/stylesheets/kozenet_ui/base.css +5 -0
- data/app/assets/stylesheets/kozenet_ui/components/avatar.css +4 -1
- data/app/assets/stylesheets/kozenet_ui/components/badge.css +11 -1
- data/app/assets/stylesheets/kozenet_ui/components/button.css +21 -1
- data/app/assets/stylesheets/kozenet_ui/components/header.css +227 -38
- data/app/assets/stylesheets/kozenet_ui/components/utilities.css +52 -1
- data/app/assets/stylesheets/kozenet_ui/fonts.css +37 -0
- data/app/assets/stylesheets/kozenet_ui/tokens.css +150 -53
- data/app/components/kozenet_ui/base_component.rb +21 -1
- data/app/components/kozenet_ui/header_component/action_button_component.html.erb +4 -2
- data/app/components/kozenet_ui/header_component/action_button_component.rb +78 -4
- data/app/components/kozenet_ui/header_component/nav_item_component.html.erb +1 -1
- data/app/components/kozenet_ui/header_component/nav_item_component.rb +6 -0
- data/app/components/kozenet_ui/header_component/user_menu_component.html.erb +5 -7
- data/app/components/kozenet_ui/header_component.html.erb +39 -30
- data/app/components/kozenet_ui/header_component.rb +26 -4
- data/app/helpers/kozenet_ui/component_helper.rb +82 -8
- data/app/helpers/kozenet_ui/icon_helper.rb +11 -7
- data/docs/README.md +25 -0
- data/docs/components/README.md +44 -0
- data/docs/components/avatar.md +73 -0
- data/docs/components/badge.md +74 -0
- data/docs/components/button.md +95 -0
- data/docs/components/header.md +199 -0
- data/docs/foundations/README.md +14 -0
- data/docs/foundations/fonts.md +136 -0
- data/lib/generators/kozenet_ui/install/install_generator.rb +94 -8
- data/lib/generators/kozenet_ui/install/templates/kozenet_ui.rb +3 -0
- data/lib/kozenet_ui/configuration.rb +35 -0
- data/lib/kozenet_ui/engine.rb +89 -14
- data/lib/kozenet_ui/theme/palette.rb +21 -7
- data/lib/kozenet_ui/theme/variants.rb +1 -0
- data/lib/kozenet_ui/version.rb +1 -1
- data/lib/kozenet_ui.rb +2 -0
- metadata +34 -13
- data/app/assets/images/kozenet_ui/icons/cart.svg +0 -1
- data/app/assets/images/kozenet_ui/icons/heart.svg +0 -1
|
@@ -3,6 +3,8 @@
|
|
|
3
3
|
|
|
4
4
|
@layer base {
|
|
5
5
|
:root {
|
|
6
|
+
color-scheme: light;
|
|
7
|
+
|
|
6
8
|
/* Spacing */
|
|
7
9
|
--kz-spacing-xs: 0.25rem;
|
|
8
10
|
--kz-spacing-sm: 0.5rem;
|
|
@@ -54,6 +56,7 @@
|
|
|
54
56
|
|
|
55
57
|
/* Default Light Mode Colors */
|
|
56
58
|
--kz-bg-base: #ffffff;
|
|
59
|
+
--kz-bg-default: var(--kz-bg-base);
|
|
57
60
|
--kz-bg-elevated: #f8fafc;
|
|
58
61
|
--kz-bg-muted: #f1f5f9;
|
|
59
62
|
--kz-text-default: #0f172a;
|
|
@@ -69,15 +72,15 @@
|
|
|
69
72
|
--kz-cta-text: #fff;
|
|
70
73
|
--kz-cta-text-dark: #fff;
|
|
71
74
|
|
|
72
|
-
--bg-default:
|
|
73
|
-
--bg-muted:
|
|
74
|
-
--bg-elevated:
|
|
75
|
+
--bg-default: var(--kz-bg-base);
|
|
76
|
+
--bg-muted: var(--kz-bg-muted);
|
|
77
|
+
--bg-elevated: var(--kz-bg-elevated);
|
|
75
78
|
--bg-accent: #0ea5e9;
|
|
76
79
|
--bg-accent-hover: #0284c7;
|
|
77
|
-
--text-default:
|
|
78
|
-
--text-muted:
|
|
79
|
-
--border-default:
|
|
80
|
-
--border-strong:
|
|
80
|
+
--text-default: var(--kz-text-default);
|
|
81
|
+
--text-muted: var(--kz-text-muted);
|
|
82
|
+
--border-default: var(--kz-border-default);
|
|
83
|
+
--border-strong: var(--kz-border-default);
|
|
81
84
|
--focus-ring: #0ea5e9;
|
|
82
85
|
--radius-xs: 2px;
|
|
83
86
|
--radius-sm: 4px;
|
|
@@ -98,26 +101,28 @@
|
|
|
98
101
|
/* Dark Mode */
|
|
99
102
|
[data-theme="dark"],
|
|
100
103
|
.dark {
|
|
101
|
-
|
|
102
|
-
--kz-bg-
|
|
103
|
-
--kz-bg-
|
|
104
|
-
--kz-
|
|
105
|
-
--kz-
|
|
106
|
-
--kz-
|
|
107
|
-
--kz-
|
|
104
|
+
color-scheme: dark;
|
|
105
|
+
--kz-bg-base: #101318;
|
|
106
|
+
--kz-bg-default: var(--kz-bg-base);
|
|
107
|
+
--kz-bg-elevated: #181c23;
|
|
108
|
+
--kz-bg-muted: #242a35;
|
|
109
|
+
--kz-text-default: #f8fafc;
|
|
110
|
+
--kz-text-muted: #cbd5e1;
|
|
111
|
+
--kz-border-default: #303744;
|
|
112
|
+
--kz-border-muted: #242a35;
|
|
108
113
|
|
|
109
114
|
--kz-cta-gradient: var(--kz-cta-gradient-dark);
|
|
110
115
|
--kz-cta-text: var(--kz-cta-text-dark);
|
|
111
116
|
|
|
112
|
-
--bg-default:
|
|
113
|
-
--bg-muted:
|
|
114
|
-
--bg-elevated:
|
|
117
|
+
--bg-default: var(--kz-bg-base);
|
|
118
|
+
--bg-muted: var(--kz-bg-muted);
|
|
119
|
+
--bg-elevated: var(--kz-bg-elevated);
|
|
115
120
|
--bg-accent: #0284c7;
|
|
116
121
|
--bg-accent-hover: #0369a1;
|
|
117
|
-
--text-default:
|
|
118
|
-
--text-muted:
|
|
119
|
-
--border-default:
|
|
120
|
-
--border-strong:
|
|
122
|
+
--text-default: var(--kz-text-default);
|
|
123
|
+
--text-muted: var(--kz-text-muted);
|
|
124
|
+
--border-default: var(--kz-border-default);
|
|
125
|
+
--border-strong: var(--kz-border-default);
|
|
121
126
|
--focus-ring: #0ea5e9;
|
|
122
127
|
--shadow-color: 210 40% 2%;
|
|
123
128
|
--gradient-base-from: #0b0d11;
|
|
@@ -129,40 +134,132 @@
|
|
|
129
134
|
--gradient-spot-2: rgba(99,102,241,0.18);
|
|
130
135
|
}
|
|
131
136
|
|
|
137
|
+
@media (prefers-color-scheme: dark) {
|
|
138
|
+
:root:not([data-theme="light"]):not(.light) {
|
|
139
|
+
color-scheme: dark;
|
|
140
|
+
--kz-bg-base: #101318;
|
|
141
|
+
--kz-bg-default: var(--kz-bg-base);
|
|
142
|
+
--kz-bg-elevated: #181c23;
|
|
143
|
+
--kz-bg-muted: #242a35;
|
|
144
|
+
--kz-text-default: #f8fafc;
|
|
145
|
+
--kz-text-muted: #cbd5e1;
|
|
146
|
+
--kz-border-default: #303744;
|
|
147
|
+
--kz-border-muted: #242a35;
|
|
148
|
+
|
|
149
|
+
--kz-cta-gradient: var(--kz-cta-gradient-dark);
|
|
150
|
+
--kz-cta-text: var(--kz-cta-text-dark);
|
|
151
|
+
|
|
152
|
+
--bg-default: var(--kz-bg-base);
|
|
153
|
+
--bg-muted: var(--kz-bg-muted);
|
|
154
|
+
--bg-elevated: var(--kz-bg-elevated);
|
|
155
|
+
--bg-accent: #0284c7;
|
|
156
|
+
--bg-accent-hover: #0369a1;
|
|
157
|
+
--text-default: var(--kz-text-default);
|
|
158
|
+
--text-muted: var(--kz-text-muted);
|
|
159
|
+
--border-default: var(--kz-border-default);
|
|
160
|
+
--border-strong: var(--kz-border-default);
|
|
161
|
+
--focus-ring: #0ea5e9;
|
|
162
|
+
--shadow-color: 210 40% 2%;
|
|
163
|
+
--gradient-base-from: #0b0d11;
|
|
164
|
+
--gradient-base-to: #11151c;
|
|
165
|
+
--gradient-accent-from: #1e40af;
|
|
166
|
+
--gradient-accent-via: #0369a1;
|
|
167
|
+
--gradient-accent-to: #0891b2;
|
|
168
|
+
--gradient-spot-1: rgba(56,189,248,0.20);
|
|
169
|
+
--gradient-spot-2: rgba(99,102,241,0.18);
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
[data-theme="light"],
|
|
174
|
+
.light {
|
|
175
|
+
color-scheme: light;
|
|
176
|
+
--kz-bg-base: #ffffff;
|
|
177
|
+
--kz-bg-default: var(--kz-bg-base);
|
|
178
|
+
--kz-bg-elevated: #f8fafc;
|
|
179
|
+
--kz-bg-muted: #f1f5f9;
|
|
180
|
+
--kz-text-default: #0f172a;
|
|
181
|
+
--kz-text-muted: #64748b;
|
|
182
|
+
--kz-border-default: #e2e8f0;
|
|
183
|
+
--kz-border-muted: #f1f5f9;
|
|
184
|
+
|
|
185
|
+
--kz-cta-gradient: linear-gradient(110deg, #6366f1, #0ea5e9 55%, #06b6d4);
|
|
186
|
+
--kz-cta-text: #fff;
|
|
187
|
+
|
|
188
|
+
--bg-default: var(--kz-bg-base);
|
|
189
|
+
--bg-muted: var(--kz-bg-muted);
|
|
190
|
+
--bg-elevated: var(--kz-bg-elevated);
|
|
191
|
+
--bg-accent: #0ea5e9;
|
|
192
|
+
--bg-accent-hover: #0284c7;
|
|
193
|
+
--text-default: var(--kz-text-default);
|
|
194
|
+
--text-muted: var(--kz-text-muted);
|
|
195
|
+
--border-default: var(--kz-border-default);
|
|
196
|
+
--border-strong: var(--kz-border-default);
|
|
197
|
+
--focus-ring: #0ea5e9;
|
|
198
|
+
--shadow-color: 210 40% 2%;
|
|
199
|
+
--gradient-base-from: #f0f9ff;
|
|
200
|
+
--gradient-base-to: #e0f2fe;
|
|
201
|
+
--gradient-accent-from: #6366f1;
|
|
202
|
+
--gradient-accent-via: #0ea5e9;
|
|
203
|
+
--gradient-accent-to: #06b6d4;
|
|
204
|
+
--gradient-spot-1: rgba(99,102,241,0.35);
|
|
205
|
+
--gradient-spot-2: rgba(14,165,233,0.30);
|
|
206
|
+
}
|
|
207
|
+
|
|
132
208
|
html { -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale; }
|
|
133
209
|
body { background: var(--bg-default); color: var(--text-default); font-family: var(--font-sans, ui-sans-serif, system-ui, sans-serif); }
|
|
134
210
|
* { border-color: var(--border-default); }
|
|
135
211
|
::selection { background: var(--bg-accent); color: #fff; }
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
/* ── App Background Utility ────────────────────────────────────────────
|
|
215
|
+
* Lives OUTSIDE @layer so its resets always beat browser defaults and
|
|
216
|
+
* un-layered host-app CSS (e.g. Rails' default `body { margin: 8px }`).
|
|
217
|
+
* This is intentional: .app-bg is an opt-in utility, and when a consumer
|
|
218
|
+
* applies it to <body>, it must guarantee zero margin/padding so that
|
|
219
|
+
* sticky headers, full-bleed sections, etc. work edge-to-edge.
|
|
220
|
+
* ──────────────────────────────────────────────────────────────────── */
|
|
221
|
+
.app-bg {
|
|
222
|
+
position: relative;
|
|
223
|
+
overflow-x: hidden;
|
|
224
|
+
isolation: isolate;
|
|
225
|
+
margin: 0;
|
|
226
|
+
padding: 0;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
.app-bg:not(:first-of-type) { background: none !important; }
|
|
230
|
+
|
|
231
|
+
.app-bg::before {
|
|
232
|
+
content: "";
|
|
233
|
+
position: fixed; inset: 0; pointer-events: none; z-index: -1;
|
|
234
|
+
background:
|
|
235
|
+
radial-gradient(circle at 18% 22%, var(--gradient-spot-1) 0, transparent 60%),
|
|
236
|
+
radial-gradient(circle at 82% 18%, var(--gradient-spot-2) 0, transparent 62%),
|
|
237
|
+
linear-gradient(135deg, var(--gradient-base-from) 0%, var(--gradient-base-to) 100%);
|
|
238
|
+
background-attachment: fixed;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
.dark .app-bg::before {
|
|
242
|
+
background:
|
|
243
|
+
radial-gradient(circle at 18% 22%, var(--gradient-spot-1) 0, transparent 60%),
|
|
244
|
+
radial-gradient(circle at 82% 18%, var(--gradient-spot-2) 0, transparent 62%),
|
|
245
|
+
linear-gradient(140deg, var(--gradient-base-from) 0%, var(--gradient-base-to) 100%);
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
.app-bg::after {
|
|
249
|
+
content: "";
|
|
250
|
+
position: fixed; inset: 0; pointer-events: none; z-index: -1; mix-blend-mode: overlay;
|
|
251
|
+
background: linear-gradient(120deg, var(--gradient-accent-from), var(--gradient-accent-via) 55%, var(--gradient-accent-to));
|
|
252
|
+
opacity: .07; border-radius: inherit;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
.dark .app-bg::after { opacity: .10; }
|
|
256
|
+
|
|
257
|
+
@media (prefers-color-scheme: dark) {
|
|
258
|
+
:root:not([data-theme="light"]):not(.light) .app-bg::after {
|
|
259
|
+
opacity: .10;
|
|
167
260
|
}
|
|
168
|
-
}
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
@supports (-webkit-touch-callout: none) {
|
|
264
|
+
.app-bg::before, .app-bg::after { position: absolute; }
|
|
265
|
+
}
|
|
@@ -4,6 +4,8 @@ module KozenetUi
|
|
|
4
4
|
# Base component that all Kozenet UI components inherit from
|
|
5
5
|
# Provides common functionality for variant handling, class merging, etc.
|
|
6
6
|
class BaseComponent < ViewComponent::Base
|
|
7
|
+
UNSET = Object.new.freeze
|
|
8
|
+
|
|
7
9
|
attr_reader :variant, :size, :html_options
|
|
8
10
|
|
|
9
11
|
def initialize(variant: nil, size: nil, class: nil, **html_options)
|
|
@@ -78,7 +80,25 @@ module KozenetUi
|
|
|
78
80
|
|
|
79
81
|
# Stimulus controller name with prefix
|
|
80
82
|
def stimulus_controller(name)
|
|
81
|
-
"#{KozenetUi.configuration.stimulus_prefix}-#{name}"
|
|
83
|
+
"#{KozenetUi.configuration.stimulus_prefix}-#{name.to_s.tr("_", "-")}"
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
def stimulus_controllers(*names)
|
|
87
|
+
names.map { |name| stimulus_controller(name) }.join(" ")
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
def stimulus_action(controller, action, event:)
|
|
91
|
+
"#{event}->#{stimulus_controller(controller)}##{action}"
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
def stimulus_target(controller, target)
|
|
95
|
+
{ "data-#{stimulus_controller(controller)}-target" => target.to_s.tr("_", "-") }
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
def component_option(component, option, value = UNSET, fallback: nil)
|
|
99
|
+
return value unless value.equal?(UNSET)
|
|
100
|
+
|
|
101
|
+
KozenetUi.configuration.component_defaults_for(component).fetch(option.to_sym, fallback)
|
|
82
102
|
end
|
|
83
103
|
end
|
|
84
104
|
end
|
|
@@ -1,11 +1,13 @@
|
|
|
1
1
|
<%# frozen_string_literal: true %>
|
|
2
2
|
<%# ActionButtonComponent template for KozenetUi::HeaderComponent %>
|
|
3
|
-
<%= tag.a
|
|
3
|
+
<%= tag.a(**action_button_attrs) do %>
|
|
4
4
|
<% if @icon %>
|
|
5
5
|
<span class="kz-action-btn-icon"><%= render_icon(@icon) %></span>
|
|
6
6
|
<% end %>
|
|
7
7
|
<% if @label %>
|
|
8
8
|
<span class="kz-action-btn-label"><%= @label %></span>
|
|
9
9
|
<% end %>
|
|
10
|
-
|
|
10
|
+
<% if visible_text? %>
|
|
11
|
+
<span class="kz-action-btn-text"><%= visible_text %></span>
|
|
12
|
+
<% end %>
|
|
11
13
|
<% end %>
|
|
@@ -6,23 +6,97 @@ module KozenetUi
|
|
|
6
6
|
# Renders an action button (icon or text) in the header
|
|
7
7
|
#
|
|
8
8
|
# @example
|
|
9
|
-
# <%= render KozenetUi::HeaderComponent::ActionButtonComponent.new(href: "/cart", icon: :
|
|
9
|
+
# <%= render KozenetUi::HeaderComponent::ActionButtonComponent.new(href: "/cart", icon: :shopping_cart, label: "Cart") %>
|
|
10
10
|
class ActionButtonComponent < BaseComponent
|
|
11
|
-
|
|
11
|
+
PLACEMENTS = %i[start end].freeze
|
|
12
|
+
PLACEMENT_ALIASES = {
|
|
13
|
+
before: :start,
|
|
14
|
+
left: :start,
|
|
15
|
+
right: :end,
|
|
16
|
+
after: :end
|
|
17
|
+
}.freeze
|
|
18
|
+
VISIBILITIES = %i[always desktop mobile].freeze
|
|
19
|
+
VISIBILITY_ALIASES = {
|
|
20
|
+
all: :always,
|
|
21
|
+
both: :always
|
|
22
|
+
}.freeze
|
|
23
|
+
|
|
24
|
+
attr_reader :placement
|
|
25
|
+
attr_reader :visible_on
|
|
26
|
+
|
|
27
|
+
def initialize(
|
|
28
|
+
href: "#",
|
|
29
|
+
icon: nil,
|
|
30
|
+
text: nil,
|
|
31
|
+
label: nil,
|
|
32
|
+
placement: :end,
|
|
33
|
+
position: nil,
|
|
34
|
+
visible_on: :desktop,
|
|
35
|
+
**html_options
|
|
36
|
+
)
|
|
12
37
|
super(**html_options)
|
|
13
38
|
@href = href
|
|
14
39
|
@icon = icon
|
|
40
|
+
@text = text
|
|
15
41
|
@label = label
|
|
42
|
+
@placement = normalize_placement(position || placement)
|
|
43
|
+
@visible_on = normalize_visibility(visible_on)
|
|
16
44
|
end
|
|
17
45
|
|
|
18
46
|
private
|
|
19
47
|
|
|
48
|
+
def action_button_attrs
|
|
49
|
+
attrs = html_options.merge(
|
|
50
|
+
href: @href,
|
|
51
|
+
class: action_button_classes
|
|
52
|
+
)
|
|
53
|
+
attrs[:aria] = action_button_aria if action_button_aria.any?
|
|
54
|
+
attrs
|
|
55
|
+
end
|
|
56
|
+
|
|
20
57
|
def action_button_classes
|
|
21
|
-
"kz-action-btn"
|
|
58
|
+
classes = ["kz-action-btn"]
|
|
59
|
+
classes << "kz-action-btn-with-text" if visible_text?
|
|
60
|
+
classes << "kz-action-btn-placement-#{placement}"
|
|
61
|
+
classes << "kz-action-visible-#{visible_on}"
|
|
62
|
+
classes << @custom_class if defined?(@custom_class) && @custom_class
|
|
63
|
+
classes.join(" ")
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def action_button_aria
|
|
67
|
+
aria = html_options.fetch(:aria, {}).dup
|
|
68
|
+
aria[:label] = @label if @label.present?
|
|
69
|
+
aria
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def visible_text
|
|
73
|
+
@text.presence || (content if content.present?)
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
def visible_text?
|
|
77
|
+
visible_text.present?
|
|
22
78
|
end
|
|
23
79
|
|
|
24
80
|
def render_icon(icon)
|
|
25
|
-
|
|
81
|
+
return unless icon
|
|
82
|
+
|
|
83
|
+
helpers.kozenet_ui_icon(icon, class: "kz-action-btn-svg", size: 20)
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
def normalize_placement(value)
|
|
87
|
+
normalized_value = value.to_s.tr("-", "_").to_sym
|
|
88
|
+
normalized_value = PLACEMENT_ALIASES.fetch(normalized_value, normalized_value)
|
|
89
|
+
return normalized_value if PLACEMENTS.include?(normalized_value)
|
|
90
|
+
|
|
91
|
+
raise ArgumentError, "Unknown header action placement `#{value}`. Use :start or :end."
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
def normalize_visibility(value)
|
|
95
|
+
normalized_value = value.to_s.tr("-", "_").to_sym
|
|
96
|
+
normalized_value = VISIBILITY_ALIASES.fetch(normalized_value, normalized_value)
|
|
97
|
+
return normalized_value if VISIBILITIES.include?(normalized_value)
|
|
98
|
+
|
|
99
|
+
raise ArgumentError, "Unknown header action visibility `#{value}`. Use :always, :desktop, or :mobile."
|
|
26
100
|
end
|
|
27
101
|
end
|
|
28
102
|
end
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
<%# frozen_string_literal: true %>
|
|
2
2
|
<%# NavItemComponent template for KozenetUi::HeaderComponent %>
|
|
3
|
-
<%= tag.a href: @href, class: nav_item_classes, role: (@dropdown ? "button" : nil), data:
|
|
3
|
+
<%= tag.a href: @href, class: nav_item_classes, role: (@dropdown ? "button" : nil), data: nav_item_data do %>
|
|
4
4
|
<%= content %>
|
|
5
5
|
<% if @dropdown %>
|
|
6
6
|
<svg class="ml-1 opacity-60" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="m6 9 6 6 6-6"/></svg>
|
|
@@ -23,6 +23,12 @@ module KozenetUi
|
|
|
23
23
|
classes << @custom_class if defined?(@custom_class) && @custom_class
|
|
24
24
|
classes.join(" ")
|
|
25
25
|
end
|
|
26
|
+
|
|
27
|
+
def nav_item_data
|
|
28
|
+
return {} unless @dropdown
|
|
29
|
+
|
|
30
|
+
{ action: stimulus_action(:dropdown, :toggle, event: :click) }
|
|
31
|
+
end
|
|
26
32
|
end
|
|
27
33
|
end
|
|
28
34
|
end
|
|
@@ -1,18 +1,16 @@
|
|
|
1
1
|
<%# frozen_string_literal: true %>
|
|
2
2
|
<%# UserMenuComponent template for KozenetUi::HeaderComponent %>
|
|
3
|
-
<div class="kz-user-menu
|
|
4
|
-
<button type="button" class="kz-avatar-btn
|
|
3
|
+
<div class="kz-user-menu" data-controller="<%= stimulus_controller(:user_menu) %>">
|
|
4
|
+
<button type="button" class="kz-avatar-btn" data-action="<%= stimulus_action(:user_menu, :toggle, event: :click) %>">
|
|
5
5
|
<% if @avatar_url.present? %>
|
|
6
|
-
<img src="<%= @avatar_url %>" alt="<%= @user_name %>"
|
|
6
|
+
<img src="<%= @avatar_url %>" alt="<%= @user_name %>" />
|
|
7
7
|
<% else %>
|
|
8
|
-
<span class="
|
|
8
|
+
<span class="kz-user-initial">
|
|
9
9
|
<%= @user_name.to_s.first.upcase %>
|
|
10
10
|
</span>
|
|
11
11
|
<% end %>
|
|
12
|
-
<span class="kz-user-name"><%= @user_name %></span>
|
|
13
|
-
<svg class="ml-1 w-4 h-4" viewBox="0 0 20 20" fill="currentColor"><path d="M5.23 7.21a.75.75 0 0 1 1.06.02L10 11.06l3.71-3.83a.75.75 0 1 1 1.08 1.04l-4.25 4.39a.75.75 0 0 1-1.08 0L5.21 8.27a.75.75 0 0 1 .02-1.06z"/></svg>
|
|
14
12
|
</button>
|
|
15
|
-
<div class="kz-user-dropdown
|
|
13
|
+
<div class="kz-user-dropdown hidden" <%= tag.attributes(stimulus_target(:user_menu, :dropdown)) %>>
|
|
16
14
|
<%= content %>
|
|
17
15
|
</div>
|
|
18
16
|
</div>
|
|
@@ -1,10 +1,22 @@
|
|
|
1
|
-
<header
|
|
2
|
-
|
|
1
|
+
<header
|
|
2
|
+
class="<%= component_classes %>"
|
|
3
|
+
data-controller="<%= stimulus_controllers(:header, :mobile_nav) %>"
|
|
4
|
+
data-action="<%= stimulus_action(:header, :handleScroll, event: "scroll@window") %>"
|
|
5
|
+
>
|
|
6
|
+
<div class="kz-header-bar" <%= tag.attributes(stimulus_target(:header, :container)) %>>
|
|
3
7
|
<!-- Brand + Navigation -->
|
|
4
8
|
<div class="kz-header-start">
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
9
|
+
<% if start_action_buttons? %>
|
|
10
|
+
<div class="kz-header-start-actions">
|
|
11
|
+
<% start_action_buttons.each do |button| %>
|
|
12
|
+
<%= button %>
|
|
13
|
+
<% end %>
|
|
14
|
+
</div>
|
|
15
|
+
<% end %>
|
|
16
|
+
|
|
17
|
+
<% if brand? %>
|
|
18
|
+
<%= brand %>
|
|
19
|
+
<% end %>
|
|
8
20
|
<% if nav_items? %>
|
|
9
21
|
<nav class="kz-nav-links" aria-label="Primary navigation">
|
|
10
22
|
<% nav_items.each do |item| %>
|
|
@@ -25,22 +37,19 @@
|
|
|
25
37
|
<div class="kz-header-actions">
|
|
26
38
|
<!-- Mobile search trigger -->
|
|
27
39
|
<% if search? %>
|
|
28
|
-
<button
|
|
29
|
-
type="button"
|
|
30
|
-
class="kz-action-btn md:hidden"
|
|
40
|
+
<button
|
|
41
|
+
type="button"
|
|
42
|
+
class="kz-action-btn md:hidden"
|
|
31
43
|
aria-label="Search"
|
|
32
|
-
data-action="
|
|
44
|
+
data-action="<%= stimulus_action(:header, :toggleSearch, event: :click) %>"
|
|
33
45
|
>
|
|
34
|
-
|
|
35
|
-
<circle cx="11" cy="11" r="8"/>
|
|
36
|
-
<path d="m21 21-4.35-4.35"/>
|
|
37
|
-
</svg>
|
|
46
|
+
<%= helpers.kozenet_ui_icon(:magnifying_glass, class: "kz-action-btn-svg", size: 20) %>
|
|
38
47
|
</button>
|
|
39
48
|
<% end %>
|
|
40
49
|
|
|
41
50
|
<!-- Action buttons -->
|
|
42
|
-
<% if
|
|
43
|
-
<%
|
|
51
|
+
<% if end_action_buttons? %>
|
|
52
|
+
<% end_action_buttons.each do |button| %>
|
|
44
53
|
<%= button %>
|
|
45
54
|
<% end %>
|
|
46
55
|
<% end %>
|
|
@@ -53,29 +62,29 @@
|
|
|
53
62
|
<% end %>
|
|
54
63
|
|
|
55
64
|
<!-- Mobile menu trigger -->
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
</
|
|
66
|
-
|
|
65
|
+
<% if mobile_menu? %>
|
|
66
|
+
<button
|
|
67
|
+
class="kz-mobile-trigger"
|
|
68
|
+
type="button"
|
|
69
|
+
aria-label="Menu"
|
|
70
|
+
data-action="<%= stimulus_action(:mobile_nav, :toggle, event: :click) %>"
|
|
71
|
+
<%= tag.attributes(stimulus_target(:mobile_nav, :trigger)) %>
|
|
72
|
+
>
|
|
73
|
+
<%= helpers.kozenet_ui_icon(:bars_3, class: "kz-icon-menu", size: 20) %>
|
|
74
|
+
</button>
|
|
75
|
+
<% end %>
|
|
67
76
|
</div>
|
|
68
77
|
</div>
|
|
69
78
|
|
|
70
79
|
<!-- Mobile panel -->
|
|
71
80
|
<% if mobile_menu? %>
|
|
72
|
-
<div
|
|
73
|
-
class="kz-mobile-panel hidden scale-y-0 origin-top"
|
|
74
|
-
|
|
81
|
+
<div
|
|
82
|
+
class="kz-mobile-panel hidden scale-y-0 origin-top"
|
|
83
|
+
<%= tag.attributes(stimulus_target(:mobile_nav, :panel)) %>
|
|
75
84
|
role="dialog"
|
|
76
85
|
aria-label="Mobile navigation"
|
|
77
86
|
>
|
|
78
87
|
<%= mobile_menu %>
|
|
79
88
|
</div>
|
|
80
89
|
<% end %>
|
|
81
|
-
</header>
|
|
90
|
+
</header>
|
|
@@ -19,13 +19,13 @@ module KozenetUi
|
|
|
19
19
|
renders_one :mobile_menu
|
|
20
20
|
|
|
21
21
|
def initialize(
|
|
22
|
-
sticky:
|
|
23
|
-
blur:
|
|
22
|
+
sticky: BaseComponent::UNSET,
|
|
23
|
+
blur: BaseComponent::UNSET,
|
|
24
24
|
**html_options
|
|
25
25
|
)
|
|
26
26
|
super(**html_options)
|
|
27
|
-
@sticky = sticky
|
|
28
|
-
@blur = blur
|
|
27
|
+
@sticky = component_option(:header, :sticky, sticky, fallback: true)
|
|
28
|
+
@blur = component_option(:header, :blur, blur, fallback: true)
|
|
29
29
|
end
|
|
30
30
|
|
|
31
31
|
private
|
|
@@ -36,5 +36,27 @@ module KozenetUi
|
|
|
36
36
|
classes << "kz-header-blur" if @blur
|
|
37
37
|
classes.join(" ")
|
|
38
38
|
end
|
|
39
|
+
|
|
40
|
+
def start_action_buttons
|
|
41
|
+
action_buttons_for(:start)
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def start_action_buttons?
|
|
45
|
+
start_action_buttons.any?
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def end_action_buttons
|
|
49
|
+
action_buttons_for(:end)
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def end_action_buttons?
|
|
53
|
+
end_action_buttons.any?
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def action_buttons_for(placement)
|
|
57
|
+
return [] unless action_buttons?
|
|
58
|
+
|
|
59
|
+
action_buttons.select { |button| button.placement == placement }
|
|
60
|
+
end
|
|
39
61
|
end
|
|
40
62
|
end
|