Implement dashboard composition (task 20)

- Add default_dashboard_layout jsonb to users + migration 0007
- Create _core/manifest.tsx with core.activity placeholder widget
- Register core manifest alongside calendar/lists/notes
- Update all three module manifests with real async server-component
  widget renders (upcoming events, month view, list items, notes)
- Add src/lib/dashboard.ts: computeDefaultLayout greedy packer +
  parseDashboardLayout Zod validator
- Build src/app/page.tsx: 12-col CSS Grid, static smColSpan lookup,
  per-widget Suspense for parallel loading, generic widget.render()
  dispatch — no widgetId branches
- Add tests/e2e/dashboard.spec.ts; all 4 E2E specs pass

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
ginnoir
2026-05-06 13:24:56 -05:00
co-authored by Claude Sonnet 4.6
parent d7069d14a2
commit b4dde92757
11 changed files with 380 additions and 46 deletions
+43 -21
View File
@@ -1,10 +1,49 @@
import type { ModuleManifest } from "../_core/module";
import type { ModuleManifest, WidgetContext } from "../_core/module";
import { z } from "zod";
import { addItemToDefaultList } from "./server/actions";
import { listLists, searchItems, searchLists } from "./server/queries";
import { listLists, listWidgetItems, searchItems, searchLists } from "./server/queries";
const listIdsSchema = z.union([z.literal("all"), z.array(z.string().uuid())]);
const listWidgetConfigSchema = z.object({
listIds: listIdsSchema,
showCompleted: z.boolean(),
limit: z.number().int().min(1).max(50).optional(),
});
async function ListWidget({ config }: { config: unknown; ctx: WidgetContext }) {
const parsed = listWidgetConfigSchema.parse(config);
const items = await listWidgetItems({
listIds: parsed.listIds,
showCompleted: parsed.showCompleted,
limit: parsed.limit ?? 10,
});
if (items.length === 0) {
return (
<p className="text-sm text-muted-foreground">
{parsed.showCompleted ? "No items" : "No open items"}
</p>
);
}
return (
<ul className="space-y-1">
{items.map((item) => (
<li key={item.id} className="flex items-center gap-2 text-sm">
<span
className={`h-4 w-4 shrink-0 rounded-sm border border-border ${item.done ? "bg-muted" : ""}`}
/>
<span className={`truncate ${item.done ? "text-muted-foreground line-through" : ""}`}>
{item.text}
</span>
<span className="ml-auto shrink-0 text-xs text-muted-foreground">{item.listName}</span>
</li>
))}
</ul>
);
}
const manifest: ModuleManifest = {
id: "lists",
name: "Lists",
@@ -34,11 +73,7 @@ const manifest: ModuleManifest = {
defaultSize: { w: 4, h: 3 },
minSize: { w: 3, h: 2 },
defaultPriority: 30,
configSchema: z.object({
listIds: listIdsSchema,
showCompleted: z.boolean(),
limit: z.number().int().min(1).max(50).optional(),
}),
configSchema: listWidgetConfigSchema,
defaultConfig: { listIds: "all", showCompleted: false },
resolveConfigOptions: async () => ({
lists: (await listLists()).map((list) => ({
@@ -47,20 +82,7 @@ const manifest: ModuleManifest = {
name: list.name,
})),
}),
render: ({ config }) => {
const parsed = z
.object({
listIds: listIdsSchema,
showCompleted: z.boolean(),
limit: z.number().int().min(1).max(50).optional(),
})
.parse(config);
return (
<div className="text-sm text-muted-foreground">
{parsed.showCompleted ? "List items" : "Open list items"}
</div>
);
},
render: (props) => <ListWidget {...props} />,
},
],
quickAdds: [