peasy-seo-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,2856 @@
1
+ /* peasy-seo-embed v1.0.0 | MIT | https://widget.peasyseo.com */
2
+
3
+ // <define:INJECTED_CONFIG>
4
+ var define_INJECTED_CONFIG_default = { name: "PeasySEO", slug: "seo", domain: "peasyseo.com", apiBase: "https://peasyseo.com/api/v1", embedBase: "https://peasyseo.com/embed", accent: "#22C55E", attribute: "data-peasy-seo", siteWidgets: [] };
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
+ /* \u2500\u2500\u2500 Light theme \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */
16
+ .peasy-widget[data-theme="light"] {
17
+ --bg: #ffffff;
18
+ --text: #1a1a1a;
19
+ --border: #e5e7eb;
20
+ --accent: var(--site-accent);
21
+ --muted: #6b7280;
22
+ --ribbon: #f9fafb;
23
+ --badge-bg: #f3f4f6;
24
+ --badge-text: #374151;
25
+ --link: var(--site-accent);
26
+ --copy-bg: #f3f4f6;
27
+ --copy-hover: #e5e7eb;
28
+ --input-bg: #ffffff;
29
+ --input-border: #d1d5db;
30
+ --input-focus: var(--site-accent);
31
+ --shadow: 0 1px 3px rgba(0,0,0,0.08);
32
+ }
33
+
34
+ /* \u2500\u2500\u2500 Dark theme \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */
35
+ .peasy-widget[data-theme="dark"] {
36
+ --bg: #18181b;
37
+ --text: #f4f4f5;
38
+ --border: #3f3f46;
39
+ --accent: var(--site-accent);
40
+ --muted: #a1a1aa;
41
+ --ribbon: #111112;
42
+ --badge-bg: #3f3f46;
43
+ --badge-text: #d4d4d8;
44
+ --link: #93c5fd;
45
+ --copy-bg: #3f3f46;
46
+ --copy-hover: #52525b;
47
+ --input-bg: #111112;
48
+ --input-border: #52525b;
49
+ --input-focus: var(--site-accent);
50
+ --shadow: 0 1px 3px rgba(0,0,0,0.4);
51
+ }
52
+
53
+ /* \u2500\u2500\u2500 Sepia theme \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */
54
+ .peasy-widget[data-theme="sepia"] {
55
+ --bg: #f5f0e8;
56
+ --text: #3d3529;
57
+ --border: #d4c5a9;
58
+ --accent: var(--site-accent);
59
+ --muted: #8b7e6a;
60
+ --ribbon: #ede8df;
61
+ --badge-bg: #e8e0d0;
62
+ --badge-text: #5c4f3d;
63
+ --link: #7c5c3b;
64
+ --copy-bg: #e8e0d0;
65
+ --copy-hover: #ddd4c0;
66
+ --input-bg: #f5f0e8;
67
+ --input-border: #c4b49a;
68
+ --input-focus: var(--site-accent);
69
+ --shadow: 0 1px 3px rgba(61,53,41,0.12);
70
+ }
71
+
72
+ /* \u2500\u2500\u2500 Auto theme (follows OS preference) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */
73
+ @media (prefers-color-scheme: dark) {
74
+ .peasy-widget[data-theme="auto"] {
75
+ --bg: #18181b;
76
+ --text: #f4f4f5;
77
+ --border: #3f3f46;
78
+ --accent: var(--site-accent);
79
+ --muted: #a1a1aa;
80
+ --ribbon: #111112;
81
+ --badge-bg: #3f3f46;
82
+ --badge-text: #d4d4d8;
83
+ --link: #93c5fd;
84
+ --copy-bg: #3f3f46;
85
+ --copy-hover: #52525b;
86
+ --input-bg: #111112;
87
+ --input-border: #52525b;
88
+ --input-focus: var(--site-accent);
89
+ --shadow: 0 1px 3px rgba(0,0,0,0.4);
90
+ }
91
+ }
92
+
93
+ @media (prefers-color-scheme: light) {
94
+ .peasy-widget[data-theme="auto"] {
95
+ --bg: #ffffff;
96
+ --text: #1a1a1a;
97
+ --border: #e5e7eb;
98
+ --accent: var(--site-accent);
99
+ --muted: #6b7280;
100
+ --ribbon: #f9fafb;
101
+ --badge-bg: #f3f4f6;
102
+ --badge-text: #374151;
103
+ --link: var(--site-accent);
104
+ --copy-bg: #f3f4f6;
105
+ --copy-hover: #e5e7eb;
106
+ --input-bg: #ffffff;
107
+ --input-border: #d1d5db;
108
+ --input-focus: var(--site-accent);
109
+ --shadow: 0 1px 3px rgba(0,0,0,0.08);
110
+ }
111
+ }
112
+
113
+ /* \u2500\u2500\u2500 Base widget \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */
114
+ .peasy-widget {
115
+ box-sizing: border-box;
116
+ border-radius: 8px;
117
+ overflow: hidden;
118
+ border: 1px solid var(--border);
119
+ border-left: 3px solid var(--accent);
120
+ background: var(--bg);
121
+ color: var(--text);
122
+ font-size: 14px;
123
+ line-height: 1.6;
124
+ transition: border-color 0.2s;
125
+ font-family: system-ui, -apple-system, 'Segoe UI', Roboto, sans-serif;
126
+ }
127
+
128
+ .peasy-widget *, .peasy-widget *::before, .peasy-widget *::after {
129
+ box-sizing: border-box;
130
+ }
131
+
132
+ /* \u2500\u2500\u2500 Size variants \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */
133
+ .peasy-widget[data-size="compact"] {
134
+ max-width: 280px;
135
+ font-size: 13px;
136
+ }
137
+
138
+ .peasy-widget[data-size="default"] {
139
+ max-width: 480px;
140
+ font-size: 14px;
141
+ }
142
+
143
+ .peasy-widget[data-size="large"] {
144
+ max-width: 720px;
145
+ font-size: 14px;
146
+ }
147
+
148
+ /* \u2500\u2500\u2500 Loading skeleton \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */
149
+ .peasy-loading {
150
+ padding: 20px 16px;
151
+ text-align: center;
152
+ color: var(--muted);
153
+ font-size: 13px;
154
+ display: flex;
155
+ align-items: center;
156
+ justify-content: center;
157
+ gap: 8px;
158
+ }
159
+
160
+ .peasy-spinner {
161
+ width: 16px;
162
+ height: 16px;
163
+ border: 2px solid var(--border);
164
+ border-top-color: var(--accent);
165
+ border-radius: 50%;
166
+ animation: peasy-spin 0.7s linear infinite;
167
+ display: inline-block;
168
+ flex-shrink: 0;
169
+ }
170
+
171
+ @keyframes peasy-spin {
172
+ to { transform: rotate(360deg); }
173
+ }
174
+
175
+ /* \u2500\u2500\u2500 Error state \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */
176
+ .peasy-error {
177
+ padding: 16px;
178
+ color: var(--muted);
179
+ font-size: 13px;
180
+ text-align: center;
181
+ }
182
+
183
+ .peasy-error a {
184
+ color: var(--link);
185
+ text-decoration: none;
186
+ }
187
+
188
+ .peasy-error a:hover {
189
+ text-decoration: underline;
190
+ }
191
+
192
+ /* \u2500\u2500\u2500 Powered by footer \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */
193
+ .peasy-powered {
194
+ display: block;
195
+ text-align: center;
196
+ padding: 8px 16px;
197
+ font-size: 11px;
198
+ color: var(--muted);
199
+ border-top: 1px solid var(--border);
200
+ }
201
+
202
+ .peasy-powered a {
203
+ color: var(--link);
204
+ text-decoration: none;
205
+ font-weight: 500;
206
+ }
207
+
208
+ .peasy-powered a:hover {
209
+ text-decoration: underline;
210
+ }
211
+
212
+ /* \u2500\u2500\u2500 Button \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */
213
+ .peasy-btn {
214
+ background: var(--accent);
215
+ color: #fff;
216
+ border: none;
217
+ border-radius: 6px;
218
+ padding: 8px 16px;
219
+ font-size: 14px;
220
+ font-weight: 500;
221
+ cursor: pointer;
222
+ font-family: inherit;
223
+ transition: opacity 0.15s;
224
+ white-space: nowrap;
225
+ display: inline-flex;
226
+ align-items: center;
227
+ gap: 5px;
228
+ }
229
+
230
+ .peasy-btn:hover {
231
+ opacity: 0.88;
232
+ }
233
+
234
+ .peasy-btn svg {
235
+ width: 14px;
236
+ height: 14px;
237
+ }
238
+
239
+ /* \u2500\u2500\u2500 Copy button \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */
240
+ .peasy-copy-btn {
241
+ background: var(--copy-bg);
242
+ color: var(--text);
243
+ border: none;
244
+ border-radius: 5px;
245
+ padding: 5px 10px;
246
+ font-size: 12px;
247
+ cursor: pointer;
248
+ display: inline-flex;
249
+ align-items: center;
250
+ gap: 5px;
251
+ transition: background 0.15s;
252
+ font-family: inherit;
253
+ }
254
+
255
+ .peasy-copy-btn:hover {
256
+ background: var(--copy-hover);
257
+ }
258
+
259
+ .peasy-copy-btn svg {
260
+ width: 13px;
261
+ height: 13px;
262
+ }
263
+
264
+ /* \u2500\u2500\u2500 Badge (inline) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */
265
+ .peasy-badge {
266
+ display: inline-flex;
267
+ align-items: center;
268
+ gap: 4px;
269
+ font-size: 11px;
270
+ font-weight: 500;
271
+ padding: 2px 8px;
272
+ border-radius: 4px;
273
+ background: var(--badge-bg);
274
+ color: var(--badge-text);
275
+ text-transform: uppercase;
276
+ letter-spacing: 0.04em;
277
+ border-left: 3px solid var(--accent);
278
+ font-family: inherit;
279
+ }
280
+
281
+ /* \u2500\u2500\u2500 Content area \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */
282
+ .peasy-body {
283
+ padding: 14px 16px 12px;
284
+ }
285
+
286
+ .peasy-widget[data-size="compact"] .peasy-body {
287
+ padding: 10px 12px;
288
+ }
289
+
290
+ /* \u2500\u2500\u2500 Title / subtitle \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */
291
+ .peasy-title {
292
+ font-size: 16px;
293
+ font-weight: 700;
294
+ color: var(--text);
295
+ margin: 0 0 4px 0;
296
+ line-height: 1.3;
297
+ }
298
+
299
+ .peasy-subtitle {
300
+ font-size: 13px;
301
+ color: var(--muted);
302
+ margin: 0 0 10px 0;
303
+ }
304
+
305
+ .peasy-summary {
306
+ font-size: 14px;
307
+ color: var(--text);
308
+ margin: 0;
309
+ line-height: 1.65;
310
+ display: -webkit-box;
311
+ -webkit-line-clamp: 4;
312
+ -webkit-box-orient: vertical;
313
+ overflow: hidden;
314
+ }
315
+
316
+ /* \u2500\u2500\u2500 Actions row \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */
317
+ .peasy-actions {
318
+ display: flex;
319
+ align-items: center;
320
+ justify-content: space-between;
321
+ gap: 8px;
322
+ padding: 10px 16px;
323
+ border-top: 1px solid var(--border);
324
+ background: var(--bg);
325
+ }
326
+
327
+ .peasy-widget[data-size="compact"] .peasy-actions {
328
+ padding: 8px 12px;
329
+ }
330
+
331
+ /* \u2500\u2500\u2500 Link \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */
332
+ .peasy-link {
333
+ font-size: 12px;
334
+ font-weight: 500;
335
+ color: var(--link);
336
+ text-decoration: none;
337
+ display: inline-flex;
338
+ align-items: center;
339
+ gap: 4px;
340
+ transition: opacity 0.15s;
341
+ }
342
+
343
+ .peasy-link:hover {
344
+ opacity: 0.8;
345
+ text-decoration: underline;
346
+ }
347
+
348
+ .peasy-link svg {
349
+ width: 12px;
350
+ height: 12px;
351
+ flex-shrink: 0;
352
+ }
353
+
354
+ /* \u2500\u2500\u2500 Search box \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */
355
+ .peasy-search-wrap {
356
+ padding: 14px 16px;
357
+ }
358
+
359
+ .peasy-search-form {
360
+ display: flex;
361
+ gap: 8px;
362
+ }
363
+
364
+ .peasy-search-input {
365
+ flex: 1;
366
+ padding: 8px 12px;
367
+ border: 1px solid var(--input-border);
368
+ border-radius: 6px;
369
+ background: var(--input-bg);
370
+ color: var(--text);
371
+ font-size: 14px;
372
+ font-family: inherit;
373
+ outline: none;
374
+ transition: border-color 0.15s;
375
+ }
376
+
377
+ .peasy-search-input:focus {
378
+ border-color: var(--input-focus);
379
+ box-shadow: 0 0 0 2px color-mix(in srgb, var(--input-focus) 20%, transparent);
380
+ }
381
+
382
+ .peasy-search-input::placeholder {
383
+ color: var(--muted);
384
+ }
385
+
386
+ /* \u2500\u2500\u2500 Meta row (badges, stats) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */
387
+ .peasy-meta {
388
+ display: flex;
389
+ align-items: center;
390
+ flex-wrap: wrap;
391
+ gap: 6px;
392
+ margin-bottom: 10px;
393
+ }
394
+
395
+ .peasy-stat {
396
+ display: inline-flex;
397
+ align-items: center;
398
+ gap: 4px;
399
+ font-size: 12px;
400
+ color: var(--muted);
401
+ background: var(--badge-bg);
402
+ border-radius: 4px;
403
+ padding: 3px 8px;
404
+ }
405
+
406
+ /* \u2500\u2500\u2500 Fade-in animation \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */
407
+ @keyframes peasy-fade-in {
408
+ from { opacity: 0; transform: translateY(4px); }
409
+ to { opacity: 1; transform: translateY(0); }
410
+ }
411
+
412
+ .peasy-widget {
413
+ animation: peasy-fade-in 0.2s ease-out;
414
+ }
415
+
416
+ /* \u2500\u2500\u2500 Reduced motion \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */
417
+ @media (prefers-reduced-motion: reduce) {
418
+ .peasy-widget {
419
+ animation: none;
420
+ }
421
+ .peasy-spinner {
422
+ animation: none;
423
+ border-top-color: var(--accent);
424
+ opacity: 0.6;
425
+ }
426
+ }
427
+ `;
428
+ }
429
+
430
+ // src/shadow.ts
431
+ function createShadow(el, config) {
432
+ const shadow = el.attachShadow({ mode: "open" });
433
+ const style = document.createElement("style");
434
+ style.textContent = getThemeCSS(config.accent);
435
+ shadow.appendChild(style);
436
+ return shadow;
437
+ }
438
+ function createWidgetRoot(shadow, el, extraClass) {
439
+ const opts = parseWidgetOptions(el);
440
+ const div = document.createElement("div");
441
+ div.className = ["peasy-widget", extraClass].filter(Boolean).join(" ");
442
+ div.setAttribute("data-theme", opts.theme);
443
+ div.setAttribute("data-size", opts.size);
444
+ shadow.appendChild(div);
445
+ return div;
446
+ }
447
+ function parseWidgetOptions(el) {
448
+ const dataset = el.dataset;
449
+ const theme = dataset.theme || "light";
450
+ const size = dataset.size || "default";
451
+ const lang = dataset.lang || "en";
452
+ const track = dataset.track === "true";
453
+ return { theme, size, lang, track };
454
+ }
455
+ function renderLoading(container) {
456
+ container.innerHTML = `
457
+ <div class="peasy-loading">
458
+ <span class="peasy-spinner"></span>
459
+ Loading\u2026
460
+ </div>
461
+ `;
462
+ }
463
+ function renderError(container, message, config) {
464
+ container.innerHTML = `
465
+ <div class="peasy-error">
466
+ <p>${esc(message)}</p>
467
+ <a href="https://${config.domain}" target="_blank" rel="noopener">
468
+ Visit ${esc(config.name)} ${externalLinkIcon}
469
+ </a>
470
+ </div>
471
+ `;
472
+ }
473
+ function poweredByHTML(config) {
474
+ return `<span class="peasy-powered">Powered by <a href="https://${config.domain}" target="_blank" rel="noopener">${esc(config.name)}</a></span>`;
475
+ }
476
+ function bindCopyButton(btn, text) {
477
+ btn.onclick = () => {
478
+ navigator.clipboard?.writeText(text).then(() => {
479
+ btn.innerHTML = `${checkIcon} Copied!`;
480
+ setTimeout(() => {
481
+ btn.innerHTML = `${copyIcon} Copy`;
482
+ }, 2e3);
483
+ }).catch(() => {
484
+ legacyCopy(text);
485
+ btn.innerHTML = `${checkIcon} Copied!`;
486
+ setTimeout(() => {
487
+ btn.innerHTML = `${copyIcon} Copy`;
488
+ }, 2e3);
489
+ });
490
+ };
491
+ }
492
+ function legacyCopy(text) {
493
+ const ta = document.createElement("textarea");
494
+ ta.value = text;
495
+ ta.style.cssText = "position:fixed;top:0;left:0;opacity:0;pointer-events:none";
496
+ document.body.appendChild(ta);
497
+ try {
498
+ ta.select();
499
+ document.execCommand("copy");
500
+ } finally {
501
+ document.body.removeChild(ta);
502
+ }
503
+ }
504
+ function esc(s) {
505
+ return s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&#39;");
506
+ }
507
+ 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" width="12" height="12"><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>`;
508
+ 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" width="13" height="13"><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>`;
509
+ 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" width="13" height="13"><polyline points="20 6 9 17 4 12"/></svg>`;
510
+ var fileIcon = `<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" width="14" height="14"><path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/><polyline points="14 2 14 8 20 8"/></svg>`;
511
+ var searchIcon = `<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" width="14" height="14"><circle cx="11" cy="11" r="8"/><line x1="21" y1="21" x2="16.65" y2="16.65"/></svg>`;
512
+ var bookIcon = `<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" width="14" height="14"><path d="M4 19.5A2.5 2.5 0 0 1 6.5 17H20"/><path d="M6.5 2H20v20H6.5A2.5 2.5 0 0 1 4 19.5v-15A2.5 2.5 0 0 1 6.5 2z"/></svg>`;
513
+ var toolIcon = `<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" width="14" height="14"><path d="M14.7 6.3a1 1 0 0 0 0 1.4l1.6 1.6a1 1 0 0 0 1.4 0l3.77-3.77a6 6 0 0 1-7.94 7.94l-6.91 6.91a2.12 2.12 0 0 1-3-3l6.91-6.91a6 6 0 0 1 7.94-7.94l-3.76 3.76z"/></svg>`;
514
+
515
+ // src/lazy.ts
516
+ var sharedObserver = null;
517
+ var callbacks = /* @__PURE__ */ new Map();
518
+ var observed = /* @__PURE__ */ new WeakSet();
519
+ function getObserver() {
520
+ if (!sharedObserver) {
521
+ sharedObserver = new IntersectionObserver((entries) => {
522
+ entries.forEach((entry) => {
523
+ if (entry.isIntersecting) {
524
+ sharedObserver.unobserve(entry.target);
525
+ const cb = callbacks.get(entry.target);
526
+ callbacks.delete(entry.target);
527
+ cb?.();
528
+ }
529
+ });
530
+ }, { rootMargin: "200px" });
531
+ }
532
+ return sharedObserver;
533
+ }
534
+ function lazyInit(el, callback) {
535
+ if (observed.has(el)) return;
536
+ observed.add(el);
537
+ if (!("IntersectionObserver" in window)) {
538
+ callback();
539
+ return;
540
+ }
541
+ callbacks.set(el, callback);
542
+ getObserver().observe(el);
543
+ }
544
+
545
+ // src/api.ts
546
+ var MEM_CACHE = /* @__PURE__ */ new Map();
547
+ var MEM_TTL = 5 * 60 * 1e3;
548
+ var LS_TTL = 24 * 60 * 60 * 1e3;
549
+ var MAX_CONCURRENT = 4;
550
+ var activeRequests = 0;
551
+ var queue = [];
552
+ function tryLS(key) {
553
+ try {
554
+ const raw = localStorage.getItem(`peasy:${key}`);
555
+ if (!raw) return null;
556
+ const { data, ts } = JSON.parse(raw);
557
+ if (Date.now() - ts > LS_TTL) {
558
+ localStorage.removeItem(`peasy:${key}`);
559
+ return null;
560
+ }
561
+ return data;
562
+ } catch {
563
+ return null;
564
+ }
565
+ }
566
+ function setLS(key, data) {
567
+ try {
568
+ localStorage.setItem(`peasy:${key}`, JSON.stringify({ data, ts: Date.now() }));
569
+ } catch {
570
+ }
571
+ }
572
+ function processQueue() {
573
+ while (activeRequests < MAX_CONCURRENT && queue.length > 0) {
574
+ const next = queue.shift();
575
+ if (next) next();
576
+ }
577
+ }
578
+ async function fetchAPI(config, path, opts) {
579
+ const url = `${config.apiBase}${path}`;
580
+ const lang = opts?.lang || "en";
581
+ const cacheKey = `${url}:${lang}`;
582
+ const memCached = MEM_CACHE.get(cacheKey);
583
+ if (memCached && Date.now() - memCached.ts < MEM_TTL) return memCached.data;
584
+ if (opts?.useLS) {
585
+ const lsCached = tryLS(cacheKey);
586
+ if (lsCached) {
587
+ MEM_CACHE.set(cacheKey, { data: lsCached, ts: Date.now() });
588
+ return lsCached;
589
+ }
590
+ }
591
+ if (activeRequests >= MAX_CONCURRENT) {
592
+ await new Promise((resolve) => queue.push(resolve));
593
+ }
594
+ activeRequests++;
595
+ try {
596
+ const headers = { Accept: "application/json" };
597
+ if (lang !== "en") headers["Accept-Language"] = lang;
598
+ const res = await fetch(url, { headers });
599
+ if (res.status === 429) {
600
+ const retryAfter = Math.min(parseInt(res.headers.get("Retry-After") || "10", 10), 10);
601
+ await new Promise((r) => setTimeout(r, retryAfter * 1e3));
602
+ const retry = await fetch(url, { headers });
603
+ if (!retry.ok) return null;
604
+ const data2 = await retry.json();
605
+ MEM_CACHE.set(cacheKey, { data: data2, ts: Date.now() });
606
+ if (opts?.useLS) setLS(cacheKey, data2);
607
+ return data2;
608
+ }
609
+ if (!res.ok) return null;
610
+ const data = await res.json();
611
+ MEM_CACHE.set(cacheKey, { data, ts: Date.now() });
612
+ if (opts?.useLS) setLS(cacheKey, data);
613
+ return data;
614
+ } catch {
615
+ return null;
616
+ } finally {
617
+ activeRequests--;
618
+ processQueue();
619
+ }
620
+ }
621
+
622
+ // src/widgets/format-info.ts
623
+ function init(el, config, opts) {
624
+ const slug = el.dataset.slug;
625
+ if (!slug) {
626
+ return;
627
+ }
628
+ const shadow = createShadow(el, config);
629
+ const root = createWidgetRoot(shadow, el, "peasy-format-info");
630
+ renderLoading(root);
631
+ fetchAPI(config, `/api/v1/formats/${slug}/`, { useLS: true, lang: opts.lang }).then((data) => {
632
+ if (!data) {
633
+ renderError(root, "Could not load format information.", config);
634
+ return;
635
+ }
636
+ const advantageItems = (data.advantages || []).map((a) => `<li>${esc(a)}</li>`).join("");
637
+ root.innerHTML = `
638
+ <style>
639
+ .peasy-format-info-header {
640
+ display: flex;
641
+ align-items: center;
642
+ gap: 8px;
643
+ padding: 14px 16px 10px;
644
+ border-bottom: 1px solid var(--border);
645
+ background: var(--ribbon);
646
+ }
647
+ .peasy-format-info-header svg { color: var(--accent); flex-shrink: 0; }
648
+ .peasy-format-info-props {
649
+ display: grid;
650
+ grid-template-columns: auto 1fr;
651
+ gap: 0;
652
+ }
653
+ .peasy-format-info-prop-label {
654
+ padding: 7px 16px 7px 16px;
655
+ font-size: 12px;
656
+ color: var(--muted);
657
+ font-weight: 500;
658
+ border-bottom: 1px solid var(--border);
659
+ white-space: nowrap;
660
+ }
661
+ .peasy-format-info-prop-value {
662
+ padding: 7px 16px 7px 8px;
663
+ font-size: 13px;
664
+ color: var(--text);
665
+ border-bottom: 1px solid var(--border);
666
+ word-break: break-all;
667
+ }
668
+ .peasy-format-info-desc {
669
+ padding: 10px 16px;
670
+ font-size: 13px;
671
+ color: var(--text);
672
+ line-height: 1.6;
673
+ border-bottom: 1px solid var(--border);
674
+ }
675
+ .peasy-format-info-advantages {
676
+ padding: 10px 16px;
677
+ border-bottom: 1px solid var(--border);
678
+ }
679
+ .peasy-format-info-advantages-title {
680
+ font-size: 11px;
681
+ font-weight: 600;
682
+ text-transform: uppercase;
683
+ letter-spacing: 0.06em;
684
+ color: var(--muted);
685
+ margin: 0 0 6px 0;
686
+ }
687
+ .peasy-format-info-advantages ul {
688
+ margin: 0;
689
+ padding-left: 18px;
690
+ }
691
+ .peasy-format-info-advantages li {
692
+ font-size: 13px;
693
+ color: var(--text);
694
+ line-height: 1.5;
695
+ margin-bottom: 2px;
696
+ }
697
+ .peasy-bool-yes { color: #16a34a; font-weight: 500; }
698
+ .peasy-bool-no { color: var(--muted); }
699
+ </style>
700
+ <div class="peasy-format-info-header">
701
+ ${fileIcon}
702
+ <span class="peasy-title" style="margin:0;">${esc(data.full_name)}</span>
703
+ <span class="peasy-badge" style="margin-left:auto;">${esc(data.extension.toUpperCase())}</span>
704
+ </div>
705
+ <div class="peasy-format-info-props">
706
+ <span class="peasy-format-info-prop-label">Extension</span>
707
+ <span class="peasy-format-info-prop-value">${esc(data.extension)}</span>
708
+ <span class="peasy-format-info-prop-label">MIME Type</span>
709
+ <span class="peasy-format-info-prop-value">${esc(data.mime_type)}</span>
710
+ <span class="peasy-format-info-prop-label">Category</span>
711
+ <span class="peasy-format-info-prop-value">${esc(data.category)}</span>
712
+ <span class="peasy-format-info-prop-label">Binary</span>
713
+ <span class="peasy-format-info-prop-value">${data.is_binary ? '<span class="peasy-bool-yes">Yes</span>' : '<span class="peasy-bool-no">No</span>'}</span>
714
+ <span class="peasy-format-info-prop-label">Lossy</span>
715
+ <span class="peasy-format-info-prop-value">${data.is_lossy ? '<span class="peasy-bool-yes">Yes</span>' : '<span class="peasy-bool-no">No</span>'}</span>
716
+ </div>
717
+ ${data.description ? `<div class="peasy-format-info-desc">${esc(data.description)}</div>` : ""}
718
+ ${advantageItems ? `
719
+ <div class="peasy-format-info-advantages">
720
+ <p class="peasy-format-info-advantages-title">Advantages</p>
721
+ <ul>${advantageItems}</ul>
722
+ </div>
723
+ ` : ""}
724
+ <div class="peasy-actions">
725
+ <a class="peasy-link" href="https://${config.domain}/formats/${slug}/" target="_blank" rel="noopener">
726
+ ${esc(data.full_name)} format guide on ${esc(config.name)} ${externalLinkIcon}
727
+ </a>
728
+ </div>
729
+ ${poweredByHTML(config)}
730
+ `;
731
+ });
732
+ }
733
+
734
+ // src/widgets/tool-card.ts
735
+ function init2(el, config, opts) {
736
+ const slug = el.dataset.slug;
737
+ if (!slug) {
738
+ return;
739
+ }
740
+ const shadow = createShadow(el, config);
741
+ const root = createWidgetRoot(shadow, el, "peasy-tool-card");
742
+ renderLoading(root);
743
+ fetchAPI(config, `/api/v1/tools/${slug}/`, { lang: opts.lang }).then((data) => {
744
+ if (!data) {
745
+ renderError(root, "Could not load tool information.", config);
746
+ return;
747
+ }
748
+ const toolUrl = `https://${config.domain}/${data.category.slug}/${data.slug}/`;
749
+ root.innerHTML = `
750
+ <style>
751
+ .peasy-tool-card-header {
752
+ display: flex;
753
+ align-items: flex-start;
754
+ gap: 12px;
755
+ padding: 14px 16px 10px;
756
+ border-bottom: 1px solid var(--border);
757
+ background: var(--ribbon);
758
+ }
759
+ .peasy-tool-card-icon {
760
+ width: 40px;
761
+ height: 40px;
762
+ border-radius: 8px;
763
+ background: color-mix(in srgb, var(--accent) 12%, transparent);
764
+ display: flex;
765
+ align-items: center;
766
+ justify-content: center;
767
+ flex-shrink: 0;
768
+ font-size: 20px;
769
+ color: var(--accent);
770
+ }
771
+ .peasy-tool-card-meta { flex: 1; min-width: 0; }
772
+ .peasy-tool-card-desc {
773
+ padding: 10px 16px 12px;
774
+ font-size: 13px;
775
+ color: var(--text);
776
+ line-height: 1.6;
777
+ border-bottom: 1px solid var(--border);
778
+ }
779
+ </style>
780
+ <div class="peasy-tool-card-header">
781
+ <div class="peasy-tool-card-icon">
782
+ ${toolIcon}
783
+ </div>
784
+ <div class="peasy-tool-card-meta">
785
+ <p class="peasy-title" style="margin:0 0 4px 0;">${esc(data.name)}</p>
786
+ <div class="peasy-meta" style="margin-bottom:0;">
787
+ <span class="peasy-badge">${esc(data.category.name)}</span>
788
+ </div>
789
+ </div>
790
+ </div>
791
+ ${data.tagline ? `<p class="peasy-subtitle" style="padding:8px 16px 0;margin:0;">${esc(data.tagline)}</p>` : ""}
792
+ ${data.description ? `<div class="peasy-tool-card-desc">${esc(data.description)}</div>` : ""}
793
+ <div class="peasy-actions">
794
+ <a class="peasy-btn" href="${toolUrl}" target="_blank" rel="noopener">
795
+ Try ${esc(data.name)} on ${esc(config.name)} ${externalLinkIcon}
796
+ </a>
797
+ </div>
798
+ ${poweredByHTML(config)}
799
+ `;
800
+ });
801
+ }
802
+
803
+ // src/widgets/conversion-card.ts
804
+ var QUALITY_MAP = {
805
+ lossless: { label: "Lossless \u2713", color: "#16a34a" },
806
+ lossy: { label: "Lossy", color: "#ea580c" },
807
+ depends: { label: "Depends on content", color: "#ca8a04" }
808
+ };
809
+ var DIFFICULTY_MAP = {
810
+ easy: { label: "Easy", color: "#16a34a", bg: "#dcfce7" },
811
+ medium: { label: "Medium", color: "#ca8a04", bg: "#fef9c3" },
812
+ hard: { label: "Hard", color: "#dc2626", bg: "#fee2e2" }
813
+ };
814
+ function init3(el, config, opts) {
815
+ const slug = el.dataset.slug;
816
+ if (!slug) {
817
+ return;
818
+ }
819
+ const shadow = createShadow(el, config);
820
+ const root = createWidgetRoot(shadow, el, "peasy-conversion-card");
821
+ renderLoading(root);
822
+ fetchAPI(config, `/api/v1/conversions/${slug}/`, { lang: opts.lang }).then((data) => {
823
+ if (!data) {
824
+ renderError(root, "Could not load conversion information.", config);
825
+ return;
826
+ }
827
+ const quality = QUALITY_MAP[data.quality_impact] ?? QUALITY_MAP["depends"];
828
+ const diff = DIFFICULTY_MAP[data.difficulty] ?? DIFFICULTY_MAP["medium"];
829
+ root.innerHTML = `
830
+ <style>
831
+ .peasy-conv-visual {
832
+ display: flex;
833
+ align-items: center;
834
+ justify-content: center;
835
+ gap: 12px;
836
+ padding: 16px;
837
+ background: var(--ribbon);
838
+ border-bottom: 1px solid var(--border);
839
+ }
840
+ .peasy-conv-format {
841
+ display: flex;
842
+ flex-direction: column;
843
+ align-items: center;
844
+ gap: 4px;
845
+ }
846
+ .peasy-conv-format-ext {
847
+ font-size: 18px;
848
+ font-weight: 700;
849
+ color: var(--accent);
850
+ }
851
+ .peasy-conv-format-name {
852
+ font-size: 11px;
853
+ color: var(--muted);
854
+ text-align: center;
855
+ }
856
+ .peasy-conv-arrow {
857
+ font-size: 20px;
858
+ color: var(--muted);
859
+ flex-shrink: 0;
860
+ }
861
+ .peasy-conv-props {
862
+ padding: 10px 16px;
863
+ display: flex;
864
+ flex-direction: column;
865
+ gap: 8px;
866
+ border-bottom: 1px solid var(--border);
867
+ }
868
+ .peasy-conv-prop-row {
869
+ display: flex;
870
+ align-items: center;
871
+ justify-content: space-between;
872
+ gap: 8px;
873
+ font-size: 13px;
874
+ }
875
+ .peasy-conv-prop-label { color: var(--muted); }
876
+ .peasy-conv-desc {
877
+ padding: 8px 16px 12px;
878
+ font-size: 13px;
879
+ color: var(--text);
880
+ line-height: 1.6;
881
+ border-bottom: 1px solid var(--border);
882
+ }
883
+ .peasy-diff-badge {
884
+ display: inline-block;
885
+ padding: 2px 8px;
886
+ border-radius: 4px;
887
+ font-size: 12px;
888
+ font-weight: 500;
889
+ }
890
+ </style>
891
+ <div class="peasy-conv-visual">
892
+ <div class="peasy-conv-format">
893
+ ${fileIcon}
894
+ <span class="peasy-conv-format-ext">${esc(data.source_format.extension.toUpperCase())}</span>
895
+ <span class="peasy-conv-format-name">${esc(data.source_format.full_name)}</span>
896
+ </div>
897
+ <div class="peasy-conv-arrow">\u2192</div>
898
+ <div class="peasy-conv-format">
899
+ ${fileIcon}
900
+ <span class="peasy-conv-format-ext">${esc(data.target_format.extension.toUpperCase())}</span>
901
+ <span class="peasy-conv-format-name">${esc(data.target_format.full_name)}</span>
902
+ </div>
903
+ </div>
904
+ <div class="peasy-conv-props">
905
+ <div class="peasy-conv-prop-row">
906
+ <span class="peasy-conv-prop-label">Quality impact</span>
907
+ <span style="font-weight:500;color:${quality.color};">${quality.label}</span>
908
+ </div>
909
+ <div class="peasy-conv-prop-row">
910
+ <span class="peasy-conv-prop-label">Difficulty</span>
911
+ <span class="peasy-diff-badge" style="background:${diff.bg};color:${diff.color};">${diff.label}</span>
912
+ </div>
913
+ ${data.recommended_tool ? `
914
+ <div class="peasy-conv-prop-row">
915
+ <span class="peasy-conv-prop-label">Recommended tool</span>
916
+ <a class="peasy-link" href="https://${config.domain}/search/?q=${encodeURIComponent(data.recommended_tool.name)}" target="_blank" rel="noopener">${esc(data.recommended_tool.name)}</a>
917
+ </div>
918
+ ` : ""}
919
+ </div>
920
+ ${data.description ? `<div class="peasy-conv-desc">${esc(data.description)}</div>` : ""}
921
+ <div class="peasy-actions">
922
+ <a class="peasy-link" href="https://${config.domain}/conversions/${slug}/" target="_blank" rel="noopener">
923
+ ${esc(data.source_format.extension.toUpperCase())} to ${esc(data.target_format.extension.toUpperCase())} conversion guide on ${esc(config.name)} ${externalLinkIcon}
924
+ </a>
925
+ </div>
926
+ ${poweredByHTML(config)}
927
+ `;
928
+ });
929
+ }
930
+
931
+ // src/widgets/format-compare.ts
932
+ function init4(el, config, opts) {
933
+ const dataset = el.dataset;
934
+ const slugA = dataset.a;
935
+ const slugB = dataset.b;
936
+ if (!slugA || !slugB) {
937
+ return;
938
+ }
939
+ const shadow = createShadow(el, config);
940
+ const root = createWidgetRoot(shadow, el, "peasy-format-compare");
941
+ if (!dataset.size) {
942
+ root.setAttribute("data-size", "large");
943
+ }
944
+ renderLoading(root);
945
+ Promise.all([
946
+ fetchAPI(config, `/api/v1/formats/${slugA}/`, { useLS: true, lang: opts.lang }),
947
+ fetchAPI(config, `/api/v1/formats/${slugB}/`, { useLS: true, lang: opts.lang })
948
+ ]).then(([fmtA, fmtB]) => {
949
+ if (!fmtA || !fmtB) {
950
+ renderError(root, "Could not load format comparison data.", config);
951
+ return;
952
+ }
953
+ function lossyCell(val) {
954
+ if (val) return `<span style="color:#ea580c;font-weight:500;">Yes (lossy)</span>`;
955
+ return `<span style="color:#16a34a;font-weight:500;">No (lossless)</span>`;
956
+ }
957
+ function binaryCell(val) {
958
+ return val ? "Yes" : "No";
959
+ }
960
+ root.innerHTML = `
961
+ <style>
962
+ .peasy-compare-header {
963
+ display: grid;
964
+ grid-template-columns: 1fr 1fr;
965
+ gap: 0;
966
+ border-bottom: 1px solid var(--border);
967
+ background: var(--ribbon);
968
+ }
969
+ .peasy-compare-header-cell {
970
+ padding: 12px 16px;
971
+ text-align: center;
972
+ font-weight: 600;
973
+ font-size: 15px;
974
+ color: var(--accent);
975
+ }
976
+ .peasy-compare-header-cell:first-child {
977
+ border-right: 1px solid var(--border);
978
+ }
979
+ .peasy-compare-header-sub {
980
+ font-size: 11px;
981
+ font-weight: 400;
982
+ color: var(--muted);
983
+ margin-top: 2px;
984
+ }
985
+ .peasy-compare-table {
986
+ width: 100%;
987
+ border-collapse: collapse;
988
+ }
989
+ .peasy-compare-row-label {
990
+ padding: 8px 16px;
991
+ font-size: 12px;
992
+ color: var(--muted);
993
+ font-weight: 500;
994
+ background: var(--ribbon);
995
+ border-bottom: 1px solid var(--border);
996
+ border-top: 1px solid var(--border);
997
+ text-align: center;
998
+ letter-spacing: 0.03em;
999
+ text-transform: uppercase;
1000
+ font-size: 11px;
1001
+ }
1002
+ .peasy-compare-cell {
1003
+ padding: 8px 16px;
1004
+ font-size: 13px;
1005
+ color: var(--text);
1006
+ border-bottom: 1px solid var(--border);
1007
+ text-align: center;
1008
+ vertical-align: middle;
1009
+ word-break: break-word;
1010
+ }
1011
+ .peasy-compare-cell:first-child {
1012
+ border-right: 1px solid var(--border);
1013
+ }
1014
+ .peasy-compare-desc-row {
1015
+ padding: 10px 16px;
1016
+ font-size: 13px;
1017
+ color: var(--text);
1018
+ line-height: 1.55;
1019
+ text-align: center;
1020
+ border-bottom: 1px solid var(--border);
1021
+ }
1022
+ .peasy-compare-links {
1023
+ display: flex;
1024
+ align-items: center;
1025
+ justify-content: space-between;
1026
+ gap: 8px;
1027
+ padding: 10px 16px;
1028
+ border-top: 1px solid var(--border);
1029
+ }
1030
+ </style>
1031
+ <div class="peasy-compare-header">
1032
+ <div class="peasy-compare-header-cell">
1033
+ ${esc(fmtA.extension.toUpperCase())}
1034
+ <div class="peasy-compare-header-sub">${esc(fmtA.full_name)}</div>
1035
+ </div>
1036
+ <div class="peasy-compare-header-cell">
1037
+ ${esc(fmtB.extension.toUpperCase())}
1038
+ <div class="peasy-compare-header-sub">${esc(fmtB.full_name)}</div>
1039
+ </div>
1040
+ </div>
1041
+ <table class="peasy-compare-table">
1042
+ <tbody>
1043
+ <tr><td class="peasy-compare-row-label" colspan="2">Extension</td></tr>
1044
+ <tr>
1045
+ <td class="peasy-compare-cell">${esc(fmtA.extension)}</td>
1046
+ <td class="peasy-compare-cell">${esc(fmtB.extension)}</td>
1047
+ </tr>
1048
+ <tr><td class="peasy-compare-row-label" colspan="2">MIME Type</td></tr>
1049
+ <tr>
1050
+ <td class="peasy-compare-cell">${esc(fmtA.mime_type)}</td>
1051
+ <td class="peasy-compare-cell">${esc(fmtB.mime_type)}</td>
1052
+ </tr>
1053
+ <tr><td class="peasy-compare-row-label" colspan="2">Category</td></tr>
1054
+ <tr>
1055
+ <td class="peasy-compare-cell">${esc(fmtA.category)}</td>
1056
+ <td class="peasy-compare-cell">${esc(fmtB.category)}</td>
1057
+ </tr>
1058
+ <tr><td class="peasy-compare-row-label" colspan="2">Binary</td></tr>
1059
+ <tr>
1060
+ <td class="peasy-compare-cell">${binaryCell(fmtA.is_binary)}</td>
1061
+ <td class="peasy-compare-cell">${binaryCell(fmtB.is_binary)}</td>
1062
+ </tr>
1063
+ <tr><td class="peasy-compare-row-label" colspan="2">Lossy Compression</td></tr>
1064
+ <tr>
1065
+ <td class="peasy-compare-cell">${lossyCell(fmtA.is_lossy)}</td>
1066
+ <td class="peasy-compare-cell">${lossyCell(fmtB.is_lossy)}</td>
1067
+ </tr>
1068
+ </tbody>
1069
+ </table>
1070
+ <div class="peasy-compare-links">
1071
+ <a class="peasy-link" href="https://${config.domain}/formats/${slugA}/" target="_blank" rel="noopener">
1072
+ ${esc(fmtA.full_name)} format guide ${externalLinkIcon}
1073
+ </a>
1074
+ <a class="peasy-link" href="https://${config.domain}/formats/${slugB}/" target="_blank" rel="noopener">
1075
+ ${esc(fmtB.full_name)} format guide ${externalLinkIcon}
1076
+ </a>
1077
+ </div>
1078
+ ${poweredByHTML(config)}
1079
+ `;
1080
+ });
1081
+ }
1082
+
1083
+ // src/widgets/glossary-tooltip.ts
1084
+ function init5(el, config, opts) {
1085
+ const dataset = el.dataset;
1086
+ const slug = dataset.slug;
1087
+ const display = dataset.display || "inline";
1088
+ if (!slug) {
1089
+ return;
1090
+ }
1091
+ if (display === "inline") {
1092
+ const shadow2 = el.attachShadow({ mode: "open" });
1093
+ const style = document.createElement("style");
1094
+ style.textContent = `
1095
+ :host {
1096
+ display: inline;
1097
+ font-family: inherit;
1098
+ cursor: help;
1099
+ position: relative;
1100
+ }
1101
+ .peasy-glossary-term {
1102
+ text-decoration: underline dotted var(--accent, #6366f1);
1103
+ text-underline-offset: 3px;
1104
+ color: inherit;
1105
+ }
1106
+ .peasy-glossary-popup {
1107
+ display: none;
1108
+ position: absolute;
1109
+ bottom: calc(100% + 8px);
1110
+ left: 0;
1111
+ z-index: 9999;
1112
+ background: #1a1a1a;
1113
+ color: #f3f4f6;
1114
+ border-radius: 8px;
1115
+ padding: 12px 14px;
1116
+ font-size: 13px;
1117
+ min-width: 220px;
1118
+ max-width: 300px;
1119
+ box-shadow: 0 4px 16px rgba(0,0,0,0.24);
1120
+ line-height: 1.55;
1121
+ pointer-events: none;
1122
+ white-space: normal;
1123
+ font-family: system-ui, -apple-system, 'Segoe UI', Roboto, sans-serif;
1124
+ }
1125
+ .peasy-glossary-term:hover + .peasy-glossary-popup,
1126
+ .peasy-glossary-popup:hover {
1127
+ display: block;
1128
+ }
1129
+ .peasy-glossary-popup-term {
1130
+ font-weight: 600;
1131
+ font-size: 14px;
1132
+ margin-bottom: 6px;
1133
+ color: #93c5fd;
1134
+ }
1135
+ .peasy-glossary-popup-def {
1136
+ opacity: 0.9;
1137
+ margin-bottom: 8px;
1138
+ }
1139
+ .peasy-glossary-popup-link {
1140
+ font-size: 11px;
1141
+ color: #93c5fd;
1142
+ text-decoration: none;
1143
+ opacity: 0.8;
1144
+ display: inline-flex;
1145
+ align-items: center;
1146
+ gap: 3px;
1147
+ pointer-events: auto;
1148
+ }
1149
+ `;
1150
+ shadow2.appendChild(style);
1151
+ const termSpan = document.createElement("span");
1152
+ termSpan.className = "peasy-glossary-term";
1153
+ termSpan.textContent = el.textContent || slug;
1154
+ shadow2.appendChild(termSpan);
1155
+ fetchAPI(config, `/api/v1/glossary/${slug}/`, { useLS: true, lang: opts.lang }).then((data) => {
1156
+ if (!data) {
1157
+ return;
1158
+ }
1159
+ const popup = document.createElement("div");
1160
+ popup.className = "peasy-glossary-popup";
1161
+ popup.innerHTML = `
1162
+ <div class="peasy-glossary-popup-term">${esc(data.term)}</div>
1163
+ <div class="peasy-glossary-popup-def">${esc(data.explanation_simple || data.definition)}</div>
1164
+ <a class="peasy-glossary-popup-link" href="https://${config.domain}/glossary/${encodeURIComponent(slug)}/" target="_blank" rel="noopener">
1165
+ Learn more about ${esc(data.term)} on ${esc(config.name)}
1166
+ <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" width="10" height="10"><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>
1167
+ </a>
1168
+ `;
1169
+ shadow2.appendChild(popup);
1170
+ });
1171
+ return;
1172
+ }
1173
+ const shadow = createShadow(el, config);
1174
+ const root = createWidgetRoot(shadow, el, "peasy-glossary-card");
1175
+ renderLoading(root);
1176
+ fetchAPI(config, `/api/v1/glossary/${slug}/`, { useLS: true, lang: opts.lang }).then((data) => {
1177
+ if (!data) {
1178
+ renderError(root, "Could not load glossary term.", config);
1179
+ return;
1180
+ }
1181
+ const relatedLinks = (data.related_terms || []).slice(0, 4).map((t) => `<a class="peasy-link" href="https://${config.domain}/glossary/${encodeURIComponent(t.slug)}/" target="_blank" rel="noopener">${esc(t.term)}</a>`).join("");
1182
+ root.innerHTML = `
1183
+ <style>
1184
+ .peasy-glossary-header {
1185
+ display: flex;
1186
+ align-items: center;
1187
+ gap: 8px;
1188
+ padding: 12px 16px 10px;
1189
+ border-bottom: 1px solid var(--border);
1190
+ background: var(--ribbon);
1191
+ }
1192
+ .peasy-glossary-header svg { color: var(--accent); }
1193
+ .peasy-glossary-cat {
1194
+ margin-left: auto;
1195
+ }
1196
+ .peasy-glossary-body {
1197
+ padding: 12px 16px;
1198
+ border-bottom: 1px solid var(--border);
1199
+ }
1200
+ .peasy-glossary-label {
1201
+ font-size: 11px;
1202
+ font-weight: 600;
1203
+ text-transform: uppercase;
1204
+ letter-spacing: 0.06em;
1205
+ color: var(--muted);
1206
+ margin: 0 0 4px 0;
1207
+ }
1208
+ .peasy-glossary-section {
1209
+ font-size: 13px;
1210
+ color: var(--text);
1211
+ line-height: 1.6;
1212
+ margin-bottom: 12px;
1213
+ }
1214
+ .peasy-glossary-related {
1215
+ display: flex;
1216
+ flex-wrap: wrap;
1217
+ gap: 6px;
1218
+ padding: 10px 16px;
1219
+ border-bottom: 1px solid var(--border);
1220
+ }
1221
+ </style>
1222
+ <div class="peasy-glossary-header">
1223
+ ${bookIcon}
1224
+ <span class="peasy-title" style="margin:0;">${esc(data.term)}</span>
1225
+ <span class="peasy-badge peasy-glossary-cat">${esc(data.category)}</span>
1226
+ </div>
1227
+ <div class="peasy-glossary-body">
1228
+ ${data.explanation_technical ? `
1229
+ <p class="peasy-glossary-label">Technical Explanation</p>
1230
+ <div class="peasy-glossary-section">${esc(data.explanation_technical)}</div>
1231
+ ` : ""}
1232
+ ${data.definition ? `
1233
+ <p class="peasy-glossary-label">Definition</p>
1234
+ <div class="peasy-glossary-section">${esc(data.definition)}</div>
1235
+ ` : ""}
1236
+ </div>
1237
+ ${relatedLinks ? `
1238
+ <div class="peasy-glossary-related">
1239
+ <span style="font-size:11px;font-weight:600;color:var(--muted);text-transform:uppercase;letter-spacing:0.06em;display:flex;align-items:center;">Related:</span>
1240
+ ${relatedLinks}
1241
+ </div>
1242
+ ` : ""}
1243
+ <div class="peasy-actions">
1244
+ <a class="peasy-link" href="https://${config.domain}/glossary/${slug}/" target="_blank" rel="noopener">
1245
+ ${esc(data.term)} \u2014 full glossary entry on ${esc(config.name)} ${externalLinkIcon}
1246
+ </a>
1247
+ </div>
1248
+ ${poweredByHTML(config)}
1249
+ `;
1250
+ });
1251
+ }
1252
+
1253
+ // src/widgets/tool-gallery.ts
1254
+ function init6(el, config, opts) {
1255
+ const dataset = el.dataset;
1256
+ const limit = parseInt(dataset.limit || "6", 10);
1257
+ const category = dataset.category || "";
1258
+ const shadow = createShadow(el, config);
1259
+ const root = createWidgetRoot(shadow, el, "peasy-tool-gallery");
1260
+ renderLoading(root);
1261
+ const qs = category ? `?limit=${limit}&category=${encodeURIComponent(category)}` : `?limit=${limit}`;
1262
+ fetchAPI(config, `/api/v1/tools/${qs}`, { lang: opts.lang }).then((resp) => {
1263
+ if (!resp || !resp.results) {
1264
+ renderError(root, "Could not load tools gallery.", config);
1265
+ return;
1266
+ }
1267
+ const toolTiles = resp.results.map((tool) => {
1268
+ const url = `https://${config.domain}/${tool.category.slug}/${tool.slug}/`;
1269
+ const tagline = tool.tagline ? tool.tagline.length > 55 ? tool.tagline.substring(0, 55) + "\u2026" : tool.tagline : "";
1270
+ return `
1271
+ <a class="peasy-gallery-tile" href="${url}" target="_blank" rel="noopener" title="${esc(tool.name)}">
1272
+ <div class="peasy-gallery-tile-icon">${esc(tool.icon || "\u{1F527}")}</div>
1273
+ <div class="peasy-gallery-tile-name">${esc(tool.name)}</div>
1274
+ ${tagline ? `<div class="peasy-gallery-tile-tagline">${esc(tagline)}</div>` : ""}
1275
+ </a>
1276
+ `;
1277
+ }).join("");
1278
+ const allToolsUrl = `https://${config.domain}/`;
1279
+ root.innerHTML = `
1280
+ <style>
1281
+ .peasy-gallery-header {
1282
+ display: flex;
1283
+ align-items: center;
1284
+ justify-content: space-between;
1285
+ padding: 10px 16px 8px;
1286
+ border-bottom: 1px solid var(--border);
1287
+ background: var(--ribbon);
1288
+ }
1289
+ .peasy-gallery-count {
1290
+ font-size: 12px;
1291
+ color: var(--muted);
1292
+ }
1293
+ .peasy-gallery-grid {
1294
+ display: grid;
1295
+ grid-template-columns: repeat(auto-fit, minmax(120px, 1fr));
1296
+ gap: 1px;
1297
+ background: var(--border);
1298
+ border-bottom: 1px solid var(--border);
1299
+ }
1300
+ .peasy-gallery-tile {
1301
+ display: flex;
1302
+ flex-direction: column;
1303
+ align-items: center;
1304
+ gap: 4px;
1305
+ padding: 14px 8px 12px;
1306
+ background: var(--bg);
1307
+ text-decoration: none;
1308
+ color: var(--text);
1309
+ text-align: center;
1310
+ transition: background 0.15s;
1311
+ }
1312
+ .peasy-gallery-tile:hover {
1313
+ background: var(--ribbon);
1314
+ }
1315
+ .peasy-gallery-tile-icon {
1316
+ font-size: 22px;
1317
+ line-height: 1;
1318
+ }
1319
+ .peasy-gallery-tile-name {
1320
+ font-size: 13px;
1321
+ font-weight: 600;
1322
+ color: var(--text);
1323
+ }
1324
+ .peasy-gallery-tile-tagline {
1325
+ font-size: 11px;
1326
+ color: var(--muted);
1327
+ line-height: 1.4;
1328
+ }
1329
+ .peasy-gallery-footer {
1330
+ padding: 10px 16px;
1331
+ border-top: 1px solid var(--border);
1332
+ text-align: center;
1333
+ }
1334
+ </style>
1335
+ <div class="peasy-gallery-header">
1336
+ <span class="peasy-title" style="font-size:14px;margin:0;">${category ? esc(category) + " Tools" : "Tool Gallery"}</span>
1337
+ <span class="peasy-gallery-count">${resp.count}+ tools on ${esc(config.name)}</span>
1338
+ </div>
1339
+ <div class="peasy-gallery-grid">
1340
+ ${toolTiles}
1341
+ </div>
1342
+ <div class="peasy-gallery-footer">
1343
+ <a class="peasy-link" href="${allToolsUrl}" target="_blank" rel="noopener">
1344
+ Browse all ${resp.count} tools on ${esc(config.name)} ${externalLinkIcon}
1345
+ </a>
1346
+ </div>
1347
+ ${poweredByHTML(config)}
1348
+ `;
1349
+ });
1350
+ }
1351
+
1352
+ // src/widgets/guide-card.ts
1353
+ function init7(el, config, opts) {
1354
+ const slug = el.dataset.slug;
1355
+ if (!slug) {
1356
+ return;
1357
+ }
1358
+ const shadow = createShadow(el, config);
1359
+ const root = createWidgetRoot(shadow, el, "peasy-guide-card");
1360
+ renderLoading(root);
1361
+ fetchAPI(config, `/api/v1/guides/${slug}/`, { lang: opts.lang }).then((data) => {
1362
+ if (!data) {
1363
+ renderError(root, "Could not load guide information.", config);
1364
+ return;
1365
+ }
1366
+ const takeawayItems = (data.key_takeaways || []).slice(0, 5).map((t) => `<li>${esc(t)}</li>`).join("");
1367
+ const levelColors = {
1368
+ beginner: "#16a34a",
1369
+ intermediate: "#ca8a04",
1370
+ advanced: "#dc2626"
1371
+ };
1372
+ const levelColor = levelColors[(data.audience_level || "").toLowerCase()] || "#6b7280";
1373
+ root.innerHTML = `
1374
+ <style>
1375
+ .peasy-guide-header {
1376
+ display: flex;
1377
+ align-items: flex-start;
1378
+ gap: 8px;
1379
+ padding: 12px 16px 10px;
1380
+ border-bottom: 1px solid var(--border);
1381
+ background: var(--ribbon);
1382
+ }
1383
+ .peasy-guide-header svg { color: var(--accent); margin-top: 3px; flex-shrink: 0; }
1384
+ .peasy-guide-body {
1385
+ padding: 10px 16px;
1386
+ border-bottom: 1px solid var(--border);
1387
+ }
1388
+ .peasy-guide-summary {
1389
+ font-size: 13px;
1390
+ color: var(--text);
1391
+ line-height: 1.6;
1392
+ margin-bottom: 10px;
1393
+ }
1394
+ .peasy-guide-takeaways {
1395
+ padding: 8px 16px 12px;
1396
+ border-bottom: 1px solid var(--border);
1397
+ }
1398
+ .peasy-guide-takeaways-label {
1399
+ font-size: 11px;
1400
+ font-weight: 600;
1401
+ text-transform: uppercase;
1402
+ letter-spacing: 0.06em;
1403
+ color: var(--muted);
1404
+ margin: 0 0 6px 0;
1405
+ }
1406
+ .peasy-guide-takeaways ul {
1407
+ margin: 0;
1408
+ padding-left: 18px;
1409
+ }
1410
+ .peasy-guide-takeaways li {
1411
+ font-size: 13px;
1412
+ color: var(--text);
1413
+ line-height: 1.5;
1414
+ margin-bottom: 3px;
1415
+ }
1416
+ .peasy-guide-takeaways li::marker { color: var(--accent); }
1417
+ </style>
1418
+ <div class="peasy-guide-header">
1419
+ ${bookIcon}
1420
+ <div>
1421
+ <p class="peasy-title" style="margin:0 0 5px 0;">${esc(data.title)}</p>
1422
+ <div class="peasy-meta" style="margin-bottom:0;">
1423
+ ${data.reading_time_minutes ? `<span class="peasy-stat">${data.reading_time_minutes} min read</span>` : ""}
1424
+ ${data.audience_level ? `<span class="peasy-stat" style="color:${levelColor};">${esc(data.audience_level)}</span>` : ""}
1425
+ ${data.quality_score ? `<span class="peasy-stat">Score: ${data.quality_score}/100</span>` : ""}
1426
+ </div>
1427
+ </div>
1428
+ </div>
1429
+ ${data.summary ? `
1430
+ <div class="peasy-guide-body">
1431
+ <p class="peasy-guide-summary">${esc(data.summary)}</p>
1432
+ </div>
1433
+ ` : ""}
1434
+ ${takeawayItems ? `
1435
+ <div class="peasy-guide-takeaways">
1436
+ <p class="peasy-guide-takeaways-label">Key Takeaways</p>
1437
+ <ul>${takeawayItems}</ul>
1438
+ </div>
1439
+ ` : ""}
1440
+ <div class="peasy-actions">
1441
+ <a class="peasy-link" href="https://${config.domain}/guides/${slug}/" target="_blank" rel="noopener">
1442
+ Read full guide on ${esc(config.name)} ${externalLinkIcon}
1443
+ </a>
1444
+ </div>
1445
+ ${poweredByHTML(config)}
1446
+ `;
1447
+ });
1448
+ }
1449
+
1450
+ // src/widgets/usecase-card.ts
1451
+ function init8(el, config, opts) {
1452
+ const slug = el.dataset.slug;
1453
+ if (!slug) {
1454
+ return;
1455
+ }
1456
+ const shadow = createShadow(el, config);
1457
+ const root = createWidgetRoot(shadow, el, "peasy-usecase-card");
1458
+ renderLoading(root);
1459
+ fetchAPI(config, `/api/v1/use-cases/${slug}/`, { lang: opts.lang }).then((data) => {
1460
+ if (!data) {
1461
+ renderError(root, "Could not load use case information.", config);
1462
+ return;
1463
+ }
1464
+ const toolsHtml = data.tools && data.tools.length > 0 ? `<div class="peasy-usecase-tools">
1465
+ <span class="peasy-usecase-tools-label">Tools:</span>
1466
+ ${data.tools.slice(0, 4).map(
1467
+ (t) => `<a class="peasy-link" href="https://${config.domain}/search/?q=${encodeURIComponent(t.name)}" target="_blank" rel="noopener">${esc(t.name)}</a>`
1468
+ ).join("")}
1469
+ </div>` : "";
1470
+ root.innerHTML = `
1471
+ <style>
1472
+ .peasy-usecase-header {
1473
+ padding: 12px 16px 10px;
1474
+ border-bottom: 1px solid var(--border);
1475
+ background: var(--ribbon);
1476
+ }
1477
+ .peasy-usecase-desc {
1478
+ padding: 10px 16px 12px;
1479
+ font-size: 13px;
1480
+ color: var(--text);
1481
+ line-height: 1.6;
1482
+ border-bottom: 1px solid var(--border);
1483
+ }
1484
+ .peasy-usecase-tools {
1485
+ display: flex;
1486
+ align-items: center;
1487
+ flex-wrap: wrap;
1488
+ gap: 8px;
1489
+ padding: 8px 16px;
1490
+ border-bottom: 1px solid var(--border);
1491
+ }
1492
+ .peasy-usecase-tools-label {
1493
+ font-size: 11px;
1494
+ font-weight: 600;
1495
+ text-transform: uppercase;
1496
+ letter-spacing: 0.06em;
1497
+ color: var(--muted);
1498
+ }
1499
+ </style>
1500
+ <div class="peasy-usecase-header">
1501
+ <p class="peasy-title" style="margin:0 0 6px 0;">${esc(data.name)}</p>
1502
+ <div class="peasy-meta" style="margin-bottom:0;">
1503
+ ${data.industry ? `<span class="peasy-badge">${esc(data.industry)}</span>` : ""}
1504
+ </div>
1505
+ </div>
1506
+ ${data.description ? `<div class="peasy-usecase-desc">${esc(data.description)}</div>` : ""}
1507
+ ${toolsHtml}
1508
+ <div class="peasy-actions">
1509
+ <a class="peasy-link" href="https://${config.domain}/use-cases/${slug}/" target="_blank" rel="noopener">
1510
+ ${esc(data.name)} use case guide on ${esc(config.name)} ${externalLinkIcon}
1511
+ </a>
1512
+ </div>
1513
+ ${poweredByHTML(config)}
1514
+ `;
1515
+ });
1516
+ }
1517
+
1518
+ // src/widgets/format-badge.ts
1519
+ function init9(el, config, opts) {
1520
+ const slug = el.dataset.slug;
1521
+ if (!slug) {
1522
+ return;
1523
+ }
1524
+ const shadow = el.attachShadow({ mode: "open" });
1525
+ const style = document.createElement("style");
1526
+ style.textContent = `
1527
+ :host {
1528
+ display: inline-block;
1529
+ position: relative;
1530
+ font-family: system-ui, -apple-system, 'Segoe UI', Roboto, sans-serif;
1531
+ }
1532
+ .peasy-badge-link {
1533
+ display: inline-flex;
1534
+ align-items: center;
1535
+ gap: 4px;
1536
+ font-size: 11px;
1537
+ font-weight: 600;
1538
+ padding: 2px 8px;
1539
+ border-radius: 4px;
1540
+ background: #f3f4f6;
1541
+ color: #374151;
1542
+ text-decoration: none;
1543
+ text-transform: uppercase;
1544
+ letter-spacing: 0.04em;
1545
+ border-left: 3px solid ${config.accent};
1546
+ cursor: pointer;
1547
+ transition: background 0.15s;
1548
+ }
1549
+ .peasy-badge-link:hover { background: #e5e7eb; }
1550
+ .peasy-badge-link:hover + .peasy-badge-tooltip { display: block; }
1551
+ .peasy-badge-tooltip {
1552
+ display: none;
1553
+ position: absolute;
1554
+ top: calc(100% + 6px);
1555
+ left: 0;
1556
+ z-index: 9999;
1557
+ background: #1a1a1a;
1558
+ color: #f3f4f6;
1559
+ border-radius: 6px;
1560
+ padding: 10px 12px;
1561
+ font-size: 12px;
1562
+ min-width: 200px;
1563
+ max-width: 260px;
1564
+ box-shadow: 0 4px 12px rgba(0,0,0,0.2);
1565
+ line-height: 1.5;
1566
+ pointer-events: none;
1567
+ }
1568
+ .peasy-badge-tooltip-title {
1569
+ font-weight: 600;
1570
+ font-size: 13px;
1571
+ margin-bottom: 4px;
1572
+ }
1573
+ .peasy-badge-tooltip-row {
1574
+ display: flex;
1575
+ gap: 6px;
1576
+ margin-top: 3px;
1577
+ }
1578
+ .peasy-badge-tooltip-label { color: #9ca3af; }
1579
+ .peasy-badge-tooltip-value { color: #f3f4f6; }
1580
+ `;
1581
+ shadow.appendChild(style);
1582
+ const placeholder = document.createElement("span");
1583
+ placeholder.style.cssText = "display:inline-block;width:48px;height:18px;border-radius:4px;background:#e5e7eb;";
1584
+ shadow.appendChild(placeholder);
1585
+ fetchAPI(config, `/api/v1/formats/${slug}/`, { useLS: true, lang: opts.lang }).then((data) => {
1586
+ placeholder.remove();
1587
+ if (!data) {
1588
+ const err = document.createElement("span");
1589
+ err.textContent = slug.toUpperCase();
1590
+ err.style.cssText = "font-size:11px;color:#9ca3af;font-family:monospace;";
1591
+ shadow.appendChild(err);
1592
+ return;
1593
+ }
1594
+ const wrapper = document.createElement("div");
1595
+ wrapper.style.cssText = "position:relative;display:inline-block;";
1596
+ wrapper.innerHTML = `
1597
+ <a class="peasy-badge-link" href="https://${config.domain}/formats/${encodeURIComponent(slug)}/" target="_blank" rel="noopener">
1598
+ <span>${esc(data.extension.toUpperCase())}</span>
1599
+ <span style="opacity:0.5;">\u25AA</span>
1600
+ <span>${esc(data.category)}</span>
1601
+ </a>
1602
+ <div class="peasy-badge-tooltip">
1603
+ <div class="peasy-badge-tooltip-title">${esc(data.full_name)}</div>
1604
+ ${data.description ? `<div style="margin-bottom:6px;opacity:0.8;">${esc(data.description.substring(0, 100))}${data.description.length > 100 ? "\u2026" : ""}</div>` : ""}
1605
+ <div class="peasy-badge-tooltip-row">
1606
+ <span class="peasy-badge-tooltip-label">MIME:</span>
1607
+ <span class="peasy-badge-tooltip-value">${esc(data.mime_type)}</span>
1608
+ </div>
1609
+ <div class="peasy-badge-tooltip-row">
1610
+ <span class="peasy-badge-tooltip-label">Lossy:</span>
1611
+ <span class="peasy-badge-tooltip-value">${data.is_lossy ? "Yes" : "No"}</span>
1612
+ </div>
1613
+ </div>
1614
+ `;
1615
+ shadow.appendChild(wrapper);
1616
+ });
1617
+ }
1618
+
1619
+ // src/widgets/search-box.ts
1620
+ function debounce(fn, ms) {
1621
+ let timer;
1622
+ return (...args) => {
1623
+ clearTimeout(timer);
1624
+ timer = setTimeout(() => fn(...args), ms);
1625
+ };
1626
+ }
1627
+ function getResultIcon(type) {
1628
+ if (type === "format") return fileIcon;
1629
+ if (type === "glossary") return bookIcon;
1630
+ return toolIcon;
1631
+ }
1632
+ function getResultUrl(config, result) {
1633
+ if (result.type === "format") return `https://${config.domain}/formats/${result.slug}/`;
1634
+ if (result.type === "glossary") return `https://${config.domain}/glossary/${result.slug}/`;
1635
+ const catSlug = result.category?.slug || "tools";
1636
+ return `https://${config.domain}/${catSlug}/${result.slug}/`;
1637
+ }
1638
+ function init10(el, config, opts) {
1639
+ const dataset = el.dataset;
1640
+ const placeholder = dataset.placeholder || `Search tools, formats, guides on ${config.name}\u2026`;
1641
+ const shadow = createShadow(el, config);
1642
+ const root = createWidgetRoot(shadow, el, "peasy-search-box");
1643
+ root.innerHTML = `
1644
+ <style>
1645
+ .peasy-search-outer { padding: 14px 16px 0; position: relative; }
1646
+ .peasy-search-inner {
1647
+ display: flex;
1648
+ align-items: center;
1649
+ gap: 8px;
1650
+ border: 1px solid var(--input-border);
1651
+ border-radius: 8px;
1652
+ background: var(--input-bg);
1653
+ padding: 0 10px;
1654
+ transition: border-color 0.15s;
1655
+ }
1656
+ .peasy-search-inner:focus-within {
1657
+ border-color: var(--input-focus);
1658
+ box-shadow: 0 0 0 2px color-mix(in srgb, var(--input-focus) 20%, transparent);
1659
+ }
1660
+ .peasy-search-icon-wrap { color: var(--muted); display: flex; }
1661
+ .peasy-search-input-el {
1662
+ flex: 1;
1663
+ border: none;
1664
+ outline: none;
1665
+ background: transparent;
1666
+ color: var(--text);
1667
+ font-size: 14px;
1668
+ font-family: inherit;
1669
+ padding: 10px 0;
1670
+ min-width: 0;
1671
+ }
1672
+ .peasy-search-input-el::placeholder { color: var(--muted); }
1673
+ .peasy-search-results {
1674
+ display: none;
1675
+ position: absolute;
1676
+ top: calc(100% + 4px);
1677
+ left: 0;
1678
+ right: 0;
1679
+ z-index: 999;
1680
+ background: var(--bg);
1681
+ border: 1px solid var(--border);
1682
+ border-radius: 8px;
1683
+ box-shadow: var(--shadow), 0 4px 16px rgba(0,0,0,0.1);
1684
+ max-height: 320px;
1685
+ overflow-y: auto;
1686
+ }
1687
+ .peasy-search-results.open { display: block; }
1688
+ .peasy-search-result-item {
1689
+ display: flex;
1690
+ align-items: flex-start;
1691
+ gap: 10px;
1692
+ padding: 10px 14px;
1693
+ text-decoration: none;
1694
+ color: var(--text);
1695
+ border-bottom: 1px solid var(--border);
1696
+ cursor: pointer;
1697
+ transition: background 0.1s;
1698
+ }
1699
+ .peasy-search-result-item:last-child { border-bottom: none; }
1700
+ .peasy-search-result-item:hover,
1701
+ .peasy-search-result-item.peasy-highlighted {
1702
+ background: var(--ribbon);
1703
+ }
1704
+ .peasy-search-result-icon { color: var(--accent); flex-shrink: 0; margin-top: 2px; }
1705
+ .peasy-search-result-name { font-weight: 600; font-size: 13px; }
1706
+ .peasy-search-result-desc {
1707
+ font-size: 12px;
1708
+ color: var(--muted);
1709
+ line-height: 1.4;
1710
+ margin-top: 2px;
1711
+ display: -webkit-box;
1712
+ -webkit-line-clamp: 2;
1713
+ -webkit-box-orient: vertical;
1714
+ overflow: hidden;
1715
+ }
1716
+ .peasy-search-result-type {
1717
+ font-size: 10px;
1718
+ text-transform: uppercase;
1719
+ letter-spacing: 0.05em;
1720
+ color: var(--muted);
1721
+ margin-top: 2px;
1722
+ }
1723
+ .peasy-search-empty {
1724
+ padding: 16px;
1725
+ text-align: center;
1726
+ font-size: 13px;
1727
+ color: var(--muted);
1728
+ }
1729
+ .peasy-search-footer { padding: 8px 14px 14px; }
1730
+ </style>
1731
+ <div class="peasy-search-outer">
1732
+ <div class="peasy-search-inner">
1733
+ <span class="peasy-search-icon-wrap">${searchIcon}</span>
1734
+ <input class="peasy-search-input-el" type="search" placeholder="${esc(placeholder)}" autocomplete="off" spellcheck="false" />
1735
+ </div>
1736
+ <div class="peasy-search-results" role="listbox"></div>
1737
+ </div>
1738
+ <div class="peasy-search-footer">
1739
+ ${poweredByHTML(config)}
1740
+ </div>
1741
+ `;
1742
+ const input = root.querySelector(".peasy-search-input-el");
1743
+ const resultsBox = root.querySelector(".peasy-search-results");
1744
+ let currentResults = [];
1745
+ let highlightedIndex = -1;
1746
+ function openResults() {
1747
+ resultsBox.classList.add("open");
1748
+ }
1749
+ function closeResults() {
1750
+ resultsBox.classList.remove("open");
1751
+ highlightedIndex = -1;
1752
+ }
1753
+ function renderResults(results, query) {
1754
+ if (results.length === 0) {
1755
+ const emptyDiv = document.createElement("div");
1756
+ emptyDiv.className = "peasy-search-empty";
1757
+ emptyDiv.textContent = `No results for "${query}" \u2014 try a different search term`;
1758
+ resultsBox.replaceChildren(emptyDiv);
1759
+ openResults();
1760
+ return;
1761
+ }
1762
+ resultsBox.innerHTML = results.map((r, i) => {
1763
+ const url = getResultUrl(config, r);
1764
+ const desc = r.description ? r.description.length > 100 ? r.description.substring(0, 100) + "\u2026" : r.description : "";
1765
+ return `
1766
+ <a class="peasy-search-result-item" href="${url}" target="_blank" rel="noopener" data-index="${i}">
1767
+ <span class="peasy-search-result-icon">${getResultIcon(r.type)}</span>
1768
+ <div>
1769
+ <div class="peasy-search-result-name">${esc(r.name)}</div>
1770
+ ${desc ? `<div class="peasy-search-result-desc">${esc(desc)}</div>` : ""}
1771
+ <div class="peasy-search-result-type">${esc(r.type)}</div>
1772
+ </div>
1773
+ </a>
1774
+ `;
1775
+ }).join("");
1776
+ openResults();
1777
+ highlightedIndex = -1;
1778
+ }
1779
+ function updateHighlight() {
1780
+ const items = resultsBox.querySelectorAll(".peasy-search-result-item");
1781
+ items.forEach((item, i) => {
1782
+ item.classList.toggle("peasy-highlighted", i === highlightedIndex);
1783
+ });
1784
+ }
1785
+ const doSearch = debounce((...args) => {
1786
+ const query = args[0];
1787
+ if (query.length < 2) {
1788
+ closeResults();
1789
+ return;
1790
+ }
1791
+ fetchAPI(config, `/api/v1/search/?q=${encodeURIComponent(query)}`, { lang: opts.lang }).then((resp) => {
1792
+ currentResults = resp?.results ?? [];
1793
+ renderResults(currentResults, query);
1794
+ });
1795
+ }, 300);
1796
+ input.addEventListener("input", () => {
1797
+ const q = input.value.trim();
1798
+ if (q.length < 2) {
1799
+ closeResults();
1800
+ return;
1801
+ }
1802
+ doSearch(q);
1803
+ });
1804
+ input.addEventListener("keydown", (e) => {
1805
+ const items = resultsBox.querySelectorAll(".peasy-search-result-item");
1806
+ if (!resultsBox.classList.contains("open") || items.length === 0) return;
1807
+ if (e.key === "ArrowDown") {
1808
+ e.preventDefault();
1809
+ highlightedIndex = Math.min(highlightedIndex + 1, items.length - 1);
1810
+ updateHighlight();
1811
+ } else if (e.key === "ArrowUp") {
1812
+ e.preventDefault();
1813
+ highlightedIndex = Math.max(highlightedIndex - 1, 0);
1814
+ updateHighlight();
1815
+ } else if (e.key === "Enter" && highlightedIndex >= 0) {
1816
+ e.preventDefault();
1817
+ const link = items[highlightedIndex];
1818
+ if (link.href) {
1819
+ window.open(link.href, "_blank", "noopener");
1820
+ closeResults();
1821
+ }
1822
+ } else if (e.key === "Escape") {
1823
+ closeResults();
1824
+ }
1825
+ });
1826
+ document.addEventListener("click", (e) => {
1827
+ if (!el.contains(e.target)) {
1828
+ closeResults();
1829
+ }
1830
+ }, { capture: true });
1831
+ }
1832
+
1833
+ // src/iframe/interactive.ts
1834
+ function init11(el, config, opts) {
1835
+ const dataset = el.dataset;
1836
+ const slug = dataset.slug;
1837
+ const height = dataset.height || "600";
1838
+ if (!slug) {
1839
+ return;
1840
+ }
1841
+ const iframeUrl = `${config.embedBase}/${encodeURI(slug)}/?theme=${encodeURIComponent(opts.theme)}`;
1842
+ const toolUrl = `https://${config.domain}/${slug}/`;
1843
+ const shadow = el.attachShadow({ mode: "open" });
1844
+ const style = document.createElement("style");
1845
+ style.textContent = getThemeCSS(config.accent) + `
1846
+ .peasy-iframe-wrap {
1847
+ width: 100%;
1848
+ font-family: system-ui, -apple-system, 'Segoe UI', Roboto, sans-serif;
1849
+ }
1850
+ .peasy-iframe-skeleton {
1851
+ width: 100%;
1852
+ border-radius: 8px;
1853
+ background: linear-gradient(90deg, var(--border) 25%, var(--ribbon) 50%, var(--border) 75%);
1854
+ background-size: 200% 100%;
1855
+ animation: peasy-shimmer 1.4s infinite;
1856
+ display: flex;
1857
+ align-items: center;
1858
+ justify-content: center;
1859
+ color: var(--muted);
1860
+ font-size: 13px;
1861
+ gap: 8px;
1862
+ }
1863
+ @keyframes peasy-shimmer {
1864
+ 0% { background-position: 200% 0; }
1865
+ 100% { background-position: -200% 0; }
1866
+ }
1867
+ .peasy-iframe-el {
1868
+ display: none;
1869
+ width: 100%;
1870
+ border: none;
1871
+ border-radius: 8px;
1872
+ border: 1px solid var(--border);
1873
+ }
1874
+ .peasy-iframe-footer {
1875
+ display: flex;
1876
+ align-items: center;
1877
+ justify-content: space-between;
1878
+ padding: 8px 4px 0;
1879
+ }
1880
+ `;
1881
+ shadow.appendChild(style);
1882
+ const container = document.createElement("div");
1883
+ container.className = "peasy-widget peasy-iframe-wrap";
1884
+ container.setAttribute("data-theme", opts.theme);
1885
+ container.setAttribute("data-size", opts.size);
1886
+ const skeleton = document.createElement("div");
1887
+ skeleton.className = "peasy-iframe-skeleton";
1888
+ skeleton.style.height = `${height}px`;
1889
+ skeleton.innerHTML = `
1890
+ <span class="peasy-spinner" style="width:18px;height:18px;border:2px solid #e5e7eb;border-top-color:${config.accent};border-radius:50%;animation:peasy-spin 0.7s linear infinite;display:inline-block;"></span>
1891
+ Loading ${config.name} tool\u2026
1892
+ `;
1893
+ container.appendChild(skeleton);
1894
+ const iframe = document.createElement("iframe");
1895
+ iframe.className = "peasy-iframe-el";
1896
+ iframe.src = iframeUrl;
1897
+ iframe.width = "100%";
1898
+ iframe.height = `${height}`;
1899
+ iframe.setAttribute("frameborder", "0");
1900
+ iframe.setAttribute("allow", "clipboard-write");
1901
+ iframe.setAttribute("style", `border:none;border-radius:8px;width:100%;height:${height}px;`);
1902
+ iframe.setAttribute("loading", "lazy");
1903
+ iframe.addEventListener("load", () => {
1904
+ skeleton.style.display = "none";
1905
+ iframe.style.display = "block";
1906
+ });
1907
+ container.appendChild(iframe);
1908
+ const footer = document.createElement("div");
1909
+ footer.className = "peasy-iframe-footer";
1910
+ footer.innerHTML = `
1911
+ <a class="peasy-link" href="${toolUrl}" target="_blank" rel="noopener" style="font-size:12px;font-weight:500;color:${config.accent};text-decoration:none;display:inline-flex;align-items:center;gap:4px;">
1912
+ Open ${esc(slug.split("/").pop() || "tool")} in full view on ${esc(config.name)} ${externalLinkIcon}
1913
+ </a>
1914
+ ${poweredByHTML(config)}
1915
+ `;
1916
+ container.appendChild(footer);
1917
+ shadow.appendChild(container);
1918
+ }
1919
+
1920
+ // src/tools/word-counter.ts
1921
+ function init12(el, config, opts) {
1922
+ const dataset = el.dataset;
1923
+ const placeholder = dataset.placeholder || "Type or paste your text here\u2026";
1924
+ const initialText = dataset.text || "";
1925
+ const shadow = createShadow(el, config);
1926
+ const root = createWidgetRoot(shadow, el, "peasy-word-counter");
1927
+ root.innerHTML = `
1928
+ <style>
1929
+ .peasy-wc-body { padding: 12px 16px; }
1930
+ .peasy-wc-textarea {
1931
+ width: 100%;
1932
+ min-height: 120px;
1933
+ padding: 10px 12px;
1934
+ border: 1px solid var(--input-border);
1935
+ border-radius: 6px;
1936
+ background: var(--input-bg);
1937
+ color: var(--text);
1938
+ font-size: 14px;
1939
+ font-family: inherit;
1940
+ line-height: 1.6;
1941
+ resize: vertical;
1942
+ outline: none;
1943
+ transition: border-color 0.15s;
1944
+ box-sizing: border-box;
1945
+ }
1946
+ .peasy-wc-textarea:focus {
1947
+ border-color: var(--input-focus);
1948
+ box-shadow: 0 0 0 2px color-mix(in srgb, var(--input-focus) 20%, transparent);
1949
+ }
1950
+ .peasy-wc-stats {
1951
+ display: grid;
1952
+ grid-template-columns: repeat(auto-fit, minmax(80px, 1fr));
1953
+ gap: 8px;
1954
+ margin-top: 10px;
1955
+ }
1956
+ .peasy-wc-stat-card {
1957
+ background: var(--ribbon);
1958
+ border: 1px solid var(--border);
1959
+ border-radius: 6px;
1960
+ padding: 8px 10px;
1961
+ text-align: center;
1962
+ }
1963
+ .peasy-wc-stat-value {
1964
+ font-size: 18px;
1965
+ font-weight: 700;
1966
+ color: var(--accent);
1967
+ line-height: 1.2;
1968
+ }
1969
+ .peasy-wc-stat-label {
1970
+ font-size: 11px;
1971
+ color: var(--muted);
1972
+ margin-top: 2px;
1973
+ }
1974
+ </style>
1975
+ <div class="peasy-wc-body">
1976
+ <textarea class="peasy-wc-textarea" placeholder="${esc(placeholder)}">${esc(initialText)}</textarea>
1977
+ <div class="peasy-wc-stats">
1978
+ <div class="peasy-wc-stat-card">
1979
+ <div class="peasy-wc-stat-value" id="wc-words">0</div>
1980
+ <div class="peasy-wc-stat-label">Words</div>
1981
+ </div>
1982
+ <div class="peasy-wc-stat-card">
1983
+ <div class="peasy-wc-stat-value" id="wc-chars">0</div>
1984
+ <div class="peasy-wc-stat-label">Characters</div>
1985
+ </div>
1986
+ <div class="peasy-wc-stat-card">
1987
+ <div class="peasy-wc-stat-value" id="wc-sentences">0</div>
1988
+ <div class="peasy-wc-stat-label">Sentences</div>
1989
+ </div>
1990
+ <div class="peasy-wc-stat-card">
1991
+ <div class="peasy-wc-stat-value" id="wc-lines">0</div>
1992
+ <div class="peasy-wc-stat-label">Lines</div>
1993
+ </div>
1994
+ <div class="peasy-wc-stat-card">
1995
+ <div class="peasy-wc-stat-value" id="wc-reading">0</div>
1996
+ <div class="peasy-wc-stat-label">Min read</div>
1997
+ </div>
1998
+ </div>
1999
+ </div>
2000
+ ${poweredByHTML(config)}
2001
+ `;
2002
+ const textarea = root.querySelector(".peasy-wc-textarea");
2003
+ const wcWords = root.querySelector("#wc-words");
2004
+ const wcChars = root.querySelector("#wc-chars");
2005
+ const wcSentences = root.querySelector("#wc-sentences");
2006
+ const wcLines = root.querySelector("#wc-lines");
2007
+ const wcReading = root.querySelector("#wc-reading");
2008
+ function update() {
2009
+ const text = textarea.value;
2010
+ const words = text.trim() === "" ? 0 : text.trim().split(/\s+/).length;
2011
+ const chars = text.length;
2012
+ const sentences = text === "" ? 0 : (text.match(/[.!?]+/g) || []).length;
2013
+ const lines = text === "" ? 0 : text.split(/\n/).length;
2014
+ const readingTime = Math.max(1, Math.ceil(words / 200));
2015
+ wcWords.textContent = String(words);
2016
+ wcChars.textContent = String(chars);
2017
+ wcSentences.textContent = String(sentences);
2018
+ wcLines.textContent = String(lines);
2019
+ wcReading.textContent = String(readingTime);
2020
+ }
2021
+ textarea.addEventListener("input", update);
2022
+ if (initialText) {
2023
+ update();
2024
+ }
2025
+ }
2026
+
2027
+ // src/tools/encoder.ts
2028
+ function encodeBase64(text) {
2029
+ try {
2030
+ return btoa(unescape(encodeURIComponent(text)));
2031
+ } catch {
2032
+ return btoa(text);
2033
+ }
2034
+ }
2035
+ function encodeHTML(text) {
2036
+ return text.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&#39;");
2037
+ }
2038
+ function encodeUnicode(text) {
2039
+ return Array.from(text).map((c) => {
2040
+ const cp = c.codePointAt(0);
2041
+ if (cp === void 0) return "";
2042
+ return `U+${cp.toString(16).toUpperCase().padStart(4, "0")}`;
2043
+ }).join(" ");
2044
+ }
2045
+ function encode(text, mode) {
2046
+ switch (mode) {
2047
+ case "base64":
2048
+ return encodeBase64(text);
2049
+ case "url":
2050
+ return encodeURIComponent(text);
2051
+ case "html":
2052
+ return encodeHTML(text);
2053
+ case "unicode":
2054
+ return encodeUnicode(text);
2055
+ }
2056
+ }
2057
+ function init13(el, config, opts) {
2058
+ const shadow = createShadow(el, config);
2059
+ const root = createWidgetRoot(shadow, el, "peasy-encoder");
2060
+ root.innerHTML = `
2061
+ <style>
2062
+ .peasy-enc-body { padding: 12px 16px; }
2063
+ .peasy-enc-textarea {
2064
+ width: 100%;
2065
+ min-height: 80px;
2066
+ padding: 10px 12px;
2067
+ border: 1px solid var(--input-border);
2068
+ border-radius: 6px;
2069
+ background: var(--input-bg);
2070
+ color: var(--text);
2071
+ font-size: 13px;
2072
+ font-family: ui-monospace, 'Fira Code', monospace;
2073
+ line-height: 1.5;
2074
+ resize: vertical;
2075
+ outline: none;
2076
+ transition: border-color 0.15s;
2077
+ box-sizing: border-box;
2078
+ margin-bottom: 10px;
2079
+ }
2080
+ .peasy-enc-textarea:focus {
2081
+ border-color: var(--input-focus);
2082
+ box-shadow: 0 0 0 2px color-mix(in srgb, var(--input-focus) 20%, transparent);
2083
+ }
2084
+ .peasy-enc-tabs {
2085
+ display: flex;
2086
+ gap: 4px;
2087
+ margin-bottom: 8px;
2088
+ flex-wrap: wrap;
2089
+ }
2090
+ .peasy-enc-tab {
2091
+ padding: 5px 12px;
2092
+ border-radius: 5px;
2093
+ border: 1px solid var(--border);
2094
+ background: var(--ribbon);
2095
+ color: var(--text);
2096
+ font-size: 12px;
2097
+ font-weight: 500;
2098
+ cursor: pointer;
2099
+ font-family: inherit;
2100
+ transition: background 0.1s, border-color 0.1s;
2101
+ }
2102
+ .peasy-enc-tab.active {
2103
+ background: var(--accent);
2104
+ color: #fff;
2105
+ border-color: var(--accent);
2106
+ }
2107
+ .peasy-enc-output-wrap {
2108
+ position: relative;
2109
+ }
2110
+ .peasy-enc-copy-row {
2111
+ display: flex;
2112
+ justify-content: flex-end;
2113
+ margin-top: 6px;
2114
+ }
2115
+ </style>
2116
+ <div class="peasy-enc-body">
2117
+ <textarea class="peasy-enc-textarea" placeholder="Enter text to encode\u2026"></textarea>
2118
+ <div class="peasy-enc-tabs">
2119
+ <button class="peasy-enc-tab active" data-mode="base64">Base64</button>
2120
+ <button class="peasy-enc-tab" data-mode="url">URL Encode</button>
2121
+ <button class="peasy-enc-tab" data-mode="html">HTML Entities</button>
2122
+ <button class="peasy-enc-tab" data-mode="unicode">Unicode</button>
2123
+ </div>
2124
+ <div class="peasy-enc-output-wrap">
2125
+ <textarea class="peasy-enc-textarea" id="enc-output" readonly placeholder="Encoded output will appear here\u2026" style="background:var(--ribbon);margin-bottom:0;"></textarea>
2126
+ <div class="peasy-enc-copy-row">
2127
+ <button class="peasy-copy-btn" id="enc-copy-btn">${copyIcon} Copy</button>
2128
+ </div>
2129
+ </div>
2130
+ </div>
2131
+ ${poweredByHTML(config)}
2132
+ `;
2133
+ const inputTA = root.querySelector(".peasy-enc-textarea");
2134
+ const outputTA = root.querySelector("#enc-output");
2135
+ const copyBtn = root.querySelector("#enc-copy-btn");
2136
+ const tabs = root.querySelectorAll(".peasy-enc-tab");
2137
+ let currentMode = "base64";
2138
+ function runEncode() {
2139
+ outputTA.value = inputTA.value ? encode(inputTA.value, currentMode) : "";
2140
+ bindCopyButton(copyBtn, outputTA.value);
2141
+ }
2142
+ tabs.forEach((tab) => {
2143
+ tab.addEventListener("click", () => {
2144
+ tabs.forEach((t) => t.classList.remove("active"));
2145
+ tab.classList.add("active");
2146
+ currentMode = tab.dataset.mode;
2147
+ runEncode();
2148
+ });
2149
+ });
2150
+ inputTA.addEventListener("input", runEncode);
2151
+ bindCopyButton(copyBtn, "");
2152
+ }
2153
+
2154
+ // src/tools/hash-generator.ts
2155
+ async function computeHash(algo, text) {
2156
+ const encoder = new TextEncoder();
2157
+ const data = encoder.encode(text);
2158
+ const hashBuffer = await crypto.subtle.digest(algo, data);
2159
+ const hashArray = Array.from(new Uint8Array(hashBuffer));
2160
+ return hashArray.map((b) => b.toString(16).padStart(2, "0")).join("");
2161
+ }
2162
+ function init14(el, config, opts) {
2163
+ const shadow = createShadow(el, config);
2164
+ const root = createWidgetRoot(shadow, el, "peasy-hash-generator");
2165
+ root.innerHTML = `
2166
+ <style>
2167
+ .peasy-hash-body { padding: 12px 16px; }
2168
+ .peasy-hash-textarea {
2169
+ width: 100%;
2170
+ min-height: 80px;
2171
+ padding: 10px 12px;
2172
+ border: 1px solid var(--input-border);
2173
+ border-radius: 6px;
2174
+ background: var(--input-bg);
2175
+ color: var(--text);
2176
+ font-size: 13px;
2177
+ font-family: inherit;
2178
+ line-height: 1.5;
2179
+ resize: vertical;
2180
+ outline: none;
2181
+ transition: border-color 0.15s;
2182
+ box-sizing: border-box;
2183
+ margin-bottom: 12px;
2184
+ }
2185
+ .peasy-hash-textarea:focus {
2186
+ border-color: var(--input-focus);
2187
+ box-shadow: 0 0 0 2px color-mix(in srgb, var(--input-focus) 20%, transparent);
2188
+ }
2189
+ .peasy-hash-row {
2190
+ margin-bottom: 10px;
2191
+ }
2192
+ .peasy-hash-label {
2193
+ font-size: 11px;
2194
+ font-weight: 600;
2195
+ text-transform: uppercase;
2196
+ letter-spacing: 0.06em;
2197
+ color: var(--muted);
2198
+ margin-bottom: 4px;
2199
+ display: block;
2200
+ }
2201
+ .peasy-hash-value-row {
2202
+ display: flex;
2203
+ align-items: center;
2204
+ gap: 6px;
2205
+ }
2206
+ .peasy-hash-value {
2207
+ flex: 1;
2208
+ font-family: ui-monospace, 'Fira Code', monospace;
2209
+ font-size: 11px;
2210
+ color: var(--text);
2211
+ background: var(--ribbon);
2212
+ border: 1px solid var(--border);
2213
+ border-radius: 5px;
2214
+ padding: 6px 10px;
2215
+ word-break: break-all;
2216
+ min-height: 30px;
2217
+ }
2218
+ .peasy-hash-placeholder { color: var(--muted); font-style: italic; }
2219
+ </style>
2220
+ <div class="peasy-hash-body">
2221
+ <textarea class="peasy-hash-textarea" placeholder="Enter text to generate cryptographic hash\u2026"></textarea>
2222
+ <div class="peasy-hash-row">
2223
+ <span class="peasy-hash-label">SHA-1</span>
2224
+ <div class="peasy-hash-value-row">
2225
+ <div class="peasy-hash-value" id="hash-sha1"><span class="peasy-hash-placeholder">\u2014</span></div>
2226
+ <button class="peasy-copy-btn" id="hash-sha1-copy">${copyIcon} Copy</button>
2227
+ </div>
2228
+ </div>
2229
+ <div class="peasy-hash-row">
2230
+ <span class="peasy-hash-label">SHA-256</span>
2231
+ <div class="peasy-hash-value-row">
2232
+ <div class="peasy-hash-value" id="hash-sha256"><span class="peasy-hash-placeholder">\u2014</span></div>
2233
+ <button class="peasy-copy-btn" id="hash-sha256-copy">${copyIcon} Copy</button>
2234
+ </div>
2235
+ </div>
2236
+ </div>
2237
+ ${poweredByHTML(config)}
2238
+ `;
2239
+ const textarea = root.querySelector(".peasy-hash-textarea");
2240
+ const sha1El = root.querySelector("#hash-sha1");
2241
+ const sha256El = root.querySelector("#hash-sha256");
2242
+ const sha1CopyBtn = root.querySelector("#hash-sha1-copy");
2243
+ const sha256CopyBtn = root.querySelector("#hash-sha256-copy");
2244
+ function setPlaceholder(el2) {
2245
+ el2.innerHTML = '<span class="peasy-hash-placeholder">\u2014</span>';
2246
+ }
2247
+ async function updateHashes() {
2248
+ const text = textarea.value;
2249
+ if (!text) {
2250
+ setPlaceholder(sha1El);
2251
+ setPlaceholder(sha256El);
2252
+ return;
2253
+ }
2254
+ const [sha1, sha256] = await Promise.all([
2255
+ computeHash("SHA-1", text),
2256
+ computeHash("SHA-256", text)
2257
+ ]);
2258
+ sha1El.textContent = sha1;
2259
+ sha256El.textContent = sha256;
2260
+ bindCopyButton(sha1CopyBtn, sha1);
2261
+ bindCopyButton(sha256CopyBtn, sha256);
2262
+ }
2263
+ textarea.addEventListener("input", () => {
2264
+ void updateHashes();
2265
+ });
2266
+ bindCopyButton(sha1CopyBtn, "");
2267
+ bindCopyButton(sha256CopyBtn, "");
2268
+ }
2269
+
2270
+ // src/tools/css-preview.ts
2271
+ var DEFAULT_CSS = `/* Try editing this CSS */
2272
+ body {
2273
+ font-family: system-ui, sans-serif;
2274
+ padding: 20px;
2275
+ background: #f9fafb;
2276
+ }
2277
+
2278
+ .preview {
2279
+ background: white;
2280
+ padding: 16px;
2281
+ border-radius: 8px;
2282
+ box-shadow: 0 1px 3px rgba(0,0,0,0.1);
2283
+ }
2284
+
2285
+ span {
2286
+ color: #6366f1;
2287
+ font-weight: 600;
2288
+ }`;
2289
+ function init15(el, config, opts) {
2290
+ const dataset = el.dataset;
2291
+ const initialCode = dataset.code || DEFAULT_CSS;
2292
+ const shadow = createShadow(el, config);
2293
+ const root = createWidgetRoot(shadow, el, "peasy-css-preview");
2294
+ root.innerHTML = `
2295
+ <style>
2296
+ .peasy-css-body {
2297
+ display: flex;
2298
+ flex-direction: column;
2299
+ gap: 0;
2300
+ }
2301
+ .peasy-css-editor-wrap {
2302
+ padding: 12px 16px;
2303
+ border-bottom: 1px solid var(--border);
2304
+ }
2305
+ .peasy-css-label {
2306
+ font-size: 11px;
2307
+ font-weight: 600;
2308
+ text-transform: uppercase;
2309
+ letter-spacing: 0.06em;
2310
+ color: var(--muted);
2311
+ margin-bottom: 6px;
2312
+ display: block;
2313
+ }
2314
+ .peasy-css-textarea {
2315
+ width: 100%;
2316
+ min-height: 140px;
2317
+ padding: 10px 12px;
2318
+ border: 1px solid var(--input-border);
2319
+ border-radius: 6px;
2320
+ background: var(--input-bg);
2321
+ color: var(--text);
2322
+ font-size: 12px;
2323
+ font-family: ui-monospace, 'Fira Code', monospace;
2324
+ line-height: 1.6;
2325
+ resize: vertical;
2326
+ outline: none;
2327
+ transition: border-color 0.15s;
2328
+ box-sizing: border-box;
2329
+ tab-size: 2;
2330
+ }
2331
+ .peasy-css-textarea:focus {
2332
+ border-color: var(--input-focus);
2333
+ box-shadow: 0 0 0 2px color-mix(in srgb, var(--input-focus) 20%, transparent);
2334
+ }
2335
+ .peasy-css-preview-wrap {
2336
+ padding: 12px 16px;
2337
+ }
2338
+ .peasy-css-preview-frame {
2339
+ width: 100%;
2340
+ min-height: 120px;
2341
+ border: 1px solid var(--border);
2342
+ border-radius: 6px;
2343
+ background: white;
2344
+ box-sizing: border-box;
2345
+ }
2346
+ </style>
2347
+ <div class="peasy-css-body">
2348
+ <div class="peasy-css-editor-wrap">
2349
+ <span class="peasy-css-label">CSS Code</span>
2350
+ <textarea class="peasy-css-textarea" spellcheck="false">${initialCode}</textarea>
2351
+ </div>
2352
+ <div class="peasy-css-preview-wrap">
2353
+ <span class="peasy-css-label">Preview</span>
2354
+ <iframe class="peasy-css-preview-frame" sandbox="" frameborder="0"></iframe>
2355
+ </div>
2356
+ </div>
2357
+ ${poweredByHTML(config)}
2358
+ `;
2359
+ const textarea = root.querySelector(".peasy-css-textarea");
2360
+ const frame = root.querySelector(".peasy-css-preview-frame");
2361
+ function updatePreview() {
2362
+ const css = textarea.value;
2363
+ const srcdoc = `<!DOCTYPE html><html><head><style>${css}</style></head><body><div class="preview">Sample text <span>and elements</span> \u2014 CSS preview</div></body></html>`;
2364
+ frame.srcdoc = srcdoc;
2365
+ frame.onload = () => {
2366
+ try {
2367
+ const h = frame.contentDocument?.body.scrollHeight;
2368
+ if (h) frame.style.height = `${Math.max(80, h)}px`;
2369
+ } catch {
2370
+ }
2371
+ };
2372
+ }
2373
+ textarea.addEventListener("input", updatePreview);
2374
+ updatePreview();
2375
+ }
2376
+
2377
+ // src/tools/css-minifier.ts
2378
+ function minifyCSS(css) {
2379
+ return css.replace(/\/\*[\s\S]*?\*\//g, "").replace(/\s+/g, " ").replace(/\s*([{}:;,>+~])\s*/g, "$1").replace(/;}/g, "}").trim();
2380
+ }
2381
+ function formatBytes(bytes) {
2382
+ if (bytes < 1024) return `${bytes} B`;
2383
+ return `${(bytes / 1024).toFixed(1)} KB`;
2384
+ }
2385
+ function init16(el, config, opts) {
2386
+ const shadow = createShadow(el, config);
2387
+ const root = createWidgetRoot(shadow, el, "peasy-css-minifier");
2388
+ root.innerHTML = `
2389
+ <style>
2390
+ .peasy-min-body { padding: 12px 16px; }
2391
+ .peasy-min-textarea {
2392
+ width: 100%;
2393
+ min-height: 120px;
2394
+ padding: 10px 12px;
2395
+ border: 1px solid var(--input-border);
2396
+ border-radius: 6px;
2397
+ background: var(--input-bg);
2398
+ color: var(--text);
2399
+ font-size: 12px;
2400
+ font-family: ui-monospace, 'Fira Code', monospace;
2401
+ line-height: 1.5;
2402
+ resize: vertical;
2403
+ outline: none;
2404
+ transition: border-color 0.15s;
2405
+ box-sizing: border-box;
2406
+ margin-bottom: 10px;
2407
+ tab-size: 2;
2408
+ }
2409
+ .peasy-min-textarea:focus {
2410
+ border-color: var(--input-focus);
2411
+ box-shadow: 0 0 0 2px color-mix(in srgb, var(--input-focus) 20%, transparent);
2412
+ }
2413
+ .peasy-min-stats {
2414
+ display: flex;
2415
+ gap: 10px;
2416
+ flex-wrap: wrap;
2417
+ margin-bottom: 10px;
2418
+ align-items: center;
2419
+ }
2420
+ .peasy-min-stat {
2421
+ font-size: 12px;
2422
+ color: var(--muted);
2423
+ }
2424
+ .peasy-min-stat strong { color: var(--text); }
2425
+ .peasy-min-savings {
2426
+ font-size: 12px;
2427
+ font-weight: 600;
2428
+ color: #16a34a;
2429
+ margin-left: auto;
2430
+ }
2431
+ .peasy-min-bar-wrap {
2432
+ width: 100%;
2433
+ height: 6px;
2434
+ background: var(--border);
2435
+ border-radius: 3px;
2436
+ overflow: hidden;
2437
+ margin-bottom: 10px;
2438
+ }
2439
+ .peasy-min-bar {
2440
+ height: 100%;
2441
+ background: var(--accent);
2442
+ border-radius: 3px;
2443
+ transition: width 0.3s;
2444
+ width: 0%;
2445
+ }
2446
+ .peasy-min-output-label {
2447
+ font-size: 11px;
2448
+ font-weight: 600;
2449
+ text-transform: uppercase;
2450
+ letter-spacing: 0.06em;
2451
+ color: var(--muted);
2452
+ margin-bottom: 4px;
2453
+ }
2454
+ .peasy-min-copy-row {
2455
+ display: flex;
2456
+ justify-content: flex-end;
2457
+ margin-top: 6px;
2458
+ }
2459
+ </style>
2460
+ <div class="peasy-min-body">
2461
+ <textarea class="peasy-min-textarea" placeholder="Paste your CSS here to minify\u2026"></textarea>
2462
+ <div class="peasy-min-stats">
2463
+ <span class="peasy-min-stat">Original: <strong id="min-orig-size">0 B</strong></span>
2464
+ <span class="peasy-min-stat">Minified: <strong id="min-mini-size">0 B</strong></span>
2465
+ <span class="peasy-min-savings" id="min-savings" style="display:none;"></span>
2466
+ </div>
2467
+ <div class="peasy-min-bar-wrap">
2468
+ <div class="peasy-min-bar" id="min-bar"></div>
2469
+ </div>
2470
+ <div class="peasy-min-output-label">Minified Output</div>
2471
+ <textarea class="peasy-min-textarea" id="min-output" readonly placeholder="Minified CSS appears here\u2026" style="background:var(--ribbon);min-height:60px;margin-bottom:0;"></textarea>
2472
+ <div class="peasy-min-copy-row">
2473
+ <button class="peasy-copy-btn" id="min-copy-btn">${copyIcon} Copy Minified</button>
2474
+ </div>
2475
+ </div>
2476
+ ${poweredByHTML(config)}
2477
+ `;
2478
+ const inputTA = root.querySelector(".peasy-min-textarea");
2479
+ const outputTA = root.querySelector("#min-output");
2480
+ const copyBtn = root.querySelector("#min-copy-btn");
2481
+ const origSizeEl = root.querySelector("#min-orig-size");
2482
+ const miniSizeEl = root.querySelector("#min-mini-size");
2483
+ const savingsEl = root.querySelector("#min-savings");
2484
+ const bar = root.querySelector("#min-bar");
2485
+ function update() {
2486
+ const css = inputTA.value;
2487
+ const minified = minifyCSS(css);
2488
+ outputTA.value = minified;
2489
+ const origBytes = new TextEncoder().encode(css).length;
2490
+ const miniBytes = new TextEncoder().encode(minified).length;
2491
+ const saving = origBytes > 0 ? Math.round((1 - miniBytes / origBytes) * 100) : 0;
2492
+ const barWidth = origBytes > 0 ? Math.max(5, 100 - saving) : 0;
2493
+ origSizeEl.textContent = formatBytes(origBytes);
2494
+ miniSizeEl.textContent = formatBytes(miniBytes);
2495
+ bar.style.width = `${barWidth}%`;
2496
+ if (origBytes > 0 && saving > 0) {
2497
+ savingsEl.textContent = `\u2212${saving}% saved`;
2498
+ savingsEl.style.display = "inline";
2499
+ } else {
2500
+ savingsEl.style.display = "none";
2501
+ }
2502
+ bindCopyButton(copyBtn, minified);
2503
+ }
2504
+ inputTA.addEventListener("input", update);
2505
+ bindCopyButton(copyBtn, "");
2506
+ }
2507
+
2508
+ // src/tools/color-converter.ts
2509
+ function parseHex(hex) {
2510
+ const clean = hex.replace("#", "");
2511
+ if (!/^[0-9a-fA-F]{3}$|^[0-9a-fA-F]{6}$/.test(clean)) return null;
2512
+ const full = clean.length === 3 ? clean.split("").map((c) => c + c).join("") : clean;
2513
+ return {
2514
+ r: parseInt(full.slice(0, 2), 16),
2515
+ g: parseInt(full.slice(2, 4), 16),
2516
+ b: parseInt(full.slice(4, 6), 16)
2517
+ };
2518
+ }
2519
+ function rgbToHsl(r, g, b) {
2520
+ const rn = r / 255;
2521
+ const gn = g / 255;
2522
+ const bn = b / 255;
2523
+ const max = Math.max(rn, gn, bn);
2524
+ const min = Math.min(rn, gn, bn);
2525
+ let h = 0;
2526
+ let s = 0;
2527
+ const l = (max + min) / 2;
2528
+ if (max !== min) {
2529
+ const d = max - min;
2530
+ s = l > 0.5 ? d / (2 - max - min) : d / (max + min);
2531
+ switch (max) {
2532
+ case rn:
2533
+ h = ((gn - bn) / d + (gn < bn ? 6 : 0)) / 6;
2534
+ break;
2535
+ case gn:
2536
+ h = ((bn - rn) / d + 2) / 6;
2537
+ break;
2538
+ case bn:
2539
+ h = ((rn - gn) / d + 4) / 6;
2540
+ break;
2541
+ }
2542
+ }
2543
+ return { h: Math.round(h * 360), s: Math.round(s * 100), l: Math.round(l * 100) };
2544
+ }
2545
+ function init17(el, config, opts) {
2546
+ const dataset = el.dataset;
2547
+ const initialHex = dataset.hex || "";
2548
+ const shadow = createShadow(el, config);
2549
+ const root = createWidgetRoot(shadow, el, "peasy-color-converter");
2550
+ root.innerHTML = `
2551
+ <style>
2552
+ .peasy-color-body { padding: 12px 16px; }
2553
+ .peasy-color-input-row {
2554
+ display: flex;
2555
+ align-items: center;
2556
+ gap: 8px;
2557
+ margin-bottom: 12px;
2558
+ }
2559
+ .peasy-color-swatch {
2560
+ width: 36px;
2561
+ height: 36px;
2562
+ border-radius: 6px;
2563
+ border: 1px solid var(--border);
2564
+ flex-shrink: 0;
2565
+ background: #f3f4f6;
2566
+ transition: background 0.2s;
2567
+ }
2568
+ .peasy-color-hex-input {
2569
+ flex: 1;
2570
+ padding: 8px 12px;
2571
+ border: 1px solid var(--input-border);
2572
+ border-radius: 6px;
2573
+ background: var(--input-bg);
2574
+ color: var(--text);
2575
+ font-size: 14px;
2576
+ font-family: ui-monospace, 'Fira Code', monospace;
2577
+ outline: none;
2578
+ transition: border-color 0.15s;
2579
+ }
2580
+ .peasy-color-hex-input:focus {
2581
+ border-color: var(--input-focus);
2582
+ box-shadow: 0 0 0 2px color-mix(in srgb, var(--input-focus) 20%, transparent);
2583
+ }
2584
+ .peasy-color-rows { display: flex; flex-direction: column; gap: 6px; }
2585
+ .peasy-color-row {
2586
+ display: flex;
2587
+ align-items: center;
2588
+ gap: 8px;
2589
+ padding: 6px 10px;
2590
+ background: var(--ribbon);
2591
+ border: 1px solid var(--border);
2592
+ border-radius: 6px;
2593
+ }
2594
+ .peasy-color-row-label {
2595
+ font-size: 11px;
2596
+ font-weight: 700;
2597
+ text-transform: uppercase;
2598
+ color: var(--muted);
2599
+ width: 48px;
2600
+ flex-shrink: 0;
2601
+ }
2602
+ .peasy-color-row-value {
2603
+ flex: 1;
2604
+ font-size: 13px;
2605
+ font-family: ui-monospace, 'Fira Code', monospace;
2606
+ color: var(--text);
2607
+ }
2608
+ .peasy-color-empty { color: var(--muted); font-size: 13px; }
2609
+ </style>
2610
+ <div class="peasy-color-body">
2611
+ <div class="peasy-color-input-row">
2612
+ <div class="peasy-color-swatch" id="color-swatch"></div>
2613
+ <input class="peasy-color-hex-input" type="text" placeholder="#FF6B35 or FF6B35" maxlength="7" value="${initialHex}" />
2614
+ </div>
2615
+ <div class="peasy-color-rows" id="color-results">
2616
+ <div class="peasy-color-empty">Enter a hex color code above to convert formats</div>
2617
+ </div>
2618
+ </div>
2619
+ ${poweredByHTML(config)}
2620
+ `;
2621
+ const hexInput = root.querySelector(".peasy-color-hex-input");
2622
+ const swatch = root.querySelector("#color-swatch");
2623
+ const resultsEl = root.querySelector("#color-results");
2624
+ function makeRow(label, value, id) {
2625
+ return `
2626
+ <div class="peasy-color-row">
2627
+ <span class="peasy-color-row-label">${label}</span>
2628
+ <span class="peasy-color-row-value" id="${id}">${value}</span>
2629
+ <button class="peasy-copy-btn" data-copy-id="${id}">${copyIcon} Copy</button>
2630
+ </div>
2631
+ `;
2632
+ }
2633
+ function update() {
2634
+ const raw = hexInput.value.trim();
2635
+ const hex = raw.startsWith("#") ? raw : `#${raw}`;
2636
+ const rgb = parseHex(hex);
2637
+ if (!rgb) {
2638
+ swatch.style.background = "#f3f4f6";
2639
+ resultsEl.innerHTML = raw.length < 3 ? '<div class="peasy-color-empty">Enter a hex color code above to convert formats</div>' : '<div class="peasy-color-empty" style="color:#dc2626;">Invalid hex color \u2014 use format #RRGGBB</div>';
2640
+ return;
2641
+ }
2642
+ const hsl = rgbToHsl(rgb.r, rgb.g, rgb.b);
2643
+ const cleanHex = hex.toUpperCase();
2644
+ const rgbStr = `rgb(${rgb.r}, ${rgb.g}, ${rgb.b})`;
2645
+ const hslStr = `hsl(${hsl.h}, ${hsl.s}%, ${hsl.l}%)`;
2646
+ swatch.style.background = cleanHex;
2647
+ resultsEl.innerHTML = makeRow("HEX", cleanHex, "cv-hex") + makeRow("RGB", rgbStr, "cv-rgb") + makeRow("HSL", hslStr, "cv-hsl");
2648
+ resultsEl.querySelectorAll("[data-copy-id]").forEach((btn) => {
2649
+ const id = btn.dataset.copyId;
2650
+ const valueEl = resultsEl.querySelector(`#${id}`);
2651
+ bindCopyButton(btn, valueEl?.textContent || "");
2652
+ });
2653
+ }
2654
+ hexInput.addEventListener("input", update);
2655
+ if (initialHex) {
2656
+ update();
2657
+ }
2658
+ }
2659
+
2660
+ // src/tools/compression-ratio.ts
2661
+ var ALGOS = [
2662
+ { name: "Gzip", ratio: 0.32, color: "#3b82f6" },
2663
+ { name: "Brotli", ratio: 0.28, color: "#8b5cf6" },
2664
+ { name: "Zstd", ratio: 0.29, color: "#06b6d4" },
2665
+ { name: "LZ4", ratio: 0.41, color: "#f59e0b" }
2666
+ ];
2667
+ function formatSize(mb) {
2668
+ if (mb < 1e-3) return `${(mb * 1024 * 1024).toFixed(0)} B`;
2669
+ if (mb < 1) return `${(mb * 1024).toFixed(1)} KB`;
2670
+ return `${mb.toFixed(2)} MB`;
2671
+ }
2672
+ function init18(el, config, opts) {
2673
+ const shadow = createShadow(el, config);
2674
+ const root = createWidgetRoot(shadow, el, "peasy-compression-ratio");
2675
+ root.innerHTML = `
2676
+ <style>
2677
+ .peasy-cr-body { padding: 14px 16px; }
2678
+ .peasy-cr-input-row {
2679
+ display: flex;
2680
+ align-items: center;
2681
+ gap: 10px;
2682
+ margin-bottom: 14px;
2683
+ flex-wrap: wrap;
2684
+ }
2685
+ .peasy-cr-input-label {
2686
+ font-size: 13px;
2687
+ color: var(--text);
2688
+ font-weight: 500;
2689
+ flex-shrink: 0;
2690
+ }
2691
+ .peasy-cr-number-input {
2692
+ width: 90px;
2693
+ padding: 7px 10px;
2694
+ border: 1px solid var(--input-border);
2695
+ border-radius: 6px;
2696
+ background: var(--input-bg);
2697
+ color: var(--text);
2698
+ font-size: 14px;
2699
+ font-family: inherit;
2700
+ outline: none;
2701
+ transition: border-color 0.15s;
2702
+ }
2703
+ .peasy-cr-number-input:focus {
2704
+ border-color: var(--input-focus);
2705
+ box-shadow: 0 0 0 2px color-mix(in srgb, var(--input-focus) 20%, transparent);
2706
+ }
2707
+ .peasy-cr-slider {
2708
+ flex: 1;
2709
+ min-width: 100px;
2710
+ accent-color: var(--accent);
2711
+ }
2712
+ .peasy-cr-algo-rows { display: flex; flex-direction: column; gap: 8px; }
2713
+ .peasy-cr-algo-row {
2714
+ display: grid;
2715
+ grid-template-columns: 60px 1fr 80px;
2716
+ align-items: center;
2717
+ gap: 10px;
2718
+ }
2719
+ .peasy-cr-algo-name { font-size: 13px; font-weight: 600; color: var(--text); }
2720
+ .peasy-cr-bar-track {
2721
+ height: 10px;
2722
+ background: var(--border);
2723
+ border-radius: 5px;
2724
+ overflow: hidden;
2725
+ }
2726
+ .peasy-cr-bar-fill {
2727
+ height: 100%;
2728
+ border-radius: 5px;
2729
+ transition: width 0.3s;
2730
+ }
2731
+ .peasy-cr-algo-size { font-size: 12px; color: var(--muted); text-align: right; }
2732
+ </style>
2733
+ <div class="peasy-cr-body">
2734
+ <div class="peasy-cr-input-row">
2735
+ <span class="peasy-cr-input-label">File size:</span>
2736
+ <input class="peasy-cr-number-input" type="number" id="cr-size-input" min="0.001" max="10000" step="0.1" value="10" />
2737
+ <span class="peasy-cr-input-label">MB</span>
2738
+ <input class="peasy-cr-slider" type="range" id="cr-size-slider" min="1" max="100" value="10" step="1" />
2739
+ </div>
2740
+ <div class="peasy-cr-algo-rows" id="cr-results"></div>
2741
+ </div>
2742
+ ${poweredByHTML(config)}
2743
+ `;
2744
+ const sizeInput = root.querySelector("#cr-size-input");
2745
+ const sizeSlider = root.querySelector("#cr-size-slider");
2746
+ const resultsEl = root.querySelector("#cr-results");
2747
+ function renderResults(sizeMB) {
2748
+ resultsEl.innerHTML = ALGOS.map((algo) => {
2749
+ const compressed = sizeMB * algo.ratio;
2750
+ const pct = Math.round(algo.ratio * 100);
2751
+ return `
2752
+ <div class="peasy-cr-algo-row">
2753
+ <span class="peasy-cr-algo-name">${algo.name}</span>
2754
+ <div class="peasy-cr-bar-track">
2755
+ <div class="peasy-cr-bar-fill" style="width:${pct}%;background:${algo.color};"></div>
2756
+ </div>
2757
+ <span class="peasy-cr-algo-size">${formatSize(compressed)} (${pct}%)</span>
2758
+ </div>
2759
+ `;
2760
+ }).join("");
2761
+ }
2762
+ function update() {
2763
+ const val = parseFloat(sizeInput.value);
2764
+ if (isNaN(val) || val <= 0) {
2765
+ return;
2766
+ }
2767
+ renderResults(val);
2768
+ }
2769
+ sizeInput.addEventListener("input", () => {
2770
+ sizeSlider.value = String(Math.min(100, parseFloat(sizeInput.value) || 10));
2771
+ update();
2772
+ });
2773
+ sizeSlider.addEventListener("input", () => {
2774
+ sizeInput.value = sizeSlider.value;
2775
+ update();
2776
+ });
2777
+ renderResults(10);
2778
+ }
2779
+
2780
+ // src/core.ts
2781
+ var RENDERERS = {
2782
+ // Layer 1
2783
+ format: init,
2784
+ tool: init2,
2785
+ convert: init3,
2786
+ compare: init4,
2787
+ glossary: init5,
2788
+ gallery: init6,
2789
+ guide: init7,
2790
+ usecase: init8,
2791
+ badge: init9,
2792
+ search: init10,
2793
+ // Layer 2
2794
+ interactive: init11,
2795
+ // Layer 3
2796
+ wordcount: init12,
2797
+ encode: init13,
2798
+ hash: init14,
2799
+ preview: init15,
2800
+ minify: init16,
2801
+ color: init17,
2802
+ ratio: init18
2803
+ };
2804
+ function initAll(config) {
2805
+ const elements = document.querySelectorAll(`[${config.attribute}]`);
2806
+ elements.forEach((el) => {
2807
+ if (el.shadowRoot) return;
2808
+ const widgetType = el.getAttribute(config.attribute);
2809
+ if (!widgetType) return;
2810
+ const renderer = RENDERERS[widgetType];
2811
+ if (!renderer) return;
2812
+ const opts = parseWidgetOptions(el);
2813
+ if (widgetType === "badge") {
2814
+ renderer(el, config, opts);
2815
+ } else {
2816
+ lazyInit(el, () => renderer(el, config, opts));
2817
+ }
2818
+ });
2819
+ }
2820
+ function maybeInit(el, config) {
2821
+ if (!el.hasAttribute(config.attribute) || el.shadowRoot) return;
2822
+ const widgetType = el.getAttribute(config.attribute);
2823
+ if (!widgetType) return;
2824
+ const renderer = RENDERERS[widgetType];
2825
+ if (!renderer) return;
2826
+ const opts = parseWidgetOptions(el);
2827
+ if (widgetType === "badge") {
2828
+ renderer(el, config, opts);
2829
+ } else {
2830
+ lazyInit(el, () => renderer(el, config, opts));
2831
+ }
2832
+ }
2833
+ (function bootstrap() {
2834
+ const config = define_INJECTED_CONFIG_default;
2835
+ if (document.readyState === "loading") {
2836
+ document.addEventListener("DOMContentLoaded", () => initAll(config));
2837
+ } else {
2838
+ initAll(config);
2839
+ }
2840
+ const observer = new MutationObserver((mutations) => {
2841
+ mutations.forEach((mutation) => {
2842
+ mutation.addedNodes.forEach((node) => {
2843
+ if (node.nodeType !== Node.ELEMENT_NODE) return;
2844
+ const el = node;
2845
+ maybeInit(el, config);
2846
+ el.querySelectorAll?.(`[${config.attribute}]`).forEach((child) => {
2847
+ maybeInit(child, config);
2848
+ });
2849
+ });
2850
+ });
2851
+ });
2852
+ observer.observe(document.body || document.documentElement, {
2853
+ childList: true,
2854
+ subtree: true
2855
+ });
2856
+ })();