@skhema/embed 0.1.9 → 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 +79 -440
  12. package/dist/index.cjs.map +1 -1
  13. package/dist/index.es.js +81 -442
  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 +1 -1
  25. package/dist/tokens.d.ts +1 -1
  26. package/dist/tokens.d.ts.map +1 -1
  27. package/dist/tokens.es.js +1 -1
  28. package/dist/utils/color.d.ts +25 -0
  29. package/dist/utils/color.d.ts.map +1 -0
  30. package/dist/utils/sanitization.d.ts +7 -1
  31. package/dist/utils/sanitization.d.ts.map +1 -1
  32. package/dist/{validation-CNWaQfh_.cjs → validation-DCw9rQeH.cjs} +40 -95
  33. package/dist/validation-DCw9rQeH.cjs.map +1 -0
  34. package/dist/{validation-MSiRF5Wt.js → validation-Dt4L_df8.js} +42 -97
  35. package/dist/validation-Dt4L_df8.js.map +1 -0
  36. package/package.json +11 -4
  37. package/dist/utils/author-attribution.d.ts +0 -16
  38. package/dist/utils/author-attribution.d.ts.map +0 -1
  39. package/dist/validation-CNWaQfh_.cjs.map +0 -1
  40. package/dist/validation-MSiRF5Wt.js.map +0 -1
package/dist/index.cjs CHANGED
@@ -1,40 +1,7 @@
1
1
  "use strict";
2
2
  Object.defineProperties(exports, { __esModule: { value: true }, [Symbol.toStringTag]: { value: "Module" } });
3
- const validation = require("./validation-CNWaQfh_.cjs");
4
- function readAuthorAttribution(element) {
5
- const authorName = element.getAttribute("author-name") || element.getAttribute("contributor-name");
6
- if (!authorName?.trim()) {
7
- return null;
8
- }
9
- const authorSlug = element.getAttribute("author-slug")?.trim();
10
- return {
11
- authorName: authorName.trim(),
12
- authorSlug: authorSlug || void 0
13
- };
14
- }
15
- function escapeHtml(text) {
16
- return text.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
17
- }
18
- function formatAuthorAttributionHtml(authorName, authorSlug) {
19
- const label = `By ${escapeHtml(authorName)}`;
20
- if (authorSlug) {
21
- return `<a href="https://skhema.com/contributors/${encodeURIComponent(authorSlug)}" class="skhema-author-link" target="_blank" rel="noopener noreferrer">${label}</a>`;
22
- }
23
- return `<span class="skhema-author-text">${label}</span>`;
24
- }
25
- function formatContributorIdFallback(contributorId) {
26
- return contributorId.split(/[_-]/).map((word) => word.charAt(0).toUpperCase() + word.slice(1).toLowerCase()).join(" ");
27
- }
28
- function resolveAuthorFooterHtml(element, contributorId) {
29
- const attribution = readAuthorAttribution(element);
30
- if (attribution) {
31
- return formatAuthorAttributionHtml(
32
- attribution.authorName,
33
- attribution.authorSlug
34
- );
35
- }
36
- return `<span class="skhema-author-text">By ${escapeHtml(formatContributorIdFallback(contributorId))}</span>`;
37
- }
3
+ const render = require("./index-DBeyneZT.cjs");
4
+ const validation = require("./validation-DCw9rQeH.cjs");
38
5
  const ANALYTICS_ENDPOINT = "https://analytics.skhema.com/functions/v1/embed-manage";
39
6
  const TRACKING_COOKIE_NAME = "_sk";
40
7
  const TRACKING_EXPIRY_HOURS = 24;
@@ -427,75 +394,6 @@ function generateComponentHash(elements) {
427
394
  const combined = sorted.map((e) => `${e.elementType}:${e.content}`).join("|");
428
395
  return generateContentHash(combined);
429
396
  }
