kozenet_ui 0.1.5 → 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 (43) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +79 -16
  3. data/app/assets/fonts/kozenet_ui/inter-latin.woff2 +0 -0
  4. data/app/assets/fonts/kozenet_ui/jetbrains-mono-latin.woff2 +0 -0
  5. data/app/assets/fonts/kozenet_ui/source-serif-4-latin.woff2 +0 -0
  6. data/app/assets/javascripts/kozenet_ui/index.js +226 -17
  7. data/app/assets/stylesheets/kozenet_ui/base.css +5 -0
  8. data/app/assets/stylesheets/kozenet_ui/components/avatar.css +4 -1
  9. data/app/assets/stylesheets/kozenet_ui/components/badge.css +11 -1
  10. data/app/assets/stylesheets/kozenet_ui/components/button.css +21 -1
  11. data/app/assets/stylesheets/kozenet_ui/components/header.css +227 -38
  12. data/app/assets/stylesheets/kozenet_ui/components/utilities.css +52 -1
  13. data/app/assets/stylesheets/kozenet_ui/fonts.css +37 -0
  14. data/app/assets/stylesheets/kozenet_ui/tokens.css +150 -53
  15. data/app/components/kozenet_ui/base_component.rb +21 -1
  16. data/app/components/kozenet_ui/header_component/action_button_component.html.erb +4 -2
  17. data/app/components/kozenet_ui/header_component/action_button_component.rb +76 -5
  18. data/app/components/kozenet_ui/header_component/nav_item_component.html.erb +1 -1
  19. data/app/components/kozenet_ui/header_component/nav_item_component.rb +6 -0
  20. data/app/components/kozenet_ui/header_component/user_menu_component.html.erb +5 -7
  21. data/app/components/kozenet_ui/header_component.html.erb +39 -30
  22. data/app/components/kozenet_ui/header_component.rb +26 -4
  23. data/app/helpers/kozenet_ui/component_helper.rb +82 -8
  24. data/app/helpers/kozenet_ui/icon_helper.rb +6 -19
  25. data/docs/README.md +25 -0
  26. data/docs/components/README.md +44 -0
  27. data/docs/components/avatar.md +73 -0
  28. data/docs/components/badge.md +74 -0
  29. data/docs/components/button.md +95 -0
  30. data/docs/components/header.md +199 -0
  31. data/docs/foundations/README.md +14 -0
  32. data/docs/foundations/fonts.md +136 -0
  33. data/lib/generators/kozenet_ui/install/install_generator.rb +94 -8
  34. data/lib/generators/kozenet_ui/install/templates/kozenet_ui.rb +3 -0
  35. data/lib/kozenet_ui/configuration.rb +35 -0
  36. data/lib/kozenet_ui/engine.rb +89 -14
  37. data/lib/kozenet_ui/theme/palette.rb +21 -7
  38. data/lib/kozenet_ui/theme/variants.rb +1 -0
  39. data/lib/kozenet_ui/version.rb +1 -1
  40. data/lib/kozenet_ui.rb +2 -0
  41. metadata +29 -22
  42. data/app/assets/images/kozenet_ui/icons/cart.svg +0 -1
  43. data/app/assets/images/kozenet_ui/icons/heart.svg +0 -1
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 630f1277f958277505d69a7087f698c54772615ce49b5e461127f252a18d532f
4
- data.tar.gz: 826562e84fcc99ae538683cbdf7685dd0065908eed308c6a58ec0ed9261a1371
3
+ metadata.gz: 7dba396e448979a52c6053afc1eb12bdb9a15bad47b3e84f0df956c7f825f799
4
+ data.tar.gz: 34168a38aeda23eb60c02c0cb3af1d9c2885fec1faa60a30d48d7c1ed779e626
5
5
  SHA512:
6
- metadata.gz: 7c06c46de9012183f644f314cfc60f58099369696a30b25b83ab5a10aff6fdb5f62f9d6f27c988002a7c36ad44d796b83fa9a721b63d36a727bf85fba0bc6910
7
- data.tar.gz: 356c12469d4f01569fc86ea4e46b036fd78f3c051c60daa0a1cc26e70761039f59f414bf60cde3fb9a215b7f8ee2a261fd9f833bb0d6a4e3a841c5aca485a843
6
+ metadata.gz: 8be433e46c7ccf17ab464e323cd2232a660c6438910e6d0de676eccf342f325774c61c0b62a080f9ecbffab5fda1cebb5688251643a2cd43990e6259ec9ed56d
7
+ data.tar.gz: 3ad8f9ca442520bd17598550219cb376dc70525a022918cc4e9b8dcd4742bd4b0026706989ea35a33ef93fa36ef57e04b008a93f6db16021592e81ebe0dce4cd
data/README.md CHANGED
@@ -1,56 +1,119 @@
1
+ [![Ruby CI](https://github.com/kozenetpro/kozenet_ui/actions/workflows/main.yml/badge.svg?branch=master&job=ruby)](https://github.com/kozenetpro/kozenet_ui/actions/workflows/main.yml?query=branch%3Amaster+job%3Aruby)
2
+ [![Kozenet UI CI](https://github.com/kozenetpro/kozenet_ui/actions/workflows/main.yml/badge.svg)](https://github.com/kozenetpro/kozenet_ui/actions/workflows/main.yml)
1
3
  # Kozenet UI
2
4
 
3
- [![Ruby](https://github.com/kozenetpro/kozenet_ui/actions/workflows/main.yml/badge.svg)](https://github.com/kozenetpro/kozenet_ui/actions/workflows/main.yml)
4
- ![Test Status](https://img.shields.io/badge/tests-passing-brightgreen?style=flat-square)
5
-
6
5
  Beautiful, minimal, Apple-inspired UI components for Rails.
7
6
 
8
7
  ## Installation
9
8
 
10
- Add to your Gemfile:
9
+ For local gem development, point the Rails app at this checkout:
11
10
 
12
11
  ```ruby
13
- gem "kozenet_ui", github: "kozenetpro/kozenet_ui"
12
+ gem "kozenet_ui", path: "../.."
14
13
  ```
15
14
 
16
15
  Then run:
17
16
 
18
17
  ```bash
19
18
  bundle install
19
+ bin/rails generate kozenet_ui:install
20
+ ```
21
+
22
+ For an app outside this repository, use GitHub:
23
+
24
+ ```ruby
25
+ gem "kozenet_ui", github: "kozenetpro/kozenet_ui"
20
26
  ```
21
27
 
22
- If you want to use a released version from RubyGems.org (after release):
28
+ Or use the released RubyGems version:
23
29
 
24
30
  ```bash
25
31
  bundle add kozenet_ui
26
32
  ```
27
33
 
28
- Or:
34
+ Then run:
29
35
 
30
36
  ```bash
31
- gem install kozenet_ui
37
+ bin/rails generate kozenet_ui:install
32
38
  ```
33
39
 
40
+ `gem install kozenet_ui` is not enough for a Rails app. The app must have
41
+ `kozenet_ui` in its `Gemfile`, because Rails generators are loaded through
42
+ Bundler.
43
+
34
44
  ## Usage in your Rails app
35
45
 
36
- 1. **Add the theme variables tag to your layout `<head>`:**
46
+ Usage guides live in [docs](docs/README.md):
47
+
48
+ - [Components](docs/components/README.md)
49
+ - [Fonts](docs/foundations/fonts.md)
50
+
51
+ 1. **Load Kozenet UI once from your layout `<head>`:**
52
+ ```erb
53
+ <%= kozenet_ui_head_tags %>
54
+ ```
55
+
56
+ This loads digested CSS assets, theme variables, and JavaScript. When using
57
+ importmap, keep this after `javascript_importmap_tags`.
58
+
59
+ In Rails 8 apps using `stylesheet_link_tag :app`, the install generator
60
+ copies Kozenet UI styles into `app/assets/stylesheets/kozenet_ui` and inserts:
61
+
37
62
  ```erb
38
- <%= kozenet_ui_theme_variables_tag %>
63
+ <%= kozenet_ui_head_tags(stylesheets: false) %>
39
64
  ```
40
65
 
41
- 2. **Import Kozenet UI styles in your main application.css:**
66
+ 2. **Customize copied styles when needed:**
67
+ The install generator copies Kozenet UI CSS into
68
+ `app/assets/stylesheets/kozenet_ui`, so developers can edit the frontend
69
+ without touching gem internals.
70
+
71
+ 3. **Only use CSS imports when you have a CSS bundler:**
42
72
  ```css
43
73
  @import "kozenet_ui/tokens.css";
74
+ @import "kozenet_ui/fonts.css";
44
75
  @import "kozenet_ui/base.css";
45
- @import "kozenet_ui/components.css";
76
+ @import "kozenet_ui/components/button.css";
77
+ @import "kozenet_ui/components/header.css";
78
+ @import "kozenet_ui/components/avatar.css";
79
+ @import "kozenet_ui/components/badge.css";
80
+ @import "kozenet_ui/components/utilities.css";
81
+ ```
82
+
83
+ For plain Propshaft/Sprockets apps, prefer `kozenet_ui_head_tags`, because
84
+ stylesheet tags generate digest-safe production asset URLs.
85
+
86
+ 4. **Skip direct stylesheet tags if your app bundles the CSS itself:**
87
+ ```erb
88
+ <%= kozenet_ui_head_tags(stylesheets: false) %>
46
89
  ```
47
90
 
48
- 3. **(Optional) Override icons:**
49
- Place your own SVGs in `app/assets/images/icons/` to override the gem's icons.
91
+ 5. **Use Heroicons by name:**
92
+ ```erb
93
+ <% header.with_action_button(href: "/saved", icon: :heart, label: "Saved") %>
94
+ <% header.with_action_button(href: "/cart", icon: :shopping_cart, label: "Cart") %>
95
+ ```
50
96
 
51
- 4. **Customize colors in `config/initializers/kozenet_ui.rb`.**
97
+ Kozenet UI normalizes Ruby-style names like `:shopping_cart` to Heroicons'
98
+ `shopping-cart` name.
99
+
100
+ 6. **Customize colors and component defaults in `config/initializers/kozenet_ui.rb`:**
101
+ ```ruby
102
+ KozenetUi.configure do |config|
103
+ config.theme = :system
104
+ config.stimulus_prefix = "kz"
105
+ config.component :header, sticky: true, blur: true
106
+ end
107
+ ```
108
+
109
+ Per-render options still win:
110
+ ```erb
111
+ <%= kz_header(sticky: false) do |header| %>
112
+ ...
113
+ <% end %>
114
+ ```
52
115
 
53
- 5. **Use components in your views:**
116
+ 7. **Use components in your views:**
54
117
  ```erb
55
118
  <%= render KozenetUi::HeaderComponent.new do |header| %>
56
119
  ...
@@ -1,23 +1,232 @@
1
1
  // Kozenet UI JavaScript Entry Point
2
- import { Application } from "@hotwired/stimulus"
2
+ import { Application, Controller } from "@hotwired/stimulus"
3
3
 
4
- // Import controllers
5
- import HeaderController from "./controllers/header_controller"
6
- import MobileNavController from "./controllers/mobile_nav_controller"
7
- import DropdownController from "./controllers/dropdown_controller"
8
- import UserMenuController from "./controllers/user_menu_controller"
4
+ class HeaderController extends Controller {
5
+ static targets = ["container"]
9
6
 
10
- // Start Stimulus application
11
- const application = Application.start()
7
+ connect() {
8
+ this.scrolled = false
9
+ }
12
10
 
13
- // Configure Stimulus
14
- application.debug = false
15
- window.Stimulus = application
11
+ handleScroll() {
12
+ const scrollPosition = window.scrollY
16
13
 
17
- // Register controllers with kz- prefix
18
- application.register("kz-header", HeaderController)
19
- application.register("kz-mobile-nav", MobileNavController)
20
- application.register("kz-dropdown", DropdownController)
21
- application.register("kz-user-menu", UserMenuController)
14
+ if (scrollPosition > 10 && !this.scrolled) {
15
+ this.scrolled = true
16
+ this.element.classList.add("kz-header-scrolled")
17
+ } else if (scrollPosition <= 10 && this.scrolled) {
18
+ this.scrolled = false
19
+ this.element.classList.remove("kz-header-scrolled")
20
+ }
21
+ }
22
22
 
23
- export { application }
23
+ toggleSearch(event) {
24
+ event.preventDefault()
25
+ const searchCol = this.element.querySelector(".kz-search-col")
26
+
27
+ if (searchCol) {
28
+ searchCol.classList.toggle("hidden")
29
+ searchCol.classList.toggle("block")
30
+ const input = searchCol.querySelector("input")
31
+ if (input) input.focus()
32
+ }
33
+ }
34
+ }
35
+
36
+ class MobileNavController extends Controller {
37
+ static targets = ["panel", "trigger"]
38
+
39
+ connect() {
40
+ this.isOpen = false
41
+ }
42
+
43
+ toggle(event) {
44
+ event.preventDefault()
45
+ this.isOpen = !this.isOpen
46
+
47
+ if (this.isOpen) {
48
+ this.open()
49
+ } else {
50
+ this.close()
51
+ }
52
+ }
53
+
54
+ open() {
55
+ if (!this.hasPanelTarget || !this.hasTriggerTarget) return
56
+
57
+ this.panelTarget.classList.remove("hidden", "scale-y-0")
58
+ this.panelTarget.classList.add("scale-y-100")
59
+ this.triggerTarget.setAttribute("aria-expanded", "true")
60
+ document.body.style.overflow = "hidden"
61
+ }
62
+
63
+ close() {
64
+ if (!this.hasPanelTarget || !this.hasTriggerTarget) return
65
+
66
+ this.panelTarget.classList.add("scale-y-0")
67
+ this.panelTarget.classList.remove("scale-y-100")
68
+ this.triggerTarget.setAttribute("aria-expanded", "false")
69
+
70
+ setTimeout(() => {
71
+ this.panelTarget.classList.add("hidden")
72
+ document.body.style.overflow = ""
73
+ }, 300)
74
+ }
75
+
76
+ disconnect() {
77
+ document.body.style.overflow = ""
78
+ }
79
+ }
80
+
81
+ class DropdownController extends Controller {
82
+ static targets = ["menu"]
83
+
84
+ connect() {
85
+ this.isOpen = false
86
+ }
87
+
88
+ toggle(event) {
89
+ event.preventDefault()
90
+ event.stopPropagation()
91
+
92
+ this.isOpen = !this.isOpen
93
+
94
+ if (this.isOpen) {
95
+ this.open()
96
+ } else {
97
+ this.close()
98
+ }
99
+ }
100
+
101
+ open() {
102
+ if (!this.hasMenuTarget) return
103
+
104
+ this.menuTarget.classList.remove("hidden")
105
+ this.menuTarget.classList.add("animate-fadeIn")
106
+ this.element.setAttribute("aria-expanded", "true")
107
+
108
+ setTimeout(() => {
109
+ document.addEventListener("click", this.closeOnOutsideClick)
110
+ }, 10)
111
+ }
112
+
113
+ close() {
114
+ if (!this.hasMenuTarget) return
115
+
116
+ this.menuTarget.classList.add("hidden")
117
+ this.menuTarget.classList.remove("animate-fadeIn")
118
+ this.element.setAttribute("aria-expanded", "false")
119
+
120
+ document.removeEventListener("click", this.closeOnOutsideClick)
121
+ }
122
+
123
+ closeOnOutsideClick = (event) => {
124
+ if (!this.element.contains(event.target)) {
125
+ this.close()
126
+ }
127
+ }
128
+
129
+ disconnect() {
130
+ document.removeEventListener("click", this.closeOnOutsideClick)
131
+ }
132
+ }
133
+
134
+ class UserMenuController extends Controller {
135
+ static targets = ["dropdown"]
136
+
137
+ connect() {
138
+ this.isOpen = false
139
+ }
140
+
141
+ toggle(event) {
142
+ event.preventDefault()
143
+ event.stopPropagation()
144
+
145
+ this.isOpen = !this.isOpen
146
+
147
+ if (this.isOpen) {
148
+ this.open()
149
+ } else {
150
+ this.close()
151
+ }
152
+ }
153
+
154
+ open() {
155
+ if (!this.hasDropdownTarget) return
156
+
157
+ this.dropdownTarget.classList.remove("hidden")
158
+ this.dropdownTarget.classList.add("animate-fadeIn")
159
+ this.element.querySelector("button")?.setAttribute("aria-expanded", "true")
160
+
161
+ setTimeout(() => {
162
+ document.addEventListener("click", this.handleOutsideClick)
163
+ document.addEventListener("keydown", this.handleEscape)
164
+ }, 10)
165
+ }
166
+
167
+ close() {
168
+ if (!this.hasDropdownTarget) return
169
+
170
+ this.dropdownTarget.classList.add("hidden")
171
+ this.dropdownTarget.classList.remove("animate-fadeIn")
172
+ this.element.querySelector("button")?.setAttribute("aria-expanded", "false")
173
+
174
+ document.removeEventListener("click", this.handleOutsideClick)
175
+ document.removeEventListener("keydown", this.handleEscape)
176
+ }
177
+
178
+ handleOutsideClick = (event) => {
179
+ if (!this.element.contains(event.target)) {
180
+ this.close()
181
+ }
182
+ }
183
+
184
+ handleEscape = (event) => {
185
+ if (event.key === "Escape") {
186
+ this.close()
187
+ }
188
+ }
189
+
190
+ disconnect() {
191
+ document.removeEventListener("click", this.handleOutsideClick)
192
+ document.removeEventListener("keydown", this.handleEscape)
193
+ }
194
+ }
195
+
196
+ function configuredStimulusPrefix() {
197
+ return document.querySelector("meta[name='kozenet-ui-stimulus-prefix']")?.content?.trim() || "kz"
198
+ }
199
+
200
+ // Auto-initialization function
201
+ function startKozenetUi() {
202
+ let application = window.Stimulus
203
+
204
+ if (!application) {
205
+ application = Application.start()
206
+ application.debug = false
207
+ window.Stimulus = application
208
+ }
209
+
210
+ window.KozenetUi = Object.assign(window.KozenetUi || {}, {
211
+ stimulusPrefix: configuredStimulusPrefix()
212
+ })
213
+
214
+ if (!window.KozenetUi.controllersRegistered) {
215
+ Array.from(new Set(["kz", window.KozenetUi.stimulusPrefix])).forEach(prefix => {
216
+ application.register(`${prefix}-header`, HeaderController)
217
+ application.register(`${prefix}-mobile-nav`, MobileNavController)
218
+ application.register(`${prefix}-dropdown`, DropdownController)
219
+ application.register(`${prefix}-user-menu`, UserMenuController)
220
+ })
221
+ window.KozenetUi.controllersRegistered = true
222
+ }
223
+ }
224
+
225
+ // Ensure we initialize after the host app has a chance to set up window.Stimulus
226
+ if (document.readyState === "loading") {
227
+ document.addEventListener("DOMContentLoaded", () => setTimeout(startKozenetUi, 10))
228
+ } else {
229
+ setTimeout(startKozenetUi, 10)
230
+ }
231
+
232
+ export { HeaderController, MobileNavController, DropdownController, UserMenuController, startKozenetUi as start }
@@ -2,6 +2,11 @@
2
2
 
3
3
  @layer base {
4
4
  /* Reset and base improvements */
5
+ html, body {
6
+ margin: 0;
7
+ padding: 0;
8
+ }
9
+
5
10
  *,
6
11
  *::before,
7
12
  *::after {
@@ -2,6 +2,9 @@
2
2
  /* Avatar base */
3
3
  .kz-avatar {
4
4
  position: relative;
5
+ display: inline-flex;
6
+ align-items: center;
7
+ justify-content: center;
5
8
  flex-shrink: 0;
6
9
  border-radius: var(--kz-radius-md);
7
10
  background: linear-gradient(135deg, #6366f1, #0ea5e9);
@@ -85,4 +88,4 @@
85
88
  height: 100%;
86
89
  object-fit: cover;
87
90
  }
88
- }
91
+ }
@@ -1,6 +1,9 @@
1
1
  @layer components {
2
2
  /* Badge base */
3
3
  .kz-badge {
4
+ display: inline-flex;
5
+ align-items: center;
6
+ gap: 0.25rem;
4
7
  border-radius: var(--kz-radius-sm);
5
8
  font-weight: var(--kz-font-weight-bold);
6
9
  letter-spacing: 0.05em;
@@ -47,6 +50,13 @@
47
50
  box-shadow: 0 2px 8px -2px rgba(139, 92, 246, 0.4);
48
51
  }
49
52
 
53
+ .kz-badge.kz-badge-accent,
54
+ .kz-badge.kz-variant-accent {
55
+ background: linear-gradient(120deg, #06b6d4, #0891b2);
56
+ color: white;
57
+ box-shadow: 0 2px 8px -2px rgba(6, 182, 212, 0.4);
58
+ }
59
+
50
60
  .kz-badge.kz-badge-success,
51
61
  .kz-badge.kz-variant-success {
52
62
  background: linear-gradient(120deg, #10b981, #059669);
@@ -98,4 +108,4 @@
98
108
  width: 0.875em;
99
109
  height: 0.875em;
100
110
  }
101
- }
111
+ }
@@ -2,6 +2,10 @@
2
2
  /* Base button styles */
3
3
  .kz-btn {
4
4
  position: relative;
5
+ display: inline-flex;
6
+ align-items: center;
7
+ justify-content: center;
8
+ gap: 0.5rem;
5
9
  border: 0;
6
10
  cursor: pointer;
7
11
  user-select: none;
@@ -136,6 +140,22 @@
136
140
  background: rgba(255,255,255,0.08);
137
141
  }
138
142
 
143
+ @media (prefers-color-scheme: dark) {
144
+ :root:not([data-theme="light"]):not(.light) .kz-btn.kz-variant-secondary {
145
+ background: linear-gradient(135deg, rgba(255,255,255,.12), rgba(255,255,255,.08));
146
+ border-color: rgba(255,255,255,0.1);
147
+ }
148
+
149
+ :root:not([data-theme="light"]):not(.light) .kz-btn.kz-variant-secondary:hover:not(:disabled) {
150
+ background: linear-gradient(135deg, rgba(255,255,255,.18), rgba(255,255,255,.12));
151
+ }
152
+
153
+ :root:not([data-theme="light"]):not(.light) .kz-btn.kz-variant-ghost:hover:not(:disabled),
154
+ :root:not([data-theme="light"]):not(.light) .kz-btn.kz-variant-outline:hover:not(:disabled) {
155
+ background: rgba(255,255,255,0.08);
156
+ }
157
+ }
158
+
139
159
  /* Accent variant */
140
160
  .kz-btn.kz-variant-accent {
141
161
  background: linear-gradient(125deg, #6366f1, #0ea5e9 55%, #06b6d4);
@@ -227,4 +247,4 @@
227
247
  transform: none;
228
248
  }
229
249
  }
230
- }
250
+ }