Merge branch 'dev'
This commit is contained in:
commit
cce4e8d357
|
|
@ -76,6 +76,16 @@ const styles = css`
|
|||
pointer-events: none;
|
||||
}
|
||||
|
||||
:host(:state(editing)) .slot-container {
|
||||
pointer-events: auto;
|
||||
}
|
||||
|
||||
:host(:state(editing)) {
|
||||
outline: 2px solid #14b8a6;
|
||||
outline-offset: 2px;
|
||||
cursor: text;
|
||||
}
|
||||
|
||||
::slotted(*) {
|
||||
cursor: default;
|
||||
pointer-events: auto;
|
||||
|
|
@ -263,6 +273,37 @@ export class FolkShape extends FolkElement {
|
|||
: 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() {
|
||||
const root = super.createRenderRoot();
|
||||
|
||||
|
|
@ -272,6 +313,11 @@ export class FolkShape extends FolkElement {
|
|||
this.addEventListener("touchend", this);
|
||||
this.addEventListener("keydown", this);
|
||||
|
||||
this.addEventListener("dblclick", (e: MouseEvent) => {
|
||||
e.stopPropagation();
|
||||
this.enterEditMode();
|
||||
});
|
||||
|
||||
(root as ShadowRoot).setHTMLUnsafe(
|
||||
html`<button part="rotation-top-left" 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.
|
||||
* 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) {
|
||||
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 };
|
||||
|
||||
// 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 overlapY = me.y < other.y + other.h + gap && me.y + me.h + gap > other.y;
|
||||
|
||||
if (!overlapX || !overlapY) continue;
|
||||
|
||||
// Compute penetration depths from each side
|
||||
const pushRight = (other.x + other.w + gap) - me.x;
|
||||
const pushLeft = me.x + me.w + gap - other.x;
|
||||
const pushDown = (other.y + other.h + gap) - me.y;
|
||||
const pushUp = me.y + me.h + gap - other.y;
|
||||
// Distance to clear on each side
|
||||
const clearRight = (other.x + other.w + gap) - me.x; // push me right of other
|
||||
const clearLeft = other.x - (me.x + me.w + gap); // push me left of other (negative)
|
||||
const clearDown = (other.y + other.h + gap) - me.y; // push me below other
|
||||
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
|
||||
const minX = pushRight < pushLeft ? -pushRight : pushLeft;
|
||||
const minY = pushDown < pushUp ? -pushDown : pushUp;
|
||||
// Pick push direction per axis based on movement direction
|
||||
const pushX = dx >= 0 ? clearRight : clearLeft;
|
||||
const pushY = dy >= 0 ? clearDown : clearUp;
|
||||
|
||||
if (Math.abs(minX) < Math.abs(minY)) {
|
||||
// Slide horizontally
|
||||
this.#rect.x += dx <= 0 ? -pushLeft : pushRight;
|
||||
// Apply the axis with the smallest absolute displacement
|
||||
if (Math.abs(pushX) <= Math.abs(pushY)) {
|
||||
this.#rect.x += pushX;
|
||||
} else {
|
||||
// Slide vertically
|
||||
this.#rect.y += dy <= 0 ? -pushUp : pushDown;
|
||||
this.#rect.y += pushY;
|
||||
}
|
||||
|
||||
me.x = this.#rect.x;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
Loading…
Reference in New Issue