@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 +1 -1
- package/src/components/layout/PageHeader.svelte +293 -0
- package/src/components/layout/SettingCard.svelte +153 -0
- package/src/components/navigation/ScrollNav.svelte +174 -0
- package/src/components/navigation/TabNav.svelte +248 -0
- package/src/components/primitives/FilterChip.svelte +162 -0
- package/src/components/primitives/MemberCard.svelte +291 -0
- package/src/components/primitives/MetricCard.svelte +266 -0
- package/src/components/primitives/StatusLine.svelte +121 -0
- package/src/components/primitives/Tooltip.svelte +201 -0
- package/src/index.js +14 -0
package/package.json
CHANGED
|
@@ -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>
|