Initial commit

This commit is contained in:
2026-02-06 15:28:27 -03:00
commit 4bc96abf4a
116 changed files with 13806 additions and 0 deletions

View File

@@ -0,0 +1,71 @@
<script lang="ts">
import type { ProcessedLink } from '$lib/data/types';
import LinkIcon from './LinkIcon.svelte';
import Panel from './Panel.svelte';
import IconContact from './icons/IconContact.svelte';
let {
open = false,
links = [],
onclose = () => {},
}: {
open: boolean;
links?: ProcessedLink[];
onclose: () => void;
} = $props();
const componentId = $props.id();
</script>
<Panel {open} icon={IconContact} title="Contact" {onclose}>
{#snippet children()}
<ul class="contact-panel-list">
{#each links as link (link.label)}
{@const descriptionId = `${componentId}-description-${link.label}`}
<li>
{#if link.description}
<div id={descriptionId} class="description">{link.description}</div>
{/if}
<a
href={link.href}
class="panel-btn"
onclick={onclose}
target="_blank"
rel="noopener noreferrer"
aria-describedby={link.description ? descriptionId : undefined}
>
{#if link.icon}
<LinkIcon href={link.href} icon={link.icon} label={link.label} />
{/if}
{link.label}
</a>
</li>
{/each}
</ul>
{/snippet}
</Panel>
<style>
.contact-panel-list {
list-style: none;
margin: 0;
padding: 0;
}
.contact-panel-list li {
margin-bottom: 0.5rem;
&:last-child {
margin-bottom: 0;
}
}
.description {
color: var(--color-secondary-muted);
display: block;
font-size: 0.875rem;
margin-bottom: 0.5rem;
text-align: center;
}
</style>

View File

@@ -0,0 +1,18 @@
<footer class="footer">
<p>© {new Date().getFullYear()} Mike Fitzpatrick. All rights reserved.</p>
</footer>
<style>
.footer {
border-top: 1px solid var(--color-border-subtle);
color: var(--color-secondary-muted);
font-size: 0.875rem;
margin-top: 2.5rem;
padding: 1.5rem 0;
text-align: center;
& p {
margin: 0;
}
}
</style>

View File

@@ -0,0 +1,293 @@
<script lang="ts">
import { HeroLayout } from '$lib/data/constants';
import type { Site } from '$lib/data/types';
import IconContact from './icons/IconContact.svelte';
import IconShare from './icons/IconShare.svelte';
let {
contactOpen = $bindable(),
heroLayout,
location,
person,
profileImage,
pronouns,
pronunciation,
shareOpen = $bindable(),
showContactButton,
} = $props<Site & { contactOpen: boolean; shareOpen: boolean; showContactButton: boolean }>();
</script>
<div class="hero-backdrop" aria-label="Header background">
{#if profileImage}
<div
class="hero-bg"
style="background-image: url('/assets/images/{profileImage}.webp')"
aria-hidden="true"
></div>
{/if}
<div class="hero-actions">
{#if showContactButton}
<button
type="button"
class="action"
aria-label="Contact"
onclick={() => {
contactOpen = true;
shareOpen = false;
}}
>
<IconContact size={20} />
</button>
{/if}
<button
type="button"
class="action"
aria-label="Share"
onclick={() => {
shareOpen = true;
contactOpen = false;
}}
>
<IconShare size={20} />
</button>
</div>
</div>
<div class="card hero-header-wrapper">
<header class="hero-header" data-layout={heroLayout ?? 'side-by-side'}>
{#if profileImage}
<div class="hero-avatar">
<img
src="/assets/images/{profileImage}.webp"
alt="{person?.name ?? 'mifi'} profile"
width="160"
height="160"
fetchpriority="high"
/>
</div>
{/if}
<div class="hero-content">
<h1 class="wordmark">mifi</h1>
<h2 class="wordmark-explainer">
<strong>Mi</strong>ke <strong>Fi</strong>tzpatrick
</h2>
{#if pronunciation || pronouns || location}
<p class="hero-meta">
{#if pronunciation}<span>{pronunciation}</span>{/if}
{#if pronunciation && pronouns}
<span class="hero-meta-sep" aria-hidden="true"> · </span>{/if}
{#if pronouns}<span>{pronouns}</span>{/if}
{#if pronouns && location}
{#if (heroLayout ?? HeroLayout.SIDE_BY_SIDE) === HeroLayout.SIDE_BY_SIDE}
<br />
{:else}
<span class="hero-meta-sep" aria-hidden="true"> · </span>
{/if}
{/if}
{#if location}<span>{location}</span>{/if}
</p>
{/if}
</div>
<div class="button-group">
{#if showContactButton}
<button
type="button"
class="button"
aria-label="Contact"
onclick={() => {
contactOpen = true;
shareOpen = false;
}}
>
<IconContact size={20} />
<span>Contact</span>
</button>
{/if}
<button
type="button"
class="button"
aria-label="Share"
onclick={() => {
shareOpen = true;
contactOpen = false;
}}
>
<IconShare size={20} />
<span>Share</span>
</button>
</div>
</header>
</div>
<style>
.hero-backdrop {
position: relative;
height: 24vh;
min-height: 12rem;
overflow: hidden;
margin-bottom: 0;
}
.hero-bg {
position: absolute;
inset: 0;
background-size: 220%;
background-position: center;
background-repeat: no-repeat;
filter: blur(40px);
transform: scale(1.1);
z-index: 0;
&::after {
content: '';
position: absolute;
inset: 0;
background: linear-gradient(
to bottom,
var(--color-bg) 0%,
transparent 50%,
var(--color-bg) 100%
);
opacity: 0.85;
}
}
.hero-actions {
position: absolute;
top: 1rem;
right: 1rem;
display: flex;
gap: 0.5rem;
z-index: 2;
}
.hero-header-wrapper {
border-radius: 1rem;
box-shadow: 0 4px 1.5rem rgba(0, 0, 0, 0.08);
margin: -6rem auto 0;
max-width: 50ch;
overflow: visible;
padding: 1rem 1.5rem 1.5rem;
position: relative;
z-index: 1;
}
/* Hero content (avatar + wordmark) inside card */
.hero-header {
position: relative;
display: flex;
flex-direction: column;
align-items: center;
gap: 1.25rem;
text-align: center;
&[data-layout='side-by-side'] {
display: grid;
grid-template-areas:
'avatar content'
'buttons buttons';
grid-template-columns: auto 1fr;
grid-template-rows: auto auto;
gap: 1.5rem;
text-align: left;
}
}
/* Avatar: extends outside card top (half in card, half above) */
.hero-avatar {
background: var(--color-surface-elevated);
border: 3px solid var(--color-surface);
border-radius: 50%;
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.12);
grid-area: avatar;
flex-shrink: 0;
height: 8rem;
overflow: hidden;
transform: translateY(-50%);
width: 8rem;
& img {
width: 100%;
height: 100%;
object-fit: cover;
object-position: center;
display: block;
}
}
.hero-content {
grid-area: content;
min-width: 0;
}
.wordmark {
font-family: var(--font-wordmark, var(--font-sans));
font-weight: 700;
font-size: clamp(2.5rem, 5vw, 3.5rem);
letter-spacing: unset;
line-height: 1.1;
margin: 0 0 0.25rem;
font-variant-ligatures: common-ligatures discretionary-ligatures;
}
.wordmark-explainer {
font-family: var(--font-wordmark, var(--font-sans));
font-weight: 400;
font-size: 1rem;
color: var(--color-primary-muted);
margin: 0 0 0.5rem;
& strong {
font-weight: 700;
}
}
.hero-meta {
font-family: var(--font-body, var(--font-sans));
font-size: 0.875rem;
color: var(--color-secondary-muted);
margin: 0;
}
.hero-meta-sep {
white-space: pre;
}
.button-group {
grid-area: buttons;
display: flex;
gap: 0.5rem;
justify-content: center;
width: 100%;
}
.button {
align-items: center;
background: var(--color-surface-elevated);
border: 1px solid var(--color-secondary-muted);
border-radius: 0.25rem;
color: var(--color-fg);
cursor: pointer;
display: flex;
flex: 1 1 auto;
font-size: 1rem;
gap: 0.5rem;
justify-content: center;
max-width: 50%;
padding: 0.5rem 1rem;
text-align: center;
text-decoration: none;
transition: background 0.2s ease-in-out;
&:hover,
&:focus-visible {
background: var(--color-secondary-muted);
color: var(--color-surface-elevated);
}
&:focus-visible {
outline: 2px solid var(--color-focus-ring);
outline-offset: 2px;
}
}
</style>

View File

@@ -0,0 +1,80 @@
<script lang="ts">
import type { ProcessedLink } from '$lib/data/types';
import LinkIcon from './LinkIcon.svelte';
let { href, icon, label, description }: ProcessedLink = $props();
</script>
<a {href} rel="noopener noreferrer" target="_blank" class="link">
<span class="icon" aria-hidden="true">
<LinkIcon {href} {icon} {label} />
</span>
<div class="link-row-content">
<span class="title">{label}</span>
{#if description}
<span class="description">{description}</span>
{/if}
</div>
</a>
<style>
.link {
display: flex;
align-items: center;
gap: 1rem;
text-decoration: none;
&:hover {
& .title {
color: var(--color-link-hover);
text-decoration: underline;
}
& .description {
color: var(--color-secondary);
text-decoration: none;
}
& .icon {
color: var(--color-link-hover);
}
}
&:focus-visible {
outline: 2px solid var(--color-focus-ring);
}
}
.title,
.description,
.icon {
transition:
color 0.2s ease-in-out,
text-decoration 0.2s ease-in-out;
}
.icon {
color: var(--color-link);
flex-shrink: 0;
height: 1.5rem;
width: 1.5rem;
}
.link-row-content {
flex: 1;
min-width: 0;
}
.title {
color: var(--color-link);
text-decoration: none;
}
.description {
color: var(--color-secondary-muted);
display: block;
font-size: 0.875rem;
margin-top: 0.15rem;
text-decoration: none;
}
</style>

View File

@@ -0,0 +1,58 @@
<script lang="ts">
import type { ProcessedLink, Section } from '$lib/data/types';
import Link from './Link.svelte';
let {
id,
links,
order,
showHeading = false,
title,
} = $props<
Omit<Section, 'links' | 'order'> & {
links: ProcessedLink[];
order: number;
showHeading?: boolean;
}
>();
</script>
<section class="card" aria-labelledby={`section-${id}`} style="order:{order}">
{#if showHeading}<h3 id={`section-${id}`} class="link-section-heading heading">
{title}
</h3>{/if}
<ul class="link-list">
{#each links as link (link.label)}
<li class="link-row">
<Link {...link} />
</li>
{/each}
</ul>
</section>
<style>
.link-section-heading {
color: var(--color-primary);
font-family: var(--font-heading, var(--font-sans));
font-size: 1.125rem;
font-weight: 500;
font-variation-settings: 'opsz' var(--font-heading-opsz, 36);
letter-spacing: 0.045em;
margin: 0 0 1rem;
}
.link-list {
list-style: none;
margin: 0;
padding: 0;
}
.link-row {
margin-bottom: 0.75rem;
&:last-child {
margin-bottom: 0;
}
}
</style>

View File

@@ -0,0 +1,60 @@
<script lang="ts" module>
export type IconName =
| 'GitHub'
| 'LinkedIn'
| 'Instagram'
| 'Facebook'
| 'YouTube'
| 'TikTok'
| 'Snapchat'
| 'Discord'
| 'Strava'
| 'Flickr'
| 'Calendar'
| 'Resume'
| 'Mifi';
</script>
<script lang="ts">
import IconLink from './icons/IconLink.svelte';
import IconGitHub from './icons/IconGitHub.svelte';
import IconLinkedIn from './icons/IconLinkedIn.svelte';
import IconInstagram from './icons/IconInstagram.svelte';
import IconFacebook from './icons/IconFacebook.svelte';
import IconYouTube from './icons/IconYouTube.svelte';
import IconTikTok from './icons/IconTikTok.svelte';
import IconSnapchat from './icons/IconSnapchat.svelte';
import IconDiscord from './icons/IconDiscord.svelte';
import IconStrava from './icons/IconStrava.svelte';
import IconFlickr from './icons/IconFlickr.svelte';
import IconCal from './icons/IconCal.svelte';
import IconResume from './icons/IconResume.svelte';
import IconMi from './icons/IconMi.svelte';
import type { Link } from '$lib/data/types';
let {
icon,
label,
size = 24,
} = $props<Pick<Link, 'href' | 'icon' | 'label'> & { size?: number }>();
const iconMap: Record<IconName, typeof IconLink> = {
GitHub: IconGitHub,
LinkedIn: IconLinkedIn,
Instagram: IconInstagram,
Facebook: IconFacebook,
YouTube: IconYouTube,
TikTok: IconTikTok,
Snapchat: IconSnapchat,
Discord: IconDiscord,
Strava: IconStrava,
Flickr: IconFlickr,
Calendar: IconCal,
Resume: IconResume,
Mifi: IconMi,
};
const IconComponent = $derived(iconMap[(icon || label) as IconName] ?? IconLink);
</script>
<IconComponent {size} />

View File

@@ -0,0 +1,169 @@
<script lang="ts">
import type { Component } from 'svelte';
import { tick } from 'svelte';
let {
children,
icon: IconComponent,
onclose = () => {},
open = false,
title = '',
}: {
children: import('svelte').Snippet;
icon?: Component<{ size?: number }>;
onclose: () => void;
open: boolean;
title: string;
} = $props();
let dialogEl: HTMLDialogElement | undefined;
let closeBtnEl: HTMLButtonElement | undefined;
function handleDialogClose() {
onclose();
}
$effect(() => {
if (!dialogEl) return;
if (open) {
dialogEl.showModal();
void tick().then(() => closeBtnEl?.focus());
} else {
dialogEl.close();
}
});
</script>
<dialog
bind:this={dialogEl}
class="panel-dialog"
closedby="any"
aria-labelledby="panel-title"
aria-modal="true"
onclose={handleDialogClose}
oncancel={(e) => {
e.preventDefault();
onclose();
}}
>
<div class="panel">
<header class="header">
<h2 id="panel-title" class="title heading">
{#if IconComponent}
<IconComponent size={24} />
{/if}
{title}
</h2>
<button
type="button"
class="close-button"
aria-label="Close"
bind:this={closeBtnEl}
onclick={onclose}
>
<span aria-hidden="true">×</span>
</button>
</header>
<div class="body">
{@render children?.()}
</div>
</div>
</dialog>
<style>
.panel-dialog {
background-color: transparent;
margin: 0;
padding: 0;
border: none;
max-height: 80vh;
max-width: 100%;
width: 100%;
inset: auto;
position: fixed;
bottom: 0;
left: 50%;
transform: translateX(-50%);
&::backdrop {
background: rgba(0, 0, 0, 0.4);
}
@media (min-width: 769px) {
bottom: auto;
max-width: min(28rem, calc(100vw - 2rem));
top: 50%;
transform: translate(-50%, -50%);
}
}
.panel {
background: var(--color-surface);
border: 1px solid var(--color-border);
border-radius: 1rem 1rem 0 0;
box-shadow: 0 -4px 1rem rgb(0 0 0 / 15%);
flex-direction: column;
display: flex;
max-height: 70vh;
padding-bottom: 2rem;
overflow: hidden;
width: 100%;
@media (min-width: 769px) {
max-height: 80vh;
border-radius: 1rem;
}
}
.header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 1rem 1.25rem;
border-bottom: 1px solid var(--color-border-subtle);
flex-shrink: 0;
}
.title {
align-items: center;
color: var(--color-fg);
display: flex;
flex: 1 1 auto;
font-family: var(--font-heading, var(--font-sans));
font-weight: 600;
font-size: 1.125rem;
gap: 0.5rem;
justify-content: center;
margin: 0;
}
.close-button {
flex: 0 0 auto;
font: inherit;
font-size: 1.5rem;
line-height: 1;
padding: 0.25rem;
background: none;
border: none;
color: var(--color-fg);
cursor: pointer;
border-radius: 0.25rem;
&:hover,
&:focus-visible {
background: var(--color-border-subtle);
}
&:focus-visible {
outline: 2px solid var(--color-focus-ring);
outline-offset: 2px;
}
}
.body {
padding: 1.25rem;
overflow-y: auto;
flex: 1;
min-height: 0;
}
</style>

View File

@@ -0,0 +1,100 @@
<script lang="ts">
import Panel from './Panel.svelte';
import IconCopy from './icons/IconCopy.svelte';
import IconEmail from './icons/IconEmail.svelte';
import IconShare from './icons/IconShare.svelte';
let {
open = false,
url = '',
qrCodeImage = '',
emailSubject = 'Link from mifi',
emailBody = '',
onclose = () => {},
}: {
open: boolean;
url: string;
qrCodeImage?: string | null;
emailSubject?: string;
emailBody?: string;
onclose: () => void;
} = $props();
let copied = $state(false);
function copyLink() {
if (typeof navigator === 'undefined' || !navigator.clipboard) return;
navigator.clipboard.writeText(url).then(() => {
copied = true;
setTimeout(() => (copied = false), 2000);
});
}
function share() {
if (typeof navigator === 'undefined' || !navigator.share) return;
navigator
.share({ title: 'mifi', url })
.then(() => onclose())
.catch(() => {});
}
const canShare = $derived(typeof navigator !== 'undefined' && !!navigator.share);
const mailtoHref = $derived(
`mailto:?subject=${encodeURIComponent(emailSubject)}&body=${encodeURIComponent(emailBody || url)}`,
);
</script>
<Panel {open} icon={IconShare} title="Share" {onclose}>
{#snippet children()}
<div class="share-panel">
{#if qrCodeImage}
<div class="share-qr">
<img
src="/assets/images/{qrCodeImage}.png"
alt="QR code for this page"
width="160"
height="160"
/>
</div>
{/if}
<button type="button" class="panel-btn" onclick={copyLink}>
<IconCopy size={20} />
{copied ? 'Copied!' : 'Copy link'}
</button>
<a
href={mailtoHref}
class="panel-btn"
onclick={onclose}
target="_blank"
rel="noopener noreferrer"
>
<IconEmail size={20} />
Email link
</a>
{#if canShare}
<button type="button" class="panel-btn" onclick={share}>
<IconShare size={20} />
Share…
</button>
{/if}
</div>
{/snippet}
</Panel>
<style>
.share-panel {
display: flex;
flex-direction: column;
gap: 0.75rem;
}
.share-qr {
display: flex;
justify-content: center;
margin-bottom: 0.5rem;
& img {
display: block;
}
}
</style>

View File

@@ -0,0 +1,157 @@
<script lang="ts">
import { onMount } from 'svelte';
import { Moon, SunMoon, Sun } from '@lucide/svelte';
import { getStoredTheme, setTheme } from '$lib/theme';
import type { ThemeMode } from '$lib/theme';
const SLOT_WIDTH = 48;
const GAP = 8;
/** When collapsed, track translateX so the active option is in the 48px viewport (left=Light, middle=Dark, right=Auto). */
const OFFSETS: Record<ThemeMode, number> = {
light: 0,
dark: -(SLOT_WIDTH + GAP),
auto: -(SLOT_WIDTH + GAP) * 2,
};
let current = $state<ThemeMode>('auto');
let expanded = $state(false);
const themeOffset = $derived(OFFSETS[current]);
onMount(() => {
current = getStoredTheme();
});
function choose(mode: ThemeMode) {
setTheme(mode);
current = mode;
}
function handleClick(mode: ThemeMode) {
if (expanded) {
choose(mode);
expanded = false;
} else {
expanded = true;
}
}
</script>
{#if expanded}
<!-- Click outside to collapse -->
<button
type="button"
class="theme-toggle-backdrop"
aria-label="Close theme menu"
tabindex="-1"
onclick={() => (expanded = false)}
></button>
{/if}
<div
class="theme-toggle"
class:expanded
role="group"
aria-label="Color theme"
style="--theme-offset: {themeOffset}px;"
>
<div class="theme-toggle-track">
<button
type="button"
class="action theme-option"
class:active={expanded && current === 'light'}
aria-label="Light"
aria-current={current === 'light' ? 'true' : undefined}
title="Light"
onclick={() => handleClick('light')}
>
<Sun size={24} />
</button>
<button
type="button"
class="action theme-option"
class:active={expanded && current === 'dark'}
aria-label="Dark"
aria-current={current === 'dark' ? 'true' : undefined}
title="Dark"
onclick={() => handleClick('dark')}
>
<Moon size={24} />
</button>
<button
type="button"
class="action theme-option"
class:active={expanded && current === 'auto'}
aria-label="Auto (system)"
aria-current={current === 'auto' ? 'true' : undefined}
title="Auto (system)"
onclick={() => handleClick('auto')}
>
<SunMoon size={24} />
</button>
</div>
</div>
<style>
.theme-toggle-backdrop {
position: fixed;
inset: 0;
z-index: 99;
padding: 0;
border: none;
background: transparent;
cursor: default;
}
/* Collapsed: match hero-action (other header buttons); expanded: glass panel */
.theme-toggle {
position: relative;
z-index: 100;
display: inline-block;
width: 48px;
padding: 0;
overflow: hidden;
border-radius: 50%;
border: none;
transition:
width 0.22s ease-out,
padding 0.22s ease-out,
border-radius 0.22s ease-out,
background 0.22s ease-out,
box-shadow 0.22s ease-out,
backdrop-filter 0.22s ease-out;
background: var(--color-surface-elevated);
box-shadow: 0 2px 8px rgb(0 0 0 / 15%);
}
.theme-toggle.expanded {
width: 176px;
padding: 0.5rem;
border-radius: 1rem;
border: 1px solid color-mix(in srgb, var(--color-border) 60%, transparent);
background: color-mix(in srgb, var(--color-surface-elevated) 52%, transparent);
backdrop-filter: blur(20px) saturate(1.2);
-webkit-backdrop-filter: blur(20px) saturate(1.2);
box-shadow:
0 0 0 1px color-mix(in srgb, white 12%, transparent) inset,
0 4px 24px rgba(0, 0, 0, 0.12),
0 2px 8px rgba(0, 0, 0, 0.08);
}
.theme-toggle-track {
display: flex;
gap: 0.5rem;
width: 160px;
transform: translateX(var(--theme-offset));
transition: transform 0.22s ease-out;
}
.theme-toggle.expanded .theme-toggle-track {
transform: translateX(0);
}
.theme-option.active {
background: var(--color-primary);
color: var(--color-bg);
}
</style>

View File

@@ -0,0 +1,22 @@
<script lang="ts">
/** Calendar / booking icon. */
let { size = 24 }: { size?: number } = $props();
</script>
<svg
xmlns="http://www.w3.org/2000/svg"
width={size}
height={size}
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
aria-hidden="true"
>
<rect x="3" y="4" width="18" height="18" rx="2" ry="2" />
<line x1="16" y1="2" x2="16" y2="6" />
<line x1="8" y1="2" x2="8" y2="6" />
<line x1="3" y1="10" x2="21" y2="10" />
</svg>

View File

@@ -0,0 +1,18 @@
<script lang="ts">
let { size = 24 }: { size?: number } = $props();
</script>
<svg
xmlns="http://www.w3.org/2000/svg"
width={size}
height={size}
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
aria-hidden="true"
>
<path
d="M2.992 16.342a2 2 0 0 1 .094 1.167l-1.065 3.29a1 1 0 0 0 1.236 1.168l3.413-.998a2 2 0 0 1 1.099.092 10 10 0 1 0-4.777-4.719"
/>
</svg>

View File

@@ -0,0 +1,19 @@
<script lang="ts">
let { size = 24 } = $props<{ size?: number }>();
</script>
<svg
xmlns="http://www.w3.org/2000/svg"
width={size}
height={size}
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
aria-hidden="true"
>
<rect width="14" height="14" x="8" y="8" rx="2" ry="2" />
<path d="M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2" />
</svg>

View File

@@ -0,0 +1,16 @@
<script lang="ts">
let { size = 24 }: { size?: number } = $props();
</script>
<svg
xmlns="http://www.w3.org/2000/svg"
width={size}
height={size}
viewBox="0 0 24 24"
fill="currentColor"
aria-hidden="true"
>
<path
d="M20.317 4.37a19.791 19.791 0 0 0-4.885-1.515.074.074 0 0 0-.079.037c-.21.375-.444.864-.608 1.25a18.27 18.27 0 0 0-5.487 0 12.64 12.64 0 0 0-.617-1.25.077.077 0 0 0-.079-.037A19.736 19.736 0 0 0 3.677 4.37a.07.07 0 0 0-.032.027C.533 9.046-.32 13.58.099 18.057a.082.082 0 0 0 .031.057 19.9 19.9 0 0 0 5.993 3.03.078.078 0 0 0 .084-.028 14.09 14.09 0 0 0 1.226-1.994.076.076 0 0 0-.041-.106 13.107 13.107 0 0 1-1.872-.892.077.077 0 0 1-.008-.128 10.2 10.2 0 0 0 .372-.292.074.074 0 0 1 .077-.01c3.928 1.793 8.18 1.793 12.062 0a.074.074 0 0 1 .078.01c.12.098.246.198.373.292a.077.077 0 0 1-.006.127 12.299 12.299 0 0 1-1.873.892.077.077 0 0 0-.041.107c.36.698.772 1.362 1.225 1.993a.076.076 0 0 0 .084.028 19.839 19.839 0 0 0 6.002-3.03.077.077 0 0 0 .032-.054c.5-5.177-.838-9.674-3.549-13.66a.061.061 0 0 0-.031-.03zM8.02 15.33c-1.183 0-2.157-1.085-2.157-2.419 0-1.333.956-2.419 2.157-2.419 1.21 0 2.176 1.096 2.157 2.42 0 1.333-.956 2.418-2.157 2.418zm7.975 0c-1.183 0-2.157-1.085-2.157-2.419 0-1.333.955-2.419 2.157-2.419 1.21 0 2.176 1.096 2.157 2.42 0 1.333-.946 2.418-2.157 2.418z"
/>
</svg>

View File

@@ -0,0 +1,21 @@
<script lang="ts">
let { size = 24 } = $props<{ size?: number }>();
</script>
<svg
xmlns="http://www.w3.org/2000/svg"
width={size}
height={size}
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
aria-hidden="true"
>
<path
d="M14.536 21.686a.5.5 0 0 0 .937-.024l6.5-19a.496.496 0 0 0-.635-.635l-19 6.5a.5.5 0 0 0-.024.937l7.93 3.18a2 2 0 0 1 1.112 1.11z"
/>
<path d="m21.854 2.147-10.94 10.939" />
</svg>

View File

@@ -0,0 +1,16 @@
<script lang="ts">
let { size = 24 }: { size?: number } = $props();
</script>
<svg
xmlns="http://www.w3.org/2000/svg"
width={size}
height={size}
viewBox="0 0 24 24"
fill="currentColor"
aria-hidden="true"
>
<path
d="M24 12.073c0-6.627-5.373-12-12-12s-12 5.373-12 12c0 5.99 4.388 10.954 10.125 11.854v-8.385H7.078v-3.47h3.047V9.43c0-3.007 1.792-4.669 4.533-4.669 1.312 0 2.686.235 2.686.235v2.953H15.83c-1.491 0-1.956.925-1.956 1.874v2.25h3.328l-.532 3.47h-2.796v8.385C19.612 23.027 24 18.062 24 12.073z"
/>
</svg>

View File

@@ -0,0 +1,16 @@
<script lang="ts">
let { size = 24 }: { size?: number } = $props();
</script>
<svg
xmlns="http://www.w3.org/2000/svg"
width={size}
height={size}
viewBox="0 0 24 24"
fill="currentColor"
aria-hidden="true"
>
<path
d="M5.5 12c0-1.653 1.347-3 3-3s3 1.347 3 3-1.347 3-3 3-3-1.347-3-3zm10.5 0c0-1.653 1.347-3 3-3s3 1.347 3 3-1.347 3-3 3-3-1.347-3-3z"
/>
</svg>

View File

@@ -0,0 +1,16 @@
<script lang="ts">
let { size = 24 }: { size?: number } = $props();
</script>
<svg
xmlns="http://www.w3.org/2000/svg"
width={size}
height={size}
viewBox="0 0 24 24"
fill="currentColor"
aria-hidden="true"
>
<path
d="M12 0c-6.626 0-12 5.373-12 12 0 5.302 3.438 9.8 8.207 11.387.599.111.793-.261.793-.577v-2.234c-3.338.726-4.033-1.416-4.033-1.416-.546-1.387-1.333-1.756-1.333-1.756-1.089-.745.083-.729.083-.729 1.205.084 1.839 1.237 1.839 1.237 1.07 1.834 2.807 1.304 3.492.997.107-.775.418-1.305.762-1.604-2.665-.305-5.467-1.334-5.467-5.931 0-1.311.469-2.381 1.236-3.221-.124-.303-.535-1.524.117-3.176 0 0 1.008-.322 3.301 1.23.957-.266 1.983-.399 3.003-.404 1.02.005 2.047.138 3.006.404 2.291-1.552 3.297-1.23 3.297-1.23.653 1.653.242 2.874.118 3.176.77.84 1.235 1.911 1.235 3.221 0 4.609-2.807 5.624-5.479 5.921.43.372.823 1.102.823 2.222v3.293c0 .319.192.694.801.576 4.765-1.589 8.199-6.086 8.199-11.386 0-6.627-5.373-12-12-12z"
/>
</svg>

View File

@@ -0,0 +1,16 @@
<script lang="ts">
let { size = 24 }: { size?: number } = $props();
</script>
<svg
xmlns="http://www.w3.org/2000/svg"
width={size}
height={size}
viewBox="0 0 24 24"
fill="currentColor"
aria-hidden="true"
>
<path
d="M12 0C8.74 0 8.333.015 7.053.072 5.775.132 4.905.333 4.14.63c-.789.306-1.459.717-2.126 1.384S.935 3.35.63 4.14C.333 4.905.131 5.775.072 7.053.012 8.333 0 8.74 0 12s.015 3.667.072 4.947c.06 1.277.261 2.148.558 2.913.306.788.717 1.459 1.384 2.126.667.666 1.336 1.079 2.126 1.384.766.296 1.636.499 2.913.558C8.333 23.988 8.74 24 12 24s3.667-.015 4.947-.072c1.277-.06 2.148-.262 2.913-.558.788-.306 1.459-.718 2.126-1.384.666-.667 1.079-1.335 1.384-2.126.296-.765.499-1.636.558-2.913.06-1.28.072-1.687.072-4.947s-.015-3.667-.072-4.947c-.06-1.277-.262-2.149-.558-2.913-.306-.789-.718-1.459-1.384-2.126C21.319 1.347 20.651.935 19.86.63c-.765-.297-1.636-.499-2.913-.558C15.667.012 15.26 0 12 0zm0 2.16c3.203 0 3.585.016 4.85.071 1.17.055 1.805.249 2.227.415.562.217.96.477 1.382.896.419.42.679.819.896 1.381.164.422.36 1.057.413 2.227.057 1.266.07 1.646.07 4.85s-.015 3.585-.074 4.85c-.061 1.17-.256 1.805-.421 2.227-.224.562-.479.96-.899 1.382-.419.419-.824.679-1.38.896-.42.164-1.065.36-2.235.413-1.274.057-1.649.07-4.859.07-3.211 0-3.586-.015-4.859-.074-1.171-.061-1.816-.256-2.236-.421-.569-.224-.96-.479-1.379-.899-.421-.419-.69-.824-.9-1.38-.165-.42-.359-1.065-.42-2.235-.045-1.26-.061-1.649-.061-4.844 0-3.196.016-3.586.061-4.861.061-1.17.255-1.814.42-2.234.21-.57.479-.96.9-1.381.419-.419.81-.689 1.379-.898.42-.166 1.051-.361 2.221-.421 1.275-.045 1.65-.06 4.859-.06l.045.03zm0 5.838c-3.403 0-6.162 2.759-6.162 6.162S8.597 20.163 12 20.163s6.162-2.759 6.162-6.163c0-3.403-2.759-6.162-6.162-6.162zM12 16c-2.21 0-4-1.79-4-4s1.79-4 4-4 4 1.79 4 4-1.79 4-4 4zm7.846-10.405a1.441 1.441 0 0 1-2.88 0 1.44 1.44 0 0 1 2.88 0z"
/>
</svg>

View File

@@ -0,0 +1,20 @@
<script lang="ts">
/** Default/link icon. 24×24, currentColor. */
let { size = 24 }: { size?: number } = $props();
</script>
<svg
xmlns="http://www.w3.org/2000/svg"
width={size}
height={size}
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
aria-hidden="true"
>
<path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71" />
<path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71" />
</svg>

View File

@@ -0,0 +1,16 @@
<script lang="ts">
let { size = 24 }: { size?: number } = $props();
</script>
<svg
xmlns="http://www.w3.org/2000/svg"
width={size}
height={size}
viewBox="0 0 24 24"
fill="currentColor"
aria-hidden="true"
>
<path
d="M20.447 20.452h-3.554v-5.569c0-1.328-.027-3.037-1.852-3.037-1.853 0-2.136 1.445-2.136 2.939v5.667H9.351V9h3.414v1.561h.046c.477-.9 1.637-1.85 3.37-1.85 3.601 0 4.267 2.37 4.267 5.455v6.286zM5.337 7.433c-1.144 0-2.063-.926-2.063-2.065 0-1.138.92-2.063 2.063-2.063 1.14 0 2.064.925 2.064 2.063 0 1.139-.925 2.065-2.064 2.065zm1.782 13.019H3.555V9h3.564v11.452zM22.225 0H1.771C.792 0 0 .774 0 1.729v20.542C0 23.227.792 24 1.771 24h20.451C23.2 24 24 23.227 24 22.271V1.729C24 .774 23.2 0 22.222 0h.003z"
/>
</svg>

View File

@@ -0,0 +1,16 @@
<script lang="ts">
let { size = 24 }: { size?: number } = $props();
</script>
<svg
xmlns="http://www.w3.org/2000/svg"
width={size}
height={size}
viewBox="0 0 2134 2134"
fill="currentColor"
aria-hidden="true"
>
<path
d="M2133.333,400l0,1333.333c0,220.766 -179.234,400 -400,400l-1333.333,0c-220.766,0 -400,-179.234 -400,-400l0,-1333.333c0,-220.766 179.234,-400 400,-400l1333.333,0c220.766,0 400,179.234 400,400Zm-400.047,334.954l221.363,0l0,-236.121l-221.363,0l0,236.121Zm-806.91,230.385c-20.078,-34.566 -48.157,-64.067 -84.236,-88.502c-57.484,-38.932 -123.612,-58.398 -198.384,-58.398c-68.587,0 -128.391,16.304 -179.41,48.911c-32.843,20.991 -58.493,48.971 -76.95,83.94l0,-112.612l-208.714,0l0,916.655l221.363,0l0,-538.018c0,-40.478 7.379,-75.264 22.136,-104.357c14.758,-29.093 35.488,-51.722 62.193,-67.885c26.704,-16.163 57.765,-24.245 93.183,-24.245c36.261,0 67.463,8.082 93.605,24.245c26.142,16.163 46.521,38.721 61.138,67.674c14.617,28.953 21.926,63.809 21.926,104.568l0,538.018l221.363,0l0,-538.018c0,-40.478 7.379,-75.264 22.136,-104.357c14.758,-29.093 35.559,-51.722 62.403,-67.885c26.845,-16.163 57.836,-24.245 92.973,-24.245c36.261,0 67.463,8.082 93.605,24.245c26.142,16.163 46.521,38.721 61.138,67.674c14.617,28.953 21.926,63.809 21.926,104.568l0,538.018l221.363,0l0,-590.724c0,-68.306 -14.828,-128.461 -44.483,-180.464c-29.656,-52.003 -70.063,-92.621 -121.223,-121.855c-51.16,-29.234 -109.206,-43.851 -174.139,-43.851c-73.366,0 -138.089,18.06 -194.167,54.181c-35.933,23.145 -66.182,54.051 -90.747,92.718Zm806.91,789.994l221.363,0l0,-916.655l-221.363,0l0,916.655Z"
/>
</svg>

View File

@@ -0,0 +1,22 @@
<script lang="ts">
/** PDF document / resume icon. */
let { size = 24 }: { size?: number } = $props();
</script>
<svg
xmlns="http://www.w3.org/2000/svg"
width={size}
height={size}
viewBox="0 0 640 640"
fill="currentColor"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
aria-hidden="true"
>
<!--!Font Awesome Pro v7.1.0 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license (Commercial License) Copyright 2026 Fonticons, Inc.-->
<path
d="M240 112L128 112C119.2 112 112 119.2 112 128L112 512C112 520.8 119.2 528 128 528L208 528L208 576L128 576C92.7 576 64 547.3 64 512L64 128C64 92.7 92.7 64 128 64L261.5 64C278.5 64 294.8 70.7 306.8 82.7L429.3 205.3C441.3 217.3 448 233.6 448 250.6L448 400.1L400 400.1L400 272.1L312 272.1C272.2 272.1 240 239.9 240 200.1L240 112.1zM380.1 224L288 131.9L288 200C288 213.3 298.7 224 312 224L380.1 224zM272 444L304 444C337.1 444 364 470.9 364 504C364 537.1 337.1 564 304 564L292 564L292 592C292 603 283 612 272 612C261 612 252 603 252 592L252 464C252 453 261 444 272 444zM304 524C315 524 324 515 324 504C324 493 315 484 304 484L292 484L292 524L304 524zM400 444L432 444C460.7 444 484 467.3 484 496L484 560C484 588.7 460.7 612 432 612L400 612C389 612 380 603 380 592L380 464C380 453 389 444 400 444zM432 572C438.6 572 444 566.6 444 560L444 496C444 489.4 438.6 484 432 484L420 484L420 572L432 572zM508 464C508 453 517 444 528 444L576 444C587 444 596 453 596 464C596 475 587 484 576 484L548 484L548 508L576 508C587 508 596 517 596 528C596 539 587 548 576 548L548 548L548 592C548 603 539 612 528 612C517 612 508 603 508 592L508 464z"
/>
</svg>

View File

@@ -0,0 +1,20 @@
<script lang="ts">
let { size = 24 }: { size?: number } = $props();
</script>
<svg
xmlns="http://www.w3.org/2000/svg"
width={size}
height={size}
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
aria-hidden="true"
>
<circle cx="18" cy="5" r="3" />
<circle cx="6" cy="12" r="3" />
<circle cx="18" cy="19" r="3" />
<line x1="8.59" y1="13.51" x2="15.42" y2="17.49" />
<line x1="15.41" y1="6.51" x2="8.59" y2="10.49" />
</svg>

View File

@@ -0,0 +1,16 @@
<script lang="ts">
let { size = 24 }: { size?: number } = $props();
</script>
<svg
xmlns="http://www.w3.org/2000/svg"
width={size}
height={size}
viewBox="0 0 24 24"
fill="currentColor"
aria-hidden="true"
>
<path
d="M12.206.793c.99 0 4.347.276 5.93 3.821.529 1.193.403 3.219.299 4.847l-.003.06c-.012.18-.022.345-.03.51.075.045.203.09.401.09.3-.016.659-.12 1.033-.301.165-.088.344-.104.464-.104.182 0 .359.029.509.09.45.149.734.479.734.838.015.449-.39.839-1.213 1.168-.089.029-.209.075-.344.119-.45.135-1.139.36-1.333.81-.09.224-.061.524.12.868l.015.015c.06.136 1.526 3.475 4.791 4.014.255.044.435.27.42.509 0 .075-.015.149-.045.225-.24.569-1.273.988-3.146 1.271-.059.091-.12.375-.164.57-.029.179-.074.36-.134.553-.076.271-.27.405-.555.405h-.03c-.135 0-.313-.031-.538-.074-.36-.075-.765-.135-1.273-.135-.3 0-.599.015-.913.074-.6.104-1.123.464-1.723.884-.853.599-1.826 1.288-3.294 1.288-.06 0-.119-.015-.18-.015h-.149c-1.468 0-2.427-.675-3.279-1.288-.599-.42-1.107-.779-1.707-.884-.314-.045-.629-.074-.928-.074-.54 0-.958.089-1.272.149-.211.043-.391.074-.54.074-.374 0-.523-.224-.583-.42-.061-.192-.09-.389-.135-.567-.046-.181-.105-.494-.166-.57-1.918-.222-2.95-.642-3.189-1.226-.031-.063-.052-.15-.052-.225-.015-.239.165-.465.42-.509 3.264-.54 4.73-3.879 4.791-4.02l.016-.029c.18-.345.224-.645.119-.869-.195-.434-.884-.658-1.333-.809-.121-.029-.24-.074-.346-.119-1.107-.435-1.257-.93-1.197-1.273.089-.479.674-.793 1.168-.793.146 0 .27.029.383.074.42.194.789.3 1.104.3.234 0 .384-.06.465-.105l-.046-.569c-.098-1.626-.225-3.651.307-4.837C7.392 1.077 10.739.807 11.727.807l.419-.015h.06z"
/>
</svg>

View File

@@ -0,0 +1,16 @@
<script lang="ts">
let { size = 24 }: { size?: number } = $props();
</script>
<svg
xmlns="http://www.w3.org/2000/svg"
width={size}
height={size}
viewBox="0 0 24 24"
fill="currentColor"
aria-hidden="true"
>
<path
d="M15.387 17.944l-2.089-4.116h-3.065L15.387 24l5.15-10.172h-3.066m-7.008-5.113l2.836 5.116h3.172l-4.994-9.016c-.363-.636-1.134-.828-1.771-.828-.576 0-1.224.192-1.589.644L2.086 17.944h3.157l2.136-4.113zm4.008-5.113c-.748 0-1.271.312-1.271.312v3.177h2.453c.261 0 .523-.052.784-.156.523-.208.888-.572 1.044-1.036.156-.468.104-.988-.156-1.456-.416-.728-1.304-1.192-2.804-1.192z"
/>
</svg>

View File

@@ -0,0 +1,16 @@
<script lang="ts">
let { size = 24 }: { size?: number } = $props();
</script>
<svg
xmlns="http://www.w3.org/2000/svg"
width={size}
height={size}
viewBox="0 0 24 24"
fill="currentColor"
aria-hidden="true"
>
<path
d="M12.525.02c1.31-.02 2.61-.01 3.91-.02.08 1.53.63 3.09 1.75 4.17 1.12 1.11 2.7 1.62 4.24 1.79v4.03c-1.44-.05-2.89-.35-4.2-.97-.57-.26-1.1-.59-1.62-.93-.01 2.92.01 5.84-.02 8.75-.08 1.4-.54 2.79-1.35 3.94-1.31 1.92-3.58 3.17-5.91 3.21-1.43.08-2.86-.31-4.08-1.03-2.02-1.19-3.44-3.37-3.65-5.71-.02-.5-.03-1-.01-1.49.18-1.9 1.12-3.72 2.58-4.96 1.66-1.44 3.98-2.13 6.15-1.72.02 1.48-.04 2.96-.04 4.44-.99-.32-2.15-.23-3.02.37-.63.41-1.11 1.04-1.36 1.75-.21.51-.15 1.07-.14 1.61.24 1.64 1.82 3.02 3.5 2.87 1.12-.01 2.19-.66 2.77-1.61.19-.33.4-.67.41-1.06.1-1.79.06-3.57.07-5.36.01-4.03-.01-8.05.02-12.07z"
/>
</svg>

View File

@@ -0,0 +1,16 @@
<script lang="ts">
let { size = 24 }: { size?: number } = $props();
</script>
<svg
xmlns="http://www.w3.org/2000/svg"
width={size}
height={size}
viewBox="0 0 24 24"
fill="currentColor"
aria-hidden="true"
>
<path
d="M23.498 6.186a3.016 3.016 0 0 0-2.122-2.136C19.505 3.545 12 3.545 12 3.545s-7.505 0-9.377.505A3.017 3.017 0 0 0 .502 6.186C0 8.07 0 12 0 12s0 3.93.502 5.814a3.016 3.016 0 0 0 2.122 2.136c1.871.505 9.376.505 9.376.505s7.505 0 9.377-.505a3.015 3.015 0 0 0 2.122-2.136C24 15.93 24 12 24 12s0-3.93-.502-5.814zM9.545 15.568V8.432L15.818 12l-6.273 3.568z"
/>
</svg>