@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.
- package/dist/components/types.d.ts +1 -1
- package/dist/components/types.d.ts.map +1 -1
- package/dist/embed.min.js +1 -1
- package/dist/embed.min.js.map +1 -1
- package/dist/{index-nkGvTyQR.js → index-C4JT9tyk.js} +2 -2
- package/dist/index-C4JT9tyk.js.map +1 -0
- package/dist/{index-B8ZF3snD.cjs → index-DNCkzeZj.cjs} +23 -23
- package/dist/index-DNCkzeZj.cjs.map +1 -0
- package/dist/index.cjs +43 -43
- package/dist/index.cjs.map +1 -1
- package/dist/index.es.js +11 -11
- package/dist/index.es.js.map +1 -1
- package/dist/render.cjs +2 -2
- package/dist/render.es.js +2 -2
- package/dist/tokens.cjs +13 -13
- package/dist/tokens.es.js +8 -8
- package/dist/utils/component-validation.d.ts +1 -1
- package/dist/utils/component-validation.d.ts.map +1 -1
- package/dist/utils/seo.d.ts +1 -1
- package/dist/utils/seo.d.ts.map +1 -1
- package/dist/utils/validation.d.ts +1 -1
- package/dist/utils/validation.d.ts.map +1 -1
- package/dist/{tokens-BQf5zJ3j.js → validation-BcXe-9fn.js} +16 -172
- package/dist/validation-BcXe-9fn.js.map +1 -0
- package/dist/{tokens-BxpYMhDq.cjs → validation-IgKmvB17.cjs} +12 -168
- package/dist/validation-IgKmvB17.cjs.map +1 -0
- package/package.json +4 -1
- package/dist/index-B8ZF3snD.cjs.map +0 -1
- package/dist/index-nkGvTyQR.js.map +0 -1
- package/dist/method-schema.d.ts +0 -244
- package/dist/method-schema.d.ts.map +0 -1
- package/dist/tokens-BQf5zJ3j.js.map +0 -1
- package/dist/tokens-BxpYMhDq.cjs.map +0 -1
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { COMPONENT_TYPES, SKHEMA_MAPPING, ELEMENT_TYPES } from "@skhema/method/vocabulary";
|
|
1
2
|
const COMPONENT_COLORS = {
|
|
2
3
|
diagnosis: {
|
|
3
4
|
bg: "oklch(0.65 0.06 25 / 0.15)",
|
|
@@ -76,163 +77,6 @@ const CARD_VARS = {
|
|
|
76
77
|
}
|
|
77
78
|
};
|
|
78
79
|
const USER_ICON_SVG = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M19 21v-2a4 4 0 0 0-4-4H9a4 4 0 0 0-4 4v2"/><circle cx="12" cy="7" r="4"/></svg>`;
|
|
79
|
-
const ELEMENT_TYPES = {
|
|
80
|
-
KEY_CHALLENGE: {
|
|
81
|
-
value: "key_challenge",
|
|
82
|
-
label: "Key Challenge",
|
|
83
|
-
acronym: "CHL"
|
|
84
|
-
},
|
|
85
|
-
SUPPORTING_FACT: {
|
|
86
|
-
value: "supporting_fact",
|
|
87
|
-
label: "Supporting Fact",
|
|
88
|
-
acronym: "FCT"
|
|
89
|
-
},
|
|
90
|
-
IMPACT: {
|
|
91
|
-
value: "impact",
|
|
92
|
-
label: "Impact",
|
|
93
|
-
acronym: "IMP"
|
|
94
|
-
},
|
|
95
|
-
GUIDING_POLICY: {
|
|
96
|
-
value: "guiding_policy",
|
|
97
|
-
label: "Guiding Policy",
|
|
98
|
-
acronym: "POL"
|
|
99
|
-
},
|
|
100
|
-
COMPETITOR_MOVE: {
|
|
101
|
-
value: "competitor_move",
|
|
102
|
-
label: "Competitor Move",
|
|
103
|
-
acronym: "CMV"
|
|
104
|
-
},
|
|
105
|
-
SCOPE: {
|
|
106
|
-
value: "scope",
|
|
107
|
-
label: "Scope",
|
|
108
|
-
acronym: "SCP"
|
|
109
|
-
},
|
|
110
|
-
CONSTRAINT: {
|
|
111
|
-
value: "constraint",
|
|
112
|
-
label: "Constraint",
|
|
113
|
-
acronym: "CST"
|
|
114
|
-
},
|
|
115
|
-
SOLUTION: {
|
|
116
|
-
value: "solution",
|
|
117
|
-
label: "Solution",
|
|
118
|
-
acronym: "SOL"
|
|
119
|
-
},
|
|
120
|
-
ASSUMPTION_HYPOTHESIS: {
|
|
121
|
-
value: "assumption_hypothesis",
|
|
122
|
-
label: "Assumption Hypothesis",
|
|
123
|
-
acronym: "HYP"
|
|
124
|
-
},
|
|
125
|
-
EXPERIMENT: {
|
|
126
|
-
value: "experiment",
|
|
127
|
-
label: "Experiment",
|
|
128
|
-
acronym: "EXP"
|
|
129
|
-
},
|
|
130
|
-
ACTION: {
|
|
131
|
-
value: "action",
|
|
132
|
-
label: "Action",
|
|
133
|
-
acronym: "ACT"
|
|
134
|
-
},
|
|
135
|
-
INVESTMENT: {
|
|
136
|
-
value: "investment",
|
|
137
|
-
label: "Investment",
|
|
138
|
-
acronym: "INV"
|
|
139
|
-
},
|
|
140
|
-
ESTIMATE: {
|
|
141
|
-
value: "estimate",
|
|
142
|
-
label: "Estimate",
|
|
143
|
-
acronym: "EST"
|
|
144
|
-
},
|
|
145
|
-
BASELINE: {
|
|
146
|
-
value: "baseline",
|
|
147
|
-
label: "Baseline",
|
|
148
|
-
acronym: "BSL"
|
|
149
|
-
},
|
|
150
|
-
OUTCOME: {
|
|
151
|
-
value: "outcome",
|
|
152
|
-
label: "Outcome",
|
|
153
|
-
acronym: "OCM"
|
|
154
|
-
},
|
|
155
|
-
PERFORMANCE_VARIABLE: {
|
|
156
|
-
value: "performance_variable",
|
|
157
|
-
label: "Performance Variable",
|
|
158
|
-
acronym: "VAR"
|
|
159
|
-
},
|
|
160
|
-
CAPABILITY: {
|
|
161
|
-
value: "capability",
|
|
162
|
-
label: "Capability",
|
|
163
|
-
acronym: "CAP"
|
|
164
|
-
},
|
|
165
|
-
SYSTEM: {
|
|
166
|
-
value: "system",
|
|
167
|
-
label: "System",
|
|
168
|
-
acronym: "SYS"
|
|
169
|
-
},
|
|
170
|
-
PRINCIPLE: {
|
|
171
|
-
value: "principle",
|
|
172
|
-
label: "Principle",
|
|
173
|
-
acronym: "PRN"
|
|
174
|
-
}
|
|
175
|
-
};
|
|
176
|
-
const COMPONENT_TYPES = {
|
|
177
|
-
DIAGNOSIS: {
|
|
178
|
-
value: "diagnosis",
|
|
179
|
-
label: "Diagnosis",
|
|
180
|
-
acronym: "BD"
|
|
181
|
-
},
|
|
182
|
-
METHOD: {
|
|
183
|
-
value: "method",
|
|
184
|
-
label: "Method & Positioning",
|
|
185
|
-
acronym: "MP"
|
|
186
|
-
},
|
|
187
|
-
INITIATIVES: {
|
|
188
|
-
value: "initiatives",
|
|
189
|
-
label: "Portfolio of Initiatives",
|
|
190
|
-
acronym: "PI"
|
|
191
|
-
},
|
|
192
|
-
MEASURES: {
|
|
193
|
-
value: "measures",
|
|
194
|
-
label: "Measures",
|
|
195
|
-
acronym: "M"
|
|
196
|
-
},
|
|
197
|
-
SUPPORT: {
|
|
198
|
-
value: "support",
|
|
199
|
-
label: "Support Structures",
|
|
200
|
-
acronym: "SS"
|
|
201
|
-
}
|
|
202
|
-
};
|
|
203
|
-
const SKHEMA_MAPPING = {
|
|
204
|
-
elementFlow: {
|
|
205
|
-
[COMPONENT_TYPES.DIAGNOSIS.value]: [
|
|
206
|
-
ELEMENT_TYPES.KEY_CHALLENGE,
|
|
207
|
-
ELEMENT_TYPES.SUPPORTING_FACT,
|
|
208
|
-
ELEMENT_TYPES.IMPACT
|
|
209
|
-
],
|
|
210
|
-
[COMPONENT_TYPES.METHOD.value]: [
|
|
211
|
-
ELEMENT_TYPES.GUIDING_POLICY,
|
|
212
|
-
ELEMENT_TYPES.COMPETITOR_MOVE,
|
|
213
|
-
ELEMENT_TYPES.SCOPE,
|
|
214
|
-
ELEMENT_TYPES.CONSTRAINT
|
|
215
|
-
],
|
|
216
|
-
[COMPONENT_TYPES.INITIATIVES.value]: [
|
|
217
|
-
ELEMENT_TYPES.SOLUTION,
|
|
218
|
-
ELEMENT_TYPES.ASSUMPTION_HYPOTHESIS,
|
|
219
|
-
ELEMENT_TYPES.EXPERIMENT,
|
|
220
|
-
ELEMENT_TYPES.ACTION,
|
|
221
|
-
ELEMENT_TYPES.ESTIMATE,
|
|
222
|
-
ELEMENT_TYPES.INVESTMENT
|
|
223
|
-
],
|
|
224
|
-
[COMPONENT_TYPES.MEASURES.value]: [
|
|
225
|
-
ELEMENT_TYPES.BASELINE,
|
|
226
|
-
ELEMENT_TYPES.OUTCOME,
|
|
227
|
-
ELEMENT_TYPES.PERFORMANCE_VARIABLE
|
|
228
|
-
],
|
|
229
|
-
[COMPONENT_TYPES.SUPPORT.value]: [
|
|
230
|
-
ELEMENT_TYPES.CAPABILITY,
|
|
231
|
-
ELEMENT_TYPES.SYSTEM,
|
|
232
|
-
ELEMENT_TYPES.PRINCIPLE
|
|
233
|
-
]
|
|
234
|
-
}
|
|
235
|
-
};
|
|
236
80
|
function isValidComponentType(componentType) {
|
|
237
81
|
const validTypes = Object.values(COMPONENT_TYPES).map((t) => t.value);
|
|
238
82
|
return validTypes.includes(componentType);
|
|
@@ -307,27 +151,27 @@ function getElementTypeAcronym(elementType) {
|
|
|
307
151
|
return type?.acronym || elementType.substring(0, 2).toUpperCase();
|
|
308
152
|
}
|
|
309
153
|
export {
|
|
310
|
-
|
|
154
|
+
CARD_VARS as C,
|
|
311
155
|
FONT_STACK as F,
|
|
312
156
|
MONO_STACK as M,
|
|
313
157
|
PRIMARY_HEX as P,
|
|
314
158
|
USER_ICON_SVG as U,
|
|
315
|
-
|
|
159
|
+
COMPONENT_COLORS as a,
|
|
316
160
|
getComponentTypeLabel as b,
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
161
|
+
getElementTypeAcronym as c,
|
|
162
|
+
getElementTypeLabel as d,
|
|
163
|
+
getElementTypesForComponent as e,
|
|
164
|
+
isValidElementType as f,
|
|
165
|
+
getComponentTypeAcronym as g,
|
|
166
|
+
CARD_PALETTE as h,
|
|
323
167
|
isValidComponentType as i,
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
168
|
+
CARD_RADIUS as j,
|
|
169
|
+
CARD_SHADOW as k,
|
|
170
|
+
CARD_SHADOW_LG as l,
|
|
171
|
+
PRIMARY_HOVER_HEX as m,
|
|
172
|
+
PRIMARY_PRESSED_HEX as n,
|
|
173
|
+
validateAttributes as o,
|
|
330
174
|
resolveComponentType as r,
|
|
331
175
|
validateElementBelongsToComponent as v
|
|
332
176
|
};
|
|
333
|
-
//# sourceMappingURL=
|
|
177
|
+
//# sourceMappingURL=validation-BcXe-9fn.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"validation-BcXe-9fn.js","sources":["../src/styles/design-tokens.ts","../src/utils/component-validation.ts","../src/utils/validation.ts"],"sourcesContent":["/**\n * Hardcoded color tokens for component-type badge colors.\n * Embeds can't rely on host page CSS vars, so we use inline values.\n * Source: skhema-web/src/app/globals.css oklch tokens converted to HSL-ish for browser compat.\n */\n\nexport const COMPONENT_COLORS = {\n diagnosis: {\n bg: 'oklch(0.65 0.06 25 / 0.15)',\n text: 'oklch(0.65 0.06 25)',\n border: 'oklch(0.65 0.06 25 / 0.3)',\n },\n method: {\n bg: 'oklch(0.6 0.06 250 / 0.15)',\n text: 'oklch(0.6 0.06 250)',\n border: 'oklch(0.6 0.06 250 / 0.3)',\n },\n initiatives: {\n bg: 'oklch(0.6 0.06 155 / 0.15)',\n text: 'oklch(0.6 0.06 155)',\n border: 'oklch(0.6 0.06 155 / 0.3)',\n },\n measures: {\n bg: 'oklch(0.6 0.06 300 / 0.15)',\n text: 'oklch(0.6 0.06 300)',\n border: 'oklch(0.6 0.06 300 / 0.3)',\n },\n support: {\n bg: 'oklch(0.65 0.06 65 / 0.15)',\n text: 'oklch(0.65 0.06 65)',\n border: 'oklch(0.65 0.06 65 / 0.3)',\n },\n} as const\n\nexport type ComponentColorKey = keyof typeof COMPONENT_COLORS\n\n/**\n * Email-safe flat hex palette for the card surface, per theme. These mirror the\n * Tailwind slate values behind the `CARD_VARS` HSL tokens, pre-resolved to hex\n * because email clients ignore `<style>` / CSS vars. The card renderer\n * (`@skhema/embed/render`) inlines these so the live browser embed and email\n * share one colour system (see the \"converge on hex\" decision).\n */\nexport const CARD_PALETTE = {\n light: {\n cardBg: '#ffffff', // hsl(0 0% 100%)\n border: '#e2e8f0', // hsl(214.3 31.8% 91.4%) — slate-200\n text: '#020817', // hsl(222.2 84% 4.9%) — slate-950\n textMuted: '#64748b', // hsl(215.4 16.3% 46.9%) — slate-500\n },\n dark: {\n cardBg: '#020817', // hsl(222.2 84% 4.9%) — slate-950\n border: '#1e293b', // hsl(217.2 32.6% 17.5%) — slate-800\n text: '#f8fafc', // hsl(210 40% 98%) — slate-50\n textMuted: '#94a3b8', // hsl(215 20.2% 65.1%) — slate-400\n },\n} as const\n\nexport type CardTheme = keyof typeof CARD_PALETTE\n\n/** Save-button brand colours (hex) — match the `--skhema-primary*` HSL tokens. */\nexport const PRIMARY_HEX = '#cd476a' // hsl(344 57% 54%)\nexport const PRIMARY_HOVER_HEX = '#b53d5e' // hsl(344 50% 47%)\nexport const PRIMARY_PRESSED_HEX = '#9d3552' // hsl(343 50% 41%)\n\n/** Card geometry / typography shared by the renderer and the browser embed. */\nexport const CARD_RADIUS = '4px' // calc(0.1rem + 2px) ≈ 3.6px\nexport const CARD_SHADOW =\n '0 1px 3px 0 rgba(0,0,0,0.1), 0 1px 2px -1px rgba(0,0,0,0.1)'\nexport const CARD_SHADOW_LG =\n '0 10px 15px -3px rgba(0,0,0,0.1), 0 4px 6px -4px rgba(0,0,0,0.1)'\nexport const FONT_STACK =\n \"-apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Inter', sans-serif\"\nexport const MONO_STACK =\n \"ui-monospace, SFMono-Regular, 'SF Mono', Menlo, Consolas, monospace\"\n\n/**\n * Shared base card CSS variables for light and dark modes.\n */\nexport const CARD_VARS = {\n light: {\n '--skhema-bg': 'hsl(0 0% 100%)',\n '--skhema-card': 'hsl(0 0% 100%)',\n '--skhema-border': 'hsl(214.3 31.8% 91.4%)',\n '--skhema-text': 'hsl(222.2 84% 4.9%)',\n '--skhema-text-muted': 'hsl(215.4 16.3% 46.9%)',\n '--skhema-accent': 'hsl(210 40% 96%)',\n '--skhema-surface-subtle': 'hsl(210 40% 97%)',\n },\n dark: {\n '--skhema-bg': 'hsl(222.2 84% 4.9%)',\n '--skhema-card': 'hsl(222.2 84% 4.9%)',\n '--skhema-border': 'hsl(217.2 32.6% 17.5%)',\n '--skhema-text': 'hsl(210 40% 98%)',\n '--skhema-text-muted': 'hsl(215 20.2% 65.1%)',\n '--skhema-accent': 'hsl(217.2 32.6% 17.5%)',\n '--skhema-surface-subtle': 'hsl(217.2 32.6% 12%)',\n },\n} as const\n\n/**\n * User SVG icon (inline, no external deps).\n */\nexport const USER_ICON_SVG = `<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"M19 21v-2a4 4 0 0 0-4-4H9a4 4 0 0 0-4 4v2\"/><circle cx=\"12\" cy=\"7\" r=\"4\"/></svg>`\n","import type { ComponentValue, ElementValue } from '@skhema/method/vocabulary'\nimport { COMPONENT_TYPES, SKHEMA_MAPPING } from '@skhema/method/vocabulary'\n\n/**\n * Validate that a string is a valid component type.\n */\nexport function isValidComponentType(\n componentType: string\n): componentType is ComponentValue {\n const validTypes = Object.values(COMPONENT_TYPES).map((t) => t.value)\n return validTypes.includes(componentType as ComponentValue)\n}\n\n/**\n * Validate that an element type belongs to the given component type.\n */\nexport function validateElementBelongsToComponent(\n elementType: string,\n componentType: string\n): boolean {\n if (!isValidComponentType(componentType)) return false\n\n const validElements =\n SKHEMA_MAPPING.elementFlow[\n componentType as keyof typeof SKHEMA_MAPPING.elementFlow\n ]\n if (!validElements) return false\n\n return validElements.some((e) => e.value === elementType)\n}\n\n/**\n * Get the human-readable label for a component type.\n */\nexport function getComponentTypeLabel(componentType: string): string {\n const type = Object.values(COMPONENT_TYPES).find(\n (t) => t.value === componentType\n )\n return type?.label || componentType\n}\n\n/**\n * Get the acronym for a component type (e.g., 'BD' for diagnosis).\n */\nexport function getComponentTypeAcronym(componentType: string): string {\n const type = Object.values(COMPONENT_TYPES).find(\n (t) => t.value === componentType\n )\n return type?.acronym || componentType.substring(0, 2).toUpperCase()\n}\n\n/**\n * Resolve which component type an element belongs to.\n * Returns the component value, or 'diagnosis' as fallback.\n */\nexport function resolveComponentType(elementType: string): ComponentValue {\n for (const [componentValue, elements] of Object.entries(\n SKHEMA_MAPPING.elementFlow\n )) {\n if (elements.some((e) => e.value === elementType)) {\n return componentValue as ComponentValue\n }\n }\n return 'diagnosis'\n}\n\n/**\n * Get all valid element types for a given component, ordered per the mapping.\n */\nexport function getElementTypesForComponent(\n componentType: string\n): ElementValue[] {\n if (!isValidComponentType(componentType)) return []\n const elements =\n SKHEMA_MAPPING.elementFlow[\n componentType as keyof typeof SKHEMA_MAPPING.elementFlow\n ]\n return elements?.map((e) => e.value) || []\n}\n","import type { ElementValue } from '@skhema/method/vocabulary'\nimport { ELEMENT_TYPES } from '@skhema/method/vocabulary'\n\nexport function isValidElementType(\n elementType: string\n): elementType is ElementValue {\n const validTypes = Object.values(ELEMENT_TYPES).map((type) => type.value)\n return validTypes.includes(elementType as ElementValue)\n}\n\nexport function validateAttributes(element: HTMLElement): {\n isValid: boolean\n errors: string[]\n elementType?: ElementValue\n contributorId?: string\n} {\n const errors: string[] = []\n\n const elementType = element.getAttribute('element-type')\n const contributorId = element.getAttribute('contributor-id')\n\n if (!elementType) {\n errors.push('Missing required attribute: element-type')\n } else if (!isValidElementType(elementType)) {\n const validTypes = Object.values(ELEMENT_TYPES)\n .map((t) => t.value)\n .join(', ')\n errors.push(\n `Invalid element-type \"${elementType}\". Valid types: ${validTypes}`\n )\n }\n\n if (!contributorId) {\n errors.push('Missing required attribute: contributor-id')\n } else if (contributorId.trim().length === 0) {\n errors.push('contributor-id cannot be empty')\n }\n\n return {\n isValid: errors.length === 0,\n errors,\n elementType: isValidElementType(elementType || '')\n ? (elementType as ElementValue)\n : undefined,\n contributorId: contributorId || undefined,\n }\n}\n\nexport function getElementTypeLabel(elementType: ElementValue): string {\n const type = Object.values(ELEMENT_TYPES).find((t) => t.value === elementType)\n return type?.label || elementType\n}\n\nexport function getElementTypeAcronym(elementType: ElementValue): string {\n const type = Object.values(ELEMENT_TYPES).find((t) => t.value === elementType)\n return type?.acronym || elementType.substring(0, 2).toUpperCase()\n}\n"],"names":[],"mappings":";AAMO,MAAM,mBAAmB;AAAA,EAC9B,WAAW;AAAA,IACT,IAAI;AAAA,IACJ,MAAM;AAAA,IACN,QAAQ;AAAA,EAAA;AAAA,EAEV,QAAQ;AAAA,IACN,IAAI;AAAA,IACJ,MAAM;AAAA,IACN,QAAQ;AAAA,EAAA;AAAA,EAEV,aAAa;AAAA,IACX,IAAI;AAAA,IACJ,MAAM;AAAA,IACN,QAAQ;AAAA,EAAA;AAAA,EAEV,UAAU;AAAA,IACR,IAAI;AAAA,IACJ,MAAM;AAAA,IACN,QAAQ;AAAA,EAAA;AAAA,EAEV,SAAS;AAAA,IACP,IAAI;AAAA,IACJ,MAAM;AAAA,IACN,QAAQ;AAAA,EAAA;AAEZ;AAWO,MAAM,eAAe;AAAA,EAC1B,OAAO;AAAA,IACL,QAAQ;AAAA;AAAA,IACR,QAAQ;AAAA;AAAA,IACR,MAAM;AAAA;AAAA,IACN,WAAW;AAAA;AAAA,EAAA;AAAA,EAEb,MAAM;AAAA,IACJ,QAAQ;AAAA;AAAA,IACR,QAAQ;AAAA;AAAA,IACR,MAAM;AAAA;AAAA,IACN,WAAW;AAAA;AAAA,EAAA;AAEf;AAKO,MAAM,cAAc;AACpB,MAAM,oBAAoB;AAC1B,MAAM,sBAAsB;AAG5B,MAAM,cAAc;AACpB,MAAM,cACX;AACK,MAAM,iBACX;AACK,MAAM,aACX;AACK,MAAM,aACX;AAKK,MAAM,YAAY;AAAA,EACvB,OAAO;AAAA,IACL,eAAe;AAAA,IACf,iBAAiB;AAAA,IACjB,mBAAmB;AAAA,IACnB,iBAAiB;AAAA,IACjB,uBAAuB;AAAA,IACvB,mBAAmB;AAAA,IACnB,2BAA2B;AAAA,EAAA;AAAA,EAE7B,MAAM;AAAA,IACJ,eAAe;AAAA,IACf,iBAAiB;AAAA,IACjB,mBAAmB;AAAA,IACnB,iBAAiB;AAAA,IACjB,uBAAuB;AAAA,IACvB,mBAAmB;AAAA,IACnB,2BAA2B;AAAA,EAAA;AAE/B;AAKO,MAAM,gBAAgB;ACjGtB,SAAS,qBACd,eACiC;AACjC,QAAM,aAAa,OAAO,OAAO,eAAe,EAAE,IAAI,CAAC,MAAM,EAAE,KAAK;AACpE,SAAO,WAAW,SAAS,aAA+B;AAC5D;AAKO,SAAS,kCACd,aACA,eACS;AACT,MAAI,CAAC,qBAAqB,aAAa,EAAG,QAAO;AAEjD,QAAM,gBACJ,eAAe,YACb,aACF;AACF,MAAI,CAAC,cAAe,QAAO;AAE3B,SAAO,cAAc,KAAK,CAAC,MAAM,EAAE,UAAU,WAAW;AAC1D;AAKO,SAAS,sBAAsB,eAA+B;AACnE,QAAM,OAAO,OAAO,OAAO,eAAe,EAAE;AAAA,IAC1C,CAAC,MAAM,EAAE,UAAU;AAAA,EAAA;AAErB,SAAO,MAAM,SAAS;AACxB;AAKO,SAAS,wBAAwB,eAA+B;AACrE,QAAM,OAAO,OAAO,OAAO,eAAe,EAAE;AAAA,IAC1C,CAAC,MAAM,EAAE,UAAU;AAAA,EAAA;AAErB,SAAO,MAAM,WAAW,cAAc,UAAU,GAAG,CAAC,EAAE,YAAA;AACxD;AAMO,SAAS,qBAAqB,aAAqC;AACxE,aAAW,CAAC,gBAAgB,QAAQ,KAAK,OAAO;AAAA,IAC9C,eAAe;AAAA,EAAA,GACd;AACD,QAAI,SAAS,KAAK,CAAC,MAAM,EAAE,UAAU,WAAW,GAAG;AACjD,aAAO;AAAA,IACT;AAAA,EACF;AACA,SAAO;AACT;AAKO,SAAS,4BACd,eACgB;AAChB,MAAI,CAAC,qBAAqB,aAAa,UAAU,CAAA;AACjD,QAAM,WACJ,eAAe,YACb,aACF;AACF,SAAO,UAAU,IAAI,CAAC,MAAM,EAAE,KAAK,KAAK,CAAA;AAC1C;AC3EO,SAAS,mBACd,aAC6B;AAC7B,QAAM,aAAa,OAAO,OAAO,aAAa,EAAE,IAAI,CAAC,SAAS,KAAK,KAAK;AACxE,SAAO,WAAW,SAAS,WAA2B;AACxD;AAEO,SAAS,mBAAmB,SAKjC;AACA,QAAM,SAAmB,CAAA;AAEzB,QAAM,cAAc,QAAQ,aAAa,cAAc;AACvD,QAAM,gBAAgB,QAAQ,aAAa,gBAAgB;AAE3D,MAAI,CAAC,aAAa;AAChB,WAAO,KAAK,0CAA0C;AAAA,EACxD,WAAW,CAAC,mBAAmB,WAAW,GAAG;AAC3C,UAAM,aAAa,OAAO,OAAO,aAAa,EAC3C,IAAI,CAAC,MAAM,EAAE,KAAK,EAClB,KAAK,IAAI;AACZ,WAAO;AAAA,MACL,yBAAyB,WAAW,mBAAmB,UAAU;AAAA,IAAA;AAAA,EAErE;AAEA,MAAI,CAAC,eAAe;AAClB,WAAO,KAAK,4CAA4C;AAAA,EAC1D,WAAW,cAAc,KAAA,EAAO,WAAW,GAAG;AAC5C,WAAO,KAAK,gCAAgC;AAAA,EAC9C;AAEA,SAAO;AAAA,IACL,SAAS,OAAO,WAAW;AAAA,IAC3B;AAAA,IACA,aAAa,mBAAmB,eAAe,EAAE,IAC5C,cACD;AAAA,IACJ,eAAe,iBAAiB;AAAA,EAAA;AAEpC;AAEO,SAAS,oBAAoB,aAAmC;AACrE,QAAM,OAAO,OAAO,OAAO,aAAa,EAAE,KAAK,CAAC,MAAM,EAAE,UAAU,WAAW;AAC7E,SAAO,MAAM,SAAS;AACxB;AAEO,SAAS,sBAAsB,aAAmC;AACvE,QAAM,OAAO,OAAO,OAAO,aAAa,EAAE,KAAK,CAAC,MAAM,EAAE,UAAU,WAAW;AAC7E,SAAO,MAAM,WAAW,YAAY,UAAU,GAAG,CAAC,EAAE,YAAA;AACtD;"}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
"use strict";
|
|
2
|
+
const vocabulary = require("@skhema/method/vocabulary");
|
|
2
3
|
const COMPONENT_COLORS = {
|
|
3
4
|
diagnosis: {
|
|
4
5
|
bg: "oklch(0.65 0.06 25 / 0.15)",
|
|
@@ -77,188 +78,31 @@ const CARD_VARS = {
|
|
|
77
78
|
}
|
|
78
79
|
};
|
|
79
80
|
const USER_ICON_SVG = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M19 21v-2a4 4 0 0 0-4-4H9a4 4 0 0 0-4 4v2"/><circle cx="12" cy="7" r="4"/></svg>`;
|
|
80
|
-
const ELEMENT_TYPES = {
|
|
81
|
-
KEY_CHALLENGE: {
|
|
82
|
-
value: "key_challenge",
|
|
83
|
-
label: "Key Challenge",
|
|
84
|
-
acronym: "CHL"
|
|
85
|
-
},
|
|
86
|
-
SUPPORTING_FACT: {
|
|
87
|
-
value: "supporting_fact",
|
|
88
|
-
label: "Supporting Fact",
|
|
89
|
-
acronym: "FCT"
|
|
90
|
-
},
|
|
91
|
-
IMPACT: {
|
|
92
|
-
value: "impact",
|
|
93
|
-
label: "Impact",
|
|
94
|
-
acronym: "IMP"
|
|
95
|
-
},
|
|
96
|
-
GUIDING_POLICY: {
|
|
97
|
-
value: "guiding_policy",
|
|
98
|
-
label: "Guiding Policy",
|
|
99
|
-
acronym: "POL"
|
|
100
|
-
},
|
|
101
|
-
COMPETITOR_MOVE: {
|
|
102
|
-
value: "competitor_move",
|
|
103
|
-
label: "Competitor Move",
|
|
104
|
-
acronym: "CMV"
|
|
105
|
-
},
|
|
106
|
-
SCOPE: {
|
|
107
|
-
value: "scope",
|
|
108
|
-
label: "Scope",
|
|
109
|
-
acronym: "SCP"
|
|
110
|
-
},
|
|
111
|
-
CONSTRAINT: {
|
|
112
|
-
value: "constraint",
|
|
113
|
-
label: "Constraint",
|
|
114
|
-
acronym: "CST"
|
|
115
|
-
},
|
|
116
|
-
SOLUTION: {
|
|
117
|
-
value: "solution",
|
|
118
|
-
label: "Solution",
|
|
119
|
-
acronym: "SOL"
|
|
120
|
-
},
|
|
121
|
-
ASSUMPTION_HYPOTHESIS: {
|
|
122
|
-
value: "assumption_hypothesis",
|
|
123
|
-
label: "Assumption Hypothesis",
|
|
124
|
-
acronym: "HYP"
|
|
125
|
-
},
|
|
126
|
-
EXPERIMENT: {
|
|
127
|
-
value: "experiment",
|
|
128
|
-
label: "Experiment",
|
|
129
|
-
acronym: "EXP"
|
|
130
|
-
},
|
|
131
|
-
ACTION: {
|
|
132
|
-
value: "action",
|
|
133
|
-
label: "Action",
|
|
134
|
-
acronym: "ACT"
|
|
135
|
-
},
|
|
136
|
-
INVESTMENT: {
|
|
137
|
-
value: "investment",
|
|
138
|
-
label: "Investment",
|
|
139
|
-
acronym: "INV"
|
|
140
|
-
},
|
|
141
|
-
ESTIMATE: {
|
|
142
|
-
value: "estimate",
|
|
143
|
-
label: "Estimate",
|
|
144
|
-
acronym: "EST"
|
|
145
|
-
},
|
|
146
|
-
BASELINE: {
|
|
147
|
-
value: "baseline",
|
|
148
|
-
label: "Baseline",
|
|
149
|
-
acronym: "BSL"
|
|
150
|
-
},
|
|
151
|
-
OUTCOME: {
|
|
152
|
-
value: "outcome",
|
|
153
|
-
label: "Outcome",
|
|
154
|
-
acronym: "OCM"
|
|
155
|
-
},
|
|
156
|
-
PERFORMANCE_VARIABLE: {
|
|
157
|
-
value: "performance_variable",
|
|
158
|
-
label: "Performance Variable",
|
|
159
|
-
acronym: "VAR"
|
|
160
|
-
},
|
|
161
|
-
CAPABILITY: {
|
|
162
|
-
value: "capability",
|
|
163
|
-
label: "Capability",
|
|
164
|
-
acronym: "CAP"
|
|
165
|
-
},
|
|
166
|
-
SYSTEM: {
|
|
167
|
-
value: "system",
|
|
168
|
-
label: "System",
|
|
169
|
-
acronym: "SYS"
|
|
170
|
-
},
|
|
171
|
-
PRINCIPLE: {
|
|
172
|
-
value: "principle",
|
|
173
|
-
label: "Principle",
|
|
174
|
-
acronym: "PRN"
|
|
175
|
-
}
|
|
176
|
-
};
|
|
177
|
-
const COMPONENT_TYPES = {
|
|
178
|
-
DIAGNOSIS: {
|
|
179
|
-
value: "diagnosis",
|
|
180
|
-
label: "Diagnosis",
|
|
181
|
-
acronym: "BD"
|
|
182
|
-
},
|
|
183
|
-
METHOD: {
|
|
184
|
-
value: "method",
|
|
185
|
-
label: "Method & Positioning",
|
|
186
|
-
acronym: "MP"
|
|
187
|
-
},
|
|
188
|
-
INITIATIVES: {
|
|
189
|
-
value: "initiatives",
|
|
190
|
-
label: "Portfolio of Initiatives",
|
|
191
|
-
acronym: "PI"
|
|
192
|
-
},
|
|
193
|
-
MEASURES: {
|
|
194
|
-
value: "measures",
|
|
195
|
-
label: "Measures",
|
|
196
|
-
acronym: "M"
|
|
197
|
-
},
|
|
198
|
-
SUPPORT: {
|
|
199
|
-
value: "support",
|
|
200
|
-
label: "Support Structures",
|
|
201
|
-
acronym: "SS"
|
|
202
|
-
}
|
|
203
|
-
};
|
|
204
|
-
const SKHEMA_MAPPING = {
|
|
205
|
-
elementFlow: {
|
|
206
|
-
[COMPONENT_TYPES.DIAGNOSIS.value]: [
|
|
207
|
-
ELEMENT_TYPES.KEY_CHALLENGE,
|
|
208
|
-
ELEMENT_TYPES.SUPPORTING_FACT,
|
|
209
|
-
ELEMENT_TYPES.IMPACT
|
|
210
|
-
],
|
|
211
|
-
[COMPONENT_TYPES.METHOD.value]: [
|
|
212
|
-
ELEMENT_TYPES.GUIDING_POLICY,
|
|
213
|
-
ELEMENT_TYPES.COMPETITOR_MOVE,
|
|
214
|
-
ELEMENT_TYPES.SCOPE,
|
|
215
|
-
ELEMENT_TYPES.CONSTRAINT
|
|
216
|
-
],
|
|
217
|
-
[COMPONENT_TYPES.INITIATIVES.value]: [
|
|
218
|
-
ELEMENT_TYPES.SOLUTION,
|
|
219
|
-
ELEMENT_TYPES.ASSUMPTION_HYPOTHESIS,
|
|
220
|
-
ELEMENT_TYPES.EXPERIMENT,
|
|
221
|
-
ELEMENT_TYPES.ACTION,
|
|
222
|
-
ELEMENT_TYPES.ESTIMATE,
|
|
223
|
-
ELEMENT_TYPES.INVESTMENT
|
|
224
|
-
],
|
|
225
|
-
[COMPONENT_TYPES.MEASURES.value]: [
|
|
226
|
-
ELEMENT_TYPES.BASELINE,
|
|
227
|
-
ELEMENT_TYPES.OUTCOME,
|
|
228
|
-
ELEMENT_TYPES.PERFORMANCE_VARIABLE
|
|
229
|
-
],
|
|
230
|
-
[COMPONENT_TYPES.SUPPORT.value]: [
|
|
231
|
-
ELEMENT_TYPES.CAPABILITY,
|
|
232
|
-
ELEMENT_TYPES.SYSTEM,
|
|
233
|
-
ELEMENT_TYPES.PRINCIPLE
|
|
234
|
-
]
|
|
235
|
-
}
|
|
236
|
-
};
|
|
237
81
|
function isValidComponentType(componentType) {
|
|
238
|
-
const validTypes = Object.values(COMPONENT_TYPES).map((t) => t.value);
|
|
82
|
+
const validTypes = Object.values(vocabulary.COMPONENT_TYPES).map((t) => t.value);
|
|
239
83
|
return validTypes.includes(componentType);
|
|
240
84
|
}
|
|
241
85
|
function validateElementBelongsToComponent(elementType, componentType) {
|
|
242
86
|
if (!isValidComponentType(componentType)) return false;
|
|
243
|
-
const validElements = SKHEMA_MAPPING.elementFlow[componentType];
|
|
87
|
+
const validElements = vocabulary.SKHEMA_MAPPING.elementFlow[componentType];
|
|
244
88
|
if (!validElements) return false;
|
|
245
89
|
return validElements.some((e) => e.value === elementType);
|
|
246
90
|
}
|
|
247
91
|
function getComponentTypeLabel(componentType) {
|
|
248
|
-
const type = Object.values(COMPONENT_TYPES).find(
|
|
92
|
+
const type = Object.values(vocabulary.COMPONENT_TYPES).find(
|
|
249
93
|
(t) => t.value === componentType
|
|
250
94
|
);
|
|
251
95
|
return type?.label || componentType;
|
|
252
96
|
}
|
|
253
97
|
function getComponentTypeAcronym(componentType) {
|
|
254
|
-
const type = Object.values(COMPONENT_TYPES).find(
|
|
98
|
+
const type = Object.values(vocabulary.COMPONENT_TYPES).find(
|
|
255
99
|
(t) => t.value === componentType
|
|
256
100
|
);
|
|
257
101
|
return type?.acronym || componentType.substring(0, 2).toUpperCase();
|
|
258
102
|
}
|
|
259
103
|
function resolveComponentType(elementType) {
|
|
260
104
|
for (const [componentValue, elements] of Object.entries(
|
|
261
|
-
SKHEMA_MAPPING.elementFlow
|
|
105
|
+
vocabulary.SKHEMA_MAPPING.elementFlow
|
|
262
106
|
)) {
|
|
263
107
|
if (elements.some((e) => e.value === elementType)) {
|
|
264
108
|
return componentValue;
|
|
@@ -268,11 +112,11 @@ function resolveComponentType(elementType) {
|
|
|
268
112
|
}
|
|
269
113
|
function getElementTypesForComponent(componentType) {
|
|
270
114
|
if (!isValidComponentType(componentType)) return [];
|
|
271
|
-
const elements = SKHEMA_MAPPING.elementFlow[componentType];
|
|
115
|
+
const elements = vocabulary.SKHEMA_MAPPING.elementFlow[componentType];
|
|
272
116
|
return elements?.map((e) => e.value) || [];
|
|
273
117
|
}
|
|
274
118
|
function isValidElementType(elementType) {
|
|
275
|
-
const validTypes = Object.values(ELEMENT_TYPES).map((type) => type.value);
|
|
119
|
+
const validTypes = Object.values(vocabulary.ELEMENT_TYPES).map((type) => type.value);
|
|
276
120
|
return validTypes.includes(elementType);
|
|
277
121
|
}
|
|
278
122
|
function validateAttributes(element) {
|
|
@@ -282,7 +126,7 @@ function validateAttributes(element) {
|
|
|
282
126
|
if (!elementType) {
|
|
283
127
|
errors.push("Missing required attribute: element-type");
|
|
284
128
|
} else if (!isValidElementType(elementType)) {
|
|
285
|
-
const validTypes = Object.values(ELEMENT_TYPES).map((t) => t.value).join(", ");
|
|
129
|
+
const validTypes = Object.values(vocabulary.ELEMENT_TYPES).map((t) => t.value).join(", ");
|
|
286
130
|
errors.push(
|
|
287
131
|
`Invalid element-type "${elementType}". Valid types: ${validTypes}`
|
|
288
132
|
);
|
|
@@ -300,11 +144,11 @@ function validateAttributes(element) {
|
|
|
300
144
|
};
|
|
301
145
|
}
|
|
302
146
|
function getElementTypeLabel(elementType) {
|
|
303
|
-
const type = Object.values(ELEMENT_TYPES).find((t) => t.value === elementType);
|
|
147
|
+
const type = Object.values(vocabulary.ELEMENT_TYPES).find((t) => t.value === elementType);
|
|
304
148
|
return type?.label || elementType;
|
|
305
149
|
}
|
|
306
150
|
function getElementTypeAcronym(elementType) {
|
|
307
|
-
const type = Object.values(ELEMENT_TYPES).find((t) => t.value === elementType);
|
|
151
|
+
const type = Object.values(vocabulary.ELEMENT_TYPES).find((t) => t.value === elementType);
|
|
308
152
|
return type?.acronym || elementType.substring(0, 2).toUpperCase();
|
|
309
153
|
}
|
|
310
154
|
exports.CARD_PALETTE = CARD_PALETTE;
|
|
@@ -329,4 +173,4 @@ exports.isValidElementType = isValidElementType;
|
|
|
329
173
|
exports.resolveComponentType = resolveComponentType;
|
|
330
174
|
exports.validateAttributes = validateAttributes;
|
|
331
175
|
exports.validateElementBelongsToComponent = validateElementBelongsToComponent;
|
|
332
|
-
//# sourceMappingURL=
|
|
176
|
+
//# sourceMappingURL=validation-IgKmvB17.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"validation-IgKmvB17.cjs","sources":["../src/styles/design-tokens.ts","../src/utils/component-validation.ts","../src/utils/validation.ts"],"sourcesContent":["/**\n * Hardcoded color tokens for component-type badge colors.\n * Embeds can't rely on host page CSS vars, so we use inline values.\n * Source: skhema-web/src/app/globals.css oklch tokens converted to HSL-ish for browser compat.\n */\n\nexport const COMPONENT_COLORS = {\n diagnosis: {\n bg: 'oklch(0.65 0.06 25 / 0.15)',\n text: 'oklch(0.65 0.06 25)',\n border: 'oklch(0.65 0.06 25 / 0.3)',\n },\n method: {\n bg: 'oklch(0.6 0.06 250 / 0.15)',\n text: 'oklch(0.6 0.06 250)',\n border: 'oklch(0.6 0.06 250 / 0.3)',\n },\n initiatives: {\n bg: 'oklch(0.6 0.06 155 / 0.15)',\n text: 'oklch(0.6 0.06 155)',\n border: 'oklch(0.6 0.06 155 / 0.3)',\n },\n measures: {\n bg: 'oklch(0.6 0.06 300 / 0.15)',\n text: 'oklch(0.6 0.06 300)',\n border: 'oklch(0.6 0.06 300 / 0.3)',\n },\n support: {\n bg: 'oklch(0.65 0.06 65 / 0.15)',\n text: 'oklch(0.65 0.06 65)',\n border: 'oklch(0.65 0.06 65 / 0.3)',\n },\n} as const\n\nexport type ComponentColorKey = keyof typeof COMPONENT_COLORS\n\n/**\n * Email-safe flat hex palette for the card surface, per theme. These mirror the\n * Tailwind slate values behind the `CARD_VARS` HSL tokens, pre-resolved to hex\n * because email clients ignore `<style>` / CSS vars. The card renderer\n * (`@skhema/embed/render`) inlines these so the live browser embed and email\n * share one colour system (see the \"converge on hex\" decision).\n */\nexport const CARD_PALETTE = {\n light: {\n cardBg: '#ffffff', // hsl(0 0% 100%)\n border: '#e2e8f0', // hsl(214.3 31.8% 91.4%) — slate-200\n text: '#020817', // hsl(222.2 84% 4.9%) — slate-950\n textMuted: '#64748b', // hsl(215.4 16.3% 46.9%) — slate-500\n },\n dark: {\n cardBg: '#020817', // hsl(222.2 84% 4.9%) — slate-950\n border: '#1e293b', // hsl(217.2 32.6% 17.5%) — slate-800\n text: '#f8fafc', // hsl(210 40% 98%) — slate-50\n textMuted: '#94a3b8', // hsl(215 20.2% 65.1%) — slate-400\n },\n} as const\n\nexport type CardTheme = keyof typeof CARD_PALETTE\n\n/** Save-button brand colours (hex) — match the `--skhema-primary*` HSL tokens. */\nexport const PRIMARY_HEX = '#cd476a' // hsl(344 57% 54%)\nexport const PRIMARY_HOVER_HEX = '#b53d5e' // hsl(344 50% 47%)\nexport const PRIMARY_PRESSED_HEX = '#9d3552' // hsl(343 50% 41%)\n\n/** Card geometry / typography shared by the renderer and the browser embed. */\nexport const CARD_RADIUS = '4px' // calc(0.1rem + 2px) ≈ 3.6px\nexport const CARD_SHADOW =\n '0 1px 3px 0 rgba(0,0,0,0.1), 0 1px 2px -1px rgba(0,0,0,0.1)'\nexport const CARD_SHADOW_LG =\n '0 10px 15px -3px rgba(0,0,0,0.1), 0 4px 6px -4px rgba(0,0,0,0.1)'\nexport const FONT_STACK =\n \"-apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Inter', sans-serif\"\nexport const MONO_STACK =\n \"ui-monospace, SFMono-Regular, 'SF Mono', Menlo, Consolas, monospace\"\n\n/**\n * Shared base card CSS variables for light and dark modes.\n */\nexport const CARD_VARS = {\n light: {\n '--skhema-bg': 'hsl(0 0% 100%)',\n '--skhema-card': 'hsl(0 0% 100%)',\n '--skhema-border': 'hsl(214.3 31.8% 91.4%)',\n '--skhema-text': 'hsl(222.2 84% 4.9%)',\n '--skhema-text-muted': 'hsl(215.4 16.3% 46.9%)',\n '--skhema-accent': 'hsl(210 40% 96%)',\n '--skhema-surface-subtle': 'hsl(210 40% 97%)',\n },\n dark: {\n '--skhema-bg': 'hsl(222.2 84% 4.9%)',\n '--skhema-card': 'hsl(222.2 84% 4.9%)',\n '--skhema-border': 'hsl(217.2 32.6% 17.5%)',\n '--skhema-text': 'hsl(210 40% 98%)',\n '--skhema-text-muted': 'hsl(215 20.2% 65.1%)',\n '--skhema-accent': 'hsl(217.2 32.6% 17.5%)',\n '--skhema-surface-subtle': 'hsl(217.2 32.6% 12%)',\n },\n} as const\n\n/**\n * User SVG icon (inline, no external deps).\n */\nexport const USER_ICON_SVG = `<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"M19 21v-2a4 4 0 0 0-4-4H9a4 4 0 0 0-4 4v2\"/><circle cx=\"12\" cy=\"7\" r=\"4\"/></svg>`\n","import type { ComponentValue, ElementValue } from '@skhema/method/vocabulary'\nimport { COMPONENT_TYPES, SKHEMA_MAPPING } from '@skhema/method/vocabulary'\n\n/**\n * Validate that a string is a valid component type.\n */\nexport function isValidComponentType(\n componentType: string\n): componentType is ComponentValue {\n const validTypes = Object.values(COMPONENT_TYPES).map((t) => t.value)\n return validTypes.includes(componentType as ComponentValue)\n}\n\n/**\n * Validate that an element type belongs to the given component type.\n */\nexport function validateElementBelongsToComponent(\n elementType: string,\n componentType: string\n): boolean {\n if (!isValidComponentType(componentType)) return false\n\n const validElements =\n SKHEMA_MAPPING.elementFlow[\n componentType as keyof typeof SKHEMA_MAPPING.elementFlow\n ]\n if (!validElements) return false\n\n return validElements.some((e) => e.value === elementType)\n}\n\n/**\n * Get the human-readable label for a component type.\n */\nexport function getComponentTypeLabel(componentType: string): string {\n const type = Object.values(COMPONENT_TYPES).find(\n (t) => t.value === componentType\n )\n return type?.label || componentType\n}\n\n/**\n * Get the acronym for a component type (e.g., 'BD' for diagnosis).\n */\nexport function getComponentTypeAcronym(componentType: string): string {\n const type = Object.values(COMPONENT_TYPES).find(\n (t) => t.value === componentType\n )\n return type?.acronym || componentType.substring(0, 2).toUpperCase()\n}\n\n/**\n * Resolve which component type an element belongs to.\n * Returns the component value, or 'diagnosis' as fallback.\n */\nexport function resolveComponentType(elementType: string): ComponentValue {\n for (const [componentValue, elements] of Object.entries(\n SKHEMA_MAPPING.elementFlow\n )) {\n if (elements.some((e) => e.value === elementType)) {\n return componentValue as ComponentValue\n }\n }\n return 'diagnosis'\n}\n\n/**\n * Get all valid element types for a given component, ordered per the mapping.\n */\nexport function getElementTypesForComponent(\n componentType: string\n): ElementValue[] {\n if (!isValidComponentType(componentType)) return []\n const elements =\n SKHEMA_MAPPING.elementFlow[\n componentType as keyof typeof SKHEMA_MAPPING.elementFlow\n ]\n return elements?.map((e) => e.value) || []\n}\n","import type { ElementValue } from '@skhema/method/vocabulary'\nimport { ELEMENT_TYPES } from '@skhema/method/vocabulary'\n\nexport function isValidElementType(\n elementType: string\n): elementType is ElementValue {\n const validTypes = Object.values(ELEMENT_TYPES).map((type) => type.value)\n return validTypes.includes(elementType as ElementValue)\n}\n\nexport function validateAttributes(element: HTMLElement): {\n isValid: boolean\n errors: string[]\n elementType?: ElementValue\n contributorId?: string\n} {\n const errors: string[] = []\n\n const elementType = element.getAttribute('element-type')\n const contributorId = element.getAttribute('contributor-id')\n\n if (!elementType) {\n errors.push('Missing required attribute: element-type')\n } else if (!isValidElementType(elementType)) {\n const validTypes = Object.values(ELEMENT_TYPES)\n .map((t) => t.value)\n .join(', ')\n errors.push(\n `Invalid element-type \"${elementType}\". Valid types: ${validTypes}`\n )\n }\n\n if (!contributorId) {\n errors.push('Missing required attribute: contributor-id')\n } else if (contributorId.trim().length === 0) {\n errors.push('contributor-id cannot be empty')\n }\n\n return {\n isValid: errors.length === 0,\n errors,\n elementType: isValidElementType(elementType || '')\n ? (elementType as ElementValue)\n : undefined,\n contributorId: contributorId || undefined,\n }\n}\n\nexport function getElementTypeLabel(elementType: ElementValue): string {\n const type = Object.values(ELEMENT_TYPES).find((t) => t.value === elementType)\n return type?.label || elementType\n}\n\nexport function getElementTypeAcronym(elementType: ElementValue): string {\n const type = Object.values(ELEMENT_TYPES).find((t) => t.value === elementType)\n return type?.acronym || elementType.substring(0, 2).toUpperCase()\n}\n"],"names":["COMPONENT_TYPES","SKHEMA_MAPPING","ELEMENT_TYPES"],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@skhema/embed",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.12",
|
|
4
4
|
"description": "Embeddable component for integrating strategic elements with Skhema.",
|
|
5
5
|
"main": "dist/index.cjs",
|
|
6
6
|
"module": "dist/index.es.js",
|
|
@@ -69,6 +69,9 @@
|
|
|
69
69
|
],
|
|
70
70
|
"author": "Skhema Team",
|
|
71
71
|
"license": "MIT",
|
|
72
|
+
"dependencies": {
|
|
73
|
+
"@skhema/method": "0.3.0"
|
|
74
|
+
},
|
|
72
75
|
"devDependencies": {
|
|
73
76
|
"@skhema/linter": "2.2.0",
|
|
74
77
|
"@skhema/prettier-config": "1.0.0",
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"index-B8ZF3snD.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, '&')\n .replace(/</g, '<')\n .replace(/>/g, '>')\n .replace(/\"/g, '"')\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 '../method-schema.js'\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, '&')\n .replace(/</g, '<')\n .replace(/>/g, '>')\n .replace(/\"/g, '"')\n}\n\nfunction escapeAttr(text: string): string {\n return escapeHtml(text).replace(/'/g, ''')\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 ' '\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 →</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};\"> — </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};\"> </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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"}
|