@open-assets/open-assets 0.1.12 → 0.3.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/README.md +45 -40
- package/bin/open-assets.mjs +60 -6
- package/lib/add.mjs +6 -6
- package/lib/init.mjs +1 -1
- package/lib/list.mjs +7 -9
- package/lib/localization.mjs +159 -0
- package/lib/lockfile.mjs +81 -6
- package/lib/manifest.mjs +11 -4
- package/lib/render.mjs +156 -93
- package/lib/renderer.mjs +84 -3
- package/lib/server.mjs +44 -23
- package/lib/validate.mjs +43 -10
- package/package.json +2 -2
- package/public/img/examples/cat-iq-test-icon.png +0 -0
- package/public/img/examples/cat-iq-test.png +0 -0
- package/public/img/examples/no-context-icon.png +0 -0
- package/public/img/examples/no-context.png +0 -0
- package/public/img/examples/only-recipes-icon.png +0 -0
- package/public/img/examples/only-recipes.png +0 -0
- package/public/viewer.html +35 -35
- package/skills/open-assets/SKILL.md +155 -30
package/lib/server.mjs
CHANGED
|
@@ -146,7 +146,7 @@ export async function startServer(projectDir, options = {}, renderer) {
|
|
|
146
146
|
// API: render screenshot (unified for all collection types)
|
|
147
147
|
if (req.method === "POST" && pathname === "/api/render-screenshot") {
|
|
148
148
|
try {
|
|
149
|
-
const { file, width, height, sourceW, sourceH } = JSON.parse(
|
|
149
|
+
const { file, width, height, sourceW, sourceH, background } = JSON.parse(
|
|
150
150
|
await readBody(req)
|
|
151
151
|
);
|
|
152
152
|
if (!file || !width || !height) {
|
|
@@ -158,7 +158,7 @@ export async function startServer(projectDir, options = {}, renderer) {
|
|
|
158
158
|
const sw = sourceW || width;
|
|
159
159
|
const sh = sourceH || height;
|
|
160
160
|
log(`Rendering ${file} at ${width}x${height} (source ${sw}x${sh})...`);
|
|
161
|
-
const buffer = await renderScreenshot(projectDir, file, width, height, sw, sh);
|
|
161
|
+
const buffer = await renderScreenshot(projectDir, file, width, height, sw, sh, { background });
|
|
162
162
|
res.writeHead(200, {
|
|
163
163
|
"Content-Type": "image/png",
|
|
164
164
|
"Content-Length": buffer.length,
|
|
@@ -181,7 +181,8 @@ export async function startServer(projectDir, options = {}, renderer) {
|
|
|
181
181
|
res.writeHead(404, { "Content-Type": "application/json" });
|
|
182
182
|
return res.end(JSON.stringify({ ok: false, error: "Collection not found" }));
|
|
183
183
|
}
|
|
184
|
-
const
|
|
184
|
+
const outputs = (col.export || []).filter((e) => e.type);
|
|
185
|
+
const output = outputs[outputIndex || 0];
|
|
185
186
|
if (!output) {
|
|
186
187
|
res.writeHead(404, { "Content-Type": "application/json" });
|
|
187
188
|
return res.end(JSON.stringify({ ok: false, error: "Output not found" }));
|
|
@@ -245,30 +246,50 @@ export async function startServer(projectDir, options = {}, renderer) {
|
|
|
245
246
|
res.end();
|
|
246
247
|
});
|
|
247
248
|
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
249
|
+
const maxRetries = 10;
|
|
250
|
+
let currentPort = port;
|
|
251
|
+
let resolved = false;
|
|
252
|
+
|
|
253
|
+
const actualPort = await new Promise((resolvePort) => {
|
|
254
|
+
function tryListen() {
|
|
255
|
+
server.once("error", (err) => {
|
|
256
|
+
if (err.code === "EADDRINUSE") {
|
|
257
|
+
if (currentPort - port >= maxRetries) {
|
|
258
|
+
console.error(`Error: Ports ${port}–${currentPort} are all in use.`);
|
|
259
|
+
process.exit(1);
|
|
260
|
+
}
|
|
261
|
+
currentPort++;
|
|
262
|
+
server.close(() => tryListen());
|
|
263
|
+
return;
|
|
264
|
+
}
|
|
265
|
+
throw err;
|
|
266
|
+
});
|
|
262
267
|
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
+
server.listen(currentPort, host, () => {
|
|
269
|
+
if (resolved) return;
|
|
270
|
+
resolved = true;
|
|
271
|
+
const displayHost = host === "0.0.0.0" ? "localhost" : host;
|
|
272
|
+
if (currentPort !== port) {
|
|
273
|
+
log();
|
|
274
|
+
log(` Port ${port} is in use, using ${currentPort} instead.`);
|
|
275
|
+
}
|
|
276
|
+
log();
|
|
277
|
+
log(` Open Assets is running on http://${displayHost}:${currentPort}`);
|
|
278
|
+
log();
|
|
279
|
+
log(` Project: ${projectDir}`);
|
|
280
|
+
if (staticDirs.length > 0) {
|
|
281
|
+
log(` Static: ${staticDirs.join(", ")}`);
|
|
282
|
+
}
|
|
283
|
+
log();
|
|
284
|
+
resolvePort(currentPort);
|
|
285
|
+
});
|
|
268
286
|
}
|
|
269
|
-
|
|
287
|
+
|
|
288
|
+
tryListen();
|
|
270
289
|
});
|
|
271
290
|
|
|
291
|
+
return { port: actualPort };
|
|
292
|
+
|
|
272
293
|
function cleanup() {
|
|
273
294
|
for (const w of watchers) w.close();
|
|
274
295
|
for (const client of sseClients) client.end();
|
package/lib/validate.mjs
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { existsSync } from "fs";
|
|
2
2
|
import { resolve } from "path";
|
|
3
|
-
import { flatSizes } from "./manifest.mjs";
|
|
3
|
+
import { flatSizes, flatOutputs } from "./manifest.mjs";
|
|
4
|
+
import { loadLocalizations, getLocales } from "./localization.mjs";
|
|
4
5
|
|
|
5
6
|
/**
|
|
6
7
|
* Validate a config object and check that referenced files exist.
|
|
@@ -12,6 +13,8 @@ import { flatSizes } from "./manifest.mjs";
|
|
|
12
13
|
*/
|
|
13
14
|
export function validateConfig(manifest, projectDir) {
|
|
14
15
|
const checks = [];
|
|
16
|
+
const missingTemplates = [];
|
|
17
|
+
const emptyCollections = [];
|
|
15
18
|
let errors = 0;
|
|
16
19
|
|
|
17
20
|
const ok = (msg) => checks.push({ ok: true, message: msg });
|
|
@@ -31,7 +34,7 @@ export function validateConfig(manifest, projectDir) {
|
|
|
31
34
|
ok(`${manifest.collections.length} collection(s) defined`);
|
|
32
35
|
} else {
|
|
33
36
|
fail("Missing or empty 'collections' array");
|
|
34
|
-
return { checks, errors };
|
|
37
|
+
return { checks, errors, missingTemplates, emptyCollections };
|
|
35
38
|
}
|
|
36
39
|
|
|
37
40
|
// Check each collection
|
|
@@ -54,6 +57,7 @@ export function validateConfig(manifest, projectDir) {
|
|
|
54
57
|
|
|
55
58
|
if (!col.templates || col.templates.length === 0) {
|
|
56
59
|
fail(`Collection "${col.id}" has no templates`);
|
|
60
|
+
emptyCollections.push({ collectionId: col.id });
|
|
57
61
|
} else {
|
|
58
62
|
for (const template of col.templates) {
|
|
59
63
|
if (!template.src) {
|
|
@@ -65,6 +69,11 @@ export function validateConfig(manifest, projectDir) {
|
|
|
65
69
|
ok(`${template.src} exists`);
|
|
66
70
|
} else {
|
|
67
71
|
fail(`${template.src} not found`);
|
|
72
|
+
missingTemplates.push({
|
|
73
|
+
collectionId: col.id,
|
|
74
|
+
templateSrc: template.src,
|
|
75
|
+
templateName: template.name || template.label,
|
|
76
|
+
});
|
|
68
77
|
}
|
|
69
78
|
}
|
|
70
79
|
}
|
|
@@ -74,17 +83,41 @@ export function validateConfig(manifest, projectDir) {
|
|
|
74
83
|
ok(`Collection "${col.id}" has ${sizes.length} export size(s)`);
|
|
75
84
|
}
|
|
76
85
|
|
|
77
|
-
// Validate
|
|
78
|
-
if (col.
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
86
|
+
// Validate localizations if configured
|
|
87
|
+
if (col.localizations) {
|
|
88
|
+
const l10nPath = resolve(projectDir, col.localizations);
|
|
89
|
+
if (!existsSync(l10nPath)) {
|
|
90
|
+
fail(`Collection "${col.id}" localizations file not found: ${col.localizations}`);
|
|
91
|
+
} else {
|
|
92
|
+
try {
|
|
93
|
+
const l10nData = loadLocalizations(projectDir, col.localizations);
|
|
94
|
+
if (!l10nData.sourceLanguage) {
|
|
95
|
+
fail(`Collection "${col.id}" localizations missing "sourceLanguage"`);
|
|
96
|
+
}
|
|
97
|
+
if (!l10nData.strings || Object.keys(l10nData.strings).length === 0) {
|
|
98
|
+
fail(`Collection "${col.id}" localizations has no strings defined`);
|
|
99
|
+
} else {
|
|
100
|
+
const locales = getLocales(l10nData, col.locales);
|
|
101
|
+
ok(`Collection "${col.id}" localizations: ${Object.keys(l10nData.strings).length} key(s), ${locales.length} locale(s)`);
|
|
102
|
+
if (col.locales) {
|
|
103
|
+
const missing = col.locales.filter((l) => !locales.includes(l));
|
|
104
|
+
for (const m of missing) {
|
|
105
|
+
fail(`Collection "${col.id}" requests locale "${m}" but it is not defined in ${col.localizations}`);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
} catch (err) {
|
|
110
|
+
fail(`Collection "${col.id}" localizations error: ${err.message}`);
|
|
84
111
|
}
|
|
85
112
|
}
|
|
86
113
|
}
|
|
114
|
+
|
|
115
|
+
// Validate outputs (export entries with a type field)
|
|
116
|
+
const outputs = flatOutputs(col);
|
|
117
|
+
for (const output of outputs) {
|
|
118
|
+
ok(`Output: ${output.type}`);
|
|
119
|
+
}
|
|
87
120
|
}
|
|
88
121
|
|
|
89
|
-
return { checks, errors };
|
|
122
|
+
return { checks, errors, missingTemplates, emptyCollections };
|
|
90
123
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@open-assets/open-assets",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
4
4
|
"description": "Dev server and export tool for app screenshots, icons, and logos. Like Storybook for marketing assets.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -39,7 +39,7 @@
|
|
|
39
39
|
"access": "public"
|
|
40
40
|
},
|
|
41
41
|
"scripts": {
|
|
42
|
-
"assets": "node bin/open-assets.mjs render --
|
|
42
|
+
"assets": "node bin/open-assets.mjs render --force -o ./public/img",
|
|
43
43
|
"test": "node --experimental-vm-modules node_modules/.bin/jest",
|
|
44
44
|
"release": "./scripts/release.sh"
|
|
45
45
|
},
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
package/public/viewer.html
CHANGED
|
@@ -254,46 +254,46 @@
|
|
|
254
254
|
const sh = col.sourceSize.height;
|
|
255
255
|
const hasMultipleTemplates = col.templates.length > 1;
|
|
256
256
|
|
|
257
|
-
// Outputs (e.g., Xcode)
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
257
|
+
// Outputs (e.g., Xcode, copy-source) — export entries with a type field
|
|
258
|
+
const allExports = col.export || [];
|
|
259
|
+
const outputs = allExports.filter(e => e.type);
|
|
260
|
+
for (let i = 0; i < outputs.length; i++) {
|
|
261
|
+
const output = outputs[i];
|
|
262
|
+
if (output.type === 'xcode') {
|
|
263
|
+
const sectionLabel = document.createElement('div');
|
|
264
|
+
sectionLabel.className = 'section-label';
|
|
265
|
+
sectionLabel.textContent = 'Xcode';
|
|
266
|
+
exportDropdown.appendChild(sectionLabel);
|
|
267
|
+
|
|
268
|
+
const btn = document.createElement('button');
|
|
269
|
+
btn.innerHTML = 'Export to Xcode <span class="dim">.appiconset</span>';
|
|
270
|
+
btn.addEventListener('click', () => runOutput(col.id, i));
|
|
271
|
+
exportDropdown.appendChild(btn);
|
|
272
|
+
|
|
273
|
+
exportDropdown.appendChild(document.createElement('hr'));
|
|
274
|
+
} else if (output.type === 'copy-source') {
|
|
275
|
+
const sectionLabel = document.createElement('div');
|
|
276
|
+
sectionLabel.className = 'section-label';
|
|
277
|
+
sectionLabel.textContent = output.format.toUpperCase();
|
|
278
|
+
exportDropdown.appendChild(sectionLabel);
|
|
279
|
+
|
|
280
|
+
// SVG download for each template
|
|
281
|
+
for (const template of col.templates) {
|
|
282
|
+
if (template.src.endsWith('.' + output.format)) {
|
|
283
|
+
const btn = document.createElement('button');
|
|
284
|
+
const displayName = hasMultipleTemplates ? template.label : `Download ${output.format.toUpperCase()}`;
|
|
285
|
+
btn.innerHTML = `${displayName} <span class="dim">${output.format}</span>`;
|
|
286
|
+
btn.addEventListener('click', () => downloadSource(template));
|
|
287
|
+
exportDropdown.appendChild(btn);
|
|
288
288
|
}
|
|
289
|
-
|
|
290
|
-
exportDropdown.appendChild(document.createElement('hr'));
|
|
291
289
|
}
|
|
290
|
+
|
|
291
|
+
exportDropdown.appendChild(document.createElement('hr'));
|
|
292
292
|
}
|
|
293
293
|
}
|
|
294
294
|
|
|
295
|
-
// Export sizes
|
|
296
|
-
const sizes =
|
|
295
|
+
// Export sizes (entries without a type field)
|
|
296
|
+
const sizes = allExports.filter(e => !e.type);
|
|
297
297
|
if (sizes.length > 0) {
|
|
298
298
|
const sectionLabel = document.createElement('div');
|
|
299
299
|
sectionLabel.className = 'section-label';
|
|
@@ -15,14 +15,13 @@ open-assets is a dev server and export tool for app marketing assets — screens
|
|
|
15
15
|
open-assets dev
|
|
16
16
|
|
|
17
17
|
# Or with Tailwind
|
|
18
|
-
concurrently "npx @tailwindcss/cli -i
|
|
18
|
+
concurrently "npx @tailwindcss/cli -i assets/styles.css -o dist/styles.css --watch" "open-assets dev"
|
|
19
19
|
|
|
20
20
|
# Headless render
|
|
21
21
|
open-assets render --collection screenshots --size iphone-6.9
|
|
22
|
-
open-assets render --collection icon --
|
|
23
|
-
open-assets render --
|
|
24
|
-
open-assets render --template 01-hero --
|
|
25
|
-
open-assets render --force # ignore cache, re-render everything
|
|
22
|
+
open-assets render --collection icon --force
|
|
23
|
+
open-assets render --force
|
|
24
|
+
open-assets render --template 01-hero --force
|
|
26
25
|
```
|
|
27
26
|
|
|
28
27
|
## Concepts
|
|
@@ -44,7 +43,7 @@ The `assets.json` at the project root defines all asset collections. All collect
|
|
|
44
43
|
"version": 1,
|
|
45
44
|
"name": "App Display Name",
|
|
46
45
|
"publicDir": "public",
|
|
47
|
-
"command": "npx open-assets render --
|
|
46
|
+
"command": "npx open-assets render --force",
|
|
48
47
|
"collections": [
|
|
49
48
|
{
|
|
50
49
|
"id": "unique-id",
|
|
@@ -52,12 +51,11 @@ The `assets.json` at the project root defines all asset collections. All collect
|
|
|
52
51
|
"sourceSize": { "width": 440, "height": 956 },
|
|
53
52
|
"borderRadius": 4,
|
|
54
53
|
"templates": [
|
|
55
|
-
{ "src": "
|
|
54
|
+
{ "src": "assets/screenshots/01-hero.html", "name": "01-hero", "label": "Hero" }
|
|
56
55
|
],
|
|
57
56
|
"export": [
|
|
58
57
|
{ "name": "iphone-6.9", "label": "iPhone 6.9\"", "size": { "width": 1320, "height": 2868 } }
|
|
59
58
|
],
|
|
60
|
-
"outputs": [],
|
|
61
59
|
"customExport": { "defaultWidth": 1320, "defaultHeight": 2868 }
|
|
62
60
|
}
|
|
63
61
|
]
|
|
@@ -73,12 +71,13 @@ The `assets.json` at the project root defines all asset collections. All collect
|
|
|
73
71
|
| `sourceSize` | object | `{ width, height }` — dimensions templates are authored at |
|
|
74
72
|
| `borderRadius` | number | Border radius for preview frames (px) |
|
|
75
73
|
| `templates` | array | Source files: `{ src, name, label }` |
|
|
76
|
-
| `export` | array | Export sizes
|
|
77
|
-
| `outputs` | array | Post-render actions (optional) |
|
|
74
|
+
| `export` | array | Export sizes and output actions. Entries with a `type` field are post-render actions |
|
|
78
75
|
| `customExport` | object | Custom size defaults for UI (optional) |
|
|
79
76
|
|
|
80
77
|
### Output Types
|
|
81
78
|
|
|
79
|
+
Export entries with a `type` field are post-render actions (run with `--force`):
|
|
80
|
+
|
|
82
81
|
| Type | Config | Description |
|
|
83
82
|
|------|--------|-------------|
|
|
84
83
|
| `xcode` | `{ "type": "xcode", "path": "..." }` | Render icon and write to Xcode asset catalog |
|
|
@@ -94,8 +93,8 @@ The `assets.json` at the project root defines all asset collections. All collect
|
|
|
94
93
|
"sourceSize": { "width": 440, "height": 956 },
|
|
95
94
|
"borderRadius": 4,
|
|
96
95
|
"templates": [
|
|
97
|
-
{ "src": "
|
|
98
|
-
{ "src": "
|
|
96
|
+
{ "src": "assets/screenshots/01-hero.html", "name": "01-hero", "label": "Hero" },
|
|
97
|
+
{ "src": "assets/screenshots/02-features.html", "name": "02-features", "label": "Features" }
|
|
99
98
|
],
|
|
100
99
|
"export": [
|
|
101
100
|
{ "name": "iphone-6.9", "label": "iPhone 6.9\"", "size": { "width": 1320, "height": 2868 } },
|
|
@@ -112,13 +111,13 @@ The `assets.json` at the project root defines all asset collections. All collect
|
|
|
112
111
|
"sourceSize": { "width": 1024, "height": 1024 },
|
|
113
112
|
"borderRadius": 224,
|
|
114
113
|
"templates": [
|
|
115
|
-
{ "src": "
|
|
114
|
+
{ "src": "assets/icon.html", "name": "icon", "label": "App Icon" }
|
|
116
115
|
],
|
|
117
116
|
"export": [
|
|
118
117
|
{ "name": "1024", "label": "1024px", "size": { "width": 1024, "height": 1024 } },
|
|
119
|
-
{ "name": "180", "label": "180px", "size": { "width": 180, "height": 180 } }
|
|
120
|
-
|
|
121
|
-
|
|
118
|
+
{ "name": "180", "label": "180px", "size": { "width": 180, "height": 180 } },
|
|
119
|
+
{ "type": "xcode", "path": "../MyApp/Assets.xcassets/AppIcon.appiconset" }
|
|
120
|
+
]
|
|
122
121
|
}
|
|
123
122
|
```
|
|
124
123
|
|
|
@@ -130,8 +129,8 @@ The `assets.json` at the project root defines all asset collections. All collect
|
|
|
130
129
|
"sourceSize": { "width": 1024, "height": 1024 },
|
|
131
130
|
"borderRadius": 224,
|
|
132
131
|
"templates": [
|
|
133
|
-
{ "src": "
|
|
134
|
-
{ "src": "
|
|
132
|
+
{ "src": "assets/icons/concept-a.html", "name": "concept-a", "label": "Concept A" },
|
|
133
|
+
{ "src": "assets/icons/concept-b.html", "name": "concept-b", "label": "Concept B" }
|
|
135
134
|
],
|
|
136
135
|
"export": [
|
|
137
136
|
{ "name": "1024", "label": "1024px", "size": { "width": 1024, "height": 1024 } }
|
|
@@ -146,14 +145,14 @@ The `assets.json` at the project root defines all asset collections. All collect
|
|
|
146
145
|
"label": "Logo",
|
|
147
146
|
"sourceSize": { "width": 940, "height": 940 },
|
|
148
147
|
"templates": [
|
|
149
|
-
{ "src": "
|
|
150
|
-
{ "src": "
|
|
148
|
+
{ "src": "assets/logo.svg", "name": "logo", "label": "Logo" },
|
|
149
|
+
{ "src": "assets/logo-dark.svg", "name": "logo-dark", "label": "Dark" }
|
|
151
150
|
],
|
|
152
151
|
"export": [
|
|
153
152
|
{ "name": "1024", "label": "1024px", "size": { "width": 1024, "height": 1024 } },
|
|
154
|
-
{ "name": "512", "label": "512px", "size": { "width": 512, "height": 512 } }
|
|
155
|
-
|
|
156
|
-
|
|
153
|
+
{ "name": "512", "label": "512px", "size": { "width": 512, "height": 512 } },
|
|
154
|
+
{ "type": "copy-source", "format": "svg" }
|
|
155
|
+
]
|
|
157
156
|
}
|
|
158
157
|
```
|
|
159
158
|
|
|
@@ -164,7 +163,7 @@ The `assets.json` at the project root defines all asset collections. All collect
|
|
|
164
163
|
"label": "Social Cards",
|
|
165
164
|
"sourceSize": { "width": 1200, "height": 630 },
|
|
166
165
|
"templates": [
|
|
167
|
-
{ "src": "
|
|
166
|
+
{ "src": "assets/og/default.html", "name": "default", "label": "Default OG" }
|
|
168
167
|
],
|
|
169
168
|
"export": [
|
|
170
169
|
{ "name": "og", "label": "OG Image", "size": { "width": 1200, "height": 630 } }
|
|
@@ -181,7 +180,7 @@ The `assets.json` at the project root defines all asset collections. All collect
|
|
|
181
180
|
| `open-assets dev [dir]` | Start dev server with live preview and export UI |
|
|
182
181
|
| `open-assets render [dir]` | Render assets headlessly via CLI |
|
|
183
182
|
| `open-assets list [dir]` | List all collections and templates in the config |
|
|
184
|
-
| `open-assets validate [dir]` | Validate assets.json and check referenced files exist |
|
|
183
|
+
| `open-assets validate [dir]` | Validate assets.json and check referenced files exist. Use `--fix` to interactively remove missing templates |
|
|
185
184
|
| `open-assets init [dir]` | Scaffold a new assets.json and example assets |
|
|
186
185
|
| `open-assets add collection [dir]` | Add a new collection (from presets or custom) |
|
|
187
186
|
| `open-assets add template [dir]` | Add a new template to an existing collection |
|
|
@@ -195,15 +194,15 @@ The `assets.json` at the project root defines all asset collections. All collect
|
|
|
195
194
|
| `--collection <id>` | Render only the collection with this ID |
|
|
196
195
|
| `--template <name>` | Render only the template with this name |
|
|
197
196
|
| `--size <name>` | Use a named export size from config |
|
|
198
|
-
| `--
|
|
199
|
-
|
|
|
197
|
+
| `--locale <code>` | Render only this locale (e.g. `en`, `ar`, `ja`) |
|
|
198
|
+
| `-f, --force` | Export at every size and re-render all (ignores cache) |
|
|
200
199
|
| `-o, --output <dir>` | Output directory (default: `./exports`) |
|
|
201
200
|
| `--json` | Output results as JSON |
|
|
202
201
|
| `-q, --quiet` | Suppress progress logs |
|
|
203
202
|
|
|
204
203
|
Flags compose naturally:
|
|
205
204
|
- `--collection screenshots --template 01-hero --size iphone-6.9` → single template, single size
|
|
206
|
-
- `--collection screenshots --template 01-hero --
|
|
205
|
+
- `--collection screenshots --template 01-hero --force` → one template, all sizes
|
|
207
206
|
- `--collection screenshots --size iphone-6.9` → all templates, one size
|
|
208
207
|
|
|
209
208
|
### add commands
|
|
@@ -337,19 +336,145 @@ The `render` command maintains a `assets.lock` file with SHA256 checksums of sou
|
|
|
337
336
|
|
|
338
337
|
Note: only source HTML/SVG files are checksummed. Changes to referenced assets (images in publicDir, compiled CSS) will not trigger re-renders — use `--force` when those change.
|
|
339
338
|
|
|
339
|
+
## Localization (i18n)
|
|
340
|
+
|
|
341
|
+
Collections can be localized so that templates render in multiple languages. When localizations are configured, each template is rendered once per locale, producing locale-specific exports with proper text direction (LTR/RTL).
|
|
342
|
+
|
|
343
|
+
### Localizations file
|
|
344
|
+
|
|
345
|
+
Create a JSON file (iOS `.xcstrings`-inspired format) that maps string keys to per-locale values:
|
|
346
|
+
|
|
347
|
+
```json
|
|
348
|
+
{
|
|
349
|
+
"sourceLanguage": "en",
|
|
350
|
+
"strings": {
|
|
351
|
+
"hero_title": {
|
|
352
|
+
"localizations": {
|
|
353
|
+
"en": { "value": "Track your tax days" },
|
|
354
|
+
"ar": { "value": "تتبع أيام الضرائب الخاصة بك" },
|
|
355
|
+
"ja": { "value": "税金の日を追跡する" },
|
|
356
|
+
"de": { "value": "Verfolge deine Steuertage" }
|
|
357
|
+
}
|
|
358
|
+
},
|
|
359
|
+
"day_count_label": {
|
|
360
|
+
"localizations": {
|
|
361
|
+
"en": { "value": "You have {{n:183}} days remaining" },
|
|
362
|
+
"ar": { "value": "لديك {{n:183}} يوم متبقي" },
|
|
363
|
+
"de": { "value": "Sie haben {{n:183}} Tage übrig" }
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
```
|
|
369
|
+
|
|
370
|
+
**Number formatting:** Use `{{n:NUMBER}}` inside localized values for locale-aware number formatting (e.g., `1,234` in English, `1.234` in German). Numbers are formatted using `Intl.NumberFormat` for the target locale.
|
|
371
|
+
|
|
372
|
+
### Wiring localizations to a collection
|
|
373
|
+
|
|
374
|
+
Add `localizations` (path to the JSON file) and optionally `locales` (filter to specific locales) to a collection in `assets.json`:
|
|
375
|
+
|
|
376
|
+
```json
|
|
377
|
+
{
|
|
378
|
+
"id": "screenshots",
|
|
379
|
+
"label": "App Store Screenshots",
|
|
380
|
+
"sourceSize": { "width": 440, "height": 956 },
|
|
381
|
+
"localizations": "localizations.json",
|
|
382
|
+
"locales": ["en", "ar", "ja", "de"],
|
|
383
|
+
"templates": [
|
|
384
|
+
{ "src": "assets/screenshots/01-hero.html", "name": "01-hero", "label": "Hero" }
|
|
385
|
+
],
|
|
386
|
+
"export": [
|
|
387
|
+
{ "name": "iphone-6.9", "label": "iPhone 6.9\"", "size": { "width": 1320, "height": 2868 } }
|
|
388
|
+
]
|
|
389
|
+
}
|
|
390
|
+
```
|
|
391
|
+
|
|
392
|
+
| Field | Type | Description |
|
|
393
|
+
|-------|------|-------------|
|
|
394
|
+
| `localizations` | string | Path to the localizations JSON file (relative to project root) |
|
|
395
|
+
| `locales` | string[] | Optional filter — render only these locales. Omit to render all locales in the file. |
|
|
396
|
+
|
|
397
|
+
### Using placeholders in templates
|
|
398
|
+
|
|
399
|
+
Templates use `{{key}}` mustache-style placeholders that get replaced with localized values before rendering:
|
|
400
|
+
|
|
401
|
+
```html
|
|
402
|
+
<h1>{{hero_title}}</h1>
|
|
403
|
+
<p>{{day_count_label}}</p>
|
|
404
|
+
```
|
|
405
|
+
|
|
406
|
+
The source language values serve as the default/fallback content visible during development. At render time, placeholders are substituted with the localized value for each target locale.
|
|
407
|
+
|
|
408
|
+
### RTL support
|
|
409
|
+
|
|
410
|
+
Right-to-left locales (Arabic, Hebrew, Persian, Urdu, etc.) are automatically detected. For RTL locales, the renderer sets `dir="rtl"` and `lang` on the `<html>` element before taking the screenshot.
|
|
411
|
+
|
|
412
|
+
**Design templates with RTL in mind:**
|
|
413
|
+
- Use `logical` CSS properties (`margin-inline-start` instead of `margin-left`, `padding-inline-end` instead of `padding-right`)
|
|
414
|
+
- Use flexbox `gap` instead of directional margins between items
|
|
415
|
+
- Avoid hardcoded `text-align: left` — use `text-align: start` instead
|
|
416
|
+
- For layouts that must flip in RTL, use `[dir="rtl"]` CSS selectors:
|
|
417
|
+
```css
|
|
418
|
+
.arrow { transform: rotate(0deg); }
|
|
419
|
+
[dir="rtl"] .arrow { transform: rotate(180deg); }
|
|
420
|
+
```
|
|
421
|
+
|
|
422
|
+
### Output structure with locales
|
|
423
|
+
|
|
424
|
+
When localizations are active, locale codes are inserted into the output path:
|
|
425
|
+
|
|
426
|
+
```
|
|
427
|
+
exports/
|
|
428
|
+
screenshots/
|
|
429
|
+
en/
|
|
430
|
+
iphone-6.9/
|
|
431
|
+
01-hero.png
|
|
432
|
+
ar/
|
|
433
|
+
iphone-6.9/
|
|
434
|
+
01-hero.png
|
|
435
|
+
ja/
|
|
436
|
+
iphone-6.9/
|
|
437
|
+
01-hero.png
|
|
438
|
+
```
|
|
439
|
+
|
|
440
|
+
With `outFile`, use the `{locale}` template variable:
|
|
441
|
+
```json
|
|
442
|
+
{ "outFile": "output/{locale}/{template}.png" }
|
|
443
|
+
```
|
|
444
|
+
|
|
445
|
+
### CLI
|
|
446
|
+
|
|
447
|
+
```bash
|
|
448
|
+
# Render all locales for a collection
|
|
449
|
+
open-assets render --collection screenshots --force
|
|
450
|
+
|
|
451
|
+
# Render only Arabic
|
|
452
|
+
open-assets render --collection screenshots --force --locale ar
|
|
453
|
+
|
|
454
|
+
# Render a single template in Japanese
|
|
455
|
+
open-assets render --collection screenshots --template 01-hero --locale ja --force
|
|
456
|
+
```
|
|
457
|
+
|
|
458
|
+
### Locale fallback order
|
|
459
|
+
|
|
460
|
+
When resolving a string for a locale:
|
|
461
|
+
1. Exact match (e.g., `es-419`)
|
|
462
|
+
2. Base language (e.g., `es`)
|
|
463
|
+
3. Source language (e.g., `en`)
|
|
464
|
+
|
|
340
465
|
## File Structure Convention
|
|
341
466
|
|
|
342
467
|
```
|
|
343
468
|
project/
|
|
344
469
|
assets.json
|
|
345
|
-
assets.lock # Auto-generated
|
|
470
|
+
assets.lock # Auto-generated cache (commit this)
|
|
346
471
|
public/ # Shared assets (images, logos, icons, photos)
|
|
347
472
|
logo-round.png
|
|
348
473
|
social/
|
|
349
474
|
youtube.svg
|
|
350
475
|
screenshots/ # Real app screenshots for use in templates
|
|
351
476
|
01.png
|
|
352
|
-
|
|
477
|
+
assets/
|
|
353
478
|
styles.css # Tailwind input
|
|
354
479
|
icon.html # App icon template
|
|
355
480
|
icons/ # Icon concept explorations
|