diff --git a/lib/community-sync.ts b/lib/community-sync.ts index 634abd7..f8d5a87 100644 --- a/lib/community-sync.ts +++ b/lib/community-sync.ts @@ -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; + } + } } /** diff --git a/lib/folk-wrapper.ts b/lib/folk-wrapper.ts new file mode 100644 index 0000000..b0205d1 --- /dev/null +++ b/lib/folk-wrapper.ts @@ -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` +