Skip to content

Commit 77b0bbe

Browse files
committed
feat: refine dashboard reading experience
1 parent 8f0b4a4 commit 77b0bbe

8 files changed

Lines changed: 205 additions & 118 deletions

File tree

docs/design-system.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,8 @@ Zaati OS is a decision interface, not a marketing dashboard.
2323

2424
Repeated facts belong in a table or row. Empty cards, decorative KPIs, arbitrary bold text, and chart-shaped wallpaper do not belong.
2525

26+
Long lists and tables disclose an initial decision-sized set, with the remaining validated content available on demand. Metric values wrap without truncation. Line charts use a data-relative domain so meaningful change stays visible; categorical bar charts retain a zero baseline.
27+
2628
Complexity should come from hierarchy, not ornament. Use `dashboard` for a dominant view plus supporting evidence, `focus` for one leading decision, and `timeline` for a narrow sequence. A block may span one, two, or all dashboard columns. Do not simulate complexity with nested cards, arbitrary component trees, or repeated metrics.
2729

2830
## Components

docs/llm-contract.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,8 @@ Each source-specific domain schema requires stable `data.facts`. Facts carry dur
4545
| `notice` | One caveat, risk, or insight | Repeating normal content |
4646
| `text` | Short analysis that loses meaning when structured | Executable Markdown or HTML |
4747

48+
Calendar events that cover a date without a meaningful time should set `all_day: true`. Keep the required ISO `start` and optional `end` values for ordering and provenance; the renderer presents the event as “All day” instead of inventing a midnight appointment.
49+
4850
## Safety limits
4951

5052
The schema rejects unknown properties and limits block, row, point, item, series, text, and URL sizes. Links require HTTPS. Snapshots cannot contain scripts, HTML execution, private-key blocks, secret-shaped values, authentication links, raw messages, or source-specific forbidden content. Validation reports field paths and rule names without repeating the suspect value. The app never evaluates snapshot text.

schemas/ui-blocks.schema.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -184,6 +184,7 @@
184184
"title": { "type": "string", "minLength": 1, "maxLength": 120 },
185185
"start": { "type": "string", "format": "date-time" },
186186
"end": { "type": "string", "format": "date-time" },
187+
"all_day": { "type": "boolean" },
187188
"location": { "type": "string", "maxLength": 100 },
188189
"tone": { "$ref": "#/$defs/tone" }
189190
}

src/App.tsx

