Update to Hen Screenshots
Snapshot Oct 24, 2026 · 13:21 UTC · version 0.2.8
Package or technical metadata updated
Package or technical metadata updated
Observed in package metadata. These changes alone do not establish a new customer-facing feature.
Up to 3 plugins free · no card required.
All fields and original values.
These are saved file metadata. Open a changed file to read its text edits.
Package file
Sep 30, 2026 · 23:16 UTC
bb2f8bd86f949e5260ce40f1c3215672ba0f1e576284dd152053cca7ac90afaf
Oct 24, 2026 · 13:21 UTC
b9cbe58f74b25ba251d3a0205c6b25dd41276d21348fa817ebd5dc9dfe92514e
Package file
Sep 30, 2026 · 23:16 UTC
519
Oct 24, 2026 · 13:21 UTC
543
Package file
Sep 30, 2026 · 23:16 UTC
{"sha256":"5abf911a83f89a2a655d94ec0bad78a3b9f633388159a9de6305485ee7d910ff","size":14032}
Oct 24, 2026 · 13:21 UTC
Not present
Package file
Sep 30, 2026 · 23:16 UTC
{"sha256":"5abf911a83f89a2a655d94ec0bad78a3b9f633388159a9de6305485ee7d910ff","size":14032}
Oct 24, 2026 · 13:21 UTC
Not present
Compare saved observations
Download comparison JSONChanged files
Choose a file to see its text changes. Binary or large files show metadata only.
.codex-plugin/assets/composer-icon.png →
dist/cli.mjs
− removed · + added
--- before +++ after @@ -8,7 +8,7 @@ import "konva/skia-backend"; import { FontLibrary, Image as Image$1 } from "skia-canvas"; //#region plugins/hen-screenshots/package.json -var version = "0.2.4"; +var version = "0.2.8"; //#endregion //#region src/core/device-composition-spec.ts var deviceCompositions = { @@ -57,6 +57,303 @@ return panoramaStart(id) !== void 0; } //#endregion +//#region src/core/additional-store-formats.ts +var APPLE_SCREENSHOTS$1 = "https://developer.apple.com/help/app-store-connect/reference/app-information/screenshot-specifications/"; +var PLAY_SCREENSHOTS$1 = "https://support.google.com/googleplay/android-developer/answer/9866151?hl=en"; +var additionalStoreProfiles = [ + { + id: "apple-iphone63-portrait", + name: "App Store · iPhone · 6.3-inch · Portrait", + store: "apple", + category: "phone", + width: 1206, + height: 2622, + maxCount: 10, + note: "Optional device slot; larger screenshots can be scaled by App Store Connect.", + source: APPLE_SCREENSHOTS$1 + }, + { + id: "apple-iphone63-landscape", + name: "App Store · iPhone · 6.3-inch · Landscape", + store: "apple", + category: "phone", + width: 2622, + height: 1206, + maxCount: 10, + note: "Optional device slot; larger screenshots can be scaled by App Store Connect.", + source: APPLE_SCREENSHOTS$1 + }, + { + id: "apple-iphone61-portrait", + name: "App Store · iPhone · 6.1-inch · Portrait", + store: "apple", + category: "phone", + width: 1170, + height: 2532, + maxCount: 10, + note: "Optional device slot; larger screenshots can be scaled by App Store Connect.", + source: APPLE_SCREENSHOTS$1 + }, + { + id: "apple-iphone61-landscape", + name: "App Store · iPhone · 6.1-inch · Landscape", + store: "apple", + category: "phone", + width: 2532, + height: 1170, + maxCount: 10, + note: "Optional device slot; larger screenshots can be scaled by App Store Connect.", + source: APPLE_SCREENSHOTS$1 + }, + { + id: "apple-iphone55-portrait", + name: "App Store · iPhone · 5.5-inch · Portrait", + store: "apple", + category: "phone", + width: 1242, + height: 2208, + maxCount: 10, + note: "Optional device slot; larger screenshots can be scaled by App Store Connect.", + source: APPLE_SCREENSHOTS$1 + }, + { + id: "apple-iphone55-landscape", + name: "App Store · iPhone · 5.5-inch · Landscape", + store: "apple", + category: "phone", + width: 2208, + height: 1242, + maxCount: 10, + note: "Optional device slot; larger screenshots can be scaled by App Store Connect.", + source: APPLE_SCREENSHOTS$1 + }, + { + id: "apple-iphone47-portrait", + name: "App Store · iPhone · 4.7-inch · Portrait", + store: "apple", + category: "phone", + width: 750, + height: 1334, + maxCount: 10, + note: "Optional device slot; larger screenshots can be scaled by App Store Connect.", + source: APPLE_SCREENSHOTS$1 + }, + { + id: "apple-iphone47-landscape", + name: "App Store · iPhone · 4.7-inch · Landscape", + store: "apple", + category: "phone", + width: 1334, + height: 750, + maxCount: 10, + note: "Optional device slot; larger screenshots can be scaled by App Store Connect.", + source: APPLE_SCREENSHOTS$1 + }, + { + id: "apple-iphone4-portrait", + name: "App Store · iPhone · 4-inch · Portrait", + store: "apple", + category: "phone", + width: 640, + height: 1136, + maxCount: 10, + note: "Optional device slot; larger screenshots can be scaled by App Store Connect.", + source: APPLE_SCREENSHOTS$1 + }, + { + id: "apple-iphone4-landscape", + name: "App Store · iPhone · 4-inch · Landscape", + store: "apple", + category: "phone", + width: 1136, + height: 640, + maxCount: 10, + note: "Optional device slot; larger screenshots can be scaled by App Store Connect.", + source: APPLE_SCREENSHOTS$1 + }, + { + id: "apple-iphone35-portrait", + name: "App Store · iPhone · 3.5-inch · Portrait", + store: "apple", + category: "phone", + width: 640, + height: 960, + maxCount: 10, + note: "Optional device slot; larger screenshots can be scaled by App Store Connect.", + source: APPLE_SCREENSHOTS$1 + }, + { + id: "apple-iphone35-landscape", + name: "App Store · iPhone · 3.5-inch · Landscape", + store: "apple", + category: "phone", + width: 960, + height: 640, + maxCount: 10, + note: "Optional device slot; larger screenshots can be scaled by App Store Connect.", + source: APPLE_SCREENSHOTS$1 + }, + { + id: "apple-ipad129-portrait", + name: "App Store · iPad · 12.9-inch · Portrait", + store: "apple", + category: "tablet", + width: 2048, + height: 2732, + maxCount: 10, + note: "Optional device slot; larger screenshots can be scaled by App Store Connect.", + source: APPLE_SCREENSHOTS$1 + }, + { + id: "apple-ipad129-landscape", + name: "App Store · iPad · 12.9-inch · Landscape", + store: "apple", + category: "tablet", + width: 2732, + height: 2048, + maxCount: 10, + note: "Optional device slot; larger screenshots can be scaled by App Store Connect.", + source: APPLE_SCREENSHOTS$1 + }, + { + id: "apple-ipad105-portrait", + name: "App Store · iPad · 10.5-inch · Portrait", + store: "apple", + category: "tablet", + width: 1668, + height: 2224, + maxCount: 10, + note: "Optional device slot; larger screenshots can be scaled by App Store Connect.", + source: APPLE_SCREENSHOTS$1 + }, + { + id: "apple-ipad105-landscape", + name: "App Store · iPad · 10.5-inch · Landscape", + store: "apple", + category: "tablet", + width: 2224, + height: 1668, + maxCount: 10, + note: "Optional device slot; larger screenshots can be scaled by App Store Connect.", + source: APPLE_SCREENSHOTS$1 + }, + { + id: "apple-ipad97-portrait", + name: "App Store · iPad · 9.7-inch · Portrait", + store: "apple", + category: "tablet", + width: 1536, + height: 2048, + maxCount: 10, + note: "Optional device slot; larger screenshots can be scaled by App Store Connect.", + source: APPLE_SCREENSHOTS$1 + }, + { + id: "apple-ipad97-landscape", + name: "App Store · iPad · 9.7-inch · Landscape", + store: "apple", + category: "tablet", + width: 2048, + height: 1536, + maxCount: 10, + note: "Optional device slot; larger screenshots can be scaled by App Store Connect.", + source: APPLE_SCREENSHOTS$1 + }, + { + id: "play-tablet7-8k-portrait", + name: "Google Play · Tablet · 7-inch · 8K · Portrait", + store: "google", + category: "tablet", + width: 4320, + height: 7680, + maxCount: 8, + note: "Large-screen screenshot: 16:9 or 9:16, up to 7680 pixels.", + source: PLAY_SCREENSHOTS$1 + }, + { + id: "play-tablet7-8k-landscape", + name: "Google Play · Tablet · 7-inch · 8K · Landscape", + store: "google", + category: "tablet", + width: 7680, + height: 4320, + maxCount: 8, + note: "Large-screen screenshot: 16:9 or 9:16, up to 7680 pixels.", + source: PLAY_SCREENSHOTS$1 + }, + { + id: "play-tablet10-8k-portrait", + name: "Google Play · Tablet · 10-inch · 8K · Portrait", + store: "google", + category: "tablet", + width: 4320, + height: 7680, + maxCount: 8, + note: "Large-screen screenshot: 16:9 or 9:16, up to 7680 pixels.", + source: PLAY_SCREENSHOTS$1 + }, + { + id: "play-tablet10-8k-landscape", + name: "Google Play · Tablet · 10-inch · 8K · Landscape", + store: "google", + category: "tablet", + width: 7680, + height: 4320, + maxCount: 8, + note: "Large-screen screenshot: 16:9 or 9:16, up to 7680 pixels.", + source: PLAY_SCREENSHOTS$1 + }, + { + id: "play-chromebook-8k-landscape", + name: "Google Play · Chromebook · 8K · Landscape", + store: "google", + category: "desktop", + width: 7680, + height: 4320, + maxCount: 8, + note: "Large-screen screenshot: 16:9 or 9:16, up to 7680 pixels.", + source: PLAY_SCREENSHOTS$1 + } +]; +var appleFrameVariants = { + "apple-iphone63": { + ratio: 2622 / 1206, + hardware: "island" + }, + "apple-iphone61": { + ratio: 2532 / 1170, + hardware: "notch" + }, + "apple-iphone55": { + ratio: 2208 / 1242, + hardware: "home" + }, + "apple-iphone47": { + ratio: 1334 / 750, + hardware: "home" + }, + "apple-iphone4": { + ratio: 1136 / 640, + hardware: "home" + }, + "apple-iphone35": { + ratio: 960 / 640, + hardware: "home" + }, + "apple-ipad129": { + ratio: 2732 / 2048, + hardware: "home" + }, + "apple-ipad105": { + ratio: 2224 / 1668, + hardware: "home" + }, + "apple-ipad97": { + ratio: 2048 / 1536, + hardware: "home" + } +}; +//#endregion //#region src/core/special-profiles.ts var specialProfiles = [ { @@ -238,6 +535,7 @@ } var exportProfiles = [ ...specialProfiles, + ...additionalStoreProfiles, { id: "play-feature-graphic", name: "Google Play · Feature graphic", @@ -404,6 +702,28 @@ source: APPLE_SCREENSHOTS }, { + id: "apple-ipad11-portrait", + name: "App Store · iPad 11-inch · Portrait", + store: "apple", + category: "tablet", + width: 1668, + height: 2420, + maxCount: 10, + note: "An accepted size for the 11-inch iPad slot.", + source: APPLE_SCREENSHOTS + }, + { + id: "apple-ipad11-landscape", + name: "App Store · iPad 11-inch · Landscape", + store: "apple", + category: "tablet", + width: 2420, + height: 1668, + maxCount: 10, + note: "An accepted size for the 11-inch iPad slot.", + source: APPLE_SCREENSHOTS + }, + { id: "apple-mac", name: "App Store · Mac", store: "apple", @@ -521,7 +841,7 @@ /** Size rules are separate from the allowlisted preset values, so bad edits fail closed. */ function validateDimensions(profile, width, height) { if (width !== profile.width || height !== profile.height || !Number.isInteger(width) || !Number.isInteger(height)) throw new Error("The exported image does not match the selected preset dimensions."); - const special = specialProfiles.find((item) => item.id === profile.id); + const special = [...specialProfiles, ...additionalStoreProfiles].find((item) => item.id === profile.id); if (special) { if (profile.store !== special.store || profile.category !== special.category || width !== special.width || height !== special.height) throw new Error("These dimensions do not match the selected store device slot."); return; @@ -538,7 +858,7 @@ }); if (profile.store === "google") { const short = Math.min(width, height), long = Math.max(width, height); - if (short < 320 || long > 3840 || long > short * 2) throw new Error("These dimensions do not satisfy Google Play screenshot rules."); + if (short < 320 || long > (profile.category === "phone" ? 3840 : 7680) || long > short * 2) throw new Error("These dimensions do not satisfy Google Play screenshot rules."); if (profile.category !== "phone" && (short < 1080 || long * 9 !== short * 16)) throw new Error("Google Play large-screen presets require 16:9 or 9:16 and at least 1080 pixels."); } else if (profile.store === "apple") { if (!{ @@ -555,7 +875,16 @@ [1260, 2736], [2736, 1260] ], - tablet: [ + tablet: profile.id.includes("ipad11") ? [ + [1488, 2266], + [2266, 1488], + [1668, 2420], + [2420, 1668], + [1668, 2388], + [2388, 1668], + [1640, 2360], + [2360, 1640] + ] : [ [2064, 2752], [2752, 2064], [2048, 2732], @@ -763,7 +1092,7 @@ } }; var LIMITS = { - shots: 20, + shots: 50, overlays: 8, assetBytes: 52428800, totalBytes: 125829120, @@ -806,6 +1135,10 @@ "orbit", "orbit-end", "workbench", + "portfolio-laptop", + "portfolio-monitor", + "portfolio-laptop-editorial", + "portfolio-monitor-dark", "nocturne", "carbon", "ember", @@ -1344,22 +1677,27 @@ function positive(value, name) { if (!Number.isFinite(value) || value <= 0) throw new Error(`${name} must be a positive number.`); } -function handheldGeometry(family, width, frame) { +function handheldGeometry(family, width, frame, exportProfile) { const tablet = family === "ipad" || family === "android-tablet"; - const inset = frame ? width * (tablet ? .035 : family === "ios" ? .027 : .024) : 0; + const variant = family === "ipad" && exportProfile?.startsWith("apple-ipad") || family === "ios" && exportProfile?.startsWith("apple-iphone") ? appleFrameVariants[exportProfile?.replace(/-(portrait|landscape)$/, "") ?? ""] : void 0; + const home = variant?.hardware === "home"; + const notch = variant?.hardware === "notch"; + const ipad11 = exportProfile?.startsWith("apple-ipad11-") ?? false; + const inset = frame ? width * (home ? .045 : tablet ? .035 : family === "ios" ? .027 : .024) : 0; + const verticalInset = frame && home ? width * .14 : inset; const screenWidth = width - inset * 2; - const screenHeight = screenWidth * (family === "ipad" ? 4 / 3 : family === "android-tablet" ? 16 / 10 : family === "ios" ? 19.5 / 9 : 20 / 9); - const radius = tablet ? frame ? width * (family === "ipad" ? .058 : .043) : 0 : width * (family === "ios" ? frame ? .13 : .102 : .078); + const screenHeight = screenWidth * (variant?.ratio ?? (family === "ipad" ? ipad11 ? 2420 / 1668 : 4 / 3 : family === "android-tablet" ? 16 / 10 : family === "ios" ? 19.5 / 9 : 20 / 9)); + const radius = home ? frame ? width * .055 : 0 : tablet ? frame ? width * (family === "ipad" ? .058 : .043) : 0 : width * (family === "ios" ? frame ? .13 : .102 : .078); const screen = { x: inset, - y: inset, + y: verticalInset, width: screenWidth, height: screenHeight, - radius: Math.max(0, radius - inset) + radius: home ? 0 : Math.max(0, radius - inset) }; - const cameraWidth = tablet ? width * .009 : family === "ios" ? screenWidth * .29 : screenWidth * .032; - const cameraHeight = family === "ios" ? screenWidth * .081 : cameraWidth; - const height = screenHeight + inset * 2; + const cameraWidth = home ? width * .013 : notch ? screenWidth * .42 : tablet ? width * .009 : family === "ios" ? screenWidth * .29 : screenWidth * .032; + const cameraHeight = home ? cameraWidth : notch ? screenWidth * .075 : family === "ios" ? screenWidth * .081 : cameraWidth; + const height = screenHeight + verticalInset * 2; const edge = width * (family === "ios" ? .006 : .004); const sideButton = (right, y, length) => ({ x: right ? width - edge * 1.5 : 0, @@ -1393,9 +1731,17 @@ height, radius, screen, + cameraKind: home || tablet || family !== "ios" ? "lens" : notch ? "notch" : "island", + ...home && frame ? { homeButton: { + x: width * .465, + y: height - verticalInset / 2 - width * .035, + width: width * .07, + height: width * .07, + radius: width * .035 + } } : {}, camera: { - x: tablet && frame ? width - (inset + cameraWidth) / 2 : (width - cameraWidth) / 2, - y: tablet ? frame ? (height - cameraHeight) / 2 : width * .012 : inset + screenWidth * (family === "ios" ? .026 : .022), + x: !home && tablet && frame ? width - (inset + cameraWidth) / 2 : (width - cameraWidth) / 2, + y: home ? verticalInset * .42 : notch ? inset : tablet ? frame ? (height - cameraHeight) / 2 : width * .012 : inset + screenWidth * (family === "ios" ? .026 : .022), width: cameraWidth, height: cameraHeight, radius: cameraHeight / 2 @@ -1427,7 +1773,7 @@ }; } /** Generic geometry. Width covers the entire device, including a laptop's base. */ -function deviceGeometry(family, width, frame, orientation = "portrait") { +function deviceGeometry(family, width, frame, orientation = "portrait", exportProfile) { positive(width, "Device width"); if (family === "card") { const height = width * (orientation === "landscape" ? 3 / 4 : 4 / 3); @@ -1591,8 +1937,8 @@ "ipad", "android-tablet" ].includes(family)) throw new Error("This device frame is not supported."); - if (orientation === "portrait") return handheldGeometry(family, width, frame); - const portrait = handheldGeometry(family, width / handheldGeometry(family, 1, frame).height, frame); + if (orientation === "portrait") return handheldGeometry(family, width, frame, exportProfile); + const portrait = handheldGeometry(family, width / handheldGeometry(family, 1, frame, exportProfile).height, frame, exportProfile); const rotate = (rect) => ({ x: rect.y, y: portrait.width - rect.x - rect.width, @@ -1606,6 +1952,7 @@ height: portrait.width, screen: rotate(portrait.screen), camera: rotate(portrait.camera), + ...portrait.homeButton ? { homeButton: rotate(portrait.homeButton) } : {}, ...portrait.handheld ? { handheld: { shell: rotate(portrait.handheld.shell), buttons: portrait.handheld.buttons.map(rotate), @@ -1900,9 +2247,8 @@ function linkedShots(project, shotId) { return panoramaPair(project, shotId) ?? project.shots.filter((shot) => shot.id === shotId); } -function shotCapacity(project) { - const profile = resolveExportProfile(project); - return profile.category === "banner" ? LIMITS.shots : Math.min(LIMITS.shots, profile.maxCount); +function shotCapacity(_project) { + return LIMITS.shots; } var panoramaStyle = { template: "panorama", @@ -1956,7 +2302,7 @@ titleSize: 116 } }; -function collectionLayout$1(h, style, id) { +function collectionLayout$1(h, style, id, exportProfile) { const { composition, rotation, centerX: preferredCenter } = collectionPanoramaLayouts[id]; const right = isPanoramaEnd(style.template); const wide = h <= 1080; @@ -1970,11 +2316,11 @@ y: h * .08, height: h * .84 }; - const unit = deviceGeometry(style.device, 1e3, style.frame, style.deviceOrientation); + const unit = deviceGeometry(style.device, 1e3, style.frame, style.deviceOrientation, exportProfile); const angle = Math.abs(rotation) * Math.PI / 180; const scale = Math.min(area.width / (Math.cos(angle) * unit.width + Math.sin(angle) * unit.height), area.height / (Math.sin(angle) * unit.width + Math.cos(angle) * unit.height)); const width = Math.max(32, Math.min(2160, Math.floor(unit.width * scale))); - const height = deviceGeometry(style.device, width, style.frame, style.deviceOrientation).height; + const height = deviceGeometry(style.device, width, style.frame, style.deviceOrientation, exportProfile).height; const boundWidth = Math.cos(angle) * width + Math.sin(angle) * height; const centerX = 1080 + Math.min(preferredCenter - 1080, boundWidth * .2); const low = composition === "reverse" ? !right : right; @@ -2015,19 +2361,19 @@ function panoramaLayout(project, style) { const { height: h } = canonicalCanvas(project); const collection = collectionPanoramaId(style.template); - if (collection) return collectionLayout$1(h, style, collection); + if (collection) return collectionLayout$1(h, style, collection, project.exportProfile); const right = isPanoramaEnd(style.template); const family = panoramaStart(style.template); const expressive = family !== "panorama"; const wide = h <= 1080; const rotation = family === "daybreak" ? 16 : family === "tidal" ? -10 : family === "orbit" ? 12 : family === "moonlight" ? -12 : -8; - const unit = deviceGeometry(style.device, 1e3, style.frame, style.deviceOrientation); + const unit = deviceGeometry(style.device, 1e3, style.frame, style.deviceOrientation, project.exportProfile); const radians = Math.abs(rotation) * Math.PI / 180; const boundW = Math.cos(radians) * unit.width + Math.sin(radians) * unit.height; const boundH = Math.sin(radians) * unit.width + Math.cos(radians) * unit.height; const scale = Math.min((expressive ? 1100 : 980) / boundW, h * .85 / boundH); const width = Math.floor(unit.width * scale); - const height = deviceGeometry(style.device, width, style.frame, style.deviceOrientation).height; + const height = deviceGeometry(style.device, width, style.frame, style.deviceOrientation, project.exportProfile).height; if (expressive) return { phone: { x: Math.round(1080 - width / 2), @@ -2601,6 +2947,102 @@ }; } //#endregion +//#region src/core/portfolio-templates.ts +/** Explicit hardware presets; other single-device templates keep the user's frame. */ +var portfolioTemplates = [ + { + id: "portfolio-laptop", + name: "Laptop Studio", + device: "laptop", + appearance: "light", + colors: [ + "#ECEFEA", + "#D7DED4", + "#2C382E", + "#72856C" + ], + description: "A spacious laptop cover for your next case study." + }, + { + id: "portfolio-monitor", + name: "Monitor Gallery", + device: "monitor", + appearance: "light", + colors: [ + "#F4EDE3", + "#E6D8C7", + "#43382C", + "#A1805F" + ], + description: "A desktop centerpiece on a warm gallery wall." + }, + { + id: "portfolio-laptop-editorial", + name: "Laptop Editorial", + device: "laptop", + appearance: "light", + colors: [ + "#F2ECE0", + "#E2D7C5", + "#3C352E", + "#987352" + ], + description: "Warm paper, a serif headline, and your work on a laptop." + }, + { + id: "portfolio-monitor-dark", + name: "Monitor Focus", + device: "monitor", + appearance: "dark", + colors: [ + "#232C30", + "#344248", + "#F0EDE4", + "#97B6B8" + ], + description: "A quiet dark stage for detailed desktop interfaces." + } +].map((design) => { + const template = defineTemplate({ + id: design.id, + name: design.name, + category: design.id === "portfolio-laptop-editorial" ? "Editorial" : "Minimal", + appearance: design.appearance, + composition: "desk", + description: design.description, + note: "Sets up a landscape hardware frame with a large screenshot area, a headline above, and a caption below. Your text and image are preserved.", + surfaceLabel: design.device === "laptop" ? "Laptop showcase" : "Monitor showcase", + keywords: [ + "portfolio", + "case study", + "desktop", + "web", + "website", + "hardware", + "laptop", + "monitor", + "portafolio", + "computadora", + design.device + ], + ...design.id === "portfolio-laptop-editorial" ? { + titleFont: "Fraunces", + titleWeight: "600" + } : {} + }, design.colors, { size: 100 }); + return { + ...template, + style: { + ...template.style, + device: design.device, + deviceOrientation: "landscape", + frame: true, + camera: false, + fit: "contain" + } + }; +}); +//#endregion //#region src/core/device-composition.ts function companionFor(shot, element) { return shot.companions?.find((device) => `device:${device.id}` === element); @@ -2739,7 +3181,7 @@ ].includes(id); const boxes = frames.map((frame, index) => { const pose = { ...poses[index] }; - const size = deviceGeometry(frame.device, pose.width, frame.frame, frame.deviceOrientation); + const size = deviceGeometry(frame.device, pose.width, frame.frame, frame.deviceOrientation, project.exportProfile); if (mirrored) { pose.x = 1e3 - pose.x - size.width; pose.rotation *= -1; @@ -2764,7 +3206,7 @@ const devices = boxes.map((box, index) => { const width = Math.max(PLACEMENT_LIMITS.width.min, box.width * scale); const frame = frames[index]; - const geometry = deviceGeometry(frame.device, width, frame.frame, frame.deviceOrientation); + const geometry = deviceGeometry(frame.device, width, frame.frame, frame.deviceOrientation, project.exportProfile); const angle = Math.abs(box.rotation) * Math.PI / 180; const rotatedWidth = Math.cos(angle) * geometry.width + Math.sin(angle) * geometry.height; const rotatedHeight = Math.sin(angle) * geometry.width + Math.cos(angle) * geometry.height; @@ -3662,6 +4104,7 @@ //#region src/core/templates.ts /** IDs and geometry are part of document v2. Add new IDs for incompatible designs. */ var templates = [ + ...portfolioTemplates, ...collectionPanoramaTemplates, ...holidayTemplates, ...multiDeviceTemplates, @@ -4328,13 +4771,13 @@ }; } const rotation = style.template === "tilt" ? -6 : 0; - const unit = deviceGeometry(style.device, 1e3, style.frame, style.deviceOrientation); + const unit = deviceGeometry(style.device, 1e3, style.frame, style.deviceOrientation, project.exportProfile); const radians = Math.abs(rotation) * Math.PI / 180; const boundW = Math.cos(radians) * unit.width + Math.sin(radians) * unit.height; const boundH = Math.sin(radians) * unit.width + Math.cos(radians) * unit.height; const scale = Math.min(area.width / boundW, area.height / boundH); const width = Math.round(unit.width * scale); - const height = deviceGeometry(style.device, width, style.frame, style.deviceOrientation).height; + const height = deviceGeometry(style.device, width, style.frame, style.deviceOrientation, project.exportProfile).height; const phone = { width, x: Math.round(area.x + (area.width - width) / 2), @@ -4452,13 +4895,13 @@ height: area.height * (1 - inset * 2) }; } - const unit = deviceGeometry(style.device, 1e3, style.frame, style.deviceOrientation); + const unit = deviceGeometry(style.device, 1e3, style.frame, style.deviceOrientation, project.exportProfile); const rotation = template.composition ? template.phone.rotation : style.template === "punch" ? -6 : 0; const radians = Math.abs(rotation) * Math.PI / 180; const boundW = Math.cos(radians) * unit.width + Math.sin(radians) * unit.height; const boundH = Math.sin(radians) * unit.width + Math.cos(radians) * unit.height; const width = Math.floor(rotation === 0 ? Math.min(area.width, area.height / unit.height * 1e3) : 1e3 * Math.min(area.width / boundW, area.height / boundH)); - const height = deviceGeometry(style.device, width, style.frame, style.deviceOrientation).height; + const height = deviceGeometry(style.device, width, style.frame, style.deviceOrientation, project.exportProfile).height; return { ...template, title, @@ -4689,7 +5132,7 @@ } /** Validate the whole batch before returning any assets; the caller owns persistence. */ async function importImages(files) { - if (files.length > LIMITS.shots) throw new Error("Choose up to 20 images at a time."); + if (files.length > LIMITS.shots) throw new Error("Choose up to 50 images at a time."); for (const file of files) { if (!file.size) throw new Error(`“${file.name}” is empty (0 bytes). Choose the original image or download it again.`); if (file.size > LIMITS.assetBytes) throw new Error(`“${file.name}” is ${(file.size / 1024 / 1024).toFixed(1)} MB. The limit is 50 MB per image. Compress a copy or choose a smaller image.`); @@ -5018,7 +5461,7 @@ if (!exportProfiles.some((p) => p.id === raw.profile)) throw new Error("Unknown export profile. Run hen profiles for supported IDs."); if (raw.sourceLanguage !== void 0 && !isLanguage(raw.sourceLanguage)) throw new Error("Unsupported sourceLanguage. Run hen languages."); if (raw.brandKit !== void 0) string(raw.brandKit, 4096, "brandKit"); - if (!Array.isArray(raw.slides) || !raw.slides.length || raw.slides.length > LIMITS.shots) throw new Error("Provide 1–20 slides."); + if (!Array.isArray(raw.slides) || !raw.slides.length || raw.slides.length > LIMITS.shots) throw new Error("Provide 1–50 slides."); raw.slides.forEach((value, index) => { const at = `slides[${index}]`; const slide = object(value, [ @@ -5335,13 +5778,31 @@ strokeWidth: Math.min(device.width, device.height) * .0015, listening: false })); + if (device.frame && device.homeButton) group.add(new Konva.Rect({ + ...device.homeButton, + cornerRadius: device.homeButton.radius, + fill: "#101114", + stroke: "#70747A", + strokeWidth: Math.min(device.width, device.height) * .002, + listening: false + })); if (!camera || device.family === "card") return; const cutout = device.camera; const diameter = Math.min(cutout.width, cutout.height); - const island = device.family === "ios"; + const island = device.family === "ios" && device.cameraKind !== "lens"; group.add(new Konva.Rect({ ...cutout, - cornerRadius: cutout.radius, + cornerRadius: device.cameraKind === "notch" ? cutout.width > cutout.height ? [ + 0, + 0, + cutout.radius, + cutout.radius + ] : [ + 0, + cutout.radius, + cutout.radius, + 0 + ] : cutout.radius, fill: "#050608", stroke: "#24262B", strokeWidth: diameter * .035, @@ -7004,6 +7465,38 @@ fill, opacity })); + if (style.template.startsWith("portfolio-")) { + const editorial = style.template === "portfolio-laptop-editorial"; + rect({ + x: x - 16, + y: y - 16, + width: w + 32, + height: ph + 32, + fill: style.backgroundEnd, + opacity: editorial ? .45 : .7, + cornerRadius: editorial ? 4 : 24 + }); + line([ + 80, + h * .85, + 1e3, + h * .85 + ], .45, 1); + if (editorial) { + line([ + 76, + h * .235, + 1004, + h * .235 + ], .65, 1.5); + line([ + 76, + h * .035, + 1004, + h * .035 + ], .65, 2); + } + } if (style.template === "prism") { polygon([ x - 30, @@ -8798,7 +9291,7 @@ if (empty && !options.emptyDeviceLabel) continue; if (!empty && (!source?.complete || source.naturalWidth <= 0 || source.naturalHeight <= 0)) throw new Error("The screenshot has not finished loading."); const imageWidth = source?.naturalWidth ?? 0, imageHeight = source?.naturalHeight ?? 0; - const device = deviceGeometry(style.device, slot.phone.width, style.frame, style.deviceOrientation); + const device = deviceGeometry(style.device, slot.phone.width, style.frame, style.deviceOrientation, project.exportProfile); const phone = new Konva.Group({ x: slot.phone.x + device.width / 2 - cropOffset, y: slot.phone.y + device.height / 2, @@ -9012,7 +9505,7 @@ for (const slot of [shot, ...(shot.companions ?? []).map((d) => deviceShot(shot, `device:${d.id}`))]) { if (slot.assetId !== asset.id) continue; const style = resolveStyle(project, slot); - const screen = deviceGeometry(style.device, slot.phone.width, style.frame, style.deviceOrientation).screen; + const screen = deviceGeometry(style.device, slot.phone.width, style.frame, style.deviceOrientation, project.exportProfile).screen; scales.push(fitImage(asset.width, asset.height, screen, style.fit).width / asset.width); } for (const owner of linkedShots(project, shot.id)) for (const overlay of owner.overlays ?? []) if (overlay.assetId === asset.id) scales.push(Math.max(overlay.width / asset.width, overlay.height / asset.height));
Full technical diff · 18 changed fields
changed /files/.claude-plugin~1plugin.json/sha256
"bb2f8bd86f949e5260ce40f1c3215672ba0f1e576284dd152053cca7ac90afaf"
"b9cbe58f74b25ba251d3a0205c6b25dd41276d21348fa817ebd5dc9dfe92514e"
changed /files/.claude-plugin~1plugin.json/size
519
543
removed /files/.codex-plugin~1assets~1composer-icon.png
{
"sha256": "5abf911a83f89a2a655d94ec0bad78a3b9f633388159a9de6305485ee7d910ff",
"size": 14032
}Field is absent
removed /files/.codex-plugin~1assets~1logo.png
{
"sha256": "5abf911a83f89a2a655d94ec0bad78a3b9f633388159a9de6305485ee7d910ff",
"size": 14032
}Field is absent
changed /files/.codex-plugin~1plugin.json/sha256
"54fda44ea82e66c14af4931176e733f86aa1d35937797e90826c4d06e230bb64"
"871b37074b54c476c2370fc9f5bed0e940c53abc2b935292ccfc7631a5168a7c"
changed /files/.codex-plugin~1plugin.json/size
2697
2487
changed /files/.cursor-plugin~1plugin.json/sha256
"37fd55422d25ec02990737b4245e26b286adf9f98d5a0983cc1b46b4fa755ae1"
"3e26faf8d304d398c6611c4397a55bb67c80174e6049a5a6c9539d675a100554"
changed /files/.cursor-plugin~1plugin.json/size
539
563
changed /files/PRIVACY.md/sha256
"0e53625b54bb91924cb0aa236c58d695fdeb49f25bade85622cb63c533cadf76"
"7fdfe3f367a6f64a4630ec35b84ea3fbc6b6df62b1cc248e32c7821dd4db0eac"
changed /files/PRIVACY.md/size
1276
3008
changed /files/README.md/sha256
"fbe184e28f873f40a6f4ef978024174ddb4b211e7de59da26e7c77dfd707b78a"
"23b46e8bee6c114954740e6470e686061b445c001cf1d7a11d0386fb1c52f824"
changed /files/README.md/size
9001
10388
changed /files/dist~1cli.mjs/sha256
"ba8ddc3e985f19209704059a02ededb3c2cb157a022c69510637b645d9dff1fa"
"9cfc0af21abbdba3cb9d75a379e551fe2b9a73c76dd21f5ef346c7bd65363611"
changed /files/dist~1cli.mjs/size
248598
261782
added /files/gemini-extension.json
Field was absent
{
"sha256": "8d620e9b087aa23f96e4e1282c96bcde3728bd8819d4b0b0987f84c5ef43eb8a",
"size": 263
}changed /files/package-lock.json/sha256
"0f476d2a402f45d6b4659ee0b4cc7328da1919ebdfbeac74db5e621d2bb2435e"
"34a2c9ed9c957e477a88ddafb79c332d790109d59f9dcf16e0e2854c46711fd4"
changed /files/package.json/sha256
"5db7997168a45aae3609aa5b816b60da08859c9deb01b56387aa8fedb20ae1e6"
"2fee52a495fae5a2953f766f039fab08f05038e3380e574a0081e56d14aa2b1c"
changed /files/plugin.json/sha256
"002f1a660c59ab18b2b0cb0bc816a99afb4a1ae47727d86eb063def1f3890754"
"3a6883db6dec3a5adb47a0d11733a9e0d92123560449ad9f2ca3bb465734b2c2"
Full snapshot data
{
"files": {
".claude-plugin/plugin.json": {
"sha256": "b9cbe58f74b25ba251d3a0205c6b25dd41276d21348fa817ebd5dc9dfe92514e",
"size": 543
},
".codex-plugin/plugin.json": {
"sha256": "871b37074b54c476c2370fc9f5bed0e940c53abc2b935292ccfc7631a5168a7c",
"size": 2487
},
".cursor-plugin/plugin.json": {
"sha256": "3e26faf8d304d398c6611c4397a55bb67c80174e6049a5a6c9539d675a100554",
"size": 563
},
"LICENSE": {
"sha256": "017fa967a6f8eb914b72104be8f53001f7c40326dfb2beb99df4855c8d7eb3df",
"size": 1064
},
"PRIVACY.md": {
"sha256": "7fdfe3f367a6f64a4630ec35b84ea3fbc6b6df62b1cc248e32c7821dd4db0eac",
"size": 3008
},
"README.md": {
"sha256": "23b46e8bee6c114954740e6470e686061b445c001cf1d7a11d0386fb1c52f824",
"size": 10388
},
"THIRD_PARTY_NOTICES.md": {
"sha256": "eba57deade7af4cac0cb5e90f9c1806992b1cee93033fcdf0e2997e3b6ff40fd",
"size": 1278
},
"assets/fonts/Fraunces-Semibold.ttf": {
"sha256": "1390d372c2edc7881191bef9fb673ed8ec4d2bf143a2f68b63c01094794307ac",
"size": 72952
},
"assets/fonts/Manrope.ttf": {
"sha256": "d0639be45d0af36e798172419d7bd173c4bd4f29e2b76cbb69db1d11bf8b0a40",
"size": 165420
},
"assets/fonts/OFL-Fraunces.txt": {
"sha256": "bdf4c22802eaf804f998195871c6b8938aac2ac14b2d78a8bd66a6f1eced833b",
"size": 4391
},
"assets/fonts/OFL.txt": {
"sha256": "e01b637272e0cbdfb240184dd98ea5cc671556d9894dae2668d92ab2c906787c",
"size": 4384
},
"assets/icon.png": {
"sha256": "5abf911a83f89a2a655d94ec0bad78a3b9f633388159a9de6305485ee7d910ff",
"size": 14032
},
"dist/cli.mjs": {
"sha256": "9cfc0af21abbdba3cb9d75a379e551fe2b9a73c76dd21f5ef346c7bd65363611",
"size": 261782
},
"gemini-extension.json": {
"sha256": "8d620e9b087aa23f96e4e1282c96bcde3728bd8819d4b0b0987f84c5ef43eb8a",
"size": 263
},
"package-lock.json": {
"sha256": "34a2c9ed9c957e477a88ddafb79c332d790109d59f9dcf16e0e2854c46711fd4",
"size": 24103
},
"package.json": {
"sha256": "2fee52a495fae5a2953f766f039fab08f05038e3380e574a0081e56d14aa2b1c",
"size": 291
},
"plugin.json": {
"sha256": "3a6883db6dec3a5adb47a0d11733a9e0d92123560449ad9f2ca3bb465734b2c2",
"size": 618
},
"scripts/hen.mjs": {
"sha256": "635ad1625a2ae6506d53eeef9e172287e3cfcf3a712b8ab694dcb63d6857c749",
"size": 762
},
"scripts/setup.mjs": {
"sha256": "540a8b07a36757453607733a63a5b4d383490543b505daa0c1abe99401f38903",
"size": 2016
},
"skills/create-screenshots/SKILL.md": {
"sha256": "62c265404fa06509952aadfe7fa366b222712cc7c34ddf93e8fc76fbd6bc21dc",
"size": 4530
},
"skills/create-screenshots/references/design-spec.md": {
"sha256": "e887973db4a5a59f670dab59fa61d9f382522c0ca990d12cd5c06e481b7a9867",
"size": 6862
}
}
}SHA-256 of public snapshot: 625bfa35ec2afe5578173e3177fa6e33f7bb9fe699855b1cfadc6348c5e0d7a6