Changeset 0.37.11 (#223)
Co-authored-by: gobha <jasafpro@gmail.com> Co-committed-by: gobha <jasafpro@gmail.com>
This commit is contained in:
67
CHANGELOG.md
67
CHANGELOG.md
@@ -1,5 +1,72 @@
|
|||||||
# Changelog
|
# 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
|
## [0.37.10] — 2026-03-21
|
||||||
|
|
||||||
### Summary
|
### Summary
|
||||||
|
|||||||
@@ -48,8 +48,8 @@ v0.9.x–v0.28.7 Foundation through Platform Polish ✅
|
|||||||
│ v0.37.2 Primitives ✅ │
|
│ v0.37.2 Primitives ✅ │
|
||||||
│ v0.37.3 SDK │
|
│ v0.37.3 SDK │
|
||||||
│ v0.37.4 Shell │
|
│ v0.37.4 Shell │
|
||||||
│ v0.37.5–10 Surfaces ✅ │
|
│ v0.37.5–11 Surfaces ✅ │
|
||||||
│ v0.37.11–13 Surfaces │
|
│ v0.37.12–18 Surfaces │
|
||||||
│ │ │
|
│ │ │
|
||||||
══════╪════════════╪═════════════════╪══════
|
══════╪════════════╪═════════════════╪══════
|
||||||
│ MVP v0.50.0 │
|
│ MVP v0.50.0 │
|
||||||
@@ -625,7 +625,7 @@ Depends on: v0.37.4.
|
|||||||
bridge sections may reference helpers)
|
bridge sections may reference helpers)
|
||||||
- BYOK sections not tested with policy enabled
|
- 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.
|
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.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.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.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.12 | Extension surface container | `.pkg` mount point |
|
||||||
| v0.37.13 | Workflow surfaces | Form renderer, review views |
|
| 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):
|
Notes surface layout (composed kit into full-page Preact surface):
|
||||||
- [ ] **Markdown toolbar** — bold, italic, heading, link, code, list, checkbox
|
- [x] Sidebar with folder tree + tag browser (sidebar-folders.js, sidebar-tags.js)
|
||||||
buttons above editor textarea/CM6. High value for non-markdown users.
|
- [x] Main content area with NotesPane standalone=false
|
||||||
- [ ] **Hover preview on wikilinks** — floating card on `[[Title]]` hover
|
- [x] Mobile-responsive layout (768px breakpoint, overlay sidebar)
|
||||||
showing first ~150 chars of target note. Obsidian signature UX.
|
- [x] UserMenu in sidebar footer (same as chat surface)
|
||||||
- [ ] **Breadcrumb trail** — navigation history bar when jumping through
|
- [x] Template rewrite — crash catcher + single mount point pattern
|
||||||
wikilinks (note A → B → C). Back button walks the trail vs. list.
|
- [x] Menu primitive CSS fix — `sw-primitives.css` added to base.html (cross-surface)
|
||||||
- [ ] **Split view (editor + preview)** — side-by-side markdown editing with
|
- [x] Avatar initials fix — single-word names show first letter only (cross-surface)
|
||||||
live rendered preview. Editor and reader already exist as separate pieces.
|
- [x] Old `.surface-notes*` CSS deleted from surfaces.css
|
||||||
- [ ] **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 (compose kit into full-page layout):
|
NotesPane kit enhancements (deferred to v0.37.14 pane audit):
|
||||||
- [ ] Sidebar with folder tree + tag browser
|
- [ ] Markdown toolbar — bold, italic, heading, link, code, list, checkbox
|
||||||
- [ ] Main content area with NotesPane
|
- [ ] 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
|
- [ ] 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`
|
- [ ] Light mode CSS variable audit for `sw-notes-pane.css`
|
||||||
- [ ] Smooth view transitions (fade/slide between list → reader → editor)
|
- [ ] Smooth view transitions (fade/slide between list → reader → editor)
|
||||||
- [ ] Note word count goal (optional target with progress bar)
|
- [ ] Note word count goal (optional target with progress bar)
|
||||||
- [ ] Server-side favorites via note metadata (cross-device pinning)
|
- [ ] 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
|
## MVP v0.50.0
|
||||||
|
|||||||
@@ -16,6 +16,7 @@
|
|||||||
{{/* panels.css, pane-container.css, chat-pane.css removed in v0.37.10 */}}
|
{{/* panels.css, pane-container.css, chat-pane.css removed in v0.37.10 */}}
|
||||||
<link rel="stylesheet" href="{{.BasePath}}/css/surfaces.css?v={{.Version}}">
|
<link rel="stylesheet" href="{{.BasePath}}/css/surfaces.css?v={{.Version}}">
|
||||||
<link rel="stylesheet" href="{{.BasePath}}/css/splash.css?v={{.Version}}">
|
<link rel="stylesheet" href="{{.BasePath}}/css/splash.css?v={{.Version}}">
|
||||||
|
<link rel="stylesheet" href="{{.BasePath}}/css/sw-primitives.css?v={{.Version}}">
|
||||||
<link rel="stylesheet" href="{{.BasePath}}/css/sw-chat-pane.css?v={{.Version}}">
|
<link rel="stylesheet" href="{{.BasePath}}/css/sw-chat-pane.css?v={{.Version}}">
|
||||||
<link rel="stylesheet" href="{{.BasePath}}/css/sw-notes-pane.css?v={{.Version}}">
|
<link rel="stylesheet" href="{{.BasePath}}/css/sw-notes-pane.css?v={{.Version}}">
|
||||||
<link rel="stylesheet" href="{{.BasePath}}/css/user-menu.css?v={{.Version}}">
|
<link rel="stylesheet" href="{{.BasePath}}/css/user-menu.css?v={{.Version}}">
|
||||||
@@ -24,6 +25,7 @@
|
|||||||
<link rel="stylesheet" href="{{.BasePath}}/css/admin-surfaces.css?v={{.Version}}">
|
<link rel="stylesheet" href="{{.BasePath}}/css/admin-surfaces.css?v={{.Version}}">
|
||||||
<link rel="stylesheet" href="{{.BasePath}}/css/extension-surface.css?v={{.Version}}">
|
<link rel="stylesheet" href="{{.BasePath}}/css/extension-surface.css?v={{.Version}}">
|
||||||
{{if eq .Surface "chat"}}{{template "css-chat" .}}{{end}}
|
{{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 */}}
|
{{/* v0.27.0: Extension surface CSS — loaded from /surfaces/{id}/css/main.css */}}
|
||||||
{{if and .Manifest (eq .Manifest.Source "extension")}}
|
{{if and .Manifest (eq .Manifest.Source "extension")}}
|
||||||
<link rel="stylesheet" href="{{.BasePath}}/surfaces/{{.Surface}}/css/main.css?v={{.Version}}">
|
<link rel="stylesheet" href="{{.BasePath}}/surfaces/{{.Surface}}/css/main.css?v={{.Version}}">
|
||||||
|
|||||||
@@ -1,47 +1,65 @@
|
|||||||
{{/*
|
{{/*
|
||||||
Notes surface (v0.37.9).
|
Notes surface (v0.37.11 — Preact).
|
||||||
Server renders the layout shell. SDK boots Preact NotesPane kit
|
Replaced the v0.37.9 template mount with a single mount point.
|
||||||
into #notesMainMount via sw.notesPane().
|
Preact NotesSurface component handles all rendering.
|
||||||
*/}}
|
*/}}
|
||||||
|
|
||||||
{{define "surface-notes"}}
|
{{define "surface-notes"}}
|
||||||
<div class="surface-notes" id="surfaceNotes">
|
|
||||||
<div class="surface-notes-body">
|
{{/* ── Crash Catcher (visible without devtools) ── */}}
|
||||||
{{/* Notes list + editor */}}
|
<div id="crashBanner" style="display:none;position:fixed;top:0;left:0;right:0;z-index:99999;background:#b00020;color:#fff;padding:12px 20px;font-family:monospace;font-size:13px;max-height:40vh;overflow-y:auto;">
|
||||||
<div class="surface-notes-main" id="notesMainMount"></div>
|
<strong>Init Error</strong> <button onclick="this.parentElement.style.display='none'" style="float:right;background:none;border:1px solid #fff;color:#fff;cursor:pointer;padding:2px 8px;border-radius:3px">X</button>
|
||||||
{{/* Chat assist pane (optional, can toggle) */}}
|
<pre id="crashDetail" style="margin:8px 0 0;white-space:pre-wrap;font-size:12px;"></pre>
|
||||||
<div class="surface-notes-assist" id="notesAssistMount"></div>
|
</div>
|
||||||
|
<script nonce="{{$.CSPNonce}}">
|
||||||
|
window.addEventListener('error', function(e) {
|
||||||
|
var b = document.getElementById('crashBanner');
|
||||||
|
var d = document.getElementById('crashDetail');
|
||||||
|
if (b && d) {
|
||||||
|
b.style.display = '';
|
||||||
|
d.textContent += (e.filename || '') + ':' + (e.lineno || '') + ' ' + (e.message || e) + '\n';
|
||||||
|
}
|
||||||
|
});
|
||||||
|
window.addEventListener('unhandledrejection', function(e) {
|
||||||
|
var b = document.getElementById('crashBanner');
|
||||||
|
var d = document.getElementById('crashDetail');
|
||||||
|
if (b && d) {
|
||||||
|
b.style.display = '';
|
||||||
|
d.textContent += 'Promise: ' + (e.reason?.message || e.reason || 'unknown') + '\n' + (e.reason?.stack || '') + '\n';
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
{{/* ── Mount Point ────────────────────────── */}}
|
||||||
|
<div id="notes-mount" style="height:100%;display:flex;">
|
||||||
|
<div style="display:flex;align-items:center;justify-content:center;width:100%;color:var(--text-2,#999);font-size:14px;">
|
||||||
|
Loading...
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{{end}}
|
{{end}}
|
||||||
|
|
||||||
{{define "css-notes"}}
|
{{define "css-notes"}}
|
||||||
{{/* v0.37.9: Notes surface CSS — sw-notes-pane.css loaded in base.html */}}
|
<link rel="stylesheet" href="{{.BasePath}}/css/sw-notes-surface.css?v={{.Version}}">
|
||||||
<link rel="stylesheet" href="{{.BasePath}}/branding/custom.css" onerror="this.remove()">
|
<link rel="stylesheet" href="{{.BasePath}}/branding/custom.css" onerror="this.remove()">
|
||||||
{{end}}
|
{{end}}
|
||||||
|
|
||||||
{{define "scripts-notes"}}
|
{{define "scripts-notes"}}
|
||||||
{{/* v0.37.9: Notes surface — Preact vendor globals + SDK boot + NotesPane */}}
|
<script nonce="{{$.CSPNonce}}" src="{{$.BasePath}}/vendor/marked.min.js"></script>
|
||||||
<script type="module" nonce="{{.CSPNonce}}">
|
<script nonce="{{$.CSPNonce}}" src="{{$.BasePath}}/vendor/purify.min.js"></script>
|
||||||
// Vendor: Preact + htm → globals
|
<script type="module" nonce="{{$.CSPNonce}}">
|
||||||
const { h, render } = await import('{{.BasePath}}/js/sw/vendor/preact.module.js');
|
// v0.37.11: Preact boot — same pattern as chat surface.
|
||||||
const hooksModule = await import('{{.BasePath}}/js/sw/vendor/hooks.module.js');
|
const { h, render } = await import('{{$.BasePath}}/js/sw/vendor/preact.module.js');
|
||||||
const { default: htm } = await import('{{.BasePath}}/js/sw/vendor/htm.module.js');
|
const hooksModule = await import('{{$.BasePath}}/js/sw/vendor/hooks.module.js');
|
||||||
|
const { default: htm } = await import('{{$.BasePath}}/js/sw/vendor/htm.module.js');
|
||||||
const html = htm.bind(h);
|
const html = htm.bind(h);
|
||||||
window.preact = window.preact || { h, render };
|
window.preact = window.preact || { h, render };
|
||||||
window.hooks = window.hooks || hooksModule;
|
window.hooks = window.hooks || hooksModule;
|
||||||
window.html = window.html || html;
|
window.html = window.html || html;
|
||||||
|
|
||||||
// Boot SDK
|
const { boot } = await import('{{$.BasePath}}/js/sw/sdk/index.js?v={{$.Version}}');
|
||||||
const { boot } = await import('{{.BasePath}}/js/sw/sdk/index.js?v={{.Version}}');
|
await boot();
|
||||||
const sw = await boot();
|
|
||||||
|
|
||||||
// Mount NotesPane
|
await import('{{$.BasePath}}/js/sw/surfaces/notes/index.js?v={{$.Version}}');
|
||||||
const mount = document.getElementById('notesMainMount');
|
|
||||||
if (mount) sw.notesPane(mount, { standalone: true });
|
|
||||||
|
|
||||||
// Optional: mount chat assist pane
|
|
||||||
const assist = document.getElementById('notesAssistMount');
|
|
||||||
if (assist) sw.chatPane(assist, { standalone: true });
|
|
||||||
</script>
|
</script>
|
||||||
{{end}}
|
{{end}}
|
||||||
|
|||||||
@@ -1,3 +1,3 @@
|
|||||||
package main
|
package main
|
||||||
|
|
||||||
const Version = "0.37.10"
|
const Version = "0.37.11"
|
||||||
|
|||||||
@@ -761,11 +761,4 @@
|
|||||||
.chat-item.indented { padding-left: 28px; }
|
.chat-item.indented { padding-left: 28px; }
|
||||||
|
|
||||||
/* ── Notes Surface ─────────────────────── */
|
/* ── Notes Surface ─────────────────────── */
|
||||||
.surface-notes { display: flex; flex-direction: column; height: 100%; overflow: hidden; }
|
/* v0.37.11: Moved to sw-notes-surface.css (Preact surface) */
|
||||||
.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; } }
|
|
||||||
|
|||||||
366
src/css/sw-notes-surface.css
Normal file
366
src/css/sw-notes-surface.css
Normal file
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -84,6 +84,12 @@ export function NotesPane(props) {
|
|||||||
refreshList() { notes.refreshList(); },
|
refreshList() { notes.refreshList(); },
|
||||||
getEditingId() { return notes.editingId; },
|
getEditingId() { return notes.editingId; },
|
||||||
openQuickSwitcher() { notes.setQuickSwitcherOpen(true); },
|
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}
|
onNewNote=${notes.newNote}
|
||||||
onDailyNote=${notes.openDailyNote}
|
onDailyNote=${notes.openDailyNote}
|
||||||
onGraphClick=${() => notes.setView('graph')}
|
onGraphClick=${() => notes.setView('graph')}
|
||||||
pinnedIds=${notes.pinnedIds} togglePin=${notes.togglePin} />`;
|
pinnedIds=${notes.pinnedIds} togglePin=${notes.togglePin}
|
||||||
|
standalone=${standalone} />`;
|
||||||
|
|
||||||
case 'editor':
|
case 'editor':
|
||||||
return html`
|
return html`
|
||||||
|
|||||||
@@ -37,8 +37,12 @@ export function NoteList(props) {
|
|||||||
toggleSelect, toggleSelectAll, bulkDelete,
|
toggleSelect, toggleSelectAll, bulkDelete,
|
||||||
onNoteClick, onNewNote, onDailyNote, onGraphClick,
|
onNoteClick, onNewNote, onDailyNote, onGraphClick,
|
||||||
pinnedIds, togglePin,
|
pinnedIds, togglePin,
|
||||||
|
standalone = true,
|
||||||
} = props;
|
} = props;
|
||||||
|
|
||||||
|
// When standalone=false, the surface sidebar owns search/filter/tag controls
|
||||||
|
const showFilters = standalone !== false;
|
||||||
|
|
||||||
const searchTimerRef = useRef(null);
|
const searchTimerRef = useRef(null);
|
||||||
const searchInputRef = useRef(null);
|
const searchInputRef = useRef(null);
|
||||||
|
|
||||||
@@ -81,13 +85,15 @@ export function NoteList(props) {
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
${showFilters && html`
|
||||||
<div class="sw-notes-pane__search-row">
|
<div class="sw-notes-pane__search-row">
|
||||||
<input class="sw-notes-pane__search-input" type="text"
|
<input class="sw-notes-pane__search-input" type="text"
|
||||||
placeholder="Search notes\u2026" autocomplete="off"
|
placeholder="Search notes\u2026" autocomplete="off"
|
||||||
ref=${searchInputRef} onInput=${onSearchInput}
|
ref=${searchInputRef} onInput=${onSearchInput}
|
||||||
value=${searchQuery} />
|
value=${searchQuery} />
|
||||||
</div>
|
</div>`}
|
||||||
|
|
||||||
|
${showFilters && html`
|
||||||
<div class="sw-notes-pane__filter-row">
|
<div class="sw-notes-pane__filter-row">
|
||||||
<select class="sw-notes-pane__filter-select"
|
<select class="sw-notes-pane__filter-select"
|
||||||
value=${folder} onChange=${e => { setFolder(e.target.value); }}>
|
value=${folder} onChange=${e => { setFolder(e.target.value); }}>
|
||||||
@@ -101,9 +107,9 @@ export function NoteList(props) {
|
|||||||
<option value="created_desc">Created</option>
|
<option value="created_desc">Created</option>
|
||||||
<option value="alpha">Alphabetical</option>
|
<option value="alpha">Alphabetical</option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>`}
|
||||||
|
|
||||||
${topTags.length > 0 && html`
|
${showFilters && topTags.length > 0 && html`
|
||||||
<div class="sw-notes-pane__tag-cloud">
|
<div class="sw-notes-pane__tag-cloud">
|
||||||
${topTags.map(([tag, count]) => html`
|
${topTags.map(([tag, count]) => html`
|
||||||
<span key=${tag}
|
<span key=${tag}
|
||||||
|
|||||||
@@ -8,9 +8,12 @@ const { useState } = hooks;
|
|||||||
function initials(name) {
|
function initials(name) {
|
||||||
if (!name) return '?';
|
if (!name) return '?';
|
||||||
const parts = name.trim().split(/\s+/);
|
const parts = name.trim().split(/\s+/);
|
||||||
return parts.length > 1
|
if (parts.length > 1) {
|
||||||
? (parts[0][0] + parts[parts.length - 1][0]).toUpperCase()
|
// Multi-word: first + last initials ("John Doe" → "JD")
|
||||||
: name.slice(0, 2).toUpperCase();
|
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' }) {
|
export function Avatar({ src, name = '', size = 'md' }) {
|
||||||
|
|||||||
90
src/js/sw/surfaces/notes/index.js
Normal file
90
src/js/sw/surfaces/notes/index.js
Normal file
@@ -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`
|
||||||
|
<div class="sw-notes-surface">
|
||||||
|
${/* Mobile sidebar overlay */''}
|
||||||
|
${workspace.sidebarOpen && html`
|
||||||
|
<div class="sw-notes-surface__sidebar-overlay"
|
||||||
|
onClick=${_toggleSidebar} />`}
|
||||||
|
|
||||||
|
${/* Sidebar */''}
|
||||||
|
${workspace.sidebarOpen && html`
|
||||||
|
<${Sidebar}
|
||||||
|
sidebar=${sidebar}
|
||||||
|
onNewNote=${_onNewNote}
|
||||||
|
onCollapse=${_toggleSidebar}
|
||||||
|
onFolderSelect=${_onFolderSelect}
|
||||||
|
onTagSelect=${_onTagSelect}
|
||||||
|
onSearch=${_onSearch} />`}
|
||||||
|
|
||||||
|
${/* Main workspace */''}
|
||||||
|
<div class="sw-notes-surface__main">
|
||||||
|
<${NotesWorkspace}
|
||||||
|
sidebarOpen=${workspace.sidebarOpen}
|
||||||
|
onToggleSidebar=${_toggleSidebar}
|
||||||
|
initialNoteId=${workspace.activeNoteId}
|
||||||
|
onNoteChange=${_onNoteChange}
|
||||||
|
handleRef=${notesRef} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<${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 };
|
||||||
53
src/js/sw/surfaces/notes/notes-workspace.js
Normal file
53
src/js/sw/surfaces/notes/notes-workspace.js
Normal file
@@ -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`
|
||||||
|
<svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor"
|
||||||
|
stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||||
|
<rect x="3" y="3" width="18" height="18" rx="2"/>
|
||||||
|
<line x1="9" y1="3" x2="9" y2="21"/>
|
||||||
|
</svg>`;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @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`
|
||||||
|
<div class="sw-notes-surface__workspace">
|
||||||
|
<div class="sw-notes-surface__workspace-header">
|
||||||
|
<div class="sw-notes-surface__workspace-header-left">
|
||||||
|
${!sidebarOpen && html`
|
||||||
|
<button class="sw-notes-surface__sidebar-toggle"
|
||||||
|
onClick=${onToggleSidebar}
|
||||||
|
title="Show sidebar"
|
||||||
|
aria-label="Show sidebar">
|
||||||
|
${PANEL_SVG}
|
||||||
|
</button>`}
|
||||||
|
</div>
|
||||||
|
<div class="sw-notes-surface__workspace-header-right" />
|
||||||
|
</div>
|
||||||
|
<${NotesPane}
|
||||||
|
standalone=${false}
|
||||||
|
initialNoteId=${initialNoteId}
|
||||||
|
handleRef=${handleRef}
|
||||||
|
onNoteChange=${onNoteChange}
|
||||||
|
className="sw-notes-surface__notes-pane" />
|
||||||
|
</div>`;
|
||||||
|
}
|
||||||
71
src/js/sw/surfaces/notes/sidebar-folders.js
Normal file
71
src/js/sw/surfaces/notes/sidebar-folders.js
Normal file
@@ -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`
|
||||||
|
<svg width="10" height="10" viewBox="0 0 24 24" fill="none" stroke="currentColor"
|
||||||
|
stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
|
||||||
|
<polyline points="9 18 15 12 9 6"/>
|
||||||
|
</svg>`;
|
||||||
|
|
||||||
|
const FOLDER_SVG = html`
|
||||||
|
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor"
|
||||||
|
stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||||
|
<path d="M22 19a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h5l2 3h9a2 2 0 0 1 2 2z"/>
|
||||||
|
</svg>`;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @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`
|
||||||
|
<div class="sw-notes-surface__section">
|
||||||
|
<button class="sw-notes-surface__section-header"
|
||||||
|
onClick=${onToggle}
|
||||||
|
aria-expanded=${!collapsed}>
|
||||||
|
<span class=${'sw-notes-surface__chevron' + (collapsed ? '' : ' sw-notes-surface__chevron--open')}>
|
||||||
|
${CHEVRON_SVG}
|
||||||
|
</span>
|
||||||
|
<span class="sw-notes-surface__section-label">Folders</span>
|
||||||
|
${totalCount > 0 && html`
|
||||||
|
<span class="sw-notes-surface__section-count">${totalCount}</span>`}
|
||||||
|
</button>
|
||||||
|
${!collapsed && html`
|
||||||
|
<div class="sw-notes-surface__section-body" role="listbox">
|
||||||
|
${/* "All Notes" clears folder filter */''}
|
||||||
|
<button class=${'sw-notes-surface__item' + (!activeFolder ? ' sw-notes-surface__item--active' : '')}
|
||||||
|
onClick=${() => onSelect('')}
|
||||||
|
role="option"
|
||||||
|
aria-selected=${!activeFolder}>
|
||||||
|
<span class="sw-notes-surface__item-icon">${FOLDER_SVG}</span>
|
||||||
|
<span class="sw-notes-surface__item-title">All Notes</span>
|
||||||
|
</button>
|
||||||
|
${folders.map(f => html`
|
||||||
|
<button key=${f.path}
|
||||||
|
class=${'sw-notes-surface__item' + (activeFolder === f.path ? ' sw-notes-surface__item--active' : '')}
|
||||||
|
onClick=${() => onSelect(f.path)}
|
||||||
|
role="option"
|
||||||
|
aria-selected=${activeFolder === f.path}
|
||||||
|
title=${f.path}>
|
||||||
|
<span class="sw-notes-surface__item-icon">${FOLDER_SVG}</span>
|
||||||
|
<span class="sw-notes-surface__item-title">${f.path}</span>
|
||||||
|
${f.count > 0 && html`
|
||||||
|
<span class="sw-notes-surface__item-count">${f.count}</span>`}
|
||||||
|
</button>`)}
|
||||||
|
${folders.length === 0 && html`
|
||||||
|
<div class="sw-notes-surface__empty">No folders</div>`}
|
||||||
|
</div>`}
|
||||||
|
</div>`;
|
||||||
|
}
|
||||||
52
src/js/sw/surfaces/notes/sidebar-tags.js
Normal file
52
src/js/sw/surfaces/notes/sidebar-tags.js
Normal file
@@ -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`
|
||||||
|
<svg width="10" height="10" viewBox="0 0 24 24" fill="none" stroke="currentColor"
|
||||||
|
stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
|
||||||
|
<polyline points="9 18 15 12 9 6"/>
|
||||||
|
</svg>`;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @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`
|
||||||
|
<div class="sw-notes-surface__section">
|
||||||
|
<button class="sw-notes-surface__section-header"
|
||||||
|
onClick=${onToggle}
|
||||||
|
aria-expanded=${!collapsed}>
|
||||||
|
<span class=${'sw-notes-surface__chevron' + (collapsed ? '' : ' sw-notes-surface__chevron--open')}>
|
||||||
|
${CHEVRON_SVG}
|
||||||
|
</span>
|
||||||
|
<span class="sw-notes-surface__section-label">Tags</span>
|
||||||
|
${tags.length > 0 && html`
|
||||||
|
<span class="sw-notes-surface__section-count">${tags.length}</span>`}
|
||||||
|
</button>
|
||||||
|
${!collapsed && html`
|
||||||
|
<div class="sw-notes-surface__section-body">
|
||||||
|
<div class="sw-notes-surface__tag-cloud">
|
||||||
|
${tags.length === 0 && html`
|
||||||
|
<div class="sw-notes-surface__empty">No tags</div>`}
|
||||||
|
${tags.map(([tag, count]) => html`
|
||||||
|
<span key=${tag}
|
||||||
|
class=${'sw-notes-surface__tag-chip' + (activeTag === tag ? ' sw-notes-surface__tag-chip--active' : '')}
|
||||||
|
onClick=${() => onSelect(activeTag === tag ? '' : tag)}
|
||||||
|
title=${tag + ' (' + count + ')'}>
|
||||||
|
${tag}
|
||||||
|
</span>`)}
|
||||||
|
</div>
|
||||||
|
</div>`}
|
||||||
|
</div>`;
|
||||||
|
}
|
||||||
121
src/js/sw/surfaces/notes/sidebar.js
Normal file
121
src/js/sw/surfaces/notes/sidebar.js
Normal file
@@ -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`
|
||||||
|
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor"
|
||||||
|
stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||||
|
<line x1="12" y1="5" x2="12" y2="19"/>
|
||||||
|
<line x1="5" y1="12" x2="19" y2="12"/>
|
||||||
|
</svg>`;
|
||||||
|
|
||||||
|
const SEARCH_SVG = html`
|
||||||
|
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor"
|
||||||
|
stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||||
|
<circle cx="11" cy="11" r="8"/>
|
||||||
|
<line x1="21" y1="21" x2="16.65" y2="16.65"/>
|
||||||
|
</svg>`;
|
||||||
|
|
||||||
|
const COLLAPSE_SVG = html`
|
||||||
|
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor"
|
||||||
|
stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||||
|
<rect x="3" y="3" width="18" height="18" rx="2"/>
|
||||||
|
<line x1="9" y1="3" x2="9" y2="21"/>
|
||||||
|
</svg>`;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {{
|
||||||
|
* sidebar: ReturnType<import('./use-sidebar.js').useSidebar>,
|
||||||
|
* 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`
|
||||||
|
<div class="sw-notes-surface__sidebar" role="navigation" aria-label="Notes sidebar">
|
||||||
|
${/* Header: new note + collapse */''}
|
||||||
|
<div class="sw-notes-surface__sidebar-header">
|
||||||
|
<button class="sw-notes-surface__new-note-btn"
|
||||||
|
onClick=${onNewNote}
|
||||||
|
title="New note"
|
||||||
|
aria-label="Create new note">
|
||||||
|
${PLUS_SVG}
|
||||||
|
<span>New Note</span>
|
||||||
|
</button>
|
||||||
|
<button class="sw-notes-surface__collapse-btn"
|
||||||
|
onClick=${onCollapse}
|
||||||
|
title="Collapse sidebar"
|
||||||
|
aria-label="Collapse sidebar">
|
||||||
|
${COLLAPSE_SVG}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
${/* Search */''}
|
||||||
|
<div class="sw-notes-surface__search-wrap">
|
||||||
|
<span class="sw-notes-surface__search-icon">${SEARCH_SVG}</span>
|
||||||
|
<input class="sw-notes-surface__search"
|
||||||
|
type="text"
|
||||||
|
placeholder="Search notes\u2026"
|
||||||
|
value=${sidebar.search}
|
||||||
|
onInput=${_onSearchInput}
|
||||||
|
aria-label="Search notes" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
${/* Scrollable content */''}
|
||||||
|
<div class="sw-notes-surface__sidebar-body">
|
||||||
|
<${SidebarFolders}
|
||||||
|
folders=${sidebar.folders}
|
||||||
|
activeFolder=${sidebar.activeFolder}
|
||||||
|
collapsed=${sidebar.collapsed.folders}
|
||||||
|
onToggle=${() => sidebar.toggleCollapsed('folders')}
|
||||||
|
onSelect=${_onFolderSelect} />
|
||||||
|
|
||||||
|
<${SidebarTags}
|
||||||
|
tags=${sidebar.tags}
|
||||||
|
activeTag=${sidebar.activeTag}
|
||||||
|
collapsed=${sidebar.collapsed.tags}
|
||||||
|
onToggle=${() => sidebar.toggleCollapsed('tags')}
|
||||||
|
onSelect=${_onTagSelect} />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
${/* Footer: UserMenu only */''}
|
||||||
|
<div class="sw-notes-surface__sidebar-footer">
|
||||||
|
<${UserMenu} placement="up-right" />
|
||||||
|
</div>
|
||||||
|
</div>`;
|
||||||
|
}
|
||||||
122
src/js/sw/surfaces/notes/use-sidebar.js
Normal file
122
src/js/sw/surfaces/notes/use-sidebar.js
Normal file
@@ -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 {}; }
|
||||||
|
}
|
||||||
59
src/js/sw/surfaces/notes/use-workspace.js
Normal file
59
src/js/sw/surfaces/notes/use-workspace.js
Normal file
@@ -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; }
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user