feat(canvas): folk-shape and canvas.html updates
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
0d8d42c49e
commit
851cc283ee
|
|
@ -114,6 +114,10 @@ const styles = css`
|
||||||
transition: opacity 0.3s ease, filter 0.3s ease;
|
transition: opacity 0.3s ease, filter 0.3s ease;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
:host(:state(locked)) {
|
||||||
|
cursor: default;
|
||||||
|
}
|
||||||
|
|
||||||
[part="forgotten-tooltip"] {
|
[part="forgotten-tooltip"] {
|
||||||
display: none;
|
display: none;
|
||||||
position: absolute;
|
position: absolute;
|
||||||
|
|
@ -319,6 +323,18 @@ export class FolkShape extends FolkElement {
|
||||||
: this.#internals.states.delete("forgotten");
|
: this.#internals.states.delete("forgotten");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#locked = false;
|
||||||
|
get locked() {
|
||||||
|
return this.#locked;
|
||||||
|
}
|
||||||
|
set locked(locked) {
|
||||||
|
if (this.#locked === locked) return;
|
||||||
|
this.#locked = locked;
|
||||||
|
locked
|
||||||
|
? this.#internals.states.add("locked")
|
||||||
|
: this.#internals.states.delete("locked");
|
||||||
|
}
|
||||||
|
|
||||||
#editing = false;
|
#editing = false;
|
||||||
get editing() {
|
get editing() {
|
||||||
return this.#editing;
|
return this.#editing;
|
||||||
|
|
@ -470,6 +486,9 @@ export class FolkShape extends FolkElement {
|
||||||
// In feed mode, suppress all drag/resize interactions
|
// In feed mode, suppress all drag/resize interactions
|
||||||
if (this.closest('#canvas.feed-mode')) return;
|
if (this.closest('#canvas.feed-mode')) return;
|
||||||
|
|
||||||
|
// Locked shapes cannot be moved or resized, but can still be interacted with
|
||||||
|
if (this.#locked) return;
|
||||||
|
|
||||||
// Handle touch events for mobile drag support
|
// Handle touch events for mobile drag support
|
||||||
if (event instanceof TouchEvent) {
|
if (event instanceof TouchEvent) {
|
||||||
const target = event.composedPath()[0] as HTMLElement;
|
const target = event.composedPath()[0] as HTMLElement;
|
||||||
|
|
@ -871,6 +890,8 @@ export class FolkShape extends FolkElement {
|
||||||
rotation: this.rotation,
|
rotation: this.rotation,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
if (this.#locked) json.locked = true;
|
||||||
|
|
||||||
// Include port values when ports have data
|
// Include port values when ports have data
|
||||||
if (this.#ports.size > 0) {
|
if (this.#ports.size > 0) {
|
||||||
const portValues: Record<string, unknown> = {};
|
const portValues: Record<string, unknown> = {};
|
||||||
|
|
@ -907,6 +928,7 @@ export class FolkShape extends FolkElement {
|
||||||
if (typeof data.rotation === "number" && Number.isFinite(data.rotation)) {
|
if (typeof data.rotation === "number" && Number.isFinite(data.rotation)) {
|
||||||
shape.rotation = data.rotation;
|
shape.rotation = data.rotation;
|
||||||
}
|
}
|
||||||
|
if (data.locked) shape.locked = true;
|
||||||
|
|
||||||
return shape;
|
return shape;
|
||||||
}
|
}
|
||||||
|
|
@ -921,6 +943,7 @@ export class FolkShape extends FolkElement {
|
||||||
if (this.width !== data.width) this.width = data.width;
|
if (this.width !== data.width) this.width = data.width;
|
||||||
if (this.height !== data.height) this.height = data.height;
|
if (this.height !== data.height) this.height = data.height;
|
||||||
if (this.rotation !== data.rotation) this.rotation = data.rotation;
|
if (this.rotation !== data.rotation) this.rotation = data.rotation;
|
||||||
|
if (!!data.locked !== this.#locked) this.locked = !!data.locked;
|
||||||
|
|
||||||
// Restore port values without dispatching events (avoids sync loops)
|
// Restore port values without dispatching events (avoids sync loops)
|
||||||
if (data.ports && typeof data.ports === "object") {
|
if (data.ports && typeof data.ports === "object") {
|
||||||
|
|
|
||||||
|
|
@ -2411,6 +2411,25 @@
|
||||||
.shape-schedule-icon:hover {
|
.shape-schedule-icon:hover {
|
||||||
background: #2a2a3e; border-color: #818cf8;
|
background: #2a2a3e; border-color: #818cf8;
|
||||||
}
|
}
|
||||||
|
.shape-lock-icon {
|
||||||
|
position: fixed; z-index: 9999; width: 28px; height: 28px;
|
||||||
|
border-radius: 50%; border: 1px solid #444;
|
||||||
|
background: var(--rs-bg-surface, #1e1e2e); color: #e0e0e0;
|
||||||
|
font-size: 14px; cursor: pointer; display: flex;
|
||||||
|
align-items: center; justify-content: center;
|
||||||
|
box-shadow: 0 2px 8px rgba(0,0,0,0.4);
|
||||||
|
opacity: 0; transition: opacity 0.15s; pointer-events: none;
|
||||||
|
padding: 0; line-height: 1;
|
||||||
|
}
|
||||||
|
.shape-lock-icon.visible {
|
||||||
|
opacity: 1; pointer-events: auto;
|
||||||
|
}
|
||||||
|
.shape-lock-icon:hover {
|
||||||
|
background: #2a2a3e; border-color: #818cf8;
|
||||||
|
}
|
||||||
|
.shape-lock-icon.locked {
|
||||||
|
border-color: #f59e0b; color: #f59e0b;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
<div id="reminder-widget">
|
<div id="reminder-widget">
|
||||||
<div class="rw-header"><span class="rw-header-icon">🔔</span> Remind me of this on:</div>
|
<div class="rw-header"><span class="rw-header-icon">🔔</span> Remind me of this on:</div>
|
||||||
|
|
@ -3156,6 +3175,7 @@
|
||||||
}
|
}
|
||||||
__miCanvasBridge.setSelection([...selectedShapeIds]);
|
__miCanvasBridge.setSelection([...selectedShapeIds]);
|
||||||
updateScheduleIcon();
|
updateScheduleIcon();
|
||||||
|
updateLockIcon();
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Floating schedule icon on selected shape ──
|
// ── Floating schedule icon on selected shape ──
|
||||||
|
|
@ -3201,6 +3221,51 @@
|
||||||
if (rwWidget) rwWidget.classList.remove("visible");
|
if (rwWidget) rwWidget.classList.remove("visible");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ── Floating lock icon on selected shape ──
|
||||||
|
let lockIconEl = null;
|
||||||
|
function updateLockIcon() {
|
||||||
|
if (selectedShapeIds.size >= 1) {
|
||||||
|
const id = [...selectedShapeIds][0];
|
||||||
|
const el = document.getElementById(id);
|
||||||
|
if (el) {
|
||||||
|
if (!lockIconEl) {
|
||||||
|
lockIconEl = document.createElement("button");
|
||||||
|
lockIconEl.className = "shape-lock-icon";
|
||||||
|
lockIconEl.textContent = "🔒";
|
||||||
|
lockIconEl.title = "Lock/unlock shape";
|
||||||
|
lockIconEl.addEventListener("click", (ev) => {
|
||||||
|
ev.stopPropagation();
|
||||||
|
// Toggle lock on all selected shapes
|
||||||
|
const ids = [...selectedShapeIds];
|
||||||
|
const firstEl = document.getElementById(ids[0]);
|
||||||
|
const newLocked = !firstEl?.locked;
|
||||||
|
for (const sid of ids) {
|
||||||
|
const sel = document.getElementById(sid);
|
||||||
|
if (sel) {
|
||||||
|
sel.locked = newLocked;
|
||||||
|
// Trigger sync update
|
||||||
|
sel.dispatchEvent(new CustomEvent("content-change", { bubbles: true }));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
lockIconEl.textContent = newLocked ? "🔒" : "🔓";
|
||||||
|
lockIconEl.classList.toggle("locked", newLocked);
|
||||||
|
});
|
||||||
|
document.body.appendChild(lockIconEl);
|
||||||
|
}
|
||||||
|
const rect = el.getBoundingClientRect();
|
||||||
|
lockIconEl.style.left = (rect.right + 4) + "px";
|
||||||
|
lockIconEl.style.top = (rect.top + 28) + "px"; // below schedule icon
|
||||||
|
lockIconEl.classList.add("visible");
|
||||||
|
// Reflect current lock state
|
||||||
|
const isLocked = el.locked;
|
||||||
|
lockIconEl.textContent = isLocked ? "🔒" : "🔓";
|
||||||
|
lockIconEl.classList.toggle("locked", isLocked);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (lockIconEl) lockIconEl.classList.remove("visible");
|
||||||
|
}
|
||||||
|
|
||||||
function rectsOverlapScreen(sel, r) {
|
function rectsOverlapScreen(sel, r) {
|
||||||
return !(sel.left > r.right || sel.right < r.left ||
|
return !(sel.left > r.right || sel.right < r.left ||
|
||||||
sel.top > r.bottom || sel.bottom < r.top);
|
sel.top > r.bottom || sel.bottom < r.top);
|
||||||
|
|
@ -4809,7 +4874,7 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
// Disable shape interaction when drawing tools are active.
|
// Disable shape interaction when drawing tools are active.
|
||||||
// Eraser keeps canvasContent interactive so it can delete wb-drawing shapes.
|
// Eraser keeps canvasContent interactive so it can erase any shape.
|
||||||
if (wbTool && wbTool !== "eraser") {
|
if (wbTool && wbTool !== "eraser") {
|
||||||
canvasContent.style.pointerEvents = "none";
|
canvasContent.style.pointerEvents = "none";
|
||||||
wbOverlay.style.pointerEvents = "all";
|
wbOverlay.style.pointerEvents = "all";
|
||||||
|
|
@ -5224,12 +5289,14 @@
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Eraser for wb-drawing folk-shapes — capturing phase intercepts
|
// Eraser for any folk-shape — capturing phase intercepts
|
||||||
// before normal shape interaction (drag/select) kicks in
|
// before normal shape interaction (drag/select) kicks in
|
||||||
canvasContent.addEventListener("pointerdown", (e) => {
|
canvasContent.addEventListener("pointerdown", (e) => {
|
||||||
if (wbTool !== "eraser") return;
|
if (wbTool !== "eraser") return;
|
||||||
const target = e.target.closest?.("[data-wb-drawing]");
|
const target = e.target.closest?.("folk-shape");
|
||||||
if (!target) return;
|
if (!target) return;
|
||||||
|
// Locked shapes cannot be erased
|
||||||
|
if (target.locked) return;
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
const state = sync.getShapeVisualState(target.id);
|
const state = sync.getShapeVisualState(target.id);
|
||||||
|
|
@ -5891,6 +5958,7 @@
|
||||||
// Update remote cursors to match new camera position
|
// Update remote cursors to match new camera position
|
||||||
presence.setCamera(panX, panY, scale);
|
presence.setCamera(panX, panY, scale);
|
||||||
updateScheduleIcon();
|
updateScheduleIcon();
|
||||||
|
updateLockIcon();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Re-render canvas background when user changes preference
|
// Re-render canvas background when user changes preference
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue