feat(app): add weekly recap cards with swipeable story UI and export

Introduce a Spotify-Wrapped-style recap feature: five themed cards
  (Cover, Path, Vibe, Workflow, Closing) rendered per archetype palette,
  with keyboard/swipe navigation and image export via capture IPC. Add
  RecapList, RecapDetail, RecapExport views and recap component library.
  Wire recap:list/read/updated IPC channels through preload, document the
  retrieval-to-card contract in references/recap-patterns.md, and bundle
  dist-renderer for production use.
This commit is contained in:
tommy0103
2026-06-14 03:16:49 +08:00
parent 4eec6b38c9
commit 9fc7f202f0
30 changed files with 4924 additions and 76 deletions
+2
View File
@@ -46,6 +46,8 @@ const breadcrumbs = computed(() => {
crumbs.push({ label: filename, terminal: true, filename: true });
} else if (name === 'Activity') {
crumbs.push({ label: 'Activity', terminal: true });
} else if (name === 'Recap') {
crumbs.push({ label: 'Recap', terminal: true });
}
return crumbs;
@@ -0,0 +1,77 @@
<script setup>
defineProps({
headline: String,
stats: Array,
mostSaidPhrase: String,
signoff: String,
idx: { type: Number, default: 5 },
total: { type: Number, default: 5 },
});
</script>
<template>
<article class="card card-closing">
<div class="eyebrow">
<span class="diamond"></span>
<span>The week, carved.</span>
<span class="eyebrow-spacer"></span>
<span class="slot">{{ String(idx).padStart(2, '0') }} · {{ String(total).padStart(2, '0') }}</span>
</div>
<div class="closing-body">
<div class="closing-headline">{{ headline }}</div>
<div class="closing-stats" v-if="stats">
<div v-for="(line, i) in stats" :key="i">{{ line }}</div>
</div>
<div class="closing-quote" v-if="mostSaidPhrase">
<span>"{{ mostSaidPhrase }}"</span>
<span class="verb"> most-said phrase</span>
</div>
<div class="closing-signoff">{{ signoff }}</div>
</div>
</article>
</template>
<style scoped>
@import './card-base.css';
.card-closing {
background:
radial-gradient(80% 70% at 50% 30%, var(--tg-soft) 0%, transparent 60%),
radial-gradient(60% 50% at 50% 50%, var(--tg-mid) 0%, transparent 70%),
linear-gradient(180deg, rgba(10,11,20,0.6) 0%, rgba(10,11,20,0.95) 100%);
transition: background var(--theme-ease);
}
.closing-body {
flex: 1; display: flex; flex-direction: column;
align-items: center; justify-content: center;
text-align: center; padding: 0 40px; gap: 32px;
position: relative; z-index: 1;
}
.closing-headline {
font-family: var(--font-serif); font-size: 72px;
line-height: 1; font-weight: 500; letter-spacing: -0.02em;
color: var(--fg); text-shadow: 0 4px 24px var(--tg);
transition: text-shadow var(--theme-ease);
}
.closing-stats {
font-family: var(--font-mono); font-size: 13px; color: var(--muted);
font-variant-numeric: tabular-nums;
display: flex; flex-direction: column; gap: 4px;
}
.closing-quote {
font-family: var(--font-serif); font-style: italic;
font-size: 19px; color: var(--fg-2); line-height: 1.5; max-width: 360px;
}
.closing-quote .verb {
font-family: var(--font-serif); font-style: italic;
font-size: 13px; color: var(--muted); display: block; margin-top: 8px;
}
.closing-signoff {
font-family: var(--font-serif); font-size: 15px;
color: var(--muted); font-style: italic;
}
</style>
@@ -0,0 +1,142 @@
<script setup>
import { computed } from 'vue';
import { CORNER_SEALS } from './seals.js';
const props = defineProps({
archKey: String,
badge: String,
title: String,
subtitle: String,
activity: Array,
footer: String,
idx: { type: Number, default: 1 },
total: { type: Number, default: 5 },
});
const sealSvg = computed(() => CORNER_SEALS[props.archKey] || '');
</script>
<template>
<article class="card card-cover">
<div class="cover-stars">
<span></span><span></span><span></span><span></span><span></span>
</div>
<div class="eyebrow">
<span class="diamond"></span>
<span>{{ badge }}</span>
</div>
<div class="cover-seal-corner" v-html="sealSvg"></div>
<div class="cover-body">
<div class="cover-archetype">{{ title }}</div>
<div class="cover-subtitle">{{ subtitle }}</div>
<div class="cover-activity">
<div class="cover-activity-row">
<div
v-for="(val, i) in activity" :key="i"
class="cover-activity-cell"
:class="{ dim: val < 0.4 }"
>
<div v-if="val > 0" class="fill" :style="{ height: val * 100 + '%' }"></div>
</div>
</div>
<div class="cover-activity-labels">
<span>M</span><span>T</span><span>W</span><span>T</span><span>F</span><span>S</span><span>S</span>
</div>
</div>
<div class="cover-footer" v-html="footer"></div>
</div>
</article>
</template>
<style scoped>
@import './card-base.css';
.card-cover {
background:
radial-gradient(120% 80% at 50% 100%, var(--tg) 0%, transparent 55%),
radial-gradient(100% 70% at 50% 80%, var(--tg-mid) 0%, transparent 65%),
radial-gradient(80% 50% at 50% 60%, var(--tg-soft) 0%, transparent 65%),
linear-gradient(180deg, rgba(10,11,20,0.4) 0%, rgba(10,11,20,0.85) 70%);
transition: background var(--theme-ease);
}
.cover-stars {
position: absolute; inset: 0; pointer-events: none;
}
.cover-stars span {
position: absolute;
width: 1.5px; height: 1.5px;
background: rgba(255,255,255,0.85);
border-radius: 50%;
box-shadow: 0 0 4px rgba(255,255,255,0.6);
}
.cover-stars span:nth-child(1) { top: 12%; left: 18%; }
.cover-stars span:nth-child(2) { top: 8%; left: 78%; width: 2px; height: 2px; }
.cover-stars span:nth-child(3) { top: 22%; left: 88%; opacity: 0.6; }
.cover-stars span:nth-child(4) { top: 32%; left: 8%; opacity: 0.5; }
.cover-stars span:nth-child(5) { top: 18%; left: 52%; width: 1px; height: 1px; opacity: 0.7; }
.cover-seal-corner {
position: absolute;
top: 22px; right: 24px;
width: 60px; height: 60px; z-index: 3;
}
.cover-seal-corner :deep(svg) {
width: 100%; height: 100%;
filter: drop-shadow(0 0 12px var(--tg));
transition: filter var(--theme-ease);
}
.cover-body {
flex: 1; display: flex; flex-direction: column;
padding: 0 36px; position: relative; z-index: 1;
}
.cover-archetype {
margin-top: auto;
font-family: var(--font-serif);
font-size: 64px; line-height: 1.05; font-weight: 500;
letter-spacing: -0.02em; color: var(--fg);
margin-bottom: 18px;
text-shadow: 0 2px 24px rgba(0,0,0,0.4);
}
.cover-subtitle {
font-family: var(--font-serif); font-style: italic;
font-size: 19px; line-height: 1.5; color: var(--fg-2);
margin-bottom: 36px; max-width: 92%;
}
.cover-activity { margin-bottom: 28px; }
.cover-activity-row {
display: grid; grid-template-columns: repeat(7, 1fr);
gap: 6px; margin-bottom: 8px;
}
.cover-activity-cell {
height: 32px; border-radius: 3px;
background: rgba(255,255,255,0.04);
border: 1px solid var(--hairline);
position: relative; overflow: hidden;
}
.cover-activity-cell .fill {
position: absolute; bottom: 0; left: 0; right: 0;
background: linear-gradient(to top, var(--tc), var(--tc-2));
box-shadow: 0 0 10px var(--tg);
border-radius: 0 0 2px 2px;
transition: background var(--theme-ease), box-shadow var(--theme-ease);
}
.cover-activity-cell.dim .fill {
background: linear-gradient(to top, rgba(255,255,255,0.15), rgba(255,255,255,0.06));
box-shadow: none;
}
.cover-activity-labels {
display: grid; grid-template-columns: repeat(7, 1fr); gap: 6px;
font-family: var(--font-mono); font-size: 10.5px;
color: var(--muted-2); text-align: center;
}
.cover-footer {
padding-bottom: 28px;
font-family: var(--font-mono); font-size: 13px; color: var(--muted);
display: flex; gap: 14px; font-variant-numeric: tabular-nums;
}
.cover-footer :deep(strong) { color: var(--fg); font-weight: 500; }
.cover-footer :deep(.sep) { color: var(--muted-3); }
</style>
@@ -0,0 +1,94 @@
<script setup>
defineProps({
title: String,
items: Array,
idx: { type: Number, default: 2 },
total: { type: Number, default: 5 },
});
</script>
<template>
<article class="card" data-themed>
<div class="eyebrow">
<span class="diamond"></span>
<span>Your thinking path</span>
<span class="eyebrow-spacer"></span>
<span class="slot">{{ String(idx).padStart(2, '0') }} · {{ String(total).padStart(2, '0') }}</span>
</div>
<div class="card-title">{{ title }}</div>
<div class="timeline-wrap">
<div class="timeline">
<div v-for="(item, i) in items" :key="i" class="tl-item">
<div class="tl-node"></div>
<div class="tl-day">{{ item.day }}</div>
<div class="tl-prompt">{{ item.prompt }}</div>
<div class="tl-outcome">
<span>{{ item.outcome }}</span>
</div>
</div>
</div>
</div>
</article>
</template>
<style scoped>
@import './card-base.css';
.timeline-wrap {
flex: 1; padding: 0 36px 24px 36px;
overflow-y: auto; position: relative; z-index: 1;
}
.timeline { position: relative; padding-left: 28px; }
.timeline::before {
content: ''; position: absolute;
left: 6px; top: 14px; bottom: 14px; width: 1px;
background: linear-gradient(to bottom,
var(--tg) 0%, var(--tg-mid) 30%,
rgba(255,255,255,0.12) 70%, rgba(255,255,255,0.06) 100%);
transition: background var(--theme-ease);
}
.tl-item { position: relative; padding: 8px 0 10px; }
.tl-item:first-child { padding-top: 4px; }
.tl-item:last-child { padding-bottom: 0; }
.tl-node {
position: absolute; left: -28px; top: 16px;
width: 13px; height: 13px;
}
.tl-item:first-child .tl-node { top: 12px; }
.tl-node::before {
content: ''; position: absolute;
left: 50%; top: 50%;
width: 7px; height: 7px;
background: var(--tc);
transform: translate(-50%, -50%) rotate(45deg);
box-shadow: 0 0 8px var(--tg), 0 0 0 3px rgba(10,11,20,1);
transition: background var(--theme-ease), box-shadow var(--theme-ease);
}
.tl-day {
display: inline-block;
font-family: var(--font-mono); font-size: 12px; font-weight: 600;
color: var(--tc-2); margin-bottom: 4px; letter-spacing: 0.01em;
transition: color var(--theme-ease);
}
.tl-prompt {
font-family: var(--font-serif); font-style: italic;
font-size: 16px; line-height: 1.35; color: var(--fg); margin-bottom: 6px;
}
.tl-prompt::before { content: '\201C'; color: var(--muted-2); margin-right: 1px; }
.tl-prompt::after { content: '\201D'; color: var(--muted-2); margin-left: 1px; }
.tl-outcome {
display: inline-flex; align-items: baseline; gap: 8px;
font-family: var(--font-mono); font-size: 12px; color: var(--fg-2);
padding: 4px 10px;
border-radius: 4px;
background: rgba(255,255,255,0.025);
border: 1px solid var(--hairline);
border-left: 2px solid var(--tc);
box-shadow: -2px 0 8px -2px var(--tg-mid);
transition: border-left-color var(--theme-ease), box-shadow var(--theme-ease);
}
</style>
@@ -0,0 +1,132 @@
<script setup>
defineProps({
title: String,
observations: Array,
meter: Object,
quote: Object,
idx: { type: Number, default: 3 },
total: { type: Number, default: 5 },
});
</script>
<template>
<article class="card" data-themed>
<div class="eyebrow">
<span class="diamond"></span>
<span>Your vibe this week</span>
<span class="eyebrow-spacer"></span>
<span class="slot">{{ String(idx).padStart(2, '0') }} · {{ String(total).padStart(2, '0') }}</span>
</div>
<div class="card-title">{{ title }}</div>
<div class="vibe-content">
<div class="vibe-section">
<div class="section-label">Things you kept saying</div>
<div class="vibe-observations">
<div v-for="(obs, i) in observations" :key="i" class="vibe-obs">
<div class="vibe-obs-text">{{ obs.text }}</div>
<div class="vibe-obs-meta">
<template v-if="obs.count">×{{ obs.count }} · </template>
{{ obs.label }}
<template v-if="obs.time"> · {{ obs.time }}</template>
</div>
</div>
</div>
</div>
<div class="vibe-section" v-if="meter">
<div class="vibe-meter">
<div class="vibe-meter-track">
<div class="vibe-meter-fill" :style="{ width: meter.value * 100 + '%' }"></div>
</div>
<div class="vibe-meter-row">
<span class="vibe-meter-label">{{ meter.label }}</span>
<span class="vibe-meter-caption">{{ meter.caption }}</span>
</div>
</div>
</div>
<div class="vibe-quote" v-if="quote">
<div class="vibe-quote-text">{{ quote.text }}</div>
<div class="vibe-quote-caption" v-if="quote.caption"> {{ quote.caption }}</div>
</div>
</div>
</article>
</template>
<style scoped>
@import './card-base.css';
.vibe-content {
flex: 1; padding: 0 36px 32px;
display: flex; flex-direction: column; gap: 22px;
overflow-y: auto; position: relative; z-index: 1;
}
.vibe-section { display: flex; flex-direction: column; gap: 12px; }
.vibe-observations { display: flex; flex-direction: column; gap: 10px; }
.vibe-obs {
display: flex; align-items: baseline; gap: 12px;
padding: 10px 14px;
background: rgba(255,255,255,0.025);
border: 1px solid var(--hairline);
border-left: 2px solid var(--tg-mid);
border-radius: 4px;
transition: border-left-color var(--theme-ease);
}
.vibe-obs-text {
font-family: var(--font-serif); font-style: italic;
font-size: 18px; line-height: 1.4; color: var(--fg); flex: 1;
}
.vibe-obs-text::before { content: '\201C'; color: var(--muted-2); }
.vibe-obs-text::after { content: '\201D'; color: var(--muted-2); }
.vibe-obs-meta {
font-family: var(--font-mono); font-size: 12px; color: var(--muted);
white-space: nowrap; flex-shrink: 0; font-variant-numeric: tabular-nums;
}
.vibe-correction {
font-family: var(--font-serif); font-size: 14.5px;
line-height: 1.6; color: var(--fg-2);
}
.vibe-correction :deep(strong) { color: var(--fg); font-weight: 600; font-variant-numeric: tabular-nums; }
.vibe-correction :deep(.vs) { color: var(--muted); font-style: italic; margin: 0 6px; }
.vibe-meter { display: flex; flex-direction: column; gap: 8px; }
.vibe-meter-track {
position: relative; height: 10px;
background: rgba(255,255,255,0.04);
border: 1px solid var(--hairline); border-radius: 2px; overflow: hidden;
}
.vibe-meter-fill {
position: absolute; top: 0; left: 0; bottom: 0;
background: linear-gradient(to right, var(--tc), var(--tc-2));
box-shadow: 0 0 12px var(--tg); border-radius: 1px;
transition: background var(--theme-ease), box-shadow var(--theme-ease);
}
.vibe-meter-row {
display: flex; align-items: baseline; justify-content: space-between;
font-family: var(--font-mono); font-size: 11.5px;
}
.vibe-meter-label {
color: var(--muted); font-style: italic;
font-family: var(--font-serif); font-size: 14px;
}
.vibe-meter-caption {
color: var(--tc-2); font-weight: 600;
transition: color var(--theme-ease);
}
.vibe-quote {
margin-top: auto; padding: 18px 0 0;
border-top: 1px solid var(--hairline);
}
.vibe-quote-text {
font-family: var(--font-serif); font-size: 22px;
line-height: 1.4; color: var(--fg); font-weight: 500;
letter-spacing: -0.01em; margin-bottom: 8px;
}
.vibe-quote-caption {
font-family: var(--font-serif); font-style: italic;
font-size: 13px; color: var(--muted);
}
</style>
@@ -0,0 +1,98 @@
<script setup>
defineProps({
title: String,
summary: String,
stats: String,
items: Array,
verdict: String,
idx: { type: Number, default: 4 },
total: { type: Number, default: 5 },
});
</script>
<template>
<article class="card" data-themed>
<div class="eyebrow">
<span class="diamond"></span>
<span>Workflows</span>
<span class="eyebrow-spacer"></span>
<span class="slot">{{ String(idx).padStart(2, '0') }} · {{ String(total).padStart(2, '0') }}</span>
</div>
<div class="card-title">{{ title }}</div>
<div class="card-deck-text" v-if="summary">{{ summary }}</div>
<div class="wf-content">
<div class="wf-stats" v-if="stats">{{ stats }}</div>
<div class="wf-list">
<div v-for="(item, i) in items" :key="i" class="wf-item">
<div class="wf-item-name">{{ item.name }}</div>
<div class="wf-item-outcome">{{ item.outcome }}</div>
</div>
</div>
<div class="wf-verdict" v-if="verdict">
<div class="wf-verdict-label">Verdict </div>
<div class="wf-verdict-text">{{ verdict }}</div>
</div>
</div>
</article>
</template>
<style scoped>
@import './card-base.css';
.wf-content {
flex: 1; padding: 0 36px 32px;
display: flex; flex-direction: column; gap: 18px;
overflow-y: auto; position: relative; z-index: 1;
}
.wf-stats {
font-family: var(--font-mono); font-size: 13px; color: var(--muted);
font-variant-numeric: tabular-nums; display: flex; gap: 14px;
}
.wf-stats :deep(strong) { color: var(--fg); font-weight: 500; }
.wf-stats :deep(.sep) { color: var(--muted-3); }
.wf-list {
display: flex; flex-direction: column; gap: 1px;
background: var(--hairline); border: 1px solid var(--hairline);
border-radius: 6px; overflow: hidden;
}
.wf-item {
padding: 14px 16px; background: rgba(10,11,20,0.4);
display: flex; flex-direction: column; gap: 6px;
}
.wf-item-name {
font-family: var(--font-mono); font-size: 13px; font-weight: 500;
color: var(--fg); letter-spacing: -0.005em;
}
.wf-item-outcome {
font-family: var(--font-serif); font-style: italic;
font-size: 16px; color: var(--fg-2); line-height: 1.4;
}
.wf-item-outcome::before { content: '\201C'; color: var(--muted-2); }
.wf-item-outcome::after { content: '\201D'; color: var(--muted-2); }
.wf-verdict {
margin-top: auto; padding: 16px 18px;
border: 1px solid var(--hairline-strong); border-radius: 6px;
background: rgba(255,255,255,0.025);
display: flex; flex-direction: column; gap: 6px;
position: relative; overflow: hidden;
}
.wf-verdict::before {
content: ''; position: absolute;
left: 0; top: 0; bottom: 0; width: 2px;
background: var(--tc); box-shadow: 0 0 8px var(--tg);
transition: background var(--theme-ease), box-shadow var(--theme-ease);
}
.wf-verdict-label {
font-family: var(--font-serif); font-style: italic;
font-size: 13px; color: var(--muted);
}
.wf-verdict-text {
font-family: var(--font-serif); font-size: 22px; font-weight: 500;
color: var(--fg); letter-spacing: -0.01em;
}
</style>
@@ -0,0 +1,21 @@
export const PALETTES = {
architect: { tc: '#a78bfa', tc2: '#c4b5fd', glow: 'rgba(167,139,250,0.40)', mid: 'rgba(167,139,250,0.22)', soft: 'rgba(167,139,250,0.10)' },
debugger: { tc: '#fbbf24', tc2: '#fde68a', glow: 'rgba(251,191,36,0.40)', mid: 'rgba(251,191,36,0.22)', soft: 'rgba(251,191,36,0.10)' },
shipper: { tc: '#f472b6', tc2: '#fda4af', glow: 'rgba(244,114,182,0.40)', mid: 'rgba(244,114,182,0.22)', soft: 'rgba(244,114,182,0.10)' },
curator: { tc: '#67e8f9', tc2: '#a5f3fc', glow: 'rgba(103,232,249,0.40)', mid: 'rgba(103,232,249,0.22)', soft: 'rgba(103,232,249,0.10)' },
director: { tc: '#fcd34d', tc2: '#fde68a', glow: 'rgba(252,211,77,0.40)', mid: 'rgba(252,211,77,0.22)', soft: 'rgba(252,211,77,0.10)' },
cartographer: { tc: '#34d399', tc2: '#6ee7b7', glow: 'rgba(52,211,153,0.40)', mid: 'rgba(52,211,153,0.22)', soft: 'rgba(52,211,153,0.10)' },
wanderer: { tc: '#64748b', tc2: '#94a3b8', glow: 'rgba(100,116,139,0.45)', mid: 'rgba(100,116,139,0.25)', soft: 'rgba(100,116,139,0.12)' },
};
export const ARCHETYPE_NAMES = {
architect: 'The Architect',
debugger: 'The Debugger',
shipper: 'The Shipper',
curator: 'The Curator',
director: 'The Director',
cartographer: 'The Cartographer',
wanderer: 'The Wanderer',
};
export const ARCH_KEYS = ['architect', 'debugger', 'shipper', 'curator', 'director', 'cartographer', 'wanderer'];
@@ -0,0 +1,70 @@
.card {
position: absolute; inset: 0;
border-radius: 14px;
background: linear-gradient(165deg, rgba(255,255,255,0.04) 0%, rgba(255,255,255,0.015) 100%);
border: 1px solid var(--hairline-strong);
box-shadow:
0 30px 80px rgba(0,0,0,0.5),
0 12px 32px rgba(0,0,0,0.3),
inset 0 1px 0 rgba(255,255,255,0.08);
backdrop-filter: blur(24px);
-webkit-backdrop-filter: blur(24px);
overflow: hidden;
display: flex;
flex-direction: column;
}
.card[data-themed]::before {
content: '';
position: absolute; pointer-events: none; z-index: 0;
width: 60%; height: 50%; bottom: 0; right: 0;
background: radial-gradient(ellipse at 100% 100%, var(--tg-mid) 0%, transparent 70%);
opacity: 0.6;
transition: background var(--theme-ease);
}
.card[data-themed]::after {
content: '';
position: absolute; pointer-events: none; z-index: 0;
left: 0; right: 0; bottom: 0; height: 1px;
background: linear-gradient(to right, transparent 0%, var(--tg) 50%, transparent 100%);
opacity: 0.6;
transition: background var(--theme-ease);
}
.eyebrow {
display: flex; align-items: center; gap: 10px;
padding: 22px 28px 0;
font-family: var(--font-mono); font-size: 12px;
color: var(--muted); letter-spacing: 0.01em;
position: relative; z-index: 1;
}
.eyebrow .diamond {
width: 6px; height: 6px;
background: var(--tc); transform: rotate(45deg);
box-shadow: 0 0 8px var(--tg); flex-shrink: 0;
transition: background var(--theme-ease), box-shadow var(--theme-ease);
}
.eyebrow-spacer { flex: 1; }
.eyebrow .slot {
color: var(--muted-2); font-variant-numeric: tabular-nums;
}
.card-title {
padding: 18px 36px 6px;
font-family: var(--font-serif); font-size: 30px;
letter-spacing: -0.015em; font-weight: 500;
color: var(--fg); line-height: 1.2;
position: relative; z-index: 1;
}
.card-deck-text {
padding: 0 36px 22px;
font-size: 15px; color: var(--fg-3);
line-height: 1.55; font-style: italic;
font-family: var(--font-serif);
position: relative; z-index: 1;
}
.section-label {
font-family: var(--font-serif); font-style: italic;
font-size: 13px; color: var(--muted);
}
@@ -0,0 +1,19 @@
export const MINI_SEALS = {
architect: `<svg viewBox="0 0 110 110" fill="none"><circle cx="55" cy="55" r="36" stroke="#a78bfa" stroke-width="4" stroke-opacity="0.7"/><polygon points="55,32 50,42 60,42" fill="#c4b5fd"/><polygon points="50,42 60,42 58,72 52,72" fill="#a78bfa"/></svg>`,
debugger: `<svg viewBox="0 0 110 110" fill="none"><circle cx="55" cy="55" r="36" stroke="#fbbf24" stroke-width="4" stroke-opacity="0.7"/><path d="M 55 34 A 21 21 0 1 1 34 55 A 16 16 0 1 0 55 39 A 11 11 0 1 1 44 55" stroke="#fde68a" stroke-width="3.5" fill="none" stroke-linecap="round"/></svg>`,
shipper: `<svg viewBox="0 0 110 110" fill="none"><circle cx="55" cy="55" r="36" stroke="#f472b6" stroke-width="4" stroke-opacity="0.7"/><rect x="36" y="48" width="13" height="13" rx="1.5" fill="#f472b6" opacity="0.45"/><rect x="50" y="48" width="13" height="13" rx="1.5" fill="#f472b6" opacity="0.85"/><rect x="64" y="48" width="13" height="13" rx="1.5" fill="#fda4af"/></svg>`,
curator: `<svg viewBox="0 0 110 110" fill="none"><circle cx="55" cy="55" r="36" stroke="#67e8f9" stroke-width="4" stroke-opacity="0.7"/><rect x="34" y="46" width="42" height="5" rx="1" fill="#a5f3fc" opacity="0.85"/><rect x="38" y="55" width="34" height="5" rx="1" fill="#67e8f9" opacity="0.7"/><rect x="34" y="64" width="42" height="5" rx="1" fill="#22d3ee" opacity="0.55"/></svg>`,
director: `<svg viewBox="0 0 110 110" fill="none"><circle cx="55" cy="55" r="36" stroke="#fcd34d" stroke-width="4" stroke-opacity="0.7"/><g stroke="#fde68a" stroke-width="3" stroke-linecap="round" opacity="0.85"><line x1="55" y1="55" x2="55" y2="36"/><line x1="55" y1="55" x2="72" y2="44"/><line x1="55" y1="55" x2="72" y2="66"/><line x1="55" y1="55" x2="55" y2="74"/><line x1="55" y1="55" x2="38" y2="66"/><line x1="55" y1="55" x2="38" y2="44"/></g><circle cx="55" cy="55" r="4" fill="#fcd34d"/></svg>`,
cartographer: `<svg viewBox="0 0 110 110" fill="none"><circle cx="55" cy="55" r="36" stroke="#34d399" stroke-width="4" stroke-opacity="0.7"/><g stroke="#34d399" stroke-width="1.5" stroke-opacity="0.4" stroke-dasharray="3 3"><line x1="34" y1="55" x2="76" y2="55"/><line x1="55" y1="34" x2="55" y2="76"/></g><polygon points="55,38 51,55 55,53 59,55" fill="#6ee7b7"/><polygon points="55,38 55,53 59,55" fill="#34d399"/></svg>`,
wanderer: `<svg viewBox="0 0 110 110" fill="none"><circle cx="55" cy="55" r="36" stroke="#64748b" stroke-width="4" stroke-opacity="0.85"/><path d="M 38 40 C 44 50, 50 44, 54 50 C 60 60, 50 66, 56 72 C 62 78, 70 66, 76 70" stroke="#94a3b8" stroke-width="2.6" fill="none" stroke-linecap="round" opacity="0.95"/><circle cx="38" cy="40" r="3" fill="#94a3b8"/><circle cx="76" cy="70" r="3" fill="#94a3b8"/></svg>`,
};
export const CORNER_SEALS = {
architect: `<svg viewBox="0 0 110 110" fill="none"><defs><radialGradient id="cs-arc" cx="50%" cy="50%" r="50%"><stop offset="0%" stop-color="#a78bfa" stop-opacity="0.5"/><stop offset="100%" stop-color="#a78bfa" stop-opacity="0"/></radialGradient></defs><circle cx="55" cy="55" r="50" fill="url(#cs-arc)"/><circle cx="55" cy="55" r="42" stroke="rgba(255,255,255,0.20)" stroke-width="1"/><circle cx="55" cy="55" r="38" stroke="#a78bfa" stroke-width="1.4" stroke-opacity="0.85"/><polygon points="55,28 50,38 60,38" fill="#c4b5fd"/><polygon points="50,38 60,38 58,76 52,76" fill="#a78bfa"/><polygon points="55,38 60,38 58,76 55,76" fill="#7c3aed" opacity="0.75"/><rect x="48" y="76" width="14" height="2" rx="0.4" fill="#1e293b"/></svg>`,
debugger: `<svg viewBox="0 0 110 110" fill="none"><defs><radialGradient id="cs-dbg" cx="50%" cy="50%" r="50%"><stop offset="0%" stop-color="#fbbf24" stop-opacity="0.5"/><stop offset="100%" stop-color="#fbbf24" stop-opacity="0"/></radialGradient></defs><circle cx="55" cy="55" r="50" fill="url(#cs-dbg)"/><circle cx="55" cy="55" r="42" stroke="rgba(255,255,255,0.20)" stroke-width="1"/><circle cx="55" cy="55" r="38" stroke="#fbbf24" stroke-width="1.4" stroke-opacity="0.85"/><path d="M 55 27 A 28 28 0 1 1 27 55 A 22 22 0 1 0 55 33 A 16 16 0 1 1 39 55 A 11 11 0 1 0 55 44 A 6 6 0 1 1 49 55 L 55 55" stroke="#fde68a" stroke-width="1.7" fill="none" stroke-linecap="round"/><circle cx="55" cy="55" r="2.5" fill="#fde68a"/></svg>`,
shipper: `<svg viewBox="0 0 110 110" fill="none"><defs><radialGradient id="cs-shp" cx="50%" cy="50%" r="50%"><stop offset="0%" stop-color="#f472b6" stop-opacity="0.5"/><stop offset="100%" stop-color="#f472b6" stop-opacity="0"/></radialGradient></defs><circle cx="55" cy="55" r="50" fill="url(#cs-shp)"/><circle cx="55" cy="55" r="42" stroke="rgba(255,255,255,0.20)" stroke-width="1"/><circle cx="55" cy="55" r="38" stroke="#f472b6" stroke-width="1.4" stroke-opacity="0.85"/><rect x="32" y="48" width="14" height="14" rx="1.5" fill="#f472b6" opacity="0.35"/><rect x="48" y="48" width="14" height="14" rx="1.5" fill="#f472b6" opacity="0.65"/><rect x="64" y="48" width="14" height="14" rx="1.5" fill="#fda4af"/><path d="M 32 72 L 78 72 M 73 68 L 78 72 L 73 76" stroke="#fda4af" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round" fill="none"/></svg>`,
curator: `<svg viewBox="0 0 110 110" fill="none"><defs><radialGradient id="cs-cur" cx="50%" cy="50%" r="50%"><stop offset="0%" stop-color="#67e8f9" stop-opacity="0.45"/><stop offset="100%" stop-color="#67e8f9" stop-opacity="0"/></radialGradient></defs><circle cx="55" cy="55" r="50" fill="url(#cs-cur)"/><circle cx="55" cy="55" r="42" stroke="rgba(255,255,255,0.20)" stroke-width="1"/><circle cx="55" cy="55" r="38" stroke="#67e8f9" stroke-width="1.4" stroke-opacity="0.85"/><rect x="30" y="42" width="50" height="6" rx="1" fill="#a5f3fc" opacity="0.85"/><rect x="34" y="52" width="42" height="6" rx="1" fill="#67e8f9" opacity="0.7"/><rect x="30" y="62" width="50" height="6" rx="1" fill="#22d3ee" opacity="0.55"/><rect x="38" y="72" width="34" height="4" rx="1" fill="#0891b2" opacity="0.5"/></svg>`,
director: `<svg viewBox="0 0 110 110" fill="none"><defs><radialGradient id="cs-dir" cx="50%" cy="50%" r="50%"><stop offset="0%" stop-color="#fcd34d" stop-opacity="0.45"/><stop offset="100%" stop-color="#fcd34d" stop-opacity="0"/></radialGradient></defs><circle cx="55" cy="55" r="50" fill="url(#cs-dir)"/><circle cx="55" cy="55" r="42" stroke="rgba(255,255,255,0.20)" stroke-width="1"/><circle cx="55" cy="55" r="38" stroke="#fcd34d" stroke-width="1.4" stroke-opacity="0.85"/><g stroke="#fde68a" stroke-width="1.1" stroke-linecap="round" opacity="0.85"><line x1="55" y1="55" x2="55" y2="32"/><line x1="55" y1="55" x2="74" y2="42"/><line x1="55" y1="55" x2="74" y2="68"/><line x1="55" y1="55" x2="55" y2="78"/><line x1="55" y1="55" x2="36" y2="68"/><line x1="55" y1="55" x2="36" y2="42"/></g><g fill="#fde68a"><circle cx="55" cy="32" r="2.5"/><circle cx="74" cy="42" r="2.5"/><circle cx="74" cy="68" r="2.5"/><circle cx="55" cy="78" r="2.5"/><circle cx="36" cy="68" r="2.5"/><circle cx="36" cy="42" r="2.5"/></g><circle cx="55" cy="55" r="3.5" fill="#fcd34d"/></svg>`,
cartographer: `<svg viewBox="0 0 110 110" fill="none"><defs><radialGradient id="cs-cart" cx="50%" cy="50%" r="50%"><stop offset="0%" stop-color="#34d399" stop-opacity="0.5"/><stop offset="100%" stop-color="#34d399" stop-opacity="0"/></radialGradient></defs><circle cx="55" cy="55" r="50" fill="url(#cs-cart)"/><circle cx="55" cy="55" r="42" stroke="rgba(255,255,255,0.20)" stroke-width="1"/><circle cx="55" cy="55" r="38" stroke="#34d399" stroke-width="1.4" stroke-opacity="0.85"/><g stroke="#34d399" stroke-width="0.5" stroke-opacity="0.4" stroke-dasharray="2 2"><line x1="32" y1="44" x2="78" y2="44"/><line x1="32" y1="55" x2="78" y2="55"/><line x1="32" y1="66" x2="78" y2="66"/><line x1="44" y1="32" x2="44" y2="78"/><line x1="55" y1="32" x2="55" y2="78"/><line x1="66" y1="32" x2="66" y2="78"/></g><polygon points="55,38 52.5,55 55,53 57.5,55" fill="#6ee7b7"/><polygon points="55,38 55,53 57.5,55" fill="#34d399"/><polygon points="55,72 52.5,55 55,57 57.5,55" fill="#34d399" opacity="0.6"/><polygon points="72,55 55,52.5 57,55 55,57.5" fill="#6ee7b7" opacity="0.7"/><polygon points="38,55 55,52.5 53,55 55,57.5" fill="#6ee7b7" opacity="0.7"/><circle cx="55" cy="55" r="2" fill="#0a0b14"/><circle cx="55" cy="55" r="2.4" stroke="#6ee7b7" stroke-width="0.6" fill="none"/></svg>`,
wanderer: `<svg viewBox="0 0 110 110" fill="none"><defs><radialGradient id="cs-wand" cx="50%" cy="50%" r="50%"><stop offset="0%" stop-color="#64748b" stop-opacity="0.5"/><stop offset="100%" stop-color="#64748b" stop-opacity="0"/></radialGradient></defs><circle cx="55" cy="55" r="50" fill="url(#cs-wand)"/><circle cx="55" cy="55" r="42" stroke="rgba(255,255,255,0.20)" stroke-width="1"/><circle cx="55" cy="55" r="38" stroke="#64748b" stroke-width="1.5" stroke-opacity="0.9"/><path d="M 36 38 C 42 50, 48 42, 54 50 C 60 60, 50 65, 56 72 C 62 78, 70 64, 76 70" stroke="#94a3b8" stroke-width="1.5" fill="none" stroke-linecap="round" stroke-linejoin="round" opacity="0.95"/><circle cx="36" cy="38" r="2.4" fill="#94a3b8"/><circle cx="54" cy="50" r="1.7" fill="#94a3b8" opacity="0.9"/><circle cx="56" cy="72" r="1.7" fill="#94a3b8" opacity="0.9"/><circle cx="76" cy="70" r="2.4" fill="#94a3b8"/></svg>`,
};