@appshoteditor/shot-dsl 0.1.0 → 0.1.2

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@appshoteditor/shot-dsl",
3
- "version": "0.1.0",
3
+ "version": "0.1.2",
4
4
  "description": "App Shot Editor layout DSL + device-frame geometry — framework-free building blocks for composing editable App Store screenshot layouts. Intended for use via a bundler (Vite, esbuild, etc.).",
5
5
  "license": "MIT",
6
6
  "type": "module",
package/src/builders.ts CHANGED
@@ -1,4 +1,4 @@
1
- import type { BackgroundJSON, ColorStop, LayerJSON, ScreenLayersJSON, Template, TemplateRole } from './types';
1
+ import type { BackgroundJSON, ColorStop, DeviceClass, LayerJSON, ScreenLayersJSON, Template, TemplateRole } from './types';
2
2
  import { CURRENT_SCHEMA_VERSION } from './types';
3
3
  import { generateLayerId } from './validate';
4
4
 
@@ -176,11 +176,13 @@ export function makeScreen(opts: {
176
176
  background?: BackgroundJSON;
177
177
  canvasWidth?: number;
178
178
  canvasHeight?: number;
179
+ deviceClass?: DeviceClass;
179
180
  }): ScreenLayersJSON {
180
181
  return {
181
182
  schemaVersion: CURRENT_SCHEMA_VERSION,
182
183
  canvasWidth: opts.canvasWidth ?? DEFAULT_CANVAS_WIDTH,
183
184
  canvasHeight: opts.canvasHeight ?? DEFAULT_CANVAS_HEIGHT,
185
+ deviceClass: opts.deviceClass,
184
186
  layers: opts.layers,
185
187
  background: opts.background
186
188
  };
package/src/compose.ts CHANGED
@@ -1,6 +1,26 @@
1
1
  import type { BackgroundJSON, Template } from './types';
2
2
  import { makeTextLayer, makeScreen, makeTemplate } from './builders';
3
3
  import { makeDeviceFrameLayers } from './frames';
4
+ import { getDeviceFrame, deviceClassForDeviceId } from './device-frames';
5
+
6
+ /**
7
+ * Editor-unit canvas dimensions for a device, by frame class — so a plan that mixes iPhone, iPad,
8
+ * Android and Mac screens gets the right aspect per screen (multi-device). Matches the app's
9
+ * Page Setup presets (phone 280×608, tablet 450×600, laptop/desktop 608×380). Export scales these
10
+ * up to the real App Store pixel sizes.
11
+ */
12
+ function canvasDimsForDevice(deviceId: string): { width: number; height: number } {
13
+ switch (getDeviceFrame(deviceId)?.category) {
14
+ case 'tablet':
15
+ return { width: 450, height: 600 };
16
+ case 'laptop':
17
+ case 'desktop':
18
+ return { width: 608, height: 380 };
19
+ case 'phone':
20
+ default:
21
+ return { width: 280, height: 608 };
22
+ }
23
+ }
4
24
 
5
25
  /** One screen's worth of plan input (the skill decides these per benefit). */
6
26
  export interface ComposeScreenPlan {
@@ -25,10 +45,15 @@ export interface ComposePlan {
25
45
  * plan (which benefit, copy, device, palette); this turns it into valid DSL.
26
46
  */
27
47
  export function composeTemplate(plan: ComposePlan): Template {
28
- const canvasWidth = plan.canvasWidth ?? 280;
29
- const canvasHeight = plan.canvasHeight ?? 600;
30
-
31
48
  const screens = plan.screens.map((screen) => {
49
+ // Per-screen canvas dims: honor explicit plan-level dims (backward compatible — same as the
50
+ // old single-size behavior), else derive from the screen's device class so a mixed-device
51
+ // plan gets the correct aspect per screen.
52
+ const explicit = plan.canvasWidth != null || plan.canvasHeight != null;
53
+ const { width: canvasWidth, height: canvasHeight } = explicit
54
+ ? { width: plan.canvasWidth ?? 280, height: plan.canvasHeight ?? 600 }
55
+ : canvasDimsForDevice(screen.deviceId);
56
+
32
57
  const { screenshot, frame } = makeDeviceFrameLayers({
33
58
  deviceId: screen.deviceId,
34
59
  screenshotUrl: screen.screenshot.url,
@@ -60,6 +85,9 @@ export function composeTemplate(plan: ComposePlan): Template {
60
85
  background: screen.background,
61
86
  canvasWidth,
62
87
  canvasHeight,
88
+ // Tag the device GROUP (multi-device) so a mixed plan lands as separate sidebar groups in
89
+ // the editor instead of relying on frame inference. Absent ⇒ editor infers it.
90
+ deviceClass: deviceClassForDeviceId(screen.deviceId) ?? undefined,
63
91
  layers: [screenshot, frame, headline]
64
92
  });
65
93
  });
@@ -1,3 +1,5 @@
1
+ import type { DeviceClass } from './types';
2
+
1
3
  // =============================================================================
2
4
  // DEVICE FRAME DEFINITIONS
3
5
  // =============================================================================
@@ -36,7 +38,7 @@ export const deviceFrames: DeviceFrame[] = [
36
38
  name: 'iPhone 16 Pro',
37
39
  platform: 'ios',
38
40
  category: 'phone',
39
- frameAsset: '/devices/iphone-16-pro.png',
41
+ frameAsset: '/devices/iphone-16-pro.webp',
40
42
  imageDimensions: { width: 1406, height: 2822 },
41
43
  // Screen centered within frame: (1406-1212)/2=97, (2822-2618)/2=102
42
44
  screenBounds: { x: 97, y: 100, width: 1212, height: 2624 },
@@ -47,7 +49,7 @@ export const deviceFrames: DeviceFrame[] = [
47
49
  name: 'iPhone 16 Pro Max',
48
50
  platform: 'ios',
49
51
  category: 'phone',
50
- frameAsset: '/devices/iphone-16-pro-max.png',
52
+ frameAsset: '/devices/iphone-16-pro-max.webp',
51
53
  imageDimensions: { width: 1520, height: 3068 },
52
54
  // Screen centered within frame: (1520-1310)/2=105, (3068-2846)/2=111
53
55
  screenBounds: { x: 100, y: 100, width: 1320, height: 2870 },
@@ -58,7 +60,7 @@ export const deviceFrames: DeviceFrame[] = [
58
60
  name: 'iPhone 16',
59
61
  platform: 'ios',
60
62
  category: 'phone',
61
- frameAsset: '/devices/iphone-16.png',
63
+ frameAsset: '/devices/iphone-16.webp',
62
64
  imageDimensions: { width: 1379, height: 2756 },
63
65
  screenBounds: { x: 95, y: 98, width: 1189, height: 2563 },
64
66
  cornerRadius: 118
@@ -68,7 +70,7 @@ export const deviceFrames: DeviceFrame[] = [
68
70
  name: 'iPhone 16 Plus',
69
71
  platform: 'ios',
70
72
  category: 'phone',
71
- frameAsset: '/devices/iphone-16-plus.png',
73
+ frameAsset: '/devices/iphone-16-plus.webp',
72
74
  imageDimensions: { width: 1490, height: 2996 },
73
75
  screenBounds: { x: 103, y: 106, width: 1284, height: 2786 },
74
76
  cornerRadius: 128
@@ -78,7 +80,7 @@ export const deviceFrames: DeviceFrame[] = [
78
80
  name: 'iPhone 17 Pro',
79
81
  platform: 'ios',
80
82
  category: 'phone',
81
- frameAsset: '/devices/iphone-17-pro.png',
83
+ frameAsset: '/devices/iphone-17-pro.webp',
82
84
  imageDimensions: { width: 1406, height: 2822 },
83
85
  screenBounds: { x: 97, y: 100, width: 1212, height: 2624 },
84
86
  cornerRadius: 120
@@ -88,7 +90,7 @@ export const deviceFrames: DeviceFrame[] = [
88
90
  name: 'iPhone 17 Pro Max',
89
91
  platform: 'ios',
90
92
  category: 'phone',
91
- frameAsset: '/devices/iphone-17-pro-max.png',
93
+ frameAsset: '/devices/iphone-17-pro-max.webp',
92
94
  imageDimensions: { width: 1520, height: 3068 },
93
95
  screenBounds: { x: 100, y: 100, width: 1320, height: 2870 },
94
96
  cornerRadius: 140
@@ -98,7 +100,7 @@ export const deviceFrames: DeviceFrame[] = [
98
100
  name: 'iPhone Air',
99
101
  platform: 'ios',
100
102
  category: 'phone',
101
- frameAsset: '/devices/iphone-air.png',
103
+ frameAsset: '/devices/iphone-air.webp',
102
104
  imageDimensions: { width: 1490, height: 2996 },
103
105
  screenBounds: { x: 103, y: 106, width: 1284, height: 2786 },
104
106
  cornerRadius: 128
@@ -112,7 +114,7 @@ export const deviceFrames: DeviceFrame[] = [
112
114
  name: 'Google Pixel 9 Pro',
113
115
  platform: 'android',
114
116
  category: 'phone',
115
- frameAsset: '/devices/pixel-9-pro.png',
117
+ frameAsset: '/devices/pixel-9-pro.webp',
116
118
  imageDimensions: { width: 1620, height: 3136 },
117
119
  screenBounds: { x: 112, y: 111, width: 1396, height: 2916 },
118
120
  cornerRadius: 135
@@ -122,7 +124,7 @@ export const deviceFrames: DeviceFrame[] = [
122
124
  name: 'Google Pixel 9 Pro XL',
123
125
  platform: 'android',
124
126
  category: 'phone',
125
- frameAsset: '/devices/pixel-9-pro-xl.png',
127
+ frameAsset: '/devices/pixel-9-pro-xl.webp',
126
128
  imageDimensions: { width: 1684, height: 3272 },
127
129
  screenBounds: { x: 116, y: 116, width: 1452, height: 3043 },
128
130
  cornerRadius: 140
@@ -136,7 +138,7 @@ export const deviceFrames: DeviceFrame[] = [
136
138
  name: 'iPad Pro 13" (M4)',
137
139
  platform: 'ios',
138
140
  category: 'tablet',
139
- frameAsset: '/devices/ipad-pro-13-m4.png',
141
+ frameAsset: '/devices/ipad-pro-13-m4.webp',
140
142
  imageDimensions: { width: 2264, height: 2952 },
141
143
  screenBounds: { x: 100, y: 100, width: 2064, height: 2752 },
142
144
  cornerRadius: 40
@@ -146,7 +148,7 @@ export const deviceFrames: DeviceFrame[] = [
146
148
  name: 'iPad Pro 11" (M4)',
147
149
  platform: 'ios',
148
150
  category: 'tablet',
149
- frameAsset: '/devices/ipad-pro-11-m4.png',
151
+ frameAsset: '/devices/ipad-pro-11-m4.webp',
150
152
  imageDimensions: { width: 1868, height: 2620 },
151
153
  screenBounds: { x: 100, y: 116, width: 1668, height: 2388 },
152
154
  cornerRadius: 40
@@ -156,7 +158,7 @@ export const deviceFrames: DeviceFrame[] = [
156
158
  name: 'iPad Air 13"',
157
159
  platform: 'ios',
158
160
  category: 'tablet',
159
- frameAsset: '/devices/ipad-air-13.png',
161
+ frameAsset: '/devices/ipad-air-13.webp',
160
162
  imageDimensions: { width: 2248, height: 2932 },
161
163
  screenBounds: { x: 100, y: 100, width: 2048, height: 2732 },
162
164
  cornerRadius: 40
@@ -166,7 +168,7 @@ export const deviceFrames: DeviceFrame[] = [
166
168
  name: 'iPad Air 11"',
167
169
  platform: 'ios',
168
170
  category: 'tablet',
169
- frameAsset: '/devices/ipad-air-11.png',
171
+ frameAsset: '/devices/ipad-air-11.webp',
170
172
  imageDimensions: { width: 1880, height: 2600 },
171
173
  screenBounds: { x: 120, y: 120, width: 1640, height: 2360 },
172
174
  cornerRadius: 40
@@ -176,7 +178,7 @@ export const deviceFrames: DeviceFrame[] = [
176
178
  name: 'iPad mini 7',
177
179
  platform: 'ios',
178
180
  category: 'tablet',
179
- frameAsset: '/devices/ipad-mini-7.png',
181
+ frameAsset: '/devices/ipad-mini-7.webp',
180
182
  imageDimensions: { width: 1888, height: 2666 },
181
183
  screenBounds: { x: 200, y: 200, width: 1488, height: 2266 },
182
184
  cornerRadius: 40
@@ -190,7 +192,7 @@ export const deviceFrames: DeviceFrame[] = [
190
192
  name: 'MacBook Air 13"',
191
193
  platform: 'macos',
192
194
  category: 'laptop',
193
- frameAsset: '/devices/macbook-air-13.png',
195
+ frameAsset: '/devices/macbook-air-13.webp',
194
196
  imageDimensions: { width: 3260, height: 2164 },
195
197
  screenBounds: { x: 350, y: 306, width: 2560, height: 1608 },
196
198
  cornerRadius: 20
@@ -200,7 +202,7 @@ export const deviceFrames: DeviceFrame[] = [
200
202
  name: 'MacBook Air 13" (Menu Bar)',
201
203
  platform: 'macos',
202
204
  category: 'laptop',
203
- frameAsset: '/devices/macbook-air-13-menu-bar.png',
205
+ frameAsset: '/devices/macbook-air-13-menu-bar.webp',
204
206
  imageDimensions: { width: 3260, height: 2164 },
205
207
  screenBounds: { x: 350, y: 312, width: 2560, height: 1602 },
206
208
  cornerRadius: 20
@@ -210,7 +212,7 @@ export const deviceFrames: DeviceFrame[] = [
210
212
  name: 'MacBook Air 15"',
211
213
  platform: 'macos',
212
214
  category: 'laptop',
213
- frameAsset: '/devices/macbook-air-15.png',
215
+ frameAsset: '/devices/macbook-air-15.webp',
214
216
  imageDimensions: { width: 3580, height: 2364 },
215
217
  screenBounds: { x: 350, y: 306, width: 2880, height: 1808 },
216
218
  cornerRadius: 20
@@ -220,7 +222,7 @@ export const deviceFrames: DeviceFrame[] = [
220
222
  name: 'MacBook Air 15" (Menu Bar)',
221
223
  platform: 'macos',
222
224
  category: 'laptop',
223
- frameAsset: '/devices/macbook-air-15-menu-bar.png',
225
+ frameAsset: '/devices/macbook-air-15-menu-bar.webp',
224
226
  imageDimensions: { width: 3580, height: 2364 },
225
227
  screenBounds: { x: 350, y: 308, width: 2880, height: 1806 },
226
228
  cornerRadius: 20
@@ -230,7 +232,7 @@ export const deviceFrames: DeviceFrame[] = [
230
232
  name: 'MacBook Pro 14"',
231
233
  platform: 'macos',
232
234
  category: 'laptop',
233
- frameAsset: '/devices/macbook-pro-14.png',
235
+ frameAsset: '/devices/macbook-pro-14.webp',
234
236
  imageDimensions: { width: 3944, height: 2564 },
235
237
  screenBounds: { x: 461, y: 364, width: 3022, height: 1900 },
236
238
  cornerRadius: 20
@@ -240,7 +242,7 @@ export const deviceFrames: DeviceFrame[] = [
240
242
  name: 'MacBook Pro 14" (Menu Bar)',
241
243
  platform: 'macos',
242
244
  category: 'laptop',
243
- frameAsset: '/devices/macbook-pro-14-menu-bar.png',
245
+ frameAsset: '/devices/macbook-pro-14-menu-bar.webp',
244
246
  imageDimensions: { width: 3824, height: 2564 },
245
247
  screenBounds: { x: 401, y: 374, width: 3022, height: 1890 },
246
248
  cornerRadius: 20
@@ -250,7 +252,7 @@ export const deviceFrames: DeviceFrame[] = [
250
252
  name: 'MacBook Pro 16"',
251
253
  platform: 'macos',
252
254
  category: 'laptop',
253
- frameAsset: '/devices/macbook-pro-16.png',
255
+ frameAsset: '/devices/macbook-pro-16.webp',
254
256
  imageDimensions: { width: 4340, height: 2860 },
255
257
  screenBounds: { x: 442, y: 377, width: 3456, height: 2170 },
256
258
  cornerRadius: 20
@@ -260,7 +262,7 @@ export const deviceFrames: DeviceFrame[] = [
260
262
  name: 'MacBook Pro 16" (Menu Bar)',
261
263
  platform: 'macos',
262
264
  category: 'laptop',
263
- frameAsset: '/devices/macbook-pro-16-menu-bar.png',
265
+ frameAsset: '/devices/macbook-pro-16-menu-bar.webp',
264
266
  imageDimensions: { width: 4340, height: 2860 },
265
267
  screenBounds: { x: 442, y: 389, width: 3456, height: 2158 },
266
268
  cornerRadius: 20
@@ -274,7 +276,7 @@ export const deviceFrames: DeviceFrame[] = [
274
276
  name: 'iMac 24"',
275
277
  platform: 'macos',
276
278
  category: 'desktop',
277
- frameAsset: '/devices/imac-24.png',
279
+ frameAsset: '/devices/imac-24.webp',
278
280
  imageDimensions: { width: 4880, height: 5720 },
279
281
  screenBounds: { x: 200, y: 1600, width: 4480, height: 2520 },
280
282
  cornerRadius: 0
@@ -284,7 +286,7 @@ export const deviceFrames: DeviceFrame[] = [
284
286
  name: 'Studio Display',
285
287
  platform: 'macos',
286
288
  category: 'desktop',
287
- frameAsset: '/devices/studio-display.png',
289
+ frameAsset: '/devices/studio-display.webp',
288
290
  imageDimensions: { width: 5520, height: 4316 },
289
291
  screenBounds: { x: 200, y: 200, width: 5120, height: 2880 },
290
292
  cornerRadius: 0
@@ -294,7 +296,7 @@ export const deviceFrames: DeviceFrame[] = [
294
296
  name: 'Pro Display XDR',
295
297
  platform: 'macos',
296
298
  category: 'desktop',
297
- frameAsset: '/devices/pro-display-xdr.png',
299
+ frameAsset: '/devices/pro-display-xdr.webp',
298
300
  imageDimensions: { width: 6416, height: 4865 },
299
301
  screenBounds: { x: 200, y: 200, width: 6016, height: 3384 },
300
302
  cornerRadius: 0
@@ -316,3 +318,16 @@ export function getDeviceFramesByPlatform(platform: DeviceFrame['platform']): De
316
318
  export function getDeviceFramesByCategory(category: DeviceFrame['category']): DeviceFrame[] {
317
319
  return deviceFrames.filter((d) => d.category === category);
318
320
  }
321
+
322
+ /**
323
+ * Map a device-frame id to its device GROUP (multi-device), using the frame's platform + category.
324
+ * Returns null for an unknown id. Kept in sync with the app's `device-groups.ts` mapping.
325
+ */
326
+ export function deviceClassForDeviceId(deviceId: string): DeviceClass | null {
327
+ const frame = getDeviceFrame(deviceId);
328
+ if (!frame) return null;
329
+ if (frame.platform === 'ios') return frame.category === 'tablet' ? 'ipad_13' : 'iphone_6_9';
330
+ if (frame.platform === 'android') return 'android_phone';
331
+ if (frame.platform === 'macos' || frame.platform === 'windows') return 'macbook';
332
+ return null;
333
+ }
package/src/types.ts CHANGED
@@ -48,11 +48,19 @@ export interface BackgroundJSON {
48
48
  };
49
49
  }
50
50
 
51
+ /**
52
+ * Device GROUP a screen belongs to (multi-device). Mirrors the app's `db.ts` union so a composed
53
+ * template can tag each screen and the editor buckets it into the right sidebar group on import.
54
+ */
55
+ export type DeviceClass = 'iphone_6_9' | 'ipad_13' | 'android_phone' | 'macbook';
56
+
51
57
  /** One screen (canvas) of a design. */
52
58
  export interface ScreenLayersJSON {
53
59
  schemaVersion: number;
54
60
  canvasWidth?: number;
55
61
  canvasHeight?: number;
62
+ /** Device group (multi-device); optional/additive — absent ⇒ the editor infers it. */
63
+ deviceClass?: DeviceClass;
56
64
  layers: LayerJSON[];
57
65
  background?: BackgroundJSON;
58
66
  }