@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.es.js CHANGED
@@ -1,159 +1,6 @@
1
- import { SKHEMA_MAPPING, COMPONENT_TYPES, ELEMENT_TYPES } from "@skhema/types";
2
- const COMPONENT_COLORS = {
3
- diagnosis: {
4
- bg: "oklch(0.65 0.06 25 / 0.15)",
5
- text: "oklch(0.65 0.06 25)",
6
- border: "oklch(0.65 0.06 25 / 0.3)"
7
- },
8
- method: {
9
- bg: "oklch(0.6 0.06 250 / 0.15)",
10
- text: "oklch(0.6 0.06 250)",
11
- border: "oklch(0.6 0.06 250 / 0.3)"
12
- },
13
- initiatives: {
14
- bg: "oklch(0.6 0.06 155 / 0.15)",
15
- text: "oklch(0.6 0.06 155)",
16
- border: "oklch(0.6 0.06 155 / 0.3)"
17
- },
18
- measures: {
19
- bg: "oklch(0.6 0.06 300 / 0.15)",
20
- text: "oklch(0.6 0.06 300)",
21
- border: "oklch(0.6 0.06 300 / 0.3)"
22
- },
23
- support: {
24
- bg: "oklch(0.65 0.06 65 / 0.15)",
25
- text: "oklch(0.65 0.06 65)",
26
- border: "oklch(0.65 0.06 65 / 0.3)"
27
- }
28
- };
29
- const SHARED_CARD_STYLES = `
30
- /* Monospace acronym badge */
31
- .skhema-acronym-badge {
32
- display: inline-flex;
33
- align-items: center;
34
- font-family: ui-monospace, SFMono-Regular, 'SF Mono', Menlo, Consolas, monospace;
35
- font-size: 10px;
36
- font-weight: 600;
37
- padding: 2px 6px;
38
- border-radius: 2px;
39
- letter-spacing: 0.02em;
40
- flex-shrink: 0;
41
- }
42
-
43
- /* Footer attribution tagline */
44
- .skhema-footer-attribution {
45
- font-size: 11px;
46
- color: var(--skhema-text-muted);
47
- line-height: 1.4;
48
- }
49
-
50
- .skhema-footer-attribution a {
51
- color: var(--skhema-text-muted);
52
- text-decoration: underline;
53
- text-decoration-color: var(--skhema-border);
54
- text-underline-offset: 2px;
55
- }
56
-
57
- .skhema-footer-attribution a:hover {
58
- color: var(--skhema-text);
59
- }
60
-
61
- /* Contributor line in footer */
62
- .skhema-contributor-line {
63
- display: flex;
64
- align-items: center;
65
- gap: 6px;
66
- font-size: 12px;
67
- color: var(--skhema-text-muted);
68
- }
69
-
70
- .skhema-contributor-line svg {
71
- width: 14px;
72
- height: 14px;
73
- flex-shrink: 0;
74
- }
75
-
76
- .skhema-author-link {
77
- color: inherit;
78
- text-decoration: none;
79
- }
80
-
81
- .skhema-author-link:hover {
82
- color: var(--skhema-text);
83
- text-decoration: underline;
84
- }
85
-
86
- /* Save button — primary brand color */
87
- .skhema-save-btn {
88
- display: inline-flex;
89
- align-items: center;
90
- gap: 6px;
91
- background: var(--skhema-primary);
92
- color: white;
93
- border: none;
94
- padding: 6px 14px;
95
- border-radius: calc(var(--skhema-radius) + 2px);
96
- font-size: 12px;
97
- font-weight: 500;
98
- text-decoration: none;
99
- cursor: pointer;
100
- transition: background 0.15s ease;
101
- white-space: nowrap;
102
- }
103
-
104
- .skhema-save-btn:hover {
105
- background: var(--skhema-primary-hover);
106
- }
107
-
108
- .skhema-save-btn:active {
109
- background: var(--skhema-primary-pressed);
110
- }
111
-
112
- .skhema-save-btn:focus {
113
- outline: 2px solid var(--skhema-primary);
114
- outline-offset: 2px;
115
- }
116
-
117
- .skhema-save-btn::after {
118
- content: '\\2192';
119
- font-size: 13px;
120
- }
121
- `;
122
- 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>`;
123
- function readAuthorAttribution(element) {
124
- const authorName = element.getAttribute("author-name") || element.getAttribute("contributor-name");
125
- if (!authorName?.trim()) {
126
- return null;
127
- }
128
- const authorSlug = element.getAttribute("author-slug")?.trim();
129
- return {
130
- authorName: authorName.trim(),
131
- authorSlug: authorSlug || void 0
132
- };
133
- }
134
- function escapeHtml(text) {
135
- return text.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
136
- }
137
- function formatAuthorAttributionHtml(authorName, authorSlug) {
138
- const label = `By ${escapeHtml(authorName)}`;
139
- if (authorSlug) {
140
- return `<a href="https://skhema.com/contributors/${encodeURIComponent(authorSlug)}" class="skhema-author-link" target="_blank" rel="noopener noreferrer">${label}</a>`;
141
- }
142
- return `<span class="skhema-author-text">${label}</span>`;
143
- }
144
- function formatContributorIdFallback(contributorId) {
145
- return contributorId.split(/[_-]/).map((word) => word.charAt(0).toUpperCase() + word.slice(1).toLowerCase()).join(" ");
146
- }
147
- function resolveAuthorFooterHtml(element, contributorId) {
148
- const attribution = readAuthorAttribution(element);
149
- if (attribution) {
150
- return formatAuthorAttributionHtml(
151
- attribution.authorName,
152
- attribution.authorSlug
153
- );
154
- }
155
- return `<span class="skhema-author-text">By ${escapeHtml(formatContributorIdFallback(contributorId))}</span>`;
156
- }
1
+ import { r as renderComponentCardHtml, a as renderElementCardHtml, v as validateContentSecurity } from "./index-jwx5CXLB.js";
2
+ import { d as getElementTypeLabel, b as getComponentTypeLabel, i as isValidComponentType, v as validateElementBelongsToComponent, e as getElementTypesForComponent, l as CARD_SHADOW_LG, m as PRIMARY_HOVER_HEX, n as PRIMARY_PRESSED_HEX, o as validateAttributes, P as PRIMARY_HEX } from "./validation-Dt4L_df8.js";
3
+ import { g, c, f, r } from "./validation-Dt4L_df8.js";
157
4
  const ANALYTICS_ENDPOINT = "https://analytics.skhema.com/functions/v1/embed-manage";
158
5
  const TRACKING_COOKIE_NAME = "_sk";
159
6
  const TRACKING_EXPIRY_HOURS = 24;
@@ -527,43 +374,6 @@ function shouldTrackAnalytics(element) {
527
374
  const trackAnalytics = element.getAttribute("track-analytics");
528
375
  return trackAnalytics !== "false";
529
376
  }
530
- function isValidComponentType(componentType) {
531
- const validTypes = Object.values(COMPONENT_TYPES).map((t) => t.value);
532
- return validTypes.includes(componentType);
533
- }
534
- function validateElementBelongsToComponent(elementType, componentType) {
535
- if (!isValidComponentType(componentType)) return false;
536
- const validElements = SKHEMA_MAPPING.elementFlow[componentType];
537
- if (!validElements) return false;
538
- return validElements.some((e) => e.value === elementType);
539
- }
540
- function getComponentTypeLabel(componentType) {
541
- const type = Object.values(COMPONENT_TYPES).find(
542
- (t) => t.value === componentType
543
- );
544
- return type?.label || componentType;
545
- }
546
- function getComponentTypeAcronym(componentType) {
547
- const type = Object.values(COMPONENT_TYPES).find(
548
- (t) => t.value === componentType
549
- );
550
- return type?.acronym || componentType.substring(0, 2).toUpperCase();
551
- }
552
- function resolveComponentType(elementType) {
553
- for (const [componentValue, elements] of Object.entries(
554
- SKHEMA_MAPPING.elementFlow
555
- )) {
556
- if (elements.some((e) => e.value === elementType)) {
557
- return componentValue;
558
- }
559
- }
560
- return "diagnosis";
561
- }
562
- function getElementTypesForComponent(componentType) {
563
- if (!isValidComponentType(componentType)) return [];
564
- const elements = SKHEMA_MAPPING.elementFlow[componentType];
565
- return elements?.map((e) => e.value) || [];
566
- }
567
377
  function generateContentHash(content) {
568
378
  let hash = 0;
569
379
  const cleanContent = content.trim().substring(0, 200);
@@ -583,111 +393,6 @@ function generateComponentHash(elements) {
583
393
  const combined = sorted.map((e) => `${e.elementType}:${e.content}`).join("|");
584
394
  return generateContentHash(combined);
585
395
  }
586
- function sanitizeContent(content) {
587
- const htmlEncode = (str) => {
588
- const div = document.createElement("div");
589
- div.textContent = str;
590
- return div.innerHTML;
591
- };
592
- let sanitized = stripUrls(content);
593
- sanitized = htmlEncode(sanitized);
594
- sanitized = sanitized.replace(/\n/g, "<br>");
595
- sanitized = applyTextWrapping(sanitized);
596
- return sanitized;
597
- }
598
- function stripUrls(text) {
599
- const patterns = [
600
- // Standard URLs with protocols
601
- /https?:\/\/[^\s<>"{}|\\^`[\]]+/gi,
602
- // FTP URLs
603
- /ftp:\/\/[^\s<>"{}|\\^`[\]]+/gi,
604
- // URLs without protocol but with www
605
- /www\.[^\s<>"{}|\\^`[\]]+/gi,
606
- // Email-like patterns
607
- /[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}/gi,
608
- // Common domain patterns (anything.com, anything.org, etc.)
609
- /(?:^|\s)([a-zA-Z0-9-]+\.)+[a-zA-Z]{2,}(?:\/[^\s]*)?/gi
610
- ];
611
- let stripped = text;
612
- patterns.forEach((pattern) => {
613
- stripped = stripped.replace(pattern, "");
614
- });
615
- stripped = stripped.replace(/\s+/g, " ").trim();
616
- return stripped;
617
- }
618
- function applyTextWrapping(text) {
619
- const words = text.split(/(\s+)/);
620
- return words.map((word) => {
621
- if (/^\s+$/.test(word) || word.includes("<")) {
622
- return word;
623
- }
624
- if (word.length > 30) {
625
- return word.replace(/(.{10})/g, "$1​");
626
- }
627
- return word;
628
- }).join("");
629
- }
630
- function validateContentSecurity(content) {
631
- const issues = [];
632
- if (/<script[\s>]/i.test(content)) {
633
- issues.push("Script tags detected");
634
- }
635
- if (/on\w+\s*=/i.test(content)) {
636
- issues.push("Event handlers detected");
637
- }
638
- if (/javascript:/i.test(content)) {
639
- issues.push("JavaScript protocol detected");
640
- }
641
- if (/data:[^,]*script/i.test(content)) {
642
- issues.push("Data URL with script detected");
643
- }
644
- if (/<iframe[\s>]/i.test(content)) {
645
- issues.push("Iframe tags detected");
646
- }
647
- if (/https?:\/\//i.test(content) || /www\./i.test(content)) {
648
- issues.push("URLs detected in content");
649
- }
650
- return {
651
- isSecure: issues.length === 0,
652
- issues
653
- };
654
- }
655
- function isValidElementType(elementType) {
656
- const validTypes = Object.values(ELEMENT_TYPES).map((type) => type.value);
657
- return validTypes.includes(elementType);
658
- }
659
- function validateAttributes(element) {
660
- const errors = [];
661
- const elementType = element.getAttribute("element-type");
662
- const contributorId = element.getAttribute("contributor-id");
663
- if (!elementType) {
664
- errors.push("Missing required attribute: element-type");
665
- } else if (!isValidElementType(elementType)) {
666
- const validTypes = Object.values(ELEMENT_TYPES).map((t) => t.value).join(", ");
667
- errors.push(
668
- `Invalid element-type "${elementType}". Valid types: ${validTypes}`
669
- );
670
- }
671
- if (!contributorId) {
672
- errors.push("Missing required attribute: contributor-id");
673
- } else if (contributorId.trim().length === 0) {
674
- errors.push("contributor-id cannot be empty");
675
- }
676
- return {
677
- isValid: errors.length === 0,
678
- errors,
679
- elementType: isValidElementType(elementType || "") ? elementType : void 0,
680
- contributorId: contributorId || void 0
681
- };
682
- }
683
- function getElementTypeLabel(elementType) {
684
- const type = Object.values(ELEMENT_TYPES).find((t) => t.value === elementType);
685
- return type?.label || elementType;
686
- }
687
- function getElementTypeAcronym(elementType) {
688
- const type = Object.values(ELEMENT_TYPES).find((t) => t.value === elementType);
689
- return type?.acronym || elementType.substring(0, 2).toUpperCase();
690
- }
691
396
  function generateStructuredData(content, elementType, contributorId, sourceUrl) {
692
397
  return {
693
398
  "@context": "https://schema.org",
@@ -819,7 +524,7 @@ const styles$1 = `
819
524
  color: var(--skhema-text);
820
525
  }
821
526
 
822
- /* Dark mode */
527
+ /* Dark mode (skeleton/error only) */
823
528
  .skhema-component-card[data-theme="dark"],
824
529
  .skhema-skeleton[data-theme="dark"] {
825
530
  --skhema-bg: hsl(222.2 84% 4.9%);
@@ -831,106 +536,40 @@ const styles$1 = `
831
536
  --skhema-surface-subtle: hsl(217.2 32.6% 12%);
832
537
  }
833
538
 
834
- /* Main card */
835
- .skhema-component-card {
836
- position: relative;
837
- background: var(--skhema-card);
838
- border: 1px solid var(--skhema-border);
839
- border-radius: calc(var(--skhema-radius) + 2px);
840
- padding: 0;
841
- box-shadow: var(--skhema-shadow);
842
- transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1);
843
- max-width: 600px;
844
- margin: 8px 0;
845
- overflow: hidden;
539
+ /* Progressive enhancement over the inline-styled, rendered card. Inline styles
540
+ win on specificity, so the changed properties use !important to layer on. */
541
+ .skhema-card {
542
+ transition: box-shadow 0.2s cubic-bezier(0.4, 0, 0.2, 1), transform 0.2s cubic-bezier(0.4, 0, 0.2, 1);
846
543
  }
847
544
 
848
- .skhema-component-card:hover {
849
- box-shadow: var(--skhema-shadow-lg);
545
+ .skhema-card:hover {
546
+ box-shadow: ${CARD_SHADOW_LG} !important;
850
547
  transform: translateY(-1px);
851
548
  }
852
549
 
853
- /* Colored top border */
854
- .skhema-top-border {
855
- height: 2px;
856
- width: 100%;
857
- }
858
-
859
- /* Header */
860
- .skhema-header {
861
- display: flex;
862
- align-items: center;
863
- gap: 8px;
864
- padding: 12px 16px;
865
- border-bottom: 1px solid var(--skhema-border);
866
- }
867
-
868
- .skhema-type-label {
869
- font-size: 13px;
870
- font-weight: 500;
871
- color: var(--skhema-text-muted);
872
- }
873
-
874
- .skhema-title-separator {
875
- color: var(--skhema-text-muted);
876
- font-size: 13px;
877
- }
878
-
879
- .skhema-title {
880
- font-size: 13px;
881
- font-weight: 600;
882
- color: var(--skhema-text);
883
- }
884
-
885
- /* Elements section */
886
- .skhema-elements {
887
- padding: 16px;
888
- display: flex;
889
- flex-direction: column;
890
- gap: 16px;
891
- }
892
-
893
- /* Element group */
894
- .skhema-element-group {
895
- display: flex;
896
- flex-direction: column;
897
- gap: 4px;
898
- }
899
-
900
- .skhema-element-label {
901
- text-transform: uppercase;
902
- letter-spacing: 0.05em;
903
- font-family: ui-monospace, SFMono-Regular, 'SF Mono', Menlo, Consolas, monospace;
904
- font-size: 10px;
905
- font-weight: 600;
906
- color: var(--skhema-text-muted);
550
+ .skhema-save-btn {
551
+ transition: background 0.15s ease;
907
552
  }
908
553
 
909
- .skhema-element-content {
910
- font-size: 14px;
911
- line-height: 1.6;
912
- color: var(--skhema-text);
913
- padding-left: 8px;
914
- border-left: 2px solid var(--skhema-border);
915
- word-wrap: break-word;
916
- overflow-wrap: break-word;
554
+ .skhema-save-btn:hover {
555
+ background: ${PRIMARY_HOVER_HEX} !important;
917
556
  }
918
557
 
919
- /* Footer */
920
- .skhema-footer {
921
- padding: 12px 16px;
922
- border-top: 1px solid var(--skhema-border);
558
+ .skhema-save-btn:active {
559
+ background: ${PRIMARY_PRESSED_HEX} !important;
923
560
  }
924
561
 
925
- .skhema-footer-row {
926
- display: flex;
927
- align-items: center;
928
- justify-content: space-between;
929
- gap: 12px;
562
+ /* Error card wrapper (browser-only state) */
563
+ .skhema-component-card {
564
+ background: var(--skhema-card);
565
+ border: 1px solid var(--skhema-border);
566
+ border-radius: calc(var(--skhema-radius) + 2px);
567
+ box-shadow: var(--skhema-shadow);
568
+ max-width: 600px;
569
+ margin: 8px 0;
570
+ overflow: hidden;
930
571
  }
931
572
 
932
- ${SHARED_CARD_STYLES}
933
-
934
573
  /* Error state */
935
574
  .skhema-error {
936
575
  background: hsl(0 93% 94%);
@@ -1015,39 +654,9 @@ ${SHARED_CARD_STYLES}
1015
654
  100% { background-position: 200% 0; }
1016
655
  }
1017
656
 
1018
- /* Responsive */
1019
- @media (max-width: 640px) {
1020
- .skhema-component-card {
1021
- margin: 4px 0;
1022
- }
1023
-
1024
- .skhema-header {
1025
- padding: 10px 12px;
1026
- flex-wrap: wrap;
1027
- }
1028
-
1029
- .skhema-elements {
1030
- padding: 12px;
1031
- }
1032
-
1033
- .skhema-footer {
1034
- padding: 10px 12px;
1035
- }
1036
-
1037
- .skhema-footer-row {
1038
- flex-direction: column;
1039
- align-items: stretch;
1040
- gap: 8px;
1041
- }
1042
-
1043
- .skhema-save-btn {
1044
- justify-content: center;
1045
- }
1046
- }
1047
-
1048
657
  /* Accessibility */
1049
658
  @media (prefers-reduced-motion: reduce) {
1050
- .skhema-component-card,
659
+ .skhema-card,
1051
660
  .skhema-save-btn {
1052
661
  transition: none;
1053
662
  }
@@ -1213,9 +822,6 @@ class SkhemaComponent extends HTMLElement {
1213
822
  if (!this.contentData) return;
1214
823
  const { component_type, contributor_id, title } = this.contentData;
1215
824
  const componentLabel = getComponentTypeLabel(component_type);
1216
- const acronym = getComponentTypeAcronym(component_type);
1217
- const colors = COMPONENT_COLORS[component_type];
1218
- const authorFooterHtml = resolveAuthorFooterHtml(this, contributor_id);
1219
825
  const themeAttribute = this.getAttribute("theme") || "auto";
1220
826
  const actualTheme = this.getActualTheme(themeAttribute);
1221
827
  const redirectUrl = generateComponentRedirectUrl(
@@ -1223,65 +829,25 @@ class SkhemaComponent extends HTMLElement {
1223
829
  component_type,
1224
830
  contributor_id
1225
831
  );
1226
- const badgeStyle = colors ? `background: ${colors.bg}; color: ${colors.text}; border: 1px solid ${colors.border};` : "";
1227
- const topBorderColor = colors ? colors.text : "var(--skhema-primary)";
1228
- const groupedElements = this.groupElementsByType(elements);
1229
- const elementSectionsHtml = groupedElements.map(
1230
- (group) => `
1231
- <div class="skhema-element-group">
1232
- <div class="skhema-element-label">${getElementTypeLabel(group.elementType)}</div>
1233
- ${group.items.map(
1234
- (item) => `<div class="skhema-element-content">${sanitizeContent(item.content)}</div>`
1235
- ).join("")}
1236
- </div>
1237
- `
1238
- ).join("");
1239
832
  this.setAttribute("role", "article");
1240
833
  this.setAttribute(
1241
834
  "aria-label",
1242
835
  `${componentLabel} component${title ? ` — ${title}` : ""}`
1243
836
  );
1244
- const titleHtml = title ? `<span class="skhema-title-separator">&mdash;</span><span class="skhema-title">${title}</span>` : "";
1245
- this.shadow.innerHTML = `
1246
- <style>${styles$1}</style>
1247
-
1248
- <div class="skhema-component-card" data-theme="${actualTheme}">
1249
- <div class="skhema-top-border" style="background: ${topBorderColor};"></div>
1250
-
1251
- <div class="skhema-header">
1252
- <span class="skhema-acronym-badge" style="${badgeStyle}" title="${componentLabel}">
1253
- ${acronym}
1254
- </span>
1255
- <span class="skhema-type-label">${componentLabel}</span>
1256
- ${titleHtml}
1257
- </div>
1258
-
1259
- <div class="skhema-elements">
1260
- ${elementSectionsHtml}
1261
- </div>
1262
-
1263
- <div class="skhema-footer">
1264
- <div class="skhema-footer-row">
1265
- <span class="skhema-contributor-line">
1266
- ${USER_ICON_SVG}
1267
- ${authorFooterHtml}
1268
- </span>
1269
- <a href="${redirectUrl}"
1270
- class="skhema-save-btn"
1271
- target="_blank"
1272
- rel="noopener noreferrer"
1273
- title="Save this component to Skhema">
1274
- Save to Skhema
1275
- </a>
1276
- </div>
1277
- <div class="skhema-footer-attribution">
1278
- Strategy powered by <a href="https://skhema.com" target="_blank" rel="noopener noreferrer">Skhema</a>
1279
- </div>
1280
- </div>
1281
- </div>
1282
-
1283
- <slot style="display:none;"></slot>
1284
- `;
837
+ const cardHtml = renderComponentCardHtml({
838
+ componentType: component_type,
839
+ title,
840
+ elements: elements.map((el) => ({
841
+ elementType: el.elementType,
842
+ content: el.content
843
+ })),
844
+ saveUrl: redirectUrl,
845
+ authorName: this.getAttribute("author-name") || this.getAttribute("contributor-name"),
846
+ authorSlug: this.getAttribute("author-slug"),
847
+ contributorId: contributor_id,
848
+ theme: actualTheme
849
+ });
850
+ this.shadow.innerHTML = `<style>${styles$1}</style>${cardHtml}<slot style="display:none;"></slot>`;
1285
851
  const saveBtn = this.shadow.querySelector(
1286
852
  ".skhema-save-btn"
1287
853
  );
@@ -1291,21 +857,6 @@ class SkhemaComponent extends HTMLElement {
1291
857
  });
1292
858
  }
1293
859
  }
1294
- groupElementsByType(elements) {
1295
- const groups = /* @__PURE__ */ new Map();
1296
- for (const el of elements) {
1297
- const existing = groups.get(el.elementType);
1298
- if (existing) {
1299
- existing.push(el);
1300
- } else {
1301
- groups.set(el.elementType, [el]);
1302
- }
1303
- }
1304
- return Array.from(groups.entries()).map(([elementType, items]) => ({
1305
- elementType,
1306
- items
1307
- }));
1308
- }
1309
860
  getActualTheme(themeAttribute) {
1310
861
  if (themeAttribute === "light" || themeAttribute === "dark") {
1311
862
  return themeAttribute;
@@ -1493,12 +1044,8 @@ class SkhemaComponent extends HTMLElement {
1493
1044
  }
1494
1045
  updateTheme() {
1495
1046
  const themeAttribute = this.getAttribute("theme") || "auto";
1496
- if (themeAttribute === "auto") {
1497
- const card = this.shadow.querySelector(".skhema-component-card");
1498
- if (card) {
1499
- const newTheme = this.getActualTheme("auto");
1500
- card.setAttribute("data-theme", newTheme);
1501
- }
1047
+ if (themeAttribute === "auto" && this.contentData) {
1048
+ this.scheduleRender();
1502
1049
  }
1503
1050
  }
1504
1051
  }
@@ -1530,7 +1077,7 @@ const styles = `
1530
1077
  color: var(--skhema-text);
1531
1078
  }
1532
1079
 
1533
- /* Dark mode styles - applied via data-theme attribute */
1080
+ /* Dark mode styles - applied via data-theme attribute (skeleton/error only) */
1534
1081
  .skhema-element-card[data-theme="dark"],
1535
1082
  .skhema-skeleton[data-theme="dark"] {
1536
1083
  --skhema-bg: hsl(222.2 84% 4.9%);
@@ -1541,71 +1088,42 @@ const styles = `
1541
1088
  --skhema-accent: hsl(217.2 32.6% 17.5%);
1542
1089
  }
1543
1090
 
1544
- /* Main component card */
1545
- .skhema-element-card {
1546
- position: relative;
1547
- background: var(--skhema-card);
1548
- border: 1px solid var(--skhema-border);
1549
- border-radius: calc(var(--skhema-radius) + 2px);
1550
- padding: 0;
1551
- box-shadow: var(--skhema-shadow);
1552
- transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1);
1553
- max-width: 600px;
1554
- margin: 8px 0;
1555
- overflow: hidden;
1091
+ /* Progressive enhancement over the inline-styled, rendered card. Inline styles
1092
+ win on specificity, so the changed properties use !important to layer on. */
1093
+ .skhema-card {
1094
+ 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);
1556
1095
  }
1557
1096
 
1558
- .skhema-element-card:hover {
1559
- box-shadow: var(--skhema-shadow-lg);
1560
- border-color: var(--skhema-primary);
1097
+ .skhema-card:hover {
1098
+ box-shadow: ${CARD_SHADOW_LG} !important;
1561
1099
  transform: translateY(-1px);
1562
1100
  }
1563
1101
 
1564
- /* Header section with acronym badge + element type label */
1565
- .skhema-header {
1566
- display: flex;
1567
- align-items: center;
1568
- gap: 8px;
1569
- padding: 12px 16px;
1570
- border-bottom: 1px solid var(--skhema-border);
1571
- }
1572
-
1573
- /* Content section */
1574
- .skhema-content {
1575
- padding: 16px;
1102
+ .skhema-card[data-skhema-kind="element"]:hover {
1103
+ border-color: ${PRIMARY_HEX} !important;
1576
1104
  }
1577
1105
 
1578
- .skhema-content-text {
1579
- font-size: 15px;
1580
- line-height: 1.6;
1581
- color: var(--skhema-text);
1582
- margin: 0;
1583
- word-wrap: break-word;
1584
- overflow-wrap: break-word;
1585
- hyphens: auto;
1586
- max-width: 100%;
1106
+ .skhema-save-btn {
1107
+ transition: background 0.15s ease;
1587
1108
  }
1588
1109
 
1589
- /* Footer section */
1590
- .skhema-footer {
1591
- padding: 12px 16px;
1592
- border-top: 1px solid var(--skhema-border);
1110
+ .skhema-save-btn:hover {
1111
+ background: ${PRIMARY_HOVER_HEX} !important;
1593
1112
  }
1594
1113
 
1595
- .skhema-footer-row {
1596
- display: flex;
1597
- align-items: center;
1598
- justify-content: space-between;
1599
- gap: 12px;
1114
+ .skhema-save-btn:active {
1115
+ background: ${PRIMARY_PRESSED_HEX} !important;
1600
1116
  }
1601
1117
 
1602
- ${SHARED_CARD_STYLES}
1603
-
1604
- /* Element type label next to badge */
1605
- .skhema-type-label {
1606
- font-size: 13px;
1607
- font-weight: 500;
1608
- color: var(--skhema-text);
1118
+ /* Error card wrapper (browser-only state) */
1119
+ .skhema-element-card {
1120
+ background: var(--skhema-card);
1121
+ border: 1px solid var(--skhema-border);
1122
+ border-radius: calc(var(--skhema-radius) + 2px);
1123
+ box-shadow: var(--skhema-shadow);
1124
+ max-width: 600px;
1125
+ margin: 8px 0;
1126
+ overflow: hidden;
1609
1127
  }
1610
1128
 
1611
1129
  /* Error state */
@@ -1705,49 +1223,12 @@ ${SHARED_CARD_STYLES}
1705
1223
  }
1706
1224
  }
1707
1225
 
1708
- /* Responsive design */
1709
- @media (max-width: 640px) {
1710
- .skhema-element-card {
1711
- margin: 4px 0;
1712
- }
1713
-
1714
- .skhema-header {
1715
- padding: 10px 12px;
1716
- }
1717
-
1718
- .skhema-content {
1719
- padding: 12px;
1720
- }
1721
-
1722
- .skhema-footer {
1723
- padding: 10px 12px;
1724
- }
1725
-
1726
- .skhema-footer-row {
1727
- flex-direction: column;
1728
- align-items: stretch;
1729
- gap: 8px;
1730
- }
1731
-
1732
- .skhema-save-btn {
1733
- justify-content: center;
1734
- }
1735
- }
1736
-
1737
1226
  /* Accessibility */
1738
1227
  @media (prefers-reduced-motion: reduce) {
1739
- .skhema-element-card,
1228
+ .skhema-card,
1740
1229
  .skhema-save-btn {
1741
1230
  transition: none;
1742
1231
  }
1743
-
1744
- .skhema-save-btn::after {
1745
- transition: none;
1746
- }
1747
-
1748
- .skhema-save-btn:hover::after {
1749
- transform: none;
1750
- }
1751
1232
  }
1752
1233
 
1753
1234
  .skhema-structured-data {
@@ -1885,7 +1366,6 @@ class SkhemaElement extends HTMLElement {
1885
1366
  renderContent() {
1886
1367
  if (!this.contentData) return;
1887
1368
  const { element_type, contributor_id, content } = this.contentData;
1888
- const label = getElementTypeLabel(element_type);
1889
1369
  const redirectUrl = generateRedirectUrl(
1890
1370
  content,
1891
1371
  element_type,
@@ -1893,50 +1373,20 @@ class SkhemaElement extends HTMLElement {
1893
1373
  );
1894
1374
  const themeAttribute = this.getAttribute("theme") || "auto";
1895
1375
  const actualTheme = this.getActualTheme(themeAttribute);
1896
- const authorFooterHtml = resolveAuthorFooterHtml(this, contributor_id);
1897
- const componentType = resolveComponentType(element_type);
1898
- const acronym = getComponentTypeAcronym(componentType);
1899
- const colors = COMPONENT_COLORS[componentType];
1900
1376
  const ariaAttrs = createAriaAttributes(element_type);
1901
1377
  Object.entries(ariaAttrs).forEach(([key, value]) => {
1902
1378
  this.setAttribute(key, value);
1903
1379
  });
1904
- const badgeStyle = colors ? `background: ${colors.bg}; color: ${colors.text}; border: 1px solid ${colors.border};` : "";
1905
- this.shadow.innerHTML = `
1906
- <style>${styles}</style>
1907
-
1908
- <div class="skhema-element-card" data-theme="${actualTheme}">
1909
- <div class="skhema-header">
1910
- <span class="skhema-acronym-badge" style="${badgeStyle}" title="${label}">
1911
- ${acronym}
1912
- </span>
1913
- <span class="skhema-type-label">${label}</span>
1914
- </div>
1915
-
1916
- <div class="skhema-content">
1917
- <div class="skhema-content-text">${sanitizeContent(content)}</div>
1918
- </div>
1919
-
1920
- <div class="skhema-footer">
1921
- <div class="skhema-footer-row">
1922
- <span class="skhema-contributor-line">
1923
- ${USER_ICON_SVG}
1924
- ${authorFooterHtml}
1925
- </span>
1926
- <a href="${redirectUrl}"
1927
- class="skhema-save-btn"
1928
- target="_blank"
1929
- rel="noopener noreferrer"
1930
- title="Save this element to Skhema">
1931
- Save to Skhema
1932
- </a>
1933
- </div>
1934
- <div class="skhema-footer-attribution">
1935
- Strategy powered by <a href="https://skhema.com" target="_blank" rel="noopener noreferrer">Skhema</a>
1936
- </div>
1937
- </div>
1938
- </div>
1939
- `;
1380
+ const cardHtml = renderElementCardHtml({
1381
+ elementType: element_type,
1382
+ content,
1383
+ saveUrl: redirectUrl,
1384
+ authorName: this.getAttribute("author-name") || this.getAttribute("contributor-name"),
1385
+ authorSlug: this.getAttribute("author-slug"),
1386
+ contributorId: contributor_id,
1387
+ theme: actualTheme
1388
+ });
1389
+ this.shadow.innerHTML = `<style>${styles}</style>${cardHtml}`;
1940
1390
  const saveBtn = this.shadow.querySelector(
1941
1391
  ".skhema-save-btn"
1942
1392
  );
@@ -2124,12 +1574,8 @@ class SkhemaElement extends HTMLElement {
2124
1574
  }
2125
1575
  updateTheme() {
2126
1576
  const themeAttribute = this.getAttribute("theme") || "auto";
2127
- if (themeAttribute === "auto") {
2128
- const card = this.shadow.querySelector(".skhema-element-card");
2129
- if (card) {
2130
- const newTheme = this.getActualTheme("auto");
2131
- card.setAttribute("data-theme", newTheme);
2132
- }
1577
+ if (themeAttribute === "auto" && this.contentData && !this.nestedMode) {
1578
+ this.renderContent();
2133
1579
  }
2134
1580
  }
2135
1581
  }
@@ -2172,15 +1618,15 @@ export {
2172
1618
  generateComponentStructuredData,
2173
1619
  generateRedirectUrl,
2174
1620
  generateStructuredData,
2175
- getComponentTypeAcronym,
1621
+ g as getComponentTypeAcronym,
2176
1622
  getComponentTypeLabel,
2177
- getElementTypeAcronym,
1623
+ c as getElementTypeAcronym,
2178
1624
  getElementTypeLabel,
2179
1625
  isValidComponentType,
2180
- isValidElementType,
1626
+ f as isValidElementType,
2181
1627
  registerSkhemaComponent,
2182
1628
  registerSkhemaElement,
2183
- resolveComponentType,
1629
+ r as resolveComponentType,
2184
1630
  shouldTrackAnalytics,
2185
1631
  trackComponentClick,
2186
1632
  trackComponentEmbedLoad,