alpha
Login
or
Join now
xcc.es
/
morganwill.com
Star
0
Fork
0
Atom
Configure Feed
Issues
Pull Requests
Commits
Tags
Feed URL
Select the types of activity you want to include in your feed.
[READ-ONLY] Mirror of https://github.com/mrgnw/morganwill.com.
morganwill.com
Star
0
Fork
0
Atom
Configure Feed
Issues
Pull Requests
Commits
Tags
Feed URL
Select the types of activity you want to include in your feed.
Overview
Issues
Pulls
Pipelines
constistent qr / qrs styling
author
Morgan
date
8 months ago
(Nov 28, 2025, 12:23 PM +0100)
commit
ef3f58d9
ef3f58d9169f2134a2714f174a81826f6bf43f8b
parent
3ccf43b7
3ccf43b72b8e424d1e7abdc531909a77e8e1b933
+28
-97
2 changed files
Expand all
Collapse all
Unified
Split
src
components
LinkIcons.svelte
routes
+page.server.js
+26
-89
src/components/LinkIcons.svelte
View file
Reviewed
···
2
2
import { onMount } from "svelte";
3
3
import { fade } from "svelte/transition";
4
4
import { innerWidth, innerHeight } from "svelte/reactivity/window";
5
5
+
import ColoredQr from "./ColoredQr.svelte";
5
6
6
7
// Icon imports - managed internally
7
8
import JamLinkedinCircle from "~icons/jam/linkedin-circle";
···
250
251
251
252
// Reactive size unit for grid
252
253
let sizeUnit = $derived(size ? `${size}px` : `${initialSize}vh`);
253
253
-
254
254
-
/**
255
255
-
* Build SVG gradient defs from colors array
256
256
-
* @param {string[]} colors
257
257
-
* @param {string} id
258
258
-
* @param {number} size - SVG viewBox size
259
259
-
* @returns {string}
260
260
-
*/
261
261
-
function buildGradientDefs(colors, id, size) {
262
262
-
const stops = colors.map((color, i) => {
263
263
-
const offset = (i / (colors.length - 1)) * 100;
264
264
-
return `<stop offset="${offset}%" stop-color="${color}"/>`;
265
265
-
}).join('');
266
266
-
// Use userSpaceOnUse so gradient spans the entire SVG, not each rect
267
267
-
// Diagonal from top-left to bottom-right
268
268
-
return `<defs><linearGradient id="${id}" gradientUnits="userSpaceOnUse" x1="0" y1="0" x2="${size}" y2="${size}">${stops}</linearGradient></defs>`;
269
269
-
}
270
270
-
271
271
-
/**
272
272
-
* Svelte action to use shadow DOM for gradient isolation
273
273
-
* @param {HTMLElement} node
274
274
-
* @param {{ qr: string, colors: string[], id: string }} params
275
275
-
*/
276
276
-
function shadowGradient(node, { qr, colors, id }) {
277
277
-
const shadowRoot = node.attachShadow({ mode: 'open' });
278
278
-
279
279
-
// Extract the viewBox size from the SVG
280
280
-
const sizeMatch = qr.match(/viewBox="0 0 (\d+) (\d+)"/);
281
281
-
const svgSize = sizeMatch ? parseInt(sizeMatch[1]) : 164;
282
282
-
283
283
-
// Inject the gradient defs into the SVG
284
284
-
const gradientDefs = buildGradientDefs(colors, id, svgSize);
285
285
-
// Replace fill="currentColor" with fill="url(#id)"
286
286
-
const coloredQr = qr.replace(/fill="currentColor"/g, `fill="url(#${id})"`);
287
287
-
// Insert defs after opening svg tag
288
288
-
const svgWithDefs = coloredQr.replace(/(<svg[^>]*>)/, `$1${gradientDefs}`);
289
289
-
290
290
-
shadowRoot.innerHTML = `
291
291
-
<style>
292
292
-
:host { display: contents; }
293
293
-
svg { width: 100%; height: 100%; }
294
294
-
rect {
295
295
-
opacity: 0;
296
296
-
animation: fadeIn 0.15s ease-out forwards;
297
297
-
}
298
298
-
@keyframes fadeIn {
299
299
-
to { opacity: 1; }
300
300
-
}
301
301
-
</style>
302
302
-
${svgWithDefs}
303
303
-
`;
304
304
-
305
305
-
return {
306
306
-
destroy() {
307
307
-
// Cleanup if needed
308
308
-
}
309
309
-
};
310
310
-
}
311
254
</script>
312
255
313
256
<div class="link-icons" class:qrs-mode={qrsMode}>
···
321
264
>
322
265
{#each links as link, index (link.title)}
323
266
{@const animOrder = cardOrder.get(index) ?? index}
324
324
-
{@const isGradient = (link.colors?.length ?? 0) > 1}
325
325
-
{@const singleColor = link.colors?.[0] ?? '#888'}
326
267
<a
327
268
href={link.url}
328
269
target="_blank"
···
331
272
onmouseenter={() => hoveredCard = index}
332
273
onmouseleave={() => hoveredCard = null}
333
274
>
334
334
-
{#if isGradient && link.qr && link.colors}
335
335
-
<div
336
336
-
class="qr-card-code"
337
337
-
use:shadowGradient={{ qr: link.qr, colors: link.colors, id: `grad-${link.title}` }}
338
338
-
use:waveAction={100 + animOrder * 80}
339
339
-
></div>
340
340
-
{:else}
341
341
-
<div
342
342
-
class="qr-card-code"
343
343
-
style="--brand-color: {singleColor}"
344
344
-
use:waveAction={100 + animOrder * 80}
345
345
-
>
346
346
-
{@html link.qr}
347
347
-
</div>
348
348
-
{/if}
275
275
+
<div
276
276
+
class="qr-card-code"
277
277
+
use:waveAction={100 + animOrder * 80}
278
278
+
>
279
279
+
{#if link.qr}
280
280
+
<ColoredQr
281
281
+
qr={link.qr}
282
282
+
colors={link.colors ?? ['#888']}
283
283
+
id="qr-{link.title}"
284
284
+
animate={false}
285
285
+
/>
286
286
+
{/if}
287
287
+
</div>
349
288
<span class="qr-card-title">{link.title}</span>
350
289
</a>
351
290
{/each}
352
291
</div>
353
292
{:else}
354
293
<h1 class="title" ondblclick={toggleQrMode}>
355
355
-
{#if qrMode && selectedQr}
294
294
+
{#if qrMode && selectedLink?.qr}
356
295
<div class="qr-wrapper">
357
296
{#if selectedUrl}
358
297
<div class="url-display" transition:fade={{ duration: 300 }}>
···
365
304
</a>
366
305
</div>
367
306
{/if}
368
368
-
{#if typeof selectedQr === "string"}
369
369
-
{@html selectedQr}
370
370
-
{/if}
307
307
+
<ColoredQr
308
308
+
qr={selectedLink.qr}
309
309
+
colors={selectedLink.colors ?? ['#888']}
310
310
+
id="qr-{selectedLink.title}"
311
311
+
animate={false}
312
312
+
/>
371
313
</div>
372
314
{:else}
373
315
{selected ?? defaultTitle}
···
512
454
height: 100%;
513
455
min-width: 0;
514
456
min-height: 0;
515
515
-
color: var(--brand-color, #888);
516
457
position: relative;
517
458
}
518
459
···
549
490
550
491
/* Animate individual QR modules (rects) with radial bloom effect */
551
492
.qr-card-code :global(svg rect) {
552
552
-
fill: currentColor;
553
493
opacity: 0;
554
494
transform-origin: center center;
555
495
transform: scale(0) rotate(180deg);
···
610
550
fill: transparent;
611
551
}
612
552
613
613
-
.qr-card:hover .qr-card-code {
614
614
-
color: var(--highlight);
615
615
-
}
616
616
-
617
553
.qr-card:hover .qr-card-code :global(svg path:last-child) {
618
554
stroke: var(--highlight);
619
619
-
}
620
620
-
621
621
-
.qr-card:hover .qr-card-code :global(svg rect) {
622
622
-
fill: currentColor;
623
555
}
624
556
625
557
.qr-card:hover .qr-card-title {
···
645
577
display: flex;
646
578
flex-direction: column;
647
579
align-items: center;
580
580
+
}
581
581
+
582
582
+
.qr-wrapper :global(.colored-qr) {
583
583
+
width: 164px;
584
584
+
height: 164px;
648
585
}
649
586
650
587
.qr-wrapper :global(svg) {
+2
-8
src/routes/+page.server.js
View file
Reviewed
···
176
176
const linksWithQr = await Promise.all(
177
177
combinedLinks.map(async (link) => {
178
178
const qrUrl = link.shortUrl ?? link.url;
179
179
-
// Use animated SVG (individual rects) for qrs mode
180
180
-
const qr = willBeQrsMode
181
181
-
? generateAnimatedQRSvg(qrUrl, 164)
182
182
-
: await QRCode.toString(qrUrl, {
183
183
-
type: "svg",
184
184
-
width: 164,
185
185
-
margin: 0,
186
186
-
});
179
179
+
// Always use our custom SVG format with fill="currentColor" for consistent styling
180
180
+
const qr = generateAnimatedQRSvg(qrUrl, 164);
187
181
return { ...link, qr };
188
182
})
189
183
);