perf(rnetwork): optimize 3D graph rendering + fix cross-platform mobile PWA
Graph performance: seed node positions to prevent NaN geometry warnings, cache sprite textures, reduce simulation ticks (120→50 warmup, 300→150 cooldown), cap particles at 2, lower sphere segments, halve label canvas size, add sessionStorage graph data cache with 60s TTL. Mobile PWA: add both mobile-web-app-capable (Chrome/Android) and apple-mobile-web-app-capable (iOS), viewport-fit=cover for notch support, apple-mobile-web-app-title, safe-area CSS insets on header/body, fix admin.html missing all mobile meta tags. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
e498233666
commit
9008640f79
|
|
@ -160,6 +160,10 @@ class FolkGraphViewer extends HTMLElement {
|
||||||
private resizeObserver: ResizeObserver | null = null;
|
private resizeObserver: ResizeObserver | null = null;
|
||||||
private companyColors: Map<string, number> = new Map();
|
private companyColors: Map<string, number> = new Map();
|
||||||
|
|
||||||
|
// Sprite caches — avoid recreating Canvas+Texture per node per frame
|
||||||
|
private _textSpriteCache = new Map<string, any>();
|
||||||
|
private _badgeSpriteCache = new Map<string, any>();
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
super();
|
super();
|
||||||
this.shadow = this.attachShadow({ mode: "open" });
|
this.shadow = this.attachShadow({ mode: "open" });
|
||||||
|
|
@ -192,16 +196,32 @@ class FolkGraphViewer extends HTMLElement {
|
||||||
const base = this.getApiBase();
|
const base = this.getApiBase();
|
||||||
try {
|
try {
|
||||||
const trustParam = this.trustMode ? `?trust=true&authority=${encodeURIComponent(this.authority)}` : "";
|
const trustParam = this.trustMode ? `?trust=true&authority=${encodeURIComponent(this.authority)}` : "";
|
||||||
|
const graphCacheKey = `rnetwork:graph:${this.space}:${this.trustMode}:${this.authority}`;
|
||||||
|
|
||||||
|
// Check sessionStorage cache (60s TTL, matches server cache)
|
||||||
|
let graphData: any = null;
|
||||||
|
try {
|
||||||
|
const cached = sessionStorage.getItem(graphCacheKey);
|
||||||
|
if (cached) {
|
||||||
|
const { data, ts } = JSON.parse(cached);
|
||||||
|
if (Date.now() - ts < 60_000) graphData = data;
|
||||||
|
}
|
||||||
|
} catch { /* storage unavailable */ }
|
||||||
|
|
||||||
const [wsRes, infoRes, graphRes] = await Promise.all([
|
const [wsRes, infoRes, graphRes] = await Promise.all([
|
||||||
fetch(`${base}/api/workspaces`),
|
fetch(`${base}/api/workspaces`),
|
||||||
fetch(`${base}/api/info`),
|
fetch(`${base}/api/info`),
|
||||||
fetch(`${base}/api/graph${trustParam}`),
|
graphData ? Promise.resolve(null) : fetch(`${base}/api/graph${trustParam}`),
|
||||||
]);
|
]);
|
||||||
if (wsRes.ok) this.workspaces = await wsRes.json();
|
if (wsRes.ok) this.workspaces = await wsRes.json();
|
||||||
if (infoRes.ok) this.info = await infoRes.json();
|
if (infoRes.ok) this.info = await infoRes.json();
|
||||||
if (graphRes.ok) {
|
|
||||||
const graph = await graphRes.json();
|
if (graphData) {
|
||||||
this.importGraph(graph);
|
this.importGraph(graphData);
|
||||||
|
} else if (graphRes && graphRes.ok) {
|
||||||
|
graphData = await graphRes.json();
|
||||||
|
this.importGraph(graphData);
|
||||||
|
try { sessionStorage.setItem(graphCacheKey, JSON.stringify({ data: graphData, ts: Date.now() })); } catch { /* quota */ }
|
||||||
}
|
}
|
||||||
} catch { /* offline */ }
|
} catch { /* offline */ }
|
||||||
this.updateStatsBar();
|
this.updateStatsBar();
|
||||||
|
|
@ -213,6 +233,8 @@ class FolkGraphViewer extends HTMLElement {
|
||||||
private async reloadWithAuthority(authority: AuthoritySelection) {
|
private async reloadWithAuthority(authority: AuthoritySelection) {
|
||||||
this.authority = authority;
|
this.authority = authority;
|
||||||
this.trustMode = true;
|
this.trustMode = true;
|
||||||
|
this._textSpriteCache.clear();
|
||||||
|
this._badgeSpriteCache.clear();
|
||||||
await this.loadData();
|
await this.loadData();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -968,8 +990,8 @@ class FolkGraphViewer extends HTMLElement {
|
||||||
})
|
})
|
||||||
.linkDirectionalArrowRelPos(1)
|
.linkDirectionalArrowRelPos(1)
|
||||||
.linkDirectionalParticles((link: GraphEdge) => {
|
.linkDirectionalParticles((link: GraphEdge) => {
|
||||||
if (link.type === "cross_layer_flow") return Math.ceil((link.strength || 0.5) * 3);
|
if (link.type === "cross_layer_flow") return Math.min(2, Math.ceil((link.strength || 0.5) * 3));
|
||||||
return link.type === "delegates_to" ? Math.ceil((link.weight || 0.5) * 4) : 0;
|
return link.type === "delegates_to" ? Math.min(2, Math.ceil((link.weight || 0.5) * 4)) : 0;
|
||||||
})
|
})
|
||||||
.linkDirectionalParticleSpeed((link: GraphEdge) =>
|
.linkDirectionalParticleSpeed((link: GraphEdge) =>
|
||||||
link.type === "cross_layer_flow" ? 0.006 : 0.004
|
link.type === "cross_layer_flow" ? 0.006 : 0.004
|
||||||
|
|
@ -1018,8 +1040,8 @@ class FolkGraphViewer extends HTMLElement {
|
||||||
})
|
})
|
||||||
.d3AlphaDecay(0.02)
|
.d3AlphaDecay(0.02)
|
||||||
.d3VelocityDecay(0.3)
|
.d3VelocityDecay(0.3)
|
||||||
.warmupTicks(120)
|
.warmupTicks(50)
|
||||||
.cooldownTicks(300);
|
.cooldownTicks(150);
|
||||||
|
|
||||||
this.graph = graph;
|
this.graph = graph;
|
||||||
|
|
||||||
|
|
@ -1118,8 +1140,8 @@ class FolkGraphViewer extends HTMLElement {
|
||||||
const group = new THREE.Group();
|
const group = new THREE.Group();
|
||||||
|
|
||||||
// Sphere geometry
|
// Sphere geometry
|
||||||
const segments = (node.type === "module" || node.type === "feed") ? 24 : 16;
|
const segments = (node.type === "module" || node.type === "feed") ? 24 : 12;
|
||||||
const geometry = new THREE.SphereGeometry(radius, segments, segments * 3 / 4);
|
const geometry = new THREE.SphereGeometry(radius, segments, Math.round(segments * 3 / 4));
|
||||||
const opacity = node.type === "module" ? 0.95 : node.type === "feed" ? 0.85 : node.type === "company" ? 0.9 : 0.75;
|
const opacity = node.type === "module" ? 0.95 : node.type === "feed" ? 0.85 : node.type === "company" ? 0.9 : 0.75;
|
||||||
const material = new THREE.MeshLambertMaterial({
|
const material = new THREE.MeshLambertMaterial({
|
||||||
color,
|
color,
|
||||||
|
|
@ -1205,22 +1227,27 @@ class FolkGraphViewer extends HTMLElement {
|
||||||
}
|
}
|
||||||
|
|
||||||
private createTextSprite(THREE: any, node: GraphNode): any {
|
private createTextSprite(THREE: any, node: GraphNode): any {
|
||||||
|
const isSelected = this.selectedNode?.id === node.id;
|
||||||
|
const cacheKey = `${node.id}:${isSelected}:${this.trustMode}:${this.authority}`;
|
||||||
|
const cached = this._textSpriteCache.get(cacheKey);
|
||||||
|
if (cached) return cached.clone();
|
||||||
|
|
||||||
const canvas = document.createElement("canvas");
|
const canvas = document.createElement("canvas");
|
||||||
const ctx = canvas.getContext("2d");
|
const ctx = canvas.getContext("2d");
|
||||||
if (!ctx) return null;
|
if (!ctx) return null;
|
||||||
|
|
||||||
const text = node.name;
|
const text = node.name;
|
||||||
const fontSize = node.type === "module" ? 44 : node.type === "company" ? 42 : node.type === "feed" ? 30 : 36;
|
const fontSize = node.type === "module" ? 44 : node.type === "company" ? 42 : node.type === "feed" ? 30 : 36;
|
||||||
canvas.width = 512;
|
canvas.width = 256;
|
||||||
canvas.height = 96;
|
canvas.height = 48;
|
||||||
|
|
||||||
ctx.font = `${(node.type === "company" || node.type === "module") ? "600" : "500"} ${fontSize}px system-ui, sans-serif`;
|
ctx.font = `${(node.type === "company" || node.type === "module") ? "600" : "500"} ${Math.round(fontSize / 2)}px system-ui, sans-serif`;
|
||||||
ctx.fillStyle = "#e2e8f0";
|
ctx.fillStyle = "#e2e8f0";
|
||||||
ctx.textAlign = "center";
|
ctx.textAlign = "center";
|
||||||
ctx.textBaseline = "middle";
|
ctx.textBaseline = "middle";
|
||||||
ctx.shadowColor = "rgba(0,0,0,0.9)";
|
ctx.shadowColor = "rgba(0,0,0,0.9)";
|
||||||
ctx.shadowBlur = 6;
|
ctx.shadowBlur = 3;
|
||||||
ctx.fillText(text.length > 24 ? text.slice(0, 22) + "\u2026" : text, 256, 48);
|
ctx.fillText(text.length > 24 ? text.slice(0, 22) + "\u2026" : text, 128, 24);
|
||||||
|
|
||||||
const texture = new THREE.CanvasTexture(canvas);
|
const texture = new THREE.CanvasTexture(canvas);
|
||||||
texture.needsUpdate = true;
|
texture.needsUpdate = true;
|
||||||
|
|
@ -1231,10 +1258,15 @@ class FolkGraphViewer extends HTMLElement {
|
||||||
});
|
});
|
||||||
const sprite = new THREE.Sprite(spriteMaterial);
|
const sprite = new THREE.Sprite(spriteMaterial);
|
||||||
sprite.scale.set(14, 3.5, 1);
|
sprite.scale.set(14, 3.5, 1);
|
||||||
return sprite;
|
this._textSpriteCache.set(cacheKey, sprite);
|
||||||
|
return sprite.clone();
|
||||||
}
|
}
|
||||||
|
|
||||||
private createBadgeSprite(THREE: any, text: string, color = "#7c3aed"): any {
|
private createBadgeSprite(THREE: any, text: string, color = "#7c3aed"): any {
|
||||||
|
const cacheKey = `${text}:${color}`;
|
||||||
|
const cached = this._badgeSpriteCache.get(cacheKey);
|
||||||
|
if (cached) return cached.clone();
|
||||||
|
|
||||||
const canvas = document.createElement("canvas");
|
const canvas = document.createElement("canvas");
|
||||||
const ctx = canvas.getContext("2d");
|
const ctx = canvas.getContext("2d");
|
||||||
if (!ctx) return null;
|
if (!ctx) return null;
|
||||||
|
|
@ -1262,7 +1294,8 @@ class FolkGraphViewer extends HTMLElement {
|
||||||
});
|
});
|
||||||
const sprite = new THREE.Sprite(spriteMaterial);
|
const sprite = new THREE.Sprite(spriteMaterial);
|
||||||
sprite.scale.set(1.5, 1.5, 1);
|
sprite.scale.set(1.5, 1.5, 1);
|
||||||
return sprite;
|
this._badgeSpriteCache.set(cacheKey, sprite);
|
||||||
|
return sprite.clone();
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Data update (no DOM rebuild) ──
|
// ── Data update (no DOM rebuild) ──
|
||||||
|
|
@ -1304,6 +1337,13 @@ class FolkGraphViewer extends HTMLElement {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Seed initial positions to prevent NaN geometry warnings during sim warmup
|
||||||
|
for (const n of filtered) {
|
||||||
|
if (n.x == null || isNaN(n.x)) n.x = (Math.random() - 0.5) * 100;
|
||||||
|
if (n.y == null || isNaN(n.y)) n.y = (Math.random() - 0.5) * 100;
|
||||||
|
if (n.z == null || isNaN(n.z)) n.z = (Math.random() - 0.5) * 100;
|
||||||
|
}
|
||||||
|
|
||||||
this.graph.graphData({
|
this.graph.graphData({
|
||||||
nodes: filtered,
|
nodes: filtered,
|
||||||
links: filteredEdges,
|
links: filteredEdges,
|
||||||
|
|
|
||||||
|
|
@ -113,13 +113,15 @@ export function renderShell(opts: ShellOptions): string {
|
||||||
<html lang="en">
|
<html lang="en">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="utf-8">
|
<meta charset="utf-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover">
|
||||||
<link rel="icon" type="image/png" href="/favicon.png">
|
<link rel="icon" type="image/png" href="/favicon.png">
|
||||||
<link rel="manifest" href="/manifest.json">
|
<link rel="manifest" href="/manifest.json">
|
||||||
<meta name="theme-color" content="#0f172a" media="(prefers-color-scheme: dark)">
|
<meta name="theme-color" content="#0f172a" media="(prefers-color-scheme: dark)">
|
||||||
<meta name="theme-color" content="#f5f5f0" media="(prefers-color-scheme: light)">
|
<meta name="theme-color" content="#f5f5f0" media="(prefers-color-scheme: light)">
|
||||||
<link rel="apple-touch-icon" href="/icons/apple-touch-icon.png">
|
<link rel="apple-touch-icon" href="/icons/apple-touch-icon.png">
|
||||||
|
<meta name="mobile-web-app-capable" content="yes">
|
||||||
<meta name="apple-mobile-web-app-capable" content="yes">
|
<meta name="apple-mobile-web-app-capable" content="yes">
|
||||||
|
<meta name="apple-mobile-web-app-title" content="rSpace">
|
||||||
<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>
|
||||||
<meta name="description" content="rSpace — local-first community platform with 25+ composable apps. Encrypted, interoperable, self-sovereign.">
|
<meta name="description" content="rSpace — local-first community platform with 25+ composable apps. Encrypted, interoperable, self-sovereign.">
|
||||||
|
|
@ -1508,13 +1510,15 @@ export function renderModuleLanding(opts: ModuleLandingOptions): string {
|
||||||
<html lang="en">
|
<html lang="en">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="utf-8">
|
<meta charset="utf-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover">
|
||||||
<link rel="icon" type="image/png" href="/favicon.png">
|
<link rel="icon" type="image/png" href="/favicon.png">
|
||||||
<link rel="manifest" href="/manifest.json">
|
<link rel="manifest" href="/manifest.json">
|
||||||
<meta name="theme-color" content="#0f172a" media="(prefers-color-scheme: dark)">
|
<meta name="theme-color" content="#0f172a" media="(prefers-color-scheme: dark)">
|
||||||
<meta name="theme-color" content="#f5f5f0" media="(prefers-color-scheme: light)">
|
<meta name="theme-color" content="#f5f5f0" media="(prefers-color-scheme: light)">
|
||||||
<link rel="apple-touch-icon" href="/icons/apple-touch-icon.png">
|
<link rel="apple-touch-icon" href="/icons/apple-touch-icon.png">
|
||||||
|
<meta name="mobile-web-app-capable" content="yes">
|
||||||
<meta name="apple-mobile-web-app-capable" content="yes">
|
<meta name="apple-mobile-web-app-capable" content="yes">
|
||||||
|
<meta name="apple-mobile-web-app-title" content="rSpace">
|
||||||
<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);var b=localStorage.getItem('canvas-bg')||'grid';document.documentElement.setAttribute('data-canvas-bg',b)})()</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>
|
||||||
|
|
@ -1851,13 +1855,15 @@ export function renderSubPageInfo(opts: SubPageInfoOptions): string {
|
||||||
<html lang="en">
|
<html lang="en">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="utf-8">
|
<meta charset="utf-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover">
|
||||||
<link rel="icon" type="image/png" href="/favicon.png">
|
<link rel="icon" type="image/png" href="/favicon.png">
|
||||||
<link rel="manifest" href="/manifest.json">
|
<link rel="manifest" href="/manifest.json">
|
||||||
<meta name="theme-color" content="#0f172a" media="(prefers-color-scheme: dark)">
|
<meta name="theme-color" content="#0f172a" media="(prefers-color-scheme: dark)">
|
||||||
<meta name="theme-color" content="#f5f5f0" media="(prefers-color-scheme: light)">
|
<meta name="theme-color" content="#f5f5f0" media="(prefers-color-scheme: light)">
|
||||||
<link rel="apple-touch-icon" href="/icons/apple-touch-icon.png">
|
<link rel="apple-touch-icon" href="/icons/apple-touch-icon.png">
|
||||||
|
<meta name="mobile-web-app-capable" content="yes">
|
||||||
<meta name="apple-mobile-web-app-capable" content="yes">
|
<meta name="apple-mobile-web-app-capable" content="yes">
|
||||||
|
<meta name="apple-mobile-web-app-title" content="rSpace">
|
||||||
<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);var b=localStorage.getItem('canvas-bg')||'grid';document.documentElement.setAttribute('data-canvas-bg',b)})()</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>
|
||||||
|
|
|
||||||
|
|
@ -2,8 +2,16 @@
|
||||||
<html lang="en">
|
<html lang="en">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8" />
|
<meta charset="UTF-8" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover" />
|
||||||
<link rel="icon" type="image/png" href="/favicon.png" />
|
<link rel="icon" type="image/png" href="/favicon.png" />
|
||||||
|
<link rel="manifest" href="/manifest.json">
|
||||||
|
<meta name="theme-color" content="#0f172a" media="(prefers-color-scheme: dark)">
|
||||||
|
<meta name="theme-color" content="#f5f5f0" media="(prefers-color-scheme: light)">
|
||||||
|
<link rel="apple-touch-icon" href="/icons/apple-touch-icon.png">
|
||||||
|
<meta name="mobile-web-app-capable" content="yes">
|
||||||
|
<meta name="apple-mobile-web-app-capable" content="yes">
|
||||||
|
<meta name="apple-mobile-web-app-title" content="rSpace">
|
||||||
|
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent">
|
||||||
<title>Admin Dashboard — rSpace</title>
|
<title>Admin Dashboard — rSpace</title>
|
||||||
<style>
|
<style>
|
||||||
* {
|
* {
|
||||||
|
|
|
||||||
|
|
@ -2,13 +2,15 @@
|
||||||
<html lang="en">
|
<html lang="en">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8" />
|
<meta charset="UTF-8" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover" />
|
||||||
<link rel="icon" type="image/png" href="/favicon.png" />
|
<link rel="icon" type="image/png" href="/favicon.png" />
|
||||||
<link rel="manifest" href="/manifest.json">
|
<link rel="manifest" href="/manifest.json">
|
||||||
<meta name="theme-color" content="#0f172a" media="(prefers-color-scheme: dark)">
|
<meta name="theme-color" content="#0f172a" media="(prefers-color-scheme: dark)">
|
||||||
<meta name="theme-color" content="#f5f5f0" media="(prefers-color-scheme: light)">
|
<meta name="theme-color" content="#f5f5f0" media="(prefers-color-scheme: light)">
|
||||||
<link rel="apple-touch-icon" href="/icons/apple-touch-icon.png">
|
<link rel="apple-touch-icon" href="/icons/apple-touch-icon.png">
|
||||||
|
<meta name="mobile-web-app-capable" content="yes">
|
||||||
<meta name="apple-mobile-web-app-capable" content="yes">
|
<meta name="apple-mobile-web-app-capable" content="yes">
|
||||||
|
<meta name="apple-mobile-web-app-title" content="rSpace">
|
||||||
<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);var b=localStorage.getItem('canvas-bg')||'grid';document.documentElement.setAttribute('data-canvas-bg',b)})()</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>
|
||||||
|
|
|
||||||
|
|
@ -2,13 +2,15 @@
|
||||||
<html lang="en">
|
<html lang="en">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8" />
|
<meta charset="UTF-8" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover" />
|
||||||
<link rel="icon" type="image/png" href="/favicon.png" />
|
<link rel="icon" type="image/png" href="/favicon.png" />
|
||||||
<link rel="manifest" href="/manifest.json">
|
<link rel="manifest" href="/manifest.json">
|
||||||
<meta name="theme-color" content="#0f172a" media="(prefers-color-scheme: dark)">
|
<meta name="theme-color" content="#0f172a" media="(prefers-color-scheme: dark)">
|
||||||
<meta name="theme-color" content="#f5f5f0" media="(prefers-color-scheme: light)">
|
<meta name="theme-color" content="#f5f5f0" media="(prefers-color-scheme: light)">
|
||||||
<link rel="apple-touch-icon" href="/icons/apple-touch-icon.png">
|
<link rel="apple-touch-icon" href="/icons/apple-touch-icon.png">
|
||||||
|
<meta name="mobile-web-app-capable" content="yes">
|
||||||
<meta name="apple-mobile-web-app-capable" content="yes">
|
<meta name="apple-mobile-web-app-capable" content="yes">
|
||||||
|
<meta name="apple-mobile-web-app-title" content="rSpace">
|
||||||
<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);var b=localStorage.getItem('canvas-bg')||'grid';document.documentElement.setAttribute('data-canvas-bg',b)})()</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>
|
||||||
|
|
|
||||||
|
|
@ -2,13 +2,15 @@
|
||||||
<html lang="en">
|
<html lang="en">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8" />
|
<meta charset="UTF-8" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover" />
|
||||||
<link rel="icon" type="image/png" href="/favicon.png" />
|
<link rel="icon" type="image/png" href="/favicon.png" />
|
||||||
<link rel="manifest" href="/manifest.json">
|
<link rel="manifest" href="/manifest.json">
|
||||||
<meta name="theme-color" content="#0f172a" media="(prefers-color-scheme: dark)">
|
<meta name="theme-color" content="#0f172a" media="(prefers-color-scheme: dark)">
|
||||||
<meta name="theme-color" content="#f5f5f0" media="(prefers-color-scheme: light)">
|
<meta name="theme-color" content="#f5f5f0" media="(prefers-color-scheme: light)">
|
||||||
<link rel="apple-touch-icon" href="/icons/apple-touch-icon.png">
|
<link rel="apple-touch-icon" href="/icons/apple-touch-icon.png">
|
||||||
|
<meta name="mobile-web-app-capable" content="yes">
|
||||||
<meta name="apple-mobile-web-app-capable" content="yes">
|
<meta name="apple-mobile-web-app-capable" content="yes">
|
||||||
|
<meta name="apple-mobile-web-app-title" content="rSpace">
|
||||||
<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>(you)rSpace — Collaborative Community Spaces</title>
|
<title>(you)rSpace — Collaborative Community Spaces</title>
|
||||||
<style>
|
<style>
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,7 @@ body {
|
||||||
min-height: 100vh;
|
min-height: 100vh;
|
||||||
background: var(--rs-bg-page);
|
background: var(--rs-bg-page);
|
||||||
color: var(--rs-text-primary);
|
color: var(--rs-text-primary);
|
||||||
|
padding-bottom: env(safe-area-inset-bottom);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ── Header bar ── */
|
/* ── Header bar ── */
|
||||||
|
|
@ -18,7 +19,8 @@ body {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
padding: 0 16px;
|
padding: 0 max(16px, env(safe-area-inset-right)) 0 max(16px, env(safe-area-inset-left));
|
||||||
|
padding-top: env(safe-area-inset-top);
|
||||||
z-index: 9999;
|
z-index: 9999;
|
||||||
border-bottom: 1px solid var(--rs-glass-border);
|
border-bottom: 1px solid var(--rs-glass-border);
|
||||||
color: var(--rs-text-primary);
|
color: var(--rs-text-primary);
|
||||||
|
|
@ -357,7 +359,7 @@ body.rstack-sidebar-open #toolbar {
|
||||||
the header wrap naturally to two rows. */
|
the header wrap naturally to two rows. */
|
||||||
.rstack-header {
|
.rstack-header {
|
||||||
position: sticky;
|
position: sticky;
|
||||||
padding: 6px 12px;
|
padding: calc(6px + env(safe-area-inset-top)) max(12px, env(safe-area-inset-right)) 6px max(12px, env(safe-area-inset-left));
|
||||||
height: auto;
|
height: auto;
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
gap: 0;
|
gap: 0;
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue