@miozu/jera 0.7.2 → 0.8.1

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.
@@ -0,0 +1,248 @@
1
+ <!--
2
+ @component TabNav
3
+
4
+ A flexible tab navigation component with multiple variants.
5
+ Supports icons, badges, counts, and different visual styles.
6
+
7
+ @example Basic tabs
8
+ <TabNav
9
+ tabs={[
10
+ {id: 'overview', label: 'Overview'},
11
+ {id: 'settings', label: 'Settings'}
12
+ ]}
13
+ bind:active={activeTab}
14
+ />
15
+
16
+ @example With icons and counts
17
+ <TabNav
18
+ tabs={[
19
+ {id: 'inbox', label: 'Inbox', icon: InboxIcon, count: 12},
20
+ {id: 'sent', label: 'Sent', icon: SendIcon}
21
+ ]}
22
+ bind:active={activeTab}
23
+ variant="pills"
24
+ />
25
+
26
+ @example Underline variant
27
+ <TabNav
28
+ tabs={[{id: 'all', label: 'All'}, {id: 'active', label: 'Active'}]}
29
+ bind:active={activeTab}
30
+ variant="underline"
31
+ />
32
+ -->
33
+ <script>
34
+ let {
35
+ tabs = [],
36
+ active = $bindable(''),
37
+ variant = 'default',
38
+ size = 'md',
39
+ class: className = '',
40
+ onchange
41
+ } = $props();
42
+
43
+ function handleTabClick(tab) {
44
+ if (tab.disabled) return;
45
+ active = tab.id;
46
+ onchange?.(tab);
47
+ }
48
+ </script>
49
+
50
+ <nav class="tab-nav tab-nav-{variant} tab-nav-{size} {className}" role="tablist">
51
+ {#each tabs as tab}
52
+ <button
53
+ type="button"
54
+ class="tab-item"
55
+ class:active={active === tab.id}
56
+ class:disabled={tab.disabled}
57
+ role="tab"
58
+ aria-selected={active === tab.id}
59
+ aria-disabled={tab.disabled}
60
+ onclick={() => handleTabClick(tab)}
61
+ >
62
+ {#if tab.icon}
63
+ <span class="tab-icon">
64
+ {@render tab.icon()}
65
+ </span>
66
+ {/if}
67
+ <span class="tab-label">{tab.label}</span>
68
+ {#if tab.count !== undefined}
69
+ <span class="tab-count">({tab.count})</span>
70
+ {/if}
71
+ {#if tab.badge}
72
+ <span class="tab-badge">{tab.badge}</span>
73
+ {/if}
74
+ </button>
75
+ {/each}
76
+ </nav>
77
+
78
+ <style>
79
+ .tab-nav {
80
+ display: flex;
81
+ align-items: center;
82
+ gap: var(--space-1);
83
+ }
84
+
85
+ /* Default variant - subtle background */
86
+ .tab-nav-default {
87
+ padding: var(--space-1);
88
+ background: color-mix(in srgb, var(--color-base01) 70%, transparent);
89
+ border-radius: var(--radius-lg);
90
+ }
91
+
92
+ .tab-nav-default .tab-item {
93
+ padding: var(--space-2) var(--space-3);
94
+ border-radius: var(--radius-md);
95
+ background: transparent;
96
+ border: none;
97
+ color: var(--color-base04);
98
+ font-size: var(--text-sm);
99
+ font-weight: 500;
100
+ cursor: pointer;
101
+ transition: all 0.15s ease;
102
+ }
103
+
104
+ .tab-nav-default .tab-item:hover:not(.disabled) {
105
+ color: var(--color-base05);
106
+ background: color-mix(in srgb, var(--color-base02) 50%, transparent);
107
+ }
108
+
109
+ .tab-nav-default .tab-item.active {
110
+ background: var(--color-base00);
111
+ color: var(--color-base06);
112
+ box-shadow: 0 1px 2px rgba(0, 0, 0, 0.1);
113
+ }
114
+
115
+ /* Pills variant */
116
+ .tab-nav-pills {
117
+ gap: var(--space-2);
118
+ }
119
+
120
+ .tab-nav-pills .tab-item {
121
+ padding: var(--space-2) var(--space-4);
122
+ border-radius: var(--radius-lg);
123
+ background: transparent;
124
+ border: 1px solid transparent;
125
+ color: var(--color-base04);
126
+ font-size: var(--text-sm);
127
+ font-weight: 500;
128
+ cursor: pointer;
129
+ transition: all 0.15s ease;
130
+ }
131
+
132
+ .tab-nav-pills .tab-item:hover:not(.disabled) {
133
+ background: var(--color-base01);
134
+ color: var(--color-base05);
135
+ }
136
+
137
+ .tab-nav-pills .tab-item.active {
138
+ background: color-mix(in srgb, var(--color-base0D) 10%, transparent);
139
+ border-color: color-mix(in srgb, var(--color-base0D) 30%, transparent);
140
+ color: var(--color-base0D);
141
+ }
142
+
143
+ /* Underline variant */
144
+ .tab-nav-underline {
145
+ gap: 0;
146
+ border-bottom: 1px solid var(--color-base02);
147
+ }
148
+
149
+ .tab-nav-underline .tab-item {
150
+ padding: var(--space-3) var(--space-4);
151
+ background: transparent;
152
+ border: none;
153
+ border-bottom: 2px solid transparent;
154
+ margin-bottom: -1px;
155
+ color: var(--color-base04);
156
+ font-size: var(--text-sm);
157
+ font-weight: 500;
158
+ cursor: pointer;
159
+ transition: all 0.15s ease;
160
+ }
161
+
162
+ .tab-nav-underline .tab-item:hover:not(.disabled) {
163
+ color: var(--color-base05);
164
+ border-bottom-color: var(--color-base03);
165
+ }
166
+
167
+ .tab-nav-underline .tab-item.active {
168
+ color: var(--color-base0D);
169
+ border-bottom-color: var(--color-base0D);
170
+ }
171
+
172
+ /* Sizes */
173
+ .tab-nav-sm .tab-item {
174
+ padding: var(--space-1) var(--space-2);
175
+ font-size: var(--text-xs);
176
+ }
177
+
178
+ .tab-nav-lg .tab-item {
179
+ padding: var(--space-3) var(--space-5);
180
+ font-size: var(--text-base);
181
+ }
182
+
183
+ /* Shared styles */
184
+ .tab-item {
185
+ display: inline-flex;
186
+ align-items: center;
187
+ gap: var(--space-2);
188
+ white-space: nowrap;
189
+ flex-shrink: 0;
190
+ }
191
+
192
+ .tab-item.disabled {
193
+ opacity: 0.5;
194
+ cursor: not-allowed;
195
+ }
196
+
197
+ .tab-item:focus-visible {
198
+ outline: 2px solid var(--color-base0D);
199
+ outline-offset: 2px;
200
+ }
201
+
202
+ .tab-icon {
203
+ display: flex;
204
+ align-items: center;
205
+ justify-content: center;
206
+ }
207
+
208
+ .tab-label {
209
+ line-height: 1;
210
+ }
211
+
212
+ .tab-count {
213
+ font-size: var(--text-xs);
214
+ color: var(--color-base04);
215
+ }
216
+
217
+ .tab-item.active .tab-count {
218
+ color: inherit;
219
+ opacity: 0.7;
220
+ }
221
+
222
+ .tab-badge {
223
+ display: inline-flex;
224
+ align-items: center;
225
+ justify-content: center;
226
+ min-width: 1.25rem;
227
+ height: 1.25rem;
228
+ padding: 0 var(--space-1);
229
+ font-size: var(--text-xs);
230
+ font-weight: 600;
231
+ background: var(--color-base09);
232
+ color: var(--color-base00);
233
+ border-radius: 9999px;
234
+ }
235
+
236
+ /* Scrollable container */
237
+ @media (max-width: 768px) {
238
+ .tab-nav {
239
+ overflow-x: auto;
240
+ scrollbar-width: none;
241
+ -ms-overflow-style: none;
242
+ }
243
+
244
+ .tab-nav::-webkit-scrollbar {
245
+ display: none;
246
+ }
247
+ }
248
+ </style>
@@ -0,0 +1,162 @@
1
+ <!--
2
+ @component FilterChip
3
+
4
+ A toggleable chip for filtering content.
5
+ Supports icons, counts, and multiple selection modes.
6
+
7
+ @example Basic filter chip
8
+ <FilterChip
9
+ label="Active"
10
+ active={filters.active}
11
+ onclick={() => toggleFilter('active')}
12
+ />
13
+
14
+ @example With icon and count
15
+ <FilterChip
16
+ label="Critical"
17
+ active={showCritical}
18
+ count={criticalCount}
19
+ variant="error"
20
+ >
21
+ {#snippet icon()}
22
+ <AlertIcon size={14} />
23
+ {/snippet}
24
+ </FilterChip>
25
+
26
+ @example Group of filters
27
+ <div class="filter-group">
28
+ {#each filters as filter}
29
+ <FilterChip
30
+ label={filter.label}
31
+ active={activeFilters.includes(filter.id)}
32
+ onclick={() => toggleFilter(filter.id)}
33
+ />
34
+ {/each}
35
+ </div>
36
+ -->
37
+ <script>
38
+ let {
39
+ label = '',
40
+ active = false,
41
+ count = null,
42
+ variant = 'default',
43
+ disabled = false,
44
+ class: className = '',
45
+ icon,
46
+ onclick
47
+ } = $props();
48
+ </script>
49
+
50
+ <button
51
+ type="button"
52
+ class="filter-chip filter-chip-{variant}"
53
+ class:active
54
+ class:disabled
55
+ {disabled}
56
+ {onclick}
57
+ aria-pressed={active}
58
+ >
59
+ {#if icon}
60
+ <span class="chip-icon">
61
+ {@render icon()}
62
+ </span>
63
+ {/if}
64
+ <span class="chip-label">{label}</span>
65
+ {#if count !== null}
66
+ <span class="chip-count">{count}</span>
67
+ {/if}
68
+ </button>
69
+
70
+ <style>
71
+ .filter-chip {
72
+ display: inline-flex;
73
+ align-items: center;
74
+ gap: var(--space-2);
75
+ padding: var(--space-1) var(--space-3);
76
+ font-size: var(--text-sm);
77
+ font-weight: 500;
78
+ background: transparent;
79
+ border: 1px solid var(--color-base02);
80
+ border-radius: var(--radius-lg);
81
+ color: var(--color-base04);
82
+ cursor: pointer;
83
+ transition: all 0.15s ease;
84
+ }
85
+
86
+ .filter-chip:hover:not(.disabled) {
87
+ border-color: var(--color-base03);
88
+ color: var(--color-base05);
89
+ background: var(--color-base01);
90
+ }
91
+
92
+ .filter-chip:focus-visible {
93
+ outline: 2px solid var(--color-base0D);
94
+ outline-offset: 2px;
95
+ }
96
+
97
+ .filter-chip.disabled {
98
+ opacity: 0.5;
99
+ cursor: not-allowed;
100
+ }
101
+
102
+ /* Active states by variant */
103
+ .filter-chip-default.active {
104
+ background: color-mix(in srgb, var(--color-base0D) 10%, transparent);
105
+ border-color: color-mix(in srgb, var(--color-base0D) 40%, transparent);
106
+ color: var(--color-base0D);
107
+ }
108
+
109
+ .filter-chip-error.active {
110
+ background: color-mix(in srgb, var(--color-base08) 10%, transparent);
111
+ border-color: color-mix(in srgb, var(--color-base08) 40%, transparent);
112
+ color: var(--color-base08);
113
+ }
114
+
115
+ .filter-chip-warning.active {
116
+ background: color-mix(in srgb, var(--color-base0A) 10%, transparent);
117
+ border-color: color-mix(in srgb, var(--color-base0A) 40%, transparent);
118
+ color: var(--color-base0A);
119
+ }
120
+
121
+ .filter-chip-success.active {
122
+ background: color-mix(in srgb, var(--color-base0B) 10%, transparent);
123
+ border-color: color-mix(in srgb, var(--color-base0B) 40%, transparent);
124
+ color: var(--color-base0B);
125
+ }
126
+
127
+ .filter-chip-info.active {
128
+ background: color-mix(in srgb, var(--color-base0C) 10%, transparent);
129
+ border-color: color-mix(in srgb, var(--color-base0C) 40%, transparent);
130
+ color: var(--color-base0C);
131
+ }
132
+
133
+ .chip-icon {
134
+ display: flex;
135
+ align-items: center;
136
+ justify-content: center;
137
+ }
138
+
139
+ .chip-label {
140
+ line-height: 1;
141
+ }
142
+
143
+ .chip-count {
144
+ display: inline-flex;
145
+ align-items: center;
146
+ justify-content: center;
147
+ min-width: 1.25rem;
148
+ height: 1.25rem;
149
+ padding: 0 var(--space-1);
150
+ font-size: var(--text-xs);
151
+ font-weight: 600;
152
+ background: var(--color-base02);
153
+ color: var(--color-base05);
154
+ border-radius: 9999px;
155
+ }
156
+
157
+ .filter-chip.active .chip-count {
158
+ background: currentColor;
159
+ color: var(--color-base00);
160
+ opacity: 0.8;
161
+ }
162
+ </style>
@@ -0,0 +1,291 @@
1
+ <!--
2
+ @component MemberCard
3
+
4
+ A card displaying a team member with avatar, name, role, and actions.
5
+ Supports various layouts and interactive states.
6
+
7
+ @example Basic member card
8
+ <MemberCard
9
+ name="John Doe"
10
+ email="john@example.com"
11
+ role="Developer"
12
+ />
13
+
14
+ @example With avatar and status
15
+ <MemberCard
16
+ name="Jane Smith"
17
+ email="jane@example.com"
18
+ role="Admin"
19
+ avatarUrl="/avatars/jane.jpg"
20
+ status="online"
21
+ />
22
+
23
+ @example With actions
24
+ <MemberCard name="Bob Wilson" email="bob@example.com">
25
+ {#snippet actions()}
26
+ <Button size="sm" variant="ghost">Edit</Button>
27
+ <Button size="sm" variant="danger">Remove</Button>
28
+ {/snippet}
29
+ </MemberCard>
30
+
31
+ @example Compact variant
32
+ <MemberCard
33
+ name="Alice Johnson"
34
+ role="Designer"
35
+ variant="compact"
36
+ />
37
+ -->
38
+ <script>
39
+ let {
40
+ name = '',
41
+ email = '',
42
+ role = '',
43
+ avatarUrl = null,
44
+ status = null,
45
+ timestamp = null,
46
+ variant = 'default',
47
+ class: className = '',
48
+ badge,
49
+ actions,
50
+ onclick
51
+ } = $props();
52
+
53
+ const initials = $derived.by(() => {
54
+ if (!name) return '?';
55
+ const parts = name.split(' ');
56
+ if (parts.length >= 2) {
57
+ return parts[0][0] + parts[1][0];
58
+ }
59
+ return name.slice(0, 2);
60
+ });
61
+
62
+ const statusColors = {
63
+ online: 'status-online',
64
+ offline: 'status-offline',
65
+ away: 'status-away',
66
+ busy: 'status-busy'
67
+ };
68
+
69
+ function formatRelativeTime(dateStr) {
70
+ if (!dateStr) return '';
71
+ const date = new Date(dateStr);
72
+ const now = new Date();
73
+ const diff = now - date;
74
+ const days = Math.floor(diff / (1000 * 60 * 60 * 24));
75
+
76
+ if (days === 0) return 'Today';
77
+ if (days === 1) return 'Yesterday';
78
+ if (days < 7) return `${days} days ago`;
79
+ if (days < 30) return `${Math.floor(days / 7)} weeks ago`;
80
+ return date.toLocaleDateString('en-US', { month: 'short', day: 'numeric' });
81
+ }
82
+ </script>
83
+
84
+ <div
85
+ class="member-card member-card-{variant} {onclick ? 'member-card-clickable' : ''} {className}"
86
+ {onclick}
87
+ role={onclick ? 'button' : undefined}
88
+ tabindex={onclick ? 0 : undefined}
89
+ >
90
+ <div class="member-avatar">
91
+ {#if avatarUrl}
92
+ <img src={avatarUrl} alt={name} class="avatar-image" />
93
+ {:else}
94
+ <span class="avatar-initials">{initials.toUpperCase()}</span>
95
+ {/if}
96
+ {#if status}
97
+ <span class="avatar-status {statusColors[status] || ''}"></span>
98
+ {/if}
99
+ </div>
100
+
101
+ <div class="member-info">
102
+ <div class="member-name">{name}</div>
103
+ {#if variant !== 'compact' && email}
104
+ <div class="member-email">{email}</div>
105
+ {/if}
106
+ {#if role || timestamp}
107
+ <div class="member-meta">
108
+ {#if role}
109
+ <span class="member-role">{role}</span>
110
+ {/if}
111
+ {#if timestamp}
112
+ <span class="member-timestamp">{formatRelativeTime(timestamp)}</span>
113
+ {/if}
114
+ </div>
115
+ {/if}
116
+ </div>
117
+
118
+ {#if badge}
119
+ <div class="member-badge">
120
+ {@render badge()}
121
+ </div>
122
+ {/if}
123
+
124
+ {#if actions}
125
+ <div class="member-actions" onclick={(e) => e.stopPropagation()}>
126
+ {@render actions()}
127
+ </div>
128
+ {/if}
129
+ </div>
130
+
131
+ <style>
132
+ .member-card {
133
+ display: flex;
134
+ align-items: center;
135
+ gap: var(--space-3);
136
+ padding: var(--space-3);
137
+ background: transparent;
138
+ border-radius: var(--radius-lg);
139
+ transition: background 0.15s ease;
140
+ }
141
+
142
+ .member-card-default {
143
+ padding: var(--space-4);
144
+ }
145
+
146
+ .member-card-compact {
147
+ padding: var(--space-2);
148
+ gap: var(--space-2);
149
+ }
150
+
151
+ .member-card-clickable {
152
+ cursor: pointer;
153
+ }
154
+
155
+ .member-card-clickable:hover {
156
+ background: var(--color-base01);
157
+ }
158
+
159
+ .member-card-clickable:focus-visible {
160
+ outline: 2px solid var(--color-base0D);
161
+ outline-offset: 2px;
162
+ }
163
+
164
+ .member-avatar {
165
+ position: relative;
166
+ flex-shrink: 0;
167
+ width: var(--space-10);
168
+ height: var(--space-10);
169
+ border-radius: var(--radius-lg);
170
+ background: color-mix(in srgb, var(--color-base0D) 15%, transparent);
171
+ overflow: hidden;
172
+ }
173
+
174
+ .member-card-compact .member-avatar {
175
+ width: var(--space-8);
176
+ height: var(--space-8);
177
+ border-radius: var(--radius-md);
178
+ }
179
+
180
+ .avatar-image {
181
+ width: 100%;
182
+ height: 100%;
183
+ object-fit: cover;
184
+ }
185
+
186
+ .avatar-initials {
187
+ display: flex;
188
+ align-items: center;
189
+ justify-content: center;
190
+ width: 100%;
191
+ height: 100%;
192
+ font-size: var(--text-sm);
193
+ font-weight: 600;
194
+ color: var(--color-base0D);
195
+ }
196
+
197
+ .member-card-compact .avatar-initials {
198
+ font-size: var(--text-xs);
199
+ }
200
+
201
+ .avatar-status {
202
+ position: absolute;
203
+ bottom: 0;
204
+ right: 0;
205
+ width: var(--space-3);
206
+ height: var(--space-3);
207
+ border-radius: var(--radius-full);
208
+ border: 2px solid var(--color-base00);
209
+ }
210
+
211
+ .status-online {
212
+ background: var(--color-base0B);
213
+ }
214
+
215
+ .status-offline {
216
+ background: var(--color-base03);
217
+ }
218
+
219
+ .status-away {
220
+ background: var(--color-base0A);
221
+ }
222
+
223
+ .status-busy {
224
+ background: var(--color-base08);
225
+ }
226
+
227
+ .member-info {
228
+ flex: 1;
229
+ min-width: 0;
230
+ }
231
+
232
+ .member-name {
233
+ font-size: var(--text-sm);
234
+ font-weight: 500;
235
+ color: var(--color-base06);
236
+ white-space: nowrap;
237
+ overflow: hidden;
238
+ text-overflow: ellipsis;
239
+ }
240
+
241
+ .member-card-compact .member-name {
242
+ font-size: var(--text-xs);
243
+ }
244
+
245
+ .member-email {
246
+ font-size: var(--text-xs);
247
+ color: var(--color-base04);
248
+ white-space: nowrap;
249
+ overflow: hidden;
250
+ text-overflow: ellipsis;
251
+ }
252
+
253
+ .member-meta {
254
+ display: flex;
255
+ align-items: center;
256
+ gap: var(--space-2);
257
+ margin-top: var(--space-1);
258
+ }
259
+
260
+ .member-role {
261
+ font-size: var(--text-xs);
262
+ color: var(--color-base04);
263
+ }
264
+
265
+ .member-timestamp {
266
+ font-size: var(--text-xs);
267
+ color: var(--color-base03);
268
+ }
269
+
270
+ .member-timestamp::before {
271
+ content: '·';
272
+ margin-right: var(--space-2);
273
+ color: var(--color-base03);
274
+ }
275
+
276
+ .member-badge {
277
+ flex-shrink: 0;
278
+ }
279
+
280
+ .member-actions {
281
+ display: flex;
282
+ align-items: center;
283
+ gap: var(--space-2);
284
+ opacity: 0;
285
+ transition: opacity 0.15s ease;
286
+ }
287
+
288
+ .member-card:hover .member-actions {
289
+ opacity: 1;
290
+ }
291
+ </style>