@pure-ds/core 0.7.60 → 0.7.61

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,768 @@
1
+ import { PDS, html, State } from "#pds";
2
+
3
+ /**
4
+ * <pds-render> - Pure Design System sandboxed renderer
5
+ *
6
+ * A first-class PDS web component that embeds an isolated iframe with full PDS
7
+ * support: tokens, components, enhancements, theme switching, and preset switching.
8
+ *
9
+ * Attributes (observed, reactive):
10
+ * theme - "light" | "dark" | "system" (default: "light")
11
+ * preset - any PDS preset ID (default: "default")
12
+ * height - CSS length for iframe height (default: "200px")
13
+ * src-base - base URL for PDS assets (default: auto-detected)
14
+ * padding - body padding inside iframe (default: "1rem")
15
+ * background - override body background (default: "")
16
+ * viewport - "mobile" (375 px) | "tablet" (768 px) | "desktop" (window width)
17
+ * sets the virtual viewport width for auto-zoom (default: "desktop")
18
+ * locale - BCP-47 language tag set on <html lang> (default: "en")
19
+ * expanded - forces fullscreen preview mode when present
20
+ *
21
+ * Properties (live-update without full reload):
22
+ * html - HTML string to render in the iframe body
23
+ * css - CSS string injected as <style> in the iframe
24
+ * js - JS module string executed after PDS is ready
25
+ * JS changes force a full iframe reload
26
+ * expanded - reflected fullscreen state
27
+ *
28
+ * Slots:
29
+ * expand-icon - overrides the default expand icon
30
+ * collapse-icon - overrides the default collapse icon
31
+ *
32
+ * CSS Parts:
33
+ * frame, iframe, overlay, toolbar, expand-button, collapse-button,
34
+ * expand-icon, collapse-icon
35
+ *
36
+ * Events:
37
+ * pds-render-ready - fired when the iframe is initialized and PDS is running
38
+ * pds-render-error - fired if the iframe script throws; detail.error = error object
39
+ *
40
+ * Methods:
41
+ * setContent(content, options?) - batch updates html/css/js via a single public contract
42
+ * update(content, options?) - alias of setContent for API ergonomics
43
+ * reload() - force full srcdoc rebuild
44
+ */
45
+
46
+ /**
47
+ * @typedef {object} PdsRenderContent
48
+ * @property {string} [html] HTML string rendered in the iframe body.
49
+ * @property {string} [css] CSS text injected into the iframe user stylesheet.
50
+ * @property {string} [js] JavaScript module source evaluated in the iframe runtime.
51
+ */
52
+
53
+ /**
54
+ * @typedef {object} PdsRenderSetContentOptions
55
+ * @property {boolean} [reload=false] Force full iframe rebuild after applying content.
56
+ */
57
+
58
+ const LAYERS = ["primitives", "components", "utilities"];
59
+
60
+ const COMPONENT_CSS = /*css*/ `
61
+ @layer pds-render {
62
+ :host {
63
+ display: block;
64
+ position: relative;
65
+ overflow: hidden;
66
+ border-radius: var(--radius-md, 0.375rem);
67
+ background: var(--pds-render-surface, var(--color-surface-base, Canvas));
68
+ }
69
+
70
+ :host([bordered]) {
71
+ border: var(--border-width-thin, 1px) solid var(--pds-render-border-color, var(--color-border, currentColor));
72
+ }
73
+
74
+ :host([expanded]) {
75
+ position: fixed !important;
76
+ inset: 0 !important;
77
+ inline-size: 100vw !important;
78
+ block-size: 100vh !important;
79
+ z-index: var(--z-modal, 9999);
80
+ border-radius: 0 !important;
81
+ overflow: hidden;
82
+ }
83
+
84
+ .zoom-wrapper {
85
+ position: relative;
86
+ transform-origin: top left;
87
+ }
88
+
89
+ iframe {
90
+ display: block;
91
+ border: none;
92
+ background: var(--pds-render-frame-background, transparent);
93
+ }
94
+
95
+ .overlay {
96
+ display: flex;
97
+ position: absolute;
98
+ inset: 0;
99
+ align-items: center;
100
+ justify-content: center;
101
+ opacity: 0;
102
+ pointer-events: none;
103
+ transform: none;
104
+ isolation: isolate;
105
+ transition:
106
+ opacity var(--transition-fast, 0.22s) ease,
107
+ background var(--transition-fast, 0.22s) ease;
108
+ background: color-mix(in oklab, var(--color-scrim, CanvasText) 0%, transparent);
109
+ z-index: 1;
110
+ }
111
+
112
+ :host([resizable]) .overlay {
113
+ pointer-events: auto;
114
+ }
115
+
116
+ :host([resizable]:hover) .overlay {
117
+ opacity: 1;
118
+ background: color-mix(in oklab, var(--color-scrim, CanvasText) 42%, transparent);
119
+ }
120
+
121
+ :host([expanded]) .overlay {
122
+ display: none;
123
+ }
124
+
125
+ .toolbar {
126
+ transform: none;
127
+ isolation: isolate;
128
+ display: none;
129
+ position: absolute;
130
+ inset-block-start: var(--spacing-5, 1.25rem);
131
+ inset-inline-end: var(--spacing-5, 1.25rem);
132
+ opacity: 0.2;
133
+ transition: opacity var(--transition-fast, 0.22s) ease;
134
+ z-index: 2;
135
+ pointer-events: none;
136
+ }
137
+
138
+ :host([expanded]) .toolbar {
139
+ display: flex;
140
+ pointer-events: auto;
141
+ }
142
+
143
+ :host([expanded]) .toolbar:hover {
144
+ opacity: 0.9;
145
+ }
146
+
147
+ .control-button {
148
+ appearance: none;
149
+ display: inline-flex;
150
+ align-items: center;
151
+ justify-content: center;
152
+ padding: 0;
153
+ border: 0;
154
+ background: none;
155
+ color: var(--pds-render-control-color, var(--color-static-white, #fff));
156
+ cursor: pointer;
157
+ }
158
+
159
+ .control-button:focus-visible {
160
+ outline: var(--focus-outline, 2px solid currentColor);
161
+ outline-offset: var(--spacing-1, 0.25rem);
162
+ }
163
+
164
+ .overlay-btn {
165
+ filter: drop-shadow(0 0 1rem color-mix(in oklab, black 85%, transparent));
166
+ transform: scale(5);
167
+ transform-origin: center;
168
+ }
169
+
170
+ .toolbar-btn {
171
+ filter:
172
+ drop-shadow(0 0 0.5rem color-mix(in oklab, black 100%, transparent))
173
+ drop-shadow(0 0 1rem color-mix(in oklab, black 90%, transparent))
174
+ drop-shadow(0 0 2rem color-mix(in oklab, black 80%, transparent));
175
+ }
176
+
177
+ .icon-wrap {
178
+ display: inline-flex;
179
+ align-items: center;
180
+ justify-content: center;
181
+ inline-size: 100%;
182
+ block-size: 100%;
183
+ }
184
+
185
+ .overlay-btn .icon-wrap {
186
+ inline-size: 1rem;
187
+ block-size: 1rem;
188
+ }
189
+
190
+ .toolbar-btn .icon-wrap {
191
+ inline-size: 5rem;
192
+ block-size: 5rem;
193
+ }
194
+
195
+ slot[name="expand-icon"],
196
+ slot[name="collapse-icon"] {
197
+ display: inline-flex;
198
+ align-items: center;
199
+ justify-content: center;
200
+ inline-size: 100%;
201
+ block-size: 100%;
202
+ }
203
+
204
+ ::slotted([slot="expand-icon"]),
205
+ ::slotted([slot="collapse-icon"]) {
206
+ display: inline-flex;
207
+ align-items: center;
208
+ justify-content: center;
209
+ inline-size: 100%;
210
+ block-size: 100%;
211
+ color: currentColor;
212
+ }
213
+
214
+ svg {
215
+ display: block;
216
+ inline-size: 100%;
217
+ block-size: 100%;
218
+ fill: currentColor;
219
+ }
220
+
221
+ [hidden] {
222
+ display: none !important;
223
+ }
224
+ }
225
+ `;
226
+
227
+ class PdsRender extends HTMLElement {
228
+ static #componentSheet;
229
+
230
+ static get observedAttributes() {
231
+ return [
232
+ "theme",
233
+ "preset",
234
+ "height",
235
+ "aspect-ratio",
236
+ "src-base",
237
+ "padding",
238
+ "background",
239
+ "zoom",
240
+ "viewport",
241
+ "locale",
242
+ "resizable",
243
+ "expanded",
244
+ ];
245
+ }
246
+
247
+ constructor() {
248
+ super();
249
+ this._html = "";
250
+ this._css = "";
251
+ this._js = "";
252
+ this._ready = false;
253
+ this._iframeReady = false;
254
+ this._pendingMessages = [];
255
+ this._bodyOverflow = null;
256
+ this._adoptPromise = null;
257
+
258
+ this._state = new State({ expanded: this.hasAttribute("expanded") }, this);
259
+ this._state.on("change:expanded", () => {
260
+ this._applyExpandedState(Boolean(this._state.expanded));
261
+ });
262
+
263
+ this._messageHandler = (e) => {
264
+ if (e.source !== this._iframe?.contentWindow) return;
265
+ const { type, detail } = e.data || {};
266
+ if (type === "pds-render:ready") {
267
+ this._iframeReady = true;
268
+ for (const msg of this._pendingMessages) this._send(msg);
269
+ this._pendingMessages = [];
270
+ this.dispatchEvent(new CustomEvent("pds-render-ready", { bubbles: true, composed: true }));
271
+ }
272
+ if (type === "pds-render:error") {
273
+ this.dispatchEvent(
274
+ new CustomEvent("pds-render-error", {
275
+ bubbles: true,
276
+ composed: true,
277
+ detail: { error: detail },
278
+ }),
279
+ );
280
+ }
281
+ };
282
+
283
+ this._escHandler = (e) => {
284
+ if (e.key === "Escape" && this.expanded) this._toggleExpanded(false);
285
+ };
286
+
287
+ this._root = this.attachShadow({ mode: "open" });
288
+ this._renderShadow();
289
+ this._cacheShadowRefs();
290
+ this._syncExpandedControls();
291
+ this._adoptStyles().catch((error) => {
292
+ console.warn("[pds-render] adoptLayers failed", error);
293
+ });
294
+ }
295
+
296
+ connectedCallback() {
297
+ this._ready = true;
298
+
299
+ for (const [pub, priv] of [["html", "_html"], ["css", "_css"], ["js", "_js"]]) {
300
+ if (Object.prototype.hasOwnProperty.call(this, pub)) {
301
+ const val = this[pub];
302
+ delete this[pub];
303
+ this[priv] = String(val || "");
304
+ }
305
+ }
306
+
307
+ window.addEventListener("message", this._messageHandler);
308
+ window.addEventListener("keydown", this._escHandler);
309
+
310
+ this._resizeObserver = new ResizeObserver(() => {
311
+ if (this.getAttribute("zoom") === null) this._applyZoom();
312
+ });
313
+ this._resizeObserver.observe(this);
314
+
315
+ this._applyExpandedState(this.expanded, { syncAttribute: false });
316
+ this._buildSrcdoc();
317
+ }
318
+
319
+ disconnectedCallback() {
320
+ window.removeEventListener("message", this._messageHandler);
321
+ window.removeEventListener("keydown", this._escHandler);
322
+ this._resizeObserver?.disconnect();
323
+ this._setBodyScrollLocked(false);
324
+ this._ready = false;
325
+ this._iframeReady = false;
326
+ }
327
+
328
+ attributeChangedCallback(name, oldVal, newVal) {
329
+ if (name === "expanded") {
330
+ const expanded = newVal !== null;
331
+ if (this._state.expanded !== expanded) {
332
+ this._state.expanded = expanded;
333
+ }
334
+ return;
335
+ }
336
+
337
+ if (!this._ready || oldVal === newVal) return;
338
+ if (name === "resizable") return;
339
+ if (name === "height" || name === "aspect-ratio" || name === "zoom" || name === "viewport") {
340
+ this._applyZoom();
341
+ return;
342
+ }
343
+ if (name === "theme" && this._iframeReady) {
344
+ this._post({ type: "pds-render:set-theme", value: newVal || "light" });
345
+ return;
346
+ }
347
+ if (name === "preset" && this._iframeReady) {
348
+ this._post({ type: "pds-render:set-preset", value: newVal || "default" });
349
+ return;
350
+ }
351
+ if (name === "locale") {
352
+ if (this._iframeReady) {
353
+ this._post({ type: "pds-render:set-locale", value: newVal || "en" });
354
+ } else {
355
+ this._buildSrcdoc();
356
+ }
357
+ return;
358
+ }
359
+ this._buildSrcdoc();
360
+ }
361
+
362
+ get html() {
363
+ return this._html;
364
+ }
365
+
366
+ set html(value) {
367
+ this.setContent({ html: value });
368
+ }
369
+
370
+ get css() {
371
+ return this._css;
372
+ }
373
+
374
+ set css(value) {
375
+ this.setContent({ css: value });
376
+ }
377
+
378
+ get js() {
379
+ return this._js;
380
+ }
381
+
382
+ set js(value) {
383
+ this.setContent({ js: value }, { reload: true });
384
+ }
385
+
386
+ /**
387
+ * Batch update iframe content using the public component contract.
388
+ * Prefer this method when applying multiple fields at once.
389
+ *
390
+ * @param {PdsRenderContent} content Content payload with any combination of html/css/js.
391
+ * @param {PdsRenderSetContentOptions} [options] Content update options.
392
+ */
393
+ setContent(content = {}, options = {}) {
394
+ const next = content && typeof content === "object" ? content : {};
395
+ const { reload = false } = options || {};
396
+
397
+ const hasHtml = Object.prototype.hasOwnProperty.call(next, "html");
398
+ const hasCss = Object.prototype.hasOwnProperty.call(next, "css");
399
+ const hasJs = Object.prototype.hasOwnProperty.call(next, "js");
400
+
401
+ const nextHtml = hasHtml ? String(next.html || "") : this._html;
402
+ const nextCss = hasCss ? String(next.css || "") : this._css;
403
+ const nextJs = hasJs ? String(next.js || "") : this._js;
404
+
405
+ const htmlChanged = hasHtml && nextHtml !== this._html;
406
+ const cssChanged = hasCss && nextCss !== this._css;
407
+ const jsChanged = hasJs && nextJs !== this._js;
408
+
409
+ if (hasHtml) this._html = nextHtml;
410
+ if (hasCss) this._css = nextCss;
411
+ if (hasJs) this._js = nextJs;
412
+
413
+ if (!this._ready || !(htmlChanged || cssChanged || jsChanged || reload)) return;
414
+
415
+ if (reload || jsChanged) {
416
+ this.reload();
417
+ return;
418
+ }
419
+
420
+ if (this._iframeReady) {
421
+ if (htmlChanged) this._post({ type: "pds-render:set-html", value: this._html });
422
+ if (cssChanged) this._post({ type: "pds-render:set-css", value: this._css });
423
+ return;
424
+ }
425
+
426
+ this._buildSrcdoc();
427
+ }
428
+
429
+ /**
430
+ * Alias for setContent to support fluent consumer APIs.
431
+ *
432
+ * @param {PdsRenderContent} content Content payload with any combination of html/css/js.
433
+ * @param {PdsRenderSetContentOptions} [options] Content update options.
434
+ */
435
+ update(content = {}, options = {}) {
436
+ this.setContent(content, options);
437
+ }
438
+
439
+ get expanded() {
440
+ return Boolean(this._state.expanded);
441
+ }
442
+
443
+ set expanded(value) {
444
+ this.toggleAttribute("expanded", Boolean(value));
445
+ }
446
+
447
+ reload() {
448
+ this._iframeReady = false;
449
+ this._pendingMessages = [];
450
+ this._buildSrcdoc();
451
+ }
452
+
453
+ _renderShadow() {
454
+ const expanded = this.expanded;
455
+ this._root.replaceChildren(
456
+ html`
457
+ <div class="zoom-wrapper" part="frame">
458
+ <iframe title="PDS Render" part="iframe"></iframe>
459
+ </div>
460
+ <div class="overlay" part="overlay" aria-hidden="${expanded ? "true" : "false"}">
461
+ <button
462
+ type="button"
463
+ class="control-button overlay-btn"
464
+ part="expand-button"
465
+ aria-label="Expand preview"
466
+ ?hidden=${expanded}
467
+ @click=${(e) => {
468
+ e.stopPropagation();
469
+ this._toggleExpanded(true);
470
+ }}
471
+ >
472
+ <span class="icon-wrap" part="expand-icon">
473
+ <slot name="expand-icon">
474
+ <svg viewBox="0 0 256 256" aria-hidden="true">
475
+ <path d="M216,48V88a8,8,0,0,1-16,0V56H168a8,8,0,0,1,0-16h40A8,8,0,0,1,216,48ZM88,200H56V168a8,8,0,0,0-16,0v40a8,8,0,0,0,8,8H88a8,8,0,0,0,0-16Zm120-40a8,8,0,0,0-8,8v32H168a8,8,0,0,0,0,16h40a8,8,0,0,0,8-8V168A8,8,0,0,0,208,160ZM88,40H48a8,8,0,0,0-8,8V88a8,8,0,0,0,16,0V56H88a8,8,0,0,0,0-16Z"></path>
476
+ </svg>
477
+ </slot>
478
+ </span>
479
+ </button>
480
+ </div>
481
+ <div class="toolbar" part="toolbar" aria-hidden="${expanded ? "false" : "true"}">
482
+ <button
483
+ type="button"
484
+ class="control-button toolbar-btn"
485
+ part="collapse-button"
486
+ aria-label="Exit fullscreen preview"
487
+ ?hidden=${!expanded}
488
+ @click=${(e) => {
489
+ e.stopPropagation();
490
+ this._toggleExpanded(false);
491
+ }}
492
+ >
493
+ <span class="icon-wrap" part="collapse-icon">
494
+ <slot name="collapse-icon">
495
+ <svg viewBox="0 0 256 256" aria-hidden="true">
496
+ <path d="M152,96V48a8,8,0,0,1,16,0V88h40a8,8,0,0,1,0,16H160A8,8,0,0,1,152,96ZM96,152H48a8,8,0,0,0,0,16H88v40a8,8,0,0,0,16,0V160A8,8,0,0,0,96,152Zm112,0H160a8,8,0,0,0-8,8v48a8,8,0,0,0,16,0V168h40a8,8,0,0,0,0-16ZM96,40a8,8,0,0,0-8,8V88H48a8,8,0,0,0,0,16H96a8,8,0,0,0,8-8V48A8,8,0,0,0,96,40Z"></path>
497
+ </svg>
498
+ </slot>
499
+ </span>
500
+ </button>
501
+ </div>
502
+ `,
503
+ );
504
+ }
505
+
506
+ _cacheShadowRefs() {
507
+ this._zoomWrapper = this._root.querySelector(".zoom-wrapper");
508
+ this._iframe = this._root.querySelector("iframe");
509
+ this._overlay = this._root.querySelector(".overlay");
510
+ this._toolbar = this._root.querySelector(".toolbar");
511
+ this._overlayBtn = this._root.querySelector(".overlay-btn");
512
+ this._toolbarBtn = this._root.querySelector(".toolbar-btn");
513
+ }
514
+
515
+ async _adoptStyles() {
516
+ if (this._adoptPromise) return this._adoptPromise;
517
+ if (!PdsRender.#componentSheet) {
518
+ PdsRender.#componentSheet = PDS.createStylesheet(COMPONENT_CSS);
519
+ }
520
+ this._adoptPromise = PDS.adoptLayers(this._root, LAYERS, [PdsRender.#componentSheet]);
521
+ return this._adoptPromise;
522
+ }
523
+
524
+ _applyExpandedState(expanded, options = {}) {
525
+ const { syncAttribute = true } = options;
526
+
527
+ if (syncAttribute) {
528
+ this.toggleAttribute("expanded", expanded);
529
+ }
530
+
531
+ this._syncExpandedControls();
532
+
533
+ if (!this.isConnected) return;
534
+
535
+ this._setBodyScrollLocked(expanded);
536
+
537
+ if (this._ready) {
538
+ this._applyZoom();
539
+ }
540
+ }
541
+
542
+ _syncExpandedControls() {
543
+ const expanded = this.expanded;
544
+ this._overlay?.setAttribute("aria-hidden", String(expanded));
545
+ this._toolbar?.setAttribute("aria-hidden", String(!expanded));
546
+ if (this._overlayBtn) this._overlayBtn.hidden = expanded;
547
+ if (this._toolbarBtn) this._toolbarBtn.hidden = !expanded;
548
+ }
549
+
550
+ _setBodyScrollLocked(locked) {
551
+ if (locked) {
552
+ if (this._bodyOverflow === null) {
553
+ this._bodyOverflow = document.body.style.overflow;
554
+ }
555
+ document.body.style.overflow = "hidden";
556
+ return;
557
+ }
558
+
559
+ if (this._bodyOverflow !== null) {
560
+ document.body.style.overflow = this._bodyOverflow;
561
+ this._bodyOverflow = null;
562
+ }
563
+ }
564
+
565
+ _effectiveZoom() {
566
+ const attr = this.getAttribute("zoom");
567
+ if (attr !== null) return Math.max(0.05, Math.min(1, parseFloat(attr) || 1));
568
+ const viewport = this.getAttribute("viewport") || "desktop";
569
+ const refWidth = viewport === "mobile"
570
+ ? 375
571
+ : viewport === "tablet"
572
+ ? 768
573
+ : (window.innerWidth || 1280);
574
+ const width = this.offsetWidth;
575
+ if (!width) return 1;
576
+ return Math.min(1, width / refWidth);
577
+ }
578
+
579
+ _applyZoom() {
580
+ if (!this._zoomWrapper || !this._iframe) return;
581
+
582
+ if (this.expanded) {
583
+ const width = window.innerWidth;
584
+ const height = window.innerHeight;
585
+ this._zoomWrapper.style.transform = "";
586
+ this._zoomWrapper.style.width = `${width}px`;
587
+ this._zoomWrapper.style.height = `${height}px`;
588
+ this._iframe.style.width = `${width}px`;
589
+ this._iframe.style.height = `${height}px`;
590
+ this.style.height = "";
591
+ return;
592
+ }
593
+
594
+ const zoom = this._effectiveZoom();
595
+ const hostWidth = this.offsetWidth || 300;
596
+
597
+ const arAttr = this.getAttribute("aspect-ratio");
598
+ let displayHeight;
599
+ if (arAttr) {
600
+ const [aspectWidth, aspectHeight] = arAttr.split("/").map(Number);
601
+ displayHeight = Math.round(hostWidth * ((aspectHeight || 1) / (aspectWidth || 1)));
602
+ this.style.height = `${displayHeight}px`;
603
+ } else {
604
+ displayHeight = parseFloat(this.getAttribute("height")) || 200;
605
+ this.style.height = `${displayHeight}px`;
606
+ }
607
+
608
+ const iframeWidth = Math.round(hostWidth / zoom);
609
+ const iframeHeight = Math.round(displayHeight / zoom);
610
+
611
+ this._zoomWrapper.style.transform = zoom < 1 ? `scale(${zoom})` : "";
612
+ this._zoomWrapper.style.width = `${iframeWidth}px`;
613
+ this._zoomWrapper.style.height = `${iframeHeight}px`;
614
+ this._iframe.style.width = `${iframeWidth}px`;
615
+ this._iframe.style.height = `${iframeHeight}px`;
616
+ }
617
+
618
+ _toggleExpanded(force) {
619
+ const expanded = typeof force === "boolean" ? force : !this.expanded;
620
+
621
+ const doToggle = () => {
622
+ this._state.expanded = expanded;
623
+ };
624
+
625
+ if (!document.startViewTransition) {
626
+ doToggle();
627
+ return;
628
+ }
629
+
630
+ if (!document.getElementById("__pds-render-vt-style")) {
631
+ const style = document.createElement("style");
632
+ style.id = "__pds-render-vt-style";
633
+ style.textContent = [
634
+ "::view-transition-group(pds-render-expand){animation-duration:0.4s;animation-timing-function:cubic-bezier(0.4,0,0.2,1)}",
635
+ "::view-transition-old(pds-render-expand){animation:none;mix-blend-mode:normal}",
636
+ "::view-transition-new(pds-render-expand){animation:none;mix-blend-mode:normal}",
637
+ ].join("\n");
638
+ document.head.appendChild(style);
639
+ }
640
+
641
+ this.style.viewTransitionName = "pds-render-expand";
642
+ const transition = document.startViewTransition(doToggle);
643
+ transition.finished.finally(() => {
644
+ this.style.viewTransitionName = "";
645
+ });
646
+ }
647
+
648
+ _resolveSrcBase() {
649
+ const attr = this.getAttribute("src-base");
650
+ if (attr) return attr.endsWith("/") ? attr : `${attr}/`;
651
+ const scripts = document.querySelectorAll("script[src]");
652
+ for (const script of scripts) {
653
+ if (script.src.includes("/components/pds-render")) {
654
+ return script.src.replace(/\/components\/pds-render\.js.*$/, "/");
655
+ }
656
+ }
657
+ return `${window.location.origin}/assets/pds/`;
658
+ }
659
+
660
+ _buildSrcdoc() {
661
+ if (!this._ready || !this._iframe) return;
662
+ this._iframeReady = false;
663
+ this._pendingMessages = [];
664
+
665
+ const theme = this.getAttribute("theme") || "light";
666
+ const preset = this.getAttribute("preset") || "default";
667
+ const padding = this.getAttribute("padding") || "1rem";
668
+ const background = this.getAttribute("background") || "";
669
+ const lang = this.getAttribute("locale") || "en";
670
+ const base = this._resolveSrcBase();
671
+
672
+ this._applyZoom();
673
+
674
+ const cssUrl = `${base}styles/pds-styles.css`;
675
+ const coreUrl = `${base}core.js`;
676
+ const litUrl = `${base}external/lit.js`;
677
+
678
+ const presetInit = preset && preset !== "default"
679
+ ? `try { await PDS.applyLivePreset(${JSON.stringify(preset)}, { persist: false }); } catch(e) { notify('error', e); }`
680
+ : "";
681
+
682
+ const userJs = this._js
683
+ ? `try {\n${this._js}\n} catch(e) { notify('error', e); }`
684
+ : "";
685
+
686
+ const srcdoc = `<!DOCTYPE html>
687
+ <html lang="${lang}" data-theme="${theme}">
688
+ <head>
689
+ <meta charset="UTF-8">
690
+ <meta name="viewport" content="width=device-width,initial-scale=1">
691
+ <link rel="stylesheet" href="${cssUrl}">
692
+ <script type="importmap">
693
+ {"imports":{"#pds":"${coreUrl}","#pds/lit":"${litUrl}"}}
694
+ </script>
695
+ <style>
696
+ *,*::before,*::after{box-sizing:border-box}
697
+ html,body{margin:0;padding:0}
698
+ body{padding:${padding};${background ? `background:${background};` : ""}min-height:100vh}
699
+ </style>
700
+ <style id="__user-css">${this._css}</style>
701
+ </head>
702
+ <body>${this._html}
703
+ <script type="module">
704
+ import { PDS } from '#pds';
705
+
706
+ const __pdsBase = ${JSON.stringify(base)};
707
+
708
+ function notify(type, detail) {
709
+ window.parent.postMessage({ type: 'pds-render:' + type, detail: detail ? String(detail) : null }, '*');
710
+ }
711
+
712
+ try {
713
+ await PDS.start({ mode: 'live', preset: 'default' });
714
+ } catch(e) {}
715
+
716
+ ${presetInit}
717
+
718
+ ${userJs}
719
+
720
+ window.addEventListener('message', async (e) => {
721
+ if (!e.data || typeof e.data !== 'object') return;
722
+ const { type, value } = e.data;
723
+
724
+ if (type === 'pds-render:set-html') {
725
+ document.body.innerHTML = value;
726
+ }
727
+ if (type === 'pds-render:set-css') {
728
+ const style = document.getElementById('__user-css');
729
+ if (style) style.textContent = value;
730
+ }
731
+ if (type === 'pds-render:set-theme') {
732
+ document.documentElement.setAttribute('data-theme', value);
733
+ try { PDS.theme = value; } catch {}
734
+ }
735
+ if (type === 'pds-render:set-preset') {
736
+ try { await PDS.applyLivePreset(value, { persist: false }); } catch(e) { notify('error', e); }
737
+ }
738
+ if (type === 'pds-render:set-locale') {
739
+ document.documentElement.setAttribute('lang', value);
740
+ }
741
+ });
742
+
743
+ notify('ready', null);
744
+ </script>
745
+ </body>
746
+ </html>`;
747
+
748
+ this._iframe.srcdoc = srcdoc;
749
+ }
750
+
751
+ _post(msg) {
752
+ if (this._iframeReady) {
753
+ this._send(msg);
754
+ } else {
755
+ this._pendingMessages.push(msg);
756
+ }
757
+ }
758
+
759
+ _send(msg) {
760
+ try {
761
+ this._iframe.contentWindow?.postMessage(msg, "*");
762
+ } catch {}
763
+ }
764
+ }
765
+
766
+ if (!customElements.get("pds-render")) {
767
+ customElements.define("pds-render", PdsRender);
768
+ }