Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
48 commits
Select commit Hold shift + click to select a range
389af46
feat(hub-client): shelves-based projects home (UI exploration)
icarusz Jul 7, 2026
9dbc89c
docs(hub-client): changelog for shelves projects-home exploration
icarusz Jul 7, 2026
1a37f9e
feat(hub-client): drag projects between shelves and the unshelved list
icarusz Jul 7, 2026
11f5c28
docs(hub-client): changelog for shelf drag-and-drop
icarusz Jul 7, 2026
faca6ac
chore(hub-client): empty-shelf hint mentions drag-and-drop
icarusz Jul 7, 2026
1092bc7
feat(hub-client): order shelf cards by recency, newest first
icarusz Jul 7, 2026
0f33847
feat(hub-client): mock collaborator facepiles on cards, shelves, and …
icarusz Jul 7, 2026
281fd9d
docs(hub-client): changelog for mock facepiles
icarusz Jul 7, 2026
71b6c58
feat(hub-client): explicit shelf sharing with members popover and inv…
icarusz Jul 8, 2026
8997ea1
docs(hub-client): changelog for shelf sharing and invite flow
icarusz Jul 9, 2026
b379c89
feat(hub-client): rename "shelf" to "collection" throughout
icarusz Jul 9, 2026
4faebe7
docs(hub-client): changelog for shelf-to-collection rename
icarusz Jul 9, 2026
dbd1bc4
feat(hub-client): avatar chips on every collection open the members p…
icarusz Jul 9, 2026
c8a5394
docs(hub-client): changelog for collection avatar chips
icarusz Jul 9, 2026
1c447ee
feat(hub-client): warn before moving a project out of a shared collec…
icarusz Jul 9, 2026
3839162
docs(hub-client): changelog for shared-collection move warning
icarusz Jul 9, 2026
52c92f9
feat(hub-client): download a project as ZIP from the projects home
icarusz Jul 9, 2026
58fffc2
docs(hub-client): changelog for home-page ZIP export
icarusz Jul 9, 2026
60abc1d
fix(hub-client): replace native prompt/confirm with in-app dialogs
icarusz Jul 10, 2026
2fec8b5
docs(hub-client): changelog for dialog fix
icarusz Jul 10, 2026
a239108
feat(hub-client): duplicate a project from the projects home
icarusz Jul 13, 2026
723fe42
docs(hub-client): changelog for project duplicate
icarusz Jul 13, 2026
3e3e149
feat(hub-client): cache a peek summary on project-set entries
icarusz Jul 13, 2026
a5fd01d
docs(hub-client): changelog for peek summaries
icarusz Jul 13, 2026
a997d3f
feat(hub-client): duplicate dialog with rename/destination + visible …
icarusz Jul 13, 2026
e8c855a
docs(hub-client): changelog for duplicate dialog and fork button
icarusz Jul 13, 2026
ea5bea8
docs(plans): collections-as-project-sets migration plan
icarusz Jul 13, 2026
53e9e48
feat(hub-client): collections pointer storage + schema name (phase 1)
icarusz Jul 13, 2026
81be0a7
refactor(hub-client): multi-collection projectSetService (phase 2a)
icarusz Jul 13, 2026
c0f0984
feat(hub-client): useCollectionSets hook + local collections migratio…
icarusz Jul 13, 2026
19c69ff
feat(hub-client): wire projects home onto real collection documents (…
icarusz Jul 13, 2026
3f2d199
docs(hub-client): changelog for collections-as-documents
icarusz Jul 13, 2026
85108dd
fix(hub-client): show subscribed collections when the root is empty
icarusz Jul 14, 2026
c16f89e
docs(hub-client): changelog for empty-root collection fix
icarusz Jul 14, 2026
77820e2
docs(plans): record local-prod verification for collections migration
icarusz Jul 14, 2026
645d5b5
fix(hub-client): collection invite handles a browser with a legacy pr…
icarusz Jul 14, 2026
850fbc8
docs(hub-client): changelog for invite-with-legacy-project fix
icarusz Jul 14, 2026
7a7dafd
fix(hub-client): stop showing fabricated authors on project cards
icarusz Jul 14, 2026
55133c6
docs(hub-client): changelog for real-only facepiles
icarusz Jul 14, 2026
214d62e
fix(schema): union contributors in project summary instead of replacing
icarusz Jul 14, 2026
90009fb
docs(hub-client): changelog for contributor-union fix
icarusz Jul 14, 2026
f0ce3a3
feat(hub-client): peek as a hover-card on a magnifying-glass icon
icarusz Jul 14, 2026
16856df
docs(hub-client): changelog for hover-peek
icarusz Jul 14, 2026
02095b9
feat(hub-client): trim redundant actions from the peek card
icarusz Jul 14, 2026
b47d545
docs(hub-client): changelog for peek footer trim
icarusz Jul 14, 2026
f52d87e
test(hub-client)+docs: cover and document the v5 collections migration
icarusz Jul 14, 2026
3992e45
hub-client: identities, root ordering, and debug-view fixes for colle…
icarusz Jul 15, 2026
ccdce7e
hub-client: changelog for collections identities/root/debug-view fixes
icarusz Jul 16, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 15 additions & 1 deletion claude-notes/instructions/hub-client-storage.md
Original file line number Diff line number Diff line change
Expand Up @@ -121,10 +121,24 @@ export interface NewStoreEntry {

| Store | Key | Purpose |
|-------|-----|---------|
| `projects` | `id` (UUID) | Project connection info (sync server, index doc ID) |
| `projects` | `id` (UUID) | Legacy project connection info (sync server, index doc ID). Kept as a safety net / migration source; superseded by the synced project-set document. |
| `projectSet` | `key` | Pointers to the synced Automerge document(s). Holds the legacy singleton `'projectSet'` pointer **and** the `'collections'` pointer array (see Migration History v4/v5). |
| `userSettings` | `key` (singleton: `'identity'`) | User identity for presence features |
| `_meta` | `key` (singleton: `'schema'`) | Schema version and migration history |

## Migration History

| Schema | DB ver | Kind | What it does |
|-------:|-------:|------|--------------|
| v2 | 2 | structural + transform | Add `_meta` and `userSettings` stores; seed a default user identity. |
| v3 | 3 | no-op | Formerly a `sassCache` store; caching moved to the `quarto-cache` DB. Kept so existing v3 databases don't mismatch. |
| v4 | 4 | structural | Add the `projectSet` store. The project list moves from the `projects` store into a synced Automerge **ProjectSetDocument**; IndexedDB keeps only a singleton `'projectSet'` pointer `{ projectSetDocId, syncServer }` to it. |
| v5 | 4 | transform-only | **Collections.** The root goes from a single project set to an array of collections (each collection is its own ProjectSetDocument). The `'projectSet'` singleton pointer is converted into a one-element `'collections'` array (`{ key:'collections', collections:[{ projectSetDocId, syncServer }] }`), stored in the same `projectSet` store. The legacy singleton is **retained untouched** as a safety net. See `migratePointerToCollections` in `migrations.ts` and the plan `claude-notes/plans/2026-07-10-collections-as-project-sets.md`. |

**Note on v5 (transform-only):** it bumps `CURRENT_SCHEMA_VERSION` (5) but **not** `CURRENT_DB_VERSION` (stays 4), because it adds no store/index — it only rewrites a record. The migration runner keys off the stored schema version, so it applies the v5 `transform` even though the IndexedDB version is unchanged. `getCollectionPointers` in `projectSetStorage.ts` also self-heals (runs the same conversion lazily) as defense in depth. Tests: `migrations.test.ts` and the collection-pointer suite in `projectSetStorage.test.ts`.

**Second migration (app-level, not IndexedDB):** legacy per-browser "collections" that lived in `localStorage` under `qh-collections-v1` (the pre-synced exploration) are converted into real synced collection documents on first load by `migrateLocalCollections` in `hooks/useCollectionSets.ts`; the original JSON is preserved under `qh-collections-v1-migrated` and never re-imported.

## Export/Import Format

The `exportData()` function produces JSON with schema version for forward compatibility:
Expand Down
125 changes: 125 additions & 0 deletions claude-notes/plans/2026-07-10-collections-as-project-sets.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
# Collections as project sets

## Overview

Re-founder the projects-home "collections" (branch `explore/projects-collections-ui`)
on real synced documents, per the 2026-07-09 design discussion (Andrew, Carlos,
Elliot). Today a collection is a localStorage grouping of indexDocIds inside a
single ProjectSetDocument world; membership, sharing, and invites are mock.
After this change:

- **Each collection IS a ProjectSetDocument** (same schema, plus an optional
`name`). We reuse all existing set logic: entries, dedupe keys, sync,
link-another-browser.
- **The browser's root pointer becomes an array**: IndexedDB goes from one
`{projectSetDocId, syncServer}` pointer to a list of collection pointers.
"The root of all your information" is now a set of project sets.
- **Sharing a collection = sharing its doc id**, exactly like today's
link-project-set flow. The join-collection invite flow stops being theater:
joining appends the collection's doc id to your array.
- **Real contributor identities** come from each project index doc's
`identities` map (per Carlos: the blessed, self-populating source), not
presence-at-open.

Everything about individual projects (index docs, files, share links) is
untouched. "Minimum blast surface."

## Checklist

### Phase 1 — schema + tests
- [x] `ProjectSetDocument` gains optional `name?: string` (collection display
name) with helper `setProjectSetName` + tests
- [x] New IDB shape: `CollectionsPointer { key: 'collections', collections:
Array<{ projectSetDocId, syncServer }> }` in storage types
- [x] IDB migration 4→5 (structural no-op, transform converts the singleton
`projectSet` pointer into a one-element `collections` array; old pointer
kept as safety net, never deleted by the migration)
- [x] Migration tests: fresh install, existing pointer, re-run idempotence

### Phase 2 — service layer
- [x] `projectSetService` → manages a MAP of doc handles keyed by docId
(connect-all on startup; add/remove collection connections at runtime)
- [x] Operations gain a collection-doc parameter: addProject(collectionId,
entry), removeProject, updateSummary, touch, rename collection
- [x] Create-collection (new empty set doc), subscribe-collection (existing
doc id), unsubscribe (drop from array; doc untouched — "leave")
- [x] `useProjectSet` → `useCollectionSets`: exposes
`Array<{ docId, name, entries, syncServer }>` + actions

### Phase 3 — localStorage collections migration
- [x] On first load with the new code: for each entry in `qh-collections-v1`,
create a new collection doc named after it, copy the matching entries
from the personal set into it, append to the pointer array
- [x] The original set becomes the personal root collection (name it
"My projects" when the name field is empty; user-editable)
- [x] Mark `qh-collections-v1` migrated (rename key, don't delete) — one-way,
idempotent, safe to interrupt (commit point = pointer-array write)

### Phase 4 — UI rewiring
- [x] ProjectsHome reads collections from the hook instead of localStorage;
"Everything else" = personal-root entries not present in any other
collection (display-level; the root stays a superset so nothing is
ever lost)
- [x] Move keeps current semantics (remove+add across docs); new "Add to
collection" menu item adds without removing (multi-membership is now
natural — entries in several sets)
- [x] Share/members popover backed by the real doc: invite link carries the
collection doc id + server only (entries no longer inlined in the URL)
- [x] JoinCollectionLanding: real subscribe (append docId to pointer array),
auto-create personal root for fresh browsers as today
- [x] Facepiles/peek contributors read from project index-doc `identities`
(one-shot read piggybacked on the existing summary write; peek refresh
also picks them up)

### Phase 5 — verification
- [x] `npm run build:all` + full `npm run test:ci`
- [x] Manual: fresh browser, upgraded browser (with and without localStorage
collections), two-browser share/join round trip, debug.html inspection
of resulting docs

## Details and open questions

**Everything-else semantics.** Keeping the personal root as a superset of all
your projects (and filtering the home display) means deleting a shared
collection can never orphan a project you've opened, matching "no action
deletes entries for people who are part of a collection." Alternative — root
holds only unfiled projects — is cleaner data but loses the safety property.
Recommend superset. **Flag for Carlos.**

**Summaries per entry.** The peek `summary` cache rides on entries, so a
project in three collections has three copies. Acceptable duplication for now;
revisit when identities move to index docs (Phase 4 reduces what summary needs
to carry).

**Members display.** A shared collection's facepile can be derived from the
union of its projects' index-doc identities, replacing mock members. A
dedicated `members` map on the collection doc (self-reported on join) is a
possible follow-on, not required for this plan.

**History viewer** (Carlos's ask: when were entries added/removed + restore)
is deliberately out of scope here; it layers on automerge history of the
collection doc afterward. Charlie's history view is prior art.

**Race note.** The current app connects the singleton set on init and the
link-project-set route races it (observed 2026-07-09: link failed to displace
a fresh pointer). The multi-connection refactor should make "append a
collection" not contend with init at all, fixing that class of bug.

## Local-prod verification (2026-07-14)

Ran the full stack via `npm run local-prod` (hub binary + local sync
server on 127.0.0.1:8080, fresh `.local-prod-data`). Verified end to end:
fresh-browser project-set create; new project; new collection ("Team
docs") + drag a project in; invite link is doc-id-only (`server=`, no
`entries=`); a second browser joined via the invite and saw the synced
project; created "meeting agenda" targeted at Team docs on browser 2;
it appeared **live** on browser 1 (no reload). Hub log clean (only the
expected `auth_disabled` warnings under `--allow-insecure-auth`).

Found + fixed one bug: the empty-state guard hid a subscribed collection
when the personal root was still empty (85108dde).

Deferred/known: the People popover shows "only you" until members open
projects (contributors derive from cached summaries; a real members map
is the documented follow-on).

34 changes: 34 additions & 0 deletions hub-client/changelog.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,8 +23,42 @@ WASM rebuild is needed for a changelog-only edit.

-->

### 2026-07-15

- [`3992e45d`](https://github.com/quarto-dev/q2/commits/3992e45d): UI exploration (branch only): opening a project on a local hub without sign-in now records you as a contributor, so collection avatar chips fill in during local testing instead of staying empty.
- [`3992e45d`](https://github.com/quarto-dev/q2/commits/3992e45d): UI exploration (branch only): the Automerge debugger now opens against an auth-disabled local hub and lists every collection as its own named synced document, instead of showing only a single project set.
- [`3992e45d`](https://github.com/quarto-dev/q2/commits/3992e45d): UI exploration (branch only): project cards fade their hover action icons (peek, fork, menu) beneath long titles so they no longer overlap the text, and the empty-state now reads "Everything is in a collection".

### 2026-07-10

- [`a2391089`](https://github.com/quarto-dev/q2/commits/a2391089): UI exploration (branch only): projects can be duplicated from the projects home (project menu → Duplicate) — the copy gets all files, the name "(copy)", and the same collection as the original.
- [`02095b9a`](https://github.com/quarto-dev/q2/commits/02095b9a): UI exploration (branch only): the Peek preview no longer repeats Rename/Open/Remove (those are in the ⋯ menu) — it's a read-only preview with just a Refresh action.
- [`f0ce3a31`](https://github.com/quarto-dev/q2/commits/f0ce3a31): UI exploration (branch only): Peek is now a magnifying-glass icon next to the fork icon on each project — hover it to get a floating preview of the project (files, who's joined), instead of a menu item.
- [`214d62e4`](https://github.com/quarto-dev/q2/commits/214d62e4): UI exploration (branch only): author chips on shared collections now accumulate instead of overwriting — when a second person opens a shared project, both show as authors rather than the latest editor replacing the previous one.
- [`7a7dafdf`](https://github.com/quarto-dev/q2/commits/7a7dafdf): UI exploration (branch only): project cards no longer show placeholder "authors" — a new file starts with just you, and cards show the real people who've opened the project.
- [`645d5b53`](https://github.com/quarto-dev/q2/commits/645d5b53): UI exploration (branch only): opening a collection invite in a browser that already had a project no longer shows a confusing "migrate your project list" screen — it shows the invitation and quietly folds any existing project into your list.
- [`85108dde`](https://github.com/quarto-dev/q2/commits/85108dde): UI exploration (branch only): fixed a just-joined collection not appearing when your own project list was still empty.
- [`19c69ff3`](https://github.com/quarto-dev/q2/commits/19c69ff3): UI exploration (branch only): collections are now real synced documents rather than a per-browser list — they sync across your browsers, sharing one genuinely gives collaborators access, and a project can live in more than one collection ("Add to collection"). Existing collections migrate automatically on first load.
- [`a997d3fb`](https://github.com/quarto-dev/q2/commits/a997d3fb): UI exploration (branch only): duplicating a project now opens a dialog to rename the copy and pick its collection, and a fork button appears on hover on every project card and row.
- [`3e3e1495`](https://github.com/quarto-dev/q2/commits/3e3e1495): UI exploration (branch only): project cards now show real file counts, facepiles show the actual people seen on a project once you've opened it, and Peek is available on every project — opening instantly with the cached file list, plus a refresh that fetches details for projects never opened on this device.
- [`60abc1db`](https://github.com/quarto-dev/q2/commits/60abc1db): UI exploration (branch only): fixed "New collection" doing nothing in some browsers — creating and renaming collections now use proper dialogs, and remove/leave/delete confirmations are in-app dialogs instead of native popups.

### 2026-07-09

- [`52c92f94`](https://github.com/quarto-dev/q2/commits/52c92f94): UI exploration (branch only): projects can be downloaded as a ZIP straight from the projects home (project menu → "Download as ZIP") without opening them; the avatar-menu backup entries are now named "Export/Import project list (JSON)".
- [`1c447eea`](https://github.com/quarto-dev/q2/commits/1c447eea): UI exploration (branch only): moving a project out of a collection that's shared with other people now asks for confirmation ("you're changing other people's view of this collection"), with a "Don't show this again" opt-out. Private collections are unaffected.
- [`dbd1bc44`](https://github.com/quarto-dev/q2/commits/dbd1bc44): UI exploration (branch only): every collection header now shows avatar chips — dimmed and just you while private, glyph plus member facepile when shared. Clicking the chips opens the members popover; for a private collection, copying the invite link is what turns sharing on.
- [`b379c89d`](https://github.com/quarto-dev/q2/commits/b379c89d): UI exploration (branch only): "shelf" is now called "collection" everywhere — menus, dialogs, and invite links. Existing arrangements carry over automatically.

### 2026-07-08

- [`71b6c585`](https://github.com/quarto-dev/q2/commits/71b6c585): UI exploration (branch only): shelves can be explicitly shared — a people glyph and member facepile appear on shared shelves, opening a members popover with an invite link. Invite links carry the shelf's projects, so joining from another browser delivers them for real; a brand-new browser skips setup and is asked only for a name and cursor color. Membership itself is still local mock data.

### 2026-07-07

- [`0f338475`](https://github.com/quarto-dev/q2/commits/0f338475): UI exploration (branch only): project cards, shelf headers, and the Peek popover show collaborator facepiles (colored initial disks). Collaborators are mock data for now — real attribution needs automerge-history plumbing.
- [`1a37f9eb`](https://github.com/quarto-dev/q2/commits/1a37f9eb): UI exploration (branch only): projects can be dragged between shelves and the unshelved list, with the drop target highlighted while dragging.
- [`389af46b`](https://github.com/quarto-dev/q2/commits/389af46b): UI exploration (branch only): a shelves-based full-page projects home replaces the project-selector modal — search with Cmd+K, personal shelves to group projects, streamlined New/Connect/Import dialogs, and an avatar menu holding identity, cursor color, device linking, and JSON backup. An avatar-menu item switches back to the classic UI.
- [`4af08ef3`](https://github.com/quarto-dev/q2/commits/4af08ef3): Dragging a text selection in the preview now opens the rich-text editor with that selection already active (so Bold/Italic/Link apply immediately); a selection dragged across multiple blocks no longer opens an editor, keeping the selection available for copying.
- [`6cfc098f`](https://github.com/quarto-dev/q2/commits/6cfc098f): Raised the service-worker precache size limit so the (now about 37 MB) WASM module is cached again for offline use, and to stop continued WASM growth from breaking the production build.

Expand Down
19 changes: 19 additions & 0 deletions hub-client/src/App.css
Original file line number Diff line number Diff line change
@@ -1 +1,20 @@
/* Minimal app styles - component-specific styles in their own CSS files */

/* Floating switch back to the collections UI exploration (explore/projects-collections-ui) */
.ui-variant-toggle {
position: fixed;
bottom: 16px;
right: 16px;
z-index: 2000;
padding: 8px 14px;
font-size: 12.5px;
font-weight: 600;
color: #ffffff;
background: var(--posit-teal);
border: none;
border-radius: 8px;
cursor: pointer;
box-shadow: 0 4px 14px rgba(0, 0, 0, 0.2);
}

.ui-variant-toggle:hover { filter: brightness(1.08); }
Loading