feat(ui): enhance Countdown and layout components
* Improve Countdown component styling and structure * Update layout to include responsive menu and background pattern * Adjust PhotoBackground and page styles for better presentation * Add damask pattern SVG for background use
This commit is contained in:
@@ -5,7 +5,11 @@
|
|||||||
pastMessage?: string;
|
pastMessage?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
let { target, compact = false, pastMessage = 'The big day is here!' }: Props = $props();
|
let {
|
||||||
|
target,
|
||||||
|
compact = false,
|
||||||
|
pastMessage = "The big day is here!",
|
||||||
|
}: Props = $props();
|
||||||
|
|
||||||
let now = $state(Date.now());
|
let now = $state(Date.now());
|
||||||
|
|
||||||
@@ -23,31 +27,45 @@
|
|||||||
const seconds = $derived(Math.floor((diffMs % 60_000) / 1000));
|
const seconds = $derived(Math.floor((diffMs % 60_000) / 1000));
|
||||||
|
|
||||||
const units = $derived([
|
const units = $derived([
|
||||||
{ label: 'days', value: days },
|
{ label: "days", value: days },
|
||||||
{ label: 'hours', value: hours },
|
{ label: "hours", value: hours },
|
||||||
{ label: 'minutes', value: minutes },
|
{ label: "minutes", value: minutes },
|
||||||
{ label: 'seconds', value: seconds }
|
{ label: "seconds", value: seconds },
|
||||||
]);
|
]);
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
{#if isPast}
|
{#if isPast}
|
||||||
<p class={compact ? 'text-lg font-medium sm:text-xl' : 'text-surface-50 text-xl font-medium'}>{pastMessage}</p>
|
<p
|
||||||
|
class={compact
|
||||||
|
? "text-lg font-medium sm:text-xl sm:text-md"
|
||||||
|
: "text-surface-50 text-2xl sm:text-md font-medium"}
|
||||||
|
>
|
||||||
|
{pastMessage}
|
||||||
|
</p>
|
||||||
{:else if compact}
|
{:else if compact}
|
||||||
<div class="flex items-baseline gap-2 text-lg sm:text-xl" role="timer" aria-live="polite">
|
<div class="flex items-baseline gap-2 z-10" role="timer" aria-live="polite">
|
||||||
{#each units as unit, i (unit.label)}
|
{#each units as unit, i (unit.label)}
|
||||||
{#if i > 0}<span class="text-secondary-300/60">·</span>{/if}
|
{#if i > 0}<span class="text-secondary-300/60">·</span>{/if}
|
||||||
<span class="font-semibold tabular-nums">{unit.value}</span>
|
<span class="font-semibold tabular-nums text-md md:text-3xl"
|
||||||
<span class="text-secondary-100/80 text-base">{unit.label}</span>
|
>{unit.value}</span
|
||||||
|
>
|
||||||
|
<span class="text-secondary-100/80 text-md md:text-3xl">{unit.label}</span
|
||||||
|
>
|
||||||
{/each}
|
{/each}
|
||||||
</div>
|
</div>
|
||||||
{:else}
|
{:else}
|
||||||
<div class="flex gap-4 sm:gap-8" role="timer" aria-live="polite">
|
<div class="flex gap-4 sm:gap-8" role="timer" aria-live="polite">
|
||||||
{#each units as unit (unit.label)}
|
{#each units as unit (unit.label)}
|
||||||
<div class="flex flex-col items-center">
|
<div class="flex flex-col items-center">
|
||||||
<span class="text-secondary-400 text-4xl font-bold tabular-nums sm:text-6xl">
|
<span
|
||||||
{String(unit.value).padStart(2, '0')}
|
class="text-secondary-400 text-4xl font-bold tabular-nums sm:text-6xl"
|
||||||
|
>
|
||||||
|
{String(unit.value).padStart(2, "0")}
|
||||||
</span>
|
</span>
|
||||||
<span class="text-surface-200 text-base tracking-wide uppercase sm:text-lg">{unit.label}</span>
|
<span
|
||||||
|
class="text-surface-200 text-base tracking-wide uppercase sm:text-lg"
|
||||||
|
>{unit.label}</span
|
||||||
|
>
|
||||||
</div>
|
</div>
|
||||||
{/each}
|
{/each}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,12 +1,15 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { backOut, cubicIn } from 'svelte/easing';
|
import { backOut, cubicIn } from "svelte/easing";
|
||||||
import { browser } from '$app/environment';
|
import { browser } from "$app/environment";
|
||||||
|
|
||||||
const modules = import.meta.glob('../../../assets/photos/*.{jpg,jpeg,png,JPG,JPEG,PNG}', {
|
const modules = import.meta.glob(
|
||||||
|
"../../../assets/photos/*.{jpg,jpeg,png,JPG,JPEG,PNG}",
|
||||||
|
{
|
||||||
eager: true,
|
eager: true,
|
||||||
import: 'default',
|
import: "default",
|
||||||
query: '?url'
|
query: "?url",
|
||||||
}) as Record<string, string>;
|
},
|
||||||
|
) as Record<string, string>;
|
||||||
|
|
||||||
function shuffle<T>(items: T[]): T[] {
|
function shuffle<T>(items: T[]): T[] {
|
||||||
const result = [...items];
|
const result = [...items];
|
||||||
@@ -28,7 +31,10 @@
|
|||||||
|
|
||||||
// Each photo flies in/out from a random angle with its own spin and pop, so
|
// Each photo flies in/out from a random angle with its own spin and pop, so
|
||||||
// no two transitions look alike.
|
// no two transitions look alike.
|
||||||
function funky(node: HTMLElement, { duration, variant }: { duration: number; variant: 'in' | 'out' }) {
|
function funky(
|
||||||
|
node: HTMLElement,
|
||||||
|
{ duration, variant }: { duration: number; variant: "in" | "out" },
|
||||||
|
) {
|
||||||
if (reducedMotion) {
|
if (reducedMotion) {
|
||||||
return { duration: 400, css: (t: number) => `opacity: ${t}` };
|
return { duration: 400, css: (t: number) => `opacity: ${t}` };
|
||||||
}
|
}
|
||||||
@@ -40,11 +46,11 @@
|
|||||||
const y = Math.sin(angle) * dist;
|
const y = Math.sin(angle) * dist;
|
||||||
return {
|
return {
|
||||||
duration: duration + Math.random() * 300,
|
duration: duration + Math.random() * 300,
|
||||||
easing: variant === 'in' ? backOut : cubicIn,
|
easing: variant === "in" ? backOut : cubicIn,
|
||||||
css: (t: number, u: number) => `
|
css: (t: number, u: number) => `
|
||||||
transform: translate(${u * x}%, ${u * y}%) rotate(${u * rot}deg) scale(${1 - u * (1 - scaleFrom)});
|
transform: translate(${u * x}%, ${u * y}%) rotate(${u * rot}deg) scale(${1 - u * (1 - scaleFrom)});
|
||||||
opacity: ${t};
|
opacity: ${t};
|
||||||
`
|
`,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -55,11 +61,12 @@
|
|||||||
|
|
||||||
$effect(() => {
|
$effect(() => {
|
||||||
if (!browser) return;
|
if (!browser) return;
|
||||||
const query = window.matchMedia('(prefers-reduced-motion: reduce)');
|
const query = window.matchMedia("(prefers-reduced-motion: reduce)");
|
||||||
reducedMotion = query.matches;
|
reducedMotion = query.matches;
|
||||||
const onChange = (event: MediaQueryListEvent) => (reducedMotion = event.matches);
|
const onChange = (event: MediaQueryListEvent) =>
|
||||||
query.addEventListener('change', onChange);
|
(reducedMotion = event.matches);
|
||||||
return () => query.removeEventListener('change', onChange);
|
query.addEventListener("change", onChange);
|
||||||
|
return () => query.removeEventListener("change", onChange);
|
||||||
});
|
});
|
||||||
|
|
||||||
$effect(() => {
|
$effect(() => {
|
||||||
@@ -72,16 +79,21 @@
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
{#if photos.length > 0}
|
{#if photos.length > 0}
|
||||||
<div class="pointer-events-none bg-surface-950 absolute inset-0 overflow-hidden" aria-hidden="true">
|
<div
|
||||||
|
class="pointer-events-none absolute inset-0 overflow-hidden"
|
||||||
|
aria-hidden="true"
|
||||||
|
>
|
||||||
{#key index}
|
{#key index}
|
||||||
<img
|
<img
|
||||||
src={photos[index]}
|
src={photos[index]}
|
||||||
alt=""
|
alt=""
|
||||||
class="absolute inset-0 h-full w-full object-contain"
|
class="absolute inset-0 h-full w-full object-cover md:object-contain"
|
||||||
in:funky={{ duration: 900, variant: 'in' }}
|
in:funky={{ duration: 900, variant: "in" }}
|
||||||
out:funky={{ duration: 700, variant: 'out' }}
|
out:funky={{ duration: 700, variant: "out" }}
|
||||||
/>
|
/>
|
||||||
{/key}
|
{/key}
|
||||||
<div class="absolute inset-0 bg-surface-950/65 dark:bg-surface-950/75"></div>
|
<div
|
||||||
|
class="absolute inset-0 bg-surface-950/65 dark:bg-surface-950/75"
|
||||||
|
></div>
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
|||||||
@@ -1,28 +1,48 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import '../app.css';
|
import "../app.css";
|
||||||
import favicon from '$lib/assets/favicon.svg';
|
import favicon from "$lib/assets/favicon.svg";
|
||||||
import { page } from '$app/state';
|
import { page } from "$app/state";
|
||||||
import Countdown from '$lib/components/Countdown.svelte';
|
import Countdown from "$lib/components/Countdown.svelte";
|
||||||
import { createWeddingInfo } from '$lib/wedding-info.svelte';
|
import { createWeddingInfo } from "$lib/wedding-info.svelte";
|
||||||
|
|
||||||
let { children } = $props();
|
let { children } = $props();
|
||||||
|
|
||||||
const wedding = createWeddingInfo();
|
const wedding = createWeddingInfo();
|
||||||
|
|
||||||
const links = [
|
const links = [
|
||||||
{ href: '/', label: 'Home' },
|
{ href: "/", label: "Home" },
|
||||||
{ href: '/rsvp', label: 'RSVP' },
|
{ href: "/rsvp", label: "RSVP" },
|
||||||
{ href: '/photos', label: 'Photos' }
|
{ href: "/photos", label: "Photos" },
|
||||||
];
|
];
|
||||||
|
|
||||||
|
let menuOpen = $state(false);
|
||||||
|
|
||||||
|
$effect(() => {
|
||||||
|
page.url.pathname;
|
||||||
|
menuOpen = false;
|
||||||
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<svelte:head><link rel="icon" href={favicon} /></svelte:head>
|
<svelte:head><link rel="icon" href={favicon} /></svelte:head>
|
||||||
|
|
||||||
<div class="bg-surface-50 dark:bg-surface-950 flex min-h-screen flex-col">
|
<div
|
||||||
|
class="bg-surface-50 dark:bg-surface-950 flex min-h-screen flex-col bg-[url('/patterns/damask.svg')] bg-repeat [background-size:160px_160px]"
|
||||||
|
>
|
||||||
<div class="sticky top-0 z-20 flex flex-col">
|
<div class="sticky top-0 z-20 flex flex-col">
|
||||||
<nav class="bg-primary-950 flex items-center justify-between px-4 py-3 sm:px-8">
|
<nav
|
||||||
<a href="/" class="text-surface-50 font-malibu text-xl tracking-wide">Zoé & Shaldon</a>
|
class="bg-primary-950 flex items-center justify-between px-4 py-3 sm:px-8"
|
||||||
<div class="flex gap-2">
|
>
|
||||||
|
<a
|
||||||
|
href="/"
|
||||||
|
class="text-surface-50 font-malibu hidden text-3xl tracking-wide sm:inline"
|
||||||
|
>Zoé & Shaldon</a
|
||||||
|
>
|
||||||
|
<Countdown
|
||||||
|
target={wedding.date}
|
||||||
|
compact
|
||||||
|
pastMessage="Already married! 🎉"
|
||||||
|
/>
|
||||||
|
<div class="hidden gap-2 sm:flex">
|
||||||
{#each links as link (link.href)}
|
{#each links as link (link.href)}
|
||||||
<a
|
<a
|
||||||
href={link.href}
|
href={link.href}
|
||||||
@@ -34,10 +54,38 @@
|
|||||||
</a>
|
</a>
|
||||||
{/each}
|
{/each}
|
||||||
</div>
|
</div>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="text-surface-50 flex items-center justify-center p-2 sm:hidden"
|
||||||
|
aria-label="Menu"
|
||||||
|
aria-expanded={menuOpen}
|
||||||
|
onclick={() => (menuOpen = !menuOpen)}
|
||||||
|
>
|
||||||
|
{#if menuOpen}
|
||||||
|
<svg viewBox="0 0 24 24" class="size-6" fill="none" stroke="currentColor" stroke-width="2">
|
||||||
|
<path stroke-linecap="round" d="M6 6l12 12M18 6L6 18" />
|
||||||
|
</svg>
|
||||||
|
{:else}
|
||||||
|
<svg viewBox="0 0 24 24" class="size-6" fill="none" stroke="currentColor" stroke-width="2">
|
||||||
|
<path stroke-linecap="round" d="M4 7h16M4 12h16M4 17h16" />
|
||||||
|
</svg>
|
||||||
|
{/if}
|
||||||
|
</button>
|
||||||
</nav>
|
</nav>
|
||||||
<div class="text-surface-50 flex justify-center px-4 py-3 drop-shadow-[0_2px_6px_rgba(0,0,0,0.6)]">
|
{#if menuOpen}
|
||||||
<Countdown target={wedding.date} compact pastMessage="Already married! 🎉" />
|
<div class="bg-primary-950 border-primary-800 flex flex-col gap-1 border-t px-4 py-3 sm:hidden">
|
||||||
|
{#each links as link (link.href)}
|
||||||
|
<a
|
||||||
|
href={link.href}
|
||||||
|
class="btn field-sm w-full {page.url.pathname === link.href
|
||||||
|
? 'preset-filled-secondary-500'
|
||||||
|
: 'text-surface-100 hover:text-secondary-400'}"
|
||||||
|
>
|
||||||
|
{link.label}
|
||||||
|
</a>
|
||||||
|
{/each}
|
||||||
</div>
|
</div>
|
||||||
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
<main class="flex flex-1 flex-col">
|
<main class="flex flex-1 flex-col">
|
||||||
{@render children()}
|
{@render children()}
|
||||||
|
|||||||
@@ -8,7 +8,7 @@
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<section
|
<section
|
||||||
class="relative flex flex-1 items-center justify-center overflow-hidden px-4 py-16 text-center"
|
class="relative flex flex-1 items-center justify-center overflow-hidden px-4 py-16 text-center z-10"
|
||||||
>
|
>
|
||||||
<PhotoBackground />
|
<PhotoBackground />
|
||||||
|
|
||||||
|
|||||||
@@ -65,7 +65,7 @@
|
|||||||
{#if !isOpen}
|
{#if !isOpen}
|
||||||
<div class="flex flex-col items-center gap-6 text-center">
|
<div class="flex flex-col items-center gap-6 text-center">
|
||||||
<h1 class="font-serif text-4xl">Photo uploads open on the wedding day</h1>
|
<h1 class="font-serif text-4xl">Photo uploads open on the wedding day</h1>
|
||||||
<p class="text-surface-600-400 text-lg">Come back on {formatFullDate(wedding.date)} to share your photos with Zoé & Shaldon.</p>
|
<p class="text-surface-600-400 text-2xl">Come back on {formatFullDate(wedding.date)} to share your photos with Zoé & Shaldon.</p>
|
||||||
</div>
|
</div>
|
||||||
{:else if status === 'done'}
|
{:else if status === 'done'}
|
||||||
<div class="preset-filled-primary-500 rounded-base max-w-md p-4 text-center">
|
<div class="preset-filled-primary-500 rounded-base max-w-md p-4 text-center">
|
||||||
@@ -74,36 +74,36 @@
|
|||||||
{:else}
|
{:else}
|
||||||
<div class="w-full max-w-md">
|
<div class="w-full max-w-md">
|
||||||
<h1 class="font-serif text-4xl">Share your photos</h1>
|
<h1 class="font-serif text-4xl">Share your photos</h1>
|
||||||
<p class="text-surface-600-400 mt-2 mb-8 text-lg">
|
<p class="text-surface-600-400 mt-2 mb-8 text-2xl">
|
||||||
Upload photos from the day — up to {MAX_FILES} at a time, {MAX_SIZE_MB}MB each.
|
Upload photos from the day — up to {MAX_FILES} at a time, {MAX_SIZE_MB}MB each.
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<form class="flex flex-col gap-4" onsubmit={onSubmit}>
|
<form class="flex flex-col gap-4" onsubmit={onSubmit}>
|
||||||
<label class="label">
|
<label class="label">
|
||||||
<span class="label-text">Name *</span>
|
<span class="label-text text-2xl">Name *</span>
|
||||||
<input class="input border border-surface-300 focus:border-primary-500 dark:border-surface-700" type="text" required bind:value={name} disabled={status === 'submitting'} />
|
<input class="input border border-surface-300 focus:border-primary-500 dark:border-surface-700 text-2xl" type="text" required bind:value={name} disabled={status === 'submitting'} />
|
||||||
</label>
|
</label>
|
||||||
|
|
||||||
<label class="label">
|
<label class="label">
|
||||||
<span class="label-text">Message to the couple (optional)</span>
|
<span class="label-text text-2xl">Message to the couple (optional)</span>
|
||||||
<textarea class="textarea border border-surface-300 focus:border-primary-500 dark:border-surface-700" rows="3" bind:value={message} disabled={status === 'submitting'}
|
<textarea class="textarea border border-surface-300 focus:border-primary-500 dark:border-surface-700 text-2xl" rows="3" bind:value={message} disabled={status === 'submitting'}
|
||||||
></textarea>
|
></textarea>
|
||||||
</label>
|
</label>
|
||||||
|
|
||||||
<label class="label">
|
<label class="label">
|
||||||
<span class="label-text">Email (optional)</span>
|
<span class="label-text text-2xl">Email (optional)</span>
|
||||||
<input class="input border border-surface-300 focus:border-primary-500 dark:border-surface-700" type="email" bind:value={email} disabled={status === 'submitting'} />
|
<input class="input border border-surface-300 focus:border-primary-500 dark:border-surface-700 text-2xl" type="email" bind:value={email} disabled={status === 'submitting'} />
|
||||||
</label>
|
</label>
|
||||||
|
|
||||||
<label class="label">
|
<label class="label">
|
||||||
<span class="label-text">Phone (optional)</span>
|
<span class="label-text text-2xl">Phone (optional)</span>
|
||||||
<input class="input border border-surface-300 focus:border-primary-500 dark:border-surface-700" type="tel" bind:value={phone} disabled={status === 'submitting'} />
|
<input class="input border border-surface-300 focus:border-primary-500 dark:border-surface-700 text-2xl" type="tel" bind:value={phone} disabled={status === 'submitting'} />
|
||||||
</label>
|
</label>
|
||||||
|
|
||||||
<label class="label">
|
<label class="label">
|
||||||
<span class="label-text">Photos *</span>
|
<span class="label-text text-2xl">Photos *</span>
|
||||||
<input
|
<input
|
||||||
class="input border border-surface-300 focus:border-primary-500 dark:border-surface-700"
|
class="input border border-surface-300 focus:border-primary-500 dark:border-surface-700 text-2xl"
|
||||||
type="file"
|
type="file"
|
||||||
accept="image/*"
|
accept="image/*"
|
||||||
multiple
|
multiple
|
||||||
@@ -114,10 +114,10 @@
|
|||||||
</label>
|
</label>
|
||||||
|
|
||||||
{#if status === 'error'}
|
{#if status === 'error'}
|
||||||
<p class="text-error-500 text-base">{errorMessage}</p>
|
<p class="text-error-500 text-2xl">{errorMessage}</p>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
<button class="btn preset-filled-secondary-500 mt-2" type="submit" disabled={status === 'submitting'}>
|
<button class="btn preset-filled-secondary-500 mt-2 text-2xl" type="submit" disabled={status === 'submitting'}>
|
||||||
{status === 'submitting' ? 'Uploading…' : 'Upload photos'}
|
{status === 'submitting' ? 'Uploading…' : 'Upload photos'}
|
||||||
</button>
|
</button>
|
||||||
</form>
|
</form>
|
||||||
|
|||||||
@@ -7,7 +7,7 @@
|
|||||||
|
|
||||||
<section class="flex flex-1 items-center justify-center px-4 py-16">
|
<section class="flex flex-1 items-center justify-center px-4 py-16">
|
||||||
<div class="border-surface-300 dark:border-surface-700 flex flex-col items-center gap-4 border px-6 py-10 text-center">
|
<div class="border-surface-300 dark:border-surface-700 flex flex-col items-center gap-4 border px-6 py-10 text-center">
|
||||||
<p class="text-primary-600-400 text-base tracking-[0.25em] uppercase">Scan to visit</p>
|
<p class="text-primary-600-400 text-2xl tracking-[0.25em] uppercase">Scan to visit</p>
|
||||||
<h1 class="font-malibu text-4xl">Zoé & Shaldon</h1>
|
<h1 class="font-malibu text-4xl">Zoé & Shaldon</h1>
|
||||||
|
|
||||||
<QrCode value={wedding.siteUrl} class="flex flex-col items-center gap-4">
|
<QrCode value={wedding.siteUrl} class="flex flex-col items-center gap-4">
|
||||||
@@ -15,7 +15,7 @@
|
|||||||
<QrCode.Pattern />
|
<QrCode.Pattern />
|
||||||
</QrCode.Frame>
|
</QrCode.Frame>
|
||||||
|
|
||||||
<p class="text-surface-700 dark:text-surface-300 text-base break-all">{wedding.siteUrl}</p>
|
<p class="text-surface-700 dark:text-surface-300 text-2xl break-all">{wedding.siteUrl}</p>
|
||||||
|
|
||||||
<QrCode.DownloadTrigger mimeType="image/png" fileName="zoe-and-shaldon-qr.png">
|
<QrCode.DownloadTrigger mimeType="image/png" fileName="zoe-and-shaldon-qr.png">
|
||||||
Download PNG
|
Download PNG
|
||||||
|
|||||||
@@ -60,7 +60,7 @@
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<section
|
<section
|
||||||
class="relative flex flex-1 items-center justify-center overflow-hidden px-4 py-16"
|
class="relative flex flex-1 items-center justify-center overflow-hidden px-4 py-16 z-10"
|
||||||
>
|
>
|
||||||
<PhotoBackground />
|
<PhotoBackground />
|
||||||
|
|
||||||
@@ -159,7 +159,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<label class="label">
|
<label class="label">
|
||||||
<span class="label-text text-surface-100"
|
<span class="label-text text-surface-100 text-2xl"
|
||||||
>Message to the couple (optional)</span
|
>Message to the couple (optional)</span
|
||||||
>
|
>
|
||||||
<textarea
|
<textarea
|
||||||
|
|||||||
@@ -0,0 +1,16 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="160" height="160" viewBox="0 0 160 160">
|
||||||
|
<g fill="none" stroke="#D9A441" stroke-width="1" stroke-opacity="0.35">
|
||||||
|
<path d="M80,0 L160,80 L80,160 L0,80 Z" />
|
||||||
|
</g>
|
||||||
|
<g fill="#D9A441">
|
||||||
|
<circle cx="80" cy="80" r="3" fill-opacity="0.28" />
|
||||||
|
<circle cx="80" cy="71" r="7" fill-opacity="0.14" />
|
||||||
|
<circle cx="80" cy="89" r="7" fill-opacity="0.14" />
|
||||||
|
<circle cx="71" cy="80" r="7" fill-opacity="0.14" />
|
||||||
|
<circle cx="89" cy="80" r="7" fill-opacity="0.14" />
|
||||||
|
<circle cx="80" cy="0" r="2" fill-opacity="0.3" />
|
||||||
|
<circle cx="160" cy="80" r="2" fill-opacity="0.3" />
|
||||||
|
<circle cx="80" cy="160" r="2" fill-opacity="0.3" />
|
||||||
|
<circle cx="0" cy="80" r="2" fill-opacity="0.3" />
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 736 B |
Reference in New Issue
Block a user