feat(web/ui6): migrate dashboard, workspaces, search, chat pages
Phase UI6 — the three highest-traffic operator surfaces plus chat
move off legacy globals onto @bytelyst/ui Card + Badge + Input +
Select + Textarea + Button primitives via the local adapter.
dashboard/page.tsx:
- Welcome card, Saved views card, Quick links card, Operator workflows
card, Recent note activity card — all section.surface-card → Card.
- All saved-view/quick-link/workflow/note rows: surface-muted with
inline styles → grid+rounded+bg utility classes with hover state.
- All inline 'badge' spans (scope, status, tags) → Badge with
semantic variants (workflow status maps to warning/success).
workspaces/page.tsx:
- Saved-views aside, filter section, workspace article rows, error
banner — all surface-card → Card.
- Filter input → Input. Visibility/owner/tag chips → Badge.
- Workspace-note rows → utility-class hover panels.
search/page.tsx:
- POST /notes/search action chip → Badge.
- Saved searches aside + results pane — both surface-card → Card.
- '+ Save current' button + per-view Remove button — raw <button>
→ Button (size sm, ghost variant for Remove).
- Search input + filter chips + result rows — Input + Badge +
utility-class panels.
chat/page.tsx:
- Workspace <select> → Select with options=[{value,label}].
- Question <textarea> → Textarea.
Ratchet impact for this commit:
raw interactive controls 25 → 19 (-6)
legacy global surface classes 67 → 38 (-29)
Cumulative since session start (38/92 baseline):
raw 38 → 19 (-19)
legacy 92 → 38 (-54)
Verified: pnpm typecheck, test (96/96), audit:ui:ratchet at new
baseline.
This commit is contained in:
parent
2408f43426
commit
8d484c30d1
@ -3,7 +3,7 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import Link from "next/link";
|
||||
import { AppShell } from "@/components/AppShell";
|
||||
import { Badge, Button, Card } from "@/components/ui/Primitives";
|
||||
import { Badge, Button, Card, Select, Textarea } from "@/components/ui/Primitives";
|
||||
import { chatOverWorkspace, listWorkspaceSummaries } from "@/lib/notes-client";
|
||||
import type { WorkspaceSummary } from "@/lib/types";
|
||||
import { toast } from "@/lib/toast";
|
||||
@ -53,19 +53,16 @@ export default function ChatPage() {
|
||||
Create a workspace first (Dashboard seed or Workspaces), then return here.
|
||||
</p>
|
||||
) : (
|
||||
<select className="input-shell" value={workspaceId} onChange={(e) => setWorkspaceId(e.target.value)}>
|
||||
{workspaces.map((w) => (
|
||||
<option key={w.id} value={w.id}>
|
||||
{w.name}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
<Select
|
||||
value={workspaceId}
|
||||
onChange={(e) => setWorkspaceId(e.target.value)}
|
||||
options={workspaces.map((w) => ({ value: w.id, label: w.name }))}
|
||||
/>
|
||||
)}
|
||||
</label>
|
||||
<label style={{ display: "grid", gap: 8 }}>
|
||||
<span style={{ fontWeight: 600 }}>Question</span>
|
||||
<textarea
|
||||
className="input-shell"
|
||||
<Textarea
|
||||
rows={4}
|
||||
value={message}
|
||||
onChange={(e) => setMessage(e.target.value)}
|
||||
|
||||
@ -6,7 +6,7 @@ import { Suspense, useCallback, useEffect, useState } from "react";
|
||||
import { AppShell } from "@/components/AppShell";
|
||||
import { CreateNoteModal } from "@/components/CreateNoteModal";
|
||||
import { StateNotice } from "@/components/StateNotice";
|
||||
import { Button, Card } from "@/components/ui/Primitives";
|
||||
import { Badge, Button, Card } from "@/components/ui/Primitives";
|
||||
import { listNoteSummaries, listWorkspaceSummaries, seedOnboardingWorkspace } from "@/lib/notes-client";
|
||||
import { listApprovalQueue } from "@/lib/review-client";
|
||||
import { listSavedViews, type SavedView } from "@/lib/saved-views-client";
|
||||
@ -174,7 +174,7 @@ function DashboardContent() {
|
||||
</section>
|
||||
|
||||
{workspaces.length === 0 ? (
|
||||
<section className="surface-card" style={{ padding: "var(--nl-space-5)", display: "grid", gap: "var(--nl-space-3)" }}>
|
||||
<Card padding="md" className="grid gap-3">
|
||||
<strong>Welcome — create a sample workspace</strong>
|
||||
<p style={{ margin: 0, color: "var(--nl-text-secondary)" }}>
|
||||
Seeds a "Getting started" workspace with sample notes and one agent item in the review queue (backend flag{" "}
|
||||
@ -201,18 +201,18 @@ function DashboardContent() {
|
||||
>
|
||||
Seed sample workspace
|
||||
</Button>
|
||||
</section>
|
||||
</Card>
|
||||
) : null}
|
||||
|
||||
<section style={{ display: "grid", gridTemplateColumns: "minmax(0, 1.1fr) minmax(320px, 0.9fr)", gap: "var(--nl-space-4)" }}>
|
||||
<section className="surface-card" style={{ padding: "var(--nl-space-6)", display: "grid", gap: "var(--nl-space-4)" }}>
|
||||
<Card padding="lg" className="grid gap-4">
|
||||
<div style={{ fontSize: "var(--nl-fs-xl)", fontWeight: 700 }}>Saved views</div>
|
||||
<p style={{ margin: 0, color: "var(--nl-text-secondary)", fontSize: "var(--nl-fs-sm)" }}>
|
||||
Views stored in your account (Search page can add more). Below that, quick links stay on the dashboard for one-tap access.
|
||||
</p>
|
||||
<div style={{ display: "grid", gap: "var(--nl-space-3)" }}>
|
||||
{sortedSaved.length === 0 ? (
|
||||
<div className="surface-muted" style={{ padding: "var(--nl-space-4)", color: "var(--nl-text-secondary)" }}>
|
||||
<div className="rounded-[var(--nl-radius-sm)] bg-[color:var(--nl-surface-muted)] p-4 text-[color:var(--nl-text-secondary)]">
|
||||
No saved views yet. Open <Link href="/search">Search</Link> and save a query to pin it here.
|
||||
</div>
|
||||
) : (
|
||||
@ -220,12 +220,11 @@ function DashboardContent() {
|
||||
<Link
|
||||
key={view.id}
|
||||
href={hrefForSavedView(view)}
|
||||
className="surface-muted"
|
||||
style={{ padding: "var(--nl-space-4)", display: "grid", gap: "var(--nl-space-2)" }}
|
||||
className="grid gap-2 rounded-[var(--nl-radius-sm)] bg-[color:var(--nl-surface-muted)] p-4 transition-colors hover:bg-[color:var(--nl-surface-elevated)]"
|
||||
>
|
||||
<div style={{ display: "flex", justifyContent: "space-between", gap: "var(--nl-space-3)", flexWrap: "wrap" }}>
|
||||
<div className="flex flex-wrap items-center justify-between gap-3">
|
||||
<strong>{view.name}</strong>
|
||||
<span className="badge">{view.scope}</span>
|
||||
<Badge variant="neutral">{view.scope}</Badge>
|
||||
</div>
|
||||
{view.description ? (
|
||||
<div style={{ color: "var(--nl-text-secondary)" }}>{view.description}</div>
|
||||
@ -245,12 +244,11 @@ function DashboardContent() {
|
||||
<Link
|
||||
key={view.id}
|
||||
href={view.href}
|
||||
className="surface-muted"
|
||||
style={{ padding: "var(--nl-space-4)", display: "grid", gap: "var(--nl-space-2)" }}
|
||||
className="grid gap-2 rounded-[var(--nl-radius-sm)] bg-[color:var(--nl-surface-muted)] p-4 transition-colors hover:bg-[color:var(--nl-surface-elevated)]"
|
||||
>
|
||||
<div style={{ display: "flex", justifyContent: "space-between", gap: "var(--nl-space-3)", flexWrap: "wrap" }}>
|
||||
<div className="flex flex-wrap items-center justify-between gap-3">
|
||||
<strong>{view.name}</strong>
|
||||
<span className="badge">{view.scope}</span>
|
||||
<Badge variant="neutral">{view.scope}</Badge>
|
||||
</div>
|
||||
<div style={{ color: "var(--nl-text-secondary)" }}>{view.description}</div>
|
||||
<div style={{ display: "flex", justifyContent: "space-between", gap: "var(--nl-space-3)", flexWrap: "wrap" }}>
|
||||
@ -260,21 +258,20 @@ function DashboardContent() {
|
||||
</Link>
|
||||
))}
|
||||
</div>
|
||||
</section>
|
||||
</Card>
|
||||
|
||||
<section className="surface-card" style={{ padding: "var(--nl-space-6)", display: "grid", gap: "var(--nl-space-4)" }}>
|
||||
<Card padding="lg" className="grid gap-4">
|
||||
<div style={{ fontSize: "var(--nl-fs-xl)", fontWeight: 700 }}>Operator workflows</div>
|
||||
<div style={{ display: "grid", gap: "var(--nl-space-3)" }}>
|
||||
{operatorWorkflows.map((workflow) => (
|
||||
<Link
|
||||
key={workflow.id}
|
||||
href={getWorkflowHref(workflow)}
|
||||
className="surface-muted"
|
||||
style={{ padding: "var(--nl-space-4)", display: "grid", gap: "var(--nl-space-2)" }}
|
||||
className="grid gap-2 rounded-[var(--nl-radius-sm)] bg-[color:var(--nl-surface-muted)] p-4 transition-colors hover:bg-[color:var(--nl-surface-elevated)]"
|
||||
>
|
||||
<div style={{ display: "flex", justifyContent: "space-between", gap: "var(--nl-space-3)", flexWrap: "wrap" }}>
|
||||
<div className="flex flex-wrap items-center justify-between gap-3">
|
||||
<strong>{workflow.name}</strong>
|
||||
<span className="badge">{workflow.status}</span>
|
||||
<Badge variant={workflow.status === "at_risk" ? "warning" : "success"}>{workflow.status}</Badge>
|
||||
</div>
|
||||
<div style={{ color: "var(--nl-text-secondary)" }}>Owner: {workflow.owner}</div>
|
||||
<div style={{ display: "flex", justifyContent: "space-between", gap: "var(--nl-space-3)", flexWrap: "wrap" }}>
|
||||
@ -284,10 +281,10 @@ function DashboardContent() {
|
||||
</Link>
|
||||
))}
|
||||
</div>
|
||||
</section>
|
||||
</Card>
|
||||
</section>
|
||||
|
||||
<section className="surface-card" style={{ padding: "var(--nl-space-6)", display: "grid", gap: "var(--nl-space-4)" }}>
|
||||
<Card padding="lg" className="grid gap-4">
|
||||
<div style={{ fontSize: "var(--nl-fs-xl)", fontWeight: 700 }}>Recent note activity</div>
|
||||
{errorState ? <StateNotice state={errorState} onAction={() => void loadDashboard()} compact /> : null}
|
||||
<div style={{ display: "grid", gap: "var(--nl-space-3)" }}>
|
||||
@ -295,27 +292,27 @@ function DashboardContent() {
|
||||
<StateNotice state={getEmptyState("backend", "recent notes")} compact />
|
||||
) : null}
|
||||
{recentNotes.map((note) => (
|
||||
<div key={note.id} className="surface-muted" style={{ padding: "var(--nl-space-4)", display: "grid", gap: "var(--nl-space-2)" }}>
|
||||
<div style={{ display: "flex", justifyContent: "space-between", gap: "var(--nl-space-3)", flexWrap: "wrap" }}>
|
||||
<div key={note.id} className="grid gap-2 rounded-[var(--nl-radius-sm)] bg-[color:var(--nl-surface-muted)] p-4">
|
||||
<div className="flex flex-wrap items-center justify-between gap-3">
|
||||
<Link href={`/notes/${note.id}`}>
|
||||
<strong>{note.title}</strong>
|
||||
</Link>
|
||||
<span style={{ color: "var(--nl-text-secondary)" }}>{note.updatedBy}</span>
|
||||
<span className="text-[color:var(--nl-text-secondary)]">{note.updatedBy}</span>
|
||||
</div>
|
||||
<Link href={`/notes/${note.id}`} style={{ color: "var(--nl-text-secondary)" }}>
|
||||
<Link href={`/notes/${note.id}`} className="text-[color:var(--nl-text-secondary)]">
|
||||
{note.excerpt}
|
||||
</Link>
|
||||
<div style={{ display: "flex", gap: "var(--nl-space-2)", flexWrap: "wrap" }}>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{note.tags.map((tag) => (
|
||||
<Link key={tag} href={`/search?q=${encodeURIComponent(tag)}`} className="badge">
|
||||
#{tag}
|
||||
<Link key={tag} href={`/search?q=${encodeURIComponent(tag)}`}>
|
||||
<Badge variant="neutral">#{tag}</Badge>
|
||||
</Link>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</section>
|
||||
</Card>
|
||||
{showCreateNote && (
|
||||
<CreateNoteModal
|
||||
workspaces={workspaces}
|
||||
|
||||
@ -4,6 +4,7 @@ import Link from "next/link";
|
||||
import { useSearchParams } from "next/navigation";
|
||||
import { Suspense, useCallback, useEffect, useMemo, useState } from "react";
|
||||
import { AppShell } from "@/components/AppShell";
|
||||
import { Badge, Button, Card, Input } from "@/components/ui/Primitives";
|
||||
import { searchNotesRanked, type SearchRankedHit } from "@/lib/notes-client";
|
||||
import { listSavedViews, createSavedView, deleteSavedView, type SavedView } from "@/lib/saved-views-client";
|
||||
import { useDebounce } from "@/lib/use-debounce";
|
||||
@ -90,50 +91,44 @@ function SearchPageInner() {
|
||||
<AppShell
|
||||
title="Search"
|
||||
description="Lexical and hybrid ranked search with match hints (title, body, tag). Toggle modes to compare behavior."
|
||||
actions={<div className="badge">POST /notes/search</div>}
|
||||
actions={<Badge variant="info">POST /notes/search</Badge>}
|
||||
>
|
||||
<section style={{ display: "grid", gridTemplateColumns: "minmax(260px, 320px) minmax(0, 1fr)", gap: "var(--nl-space-4)" }}>
|
||||
<aside className="surface-card" style={{ padding: "var(--nl-space-5)", display: "grid", gap: "var(--nl-space-4)" }}>
|
||||
<div style={{ display: "flex", justifyContent: "space-between", alignItems: "center" }}>
|
||||
<div style={{ fontWeight: 700 }}>Saved searches</div>
|
||||
<Card padding="md" className="grid gap-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="font-bold">Saved searches</div>
|
||||
{query.trim() ? (
|
||||
<button
|
||||
<Button
|
||||
type="button"
|
||||
className="badge"
|
||||
size="sm"
|
||||
variant="secondary"
|
||||
onClick={() => void handleSaveCurrentSearch()}
|
||||
style={{ cursor: "pointer" }}
|
||||
>
|
||||
+ Save current
|
||||
</button>
|
||||
</Button>
|
||||
) : null}
|
||||
</div>
|
||||
<div style={{ display: "grid", gap: "var(--nl-space-3)" }}>
|
||||
{savedViews.map((view) => (
|
||||
<div
|
||||
key={view.id}
|
||||
className="surface-muted"
|
||||
style={{ padding: "var(--nl-space-4)", display: "grid", gap: "var(--nl-space-2)" }}
|
||||
className="grid gap-2 rounded-[var(--nl-radius-sm)] bg-[color:var(--nl-surface-muted)] p-4"
|
||||
>
|
||||
<div style={{ display: "flex", justifyContent: "space-between", alignItems: "start" }}>
|
||||
<Link href={`/search?q=${encodeURIComponent(view.query)}`} style={{ display: "grid", gap: "var(--nl-space-2)" }}>
|
||||
<div className="flex items-start justify-between">
|
||||
<Link href={`/search?q=${encodeURIComponent(view.query)}`} className="grid gap-2">
|
||||
<strong>{view.name}</strong>
|
||||
<span style={{ color: "var(--nl-text-secondary)" }}>{view.query}</span>
|
||||
<span className="text-[color:var(--nl-text-secondary)]">{view.query}</span>
|
||||
</Link>
|
||||
<button
|
||||
<Button
|
||||
type="button"
|
||||
size="sm"
|
||||
variant="ghost"
|
||||
onClick={() => {
|
||||
if (window.confirm("Remove this saved view?")) void handleDeleteSavedView(view.id);
|
||||
}}
|
||||
style={{
|
||||
color: "var(--nl-text-secondary)",
|
||||
background: "none",
|
||||
border: "none",
|
||||
cursor: "pointer",
|
||||
fontSize: "0.75rem",
|
||||
}}
|
||||
>
|
||||
Remove
|
||||
</button>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
@ -153,40 +148,45 @@ function SearchPageInner() {
|
||||
Lexical only
|
||||
</label>
|
||||
</div>
|
||||
</aside>
|
||||
</Card>
|
||||
|
||||
<section className="surface-card" style={{ padding: "var(--nl-space-6)", display: "grid", gap: "var(--nl-space-4)" }}>
|
||||
<input
|
||||
<Card padding="lg" className="grid gap-4">
|
||||
<Input
|
||||
aria-label="Search notes"
|
||||
className="input-shell"
|
||||
placeholder="Search notes, tags, and body text"
|
||||
value={query}
|
||||
onChange={(event) => setQuery(event.target.value)}
|
||||
/>
|
||||
<div style={{ display: "flex", gap: "var(--nl-space-2)", flexWrap: "wrap" }}>
|
||||
<span className="badge">mode:{mode}</span>
|
||||
<span className="badge">{hits.length} hits</span>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
<Badge variant="neutral">mode:{mode}</Badge>
|
||||
<Badge variant="neutral">{hits.length} hits</Badge>
|
||||
</div>
|
||||
{error ? <div style={{ color: "var(--nl-text-secondary)" }}>{error}</div> : null}
|
||||
<div style={{ display: "grid", gap: "var(--nl-space-3)" }}>
|
||||
{hits.map((hit) => (
|
||||
<div key={hit.noteId} className="surface-muted" style={{ padding: "var(--nl-space-4)", display: "grid", gap: "var(--nl-space-2)" }}>
|
||||
<div style={{ display: "flex", justifyContent: "space-between", gap: "var(--nl-space-3)", flexWrap: "wrap" }}>
|
||||
<Link href={`/notes/${hit.noteId}`} style={{ fontWeight: 700 }}>
|
||||
<div
|
||||
key={hit.noteId}
|
||||
className="grid gap-2 rounded-[var(--nl-radius-sm)] bg-[color:var(--nl-surface-muted)] p-4"
|
||||
>
|
||||
<div className="flex flex-wrap items-center justify-between gap-3">
|
||||
<Link href={`/notes/${hit.noteId}`} className="font-bold">
|
||||
{hit.title}
|
||||
</Link>
|
||||
<span className="badge">
|
||||
<Badge variant="neutral">
|
||||
{hit.matchKind} · score {hit.score}
|
||||
</span>
|
||||
</Badge>
|
||||
</div>
|
||||
<div style={{ color: "var(--nl-text-secondary)", fontSize: "var(--nl-fs-sm)" }}>{hit.snippet}</div>
|
||||
<Link href={`/search?q=${encodeURIComponent(hit.workspaceId)}`} style={{ color: "var(--nl-text-secondary)", fontSize: "var(--nl-fs-sm)" }}>
|
||||
<div className="text-[length:var(--nl-fs-sm)] text-[color:var(--nl-text-secondary)]">{hit.snippet}</div>
|
||||
<Link
|
||||
href={`/search?q=${encodeURIComponent(hit.workspaceId)}`}
|
||||
className="text-[length:var(--nl-fs-sm)] text-[color:var(--nl-text-secondary)]"
|
||||
>
|
||||
workspace: {hit.workspaceId}
|
||||
</Link>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</section>
|
||||
</Card>
|
||||
</section>
|
||||
</AppShell>
|
||||
);
|
||||
|
||||
@ -5,7 +5,7 @@ import { useSearchParams } from "next/navigation";
|
||||
import { Suspense, useEffect, useMemo, useState } from "react";
|
||||
import { AppShell } from "@/components/AppShell";
|
||||
import { CreateWorkspaceModal } from "@/components/CreateWorkspaceModal";
|
||||
import { Button } from "@/components/ui/Primitives";
|
||||
import { Badge, Button, Card, Input } from "@/components/ui/Primitives";
|
||||
import { downloadNotesExport, exportNotes, listNoteSummaries, listWorkspaceSummaries, deleteWorkspace } from "@/lib/notes-client";
|
||||
import { buildWorkspaceContextPackMarkdown } from "@/lib/context-pack";
|
||||
import { toast } from "@/lib/toast";
|
||||
@ -169,60 +169,58 @@ function WorkspacesPageInner() {
|
||||
}
|
||||
>
|
||||
<section className="workspace-layout-grid" style={{ display: "grid", gridTemplateColumns: "minmax(260px, 320px) minmax(0, 1fr)", gap: "var(--nl-space-4)" }}>
|
||||
<aside className="surface-card" style={{ padding: "var(--nl-space-5)", display: "grid", gap: "var(--nl-space-4)" }}>
|
||||
<div style={{ fontWeight: 700 }}>Saved views</div>
|
||||
<div style={{ display: "grid", gap: "var(--nl-space-3)" }}>
|
||||
<Card padding="md" className="grid gap-4">
|
||||
<div className="font-bold">Saved views</div>
|
||||
<div className="grid gap-3">
|
||||
{savedViews.map((view) => (
|
||||
<Link
|
||||
key={view.id}
|
||||
href={view.id === "workspace-shared" ? "/workspaces?q=shared" : "/workspaces"}
|
||||
className="surface-muted"
|
||||
style={{ padding: "var(--nl-space-4)", display: "grid", gap: "var(--nl-space-2)" }}
|
||||
className="grid gap-2 rounded-[var(--nl-radius-sm)] bg-[color:var(--nl-surface-muted)] p-4 transition-colors hover:bg-[color:var(--nl-surface-elevated)]"
|
||||
>
|
||||
<strong>{view.name}</strong>
|
||||
<span style={{ color: "var(--nl-text-secondary)" }}>{view.description}</span>
|
||||
<span className="badge">{view.resultCount} results</span>
|
||||
<span className="text-[color:var(--nl-text-secondary)]">{view.description}</span>
|
||||
<Badge variant="neutral" className="justify-self-start">{view.resultCount} results</Badge>
|
||||
</Link>
|
||||
))}
|
||||
</div>
|
||||
</aside>
|
||||
</Card>
|
||||
|
||||
<section className="surface-card" style={{ padding: "var(--nl-space-5)", display: "grid", gap: "var(--nl-space-4)" }}>
|
||||
<div style={{ display: "grid", gap: "var(--nl-space-3)" }}>
|
||||
<input
|
||||
<Card padding="md" className="grid gap-4">
|
||||
<div className="grid gap-3">
|
||||
<Input
|
||||
aria-label="Filter workspaces"
|
||||
className="input-shell"
|
||||
placeholder="Filter workspaces by owner, tag, or visibility"
|
||||
value={query}
|
||||
onChange={(event) => setQuery(event.target.value)}
|
||||
/>
|
||||
<div style={{ display: "flex", gap: "var(--nl-space-2)", flexWrap: "wrap" }}>
|
||||
<span className="badge">owner:any</span>
|
||||
<span className="badge">visibility:any</span>
|
||||
<span className="badge">sort:updated</span>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
<Badge variant="neutral">owner:any</Badge>
|
||||
<Badge variant="neutral">visibility:any</Badge>
|
||||
<Badge variant="neutral">sort:updated</Badge>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</Card>
|
||||
</section>
|
||||
|
||||
{error ? (
|
||||
<section className="surface-card" style={{ padding: "var(--nl-space-6)", color: "var(--nl-text-secondary)" }}>
|
||||
<Card padding="lg" className="text-[color:var(--nl-text-secondary)]">
|
||||
{error}
|
||||
</section>
|
||||
</Card>
|
||||
) : null}
|
||||
<section style={{ display: "grid", gap: "var(--nl-space-4)" }}>
|
||||
{filteredWorkspaces.map((workspace) => {
|
||||
const workspaceNotes = notesByWorkspace.get(workspace.id) ?? [];
|
||||
return (
|
||||
<article key={workspace.id} className="surface-card" style={{ padding: "var(--nl-space-6)", display: "grid", gap: "var(--nl-space-4)" }}>
|
||||
<Card key={workspace.id} padding="lg" className="grid gap-4">
|
||||
<div style={{ display: "flex", justifyContent: "space-between", gap: "var(--nl-space-4)", flexWrap: "wrap" }}>
|
||||
<div>
|
||||
<div style={{ fontSize: "var(--nl-fs-xl)", fontWeight: 700 }}>{workspace.name}</div>
|
||||
<div style={{ color: "var(--nl-text-secondary)", marginTop: 6 }}>{workspace.description}</div>
|
||||
</div>
|
||||
<div style={{ display: "grid", gap: 8, justifyItems: "end" }}>
|
||||
<Link href={`/workspaces?q=${encodeURIComponent(workspace.visibility)}`} className="badge">
|
||||
{workspace.visibility}
|
||||
<Link href={`/workspaces?q=${encodeURIComponent(workspace.visibility)}`}>
|
||||
<Badge variant="neutral">{workspace.visibility}</Badge>
|
||||
</Link>
|
||||
<Link href={`/workspaces?q=${encodeURIComponent(workspace.owner)}`} style={{ color: "var(--nl-text-secondary)" }}>
|
||||
Owner: {workspace.owner}
|
||||
@ -263,20 +261,24 @@ function WorkspacesPageInner() {
|
||||
</div>
|
||||
<div style={{ display: "flex", gap: "var(--nl-space-2)", flexWrap: "wrap" }}>
|
||||
{workspace.tags.map((tag) => (
|
||||
<Link key={tag} href={`/workspaces?q=${encodeURIComponent(tag)}`} className="badge">
|
||||
#{tag}
|
||||
<Link key={tag} href={`/workspaces?q=${encodeURIComponent(tag)}`}>
|
||||
<Badge variant="neutral">#{tag}</Badge>
|
||||
</Link>
|
||||
))}
|
||||
</div>
|
||||
<div style={{ display: "grid", gap: "var(--nl-space-3)" }}>
|
||||
{workspaceNotes.map((note) => (
|
||||
<Link key={note.id} href={`/notes/${note.id}`} className="surface-muted" style={{ padding: "var(--nl-space-4)", display: "grid", gap: "var(--nl-space-2)" }}>
|
||||
<Link
|
||||
key={note.id}
|
||||
href={`/notes/${note.id}`}
|
||||
className="grid gap-2 rounded-[var(--nl-radius-sm)] bg-[color:var(--nl-surface-muted)] p-4 transition-colors hover:bg-[color:var(--nl-surface-elevated)]"
|
||||
>
|
||||
<strong>{note.title}</strong>
|
||||
<span style={{ color: "var(--nl-text-secondary)" }}>{note.excerpt}</span>
|
||||
<span className="text-[color:var(--nl-text-secondary)]">{note.excerpt}</span>
|
||||
</Link>
|
||||
))}
|
||||
</div>
|
||||
</article>
|
||||
</Card>
|
||||
);
|
||||
})}
|
||||
</section>
|
||||
|
||||
Loading…
Reference in New Issue
Block a user