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 { useEffect, useState } from "react";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { AppShell } from "@/components/AppShell";
|
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 { chatOverWorkspace, listWorkspaceSummaries } from "@/lib/notes-client";
|
||||||
import type { WorkspaceSummary } from "@/lib/types";
|
import type { WorkspaceSummary } from "@/lib/types";
|
||||||
import { toast } from "@/lib/toast";
|
import { toast } from "@/lib/toast";
|
||||||
@ -53,19 +53,16 @@ export default function ChatPage() {
|
|||||||
Create a workspace first (Dashboard seed or Workspaces), then return here.
|
Create a workspace first (Dashboard seed or Workspaces), then return here.
|
||||||
</p>
|
</p>
|
||||||
) : (
|
) : (
|
||||||
<select className="input-shell" value={workspaceId} onChange={(e) => setWorkspaceId(e.target.value)}>
|
<Select
|
||||||
{workspaces.map((w) => (
|
value={workspaceId}
|
||||||
<option key={w.id} value={w.id}>
|
onChange={(e) => setWorkspaceId(e.target.value)}
|
||||||
{w.name}
|
options={workspaces.map((w) => ({ value: w.id, label: w.name }))}
|
||||||
</option>
|
/>
|
||||||
))}
|
|
||||||
</select>
|
|
||||||
)}
|
)}
|
||||||
</label>
|
</label>
|
||||||
<label style={{ display: "grid", gap: 8 }}>
|
<label style={{ display: "grid", gap: 8 }}>
|
||||||
<span style={{ fontWeight: 600 }}>Question</span>
|
<span style={{ fontWeight: 600 }}>Question</span>
|
||||||
<textarea
|
<Textarea
|
||||||
className="input-shell"
|
|
||||||
rows={4}
|
rows={4}
|
||||||
value={message}
|
value={message}
|
||||||
onChange={(e) => setMessage(e.target.value)}
|
onChange={(e) => setMessage(e.target.value)}
|
||||||
|
|||||||
@ -6,7 +6,7 @@ import { Suspense, useCallback, useEffect, useState } from "react";
|
|||||||
import { AppShell } from "@/components/AppShell";
|
import { AppShell } from "@/components/AppShell";
|
||||||
import { CreateNoteModal } from "@/components/CreateNoteModal";
|
import { CreateNoteModal } from "@/components/CreateNoteModal";
|
||||||
import { StateNotice } from "@/components/StateNotice";
|
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 { listNoteSummaries, listWorkspaceSummaries, seedOnboardingWorkspace } from "@/lib/notes-client";
|
||||||
import { listApprovalQueue } from "@/lib/review-client";
|
import { listApprovalQueue } from "@/lib/review-client";
|
||||||
import { listSavedViews, type SavedView } from "@/lib/saved-views-client";
|
import { listSavedViews, type SavedView } from "@/lib/saved-views-client";
|
||||||
@ -174,7 +174,7 @@ function DashboardContent() {
|
|||||||
</section>
|
</section>
|
||||||
|
|
||||||
{workspaces.length === 0 ? (
|
{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>
|
<strong>Welcome — create a sample workspace</strong>
|
||||||
<p style={{ margin: 0, color: "var(--nl-text-secondary)" }}>
|
<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{" "}
|
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
|
Seed sample workspace
|
||||||
</Button>
|
</Button>
|
||||||
</section>
|
</Card>
|
||||||
) : null}
|
) : null}
|
||||||
|
|
||||||
<section style={{ display: "grid", gridTemplateColumns: "minmax(0, 1.1fr) minmax(320px, 0.9fr)", gap: "var(--nl-space-4)" }}>
|
<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>
|
<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)" }}>
|
<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.
|
Views stored in your account (Search page can add more). Below that, quick links stay on the dashboard for one-tap access.
|
||||||
</p>
|
</p>
|
||||||
<div style={{ display: "grid", gap: "var(--nl-space-3)" }}>
|
<div style={{ display: "grid", gap: "var(--nl-space-3)" }}>
|
||||||
{sortedSaved.length === 0 ? (
|
{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.
|
No saved views yet. Open <Link href="/search">Search</Link> and save a query to pin it here.
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
@ -220,12 +220,11 @@ function DashboardContent() {
|
|||||||
<Link
|
<Link
|
||||||
key={view.id}
|
key={view.id}
|
||||||
href={hrefForSavedView(view)}
|
href={hrefForSavedView(view)}
|
||||||
className="surface-muted"
|
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)]"
|
||||||
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 className="flex flex-wrap items-center justify-between gap-3">
|
||||||
<strong>{view.name}</strong>
|
<strong>{view.name}</strong>
|
||||||
<span className="badge">{view.scope}</span>
|
<Badge variant="neutral">{view.scope}</Badge>
|
||||||
</div>
|
</div>
|
||||||
{view.description ? (
|
{view.description ? (
|
||||||
<div style={{ color: "var(--nl-text-secondary)" }}>{view.description}</div>
|
<div style={{ color: "var(--nl-text-secondary)" }}>{view.description}</div>
|
||||||
@ -245,12 +244,11 @@ function DashboardContent() {
|
|||||||
<Link
|
<Link
|
||||||
key={view.id}
|
key={view.id}
|
||||||
href={view.href}
|
href={view.href}
|
||||||
className="surface-muted"
|
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)]"
|
||||||
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 className="flex flex-wrap items-center justify-between gap-3">
|
||||||
<strong>{view.name}</strong>
|
<strong>{view.name}</strong>
|
||||||
<span className="badge">{view.scope}</span>
|
<Badge variant="neutral">{view.scope}</Badge>
|
||||||
</div>
|
</div>
|
||||||
<div style={{ color: "var(--nl-text-secondary)" }}>{view.description}</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" }}>
|
<div style={{ display: "flex", justifyContent: "space-between", gap: "var(--nl-space-3)", flexWrap: "wrap" }}>
|
||||||
@ -260,21 +258,20 @@ function DashboardContent() {
|
|||||||
</Link>
|
</Link>
|
||||||
))}
|
))}
|
||||||
</div>
|
</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={{ fontSize: "var(--nl-fs-xl)", fontWeight: 700 }}>Operator workflows</div>
|
||||||
<div style={{ display: "grid", gap: "var(--nl-space-3)" }}>
|
<div style={{ display: "grid", gap: "var(--nl-space-3)" }}>
|
||||||
{operatorWorkflows.map((workflow) => (
|
{operatorWorkflows.map((workflow) => (
|
||||||
<Link
|
<Link
|
||||||
key={workflow.id}
|
key={workflow.id}
|
||||||
href={getWorkflowHref(workflow)}
|
href={getWorkflowHref(workflow)}
|
||||||
className="surface-muted"
|
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)]"
|
||||||
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 className="flex flex-wrap items-center justify-between gap-3">
|
||||||
<strong>{workflow.name}</strong>
|
<strong>{workflow.name}</strong>
|
||||||
<span className="badge">{workflow.status}</span>
|
<Badge variant={workflow.status === "at_risk" ? "warning" : "success"}>{workflow.status}</Badge>
|
||||||
</div>
|
</div>
|
||||||
<div style={{ color: "var(--nl-text-secondary)" }}>Owner: {workflow.owner}</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" }}>
|
<div style={{ display: "flex", justifyContent: "space-between", gap: "var(--nl-space-3)", flexWrap: "wrap" }}>
|
||||||
@ -284,10 +281,10 @@ function DashboardContent() {
|
|||||||
</Link>
|
</Link>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</Card>
|
||||||
</section>
|
</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>
|
<div style={{ fontSize: "var(--nl-fs-xl)", fontWeight: 700 }}>Recent note activity</div>
|
||||||
{errorState ? <StateNotice state={errorState} onAction={() => void loadDashboard()} compact /> : null}
|
{errorState ? <StateNotice state={errorState} onAction={() => void loadDashboard()} compact /> : null}
|
||||||
<div style={{ display: "grid", gap: "var(--nl-space-3)" }}>
|
<div style={{ display: "grid", gap: "var(--nl-space-3)" }}>
|
||||||
@ -295,27 +292,27 @@ function DashboardContent() {
|
|||||||
<StateNotice state={getEmptyState("backend", "recent notes")} compact />
|
<StateNotice state={getEmptyState("backend", "recent notes")} compact />
|
||||||
) : null}
|
) : null}
|
||||||
{recentNotes.map((note) => (
|
{recentNotes.map((note) => (
|
||||||
<div key={note.id} className="surface-muted" style={{ padding: "var(--nl-space-4)", display: "grid", gap: "var(--nl-space-2)" }}>
|
<div key={note.id} className="grid gap-2 rounded-[var(--nl-radius-sm)] bg-[color:var(--nl-surface-muted)] p-4">
|
||||||
<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">
|
||||||
<Link href={`/notes/${note.id}`}>
|
<Link href={`/notes/${note.id}`}>
|
||||||
<strong>{note.title}</strong>
|
<strong>{note.title}</strong>
|
||||||
</Link>
|
</Link>
|
||||||
<span style={{ color: "var(--nl-text-secondary)" }}>{note.updatedBy}</span>
|
<span className="text-[color:var(--nl-text-secondary)]">{note.updatedBy}</span>
|
||||||
</div>
|
</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}
|
{note.excerpt}
|
||||||
</Link>
|
</Link>
|
||||||
<div style={{ display: "flex", gap: "var(--nl-space-2)", flexWrap: "wrap" }}>
|
<div className="flex flex-wrap gap-2">
|
||||||
{note.tags.map((tag) => (
|
{note.tags.map((tag) => (
|
||||||
<Link key={tag} href={`/search?q=${encodeURIComponent(tag)}`} className="badge">
|
<Link key={tag} href={`/search?q=${encodeURIComponent(tag)}`}>
|
||||||
#{tag}
|
<Badge variant="neutral">#{tag}</Badge>
|
||||||
</Link>
|
</Link>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</Card>
|
||||||
{showCreateNote && (
|
{showCreateNote && (
|
||||||
<CreateNoteModal
|
<CreateNoteModal
|
||||||
workspaces={workspaces}
|
workspaces={workspaces}
|
||||||
|
|||||||
@ -4,6 +4,7 @@ import Link from "next/link";
|
|||||||
import { useSearchParams } from "next/navigation";
|
import { useSearchParams } from "next/navigation";
|
||||||
import { Suspense, useCallback, useEffect, useMemo, useState } from "react";
|
import { Suspense, useCallback, useEffect, useMemo, useState } from "react";
|
||||||
import { AppShell } from "@/components/AppShell";
|
import { AppShell } from "@/components/AppShell";
|
||||||
|
import { Badge, Button, Card, Input } from "@/components/ui/Primitives";
|
||||||
import { searchNotesRanked, type SearchRankedHit } from "@/lib/notes-client";
|
import { searchNotesRanked, type SearchRankedHit } from "@/lib/notes-client";
|
||||||
import { listSavedViews, createSavedView, deleteSavedView, type SavedView } from "@/lib/saved-views-client";
|
import { listSavedViews, createSavedView, deleteSavedView, type SavedView } from "@/lib/saved-views-client";
|
||||||
import { useDebounce } from "@/lib/use-debounce";
|
import { useDebounce } from "@/lib/use-debounce";
|
||||||
@ -90,50 +91,44 @@ function SearchPageInner() {
|
|||||||
<AppShell
|
<AppShell
|
||||||
title="Search"
|
title="Search"
|
||||||
description="Lexical and hybrid ranked search with match hints (title, body, tag). Toggle modes to compare behavior."
|
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)" }}>
|
<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)" }}>
|
<Card padding="md" className="grid gap-4">
|
||||||
<div style={{ display: "flex", justifyContent: "space-between", alignItems: "center" }}>
|
<div className="flex items-center justify-between">
|
||||||
<div style={{ fontWeight: 700 }}>Saved searches</div>
|
<div className="font-bold">Saved searches</div>
|
||||||
{query.trim() ? (
|
{query.trim() ? (
|
||||||
<button
|
<Button
|
||||||
type="button"
|
type="button"
|
||||||
className="badge"
|
size="sm"
|
||||||
|
variant="secondary"
|
||||||
onClick={() => void handleSaveCurrentSearch()}
|
onClick={() => void handleSaveCurrentSearch()}
|
||||||
style={{ cursor: "pointer" }}
|
|
||||||
>
|
>
|
||||||
+ Save current
|
+ Save current
|
||||||
</button>
|
</Button>
|
||||||
) : null}
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
<div style={{ display: "grid", gap: "var(--nl-space-3)" }}>
|
<div style={{ display: "grid", gap: "var(--nl-space-3)" }}>
|
||||||
{savedViews.map((view) => (
|
{savedViews.map((view) => (
|
||||||
<div
|
<div
|
||||||
key={view.id}
|
key={view.id}
|
||||||
className="surface-muted"
|
className="grid gap-2 rounded-[var(--nl-radius-sm)] bg-[color:var(--nl-surface-muted)] p-4"
|
||||||
style={{ padding: "var(--nl-space-4)", display: "grid", gap: "var(--nl-space-2)" }}
|
|
||||||
>
|
>
|
||||||
<div style={{ display: "flex", justifyContent: "space-between", alignItems: "start" }}>
|
<div className="flex items-start justify-between">
|
||||||
<Link href={`/search?q=${encodeURIComponent(view.query)}`} style={{ display: "grid", gap: "var(--nl-space-2)" }}>
|
<Link href={`/search?q=${encodeURIComponent(view.query)}`} className="grid gap-2">
|
||||||
<strong>{view.name}</strong>
|
<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>
|
</Link>
|
||||||
<button
|
<Button
|
||||||
type="button"
|
type="button"
|
||||||
|
size="sm"
|
||||||
|
variant="ghost"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
if (window.confirm("Remove this saved view?")) void handleDeleteSavedView(view.id);
|
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
|
Remove
|
||||||
</button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
@ -153,40 +148,45 @@ function SearchPageInner() {
|
|||||||
Lexical only
|
Lexical only
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
</aside>
|
</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">
|
||||||
<input
|
<Input
|
||||||
aria-label="Search notes"
|
aria-label="Search notes"
|
||||||
className="input-shell"
|
|
||||||
placeholder="Search notes, tags, and body text"
|
placeholder="Search notes, tags, and body text"
|
||||||
value={query}
|
value={query}
|
||||||
onChange={(event) => setQuery(event.target.value)}
|
onChange={(event) => setQuery(event.target.value)}
|
||||||
/>
|
/>
|
||||||
<div style={{ display: "flex", gap: "var(--nl-space-2)", flexWrap: "wrap" }}>
|
<div className="flex flex-wrap gap-2">
|
||||||
<span className="badge">mode:{mode}</span>
|
<Badge variant="neutral">mode:{mode}</Badge>
|
||||||
<span className="badge">{hits.length} hits</span>
|
<Badge variant="neutral">{hits.length} hits</Badge>
|
||||||
</div>
|
</div>
|
||||||
{error ? <div style={{ color: "var(--nl-text-secondary)" }}>{error}</div> : null}
|
{error ? <div style={{ color: "var(--nl-text-secondary)" }}>{error}</div> : null}
|
||||||
<div style={{ display: "grid", gap: "var(--nl-space-3)" }}>
|
<div style={{ display: "grid", gap: "var(--nl-space-3)" }}>
|
||||||
{hits.map((hit) => (
|
{hits.map((hit) => (
|
||||||
<div key={hit.noteId} className="surface-muted" style={{ padding: "var(--nl-space-4)", display: "grid", gap: "var(--nl-space-2)" }}>
|
<div
|
||||||
<div style={{ display: "flex", justifyContent: "space-between", gap: "var(--nl-space-3)", flexWrap: "wrap" }}>
|
key={hit.noteId}
|
||||||
<Link href={`/notes/${hit.noteId}`} style={{ fontWeight: 700 }}>
|
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}
|
{hit.title}
|
||||||
</Link>
|
</Link>
|
||||||
<span className="badge">
|
<Badge variant="neutral">
|
||||||
{hit.matchKind} · score {hit.score}
|
{hit.matchKind} · score {hit.score}
|
||||||
</span>
|
</Badge>
|
||||||
</div>
|
</div>
|
||||||
<div style={{ color: "var(--nl-text-secondary)", fontSize: "var(--nl-fs-sm)" }}>{hit.snippet}</div>
|
<div className="text-[length:var(--nl-fs-sm)] text-[color:var(--nl-text-secondary)]">{hit.snippet}</div>
|
||||||
<Link href={`/search?q=${encodeURIComponent(hit.workspaceId)}`} style={{ color: "var(--nl-text-secondary)", fontSize: "var(--nl-fs-sm)" }}>
|
<Link
|
||||||
|
href={`/search?q=${encodeURIComponent(hit.workspaceId)}`}
|
||||||
|
className="text-[length:var(--nl-fs-sm)] text-[color:var(--nl-text-secondary)]"
|
||||||
|
>
|
||||||
workspace: {hit.workspaceId}
|
workspace: {hit.workspaceId}
|
||||||
</Link>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</Card>
|
||||||
</section>
|
</section>
|
||||||
</AppShell>
|
</AppShell>
|
||||||
);
|
);
|
||||||
|
|||||||
@ -5,7 +5,7 @@ import { useSearchParams } from "next/navigation";
|
|||||||
import { Suspense, useEffect, useMemo, useState } from "react";
|
import { Suspense, useEffect, useMemo, useState } from "react";
|
||||||
import { AppShell } from "@/components/AppShell";
|
import { AppShell } from "@/components/AppShell";
|
||||||
import { CreateWorkspaceModal } from "@/components/CreateWorkspaceModal";
|
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 { downloadNotesExport, exportNotes, listNoteSummaries, listWorkspaceSummaries, deleteWorkspace } from "@/lib/notes-client";
|
||||||
import { buildWorkspaceContextPackMarkdown } from "@/lib/context-pack";
|
import { buildWorkspaceContextPackMarkdown } from "@/lib/context-pack";
|
||||||
import { toast } from "@/lib/toast";
|
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)" }}>
|
<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)" }}>
|
<Card padding="md" className="grid gap-4">
|
||||||
<div style={{ fontWeight: 700 }}>Saved views</div>
|
<div className="font-bold">Saved views</div>
|
||||||
<div style={{ display: "grid", gap: "var(--nl-space-3)" }}>
|
<div className="grid gap-3">
|
||||||
{savedViews.map((view) => (
|
{savedViews.map((view) => (
|
||||||
<Link
|
<Link
|
||||||
key={view.id}
|
key={view.id}
|
||||||
href={view.id === "workspace-shared" ? "/workspaces?q=shared" : "/workspaces"}
|
href={view.id === "workspace-shared" ? "/workspaces?q=shared" : "/workspaces"}
|
||||||
className="surface-muted"
|
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)]"
|
||||||
style={{ padding: "var(--nl-space-4)", display: "grid", gap: "var(--nl-space-2)" }}
|
|
||||||
>
|
>
|
||||||
<strong>{view.name}</strong>
|
<strong>{view.name}</strong>
|
||||||
<span style={{ color: "var(--nl-text-secondary)" }}>{view.description}</span>
|
<span className="text-[color:var(--nl-text-secondary)]">{view.description}</span>
|
||||||
<span className="badge">{view.resultCount} results</span>
|
<Badge variant="neutral" className="justify-self-start">{view.resultCount} results</Badge>
|
||||||
</Link>
|
</Link>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
</aside>
|
</Card>
|
||||||
|
|
||||||
<section className="surface-card" style={{ padding: "var(--nl-space-5)", display: "grid", gap: "var(--nl-space-4)" }}>
|
<Card padding="md" className="grid gap-4">
|
||||||
<div style={{ display: "grid", gap: "var(--nl-space-3)" }}>
|
<div className="grid gap-3">
|
||||||
<input
|
<Input
|
||||||
aria-label="Filter workspaces"
|
aria-label="Filter workspaces"
|
||||||
className="input-shell"
|
|
||||||
placeholder="Filter workspaces by owner, tag, or visibility"
|
placeholder="Filter workspaces by owner, tag, or visibility"
|
||||||
value={query}
|
value={query}
|
||||||
onChange={(event) => setQuery(event.target.value)}
|
onChange={(event) => setQuery(event.target.value)}
|
||||||
/>
|
/>
|
||||||
<div style={{ display: "flex", gap: "var(--nl-space-2)", flexWrap: "wrap" }}>
|
<div className="flex flex-wrap gap-2">
|
||||||
<span className="badge">owner:any</span>
|
<Badge variant="neutral">owner:any</Badge>
|
||||||
<span className="badge">visibility:any</span>
|
<Badge variant="neutral">visibility:any</Badge>
|
||||||
<span className="badge">sort:updated</span>
|
<Badge variant="neutral">sort:updated</Badge>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</Card>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
{error ? (
|
{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}
|
{error}
|
||||||
</section>
|
</Card>
|
||||||
) : null}
|
) : null}
|
||||||
<section style={{ display: "grid", gap: "var(--nl-space-4)" }}>
|
<section style={{ display: "grid", gap: "var(--nl-space-4)" }}>
|
||||||
{filteredWorkspaces.map((workspace) => {
|
{filteredWorkspaces.map((workspace) => {
|
||||||
const workspaceNotes = notesByWorkspace.get(workspace.id) ?? [];
|
const workspaceNotes = notesByWorkspace.get(workspace.id) ?? [];
|
||||||
return (
|
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 style={{ display: "flex", justifyContent: "space-between", gap: "var(--nl-space-4)", flexWrap: "wrap" }}>
|
||||||
<div>
|
<div>
|
||||||
<div style={{ fontSize: "var(--nl-fs-xl)", fontWeight: 700 }}>{workspace.name}</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 style={{ color: "var(--nl-text-secondary)", marginTop: 6 }}>{workspace.description}</div>
|
||||||
</div>
|
</div>
|
||||||
<div style={{ display: "grid", gap: 8, justifyItems: "end" }}>
|
<div style={{ display: "grid", gap: 8, justifyItems: "end" }}>
|
||||||
<Link href={`/workspaces?q=${encodeURIComponent(workspace.visibility)}`} className="badge">
|
<Link href={`/workspaces?q=${encodeURIComponent(workspace.visibility)}`}>
|
||||||
{workspace.visibility}
|
<Badge variant="neutral">{workspace.visibility}</Badge>
|
||||||
</Link>
|
</Link>
|
||||||
<Link href={`/workspaces?q=${encodeURIComponent(workspace.owner)}`} style={{ color: "var(--nl-text-secondary)" }}>
|
<Link href={`/workspaces?q=${encodeURIComponent(workspace.owner)}`} style={{ color: "var(--nl-text-secondary)" }}>
|
||||||
Owner: {workspace.owner}
|
Owner: {workspace.owner}
|
||||||
@ -263,20 +261,24 @@ function WorkspacesPageInner() {
|
|||||||
</div>
|
</div>
|
||||||
<div style={{ display: "flex", gap: "var(--nl-space-2)", flexWrap: "wrap" }}>
|
<div style={{ display: "flex", gap: "var(--nl-space-2)", flexWrap: "wrap" }}>
|
||||||
{workspace.tags.map((tag) => (
|
{workspace.tags.map((tag) => (
|
||||||
<Link key={tag} href={`/workspaces?q=${encodeURIComponent(tag)}`} className="badge">
|
<Link key={tag} href={`/workspaces?q=${encodeURIComponent(tag)}`}>
|
||||||
#{tag}
|
<Badge variant="neutral">#{tag}</Badge>
|
||||||
</Link>
|
</Link>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
<div style={{ display: "grid", gap: "var(--nl-space-3)" }}>
|
<div style={{ display: "grid", gap: "var(--nl-space-3)" }}>
|
||||||
{workspaceNotes.map((note) => (
|
{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>
|
<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>
|
</Link>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
</article>
|
</Card>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
</section>
|
</section>
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user