@skhema/embed 0.1.8 → 0.1.10

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.
Files changed (40) hide show
  1. package/README.md +55 -0
  2. package/dist/components/SkhemaComponent.d.ts +0 -1
  3. package/dist/components/SkhemaComponent.d.ts.map +1 -1
  4. package/dist/components/SkhemaElement.d.ts.map +1 -1
  5. package/dist/embed.min.js +1 -1
  6. package/dist/embed.min.js.map +1 -1
  7. package/dist/index-DBeyneZT.cjs +215 -0
  8. package/dist/index-DBeyneZT.cjs.map +1 -0
  9. package/dist/index-jwx5CXLB.js +216 -0
  10. package/dist/index-jwx5CXLB.js.map +1 -0
  11. package/dist/index.cjs +113 -668
  12. package/dist/index.cjs.map +1 -1
  13. package/dist/index.es.js +83 -637
  14. package/dist/index.es.js.map +1 -1
  15. package/dist/render/index.d.ts +55 -0
  16. package/dist/render/index.d.ts.map +1 -0
  17. package/dist/render.cjs +7 -0
  18. package/dist/render.cjs.map +1 -0
  19. package/dist/render.d.ts +2 -0
  20. package/dist/render.es.js +7 -0
  21. package/dist/render.es.js.map +1 -0
  22. package/dist/styles/design-tokens.d.ts +32 -4
  23. package/dist/styles/design-tokens.d.ts.map +1 -1
  24. package/dist/tokens.cjs +16 -0
  25. package/dist/tokens.cjs.map +1 -0
  26. package/dist/tokens.d.ts +17 -0
  27. package/dist/tokens.d.ts.map +1 -0
  28. package/dist/tokens.es.js +16 -0
  29. package/dist/tokens.es.js.map +1 -0
  30. package/dist/utils/color.d.ts +25 -0
  31. package/dist/utils/color.d.ts.map +1 -0
  32. package/dist/utils/sanitization.d.ts +7 -1
  33. package/dist/utils/sanitization.d.ts.map +1 -1
  34. package/dist/validation-DCw9rQeH.cjs +176 -0
  35. package/dist/validation-DCw9rQeH.cjs.map +1 -0
  36. package/dist/validation-Dt4L_df8.js +177 -0
  37. package/dist/validation-Dt4L_df8.js.map +1 -0
  38. package/package.json +20 -7
  39. package/dist/utils/author-attribution.d.ts +0 -16
  40. package/dist/utils/author-attribution.d.ts.map +0 -1
package/dist/index.cjs CHANGED
@@ -1,161 +1,7 @@
1
1
  "use strict";
2
2
  Object.defineProperties(exports, { __esModule: { value: true }, [Symbol.toStringTag]: { value: "Module" } });
3
- const types = require("@skhema/types");
4
- const COMPONENT_COLORS = {
5
- diagnosis: {
6
- bg: "oklch(0.65 0.06 25 / 0.15)",
7
- text: "oklch(0.65 0.06 25)",
8
- border: "oklch(0.65 0.06 25 / 0.3)"
9
- },
10
- method: {
11
- bg: "oklch(0.6 0.06 250 / 0.15)",
12
- text: "oklch(0.6 0.06 250)",
13
- border: "oklch(0.6 0.06 250 / 0.3)"
14
- },
15
- initiatives: {
16
- bg: "oklch(0.6 0.06 155 / 0.15)",
17
- text: "oklch(0.6 0.06 155)",
18
- border: "oklch(0.6 0.06 155 / 0.3)"
19
- },
20
- measures: {
21
- bg: "oklch(0.6 0.06 300 / 0.15)",
22
- text: "oklch(0.6 0.06 300)",
23
- border: "oklch(0.6 0.06 300 / 0.3)"
24
- },
25
- support: {
26
- bg: "oklch(0.65 0.06 65 / 0.15)",
27
- text: "oklch(0.65 0.06 65)",
28
- border: "oklch(0.65 0.06 65 / 0.3)"
29
- }
30
- };
31
- const SHARED_CARD_STYLES = `
32
- /* Monospace acronym badge */
33
- .skhema-acronym-badge {
34
- display: inline-flex;
35
- align-items: center;
36
- font-family: ui-monospace, SFMono-Regular, 'SF Mono', Menlo, Consolas, monospace;
37
- font-size: 10px;
38
- font-weight: 600;
39
- padding: 2px 6px;
40
- border-radius: 2px;
41
- letter-spacing: 0.02em;
42
- flex-shrink: 0;
43
- }
44
-
45
- /* Footer attribution tagline */
46
- .skhema-footer-attribution {
47
- font-size: 11px;
48
- color: var(--skhema-text-muted);
49
- line-height: 1.4;
50
- }
51
-
52
- .skhema-footer-attribution a {
53
- color: var(--skhema-text-muted);
54
- text-decoration: underline;
55
- text-decoration-color: var(--skhema-border);
56
- text-underline-offset: 2px;
57
- }
58
-
59
- .skhema-footer-attribution a:hover {
60
- color: var(--skhema-text);
61
- }
62
-
63
- /* Contributor line in footer */
64
- .skhema-contributor-line {
65
- display: flex;
66
- align-items: center;
67
- gap: 6px;
68
- font-size: 12px;
69
- color: var(--skhema-text-muted);
70
- }
71
-
72
- .skhema-contributor-line svg {
73
- width: 14px;
74
- height: 14px;
75
- flex-shrink: 0;
76
- }
77
-
78
- .skhema-author-link {
79
- color: inherit;
80
- text-decoration: none;
81
- }
82
-
83
- .skhema-author-link:hover {
84
- color: var(--skhema-text);
85
- text-decoration: underline;
86
- }
87
-
88
- /* Save button — primary brand color */
89
- .skhema-save-btn {
90
- display: inline-flex;
91
- align-items: center;
92
- gap: 6px;
93
- background: var(--skhema-primary);
94
- color: white;
95
- border: none;
96
- padding: 6px 14px;
97
- border-radius: calc(var(--skhema-radius) + 2px);
98
- font-size: 12px;
99
- font-weight: 500;
100
- text-decoration: none;
101
- cursor: pointer;
102
- transition: background 0.15s ease;
103
- white-space: nowrap;
104
- }
105
-
106
- .skhema-save-btn:hover {
107
- background: var(--skhema-primary-hover);
108
- }
109
-
110
- .skhema-save-btn:active {
111
- background: var(--skhema-primary-pressed);
112
- }
113
-
114
- .skhema-save-btn:focus {
115
- outline: 2px solid var(--skhema-primary);
116
- outline-offset: 2px;
117
- }
118
-
119
- .skhema-save-btn::after {
120
- content: '\\2192';
121
- font-size: 13px;
122
- }
123
- `;
124
- 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>`;
125
- function readAuthorAttribution(element) {
126
- const authorName = element.getAttribute("author-name") || element.getAttribute("contributor-name");
127
- if (!authorName?.trim()) {
128
- return null;
129
- }
130
- const authorSlug = element.getAttribute("author-slug")?.trim();
131
- return {
132
- authorName: authorName.trim(),
133
- authorSlug: authorSlug || void 0
134
- };
135
- }
136
- function escapeHtml(text) {
137
- return text.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
138
- }
139
- function formatAuthorAttributionHtml(authorName, authorSlug) {
140
- const label = `By ${escapeHtml(authorName)}`;
141
- if (authorSlug) {
142
- return `<a href="https://skhema.com/contributors/${encodeURIComponent(authorSlug)}" class="skhema-author-link" target="_blank" rel="noopener noreferrer">${label}</a>`;
143
- }
144
- return `<span class="skhema-author-text">${label}</span>`;
145
- }
146
- function formatContributorIdFallback(contributorId) {
147
- return contributorId.split(/[_-]/).map((word) => word.charAt(0).toUpperCase() + word.slice(1).toLowerCase()).join(" ");
148
- }
149
- function resolveAuthorFooterHtml(element, contributorId) {
150
- const attribution = readAuthorAttribution(element);
151
- if (attribution) {
152
- return formatAuthorAttributionHtml(
153
- attribution.authorName,
154
- attribution.authorSlug
155
- );
156
- }
157
- return `<span class="skhema-author-text">By ${escapeHtml(formatContributorIdFallback(contributorId))}</span>`;
158
- }
3
+ const render = require("./index-DBeyneZT.cjs");
4
+ const validation = require("./validation-DCw9rQeH.cjs");
159
5
  const ANALYTICS_ENDPOINT = "https://analytics.skhema.com/functions/v1/embed-manage";
160
6
  const TRACKING_COOKIE_NAME = "_sk";
161
7
  const TRACKING_EXPIRY_HOURS = 24;
@@ -529,43 +375,6 @@ function shouldTrackAnalytics(element) {
529
375
  const trackAnalytics = element.getAttribute("track-analytics");
530
376
  return trackAnalytics !== "false";
531
377
  }
532
- function isValidComponentType(componentType) {
533
- const validTypes = Object.values(types.COMPONENT_TYPES).map((t) => t.value);
534
- return validTypes.includes(componentType);
535
- }
536
- function validateElementBelongsToComponent(elementType, componentType) {
537
- if (!isValidComponentType(componentType)) return false;
538
- const validElements = types.SKHEMA_MAPPING.elementFlow[componentType];
539
- if (!validElements) return false;
540
- return validElements.some((e) => e.value === elementType);
541
- }
542
- function getComponentTypeLabel(componentType) {
543
- const type = Object.values(types.COMPONENT_TYPES).find(
544
- (t) => t.value === componentType
545
- );
546
- return type?.label || componentType;
547
- }
548
- function getComponentTypeAcronym(componentType) {
549
- const type = Object.values(types.COMPONENT_TYPES).find(
550
- (t) => t.value === componentType
551
- );
552
- return type?.acronym || componentType.substring(0, 2).toUpperCase();
553
- }
554
- function resolveComponentType(elementType) {
555
- for (const [componentValue, elements] of Object.entries(
556
- types.SKHEMA_MAPPING.elementFlow
557
- )) {
558
- if (elements.some((e) => e.value === elementType)) {
559
- return componentValue;
560
- }
561
- }
562
- return "diagnosis";
563
- }
564
- function getElementTypesForComponent(componentType) {
565
- if (!isValidComponentType(componentType)) return [];
566
- const elements = types.SKHEMA_MAPPING.elementFlow[componentType];
567
- return elements?.map((e) => e.value) || [];
568
- }
569
378
  function generateContentHash(content) {
570
379
  let hash = 0;
571
380
  const cleanContent = content.trim().substring(0, 200);
@@ -585,118 +394,13 @@ function generateComponentHash(elements) {
585
394
  const combined = sorted.map((e) => `${e.elementType}:${e.content}`).join("|");
586
395
  return generateContentHash(combined);
587
396
  }
588
- function sanitizeContent(content) {
589
- const htmlEncode = (str) => {
590
- const div = document.createElement("div");
591
- div.textContent = str;
592
- return div.innerHTML;
593
- };
594
- let sanitized = stripUrls(content);
595
- sanitized = htmlEncode(sanitized);
596
- sanitized = sanitized.replace(/\n/g, "<br>");
597
- sanitized = applyTextWrapping(sanitized);
598
- return sanitized;
599
- }
600
- function stripUrls(text) {
601
- const patterns = [
602
- // Standard URLs with protocols
603
- /https?:\/\/[^\s<>"{}|\\^`[\]]+/gi,
604
- // FTP URLs
605
- /ftp:\/\/[^\s<>"{}|\\^`[\]]+/gi,
606
- // URLs without protocol but with www
607
- /www\.[^\s<>"{}|\\^`[\]]+/gi,
608
- // Email-like patterns
609
- /[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}/gi,
610
- // Common domain patterns (anything.com, anything.org, etc.)
611
- /(?:^|\s)([a-zA-Z0-9-]+\.)+[a-zA-Z]{2,}(?:\/[^\s]*)?/gi
612
- ];
613
- let stripped = text;
614
- patterns.forEach((pattern) => {
615
- stripped = stripped.replace(pattern, "");
616
- });
617
- stripped = stripped.replace(/\s+/g, " ").trim();
618
- return stripped;
619
- }
620
- function applyTextWrapping(text) {
621
- const words = text.split(/(\s+)/);
622
- return words.map((word) => {
623
- if (/^\s+$/.test(word) || word.includes("<")) {
624
- return word;
625
- }
626
- if (word.length > 30) {
627
- return word.replace(/(.{10})/g, "$1​");
628
- }
629
- return word;
630
- }).join("");
631
- }
632
- function validateContentSecurity(content) {
633
- const issues = [];
634
- if (/<script[\s>]/i.test(content)) {
635
- issues.push("Script tags detected");
636
- }
637
- if (/on\w+\s*=/i.test(content)) {
638
- issues.push("Event handlers detected");
639
- }
640
- if (/javascript:/i.test(content)) {
641
- issues.push("JavaScript protocol detected");
642
- }
643
- if (/data:[^,]*script/i.test(content)) {
644
- issues.push("Data URL with script detected");
645
- }
646
- if (/<iframe[\s>]/i.test(content)) {
647
- issues.push("Iframe tags detected");
648
- }
649
- if (/https?:\/\//i.test(content) || /www\./i.test(content)) {
650
- issues.push("URLs detected in content");
651
- }
652
- return {
653
- isSecure: issues.length === 0,
654
- issues
655
- };
656
- }
657
- function isValidElementType(elementType) {
658
- const validTypes = Object.values(types.ELEMENT_TYPES).map((type) => type.value);
659
- return validTypes.includes(elementType);
660
- }
661
- function validateAttributes(element) {
662
- const errors = [];
663
- const elementType = element.getAttribute("element-type");
664
- const contributorId = element.getAttribute("contributor-id");
665
- if (!elementType) {
666
- errors.push("Missing required attribute: element-type");
667
- } else if (!isValidElementType(elementType)) {
668
- const validTypes = Object.values(types.ELEMENT_TYPES).map((t) => t.value).join(", ");
669
- errors.push(
670
- `Invalid element-type "${elementType}". Valid types: ${validTypes}`
671
- );
672
- }
673
- if (!contributorId) {
674
- errors.push("Missing required attribute: contributor-id");
675
- } else if (contributorId.trim().length === 0) {
676
- errors.push("contributor-id cannot be empty");
677
- }
678
- return {
679
- isValid: errors.length === 0,
680
- errors,
681
- elementType: isValidElementType(elementType || "") ? elementType : void 0,
682
- contributorId: contributorId || void 0
683
- };
684
- }
685
- function getElementTypeLabel(elementType) {
686
- const type = Object.values(types.ELEMENT_TYPES).find((t) => t.value === elementType);
687
- return type?.label || elementType;
688
- }
689
- function getElementTypeAcronym(elementType) {
690
- const type = Object.values(types.ELEMENT_TYPES).find((t) => t.value === elementType);
691
- return type?.acronym || elementType.substring(0, 2).toUpperCase();
692
- }
693
397
  function generateStructuredData(content, elementType, contributorId, sourceUrl) {
694
398
  return {
695
399
  "@context": "https://schema.org",
696
400
  "@type": "AnalysisContent",
697
401
  text: content,
698
402
  analysisType: elementType,
699
- category: getElementTypeLabel(elementType),
403
+ category: validation.getElementTypeLabel(elementType),
700
404
  contributor: contributorId,
701
405
  url: generateRedirectUrl(content, elementType, contributorId),
702
406
  provider: {
@@ -728,7 +432,7 @@ function generateRedirectUrl(content, elementType, contributorId, options = {})
728
432
  return `${baseUrl}?type=contributor&contributor_id=${contributorId}&element_type=${elementType}&content_hash=${contentHash}&${params.toString()}`;
729
433
  }
730
434
  function createMetaTags(content, elementType, contributorId) {
731
- const label = getElementTypeLabel(elementType);
435
+ const label = validation.getElementTypeLabel(elementType);
732
436
  return `
733
437
  <div itemscope itemtype="https://schema.org/AnalysisContent" style="display:none;">
734
438
  <meta itemprop="analysisType" content="${elementType}">
@@ -740,7 +444,7 @@ function createMetaTags(content, elementType, contributorId) {
740
444
  `;
741
445
  }
742
446
  function generateComponentStructuredData(title, componentType, contributorId, elements, sourceUrl) {
743
- const componentLabel = getComponentTypeLabel(componentType);
447
+ const componentLabel = validation.getComponentTypeLabel(componentType);
744
448
  return {
745
449
  "@context": "https://schema.org",
746
450
  "@type": "CreativeWork",
@@ -757,7 +461,7 @@ function generateComponentStructuredData(title, componentType, contributorId, el
757
461
  "@type": "AnalysisContent",
758
462
  text: el.content,
759
463
  analysisType: el.elementType,
760
- category: getElementTypeLabel(el.elementType)
464
+ category: validation.getElementTypeLabel(el.elementType)
761
465
  })),
762
466
  provider: {
763
467
  "@type": "Organization",
@@ -787,7 +491,7 @@ function generateComponentRedirectUrl(componentHash, componentType, contributorI
787
491
  return `${baseUrl}?type=component&component_type=${componentType}&component_hash=${componentHash}&contributor_id=${contributorId}&${params.toString()}`;
788
492
  }
789
493
  function createAriaAttributes(elementType) {
790
- const label = getElementTypeLabel(elementType);
494
+ const label = validation.getElementTypeLabel(elementType);
791
495
  return {
792
496
  role: "article",
793
497
  "aria-label": `${label} - Strategic element`,
@@ -821,7 +525,7 @@ const styles$1 = `
821
525
  color: var(--skhema-text);
822
526
  }
823
527
 
824
- /* Dark mode */
528
+ /* Dark mode (skeleton/error only) */
825
529
  .skhema-component-card[data-theme="dark"],
826
530
  .skhema-skeleton[data-theme="dark"] {
827
531
  --skhema-bg: hsl(222.2 84% 4.9%);
@@ -833,106 +537,40 @@ const styles$1 = `
833
537
  --skhema-surface-subtle: hsl(217.2 32.6% 12%);
834
538
  }
835
539
 
836
- /* Main card */
837
- .skhema-component-card {
838
- position: relative;
839
- background: var(--skhema-card);
840
- border: 1px solid var(--skhema-border);
841
- border-radius: calc(var(--skhema-radius) + 2px);
842
- padding: 0;
843
- box-shadow: var(--skhema-shadow);
844
- transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1);
845
- max-width: 600px;
846
- margin: 8px 0;
847
- overflow: hidden;
540
+ /* Progressive enhancement over the inline-styled, rendered card. Inline styles
541
+ win on specificity, so the changed properties use !important to layer on. */
542
+ .skhema-card {
543
+ transition: box-shadow 0.2s cubic-bezier(0.4, 0, 0.2, 1), transform 0.2s cubic-bezier(0.4, 0, 0.2, 1);
848
544
  }
849
545
 
850
- .skhema-component-card:hover {
851
- box-shadow: var(--skhema-shadow-lg);
546
+ .skhema-card:hover {
547
+ box-shadow: ${validation.CARD_SHADOW_LG} !important;
852
548
  transform: translateY(-1px);
853
549
  }
854
550
 
855
- /* Colored top border */
856
- .skhema-top-border {
857
- height: 2px;
858
- width: 100%;
859
- }
860
-
861
- /* Header */
862
- .skhema-header {
863
- display: flex;
864
- align-items: center;
865
- gap: 8px;
866
- padding: 12px 16px;
867
- border-bottom: 1px solid var(--skhema-border);
868
- }
869
-
870
- .skhema-type-label {
871
- font-size: 13px;
872
- font-weight: 500;
873
- color: var(--skhema-text-muted);
874
- }
875
-
876
- .skhema-title-separator {
877
- color: var(--skhema-text-muted);
878
- font-size: 13px;
879
- }
880
-
881
- .skhema-title {
882
- font-size: 13px;
883
- font-weight: 600;
884
- color: var(--skhema-text);
885
- }
886
-
887
- /* Elements section */
888
- .skhema-elements {
889
- padding: 16px;
890
- display: flex;
891
- flex-direction: column;
892
- gap: 16px;
893
- }
894
-
895
- /* Element group */
896
- .skhema-element-group {
897
- display: flex;
898
- flex-direction: column;
899
- gap: 4px;
900
- }
901
-
902
- .skhema-element-label {
903
- text-transform: uppercase;
904
- letter-spacing: 0.05em;
905
- font-family: ui-monospace, SFMono-Regular, 'SF Mono', Menlo, Consolas, monospace;
906
- font-size: 10px;
907
- font-weight: 600;
908
- color: var(--skhema-text-muted);
551
+ .skhema-save-btn {
552
+ transition: background 0.15s ease;
909
553
  }
910
554
 
911
- .skhema-element-content {
912
- font-size: 14px;
913
- line-height: 1.6;
914
- color: var(--skhema-text);
915
- padding-left: 8px;
916
- border-left: 2px solid var(--skhema-border);
917
- word-wrap: break-word;
918
- overflow-wrap: break-word;
555
+ .skhema-save-btn:hover {
556
+ background: ${validation.PRIMARY_HOVER_HEX} !important;
919
557
  }
920
558
 
921
- /* Footer */
922
- .skhema-footer {
923
- padding: 12px 16px;
924
- border-top: 1px solid var(--skhema-border);
559
+ .skhema-save-btn:active {
560
+ background: ${validation.PRIMARY_PRESSED_HEX} !important;
925
561
  }
926
562
 
927
- .skhema-footer-row {
928
- display: flex;
929
- align-items: center;
930
- justify-content: space-between;
931
- gap: 12px;
563
+ /* Error card wrapper (browser-only state) */
564
+ .skhema-component-card {
565
+ background: var(--skhema-card);
566
+ border: 1px solid var(--skhema-border);
567
+ border-radius: calc(var(--skhema-radius) + 2px);
568
+ box-shadow: var(--skhema-shadow);
569
+ max-width: 600px;
570
+ margin: 8px 0;
571
+ overflow: hidden;
932
572
  }
933
573
 
934
- ${SHARED_CARD_STYLES}
935
-
936
574
  /* Error state */
937
575
  .skhema-error {
938
576
  background: hsl(0 93% 94%);
@@ -1017,39 +655,9 @@ ${SHARED_CARD_STYLES}
1017
655
  100% { background-position: 200% 0; }
1018
656
  }
1019
657
 
1020
- /* Responsive */
1021
- @media (max-width: 640px) {
1022
- .skhema-component-card {
1023
- margin: 4px 0;
1024
- }
1025
-
1026
- .skhema-header {
1027
- padding: 10px 12px;
1028
- flex-wrap: wrap;
1029
- }
1030
-
1031
- .skhema-elements {
1032
- padding: 12px;
1033
- }
1034
-
1035
- .skhema-footer {
1036
- padding: 10px 12px;
1037
- }
1038
-
1039
- .skhema-footer-row {
1040
- flex-direction: column;
1041
- align-items: stretch;
1042
- gap: 8px;
1043
- }
1044
-
1045
- .skhema-save-btn {
1046
- justify-content: center;
1047
- }
1048
- }
1049
-
1050
658
  /* Accessibility */
1051
659
  @media (prefers-reduced-motion: reduce) {
1052
- .skhema-component-card,
660
+ .skhema-card,
1053
661
  .skhema-save-btn {
1054
662
  transition: none;
1055
663
  }
@@ -1126,13 +734,13 @@ class SkhemaComponent extends HTMLElement {
1126
734
  });
1127
735
  }
1128
736
  render() {
1129
- const validation = this.validateComponentAttributes();
1130
- if (!validation.isValid) {
1131
- this.renderError("Invalid component attributes", validation.errors);
737
+ const validation2 = this.validateComponentAttributes();
738
+ if (!validation2.isValid) {
739
+ this.renderError("Invalid component attributes", validation2.errors);
1132
740
  return;
1133
741
  }
1134
- const componentType = validation.componentType;
1135
- const contributorId = validation.contributorId;
742
+ const componentType = validation2.componentType;
743
+ const contributorId = validation2.contributorId;
1136
744
  const title = this.getAttribute("title") || "";
1137
745
  const childElements = this.gatherChildElements(componentType);
1138
746
  if (childElements.length === 0) {
@@ -1171,7 +779,7 @@ class SkhemaComponent extends HTMLElement {
1171
779
  const contributorId = this.getAttribute("contributor-id");
1172
780
  if (!componentType) {
1173
781
  errors.push("Missing required attribute: component-type");
1174
- } else if (!isValidComponentType(componentType)) {
782
+ } else if (!validation.isValidComponentType(componentType)) {
1175
783
  errors.push(`Invalid component-type "${componentType}"`);
1176
784
  }
1177
785
  if (!contributorId) {
@@ -1182,7 +790,7 @@ class SkhemaComponent extends HTMLElement {
1182
790
  return {
1183
791
  isValid: errors.length === 0,
1184
792
  errors,
1185
- componentType: isValidComponentType(componentType || "") ? componentType : void 0,
793
+ componentType: validation.isValidComponentType(componentType || "") ? componentType : void 0,
1186
794
  contributorId: contributorId || void 0
1187
795
  };
1188
796
  }
@@ -1194,14 +802,14 @@ class SkhemaComponent extends HTMLElement {
1194
802
  for (const child of children) {
1195
803
  const data = child.getElementData?.();
1196
804
  if (!data) continue;
1197
- if (!validateElementBelongsToComponent(data.elementType, componentType)) {
805
+ if (!validation.validateElementBelongsToComponent(data.elementType, componentType)) {
1198
806
  console.warn(
1199
807
  `skhema-component: element type "${data.elementType}" does not belong to component type "${componentType}"`
1200
808
  );
1201
809
  }
1202
810
  elements.push(data);
1203
811
  }
1204
- const orderedTypes = getElementTypesForComponent(componentType);
812
+ const orderedTypes = validation.getElementTypesForComponent(componentType);
1205
813
  elements.sort((a, b) => {
1206
814
  const aIdx = orderedTypes.indexOf(a.elementType);
1207
815
  const bIdx = orderedTypes.indexOf(b.elementType);
@@ -1214,10 +822,7 @@ class SkhemaComponent extends HTMLElement {
1214
822
  renderContent(elements) {
1215
823
  if (!this.contentData) return;
1216
824
  const { component_type, contributor_id, title } = this.contentData;
1217
- const componentLabel = getComponentTypeLabel(component_type);
1218
- const acronym = getComponentTypeAcronym(component_type);
1219
- const colors = COMPONENT_COLORS[component_type];
1220
- const authorFooterHtml = resolveAuthorFooterHtml(this, contributor_id);
825
+ const componentLabel = validation.getComponentTypeLabel(component_type);
1221
826
  const themeAttribute = this.getAttribute("theme") || "auto";
1222
827
  const actualTheme = this.getActualTheme(themeAttribute);
1223
828
  const redirectUrl = generateComponentRedirectUrl(
@@ -1225,65 +830,25 @@ class SkhemaComponent extends HTMLElement {
1225
830
  component_type,
1226
831
  contributor_id
1227
832
  );
1228
- const badgeStyle = colors ? `background: ${colors.bg}; color: ${colors.text}; border: 1px solid ${colors.border};` : "";
1229
- const topBorderColor = colors ? colors.text : "var(--skhema-primary)";
1230
- const groupedElements = this.groupElementsByType(elements);
1231
- const elementSectionsHtml = groupedElements.map(
1232
- (group) => `
1233
- <div class="skhema-element-group">
1234
- <div class="skhema-element-label">${getElementTypeLabel(group.elementType)}</div>
1235
- ${group.items.map(
1236
- (item) => `<div class="skhema-element-content">${sanitizeContent(item.content)}</div>`
1237
- ).join("")}
1238
- </div>
1239
- `
1240
- ).join("");
1241
833
  this.setAttribute("role", "article");
1242
834
  this.setAttribute(
1243
835
  "aria-label",
1244
836
  `${componentLabel} component${title ? ` — ${title}` : ""}`
1245
837
  );
1246
- const titleHtml = title ? `<span class="skhema-title-separator">&mdash;</span><span class="skhema-title">${title}</span>` : "";
1247
- this.shadow.innerHTML = `
1248
- <style>${styles$1}</style>
1249
-
1250
- <div class="skhema-component-card" data-theme="${actualTheme}">
1251
- <div class="skhema-top-border" style="background: ${topBorderColor};"></div>
1252
-
1253
- <div class="skhema-header">
1254
- <span class="skhema-acronym-badge" style="${badgeStyle}" title="${componentLabel}">
1255
- ${acronym}
1256
- </span>
1257
- <span class="skhema-type-label">${componentLabel}</span>
1258
- ${titleHtml}
1259
- </div>
1260
-
1261
- <div class="skhema-elements">
1262
- ${elementSectionsHtml}
1263
- </div>
1264
-
1265
- <div class="skhema-footer">
1266
- <div class="skhema-footer-row">
1267
- <span class="skhema-contributor-line">
1268
- ${USER_ICON_SVG}
1269
- ${authorFooterHtml}
1270
- </span>
1271
- <a href="${redirectUrl}"
1272
- class="skhema-save-btn"
1273
- target="_blank"
1274
- rel="noopener noreferrer"
1275
- title="Save this component to Skhema">
1276
- Save to Skhema
1277
- </a>
1278
- </div>
1279
- <div class="skhema-footer-attribution">
1280
- Strategy powered by <a href="https://skhema.com" target="_blank" rel="noopener noreferrer">Skhema</a>
1281
- </div>
1282
- </div>
1283
- </div>
1284
-
1285
- <slot style="display:none;"></slot>
1286
- `;
838
+ const cardHtml = render.renderComponentCardHtml({
839
+ componentType: component_type,
840
+ title,
841
+ elements: elements.map((el) => ({
842
+ elementType: el.elementType,
843
+ content: el.content
844
+ })),
845
+ saveUrl: redirectUrl,
846
+ authorName: this.getAttribute("author-name") || this.getAttribute("contributor-name"),
847
+ authorSlug: this.getAttribute("author-slug"),
848
+ contributorId: contributor_id,
849
+ theme: actualTheme
850
+ });
851
+ this.shadow.innerHTML = `<style>${styles$1}</style>${cardHtml}<slot style="display:none;"></slot>`;
1287
852
  const saveBtn = this.shadow.querySelector(
1288
853
  ".skhema-save-btn"
1289
854
  );
@@ -1293,21 +858,6 @@ class SkhemaComponent extends HTMLElement {
1293
858
  });
1294
859
  }
1295
860
  }
1296
- groupElementsByType(elements) {
1297
- const groups = /* @__PURE__ */ new Map();
1298
- for (const el of elements) {
1299
- const existing = groups.get(el.elementType);
1300
- if (existing) {
1301
- existing.push(el);
1302
- } else {
1303
- groups.set(el.elementType, [el]);
1304
- }
1305
- }
1306
- return Array.from(groups.entries()).map(([elementType, items]) => ({
1307
- elementType,
1308
- items
1309
- }));
1310
- }
1311
861
  getActualTheme(themeAttribute) {
1312
862
  if (themeAttribute === "light" || themeAttribute === "dark") {
1313
863
  return themeAttribute;
@@ -1495,12 +1045,8 @@ class SkhemaComponent extends HTMLElement {
1495
1045
  }
1496
1046
  updateTheme() {
1497
1047
  const themeAttribute = this.getAttribute("theme") || "auto";
1498
- if (themeAttribute === "auto") {
1499
- const card = this.shadow.querySelector(".skhema-component-card");
1500
- if (card) {
1501
- const newTheme = this.getActualTheme("auto");
1502
- card.setAttribute("data-theme", newTheme);
1503
- }
1048
+ if (themeAttribute === "auto" && this.contentData) {
1049
+ this.scheduleRender();
1504
1050
  }
1505
1051
  }
1506
1052
  }
@@ -1532,7 +1078,7 @@ const styles = `
1532
1078
  color: var(--skhema-text);
1533
1079
  }
1534
1080
 
1535
- /* Dark mode styles - applied via data-theme attribute */
1081
+ /* Dark mode styles - applied via data-theme attribute (skeleton/error only) */
1536
1082
  .skhema-element-card[data-theme="dark"],
1537
1083
  .skhema-skeleton[data-theme="dark"] {
1538
1084
  --skhema-bg: hsl(222.2 84% 4.9%);
@@ -1543,71 +1089,42 @@ const styles = `
1543
1089
  --skhema-accent: hsl(217.2 32.6% 17.5%);
1544
1090
  }
1545
1091
 
1546
- /* Main component card */
1547
- .skhema-element-card {
1548
- position: relative;
1549
- background: var(--skhema-card);
1550
- border: 1px solid var(--skhema-border);
1551
- border-radius: calc(var(--skhema-radius) + 2px);
1552
- padding: 0;
1553
- box-shadow: var(--skhema-shadow);
1554
- transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1);
1555
- max-width: 600px;
1556
- margin: 8px 0;
1557
- overflow: hidden;
1092
+ /* Progressive enhancement over the inline-styled, rendered card. Inline styles
1093
+ win on specificity, so the changed properties use !important to layer on. */
1094
+ .skhema-card {
1095
+ transition: box-shadow 0.2s cubic-bezier(0.4, 0, 0.2, 1), transform 0.2s cubic-bezier(0.4, 0, 0.2, 1), border-color 0.2s cubic-bezier(0.4, 0, 0.2, 1);
1558
1096
  }
1559
1097
 
1560
- .skhema-element-card:hover {
1561
- box-shadow: var(--skhema-shadow-lg);
1562
- border-color: var(--skhema-primary);
1098
+ .skhema-card:hover {
1099
+ box-shadow: ${validation.CARD_SHADOW_LG} !important;
1563
1100
  transform: translateY(-1px);
1564
1101
  }
1565
1102
 
1566
- /* Header section with acronym badge + element type label */
1567
- .skhema-header {
1568
- display: flex;
1569
- align-items: center;
1570
- gap: 8px;
1571
- padding: 12px 16px;
1572
- border-bottom: 1px solid var(--skhema-border);
1573
- }
1574
-
1575
- /* Content section */
1576
- .skhema-content {
1577
- padding: 16px;
1103
+ .skhema-card[data-skhema-kind="element"]:hover {
1104
+ border-color: ${validation.PRIMARY_HEX} !important;
1578
1105
  }
1579
1106
 
1580
- .skhema-content-text {
1581
- font-size: 15px;
1582
- line-height: 1.6;
1583
- color: var(--skhema-text);
1584
- margin: 0;
1585
- word-wrap: break-word;
1586
- overflow-wrap: break-word;
1587
- hyphens: auto;
1588
- max-width: 100%;
1107
+ .skhema-save-btn {
1108
+ transition: background 0.15s ease;
1589
1109
  }
1590
1110
 
1591
- /* Footer section */
1592
- .skhema-footer {
1593
- padding: 12px 16px;
1594
- border-top: 1px solid var(--skhema-border);
1111
+ .skhema-save-btn:hover {
1112
+ background: ${validation.PRIMARY_HOVER_HEX} !important;
1595
1113
  }
1596
1114
 
1597
- .skhema-footer-row {
1598
- display: flex;
1599
- align-items: center;
1600
- justify-content: space-between;
1601
- gap: 12px;
1115
+ .skhema-save-btn:active {
1116
+ background: ${validation.PRIMARY_PRESSED_HEX} !important;
1602
1117
  }
1603
1118
 
1604
- ${SHARED_CARD_STYLES}
1605
-
1606
- /* Element type label next to badge */
1607
- .skhema-type-label {
1608
- font-size: 13px;
1609
- font-weight: 500;
1610
- color: var(--skhema-text);
1119
+ /* Error card wrapper (browser-only state) */
1120
+ .skhema-element-card {
1121
+ background: var(--skhema-card);
1122
+ border: 1px solid var(--skhema-border);
1123
+ border-radius: calc(var(--skhema-radius) + 2px);
1124
+ box-shadow: var(--skhema-shadow);
1125
+ max-width: 600px;
1126
+ margin: 8px 0;
1127
+ overflow: hidden;
1611
1128
  }
1612
1129
 
1613
1130
  /* Error state */
@@ -1707,49 +1224,12 @@ ${SHARED_CARD_STYLES}
1707
1224
  }
1708
1225
  }
1709
1226
 
1710
- /* Responsive design */
1711
- @media (max-width: 640px) {
1712
- .skhema-element-card {
1713
- margin: 4px 0;
1714
- }
1715
-
1716
- .skhema-header {
1717
- padding: 10px 12px;
1718
- }
1719
-
1720
- .skhema-content {
1721
- padding: 12px;
1722
- }
1723
-
1724
- .skhema-footer {
1725
- padding: 10px 12px;
1726
- }
1727
-
1728
- .skhema-footer-row {
1729
- flex-direction: column;
1730
- align-items: stretch;
1731
- gap: 8px;
1732
- }
1733
-
1734
- .skhema-save-btn {
1735
- justify-content: center;
1736
- }
1737
- }
1738
-
1739
1227
  /* Accessibility */
1740
1228
  @media (prefers-reduced-motion: reduce) {
1741
- .skhema-element-card,
1229
+ .skhema-card,
1742
1230
  .skhema-save-btn {
1743
1231
  transition: none;
1744
1232
  }
1745
-
1746
- .skhema-save-btn::after {
1747
- transition: none;
1748
- }
1749
-
1750
- .skhema-save-btn:hover::after {
1751
- transform: none;
1752
- }
1753
1233
  }
1754
1234
 
1755
1235
  .skhema-structured-data {
@@ -1786,11 +1266,11 @@ class SkhemaElement extends HTMLElement {
1786
1266
  this.componentConnected = true;
1787
1267
  if (this.closest("skhema-component")) {
1788
1268
  this.nestedMode = true;
1789
- const validation = validateAttributes(this);
1790
- if (!validation.isValid) {
1269
+ const validation$1 = validation.validateAttributes(this);
1270
+ if (!validation$1.isValid) {
1791
1271
  console.warn(
1792
1272
  "skhema-element: invalid attributes in nested mode",
1793
- validation.errors
1273
+ validation$1.errors
1794
1274
  );
1795
1275
  return;
1796
1276
  }
@@ -1800,8 +1280,8 @@ class SkhemaElement extends HTMLElement {
1800
1280
  return;
1801
1281
  }
1802
1282
  this.contentData = {
1803
- contributor_id: validation.contributorId,
1804
- element_type: validation.elementType,
1283
+ contributor_id: validation$1.contributorId,
1284
+ element_type: validation$1.elementType,
1805
1285
  content,
1806
1286
  content_hash: generateContentHash(content),
1807
1287
  source_url: this.getAttribute("source-url") || window.location.href,
@@ -1849,9 +1329,9 @@ class SkhemaElement extends HTMLElement {
1849
1329
  };
1850
1330
  }
1851
1331
  render() {
1852
- const validation = validateAttributes(this);
1853
- if (!validation.isValid) {
1854
- 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);
1855
1335
  return;
1856
1336
  }
1857
1337
  const content = this.getContent();
@@ -1861,7 +1341,7 @@ class SkhemaElement extends HTMLElement {
1861
1341
  ]);
1862
1342
  return;
1863
1343
  }
1864
- const securityValidation = validateContentSecurity(content);
1344
+ const securityValidation = render.validateContentSecurity(content);
1865
1345
  if (!securityValidation.isSecure) {
1866
1346
  this.renderError(
1867
1347
  "Content security validation failed",
@@ -1870,8 +1350,8 @@ class SkhemaElement extends HTMLElement {
1870
1350
  return;
1871
1351
  }
1872
1352
  this.contentData = {
1873
- contributor_id: validation.contributorId,
1874
- element_type: validation.elementType,
1353
+ contributor_id: validation$1.contributorId,
1354
+ element_type: validation$1.elementType,
1875
1355
  content,
1876
1356
  content_hash: generateContentHash(content),
1877
1357
  source_url: this.getAttribute("source-url") || window.location.href,
@@ -1887,7 +1367,6 @@ class SkhemaElement extends HTMLElement {
1887
1367
  renderContent() {
1888
1368
  if (!this.contentData) return;
1889
1369
  const { element_type, contributor_id, content } = this.contentData;
1890
- const label = getElementTypeLabel(element_type);
1891
1370
  const redirectUrl = generateRedirectUrl(
1892
1371
  content,
1893
1372
  element_type,
@@ -1895,50 +1374,20 @@ class SkhemaElement extends HTMLElement {
1895
1374
  );
1896
1375
  const themeAttribute = this.getAttribute("theme") || "auto";
1897
1376
  const actualTheme = this.getActualTheme(themeAttribute);
1898
- const authorFooterHtml = resolveAuthorFooterHtml(this, contributor_id);
1899
- const componentType = resolveComponentType(element_type);
1900
- const acronym = getComponentTypeAcronym(componentType);
1901
- const colors = COMPONENT_COLORS[componentType];
1902
1377
  const ariaAttrs = createAriaAttributes(element_type);
1903
1378
  Object.entries(ariaAttrs).forEach(([key, value]) => {
1904
1379
  this.setAttribute(key, value);
1905
1380
  });
1906
- const badgeStyle = colors ? `background: ${colors.bg}; color: ${colors.text}; border: 1px solid ${colors.border};` : "";
1907
- this.shadow.innerHTML = `
1908
- <style>${styles}</style>
1909
-
1910
- <div class="skhema-element-card" data-theme="${actualTheme}">
1911
- <div class="skhema-header">
1912
- <span class="skhema-acronym-badge" style="${badgeStyle}" title="${label}">
1913
- ${acronym}
1914
- </span>
1915
- <span class="skhema-type-label">${label}</span>
1916
- </div>
1917
-
1918
- <div class="skhema-content">
1919
- <div class="skhema-content-text">${sanitizeContent(content)}</div>
1920
- </div>
1921
-
1922
- <div class="skhema-footer">
1923
- <div class="skhema-footer-row">
1924
- <span class="skhema-contributor-line">
1925
- ${USER_ICON_SVG}
1926
- ${authorFooterHtml}
1927
- </span>
1928
- <a href="${redirectUrl}"
1929
- class="skhema-save-btn"
1930
- target="_blank"
1931
- rel="noopener noreferrer"
1932
- title="Save this element to Skhema">
1933
- Save to Skhema
1934
- </a>
1935
- </div>
1936
- <div class="skhema-footer-attribution">
1937
- Strategy powered by <a href="https://skhema.com" target="_blank" rel="noopener noreferrer">Skhema</a>
1938
- </div>
1939
- </div>
1940
- </div>
1941
- `;
1381
+ const cardHtml = render.renderElementCardHtml({
1382
+ elementType: element_type,
1383
+ content,
1384
+ saveUrl: redirectUrl,
1385
+ authorName: this.getAttribute("author-name") || this.getAttribute("contributor-name"),
1386
+ authorSlug: this.getAttribute("author-slug"),
1387
+ contributorId: contributor_id,
1388
+ theme: actualTheme
1389
+ });
1390
+ this.shadow.innerHTML = `<style>${styles}</style>${cardHtml}`;
1942
1391
  const saveBtn = this.shadow.querySelector(
1943
1392
  ".skhema-save-btn"
1944
1393
  );
@@ -2126,12 +1575,8 @@ class SkhemaElement extends HTMLElement {
2126
1575
  }
2127
1576
  updateTheme() {
2128
1577
  const themeAttribute = this.getAttribute("theme") || "auto";
2129
- if (themeAttribute === "auto") {
2130
- const card = this.shadow.querySelector(".skhema-element-card");
2131
- if (card) {
2132
- const newTheme = this.getActualTheme("auto");
2133
- card.setAttribute("data-theme", newTheme);
2134
- }
1578
+ if (themeAttribute === "auto" && this.contentData && !this.nestedMode) {
1579
+ this.renderContent();
2135
1580
  }
2136
1581
  }
2137
1582
  }
@@ -2165,6 +1610,15 @@ if (typeof window !== "undefined") {
2165
1610
  );
2166
1611
  }
2167
1612
  }
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;
2168
1622
  exports.SkhemaComponent = SkhemaComponent;
2169
1623
  exports.SkhemaElement = SkhemaElement;
2170
1624
  exports.default = SkhemaElement;
@@ -2173,18 +1627,9 @@ exports.generateComponentRedirectUrl = generateComponentRedirectUrl;
2173
1627
  exports.generateComponentStructuredData = generateComponentStructuredData;
2174
1628
  exports.generateRedirectUrl = generateRedirectUrl;
2175
1629
  exports.generateStructuredData = generateStructuredData;
2176
- exports.getComponentTypeAcronym = getComponentTypeAcronym;
2177
- exports.getComponentTypeLabel = getComponentTypeLabel;
2178
- exports.getElementTypeAcronym = getElementTypeAcronym;
2179
- exports.getElementTypeLabel = getElementTypeLabel;
2180
- exports.isValidComponentType = isValidComponentType;
2181
- exports.isValidElementType = isValidElementType;
2182
1630
  exports.registerSkhemaComponent = registerSkhemaComponent;
2183
1631
  exports.registerSkhemaElement = registerSkhemaElement;
2184
- exports.resolveComponentType = resolveComponentType;
2185
1632
  exports.shouldTrackAnalytics = shouldTrackAnalytics;
2186
1633
  exports.trackComponentClick = trackComponentClick;
2187
1634
  exports.trackComponentEmbedLoad = trackComponentEmbedLoad;
2188
- exports.validateAttributes = validateAttributes;
2189
- exports.validateElementBelongsToComponent = validateElementBelongsToComponent;
2190
1635
  //# sourceMappingURL=index.cjs.map