Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
117 changes: 61 additions & 56 deletions src/renderer/components/dashboard/DashboardView.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -164,7 +164,7 @@ function HeroBand({ status, greeting, summary, onImport, onAdd }) {
{greeting}
</span>
</Heading>
<Text variant="small" className="mt-1">
<Text variant="small" className="dashboard-hero-summary mt-1">
{summary}
</Text>
</div>
Expand Down Expand Up @@ -262,7 +262,7 @@ function PipelineStage({ icon: Icon, label, value, tone, isEntryHint, onClick })
<Icon className="h-5 w-5" aria-hidden="true" />
</span>
<div
className={`mt-2 font-mono text-3xl font-semibold leading-none tracking-tight tabular-nums ${valueClass}`}
className={`dashboard-stage-value mt-2 font-mono text-3xl font-semibold leading-none tracking-tight tabular-nums ${valueClass}`}
>
<AnimatedCount value={value} />
</div>
Expand Down Expand Up @@ -322,48 +322,50 @@ function PipelineCard({
spans icon-center to icon-center (each of three equal columns centers
its icon at 1/6, 1/2, 5/6 of the width); the success-tinted fill grows
toward the Organized node as work completes. */}
<div className="relative mt-4 rounded-xl border border-border-soft/50 bg-system-gray-100/25 p-4">
<div
aria-hidden="true"
className="pointer-events-none absolute left-[16.66%] right-[16.66%] top-9 h-0.5 -translate-y-1/2 overflow-hidden rounded-full bg-system-gray-200/80"
>
<motion.div
className="relative h-full overflow-hidden rounded-full bg-gradient-to-r from-stratosort-warning/70 via-stratosort-blue/80 to-stratosort-success"
initial={reduceMotion ? false : { width: '0%' }}
animate={{ width: `${railPct}%` }}
transition={{ duration: 0.7, ease: [0.22, 1, 0.36, 1] }}
<div className="mt-[var(--dash-section-gap)] rounded-xl border border-border-soft/50 bg-system-gray-100/25 p-[var(--dash-inner-pad)] lg:flex lg:flex-1 lg:flex-col lg:justify-center">
<div className="relative">
<div
aria-hidden="true"
className="pointer-events-none absolute left-[16.66%] right-[16.66%] top-5 h-0.5 -translate-y-1/2 overflow-hidden rounded-full bg-system-gray-200/80"
>
{/* Travelling sheen only while work is actually moving — silenced under
<motion.div
className="relative h-full overflow-hidden rounded-full bg-gradient-to-r from-stratosort-warning/70 via-stratosort-blue/80 to-stratosort-success"
initial={reduceMotion ? false : { width: '0%' }}
animate={{ width: `${railPct}%` }}
transition={{ duration: 0.7, ease: [0.22, 1, 0.36, 1] }}
>
{/* Travelling sheen only while work is actually moving — silenced under
reduced motion by the utility's own media query (styles.css). */}
{inFlight > 0 ? <span aria-hidden="true" className="pipeline-rail-sheen" /> : null}
</motion.div>
</div>
<div className="relative z-10 flex items-start justify-between gap-compact">
<PipelineStage
icon={Sparkles}
label="Awaiting analysis"
value={stats.pending}
tone="warning"
isEntryHint={isEmpty}
onClick={onPendingStage}
/>
<PipelineStage
icon={FolderInput}
label="Ready to organize"
value={stats.ready}
tone="accent"
onClick={onReadyStage}
/>
<PipelineStage
icon={CheckCircle2}
label="Organized"
value={organizedCount}
tone="success"
onClick={onOrganizedStage}
/>
{inFlight > 0 ? <span aria-hidden="true" className="pipeline-rail-sheen" /> : null}
</motion.div>
</div>
<div className="relative z-10 flex items-start justify-between gap-compact">
<PipelineStage
icon={Sparkles}
label="Awaiting analysis"
value={stats.pending}
tone="warning"
isEntryHint={isEmpty}
onClick={onPendingStage}
/>
<PipelineStage
icon={FolderInput}
label="Ready to organize"
value={stats.ready}
tone="accent"
onClick={onReadyStage}
/>
<PipelineStage
icon={CheckCircle2}
label="Organized"
value={organizedCount}
tone="success"
onClick={onOrganizedStage}
/>
</div>
</div>
</div>
<div className="mt-4 flex items-center justify-between gap-cozy lg:mt-auto lg:pt-4">
<div className="mt-[var(--dash-section-gap)] flex items-center justify-between gap-cozy lg:mt-auto lg:pt-[var(--dash-section-gap)]">
<Text variant="tiny">{`${inFlight} of ${stats.total} files in flight`}</Text>
{stats.failed > 0 ? (
<Button variant="ghost" size="sm" onClick={onReviewFailed}>
Expand Down Expand Up @@ -412,19 +414,19 @@ function SmartFoldersCard({ folders, onManage, className }) {
Quick-actions card via lg:items-stretch), so a handful of folders read
as substantial cards instead of short pills floated at the top.
min-h keeps a floor for the many-folders case. */}
<div className="mt-4 grid flex-1 auto-rows-fr grid-cols-2 gap-compact lg:grid-cols-3">
<div className="mt-[var(--dash-section-gap)] grid flex-1 auto-rows-fr grid-cols-2 gap-compact lg:grid-cols-3">
{visible.map((folder) => (
<div
key={folder.id || folder.name}
className="flex min-h-[2.75rem] items-center gap-compact rounded-xl border border-border-soft/50 bg-system-gray-100/40 px-3 py-2 text-sm font-medium"
className="dashboard-chip flex min-h-[2.75rem] items-center gap-compact rounded-xl border border-border-soft/50 bg-system-gray-100/40 px-3 py-2 text-sm font-medium"
title={folder.name}
>
<FolderTree className="h-4 w-4 shrink-0 text-stratosort-warning" aria-hidden="true" />
<span className="truncate">{folder.name}</span>
</div>
))}
{overflow > 0 ? (
<div className="flex min-h-[2.75rem] items-center justify-center rounded-xl border border-dashed border-border-soft/50 px-3 py-2 text-sm text-system-gray-600">
<div className="dashboard-chip flex min-h-[2.75rem] items-center justify-center rounded-xl border border-dashed border-border-soft/50 px-3 py-2 text-sm text-system-gray-600">
{`+${overflow} more`}
</div>
) : null}
Expand All @@ -446,9 +448,9 @@ function QuickActionTile({ icon: Icon, title, description, onClick, tone }) {
type="button"
variant="interactive"
onClick={onClick}
className="group/tile interactive-lift text-left w-full h-full focus-visible:ring-2 focus-visible:ring-stratosort-blue focus-visible:ring-offset-2 focus-visible:outline-none"
className="dashboard-tile group/tile interactive-lift flex items-center text-left w-full h-full focus-visible:ring-2 focus-visible:ring-stratosort-blue focus-visible:ring-offset-2 focus-visible:outline-none"
>
<div className="flex items-start gap-cozy">
<div className="flex w-full items-start gap-cozy">
<span
className={`mt-0.5 flex h-9 w-9 shrink-0 items-center justify-center rounded-lg transition-transform duration-150 ease-out group-hover/tile:scale-105 ${
ACTION_TONE[tone] || ACTION_TONE.blue
Expand Down Expand Up @@ -510,7 +512,7 @@ function ActivityCard({
icon={FolderTree}
title="No activity yet"
description="Organized files will show up here as you confirm suggestions."
className="mt-4 lg:mt-0"
className="mt-[var(--dash-section-gap)] lg:mt-0"
action={
<Button variant="subtle" size="sm" onClick={onAddFiles}>
Add files to start
Expand Down Expand Up @@ -543,7 +545,7 @@ function ActivityCard({
);
})}
</Stack>
<div className="mt-4 lg:mt-auto lg:pt-4">
<div className="mt-[var(--dash-section-gap)] lg:mt-auto lg:pt-[var(--dash-section-gap)]">
<Button variant="ghost" size="sm" onClick={onViewQueue}>
{queue.some((file) => file.status === 'ready')
? 'Review ready files'
Expand All @@ -553,7 +555,7 @@ function ActivityCard({
</>
) : (
<>
<Stack gap="compact" className="mt-4">
<Stack gap="compact" className="mt-[var(--dash-section-gap)]">
{recent.map((file, idx) => (
<div
key={`${file.originalPath || file.newName || 'file'}-${idx}`}
Expand All @@ -571,7 +573,7 @@ function ActivityCard({
</div>
))}
</Stack>
<div className="mt-4 lg:mt-auto lg:pt-4">
<div className="mt-[var(--dash-section-gap)] lg:mt-auto lg:pt-[var(--dash-section-gap)]">
<Button variant="ghost" size="sm" onClick={onViewAll}>
View all results
</Button>
Expand Down Expand Up @@ -746,7 +748,7 @@ export default function DashboardView() {
return (
<div
{...dragProps}
className={`relative flex w-full flex-col rounded-2xl transition-shadow ${
className={`dashboard-view relative flex w-full flex-col rounded-2xl lg:flex-1 transition-shadow ${
isDragging ? 'ring-2 ring-stratosort-blue/40 ring-offset-2' : ''
}`}
>
Expand Down Expand Up @@ -775,7 +777,7 @@ export default function DashboardView() {
variants={STAGGER_VARIANTS}
initial="hidden"
animate="show"
className="relative z-10 flex flex-col gap-default"
className="relative z-10 flex flex-col gap-[var(--dash-gap)] lg:flex-1"
>
<motion.div variants={blockVariants}>
<HeroBand
Expand All @@ -787,14 +789,17 @@ export default function DashboardView() {
/>
</motion.div>

<motion.div variants={blockVariants} className="flex flex-col gap-default">
<motion.div
variants={blockVariants}
className="flex flex-col gap-[var(--dash-gap)] lg:flex-1"
>
{isAnalyzing && progress && <AnalysisProgress progress={progress} />}

{/* Live-status zone — the two surfaces that answer "what's happening?".
The pipeline holds the aggregate counts; the activity rail holds the
individual items behind those counts, so they are paired and share a
height via items-stretch rather than living in separate columns. */}
<div className="grid grid-cols-1 gap-default lg:grid-cols-[2fr_1fr] lg:items-stretch">
<div className="grid grid-cols-1 gap-[var(--dash-gap)] lg:grow lg:grid-cols-[minmax(0,2fr)_minmax(0,1fr)] lg:items-stretch">
<PipelineCard
stats={stats}
organizedCount={organizedCount}
Expand Down Expand Up @@ -827,10 +832,10 @@ export default function DashboardView() {
{/* Get-things-done zone — shortcuts to the working surfaces paired with
the smart-folder destinations they feed. Same 2fr/1fr split as the
status row above so the column gutter lines up down the page. */}
<div className="grid grid-cols-1 gap-default lg:grid-cols-[2fr_1fr] lg:items-stretch">
<div className="grid grid-cols-1 gap-[var(--dash-gap)] lg:grow lg:grid-cols-[minmax(0,2fr)_minmax(0,1fr)] lg:items-stretch">
<Card variant="default" className="h-full lg:flex lg:flex-col">
<SectionLabel>Quick actions</SectionLabel>
<div className="mt-4 grid grid-cols-1 gap-cozy sm:grid-cols-3 sm:items-stretch lg:flex-1">
<div className="mt-[var(--dash-section-gap)] grid grid-cols-1 gap-cozy sm:grid-cols-3 sm:items-stretch lg:flex-1">
<QuickActionTile
icon={FolderPlus}
title="Add & analyze files"
Expand Down Expand Up @@ -865,7 +870,7 @@ export default function DashboardView() {
{/* Knowledge layer — a pride-surface summary of every file the local
AI has processed. Sits at the foot of the board so it reads as a
reward / closing thought after the pipeline and quick-action rows. */}
<KnowledgeStatsCard />
<KnowledgeStatsCard className="lg:flex lg:grow lg:flex-col" />
</motion.div>
</motion.div>
</div>
Expand Down
41 changes: 27 additions & 14 deletions src/renderer/components/dashboard/KnowledgeStatsCard.jsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import React, { useEffect, useState } from 'react';
import PropTypes from 'prop-types';
import { CalendarRange, Archive, HardDrive, Tags } from 'lucide-react';
import { formatBytes } from '../../utils/format';
import { isIsoDateOnly } from '../../../shared/contentDateSanity';
Expand Down Expand Up @@ -59,17 +60,22 @@ function isoToYear(iso) {
* @param {{ icon: React.ElementType, value: string, label: string }} props
*/
function StatTile({ icon: Icon, value, label }) {
// dashboard-stat / dashboard-stat-text: on short windows the dashboard
// density rules (styles.css, .dashboard-view) lay the chip beside the
// value instead of above it, saving roughly 50px of height.
return (
<div className="flex flex-col items-center text-center">
<span className="flex h-10 w-10 items-center justify-center rounded-xl bg-stratosort-blue/12 text-stratosort-blue">
<div className="dashboard-stat flex flex-col items-center text-center">
<span className="flex h-10 w-10 shrink-0 items-center justify-center rounded-xl bg-stratosort-blue/12 text-stratosort-blue">
<Icon className="h-5 w-5" aria-hidden="true" />
</span>
<div className="mt-2 font-mono text-2xl font-semibold leading-none tracking-tight tabular-nums text-stratosort-blue">
{value}
<div className="dashboard-stat-text mt-2">
<div className="font-mono text-2xl font-semibold leading-none tracking-tight tabular-nums text-stratosort-blue">
{value}
</div>
<Text variant="tiny" className="mt-1">
{label}
</Text>
</div>
<Text variant="tiny" className="mt-1">
{label}
</Text>
</div>
);
}
Expand All @@ -78,8 +84,11 @@ function StatTile({ icon: Icon, value, label }) {
* KnowledgeStatsCard — a "pride-surface" summary of everything the user's
* local AI has seen. Fetches once on mount via the preload bridge; no Redux
* dependency (all data comes from analysisHistory, not the file pipeline).
*
* @param {{ className?: string }} props - className lets the dashboard make the
* card fill its row (the stats row then centers in the extra height).
*/
export default function KnowledgeStatsCard() {
export default function KnowledgeStatsCard({ className = '' }) {
const [stats, setStats] = useState(/** @type {KnowledgeStats | null} */ (null));
const [error, setError] = useState(false);

Expand Down Expand Up @@ -108,7 +117,7 @@ export default function KnowledgeStatsCard() {
// API unavailable or thrown
if (error) {
return (
<Card variant="default">
<Card variant="default" className={className}>
<SectionLabel>Your knowledge layer</SectionLabel>
<Text variant="tiny" className="mt-compact block text-system-gray-500" role="alert">
Knowledge stats unavailable.
Expand All @@ -120,7 +129,7 @@ export default function KnowledgeStatsCard() {
// Loading
if (stats === null) {
return (
<Card variant="default">
<Card variant="default" className={className}>
<SectionLabel>Your knowledge layer</SectionLabel>
<div role="status" className="mt-compact text-micro text-system-gray-500">
Loading…
Expand All @@ -137,7 +146,7 @@ export default function KnowledgeStatsCard() {
// banner carries the detail).
if (stats.isEmpty === undefined && stats.totalFiles === undefined) {
return (
<Card variant="default">
<Card variant="default" className={className}>
<SectionLabel>Your knowledge layer</SectionLabel>
<Text variant="tiny" className="mt-compact block text-system-gray-500" role="alert">
Knowledge stats unavailable.
Expand All @@ -149,7 +158,7 @@ export default function KnowledgeStatsCard() {
// Empty state — explicit zero-entry stats only (see the degraded guard above).
if (stats.isEmpty || !(Number(stats.totalFiles) > 0)) {
return (
<Card variant="default">
<Card variant="default" className={className}>
<SectionLabel>Your knowledge layer</SectionLabel>
<Text variant="small" className="mt-compact block text-system-gray-600">
Analyze files to build your knowledge layer.
Expand Down Expand Up @@ -195,10 +204,10 @@ export default function KnowledgeStatsCard() {
);

return (
<Card variant="default">
<Card variant="default" className={className}>
<SectionLabel>Your knowledge layer</SectionLabel>
<div
className={`mt-cozy grid grid-cols-2 gap-cozy ${
className={`mt-cozy grid grid-cols-2 gap-cozy lg:flex-1 lg:content-center ${
tiles.length === 4 ? 'sm:grid-cols-4' : 'sm:grid-cols-3'
}`}
>
Expand All @@ -209,3 +218,7 @@ export default function KnowledgeStatsCard() {
</Card>
);
}

KnowledgeStatsCard.propTypes = {
className: PropTypes.string
};
Loading
Loading