Merge branch 'dev'
This commit is contained in:
commit
cce4e8d357
|
|
@ -76,6 +76,16 @@ const styles = css`
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
:host(:state(editing)) .slot-container {
|
||||||
|
pointer-events: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
:host(:state(editing)) {
|
||||||
|
outline: 2px solid #14b8a6;
|
||||||
|
outline-offset: 2px;
|
||||||
|
cursor: text;
|
||||||
|
}
|
||||||
|
|
||||||
::slotted(*) {
|
::slotted(*) {
|
||||||
cursor: default;
|
cursor: default;
|
||||||
pointer-events: auto;
|
pointer-events: auto;
|
||||||
|
|
@ -263,6 +273,37 @@ export class FolkShape extends FolkElement {
|
||||||
: this.#internals.states.delete("highlighted");
|
: this.#internals.states.delete("highlighted");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#editing = false;
|
||||||
|
get editing() {
|
||||||
|
return this.#editing;
|
||||||
|
}
|
||||||
|
|
||||||
|
enterEditMode() {
|
||||||
|
if (this.#editing) return;
|
||||||
|
this.#editing = true;
|
||||||
|
this.#internals.states.add("editing");
|
||||||
|
|
||||||
|
// Find first focusable child and focus it
|
||||||
|
const root = this.renderRoot as ShadowRoot;
|
||||||
|
const focusable = root.querySelector<HTMLElement>(
|
||||||
|
'input, textarea, [contenteditable="true"], select'
|
||||||
|
) ?? this.querySelector<HTMLElement>(
|
||||||
|
'input, textarea, [contenteditable="true"], select'
|
||||||
|
);
|
||||||
|
if (focusable) {
|
||||||
|
focusable.focus();
|
||||||
|
}
|
||||||
|
|
||||||
|
this.dispatchEvent(new CustomEvent("edit-enter"));
|
||||||
|
}
|
||||||
|
|
||||||
|
exitEditMode() {
|
||||||
|
if (!this.#editing) return;
|
||||||
|
this.#editing = false;
|
||||||
|
this.#internals.states.delete("editing");
|
||||||
|
this.dispatchEvent(new CustomEvent("edit-exit"));
|
||||||
|
}
|
||||||
|
|
||||||
override createRenderRoot() {
|
override createRenderRoot() {
|
||||||
const root = super.createRenderRoot();
|
const root = super.createRenderRoot();
|
||||||
|
|
||||||
|
|
@ -272,6 +313,11 @@ export class FolkShape extends FolkElement {
|
||||||
this.addEventListener("touchend", this);
|
this.addEventListener("touchend", this);
|
||||||
this.addEventListener("keydown", this);
|
this.addEventListener("keydown", this);
|
||||||
|
|
||||||
|
this.addEventListener("dblclick", (e: MouseEvent) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
this.enterEditMode();
|
||||||
|
});
|
||||||
|
|
||||||
(root as ShadowRoot).setHTMLUnsafe(
|
(root as ShadowRoot).setHTMLUnsafe(
|
||||||
html`<button part="rotation-top-left" tabindex="-1"></button>
|
html`<button part="rotation-top-left" tabindex="-1"></button>
|
||||||
<button part="rotation-top-right" tabindex="-1"></button>
|
<button part="rotation-top-right" tabindex="-1"></button>
|
||||||
|
|
@ -609,7 +655,7 @@ export class FolkShape extends FolkElement {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* After moving, push this shape away from any overlapping siblings.
|
* After moving, push this shape away from any overlapping siblings.
|
||||||
* Uses the direction of the move to decide which side to slide to.
|
* Resolves by minimum penetration on the axis aligned with movement direction.
|
||||||
*/
|
*/
|
||||||
#resolveOverlaps(dx: number, dy: number) {
|
#resolveOverlaps(dx: number, dy: number) {
|
||||||
const parent = this.parentElement;
|
const parent = this.parentElement;
|
||||||
|
|
@ -624,28 +670,27 @@ export class FolkShape extends FolkElement {
|
||||||
|
|
||||||
const other = { x: sibling.x, y: sibling.y, w: sibling.width, h: sibling.height };
|
const other = { x: sibling.x, y: sibling.y, w: sibling.width, h: sibling.height };
|
||||||
|
|
||||||
// Check overlap (axis-aligned)
|
// Check overlap (with gap buffer)
|
||||||
const overlapX = me.x < other.x + other.w + gap && me.x + me.w + gap > other.x;
|
const overlapX = me.x < other.x + other.w + gap && me.x + me.w + gap > other.x;
|
||||||
const overlapY = me.y < other.y + other.h + gap && me.y + me.h + gap > other.y;
|
const overlapY = me.y < other.y + other.h + gap && me.y + me.h + gap > other.y;
|
||||||
|
|
||||||
if (!overlapX || !overlapY) continue;
|
if (!overlapX || !overlapY) continue;
|
||||||
|
|
||||||
// Compute penetration depths from each side
|
// Distance to clear on each side
|
||||||
const pushRight = (other.x + other.w + gap) - me.x;
|
const clearRight = (other.x + other.w + gap) - me.x; // push me right of other
|
||||||
const pushLeft = me.x + me.w + gap - other.x;
|
const clearLeft = other.x - (me.x + me.w + gap); // push me left of other (negative)
|
||||||
const pushDown = (other.y + other.h + gap) - me.y;
|
const clearDown = (other.y + other.h + gap) - me.y; // push me below other
|
||||||
const pushUp = me.y + me.h + gap - other.y;
|
const clearUp = other.y - (me.y + me.h + gap); // push me above other (negative)
|
||||||
|
|
||||||
// Pick the axis with the smallest penetration, biased by move direction
|
// Pick push direction per axis based on movement direction
|
||||||
const minX = pushRight < pushLeft ? -pushRight : pushLeft;
|
const pushX = dx >= 0 ? clearRight : clearLeft;
|
||||||
const minY = pushDown < pushUp ? -pushDown : pushUp;
|
const pushY = dy >= 0 ? clearDown : clearUp;
|
||||||
|
|
||||||
if (Math.abs(minX) < Math.abs(minY)) {
|
// Apply the axis with the smallest absolute displacement
|
||||||
// Slide horizontally
|
if (Math.abs(pushX) <= Math.abs(pushY)) {
|
||||||
this.#rect.x += dx <= 0 ? -pushLeft : pushRight;
|
this.#rect.x += pushX;
|
||||||
} else {
|
} else {
|
||||||
// Slide vertically
|
this.#rect.y += pushY;
|
||||||
this.#rect.y += dy <= 0 ? -pushUp : pushDown;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
me.x = this.#rect.x;
|
me.x = this.#rect.x;
|
||||||
|
|
|
||||||
|
|
@ -105,6 +105,69 @@
|
||||||
color: white;
|
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 */
|
/* Separator between sections */
|
||||||
.toolbar-sep {
|
.toolbar-sep {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
|
@ -586,6 +649,16 @@
|
||||||
#memory-panel {
|
#memory-panel {
|
||||||
max-width: calc(100vw - 32px);
|
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>
|
</style>
|
||||||
<link rel="stylesheet" href="/shell.css">
|
<link rel="stylesheet" href="/shell.css">
|
||||||
|
|
@ -619,6 +692,8 @@
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div id="toolbar">
|
<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">
|
<div class="toolbar-group">
|
||||||
<button class="toolbar-group-toggle">✏️ Draw</button>
|
<button class="toolbar-group-toggle">✏️ Draw</button>
|
||||||
<div class="toolbar-dropdown">
|
<div class="toolbar-dropdown">
|
||||||
|
|
@ -746,6 +821,11 @@
|
||||||
<button id="toolbar-collapse" title="Minimize toolbar">···</button>
|
<button id="toolbar-collapse" title="Minimize toolbar">···</button>
|
||||||
</div>
|
</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">
|
||||||
<div id="memory-panel-header">
|
<div id="memory-panel-header">
|
||||||
<h3>💭 Memory</h3>
|
<h3>💭 Memory</h3>
|
||||||
|
|
@ -1415,10 +1495,10 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
shape.id = data.id;
|
shape.id = data.id;
|
||||||
shape.x = data.x || 100;
|
shape.x = data.x ?? 100;
|
||||||
shape.y = data.y || 100;
|
shape.y = data.y ?? 100;
|
||||||
shape.width = data.width || 300;
|
shape.width = data.width ?? 300;
|
||||||
shape.height = data.height || 200;
|
shape.height = data.height ?? 200;
|
||||||
if (data.rotation) shape.rotation = data.rotation;
|
if (data.rotation) shape.rotation = data.rotation;
|
||||||
|
|
||||||
return shape;
|
return shape;
|
||||||
|
|
@ -1521,11 +1601,18 @@
|
||||||
.map(el => ({ x: el.x, y: el.y, width: el.width, height: el.height }));
|
.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
|
// Find a free position that doesn't overlap existing shapes.
|
||||||
function findFreePosition(width, height) {
|
// If preferX/preferY are provided, use that as the anchor; otherwise use viewport center.
|
||||||
const center = getViewportCenter();
|
function findFreePosition(width, height, preferX, preferY) {
|
||||||
const candidateX = center.x - width / 2;
|
let candidateX, candidateY;
|
||||||
const candidateY = center.y - height / 2;
|
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 gap = 20;
|
||||||
const existing = getExistingShapeRects();
|
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
|
// 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 id = `shape-${Date.now()}-${++shapeCounter}`;
|
||||||
const defaults = SHAPE_DEFAULTS[tagName] || { width: 300, height: 200 };
|
const defaults = SHAPE_DEFAULTS[tagName] || { width: 300, height: 200 };
|
||||||
|
|
||||||
const shape = document.createElement(tagName);
|
const shape = document.createElement(tagName);
|
||||||
shape.id = id;
|
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.x = pos.x;
|
||||||
shape.y = pos.y;
|
shape.y = pos.y;
|
||||||
shape.width = defaults.width;
|
shape.width = defaults.width;
|
||||||
|
|
@ -1606,99 +1740,95 @@
|
||||||
window.__canvasApi = { newShape, findFreePosition, SHAPE_DEFAULTS, setupShapeEventListeners, sync, canvasContent };
|
window.__canvasApi = { newShape, findFreePosition, SHAPE_DEFAULTS, setupShapeEventListeners, sync, canvasContent };
|
||||||
installSelectionTransforms();
|
installSelectionTransforms();
|
||||||
|
|
||||||
// Toolbar button handlers
|
// Toolbar button handlers — set pending tool for click-to-place
|
||||||
document.getElementById("new-markdown").addEventListener("click", () => {
|
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", () => {
|
document.getElementById("new-wrapper").addEventListener("click", () => {
|
||||||
const colors = ["#14b8a6", "#8b5cf6", "#f59e0b", "#ef4444", "#3b82f6", "#22c55e"];
|
const colors = ["#14b8a6", "#8b5cf6", "#f59e0b", "#ef4444", "#3b82f6", "#22c55e"];
|
||||||
const icons = ["📋", "💡", "📌", "🔗", "📁", "⭐"];
|
const icons = ["📋", "💡", "📌", "🔗", "📁", "⭐"];
|
||||||
const shape = newShape("folk-wrapper", {
|
setPendingTool("folk-wrapper", {
|
||||||
title: "New Card",
|
title: "New Card",
|
||||||
icon: icons[Math.floor(Math.random() * icons.length)],
|
icon: icons[Math.floor(Math.random() * icons.length)],
|
||||||
primaryColor: colors[Math.floor(Math.random() * colors.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", () => {
|
document.getElementById("new-slide").addEventListener("click", () => {
|
||||||
newShape("folk-slide", { label: `Slide ${shapeCounter}` });
|
setPendingTool("folk-slide", { label: `Slide ${shapeCounter}` });
|
||||||
});
|
});
|
||||||
|
|
||||||
document.getElementById("new-chat").addEventListener("click", () => {
|
document.getElementById("new-chat").addEventListener("click", () => {
|
||||||
const id = `shape-${Date.now()}-${shapeCounter}`;
|
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-piano").addEventListener("click", () => setPendingTool("folk-piano"));
|
||||||
document.getElementById("new-embed").addEventListener("click", () => newShape("folk-embed"));
|
document.getElementById("new-embed").addEventListener("click", () => setPendingTool("folk-embed"));
|
||||||
document.getElementById("new-calendar").addEventListener("click", () => newShape("folk-calendar"));
|
document.getElementById("new-calendar").addEventListener("click", () => setPendingTool("folk-calendar"));
|
||||||
document.getElementById("new-map").addEventListener("click", () => newShape("folk-map"));
|
document.getElementById("new-map").addEventListener("click", () => setPendingTool("folk-map"));
|
||||||
document.getElementById("new-image-gen").addEventListener("click", () => newShape("folk-image-gen"));
|
document.getElementById("new-image-gen").addEventListener("click", () => setPendingTool("folk-image-gen"));
|
||||||
document.getElementById("new-video-gen").addEventListener("click", () => newShape("folk-video-gen"));
|
document.getElementById("new-video-gen").addEventListener("click", () => setPendingTool("folk-video-gen"));
|
||||||
document.getElementById("new-prompt").addEventListener("click", () => newShape("folk-prompt"));
|
document.getElementById("new-prompt").addEventListener("click", () => setPendingTool("folk-prompt"));
|
||||||
document.getElementById("new-transcription").addEventListener("click", () => newShape("folk-transcription"));
|
document.getElementById("new-transcription").addEventListener("click", () => setPendingTool("folk-transcription"));
|
||||||
document.getElementById("new-video-chat").addEventListener("click", () => newShape("folk-video-chat"));
|
document.getElementById("new-video-chat").addEventListener("click", () => setPendingTool("folk-video-chat"));
|
||||||
document.getElementById("new-obs-note").addEventListener("click", () => newShape("folk-obs-note"));
|
document.getElementById("new-obs-note").addEventListener("click", () => setPendingTool("folk-obs-note"));
|
||||||
document.getElementById("new-workflow").addEventListener("click", () => newShape("folk-workflow-block"));
|
document.getElementById("new-workflow").addEventListener("click", () => setPendingTool("folk-workflow-block"));
|
||||||
document.getElementById("new-splat").addEventListener("click", () => newShape("folk-splat"));
|
document.getElementById("new-splat").addEventListener("click", () => setPendingTool("folk-splat"));
|
||||||
document.getElementById("new-blender").addEventListener("click", () => newShape("folk-blender"));
|
document.getElementById("new-blender").addEventListener("click", () => setPendingTool("folk-blender"));
|
||||||
document.getElementById("new-drawfast").addEventListener("click", () => newShape("folk-drawfast"));
|
document.getElementById("new-drawfast").addEventListener("click", () => setPendingTool("folk-drawfast"));
|
||||||
document.getElementById("new-freecad").addEventListener("click", () => newShape("folk-freecad"));
|
document.getElementById("new-freecad").addEventListener("click", () => setPendingTool("folk-freecad"));
|
||||||
document.getElementById("new-kicad").addEventListener("click", () => newShape("folk-kicad"));
|
document.getElementById("new-kicad").addEventListener("click", () => setPendingTool("folk-kicad"));
|
||||||
document.getElementById("new-google-item").addEventListener("click", () => {
|
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
|
// Trip planning components
|
||||||
document.getElementById("new-itinerary").addEventListener("click", () => newShape("folk-itinerary"));
|
document.getElementById("new-itinerary").addEventListener("click", () => setPendingTool("folk-itinerary"));
|
||||||
document.getElementById("new-destination").addEventListener("click", () => newShape("folk-destination"));
|
document.getElementById("new-destination").addEventListener("click", () => setPendingTool("folk-destination"));
|
||||||
document.getElementById("new-budget").addEventListener("click", () => newShape("folk-budget"));
|
document.getElementById("new-budget").addEventListener("click", () => setPendingTool("folk-budget"));
|
||||||
document.getElementById("new-packing-list").addEventListener("click", () => newShape("folk-packing-list"));
|
document.getElementById("new-packing-list").addEventListener("click", () => setPendingTool("folk-packing-list"));
|
||||||
document.getElementById("new-booking").addEventListener("click", () => newShape("folk-booking"));
|
document.getElementById("new-booking").addEventListener("click", () => setPendingTool("folk-booking"));
|
||||||
|
|
||||||
// Token creation - creates a mint + ledger pair with connecting arrow
|
// Token creation - creates a mint + ledger pair with connecting arrow
|
||||||
document.getElementById("new-token").addEventListener("click", () => {
|
document.getElementById("new-token").addEventListener("click", () => {
|
||||||
const mint = newShape("folk-token-mint", {
|
setPendingTool("folk-token-mint", {
|
||||||
tokenName: "New Token",
|
tokenName: "New Token",
|
||||||
tokenSymbol: "TKN",
|
tokenSymbol: "TKN",
|
||||||
totalSupply: 1000,
|
totalSupply: 1000,
|
||||||
tokenColor: "#8b5cf6",
|
tokenColor: "#8b5cf6",
|
||||||
tokenIcon: "🪙",
|
tokenIcon: "🪙",
|
||||||
createdAt: new Date().toISOString(),
|
createdAt: new Date().toISOString(),
|
||||||
});
|
__postCreate: (mint) => {
|
||||||
if (mint) {
|
const ledger = newShape("folk-token-ledger", {
|
||||||
const ledger = newShape("folk-token-ledger", {
|
mintId: mint.id,
|
||||||
mintId: mint.id,
|
entries: [],
|
||||||
entries: [],
|
}, { x: mint.x + mint.width + 60 + 150, y: mint.y + mint.height / 2 });
|
||||||
});
|
if (ledger) {
|
||||||
if (ledger) {
|
const arrowId = `arrow-${Date.now()}-${++shapeCounter}`;
|
||||||
// Position ledger to the right of mint
|
const arrow = document.createElement("folk-arrow");
|
||||||
ledger.x = mint.x + mint.width + 60;
|
arrow.id = arrowId;
|
||||||
ledger.y = mint.y;
|
arrow.sourceId = mint.id;
|
||||||
// Connect with an arrow
|
arrow.targetId = ledger.id;
|
||||||
const arrowId = `arrow-${Date.now()}-${++shapeCounter}`;
|
arrow.color = "#8b5cf6";
|
||||||
const arrow = document.createElement("folk-arrow");
|
canvasContent.appendChild(arrow);
|
||||||
arrow.id = arrowId;
|
sync.registerShape(arrow);
|
||||||
arrow.sourceId = mint.id;
|
}
|
||||||
arrow.targetId = ledger.id;
|
|
||||||
arrow.color = "#8b5cf6";
|
|
||||||
canvasContent.appendChild(arrow);
|
|
||||||
sync.registerShape(arrow);
|
|
||||||
}
|
}
|
||||||
}
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
// Decision/choice components
|
// Decision/choice components
|
||||||
document.getElementById("new-choice-vote").addEventListener("click", () => {
|
document.getElementById("new-choice-vote").addEventListener("click", () => {
|
||||||
newShape("folk-choice-vote", {
|
setPendingTool("folk-choice-vote", {
|
||||||
title: "Quick Poll",
|
title: "Quick Poll",
|
||||||
options: [
|
options: [
|
||||||
{ id: "opt-1", label: "Option A", color: "#3b82f6" },
|
{ id: "opt-1", label: "Option A", color: "#3b82f6" },
|
||||||
|
|
@ -1712,7 +1842,7 @@
|
||||||
});
|
});
|
||||||
|
|
||||||
document.getElementById("new-choice-rank").addEventListener("click", () => {
|
document.getElementById("new-choice-rank").addEventListener("click", () => {
|
||||||
newShape("folk-choice-rank", {
|
setPendingTool("folk-choice-rank", {
|
||||||
title: "Rank These",
|
title: "Rank These",
|
||||||
options: [
|
options: [
|
||||||
{ id: "opt-1", label: "Option A" },
|
{ id: "opt-1", label: "Option A" },
|
||||||
|
|
@ -1724,7 +1854,7 @@
|
||||||
});
|
});
|
||||||
|
|
||||||
document.getElementById("new-choice-spider").addEventListener("click", () => {
|
document.getElementById("new-choice-spider").addEventListener("click", () => {
|
||||||
newShape("folk-choice-spider", {
|
setPendingTool("folk-choice-spider", {
|
||||||
title: "Evaluate Options",
|
title: "Evaluate Options",
|
||||||
options: [
|
options: [
|
||||||
{ id: "opt-1", label: "Option A" },
|
{ id: "opt-1", label: "Option A" },
|
||||||
|
|
@ -1742,7 +1872,7 @@
|
||||||
|
|
||||||
// Social media post
|
// Social media post
|
||||||
document.getElementById("new-social-post").addEventListener("click", () => {
|
document.getElementById("new-social-post").addEventListener("click", () => {
|
||||||
newShape("folk-social-post", {
|
setPendingTool("folk-social-post", {
|
||||||
platform: "x",
|
platform: "x",
|
||||||
postType: "text",
|
postType: "text",
|
||||||
content: "Write your post content here...",
|
content: "Write your post content here...",
|
||||||
|
|
@ -1775,7 +1905,7 @@
|
||||||
const btn = document.getElementById(app.btnId);
|
const btn = document.getElementById(app.btnId);
|
||||||
if (btn) {
|
if (btn) {
|
||||||
btn.addEventListener("click", () => {
|
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");
|
const toolbarEl = document.getElementById("toolbar");
|
||||||
|
|
||||||
mobileMenuBtn.addEventListener("click", () => {
|
mobileMenuBtn.addEventListener("click", () => {
|
||||||
const isOpen = toolbarEl.classList.toggle("mobile-open");
|
// On mobile, first tap opens rApps group in the popout panel
|
||||||
mobileMenuBtn.textContent = isOpen ? "✕" : "✚";
|
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
|
// 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 => {
|
toolbarEl.querySelectorAll(".toolbar-group-toggle").forEach(toggle => {
|
||||||
toggle.addEventListener("click", (e) => {
|
toggle.addEventListener("click", (e) => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
const group = toggle.closest(".toolbar-group");
|
const group = toggle.closest(".toolbar-group");
|
||||||
const wasOpen = group.classList.contains("open");
|
if (activeToolbarGroup === group) {
|
||||||
// Close all other groups
|
closeToolbarPanel();
|
||||||
toolbarEl.querySelectorAll(".toolbar-group.open").forEach(g => g.classList.remove("open"));
|
} else {
|
||||||
// Toggle this one
|
openToolbarPanel(group);
|
||||||
if (!wasOpen) group.classList.add("open");
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
// Close dropdowns when clicking a tool inside one
|
// Close panel when clicking outside toolbar + panel
|
||||||
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
|
|
||||||
document.addEventListener("click", (e) => {
|
document.addEventListener("click", (e) => {
|
||||||
if (!e.target.closest("#toolbar")) {
|
if (!e.target.closest("#toolbar") && !e.target.closest("#toolbar-panel")) {
|
||||||
toolbarEl.querySelectorAll(".toolbar-group.open").forEach(g => g.classList.remove("open"));
|
closeToolbarPanel();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -2317,9 +2498,31 @@
|
||||||
canvas.addEventListener("pointerdown", (e) => {
|
canvas.addEventListener("pointerdown", (e) => {
|
||||||
if (e.target !== canvas && e.target !== canvasContent) return;
|
if (e.target !== canvas && e.target !== canvasContent) return;
|
||||||
if (connectMode) 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;
|
selectedShapeId = null;
|
||||||
__miCanvasBridge.setSelection([]);
|
__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;
|
isPanning = true;
|
||||||
panPointerId = e.pointerId;
|
panPointerId = e.pointerId;
|
||||||
panStartX = e.clientX;
|
panStartX = e.clientX;
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue