feat: canvas background selector — grid, dot, or blank preference
Add user-selectable canvas background style via data-canvas-bg attribute and CSS custom properties (--rs-canvas-bg-image, --rs-canvas-bg-size). Three options: grid (default), dot, blank — persisted in localStorage. - theme.css: new tokens + [data-canvas-bg] selectors - rstack-identity.ts: Grid/Dot/Blank selector in user dropdown - canvas.html: CSS vars, zoom-aware scaling, canvas-bg-change listener - flows.css: use shared bg-image/bg-size vars (fixes rFlows theme bug) - FOUC prevention in all entry points (shell.ts, create-space.html) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
72100c0922
commit
8072b250ea
|
|
@ -241,8 +241,8 @@
|
||||||
width: 100%; height: 100%; display: block;
|
width: 100%; height: 100%; display: block;
|
||||||
cursor: grab;
|
cursor: grab;
|
||||||
background-color: var(--rs-canvas-bg);
|
background-color: var(--rs-canvas-bg);
|
||||||
background-image: radial-gradient(circle, var(--rs-canvas-grid) 1px, transparent 1px);
|
background-image: var(--rs-canvas-bg-image);
|
||||||
background-size: 20px 20px;
|
background-size: var(--rs-canvas-bg-size);
|
||||||
}
|
}
|
||||||
.flows-canvas-svg.panning { cursor: grabbing; }
|
.flows-canvas-svg.panning { cursor: grabbing; }
|
||||||
.flows-canvas-svg.dragging { cursor: move; }
|
.flows-canvas-svg.dragging { cursor: move; }
|
||||||
|
|
|
||||||
|
|
@ -111,7 +111,7 @@ export function renderShell(opts: ShellOptions): string {
|
||||||
<meta name="apple-mobile-web-app-capable" content="yes">
|
<meta name="apple-mobile-web-app-capable" content="yes">
|
||||||
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent">
|
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent">
|
||||||
<title>${escapeHtml(title)}</title>
|
<title>${escapeHtml(title)}</title>
|
||||||
<script>(function(){var t=localStorage.getItem('canvas-theme');if(!t)t=matchMedia('(prefers-color-scheme:light)').matches?'light':'dark';document.documentElement.setAttribute('data-theme',t)})()</script>
|
<script>(function(){var t=localStorage.getItem('canvas-theme');if(!t)t=matchMedia('(prefers-color-scheme:light)').matches?'light':'dark';document.documentElement.setAttribute('data-theme',t);var b=localStorage.getItem('canvas-bg')||'grid';document.documentElement.setAttribute('data-canvas-bg',b)})()</script>
|
||||||
<link rel="stylesheet" href="/theme.css">
|
<link rel="stylesheet" href="/theme.css">
|
||||||
<link rel="stylesheet" href="/shell.css">
|
<link rel="stylesheet" href="/shell.css">
|
||||||
<style>
|
<style>
|
||||||
|
|
@ -753,7 +753,7 @@ export function renderExternalAppShell(opts: ExternalAppShellOptions): string {
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
<link rel="icon" type="image/png" href="/favicon.png">
|
<link rel="icon" type="image/png" href="/favicon.png">
|
||||||
<title>${escapeHtml(title)}</title>
|
<title>${escapeHtml(title)}</title>
|
||||||
<script>(function(){var t=localStorage.getItem('canvas-theme');if(!t)t=matchMedia('(prefers-color-scheme:light)').matches?'light':'dark';document.documentElement.setAttribute('data-theme',t)})()</script>
|
<script>(function(){var t=localStorage.getItem('canvas-theme');if(!t)t=matchMedia('(prefers-color-scheme:light)').matches?'light':'dark';document.documentElement.setAttribute('data-theme',t);var b=localStorage.getItem('canvas-bg')||'grid';document.documentElement.setAttribute('data-canvas-bg',b)})()</script>
|
||||||
<link rel="stylesheet" href="/theme.css">
|
<link rel="stylesheet" href="/theme.css">
|
||||||
<link rel="stylesheet" href="/shell.css">
|
<link rel="stylesheet" href="/shell.css">
|
||||||
<style>
|
<style>
|
||||||
|
|
@ -1013,7 +1013,7 @@ export function renderModuleLanding(opts: ModuleLandingOptions): string {
|
||||||
<meta name="apple-mobile-web-app-capable" content="yes">
|
<meta name="apple-mobile-web-app-capable" content="yes">
|
||||||
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent">
|
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent">
|
||||||
<title>${escapeHtml(mod.name)} — rSpace</title>
|
<title>${escapeHtml(mod.name)} — rSpace</title>
|
||||||
<script>(function(){var t=localStorage.getItem('canvas-theme');if(!t)t=matchMedia('(prefers-color-scheme:light)').matches?'light':'dark';document.documentElement.setAttribute('data-theme',t)})()</script>
|
<script>(function(){var t=localStorage.getItem('canvas-theme');if(!t)t=matchMedia('(prefers-color-scheme:light)').matches?'light':'dark';document.documentElement.setAttribute('data-theme',t);var b=localStorage.getItem('canvas-bg')||'grid';document.documentElement.setAttribute('data-canvas-bg',b)})()</script>
|
||||||
<link rel="stylesheet" href="/theme.css">
|
<link rel="stylesheet" href="/theme.css">
|
||||||
<link rel="stylesheet" href="/shell.css">
|
<link rel="stylesheet" href="/shell.css">
|
||||||
${cssBlock}
|
${cssBlock}
|
||||||
|
|
@ -1355,7 +1355,7 @@ export function renderSubPageInfo(opts: SubPageInfoOptions): string {
|
||||||
<meta name="apple-mobile-web-app-capable" content="yes">
|
<meta name="apple-mobile-web-app-capable" content="yes">
|
||||||
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent">
|
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent">
|
||||||
<title>${escapeHtml(subPage.title)} — ${escapeHtml(mod.name)} | rSpace</title>
|
<title>${escapeHtml(subPage.title)} — ${escapeHtml(mod.name)} | rSpace</title>
|
||||||
<script>(function(){var t=localStorage.getItem('canvas-theme');if(!t)t=matchMedia('(prefers-color-scheme:light)').matches?'light':'dark';document.documentElement.setAttribute('data-theme',t)})()</script>
|
<script>(function(){var t=localStorage.getItem('canvas-theme');if(!t)t=matchMedia('(prefers-color-scheme:light)').matches?'light':'dark';document.documentElement.setAttribute('data-theme',t);var b=localStorage.getItem('canvas-bg')||'grid';document.documentElement.setAttribute('data-canvas-bg',b)})()</script>
|
||||||
<link rel="stylesheet" href="/theme.css">
|
<link rel="stylesheet" href="/theme.css">
|
||||||
<link rel="stylesheet" href="/shell.css">
|
<link rel="stylesheet" href="/shell.css">
|
||||||
<style>${MODULE_LANDING_CSS}</style>
|
<style>${MODULE_LANDING_CSS}</style>
|
||||||
|
|
|
||||||
|
|
@ -362,6 +362,14 @@ export class RStackIdentity extends HTMLElement {
|
||||||
</label>
|
</label>
|
||||||
<span class="theme-icon">🌙</span>
|
<span class="theme-icon">🌙</span>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="dropdown-canvas-row">
|
||||||
|
<span class="canvas-label">Canvas</span>
|
||||||
|
<div class="canvas-options" id="canvas-bg-options">
|
||||||
|
<button class="canvas-opt${(localStorage.getItem("canvas-bg") || "grid") === "grid" ? " active" : ""}" data-bg="grid">Grid</button>
|
||||||
|
<button class="canvas-opt${localStorage.getItem("canvas-bg") === "dot" ? " active" : ""}" data-bg="dot">Dot</button>
|
||||||
|
<button class="canvas-opt${localStorage.getItem("canvas-bg") === "blank" ? " active" : ""}" data-bg="blank">Blank</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<div class="dropdown-divider"></div>
|
<div class="dropdown-divider"></div>
|
||||||
<button class="dropdown-item dropdown-item--danger" data-action="signout">🚪 Sign Out</button>
|
<button class="dropdown-item dropdown-item--danger" data-action="signout">🚪 Sign Out</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -406,6 +414,22 @@ export class RStackIdentity extends HTMLElement {
|
||||||
this.dispatchEvent(new CustomEvent("theme-change", { bubbles: true, composed: true, detail: { theme: newTheme } }));
|
this.dispatchEvent(new CustomEvent("theme-change", { bubbles: true, composed: true, detail: { theme: newTheme } }));
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Canvas background style selector
|
||||||
|
const canvasBgOptions = this.#shadow.getElementById("canvas-bg-options");
|
||||||
|
if (canvasBgOptions) {
|
||||||
|
canvasBgOptions.addEventListener("click", (e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
const btn = (e.target as HTMLElement).closest("[data-bg]") as HTMLElement | null;
|
||||||
|
if (!btn) return;
|
||||||
|
const bg = btn.dataset.bg!;
|
||||||
|
localStorage.setItem("canvas-bg", bg);
|
||||||
|
document.documentElement.setAttribute("data-canvas-bg", bg);
|
||||||
|
canvasBgOptions.querySelectorAll(".canvas-opt").forEach(b => b.classList.remove("active"));
|
||||||
|
btn.classList.add("active");
|
||||||
|
window.dispatchEvent(new Event("canvas-bg-change"));
|
||||||
|
});
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
this.#shadow.innerHTML = `
|
this.#shadow.innerHTML = `
|
||||||
<style>${STYLES}</style>
|
<style>${STYLES}</style>
|
||||||
|
|
@ -1402,6 +1426,28 @@ const STYLES = `
|
||||||
.theme-toggle input:checked + .theme-slider { background: #6366f1; }
|
.theme-toggle input:checked + .theme-slider { background: #6366f1; }
|
||||||
.theme-toggle input:checked + .theme-slider::before { transform: translateX(18px); }
|
.theme-toggle input:checked + .theme-slider::before { transform: translateX(18px); }
|
||||||
|
|
||||||
|
/* Canvas background selector in dropdown */
|
||||||
|
.dropdown-canvas-row {
|
||||||
|
display: flex; align-items: center; justify-content: center;
|
||||||
|
gap: 8px; padding: 6px 16px;
|
||||||
|
}
|
||||||
|
.canvas-label {
|
||||||
|
font-size: 0.7rem; font-weight: 600; opacity: 0.5;
|
||||||
|
text-transform: uppercase; letter-spacing: 0.04em;
|
||||||
|
}
|
||||||
|
.canvas-options { display: flex; gap: 4px; }
|
||||||
|
.canvas-opt {
|
||||||
|
font-size: 0.65rem; font-weight: 600; padding: 3px 8px;
|
||||||
|
border: 1px solid var(--rs-border); border-radius: 6px;
|
||||||
|
background: transparent; color: var(--rs-text-secondary);
|
||||||
|
cursor: pointer; transition: all 0.15s;
|
||||||
|
}
|
||||||
|
.canvas-opt:hover { background: var(--rs-bg-hover); }
|
||||||
|
.canvas-opt.active {
|
||||||
|
background: var(--rs-accent); color: white;
|
||||||
|
border-color: var(--rs-accent);
|
||||||
|
}
|
||||||
|
|
||||||
/* Avatar wrapper + notification badge */
|
/* Avatar wrapper + notification badge */
|
||||||
.avatar-wrap { position: relative; }
|
.avatar-wrap { position: relative; }
|
||||||
.notif-badge {
|
.notif-badge {
|
||||||
|
|
|
||||||
|
|
@ -11,7 +11,7 @@
|
||||||
<meta name="apple-mobile-web-app-capable" content="yes">
|
<meta name="apple-mobile-web-app-capable" content="yes">
|
||||||
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent">
|
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent">
|
||||||
<title>rSpace Canvas</title>
|
<title>rSpace Canvas</title>
|
||||||
<script>(function(){var t=localStorage.getItem('canvas-theme');if(!t)t=matchMedia('(prefers-color-scheme:light)').matches?'light':'dark';document.documentElement.setAttribute('data-theme',t)})()</script>
|
<script>(function(){var t=localStorage.getItem('canvas-theme');if(!t)t=matchMedia('(prefers-color-scheme:light)').matches?'light':'dark';document.documentElement.setAttribute('data-theme',t);var b=localStorage.getItem('canvas-bg')||'grid';document.documentElement.setAttribute('data-canvas-bg',b)})()</script>
|
||||||
<link rel="stylesheet" href="/theme.css?v=1" />
|
<link rel="stylesheet" href="/theme.css?v=1" />
|
||||||
<style>
|
<style>
|
||||||
/* When loaded inside an iframe, hide shell chrome */
|
/* When loaded inside an iframe, hide shell chrome */
|
||||||
|
|
@ -972,10 +972,8 @@
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
background-color: var(--rs-canvas-bg);
|
background-color: var(--rs-canvas-bg);
|
||||||
background-image:
|
background-image: var(--rs-canvas-bg-image);
|
||||||
linear-gradient(var(--rs-canvas-grid) 1px, transparent 1px),
|
background-size: var(--rs-canvas-bg-size);
|
||||||
linear-gradient(90deg, var(--rs-canvas-grid) 1px, transparent 1px);
|
|
||||||
background-size: 20px 20px;
|
|
||||||
background-position: -1px -1px;
|
background-position: -1px -1px;
|
||||||
position: relative;
|
position: relative;
|
||||||
touch-action: none; /* Prevent browser gestures, handle manually */
|
touch-action: none; /* Prevent browser gestures, handle manually */
|
||||||
|
|
@ -5414,9 +5412,12 @@
|
||||||
function updateCanvasTransform() {
|
function updateCanvasTransform() {
|
||||||
// Transform only the content layer — canvas viewport stays fixed
|
// Transform only the content layer — canvas viewport stays fixed
|
||||||
canvasContent.style.transform = `translate(${panX}px, ${panY}px) scale(${scale})`;
|
canvasContent.style.transform = `translate(${panX}px, ${panY}px) scale(${scale})`;
|
||||||
// Adjust grid to track pan/zoom so it appears infinite
|
// Adjust grid/dot pattern to track pan/zoom (skip for blank)
|
||||||
const gridSize = 20 * scale;
|
const bgStyle = document.documentElement.getAttribute('data-canvas-bg') || 'grid';
|
||||||
canvas.style.backgroundSize = `${gridSize}px ${gridSize}px`;
|
if (bgStyle !== 'blank') {
|
||||||
|
const gridSize = 20 * scale;
|
||||||
|
canvas.style.backgroundSize = `${gridSize}px ${gridSize}px`;
|
||||||
|
}
|
||||||
canvas.style.backgroundPosition = `${panX - 1}px ${panY - 1}px`;
|
canvas.style.backgroundPosition = `${panX - 1}px ${panY - 1}px`;
|
||||||
// Keep MI bridge in sync
|
// Keep MI bridge in sync
|
||||||
__miCanvasBridge.setViewport(panX, panY, scale);
|
__miCanvasBridge.setViewport(panX, panY, scale);
|
||||||
|
|
@ -5430,6 +5431,9 @@
|
||||||
presence.setCamera(panX, panY, scale);
|
presence.setCamera(panX, panY, scale);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Re-render canvas background when user changes preference
|
||||||
|
window.addEventListener("canvas-bg-change", () => updateCanvasTransform());
|
||||||
|
|
||||||
document.getElementById("zoom-in").addEventListener("click", () => {
|
document.getElementById("zoom-in").addEventListener("click", () => {
|
||||||
scale = Math.min(scale * 1.1, maxScale);
|
scale = Math.min(scale * 1.1, maxScale);
|
||||||
updateCanvasTransform();
|
updateCanvasTransform();
|
||||||
|
|
|
||||||
|
|
@ -11,7 +11,7 @@
|
||||||
<meta name="apple-mobile-web-app-capable" content="yes">
|
<meta name="apple-mobile-web-app-capable" content="yes">
|
||||||
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent">
|
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent">
|
||||||
<title>Create a Space — rSpace</title>
|
<title>Create a Space — rSpace</title>
|
||||||
<script>(function(){var t=localStorage.getItem('canvas-theme');if(!t)t=matchMedia('(prefers-color-scheme:light)').matches?'light':'dark';document.documentElement.setAttribute('data-theme',t)})()</script>
|
<script>(function(){var t=localStorage.getItem('canvas-theme');if(!t)t=matchMedia('(prefers-color-scheme:light)').matches?'light':'dark';document.documentElement.setAttribute('data-theme',t);var b=localStorage.getItem('canvas-bg')||'grid';document.documentElement.setAttribute('data-canvas-bg',b)})()</script>
|
||||||
<link rel="stylesheet" href="/theme.css">
|
<link rel="stylesheet" href="/theme.css">
|
||||||
<link rel="stylesheet" href="/shell.css">
|
<link rel="stylesheet" href="/shell.css">
|
||||||
<style>
|
<style>
|
||||||
|
|
|
||||||
|
|
@ -69,6 +69,10 @@
|
||||||
/* Canvas */
|
/* Canvas */
|
||||||
--rs-canvas-bg: #0f172a;
|
--rs-canvas-bg: #0f172a;
|
||||||
--rs-canvas-grid: rgba(255, 255, 255, 0.04);
|
--rs-canvas-grid: rgba(255, 255, 255, 0.04);
|
||||||
|
--rs-canvas-dot: rgba(255, 255, 255, 0.06);
|
||||||
|
--rs-canvas-bg-image: linear-gradient(var(--rs-canvas-grid) 1px, transparent 1px),
|
||||||
|
linear-gradient(90deg, var(--rs-canvas-grid) 1px, transparent 1px);
|
||||||
|
--rs-canvas-bg-size: 20px 20px;
|
||||||
|
|
||||||
/* Toolbar */
|
/* Toolbar */
|
||||||
--rs-toolbar-bg: #1e293b;
|
--rs-toolbar-bg: #1e293b;
|
||||||
|
|
@ -128,6 +132,7 @@
|
||||||
/* Canvas */
|
/* Canvas */
|
||||||
--rs-canvas-bg: #fafaf7;
|
--rs-canvas-bg: #fafaf7;
|
||||||
--rs-canvas-grid: #f1f5f9;
|
--rs-canvas-grid: #f1f5f9;
|
||||||
|
--rs-canvas-dot: #e2e8f0;
|
||||||
|
|
||||||
/* Toolbar */
|
/* Toolbar */
|
||||||
--rs-toolbar-bg: #fafaf7;
|
--rs-toolbar-bg: #fafaf7;
|
||||||
|
|
@ -181,6 +186,7 @@
|
||||||
|
|
||||||
--rs-canvas-bg: #fafaf7;
|
--rs-canvas-bg: #fafaf7;
|
||||||
--rs-canvas-grid: #f1f5f9;
|
--rs-canvas-grid: #f1f5f9;
|
||||||
|
--rs-canvas-dot: #e2e8f0;
|
||||||
|
|
||||||
--rs-toolbar-bg: #fafaf7;
|
--rs-toolbar-bg: #fafaf7;
|
||||||
--rs-toolbar-btn-bg: #f0efe9;
|
--rs-toolbar-btn-bg: #f0efe9;
|
||||||
|
|
@ -191,3 +197,13 @@
|
||||||
--rs-toolbar-panel-border: #e2e8f0;
|
--rs-toolbar-panel-border: #e2e8f0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ── Canvas background style overrides ── */
|
||||||
|
[data-canvas-bg="dot"] {
|
||||||
|
--rs-canvas-bg-image: radial-gradient(circle, var(--rs-canvas-dot) 1px, transparent 1px);
|
||||||
|
--rs-canvas-bg-size: 20px 20px;
|
||||||
|
}
|
||||||
|
[data-canvas-bg="blank"] {
|
||||||
|
--rs-canvas-bg-image: none;
|
||||||
|
--rs-canvas-bg-size: auto;
|
||||||
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue