@skhema/web-component 0.0.16 → 0.0.18

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.
@@ -3,21 +3,31 @@ export declare class SkhemaElement extends HTMLElement {
3
3
  private shadow;
4
4
  private contentData;
5
5
  private componentConnected;
6
+ private hasTrackedLoad;
7
+ private themeObserver;
8
+ private mediaQueryListener;
6
9
  constructor();
7
10
  static get observedAttributes(): (keyof SkhemaElementAttributes)[];
8
11
  connectedCallback(): void;
12
+ disconnectedCallback(): void;
9
13
  attributeChangedCallback(_name: keyof SkhemaElementAttributes, oldValue: string | null, newValue: string | null): void;
10
14
  private render;
11
15
  private getContent;
12
16
  private renderContent;
17
+ private getActualTheme;
13
18
  private formatContributorName;
14
19
  private getInitials;
20
+ private addPreconnectHints;
21
+ private renderSkeleton;
15
22
  private renderError;
16
23
  private addStructuredData;
17
24
  private trackLoad;
18
25
  private handleSaveClick;
19
26
  getContentData(): ContentData | null;
20
27
  refresh(): void;
28
+ private setupThemeListeners;
29
+ private cleanupThemeListeners;
30
+ private updateTheme;
21
31
  }
22
32
  declare global {
23
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;;IAOlC,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;YAyBT,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
@@ -10,53 +10,202 @@ function toUrlSafeBase64(str) {
10
10
  );
11
11
  return base64.replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
12
12
  }
