@tangle-network/agent-app 0.25.0 → 0.27.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{DesignCanvas-MA5EH324.js → DesignCanvas-73IUJJUP.js} +2 -2
- package/dist/DesignCanvasEditor-6F2UTOSW.js +9 -0
- package/dist/brand-extraction/index.d.ts +212 -0
- package/dist/brand-extraction/index.js +446 -0
- package/dist/brand-extraction/index.js.map +1 -0
- package/dist/{chunk-LPAVBGVY.js → chunk-27LQXQSS.js} +206 -124
- package/dist/chunk-27LQXQSS.js.map +1 -0
- package/dist/{chunk-XDCHKFBX.js → chunk-GZGF3JWQ.js} +77 -2
- package/dist/chunk-GZGF3JWQ.js.map +1 -0
- package/dist/{chunk-FSUR2752.js → chunk-HABVCJJT.js} +26 -7
- package/dist/chunk-HABVCJJT.js.map +1 -0
- package/dist/design-canvas-react/index.d.ts +46 -4
- package/dist/design-canvas-react/index.js +6 -4
- package/dist/design-canvas-react/index.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +7 -3
- package/dist/sandbox/index.d.ts +7 -1
- package/dist/sandbox/index.js +7 -3
- package/package.json +6 -1
- package/dist/DesignCanvasEditor-YLCO7I5I.js +0 -9
- package/dist/chunk-FSUR2752.js.map +0 -1
- package/dist/chunk-LPAVBGVY.js.map +0 -1
- package/dist/chunk-XDCHKFBX.js.map +0 -1
- /package/dist/{DesignCanvas-MA5EH324.js.map → DesignCanvas-73IUJJUP.js.map} +0 -0
- /package/dist/{DesignCanvasEditor-YLCO7I5I.js.map → DesignCanvasEditor-6F2UTOSW.js.map} +0 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/brand-extraction/extract.ts","../../src/brand-extraction/map.ts"],"sourcesContent":["/**\n * Brand-kit extraction engine.\n *\n * Given a website URL (or raw HTML), parse the page and pull a typed BrandKit:\n * logo candidates, color palette, fonts, and prominent images. Parsing is\n * regex-based on purpose — this runs on Cloudflare Workers and other edge\n * runtimes where DOM libraries (jsdom/cheerio) are unavailable, and the page\n * structures we read (link tags, meta tags, CSS custom properties, font-family\n * declarations) are flat enough that a DOM buys nothing.\n *\n * Degrades gracefully: a missing favicon, no CSS tokens, or an image-only page\n * each narrow the kit rather than failing it. The ONLY hard failures are a bad\n * input (no html and no fetch) and a fetch error — both returned as a typed\n * `{ succeeded: false }` outcome so callers never mistake an empty kit for a\n * real one.\n */\n\nimport type {\n BrandColor,\n BrandExtractionResult,\n BrandFont,\n BrandImage,\n BrandKit,\n BrandLogoCandidate,\n ExtractBrandKitOptions,\n FetchLike,\n} from './types'\n\nconst DEFAULT_TIMEOUT_MS = 15000\nconst DEFAULT_MAX_PER_LIST = 12\n\n// ── URL helpers ──────────────────────────────────────────────────────────────\n\n/** Normalize a user-supplied site URL: add https:// when scheme-less, validate. */\nexport function normalizeSiteUrl(raw: string): string | null {\n const trimmed = raw.trim()\n if (!trimmed) return null\n // A non-http(s) scheme is an explicit reject — don't paper over it by\n // prepending https:// (that would turn \"ftp://x\" into host \"ftp\").\n if (/^[a-z][a-z0-9+.-]*:/i.test(trimmed) && !/^https?:\\/\\//i.test(trimmed)) return null\n const withScheme = /^https?:\\/\\//i.test(trimmed) ? trimmed : `https://${trimmed}`\n try {\n const u = new URL(withScheme)\n if (u.protocol !== 'http:' && u.protocol !== 'https:') return null\n return u.toString()\n } catch {\n return null\n }\n}\n\n/** Resolve a possibly-relative href against the page URL. Returns null for\n * data: URIs and unresolvable values — we want servable http(s) URLs only. */\nfunction absolutize(href: string, base: string): string | null {\n const v = href.trim()\n if (!v || v.startsWith('data:') || v.startsWith('javascript:')) return null\n try {\n const u = new URL(v, base)\n if (u.protocol !== 'http:' && u.protocol !== 'https:') return null\n return u.toString()\n } catch {\n return null\n }\n}\n\n// ── Attribute parsing ────────────────────────────────────────────────────────\n\n/** Pull all `<tag ...>` open-tags from html (case-insensitive). */\nfunction matchTags(html: string, tag: string): string[] {\n const re = new RegExp(`<${tag}\\\\b[^>]*>`, 'gi')\n return html.match(re) ?? []\n}\n\n/** Read an attribute value from a single tag string. Handles quoted and\n * unquoted values. Returns undefined when absent. */\nfunction attr(tag: string, name: string): string | undefined {\n const quoted = new RegExp(`\\\\b${name}\\\\s*=\\\\s*[\"']([^\"']*)[\"']`, 'i').exec(tag)\n if (quoted) return decodeEntities(quoted[1] ?? '')\n const bare = new RegExp(`\\\\b${name}\\\\s*=\\\\s*([^\\\\s\"'>]+)`, 'i').exec(tag)\n return bare ? decodeEntities(bare[1] ?? '') : undefined\n}\n\nfunction decodeEntities(s: string): string {\n return s\n .replace(/&/g, '&')\n .replace(/</g, '<')\n .replace(/>/g, '>')\n .replace(/"/g, '\"')\n .replace(/'/g, \"'\")\n .replace(///gi, '/')\n}\n\n// ── Name / description ───────────────────────────────────────────────────────\n\nfunction extractName(html: string): string | undefined {\n for (const tag of matchTags(html, 'meta')) {\n const prop = (attr(tag, 'property') ?? attr(tag, 'name'))?.toLowerCase()\n if (prop === 'og:site_name') {\n const c = attr(tag, 'content')?.trim()\n if (c) return c\n }\n }\n const title = /<title[^>]*>([\\s\\S]*?)<\\/title>/i.exec(html)?.[1]\n if (title) {\n // \"Acme — the best widget\" → \"Acme\". Split on common separators, take head.\n const head = (decodeEntities(title).split(/\\s+[|–—\\-:·]\\s+/)[0] ?? '').trim()\n if (head) return head\n }\n return undefined\n}\n\nfunction extractDescription(html: string): string | undefined {\n for (const tag of matchTags(html, 'meta')) {\n const prop = (attr(tag, 'property') ?? attr(tag, 'name'))?.toLowerCase()\n if (prop === 'description' || prop === 'og:description') {\n const c = attr(tag, 'content')?.trim()\n if (c) return c\n }\n }\n return undefined\n}\n\n// ── Logos ────────────────────────────────────────────────────────────────────\n\nconst MIME_BY_EXT: Record<string, string> = {\n png: 'image/png',\n jpg: 'image/jpeg',\n jpeg: 'image/jpeg',\n gif: 'image/gif',\n webp: 'image/webp',\n svg: 'image/svg+xml',\n ico: 'image/x-icon',\n}\n\nfunction mimeFromUrl(url: string): string | undefined {\n const ext = new URL(url).pathname.split('.').pop()?.toLowerCase()\n return ext ? MIME_BY_EXT[ext] : undefined\n}\n\nfunction parseSizes(sizes: string | undefined): { width?: number; height?: number } {\n if (!sizes) return {}\n const m = /(\\d+)\\s*[x×]\\s*(\\d+)/i.exec(sizes)\n if (!m) return {}\n return { width: Number(m[1]), height: Number(m[2]) }\n}\n\nfunction extractLogos(html: string, base: string): BrandLogoCandidate[] {\n const out: BrandLogoCandidate[] = []\n const seen = new Set<string>()\n const push = (c: BrandLogoCandidate) => {\n if (seen.has(c.url)) return\n seen.add(c.url)\n out.push(c)\n }\n\n // link rel icons / apple-touch-icon\n for (const tag of matchTags(html, 'link')) {\n const rel = attr(tag, 'rel')?.toLowerCase() ?? ''\n const href = attr(tag, 'href')\n if (!href) continue\n const url = absolutize(href, base)\n if (!url) continue\n const { width, height } = parseSizes(attr(tag, 'sizes'))\n if (rel.includes('apple-touch-icon')) {\n push({ url, source: 'apple-touch-icon', confidence: 0.7, width, height, mimeType: mimeFromUrl(url) })\n } else if (rel.split(/\\s+/).includes('icon') || rel.includes('shortcut icon')) {\n push({ url, source: 'favicon', confidence: 0.5, width, height, mimeType: mimeFromUrl(url) })\n }\n }\n\n // og:image (often shows the logo in context — moderate confidence)\n for (const tag of matchTags(html, 'meta')) {\n const prop = (attr(tag, 'property') ?? attr(tag, 'name'))?.toLowerCase()\n if (prop === 'og:image' || prop === 'og:image:url') {\n const href = attr(tag, 'content')\n const url = href ? absolutize(href, base) : null\n if (url) push({ url, source: 'og:image', confidence: 0.55, mimeType: mimeFromUrl(url) })\n }\n }\n\n // <img> whose src/class/alt/id mentions \"logo\" — the strongest in-page signal\n for (const tag of matchTags(html, 'img')) {\n const src = attr(tag, 'src') ?? attr(tag, 'data-src')\n if (!src) continue\n const alt = attr(tag, 'alt')\n const cls = attr(tag, 'class') ?? ''\n const id = attr(tag, 'id') ?? ''\n const looksLikeLogo = /logo|brand|wordmark/i.test(`${src} ${alt ?? ''} ${cls} ${id}`)\n if (!looksLikeLogo) continue\n const url = absolutize(src, base)\n if (!url) continue\n const w = attr(tag, 'width')\n const h = attr(tag, 'height')\n push({\n url,\n source: 'img-logo',\n confidence: 0.85,\n alt,\n width: w ? Number(w) || undefined : undefined,\n height: h ? Number(h) || undefined : undefined,\n mimeType: mimeFromUrl(url),\n })\n }\n\n // No favicon link tag at all? Fall back to the conventional /favicon.ico —\n // it almost always exists. Lowest confidence.\n if (!out.some((l) => l.source === 'favicon' || l.source === 'apple-touch-icon')) {\n const fav = absolutize('/favicon.ico', base)\n if (fav) push({ url: fav, source: 'favicon', confidence: 0.3, mimeType: 'image/x-icon' })\n }\n\n return out.sort((a, b) => b.confidence - a.confidence)\n}\n\n// ── Colors ───────────────────────────────────────────────────────────────────\n\n/** Normalize any CSS color literal to #rrggbb / #rrggbbaa hex, or null. */\nexport function normalizeColor(raw: string): string | null {\n const v = raw.trim().toLowerCase()\n // #rgb / #rgba / #rrggbb / #rrggbbaa\n const hex = /^#([0-9a-f]{3,4}|[0-9a-f]{6}|[0-9a-f]{8})$/i.exec(v)\n if (hex) {\n const h = hex[1] ?? ''\n // Expand 3/4-digit shorthand by doubling each nibble.\n if (h.length === 3 || h.length === 4) {\n return `#${h.split('').map((c) => c + c).join('')}`\n }\n return `#${h}`\n }\n // rgb()/rgba()\n const rgb = /^rgba?\\(\\s*([\\d.]+)[\\s,]+([\\d.]+)[\\s,]+([\\d.]+)(?:[\\s,/]+([\\d.%]+))?\\s*\\)$/.exec(v)\n if (rgb) {\n const to255 = (n: string | undefined) => Math.max(0, Math.min(255, Math.round(Number(n))))\n const r = to255(rgb[1]).toString(16).padStart(2, '0')\n const g = to255(rgb[2]).toString(16).padStart(2, '0')\n const b = to255(rgb[3]).toString(16).padStart(2, '0')\n let a = ''\n if (rgb[4] !== undefined) {\n const av = rgb[4].endsWith('%') ? Number(rgb[4].slice(0, -1)) / 100 : Number(rgb[4])\n const ai = Math.max(0, Math.min(255, Math.round(av * 255)))\n if (ai < 255) a = ai.toString(16).padStart(2, '0')\n }\n return `#${r}${g}${b}${a}`\n }\n return null\n}\n\nconst COLOR_LITERAL_RE = /#[0-9a-fA-F]{3,8}\\b|rgba?\\([^)]*\\)/g\nconst ROOT_VAR_RE = /(--[a-z0-9-]*(?:color|colour|bg|background|accent|brand|primary|secondary|surface|text|fg|ink)[a-z0-9-]*)\\s*:\\s*([^;}{]+)/gi\n\nfunction extractPalette(html: string): BrandColor[] {\n const byHex = new Map<string, BrandColor>()\n const bump = (hex: string, fromToken: boolean, tokenName?: string) => {\n const existing = byHex.get(hex)\n if (existing) {\n existing.occurrences += 1\n if (fromToken && !existing.fromToken) {\n existing.fromToken = true\n existing.tokenName = tokenName\n }\n } else {\n byHex.set(hex, { hex, occurrences: 1, fromToken, ...(tokenName ? { tokenName } : {}) })\n }\n }\n\n // CSS custom properties — highest-fidelity design tokens.\n for (const m of html.matchAll(ROOT_VAR_RE)) {\n const tokenName = m[1]\n const value = m[2]\n if (!value) continue\n const lit = value.match(COLOR_LITERAL_RE)?.[0]\n if (!lit) continue\n const hex = normalizeColor(lit)\n if (hex) bump(hex, true, tokenName)\n }\n\n // All other color literals across the document (inline styles, <style>).\n for (const m of html.matchAll(COLOR_LITERAL_RE)) {\n const hex = normalizeColor(m[0])\n if (hex) bump(hex, false)\n }\n\n // Rank: token-sourced first, then by popularity. Drop pure white/black noise\n // to the back so a real brand color leads.\n const isNeutral = (h: string) => /^#(0{6}|f{6})(ff)?$/i.test(h)\n return [...byHex.values()].sort((a, b) => {\n if (a.fromToken !== b.fromToken) return a.fromToken ? -1 : 1\n const an = isNeutral(a.hex) ? 1 : 0\n const bn = isNeutral(b.hex) ? 1 : 0\n if (an !== bn) return an - bn\n return b.occurrences - a.occurrences\n })\n}\n\n// ── Fonts ────────────────────────────────────────────────────────────────────\n\nconst GENERIC_FAMILIES = new Set([\n 'serif', 'sans-serif', 'monospace', 'cursive', 'fantasy', 'system-ui',\n 'ui-sans-serif', 'ui-serif', 'ui-monospace', 'ui-rounded', 'inherit', 'initial', 'unset',\n])\n\n/** font-family declarations, with the selector chunk preceding them for role\n * inference. Captures the selector text in group 1, the stack in group 2. */\nconst FONT_DECL_RE = /([^{}]*)\\{[^{}]*font-family\\s*:\\s*([^;}{]+)[;}]/gi\n\nfunction parseFontStack(raw: string | undefined): string[] {\n if (!raw) return []\n return raw\n .split(',')\n .map((s) => s.trim().replace(/^[\"']|[\"']$/g, ''))\n .filter(Boolean)\n}\n\nfunction roleFromSelector(selector: string): 'display' | 'body' | 'unknown' {\n const s = selector.toLowerCase()\n if (/\\b(h[1-3]|\\.h[1-3]|heading|title|display|hero)\\b/.test(s)) return 'display'\n if (/\\b(body|p|html|\\.text|\\.content|root|:root|\\*)\\b/.test(s)) return 'body'\n return 'unknown'\n}\n\nfunction extractFonts(html: string): BrandFont[] {\n // Restrict to <style> blocks + the whole doc; inline style=\"\" attributes are\n // included because the regex matches any `{...font-family...}` chunk, but we\n // also scan style attributes explicitly below.\n const byFamily = new Map<string, BrandFont>()\n const consider = (family: string, stack: string[], role: BrandFont['role']) => {\n const key = family.toLowerCase()\n if (!family || GENERIC_FAMILIES.has(key)) return\n const existing = byFamily.get(key)\n if (existing) {\n existing.occurrences += 1\n if (existing.role === 'unknown' && role !== 'unknown') existing.role = role\n } else {\n byFamily.set(key, { family, stack, role, occurrences: 1 })\n }\n }\n\n for (const m of html.matchAll(FONT_DECL_RE)) {\n const selector = m[1] ?? ''\n const stack = parseFontStack(m[2])\n const primary = stack[0]\n if (!primary) continue\n consider(primary, stack, roleFromSelector(selector))\n }\n\n // Bare `font-family:` declarations inside inline style attributes and tokens.\n const BARE_RE = /font-family\\s*:\\s*([^;\"'}{]+)/gi\n for (const m of html.matchAll(BARE_RE)) {\n const stack = parseFontStack(m[1])\n const primary = stack[0]\n if (!primary) continue\n consider(primary, stack, 'unknown')\n }\n\n // Google Fonts <link href=\"...family=Inter:wght@400...\"> — strong signal.\n for (const tag of matchTags(html, 'link')) {\n const href = attr(tag, 'href') ?? ''\n if (!/fonts\\.googleapis\\.com/i.test(href)) continue\n for (const fam of href.matchAll(/family=([^:&]+)/gi)) {\n const captured = fam[1]\n if (!captured) continue\n const family = decodeURIComponent(captured.replace(/\\+/g, ' ')).trim()\n consider(family, [family], 'display')\n }\n }\n\n return [...byFamily.values()].sort((a, b) => {\n const rank = (r: BrandFont['role']) => (r === 'display' ? 0 : r === 'body' ? 1 : 2)\n const rr = rank(a.role) - rank(b.role)\n if (rr !== 0) return rr\n return b.occurrences - a.occurrences\n })\n}\n\n// ── Images ───────────────────────────────────────────────────────────────────\n\nfunction extractImages(html: string, base: string): BrandImage[] {\n const out: BrandImage[] = []\n const seen = new Set<string>()\n const push = (img: BrandImage) => {\n if (seen.has(img.url)) return\n seen.add(img.url)\n out.push(img)\n }\n\n for (const tag of matchTags(html, 'meta')) {\n const prop = (attr(tag, 'property') ?? attr(tag, 'name'))?.toLowerCase()\n const href = attr(tag, 'content')\n const url = href ? absolutize(href, base) : null\n if (!url) continue\n if (prop === 'og:image' || prop === 'og:image:url') push({ url, source: 'og:image' })\n else if (prop === 'twitter:image' || prop === 'twitter:image:src') push({ url, source: 'twitter:image' })\n }\n\n for (const tag of matchTags(html, 'img')) {\n const src = attr(tag, 'src') ?? attr(tag, 'data-src')\n if (!src) continue\n const url = absolutize(src, base)\n if (!url) continue\n const cls = `${attr(tag, 'class') ?? ''} ${attr(tag, 'id') ?? ''}`\n // Skip obvious logos/icons — those go in the logos list, not images.\n if (/logo|icon|favicon|wordmark/i.test(`${url} ${cls}`)) continue\n const isHero = /hero|banner|cover|feature|splash/i.test(cls)\n const w = attr(tag, 'width')\n const h = attr(tag, 'height')\n push({\n url,\n source: isHero ? 'img-hero' : 'img-content',\n alt: attr(tag, 'alt'),\n width: w ? Number(w) || undefined : undefined,\n height: h ? Number(h) || undefined : undefined,\n })\n }\n\n const rank = (s: BrandImage['source']) =>\n s === 'og:image' ? 0 : s === 'twitter:image' ? 1 : s === 'img-hero' ? 2 : 3\n return out.sort((a, b) => rank(a.source) - rank(b.source))\n}\n\n// ── Public API ───────────────────────────────────────────────────────────────\n\n/** Parse already-fetched HTML into a BrandKit. Pure — no network. Exposed so\n * callers that hold the HTML (or want to combine multiple pages) can reuse the\n * parsing without re-fetching. */\nexport function parseBrandKit(html: string, sourceUrl: string, maxPerList = DEFAULT_MAX_PER_LIST): BrandKit {\n const logos = extractLogos(html, sourceUrl).slice(0, maxPerList)\n const palette = extractPalette(html).slice(0, maxPerList)\n const fonts = extractFonts(html).slice(0, maxPerList)\n const images = extractImages(html, sourceUrl).slice(0, maxPerList)\n const name = extractName(html)\n const description = extractDescription(html)\n\n return {\n sourceUrl,\n ...(name ? { name } : {}),\n ...(description ? { description } : {}),\n logos,\n palette,\n fonts,\n images,\n extractedFrom: [sourceUrl],\n }\n}\n\n/**\n * Fetch a website (or use supplied HTML) and extract its BrandKit.\n *\n * Returns a typed outcome — callers MUST check `succeeded`. A fetch failure or\n * empty input is a real, surfaced error, never an empty kit masquerading as a\n * result. Parsing itself never throws: a malformed page yields a sparse kit\n * with warnings, which is information, not failure.\n */\nexport async function extractBrandKit(\n url: string,\n options: ExtractBrandKitOptions = {},\n): Promise<BrandExtractionResult> {\n const { html: providedHtml, fetchImpl, timeoutMs = DEFAULT_TIMEOUT_MS, maxPerList = DEFAULT_MAX_PER_LIST } = options\n const warnings: string[] = []\n\n const sourceUrl = normalizeSiteUrl(url)\n if (!sourceUrl) {\n return { succeeded: false, error: `Not a valid http(s) URL: ${JSON.stringify(url)}`, stage: 'input' }\n }\n\n let html = providedHtml\n if (html === undefined) {\n const doFetch: FetchLike | undefined =\n fetchImpl ?? (typeof globalThis.fetch === 'function'\n ? (u, init) => globalThis.fetch(u, init as RequestInit)\n : undefined)\n if (!doFetch) {\n return { succeeded: false, error: 'No html provided and no fetch implementation available', stage: 'input' }\n }\n const controller = new AbortController()\n const timer = setTimeout(() => controller.abort(), timeoutMs)\n try {\n const res = await doFetch(sourceUrl, {\n signal: controller.signal,\n headers: { 'user-agent': 'TangleBrandExtractor/1.0 (+https://tangle.tools)' },\n })\n if (!res.ok) {\n return { succeeded: false, error: `Fetch failed: HTTP ${res.status} for ${sourceUrl}`, stage: 'fetch' }\n }\n html = await res.text()\n } catch (err) {\n const reason = err instanceof Error ? err.message : String(err)\n return { succeeded: false, error: `Fetch error for ${sourceUrl}: ${reason}`, stage: 'fetch' }\n } finally {\n clearTimeout(timer)\n }\n }\n\n if (!html.trim()) {\n return { succeeded: false, error: `Empty document for ${sourceUrl}`, stage: 'parse' }\n }\n\n const kit = parseBrandKit(html, sourceUrl, maxPerList)\n\n if (kit.logos.length === 0) warnings.push('No logo candidates found.')\n if (kit.palette.length === 0) warnings.push('No colors extracted — page has no inline CSS or design tokens.')\n if (kit.fonts.length === 0) warnings.push('No custom fonts found — site likely uses system defaults.')\n if (kit.images.length === 0) warnings.push('No prominent images found.')\n\n return { succeeded: true, kit, warnings }\n}\n","/**\n * Pure mapping helpers from an extracted BrandKit onto consumable shapes.\n *\n * Extraction yields ranked candidate lists; products need decided roles\n * (background vs accent, display vs body). These helpers make the obvious,\n * defensible default choice and expose the reasoning so a confirmation UI can\n * show \"we picked X — change it?\". They NEVER invent a value: when a role can't\n * be filled from the kit, it is omitted, and the caller decides the fallback.\n */\n\nimport type { BrandColor, BrandFont, BrandKit } from './types'\n\n/** Relative luminance (0..1) of an #rrggbb(aa) hex — for light/dark sorting. */\nexport function luminance(hex: string): number {\n const h = hex.replace('#', '').slice(0, 6)\n const r = parseInt(h.slice(0, 2), 16) / 255\n const g = parseInt(h.slice(2, 4), 16) / 255\n const b = parseInt(h.slice(4, 6), 16) / 255\n const lin = (c: number) => (c <= 0.03928 ? c / 12.92 : ((c + 0.055) / 1.055) ** 2.4)\n return 0.2126 * lin(r) + 0.7152 * lin(g) + 0.0722 * lin(b)\n}\n\nfunction isGreyish(hex: string): boolean {\n const h = hex.replace('#', '').slice(0, 6)\n const r = parseInt(h.slice(0, 2), 16)\n const g = parseInt(h.slice(2, 4), 16)\n const b = parseInt(h.slice(4, 6), 16)\n const max = Math.max(r, g, b)\n const min = Math.min(r, g, b)\n return max - min < 18\n}\n\nexport interface DecidedPalette {\n /** Lightest neutral — page background. */\n background?: string\n /** A raised neutral one step from background. */\n surface?: string\n /** Darkest readable color — primary text. */\n textPrimary?: string\n /** Mid-tone neutral — secondary text. */\n textSecondary?: string\n /** Most saturated / popular brand color — accent. */\n accent?: string\n /** Readable text color to sit on the accent (black or white by contrast). */\n accentText?: string\n}\n\n/**\n * Assign palette roles from the ranked colors. Heuristic, not authoritative —\n * a confirmation step should let the user correct it. Roles only appear when\n * the kit actually contained a color that fits.\n */\nexport function decidePalette(palette: BrandColor[]): DecidedPalette {\n if (palette.length === 0) return {}\n const hexes = palette.map((c) => c.hex)\n const greys = hexes.filter(isGreyish).sort((a, b) => luminance(b) - luminance(a))\n const colored = hexes.filter((h) => !isGreyish(h))\n\n // Accent: the top-ranked non-grey (token-first, popularity-first) color.\n const accent = colored[0]\n // Background: lightest grey (or lightest overall when no greys).\n const sortedLight = [...hexes].sort((a, b) => luminance(b) - luminance(a))\n const background = greys[0] ?? sortedLight[0]\n // Surface: next grey under background, else second-lightest.\n const surface = greys[1] ?? sortedLight.find((h) => h !== background)\n // Text primary: darkest color overall.\n const textPrimary = [...hexes].sort((a, b) => luminance(a) - luminance(b))[0]\n // Text secondary: a mid-luminance grey distinct from primary/background.\n const textSecondary = greys.find((h) => h !== background && h !== surface && h !== textPrimary)\n\n const result: DecidedPalette = {}\n if (background) result.background = background\n if (surface) result.surface = surface\n if (textPrimary) result.textPrimary = textPrimary\n if (textSecondary) result.textSecondary = textSecondary\n if (accent) {\n result.accent = accent\n result.accentText = luminance(accent) > 0.45 ? '#000000' : '#ffffff'\n }\n return result\n}\n\nexport interface DecidedFonts {\n display?: BrandFont\n body?: BrandFont\n}\n\n/** Pick a display and body font from the ranked list. When only one usable\n * font exists it fills both roles — a single-typeface brand is valid. */\nexport function decideFonts(fonts: BrandFont[]): DecidedFonts {\n if (fonts.length === 0) return {}\n const display = fonts.find((f) => f.role === 'display') ?? fonts[0]\n const body = fonts.find((f) => f.role === 'body') ?? fonts.find((f) => f !== display) ?? display\n return { display, body }\n}\n\n/** Everything a confirmation step needs from a kit, with roles decided. */\nexport interface DecidedBrandKit {\n name?: string\n description?: string\n sourceUrl: string\n palette: DecidedPalette\n fonts: DecidedFonts\n /** Best logo URL, when any candidate was found. */\n primaryLogoUrl?: string\n /** All logo URLs, ranked. */\n logoUrls: string[]\n /** Prominent image URLs, ranked. */\n imageUrls: string[]\n extractedFrom: string[]\n}\n\n/** Collapse a raw BrandKit into decided roles — the shape a product persists. */\nexport function decideBrandKit(kit: BrandKit): DecidedBrandKit {\n const palette = decidePalette(kit.palette)\n const fonts = decideFonts(kit.fonts)\n const logoUrls = kit.logos.map((l) => l.url)\n const result: DecidedBrandKit = {\n sourceUrl: kit.sourceUrl,\n palette,\n fonts,\n logoUrls,\n imageUrls: kit.images.map((i) => i.url),\n extractedFrom: kit.extractedFrom,\n }\n if (kit.name) result.name = kit.name\n if (kit.description) result.description = kit.description\n if (logoUrls[0]) result.primaryLogoUrl = logoUrls[0]\n return result\n}\n"],"mappings":"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","names":[]}
|
|
@@ -731,6 +731,7 @@ function glyph(paths) {
|
|
|
731
731
|
}
|
|
732
732
|
var UndoGlyph = glyph(/* @__PURE__ */ jsx2("path", { d: "M3 7v6h6M3 13a9 9 0 1 0 3-7.7" }));
|
|
733
733
|
var RedoGlyph = glyph(/* @__PURE__ */ jsx2("path", { d: "M21 7v6h-6M21 13a9 9 0 1 1-3-7.7" }));
|
|
734
|
+
var SwapGlyph = glyph(/* @__PURE__ */ jsx2("path", { d: "M7 4 3 8l4 4M3 8h14M17 20l4-4-4-4M21 16H7" }));
|
|
734
735
|
var EyeGlyph = glyph(
|
|
735
736
|
/* @__PURE__ */ jsxs2(Fragment2, { children: [
|
|
736
737
|
/* @__PURE__ */ jsx2("path", { d: "M2 12s3-7 10-7 10 7 10 7-3 7-10 7-10-7-10-7z" }),
|
|
@@ -887,7 +888,8 @@ function PagesStrip({
|
|
|
887
888
|
onAddPage,
|
|
888
889
|
onDuplicatePage,
|
|
889
890
|
onDeletePage,
|
|
890
|
-
onReorderPage
|
|
891
|
+
onReorderPage,
|
|
892
|
+
canManagePages = true
|
|
891
893
|
}) {
|
|
892
894
|
const [thumbnails, setThumbnails] = useState({});
|
|
893
895
|
const thumbnailVersionRef = useRef(0);
|
|
@@ -983,7 +985,7 @@ function PagesStrip({
|
|
|
983
985
|
}
|
|
984
986
|
),
|
|
985
987
|
/* @__PURE__ */ jsx3("span", { className: "max-w-[80px] truncate text-[10px] text-[var(--text-secondary)]", children: page.name }),
|
|
986
|
-
canWrite ? /* @__PURE__ */ jsxs3("div", { className: "pointer-events-none absolute -top-1 right-0 flex gap-0.5 opacity-0 transition-opacity group-hover:pointer-events-auto group-hover:opacity-100", children: [
|
|
988
|
+
canWrite && canManagePages ? /* @__PURE__ */ jsxs3("div", { className: "pointer-events-none absolute -top-1 right-0 flex gap-0.5 opacity-0 transition-opacity group-hover:pointer-events-auto group-hover:opacity-100", children: [
|
|
987
989
|
/* @__PURE__ */ jsx3(
|
|
988
990
|
"button",
|
|
989
991
|
{
|
|
@@ -1017,7 +1019,7 @@ function PagesStrip({
|
|
|
1017
1019
|
page.id
|
|
1018
1020
|
);
|
|
1019
1021
|
}),
|
|
1020
|
-
canWrite ? /* @__PURE__ */ jsxs3(
|
|
1022
|
+
canWrite && canManagePages ? /* @__PURE__ */ jsxs3(
|
|
1021
1023
|
"button",
|
|
1022
1024
|
{
|
|
1023
1025
|
type: "button",
|
|
@@ -1527,6 +1529,7 @@ function Toolbar({
|
|
|
1527
1529
|
page,
|
|
1528
1530
|
selectedElements,
|
|
1529
1531
|
canWrite,
|
|
1532
|
+
mode = "edit",
|
|
1530
1533
|
canUndo,
|
|
1531
1534
|
canRedo,
|
|
1532
1535
|
gridEnabled,
|
|
@@ -1562,46 +1565,55 @@ function Toolbar({
|
|
|
1562
1565
|
const selectedIds = selectedElements.map((e) => e.id);
|
|
1563
1566
|
const isGroup = single?.kind === "group";
|
|
1564
1567
|
const groupable = selectedElements.length >= 2;
|
|
1568
|
+
const review = mode === "review";
|
|
1565
1569
|
return /* @__PURE__ */ jsxs5("div", { className: "flex min-h-11 shrink-0 flex-wrap items-center gap-x-2 gap-y-1 border-b border-[var(--border-default)] bg-[var(--bg-input)] px-3 py-1", children: [
|
|
1566
1570
|
/* @__PURE__ */ jsxs5("div", { className: "flex shrink-0 items-center gap-2", children: [
|
|
1567
1571
|
/* @__PURE__ */ jsx5("button", { type: "button", "aria-label": "Undo", disabled: !canUndo || !canWrite, onClick: onUndo, className: BTN2, children: /* @__PURE__ */ jsx5(UndoGlyph, { className: "h-3.5 w-3.5" }) }),
|
|
1568
1572
|
/* @__PURE__ */ jsx5("button", { type: "button", "aria-label": "Redo", disabled: !canRedo || !canWrite, onClick: onRedo, className: BTN2, children: /* @__PURE__ */ jsx5(RedoGlyph, { className: "h-3.5 w-3.5" }) }),
|
|
1569
|
-
|
|
1570
|
-
|
|
1571
|
-
|
|
1572
|
-
|
|
1573
|
-
|
|
1573
|
+
!review ? /* @__PURE__ */ jsxs5(Fragment4, { children: [
|
|
1574
|
+
SEP,
|
|
1575
|
+
/* @__PURE__ */ jsx5("button", { type: "button", "aria-label": "Toggle rulers", "aria-pressed": showRulers, onClick: onToggleRulers, className: showRulers ? BTN_ACTIVE : BTN2, children: /* @__PURE__ */ jsx5(RulerGlyph, { className: "h-3.5 w-3.5" }) }),
|
|
1576
|
+
/* @__PURE__ */ jsx5("button", { type: "button", "aria-label": "Toggle grid", "aria-pressed": gridEnabled, onClick: onToggleGrid, className: gridEnabled ? BTN_ACTIVE : BTN2, children: /* @__PURE__ */ jsx5(GridGlyph, { className: "h-3.5 w-3.5" }) }),
|
|
1577
|
+
/* @__PURE__ */ jsx5("button", { type: "button", "aria-label": "Toggle snap", "aria-pressed": snapEnabled, onClick: onToggleSnap, className: snapEnabled ? BTN_ACTIVE : BTN2, children: /* @__PURE__ */ jsx5(MagnetGlyph, { className: "h-3.5 w-3.5" }) }),
|
|
1578
|
+
/* @__PURE__ */ jsx5("button", { type: "button", "aria-label": "Toggle bleed overlay", "aria-pressed": showBleed, onClick: onToggleBleed, className: showBleed ? BTN_ACTIVE : BTN2, disabled: !page.bleed, children: /* @__PURE__ */ jsx5(BleedGlyph, { className: "h-3.5 w-3.5" }) })
|
|
1579
|
+
] }) : null
|
|
1574
1580
|
] }),
|
|
1575
|
-
|
|
1576
|
-
|
|
1577
|
-
|
|
1578
|
-
|
|
1579
|
-
|
|
1580
|
-
|
|
1581
|
-
|
|
1582
|
-
|
|
1583
|
-
|
|
1584
|
-
|
|
1585
|
-
|
|
1586
|
-
|
|
1587
|
-
|
|
1588
|
-
|
|
1589
|
-
|
|
1590
|
-
|
|
1591
|
-
|
|
1592
|
-
|
|
1593
|
-
|
|
1594
|
-
|
|
1595
|
-
|
|
1596
|
-
|
|
1597
|
-
|
|
1598
|
-
|
|
1599
|
-
|
|
1600
|
-
|
|
1601
|
-
|
|
1602
|
-
|
|
1603
|
-
|
|
1604
|
-
|
|
1581
|
+
/* @__PURE__ */ jsx5("div", { className: "flex flex-wrap items-center gap-x-2 gap-y-1", children: hasSelection ? /* @__PURE__ */ jsxs5(Fragment4, { children: [
|
|
1582
|
+
SEP,
|
|
1583
|
+
/* @__PURE__ */ jsx5(
|
|
1584
|
+
SelectionControls,
|
|
1585
|
+
{
|
|
1586
|
+
elements: selectedElements,
|
|
1587
|
+
single,
|
|
1588
|
+
isGroup,
|
|
1589
|
+
groupable,
|
|
1590
|
+
allSameKind,
|
|
1591
|
+
firstKind,
|
|
1592
|
+
canWrite,
|
|
1593
|
+
review,
|
|
1594
|
+
patchAll,
|
|
1595
|
+
reorderSingle,
|
|
1596
|
+
onGroup: () => onGroup(selectedIds),
|
|
1597
|
+
onUngroup: () => {
|
|
1598
|
+
if (single) onUngroup(single.id);
|
|
1599
|
+
},
|
|
1600
|
+
onDelete: () => onDelete(selectedIds),
|
|
1601
|
+
onBindSlot: single ? (slot) => onBindSlot(single.id, slot) : void 0,
|
|
1602
|
+
currentSlot: single?.slot ?? null
|
|
1603
|
+
}
|
|
1604
|
+
)
|
|
1605
|
+
] }) : !review ? /* @__PURE__ */ jsxs5(Fragment4, { children: [
|
|
1606
|
+
SEP,
|
|
1607
|
+
/* @__PURE__ */ jsx5(
|
|
1608
|
+
PagePropsControls,
|
|
1609
|
+
{
|
|
1610
|
+
page,
|
|
1611
|
+
canWrite,
|
|
1612
|
+
onSetPageProps,
|
|
1613
|
+
onSetPageGuides
|
|
1614
|
+
}
|
|
1615
|
+
)
|
|
1616
|
+
] }) : null })
|
|
1605
1617
|
] });
|
|
1606
1618
|
}
|
|
1607
1619
|
function SelectionControls({
|
|
@@ -1612,6 +1624,7 @@ function SelectionControls({
|
|
|
1612
1624
|
allSameKind,
|
|
1613
1625
|
firstKind,
|
|
1614
1626
|
canWrite,
|
|
1627
|
+
review,
|
|
1615
1628
|
patchAll,
|
|
1616
1629
|
reorderSingle,
|
|
1617
1630
|
onGroup,
|
|
@@ -1648,83 +1661,85 @@ function SelectionControls({
|
|
|
1648
1661
|
className: "w-14"
|
|
1649
1662
|
}
|
|
1650
1663
|
),
|
|
1651
|
-
|
|
1652
|
-
|
|
1653
|
-
|
|
1654
|
-
|
|
1655
|
-
|
|
1656
|
-
|
|
1657
|
-
|
|
1658
|
-
|
|
1659
|
-
|
|
1660
|
-
|
|
1661
|
-
|
|
1662
|
-
|
|
1663
|
-
|
|
1664
|
-
|
|
1665
|
-
|
|
1666
|
-
|
|
1667
|
-
|
|
1668
|
-
|
|
1669
|
-
|
|
1670
|
-
|
|
1671
|
-
|
|
1672
|
-
|
|
1673
|
-
|
|
1674
|
-
|
|
1675
|
-
|
|
1676
|
-
|
|
1677
|
-
|
|
1678
|
-
type: "button",
|
|
1679
|
-
"aria-label": currentSlot ? `Slot: ${currentSlot}` : "Bind slot",
|
|
1680
|
-
onClick: () => {
|
|
1681
|
-
setSlotInput(currentSlot ?? "");
|
|
1682
|
-
setSlotPopoverOpen((v) => !v);
|
|
1683
|
-
},
|
|
1684
|
-
className: currentSlot ? BTN_ACTIVE : BTN2,
|
|
1685
|
-
title: currentSlot ? `Slot: ${currentSlot}` : "Bind slot",
|
|
1686
|
-
children: /* @__PURE__ */ jsx5(SlotGlyph, { className: "h-3.5 w-3.5" })
|
|
1687
|
-
}
|
|
1688
|
-
),
|
|
1689
|
-
children: /* @__PURE__ */ jsxs5("div", { className: `${POPOVER_PANEL} w-48 gap-2 p-2`, children: [
|
|
1690
|
-
/* @__PURE__ */ jsx5(
|
|
1691
|
-
"input",
|
|
1664
|
+
review ? null : /* @__PURE__ */ jsxs5(Fragment4, { children: [
|
|
1665
|
+
SEP,
|
|
1666
|
+
single ? /* @__PURE__ */ jsxs5(Fragment4, { children: [
|
|
1667
|
+
/* @__PURE__ */ jsx5("button", { type: "button", "aria-label": "Bring to front", disabled: !canWrite, onClick: () => reorderSingle("front"), className: BTN2, children: /* @__PURE__ */ jsx5(BringFrontGlyph, { className: "h-3.5 w-3.5" }) }),
|
|
1668
|
+
/* @__PURE__ */ jsx5("button", { type: "button", "aria-label": "Send to back", disabled: !canWrite, onClick: () => reorderSingle("back"), className: BTN2, children: /* @__PURE__ */ jsx5(SendBackGlyph, { className: "h-3.5 w-3.5" }) }),
|
|
1669
|
+
SEP
|
|
1670
|
+
] }) : null,
|
|
1671
|
+
groupable ? /* @__PURE__ */ jsx5("button", { type: "button", "aria-label": "Group elements", disabled: !canWrite, onClick: onGroup, className: BTN2, children: /* @__PURE__ */ jsx5(GroupGlyph, { className: "h-3.5 w-3.5" }) }) : null,
|
|
1672
|
+
isGroup ? /* @__PURE__ */ jsx5("button", { type: "button", "aria-label": "Ungroup", disabled: !canWrite, onClick: onUngroup, className: BTN2, children: /* @__PURE__ */ jsx5(UngroupGlyph, { className: "h-3.5 w-3.5" }) }) : null,
|
|
1673
|
+
single ? /* @__PURE__ */ jsx5(
|
|
1674
|
+
"button",
|
|
1675
|
+
{
|
|
1676
|
+
type: "button",
|
|
1677
|
+
"aria-label": single.locked ? "Unlock element" : "Lock element",
|
|
1678
|
+
disabled: !canWrite,
|
|
1679
|
+
onClick: () => patchAll({ locked: !single.locked }),
|
|
1680
|
+
className: single.locked ? BTN_ACTIVE : BTN2,
|
|
1681
|
+
children: single.locked ? /* @__PURE__ */ jsx5(LockGlyph, { className: "h-3.5 w-3.5" }) : /* @__PURE__ */ jsx5(UnlockGlyph, { className: "h-3.5 w-3.5" })
|
|
1682
|
+
}
|
|
1683
|
+
) : null,
|
|
1684
|
+
single && onBindSlot ? /* @__PURE__ */ jsx5(
|
|
1685
|
+
Popover,
|
|
1686
|
+
{
|
|
1687
|
+
open: slotPopoverOpen,
|
|
1688
|
+
onClose: () => setSlotPopoverOpen(false),
|
|
1689
|
+
trigger: /* @__PURE__ */ jsx5(
|
|
1690
|
+
"button",
|
|
1692
1691
|
{
|
|
1693
|
-
|
|
1694
|
-
|
|
1695
|
-
|
|
1696
|
-
|
|
1697
|
-
|
|
1692
|
+
type: "button",
|
|
1693
|
+
"aria-label": currentSlot ? `Slot: ${currentSlot}` : "Bind slot",
|
|
1694
|
+
onClick: () => {
|
|
1695
|
+
setSlotInput(currentSlot ?? "");
|
|
1696
|
+
setSlotPopoverOpen((v) => !v);
|
|
1697
|
+
},
|
|
1698
|
+
className: currentSlot ? BTN_ACTIVE : BTN2,
|
|
1699
|
+
title: currentSlot ? `Slot: ${currentSlot}` : "Bind slot",
|
|
1700
|
+
children: /* @__PURE__ */ jsx5(SlotGlyph, { className: "h-3.5 w-3.5" })
|
|
1698
1701
|
}
|
|
1699
1702
|
),
|
|
1700
|
-
/* @__PURE__ */ jsxs5("div", { className:
|
|
1703
|
+
children: /* @__PURE__ */ jsxs5("div", { className: `${POPOVER_PANEL} w-48 gap-2 p-2`, children: [
|
|
1701
1704
|
/* @__PURE__ */ jsx5(
|
|
1702
|
-
"
|
|
1705
|
+
"input",
|
|
1703
1706
|
{
|
|
1704
|
-
|
|
1705
|
-
|
|
1706
|
-
|
|
1707
|
-
|
|
1708
|
-
|
|
1709
|
-
className: "flex-1 rounded border border-[var(--brand-primary)] px-2 py-0.5 text-[11px] text-[var(--brand-primary)] hover:bg-[var(--brand-primary)]/10",
|
|
1710
|
-
children: slotInput.trim() ? "Bind" : "Unbind"
|
|
1707
|
+
autoFocus: true,
|
|
1708
|
+
value: slotInput,
|
|
1709
|
+
onChange: (event) => setSlotInput(event.target.value),
|
|
1710
|
+
placeholder: "slot-name",
|
|
1711
|
+
className: "rounded border border-[var(--border-default)] bg-transparent px-2 py-1 text-[12px] text-[var(--text-primary)] outline-none focus:border-[var(--brand-primary)]"
|
|
1711
1712
|
}
|
|
1712
1713
|
),
|
|
1713
|
-
/* @__PURE__ */
|
|
1714
|
-
|
|
1715
|
-
|
|
1716
|
-
|
|
1717
|
-
|
|
1718
|
-
|
|
1719
|
-
|
|
1720
|
-
|
|
1721
|
-
|
|
1714
|
+
/* @__PURE__ */ jsxs5("div", { className: "flex gap-2", children: [
|
|
1715
|
+
/* @__PURE__ */ jsx5(
|
|
1716
|
+
"button",
|
|
1717
|
+
{
|
|
1718
|
+
type: "button",
|
|
1719
|
+
onClick: () => {
|
|
1720
|
+
onBindSlot(slotInput.trim() || null);
|
|
1721
|
+
setSlotPopoverOpen(false);
|
|
1722
|
+
},
|
|
1723
|
+
className: "flex-1 rounded border border-[var(--brand-primary)] px-2 py-0.5 text-[11px] text-[var(--brand-primary)] hover:bg-[var(--brand-primary)]/10",
|
|
1724
|
+
children: slotInput.trim() ? "Bind" : "Unbind"
|
|
1725
|
+
}
|
|
1726
|
+
),
|
|
1727
|
+
/* @__PURE__ */ jsx5(
|
|
1728
|
+
"button",
|
|
1729
|
+
{
|
|
1730
|
+
type: "button",
|
|
1731
|
+
onClick: () => setSlotPopoverOpen(false),
|
|
1732
|
+
className: "rounded border border-[var(--border-default)] px-2 py-0.5 text-[11px] text-[var(--text-secondary)]",
|
|
1733
|
+
children: "Cancel"
|
|
1734
|
+
}
|
|
1735
|
+
)
|
|
1736
|
+
] })
|
|
1722
1737
|
] })
|
|
1723
|
-
|
|
1724
|
-
|
|
1725
|
-
|
|
1726
|
-
|
|
1727
|
-
|
|
1738
|
+
}
|
|
1739
|
+
) : null,
|
|
1740
|
+
SEP,
|
|
1741
|
+
/* @__PURE__ */ jsx5("button", { type: "button", "aria-label": "Delete selection", disabled: !canWrite, onClick: onDelete, className: BTN2, children: /* @__PURE__ */ jsx5(TrashGlyph, { className: "h-3.5 w-3.5 text-rose-400" }) })
|
|
1742
|
+
] })
|
|
1728
1743
|
] });
|
|
1729
1744
|
}
|
|
1730
1745
|
function TextControls({ element, canWrite, onPatch }) {
|
|
@@ -1779,21 +1794,84 @@ function ShapeControls({ element, canWrite, onPatch, showCornerRadius }) {
|
|
|
1779
1794
|
] });
|
|
1780
1795
|
}
|
|
1781
1796
|
function ImageControls({ element, canWrite, onPatch }) {
|
|
1782
|
-
|
|
1783
|
-
|
|
1784
|
-
|
|
1785
|
-
|
|
1786
|
-
|
|
1787
|
-
|
|
1788
|
-
|
|
1789
|
-
|
|
1790
|
-
|
|
1791
|
-
|
|
1792
|
-
|
|
1793
|
-
|
|
1794
|
-
|
|
1795
|
-
|
|
1796
|
-
|
|
1797
|
+
const [swapOpen, setSwapOpen] = useState3(false);
|
|
1798
|
+
const [swapUrl, setSwapUrl] = useState3("");
|
|
1799
|
+
return /* @__PURE__ */ jsxs5(Fragment4, { children: [
|
|
1800
|
+
/* @__PURE__ */ jsx5(
|
|
1801
|
+
SelectControl,
|
|
1802
|
+
{
|
|
1803
|
+
label: "Fit",
|
|
1804
|
+
value: element.fit,
|
|
1805
|
+
disabled: !canWrite,
|
|
1806
|
+
onChange: (fit) => onPatch({ fit }),
|
|
1807
|
+
buttonClassName: "w-24",
|
|
1808
|
+
options: [
|
|
1809
|
+
{ value: "fill", label: "Fill" },
|
|
1810
|
+
{ value: "cover", label: "Cover" },
|
|
1811
|
+
{ value: "contain", label: "Contain" }
|
|
1812
|
+
]
|
|
1813
|
+
}
|
|
1814
|
+
),
|
|
1815
|
+
/* @__PURE__ */ jsx5(
|
|
1816
|
+
Popover,
|
|
1817
|
+
{
|
|
1818
|
+
open: swapOpen,
|
|
1819
|
+
onClose: () => setSwapOpen(false),
|
|
1820
|
+
trigger: /* @__PURE__ */ jsx5(
|
|
1821
|
+
"button",
|
|
1822
|
+
{
|
|
1823
|
+
type: "button",
|
|
1824
|
+
"aria-label": "Replace image",
|
|
1825
|
+
disabled: !canWrite,
|
|
1826
|
+
onClick: () => {
|
|
1827
|
+
setSwapUrl(element.src);
|
|
1828
|
+
setSwapOpen((v) => !v);
|
|
1829
|
+
},
|
|
1830
|
+
className: BTN2,
|
|
1831
|
+
title: "Replace image",
|
|
1832
|
+
children: /* @__PURE__ */ jsx5(SwapGlyph, { className: "h-3.5 w-3.5" })
|
|
1833
|
+
}
|
|
1834
|
+
),
|
|
1835
|
+
children: /* @__PURE__ */ jsxs5("div", { className: `${POPOVER_PANEL} w-64 gap-2 p-2`, children: [
|
|
1836
|
+
/* @__PURE__ */ jsx5(
|
|
1837
|
+
"input",
|
|
1838
|
+
{
|
|
1839
|
+
autoFocus: true,
|
|
1840
|
+
value: swapUrl,
|
|
1841
|
+
onChange: (event) => setSwapUrl(event.target.value),
|
|
1842
|
+
placeholder: "https://\u2026 image URL",
|
|
1843
|
+
"aria-label": "New image URL",
|
|
1844
|
+
className: "rounded border border-[var(--border-default)] bg-transparent px-2 py-1 text-[12px] text-[var(--text-primary)] outline-none focus:border-[var(--brand-primary)]"
|
|
1845
|
+
}
|
|
1846
|
+
),
|
|
1847
|
+
/* @__PURE__ */ jsxs5("div", { className: "flex gap-2", children: [
|
|
1848
|
+
/* @__PURE__ */ jsx5(
|
|
1849
|
+
"button",
|
|
1850
|
+
{
|
|
1851
|
+
type: "button",
|
|
1852
|
+
disabled: !swapUrl.trim() || swapUrl.trim() === element.src,
|
|
1853
|
+
onClick: () => {
|
|
1854
|
+
onPatch({ src: swapUrl.trim() });
|
|
1855
|
+
setSwapOpen(false);
|
|
1856
|
+
},
|
|
1857
|
+
className: "flex-1 rounded border border-[var(--brand-primary)] px-2 py-0.5 text-[11px] text-[var(--brand-primary)] hover:bg-[var(--brand-primary)]/10 disabled:cursor-default disabled:opacity-40",
|
|
1858
|
+
children: "Replace"
|
|
1859
|
+
}
|
|
1860
|
+
),
|
|
1861
|
+
/* @__PURE__ */ jsx5(
|
|
1862
|
+
"button",
|
|
1863
|
+
{
|
|
1864
|
+
type: "button",
|
|
1865
|
+
onClick: () => setSwapOpen(false),
|
|
1866
|
+
className: "rounded border border-[var(--border-default)] px-2 py-0.5 text-[11px] text-[var(--text-secondary)]",
|
|
1867
|
+
children: "Cancel"
|
|
1868
|
+
}
|
|
1869
|
+
)
|
|
1870
|
+
] })
|
|
1871
|
+
] })
|
|
1872
|
+
}
|
|
1873
|
+
)
|
|
1874
|
+
] });
|
|
1797
1875
|
}
|
|
1798
1876
|
function PagePropsControls({ page, canWrite, onSetPageProps, onSetPageGuides }) {
|
|
1799
1877
|
const matchedPreset = matchPreset(page.width, page.height);
|
|
@@ -2126,6 +2204,7 @@ function DesignCanvas({
|
|
|
2126
2204
|
document: initialDocument,
|
|
2127
2205
|
rev: initialRev,
|
|
2128
2206
|
canWrite,
|
|
2207
|
+
mode = "edit",
|
|
2129
2208
|
onApplyOperations,
|
|
2130
2209
|
onSelectionChange,
|
|
2131
2210
|
renderAgentPanel,
|
|
@@ -2385,6 +2464,7 @@ function DesignCanvas({
|
|
|
2385
2464
|
if (!activePage) return [];
|
|
2386
2465
|
return editorState.selectedElementIds.map((id) => activePage.elements.find((el) => el.id === id)).filter((el) => el !== void 0);
|
|
2387
2466
|
}, [activePage, editorState.selectedElementIds]);
|
|
2467
|
+
const review = mode === "review";
|
|
2388
2468
|
if (!activePage) {
|
|
2389
2469
|
return /* @__PURE__ */ jsx8("div", { className: `flex h-full items-center justify-center bg-[var(--bg-input)] text-[var(--text-muted)] ${className ?? ""}`, children: "No pages in document" });
|
|
2390
2470
|
}
|
|
@@ -2395,7 +2475,7 @@ function DesignCanvas({
|
|
|
2395
2475
|
left: activePage.bleed.left * editorState.zoom
|
|
2396
2476
|
} : null;
|
|
2397
2477
|
return /* @__PURE__ */ jsxs8("div", { className: `flex h-full min-h-0 bg-[var(--bg-input)] text-[var(--text-primary)] ${className ?? ""}`, children: [
|
|
2398
|
-
renderSidePanel ? /* @__PURE__ */ jsx8("aside", { className: "flex w-64 shrink-0 flex-col overflow-hidden border-r border-[var(--border-default)]", children: renderSidePanel() }) : null,
|
|
2478
|
+
renderSidePanel && !review ? /* @__PURE__ */ jsx8("aside", { className: "flex w-64 shrink-0 flex-col overflow-hidden border-r border-[var(--border-default)]", children: renderSidePanel() }) : null,
|
|
2399
2479
|
/* @__PURE__ */ jsxs8("div", { className: "flex min-w-0 flex-1 flex-col", children: [
|
|
2400
2480
|
/* @__PURE__ */ jsxs8("div", { className: "flex shrink-0 items-stretch", children: [
|
|
2401
2481
|
/* @__PURE__ */ jsx8("div", { className: "min-w-0 flex-1", children: /* @__PURE__ */ jsx8(
|
|
@@ -2404,6 +2484,7 @@ function DesignCanvas({
|
|
|
2404
2484
|
page: activePage,
|
|
2405
2485
|
selectedElements,
|
|
2406
2486
|
canWrite,
|
|
2487
|
+
mode,
|
|
2407
2488
|
canUndo: stack.canUndo(),
|
|
2408
2489
|
canRedo: stack.canRedo(),
|
|
2409
2490
|
gridEnabled: editorState.gridEnabled,
|
|
@@ -2522,6 +2603,7 @@ function DesignCanvas({
|
|
|
2522
2603
|
pages: editorState.document.pages,
|
|
2523
2604
|
activePageId: editorState.activePageId,
|
|
2524
2605
|
canWrite,
|
|
2606
|
+
canManagePages: !review,
|
|
2525
2607
|
renderThumbnail,
|
|
2526
2608
|
onSelectPage: setActivePage,
|
|
2527
2609
|
onAddPage: handleAddPage,
|
|
@@ -2588,4 +2670,4 @@ export {
|
|
|
2588
2670
|
DesignCanvas,
|
|
2589
2671
|
DesignCanvas_default
|
|
2590
2672
|
};
|
|
2591
|
-
//# sourceMappingURL=chunk-
|
|
2673
|
+
//# sourceMappingURL=chunk-27LQXQSS.js.map
|