@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/LICENSE +21 -0
- package/dist/index.d.mts +518 -0
- package/dist/index.mjs +820 -0
- package/dist/styles/index.css +591 -0
- package/package.json +51 -0
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 };
|