widetorah-embed 1.0.0

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.
@@ -0,0 +1,965 @@
1
+ /* widetorah-embed v1.0.0 | MIT | https://widget.widetorah.com */
2
+
3
+ // <define:SITE_CONFIG>
4
+ var define_SITE_CONFIG_default = { site: "widetorah", name: "WideTorah", domain: "widetorah.com", apiBase: "https://widetorah.com/api/v1/torah", votdEndpoint: "https://widetorah.com/api/v1/verse-of-the-day/", searchPath: "/search/", accent: "#2563EB", attribute: "data-widetorah", religion: "judaism", scriptureLabel: "Torah", defaultTranslation: "jps" };
5
+
6
+ // src/themes.ts
7
+ function getThemeCSS(accent) {
8
+ return `
9
+ :host {
10
+ display: block;
11
+ font-family: system-ui, -apple-system, 'Segoe UI', Roboto, sans-serif;
12
+ --site-accent: ${accent};
13
+ }
14
+
15
+ .wide-widget {
16
+ box-sizing: border-box;
17
+ min-width: 280px;
18
+ border-radius: 8px;
19
+ overflow: hidden;
20
+ border: 1px solid var(--border);
21
+ background: var(--bg);
22
+ color: var(--text);
23
+ font-size: 15px;
24
+ line-height: 1.6;
25
+ transition: border-color 0.2s;
26
+ }
27
+
28
+ .wide-widget[data-theme="light"] {
29
+ --bg: #ffffff;
30
+ --text: #1a1a1a;
31
+ --border: #e5e7eb;
32
+ --accent: var(--site-accent);
33
+ --muted: #6b7280;
34
+ --ribbon: #f9fafb;
35
+ --badge-bg: #f3f4f6;
36
+ --badge-text: #374151;
37
+ --link: var(--site-accent);
38
+ --copy-bg: #f3f4f6;
39
+ --copy-hover: #e5e7eb;
40
+ --input-bg: #ffffff;
41
+ --input-border: #d1d5db;
42
+ --input-focus: var(--site-accent);
43
+ --shadow: 0 1px 3px rgba(0,0,0,0.08);
44
+ }
45
+
46
+ .wide-widget[data-theme="dark"] {
47
+ --bg: #1a1a1a;
48
+ --text: #f3f4f6;
49
+ --border: #374151;
50
+ --accent: var(--site-accent);
51
+ --muted: #9ca3af;
52
+ --ribbon: #111111;
53
+ --badge-bg: #374151;
54
+ --badge-text: #d1d5db;
55
+ --link: #93c5fd;
56
+ --copy-bg: #374151;
57
+ --copy-hover: #4b5563;
58
+ --input-bg: #111111;
59
+ --input-border: #4b5563;
60
+ --input-focus: var(--site-accent);
61
+ --shadow: 0 1px 3px rgba(0,0,0,0.4);
62
+ }
63
+
64
+ .wide-widget[data-theme="sepia"] {
65
+ --bg: #f5f0e8;
66
+ --text: #3d3529;
67
+ --border: #d4c5a9;
68
+ --accent: var(--site-accent);
69
+ --muted: #8b7d6b;
70
+ --ribbon: #ede8df;
71
+ --badge-bg: #e8e0d0;
72
+ --badge-text: #5c4f3d;
73
+ --link: #7c5c3b;
74
+ --copy-bg: #e8e0d0;
75
+ --copy-hover: #ddd4c0;
76
+ --input-bg: #f5f0e8;
77
+ --input-border: #c4b49a;
78
+ --input-focus: var(--site-accent);
79
+ --shadow: 0 1px 3px rgba(61,53,41,0.12);
80
+ }
81
+
82
+ .wide-widget *, .wide-widget *::before, .wide-widget *::after {
83
+ box-sizing: border-box;
84
+ }
85
+
86
+ /* Loading state */
87
+ .wide-loading {
88
+ padding: 20px 16px;
89
+ text-align: center;
90
+ color: var(--muted);
91
+ font-size: 13px;
92
+ display: flex;
93
+ align-items: center;
94
+ justify-content: center;
95
+ gap: 8px;
96
+ }
97
+
98
+ .wide-spinner {
99
+ width: 16px;
100
+ height: 16px;
101
+ border: 2px solid var(--border);
102
+ border-top-color: var(--accent);
103
+ border-radius: 50%;
104
+ animation: wide-spin 0.7s linear infinite;
105
+ display: inline-block;
106
+ flex-shrink: 0;
107
+ }
108
+
109
+ @keyframes wide-spin {
110
+ to { transform: rotate(360deg); }
111
+ }
112
+
113
+ /* Error state */
114
+ .wide-error {
115
+ padding: 16px;
116
+ color: var(--muted);
117
+ font-size: 13px;
118
+ text-align: center;
119
+ }
120
+
121
+ .wide-error a {
122
+ color: var(--link);
123
+ text-decoration: none;
124
+ }
125
+
126
+ .wide-error a:hover {
127
+ text-decoration: underline;
128
+ }
129
+
130
+ /* Scripture ribbon blockquote */
131
+ .wide-ribbon {
132
+ border-left: 4px solid var(--accent);
133
+ background: var(--ribbon);
134
+ margin: 0;
135
+ padding: 14px 16px;
136
+ }
137
+
138
+ .wide-ribbon.compact {
139
+ padding: 10px 12px;
140
+ }
141
+
142
+ /* Scripture text */
143
+ .wide-verse-text {
144
+ font-family: Georgia, 'Times New Roman', 'Palatino Linotype', serif;
145
+ font-size: 16px;
146
+ line-height: 1.75;
147
+ color: var(--text);
148
+ margin: 0 0 8px 0;
149
+ }
150
+
151
+ .wide-verse-text.compact {
152
+ font-size: 14px;
153
+ line-height: 1.6;
154
+ }
155
+
156
+ /* Original language text */
157
+ .wide-original {
158
+ font-size: 18px;
159
+ line-height: 1.8;
160
+ color: var(--muted);
161
+ margin: 8px 0 0 0;
162
+ font-family: serif;
163
+ direction: auto;
164
+ }
165
+
166
+ /* Content area */
167
+ .wide-body {
168
+ padding: 14px 16px 12px;
169
+ }
170
+
171
+ .wide-body.compact {
172
+ padding: 10px 12px 10px;
173
+ }
174
+
175
+ /* Reference + badges */
176
+ .wide-meta {
177
+ display: flex;
178
+ align-items: center;
179
+ flex-wrap: wrap;
180
+ gap: 6px;
181
+ margin-bottom: 10px;
182
+ }
183
+
184
+ .wide-ref {
185
+ font-size: 13px;
186
+ font-weight: 600;
187
+ color: var(--text);
188
+ }
189
+
190
+ .wide-badge {
191
+ display: inline-block;
192
+ font-size: 11px;
193
+ font-weight: 500;
194
+ padding: 2px 7px;
195
+ border-radius: 4px;
196
+ background: var(--badge-bg);
197
+ color: var(--badge-text);
198
+ text-transform: uppercase;
199
+ letter-spacing: 0.04em;
200
+ }
201
+
202
+ /* Actions row */
203
+ .wide-actions {
204
+ display: flex;
205
+ align-items: center;
206
+ justify-content: space-between;
207
+ gap: 8px;
208
+ padding: 10px 16px;
209
+ border-top: 1px solid var(--border);
210
+ background: var(--bg);
211
+ }
212
+
213
+ .wide-actions.compact {
214
+ padding: 8px 12px;
215
+ }
216
+
217
+ .wide-link {
218
+ font-size: 12px;
219
+ font-weight: 500;
220
+ color: var(--link);
221
+ text-decoration: none;
222
+ display: inline-flex;
223
+ align-items: center;
224
+ gap: 4px;
225
+ transition: opacity 0.15s;
226
+ }
227
+
228
+ .wide-link:hover {
229
+ opacity: 0.8;
230
+ text-decoration: underline;
231
+ }
232
+
233
+ .wide-link svg {
234
+ width: 12px;
235
+ height: 12px;
236
+ flex-shrink: 0;
237
+ }
238
+
239
+ .wide-copy-btn {
240
+ background: var(--copy-bg);
241
+ color: var(--text);
242
+ border: none;
243
+ border-radius: 5px;
244
+ padding: 5px 10px;
245
+ font-size: 12px;
246
+ cursor: pointer;
247
+ display: inline-flex;
248
+ align-items: center;
249
+ gap: 5px;
250
+ transition: background 0.15s;
251
+ font-family: inherit;
252
+ }
253
+
254
+ .wide-copy-btn:hover {
255
+ background: var(--copy-hover);
256
+ }
257
+
258
+ .wide-copy-btn svg {
259
+ width: 13px;
260
+ height: 13px;
261
+ }
262
+
263
+ /* Title */
264
+ .wide-title {
265
+ font-size: 17px;
266
+ font-weight: 700;
267
+ color: var(--text);
268
+ margin: 0 0 6px 0;
269
+ line-height: 1.3;
270
+ }
271
+
272
+ .wide-subtitle {
273
+ font-size: 13px;
274
+ color: var(--muted);
275
+ margin: 0 0 10px 0;
276
+ }
277
+
278
+ .wide-summary {
279
+ font-size: 14px;
280
+ color: var(--text);
281
+ margin: 0;
282
+ line-height: 1.65;
283
+ display: -webkit-box;
284
+ -webkit-line-clamp: 4;
285
+ -webkit-box-orient: vertical;
286
+ overflow: hidden;
287
+ }
288
+
289
+ /* Verse count / stat badges */
290
+ .wide-stat {
291
+ display: inline-flex;
292
+ align-items: center;
293
+ gap: 4px;
294
+ font-size: 12px;
295
+ color: var(--muted);
296
+ background: var(--badge-bg);
297
+ border-radius: 4px;
298
+ padding: 3px 8px;
299
+ }
300
+
301
+ /* Compare layout */
302
+ .wide-compare-grid {
303
+ display: grid;
304
+ grid-template-columns: 1fr 1fr;
305
+ gap: 12px;
306
+ padding: 14px 16px;
307
+ }
308
+
309
+ @media (max-width: 480px) {
310
+ .wide-compare-grid {
311
+ grid-template-columns: 1fr;
312
+ }
313
+ }
314
+
315
+ .wide-compare-col {
316
+ border-left: 3px solid var(--accent);
317
+ padding-left: 10px;
318
+ }
319
+
320
+ .wide-compare-label {
321
+ font-size: 11px;
322
+ font-weight: 600;
323
+ color: var(--muted);
324
+ text-transform: uppercase;
325
+ letter-spacing: 0.05em;
326
+ margin-bottom: 6px;
327
+ }
328
+
329
+ .wide-compare-text {
330
+ font-family: Georgia, serif;
331
+ font-size: 14px;
332
+ line-height: 1.7;
333
+ color: var(--text);
334
+ }
335
+
336
+ /* Search box */
337
+ .wide-search-wrap {
338
+ padding: 14px 16px;
339
+ }
340
+
341
+ .wide-search-form {
342
+ display: flex;
343
+ gap: 8px;
344
+ }
345
+
346
+ .wide-search-input {
347
+ flex: 1;
348
+ padding: 8px 12px;
349
+ border: 1px solid var(--input-border);
350
+ border-radius: 6px;
351
+ background: var(--input-bg);
352
+ color: var(--text);
353
+ font-size: 14px;
354
+ font-family: inherit;
355
+ outline: none;
356
+ transition: border-color 0.15s;
357
+ }
358
+
359
+ .wide-search-input:focus {
360
+ border-color: var(--input-focus);
361
+ box-shadow: 0 0 0 2px color-mix(in srgb, var(--input-focus) 20%, transparent);
362
+ }
363
+
364
+ .wide-search-input::placeholder {
365
+ color: var(--muted);
366
+ }
367
+
368
+ .wide-search-btn {
369
+ background: var(--accent);
370
+ color: #fff;
371
+ border: none;
372
+ border-radius: 6px;
373
+ padding: 8px 16px;
374
+ font-size: 14px;
375
+ font-weight: 500;
376
+ cursor: pointer;
377
+ font-family: inherit;
378
+ transition: opacity 0.15s;
379
+ white-space: nowrap;
380
+ }
381
+
382
+ .wide-search-btn:hover {
383
+ opacity: 0.9;
384
+ }
385
+
386
+ /* Powered by footer */
387
+ .wide-powered {
388
+ display: block;
389
+ text-align: center;
390
+ padding: 8px 16px;
391
+ font-size: 11px;
392
+ color: var(--muted);
393
+ border-top: 1px solid var(--border);
394
+ }
395
+
396
+ .wide-powered a {
397
+ color: var(--link);
398
+ text-decoration: none;
399
+ font-weight: 500;
400
+ }
401
+
402
+ .wide-powered a:hover {
403
+ text-decoration: underline;
404
+ }
405
+
406
+ /* VOTD header accent bar */
407
+ .wide-accent-bar {
408
+ height: 3px;
409
+ background: var(--accent);
410
+ width: 100%;
411
+ }
412
+
413
+ /* Person card avatar placeholder */
414
+ .wide-person-header {
415
+ display: flex;
416
+ align-items: center;
417
+ gap: 12px;
418
+ padding: 14px 16px 0;
419
+ }
420
+
421
+ .wide-person-icon {
422
+ width: 42px;
423
+ height: 42px;
424
+ border-radius: 50%;
425
+ background: var(--badge-bg);
426
+ display: flex;
427
+ align-items: center;
428
+ justify-content: center;
429
+ flex-shrink: 0;
430
+ color: var(--accent);
431
+ }
432
+
433
+ .wide-person-icon svg {
434
+ width: 22px;
435
+ height: 22px;
436
+ }
437
+
438
+ .wide-person-name {
439
+ font-size: 16px;
440
+ font-weight: 700;
441
+ color: var(--text);
442
+ margin: 0;
443
+ }
444
+
445
+ .wide-person-era {
446
+ font-size: 12px;
447
+ color: var(--muted);
448
+ margin: 2px 0 0 0;
449
+ }
450
+ `;
451
+ }
452
+
453
+ // src/shadow.ts
454
+ function createShadow(el, config) {
455
+ const shadow = el.attachShadow({ mode: "open" });
456
+ const style = document.createElement("style");
457
+ style.textContent = getThemeCSS(config.accent);
458
+ shadow.appendChild(style);
459
+ return shadow;
460
+ }
461
+ function createWidgetRoot(shadow, el, extraClass) {
462
+ const theme = el.dataset.theme || "light";
463
+ const size = el.dataset.size || "default";
464
+ const div = document.createElement("div");
465
+ div.className = ["wide-widget", extraClass].filter(Boolean).join(" ");
466
+ div.setAttribute("data-theme", theme);
467
+ div.setAttribute("data-size", size);
468
+ shadow.appendChild(div);
469
+ return div;
470
+ }
471
+ function renderLoading(container) {
472
+ container.innerHTML = `
473
+ <div class="wide-loading">
474
+ <span class="wide-spinner"></span>
475
+ Loading\u2026
476
+ </div>
477
+ `;
478
+ }
479
+ function renderError(container, message, config) {
480
+ container.innerHTML = `
481
+ <div class="wide-error">
482
+ <p>${message}</p>
483
+ <a href="https://${config.domain}" target="_blank" rel="noopener">
484
+ Visit ${config.name}
485
+ </a>
486
+ </div>
487
+ `;
488
+ }
489
+ var externalLinkIcon = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6"/><polyline points="15 3 21 3 21 9"/><line x1="10" y1="14" x2="21" y2="3"/></svg>`;
490
+ var copyIcon = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="9" y="9" width="13" height="13" rx="2" ry="2"/><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"/></svg>`;
491
+ var checkIcon = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><polyline points="20 6 9 17 4 12"/></svg>`;
492
+ var personIcon = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2"/><circle cx="12" cy="7" r="4"/></svg>`;
493
+ function poweredByHTML(config) {
494
+ return `<span class="wide-powered">Powered by <a href="https://${config.domain}" target="_blank" rel="noopener">${config.name}</a></span>`;
495
+ }
496
+ function bindCopyButton(btn, text) {
497
+ btn.addEventListener("click", () => {
498
+ var _a;
499
+ (_a = navigator.clipboard) == null ? void 0 : _a.writeText(text).then(() => {
500
+ btn.innerHTML = `${checkIcon} Copied!`;
501
+ setTimeout(() => {
502
+ btn.innerHTML = `${copyIcon} Copy`;
503
+ }, 2e3);
504
+ }).catch(() => {
505
+ const ta = document.createElement("textarea");
506
+ ta.value = text;
507
+ ta.style.position = "fixed";
508
+ ta.style.opacity = "0";
509
+ document.body.appendChild(ta);
510
+ ta.select();
511
+ document.execCommand("copy");
512
+ document.body.removeChild(ta);
513
+ btn.innerHTML = `${checkIcon} Copied!`;
514
+ setTimeout(() => {
515
+ btn.innerHTML = `${copyIcon} Copy`;
516
+ }, 2e3);
517
+ });
518
+ });
519
+ }
520
+
521
+ // src/widgets/verse.ts
522
+ function parseRef(ref) {
523
+ const withBook = ref.match(/^(.+?)\s+(\d+):(\d+)$/);
524
+ if (withBook) {
525
+ return { book: withBook[1], chapter: withBook[2], verse: withBook[3] };
526
+ }
527
+ const noBook = ref.match(/^(\d+):(\d+)$/);
528
+ if (noBook) {
529
+ return { book: "", chapter: noBook[1], verse: noBook[2] };
530
+ }
531
+ return null;
532
+ }
533
+ function buildApiUrl(config, ref, translation) {
534
+ const parsed = parseRef(ref);
535
+ if (!parsed) return "";
536
+ const params = new URLSearchParams();
537
+ if (parsed.book) params.set("book", parsed.book);
538
+ params.set("chapter", parsed.chapter);
539
+ params.set("verse", parsed.verse);
540
+ if (translation) params.set("translation", translation);
541
+ return `${config.apiBase}/verses/?${params.toString()}`;
542
+ }
543
+ function renderVerseWidget(container, data, el, config) {
544
+ const isCompact = el.dataset.size === "compact";
545
+ const showOriginal = el.dataset.showOriginal === "true";
546
+ const ref = data.reference || el.dataset.ref || "";
547
+ const translation = data.translation || config.defaultTranslation;
548
+ const verseUrl = data.url || `https://${config.domain}`;
549
+ const originalHtml = showOriginal && data.original_text ? `<p class="wide-original" lang="${data.original_language || ""}">${data.original_text}</p>` : "";
550
+ container.innerHTML = `
551
+ <div class="wide-ribbon${isCompact ? " compact" : ""}">
552
+ <p class="wide-verse-text${isCompact ? " compact" : ""}">${data.text}</p>
553
+ ${originalHtml}
554
+ </div>
555
+ <div class="wide-body${isCompact ? " compact" : ""}">
556
+ <div class="wide-meta">
557
+ <span class="wide-ref">${ref}</span>
558
+ <span class="wide-badge">${translation.toUpperCase()}</span>
559
+ </div>
560
+ </div>
561
+ <div class="wide-actions${isCompact ? " compact" : ""}">
562
+ <a class="wide-link" href="${verseUrl}" target="_blank" rel="noopener">
563
+ Read on ${config.name} ${externalLinkIcon}
564
+ </a>
565
+ <button class="wide-copy-btn" type="button">
566
+ ${copyIcon} Copy
567
+ </button>
568
+ </div>
569
+ ${poweredByHTML(config)}
570
+ `;
571
+ const copyBtn = container.querySelector(".wide-copy-btn");
572
+ if (copyBtn) {
573
+ bindCopyButton(copyBtn, `${data.text} \u2014 ${ref}`);
574
+ }
575
+ }
576
+ function initVerseWidget(el, config) {
577
+ const shadow = createShadow(el, config);
578
+ const container = createWidgetRoot(shadow, el);
579
+ renderLoading(container);
580
+ const ref = el.dataset.ref || "";
581
+ const translation = el.dataset.translation || config.defaultTranslation;
582
+ if (!ref) {
583
+ renderError(container, "Missing data-ref attribute.", config);
584
+ return;
585
+ }
586
+ const url = buildApiUrl(config, ref, translation);
587
+ if (!url) {
588
+ renderError(container, `Could not parse reference: "${ref}"`, config);
589
+ return;
590
+ }
591
+ fetch(url).then((r) => {
592
+ if (!r.ok) throw new Error(`HTTP ${r.status}`);
593
+ return r.json();
594
+ }).then((data) => {
595
+ const verse = "results" in data && Array.isArray(data.results) ? data.results[0] : data;
596
+ if (!verse || !verse.text) throw new Error("No verse found");
597
+ renderVerseWidget(container, verse, el, config);
598
+ }).catch(() => {
599
+ renderError(
600
+ container,
601
+ `Could not load "${ref}". Visit ${config.name} to read scripture.`,
602
+ config
603
+ );
604
+ });
605
+ }
606
+
607
+ // src/widgets/chapter.ts
608
+ function parseChapterRef(ref) {
609
+ const withBook = ref.match(/^(.+?)\s+(\d+)$/);
610
+ if (withBook) return { book: withBook[1], chapter: withBook[2] };
611
+ const noBook = ref.match(/^(\d+)$/);
612
+ if (noBook) return { book: "", chapter: noBook[1] };
613
+ return null;
614
+ }
615
+ function truncateWords(text, wordLimit) {
616
+ const words = text.split(/\s+/);
617
+ if (words.length <= wordLimit) return text;
618
+ return words.slice(0, wordLimit).join(" ") + "\u2026";
619
+ }
620
+ function initChapterWidget(el, config) {
621
+ const shadow = createShadow(el, config);
622
+ const container = createWidgetRoot(shadow, el);
623
+ renderLoading(container);
624
+ const ref = el.dataset.ref || "";
625
+ if (!ref) {
626
+ renderError(container, "Missing data-ref attribute.", config);
627
+ return;
628
+ }
629
+ const parsed = parseChapterRef(ref);
630
+ if (!parsed) {
631
+ renderError(container, `Could not parse chapter reference: "${ref}"`, config);
632
+ return;
633
+ }
634
+ const apiUrl = parsed.book ? `${config.apiBase}/chapters/${encodeURIComponent(parsed.book)}/${parsed.chapter}/` : `${config.apiBase}/chapters/${parsed.chapter}/`;
635
+ fetch(apiUrl).then((r) => {
636
+ if (!r.ok) throw new Error(`HTTP ${r.status}`);
637
+ return r.json();
638
+ }).then((data) => {
639
+ var _a;
640
+ const title = data.title || data.name || ref;
641
+ const bookName = data.book_name || data.book || "";
642
+ const summary = data.summary || data.description || "";
643
+ const verseCount = (_a = data.verse_count) != null ? _a : Array.isArray(data.verses) ? data.verses.length : null;
644
+ const chapterUrl = data.url || `https://${config.domain}`;
645
+ const summaryText = summary ? truncateWords(summary, 100) : "";
646
+ container.innerHTML = `
647
+ <div class="wide-body">
648
+ <div class="wide-meta">
649
+ ${bookName ? `<span class="wide-badge">${bookName}</span>` : ""}
650
+ ${verseCount != null ? `<span class="wide-stat">${verseCount} verses</span>` : ""}
651
+ </div>
652
+ <h3 class="wide-title">${title}</h3>
653
+ ${summaryText ? `<p class="wide-summary">${summaryText}</p>` : ""}
654
+ </div>
655
+ <div class="wide-actions">
656
+ <a class="wide-link" href="${chapterUrl}" target="_blank" rel="noopener">
657
+ Read full chapter on ${config.name} ${externalLinkIcon}
658
+ </a>
659
+ </div>
660
+ ${poweredByHTML(config)}
661
+ `;
662
+ }).catch(() => {
663
+ renderError(
664
+ container,
665
+ `Could not load chapter "${ref}". Visit ${config.name} to read scripture.`,
666
+ config
667
+ );
668
+ });
669
+ }
670
+
671
+ // src/widgets/person.ts
672
+ function truncateWords2(text, wordLimit) {
673
+ const words = text.split(/\s+/);
674
+ if (words.length <= wordLimit) return text;
675
+ return words.slice(0, wordLimit).join(" ") + "\u2026";
676
+ }
677
+ function initPersonWidget(el, config) {
678
+ const shadow = createShadow(el, config);
679
+ const container = createWidgetRoot(shadow, el);
680
+ renderLoading(container);
681
+ const slug = el.dataset.slug || "";
682
+ if (!slug) {
683
+ renderError(container, "Missing data-slug attribute.", config);
684
+ return;
685
+ }
686
+ const apiUrl = `${config.apiBase}/people/${encodeURIComponent(slug)}/`;
687
+ fetch(apiUrl).then((r) => {
688
+ if (!r.ok) throw new Error(`HTTP ${r.status}`);
689
+ return r.json();
690
+ }).then((data) => {
691
+ var _a, _b;
692
+ const name = data.name || data.full_name || slug;
693
+ const era = data.era || data.period || "";
694
+ const bio = data.bio || data.description || "";
695
+ const verseCount = (_b = (_a = data.verse_count) != null ? _a : data.mentioned_verses) != null ? _b : null;
696
+ const personUrl = data.url || `https://${config.domain}/people/${slug}/`;
697
+ const bioText = bio ? truncateWords2(bio, 150) : "";
698
+ container.innerHTML = `
699
+ <div class="wide-person-header">
700
+ <div class="wide-person-icon">${personIcon}</div>
701
+ <div>
702
+ <p class="wide-person-name">${name}</p>
703
+ ${era ? `<p class="wide-person-era">${era}</p>` : ""}
704
+ </div>
705
+ </div>
706
+ <div class="wide-body">
707
+ <div class="wide-meta">
708
+ ${verseCount != null ? `<span class="wide-stat">${verseCount} verses</span>` : ""}
709
+ </div>
710
+ ${bioText ? `<p class="wide-summary">${bioText}</p>` : ""}
711
+ </div>
712
+ <div class="wide-actions">
713
+ <a class="wide-link" href="${personUrl}" target="_blank" rel="noopener">
714
+ Learn more on ${config.name} ${externalLinkIcon}
715
+ </a>
716
+ </div>
717
+ ${poweredByHTML(config)}
718
+ `;
719
+ }).catch(() => {
720
+ renderError(
721
+ container,
722
+ `Could not load "${slug}". Visit ${config.name} for biblical figures.`,
723
+ config
724
+ );
725
+ });
726
+ }
727
+
728
+ // src/widgets/compare.ts
729
+ function parseRef2(ref) {
730
+ const params = new URLSearchParams();
731
+ const withBook = ref.match(/^(.+?)\s+(\d+):(\d+)$/);
732
+ if (withBook) {
733
+ params.set("book", withBook[1]);
734
+ params.set("chapter", withBook[2]);
735
+ params.set("verse", withBook[3]);
736
+ return params;
737
+ }
738
+ const noBook = ref.match(/^(\d+):(\d+)$/);
739
+ if (noBook) {
740
+ params.set("chapter", noBook[1]);
741
+ params.set("verse", noBook[2]);
742
+ }
743
+ return params;
744
+ }
745
+ async function fetchVerse(config, ref) {
746
+ var _a, _b;
747
+ const params = parseRef2(ref);
748
+ const url = `${config.apiBase}/verses/?${params.toString()}`;
749
+ const r = await fetch(url);
750
+ if (!r.ok) throw new Error(`HTTP ${r.status}`);
751
+ const data = await r.json();
752
+ const verse = (_b = (_a = data.results) == null ? void 0 : _a[0]) != null ? _b : data;
753
+ if (!(verse == null ? void 0 : verse.text)) throw new Error("No verse data");
754
+ return { text: verse.text, reference: verse.reference || ref, url: verse.url };
755
+ }
756
+ function initCompareWidget(el, config) {
757
+ const shadow = createShadow(el, config);
758
+ const container = createWidgetRoot(shadow, el);
759
+ renderLoading(container);
760
+ const refA = el.dataset.a || "";
761
+ const refB = el.dataset.b || "";
762
+ if (!refA || !refB) {
763
+ renderError(container, "Missing data-a or data-b attributes.", config);
764
+ return;
765
+ }
766
+ Promise.all([fetchVerse(config, refA), fetchVerse(config, refB)]).then(([verseA, verseB]) => {
767
+ const compareUrl = `https://${config.domain}/compare/`;
768
+ container.innerHTML = `
769
+ <div class="wide-compare-grid">
770
+ <div class="wide-compare-col">
771
+ <p class="wide-compare-label">${verseA.reference}</p>
772
+ <p class="wide-compare-text">${verseA.text}</p>
773
+ </div>
774
+ <div class="wide-compare-col">
775
+ <p class="wide-compare-label">${verseB.reference}</p>
776
+ <p class="wide-compare-text">${verseB.text}</p>
777
+ </div>
778
+ </div>
779
+ <div class="wide-actions">
780
+ <a class="wide-link" href="${compareUrl}" target="_blank" rel="noopener">
781
+ Compare on ${config.name} ${externalLinkIcon}
782
+ </a>
783
+ </div>
784
+ ${poweredByHTML(config)}
785
+ `;
786
+ }).catch(() => {
787
+ renderError(
788
+ container,
789
+ `Could not load comparison. Visit ${config.name} to compare scripture.`,
790
+ config
791
+ );
792
+ });
793
+ }
794
+
795
+ // src/widgets/votd.ts
796
+ var CACHE_PREFIX = "wide_votd_";
797
+ function getTodayKey() {
798
+ return (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
799
+ }
800
+ function getCached(site) {
801
+ try {
802
+ const key = `${CACHE_PREFIX}${site}`;
803
+ const raw = localStorage.getItem(key);
804
+ if (!raw) return null;
805
+ const parsed = JSON.parse(raw);
806
+ if (parsed.date !== getTodayKey()) {
807
+ localStorage.removeItem(key);
808
+ return null;
809
+ }
810
+ return parsed.data;
811
+ } catch (e) {
812
+ return null;
813
+ }
814
+ }
815
+ function setCache(site, data) {
816
+ try {
817
+ const key = `${CACHE_PREFIX}${site}`;
818
+ localStorage.setItem(key, JSON.stringify({ date: getTodayKey(), data }));
819
+ } catch (e) {
820
+ }
821
+ }
822
+ function initVotdWidget(el, config) {
823
+ const shadow = createShadow(el, config);
824
+ const container = createWidgetRoot(shadow, el);
825
+ renderLoading(container);
826
+ const isCompact = el.dataset.size === "compact";
827
+ function render(data) {
828
+ const translation = data.translation || config.defaultTranslation;
829
+ const verseUrl = data.url || `https://${config.domain}`;
830
+ container.innerHTML = `
831
+ <div class="wide-accent-bar"></div>
832
+ <div class="wide-ribbon${isCompact ? " compact" : ""}">
833
+ <p class="wide-verse-text${isCompact ? " compact" : ""}">${data.text}</p>
834
+ </div>
835
+ <div class="wide-body${isCompact ? " compact" : ""}">
836
+ <div class="wide-meta">
837
+ <span class="wide-ref">${data.reference}</span>
838
+ <span class="wide-badge">${translation.toUpperCase()}</span>
839
+ <span class="wide-badge">Verse of the Day</span>
840
+ </div>
841
+ </div>
842
+ <div class="wide-actions${isCompact ? " compact" : ""}">
843
+ <a class="wide-link" href="${verseUrl}" target="_blank" rel="noopener">
844
+ More at ${config.name} ${externalLinkIcon}
845
+ </a>
846
+ <button class="wide-copy-btn" type="button">
847
+ ${copyIcon} Copy
848
+ </button>
849
+ </div>
850
+ ${poweredByHTML(config)}
851
+ `;
852
+ const copyBtn = container.querySelector(".wide-copy-btn");
853
+ if (copyBtn) {
854
+ bindCopyButton(copyBtn, `${data.text} \u2014 ${data.reference}`);
855
+ }
856
+ }
857
+ const cached = getCached(config.site);
858
+ if (cached) {
859
+ render(cached);
860
+ return;
861
+ }
862
+ fetch(config.votdEndpoint).then((r) => {
863
+ if (!r.ok) throw new Error(`HTTP ${r.status}`);
864
+ return r.json();
865
+ }).then((data) => {
866
+ if (!(data == null ? void 0 : data.text)) throw new Error("No VOTD data");
867
+ setCache(config.site, data);
868
+ render(data);
869
+ }).catch(() => {
870
+ renderError(
871
+ container,
872
+ `Could not load verse of the day. Visit ${config.name} for daily scripture.`,
873
+ config
874
+ );
875
+ });
876
+ }
877
+
878
+ // src/widgets/search.ts
879
+ function initSearchWidget(el, config) {
880
+ const shadow = createShadow(el, config);
881
+ const container = createWidgetRoot(shadow, el);
882
+ const placeholder = el.dataset.placeholder || `Search ${config.scriptureLabel}\u2026`;
883
+ container.innerHTML = `
884
+ <div class="wide-search-wrap">
885
+ <form class="wide-search-form" action="https://${config.domain}${config.searchPath}" method="get" target="_blank">
886
+ <input
887
+ class="wide-search-input"
888
+ type="search"
889
+ name="q"
890
+ placeholder="${placeholder}"
891
+ aria-label="${placeholder}"
892
+ autocomplete="off"
893
+ />
894
+ <button class="wide-search-btn" type="submit">Search</button>
895
+ </form>
896
+ </div>
897
+ ${poweredByHTML(config)}
898
+ `;
899
+ }
900
+
901
+ // src/core.ts
902
+ function initWidget(el, type, config) {
903
+ switch (type) {
904
+ case "verse":
905
+ initVerseWidget(el, config);
906
+ break;
907
+ case "chapter":
908
+ initChapterWidget(el, config);
909
+ break;
910
+ case "person":
911
+ initPersonWidget(el, config);
912
+ break;
913
+ case "compare":
914
+ initCompareWidget(el, config);
915
+ break;
916
+ case "votd":
917
+ initVotdWidget(el, config);
918
+ break;
919
+ case "search":
920
+ initSearchWidget(el, config);
921
+ break;
922
+ default:
923
+ break;
924
+ }
925
+ }
926
+ function initAll(config) {
927
+ const elements = document.querySelectorAll(`[${config.attribute}]`);
928
+ elements.forEach((el) => {
929
+ if (el.shadowRoot) return;
930
+ const widgetType = el.dataset[config.attribute.replace("data-", "")];
931
+ if (!widgetType) return;
932
+ initWidget(el, widgetType, config);
933
+ });
934
+ }
935
+ (function bootstrap() {
936
+ const config = define_SITE_CONFIG_default;
937
+ if (document.readyState === "loading") {
938
+ document.addEventListener("DOMContentLoaded", () => initAll(config));
939
+ } else {
940
+ initAll(config);
941
+ }
942
+ const observer = new MutationObserver((mutations) => {
943
+ mutations.forEach((mutation) => {
944
+ mutation.addedNodes.forEach((node) => {
945
+ var _a;
946
+ if (node.nodeType !== Node.ELEMENT_NODE) return;
947
+ const el = node;
948
+ if (el.hasAttribute(config.attribute) && !el.shadowRoot) {
949
+ const widgetType = el.dataset[config.attribute.replace("data-", "")];
950
+ if (widgetType) initWidget(el, widgetType, config);
951
+ }
952
+ (_a = el.querySelectorAll) == null ? void 0 : _a.call(el, `[${config.attribute}]`).forEach((child) => {
953
+ if (!child.shadowRoot) {
954
+ const widgetType = child.dataset[config.attribute.replace("data-", "")];
955
+ if (widgetType) initWidget(child, widgetType, config);
956
+ }
957
+ });
958
+ });
959
+ });
960
+ });
961
+ observer.observe(document.body || document.documentElement, {
962
+ childList: true,
963
+ subtree: true
964
+ });
965
+ })();