feat(skills/live-artifact): add 7 example dashboards + contract demo (#716)

* feat(skills/live-artifact): add 7 example dashboards + contract demo

Seven self-contained HTML prototypes under skills/live-artifact/examples/,
each with a distinct visual identity and built-in interactivity for video
demos:

  stock-dashboard.html     - Bloomberg-style trading floor (dark)
  crypto-dashboard.html    - DeFi/web3 cyber terminal with on-chain ribbon
  crm-table-live.html      - multi-dim CRM with Grid/Kanban/Gallery/Calendar
                             view switcher (light productivity)
  monday-operator-live.html - editorial Monday-morning briefing (paper)
  competitor-radar-live.html - mission-control radar with rotating sweep
                             and RGB threat tiers
  baby-health-live.html    - soft pastel parental panel
  stock-portfolio-live/    - full live-artifact contract example: 102
                             escaped html_template_v1 bindings + 7
                             data-od-repeat blocks, ready to register
                             via 'tools live-artifacts create'

Each interactive HTML carries refresh-with-flash, view switching, AI
panel regeneration, clickable rows/cards that mutate state, and toast
notifications. Self-contained - only Google Fonts as external dep.

stock-portfolio-live/ demonstrates the daemon contract: template.html +
data.json + artifact.json + provenance.json. Refresh runners can rewrite
data.json without re-authoring the template.

* fix(skills/live-artifact): address PR #716 review feedback

- Unroll data-od-repeat blocks into indexed data.* bindings so renderHtmlTemplateV1 can interpolate them (it does not expand data-od-repeat or repeat-local aliases like {{t.label}}).
- Rename catalysts[].body to catalysts[].text to satisfy the bounded JSON validator's forbidden-key list (body is rejected case-insensitively); update template binding accordingly.

Generated-By: looper 0.6.1 (runner=fixer, agent=claude-code)

* fix(skills/live-artifact): make stock-portfolio provenance.json contract-compliant

- generatedBy: free-form string -> "agent" (LiveArtifactProvenanceGenerator enum)
- sources[].kind -> sources[].type with LiveArtifactProvenanceSourceType enum values
  (connector for brokerage/quotes connectors, derived for AI recommendation)
- Drop non-contract per-source `note` and top-level `summary`/`transformations`/
  `refreshContract`/`safetyNotes` fields; preserve their content under the
  contract-allowed `notes` field so the example survives schema validation.

Generated-By: looper 0.6.1 (runner=fixer, agent=claude-code)

* fix(skills/live-artifact): use strict ISO-8601 generatedAt in provenance

The daemon's `validateIsoDate` requires `Date.toISOString()` round-trip
equality, so timezone-offset notation like `2026-05-06T14:32:18-05:00`
fails validation even though it parses. Switch to the canonical UTC form
`2026-05-06T19:32:18.000Z` (same instant), which the validator accepts.

* feat(skills): surface examples/*.html as derived skill cards + Live filter

A skill that ships hand-crafted samples under examples/*.html (e.g.
live-artifact's stock dashboard, baby health monitor) now lights up one
gallery card per file instead of a single parent card whose preview can
only ever show one of them. The parent stays in the listing tagged
aggregatesExamples=true so findSkillById and Use this prompt still
resolve back to its SKILL.md body, but the Examples tab hides it so the
derived <parent>:<child> cards aren't shadowed by a duplicate preview.

Subfolder layouts (examples/<name>/template.html + data.json) are
deliberately skipped — their templates still hold {{data.x}}
placeholders that only the daemon-side renderer fills in, so showing
the raw template would render visible braces in the gallery. Ship the
baked output as examples/<name>.html alongside the folder to surface it.

Adds an examples.modeLive filter pill (translated across all 21 locales)
that selects skill.scenario === 'live', so refreshable / connector-backed
samples are easy to find without scrolling through every desktop
prototype. live-artifact's SKILL.md gains scenario: live so it (and
every derived card) lights up there.

Co-authored-by: Cursor <cursoragent@cursor.com>

* perf(web): parallelize entry-view bootstrap so each tab renders independently

Bootstrap used to wait on a single Promise.all behind a global
'Loading workspace…' placeholder, which made the slowest endpoint
(typically /api/agents on cold start, since it probes CLI versions)
gate every tab including the ones that don't need agents at all.

Splits the global bootstrapping flag into per-resource loading flags
(agentsLoading, skillsLoading, dsLoading, projectsLoading,
promptTemplatesLoading) plus a daemonConfigLoaded flag for the merged
daemon config. Each tab now blocks only on the data it actually needs:
Examples renders as soon as skills land, Design Systems on dsList,
Designs on projects+skills+designSystems, etc.

Auto-selecting the first available agent and the default design system
moves into dedicated effects gated on daemonConfigLoaded so they no
longer race ahead of the daemon-stored choice and overwrite it with a
freshly picked first-available pick.

EntryView swaps its single loading prop for skillsLoading,
designSystemsLoading, projectsLoading, promptTemplatesLoading so each
inner tab can pick the right gate without leaking the parent's coarse
state.

Co-authored-by: Cursor <cursoragent@cursor.com>

---------

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Tom Huang 2026-05-08 17:38:29 +08:00 committed by GitHub
parent 9ed4ea1263
commit 1d1df52f3b
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
35 changed files with 10488 additions and 142 deletions

View file

@ -30,7 +30,7 @@ import {
spawnEnvForAgent,
} from './agents.js';
import { migrateLegacyDataDirSync } from './legacy-data-migrator.js';
import { findSkillById, listSkills } from './skills.js';
import { findSkillById, listSkills, splitDerivedSkillId } from './skills.js';
import { validateLinkedDirs } from './linked-dirs.js';
import { buildWindowsFolderDialogCommand, parseFolderDialogStdout } from './native-folder-dialog.js';
import { listCodexPets, readCodexPetSpritesheet } from './codex-pets.js';
@ -2663,18 +2663,56 @@ export async function startServer({ port = 7456, host = process.env.OD_BIND_HOST
// so we resolve the actual directory via listSkills() rather than guessing.
//
// Resolution order:
// 1. <skillDir>/example.html — fully-baked static example (preferred)
// 2. <skillDir>/assets/template.html +
// 1. Derived id (`<parent>:<child>`):
// <parentDir>/examples/<child>.html — pre-baked single-file sample.
// Subfolder layouts (e.g. live-artifact's
// `examples/<name>/template.html`) are intentionally not served:
// they still contain `{{data.x}}` placeholders that only the
// daemon-side renderer fills in, and serving the raw template
// would render visible placeholder braces in the gallery.
// 2. <skillDir>/example.html — fully-baked static example (preferred)
// 3. <skillDir>/assets/template.html +
// <skillDir>/assets/example-slides.html — assemble at request time
// by replacing the `<!-- SLIDES_HERE -->` marker with the snippet
// and patching the placeholder <title>. Lets a skill ship one
// canonical seed plus a small content fragment, so the example
// never drifts from the seed.
// 3. <skillDir>/assets/template.html — raw template, no content slides
// 4. <skillDir>/assets/index.html — generic fallback
// 4. <skillDir>/assets/template.html — raw template, no content slides
// 5. <skillDir>/assets/index.html — generic fallback
// 6. First .html in <skillDir>/examples/ — used as a friendly fallback
// so a skill that aggregates examples (like live-artifact) still has
// a real preview on its parent card instead of returning 404.
app.get('/api/skills/:id/example', async (req, res) => {
try {
const skills = await listSkills(SKILLS_DIR);
// 1. Derived `<parent>:<child>` id — resolve straight to the matching
// file under <parentDir>/examples/. Done before findSkillById so the
// parent's normal fallback chain never accidentally serves a stale
// file when a sample is missing (we'd rather 404 explicitly).
const derived = splitDerivedSkillId(req.params.id);
if (derived) {
const parent = findSkillById(skills, derived.parentId);
if (!parent) {
return res.status(404).type('text/plain').send('skill not found');
}
const candidate = path.join(
parent.dir,
'examples',
`${derived.childKey}.html`,
);
if (fs.existsSync(candidate)) {
const html = await fs.promises.readFile(candidate, 'utf8');
return res
.type('text/html')
.send(rewriteSkillAssetUrls(html, parent.id));
}
return res
.status(404)
.type('text/plain')
.send('derived example not found');
}
const skill = findSkillById(skills, req.params.id);
if (!skill) {
return res.status(404).type('text/plain').send('skill not found');
@ -2715,11 +2753,44 @@ export async function startServer({ port = 7456, host = process.env.OD_BIND_HOST
.type('text/html')
.send(rewriteSkillAssetUrls(html, skill.id));
}
// Friendly fallback for skills that aggregate examples in a sibling
// `examples/` folder (e.g. live-artifact). The parent card would
// otherwise 404 even though plenty of perfectly valid samples ship
// alongside SKILL.md; pick the first .html file alphabetically so
// direct URL access (e.g. deep links) shows something representative.
// Subfolder layouts are excluded for the same reason as the derived
// resolver above — their `template.html` still has unresolved
// `{{data.x}}` placeholders.
const examplesDir = path.join(skill.dir, 'examples');
if (fs.existsSync(examplesDir)) {
let entries: string[] = [];
try {
entries = await fs.promises.readdir(examplesDir);
} catch {
entries = [];
}
entries.sort();
for (const name of entries) {
if (name.startsWith('.')) continue;
if (!name.toLowerCase().endsWith('.html')) continue;
const direct = path.join(examplesDir, name);
try {
const html = await fs.promises.readFile(direct, 'utf8');
return res
.type('text/html')
.send(rewriteSkillAssetUrls(html, skill.id));
} catch {
continue;
}
}
}
res
.status(404)
.type('text/plain')
.send(
'no example.html, assets/template.html, or assets/index.html for this skill',
'no example.html, assets/template.html, assets/index.html, or examples/*.html for this skill',
);
} catch (err) {
res.status(500).type('text/plain').send(String(err));

View file

@ -58,26 +58,45 @@ export async function listSkills(skillsRoot) {
const hasAttachments = await dirHasAttachments(dir);
const mode = data.od?.mode || inferMode(body, data.description);
const surface = normalizeSurface(data.od?.surface, mode);
const platform = normalizePlatform(
data.od?.platform,
mode,
body,
data.description
);
const scenario = normalizeScenario(
data.od?.scenario,
body,
data.description
);
const designSystemRequired = data.od?.design_system?.requires ?? true;
const upstream =
typeof data.od?.upstream === "string" ? data.od.upstream : null;
const previewType = data.od?.preview?.type || "html";
const parentId = data.name || entry.name;
const parentBody = hasAttachments ? withSkillRootPreamble(body, dir) : body;
// Pre-compute derived examples so the parent entry can advertise
// `aggregatesExamples` in the same push. The frontend uses that
// flag to hide the parent card from the gallery (its preview would
// duplicate one of the derived cards), while the daemon keeps the
// parent in the listing so `findSkillById` still resolves it for
// system-prompt composition and id alias lookups.
const derivedExamples = await collectDerivedExamples(dir);
const aggregatesExamples = derivedExamples.length > 0;
out.push({
id: data.name || entry.name,
name: data.name || entry.name,
id: parentId,
name: parentId,
description: data.description || "",
triggers: Array.isArray(data.triggers) ? data.triggers : [],
mode,
surface,
craftRequires: normalizeCraftRequires(data.od?.craft?.requires),
platform: normalizePlatform(
data.od?.platform,
mode,
body,
data.description
),
scenario: normalizeScenario(data.od?.scenario, body, data.description),
previewType: data.od?.preview?.type || "html",
designSystemRequired: data.od?.design_system?.requires ?? true,
platform,
scenario,
previewType,
designSystemRequired,
defaultFor: normalizeDefaultFor(data.od?.default_for),
upstream:
typeof data.od?.upstream === "string" ? data.od.upstream : null,
upstream,
featured: normalizeFeatured(data.od?.featured),
// Optional metadata hints used by 'Use this prompt' fast-create so
// the resulting project mirrors the shipped example.html. Each hint
@ -87,9 +106,49 @@ export async function listSkills(skillsRoot) {
speakerNotes: normalizeBoolHint(data.od?.speaker_notes),
animations: normalizeBoolHint(data.od?.animations),
examplePrompt: derivePrompt(data),
body: hasAttachments ? withSkillRootPreamble(body, dir) : body,
aggregatesExamples,
body: parentBody,
dir,
});
// Surface every example sitting next to a SKILL.md as its own card so
// a single skill (e.g. live-artifact) can ship a small gallery of
// hand-crafted samples without needing one SKILL.md per sample. Each
// derived card inherits the parent's mode/platform/surface/scenario
// so existing TYPE/SURFACE filters keep working; the synthetic id
// `<parent>:<child>` lets `/api/skills/:id/example` resolve straight
// to the matching HTML on disk. We deliberately do not inherit
// `featured` so derived cards never crowd the magazine row.
for (const example of derivedExamples) {
out.push({
id: `${parentId}:${example.key}`,
name: humanizeExampleName(example.key),
description: data.description || "",
triggers: Array.isArray(data.triggers) ? data.triggers : [],
mode,
surface,
craftRequires: [],
platform,
scenario,
previewType,
designSystemRequired,
defaultFor: [],
upstream,
featured: null,
fidelity: normalizeFidelity(data.od?.fidelity),
speakerNotes: normalizeBoolHint(data.od?.speaker_notes),
animations: normalizeBoolHint(data.od?.animations),
examplePrompt: derivePrompt(data),
aggregatesExamples: false,
// Inherit the parent's full SKILL.md body so 'Use this prompt'
// on a derived card seeds the agent with the same workflow the
// parent describes. Without this, picking a derived card would
// compose an empty system prompt and the agent would have no
// skill instructions.
body: parentBody,
dir,
});
}
} catch {
// Skip unreadable entries — this is discovery, not validation.
}
@ -97,6 +156,87 @@ export async function listSkills(skillsRoot) {
return out;
}
// Discover example artifacts that live alongside SKILL.md under
// `<dir>/examples/`. Only the single-file layout is surfaced:
//
// `examples/<name>.html` — pre-baked, self-contained sample.
//
// We deliberately do not surface the subfolder layout (e.g. live-artifact's
// `examples/<name>/template.html` + `data.json`) because those templates
// still hold `{{data.x}}` placeholders that only the daemon-side renderer
// fills in. Showing the raw template would render visible placeholder
// braces in the gallery — worse than not surfacing the example at all.
// To ship a subfolder-style example, place the baked output beside the
// folder as `examples/<name>.html` (the canonical render) and keep the
// subfolder around as agent-readable source.
async function collectDerivedExamples(dir) {
const examplesDir = path.join(dir, "examples");
let entries = [];
try {
entries = await readdir(examplesDir, { withFileTypes: true });
} catch {
return [];
}
const out = [];
for (const entry of entries) {
if (!entry.isFile()) continue;
if (!entry.name.toLowerCase().endsWith(".html")) continue;
const key = entry.name.replace(/\.html$/i, "");
if (!isSafeExampleKey(key)) continue;
out.push({ key });
}
// Stable order so the gallery renders the same sequence on every reload.
out.sort((a, b) => a.key.localeCompare(b.key));
return out;
}
// Reject keys that could escape the examples folder or break the
// `<parent>:<child>` id format. Letters/digits/dash/dot/underscore only,
// and never the dotfile path-traversal patterns.
function isSafeExampleKey(key) {
if (!key || key.startsWith(".")) return false;
if (key.includes(":")) return false;
return /^[A-Za-z0-9._-]+$/.test(key);
}
// Turn a basename like `stock-portfolio-live` into a title-cased label
// (`Stock Portfolio Live`) so the gallery card has a readable heading
// without forcing every example to ship its own frontmatter.
function humanizeExampleName(key) {
return key
.replace(/[-_]+/g, " ")
.replace(/\s+/g, " ")
.trim()
.split(" ")
.map((word) =>
word.length === 0
? word
: word.charAt(0).toUpperCase() + word.slice(1).toLowerCase()
)
.join(" ");
}
// Used by `/api/skills/:id/example` to resolve a derived id back to its
// on-disk file. Returns null when the key is unsafe; the route checks
// `fs.existsSync` against the returned path before reading.
export function resolveDerivedExamplePath(parentDir, childKey) {
if (!isSafeExampleKey(childKey)) return null;
return path.join(parentDir, "examples", `${childKey}.html`);
}
// Split a `<parent>:<child>` synthetic id into its two halves. Returns
// null for non-derived ids so the caller can fall through to the regular
// listing-based lookup.
export function splitDerivedSkillId(id) {
if (typeof id !== "string") return null;
const idx = id.indexOf(":");
if (idx <= 0 || idx === id.length - 1) return null;
const parentId = id.slice(0, idx);
const childKey = id.slice(idx + 1);
if (!isSafeExampleKey(childKey)) return null;
return { parentId, childKey };
}
// Skills that ship side files (e.g. `assets/template.html`, `references/*.md`)
// need the agent to know where the skill lives on disk — relative paths in the
// SKILL.md body would otherwise resolve against the agent's CWD, which is the

View file

@ -127,10 +127,21 @@ export function App() {
const [appVersionInfo, setAppVersionInfo] = useState<AppVersionInfo | null>(
null,
);
// Goes false once the bootstrap effect has finished its initial round of
// fetches. The entry view uses this to show shimmer / skeleton states
// instead of an "empty" page that flickers before data lands.
const [bootstrapping, setBootstrapping] = useState(true);
// Per-resource loading flags. Each goes false the moment its own fetch
// resolves so each entry-view tab can render as its data lands instead of
// every tab waiting on the slowest endpoint (typically `/api/agents`,
// which probes CLI versions and can take seconds on cold start). The entry
// view picks the right flag for whichever tab the user is currently on.
const [agentsLoading, setAgentsLoading] = useState(true);
const [skillsLoading, setSkillsLoading] = useState(true);
const [dsLoading, setDsLoading] = useState(true);
const [projectsLoading, setProjectsLoading] = useState(true);
const [promptTemplatesLoading, setPromptTemplatesLoading] = useState(true);
// Goes true once the daemon-persisted config (agentId/designSystemId/etc.)
// has merged into local state. Auto-selection effects below wait on this
// so they don't race ahead of the daemon-stored choice and overwrite it
// with a freshly picked first-available agent.
const [daemonConfigLoaded, setDaemonConfigLoaded] = useState(false);
// Narrower flag dedicated to the Composio API key hydration. The key is
// persisted by the daemon (and only reflected back via apiKeyConfigured
// + apiKeyTail), so after a dev-server restart there is a window where
@ -173,100 +184,158 @@ export function App() {
});
}, [activeProjectId, activeFileName]);
// Bootstrap — detect daemon, load pickers, seed sensible defaults.
// Bootstrap — detect daemon, then fan out independent fetches so each
// entry-view tab can render the moment its own data lands. Earlier this
// was one Promise.all behind a global "Loading workspace…" placeholder,
// which made the slowest endpoint (typically `/api/agents` on cold start)
// gate every tab including the ones that don't need agents at all.
useEffect(() => {
let cancelled = false;
(async () => {
const alive = await daemonIsLive();
if (cancelled) return;
setDaemonLive(alive);
const [
agentList,
skillList,
dsList,
projectList,
templateList,
promptTemplateList,
versionInfo,
daemonConfig,
daemonComposioConfig,
] = await Promise.all([
alive ? fetchAgents() : Promise.resolve([] as AgentInfo[]),
alive ? fetchSkills() : Promise.resolve([] as SkillSummary[]),
alive
? fetchDesignSystems()
: Promise.resolve([] as DesignSystemSummary[]),
alive ? listProjects() : Promise.resolve([] as Project[]),
alive ? listTemplates() : Promise.resolve([] as ProjectTemplate[]),
alive
? fetchPromptTemplates()
: Promise.resolve([] as PromptTemplateSummary[]),
alive ? fetchAppVersionInfo() : Promise.resolve(null),
alive ? fetchDaemonConfig() : Promise.resolve(null),
alive ? fetchComposioConfigFromDaemon() : Promise.resolve(null),
]);
if (cancelled) return;
setAgents(agentList);
setSkills(skillList);
setDesignSystems(dsList);
setProjects(projectList);
setTemplates(templateList);
setPromptTemplates(promptTemplateList);
setAppVersionInfo(versionInfo);
setConfig((prev) => {
// Merge daemon-persisted config — daemon values win for the fields
// it tracks so that the choice survives origin/storage resets.
const next = mergeDaemonConfig(prev, daemonConfig);
if (!alive) {
// No daemon — clear every loading flag so empty states render
// instead of the entry view sitting on indefinite spinners.
setAgentsLoading(false);
setSkillsLoading(false);
setDsLoading(false);
setProjectsLoading(false);
setPromptTemplatesLoading(false);
setDaemonConfigLoaded(true);
// Composio hydration also depends on the daemon. With no daemon
// we just keep whatever localStorage already held; drop the
// skeleton so the Settings → Connectors input reflects state.
setComposioConfigLoading(false);
return;
}
if (alive) {
void fetchAgents().then((list) => {
if (cancelled) return;
setAgents(list);
setAgentsLoading(false);
});
void fetchSkills().then((list) => {
if (cancelled) return;
setSkills(list);
setSkillsLoading(false);
});
void fetchDesignSystems().then((list) => {
if (cancelled) return;
setDesignSystems(list);
setDsLoading(false);
});
void listProjects().then((list) => {
if (cancelled) return;
setProjects(list);
setProjectsLoading(false);
});
void listTemplates().then((list) => {
if (cancelled) return;
setTemplates(list);
});
void fetchPromptTemplates().then((list) => {
if (cancelled) return;
setPromptTemplates(list);
setPromptTemplatesLoading(false);
});
void fetchAppVersionInfo().then((info) => {
if (cancelled) return;
setAppVersionInfo(info);
});
// Daemon-persisted config + composio config land together so the
// welcome-modal decision and the daemon-side composio key both apply
// in one merge, avoiding a flash where local-only state is shown
// before daemon overrides it.
void Promise.all([
fetchDaemonConfig(),
fetchComposioConfigFromDaemon(),
]).then(([daemonConfig, daemonComposioConfig]) => {
if (cancelled) return;
setConfig((prev) => {
const next = mergeDaemonConfig(prev, daemonConfig);
const hasLocalComposioKey = Boolean(next.composio?.apiKey?.trim());
if (!hasLocalComposioKey && daemonComposioConfig) {
next.composio = daemonComposioConfig;
}
if (!next.agentId) {
const firstAvailable = agentList.find((a) => a.available);
if (firstAvailable) next.agentId = firstAvailable.id;
saveConfig(next);
if (hasAnyConfiguredProvider(next.mediaProviders)) {
void syncMediaProvidersToDaemon(next.mediaProviders);
}
if (!next.designSystemId && dsList.length > 0) {
next.designSystemId =
dsList.find((d) => d.id === 'default')?.id ?? dsList[0]!.id;
}
}
saveConfig(next);
if (alive && hasAnyConfiguredProvider(next.mediaProviders)) {
void syncMediaProvidersToDaemon(next.mediaProviders);
}
// Migrate localStorage prefs to daemon on first boot with the new
// endpoint. If daemon already had values the merge above used them;
// writing back is idempotent and ensures both sides stay in sync.
if (alive) {
// Migrate localStorage prefs to daemon on first boot with the new
// endpoint. If daemon already had values the merge above used
// them; writing back is idempotent and keeps both sides in sync.
void syncConfigToDaemon(next);
void syncComposioConfigToDaemon(next.composio);
}
// Pop the onboarding modal only on the first run. Once the user has
// saved or skipped past it once, we trust their stored config and
// let them re-open Settings explicitly via the env pill.
if (!next.onboardingCompleted) {
setSettingsWelcome(true);
setSettingsOpen(true);
}
return next;
// Pop the onboarding modal only on the first run. Once the user
// has saved or skipped past it once, we trust their stored config
// and let them re-open Settings explicitly via the env pill.
if (!next.onboardingCompleted) {
setSettingsWelcome(true);
setSettingsOpen(true);
}
return next;
});
setDaemonConfigLoaded(true);
// Composio key hydration is part of this same daemon-config
// fetch — by the time we land here the daemon has either
// returned the saved-key shape (apiKeyConfigured + tail) or
// it errored and we kept whatever localStorage held. Either
// way it is safe to drop the skeleton.
setComposioConfigLoading(false);
});
setBootstrapping(false);
// Composio hydration is part of the same Promise.all above — by the
// time we land here either the daemon returned the saved-key shape
// (apiKeyConfigured + tail) or the daemon was offline and we kept
// whatever localStorage held. Either way it is safe to drop the
// skeleton: the input now reflects the source of truth.
setComposioConfigLoading(false);
})();
return () => {
cancelled = true;
};
}, []);
// Auto-pick the first available agent once both the daemon-stored config
// and the agents listing have landed. Splitting this out of bootstrap
// avoids racing the local-config initial value against a slow agents
// probe — by the time this runs, daemonConfig has already overlaid the
// user's previous choice, so we only fill an empty slot.
useEffect(() => {
if (!daemonConfigLoaded || agentsLoading) return;
if (config.agentId) return;
const firstAvailable = agents.find((a) => a.available);
if (!firstAvailable) return;
setConfig((prev) => {
if (prev.agentId) return prev;
const next: AppConfig = { ...prev, agentId: firstAvailable.id };
saveConfig(next);
void syncConfigToDaemon(next);
return next;
});
}, [daemonConfigLoaded, agentsLoading, agents, config.agentId]);
// Auto-pick the default design system the same way — only after daemon
// config has merged so we never overwrite a daemon-stored selection.
useEffect(() => {
if (!daemonConfigLoaded || dsLoading) return;
if (config.designSystemId) return;
if (designSystems.length === 0) return;
const id =
designSystems.find((d) => d.id === 'default')?.id ?? designSystems[0]!.id;
setConfig((prev) => {
if (prev.designSystemId) return prev;
const next: AppConfig = { ...prev, designSystemId: id };
saveConfig(next);
void syncConfigToDaemon(next);
return next;
});
}, [daemonConfigLoaded, dsLoading, designSystems, config.designSystemId]);
// One-shot self-healing migration for pets adopted before the
// overlay learned atlas-row switching. If the stored pet is a
// custom / codex pet whose imageUrl is a single-row strip
@ -658,7 +727,10 @@ export function App() {
defaultDesignSystemId={config.designSystemId}
config={config}
agents={agents}
loading={bootstrapping}
skillsLoading={skillsLoading}
designSystemsLoading={dsLoading}
projectsLoading={projectsLoading}
promptTemplatesLoading={promptTemplatesLoading}
onCreateProject={handleCreateProject}
onImportClaudeDesign={handleImportClaudeDesign}
onImportFolder={handleImportFolder}

View file

@ -46,7 +46,15 @@ interface Props {
defaultDesignSystemId: string | null;
config: AppConfig;
agents: AgentInfo[];
loading?: boolean;
// Per-resource loading flags. Each tab gates its own content on whichever
// flag matches the data it renders, so a slow `/api/agents` probe does
// not block tabs that don't need agents. Templates are not gated here —
// the sidebar 'From template' tab renders an empty state until they
// arrive (fast fetch), which keeps the prop surface narrower.
skillsLoading?: boolean;
designSystemsLoading?: boolean;
projectsLoading?: boolean;
promptTemplatesLoading?: boolean;
onCreateProject: (input: CreateInput & { pendingPrompt?: string }) => void;
onImportClaudeDesign: (file: File) => Promise<void> | void;
onImportFolder?: (baseDir: string) => Promise<void> | void;
@ -213,7 +221,10 @@ export function EntryView({
defaultDesignSystemId,
config,
agents,
loading = false,
skillsLoading = false,
designSystemsLoading = false,
projectsLoading = false,
promptTemplatesLoading = false,
onCreateProject,
onImportClaudeDesign,
onImportFolder,
@ -469,7 +480,7 @@ export function EntryView({
connectors={connectors}
connectorsLoading={connectorsLoading}
onOpenConnectorsTab={() => onOpenSettings('composio')}
loading={loading}
loading={skillsLoading || designSystemsLoading}
/>
<div className="entry-side-foot">
<button
@ -565,47 +576,65 @@ export function EntryView({
</div>
</div>
<div className="entry-tab-content">
{loading ? (
<CenteredLoader label={t('entry.loadingWorkspace')} />
) : (
<>
{topTab === 'designs' ? (
<DesignsTab
projects={projects}
skills={skills}
designSystems={designSystems}
onOpen={onOpenProject}
onOpenLiveArtifact={onOpenLiveArtifact}
onDelete={onDeleteProject}
/>
) : null}
{topTab === 'examples' ? (
<ExamplesTab skills={skills} onUsePrompt={usePromptFromSkill} />
) : null}
{topTab === 'design-systems' ? (
<DesignSystemsTab
systems={designSystems}
selectedId={defaultDesignSystemId}
onSelect={onChangeDefaultDesignSystem}
onPreview={previewDesignSystem}
/>
) : null}
{topTab === 'image-templates' ? (
<PromptTemplatesTab
surface="image"
templates={promptTemplates}
onPreview={setPreviewPromptTemplate}
/>
) : null}
{topTab === 'video-templates' ? (
<PromptTemplatesTab
surface="video"
templates={promptTemplates}
onPreview={setPreviewPromptTemplate}
/>
) : null}
</>
)}
{topTab === 'designs' ? (
// DesignsTab uses skills + designSystems for tag rendering on
// each card, so wait until projects + that metadata are present
// to avoid a flash of "No projects yet" before the real list
// arrives.
projectsLoading || skillsLoading || designSystemsLoading ? (
<CenteredLoader label={t('common.loading')} />
) : (
<DesignsTab
projects={projects}
skills={skills}
designSystems={designSystems}
onOpen={onOpenProject}
onOpenLiveArtifact={onOpenLiveArtifact}
onDelete={onDeleteProject}
/>
)
) : null}
{topTab === 'examples' ? (
skillsLoading ? (
<CenteredLoader label={t('common.loading')} />
) : (
<ExamplesTab skills={skills} onUsePrompt={usePromptFromSkill} />
)
) : null}
{topTab === 'design-systems' ? (
designSystemsLoading ? (
<CenteredLoader label={t('common.loading')} />
) : (
<DesignSystemsTab
systems={designSystems}
selectedId={defaultDesignSystemId}
onSelect={onChangeDefaultDesignSystem}
onPreview={previewDesignSystem}
/>
)
) : null}
{topTab === 'image-templates' ? (
promptTemplatesLoading ? (
<CenteredLoader label={t('common.loading')} />
) : (
<PromptTemplatesTab
surface="image"
templates={promptTemplates}
onPreview={setPreviewPromptTemplate}
/>
)
) : null}
{topTab === 'video-templates' ? (
promptTemplatesLoading ? (
<CenteredLoader label={t('common.loading')} />
) : (
<PromptTemplatesTab
surface="video"
templates={promptTemplates}
onPreview={setPreviewPromptTemplate}
/>
)
) : null}
</div>
</main>
{petRailHidden ? null : (

View file

@ -19,7 +19,14 @@ interface Props {
onUsePrompt: (skill: SkillSummary) => void;
}
type ModeFilter = 'all' | 'prototype-desktop' | 'prototype-mobile' | 'deck' | 'document' | 'orbit';
type ModeFilter =
| 'all'
| 'prototype-desktop'
| 'prototype-mobile'
| 'deck'
| 'document'
| 'orbit'
| 'live';
type SurfaceFilter = 'all' | Surface;
type ScenarioFilter = string;
@ -38,6 +45,7 @@ const MODE_PILLS: { value: ModeFilter; labelKey: keyof Dict }[] = [
{ value: 'deck', labelKey: 'examples.modeDeck' },
{ value: 'document', labelKey: 'examples.modeDocument' },
{ value: 'orbit', labelKey: 'examples.modeOrbit' },
{ value: 'live', labelKey: 'examples.modeLive' },
];
const SCENARIO_LABEL_KEY: Record<string, keyof Dict> = {
@ -87,6 +95,12 @@ function matchesMode(skill: SkillSummary, filter: ModeFilter): boolean {
return skill.mode === 'prototype' && skill.platform === 'mobile';
if (filter === 'document') return skill.mode === 'template';
if (filter === 'orbit') return skill.scenario === 'orbit';
// Live artifacts ride on the prototype mode but want their own bucket so
// refreshable / connector-backed samples are easy to find without
// scrolling through every desktop prototype. The parent live-artifact
// skill and every derived `live-artifact:<example>` card share the
// `live` scenario, so they all light up here together.
if (filter === 'live') return skill.scenario === 'live';
return true;
}
@ -104,8 +118,18 @@ function quotePrompt(locale: string, text: string): string {
return locale === 'de' ? `${text}` : `${text}`;
}
export function ExamplesTab({ skills, onUsePrompt }: Props) {
export function ExamplesTab({ skills: rawSkills, onUsePrompt }: Props) {
const { locale, t } = useI18n();
// Skills tagged `aggregatesExamples: true` are containers whose preview
// would just duplicate one of their derived `<parent>:<child>` cards
// (e.g. live-artifact ships a sample gallery under `examples/`). Drop
// them up front so every count, filter, and rendered card downstream
// sees only the user-facing entries. The full listing is still passed
// through for `findSkillById` lookups elsewhere in the app.
const skills = useMemo(
() => rawSkills.filter((s) => !s.aggregatesExamples),
[rawSkills],
);
// Hold preview HTML per skill across re-renders so cards never re-flicker.
const [previews, setPreviews] = useState<Record<string, string | null>>({});
// Track per-skill fetch failures separately so the preview modal can show
@ -214,6 +238,7 @@ export function ExamplesTab({ skills, onUsePrompt }: Props) {
deck: 0,
document: 0,
orbit: 0,
live: 0,
};
for (const s of surfaceScoped) {
if (matchesMode(s, 'prototype-desktop')) c['prototype-desktop']++;
@ -221,6 +246,7 @@ export function ExamplesTab({ skills, onUsePrompt }: Props) {
if (matchesMode(s, 'deck')) c.deck++;
if (matchesMode(s, 'document')) c.document++;
if (matchesMode(s, 'orbit')) c.orbit++;
if (matchesMode(s, 'live')) c.live++;
}
return c;
}, [skills, surfaceFilter]);

View file

@ -407,6 +407,7 @@ export const ar: Dict = {
'examples.modeDeck': 'شرائح',
'examples.modeDocument': 'مستندات وقوالب',
'examples.modeOrbit': 'Orbit',
'examples.modeLive': 'مباشر',
'examples.scenarioGeneral': 'عام',
'examples.scenarioEngineering': 'هندسة',
'examples.scenarioProduct': 'منتج',

View file

@ -300,6 +300,7 @@ export const de: Dict = {
'examples.modeDeck': 'Folien',
'examples.modeDocument': 'Dokumente & Templates',
'examples.modeOrbit': 'Orbit',
'examples.modeLive': 'Live',
'examples.scenarioGeneral': 'Allgemein',
'examples.scenarioEngineering': 'Engineering',
'examples.scenarioProduct': 'Produkt',

View file

@ -418,6 +418,7 @@ export const en: Dict = {
'examples.modeDeck': 'Slides',
'examples.modeDocument': 'Docs & templates',
'examples.modeOrbit': 'Orbit',
'examples.modeLive': 'Live',
'examples.scenarioGeneral': 'General',
'examples.scenarioEngineering': 'Engineering',
'examples.scenarioProduct': 'Product',

View file

@ -301,6 +301,7 @@ export const esES: Dict = {
'examples.modeDeck': 'Diapositivas',
'examples.modeDocument': 'Documentos y plantillas',
'examples.modeOrbit': 'Orbit',
'examples.modeLive': 'En vivo',
'examples.scenarioGeneral': 'General',
'examples.scenarioEngineering': 'Ingeniería',
'examples.scenarioProduct': 'Producto',

View file

@ -418,6 +418,7 @@ export const fa: Dict = {
'examples.modeDeck': 'اسلایدها',
'examples.modeDocument': 'اسناد و قالب‌ها',
'examples.modeOrbit': 'Orbit',
'examples.modeLive': 'زنده',
'examples.scenarioGeneral': 'عمومی',
'examples.scenarioEngineering': 'مهندسی',
'examples.scenarioProduct': 'محصول',

View file

@ -407,6 +407,7 @@ export const fr: Dict = {
'examples.modeDeck': 'Diaporamas',
'examples.modeDocument': 'Docs et modèles',
'examples.modeOrbit': 'Orbit',
'examples.modeLive': 'Live',
'examples.scenarioGeneral': 'Général',
'examples.scenarioEngineering': 'Ingénierie',
'examples.scenarioProduct': 'Produit',

View file

@ -407,6 +407,7 @@ export const hu: Dict = {
'examples.modeDeck': 'Diák',
'examples.modeDocument': 'Dokumentumok és sablonok',
'examples.modeOrbit': 'Orbit',
'examples.modeLive': 'Élő',
'examples.scenarioGeneral': 'Általános',
'examples.scenarioEngineering': 'Mérnöki',
'examples.scenarioProduct': 'Termék',

View file

@ -511,6 +511,7 @@ export const id: Dict = {
'examples.modeDeck': 'Slide',
'examples.modeDocument': 'Dokumen & templat',
'examples.modeOrbit': 'Orbit',
'examples.modeLive': 'Langsung',
'examples.scenarioGeneral': 'Umum',
'examples.scenarioEngineering': 'Engineering',
'examples.scenarioProduct': 'Produk',

View file

@ -299,6 +299,7 @@ export const ja: Dict = {
'examples.modeDeck': 'スライド',
'examples.modeDocument': 'ドキュメント & テンプレート',
'examples.modeOrbit': 'Orbit',
'examples.modeLive': 'ライブ',
'examples.scenarioGeneral': '一般',
'examples.scenarioEngineering': 'エンジニアリング',
'examples.scenarioProduct': 'プロダクト',

View file

@ -407,6 +407,7 @@ export const ko: Dict = {
'examples.modeDeck': '슬라이드',
'examples.modeDocument': '문서 및 템플릿',
'examples.modeOrbit': 'Orbit',
'examples.modeLive': '라이브',
'examples.scenarioGeneral': '일반',
'examples.scenarioEngineering': '엔지니어링',
'examples.scenarioProduct': '제품',

View file

@ -407,6 +407,7 @@ export const pl: Dict = {
'examples.modeDeck': 'Slajdy',
'examples.modeDocument': 'Dokumenty i szablony',
'examples.modeOrbit': 'Orbit',
'examples.modeLive': 'Live',
'examples.scenarioGeneral': 'Ogólne',
'examples.scenarioEngineering': 'Inżynieria',
'examples.scenarioProduct': 'Produkt',

View file

@ -417,6 +417,7 @@ export const ptBR: Dict = {
'examples.modeDeck': 'Slides',
'examples.modeDocument': 'Docs e templates',
'examples.modeOrbit': 'Orbit',
'examples.modeLive': 'Ao vivo',
'examples.scenarioGeneral': 'Geral',
'examples.scenarioEngineering': 'Engenharia',
'examples.scenarioProduct': 'Produto',

View file

@ -417,6 +417,7 @@ export const ru: Dict = {
'examples.modeDeck': 'Презентации',
'examples.modeDocument': 'Документы и шаблоны',
'examples.modeOrbit': 'Orbit',
'examples.modeLive': 'Live',
'examples.scenarioGeneral': 'Общее',
'examples.scenarioEngineering': 'Инженерия',
'examples.scenarioProduct': 'Продукт',

View file

@ -400,6 +400,7 @@ export const tr: Dict = {
'examples.modeDeck': 'Slaytlar',
'examples.modeDocument': 'Doküman & şablonlar',
'examples.modeOrbit': 'Orbit',
'examples.modeLive': 'Canlı',
'examples.scenarioGeneral': 'Genel',
'examples.scenarioEngineering': 'Mühendislik',
'examples.scenarioProduct': 'Ürün',

View file

@ -418,6 +418,7 @@ export const uk: Dict = {
'examples.modeDeck': 'Слайди',
'examples.modeDocument': 'Документи та шаблони',
'examples.modeOrbit': 'Orbit',
'examples.modeLive': 'Live',
'examples.scenarioGeneral': 'Загальне',
'examples.scenarioEngineering': 'Інженерія',
'examples.scenarioProduct': 'Продукт',

View file

@ -412,6 +412,7 @@ export const zhCN: Dict = {
'examples.modeDeck': '幻灯片',
'examples.modeDocument': '文档与模板',
'examples.modeOrbit': 'Orbit',
'examples.modeLive': '实时',
'examples.scenarioGeneral': '通用',
'examples.scenarioEngineering': '工程',
'examples.scenarioProduct': '产品',

View file

@ -412,6 +412,7 @@ export const zhTW: Dict = {
'examples.modeDeck': '投影片',
'examples.modeDocument': '文件與範本',
'examples.modeOrbit': 'Orbit',
'examples.modeLive': '即時',
'examples.scenarioGeneral': '通用',
'examples.scenarioEngineering': '工程',
'examples.scenarioProduct': '產品',

View file

@ -570,6 +570,7 @@ export interface Dict {
'examples.modeDeck': string;
'examples.modeDocument': string;
'examples.modeOrbit': string;
'examples.modeLive': string;
'examples.scenarioGeneral': string;
'examples.scenarioEngineering': string;
'examples.scenarioProduct': string;

View file

@ -45,6 +45,14 @@ export interface SkillSummary {
craftRequires?: string[];
hasBody: boolean;
examplePrompt: string;
// True when this skill exists only to group derived `<parent>:<child>`
// example cards. The Examples gallery hides such cards because their
// preview would duplicate one of the derived cards and add no extra
// information, but the entry stays in the listing so `findSkillById`
// resolves the parent for system-prompt composition and "Use this
// prompt" fast-create on a derived card still composes the parent's
// SKILL.md body.
aggregatesExamples?: boolean;
}
export interface SkillDetail extends SkillSummary {

View file

@ -14,6 +14,7 @@ triggers:
- "实时看板"
od:
mode: prototype
scenario: live
preview:
type: html
entry: index.html

View file

@ -0,0 +1,761 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Mira's First Month · A quiet panel</title>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Instrument+Serif:ital@0;1&family=Inter:wght@400;500;600&family=Caveat:wght@500;600&display=swap" rel="stylesheet">
<style>
:root {
/* Soft, warm, parental */
--bg: #fdf9f5;
--paper: #ffffff;
--paper-tint: #faf5ef;
--paper-deep: #f4ece2;
--line: #ece2d5;
--line-strong: #d8c8b3;
--hairline: rgba(60,40,20,0.06);
--ink: #2a2520;
--ink-soft: #4d433b;
--muted: #8a7e72;
--muted-2: #b3a89a;
/* Soft pastel accents */
--peach: #f5b885;
--peach-soft: #fce4cf;
--sage: #9ec79e;
--sage-soft: #e0eddc;
--lavender: #c5b8e0;
--lavender-soft: #e8e1f5;
--sky: #b3d4e8;
--sky-soft: #def0fa;
--rose: #e8b3c2;
--rose-soft: #fadfe6;
--gold: #e8d094;
--gold-soft: #faf1d6;
/* Type */
--serif: 'Instrument Serif', Iowan, Charter, Georgia, serif;
--display: 'Instrument Serif', Iowan, Charter, Georgia, serif;
--body: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
--hand: 'Caveat', 'Instrument Serif', cursive;
--r: 16px; /* softer corners */
--r-pill: 999px;
}
* { box-sizing: border-box; }
html, body { margin: 0; padding: 0; }
body {
background:
radial-gradient(800px 400px at 100% -10%, rgba(245,184,133,0.15), transparent 60%),
radial-gradient(700px 400px at 0% 60%, rgba(197,184,224,0.12), transparent 60%),
radial-gradient(900px 500px at 50% 110%, rgba(158,199,158,0.10), transparent 60%),
var(--bg);
color: var(--ink);
font-family: var(--body);
font-size: 14px;
line-height: 1.6;
-webkit-font-smoothing: antialiased;
text-rendering: optimizeLegibility;
min-height: 100vh;
}
a { color: inherit; text-decoration: none; }
button { font-family: inherit; cursor: pointer; }
/* ─────── Layout ─────── */
main {
max-width: 880px; margin: 0 auto;
padding: 60px 32px 80px;
}
header.app {
display: flex; align-items: center; justify-content: space-between;
padding: 22px 32px;
border-bottom: 1px solid var(--hairline);
}
.word-mark {
font-family: var(--serif); font-style: italic;
font-size: 19px; letter-spacing: -0.005em;
color: var(--ink);
}
.word-mark .small {
font-family: var(--body); font-style: normal;
font-size: 11px; color: var(--muted);
text-transform: uppercase; letter-spacing: 0.16em;
margin-right: 10px;
}
.icon-btn {
padding: 7px 14px; font-size: 12.5px;
border-radius: var(--r-pill);
background: var(--paper); border: 1px solid var(--line);
color: var(--ink-soft); transition: all 0.15s;
display: inline-flex; gap: 8px; align-items: center;
}
.icon-btn:hover { border-color: var(--line-strong); color: var(--ink); }
.icon-btn.warm {
background: linear-gradient(180deg, #fef4e6, var(--paper));
border-color: rgba(245,184,133,0.45);
color: #8a5a2a;
}
.icon-btn .ic { width: 13px; height: 13px; }
.icon-btn.spin .ic { animation: spin 0.8s linear; }
@keyframes spin { to { transform: rotate(360deg); } }
/* ─────── Hero ─────── */
.hero {
text-align: center;
margin-bottom: 56px;
}
.hero .pre {
font-size: 11px; color: var(--muted); text-transform: uppercase;
letter-spacing: 0.2em; margin-bottom: 18px;
}
.hero .name {
font-family: var(--serif); font-weight: 400;
font-size: 80px; line-height: 1.05;
margin: 0; letter-spacing: -0.02em;
color: var(--ink);
}
.hero .name em {
font-style: italic;
background: linear-gradient(180deg, #f5b885 0%, #c5b8e0 100%);
-webkit-background-clip: text; background-clip: text; color: transparent;
}
.hero .age {
font-family: var(--hand);
font-size: 32px; line-height: 1;
color: var(--peach);
margin-top: 14px;
transform: rotate(-2deg);
display: inline-block;
}
.hero .meta {
margin-top: 22px;
display: inline-flex; gap: 18px;
font-size: 13px; color: var(--muted);
}
.hero .meta .v { color: var(--ink); }
.hero .meta .sep { color: var(--muted-2); }
/* ─────── Section ─────── */
section.block {
margin-bottom: 44px;
}
.block-head {
display: flex; align-items: baseline; justify-content: space-between;
margin-bottom: 18px;
}
.block-head h2 {
font-family: var(--serif); font-weight: 400; font-size: 30px;
margin: 0; line-height: 1; letter-spacing: -0.005em;
color: var(--ink);
}
.block-head h2 em {
font-style: italic; color: var(--peach);
margin-right: 8px; font-size: 21px; vertical-align: 4px;
}
.block-head .meta {
font-size: 11.5px; color: var(--muted);
text-transform: uppercase; letter-spacing: 0.14em;
}
.block-head .meta .src { color: var(--ink-soft); margin-right: 4px; }
.card {
background: var(--paper);
border: 1px solid var(--line);
border-radius: var(--r);
padding: 28px;
box-shadow: 0 2px 0 rgba(60,40,20,0.02);
}
/* ─────── Sleep circle ─────── */
.sleep-card {
display: grid; grid-template-columns: 280px 1fr; gap: 28px; align-items: center;
}
.sleep-circle {
width: 280px; height: 280px;
margin: 0 auto;
}
.sleep-circle svg { width: 100%; height: 100%; display: block; }
.sleep-stat .lbl {
font-size: 11px; color: var(--muted); text-transform: uppercase; letter-spacing: 0.14em;
}
.sleep-stat .big {
font-family: var(--serif); font-weight: 400; font-size: 56px;
line-height: 1; margin-top: 8px; letter-spacing: -0.02em;
}
.sleep-stat .delta {
margin-top: 10px; font-size: 13px; color: var(--sage);
}
.sleep-stat .delta strong { color: var(--ink); font-weight: 600; }
.sleep-stat .quote {
font-family: var(--serif); font-style: italic;
font-size: 17px; color: var(--ink-soft);
margin-top: 22px; padding-left: 14px;
border-left: 2px solid var(--lavender);
line-height: 1.5;
max-width: 36ch;
}
/* ─────── Feeding timeline ─────── */
.feed-timeline {
position: relative; padding: 28px 0 32px;
}
.feed-line {
height: 2px;
background: linear-gradient(90deg, var(--lavender), var(--peach), var(--sage));
border-radius: 2px;
position: relative;
margin: 0 14px;
}
.feed-marker {
position: absolute; top: 50%; transform: translate(-50%, -50%);
width: 14px; height: 14px; border-radius: 50%;
cursor: pointer;
transition: transform 0.15s;
}
.feed-marker:hover { transform: translate(-50%, -50%) scale(1.4); }
.feed-marker.bottle { background: var(--peach); box-shadow: 0 0 0 4px var(--peach-soft); }
.feed-marker.breast { background: var(--rose); box-shadow: 0 0 0 4px var(--rose-soft); }
.feed-marker.now {
background: var(--ink); box-shadow: 0 0 0 4px var(--paper), 0 0 0 6px var(--ink);
animation: nowBreath 2.4s ease-in-out infinite;
}
@keyframes nowBreath {
0%, 100% { transform: translate(-50%, -50%) scale(1); }
50% { transform: translate(-50%, -50%) scale(1.15); }
}
.feed-tip {
position: absolute; top: -36px; transform: translate(-50%, 0);
background: var(--ink); color: var(--paper);
font-size: 11px; padding: 4px 8px; border-radius: 5px;
white-space: nowrap;
opacity: 0; pointer-events: none; transition: opacity 0.15s;
font-family: var(--body);
}
.feed-tip::after {
content: ""; position: absolute; left: 50%; top: 100%; transform: translateX(-50%);
border: 4px solid transparent; border-top-color: var(--ink);
}
.feed-marker:hover .feed-tip { opacity: 1; }
.feed-axis {
margin-top: 36px; padding: 0 14px;
display: grid; grid-template-columns: repeat(7, 1fr);
font-family: var(--body); font-size: 11px; color: var(--muted);
text-align: center;
}
.feed-legend {
margin-top: 24px;
display: flex; gap: 22px; flex-wrap: wrap;
font-size: 12px; color: var(--ink-soft);
}
.feed-legend .swatch { display: inline-flex; gap: 8px; align-items: center; }
.feed-legend .swatch::before { content: ""; width: 10px; height: 10px; border-radius: 50%; }
.feed-legend .swatch.bottle::before { background: var(--peach); box-shadow: 0 0 0 3px var(--peach-soft); }
.feed-legend .swatch.breast::before { background: var(--rose); box-shadow: 0 0 0 3px var(--rose-soft); }
.feed-legend .swatch.now::before { background: var(--ink); }
.feed-legend .v { font-family: var(--serif); font-style: italic; color: var(--ink); }
/* ─────── Weight curve ─────── */
.growth-card { padding: 28px 32px; }
.growth-card svg { width: 100%; height: 220px; display: block; margin-top: 8px; }
.growth-stats {
display: grid; grid-template-columns: repeat(3, 1fr); gap: 20px;
margin-top: 14px; padding-top: 14px; border-top: 1px solid var(--hairline);
}
.growth-stats .stat .lbl { font-size: 10.5px; color: var(--muted); text-transform: uppercase; letter-spacing: 0.14em; }
.growth-stats .stat .v { font-family: var(--serif); font-size: 24px; line-height: 1; margin-top: 6px; letter-spacing: -0.01em; }
.growth-stats .stat .pctile { color: var(--sage); font-size: 11.5px; margin-top: 4px; }
/* ─────── Today reminders ─────── */
.reminders {
display: grid; grid-template-columns: 1fr 1fr; gap: 14px;
}
.rem-card {
padding: 18px 20px; border-radius: 12px;
border: 1px solid var(--line);
background: var(--paper);
display: flex; flex-direction: column; gap: 8px;
cursor: pointer;
transition: all 0.15s;
}
.rem-card:hover { transform: translateY(-2px); box-shadow: 0 6px 18px rgba(60,40,20,0.06); }
.rem-card .l-row { display: flex; gap: 10px; align-items: center; color: var(--muted); font-size: 11.5px; text-transform: uppercase; letter-spacing: 0.14em; }
.rem-card .glyph {
width: 28px; height: 28px; border-radius: 50%;
display: inline-flex; align-items: center; justify-content: center;
font-size: 14px;
}
.rem-card.feed .glyph { background: var(--peach-soft); color: #8a5a2a; }
.rem-card.nap .glyph { background: var(--lavender-soft); color: #5e4d8e; }
.rem-card.appt .glyph { background: var(--sky-soft); color: #2a5e7a; }
.rem-card.bath .glyph { background: var(--sage-soft); color: #4a6e4a; }
.rem-card .when {
font-family: var(--serif); font-size: 28px; line-height: 1; letter-spacing: -0.01em;
}
.rem-card .desc { font-size: 12.5px; color: var(--ink-soft); line-height: 1.45; }
/* ─────── AI insights ─────── */
.insight-card {
background: linear-gradient(180deg, var(--lavender-soft) 0%, var(--paper) 100%);
border-color: rgba(197,184,224,0.4);
padding: 28px 32px;
}
.insight-card .badge {
display: inline-flex; gap: 8px; align-items: center;
font-family: var(--body); font-size: 10.5px;
text-transform: uppercase; letter-spacing: 0.16em;
color: #6e5e9e;
}
.insight-card .badge .glyph {
width: 22px; height: 22px; border-radius: 50%;
background: linear-gradient(135deg, var(--lavender), var(--peach));
display: inline-flex; align-items: center; justify-content: center;
color: #fff; font-size: 12px; font-weight: 600;
box-shadow: 0 0 12px rgba(197,184,224,0.4);
}
.insight-card h3 {
font-family: var(--serif); font-weight: 400; font-size: 28px;
line-height: 1.2; margin: 12px 0 6px; letter-spacing: -0.01em;
}
.insight-card h3 em { font-style: italic; color: #6e5e9e; }
.insight-card p {
color: var(--ink-soft); font-size: 14px; margin: 0;
line-height: 1.55; max-width: 56ch;
}
.insight-list {
margin-top: 16px; display: flex; flex-direction: column; gap: 10px;
}
.insight-list .row {
display: grid; grid-template-columns: 22px 1fr; gap: 10px;
align-items: flex-start;
}
.insight-list .check {
color: var(--sage); font-family: var(--serif); font-size: 18px; line-height: 1;
}
.insight-list .text { font-size: 13px; color: var(--ink-soft); line-height: 1.5; }
.insight-list .text strong { color: var(--ink); }
.insight-list .text .hand {
font-family: var(--hand); font-size: 16px; color: var(--peach);
transform: rotate(-1deg); display: inline-block; padding: 0 4px;
}
/* ─────── Photo strip (placeholder) ─────── */
.photo-strip {
display: grid; grid-template-columns: repeat(4, 1fr); gap: 10px;
}
.photo {
aspect-ratio: 4 / 5; border-radius: 10px;
background: linear-gradient(135deg, var(--peach-soft), var(--lavender-soft));
border: 1px solid var(--line);
position: relative;
overflow: hidden;
cursor: pointer;
transition: transform 0.15s;
}
.photo:hover { transform: translateY(-2px) rotate(-1deg); }
.photo:nth-child(2) { background: linear-gradient(135deg, var(--rose-soft), var(--sky-soft)); transform: rotate(0.5deg); }
.photo:nth-child(3) { background: linear-gradient(135deg, var(--sage-soft), var(--gold-soft)); transform: rotate(-0.5deg); }
.photo:nth-child(4) { background: linear-gradient(135deg, var(--gold-soft), var(--peach-soft)); transform: rotate(0.8deg); }
.photo .cap {
position: absolute; bottom: 8px; left: 10px; right: 10px;
font-family: var(--hand); font-size: 16px;
color: var(--ink-soft); transform: rotate(-1deg);
}
.photo .day {
position: absolute; top: 8px; right: 10px;
font-family: var(--body); font-size: 10.5px; color: var(--muted);
background: rgba(255,255,255,0.7); padding: 2px 6px; border-radius: 3px;
backdrop-filter: blur(2px);
}
/* Closing line */
.closing {
text-align: center; margin-top: 40px;
font-family: var(--serif); font-style: italic;
font-size: 18px; color: var(--muted);
line-height: 1.5;
}
.closing .author {
font-family: var(--body); font-style: normal;
font-size: 11px; color: var(--muted-2);
text-transform: uppercase; letter-spacing: 0.16em;
margin-top: 8px;
}
/* Toast */
.toast-box { position: fixed; bottom: 24px; right: 24px; z-index: 200; display: flex; flex-direction: column; gap: 10px; pointer-events: none; }
.toast {
pointer-events: auto;
min-width: 280px; max-width: 380px;
padding: 14px 16px;
background: var(--paper);
border: 1px solid var(--line);
border-left: 3px solid var(--peach);
border-radius: 12px;
box-shadow: 0 16px 40px rgba(60,40,20,0.10);
font-size: 13px;
color: var(--ink);
animation: toastIn 0.32s cubic-bezier(0.2, 0.8, 0.2, 1);
}
.toast.lavender { border-left-color: var(--lavender); }
.toast.sage { border-left-color: var(--sage); }
.toast .t-title { font-family: var(--serif); font-size: 16px; line-height: 1.2; margin-bottom: 4px; }
.toast .t-body { color: var(--ink-soft); line-height: 1.45; }
.toast .t-body strong { color: var(--ink); }
.toast.fade { opacity: 0; transform: translateX(20px); transition: all 0.25s; }
@keyframes toastIn { from { opacity: 0; transform: translateX(20px); } to { opacity: 1; transform: translateX(0); } }
@media (max-width: 720px) {
main { padding: 40px 18px 60px; }
.hero .name { font-size: 56px; }
.sleep-card { grid-template-columns: 1fr; }
.sleep-circle { width: 220px; height: 220px; }
.reminders { grid-template-columns: 1fr; }
.photo-strip { grid-template-columns: repeat(2, 1fr); }
}
</style>
</head>
<body>
<header class="app">
<div class="word-mark">
<span class="small">a quiet panel from</span>
<em>quiver</em>
</div>
<button class="icon-btn warm" id="logBtn">
<svg class="ic" viewBox="0 0 16 16" fill="none"><path d="M8 3v10M3 8h10" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/></svg>
Log a feed
</button>
</header>
<main>
<!-- ─────── Hero ─────── -->
<div class="hero">
<div class="pre">Mira's first month · 2026</div>
<h1 class="name">Mira <em>Avery</em></h1>
<div class="age">28 days &amp; counting</div>
<div class="meta">
<span><span class="v">Born</span> April 9 · 2026</span>
<span class="sep">·</span>
<span><span class="v">3.42 kg</span> · 50.8 cm</span>
<span class="sep">·</span>
<span><span class="v">Mom &amp; dad</span> have her back</span>
</div>
</div>
<!-- ─────── 1. Sleep ─────── -->
<section class="block">
<div class="block-head">
<h2><em>i.</em>How she slept last night</h2>
<span class="meta"><span class="src">Huckleberry</span>· synced 6:42 AM</span>
</div>
<div class="card sleep-card">
<div class="sleep-circle">
<svg viewBox="0 0 200 200">
<defs>
<radialGradient id="bgG" cx="50%" cy="50%" r="50%">
<stop offset="0%" stop-color="#fff" stop-opacity="0.6"/>
<stop offset="100%" stop-color="#c5b8e0" stop-opacity="0.06"/>
</radialGradient>
</defs>
<circle cx="100" cy="100" r="92" fill="url(#bgG)" stroke="#ece2d5" stroke-width="0.8"/>
<!-- 24h tick marks -->
<g stroke="#d8c8b3" stroke-width="0.6">
<line x1="100" y1="14" x2="100" y2="20"/>
<line x1="186" y1="100" x2="180" y2="100"/>
<line x1="100" y1="186" x2="100" y2="180"/>
<line x1="14" y1="100" x2="20" y2="100"/>
</g>
<!-- Hour labels -->
<g font-family="Inter,sans-serif" font-size="9" fill="#8a7e72">
<text x="100" y="11" text-anchor="middle">12 am</text>
<text x="194" y="103" text-anchor="end">6</text>
<text x="100" y="197" text-anchor="middle">12 pm</text>
<text x="6" y="103">6</text>
</g>
<!-- Sleep arcs (purple = night, peach = naps) -->
<!-- Night sleep 7:30 PM → 6:30 AM (with 4 brief wakes) -->
<!-- Stroke arcs at radius 76 -->
<path d="M 154.5 145.5 A 76 76 0 0 1 100 24" fill="none" stroke="#c5b8e0" stroke-width="14" stroke-linecap="round" opacity="0.85"/>
<path d="M 100 24 A 76 76 0 0 1 134.7 32.5" fill="none" stroke="#c5b8e0" stroke-width="14" stroke-linecap="round" opacity="0.5"/>
<!-- 1st morning nap 9:0010:30 -->
<path d="M 162.4 76.6 A 76 76 0 0 1 173.1 105.4" fill="none" stroke="#f5b885" stroke-width="14" stroke-linecap="round" opacity="0.85"/>
<!-- afternoon nap 1:303:00 -->
<path d="M 142.1 168.6 A 76 76 0 0 1 100 176" fill="none" stroke="#f5b885" stroke-width="14" stroke-linecap="round" opacity="0.85"/>
<!-- evening nap 6:006:45 -->
<path d="M 24 100 A 76 76 0 0 1 32.5 65.3" fill="none" stroke="#f5b885" stroke-width="14" stroke-linecap="round" opacity="0.65"/>
<!-- Center text -->
<text x="100" y="92" text-anchor="middle" font-family="Instrument Serif,serif" font-size="34" fill="#2a2520" letter-spacing="-0.5">14h 22m</text>
<text x="100" y="115" text-anchor="middle" font-family="Inter,sans-serif" font-size="10" fill="#8a7e72" letter-spacing="0.6">total · 24h window</text>
<!-- now hand -->
<line x1="100" y1="100" x2="100" y2="62" stroke="#2a2520" stroke-width="1.5" stroke-linecap="round" transform="rotate(105 100 100)"/>
<circle cx="100" cy="100" r="3" fill="#2a2520"/>
</svg>
</div>
<div class="sleep-stat">
<div class="lbl">Last night · 7:30 PM → 6:30 AM</div>
<div class="big">9h 14m</div>
<div class="delta">3 wakes (down from 4) · longest stretch <strong>3h 12m</strong></div>
<div class="quote">"She slept 22 minutes longer per night this week. Not a streak — a rhythm."</div>
</div>
</div>
</section>
<!-- ─────── 2. Feeding ─────── -->
<section class="block">
<div class="block-head">
<h2><em>ii.</em>How she ate today</h2>
<span class="meta"><span class="src">Baby Tracker + scale</span>· last 24h</span>
</div>
<div class="card">
<div class="feed-timeline">
<div class="feed-line">
<!-- timeline runs midnight → midnight → markers as % -->
<div class="feed-marker bottle" style="left:8.3%"><span class="feed-tip">2:00 AM · 90ml bottle</span></div>
<div class="feed-marker breast" style="left:21%"><span class="feed-tip">5:00 AM · breast 18 min</span></div>
<div class="feed-marker bottle" style="left:33%"><span class="feed-tip">8:00 AM · 110ml bottle</span></div>
<div class="feed-marker breast" style="left:46%"><span class="feed-tip">11:00 AM · breast 22 min</span></div>
<div class="feed-marker bottle" style="left:58%"><span class="feed-tip">2:00 PM · 100ml bottle</span></div>
<div class="feed-marker now" style="left:60.5%"><span class="feed-tip">Now · 14:32</span></div>
<div class="feed-marker breast" style="left:71%"><span class="feed-tip">5:00 PM · breast 16 min (est.)</span></div>
<div class="feed-marker bottle" style="left:83%"><span class="feed-tip">8:00 PM · 100ml bottle (est.)</span></div>
<div class="feed-marker breast" style="left:96%"><span class="feed-tip">11:30 PM · breast (est.)</span></div>
</div>
<div class="feed-axis">
<span>12am</span><span>4am</span><span>8am</span><span>noon</span><span>4pm</span><span>8pm</span><span>12am</span>
</div>
<div class="feed-legend">
<span class="swatch bottle">Bottle <span class="v" style="margin-left:4px;">4 today · 400 ml</span></span>
<span class="swatch breast">Breast <span class="v" style="margin-left:4px;">5 today · ~110 min</span></span>
<span class="swatch now">Now</span>
</div>
</div>
</div>
</section>
<!-- ─────── 3. Weight curve ─────── -->
<section class="block">
<div class="block-head">
<h2><em>iii.</em>How she's growing</h2>
<span class="meta"><span class="src">Withings smart scale</span>· weekly</span>
</div>
<div class="card growth-card">
<svg viewBox="0 0 600 220" preserveAspectRatio="none">
<defs>
<linearGradient id="growthArea" x1="0" x2="0" y1="0" y2="1">
<stop offset="0%" stop-color="#9ec79e" stop-opacity="0.28"/>
<stop offset="100%" stop-color="#9ec79e" stop-opacity="0"/>
</linearGradient>
<linearGradient id="bandG" x1="0" x2="0" y1="0" y2="1">
<stop offset="0%" stop-color="#c5b8e0" stop-opacity="0.18"/>
<stop offset="100%" stop-color="#c5b8e0" stop-opacity="0.04"/>
</linearGradient>
</defs>
<!-- WHO percentile band (1090) -->
<polygon fill="url(#bandG)" points="40,170 120,154 200,138 280,122 360,108 440,96 520,84 580,76 580,140 520,150 440,160 360,168 280,176 200,182 120,188 40,194"/>
<!-- 50th percentile dashed line -->
<polyline fill="none" stroke="#c5b8e0" stroke-width="1" stroke-dasharray="3 4"
points="40,182 120,170 200,158 280,148 360,138 440,128 520,118 580,108"/>
<!-- Mira's actual weight -->
<polygon fill="url(#growthArea)" points="40,200 40,178 130,170 220,158 310,142 400,124 490,108 580,90 580,200"/>
<polyline fill="none" stroke="#9ec79e" stroke-width="2.4" stroke-linejoin="round" stroke-linecap="round"
points="40,178 130,170 220,158 310,142 400,124 490,108 580,90"/>
<!-- Markers -->
<g>
<circle cx="40" cy="178" r="3.5" fill="#9ec79e"/>
<circle cx="130" cy="170" r="3.5" fill="#9ec79e"/>
<circle cx="220" cy="158" r="3.5" fill="#9ec79e"/>
<circle cx="310" cy="142" r="3.5" fill="#9ec79e"/>
<circle cx="400" cy="124" r="3.5" fill="#9ec79e"/>
<circle cx="490" cy="108" r="3.5" fill="#9ec79e"/>
<circle cx="580" cy="90" r="5" fill="#9ec79e" stroke="#fff" stroke-width="2"/>
</g>
<!-- X labels -->
<g font-family="Inter,sans-serif" font-size="10" fill="#8a7e72" text-anchor="middle">
<text x="40" y="215">birth</text>
<text x="130" y="215">wk 1</text>
<text x="220" y="215">wk 2</text>
<text x="310" y="215">wk 3</text>
<text x="400" y="215">wk 4</text>
<text x="490" y="215">wk 5</text>
<text x="580" y="215" fill="#2a2520" font-weight="600">today</text>
</g>
<!-- "now" callout -->
<text x="572" y="80" text-anchor="end" font-family="Caveat,cursive" font-size="20" fill="#9ec79e" transform="rotate(-3 572 80)">on track ✓</text>
<!-- Y axis labels -->
<g font-family="Geist Mono,monospace" font-size="9.5" fill="#8a7e72">
<text x="6" y="80">5.0</text>
<text x="6" y="120">4.5</text>
<text x="6" y="160">4.0</text>
<text x="6" y="200">3.5</text>
</g>
</svg>
<div class="growth-stats">
<div class="stat">
<div class="lbl">Weight today</div>
<div class="v">4.84 kg</div>
<div class="pctile">62nd percentile · WHO</div>
</div>
<div class="stat">
<div class="lbl">Length</div>
<div class="v">53.4 cm</div>
<div class="pctile">58th percentile · WHO</div>
</div>
<div class="stat">
<div class="lbl">Gain · 7 days</div>
<div class="v">+218 g</div>
<div class="pctile">healthy range (150280g)</div>
</div>
</div>
</div>
</section>
<!-- ─────── 4. Today's reminders ─────── -->
<section class="block">
<div class="block-head">
<h2><em>iv.</em>Today's gentle reminders</h2>
<span class="meta">4 things, no alarms</span>
</div>
<div class="reminders">
<div class="rem-card feed" data-rem="feed">
<div class="l-row"><span class="glyph"></span> Next feed</div>
<div class="when">in ~38 min</div>
<div class="desc">Last feed at 2:00 PM (100 ml bottle). Suggested 100110 ml.</div>
</div>
<div class="rem-card nap" data-rem="nap">
<div class="l-row"><span class="glyph"></span> Next likely nap</div>
<div class="when">3:30 — 5:00 PM</div>
<div class="desc">She tends to fade ~90 min after morning rouse. Recent pattern.</div>
</div>
<div class="rem-card appt" data-rem="appt">
<div class="l-row"><span class="glyph"></span> Doctor visit</div>
<div class="when">Friday 10:30 AM</div>
<div class="desc">1-month checkup with Dr. Wei. We'll bring the weight log.</div>
</div>
<div class="rem-card bath" data-rem="bath">
<div class="l-row"><span class="glyph">~</span> Bath night</div>
<div class="when">tonight, 6:30 PM</div>
<div class="desc">Every other day · grandma's lullaby works.</div>
</div>
</div>
</section>
<!-- ─────── 5. AI Insight ─────── -->
<section class="block">
<div class="block-head">
<h2><em>v.</em>What we noticed this week</h2>
<span class="meta"><span class="src">Quiver</span>· soft patterns</span>
</div>
<div class="card insight-card">
<div class="badge">
<span class="glyph"></span>
Quiver insights · weekly
</div>
<h3>She's settling into a <em>longer-stretch</em> sleep at night.</h3>
<p>Five small patterns matched up this week. None demand action — they just paint a picture.</p>
<div class="insight-list">
<div class="row"><span class="check"></span><span class="text"><strong>Longer first stretch:</strong> first sleep block grew from 2h 14m → 3h 12m on average. <span class="hand">good for you both</span></span></div>
<div class="row"><span class="check"></span><span class="text"><strong>Feed-then-sleep latency dropped to 14 min</strong> (was 28 min in week 2). She's calmer post-feed.</span></div>
<div class="row"><span class="check"></span><span class="text"><strong>Bottle preference stabilized</strong> on the slow-flow Dr. Brown's after the size-2 trial. No spit-up since Monday.</span></div>
<div class="row"><span class="check"></span><span class="text"><strong>62nd percentile</strong> on weight, up from 58th — within healthy range. Length is steady at 58th.</span></div>
<div class="row"><span class="check"></span><span class="text"><strong>Diaper count averaged 8 per day</strong> (target ≥6). Hydration is comfortable.</span></div>
</div>
</div>
</section>
<!-- ─────── 6. Photo memory ─────── -->
<section class="block">
<div class="block-head">
<h2><em>vi.</em>This week, in pictures</h2>
<span class="meta"><span class="src">iCloud Photos</span>· auto-curated</span>
</div>
<div class="photo-strip">
<div class="photo">
<div class="day">Mon</div>
<div class="cap">first finger-grip</div>
</div>
<div class="photo">
<div class="day">Wed</div>
<div class="cap">sun nap on the porch</div>
</div>
<div class="photo">
<div class="day">Thu</div>
<div class="cap">grandma met her</div>
</div>
<div class="photo">
<div class="day">Sat</div>
<div class="cap">first real smile (we think)</div>
</div>
</div>
</section>
<div class="closing">
"What you measure, you remember."<br/>
<span class="author">— quiver, a quiet panel · auto-refreshes daily at 6:30 AM</span>
</div>
</main>
<div class="toast-box" id="toastBox"></div>
<script>
(() => {
// Toast
const toastBox = document.getElementById('toastBox');
function toast(opts) {
const { kind = '', title = '', body = '' } = (typeof opts === 'string') ? { body: opts } : opts;
const node = document.createElement('div');
node.className = `toast ${kind}`;
node.innerHTML = `<div class="t-title">${title}</div><div class="t-body">${body}</div>`;
toastBox.appendChild(node);
setTimeout(() => {
node.classList.add('fade');
setTimeout(() => node.remove(), 280);
}, 4200);
}
// Reminders
const remCopy = {
feed: { kind: '', title: 'Logged a feed', body: 'Recorded · <strong>110 ml at 3:10 PM</strong> bottle. Updated next-feed estimate to 6:15 PM.' },
nap: { kind: 'lavender', title: 'Nap window noted', body: 'We\'ll send a quiet ping at <strong>3:25 PM</strong> if she\'s still awake.' },
appt: { kind: 'sage', title: 'Friday checkup', body: 'Calendar event opened · 1-month checkup with Dr. Wei · 10:30 AM.<br/>We\'ll auto-prepare the weight log.' },
bath: { kind: '', title: 'Bath time saved', body: 'Tonight at 6:30 PM. We\'ll dim the smart lights at 6:25.' }
};
document.querySelectorAll('[data-rem]').forEach(r => {
r.addEventListener('click', () => {
const k = r.dataset.rem;
if (remCopy[k]) toast(remCopy[k]);
});
});
// Log a feed (header button)
document.getElementById('logBtn').addEventListener('click', () => {
toast({ kind: 'sage', title: 'New feed logged', body: '<strong>110 ml</strong> bottle · 3:10 PM<br/>Avg gain trend: +218 g this week, on track.' });
});
// Photos
document.querySelectorAll('.photo').forEach(p => {
p.addEventListener('click', () => {
const cap = p.querySelector('.cap').textContent;
toast({ kind: 'lavender', title: 'Photo memory', body: `"${cap}" · saved to your Mira album.` });
});
});
// Feed marker click (open detail)
document.querySelectorAll('.feed-marker').forEach(m => {
m.addEventListener('click', e => {
const tip = m.querySelector('.feed-tip');
if (tip) toast({ kind: '', title: 'Feed entry', body: tip.textContent });
});
});
})();
</script>
</body>
</html>

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,919 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Monday morning briefing · Quiver</title>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Instrument+Serif:ital@0;1&family=Inter:wght@400;500;600;700&family=Geist+Mono:wght@400;500;600&display=swap" rel="stylesheet">
<style>
:root {
/* Editorial · calm Monday morning */
--paper: #f5f1ea; /* warm cream background */
--paper-elev: #fdfcf9; /* card surface */
--paper-deep: #efe9dd; /* elevation contrast */
--ink: #1c1c1f; /* deep near-black */
--ink-soft: #3b3b3f;
--muted: #6b6b66;
--muted-2: #8e8e88;
--line: #e3ddd0;
--line-strong: #cdc4b1;
--hairline: rgba(28,28,31,0.06);
/* Editorial accent palette — earthy, calm */
--espresso: #6f4e37; /* coffee */
--sage: #6e8865;
--sage-soft: #e7eee2;
--slate-blue: #4a6e8a;
--slate-soft: #e3ebf2;
--terracotta: #c8775c;
--tc-soft: #f6e4dd;
--gold: #c9a45a;
--gold-soft: #f1e7ce;
--plum: #6e4e6e;
--plum-soft: #ece2ec;
/* Type */
--serif: 'Instrument Serif', Iowan, Charter, Georgia, serif;
--display: 'Instrument Serif', Iowan, Charter, Georgia, serif;
--body: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
--mono: 'Geist Mono', ui-monospace, SFMono-Regular, Menlo, monospace;
--r: 12px;
--r-pill: 999px;
}
* { box-sizing: border-box; }
html, body { margin: 0; padding: 0; }
body {
background:
/* paper grain — barely-there noise via SVG */
url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='160' height='160' viewBox='0 0 160 160'><filter id='n'><feTurbulence type='fractalNoise' baseFrequency='0.85' numOctaves='2' stitchTiles='stitch'/><feColorMatrix values='0 0 0 0 0.07 0 0 0 0 0.06 0 0 0 0 0.04 0 0 0 0.06 0'/></filter><rect width='160' height='160' filter='url(%23n)'/></svg>"),
var(--paper);
color: var(--ink);
font-family: var(--body);
font-size: 14px;
line-height: 1.6;
-webkit-font-smoothing: antialiased;
text-rendering: optimizeLegibility;
min-height: 100vh;
}
a { color: inherit; text-decoration: none; }
button { font-family: inherit; cursor: pointer; }
/* ─────── Header ─────── */
header.app {
border-bottom: 1px solid var(--line);
padding: 22px 32px 18px;
display: flex; align-items: center; justify-content: space-between;
background: var(--paper);
}
.masthead {
font-family: var(--serif);
font-size: 22px;
letter-spacing: -0.01em;
line-height: 1;
}
.masthead .small {
font-family: var(--mono); font-size: 10.5px;
color: var(--muted); text-transform: uppercase; letter-spacing: 0.16em;
margin-bottom: 6px;
}
.masthead .word {
display: inline-flex; gap: 8px; align-items: baseline;
}
.masthead .quiver {
font-family: var(--serif); font-style: italic;
color: var(--espresso);
font-size: 22px;
}
.head-right { display: flex; align-items: center; gap: 14px; }
.pill-soft {
padding: 6px 12px; font-size: 12px;
border: 1px solid var(--line); border-radius: var(--r-pill);
background: var(--paper-elev); color: var(--ink-soft);
font-family: var(--mono); letter-spacing: 0.04em;
transition: all 0.15s;
}
.pill-soft:hover { border-color: var(--line-strong); color: var(--ink); }
.pill-soft .dot {
display: inline-block; width: 6px; height: 6px; border-radius: 50%;
background: var(--sage); margin-right: 8px; vertical-align: 1px;
box-shadow: 0 0 6px var(--sage);
}
.icon-btn {
padding: 7px 14px; border-radius: var(--r-pill);
background: var(--ink); color: var(--paper);
font-size: 12px; font-weight: 500; letter-spacing: 0.01em;
border: 1px solid var(--ink);
display: inline-flex; gap: 8px; align-items: center;
transition: all 0.15s;
}
.icon-btn:hover { transform: translateY(-1px); }
.icon-btn.ghost { background: transparent; color: var(--ink); }
.icon-btn .ic { width: 13px; height: 13px; }
.icon-btn.spin .ic { animation: spin 0.8s linear; }
@keyframes spin { to { transform: rotate(360deg); } }
/* ─────── Layout ─────── */
main {
max-width: 1220px; margin: 0 auto;
padding: 40px 32px 80px;
display: grid;
grid-template-columns: 1fr 280px;
gap: 48px;
}
/* ─────── Hero greeting ─────── */
.greeting {
margin-bottom: 36px;
border-bottom: 1px solid var(--hairline);
padding-bottom: 28px;
}
.greeting .pretitle {
font-family: var(--mono);
font-size: 11px;
text-transform: uppercase;
letter-spacing: 0.18em;
color: var(--muted);
margin-bottom: 10px;
}
.greeting h1 {
font-family: var(--serif);
font-weight: 400;
font-size: 56px;
line-height: 1.05;
letter-spacing: -0.015em;
margin: 0;
color: var(--ink);
}
.greeting h1 em {
font-style: italic; color: var(--espresso);
}
.greeting .lede {
font-family: var(--serif);
font-style: italic;
font-size: 19px;
color: var(--ink-soft);
margin-top: 10px;
max-width: 64ch;
line-height: 1.5;
}
.greeting .meta-row {
margin-top: 18px;
display: flex; gap: 24px; flex-wrap: wrap;
font-family: var(--mono); font-size: 12px;
color: var(--muted);
}
.greeting .meta-row .v { color: var(--ink); margin-right: 4px; }
.greeting .meta-row .sep { color: var(--line-strong); }
/* ─────── Section style ─────── */
section.block {
margin-bottom: 40px;
}
.block > .head {
display: flex; align-items: baseline; justify-content: space-between;
margin-bottom: 14px;
padding-bottom: 6px;
border-bottom: 1px solid var(--hairline);
}
.block > .head h2 {
font-family: var(--serif);
font-weight: 400;
font-size: 26px;
letter-spacing: -0.005em;
line-height: 1;
margin: 0;
}
.block > .head h2 .ord {
font-family: var(--serif); font-style: italic; color: var(--espresso);
margin-right: 12px; font-size: 18px; vertical-align: 4px;
}
.block > .head .meta {
font-family: var(--mono); font-size: 11px;
text-transform: uppercase; letter-spacing: 0.12em;
color: var(--muted);
}
.block > .head .pill-soft { padding: 4px 10px; font-size: 11px; }
.card {
background: var(--paper-elev);
border: 1px solid var(--line);
border-radius: var(--r);
padding: 20px 22px;
box-shadow: 0 1px 0 rgba(28,28,31,0.02);
}
/* ─────── Revenue card ─────── */
.rev-card {
display: grid;
grid-template-columns: 220px 1fr;
gap: 28px;
align-items: center;
padding: 24px 28px;
}
.rev-stat .label {
font-family: var(--mono); font-size: 10.5px;
color: var(--muted); text-transform: uppercase; letter-spacing: 0.14em;
}
.rev-stat .big {
font-family: var(--serif);
font-weight: 400;
font-size: 60px;
line-height: 1;
letter-spacing: -0.02em;
margin-top: 8px;
color: var(--ink);
}
.rev-stat .delta {
margin-top: 10px; font-size: 13px;
color: var(--sage); font-weight: 500;
}
.rev-stat .delta .arr { font-family: var(--mono); }
.rev-stat .sub { color: var(--muted); font-size: 12px; margin-top: 12px; max-width: 30ch; }
.rev-chart svg { width: 100%; height: 140px; display: block; }
/* ─────── Email digest ─────── */
.digest-list { display: flex; flex-direction: column; gap: 12px; }
.digest-item {
background: var(--paper-elev);
border: 1px solid var(--line);
border-radius: var(--r);
padding: 16px 20px;
display: grid;
grid-template-columns: 38px 1fr auto;
gap: 14px;
cursor: pointer;
transition: all 0.15s;
}
.digest-item:hover {
border-color: var(--line-strong);
transform: translateY(-1px);
box-shadow: 0 4px 14px rgba(28,28,31,0.05);
}
.digest-av {
width: 38px; height: 38px; border-radius: 50%;
color: var(--paper); font-family: var(--serif); font-size: 16px;
display: inline-flex; align-items: center; justify-content: center;
flex-shrink: 0;
}
.digest-meta { font-family: var(--mono); font-size: 11px; color: var(--muted); text-transform: uppercase; letter-spacing: 0.08em; }
.digest-from { color: var(--ink); font-weight: 500; }
.digest-subject {
margin-top: 4px; font-family: var(--serif); font-size: 17px; line-height: 1.35;
color: var(--ink);
}
.digest-snippet {
margin-top: 4px; color: var(--muted); font-size: 12.5px;
overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
max-width: 64ch;
}
.digest-tag {
align-self: center;
font-family: var(--mono); font-size: 10px;
padding: 3px 9px; border-radius: 4px;
text-transform: uppercase; letter-spacing: 0.1em;
}
.digest-tag.urgent { background: var(--tc-soft); color: var(--terracotta); }
.digest-tag.warm { background: var(--gold-soft); color: #8e6f1f; }
.digest-tag.calm { background: var(--sage-soft); color: #496938; }
.digest-tag.fyi { background: var(--slate-soft); color: var(--slate-blue); }
/* ─────── Stuck issues ─────── */
.stuck-list { display: flex; flex-direction: column; gap: 10px; }
.stuck-row {
background: var(--paper-elev);
border: 1px solid var(--line);
border-radius: var(--r);
padding: 14px 18px;
display: grid; grid-template-columns: 1fr auto auto auto; gap: 18px;
align-items: center;
cursor: pointer;
transition: all 0.15s;
}
.stuck-row:hover { border-color: var(--line-strong); }
.stuck-row .id {
font-family: var(--mono); font-size: 11px;
color: var(--muted); margin-bottom: 4px;
letter-spacing: 0.04em;
}
.stuck-row .title { font-size: 14.5px; color: var(--ink); font-weight: 500; }
.stuck-row .who {
display: inline-flex; gap: 6px; align-items: center;
font-size: 12px; color: var(--muted);
}
.stuck-row .who .av {
width: 22px; height: 22px; border-radius: 50%;
color: #fff; font-size: 10px; font-weight: 600;
display: inline-flex; align-items: center; justify-content: center;
}
.stuck-row .age {
font-family: var(--mono); font-size: 11.5px;
padding: 3px 9px; border-radius: 5px;
background: var(--tc-soft); color: var(--terracotta);
font-weight: 600;
}
.stuck-row .age.warm { background: var(--gold-soft); color: #8e6f1f; }
.stuck-row .nudge {
padding: 5px 11px; font-family: var(--mono); font-size: 11px;
border-radius: 6px; border: 1px solid var(--line); background: var(--paper);
color: var(--ink-soft); font-weight: 500;
transition: all 0.15s;
}
.stuck-row .nudge:hover { background: var(--ink); color: var(--paper); border-color: var(--ink); }
/* ─────── Schedule timeline ─────── */
.schedule {
display: flex; flex-direction: column; gap: 0;
padding: 0;
background: var(--paper-elev);
border: 1px solid var(--line);
border-radius: var(--r);
overflow: hidden;
}
.sched-row {
display: grid; grid-template-columns: 80px 12px 1fr auto; gap: 16px;
align-items: center;
padding: 14px 22px;
border-bottom: 1px solid var(--hairline);
transition: background 0.12s;
position: relative;
cursor: pointer;
}
.sched-row:last-child { border-bottom: 0; }
.sched-row:hover { background: var(--paper-deep); }
.sched-row .time {
font-family: var(--mono); font-size: 12px;
color: var(--ink); font-weight: 500;
}
.sched-row .time .dur { color: var(--muted); font-size: 10.5px; display: block; margin-top: 2px; }
.sched-row .dot {
width: 10px; height: 10px; border-radius: 50%;
background: var(--sage); justify-self: center;
}
.sched-row.now .dot {
background: var(--terracotta);
box-shadow: 0 0 0 4px var(--tc-soft);
animation: nowPulse 2.4s ease-out infinite;
}
@keyframes nowPulse {
0%, 100% { transform: scale(1); }
50% { transform: scale(1.18); }
}
.sched-row.past .dot { background: var(--muted-2); opacity: 0.55; }
.sched-row.past .title, .sched-row.past .desc { opacity: 0.55; }
.sched-row.past .title { text-decoration: line-through; text-decoration-color: var(--muted-2); }
.sched-row .title { font-size: 14.5px; color: var(--ink); font-weight: 500; }
.sched-row .desc { font-size: 12px; color: var(--muted); margin-top: 2px; }
.sched-row .source {
font-family: var(--mono); font-size: 10.5px;
padding: 2px 8px; border-radius: 4px;
color: var(--muted); border: 1px solid var(--line);
text-transform: uppercase; letter-spacing: 0.08em;
}
/* ─────── PR list ─────── */
.pr-list { display: flex; flex-direction: column; gap: 0;
background: var(--paper-elev);
border: 1px solid var(--line);
border-radius: var(--r);
overflow: hidden;
}
.pr-row {
display: grid; grid-template-columns: 1fr auto auto auto; gap: 14px;
padding: 14px 22px;
border-bottom: 1px solid var(--hairline);
align-items: center;
cursor: pointer;
transition: background 0.12s;
}
.pr-row:last-child { border-bottom: 0; }
.pr-row:hover { background: var(--paper-deep); }
.pr-row .num { font-family: var(--mono); font-size: 11px; color: var(--muted); }
.pr-row .title { font-size: 14px; color: var(--ink); font-weight: 500; margin-top: 2px; }
.pr-row .files {
font-family: var(--mono); font-size: 11px; color: var(--muted);
background: var(--paper-deep);
padding: 3px 8px; border-radius: 4px;
}
.pr-row .age {
font-family: var(--mono); font-size: 11.5px;
padding: 3px 8px; border-radius: 5px;
color: var(--ink-soft);
}
.pr-row .age.urgent { background: var(--tc-soft); color: var(--terracotta); }
.pr-row .age.warm { background: var(--gold-soft); color: #8e6f1f; }
.pr-row .age.calm { background: var(--sage-soft); color: #496938; }
/* ─────── Wins (subtle bottom) ─────── */
.wins {
margin-top: 36px;
padding: 32px 36px;
background: linear-gradient(180deg, transparent, var(--paper-deep));
border-radius: var(--r);
border: 1px dashed var(--line-strong);
}
.wins-title {
font-family: var(--serif); font-style: italic; font-size: 28px;
color: var(--espresso); margin: 0 0 14px;
line-height: 1; letter-spacing: -0.01em;
}
.wins-list { columns: 2; column-gap: 32px; }
.wins-list .win {
break-inside: avoid;
margin-bottom: 14px;
display: grid; grid-template-columns: 18px 1fr; gap: 8px; align-items: flex-start;
}
.wins-list .checkmark {
color: var(--sage); font-family: var(--serif); font-size: 18px; line-height: 1;
}
.wins-list .text { font-size: 13.5px; color: var(--ink-soft); line-height: 1.5; }
/* ─────── Sidebar ─────── */
aside.briefing-side {
align-self: start;
position: sticky; top: 32px;
}
.first-thing {
background: var(--ink);
color: var(--paper);
border-radius: var(--r);
padding: 20px 22px;
margin-bottom: 14px;
position: relative;
overflow: hidden;
}
.first-thing::before {
content: ""; position: absolute; inset: 0;
background: radial-gradient(220px 120px at 110% -20%, rgba(255,180,120,0.12), transparent 70%);
pointer-events: none;
}
.first-thing .label {
font-family: var(--mono); font-size: 10.5px;
text-transform: uppercase; letter-spacing: 0.14em;
color: rgba(255,255,255,0.55);
}
.first-thing h3 {
font-family: var(--serif); font-size: 22px; font-weight: 400;
margin: 8px 0 8px; line-height: 1.15; color: var(--paper);
letter-spacing: -0.005em;
}
.first-thing p {
color: rgba(255,255,255,0.78); font-size: 13px; margin: 0;
line-height: 1.55;
}
.first-thing .actions {
margin-top: 16px; display: flex; gap: 8px;
}
.first-thing .a-btn {
padding: 6px 12px; font-size: 12px; border-radius: 6px;
border: 1px solid rgba(255,255,255,0.2);
background: rgba(255,255,255,0.06); color: var(--paper);
font-family: var(--mono); letter-spacing: 0.04em;
transition: all 0.15s;
}
.first-thing .a-btn:hover { background: rgba(255,255,255,0.14); }
.first-thing .a-btn.primary {
background: var(--gold); color: var(--ink); border-color: var(--gold);
font-weight: 600;
}
.side-card {
background: var(--paper-elev);
border: 1px solid var(--line);
border-radius: var(--r);
padding: 16px 18px;
margin-bottom: 14px;
}
.side-card h4 {
font-family: var(--mono); font-size: 10.5px;
text-transform: uppercase; letter-spacing: 0.14em;
color: var(--muted); margin: 0 0 10px; font-weight: 500;
}
.quick-action {
display: flex; align-items: center; justify-content: space-between;
padding: 8px 0; font-size: 13px; color: var(--ink-soft);
border-bottom: 1px dashed var(--hairline);
cursor: pointer;
transition: color 0.12s;
}
.quick-action:last-child { border-bottom: 0; }
.quick-action:hover { color: var(--ink); }
.quick-action .arr { font-family: var(--mono); color: var(--muted); }
.weather-card {
display: flex; align-items: center; gap: 14px;
padding: 14px 16px;
}
.weather-glyph {
width: 44px; height: 44px;
background: linear-gradient(180deg, #ffce72, #f0a23a);
border-radius: 50%;
box-shadow: 0 0 22px rgba(255,180,80,0.45);
flex-shrink: 0;
}
.weather-card .temp {
font-family: var(--serif); font-size: 28px; line-height: 1; letter-spacing: -0.01em;
}
.weather-card .desc { color: var(--muted); font-size: 12px; margin-top: 4px; }
/* Toast */
.toast-box {
position: fixed; bottom: 24px; right: 24px; z-index: 200;
display: flex; flex-direction: column; gap: 10px;
pointer-events: none;
}
.toast {
pointer-events: auto;
min-width: 280px; max-width: 380px;
padding: 14px 16px;
background: var(--paper-elev);
border: 1px solid var(--line);
border-left: 3px solid var(--espresso);
border-radius: 8px;
box-shadow: 0 16px 48px rgba(28,28,31,0.10);
font-size: 13px;
color: var(--ink);
animation: toastIn 0.32s cubic-bezier(0.2, 0.8, 0.2, 1);
}
.toast.success { border-left-color: var(--sage); }
.toast.warn { border-left-color: var(--gold); }
.toast.urgent { border-left-color: var(--terracotta); }
.toast .t-title { font-family: var(--serif); font-size: 16px; line-height: 1.2; margin-bottom: 4px; }
.toast .t-body { color: var(--ink-soft); line-height: 1.45; }
.toast .t-body strong { color: var(--ink); }
.toast.fade { opacity: 0; transform: translateX(20px); transition: all 0.25s; }
@keyframes toastIn {
from { opacity: 0; transform: translateX(20px); }
to { opacity: 1; transform: translateX(0); }
}
@media (max-width: 1080px) {
main { grid-template-columns: 1fr; }
aside.briefing-side { position: static; }
.greeting h1 { font-size: 40px; }
.rev-card { grid-template-columns: 1fr; }
.wins-list { columns: 1; }
}
</style>
</head>
<body>
<header class="app">
<div class="masthead">
<div class="small">Daily Briefing · Volume IV · Issue 18</div>
<div class="word">
<span>The Monday Memo</span>
<span class="quiver">— from quiver</span>
</div>
</div>
<div class="head-right">
<span class="pill-soft"><span class="dot"></span>5 sources connected · synced 6:42 AM</span>
<button class="icon-btn ghost" id="snoozeBtn">Snooze 1h</button>
<button class="icon-btn" id="doneBtn">
<svg class="ic" viewBox="0 0 16 16" fill="none"><path d="M3 8.5l3 3 7-7" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round"/></svg>
Mark briefing done
</button>
</div>
</header>
<main>
<div>
<!-- ─────── Greeting ─────── -->
<div class="greeting">
<div class="pretitle">Monday · May 6, 2026 · 8:42 AM</div>
<h1>Good morning, <em>PT.</em></h1>
<div class="lede">
Six things deserve your attention this morning. The rest can wait until coffee #2.
</div>
<div class="meta-row">
<span><span class="v">San Francisco</span></span><span class="sep">·</span>
<span><span class="v">62°</span> light fog, clearing 11 AM</span><span class="sep">·</span>
<span><span class="v">Sunset</span> 8:09 PM</span><span class="sep">·</span>
<span><span class="v" style="color:var(--sage)">7h 24m</span> sleep</span>
</div>
</div>
<!-- ─────── 1. Revenue ─────── -->
<section class="block">
<div class="head">
<h2><span class="ord">i.</span>This week's revenue</h2>
<span class="meta">Stripe · 7-day rolling</span>
</div>
<div class="rev-card card">
<div class="rev-stat">
<div class="label">Past 7 days</div>
<div class="big">$84,210</div>
<div class="delta"><span class="arr"></span> +12.4% vs prior week · +$9,316</div>
<div class="sub">Driven by 3 enterprise expansions on WedThu. Two SMB churns absorbed in net new.</div>
</div>
<div class="rev-chart">
<svg viewBox="0 0 600 140" preserveAspectRatio="none">
<defs>
<linearGradient id="revG" x1="0" x2="0" y1="0" y2="1">
<stop offset="0%" stop-color="#6e8865" stop-opacity="0.32"/>
<stop offset="100%" stop-color="#6e8865" stop-opacity="0"/>
</linearGradient>
</defs>
<!-- horizontal guides -->
<line x1="0" y1="40" x2="600" y2="40" stroke="rgba(28,28,31,0.06)" stroke-dasharray="2 4"/>
<line x1="0" y1="80" x2="600" y2="80" stroke="rgba(28,28,31,0.06)" stroke-dasharray="2 4"/>
<line x1="0" y1="120" x2="600" y2="120" stroke="rgba(28,28,31,0.06)" stroke-dasharray="2 4"/>
<!-- prior week ghost line -->
<polyline fill="none" stroke="rgba(28,28,31,0.32)" stroke-width="1.2" stroke-dasharray="3 4"
points="20,90 100,86 180,82 260,86 340,80 420,78 500,72 580,70"/>
<!-- this week area + line -->
<polygon fill="url(#revG)" points="20,140 20,82 100,76 180,68 260,70 340,58 420,52 500,40 580,30 580,140"/>
<polyline fill="none" stroke="#6e8865" stroke-width="2.2" stroke-linejoin="round" stroke-linecap="round"
points="20,82 100,76 180,68 260,70 340,58 420,52 500,40 580,30"/>
<!-- weekday labels -->
<g font-family="Geist Mono,monospace" font-size="9.5" fill="#6b6b66" text-anchor="middle">
<text x="20" y="138">Mon</text>
<text x="100" y="138">Tue</text>
<text x="180" y="138">Wed</text>
<text x="260" y="138">Thu</text>
<text x="340" y="138">Fri</text>
<text x="420" y="138">Sat</text>
<text x="500" y="138">Sun</text>
<text x="580" y="138" fill="#1c1c1f" font-weight="600">Mon</text>
</g>
<!-- "now" marker -->
<circle cx="580" cy="30" r="3.5" fill="#1c1c1f"/>
<text x="586" y="22" font-family="Instrument Serif,serif" font-style="italic" font-size="13" fill="#1c1c1f">today</text>
</svg>
</div>
</div>
</section>
<!-- ─────── 2. Email digest ─────── -->
<section class="block">
<div class="head">
<h2><span class="ord">ii.</span>Customers who deserve a reply</h2>
<span class="meta">Gmail · curated by relevance</span>
</div>
<div class="digest-list" id="digest"></div>
</section>
<!-- ─────── 3. Stuck Linear issues ─────── -->
<section class="block">
<div class="head">
<h2><span class="ord">iii.</span>What's stuck on the team</h2>
<span class="meta">Linear · idle &gt; 48h</span>
</div>
<div class="stuck-list" id="stuck"></div>
</section>
<!-- ─────── 4. Today's schedule ─────── -->
<section class="block">
<div class="head">
<h2><span class="ord">iv.</span>Today, hour by hour</h2>
<span class="meta">Google Calendar · 6 events</span>
</div>
<div class="schedule" id="schedule"></div>
</section>
<!-- ─────── 5. PRs ─────── -->
<section class="block">
<div class="head">
<h2><span class="ord">v.</span>Pull requests waiting on you</h2>
<span class="meta">GitHub · 4 PRs</span>
</div>
<div class="pr-list" id="prList"></div>
</section>
<!-- ─────── 6. Wins ─────── -->
<div class="wins">
<div class="wins-title">Yesterday, quietly:</div>
<div class="wins-list">
<div class="win"><span class="checkmark"></span><span class="text"><strong>Voltage Co.</strong> went live in production at 6:14 PM. Zero rollback. Onboarding kickoff tomorrow.</span></div>
<div class="win"><span class="checkmark"></span><span class="text">Mira closed <strong>Atlas Cooperative</strong> ($240k) on a Sunday call. The handshake email landed at 11:47 PM.</span></div>
<div class="win"><span class="checkmark"></span><span class="text">QA cleared the Q2 release — <strong>0 P0s, 2 P3s</strong>. Cleanest cut in 7 quarters.</span></div>
<div class="win"><span class="checkmark"></span><span class="text"><strong>Nora's onboarding</strong> wrapped a week ahead. Already shipping in <code>src/agent/router</code>.</span></div>
<div class="win"><span class="checkmark"></span><span class="text">Stripe weekly closed at <strong>$84.2k</strong> — fourth straight week above $80k. Comfortable Q2 trajectory.</span></div>
<div class="win"><span class="checkmark"></span><span class="text">Sam shipped the auth migration with a <strong>54-line PR</strong>. The rollback plan was three commits.</span></div>
</div>
</div>
</div>
<!-- ─────── Sidebar ─────── -->
<aside class="briefing-side">
<div class="first-thing">
<div class="label">Your first thing today</div>
<h3>Reply to Lattice Health by 10 AM.</h3>
<p>Their legal review wraps tomorrow. A short "we're ready when you are" keeps the proposal on Friday's close list.</p>
<div class="actions">
<button class="a-btn primary" data-action="draft">Draft reply</button>
<button class="a-btn" data-action="snooze1">Snooze 1h</button>
</div>
</div>
<div class="weather-card side-card">
<div class="weather-glyph"></div>
<div>
<div class="temp">62°</div>
<div class="desc">Light fog · clearing by 11</div>
</div>
</div>
<div class="side-card">
<h4>Quick actions</h4>
<div class="quick-action" data-action="generatePlan"><span>✦ Generate next week's plan</span><span class="arr"></span></div>
<div class="quick-action" data-action="standup"><span>Post standup digest</span><span class="arr"></span></div>
<div class="quick-action" data-action="snooze"><span>Snooze whole briefing 1h</span><span class="arr"></span></div>
<div class="quick-action" data-action="email"><span>Email me a copy at 7 PM</span><span class="arr"></span></div>
</div>
<div class="side-card">
<h4>Connected sources</h4>
<div style="display:flex;flex-direction:column;gap:8px;font-size:13px;">
<div style="display:flex;justify-content:space-between;color:var(--ink-soft);">
<span>Stripe</span><span style="font-family:var(--mono);font-size:11px;color:var(--sage);">live</span>
</div>
<div style="display:flex;justify-content:space-between;color:var(--ink-soft);">
<span>Gmail</span><span style="font-family:var(--mono);font-size:11px;color:var(--sage);">live</span>
</div>
<div style="display:flex;justify-content:space-between;color:var(--ink-soft);">
<span>Linear</span><span style="font-family:var(--mono);font-size:11px;color:var(--sage);">live</span>
</div>
<div style="display:flex;justify-content:space-between;color:var(--ink-soft);">
<span>GitHub</span><span style="font-family:var(--mono);font-size:11px;color:var(--sage);">live</span>
</div>
<div style="display:flex;justify-content:space-between;color:var(--ink-soft);">
<span>Calendar</span><span style="font-family:var(--mono);font-size:11px;color:var(--sage);">live</span>
</div>
</div>
</div>
<div style="text-align:center;font-family:var(--serif);font-style:italic;color:var(--muted);font-size:14px;line-height:1.4;padding:14px 8px;">
"What gets your attention<br/>shapes your week."
<div style="font-family:var(--mono);font-style:normal;font-size:10.5px;color:var(--muted-2);margin-top:6px;letter-spacing:0.06em;">— quiver, vol. IV</div>
</div>
</aside>
</main>
<div class="toast-box" id="toastBox"></div>
<script>
(() => {
// ─────── Email digest ───────
const EMAILS = [
{ from: 'Pioneer Robotics — Sarah Chen', subject: 'Re: Pricing 2-pager and onboarding timeline', snippet: 'Quick one — the team is ready to move forward, just need the term sheet by Wednesday so legal can…', tag: 'urgent', ago: '2h', av: 'S', avBg: '#1f2937' },
{ from: 'Lattice Health — David Park', subject: 'Procurement asked for a SOC 2 letter', snippet: 'Hey, our procurement team had one last item on the list. Can you send the SOC 2 attestation letter to…', tag: 'warm', ago: '4h', av: 'D', avBg: '#0ea5e9' },
{ from: 'Foundry Group — Thomas Brun', subject: 'Loved the demo recording — sharing internally', snippet: 'Just to close the loop: I forwarded the recording to two engineering directors. They\'re penciled in for…', tag: 'calm', ago: '14h', av: 'T', avBg: '#dc2626' },
{ from: 'Ironclad Mfg — Priya Anand', subject: 'Term sheet v2 attached', snippet: 'Here\'s our redline. Two small changes on the indemnification and one substantial on the data residency…', tag: 'urgent', ago: '18h', av: 'P', avBg: '#475569' },
{ from: 'Mosaic Health — Lin Chen', subject: 'Intro to procurement (cc\'d)', snippet: 'Looping in our head of procurement. They have time this Thursday or next Monday for a first call…', tag: 'fyi', ago: '1d', av: 'L', avBg: '#7c3aed' }
];
const dgEl = document.getElementById('digest');
dgEl.innerHTML = EMAILS.map((e, i) => `
<div class="digest-item" data-i="${i}">
<span class="digest-av" style="background:${e.avBg}">${e.av}</span>
<div>
<div class="digest-meta"><span class="digest-from">${e.from}</span> · ${e.ago} ago</div>
<div class="digest-subject">${e.subject}</div>
<div class="digest-snippet">${e.snippet}</div>
</div>
<span class="digest-tag ${e.tag}">${e.tag === 'urgent' ? 'reply today' : e.tag === 'warm' ? 'this week' : e.tag === 'calm' ? 'reviewed' : 'fyi'}</span>
</div>
`).join('');
dgEl.querySelectorAll('.digest-item').forEach(item => {
const e = EMAILS[+item.dataset.i];
item.addEventListener('click', () => {
toast({ kind: e.tag === 'urgent' ? 'urgent' : 'warn', title: e.subject, body: `From <strong>${e.from}</strong> · Opening Gmail thread…` });
});
});
// ─────── Stuck issues ───────
const STUCK = [
{ id: 'ENG-1284', title: 'Fix flaky test in payment-rollback path', who: 'Mira O.', whoBg: '#f97316', age: '5d', ageClass: '' },
{ id: 'ENG-1271', title: 'Refactor session token rotation (security review feedback)', who: 'Sam D.', whoBg: '#10b981', age: '3d', ageClass: 'warm' },
{ id: 'GROW-462', title: 'Wire Mixpanel funnel to onboarding step 3', who: 'Jules K.', whoBg: '#a855f7', age: '4d', ageClass: '' },
{ id: 'ENG-1268', title: 'Migrate cron worker to durable queue', who: 'Sam D.', whoBg: '#10b981', age: '6d', ageClass: '' },
{ id: 'DESIGN-118', title: 'Settings page redesign — needs eng pairing', who: 'Nora L.', whoBg: '#0ea5e9', age: '2d', ageClass: 'warm' }
];
document.getElementById('stuck').innerHTML = STUCK.map((s, i) => `
<div class="stuck-row" data-i="${i}">
<div>
<div class="id">${s.id}</div>
<div class="title">${s.title}</div>
</div>
<span class="who"><span class="av" style="background:${s.whoBg}">${s.who[0]}</span>${s.who}</span>
<span class="age ${s.ageClass}">stuck ${s.age}</span>
<button class="nudge" data-i="${i}">Nudge</button>
</div>
`).join('');
document.querySelectorAll('.stuck-row').forEach(row => {
const s = STUCK[+row.dataset.i];
row.querySelector('.nudge').addEventListener('click', e => {
e.stopPropagation();
toast({ kind: 'success', title: 'Nudge sent', body: `Sent ${s.who} a gentle Slack DM about <strong>${s.id}</strong>.` });
});
row.addEventListener('click', () => {
toast({ title: s.id, body: `<strong>${s.title}</strong> · stuck ${s.age} · assigned ${s.who}` });
});
});
// ─────── Schedule ───────
const NOW_HOUR = 8 * 60 + 42; // 8:42 AM
const SCHED = [
{ time: '7:30 AM', dur: '30m', title: 'Morning run', desc: 'Bay-side loop · Strava', source: 'CAL', t: 7*60 + 30 },
{ time: '9:00 AM', dur: '15m', title: 'Sync with Mira', desc: 'Pipeline review · 1:1', source: 'GMEET', t: 9*60 },
{ time: '10:00 AM', dur: '45m', title: 'Lattice Health follow-up', desc: 'Review SOC 2 + draft reply', source: 'FOCUS', t: 10*60 },
{ time: '11:30 AM', dur: '30m', title: 'Eng standup', desc: 'Fri release + Q3 capacity', source: 'GMEET', t: 11*60 + 30 },
{ time: '2:00 PM', dur: '60m', title: 'Pioneer Robotics — term sheet review', desc: 'Joint with Sam · legal walks through redlines', source: 'ZOOM', t: 14*60 },
{ time: '4:30 PM', dur: '20m', title: 'PR review block', desc: 'Clear the 4 PRs in queue', source: 'FOCUS', t: 16*60 + 30 }
];
document.getElementById('schedule').innerHTML = SCHED.map((s, i) => {
const past = s.t + parseInt(s.dur) < NOW_HOUR;
const now = s.t <= NOW_HOUR && NOW_HOUR < s.t + parseInt(s.dur);
const cls = past ? 'past' : (now ? 'now' : '');
return `
<div class="sched-row ${cls}" data-i="${i}">
<div class="time">${s.time}<span class="dur">${s.dur}</span></div>
<div class="dot"></div>
<div>
<div class="title">${s.title}</div>
<div class="desc">${s.desc}</div>
</div>
<span class="source">${s.source}</span>
</div>
`;
}).join('');
document.querySelectorAll('.sched-row').forEach(r => {
const s = SCHED[+r.dataset.i];
r.addEventListener('click', () => toast({ title: s.title, body: `${s.time} · ${s.dur} · ${s.source.toLowerCase()}<br/>${s.desc}` }));
});
// ─────── PRs ───────
const PRS = [
{ num: '#1284', title: 'Add session-token rotation primitive', files: '6 files', author: 'Sam', age: 'opened 18h ago', ageClass: 'urgent' },
{ num: '#1271', title: 'Migrate cron worker to durable queue (RFC implementation)', files: '14 files', author: 'Sam', age: '2d ago', ageClass: 'urgent' },
{ num: '#1268', title: 'Mixpanel funnel hooks for onboarding step 3', files: '4 files', author: 'Jules', age: '1d ago', ageClass: 'warm' },
{ num: '#1262', title: 'Settings page redesign · scaffolding only', files: '22 files', author: 'Nora', age: '4h ago', ageClass: 'calm' }
];
document.getElementById('prList').innerHTML = PRS.map((p, i) => `
<div class="pr-row" data-i="${i}">
<div>
<span class="num">${p.num} · @${p.author}</span>
<div class="title">${p.title}</div>
</div>
<span class="files">${p.files}</span>
<span class="age ${p.ageClass}">${p.age}</span>
</div>
`).join('');
document.querySelectorAll('.pr-row').forEach(r => {
const p = PRS[+r.dataset.i];
r.addEventListener('click', () => toast({ title: p.num + ' · ' + p.title, body: `${p.files} · @${p.author} · ${p.age}<br/>Opening on GitHub…` }));
});
// ─────── Toasts ───────
const toastBox = document.getElementById('toastBox');
function toast(opts) {
const { kind = '', title = '', body = '' } = (typeof opts === 'string') ? { body: opts } : opts;
const node = document.createElement('div');
node.className = `toast ${kind}`;
node.innerHTML = `<div class="t-title">${title}</div><div class="t-body">${body}</div>`;
toastBox.appendChild(node);
setTimeout(() => {
node.classList.add('fade');
setTimeout(() => node.remove(), 280);
}, 4200);
}
// ─────── Side actions + header ───────
const map = {
draft: { kind: 'success', title: 'Reply drafted', body: 'A short "we\'re ready when you are" is in your <strong>Lattice Health</strong> Gmail draft. Send when ready.' },
snooze1: { kind: 'warn', title: 'Snoozed', body: 'Lattice Health bumped to 11 AM.' },
generatePlan: { kind: '', title: 'Plan drafted', body: 'Next week\'s plan ready in Notion · 4 priorities · 14 commitments inferred from your week.' },
standup: { kind: 'success', title: 'Standup posted', body: '#eng-standup got a 4-line digest of yesterday\'s wins + today\'s focus.' },
snooze: { kind: 'warn', title: 'Briefing snoozed', body: 'See you in an hour. We\'ll re-pull fresh data.' },
email: { kind: 'success', title: 'Email scheduled', body: 'You\'ll get a copy of this briefing in your inbox at 7 PM.' }
};
document.querySelectorAll('[data-action]').forEach(b => {
b.addEventListener('click', () => {
const k = b.dataset.action;
if (map[k]) toast(map[k]);
});
});
document.getElementById('doneBtn').addEventListener('click', () => {
toast({ kind: 'success', title: 'Briefing done', body: '6 of 6 sections reviewed. See you tomorrow at 6:42 AM.' });
});
document.getElementById('snoozeBtn').addEventListener('click', () => {
toast({ kind: 'warn', title: 'Briefing snoozed', body: 'Re-pulling at 9:42 AM.' });
});
})();
</script>
</body>
</html>

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,16 @@
{
"title": "Quiver Live · Personal Portfolio",
"slug": "quiver-live-portfolio",
"pinned": true,
"preview": {
"type": "html",
"entry": "index.html"
},
"document": {
"format": "html_template_v1",
"templatePath": "template.html",
"generatedPreviewPath": "index.html",
"dataPath": "data.json",
"dataJson": {}
}
}

View file

@ -0,0 +1,453 @@
{
"summary": {
"brand": "quiver",
"pro": "Live",
"marketState": "NYSE Open · 14:32:18 EST",
"lastSync": "14:32:18 EST",
"totalLabel": "Total portfolio value · USD",
"accountsLabel": "All accounts (Robinhood + IBKR)",
"totalValue": "$284,521.64",
"todayPnl": "+$3,182.40",
"todayPct": "+1.13%",
"todayDirClass": "up",
"todayArrow": "▲",
"allTimePnl": "+$84,521.64",
"allTimePct": "+42.3%",
"allTimeDirClass": "up",
"allTimeArrow": "▲",
"alphaLabel": "vs S&P · YTD",
"alpha": "+18.7pp",
"areaPoints": "10,150 10,128 60,124 110,116 160,118 210,108 260,114 310,98 360,90 410,96 460,80 510,72 560,76 610,60 660,54 710,46 760,40 810,32 860,28 870,28 870,150",
"linePoints": "10,128 60,124 110,116 160,118 210,108 260,114 310,98 360,90 410,96 460,80 510,72 560,76 610,60 660,54 710,46 760,40 810,32 860,28",
"markerX": "860",
"markerY": "28",
"axisTicks": [
{ "label": "May '24" },
{ "label": "Jul" },
{ "label": "Sep" },
{ "label": "Nov" },
{ "label": "Jan '25" },
{ "label": "Mar" },
{ "label": "Today" }
]
},
"kpis": [
{
"label": "Today's P&L",
"value": "+$3,182.40",
"valueClass": "up",
"sub": "+1.13% · 8 winners / 2 losers",
"subClass": "up"
},
{
"label": "Buying power",
"value": "$12,438.21",
"valueClass": "",
"sub": "Cash + 2× margin available",
"subClass": ""
},
{
"label": "Best position · NVDA",
"value": "+187.4%",
"valueClass": "up",
"sub": "Avg cost $321.55 · Now $924.31",
"subClass": "up"
},
{
"label": "Alpha vs S&P 500 · YTD",
"value": "+18.7pp",
"valueClass": "alpha",
"sub": "Sharpe 1.42 · Beta 1.18 · Max DD 9.4%",
"subClass": ""
}
],
"featured": {
"iconText": "N",
"iconBg": "linear-gradient(135deg,#76b900,#4d8400)",
"iconFg": "#0a1502",
"symbol": "NVDA",
"name": "NVIDIA Corporation",
"sector": "Semiconductors · Mkt cap $2.27T",
"exchangeName": "NASDAQ",
"exchangeUrl": "https://www.nasdaq.com/market-activity/stocks/nvda",
"yahooUrl": "https://finance.yahoo.com/quote/NVDA",
"tradingViewUrl": "https://www.tradingview.com/symbols/NASDAQ-NVDA/",
"price": "$924.31",
"delta": "▲ +$21.74 +2.41% today",
"dirClass": "up",
"stats": [
{ "label": "Open", "value": "906.80" },
{ "label": "High", "value": "929.18" },
{ "label": "Low", "value": "902.44" },
{ "label": "Volume", "value": "38.42M" },
{ "label": "P/E", "value": "62.4" },
{ "label": "52W range", "value": "412 — 974" }
],
"areaPoints": "40,260 40,220 80,210 120,200 160,194 200,184 240,180 280,170 320,160 360,150 400,138 440,128 480,120 520,108 560,100 600,86 640,76 680,64 720,56 760,48 800,40 840,30 850,30 850,260",
"linePoints": "40,220 80,210 120,200 160,194 200,184 240,180 280,170 320,160 360,150 400,138 440,128 480,120 520,108 560,100 600,86 640,76 680,64 720,56 760,48 800,40 840,30",
"priceY": "30",
"priceLabelY": "21",
"priceLabelTextY": "34",
"priceLabelText": "924.31"
},
"ai": {
"tag": "Quiver AI · Signal #2417",
"headlineAccent": "Add to NVDA",
"headlineRest": "on Blackwell ramp + AI capex inflection.",
"sub": "Three converging catalysts and resilient channel checks. Conviction has stepped up since last week's hyperscaler guidance.",
"conviction": "87%",
"convictionWidth": "87%",
"targetLabel": "12-mo target",
"target": "$1,050",
"upside": "+13.6%",
"size": "+12 sh",
"sizeSub": "≈ $11,092 · 4.2% port",
"catalysts": [
{
"n": "01",
"title": "Blackwell shipments accelerating",
"text": "Foxconn and Wiwynn guidance both implied an Aug volume step-up. Backlog visibility extends through Q1 '26."
},
{
"n": "02",
"title": "Hyperscaler capex revised up",
"text": "MSFT, META, AMZN combined FY '25 capex now +34% YoY. AI infra is the binding allocation."
},
{
"n": "03",
"title": "Sentiment de-risked",
"text": "Short interest at 6-month high but RSI cooling from overbought. Setup mirrors the May '24 base before the +47% leg."
}
]
},
"holdingsPanel": {
"title": "Holdings · 9 positions",
"subtitle": "Live · last quote 14:32:18 EST"
},
"holdings": [
{
"symbol": "NVDA",
"companyName": "NVIDIA Corporation",
"icon": "N",
"iconBg": "linear-gradient(135deg,#76b900,#3d6800)",
"iconFg": "#0a1502",
"exchName": "NASDAQ",
"exchUrl": "https://www.nasdaq.com/market-activity/stocks/nvda",
"shares": "100",
"avgCost": "321.55",
"last": "924.31",
"marketValue": "$92,431",
"pnl": "+$60,276",
"pnlPct": "+187.4%",
"dirClass": "up",
"alloc": "32.5%",
"allocWidth": "32%"
},
{
"symbol": "AAPL",
"companyName": "Apple Inc.",
"icon": "A",
"iconBg": "linear-gradient(135deg,#0c0c0c,#3a3a3a)",
"iconFg": "#ffffff",
"exchName": "NASDAQ",
"exchUrl": "https://www.nasdaq.com/market-activity/stocks/aapl",
"shares": "220",
"avgCost": "152.10",
"last": "228.74",
"marketValue": "$50,323",
"pnl": "+$16,861",
"pnlPct": "+50.4%",
"dirClass": "up",
"alloc": "17.7%",
"allocWidth": "18%"
},
{
"symbol": "MSFT",
"companyName": "Microsoft Corp.",
"icon": "M",
"iconBg": "linear-gradient(135deg,#0078d4,#005a9e)",
"iconFg": "#ffffff",
"exchName": "NASDAQ",
"exchUrl": "https://www.nasdaq.com/market-activity/stocks/msft",
"shares": "85",
"avgCost": "328.40",
"last": "438.12",
"marketValue": "$37,240",
"pnl": "+$9,326",
"pnlPct": "+33.4%",
"dirClass": "up",
"alloc": "13.1%",
"allocWidth": "13%"
},
{
"symbol": "META",
"companyName": "Meta Platforms",
"icon": "M",
"iconBg": "linear-gradient(135deg,#1877f2,#0c5fc3)",
"iconFg": "#ffffff",
"exchName": "NASDAQ",
"exchUrl": "https://www.nasdaq.com/market-activity/stocks/meta",
"shares": "55",
"avgCost": "312.20",
"last": "596.18",
"marketValue": "$32,790",
"pnl": "+$15,619",
"pnlPct": "+91.0%",
"dirClass": "up",
"alloc": "11.5%",
"allocWidth": "11%"
},
{
"symbol": "GOOGL",
"companyName": "Alphabet Class A",
"icon": "G",
"iconBg": "linear-gradient(135deg,#4285f4,#34a853)",
"iconFg": "#ffffff",
"exchName": "NASDAQ",
"exchUrl": "https://www.nasdaq.com/market-activity/stocks/googl",
"shares": "160",
"avgCost": "128.90",
"last": "186.40",
"marketValue": "$29,824",
"pnl": "+$9,200",
"pnlPct": "+44.6%",
"dirClass": "up",
"alloc": "10.5%",
"allocWidth": "10%"
},
{
"symbol": "AMZN",
"companyName": "Amazon.com Inc.",
"icon": "A",
"iconBg": "linear-gradient(135deg,#ff9900,#cc7a00)",
"iconFg": "#1a0e00",
"exchName": "NASDAQ",
"exchUrl": "https://www.nasdaq.com/market-activity/stocks/amzn",
"shares": "90",
"avgCost": "142.50",
"last": "218.94",
"marketValue": "$19,705",
"pnl": "+$6,880",
"pnlPct": "+53.6%",
"dirClass": "up",
"alloc": "6.9%",
"allocWidth": "7%"
},
{
"symbol": "TSLA",
"companyName": "Tesla, Inc.",
"icon": "T",
"iconBg": "linear-gradient(135deg,#cc0000,#660000)",
"iconFg": "#ffffff",
"exchName": "NASDAQ",
"exchUrl": "https://www.nasdaq.com/market-activity/stocks/tsla",
"shares": "42",
"avgCost": "288.40",
"last": "312.55",
"marketValue": "$13,127",
"pnl": "+$1,015",
"pnlPct": "+8.4%",
"dirClass": "up",
"alloc": "4.6%",
"allocWidth": "5%"
},
{
"symbol": "AMD",
"companyName": "Advanced Micro Devices",
"icon": "A",
"iconBg": "linear-gradient(135deg,#ed1c24,#7a0000)",
"iconFg": "#ffffff",
"exchName": "NASDAQ",
"exchUrl": "https://www.nasdaq.com/market-activity/stocks/amd",
"shares": "52",
"avgCost": "158.90",
"last": "142.06",
"marketValue": "$7,387",
"pnl": "$876",
"pnlPct": "10.6%",
"dirClass": "down",
"alloc": "2.6%",
"allocWidth": "3%"
},
{
"symbol": "USD",
"companyName": "Settled cash",
"icon": "$",
"iconBg": "linear-gradient(135deg,#facc15,#a17400)",
"iconFg": "#1a1100",
"exchName": "—",
"exchUrl": "#",
"shares": "—",
"avgCost": "—",
"last": "1.00",
"marketValue": "$1,694",
"pnl": "—",
"pnlPct": "—",
"dirClass": "",
"alloc": "0.6%",
"allocWidth": "1%"
}
],
"watchlistPanel": {
"title": "Watchlist · curated",
"subtitle": "12 tickers · 14:32 EST"
},
"watchlist": [
{
"symbol": "PLTR",
"companyName": "Palantir",
"price": "$58.42",
"pct": "▲ 4.81%",
"dirClass": "up",
"sparkColor": "#22e58c",
"sparkPoints": "0,28 20,30 40,26 60,24 80,28 100,22 120,18 140,20 160,14 180,10 200,6",
"volume": "4.21M",
"exchName": "NASDAQ",
"exchUrl": "https://www.nasdaq.com/market-activity/stocks/pltr"
},
{
"symbol": "SHOP",
"companyName": "Shopify",
"price": "$112.84",
"pct": "▲ 2.13%",
"dirClass": "up",
"sparkColor": "#22e58c",
"sparkPoints": "0,26 20,28 40,22 60,24 80,20 100,22 120,18 140,16 160,12 180,14 200,10",
"volume": "6.84M",
"exchName": "NYSE",
"exchUrl": "https://www.nyse.com/quote/XNYS:SHOP"
},
{
"symbol": "TSM",
"companyName": "TSMC",
"price": "$208.77",
"pct": "▲ 1.62%",
"dirClass": "up",
"sparkColor": "#22e58c",
"sparkPoints": "0,24 20,22 40,26 60,20 80,18 100,20 120,16 140,14 160,12 180,10 200,8",
"volume": "12.4M",
"exchName": "NYSE",
"exchUrl": "https://www.nyse.com/quote/XNYS:TSM"
},
{
"symbol": "COIN",
"companyName": "Coinbase",
"price": "$236.10",
"pct": "▲ 3.48%",
"dirClass": "up",
"sparkColor": "#22e58c",
"sparkPoints": "0,28 20,30 40,26 60,28 80,22 100,18 120,20 140,14 160,10 180,12 200,6",
"volume": "8.92M",
"exchName": "NASDAQ",
"exchUrl": "https://www.nasdaq.com/market-activity/stocks/coin"
},
{
"symbol": "CRWD",
"companyName": "CrowdStrike",
"price": "$352.09",
"pct": "▼ 1.24%",
"dirClass": "down",
"sparkColor": "#ff4f6d",
"sparkPoints": "0,14 20,12 40,16 60,14 80,18 100,20 120,18 140,22 160,24 180,28 200,30",
"volume": "3.24M",
"exchName": "NASDAQ",
"exchUrl": "https://www.nasdaq.com/market-activity/stocks/crwd"
},
{
"symbol": "SMCI",
"companyName": "Super Micro",
"price": "$48.93",
"pct": "▲ 5.62%",
"dirClass": "up",
"sparkColor": "#22e58c",
"sparkPoints": "0,30 20,32 40,28 60,30 80,26 100,20 120,16 140,18 160,12 180,8 200,4",
"volume": "18.2M",
"exchName": "NASDAQ",
"exchUrl": "https://www.nasdaq.com/market-activity/stocks/smci"
},
{
"symbol": "UBER",
"companyName": "Uber Technologies",
"price": "$72.41",
"pct": "▲ 1.18%",
"dirClass": "up",
"sparkColor": "#22e58c",
"sparkPoints": "0,24 20,26 40,22 60,20 80,22 100,18 120,16 140,18 160,14 180,12 200,12",
"volume": "7.42M",
"exchName": "NYSE",
"exchUrl": "https://www.nyse.com/quote/XNYS:UBER"
},
{
"symbol": "NFLX",
"companyName": "Netflix",
"price": "$728.50",
"pct": "▼ 0.66%",
"dirClass": "down",
"sparkColor": "#ff4f6d",
"sparkPoints": "0,16 20,14 40,18 60,16 80,18 100,16 120,20 140,18 160,22 180,20 200,24",
"volume": "2.86M",
"exchName": "NASDAQ",
"exchUrl": "https://www.nasdaq.com/market-activity/stocks/nflx"
}
],
"newsPanel": {
"title": "Live signals & news",
"subtitle": "Filtered to your book"
},
"news": [
{
"time": "14:31",
"source": "AI · Quiver Research",
"headline": "▲ Hyperscaler capex tracker rolls forward — MSFT + META combined FY '25 guide now +34% YoY, reinforcing NVDA/SMCI thesis.",
"impactLabel": "+ Bull",
"impactClass": "pos"
},
{
"time": "14:18",
"source": "Bloomberg",
"headline": "Tesla cuts Model Y prices in China by 6% as BYD pressure intensifies in Q2 sales window.",
"impactLabel": "High risk",
"impactClass": "high"
},
{
"time": "13:54",
"source": "Reuters",
"headline": "FOMC minutes signal one more cut probable in Sept — yields fall 9 bps on the long end.",
"impactLabel": "+ Bull",
"impactClass": "pos"
},
{
"time": "13:30",
"source": "AI · Earnings whisper",
"headline": "CRWD whisper revised 2.1% after channel checks show enterprise renewal slippage.",
"impactLabel": "Watch",
"impactClass": "med"
},
{
"time": "12:48",
"source": "CNBC",
"headline": "Apple confirms Vision Pro 2 component orders for Q4 — 1.4M unit run rate implied.",
"impactLabel": "+ Bull",
"impactClass": "pos"
},
{
"time": "11:22",
"source": "AI · Anomaly detector",
"headline": "▲ SMCI dark-pool prints up 220% vs 30-day baseline — likely block accumulation.",
"impactLabel": "Watch",
"impactClass": "med"
}
],
"footer": {
"live": "Live data — refresh on demand",
"source": "Quotes via IEX Cloud · Robinhood + IBKR linked",
"version": "Quiver Live · v1.4.0"
}
}

View file

@ -0,0 +1,19 @@
{
"generatedAt": "2026-05-06T19:32:18.000Z",
"generatedBy": "agent",
"notes": "Personal trading portfolio dashboard — total value, today's P&L, featured ticker chart, holdings table, watchlist, news/signals, and an AI-generated investment recommendation. Designed to be refreshable on demand: price/holdings/news fields can be rewritten in data.json without re-authoring the template. All numbers, percentages, and SVG polyline points are pre-formatted into display-ready strings so the template stays free of formatting/expression logic; direction is encoded via an explicit dirClass field. External links (exchange, Yahoo, TradingView, per-row exchange page) are bound as plain attribute interpolations — no inline scripting. Refresh runner should write a fresh data.json that matches the template's binding shape; daemon validates against bounded JSON limits and re-derives index.html from template.html + data.json. No credentials, OAuth tokens, cookies, raw provider responses, or HTTP envelopes are persisted; forbidden keys (raw, payload, body, headers, cookie, authorization, token, secret, credential, password) are not present in data.json.",
"sources": [
{
"label": "Brokerage holdings (mocked)",
"type": "connector"
},
{
"label": "Quotes (mocked)",
"type": "connector"
},
{
"label": "AI recommendation",
"type": "derived"
}
]
}

File diff suppressed because it is too large Load diff