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.
Files changed (44) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +11 -0
  3. data/README.md +79 -16
  4. data/app/assets/fonts/kozenet_ui/inter-latin.woff2 +0 -0
  5. data/app/assets/fonts/kozenet_ui/jetbrains-mono-latin.woff2 +0 -0
  6. data/app/assets/fonts/kozenet_ui/source-serif-4-latin.woff2 +0 -0
  7. data/app/assets/javascripts/kozenet_ui/index.js +226 -17
  8. data/app/assets/stylesheets/kozenet_ui/base.css +5 -0
  9. data/app/assets/stylesheets/kozenet_ui/components/avatar.css +4 -1
  10. data/app/assets/stylesheets/kozenet_ui/components/badge.css +11 -1
  11. data/app/assets/stylesheets/kozenet_ui/components/button.css +21 -1
  12. data/app/assets/stylesheets/kozenet_ui/components/header.css +227 -38
  13. data/app/assets/stylesheets/kozenet_ui/components/utilities.css +52 -1
  14. data/app/assets/stylesheets/kozenet_ui/fonts.css +37 -0
  15. data/app/assets/stylesheets/kozenet_ui/tokens.css +150 -53
  16. data/app/components/kozenet_ui/base_component.rb +21 -1
  17. data/app/components/kozenet_ui/header_component/action_button_component.html.erb +4 -2
  18. data/app/components/kozenet_ui/header_component/action_button_component.rb +78 -4
  19. data/app/components/kozenet_ui/header_component/nav_item_component.html.erb +1 -1
  20. data/app/components/kozenet_ui/header_component/nav_item_component.rb +6 -0
  21. data/app/components/kozenet_ui/header_component/user_menu_component.html.erb +5 -7
  22. data/app/components/kozenet_ui/header_component.html.erb +39 -30
  23. data/app/components/kozenet_ui/header_component.rb +26 -4
  24. data/app/helpers/kozenet_ui/component_helper.rb +82 -8
  25. data/app/helpers/kozenet_ui/icon_helper.rb +11 -7
  26. data/docs/README.md +25 -0
  27. data/docs/components/README.md +44 -0
  28. data/docs/components/avatar.md +73 -0
  29. data/docs/components/badge.md +74 -0
  30. data/docs/components/button.md +95 -0
  31. data/docs/components/header.md +199 -0
  32. data/docs/foundations/README.md +14 -0
  33. data/docs/foundations/fonts.md +136 -0
  34. data/lib/generators/kozenet_ui/install/install_generator.rb +94 -8
  35. data/lib/generators/kozenet_ui/install/templates/kozenet_ui.rb +3 -0
  36. data/lib/kozenet_ui/configuration.rb +35 -0
  37. data/lib/kozenet_ui/engine.rb +89 -14
  38. data/lib/kozenet_ui/theme/palette.rb +21 -7
  39. data/lib/kozenet_ui/theme/variants.rb +1 -0
  40. data/lib/kozenet_ui/version.rb +1 -1
  41. data/lib/kozenet_ui.rb +2 -0
  42. metadata +34 -13
  43. data/app/assets/images/kozenet_ui/icons/cart.svg +0 -1
  44. data/app/assets/images/kozenet_ui/icons/heart.svg +0 -1
@@ -28,28 +28,102 @@ module KozenetUi
28
28
 
29
29
  # Include theme styles in layout
30
30
  def kozenet_ui_stylesheet_tag
31
- stylesheet_link_tag "kozenet_ui/tokens", "kozenet_ui/base", "kozenet_ui/components"
31
+ stylesheet_link_tag(
32
+ "kozenet_ui/tokens",
33
+ "kozenet_ui/fonts",
34
+ "kozenet_ui/base",
35
+ "kozenet_ui/components/button",
36
+ "kozenet_ui/components/header",
37
+ "kozenet_ui/components/avatar",
38
+ "kozenet_ui/components/badge",
39
+ "kozenet_ui/components/utilities",
40
+ "data-turbo-track": "reload"
41
+ )
32
42
  end
33
43
 
34
44
  # Include theme JavaScript
35
45
  def kozenet_ui_javascript_tag
