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:
@@ -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>`,
|
||||
};
|
||||
Reference in New Issue
Block a user