odori 0.0.3 → 0.0.5

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.
@@ -1,4 +1,4 @@
1
- import { D as Duration, B as Brand } from './brand-D71KbhAe.js';
1
+ import { D as Duration, B as Brand } from './brand-B-Mv4S_Z.js';
2
2
  import * as react_jsx_runtime from 'react/jsx-runtime';
3
3
  import { ReactNode, ComponentPropsWithoutRef, CSSProperties } from 'react';
4
4
 
@@ -149,8 +149,20 @@ type CompiledScene = {
149
149
  /** Frames this scene starts before the previous one ends. Zero is a cut. */
150
150
  overlap: number;
151
151
  };
152
+ /**
153
+ * A clip on the video's own clock. Clips placed inside a scene belong to that
154
+ * scene's internals and are not listed here: this is the video timeline.
155
+ */
156
+ type CompiledClip = {
157
+ id: string;
158
+ index: number;
159
+ start: number;
160
+ durationInFrames: number;
161
+ };
152
162
  type CompiledTimeline = {
153
163
  scenes: CompiledScene[];
164
+ /** Absolutely placed children of `<Video>`, in source order. */
165
+ clips: CompiledClip[];
154
166
  durationInFrames: number;
155
167
  };
156
168
  declare const Fill: ({ style, ...props }: ComponentPropsWithoutRef<"div">) => react_jsx_runtime.JSX.Element;