36
- javascript_include_tag "kozenet_ui/index", type: "module"
46
+ javascript_include_tag "kozenet_ui/index", type: "module", "data-turbo-track": "reload"
47
+ end
48
+
49
+ # Include runtime tags once in the application layout.
50
+ #
51
+ # Stylesheets are loaded directly so Propshaft/Sprockets can emit digested
52
+ # asset URLs in production. Apps can pass stylesheets: false when bundling
53
+ # Kozenet UI CSS with their own build pipeline.
54
+ def kozenet_ui_head_tags(stylesheets: true, javascript: true)
55
+ tags = []
56
+ tags << kozenet_ui_stylesheet_tag if stylesheets
57
+ tags << kozenet_ui_config_tag
58
+ tags << kozenet_ui_theme_variables_tag
59
+ tags << kozenet_ui_javascript_tag if javascript
60
+
61
+ safe_join(tags, "\n")
62
+ end
63
+
64
+ def kozenet_ui_config_tag
65
+ tag.meta name: "kozenet-ui-stimulus-prefix", content: KozenetUi.configuration.stimulus_prefix
37
66
  end
38
67
 
39
68
  # Inject inline theme variables (CSP-compliant)
40
69
  def kozenet_ui_theme_variables_tag
41
70
  # rubocop:disable Rails/OutputSafety
42
- content_tag(:style, nonce: content_security_policy_nonce) do
43
- palette = KozenetUi.configuration.palette
44
- tokens = KozenetUi::Theme::Tokens
71
+ content_tag(:style, kozenet_ui_theme_variables, nonce: content_security_policy_nonce)
72
+ # rubocop:enable Rails/OutputSafety
73
+ end
74
+
75
+ def kozenet_ui_theme_variables
76
+ # rubocop:disable Rails/OutputSafety
77
+ palette = KozenetUi.configuration.palette
78
+ light_palette = palette.to_css_variables(mode: :light)
79
+ dark_palette = palette.to_css_variables(mode: :dark)
80
+ tokens = KozenetUi::Theme::Tokens.to_css_variables
45
81
 
82
+ case KozenetUi.configuration.theme
83
+ when :dark, "dark"
84
+ <<~CSS.html_safe
85
+ :root {
86
+ color-scheme: dark;
87
+ #{tokens}
88
+ #{dark_palette}
89
+ }
90
+ [data-theme="light"], .light {
91
+ color-scheme: light;
92
+ #{light_palette}
93
+ }
94
+ CSS
95
+ when :system, "system"
96
+ <<~CSS.html_safe
97
+ :root {
98
+ color-scheme: light;
99
+ #{tokens}
100
+ #{light_palette}
101
+ }
102
+ @media (prefers-color-scheme: dark) {
103
+ :root:not([data-theme="light"]) {
104
+ color-scheme: dark;
105
+ #{dark_palette}
106
+ }
107
+ }
108
+ [data-theme="dark"], .dark {
109
+ color-scheme: dark;
110
+ #{dark_palette}
111
+ }
112
+ [data-theme="light"], .light {
113
+ color-scheme: light;
114
+ #{light_palette}
115
+ }
116
+ CSS
117
+ else
46
118
  <<~CSS.html_safe
47
119
  :root {
48
- #{tokens.to_css_variables}
49
- #{palette.to_css_variables(mode: :light)}
120
+ color-scheme: light;
121
+ #{tokens}
122
+ #{light_palette}
50
123
  }
51
124
  [data-theme="dark"], .dark {
52
- #{palette.to_css_variables(mode: :dark)}
125
+ color-scheme: dark;
126
+ #{dark_palette}
53
127
  }
54
128
  CSS
55
129
  end
@@ -3,14 +3,18 @@
3
3
  module KozenetUi
4
4
  # Helper methods for rendering SVG icons in Kozenet UI
5
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")
6
+ include RailsHeroicon::Helper
7
+
8
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)
9
+ heroicon(normalize_icon_name(name), **options)
10
+ rescue RailsHeroicon::UndefinedIcon => e
11
+ raise ArgumentError, "Unknown Heroicon `#{name}`. Use a valid icon name from Heroicons.", e.backtrace
12
+ end
13
+
14
+ private
15
+
16
+ def normalize_icon_name(name)
17
+ name.to_s.tr("_", "-")
14
18
  end