13
- async function trackEmbedLoad(analytics) {
13
+ const TRACKING_COOKIE_NAME = "_sk";
14
+ const TRACKING_EXPIRY_HOURS = 24;
15
+ const MAX_TRACKED_ITEMS = 50;
16
+ function getTrackedEmbeds() {
14
17
  try {
15
- const data = new URLSearchParams({
16
- contributor_id: analytics.contributorId,
17
- element_type: analytics.elementType,
18
- content_hash: analytics.contentHash,
19
- content: toUrlSafeBase64(analytics.content),
20
- page_url: analytics.pageUrl,
21
- page_title: analytics.pageTitle || "",
22
- timestamp: analytics.timestamp.toString(),
23
- user_agent: analytics.userAgent || ""
24
- });
25
- if (navigator.sendBeacon) {
26
- navigator.sendBeacon(
27
- "https://api.skhema.com/api:XGdoUqHx/component/embed",
28
- data
29
- );
30
- } else {
31
- fetch("https://api.skhema.com/api:XGdoUqHx/component/embed", {
18
+ const cookie = document.cookie.split("; ").find((row) => row.startsWith(`${TRACKING_COOKIE_NAME}=`));
19
+ if (!cookie) return [];
20
+ const data = JSON.parse(decodeURIComponent(cookie.split("=")[1]));
21
+ const now = Date.now();
22
+ const cutoff = now - TRACKING_EXPIRY_HOURS * 60 * 60 * 1e3;
23
+ return data.filter((item) => item.timestamp > cutoff);
24
+ } catch {
25
+ return [];
26
+ }
27
+ }
28
+ function setTrackedEmbeds(tracked) {
29
+ try {
30
+ const limited = tracked.slice(-MAX_TRACKED_ITEMS);
31
+ const expires = new Date(
32
+ Date.now() + TRACKING_EXPIRY_HOURS * 60 * 60 * 1e3
33
+ );
34
+ document.cookie = `${TRACKING_COOKIE_NAME}=${encodeURIComponent(
35
+ JSON.stringify(limited)
36
+ )}; expires=${expires.toUTCString()}; path=/; SameSite=Lax`;
37
+ } catch {
38
+ }
39
+ }
40
+ function hasBeenTracked(contentHash) {
41
+ const tracked = getTrackedEmbeds();
42
+ return tracked.some((item) => item.contentHash === contentHash);
43
+ }
44
+ function markAsTracked(contentHash) {
45
+ const tracked = getTrackedEmbeds();
46
+ tracked.push({
47
+ contentHash,
48
+ timestamp: Date.now()
49
+ });
50
+ setTrackedEmbeds(tracked);
51
+ }
52
+ class AnalyticsBatcher {
53
+ constructor() {
54
+ this.batch = { embeds: [], clicks: [] };
55
+ this.batchTimeout = null;
56
+ this.BATCH_DELAY = 2e3;
57
+ this.MAX_BATCH_SIZE = 10;
58
+ }
59
+ addEmbedLoad(analytics) {
60
+ this.batch.embeds.push(analytics);
61
+ this.scheduleBatchSend();
62
+ }
63
+ addClick(contentData) {
64
+ this.batch.clicks.push(contentData);
65
+ this.scheduleBatchSend();
66
+ }
67
+ scheduleBatchSend() {
68
+ if (this.batchTimeout) {
69
+ clearTimeout(this.batchTimeout);
70
+ }
71
+ if (this.batch.embeds.length >= this.MAX_BATCH_SIZE || this.batch.clicks.length >= this.MAX_BATCH_SIZE) {
72
+ this.sendBatch();
73
+ return;
74
+ }
75
+ this.batchTimeout = window.setTimeout(() => {
76
+ this.sendBatch();
77
+ }, this.BATCH_DELAY);
78
+ }
79
+ async sendBatch() {
80
+ if (this.batchTimeout) {
81
+ clearTimeout(this.batchTimeout);
82
+ this.batchTimeout = null;
83
+ }
84
+ const currentBatch = { ...this.batch };
85
+ this.batch = { embeds: [], clicks: [] };
86
+ if (currentBatch.embeds.length === 0 && currentBatch.clicks.length === 0) {
87
+ return;
88
+ }
89
+ if (currentBatch.embeds.length > 0) {
90
+ await this.sendEmbeds(currentBatch.embeds);
91
+ }
92
+ if (currentBatch.clicks.length > 0) {
93
+ await this.sendClicks(currentBatch.clicks);
94
+ }
95
+ }
96
+ async sendEmbeds(embeds) {
97
+ for (const embed of embeds) {
98
+ try {
99
+ const data = new URLSearchParams({
100
+ contributor_id: embed.contributorId,
101
+ element_type: embed.elementType,
102
+ content_hash: embed.contentHash,
103
+ content: toUrlSafeBase64(embed.content),
104
+ page_url: embed.pageUrl,
105
+ page_title: embed.pageTitle || "",
106
+ timestamp: embed.timestamp.toString(),
107
+ 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);
123
+ }
124
+ }
125
+ }
126
+ async sendClicks(clicks) {
127
+ for (const click of clicks) {
128
+ try {
129
+ const data = {
130
+ contributor_id: click.contributor_id,
131
+ element_type: click.element_type,
132
+ content_hash: click.content_hash,
133
+ source_url: click.source_url,
134
+ timestamp: click.timestamp
135
+ };
136
+ await sendWithRetry(
137
+ "https://api.skhema.com/api:XGdoUqHx/component/click",
138
+ data,
139
+ "json"
140
+ );
141
+ } catch (error) {
142
+ console.debug("Click tracking failed:", error);
143
+ }
144
+ }
145
+ }
146
+ // Ensure batch is sent when page unloads
147
+ flush() {
148
+ if (this.batch.embeds.length > 0 || this.batch.clicks.length > 0) {
149
+ this.sendBatch();
150
+ }
151
+ }
152
+ }
153
+ const analyticsBatcher = new AnalyticsBatcher();
154
+ if (typeof window !== "undefined") {
155
+ window.addEventListener("beforeunload", () => {
156
+ analyticsBatcher.flush();
157
+ });
158
+ document.addEventListener("visibilitychange", () => {
159
+ if (document.visibilityState === "hidden") {
160
+ analyticsBatcher.flush();
161
+ }
162
+ });
163
+ }
164
+ async function sendWithRetry(url, data, contentType = "json", maxRetries = 3) {
165
+ for (let attempt = 0; attempt < maxRetries; attempt++) {
166
+ try {
167
+ const options = {
32
168
  method: "POST",
33
- body: data,
34
169
  credentials: "omit",
35
170
  keepalive: true
36
- }).catch(() => {
37
- });
171
+ };
172
+ if (contentType === "json") {
173
+ options.headers = { "Content-Type": "application/json" };
174
+ options.body = JSON.stringify(data);
175
+ } else {
176
+ options.body = data;
177
+ }
178
+ const response = await fetch(url, options);
179
+ if (response.ok) return;
180
+ if (response.status >= 400 && response.status < 500) {
181
+ break;
182
+ }
183
+ } catch (error) {
184
+ if (attempt === maxRetries - 1) {
185
+ console.debug("Analytics failed after retries:", error);
186
+ return;
187
+ }
188
+ }
189
+ await new Promise(
190
+ (resolve) => setTimeout(resolve, Math.pow(2, attempt) * 1e3)
191
+ );
192
+ }
193
+ }
194
+ async function trackEmbedLoad(analytics) {
195
+ try {
196
+ if (hasBeenTracked(analytics.contentHash)) {
197
+ console.debug("Embed already tracked, skipping:", analytics.contentHash);
198
+ return;
38
199
  }
200
+ markAsTracked(analytics.contentHash);
201
+ analyticsBatcher.addEmbedLoad(analytics);
39
202
  } catch (error) {
40
203
  console.debug("Analytics tracking failed:", error);
41
204
  }
42
205
  }
43
206
  async function trackClick(contentData) {
44
207
  try {
45
- const data = {
46
- contributor_id: contentData.contributor_id,
47
- element_type: contentData.element_type,
48
- content_hash: contentData.content_hash,
49
- source_url: contentData.source_url,
50
- timestamp: contentData.timestamp
51
- };
52
- fetch("https://api.skhema.com/api:XGdoUqHx/component/click", {
53
- method: "POST",
54
- headers: { "Content-Type": "application/json" },
55
- body: JSON.stringify(data),
56
- credentials: "omit",
57
- keepalive: true
58
- }).catch(() => {
59
- });
208
+ analyticsBatcher.addClick(contentData);
60
209
  } catch (error) {
61
210
  console.debug("Click tracking failed:", error);
62
211
  }
@@ -266,8 +415,9 @@ const styles = `
266
415
  color: var(--skhema-text);
267
416
  }
268
417
 
269
- :host([theme="dark"]) {
270
- /* Dark mode colors */
418
+ /* Dark mode styles - applied via data-theme attribute */
419
+ .skhema-insight-card[data-theme="dark"],
420
+ .skhema-skeleton[data-theme="dark"] {
271
421
  --skhema-bg: hsl(222.2 84% 4.9%);
272
422
  --skhema-card: hsl(222.2 84% 4.9%);
273
423
  --skhema-border: hsl(217.2 32.6% 17.5%);
@@ -486,26 +636,81 @@ const styles = `
486
636
  padding-left: 16px;
487
637
  }
488
638
 
489
- /* Loading state */
490
- .skhema-loading {
491
- background: var(--skhema-accent);
639
+ /* Skeleton loading state */
640
+ .skhema-skeleton {
641
+ background: var(--skhema-card);
492
642
  border: 1px solid var(--skhema-border);
493
- padding: 12px;
643
+ border-radius: calc(var(--skhema-radius) * 2);
644
+ padding: 16px;
645
+ box-shadow: var(--skhema-shadow);
646
+ max-width: 600px;
647
+ margin: 8px 0;
648
+ animation: skeletonPulse 1.5s ease-in-out infinite;
649
+ }
650
+
651
+ .skhema-skeleton-header {
652
+ display: flex;
653
+ align-items: center;
654
+ gap: 12px;
655
+ margin-bottom: 12px;
656
+ }
657
+
658
+ .skhema-skeleton-avatar {
659
+ width: 32px;
660
+ height: 32px;
661
+ border-radius: 50%;
662
+ background: linear-gradient(90deg,
663
+ var(--skhema-border) 25%,
664
+ var(--skhema-accent) 50%,
665
+ var(--skhema-border) 75%);
666
+ background-size: 200% 100%;
667
+ animation: shimmer 1.5s infinite;
668
+ }
669
+
670
+ .skhema-skeleton-text {
671
+ flex: 1;
672
+ }
673
+
674
+ .skhema-skeleton-line {
675
+ height: 12px;
676
+ background: linear-gradient(90deg,
677
+ var(--skhema-border) 25%,
678
+ var(--skhema-accent) 50%,
679
+ var(--skhema-border) 75%);
680
+ background-size: 200% 100%;
681
+ animation: shimmer 1.5s infinite;
494
682
  border-radius: var(--skhema-radius);
495
- color: var(--skhema-text-muted);
496
- font-size: 13px;
497
- text-align: center;
683
+ margin: 6px 0;
684
+ }
685
+
686
+ .skhema-skeleton-line.short {
687
+ width: 40%;
688
+ }
689
+
690
+ .skhema-skeleton-line.medium {
691
+ width: 70%;
498
692
  }
499
693
 
500
- .skhema-loading::after {
501
- content: '...';
502
- animation: loading 1.5s infinite;
694
+ .skhema-skeleton-content {
695
+ margin: 16px 0;
503
696
  }
504
697
 
505
- @keyframes loading {
506
- 0%, 33% { content: '...'; }
507
- 66% { content: '..'; }
508
- 100% { content: '.'; }
698
+ @keyframes skeletonPulse {
699
+ 0%, 100% {
700
+ opacity: 1;
701
+ }
702
+ 50% {
703
+ opacity: 0.8;
704
+ }
705
+ }
706
+
707
+ @keyframes shimmer {
708
+ 0% {
709
+ background-position: -200% 0;
710
+ }
711
+ 100% {
712
+ background-position: 200% 0;
713
+ }
509
714
  }
510
715
 
511
716
  /* Responsive design */
@@ -557,7 +762,11 @@ class SkhemaElement extends HTMLElement {
557
762
  super();
558
763
  this.contentData = null;
559
764
  this.componentConnected = false;
765
+ this.hasTrackedLoad = false;
766
+ this.themeObserver = null;
767
+ this.mediaQueryListener = null;
560
768
  this.shadow = this.attachShadow({ mode: "closed" });
769
+ this.renderSkeleton();
561
770
  }
562
771
  static get observedAttributes() {
563
772
  return [
@@ -573,12 +782,19 @@ class SkhemaElement extends HTMLElement {
573
782
  if (this.componentConnected) return;
574
783
  this.componentConnected = true;
575
784
  try {
576
- this.render();
577
- this.trackLoad();
785
+ this.addPreconnectHints();
786
+ requestAnimationFrame(() => {
787
+ this.render();
788
+ this.trackLoad();
789
+ this.setupThemeListeners();
790
+ });
578
791
  } catch (error) {
579
792
  this.renderError("Failed to initialize component", error);
580
793
  }
581
794
  }
795
+ disconnectedCallback() {
796
+ this.cleanupThemeListeners();
797
+ }
582
798
  attributeChangedCallback(_name, oldValue, newValue) {
583
799
  if (oldValue !== newValue && this.componentConnected) {
584
800
  this.render();
@@ -629,7 +845,8 @@ class SkhemaElement extends HTMLElement {
629
845
  element_type,
630
846
  contributor_id
631
847
  );
632
- const theme = this.getAttribute("theme") || "auto";
848
+ const themeAttribute = this.getAttribute("theme") || "auto";
849
+ const actualTheme = this.getActualTheme(themeAttribute);
633
850
  const displayName = this.formatContributorName(contributor_id);
634
851
  const initials = this.getInitials(displayName);
635
852
  const ariaAttrs = createAriaAttributes(element_type);
@@ -638,8 +855,8 @@ class SkhemaElement extends HTMLElement {
638
855
  });
639
856
  this.shadow.innerHTML = `
640
857
  <style>${styles}</style>
641
-
642
- <div class="skhema-insight-card" data-theme="${theme}">
858
+
859
+ <div class="skhema-insight-card" data-theme="${actualTheme}">
643
860
  <div class="skhema-header">
644
861
  <div class="skhema-contributor">
645
862
  <div class="skhema-avatar" title="${displayName}">
@@ -682,17 +899,78 @@ class SkhemaElement extends HTMLElement {
682
899
  });
683
900
  }
684
901
  }
902
+ getActualTheme(themeAttribute) {
903
+ if (themeAttribute === "light" || themeAttribute === "dark") {
904
+ return themeAttribute;
905
+ }
906
+ const htmlElement = document.documentElement;
907
+ const bodyElement = document.body;
908
+ const htmlTheme = htmlElement.getAttribute("data-theme") || htmlElement.getAttribute("theme") || htmlElement.className.match(/theme-(\w+)/)?.[1];
909
+ const bodyTheme = bodyElement.getAttribute("data-theme") || bodyElement.getAttribute("theme") || bodyElement.className.match(/theme-(\w+)/)?.[1];
910
+ const hasDarkClass = htmlElement.classList.contains("dark") || bodyElement.classList.contains("dark") || htmlElement.classList.contains("dark-mode") || bodyElement.classList.contains("dark-mode");
911
+ if (hasDarkClass || htmlTheme === "dark" || bodyTheme === "dark") {
912
+ return "dark";
913
+ }
914
+ const computedStyles = window.getComputedStyle(htmlElement);
915
+ const colorScheme = computedStyles.getPropertyValue("color-scheme");
916
+ if (colorScheme && colorScheme.includes("dark")) {
917
+ return "dark";
918
+ }
919
+ if (window.matchMedia && window.matchMedia("(prefers-color-scheme: dark)").matches) {
920
+ return "dark";
921
+ }
922
+ return "light";
923
+ }
685
924
  formatContributorName(contributorId) {
686
925
  return contributorId.split(/[_-]/).map((word) => word.charAt(0).toUpperCase() + word.slice(1).toLowerCase()).join(" ");
687
926
  }
688
927
  getInitials(name) {
689
928
  return name.split(" ").map((word) => word.charAt(0)).join("").toUpperCase().substring(0, 2);
690
929
  }
930
+ addPreconnectHints() {
931
+ if (document.querySelector('link[rel="preconnect"][href*="api.skhema.com"]')) {
932
+ return;
933
+ }
934
+ try {
935
+ const preconnectApi = document.createElement("link");
936
+ preconnectApi.rel = "preconnect";
937
+ preconnectApi.href = "https://api.skhema.com";
938
+ document.head.appendChild(preconnectApi);
939
+ const dnsPrefetch = document.createElement("link");
940
+ dnsPrefetch.rel = "dns-prefetch";
941
+ dnsPrefetch.href = "https://skhema.com";
942
+ document.head.appendChild(dnsPrefetch);
943
+ } catch (error) {
944
+ console.debug("Failed to add preconnect hints:", error);
945
+ }
946
+ }
947
+ renderSkeleton() {
948
+ const themeAttribute = this.getAttribute("theme") || "auto";
949
+ const actualTheme = this.getActualTheme(themeAttribute);
950
+ this.shadow.innerHTML = `
951
+ <style>${styles}</style>
952
+
953
+ <div class="skhema-skeleton" data-theme="${actualTheme}">
954
+ <div class="skhema-skeleton-header">
955
+ <div class="skhema-skeleton-avatar"></div>
956
+ <div class="skhema-skeleton-text">
957
+ <div class="skhema-skeleton-line medium"></div>
958
+ <div class="skhema-skeleton-line short"></div>
959
+ </div>
960
+ </div>
961
+ <div class="skhema-skeleton-content">
962
+ <div class="skhema-skeleton-line"></div>
963
+ <div class="skhema-skeleton-line"></div>
964
+ <div class="skhema-skeleton-line medium"></div>
965
+ </div>
966
+ </div>
967
+ `;
968
+ }
691
969
  renderError(title, errors) {
692
970
  const errorList = Array.isArray(errors) ? errors : [String(errors)];
693
971
  this.shadow.innerHTML = `
694
972
  <style>${styles}</style>
695
-
973
+
696
974
  <div class="skhema-insight-card">
697
975
  <div class="skhema-error">
698
976
  <div class="skhema-error-title">Skhema Component Error: ${title}</div>
@@ -729,7 +1007,10 @@ class SkhemaElement extends HTMLElement {
729
1007
  document.body.appendChild(metaDiv);
730
1008
  }
731
1009
  async trackLoad() {
732
- if (!shouldTrackAnalytics(this) || !this.contentData) return;
1010
+ if (!shouldTrackAnalytics(this) || !this.contentData || this.hasTrackedLoad) {
1011
+ return;
1012
+ }
1013
+ this.hasTrackedLoad = true;
733
1014
  const analytics = {
734
1015
  contributorId: this.contentData.contributor_id,
735
1016
  elementType: this.contentData.element_type,
@@ -767,6 +1048,46 @@ class SkhemaElement extends HTMLElement {
767
1048
  refresh() {
768
1049
  this.render();
769
1050
  }
1051
+ setupThemeListeners() {
1052
+ const themeAttribute = this.getAttribute("theme");
1053
+ if (themeAttribute === "auto" || !themeAttribute) {
1054
+ if (window.matchMedia) {
1055
+ this.mediaQueryListener = window.matchMedia(
1056
+ "(prefers-color-scheme: dark)"
1057
+ );
1058
+ const handleThemeChange = () => this.updateTheme();
1059
+ this.mediaQueryListener.addEventListener("change", handleThemeChange);
1060
+ }
1061
+ this.themeObserver = new MutationObserver(() => this.updateTheme());
1062
+ this.themeObserver.observe(document.documentElement, {
1063
+ attributes: true,
1064
+ attributeFilter: ["class", "data-theme", "theme"]
1065
+ });
1066
+ this.themeObserver.observe(document.body, {
1067
+ attributes: true,
1068
+ attributeFilter: ["class", "data-theme", "theme"]
1069
+ });
1070
+ }
1071
+ }
1072
+ cleanupThemeListeners() {
1073
+ if (this.themeObserver) {
1074
+ this.themeObserver.disconnect();
1075
+ this.themeObserver = null;
1076
+ }
1077
+ if (this.mediaQueryListener) {
1078
+ this.mediaQueryListener = null;
1079
+ }
1080
+ }
1081
+ updateTheme() {
1082
+ const themeAttribute = this.getAttribute("theme") || "auto";
1083
+ if (themeAttribute === "auto") {
1084
+ const card = this.shadow.querySelector(".skhema-insight-card");
1085
+ if (card) {
1086
+ const newTheme = this.getActualTheme("auto");
1087
+ card.setAttribute("data-theme", newTheme);
1088
+ }
1089
+ }
1090
+ }
770
1091
  }
771
1092
  function registerSkhemaElement() {
772
1093
  if (typeof window !== "undefined" && !customElements.get("skhema-element")) {