From c687002015941feabaf1bd73b747189a606f5412 Mon Sep 17 00:00:00 2001 From: gobha Date: Sun, 22 Mar 2026 00:57:49 +0000 Subject: [PATCH] Changeset 0.37.11 (#223) Co-authored-by: gobha Co-committed-by: gobha --- CHANGELOG.md | 67 ++++ VERSION | 2 +- docs/ROADMAP.md | 77 ++-- server/pages/templates/base.html | 2 + server/pages/templates/surfaces/notes.html | 70 ++-- server/version.go | 2 +- src/css/surfaces.css | 9 +- src/css/sw-notes-surface.css | 366 +++++++++++++++++++ src/js/sw/components/notes-pane/index.js | 9 +- src/js/sw/components/notes-pane/note-list.js | 48 +-- src/js/sw/primitives/avatar.js | 9 +- src/js/sw/surfaces/notes/index.js | 90 +++++ src/js/sw/surfaces/notes/notes-workspace.js | 53 +++ src/js/sw/surfaces/notes/sidebar-folders.js | 71 ++++ src/js/sw/surfaces/notes/sidebar-tags.js | 52 +++ src/js/sw/surfaces/notes/sidebar.js | 121 ++++++ src/js/sw/surfaces/notes/use-sidebar.js | 122 +++++++ src/js/sw/surfaces/notes/use-workspace.js | 59 +++ 18 files changed, 1142 insertions(+), 87 deletions(-) create mode 100644 src/css/sw-notes-surface.css create mode 100644 src/js/sw/surfaces/notes/index.js create mode 100644 src/js/sw/surfaces/notes/notes-workspace.js create mode 100644 src/js/sw/surfaces/notes/sidebar-folders.js create mode 100644 src/js/sw/surfaces/notes/sidebar-tags.js create mode 100644 src/js/sw/surfaces/notes/sidebar.js create mode 100644 src/js/sw/surfaces/notes/use-sidebar.js create mode 100644 src/js/sw/surfaces/notes/use-workspace.js diff --git a/CHANGELOG.md b/CHANGELOG.md index 4277286..14df1fc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,72 @@ # Changelog +## [0.37.11] — 2026-03-22 + +### Summary + +Notes surface built as full Preact surface, replacing the v0.37.9 template +mount. Sidebar with folder tree, tag cloud, search, and UserMenu. NotesPane +runs `standalone=false` — the surface sidebar owns filter controls. Cross-surface +fixes: Menu primitive CSS was never loaded (now added to base.html), Avatar +initials fixed for single-word names. + +### New + +- `src/js/sw/surfaces/notes/index.js` — NotesSurface root, self-mounts at `#notes-mount` +- `src/js/sw/surfaces/notes/use-workspace.js` — sidebar toggle + active note (sessionStorage) +- `src/js/sw/surfaces/notes/use-sidebar.js` — folders, tags, search, WS live updates +- `src/js/sw/surfaces/notes/sidebar.js` — search input, folder/tag sections, UserMenu footer +- `src/js/sw/surfaces/notes/sidebar-folders.js` — collapsible folder tree with counts +- `src/js/sw/surfaces/notes/sidebar-tags.js` — collapsible tag cloud with active state +- `src/js/sw/surfaces/notes/notes-workspace.js` — workspace header + NotesPane +- `src/css/sw-notes-surface.css` — surface layout (sidebar + workspace + mobile responsive) + +### Changed + +- `src/js/sw/components/notes-pane/index.js` — handleRef expanded with `setFolder`, + `setTagFilter`, `setSearchQuery`, `getFolder`, `getTagFilter`; threads `standalone` + prop to NoteList +- `src/js/sw/components/notes-pane/note-list.js` — `standalone=false` hides search row, + folder dropdown, tag cloud (surface sidebar owns these) +- `src/js/sw/primitives/avatar.js` — single-word names show first letter only + ("admin" → "A" not "AD"); multi-word unchanged ("John Doe" → "JD") +- `server/pages/templates/surfaces/notes.html` — full rewrite: crash catcher + + `#notes-mount` + Preact boot (mirrors chat.html pattern) +- `server/pages/templates/base.html` — added `sw-primitives.css` link (Menu primitive + was unstyled on all surfaces); added `css-notes` conditional +- `src/css/surfaces.css` — deleted `.surface-notes*` rules (9 lines, replaced by + `sw-notes-surface.css`) + +### Design Notes + +- **Sidebar ↔ NotesPane coordination:** write-only via handleRef. Sidebar sets + folder/tag/search on the kit; kit reloads its own list. No bidirectional sync + needed because `standalone=false` hides the kit's own filter controls. +- **Menu primitive bug:** `sw-primitives.css` was never loaded in `base.html`. + The Menu component (`sw-menu__item`) had no hover, no cursor, no background. + Chat surface masked this by using its own `.sw-chat-surface__context-menu` + styles. Fixed globally — all surfaces now get proper Menu styles. +- **Kit enhancements deferred:** markdown toolbar, wikilink hover preview, + breadcrumbs, split view, templates, slash commands, graph minimap all moved + to v0.37.14 pane audit. + +### Cumulative Layer Status + +| Layer | Version | Status | +|-------|---------|--------| +| Preact+htm vendor | v0.37.2 | ✅ | +| Layer 0: Primitives | v0.37.11 | ✅ (Menu CSS fix, Avatar fix) | +| Layer 1: SDK | v0.37.10 | ✅ (8 modules) | +| Layer 1.5: ChatPane | v0.37.10 | ✅ (13 files) | +| Layer 1.5: NotesPane | v0.37.11 | ✅ (10 files, standalone prop) | +| Layer 2: Shell | v0.37.4 | ✅ (5 components) | +| Surface: Login | v0.37.5 | ✅ | +| Surface: Settings | v0.37.7 | ✅ | +| Surface: Admin | v0.37.6 | ✅ | +| Surface: Team Admin | v0.37.7 | ✅ | +| Surface: Chat | v0.37.10 | ✅ | +| Surface: Notes | v0.37.11 | ✅ | + ## [0.37.10] — 2026-03-21 ### Summary diff --git a/VERSION b/VERSION index b5c8e6b..6979856 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -0.37.10 +0.37.11 diff --git a/docs/ROADMAP.md b/docs/ROADMAP.md index fe8e4ab..36b2203 100644 --- a/docs/ROADMAP.md +++ b/docs/ROADMAP.md @@ -48,8 +48,8 @@ v0.9.x–v0.28.7 Foundation through Platform Polish ✅ │ v0.37.2 Primitives ✅ │ │ v0.37.3 SDK │ │ v0.37.4 Shell │ - │ v0.37.5–10 Surfaces ✅ │ - │ v0.37.11–13 Surfaces │ + │ v0.37.5–11 Surfaces ✅ │ + │ v0.37.12–18 Surfaces │ │ │ │ ══════╪════════════╪═════════════════╪══════ │ MVP v0.50.0 │ @@ -625,7 +625,7 @@ Depends on: v0.37.4. bridge sections may reference helpers) - BYOK sections not tested with policy enabled -### v0.37.6–v0.37.13 — Remaining Surface Rebuilds +### v0.37.6–v0.37.18 — Remaining Surface Rebuilds Each surface rebuilt as Preact component tree. Old JS deleted per surface. @@ -636,41 +636,66 @@ Each surface rebuilt as Preact component tree. Old JS deleted per surface. | v0.37.8 | ChatPane ✅ | Composable kit (9 files), standalone stripped from old chat-pane.js | | v0.37.9 | NotesPane ✅ | Obsidian-grade kit (10 files), old notes code scorched (~1,630 lines) | | v0.37.10 | Chat surface ✅ | Scorched earth: 21 files deleted (−12,841 net), Preact surface + ChatPane kit upgrades | -| v0.37.11 | Notes surface | Composes NotesPane into full layout + enhancements (see below) | +| v0.37.11 | Notes surface ✅ | Preact surface (7 JS + 1 CSS), sidebar folders/tags, UserMenu, template rewrite, Menu primitive fix | | v0.37.12 | Extension surface container | `.pkg` mount point | | v0.37.13 | Workflow surfaces | Form renderer, review views | -| v0.37.14 | Tag | Light mode CSS audit, functionality restored, old code deleted | +| v0.37.14 | Pane audit | Collaborative walkthrough: each pane checked for form and function | +| v0.37.15 | Projects surface | Project views, note/channel associations | +| v0.37.16 | Workflow assignment | Workflow routing, assignment UX | +| v0.37.17 | Debug / model surface | Debug tooling, model configuration UI | +| v0.37.18 | Tag | Light mode CSS audit, dead code hunt, all features verified | -**v0.37.11 — Notes Surface Enhancements:** +**v0.37.11 — Notes Surface ✅:** -NotesPane kit enhancements (add to `notes-pane/` as new components): -- [ ] **Markdown toolbar** — bold, italic, heading, link, code, list, checkbox - buttons above editor textarea/CM6. High value for non-markdown users. -- [ ] **Hover preview on wikilinks** — floating card on `[[Title]]` hover - showing first ~150 chars of target note. Obsidian signature UX. -- [ ] **Breadcrumb trail** — navigation history bar when jumping through - wikilinks (note A → B → C). Back button walks the trail vs. list. -- [ ] **Split view (editor + preview)** — side-by-side markdown editing with - live rendered preview. Editor and reader already exist as separate pieces. -- [ ] **Note templates** — "New from template" in toolbar: Meeting Notes, - Project Brief, Weekly Review. Templates stored in `/templates/` folder. -- [ ] **Slash commands in editor** — `/table`, `/code`, `/heading`, `/date`, - `/link` insert markdown snippets at cursor position. -- [ ] **Graph minimap** — small overview of full graph in corner with - draggable viewport rectangle. +Notes surface layout (composed kit into full-page Preact surface): +- [x] Sidebar with folder tree + tag browser (sidebar-folders.js, sidebar-tags.js) +- [x] Main content area with NotesPane standalone=false +- [x] Mobile-responsive layout (768px breakpoint, overlay sidebar) +- [x] UserMenu in sidebar footer (same as chat surface) +- [x] Template rewrite — crash catcher + single mount point pattern +- [x] Menu primitive CSS fix — `sw-primitives.css` added to base.html (cross-surface) +- [x] Avatar initials fix — single-word names show first letter only (cross-surface) +- [x] Old `.surface-notes*` CSS deleted from surfaces.css -Notes surface layout (compose kit into full-page layout): -- [ ] Sidebar with folder tree + tag browser -- [ ] Main content area with NotesPane +NotesPane kit enhancements (deferred to v0.37.14 pane audit): +- [ ] Markdown toolbar — bold, italic, heading, link, code, list, checkbox +- [ ] Hover preview on wikilinks — floating card on `[[Title]]` hover +- [ ] Breadcrumb trail — navigation history through wikilinks +- [ ] Split view (editor + preview) — side-by-side live preview +- [ ] Note templates — "New from template" in toolbar +- [ ] Slash commands in editor — `/table`, `/code`, `/heading`, `/date`, `/link` +- [ ] Graph minimap — small overview with draggable viewport - [ ] Resizable split panes -- [ ] Mobile-responsive layout -Polish (pre-tag): +Polish (deferred to v0.37.18 tag): - [ ] Light mode CSS variable audit for `sw-notes-pane.css` - [ ] Smooth view transitions (fade/slide between list → reader → editor) - [ ] Note word count goal (optional target with progress bar) - [ ] Server-side favorites via note metadata (cross-device pinning) +**v0.37.14 — Pane Audit:** + +Collaborative walkthrough of each pane (ChatPane, NotesPane) and surface +checking form and function. Fixes discovered during the audit are applied +in-place. No new architecture — just making everything work correctly. + +- [ ] ChatPane: message rendering, streaming, model selector, markdown, code blocks +- [ ] NotesPane: list, editor, reader, graph, quick switcher, save-to-note +- [ ] Cross-pane: toast, dialog, user menu, keyboard shortcuts +- [ ] Kit enhancement backlog triage (above items evaluated for inclusion) + +**v0.37.15 — Projects Surface:** + +Project views, note/channel associations, project-scoped navigation. + +**v0.37.16 — Workflow Assignment:** + +Workflow routing and assignment UX. + +**v0.37.17 — Debug / Model Surface:** + +Debug tooling, model configuration UI, provider diagnostics. + --- ## MVP v0.50.0 diff --git a/server/pages/templates/base.html b/server/pages/templates/base.html index 681994b..0db94b0 100644 --- a/server/pages/templates/base.html +++ b/server/pages/templates/base.html @@ -16,6 +16,7 @@ {{/* panels.css, pane-container.css, chat-pane.css removed in v0.37.10 */}} + @@ -24,6 +25,7 @@ {{if eq .Surface "chat"}}{{template "css-chat" .}}{{end}} + {{if eq .Surface "notes"}}{{template "css-notes" .}}{{end}} {{/* v0.27.0: Extension surface CSS — loaded from /surfaces/{id}/css/main.css */}} {{if and .Manifest (eq .Manifest.Source "extension")}} diff --git a/server/pages/templates/surfaces/notes.html b/server/pages/templates/surfaces/notes.html index 1b8bde0..83484fe 100644 --- a/server/pages/templates/surfaces/notes.html +++ b/server/pages/templates/surfaces/notes.html @@ -1,47 +1,65 @@ {{/* - Notes surface (v0.37.9). - Server renders the layout shell. SDK boots Preact NotesPane kit - into #notesMainMount via sw.notesPane(). + Notes surface (v0.37.11 — Preact). + Replaced the v0.37.9 template mount with a single mount point. + Preact NotesSurface component handles all rendering. */}} {{define "surface-notes"}} -
-
- {{/* Notes list + editor */}} -
- {{/* Chat assist pane (optional, can toggle) */}} -
+ +{{/* ── Crash Catcher (visible without devtools) ── */}} + + + +{{/* ── Mount Point ────────────────────────── */}} +
+
+ Loading...
+ {{end}} {{define "css-notes"}} -{{/* v0.37.9: Notes surface CSS — sw-notes-pane.css loaded in base.html */}} + {{end}} {{define "scripts-notes"}} -{{/* v0.37.9: Notes surface — Preact vendor globals + SDK boot + NotesPane */}} - + + {{end}} diff --git a/server/version.go b/server/version.go index 8e329ce..d41d66b 100644 --- a/server/version.go +++ b/server/version.go @@ -1,3 +1,3 @@ package main -const Version = "0.37.10" +const Version = "0.37.11" diff --git a/src/css/surfaces.css b/src/css/surfaces.css index 0261600..eb3ca76 100644 --- a/src/css/surfaces.css +++ b/src/css/surfaces.css @@ -761,11 +761,4 @@ .chat-item.indented { padding-left: 28px; } /* ── Notes Surface ─────────────────────── */ -.surface-notes { display: flex; flex-direction: column; height: 100%; overflow: hidden; } -.surface-notes-body { flex: 1; display: flex; min-height: 0; overflow: hidden; } -.surface-notes-main { flex: 1; display: flex; flex-direction: column; min-width: 0; overflow: hidden; } -.surface-notes-assist { - width: 400px; border-left: 1px solid var(--border); - display: flex; flex-direction: column; overflow: hidden; -} -@media (max-width: 768px) { .surface-notes-assist { display: none; } } +/* v0.37.11: Moved to sw-notes-surface.css (Preact surface) */ diff --git a/src/css/sw-notes-surface.css b/src/css/sw-notes-surface.css new file mode 100644 index 0000000..b158aff --- /dev/null +++ b/src/css/sw-notes-surface.css @@ -0,0 +1,366 @@ +/* ========================================== + Chat Switchboard — Notes Surface CSS + ========================================== + v0.37.11: Preact notes surface layout. + sw-notes-surface__ prefixed, no legacy conflicts. + ========================================== */ + +/* ── Root Layout ──────────────────────────── */ + +.sw-notes-surface { + display: flex; + width: 100%; + height: 100%; + flex: 1 1 0%; + overflow: hidden; + background: var(--bg, #0e0e10); +} + +/* ── Sidebar ──────────────────────────────── */ + +.sw-notes-surface__sidebar { + width: 260px; + flex-shrink: 0; + display: flex; + flex-direction: column; + background: var(--bg-secondary, #151517); + border-right: 1px solid var(--border, #2a2a2e); + overflow: hidden; + z-index: 20; +} + +.sw-notes-surface__sidebar-header { + display: flex; + align-items: center; + gap: 6px; + padding: 10px 12px 6px; + flex-shrink: 0; +} + +.sw-notes-surface__new-note-btn { + display: flex; + align-items: center; + gap: 8px; + flex: 1; + padding: 8px 12px; + background: var(--accent-dim, rgba(179, 138, 78, 0.1)); + border: 1px solid var(--accent, #b38a4e); + border-radius: 8px; + color: var(--accent, #b38a4e); + font-size: 13px; + font-weight: 600; + cursor: pointer; + transition: background 0.15s; +} + +.sw-notes-surface__new-note-btn:hover { + background: rgba(179, 138, 78, 0.2); +} + +.sw-notes-surface__collapse-btn { + display: flex; + align-items: center; + justify-content: center; + width: 32px; + height: 32px; + background: none; + border: none; + border-radius: 6px; + color: var(--text-3, #555); + cursor: pointer; + flex-shrink: 0; + transition: color 0.15s, background 0.15s; +} + +.sw-notes-surface__collapse-btn:hover { + color: var(--text, #eee); + background: var(--bg, #0e0e10); +} + +/* ── Search ───────────────────────────────── */ + +.sw-notes-surface__search-wrap { + position: relative; + padding: 4px 12px 8px; + flex-shrink: 0; +} + +.sw-notes-surface__search-icon { + position: absolute; + left: 20px; + top: 50%; + transform: translateY(-50%); + color: var(--text-3, #555); + pointer-events: none; + display: flex; +} + +.sw-notes-surface__search { + width: 100%; + padding: 6px 8px 6px 30px; + background: var(--bg, #0e0e10); + border: 1px solid var(--border, #2a2a2e); + border-radius: 6px; + color: var(--text, #eee); + font-size: 12px; + font-family: inherit; + outline: none; + box-sizing: border-box; +} + +.sw-notes-surface__search:focus { + border-color: var(--accent, #b38a4e); +} + +/* ── Sidebar Body (scrollable) ────────────── */ + +.sw-notes-surface__sidebar-body { + flex: 1; + overflow-y: auto; + min-height: 0; + padding-bottom: 8px; +} + +/* ── Section ──────────────────────────────── */ + +.sw-notes-surface__section { + margin-top: 4px; +} + +.sw-notes-surface__section-header { + display: flex; + align-items: center; + gap: 6px; + width: 100%; + padding: 6px 12px; + background: none; + border: none; + color: var(--text-2, #999); + font-size: 11px; + font-weight: 700; + text-transform: uppercase; + letter-spacing: 0.5px; + cursor: pointer; + transition: color 0.15s; +} + +.sw-notes-surface__section-header:hover { + color: var(--text, #eee); +} + +.sw-notes-surface__chevron { + display: flex; + transition: transform 0.15s; +} + +.sw-notes-surface__chevron--open { + transform: rotate(90deg); +} + +.sw-notes-surface__section-count { + margin-left: auto; + font-size: 10px; + color: var(--text-3, #555); + font-weight: 400; +} + +.sw-notes-surface__section-body { + padding: 0 4px; +} + +.sw-notes-surface__empty { + padding: 8px 16px; + font-size: 12px; + color: var(--text-3, #555); +} + +/* ── Sidebar Item ─────────────────────────── */ + +.sw-notes-surface__item { + display: flex; + align-items: center; + gap: 8px; + width: 100%; + padding: 6px 12px; + background: none; + border: none; + border-radius: 6px; + color: var(--text-2, #999); + font-size: 13px; + cursor: pointer; + text-align: left; + transition: background 0.1s, color 0.1s; + position: relative; +} + +.sw-notes-surface__item:hover { + background: var(--bg, #0e0e10); + color: var(--text, #eee); +} + +.sw-notes-surface__item--active { + background: var(--accent-dim, rgba(179, 138, 78, 0.1)); + color: var(--text, #eee); +} + +.sw-notes-surface__item-icon { + display: flex; + flex-shrink: 0; + color: var(--text-3, #555); +} + +.sw-notes-surface__item-title { + flex: 1; + min-width: 0; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.sw-notes-surface__item-count { + font-size: 10px; + color: var(--text-3, #555); + flex-shrink: 0; +} + +/* ── Tag Cloud ───────────────────────────── */ + +.sw-notes-surface__tag-cloud { + display: flex; + flex-wrap: wrap; + gap: 4px; + padding: 4px 8px; +} + +.sw-notes-surface__tag-chip { + display: inline-block; + padding: 2px 8px; + background: var(--bg, #0e0e10); + border: 1px solid var(--border, #2a2a2e); + border-radius: 12px; + color: var(--text-2, #999); + font-size: 11px; + cursor: pointer; + transition: background 0.1s, color 0.1s, border-color 0.1s; + user-select: none; +} + +.sw-notes-surface__tag-chip:hover { + color: var(--text, #eee); + border-color: var(--text-3, #555); +} + +.sw-notes-surface__tag-chip--active { + background: var(--accent-dim, rgba(179, 138, 78, 0.1)); + border-color: var(--accent, #b38a4e); + color: var(--accent, #b38a4e); +} + +/* ── Sidebar Footer ──────────────────────── */ + +.sw-notes-surface__sidebar-footer { + display: flex; + align-items: center; + padding: 8px 12px; + border-top: 1px solid var(--border, #2a2a2e); + flex-shrink: 0; + position: relative; +} + +/* ── Main Area ───────────────────────────── */ + +.sw-notes-surface__main { + flex: 1; + display: flex; + flex-direction: column; + min-width: 0; + position: relative; +} + +/* ── Workspace ───────────────────────────── */ + +.sw-notes-surface__workspace { + flex: 1 1 0%; + display: flex; + flex-direction: column; + min-height: 0; + min-width: 0; + position: relative; + overflow: hidden; +} + +.sw-notes-surface__workspace-header { + display: flex; + align-items: center; + justify-content: space-between; + padding: 4px 10px; + border-bottom: 1px solid var(--border, #2a2a2e); + background: var(--bg-secondary, #151517); + flex-shrink: 0; + min-height: 40px; +} + +.sw-notes-surface__workspace-header-left, +.sw-notes-surface__workspace-header-right { + display: flex; + align-items: center; + gap: 6px; +} + +.sw-notes-surface__sidebar-toggle { + display: flex; + align-items: center; + justify-content: center; + width: 30px; + height: 30px; + background: none; + border: none; + border-radius: 6px; + color: var(--text-3, #555); + cursor: pointer; + transition: color 0.15s, background 0.15s; +} + +.sw-notes-surface__sidebar-toggle:hover { + color: var(--text, #eee); + background: var(--bg, #0e0e10); +} + +/* ── NotesPane Container ─────────────────── */ + +.sw-notes-surface__notes-pane { + flex: 1 1 0%; + min-height: 0; + min-width: 0; +} + +/* ── Sidebar Overlay (mobile) ────────────── */ + +.sw-notes-surface__sidebar-overlay { + display: none; /* Hidden on desktop */ + position: fixed; + inset: 0; + background: rgba(0, 0, 0, 0.5); + z-index: 15; +} + +/* ── Responsive: Mobile ──────────────────── */ + +@media (max-width: 768px) { + .sw-notes-surface__sidebar { + position: fixed; + left: 0; + top: 0; + bottom: 0; + width: 280px; + z-index: 20; + } + + .sw-notes-surface__sidebar-overlay { + display: block; + } + + .sw-notes-surface__collapse-btn { + display: none; + } +} diff --git a/src/js/sw/components/notes-pane/index.js b/src/js/sw/components/notes-pane/index.js index 03f8508..1707847 100644 --- a/src/js/sw/components/notes-pane/index.js +++ b/src/js/sw/components/notes-pane/index.js @@ -84,6 +84,12 @@ export function NotesPane(props) { refreshList() { notes.refreshList(); }, getEditingId() { return notes.editingId; }, openQuickSwitcher() { notes.setQuickSwitcherOpen(true); }, + // Surface-driven filter controls (v0.37.11) + setFolder(f) { notes.setFolder(f); }, + setTagFilter(t) { notes.setTagFilter(t); }, + setSearchQuery(q) { notes.setSearchQuery(q); }, + getFolder() { return notes.folder; }, + getTagFilter() { return notes.tagFilter; }, }; } }); @@ -115,7 +121,8 @@ export function NotesPane(props) { onNewNote=${notes.newNote} onDailyNote=${notes.openDailyNote} onGraphClick=${() => notes.setView('graph')} - pinnedIds=${notes.pinnedIds} togglePin=${notes.togglePin} />`; + pinnedIds=${notes.pinnedIds} togglePin=${notes.togglePin} + standalone=${standalone} />`; case 'editor': return html` diff --git a/src/js/sw/components/notes-pane/note-list.js b/src/js/sw/components/notes-pane/note-list.js index 47cf8d5..b59aa24 100644 --- a/src/js/sw/components/notes-pane/note-list.js +++ b/src/js/sw/components/notes-pane/note-list.js @@ -37,8 +37,12 @@ export function NoteList(props) { toggleSelect, toggleSelectAll, bulkDelete, onNoteClick, onNewNote, onDailyNote, onGraphClick, pinnedIds, togglePin, + standalone = true, } = props; + // When standalone=false, the surface sidebar owns search/filter/tag controls + const showFilters = standalone !== false; + const searchTimerRef = useRef(null); const searchInputRef = useRef(null); @@ -81,29 +85,31 @@ export function NoteList(props) {
-
- -
+ ${showFilters && html` +
+ +
`} -
- - -
+ ${showFilters && html` +
+ + +
`} - ${topTags.length > 0 && html` + ${showFilters && topTags.length > 0 && html`
${topTags.map(([tag, count]) => html` 1 - ? (parts[0][0] + parts[parts.length - 1][0]).toUpperCase() - : name.slice(0, 2).toUpperCase(); + if (parts.length > 1) { + // Multi-word: first + last initials ("John Doe" → "JD") + return (parts[0][0] + parts[parts.length - 1][0]).toUpperCase(); + } + // Single word: first letter only ("admin" → "A") + return name[0].toUpperCase(); } export function Avatar({ src, name = '', size = 'md' }) { diff --git a/src/js/sw/surfaces/notes/index.js b/src/js/sw/surfaces/notes/index.js new file mode 100644 index 0000000..6525280 --- /dev/null +++ b/src/js/sw/surfaces/notes/index.js @@ -0,0 +1,90 @@ +// ========================================== +// Notes Surface — Root Component +// ========================================== +// Main notes surface: sidebar + workspace. +// Follows the chat surface mount pattern. +// +// v0.37.11: New Preact surface replacing the legacy template mount. + +import { ToastContainer } from '../../primitives/toast.js'; +import { DialogStack } from '../../shell/dialog-stack.js'; +import { useWorkspace } from './use-workspace.js'; +import { useSidebar } from './use-sidebar.js'; +import { Sidebar } from './sidebar.js'; +import { NotesWorkspace } from './notes-workspace.js'; + +const { render } = window.preact; +const html = window.html; +const { useRef, useCallback } = window.hooks; + +function NotesSurface() { + const workspace = useWorkspace(); + const sidebar = useSidebar(); + const notesRef = useRef(null); + + const _onNewNote = useCallback(() => { + if (notesRef.current) notesRef.current.newNote(); + }, []); + + const _toggleSidebar = useCallback(() => { + workspace.setSidebarOpen(!workspace.sidebarOpen); + }, [workspace.sidebarOpen, workspace.setSidebarOpen]); + + const _onFolderSelect = useCallback((path) => { + if (notesRef.current?.setFolder) notesRef.current.setFolder(path); + }, []); + + const _onTagSelect = useCallback((tag) => { + if (notesRef.current?.setTagFilter) notesRef.current.setTagFilter(tag); + }, []); + + const _onSearch = useCallback((query) => { + if (notesRef.current?.setSearchQuery) notesRef.current.setSearchQuery(query); + }, []); + + const _onNoteChange = useCallback((note) => { + if (note?.id) { + workspace.selectNote(note.id); + } + }, [workspace.selectNote]); + + return html` +
+ ${/* Mobile sidebar overlay */''} + ${workspace.sidebarOpen && html` +
`} + + ${/* Sidebar */''} + ${workspace.sidebarOpen && html` + <${Sidebar} + sidebar=${sidebar} + onNewNote=${_onNewNote} + onCollapse=${_toggleSidebar} + onFolderSelect=${_onFolderSelect} + onTagSelect=${_onTagSelect} + onSearch=${_onSearch} />`} + + ${/* Main workspace */''} +
+ <${NotesWorkspace} + sidebarOpen=${workspace.sidebarOpen} + onToggleSidebar=${_toggleSidebar} + initialNoteId=${workspace.activeNoteId} + onNoteChange=${_onNoteChange} + handleRef=${notesRef} /> +
+
+ <${ToastContainer} /> + <${DialogStack} /> + `; +} + +// ── Mount ──────────────────────────────────── +const mount = document.getElementById('notes-mount'); +if (mount) { + render(html`<${NotesSurface} />`, mount); + console.log('[notes] Surface mounted (v0.37.11)'); +} + +export { NotesSurface }; diff --git a/src/js/sw/surfaces/notes/notes-workspace.js b/src/js/sw/surfaces/notes/notes-workspace.js new file mode 100644 index 0000000..c2a8bfd --- /dev/null +++ b/src/js/sw/surfaces/notes/notes-workspace.js @@ -0,0 +1,53 @@ +// ========================================== +// Notes Surface — NotesWorkspace Component +// ========================================== +// Header bar (sidebar toggle) + NotesPane. +// NotesPane runs standalone=false — the surface manages filters. +// +// v0.37.11 + +import { NotesPane } from '../../components/notes-pane/index.js'; + +const html = window.html; + +const PANEL_SVG = html` + + + + `; + +/** + * @param {{ + * sidebarOpen: boolean, + * onToggleSidebar: () => void, + * initialNoteId?: string, + * onNoteChange?: (note: object|null) => void, + * handleRef?: { current: any }, + * }} props + */ +export function NotesWorkspace({ + sidebarOpen, onToggleSidebar, initialNoteId, onNoteChange, handleRef, +}) { + return html` +
+
+
+ ${!sidebarOpen && html` + `} +
+
+
+ <${NotesPane} + standalone=${false} + initialNoteId=${initialNoteId} + handleRef=${handleRef} + onNoteChange=${onNoteChange} + className="sw-notes-surface__notes-pane" /> +
`; +} diff --git a/src/js/sw/surfaces/notes/sidebar-folders.js b/src/js/sw/surfaces/notes/sidebar-folders.js new file mode 100644 index 0000000..e161ed1 --- /dev/null +++ b/src/js/sw/surfaces/notes/sidebar-folders.js @@ -0,0 +1,71 @@ +// ========================================== +// Notes Surface — SidebarFolders Component +// ========================================== +// Collapsible folder tree for the notes sidebar. +// v0.37.11 + +const html = window.html; + +const CHEVRON_SVG = html` + + + `; + +const FOLDER_SVG = html` + + + `; + +/** + * @param {{ + * folders: Array<{ path: string, count: number }>, + * activeFolder: string, + * collapsed: boolean, + * onToggle: () => void, + * onSelect: (path: string) => void, + * }} props + */ +export function SidebarFolders({ folders, activeFolder, collapsed, onToggle, onSelect }) { + const totalCount = folders.reduce((sum, f) => sum + (f.count || 0), 0); + + return html` +
+ + ${!collapsed && html` +
+ ${/* "All Notes" clears folder filter */''} + + ${folders.map(f => html` + `)} + ${folders.length === 0 && html` +
No folders
`} +
`} +
`; +} diff --git a/src/js/sw/surfaces/notes/sidebar-tags.js b/src/js/sw/surfaces/notes/sidebar-tags.js new file mode 100644 index 0000000..2c86dc0 --- /dev/null +++ b/src/js/sw/surfaces/notes/sidebar-tags.js @@ -0,0 +1,52 @@ +// ========================================== +// Notes Surface — SidebarTags Component +// ========================================== +// Collapsible tag cloud for the notes sidebar. +// v0.37.11 + +const html = window.html; + +const CHEVRON_SVG = html` + + + `; + +/** + * @param {{ + * tags: Array<[string, number]>, + * activeTag: string, + * collapsed: boolean, + * onToggle: () => void, + * onSelect: (tag: string) => void, + * }} props + */ +export function SidebarTags({ tags, activeTag, collapsed, onToggle, onSelect }) { + return html` +
+ + ${!collapsed && html` +
+
+ ${tags.length === 0 && html` +
No tags
`} + ${tags.map(([tag, count]) => html` + onSelect(activeTag === tag ? '' : tag)} + title=${tag + ' (' + count + ')'}> + ${tag} + `)} +
+
`} +
`; +} diff --git a/src/js/sw/surfaces/notes/sidebar.js b/src/js/sw/surfaces/notes/sidebar.js new file mode 100644 index 0000000..27bd273 --- /dev/null +++ b/src/js/sw/surfaces/notes/sidebar.js @@ -0,0 +1,121 @@ +// ========================================== +// Notes Surface — Sidebar Component +// ========================================== +// Search bar, new note, folder tree, tag cloud. +// Footer has UserMenu — same pattern as chat sidebar. +// +// v0.37.11 + +import { SidebarFolders } from './sidebar-folders.js'; +import { SidebarTags } from './sidebar-tags.js'; +import { UserMenu } from '../../shell/user-menu.js'; + +const html = window.html; +const { useCallback, useRef } = window.hooks; + +const PLUS_SVG = html` + + + + `; + +const SEARCH_SVG = html` + + + + `; + +const COLLAPSE_SVG = html` + + + + `; + +/** + * @param {{ + * sidebar: ReturnType, + * onNewNote: () => void, + * onCollapse: () => void, + * onFolderSelect: (path: string) => void, + * onTagSelect: (tag: string) => void, + * onSearch: (query: string) => void, + * }} props + */ +export function Sidebar({ sidebar, onNewNote, onCollapse, onFolderSelect, onTagSelect, onSearch }) { + const searchTimerRef = useRef(null); + + const _onSearchInput = useCallback((e) => { + const val = e.target.value; + sidebar.setSearch(val); + clearTimeout(searchTimerRef.current); + searchTimerRef.current = setTimeout(() => { + onSearch(val.trim()); + }, 300); + }, [sidebar.setSearch, onSearch]); + + const _onFolderSelect = useCallback((path) => { + sidebar.setActiveFolder(path); + onFolderSelect(path); + }, [sidebar.setActiveFolder, onFolderSelect]); + + const _onTagSelect = useCallback((tag) => { + sidebar.setActiveTag(tag); + onTagSelect(tag); + }, [sidebar.setActiveTag, onTagSelect]); + + return html` + `; +} diff --git a/src/js/sw/surfaces/notes/use-sidebar.js b/src/js/sw/surfaces/notes/use-sidebar.js new file mode 100644 index 0000000..33ae6db --- /dev/null +++ b/src/js/sw/surfaces/notes/use-sidebar.js @@ -0,0 +1,122 @@ +// ========================================== +// Notes Surface — useSidebar Hook +// ========================================== +// Loads folders and tags for the sidebar. +// Subscribes to WS events for live updates. +// +// v0.37.11: New Preact surface. + +const { useState, useEffect, useCallback, useMemo } = window.hooks; + +const COLLAPSE_KEY = 'sw-notes-sidebar-collapsed'; + +/** + * @returns {{ + * folders: Array<{ path: string, count: number }>, + * tags: Array<[string, number]>, + * activeFolder: string, + * activeTag: string, + * search: string, + * setActiveFolder: (f: string) => void, + * setActiveTag: (t: string) => void, + * setSearch: (q: string) => void, + * collapsed: { folders: boolean, tags: boolean }, + * toggleCollapsed: (key: string) => void, + * loading: boolean, + * }} + */ +export function useSidebar() { + const [folders, setFolders] = useState([]); + const [allNotes, setAllNotes] = useState([]); + const [activeFolder, setActiveFolder] = useState(''); + const [activeTag, setActiveTag] = useState(''); + const [search, setSearch] = useState(''); + const [collapsed, setCollapsed] = useState(_loadCollapsed); + const [loading, setLoading] = useState(true); + + // ── Load data ─────────────────────────── + const reload = useCallback(async () => { + if (!window.sw?.api?.notes) return; + + try { + const [fResp, nResp] = await Promise.all([ + window.sw.api.notes.folders().catch(() => []), + window.sw.api.notes.list({ per_page: 200 }).catch(() => []), + ]); + + setFolders(_extract(fResp)); + setAllNotes(_extract(nResp)); + } catch (_) {} + + setLoading(false); + }, []); + + useEffect(() => { reload(); }, [reload]); + + // ── WebSocket live updates ────────────── + useEffect(() => { + if (!window.sw?.on) return; + + const events = [ + 'note.created', 'note.updated', 'note.deleted', + ]; + + events.forEach(ev => window.sw.on(ev, reload)); + return () => { + events.forEach(ev => window.sw.off?.(ev, reload)); + }; + }, [reload]); + + // ── Aggregate tags from notes ─────────── + const tags = useMemo(() => { + const counts = {}; + for (const n of allNotes) { + for (const t of (n.tags || [])) { + counts[t] = (counts[t] || 0) + 1; + } + } + return Object.entries(counts) + .sort((a, b) => b[1] - a[1]) + .slice(0, 30); + }, [allNotes]); + + // ── Collapse sections ─────────────────── + const toggleCollapsed = useCallback((key) => { + setCollapsed(prev => { + const next = { ...prev, [key]: !prev[key] }; + try { localStorage.setItem(COLLAPSE_KEY, JSON.stringify(next)); } catch (_) {} + return next; + }); + }, []); + + return { + folders, + tags, + activeFolder, + activeTag, + search, + setActiveFolder, + setActiveTag, + setSearch, + collapsed, + toggleCollapsed, + loading, + }; +} + +function _extract(resp) { + if (!resp) return []; + if (Array.isArray(resp)) return resp; + if (Array.isArray(resp.data)) return resp.data; + const vals = Object.values(resp); + for (const v of vals) { + if (Array.isArray(v)) return v; + } + return []; +} + +function _loadCollapsed() { + try { + return JSON.parse(localStorage.getItem(COLLAPSE_KEY)) || {}; + } catch (_) { return {}; } +} diff --git a/src/js/sw/surfaces/notes/use-workspace.js b/src/js/sw/surfaces/notes/use-workspace.js new file mode 100644 index 0000000..2c30002 --- /dev/null +++ b/src/js/sw/surfaces/notes/use-workspace.js @@ -0,0 +1,59 @@ +// ========================================== +// Notes Surface — useWorkspace Hook +// ========================================== +// Coordinator hook: manages sidebar state and active note. +// Persists active selection to sessionStorage for reload restore. +// +// v0.37.11: New Preact surface. + +const { useState, useCallback, useEffect } = window.hooks; + +const STORAGE_KEY = 'sw-notes-active'; + +/** + * @returns {{ + * activeNoteId: string|null, + * sidebarOpen: boolean, + * setSidebarOpen: (v: boolean) => void, + * selectNote: (id: string) => void, + * clearSelection: () => void, + * }} + */ +export function useWorkspace() { + const stored = _loadStored(); + const [activeNoteId, setActiveNoteId] = useState(stored); + const [sidebarOpen, setSidebarOpen] = useState(true); + + // Persist on change + useEffect(() => { + if (activeNoteId) { + try { sessionStorage.setItem(STORAGE_KEY, activeNoteId); } catch (_) {} + } else { + sessionStorage.removeItem(STORAGE_KEY); + } + }, [activeNoteId]); + + const selectNote = useCallback((id) => { + setActiveNoteId(id); + // Auto-close sidebar on mobile + if (window.innerWidth < 768) setSidebarOpen(false); + }, []); + + const clearSelection = useCallback(() => { + setActiveNoteId(null); + }, []); + + return { + activeNoteId, + sidebarOpen, + setSidebarOpen, + selectNote, + clearSelection, + }; +} + +function _loadStored() { + try { + return sessionStorage.getItem(STORAGE_KEY) || null; + } catch (_) { return null; } +}