15
19
  end
16
20
  end
data/docs/README.md ADDED
@@ -0,0 +1,25 @@
1
+ # Kozenet UI Docs
2
+
3
+ This folder documents how to use Kozenet UI as a Rails gem.
4
+
5
+ ## Foundations
6
+
7
+ Foundations are the shared UI base used by every component.
8
+
9
+ - [Fonts](./foundations/fonts.md)
10
+
11
+ ## Components
12
+
13
+ Components are the public ViewComponent APIs used in Rails views.
14
+
15
+ - [Component Docs](./components/README.md)
16
+ - [Avatar](./components/avatar.md)
17
+ - [Badge](./components/badge.md)
18
+ - [Button](./components/button.md)
19
+ - [Header](./components/header.md)
20
+
21
+ ## Adding New Docs
22
+
23
+ When adding a new component, add one Markdown file in `docs/components`.
24
+
25
+ When adding a shared system feature, add one Markdown file in `docs/foundations`.
@@ -0,0 +1,44 @@
1
+ # Component Docs
2
+
3
+ This folder is the public usage guide for Kozenet UI components. Each component gets one Markdown file with the same shape so the documentation can grow without becoming messy.
4
+
5
+ ## Components
6
+
7
+ - [Avatar](./avatar.md)
8
+ - [Badge](./badge.md)
9
+ - [Button](./button.md)
10
+ - [Header](./header.md)
11
+
12
+ Shared UI foundations live in [../foundations](../foundations/README.md).
13
+
14
+ ## Documentation Pattern
15
+
16
+ Each component page should include:
17
+
18
+ - Purpose: when to use the component.
19
+ - Quick usage: the recommended helper API.
20
+ - Direct render: the ViewComponent API.
21
+ - Options: accepted options and defaults.
22
+ - Examples: common production patterns.
23
+ - Best practices: how to keep apps clean, fast, and consistent.
24
+
25
+ ## Rails Setup
26
+
27
+ Load Kozenet UI once in your application layout:
28
+
29
+ ```erb
30
+ <%= javascript_importmap_tags %>
31
+ <%= kozenet_ui_head_tags %>
32
+ ```
33
+
34
+ Configure global defaults in `config/initializers/kozenet_ui.rb`:
35
+
36
+ ```ruby
37
+ KozenetUi.configure do |config|
38
+ config.theme = :system
39
+ config.stimulus_prefix = "kz"
40
+ config.component :header, sticky: true, blur: true
41
+ end
42
+ ```
43
+
44
+ Per-render options should always win over initializer defaults.
@@ -0,0 +1,73 @@
1
+ # Avatar
2
+
3
+ Use `AvatarComponent` for people, teams, authors, and account menu triggers.
4
+
5
+ ## Quick Usage
6
+
7
+ ```erb
8
+ <%= kz_avatar(src: user.avatar_url, alt: user.name) %>
9
+ ```
10
+
11
+ ## Direct Render
12
+
13
+ ```erb
14
+ <%= render KozenetUi::AvatarComponent.new(initials: "KP", variant: :primary) %>
15
+ ```
16
+
17
+ ## Options
18
+
19
+ | Option | Default | Description |
20
+ | --- | --- | --- |
21
+ | `src` | `nil` | Image URL. |
22
+ | `alt` | `"Avatar"` | Image alt text. |
23
+ | `initials` | `nil` | Text fallback when no image is present. |
24
+ | `variant` | `:primary` | Background style for initials/default state. |
25
+ | `size` | `:md` | Avatar size. |
26
+ | `html_options` | `{}` | Extra HTML attributes. |
27
+
28
+ ## Image Avatar
29
+
30
+ ```erb
31
+ <%= kz_avatar(src: current_user.avatar_url, alt: current_user.name, size: :lg) %>
32
+ ```
33
+
34
+ ## Initials Avatar
35
+
36
+ ```erb
37
+ <%= kz_avatar(initials: "JD", variant: :accent) %>
38
+ ```
39
+
40
+ ## Default Icon Avatar
41
+
42
+ ```erb
43
+ <%= kz_avatar %>
44
+ ```
45
+
46
+ ## Sizes
47
+
48
+ ```erb
49
+ <%= kz_avatar(initials: "XS", size: :xs) %>
50
+ <%= kz_avatar(initials: "SM", size: :sm) %>
51
+ <%= kz_avatar(initials: "MD", size: :md) %>
52
+ <%= kz_avatar(initials: "LG", size: :lg) %>
53
+ <%= kz_avatar(initials: "XL", size: :xl) %>
54
+ ```
55
+
56
+ ## With Extra Attributes
57
+
58
+ ```erb
59
+ <%= kz_avatar(
60
+ initials: "KP",
61
+ html_options: {
62
+ title: "Kozenet Pro",
63
+ data: { testid: "author-avatar" }
64
+ }
65
+ ) %>
66
+ ```
67
+
68
+ ## Best Practices
69
+
70
+ - Always provide meaningful `alt` text for real user images.
71
+ - Use initials when the user has no uploaded image.
72
+ - Keep avatar size consistent inside repeated lists.
73
+ - Use the same variant for the same identity type across an app.
@@ -0,0 +1,74 @@
1
+ # Badge
2
+
3
+ Use `BadgeComponent` for statuses, labels, counters, categories, and small metadata.
4
+
5
+ ## Quick Usage
6
+
7
+ ```erb
8
+ <%= kz_badge(variant: :success) { "Published" } %>
9
+ ```
10
+
11
+ ## Direct Render
12
+
13
+ ```erb
14
+ <%= render KozenetUi::BadgeComponent.new(variant: :warning, size: :sm) do %>
15
+ Pending
16
+ <% end %>
17
+ ```
18
+
19
+ ## Options
20
+
21
+ | Option | Default | Description |
22
+ | --- | --- | --- |
23
+ | `variant` | `:primary` | Visual style. |
24
+ | `size` | `:md` | Badge size. |
25
+ | `pill` | `true` | Uses rounded pill shape. |
26
+ | `**html_options` | `{}` | Extra HTML attributes. |
27
+
28
+ ## Variants
29
+
30
+ ```erb
31
+ <%= kz_badge(variant: :primary) { "Primary" } %>
32
+ <%= kz_badge(variant: :secondary) { "Secondary" } %>
33
+ <%= kz_badge(variant: :accent) { "Accent" } %>
34
+ <%= kz_badge(variant: :success) { "Success" } %>
35
+ <%= kz_badge(variant: :warning) { "Warning" } %>
36
+ <%= kz_badge(variant: :error) { "Error" } %>
37
+ <%= kz_badge(variant: :info) { "Info" } %>
38
+ ```
39
+
40
+ ## Count Badge
41
+
42
+ ```erb
43
+ <%= kz_badge(variant: :primary, size: :sm) { "99+" } %>
44
+ ```
45
+
46
+ ## Category Badge
47
+
48
+ ```erb
49
+ <%= kz_badge(variant: :accent) { blog.category_name } %>
50
+ ```
51
+
52
+ ## With Icon
53
+
54
+ ```erb
55
+ <%= kz_badge(variant: :warning) do |badge| %>
56
+ <% badge.with_icon do %>
57
+ <%= kozenet_ui_icon(:clock, size: 14) %>
58
+ <% end %>
59
+ Pending
60
+ <% end %>
61
+ ```
62
+
63
+ ## Less Rounded
64
+
65
+ ```erb
66
+ <%= kz_badge(variant: :secondary, pill: false) { "Draft" } %>
67
+ ```
68
+
69
+ ## Best Practices
70
+
71
+ - Keep text short: one to three words.
72
+ - Use semantic variants for state: `:success`, `:warning`, `:error`.
73
+ - Use `:accent` for brand/category emphasis.
74
+ - Avoid using badges as buttons. Use `kz_button` for actions.
@@ -0,0 +1,95 @@
1
+ # Button
2
+
3
+ Use `ButtonComponent` for primary actions, secondary actions, links that should look like buttons, and loading or disabled states.
4
+
5
+ ## Quick Usage
6
+
7
+ ```erb
8
+ <%= kz_button { "Save changes" } %>
9
+ ```
10
+
11
+ ```erb
12
+ <%= kz_button(variant: :secondary, size: :lg) { "Preview" } %>
13
+ ```
14
+
15
+ ## Direct Render
16
+
17
+ ```erb
18
+ <%= render KozenetUi::ButtonComponent.new(variant: :primary, size: :md) do %>
19
+ Save changes
20
+ <% end %>
21
+ ```
22
+
23
+ ## Options
24
+
25
+ | Option | Default | Description |
26
+ | --- | --- | --- |
27
+ | `variant` | `:primary` | Visual style. |
28
+ | `size` | `:md` | Button size. |
29
+ | `type` | `:button` | Native button type. |
30
+ | `href` | `nil` | Renders an anchor when present. |
31
+ | `disabled` | `false` | Disables the action visually and semantically. |
32
+ | `loading` | `false` | Shows spinner and marks the button busy. |
33
+ | `full_width` | `false` | Makes the button fill the available width. |
34
+ | `html_options` | `{}` | Extra HTML attributes. |
35
+
36
+ ## Variants
37
+
38
+ ```erb
39
+ <%= kz_button(variant: :primary) { "Primary" } %>
40
+ <%= kz_button(variant: :secondary) { "Secondary" } %>
41
+ <%= kz_button(variant: :accent) { "Accent" } %>
42
+ <%= kz_button(variant: :success) { "Success" } %>
43
+ <%= kz_button(variant: :warning) { "Warning" } %>
44
+ <%= kz_button(variant: :error) { "Error" } %>
45
+ <%= kz_button(variant: :ghost) { "Ghost" } %>
46
+ <%= kz_button(variant: :outline) { "Outline" } %>
47
+ ```
48
+
49
+ ## Sizes
50
+
51
+ ```erb
52
+ <%= kz_button(size: :xs) { "XS" } %>
53
+ <%= kz_button(size: :sm) { "SM" } %>
54
+ <%= kz_button(size: :md) { "MD" } %>
55
+ <%= kz_button(size: :lg) { "LG" } %>
56
+ <%= kz_button(size: :xl) { "XL" } %>
57
+ ```
58
+
59
+ ## Link Button
60
+
61
+ ```erb
62
+ <%= kz_button(href: pricing_path, variant: :ghost) { "View pricing" } %>
63
+ ```
64
+
65
+ ## Loading State
66
+
67
+ ```erb
68
+ <%= kz_button(loading: true) { "Saving..." } %>
69
+ ```
70
+
71
+ ## With Icon
72
+
73
+ ```erb
74
+ <%= kz_button(variant: :secondary) do |button| %>
75
+ <% button.with_icon do %>
76
+ <%= kozenet_ui_icon(:arrow_down_tray, size: 18) %>
77
+ <% end %>
78
+ Download
79
+ <% end %>
80
+ ```
81
+
82
+ ## Form Submit
83
+
84
+ ```erb
85
+ <%= form_with model: @post do |form| %>
86
+ <%= kz_button(type: :submit, variant: :primary) { "Publish" } %>
87
+ <% end %>
88
+ ```
89
+
90
+ ## Best Practices
91
+
92
+ - Use `:primary` for one main action per screen or section.
93
+ - Use `:secondary`, `:ghost`, or `:outline` for supporting actions.
94
+ - Use `loading: true` while a submission is in progress.
95
+ - Use `href:` only for navigation. Use button submit types for forms.
@@ -0,0 +1,199 @@
1
+ # Header
2
+
3
+ Use `HeaderComponent` for primary application navigation. It supports brand, nav links, search, icon actions, CTA, user menu, and mobile menu slots.
4
+
5
+ ## Quick Usage
6
+
7
+ ```erb
8
+ <%= kz_header do |header| %>
9
+ <% header.with_brand(href: root_path) do %>
10
+ <span>Kozenet</span>
11
+ <% end %>
12
+
13
+ <% header.with_nav_item(href: posts_path, active: current_page?(posts_path)) { "Posts" } %>
14
+ <% header.with_nav_item(href: new_post_path) { "New" } %>
15
+
16
+ <% header.with_search(placeholder: "Search posts", action: posts_path, value: params[:q]) %>
17
+ <% header.with_action_button(href: saved_posts_path, icon: :heart, label: "Saved") %>
18
+ <% header.with_cta(href: new_post_path) { "New post" } %>
19
+ <% end %>
20
+ ```
21
+
22
+ ## Direct Render
23
+
24
+ ```erb
25
+ <%= render KozenetUi::HeaderComponent.new(sticky: true, blur: true) do |header| %>
26
+ <% header.with_brand(href: root_path) { "Brand" } %>
27
+ <% end %>
28
+ ```
29
+
30
+ ## Options
31
+
32
+ | Option | Default | Description |
33
+ | --- | --- | --- |
34
+ | `sticky` | initializer default, fallback `true` | Keeps header sticky at top. |
35
+ | `blur` | initializer default, fallback `true` | Enables glass/blur treatment. |
36
+ | `**html_options` | `{}` | Extra HTML attributes. |
37
+
38
+ ## Global Defaults
39
+
40
+ Set project defaults in `config/initializers/kozenet_ui.rb`:
41
+
42
+ ```ruby
43
+ KozenetUi.configure do |config|
44
+ config.component :header, sticky: true, blur: true
45
+ end
46
+ ```
47
+
48
+ Override for one render:
49
+
50
+ ```erb
51
+ <%= kz_header(sticky: false, blur: false) do |header| %>
52
+ <% header.with_brand(href: root_path) { "Plain Header" } %>
53
+ <% end %>
54
+ ```
55
+
56
+ ## Slots
57
+
58
+ | Slot | Purpose |
59
+ | --- | --- |
60
+ | `with_brand` | Brand/logo link. |
61
+ | `with_nav_item` | Primary nav links. |
62
+ | `with_search` | Header search form. |
63
+ | `with_action_button` | Icon or compact text action button. |
64
+ | `with_cta` | Main call-to-action. |
65
+ | `with_user_menu` | Account/avatar menu. |
66
+ | `with_mobile_menu` | Mobile navigation panel. |
67
+
68
+ ## Brand
69
+
70
+ ```erb
71
+ <% header.with_brand(href: root_path) do %>
72
+ <span class="brand-mark">K</span>
73
+ <span>Kozenet <span class="accent">UI</span></span>
74
+ <% end %>
75
+ ```
76
+
77
+ ## Nav Items
78
+
79
+ ```erb
80
+ <% header.with_nav_item(href: dashboard_path, active: current_page?(dashboard_path)) { "Dashboard" } %>
81
+ <% header.with_nav_item(href: settings_path) { "Settings" } %>
82
+ ```
83
+
84
+ ## Search
85
+
86
+ ```erb
87
+ <% header.with_search(
88
+ placeholder: "Search",
89
+ name: :q,
90
+ value: params[:q],
91
+ action: posts_path,
92
+ method: :get
93
+ ) %>
94
+ ```
95
+
96
+ ## Action Button
97
+
98
+ Action buttons use Heroicons names. Ruby-style names are normalized:
99
+
100
+ ```erb
101
+ <% header.with_action_button(href: notifications_path, icon: :bell, label: "Notifications") %>
102
+ <% header.with_action_button(href: cart_path, icon: :shopping_cart, label: "Cart") %>
103
+ ```
104
+
105
+ `label` is used for accessibility and is visually hidden in the icon-only button.
106
+
107
+ By default, action buttons render in the end group, on the right side of the desktop header. Use `placement: :start` for actions that belong at the left/start edge, such as a sidebar toggle:
108
+
109
+ ```erb
110
+ <% header.with_action_button(href: "#", icon: :bars_3, label: "Open menu", placement: :start) %>
111
+ <% header.with_action_button(href: saved_posts_path, icon: :heart, label: "Saved", placement: :end) %>
112
+ ```
113
+
114
+ `position: :left` and `position: :right` are accepted as aliases, but `placement: :start` and `placement: :end` are preferred.
115
+
116
+ Header action buttons are desktop-only by default so mobile headers stay clean. Use `visible_on:` when an action should appear somewhere else:
117
+
118
+ ```erb
119
+ <% header.with_action_button(href: "#", icon: :bars_3, label: "Open menu", placement: :start, visible_on: :mobile) %>
120
+ <% header.with_action_button(href: settings_path, icon: :cog_6_tooth, label: "Settings", visible_on: :desktop) %>
121
+ ```
122
+
123
+ Accepted values are `:desktop`, `:mobile`, and `:always`. The default is `:desktop`.
124
+
125
+ Use `text:` when the action should be visible as a compact text button:
126
+
127
+ ```erb
128
+ <% header.with_action_button(href: saved_posts_path, text: "Saved", label: "Saved posts") %>
129
+ ```
130
+
131
+ You can combine icon and text:
132
+
133
+ ```erb
134
+ <% header.with_action_button(href: saved_posts_path, icon: :heart, text: "Saved", label: "Saved posts") %>
135
+ ```
136
+
137
+ ## CTA
138
+
139
+ ```erb
140
+ <% header.with_cta(href: new_post_path) { "New post" } %>
141
+ ```
142
+
143
+ ## User Menu
144
+
145
+ ```erb
146
+ <% header.with_user_menu(user_name: current_user.name, avatar_url: current_user.avatar_url) do %>
147
+ <%= link_to "Profile", profile_path, class: "menu-link" %>
148
+ <%= link_to "Settings", settings_path, class: "menu-link" %>
149
+ <% end %>
150
+ ```
151
+
152
+ ## Mobile Menu
153
+
154
+ ```erb
155
+ <% header.with_mobile_menu do %>
156
+ <nav aria-label="Mobile navigation">
157
+ <%= link_to "Posts", posts_path %>
158
+ <%= link_to "New post", new_post_path %>
159
+ </nav>
160
+ <% end %>
161
+ ```
162
+
163
+ ## Full Example
164
+
165
+ ```erb
166
+ <%= kz_header do |header| %>
167
+ <% header.with_brand(href: root_path) do %>
168
+ <span class="brand-mark">K</span>
169
+ <span>Kozenet <span class="accent">Blog</span></span>
170
+ <% end %>
171
+
172
+ <% header.with_nav_item(href: posts_path, active: current_page?(posts_path)) { "Posts" } %>
173
+ <% header.with_nav_item(href: new_post_path, active: current_page?(new_post_path)) { "New" } %>
174
+ <% header.with_search(placeholder: "Search posts", action: posts_path, value: params[:q]) %>
175
+ <% header.with_action_button(href: saved_posts_path, icon: :heart, label: "Saved") %>
176
+
177
+ <% header.with_user_menu(user_name: "Kozenet") do %>
178
+ <%= link_to "All posts", posts_path, class: "menu-link" %>
179
+ <%= link_to "New post", new_post_path, class: "menu-link" %>
180
+ <% end %>
181
+
182
+ <% header.with_mobile_menu do %>
183
+ <nav aria-label="Mobile navigation">
184
+ <%= link_to "Posts", posts_path %>
185
+ <%= link_to "New post", new_post_path %>
186
+ </nav>
187
+ <% end %>
188
+ <% end %>
189
+ ```
190
+
191
+ ## Best Practices
192
+
193
+ - Load `kozenet_ui_head_tags` once in the layout, not per page.
194
+ - Prefer initializer defaults for app-wide header behavior.
195
+ - Keep per-page overrides close to the render call.
196
+ - Use Heroicons names for icon actions.
197
+ - Always provide labels for icon-only actions.
198
+ - Keep navigation items short so desktop and mobile layouts stay balanced.
199
+ - Keep mobile actions intentional; prefer one menu/search trigger and move secondary links into `with_mobile_menu`.
@@ -0,0 +1,14 @@
1
+ # Foundations
2
+
3
+ Foundations are the shared design and runtime pieces behind Kozenet UI components.
4
+
5
+ - [Fonts](./fonts.md)
6
+
7
+ Future foundation docs can live here too:
8
+
9
+ - Colors
10
+ - Theme
11
+ - Tokens
12
+ - Icons
13
+ - JavaScript
14
+ - Accessibility