Add FolkWrapper component for standardized card UI
- Port StandardizedWrapper from React to web component - Header with title, icon, color theming - Pin, minimize, close action buttons - Tags footer with add/remove functionality - Integrate into canvas with "Card" toolbar button - Sync wrapper properties via Automerge CRDT 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
d6042fcfe7
commit
ddab22abc2
|
|
@ -14,6 +14,13 @@ export interface ShapeData {
|
|||
// Arrow-specific
|
||||
sourceId?: string;
|
||||
targetId?: string;
|
||||
// Wrapper-specific
|
||||
title?: string;
|
||||
icon?: string;
|
||||
primaryColor?: string;
|
||||
isMinimized?: boolean;
|
||||
isPinned?: boolean;
|
||||
tags?: string[];
|
||||
}
|
||||
|
||||
// Automerge document structure
|
||||
|
|
@ -312,6 +319,17 @@ export class CommunitySync extends EventTarget {
|
|||
data.targetId = (shape as any).targetId;
|
||||
}
|
||||
|
||||
// Add wrapper properties
|
||||
if (shape.tagName.toLowerCase() === "folk-wrapper") {
|
||||
const wrapper = shape as any;
|
||||
if (wrapper.title) data.title = wrapper.title;
|
||||
if (wrapper.icon) data.icon = wrapper.icon;
|
||||
if (wrapper.primaryColor) data.primaryColor = wrapper.primaryColor;
|
||||
if (wrapper.isMinimized !== undefined) data.isMinimized = wrapper.isMinimized;
|
||||
if (wrapper.isPinned !== undefined) data.isPinned = wrapper.isPinned;
|
||||
if (wrapper.tags?.length) data.tags = wrapper.tags;
|
||||
}
|
||||
|
||||
return data;
|
||||
}
|
||||
|
||||
|
|
@ -443,6 +461,29 @@ export class CommunitySync extends EventTarget {
|
|||
shapeWithContent.content = data.content;
|
||||
}
|
||||
}
|
||||
|
||||
// Update wrapper-specific properties
|
||||
if (data.type === "folk-wrapper") {
|
||||
const wrapper = shape as any;
|
||||
if (data.title !== undefined && wrapper.title !== data.title) {
|
||||
wrapper.title = data.title;
|
||||
}
|
||||
if (data.icon !== undefined && wrapper.icon !== data.icon) {
|
||||
wrapper.icon = data.icon;
|
||||
}
|
||||
if (data.primaryColor !== undefined && wrapper.primaryColor !== data.primaryColor) {
|
||||
wrapper.primaryColor = data.primaryColor;
|
||||
}
|
||||
if (data.isMinimized !== undefined && wrapper.isMinimized !== data.isMinimized) {
|
||||
wrapper.isMinimized = data.isMinimized;
|
||||
}
|
||||
if (data.isPinned !== undefined && wrapper.isPinned !== data.isPinned) {
|
||||
wrapper.isPinned = data.isPinned;
|
||||
}
|
||||
if (data.tags !== undefined) {
|
||||
wrapper.tags = data.tags;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -0,0 +1,488 @@
|
|||
import { FolkShape } from "./folk-shape";
|
||||
import { css, html } from "./tags";
|
||||
|
||||
const styles = css`
|
||||
:host {
|
||||
background: white;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
||||
min-width: 200px;
|
||||
min-height: 100px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
:host([selected]) {
|
||||
box-shadow: 0 0 0 2px var(--primary-color, #14b8a6), 0 4px 8px rgba(0, 0, 0, 0.15);
|
||||
}
|
||||
|
||||
.header {
|
||||
height: 40px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 0 12px;
|
||||
cursor: move;
|
||||
user-select: none;
|
||||
flex-shrink: 0;
|
||||
transition: background-color 0.2s ease;
|
||||
background: var(--header-bg, rgba(20, 184, 166, 0.1));
|
||||
border-bottom: 1px solid var(--header-border, rgba(20, 184, 166, 0.2));
|
||||
}
|
||||
|
||||
:host([selected]) .header {
|
||||
background: var(--primary-color, #14b8a6);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.header-title {
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
color: var(--primary-color, #14b8a6);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
:host([selected]) .header-title {
|
||||
color: white;
|
||||
}
|
||||
|
||||
.header-icon {
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.header-actions {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.header-actions button {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
border-radius: 4px;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
transition: background-color 0.15s ease;
|
||||
background: var(--button-bg, rgba(20, 184, 166, 0.2));
|
||||
color: var(--primary-color, #14b8a6);
|
||||
}
|
||||
|
||||
:host([selected]) .header-actions button {
|
||||
background: rgba(255, 255, 255, 0.2);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.header-actions button:hover {
|
||||
background: var(--button-hover-bg, rgba(20, 184, 166, 0.3));
|
||||
}
|
||||
|
||||
:host([selected]) .header-actions button:hover {
|
||||
background: rgba(255, 255, 255, 0.3);
|
||||
}
|
||||
|
||||
.header-actions button.active {
|
||||
background: var(--primary-color, #14b8a6);
|
||||
color: white;
|
||||
}
|
||||
|
||||
:host([selected]) .header-actions button.active {
|
||||
background: rgba(255, 255, 255, 0.4);
|
||||
}
|
||||
|
||||
.content {
|
||||
flex: 1;
|
||||
overflow: auto;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
:host([minimized]) .content {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.tags {
|
||||
padding: 8px 12px;
|
||||
border-top: 1px solid #e0e0e0;
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 4px;
|
||||
align-items: center;
|
||||
min-height: 32px;
|
||||
background: #f8f9fa;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
:host([minimized]) .tags {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.tag {
|
||||
background: #6b7280;
|
||||
color: white;
|
||||
padding: 4px 8px;
|
||||
border-radius: 12px;
|
||||
font-size: 10px;
|
||||
font-weight: 500;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.tag:hover {
|
||||
background: #4b5563;
|
||||
}
|
||||
|
||||
.tag .remove {
|
||||
font-size: 8px;
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
.tag .remove:hover {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.add-tag {
|
||||
background: #9ca3af;
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 12px;
|
||||
padding: 4px 10px;
|
||||
font-size: 10px;
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.add-tag:hover {
|
||||
background: #6b7280;
|
||||
}
|
||||
|
||||
.tag-input {
|
||||
border: 1px solid #9ca3af;
|
||||
border-radius: 12px;
|
||||
padding: 2px 6px;
|
||||
font-size: 10px;
|
||||
outline: none;
|
||||
min-width: 60px;
|
||||
flex: 1;
|
||||
background: white;
|
||||
}
|
||||
`;
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"folk-wrapper": FolkWrapper;
|
||||
}
|
||||
}
|
||||
|
||||
export class FolkWrapper extends FolkShape {
|
||||
static override tagName = "folk-wrapper";
|
||||
|
||||
// Merge parent and child styles
|
||||
static {
|
||||
const sheet = new CSSStyleSheet();
|
||||
const parentRules = Array.from(FolkShape.styles.cssRules)
|
||||
.map((r) => r.cssText)
|
||||
.join("\n");
|
||||
const childRules = Array.from(styles.cssRules)
|
||||
.map((r) => r.cssText)
|
||||
.join("\n");
|
||||
sheet.replaceSync(`${parentRules}\n${childRules}`);
|
||||
this.styles = sheet;
|
||||
}
|
||||
|
||||
#title = "Untitled";
|
||||
#icon = "";
|
||||
#primaryColor = "#14b8a6";
|
||||
#isMinimized = false;
|
||||
#isPinned = false;
|
||||
#tags: string[] = [];
|
||||
#isEditingTags = false;
|
||||
#tagInput: HTMLInputElement | null = null;
|
||||
|
||||
get title() {
|
||||
return this.#title;
|
||||
}
|
||||
|
||||
set title(value: string) {
|
||||
this.#title = value;
|
||||
this.requestUpdate("title");
|
||||
}
|
||||
|
||||
get icon() {
|
||||
return this.#icon;
|
||||
}
|
||||
|
||||
set icon(value: string) {
|
||||
this.#icon = value;
|
||||
this.requestUpdate("icon");
|
||||
}
|
||||
|
||||
get primaryColor() {
|
||||
return this.#primaryColor;
|
||||
}
|
||||
|
||||
set primaryColor(value: string) {
|
||||
this.#primaryColor = value;
|
||||
this.style.setProperty("--primary-color", value);
|
||||
this.style.setProperty("--header-bg", `${value}10`);
|
||||
this.style.setProperty("--header-border", `${value}30`);
|
||||
this.style.setProperty("--button-bg", `${value}20`);
|
||||
this.style.setProperty("--button-hover-bg", `${value}30`);
|
||||
this.requestUpdate("primaryColor");
|
||||
}
|
||||
|
||||
get isMinimized() {
|
||||
return this.#isMinimized;
|
||||
}
|
||||
|
||||
set isMinimized(value: boolean) {
|
||||
this.#isMinimized = value;
|
||||
this.toggleAttribute("minimized", value);
|
||||
}
|
||||
|
||||
get isPinned() {
|
||||
return this.#isPinned;
|
||||
}
|
||||
|
||||
set isPinned(value: boolean) {
|
||||
this.#isPinned = value;
|
||||
this.requestUpdate("isPinned");
|
||||
}
|
||||
|
||||
get tags() {
|
||||
return [...this.#tags];
|
||||
}
|
||||
|
||||
set tags(value: string[]) {
|
||||
this.#tags = [...value];
|
||||
this.requestUpdate("tags");
|
||||
}
|
||||
|
||||
override createRenderRoot() {
|
||||
const root = super.createRenderRoot();
|
||||
|
||||
// Add wrapper UI
|
||||
const wrapper = document.createElement("div");
|
||||
wrapper.innerHTML = html`
|
||||
<div class="header">
|
||||
<div class="header-title">
|
||||
<span class="header-icon"></span>
|
||||
<span class="title-text"></span>
|
||||
</div>
|
||||
<div class="header-actions">
|
||||
<button class="pin-btn" title="Pin to view">📌</button>
|
||||
<button class="minimize-btn" title="Minimize">_</button>
|
||||
<button class="close-btn" title="Close">×</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="content">
|
||||
<slot></slot>
|
||||
</div>
|
||||
<div class="tags"></div>
|
||||
`;
|
||||
|
||||
// Replace existing content structure
|
||||
const existingSlot = root.querySelector("slot");
|
||||
if (existingSlot?.parentElement) {
|
||||
existingSlot.parentElement.innerHTML = "";
|
||||
existingSlot.parentElement.appendChild(wrapper);
|
||||
}
|
||||
|
||||
// Get references
|
||||
const header = wrapper.querySelector(".header") as HTMLElement;
|
||||
const titleIcon = wrapper.querySelector(".header-icon") as HTMLElement;
|
||||
const titleText = wrapper.querySelector(".title-text") as HTMLElement;
|
||||
const pinBtn = wrapper.querySelector(".pin-btn") as HTMLButtonElement;
|
||||
const minimizeBtn = wrapper.querySelector(".minimize-btn") as HTMLButtonElement;
|
||||
const closeBtn = wrapper.querySelector(".close-btn") as HTMLButtonElement;
|
||||
const tagsContainer = wrapper.querySelector(".tags") as HTMLElement;
|
||||
|
||||
// Initialize
|
||||
titleIcon.textContent = this.#icon;
|
||||
titleText.textContent = this.#title;
|
||||
this.primaryColor = this.getAttribute("color") || this.#primaryColor;
|
||||
this.title = this.getAttribute("title") || this.#title;
|
||||
this.icon = this.getAttribute("icon") || this.#icon;
|
||||
|
||||
// Parse tags from attribute
|
||||
const tagsAttr = this.getAttribute("tags");
|
||||
if (tagsAttr) {
|
||||
this.#tags = tagsAttr.split(",").map((t) => t.trim()).filter(Boolean);
|
||||
}
|
||||
|
||||
// Event handlers
|
||||
pinBtn.addEventListener("click", (e) => {
|
||||
e.stopPropagation();
|
||||
this.#isPinned = !this.#isPinned;
|
||||
pinBtn.classList.toggle("active", this.#isPinned);
|
||||
this.dispatchEvent(new CustomEvent("pin-toggle", { detail: { pinned: this.#isPinned } }));
|
||||
});
|
||||
|
||||
minimizeBtn.addEventListener("click", (e) => {
|
||||
e.stopPropagation();
|
||||
this.isMinimized = !this.isMinimized;
|
||||
this.dispatchEvent(new CustomEvent("minimize-toggle", { detail: { minimized: this.#isMinimized } }));
|
||||
});
|
||||
|
||||
closeBtn.addEventListener("click", (e) => {
|
||||
e.stopPropagation();
|
||||
this.dispatchEvent(new CustomEvent("close"));
|
||||
});
|
||||
|
||||
// Render tags
|
||||
this.#renderTags(tagsContainer);
|
||||
|
||||
// Watch for attribute changes
|
||||
const observer = new MutationObserver((mutations) => {
|
||||
for (const mutation of mutations) {
|
||||
if (mutation.type === "attributes") {
|
||||
switch (mutation.attributeName) {
|
||||
case "title":
|
||||
titleText.textContent = this.getAttribute("title") || "";
|
||||
break;
|
||||
case "icon":
|
||||
titleIcon.textContent = this.getAttribute("icon") || "";
|
||||
break;
|
||||
case "color":
|
||||
this.primaryColor = this.getAttribute("color") || "#14b8a6";
|
||||
break;
|
||||
case "tags":
|
||||
const tagsAttr = this.getAttribute("tags");
|
||||
if (tagsAttr) {
|
||||
this.#tags = tagsAttr.split(",").map((t) => t.trim()).filter(Boolean);
|
||||
this.#renderTags(tagsContainer);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
observer.observe(this, { attributes: true });
|
||||
|
||||
return root;
|
||||
}
|
||||
|
||||
#renderTags(container: HTMLElement) {
|
||||
container.innerHTML = "";
|
||||
|
||||
for (const tag of this.#tags.slice(0, 5)) {
|
||||
const tagEl = document.createElement("span");
|
||||
tagEl.className = "tag";
|
||||
tagEl.innerHTML = `${tag.replace("#", "")} <span class="remove">×</span>`;
|
||||
|
||||
tagEl.querySelector(".remove")?.addEventListener("click", (e) => {
|
||||
e.stopPropagation();
|
||||
this.#tags = this.#tags.filter((t) => t !== tag);
|
||||
this.#renderTags(container);
|
||||
this.dispatchEvent(new CustomEvent("tags-change", { detail: { tags: this.#tags } }));
|
||||
});
|
||||
|
||||
container.appendChild(tagEl);
|
||||
}
|
||||
|
||||
if (this.#tags.length > 5) {
|
||||
const moreTag = document.createElement("span");
|
||||
moreTag.className = "tag";
|
||||
moreTag.textContent = `+${this.#tags.length - 5}`;
|
||||
container.appendChild(moreTag);
|
||||
}
|
||||
|
||||
// Add tag button
|
||||
if (this.#tags.length < 10) {
|
||||
if (this.#isEditingTags) {
|
||||
const input = document.createElement("input");
|
||||
input.className = "tag-input";
|
||||
input.placeholder = "Add tag...";
|
||||
input.addEventListener("keydown", (e) => {
|
||||
if (e.key === "Enter") {
|
||||
e.preventDefault();
|
||||
const value = input.value.trim().replace("#", "");
|
||||
if (value && !this.#tags.includes(value)) {
|
||||
this.#tags.push(value);
|
||||
this.#renderTags(container);
|
||||
this.dispatchEvent(new CustomEvent("tags-change", { detail: { tags: this.#tags } }));
|
||||
}
|
||||
this.#isEditingTags = false;
|
||||
this.#renderTags(container);
|
||||
} else if (e.key === "Escape") {
|
||||
this.#isEditingTags = false;
|
||||
this.#renderTags(container);
|
||||
}
|
||||
});
|
||||
input.addEventListener("blur", () => {
|
||||
const value = input.value.trim().replace("#", "");
|
||||
if (value && !this.#tags.includes(value)) {
|
||||
this.#tags.push(value);
|
||||
this.dispatchEvent(new CustomEvent("tags-change", { detail: { tags: this.#tags } }));
|
||||
}
|
||||
this.#isEditingTags = false;
|
||||
this.#renderTags(container);
|
||||
});
|
||||
container.appendChild(input);
|
||||
setTimeout(() => input.focus(), 0);
|
||||
} else {
|
||||
const addBtn = document.createElement("button");
|
||||
addBtn.className = "add-tag";
|
||||
addBtn.textContent = "+ Add";
|
||||
addBtn.addEventListener("click", (e) => {
|
||||
e.stopPropagation();
|
||||
this.#isEditingTags = true;
|
||||
this.#renderTags(container);
|
||||
});
|
||||
container.appendChild(addBtn);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
addTag(tag: string) {
|
||||
const cleanTag = tag.trim().replace("#", "");
|
||||
if (cleanTag && !this.#tags.includes(cleanTag)) {
|
||||
this.#tags.push(cleanTag);
|
||||
const tagsContainer = this.shadowRoot?.querySelector(".tags") as HTMLElement;
|
||||
if (tagsContainer) {
|
||||
this.#renderTags(tagsContainer);
|
||||
}
|
||||
this.dispatchEvent(new CustomEvent("tags-change", { detail: { tags: this.#tags } }));
|
||||
}
|
||||
}
|
||||
|
||||
removeTag(tag: string) {
|
||||
const cleanTag = tag.trim().replace("#", "");
|
||||
this.#tags = this.#tags.filter((t) => t !== cleanTag);
|
||||
const tagsContainer = this.shadowRoot?.querySelector(".tags") as HTMLElement;
|
||||
if (tagsContainer) {
|
||||
this.#renderTags(tagsContainer);
|
||||
}
|
||||
this.dispatchEvent(new CustomEvent("tags-change", { detail: { tags: this.#tags } }));
|
||||
}
|
||||
|
||||
toJSON() {
|
||||
return {
|
||||
type: "folk-wrapper",
|
||||
id: this.id,
|
||||
x: this.x,
|
||||
y: this.y,
|
||||
width: this.width,
|
||||
height: this.height,
|
||||
rotation: this.rotation,
|
||||
title: this.#title,
|
||||
icon: this.#icon,
|
||||
primaryColor: this.#primaryColor,
|
||||
isMinimized: this.#isMinimized,
|
||||
isPinned: this.#isPinned,
|
||||
tags: this.#tags,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
@ -21,6 +21,7 @@ export * from "./tags";
|
|||
// Components
|
||||
export * from "./folk-shape";
|
||||
export * from "./folk-markdown";
|
||||
export * from "./folk-wrapper";
|
||||
|
||||
// Sync
|
||||
export * from "./community-sync";
|
||||
|
|
|
|||
|
|
@ -125,7 +125,8 @@
|
|||
overflow: hidden;
|
||||
}
|
||||
|
||||
folk-markdown {
|
||||
folk-markdown,
|
||||
folk-wrapper {
|
||||
position: absolute;
|
||||
}
|
||||
</style>
|
||||
|
|
@ -137,7 +138,8 @@
|
|||
</div>
|
||||
|
||||
<div id="toolbar">
|
||||
<button id="add-markdown" title="Add Markdown Note">+ Add Note</button>
|
||||
<button id="add-markdown" title="Add Markdown Note">📝 Note</button>
|
||||
<button id="add-wrapper" title="Add Wrapped Card">🗂️ Card</button>
|
||||
<button id="zoom-in" title="Zoom In">+</button>
|
||||
<button id="zoom-out" title="Zoom Out">-</button>
|
||||
<button id="reset-view" title="Reset View">Reset</button>
|
||||
|
|
@ -151,11 +153,12 @@
|
|||
<div id="canvas"></div>
|
||||
|
||||
<script type="module">
|
||||
import { FolkShape, FolkMarkdown, CommunitySync } from "@lib";
|
||||
import { FolkShape, FolkMarkdown, FolkWrapper, CommunitySync } from "@lib";
|
||||
|
||||
// Register custom elements
|
||||
FolkShape.define();
|
||||
FolkMarkdown.define();
|
||||
FolkWrapper.define();
|
||||
|
||||
// Get community info from URL
|
||||
const hostname = window.location.hostname;
|
||||
|
|
@ -223,6 +226,15 @@
|
|||
let shape;
|
||||
|
||||
switch (data.type) {
|
||||
case "folk-wrapper":
|
||||
shape = document.createElement("folk-wrapper");
|
||||
if (data.title) shape.title = data.title;
|
||||
if (data.icon) shape.icon = data.icon;
|
||||
if (data.primaryColor) shape.primaryColor = data.primaryColor;
|
||||
if (data.isMinimized) shape.isMinimized = data.isMinimized;
|
||||
if (data.isPinned) shape.isPinned = data.isPinned;
|
||||
if (data.tags) shape.tags = data.tags;
|
||||
break;
|
||||
case "folk-markdown":
|
||||
default:
|
||||
shape = document.createElement("folk-markdown");
|
||||
|
|
@ -279,6 +291,34 @@
|
|||
sync.registerShape(shape);
|
||||
});
|
||||
|
||||
// Add wrapper card button
|
||||
document.getElementById("add-wrapper").addEventListener("click", () => {
|
||||
const id = `shape-${Date.now()}-${++shapeCounter}`;
|
||||
const colors = ["#14b8a6", "#8b5cf6", "#f59e0b", "#ef4444", "#3b82f6", "#22c55e"];
|
||||
const icons = ["📋", "💡", "📌", "🔗", "📁", "⭐"];
|
||||
|
||||
const shape = document.createElement("folk-wrapper");
|
||||
shape.id = id;
|
||||
shape.x = 100 + Math.random() * 200;
|
||||
shape.y = 100 + Math.random() * 200;
|
||||
shape.width = 320;
|
||||
shape.height = 240;
|
||||
shape.title = "New Card";
|
||||
shape.icon = icons[Math.floor(Math.random() * icons.length)];
|
||||
shape.primaryColor = colors[Math.floor(Math.random() * colors.length)];
|
||||
|
||||
// Add some placeholder content inside the wrapper
|
||||
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);
|
||||
|
||||
setupShapeEventListeners(shape);
|
||||
canvas.appendChild(shape);
|
||||
sync.registerShape(shape);
|
||||
});
|
||||
|
||||
// Zoom controls
|
||||
let scale = 1;
|
||||
const minScale = 0.25;
|
||||
|
|
|
|||
Loading…
Reference in New Issue