capture-flow 0.0.0-stage → 0.1.1

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.
Files changed (107) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +256 -2
  3. package/bin/capture-flow.mjs +12 -0
  4. package/package.json +56 -4
  5. package/skills/capture-flow/SKILL.md +248 -0
  6. package/skills/capture-flow/references/capture-recipes.md +155 -0
  7. package/skills/capture-flow/references/store-sizes.md +91 -0
  8. package/src/build.ts +186 -0
  9. package/src/capture.ts +77 -0
  10. package/src/cli.ts +139 -0
  11. package/src/index.ts +11 -0
  12. package/src/manifest.ts +98 -0
  13. package/src/render.ts +75 -0
  14. package/src/verify.ts +90 -0
  15. package/vendor/wajha/LICENSE +21 -0
  16. package/vendor/wajha/NOTICE +21 -0
  17. package/vendor/wajha/engine/background.ts +55 -0
  18. package/vendor/wajha/engine/bidi.ts +589 -0
  19. package/vendor/wajha/engine/cache.ts +96 -0
  20. package/vendor/wajha/engine/color.ts +125 -0
  21. package/vendor/wajha/engine/devices/android.ts +53 -0
  22. package/vendor/wajha/engine/devices/browser.ts +170 -0
  23. package/vendor/wajha/engine/devices/finish.ts +154 -0
  24. package/vendor/wajha/engine/devices/geometry.ts +326 -0
  25. package/vendor/wajha/engine/devices/index.ts +143 -0
  26. package/vendor/wajha/engine/devices/ipad.ts +21 -0
  27. package/vendor/wajha/engine/devices/iphone.ts +55 -0
  28. package/vendor/wajha/engine/devices/macbook.ts +187 -0
  29. package/vendor/wajha/engine/devices/mock/apps/calendar.ts +145 -0
  30. package/vendor/wajha/engine/devices/mock/apps/chat.ts +213 -0
  31. package/vendor/wajha/engine/devices/mock/apps/finance.ts +164 -0
  32. package/vendor/wajha/engine/devices/mock/apps/fitness.ts +145 -0
  33. package/vendor/wajha/engine/devices/mock/apps/index.ts +14 -0
  34. package/vendor/wajha/engine/devices/mock/apps/meditation.ts +167 -0
  35. package/vendor/wajha/engine/devices/mock/apps/music.ts +152 -0
  36. package/vendor/wajha/engine/devices/mock/apps/shop.ts +116 -0
  37. package/vendor/wajha/engine/devices/mock/apps/social.ts +152 -0
  38. package/vendor/wajha/engine/devices/mock/apps/tasks.ts +131 -0
  39. package/vendor/wajha/engine/devices/mock/apps/travel.ts +121 -0
  40. package/vendor/wajha/engine/devices/mock/art.ts +557 -0
  41. package/vendor/wajha/engine/devices/mock/chrome.ts +337 -0
  42. package/vendor/wajha/engine/devices/mock/icons.ts +614 -0
  43. package/vendor/wajha/engine/devices/mock/kit.ts +213 -0
  44. package/vendor/wajha/engine/devices/mock/rng.ts +40 -0
  45. package/vendor/wajha/engine/devices/mock/theme.ts +133 -0
  46. package/vendor/wajha/engine/devices/mock/widgets.ts +231 -0
  47. package/vendor/wajha/engine/devices/paths.ts +113 -0
  48. package/vendor/wajha/engine/devices/screen.ts +75 -0
  49. package/vendor/wajha/engine/devices/shadow.ts +124 -0
  50. package/vendor/wajha/engine/devices/slab.ts +200 -0
  51. package/vendor/wajha/engine/devices/vision.ts +43 -0
  52. package/vendor/wajha/engine/devices/watch.ts +14 -0
  53. package/vendor/wajha/engine/geometry.ts +152 -0
  54. package/vendor/wajha/engine/images.ts +96 -0
  55. package/vendor/wajha/engine/index.ts +20 -0
  56. package/vendor/wajha/engine/linebreak.ts +226 -0
  57. package/vendor/wajha/engine/markup.ts +84 -0
  58. package/vendor/wajha/engine/mock.ts +158 -0
  59. package/vendor/wajha/engine/paint.ts +474 -0
  60. package/vendor/wajha/engine/patterns.ts +263 -0
  61. package/vendor/wajha/engine/render.ts +364 -0
  62. package/vendor/wajha/engine/shapes.ts +260 -0
  63. package/vendor/wajha/engine/testing/canvasNode.ts +121 -0
  64. package/vendor/wajha/engine/testing/fakeDevices.ts +85 -0
  65. package/vendor/wajha/engine/text.ts +694 -0
  66. package/vendor/wajha/engine/types.ts +190 -0
  67. package/vendor/wajha/export/paths.ts +197 -0
  68. package/vendor/wajha/export/png.ts +223 -0
  69. package/vendor/wajha/export/types.ts +60 -0
  70. package/vendor/wajha/model/adapt.ts +889 -0
  71. package/vendor/wajha/model/catalog.ts +369 -0
  72. package/vendor/wajha/model/clone.ts +58 -0
  73. package/vendor/wajha/model/colorMath.ts +128 -0
  74. package/vendor/wajha/model/defaults.ts +386 -0
  75. package/vendor/wajha/model/geometry.ts +95 -0
  76. package/vendor/wajha/model/ids.ts +13 -0
  77. package/vendor/wajha/model/instantiate.ts +280 -0
  78. package/vendor/wajha/model/mirror.ts +43 -0
  79. package/vendor/wajha/model/resolve.ts +87 -0
  80. package/vendor/wajha/model/rules.ts +249 -0
  81. package/vendor/wajha/model/stub.ts +4 -0
  82. package/vendor/wajha/model/types.ts +456 -0
  83. package/vendor/wajha/model/typography.ts +28 -0
  84. package/vendor/wajha/model/validate.ts +917 -0
  85. package/vendor/wajha/templates/builtin/aurora.ts +163 -0
  86. package/vendor/wajha/templates/builtin/bubblegum.ts +132 -0
  87. package/vendor/wajha/templates/builtin/citrusPop.ts +124 -0
  88. package/vendor/wajha/templates/builtin/clayGarden.ts +114 -0
  89. package/vendor/wajha/templates/builtin/desertDune.ts +97 -0
  90. package/vendor/wajha/templates/builtin/glassPrism.ts +139 -0
  91. package/vendor/wajha/templates/builtin/launchpad.ts +140 -0
  92. package/vendor/wajha/templates/builtin/loudAndClear.ts +109 -0
  93. package/vendor/wajha/templates/builtin/midnightNeon.ts +108 -0
  94. package/vendor/wajha/templates/builtin/monoSwiss.ts +149 -0
  95. package/vendor/wajha/templates/builtin/noirGold.ts +149 -0
  96. package/vendor/wajha/templates/builtin/noor.ts +107 -0
  97. package/vendor/wajha/templates/builtin/obsidian.ts +137 -0
  98. package/vendor/wajha/templates/builtin/panoramaPeaks.ts +145 -0
  99. package/vendor/wajha/templates/builtin/panoramaRibbon.ts +138 -0
  100. package/vendor/wajha/templates/builtin/panoramaWaves.ts +138 -0
  101. package/vendor/wajha/templates/builtin/paperAndInk.ts +98 -0
  102. package/vendor/wajha/templates/builtin/sage.ts +94 -0
  103. package/vendor/wajha/templates/builtin/splitTone.ts +129 -0
  104. package/vendor/wajha/templates/builtin/sunsetBoulevard.ts +127 -0
  105. package/vendor/wajha/templates/index.ts +55 -0
  106. package/vendor/wajha/templates/kit.ts +761 -0
  107. package/vendor/wajha/ui/sortShots.ts +239 -0
@@ -0,0 +1,98 @@
1
+ /**
2
+ * A shots manifest: the small, framework-agnostic input contract.
3
+ * Paths are relative to the manifest file itself.
4
+ */
5
+ export interface ShotSlide {
6
+ /** Index in the template's slide list (default: the manifest slide's index). */
7
+ templateSlide?: number;
8
+ headline: string;
9
+ subtitle?: string;
10
+ /** Device family -> captured PNG/JPEG/WebP path. */
11
+ captures: Record<string, string>;
12
+ }
13
+
14
+ export interface ShotManifest {
15
+ app: string;
16
+ template: string;
17
+ out?: string;
18
+ structure?: 'stores' | 'flat' | 'fastlane';
19
+ languages: string[];
20
+ /** Optional subset of catalog size ids (default: every default size of the used families). */
21
+ sizes?: string[];
22
+ slides: ShotSlide[];
23
+ }
24
+
25
+ const isRecord = (value: unknown): value is Record<string, unknown> =>
26
+ typeof value === 'object' && value !== null && !Array.isArray(value);
27
+
28
+ function fail(message: string): never {
29
+ throw new Error(`Invalid manifest: ${message}`);
30
+ }
31
+
32
+ function nonEmptyString(value: unknown, path: string): string {
33
+ if (typeof value !== 'string' || value.trim() === '') fail(`"${path}" must be a non-empty string`);
34
+ return value;
35
+ }
36
+
37
+ function parseSlide(value: unknown, index: number): ShotSlide {
38
+ if (!isRecord(value)) fail(`slides[${index}] must be an object`);
39
+ const headline = nonEmptyString(value.headline, `slides[${index}].headline`);
40
+
41
+ let subtitle: string | undefined;
42
+ if (value.subtitle !== undefined) subtitle = nonEmptyString(value.subtitle, `slides[${index}].subtitle`);
43
+
44
+ let templateSlide: number | undefined;
45
+ if (value.templateSlide !== undefined) {
46
+ if (!Number.isInteger(value.templateSlide) || (value.templateSlide as number) < 0) {
47
+ fail(`slides[${index}].templateSlide must be a non-negative integer`);
48
+ }
49
+ templateSlide = value.templateSlide as number;
50
+ }
51
+
52
+ if (!isRecord(value.captures) || Object.keys(value.captures).length === 0) {
53
+ fail(`slides[${index}].captures must map a device family to a file`);
54
+ }
55
+ const captures: Record<string, string> = {};
56
+ for (const [family, file] of Object.entries(value.captures)) {
57
+ captures[family] = nonEmptyString(file, `slides[${index}].captures.${family}`);
58
+ }
59
+
60
+ return { headline, subtitle, templateSlide, captures };
61
+ }
62
+
63
+ /** Validates the manifest just enough that build errors name the offending field. */
64
+ export function parseManifest(raw: unknown): ShotManifest {
65
+ if (!isRecord(raw)) fail('expected an object');
66
+
67
+ const app = nonEmptyString(raw.app, 'app');
68
+ const template = nonEmptyString(raw.template, 'template');
69
+
70
+ if (!Array.isArray(raw.languages) || raw.languages.length === 0) {
71
+ fail('"languages" must be a non-empty array of language codes');
72
+ }
73
+ const languages = raw.languages.map((lang, i) => nonEmptyString(lang, `languages[${i}]`));
74
+
75
+ if (!Array.isArray(raw.slides) || raw.slides.length === 0) fail('"slides" must be a non-empty array');
76
+ const slides = raw.slides.map(parseSlide);
77
+
78
+ const structure = raw.structure;
79
+ if (structure !== undefined && structure !== 'stores' && structure !== 'flat' && structure !== 'fastlane') {
80
+ fail('"structure" must be "stores", "flat" or "fastlane"');
81
+ }
82
+
83
+ let sizes: string[] | undefined;
84
+ if (raw.sizes !== undefined) {
85
+ if (!Array.isArray(raw.sizes) || raw.sizes.length === 0) fail('"sizes" must be a non-empty array of size ids');
86
+ sizes = raw.sizes.map((size, i) => nonEmptyString(size, `sizes[${i}]`));
87
+ }
88
+
89
+ return {
90
+ app,
91
+ template,
92
+ out: raw.out === undefined ? undefined : nonEmptyString(raw.out, 'out'),
93
+ structure: structure as ShotManifest['structure'],
94
+ languages,
95
+ sizes,
96
+ slides,
97
+ };
98
+ }
package/src/render.ts ADDED
@@ -0,0 +1,75 @@
1
+ /**
2
+ * Render one slide at one exact size to an RGB PNG.
3
+ * The Wajha engine is pure Canvas 2D; @napi-rs/canvas supplies the context in Node.
4
+ */
5
+ import { loadImage, type Image } from '@napi-rs/canvas';
6
+ import type { AssetInfo, FamilyId, LangCode, Project, Slide } from '../vendor/wajha/model/types';
7
+ import type { Ctx2D, ImageLike, RenderResult } from '../vendor/wajha/engine/types';
8
+ import { renderSlide } from '../vendor/wajha/engine';
9
+ import { isRtl } from '../vendor/wajha/model/catalog';
10
+ import { mirrorsLayout } from '../vendor/wajha/model/mirror';
11
+ import { resolveLayout } from '../vendor/wajha/model/resolve';
12
+ import { languageTypography } from '../vendor/wajha/model/typography';
13
+ import { nodeEnv } from '../vendor/wajha/engine/testing/canvasNode';
14
+ import { encodePng } from '../vendor/wajha/export/png';
15
+
16
+ export interface RenderedPng {
17
+ bytes: Uint8Array;
18
+ result: RenderResult;
19
+ }
20
+
21
+ export interface RenderOptions {
22
+ project: Project;
23
+ slide: Slide;
24
+ slideIndex: number;
25
+ family: FamilyId;
26
+ lang: LangCode;
27
+ width: number;
28
+ height: number;
29
+ images: Map<string, ImageLike>;
30
+ }
31
+
32
+ const imageCache = new Map<string, Image>();
33
+
34
+ /** Loads a capture once per process; the engine only needs drawImage + width/height. */
35
+ export async function loadCapture(path: string): Promise<ImageLike> {
36
+ const cached = imageCache.get(path);
37
+ if (cached) return cached as unknown as ImageLike;
38
+ const image = await loadImage(path);
39
+ imageCache.set(path, image);
40
+ return image as unknown as ImageLike;
41
+ }
42
+
43
+ /** Built from AssetInfo so layout does not wait on decoding. */
44
+ export function assetAspect(info: AssetInfo | undefined): number | null {
45
+ if (!info) return null;
46
+ const { width, height } = info;
47
+ return width > 0 && height > 0 ? height / width : null;
48
+ }
49
+
50
+ export function renderSlidePng(opts: RenderOptions): RenderedPng {
51
+ const { project, slide, slideIndex, family, lang, width, height, images } = opts;
52
+ const layout = resolveLayout(project, slide, family);
53
+ const { ctx } = nodeEnv.createCanvas(width, height);
54
+ const result = renderSlide(ctx as Ctx2D, {
55
+ width,
56
+ height,
57
+ family,
58
+ slide,
59
+ layout,
60
+ theme: project.theme,
61
+ lang,
62
+ baseLang: project.languages[0] ?? lang,
63
+ rtl: isRtl(lang),
64
+ mirror: mirrorsLayout(project, lang),
65
+ typography: languageTypography(project, lang),
66
+ slideIndex,
67
+ slideCount: project.slides.length,
68
+ getImage: (assetId) => images.get(assetId) ?? null,
69
+ getAssetAspect: (assetId) => assetAspect(project.assets[assetId]),
70
+ placeholder: 'empty',
71
+ env: nodeEnv,
72
+ });
73
+ const data = ctx.getImageData(0, 0, width, height).data;
74
+ return { bytes: encodePng(data, width, height), result };
75
+ }
package/src/verify.ts ADDED
@@ -0,0 +1,90 @@
1
+ /**
2
+ * capture-flow verify: checks the built tree, not the renderer's intent.
3
+ * A file is valid only when its IHDR says 8-bit RGB (colour type 2) at the
4
+ * exact size the store asks for.
5
+ */
6
+ import { existsSync, readFileSync, readdirSync, statSync } from 'node:fs';
7
+ import { join, relative, resolve } from 'node:path';
8
+ import type { BuildReport } from './build';
9
+
10
+ export interface VerifiedFile {
11
+ path: string;
12
+ width: number;
13
+ height: number;
14
+ colorType: number;
15
+ bitDepth: number;
16
+ bytes: number;
17
+ }
18
+
19
+ const pngHeader = (file: string): Omit<VerifiedFile, 'path' | 'bytes'> | null => {
20
+ const data = readFileSync(file);
21
+ if (data.length < 33) return null;
22
+ const signature = data.subarray(0, 8).toString('hex');
23
+ if (signature !== '89504e470d0a1a0a') return null;
24
+ if (data.subarray(12, 16).toString('ascii') !== 'IHDR') return null;
25
+ return {
26
+ width: data.readUInt32BE(16),
27
+ height: data.readUInt32BE(20),
28
+ bitDepth: data[24],
29
+ colorType: data[25],
30
+ };
31
+ };
32
+
33
+ function walk(dir: string): string[] {
34
+ const out: string[] = [];
35
+ for (const name of readdirSync(dir)) {
36
+ const full = join(dir, name);
37
+ if (statSync(full).isDirectory()) out.push(...walk(full));
38
+ else if (/\.(png|jpe?g)$/i.test(name)) out.push(full);
39
+ }
40
+ return out;
41
+ }
42
+
43
+ export interface VerifyResult {
44
+ ok: boolean;
45
+ files: VerifiedFile[];
46
+ errors: string[];
47
+ }
48
+
49
+ export function verify(dir: string): VerifyResult {
50
+ const root = resolve(dir);
51
+ const errors: string[] = [];
52
+ const files: VerifiedFile[] = [];
53
+ const reportPath = join(root, 'capture-flow-report.json');
54
+ if (!existsSync(root)) return { ok: false, files, errors: [`missing output directory: ${root}`] };
55
+
56
+ const found = new Map<string, string>();
57
+ for (const file of walk(root)) {
58
+ const rel = relative(root, file).replaceAll('\\', '/');
59
+ const header = pngHeader(file);
60
+ if (!header) {
61
+ if (file.toLowerCase().endsWith('.png')) errors.push(`${rel}: not a readable PNG`);
62
+ continue;
63
+ }
64
+ found.set(rel, file);
65
+ files.push({ ...header, path: rel, bytes: statSync(file).size });
66
+ if (!file.toLowerCase().endsWith('.png')) continue;
67
+ if (header.bitDepth !== 8 || header.colorType !== 2) {
68
+ errors.push(`${rel}: expected 8-bit RGB without alpha, got bit depth ${header.bitDepth}, colour type ${header.colorType}`);
69
+ }
70
+ }
71
+
72
+ if (existsSync(reportPath)) {
73
+ const report = JSON.parse(readFileSync(reportPath, 'utf8')) as BuildReport;
74
+ for (const expected of report.files) {
75
+ const file = found.get(expected.path);
76
+ if (!file) {
77
+ errors.push(`${expected.path}: missing`);
78
+ continue;
79
+ }
80
+ const header = pngHeader(file);
81
+ if (header && (header.width !== expected.width || header.height !== expected.height)) {
82
+ errors.push(`${expected.path}: expected ${expected.width}x${expected.height}, got ${header.width}x${header.height}`);
83
+ }
84
+ }
85
+ } else {
86
+ errors.push('capture-flow-report.json: missing, cannot check the plan against the files');
87
+ }
88
+
89
+ return { ok: errors.length === 0, files, errors };
90
+ }
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Wajha contributors
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,21 @@
1
+ vendor/wajha is derived from Wajha — https://github.com/M-Alkassem/Wajha
2
+
3
+ Copyright (c) 2026 Wajha contributors, released under the MIT License (see
4
+ LICENSE in this directory).
5
+
6
+ Vendored paths:
7
+ model/ project model, catalogue, templates, typography, validation
8
+ engine/ Canvas 2D renderer (plus the Node test adapter)
9
+ export/ store folder/file naming (paths.ts) and PNG encoding (png.ts)
10
+ templates/ the built-in template set
11
+ ui/sortShots.ts slide ordering helpers
12
+
13
+ Changes for capture-flow:
14
+ - imports kept as-is except for the renamed Node adapter
15
+ (engine/testing/canvasNode.ts) and its font root;
16
+ - export/csv.ts, export/index.ts, export/plan.ts, export/runner.ts,
17
+ export/worker.ts and export/png.worker.ts removed (browser-only or unused);
18
+ - ExportTarget widened to string so a CLI can name its own sizes;
19
+ - ui/sortShots.ts import paths repointed at ../model.
20
+
21
+ The MIT notice in LICENSE applies to these files and to this repository.
@@ -0,0 +1,55 @@
1
+ /**
2
+ * Slide backgrounds. A slide-level background (layout.background) is drawn
3
+ * over the slide alone. The theme background is too, unless the theme is a
4
+ * panorama: then it is laid out over one virtual canvas `slideCount x
5
+ * width` wide and each slide shows its own window of it, so gradients,
6
+ * mesh blobs, images and patterns continue across the store listing.
7
+ */
8
+ import type { Background } from '../model/types';
9
+ import type { Ctx2D } from './types';
10
+ import { paintFill, type PaintContext } from './paint';
11
+ import { paintPattern } from './patterns';
12
+ import { clamp, num, type Rect } from './geometry';
13
+
14
+ export interface BackgroundInput {
15
+ width: number;
16
+ height: number;
17
+ /** Slide-level override; never panoramic. */
18
+ own: Background | null;
19
+ theme: Background | null;
20
+ panorama: boolean;
21
+ slideIndex: number;
22
+ slideCount: number;
23
+ }
24
+
25
+ /** The painted box and the visible window of a slide background, in slide pixels. */
26
+ export function backgroundFrame(input: BackgroundInput): { background: Background | null; box: Rect; offset: number } {
27
+ const { width: w, height: h } = input;
28
+ const own = input.own && typeof input.own === 'object' ? input.own : null;
29
+ const background = own ?? (input.theme && typeof input.theme === 'object' ? input.theme : null);
30
+ const count = Math.floor(clamp(num(input.slideCount, 1), 1, 1000));
31
+ if (own || !input.panorama || count < 2) return { background, box: { x: 0, y: 0, w, h }, offset: 0 };
32
+ const index = Math.floor(clamp(num(input.slideIndex, 0), 0, count - 1));
33
+ return { background, box: { x: 0, y: 0, w: w * count, h }, offset: index * w };
34
+ }
35
+
36
+ /**
37
+ * Paints white (exports are flattened onto white anyway, and JPEG would
38
+ * turn transparency black), then the background fill and its pattern.
39
+ */
40
+ export function paintBackground(ctx: Ctx2D, input: BackgroundInput, pc: PaintContext): void {
41
+ const { width: w, height: h } = input;
42
+ ctx.fillStyle = '#ffffff';
43
+ ctx.fillRect(0, 0, w, h);
44
+ const { background, box, offset } = backgroundFrame(input);
45
+ if (!background) return;
46
+ ctx.save();
47
+ try {
48
+ ctx.translate(-offset, 0);
49
+ const area: Rect = { x: offset, y: 0, w, h };
50
+ paintFill(ctx, background.fill, box, pc, area);
51
+ paintPattern(ctx, background.pattern, box, area, pc.u, pc.env, pc.pxScale);
52
+ } finally {
53
+ ctx.restore();
54
+ }
55
+ }