@dragcraft/device-frames 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.mjs ADDED
@@ -0,0 +1,820 @@
1
+ import { DEFAULT_LAYOUT_REGION, normalizeStyleValueMap } from "@dragcraft/core";
2
+ import { computed, defineComponent, h, inject, nextTick, onBeforeUnmount, onMounted, onUpdated, ref } from "vue";
3
+ import { IconDesktop, IconLaptop, IconNavBack, IconNavHome, IconNavRecent, IconPhone, IconRobot, IconSignal, IconSignalBar } from "@dragcraft/icons";
4
+ //#region src/components/frame-viewport.ts
5
+ function renderFrameRootSelectionPlane(selectionPresentation) {
6
+ return h("div", {
7
+ "ref": (element) => {
8
+ selectionPresentation?.registerPlane("root", element instanceof HTMLElement ? element : null);
9
+ },
10
+ "class": "dc-device-frame__selection-plane dc-device-frame__selection-plane--root",
11
+ "data-dc-selection-plane": "root",
12
+ "aria-hidden": "true"
13
+ });
14
+ }
15
+ function renderDeviceFrame(modifierClass, selectionPresentation, children, frameOverlay) {
16
+ return h("div", {
17
+ "class": ["dc-device-frame", modifierClass],
18
+ "data-dc-toolbar-boundary": ""
19
+ }, [
20
+ h("div", { class: "dc-device-frame__surface" }, children),
21
+ frameOverlay,
22
+ renderFrameRootSelectionPlane(selectionPresentation)
23
+ ]);
24
+ }
25
+ function edgeClass(edge) {
26
+ return `dc-device-frame__chrome--${edge}`;
27
+ }
28
+ function measureVar(edge) {
29
+ return `--dc-measured-inset-${edge}`;
30
+ }
31
+ function sizeVar(edge) {
32
+ return `--dc-sized-inset-${edge}`;
33
+ }
34
+ function sizeToCss(value) {
35
+ if (typeof value === "number") return `${value}px`;
36
+ return value ?? "0px";
37
+ }
38
+ function layerItemStyle(entry) {
39
+ const placement = entry.layout.placement;
40
+ if (placement.mode !== "framework") return void 0;
41
+ const style = {};
42
+ const block = placement.anchor.block ?? "end";
43
+ const inline = placement.anchor.inline ?? "end";
44
+ if (block === "start") style.top = sizeToCss(placement.offset?.blockStart ?? "calc(var(--dc-inset-block-start) + 16px)");
45
+ else if (block === "center") style.top = "50%";
46
+ else style.bottom = sizeToCss(placement.offset?.blockEnd ?? "calc(var(--dc-inset-block-end) + 16px)");
47
+ if (inline === "start") style.left = sizeToCss(placement.offset?.inlineStart ?? "calc(var(--dc-inset-inline-start) + 16px)");
48
+ else if (inline === "center") style.left = "50%";
49
+ else style.right = sizeToCss(placement.offset?.inlineEnd ?? "calc(var(--dc-inset-inline-end) + 16px)");
50
+ if (block === "center" && inline === "center") style.transform = "translate(-50%, -50%)";
51
+ else if (block === "center") style.transform = "translateY(-50%)";
52
+ else if (inline === "center") style.transform = "translateX(-50%)";
53
+ return style;
54
+ }
55
+ function renderChromeItem(entry, vnode) {
56
+ const placement = entry.layout.placement;
57
+ return h("div", {
58
+ "key": entry.node.id,
59
+ "class": [
60
+ "dc-device-frame__chrome-item",
61
+ edgeClass(placement.edge),
62
+ `dc-device-frame__chrome-item--${placement.position}`
63
+ ],
64
+ "data-node-id": entry.node.id,
65
+ "data-dc-chrome-edge": placement.edge,
66
+ "data-dc-chrome-position": placement.position,
67
+ "data-dc-reserve-mode": placement.reserve.mode,
68
+ "data-dc-avoid-content": String(placement.avoidContent)
69
+ }, [vnode]);
70
+ }
71
+ function renderChrome(plan, chromeVNodes) {
72
+ if (!plan || chromeVNodes.length === 0) return null;
73
+ const groups = {
74
+ "block-start": {
75
+ reserved: [],
76
+ overlay: []
77
+ },
78
+ "block-end": {
79
+ reserved: [],
80
+ overlay: []
81
+ },
82
+ "inline-start": {
83
+ reserved: [],
84
+ overlay: []
85
+ },
86
+ "inline-end": {
87
+ reserved: [],
88
+ overlay: []
89
+ }
90
+ };
91
+ for (const [index, entry] of plan.chrome.entries()) {
92
+ const placement = entry.layout.placement;
93
+ if (placement.position === "fixed") {
94
+ const stack = placement.avoidContent ? "reserved" : "overlay";
95
+ groups[placement.edge][stack].push(renderChromeItem(entry, chromeVNodes[index]));
96
+ }
97
+ }
98
+ const stacks = Object.entries(groups).flatMap(([edge, edgeGroups]) => ["reserved", "overlay"].flatMap((stack) => {
99
+ const items = edgeGroups[stack];
100
+ return items.length > 0 ? [h("div", {
101
+ "key": `${edge}:${stack}`,
102
+ "class": [
103
+ "dc-device-frame__chrome-stack",
104
+ `dc-device-frame__chrome-stack--${edge}`,
105
+ `dc-device-frame__chrome-stack--${stack}`
106
+ ],
107
+ "data-dc-chrome-edge": edge,
108
+ "data-dc-avoid-content-stack": String(stack === "reserved")
109
+ }, items)] : [];
110
+ }));
111
+ return stacks.length > 0 ? h("div", { class: "dc-device-frame__chrome" }, stacks) : null;
112
+ }
113
+ function groupContentChrome(plan, chromeVNodes) {
114
+ const groups = {
115
+ "block-start": [],
116
+ "block-end": [],
117
+ "inline-start": [],
118
+ "inline-end": []
119
+ };
120
+ for (const [index, entry] of (plan?.chrome ?? []).entries()) {
121
+ const placement = entry.layout.placement;
122
+ if (placement.position !== "fixed") groups[placement.edge].push(renderChromeItem(entry, chromeVNodes[index]));
123
+ }
124
+ return groups;
125
+ }
126
+ function renderLayers(plan, layerVNodes) {
127
+ if (!plan) return [];
128
+ return Array.from(plan.layers.entries()).map(([layer, entries]) => {
129
+ const vnodes = layerVNodes[layer] ?? [];
130
+ const children = entries.map((entry, index) => {
131
+ const placement = entry.layout.placement;
132
+ return h("div", {
133
+ "key": entry.node.id,
134
+ "class": ["dc-device-frame__layer-item", `dc-device-frame__layer-item--${placement.mode}`],
135
+ "data-node-id": entry.node.id,
136
+ "data-dc-layer-mode": placement.mode,
137
+ "style": layerItemStyle(entry)
138
+ }, [vnodes[index]]);
139
+ });
140
+ return h("div", {
141
+ "key": layer,
142
+ "class": ["dc-device-frame__layer", `dc-device-frame__layer--${layer}`],
143
+ "data-dc-layer": layer
144
+ }, children);
145
+ });
146
+ }
147
+ function useMeasuredChromeInsets(viewportRef) {
148
+ let observer = null;
149
+ let observedTargets = /* @__PURE__ */ new Set();
150
+ let updateFrame = null;
151
+ function getChromeMeasureTarget(item) {
152
+ return item.querySelector(":scope > .dc-node") ?? item;
153
+ }
154
+ function update() {
155
+ const viewport = viewportRef.value;
156
+ if (!viewport) return;
157
+ const totals = {
158
+ "block-start": 0,
159
+ "block-end": 0,
160
+ "inline-start": 0,
161
+ "inline-end": 0
162
+ };
163
+ for (const item of Array.from(viewport.querySelectorAll("[data-dc-chrome-position=\"fixed\"][data-dc-avoid-content=\"true\"]"))) {
164
+ if (item.dataset.dcReserveMode !== "measure") continue;
165
+ const edge = item.dataset.dcChromeEdge;
166
+ const rect = getChromeMeasureTarget(item).getBoundingClientRect();
167
+ totals[edge] += edge === "block-start" || edge === "block-end" ? rect.height : rect.width;
168
+ }
169
+ for (const [edge, value] of Object.entries(totals)) viewport.style.setProperty(measureVar(edge), `${value}px`);
170
+ }
171
+ function scheduleUpdate() {
172
+ if (updateFrame !== null) return;
173
+ updateFrame = window.requestAnimationFrame(() => {
174
+ updateFrame = null;
175
+ update();
176
+ });
177
+ }
178
+ function refreshObserver() {
179
+ const viewport = viewportRef.value;
180
+ if (!viewport) return;
181
+ if (typeof ResizeObserver === "undefined") {
182
+ scheduleUpdate();
183
+ return;
184
+ }
185
+ observer ??= new ResizeObserver(scheduleUpdate);
186
+ const nextTargets = new Set(Array.from(viewport.querySelectorAll("[data-dc-chrome-position=\"fixed\"][data-dc-avoid-content=\"true\"]"), getChromeMeasureTarget));
187
+ if (nextTargets.size !== observedTargets.size || [...nextTargets].some((target) => !observedTargets.has(target))) {
188
+ observer.disconnect();
189
+ for (const target of nextTargets) observer.observe(target);
190
+ observedTargets = nextTargets;
191
+ }
192
+ nextTick(scheduleUpdate);
193
+ }
194
+ onMounted(refreshObserver);
195
+ onUpdated(refreshObserver);
196
+ onBeforeUnmount(() => {
197
+ observer?.disconnect();
198
+ if (updateFrame !== null) window.cancelAnimationFrame(updateFrame);
199
+ });
200
+ }
201
+ function viewportStyle(plan) {
202
+ const fixedChromeIds = new Set((plan?.chrome ?? []).flatMap((entry) => {
203
+ return entry.layout.placement.position === "fixed" ? [entry.node.id] : [];
204
+ }));
205
+ const contributors = (plan?.insets.contributors ?? []).filter((contributor) => fixedChromeIds.has(contributor.sourceNodeId));
206
+ const sizedTotals = {
207
+ "block-start": [],
208
+ "block-end": [],
209
+ "inline-start": [],
210
+ "inline-end": []
211
+ };
212
+ const measuredFallbackTotals = {
213
+ "block-start": [],
214
+ "block-end": [],
215
+ "inline-start": [],
216
+ "inline-end": []
217
+ };
218
+ for (const contributor of contributors) if (contributor.reserve.mode === "size") sizedTotals[contributor.edge].push(sizeToCss(contributor.reserve.size));
219
+ else if (contributor.reserve.mode === "measure" && contributor.reserve.size !== void 0) measuredFallbackTotals[contributor.edge].push(sizeToCss(contributor.reserve.size));
220
+ const style = {};
221
+ for (const [edge, values] of Object.entries(sizedTotals)) style[sizeVar(edge)] = values.length === 0 ? "0px" : values.length === 1 ? values[0] : `calc(${values.join(" + ")})`;
222
+ for (const [edge, values] of Object.entries(measuredFallbackTotals)) {
223
+ if (values.length === 0) continue;
224
+ style[measureVar(edge)] = values.length === 1 ? values[0] : `calc(${values.join(" + ")})`;
225
+ }
226
+ return style;
227
+ }
228
+ function useScrollMetrics(scrollerRef) {
229
+ const thumbStyle = ref({ display: "none" });
230
+ let observer = null;
231
+ let observedTarget = null;
232
+ let updateFrame = null;
233
+ function setThumbStyle(next) {
234
+ const prev = thumbStyle.value;
235
+ const keys = /* @__PURE__ */ new Set([...Object.keys(prev), ...Object.keys(next)]);
236
+ for (const key of keys) if (prev[key] !== next[key]) {
237
+ thumbStyle.value = next;
238
+ return;
239
+ }
240
+ }
241
+ function update() {
242
+ const scroller = scrollerRef.value;
243
+ if (!scroller) {
244
+ setThumbStyle({ display: "none" });
245
+ return;
246
+ }
247
+ const { clientHeight, scrollHeight, scrollTop } = scroller;
248
+ if (scrollHeight <= clientHeight + 1) {
249
+ setThumbStyle({ display: "none" });
250
+ return;
251
+ }
252
+ const thumbHeight = Math.max(24, clientHeight * clientHeight / scrollHeight);
253
+ const thumbTop = Math.max(0, clientHeight - thumbHeight) * (scrollTop / Math.max(1, scrollHeight - clientHeight));
254
+ setThumbStyle({
255
+ height: `${thumbHeight}px`,
256
+ transform: `translateY(${thumbTop}px)`
257
+ });
258
+ }
259
+ function scheduleUpdate() {
260
+ if (updateFrame !== null) return;
261
+ updateFrame = window.requestAnimationFrame(() => {
262
+ updateFrame = null;
263
+ update();
264
+ });
265
+ }
266
+ function refreshObserver() {
267
+ const scroller = scrollerRef.value;
268
+ if (!scroller) return;
269
+ if (typeof ResizeObserver === "undefined") {
270
+ scheduleUpdate();
271
+ return;
272
+ }
273
+ observer ??= new ResizeObserver(scheduleUpdate);
274
+ const nextTarget = scroller.firstElementChild instanceof HTMLElement ? scroller.firstElementChild : scroller;
275
+ if (nextTarget !== observedTarget) {
276
+ observer.disconnect();
277
+ observer.observe(nextTarget);
278
+ observedTarget = nextTarget;
279
+ }
280
+ nextTick(scheduleUpdate);
281
+ }
282
+ onMounted(refreshObserver);
283
+ onUpdated(refreshObserver);
284
+ onBeforeUnmount(() => {
285
+ observer?.disconnect();
286
+ if (updateFrame !== null) window.cancelAnimationFrame(updateFrame);
287
+ });
288
+ return {
289
+ thumbStyle,
290
+ update: scheduleUpdate
291
+ };
292
+ }
293
+ function useFrameViewport(options) {
294
+ const viewportRef = ref(null);
295
+ const scrollerRef = ref(null);
296
+ useMeasuredChromeInsets(viewportRef);
297
+ const { thumbStyle, update: updateScrollMetrics } = useScrollMetrics(scrollerRef);
298
+ return () => {
299
+ const current = options();
300
+ const surfaceStyle = normalizeStyleValueMap(current.surfaceStyle);
301
+ const chromeVNodes = current.chromeVNodes ?? [];
302
+ const contentChrome = groupContentChrome(current.plan, chromeVNodes);
303
+ return h("div", {
304
+ "ref": viewportRef,
305
+ "class": "dc-device-frame__viewport",
306
+ "data-dc-overlay-boundary": "",
307
+ "style": viewportStyle(current.plan)
308
+ }, [
309
+ h("div", { class: "dc-device-frame__content" }, [h("div", {
310
+ ref: scrollerRef,
311
+ class: "dc-device-frame__content-scroller",
312
+ onScroll: updateScrollMetrics
313
+ }, [h("div", { class: "dc-device-frame__content-layout" }, [
314
+ ...contentChrome["block-start"],
315
+ h("div", { class: "dc-device-frame__content-row" }, [
316
+ contentChrome["inline-start"].length > 0 ? h("div", { class: "dc-device-frame__content-edge dc-device-frame__content-edge--inline-start" }, contentChrome["inline-start"]) : null,
317
+ h("div", {
318
+ "class": "dc-device-frame__content-surface",
319
+ "data-dc-component": "container-shell",
320
+ "style": surfaceStyle
321
+ }, current.content ?? []),
322
+ contentChrome["inline-end"].length > 0 ? h("div", { class: "dc-device-frame__content-edge dc-device-frame__content-edge--inline-end" }, contentChrome["inline-end"]) : null
323
+ ]),
324
+ ...contentChrome["block-end"],
325
+ h("div", {
326
+ "ref": (element) => {
327
+ current.selectionPresentation?.registerPlane("content", element instanceof HTMLElement ? element : null);
328
+ },
329
+ "class": "dc-device-frame__selection-plane dc-device-frame__selection-plane--content",
330
+ "data-dc-selection-plane": "content",
331
+ "aria-hidden": "true"
332
+ })
333
+ ])]), h("div", {
334
+ "class": "dc-device-frame__scrollbar",
335
+ "aria-hidden": "true"
336
+ }, [h("div", {
337
+ class: "dc-device-frame__scrollbar-thumb",
338
+ style: thumbStyle.value
339
+ })])]),
340
+ renderChrome(current.plan, chromeVNodes),
341
+ ...renderLayers(current.plan, current.layerVNodes ?? {}),
342
+ h("div", {
343
+ "ref": (element) => {
344
+ current.selectionPresentation?.registerPlane("viewport", element instanceof HTMLElement ? element : null);
345
+ },
346
+ "class": "dc-device-frame__selection-plane dc-device-frame__selection-plane--viewport",
347
+ "data-dc-selection-plane": "viewport",
348
+ "aria-hidden": "true"
349
+ })
350
+ ]);
351
+ };
352
+ }
353
+ //#endregion
354
+ //#region src/components/frames/AndroidFrame.ts
355
+ /**
356
+ * Android phone frame with status bar and bottom navigation bar.
357
+ * Renders children via the default slot inside the content area.
358
+ */
359
+ var AndroidFrame_default = defineComponent({
360
+ name: "DcAndroidFrame",
361
+ props: {
362
+ layoutPlan: {
363
+ type: Object,
364
+ default: void 0
365
+ },
366
+ chromeVNodes: {
367
+ type: Array,
368
+ default: () => []
369
+ },
370
+ layerVNodes: {
371
+ type: Object,
372
+ default: () => ({})
373
+ },
374
+ forbiddenOverlayVNode: {
375
+ type: Object,
376
+ default: null
377
+ },
378
+ surfaceStyle: {
379
+ type: Object,
380
+ default: void 0
381
+ },
382
+ selectionPresentation: {
383
+ type: Object,
384
+ default: void 0
385
+ }
386
+ },
387
+ setup(props, { slots }) {
388
+ const renderViewport = useFrameViewport(() => ({
389
+ content: slots.default?.() ?? [],
390
+ chromeVNodes: props.chromeVNodes,
391
+ layerVNodes: props.layerVNodes,
392
+ plan: props.layoutPlan,
393
+ surfaceStyle: props.surfaceStyle,
394
+ selectionPresentation: props.selectionPresentation
395
+ }));
396
+ return () => renderDeviceFrame("dc-device-frame--android", props.selectionPresentation, [
397
+ h("div", { class: "dc-device-frame__status-bar" }, [h("span", { class: "dc-device-frame__status-time" }, "12:00"), h("span", { class: "dc-device-frame__status-icons" }, [h("span", null, h(IconSignal, { size: 10 })), h("span", null, h(IconSignalBar, { size: 10 }))])]),
398
+ renderViewport(),
399
+ h("div", { class: "dc-device-frame__nav-bar" }, [
400
+ h("span", { class: "dc-device-frame__nav-btn" }, h(IconNavBack, { size: 12 })),
401
+ h("span", { class: "dc-device-frame__nav-btn dc-device-frame__nav-btn--home" }, h(IconNavHome, { size: 12 })),
402
+ h("span", { class: "dc-device-frame__nav-btn" }, h(IconNavRecent, { size: 12 }))
403
+ ])
404
+ ], props.forbiddenOverlayVNode);
405
+ }
406
+ });
407
+ //#endregion
408
+ //#region src/components/frames/DesktopFrame.ts
409
+ /**
410
+ * Desktop browser chrome frame with title bar, traffic lights, and URL bar.
411
+ * Renders children via the default slot inside the content area.
412
+ */
413
+ var DesktopFrame_default = defineComponent({
414
+ name: "DcDesktopFrame",
415
+ props: {
416
+ layoutPlan: {
417
+ type: Object,
418
+ default: void 0
419
+ },
420
+ chromeVNodes: {
421
+ type: Array,
422
+ default: () => []
423
+ },
424
+ layerVNodes: {
425
+ type: Object,
426
+ default: () => ({})
427
+ },
428
+ forbiddenOverlayVNode: {
429
+ type: Object,
430
+ default: null
431
+ },
432
+ surfaceStyle: {
433
+ type: Object,
434
+ default: void 0
435
+ },
436
+ selectionPresentation: {
437
+ type: Object,
438
+ default: void 0
439
+ }
440
+ },
441
+ setup(props, { slots }) {
442
+ const renderViewport = useFrameViewport(() => ({
443
+ content: slots.default?.() ?? [],
444
+ chromeVNodes: props.chromeVNodes,
445
+ layerVNodes: props.layerVNodes,
446
+ plan: props.layoutPlan,
447
+ surfaceStyle: props.surfaceStyle,
448
+ selectionPresentation: props.selectionPresentation
449
+ }));
450
+ return () => renderDeviceFrame("dc-device-frame--desktop", props.selectionPresentation, [h("div", { class: "dc-device-frame__title-bar" }, [h("div", { class: "dc-device-frame__traffic-lights" }, [
451
+ h("span", { class: "dc-device-frame__traffic-dot dc-device-frame__traffic-dot--close" }),
452
+ h("span", { class: "dc-device-frame__traffic-dot dc-device-frame__traffic-dot--minimize" }),
453
+ h("span", { class: "dc-device-frame__traffic-dot dc-device-frame__traffic-dot--maximize" })
454
+ ]), h("div", { class: "dc-device-frame__url-bar" }, "localhost:5173")]), renderViewport()], props.forbiddenOverlayVNode);
455
+ }
456
+ });
457
+ //#endregion
458
+ //#region src/components/frames/IPhoneFrame.ts
459
+ /**
460
+ * iPhone frame with Dynamic Island notch, status bar, and home indicator.
461
+ * Status bar uses a 3-column grid layout: time (left) | Dynamic Island (center) | icons (right),
462
+ * matching the real iPhone status bar layout.
463
+ * Renders children via the default slot inside the content area.
464
+ */
465
+ var IPhoneFrame_default = defineComponent({
466
+ name: "DcIPhoneFrame",
467
+ props: {
468
+ layoutPlan: {
469
+ type: Object,
470
+ default: void 0
471
+ },
472
+ chromeVNodes: {
473
+ type: Array,
474
+ default: () => []
475
+ },
476
+ layerVNodes: {
477
+ type: Object,
478
+ default: () => ({})
479
+ },
480
+ forbiddenOverlayVNode: {
481
+ type: Object,
482
+ default: null
483
+ },
484
+ surfaceStyle: {
485
+ type: Object,
486
+ default: void 0
487
+ },
488
+ selectionPresentation: {
489
+ type: Object,
490
+ default: void 0
491
+ }
492
+ },
493
+ setup(props, { slots }) {
494
+ const renderViewport = useFrameViewport(() => ({
495
+ content: slots.default?.() ?? [],
496
+ chromeVNodes: props.chromeVNodes,
497
+ layerVNodes: props.layerVNodes,
498
+ plan: props.layoutPlan,
499
+ surfaceStyle: props.surfaceStyle,
500
+ selectionPresentation: props.selectionPresentation
501
+ }));
502
+ return () => renderDeviceFrame("dc-device-frame--iphone", props.selectionPresentation, [
503
+ h("div", { class: "dc-device-frame__status-bar" }, [
504
+ h("span", { class: "dc-device-frame__status-time" }, "9:41"),
505
+ h("div", { class: "dc-device-frame__notch dc-device-frame__notch--dynamic-island" }),
506
+ h("span", { class: "dc-device-frame__status-icons" }, [
507
+ h("span", { class: "dc-device-frame__cellular" }, [
508
+ h("span", { class: "dc-device-frame__cellular-bar" }),
509
+ h("span", { class: "dc-device-frame__cellular-bar" }),
510
+ h("span", { class: "dc-device-frame__cellular-bar" }),
511
+ h("span", { class: "dc-device-frame__cellular-bar" })
512
+ ]),
513
+ h("svg", {
514
+ class: "dc-device-frame__wifi",
515
+ width: "15",
516
+ height: "12",
517
+ viewBox: "0 0 15 12",
518
+ fill: "currentColor"
519
+ }, [
520
+ h("circle", {
521
+ cx: "7.5",
522
+ cy: "10.5",
523
+ r: "1.5"
524
+ }),
525
+ h("path", {
526
+ "d": "M5.1 7.6a3.5 3.5 0 014.8 0",
527
+ "stroke": "currentColor",
528
+ "stroke-width": "1.6",
529
+ "stroke-linecap": "round",
530
+ "fill": "none"
531
+ }),
532
+ h("path", {
533
+ "d": "M2.3 4.8a7.2 7.2 0 0110.4 0",
534
+ "stroke": "currentColor",
535
+ "stroke-width": "1.6",
536
+ "stroke-linecap": "round",
537
+ "fill": "none"
538
+ })
539
+ ]),
540
+ h("span", { class: "dc-device-frame__battery" }, [h("span", { class: "dc-device-frame__battery-level" })])
541
+ ])
542
+ ]),
543
+ renderViewport(),
544
+ h("div", { class: "dc-device-frame__home-indicator" })
545
+ ], props.forbiddenOverlayVNode);
546
+ }
547
+ });
548
+ //#endregion
549
+ //#region src/components/frames/TabletFrame.ts
550
+ /**
551
+ * Tablet / iPad frame with minimal chrome and thin bezels.
552
+ * Renders children via the default slot inside the content area.
553
+ */
554
+ var TabletFrame_default = defineComponent({
555
+ name: "DcTabletFrame",
556
+ props: {
557
+ layoutPlan: {
558
+ type: Object,
559
+ default: void 0
560
+ },
561
+ chromeVNodes: {
562
+ type: Array,
563
+ default: () => []
564
+ },
565
+ layerVNodes: {
566
+ type: Object,
567
+ default: () => ({})
568
+ },
569
+ forbiddenOverlayVNode: {
570
+ type: Object,
571
+ default: null
572
+ },
573
+ surfaceStyle: {
574
+ type: Object,
575
+ default: void 0
576
+ },
577
+ selectionPresentation: {
578
+ type: Object,
579
+ default: void 0
580
+ }
581
+ },
582
+ setup(props, { slots }) {
583
+ const renderViewport = useFrameViewport(() => ({
584
+ content: slots.default?.() ?? [],
585
+ chromeVNodes: props.chromeVNodes,
586
+ layerVNodes: props.layerVNodes,
587
+ plan: props.layoutPlan,
588
+ surfaceStyle: props.surfaceStyle,
589
+ selectionPresentation: props.selectionPresentation
590
+ }));
591
+ return () => renderDeviceFrame("dc-device-frame--tablet", props.selectionPresentation, [h("div", { class: "dc-device-frame__status-bar" }, [h("span", { class: "dc-device-frame__status-time" }, "9:41"), h("span", { class: "dc-device-frame__status-icons" }, [h("span", null, h(IconSignal, { size: 10 })), h("span", null, h(IconSignalBar, { size: 10 }))])]), renderViewport()], props.forbiddenOverlayVNode);
592
+ }
593
+ });
594
+ //#endregion
595
+ //#region src/presets.ts
596
+ const IPHONE_PRESET = {
597
+ type: "iphone",
598
+ label: "iPhone",
599
+ labelKey: "device.iphone",
600
+ icon: IconPhone,
601
+ width: 375,
602
+ height: 812,
603
+ frameComponent: IPhoneFrame_default
604
+ };
605
+ const ANDROID_PRESET = {
606
+ type: "android",
607
+ label: "Android",
608
+ labelKey: "device.android",
609
+ icon: IconRobot,
610
+ width: 360,
611
+ height: 800,
612
+ frameComponent: AndroidFrame_default
613
+ };
614
+ const TABLET_PRESET = {
615
+ type: "tablet",
616
+ label: "Tablet",
617
+ labelKey: "device.tablet",
618
+ icon: IconLaptop,
619
+ width: 768,
620
+ height: 1024,
621
+ frameComponent: TabletFrame_default
622
+ };
623
+ const DESKTOP_PRESET = {
624
+ type: "desktop",
625
+ label: "Desktop",
626
+ labelKey: "device.desktop",
627
+ icon: IconDesktop,
628
+ width: 1280,
629
+ height: 800,
630
+ frameComponent: DesktopFrame_default
631
+ };
632
+ /**
633
+ * Returns the built-in device presets: iPhone, Android, Tablet, Desktop.
634
+ */
635
+ function getDefaultPresets() {
636
+ return [
637
+ IPHONE_PRESET,
638
+ ANDROID_PRESET,
639
+ TABLET_PRESET,
640
+ DESKTOP_PRESET
641
+ ];
642
+ }
643
+ //#endregion
644
+ //#region src/types.ts
645
+ /**
646
+ * Injection key for the device frame context.
647
+ */
648
+ const DEVICE_FRAME_CONTEXT_KEY = Symbol("dc-device-frame");
649
+ //#endregion
650
+ //#region src/components/DeviceFrameShell.ts
651
+ /**
652
+ * Stable container shell component for the renderer's containerShell extension.
653
+ *
654
+ * Reads the current device from DeviceFrameContext (provide/inject) and
655
+ * renders the corresponding frame component. Because this is a single
656
+ * stable component reference, RootRenderer's computed() never swaps
657
+ * components — only the internal render output changes reactively.
658
+ *
659
+ * Falls back to the iPhone frame if no context is provided.
660
+ */
661
+ const DeviceFrameShell = defineComponent({
662
+ name: "DcDeviceFrameShell",
663
+ props: {
664
+ layoutPlan: {
665
+ type: Object,
666
+ default: void 0
667
+ },
668
+ regionVNodes: {
669
+ type: Object,
670
+ default: () => ({})
671
+ },
672
+ chromeVNodes: {
673
+ type: Array,
674
+ default: () => []
675
+ },
676
+ layerVNodes: {
677
+ type: Object,
678
+ default: () => ({})
679
+ },
680
+ forbiddenOverlayVNode: {
681
+ type: Object,
682
+ default: null
683
+ },
684
+ surfaceStyle: {
685
+ type: Object,
686
+ default: void 0
687
+ },
688
+ selectionPresentation: {
689
+ type: Object,
690
+ default: void 0
691
+ }
692
+ },
693
+ setup(props, { slots }) {
694
+ const ctx = inject(DEVICE_FRAME_CONTEXT_KEY, null);
695
+ const fallbackFrame = getDefaultPresets()[0].frameComponent;
696
+ const activeFrame = computed(() => {
697
+ if (!ctx) return fallbackFrame;
698
+ return ctx.getPreset(ctx.currentDevice.value)?.frameComponent ?? fallbackFrame;
699
+ });
700
+ return () => {
701
+ const additionalRegions = Object.entries(props.regionVNodes).filter(([region]) => region !== DEFAULT_LAYOUT_REGION).map(([region, nodes]) => h("div", {
702
+ "key": region,
703
+ "class": "dc-device-frame__content-region",
704
+ "data-dc-layout-region": region
705
+ }, nodes));
706
+ return h(activeFrame.value, {
707
+ layoutPlan: props.layoutPlan,
708
+ chromeVNodes: props.chromeVNodes,
709
+ layerVNodes: props.layerVNodes,
710
+ forbiddenOverlayVNode: props.forbiddenOverlayVNode,
711
+ surfaceStyle: props.surfaceStyle,
712
+ selectionPresentation: props.selectionPresentation
713
+ }, { default: () => [...slots.default?.() ?? [], ...additionalRegions] });
714
+ };
715
+ }
716
+ });
717
+ const DeviceFrameShellWithOverlay = DeviceFrameShell;
718
+ DeviceFrameShellWithOverlay.__dcHandlesForbiddenOverlay = true;
719
+ //#endregion
720
+ //#region src/components/DevicePicker.ts
721
+ var DevicePicker_default = defineComponent({
722
+ name: "DcDevicePicker",
723
+ props: {
724
+ context: {
725
+ type: Object,
726
+ required: true
727
+ },
728
+ translate: {
729
+ type: Function,
730
+ default: void 0
731
+ }
732
+ },
733
+ setup(props) {
734
+ return () => h("div", {
735
+ "class": "dc-device-picker",
736
+ "role": "group",
737
+ "aria-label": props.translate?.("device.group", "预览设备") ?? "Preview device"
738
+ }, props.context.presets.map((preset) => {
739
+ const label = preset.labelKey && props.translate ? props.translate(preset.labelKey, preset.label) : preset.label;
740
+ return h("button", {
741
+ "type": "button",
742
+ "class": {
743
+ "dc-device-picker__btn": true,
744
+ "dc-device-picker__btn--active": props.context.currentDevice.value === preset.type
745
+ },
746
+ "aria-pressed": props.context.currentDevice.value === preset.type,
747
+ "aria-label": label,
748
+ "title": label,
749
+ "onClick": () => props.context.setDevice(preset.type)
750
+ }, [typeof preset.icon === "string" ? preset.icon : h(preset.icon, { size: 15 }), h("span", { class: "dc-device-picker__label" }, label)]);
751
+ }));
752
+ }
753
+ });
754
+ //#endregion
755
+ //#region src/context.ts
756
+ /**
757
+ * Creates a DeviceFrameContext. The caller is responsible for calling
758
+ * `provide(DEVICE_FRAME_CONTEXT_KEY, ctx)` in their setup().
759
+ *
760
+ * @example
761
+ * ```ts
762
+ * const ctx = createDeviceFrameContext({ initialDevice: 'iphone' })
763
+ * provide(DEVICE_FRAME_CONTEXT_KEY, ctx)
764
+ * ```
765
+ */
766
+ function createDeviceFrameContext(options = {}) {
767
+ const presets = options.presets ?? getDefaultPresets();
768
+ const currentDevice = ref(options.initialDevice ?? "iphone");
769
+ const presetMap = new Map(presets.map((p) => [p.type, p]));
770
+ return {
771
+ currentDevice,
772
+ presets,
773
+ getPreset: (type) => presetMap.get(type),
774
+ setDevice: (type) => {
775
+ if (presetMap.has(type)) currentDevice.value = type;
776
+ }
777
+ };
778
+ }
779
+ /**
780
+ * Injects the DeviceFrameContext from the nearest ancestor provider.
781
+ * Throws if not found.
782
+ */
783
+ function useDeviceFrameContext() {
784
+ const ctx = inject(DEVICE_FRAME_CONTEXT_KEY);
785
+ if (!ctx) throw new Error("[dragcraft/device-frames] DeviceFrameContext not found. Ensure an ancestor calls provide(DEVICE_FRAME_CONTEXT_KEY, ctx).");
786
+ return ctx;
787
+ }
788
+ //#endregion
789
+ //#region src/composables/useDeviceFrame.ts
790
+ /**
791
+ * Convenience composable for consuming device frame state in any descendant.
792
+ * Returns the full context plus commonly needed computed values.
793
+ *
794
+ * @example
795
+ * ```ts
796
+ * const { currentDevice, setDevice, presets } = useDeviceFrame()
797
+ * ```
798
+ */
799
+ function useDeviceFrame() {
800
+ const ctx = useDeviceFrameContext();
801
+ const currentPreset = computed(() => ctx.getPreset(ctx.currentDevice.value));
802
+ const viewportWidth = computed(() => currentPreset.value?.width ?? 375);
803
+ const viewportHeight = computed(() => currentPreset.value?.height ?? 812);
804
+ return {
805
+ /** Reactive current device type */
806
+ currentDevice: ctx.currentDevice,
807
+ /** Reactive current preset object */
808
+ currentPreset,
809
+ /** Reactive viewport width */
810
+ viewportWidth,
811
+ /** Reactive viewport height */
812
+ viewportHeight,
813
+ /** All available presets */
814
+ presets: ctx.presets,
815
+ /** Switch device */
816
+ setDevice: ctx.setDevice
817
+ };
818
+ }
819
+ //#endregion
820
+ export { ANDROID_PRESET, AndroidFrame_default as AndroidFrame, DESKTOP_PRESET, DEVICE_FRAME_CONTEXT_KEY, DesktopFrame_default as DesktopFrame, DeviceFrameShell, DevicePicker_default as DevicePicker, IPHONE_PRESET, IPhoneFrame_default as IPhoneFrame, TABLET_PRESET, TabletFrame_default as TabletFrame, createDeviceFrameContext, getDefaultPresets, useDeviceFrame, useDeviceFrameContext };