From 244f11cc5094dc0af9b13b279dbbb489bbe27156 Mon Sep 17 00:00:00 2001 From: "Brian J. Fox" Date: Wed, 4 Nov 2026 09:35:05 +0000 Subject: [PATCH 1/3] feat(web): show projects as tiles or as a list, by the person's choice The Projects page showed tiles only. A toggle in its header switches to one line per project, with the name, stage, last activity and menu; the choice is kept in this browser like the other preferences. --- apps/web/src/config.ts | 5 +++ apps/web/src/library-view-setting.test.ts | 9 ++++ apps/web/src/library-view-setting.ts | 24 ++++++++++ apps/web/src/pages/project-library.css | 53 +++++++++++++++++++++++ apps/web/src/pages/project-library.tsx | 47 +++++++++++++++++++- 5 files changed, 136 insertions(+), 2 deletions(-) create mode 100644 apps/web/src/library-view-setting.test.ts create mode 100644 apps/web/src/library-view-setting.ts diff --git a/apps/web/src/config.ts b/apps/web/src/config.ts index a5bc2ad2..95b3f852 100644 --- a/apps/web/src/config.ts +++ b/apps/web/src/config.ts @@ -337,6 +337,11 @@ export const wording = { archived: "Archived", }, lastActivity: (relative: string) => `Active ${relative}`, + view: { + label: "Show projects as", + grid: "Tiles", + rows: "List", + }, loading: "Reading your projects…", emptyTitle: "No projects yet", emptyLead: "Start one from New project; it shows here from then on.", diff --git a/apps/web/src/library-view-setting.test.ts b/apps/web/src/library-view-setting.test.ts new file mode 100644 index 00000000..06dad2d7 --- /dev/null +++ b/apps/web/src/library-view-setting.test.ts @@ -0,0 +1,9 @@ +import { expect, test } from "bun:test"; +import { parseLibraryView } from "./library-view-setting.ts"; + +test("only the two layouts are read back; anything else leaves the default", () => { + expect(parseLibraryView("grid")).toBe("grid"); + expect(parseLibraryView("rows")).toBe("rows"); + expect(parseLibraryView("tiles")).toBeNull(); + expect(parseLibraryView("")).toBeNull(); +}); diff --git a/apps/web/src/library-view-setting.ts b/apps/web/src/library-view-setting.ts new file mode 100644 index 00000000..529fe3a1 --- /dev/null +++ b/apps/web/src/library-view-setting.ts @@ -0,0 +1,24 @@ +/** + * How the Projects page lays projects out, tiles or a list of one-liners, kept + * in this browser. Tiles until the person chooses. + */ +import { + useStoredPreference, + writeStoredPreference, +} from "./stored-preference.ts"; + +const LIBRARY_VIEW_KEY = "builder-library-view"; + +/** Tiles, or a list of one line per project. */ +export type ViewMode = "grid" | "rows"; + +export const parseLibraryView = (raw: string): ViewMode | null => + raw === "grid" || raw === "rows" ? raw : null; + +export function useLibraryView(): ViewMode { + return useStoredPreference(LIBRARY_VIEW_KEY, parseLibraryView, "grid"); +} + +export function writeLibraryView(mode: ViewMode): void { + writeStoredPreference(LIBRARY_VIEW_KEY, mode); +} diff --git a/apps/web/src/pages/project-library.css b/apps/web/src/pages/project-library.css index bd344c59..fc7b5a8e 100644 --- a/apps/web/src/pages/project-library.css +++ b/apps/web/src/pages/project-library.css @@ -142,3 +142,56 @@ ), color-mix(in oklab, var(--cover-b) 15%, var(--wb-card)); } + +.library-page .library-view { + display: inline-flex; + gap: var(--space-1); +} + +/* As a list: one line per project, a small swatch of its cover at the start, + the stage and last activity after the name, and the menu at the end. */ +.library-page .library-grid[data-view="rows"] { + display: flex; + flex-direction: column; + gap: var(--space-2); +} + +.library-page .library-grid[data-view="rows"] .library-card-open { + flex-direction: row; + align-items: center; + gap: var(--space-3); + padding: var(--space-2) var(--space-10) var(--space-2) var(--space-3); +} + +.library-page .library-grid[data-view="rows"] .library-cover { + flex: none; + width: 2rem; + aspect-ratio: 1; + border-radius: var(--radius-lg); +} + +.library-page .library-grid[data-view="rows"] .library-card-body { + display: flex; + flex: 1; + min-width: 0; + align-items: center; + gap: var(--space-3); + padding: 0; +} + +.library-page .library-grid[data-view="rows"] .library-card-title { + flex: 1; + min-width: 0; +} + +.library-page .library-grid[data-view="rows"] .library-card-meta { + flex-wrap: nowrap; + padding-right: 0; + white-space: nowrap; +} + +.library-page .library-grid[data-view="rows"] .library-card-menu { + top: 50%; + bottom: auto; + transform: translateY(-50%); +} diff --git a/apps/web/src/pages/project-library.tsx b/apps/web/src/pages/project-library.tsx index 13655bd1..0cd1ffd7 100644 --- a/apps/web/src/pages/project-library.tsx +++ b/apps/web/src/pages/project-library.tsx @@ -13,7 +13,12 @@ import { formatRelativeTime, type BadgeTone, } from "@corbits/react-ui"; -import { Ellipsis, Plus } from "lucide-react"; +import { Ellipsis, LayoutGrid, List, Plus } from "lucide-react"; +import { + useLibraryView, + writeLibraryView, + type ViewMode, +} from "../library-view-setting.ts"; import { useEffect, useRef, useState, type CSSProperties } from "react"; import type { ProjectStage } from "@corbits-builder/schemas/project"; import type { ProjectSummary } from "../api/types.ts"; @@ -75,6 +80,42 @@ function coverOf(projectId: string) { }; } +/** + * Tiles or a list, as two pressed-state buttons in a group: each names itself for assistive tech, + * and the page owns the choice, which outlives the page. + */ +function ViewToggle({ + mode, + onChange, +}: { + mode: ViewMode; + onChange: (mode: ViewMode) => void; +}) { + const text = wording.library.view; + return ( +
+ + +
+ ); +} + export function ProjectLibrary({ projects, loaded, @@ -100,6 +141,7 @@ export function ProjectLibrary({ ) => void; }) { const [filter, setFilter] = useState(null); + const view = useLibraryView(); const sorted = projects.toSorted((a, b) => b.lastActivityAt.localeCompare(a.lastActivityAt), ); @@ -162,6 +204,7 @@ export function ProjectLibrary({ ))} + + } + /> + ) : ( + + ); + else + body = ( +
    + {shown.map((project, index) => { + const cover = coverOf(project.id); + return ( +
  • + + +
  • + ); + })} +
+ ); return (
- {!loaded ? ( -

- {failed ? wording.library.failed : wording.library.loading} -

- ) : shown.length === 0 ? ( - filter === null ? ( - - {wording.shell.newProject} - - } - /> - ) : ( - - ) - ) : ( -
    - {shown.map((project, index) => { - const cover = coverOf(project.id); - return ( -
  • - - -
  • - ); - })} -
- )} + {body}
); } From 71fee0fa9aa6eca17b0db958c86925b941a2af2f Mon Sep 17 00:00:00 2001 From: "Brian J. Fox" Date: Wed, 4 Nov 2026 09:44:01 +0000 Subject: [PATCH 3/3] fix(web): pad the project row with a spacing token that exists --space-10 is not defined, so the row's padding fell to nothing and the menu button covered the stage and last activity. --- apps/web/src/pages/project-library.css | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/web/src/pages/project-library.css b/apps/web/src/pages/project-library.css index fc7b5a8e..97b7e0cd 100644 --- a/apps/web/src/pages/project-library.css +++ b/apps/web/src/pages/project-library.css @@ -160,7 +160,7 @@ flex-direction: row; align-items: center; gap: var(--space-3); - padding: var(--space-2) var(--space-10) var(--space-2) var(--space-3); + padding: var(--space-2) var(--space-12) var(--space-2) var(--space-3); } .library-page .library-grid[data-view="rows"] .library-cover {