@miozu/jera 0.7.2 → 0.8.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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@miozu/jera",
3
- "version": "0.7.2",
3
+ "version": "0.8.0",
4
4
  "description": "Zero-dependency, AI-first component library for Svelte 5",
5
5
  "type": "module",
6
6
  "svelte": "./src/index.js",
@@ -0,0 +1,293 @@
1
+ <!--
2
+ @component PageHeader
3
+
4
+ A flexible page header with icon, title, description, stats, and action slots.
5
+ Designed for data-heavy pages with search and filtering capabilities.
6
+
7
+ @example Basic
8
+ <PageHeader title="Dashboard" description="Overview of your system" />
9
+
10
+ @example With icon and stats
11
+ <PageHeader
12
+ title="Team Members"
13
+ description="Manage your workspace team"
14
+ stats={[
15
+ {label: "Members", value: 12},
16
+ {label: "Active", value: 11, variant: "success"}
17
+ ]}
18
+ >
19
+ {#snippet icon()}
20
+ <Users size={20} />
21
+ {/snippet}
22
+ </PageHeader>
23
+
24
+ @example With actions and search
25
+ <PageHeader title="Products">
26
+ {#snippet actions()}
27
+ <Button>Add Product</Button>
28
+ {/snippet}
29
+ {#snippet search()}
30
+ <input placeholder="Search..." />
31
+ {/snippet}
32
+ </PageHeader>
33
+ -->
34
+ <script>
35
+ let {
36
+ title = '',
37
+ description = '',
38
+ stats = [],
39
+ size = 'default',
40
+ class: className = '',
41
+ icon,
42
+ actions,
43
+ search,
44
+ filters
45
+ } = $props();
46
+ </script>
47
+
48
+ <header class="page-header page-header-{size} {className}">
49
+ <div class="header-main">
50
+ <div class="header-title-section">
51
+ {#if icon}
52
+ <div class="title-icon">
53
+ {@render icon()}
54
+ </div>
55
+ {/if}
56
+ <div class="title-text">
57
+ <h1 class="page-title">{title}</h1>
58
+ {#if description}
59
+ <p class="page-description">{description}</p>
60
+ {/if}
61
+ </div>
62
+ </div>
63
+
64
+ {#if stats.length > 0}
65
+ <div class="header-stats">
66
+ {#each stats as stat}
67
+ <div class="header-stat">
68
+ {#if stat.icon}
69
+ <span class="stat-icon" class:stat-icon-success={stat.variant === 'success'} class:stat-icon-warning={stat.variant === 'warning'} class:stat-icon-error={stat.variant === 'error'}>
70
+ {@render stat.icon()}
71
+ </span>
72
+ {/if}
73
+ <span class="stat-label">{stat.label}</span>
74
+ <span class="stat-value" class:stat-value-success={stat.variant === 'success'} class:stat-value-warning={stat.variant === 'warning'} class:stat-value-error={stat.variant === 'error'}>
75
+ {stat.value}
76
+ </span>
77
+ </div>
78
+ {/each}
79
+ </div>
80
+ {/if}
81
+
82
+ {#if actions}
83
+ <div class="header-actions">
84
+ {@render actions()}
85
+ </div>
86
+ {/if}
87
+ </div>
88
+
89
+ {#if search || filters}
90
+ <div class="header-toolbar">
91
+ {#if search}
92
+ <div class="toolbar-search">
93
+ {@render search()}
94
+ </div>
95
+ {/if}
96
+ {#if filters}
97
+ <div class="toolbar-filters">
98
+ {@render filters()}
99
+ </div>
100
+ {/if}
101
+ </div>
102
+ {/if}
103
+ </header>
104
+
105
+ <style>
106
+ .page-header {
107
+ display: flex;
108
+ flex-direction: column;
109
+ gap: var(--space-4);
110
+ padding: var(--space-6);
111
+ background: var(--color-base00);
112
+ border-bottom: 1px solid var(--color-base02);
113
+ }
114
+
115
+ .page-header-compact {
116
+ padding: var(--space-4);
117
+ gap: var(--space-3);
118
+ }
119
+
120
+ .page-header-large {
121
+ padding: var(--space-8);
122
+ gap: var(--space-6);
123
+ }
124
+
125
+ .header-main {
126
+ display: flex;
127
+ align-items: flex-start;
128
+ justify-content: space-between;
129
+ gap: var(--space-6);
130
+ flex-wrap: wrap;
131
+ }
132
+
133
+ .header-title-section {
134
+ display: flex;
135
+ align-items: flex-start;
136
+ gap: var(--space-3);
137
+ flex: 1;
138
+ min-width: 0;
139
+ }
140
+
141
+ .title-icon {
142
+ display: flex;
143
+ align-items: center;
144
+ justify-content: center;
145
+ width: 2.5rem;
146
+ height: 2.5rem;
147
+ border-radius: var(--radius-lg);
148
+ background: color-mix(in srgb, var(--color-base0D) 10%, transparent);
149
+ color: var(--color-base0D);
150
+ flex-shrink: 0;
151
+ }
152
+
153
+ .title-text {
154
+ display: flex;
155
+ flex-direction: column;
156
+ gap: var(--space-1);
157
+ min-width: 0;
158
+ }
159
+
160
+ .page-title {
161
+ margin: 0;
162
+ font-size: var(--text-2xl);
163
+ font-weight: 600;
164
+ color: var(--color-base06);
165
+ line-height: 1.2;
166
+ }
167
+
168
+ .page-header-compact .page-title {
169
+ font-size: var(--text-xl);
170
+ }
171
+
172
+ .page-header-large .page-title {
173
+ font-size: var(--text-3xl);
174
+ }
175
+
176
+ .page-description {
177
+ margin: 0;
178
+ font-size: var(--text-sm);
179
+ color: var(--color-base04);
180
+ line-height: 1.5;
181
+ }
182
+
183
+ .header-stats {
184
+ display: flex;
185
+ align-items: center;
186
+ gap: var(--space-6);
187
+ flex-shrink: 0;
188
+ }
189
+
190
+ .header-stat {
191
+ display: flex;
192
+ align-items: center;
193
+ gap: var(--space-2);
194
+ }
195
+
196
+ .stat-icon {
197
+ color: var(--color-base04);
198
+ }
199
+
200
+ .stat-icon-success {
201
+ color: var(--color-base0B);
202
+ }
203
+
204
+ .stat-icon-warning {
205
+ color: var(--color-base0A);
206
+ }
207
+
208
+ .stat-icon-error {
209
+ color: var(--color-base08);
210
+ }
211
+
212
+ .stat-label {
213
+ font-size: var(--text-xs);
214
+ font-weight: 500;
215
+ text-transform: uppercase;
216
+ letter-spacing: 0.05em;
217
+ color: var(--color-base04);
218
+ }
219
+
220
+ .stat-value {
221
+ font-size: var(--text-lg);
222
+ font-weight: 600;
223
+ color: var(--color-base06);
224
+ }
225
+
226
+ .stat-value-success {
227
+ color: var(--color-base0B);
228
+ }
229
+
230
+ .stat-value-warning {
231
+ color: var(--color-base0A);
232
+ }
233
+
234
+ .stat-value-error {
235
+ color: var(--color-base08);
236
+ }
237
+
238
+ .header-actions {
239
+ display: flex;
240
+ align-items: center;
241
+ gap: var(--space-3);
242
+ flex-shrink: 0;
243
+ }
244
+
245
+ .header-toolbar {
246
+ display: flex;
247
+ align-items: center;
248
+ justify-content: space-between;
249
+ gap: var(--space-4);
250
+ padding: var(--space-3) var(--space-4);
251
+ background: var(--color-base01);
252
+ border-radius: var(--radius-lg);
253
+ }
254
+
255
+ .toolbar-search {
256
+ flex: 1;
257
+ min-width: 0;
258
+ max-width: 24rem;
259
+ }
260
+
261
+ .toolbar-filters {
262
+ display: flex;
263
+ align-items: center;
264
+ gap: var(--space-2);
265
+ }
266
+
267
+ /* Responsive */
268
+ @media (max-width: 768px) {
269
+ .header-main {
270
+ flex-direction: column;
271
+ align-items: stretch;
272
+ }
273
+
274
+ .header-stats {
275
+ flex-wrap: wrap;
276
+ gap: var(--space-4);
277
+ }
278
+
279
+ .header-actions {
280
+ width: 100%;
281
+ justify-content: flex-end;
282
+ }
283
+
284
+ .header-toolbar {
285
+ flex-direction: column;
286
+ align-items: stretch;
287
+ }
288
+
289
+ .toolbar-search {
290
+ max-width: none;
291
+ }
292
+ }
293
+ </style>
@@ -0,0 +1,153 @@
1
+ <!--
2
+ @component SettingCard
3
+
4
+ A card container for settings sections with optional danger variant.
5
+ Provides consistent layout for settings items with labels, descriptions, and actions.
6
+
7
+ @example Basic settings card
8
+ <SettingCard title="Account Settings">
9
+ <div class="setting-item">
10
+ <div class="setting-content">
11
+ <h4 class="setting-label">Display Name</h4>
12
+ <p class="setting-description">Your public display name</p>
13
+ </div>
14
+ <Input value={name} />
15
+ </div>
16
+ </SettingCard>
17
+
18
+ @example Danger zone
19
+ <SettingCard title="Danger Zone" variant="danger">
20
+ <div class="setting-item">
21
+ <div class="setting-content">
22
+ <h4 class="setting-label">Delete Account</h4>
23
+ <p class="setting-description">This cannot be undone</p>
24
+ </div>
25
+ <Button variant="danger">Delete</Button>
26
+ </div>
27
+ </SettingCard>
28
+ -->
29
+ <script>
30
+ let {
31
+ title = '',
32
+ variant = 'default',
33
+ class: className = '',
34
+ children
35
+ } = $props();
36
+ </script>
37
+
38
+ <div class="setting-card setting-card-{variant} {className}">
39
+ {#if title}
40
+ <h3 class="card-title">{title}</h3>
41
+ {/if}
42
+ {#if children}
43
+ <div class="card-content">
44
+ {@render children()}
45
+ </div>
46
+ {/if}
47
+ </div>
48
+
49
+ <style>
50
+ .setting-card {
51
+ background: transparent;
52
+ border: 1px solid color-mix(in srgb, var(--color-base02) 60%, transparent);
53
+ border-radius: var(--radius-xl);
54
+ padding: var(--space-6);
55
+ transition: border-color 0.15s ease;
56
+ }
57
+
58
+ .setting-card:hover {
59
+ border-color: var(--color-base02);
60
+ }
61
+
62
+ .setting-card-danger {
63
+ border-color: color-mix(in srgb, var(--color-base08) 30%, transparent);
64
+ background: color-mix(in srgb, var(--color-base08) 3%, transparent);
65
+ }
66
+
67
+ .setting-card-danger:hover {
68
+ border-color: color-mix(in srgb, var(--color-base08) 50%, transparent);
69
+ }
70
+
71
+ .card-title {
72
+ margin: 0 0 var(--space-5);
73
+ font-size: var(--text-base);
74
+ font-weight: 500;
75
+ color: var(--color-base06);
76
+ }
77
+
78
+ .setting-card-danger .card-title {
79
+ color: var(--color-base08);
80
+ }
81
+
82
+ .card-content {
83
+ display: flex;
84
+ flex-direction: column;
85
+ }
86
+
87
+ /* Utility classes for setting items - exposed globally */
88
+ :global(.setting-item) {
89
+ display: flex;
90
+ align-items: center;
91
+ justify-content: space-between;
92
+ gap: var(--space-4);
93
+ padding: var(--space-4) 0;
94
+ border-bottom: 1px solid var(--color-base02);
95
+ }
96
+
97
+ :global(.setting-item:last-child) {
98
+ border-bottom: none;
99
+ padding-bottom: 0;
100
+ }
101
+
102
+ :global(.setting-item:first-child) {
103
+ padding-top: 0;
104
+ }
105
+
106
+ :global(.setting-content) {
107
+ flex: 1;
108
+ min-width: 0;
109
+ }
110
+
111
+ :global(.setting-label) {
112
+ margin: 0 0 var(--space-1);
113
+ font-size: var(--text-sm);
114
+ font-weight: 500;
115
+ color: var(--color-base06);
116
+ }
117
+
118
+ :global(.setting-description) {
119
+ margin: 0;
120
+ font-size: var(--text-xs);
121
+ color: var(--color-base04);
122
+ line-height: 1.5;
123
+ }
124
+
125
+ :global(.setting-item-icon) {
126
+ display: flex;
127
+ align-items: center;
128
+ gap: var(--space-3);
129
+ color: color-mix(in srgb, var(--color-base04) 80%, transparent);
130
+ }
131
+
132
+ :global(.setting-item-with-icon) {
133
+ display: flex;
134
+ align-items: flex-start;
135
+ gap: var(--space-3);
136
+ padding: var(--space-4) 0;
137
+ border-bottom: 1px solid var(--color-base02);
138
+ }
139
+
140
+ :global(.setting-item-with-icon:last-child) {
141
+ border-bottom: none;
142
+ padding-bottom: 0;
143
+ }
144
+
145
+ /* Responsive */
146
+ @media (max-width: 640px) {
147
+ :global(.setting-item) {
148
+ flex-direction: column;
149
+ align-items: flex-start;
150
+ gap: var(--space-3);
151
+ }
152
+ }
153
+ </style>
@@ -0,0 +1,174 @@
1
+ <!--
2
+ @component ScrollNav
3
+
4
+ A horizontally scrollable navigation container with gradient indicators.
5
+ Perfect for tab navigation that overflows on mobile.
6
+
7
+ @example Basic scroll nav
8
+ <ScrollNav>
9
+ <TabNav tabs={tabs} bind:active={activeTab} />
10
+ </ScrollNav>
11
+
12
+ @example With custom gradient size
13
+ <ScrollNav gradientSize={80}>
14
+ <div class="nav-items">
15
+ {#each items as item}
16
+ <a href={item.href}>{item.label}</a>
17
+ {/each}
18
+ </div>
19
+ </ScrollNav>
20
+ -->
21
+ <script>
22
+ import { onMount } from 'svelte';
23
+
24
+ let {
25
+ gradientSize = 60,
26
+ scrollAmount = 200,
27
+ class: className = '',
28
+ children
29
+ } = $props();
30
+
31
+ let containerRef = $state(null);
32
+ let canScrollLeft = $state(false);
33
+ let canScrollRight = $state(false);
34
+
35
+ function checkScroll() {
36
+ if (!containerRef) return;
37
+ const { scrollLeft, scrollWidth, clientWidth } = containerRef;
38
+ canScrollLeft = scrollLeft > 0;
39
+ canScrollRight = scrollLeft + clientWidth < scrollWidth - 1;
40
+ }
41
+
42
+ function scrollTo(direction) {
43
+ if (!containerRef) return;
44
+ const amount = direction === 'left' ? -scrollAmount : scrollAmount;
45
+ containerRef.scrollBy({ left: amount, behavior: 'smooth' });
46
+ }
47
+
48
+ onMount(() => {
49
+ checkScroll();
50
+ const observer = new ResizeObserver(checkScroll);
51
+ if (containerRef) {
52
+ observer.observe(containerRef);
53
+ }
54
+ return () => observer.disconnect();
55
+ });
56
+ </script>
57
+
58
+ <div class="scroll-nav {className}">
59
+ {#if canScrollLeft}
60
+ <div class="scroll-gradient scroll-gradient-left" style="width: {gradientSize}px">
61
+ <button
62
+ type="button"
63
+ class="scroll-btn scroll-btn-left"
64
+ onclick={() => scrollTo('left')}
65
+ aria-label="Scroll left"
66
+ >
67
+ <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
68
+ <polyline points="15 18 9 12 15 6"></polyline>
69
+ </svg>
70
+ </button>
71
+ </div>
72
+ {/if}
73
+
74
+ <div
75
+ class="scroll-container"
76
+ bind:this={containerRef}
77
+ onscroll={checkScroll}
78
+ >
79
+ {#if children}
80
+ {@render children()}
81
+ {/if}
82
+ </div>
83
+
84
+ {#if canScrollRight}
85
+ <div class="scroll-gradient scroll-gradient-right" style="width: {gradientSize}px">
86
+ <button
87
+ type="button"
88
+ class="scroll-btn scroll-btn-right"
89
+ onclick={() => scrollTo('right')}
90
+ aria-label="Scroll right"
91
+ >
92
+ <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
93
+ <polyline points="9 18 15 12 9 6"></polyline>
94
+ </svg>
95
+ </button>
96
+ </div>
97
+ {/if}
98
+ </div>
99
+
100
+ <style>
101
+ .scroll-nav {
102
+ position: relative;
103
+ display: flex;
104
+ align-items: center;
105
+ }
106
+
107
+ .scroll-container {
108
+ display: flex;
109
+ align-items: center;
110
+ overflow-x: auto;
111
+ scroll-behavior: smooth;
112
+ scrollbar-width: none;
113
+ -ms-overflow-style: none;
114
+ }
115
+
116
+ .scroll-container::-webkit-scrollbar {
117
+ display: none;
118
+ }
119
+
120
+ .scroll-gradient {
121
+ position: absolute;
122
+ top: 0;
123
+ bottom: 0;
124
+ display: flex;
125
+ align-items: center;
126
+ pointer-events: none;
127
+ z-index: 1;
128
+ }
129
+
130
+ .scroll-gradient-left {
131
+ left: 0;
132
+ background: linear-gradient(to right, var(--color-base00), transparent);
133
+ justify-content: flex-start;
134
+ padding-left: var(--space-1);
135
+ }
136
+
137
+ .scroll-gradient-right {
138
+ right: 0;
139
+ background: linear-gradient(to left, var(--color-base00), transparent);
140
+ justify-content: flex-end;
141
+ padding-right: var(--space-1);
142
+ }
143
+
144
+ .scroll-btn {
145
+ display: flex;
146
+ align-items: center;
147
+ justify-content: center;
148
+ width: 2rem;
149
+ height: 2rem;
150
+ padding: 0;
151
+ background: var(--color-base01);
152
+ border: 1px solid var(--color-base02);
153
+ border-radius: 9999px;
154
+ color: var(--color-base05);
155
+ cursor: pointer;
156
+ pointer-events: all;
157
+ transition: all 0.15s ease;
158
+ box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
159
+ }
160
+
161
+ .scroll-btn:hover {
162
+ background: var(--color-base02);
163
+ color: var(--color-base06);
164
+ }
165
+
166
+ .scroll-btn:focus-visible {
167
+ outline: 2px solid var(--color-base0D);
168
+ outline-offset: 2px;
169
+ }
170
+
171
+ .scroll-btn:active {
172
+ transform: scale(0.95);
173
+ }
174
+ </style>