Canvas V2

This commit is contained in:
2026-10-04 15:58:27 +02:00
parent 95002a34ad
commit 06da56d0a0
7 changed files with 1008 additions and 333 deletions
+884 -239
View File
File diff suppressed because it is too large Load Diff
+3 -46
View File
@@ -13,61 +13,18 @@ navigation and canvas.js can save on pagehide. */}}
{{define "shell"}}
<div class="canvas-app" data-version="{{.Version}}"{{if .Error}} data-readonly{{end}}>
<div class="canvas-bar">
<span class="canvas-title truncate">{{.Title}}</span>
{{if not .Error}}
<span class="canvas-tools">
<button type="button" class="btn btn-tool" data-canvas="text" title="Add a note (or double-click the background)">TEXT</button>
<button type="button" class="btn btn-tool" data-canvas="file" title="Add a file or page">FILE</button>
<button type="button" class="btn btn-tool" data-canvas="link" title="Add a web link">LINK</button>
<button type="button" class="btn btn-tool" data-canvas="group" title="Group the selection">GROUP</button>
<span class="toolbar-sep"></span>
<span class="dropdown">
<button type="button" class="btn btn-tool dropdown-toggle" title="Color of the selection">COLOR▾</button>
<div class="dropdown-menu canvas-colors">
<button type="button" class="btn btn-tool btn-block" data-canvas="color" data-value="">None</button>
<button type="button" class="btn btn-tool btn-block" data-canvas="color" data-value="1"><span class="canvas-swatch" data-color="1"></span>Red</button>
<button type="button" class="btn btn-tool btn-block" data-canvas="color" data-value="2"><span class="canvas-swatch" data-color="2"></span>Orange</button>
<button type="button" class="btn btn-tool btn-block" data-canvas="color" data-value="3"><span class="canvas-swatch" data-color="3"></span>Yellow</button>
<button type="button" class="btn btn-tool btn-block" data-canvas="color" data-value="4"><span class="canvas-swatch" data-color="4"></span>Green</button>
<button type="button" class="btn btn-tool btn-block" data-canvas="color" data-value="5"><span class="canvas-swatch" data-color="5"></span>Cyan</button>
<button type="button" class="btn btn-tool btn-block" data-canvas="color" data-value="6"><span class="canvas-swatch" data-color="6"></span>Purple</button>
</div>
</span>
<input type="color" class="canvas-color-input" title="Custom color of the selection" value="#c48401" />
<span class="dropdown">
<button type="button" class="btn btn-tool dropdown-toggle" title="Arrows of the selected connections">ARROWS▾</button>
<div class="dropdown-menu">
<button type="button" class="btn btn-tool btn-block" data-canvas="ends" data-value="none arrow">One way →</button>
<button type="button" class="btn btn-tool btn-block" data-canvas="ends" data-value="arrow arrow">Both ways ↔</button>
<button type="button" class="btn btn-tool btn-block" data-canvas="ends" data-value="none none">No arrows —</button>
<button type="button" class="btn btn-tool btn-block" data-canvas="ends" data-value="reverse">Reverse ⇄</button>
</div>
</span>
<span class="toolbar-sep"></span>
<button type="button" class="btn btn-tool" data-canvas="undo" title="Undo (Ctrl+Z)">↶</button>
<button type="button" class="btn btn-tool" data-canvas="redo" title="Redo (Ctrl+Y)">↷</button>
<button type="button" class="btn btn-tool danger" data-canvas="delete" title="Delete the selection (Del)">DEL</button>
<span class="toolbar-sep"></span>
<button type="button" class="btn btn-tool" data-canvas="zoomout" title="Zoom out">−</button>
<button type="button" class="btn btn-tool canvas-zoom" data-canvas="zoomreset" title="Zoom to 100%">100%</button>
<button type="button" class="btn btn-tool" data-canvas="zoomin" title="Zoom in">+</button>
<button type="button" class="btn btn-tool" data-canvas="fit" title="Show everything">FIT</button>
</span>
<span class="canvas-status muted" aria-live="polite"></span>
{{end}}
</div>
{{if .Error}}<p class="canvas-error">This canvas cannot be edited: {{.Error}}</p>{{end}}
<div class="canvas-stage">
<div class="canvas-node-toolbar" hidden>{{template "editor-toolbar"}}</div>
<div class="canvas-viewport" tabindex="0">
<div class="canvas-world">
<svg class="canvas-edges" aria-hidden="true"></svg>
<div class="canvas-edge-labels"></div>
<div class="canvas-nodes">{{range .Nodes}}{{template "canvas-node" .}}{{end}}</div>
</div>
<p class="canvas-hint"{{if .Nodes}} hidden{{end}}>Double-click to add a note</p>
<p class="canvas-hint"{{if .Nodes}} hidden{{end}}>Double-click to add a note. Right-click or long-press for more.</p>
</div>
<div class="canvas-statusbar muted"><span class="canvas-status" aria-live="polite"></span><span class="canvas-zoom"></span></div>
{{if not .Error}}<div class="canvas-editor-toolbar" hidden>{{template "editor-toolbar"}}</div>{{end}}
</div>
<script type="application/json" id="canvas-data">{{.Doc}}</script>
</div>
+1 -1
View File
@@ -13,7 +13,7 @@ units; canvas.js keeps style and data-color in sync while editing. */}}
{{define "canvas-file"}}<a class="canvas-card" href="{{.URL}}"><span class="canvas-card-icon">{{.Icon}}</span><span class="truncate">{{.Name}}</span></a>{{end}}
{{define "canvas-link"}}<a class="canvas-card" href="{{.URL}}" target="_blank" rel="noopener noreferrer"><img class="canvas-card-icon" src="https://icons.duckduckgo.com/ip3/{{.Host}}.ico" alt="" width="16" height="16" draggable="false"><span class="canvas-card-text"><span class="truncate">{{.Host}}</span><span class="muted truncate">{{.URL}}</span></span></a>{{end}}
{{define "canvas-link"}}<a class="canvas-card" href="{{.URL}}" target="_blank" rel="noopener noreferrer" title="{{.URL}}"><img class="canvas-card-icon" src="https://icons.duckduckgo.com/ip3/{{.Host}}.ico" alt="" width="16" height="16" draggable="false"><span class="canvas-card-text">{{if .Label}}<span class="truncate">{{.Label}}</span><span class="muted truncate">{{.Host}}</span>{{else}}<span class="truncate">{{.Host}}</span><span class="muted truncate">{{.URL}}</span>{{end}}</span></a>{{end}}
{{define "canvas-missing"}}<p class="canvas-missing">Missing: {{.}}</p>{{end}}
+4
View File
@@ -6,6 +6,7 @@
// MarkdownEditor.create(mount, opts) → { view, actions, getValue, focus, destroy }
// opts.doc initial text
// opts.keymap extra CM key bindings, ahead of the defaults
// opts.onChange called after every change to the text
// MarkdownEditor.bind(root, getActions)
// wires root's [data-action] buttons, [data-key] shortcuts and toolbar
// dropdowns to whatever getActions() returns (null = no editor open).
@@ -89,6 +90,9 @@ window.MarkdownEditor = (function () {
CM.closeBrackets(),
CM.autocompletion({ override: [WikiComplete.source] }),
CM.theme,
opts.onChange ? CM.EditorView.updateListener.of(function (u) {
if (u.docChanged) opts.onChange();
}) : [],
CM.Prec.highest(CM.keymap.of(tableKeymap)),
CM.keymap.of([].concat(
CM.closeBracketsKeymap,
+110 -46
View File
@@ -950,51 +950,31 @@ label.tree-name { flex: 1; cursor: pointer; position: relative; }
display: flex;
flex-direction: column;
}
.canvas-bar {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: var(--space-1) var(--space-4);
padding: var(--space-2) var(--space-4);
border-bottom: var(--border-dashed);
font-size: var(--font-sm);
}
.canvas-title { min-width: 0; max-width: 30rem; }
.canvas-tools { display: flex; flex-wrap: wrap; align-items: center; gap: var(--space-1); }
.canvas-status { margin-left: auto; }
.canvas-status.is-error { color: var(--danger); }
.canvas-zoom { min-width: 4.5em; text-align: center; }
.canvas-color-input {
width: 1.6rem;
height: 1.2rem;
padding: 0;
border: var(--border);
background: none;
cursor: pointer;
}
.canvas-swatch {
display: inline-block;
width: 0.8em;
height: 0.8em;
margin-right: var(--space-2);
background: var(--node-color);
}
.canvas-colors .btn-block { justify-content: flex-start; align-items: center; }
.canvas-error { padding: var(--space-2) var(--space-4); color: var(--danger); }
/* The stage holds the viewport and, floating over its top edge while a note
is open, the page editor's toolbar, so opening it moves nothing. */
/* The stage holds the viewport and what floats over it: the status line,
the context menu and the edit window. */
.canvas-stage { position: relative; flex: 1 1 auto; min-height: 0; }
.canvas-node-toolbar { position: absolute; top: 0; left: 0; right: 0; z-index: 10; }
.canvas-node-toolbar[hidden] { display: none; }
.canvas-node-toolbar .editor-toolbar { position: static; border-left: none; border-right: none; }
.canvas-statusbar {
position: absolute;
left: var(--space-2);
bottom: var(--space-2);
z-index: 10;
display: flex;
gap: var(--space-3);
padding: 0 var(--space-2);
background: var(--bg);
pointer-events: none;
}
.canvas-status.is-error { color: var(--danger); pointer-events: auto; }
.canvas-editor-toolbar[hidden] { display: none; }
.canvas-viewport {
position: absolute;
inset: 0;
overflow: hidden;
touch-action: none;
cursor: grab;
outline: none;
-webkit-touch-callout: none;
background-image: radial-gradient(circle, var(--bg-panel-hover) 1px, transparent 1.5px);
}
.canvas-viewport.is-far { background-image: none; }
@@ -1066,7 +1046,7 @@ label.tree-name { flex: 1; cursor: pointer; position: relative; }
.canvas-node-body {
flex: 1 1 auto;
min-height: 0;
overflow: auto;
overflow: hidden;
padding: var(--space-2) var(--space-3);
word-break: break-word;
}
@@ -1097,13 +1077,6 @@ audio.canvas-media { height: auto; }
.canvas-group-bg-ratio { background-size: contain; background-repeat: no-repeat; }
.canvas-group-bg-repeat { background-repeat: repeat; }
/* The note editor inside a text node (editor/core.js). */
.canvas-node.is-editing { z-index: 5; cursor: auto; user-select: text; -webkit-user-select: text; }
.canvas-node-editor { flex: 1 1 auto; min-height: 0; display: flex; }
.canvas-node-editor .cm-editor { flex: 1 1 auto; min-width: 0; border: none; }
.canvas-node-editor .cm-editor .cm-scroller { min-height: 0; }
.canvas-node-editor .cm-editor .cm-content { padding: var(--space-2) var(--space-3); }
/* Handles: connect from a side, resize from the corner. */
.canvas-handle, .canvas-resize { position: absolute; display: none; z-index: 1; }
.canvas-handle {
@@ -1132,8 +1105,6 @@ audio.canvas-media { height: auto; }
@media (hover: hover) {
.canvas-node:hover > .canvas-handle { display: block; }
}
.canvas-node.is-editing > .canvas-handle,
.canvas-node.is-editing > .canvas-resize { display: none; }
/* Edges: an SVG sized to their bounding box (canvas.js). The wide invisible
hit path makes thin lines easy to click. */
@@ -1169,6 +1140,99 @@ audio.canvas-media { height: auto; }
}
.canvas-edge-label.is-selected { color: var(--primary-hover); }
/* Context menu (canvas.js openMenu): fixed at the pointer. */
.canvas-menu {
position: fixed;
z-index: 100;
min-width: 12rem;
padding: var(--space-1) 0;
font-size: var(--font-sm);
}
.canvas-menu .btn-block { padding: 0.3rem var(--space-3); }
.canvas-menu .btn:disabled { color: var(--text-muted); cursor: default; }
.canvas-menu-sep { border-top: var(--border-dashed); margin: var(--space-1) 0; }
.canvas-menu-row {
display: flex;
align-items: center;
gap: var(--space-2);
padding: 0.3rem var(--space-3);
}
.canvas-menu-row > .muted { min-width: 3rem; }
.canvas-swatch {
width: 1.1rem;
height: 1.1rem;
padding: 0;
border: var(--border);
background: var(--node-color, transparent);
cursor: pointer;
}
.canvas-swatch:not([data-color]):not(input) {
background: linear-gradient(to top right, transparent 45%, var(--text-muted) 45%, var(--text-muted) 55%, transparent 55%);
}
.canvas-swatch.is-current { outline: 2px solid var(--primary-hover); outline-offset: 1px; }
input.canvas-swatch { background: none; }
/* Edit window (canvas.js openWindow): not modal, placed beside its card;
small screens dock it to the bottom. */
.canvas-window {
position: absolute;
z-index: 20;
width: 24rem;
max-width: calc(100% - 2rem);
max-height: calc(100% - 2rem);
display: flex;
flex-direction: column;
box-shadow: 0 0.5rem 1.5rem rgba(0, 0, 0, 0.4);
}
.canvas-window.is-wide { width: 34rem; height: 24rem; resize: both; overflow: hidden; min-width: 18rem; min-height: 12rem; }
.canvas-window .panel-header {
display: flex;
align-items: center;
justify-content: space-between;
gap: var(--space-2);
font-size: var(--font-sm);
color: var(--text);
padding: 0.4rem var(--space-4);
margin-bottom: 0;
cursor: move;
user-select: none;
}
.canvas-window .panel-body { flex: 1 1 auto; min-height: 0; overflow: auto; }
.canvas-window .panel-footer { justify-content: flex-end; }
.canvas-window .input { background: var(--bg); }
.canvas-field { display: flex; flex-direction: column; gap: var(--space-1); min-width: 0; }
.canvas-field-label { font-size: var(--font-xs); text-transform: uppercase; letter-spacing: 0.05em; color: var(--text-muted); }
.canvas-field-row { display: flex; align-items: baseline; gap: var(--space-2); min-width: 0; }
.canvas-field-row > :first-child { flex: 1 1 auto; min-width: 0; }
.canvas-field-pair { display: grid; grid-template-columns: 1fr 1fr; gap: var(--space-3); }
.canvas-fields { display: flex; flex-direction: column; gap: var(--space-3); }
.canvas-window.is-wide .panel-body { padding: 0; }
.canvas-window-editor { flex: 1 1 auto; min-height: 0; display: flex; flex-direction: column; }
.canvas-window-editor .editor-toolbar { position: static; border-left: none; border-right: none; border-top: none; }
.canvas-window-cm { flex: 1 1 auto; min-height: 0; display: flex; }
.canvas-window-cm .cm-editor { flex: 1 1 auto; min-width: 0; border: none; }
.canvas-window-cm .cm-editor .cm-scroller { min-height: 0; }
.canvas-window-cm .cm-editor .cm-content { padding: var(--space-2) var(--space-3); }
@media (max-width: 600px) {
.canvas-window, .canvas-window.is-wide {
position: fixed;
left: 0;
right: 0;
bottom: 0;
top: auto;
width: auto;
max-width: none;
height: auto;
max-height: 60vh;
resize: none;
padding-bottom: env(safe-area-inset-bottom);
}
.canvas-window.is-wide { height: 50vh; }
.canvas-window .panel-header { cursor: default; }
.canvas-menu .btn-block { min-height: 2.5rem; align-items: center; }
.canvas-swatch { width: 1.6rem; height: 1.6rem; }
}
/* === Responsive === */
@media (max-width: 1100px) {
.tree-sidebar, .sidebar { display: none; }