@waits/cadence 0.3.0 → 0.4.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@waits/cadence",
3
- "version": "0.3.0",
3
+ "version": "0.4.0",
4
4
  "type": "module",
5
5
  "license": "MIT",
6
6
  "bin": {
@@ -0,0 +1,74 @@
1
+ /**
2
+ * Project-local `.cadence/` resolution shared by render / storyboard / make.
3
+ *
4
+ * A repo captures its own cadence setup under `<project>/.cadence/`:
5
+ * - `theme.json` — brand (colors, mono font, code syntax theme, code chrome)
6
+ * cadence discovers it automatically when run against the project, and writes
7
+ * **outputs alongside it** (`<project>/.cadence/out/`) so renders are anchored to
8
+ * the project, not the current directory. No project-specific files live in the
9
+ * cadence engine itself.
10
+ *
11
+ * Theme precedence: --theme-file > --theme <name> > .cadence/theme.json > default.
12
+ * Output dir: --out <dir> > <project>/.cadence/out > ./out.
13
+ */
14
+ import { existsSync } from "node:fs";
15
+ import { dirname, join, resolve } from "node:path";
16
+ import { THEMES } from "../src/theme";
17
+
18
+ /** Walk up from `start` (max 6 levels) for a `.cadence` directory. */
19
+ export function findCadenceDir(start: string): string | undefined {
20
+ let dir = resolve(start);
21
+ for (let i = 0; i < 6; i++) {
22
+ const p = join(dir, ".cadence");
23
+ if (existsSync(p)) return p;
24
+ const parent = dirname(dir);
25
+ if (parent === dir) break;
26
+ dir = parent;
27
+ }
28
+ return undefined;
29
+ }
30
+
31
+ /** A project's `.cadence/theme.json`, if one exists near `start`. */
32
+ export function discoverProjectTheme(start: string): string | undefined {
33
+ const cad = findCadenceDir(start);
34
+ if (!cad) return undefined;
35
+ const p = join(cad, "theme.json");
36
+ return existsSync(p) ? p : undefined;
37
+ }
38
+
39
+ /**
40
+ * Where renders are written. Precedence: an explicit `--out <dir>` > a project's
41
+ * `<project>/.cadence/out` (so outputs are anchored to the project regardless of
42
+ * cwd) > `./out`. `beatsFile` (when present) anchors discovery near the beats
43
+ * file; otherwise discovery starts from the current directory.
44
+ */
45
+ export function resolveOutDir(opts: { outFlag?: string; beatsFile?: string }): string {
46
+ if (opts.outFlag) return resolve(opts.outFlag);
47
+ const start = opts.beatsFile ? dirname(resolve(opts.beatsFile)) : process.cwd();
48
+ const cad = findCadenceDir(start);
49
+ return cad ? join(cad, "out") : "out";
50
+ }
51
+
52
+ /**
53
+ * Resolve which theme to use. Returns the (possibly auto-discovered) `themeFile`
54
+ * and/or named `theme`. Exits on an unknown named theme. Logs when a project
55
+ * theme is auto-discovered so it's never a silent surprise.
56
+ */
57
+ export function resolveTheme(opts: { theme?: string; themeFile?: string; beatsFile?: string }): {
58
+ theme?: string;
59
+ themeFile?: string;
60
+ } {
61
+ let { theme, themeFile } = opts;
62
+ if (!themeFile && !theme && opts.beatsFile) {
63
+ const found = discoverProjectTheme(dirname(resolve(opts.beatsFile)));
64
+ if (found) {
65
+ themeFile = found;
66
+ console.error(`· using project theme ${found}`);
67
+ }
68
+ }
69
+ if (theme && !themeFile && !THEMES[theme]) {
70
+ console.error(`unknown --theme "${theme}". have: ${Object.keys(THEMES).join(", ")}`);
71
+ process.exit(1);
72
+ }
73
+ return { theme, themeFile };
74
+ }
package/scripts/cli.ts CHANGED
@@ -47,7 +47,8 @@ utilities:
47
47
  templates list available templates
48
48
  themes list available themes
49
49
 
50
- flags shared by create/render/redesign: --format 16x9|1x1|9x16, --theme <name>, --theme-file <path>, --frame <n>
50
+ flags shared by create/render/redesign: --format 16x9|1x1|9x16, --theme <name>, --theme-file <path>, --frame <n>, --out <dir>
51
+ outputs: written to <project>/.cadence/out when run against a repo with a .cadence/ dir, else ./out (override with --out)
51
52
  preview before rendering: cadence storyboard <beats> · cadence create … --dry-run (plan + one still per beat, no MP4)`;
52
53
 
53
54
  if (!sub || sub === "help" || sub === "--help") {
package/scripts/make.ts CHANGED
@@ -13,6 +13,7 @@ import { manifestFromChangelogText, manifestFromReleaseBody } from "../src/adapt
13
13
  import type { BackgroundSpec } from "../src/templates";
14
14
  import { TEMPLATES } from "../src/templates";
15
15
  import { binPath, pkgFile } from "./_pkg";
16
+ import { resolveOutDir } from "./_theme";
16
17
 
17
18
  const args = process.argv.slice(2);
18
19
  const flag = (n: string) => {
@@ -68,18 +69,21 @@ const beats = template(manifest, {
68
69
  console.error(`· ${manifest.product} ${manifest.version}: ${manifest.features.length} features${manifest.dropped ? ` (+${manifest.dropped} dropped)` : ""} → ${templateName}`);
69
70
 
70
71
  // 3. Write beats JSON, then render — or, with --dry-run, storyboard it (preview
71
- // sheet + plan, no MP4).
72
- mkdirSync("out", { recursive: true });
73
- const jsonPath = join("out", `make-${manifest.product}.beats.json`);
72
+ // sheet + plan, no MP4). Outputs go to the project's .cadence/out (or --out).
73
+ const outDir = resolveOutDir({ outFlag: flag("--out") });
74
+ mkdirSync(outDir, { recursive: true });
75
+ const jsonPath = join(outDir, `make-${manifest.product}.beats.json`);
74
76
  writeFileSync(jsonPath, JSON.stringify(beats));
77
+ // Pin the child to the same out dir so beats + renders colocate.
78
+ const outArgs = ["--out", outDir];
75
79
 
76
80
  if (args.includes("--dry-run")) {
77
81
  // repo → storyboard. --frame is meaningless here; --theme-file is supported.
78
82
  const passthru = ["--format", "--theme", "--theme-file"].flatMap((f) => (flag(f) ? [f, flag(f)!] : []));
79
- const res = spawnSync(binPath("tsx"), [pkgFile("scripts/storyboard.ts"), jsonPath, ...passthru], { stdio: "inherit" });
83
+ const res = spawnSync(binPath("tsx"), [pkgFile("scripts/storyboard.ts"), jsonPath, ...passthru, ...outArgs], { stdio: "inherit" });
80
84
  process.exit(res.status ?? 0);
81
85
  }
82
86
 
83
87
  const passthru = ["--format", "--theme", "--frame"].flatMap((f) => (flag(f) ? [f, flag(f)!] : []));
84
- const res = spawnSync(binPath("tsx"), [pkgFile("scripts/render.ts"), jsonPath, ...passthru], { stdio: "inherit" });
88
+ const res = spawnSync(binPath("tsx"), [pkgFile("scripts/render.ts"), jsonPath, ...passthru, ...outArgs], { stdio: "inherit" });
85
89
  process.exit(res.status ?? 0);
package/scripts/render.ts CHANGED
@@ -10,9 +10,9 @@ import { spawnSync } from "node:child_process";
10
10
  import { mkdirSync, readFileSync, writeFileSync } from "node:fs";
11
11
  import { basename, join, resolve } from "node:path";
12
12
  import { type Format } from "../src/schema/beats";
13
- import { THEMES } from "../src/theme";
14
13
  import { loadBeats } from "./_beats";
15
14
  import { binPath, pkgFile } from "./_pkg";
15
+ import { resolveOutDir, resolveTheme } from "./_theme";
16
16
 
17
17
  const args = process.argv.slice(2);
18
18
  const getFlag = (name: string) => {
@@ -34,26 +34,22 @@ const parsed = await loadBeats(file);
34
34
  const fmt = getFlag("--format") as Format | undefined;
35
35
  if (fmt) parsed.format = fmt;
36
36
  const frame = getFlag("--frame");
37
- const theme = getFlag("--theme");
38
- const themeFile = getFlag("--theme-file");
39
- if (theme && !themeFile && !THEMES[theme]) {
40
- console.error(`unknown --theme "${theme}". have: ${Object.keys(THEMES).join(", ")}`);
41
- process.exit(1);
42
- }
37
+ const { theme, themeFile } = resolveTheme({ theme: getFlag("--theme"), themeFile: getFlag("--theme-file"), beatsFile: file });
43
38
  const env: NodeJS.ProcessEnv = { ...process.env };
44
39
  if (theme) env.REMOTION_VIDEO_THEME = theme;
45
40
  if (themeFile) env.REMOTION_VIDEO_THEME_JSON = readFileSync(resolve(themeFile), "utf8");
46
41
 
47
- mkdirSync("out", { recursive: true });
42
+ const outDir = resolveOutDir({ outFlag: getFlag("--out"), beatsFile: file });
43
+ mkdirSync(outDir, { recursive: true });
48
44
  const name = basename(file).replace(/\.beats\.(ts|js|json)$/, "").replace(/\.(ts|js|json)$/, "");
49
- const propsPath = join("out", `.props-${name}-${parsed.format}.json`);
45
+ const propsPath = join(outDir, `.props-${name}-${parsed.format}.json`);
50
46
  writeFileSync(propsPath, JSON.stringify(parsed));
51
47
 
52
48
  const bin = binPath("remotion");
53
49
  const common = [pkgFile("src/index.ts"), "Changelog"];
54
50
  const suffix = theme && theme !== "default" ? `-${theme}` : "";
55
51
  const res = frame
56
- ? spawnSync(bin, ["still", ...common, join("out", `${name}-${parsed.format}${suffix}-f${frame}.png`), `--props=${propsPath}`, `--frame=${frame}`], { stdio: "inherit", env })
57
- : spawnSync(bin, ["render", ...common, join("out", `${name}-${parsed.format}${suffix}.mp4`), `--props=${propsPath}`, "--image-format=jpeg"], { stdio: "inherit", env });
52
+ ? spawnSync(bin, ["still", ...common, join(outDir, `${name}-${parsed.format}${suffix}-f${frame}.png`), `--props=${propsPath}`, `--frame=${frame}`], { stdio: "inherit", env })
53
+ : spawnSync(bin, ["render", ...common, join(outDir, `${name}-${parsed.format}${suffix}.mp4`), `--props=${propsPath}`, "--image-format=jpeg"], { stdio: "inherit", env });
58
54
 
59
55
  process.exit(res.status ?? 0);
@@ -15,10 +15,10 @@ import { existsSync, mkdirSync, readFileSync, rmSync, statSync, writeFileSync }
15
15
  import { basename, join } from "node:path";
16
16
  import { type Beat, type Format } from "../src/schema/beats";
17
17
  import type { StoryboardCell, StoryboardProps } from "../src/components/Storyboard";
18
- import { THEMES } from "../src/theme";
19
18
  import { auditBeats, ICON, rankFindings } from "./_audit";
20
19
  import { beatTimings, FPS, loadBeats } from "./_beats";
21
20
  import { binPath, pkgFile } from "./_pkg";
21
+ import { resolveOutDir, resolveTheme } from "./_theme";
22
22
 
23
23
  const args = process.argv.slice(2);
24
24
  const getFlag = (name: string) => {
@@ -34,12 +34,7 @@ if (!file) {
34
34
  process.exit(1);
35
35
  }
36
36
 
37
- const theme = getFlag("--theme");
38
- const themeFile = getFlag("--theme-file");
39
- if (theme && !themeFile && !THEMES[theme]) {
40
- console.error(`unknown --theme "${theme}". have: ${Object.keys(THEMES).join(", ")}`);
41
- process.exit(1);
42
- }
37
+ const { theme, themeFile } = resolveTheme({ theme: getFlag("--theme"), themeFile: getFlag("--theme-file"), beatsFile: file });
43
38
 
44
39
  /** Short label for a beat's backdrop (omitted background = the procedural default). */
45
40
  const bgTag = (b: Beat): string => {
@@ -83,8 +78,9 @@ const env: NodeJS.ProcessEnv = { ...process.env };
83
78
  if (theme) env.REMOTION_VIDEO_THEME = theme;
84
79
  if (themeFile) env.REMOTION_VIDEO_THEME_JSON = readFileSync(themeFile, "utf8");
85
80
 
86
- mkdirSync("out", { recursive: true });
87
- const tmp = join("out", `.sb-${name}`);
81
+ const outDir = resolveOutDir({ outFlag: getFlag("--out"), beatsFile: file });
82
+ mkdirSync(outDir, { recursive: true });
83
+ const tmp = join(outDir, `.sb-${name}`);
88
84
  mkdirSync(tmp, { recursive: true });
89
85
  const propsPath = join(tmp, "props.json");
90
86
  writeFileSync(propsPath, JSON.stringify(parsed));
@@ -115,7 +111,7 @@ const sheetProps: StoryboardProps = { format: fmt, name, cells };
115
111
  const sheetPropsPath = join(tmp, "sheet.json");
116
112
  writeFileSync(sheetPropsPath, JSON.stringify(sheetProps));
117
113
 
118
- const outPng = join("out", `${name}.storyboard.png`);
114
+ const outPng = join(outDir, `${name}.storyboard.png`);
119
115
  const sheet = spawnSync(bin, ["still", entry, "Storyboard", outPng, `--props=${sheetPropsPath}`], {
120
116
  stdio: ["ignore", "ignore", "inherit"],
121
117
  env,
@@ -9,6 +9,9 @@ export const COLORS = activeTheme.colors;
9
9
 
10
10
  export const CARET_BG = activeTheme.caretBg;
11
11
 
12
+ /** Code window chrome — `"window"` (floating editor) or `"minimal"` (docs-style). */
13
+ export const CODE_CHROME = activeTheme.codeChrome ?? "window";
14
+
12
15
  /** Light [from, to] gradient for the procedural default backdrop ("shapes"). */
13
16
  export const BACKDROP: [string, string] = activeTheme.backdrop ?? ["#e6edff", "#f8faff"];
14
17
 
@@ -1,5 +1,5 @@
1
1
  import { useCurrentFrame } from "remotion";
2
- import { CARET_BG, COLORS, FLOAT_SHADOW } from "../brand/tokens";
2
+ import { CARET_BG, CODE_CHROME, COLORS, FLOAT_SHADOW } from "../brand/tokens";
3
3
  import { FONTS } from "../brand/fonts";
4
4
  import { useMotion, type MotionSpec } from "../motion/useMotion";
5
5
  import { CODE_BG, type CodeLine } from "../code/highlight";
@@ -24,6 +24,7 @@ export const codeTypingDoneFrame = (tokens: CodeLine[], motion?: MotionSpec) =>
24
24
  export const CodeWindow: React.FC<Props> = ({ filename, tokens, motion = { enter: "settle", delay: 12 }, fontSize = 22 }) => {
25
25
  const frame = useCurrentFrame();
26
26
  const style = useMotion(motion);
27
+ const minimal = CODE_CHROME === "minimal";
27
28
 
28
29
  const typeStart = typeStartFor(motion);
29
30
  const total = totalChars(tokens);
@@ -81,7 +82,7 @@ export const CodeWindow: React.FC<Props> = ({ filename, tokens, motion = { enter
81
82
  <div
82
83
  style={{
83
84
  width: "100%",
84
- borderRadius: 20,
85
+ borderRadius: minimal ? 12 : 20,
85
86
  background: `${CODE_BG}f7`,
86
87
  boxShadow: FLOAT_SHADOW,
87
88
  backdropFilter: "blur(6px)",
@@ -90,11 +91,13 @@ export const CodeWindow: React.FC<Props> = ({ filename, tokens, motion = { enter
90
91
  ...style,
91
92
  }}
92
93
  >
93
- <div style={{ height: 54, display: "flex", alignItems: "center", padding: "0 22px", gap: 9 }}>
94
- <Dot color="#f6645f" /><Dot color="#f7bd45" /><Dot color="#2fc94e" />
95
- <span style={{ flex: 1, textAlign: "center", color: "rgba(0,0,0,0.34)", fontSize: 15, fontFamily: FONTS.mono, marginRight: 60 }}>{filename}</span>
96
- </div>
97
- <div style={{ padding: "26px 40px 42px", fontFamily: FONTS.mono, fontSize, lineHeight: `${lineHeight}px`, color: COLORS.ink }}>
94
+ {!minimal && (
95
+ <div style={{ height: 54, display: "flex", alignItems: "center", padding: "0 22px", gap: 9 }}>
96
+ <Dot color="#f6645f" /><Dot color="#f7bd45" /><Dot color="#2fc94e" />
97
+ <span style={{ flex: 1, textAlign: "center", color: "rgba(0,0,0,0.34)", fontSize: 15, fontFamily: FONTS.mono, marginRight: 60 }}>{filename}</span>
98
+ </div>
99
+ )}
100
+ <div style={{ padding: minimal ? "30px 34px" : "26px 40px 42px", fontFamily: FONTS.mono, fontSize, lineHeight: `${lineHeight}px`, color: COLORS.ink }}>
98
101
  {lines}
99
102
  </div>
100
103
  </div>
@@ -62,6 +62,12 @@ export type ThemeConfig = {
62
62
  caretBg: string;
63
63
  /** Syntax colors. */
64
64
  codeTheme: CodeTheme;
65
+ /**
66
+ * Code window chrome. `"window"` (default) is a floating editor with traffic-
67
+ * light dots + a filename tab; `"minimal"` is chromeless (just the code surface)
68
+ * to match a docs-style snippet component.
69
+ */
70
+ codeChrome?: "window" | "minimal";
65
71
  /** Light [from, to] gradient for the procedural default backdrop (Background "shapes"). */
66
72
  backdrop?: [string, string];
67
73
  };