@skhema/web-component 0.0.17 → 0.0.19

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.
@@ -4,21 +4,30 @@ export declare class SkhemaElement extends HTMLElement {
4
4
  private contentData;
5
5
  private componentConnected;
6
6
  private hasTrackedLoad;
7
+ private themeObserver;
8
+ private mediaQueryListener;
7
9
  constructor();
8
10
  static get observedAttributes(): (keyof SkhemaElementAttributes)[];
9
11
  connectedCallback(): void;
12
+ disconnectedCallback(): void;
10
13
  attributeChangedCallback(_name: keyof SkhemaElementAttributes, oldValue: string | null, newValue: string | null): void;
11
14
  private render;
12
15
  private getContent;
13
16
  private renderContent;
17
+ private getActualTheme;
14
18
  private formatContributorName;
15
19
  private getInitials;
20
+ private addPreconnectHints;
21
+ private renderSkeleton;
16
22
  private renderError;
17
23
  private addStructuredData;
18
24
  private trackLoad;
19
25
  private handleSaveClick;
20
26
  getContentData(): ContentData | null;
21
27
  refresh(): void;
28
+ private setupThemeListeners;
29
+ private cleanupThemeListeners;
30
+ private updateTheme;
22
31
  }
23
32
  declare global {
24
33
  interface HTMLElementEventMap extends SkhemaElementEventMap {
@@ -1 +1 @@
1
- {"version":3,"file":"SkhemaElement.d.ts","sourceRoot":"","sources":["../../src/components/SkhemaElement.ts"],"names":[],"mappings":"AAiBA,OAAO,KAAK,EACV,WAAW,EAEX,uBAAuB,EACvB,qBAAqB,EACtB,MAAM,YAAY,CAAA;AA+TnB,qBAAa,aAAc,SAAQ,WAAW;IAC5C,OAAO,CAAC,MAAM,CAAY;IAC1B,OAAO,CAAC,WAAW,CAA2B;IAC9C,OAAO,CAAC,kBAAkB,CAAQ;IAClC,OAAO,CAAC,cAAc,CAAQ;;IAO9B,MAAM,KAAK,kBAAkB,IAAI,CAAC,MAAM,uBAAuB,CAAC,EAAE,CASjE;IAED,iBAAiB;IAYjB,wBAAwB,CACtB,KAAK,EAAE,MAAM,uBAAuB,EACpC,QAAQ,EAAE,MAAM,GAAG,IAAI,EACvB,QAAQ,EAAE,MAAM,GAAG,IAAI;IAOzB,OAAO,CAAC,MAAM;IAwCd,OAAO,CAAC,UAAU;IAIlB,OAAO,CAAC,aAAa;IAuErB,OAAO,CAAC,qBAAqB;IAQ7B,OAAO,CAAC,WAAW;IASnB,OAAO,CAAC,WAAW;IAyBnB,OAAO,CAAC,iBAAiB;YA0BX,SAAS;YAiCT,eAAe;IAkBtB,cAAc,IAAI,WAAW,GAAG,IAAI;IAIpC,OAAO,IAAI,IAAI;CAGvB;AAGD,OAAO,CAAC,MAAM,CAAC;IAEb,UAAU,mBAAoB,SAAQ,qBAAqB;KAAG;IAE9D,UAAU,gBAAiB,SAAQ,OAAO,CAAC,uBAAuB,CAAC;QACjE,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAA;KACvB;IAGD,UAAU,oBAAoB;QAC5B,gBAAgB,EAAE,gBAAgB,CAAA;KACnC;CACF"}
1
+ {"version":3,"file":"SkhemaElement.d.ts","sourceRoot":"","sources":["../../src/components/SkhemaElement.ts"],"names":[],"mappings":"AAiBA,OAAO,KAAK,EACV,WAAW,EAEX,uBAAuB,EACvB,qBAAqB,EACtB,MAAM,YAAY,CAAA;AAuXnB,qBAAa,aAAc,SAAQ,WAAW;IAC5C,OAAO,CAAC,MAAM,CAAY;IAC1B,OAAO,CAAC,WAAW,CAA2B;IAC9C,OAAO,CAAC,kBAAkB,CAAQ;IAClC,OAAO,CAAC,cAAc,CAAQ;IAC9B,OAAO,CAAC,aAAa,CAAgC;IACrD,OAAO,CAAC,kBAAkB,CAA8B;;IASxD,MAAM,KAAK,kBAAkB,IAAI,CAAC,MAAM,uBAAuB,CAAC,EAAE,CASjE;IAED,iBAAiB;IAmBjB,oBAAoB;IAIpB,wBAAwB,CACtB,KAAK,EAAE,MAAM,uBAAuB,EACpC,QAAQ,EAAE,MAAM,GAAG,IAAI,EACvB,QAAQ,EAAE,MAAM,GAAG,IAAI;IAOzB,OAAO,CAAC,MAAM;IAwCd,OAAO,CAAC,UAAU;IAIlB,OAAO,CAAC,aAAa;IA0ErB,OAAO,CAAC,cAAc;IAmDtB,OAAO,CAAC,qBAAqB;IAQ7B,OAAO,CAAC,WAAW;IASnB,OAAO,CAAC,kBAAkB;IAyB1B,OAAO,CAAC,cAAc;IAyBtB,OAAO,CAAC,WAAW;IAyBnB,OAAO,CAAC,iBAAiB;YA0BX,SAAS;YAiCT,eAAe;IAkBtB,cAAc,IAAI,WAAW,GAAG,IAAI;IAIpC,OAAO,IAAI,IAAI;IAItB,OAAO,CAAC,mBAAmB;IA6B3B,OAAO,CAAC,qBAAqB;IAa7B,OAAO,CAAC,WAAW;CAYpB;AAGD,OAAO,CAAC,MAAM,CAAC;IAEb,UAAU,mBAAoB,SAAQ,qBAAqB;KAAG;IAE9D,UAAU,gBAAiB,SAAQ,OAAO,CAAC,uBAAuB,CAAC;QACjE,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAA;KACvB;IAGD,UAAU,oBAAoB;QAC5B,gBAAgB,EAAE,gBAAgB,CAAA;KACnC;CACF"}
package/dist/index.cjs CHANGED
@@ -94,9 +94,10 @@ class AnalyticsBatcher {
94
94
  }
95
95
  }
96
96
  async sendEmbeds(embeds) {
97
- for (const embed of embeds) {
98
- try {
99
- const data = new URLSearchParams({
97
+ if (embeds.length === 0) return;
98
+ try {
99
+ const payload = {
100
+ embeds: embeds.map((embed) => ({
100
101
  contributor_id: embed.contributorId,
101
102
  element_type: embed.elementType,
102
103
  content_hash: embed.contentHash,
@@ -105,22 +106,24 @@ class AnalyticsBatcher {
105
106
  page_title: embed.pageTitle || "",
106
107
  timestamp: embed.timestamp.toString(),
107
108
  user_agent: embed.userAgent || ""
108
- });
109
- if (navigator.sendBeacon) {
110
- navigator.sendBeacon(
111
- "https://api.skhema.com/api:XGdoUqHx/component/embed",
112
- data
113
- );
114
- } else {
115
- await sendWithRetry(
116
- "https://api.skhema.com/api:XGdoUqHx/component/embed",
117
- data,
118
- "urlencoded"
119
- );
120
- }
121
- } catch (error) {
122
- console.debug("Embed tracking failed:", error);
109
+ })),
110
+ batch_size: embeds.length,
111
+ batch_timestamp: Date.now()
112
+ };
113
+ if (navigator.sendBeacon) {
114
+ navigator.sendBeacon(
115
+ "https://api.skhema.com/api:XGdoUqHx/component/embed",
116
+ new Blob([JSON.stringify(payload)], { type: "application/json" })
117
+ );
118
+ } else {
119
+ await sendWithRetry(
120
+ "https://api.skhema.com/api:XGdoUqHx/component/embed",
121
+ payload,
122
+ "json"
123
+ );
123
124
  }
125
+ } catch (error) {
126
+ console.debug("Embed tracking failed:", error);
124
127
  }
125
128
  }
126
129
  async sendClicks(clicks) {
@@ -133,11 +136,18 @@ class AnalyticsBatcher {
133
136
  source_url: click.source_url,
134
137
  timestamp: click.timestamp
135
138
  };
136
- await sendWithRetry(
137
- "https://api.skhema.com/api:XGdoUqHx/component/click",
138
- data,
139
- "json"
140
- );
139
+ if (navigator.sendBeacon) {
140
+ navigator.sendBeacon(
141
+ "https://api.skhema.com/api:XGdoUqHx/component/click",
142
+ new Blob([JSON.stringify(data)], { type: "application/json" })
143
+ );
144
+ } else {
145
+ await sendWithRetry(
146
+ "https://api.skhema.com/api:XGdoUqHx/component/click",
147
+ data,
148
+ "json"
149
+ );
150
+ }
141
151
  } catch (error) {
142
152
  console.debug("Click tracking failed:", error);
143
153
  }
@@ -415,8 +425,9 @@ const styles = `
415
425
  color: var(--skhema-text);
416
426
  }
417
427
 
418
- :host([theme="dark"]) {
419
- /* Dark mode colors */
428
+ /* Dark mode styles - applied via data-theme attribute */
429
+ .skhema-insight-card[data-theme="dark"],
430
+ .skhema-skeleton[data-theme="dark"] {
420
431
  --skhema-bg: hsl(222.2 84% 4.9%);
421
432
  --skhema-card: hsl(222.2 84% 4.9%);
422
433
  --skhema-border: hsl(217.2 32.6% 17.5%);
@@ -635,26 +646,81 @@ const styles = `
635
646
  padding-left: 16px;
636
647
  }
637
648
 
638
- /* Loading state */
639
- .skhema-loading {
640
- background: var(--skhema-accent);
649
+ /* Skeleton loading state */
650
+ .skhema-skeleton {
651
+ background: var(--skhema-card);
641
652
  border: 1px solid var(--skhema-border);
642
- padding: 12px;
653
+ border-radius: calc(var(--skhema-radius) * 2);
654
+ padding: 16px;
655
+ box-shadow: var(--skhema-shadow);
656
+ max-width: 600px;
657
+ margin: 8px 0;
658
+ animation: skeletonPulse 1.5s ease-in-out infinite;
659
+ }
660
+
661
+ .skhema-skeleton-header {
662
+ display: flex;
663
+ align-items: center;
664
+ gap: 12px;
665
+ margin-bottom: 12px;
666
+ }
667
+
668
+ .skhema-skeleton-avatar {
669
+ width: 32px;
670
+ height: 32px;
671
+ border-radius: 50%;
672
+ background: linear-gradient(90deg,
673
+ var(--skhema-border) 25%,
674
+ var(--skhema-accent) 50%,
675
+ var(--skhema-border) 75%);
676
+ background-size: 200% 100%;
677
+ animation: shimmer 1.5s infinite;
678
+ }
679
+
680
+ .skhema-skeleton-text {
681
+ flex: 1;
682
+ }
683
+
684
+ .skhema-skeleton-line {
685
+ height: 12px;
686
+ background: linear-gradient(90deg,
687
+ var(--skhema-border) 25%,
688
+ var(--skhema-accent) 50%,
689
+ var(--skhema-border) 75%);
690
+ background-size: 200% 100%;
691
+ animation: shimmer 1.5s infinite;
643
692
  border-radius: var(--skhema-radius);
644
- color: var(--skhema-text-muted);
645
- font-size: 13px;
646
- text-align: center;
693
+ margin: 6px 0;
647
694
  }
648
695
 
649
- .skhema-loading::after {
650
- content: '...';
651
- animation: loading 1.5s infinite;
696
+ .skhema-skeleton-line.short {
697
+ width: 40%;
652
698
  }
653
699
 
654
- @keyframes loading {
655
- 0%, 33% { content: '...'; }
656
- 66% { content: '..'; }
657
- 100% { content: '.'; }
700
+ .skhema-skeleton-line.medium {
701
+ width: 70%;
702
+ }
703
+
704
+ .skhema-skeleton-content {
705
+ margin: 16px 0;
706
+ }
707
+
708
+ @keyframes skeletonPulse {
709
+ 0%, 100% {
710
+ opacity: 1;
711
+ }
712
+ 50% {
713
+ opacity: 0.8;
714
+ }
715
+ }
716
+
717
+ @keyframes shimmer {
718
+ 0% {
719
+ background-position: -200% 0;
720
+ }
721
+ 100% {
722
+ background-position: 200% 0;
723
+ }
658
724
  }
659
725
 
660
726
  /* Responsive design */
@@ -707,7 +773,10 @@ class SkhemaElement extends HTMLElement {
707
773
  this.contentData = null;
708
774
  this.componentConnected = false;
709
775
  this.hasTrackedLoad = false;
776
+ this.themeObserver = null;
777
+ this.mediaQueryListener = null;
710
778
  this.shadow = this.attachShadow({ mode: "closed" });
779
+ this.renderSkeleton();
711
780
  }
712
781
  static get observedAttributes() {
713
782
  return [
@@ -723,12 +792,19 @@ class SkhemaElement extends HTMLElement {
723
792
  if (this.componentConnected) return;
724
793
  this.componentConnected = true;
725
794
  try {
726
- this.render();
727
- this.trackLoad();
795
+ this.addPreconnectHints();
796
+ requestAnimationFrame(() => {
797
+ this.render();
798
+ this.trackLoad();
799
+ this.setupThemeListeners();
800
+ });
728
801
  } catch (error) {
729
802
  this.renderError("Failed to initialize component", error);
730
803
  }
731
804
  }
805
+ disconnectedCallback() {
806
+ this.cleanupThemeListeners();
807
+ }
732
808
  attributeChangedCallback(_name, oldValue, newValue) {
733
809
  if (oldValue !== newValue && this.componentConnected) {
734
810
  this.render();
@@ -779,7 +855,8 @@ class SkhemaElement extends HTMLElement {
779
855
  element_type,
780
856
  contributor_id
781
857
  );
782
- const theme = this.getAttribute("theme") || "auto";
858
+ const themeAttribute = this.getAttribute("theme") || "auto";
859
+ const actualTheme = this.getActualTheme(themeAttribute);
783
860
  const displayName = this.formatContributorName(contributor_id);
784
861
  const initials = this.getInitials(displayName);
785
862
  const ariaAttrs = createAriaAttributes(element_type);
@@ -788,8 +865,8 @@ class SkhemaElement extends HTMLElement {
788
865
  });
789
866
  this.shadow.innerHTML = `
790
867
  <style>${styles}</style>
791
-
792
- <div class="skhema-insight-card" data-theme="${theme}">
868
+
869
+ <div class="skhema-insight-card" data-theme="${actualTheme}">
793
870
  <div class="skhema-header">
794
871
  <div class="skhema-contributor">
795
872
  <div class="skhema-avatar" title="${displayName}">
@@ -832,17 +909,78 @@ class SkhemaElement extends HTMLElement {
832
909
  });
833
910
  }
834
911
  }
912
+ getActualTheme(themeAttribute) {
913
+ if (themeAttribute === "light" || themeAttribute === "dark") {
914
+ return themeAttribute;
915
+ }
916
+ const htmlElement = document.documentElement;
917
+ const bodyElement = document.body;
918
+ const htmlTheme = htmlElement.getAttribute("data-theme") || htmlElement.getAttribute("theme") || htmlElement.className.match(/theme-(\w+)/)?.[1];
919
+ const bodyTheme = bodyElement.getAttribute("data-theme") || bodyElement.getAttribute("theme") || bodyElement.className.match(/theme-(\w+)/)?.[1];
920
+ const hasDarkClass = htmlElement.classList.contains("dark") || bodyElement.classList.contains("dark") || htmlElement.classList.contains("dark-mode") || bodyElement.classList.contains("dark-mode");
921
+ if (hasDarkClass || htmlTheme === "dark" || bodyTheme === "dark") {
922
+ return "dark";
923
+ }
924
+ const computedStyles = window.getComputedStyle(htmlElement);
925
+ const colorScheme = computedStyles.getPropertyValue("color-scheme");
926
+ if (colorScheme && colorScheme.includes("dark")) {
927
+ return "dark";
928
+ }
929
+ if (window.matchMedia && window.matchMedia("(prefers-color-scheme: dark)").matches) {
930
+ return "dark";
931
+ }
932
+ return "light";
933
+ }
835
934
  formatContributorName(contributorId) {
836
935
  return contributorId.split(/[_-]/).map((word) => word.charAt(0).toUpperCase() + word.slice(1).toLowerCase()).join(" ");
837
936
  }
838
937
  getInitials(name) {
839
938
  return name.split(" ").map((word) => word.charAt(0)).join("").toUpperCase().substring(0, 2);
840
939
  }
940
+ addPreconnectHints() {
941
+ if (document.querySelector('link[rel="preconnect"][href*="api.skhema.com"]')) {
942
+ return;
943
+ }
944
+ try {
945
+ const preconnectApi = document.createElement("link");
946
+ preconnectApi.rel = "preconnect";
947
+ preconnectApi.href = "https://api.skhema.com";
948
+ document.head.appendChild(preconnectApi);
949
+ const dnsPrefetch = document.createElement("link");
950
+ dnsPrefetch.rel = "dns-prefetch";
951
+ dnsPrefetch.href = "https://skhema.com";
952
+ document.head.appendChild(dnsPrefetch);
953
+ } catch (error) {
954
+ console.debug("Failed to add preconnect hints:", error);
955
+ }
956
+ }
957
+ renderSkeleton() {
958
+ const themeAttribute = this.getAttribute("theme") || "auto";
959
+ const actualTheme = this.getActualTheme(themeAttribute);
960
+ this.shadow.innerHTML = `
961
+ <style>${styles}</style>
962
+
963
+ <div class="skhema-skeleton" data-theme="${actualTheme}">
964
+ <div class="skhema-skeleton-header">
965
+ <div class="skhema-skeleton-avatar"></div>
966
+ <div class="skhema-skeleton-text">
967
+ <div class="skhema-skeleton-line medium"></div>
968
+ <div class="skhema-skeleton-line short"></div>
969
+ </div>
970
+ </div>
971
+ <div class="skhema-skeleton-content">
972
+ <div class="skhema-skeleton-line"></div>
973
+ <div class="skhema-skeleton-line"></div>
974
+ <div class="skhema-skeleton-line medium"></div>
975
+ </div>
976
+ </div>
977
+ `;
978
+ }
841
979
  renderError(title, errors) {
842
980
  const errorList = Array.isArray(errors) ? errors : [String(errors)];
843
981
  this.shadow.innerHTML = `
844
982
  <style>${styles}</style>
845
-
983
+
846
984
  <div class="skhema-insight-card">
847
985
  <div class="skhema-error">
848
986
  <div class="skhema-error-title">Skhema Component Error: ${title}</div>
@@ -920,6 +1058,46 @@ class SkhemaElement extends HTMLElement {
920
1058
  refresh() {
921
1059
  this.render();
922
1060
  }
1061
+ setupThemeListeners() {
1062
+ const themeAttribute = this.getAttribute("theme");
1063
+ if (themeAttribute === "auto" || !themeAttribute) {
1064
+ if (window.matchMedia) {
1065
+ this.mediaQueryListener = window.matchMedia(
1066
+ "(prefers-color-scheme: dark)"
1067
+ );
1068
+ const handleThemeChange = () => this.updateTheme();
1069
+ this.mediaQueryListener.addEventListener("change", handleThemeChange);
1070
+ }
1071
+ this.themeObserver = new MutationObserver(() => this.updateTheme());
1072
+ this.themeObserver.observe(document.documentElement, {
1073
+ attributes: true,
1074
+ attributeFilter: ["class", "data-theme", "theme"]
1075
+ });
1076
+ this.themeObserver.observe(document.body, {
1077
+ attributes: true,
1078
+ attributeFilter: ["class", "data-theme", "theme"]
1079
+ });
1080
+ }
1081
+ }
1082
+ cleanupThemeListeners() {
1083
+ if (this.themeObserver) {
1084
+ this.themeObserver.disconnect();
1085
+ this.themeObserver = null;
1086
+ }
1087
+ if (this.mediaQueryListener) {
1088
+ this.mediaQueryListener = null;
1089
+ }
1090
+ }
1091
+ updateTheme() {
1092
+ const themeAttribute = this.getAttribute("theme") || "auto";
1093
+ if (themeAttribute === "auto") {
1094
+ const card = this.shadow.querySelector(".skhema-insight-card");
1095
+ if (card) {
1096
+ const newTheme = this.getActualTheme("auto");
1097
+ card.setAttribute("data-theme", newTheme);
1098
+ }
1099
+ }
1100
+ }
923
1101
  }
924
1102
  function registerSkhemaElement() {
925
1103
  if (typeof window !== "undefined" && !customElements.get("skhema-element")) {