@@ -170,6 +182,13 @@ type SceneProps = {
170
182
  * by the same amount: an overlap is a join, not an extra beat.
171
183
  */
172
184
  overlap?: Duration;
185
+ /**
186
+ * How this scene meets its neighbours. `fade` dissolves it in and out, which
187
+ * is forgiving when a scene has no entrance of its own. `cut` shows it whole
188
+ * from its first frame, which is what most film actually does and the only
189
+ * way to reproduce a reference that cuts.
190
+ */
191
+ transition?: "fade" | "cut";
173
192
  /** Injected by <Video>. Authors never pass these. */
174
193
  __start?: number;
175
194
  __index?: number;
@@ -204,7 +223,7 @@ declare const useSceneTransition: () => SceneTransition;
204
223
  * A timeline segment with a local frame clock starting at zero.
205
224
  */
206
225
  declare const Scene: {
207
- ({ children, id, name, __start, __index, __durationInFrames, __enterOverlap, __exitOverlap, __videoFrame, }: SceneProps): react_jsx_runtime.JSX.Element | null;
226
+ ({ children, id, name, transition: treatment, __start, __index, __durationInFrames, __enterOverlap, __exitOverlap, __videoFrame, }: SceneProps): react_jsx_runtime.JSX.Element | null;
208
227
  __odoriScene: true;
209
228
  };
210
229
  /** An explicit offset window inside a scene, for staggered layers. */
@@ -223,13 +242,45 @@ declare const Stagger: ({ children, from, duration }: {
223
242
  * why scrubbing backwards into a loop lands exactly where playing forwards
224
243
  * did.
225
244
  */
226
- declare const Loop: ({ children, duration, times, }: {
245
+ declare const Repeat: ({ children, duration, times, }: {
227
246
  children: ReactNode;
228
247
  /** Length of one pass. */
229
248
  duration: Duration;
230
249
  /** How many passes before it stops. Unlimited by default. */
231
250
  times?: number;
232
251
  }) => react_jsx_runtime.JSX.Element | null;
252
+ /**
253
+ * Content placed at a time, rather than after the thing before it.
254
+ *
255
+ * A scene is a length in a sequence: it starts where the previous one ended,
256
+ * so its position is a consequence of its siblings. That is the right model
257
+ * for the spine of a cut, and the wrong one for anything that has to appear
258
+ * over a scene already running — a lower third, a callout, a caption, a mark
259
+ * that outlives the shot it started in. A clip is placed instead: it names its
260
+ * own start, takes no space in the sequence, and moves nothing when its length
261
+ * changes.
262
+ *
263
+ * It reads its clock from wherever it sits, which is what makes it compose. A
264
+ * direct child of `<Video>` is placed against the whole cut; the same clip
265
+ * inside a `<Scene>` is placed against that scene and disappears with it. Its
266
+ * children see a clock starting at zero either way, so a component written for
267
+ * a scene behaves identically inside a clip.
268
+ *
269
+ * Without a `duration` it runs to the end of whatever it is placed in, which
270
+ * is what a watermark wants.
271
+ */
272
+ declare const Clip: {
273
+ ({ children, id, from, duration, }: {
274
+ children: ReactNode;
275
+ /** Names it on the timeline. Defaults to its position among the clips. */
276
+ id?: string;
277
+ /** When it starts, on the clock of whatever contains it. */
278
+ from?: Duration;
279
+ /** How long it lasts. To the end of the container by default. */
280
+ duration?: Duration;
281
+ }): react_jsx_runtime.JSX.Element | null;
282
+ __odoriClip: true;
283
+ };
233
284
  /**
234
285
  * Hold children at one frame.
235
286
  *
@@ -238,7 +289,7 @@ declare const Loop: ({ children, duration, times, }: {
238
289
  * clock that stops, not the render: children still mount and lay out
239
290
  * normally, they simply always see the same number.
240
291
  */
241
- declare const Freeze: ({ children, at }: {
292
+ declare const Hold: ({ children, at }: {
242
293
  children: ReactNode;
243
294
  at?: Duration;
244
295
  }) => react_jsx_runtime.JSX.Element;
@@ -395,4 +446,4 @@ type ExportJob = {
395
446
  updatedAt: string;
396
447
  };
397
448
 
398
- export { type AudioTrack as A, entryDurationInFrames as B, type CompiledTimeline as C, DUCK_GAIN as D, type EnvelopePoint as E, Fill as F, envelopeAtFrame as G, gainAtFrame as H, isAssetReference as I, resolveEntryLayout as J, sampleGainCurve as K, Loop as L, type ManifestAsset as M, sortCues as N, OdoriRuntime as O, trackDuration as P, trackGainAtFrame as Q, type RenderManifest as R, SafeArea as S, useSceneTransition as T, type ManifestToolchain as U, type VideoEntry as V, type VideoLayout as a, Audio as b, type AudioCue as c, type AudioPolicy as d, type AudioProps as e, type CompiledScene as f, type CreateManifestOptions as g, DUCK_RAMP_FRAMES as h, type ExportJob as i, Freeze as j, type ManifestAudioCue as k, type ManifestFont as l, type MotionPolicy as m, type OdoriRuntimeProps as n, type SafeArea$1 as o, Scene as p, type SceneTransition as q, Stagger as r, Video as s, type VideoFormat as t, type VideoLayoutInput as u, createRenderManifest as v, cueId as w, defaultLayout as x, defineVideoLayout as y, duckEnvelope as z };
449
+ export { type AudioTrack as A, duckEnvelope as B, type CompiledTimeline as C, DUCK_GAIN as D, type EnvelopePoint as E, Fill as F, entryDurationInFrames as G, Hold as H, envelopeAtFrame as I, gainAtFrame as J, isAssetReference as K, resolveEntryLayout as L, type ManifestAsset as M, sampleGainCurve as N, OdoriRuntime as O, sortCues as P, trackDuration as Q, type RenderManifest as R, SafeArea as S, trackGainAtFrame as T, useSceneTransition as U, type VideoEntry as V, type ManifestToolchain as W, type VideoLayout as a, Audio as b, type AudioCue as c, type AudioPolicy as d, type AudioProps as e, Clip as f, type CompiledScene as g, type CreateManifestOptions as h, DUCK_RAMP_FRAMES as i, type ExportJob as j, type ManifestAudioCue as k, type ManifestFont as l, type MotionPolicy as m, type OdoriRuntimeProps as n, Repeat as o, type SafeArea$1 as p, Scene as q, type SceneTransition as r, Stagger as s, Video as t, type VideoFormat as u, type VideoLayoutInput as v, createRenderManifest as w, cueId as x, defaultLayout as y, defineVideoLayout as z };
@@ -1,4 +1,4 @@
1
- export { g as CreateManifestOptions, i as ExportJob, M as ManifestAsset, k as ManifestAudioCue, l as ManifestFont, U as ManifestToolchain, R as RenderManifest, v as createRenderManifest } from './manifest-CEuFXG0U.js';
2
- import './brand-D71KbhAe.js';
1
+ export { h as CreateManifestOptions, j as ExportJob, M as ManifestAsset, k as ManifestAudioCue, l as ManifestFont, W as ManifestToolchain, R as RenderManifest, w as createRenderManifest } from './manifest-d7-HWIFj.js';
2
+ import './brand-B-Mv4S_Z.js';
3
3
  import 'react/jsx-runtime';
4
4
  import 'react';
package/dist/manifest.js CHANGED
@@ -1,8 +1,8 @@
1
1
  "use client";
2
2
  import {
3
3
  createRenderManifest
4
- } from "./chunk-VQLDJUK4.js";
5
- import "./chunk-PXG45HYV.js";
4
+ } from "./chunk-7NPIGLFR.js";
5
+ import "./chunk-V34N2I4A.js";
6
6
  export {
7
7
  createRenderManifest
8
8
  };
package/dist/preview.d.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  import * as react from 'react';
2
2
  import { Component, ReactNode, ErrorInfo, ComponentType } from 'react';
3
- import { f as CueDefinition, D as Duration, B as Brand } from './brand-D71KbhAe.js';
3
+ import { f as CueDefinition, D as Duration, B as Brand } from './brand-B-Mv4S_Z.js';
4
4
  import { F as FieldDescriptor } from './schema-DXxRezrk.js';
5
5
  import * as react_jsx_runtime from 'react/jsx-runtime';
6
6
 
@@ -24,6 +24,28 @@ type CueWaveProps = {
24
24
  */
25
25
  declare const CueWave: ({ cue, label, columns }: CueWaveProps) => react_jsx_runtime.JSX.Element;
26
26
 
27
+ type SampleWaveProps = {
28
+ /** Peak per column, 0 to 1, measured from the file when it was published. */
29
+ peaks: number[];
30
+ /** Shown under the wave. The cue name a project would register it as. */
31
+ label: string;
32
+ /** Seconds the recording runs, for the caption. */
33
+ seconds?: number;
34
+ /** Whether the file is cut to repeat. */
35
+ loops?: boolean;
36
+ };
37
+ /**
38
+ * A recording drawn from measured peaks, with a playhead crossing it in time.
39
+ *
40
+ * The sibling of `CueWave`, for sound that is a file rather than a score.
41
+ * A score can be rendered here and now, so `CueWave` draws the real samples; a
42
+ * recording would have to be fetched and decoded, which a frame cannot wait
43
+ * for and a still cannot do at all. So the peaks are measured once when the
44
+ * asset is published and travel in the registry with it: the same picture,
45
+ * deterministic, and no network inside a render.
46
+ */
47
+ declare const SampleWave: ({ peaks, label, seconds, loops }: SampleWaveProps) => react_jsx_runtime.JSX.Element;
48
+
27
49
  type PreviewBoundaryProps = {
28
50
  children: ReactNode;
29
51
  /**
@@ -62,7 +84,22 @@ declare class PreviewBoundary extends Component<PreviewBoundaryProps, State> {
62
84
  type ComponentPreview<Props extends object = Record<string, unknown>> = {
63
85
  readonly kind: "odori-component-preview";
64
86
  title: string;
65
- category: string;
87
+ /**
88
+ * Where this sits, slash separated: `Interface/Controls`.
89
+ *
90
+ * The first level is the family, which is what a catalog filters by; the
91
+ * rest are groups inside it, which is how a library stays navigable once
92
+ * one family has twenty components in it. A category with no slash is a
93
+ * family on its own, which is all most projects ever need.
94
+ *
95
+ * Optional, because the directory already says this. A component at
96
+ * `videos/components/interface/controls/combobox/` is in Interface/Controls
97
+ * without being told, so a project organises by moving files and cannot
98
+ * misspell a group into existence. Declaring it is for source that travels:
99
+ * a registry component is copied into somebody else's tree, lands wherever
100
+ * they put it, and would otherwise arrive filed under nothing.
101
+ */
102
+ category?: string;
66
103
  description?: string;
67
104
  component: ComponentType<Props>;
68
105
  canvas: {
@@ -83,10 +120,47 @@ type ComponentPreviewInput<Props extends object> = Omit<ComponentPreview<Props>,
83
120
  props: Partial<Props>;
84
121
  }>;
85
122
  };
123
+ /**
124
+ * The category a file's own location implies.
125
+ *
126
+ * Everything between the components directory and the component's own folder,
127
+ * so `videos/components/interface/controls/combobox/combobox.preview.tsx` is
128
+ * `Interface/Controls`, and a component sitting directly in the components
129
+ * directory belongs to no group at all. Names are read as words: a directory
130
+ * called `developer-proof` is the family "Developer proof", which keeps the
131
+ * shell honest and the heading readable.
132
+ */
133
+ declare const categoryFromPath: (relativeFile: string, componentsDir?: string,
134
+ /**
135
+ * What a directory calls itself, given its path under the components
136
+ * directory. This is where a `category.json` gets a say: a filesystem
137
+ * cannot tell you that `product-ui` reads "Product UI", and reading the
138
+ * name as words cannot either, because the same rule has to leave
139
+ * "Developer proof" alone.
140
+ */
141
+ labelOf?: (path: string) => string | undefined) => string;
142
+ /**
143
+ * The levels of a category, outermost first.
144
+ *
145
+ * Everything that organises components needs this - Studio, the catalog, the
146
+ * registry - and each of them had grown its own `split("/")`, which is the
147
+ * same fact in six places waiting to disagree about whitespace. It lives here
148
+ * because the convention is the framework's, not any one reader's.
149
+ */
150
+ declare const categoryPath: (category: string) => string[];
151
+ /** The family: the first level, and what a catalog filters by. */
152
+ declare const familyOf: (category: string) => string;
153
+ /** The levels inside the family, joined, or "" for a component at its top. */
154
+ declare const groupOf: (category: string) => string;
86
155
  /**
87
156
  * A development-only fixture. Production bundles never import preview modules,
88
157
  * so this contract can be as rich as Studio needs.
158
+ *
159
+ * The category is normalised here rather than by every reader: ` Interface /
160
+ * Controls ` and `Interface/Controls` are the same place, and a fixture that
161
+ * names no place at all is a mistake worth hearing about at the point it is
162
+ * written rather than seeing as a card filed under nothing.
89
163
  */
90
164
  declare const defineComponentPreview: <Props extends object>(preview: ComponentPreviewInput<Props>) => ComponentPreview<Props>;
91
165
 
92
- export { type ComponentPreview, type ComponentPreviewInput, CueWave, type CueWaveProps, PreviewBoundary, type PreviewBoundaryProps, defineComponentPreview };
166
+ export { type ComponentPreview, type ComponentPreviewInput, CueWave, type CueWaveProps, PreviewBoundary, type PreviewBoundaryProps, SampleWave, type SampleWaveProps, categoryFromPath, categoryPath, defineComponentPreview, familyOf, groupOf };
package/dist/preview.js CHANGED
@@ -7,8 +7,8 @@ import {
7
7
  useDesignScale,
8
8
  useFrame,
9
9
  useVideo
10
- } from "./chunk-2SROZSZM.js";
11
- import "./chunk-PXG45HYV.js";
10
+ } from "./chunk-XN6DA264.js";
11
+ import "./chunk-V34N2I4A.js";
12
12
 
13
13
  // src/cue-wave.tsx
14
14
  import { jsx, jsxs } from "react/jsx-runtime";
@@ -50,9 +50,39 @@ var CueWave = ({ cue, label, columns = 120 }) => {
50
50
  ] });
51
51
  };
52
52
 
53
+ // src/sample-wave.tsx
54
+ import { jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime";
55
+ var SampleWave = ({ peaks, label, seconds, loops }) => {
56
+ const frame = useFrame();
57
+ const brand = useBrand();
58
+ const scale = useDesignScale();
59
+ const { durationInFrames } = useVideo();
60
+ const progress = durationInFrames <= 1 ? 0 : frame / (durationInFrames - 1);
61
+ return /* @__PURE__ */ jsxs2(Fill, { style: { alignItems: "center", gap: 34 * scale, justifyContent: "center", padding: 120 * scale }, children: [
62
+ /* @__PURE__ */ jsx2("div", { style: { alignItems: "center", display: "flex", gap: 4 * scale, height: 260 * scale }, children: peaks.map((value, index) => /* @__PURE__ */ jsx2(
63
+ "span",
64
+ {
65
+ style: {
66
+ background: index / peaks.length <= progress ? brand.colors.accent : brand.colors.border,
67
+ borderRadius: 999,
68
+ height: Math.max(2 * scale, value * 260 * scale),
69
+ width: 6 * scale
70
+ }
71
+ },
72
+ index
73
+ )) }),
74
+ /* @__PURE__ */ jsxs2("div", { style: { color: brand.colors.muted, fontFamily: brand.typography.mono, fontSize: 28 * scale }, children: [
75
+ label,
76
+ seconds === void 0 ? "" : ` \xB7 ${seconds}s`,
77
+ loops ? " \xB7 loops" : "",
78
+ " \xB7 recorded"
79
+ ] })
80
+ ] });
81
+ };
82
+
53
83
  // src/preview-boundary.tsx
54
84
  import { Component } from "react";
55
- import { jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime";
85
+ import { jsx as jsx3, jsxs as jsxs3 } from "react/jsx-runtime";
56
86
  var sourceFrame = (stack) => {
57
87
  if (!stack) return null;
58
88
  for (const line of stack.split("\n")) {
@@ -68,7 +98,7 @@ var openInEditor = (location) => {
68
98
  var PreviewErrorPanel = ({ error, componentStack, label }) => {
69
99
  const location = sourceFrame(componentStack ?? null);
70
100
  const [file] = location ? location.split(":") : [null];
71
- return /* @__PURE__ */ jsxs2(
101
+ return /* @__PURE__ */ jsxs3(
72
102
  "div",
73
103
  {
74
104
  "data-odori-preview-error": true,
@@ -92,9 +122,9 @@ var PreviewErrorPanel = ({ error, componentStack, label }) => {
92
122
  textAlign: "left"
93
123
  },
94
124
  children: [
95
- /* @__PURE__ */ jsx2("strong", { style: { color: "#ff9b91", fontSize: 12, letterSpacing: "0.08em", textTransform: "uppercase" }, children: label ?? "This composition threw" }),
96
- /* @__PURE__ */ jsx2("span", { style: { fontSize: 15 }, children: error.message }),
97
- location ? /* @__PURE__ */ jsxs2(
125
+ /* @__PURE__ */ jsx3("strong", { style: { color: "#ff9b91", fontSize: 12, letterSpacing: "0.08em", textTransform: "uppercase" }, children: label ?? "This composition threw" }),
126
+ /* @__PURE__ */ jsx3("span", { style: { fontSize: 15 }, children: error.message }),
127
+ location ? /* @__PURE__ */ jsxs3(
98
128
  "button",
99
129
  {
100
130
  type: "button",
@@ -115,8 +145,8 @@ var PreviewErrorPanel = ({ error, componentStack, label }) => {
115
145
  ]
116
146
  }
117
147
  ) : null,
118
- componentStack ? /* @__PURE__ */ jsx2("pre", { style: { color: "#c9b6b4", fontSize: 11, margin: 0, whiteSpace: "pre-wrap" }, children: componentStack.trim().split("\n").slice(0, 6).join("\n") }) : null,
119
- /* @__PURE__ */ jsx2("span", { style: { color: "#c9b6b4", fontSize: 11 }, children: "Fix the file and save. The preview recovers on reload." })
148
+ componentStack ? /* @__PURE__ */ jsx3("pre", { style: { color: "#c9b6b4", fontSize: 11, margin: 0, whiteSpace: "pre-wrap" }, children: componentStack.trim().split("\n").slice(0, 6).join("\n") }) : null,
149
+ /* @__PURE__ */ jsx3("span", { style: { color: "#c9b6b4", fontSize: 11 }, children: "Fix the file and save. The preview recovers on reload." })
120
150
  ]
121
151
  }
122
152
  );
@@ -138,19 +168,40 @@ var PreviewBoundary = class extends Component {
138
168
  render() {
139
169
  const { error, componentStack } = this.state;
140
170
  if (!error) return this.props.children;
141
- return /* @__PURE__ */ jsx2(PreviewErrorPanel, { error, componentStack, label: this.props.label });
171
+ return /* @__PURE__ */ jsx3(PreviewErrorPanel, { error, componentStack, label: this.props.label });
142
172
  }
143
173
  };
144
174
 
145
175
  // src/preview.ts
146
- var defineComponentPreview = (preview) => ({
147
- kind: "odori-component-preview",
148
- examples: [{ name: "Default", props: {} }],
149
- ...preview
150
- });
176
+ var categoryFromPath = (relativeFile, componentsDir = "components", labelOf) => {
177
+ const parts = relativeFile.split("/").filter(Boolean);
178
+ const start = parts.lastIndexOf(componentsDir);
179
+ if (start === -1) return "";
180
+ const between = parts.slice(start + 1, -2);
181
+ return between.map(
182
+ (part, index) => labelOf?.(between.slice(0, index + 1).join("/")) ?? part.replace(/-/g, " ").replace(/^./, (letter) => letter.toUpperCase())
183
+ ).join("/");
184
+ };
185
+ var categoryPath = (category) => category.split("/").map((part) => part.trim()).filter(Boolean);
186
+ var familyOf = (category) => categoryPath(category)[0] ?? "";
187
+ var groupOf = (category) => categoryPath(category).slice(1).join("/");
188
+ var defineComponentPreview = (preview) => {
189
+ const category = preview.category === void 0 ? void 0 : categoryPath(preview.category).join("/");
190
+ return {
191
+ kind: "odori-component-preview",
192
+ examples: [{ name: "Default", props: {} }],
193
+ ...preview,
194
+ ...category === void 0 ? {} : { category }
195
+ };
196
+ };
151
197
  export {
152
198
  CueWave,
153
199
  PreviewBoundary,
154
- defineComponentPreview
200
+ SampleWave,
201
+ categoryFromPath,
202
+ categoryPath,
203
+ defineComponentPreview,
204
+ familyOf,
205
+ groupOf
155
206
  };
156
207
  //# sourceMappingURL=preview.js.map
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/cue-wave.tsx","../src/preview-boundary.tsx","../src/preview.ts"],"sourcesContent":["import {Fill} from \"./runtime\";\nimport {useBrand, useDesignScale, useFrame, useVideo} from \"./context\";\nimport {cueSamples, type CueDefinition} from \"./cue\";\nimport {SAMPLE_RATE} from \"./synth\";\n\nexport type CueWaveProps = {\n /** The score to draw. Its samples are rendered, not approximated. */\n cue: CueDefinition;\n /** Shown under the wave. Defaults to the name the cue registers. */\n label?: string;\n /** Bars across the canvas. More is finer, and slower to render. */\n columns?: number;\n};\n\n/**\n * A cue drawn from its own samples, with a playhead crossing it in time.\n *\n * A sound needs a picture on a page that cannot make noise until it is\n * clicked, and the honest picture is the waveform the score actually produces:\n * rendering is deterministic, so this is the sound rather than a decorative\n * squiggle. It lives in `odori/preview` because it is preview furniture — a\n * project owns the score it installed, not the chart of it, and fifteen cues\n * should not carry fifteen copies of this loop.\n */\nexport const CueWave = ({cue, label, columns = 120}: CueWaveProps) => {\n const frame = useFrame();\n const brand = useBrand();\n const scale = useDesignScale();\n const {fps, durationInFrames} = useVideo();\n\n const channel = cue.render({samples: cueSamples(cue, fps), sampleRate: SAMPLE_RATE}).channels[0];\n const width = Math.max(1, Math.floor(channel.length / columns));\n const peaks = Array.from({length: columns}, (_, index) => {\n let highest = 0;\n for (let sample = index * width; sample < Math.min(channel.length, (index + 1) * width); sample += 1) {\n highest = Math.max(highest, Math.abs(channel[sample]));\n }\n return highest;\n });\n\n const progress = durationInFrames <= 1 ? 0 : frame / (durationInFrames - 1);\n\n return (\n <Fill style={{alignItems: \"center\", gap: 34 * scale, justifyContent: \"center\", padding: 120 * scale}}>\n <div style={{alignItems: \"center\", display: \"flex\", gap: 4 * scale, height: 260 * scale}}>\n {peaks.map((value, index) => (\n <span\n key={index}\n style={{\n background: index / columns <= progress ? brand.colors.accent : brand.colors.border,\n borderRadius: 999,\n height: Math.max(2 * scale, value * 260 * scale),\n width: 6 * scale,\n }}\n />\n ))}\n </div>\n <div style={{color: brand.colors.muted, fontFamily: brand.typography.mono, fontSize: 28 * scale}}>\n {label ?? cue.name} · {cue.durationInFrames} frames{cue.loops ? \" · loops\" : \"\"}\n </div>\n </Fill>\n );\n};\n","\"use client\";\n\nimport {Component, type ErrorInfo, type ReactNode} from \"react\";\n\nexport type PreviewBoundaryProps = {\n children: ReactNode;\n /**\n * Changing this clears a caught error. Pass whatever identifies the thing\n * being previewed — an entry id, a format, a brand — so switching away from\n * a broken composition shows the next one instead of its predecessor's\n * error. A module edit clears it on its own, through the HMR hook below.\n */\n resetKey?: string | number;\n /** Shown above the message. Defaults to naming the composition generically. */\n label?: string;\n};\n\ntype State = {error: Error | null; componentStack: string | null};\n\n/** The first project file in a component stack, which is where an author looks. */\nexport const sourceFrame = (stack: string | null): string | null => {\n if (!stack) return null;\n for (const line of stack.split(\"\\n\")) {\n const match = /\\(?((?:\\/|[A-Za-z]:\\\\)[^):]+\\.(?:tsx|ts|jsx|js)):(\\d+):(\\d+)\\)?/.exec(line);\n if (match && !match[1].includes(\"node_modules\")) return `${match[1]}:${match[2]}:${match[3]}`;\n }\n return null;\n};\n\n/**\n * Vite's dev server exposes an editor hook. Studio and the docs site are both\n * Vite, so a file reference in this panel is a link to the line rather than a\n * path to copy by hand. It fails silently: an editor that will not open is not\n * worth a second error on top of the first.\n */\nconst openInEditor = (location: string) => {\n void fetch(`/__open-in-editor?file=${encodeURIComponent(location)}`).catch(() => {});\n};\n\nexport type PreviewErrorPanelProps = {\n error: Error;\n componentStack?: string | null;\n label?: string;\n};\n\n/** What a caught error looks like. Separated so it can be rendered and read. */\nexport const PreviewErrorPanel = ({error, componentStack, label}: PreviewErrorPanelProps) => {\n const location = sourceFrame(componentStack ?? null);\n const [file] = location ? location.split(\":\") : [null];\n\n return (\n <div\n data-odori-preview-error\n style={{\n alignItems: \"flex-start\",\n background: \"#120d0d\",\n border: \"1px solid #532\",\n borderRadius: 12,\n color: \"#f5eceb\",\n display: \"flex\",\n flexDirection: \"column\",\n fontFamily: \"ui-monospace, SFMono-Regular, Menlo, monospace\",\n fontSize: 13,\n gap: 12,\n inset: 0,\n justifyContent: \"center\",\n lineHeight: 1.6,\n overflow: \"auto\",\n padding: 24,\n position: \"absolute\",\n textAlign: \"left\",\n }}\n >\n <strong style={{color: \"#ff9b91\", fontSize: 12, letterSpacing: \"0.08em\", textTransform: \"uppercase\"}}>\n {label ?? \"This composition threw\"}\n </strong>\n <span style={{fontSize: 15}}>{error.message}</span>\n {location ? (\n <button\n type=\"button\"\n onClick={() => openInEditor(location)}\n style={{\n background: \"transparent\",\n border: \"1px solid #644\",\n borderRadius: 8,\n color: \"#f5eceb\",\n cursor: \"pointer\",\n fontFamily: \"inherit\",\n fontSize: 12,\n padding: \"6px 10px\",\n }}\n >\n {file?.split(\"/\").slice(-2).join(\"/\")}\n {location.slice(file?.length ?? 0)}\n </button>\n ) : null}\n {componentStack ? (\n <pre style={{color: \"#c9b6b4\", fontSize: 11, margin: 0, whiteSpace: \"pre-wrap\"}}>\n {componentStack.trim().split(\"\\n\").slice(0, 6).join(\"\\n\")}\n </pre>\n ) : null}\n <span style={{color: \"#c9b6b4\", fontSize: 11}}>Fix the file and save. The preview recovers on reload.</span>\n </div>\n );\n};\n\n/**\n * The designed failure for a composition that throws.\n *\n * Preview only. A render worker must fail loudly and exit non-zero rather than\n * paint an apology into an exported frame, which is why this lives in\n * `odori/preview` and no export path imports it.\n *\n * Without it, one bad component takes the whole surface down and leaves\n * whatever React last painted, with the real message in a console the author\n * may not have open.\n */\nexport class PreviewBoundary extends Component<PreviewBoundaryProps, State> {\n state: State = {error: null, componentStack: null};\n\n static getDerivedStateFromError(error: Error): Partial<State> {\n return {error};\n }\n\n componentDidUpdate(previous: PreviewBoundaryProps) {\n if (previous.resetKey !== this.props.resetKey && this.state.error) {\n this.setState({error: null, componentStack: null});\n }\n }\n\n componentDidCatch(error: Error, info: ErrorInfo) {\n this.setState({componentStack: info.componentStack ?? null});\n // Still report it: the panel is for the person watching, the console is\n // for the person debugging, and they are not always looking at the same\n // thing.\n console.error(error);\n }\n\n render() {\n const {error, componentStack} = this.state;\n if (!error) return this.props.children;\n return <PreviewErrorPanel error={error} componentStack={componentStack} label={this.props.label} />;\n }\n}\n","import {type ComponentType} from \"react\";\nimport {type Duration} from \"./time\";\nimport {type FieldDescriptor} from \"./schema\";\nimport {type Brand} from \"./brand\";\n\nexport type ComponentPreview<Props extends object = Record<string, unknown>> = {\n readonly kind: \"odori-component-preview\";\n title: string;\n category: string;\n description?: string;\n component: ComponentType<Props>;\n canvas: {width: number; height: number; duration: Duration};\n controls?: Record<string, FieldDescriptor>;\n brands?: Brand[];\n examples: Array<{name: string; props: Partial<Props>}>;\n};\n\nexport type ComponentPreviewInput<Props extends object> = Omit<ComponentPreview<Props>, \"kind\" | \"examples\"> & {\n examples?: Array<{name: string; props: Partial<Props>}>;\n};\n\n/**\n * A development-only fixture. Production bundles never import preview modules,\n * so this contract can be as rich as Studio needs.\n */\nexport const defineComponentPreview = <Props extends object>(\n preview: ComponentPreviewInput<Props>,\n): ComponentPreview<Props> => ({\n kind: \"odori-component-preview\",\n examples: [{name: \"Default\", props: {} as Partial<Props>}],\n ...preview,\n});\n\nexport {CueWave, type CueWaveProps} from \"./cue-wave\";\nexport {PreviewBoundary, type PreviewBoundaryProps} from \"./preview-boundary\";\n"],"mappings":"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","names":["jsx","jsxs"]}
1
+ {"version":3,"sources":["../src/cue-wave.tsx","../src/sample-wave.tsx","../src/preview-boundary.tsx","../src/preview.ts"],"sourcesContent":["import {Fill} from \"./runtime\";\nimport {useBrand, useDesignScale, useFrame, useVideo} from \"./context\";\nimport {cueSamples, type CueDefinition} from \"./cue\";\nimport {SAMPLE_RATE} from \"./synth\";\n\nexport type CueWaveProps = {\n /** The score to draw. Its samples are rendered, not approximated. */\n cue: CueDefinition;\n /** Shown under the wave. Defaults to the name the cue registers. */\n label?: string;\n /** Bars across the canvas. More is finer, and slower to render. */\n columns?: number;\n};\n\n/**\n * A cue drawn from its own samples, with a playhead crossing it in time.\n *\n * A sound needs a picture on a page that cannot make noise until it is\n * clicked, and the honest picture is the waveform the score actually produces:\n * rendering is deterministic, so this is the sound rather than a decorative\n * squiggle. It lives in `odori/preview` because it is preview furniture — a\n * project owns the score it installed, not the chart of it, and fifteen cues\n * should not carry fifteen copies of this loop.\n */\nexport const CueWave = ({cue, label, columns = 120}: CueWaveProps) => {\n const frame = useFrame();\n const brand = useBrand();\n const scale = useDesignScale();\n const {fps, durationInFrames} = useVideo();\n\n const channel = cue.render({samples: cueSamples(cue, fps), sampleRate: SAMPLE_RATE}).channels[0];\n const width = Math.max(1, Math.floor(channel.length / columns));\n const peaks = Array.from({length: columns}, (_, index) => {\n let highest = 0;\n for (let sample = index * width; sample < Math.min(channel.length, (index + 1) * width); sample += 1) {\n highest = Math.max(highest, Math.abs(channel[sample]));\n }\n return highest;\n });\n\n const progress = durationInFrames <= 1 ? 0 : frame / (durationInFrames - 1);\n\n return (\n <Fill style={{alignItems: \"center\", gap: 34 * scale, justifyContent: \"center\", padding: 120 * scale}}>\n <div style={{alignItems: \"center\", display: \"flex\", gap: 4 * scale, height: 260 * scale}}>\n {peaks.map((value, index) => (\n <span\n key={index}\n style={{\n background: index / columns <= progress ? brand.colors.accent : brand.colors.border,\n borderRadius: 999,\n height: Math.max(2 * scale, value * 260 * scale),\n width: 6 * scale,\n }}\n />\n ))}\n </div>\n <div style={{color: brand.colors.muted, fontFamily: brand.typography.mono, fontSize: 28 * scale}}>\n {label ?? cue.name} · {cue.durationInFrames} frames{cue.loops ? \" · loops\" : \"\"}\n </div>\n </Fill>\n );\n};\n","import {Fill} from \"./runtime\";\nimport {useBrand, useDesignScale, useFrame, useVideo} from \"./context\";\n\nexport type SampleWaveProps = {\n /** Peak per column, 0 to 1, measured from the file when it was published. */\n peaks: number[];\n /** Shown under the wave. The cue name a project would register it as. */\n label: string;\n /** Seconds the recording runs, for the caption. */\n seconds?: number;\n /** Whether the file is cut to repeat. */\n loops?: boolean;\n};\n\n/**\n * A recording drawn from measured peaks, with a playhead crossing it in time.\n *\n * The sibling of `CueWave`, for sound that is a file rather than a score.\n * A score can be rendered here and now, so `CueWave` draws the real samples; a\n * recording would have to be fetched and decoded, which a frame cannot wait\n * for and a still cannot do at all. So the peaks are measured once when the\n * asset is published and travel in the registry with it: the same picture,\n * deterministic, and no network inside a render.\n */\nexport const SampleWave = ({peaks, label, seconds, loops}: SampleWaveProps) => {\n const frame = useFrame();\n const brand = useBrand();\n const scale = useDesignScale();\n const {durationInFrames} = useVideo();\n const progress = durationInFrames <= 1 ? 0 : frame / (durationInFrames - 1);\n\n return (\n <Fill style={{alignItems: \"center\", gap: 34 * scale, justifyContent: \"center\", padding: 120 * scale}}>\n <div style={{alignItems: \"center\", display: \"flex\", gap: 4 * scale, height: 260 * scale}}>\n {peaks.map((value, index) => (\n <span\n key={index}\n style={{\n background: index / peaks.length <= progress ? brand.colors.accent : brand.colors.border,\n borderRadius: 999,\n height: Math.max(2 * scale, value * 260 * scale),\n width: 6 * scale,\n }}\n />\n ))}\n </div>\n <div style={{color: brand.colors.muted, fontFamily: brand.typography.mono, fontSize: 28 * scale}}>\n {label}\n {seconds === undefined ? \"\" : ` · ${seconds}s`}\n {loops ? \" · loops\" : \"\"} · recorded\n </div>\n </Fill>\n );\n};\n","\"use client\";\n\nimport {Component, type ErrorInfo, type ReactNode} from \"react\";\n\nexport type PreviewBoundaryProps = {\n children: ReactNode;\n /**\n * Changing this clears a caught error. Pass whatever identifies the thing\n * being previewed — an entry id, a format, a brand — so switching away from\n * a broken composition shows the next one instead of its predecessor's\n * error. A module edit clears it on its own, through the HMR hook below.\n */\n resetKey?: string | number;\n /** Shown above the message. Defaults to naming the composition generically. */\n label?: string;\n};\n\ntype State = {error: Error | null; componentStack: string | null};\n\n/** The first project file in a component stack, which is where an author looks. */\nexport const sourceFrame = (stack: string | null): string | null => {\n if (!stack) return null;\n for (const line of stack.split(\"\\n\")) {\n const match = /\\(?((?:\\/|[A-Za-z]:\\\\)[^):]+\\.(?:tsx|ts|jsx|js)):(\\d+):(\\d+)\\)?/.exec(line);\n if (match && !match[1].includes(\"node_modules\")) return `${match[1]}:${match[2]}:${match[3]}`;\n }\n return null;\n};\n\n/**\n * Vite's dev server exposes an editor hook. Studio and the docs site are both\n * Vite, so a file reference in this panel is a link to the line rather than a\n * path to copy by hand. It fails silently: an editor that will not open is not\n * worth a second error on top of the first.\n */\nconst openInEditor = (location: string) => {\n void fetch(`/__open-in-editor?file=${encodeURIComponent(location)}`).catch(() => {});\n};\n\nexport type PreviewErrorPanelProps = {\n error: Error;\n componentStack?: string | null;\n label?: string;\n};\n\n/** What a caught error looks like. Separated so it can be rendered and read. */\nexport const PreviewErrorPanel = ({error, componentStack, label}: PreviewErrorPanelProps) => {\n const location = sourceFrame(componentStack ?? null);\n const [file] = location ? location.split(\":\") : [null];\n\n return (\n <div\n data-odori-preview-error\n style={{\n alignItems: \"flex-start\",\n background: \"#120d0d\",\n border: \"1px solid #532\",\n borderRadius: 12,\n color: \"#f5eceb\",\n display: \"flex\",\n flexDirection: \"column\",\n fontFamily: \"ui-monospace, SFMono-Regular, Menlo, monospace\",\n fontSize: 13,\n gap: 12,\n inset: 0,\n justifyContent: \"center\",\n lineHeight: 1.6,\n overflow: \"auto\",\n padding: 24,\n position: \"absolute\",\n textAlign: \"left\",\n }}\n >\n <strong style={{color: \"#ff9b91\", fontSize: 12, letterSpacing: \"0.08em\", textTransform: \"uppercase\"}}>\n {label ?? \"This composition threw\"}\n </strong>\n <span style={{fontSize: 15}}>{error.message}</span>\n {location ? (\n <button\n type=\"button\"\n onClick={() => openInEditor(location)}\n style={{\n background: \"transparent\",\n border: \"1px solid #644\",\n borderRadius: 8,\n color: \"#f5eceb\",\n cursor: \"pointer\",\n fontFamily: \"inherit\",\n fontSize: 12,\n padding: \"6px 10px\",\n }}\n >\n {file?.split(\"/\").slice(-2).join(\"/\")}\n {location.slice(file?.length ?? 0)}\n </button>\n ) : null}\n {componentStack ? (\n <pre style={{color: \"#c9b6b4\", fontSize: 11, margin: 0, whiteSpace: \"pre-wrap\"}}>\n {componentStack.trim().split(\"\\n\").slice(0, 6).join(\"\\n\")}\n </pre>\n ) : null}\n <span style={{color: \"#c9b6b4\", fontSize: 11}}>Fix the file and save. The preview recovers on reload.</span>\n </div>\n );\n};\n\n/**\n * The designed failure for a composition that throws.\n *\n * Preview only. A render worker must fail loudly and exit non-zero rather than\n * paint an apology into an exported frame, which is why this lives in\n * `odori/preview` and no export path imports it.\n *\n * Without it, one bad component takes the whole surface down and leaves\n * whatever React last painted, with the real message in a console the author\n * may not have open.\n */\nexport class PreviewBoundary extends Component<PreviewBoundaryProps, State> {\n state: State = {error: null, componentStack: null};\n\n static getDerivedStateFromError(error: Error): Partial<State> {\n return {error};\n }\n\n componentDidUpdate(previous: PreviewBoundaryProps) {\n if (previous.resetKey !== this.props.resetKey && this.state.error) {\n this.setState({error: null, componentStack: null});\n }\n }\n\n componentDidCatch(error: Error, info: ErrorInfo) {\n this.setState({componentStack: info.componentStack ?? null});\n // Still report it: the panel is for the person watching, the console is\n // for the person debugging, and they are not always looking at the same\n // thing.\n console.error(error);\n }\n\n render() {\n const {error, componentStack} = this.state;\n if (!error) return this.props.children;\n return <PreviewErrorPanel error={error} componentStack={componentStack} label={this.props.label} />;\n }\n}\n","import {type ComponentType} from \"react\";\nimport {type Duration} from \"./time\";\nimport {type FieldDescriptor} from \"./schema\";\nimport {type Brand} from \"./brand\";\n\nexport type ComponentPreview<Props extends object = Record<string, unknown>> = {\n readonly kind: \"odori-component-preview\";\n title: string;\n /**\n * Where this sits, slash separated: `Interface/Controls`.\n *\n * The first level is the family, which is what a catalog filters by; the\n * rest are groups inside it, which is how a library stays navigable once\n * one family has twenty components in it. A category with no slash is a\n * family on its own, which is all most projects ever need.\n *\n * Optional, because the directory already says this. A component at\n * `videos/components/interface/controls/combobox/` is in Interface/Controls\n * without being told, so a project organises by moving files and cannot\n * misspell a group into existence. Declaring it is for source that travels:\n * a registry component is copied into somebody else's tree, lands wherever\n * they put it, and would otherwise arrive filed under nothing.\n */\n category?: string;\n description?: string;\n component: ComponentType<Props>;\n canvas: {width: number; height: number; duration: Duration};\n controls?: Record<string, FieldDescriptor>;\n brands?: Brand[];\n examples: Array<{name: string; props: Partial<Props>}>;\n};\n\nexport type ComponentPreviewInput<Props extends object> = Omit<ComponentPreview<Props>, \"kind\" | \"examples\"> & {\n examples?: Array<{name: string; props: Partial<Props>}>;\n};\n\n/**\n * The category a file's own location implies.\n *\n * Everything between the components directory and the component's own folder,\n * so `videos/components/interface/controls/combobox/combobox.preview.tsx` is\n * `Interface/Controls`, and a component sitting directly in the components\n * directory belongs to no group at all. Names are read as words: a directory\n * called `developer-proof` is the family \"Developer proof\", which keeps the\n * shell honest and the heading readable.\n */\nexport const categoryFromPath = (\n relativeFile: string,\n componentsDir = \"components\",\n /**\n * What a directory calls itself, given its path under the components\n * directory. This is where a `category.json` gets a say: a filesystem\n * cannot tell you that `product-ui` reads \"Product UI\", and reading the\n * name as words cannot either, because the same rule has to leave\n * \"Developer proof\" alone.\n */\n labelOf?: (path: string) => string | undefined,\n): string => {\n const parts = relativeFile.split(\"/\").filter(Boolean);\n const start = parts.lastIndexOf(componentsDir);\n if (start === -1) return \"\";\n // Drop the components directory, the component's own folder, and the file.\n const between = parts.slice(start + 1, -2);\n return between\n .map(\n (part, index) =>\n labelOf?.(between.slice(0, index + 1).join(\"/\")) ??\n part.replace(/-/g, \" \").replace(/^./, (letter) => letter.toUpperCase()),\n )\n .join(\"/\");\n};\n\n/**\n * The levels of a category, outermost first.\n *\n * Everything that organises components needs this - Studio, the catalog, the\n * registry - and each of them had grown its own `split(\"/\")`, which is the\n * same fact in six places waiting to disagree about whitespace. It lives here\n * because the convention is the framework's, not any one reader's.\n */\nexport const categoryPath = (category: string): string[] =>\n category\n .split(\"/\")\n .map((part) => part.trim())\n .filter(Boolean);\n\n/** The family: the first level, and what a catalog filters by. */\nexport const familyOf = (category: string): string => categoryPath(category)[0] ?? \"\";\n\n/** The levels inside the family, joined, or \"\" for a component at its top. */\nexport const groupOf = (category: string): string => categoryPath(category).slice(1).join(\"/\");\n\n/**\n * A development-only fixture. Production bundles never import preview modules,\n * so this contract can be as rich as Studio needs.\n *\n * The category is normalised here rather than by every reader: ` Interface /\n * Controls ` and `Interface/Controls` are the same place, and a fixture that\n * names no place at all is a mistake worth hearing about at the point it is\n * written rather than seeing as a card filed under nothing.\n */\nexport const defineComponentPreview = <Props extends object>(\n preview: ComponentPreviewInput<Props>,\n): ComponentPreview<Props> => {\n // Undeclared is the ordinary case now, and the reader supplies it from where\n // the file sits. What is normalised here is a category somebody wrote.\n const category = preview.category === undefined ? undefined : categoryPath(preview.category).join(\"/\");\n\n return {\n kind: \"odori-component-preview\",\n examples: [{name: \"Default\", props: {} as Partial<Props>}],\n ...preview,\n ...(category === undefined ? {} : {category}),\n };\n};\n\nexport {CueWave, type CueWaveProps} from \"./cue-wave\";\nexport {SampleWave, type SampleWaveProps} from \"./sample-wave\";\nexport {PreviewBoundary, type PreviewBoundaryProps} from \"./preview-boundary\";\n"],"mappings":"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","names":["jsx","jsxs","jsx","jsxs"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "odori",
3
- "version": "0.0.3",
3
+ "version": "0.0.5",
4
4
  "type": "module",
5
5
  "description": "An independent React video framework with a first-class videos source root, deterministic frame runtime, timeline compiler, player, and render manifest.",
6
6
  "license": "Apache-2.0",