diff --git a/README.md b/README.md index ef3a147a..ccb0d0bf 100644 --- a/README.md +++ b/README.md @@ -23,12 +23,30 @@ Unlike Canva or Adobe Express, this runs entirely on your own infrastructure. No - **Shapes** — rectangles, circles, triangles, lines with fill, stroke, border radius - **Image uploads** — drag-and-drop or click to upload, place on canvas - **Backgrounds** — solid colors, gradients, uploaded images -- **Canvas sizes** — LinkedIn Square (1080x1080), LinkedIn Landscape (1200x627), LinkedIn Portrait (1200x1500), Instagram Story (1080x1920) +- **Canvas sizes** — LinkedIn Square (1080x1080), LinkedIn Carousel (1080x1350), LinkedIn Landscape (1200x627), LinkedIn Portrait (1200x1500), Instagram Story (1080x1920) - **Undo/Redo** — full history with keyboard shortcuts (Cmd+Z / Cmd+Shift+Z) -- **2x PNG export** — crisp high-resolution output for social media +- **Carousel PDF export**: every page in one multi-page PDF, the only format LinkedIn accepts for a swipeable carousel +- **2x PNG export**: this page or every page, at exactly 2x the design size on any display - **Auto-save** — designs persist to SQLite with debounced saves - **Dual-mode UI** — human-optimized + AI-agent-optimized (`?agent=true`) +## Carousels + +A swipeable carousel on LinkedIn is a **document post**. LinkedIn's own help pages +document the format (PDF, DOC/DOCX or PPT/PPTX, up to 300 pages and 100 MB), and the +short-lived native multi-image carousel it shipped in 2022 was removed in June 2023, +with existing carousel posts deleted that December. Uploading a document is the way. + +OpenDesign builds one end to end: add a page per slide, then **Export → Carousel PDF** +writes them all into a single file, in canvas order, at the slide's own aspect ratio. +Upload that PDF to LinkedIn as a document. + +Format and limits per [LinkedIn Help: upload and share documents](https://www.linkedin.com/help/linkedin/answer/a518909). +Note that *Carousel Ads* is a separate paid ads product, unrelated to this. + +Use the **LinkedIn Carousel** canvas size (1080x1350, 4:5). It takes up the most room +on a phone screen. Exports are assembled in the browser, so nothing leaves the machine. + ## Quickstart ```bash diff --git a/agent.md b/agent.md index a635d6f5..86f66588 100644 --- a/agent.md +++ b/agent.md @@ -7,12 +7,15 @@ A design editor for creating professional social media graphics, especially Link - Pre-built LinkedIn post templates (Quote Card, Stats Highlight, Announcement, Tips List, Profile Card, Minimal Text) - Text editing with Google Fonts (Inter, Montserrat, Playfair Display) - Image uploads and placement -- Multiple canvas sizes (1080x1080 square, 1200x627 landscape) +- Multiple canvas sizes (1080x1080 square, 1080x1350 carousel, 1200x627 landscape) +- Multi-page designs exported as one PDF, which is how a LinkedIn carousel is published +- 2x PNG export, one page or all of them - Save and manage multiple designs ## When to use this template Use this template when the user wants to: - Create social media post images or graphics - Design LinkedIn posts, quote cards, or announcement banners +- Build a LinkedIn or Instagram carousel and export it as a PDF - Build a simple graphic design tool - Create branded visual content diff --git a/clawnify.json b/clawnify.json index 308dd912..3755920d 100644 --- a/clawnify.json +++ b/clawnify.json @@ -2,10 +2,10 @@ "$schema": "https://app.clawnify.com/schema/v1/clawnify.json", "version": 1, "name": "OpenDesign", - "description": "Design editor for creating professional social media graphics, LinkedIn posts, and visual content with a canvas-based UI, templates, multi-page support, image uploads, and text/shape tools. Like Canva. Best for graphic design tools, social media post creators, banner makers, or any visual content editor.", + "description": "Design editor for creating professional social media graphics, LinkedIn posts, carousels, and visual content with a canvas-based UI, templates, multi-page support, image uploads, and text/shape tools. Exports multi-page designs as a LinkedIn-ready carousel PDF, or as 2x PNGs. Like Canva. Best for graphic design tools, social media post creators, carousel makers, banner makers, or any visual content editor.", "icon": "icon.svg", "screenshot": "https://github.com/user-attachments/assets/55fcc0aa-4606-4f80-b56a-7734d7673914", - "tags": ["design", "graphics", "social-media", "canvas"], + "tags": ["design", "graphics", "social-media", "canvas", "carousel", "linkedin"], "app": { "framework": "preact+hono", diff --git a/package.json b/package.json index c35deb04..297d558e 100644 --- a/package.json +++ b/package.json @@ -14,6 +14,7 @@ "@tailwindcss/vite": "^4.2.2", "fabric": "^6.0.0", "hono": "^4.6.0", + "jspdf": "^4.2.1", "lucide-preact": "^0.575.0", "preact": "^10.25.0", "tailwindcss": "^4.2.2", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index bfe201b4..d5f5fb30 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -26,6 +26,9 @@ importers: hono: specifier: ^4.6.0 version: 4.12.10 + jspdf: + specifier: ^4.2.1 + version: 4.2.1 lucide-preact: specifier: ^0.575.0 version: 0.575.0(preact@10.29.1) @@ -149,6 +152,10 @@ packages: peerDependencies: '@babel/core': ^7.0.0-0 + '@babel/runtime@7.29.7': + resolution: {integrity: sha512-Nq8OhGWiZIZGV6hLHoyAKLLcJihP/xFeBMGJoUrxTX2psI8dCifzLhZISFb+VWS3wFMRDmCGw5R+dOySCqPLhw==} + engines: {node: '>=6.9.0'} + '@babel/template@7.28.6': resolution: {integrity: sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ==} engines: {node: '>=6.9.0'} @@ -1177,6 +1184,9 @@ packages: '@types/node@25.5.2': resolution: {integrity: sha512-tO4ZIRKNC+MDWV4qKVZe3Ql/woTnmHDr5JD8UI5hn2pwBrHEwOEMZK7WlNb5RKB6EoJ02gwmQS9OrjuFnZYdpg==} + '@types/pako@2.0.4': + resolution: {integrity: sha512-VWDCbrLeVXJM9fihYodcLiIv0ku+AlOa/TQ1SvYOaBuyrSKgEcro95LJyIsJ4vSo6BXIxOKxiJAat04CmST9Fw==} + '@types/webfontloader@1.6.38': resolution: {integrity: sha512-kUaF72Fv202suFx6yBrwXqeVRMx7hGtJTesyESZgn9sEPCUeDXm2p0SiyS1MTqW74nQP4p7JyrOCwZ7pNFns4w==} @@ -1548,6 +1558,9 @@ packages: resolution: {integrity: sha512-TqG08Xbt4rtlPsXgCjSUcZz/RsyEP57Qo21nCVRkw7zz9nR0co4SLkL9Q/zQh3tC1Yxap6M5jKFHUKV6SgPovg==} engines: {node: '>=16.20.0'} + fast-png@6.4.0: + resolution: {integrity: sha512-kAqZq1TlgBjZcLr5mcN6NP5Rv4V2f22z00c3g8vRrwkcqjerx7BEhPbOnWCPqaHUl2XWQBJQvOT/FQhdMT7X/Q==} + fdir@6.5.0: resolution: {integrity: sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==} engines: {node: '>=12.0.0'} @@ -1557,6 +1570,9 @@ packages: picomatch: optional: true + fflate@0.8.3: + resolution: {integrity: sha512-tbZNuJrLwGUp3zshBtdy4W+ORxZuIh8a5ilyIEQDC5rY1f3U20JMry0Ll3WBzU58EZKsEuJFXhb5gwv8CsPvgA==} + form-data@4.0.5: resolution: {integrity: sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==} engines: {node: '>= 6'} @@ -1661,6 +1677,9 @@ packages: inherits@2.0.4: resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==} + iobuffer@5.4.0: + resolution: {integrity: sha512-DRebOWuqDvxunfkNJAlc3IzWIPD5xVxwUNbHr7xKB8E6aLJxIPfNX3CoMJghcFjpv6RWQsrcJbghtEwSPoJqMA==} + is-fullwidth-code-point@3.0.0: resolution: {integrity: sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==} engines: {node: '>=8'} @@ -1694,6 +1713,9 @@ packages: engines: {node: '>=6'} hasBin: true + jspdf@4.2.1: + resolution: {integrity: sha512-YyAXyvnmjTbR4bHQRLzex3CuINCDlQnBqoSYyjJwTP2x9jDLuKDzy7aKUl0hgx3uhcl7xzg32agn5vlie6HIlQ==} + kleur@4.1.5: resolution: {integrity: sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ==} engines: {node: '>=6'} @@ -1882,6 +1904,9 @@ packages: openapi3-ts@4.5.0: resolution: {integrity: sha512-jaL+HgTq2Gj5jRcfdutgRGLosCy/hT8sQf6VOy+P+g36cZOjI1iukdPnijC+4CmeRzg/jEllJUboEic2FhxhtQ==} + pako@2.2.0: + resolution: {integrity: sha512-zJq6RP/5q+TO2OpFV3FHzlPnFjmkb7Nc99a5SNjJE+uu/PkpChs+NIZSSzbBoD+6kjiISXjfYdwj1ZRQ81dz/w==} + parse5@7.3.0: resolution: {integrity: sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw==} @@ -2270,6 +2295,12 @@ packages: zod@3.25.76: resolution: {integrity: sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==} +ignoredOptionalDependencies: + - canvg + - core-js + - dompurify + - html2canvas + snapshots: '@asteasolutions/zod-to-openapi@7.3.4(zod@3.25.76)': @@ -2383,6 +2414,8 @@ snapshots: transitivePeerDependencies: - supports-color + '@babel/runtime@7.29.7': {} + '@babel/template@7.28.6': dependencies: '@babel/code-frame': 7.29.0 @@ -3117,6 +3150,8 @@ snapshots: undici-types: 7.18.2 optional: true + '@types/pako@2.0.4': {} + '@types/webfontloader@1.6.38': {} abab@2.0.6: @@ -3491,10 +3526,18 @@ snapshots: - supports-color - utf-8-validate + fast-png@6.4.0: + dependencies: + '@types/pako': 2.0.4 + iobuffer: 5.4.0 + pako: 2.2.0 + fdir@6.5.0(picomatch@4.0.4): optionalDependencies: picomatch: 4.0.4 + fflate@0.8.3: {} + form-data@4.0.5: dependencies: asynckit: 0.4.0 @@ -3633,6 +3676,8 @@ snapshots: inherits@2.0.4: optional: true + iobuffer@5.4.0: {} + is-fullwidth-code-point@3.0.0: {} is-potential-custom-element-name@1.0.1: @@ -3682,6 +3727,12 @@ snapshots: json5@2.2.3: {} + jspdf@4.2.1: + dependencies: + '@babel/runtime': 7.29.7 + fast-png: 6.4.0 + fflate: 0.8.3 + kleur@4.1.5: {} kolorist@1.8.0: {} @@ -3851,6 +3902,8 @@ snapshots: dependencies: yaml: 2.8.3 + pako@2.2.0: {} + parse5@7.3.0: dependencies: entities: 6.0.1 diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index 239ace72..0bd1fe49 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -1,3 +1,11 @@ +# jsPDF's optional extras (.svg()/.html() support) are unused here, and canvg +# drags in node-canvas, a native build that would make `pnpm install` need +# cairo on every machine that clones this template. +ignoredOptionalDependencies: + - canvg + - core-js + - dompurify + - html2canvas allowBuilds: canvas: set this to true or false esbuild: set this to true or false diff --git a/src/client/components/toolbar.tsx b/src/client/components/toolbar.tsx index 6ae09359..6914e30d 100644 --- a/src/client/components/toolbar.tsx +++ b/src/client/components/toolbar.tsx @@ -9,6 +9,9 @@ import { Save, ChevronDown, Home, + FileText, + Image as ImageIcon, + Images, } from "lucide-preact"; import { useEditor, CANVAS_SIZES } from "../context"; @@ -27,6 +30,10 @@ export function Toolbar() { zoomIn, zoomOut, exportPNG, + exportAllPNG, + exportPDF, + exporting, + pages, saveDesign, saving, activeDesign, @@ -35,6 +42,7 @@ export function Toolbar() { } = useEditor(); const [showSizeDropdown, setShowSizeDropdown] = useState(false); + const [showExportMenu, setShowExportMenu] = useState(false); const [editingName, setEditingName] = useState(false); const [nameValue, setNameValue] = useState(""); @@ -56,6 +64,14 @@ export function Toolbar() { setEditingName(false); }; + const designName = activeDesign?.name ?? "design"; + const pageIds = pages.map((p) => p.id); + + const runExport = (fn: () => void) => { + setShowExportMenu(false); + fn(); + }; + return (