@@ -184,18 +193,23 @@ export function DevicePreview() {
- {isMac && frameVisible ? (
+ {isMac && !isMacBook && frameVisible ? (
) : null}
- {isMac && frameVisible ? (
+ {isMac && !isMacBook && frameVisible ? (
) : null}
+ {isMacBook && frameVisible ? (
+
+
+
+ ) : null}
{!isMac && frameVisible ? (
<>
diff --git a/src/data/devices/apple/profiles.ts b/src/data/devices/apple/profiles.ts
index fc907a0..769f7ce 100644
--- a/src/data/devices/apple/profiles.ts
+++ b/src/data/devices/apple/profiles.ts
@@ -1,10 +1,13 @@
import type { DeviceProfile, Rect } from "../../../types/device";
-const VERIFIED = "2026-07-13";
-const PHONE_SOURCE = "https://support.apple.com/specs/iphone";
-const IPAD_SOURCE = "https://support.apple.com/specs/ipad";
-const MAC_SOURCE = "https://support.apple.com/specs/mac";
+const VERIFIED = "2026-07-16";
+const PHONE_SOURCE = "https://support.apple.com/en-hk/108044";
+const IPAD_SOURCE = "https://support.apple.com/en-hk/108043";
+const MACBOOK_AIR_SOURCE = "https://support.apple.com/en-hk/102869";
+const MACBOOK_PRO_SOURCE = "https://support.apple.com/en-hk/108052";
+const IMAC_SOURCE = "https://support.apple.com/en-hk/108054";
const DISPLAY_SOURCE = "https://support.apple.com/displays";
+const MACBOOK_NEO_SOURCE = "https://support.apple.com/en-hk/126322";
function rect(x: number, y: number, width: number, height: number): Rect {
return { x, y, width, height };
@@ -13,7 +16,6 @@ function rect(x: number, y: number, width: number, height: number): Rect {
type PhoneInput = {
id: string;
model: string;
- slug: string;
year: number;
width: number;
height: number;
@@ -27,6 +29,8 @@ type PhoneInput = {
cutoutBounds?: Rect;
safeTop: number;
safeBottom: number;
+ dpr?: number;
+ sourceUrl?: string;
};
function phone(input: PhoneInput): DeviceProfile {
@@ -38,7 +42,7 @@ function phone(input: PhoneInput): DeviceProfile {
category: "iphone",
family: "iPhone",
model: input.model,
- slug: input.slug,
+ slug: input.id,
releaseYear: input.year,
supportedOrientations: ["portrait", "landscape"],
chassis: {
@@ -52,7 +56,7 @@ function phone(input: PhoneInput): DeviceProfile {
physicalHeightPx: input.height,
logicalWidthPt: input.logicalWidth,
logicalHeightPt: input.logicalHeight,
- devicePixelRatio: input.width / input.logicalWidth,
+ devicePixelRatio: input.dpr ?? input.width / input.logicalWidth,
aspectRatio: input.width / input.height,
orientation: "portrait",
cornerRadiusPx: input.corner,
@@ -87,7 +91,7 @@ function phone(input: PhoneInput): DeviceProfile {
notes: ["System wallpaper placement can vary by iOS version and user settings."],
},
metadata: {
- sourceUrls: [PHONE_SOURCE],
+ sourceUrls: [input.sourceUrl ?? PHONE_SOURCE],
lastVerified: VERIFIED,
accuracy: "high",
notes: [
@@ -101,7 +105,6 @@ const iphones: DeviceProfile[] = [
phone({
id: "iphone-se-2",
model: "iPhone SE (2nd gen)",
- slug: "iphone-se-2",
year: 2020,
width: 750,
height: 1334,
@@ -118,7 +121,6 @@ const iphones: DeviceProfile[] = [
phone({
id: "iphone-12-mini",
model: "iPhone 12 mini",
- slug: "iphone-12-mini",
year: 2020,
width: 1080,
height: 2340,
@@ -136,7 +138,6 @@ const iphones: DeviceProfile[] = [
phone({
id: "iphone-12",
model: "iPhone 12",
- slug: "iphone-12",
year: 2020,
width: 1170,
height: 2532,
@@ -154,7 +155,6 @@ const iphones: DeviceProfile[] = [
phone({
id: "iphone-12-pro-max",
model: "iPhone 12 Pro Max",
- slug: "iphone-12-pro-max",
year: 2020,
width: 1284,
height: 2778,
@@ -172,7 +172,6 @@ const iphones: DeviceProfile[] = [
phone({
id: "iphone-13-mini",
model: "iPhone 13 mini",
- slug: "iphone-13-mini",
year: 2021,
width: 1080,
height: 2340,
@@ -186,11 +185,11 @@ const iphones: DeviceProfile[] = [
cutoutBounds: rect(350, 0, 380, 102),
safeTop: 150,
safeBottom: 102,
+ dpr: 3,
}),
phone({
id: "iphone-13",
model: "iPhone 13",
- slug: "iphone-13",
year: 2021,
width: 1170,
height: 2532,
@@ -205,10 +204,94 @@ const iphones: DeviceProfile[] = [
safeTop: 141,
safeBottom: 102,
}),
+ phone({
+ id: "iphone-13-pro",
+ model: "iPhone 13 Pro",
+ year: 2021,
+ width: 1170,
+ height: 2532,
+ logicalWidth: 390,
+ logicalHeight: 844,
+ diagonal: 6.1,
+ bodyWidth: 71.5,
+ bodyHeight: 146.7,
+ corner: 132,
+ cutout: "notch",
+ cutoutBounds: rect(380, 0, 410, 108),
+ safeTop: 141,
+ safeBottom: 102,
+ }),
+ phone({
+ id: "iphone-13-pro-max",
+ model: "iPhone 13 Pro Max",
+ year: 2021,
+ width: 1284,
+ height: 2778,
+ logicalWidth: 428,
+ logicalHeight: 926,
+ diagonal: 6.7,
+ bodyWidth: 78.1,
+ bodyHeight: 160.8,
+ corner: 144,
+ cutout: "notch",
+ cutoutBounds: rect(417, 0, 450, 116),
+ safeTop: 141,
+ safeBottom: 102,
+ }),
+ phone({
+ id: "iphone-se-3",
+ model: "iPhone SE (3rd gen)",
+ year: 2022,
+ width: 750,
+ height: 1334,
+ logicalWidth: 375,
+ logicalHeight: 667,
+ diagonal: 4.7,
+ bodyWidth: 67.3,
+ bodyHeight: 138.4,
+ corner: 0,
+ cutout: "none",
+ safeTop: 40,
+ safeBottom: 0,
+ sourceUrl: "https://support.apple.com/en-hk/111866",
+ }),
+ phone({
+ id: "iphone-14",
+ model: "iPhone 14",
+ year: 2022,
+ width: 1170,
+ height: 2532,
+ logicalWidth: 390,
+ logicalHeight: 844,
+ diagonal: 6.1,
+ bodyWidth: 71.5,
+ bodyHeight: 146.7,
+ corner: 132,
+ cutout: "notch",
+ cutoutBounds: rect(380, 0, 410, 108),
+ safeTop: 141,
+ safeBottom: 102,
+ }),
+ phone({
+ id: "iphone-14-plus",
+ model: "iPhone 14 Plus",
+ year: 2022,
+ width: 1284,
+ height: 2778,
+ logicalWidth: 428,
+ logicalHeight: 926,
+ diagonal: 6.7,
+ bodyWidth: 78.1,
+ bodyHeight: 160.8,
+ corner: 144,
+ cutout: "notch",
+ cutoutBounds: rect(417, 0, 450, 116),
+ safeTop: 141,
+ safeBottom: 102,
+ }),
phone({
id: "iphone-14-pro",
model: "iPhone 14 Pro",
- slug: "iphone-14-pro",
year: 2022,
width: 1179,
height: 2556,
@@ -226,7 +309,6 @@ const iphones: DeviceProfile[] = [
phone({
id: "iphone-14-pro-max",
model: "iPhone 14 Pro Max",
- slug: "iphone-14-pro-max",
year: 2022,
width: 1290,
height: 2796,
@@ -244,7 +326,6 @@ const iphones: DeviceProfile[] = [
phone({
id: "iphone-15",
model: "iPhone 15",
- slug: "iphone-15",
year: 2023,
width: 1179,
height: 2556,
@@ -262,7 +343,6 @@ const iphones: DeviceProfile[] = [
phone({
id: "iphone-15-pro",
model: "iPhone 15 Pro",
- slug: "iphone-15-pro",
year: 2023,
width: 1179,
height: 2556,
@@ -280,7 +360,6 @@ const iphones: DeviceProfile[] = [
phone({
id: "iphone-15-plus",
model: "iPhone 15 Plus",
- slug: "iphone-15-plus",
year: 2023,
width: 1290,
height: 2796,
@@ -298,7 +377,6 @@ const iphones: DeviceProfile[] = [
phone({
id: "iphone-15-pro-max",
model: "iPhone 15 Pro Max",
- slug: "iphone-15-pro-max",
year: 2023,
width: 1290,
height: 2796,
@@ -313,11 +391,188 @@ const iphones: DeviceProfile[] = [
safeTop: 177,
safeBottom: 102,
}),
+ phone({
+ id: "iphone-16",
+ model: "iPhone 16",
+ year: 2024,
+ width: 1179,
+ height: 2556,
+ logicalWidth: 393,
+ logicalHeight: 852,
+ diagonal: 6.1,
+ bodyWidth: 71.6,
+ bodyHeight: 147.6,
+ corner: 138,
+ cutout: "dynamic-island",
+ cutoutBounds: rect(466, 33, 247, 90),
+ safeTop: 177,
+ safeBottom: 102,
+ }),
+ phone({
+ id: "iphone-16-plus",
+ model: "iPhone 16 Plus",
+ year: 2024,
+ width: 1290,
+ height: 2796,
+ logicalWidth: 430,
+ logicalHeight: 932,
+ diagonal: 6.7,
+ bodyWidth: 77.8,
+ bodyHeight: 160.9,
+ corner: 150,
+ cutout: "dynamic-island",
+ cutoutBounds: rect(521, 36, 248, 90),
+ safeTop: 177,
+ safeBottom: 102,
+ }),
+ phone({
+ id: "iphone-16-pro",
+ model: "iPhone 16 Pro",
+ year: 2024,
+ width: 1206,
+ height: 2622,
+ logicalWidth: 402,
+ logicalHeight: 874,
+ diagonal: 6.3,
+ bodyWidth: 71.5,
+ bodyHeight: 149.6,
+ corner: 140,
+ cutout: "dynamic-island",
+ cutoutBounds: rect(480, 33, 246, 90),
+ safeTop: 177,
+ safeBottom: 102,
+ sourceUrl: "https://support.apple.com/en-hk/121031",
+ }),
+ phone({
+ id: "iphone-16-pro-max",
+ model: "iPhone 16 Pro Max",
+ year: 2024,
+ width: 1320,
+ height: 2868,
+ logicalWidth: 440,
+ logicalHeight: 956,
+ diagonal: 6.9,
+ bodyWidth: 77.6,
+ bodyHeight: 163,
+ corner: 154,
+ cutout: "dynamic-island",
+ cutoutBounds: rect(536, 36, 248, 90),
+ safeTop: 177,
+ safeBottom: 102,
+ sourceUrl: "https://support.apple.com/en-hk/121032",
+ }),
+ phone({
+ id: "iphone-16e",
+ model: "iPhone 16e",
+ year: 2025,
+ width: 1170,
+ height: 2532,
+ logicalWidth: 390,
+ logicalHeight: 844,
+ diagonal: 6.1,
+ bodyWidth: 71.5,
+ bodyHeight: 146.7,
+ corner: 132,
+ cutout: "notch",
+ cutoutBounds: rect(340, 0, 490, 108),
+ safeTop: 141,
+ safeBottom: 102,
+ }),
+ phone({
+ id: "iphone-17",
+ model: "iPhone 17",
+ year: 2025,
+ width: 1206,
+ height: 2622,
+ logicalWidth: 402,
+ logicalHeight: 874,
+ diagonal: 6.3,
+ bodyWidth: 71.5,
+ bodyHeight: 149.6,
+ corner: 140,
+ cutout: "dynamic-island",
+ cutoutBounds: rect(480, 33, 246, 90),
+ safeTop: 177,
+ safeBottom: 102,
+ sourceUrl: "https://support.apple.com/en-hk/125089",
+ }),
+ phone({
+ id: "iphone-air",
+ model: "iPhone Air",
+ year: 2025,
+ width: 1260,
+ height: 2736,
+ logicalWidth: 420,
+ logicalHeight: 912,
+ diagonal: 6.5,
+ bodyWidth: 74.7,
+ bodyHeight: 156.2,
+ corner: 146,
+ cutout: "dynamic-island",
+ cutoutBounds: rect(506, 34, 248, 90),
+ safeTop: 177,
+ safeBottom: 102,
+ sourceUrl: "https://support.apple.com/en-hk/125092",
+ }),
+ phone({
+ id: "iphone-17-pro",
+ model: "iPhone 17 Pro",
+ year: 2025,
+ width: 1206,
+ height: 2622,
+ logicalWidth: 402,
+ logicalHeight: 874,
+ diagonal: 6.3,
+ bodyWidth: 71.9,
+ bodyHeight: 150,
+ corner: 140,
+ cutout: "dynamic-island",
+ cutoutBounds: rect(480, 33, 246, 90),
+ safeTop: 177,
+ safeBottom: 102,
+ sourceUrl: "https://support.apple.com/en-hk/125090",
+ }),
+ phone({
+ id: "iphone-17-pro-max",
+ model: "iPhone 17 Pro Max",
+ year: 2025,
+ width: 1320,
+ height: 2868,
+ logicalWidth: 440,
+ logicalHeight: 956,
+ diagonal: 6.9,
+ bodyWidth: 78,
+ bodyHeight: 163.4,
+ corner: 154,
+ cutout: "dynamic-island",
+ cutoutBounds: rect(536, 36, 248, 90),
+ safeTop: 177,
+ safeBottom: 102,
+ sourceUrl: "https://support.apple.com/en-hk/125091",
+ }),
+ phone({
+ id: "iphone-17e",
+ model: "iPhone 17e",
+ year: 2026,
+ width: 1170,
+ height: 2532,
+ logicalWidth: 390,
+ logicalHeight: 844,
+ diagonal: 6.1,
+ bodyWidth: 71.5,
+ bodyHeight: 146.7,
+ corner: 132,
+ cutout: "notch",
+ cutoutBounds: rect(340, 0, 490, 108),
+ safeTop: 141,
+ safeBottom: 102,
+ sourceUrl: "https://support.apple.com/en-hk/126470",
+ }),
];
type TabletInput = {
+ id: string;
model: string;
- slug: string;
year: number;
width: number;
height: number;
@@ -327,24 +582,27 @@ type TabletInput = {
diagonal: number;
bodyWidth: number;
bodyHeight: number;
+ corner?: number;
+ classic?: boolean;
+ sourceUrl?: string;
};
function tablet(input: TabletInput): DeviceProfile {
return {
- id: `apple-${input.slug}`,
+ id: `apple-${input.id}`,
brand: "Apple",
platform: "iPadOS",
category: "ipad",
family: "iPad",
model: input.model,
- slug: input.slug,
+ slug: input.id,
releaseYear: input.year,
supportedOrientations: ["portrait", "landscape"],
chassis: {
physicalWidthMm: input.bodyWidth,
physicalHeightMm: input.bodyHeight,
screenDiagonalInches: input.diagonal,
- bezelStyle: "edge-to-edge",
+ bezelStyle: input.classic ? "classic" : "edge-to-edge",
},
display: {
physicalWidthPx: input.width,
@@ -354,13 +612,15 @@ function tablet(input: TabletInput): DeviceProfile {
devicePixelRatio: input.dpr,
aspectRatio: input.width / input.height,
orientation: "portrait",
- cornerRadiusPx: 52,
+ cornerRadiusPx: input.corner ?? 52,
},
cutout: { type: "none" },
safeArea: { insetsPx: { top: 72, right: 54, bottom: 60, left: 54 }, source: "derived" },
overlays: {
statusBar: rect(0, 0, input.width, 72),
- homeIndicator: rect(input.width * 0.41, input.height - 34, input.width * 0.18, 12),
+ homeIndicator: input.classic
+ ? undefined
+ : rect(input.width * 0.41, input.height - 34, input.width * 0.18, 12),
},
wallpaperBehavior: {
systemMayZoom: true,
@@ -368,7 +628,7 @@ function tablet(input: TabletInput): DeviceProfile {
notes: ["Portrait and landscape compositions are stored independently in this editor."],
},
metadata: {
- sourceUrls: [IPAD_SOURCE],
+ sourceUrls: [input.sourceUrl ?? IPAD_SOURCE],
lastVerified: VERIFIED,
accuracy: "high",
notes: ["Published resolution with derived safe-area geometry."],
@@ -378,8 +638,23 @@ function tablet(input: TabletInput): DeviceProfile {
const ipads: DeviceProfile[] = [
tablet({
+ id: "ipad-9",
+ model: "iPad (9th gen)",
+ year: 2021,
+ width: 1620,
+ height: 2160,
+ logicalWidth: 810,
+ logicalHeight: 1080,
+ dpr: 2,
+ diagonal: 10.2,
+ bodyWidth: 174.1,
+ bodyHeight: 250.6,
+ corner: 0,
+ classic: true,
+ }),
+ tablet({
+ id: "ipad-mini-6",
model: "iPad mini (6th gen)",
- slug: "ipad-mini-6",
year: 2021,
width: 1488,
height: 2266,
@@ -389,10 +664,37 @@ const ipads: DeviceProfile[] = [
diagonal: 8.3,
bodyWidth: 134.8,
bodyHeight: 195.4,
+ corner: 54,
}),
tablet({
+ id: "ipad-pro-11-3",
+ model: "iPad Pro 11-inch (3rd gen)",
+ year: 2021,
+ width: 1668,
+ height: 2388,
+ logicalWidth: 834,
+ logicalHeight: 1194,
+ dpr: 2,
+ diagonal: 11,
+ bodyWidth: 178.5,
+ bodyHeight: 247.6,
+ }),
+ tablet({
+ id: "ipad-pro-12-9-5",
+ model: "iPad Pro 12.9-inch (5th gen)",
+ year: 2021,
+ width: 2048,
+ height: 2732,
+ logicalWidth: 1024,
+ logicalHeight: 1366,
+ dpr: 2,
+ diagonal: 12.9,
+ bodyWidth: 214.9,
+ bodyHeight: 280.6,
+ }),
+ tablet({
+ id: "ipad-10",
model: "iPad (10th gen)",
- slug: "ipad-10",
year: 2022,
width: 1640,
height: 2360,
@@ -404,8 +706,61 @@ const ipads: DeviceProfile[] = [
bodyHeight: 248.6,
}),
tablet({
+ id: "ipad-air-5",
+ model: "iPad Air (5th gen)",
+ year: 2022,
+ width: 1640,
+ height: 2360,
+ logicalWidth: 820,
+ logicalHeight: 1180,
+ dpr: 2,
+ diagonal: 10.9,
+ bodyWidth: 178.5,
+ bodyHeight: 247.6,
+ }),
+ tablet({
+ id: "ipad-pro-11-4",
+ model: "iPad Pro 11-inch (4th gen)",
+ year: 2022,
+ width: 1668,
+ height: 2388,
+ logicalWidth: 834,
+ logicalHeight: 1194,
+ dpr: 2,
+ diagonal: 11,
+ bodyWidth: 178.5,
+ bodyHeight: 247.6,
+ }),
+ tablet({
+ id: "ipad-pro-12-9-6",
+ model: "iPad Pro 12.9-inch (6th gen)",
+ year: 2022,
+ width: 2048,
+ height: 2732,
+ logicalWidth: 1024,
+ logicalHeight: 1366,
+ dpr: 2,
+ diagonal: 12.9,
+ bodyWidth: 214.9,
+ bodyHeight: 280.6,
+ }),
+ tablet({
+ id: "ipad-mini-a17-pro",
+ model: "iPad mini (A17 Pro)",
+ year: 2024,
+ width: 1488,
+ height: 2266,
+ logicalWidth: 744,
+ logicalHeight: 1133,
+ dpr: 2,
+ diagonal: 8.3,
+ bodyWidth: 134.8,
+ bodyHeight: 195.4,
+ corner: 54,
+ }),
+ tablet({
+ id: "ipad-air-11-m2",
model: "iPad Air 11-inch (M2)",
- slug: "ipad-air-11-m2",
year: 2024,
width: 1640,
height: 2360,
@@ -417,8 +772,21 @@ const ipads: DeviceProfile[] = [
bodyHeight: 247.6,
}),
tablet({
+ id: "ipad-air-13-m2",
+ model: "iPad Air 13-inch (M2)",
+ year: 2024,
+ width: 2048,
+ height: 2732,
+ logicalWidth: 1024,
+ logicalHeight: 1366,
+ dpr: 2,
+ diagonal: 13,
+ bodyWidth: 214.9,
+ bodyHeight: 280.6,
+ }),
+ tablet({
+ id: "ipad-pro-11-m4",
model: "iPad Pro 11-inch (M4)",
- slug: "ipad-pro-11-m4",
year: 2024,
width: 1668,
height: 2420,
@@ -428,10 +796,11 @@ const ipads: DeviceProfile[] = [
diagonal: 11,
bodyWidth: 177.5,
bodyHeight: 249.7,
+ corner: 48,
}),
tablet({
+ id: "ipad-pro-13-m4",
model: "iPad Pro 13-inch (M4)",
- slug: "ipad-pro-13-m4",
year: 2024,
width: 2064,
height: 2752,
@@ -441,12 +810,108 @@ const ipads: DeviceProfile[] = [
diagonal: 13,
bodyWidth: 215.5,
bodyHeight: 281.6,
+ corner: 48,
+ }),
+ tablet({
+ id: "ipad-a16",
+ model: "iPad (A16)",
+ year: 2025,
+ width: 1640,
+ height: 2360,
+ logicalWidth: 820,
+ logicalHeight: 1180,
+ dpr: 2,
+ diagonal: 11,
+ bodyWidth: 179.5,
+ bodyHeight: 248.6,
+ }),
+ tablet({
+ id: "ipad-air-11-m3",
+ model: "iPad Air 11-inch (M3)",
+ year: 2025,
+ width: 1640,
+ height: 2360,
+ logicalWidth: 820,
+ logicalHeight: 1180,
+ dpr: 2,
+ diagonal: 11,
+ bodyWidth: 178.5,
+ bodyHeight: 247.6,
+ }),
+ tablet({
+ id: "ipad-air-13-m3",
+ model: "iPad Air 13-inch (M3)",
+ year: 2025,
+ width: 2048,
+ height: 2732,
+ logicalWidth: 1024,
+ logicalHeight: 1366,
+ dpr: 2,
+ diagonal: 13,
+ bodyWidth: 214.9,
+ bodyHeight: 280.6,
+ }),
+ tablet({
+ id: "ipad-pro-11-m5",
+ model: "iPad Pro 11-inch (M5)",
+ year: 2025,
+ width: 1668,
+ height: 2420,
+ logicalWidth: 834,
+ logicalHeight: 1210,
+ dpr: 2,
+ diagonal: 11,
+ bodyWidth: 177.5,
+ bodyHeight: 249.7,
+ corner: 48,
+ sourceUrl: "https://support.apple.com/en-hk/125406",
+ }),
+ tablet({
+ id: "ipad-pro-13-m5",
+ model: "iPad Pro 13-inch (M5)",
+ year: 2025,
+ width: 2064,
+ height: 2752,
+ logicalWidth: 1032,
+ logicalHeight: 1376,
+ dpr: 2,
+ diagonal: 13,
+ bodyWidth: 215.5,
+ bodyHeight: 281.6,
+ corner: 48,
+ }),
+ tablet({
+ id: "ipad-air-11-m4",
+ model: "iPad Air 11-inch (M4)",
+ year: 2026,
+ width: 1640,
+ height: 2360,
+ logicalWidth: 820,
+ logicalHeight: 1180,
+ dpr: 2,
+ diagonal: 11,
+ bodyWidth: 178.5,
+ bodyHeight: 247.6,
+ sourceUrl: "https://support.apple.com/en-hk/126471",
+ }),
+ tablet({
+ id: "ipad-air-13-m4",
+ model: "iPad Air 13-inch (M4)",
+ year: 2026,
+ width: 2048,
+ height: 2732,
+ logicalWidth: 1024,
+ logicalHeight: 1366,
+ dpr: 2,
+ diagonal: 13,
+ bodyWidth: 214.9,
+ bodyHeight: 280.6,
}),
];
type MacInput = {
+ id: string;
model: string;
- slug: string;
year: number;
category: "macbook" | "imac" | "display";
width: number;
@@ -455,22 +920,22 @@ type MacInput = {
bodyWidth: number;
bodyHeight: number;
notch: boolean;
+ sourceUrl: string;
};
function mac(input: MacInput): DeviceProfile {
- const source = input.category === "display" ? DISPLAY_SOURCE : MAC_SOURCE;
const menuHeight = Math.round(input.height * 0.035);
const dockHeight = Math.round(input.height * 0.09);
const notchWidth = Math.round(input.width * 0.085);
return {
- id: `apple-${input.slug}`,
+ id: `apple-${input.id}`,
brand: "Apple",
platform: "macOS",
category: input.category,
family:
input.category === "display" ? "Display" : input.category === "imac" ? "iMac" : "MacBook",
model: input.model,
- slug: input.slug,
+ slug: input.id,
releaseYear: input.year,
supportedOrientations: ["landscape"],
chassis: {
@@ -516,7 +981,7 @@ function mac(input: MacInput): DeviceProfile {
],
},
metadata: {
- sourceUrls: [source],
+ sourceUrls: [input.sourceUrl],
lastVerified: VERIFIED,
accuracy: "high",
notes: [
@@ -528,8 +993,177 @@ function mac(input: MacInput): DeviceProfile {
const macs: DeviceProfile[] = [
mac({
+ id: "macbook-pro-14-m1-pro-max",
+ model: "MacBook Pro 14-inch (M1 Pro/Max)",
+ year: 2021,
+ category: "macbook",
+ width: 3024,
+ height: 1964,
+ diagonal: 14.2,
+ bodyWidth: 312.6,
+ bodyHeight: 221.2,
+ notch: true,
+ sourceUrl: MACBOOK_PRO_SOURCE,
+ }),
+ mac({
+ id: "macbook-pro-16-m1-pro-max",
+ model: "MacBook Pro 16-inch (M1 Pro/Max)",
+ year: 2021,
+ category: "macbook",
+ width: 3456,
+ height: 2234,
+ diagonal: 16.2,
+ bodyWidth: 355.7,
+ bodyHeight: 248.1,
+ notch: true,
+ sourceUrl: MACBOOK_PRO_SOURCE,
+ }),
+ mac({
+ id: "imac-24-m1",
+ model: "iMac 24-inch (M1)",
+ year: 2021,
+ category: "imac",
+ width: 4480,
+ height: 2520,
+ diagonal: 23.5,
+ bodyWidth: 547,
+ bodyHeight: 461,
+ notch: false,
+ sourceUrl: IMAC_SOURCE,
+ }),
+ mac({
+ id: "macbook-air-13-m2",
+ model: "MacBook Air 13-inch (M2)",
+ year: 2022,
+ category: "macbook",
+ width: 2560,
+ height: 1664,
+ diagonal: 13.6,
+ bodyWidth: 304.1,
+ bodyHeight: 215,
+ notch: true,
+ sourceUrl: MACBOOK_AIR_SOURCE,
+ }),
+ mac({
+ id: "macbook-pro-13-m2",
+ model: "MacBook Pro 13-inch (M2)",
+ year: 2022,
+ category: "macbook",
+ width: 2560,
+ height: 1600,
+ diagonal: 13.3,
+ bodyWidth: 304.1,
+ bodyHeight: 212.4,
+ notch: false,
+ sourceUrl: MACBOOK_PRO_SOURCE,
+ }),
+ mac({
+ id: "studio-display",
+ model: "Studio Display",
+ year: 2022,
+ category: "display",
+ width: 5120,
+ height: 2880,
+ diagonal: 27,
+ bodyWidth: 623,
+ bodyHeight: 478,
+ notch: false,
+ sourceUrl: DISPLAY_SOURCE,
+ }),
+ mac({
+ id: "macbook-air-15-m2",
+ model: "MacBook Air 15-inch (M2)",
+ year: 2023,
+ category: "macbook",
+ width: 2880,
+ height: 1864,
+ diagonal: 15.3,
+ bodyWidth: 340.4,
+ bodyHeight: 237.6,
+ notch: true,
+ sourceUrl: MACBOOK_AIR_SOURCE,
+ }),
+ mac({
+ id: "macbook-pro-14-m2-pro-max",
+ model: "MacBook Pro 14-inch (M2 Pro/Max)",
+ year: 2023,
+ category: "macbook",
+ width: 3024,
+ height: 1964,
+ diagonal: 14.2,
+ bodyWidth: 312.6,
+ bodyHeight: 221.2,
+ notch: true,
+ sourceUrl: MACBOOK_PRO_SOURCE,
+ }),
+ mac({
+ id: "macbook-pro-16-m2-pro-max",
+ model: "MacBook Pro 16-inch (M2 Pro/Max)",
+ year: 2023,
+ category: "macbook",
+ width: 3456,
+ height: 2234,
+ diagonal: 16.2,
+ bodyWidth: 355.7,
+ bodyHeight: 248.1,
+ notch: true,
+ sourceUrl: MACBOOK_PRO_SOURCE,
+ }),
+ mac({
+ id: "macbook-pro-14-m3",
+ model: "MacBook Pro 14-inch (M3)",
+ year: 2023,
+ category: "macbook",
+ width: 3024,
+ height: 1964,
+ diagonal: 14.2,
+ bodyWidth: 312.6,
+ bodyHeight: 221.2,
+ notch: true,
+ sourceUrl: MACBOOK_PRO_SOURCE,
+ }),
+ mac({
+ id: "macbook-pro-14-m3-pro-max",
+ model: "MacBook Pro 14-inch (M3 Pro/Max)",
+ year: 2023,
+ category: "macbook",
+ width: 3024,
+ height: 1964,
+ diagonal: 14.2,
+ bodyWidth: 312.6,
+ bodyHeight: 221.2,
+ notch: true,
+ sourceUrl: MACBOOK_PRO_SOURCE,
+ }),
+ mac({
+ id: "macbook-pro-16-m3-pro-max",
+ model: "MacBook Pro 16-inch (M3 Pro/Max)",
+ year: 2023,
+ category: "macbook",
+ width: 3456,
+ height: 2234,
+ diagonal: 16.2,
+ bodyWidth: 355.7,
+ bodyHeight: 248.1,
+ notch: true,
+ sourceUrl: MACBOOK_PRO_SOURCE,
+ }),
+ mac({
+ id: "imac-24-m3",
+ model: "iMac 24-inch (M3)",
+ year: 2023,
+ category: "imac",
+ width: 4480,
+ height: 2520,
+ diagonal: 23.5,
+ bodyWidth: 547,
+ bodyHeight: 461,
+ notch: false,
+ sourceUrl: IMAC_SOURCE,
+ }),
+ mac({
+ id: "macbook-air-13-m3",
model: "MacBook Air 13-inch (M3)",
- slug: "macbook-air-13-m3",
year: 2024,
category: "macbook",
width: 2560,
@@ -538,10 +1172,11 @@ const macs: DeviceProfile[] = [
bodyWidth: 304.1,
bodyHeight: 215,
notch: true,
+ sourceUrl: MACBOOK_AIR_SOURCE,
}),
mac({
+ id: "macbook-air-15-m3",
model: "MacBook Air 15-inch (M3)",
- slug: "macbook-air-15-m3",
year: 2024,
category: "macbook",
width: 2880,
@@ -550,11 +1185,12 @@ const macs: DeviceProfile[] = [
bodyWidth: 340.4,
bodyHeight: 237.6,
notch: true,
+ sourceUrl: MACBOOK_AIR_SOURCE,
}),
mac({
- model: "MacBook Pro 14-inch",
- slug: "macbook-pro-14",
- year: 2023,
+ id: "macbook-pro-14-m4",
+ model: "MacBook Pro 14-inch (M4)",
+ year: 2024,
category: "macbook",
width: 3024,
height: 1964,
@@ -562,11 +1198,25 @@ const macs: DeviceProfile[] = [
bodyWidth: 312.6,
bodyHeight: 221.2,
notch: true,
+ sourceUrl: MACBOOK_PRO_SOURCE,
}),
mac({
- model: "MacBook Pro 16-inch",
- slug: "macbook-pro-16",
- year: 2023,
+ id: "macbook-pro-14-m4-pro-max",
+ model: "MacBook Pro 14-inch (M4 Pro/Max)",
+ year: 2024,
+ category: "macbook",
+ width: 3024,
+ height: 1964,
+ diagonal: 14.2,
+ bodyWidth: 312.6,
+ bodyHeight: 221.2,
+ notch: true,
+ sourceUrl: MACBOOK_PRO_SOURCE,
+ }),
+ mac({
+ id: "macbook-pro-16-m4-pro-max",
+ model: "MacBook Pro 16-inch (M4 Pro/Max)",
+ year: 2024,
category: "macbook",
width: 3456,
height: 2234,
@@ -574,11 +1224,12 @@ const macs: DeviceProfile[] = [
bodyWidth: 355.7,
bodyHeight: 248.1,
notch: true,
+ sourceUrl: MACBOOK_PRO_SOURCE,
}),
mac({
- model: "iMac 24-inch",
- slug: "imac-24",
- year: 2023,
+ id: "imac-24-m4",
+ model: "iMac 24-inch (M4)",
+ year: 2024,
category: "imac",
width: 4480,
height: 2520,
@@ -586,18 +1237,111 @@ const macs: DeviceProfile[] = [
bodyWidth: 547,
bodyHeight: 461,
notch: false,
+ sourceUrl: IMAC_SOURCE,
}),
mac({
- model: "Studio Display",
- slug: "studio-display",
- year: 2022,
- category: "display",
- width: 5120,
- height: 2880,
- diagonal: 27,
- bodyWidth: 623,
- bodyHeight: 478,
+ id: "macbook-air-13-m4",
+ model: "MacBook Air 13-inch (M4)",
+ year: 2025,
+ category: "macbook",
+ width: 2560,
+ height: 1664,
+ diagonal: 13.6,
+ bodyWidth: 304.1,
+ bodyHeight: 215,
+ notch: true,
+ sourceUrl: MACBOOK_AIR_SOURCE,
+ }),
+ mac({
+ id: "macbook-air-15-m4",
+ model: "MacBook Air 15-inch (M4)",
+ year: 2025,
+ category: "macbook",
+ width: 2880,
+ height: 1864,
+ diagonal: 15.3,
+ bodyWidth: 340.4,
+ bodyHeight: 237.6,
+ notch: true,
+ sourceUrl: MACBOOK_AIR_SOURCE,
+ }),
+ mac({
+ id: "macbook-pro-14-m5",
+ model: "MacBook Pro 14-inch (M5)",
+ year: 2025,
+ category: "macbook",
+ width: 3024,
+ height: 1964,
+ diagonal: 14.2,
+ bodyWidth: 312.6,
+ bodyHeight: 221.2,
+ notch: true,
+ sourceUrl: MACBOOK_PRO_SOURCE,
+ }),
+ mac({
+ id: "macbook-neo",
+ model: "MacBook Neo",
+ year: 2026,
+ category: "macbook",
+ width: 2408,
+ height: 1506,
+ diagonal: 13,
+ bodyWidth: 297.5,
+ bodyHeight: 206.4,
notch: false,
+ sourceUrl: MACBOOK_NEO_SOURCE,
+ }),
+ mac({
+ id: "macbook-air-13-m5",
+ model: "MacBook Air 13-inch (M5)",
+ year: 2026,
+ category: "macbook",
+ width: 2560,
+ height: 1664,
+ diagonal: 13.6,
+ bodyWidth: 304.1,
+ bodyHeight: 215,
+ notch: true,
+ sourceUrl: MACBOOK_AIR_SOURCE,
+ }),
+ mac({
+ id: "macbook-air-15-m5",
+ model: "MacBook Air 15-inch (M5)",
+ year: 2026,
+ category: "macbook",
+ width: 2880,
+ height: 1864,
+ diagonal: 15.3,
+ bodyWidth: 340.4,
+ bodyHeight: 237.6,
+ notch: true,
+ sourceUrl: MACBOOK_AIR_SOURCE,
+ }),
+ mac({
+ id: "macbook-pro-14-m5-pro-max",
+ model: "MacBook Pro 14-inch (M5 Pro/Max)",
+ year: 2026,
+ category: "macbook",
+ width: 3024,
+ height: 1964,
+ diagonal: 14.2,
+ bodyWidth: 312.6,
+ bodyHeight: 221.2,
+ notch: true,
+ sourceUrl: MACBOOK_PRO_SOURCE,
+ }),
+ mac({
+ id: "macbook-pro-16-m5-pro-max",
+ model: "MacBook Pro 16-inch (M5 Pro/Max)",
+ year: 2026,
+ category: "macbook",
+ width: 3456,
+ height: 2234,
+ diagonal: 16.2,
+ bodyWidth: 355.7,
+ bodyHeight: 248.1,
+ notch: true,
+ sourceUrl: MACBOOK_PRO_SOURCE,
}),
];
diff --git a/src/styles/index.css b/src/styles/index.css
index 724635c..db0c74a 100644
--- a/src/styles/index.css
+++ b/src/styles/index.css
@@ -861,6 +861,10 @@ input[type="range"]::-webkit-slider-thumb {
border-radius: 12px;
background: linear-gradient(145deg, #30363d, #07090c 50%, #333942);
}
+.is-macbook .device-shell {
+ padding: 8px;
+ border-radius: 11px 11px 6px 6px;
+}
.screen-only .device-shell {
padding: 0;
border-color: transparent;
@@ -875,14 +879,14 @@ input[type="range"]::-webkit-slider-thumb {
}
.screen-bezel {
overflow: hidden;
- border-radius: 11.2% / 5.2%;
+ border-radius: var(--screen-corner-x, 11.2%) / var(--screen-corner-y, 5.2%);
background: #000;
}
.is-ipad .screen-bezel {
- border-radius: 4%;
+ border-radius: var(--screen-corner-x, 4%) / var(--screen-corner-y, 4%);
}
.is-mac .screen-bezel {
- border-radius: 6px;
+ border-radius: var(--screen-corner-x, 6px) / var(--screen-corner-y, 6px);
}
.device-screen {
position: relative;
@@ -1147,6 +1151,27 @@ input[type="range"]::-webkit-slider-thumb {
border-radius: 50%;
background: #30363d;
}
+.laptop-base {
+ position: absolute;
+ top: calc(100% - 2px);
+ left: -5%;
+ width: 110%;
+ height: 5.5%;
+ min-height: 7px;
+ border-radius: 2px 2px 10px 10px;
+ background: linear-gradient(180deg, #8e969e 0, #363d45 42%, #171b20 100%);
+ box-shadow: 0 5px 8px rgba(0, 0, 0, 0.38);
+ clip-path: polygon(4% 0, 96% 0, 100% 52%, 96% 100%, 4% 100%, 0 52%);
+}
+.laptop-base i {
+ position: absolute;
+ top: 0;
+ left: 43%;
+ width: 14%;
+ height: 42%;
+ border-radius: 0 0 8px 8px;
+ background: rgba(8, 10, 13, 0.72);
+}
.stage-hint {
display: flex;
align-items: center;
@@ -1806,6 +1831,9 @@ input:checked + .switch::after {
.is-mac .device-shell {
padding: 5px 5px 14px;
}
+ .is-macbook .device-shell {
+ padding: 5px;
+ }
.computer-chin {
height: 13px;
}
diff --git a/tests/integration/editor-controls.test.tsx b/tests/integration/editor-controls.test.tsx
index 3ef35b0..d26dd66 100644
--- a/tests/integration/editor-controls.test.tsx
+++ b/tests/integration/editor-controls.test.tsx
@@ -4,6 +4,7 @@ import { describe, expect, it } from "vitest";
import { AdjustmentControls } from "../../src/components/editor-controls/AdjustmentControls";
import { DeviceSelector } from "../../src/components/device-selector/DeviceSelector";
import { OverlayPanel } from "../../src/components/overlays/OverlayPanel";
+import { DevicePreview } from "../../src/components/preview/DevicePreview";
import { I18nProvider } from "../../src/i18n/i18n";
import { EditorProvider } from "../../src/state/editor-context";
@@ -31,28 +32,44 @@ describe("editor controls", () => {
const search = screen.getByPlaceholderText("搜尋型號、年份或解析度…");
await user.type(search, "1290");
- expect(screen.getByText("3 個結果")).toBeInTheDocument();
+ expect(screen.getByText("4 個結果")).toBeInTheDocument();
expect(screen.getByRole("option", { name: /iPhone 15 Pro Max,2023/ })).toBeInTheDocument();
await user.type(search, "{Escape}");
expect(search).toHaveValue("");
- expect(screen.getByText("12 個結果")).toBeInTheDocument();
+ expect(screen.getByText("27 個結果")).toBeInTheDocument();
});
it("shows the selected device again after browsing another category", async () => {
const user = userEvent.setup();
renderWithEditor(
);
- await user.click(screen.getByRole("button", { name: "iPad · 5" }));
- expect(screen.getByText("5 個結果")).toBeInTheDocument();
+ await user.click(screen.getByRole("button", { name: "iPad · 20" }));
+ expect(screen.getByText("20 個結果")).toBeInTheDocument();
await user.click(screen.getByRole("button", { name: /目前裝置.*iPhone 15 Pro/ }));
- expect(screen.getByRole("button", { name: "iPhone · 12" })).toHaveAttribute(
+ expect(screen.getByRole("button", { name: "iPhone · 27" })).toHaveAttribute(
"aria-pressed",
"true",
);
});
+ it("renders MacBooks as laptops instead of desktop displays", async () => {
+ const user = userEvent.setup();
+ const { container } = renderWithEditor(
+ <>
+
+
+ >,
+ );
+
+ await user.click(screen.getByRole("button", { name: "Mac · 27" }));
+ await user.click(screen.getByRole("option", { name: /MacBook Neo,2026/ }));
+
+ expect(container.querySelector(".laptop-base")).toBeInTheDocument();
+ expect(container.querySelector(".computer-stand")).not.toBeInTheDocument();
+ });
+
it("updates fit, zoom, and background controls", async () => {
const user = userEvent.setup();
renderWithEditor(
);
diff --git a/tests/unit/profiles.test.ts b/tests/unit/profiles.test.ts
index 9448139..9f7521a 100644
--- a/tests/unit/profiles.test.ts
+++ b/tests/unit/profiles.test.ts
@@ -3,12 +3,48 @@ import { deviceProfiles } from "../../src/data/devices";
import { validateDeviceProfile, validateDeviceProfiles } from "../../src/core/validation";
describe("Apple device profiles", () => {
- it("meets MVP representative profile counts", () => {
- expect(deviceProfiles.filter((profile) => profile.category === "iphone")).toHaveLength(12);
- expect(deviceProfiles.filter((profile) => profile.category === "ipad")).toHaveLength(5);
+ it("covers every requested Apple display model from the target years", () => {
+ expect(deviceProfiles.filter((profile) => profile.category === "iphone")).toHaveLength(27);
+ expect(deviceProfiles.filter((profile) => profile.category === "ipad")).toHaveLength(20);
expect(
deviceProfiles.filter((profile) => ["macbook", "imac", "display"].includes(profile.category)),
- ).toHaveLength(6);
+ ).toHaveLength(27);
+
+ for (const id of [
+ "apple-iphone-13-pro-max",
+ "apple-iphone-se-3",
+ "apple-iphone-16-pro-max",
+ "apple-iphone-air",
+ "apple-iphone-17e",
+ "apple-ipad-9",
+ "apple-ipad-pro-12-9-6",
+ "apple-ipad-mini-a17-pro",
+ "apple-ipad-pro-13-m5",
+ "apple-ipad-air-13-m4",
+ "apple-macbook-pro-16-m1-pro-max",
+ "apple-macbook-pro-13-m2",
+ "apple-macbook-neo",
+ "apple-macbook-air-15-m5",
+ "apple-macbook-pro-16-m5-pro-max",
+ ]) {
+ expect(
+ deviceProfiles.some((profile) => profile.id === id),
+ id,
+ ).toBe(true);
+ }
+ });
+
+ it("keeps same-size models distinct when their display configuration differs", () => {
+ const iphone14 = deviceProfiles.find((profile) => profile.id === "apple-iphone-14");
+ const iphone14Pro = deviceProfiles.find((profile) => profile.id === "apple-iphone-14-pro");
+ const iphone15 = deviceProfiles.find((profile) => profile.id === "apple-iphone-15");
+ const iphone15Pro = deviceProfiles.find((profile) => profile.id === "apple-iphone-15-pro");
+
+ expect(iphone14?.chassis.screenDiagonalInches).toBe(iphone14Pro?.chassis.screenDiagonalInches);
+ expect(iphone14?.display.physicalHeightPx).not.toBe(iphone14Pro?.display.physicalHeightPx);
+ expect(iphone14?.cutout.type).toBe("notch");
+ expect(iphone14Pro?.cutout.type).toBe("dynamic-island");
+ expect(iphone15?.chassis.physicalWidthMm).not.toBe(iphone15Pro?.chassis.physicalWidthMm);
});
it("validates the complete data set", () => {
diff --git a/tests/unit/transforms.test.ts b/tests/unit/transforms.test.ts
index 4e25d38..d727049 100644
--- a/tests/unit/transforms.test.ts
+++ b/tests/unit/transforms.test.ts
@@ -12,7 +12,7 @@ describe("device orientation transforms", () => {
});
it("does not swap native landscape Macs", () => {
- const profile = getDeviceProfile("apple-macbook-pro-14");
+ const profile = getDeviceProfile("apple-macbook-pro-14-m1-pro-max");
expect(orientDevice(profile, "landscape").outputWidth).toBe(3024);
});
@@ -26,7 +26,7 @@ describe("device orientation transforms", () => {
});
it("keeps a native-landscape MacBook notch at the top center", () => {
- const profile = getDeviceProfile("apple-macbook-pro-14");
+ const profile = getDeviceProfile("apple-macbook-pro-14-m1-pro-max");
expect(
orientRect(
profile.cutout.boundsPx,