@skhema/embed 0.1.9 → 0.1.11

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 (52) 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/components/types.d.ts +1 -1
  6. package/dist/components/types.d.ts.map +1 -1
  7. package/dist/embed.min.js +1 -1
  8. package/dist/embed.min.js.map +1 -1
  9. package/dist/index-B8ZF3snD.cjs +215 -0
  10. package/dist/index-B8ZF3snD.cjs.map +1 -0
  11. package/dist/index-nkGvTyQR.js +216 -0
  12. package/dist/index-nkGvTyQR.js.map +1 -0
  13. package/dist/index.cjs +113 -474
  14. package/dist/index.cjs.map +1 -1
  15. package/dist/index.es.js +88 -449
  16. package/dist/index.es.js.map +1 -1
  17. package/dist/method-schema.d.ts +244 -0
  18. package/dist/method-schema.d.ts.map +1 -0
  19. package/dist/render/index.d.ts +55 -0
  20. package/dist/render/index.d.ts.map +1 -0
  21. package/dist/render.cjs +7 -0
  22. package/dist/render.cjs.map +1 -0
  23. package/dist/render.d.ts +2 -0
  24. package/dist/render.es.js +7 -0
  25. package/dist/render.es.js.map +1 -0
  26. package/dist/styles/design-tokens.d.ts +32 -4
  27. package/dist/styles/design-tokens.d.ts.map +1 -1
  28. package/dist/tokens-BQf5zJ3j.js +333 -0
  29. package/dist/tokens-BQf5zJ3j.js.map +1 -0
  30. package/dist/tokens-BxpYMhDq.cjs +332 -0
  31. package/dist/tokens-BxpYMhDq.cjs.map +1 -0
  32. package/dist/tokens.cjs +13 -13
  33. package/dist/tokens.d.ts +1 -1
  34. package/dist/tokens.d.ts.map +1 -1
  35. package/dist/tokens.es.js +8 -8
  36. package/dist/utils/color.d.ts +25 -0
  37. package/dist/utils/color.d.ts.map +1 -0
  38. package/dist/utils/component-validation.d.ts +1 -1
  39. package/dist/utils/component-validation.d.ts.map +1 -1
  40. package/dist/utils/sanitization.d.ts +7 -1
  41. package/dist/utils/sanitization.d.ts.map +1 -1
  42. package/dist/utils/seo.d.ts +1 -1
  43. package/dist/utils/seo.d.ts.map +1 -1
  44. package/dist/utils/validation.d.ts +1 -1
  45. package/dist/utils/validation.d.ts.map +1 -1
  46. package/package.json +11 -7
  47. package/dist/utils/author-attribution.d.ts +0 -16
  48. package/dist/utils/author-attribution.d.ts.map +0 -1
  49. package/dist/validation-CNWaQfh_.cjs +0 -231
  50. package/dist/validation-CNWaQfh_.cjs.map +0 -1
  51. package/dist/validation-MSiRF5Wt.js +0 -232
  52. 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-B8ZF3snD.cjs");
