hub-client: collections-based projects home (redo project selection)#394
Open
icarusz wants to merge 48 commits into
Open
hub-client: collections-based projects home (redo project selection)#394icarusz wants to merge 48 commits into
icarusz wants to merge 48 commits into
Conversation
Implements the "Short term" section of the QH-ProjectManagement-July26 Figma design as an alternate projects view, replacing the modal ProjectSelector with a full-page home: - header bar: logo, search (Cmd+K), Connect/Import and + New menus, avatar menu holding identity, cursor color, device linking, JSON backup, and theme (relocated from the modal) - personal shelves (localStorage-only for the exploration) with project cards, paging at 8+ per shelf, move-to-shelf via the per-project menu - "Everything else" list with sort control, inline Rename/Peek for unnamed projects, and plumbing (doc ID, share link) behind the menu - New project / Connect / Import flows reworked as dialogs A persisted toggle (qh-ui-variant) switches between this and the classic UI so both can be UX-tested side by side. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Project cards and rows are draggable; shelf sections and the "Everything else" list are drop zones with a dashed teal highlight. Drop zones key off a custom dataTransfer MIME type rather than React state so dragover recognition doesn't race the state flush. Also makes the "+ New shelf" button dashed to match the Figma design. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Shelf display order is lastAccessed descending regardless of when a project was added to the shelf, so paging always walks toward older projects. Last-opened stands in for "recent edits" until per-project edit attribution is exposed from automerge history. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…peek Colored initial disks per the design's shared-shelf and peek sections. Contributors are spoofed for the exploration: a deterministic fake crew seeded from each project's doc id (stable across reloads), with the real user always first in their cursor color. collaboratorsFor() is the seam where real attribution data plugs in later (automerge history or a recent-editors summary on the index doc). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ite links Shelves start personal (no people signal). "Share shelf" in the shelf menu converts one to shared: a small people glyph + member facepile appear, and clicking them opens a popover with the member list (Owner badge, mock Remove, Leave), a copyable invite link, and an honest note that anyone with the link can join and edit. The invite drives a real #/join-shelf/ route. The link carries the shelf's project entries (real doc ids + sync servers), so joining in another browser delivers projects that genuinely sync; a fresh browser skips project-set setup entirely (the set is auto-created silently) and is asked only for a name and cursor color. Shelf membership itself remains mock/local until shared shelves become synced docs (Phase 2). Shared-shelf menu adopts the design's leave-vs-remove semantics (Leave = your list only; Delete for everyone never deletes projects). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Product-language change across the exploration UI: all user-facing copy, the invite route (#/join-collection/), and code identifiers (useCollections, Collection, CollectionMember, CSS classes). Existing data survives via a one-time localStorage fallback that reads the old qh-shelves-v1 key when qh-collections-v1 is absent. Old join-shelf invite links are not honored (none in circulation beyond tests). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…opover Private collections show a single dimmed chip (you); shared ones keep the people glyph and full-strength member facepile. Clicking the chips always opens the members-and-share popover: shared collections list who has access, private ones read "Private — only you" and copying the invite link is the act that turns sharing on (seeding mock membership, as before). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…tion Every move path (drag-and-drop and the Move-to-collection menu) now routes through a gate: when the source collection is shared with other people, a dialog explains that the move changes their view of the collection and asks to confirm, with a persisted "Don't show this again" opt-out (qh-collection-move-warning-dismissed). Private collections, same-collection drops, and moves into a collection pass straight through. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
"Download as ZIP" in the project menu connects to the project in the background, exports its files via the existing exportProjectAsZip runtime (previously reachable only inside the editor), triggers the download, and disconnects. The menu shows "Preparing ZIP…" while the background sync runs. Also renames the avatar-menu backup items to "Export/Import project list (JSON)" for findability. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
"New collection" was dead in embedded/preview browsers because window.prompt() throws there — and window.confirm() can auto-accept, silently skipping destructive guards. Collection naming (create, rename) now uses real dialogs, and remove/leave/delete flows share a generic confirmation dialog with a danger-styled action button. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
"Duplicate" in the project menu background-connects to the source, reads every file (text and binary, base64 for the latter), and feeds them through the standard creation path as "<name> (copy)", landing in the editor like any new project. The copy keeps the source's collection placement via the pending-assignment mechanism. First leg of the recurring-documents workflow (user templates are the follow-on). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
ProjectSetEntry gains an optional summary { fileCount, topFiles,
contributors, asOf } — a per-user cache of "the project as I last saw
it", written by the client whenever a project is open (kept current as
files and presence identities change; self is always included since
presence can lag). Schema helper updateProjectSummaryInSet with tests.
The projects home reads it everywhere:
- cards and rows show "N files · opened X" per the design
- facepiles show real contributors when cached (mock crew otherwise)
- Peek renders instantly from the summary — real file list, contributor
line, "as of" stamp — and is now on every project's menu, not just
unnamed rows; a Refresh/Load-details action fetches via a short
background connection for projects never opened on this device
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…fork button Duplicate (fork) now opens a dialog before doing anything: the name is prefilled with "(copy)" appended and selected for easy replacement, and an Add-to-collection select defaults to the source's collection but can target any collection or none. A fork-glyph button appears on hover on every card (next to the menu) and on every row, opening the same dialog — per the team discussion that fork-from-existing is the primary creation flow to teach. Also drops the dashed border on + New collection (review feedback). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Groundwork for collections-as-project-sets (see claude-notes/plans/2026-07-10-collections-as-project-sets.md): - ProjectSetDocument gains an optional collection display name with a setProjectSetName helper (schema package, tested) - New CollectionsPointer IDB record: the browser root becomes an array of collection pointers, stored under the 'collections' key in the existing projectSet store (no DB version bump needed) - Schema migration v5 (transform-only) converts the legacy singleton pointer into a one-element array; the legacy pointer is preserved as a safety net, and getCollectionPointers() self-heals lazily for robustness against ordering - Pointer accessors: get/set/add (deduped)/remove, with tests covering fresh install, legacy conversion, idempotence, and no-clobber No user-visible behavior change yet; the UI still runs on the singleton set until phase 2-4 land. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The service now manages a map of ProjectSetDocument connections (one per collection) over per-server shared Repos, instead of a single document on a dedicated Repo. New API: connectCollection(s), createCollection(name), disconnectCollection, listCollections, renameCollection, add/remove/move project per collection, and description/touch/summary updates applied wherever an entry appears. The legacy singleton API delegates to the first (personal root) connection, so every existing caller — useProjectSet, the reconciler, App — works unchanged; all 706 unit tests pass. Also fixes the init-vs-link connection race class noted in the plan (appending a collection no longer contends with startup). Part of claude-notes/plans/2026-07-10-collections-as-project-sets.md. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…n (phases 2b/3) Full replacement for useProjectSet: reads the collections pointer array (self-healing from the legacy singleton), connects every collection document with per-collection failure reporting, names an unnamed root "My projects", and mirrors the setup/migration actions so the setup screens can swap over unchanged. Includes the one-time phase 3 migration: legacy localStorage collections (qh-collections-v1) become real synced ProjectSetDocuments named after them, entries copied from the root superset; the original JSON is preserved under a -migrated key, never re-imported. Not yet wired into App — phase 4 swaps App/ProjectsHome/JoinCollection onto this hook in one move (two hooks sharing the service would fight over connections). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…phase 4) ProjectsHome, App, and the join flow now run on useCollectionSets instead of localStorage collections: - Collections come from connected ProjectSetDocuments (props), root excluded; "Everything else" = root entries not in any collection - Move across collections uses the service's cross-doc move; new "Add to collection" submenu adds without removing (multi-membership) - People & invite popover derives people from the projects' cached index-doc contributor summaries; the invite link now carries only the collection doc id + server (no inlined entries) - JoinCollectionLanding subscribes to the shared collection document - New/rename/leave collection act on the real docs; leaving just unsubscribes (the document is untouched, matching "nothing is destructive") - Fixes a shared-server readiness bug surfaced by the migration: the repo 'peer' event fires once, so collections after the first timed out; server readiness is now a latched promise Removes the dead localStorage useCollections hook. Verified end to end in the browser: legacy pointer + localStorage collections migrate into synced docs, survive a fresh reload with no localStorage, and create/move/share/leave all work; 706 tests pass, build clean. Closes the implementation in claude-notes/plans/2026-07-10-collections-as-project-sets.md. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The projects-home empty-state guard keyed off root entries only, so a browser that had just joined a shared collection (empty personal root, populated subscribed collection) rendered "No projects yet" and hid the collection entirely. Guard now also checks collection entries. Found during local-prod two-browser join testing. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…oject The join-collection landing was shown only for needs-setup; a browser with a pre-existing project (needs-migration) fell through to the "Upgrade: Synced Project List / Migrate Projects" screen, so an invitee saw a confusing migration prompt with no mention of the collection they were joining. (Easy to hit: opening a document share link first creates exactly one such project.) The invite route now always renders the landing, and the onboarding effect establishes the personal root for both cases — creating an empty root (needs-setup) or silently migrating the stray project into a new root (needs-migration; non-destructive, the legacy store is retained). A once-per-mount guard prevents a retry loop, since migrateProjects resets to needs-migration on failure. Verified on the dev server against the local hub: a needs-migration browser opening the invite now shows "Quick Hawk invited you to Team docs", joins, and lands home with the collection's documents; the migrated project coexists without orphaning or duplication. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Card facepiles fell back to mockCollaborators — a deterministic fake crew seeded from the doc id — whenever a project had no cached contributors yet, so a brand-new file appeared to already have other authors. Now that collections carry real contributor summaries (from index-doc identities), the mock fallback is misleading. Facepiles are real-only: show the project's cached contributors, and when there are none yet, just the current user — never invented people. Removed the now-dead mockCollaborators module; its face shape moves to utils/facepile.ts as `Face` (no longer a misnomer). Verified on the dev server against the local hub: a freshly created "Todolist" card shows only its creator; existing cards show their real contributors. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Author chips on shared collection cards clobbered each other: the peek summary is stored on a shared collection entry, and updateProjectSummaryInSet replaced the whole summary object, so whoever edited last became the sole recorded contributor. Now the file-shape fields (fileCount, topFiles, asOf) still take the latest writer's view, but `contributors` are unioned (dedup by name) with whatever is already stored. Authorship accumulates as collaborators open a project, matching expectations. Verified two-browser against the local hub: two users opening the same shared project both appear as authors. Sequential edits union correctly; a truly-concurrent first open can momentarily show one author but self-heals on the next edit by either party (confirmed: reopening as the first user restored both). Full per-actor-map contributors (zero-window, mirroring the index-doc identities map) is the follow-up for complete concurrency safety. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Moved Peek out of the project ⋯ menu (and off the inline row link) onto a dedicated magnifying-glass icon next to the fork/branch icon on every card and row. Hovering the icon opens the peek as a floating card and it stays open while the pointer is over the icon or the popover (short close delay bridges the gap so the popover's actions stay reachable); leaving closes it. The popover anchors to the card/row's left edge so it stays on-screen in every column, and the card's action row is pinned visible while peek is open so moving onto the popover doesn't dismiss it. Verified with real pointer hover on the dev server: hovering the icon opens the peek (files, contributors, actions), and moving away closes it. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Peek's footer duplicated the ⋯ menu (Rename, Open, Remove). Since Peek is a read-only preview, its footer now keeps only Refresh/Load details (the one peek-specific action); the footnote points to the ⋯ menu for acting on the project. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Adds migrations.test.ts covering the pointer→collections conversion: v5 is registered as transform-only, a singleton projectSet pointer becomes a one-element collections array (legacy pointer retained), re-running is idempotent (no clobber of a grown array), and a fresh browser with no pointer is a no-op. Documents the migration in claude-notes/instructions/hub-client-storage.md: a Migration History table (v2–v5), the projectSet store and its two pointer keys, why v5 bumps schema-version-only (not DB version) and how the runner still applies it, plus the app-level localStorage-collections migration. Verified live on the dev server: a genuine v4 pre-collections browser (singleton pointer + a project set with three projects, no collections) upgrades on reload to a one-entry collections pointer with all projects preserved in "Everything else"; _meta bumps to v5, migration recorded, legacy pointer retained. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
✅ Snyk checks have passed. No issues have been found so far.
💻 Catch issues earlier using the plugins for VS Code, JetBrains IDEs, Visual Studio, and Eclipse. |
…ctions Local testing of the collections UI surfaced several issues; this batch addresses them (all TDD): - Debug auth gate: on a loopback host, treat /auth/me 401 as "proceed read-only" instead of the sign-in dead-end, so the inspector opens against an auth-less local hub. Real deployments stay gated. - Auth-less identity stamping: derive a stable Automerge actor id from the local userId when auth is disabled, so setIdentity fires and a document's `identities` populate (also fixes local editor attribution). The server actor still wins when auth is enabled. - Root-identity hardening: getCollectionPointers pins the legacy-singleton root to index 0, so root is no longer identified by accidental array position; self-heals scrambled pointer orders. - Debug view: list every collection ProjectSetDocument (not just the legacy singleton), each labeled with its live name, so all synced collection docs are inspectable. - ProjectsHome polish: fade card hover-actions beneath long titles so the peek/fork/menu icons no longer collide with the title; fix "Everything is in a collection" copy. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Documents 3992e45. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Implements #85 — a redo of the project-selection component as a full-page projects home, built from the Figma design and the 2026-07-09 design discussion (Andrew/Carlos/Elliot).
This is a UI exploration. It ships behind a persisted toggle (
qh-ui-variant): the new home is shown by default on this branch, and an avatar-menu item switches back to the classic selector. Nothing is deployed to quartohub.com by this change, and the editor/file-selector are untouched.What #85 asked for, and where it landed
ProjectSetDocument, and the browser stores an array of collection pointers instead of a single one, so lists sync across browsers and hosted deployments can manage the pointer server-side.Highlights
claude-notes/instructions/hub-client-storage.md(Migration History) and tested (migrations.test.ts).Testing
npm run build:allandnpm run test:cipass (710 unit + 129 wasm).npm run local-prod(local hub + sync server): create/collection/drag/share/join, live cross-browser sync, and the v4→v5 migration on a genuine pre-collections browser (projects preserved). Details inclaude-notes/plans/2026-07-10-collections-as-project-sets.md.hub-client/,ts-packages/quarto-automerge-schema/,claude-notes/); no Rust crates touched.Known limitations / deferred (see plan doc)
Closes #85.
🤖 Generated with Claude Code