@hyvor/design 2.1.8-beta.2 → 2.1.8-beta.3

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,382 @@
1
+ <script lang="ts">
2
+ interface TextReview {
3
+ type: 'text';
4
+ name: string;
5
+ role: string;
6
+ quote: string;
7
+ }
8
+
9
+ interface VideoReview {
10
+ type: 'video';
11
+ name: string;
12
+ role: string;
13
+ videoUrl?: string;
14
+ posterUrl?: string;
15
+ }
16
+
17
+ type Review = TextReview | VideoReview;
18
+
19
+ interface Props {
20
+ label?: string;
21
+ title?: string;
22
+ reviews: Review[];
23
+ // the reviewer name is set in a handwritten-style font (Caveat, loaded
24
+ // from Bunny Fonts) by default — turn off to use the regular typeface,
25
+ // e.g. if the host page can't reach an external font host
26
+ handwrittenNames?: boolean;
27
+ }
28
+
29
+ let {
30
+ label = 'Testimonials',
31
+ title = 'Loved by our customers.',
32
+ reviews,
33
+ handwrittenNames = true
34
+ }: Props = $props();
35
+
36
+ // deterministic, non-photographic "identicon" per reviewer name — used as a
37
+ // placeholder avatar when no photo is available for a text review
38
+ function identicon(seed: string) {
39
+ let hash = 0;
40
+ for (let i = 0; i < seed.length; i++) {
41
+ hash = (hash * 31 + seed.charCodeAt(i)) | 0;
42
+ }
43
+ const hue = Math.abs(hash) % 360;
44
+ const cells: { x: number; y: number }[] = [];
45
+ for (let col = 0; col < 3; col++) {
46
+ for (let row = 0; row < 5; row++) {
47
+ const bit = (hash >> (col * 5 + row)) & 1;
48
+ if (!bit) continue;
49
+ cells.push({ x: col, y: row });
50
+ if (col < 2) cells.push({ x: 4 - col, y: row });
51
+ }
52
+ }
53
+ return {
54
+ bg: `hsl(${hue} 45% 92%)`,
55
+ fg: `hsl(${hue} 50% 40%)`,
56
+ cells
57
+ };
58
+ }
59
+
60
+ let scrollEl: HTMLDivElement | undefined = $state();
61
+ let dragging = $state(false);
62
+ let dragStartX = 0;
63
+ let dragStartScroll = 0;
64
+
65
+ function onPointerDown(e: PointerEvent) {
66
+ if (!scrollEl) return;
67
+ dragging = true;
68
+ dragStartX = e.clientX;
69
+ dragStartScroll = scrollEl.scrollLeft;
70
+ scrollEl.setPointerCapture(e.pointerId);
71
+ }
72
+
73
+ function onPointerMove(e: PointerEvent) {
74
+ if (!dragging || !scrollEl) return;
75
+ scrollEl.scrollLeft = dragStartScroll - (e.clientX - dragStartX);
76
+ }
77
+
78
+ function onPointerUp() {
79
+ dragging = false;
80
+ }
81
+ </script>
82
+
83
+ <svelte:head>
84
+ {#if handwrittenNames}
85
+ <link href="https://fonts.bunny.net/css?family=caveat:600,700" rel="stylesheet" />
86
+ {/if}
87
+ </svelte:head>
88
+
89
+ <section class="hds-testimonials" class:handwritten={handwrittenNames}>
90
+ <div class="hds-container head">
91
+ <p class="label">{label}</p>
92
+ <h2>{@html title}</h2>
93
+ </div>
94
+
95
+ <!-- svelte-ignore a11y_no_static_element_interactions -->
96
+ <div
97
+ class="scroll-row"
98
+ class:dragging
99
+ bind:this={scrollEl}
100
+ onpointerdown={onPointerDown}
101
+ onpointermove={onPointerMove}
102
+ onpointerup={onPointerUp}
103
+ onpointerleave={onPointerUp}
104
+ role="region"
105
+ aria-label={label}
106
+ >
107
+ {#each reviews as review}
108
+ {#if review.type === 'text'}
109
+ {@const av = identicon(review.name)}
110
+ <figure class="card text-card hds-box">
111
+ <svg class="avatar" viewBox="0 0 5 5" style="background: {av.bg}" aria-hidden="true">
112
+ {#each av.cells as cell}
113
+ <rect x={cell.x} y={cell.y} width="1" height="1" fill={av.fg} />
114
+ {/each}
115
+ </svg>
116
+ <blockquote>&ldquo;{review.quote}&rdquo;</blockquote>
117
+ <figcaption>
118
+ <span class="name">{review.name}</span>
119
+ <span class="role">{review.role}</span>
120
+ </figcaption>
121
+ </figure>
122
+ {:else}
123
+ <figure class="card video-card hds-box">
124
+ <div class="poster">
125
+ <img src={review.posterUrl} alt="" />
126
+ <div class="poster-scrim"></div>
127
+ </div>
128
+
129
+ <div class="video-avatar">
130
+ <img src={review.posterUrl} alt="" />
131
+ </div>
132
+
133
+ <button class="play-btn" aria-label="Play video testimonial" disabled>
134
+ <svg width="20" height="20" viewBox="0 0 16 16" fill="currentColor" aria-hidden="true">
135
+ <path d="M5 3.5v9l8-4.5-8-4.5z" />
136
+ </svg>
137
+ </button>
138
+
139
+ <figcaption>
140
+ <span class="name">{review.name}</span>
141
+ <span class="role">{review.role}</span>
142
+ </figcaption>
143
+ </figure>
144
+ {/if}
145
+ {/each}
146
+ </div>
147
+ </section>
148
+
149
+ <style>
150
+ .hds-testimonials {
151
+ background: var(--accent-lightest);
152
+ padding: 100px 0 96px;
153
+ overflow: hidden;
154
+ }
155
+
156
+ .head {
157
+ margin-bottom: 44px;
158
+ }
159
+
160
+ .label {
161
+ font-size: 14px;
162
+ font-weight: 600;
163
+ letter-spacing: 0.09em;
164
+ text-transform: uppercase;
165
+ color: var(--accent);
166
+ margin: 0 0 16px;
167
+ }
168
+
169
+ h2 {
170
+ font-size: clamp(30px, 4vw, 44px);
171
+ font-weight: 800;
172
+ letter-spacing: -0.02em;
173
+ line-height: 1.15;
174
+ color: var(--text);
175
+ margin: 0;
176
+ font-family: var(--font-serif);
177
+ }
178
+
179
+ .scroll-row {
180
+ display: flex;
181
+ align-items: stretch;
182
+ gap: 20px;
183
+ overflow-x: auto;
184
+ /* explicit, not left to default — a lone overflow-x: auto silently
185
+ forces overflow-y to auto too (per spec, when only one axis is
186
+ 'visible'), which was clipping each card's box-shadow into a hard
187
+ line at the top/bottom of the row instead of letting it fade out */
188
+ overflow-y: visible;
189
+ scroll-snap-type: x proximity;
190
+ cursor: grab;
191
+ /* top/bottom padding must clear --box-shadow's blur radius, or the
192
+ shadow still gets clipped by the row's own padding-box edge */
193
+ padding: 32px 15px 36px max(15px, calc((100vw - 1000px) / 2));
194
+ scrollbar-width: none;
195
+ -webkit-overflow-scrolling: touch;
196
+ touch-action: pan-y;
197
+ }
198
+
199
+ .scroll-row.dragging {
200
+ cursor: grabbing;
201
+ scroll-snap-type: none;
202
+ }
203
+
204
+ .scroll-row::-webkit-scrollbar {
205
+ display: none;
206
+ }
207
+
208
+ .card {
209
+ position: relative;
210
+ flex: 0 0 auto;
211
+ width: 320px;
212
+ height: 440px;
213
+ margin: 0;
214
+ box-sizing: border-box;
215
+ scroll-snap-align: start;
216
+ }
217
+
218
+ .text-card {
219
+ padding: 28px;
220
+ display: flex;
221
+ flex-direction: column;
222
+ }
223
+
224
+ .avatar {
225
+ width: 46px;
226
+ height: 46px;
227
+ border-radius: 50%;
228
+ flex-shrink: 0;
229
+ display: block;
230
+ shape-rendering: crispEdges;
231
+ }
232
+
233
+ blockquote {
234
+ font-size: 16px;
235
+ line-height: 1.7;
236
+ color: var(--text);
237
+ margin: 20px 0 0;
238
+ flex: 1;
239
+ }
240
+
241
+ .text-card figcaption {
242
+ display: flex;
243
+ flex-direction: column;
244
+ gap: 0;
245
+ }
246
+
247
+ .name {
248
+ font-size: 16px;
249
+ font-weight: 600;
250
+ color: var(--text);
251
+ line-height: 1.2;
252
+ }
253
+
254
+ .handwritten .name {
255
+ font-family: 'Caveat', cursive;
256
+ font-size: 28px;
257
+ font-weight: 600;
258
+ }
259
+
260
+ .role {
261
+ font-size: 13px;
262
+ color: var(--text-light);
263
+ }
264
+
265
+ .video-card {
266
+ width: 280px;
267
+ overflow: hidden;
268
+ padding: 0;
269
+ }
270
+
271
+ .poster {
272
+ position: absolute;
273
+ inset: 0;
274
+ }
275
+
276
+ .poster img {
277
+ width: 100%;
278
+ height: 100%;
279
+ object-fit: cover;
280
+ display: block;
281
+ }
282
+
283
+ /* even out contrast across whatever poster photo lands here, so the
284
+ play button and captions stay legible */
285
+ .poster-scrim {
286
+ position: absolute;
287
+ inset: 0;
288
+ background: linear-gradient(160deg, rgba(20, 14, 12, 0.45), rgba(20, 14, 12, 0.65));
289
+ }
290
+
291
+ .video-avatar {
292
+ position: absolute;
293
+ top: 16px;
294
+ left: 16px;
295
+ width: 40px;
296
+ height: 40px;
297
+ border-radius: 50%;
298
+ overflow: hidden;
299
+ box-shadow: 0 0 0 3px rgba(255, 255, 255, 0.85);
300
+ }
301
+
302
+ .video-avatar img {
303
+ width: 100%;
304
+ height: 100%;
305
+ object-fit: cover;
306
+ display: block;
307
+ }
308
+
309
+ .play-btn {
310
+ position: absolute;
311
+ top: 50%;
312
+ left: 50%;
313
+ transform: translate(-50%, -50%);
314
+ width: 56px;
315
+ height: 56px;
316
+ border-radius: 50%;
317
+ background: rgba(255, 255, 255, 0.18);
318
+ backdrop-filter: blur(6px);
319
+ -webkit-backdrop-filter: blur(6px);
320
+ border: 1px solid rgba(255, 255, 255, 0.4);
321
+ color: #fff;
322
+ display: flex;
323
+ align-items: center;
324
+ justify-content: center;
325
+ cursor: default;
326
+ transition:
327
+ transform 0.2s,
328
+ background 0.2s;
329
+ }
330
+
331
+ .play-btn:not(:disabled):hover {
332
+ transform: translate(-50%, -50%) scale(1.08);
333
+ background: rgba(255, 255, 255, 0.28);
334
+ }
335
+
336
+ .play-btn svg {
337
+ margin-left: 3px;
338
+ }
339
+
340
+ .video-card figcaption {
341
+ position: absolute;
342
+ left: 0;
343
+ right: 0;
344
+ bottom: 0;
345
+ padding: 40px 20px 20px;
346
+ background: linear-gradient(to top, rgba(0, 0, 0, 0.75), transparent);
347
+ display: flex;
348
+ flex-direction: column;
349
+ }
350
+
351
+ .video-card .name {
352
+ color: #fff;
353
+ }
354
+
355
+ .video-card .role {
356
+ color: rgba(255, 255, 255, 0.75);
357
+ }
358
+
359
+ @media (max-width: 600px) {
360
+ /* keep in step with the wider .hds-container gutter set globally for
361
+ the host page at this same breakpoint */
362
+ .scroll-row {
363
+ padding: 32px 20px 36px max(20px, calc((100vw - 1000px) / 2));
364
+ }
365
+ }
366
+
367
+ @media (max-width: 768px) {
368
+ .head {
369
+ text-align: center;
370
+ margin-bottom: 32px;
371
+ }
372
+
373
+ .card {
374
+ width: 280px;
375
+ height: 400px;
376
+ }
377
+
378
+ .video-card {
379
+ width: 240px;
380
+ }
381
+ }
382
+ </style>
@@ -0,0 +1,23 @@
1
+ interface TextReview {
2
+ type: 'text';
3
+ name: string;
4
+ role: string;
5
+ quote: string;
6
+ }
7
+ interface VideoReview {
8
+ type: 'video';
9
+ name: string;
10
+ role: string;
11
+ videoUrl?: string;
12
+ posterUrl?: string;
13
+ }
14
+ type Review = TextReview | VideoReview;
15
+ interface Props {
16
+ label?: string;
17
+ title?: string;
18
+ reviews: Review[];
19
+ handwrittenNames?: boolean;
20
+ }
21
+ declare const Testimonials: import("svelte").Component<Props, {}, "">;
22
+ type Testimonials = ReturnType<typeof Testimonials>;
23
+ export default Testimonials;
@@ -12,3 +12,10 @@ export type { NavSectionConfig, NavConfig, NavPageConfig, NavFoldingSectionConfi
12
12
  export { default as Footer } from './Footer/Footer.svelte';
13
13
  export { default as FooterLinkList } from './Footer/FooterLinkList.svelte';
14
14
  export { default as Document } from './Document/Document.svelte';
15
+ export { default as Hero } from './Hero/Hero.svelte';
16
+ export { default as FeatureSplit } from './FeatureSplit/FeatureSplit.svelte';
17
+ export { default as Testimonials } from './Testimonials/Testimonials.svelte';
18
+ export { default as FullTrialSignup } from './FullTrialSignup/FullTrialSignup.svelte';
19
+ export { default as AllFeaturesAccordion } from './AllFeaturesAccordion/AllFeaturesAccordion.svelte';
20
+ export { default as AllFeaturesAccordionFeature } from './AllFeaturesAccordion/AllFeaturesAccordionFeature.svelte';
21
+ export { default as FAQ } from './FAQ/FAQ.svelte';
@@ -10,3 +10,10 @@ export { loadDocsPage } from './Docs/fulldocs.js';
10
10
  export { default as Footer } from './Footer/Footer.svelte';
11
11
  export { default as FooterLinkList } from './Footer/FooterLinkList.svelte';
12
12
  export { default as Document } from './Document/Document.svelte';
13
+ export { default as Hero } from './Hero/Hero.svelte';
14
+ export { default as FeatureSplit } from './FeatureSplit/FeatureSplit.svelte';
15
+ export { default as Testimonials } from './Testimonials/Testimonials.svelte';
16
+ export { default as FullTrialSignup } from './FullTrialSignup/FullTrialSignup.svelte';
17
+ export { default as AllFeaturesAccordion } from './AllFeaturesAccordion/AllFeaturesAccordion.svelte';
18
+ export { default as AllFeaturesAccordionFeature } from './AllFeaturesAccordion/AllFeaturesAccordionFeature.svelte';
19
+ export { default as FAQ } from './FAQ/FAQ.svelte';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hyvor/design",
3
- "version": "2.1.8-beta.2",
3
+ "version": "2.1.8-beta.3",
4
4
  "license": "MIT",
5
5
  "private": false,
6
6
  "repository": {