@skhema/embed 0.1.11 → 0.1.12

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.
@@ -1,4 +1,4 @@
1
- import { g as getElementTypeLabel, r as resolveComponentType, a as getComponentTypeAcronym, C as CARD_PALETTE, b as getComponentTypeLabel, M as MONO_STACK, P as PRIMARY_HEX, c as COMPONENT_COLORS, d as CARD_RADIUS, e as CARD_SHADOW, F as FONT_STACK, U as USER_ICON_SVG } from "./tokens-BQf5zJ3j.js";
1
+ import { d as getElementTypeLabel, r as resolveComponentType, g as getComponentTypeAcronym, h as CARD_PALETTE, b as getComponentTypeLabel, M as MONO_STACK, P as PRIMARY_HEX, a as COMPONENT_COLORS, j as CARD_RADIUS, k as CARD_SHADOW, F as FONT_STACK, U as USER_ICON_SVG } from "./validation-BcXe-9fn.js";
2
2
  const BRAND_PINK = "#cd476a";
3
3
  function hexToRgb(hex) {
4
4
  const h = hex.replace("#", "");
@@ -213,4 +213,4 @@ export {
213
213
  renderComponentCardHtml as r,
214
214
  validateContentSecurity as v
215
215
  };
216
- //# sourceMappingURL=index-nkGvTyQR.js.map
216
+ //# sourceMappingURL=index-C4JT9tyk.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index-C4JT9tyk.js","sources":["../src/utils/color.ts","../src/utils/sanitization.ts","../src/render/index.ts"],"sourcesContent":["/**\n * DOM-free colour utilities for the email-safe card renderer.\n *\n * The card palette (`COMPONENT_COLORS`) is authored in `oklch()` for the live\n * browser embed, but email clients can't parse `oklch()` and `<style>` is\n * stripped, so every colour must be a pre-computed sRGB hex with all styles\n * inlined. This converts a single `oklch(...)` token to hex, compositing any\n * alpha over a flat background (white for light cards, the dark card surface\n * for dark cards) so the result is opaque and email-safe.\n *\n * This is the single source of the conversion that `sk comms curated` and the\n * `CuratedElements` email template previously duplicated.\n */\n\n/** Brand pink (hsl(344 57% 54%)) — fallback when a component palette is missing. */\nexport const BRAND_PINK = '#cd476a'\n\nfunction hexToRgb(hex: string): [number, number, number] {\n const h = hex.replace('#', '')\n return [\n parseInt(h.slice(0, 2), 16),\n parseInt(h.slice(2, 4), 16),\n parseInt(h.slice(4, 6), 16),\n ]\n}\n\n/**\n * Convert a single `oklch(...)` token to an email-safe sRGB hex. When the token\n * carries an alpha (e.g. the badge `bg` @ 0.15 / `border` @ 0.3), the colour is\n * composited over `overHex` so the result is a flat opaque hex.\n *\n * @param token An `oklch(L C H)` or `oklch(L C H / A)` string.\n * @param overHex The flat background to composite alpha over (default white).\n */\nexport function oklchToHex(token: string, overHex = '#ffffff'): string {\n const m = token.match(\n /oklch\\(\\s*([\\d.]+)\\s+([\\d.]+)\\s+([\\d.]+)\\s*(?:\\/\\s*([\\d.]+)\\s*)?\\)/\n )\n if (!m) return BRAND_PINK\n const L = parseFloat(m[1])\n const C = parseFloat(m[2])\n const H = parseFloat(m[3])\n const alpha = m[4] !== undefined ? parseFloat(m[4]) : 1\n const h = (H * Math.PI) / 180\n const a = C * Math.cos(h)\n const b = C * Math.sin(h)\n const l_ = L + 0.3963377774 * a + 0.2158037573 * b\n const m_ = L - 0.1055613458 * a - 0.0638541728 * b\n const s_ = L - 0.0894841775 * a - 1.291485548 * b\n const lr = l_ ** 3\n const mr = m_ ** 3\n const sr = s_ ** 3\n const R = 4.0767416621 * lr - 3.3077115913 * mr + 0.2309699292 * sr\n const G = -1.2684380046 * lr + 2.6097574011 * mr - 0.3413193965 * sr\n const B = -0.0041960863 * lr - 0.7034186147 * mr + 1.707614701 * sr\n const gamma = (x: number): number => {\n const c = Math.max(0, Math.min(1, x))\n return c <= 0.0031308 ? 12.92 * c : 1.055 * Math.pow(c, 1 / 2.4) - 0.055\n }\n let rgb = [gamma(R) * 255, gamma(G) * 255, gamma(B) * 255]\n if (alpha < 1) {\n const [br, bg, bb] = hexToRgb(overHex)\n rgb = [\n rgb[0] * alpha + br * (1 - alpha),\n rgb[1] * alpha + bg * (1 - alpha),\n rgb[2] * alpha + bb * (1 - alpha),\n ]\n }\n return (\n '#' + rgb.map((c) => Math.round(c).toString(16).padStart(2, '0')).join('')\n )\n}\n","/**\n * Content sanitization utilities for Skhema cards.\n *\n * DOM-free by design: `sanitizeContent` is imported by the email-safe card\n * renderer (`@skhema/embed/render`), which must run in Node / email / CLI\n * runtimes with no `document`. Keep every export here free of DOM access at\n * call time (the legacy `stripHtml` below is the one exception and is not used\n * by the renderer).\n */\n\n/**\n * HTML entity encoding for basic XSS protection. Regex-based (no DOM) so it is\n * safe in non-browser runtimes. Escapes the text-context entities plus quotes.\n */\nfunction htmlEncode(str: string): string {\n return str\n .replace(/&/g, '&amp;')\n .replace(/</g, '&lt;')\n .replace(/>/g, '&gt;')\n .replace(/\"/g, '&quot;')\n}\n\n/**\n * Sanitizes content to prevent XSS attacks and removes URLs\n * @param content The raw content to sanitize\n * @returns Sanitized HTML-safe content with URLs removed\n */\nexport function sanitizeContent(content: string): string {\n // Strip URLs first\n let sanitized = stripUrls(content)\n\n // Encode all HTML entities to prevent script injection\n sanitized = htmlEncode(sanitized)\n\n // Preserve line breaks for readability\n sanitized = sanitized.replace(/\\n/g, '<br>')\n\n // Apply text wrapping rules for long text\n sanitized = applyTextWrapping(sanitized)\n\n return sanitized\n}\n\n/**\n * Strips all URLs from the content\n * @param text The text containing potential URLs\n * @returns Text with all URLs removed\n */\nfunction stripUrls(text: string): string {\n // Comprehensive URL patterns to remove\n const patterns = [\n // Standard URLs with protocols\n /https?:\\/\\/[^\\s<>\"{}|\\\\^`[\\]]+/gi,\n // FTP URLs\n /ftp:\\/\\/[^\\s<>\"{}|\\\\^`[\\]]+/gi,\n // URLs without protocol but with www\n /www\\.[^\\s<>\"{}|\\\\^`[\\]]+/gi,\n // Email-like patterns\n /[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,}/gi,\n // Common domain patterns (anything.com, anything.org, etc.)\n /(?:^|\\s)([a-zA-Z0-9-]+\\.)+[a-zA-Z]{2,}(?:\\/[^\\s]*)?/gi,\n ]\n\n let stripped = text\n patterns.forEach((pattern) => {\n stripped = stripped.replace(pattern, '')\n })\n\n // Clean up any multiple spaces left after URL removal\n stripped = stripped.replace(/\\s+/g, ' ').trim()\n\n return stripped\n}\n\n/**\n * Applies intelligent text wrapping to prevent layout breaking\n * @param text The text to apply wrapping rules to\n * @returns Text with appropriate wrapping hints\n */\nfunction applyTextWrapping(text: string): string {\n // Split text into words\n const words = text.split(/(\\s+)/)\n\n return words\n .map((word) => {\n // Skip if it's whitespace or already contains HTML\n if (/^\\s+$/.test(word) || word.includes('<')) {\n return word\n }\n\n // For very long words (>30 chars), add word-break opportunities\n if (word.length > 30) {\n // Insert zero-width spaces every 10 characters for breaking\n return word.replace(/(.{10})/g, '$1\\u200B')\n }\n\n return word\n })\n .join('')\n}\n\n/**\n * Validates if content contains potentially malicious patterns or URLs\n * @param content The content to validate\n * @returns Object with validation status and detected issues\n */\nexport function validateContentSecurity(content: string): {\n isSecure: boolean\n issues: string[]\n} {\n const issues: string[] = []\n\n // Check for script tags\n if (/<script[\\s>]/i.test(content)) {\n issues.push('Script tags detected')\n }\n\n // Check for event handlers\n if (/on\\w+\\s*=/i.test(content)) {\n issues.push('Event handlers detected')\n }\n\n // Check for javascript: protocol\n if (/javascript:/i.test(content)) {\n issues.push('JavaScript protocol detected')\n }\n\n // Check for data: URLs that could contain scripts\n if (/data:[^,]*script/i.test(content)) {\n issues.push('Data URL with script detected')\n }\n\n // Check for iframe tags\n if (/<iframe[\\s>]/i.test(content)) {\n issues.push('Iframe tags detected')\n }\n\n // Check for URLs (since we want to disallow them)\n if (/https?:\\/\\//i.test(content) || /www\\./i.test(content)) {\n issues.push('URLs detected in content')\n }\n\n return {\n isSecure: issues.length === 0,\n issues,\n }\n}\n\n/**\n * Strips all HTML tags from content\n * @param content The content to strip\n * @returns Plain text content\n */\nexport function stripHtml(content: string): string {\n const div = document.createElement('div')\n div.innerHTML = content\n return div.textContent || div.innerText || ''\n}\n\n/**\n * Checks if content contains any URLs\n * @param content The content to check\n * @returns True if URLs are found\n */\nexport function containsUrls(content: string): boolean {\n const urlPatterns = [\n /https?:\\/\\//i,\n /ftp:\\/\\//i,\n /www\\./i,\n /[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,}/i,\n /(?:^|\\s)([a-zA-Z0-9-]+\\.)+[a-zA-Z]{2,}(?:\\/[^\\s]*)?/i,\n ]\n\n return urlPatterns.some((pattern) => pattern.test(content))\n}\n","/**\n * `@skhema/embed/render` — the canonical, DOM-free source of truth for the\n * official Skhema element / component card HTML.\n *\n * `renderElementCardHtml(data)` / `renderComponentCardHtml(data)` return\n * EMAIL-SAFE HTML: a `role=\"presentation\"` table layout with every style\n * inlined as flat hex (no shadow DOM, no `<style>`, no `oklch()`, no CSS vars).\n * The same output is used three ways:\n *\n * 1. the live browser embed (`SkhemaElement` / `SkhemaComponent` inject it\n * into shadow DOM and layer hover/transition CSS on top);\n * 2. the `CuratedElements` email template (injected verbatim); and\n * 3. any third-party / contributor email generator.\n *\n * The module is pure — importing it never touches the DOM, so it is safe in\n * Node, edge, and email build runtimes. It builds NO URLs: callers pass the\n * fully-formed `saveUrl` (e.g. the `/save` handoff) so each surface controls\n * its own UTM tagging.\n */\nimport type { ElementValue } from '@skhema/method/vocabulary'\nimport {\n CARD_PALETTE,\n CARD_RADIUS,\n CARD_SHADOW,\n COMPONENT_COLORS,\n FONT_STACK,\n MONO_STACK,\n PRIMARY_HEX,\n USER_ICON_SVG,\n type CardTheme,\n} from '../styles/design-tokens.js'\nimport { oklchToHex } from '../utils/color.js'\nimport {\n getComponentTypeAcronym,\n getComponentTypeLabel,\n resolveComponentType,\n} from '../utils/component-validation.js'\nimport { sanitizeContent } from '../utils/sanitization.js'\nimport { getElementTypeLabel } from '../utils/validation.js'\n\n/* ------------------------------------------------------------------ *\n * Public data shapes (documented contract — see README \"render\") *\n * ------------------------------------------------------------------ */\n\n/** Card theme. Email is always `'light'`; the browser embed passes the\n * detected page theme. Defaults to `'light'` when omitted. */\nexport type { CardTheme }\n\n/** Author attribution shared by both card kinds. */\ninterface AuthorFields {\n /** Display name. When omitted, falls back to a humanised `contributorId`. */\n authorName?: string | null\n /** Public contributor slug — when present, the name links to the profile. */\n authorSlug?: string | null\n /** Contributor id, used only for the name fallback when `authorName` is unset. */\n contributorId?: string | null\n}\n\n/** Input for {@link renderElementCardHtml}. */\nexport interface ElementCardData extends AuthorFields {\n /** Skhema element type value, e.g. `\"key_challenge\"`. */\n elementType: string\n /** The element content / premise (plain text; sanitised + escaped here). */\n content: string\n /** Pre-built handoff URL for the \"Save to Skhema\" button. */\n saveUrl: string\n /** Card theme (default `'light'`). */\n theme?: CardTheme\n}\n\n/** A single element row inside a component card. */\nexport interface ComponentCardElement {\n elementType: string\n content: string\n}\n\n/** Input for {@link renderComponentCardHtml}. */\nexport interface ComponentCardData extends AuthorFields {\n /** Skhema component type value, e.g. `\"diagnosis\"`. */\n componentType: string\n /** Optional component title shown after a \"—\" separator in the header. */\n title?: string | null\n /** The component's elements, in display order. */\n elements: ComponentCardElement[]\n /** Pre-built handoff URL for the \"Save to Skhema\" button. */\n saveUrl: string\n /** Card theme (default `'light'`). */\n theme?: CardTheme\n}\n\n/* ------------------------------------------------------------------ *\n * Internal helpers *\n * ------------------------------------------------------------------ */\n\nfunction escapeHtml(text: string): string {\n return text\n .replace(/&/g, '&amp;')\n .replace(/</g, '&lt;')\n .replace(/>/g, '&gt;')\n .replace(/\"/g, '&quot;')\n}\n\nfunction escapeAttr(text: string): string {\n return escapeHtml(text).replace(/'/g, '&#39;')\n}\n\nfunction humaniseContributorId(contributorId: string): string {\n return contributorId\n .split(/[_-]/)\n .map((word) => word.charAt(0).toUpperCase() + word.slice(1).toLowerCase())\n .join(' ')\n}\n\ninterface BadgePalette {\n badgeBg: string\n badgeText: string\n badgeBorder: string\n topBorder: string\n}\n\n/** Resolve the email-safe badge / top-border hex for a component type + theme. */\nfunction resolveBadgePalette(\n componentType: string,\n theme: CardTheme\n): BadgePalette {\n const surface = CARD_PALETTE[theme].cardBg\n const colors =\n COMPONENT_COLORS[componentType as keyof typeof COMPONENT_COLORS]\n if (!colors) {\n return {\n badgeBg: surface,\n badgeText: PRIMARY_HEX,\n badgeBorder: PRIMARY_HEX,\n topBorder: PRIMARY_HEX,\n }\n }\n const text = oklchToHex(colors.text)\n return {\n badgeBg: oklchToHex(colors.bg, surface),\n badgeText: text,\n badgeBorder: oklchToHex(colors.border, surface),\n topBorder: text,\n }\n}\n\n/** Contributor line inner HTML: person icon + \"By {author}\" (linked if slug). */\nfunction renderAuthorHtml(author: AuthorFields, mutedColor: string): string {\n let label = ''\n if (author.authorName && author.authorName.trim()) {\n const name = escapeHtml(author.authorName.trim())\n label = author.authorSlug\n ? `By <a href=\"https://skhema.com/contributors/${encodeURIComponent(\n author.authorSlug\n )}\" style=\"color:${mutedColor};text-decoration:none;\" target=\"_blank\" rel=\"noopener noreferrer\">${name}</a>`\n : `By ${name}`\n } else if (author.contributorId && author.contributorId.trim()) {\n label = `By ${escapeHtml(humaniseContributorId(author.contributorId.trim()))}`\n }\n\n if (!label) return '&nbsp;'\n\n return (\n `<span style=\"display:inline-block;width:14px;height:14px;vertical-align:middle;color:${mutedColor};\">${USER_ICON_SVG}</span>` +\n `<span style=\"vertical-align:middle;padding-left:6px;\">${label}</span>`\n )\n}\n\n/** The shared footer (contributor line + save button + attribution). */\nfunction renderFooter(\n saveUrl: string,\n author: AuthorFields,\n theme: CardTheme\n): string {\n const p = CARD_PALETTE[theme]\n return (\n `<tr><td style=\"padding:12px 16px;border-top:1px solid ${p.border};\">` +\n `<table role=\"presentation\" cellpadding=\"0\" cellspacing=\"0\" border=\"0\" style=\"width:100%;border-collapse:collapse;\"><tr>` +\n `<td style=\"vertical-align:middle;\"><span style=\"font-size:12px;color:${p.textMuted};\">${renderAuthorHtml(\n author,\n p.textMuted\n )}</span></td>` +\n `<td style=\"text-align:right;vertical-align:middle;white-space:nowrap;\">` +\n `<a href=\"${escapeAttr(saveUrl)}\" class=\"skhema-save-btn\" target=\"_blank\" rel=\"noopener noreferrer\" title=\"Save this to Skhema\" style=\"display:inline-block;background:${PRIMARY_HEX};color:#ffffff;font-size:12px;font-weight:500;text-decoration:none;padding:6px 14px;border-radius:${CARD_RADIUS};white-space:nowrap;\">Save to Skhema &rarr;</a>` +\n `</td></tr></table>` +\n `<div style=\"font-size:11px;line-height:1.4;color:${p.textMuted};margin-top:8px;\">Strategy powered by <a href=\"https://skhema.com\" style=\"color:${p.textMuted};text-decoration:underline;\" target=\"_blank\" rel=\"noopener noreferrer\">Skhema</a></div>` +\n `</td></tr>`\n )\n}\n\n/** Header row: acronym badge + type label (+ optional \"— title\" for components). */\nfunction renderHeader(\n badge: BadgePalette,\n acronym: string,\n typeLabel: string,\n labelColor: string,\n theme: CardTheme,\n title?: string | null\n): string {\n const p = CARD_PALETTE[theme]\n const titleHtml = title\n ? `<span style=\"color:${p.textMuted};\"> &mdash; </span><span style=\"font-weight:600;color:${p.text};\">${escapeHtml(\n title\n )}</span>`\n : ''\n return (\n `<tr><td style=\"padding:12px 16px;border-bottom:1px solid ${p.border};\">` +\n `<table role=\"presentation\" cellpadding=\"0\" cellspacing=\"0\" border=\"0\" style=\"width:100%;border-collapse:collapse;\"><tr>` +\n `<td style=\"width:1%;white-space:nowrap;padding-right:8px;vertical-align:middle;\">` +\n `<span class=\"skhema-acronym-badge\" title=\"${escapeAttr(typeLabel)}\" style=\"display:inline-block;font-family:${MONO_STACK};font-size:10px;font-weight:600;letter-spacing:0.02em;padding:2px 6px;border-radius:2px;background:${badge.badgeBg};color:${badge.badgeText};border:1px solid ${badge.badgeBorder};\">${escapeHtml(\n acronym\n )}</span></td>` +\n `<td style=\"vertical-align:middle;\"><span style=\"font-size:13px;font-weight:500;color:${labelColor};\">${escapeHtml(\n typeLabel\n )}${titleHtml}</span></td>` +\n `</tr></table></td></tr>`\n )\n}\n\n/** Open the outer card table with inline card styling for the theme. */\nfunction cardOpen(theme: CardTheme, kind: 'element' | 'component'): string {\n const p = CARD_PALETTE[theme]\n return (\n `<table role=\"presentation\" cellpadding=\"0\" cellspacing=\"0\" border=\"0\" class=\"skhema-card\" data-skhema-kind=\"${kind}\" data-theme=\"${theme}\" ` +\n `style=\"width:100%;max-width:600px;border-collapse:separate;background:${p.cardBg};border:1px solid ${p.border};border-radius:${CARD_RADIUS};box-shadow:${CARD_SHADOW};overflow:hidden;margin:8px 0;font-family:${FONT_STACK};line-height:1.5;color:${p.text};\">`\n )\n}\n\n/* ------------------------------------------------------------------ *\n * Public renderers *\n * ------------------------------------------------------------------ */\n\n/**\n * Render the official Skhema **element** card as email-safe HTML.\n * The badge acronym + palette resolve from the element's owning component type,\n * exactly like the live `<skhema-element>` embed.\n */\nexport function renderElementCardHtml(data: ElementCardData): string {\n const theme = data.theme ?? 'light'\n const p = CARD_PALETTE[theme]\n const label = getElementTypeLabel(data.elementType as ElementValue)\n const componentType = resolveComponentType(data.elementType)\n const acronym = getComponentTypeAcronym(componentType)\n const badge = resolveBadgePalette(componentType, theme)\n\n return (\n cardOpen(theme, 'element') +\n renderHeader(badge, acronym, label, p.text, theme) +\n `<tr><td style=\"padding:16px;\">` +\n `<div style=\"font-size:15px;line-height:1.6;color:${p.text};word-wrap:break-word;overflow-wrap:break-word;\">${sanitizeContent(\n data.content\n )}</div>` +\n `</td></tr>` +\n renderFooter(data.saveUrl, data, theme) +\n `</table>`\n )\n}\n\n/**\n * Render the official Skhema **component** card as email-safe HTML — a coloured\n * top bar, header (badge + type label + optional title), per-element-type\n * groups, and the shared footer.\n */\nexport function renderComponentCardHtml(data: ComponentCardData): string {\n const theme = data.theme ?? 'light'\n const p = CARD_PALETTE[theme]\n const label = getComponentTypeLabel(data.componentType)\n const acronym = getComponentTypeAcronym(data.componentType)\n const badge = resolveBadgePalette(data.componentType, theme)\n\n // Group elements by type (preserving first-occurrence order), mirroring the\n // live `<skhema-component>` body: one small-caps label per type, with each\n // element's content as a left-ruled block beneath it.\n const groups = new Map<string, string[]>()\n for (const el of data.elements) {\n const existing = groups.get(el.elementType)\n if (existing) existing.push(el.content)\n else groups.set(el.elementType, [el.content])\n }\n\n const groupsHtml = Array.from(groups.entries())\n .map(\n ([elementType, contents]) =>\n `<div style=\"margin-bottom:16px;\">` +\n `<div style=\"text-transform:uppercase;letter-spacing:0.05em;font-family:${MONO_STACK};font-size:10px;font-weight:600;color:${p.textMuted};margin:0 0 4px;\">${escapeHtml(\n getElementTypeLabel(elementType as ElementValue)\n )}</div>` +\n contents\n .map(\n (content) =>\n `<div style=\"font-size:14px;line-height:1.6;color:${p.text};padding-left:8px;border-left:2px solid ${p.border};word-wrap:break-word;overflow-wrap:break-word;\">${sanitizeContent(\n content\n )}</div>`\n )\n .join('') +\n `</div>`\n )\n .join('')\n\n return (\n cardOpen(theme, 'component') +\n `<tr><td style=\"height:2px;line-height:2px;font-size:0;background:${badge.topBorder};\">&nbsp;</td></tr>` +\n renderHeader(badge, acronym, label, p.textMuted, theme, data.title) +\n `<tr><td style=\"padding:16px;\">${groupsHtml}</td></tr>` +\n renderFooter(data.saveUrl, data, theme) +\n `</table>`\n )\n}\n"],"names":[],"mappings":"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"}
@@ -1,5 +1,5 @@
1
1
  "use strict";
2
- const tokens = require("./tokens-BxpYMhDq.cjs");
2
+ const validation = require("./validation-IgKmvB17.cjs");
3
3
  const BRAND_PINK = "#cd476a";
4
4
  function hexToRgb(hex) {
5
5
  const h = hex.replace("#", "");
@@ -122,14 +122,14 @@ function humaniseContributorId(contributorId) {
122
122
  return contributorId.split(/[_-]/).map((word) => word.charAt(0).toUpperCase() + word.slice(1).toLowerCase()).join(" ");
123
123
  }
124
124
  function resolveBadgePalette(componentType, theme) {
125
- const surface = tokens.CARD_PALETTE[theme].cardBg;
126
- const colors = tokens.COMPONENT_COLORS[componentType];
125
+ const surface = validation.CARD_PALETTE[theme].cardBg;
126
+ const colors = validation.COMPONENT_COLORS[componentType];
127
127
  if (!colors) {
128
128
  return {
129
129
  badgeBg: surface,
130
- badgeText: tokens.PRIMARY_HEX,
131
- badgeBorder: tokens.PRIMARY_HEX,
132
- topBorder: tokens.PRIMARY_HEX
130
+ badgeText: validation.PRIMARY_HEX,
131
+ badgeBorder: validation.PRIMARY_HEX,
132
+ topBorder: validation.PRIMARY_HEX
133
133
  };
134
134
  }
135
135
  const text = oklchToHex(colors.text);
@@ -151,36 +151,36 @@ function renderAuthorHtml(author, mutedColor) {
151
151
  label = `By ${escapeHtml(humaniseContributorId(author.contributorId.trim()))}`;
152
152
  }
153
153
  if (!label) return "&nbsp;";
154
- return `<span style="display:inline-block;width:14px;height:14px;vertical-align:middle;color:${mutedColor};">${tokens.USER_ICON_SVG}</span><span style="vertical-align:middle;padding-left:6px;">${label}</span>`;
154
+ return `<span style="display:inline-block;width:14px;height:14px;vertical-align:middle;color:${mutedColor};">${validation.USER_ICON_SVG}</span><span style="vertical-align:middle;padding-left:6px;">${label}</span>`;
155
155
  }
156
156
  function renderFooter(saveUrl, author, theme) {
157
- const p = tokens.CARD_PALETTE[theme];
157
+ const p = validation.CARD_PALETTE[theme];
158
158
  return `<tr><td style="padding:12px 16px;border-top:1px solid ${p.border};"><table role="presentation" cellpadding="0" cellspacing="0" border="0" style="width:100%;border-collapse:collapse;"><tr><td style="vertical-align:middle;"><span style="font-size:12px;color:${p.textMuted};">${renderAuthorHtml(
159
159
  author,
160
160
  p.textMuted
161
- )}</span></td><td style="text-align:right;vertical-align:middle;white-space:nowrap;"><a href="${escapeAttr(saveUrl)}" class="skhema-save-btn" target="_blank" rel="noopener noreferrer" title="Save this to Skhema" style="display:inline-block;background:${tokens.PRIMARY_HEX};color:#ffffff;font-size:12px;font-weight:500;text-decoration:none;padding:6px 14px;border-radius:${tokens.CARD_RADIUS};white-space:nowrap;">Save to Skhema &rarr;</a></td></tr></table><div style="font-size:11px;line-height:1.4;color:${p.textMuted};margin-top:8px;">Strategy powered by <a href="https://skhema.com" style="color:${p.textMuted};text-decoration:underline;" target="_blank" rel="noopener noreferrer">Skhema</a></div></td></tr>`;
161
+ )}</span></td><td style="text-align:right;vertical-align:middle;white-space:nowrap;"><a href="${escapeAttr(saveUrl)}" class="skhema-save-btn" target="_blank" rel="noopener noreferrer" title="Save this to Skhema" style="display:inline-block;background:${validation.PRIMARY_HEX};color:#ffffff;font-size:12px;font-weight:500;text-decoration:none;padding:6px 14px;border-radius:${validation.CARD_RADIUS};white-space:nowrap;">Save to Skhema &rarr;</a></td></tr></table><div style="font-size:11px;line-height:1.4;color:${p.textMuted};margin-top:8px;">Strategy powered by <a href="https://skhema.com" style="color:${p.textMuted};text-decoration:underline;" target="_blank" rel="noopener noreferrer">Skhema</a></div></td></tr>`;
162
162
  }
163
163
  function renderHeader(badge, acronym, typeLabel, labelColor, theme, title) {
164
- const p = tokens.CARD_PALETTE[theme];
164
+ const p = validation.CARD_PALETTE[theme];
165
165
  const titleHtml = title ? `<span style="color:${p.textMuted};"> &mdash; </span><span style="font-weight:600;color:${p.text};">${escapeHtml(
166
166
  title
167
167
  )}</span>` : "";
168
- return `<tr><td style="padding:12px 16px;border-bottom:1px solid ${p.border};"><table role="presentation" cellpadding="0" cellspacing="0" border="0" style="width:100%;border-collapse:collapse;"><tr><td style="width:1%;white-space:nowrap;padding-right:8px;vertical-align:middle;"><span class="skhema-acronym-badge" title="${escapeAttr(typeLabel)}" style="display:inline-block;font-family:${tokens.MONO_STACK};font-size:10px;font-weight:600;letter-spacing:0.02em;padding:2px 6px;border-radius:2px;background:${badge.badgeBg};color:${badge.badgeText};border:1px solid ${badge.badgeBorder};">${escapeHtml(
168
+ return `<tr><td style="padding:12px 16px;border-bottom:1px solid ${p.border};"><table role="presentation" cellpadding="0" cellspacing="0" border="0" style="width:100%;border-collapse:collapse;"><tr><td style="width:1%;white-space:nowrap;padding-right:8px;vertical-align:middle;"><span class="skhema-acronym-badge" title="${escapeAttr(typeLabel)}" style="display:inline-block;font-family:${validation.MONO_STACK};font-size:10px;font-weight:600;letter-spacing:0.02em;padding:2px 6px;border-radius:2px;background:${badge.badgeBg};color:${badge.badgeText};border:1px solid ${badge.badgeBorder};">${escapeHtml(
169
169
  acronym
170
170
  )}</span></td><td style="vertical-align:middle;"><span style="font-size:13px;font-weight:500;color:${labelColor};">${escapeHtml(
171
171
  typeLabel
172
172
  )}${titleHtml}</span></td></tr></table></td></tr>`;
173
173
  }
174
174
  function cardOpen(theme, kind) {
175
- const p = tokens.CARD_PALETTE[theme];
176
- return `<table role="presentation" cellpadding="0" cellspacing="0" border="0" class="skhema-card" data-skhema-kind="${kind}" data-theme="${theme}" style="width:100%;max-width:600px;border-collapse:separate;background:${p.cardBg};border:1px solid ${p.border};border-radius:${tokens.CARD_RADIUS};box-shadow:${tokens.CARD_SHADOW};overflow:hidden;margin:8px 0;font-family:${tokens.FONT_STACK};line-height:1.5;color:${p.text};">`;
175
+ const p = validation.CARD_PALETTE[theme];
176
+ return `<table role="presentation" cellpadding="0" cellspacing="0" border="0" class="skhema-card" data-skhema-kind="${kind}" data-theme="${theme}" style="width:100%;max-width:600px;border-collapse:separate;background:${p.cardBg};border:1px solid ${p.border};border-radius:${validation.CARD_RADIUS};box-shadow:${validation.CARD_SHADOW};overflow:hidden;margin:8px 0;font-family:${validation.FONT_STACK};line-height:1.5;color:${p.text};">`;
177
177
  }
178
178
  function renderElementCardHtml(data) {
179
179
  const theme = data.theme ?? "light";
180
- const p = tokens.CARD_PALETTE[theme];
181
- const label = tokens.getElementTypeLabel(data.elementType);
182
- const componentType = tokens.resolveComponentType(data.elementType);
183
- const acronym = tokens.getComponentTypeAcronym(componentType);
180
+ const p = validation.CARD_PALETTE[theme];
181
+ const label = validation.getElementTypeLabel(data.elementType);
182
+ const componentType = validation.resolveComponentType(data.elementType);
183
+ const acronym = validation.getComponentTypeAcronym(componentType);
184
184
  const badge = resolveBadgePalette(componentType, theme);
185
185
  return cardOpen(theme, "element") + renderHeader(badge, acronym, label, p.text, theme) + `<tr><td style="padding:16px;"><div style="font-size:15px;line-height:1.6;color:${p.text};word-wrap:break-word;overflow-wrap:break-word;">${sanitizeContent(
186
186
  data.content
@@ -188,9 +188,9 @@ function renderElementCardHtml(data) {
188
188
  }
189
189
  function renderComponentCardHtml(data) {
190
190
  const theme = data.theme ?? "light";
191
- const p = tokens.CARD_PALETTE[theme];
192
- const label = tokens.getComponentTypeLabel(data.componentType);
193
- const acronym = tokens.getComponentTypeAcronym(data.componentType);
191
+ const p = validation.CARD_PALETTE[theme];
192
+ const label = validation.getComponentTypeLabel(data.componentType);
193
+ const acronym = validation.getComponentTypeAcronym(data.componentType);
194
194
  const badge = resolveBadgePalette(data.componentType, theme);
195
195
  const groups = /* @__PURE__ */ new Map();
196
196
  for (const el of data.elements) {
@@ -199,8 +199,8 @@ function renderComponentCardHtml(data) {
199
199
  else groups.set(el.elementType, [el.content]);
200
200
  }
201
201
  const groupsHtml = Array.from(groups.entries()).map(
202
- ([elementType, contents]) => `<div style="margin-bottom:16px;"><div style="text-transform:uppercase;letter-spacing:0.05em;font-family:${tokens.MONO_STACK};font-size:10px;font-weight:600;color:${p.textMuted};margin:0 0 4px;">${escapeHtml(
203
- tokens.getElementTypeLabel(elementType)
202
+ ([elementType, contents]) => `<div style="margin-bottom:16px;"><div style="text-transform:uppercase;letter-spacing:0.05em;font-family:${validation.MONO_STACK};font-size:10px;font-weight:600;color:${p.textMuted};margin:0 0 4px;">${escapeHtml(
203
+ validation.getElementTypeLabel(elementType)
204
204
  )}</div>` + contents.map(
205
205
  (content) => `<div style="font-size:14px;line-height:1.6;color:${p.text};padding-left:8px;border-left:2px solid ${p.border};word-wrap:break-word;overflow-wrap:break-word;">${sanitizeContent(
206
206
  content
@@ -212,4 +212,4 @@ function renderComponentCardHtml(data) {
212
212
  exports.renderComponentCardHtml = renderComponentCardHtml;
213
213
  exports.renderElementCardHtml = renderElementCardHtml;
214
214
  exports.validateContentSecurity = validateContentSecurity;
215
- //# sourceMappingURL=index-B8ZF3snD.cjs.map
215
+ //# sourceMappingURL=index-DNCkzeZj.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index-DNCkzeZj.cjs","sources":["../src/utils/color.ts","../src/utils/sanitization.ts","../src/render/index.ts"],"sourcesContent":["/**\n * DOM-free colour utilities for the email-safe card renderer.\n *\n * The card palette (`COMPONENT_COLORS`) is authored in `oklch()` for the live\n * browser embed, but email clients can't parse `oklch()` and `<style>` is\n * stripped, so every colour must be a pre-computed sRGB hex with all styles\n * inlined. This converts a single `oklch(...)` token to hex, compositing any\n * alpha over a flat background (white for light cards, the dark card surface\n * for dark cards) so the result is opaque and email-safe.\n *\n * This is the single source of the conversion that `sk comms curated` and the\n * `CuratedElements` email template previously duplicated.\n */\n\n/** Brand pink (hsl(344 57% 54%)) — fallback when a component palette is missing. */\nexport const BRAND_PINK = '#cd476a'\n\nfunction hexToRgb(hex: string): [number, number, number] {\n const h = hex.replace('#', '')\n return [\n parseInt(h.slice(0, 2), 16),\n parseInt(h.slice(2, 4), 16),\n parseInt(h.slice(4, 6), 16),\n ]\n}\n\n/**\n * Convert a single `oklch(...)` token to an email-safe sRGB hex. When the token\n * carries an alpha (e.g. the badge `bg` @ 0.15 / `border` @ 0.3), the colour is\n * composited over `overHex` so the result is a flat opaque hex.\n *\n * @param token An `oklch(L C H)` or `oklch(L C H / A)` string.\n * @param overHex The flat background to composite alpha over (default white).\n */\nexport function oklchToHex(token: string, overHex = '#ffffff'): string {\n const m = token.match(\n /oklch\\(\\s*([\\d.]+)\\s+([\\d.]+)\\s+([\\d.]+)\\s*(?:\\/\\s*([\\d.]+)\\s*)?\\)/\n )\n if (!m) return BRAND_PINK\n const L = parseFloat(m[1])\n const C = parseFloat(m[2])\n const H = parseFloat(m[3])\n const alpha = m[4] !== undefined ? parseFloat(m[4]) : 1\n const h = (H * Math.PI) / 180\n const a = C * Math.cos(h)\n const b = C * Math.sin(h)\n const l_ = L + 0.3963377774 * a + 0.2158037573 * b\n const m_ = L - 0.1055613458 * a - 0.0638541728 * b\n const s_ = L - 0.0894841775 * a - 1.291485548 * b\n const lr = l_ ** 3\n const mr = m_ ** 3\n const sr = s_ ** 3\n const R = 4.0767416621 * lr - 3.3077115913 * mr + 0.2309699292 * sr\n const G = -1.2684380046 * lr + 2.6097574011 * mr - 0.3413193965 * sr\n const B = -0.0041960863 * lr - 0.7034186147 * mr + 1.707614701 * sr\n const gamma = (x: number): number => {\n const c = Math.max(0, Math.min(1, x))\n return c <= 0.0031308 ? 12.92 * c : 1.055 * Math.pow(c, 1 / 2.4) - 0.055\n }\n let rgb = [gamma(R) * 255, gamma(G) * 255, gamma(B) * 255]\n if (alpha < 1) {\n const [br, bg, bb] = hexToRgb(overHex)\n rgb = [\n rgb[0] * alpha + br * (1 - alpha),\n rgb[1] * alpha + bg * (1 - alpha),\n rgb[2] * alpha + bb * (1 - alpha),\n ]\n }\n return (\n '#' + rgb.map((c) => Math.round(c).toString(16).padStart(2, '0')).join('')\n )\n}\n","/**\n * Content sanitization utilities for Skhema cards.\n *\n * DOM-free by design: `sanitizeContent` is imported by the email-safe card\n * renderer (`@skhema/embed/render`), which must run in Node / email / CLI\n * runtimes with no `document`. Keep every export here free of DOM access at\n * call time (the legacy `stripHtml` below is the one exception and is not used\n * by the renderer).\n */\n\n/**\n * HTML entity encoding for basic XSS protection. Regex-based (no DOM) so it is\n * safe in non-browser runtimes. Escapes the text-context entities plus quotes.\n */\nfunction htmlEncode(str: string): string {\n return str\n .replace(/&/g, '&amp;')\n .replace(/</g, '&lt;')\n .replace(/>/g, '&gt;')\n .replace(/\"/g, '&quot;')\n}\n\n/**\n * Sanitizes content to prevent XSS attacks and removes URLs\n * @param content The raw content to sanitize\n * @returns Sanitized HTML-safe content with URLs removed\n */\nexport function sanitizeContent(content: string): string {\n // Strip URLs first\n let sanitized = stripUrls(content)\n\n // Encode all HTML entities to prevent script injection\n sanitized = htmlEncode(sanitized)\n\n // Preserve line breaks for readability\n sanitized = sanitized.replace(/\\n/g, '<br>')\n\n // Apply text wrapping rules for long text\n sanitized = applyTextWrapping(sanitized)\n\n return sanitized\n}\n\n/**\n * Strips all URLs from the content\n * @param text The text containing potential URLs\n * @returns Text with all URLs removed\n */\nfunction stripUrls(text: string): string {\n // Comprehensive URL patterns to remove\n const patterns = [\n // Standard URLs with protocols\n /https?:\\/\\/[^\\s<>\"{}|\\\\^`[\\]]+/gi,\n // FTP URLs\n /ftp:\\/\\/[^\\s<>\"{}|\\\\^`[\\]]+/gi,\n // URLs without protocol but with www\n /www\\.[^\\s<>\"{}|\\\\^`[\\]]+/gi,\n // Email-like patterns\n /[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,}/gi,\n // Common domain patterns (anything.com, anything.org, etc.)\n /(?:^|\\s)([a-zA-Z0-9-]+\\.)+[a-zA-Z]{2,}(?:\\/[^\\s]*)?/gi,\n ]\n\n let stripped = text\n patterns.forEach((pattern) => {\n stripped = stripped.replace(pattern, '')\n })\n\n // Clean up any multiple spaces left after URL removal\n stripped = stripped.replace(/\\s+/g, ' ').trim()\n\n return stripped\n}\n\n/**\n * Applies intelligent text wrapping to prevent layout breaking\n * @param text The text to apply wrapping rules to\n * @returns Text with appropriate wrapping hints\n */\nfunction applyTextWrapping(text: string): string {\n // Split text into words\n const words = text.split(/(\\s+)/)\n\n return words\n .map((word) => {\n // Skip if it's whitespace or already contains HTML\n if (/^\\s+$/.test(word) || word.includes('<')) {\n return word\n }\n\n // For very long words (>30 chars), add word-break opportunities\n if (word.length > 30) {\n // Insert zero-width spaces every 10 characters for breaking\n return word.replace(/(.{10})/g, '$1\\u200B')\n }\n\n return word\n })\n .join('')\n}\n\n/**\n * Validates if content contains potentially malicious patterns or URLs\n * @param content The content to validate\n * @returns Object with validation status and detected issues\n */\nexport function validateContentSecurity(content: string): {\n isSecure: boolean\n issues: string[]\n} {\n const issues: string[] = []\n\n // Check for script tags\n if (/<script[\\s>]/i.test(content)) {\n issues.push('Script tags detected')\n }\n\n // Check for event handlers\n if (/on\\w+\\s*=/i.test(content)) {\n issues.push('Event handlers detected')\n }\n\n // Check for javascript: protocol\n if (/javascript:/i.test(content)) {\n issues.push('JavaScript protocol detected')\n }\n\n // Check for data: URLs that could contain scripts\n if (/data:[^,]*script/i.test(content)) {\n issues.push('Data URL with script detected')\n }\n\n // Check for iframe tags\n if (/<iframe[\\s>]/i.test(content)) {\n issues.push('Iframe tags detected')\n }\n\n // Check for URLs (since we want to disallow them)\n if (/https?:\\/\\//i.test(content) || /www\\./i.test(content)) {\n issues.push('URLs detected in content')\n }\n\n return {\n isSecure: issues.length === 0,\n issues,\n }\n}\n\n/**\n * Strips all HTML tags from content\n * @param content The content to strip\n * @returns Plain text content\n */\nexport function stripHtml(content: string): string {\n const div = document.createElement('div')\n div.innerHTML = content\n return div.textContent || div.innerText || ''\n}\n\n/**\n * Checks if content contains any URLs\n * @param content The content to check\n * @returns True if URLs are found\n */\nexport function containsUrls(content: string): boolean {\n const urlPatterns = [\n /https?:\\/\\//i,\n /ftp:\\/\\//i,\n /www\\./i,\n /[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,}/i,\n /(?:^|\\s)([a-zA-Z0-9-]+\\.)+[a-zA-Z]{2,}(?:\\/[^\\s]*)?/i,\n ]\n\n return urlPatterns.some((pattern) => pattern.test(content))\n}\n","/**\n * `@skhema/embed/render` — the canonical, DOM-free source of truth for the\n * official Skhema element / component card HTML.\n *\n * `renderElementCardHtml(data)` / `renderComponentCardHtml(data)` return\n * EMAIL-SAFE HTML: a `role=\"presentation\"` table layout with every style\n * inlined as flat hex (no shadow DOM, no `<style>`, no `oklch()`, no CSS vars).\n * The same output is used three ways:\n *\n * 1. the live browser embed (`SkhemaElement` / `SkhemaComponent` inject it\n * into shadow DOM and layer hover/transition CSS on top);\n * 2. the `CuratedElements` email template (injected verbatim); and\n * 3. any third-party / contributor email generator.\n *\n * The module is pure — importing it never touches the DOM, so it is safe in\n * Node, edge, and email build runtimes. It builds NO URLs: callers pass the\n * fully-formed `saveUrl` (e.g. the `/save` handoff) so each surface controls\n * its own UTM tagging.\n */\nimport type { ElementValue } from '@skhema/method/vocabulary'\nimport {\n CARD_PALETTE,\n CARD_RADIUS,\n CARD_SHADOW,\n COMPONENT_COLORS,\n FONT_STACK,\n MONO_STACK,\n PRIMARY_HEX,\n USER_ICON_SVG,\n type CardTheme,\n} from '../styles/design-tokens.js'\nimport { oklchToHex } from '../utils/color.js'\nimport {\n getComponentTypeAcronym,\n getComponentTypeLabel,\n resolveComponentType,\n} from '../utils/component-validation.js'\nimport { sanitizeContent } from '../utils/sanitization.js'\nimport { getElementTypeLabel } from '../utils/validation.js'\n\n/* ------------------------------------------------------------------ *\n * Public data shapes (documented contract — see README \"render\") *\n * ------------------------------------------------------------------ */\n\n/** Card theme. Email is always `'light'`; the browser embed passes the\n * detected page theme. Defaults to `'light'` when omitted. */\nexport type { CardTheme }\n\n/** Author attribution shared by both card kinds. */\ninterface AuthorFields {\n /** Display name. When omitted, falls back to a humanised `contributorId`. */\n authorName?: string | null\n /** Public contributor slug — when present, the name links to the profile. */\n authorSlug?: string | null\n /** Contributor id, used only for the name fallback when `authorName` is unset. */\n contributorId?: string | null\n}\n\n/** Input for {@link renderElementCardHtml}. */\nexport interface ElementCardData extends AuthorFields {\n /** Skhema element type value, e.g. `\"key_challenge\"`. */\n elementType: string\n /** The element content / premise (plain text; sanitised + escaped here). */\n content: string\n /** Pre-built handoff URL for the \"Save to Skhema\" button. */\n saveUrl: string\n /** Card theme (default `'light'`). */\n theme?: CardTheme\n}\n\n/** A single element row inside a component card. */\nexport interface ComponentCardElement {\n elementType: string\n content: string\n}\n\n/** Input for {@link renderComponentCardHtml}. */\nexport interface ComponentCardData extends AuthorFields {\n /** Skhema component type value, e.g. `\"diagnosis\"`. */\n componentType: string\n /** Optional component title shown after a \"—\" separator in the header. */\n title?: string | null\n /** The component's elements, in display order. */\n elements: ComponentCardElement[]\n /** Pre-built handoff URL for the \"Save to Skhema\" button. */\n saveUrl: string\n /** Card theme (default `'light'`). */\n theme?: CardTheme\n}\n\n/* ------------------------------------------------------------------ *\n * Internal helpers *\n * ------------------------------------------------------------------ */\n\nfunction escapeHtml(text: string): string {\n return text\n .replace(/&/g, '&amp;')\n .replace(/</g, '&lt;')\n .replace(/>/g, '&gt;')\n .replace(/\"/g, '&quot;')\n}\n\nfunction escapeAttr(text: string): string {\n return escapeHtml(text).replace(/'/g, '&#39;')\n}\n\nfunction humaniseContributorId(contributorId: string): string {\n return contributorId\n .split(/[_-]/)\n .map((word) => word.charAt(0).toUpperCase() + word.slice(1).toLowerCase())\n .join(' ')\n}\n\ninterface BadgePalette {\n badgeBg: string\n badgeText: string\n badgeBorder: string\n topBorder: string\n}\n\n/** Resolve the email-safe badge / top-border hex for a component type + theme. */\nfunction resolveBadgePalette(\n componentType: string,\n theme: CardTheme\n): BadgePalette {\n const surface = CARD_PALETTE[theme].cardBg\n const colors =\n COMPONENT_COLORS[componentType as keyof typeof COMPONENT_COLORS]\n if (!colors) {\n return {\n badgeBg: surface,\n badgeText: PRIMARY_HEX,\n badgeBorder: PRIMARY_HEX,\n topBorder: PRIMARY_HEX,\n }\n }\n const text = oklchToHex(colors.text)\n return {\n badgeBg: oklchToHex(colors.bg, surface),\n badgeText: text,\n badgeBorder: oklchToHex(colors.border, surface),\n topBorder: text,\n }\n}\n\n/** Contributor line inner HTML: person icon + \"By {author}\" (linked if slug). */\nfunction renderAuthorHtml(author: AuthorFields, mutedColor: string): string {\n let label = ''\n if (author.authorName && author.authorName.trim()) {\n const name = escapeHtml(author.authorName.trim())\n label = author.authorSlug\n ? `By <a href=\"https://skhema.com/contributors/${encodeURIComponent(\n author.authorSlug\n )}\" style=\"color:${mutedColor};text-decoration:none;\" target=\"_blank\" rel=\"noopener noreferrer\">${name}</a>`\n : `By ${name}`\n } else if (author.contributorId && author.contributorId.trim()) {\n label = `By ${escapeHtml(humaniseContributorId(author.contributorId.trim()))}`\n }\n\n if (!label) return '&nbsp;'\n\n return (\n `<span style=\"display:inline-block;width:14px;height:14px;vertical-align:middle;color:${mutedColor};\">${USER_ICON_SVG}</span>` +\n `<span style=\"vertical-align:middle;padding-left:6px;\">${label}</span>`\n )\n}\n\n/** The shared footer (contributor line + save button + attribution). */\nfunction renderFooter(\n saveUrl: string,\n author: AuthorFields,\n theme: CardTheme\n): string {\n const p = CARD_PALETTE[theme]\n return (\n `<tr><td style=\"padding:12px 16px;border-top:1px solid ${p.border};\">` +\n `<table role=\"presentation\" cellpadding=\"0\" cellspacing=\"0\" border=\"0\" style=\"width:100%;border-collapse:collapse;\"><tr>` +\n `<td style=\"vertical-align:middle;\"><span style=\"font-size:12px;color:${p.textMuted};\">${renderAuthorHtml(\n author,\n p.textMuted\n )}</span></td>` +\n `<td style=\"text-align:right;vertical-align:middle;white-space:nowrap;\">` +\n `<a href=\"${escapeAttr(saveUrl)}\" class=\"skhema-save-btn\" target=\"_blank\" rel=\"noopener noreferrer\" title=\"Save this to Skhema\" style=\"display:inline-block;background:${PRIMARY_HEX};color:#ffffff;font-size:12px;font-weight:500;text-decoration:none;padding:6px 14px;border-radius:${CARD_RADIUS};white-space:nowrap;\">Save to Skhema &rarr;</a>` +\n `</td></tr></table>` +\n `<div style=\"font-size:11px;line-height:1.4;color:${p.textMuted};margin-top:8px;\">Strategy powered by <a href=\"https://skhema.com\" style=\"color:${p.textMuted};text-decoration:underline;\" target=\"_blank\" rel=\"noopener noreferrer\">Skhema</a></div>` +\n `</td></tr>`\n )\n}\n\n/** Header row: acronym badge + type label (+ optional \"— title\" for components). */\nfunction renderHeader(\n badge: BadgePalette,\n acronym: string,\n typeLabel: string,\n labelColor: string,\n theme: CardTheme,\n title?: string | null\n): string {\n const p = CARD_PALETTE[theme]\n const titleHtml = title\n ? `<span style=\"color:${p.textMuted};\"> &mdash; </span><span style=\"font-weight:600;color:${p.text};\">${escapeHtml(\n title\n )}</span>`\n : ''\n return (\n `<tr><td style=\"padding:12px 16px;border-bottom:1px solid ${p.border};\">` +\n `<table role=\"presentation\" cellpadding=\"0\" cellspacing=\"0\" border=\"0\" style=\"width:100%;border-collapse:collapse;\"><tr>` +\n `<td style=\"width:1%;white-space:nowrap;padding-right:8px;vertical-align:middle;\">` +\n `<span class=\"skhema-acronym-badge\" title=\"${escapeAttr(typeLabel)}\" style=\"display:inline-block;font-family:${MONO_STACK};font-size:10px;font-weight:600;letter-spacing:0.02em;padding:2px 6px;border-radius:2px;background:${badge.badgeBg};color:${badge.badgeText};border:1px solid ${badge.badgeBorder};\">${escapeHtml(\n acronym\n )}</span></td>` +\n `<td style=\"vertical-align:middle;\"><span style=\"font-size:13px;font-weight:500;color:${labelColor};\">${escapeHtml(\n typeLabel\n )}${titleHtml}</span></td>` +\n `</tr></table></td></tr>`\n )\n}\n\n/** Open the outer card table with inline card styling for the theme. */\nfunction cardOpen(theme: CardTheme, kind: 'element' | 'component'): string {\n const p = CARD_PALETTE[theme]\n return (\n `<table role=\"presentation\" cellpadding=\"0\" cellspacing=\"0\" border=\"0\" class=\"skhema-card\" data-skhema-kind=\"${kind}\" data-theme=\"${theme}\" ` +\n `style=\"width:100%;max-width:600px;border-collapse:separate;background:${p.cardBg};border:1px solid ${p.border};border-radius:${CARD_RADIUS};box-shadow:${CARD_SHADOW};overflow:hidden;margin:8px 0;font-family:${FONT_STACK};line-height:1.5;color:${p.text};\">`\n )\n}\n\n/* ------------------------------------------------------------------ *\n * Public renderers *\n * ------------------------------------------------------------------ */\n\n/**\n * Render the official Skhema **element** card as email-safe HTML.\n * The badge acronym + palette resolve from the element's owning component type,\n * exactly like the live `<skhema-element>` embed.\n */\nexport function renderElementCardHtml(data: ElementCardData): string {\n const theme = data.theme ?? 'light'\n const p = CARD_PALETTE[theme]\n const label = getElementTypeLabel(data.elementType as ElementValue)\n const componentType = resolveComponentType(data.elementType)\n const acronym = getComponentTypeAcronym(componentType)\n const badge = resolveBadgePalette(componentType, theme)\n\n return (\n cardOpen(theme, 'element') +\n renderHeader(badge, acronym, label, p.text, theme) +\n `<tr><td style=\"padding:16px;\">` +\n `<div style=\"font-size:15px;line-height:1.6;color:${p.text};word-wrap:break-word;overflow-wrap:break-word;\">${sanitizeContent(\n data.content\n )}</div>` +\n `</td></tr>` +\n renderFooter(data.saveUrl, data, theme) +\n `</table>`\n )\n}\n\n/**\n * Render the official Skhema **component** card as email-safe HTML — a coloured\n * top bar, header (badge + type label + optional title), per-element-type\n * groups, and the shared footer.\n */\nexport function renderComponentCardHtml(data: ComponentCardData): string {\n const theme = data.theme ?? 'light'\n const p = CARD_PALETTE[theme]\n const label = getComponentTypeLabel(data.componentType)\n const acronym = getComponentTypeAcronym(data.componentType)\n const badge = resolveBadgePalette(data.componentType, theme)\n\n // Group elements by type (preserving first-occurrence order), mirroring the\n // live `<skhema-component>` body: one small-caps label per type, with each\n // element's content as a left-ruled block beneath it.\n const groups = new Map<string, string[]>()\n for (const el of data.elements) {\n const existing = groups.get(el.elementType)\n if (existing) existing.push(el.content)\n else groups.set(el.elementType, [el.content])\n }\n\n const groupsHtml = Array.from(groups.entries())\n .map(\n ([elementType, contents]) =>\n `<div style=\"margin-bottom:16px;\">` +\n `<div style=\"text-transform:uppercase;letter-spacing:0.05em;font-family:${MONO_STACK};font-size:10px;font-weight:600;color:${p.textMuted};margin:0 0 4px;\">${escapeHtml(\n getElementTypeLabel(elementType as ElementValue)\n )}</div>` +\n contents\n .map(\n (content) =>\n `<div style=\"font-size:14px;line-height:1.6;color:${p.text};padding-left:8px;border-left:2px solid ${p.border};word-wrap:break-word;overflow-wrap:break-word;\">${sanitizeContent(\n content\n )}</div>`\n )\n .join('') +\n `</div>`\n )\n .join('')\n\n return (\n cardOpen(theme, 'component') +\n `<tr><td style=\"height:2px;line-height:2px;font-size:0;background:${badge.topBorder};\">&nbsp;</td></tr>` +\n renderHeader(badge, acronym, label, p.textMuted, theme, data.title) +\n `<tr><td style=\"padding:16px;\">${groupsHtml}</td></tr>` +\n renderFooter(data.saveUrl, data, theme) +\n `</table>`\n )\n}\n"],"names":["CARD_PALETTE","COMPONENT_COLORS","PRIMARY_HEX","USER_ICON_SVG","CARD_RADIUS","MONO_STACK","CARD_SHADOW","FONT_STACK","getElementTypeLabel","resolveComponentType","getComponentTypeAcronym","getComponentTypeLabel"],"mappings":"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"}
package/dist/index.cjs CHANGED
@@ -1,7 +1,7 @@
1
1
  "use strict";
2
2
  Object.defineProperties(exports, { __esModule: { value: true }, [Symbol.toStringTag]: { value: "Module" } });
3
- const render = require("./index-B8ZF3snD.cjs");
4
- const tokens = require("./tokens-BxpYMhDq.cjs");
3
+ const render = require("./index-DNCkzeZj.cjs");
4
+ const validation = require("./validation-IgKmvB17.cjs");
5
5
  const ANALYTICS_ENDPOINT = "https://analytics.skhema.com/functions/v1/embed-manage";
6
6
  const TRACKING_COOKIE_NAME = "_sk";
7
7
  const TRACKING_EXPIRY_HOURS = 24;
@@ -400,7 +400,7 @@ function generateStructuredData(content, elementType, contributorId, sourceUrl)
400
400
  "@type": "AnalysisContent",
401
401
  text: content,
402
402
  analysisType: elementType,
403
- category: tokens.getElementTypeLabel(elementType),
403
+ category: validation.getElementTypeLabel(elementType),
404
404
  contributor: contributorId,
405
405
  url: generateRedirectUrl(content, elementType, contributorId),
406
406
  provider: {
@@ -432,7 +432,7 @@ function generateRedirectUrl(content, elementType, contributorId, options = {})
432
432
  return `${baseUrl}?type=contributor&contributor_id=${contributorId}&element_type=${elementType}&content_hash=${contentHash}&${params.toString()}`;
433
433
  }
434
434
  function createMetaTags(content, elementType, contributorId) {
435
- const label = tokens.getElementTypeLabel(elementType);
435
+ const label = validation.getElementTypeLabel(elementType);
436
436
  return `
437
437
  <div itemscope itemtype="https://schema.org/AnalysisContent" style="display:none;">
438
438
  <meta itemprop="analysisType" content="${elementType}">
@@ -444,7 +444,7 @@ function createMetaTags(content, elementType, contributorId) {
444
444
  `;
445
445
  }
446
446
  function generateComponentStructuredData(title, componentType, contributorId, elements, sourceUrl) {
447
- const componentLabel = tokens.getComponentTypeLabel(componentType);
447
+ const componentLabel = validation.getComponentTypeLabel(componentType);
448
448
  return {
449
449
  "@context": "https://schema.org",
450
450
  "@type": "CreativeWork",
@@ -461,7 +461,7 @@ function generateComponentStructuredData(title, componentType, contributorId, el
461
461
  "@type": "AnalysisContent",
462
462
  text: el.content,
463
463
  analysisType: el.elementType,
464
- category: tokens.getElementTypeLabel(el.elementType)
464
+ category: validation.getElementTypeLabel(el.elementType)
465
465
  })),
466
466
  provider: {
467
467
  "@type": "Organization",
@@ -491,7 +491,7 @@ function generateComponentRedirectUrl(componentHash, componentType, contributorI
491
491
  return `${baseUrl}?type=component&component_type=${componentType}&component_hash=${componentHash}&contributor_id=${contributorId}&${params.toString()}`;
492
492
  }
493
493
  function createAriaAttributes(elementType) {
494
- const label = tokens.getElementTypeLabel(elementType);
494
+ const label = validation.getElementTypeLabel(elementType);
495
495
  return {
496
496
  role: "article",
497
497
  "aria-label": `${label} - Strategic element`,
@@ -544,7 +544,7 @@ const styles$1 = `
544
544
  }
545
545
 
546
546
  .skhema-card:hover {
547
- box-shadow: ${tokens.CARD_SHADOW_LG} !important;
547
+ box-shadow: ${validation.CARD_SHADOW_LG} !important;
548
548
  transform: translateY(-1px);
549
549
  }
550
550
 
@@ -553,11 +553,11 @@ const styles$1 = `
553
553
  }
554
554
 
555
555
  .skhema-save-btn:hover {
556
- background: ${tokens.PRIMARY_HOVER_HEX} !important;
556
+ background: ${validation.PRIMARY_HOVER_HEX} !important;
557
557
  }
558
558
 
559
559
  .skhema-save-btn:active {
560
- background: ${tokens.PRIMARY_PRESSED_HEX} !important;
560
+ background: ${validation.PRIMARY_PRESSED_HEX} !important;
561
561
  }
562
562
 
563
563
  /* Error card wrapper (browser-only state) */
@@ -734,13 +734,13 @@ class SkhemaComponent extends HTMLElement {
734
734
  });
735
735
  }
736
736
  render() {
737
- const validation = this.validateComponentAttributes();
738
- if (!validation.isValid) {
739
- this.renderError("Invalid component attributes", validation.errors);
737
+ const validation2 = this.validateComponentAttributes();
738
+ if (!validation2.isValid) {
739
+ this.renderError("Invalid component attributes", validation2.errors);
740
740
  return;
741
741
  }
742
- const componentType = validation.componentType;
743
- const contributorId = validation.contributorId;
742
+ const componentType = validation2.componentType;
743
+ const contributorId = validation2.contributorId;
744
744
  const title = this.getAttribute("title") || "";
745
745
  const childElements = this.gatherChildElements(componentType);
746
746
  if (childElements.length === 0) {
@@ -779,7 +779,7 @@ class SkhemaComponent extends HTMLElement {
779
779
  const contributorId = this.getAttribute("contributor-id");
780
780
  if (!componentType) {
781
781
  errors.push("Missing required attribute: component-type");
782
- } else if (!tokens.isValidComponentType(componentType)) {
782
+ } else if (!validation.isValidComponentType(componentType)) {
783
783
  errors.push(`Invalid component-type "${componentType}"`);
784
784
  }
785
785
  if (!contributorId) {
@@ -790,7 +790,7 @@ class SkhemaComponent extends HTMLElement {
790
790
  return {
791
791
  isValid: errors.length === 0,
792
792
  errors,
793
- componentType: tokens.isValidComponentType(componentType || "") ? componentType : void 0,
793
+ componentType: validation.isValidComponentType(componentType || "") ? componentType : void 0,
794
794
  contributorId: contributorId || void 0
795
795
  };
796
796
  }
@@ -802,14 +802,14 @@ class SkhemaComponent extends HTMLElement {
802
802
  for (const child of children) {
803
803
  const data = child.getElementData?.();
804
804
  if (!data) continue;
805
- if (!tokens.validateElementBelongsToComponent(data.elementType, componentType)) {
805
+ if (!validation.validateElementBelongsToComponent(data.elementType, componentType)) {
806
806
  console.warn(
807
807
  `skhema-component: element type "${data.elementType}" does not belong to component type "${componentType}"`
808
808
  );
809
809
  }
810
810
  elements.push(data);
811
811
  }
812
- const orderedTypes = tokens.getElementTypesForComponent(componentType);
812
+ const orderedTypes = validation.getElementTypesForComponent(componentType);
813
813
  elements.sort((a, b) => {
814
814
  const aIdx = orderedTypes.indexOf(a.elementType);
815
815
  const bIdx = orderedTypes.indexOf(b.elementType);
@@ -822,7 +822,7 @@ class SkhemaComponent extends HTMLElement {
822
822
  renderContent(elements) {
823
823
  if (!this.contentData) return;
824
824
  const { component_type, contributor_id, title } = this.contentData;
825
- const componentLabel = tokens.getComponentTypeLabel(component_type);
825
+ const componentLabel = validation.getComponentTypeLabel(component_type);
826
826
  const themeAttribute = this.getAttribute("theme") || "auto";
827
827
  const actualTheme = this.getActualTheme(themeAttribute);
828
828
  const redirectUrl = generateComponentRedirectUrl(
@@ -1096,12 +1096,12 @@ const styles = `
1096
1096
  }
1097
1097
 
1098
1098
  .skhema-card:hover {
1099
- box-shadow: ${tokens.CARD_SHADOW_LG} !important;
1099
+ box-shadow: ${validation.CARD_SHADOW_LG} !important;
1100
1100
  transform: translateY(-1px);
1101
1101
  }
1102
1102
 
1103
1103
  .skhema-card[data-skhema-kind="element"]:hover {
1104
- border-color: ${tokens.PRIMARY_HEX} !important;
1104
+ border-color: ${validation.PRIMARY_HEX} !important;
1105
1105
  }
1106
1106
 
1107
1107
  .skhema-save-btn {
@@ -1109,11 +1109,11 @@ const styles = `
1109
1109
  }
1110
1110
 
1111
1111
  .skhema-save-btn:hover {
1112
- background: ${tokens.PRIMARY_HOVER_HEX} !important;
1112
+ background: ${validation.PRIMARY_HOVER_HEX} !important;
1113
1113
  }
1114
1114
 
1115
1115
  .skhema-save-btn:active {
1116
- background: ${tokens.PRIMARY_PRESSED_HEX} !important;
1116
+ background: ${validation.PRIMARY_PRESSED_HEX} !important;
1117
1117
  }
1118
1118
 
1119
1119
  /* Error card wrapper (browser-only state) */
@@ -1266,11 +1266,11 @@ class SkhemaElement extends HTMLElement {
1266
1266
  this.componentConnected = true;
1267
1267
  if (this.closest("skhema-component")) {
1268
1268
  this.nestedMode = true;
1269
- const validation = tokens.validateAttributes(this);
1270
- if (!validation.isValid) {
1269
+ const validation$1 = validation.validateAttributes(this);
1270
+ if (!validation$1.isValid) {
1271
1271
  console.warn(
1272
1272
  "skhema-element: invalid attributes in nested mode",
1273
- validation.errors
1273
+ validation$1.errors
1274
1274
  );
1275
1275
  return;
1276
1276
  }
@@ -1280,8 +1280,8 @@ class SkhemaElement extends HTMLElement {
1280
1280
  return;
1281
1281
  }
1282
1282
  this.contentData = {
1283
- contributor_id: validation.contributorId,
1284
- element_type: validation.elementType,
1283
+ contributor_id: validation$1.contributorId,
1284
+ element_type: validation$1.elementType,
1285
1285
  content,
1286
1286
  content_hash: generateContentHash(content),
1287
1287
  source_url: this.getAttribute("source-url") || window.location.href,
@@ -1329,9 +1329,9 @@ class SkhemaElement extends HTMLElement {
1329
1329
  };
1330
1330
  }
1331
1331
  render() {
1332
- const validation = tokens.validateAttributes(this);
1333
- if (!validation.isValid) {
1334
- this.renderError("Invalid component attributes", validation.errors);
1332
+ const validation$1 = validation.validateAttributes(this);
1333
+ if (!validation$1.isValid) {
1334
+ this.renderError("Invalid component attributes", validation$1.errors);
1335
1335
  return;
1336
1336
  }
1337
1337
  const content = this.getContent();
@@ -1350,8 +1350,8 @@ class SkhemaElement extends HTMLElement {
1350
1350
  return;
1351
1351
  }
1352
1352
  this.contentData = {
1353
- contributor_id: validation.contributorId,
1354
- element_type: validation.elementType,
1353
+ contributor_id: validation$1.contributorId,
1354
+ element_type: validation$1.elementType,
1355
1355
  content,
1356
1356
  content_hash: generateContentHash(content),
1357
1357
  source_url: this.getAttribute("source-url") || window.location.href,
@@ -1610,15 +1610,15 @@ if (typeof window !== "undefined") {
1610
1610
  );
1611
1611
  }
1612
1612
  }
1613
- exports.getComponentTypeAcronym = tokens.getComponentTypeAcronym;
1614
- exports.getComponentTypeLabel = tokens.getComponentTypeLabel;
1615
- exports.getElementTypeAcronym = tokens.getElementTypeAcronym;
1616
- exports.getElementTypeLabel = tokens.getElementTypeLabel;
1617
- exports.isValidComponentType = tokens.isValidComponentType;
1618
- exports.isValidElementType = tokens.isValidElementType;
1619
- exports.resolveComponentType = tokens.resolveComponentType;
1620
- exports.validateAttributes = tokens.validateAttributes;
1621
- exports.validateElementBelongsToComponent = tokens.validateElementBelongsToComponent;
1613
+ exports.getComponentTypeAcronym = validation.getComponentTypeAcronym;
1614
+ exports.getComponentTypeLabel = validation.getComponentTypeLabel;
1615
+ exports.getElementTypeAcronym = validation.getElementTypeAcronym;
1616
+ exports.getElementTypeLabel = validation.getElementTypeLabel;
1617
+ exports.isValidComponentType = validation.isValidComponentType;
1618
+ exports.isValidElementType = validation.isValidElementType;
1619
+ exports.resolveComponentType = validation.resolveComponentType;
1620
+ exports.validateAttributes = validation.validateAttributes;
1621
+ exports.validateElementBelongsToComponent = validation.validateElementBelongsToComponent;
1622
1622
  exports.SkhemaComponent = SkhemaComponent;
1623
1623
  exports.SkhemaElement = SkhemaElement;
1624
1624
  exports.default = SkhemaElement;