|
|
|
|
@ -105,6 +105,69 @@
|
|
|
|
|
color: white;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/* Popout panel — renders group tools to the right of toolbar */
|
|
|
|
|
#toolbar-panel {
|
|
|
|
|
position: fixed;
|
|
|
|
|
top: 108px;
|
|
|
|
|
left: calc(68px + 12px + 8px);
|
|
|
|
|
min-width: 180px;
|
|
|
|
|
max-height: calc(100vh - 130px);
|
|
|
|
|
background: white;
|
|
|
|
|
border-radius: 12px;
|
|
|
|
|
box-shadow: 0 8px 30px rgba(0, 0, 0, 0.18);
|
|
|
|
|
z-index: 1001;
|
|
|
|
|
display: none;
|
|
|
|
|
flex-direction: column;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#toolbar-panel.panel-open {
|
|
|
|
|
display: flex;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#toolbar-panel-header {
|
|
|
|
|
padding: 10px 14px;
|
|
|
|
|
border-bottom: 1px solid #e2e8f0;
|
|
|
|
|
font-size: 12px;
|
|
|
|
|
font-weight: 600;
|
|
|
|
|
color: #64748b;
|
|
|
|
|
text-transform: uppercase;
|
|
|
|
|
letter-spacing: 0.5px;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#toolbar-panel-body {
|
|
|
|
|
padding: 6px;
|
|
|
|
|
display: flex;
|
|
|
|
|
flex-direction: column;
|
|
|
|
|
gap: 2px;
|
|
|
|
|
overflow-y: auto;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#toolbar-panel-body button {
|
|
|
|
|
padding: 8px 12px;
|
|
|
|
|
border: none;
|
|
|
|
|
border-radius: 6px;
|
|
|
|
|
background: transparent;
|
|
|
|
|
cursor: pointer;
|
|
|
|
|
font-size: 13px;
|
|
|
|
|
text-align: left;
|
|
|
|
|
white-space: nowrap;
|
|
|
|
|
transition: background 0.15s;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#toolbar-panel-body button:hover {
|
|
|
|
|
background: #f1f5f9;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#toolbar-panel-body button.active {
|
|
|
|
|
background: #14b8a6;
|
|
|
|
|
color: white;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/* Hide inline dropdowns — all rendering goes through the panel */
|
|
|
|
|
.toolbar-group.open > .toolbar-dropdown {
|
|
|
|
|
display: none !important;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/* Separator between sections */
|
|
|
|
|
.toolbar-sep {
|
|
|
|
|
width: 100%;
|
|
|
|
|
@ -586,6 +649,16 @@
|
|
|
|
|
#memory-panel {
|
|
|
|
|
max-width: calc(100vw - 32px);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/* Mobile: panel slides up from bottom as sheet */
|
|
|
|
|
#toolbar-panel {
|
|
|
|
|
top: auto;
|
|
|
|
|
bottom: 90px;
|
|
|
|
|
left: 8px;
|
|
|
|
|
right: 8px;
|
|
|
|
|
border-radius: 16px;
|
|
|
|
|
max-height: 50vh;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
</style>
|
|
|
|
|
<link rel="stylesheet" href="/shell.css">
|
|
|
|
|
@ -619,6 +692,8 @@
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<div id="toolbar">
|
|
|
|
|
<button id="quick-add" title="Add rApp" style="background:#14b8a6;color:white;font-size:18px;font-weight:700;padding:6px 10px;border:none;border-radius:8px;cursor:pointer;text-align:center;line-height:1;">+</button>
|
|
|
|
|
<span class="toolbar-sep"></span>
|
|
|
|
|
<div class="toolbar-group">
|
|
|
|
|
<button class="toolbar-group-toggle">✏️ Draw</button>
|
|
|
|
|
<div class="toolbar-dropdown">
|
|
|
|
|
@ -746,6 +821,11 @@
|
|
|
|
|
<button id="toolbar-collapse" title="Minimize toolbar">···</button>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<div id="toolbar-panel">
|
|
|
|
|
<div id="toolbar-panel-header"></div>
|
|
|
|
|
<div id="toolbar-panel-body"></div>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<div id="memory-panel">
|
|
|
|
|
<div id="memory-panel-header">
|
|
|
|
|
<h3>💭 Memory</h3>
|
|
|
|
|
@ -1415,10 +1495,10 @@
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
shape.id = data.id;
|
|
|
|
|
shape.x = data.x || 100;
|
|
|
|
|
shape.y = data.y || 100;
|
|
|
|
|
shape.width = data.width || 300;
|
|
|
|
|
shape.height = data.height || 200;
|
|
|
|
|
shape.x = data.x ?? 100;
|
|
|
|
|
shape.y = data.y ?? 100;
|
|
|
|
|
shape.width = data.width ?? 300;
|
|
|
|
|
shape.height = data.height ?? 200;
|
|
|
|
|
if (data.rotation) shape.rotation = data.rotation;
|
|
|
|
|
|
|
|
|
|
return shape;
|
|
|
|
|
@ -1521,11 +1601,18 @@
|
|
|
|
|
.map(el => ({ x: el.x, y: el.y, width: el.width, height: el.height }));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Find a free position near the viewport center that doesn't overlap existing shapes
|
|
|
|
|
function findFreePosition(width, height) {
|
|
|
|
|
const center = getViewportCenter();
|
|
|
|
|
const candidateX = center.x - width / 2;
|
|
|
|
|
const candidateY = center.y - height / 2;
|
|
|
|
|
// Find a free position that doesn't overlap existing shapes.
|
|
|
|
|
// If preferX/preferY are provided, use that as the anchor; otherwise use viewport center.
|
|
|
|
|
function findFreePosition(width, height, preferX, preferY) {
|
|
|
|
|
let candidateX, candidateY;
|
|
|
|
|
if (preferX !== undefined && preferY !== undefined) {
|
|
|
|
|
candidateX = preferX - width / 2;
|
|
|
|
|
candidateY = preferY - height / 2;
|
|
|
|
|
} else {
|
|
|
|
|
const center = getViewportCenter();
|
|
|
|
|
candidateX = center.x - width / 2;
|
|
|
|
|
candidateY = center.y - height / 2;
|
|
|
|
|
}
|
|
|
|
|
const gap = 20;
|
|
|
|
|
const existing = getExistingShapeRects();
|
|
|
|
|
|
|
|
|
|
@ -1569,15 +1656,62 @@
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ── Pending tool state for click-to-place ──
|
|
|
|
|
let pendingTool = null; // { tagName, props }
|
|
|
|
|
let ghostEl = null;
|
|
|
|
|
|
|
|
|
|
function setPendingTool(tagName, props = {}) {
|
|
|
|
|
pendingTool = { tagName, props };
|
|
|
|
|
canvas.style.cursor = "crosshair";
|
|
|
|
|
|
|
|
|
|
// Create ghost outline
|
|
|
|
|
if (ghostEl) ghostEl.remove();
|
|
|
|
|
const defaults = SHAPE_DEFAULTS[tagName] || { width: 300, height: 200 };
|
|
|
|
|
ghostEl = document.createElement("div");
|
|
|
|
|
ghostEl.style.cssText = `
|
|
|
|
|
position: fixed; pointer-events: none; z-index: 9999;
|
|
|
|
|
width: ${defaults.width * scale}px; height: ${defaults.height * scale}px;
|
|
|
|
|
border: 2px dashed #14b8a6; border-radius: 8px;
|
|
|
|
|
background: rgba(20, 184, 166, 0.06);
|
|
|
|
|
transform: translate(-50%, -50%);
|
|
|
|
|
transition: width 0.1s, height 0.1s;
|
|
|
|
|
`;
|
|
|
|
|
document.body.appendChild(ghostEl);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function clearPendingTool() {
|
|
|
|
|
pendingTool = null;
|
|
|
|
|
canvas.style.cursor = "";
|
|
|
|
|
if (ghostEl) { ghostEl.remove(); ghostEl = null; }
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Track ghost position
|
|
|
|
|
document.addEventListener("mousemove", (e) => {
|
|
|
|
|
if (ghostEl) {
|
|
|
|
|
ghostEl.style.left = e.clientX + "px";
|
|
|
|
|
ghostEl.style.top = e.clientY + "px";
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// ESC clears pending tool
|
|
|
|
|
document.addEventListener("keydown", (e) => {
|
|
|
|
|
if (e.key === "Escape" && pendingTool) {
|
|
|
|
|
clearPendingTool();
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// Create a shape, position it without overlapping others, add to canvas, and register for sync
|
|
|
|
|
function newShape(tagName, props = {}) {
|
|
|
|
|
// atPosition: optional { x, y } in canvas coordinates to place near
|
|
|
|
|
function newShape(tagName, props = {}, atPosition) {
|
|
|
|
|
const id = `shape-${Date.now()}-${++shapeCounter}`;
|
|
|
|
|
const defaults = SHAPE_DEFAULTS[tagName] || { width: 300, height: 200 };
|
|
|
|
|
|
|
|
|
|
const shape = document.createElement(tagName);
|
|
|
|
|
shape.id = id;
|
|
|
|
|
|
|
|
|
|
const pos = findFreePosition(defaults.width, defaults.height);
|
|
|
|
|
const pos = atPosition
|
|
|
|
|
? findFreePosition(defaults.width, defaults.height, atPosition.x, atPosition.y)
|
|
|
|
|
: findFreePosition(defaults.width, defaults.height);
|
|
|
|
|
shape.x = pos.x;
|
|
|
|
|
shape.y = pos.y;
|
|
|
|
|
shape.width = defaults.width;
|
|
|
|
|
@ -1606,99 +1740,95 @@
|
|
|
|
|
window.__canvasApi = { newShape, findFreePosition, SHAPE_DEFAULTS, setupShapeEventListeners, sync, canvasContent };
|
|
|
|
|
installSelectionTransforms();
|
|
|
|
|
|
|
|
|
|
// Toolbar button handlers
|
|
|
|
|
// Toolbar button handlers — set pending tool for click-to-place
|
|
|
|
|
document.getElementById("new-markdown").addEventListener("click", () => {
|
|
|
|
|
newShape("folk-markdown", { content: "# New Note\n\nStart typing..." });
|
|
|
|
|
setPendingTool("folk-markdown", { content: "# New Note\n\nStart typing..." });
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
document.getElementById("new-wrapper").addEventListener("click", () => {
|
|
|
|
|
const colors = ["#14b8a6", "#8b5cf6", "#f59e0b", "#ef4444", "#3b82f6", "#22c55e"];
|
|
|
|
|
const icons = ["📋", "💡", "📌", "🔗", "📁", "⭐"];
|
|
|
|
|
const shape = newShape("folk-wrapper", {
|
|
|
|
|
setPendingTool("folk-wrapper", {
|
|
|
|
|
title: "New Card",
|
|
|
|
|
icon: icons[Math.floor(Math.random() * icons.length)],
|
|
|
|
|
primaryColor: colors[Math.floor(Math.random() * colors.length)],
|
|
|
|
|
__postCreate: (shape) => {
|
|
|
|
|
const content = document.createElement("div");
|
|
|
|
|
content.style.padding = "16px";
|
|
|
|
|
content.style.color = "#374151";
|
|
|
|
|
content.innerHTML = "<p>Click to edit this card...</p>";
|
|
|
|
|
shape.appendChild(content);
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
if (shape) {
|
|
|
|
|
const content = document.createElement("div");
|
|
|
|
|
content.style.padding = "16px";
|
|
|
|
|
content.style.color = "#374151";
|
|
|
|
|
content.innerHTML = "<p>Click to edit this card...</p>";
|
|
|
|
|
shape.appendChild(content);
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
document.getElementById("new-slide").addEventListener("click", () => {
|
|
|
|
|
newShape("folk-slide", { label: `Slide ${shapeCounter}` });
|
|
|
|
|
setPendingTool("folk-slide", { label: `Slide ${shapeCounter}` });
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
document.getElementById("new-chat").addEventListener("click", () => {
|
|
|
|
|
const id = `shape-${Date.now()}-${shapeCounter}`;
|
|
|
|
|
newShape("folk-chat", { roomId: `room-${id}` });
|
|
|
|
|
setPendingTool("folk-chat", { roomId: `room-${id}` });
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
document.getElementById("new-piano").addEventListener("click", () => newShape("folk-piano"));
|
|
|
|
|
document.getElementById("new-embed").addEventListener("click", () => newShape("folk-embed"));
|
|
|
|
|
document.getElementById("new-calendar").addEventListener("click", () => newShape("folk-calendar"));
|
|
|
|
|
document.getElementById("new-map").addEventListener("click", () => newShape("folk-map"));
|
|
|
|
|
document.getElementById("new-image-gen").addEventListener("click", () => newShape("folk-image-gen"));
|
|
|
|
|
document.getElementById("new-video-gen").addEventListener("click", () => newShape("folk-video-gen"));
|
|
|
|
|
document.getElementById("new-prompt").addEventListener("click", () => newShape("folk-prompt"));
|
|
|
|
|
document.getElementById("new-transcription").addEventListener("click", () => newShape("folk-transcription"));
|
|
|
|
|
document.getElementById("new-video-chat").addEventListener("click", () => newShape("folk-video-chat"));
|
|
|
|
|
document.getElementById("new-obs-note").addEventListener("click", () => newShape("folk-obs-note"));
|
|
|
|
|
document.getElementById("new-workflow").addEventListener("click", () => newShape("folk-workflow-block"));
|
|
|
|
|
document.getElementById("new-splat").addEventListener("click", () => newShape("folk-splat"));
|
|
|
|
|
document.getElementById("new-blender").addEventListener("click", () => newShape("folk-blender"));
|
|
|
|
|
document.getElementById("new-drawfast").addEventListener("click", () => newShape("folk-drawfast"));
|
|
|
|
|
document.getElementById("new-freecad").addEventListener("click", () => newShape("folk-freecad"));
|
|
|
|
|
document.getElementById("new-kicad").addEventListener("click", () => newShape("folk-kicad"));
|
|
|
|
|
document.getElementById("new-piano").addEventListener("click", () => setPendingTool("folk-piano"));
|
|
|
|
|
document.getElementById("new-embed").addEventListener("click", () => setPendingTool("folk-embed"));
|
|
|
|
|
document.getElementById("new-calendar").addEventListener("click", () => setPendingTool("folk-calendar"));
|
|
|
|
|
document.getElementById("new-map").addEventListener("click", () => setPendingTool("folk-map"));
|
|
|
|
|
document.getElementById("new-image-gen").addEventListener("click", () => setPendingTool("folk-image-gen"));
|
|
|
|
|
document.getElementById("new-video-gen").addEventListener("click", () => setPendingTool("folk-video-gen"));
|
|
|
|
|
document.getElementById("new-prompt").addEventListener("click", () => setPendingTool("folk-prompt"));
|
|
|
|
|
document.getElementById("new-transcription").addEventListener("click", () => setPendingTool("folk-transcription"));
|
|
|
|
|
document.getElementById("new-video-chat").addEventListener("click", () => setPendingTool("folk-video-chat"));
|
|
|
|
|
document.getElementById("new-obs-note").addEventListener("click", () => setPendingTool("folk-obs-note"));
|
|
|
|
|
document.getElementById("new-workflow").addEventListener("click", () => setPendingTool("folk-workflow-block"));
|
|
|
|
|
document.getElementById("new-splat").addEventListener("click", () => setPendingTool("folk-splat"));
|
|
|
|
|
document.getElementById("new-blender").addEventListener("click", () => setPendingTool("folk-blender"));
|
|
|
|
|
document.getElementById("new-drawfast").addEventListener("click", () => setPendingTool("folk-drawfast"));
|
|
|
|
|
document.getElementById("new-freecad").addEventListener("click", () => setPendingTool("folk-freecad"));
|
|
|
|
|
document.getElementById("new-kicad").addEventListener("click", () => setPendingTool("folk-kicad"));
|
|
|
|
|
document.getElementById("new-google-item").addEventListener("click", () => {
|
|
|
|
|
newShape("folk-google-item", { service: "drive", title: "New Google Item" });
|
|
|
|
|
setPendingTool("folk-google-item", { service: "drive", title: "New Google Item" });
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// Trip planning components
|
|
|
|
|
document.getElementById("new-itinerary").addEventListener("click", () => newShape("folk-itinerary"));
|
|
|
|
|
document.getElementById("new-destination").addEventListener("click", () => newShape("folk-destination"));
|
|
|
|
|
document.getElementById("new-budget").addEventListener("click", () => newShape("folk-budget"));
|
|
|
|
|
document.getElementById("new-packing-list").addEventListener("click", () => newShape("folk-packing-list"));
|
|
|
|
|
document.getElementById("new-booking").addEventListener("click", () => newShape("folk-booking"));
|
|
|
|
|
document.getElementById("new-itinerary").addEventListener("click", () => setPendingTool("folk-itinerary"));
|
|
|
|
|
document.getElementById("new-destination").addEventListener("click", () => setPendingTool("folk-destination"));
|
|
|
|
|
document.getElementById("new-budget").addEventListener("click", () => setPendingTool("folk-budget"));
|
|
|
|
|
document.getElementById("new-packing-list").addEventListener("click", () => setPendingTool("folk-packing-list"));
|
|
|
|
|
document.getElementById("new-booking").addEventListener("click", () => setPendingTool("folk-booking"));
|
|
|
|
|
|
|
|
|
|
// Token creation - creates a mint + ledger pair with connecting arrow
|
|
|
|
|
document.getElementById("new-token").addEventListener("click", () => {
|
|
|
|
|
const mint = newShape("folk-token-mint", {
|
|
|
|
|
setPendingTool("folk-token-mint", {
|
|
|
|
|
tokenName: "New Token",
|
|
|
|
|
tokenSymbol: "TKN",
|
|
|
|
|
totalSupply: 1000,
|
|
|
|
|
tokenColor: "#8b5cf6",
|
|
|
|
|
tokenIcon: "🪙",
|
|
|
|
|
createdAt: new Date().toISOString(),
|
|
|
|
|
});
|
|
|
|
|
if (mint) {
|
|
|
|
|
const ledger = newShape("folk-token-ledger", {
|
|
|
|
|
mintId: mint.id,
|
|
|
|
|
entries: [],
|
|
|
|
|
});
|
|
|
|
|
if (ledger) {
|
|
|
|
|
// Position ledger to the right of mint
|
|
|
|
|
ledger.x = mint.x + mint.width + 60;
|
|
|
|
|
ledger.y = mint.y;
|
|
|
|
|
// Connect with an arrow
|
|
|
|
|
const arrowId = `arrow-${Date.now()}-${++shapeCounter}`;
|
|
|
|
|
const arrow = document.createElement("folk-arrow");
|
|
|
|
|
arrow.id = arrowId;
|
|
|
|
|
arrow.sourceId = mint.id;
|
|
|
|
|
arrow.targetId = ledger.id;
|
|
|
|
|
arrow.color = "#8b5cf6";
|
|
|
|
|
canvasContent.appendChild(arrow);
|
|
|
|
|
sync.registerShape(arrow);
|
|
|
|
|
__postCreate: (mint) => {
|
|
|
|
|
const ledger = newShape("folk-token-ledger", {
|
|
|
|
|
mintId: mint.id,
|
|
|
|
|
entries: [],
|
|
|
|
|
}, { x: mint.x + mint.width + 60 + 150, y: mint.y + mint.height / 2 });
|
|
|
|
|
if (ledger) {
|
|
|
|
|
const arrowId = `arrow-${Date.now()}-${++shapeCounter}`;
|
|
|
|
|
const arrow = document.createElement("folk-arrow");
|
|
|
|
|
arrow.id = arrowId;
|
|
|
|
|
arrow.sourceId = mint.id;
|
|
|
|
|
arrow.targetId = ledger.id;
|
|
|
|
|
arrow.color = "#8b5cf6";
|
|
|
|
|
canvasContent.appendChild(arrow);
|
|
|
|
|
sync.registerShape(arrow);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// Decision/choice components
|
|
|
|
|
document.getElementById("new-choice-vote").addEventListener("click", () => {
|
|
|
|
|
newShape("folk-choice-vote", {
|
|
|
|
|
setPendingTool("folk-choice-vote", {
|
|
|
|
|
title: "Quick Poll",
|
|
|
|
|
options: [
|
|
|
|
|
{ id: "opt-1", label: "Option A", color: "#3b82f6" },
|
|
|
|
|
@ -1712,7 +1842,7 @@
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
document.getElementById("new-choice-rank").addEventListener("click", () => {
|
|
|
|
|
newShape("folk-choice-rank", {
|
|
|
|
|
setPendingTool("folk-choice-rank", {
|
|
|
|
|
title: "Rank These",
|
|
|
|
|
options: [
|
|
|
|
|
{ id: "opt-1", label: "Option A" },
|
|
|
|
|
@ -1724,7 +1854,7 @@
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
document.getElementById("new-choice-spider").addEventListener("click", () => {
|
|
|
|
|
newShape("folk-choice-spider", {
|
|
|
|
|
setPendingTool("folk-choice-spider", {
|
|
|
|
|
title: "Evaluate Options",
|
|
|
|
|
options: [
|
|
|
|
|
{ id: "opt-1", label: "Option A" },
|
|
|
|
|
@ -1742,7 +1872,7 @@
|
|
|
|
|
|
|
|
|
|
// Social media post
|
|
|
|
|
document.getElementById("new-social-post").addEventListener("click", () => {
|
|
|
|
|
newShape("folk-social-post", {
|
|
|
|
|
setPendingTool("folk-social-post", {
|
|
|
|
|
platform: "x",
|
|
|
|
|
postType: "text",
|
|
|
|
|
content: "Write your post content here...",
|
|
|
|
|
@ -1775,7 +1905,7 @@
|
|
|
|
|
const btn = document.getElementById(app.btnId);
|
|
|
|
|
if (btn) {
|
|
|
|
|
btn.addEventListener("click", () => {
|
|
|
|
|
newShape("folk-rapp", { moduleId: app.moduleId, spaceSlug: communitySlug });
|
|
|
|
|
setPendingTool("folk-rapp", { moduleId: app.moduleId, spaceSlug: communitySlug });
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
@ -2180,8 +2310,22 @@
|
|
|
|
|
const toolbarEl = document.getElementById("toolbar");
|
|
|
|
|
|
|
|
|
|
mobileMenuBtn.addEventListener("click", () => {
|
|
|
|
|
const isOpen = toolbarEl.classList.toggle("mobile-open");
|
|
|
|
|
mobileMenuBtn.textContent = isOpen ? "✕" : "✚";
|
|
|
|
|
// On mobile, first tap opens rApps group in the popout panel
|
|
|
|
|
const rAppsGroup = toolbarEl.querySelector(".toolbar-group:has(#embed-notes)")
|
|
|
|
|
|| [...toolbarEl.querySelectorAll(".toolbar-group")].find(g =>
|
|
|
|
|
g.querySelector(".toolbar-group-toggle")?.textContent.includes("rApps"));
|
|
|
|
|
if (rAppsGroup && !toolbarEl.classList.contains("mobile-open")) {
|
|
|
|
|
toolbarEl.classList.add("mobile-open");
|
|
|
|
|
mobileMenuBtn.textContent = "✕";
|
|
|
|
|
// Auto-open the rApps panel
|
|
|
|
|
setTimeout(() => {
|
|
|
|
|
if (typeof openToolbarPanel === "function") openToolbarPanel(rAppsGroup);
|
|
|
|
|
}, 50);
|
|
|
|
|
} else {
|
|
|
|
|
const isOpen = toolbarEl.classList.toggle("mobile-open");
|
|
|
|
|
mobileMenuBtn.textContent = isOpen ? "✕" : "✚";
|
|
|
|
|
if (!isOpen && typeof closeToolbarPanel === "function") closeToolbarPanel();
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// Auto-close toolbar after tapping a shape-creation button on mobile
|
|
|
|
|
@ -2199,30 +2343,67 @@
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// Dropdown group toggles
|
|
|
|
|
// Popout panel references
|
|
|
|
|
const toolbarPanel = document.getElementById("toolbar-panel");
|
|
|
|
|
const toolbarPanelHeader = document.getElementById("toolbar-panel-header");
|
|
|
|
|
const toolbarPanelBody = document.getElementById("toolbar-panel-body");
|
|
|
|
|
let activeToolbarGroup = null;
|
|
|
|
|
|
|
|
|
|
function openToolbarPanel(group) {
|
|
|
|
|
const toggle = group.querySelector(".toolbar-group-toggle");
|
|
|
|
|
const dropdown = group.querySelector(".toolbar-dropdown");
|
|
|
|
|
if (!dropdown) return;
|
|
|
|
|
|
|
|
|
|
// Set header text from the toggle button
|
|
|
|
|
toolbarPanelHeader.textContent = toggle.textContent.trim();
|
|
|
|
|
|
|
|
|
|
// Clone dropdown buttons into the panel body
|
|
|
|
|
toolbarPanelBody.innerHTML = "";
|
|
|
|
|
for (const btn of dropdown.querySelectorAll("button")) {
|
|
|
|
|
const clone = btn.cloneNode(true);
|
|
|
|
|
// Forward click to the original button
|
|
|
|
|
clone.addEventListener("click", (e) => {
|
|
|
|
|
e.stopPropagation();
|
|
|
|
|
btn.click();
|
|
|
|
|
// Close panel after tool is selected (unless it's a whiteboard toggle)
|
|
|
|
|
const keepOpen = ["wb-pencil", "wb-rect", "wb-circle", "wb-line", "wb-eraser"];
|
|
|
|
|
if (!keepOpen.includes(btn.id)) {
|
|
|
|
|
closeToolbarPanel();
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
toolbarPanelBody.appendChild(clone);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Mark group as active
|
|
|
|
|
toolbarEl.querySelectorAll(".toolbar-group").forEach(g => g.classList.remove("open"));
|
|
|
|
|
group.classList.add("open");
|
|
|
|
|
activeToolbarGroup = group;
|
|
|
|
|
toolbarPanel.classList.add("panel-open");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function closeToolbarPanel() {
|
|
|
|
|
toolbarPanel.classList.remove("panel-open");
|
|
|
|
|
toolbarEl.querySelectorAll(".toolbar-group").forEach(g => g.classList.remove("open"));
|
|
|
|
|
activeToolbarGroup = null;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Dropdown group toggles → popout panel
|
|
|
|
|
toolbarEl.querySelectorAll(".toolbar-group-toggle").forEach(toggle => {
|
|
|
|
|
toggle.addEventListener("click", (e) => {
|
|
|
|
|
e.stopPropagation();
|
|
|
|
|
const group = toggle.closest(".toolbar-group");
|
|
|
|
|
const wasOpen = group.classList.contains("open");
|
|
|
|
|
// Close all other groups
|
|
|
|
|
toolbarEl.querySelectorAll(".toolbar-group.open").forEach(g => g.classList.remove("open"));
|
|
|
|
|
// Toggle this one
|
|
|
|
|
if (!wasOpen) group.classList.add("open");
|
|
|
|
|
if (activeToolbarGroup === group) {
|
|
|
|
|
closeToolbarPanel();
|
|
|
|
|
} else {
|
|
|
|
|
openToolbarPanel(group);
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// Close dropdowns when clicking a tool inside one
|
|
|
|
|
toolbarEl.querySelectorAll(".toolbar-dropdown button").forEach(btn => {
|
|
|
|
|
btn.addEventListener("click", () => {
|
|
|
|
|
toolbarEl.querySelectorAll(".toolbar-group.open").forEach(g => g.classList.remove("open"));
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// Close dropdowns when clicking outside
|
|
|
|
|
// Close panel when clicking outside toolbar + panel
|
|
|
|
|
document.addEventListener("click", (e) => {
|
|
|
|
|
if (!e.target.closest("#toolbar")) {
|
|
|
|
|
toolbarEl.querySelectorAll(".toolbar-group.open").forEach(g => g.classList.remove("open"));
|
|
|
|
|
if (!e.target.closest("#toolbar") && !e.target.closest("#toolbar-panel")) {
|
|
|
|
|
closeToolbarPanel();
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
@ -2317,9 +2498,31 @@
|
|
|
|
|
canvas.addEventListener("pointerdown", (e) => {
|
|
|
|
|
if (e.target !== canvas && e.target !== canvasContent) return;
|
|
|
|
|
if (connectMode) return;
|
|
|
|
|
// Clicking canvas background clears MI selection
|
|
|
|
|
|
|
|
|
|
// Click-to-place: if a pending tool is set, place it at the click position
|
|
|
|
|
if (pendingTool) {
|
|
|
|
|
e.preventDefault();
|
|
|
|
|
e.stopPropagation();
|
|
|
|
|
const rect = canvasContent.getBoundingClientRect();
|
|
|
|
|
const canvasX = (e.clientX - rect.left) / scale;
|
|
|
|
|
const canvasY = (e.clientY - rect.top) / scale;
|
|
|
|
|
const { tagName, props } = pendingTool;
|
|
|
|
|
const postCreate = props.__postCreate;
|
|
|
|
|
const cleanProps = { ...props };
|
|
|
|
|
delete cleanProps.__postCreate;
|
|
|
|
|
const shape = newShape(tagName, cleanProps, { x: canvasX, y: canvasY });
|
|
|
|
|
if (shape && postCreate) postCreate(shape);
|
|
|
|
|
clearPendingTool();
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Clicking canvas background clears MI selection and exits any editing shape
|
|
|
|
|
selectedShapeId = null;
|
|
|
|
|
__miCanvasBridge.setSelection([]);
|
|
|
|
|
// Exit edit mode on any currently-editing shape
|
|
|
|
|
canvasContent.querySelectorAll("folk-shape, folk-markdown, folk-wrapper, folk-slide, folk-chat, folk-obs-note, folk-rapp, folk-embed, folk-drawfast, folk-prompt, folk-workflow-block").forEach(el => {
|
|
|
|
|
if (el.exitEditMode) el.exitEditMode();
|
|
|
|
|
});
|
|
|
|
|
isPanning = true;
|
|
|
|
|
panPointerId = e.pointerId;
|
|
|
|
|
panStartX = e.clientX;
|
|
|
|
|
|