@hyvor/design 2.1.15 → 2.1.16
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.
|
@@ -9,7 +9,8 @@
|
|
|
9
9
|
|
|
10
10
|
let { label, content, children }: Props = $props();
|
|
11
11
|
|
|
12
|
-
const
|
|
12
|
+
const uid = $props.id();
|
|
13
|
+
const labelId = `detail-card-${uid}`;
|
|
13
14
|
</script>
|
|
14
15
|
|
|
15
16
|
<div class="detail-card" role="region" aria-labelledby={labelId}>
|
|
@@ -38,37 +38,13 @@
|
|
|
38
38
|
handwrittenNames = true
|
|
39
39
|
}: Props = $props();
|
|
40
40
|
|
|
41
|
-
function identicon(seed: string) {
|
|
42
|
-
let hash = 0;
|
|
43
|
-
for (let i = 0; i < seed.length; i++) {
|
|
44
|
-
hash = (hash * 31 + seed.charCodeAt(i)) | 0;
|
|
45
|
-
}
|
|
46
|
-
const hue = Math.abs(hash) % 360;
|
|
47
|
-
const cells: { x: number; y: number }[] = [];
|
|
48
|
-
for (let col = 0; col < 3; col++) {
|
|
49
|
-
for (let row = 0; row < 5; row++) {
|
|
50
|
-
const bit = (hash >> (col * 5 + row)) & 1;
|
|
51
|
-
if (!bit) continue;
|
|
52
|
-
cells.push({ x: col, y: row });
|
|
53
|
-
if (col < 2) cells.push({ x: 4 - col, y: row });
|
|
54
|
-
}
|
|
55
|
-
}
|
|
56
|
-
return {
|
|
57
|
-
bg: `hsl(${hue} 45% 92%)`,
|
|
58
|
-
fg: `hsl(${hue} 50% 40%)`,
|
|
59
|
-
cells
|
|
60
|
-
};
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
let scrollEl: HTMLDivElement | undefined = $state();
|
|
64
|
-
let dragging = $state(false);
|
|
65
|
-
let dragStartX = 0;
|
|
66
|
-
let dragStartScroll = 0;
|
|
67
|
-
|
|
68
41
|
let started: boolean[] = $state(reviews.map(() => false));
|
|
69
42
|
let playing: boolean[] = $state(reviews.map(() => false));
|
|
70
43
|
let videoEls: (HTMLVideoElement | undefined)[] = $state([]);
|
|
71
44
|
|
|
45
|
+
let rowEl: HTMLDivElement | undefined = $state();
|
|
46
|
+
let activeIndex = $state(0);
|
|
47
|
+
|
|
72
48
|
function play(i: number) {
|
|
73
49
|
started[i] = true;
|
|
74
50
|
videoEls[i]?.play().catch(() => {});
|
|
@@ -83,23 +59,48 @@
|
|
|
83
59
|
playing[i] = false;
|
|
84
60
|
}
|
|
85
61
|
|
|
86
|
-
function
|
|
87
|
-
if (
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
62
|
+
function onCardClick(e: MouseEvent, i: number) {
|
|
63
|
+
if ((e.target as HTMLElement).closest('a')) return;
|
|
64
|
+
if (!started[i]) {
|
|
65
|
+
play(i);
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
const video = videoEls[i];
|
|
69
|
+
if (!video) return;
|
|
70
|
+
if (playing[i]) {
|
|
71
|
+
video.pause();
|
|
72
|
+
} else {
|
|
73
|
+
video.play().catch(() => {});
|
|
74
|
+
}
|
|
94
75
|
}
|
|
95
76
|
|
|
96
|
-
function
|
|
97
|
-
if (
|
|
98
|
-
|
|
77
|
+
function onCardKeydown(e: KeyboardEvent, i: number) {
|
|
78
|
+
if (e.key !== 'Enter' && e.key !== ' ') return;
|
|
79
|
+
e.preventDefault();
|
|
80
|
+
onCardClick(e as unknown as MouseEvent, i);
|
|
99
81
|
}
|
|
100
82
|
|
|
101
|
-
function
|
|
102
|
-
|
|
83
|
+
function goTo(i: number) {
|
|
84
|
+
if (!rowEl) return;
|
|
85
|
+
const card = rowEl.children[i] as HTMLElement | undefined;
|
|
86
|
+
card?.scrollIntoView({ behavior: 'smooth', inline: 'center', block: 'nearest' });
|
|
87
|
+
activeIndex = i;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function onRowScroll() {
|
|
91
|
+
if (!rowEl) return;
|
|
92
|
+
const rowCenter = rowEl.getBoundingClientRect().left + rowEl.clientWidth / 2;
|
|
93
|
+
let closest = 0;
|
|
94
|
+
let closestDist = Infinity;
|
|
95
|
+
Array.from(rowEl.children).forEach((child, i) => {
|
|
96
|
+
const rect = (child as HTMLElement).getBoundingClientRect();
|
|
97
|
+
const dist = Math.abs(rect.left + rect.width / 2 - rowCenter);
|
|
98
|
+
if (dist < closestDist) {
|
|
99
|
+
closestDist = dist;
|
|
100
|
+
closest = i;
|
|
101
|
+
}
|
|
102
|
+
});
|
|
103
|
+
activeIndex = closest;
|
|
103
104
|
}
|
|
104
105
|
</script>
|
|
105
106
|
|
|
@@ -133,110 +134,118 @@
|
|
|
133
134
|
<h2>{@html title}</h2>
|
|
134
135
|
</div>
|
|
135
136
|
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
{:else}
|
|
155
|
-
<svg
|
|
156
|
-
class="avatar card-avatar"
|
|
157
|
-
viewBox="0 0 5 5"
|
|
158
|
-
style="background: {av.bg}"
|
|
159
|
-
aria-hidden="true"
|
|
160
|
-
>
|
|
161
|
-
{#each av.cells as cell}
|
|
162
|
-
<rect x={cell.x} y={cell.y} width="1" height="1" fill={av.fg} />
|
|
163
|
-
{/each}
|
|
137
|
+
<div class="hds-container-max cards-wrap">
|
|
138
|
+
<div
|
|
139
|
+
class="cards-row"
|
|
140
|
+
bind:this={rowEl}
|
|
141
|
+
onscroll={onRowScroll}
|
|
142
|
+
role="region"
|
|
143
|
+
aria-label={label}
|
|
144
|
+
>
|
|
145
|
+
{#each reviews as review, i}
|
|
146
|
+
{#if review.type === 'text'}
|
|
147
|
+
<figure class="card text-card hds-box">
|
|
148
|
+
{#if review.imageUrl}
|
|
149
|
+
<img class="avatar photo card-avatar" src={review.imageUrl} alt={review.name} />
|
|
150
|
+
{/if}
|
|
151
|
+
<svg class="quote-mark" viewBox="0 0 24 24" aria-hidden="true">
|
|
152
|
+
<path
|
|
153
|
+
d="M10 7c-3.3 0-6 2.7-6 6v4h6v-6H7c0-1.7 1.3-3 3-3V7zm10 0c-3.3 0-6 2.7-6 6v4h6v-6h-3c0-1.7 1.3-3 3-3V7z"
|
|
154
|
+
/>
|
|
164
155
|
</svg>
|
|
165
|
-
{/if}
|
|
166
|
-
<svg class="quote-mark" viewBox="0 0 24 24" aria-hidden="true">
|
|
167
|
-
<path
|
|
168
|
-
d="M10 7c-3.3 0-6 2.7-6 6v4h6v-6H7c0-1.7 1.3-3 3-3V7zm10 0c-3.3 0-6 2.7-6 6v4h6v-6h-3c0-1.7 1.3-3 3-3V7z"
|
|
169
|
-
/>
|
|
170
|
-
</svg>
|
|
171
|
-
{#if review.summary}
|
|
172
|
-
<p class="text-summary">{review.summary}</p>
|
|
173
|
-
{/if}
|
|
174
|
-
<blockquote>“{review.quote}”</blockquote>
|
|
175
|
-
<figcaption>
|
|
176
|
-
<span class="caption-text">
|
|
177
|
-
<span class="name">{review.name}</span>
|
|
178
|
-
{@render meta(review)}
|
|
179
|
-
</span>
|
|
180
|
-
</figcaption>
|
|
181
|
-
</figure>
|
|
182
|
-
{:else}
|
|
183
|
-
<figure class="card video-card hds-box" class:playing={playing[i]}>
|
|
184
|
-
{#if started[i]}
|
|
185
|
-
<!-- svelte-ignore a11y_media_has_caption -->
|
|
186
|
-
<video
|
|
187
|
-
class="video"
|
|
188
|
-
src={review.videoUrl}
|
|
189
|
-
poster={review.posterUrl}
|
|
190
|
-
controls={playing[i]}
|
|
191
|
-
playsinline
|
|
192
|
-
bind:this={videoEls[i]}
|
|
193
|
-
use:autoplayVideo
|
|
194
|
-
onplay={() => (playing[i] = true)}
|
|
195
|
-
onpause={(e) => onVideoPause(e, i)}
|
|
196
|
-
onended={() => (playing[i] = false)}
|
|
197
|
-
></video>
|
|
198
|
-
{:else}
|
|
199
|
-
<div class="poster">
|
|
200
|
-
<img src={review.posterUrl} alt="" />
|
|
201
|
-
</div>
|
|
202
|
-
{/if}
|
|
203
|
-
|
|
204
|
-
{#if !playing[i]}
|
|
205
|
-
<div class="poster-scrim"></div>
|
|
206
|
-
|
|
207
156
|
{#if review.summary}
|
|
208
|
-
<p class="
|
|
157
|
+
<p class="text-summary">{review.summary}</p>
|
|
209
158
|
{/if}
|
|
210
|
-
|
|
211
|
-
<button
|
|
212
|
-
class="play-btn"
|
|
213
|
-
aria-label={started[i] ? 'Resume video testimonial' : 'Play video testimonial'}
|
|
214
|
-
onclick={() => play(i)}
|
|
215
|
-
>
|
|
216
|
-
<svg
|
|
217
|
-
width="20"
|
|
218
|
-
height="20"
|
|
219
|
-
viewBox="0 0 16 16"
|
|
220
|
-
fill="currentColor"
|
|
221
|
-
aria-hidden="true"
|
|
222
|
-
>
|
|
223
|
-
<path d="M5 3.5v9l8-4.5-8-4.5z" />
|
|
224
|
-
</svg>
|
|
225
|
-
</button>
|
|
226
|
-
|
|
159
|
+
<blockquote>“{review.quote}”</blockquote>
|
|
227
160
|
<figcaption>
|
|
228
|
-
{#if review.imageUrl}
|
|
229
|
-
<img class="avatar photo" src={review.imageUrl} alt={review.name} />
|
|
230
|
-
{/if}
|
|
231
161
|
<span class="caption-text">
|
|
232
162
|
<span class="name">{review.name}</span>
|
|
233
163
|
{@render meta(review)}
|
|
234
164
|
</span>
|
|
235
165
|
</figcaption>
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
166
|
+
</figure>
|
|
167
|
+
{:else}
|
|
168
|
+
<!-- svelte-ignore a11y_no_noninteractive_element_to_interactive_role -->
|
|
169
|
+
<figure
|
|
170
|
+
class="card video-card hds-box"
|
|
171
|
+
class:playing={playing[i]}
|
|
172
|
+
role="button"
|
|
173
|
+
tabindex="0"
|
|
174
|
+
aria-label={playing[i]
|
|
175
|
+
? 'Pause video testimonial'
|
|
176
|
+
: started[i]
|
|
177
|
+
? 'Resume video testimonial'
|
|
178
|
+
: 'Play video testimonial'}
|
|
179
|
+
onclick={(e) => onCardClick(e, i)}
|
|
180
|
+
onkeydown={(e) => onCardKeydown(e, i)}
|
|
181
|
+
>
|
|
182
|
+
{#if started[i]}
|
|
183
|
+
<!-- svelte-ignore a11y_media_has_caption -->
|
|
184
|
+
<video
|
|
185
|
+
class="video"
|
|
186
|
+
src={review.videoUrl}
|
|
187
|
+
poster={review.posterUrl}
|
|
188
|
+
playsinline
|
|
189
|
+
bind:this={videoEls[i]}
|
|
190
|
+
use:autoplayVideo
|
|
191
|
+
onplay={() => (playing[i] = true)}
|
|
192
|
+
onpause={(e) => onVideoPause(e, i)}
|
|
193
|
+
onended={() => (playing[i] = false)}
|
|
194
|
+
></video>
|
|
195
|
+
{:else}
|
|
196
|
+
<div class="poster">
|
|
197
|
+
<img src={review.posterUrl} alt="" />
|
|
198
|
+
</div>
|
|
199
|
+
{/if}
|
|
200
|
+
|
|
201
|
+
{#if !playing[i]}
|
|
202
|
+
<div class="poster-scrim"></div>
|
|
203
|
+
|
|
204
|
+
{#if review.summary}
|
|
205
|
+
<p class="video-summary">“{review.summary}”</p>
|
|
206
|
+
{/if}
|
|
207
|
+
|
|
208
|
+
<div class="play-btn" aria-hidden="true">
|
|
209
|
+
<svg
|
|
210
|
+
width="20"
|
|
211
|
+
height="20"
|
|
212
|
+
viewBox="0 0 16 16"
|
|
213
|
+
fill="currentColor"
|
|
214
|
+
aria-hidden="true"
|
|
215
|
+
>
|
|
216
|
+
<path d="M5 3.5v9l8-4.5-8-4.5z" />
|
|
217
|
+
</svg>
|
|
218
|
+
</div>
|
|
219
|
+
|
|
220
|
+
<figcaption>
|
|
221
|
+
{#if review.imageUrl}
|
|
222
|
+
<img class="avatar photo" src={review.imageUrl} alt={review.name} />
|
|
223
|
+
{/if}
|
|
224
|
+
<span class="caption-text">
|
|
225
|
+
<span class="name">{review.name}</span>
|
|
226
|
+
{@render meta(review)}
|
|
227
|
+
</span>
|
|
228
|
+
</figcaption>
|
|
229
|
+
{/if}
|
|
230
|
+
</figure>
|
|
231
|
+
{/if}
|
|
232
|
+
{/each}
|
|
233
|
+
</div>
|
|
234
|
+
|
|
235
|
+
{#if reviews.length > 1}
|
|
236
|
+
<div class="dots" role="tablist" aria-label="Testimonials navigation">
|
|
237
|
+
{#each reviews as _, i}
|
|
238
|
+
<button
|
|
239
|
+
class="dot"
|
|
240
|
+
class:active={i === activeIndex}
|
|
241
|
+
role="tab"
|
|
242
|
+
aria-selected={i === activeIndex}
|
|
243
|
+
aria-label={`Go to testimonial ${i + 1}`}
|
|
244
|
+
onclick={() => goTo(i)}
|
|
245
|
+
></button>
|
|
246
|
+
{/each}
|
|
247
|
+
</div>
|
|
248
|
+
{/if}
|
|
240
249
|
</div>
|
|
241
250
|
</section>
|
|
242
251
|
|
|
@@ -245,6 +254,7 @@
|
|
|
245
254
|
background: var(--accent-lightest);
|
|
246
255
|
padding: 100px 0 96px;
|
|
247
256
|
overflow: hidden;
|
|
257
|
+
scroll-margin-top: calc(var(--header-height, 55px) + 20px);
|
|
248
258
|
}
|
|
249
259
|
|
|
250
260
|
.head {
|
|
@@ -270,31 +280,17 @@
|
|
|
270
280
|
font-family: var(--font-serif);
|
|
271
281
|
}
|
|
272
282
|
|
|
273
|
-
.
|
|
274
|
-
|
|
283
|
+
.cards-wrap {
|
|
284
|
+
padding: 32px 15px 4px;
|
|
285
|
+
box-sizing: border-box;
|
|
286
|
+
}
|
|
275
287
|
|
|
288
|
+
.cards-row {
|
|
276
289
|
display: flex;
|
|
290
|
+
flex-wrap: wrap;
|
|
277
291
|
align-items: stretch;
|
|
292
|
+
justify-content: center;
|
|
278
293
|
gap: 20px;
|
|
279
|
-
overflow-x: auto;
|
|
280
|
-
overflow-y: visible;
|
|
281
|
-
scroll-behavior: smooth;
|
|
282
|
-
cursor: grab;
|
|
283
|
-
padding: 32px var(--side-padding) 36px;
|
|
284
|
-
scrollbar-width: none;
|
|
285
|
-
-webkit-overflow-scrolling: touch;
|
|
286
|
-
touch-action: pan-y;
|
|
287
|
-
user-select: none;
|
|
288
|
-
-webkit-user-select: none;
|
|
289
|
-
}
|
|
290
|
-
|
|
291
|
-
.scroll-row.dragging {
|
|
292
|
-
cursor: grabbing;
|
|
293
|
-
scroll-behavior: auto;
|
|
294
|
-
}
|
|
295
|
-
|
|
296
|
-
.scroll-row::-webkit-scrollbar {
|
|
297
|
-
display: none;
|
|
298
294
|
}
|
|
299
295
|
|
|
300
296
|
.card {
|
|
@@ -310,6 +306,8 @@
|
|
|
310
306
|
}
|
|
311
307
|
|
|
312
308
|
.text-card {
|
|
309
|
+
height: auto;
|
|
310
|
+
min-height: 440px;
|
|
313
311
|
padding: 28px;
|
|
314
312
|
display: flex;
|
|
315
313
|
flex-direction: column;
|
|
@@ -417,15 +415,14 @@
|
|
|
417
415
|
}
|
|
418
416
|
|
|
419
417
|
.video-card {
|
|
420
|
-
width: 280px;
|
|
421
418
|
overflow: hidden;
|
|
422
419
|
padding: 0;
|
|
420
|
+
cursor: pointer;
|
|
423
421
|
}
|
|
424
422
|
|
|
425
|
-
.video-card
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
z-index: 1;
|
|
423
|
+
.video-card:focus-visible {
|
|
424
|
+
outline: 2px solid var(--accent);
|
|
425
|
+
outline-offset: 2px;
|
|
429
426
|
}
|
|
430
427
|
|
|
431
428
|
.poster {
|
|
@@ -528,11 +525,26 @@
|
|
|
528
525
|
color: rgba(255, 255, 255, 0.9);
|
|
529
526
|
}
|
|
530
527
|
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
528
|
+
.dots {
|
|
529
|
+
display: none;
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
.dot {
|
|
533
|
+
width: 8px;
|
|
534
|
+
height: 8px;
|
|
535
|
+
padding: 0;
|
|
536
|
+
border: none;
|
|
537
|
+
border-radius: 50%;
|
|
538
|
+
background: var(--border);
|
|
539
|
+
cursor: pointer;
|
|
540
|
+
transition:
|
|
541
|
+
background 0.2s,
|
|
542
|
+
transform 0.2s;
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
.dot.active {
|
|
546
|
+
background: var(--accent);
|
|
547
|
+
transform: scale(1.3);
|
|
536
548
|
}
|
|
537
549
|
|
|
538
550
|
@media (max-width: 768px) {
|
|
@@ -546,13 +558,44 @@
|
|
|
546
558
|
height: 400px;
|
|
547
559
|
}
|
|
548
560
|
|
|
549
|
-
.
|
|
550
|
-
|
|
561
|
+
.text-card {
|
|
562
|
+
height: auto;
|
|
563
|
+
min-height: 400px;
|
|
551
564
|
}
|
|
565
|
+
}
|
|
552
566
|
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
567
|
+
@media (max-width: 640px) {
|
|
568
|
+
.cards-wrap {
|
|
569
|
+
padding: 24px 0 4px;
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
.cards-row {
|
|
573
|
+
flex-wrap: nowrap;
|
|
574
|
+
justify-content: flex-start;
|
|
575
|
+
overflow-x: auto;
|
|
576
|
+
scroll-snap-type: x mandatory;
|
|
577
|
+
-webkit-overflow-scrolling: touch;
|
|
578
|
+
scrollbar-width: none;
|
|
579
|
+
padding: 0 24px;
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
.cards-row::-webkit-scrollbar {
|
|
583
|
+
display: none;
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
.card {
|
|
587
|
+
flex: 0 0 calc(100% - 32px);
|
|
588
|
+
width: calc(100% - 32px);
|
|
589
|
+
max-width: 360px;
|
|
590
|
+
scroll-snap-align: center;
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
.dots {
|
|
594
|
+
display: flex;
|
|
595
|
+
justify-content: center;
|
|
596
|
+
align-items: center;
|
|
597
|
+
gap: 8px;
|
|
598
|
+
margin-top: 20px;
|
|
556
599
|
}
|
|
557
600
|
}
|
|
558
601
|
</style>
|