@vosjs/shared 0.1.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/dist/limits.js ADDED
@@ -0,0 +1,59 @@
1
+ // src/limits.ts
2
+ var PLANS = ["free"];
3
+ var FREE = {
4
+ dailySaves: 10,
5
+ privateVoses: 20,
6
+ keyModelUploads: 10,
7
+ keyRecordingUploads: 20,
8
+ keyImageUploads: 40,
9
+ recordingUploads: 20,
10
+ otherUploads: 50,
11
+ recordingMaxSeconds: 30 * 60,
12
+ recordingMaxBytes: 512 * 1024 * 1024,
13
+ storageBytes: 5 * 1024 * 1024 * 1024,
14
+ versionsPerVosDay: 100,
15
+ keyVersionsPerDay: 200,
16
+ folderItems: 500,
17
+ subfolders: 50,
18
+ mediaPerVosHour: 20,
19
+ mediaPerAccountDay: 300,
20
+ artifactRenderMinutesPerDay: 60,
21
+ backdropBakesPerDay: 20,
22
+ // Folders & recipes: sanity caps, not a paywall — organization
23
+ // stays free at every size that isn't abuse.
24
+ folders: 50,
25
+ recipes: 200,
26
+ keyRecipeCreates: 20,
27
+ recipeBodyBytes: 64 * 1024
28
+ };
29
+ var LIMITS = { free: FREE };
30
+ function planLimits(plan) {
31
+ return LIMITS[plan ?? "free"] ?? FREE;
32
+ }
33
+ var FOOTAGE_BYTES_PER_SECOND = 5e6 / 8 * 1;
34
+ function bytesAsFootageHours(bytes) {
35
+ return Math.round(bytes / FOOTAGE_BYTES_PER_SECOND / 3600 * 2) / 2;
36
+ }
37
+ function formatBytes(bytes) {
38
+ if (bytes >= 1024 ** 3) return `${(bytes / 1024 ** 3).toFixed(1)} GB`;
39
+ if (bytes >= 1024 ** 2) return `${Math.round(bytes / 1024 ** 2)} MB`;
40
+ if (bytes >= 1024) return `${Math.round(bytes / 1024)} KB`;
41
+ return `${bytes} B`;
42
+ }
43
+ function formatDurationCap(seconds) {
44
+ if (seconds < 60) return `${seconds} s`;
45
+ const m = Math.round(seconds / 60);
46
+ if (m < 60) return `${m} min`;
47
+ const h = Math.floor(m / 60);
48
+ const rest = m % 60;
49
+ return rest ? `${h} h ${rest} min` : `${h} h`;
50
+ }
51
+ export {
52
+ FOOTAGE_BYTES_PER_SECOND,
53
+ PLANS,
54
+ bytesAsFootageHours,
55
+ formatBytes,
56
+ formatDurationCap,
57
+ planLimits
58
+ };
59
+ //# sourceMappingURL=limits.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/limits.ts"],"sourcesContent":["/**\n * The plan limits table.\n *\n * ONE table, keyed by `user.plan`, read by every quota verdict on the API\n * and by every client that states a limit before the user hits it (the\n * recorder's countdown, the CLI's `record`, the settings usage strip).\n * 'free' is the only row today and nothing writes `user.plan`; a paid plan\n * is a second row here plus a server-side writer, never a rewrite of the\n * guards. An unknown or absent plan resolves to free — never to a grant.\n *\n * Numbers are proposals that can be retuned as data: every\n * refusal prints its number from here, so the copy can never disagree.\n */\n\nexport type Plan = 'free'\n\nexport const PLANS: readonly Plan[] = ['free']\n\nexport interface PlanLimits {\n /** Session vos creates per rolling 24h. */\n dailySaves: number\n /** Private voses held at once (counted at create). */\n privateVoses: number\n /** Key-authed .glb/.gltf uploads per 24h, per owning user. */\n keyModelUploads: number\n /** Key-authed recording uploads per 24h, per owning user. */\n keyRecordingUploads: number\n /** Key-authed image uploads per 24h (posters/stills an agent files into a project). */\n keyImageUploads: number\n /** Session recording uploads per 24h (uploads count as uploads). */\n recordingUploads: number\n /** Session image / HDR / audio uploads per 24h. */\n otherUploads: number\n /** Longest hosted recording, seconds (stated before you record). */\n recordingMaxSeconds: number\n /** Largest single recording upload, bytes (the ingest ceiling). */\n recordingMaxBytes: number\n /** Total asset bytes an account may hold (said in hours in the UI). */\n storageBytes: number\n /** Session versions per vos per 24h. */\n versionsPerVosDay: number\n /** Key-authed version pushes per 24h across every vos. */\n keyVersionsPerDay: number\n /** Voses + assets + recipes one folder may hold. */\n folderItems: number\n /** Subfolders one folder may hold. */\n subfolders: number\n /** Plumbing media enqueues (preview + thumbnail) per vos per hour. */\n mediaPerVosHour: number\n /** Plumbing media enqueues per account per 24h. Past it, media DEFERS. */\n mediaPerAccountDay: number\n /** Artifact render output-minutes per account per 24h (armed with cloud export). */\n artifactRenderMinutesPerDay: number\n /** Own-vos backdrop bakes per account per 24h (cache hits never count). */\n backdropBakesPerDay: number\n /** Folders per user, across every nesting level. */\n folders: number\n /** Recipe files (.md assets) per user. */\n recipes: number\n /** Key-authed recipe creates per 24h. */\n keyRecipeCreates: number\n /** Recipe body byte cap (upload + in-place replace). */\n recipeBodyBytes: number\n}\n\nconst FREE: PlanLimits = {\n dailySaves: 10,\n privateVoses: 20,\n keyModelUploads: 10,\n keyRecordingUploads: 20,\n keyImageUploads: 40,\n recordingUploads: 20,\n otherUploads: 50,\n recordingMaxSeconds: 30 * 60,\n recordingMaxBytes: 512 * 1024 * 1024,\n storageBytes: 5 * 1024 * 1024 * 1024,\n versionsPerVosDay: 100,\n keyVersionsPerDay: 200,\n folderItems: 500,\n subfolders: 50,\n mediaPerVosHour: 20,\n mediaPerAccountDay: 300,\n artifactRenderMinutesPerDay: 60,\n backdropBakesPerDay: 20,\n // Folders & recipes: sanity caps, not a paywall — organization\n // stays free at every size that isn't abuse.\n folders: 50,\n recipes: 200,\n keyRecipeCreates: 20,\n recipeBodyBytes: 64 * 1024,\n}\n\n// Keyed by string on purpose: the column is free text at the wire, so a\n// value the table does not know must fall through to free, never throw.\nconst LIMITS: Partial<Record<string, PlanLimits>> = { free: FREE }\n\n/** Unknown or absent ⇒ free. A plan the table does not know is never a grant. */\nexport function planLimits(plan?: string | null): PlanLimits {\n return LIMITS[plan ?? 'free'] ?? FREE\n}\n\n/**\n * The storage ceiling in the unit a human can picture. Footage at ~5 Mbps\n * is ~0.375 GB per 10 minutes, so 5 GB reads as \"about 2 hours\"; the guard\n * counts bytes, the sentence says hours. Rounded to a half hour.\n */\nexport const FOOTAGE_BYTES_PER_SECOND = (5_000_000 / 8) * 1\nexport function bytesAsFootageHours(bytes: number): number {\n return Math.round((bytes / FOOTAGE_BYTES_PER_SECOND / 3600) * 2) / 2\n}\n\n/** `1.2 GB`, `640 MB`, `12 KB` — for the usage strip and refusals. */\nexport function formatBytes(bytes: number): string {\n if (bytes >= 1024 ** 3) return `${(bytes / 1024 ** 3).toFixed(1)} GB`\n if (bytes >= 1024 ** 2) return `${Math.round(bytes / 1024 ** 2)} MB`\n if (bytes >= 1024) return `${Math.round(bytes / 1024)} KB`\n return `${bytes} B`\n}\n\n/** `30 min`, `1 h 30 min`, `45 s` — the duration cap in words. */\nexport function formatDurationCap(seconds: number): string {\n if (seconds < 60) return `${seconds} s`\n const m = Math.round(seconds / 60)\n if (m < 60) return `${m} min`\n const h = Math.floor(m / 60)\n const rest = m % 60\n return rest ? `${h} h ${rest} min` : `${h} h`\n}\n"],"mappings":";AAgBO,IAAM,QAAyB,CAAC,MAAM;AAiD7C,IAAM,OAAmB;AAAA,EACvB,YAAY;AAAA,EACZ,cAAc;AAAA,EACd,iBAAiB;AAAA,EACjB,qBAAqB;AAAA,EACrB,iBAAiB;AAAA,EACjB,kBAAkB;AAAA,EAClB,cAAc;AAAA,EACd,qBAAqB,KAAK;AAAA,EAC1B,mBAAmB,MAAM,OAAO;AAAA,EAChC,cAAc,IAAI,OAAO,OAAO;AAAA,EAChC,mBAAmB;AAAA,EACnB,mBAAmB;AAAA,EACnB,aAAa;AAAA,EACb,YAAY;AAAA,EACZ,iBAAiB;AAAA,EACjB,oBAAoB;AAAA,EACpB,6BAA6B;AAAA,EAC7B,qBAAqB;AAAA;AAAA;AAAA,EAGrB,SAAS;AAAA,EACT,SAAS;AAAA,EACT,kBAAkB;AAAA,EAClB,iBAAiB,KAAK;AACxB;AAIA,IAAM,SAA8C,EAAE,MAAM,KAAK;AAG1D,SAAS,WAAW,MAAkC;AAC3D,SAAO,OAAO,QAAQ,MAAM,KAAK;AACnC;AAOO,IAAM,2BAA4B,MAAY,IAAK;AACnD,SAAS,oBAAoB,OAAuB;AACzD,SAAO,KAAK,MAAO,QAAQ,2BAA2B,OAAQ,CAAC,IAAI;AACrE;AAGO,SAAS,YAAY,OAAuB;AACjD,MAAI,SAAS,QAAQ,EAAG,QAAO,IAAI,QAAQ,QAAQ,GAAG,QAAQ,CAAC,CAAC;AAChE,MAAI,SAAS,QAAQ,EAAG,QAAO,GAAG,KAAK,MAAM,QAAQ,QAAQ,CAAC,CAAC;AAC/D,MAAI,SAAS,KAAM,QAAO,GAAG,KAAK,MAAM,QAAQ,IAAI,CAAC;AACrD,SAAO,GAAG,KAAK;AACjB;AAGO,SAAS,kBAAkB,SAAyB;AACzD,MAAI,UAAU,GAAI,QAAO,GAAG,OAAO;AACnC,QAAM,IAAI,KAAK,MAAM,UAAU,EAAE;AACjC,MAAI,IAAI,GAAI,QAAO,GAAG,CAAC;AACvB,QAAM,IAAI,KAAK,MAAM,IAAI,EAAE;AAC3B,QAAM,OAAO,IAAI;AACjB,SAAO,OAAO,GAAG,CAAC,MAAM,IAAI,SAAS,GAAG,CAAC;AAC3C;","names":[]}
@@ -0,0 +1,101 @@
1
+ /**
2
+ * Config params: `config.params`
3
+ * declares the `ctx.data` keys a program reads as its creative knobs — key,
4
+ * kind, range, default. THE one params module, shared by the web Remix
5
+ * panel, the API, and scripts (the knob honesty lint): edits commit by
6
+ * baking BOTH `params[i].default` and `data[key]` into the config, so
7
+ * saves/exports/server renders pick the values up through the existing
8
+ * `config.data` machinery with zero new plumbing.
9
+ *
10
+ * The engine schema does not know `params` yet (upstream addition pending) —
11
+ * `vosConfigJsonSchema` STRIPS unknown fields on parse, so the API re-attaches
12
+ * validated params at the storage boundary (the platform's server-side copy;
13
+ * change both together) and
14
+ * this module validates defensively.
15
+ */
16
+ type ParamValue = number | string | boolean;
17
+ interface ParamSpec {
18
+ /** The ctx.data key the program reads. */
19
+ key: string;
20
+ /** Human label; falls back to the key. */
21
+ label?: string;
22
+ /** One sentence on what the knob changes (U3b — knobs carry meaning). */
23
+ hint?: string;
24
+ /** Unit shown inside the number field: px, %, s, ×, °. */
25
+ unit?: string;
26
+ /** Optional card grouping; ungrouped knobs land on the Remix card. */
27
+ group?: string;
28
+ /** Sort order within a group (falls back to declaration order). */
29
+ order?: number;
30
+ kind: 'number' | 'color' | 'select' | 'toggle' | 'text' | 'font';
31
+ /** number kind */
32
+ min?: number;
33
+ max?: number;
34
+ step?: number;
35
+ /**
36
+ * select kind: the enumerated choices (REQUIRED, ≥2). font kind: an
37
+ * OPTIONAL curation — the families the knob offers; absent = the whole
38
+ * hosted catalog. Faces travel with the value: `applyParamValue` writes
39
+ * the chosen family's hosted faces into `data.fonts`, which the engine
40
+ * (core ≥0.17) registers at boot and on SET_DATA — no `config.fonts`
41
+ * declaration needed, no recompile.
42
+ */
43
+ options?: string[];
44
+ /** text kind: render a multiline editor (content knobs, e.g. a headline). */
45
+ multiline?: boolean;
46
+ default: ParamValue;
47
+ }
48
+ /**
49
+ * A Look (U3b): a named set of param values — the feel-the-range layer.
50
+ * Tap a look, then fine-tune; applying is ONE undoable multi-value commit.
51
+ */
52
+ interface LookPreset {
53
+ name: string;
54
+ values: Record<string, ParamValue>;
55
+ }
56
+ /**
57
+ * Text params carry URLs (modelUrl knobs) and bound content
58
+ * (headline knobs) — longer than other string kinds.
59
+ */
60
+ declare const TEXT_PARAM_MAX = 280;
61
+ /** Validate raw config.params defensively; invalid entries are dropped. */
62
+ declare function readParams(config: Record<string, unknown> | null | undefined): ParamSpec[];
63
+ /**
64
+ * Current committed value per param: `config.data[key]` when type-compatible
65
+ * (a previously baked edit), else the spec default.
66
+ */
67
+ declare function paramValues(config: Record<string, unknown> | null | undefined, specs: readonly ParamSpec[]): Record<string, ParamValue>;
68
+ /**
69
+ * Validate raw `config.presets` (Looks, U3b) against the declared params:
70
+ * a look keeps only values whose key is declared AND whose type matches the
71
+ * spec default; looks with no surviving values are dropped. Like params,
72
+ * looks are progressive enhancement — never a load blocker.
73
+ */
74
+ declare function readLooks(config: Record<string, unknown> | null | undefined, specs: readonly ParamSpec[]): LookPreset[];
75
+ /** Apply a set of param values to a config draft — ONE recipe, one undo. */
76
+ declare function applyParamValues(cfg: Record<string, unknown>, values: Record<string, ParamValue>): void;
77
+ /**
78
+ * Bake a param value into a config draft (patch-store recipe body): updates
79
+ * the matching `params[i].default` AND `data[key]` — data is what playback,
80
+ * exports, and server renders actually read.
81
+ */
82
+ declare function applyParamValue(cfg: Record<string, unknown>, key: string, value: ParamValue): void;
83
+ interface BoundElementProp {
84
+ /** The ctx.data key the prop is bound to. */
85
+ key: string;
86
+ elementId: string;
87
+ prop: 'content' | 'family' | 'color';
88
+ /** Split text resolves bindings at boot only — a change is structural. */
89
+ split: boolean;
90
+ }
91
+ /** Enumerate `{$data}`-bound text element props declared in a config. */
92
+ declare function readBindings(config: Record<string, unknown> | null | undefined): BoundElementProp[];
93
+ /**
94
+ * Data keys whose change is STRUCTURAL: bound into split text, which the
95
+ * engine resolves at boot only (per-unit meshes + timeline segment bindings).
96
+ * Hosts must fold these keys' VALUES into the held-program identity so a
97
+ * change costs one warm LOAD instead of leaving stale glyphs on screen.
98
+ */
99
+ declare function structuralDataKeys(config: Record<string, unknown> | null | undefined): string[];
100
+
101
+ export { type BoundElementProp, type LookPreset, type ParamSpec, type ParamValue, TEXT_PARAM_MAX, applyParamValue, applyParamValues, paramValues, readBindings, readLooks, readParams, structuralDataKeys };
package/dist/params.js ADDED
@@ -0,0 +1,205 @@
1
+ import {
2
+ findFontFamily,
3
+ fontFaceUrl
4
+ } from "./chunk-YBEIOW7L.js";
5
+
6
+ // src/params.ts
7
+ var KINDS = /* @__PURE__ */ new Set(["number", "color", "select", "toggle", "text", "font"]);
8
+ var TEXT_PARAM_MAX = 280;
9
+ function readParams(config) {
10
+ const raw = config?.params;
11
+ if (!Array.isArray(raw)) return [];
12
+ const out = [];
13
+ const seen = /* @__PURE__ */ new Set();
14
+ for (const entry of raw) {
15
+ if (!entry || typeof entry !== "object") continue;
16
+ const p = entry;
17
+ const key = p.key;
18
+ const kind = p.kind;
19
+ if (typeof key !== "string" || !key || seen.has(key)) continue;
20
+ if (typeof kind !== "string" || !KINDS.has(kind)) continue;
21
+ const label = typeof p.label === "string" ? p.label : void 0;
22
+ const meta = {
23
+ hint: typeof p.hint === "string" ? p.hint : void 0,
24
+ unit: typeof p.unit === "string" ? p.unit : void 0,
25
+ group: typeof p.group === "string" ? p.group : void 0,
26
+ order: typeof p.order === "number" ? p.order : void 0
27
+ };
28
+ if (kind === "number") {
29
+ if (typeof p.default !== "number") continue;
30
+ const min = typeof p.min === "number" ? p.min : 0;
31
+ const max = typeof p.max === "number" ? p.max : 1;
32
+ if (!(max > min)) continue;
33
+ out.push({
34
+ key,
35
+ label,
36
+ ...meta,
37
+ kind,
38
+ min,
39
+ max,
40
+ step: typeof p.step === "number" && p.step > 0 ? p.step : void 0,
41
+ default: Math.min(max, Math.max(min, p.default))
42
+ });
43
+ } else if (kind === "color") {
44
+ if (typeof p.default !== "string" || !p.default) continue;
45
+ out.push({ key, label, ...meta, kind, default: p.default });
46
+ } else if (kind === "text") {
47
+ if (typeof p.default !== "string" || p.default.length > TEXT_PARAM_MAX)
48
+ continue;
49
+ out.push({
50
+ key,
51
+ label,
52
+ ...meta,
53
+ kind,
54
+ multiline: p.multiline === true ? true : void 0,
55
+ default: p.default
56
+ });
57
+ } else if (kind === "select" || kind === "font") {
58
+ const options = Array.isArray(p.options) ? p.options.filter((o) => typeof o === "string" && !!o) : [];
59
+ if (typeof p.default !== "string") continue;
60
+ if (kind === "font" && options.length === 0) {
61
+ if (!p.default) continue;
62
+ out.push({ key, label, ...meta, kind, default: p.default });
63
+ } else {
64
+ if (options.length < 2) continue;
65
+ out.push({
66
+ key,
67
+ label,
68
+ ...meta,
69
+ kind,
70
+ options,
71
+ default: options.includes(p.default) ? p.default : options[0]
72
+ });
73
+ }
74
+ } else {
75
+ if (typeof p.default !== "boolean") continue;
76
+ out.push({ key, label, ...meta, kind: "toggle", default: p.default });
77
+ }
78
+ seen.add(key);
79
+ }
80
+ return out;
81
+ }
82
+ function paramValues(config, specs) {
83
+ const data = config?.data && typeof config.data === "object" ? config.data : {};
84
+ const out = {};
85
+ for (const spec of specs) {
86
+ const v = data[spec.key];
87
+ out[spec.key] = typeof v === typeof spec.default ? v : spec.default;
88
+ }
89
+ return out;
90
+ }
91
+ function readLooks(config, specs) {
92
+ const raw = config?.presets;
93
+ if (!Array.isArray(raw) || specs.length === 0) return [];
94
+ const byKey = new Map(specs.map((s) => [s.key, s]));
95
+ const out = [];
96
+ const seen = /* @__PURE__ */ new Set();
97
+ for (const entry of raw) {
98
+ if (!entry || typeof entry !== "object") continue;
99
+ const p = entry;
100
+ const name = p.name;
101
+ if (typeof name !== "string" || !name || seen.has(name)) continue;
102
+ if (!p.values || typeof p.values !== "object") continue;
103
+ const values = {};
104
+ for (const [k, v] of Object.entries(p.values)) {
105
+ const spec = byKey.get(k);
106
+ if (!spec) continue;
107
+ if (typeof v !== typeof spec.default) continue;
108
+ values[k] = v;
109
+ }
110
+ if (!Object.keys(values).length) continue;
111
+ out.push({ name, values });
112
+ seen.add(name);
113
+ }
114
+ return out;
115
+ }
116
+ function applyParamValues(cfg, values) {
117
+ for (const [key, value] of Object.entries(values)) {
118
+ applyParamValue(cfg, key, value);
119
+ }
120
+ }
121
+ function applyParamValue(cfg, key, value) {
122
+ if (Array.isArray(cfg.params)) {
123
+ for (const entry of cfg.params) {
124
+ if (entry && typeof entry === "object" && entry.key === key) {
125
+ ;
126
+ entry.default = value;
127
+ }
128
+ }
129
+ }
130
+ if (!cfg.data || typeof cfg.data !== "object") cfg.data = {};
131
+ cfg.data[key] = value;
132
+ syncDataFonts(cfg);
133
+ }
134
+ function facesFor(family) {
135
+ const entry = findFontFamily(family);
136
+ if (!entry) return [];
137
+ return entry.weights.map((w) => ({
138
+ family: entry.family,
139
+ weight: w,
140
+ url: fontFaceUrl(entry.slug, w)
141
+ }));
142
+ }
143
+ function syncDataFonts(cfg) {
144
+ const specs = Array.isArray(cfg.params) ? cfg.params : [];
145
+ const fontSpecs = specs.filter(
146
+ (p) => !!p && typeof p === "object" && p.kind === "font"
147
+ );
148
+ if (!fontSpecs.length) return;
149
+ const data = cfg.data;
150
+ const faces = [];
151
+ const seen = /* @__PURE__ */ new Set();
152
+ for (const spec of fontSpecs) {
153
+ const specKey = spec.key;
154
+ const dataValue = typeof specKey === "string" ? data[specKey] : void 0;
155
+ const v = typeof dataValue === "string" ? dataValue : typeof spec.default === "string" ? spec.default : "";
156
+ for (const face of facesFor(v)) {
157
+ const id = `${face.family}|${face.weight}`;
158
+ if (seen.has(id)) continue;
159
+ seen.add(id);
160
+ faces.push(face);
161
+ }
162
+ }
163
+ cfg.data = { ...data, fonts: faces };
164
+ }
165
+ function refKey(v) {
166
+ if (v && typeof v === "object" && typeof v.$data === "string" && v.$data)
167
+ return v.$data;
168
+ return null;
169
+ }
170
+ function readBindings(config) {
171
+ const elements = Array.isArray(config?.elements) ? config.elements : [];
172
+ const out = [];
173
+ elements.forEach((el, index) => {
174
+ if (!el || typeof el !== "object") return;
175
+ const e = el;
176
+ if (e.type !== "text") return;
177
+ const elementId = typeof e.id === "string" ? e.id : `element_${index}`;
178
+ const split = !!e.split;
179
+ const content = refKey(e.content);
180
+ if (content) out.push({ key: content, elementId, prop: "content", split });
181
+ const family = refKey(e.font?.family);
182
+ if (family) out.push({ key: family, elementId, prop: "family", split });
183
+ const color = refKey(e.font?.color);
184
+ if (color) out.push({ key: color, elementId, prop: "color", split });
185
+ });
186
+ return out;
187
+ }
188
+ function structuralDataKeys(config) {
189
+ return [
190
+ ...new Set(
191
+ readBindings(config).filter((b) => b.split).map((b) => b.key)
192
+ )
193
+ ];
194
+ }
195
+ export {
196
+ TEXT_PARAM_MAX,
197
+ applyParamValue,
198
+ applyParamValues,
199
+ paramValues,
200
+ readBindings,
201
+ readLooks,
202
+ readParams,
203
+ structuralDataKeys
204
+ };
205
+ //# sourceMappingURL=params.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/params.ts"],"sourcesContent":["/**\n * Config params: `config.params`\n * declares the `ctx.data` keys a program reads as its creative knobs — key,\n * kind, range, default. THE one params module, shared by the web Remix\n * panel, the API, and scripts (the knob honesty lint): edits commit by\n * baking BOTH `params[i].default` and `data[key]` into the config, so\n * saves/exports/server renders pick the values up through the existing\n * `config.data` machinery with zero new plumbing.\n *\n * The engine schema does not know `params` yet (upstream addition pending) —\n * `vosConfigJsonSchema` STRIPS unknown fields on parse, so the API re-attaches\n * validated params at the storage boundary (the platform's server-side copy;\n * change both together) and\n * this module validates defensively.\n */\n\nimport { findFontFamily, fontFaceUrl } from './fonts'\n\nexport type ParamValue = number | string | boolean\n\nexport interface ParamSpec {\n /** The ctx.data key the program reads. */\n key: string\n /** Human label; falls back to the key. */\n label?: string\n /** One sentence on what the knob changes (U3b — knobs carry meaning). */\n hint?: string\n /** Unit shown inside the number field: px, %, s, ×, °. */\n unit?: string\n /** Optional card grouping; ungrouped knobs land on the Remix card. */\n group?: string\n /** Sort order within a group (falls back to declaration order). */\n order?: number\n kind: 'number' | 'color' | 'select' | 'toggle' | 'text' | 'font'\n /** number kind */\n min?: number\n max?: number\n step?: number\n /**\n * select kind: the enumerated choices (REQUIRED, ≥2). font kind: an\n * OPTIONAL curation — the families the knob offers; absent = the whole\n * hosted catalog. Faces travel with the value: `applyParamValue` writes\n * the chosen family's hosted faces into `data.fonts`, which the engine\n * (core ≥0.17) registers at boot and on SET_DATA — no `config.fonts`\n * declaration needed, no recompile.\n */\n options?: string[]\n /** text kind: render a multiline editor (content knobs, e.g. a headline). */\n multiline?: boolean\n default: ParamValue\n}\n\n/**\n * A Look (U3b): a named set of param values — the feel-the-range layer.\n * Tap a look, then fine-tune; applying is ONE undoable multi-value commit.\n */\nexport interface LookPreset {\n name: string\n values: Record<string, ParamValue>\n}\n\nconst KINDS = new Set(['number', 'color', 'select', 'toggle', 'text', 'font'])\n\n/**\n * Text params carry URLs (modelUrl knobs) and bound content\n * (headline knobs) — longer than other string kinds.\n */\nexport const TEXT_PARAM_MAX = 280\n\n/** Validate raw config.params defensively; invalid entries are dropped. */\nexport function readParams(\n config: Record<string, unknown> | null | undefined,\n): ParamSpec[] {\n const raw = config?.params\n if (!Array.isArray(raw)) return []\n const out: ParamSpec[] = []\n const seen = new Set<string>()\n for (const entry of raw) {\n if (!entry || typeof entry !== 'object') continue\n const p = entry as Record<string, unknown>\n const key = p.key\n const kind = p.kind\n if (typeof key !== 'string' || !key || seen.has(key)) continue\n if (typeof kind !== 'string' || !KINDS.has(kind)) continue\n const label = typeof p.label === 'string' ? p.label : undefined\n const meta = {\n hint: typeof p.hint === 'string' ? p.hint : undefined,\n unit: typeof p.unit === 'string' ? p.unit : undefined,\n group: typeof p.group === 'string' ? p.group : undefined,\n order: typeof p.order === 'number' ? p.order : undefined,\n }\n if (kind === 'number') {\n if (typeof p.default !== 'number') continue\n const min = typeof p.min === 'number' ? p.min : 0\n const max = typeof p.max === 'number' ? p.max : 1\n if (!(max > min)) continue\n out.push({\n key,\n label,\n ...meta,\n kind,\n min,\n max,\n step: typeof p.step === 'number' && p.step > 0 ? p.step : undefined,\n default: Math.min(max, Math.max(min, p.default)),\n })\n } else if (kind === 'color') {\n if (typeof p.default !== 'string' || !p.default) continue\n out.push({ key, label, ...meta, kind, default: p.default })\n } else if (kind === 'text') {\n // Empty default is meaningful (a modelUrl knob's \"use the built-in\").\n if (typeof p.default !== 'string' || p.default.length > TEXT_PARAM_MAX)\n continue\n out.push({\n key,\n label,\n ...meta,\n kind,\n multiline: p.multiline === true ? true : undefined,\n default: p.default,\n })\n } else if (kind === 'select' || kind === 'font') {\n const options = Array.isArray(p.options)\n ? p.options.filter((o): o is string => typeof o === 'string' && !!o)\n : []\n if (typeof p.default !== 'string') continue\n if (kind === 'font' && options.length === 0) {\n // Optionless font knob = the whole hosted catalog. The default fails\n // open like FontField does (an unknown family renders verbatim);\n // faces travel in data.fonts via applyParamValue, so no curation is\n // required for fleet honesty since engine 0.17.\n if (!p.default) continue\n out.push({ key, label, ...meta, kind, default: p.default })\n } else {\n if (options.length < 2) continue\n out.push({\n key,\n label,\n ...meta,\n kind,\n options,\n default: options.includes(p.default) ? p.default : options[0],\n })\n }\n } else {\n if (typeof p.default !== 'boolean') continue\n out.push({ key, label, ...meta, kind: 'toggle', default: p.default })\n }\n seen.add(key)\n }\n return out\n}\n\n/**\n * Current committed value per param: `config.data[key]` when type-compatible\n * (a previously baked edit), else the spec default.\n */\nexport function paramValues(\n config: Record<string, unknown> | null | undefined,\n specs: readonly ParamSpec[],\n): Record<string, ParamValue> {\n const data =\n config?.data && typeof config.data === 'object'\n ? (config.data as Record<string, unknown>)\n : {}\n const out: Record<string, ParamValue> = {}\n for (const spec of specs) {\n const v = data[spec.key]\n out[spec.key] =\n typeof v === typeof spec.default ? (v as ParamValue) : spec.default\n }\n return out\n}\n\n/**\n * Validate raw `config.presets` (Looks, U3b) against the declared params:\n * a look keeps only values whose key is declared AND whose type matches the\n * spec default; looks with no surviving values are dropped. Like params,\n * looks are progressive enhancement — never a load blocker.\n */\nexport function readLooks(\n config: Record<string, unknown> | null | undefined,\n specs: readonly ParamSpec[],\n): LookPreset[] {\n const raw = config?.presets\n if (!Array.isArray(raw) || specs.length === 0) return []\n const byKey = new Map(specs.map((s) => [s.key, s]))\n const out: LookPreset[] = []\n const seen = new Set<string>()\n for (const entry of raw) {\n if (!entry || typeof entry !== 'object') continue\n const p = entry as Record<string, unknown>\n const name = p.name\n if (typeof name !== 'string' || !name || seen.has(name)) continue\n if (!p.values || typeof p.values !== 'object') continue\n const values: Record<string, ParamValue> = {}\n for (const [k, v] of Object.entries(p.values as Record<string, unknown>)) {\n const spec = byKey.get(k)\n if (!spec) continue\n if (typeof v !== typeof spec.default) continue\n values[k] = v as ParamValue\n }\n if (!Object.keys(values).length) continue\n out.push({ name, values })\n seen.add(name)\n }\n return out\n}\n\n/** Apply a set of param values to a config draft — ONE recipe, one undo. */\nexport function applyParamValues(\n cfg: Record<string, unknown>,\n values: Record<string, ParamValue>,\n): void {\n for (const [key, value] of Object.entries(values)) {\n applyParamValue(cfg, key, value)\n }\n}\n\n/**\n * Bake a param value into a config draft (patch-store recipe body): updates\n * the matching `params[i].default` AND `data[key]` — data is what playback,\n * exports, and server renders actually read.\n */\nexport function applyParamValue(\n cfg: Record<string, unknown>,\n key: string,\n value: ParamValue,\n): void {\n if (Array.isArray(cfg.params)) {\n for (const entry of cfg.params) {\n if (\n entry &&\n typeof entry === 'object' &&\n (entry as Record<string, unknown>).key === key\n ) {\n ;(entry as Record<string, unknown>).default = value\n }\n }\n }\n // Write IN PLACE, never replace the object: this runs inside a patch-store\n // recipe, and a replaced `data` is a patch even when nothing changed — a\n // re-commit of an equal value (a field's blur after a scrub) minted an undo\n // entry that undid nothing. An equal assignment produces no patch.\n if (!cfg.data || typeof cfg.data !== 'object') cfg.data = {}\n ;(cfg.data as Record<string, unknown>)[key] = value\n syncDataFonts(cfg)\n}\n\n/** Hosted faces for a catalog family — one entry per weight (weights are\n * files, not synthesis). Unknown families contribute nothing (fail-open:\n * the renderer falls back to the preset stack, like FontField). */\nfunction facesFor(\n family: string,\n): { family: string; url: string; weight: number }[] {\n const entry = findFontFamily(family)\n if (!entry) return []\n return entry.weights.map((w) => ({\n family: entry.family,\n weight: w,\n url: fontFaceUrl(entry.slug, w),\n }))\n}\n\n/**\n * Faces travel with the value: rebuild `data.fonts` from EVERY font-kind\n * knob's current value — the engine (core ≥0.17) registers them at boot and\n * on SET_DATA, so a font knob needs no `config.fonts` declaration and no\n * recompile. Rebuilding (not appending) prunes families no knob points at\n * any more; deterministic, so probes and saves agree byte-for-byte. Only\n * configs WITH font knobs get a `data.fonts` key.\n */\nfunction syncDataFonts(cfg: Record<string, unknown>): void {\n const specs = Array.isArray(cfg.params) ? cfg.params : []\n const fontSpecs = specs.filter(\n (p): p is Record<string, unknown> =>\n !!p && typeof p === 'object' && (p as { kind?: unknown }).kind === 'font',\n )\n if (!fontSpecs.length) return\n const data = cfg.data as Record<string, unknown>\n const faces: { family: string; url: string; weight: number }[] = []\n const seen = new Set<string>()\n for (const spec of fontSpecs) {\n const specKey = spec.key\n const dataValue = typeof specKey === 'string' ? data[specKey] : undefined\n const v =\n typeof dataValue === 'string'\n ? dataValue\n : typeof spec.default === 'string'\n ? spec.default\n : ''\n for (const face of facesFor(v)) {\n const id = `${face.family}|${face.weight}`\n if (seen.has(id)) continue\n seen.add(id)\n faces.push(face)\n }\n }\n cfg.data = { ...data, fonts: faces }\n}\n\n// ---------------------------------------------------------------------------\n// {$data} element bindings: the engine resolves `{$data: key}` refs on\n// text `content` / `font.family` / `font.color` from ctx.data and re-rasters\n// on SET_DATA — which is what gives text/font knobs something to bind.\n// ---------------------------------------------------------------------------\n\nfunction refKey(v: unknown): string | null {\n if (\n v &&\n typeof v === 'object' &&\n typeof (v as { $data?: unknown }).$data === 'string' &&\n (v as { $data: string }).$data\n )\n return (v as { $data: string }).$data\n return null\n}\n\nexport interface BoundElementProp {\n /** The ctx.data key the prop is bound to. */\n key: string\n elementId: string\n prop: 'content' | 'family' | 'color'\n /** Split text resolves bindings at boot only — a change is structural. */\n split: boolean\n}\n\n/** Enumerate `{$data}`-bound text element props declared in a config. */\nexport function readBindings(\n config: Record<string, unknown> | null | undefined,\n): BoundElementProp[] {\n const elements = Array.isArray(config?.elements) ? config.elements : []\n const out: BoundElementProp[] = []\n elements.forEach((el, index) => {\n if (!el || typeof el !== 'object') return\n const e = el as Record<string, any>\n if (e.type !== 'text') return\n const elementId = typeof e.id === 'string' ? e.id : `element_${index}`\n const split = !!e.split\n const content = refKey(e.content)\n if (content) out.push({ key: content, elementId, prop: 'content', split })\n const family = refKey(e.font?.family)\n if (family) out.push({ key: family, elementId, prop: 'family', split })\n const color = refKey(e.font?.color)\n if (color) out.push({ key: color, elementId, prop: 'color', split })\n })\n return out\n}\n\n/**\n * Data keys whose change is STRUCTURAL: bound into split text, which the\n * engine resolves at boot only (per-unit meshes + timeline segment bindings).\n * Hosts must fold these keys' VALUES into the held-program identity so a\n * change costs one warm LOAD instead of leaving stale glyphs on screen.\n */\nexport function structuralDataKeys(\n config: Record<string, unknown> | null | undefined,\n): string[] {\n return [\n ...new Set(\n readBindings(config)\n .filter((b) => b.split)\n .map((b) => b.key),\n ),\n ]\n}\n"],"mappings":"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","names":[]}
@@ -0,0 +1,35 @@
1
+ /**
2
+ * Tier-(a) timeline editing: serializable timing edits over an UNCHANGED
3
+ * `createTimeline` (the timing-overlay strategy).
4
+ *
5
+ * Instead of regenerating user animation code (impossible for opaque
6
+ * onUpdate/modifier tweens), an editor keeps the BASE config and bakes a
7
+ * `TimelineEdit[]` overlay into a thin wrapper: the original function runs and
8
+ * records as always, then `tl.applyEdits(...)` retimes the recorded entries.
9
+ * Works for every tween — structured or opaque — on the vos tween backend.
10
+ *
11
+ * Always wrap the BASE source (never an already-wrapped one): the editor owns
12
+ * the base config + the overlay, and regenerates the wrapper per commit. Lives
13
+ * in shared because two consumers bake it: the studio's program anchor (its
14
+ * composed config, in studio-core) and the web's program edits (base chunk).
15
+ */
16
+ /** One entry of the overlay — `@vosjs/tween`'s `TweenEdit`, structurally. */
17
+ interface TimelineEdit {
18
+ index: number;
19
+ startTime?: number;
20
+ duration?: number;
21
+ ease?: string;
22
+ to?: Record<string, number>;
23
+ from?: Record<string, number>;
24
+ }
25
+ /** Wrap a createTimeline function string with a baked edits overlay. */
26
+ declare function wrapCreateTimeline(baseSource: string, edits: readonly TimelineEdit[]): string;
27
+ /**
28
+ * Produce the effective config: the base config with its createTimeline wrapped
29
+ * by the overlay (the same object when there are no edits).
30
+ */
31
+ declare function applyTimelineEdits<T extends {
32
+ createTimeline?: unknown;
33
+ }>(baseConfig: T, edits: readonly TimelineEdit[]): T;
34
+
35
+ export { type TimelineEdit, applyTimelineEdits, wrapCreateTimeline };
@@ -0,0 +1,19 @@
1
+ // src/timelineEdits.ts
2
+ function wrapCreateTimeline(baseSource, edits) {
3
+ return `(ctx, content, duration) => {
4
+ const __base = (${baseSource});
5
+ const tl = __base(ctx, content, duration);
6
+ if (tl && typeof tl.applyEdits === 'function') tl.applyEdits(${JSON.stringify(edits)});
7
+ return tl;
8
+ }`;
9
+ }
10
+ function applyTimelineEdits(baseConfig, edits) {
11
+ const source = baseConfig.createTimeline;
12
+ if (!edits.length || typeof source !== "string" || !source) return baseConfig;
13
+ return { ...baseConfig, createTimeline: wrapCreateTimeline(source, edits) };
14
+ }
15
+ export {
16
+ applyTimelineEdits,
17
+ wrapCreateTimeline
18
+ };
19
+ //# sourceMappingURL=timelineEdits.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/timelineEdits.ts"],"sourcesContent":["/**\n * Tier-(a) timeline editing: serializable timing edits over an UNCHANGED\n * `createTimeline` (the timing-overlay strategy).\n *\n * Instead of regenerating user animation code (impossible for opaque\n * onUpdate/modifier tweens), an editor keeps the BASE config and bakes a\n * `TimelineEdit[]` overlay into a thin wrapper: the original function runs and\n * records as always, then `tl.applyEdits(...)` retimes the recorded entries.\n * Works for every tween — structured or opaque — on the vos tween backend.\n *\n * Always wrap the BASE source (never an already-wrapped one): the editor owns\n * the base config + the overlay, and regenerates the wrapper per commit. Lives\n * in shared because two consumers bake it: the studio's program anchor (its\n * composed config, in studio-core) and the web's program edits (base chunk).\n */\n\n/** One entry of the overlay — `@vosjs/tween`'s `TweenEdit`, structurally. */\nexport interface TimelineEdit {\n index: number\n startTime?: number\n duration?: number\n ease?: string\n to?: Record<string, number>\n from?: Record<string, number>\n}\n\n/** Wrap a createTimeline function string with a baked edits overlay. */\nexport function wrapCreateTimeline(\n baseSource: string,\n edits: readonly TimelineEdit[],\n): string {\n return `(ctx, content, duration) => {\n const __base = (${baseSource});\n const tl = __base(ctx, content, duration);\n if (tl && typeof tl.applyEdits === 'function') tl.applyEdits(${JSON.stringify(edits)});\n return tl;\n}`\n}\n\n/**\n * Produce the effective config: the base config with its createTimeline wrapped\n * by the overlay (the same object when there are no edits).\n */\nexport function applyTimelineEdits<T extends { createTimeline?: unknown }>(\n baseConfig: T,\n edits: readonly TimelineEdit[],\n): T {\n const source = baseConfig.createTimeline\n if (!edits.length || typeof source !== 'string' || !source) return baseConfig\n return { ...baseConfig, createTimeline: wrapCreateTimeline(source, edits) }\n}\n"],"mappings":";AA2BO,SAAS,mBACd,YACA,OACQ;AACR,SAAO;AAAA,oBACW,UAAU;AAAA;AAAA,iEAEmC,KAAK,UAAU,KAAK,CAAC;AAAA;AAAA;AAGtF;AAMO,SAAS,mBACd,YACA,OACG;AACH,QAAM,SAAS,WAAW;AAC1B,MAAI,CAAC,MAAM,UAAU,OAAO,WAAW,YAAY,CAAC,OAAQ,QAAO;AACnE,SAAO,EAAE,GAAG,YAAY,gBAAgB,mBAAmB,QAAQ,KAAK,EAAE;AAC5E;","names":[]}
@@ -0,0 +1,2 @@
1
+
2
+ export { }
@@ -0,0 +1,2 @@
1
+ import "../chunk-K7EIJSYQ.js";
2
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":[],"sourcesContent":[],"mappings":"","names":[]}
@@ -0,0 +1,23 @@
1
+ /**
2
+ * Shared Utilities
3
+ * Common utility functions used across frontend and backend
4
+ */
5
+ /**
6
+ * Format a filename to a human-readable label
7
+ * @example formatLabel('basic-fade') => 'Basic Fade'
8
+ */
9
+ declare function formatLabel(filename: string): string;
10
+ /**
11
+ * Generate a unique ID
12
+ */
13
+ declare function generateId(): string;
14
+ /**
15
+ * Sleep for a specified duration
16
+ */
17
+ declare function sleep(ms: number): Promise<void>;
18
+ /**
19
+ * Safely parse JSON with a fallback
20
+ */
21
+ declare function safeJsonParse<T>(json: string, fallback: T): T;
22
+
23
+ export { formatLabel, generateId, safeJsonParse, sleep };
@@ -0,0 +1,13 @@
1
+ import {
2
+ formatLabel,
3
+ generateId,
4
+ safeJsonParse,
5
+ sleep
6
+ } from "../chunk-RDD5SU6G.js";
7
+ export {
8
+ formatLabel,
9
+ generateId,
10
+ safeJsonParse,
11
+ sleep
12
+ };
13
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":[],"sourcesContent":[],"mappings":"","names":[]}
package/package.json ADDED
@@ -0,0 +1,80 @@
1
+ {
2
+ "name": "@vosjs/shared",
3
+ "version": "0.1.0",
4
+ "description": "The small shared layer under the vos CLI and the vosso studio: the semantic differ, the free-tier limits, frontmatter, params, the font, music and typeface catalogs.",
5
+ "license": "MIT",
6
+ "author": "vosso",
7
+ "homepage": "https://vos.so",
8
+ "repository": {
9
+ "type": "git",
10
+ "url": "git+https://github.com/vosjs/vos.git",
11
+ "directory": "packages/shared"
12
+ },
13
+ "bugs": {
14
+ "url": "https://github.com/vosjs/vos/issues"
15
+ },
16
+ "type": "module",
17
+ "main": "./dist/index.js",
18
+ "types": "./dist/index.d.ts",
19
+ "exports": {
20
+ ".": {
21
+ "types": "./dist/index.d.ts",
22
+ "import": "./dist/index.js"
23
+ },
24
+ "./types": {
25
+ "types": "./dist/types/index.d.ts",
26
+ "import": "./dist/types/index.js"
27
+ },
28
+ "./utils": {
29
+ "types": "./dist/utils/index.d.ts",
30
+ "import": "./dist/utils/index.js"
31
+ },
32
+ "./diff": {
33
+ "types": "./dist/diff/index.d.ts",
34
+ "import": "./dist/diff/index.js"
35
+ },
36
+ "./frontmatter": {
37
+ "types": "./dist/frontmatter.d.ts",
38
+ "import": "./dist/frontmatter.js"
39
+ },
40
+ "./params": {
41
+ "types": "./dist/params.d.ts",
42
+ "import": "./dist/params.js"
43
+ },
44
+ "./limits": {
45
+ "types": "./dist/limits.d.ts",
46
+ "import": "./dist/limits.js"
47
+ },
48
+ "./acquisition": {
49
+ "types": "./dist/acquisition.d.ts",
50
+ "import": "./dist/acquisition.js"
51
+ },
52
+ "./backdrops": {
53
+ "types": "./dist/backdrops.d.ts",
54
+ "import": "./dist/backdrops.js"
55
+ },
56
+ "./timelineEdits": {
57
+ "types": "./dist/timelineEdits.d.ts",
58
+ "import": "./dist/timelineEdits.js"
59
+ }
60
+ },
61
+ "files": [
62
+ "dist"
63
+ ],
64
+ "sideEffects": false,
65
+ "devDependencies": {
66
+ "tsup": "^8.5.0",
67
+ "typescript": "^5",
68
+ "vitest": "^3.0.5"
69
+ },
70
+ "publishConfig": {
71
+ "access": "public"
72
+ },
73
+ "scripts": {
74
+ "build": "tsup",
75
+ "lint": "eslint .",
76
+ "typecheck": "tsc --noEmit",
77
+ "test": "vitest run",
78
+ "clean": "rm -rf dist node_modules"
79
+ }
80
+ }