kozenet_ui 0.1.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 +7 -0
- data/CHANGELOG.md +5 -0
- data/CODE_OF_CONDUCT.md +132 -0
- data/LICENSE.txt +21 -0
- data/README.md +76 -0
- data/app/assets/images/kozenet_ui/icons/cart.svg +1 -0
- data/app/assets/images/kozenet_ui/icons/heart.svg +1 -0
- data/app/assets/javascripts/kozenet_ui/controllers/dropdown_controller.js +55 -0
- data/app/assets/javascripts/kozenet_ui/controllers/header_controller.js +32 -0
- data/app/assets/javascripts/kozenet_ui/controllers/mobile_nav_controller.js +43 -0
- data/app/assets/javascripts/kozenet_ui/controllers/user_menu_controller.js +60 -0
- data/app/assets/javascripts/kozenet_ui/index.js +23 -0
- data/app/assets/stylesheets/kozenet_ui/base.css +69 -0
- data/app/assets/stylesheets/kozenet_ui/components/avatar.css +88 -0
- data/app/assets/stylesheets/kozenet_ui/components/badge.css +101 -0
- data/app/assets/stylesheets/kozenet_ui/components/button.css +230 -0
- data/app/assets/stylesheets/kozenet_ui/components/header.css +389 -0
- data/app/assets/stylesheets/kozenet_ui/components/utilities.css +270 -0
- data/app/assets/stylesheets/kozenet_ui/components.css +8 -0
- data/app/assets/stylesheets/kozenet_ui/tokens.css +168 -0
- data/app/components/kozenet_ui/avatar_component.rb +72 -0
- data/app/components/kozenet_ui/badge_component.rb +62 -0
- data/app/components/kozenet_ui/base_component.rb +84 -0
- data/app/components/kozenet_ui/button_component.rb +156 -0
- data/app/components/kozenet_ui/header_component/action_button_component.html.erb +11 -0
- data/app/components/kozenet_ui/header_component/action_button_component.rb +29 -0
- data/app/components/kozenet_ui/header_component/brand_component.rb +32 -0
- data/app/components/kozenet_ui/header_component/cta_component.html.erb +5 -0
- data/app/components/kozenet_ui/header_component/cta_component.rb +23 -0
- data/app/components/kozenet_ui/header_component/nav_item_component.html.erb +8 -0
- data/app/components/kozenet_ui/header_component/nav_item_component.rb +28 -0
- data/app/components/kozenet_ui/header_component/search_component.html.erb +17 -0
- data/app/components/kozenet_ui/header_component/search_component.rb +29 -0
- data/app/components/kozenet_ui/header_component/user_menu_component.html.erb +18 -0
- data/app/components/kozenet_ui/header_component/user_menu_component.rb +21 -0
- data/app/components/kozenet_ui/header_component.html.erb +81 -0
- data/app/components/kozenet_ui/header_component.rb +40 -0
- data/app/helpers/kozenet_ui/component_helper.rb +59 -0
- data/app/helpers/kozenet_ui/icon_helper.rb +16 -0
- data/lib/generators/kozenet_ui/install/install_generator.rb +67 -0
- data/lib/generators/kozenet_ui/install/templates/kozenet_ui.rb +39 -0
- data/lib/generators/kozenet_ui/install/templates/tailwind.config.js +19 -0
- data/lib/kozenet_ui/configuration.rb +21 -0
- data/lib/kozenet_ui/engine.rb +94 -0
- data/lib/kozenet_ui/theme/palette.rb +132 -0
- data/lib/kozenet_ui/theme/tokens.rb +100 -0
- data/lib/kozenet_ui/theme/variants.rb +51 -0
- data/lib/kozenet_ui/version.rb +5 -0
- data/lib/kozenet_ui.rb +30 -0
- metadata +308 -0
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module KozenetUi
|
|
4
|
+
# Apple-inspired button component with smooth interactions
|
|
5
|
+
# Supports multiple variants, sizes, and states
|
|
6
|
+
#
|
|
7
|
+
# @example Basic usage
|
|
8
|
+
# <%= render KozenetUi::ButtonComponent.new(variant: :primary) do %>
|
|
9
|
+
# Click me
|
|
10
|
+
# <% end %>
|
|
11
|
+
#
|
|
12
|
+
# @example With helper
|
|
13
|
+
# <%= kz_button(variant: :primary, size: :lg) { "Sign up" } %>
|
|
14
|
+
#
|
|
15
|
+
# @example With icon slot
|
|
16
|
+
# <%= kz_button(variant: :secondary) do |button| %>
|
|
17
|
+
# <% button.with_icon do %>
|
|
18
|
+
# <svg>...</svg>
|
|
19
|
+
# <% end %>
|
|
20
|
+
# Save changes
|
|
21
|
+
# <% end %>
|
|
22
|
+
#
|
|
23
|
+
# @example As link
|
|
24
|
+
# <%= kz_button(variant: :ghost, href: "/path") { "Learn more" } %>
|
|
25
|
+
#
|
|
26
|
+
# @example Loading state
|
|
27
|
+
# <%= kz_button(variant: :primary, loading: true) { "Processing..." } %>
|
|
28
|
+
# rubocop:disable Metrics/ClassLength
|
|
29
|
+
class ButtonComponent < BaseComponent
|
|
30
|
+
renders_one :icon
|
|
31
|
+
|
|
32
|
+
# rubocop:disable Metrics/ParameterLists
|
|
33
|
+
def initialize(
|
|
34
|
+
variant: :primary,
|
|
35
|
+
size: :md,
|
|
36
|
+
type: :button,
|
|
37
|
+
href: nil,
|
|
38
|
+
disabled: false,
|
|
39
|
+
loading: false,
|
|
40
|
+
full_width: false,
|
|
41
|
+
html_options: {}
|
|
42
|
+
)
|
|
43
|
+
super(variant: variant, size: size, **html_options)
|
|
44
|
+
@type = type
|
|
45
|
+
@href = href
|
|
46
|
+
@disabled = disabled
|
|
47
|
+
@loading = loading
|
|
48
|
+
@full_width = full_width
|
|
49
|
+
end
|
|
50
|
+
# rubocop:enable Metrics/ParameterLists
|
|
51
|
+
|
|
52
|
+
def call
|
|
53
|
+
if link?
|
|
54
|
+
link_tag
|
|
55
|
+
else
|
|
56
|
+
button_tag
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
private
|
|
61
|
+
|
|
62
|
+
def base_classes
|
|
63
|
+
classes = [
|
|
64
|
+
"kz-btn",
|
|
65
|
+
"inline-flex items-center justify-center gap-2",
|
|
66
|
+
"font-medium transition-all duration-200",
|
|
67
|
+
"focus:outline-none focus-visible:ring-2 focus-visible:ring-offset-2",
|
|
68
|
+
"disabled:opacity-50 disabled:cursor-not-allowed disabled:transform-none"
|
|
69
|
+
]
|
|
70
|
+
|
|
71
|
+
classes << "w-full" if @full_width
|
|
72
|
+
classes.join(" ")
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
def link?
|
|
76
|
+
@href.present?
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
def button_tag
|
|
80
|
+
tag.button(**button_attrs) do
|
|
81
|
+
button_content
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
def link_tag
|
|
86
|
+
tag.a(**link_attrs) do
|
|
87
|
+
button_content
|
|
88
|
+
end
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
def button_content
|
|
92
|
+
safe_join([
|
|
93
|
+
icon_or_spinner,
|
|
94
|
+
content_wrapper
|
|
95
|
+
].compact)
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
def icon_or_spinner
|
|
99
|
+
if loading?
|
|
100
|
+
spinner_icon
|
|
101
|
+
elsif icon?
|
|
102
|
+
tag.span(class: "kz-btn-icon") { icon }
|
|
103
|
+
end
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
# rubocop:disable Metrics/MethodLength
|
|
107
|
+
def spinner_icon
|
|
108
|
+
tag.svg(
|
|
109
|
+
class: "kz-btn-spinner animate-spin",
|
|
110
|
+
width: "16",
|
|
111
|
+
height: "16",
|
|
112
|
+
viewBox: "0 0 24 24",
|
|
113
|
+
fill: "none",
|
|
114
|
+
stroke: "currentColor",
|
|
115
|
+
stroke_width: "3",
|
|
116
|
+
stroke_linecap: "round",
|
|
117
|
+
stroke_linejoin: "round"
|
|
118
|
+
) do
|
|
119
|
+
tag.path(d: "M21 12a9 9 0 1 1-6.219-8.56")
|
|
120
|
+
end
|
|
121
|
+
end
|
|
122
|
+
# rubocop:enable Metrics/MethodLength
|
|
123
|
+
|
|
124
|
+
def content_wrapper
|
|
125
|
+
tag.span(class: "kz-btn-content") { content }
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
def button_attrs
|
|
129
|
+
attrs = html_attrs
|
|
130
|
+
attrs[:type] = @type.to_s
|
|
131
|
+
attrs[:disabled] = true if disabled?
|
|
132
|
+
attrs[:"aria-busy"] = "true" if loading?
|
|
133
|
+
attrs[:"aria-disabled"] = "true" if disabled?
|
|
134
|
+
attrs
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
def link_attrs
|
|
138
|
+
attrs = html_attrs
|
|
139
|
+
attrs[:href] = @href
|
|
140
|
+
attrs[:role] = "button"
|
|
141
|
+
attrs[:"aria-busy"] = "true" if loading?
|
|
142
|
+
attrs[:"aria-disabled"] = "true" if disabled?
|
|
143
|
+
attrs[:class] = [attrs[:class], "pointer-events-none"].join(" ") if disabled?
|
|
144
|
+
attrs
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
def disabled?
|
|
148
|
+
@disabled || @loading
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
def loading?
|
|
152
|
+
@loading
|
|
153
|
+
end
|
|
154
|
+
end
|
|
155
|
+
# rubocop:enable Metrics/ClassLength
|
|
156
|
+
end
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
<%# frozen_string_literal: true %>
|
|
2
|
+
<%# ActionButtonComponent template for KozenetUi::HeaderComponent %>
|
|
3
|
+
<%= tag.a href: @href, class: "kz-action-btn", aria: { label: @label } do %>
|
|
4
|
+
<% if @icon %>
|
|
5
|
+
<span class="kz-action-btn-icon"><%= render_icon(@icon) %></span>
|
|
6
|
+
<% end %>
|
|
7
|
+
<% if @label %>
|
|
8
|
+
<span class="kz-action-btn-label"><%= @label %></span>
|
|
9
|
+
<% end %>
|
|
10
|
+
<%= content if content.present? %>
|
|
11
|
+
<% end %>
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module KozenetUi
|
|
4
|
+
class HeaderComponent < BaseComponent
|
|
5
|
+
# ActionButton section for the HeaderComponent
|
|
6
|
+
# Renders an action button (icon or text) in the header
|
|
7
|
+
#
|
|
8
|
+
# @example
|
|
9
|
+
# <%= render KozenetUi::HeaderComponent::ActionButtonComponent.new(href: "/cart", icon: :cart, label: "Cart") %>
|
|
10
|
+
class ActionButtonComponent < BaseComponent
|
|
11
|
+
def initialize(href: "#", icon: nil, label: nil, **html_options)
|
|
12
|
+
super(**html_options)
|
|
13
|
+
@href = href
|
|
14
|
+
@icon = icon
|
|
15
|
+
@label = label
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
private
|
|
19
|
+
|
|
20
|
+
def action_button_classes
|
|
21
|
+
"kz-action-btn"
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def render_icon(icon)
|
|
25
|
+
ApplicationController.helpers.kozenet_ui_icon(icon, class: "kz-action-btn-icon") if icon
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module KozenetUi
|
|
4
|
+
class HeaderComponent < BaseComponent
|
|
5
|
+
# Brand section for the HeaderComponent
|
|
6
|
+
# Renders the brand/logo area, typically on the left of the header
|
|
7
|
+
#
|
|
8
|
+
# @example
|
|
9
|
+
# <%= render KozenetUi::HeaderComponent::BrandComponent.new(href: root_path) do %>
|
|
10
|
+
# <%= image_tag "logo.svg", alt: "Logo" %>
|
|
11
|
+
# <span>MyBrand</span>
|
|
12
|
+
# <% end %>
|
|
13
|
+
class BrandComponent < BaseComponent
|
|
14
|
+
def initialize(href: "#", **html_options)
|
|
15
|
+
super(**html_options)
|
|
16
|
+
@href = href
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def call
|
|
20
|
+
tag.a(href: @href, class: brand_classes) do
|
|
21
|
+
safe_join([content])
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
private
|
|
26
|
+
|
|
27
|
+
def brand_classes
|
|
28
|
+
"kz-brand"
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module KozenetUi
|
|
4
|
+
class HeaderComponent < BaseComponent
|
|
5
|
+
# CTA (Call To Action) section for the HeaderComponent
|
|
6
|
+
# Renders a prominent call-to-action button or link in the header
|
|
7
|
+
#
|
|
8
|
+
# @example
|
|
9
|
+
# <%= render KozenetUi::HeaderComponent::CtaComponent.new(href: "/signup") { "Get Started" } %>
|
|
10
|
+
class CtaComponent < BaseComponent
|
|
11
|
+
def initialize(href: "#", **html_options)
|
|
12
|
+
super(**html_options)
|
|
13
|
+
@href = href
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
private
|
|
17
|
+
|
|
18
|
+
def cta_classes
|
|
19
|
+
"kz-cta"
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
<%# frozen_string_literal: true %>
|
|
2
|
+
<%# NavItemComponent template for KozenetUi::HeaderComponent %>
|
|
3
|
+
<%= tag.a href: @href, class: nav_item_classes, role: (@dropdown ? "button" : nil), data: (@dropdown ? { action: "click->kz-dropdown#toggle" } : {}) do %>
|
|
4
|
+
<%= content %>
|
|
5
|
+
<% if @dropdown %>
|
|
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>
|
|
7
|
+
<% end %>
|
|
8
|
+
<% end %>
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module KozenetUi
|
|
4
|
+
class HeaderComponent < BaseComponent
|
|
5
|
+
# NavItem section for the HeaderComponent
|
|
6
|
+
# Renders a navigation item (link/button) in the header
|
|
7
|
+
#
|
|
8
|
+
# @example
|
|
9
|
+
# <%= render KozenetUi::HeaderComponent::NavItemComponent.new(href: "/path", active: true) { "Home" } %>
|
|
10
|
+
class NavItemComponent < BaseComponent
|
|
11
|
+
def initialize(href: "#", active: false, dropdown: false, **html_options)
|
|
12
|
+
super(**html_options)
|
|
13
|
+
@href = href
|
|
14
|
+
@active = active
|
|
15
|
+
@dropdown = dropdown
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
private
|
|
19
|
+
|
|
20
|
+
def nav_item_classes
|
|
21
|
+
classes = ["kz-nav-link"]
|
|
22
|
+
classes << "is-active" if @active
|
|
23
|
+
classes << @custom_class if defined?(@custom_class) && @custom_class
|
|
24
|
+
classes.join(" ")
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
<%# frozen_string_literal: true %>
|
|
2
|
+
<%# SearchComponent template for KozenetUi::HeaderComponent %>
|
|
3
|
+
<form action="<%= @action %>" method="<%= @method %>" class="kz-header-search-form" role="search">
|
|
4
|
+
<div class="kz-search-wrap">
|
|
5
|
+
<span class="kz-search-icon" aria-hidden="true">
|
|
6
|
+
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="11" cy="11" r="8"/><path d="m21 21-4.35-4.35"/></svg>
|
|
7
|
+
</span>
|
|
8
|
+
<input
|
|
9
|
+
type="search"
|
|
10
|
+
name="<%= @name %>"
|
|
11
|
+
value="<%= @value %>"
|
|
12
|
+
placeholder="<%= @placeholder %>"
|
|
13
|
+
class="kz-search-input"
|
|
14
|
+
<%= tag.attributes(@html_options) %>
|
|
15
|
+
/>
|
|
16
|
+
</div>
|
|
17
|
+
</form>
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module KozenetUi
|
|
4
|
+
class HeaderComponent < BaseComponent
|
|
5
|
+
# Search section for the HeaderComponent
|
|
6
|
+
# Renders a search input or form in the header
|
|
7
|
+
#
|
|
8
|
+
# @example
|
|
9
|
+
# <%= render KozenetUi::HeaderComponent::SearchComponent.new(placeholder: "Search...") %>
|
|
10
|
+
class SearchComponent < BaseComponent
|
|
11
|
+
# rubocop:disable Metrics/MethodLength
|
|
12
|
+
def initialize(options = {})
|
|
13
|
+
placeholder = options.fetch(:placeholder, "Search...")
|
|
14
|
+
name = options.fetch(:name, "q")
|
|
15
|
+
value = options.fetch(:value, nil)
|
|
16
|
+
action = options.fetch(:action, "#")
|
|
17
|
+
method = options.fetch(:method, :get)
|
|
18
|
+
html_options = options.fetch(:html_options, {})
|
|
19
|
+
super(**html_options)
|
|
20
|
+
@placeholder = placeholder
|
|
21
|
+
@name = name
|
|
22
|
+
@value = value
|
|
23
|
+
@action = action
|
|
24
|
+
@method = method
|
|
25
|
+
end
|
|
26
|
+
# rubocop:enable Metrics/MethodLength
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
<%# frozen_string_literal: true %>
|
|
2
|
+
<%# UserMenuComponent template for KozenetUi::HeaderComponent %>
|
|
3
|
+
<div class="kz-user-menu relative" data-controller="kz-user-menu">
|
|
4
|
+
<button type="button" class="kz-avatar-btn flex items-center gap-2" data-action="click->kz-user-menu#toggle">
|
|
5
|
+
<% if @avatar_url.present? %>
|
|
6
|
+
<img src="<%= @avatar_url %>" alt="<%= @user_name %>" class="w-8 h-8 rounded-full" />
|
|
7
|
+
<% else %>
|
|
8
|
+
<span class="bg-gray-300 dark:bg-gray-700 w-8 h-8 rounded-full flex items-center justify-center font-bold">
|
|
9
|
+
<%= @user_name.to_s.first.upcase %>
|
|
10
|
+
</span>
|
|
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
|
+
</button>
|
|
15
|
+
<div class="kz-user-dropdown absolute right-0 mt-2 w-48 bg-white dark:bg-gray-900 rounded shadow-lg z-50 hidden" data-kz-user-menu-target="dropdown">
|
|
16
|
+
<%= content %>
|
|
17
|
+
</div>
|
|
18
|
+
</div>
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module KozenetUi
|
|
4
|
+
class HeaderComponent < BaseComponent
|
|
5
|
+
# UserMenu section for the HeaderComponent
|
|
6
|
+
# Renders a user menu dropdown or avatar in the header
|
|
7
|
+
#
|
|
8
|
+
# @example
|
|
9
|
+
# <%= render KozenetUi::HeaderComponent::UserMenuComponent.new(user_name: "Test User", avatar_url: nil) do %>
|
|
10
|
+
# <%= link_to "Profile", profile_path %>
|
|
11
|
+
# <%= link_to "Logout", logout_path, method: :delete %>
|
|
12
|
+
# <% end %>
|
|
13
|
+
class UserMenuComponent < BaseComponent
|
|
14
|
+
def initialize(user_name: nil, avatar_url: nil, **html_options)
|
|
15
|
+
super(**html_options)
|
|
16
|
+
@user_name = user_name
|
|
17
|
+
@avatar_url = avatar_url
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
end
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
<header class="kz-header kz-header-sticky kz-header-blur" data-controller="kz-header kz-mobile-nav" data-action="scroll@window->kz-header#handleScroll">
|
|
2
|
+
<div class="kz-header-bar" data-kz-header-target="container">
|
|
3
|
+
<!-- Brand + Navigation -->
|
|
4
|
+
<div class="kz-header-start">
|
|
5
|
+
<div class="kz-brand">
|
|
6
|
+
<%= brand if brand? %>
|
|
7
|
+
</div>
|
|
8
|
+
<% if nav_items? %>
|
|
9
|
+
<nav class="kz-nav-links" aria-label="Primary navigation">
|
|
10
|
+
<% nav_items.each do |item| %>
|
|
11
|
+
<%= item %>
|
|
12
|
+
<% end %>
|
|
13
|
+
</nav>
|
|
14
|
+
<% end %>
|
|
15
|
+
</div>
|
|
16
|
+
|
|
17
|
+
<!-- Search (desktop) -->
|
|
18
|
+
<% if search? %>
|
|
19
|
+
<div class="kz-search-col">
|
|
20
|
+
<%= search %>
|
|
21
|
+
</div>
|
|
22
|
+
<% end %>
|
|
23
|
+
|
|
24
|
+
<!-- Actions -->
|
|
25
|
+
<div class="kz-header-actions">
|
|
26
|
+
<!-- Mobile search trigger -->
|
|
27
|
+
<% if search? %>
|
|
28
|
+
<button
|
|
29
|
+
type="button"
|
|
30
|
+
class="kz-action-btn md:hidden"
|
|
31
|
+
aria-label="Search"
|
|
32
|
+
data-action="click->kz-header#toggleSearch"
|
|
33
|
+
>
|
|
34
|
+
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
|
35
|
+
<circle cx="11" cy="11" r="8"/>
|
|
36
|
+
<path d="m21 21-4.35-4.35"/>
|
|
37
|
+
</svg>
|
|
38
|
+
</button>
|
|
39
|
+
<% end %>
|
|
40
|
+
|
|
41
|
+
<!-- Action buttons -->
|
|
42
|
+
<% if action_buttons? %>
|
|
43
|
+
<% action_buttons.each do |button| %>
|
|
44
|
+
<%= button %>
|
|
45
|
+
<% end %>
|
|
46
|
+
<% end %>
|
|
47
|
+
|
|
48
|
+
<!-- CTA or User Menu -->
|
|
49
|
+
<% if user_menu? %>
|
|
50
|
+
<%= user_menu %>
|
|
51
|
+
<% elsif cta? %>
|
|
52
|
+
<%= cta %>
|
|
53
|
+
<% end %>
|
|
54
|
+
|
|
55
|
+
<!-- Mobile menu trigger -->
|
|
56
|
+
<button
|
|
57
|
+
class="kz-mobile-trigger"
|
|
58
|
+
type="button"
|
|
59
|
+
aria-label="Menu"
|
|
60
|
+
data-action="click->kz-mobile-nav#toggle"
|
|
61
|
+
data-kz-mobile-nav-target="trigger"
|
|
62
|
+
>
|
|
63
|
+
<svg class="kz-icon-menu" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
|
64
|
+
<path d="M3 6h18M3 12h18M3 18h18"/>
|
|
65
|
+
</svg>
|
|
66
|
+
</button>
|
|
67
|
+
</div>
|
|
68
|
+
</div>
|
|
69
|
+
|
|
70
|
+
<!-- Mobile panel -->
|
|
71
|
+
<% if mobile_menu? %>
|
|
72
|
+
<div
|
|
73
|
+
class="kz-mobile-panel hidden scale-y-0 origin-top"
|
|
74
|
+
data-kz-mobile-nav-target="panel"
|
|
75
|
+
role="dialog"
|
|
76
|
+
aria-label="Mobile navigation"
|
|
77
|
+
>
|
|
78
|
+
<%= mobile_menu %>
|
|
79
|
+
</div>
|
|
80
|
+
<% end %>
|
|
81
|
+
</header>
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module KozenetUi
|
|
4
|
+
# Header component for navigation and branding
|
|
5
|
+
# Supports sticky and blur options, and slots for brand, nav, actions, etc.
|
|
6
|
+
#
|
|
7
|
+
# @example Basic usage
|
|
8
|
+
# <%= kz_header(sticky: true, blur: true) do |header| %>
|
|
9
|
+
# <% header.brand { ... } %>
|
|
10
|
+
# <% header.nav_items { ... } %>
|
|
11
|
+
# <% end %>
|
|
12
|
+
class HeaderComponent < BaseComponent
|
|
13
|
+
renders_one :brand, "KozenetUi::HeaderComponent::BrandComponent"
|
|
14
|
+
renders_one :search, "KozenetUi::HeaderComponent::SearchComponent"
|
|
15
|
+
renders_many :nav_items, "KozenetUi::HeaderComponent::NavItemComponent"
|
|
16
|
+
renders_many :action_buttons, "KozenetUi::HeaderComponent::ActionButtonComponent"
|
|
17
|
+
renders_one :cta, "KozenetUi::HeaderComponent::CtaComponent"
|
|
18
|
+
renders_one :user_menu, "KozenetUi::HeaderComponent::UserMenuComponent"
|
|
19
|
+
renders_one :mobile_menu
|
|
20
|
+
|
|
21
|
+
def initialize(
|
|
22
|
+
sticky: true,
|
|
23
|
+
blur: true,
|
|
24
|
+
**html_options
|
|
25
|
+
)
|
|
26
|
+
super(**html_options)
|
|
27
|
+
@sticky = sticky
|
|
28
|
+
@blur = blur
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
private
|
|
32
|
+
|
|
33
|
+
def base_classes
|
|
34
|
+
classes = ["kz-header"]
|
|
35
|
+
classes << "kz-header-sticky" if @sticky
|
|
36
|
+
classes << "kz-header-blur" if @blur
|
|
37
|
+
classes.join(" ")
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Helper methods for rendering Kozenet UI components in views
|
|
4
|
+
module KozenetUi
|
|
5
|
+
# Helper methods for rendering Kozenet UI components in views
|
|
6
|
+
module ComponentHelper
|
|
7
|
+
include KozenetUi::IconHelper
|
|
8
|
+
|
|
9
|
+
# Render a Kozenet UI button
|
|
10
|
+
def kz_button(**options, &block)
|
|
11
|
+
render(KozenetUi::ButtonComponent.new(**options), &block)
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
# Render a Kozenet UI header
|
|
15
|
+
def kz_header(**options, &block)
|
|
16
|
+
render(KozenetUi::HeaderComponent.new(**options), &block)
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
# Render a Kozenet UI badge
|
|
20
|
+
def kz_badge(**options, &block)
|
|
21
|
+
render(KozenetUi::BadgeComponent.new(**options), &block)
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
# Render a Kozenet UI avatar
|
|
25
|
+
def kz_avatar(**options, &block)
|
|
26
|
+
render(KozenetUi::AvatarComponent.new(**options), &block)
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
# Include theme styles in layout
|
|
30
|
+
def kozenet_ui_stylesheet_tag
|
|
31
|
+
stylesheet_link_tag "kozenet_ui/tokens", "kozenet_ui/base", "kozenet_ui/components"
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
# Include theme JavaScript
|
|
35
|
+
def kozenet_ui_javascript_tag
|
|
36
|
+
javascript_include_tag "kozenet_ui/index", type: "module"
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
# Inject inline theme variables (CSP-compliant)
|
|
40
|
+
def kozenet_ui_theme_variables_tag
|
|
41
|
+
# rubocop:disable Rails/OutputSafety
|
|
42
|
+
content_tag(:style, nonce: content_security_policy_nonce) do
|
|
43
|
+
palette = KozenetUi.configuration.palette
|
|
44
|
+
tokens = KozenetUi::Theme::Tokens
|
|
45
|
+
|
|
46
|
+
<<~CSS.html_safe
|
|
47
|
+
:root {
|
|
48
|
+
#{tokens.to_css_variables}
|
|
49
|
+
#{palette.to_css_variables(mode: :light)}
|
|
50
|
+
}
|
|
51
|
+
[data-theme="dark"], .dark {
|
|
52
|
+
#{palette.to_css_variables(mode: :dark)}
|
|
53
|
+
}
|
|
54
|
+
CSS
|
|
55
|
+
end
|
|
56
|
+
# rubocop:enable Rails/OutputSafety
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
end
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module KozenetUi
|
|
4
|
+
# Helper methods for rendering SVG icons in Kozenet UI
|
|
5
|
+
module IconHelper
|
|
6
|
+
# Renders a Heroicon SVG from the gem's assets
|
|
7
|
+
# Usage: kozenet_ui_icon(:cart, class: "w-5 h-5 text-gray-500")
|
|
8
|
+
def kozenet_ui_icon(name, options = {})
|
|
9
|
+
app_path = "icons/#{name}.svg"
|
|
10
|
+
gem_path = "kozenet_ui/icons/#{name}.svg"
|
|
11
|
+
app_full_path = Rails.root.join("app/assets/images", app_path)
|
|
12
|
+
path = File.exist?(app_full_path) ? app_path : gem_path
|
|
13
|
+
image_tag(asset_path(path), options)
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
end
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "rails/generators/base"
|
|
4
|
+
|
|
5
|
+
module KozenetUi
|
|
6
|
+
module Generators
|
|
7
|
+
# Generator for installing Kozenet UI into a Rails application
|
|
8
|
+
class InstallGenerator < Rails::Generators::Base
|
|
9
|
+
source_root File.expand_path("templates", __dir__)
|
|
10
|
+
|
|
11
|
+
desc "Install Kozenet UI into your application"
|
|
12
|
+
|
|
13
|
+
def create_initializer
|
|
14
|
+
template "kozenet_ui.rb", "config/initializers/kozenet_ui.rb"
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def copy_css_files_to_app
|
|
18
|
+
# do NOT copy CSS files to the app. Let the gem expose them via the asset pipeline.
|
|
19
|
+
say "Kozenet UI stylesheets are now available via the asset pipeline. " \
|
|
20
|
+
"Import them in your Tailwind/application.css:", :green
|
|
21
|
+
say "\n@import 'kozenet_ui/tokens.css';\n@import 'kozenet_ui/base.css';" \
|
|
22
|
+
"\n@import 'kozenet_ui/components.css';\n", :cyan
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
# rubocop:disable Metrics/MethodLength
|
|
26
|
+
def add_stylesheets_to_application
|
|
27
|
+
tailwind_css = nil
|
|
28
|
+
tailwind_css = "app/assets/stylesheets/application.css" if File.exist?("app/assets/stylesheets/application.css")
|
|
29
|
+
|
|
30
|
+
if tailwind_css
|
|
31
|
+
content = File.read(tailwind_css)
|
|
32
|
+
kozenet_imports = "\n/* Kozenet UI Styles */\n" \
|
|
33
|
+
"@import 'kozenet_ui/tokens.css';\n" \
|
|
34
|
+
"@import 'kozenet_ui/base.css';\n" \
|
|
35
|
+
"@import 'kozenet_ui/components.css';\n"
|
|
36
|
+
|
|
37
|
+
if content.include?("kozenet_ui/base.css")
|
|
38
|
+
say "File unchanged! Kozenet UI styles already present in #{tailwind_css}", :yellow
|
|
39
|
+
else
|
|
40
|
+
append_to_file tailwind_css, kozenet_imports
|
|
41
|
+
say "Appended Kozenet UI styles to #{tailwind_css}", :green
|
|
42
|
+
end
|
|
43
|
+
else
|
|
44
|
+
say "Could not find app/assets/stylesheets/application.css. " \
|
|
45
|
+
"Please manually import Kozenet UI stylesheets.", :yellow
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
# rubocop:enable Metrics/MethodLength
|
|
49
|
+
|
|
50
|
+
def show_readme
|
|
51
|
+
say "\n✅ Kozenet UI installed successfully!", :green
|
|
52
|
+
say "\nNext steps:", :cyan
|
|
53
|
+
say " 1. Add <%= kozenet_ui_theme_variables_tag %> to your layout <head>"
|
|
54
|
+
say " 2. Customize colors in config/initializers/kozenet_ui.rb"
|
|
55
|
+
say " 3. Start using components: <%= kz_button { 'Click me' } %>"
|
|
56
|
+
say "\nDocumentation: https://github.com/kozenetpro/kozenet_ui\n"
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def install
|
|
60
|
+
create_initializer
|
|
61
|
+
copy_css_files_to_app
|
|
62
|
+
add_stylesheets_to_application
|
|
63
|
+
show_readme
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
end
|