JSON canvas initial
This commit is contained in:
@@ -26,6 +26,17 @@
|
||||
--danger-hover: #d03467;
|
||||
--overlay: rgba(0, 0, 0, 0.55);
|
||||
|
||||
/* JSON Canvas preset colours "1".."6" (red, orange, yellow, green, cyan,
|
||||
purple), pulled toward the app palette so a canvas does not look like a
|
||||
different program. Hex colours in a .canvas file pass through untouched. */
|
||||
--canvas-1: #e05555;
|
||||
--canvas-2: #e08a3c;
|
||||
--canvas-3: #d9b03c;
|
||||
--canvas-4: #5aab64;
|
||||
--canvas-5: #01b6c4;
|
||||
--canvas-6: #a069c4;
|
||||
--canvas-edge: #9a9a9a;
|
||||
|
||||
--border: 1px solid var(--secondary);
|
||||
--border-dashed: 1px dashed var(--secondary);
|
||||
--border-guide: 1px solid var(--bg-panel-hover);
|
||||
@@ -889,3 +900,319 @@ aside.sidebar:empty { display: none; }
|
||||
.list-item td.meta { grid-row: 2; grid-column: 2; text-align: left; }
|
||||
.toc {float: none; width: auto; max-width: none; margin: 0 0 var(--space-4);}
|
||||
}
|
||||
|
||||
/* === Canvas ===
|
||||
A canvas is an alternative view of a page (see canvas.go). The tab strip is
|
||||
shared with the normal page template; everything below it only exists in
|
||||
canvas mode. */
|
||||
|
||||
/* --- view switcher (rendered on normal pages too) --- */
|
||||
.canvas-tabs {
|
||||
gap: 0;
|
||||
flex-wrap: wrap;
|
||||
border-bottom: var(--border-dashed);
|
||||
margin-bottom: var(--space-4);
|
||||
}
|
||||
.canvas-tab {
|
||||
padding: var(--space-2) var(--space-3);
|
||||
font-size: var(--font-sm);
|
||||
color: var(--text-muted);
|
||||
background: none;
|
||||
border: 1px solid transparent;
|
||||
border-bottom: none;
|
||||
/* Overlap the strip's own border so the active tab joins it. */
|
||||
margin-bottom: -1px;
|
||||
font: inherit;
|
||||
font-size: var(--font-sm);
|
||||
cursor: pointer;
|
||||
}
|
||||
.canvas-tab:hover { color: var(--link-hover); }
|
||||
.canvas-tab.is-active {
|
||||
color: var(--text);
|
||||
border-color: var(--secondary);
|
||||
background: var(--bg-panel);
|
||||
}
|
||||
.canvas-tab-add { color: var(--secondary); }
|
||||
|
||||
/* --- full-bleed view mode ---
|
||||
The canvas owns the viewport instead of the centred reading column: main
|
||||
loses its max-width and padding, and the centre column stops scrolling so the
|
||||
surface can handle pan itself. */
|
||||
body.canvas-mode .center { overflow: hidden; }
|
||||
body.canvas-mode main {
|
||||
max-width: none;
|
||||
padding: 0;
|
||||
flex: 1 1 auto;
|
||||
min-height: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
body.canvas-mode footer { max-width: none; }
|
||||
body.canvas-mode .canvas-tabs { margin: 0; padding: var(--space-2) var(--space-4) 0; }
|
||||
|
||||
.canvas-app {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex: 1 1 auto;
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
.canvas-toolbar {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: var(--space-1);
|
||||
padding: 0.4rem 0.6rem;
|
||||
background: var(--bg-panel-hover);
|
||||
border-top: var(--border);
|
||||
border-bottom: var(--border);
|
||||
flex: 0 0 auto;
|
||||
}
|
||||
.canvas-status { margin-left: auto; padding-left: var(--space-3); }
|
||||
.canvas-swatch {
|
||||
display: inline-block;
|
||||
width: 0.7em;
|
||||
height: 0.7em;
|
||||
vertical-align: middle;
|
||||
margin-right: 0.3em;
|
||||
border: 1px solid var(--bg);
|
||||
}
|
||||
.canvas-swatch[data-swatch="1"] { background: var(--canvas-1); }
|
||||
.canvas-swatch[data-swatch="2"] { background: var(--canvas-2); }
|
||||
.canvas-swatch[data-swatch="3"] { background: var(--canvas-3); }
|
||||
.canvas-swatch[data-swatch="4"] { background: var(--canvas-4); }
|
||||
.canvas-swatch[data-swatch="5"] { background: var(--canvas-5); }
|
||||
.canvas-swatch[data-swatch="6"] { background: var(--canvas-6); }
|
||||
|
||||
/* --- surface, world, layers ---
|
||||
touch-action:none hands every touch gesture to the pointer handlers; without
|
||||
it the browser claims one-finger drags for scrolling and two-finger for page
|
||||
zoom, and neither reaches the canvas. */
|
||||
.canvas-surface {
|
||||
position: relative;
|
||||
flex: 1 1 auto;
|
||||
min-height: 0;
|
||||
overflow: hidden;
|
||||
touch-action: none;
|
||||
outline: none;
|
||||
cursor: grab;
|
||||
background-image: radial-gradient(circle, var(--bg-panel-hover) 1px, transparent 1px);
|
||||
background-repeat: repeat;
|
||||
}
|
||||
.canvas-surface.is-grid-hidden { background-image: none; }
|
||||
|
||||
/* The world is a zero-size anchor: all of its content is absolutely positioned
|
||||
in world coordinates and the whole thing is moved by one transform, so pan
|
||||
and zoom never touch a single node's layout. */
|
||||
.canvas-world {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 0;
|
||||
height: 0;
|
||||
transform-origin: 0 0;
|
||||
will-change: transform;
|
||||
}
|
||||
.canvas-layer { position: absolute; top: 0; left: 0; }
|
||||
/* Drawn in world coordinates with no viewport of its own; overflow:visible is
|
||||
what lets it paint outside its 1x1 box, including at negative coordinates. */
|
||||
.canvas-edges {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 1px;
|
||||
height: 1px;
|
||||
overflow: visible;
|
||||
}
|
||||
|
||||
/* --- nodes --- */
|
||||
.canvas-node {
|
||||
position: absolute;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
border: 1px solid var(--node-accent, var(--secondary));
|
||||
background: var(--bg-panel);
|
||||
/* Visible so the connection ports and resize handles, which sit on the
|
||||
border, are not clipped; the body clips its own content instead. */
|
||||
overflow: visible;
|
||||
contain: layout paint;
|
||||
}
|
||||
.canvas-node[data-color="1"] { --node-accent: var(--canvas-1); }
|
||||
.canvas-node[data-color="2"] { --node-accent: var(--canvas-2); }
|
||||
.canvas-node[data-color="3"] { --node-accent: var(--canvas-3); }
|
||||
.canvas-node[data-color="4"] { --node-accent: var(--canvas-4); }
|
||||
.canvas-node[data-color="5"] { --node-accent: var(--canvas-5); }
|
||||
.canvas-node[data-color="6"] { --node-accent: var(--canvas-6); }
|
||||
.canvas-node.is-selected { outline: 2px solid var(--primary-hover); outline-offset: 1px; }
|
||||
.canvas-node.is-edge-target { outline: 2px dashed var(--link); outline-offset: 1px; }
|
||||
|
||||
.canvas-node-body {
|
||||
flex: 1 1 auto;
|
||||
min-height: 0;
|
||||
overflow: auto;
|
||||
padding: var(--space-3);
|
||||
font-size: var(--font-sm);
|
||||
}
|
||||
.canvas-node-body.content { margin-bottom: 0; }
|
||||
.canvas-node-body > :first-child { margin-top: 0; }
|
||||
.canvas-node-body > :last-child { margin-bottom: 0; }
|
||||
.canvas-node-body h1 { font-size: 1.1rem; }
|
||||
.canvas-node-body h2 { font-size: 1rem; }
|
||||
.canvas-node-body h3 { font-size: 0.95rem; }
|
||||
/* Task checkboxes are rendered enabled for pages, where page/tasks.js writes
|
||||
the toggle back to index.md. Inside a canvas node there is no such wiring and
|
||||
the markdown lives in the .canvas file, so a click would be a lie. */
|
||||
.canvas-node-body .task-checkbox { pointer-events: none; }
|
||||
|
||||
.canvas-node-title {
|
||||
flex: 0 0 auto;
|
||||
padding: var(--space-1) var(--space-3);
|
||||
font-size: var(--font-xs);
|
||||
color: var(--text-muted);
|
||||
border-bottom: var(--border-dashed);
|
||||
}
|
||||
|
||||
/* A transcluded image or player fills its node edge to edge. */
|
||||
.canvas-node-body:has(> .canvas-media) { padding: 0; overflow: hidden; }
|
||||
.canvas-media { display: block; width: 100%; height: 100%; }
|
||||
.canvas-media img { display: block; width: 100%; height: 100%; object-fit: contain; }
|
||||
.canvas-broken { color: var(--danger-hover); }
|
||||
.canvas-file-fallback { padding: var(--space-2) 0; }
|
||||
|
||||
.canvas-link-card {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--space-1);
|
||||
align-items: flex-start;
|
||||
max-width: 100%;
|
||||
}
|
||||
.canvas-link-host { color: var(--link); }
|
||||
.canvas-link-url { font-size: var(--font-xs); color: var(--text-muted); max-width: 100%; }
|
||||
.link-favicon { vertical-align: middle; margin-right: 3px; }
|
||||
|
||||
.canvas-node-group {
|
||||
background: rgba(255, 255, 255, 0.03);
|
||||
border: 1px dashed var(--node-accent, var(--bg-panel-hover));
|
||||
background-position: center;
|
||||
}
|
||||
.canvas-node-group[data-bg-style="cover"] { background-size: cover; }
|
||||
.canvas-node-group[data-bg-style="ratio"] { background-size: contain; background-repeat: no-repeat; }
|
||||
.canvas-node-group[data-bg-style="repeat"] { background-repeat: repeat; }
|
||||
.canvas-node-group .canvas-node-body { display: none; }
|
||||
.canvas-group-label {
|
||||
padding: var(--space-2) var(--space-3);
|
||||
font-size: var(--font-sm);
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
/* --- ports and handles ---
|
||||
Both are positioned on the node border (the negative margin centres them on
|
||||
it) and stay invisible until the node is hovered or selected, so a dense
|
||||
canvas is not covered in dots. */
|
||||
.canvas-port {
|
||||
position: absolute;
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
margin: -6px;
|
||||
border-radius: 50%;
|
||||
background: var(--bg);
|
||||
border: 1px solid var(--secondary);
|
||||
opacity: 0;
|
||||
cursor: crosshair;
|
||||
}
|
||||
.canvas-node:hover .canvas-port,
|
||||
.canvas-node.is-selected .canvas-port { opacity: 1; }
|
||||
.canvas-port:hover { background: var(--primary-hover); }
|
||||
.canvas-port-top { top: 0; left: 50%; }
|
||||
.canvas-port-bottom { top: 100%; left: 50%; }
|
||||
.canvas-port-left { top: 50%; left: 0; }
|
||||
.canvas-port-right { top: 50%; left: 100%; }
|
||||
|
||||
.canvas-handle {
|
||||
position: absolute;
|
||||
width: 10px;
|
||||
height: 10px;
|
||||
margin: -5px;
|
||||
background: var(--primary-hover);
|
||||
opacity: 0;
|
||||
}
|
||||
.canvas-node.is-selected .canvas-handle { opacity: 1; }
|
||||
.canvas-handle-nw { top: 0; left: 0; cursor: nwse-resize; }
|
||||
.canvas-handle-ne { top: 0; left: 100%; cursor: nesw-resize; }
|
||||
.canvas-handle-se { top: 100%; left: 100%; cursor: nwse-resize; }
|
||||
.canvas-handle-sw { top: 100%; left: 0; cursor: nesw-resize; }
|
||||
|
||||
/* --- edges ---
|
||||
The hit path is a fat transparent stroke under the visible curve: a 2px
|
||||
bezier is effectively unclickable without it. */
|
||||
.canvas-edge-hit {
|
||||
stroke: transparent;
|
||||
stroke-width: 16;
|
||||
pointer-events: stroke;
|
||||
cursor: pointer;
|
||||
}
|
||||
.canvas-edge-line { stroke-width: 2; pointer-events: none; }
|
||||
.canvas-edge-head { pointer-events: none; }
|
||||
.canvas-edge.is-selected .canvas-edge-line { stroke: var(--primary-hover); stroke-width: 3; }
|
||||
.canvas-edge.is-selected .canvas-edge-head { fill: var(--primary-hover); }
|
||||
.canvas-edge-label {
|
||||
font-family: "Iosevka Etoile", monospace;
|
||||
font-size: 12px;
|
||||
fill: var(--text);
|
||||
/* A stroke painted under the fill gives the label a halo against whatever
|
||||
it overlaps, without measuring a background rect. */
|
||||
stroke: var(--bg);
|
||||
stroke-width: 4;
|
||||
paint-order: stroke;
|
||||
pointer-events: none;
|
||||
}
|
||||
.canvas-edge-temp {
|
||||
stroke: var(--primary-hover);
|
||||
stroke-width: 2;
|
||||
stroke-dasharray: 6 4;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
/* --- overlays --- */
|
||||
.canvas-marquee {
|
||||
position: absolute;
|
||||
border: 1px dashed var(--primary-hover);
|
||||
background: rgba(214, 77, 149, 0.12);
|
||||
pointer-events: none;
|
||||
}
|
||||
.canvas-hint {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
max-width: 30rem;
|
||||
padding: var(--space-4);
|
||||
text-align: center;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
/* The node editor is fixed to the viewport rather than mounted in the zoomed
|
||||
world: CodeMirror measures itself in device pixels and a scaled ancestor
|
||||
breaks its caret and selection maths. */
|
||||
.canvas-editor {
|
||||
position: fixed;
|
||||
z-index: 60;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
.canvas-editor .panel-header { padding: var(--space-2) var(--space-3); margin-bottom: 0; }
|
||||
.canvas-editor-mount { flex: 1 1 auto; min-height: 0; overflow: auto; }
|
||||
.canvas-editor-mount .cm-editor { height: 100%; }
|
||||
/* The shared editor theme sets a 60vh floor for the full-page editor; in a
|
||||
node-sized panel that would force a scrollbar at every size. The theme is
|
||||
injected at runtime and lands after this sheet, so it needs overriding. */
|
||||
.canvas-editor-mount .cm-scroller { min-height: 0 !important; }
|
||||
|
||||
@media (max-width: 600px) {
|
||||
.canvas-toolbar { gap: var(--space-2); padding: var(--space-2); }
|
||||
.canvas-toolbar .btn-tool { padding: 0.2rem 0.4rem; }
|
||||
body.canvas-mode .canvas-tabs { padding: var(--space-2) var(--space-2) 0; }
|
||||
/* Touch has no hover, so ports would never appear; show them on selection
|
||||
only and make both targets finger-sized. */
|
||||
.canvas-port { width: 16px; height: 16px; margin: -8px; }
|
||||
.canvas-handle { width: 14px; height: 14px; margin: -7px; }
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user