storeshot 0.1.0 → 0.2.0
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/CHANGELOG.md +28 -0
- package/CONTRIBUTING.md +6 -0
- package/README.md +36 -8
- package/THIRD_PARTY_LICENSES.txt +29 -0
- package/THIRD_PARTY_NOTICES.md +28 -3
- package/dist/chunk-I7JOE7XJ.js +299 -0
- package/dist/chunk-I7JOE7XJ.js.map +1 -0
- package/dist/cli.js +669 -342
- package/dist/cli.js.map +1 -1
- package/dist/node-renderer-FMVPIQAG.js +517 -0
- package/dist/node-renderer-FMVPIQAG.js.map +1 -0
- package/dist/ui/artwork/cc0-laurels/LICENSE +121 -0
- package/dist/ui/artwork/cc0-laurels/README.md +16 -0
- package/dist/ui/artwork/cc0-laurels/laurel-branch.svg +95 -0
- package/dist/ui/artwork/cc0-laurels/laurel-pair-wide.svg +88 -0
- package/dist/ui/artwork/cc0-laurels/laurel-wreath-classic.svg +45 -0
- package/dist/ui/artwork/cc0-laurels/laurel-wreath-natural.svg +75 -0
- package/dist/ui/assets/AssetsView-FCqaKK4x.js +1 -0
- package/dist/ui/assets/SetEditor-BxiRSHKv.js +440 -0
- package/dist/ui/assets/{index-DPDaBFld.js → index-BrRqpj-U.js} +2 -2
- package/dist/ui/assets/index-KYX154p7.css +2 -0
- package/dist/ui/assets/{shared-BiRg4I01.js → shared-stFaxZuu.js} +1 -1
- package/dist/ui/index.html +2 -2
- package/docs/cli.md +141 -0
- package/docs/images/storeshot-editor.png +0 -0
- package/docs/releasing.md +80 -122
- package/fonts/Geist-Variable.ttf +0 -0
- package/package.json +3 -1
- package/dist/ui/assets/AssetsView-D0lbK5rm.js +0 -1
- package/dist/ui/assets/SetEditor-BBUlgGT0.js +0 -440
- package/dist/ui/assets/index-DfZBqsD3.css +0 -2
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,34 @@ All notable changes to StoreShot will be documented in this file. The format is
|
|
|
4
4
|
based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and versioning
|
|
5
5
|
follows [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
6
6
|
|
|
7
|
+
## [0.2.0](https://github.com/mikkokut/storeshot/compare/v0.1.0...v0.2.0) (2026-07-16)
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
### ⚠ BREAKING CHANGES
|
|
11
|
+
|
|
12
|
+
* remove separate logos asset category
|
|
13
|
+
|
|
14
|
+
### Features
|
|
15
|
+
|
|
16
|
+
* **cli:** add agent-driven screenshot workflow ([4b587e5](https://github.com/mikkokut/storeshot/commit/4b587e5f579b29fa23ce5d2c4027952fb79fa683))
|
|
17
|
+
* **editor:** add CC0 laurel artwork ([9566809](https://github.com/mikkokut/storeshot/commit/9566809daba5f31cc276240fad546b6f05411304))
|
|
18
|
+
* **editor:** add layer flipping ([#2](https://github.com/mikkokut/storeshot/issues/2)) ([782db49](https://github.com/mikkokut/storeshot/commit/782db49bfc6cf5ab5587c08f3485e33aacf4f0b6))
|
|
19
|
+
* **editor:** filter image assets by category ([cddfe11](https://github.com/mikkokut/storeshot/commit/cddfe11810638e424f6ca0dc6d2c34962c19ee2f))
|
|
20
|
+
* **editor:** paste images and text onto canvas ([#3](https://github.com/mikkokut/storeshot/issues/3)) ([c1c5676](https://github.com/mikkokut/storeshot/commit/c1c56766ed8fe3f0e36ea9d8538f048001f30891))
|
|
21
|
+
* **ui:** show StoreShot in project header ([7cab54d](https://github.com/mikkokut/storeshot/commit/7cab54d12032fceb443df16ee44ec95c36e422f2))
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
### Bug Fixes
|
|
25
|
+
|
|
26
|
+
* **editor:** flip wide laurel pair branches ([7ceb4dc](https://github.com/mikkokut/storeshot/commit/7ceb4dc6fb69fa3870cf6ddbcba5eac04f40d1a9))
|
|
27
|
+
* **editor:** tighten wide laurel pair ([7221ef0](https://github.com/mikkokut/storeshot/commit/7221ef0a15af8f3ab311ab703170297ab02bfb2c))
|
|
28
|
+
* **ui:** refresh text metrics after font load ([6a04bf6](https://github.com/mikkokut/storeshot/commit/6a04bf6fc5bc9ed408e5052cf48033735dd8bd63))
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
### Code Refactoring
|
|
32
|
+
|
|
33
|
+
* remove separate logos asset category ([2783a80](https://github.com/mikkokut/storeshot/commit/2783a805f18026b86ce8a296b75724d8b358d7e9))
|
|
34
|
+
|
|
7
35
|
## [Unreleased]
|
|
8
36
|
|
|
9
37
|
## [0.1.0] - 2026-07-16
|
package/CONTRIBUTING.md
CHANGED
|
@@ -71,5 +71,11 @@ For UI changes, also exercise the affected workflow in the browser. Keep commits
|
|
|
71
71
|
focused, explain user-visible behavior in the pull request, and call out project
|
|
72
72
|
format changes explicitly.
|
|
73
73
|
|
|
74
|
+
Use a [Conventional Commit](https://www.conventionalcommits.org/) title for the
|
|
75
|
+
commit that reaches `main`, normally the squash-merge title. Use `fix:` for bug
|
|
76
|
+
fixes, `feat:` for features, and `feat!:` or a `BREAKING CHANGE:` footer for a
|
|
77
|
+
breaking change. Release Please uses that title to propose the next version and
|
|
78
|
+
generate the changelog; see the [release guide](docs/releasing.md) for details.
|
|
79
|
+
|
|
74
80
|
By submitting a contribution, you agree that it may be distributed under the
|
|
75
81
|
project's MIT License.
|
package/README.md
CHANGED
|
@@ -5,13 +5,15 @@ App Store screenshots. Run it in a folder, edit screenshot sets in the browser,
|
|
|
5
5
|
and keep the complete project as ordinary assets and JSON files that humans,
|
|
6
6
|
scripts, and AI coding agents can all inspect and change.
|
|
7
7
|
|
|
8
|
-
> **Pre-release:** StoreShot is
|
|
9
|
-
>
|
|
10
|
-
>
|
|
8
|
+
> **Pre-release:** StoreShot is under active development, changes quickly, and
|
|
9
|
+
> does not yet promise backward compatibility. It supports Apple App Store
|
|
10
|
+
> screenshots today; Google Play support is planned.
|
|
11
11
|
|
|
12
12
|
StoreShot is already used in production for the iOS app
|
|
13
13
|
[Lokimaps](https://www.lokimaps.com/).
|
|
14
14
|
|
|
15
|
+

|
|
16
|
+
|
|
15
17
|
## Why StoreShot?
|
|
16
18
|
|
|
17
19
|
Many existing screenshot tools were designed as closed, manual design silos.
|
|
@@ -25,13 +27,15 @@ database, hosted project, or remote backend.
|
|
|
25
27
|
|
|
26
28
|
## What it can do
|
|
27
29
|
|
|
28
|
-
- Organize raw screenshots, brand
|
|
30
|
+
- Organize raw screenshots, brand assets, and other reusable imagery.
|
|
29
31
|
- Create ordered screenshot sets for a locale and Apple device family.
|
|
30
32
|
- Arrange text, shapes, images, recolorable artwork, and device mockups on a
|
|
31
33
|
free-form canvas.
|
|
32
34
|
- Use undo/redo, copy/paste, alignment guides, layers, keyboard movement, and
|
|
33
35
|
canvas zoom.
|
|
34
36
|
- Export an entire set as full-resolution PNG files in a ZIP archive.
|
|
37
|
+
- Inspect, validate, edit, and render projects from the CLI with stable JSON
|
|
38
|
+
output for scripts and AI agents.
|
|
35
39
|
- Import portable device-mockup bundles with explicit frame geometry and license
|
|
36
40
|
metadata.
|
|
37
41
|
- Persist everything inside the selected project directory.
|
|
@@ -43,6 +47,7 @@ StoreShot requires Node.js 22.12 or newer.
|
|
|
43
47
|
```bash
|
|
44
48
|
npm install --global storeshot
|
|
45
49
|
mkdir app-store-screenshots
|
|
50
|
+
storeshot init app-store-screenshots --app-name "Example App" --platform ios
|
|
46
51
|
storeshot dev app-store-screenshots
|
|
47
52
|
```
|
|
48
53
|
|
|
@@ -56,6 +61,26 @@ storeshot dev ./app-store-screenshots --port 4174 --no-open
|
|
|
56
61
|
The default server is bound to the loopback interface. Be deliberate when using
|
|
57
62
|
`--host` to expose it to another interface.
|
|
58
63
|
|
|
64
|
+
## CLI and AI agents
|
|
65
|
+
|
|
66
|
+
StoreShot supports the full screenshot workflow without requiring browser
|
|
67
|
+
automation. Commands can inspect the project, create and update sets, manage
|
|
68
|
+
assets, validate direct JSON edits, and render real PNG previews or final
|
|
69
|
+
output. Global `-C/--project` selects the project and `--json` provides stable
|
|
70
|
+
machine-readable results.
|
|
71
|
+
|
|
72
|
+
```bash
|
|
73
|
+
storeshot -C ./app-store-screenshots status --json
|
|
74
|
+
storeshot -C ./app-store-screenshots validate --json
|
|
75
|
+
storeshot -C ./app-store-screenshots set show <set-id>
|
|
76
|
+
storeshot -C ./app-store-screenshots render --set <set-id> --scale 0.25 --json
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
When `storeshot dev` is open, CLI and direct filesystem changes are streamed to
|
|
80
|
+
the browser so the selected set and preview update in realtime. See the
|
|
81
|
+
[CLI and agent workflow](docs/cli.md) for the complete command surface and a
|
|
82
|
+
recommended inspect-edit-validate-render loop.
|
|
83
|
+
|
|
59
84
|
## Project format
|
|
60
85
|
|
|
61
86
|
StoreShot creates a transparent, portable project structure:
|
|
@@ -66,7 +91,6 @@ app-store-screenshots/
|
|
|
66
91
|
├── assets/
|
|
67
92
|
│ ├── screenshots/
|
|
68
93
|
│ ├── brand/
|
|
69
|
-
│ ├── logos/
|
|
70
94
|
│ └── other/
|
|
71
95
|
├── mockup-bundles/
|
|
72
96
|
│ └── my-device-frames/
|
|
@@ -78,13 +102,17 @@ app-store-screenshots/
|
|
|
78
102
|
└── finnish-iphone-e5f6g7h8.json
|
|
79
103
|
```
|
|
80
104
|
|
|
105
|
+
CLI renders are written to `renders/<set-id>/` by default. They are generated
|
|
106
|
+
output; teams can commit or ignore them according to their release workflow.
|
|
107
|
+
|
|
81
108
|
Each set stores its locale, device, output dimensions, ordered screenshots, and
|
|
82
109
|
canvas layers. StoreShot only serves files inside the selected project boundary.
|
|
83
110
|
Project assets and imported bundles remain subject to their own licenses.
|
|
84
111
|
|
|
85
|
-
The editor
|
|
86
|
-
The project itself remains local, but
|
|
87
|
-
request to Bunny Fonts.
|
|
112
|
+
The editor and CLI renderer load fonts from [Bunny Fonts](https://fonts.bunny.net/)
|
|
113
|
+
on demand. The project itself remains local, but the first browser or CLI render
|
|
114
|
+
of a hosted font requires a network request to Bunny Fonts. The CLI caches the
|
|
115
|
+
downloaded font files in the operating system's temporary directory.
|
|
88
116
|
|
|
89
117
|
## Device mockups
|
|
90
118
|
|
package/THIRD_PARTY_LICENSES.txt
CHANGED
|
@@ -348,6 +348,35 @@ The above copyright notice and this permission notice shall be included in all c
|
|
|
348
348
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
|
349
349
|
|
|
350
350
|
|
|
351
|
+
--------------------------------------------------------------------------------
|
|
352
|
+
canvas 3.2.3
|
|
353
|
+
Declared license: MIT
|
|
354
|
+
--------------------------------------------------------------------------------
|
|
355
|
+
|
|
356
|
+
(The MIT License)
|
|
357
|
+
|
|
358
|
+
Copyright (c) 2010 LearnBoost, and contributors <dev@learnboost.com>
|
|
359
|
+
|
|
360
|
+
Copyright (c) 2014 Automattic, Inc and contributors <dev@automattic.com>
|
|
361
|
+
|
|
362
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy of
|
|
363
|
+
this software and associated documentation files (the 'Software'), to deal in
|
|
364
|
+
the Software without restriction, including without limitation the rights to
|
|
365
|
+
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
|
|
366
|
+
the Software, and to permit persons to whom the Software is furnished to do so,
|
|
367
|
+
subject to the following conditions:
|
|
368
|
+
|
|
369
|
+
The above copyright notice and this permission notice shall be included in all
|
|
370
|
+
copies or substantial portions of the Software.
|
|
371
|
+
|
|
372
|
+
THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
373
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
|
374
|
+
FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
|
375
|
+
COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
|
376
|
+
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
|
377
|
+
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
|
378
|
+
|
|
379
|
+
|
|
351
380
|
--------------------------------------------------------------------------------
|
|
352
381
|
commander 15.0.0
|
|
353
382
|
Declared license: MIT
|
package/THIRD_PARTY_NOTICES.md
CHANGED
|
@@ -29,16 +29,41 @@ Paweł Kuna, under the MIT License.
|
|
|
29
29
|
The selected source files, upstream revision, attribution, and complete license
|
|
30
30
|
are retained in `src/ui/public/artwork/tabler/` and shipped with the browser UI.
|
|
31
31
|
|
|
32
|
+
### CC0 laurel artwork
|
|
33
|
+
|
|
34
|
+
StoreShot includes three laurel vectors obtained from Wikimedia Commons, plus a
|
|
35
|
+
wide pair derived from the single branch. They are dedicated to the public
|
|
36
|
+
domain under CC0 1.0:
|
|
37
|
+
|
|
38
|
+
- [Steren-Laurel.svg](https://commons.wikimedia.org/wiki/File:Steren-Laurel.svg)
|
|
39
|
+
by Steren
|
|
40
|
+
- [Greek Roman Laurel wreath vector.svg](https://commons.wikimedia.org/wiki/File:Greek_Roman_Laurel_wreath_vector.svg)
|
|
41
|
+
by Dalovar
|
|
42
|
+
- [Laurel-right.svg](https://commons.wikimedia.org/wiki/File:Laurel-right.svg)
|
|
43
|
+
by Leki, based on work by Indolences; also used for StoreShot's wide pair
|
|
44
|
+
|
|
45
|
+
The source links, authorship, and complete CC0 1.0 legal text are retained in
|
|
46
|
+
`src/ui/public/artwork/cc0-laurels/` and shipped with the browser UI.
|
|
47
|
+
|
|
32
48
|
### Geist
|
|
33
49
|
|
|
34
50
|
StoreShot self-hosts the Geist variable font through
|
|
35
|
-
[`@fontsource-variable/geist`](https://fontsource.org/fonts/geist)
|
|
36
|
-
|
|
37
|
-
|
|
51
|
+
[`@fontsource-variable/geist`](https://fontsource.org/fonts/geist) in the UI and
|
|
52
|
+
the official [Geist font repository](https://github.com/vercel/geist-font) in
|
|
53
|
+
the CLI renderer. Geist is Copyright 2024 The Geist Project Authors and licensed
|
|
54
|
+
under the SIL Open Font License 1.1.
|
|
38
55
|
|
|
39
56
|
The complete OFL text is shipped at
|
|
40
57
|
`src/ui/public/licenses/geist-OFL-1.1.txt`.
|
|
41
58
|
|
|
59
|
+
### node-canvas
|
|
60
|
+
|
|
61
|
+
CLI PNG rendering uses
|
|
62
|
+
[`canvas`](https://github.com/Automattic/node-canvas), Copyright (c) 2010
|
|
63
|
+
LearnBoost and contributors and Copyright (c) 2014 Automattic, Inc and
|
|
64
|
+
contributors, under the MIT License. The upstream license text is reproduced in
|
|
65
|
+
`THIRD_PARTY_LICENSES.txt`.
|
|
66
|
+
|
|
42
67
|
## Hosted fonts
|
|
43
68
|
|
|
44
69
|
The editor can fetch a font catalog and selected font files from
|
|
@@ -0,0 +1,299 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// src/shared.ts
|
|
4
|
+
var ASSET_CATEGORIES = ["screenshots", "brand", "other"];
|
|
5
|
+
var SCREENSHOT_DEVICE_TYPES = ["iphone", "ipad", "mac", "watch"];
|
|
6
|
+
var DEFAULT_TEXT_LINE_HEIGHT_RATIO = 1.05;
|
|
7
|
+
|
|
8
|
+
// src/device-mockups.ts
|
|
9
|
+
var MOCKUP_BUNDLE_FILENAME = "storeshot-mockups.json";
|
|
10
|
+
var MOCKUP_BUNDLE_FORMAT = "storeshot-mockup-bundle";
|
|
11
|
+
var identifierPattern = /^[a-z0-9][a-z0-9-]*$/;
|
|
12
|
+
var styles = /* @__PURE__ */ new Set([
|
|
13
|
+
"3d",
|
|
14
|
+
"colored",
|
|
15
|
+
"colored-3d",
|
|
16
|
+
"handheld",
|
|
17
|
+
"handheld-dim",
|
|
18
|
+
"handheld-silhouette",
|
|
19
|
+
"handheld-styles",
|
|
20
|
+
"standard",
|
|
21
|
+
"textured"
|
|
22
|
+
]);
|
|
23
|
+
var platforms = /* @__PURE__ */ new Set(["iphone", "ipad", "mac", "watch"]);
|
|
24
|
+
var frameExtensions = /* @__PURE__ */ new Set([".jpeg", ".jpg", ".png", ".svg", ".webp"]);
|
|
25
|
+
function emptyDeviceMockupCatalog() {
|
|
26
|
+
return { bundles: [], groups: [], mockups: [] };
|
|
27
|
+
}
|
|
28
|
+
function parseMockupBundleManifest(value) {
|
|
29
|
+
if (!isRecord(value)) throw new Error(`${MOCKUP_BUNDLE_FILENAME} must contain a JSON object`);
|
|
30
|
+
if (value.format !== MOCKUP_BUNDLE_FORMAT || value.version !== 1) {
|
|
31
|
+
throw new Error("Unsupported StoreShot mockup bundle format");
|
|
32
|
+
}
|
|
33
|
+
if (!Array.isArray(value.mockups) || value.mockups.length === 0) {
|
|
34
|
+
throw new Error("A mockup bundle must contain at least one mockup");
|
|
35
|
+
}
|
|
36
|
+
const id = readIdentifier(value.id, "Bundle id");
|
|
37
|
+
const mockups = value.mockups.map(parseMockupEntry);
|
|
38
|
+
const entryIds = /* @__PURE__ */ new Set();
|
|
39
|
+
for (const mockup of mockups) {
|
|
40
|
+
if (entryIds.has(mockup.id)) throw new Error(`Duplicate mockup id: ${mockup.id}`);
|
|
41
|
+
entryIds.add(mockup.id);
|
|
42
|
+
}
|
|
43
|
+
return {
|
|
44
|
+
format: MOCKUP_BUNDLE_FORMAT,
|
|
45
|
+
version: 1,
|
|
46
|
+
id,
|
|
47
|
+
name: readString(value.name, "Bundle name"),
|
|
48
|
+
author: readString(value.author, "Bundle author"),
|
|
49
|
+
license: parseLicense(value.license),
|
|
50
|
+
...value.source === void 0 ? {} : { source: parseSource(value.source) },
|
|
51
|
+
mockups
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
function resolveMockupBundle(manifest, baseUrl, origin) {
|
|
55
|
+
const bundlePrefix = `${manifest.id}/`;
|
|
56
|
+
const mockups = manifest.mockups.map((entry) => ({
|
|
57
|
+
...entry,
|
|
58
|
+
id: `${bundlePrefix}${entry.id}`,
|
|
59
|
+
bundleId: manifest.id,
|
|
60
|
+
groupId: `${bundlePrefix}${entry.groupId}`,
|
|
61
|
+
frameUrl: `${baseUrl}${encodeBundlePath(entry.frame)}`,
|
|
62
|
+
thumbnailUrl: `${baseUrl}${encodeBundlePath(entry.thumbnail ?? entry.frame)}`
|
|
63
|
+
}));
|
|
64
|
+
const groupEntries = /* @__PURE__ */ new Map();
|
|
65
|
+
for (const mockup of mockups) {
|
|
66
|
+
const entries = groupEntries.get(mockup.groupId) ?? [];
|
|
67
|
+
entries.push(mockup);
|
|
68
|
+
groupEntries.set(mockup.groupId, entries);
|
|
69
|
+
}
|
|
70
|
+
return {
|
|
71
|
+
bundles: [{
|
|
72
|
+
id: manifest.id,
|
|
73
|
+
name: manifest.name,
|
|
74
|
+
author: manifest.author,
|
|
75
|
+
license: manifest.license,
|
|
76
|
+
...manifest.source ? { source: manifest.source } : {},
|
|
77
|
+
mockupCount: mockups.length,
|
|
78
|
+
origin
|
|
79
|
+
}],
|
|
80
|
+
groups: [...groupEntries.entries()].map(([id, entries]) => ({
|
|
81
|
+
id,
|
|
82
|
+
bundleId: manifest.id,
|
|
83
|
+
name: manifest.mockups.find((entry) => `${bundlePrefix}${entry.groupId}` === id)?.groupName ?? entries[0].name,
|
|
84
|
+
platform: entries[0].platform,
|
|
85
|
+
style: entries[0].style,
|
|
86
|
+
thumbnailUrl: entries[0].thumbnailUrl,
|
|
87
|
+
count: entries.length
|
|
88
|
+
})),
|
|
89
|
+
mockups
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
function mergeDeviceMockupCatalogs(...catalogs) {
|
|
93
|
+
const result = emptyDeviceMockupCatalog();
|
|
94
|
+
const bundleIds = /* @__PURE__ */ new Set();
|
|
95
|
+
const groupIds = /* @__PURE__ */ new Set();
|
|
96
|
+
const mockupIds = /* @__PURE__ */ new Set();
|
|
97
|
+
for (const catalog of catalogs) {
|
|
98
|
+
for (const bundle of catalog.bundles) {
|
|
99
|
+
if (bundleIds.has(bundle.id)) continue;
|
|
100
|
+
bundleIds.add(bundle.id);
|
|
101
|
+
result.bundles.push(bundle);
|
|
102
|
+
}
|
|
103
|
+
for (const group of catalog.groups) {
|
|
104
|
+
if (groupIds.has(group.id)) continue;
|
|
105
|
+
groupIds.add(group.id);
|
|
106
|
+
result.groups.push(group);
|
|
107
|
+
}
|
|
108
|
+
for (const mockup of catalog.mockups) {
|
|
109
|
+
if (mockupIds.has(mockup.id)) continue;
|
|
110
|
+
mockupIds.add(mockup.id);
|
|
111
|
+
result.mockups.push(mockup);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
return result;
|
|
115
|
+
}
|
|
116
|
+
function bundleAssetPaths(manifest) {
|
|
117
|
+
const paths = /* @__PURE__ */ new Set();
|
|
118
|
+
if (manifest.license.file) paths.add(manifest.license.file);
|
|
119
|
+
for (const mockup of manifest.mockups) {
|
|
120
|
+
paths.add(mockup.frame);
|
|
121
|
+
if (mockup.thumbnail) paths.add(mockup.thumbnail);
|
|
122
|
+
}
|
|
123
|
+
return [...paths];
|
|
124
|
+
}
|
|
125
|
+
function isSafeBundlePath(value) {
|
|
126
|
+
if (!value || value.startsWith("/") || value.includes("\\")) return false;
|
|
127
|
+
const parts = value.split("/");
|
|
128
|
+
return parts.every((part) => part.length > 0 && part !== "." && part !== "..");
|
|
129
|
+
}
|
|
130
|
+
function parseMockupEntry(value) {
|
|
131
|
+
if (!isRecord(value)) throw new Error("Mockup entry must be an object");
|
|
132
|
+
const platform = readString(value.platform, "Mockup platform");
|
|
133
|
+
const style = readString(value.style, "Mockup style");
|
|
134
|
+
if (!platforms.has(platform)) throw new Error(`Unsupported mockup platform: ${platform}`);
|
|
135
|
+
if (!styles.has(style)) throw new Error(`Unsupported mockup style: ${style}`);
|
|
136
|
+
const frame = readFramePath(value.frame, "Mockup frame");
|
|
137
|
+
const thumbnail = value.thumbnail === void 0 ? void 0 : readFramePath(value.thumbnail, "Mockup thumbnail");
|
|
138
|
+
return {
|
|
139
|
+
id: readIdentifier(value.id, "Mockup id"),
|
|
140
|
+
groupId: readIdentifier(value.groupId, "Mockup group id"),
|
|
141
|
+
groupName: readString(value.groupName, "Mockup group name"),
|
|
142
|
+
name: readString(value.name, "Mockup name"),
|
|
143
|
+
description: readString(value.description, "Mockup description"),
|
|
144
|
+
platform,
|
|
145
|
+
style,
|
|
146
|
+
frame,
|
|
147
|
+
...thumbnail ? { thumbnail } : {},
|
|
148
|
+
width: readDimension(value.width, "Mockup width"),
|
|
149
|
+
height: readDimension(value.height, "Mockup height"),
|
|
150
|
+
screen: parseScreen(value.screen)
|
|
151
|
+
};
|
|
152
|
+
}
|
|
153
|
+
function parseScreen(value) {
|
|
154
|
+
if (!isRecord(value)) throw new Error("Mockup screen must be an object");
|
|
155
|
+
if (value.kind === "rect") {
|
|
156
|
+
return {
|
|
157
|
+
kind: "rect",
|
|
158
|
+
x: readNumber(value.x, "Screen x"),
|
|
159
|
+
y: readNumber(value.y, "Screen y"),
|
|
160
|
+
width: readDimension(value.width, "Screen width"),
|
|
161
|
+
height: readDimension(value.height, "Screen height"),
|
|
162
|
+
cornerRadius: readNonNegative(value.cornerRadius, "Screen corner radius")
|
|
163
|
+
};
|
|
164
|
+
}
|
|
165
|
+
if (value.kind === "projective") {
|
|
166
|
+
if (!Array.isArray(value.transform) || value.transform.length !== 3) throw new Error("Projective transform must have three rows");
|
|
167
|
+
const transform = value.transform.map((row) => {
|
|
168
|
+
if (!Array.isArray(row) || row.length !== 3) throw new Error("Each projective transform row must have three numbers");
|
|
169
|
+
return row.map((entry) => readNumber(entry, "Projective transform value"));
|
|
170
|
+
});
|
|
171
|
+
if (!isRecord(value.sourceCornerRadius)) throw new Error("Projective source corner radius must be an object");
|
|
172
|
+
return {
|
|
173
|
+
kind: "projective",
|
|
174
|
+
transform,
|
|
175
|
+
sourceCornerRadius: {
|
|
176
|
+
x: readNonNegative(value.sourceCornerRadius.x, "Source corner radius x"),
|
|
177
|
+
y: readNonNegative(value.sourceCornerRadius.y, "Source corner radius y")
|
|
178
|
+
}
|
|
179
|
+
};
|
|
180
|
+
}
|
|
181
|
+
throw new Error("Unsupported mockup screen geometry");
|
|
182
|
+
}
|
|
183
|
+
function parseLicense(value) {
|
|
184
|
+
if (!isRecord(value)) throw new Error("Bundle license must be an object");
|
|
185
|
+
const file = value.file === void 0 ? void 0 : readBundlePath(value.file, "License file");
|
|
186
|
+
return {
|
|
187
|
+
name: readString(value.name, "License name"),
|
|
188
|
+
...value.url === void 0 ? {} : { url: readString(value.url, "License URL") },
|
|
189
|
+
...file ? { file } : {}
|
|
190
|
+
};
|
|
191
|
+
}
|
|
192
|
+
function parseSource(value) {
|
|
193
|
+
if (!isRecord(value)) throw new Error("Bundle source must be an object");
|
|
194
|
+
return {
|
|
195
|
+
...value.name === void 0 ? {} : { name: readString(value.name, "Source name") },
|
|
196
|
+
url: readString(value.url, "Source URL"),
|
|
197
|
+
...value.revision === void 0 ? {} : { revision: readString(value.revision, "Source revision") }
|
|
198
|
+
};
|
|
199
|
+
}
|
|
200
|
+
function readFramePath(value, name) {
|
|
201
|
+
const result = readBundlePath(value, name);
|
|
202
|
+
const extensionIndex = result.lastIndexOf(".");
|
|
203
|
+
const extension = extensionIndex >= 0 ? result.slice(extensionIndex).toLowerCase() : "";
|
|
204
|
+
if (!frameExtensions.has(extension)) throw new Error(`${name} has an unsupported file type`);
|
|
205
|
+
return result;
|
|
206
|
+
}
|
|
207
|
+
function readBundlePath(value, name) {
|
|
208
|
+
const result = readString(value, name);
|
|
209
|
+
if (!isSafeBundlePath(result)) throw new Error(`${name} must be a relative path inside the bundle`);
|
|
210
|
+
return result;
|
|
211
|
+
}
|
|
212
|
+
function readIdentifier(value, name) {
|
|
213
|
+
const result = readString(value, name);
|
|
214
|
+
if (!identifierPattern.test(result)) throw new Error(`${name} must use lowercase letters, numbers, and hyphens`);
|
|
215
|
+
return result;
|
|
216
|
+
}
|
|
217
|
+
function readString(value, name) {
|
|
218
|
+
const result = typeof value === "string" ? value.trim() : "";
|
|
219
|
+
if (!result) throw new Error(`${name} must be a non-empty string`);
|
|
220
|
+
return result;
|
|
221
|
+
}
|
|
222
|
+
function readNumber(value, name) {
|
|
223
|
+
if (typeof value !== "number" || !Number.isFinite(value)) throw new Error(`${name} must be a number`);
|
|
224
|
+
return value;
|
|
225
|
+
}
|
|
226
|
+
function readDimension(value, name) {
|
|
227
|
+
const result = readNumber(value, name);
|
|
228
|
+
if (result <= 0 || result > 2e4) throw new Error(`${name} is outside the supported range`);
|
|
229
|
+
return result;
|
|
230
|
+
}
|
|
231
|
+
function readNonNegative(value, name) {
|
|
232
|
+
const result = readNumber(value, name);
|
|
233
|
+
if (result < 0 || result > 2e4) throw new Error(`${name} is outside the supported range`);
|
|
234
|
+
return result;
|
|
235
|
+
}
|
|
236
|
+
function encodeBundlePath(value) {
|
|
237
|
+
return value.split("/").map(encodeURIComponent).join("/");
|
|
238
|
+
}
|
|
239
|
+
function isRecord(value) {
|
|
240
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
// src/artwork.ts
|
|
244
|
+
var TABLER_ATTRIBUTION = "Tabler Icons";
|
|
245
|
+
var WIKIMEDIA_COMMONS_ATTRIBUTION = "Wikimedia Commons";
|
|
246
|
+
var BUILT_IN_ARTWORK = [
|
|
247
|
+
{ id: "tabler-grid-pattern", name: "Grid pattern", category: "decor", url: "/artwork/tabler/filled/grid-pattern.svg", width: 24, height: 24, attribution: TABLER_ATTRIBUTION, license: "MIT" },
|
|
248
|
+
{ id: "tabler-sparkles", name: "Sparkles", category: "decor", url: "/artwork/tabler/filled/sparkles.svg", width: 24, height: 24, attribution: TABLER_ATTRIBUTION, license: "MIT" },
|
|
249
|
+
{ id: "tabler-stars", name: "Stars", category: "decor", url: "/artwork/tabler/filled/stars.svg", width: 24, height: 24, attribution: TABLER_ATTRIBUTION, license: "MIT" },
|
|
250
|
+
{ id: "tabler-scribble", name: "Scribble", category: "lines", url: "/artwork/tabler/outline/scribble.svg", width: 24, height: 24, attribution: TABLER_ATTRIBUTION, license: "MIT" },
|
|
251
|
+
{ id: "tabler-underline", name: "Underline", category: "lines", url: "/artwork/tabler/outline/underline.svg", width: 24, height: 24, attribution: TABLER_ATTRIBUTION, license: "MIT" },
|
|
252
|
+
{ id: "tabler-laurel-wreath", name: "Laurel wreath", category: "laurels", url: "/artwork/tabler/filled/laurel-wreath.svg", width: 24, height: 24, attribution: TABLER_ATTRIBUTION, license: "MIT" },
|
|
253
|
+
{ id: "tabler-laurel-wreath-1", name: "Laurel wreath 1", category: "laurels", url: "/artwork/tabler/filled/laurel-wreath-1.svg", width: 24, height: 24, attribution: TABLER_ATTRIBUTION, license: "MIT" },
|
|
254
|
+
{ id: "tabler-laurel-wreath-2", name: "Laurel wreath 2", category: "laurels", url: "/artwork/tabler/filled/laurel-wreath-2.svg", width: 24, height: 24, attribution: TABLER_ATTRIBUTION, license: "MIT" },
|
|
255
|
+
{ id: "tabler-laurel-wreath-3", name: "Laurel wreath 3", category: "laurels", url: "/artwork/tabler/filled/laurel-wreath-3.svg", width: 24, height: 24, attribution: TABLER_ATTRIBUTION, license: "MIT" },
|
|
256
|
+
{ id: "cc0-laurel-wreath-classic", name: "Classic laurel wreath", category: "laurels", url: "/artwork/cc0-laurels/laurel-wreath-classic.svg", width: 564.23, height: 574.03, attribution: WIKIMEDIA_COMMONS_ATTRIBUTION, license: "CC0-1.0" },
|
|
257
|
+
{ id: "cc0-laurel-wreath-natural", name: "Natural laurel wreath", category: "laurels", url: "/artwork/cc0-laurels/laurel-wreath-natural.svg", width: 595.3, height: 519.9, attribution: WIKIMEDIA_COMMONS_ATTRIBUTION, license: "CC0-1.0" },
|
|
258
|
+
{ id: "cc0-laurel-branch", name: "Laurel branch", category: "laurels", url: "/artwork/cc0-laurels/laurel-branch.svg", width: 244.14168, height: 307.26953, attribution: WIKIMEDIA_COMMONS_ATTRIBUTION, license: "CC0-1.0" },
|
|
259
|
+
{ id: "cc0-laurel-pair-wide", name: "Wide laurel pair", category: "laurels", url: "/artwork/cc0-laurels/laurel-pair-wide.svg", width: 555, height: 288, attribution: WIKIMEDIA_COMMONS_ATTRIBUTION, license: "CC0-1.0" }
|
|
260
|
+
];
|
|
261
|
+
var artworkById = new Map(BUILT_IN_ARTWORK.map((artwork) => [artwork.id, artwork]));
|
|
262
|
+
function builtInArtworkById(id) {
|
|
263
|
+
return artworkById.get(id);
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
// src/package-assets.ts
|
|
267
|
+
import { access } from "fs/promises";
|
|
268
|
+
import path from "path";
|
|
269
|
+
async function resolvePackagePublicDirectory(packageRoot) {
|
|
270
|
+
const candidates = [
|
|
271
|
+
path.join(packageRoot, "dist/ui"),
|
|
272
|
+
path.join(packageRoot, "src/ui/public")
|
|
273
|
+
];
|
|
274
|
+
for (const candidate of candidates) {
|
|
275
|
+
try {
|
|
276
|
+
await access(candidate);
|
|
277
|
+
return candidate;
|
|
278
|
+
} catch {
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
throw new Error("StoreShot's bundled rendering assets could not be found");
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
export {
|
|
285
|
+
ASSET_CATEGORIES,
|
|
286
|
+
SCREENSHOT_DEVICE_TYPES,
|
|
287
|
+
DEFAULT_TEXT_LINE_HEIGHT_RATIO,
|
|
288
|
+
MOCKUP_BUNDLE_FILENAME,
|
|
289
|
+
emptyDeviceMockupCatalog,
|
|
290
|
+
parseMockupBundleManifest,
|
|
291
|
+
resolveMockupBundle,
|
|
292
|
+
mergeDeviceMockupCatalogs,
|
|
293
|
+
bundleAssetPaths,
|
|
294
|
+
isSafeBundlePath,
|
|
295
|
+
BUILT_IN_ARTWORK,
|
|
296
|
+
builtInArtworkById,
|
|
297
|
+
resolvePackagePublicDirectory
|
|
298
|
+
};
|
|
299
|
+
//# sourceMappingURL=chunk-I7JOE7XJ.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/shared.ts","../src/device-mockups.ts","../src/artwork.ts","../src/package-assets.ts"],"sourcesContent":["export const ASSET_CATEGORIES = [\"screenshots\", \"brand\", \"other\"] as const\n\nexport type AssetCategory = (typeof ASSET_CATEGORIES)[number]\n\nexport const SCREENSHOT_DEVICE_TYPES = [\"iphone\", \"ipad\", \"mac\", \"watch\"] as const\nexport type ScreenshotDeviceType = (typeof SCREENSHOT_DEVICE_TYPES)[number]\nexport type DetectedScreenshotDeviceType = ScreenshotDeviceType | \"unknown\"\n\nexport interface StoreShotConfig {\n version: 1\n appName: string\n platforms: Array<\"ios\" | \"android\">\n}\n\nexport interface Asset {\n id: string\n category: AssetCategory\n name: string\n url: string\n size: number\n modifiedAt: string\n width?: number\n height?: number\n /** The dimension-based classification before a user override is applied. */\n detectedDeviceType?: DetectedScreenshotDeviceType\n /** An explicit local catalog override. */\n deviceTypeOverride?: ScreenshotDeviceType\n /** The effective type used for mockup recommendations. */\n deviceType?: DetectedScreenshotDeviceType\n}\n\nexport interface UpdateAssetMetadataInput {\n deviceType: ScreenshotDeviceType | null\n}\n\ninterface CanvasElementBase {\n id: string\n x: number\n y: number\n width: number\n height: number\n rotation: number\n opacity: number\n /** Mirrors the layer around its vertical axis. */\n flipX?: boolean\n /** Mirrors the layer around its horizontal axis. */\n flipY?: boolean\n}\n\nexport type ImageElementSource =\n | { kind: \"builtin\"; id: string }\n | { kind: \"asset\"; assetId: string }\n\n/** A project image or built-in vector artwork placed on a screenshot. */\nexport interface ImageElement extends CanvasElementBase {\n type: \"image\"\n source: ImageElementSource\n fit: \"contain\" | \"cover\"\n /** Replaces the visible pixels while preserving their alpha. Omit to use the source colors. */\n fill?: string\n}\n\nexport interface DeviceMockupElement extends CanvasElementBase {\n type: \"mockup\"\n mockupId: string\n assetId: string\n}\n\nexport type FontWeight = 100 | 200 | 300 | 400 | 500 | 600 | 700 | 800 | 900\nexport const DEFAULT_TEXT_LINE_HEIGHT_RATIO = 1.05\n\nexport interface TextElement extends CanvasElementBase {\n type: \"text\"\n text: string\n fontFamily: string\n fontSize: number\n fontWeight: FontWeight\n /** Absolute line height in canvas pixels. When omitted, the renderer derives it from the font size. */\n lineHeight?: number\n color: string\n textAlign: \"left\" | \"center\" | \"right\"\n}\n\nexport interface ShapeElement extends CanvasElementBase {\n type: \"shape\"\n shape: \"rectangle\"\n fill: string\n cornerRadius: number\n}\n\nexport type CanvasElement = DeviceMockupElement | ImageElement | ShapeElement | TextElement\n\nexport interface ScreenshotArea {\n id: string\n name: string\n background: string\n elements: CanvasElement[]\n}\n\nexport interface ScreenshotSet {\n version: 1\n id: string\n name: string\n locale: string\n device: string\n canvas: {\n width: number\n height: number\n }\n areas: ScreenshotArea[]\n createdAt: string\n updatedAt: string\n}\n\nexport interface CreateSetInput {\n name: string\n locale: string\n device: string\n width: number\n height: number\n}\n\nexport interface UpdateSetMetadataInput {\n name: string\n locale: string\n device: string\n}\n\nexport interface StoreShotProject {\n directory: string\n config: StoreShotConfig\n assets: Record<AssetCategory, Asset[]>\n sets: ScreenshotSet[]\n}\n","export const MOCKUP_BUNDLE_FILENAME = \"storeshot-mockups.json\"\nexport const MOCKUP_BUNDLE_FORMAT = \"storeshot-mockup-bundle\"\n\nexport interface Point {\n x: number\n y: number\n}\n\nexport type DevicePlatform = \"iphone\" | \"ipad\" | \"mac\" | \"watch\"\nexport type DeviceMockupStyle =\n | \"3d\"\n | \"colored\"\n | \"colored-3d\"\n | \"handheld\"\n | \"handheld-dim\"\n | \"handheld-silhouette\"\n | \"handheld-styles\"\n | \"standard\"\n | \"textured\"\n\nexport type ProjectiveTransform = readonly [\n readonly [number, number, number],\n readonly [number, number, number],\n readonly [number, number, number],\n]\n\nexport interface RectMockupScreen {\n kind: \"rect\"\n x: number\n y: number\n width: number\n height: number\n cornerRadius: number\n}\n\nexport interface PerspectiveMockupScreen {\n kind: \"projective\"\n /** Maps normalized screenshot coordinates into normalized frame coordinates. */\n transform: ProjectiveTransform\n /** Elliptical source-image corner radius, normalized to the screenshot plane. */\n sourceCornerRadius: Point\n}\n\nexport type MockupScreen = PerspectiveMockupScreen | RectMockupScreen\n\nexport interface MockupBundleLicense {\n name: string\n url?: string\n file?: string\n}\n\nexport interface MockupBundleSource {\n name?: string\n url: string\n revision?: string\n}\n\n/** Portable, folder-based StoreShot device mockup definition. */\nexport interface MockupBundleEntry {\n id: string\n groupId: string\n groupName: string\n name: string\n description: string\n platform: DevicePlatform\n style: DeviceMockupStyle\n frame: string\n thumbnail?: string\n width: number\n height: number\n screen: MockupScreen\n}\n\nexport interface MockupBundleManifest {\n format: typeof MOCKUP_BUNDLE_FORMAT\n version: 1\n id: string\n name: string\n author: string\n license: MockupBundleLicense\n source?: MockupBundleSource\n mockups: MockupBundleEntry[]\n}\n\ninterface DeviceMockupBase {\n id: string\n bundleId: string\n groupId: string\n name: string\n description: string\n platform: DevicePlatform\n style: DeviceMockupStyle\n frameUrl: string\n thumbnailUrl: string\n width: number\n height: number\n}\n\nexport interface DeviceMockup extends DeviceMockupBase {\n screen: MockupScreen\n}\n\nexport interface DeviceMockupGroup {\n id: string\n bundleId: string\n name: string\n platform: DevicePlatform\n style: DeviceMockupStyle\n thumbnailUrl: string\n count: number\n}\n\nexport interface MockupBundleSummary {\n id: string\n name: string\n author: string\n license: MockupBundleLicense\n source?: MockupBundleSource\n mockupCount: number\n origin: \"built-in\" | \"project\"\n}\n\nexport interface DeviceMockupCatalog {\n bundles: MockupBundleSummary[]\n groups: DeviceMockupGroup[]\n mockups: DeviceMockup[]\n}\n\nconst identifierPattern = /^[a-z0-9][a-z0-9-]*$/\nconst styles = new Set<DeviceMockupStyle>([\n \"3d\",\n \"colored\",\n \"colored-3d\",\n \"handheld\",\n \"handheld-dim\",\n \"handheld-silhouette\",\n \"handheld-styles\",\n \"standard\",\n \"textured\",\n])\nconst platforms = new Set<DevicePlatform>([\"iphone\", \"ipad\", \"mac\", \"watch\"])\nconst frameExtensions = new Set([\".jpeg\", \".jpg\", \".png\", \".svg\", \".webp\"])\n\nexport function emptyDeviceMockupCatalog(): DeviceMockupCatalog {\n return { bundles: [], groups: [], mockups: [] }\n}\n\nexport function parseMockupBundleManifest(value: unknown): MockupBundleManifest {\n if (!isRecord(value)) throw new Error(`${MOCKUP_BUNDLE_FILENAME} must contain a JSON object`)\n if (value.format !== MOCKUP_BUNDLE_FORMAT || value.version !== 1) {\n throw new Error(\"Unsupported StoreShot mockup bundle format\")\n }\n if (!Array.isArray(value.mockups) || value.mockups.length === 0) {\n throw new Error(\"A mockup bundle must contain at least one mockup\")\n }\n\n const id = readIdentifier(value.id, \"Bundle id\")\n const mockups = value.mockups.map(parseMockupEntry)\n const entryIds = new Set<string>()\n for (const mockup of mockups) {\n if (entryIds.has(mockup.id)) throw new Error(`Duplicate mockup id: ${mockup.id}`)\n entryIds.add(mockup.id)\n }\n\n return {\n format: MOCKUP_BUNDLE_FORMAT,\n version: 1,\n id,\n name: readString(value.name, \"Bundle name\"),\n author: readString(value.author, \"Bundle author\"),\n license: parseLicense(value.license),\n ...(value.source === undefined ? {} : { source: parseSource(value.source) }),\n mockups,\n }\n}\n\nexport function resolveMockupBundle(\n manifest: MockupBundleManifest,\n baseUrl: string,\n origin: MockupBundleSummary[\"origin\"],\n): DeviceMockupCatalog {\n const bundlePrefix = `${manifest.id}/`\n const mockups: DeviceMockup[] = manifest.mockups.map((entry) => ({\n ...entry,\n id: `${bundlePrefix}${entry.id}`,\n bundleId: manifest.id,\n groupId: `${bundlePrefix}${entry.groupId}`,\n frameUrl: `${baseUrl}${encodeBundlePath(entry.frame)}`,\n thumbnailUrl: `${baseUrl}${encodeBundlePath(entry.thumbnail ?? entry.frame)}`,\n }))\n\n const groupEntries = new Map<string, DeviceMockup[]>()\n for (const mockup of mockups) {\n const entries = groupEntries.get(mockup.groupId) ?? []\n entries.push(mockup)\n groupEntries.set(mockup.groupId, entries)\n }\n\n return {\n bundles: [{\n id: manifest.id,\n name: manifest.name,\n author: manifest.author,\n license: manifest.license,\n ...(manifest.source ? { source: manifest.source } : {}),\n mockupCount: mockups.length,\n origin,\n }],\n groups: [...groupEntries.entries()].map(([id, entries]) => ({\n id,\n bundleId: manifest.id,\n name: manifest.mockups.find((entry) => `${bundlePrefix}${entry.groupId}` === id)?.groupName ?? entries[0].name,\n platform: entries[0].platform,\n style: entries[0].style,\n thumbnailUrl: entries[0].thumbnailUrl,\n count: entries.length,\n })),\n mockups,\n }\n}\n\nexport function mergeDeviceMockupCatalogs(...catalogs: DeviceMockupCatalog[]): DeviceMockupCatalog {\n const result = emptyDeviceMockupCatalog()\n const bundleIds = new Set<string>()\n const groupIds = new Set<string>()\n const mockupIds = new Set<string>()\n\n for (const catalog of catalogs) {\n for (const bundle of catalog.bundles) {\n if (bundleIds.has(bundle.id)) continue\n bundleIds.add(bundle.id)\n result.bundles.push(bundle)\n }\n for (const group of catalog.groups) {\n if (groupIds.has(group.id)) continue\n groupIds.add(group.id)\n result.groups.push(group)\n }\n for (const mockup of catalog.mockups) {\n if (mockupIds.has(mockup.id)) continue\n mockupIds.add(mockup.id)\n result.mockups.push(mockup)\n }\n }\n return result\n}\n\nexport function deviceMockupById(catalog: DeviceMockupCatalog, id: string): DeviceMockup | undefined {\n return catalog.mockups.find((mockup) => mockup.id === id)\n}\n\nexport function bundleAssetPaths(manifest: MockupBundleManifest): string[] {\n const paths = new Set<string>()\n if (manifest.license.file) paths.add(manifest.license.file)\n for (const mockup of manifest.mockups) {\n paths.add(mockup.frame)\n if (mockup.thumbnail) paths.add(mockup.thumbnail)\n }\n return [...paths]\n}\n\nexport function isSafeBundlePath(value: string): boolean {\n if (!value || value.startsWith(\"/\") || value.includes(\"\\\\\")) return false\n const parts = value.split(\"/\")\n return parts.every((part) => part.length > 0 && part !== \".\" && part !== \"..\")\n}\n\nfunction parseMockupEntry(value: unknown): MockupBundleEntry {\n if (!isRecord(value)) throw new Error(\"Mockup entry must be an object\")\n const platform = readString(value.platform, \"Mockup platform\") as DevicePlatform\n const style = readString(value.style, \"Mockup style\") as DeviceMockupStyle\n if (!platforms.has(platform)) throw new Error(`Unsupported mockup platform: ${platform}`)\n if (!styles.has(style)) throw new Error(`Unsupported mockup style: ${style}`)\n const frame = readFramePath(value.frame, \"Mockup frame\")\n const thumbnail = value.thumbnail === undefined ? undefined : readFramePath(value.thumbnail, \"Mockup thumbnail\")\n return {\n id: readIdentifier(value.id, \"Mockup id\"),\n groupId: readIdentifier(value.groupId, \"Mockup group id\"),\n groupName: readString(value.groupName, \"Mockup group name\"),\n name: readString(value.name, \"Mockup name\"),\n description: readString(value.description, \"Mockup description\"),\n platform,\n style,\n frame,\n ...(thumbnail ? { thumbnail } : {}),\n width: readDimension(value.width, \"Mockup width\"),\n height: readDimension(value.height, \"Mockup height\"),\n screen: parseScreen(value.screen),\n }\n}\n\nfunction parseScreen(value: unknown): MockupScreen {\n if (!isRecord(value)) throw new Error(\"Mockup screen must be an object\")\n if (value.kind === \"rect\") {\n return {\n kind: \"rect\",\n x: readNumber(value.x, \"Screen x\"),\n y: readNumber(value.y, \"Screen y\"),\n width: readDimension(value.width, \"Screen width\"),\n height: readDimension(value.height, \"Screen height\"),\n cornerRadius: readNonNegative(value.cornerRadius, \"Screen corner radius\"),\n }\n }\n if (value.kind === \"projective\") {\n if (!Array.isArray(value.transform) || value.transform.length !== 3) throw new Error(\"Projective transform must have three rows\")\n const transform = value.transform.map((row) => {\n if (!Array.isArray(row) || row.length !== 3) throw new Error(\"Each projective transform row must have three numbers\")\n return row.map((entry) => readNumber(entry, \"Projective transform value\")) as [number, number, number]\n }) as [\n [number, number, number],\n [number, number, number],\n [number, number, number],\n ]\n if (!isRecord(value.sourceCornerRadius)) throw new Error(\"Projective source corner radius must be an object\")\n return {\n kind: \"projective\",\n transform,\n sourceCornerRadius: {\n x: readNonNegative(value.sourceCornerRadius.x, \"Source corner radius x\"),\n y: readNonNegative(value.sourceCornerRadius.y, \"Source corner radius y\"),\n },\n }\n }\n throw new Error(\"Unsupported mockup screen geometry\")\n}\n\nfunction parseLicense(value: unknown): MockupBundleLicense {\n if (!isRecord(value)) throw new Error(\"Bundle license must be an object\")\n const file = value.file === undefined ? undefined : readBundlePath(value.file, \"License file\")\n return {\n name: readString(value.name, \"License name\"),\n ...(value.url === undefined ? {} : { url: readString(value.url, \"License URL\") }),\n ...(file ? { file } : {}),\n }\n}\n\nfunction parseSource(value: unknown): MockupBundleSource {\n if (!isRecord(value)) throw new Error(\"Bundle source must be an object\")\n return {\n ...(value.name === undefined ? {} : { name: readString(value.name, \"Source name\") }),\n url: readString(value.url, \"Source URL\"),\n ...(value.revision === undefined ? {} : { revision: readString(value.revision, \"Source revision\") }),\n }\n}\n\nfunction readFramePath(value: unknown, name: string): string {\n const result = readBundlePath(value, name)\n const extensionIndex = result.lastIndexOf(\".\")\n const extension = extensionIndex >= 0 ? result.slice(extensionIndex).toLowerCase() : \"\"\n if (!frameExtensions.has(extension)) throw new Error(`${name} has an unsupported file type`)\n return result\n}\n\nfunction readBundlePath(value: unknown, name: string): string {\n const result = readString(value, name)\n if (!isSafeBundlePath(result)) throw new Error(`${name} must be a relative path inside the bundle`)\n return result\n}\n\nfunction readIdentifier(value: unknown, name: string): string {\n const result = readString(value, name)\n if (!identifierPattern.test(result)) throw new Error(`${name} must use lowercase letters, numbers, and hyphens`)\n return result\n}\n\nfunction readString(value: unknown, name: string): string {\n const result = typeof value === \"string\" ? value.trim() : \"\"\n if (!result) throw new Error(`${name} must be a non-empty string`)\n return result\n}\n\nfunction readNumber(value: unknown, name: string): number {\n if (typeof value !== \"number\" || !Number.isFinite(value)) throw new Error(`${name} must be a number`)\n return value\n}\n\nfunction readDimension(value: unknown, name: string): number {\n const result = readNumber(value, name)\n if (result <= 0 || result > 20_000) throw new Error(`${name} is outside the supported range`)\n return result\n}\n\nfunction readNonNegative(value: unknown, name: string): number {\n const result = readNumber(value, name)\n if (result < 0 || result > 20_000) throw new Error(`${name} is outside the supported range`)\n return result\n}\n\nfunction encodeBundlePath(value: string): string {\n return value.split(\"/\").map(encodeURIComponent).join(\"/\")\n}\n\nfunction isRecord(value: unknown): value is Record<string, unknown> {\n return typeof value === \"object\" && value !== null && !Array.isArray(value)\n}\n","export const ARTWORK_CATEGORIES = [\"decor\", \"lines\", \"laurels\"] as const\n\nexport type ArtworkCategory = (typeof ARTWORK_CATEGORIES)[number]\n\nexport interface BuiltInArtworkDefinition {\n id: string\n name: string\n category: ArtworkCategory\n url: string\n width: number\n height: number\n attribution: string\n license: \"MIT\" | \"CC0-1.0\"\n}\n\nconst TABLER_ATTRIBUTION = \"Tabler Icons\"\nconst WIKIMEDIA_COMMONS_ATTRIBUTION = \"Wikimedia Commons\"\n\nexport const BUILT_IN_ARTWORK: BuiltInArtworkDefinition[] = [\n { id: \"tabler-grid-pattern\", name: \"Grid pattern\", category: \"decor\", url: \"/artwork/tabler/filled/grid-pattern.svg\", width: 24, height: 24, attribution: TABLER_ATTRIBUTION, license: \"MIT\" },\n { id: \"tabler-sparkles\", name: \"Sparkles\", category: \"decor\", url: \"/artwork/tabler/filled/sparkles.svg\", width: 24, height: 24, attribution: TABLER_ATTRIBUTION, license: \"MIT\" },\n { id: \"tabler-stars\", name: \"Stars\", category: \"decor\", url: \"/artwork/tabler/filled/stars.svg\", width: 24, height: 24, attribution: TABLER_ATTRIBUTION, license: \"MIT\" },\n { id: \"tabler-scribble\", name: \"Scribble\", category: \"lines\", url: \"/artwork/tabler/outline/scribble.svg\", width: 24, height: 24, attribution: TABLER_ATTRIBUTION, license: \"MIT\" },\n { id: \"tabler-underline\", name: \"Underline\", category: \"lines\", url: \"/artwork/tabler/outline/underline.svg\", width: 24, height: 24, attribution: TABLER_ATTRIBUTION, license: \"MIT\" },\n { id: \"tabler-laurel-wreath\", name: \"Laurel wreath\", category: \"laurels\", url: \"/artwork/tabler/filled/laurel-wreath.svg\", width: 24, height: 24, attribution: TABLER_ATTRIBUTION, license: \"MIT\" },\n { id: \"tabler-laurel-wreath-1\", name: \"Laurel wreath 1\", category: \"laurels\", url: \"/artwork/tabler/filled/laurel-wreath-1.svg\", width: 24, height: 24, attribution: TABLER_ATTRIBUTION, license: \"MIT\" },\n { id: \"tabler-laurel-wreath-2\", name: \"Laurel wreath 2\", category: \"laurels\", url: \"/artwork/tabler/filled/laurel-wreath-2.svg\", width: 24, height: 24, attribution: TABLER_ATTRIBUTION, license: \"MIT\" },\n { id: \"tabler-laurel-wreath-3\", name: \"Laurel wreath 3\", category: \"laurels\", url: \"/artwork/tabler/filled/laurel-wreath-3.svg\", width: 24, height: 24, attribution: TABLER_ATTRIBUTION, license: \"MIT\" },\n { id: \"cc0-laurel-wreath-classic\", name: \"Classic laurel wreath\", category: \"laurels\", url: \"/artwork/cc0-laurels/laurel-wreath-classic.svg\", width: 564.23, height: 574.03, attribution: WIKIMEDIA_COMMONS_ATTRIBUTION, license: \"CC0-1.0\" },\n { id: \"cc0-laurel-wreath-natural\", name: \"Natural laurel wreath\", category: \"laurels\", url: \"/artwork/cc0-laurels/laurel-wreath-natural.svg\", width: 595.3, height: 519.9, attribution: WIKIMEDIA_COMMONS_ATTRIBUTION, license: \"CC0-1.0\" },\n { id: \"cc0-laurel-branch\", name: \"Laurel branch\", category: \"laurels\", url: \"/artwork/cc0-laurels/laurel-branch.svg\", width: 244.14168, height: 307.26953, attribution: WIKIMEDIA_COMMONS_ATTRIBUTION, license: \"CC0-1.0\" },\n { id: \"cc0-laurel-pair-wide\", name: \"Wide laurel pair\", category: \"laurels\", url: \"/artwork/cc0-laurels/laurel-pair-wide.svg\", width: 555, height: 288, attribution: WIKIMEDIA_COMMONS_ATTRIBUTION, license: \"CC0-1.0\" },\n]\n\nconst artworkById = new Map(BUILT_IN_ARTWORK.map((artwork) => [artwork.id, artwork]))\n\nexport function builtInArtworkById(id: string): BuiltInArtworkDefinition | undefined {\n return artworkById.get(id)\n}\n\nexport function artworkCategoryLabel(category: ArtworkCategory): string {\n return { decor: \"Decor\", lines: \"Lines\", laurels: \"Laurels\" }[category]\n}\n","import { access } from \"node:fs/promises\"\nimport path from \"node:path\"\n\nexport async function resolvePackagePublicDirectory(packageRoot: string): Promise<string> {\n const candidates = [\n path.join(packageRoot, \"dist/ui\"),\n path.join(packageRoot, \"src/ui/public\"),\n ]\n for (const candidate of candidates) {\n try {\n await access(candidate)\n return candidate\n } catch {\n // Try the next development or packaged location.\n }\n }\n throw new Error(\"StoreShot's bundled rendering assets could not be found\")\n}\n"],"mappings":"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","names":[]}
|