@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.
- package/dist/components/SkhemaElement.d.ts +10 -0
- package/dist/components/SkhemaElement.d.ts.map +1 -1
- package/dist/index.cjs +380 -59
- package/dist/index.cjs.map +1 -1
- package/dist/index.es.js +380 -59
- package/dist/index.es.js.map +1 -1
- package/dist/utils/analytics.d.ts.map +1 -1
- package/dist/web-component.min.js +1 -1
- package/dist/web-component.min.js.map +1 -1
- package/package.json +1 -1
|
@@ -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;
|
|
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
|
-
|
|
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
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
)
|
|
30
|
-
|
|
31
|
-
|
|
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
|
-
}
|
|
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
|
-
|
|
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
|
-
|
|
270
|
-
|
|
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
|
-
/*
|
|
490
|
-
.skhema-
|
|
491
|
-
background: var(--skhema-
|
|
639
|
+
/* Skeleton loading state */
|
|
640
|
+
.skhema-skeleton {
|
|
641
|
+
background: var(--skhema-card);
|
|
492
642
|
border: 1px solid var(--skhema-border);
|
|
493
|
-
|
|
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
|
-
|
|
496
|
-
|
|
497
|
-
|
|
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-
|
|
501
|
-
|
|
502
|
-
animation: loading 1.5s infinite;
|
|
694
|
+
.skhema-skeleton-content {
|
|
695
|
+
margin: 16px 0;
|
|
503
696
|
}
|
|
504
697
|
|
|
505
|
-
@keyframes
|
|
506
|
-
0%,
|
|
507
|
-
|
|
508
|
-
|
|
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.
|
|
577
|
-
|
|
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
|
|
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="${
|
|
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)
|
|
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")) {
|