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,270 @@
1
+ @layer components {
2
+ /* Layout utilities */
3
+ .container-fluid {
4
+ width: 100%;
5
+ margin-left: auto;
6
+ margin-right: auto;
7
+ padding-left: 1rem;
8
+ padding-right: 1rem;
9
+ }
10
+
11
+ @media (min-width:640px) {
12
+ .container-fluid {
13
+ padding-left: 1.5rem;
14
+ padding-right: 1.5rem;
15
+ }
16
+ }
17
+
18
+ @media (min-width:1024px) {
19
+ .container-fluid {
20
+ padding-left: 2rem;
21
+ padding-right: 2rem;
22
+ }
23
+ }
24
+
25
+ /* Card components */
26
+ .card {
27
+ border-radius: var(--kz-radius-lg);
28
+ background: color-mix(in srgb, var(--kz-bg-elevated) 80%, transparent);
29
+ backdrop-filter: blur(8px);
30
+ border: 1px solid var(--kz-border-default);
31
+ box-shadow: var(--kz-shadow-sm);
32
+ padding: 1.25rem;
33
+ display: flex;
34
+ flex-direction: column;
35
+ gap: 1rem;
36
+ transition: box-shadow var(--kz-transition-base);
37
+ }
38
+
39
+ .card:hover {
40
+ box-shadow: var(--kz-shadow-md);
41
+ }
42
+
43
+ .card-muted {
44
+ background: color-mix(in srgb, var(--kz-bg-muted) 70%, transparent);
45
+ }
46
+
47
+ /* Stats display */
48
+ .stat {
49
+ display: flex;
50
+ flex-direction: column;
51
+ gap: 0.25rem;
52
+ }
53
+
54
+ .stat-label {
55
+ font-size: var(--kz-font-size-xs);
56
+ font-weight: var(--kz-font-weight-medium);
57
+ text-transform: uppercase;
58
+ letter-spacing: 0.05em;
59
+ color: var(--kz-text-muted);
60
+ }
61
+
62
+ .stat-value {
63
+ font-size: var(--kz-font-size-2xl);
64
+ font-weight: var(--kz-font-weight-semibold);
65
+ }
66
+
67
+ /* Simple button utilities (use kz-btn for full featured buttons) */
68
+ .btn {
69
+ display: inline-flex;
70
+ align-items: center;
71
+ justify-content: center;
72
+ gap: 0.5rem;
73
+ font-weight: var(--kz-font-weight-medium);
74
+ font-size: var(--kz-font-size-sm);
75
+ border-radius: var(--kz-radius-md);
76
+ padding: 0.5rem 1rem;
77
+ user-select: none;
78
+ cursor: pointer;
79
+ transition: all var(--kz-transition-fast);
80
+ position: relative;
81
+ border: 0;
82
+ background: none;
83
+ }
84
+
85
+ .btn:focus-visible {
86
+ outline: 2px solid rgb(99 102 241 / 0.5);
87
+ outline-offset: 2px;
88
+ }
89
+
90
+ .btn:disabled {
91
+ opacity: 0.4;
92
+ cursor: not-allowed;
93
+ }
94
+
95
+ .btn-primary {
96
+ background: linear-gradient(110deg, #6366f1, #0ea5e9 55%, #06b6d4);
97
+ color: #fff;
98
+ }
99
+
100
+ .btn-primary:hover:not(:disabled) {
101
+ transform: translateY(-2px);
102
+ box-shadow: var(--kz-shadow-md);
103
+ }
104
+
105
+ .btn-outline {
106
+ background: transparent;
107
+ border: 1px solid var(--kz-border-default);
108
+ color: var(--kz-text-default);
109
+ }
110
+
111
+ .btn-outline:hover:not(:disabled) {
112
+ background: var(--kz-bg-muted);
113
+ }
114
+
115
+ .btn-ghost {
116
+ background: transparent;
117
+ }
118
+
119
+ .btn-ghost:hover:not(:disabled) {
120
+ background: var(--kz-bg-muted);
121
+ }
122
+
123
+ .btn-xs {
124
+ font-size: var(--kz-font-size-xs);
125
+ padding: 0.35rem 0.6rem;
126
+ border-radius: var(--kz-radius-sm);
127
+ }
128
+
129
+ /* Badge utility (simple version) */
130
+ .badge {
131
+ display: inline-flex;
132
+ align-items: center;
133
+ border-radius: var(--kz-radius-full);
134
+ padding: 0 0.5rem;
135
+ height: 1.5rem;
136
+ font-size: var(--kz-font-size-xs);
137
+ font-weight: var(--kz-font-weight-semibold);
138
+ background: linear-gradient(120deg, rgba(99,102,241,.15), rgba(14,165,233,.18));
139
+ color: var(--kz-text-muted);
140
+ letter-spacing: 0.05em;
141
+ }
142
+
143
+ /* Input utility */
144
+ .input {
145
+ width: 100%;
146
+ border-radius: var(--kz-radius-md);
147
+ border: 1px solid var(--kz-border-default);
148
+ background: var(--kz-bg-elevated);
149
+ padding: 0.5rem 0.75rem;
150
+ font-size: var(--kz-font-size-sm);
151
+ line-height: 1.25rem;
152
+ transition: all var(--kz-transition-fast);
153
+ }
154
+
155
+ .input::placeholder {
156
+ color: var(--kz-text-muted);
157
+ }
158
+
159
+ .input:focus {
160
+ outline: 2px solid rgb(99 102 241 / 0.5);
161
+ outline-offset: 2px;
162
+ border-color: transparent;
163
+ }
164
+
165
+ /* Divider */
166
+ .divider {
167
+ width: 100%;
168
+ height: 1px;
169
+ background: var(--kz-border-default);
170
+ }
171
+
172
+ /* Navigation section label */
173
+ .nav-section-label {
174
+ font-size: 0.625rem;
175
+ font-weight: var(--kz-font-weight-semibold);
176
+ letter-spacing: 0.1em;
177
+ text-transform: uppercase;
178
+ color: var(--kz-text-muted);
179
+ padding-left: 0.75rem;
180
+ padding-right: 0.75rem;
181
+ margin-top: 1.5rem;
182
+ margin-bottom: 0.5rem;
183
+ }
184
+
185
+ /* Glass morphism effect */
186
+ .glass-panel {
187
+ background: rgba(255, 255, 255, 0.1);
188
+ backdrop-filter: blur(20px);
189
+ border: 1px solid rgba(255, 255, 255, 0.2);
190
+ border-radius: var(--kz-radius-lg);
191
+ box-shadow: var(--kz-shadow-lg);
192
+ }
193
+
194
+ .dark .glass-panel {
195
+ background: rgba(0, 0, 0, 0.2);
196
+ border-color: rgba(255, 255, 255, 0.1);
197
+ }
198
+
199
+ /* Play surface (gradient background) */
200
+ .play-surface {
201
+ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
202
+ min-height: 100vh;
203
+ }
204
+
205
+ /* Custom slider */
206
+ .slider {
207
+ -webkit-appearance: none;
208
+ appearance: none;
209
+ background: linear-gradient(to right, #8b5cf6 0%, #8b5cf6 50%, #e5e7eb 50%, #e5e7eb 100%);
210
+ outline: none;
211
+ border-radius: var(--kz-radius-md);
212
+ height: 8px;
213
+ }
214
+
215
+ .slider::-webkit-slider-thumb {
216
+ -webkit-appearance: none;
217
+ appearance: none;
218
+ height: 20px;
219
+ width: 20px;
220
+ border-radius: 50%;
221
+ background: #8b5cf6;
222
+ cursor: pointer;
223
+ box-shadow: var(--kz-shadow-sm);
224
+ }
225
+
226
+ .slider::-moz-range-thumb {
227
+ height: 20px;
228
+ width: 20px;
229
+ border-radius: 50%;
230
+ background: #8b5cf6;
231
+ cursor: pointer;
232
+ border: none;
233
+ box-shadow: var(--kz-shadow-sm);
234
+ }
235
+
236
+ /* Animations */
237
+ @keyframes fadeIn {
238
+ from {
239
+ opacity: 0;
240
+ transform: translateY(4px);
241
+ }
242
+ to {
243
+ opacity: 1;
244
+ transform: translateY(0);
245
+ }
246
+ }
247
+
248
+ .animate-fadeIn {
249
+ animation: fadeIn 0.3s ease-out;
250
+ }
251
+ }
252
+
253
+ /* Theme icon utilities */
254
+ [data-theme] .theme-icon-light,
255
+ [data-theme] .theme-icon-dark,
256
+ [data-theme] .theme-icon-system {
257
+ display: none;
258
+ }
259
+
260
+ [data-theme='light'] .theme-icon-dark {
261
+ display: inline-flex;
262
+ }
263
+
264
+ [data-theme='dark'] .theme-icon-light {
265
+ display: inline-flex;
266
+ }
267
+
268
+ [data-theme='system'] .theme-icon-system {
269
+ display: inline-flex;
270
+ }
@@ -0,0 +1,8 @@
1
+ /* Kozenet UI Components */
2
+ /* Import all component styles */
3
+
4
+ @import './components/button.css';
5
+ @import './components/header.css';
6
+ @import './components/avatar.css';
7
+ @import './components/badge.css';
8
+ @import './components/utilities.css';
@@ -0,0 +1,168 @@
1
+ /* Kozenet UI Design Tokens */
2
+ /* Auto-generated from Ruby tokens - Can be overridden */
3
+
4
+ @layer base {
5
+ :root {
6
+ /* Spacing */
7
+ --kz-spacing-xs: 0.25rem;
8
+ --kz-spacing-sm: 0.5rem;
9
+ --kz-spacing-md: 1rem;
10
+ --kz-spacing-lg: 1.5rem;
11
+ --kz-spacing-xl: 2rem;
12
+ --kz-spacing-2xl: 3rem;
13
+ --kz-spacing-3xl: 4rem;
14
+
15
+ /* Border Radius */
16
+ --kz-radius-sm: 12px;
17
+ --kz-radius-md: 16px;
18
+ --kz-radius-lg: 20px;
19
+ --kz-radius-xl: 24px;
20
+ --kz-radius-2xl: 28px;
21
+ --kz-radius-full: 9999px;
22
+
23
+ /* Typography */
24
+ --kz-font-size-xs: 0.65rem;
25
+ --kz-font-size-sm: 0.75rem;
26
+ --kz-font-size-base: 0.875rem;
27
+ --kz-font-size-lg: 1rem;
28
+ --kz-font-size-xl: 1.25rem;
29
+ --kz-font-size-2xl: 1.5rem;
30
+ --kz-font-size-3xl: 2rem;
31
+
32
+ --kz-font-weight-normal: 400;
33
+ --kz-font-weight-medium: 500;
34
+ --kz-font-weight-semibold: 600;
35
+ --kz-font-weight-bold: 700;
36
+
37
+ /* Shadows */
38
+ --kz-shadow-sm: 0 1px 2px rgba(0,0,0,.05), 0 2px 6px -2px rgba(0,0,0,.08);
39
+ --kz-shadow-md: 0 4px 14px -6px rgba(0,0,0,.25);
40
+ --kz-shadow-lg: 0 10px 30px -12px rgba(0,0,0,.45);
41
+ --kz-shadow-xl: 0 20px 60px -26px rgba(0,0,0,.28);
42
+
43
+ /* Transitions */
44
+ --kz-transition-fast: 0.15s ease;
45
+ --kz-transition-base: 0.25s ease;
46
+ --kz-transition-slow: 0.35s ease;
47
+
48
+ /* Z-index */
49
+ --kz-z-dropdown: 50;
50
+ --kz-z-sticky: 60;
51
+ --kz-z-modal: 70;
52
+ --kz-z-popover: 80;
53
+ --kz-z-toast: 90;
54
+
55
+ /* Default Light Mode Colors */
56
+ --kz-bg-base: #ffffff;
57
+ --kz-bg-elevated: #f8fafc;
58
+ --kz-bg-muted: #f1f5f9;
59
+ --kz-text-default: #0f172a;
60
+ --kz-text-muted: #64748b;
61
+ --kz-border-default: #e2e8f0;
62
+ --kz-border-muted: #f1f5f9;
63
+
64
+ --kz-primary-500: 99, 102, 241; /* #6366f1 */
65
+ --kz-accent-500: 6, 182, 212; /* #06b6d4 */
66
+ --kz-accent-400: 14, 165, 233; /* #0ea5e9 */
67
+ --kz-cta-gradient: linear-gradient(110deg, #6366f1, #0ea5e9 55%, #06b6d4);
68
+ --kz-cta-gradient-dark: linear-gradient(110deg, #818cf8, #38bdf8 55%, #06b6d4);
69
+ --kz-cta-text: #fff;
70
+ --kz-cta-text-dark: #fff;
71
+
72
+ --bg-default: #ffffff;
73
+ --bg-muted: #f8fafc;
74
+ --bg-elevated: #ffffff;
75
+ --bg-accent: #0ea5e9;
76
+ --bg-accent-hover: #0284c7;
77
+ --text-default: #0f172a;
78
+ --text-muted: #64748b;
79
+ --border-default: #e2e8f0;
80
+ --border-strong: #cbd5e1;
81
+ --focus-ring: #0ea5e9;
82
+ --radius-xs: 2px;
83
+ --radius-sm: 4px;
84
+ --radius-md: 6px;
85
+ --radius-lg: 8px;
86
+ --radius-xl: 12px;
87
+ --radius-pill: 999px;
88
+ --shadow-color: 210 40% 2%;
89
+ --gradient-base-from: #f0f9ff;
90
+ --gradient-base-to: #e0f2fe;
91
+ --gradient-accent-from: #6366f1;
92
+ --gradient-accent-via: #0ea5e9;
93
+ --gradient-accent-to: #06b6d4;
94
+ --gradient-spot-1: rgba(99,102,241,0.35);
95
+ --gradient-spot-2: rgba(14,165,233,0.30);
96
+ }
97
+
98
+ /* Dark Mode */
99
+ [data-theme="dark"],
100
+ .dark {
101
+ --kz-bg-base: #0f172a;
102
+ --kz-bg-elevated: #1e293b;
103
+ --kz-bg-muted: #334155;
104
+ --kz-text-default: #f1f5f9;
105
+ --kz-text-muted: #94a3b8;
106
+ --kz-border-default: #334155;
107
+ --kz-border-muted: #1e293b;
108
+
109
+ --kz-cta-gradient: var(--kz-cta-gradient-dark);
110
+ --kz-cta-text: var(--kz-cta-text-dark);
111
+
112
+ --bg-default: #0b0d11;
113
+ --bg-muted: #11151c;
114
+ --bg-elevated: rgba(23,28,36,0.9);
115
+ --bg-accent: #0284c7;
116
+ --bg-accent-hover: #0369a1;
117
+ --text-default: #f1f5f9;
118
+ --text-muted: #64748b;
119
+ --border-default: #1f242c;
120
+ --border-strong: #2a3039;
121
+ --focus-ring: #0ea5e9;
122
+ --shadow-color: 210 40% 2%;
123
+ --gradient-base-from: #0b0d11;
124
+ --gradient-base-to: #11151c;
125
+ --gradient-accent-from: #1e40af;
126
+ --gradient-accent-via: #0369a1;
127
+ --gradient-accent-to: #0891b2;
128
+ --gradient-spot-1: rgba(56,189,248,0.20);
129
+ --gradient-spot-2: rgba(99,102,241,0.18);
130
+ }
131
+
132
+ html { -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale; }
133
+ body { background: var(--bg-default); color: var(--text-default); font-family: var(--font-sans, ui-sans-serif, system-ui, sans-serif); }
134
+ * { border-color: var(--border-default); }
135
+ ::selection { background: var(--bg-accent); color: #fff; }
136
+ .app-bg {
137
+ position: relative;
138
+ min-height: 100vh;
139
+ overflow-x: hidden;
140
+ isolation: isolate;
141
+ }
142
+ .app-bg:not(:first-of-type) { background: none !important; }
143
+ .app-bg::before {
144
+ content: "";
145
+ position: fixed; inset:0; pointer-events:none; z-index:-1;
146
+ background:
147
+ radial-gradient(circle at 18% 22%, var(--gradient-spot-1) 0, transparent 60%),
148
+ radial-gradient(circle at 82% 18%, var(--gradient-spot-2) 0, transparent 62%),
149
+ linear-gradient(135deg, var(--gradient-base-from) 0%, var(--gradient-base-to) 100%);
150
+ background-attachment: fixed;
151
+ }
152
+ .dark .app-bg::before {
153
+ background:
154
+ radial-gradient(circle at 18% 22%, var(--gradient-spot-1) 0, transparent 60%),
155
+ radial-gradient(circle at 82% 18%, var(--gradient-spot-2) 0, transparent 62%),
156
+ linear-gradient(140deg, var(--gradient-base-from) 0%, var(--gradient-base-to) 100%);
157
+ }
158
+ .app-bg::after {
159
+ content: "";
160
+ position: fixed; inset:0; pointer-events:none; z-index:-1; mix-blend-mode:overlay;
161
+ background: linear-gradient(120deg, var(--gradient-accent-from), var(--gradient-accent-via) 55%, var(--gradient-accent-to));
162
+ opacity: .07; border-radius: inherit;
163
+ }
164
+ .dark .app-bg::after { opacity:.10; }
165
+ @supports (-webkit-touch-callout: none) {
166
+ .app-bg::before, .app-bg::after { position: absolute; }
167
+ }
168
+ }
@@ -0,0 +1,72 @@
1
+ # frozen_string_literal: true
2
+
3
+ module KozenetUi
4
+ # Avatar component for user profiles and images
5
+ # Supports images, initials, and icons with multiple sizes
6
+ #
7
+ # @example With image
8
+ # <%= kz_avatar(src: user.avatar_url, alt: user.name) %>
9
+ #
10
+ # @example With initials
11
+ # <%= kz_avatar(initials: "JD", variant: :primary) %>
12
+ #
13
+ # @example With custom size
14
+ # <%= kz_avatar(src: url, size: :lg) %>
15
+ class AvatarComponent < BaseComponent
16
+ # rubocop:disable Metrics/ParameterLists
17
+ def initialize(
18
+ src: nil,
19
+ alt: "Avatar",
20
+ initials: nil,
21
+ variant: :primary,
22
+ size: :md,
23
+ html_options: {}
24
+ )
25
+ super(variant: variant, size: size, **html_options)
26
+ @src = src
27
+ @alt = alt
28
+ @initials = initials
29
+ end
30
+ # rubocop:enable Metrics/ParameterLists
31
+
32
+ def call
33
+ tag.div(**html_attrs) do
34
+ avatar_content
35
+ end
36
+ end
37
+
38
+ private
39
+
40
+ def base_classes
41
+ "kz-avatar inline-flex items-center justify-center overflow-hidden"
42
+ end
43
+
44
+ def avatar_content
45
+ if @src.present?
46
+ tag.img(src: @src, alt: @alt, class: "w-full h-full object-cover")
47
+ elsif @initials.present?
48
+ tag.span(class: "kz-avatar-initials") { @initials }
49
+ else
50
+ default_icon
51
+ end
52
+ end
53
+
54
+ # rubocop:disable Metrics/MethodLength
55
+ def default_icon
56
+ tag.svg(
57
+ width: "20",
58
+ height: "20",
59
+ viewBox: "0 0 24 24",
60
+ fill: "none",
61
+ stroke: "currentColor",
62
+ stroke_width: "2"
63
+ ) do
64
+ safe_join([
65
+ tag.circle(cx: "12", cy: "8", r: "5"),
66
+ tag.path(d: "M20 21a8 8 0 1 0-16 0")
67
+ ])
68
+ end
69
+ end
70
+ # rubocop:enable Metrics/MethodLength
71
+ end
72
+ end
@@ -0,0 +1,62 @@
1
+ # frozen_string_literal: true
2
+
3
+ module KozenetUi
4
+ # Badge component for labels, statuses, and counts
5
+ # Supports multiple variants and sizes
6
+ #
7
+ # @example Status badge
8
+ # <%= kz_badge(variant: :success) { "Active" } %>
9
+ #
10
+ # @example Count badge
11
+ # <%= kz_badge(variant: :primary, size: :sm) { "99+" } %>
12
+ #
13
+ # @example With icon
14
+ # <%= kz_badge(variant: :warning) do |badge| %>
15
+ # <% badge.with_icon do %>
16
+ # <svg>...</svg>
17
+ # <% end %>
18
+ # Pending
19
+ # <% end %>
20
+ class BadgeComponent < BaseComponent
21
+ renders_one :icon
22
+
23
+ def initialize(
24
+ variant: :primary,
25
+ size: :md,
26
+ pill: true,
27
+ **html_options
28
+ )
29
+ super(variant: variant, size: size, **html_options)
30
+ @pill = pill
31
+ end
32
+
33
+ def call
34
+ tag.span(**html_attrs) do
35
+ badge_content
36
+ end
37
+ end
38
+
39
+ private
40
+
41
+ def base_classes
42
+ classes = [
43
+ "kz-badge",
44
+ "inline-flex items-center gap-1",
45
+ "font-semibold uppercase tracking-wider"
46
+ ]
47
+ classes << "rounded-full" if @pill
48
+ classes.join(" ")
49
+ end
50
+
51
+ def badge_content
52
+ if icon?
53
+ safe_join([
54
+ tag.span(class: "kz-badge-icon") { icon },
55
+ tag.span { content }
56
+ ])
57
+ else
58
+ content
59
+ end
60
+ end
61
+ end
62
+ end
@@ -0,0 +1,84 @@
1
+ # frozen_string_literal: true
2
+
3
+ module KozenetUi
4
+ # Base component that all Kozenet UI components inherit from
5
+ # Provides common functionality for variant handling, class merging, etc.
6
+ class BaseComponent < ViewComponent::Base
7
+ attr_reader :variant, :size, :html_options
8
+
9
+ def initialize(variant: nil, size: nil, class: nil, **html_options)
10
+ super()
11
+ @variant = variant || KozenetUi.configuration.default_variant
12
+ @size = size || KozenetUi.configuration.default_size
13
+ @custom_class = binding.local_variable_get(:class)
14
+ @html_options = html_options
15
+ end
16
+
17
+ private
18
+
19
+ # Build final CSS classes
20
+ def component_classes
21
+ [
22
+ base_classes,
23
+ variant_class,
24
+ size_class,
25
+ @custom_class
26
+ ].compact.join(" ")
27
+ end
28
+
29
+ # Override in subclasses
30
+ def base_classes
31
+ "kz-component"
32
+ end
33
+
34
+ # Get variant class from Variants helper
35
+ def variant_class
36
+ return nil unless @variant
37
+
38
+ component_type = self.class.name.demodulize.underscore.gsub("_component", "")
39
+ begin
40
+ KozenetUi::Theme::Variants.public_send(component_type, @variant)
41
+ rescue StandardError
42
+ nil
43
+ end
44
+ end
45
+
46
+ # Get size class from Variants helper
47
+ def size_class
48
+ return nil unless @size
49
+
50
+ KozenetUi::Theme::Variants.size(@size)
51
+ end
52
+
53
+ # Merge HTML attributes safely
54
+ def html_attrs
55
+ @html_options.merge(class: component_classes)
56
+ end
57
+
58
+ # Helper for rendering slots with fallback
59
+ def render_slot_or_content(slot, &fallback)
60
+ if slot?
61
+ slot
62
+ elsif fallback
63
+ capture(&fallback)
64
+ end
65
+ end
66
+
67
+ # Check if running in dark mode (from request or config)
68
+ def dark_mode?
69
+ helpers.cookies[:theme] == "dark"
70
+ rescue StandardError
71
+ false
72
+ end
73
+
74
+ # Get current theme palette
75
+ def theme_palette
76
+ KozenetUi.configuration.palette
77
+ end
78
+
79
+ # Stimulus controller name with prefix
80
+ def stimulus_controller(name)
81
+ "#{KozenetUi.configuration.stimulus_prefix}-#{name}"
82
+ end
83
+ end
84
+ end