hyperframes 0.1.7 → 0.1.8

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/dist/cli.js CHANGED
@@ -422,7 +422,7 @@ var VERSION;
422
422
  var init_version = __esm({
423
423
  "src/version.ts"() {
424
424
  "use strict";
425
- VERSION = true ? "0.1.7" : "0.0.0-dev";
425
+ VERSION = true ? "0.1.8" : "0.0.0-dev";
426
426
  }
427
427
  });
428
428
 
@@ -3499,6 +3499,37 @@ function lintHyperframeHtml(html, options = {}) {
3499
3499
  fixHint: "Register each composition timeline on `window.__timelines[compositionId]`."
3500
3500
  });
3501
3501
  }
3502
+ if (TIMELINE_REGISTRY_ASSIGN_PATTERN.test(source) && !TIMELINE_REGISTRY_INIT_PATTERN.test(source)) {
3503
+ pushFinding({
3504
+ code: "timeline_registry_missing_init",
3505
+ severity: "error",
3506
+ message: "`window.__timelines[\u2026] = \u2026` is used without initializing `window.__timelines` first.",
3507
+ fixHint: "Add `window.__timelines = window.__timelines || {};` before any timeline assignment."
3508
+ });
3509
+ }
3510
+ {
3511
+ const htmlCompIds = /* @__PURE__ */ new Set();
3512
+ const timelineRegKeys = /* @__PURE__ */ new Set();
3513
+ const compIdRe = /data-composition-id\s*=\s*["']([^"']+)["']/gi;
3514
+ const tlKeyRe = /window\.__timelines\[\s*["']([^"']+)["']\s*\]/g;
3515
+ let m;
3516
+ while ((m = compIdRe.exec(source)) !== null) {
3517
+ if (m[1]) htmlCompIds.add(m[1]);
3518
+ }
3519
+ while ((m = tlKeyRe.exec(source)) !== null) {
3520
+ if (m[1]) timelineRegKeys.add(m[1]);
3521
+ }
3522
+ for (const key2 of timelineRegKeys) {
3523
+ if (!htmlCompIds.has(key2)) {
3524
+ pushFinding({
3525
+ code: "timeline_id_mismatch",
3526
+ severity: "error",
3527
+ message: `Timeline registered as "${key2}" but no element has data-composition-id="${key2}". The runtime cannot auto-nest this timeline.`,
3528
+ fixHint: `Change window.__timelines["${key2}"] to match the data-composition-id attribute, or vice versa.`
3529
+ });
3530
+ }
3531
+ }
3532
+ }
3502
3533
  if (INVALID_SCRIPT_CLOSE_PATTERN.test(source)) {
3503
3534
  pushFinding({
3504
3535
  code: "invalid_inline_script_syntax",
@@ -23796,7 +23827,11 @@ __export(render_exports, {
23796
23827
  default: () => render_default
23797
23828
  });
23798
23829
  import { existsSync as existsSync24, mkdirSync as mkdirSync16, statSync as statSync9 } from "fs";
23830
+ import { cpus as cpus2 } from "os";
23799
23831
  import { resolve as resolve13, dirname as dirname12, join as join22 } from "path";
23832
+ function defaultWorkerCount() {
23833
+ return Math.max(1, Math.min(Math.floor(CPU_CORE_COUNT / 2), 4));
23834
+ }
23800
23835
  async function renderDocker(projectDir, outputPath, options) {
23801
23836
  const producer = await loadProducer();
23802
23837
  const startTime = Date.now();
@@ -23810,7 +23845,11 @@ async function renderDocker(projectDir, outputPath, options) {
23810
23845
  });
23811
23846
  await producer.executeRenderJob(job, projectDir, outputPath);
23812
23847
  } catch (error) {
23813
- trackRenderError({ fps: options.fps, quality: options.quality, docker: true });
23848
+ trackRenderError({
23849
+ fps: options.fps,
23850
+ quality: options.quality,
23851
+ docker: true
23852
+ });
23814
23853
  const message = error instanceof Error ? error.message : String(error);
23815
23854
  errorBox("Render failed", message, "Check Docker is running: docker info");
23816
23855
  process.exit(1);
@@ -23820,7 +23859,7 @@ async function renderDocker(projectDir, outputPath, options) {
23820
23859
  durationMs: elapsed,
23821
23860
  fps: options.fps,
23822
23861
  quality: options.quality,
23823
- workers: options.workers ?? 4,
23862
+ workers: options.workers,
23824
23863
  docker: true,
23825
23864
  gpu: options.gpu
23826
23865
  });
@@ -23845,7 +23884,11 @@ async function renderLocal(projectDir, outputPath, options) {
23845
23884
  try {
23846
23885
  await producer.executeRenderJob(job, projectDir, outputPath, onProgress);
23847
23886
  } catch (error) {
23848
- trackRenderError({ fps: options.fps, quality: options.quality, docker: false });
23887
+ trackRenderError({
23888
+ fps: options.fps,
23889
+ quality: options.quality,
23890
+ docker: false
23891
+ });
23849
23892
  const message = error instanceof Error ? error.message : String(error);
23850
23893
  errorBox("Render failed", message, "Try --docker for containerized rendering");
23851
23894
  process.exit(1);
@@ -23855,7 +23898,7 @@ async function renderLocal(projectDir, outputPath, options) {
23855
23898
  durationMs: elapsed,
23856
23899
  fps: options.fps,
23857
23900
  quality: options.quality,
23858
- workers: options.workers ?? 4,
23901
+ workers: options.workers,
23859
23902
  docker: false,
23860
23903
  gpu: options.gpu
23861
23904
  });
@@ -23873,7 +23916,7 @@ function printRenderComplete(outputPath, elapsedMs, quiet) {
23873
23916
  console.log(c.success("\u25C7") + " " + c.accent(outputPath));
23874
23917
  console.log(" " + c.bold(fileSize) + c.dim(" \xB7 " + duration + " \xB7 completed"));
23875
23918
  }
23876
- var VALID_FPS, VALID_QUALITY, VALID_FORMAT, render_default;
23919
+ var VALID_FPS, VALID_QUALITY, VALID_FORMAT, CPU_CORE_COUNT, render_default;
23877
23920
  var init_render = __esm({
23878
23921
  "src/commands/render.ts"() {
23879
23922
  "use strict";
@@ -23887,6 +23930,7 @@ var init_render = __esm({
23887
23930
  VALID_FPS = /* @__PURE__ */ new Set([24, 30, 60]);
23888
23931
  VALID_QUALITY = /* @__PURE__ */ new Set(["draft", "standard", "high"]);
23889
23932
  VALID_FORMAT = /* @__PURE__ */ new Set(["mp4", "webm"]);
23933
+ CPU_CORE_COUNT = cpus2().length;
23890
23934
  render_default = defineCommand({
23891
23935
  meta: {
23892
23936
  name: "render",
@@ -23899,19 +23943,45 @@ Examples:
23899
23943
  hyperframes render --docker --output deterministic.mp4`
23900
23944
  },
23901
23945
  args: {
23902
- dir: { type: "positional", description: "Project directory", required: false },
23903
- output: { type: "string", description: "Output path (default: renders/<name>.mp4)" },
23904
- fps: { type: "string", description: "Frame rate: 24, 30, 60", default: "30" },
23905
- quality: { type: "string", description: "Quality: draft, standard, high", default: "standard" },
23946
+ dir: {
23947
+ type: "positional",
23948
+ description: "Project directory",
23949
+ required: false
23950
+ },
23951
+ output: {
23952
+ type: "string",
23953
+ description: "Output path (default: renders/<name>.mp4)"
23954
+ },
23955
+ fps: {
23956
+ type: "string",
23957
+ description: "Frame rate: 24, 30, 60",
23958
+ default: "30"
23959
+ },
23960
+ quality: {
23961
+ type: "string",
23962
+ description: "Quality: draft, standard, high",
23963
+ default: "standard"
23964
+ },
23906
23965
  format: {
23907
23966
  type: "string",
23908
23967
  description: "Output format: mp4, webm (WebM renders with transparency)",
23909
23968
  default: "mp4"
23910
23969
  },
23911
- workers: { type: "string", description: "Parallel workers 1-8" },
23912
- docker: { type: "boolean", description: "Use Docker for deterministic render", default: false },
23970
+ workers: {
23971
+ type: "string",
23972
+ description: "Parallel render workers (1-8 or 'auto'). Default: half your CPU cores, max 4. Each worker launches a separate Chrome process."
23973
+ },
23974
+ docker: {
23975
+ type: "boolean",
23976
+ description: "Use Docker for deterministic render",
23977
+ default: false
23978
+ },
23913
23979
  gpu: { type: "boolean", description: "Use GPU encoding", default: false },
23914
- quiet: { type: "boolean", description: "Suppress verbose output", default: false }
23980
+ quiet: {
23981
+ type: "boolean",
23982
+ description: "Suppress verbose output",
23983
+ default: false
23984
+ }
23915
23985
  },
23916
23986
  async run({ args }) {
23917
23987
  const project = resolveProject(args.dir);
@@ -23934,10 +24004,10 @@ Examples:
23934
24004
  }
23935
24005
  const format = formatRaw;
23936
24006
  let workers;
23937
- if (args.workers != null) {
24007
+ if (args.workers != null && args.workers !== "auto") {
23938
24008
  const parsed = parseInt(args.workers, 10);
23939
24009
  if (isNaN(parsed) || parsed < 1 || parsed > 8) {
23940
- errorBox("Invalid workers", `Got "${args.workers}". Must be between 1 and 8.`);
24010
+ errorBox("Invalid workers", `Got "${args.workers}". Must be 1-8 or "auto".`);
23941
24011
  process.exit(1);
23942
24012
  }
23943
24013
  workers = parsed;
@@ -23945,22 +24015,18 @@ Examples:
23945
24015
  const rendersDir = resolve13("renders");
23946
24016
  const ext = format === "webm" ? ".webm" : ".mp4";
23947
24017
  const outputPath = args.output ? resolve13(args.output) : join22(rendersDir, `${project.name}${ext}`);
23948
- const outputDir = dirname12(outputPath);
23949
- if (!existsSync24(outputDir)) {
23950
- mkdirSync16(outputDir, { recursive: true });
23951
- }
24018
+ mkdirSync16(dirname12(outputPath), { recursive: true });
23952
24019
  const useDocker = args.docker ?? false;
23953
24020
  const useGpu = args.gpu ?? false;
23954
24021
  const quiet = args.quiet ?? false;
23955
- const workerCount = workers ?? 4;
24022
+ const workerCount = workers ?? defaultWorkerCount();
23956
24023
  if (!quiet) {
24024
+ const workerLabel = args.workers != null ? `${workerCount} workers` : `${workerCount} workers (auto \u2014 half of ${CPU_CORE_COUNT} cores)`;
23957
24025
  console.log("");
23958
24026
  console.log(
23959
24027
  c.accent("\u25C6") + " Rendering " + c.accent(project.name) + c.dim(" \u2192 " + outputPath)
23960
24028
  );
23961
- console.log(
23962
- c.dim(" " + fps + "fps \xB7 " + quality + " \xB7 " + workerCount + " workers")
23963
- );
24029
+ console.log(c.dim(" " + fps + "fps \xB7 " + quality + " \xB7 " + workerLabel));
23964
24030
  console.log("");
23965
24031
  }
23966
24032
  if (!useDocker) {
@@ -24007,7 +24073,7 @@ Examples:
24007
24073
  fps,
24008
24074
  quality,
24009
24075
  format,
24010
- workers,
24076
+ workers: workerCount,
24011
24077
  gpu: useGpu,
24012
24078
  quiet
24013
24079
  });
@@ -24016,7 +24082,7 @@ Examples:
24016
24082
  fps,
24017
24083
  quality,
24018
24084
  format,
24019
- workers,
24085
+ workers: workerCount,
24020
24086
  gpu: useGpu,
24021
24087
  quiet,
24022
24088
  browserPath
@@ -137,6 +137,7 @@ Video must be `muted playsinline`. Audio is always a separate `<audio>` element:
137
137
  - From sub-compositions, use `../` to reference root files
138
138
 
139
139
  For PiP, title cards, and slide show patterns, see [patterns.md](./patterns.md).
140
+ For data, stats, and infographics, see [data-in-motion.md](./data-in-motion.md).
140
141
 
141
142
  ## Output Checklist
142
143
 
@@ -0,0 +1,19 @@
1
+ # Data in Motion
2
+
3
+ Light guidance for data and stats in video compositions. The [house style](./house-style.md) handles aesthetics — this just addresses data-specific pitfalls.
4
+
5
+ ## Visual Continuity
6
+
7
+ When successive stats belong to the same concept (Q1 → Q2 → Q3 → Q4, or three metrics for the same product), keep them in the same visual space with the same aesthetic. Only the VALUE changes. An aesthetic change should signal a new concept, not just a new number.
8
+
9
+ ## Numbers Need Visual Weight
10
+
11
+ A number on its own floats in empty space. Pair every metric with a visual element that gives it presence — a proportional fill bar, a background color shift, a shape that represents the value, a progress ring. The visual doesn't need to be a chart — it just needs to fill the frame and make the data feel tangible rather than just text on a background.
12
+
13
+ ## Avoid Web Patterns
14
+
15
+ - **No pie charts** — hard to compare, looks like PowerPoint
16
+ - **No multi-axis charts** — viewer can't study intersections in a 3-second window
17
+ - **No 6-panel dashboards** — 2-3 related metrics side-by-side is fine, 6+ is a web pattern
18
+ - **No gridlines, tick marks, or legends** — visual noise that adds nothing in motion
19
+ - **No chart library output** — build with GSAP + SVG/CSS, not D3 or Chart.js
@@ -7,7 +7,8 @@ Defaults when no `visual-style.md` or animation direction is provided. These rai
7
7
  1. **Interpret the prompt.** Generate real content for the topic — don't use the prompt text as body copy. A recipe lists real ingredients. A stats dashboard shows the actual numbers given. A product showcase names real features and specs. A sci-fi HUD has actual crosshairs and readouts, not a heading that says "sci-fi HUD."
8
8
  2. **Pick a palette.** First decide: does this content call for a light or dark canvas? Food, weddings, children, wellness, education, lifestyle, nature, and celebrations → light palette (Warm/Editorial, Clean/Corporate, Nature/Earth, Pastel/Soft). Tech, finance, cinema, nightlife, horror, gaming, and premium → dark palette. Then load the file and pick one palette. Declare your bg, fg, and accent colors before writing any code.
9
9
  3. **Pick a typeface.** Don't reach for Sora, Space Grotesk, Outfit, Playfair Display, Cormorant Garamond, or Bodoni Moda — they're overused. Explore the full range of Google Fonts. Serif for editorial, mono for technical, display for impact, handwritten for personal.
10
- 4. **Pick your entrance patterns.** Plan how elements enter never use the same entrance pattern twice in a composition.
10
+ 4. **Pick a layout approach.** Don't default to the same structure every time. Options: full-bleed centered hero, left-aligned editorial column, split-frame (content left / visual right or vice versa), scattered/asymmetric positioning, grid-based with uneven cells, stacked vertical sections. Vary this across compositions.
11
+ 5. **Pick your entrance patterns.** Plan how elements enter — never use the same entrance pattern twice in a composition.
11
12
 
12
13
  ## Motion
13
14
 
@@ -85,7 +86,7 @@ Flat single-color backgrounds look digital. Avoid pure solid backgrounds — add
85
86
 
86
87
  Beyond choosing a typeface:
87
88
 
88
- - **Weight contrast** — pair a heavy weight (700-900) headline with a light weight (300-400) body. Don't use the same weight on everything.
89
+ - **Weight contrast** — pair a heavy weight (700-900) headline with a light weight (300-400) body. Always use at least two explicit font-weight values — even with display fonts that look bold by default, set labels or secondary text to a lighter weight.
89
90
  - **Case deliberately** — ALL CAPS for labels and short text (under 5 words). Sentence case for longer text. Don't uppercase paragraphs.
90
91
  - **Tracking** — tight tracking (-0.02em) on large headlines. Normal or wide tracking on small labels.
91
92
  - **One typeface, two weights** — don't mix typefaces unless you have a reason. One family at two weights creates more hierarchy than two families at one weight each.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "hyperframes",
3
- "version": "0.1.7",
3
+ "version": "0.1.8",
4
4
  "description": "HyperFrames CLI — create, preview, and render HTML video compositions",
5
5
  "repository": {
6
6
  "type": "git",