dreaming: simplify Scene and Diary UI

Scene: remove trace grid, replace with clean phase cards (Light/Deep/REM).
Diary: remove arrow nav and heatmap, replace with horizontal scrollable date chips.
Left-align content to match rest of app. Net -250 lines.
This commit is contained in:
Dave Morin
2026-04-09 16:46:04 -10:00
committed by Vignesh
parent 25db93457e
commit d1be4cec07
3 changed files with 162 additions and 437 deletions

View File

@@ -282,153 +282,56 @@
}
}
/* ---- Stats bar ---- */
/* ---- Sleep phase cards ---- */
.dreams__stats {
position: relative;
.dreams__phases {
display: flex;
align-items: center;
gap: 48px;
margin-top: 36px;
gap: 24px;
margin-top: 32px;
z-index: 1;
}
.dreams__stat {
display: flex;
flex-direction: column;
align-items: center;
gap: 4px;
}
.dreams__stat-value {
font-size: 28px;
font-weight: 700;
font-variant-numeric: tabular-nums;
}
.dreams__stat-label {
font-size: 11px;
color: var(--muted);
text-transform: uppercase;
letter-spacing: 0.08em;
}
.dreams__stat-divider {
width: 1px;
height: 32px;
background: var(--border);
}
.dreams__trace {
position: relative;
width: min(900px, calc(100% - 40px));
margin-top: 28px;
display: grid;
grid-template-columns: repeat(auto-fit, minmax(240px, 1fr));
align-items: start;
gap: 12px;
z-index: 1;
user-select: text;
max-height: min(500px, calc(100vh - 240px));
overflow-y: auto;
overflow-x: hidden;
}
.dreams__trace-section {
position: relative;
background: color-mix(in oklab, var(--panel) 82%, transparent);
border: 1px solid color-mix(in oklab, var(--border) 78%, transparent);
border-radius: 16px;
padding: 12px;
min-height: 180px;
backdrop-filter: blur(14px);
overflow: hidden;
min-width: 0;
z-index: 1;
}
.dreams__trace-header {
.dreams__phase {
display: flex;
align-items: center;
justify-content: space-between;
gap: 12px;
margin-bottom: 10px;
}
.dreams__trace-title {
font-size: 11px;
color: var(--muted);
text-transform: uppercase;
letter-spacing: 0.08em;
font-weight: 600;
}
.dreams__trace-count {
min-width: 24px;
height: 24px;
border-radius: 999px;
display: inline-flex;
align-items: center;
justify-content: center;
background: color-mix(in oklab, var(--accent-subtle) 85%, transparent);
color: var(--accent);
font-size: 12px;
font-weight: 700;
font-variant-numeric: tabular-nums;
}
.dreams__trace-list {
display: flex;
flex-direction: column;
gap: 8px;
min-width: 0;
}
.dreams__trace-item {
padding: 10px;
padding: 8px 16px;
border-radius: 12px;
background: color-mix(in oklab, var(--panel-raised) 88%, transparent);
border: 1px solid color-mix(in oklab, var(--border) 72%, transparent);
overflow: hidden;
overflow-wrap: anywhere;
word-break: break-word;
background: color-mix(in oklab, var(--panel) 70%, transparent);
border: 1px solid color-mix(in oklab, var(--border) 60%, transparent);
}
.dreams__trace-snippet {
font-size: 13px;
line-height: 1.35;
.dreams__phase--off {
opacity: 0.4;
}
.dreams__phase-dot {
width: 6px;
height: 6px;
border-radius: 50%;
background: var(--muted);
opacity: 0.4;
}
.dreams__phase-dot--on {
background: var(--ok);
opacity: 1;
box-shadow: 0 0 6px rgba(34, 197, 94, 0.3);
}
.dreams__phase-name {
font-size: 12px;
font-weight: 600;
color: var(--text);
overflow-wrap: anywhere;
word-break: break-word;
white-space: normal;
text-transform: uppercase;
letter-spacing: 0.06em;
}
.dreams__trace-source,
.dreams__trace-meta,
.dreams__trace-empty {
margin-top: 6px;
.dreams__phase-next {
font-size: 11px;
line-height: 1.35;
color: var(--muted);
}
.dreams__trace-source {
font-family: var(--mono);
overflow-wrap: anywhere;
word-break: break-word;
}
.dreams__trace-meta,
.dreams__trace-empty {
overflow-wrap: anywhere;
word-break: break-word;
}
@media (max-width: 980px) {
.dreams__trace {
grid-template-columns: 1fr;
width: min(680px, calc(100% - 32px));
}
font-variant-numeric: tabular-nums;
}
/* ---- Dreaming on/off toggle (header bar) ---- */
@@ -561,48 +464,13 @@
=========================================== */
.dreams-diary {
position: relative;
display: flex;
flex-direction: column;
align-items: center;
padding: 48px 24px 64px;
padding: 24px 32px 64px;
flex: 1;
min-height: 320px;
min-width: 0;
overflow: auto;
background: linear-gradient(
180deg,
var(--bg) 0%,
color-mix(in oklab, var(--bg) 94%, #0d0818) 40%,
color-mix(in oklab, var(--bg) 88%, #0d0818) 100%
);
}
/* Ambient shimmer across the diary surface */
.dreams-diary::before {
content: "";
position: absolute;
inset: 0;
background: linear-gradient(
135deg,
transparent 30%,
rgba(251, 191, 36, 0.012) 45%,
rgba(255, 77, 77, 0.015) 55%,
transparent 70%
);
background-size: 400% 400%;
animation: diary-shimmer 20s ease-in-out infinite;
pointer-events: none;
}
@keyframes diary-shimmer {
0%,
100% {
background-position: 0% 0%;
}
50% {
background-position: 100% 100%;
}
}
/* ---- Diary header ---- */
@@ -611,11 +479,7 @@
display: flex;
align-items: center;
gap: 16px;
margin-bottom: 32px;
width: 100%;
max-width: 520px;
position: relative;
z-index: 1;
margin-bottom: 20px;
flex-shrink: 0;
}
@@ -677,18 +541,12 @@
.dreams-diary__entry {
position: relative;
max-width: 520px;
width: 100%;
padding: 0 0 0 20px;
z-index: 1;
max-width: 680px;
padding: 0 0 0 16px;
flex-shrink: 0;
animation: diary-entry-reveal 1.4s cubic-bezier(0.22, 1, 0.36, 1) both;
}
.dreams-diary__entry--structured {
max-width: 1180px;
}
@keyframes diary-entry-reveal {
0% {
opacity: 0;
@@ -732,13 +590,25 @@
.dreams-diary__date {
display: block;
font-size: 10px;
color: var(--accent-muted);
letter-spacing: 0.08em;
text-transform: uppercase;
font-weight: 400;
font-size: 12px;
color: var(--text);
font-weight: 600;
margin-bottom: 16px;
opacity: 0.8;
}
.dreams-diary__daychips {
display: flex;
gap: 6px;
margin: 0 0 24px;
overflow-x: auto;
overflow-y: hidden;
scrollbar-width: none;
-ms-overflow-style: none;
padding-bottom: 2px;
}
.dreams-diary__daychips::-webkit-scrollbar {
display: none;
}
.dreams-diary__navigator {
@@ -804,9 +674,7 @@
}
.dreams-diary__day-chip {
width: 100%;
min-width: 0;
padding: 6px 0;
padding: 4px 12px;
border-radius: 999px;
border: 1px solid color-mix(in oklab, var(--border) 70%, transparent);
background: color-mix(in oklab, var(--panel) 84%, transparent);
@@ -815,6 +683,13 @@
font-variant-numeric: tabular-nums;
cursor: pointer;
text-align: center;
white-space: nowrap;
transition: border-color 140ms ease, color 140ms ease, background 140ms ease;
}
.dreams-diary__day-chip:hover {
color: var(--text);
border-color: color-mix(in oklab, var(--accent) 30%, var(--border));
}
.dreams-diary__day-chip--active {
@@ -965,10 +840,9 @@
.dreams-diary__para {
margin: 0 0 12px;
font-size: 14px;
line-height: 1.8;
color: color-mix(in oklab, var(--text) 85%, var(--muted));
font-style: italic;
font-size: 13px;
line-height: 1.7;
color: var(--text);
animation: diary-text-stream 2.4s cubic-bezier(0.22, 1, 0.36, 1) both;
overflow-wrap: anywhere;
word-break: break-word;
@@ -1039,29 +913,13 @@
min-height: calc(100vh - 96px);
}
.dreams__stats {
gap: 22px;
}
.dreams__phase-bar {
.dreams__phases {
gap: 12px;
flex-wrap: wrap;
gap: 8px;
padding: 10px 16px;
}
.dreams__phase-bar-phases {
gap: 10px;
justify-content: center;
}
.dreams-diary {
padding: 32px 16px 48px;
}
.dreams-diary__entry {
padding-left: 16px;
}
.dreams-diary__grid {
grid-template-columns: 1fr;
padding: 20px 16px 48px;
}
}

View File

@@ -1936,6 +1936,7 @@ export function renderApp(state: AppViewState) {
totalSignalCount: state.dreamingStatus?.totalSignalCount ?? 0,
promotedCount: state.dreamingStatus?.promotedToday ?? 0,
phaseSignalCount: state.dreamingStatus?.phaseSignalCount ?? 0,
phases: state.dreamingStatus?.phases ?? undefined,
shortTermEntries: state.dreamingStatus?.shortTermEntries ?? [],
signalEntries: state.dreamingStatus?.signalEntries ?? [],
promotedEntries: state.dreamingStatus?.promotedEntries ?? [],

View File

@@ -255,6 +255,12 @@ function renderDiaryNavigator(
`;
}
type DreamingPhaseInfo = {
enabled: boolean;
cron: string;
nextRunAtMs?: number;
};
export type DreamingProps = {
active: boolean;
shortTermCount: number;
@@ -262,6 +268,11 @@ export type DreamingProps = {
totalSignalCount: number;
promotedCount: number;
phaseSignalCount: number;
phases?: {
light: DreamingPhaseInfo;
deep: DreamingPhaseInfo;
rem: DreamingPhaseInfo;
};
shortTermEntries: {
key: string;
path: string;
@@ -474,8 +485,40 @@ export function renderDreaming(props: DreamingProps) {
// ── Scene renderer ────────────────────────────────────────────────────
// Strip source citations like [memory/2026-04-09.md:9] and section headings,
// flatten structured diary entries into plain paragraphs.
function flattenDiaryBody(body: string): string[] {
return body
.split("\n")
.map((line) => line.trim())
// Remove section headings that leak implementation
.filter(
(line) =>
line.length > 0 &&
line !== "What Happened" &&
line !== "Reflections" &&
line !== "Candidates" &&
line !== "Possible Lasting Updates",
)
// Strip source citations [memory/...]
.map((line) => line.replace(/\s*\[memory\/[^\]]+\]/g, ""))
// Strip leading list markers and labels
.map((line) =>
line
.replace(/^(?:\d+\.\s+|-\s+(?:\[[^\]]+\]\s+)?(?:[a-z_]+:\s+)?)/i, "")
.replace(/^(?:likely_durable|likely_situational|unclear):\s+/i, "")
.trim(),
)
.filter((line) => line.length > 0);
}
function formatPhaseNextRun(nextRunAtMs?: number): string {
if (!nextRunAtMs) {return "—";}
const d = new Date(nextRunAtMs);
return d.toLocaleTimeString([], { hour: "numeric", minute: "2-digit" });
}
function renderScene(props: DreamingProps, idle: boolean, dreamText: string) {
const groundedEntries = props.shortTermEntries.filter((entry) => entry.groundedCount > 0);
return html`
<section class="dreams ${idle ? "dreams--idle" : ""}">
${STARS.map(
@@ -534,6 +577,24 @@ function renderScene(props: DreamingProps, idle: boolean, dreamText: string) {
</div>
</div>
<!-- Sleep phases -->
<div class="dreams__phases">
${["light", "deep", "rem"].map((phaseId) => {
const phase = props.phases?.[phaseId as keyof NonNullable<typeof props.phases>];
const enabled = phase?.enabled ?? false;
const nextRun = formatPhaseNextRun(phase?.nextRunAtMs);
const label = phaseId.charAt(0).toUpperCase() + phaseId.slice(1);
return html`
<div class="dreams__phase ${enabled ? "" : "dreams__phase--off"}">
<div class="dreams__phase-dot ${enabled ? "dreams__phase-dot--on" : ""}"></div>
<span class="dreams__phase-name">${label}</span>
<span class="dreams__phase-next">${enabled ? nextRun : "off"}</span>
</div>
`;
})}
</div>
<!-- Actions -->
<div class="dreams__actions">
<button
class="btn btn--subtle btn--sm"
@@ -551,106 +612,6 @@ function renderScene(props: DreamingProps, idle: boolean, dreamText: string) {
>
${t("dreaming.scene.reset")}
</button>
<button
class="btn btn--subtle btn--sm"
?disabled=${props.modeSaving || props.dreamDiaryActionLoading}
@click=${() => props.onResetGroundedShortTerm()}
>
${t("dreaming.scene.clearGrounded")}
</button>
</div>
<div class="dreams__stats">
<div class="dreams__stat">
<span class="dreams__stat-value" style="color: var(--text-strong);"
>${props.shortTermCount}</span
>
<span class="dreams__stat-label">${t("dreaming.stats.shortTerm")}</span>
</div>
<div class="dreams__stat-divider"></div>
<div class="dreams__stat">
<span class="dreams__stat-value" style="color: var(--accent-muted);"
>${props.groundedSignalCount}</span
>
<span class="dreams__stat-label">${t("dreaming.stats.grounded")}</span>
</div>
<div class="dreams__stat-divider"></div>
<div class="dreams__stat">
<span class="dreams__stat-value" style="color: var(--accent);"
>${props.totalSignalCount}</span
>
<span class="dreams__stat-label">${t("dreaming.stats.signals")}</span>
</div>
<div class="dreams__stat-divider"></div>
<div class="dreams__stat">
<span class="dreams__stat-value" style="color: var(--accent-2);"
>${props.promotedCount}</span
>
<span class="dreams__stat-label">${t("dreaming.stats.promoted")}</span>
</div>
</div>
<div class="dreams__trace">
${renderTraceSection("shortTerm", props.shortTermEntries, {
count: props.shortTermCount,
emptyKey: "dreaming.trace.emptyShortTerm",
meta: (entry) =>
[
entry.recallCount > 0
? `${entry.recallCount} recall${entry.recallCount === 1 ? "" : "s"}`
: null,
entry.dailyCount > 0 ? `${entry.dailyCount} daily` : null,
entry.groundedCount > 0 ? `${entry.groundedCount} grounded` : null,
entry.phaseHitCount > 0
? `${entry.phaseHitCount} phase hit${entry.phaseHitCount === 1 ? "" : "s"}`
: null,
]
.filter(Boolean)
.join(" · "),
})}
${renderTraceSection("grounded", groundedEntries, {
count: groundedEntries.length,
emptyKey: "dreaming.trace.emptyGrounded",
meta: (entry) =>
[
`${entry.groundedCount} grounded`,
entry.recallCount > 0
? `${entry.recallCount} recall${entry.recallCount === 1 ? "" : "s"}`
: null,
entry.dailyCount > 0 ? `${entry.dailyCount} daily` : null,
isGroundedLed(entry) ? t("dreaming.trace.groundedLed") : null,
]
.filter(Boolean)
.join(" · "),
})}
${renderTraceSection("signals", props.signalEntries, {
count: props.totalSignalCount,
emptyKey: "dreaming.trace.emptySignals",
meta: (entry) =>
[
`${entry.totalSignalCount} signal${entry.totalSignalCount === 1 ? "" : "s"}`,
entry.phaseHitCount > 0
? `${entry.phaseHitCount} phase hit${entry.phaseHitCount === 1 ? "" : "s"}`
: null,
]
.filter(Boolean)
.join(" · "),
})}
${renderTraceSection("promoted", props.promotedEntries, {
count: props.promotedCount,
emptyKey: "dreaming.trace.emptyPromoted",
meta: (entry) =>
[
entry.promotedAt ? formatCompactDateTime(entry.promotedAt) : null,
entry.groundedCount > 0 ? `${entry.groundedCount} grounded` : null,
isGroundedLed(entry) ? t("dreaming.trace.groundedLed") : null,
entry.totalSignalCount > 0
? `${entry.totalSignalCount} signal${entry.totalSignalCount === 1 ? "" : "s"} before promote`
: null,
]
.filter(Boolean)
.join(" · "),
})}
</div>
${props.statusError
@@ -791,31 +752,6 @@ function renderDiarySection(props: DreamingProps) {
<section class="dreams-diary">
<div class="dreams-diary__header">
<span class="dreams-diary__title">${t("dreaming.diary.title")}</span>
<div class="dreams-diary__nav">
<button
class="dreams-diary__nav-btn"
?disabled=${!hasNext}
@click=${() => {
setDiaryPage(page + 1);
props.onRequestUpdate?.();
}}
title=${t("dreaming.diary.older")}
>
</button>
<span class="dreams-diary__page">${page + 1} / ${reversed.length}</span>
<button
class="dreams-diary__nav-btn"
?disabled=${!hasPrev}
@click=${() => {
setDiaryPage(page - 1);
props.onRequestUpdate?.();
}}
title=${t("dreaming.diary.newer")}
>
</button>
</div>
<button
class="btn btn--subtle btn--sm"
?disabled=${props.modeSaving || props.dreamDiaryLoading}
@@ -828,109 +764,39 @@ function renderDiarySection(props: DreamingProps) {
</button>
</div>
<div class="dreams-diary__navigator">
<div class="dreams-diary__navigator-content">
${renderDiaryNavigator(reversed, page, props.onRequestUpdate)}
</div>
<!-- Simple day chips -->
<div class="dreams-diary__daychips">
${reversed.map(
(e, i) => html`
<button
class="dreams-diary__day-chip ${e.page === page
? "dreams-diary__day-chip--active"
: ""}"
@click=${() => {
setDiaryPage(e.page);
props.onRequestUpdate?.();
}}
>
${formatDiaryChipLabel(e.date)}
</button>
`,
)}
</div>
<article
class="dreams-diary__entry ${structured ? "dreams-diary__entry--structured" : ""}"
key="${page}"
>
<article class="dreams-diary__entry" key="${page}">
<div class="dreams-diary__accent"></div>
${entry.date ? html`<time class="dreams-diary__date">${entry.date}</time>` : nothing}
${structured
? html`
<div class="dreams-diary__grid">
<section class="dreams-diary__panel">
<h3 class="dreams-diary__panel-title">What Happened</h3>
<div class="dreams-diary__panel-list dreams-diary__panel-list--points">
${structured.whatHappened.map(
(item, i) => html`
<div
class="dreams-diary__point"
style="animation-delay: ${0.2 + i * 0.06}s;"
>
<span class="dreams-diary__point-bullet"></span>
<p class="dreams-diary__item">${item}</p>
</div>
`,
)}
</div>
</section>
<section class="dreams-diary__panel">
<h3 class="dreams-diary__panel-title">Reflections</h3>
<div class="dreams-diary__panel-list dreams-diary__panel-list--points">
${structured.reflections.map(
(item, i) => html`
<div
class="dreams-diary__point"
style="animation-delay: ${0.26 + i * 0.06}s;"
>
<span class="dreams-diary__point-bullet"></span>
<p class="dreams-diary__item dreams-diary__item--reflection">${item}</p>
</div>
`,
)}
</div>
</section>
<section class="dreams-diary__panel">
<h3 class="dreams-diary__panel-title">Candidates + Possible Lasting Updates</h3>
${structured.candidates.length > 0
? html`
<div class="dreams-diary__panel-subtitle">Candidates</div>
<div class="dreams-diary__panel-list dreams-diary__panel-list--points">
${structured.candidates.map(
(item, i) => html`
<div
class="dreams-diary__point"
style="animation-delay: ${0.32 + i * 0.06}s;"
>
<span class="dreams-diary__point-bullet"></span>
<p class="dreams-diary__item">${item}</p>
</div>
`,
)}
</div>
`
: nothing}
${structured.lastingUpdates.length > 0
? html`
<div class="dreams-diary__panel-subtitle">Possible Lasting Updates</div>
<div class="dreams-diary__panel-list dreams-diary__panel-list--points">
${structured.lastingUpdates.map(
(item, i) => html`
<div
class="dreams-diary__point"
style="animation-delay: ${0.38 + i * 0.06}s;"
>
<span class="dreams-diary__point-bullet"></span>
<p class="dreams-diary__item dreams-diary__item--update">${item}</p>
</div>
`,
)}
</div>
`
: nothing}
</section>
</div>
`
: html`
<div class="dreams-diary__prose">
${entry.body
.split("\n")
.map(
(para, i) =>
html`<p
class="dreams-diary__para"
style="animation-delay: ${0.3 + i * 0.15}s;"
>
${para}
</p>`,
)}
</div>
`}
<div class="dreams-diary__prose">
${flattenDiaryBody(entry.body).map(
(para, i) =>
html`<p
class="dreams-diary__para"
style="animation-delay: ${0.3 + i * 0.15}s;"
>
${para}
</p>`,
)}
</div>
</article>
</section>
`;