Lines changed: 27 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -260,7 +260,7 @@ function DashboardApp({ data }: { data: DashboardData }) {
260260
onSelect={select}
261261
selectedId={selectedId}
262262
/>
263-
<div className={cn("transition-[padding] duration-200 md:pl-64", sidebarCompact && "md:pl-[76px]")}>
263+
<div className={cn("transition-[padding] duration-200 md:pl-72", sidebarCompact && "md:pl-[76px]")}>
264264
<header className="sticky top-0 z-30 flex h-16 items-center justify-between border-b border-border/80 bg-background/95 px-4 backdrop-blur-md sm:px-6 lg:px-8">
265265
<div className="flex min-w-0 items-center gap-3">
266266
<Button
@@ -328,7 +328,7 @@ function DashboardApp({ data }: { data: DashboardData }) {
328328
</Dialog>
329329
</div>
330330
</header>
331-
<main className="mx-auto w-full max-w-[1500px] px-4 py-6 sm:px-6 lg:px-8 lg:py-8" id="main-content" tabIndex={-1}>
331+
<main className="mx-auto w-full max-w-[1380px] px-4 py-5 sm:px-6 lg:px-8 lg:py-8" id="main-content" tabIndex={-1}>
332332
{selectedId === START_ID ? (
333333
<Suspense fallback={<InlineLoading />}>
334334
<Onboarding
@@ -405,7 +405,7 @@ function Sidebar({
405405
</Dialog>
406406
<aside
407407
className={cn(
408-
"fixed inset-y-0 left-0 z-40 hidden w-64 flex-col border-r border-sidebar-border bg-sidebar text-sidebar-foreground transition-[width] duration-200 md:flex",
408+
"fixed inset-y-0 left-0 z-40 hidden w-72 flex-col border-r border-sidebar-border bg-sidebar text-sidebar-foreground transition-[width] duration-200 md:flex",
409409
compact && "md:w-[76px]",
410410
)}
411411
id="primary-navigation"
@@ -496,7 +496,7 @@ function SidebarPanel({ compact, data, now, onClose, onCompact, onSelect, select
496496
)}
497497
key={definition.id}
498498
onClick={() => onSelect(definition.id)}
499-
title={compact ? `${definition.label}, ${status}` : undefined}
499+
title={`${definition.label}, ${health[status].label}`}
500500
>
501501
<Icon className={cn("size-4 shrink-0 text-sidebar-foreground/55", active && "text-sidebar-primary")} />
502502
<span className={cn("min-w-0 flex-1 truncate", compact && "md:hidden")}>{definition.label}</span>
@@ -590,18 +590,36 @@ function DashboardPage({
590590
{prompt ? <PromptDrawer prompt={prompt} sourceLabel={definition.label} /> : null}
591591
</div>
592592
</div>
593-
<div className="mt-5 flex flex-col justify-between gap-5 border-b border-border pb-7 lg:flex-row lg:items-end">
593+
<div className="mt-4 flex flex-col justify-between gap-4 border-b border-border pb-6 lg:flex-row lg:items-end">
594594
<div className="max-w-3xl">
595595
<h1 className="text-pretty text-3xl font-semibold tracking-[-0.035em] sm:text-4xl">{snapshot.data.title}</h1>
596596
<p className="mt-3 max-w-2xl text-base leading-7 text-muted-foreground">{snapshot.data.summary}</p>
597597
</div>
598598
<details className="group shrink-0 rounded-lg border border-border bg-card text-xs text-muted-foreground lg:max-w-md">
599-
<summary className="flex min-h-10 cursor-pointer list-none items-center gap-3 px-3 py-2 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring">
599+
<summary className="flex min-h-10 cursor-pointer list-none items-center gap-2 px-3 py-2 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring">
600600
<span aria-hidden="true" className={cn("size-2 rounded-full", health[freshness].dot)} />
601601
<span>{health[freshness].label}</span>
602+
{snapshot.quality.warnings.length ? (
603+
<span className="rounded-full bg-warning/10 px-2 py-0.5 text-[11px] font-medium text-warning-foreground">
604+
{snapshot.quality.warnings.length} note{snapshot.quality.warnings.length === 1 ? "" : "s"}
605+
</span>
606+
) : null}
602607
<ChevronDown aria-hidden="true" className="size-3.5 opacity-50 transition-transform group-open:rotate-180" />
603608
</summary>
604-
<div className="border-t border-border px-3 py-3">
609+
<div className="max-h-[min(70vh,34rem)] overflow-y-auto border-t border-border px-3 py-3">
610+
{snapshot.quality.warnings.length ? (
611+
<div className="mb-3 rounded-md bg-warning/10 px-3 py-2.5 text-foreground/80">
612+
<p className="font-medium text-foreground">Evidence notes</p>
613+
<ul className="mt-1.5 space-y-1.5 leading-relaxed">
614+
{snapshot.quality.warnings.map((warning) => (
615+
<li className="flex gap-2" key={warning}>
616+
<span aria-hidden="true" className="mt-1.5 size-1 shrink-0 rounded-full bg-warning" />
617+
<span>{warning}</span>
618+
</li>
619+
))}
620+
</ul>
621+
</div>
622+
) : null}
605623
<dl className="grid grid-cols-[auto_1fr] gap-x-3 gap-y-1.5">
606624
<dt>Generated</dt>
607625
<dd className="text-foreground">{formatTimestamp(snapshot.generated_at, instance)}</dd>
@@ -640,27 +658,14 @@ function DashboardPage({
640658
</div>
641659
</details>
642660
</div>
643-
{snapshot.quality.warnings.length ? (
644-
<div className="mt-5 flex gap-3 rounded-xl border border-warning/30 bg-warning/10 p-4">
645-
<RefreshCcw className="mt-0.5 size-4 shrink-0 text-warning-foreground" />
646-
<div>
647-
<p className="text-sm font-medium">Evidence note</p>
648-
{snapshot.quality.warnings.map((warning) => (
649-
<p className="mt-1 text-sm leading-relaxed text-foreground/80" key={warning}>
650-
{warning}
651-
</p>
652-
))}
653-
</div>
654-
</div>
655-
) : null}
656-
<div className={cn("mt-5 grid grid-cols-1 gap-4", blockGrid)} data-layout={layout}>
661+
<div className={cn("mt-5 grid grid-cols-1 gap-5", blockGrid)} data-layout={layout}>
657662
{snapshot.data.presentation.blocks.map((block, index) => (
658663
<BlockRenderer block={block} emphasized={layout === "focus" && index === 0} instance={instance} key={block.id} layout={layout} />
659664
))}
660665
</div>
661666
<footer className="mt-8 flex flex-col justify-between gap-3 border-t border-border pt-5 text-xs text-muted-foreground sm:flex-row">
662667
<span className="inline-flex items-center gap-1.5">
663-
<Clock3 className="size-3.5" /> Last successful update {formatTimestamp(snapshot.generated_at, instance)}
668+
<Clock3 className="size-3.5" /> Updated {formatTimestamp(snapshot.generated_at, instance)}
664669
</span>
665670
<span>
666671
{snapshot.quality.confidence} confidence, {snapshot.sources.length} source{snapshot.sources.length === 1 ? "" : "s"}, expires{" "}

0 commit comments

Comments
 (0)