4
+ const tokens = require("./tokens-BxpYMhDq.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,82 +394,13 @@ 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",
502
400
  "@type": "AnalysisContent",
503
401
  text: content,
504
402
  analysisType: elementType,
505
- category: validation.getElementTypeLabel(elementType),
403
+ category: tokens.getElementTypeLabel(elementType),
506
404
  contributor: contributorId,
507
405
  url: generateRedirectUrl(content, elementType, contributorId),
508
406
  provider: {
@@ -534,7 +432,7 @@ function generateRedirectUrl(content, elementType, contributorId, options = {})
534
432
  return `${baseUrl}?type=contributor&contributor_id=${contributorId}&element_type=${elementType}&content_hash=${contentHash}&${params.toString()}`;
535
433
  }
536
434
  function createMetaTags(content, elementType, contributorId) {
537
- const label = validation.getElementTypeLabel(elementType);
435
+ const label = tokens.getElementTypeLabel(elementType);
538
436
  return `
539
437
  <div itemscope itemtype="https://schema.org/AnalysisContent" style="display:none;">
540
438
  <meta itemprop="analysisType" content="${elementType}">
@@ -546,7 +444,7 @@ function createMetaTags(content, elementType, contributorId) {
546
444
  `;
547
445
  }
548
446
  function generateComponentStructuredData(title, componentType, contributorId, elements, sourceUrl) {
549
- const componentLabel = validation.getComponentTypeLabel(componentType);
447
+ const componentLabel = tokens.getComponentTypeLabel(componentType);
550
448
  return {
551
449
  "@context": "https://schema.org",
552
450
  "@type": "CreativeWork",
@@ -563,7 +461,7 @@ function generateComponentStructuredData(title, componentType, contributorId, el
563
461
  "@type": "AnalysisContent",
564
462
  text: el.content,
565
463
  analysisType: el.elementType,
566
- category: validation.getElementTypeLabel(el.elementType)
464
+ category: tokens.getElementTypeLabel(el.elementType)
567
465
  })),
568
466
  provider: {
569
467
  "@type": "Organization",
@@ -593,7 +491,7 @@ function generateComponentRedirectUrl(componentHash, componentType, contributorI
593
491
  return `${baseUrl}?type=component&component_type=${componentType}&component_hash=${componentHash}&contributor_id=${contributorId}&${params.toString()}`;
594
492
  }
595
493
  function createAriaAttributes(elementType) {
596
- const label = validation.getElementTypeLabel(elementType);
494
+ const label = tokens.getElementTypeLabel(elementType);
597
495
  return {
598
496
  role: "article",
599
497
  "aria-label": `${label} - Strategic element`,
@@ -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: ${tokens.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: ${tokens.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: ${tokens.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
  }
@@ -932,13 +734,13 @@ class SkhemaComponent extends HTMLElement {
932
734
  });
933
735
  }
934
736
  render() {
935
- const validation2 = this.validateComponentAttributes();
936
- if (!validation2.isValid) {
937
- this.renderError("Invalid component attributes", validation2.errors);
737
+ const validation = this.validateComponentAttributes();
738
+ if (!validation.isValid) {
739
+ this.renderError("Invalid component attributes", validation.errors);
938
740
  return;
939
741
  }
940
- const componentType = validation2.componentType;
941
- const contributorId = validation2.contributorId;
742
+ const componentType = validation.componentType;
743
+ const contributorId = validation.contributorId;
942
744
  const title = this.getAttribute("title") || "";
943
745
  const childElements = this.gatherChildElements(componentType);
944
746
  if (childElements.length === 0) {
@@ -977,7 +779,7 @@ class SkhemaComponent extends HTMLElement {
977
779
  const contributorId = this.getAttribute("contributor-id");
978
780
  if (!componentType) {
979
781
  errors.push("Missing required attribute: component-type");
980
- } else if (!validation.isValidComponentType(componentType)) {
782
+ } else if (!tokens.isValidComponentType(componentType)) {
981
783
  errors.push(`Invalid component-type "${componentType}"`);
982
784
  }
983
785
  if (!contributorId) {
@@ -988,7 +790,7 @@ class SkhemaComponent extends HTMLElement {
988
790
  return {
989
791
  isValid: errors.length === 0,
990
792
  errors,
991
- componentType: validation.isValidComponentType(componentType || "") ? componentType : void 0,
793
+ componentType: tokens.isValidComponentType(componentType || "") ? componentType : void 0,
992
794
  contributorId: contributorId || void 0
993
795
  };
994
796
  }
@@ -1000,14 +802,14 @@ class SkhemaComponent extends HTMLElement {
1000
802
  for (const child of children) {
1001
803
  const data = child.getElementData?.();
1002
804
  if (!data) continue;
1003
- if (!validation.validateElementBelongsToComponent(data.elementType, componentType)) {
805
+ if (!tokens.validateElementBelongsToComponent(data.elementType, componentType)) {
1004
806
  console.warn(
1005
807
  `skhema-component: element type "${data.elementType}" does not belong to component type "${componentType}"`
1006
808
  );
1007
809
  }
1008
810
  elements.push(data);
1009
811
  }
1010
- const orderedTypes = validation.getElementTypesForComponent(componentType);
812
+ const orderedTypes = tokens.getElementTypesForComponent(componentType);
1011
813
  elements.sort((a, b) => {
1012
814
  const aIdx = orderedTypes.indexOf(a.elementType);
1013
815
  const bIdx = orderedTypes.indexOf(b.elementType);
@@ -1020,10 +822,7 @@ class SkhemaComponent extends HTMLElement {
1020
822
  renderContent(elements) {
1021
823
  if (!this.contentData) return;
1022
824
  const { component_type, contributor_id, title } = this.contentData;
1023
- 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);
825
+ const componentLabel = tokens.getComponentTypeLabel(component_type);
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: ${tokens.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: ${tokens.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: ${tokens.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: ${tokens.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 {
@@ -1592,11 +1266,11 @@ class SkhemaElement extends HTMLElement {
1592
1266
  this.componentConnected = true;
1593
1267
  if (this.closest("skhema-component")) {
1594
1268
  this.nestedMode = true;
1595
- const validation$1 = validation.validateAttributes(this);
1596
- if (!validation$1.isValid) {
1269
+ const validation = tokens.validateAttributes(this);
1270
+ if (!validation.isValid) {
1597
1271
  console.warn(
1598
1272
  "skhema-element: invalid attributes in nested mode",
1599
- validation$1.errors
1273
+ validation.errors
1600
1274
  );
1601
1275
  return;
1602
1276
  }
@@ -1606,8 +1280,8 @@ class SkhemaElement extends HTMLElement {
1606
1280
  return;
1607
1281
  }
1608
1282
  this.contentData = {
1609
- contributor_id: validation$1.contributorId,
1610
- element_type: validation$1.elementType,
1283
+ contributor_id: validation.contributorId,
1284
+ element_type: validation.elementType,
1611
1285
  content,
1612
1286
  content_hash: generateContentHash(content),
1613
1287
  source_url: this.getAttribute("source-url") || window.location.href,
@@ -1655,9 +1329,9 @@ class SkhemaElement extends HTMLElement {
1655
1329
  };
1656
1330
  }
1657
1331
  render() {
1658
- const validation$1 = validation.validateAttributes(this);
1659
- if (!validation$1.isValid) {
1660
- this.renderError("Invalid component attributes", validation$1.errors);
1332
+ const validation = tokens.validateAttributes(this);
1333
+ if (!validation.isValid) {
1334
+ this.renderError("Invalid component attributes", validation.errors);
1661
1335
  return;
1662
1336
  }
1663
1337
  const content = this.getContent();
@@ -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",
@@ -1676,8 +1350,8 @@ class SkhemaElement extends HTMLElement {
1676
1350
  return;
1677
1351
  }
1678
1352
  this.contentData = {
1679
- contributor_id: validation$1.contributorId,
1680
- element_type: validation$1.elementType,
1353
+ contributor_id: validation.contributorId,
1354
+ element_type: validation.elementType,
1681
1355
  content,
1682
1356
  content_hash: generateContentHash(content),
1683
1357
  source_url: this.getAttribute("source-url") || window.location.href,
@@ -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
  }
@@ -1971,15 +1610,15 @@ if (typeof window !== "undefined") {
1971
1610
  );
1972
1611
  }
1973
1612
  }
1974
- exports.getComponentTypeAcronym = validation.getComponentTypeAcronym;
1975
- exports.getComponentTypeLabel = validation.getComponentTypeLabel;
1976
- exports.getElementTypeAcronym = validation.getElementTypeAcronym;
1977
- exports.getElementTypeLabel = validation.getElementTypeLabel;
1978
- exports.isValidComponentType = validation.isValidComponentType;
1979
- exports.isValidElementType = validation.isValidElementType;
1980
- exports.resolveComponentType = validation.resolveComponentType;
1981
- exports.validateAttributes = validation.validateAttributes;
1982
- exports.validateElementBelongsToComponent = validation.validateElementBelongsToComponent;
1613
+ exports.getComponentTypeAcronym = tokens.getComponentTypeAcronym;
1614
+ exports.getComponentTypeLabel = tokens.getComponentTypeLabel;
1615
+ exports.getElementTypeAcronym = tokens.getElementTypeAcronym;
1616
+ exports.getElementTypeLabel = tokens.getElementTypeLabel;
1617
+ exports.isValidComponentType = tokens.isValidComponentType;
1618
+ exports.isValidElementType = tokens.isValidElementType;
1619
+ exports.resolveComponentType = tokens.resolveComponentType;
1620
+ exports.validateAttributes = tokens.validateAttributes;
1621
+ exports.validateElementBelongsToComponent = tokens.validateElementBelongsToComponent;
1983
1622
  exports.SkhemaComponent = SkhemaComponent;
1984
1623
  exports.SkhemaElement = SkhemaElement;
1985
1624
  exports.default = SkhemaElement;