diff --git a/lib/folk-bookmark.ts b/lib/folk-bookmark.ts new file mode 100644 index 0000000..2581f9d --- /dev/null +++ b/lib/folk-bookmark.ts @@ -0,0 +1,348 @@ +import { FolkShape } from "./folk-shape"; +import { css, html } from "./tags"; + +const styles = css` + :host { + background: var(--rs-bg-surface, #fff); + color: var(--rs-text-primary, #1e293b); + border-radius: 10px; + box-shadow: 0 2px 12px rgba(0, 0, 0, 0.1); + min-width: 240px; + min-height: 100px; + cursor: pointer; + overflow: hidden; + } + + .card { + width: 100%; + height: 100%; + display: flex; + flex-direction: column; + overflow: hidden; + border-radius: 10px; + } + + .card-image { + width: 100%; + height: 160px; + object-fit: cover; + display: block; + flex-shrink: 0; + } + + .card-body { + padding: 12px 14px; + display: flex; + flex-direction: column; + gap: 6px; + flex: 1; + min-height: 0; + } + + .card-title { + font-size: 14px; + font-weight: 600; + line-height: 1.3; + color: var(--rs-text-primary, #1e293b); + overflow: hidden; + text-overflow: ellipsis; + display: -webkit-box; + -webkit-line-clamp: 2; + -webkit-box-orient: vertical; + } + + .card-description { + font-size: 12px; + color: #64748b; + line-height: 1.4; + overflow: hidden; + text-overflow: ellipsis; + display: -webkit-box; + -webkit-line-clamp: 2; + -webkit-box-orient: vertical; + } + + .card-domain { + display: flex; + align-items: center; + gap: 6px; + font-size: 11px; + color: #94a3b8; + margin-top: auto; + padding-top: 4px; + } + + .card-favicon { + width: 14px; + height: 14px; + border-radius: 2px; + } + + .url-input-container { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + height: 100%; + padding: 24px; + gap: 12px; + } + + .url-input-icon { + font-size: 28px; + } + + .url-input-label { + font-size: 13px; + color: #94a3b8; + } + + .url-input { + width: 100%; + max-width: 280px; + padding: 10px 14px; + border: 2px solid var(--rs-input-border, #e2e8f0); + border-radius: 8px; + font-size: 13px; + outline: none; + background: var(--rs-input-bg, #fff); + color: var(--rs-input-text, inherit); + } + + .url-input:focus { + border-color: #6366f1; + } + + .loading { + display: flex; + align-items: center; + justify-content: center; + height: 100%; + color: #94a3b8; + font-size: 13px; + } +`; + +declare global { + interface HTMLElementTagNameMap { + "folk-bookmark": FolkBookmark; + } +} + +export class FolkBookmark extends FolkShape { + static override tagName = "folk-bookmark"; + + 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; + } + + #url: string | null = null; + #title: string = ""; + #description: string = ""; + #image: string | null = null; + #domain: string = ""; + + get url() { return this.#url; } + set url(value: string | null) { + this.#url = value; + this.requestUpdate("url"); + this.dispatchEvent(new CustomEvent("url-change", { detail: { url: value } })); + } + + get bookmarkTitle() { return this.#title; } + set bookmarkTitle(v: string) { this.#title = v; } + + get description() { return this.#description; } + set description(v: string) { this.#description = v; } + + get image() { return this.#image; } + set image(v: string | null) { this.#image = v; } + + get domain() { return this.#domain; } + set domain(v: string) { this.#domain = v; } + + override createRenderRoot() { + const root = super.createRenderRoot(); + + this.#url = this.getAttribute("url") || null; + + const wrapper = document.createElement("div"); + wrapper.innerHTML = html` +
+ Pick a topic, choose a visual style and tone, and let AI generate a + complete 8-page zine — cover, content, and call-to-action — + with illustrations and editable text. Tweak any section, regenerate + images with feedback, then download or send to print. +
+Choose your style
+📷
+Punk
+Xerox & DIY
+🍄
+Mycelial
+Organic textures
+⚪
+Minimal
+Clean lines
+🎨
+Collage
+Mixed media
+🎶
+Retro
+70s vibes
+📚
+Academic
+Infographic
++ Each style generates unique illustrations, typography, and layouts +
+diff --git a/modules/rpubs/mod.ts b/modules/rpubs/mod.ts index 1a49236..297ff0b 100644 --- a/modules/rpubs/mod.ts +++ b/modules/rpubs/mod.ts @@ -319,6 +319,12 @@ routes.get("/api/artifact/:id/pdf", async (c) => { }); }); +// โโ Page: Zine Generator (redirect to canvas with auto-spawn) โโ +routes.get("/zine", (c) => { + const spaceSlug = c.req.param("space") || "personal"; + return c.redirect(`/${spaceSlug}?tool=folk-zine-gen`); +}); + // โโ Page: Editor โโ routes.get("/", (c) => { const spaceSlug = c.req.param("space") || "personal"; diff --git a/website/canvas.html b/website/canvas.html index fe17479..fbd61c4 100644 --- a/website/canvas.html +++ b/website/canvas.html @@ -1707,6 +1707,8 @@ folk-google-item, folk-piano, folk-embed, + folk-image, + folk-bookmark, folk-calendar, folk-map, folk-image-gen, @@ -2485,6 +2487,8 @@ FolkGoogleItem, FolkPiano, FolkEmbed, + FolkImage, + FolkBookmark, FolkCalendar, FolkMap, FolkImageGen, @@ -2613,6 +2617,8 @@ FolkGoogleItem.define(); FolkPiano.define(); FolkEmbed.define(); + FolkImage.define(); + FolkBookmark.define(); FolkCalendar.define(); FolkMap.define(); FolkImageGen.define(); @@ -2654,6 +2660,8 @@ shapeRegistry.register("folk-google-item", FolkGoogleItem); shapeRegistry.register("folk-piano", FolkPiano); shapeRegistry.register("folk-embed", FolkEmbed); + shapeRegistry.register("folk-image", FolkImage); + shapeRegistry.register("folk-bookmark", FolkBookmark); shapeRegistry.register("folk-calendar", FolkCalendar); shapeRegistry.register("folk-map", FolkMap); shapeRegistry.register("folk-image-gen", FolkImageGen); @@ -4222,6 +4230,19 @@ } } + // Auto-spawn shape from ?tool= URL param (e.g. ?tool=folk-zine-gen) + const toolParam = urlParams.get("tool"); + if (toolParam && shapeRegistry.has(toolParam)) { + // Wait for sync to initialize, then auto-place the shape + setTimeout(() => { + newShape(toolParam); + // Clean up URL param without reload + const cleanUrl = new URL(window.location.href); + cleanUrl.searchParams.delete("tool"); + history.replaceState(null, "", cleanUrl.pathname + cleanUrl.search); + }, 800); + } + // Feed shape โ pull live data from another layer/module // Arrow connection mode let connectMode = false;