430
- function sanitizeContent(content) {
431
- const htmlEncode = (str) => {
432
- const div = document.createElement("div");
433
- div.textContent = str;
434
- return div.innerHTML;
435
- };
436
- let sanitized = stripUrls(content);
437
- sanitized = htmlEncode(sanitized);
438
- sanitized = sanitized.replace(/\n/g, "<br>");
439
- sanitized = applyTextWrapping(sanitized);
440
- return sanitized;
441
- }
442
- function stripUrls(text) {
443
- const patterns = [
444
- // Standard URLs with protocols
445
- /https?:\/\/[^\s<>"{}|\\^`[\]]+/gi,
446
- // FTP URLs
447
- /ftp:\/\/[^\s<>"{}|\\^`[\]]+/gi,
448
- // URLs without protocol but with www
449
- /www\.[^\s<>"{}|\\^`[\]]+/gi,
450
- // Email-like patterns
451
- /[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}/gi,
452
- // Common domain patterns (anything.com, anything.org, etc.)
453
- /(?:^|\s)([a-zA-Z0-9-]+\.)+[a-zA-Z]{2,}(?:\/[^\s]*)?/gi
454
- ];
455
- let stripped = text;
456
- patterns.forEach((pattern) => {
457
- stripped = stripped.replace(pattern, "");
458
- });
459
- stripped = stripped.replace(/\s+/g, " ").trim();
460
- return stripped;
461
- }
462
- function applyTextWrapping(text) {
463
- const words = text.split(/(\s+)/);
464
- return words.map((word) => {
465
- if (/^\s+$/.test(word) || word.includes("<")) {
466
- return word;
467
- }
468
- if (word.length > 30) {
469
- return word.replace(/(.{10})/g, "$1​");
470
- }
471
- return word;
472
- }).join("");
473
- }
474
- function validateContentSecurity(content) {
475
- const issues = [];
476
- if (/<script[\s>]/i.test(content)) {
477
- issues.push("Script tags detected");
478
- }
479
- if (/on\w+\s*=/i.test(content)) {
480
- issues.push("Event handlers detected");
481
- }
482
- if (/javascript:/i.test(content)) {
483
- issues.push("JavaScript protocol detected");
484
- }
485
- if (/data:[^,]*script/i.test(content)) {
486
- issues.push("Data URL with script detected");
487
- }
488
- if (/<iframe[\s>]/i.test(content)) {
489
- issues.push("Iframe tags detected");
490
- }
491
- if (/https?:\/\//i.test(content) || /www\./i.test(content)) {
492
- issues.push("URLs detected in content");
493
- }
494
- return {
495
- isSecure: issues.length === 0,
496
- issues
497
- };
498
- }
499
397
  function generateStructuredData(content, elementType, contributorId, sourceUrl) {
500
398
  return {
501
399
  "@context": "https://schema.org",
@@ -627,7 +525,7 @@ const styles$1 = `
627
525
  color: var(--skhema-text);
628
526
  }
629
527
 
630
- /* Dark mode */
528
+ /* Dark mode (skeleton/error only) */
631
529
  .skhema-component-card[data-theme="dark"],
632
530
  .skhema-skeleton[data-theme="dark"] {
633
531
  --skhema-bg: hsl(222.2 84% 4.9%);
@@ -639,106 +537,40 @@ const styles$1 = `
639
537
  --skhema-surface-subtle: hsl(217.2 32.6% 12%);
640
538
  }
641
539
 
642
- /* Main card */
643
- .skhema-component-card {
644
- position: relative;
645
- background: var(--skhema-card);
646
- border: 1px solid var(--skhema-border);
647
- border-radius: calc(var(--skhema-radius) + 2px);
648
- padding: 0;
649
- box-shadow: var(--skhema-shadow);
650
- transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1);
651
- max-width: 600px;
652
- margin: 8px 0;
653
- 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);
654
544
  }
655
545
 
656
- .skhema-component-card:hover {
657
- box-shadow: var(--skhema-shadow-lg);
546
+ .skhema-card:hover {
547
+ box-shadow: ${validation.CARD_SHADOW_LG} !important;
658
548
  transform: translateY(-1px);
659
549
  }
660
550
 
661
- /* Colored top border */
662
- .skhema-top-border {
663
- height: 2px;
664
- width: 100%;
665
- }
666
-
667
- /* Header */
668
- .skhema-header {
669
- display: flex;
670
- align-items: center;
671
- gap: 8px;
672
- padding: 12px 16px;
673
- border-bottom: 1px solid var(--skhema-border);
674
- }
675
-
676
- .skhema-type-label {
677
- font-size: 13px;
678
- font-weight: 500;
679
- color: var(--skhema-text-muted);
680
- }
681
-
682
- .skhema-title-separator {
683
- color: var(--skhema-text-muted);
684
- font-size: 13px;
685
- }
686
-
687
- .skhema-title {
688
- font-size: 13px;
689
- font-weight: 600;
690
- color: var(--skhema-text);
691
- }
692
-
693
- /* Elements section */
694
- .skhema-elements {
695
- padding: 16px;
696
- display: flex;
697
- flex-direction: column;
698
- gap: 16px;
699
- }
700
-
701
- /* Element group */
702
- .skhema-element-group {
703
- display: flex;
704
- flex-direction: column;
705
- gap: 4px;
706
- }
707
-
708
- .skhema-element-label {
709
- text-transform: uppercase;
710
- letter-spacing: 0.05em;
711
- font-family: ui-monospace, SFMono-Regular, 'SF Mono', Menlo, Consolas, monospace;
712
- font-size: 10px;
713
- font-weight: 600;
714
- color: var(--skhema-text-muted);
551
+ .skhema-save-btn {
552
+ transition: background 0.15s ease;
715
553
  }
716
554
 
717
- .skhema-element-content {
718
- font-size: 14px;
719
- line-height: 1.6;
720
- color: var(--skhema-text);
721
- padding-left: 8px;
722
- border-left: 2px solid var(--skhema-border);
723
- word-wrap: break-word;
724
- overflow-wrap: break-word;
555
+ .skhema-save-btn:hover {
556
+ background: ${validation.PRIMARY_HOVER_HEX} !important;
725
557
  }
726
558
 
727
- /* Footer */
728
- .skhema-footer {
729
- padding: 12px 16px;
730
- border-top: 1px solid var(--skhema-border);
559
+ .skhema-save-btn:active {
560
+ background: ${validation.PRIMARY_PRESSED_HEX} !important;
731
561
  }
732
562
 
733
- .skhema-footer-row {
734
- display: flex;
735
- align-items: center;
736
- justify-content: space-between;
737
- 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;
738
572
  }
739
573
 
740
- ${validation.SHARED_CARD_STYLES}
741
-
742
574
  /* Error state */
743
575
  .skhema-error {
744
576
  background: hsl(0 93% 94%);
@@ -823,39 +655,9 @@ ${validation.SHARED_CARD_STYLES}
823
655
  100% { background-position: 200% 0; }
824
656
  }
825
657
 
826
- /* Responsive */
827
- @media (max-width: 640px) {
828
- .skhema-component-card {
829
- margin: 4px 0;
830
- }
831
-
832
- .skhema-header {
833
- padding: 10px 12px;
834
- flex-wrap: wrap;
835
- }
836
-
837
- .skhema-elements {
838
- padding: 12px;
839
- }
840
-
841
- .skhema-footer {
842
- padding: 10px 12px;
843
- }
844
-
845
- .skhema-footer-row {
846
- flex-direction: column;
847
- align-items: stretch;
848
- gap: 8px;
849
- }
850
-
851
- .skhema-save-btn {
852
- justify-content: center;
853
- }
854
- }
855
-
856
658
  /* Accessibility */
857
659
  @media (prefers-reduced-motion: reduce) {
858
- .skhema-component-card,
660
+ .skhema-card,
859
661
  .skhema-save-btn {
860
662
  transition: none;
861
663
  }
@@ -1021,9 +823,6 @@ class SkhemaComponent extends HTMLElement {
1021
823
  if (!this.contentData) return;
1022
824
  const { component_type, contributor_id, title } = this.contentData;
1023
825
  const componentLabel = validation.getComponentTypeLabel(component_type);
1024
- const acronym = validation.getComponentTypeAcronym(component_type);
1025
- const colors = validation.COMPONENT_COLORS[component_type];
1026
- const authorFooterHtml = resolveAuthorFooterHtml(this, contributor_id);
1027
826
  const themeAttribute = this.getAttribute("theme") || "auto";
1028
827
  const actualTheme = this.getActualTheme(themeAttribute);
1029
828
  const redirectUrl = generateComponentRedirectUrl(
@@ -1031,65 +830,25 @@ class SkhemaComponent extends HTMLElement {
1031
830
  component_type,
1032
831
  contributor_id
1033
832
  );
1034
- const badgeStyle = colors ? `background: ${colors.bg}; color: ${colors.text}; border: 1px solid ${colors.border};` : "";
1035
- const topBorderColor = colors ? colors.text : "var(--skhema-primary)";
1036
- const groupedElements = this.groupElementsByType(elements);
1037
- const elementSectionsHtml = groupedElements.map(
1038
- (group) => `
1039
- <div class="skhema-element-group">
1040
- <div class="skhema-element-label">${validation.getElementTypeLabel(group.elementType)}</div>
1041
- ${group.items.map(
1042
- (item) => `<div class="skhema-element-content">${sanitizeContent(item.content)}</div>`
1043
- ).join("")}
1044
- </div>
1045
- `
1046
- ).join("");
1047
833
  this.setAttribute("role", "article");
1048
834
  this.setAttribute(
1049
835
  "aria-label",
1050
836
  `${componentLabel} component${title ? ` — ${title}` : ""}`
1051
837
  );
1052
- const titleHtml = title ? `<span class="skhema-title-separator">&mdash;</span><span class="skhema-title">${title}</span>` : "";
1053
- this.shadow.innerHTML = `
1054
- <style>${styles$1}</style>
1055
-
1056
- <div class="skhema-component-card" data-theme="${actualTheme}">
1057
- <div class="skhema-top-border" style="background: ${topBorderColor};"></div>
1058
-
1059
- <div class="skhema-header">
1060
- <span class="skhema-acronym-badge" style="${badgeStyle}" title="${componentLabel}">
1061
- ${acronym}
1062
- </span>
1063
- <span class="skhema-type-label">${componentLabel}</span>
1064
- ${titleHtml}
1065
- </div>
1066
-
1067
- <div class="skhema-elements">
1068
- ${elementSectionsHtml}
1069
- </div>
1070
-
1071
- <div class="skhema-footer">
1072
- <div class="skhema-footer-row">
1073
- <span class="skhema-contributor-line">
1074
- ${validation.USER_ICON_SVG}
1075
- ${authorFooterHtml}
1076
- </span>
1077
- <a href="${redirectUrl}"
1078
- class="skhema-save-btn"
1079
- target="_blank"
1080
- rel="noopener noreferrer"
1081
- title="Save this component to Skhema">
1082
- Save to Skhema
1083
- </a>
1084
- </div>
1085
- <div class="skhema-footer-attribution">
1086
- Strategy powered by <a href="https://skhema.com" target="_blank" rel="noopener noreferrer">Skhema</a>
1087
- </div>
1088
- </div>
1089
- </div>
1090
-
1091
- <slot style="display:none;"></slot>
1092
- `;
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>`;
1093
852
  const saveBtn = this.shadow.querySelector(
1094
853
  ".skhema-save-btn"
1095
854
  );
@@ -1099,21 +858,6 @@ class SkhemaComponent extends HTMLElement {
1099
858
  });
1100
859
  }
1101
860
  }
1102
- groupElementsByType(elements) {
1103
- const groups = /* @__PURE__ */ new Map();
1104
- for (const el of elements) {
1105
- const existing = groups.get(el.elementType);
1106
- if (existing) {
1107
- existing.push(el);
1108
- } else {
1109
- groups.set(el.elementType, [el]);
1110
- }
1111
- }
1112
- return Array.from(groups.entries()).map(([elementType, items]) => ({
1113
- elementType,
1114
- items
1115
- }));
1116
- }
1117
861
  getActualTheme(themeAttribute) {
1118
862
  if (themeAttribute === "light" || themeAttribute === "dark") {
1119
863
  return themeAttribute;
@@ -1301,12 +1045,8 @@ class SkhemaComponent extends HTMLElement {
1301
1045
  }
1302
1046
  updateTheme() {
1303
1047
  const themeAttribute = this.getAttribute("theme") || "auto";
1304
- if (themeAttribute === "auto") {
1305
- const card = this.shadow.querySelector(".skhema-component-card");
1306
- if (card) {
1307
- const newTheme = this.getActualTheme("auto");
1308
- card.setAttribute("data-theme", newTheme);
1309
- }
1048
+ if (themeAttribute === "auto" && this.contentData) {
1049
+ this.scheduleRender();
1310
1050
  }
1311
1051
  }
1312
1052
  }
@@ -1338,7 +1078,7 @@ const styles = `
1338
1078
  color: var(--skhema-text);
1339
1079
  }
1340
1080
 
1341
- /* Dark mode styles - applied via data-theme attribute */
1081
+ /* Dark mode styles - applied via data-theme attribute (skeleton/error only) */
1342
1082
  .skhema-element-card[data-theme="dark"],
1343
1083
  .skhema-skeleton[data-theme="dark"] {
1344
1084
  --skhema-bg: hsl(222.2 84% 4.9%);
@@ -1349,71 +1089,42 @@ const styles = `
1349
1089
  --skhema-accent: hsl(217.2 32.6% 17.5%);
1350
1090
  }
1351
1091
 
1352
- /* Main component card */
1353
- .skhema-element-card {
1354
- position: relative;
1355
- background: var(--skhema-card);
1356
- border: 1px solid var(--skhema-border);
1357
- border-radius: calc(var(--skhema-radius) + 2px);
1358
- padding: 0;
1359
- box-shadow: var(--skhema-shadow);
1360
- transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1);
1361
- max-width: 600px;
1362
- margin: 8px 0;
1363
- 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);
1364
1096
  }
1365
1097
 
1366
- .skhema-element-card:hover {
1367
- box-shadow: var(--skhema-shadow-lg);
1368
- border-color: var(--skhema-primary);
1098
+ .skhema-card:hover {
1099
+ box-shadow: ${validation.CARD_SHADOW_LG} !important;
1369
1100
  transform: translateY(-1px);
1370
1101
  }
1371
1102
 
1372
- /* Header section with acronym badge + element type label */
1373
- .skhema-header {
1374
- display: flex;
1375
- align-items: center;
1376
- gap: 8px;
1377
- padding: 12px 16px;
1378
- border-bottom: 1px solid var(--skhema-border);
1379
- }
1380
-
1381
- /* Content section */
1382
- .skhema-content {
1383
- padding: 16px;
1103
+ .skhema-card[data-skhema-kind="element"]:hover {
1104
+ border-color: ${validation.PRIMARY_HEX} !important;
1384
1105
  }
1385
1106
 
1386
- .skhema-content-text {
1387
- font-size: 15px;
1388
- line-height: 1.6;
1389
- color: var(--skhema-text);
1390
- margin: 0;
1391
- word-wrap: break-word;
1392
- overflow-wrap: break-word;
1393
- hyphens: auto;
1394
- max-width: 100%;
1107
+ .skhema-save-btn {
1108
+ transition: background 0.15s ease;
1395
1109
  }
1396
1110
 
1397
- /* Footer section */
1398
- .skhema-footer {
1399
- padding: 12px 16px;
1400
- border-top: 1px solid var(--skhema-border);
1111
+ .skhema-save-btn:hover {
1112
+ background: ${validation.PRIMARY_HOVER_HEX} !important;
1401
1113
  }
1402
1114
 
1403
- .skhema-footer-row {
1404
- display: flex;
1405
- align-items: center;
1406
- justify-content: space-between;
1407
- gap: 12px;
1115
+ .skhema-save-btn:active {
1116
+ background: ${validation.PRIMARY_PRESSED_HEX} !important;
1408
1117
  }
1409
1118
 
1410
- ${validation.SHARED_CARD_STYLES}
1411
-
1412
- /* Element type label next to badge */
1413
- .skhema-type-label {
1414
- font-size: 13px;
1415
- font-weight: 500;
1416
- 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;
1417
1128
  }
1418
1129
 
1419
1130
  /* Error state */
@@ -1513,49 +1224,12 @@ ${validation.SHARED_CARD_STYLES}
1513
1224
  }
1514
1225
  }
1515
1226
 
1516
- /* Responsive design */
1517
- @media (max-width: 640px) {
1518
- .skhema-element-card {
1519
- margin: 4px 0;
1520
- }
1521
-
1522
- .skhema-header {
1523
- padding: 10px 12px;
1524
- }
1525
-
1526
- .skhema-content {
1527
- padding: 12px;
1528
- }
1529
-
1530
- .skhema-footer {
1531
- padding: 10px 12px;
1532
- }
1533
-
1534
- .skhema-footer-row {
1535
- flex-direction: column;
1536
- align-items: stretch;
1537
- gap: 8px;
1538
- }
1539
-
1540
- .skhema-save-btn {
1541
- justify-content: center;
1542
- }
1543
- }
1544
-
1545
1227
  /* Accessibility */
1546
1228
  @media (prefers-reduced-motion: reduce) {
1547
- .skhema-element-card,
1229
+ .skhema-card,
1548
1230
  .skhema-save-btn {
1549
1231
  transition: none;
1550
1232
  }
1551
-
1552
- .skhema-save-btn::after {
1553
- transition: none;
1554
- }
1555
-
1556
- .skhema-save-btn:hover::after {
1557
- transform: none;
1558
- }
1559
1233
  }
1560
1234
 
1561
1235
  .skhema-structured-data {
@@ -1667,7 +1341,7 @@ class SkhemaElement extends HTMLElement {
1667
1341
  ]);
1668
1342
  return;
1669
1343
  }
1670
- const securityValidation = validateContentSecurity(content);
1344
+ const securityValidation = render.validateContentSecurity(content);
1671
1345
  if (!securityValidation.isSecure) {
1672
1346
  this.renderError(
1673
1347
  "Content security validation failed",
@@ -1693,7 +1367,6 @@ class SkhemaElement extends HTMLElement {
1693
1367
  renderContent() {
1694
1368
  if (!this.contentData) return;
1695
1369
  const { element_type, contributor_id, content } = this.contentData;
1696
- const label = validation.getElementTypeLabel(element_type);
1697
1370
  const redirectUrl = generateRedirectUrl(
1698
1371
  content,
1699
1372
  element_type,
@@ -1701,50 +1374,20 @@ class SkhemaElement extends HTMLElement {
1701
1374
  );
1702
1375
  const themeAttribute = this.getAttribute("theme") || "auto";
1703
1376
  const actualTheme = this.getActualTheme(themeAttribute);
1704
- const authorFooterHtml = resolveAuthorFooterHtml(this, contributor_id);
1705
- const componentType = validation.resolveComponentType(element_type);
1706
- const acronym = validation.getComponentTypeAcronym(componentType);
1707
- const colors = validation.COMPONENT_COLORS[componentType];
1708
1377
  const ariaAttrs = createAriaAttributes(element_type);
1709
1378
  Object.entries(ariaAttrs).forEach(([key, value]) => {
1710
1379
  this.setAttribute(key, value);
1711
1380
  });
1712
- const badgeStyle = colors ? `background: ${colors.bg}; color: ${colors.text}; border: 1px solid ${colors.border};` : "";
1713
- this.shadow.innerHTML = `
1714
- <style>${styles}</style>
1715
-
1716
- <div class="skhema-element-card" data-theme="${actualTheme}">
1717
- <div class="skhema-header">
1718
- <span class="skhema-acronym-badge" style="${badgeStyle}" title="${label}">
1719
- ${acronym}
1720
- </span>
1721
- <span class="skhema-type-label">${label}</span>
1722
- </div>
1723
-
1724
- <div class="skhema-content">
1725
- <div class="skhema-content-text">${sanitizeContent(content)}</div>
1726
- </div>
1727
-
1728
- <div class="skhema-footer">
1729
- <div class="skhema-footer-row">
1730
- <span class="skhema-contributor-line">
1731
- ${validation.USER_ICON_SVG}
1732
- ${authorFooterHtml}
1733
- </span>
1734
- <a href="${redirectUrl}"
1735
- class="skhema-save-btn"
1736
- target="_blank"
1737
- rel="noopener noreferrer"
1738
- title="Save this element to Skhema">
1739
- Save to Skhema
1740
- </a>
1741
- </div>
1742
- <div class="skhema-footer-attribution">
1743
- Strategy powered by <a href="https://skhema.com" target="_blank" rel="noopener noreferrer">Skhema</a>
1744
- </div>
1745
- </div>
1746
- </div>
1747
- `;
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}`;
1748
1391
  const saveBtn = this.shadow.querySelector(
1749
1392
  ".skhema-save-btn"
1750
1393
  );
@@ -1932,12 +1575,8 @@ class SkhemaElement extends HTMLElement {
1932
1575
  }
1933
1576
  updateTheme() {
1934
1577
  const themeAttribute = this.getAttribute("theme") || "auto";
1935
- if (themeAttribute === "auto") {
1936
- const card = this.shadow.querySelector(".skhema-element-card");
1937
- if (card) {
1938
- const newTheme = this.getActualTheme("auto");
1939
- card.setAttribute("data-theme", newTheme);
1940
- }
1578
+ if (themeAttribute === "auto" && this.contentData && !this.nestedMode) {
1579
+ this.renderContent();
1941
1580
  }
1942
1581
  }
1943
1582
  }