chore: slash-command refinements, server import fixes, misc cleanup
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
f86c6234af
commit
06f7d67cd3
|
|
@ -0,0 +1,38 @@
|
||||||
|
---
|
||||||
|
id: TASK-82
|
||||||
|
title: Sankey-proportional edges + node satisfaction bars in rFunds diagram
|
||||||
|
status: Done
|
||||||
|
assignee: []
|
||||||
|
created_date: '2026-03-03 05:32'
|
||||||
|
labels:
|
||||||
|
- rfunds
|
||||||
|
- visualization
|
||||||
|
dependencies: []
|
||||||
|
references:
|
||||||
|
- modules/rfunds/components/folk-funds-app.ts
|
||||||
|
- modules/rfunds/components/funds.css
|
||||||
|
- modules/rfunds/lib/simulation.ts
|
||||||
|
priority: medium
|
||||||
|
---
|
||||||
|
|
||||||
|
## Description
|
||||||
|
|
||||||
|
<!-- SECTION:DESCRIPTION:BEGIN -->
|
||||||
|
Edge widths now reflect actual dollar flow (source rates, overflow excess, spending drain) instead of just allocation percentages. Zero-flow paths render as ghost edges. Edge labels show dollar amounts alongside percentages. Funnel nodes display an inflow satisfaction bar. Outcome progress bars enhanced with dollar labels.
|
||||||
|
<!-- SECTION:DESCRIPTION:END -->
|
||||||
|
|
||||||
|
## Acceptance Criteria
|
||||||
|
<!-- AC:BEGIN -->
|
||||||
|
- [ ] #1 Edge widths proportional to actual dollar flow per edge
|
||||||
|
- [ ] #2 Ghost edges (dashed, low opacity) for zero-flow paths
|
||||||
|
- [ ] #3 Edge labels show dollar amounts: $2.5k (50%)
|
||||||
|
- [ ] #4 Funnel nodes show inflow satisfaction bar (green = received, grey = gap)
|
||||||
|
- [ ] #5 Outcome nodes have enhanced 8px progress bars with dollar labels
|
||||||
|
- [ ] #6 Clean tsc and vite build with no errors
|
||||||
|
<!-- AC:END -->
|
||||||
|
|
||||||
|
## Final Summary
|
||||||
|
|
||||||
|
<!-- SECTION:FINAL_SUMMARY:BEGIN -->
|
||||||
|
Implemented in commit e644797 on dev branch. Modified `folk-funds-app.ts` (renderAllEdges two-pass with EdgeInfo, computeInflowSatisfaction, renderFunnelNodeSvg/renderOutcomeNodeSvg updated) and `funds.css` (ghost edge + satisfaction bar styles). Deployed to production.
|
||||||
|
<!-- SECTION:FINAL_SUMMARY:END -->
|
||||||
|
|
@ -0,0 +1,29 @@
|
||||||
|
---
|
||||||
|
id: TASK-83
|
||||||
|
title: Fix tab-cache inline style extraction for canvas toolbar
|
||||||
|
status: Done
|
||||||
|
assignee: []
|
||||||
|
created_date: '2026-03-03 07:42'
|
||||||
|
labels:
|
||||||
|
- bugfix
|
||||||
|
- canvas
|
||||||
|
- tab-cache
|
||||||
|
dependencies: []
|
||||||
|
references:
|
||||||
|
- shared/tab-cache.ts
|
||||||
|
priority: high
|
||||||
|
---
|
||||||
|
|
||||||
|
## Description
|
||||||
|
|
||||||
|
<!-- SECTION:DESCRIPTION:BEGIN -->
|
||||||
|
The tab-cache system (shared/tab-cache.ts) only extracted <link rel="stylesheet"> tags when switching tabs, missing inline <style> blocks. The canvas toolbar CSS is entirely in inline <style> blocks in canvas.html, causing unstyled toolbar when switching to rSpace via tab cache.
|
||||||
|
|
||||||
|
Fixed extractContent() to also collect inline <style> blocks from fetched page heads, and loadAssets() to inject them with data-tab-style attributes for deduplication.
|
||||||
|
<!-- SECTION:DESCRIPTION:END -->
|
||||||
|
|
||||||
|
## Final Summary
|
||||||
|
|
||||||
|
<!-- SECTION:FINAL_SUMMARY:BEGIN -->
|
||||||
|
Added inline <style> block extraction to tab-cache's extractContent() and injection in loadAssets(). Toolbar now renders correctly when switching to rSpace tab via client-side navigation. Committed as 4819852.
|
||||||
|
<!-- SECTION:FINAL_SUMMARY:END -->
|
||||||
|
|
@ -0,0 +1,30 @@
|
||||||
|
---
|
||||||
|
id: TASK-84
|
||||||
|
title: Fix shape x/y/size preservation on canvas reload
|
||||||
|
status: Done
|
||||||
|
assignee: []
|
||||||
|
created_date: '2026-03-03 07:42'
|
||||||
|
labels:
|
||||||
|
- bugfix
|
||||||
|
- canvas
|
||||||
|
- shapes
|
||||||
|
dependencies: []
|
||||||
|
references:
|
||||||
|
- lib/folk-shape.ts
|
||||||
|
- website/canvas.html
|
||||||
|
priority: high
|
||||||
|
---
|
||||||
|
|
||||||
|
## Description
|
||||||
|
|
||||||
|
<!-- SECTION:DESCRIPTION:BEGIN -->
|
||||||
|
All canvas shapes stacked at (0,0) on page reload. Root cause: FolkShape.createRenderRoot() unconditionally read x/y/width/height from HTML attributes, but canvas.html sets these as JS properties before DOM insertion. Since attributes don't exist, everything defaulted to 0/auto.
|
||||||
|
|
||||||
|
Fixed to only read from attributes when they actually exist (getAttribute !== null). Also fixed eraser: hardDeleteShape() was only in a click handler that never fired because pointerdown already removed the target element — moved Automerge deletion into pointerdown.
|
||||||
|
<!-- SECTION:DESCRIPTION:END -->
|
||||||
|
|
||||||
|
## Final Summary
|
||||||
|
|
||||||
|
<!-- SECTION:FINAL_SUMMARY:BEGIN -->
|
||||||
|
Fixed createRenderRoot() to conditionally read from HTML attributes only when present. Fixed eraser to persist deletions to Automerge in pointerdown handler instead of unreachable click handler. Committed as 4f9b036.
|
||||||
|
<!-- SECTION:FINAL_SUMMARY:END -->
|
||||||
|
|
@ -0,0 +1,36 @@
|
||||||
|
---
|
||||||
|
id: TASK-85
|
||||||
|
title: 'Fix folk-wrapper crash, service worker API exclusion, fal.ai image-gen'
|
||||||
|
status: Done
|
||||||
|
assignee: []
|
||||||
|
created_date: '2026-03-03 07:43'
|
||||||
|
labels:
|
||||||
|
- bugfix
|
||||||
|
- canvas
|
||||||
|
- service-worker
|
||||||
|
- ai
|
||||||
|
dependencies: []
|
||||||
|
references:
|
||||||
|
- lib/folk-wrapper.ts
|
||||||
|
- website/sw.ts
|
||||||
|
- server/index.ts
|
||||||
|
priority: high
|
||||||
|
---
|
||||||
|
|
||||||
|
## Description
|
||||||
|
|
||||||
|
<!-- SECTION:DESCRIPTION:BEGIN -->
|
||||||
|
Three fixes:
|
||||||
|
|
||||||
|
1. folk-wrapper.ts createRenderRoot() crashed with "Cannot read properties of null (reading 'appendChild')" — innerHTML="" removed the slot from DOM, making parentElement null on the next line. Fixed by saving parent ref before clearing.
|
||||||
|
|
||||||
|
2. Service worker (sw.ts) only excluded /api/ at root path, not module API paths like /jeff/rcal/api/events. These got cached and when network failed, catch handler returned undefined instead of a Response. Fixed with includes("/api/") and proper fallback Response.
|
||||||
|
|
||||||
|
3. Image generation returned 502 "No image returned" — queue.fal.run is async (returns request_id), not the actual image. Changed to synchronous fal.run endpoint for all three fal.ai endpoints (image-gen, t2v, i2v).
|
||||||
|
<!-- SECTION:DESCRIPTION:END -->
|
||||||
|
|
||||||
|
## Final Summary
|
||||||
|
|
||||||
|
<!-- SECTION:FINAL_SUMMARY:BEGIN -->
|
||||||
|
Fixed folk-wrapper parentElement null crash, service worker module API path exclusion with proper offline fallback, and fal.ai endpoint from queue.fal.run to fal.run for synchronous image/video generation. Committed as fef419f.
|
||||||
|
<!-- SECTION:FINAL_SUMMARY:END -->
|
||||||
|
|
@ -0,0 +1,43 @@
|
||||||
|
---
|
||||||
|
id: TASK-86
|
||||||
|
title: Encrypted server-side account vault for EncryptID
|
||||||
|
status: Done
|
||||||
|
assignee: []
|
||||||
|
created_date: '2026-03-03 19:15'
|
||||||
|
updated_date: '2026-03-03 19:15'
|
||||||
|
labels:
|
||||||
|
- encryptid
|
||||||
|
- security
|
||||||
|
- feature
|
||||||
|
dependencies: []
|
||||||
|
references:
|
||||||
|
- src/encryptid/vault.ts
|
||||||
|
- src/encryptid/server.ts
|
||||||
|
- shared/local-first/crypto.ts
|
||||||
|
- server/local-first/backup-routes.ts
|
||||||
|
priority: high
|
||||||
|
---
|
||||||
|
|
||||||
|
## Description
|
||||||
|
|
||||||
|
<!-- SECTION:DESCRIPTION:BEGIN -->
|
||||||
|
Zero-knowledge vault stores all EncryptID account data (profile, emails, devices, addresses, wallets, preferences) as a single AES-256-GCM encrypted JSON blob via the backup API. Key derived deterministically from WebAuthn PRF via HKDF — same passkey = same key on any device. Server never sees plaintext.
|
||||||
|
<!-- SECTION:DESCRIPTION:END -->
|
||||||
|
|
||||||
|
## Acceptance Criteria
|
||||||
|
<!-- AC:BEGIN -->
|
||||||
|
- [x] #1 VaultManager class with AccountVault interface, DocCrypto encryption, backup API storage, localStorage cache
|
||||||
|
- [x] #2 Vault auto-loads on passkey auth (handleLogin + conditionalUI), clears on logout
|
||||||
|
- [x] #3 Dashboard UI: checklist item, vault section with Save/Restore buttons, status display
|
||||||
|
- [x] #4 Save triggers passkey re-auth → AES-256-GCM encrypt → PUT /api/backup/__vault/account-vault
|
||||||
|
- [x] #5 Restore triggers passkey re-auth → GET → decrypt → populate DOM
|
||||||
|
- [x] #6 checkVaultStatus() on profile load updates checklist green check
|
||||||
|
- [x] #7 No new server routes or DB tables — uses existing backup API
|
||||||
|
- [x] #8 tsc --noEmit and vite build pass clean
|
||||||
|
<!-- AC:END -->
|
||||||
|
|
||||||
|
## Final Summary
|
||||||
|
|
||||||
|
<!-- SECTION:FINAL_SUMMARY:BEGIN -->
|
||||||
|
## Files Created\n- `src/encryptid/vault.ts` — VaultManager class, AccountVault interface, singleton pattern\n\n## Files Modified\n- `src/encryptid/index.ts` — Export vault types and functions\n- `src/encryptid/ui/login-button.ts` — Load vault after auth, clear on logout\n- `src/encryptid/server.ts` — Dashboard vault section, checklist item, inline crypto functions (deriveVaultKey, saveVault, restoreVault, checkVaultStatus)\n\n## Key Design\n- Vault key: `Master PRF → HKDF("rspace:__vault") → HKDF("doc:account-vault") → AES-256-GCM`\n- Dashboard uses inline WebCrypto (not VaultManager import) since dashboard auth doesn't initialize DocCrypto\n- Save/restore require biometric re-auth for security\n\nCommit: e2e12af, deployed to production.
|
||||||
|
<!-- SECTION:FINAL_SUMMARY:END -->
|
||||||
|
|
@ -0,0 +1,30 @@
|
||||||
|
---
|
||||||
|
id: TASK-HIGH.4
|
||||||
|
title: Fix Automerge proxy re-assignment error on canvas load
|
||||||
|
status: Done
|
||||||
|
assignee: []
|
||||||
|
created_date: '2026-03-03 19:17'
|
||||||
|
updated_date: '2026-03-03 19:18'
|
||||||
|
labels: []
|
||||||
|
dependencies: []
|
||||||
|
parent_task_id: TASK-HIGH
|
||||||
|
---
|
||||||
|
|
||||||
|
## Description
|
||||||
|
|
||||||
|
<!-- SECTION:DESCRIPTION:BEGIN -->
|
||||||
|
Fix 'Cannot create a reference to an existing document object' RangeError that fires twice on canvas load (initFromCache + WS sync). Root cause: Automerge proxy objects from doc.shapes passed through DOM elements back into Automerge.change(). Fixed by deep-cloning in #shapeToData() and #updateShapeInDoc().
|
||||||
|
<!-- SECTION:DESCRIPTION:END -->
|
||||||
|
|
||||||
|
## Acceptance Criteria
|
||||||
|
<!-- AC:BEGIN -->
|
||||||
|
- [x] #1 No RangeError in console on canvas hard-refresh
|
||||||
|
- [x] #2 Shapes persist correctly after move/resize
|
||||||
|
- [x] #3 Cross-tab sync works without errors
|
||||||
|
<!-- AC:END -->
|
||||||
|
|
||||||
|
## Implementation Notes
|
||||||
|
|
||||||
|
<!-- SECTION:NOTES:BEGIN -->
|
||||||
|
Fixed in commit 023a883. Deep-cloned shape data in #shapeToData() and #updateShapeInDoc() to break Automerge proxy chain. Deployed to production.
|
||||||
|
<!-- SECTION:NOTES:END -->
|
||||||
|
|
@ -9,6 +9,21 @@ import { Plugin, PluginKey } from '@tiptap/pm/state';
|
||||||
import type { EditorView } from '@tiptap/pm/view';
|
import type { EditorView } from '@tiptap/pm/view';
|
||||||
import type { Editor } from '@tiptap/core';
|
import type { Editor } from '@tiptap/core';
|
||||||
|
|
||||||
|
/** Inline SVG icons for slash menu items (16×16, stroke-based, currentColor) */
|
||||||
|
const SLASH_ICONS: Record<string, string> = {
|
||||||
|
text: '<svg width="16" height="16" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"><line x1="4" y1="3" x2="12" y2="3"/><line x1="8" y1="3" x2="8" y2="13"/><line x1="6" y1="13" x2="10" y2="13"/></svg>',
|
||||||
|
heading1: '<svg width="16" height="16" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"><path d="M2 3v10M8 3v10M2 8h6"/><text x="10.5" y="13" font-size="7" fill="currentColor" stroke="none" font-family="system-ui" font-weight="700">1</text></svg>',
|
||||||
|
heading2: '<svg width="16" height="16" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"><path d="M2 3v10M8 3v10M2 8h6"/><text x="10.5" y="13" font-size="7" fill="currentColor" stroke="none" font-family="system-ui" font-weight="700">2</text></svg>',
|
||||||
|
heading3: '<svg width="16" height="16" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"><path d="M2 3v10M8 3v10M2 8h6"/><text x="10.5" y="13" font-size="7" fill="currentColor" stroke="none" font-family="system-ui" font-weight="700">3</text></svg>',
|
||||||
|
bulletList: '<svg width="16" height="16" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"><line x1="6" y1="4" x2="14" y2="4"/><line x1="6" y1="8" x2="14" y2="8"/><line x1="6" y1="12" x2="14" y2="12"/><circle cx="3" cy="4" r="1" fill="currentColor" stroke="none"/><circle cx="3" cy="8" r="1" fill="currentColor" stroke="none"/><circle cx="3" cy="12" r="1" fill="currentColor" stroke="none"/></svg>',
|
||||||
|
orderedList: '<svg width="16" height="16" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"><line x1="7" y1="4" x2="14" y2="4"/><line x1="7" y1="8" x2="14" y2="8"/><line x1="7" y1="12" x2="14" y2="12"/><text x="1.5" y="5.5" font-size="5" fill="currentColor" stroke="none" font-family="system-ui" font-weight="600">1</text><text x="1.5" y="9.5" font-size="5" fill="currentColor" stroke="none" font-family="system-ui" font-weight="600">2</text><text x="1.5" y="13.5" font-size="5" fill="currentColor" stroke="none" font-family="system-ui" font-weight="600">3</text></svg>',
|
||||||
|
taskList: '<svg width="16" height="16" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><rect x="2" y="2" width="5" height="5" rx="1"/><polyline points="3.5 4.5 4.5 5.5 6 3.5"/><line x1="9" y1="4.5" x2="14" y2="4.5"/><rect x="2" y="9" width="5" height="5" rx="1"/><line x1="9" y1="11.5" x2="14" y2="11.5"/></svg>',
|
||||||
|
codeBlock: '<svg width="16" height="16" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><rect x="1.5" y="1.5" width="13" height="13" rx="2"/><polyline points="5 6 3.5 8 5 10"/><polyline points="11 6 12.5 8 11 10"/><line x1="9" y1="5" x2="7" y2="11"/></svg>',
|
||||||
|
blockquote: '<svg width="16" height="16" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="3" y1="2" x2="3" y2="14"/><line x1="7" y1="4" x2="14" y2="4"/><line x1="7" y1="8" x2="14" y2="8"/><line x1="7" y1="12" x2="12" y2="12"/></svg>',
|
||||||
|
horizontalRule: '<svg width="16" height="16" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"><line x1="2" y1="8" x2="14" y2="8"/><circle cx="4" cy="8" r="0.5" fill="currentColor"/><circle cx="8" cy="8" r="0.5" fill="currentColor"/><circle cx="12" cy="8" r="0.5" fill="currentColor"/></svg>',
|
||||||
|
image: '<svg width="16" height="16" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><rect x="1.5" y="2.5" width="13" height="11" rx="2"/><circle cx="5.5" cy="6" r="1.5"/><path d="M14.5 10.5l-3.5-3.5-5 5"/></svg>',
|
||||||
|
};
|
||||||
|
|
||||||
export interface SlashMenuItem {
|
export interface SlashMenuItem {
|
||||||
title: string;
|
title: string;
|
||||||
icon: string;
|
icon: string;
|
||||||
|
|
@ -19,71 +34,72 @@ export interface SlashMenuItem {
|
||||||
export const SLASH_ITEMS: SlashMenuItem[] = [
|
export const SLASH_ITEMS: SlashMenuItem[] = [
|
||||||
{
|
{
|
||||||
title: 'Text',
|
title: 'Text',
|
||||||
icon: 'Aa',
|
icon: 'text',
|
||||||
description: 'Plain paragraph text',
|
description: 'Plain paragraph text',
|
||||||
command: (e) => e.chain().focus().setParagraph().run(),
|
command: (e) => e.chain().focus().setParagraph().run(),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: 'Heading 1',
|
title: 'Heading 1',
|
||||||
icon: 'H1',
|
icon: 'heading1',
|
||||||
description: 'Large section heading',
|
description: 'Large section heading',
|
||||||
command: (e) => e.chain().focus().setHeading({ level: 1 }).run(),
|
command: (e) => e.chain().focus().setHeading({ level: 1 }).run(),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: 'Heading 2',
|
title: 'Heading 2',
|
||||||
icon: 'H2',
|
icon: 'heading2',
|
||||||
description: 'Medium section heading',
|
description: 'Medium section heading',
|
||||||
command: (e) => e.chain().focus().setHeading({ level: 2 }).run(),
|
command: (e) => e.chain().focus().setHeading({ level: 2 }).run(),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: 'Heading 3',
|
title: 'Heading 3',
|
||||||
icon: 'H3',
|
icon: 'heading3',
|
||||||
description: 'Small section heading',
|
description: 'Small section heading',
|
||||||
command: (e) => e.chain().focus().setHeading({ level: 3 }).run(),
|
command: (e) => e.chain().focus().setHeading({ level: 3 }).run(),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: 'Bullet List',
|
title: 'Bullet List',
|
||||||
icon: '•',
|
icon: 'bulletList',
|
||||||
description: 'Unordered bullet list',
|
description: 'Unordered bullet list',
|
||||||
command: (e) => e.chain().focus().toggleBulletList().run(),
|
command: (e) => e.chain().focus().toggleBulletList().run(),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: 'Numbered List',
|
title: 'Numbered List',
|
||||||
icon: '1.',
|
icon: 'orderedList',
|
||||||
description: 'Ordered numbered list',
|
description: 'Ordered numbered list',
|
||||||
command: (e) => e.chain().focus().toggleOrderedList().run(),
|
command: (e) => e.chain().focus().toggleOrderedList().run(),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: 'Task List',
|
title: 'Task List',
|
||||||
icon: '☑',
|
icon: 'taskList',
|
||||||
description: 'Checklist with checkboxes',
|
description: 'Checklist with checkboxes',
|
||||||
command: (e) => e.chain().focus().toggleTaskList().run(),
|
command: (e) => e.chain().focus().toggleTaskList().run(),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: 'Code Block',
|
title: 'Code Block',
|
||||||
icon: '</>',
|
icon: 'codeBlock',
|
||||||
description: 'Syntax-highlighted code block',
|
description: 'Syntax-highlighted code block',
|
||||||
command: (e) => e.chain().focus().toggleCodeBlock().run(),
|
command: (e) => e.chain().focus().toggleCodeBlock().run(),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: 'Blockquote',
|
title: 'Blockquote',
|
||||||
icon: '“',
|
icon: 'blockquote',
|
||||||
description: 'Indented quote block',
|
description: 'Indented quote block',
|
||||||
command: (e) => e.chain().focus().toggleBlockquote().run(),
|
command: (e) => e.chain().focus().toggleBlockquote().run(),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: 'Horizontal Rule',
|
title: 'Horizontal Rule',
|
||||||
icon: '—',
|
icon: 'horizontalRule',
|
||||||
description: 'Visual divider line',
|
description: 'Visual divider line',
|
||||||
command: (e) => e.chain().focus().setHorizontalRule().run(),
|
command: (e) => e.chain().focus().setHorizontalRule().run(),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: 'Image',
|
title: 'Image',
|
||||||
icon: '📷',
|
icon: 'image',
|
||||||
description: 'Insert an image from URL',
|
description: 'Insert an image from URL',
|
||||||
command: (e) => {
|
command: (e) => {
|
||||||
const url = prompt('Image URL:');
|
// Dispatch custom event for parent to show URL popover
|
||||||
if (url) e.chain().focus().setImage({ src: url }).run();
|
const event = new CustomEvent('slash-insert-image', { bubbles: true, composed: true });
|
||||||
|
(e.view.dom as HTMLElement).dispatchEvent(event);
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
@ -122,15 +138,17 @@ export function createSlashCommandPlugin(editor: Editor, shadowRoot: ShadowRoot)
|
||||||
|
|
||||||
function updateMenuContent() {
|
function updateMenuContent() {
|
||||||
if (!menuEl) return;
|
if (!menuEl) return;
|
||||||
menuEl.innerHTML = filteredItems
|
menuEl.innerHTML = `<div class="slash-menu__header">Insert block</div>` +
|
||||||
|
filteredItems
|
||||||
.map(
|
.map(
|
||||||
(item, i) =>
|
(item, i) =>
|
||||||
`<div class="slash-menu-item${i === selectedIndex ? ' selected' : ''}" data-index="${i}">
|
`<div class="slash-menu-item${i === selectedIndex ? ' selected' : ''}" data-index="${i}">
|
||||||
<span class="slash-menu-icon">${item.icon}</span>
|
<span class="slash-menu-icon">${SLASH_ICONS[item.icon] || item.icon}</span>
|
||||||
<div class="slash-menu-text">
|
<div class="slash-menu-text">
|
||||||
<div class="slash-menu-title">${item.title}</div>
|
<div class="slash-menu-title">${item.title}</div>
|
||||||
<div class="slash-menu-desc">${item.description}</div>
|
<div class="slash-menu-desc">${item.description}</div>
|
||||||
</div>
|
</div>
|
||||||
|
${i === selectedIndex ? '<span class="slash-menu-hint">Enter</span>' : ''}
|
||||||
</div>`,
|
</div>`,
|
||||||
)
|
)
|
||||||
.join('');
|
.join('');
|
||||||
|
|
|
||||||
|
|
@ -329,7 +329,7 @@ async function getSpaceConfig(slug: string): Promise<SpaceAuthConfig | null> {
|
||||||
if (!doc) return null;
|
if (!doc) return null;
|
||||||
return {
|
return {
|
||||||
spaceSlug: slug,
|
spaceSlug: slug,
|
||||||
visibility: (doc.meta.visibility || "public_read") as SpaceVisibility,
|
visibility: (doc.meta.visibility || "public") as SpaceVisibility,
|
||||||
ownerDID: doc.meta.ownerDID || undefined,
|
ownerDID: doc.meta.ownerDID || undefined,
|
||||||
app: "rspace",
|
app: "rspace",
|
||||||
};
|
};
|
||||||
|
|
@ -352,9 +352,9 @@ app.post("/api/communities", async (c) => {
|
||||||
}
|
}
|
||||||
|
|
||||||
const body = await c.req.json<{ name?: string; slug?: string; visibility?: SpaceVisibility }>();
|
const body = await c.req.json<{ name?: string; slug?: string; visibility?: SpaceVisibility }>();
|
||||||
const { name, slug, visibility = "public_read" } = body;
|
const { name, slug, visibility = "public" } = body;
|
||||||
|
|
||||||
const validVisibilities: SpaceVisibility[] = ["public", "public_read", "authenticated", "members_only"];
|
const validVisibilities: SpaceVisibility[] = ["public", "permissioned", "private"];
|
||||||
if (visibility && !validVisibilities.includes(visibility)) return c.json({ error: "Invalid visibility" }, 400);
|
if (visibility && !validVisibilities.includes(visibility)) return c.json({ error: "Invalid visibility" }, 400);
|
||||||
|
|
||||||
const result = await createSpace({
|
const result = await createSpace({
|
||||||
|
|
@ -381,7 +381,7 @@ app.post("/api/internal/provision", async (c) => {
|
||||||
name: space.charAt(0).toUpperCase() + space.slice(1),
|
name: space.charAt(0).toUpperCase() + space.slice(1),
|
||||||
slug: space,
|
slug: space,
|
||||||
ownerDID: `did:system:${space}`,
|
ownerDID: `did:system:${space}`,
|
||||||
visibility: body.public ? "public" : "public_read",
|
visibility: "public",
|
||||||
source: 'internal',
|
source: 'internal',
|
||||||
});
|
});
|
||||||
if (!result.ok) return c.json({ error: result.error }, result.status);
|
if (!result.ok) return c.json({ error: result.error }, result.status);
|
||||||
|
|
@ -1147,7 +1147,7 @@ app.post("/api/spaces/auto-provision", async (c) => {
|
||||||
name: `${claims.username}'s Space`,
|
name: `${claims.username}'s Space`,
|
||||||
slug: username,
|
slug: username,
|
||||||
ownerDID: claims.sub,
|
ownerDID: claims.sub,
|
||||||
visibility: "members_only",
|
visibility: "private",
|
||||||
source: 'auto-provision',
|
source: 'auto-provision',
|
||||||
});
|
});
|
||||||
if (!result.ok) return c.json({ error: result.error }, result.status);
|
if (!result.ok) return c.json({ error: result.error }, result.status);
|
||||||
|
|
@ -1156,9 +1156,9 @@ app.post("/api/spaces/auto-provision", async (c) => {
|
||||||
});
|
});
|
||||||
|
|
||||||
// ── Inject space visibility into HTML responses ──
|
// ── Inject space visibility into HTML responses ──
|
||||||
// Replaces the default data-space-visibility="public_read" rendered by renderShell
|
// Replaces the default data-space-visibility="public" rendered by renderShell
|
||||||
// with the actual visibility from the space config, so the client-side access gate
|
// with the actual visibility from the space config, so the client-side access gate
|
||||||
// can block content for members_only spaces when no session exists.
|
// can block content for private spaces when no session exists.
|
||||||
app.use("/:space/*", async (c, next) => {
|
app.use("/:space/*", async (c, next) => {
|
||||||
await next();
|
await next();
|
||||||
const ct = c.res.headers.get("content-type");
|
const ct = c.res.headers.get("content-type");
|
||||||
|
|
|
||||||
|
|
@ -58,7 +58,7 @@ export function renderShell(opts: ShellOptions): string {
|
||||||
modules,
|
modules,
|
||||||
theme = "dark",
|
theme = "dark",
|
||||||
head = "",
|
head = "",
|
||||||
spaceVisibility = "public_read",
|
spaceVisibility = "public",
|
||||||
} = opts;
|
} = opts;
|
||||||
|
|
||||||
// Auto-populate from space data when not explicitly provided
|
// Auto-populate from space data when not explicitly provided
|
||||||
|
|
@ -201,10 +201,10 @@ export function renderShell(opts: ShellOptions): string {
|
||||||
};
|
};
|
||||||
|
|
||||||
// ── Private space access gate ──
|
// ── Private space access gate ──
|
||||||
// If the space is members_only and no session exists, show a sign-in gate
|
// If the space is private and no session exists, show a sign-in gate
|
||||||
(function() {
|
(function() {
|
||||||
var vis = document.body.getAttribute('data-space-visibility');
|
var vis = document.body.getAttribute('data-space-visibility');
|
||||||
if (vis !== 'members_only') return;
|
if (vis !== 'private') return;
|
||||||
try {
|
try {
|
||||||
var raw = localStorage.getItem('encryptid_session');
|
var raw = localStorage.getItem('encryptid_session');
|
||||||
if (raw) {
|
if (raw) {
|
||||||
|
|
|
||||||
|
|
@ -623,6 +623,7 @@ spaces.patch("/:slug", async (c) => {
|
||||||
|
|
||||||
if (body.visibility) {
|
if (body.visibility) {
|
||||||
const valid: SpaceVisibility[] = ["public", "permissioned", "private"];
|
const valid: SpaceVisibility[] = ["public", "permissioned", "private"];
|
||||||
|
// Note: the create endpoint (line ~276) already validates correctly
|
||||||
if (!valid.includes(body.visibility)) {
|
if (!valid.includes(body.visibility)) {
|
||||||
return c.json({ error: `Invalid visibility. Must be one of: ${valid.join(", ")}` }, 400);
|
return c.json({ error: `Invalid visibility. Must be one of: ${valid.join(", ")}` }, 400);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -423,7 +423,7 @@ app.post('/api/register/complete', async (c) => {
|
||||||
|
|
||||||
const spaceSlug = username.toLowerCase().replace(/[^a-z0-9-]/g, '-');
|
const spaceSlug = username.toLowerCase().replace(/[^a-z0-9-]/g, '-');
|
||||||
if (!await communityExists(spaceSlug)) {
|
if (!await communityExists(spaceSlug)) {
|
||||||
await createCommunity(username, spaceSlug, did, 'members_only', {
|
await createCommunity(username, spaceSlug, did, 'private', {
|
||||||
enabledModules: DEFAULT_USER_MODULES,
|
enabledModules: DEFAULT_USER_MODULES,
|
||||||
nestPolicy: DEFAULT_USER_NEST_POLICY,
|
nestPolicy: DEFAULT_USER_NEST_POLICY,
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -1640,6 +1640,7 @@
|
||||||
<button id="new-choice-vote" title="New Poll">☑ Poll</button>
|
<button id="new-choice-vote" title="New Poll">☑ Poll</button>
|
||||||
<button id="new-choice-rank" title="New Ranking">📊 Ranking</button>
|
<button id="new-choice-rank" title="New Ranking">📊 Ranking</button>
|
||||||
<button id="new-choice-spider" title="New Scoring">🕸 Scoring</button>
|
<button id="new-choice-spider" title="New Scoring">🕸 Scoring</button>
|
||||||
|
<button id="new-conviction" title="Conviction Ranking">⏳ Conviction</button>
|
||||||
<button id="new-token" title="New Token">🪙 Token</button>
|
<button id="new-token" title="New Token">🪙 Token</button>
|
||||||
<button id="embed-funds" title="Embed rFunds">🌊 rFunds</button>
|
<button id="embed-funds" title="Embed rFunds">🌊 rFunds</button>
|
||||||
<button id="embed-wallet" title="Embed rWallet">💰 rWallet</button>
|
<button id="embed-wallet" title="Embed rWallet">💰 rWallet</button>
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue