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.
Files changed (50) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +5 -0
  3. data/CODE_OF_CONDUCT.md +132 -0
  4. data/LICENSE.txt +21 -0
  5. data/README.md +76 -0
  6. data/app/assets/images/kozenet_ui/icons/cart.svg +1 -0
  7. data/app/assets/images/kozenet_ui/icons/heart.svg +1 -0
  8. data/app/assets/javascripts/kozenet_ui/controllers/dropdown_controller.js +55 -0
  9. data/app/assets/javascripts/kozenet_ui/controllers/header_controller.js +32 -0
  10. data/app/assets/javascripts/kozenet_ui/controllers/mobile_nav_controller.js +43 -0
  11. data/app/assets/javascripts/kozenet_ui/controllers/user_menu_controller.js +60 -0
  12. data/app/assets/javascripts/kozenet_ui/index.js +23 -0
  13. data/app/assets/stylesheets/kozenet_ui/base.css +69 -0
  14. data/app/assets/stylesheets/kozenet_ui/components/avatar.css +88 -0
  15. data/app/assets/stylesheets/kozenet_ui/components/badge.css +101 -0
  16. data/app/assets/stylesheets/kozenet_ui/components/button.css +230 -0
  17. data/app/assets/stylesheets/kozenet_ui/components/header.css +389 -0
  18. data/app/assets/stylesheets/kozenet_ui/components/utilities.css +270 -0
  19. data/app/assets/stylesheets/kozenet_ui/components.css +8 -0
  20. data/app/assets/stylesheets/kozenet_ui/tokens.css +168 -0
  21. data/app/components/kozenet_ui/avatar_component.rb +72 -0
  22. data/app/components/kozenet_ui/badge_component.rb +62 -0
  23. data/app/components/kozenet_ui/base_component.rb +84 -0
  24. data/app/components/kozenet_ui/button_component.rb +156 -0
  25. data/app/components/kozenet_ui/header_component/action_button_component.html.erb +11 -0
  26. data/app/components/kozenet_ui/header_component/action_button_component.rb +29 -0
  27. data/app/components/kozenet_ui/header_component/brand_component.rb +32 -0
  28. data/app/components/kozenet_ui/header_component/cta_component.html.erb +5 -0
  29. data/app/components/kozenet_ui/header_component/cta_component.rb +23 -0
  30. data/app/components/kozenet_ui/header_component/nav_item_component.html.erb +8 -0
  31. data/app/components/kozenet_ui/header_component/nav_item_component.rb +28 -0
  32. data/app/components/kozenet_ui/header_component/search_component.html.erb +17 -0
  33. data/app/components/kozenet_ui/header_component/search_component.rb +29 -0
  34. data/app/components/kozenet_ui/header_component/user_menu_component.html.erb +18 -0
  35. data/app/components/kozenet_ui/header_component/user_menu_component.rb +21 -0
  36. data/app/components/kozenet_ui/header_component.html.erb +81 -0
  37. data/app/components/kozenet_ui/header_component.rb +40 -0
  38. data/app/helpers/kozenet_ui/component_helper.rb +59 -0
  39. data/app/helpers/kozenet_ui/icon_helper.rb +16 -0
  40. data/lib/generators/kozenet_ui/install/install_generator.rb +67 -0
  41. data/lib/generators/kozenet_ui/install/templates/kozenet_ui.rb +39 -0
  42. data/lib/generators/kozenet_ui/install/templates/tailwind.config.js +19 -0
  43. data/lib/kozenet_ui/configuration.rb +21 -0
  44. data/lib/kozenet_ui/engine.rb +94 -0
  45. data/lib/kozenet_ui/theme/palette.rb +132 -0
  46. data/lib/kozenet_ui/theme/tokens.rb +100 -0
  47. data/lib/kozenet_ui/theme/variants.rb +51 -0
  48. data/lib/kozenet_ui/version.rb +5 -0
  49. data/lib/kozenet_ui.rb +30 -0
  50. 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,5 @@
1
+ <%# frozen_string_literal: true %>
2
+ <%# CtaComponent template for KozenetUi::HeaderComponent %>
3
+ <%= tag.a href: @href, class: "kz-cta" do %>
4
+ <%= content %>
5
+ <% 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