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`
`;
// 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("#", "")} ×`;
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,
};
}
}