@m0saic/knowledge 0.2.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/LICENSE +21 -0
- package/README.md +71 -0
- package/dist/index.d.ts +8 -0
- package/dist/index.js +7 -0
- package/docs/README.md +60 -0
- package/docs/file-formats/m0-iteration-protocol.md +120 -0
- package/docs/file-formats/m0p-and-custom-field.md +195 -0
- package/docs/handbook/README.md +27 -0
- package/docs/handbook/composition-arithmetic.md +278 -0
- package/docs/handbook/dsl-complexity.md +75 -0
- package/docs/handbook/dsl-rules.md +367 -0
- package/docs/handbook/feasibility-precision-quantization.md +591 -0
- package/docs/handbook/m0-construction-methods.md +201 -0
- package/docs/handbook/precision-tiers.md +84 -0
- package/docs/m0saic-thesis.md +95 -0
- package/docs/runtime/README.md +17 -0
- package/docs/runtime/cli-usage.md +372 -0
- package/docs/runtime/ffmpeg-expression-limits.md +117 -0
- package/docs/runtime/reduce-to-one.md +96 -0
- package/docs/skills/README.md +40 -0
- package/docs/skills/axis-and-geometry.md +103 -0
- package/docs/skills/dsl-stdlib-method-catalog.md +7 -0
- package/docs/skills/identity.md +123 -0
- package/docs/skills/labels-and-masks.md +170 -0
- package/docs/skills/m0saic-string-generation.md +251 -0
- package/docs/skills/more-atoms-not-bigger-atoms.md +77 -0
- package/docs/skills/overlay-semantics.md +194 -0
- package/docs/skills/parse-apis.md +79 -0
- package/docs/skills/passthrough-semantics.md +136 -0
- package/docs/skills/structural-construction.md +86 -0
- package/docs/skills/text-in-templates.md +126 -0
- package/docs/skills/zero-overlay-analysis.md +87 -0
- package/docs/templates/README.md +65 -0
- package/docs/templates/capability-templates.md +72 -0
- package/docs/templates/construction-strategy.md +329 -0
- package/docs/templates/data-pipeline.md +324 -0
- package/docs/templates/emission-patterns.md +130 -0
- package/docs/templates/geometry-recipes.md +248 -0
- package/docs/templates/layout-contract.md +168 -0
- package/docs/templates/output-resolution-tree.md +202 -0
- package/docs/templates/patterns/case-study-lessons.md +69 -0
- package/docs/templates/patterns/perf-authoring-rules.md +100 -0
- package/docs/templates/patterns/primitive-extraction-pattern.md +103 -0
- package/docs/templates/philosophy-and-contract.md +310 -0
- package/docs/templates/recursion-nested-rendering.md +138 -0
- package/docs/templates/reference/grid.md +104 -0
- package/docs/templates/reference/json-prop-type.md +169 -0
- package/docs/templates/reference/mosaic-color.md +81 -0
- package/docs/templates/reference/mosaic-placement-props.md +103 -0
- package/docs/templates/reference/prop-bindings.md +203 -0
- package/docs/templates/reference/template-flags.md +205 -0
- package/docs/templates/render-lifecycle.md +117 -0
- package/docs/templates/rendering-model-contract.md +392 -0
- package/docs/templates/standalone-pack-authoring.md +233 -0
- package/docs/templates/theming.md +81 -0
- package/docs/templates/ui-controls.md +150 -0
- package/package.json +37 -0
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
# The `"json"` prop type + typed-output templates
|
|
2
|
+
|
|
3
|
+
Two additive capabilities that unblock structured-config props and typed
|
|
4
|
+
data-fetcher templates. Both are **shipped and current** — verified against
|
|
5
|
+
https://github.com/m0saic-project/m0saic-packages/blob/main/packages/types/src/template/template.ts, https://github.com/m0saic-project/m0saic-packages/blob/main/packages/template-utils/src/template,
|
|
6
|
+
and `validateTemplateProps`.
|
|
7
|
+
|
|
8
|
+
---
|
|
9
|
+
|
|
10
|
+
## 1. `type: "json"`
|
|
11
|
+
|
|
12
|
+
`"json"` is a member of `MosaicTemplatePropType`, alongside the string / number /
|
|
13
|
+
boolean / media / structural (`group`, `list`) and m0-family (`m0`, `m0c`, `m0p`)
|
|
14
|
+
entries.
|
|
15
|
+
|
|
16
|
+
### When to reach for it
|
|
17
|
+
|
|
18
|
+
Use `"json"` when a prop's value is a **structured object or array that maps to a
|
|
19
|
+
TypeScript interface**:
|
|
20
|
+
|
|
21
|
+
- `filter` / `sceneFilter` — a remote API's filter input passed wholesale
|
|
22
|
+
- `{ low, high, mode }` clip-duration configs
|
|
23
|
+
- `Array<MosaicTimeRangeMs>` — the multi-range clip seam (see §3)
|
|
24
|
+
- Per-tile decoration bundles
|
|
25
|
+
|
|
26
|
+
**Don't** use it for:
|
|
27
|
+
|
|
28
|
+
| Instead of `"json"` | Use |
|
|
29
|
+
|---|---|
|
|
30
|
+
| a primitive that merely *looks* like JSON stringified | `"string"` / `"number"` / `"boolean"` |
|
|
31
|
+
| an array of primitives | `"string[]"` / `"number[]"` |
|
|
32
|
+
| a media reference | `"media"` |
|
|
33
|
+
| a fixed set of named sub-fields you want as individual controls | `"group"` with `fields` |
|
|
34
|
+
|
|
35
|
+
That last row is the common mistake: a `"group"` **without** `fields` degrades to a
|
|
36
|
+
raw JSON bag in the editor, which is strictly worse than declaring `"json"` on
|
|
37
|
+
purpose.
|
|
38
|
+
|
|
39
|
+
### Validator behavior — permissive by design
|
|
40
|
+
|
|
41
|
+
`validateTemplateProps` accepts **any non-undefined value** when `type === "json"`.
|
|
42
|
+
The engine boundary does not parse, shape-check, or reject. That is deliberate:
|
|
43
|
+
editors may deliver an already-parsed object *or* a raw JSON string, and both are
|
|
44
|
+
legal at the boundary.
|
|
45
|
+
|
|
46
|
+
**Consequence for template authors:** render-time parsing and shape-checking is
|
|
47
|
+
**your** job. Call `JSON.parse` if the editor handed you a string, then validate
|
|
48
|
+
however you like (Zod, Ajv, hand-rolled). Do not assume you received an object.
|
|
49
|
+
|
|
50
|
+
There is intentionally no `JSON_PROP_SCHEMA_MISMATCH` diagnostic. If shape drift
|
|
51
|
+
becomes a recurring failure mode, wiring one up would be additive.
|
|
52
|
+
|
|
53
|
+
### `meta.constraints.jsonSchema` — an editor hint, never a gate
|
|
54
|
+
|
|
55
|
+
Optional, two forms:
|
|
56
|
+
|
|
57
|
+
| Form | Meaning |
|
|
58
|
+
|---|---|
|
|
59
|
+
| `{ ref: "<host-resolved-id>" }` | Points into a shared JSON-schema registry the host owns. Use when several templates share a payload shape. |
|
|
60
|
+
| inline `Record<string, unknown>` | A JSON-Schema-ish object describing the payload. |
|
|
61
|
+
|
|
62
|
+
Editors that understand the schema may render a structured form; those that don't
|
|
63
|
+
fall back to a plain code editor. **The engine never validates this field.**
|
|
64
|
+
|
|
65
|
+
### ⚠️ `as never` is NOT required — and existing casts are stale
|
|
66
|
+
|
|
67
|
+
Several shipped templates write `type: "json" as never`
|
|
68
|
+
(`charts/bar-graph/v1` + `v2`, `charts/line-chart/v1`, `internal/bars-stack`). **The
|
|
69
|
+
cast is unnecessary.** Those are leftovers from before `"json"` joined the union.
|
|
70
|
+
|
|
71
|
+
`definePropsSchema<P>` is typed as `Record<keyof P, MosaicTemplatePropDefinition>` —
|
|
72
|
+
it constrains only the *key set*, never the relationship between a prop's TS type and
|
|
73
|
+
its declared `type`. So this compiles clean, no casts:
|
|
74
|
+
|
|
75
|
+
```ts
|
|
76
|
+
type P = { barColor?: string | string[]; spec: Record<string, unknown> };
|
|
77
|
+
|
|
78
|
+
const propsSchema = definePropsSchema<P>({
|
|
79
|
+
barColor: { type: "json", required: false },
|
|
80
|
+
spec: { type: "json", required: true },
|
|
81
|
+
});
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
(Verified 2026-07-26 with the repo's own `tsc -p https://github.com/m0saic-project/m0saic-packages/blob/main/packages/templates/tsconfig.json.)
|
|
85
|
+
`brand/qr-stamp/video/v2` (`luminanceBuckets`) is the correct, cast-free reference.
|
|
86
|
+
|
|
87
|
+
**Do not copy the `as never` pattern into new templates**, and drop it opportunistically
|
|
88
|
+
when you touch one of the offenders. The same goes for the neighbouring
|
|
89
|
+
`type: "group" as any` casts — check whether they're still load-bearing before
|
|
90
|
+
propagating them.
|
|
91
|
+
|
|
92
|
+
---
|
|
93
|
+
|
|
94
|
+
## 2. The 5-generic `defineMosaicTemplate`
|
|
95
|
+
|
|
96
|
+
`MosaicTemplate` has carried 5 generics for a while; the helper now matches:
|
|
97
|
+
|
|
98
|
+
```ts
|
|
99
|
+
defineMosaicTemplate<
|
|
100
|
+
P extends MosaicTemplateProps,
|
|
101
|
+
O extends MosaicTemplateOutputs = MosaicTemplateOutputs,
|
|
102
|
+
U extends MosaicTemplateUpstreamVariables = MosaicTemplateUpstreamVariables,
|
|
103
|
+
D extends MosaicTemplateUpstreamData = MosaicTemplateUpstreamData,
|
|
104
|
+
S extends MosaicTemplateSidecars = MosaicTemplateSidecars,
|
|
105
|
+
>(template: MosaicTemplate<P, O, U, D, S>): MosaicTemplate<P, O, U, D, S>
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
Defaults preserve full back-compat — every existing 1-generic call site still
|
|
109
|
+
compiles unchanged.
|
|
110
|
+
|
|
111
|
+
**Use the full form when** your template (1) publishes typed outputs via
|
|
112
|
+
`outputsSchema`, (2) publishes typed sidecars via `sidecarsSchema`, or (3) reads
|
|
113
|
+
`ctx.upstreamData.<alias>` and wants type-checked access. Otherwise stay on
|
|
114
|
+
`defineMosaicTemplate<P>` — it remains the recommendation for ordinary renderables.
|
|
115
|
+
|
|
116
|
+
### Gotcha — don't narrow `U` / `D` to an empty shape
|
|
117
|
+
|
|
118
|
+
A fetcher that reads no upstream is tempted to tighten `U` / `D` to
|
|
119
|
+
`Record<string, never>`. **Don't.** `registerTemplate` accepts only the loose form
|
|
120
|
+
(`Record<string, unknown>`), and narrowing reverses contravariance — TS rejects the
|
|
121
|
+
registration.
|
|
122
|
+
|
|
123
|
+
Pass the loose defaults explicitly for `U` and `D`; tighten only `O` and `S`.
|
|
124
|
+
Consumer templates that *do* read upstream are the right place to tighten `U` / `D`,
|
|
125
|
+
because there the context is an **input** to the bound, not an output.
|
|
126
|
+
|
|
127
|
+
```ts
|
|
128
|
+
export const RepoFetcher = defineMosaicTemplate<
|
|
129
|
+
Props,
|
|
130
|
+
Outputs,
|
|
131
|
+
MosaicTemplateUpstreamVariables, // loose — narrowing breaks registerTemplate
|
|
132
|
+
MosaicTemplateUpstreamData, // (contravariance)
|
|
133
|
+
Sidecars
|
|
134
|
+
>({
|
|
135
|
+
id: asTemplateId("@example/github/repo-fetcher/v1"),
|
|
136
|
+
role: "data-fetcher",
|
|
137
|
+
capabilities: { tier: "capability", caps: { net: { fetch: true } } },
|
|
138
|
+
propsSchema: {
|
|
139
|
+
apiUrl: { type: "string", required: true },
|
|
140
|
+
tokenRef: { type: "string", required: false },
|
|
141
|
+
filter: {
|
|
142
|
+
type: "json",
|
|
143
|
+
required: false,
|
|
144
|
+
meta: { constraints: { jsonSchema: { ref: "@example/repo-filter/v1" } } },
|
|
145
|
+
},
|
|
146
|
+
},
|
|
147
|
+
defaultProps: { apiUrl: "https://api.github.com" },
|
|
148
|
+
async render(props, ctx) { /* … */ },
|
|
149
|
+
});
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
---
|
|
153
|
+
|
|
154
|
+
## 3. Cross-references
|
|
155
|
+
|
|
156
|
+
- **Data-fetcher pattern** — `../data-pipeline.md`. The five-generic
|
|
157
|
+
`defineMosaicTemplate<Props, Outputs, …, Sidecars>` signature its fetcher contract
|
|
158
|
+
relies on was aspirational when first documented; it is valid TypeScript today.
|
|
159
|
+
- **Multi-range clip picker** — `picker: "time-ranges"` is declared on a **single**
|
|
160
|
+
`type: "json"` prop whose value is `Array<MosaicTimeRangeMs>`
|
|
161
|
+
(`{ startMs, endMs, label? }`, integer ms, source-relative, sorted ascending;
|
|
162
|
+
overlaps allowed, empty array = no selection). Unlike the paired `"time-range"`
|
|
163
|
+
picker there is no prop pairing — the editor reads and writes the whole array
|
|
164
|
+
through that one prop in a single write. Boundary reader:
|
|
165
|
+
`parseTimeRangesValue` in `template-utils/src/media/timeRanges.ts` — use it rather
|
|
166
|
+
than hand-parsing, precisely because the value may arrive as object *or* string.
|
|
167
|
+
- **Prop-shape consistency** — `npm run audit:props` in https://github.com/m0saic-project/m0saic-packages/blob/main/packages/templates emits
|
|
168
|
+
`PROPS-SHAPE.md`, a cross-template prop-shape matrix. Run it after adding a
|
|
169
|
+
`"json"` prop to see how the shelf compares.
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
# MosaicColor
|
|
2
|
+
|
|
3
|
+
> **Source of truth:** [https://github.com/m0saic-project/m0saic-packages/blob/main/packages/types/src/colors/mosaicColor.ts](https://github.com/m0saic-project/m0saic-packages/blob/main/packages/types/src/colors/mosaicColor.ts) (the type), https://github.com/m0saic-project/m0saic-packages/blob/main/packages/types/src/colors/parseMosaicColors.ts (the validators), https://github.com/m0saic-project/m0saic-packages/blob/main/packages/types/src/colors/ffmpegNamedColors.ts (the named-color set).
|
|
4
|
+
|
|
5
|
+
## The type
|
|
6
|
+
|
|
7
|
+
Verbatim from `mosaicColor.ts:25`:
|
|
8
|
+
|
|
9
|
+
```ts
|
|
10
|
+
export type MosaicColor =
|
|
11
|
+
| "none"
|
|
12
|
+
| FfmpegNamedColor
|
|
13
|
+
| HexColor
|
|
14
|
+
| `${FfmpegNamedColor}${AlphaSuffix}`
|
|
15
|
+
| `${HexColor}${AlphaSuffix}`
|
|
16
|
+
| `none${AlphaSuffix}`;
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
Accepted: ffmpeg named colors (`"white"`, `"darkgray"`), hex (`"#fff"`,
|
|
20
|
+
`"#ffffff"`, `"#ff00ffaa"`), optional alpha suffix (`"white@0.5"`), and
|
|
21
|
+
`"none"` for transparency. `HexColor` is just `` `#${string}` `` — the type is
|
|
22
|
+
a **shape contract, not a full validator** (no hex well-formedness, alpha
|
|
23
|
+
range, or casing checks). Runtime validation is required on production paths.
|
|
24
|
+
|
|
25
|
+
## Validate with the real exports — never hand-roll
|
|
26
|
+
|
|
27
|
+
`@m0saic/types` ships the canonical pair; use them instead of local regexes
|
|
28
|
+
(a naive "has an `@` suffix" check accepts `"garbage@0.5"` — the real one
|
|
29
|
+
re-validates the base):
|
|
30
|
+
|
|
31
|
+
- **`toMosaicColor(input, ctx?)`** (`parseMosaicColors.ts:16`) — throws on
|
|
32
|
+
invalid; returns a normalized (lowercased) `MosaicColor`. Splits any `@alpha`
|
|
33
|
+
suffix, requires alpha finite and in `[0, 1]` (`:39-41`), then validates the
|
|
34
|
+
**base** as `"none"`, a named color, or well-formed hex. `ctx` names the error.
|
|
35
|
+
- **`isMosaicColor(input)`** (`parseMosaicColors.ts:75`) — non-throwing type guard.
|
|
36
|
+
- **`FFMPEG_NAMED_COLORS_SET`** (`ffmpegNamedColors.ts:157`) — the membership
|
|
37
|
+
set behind named-color validation.
|
|
38
|
+
|
|
39
|
+
Real caller: https://github.com/m0saic-project/m0saic-packages/blob/main/packages/templates/src/m0saic/charts/bar-graph/v1/internal/plot-area.ts#L30
|
|
40
|
+
imports it and uses it as `toMosaicColor("#ffffff", "PlotArea.BASELINE_COLOR")` (:83).
|
|
41
|
+
|
|
42
|
+
Where to validate: primitives validate incoming color props before passing them
|
|
43
|
+
to sources; heroes normalize early; template-utils must never emit invalid
|
|
44
|
+
`MosaicColor`. Never widen a field to `string` to silence the compiler.
|
|
45
|
+
|
|
46
|
+
## `"none"` and alpha
|
|
47
|
+
|
|
48
|
+
- `"none"` means transparent. The engine mapping to an ffmpeg color string
|
|
49
|
+
lives in the render engine source (not published) (`ffmpegColor()`) —
|
|
50
|
+
templates use `"none"` as intent, never hand-encoding the ffmpeg form.
|
|
51
|
+
- Prefer `visual.opacity` for compositing; use an alpha suffix only when
|
|
52
|
+
transparency is part of the color's identity — never encode opacity in both.
|
|
53
|
+
|
|
54
|
+
## Ternary widening (common pitfall)
|
|
55
|
+
|
|
56
|
+
When assigning a `MosaicColor` via a conditional, TypeScript widens the literal
|
|
57
|
+
branches to `string` and the assignment fails:
|
|
58
|
+
|
|
59
|
+
```ts
|
|
60
|
+
const style = {
|
|
61
|
+
borderColor: isDark ? "#fff" : "#000", // ❌ widens to string — type error
|
|
62
|
+
};
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
✅ Use `as const` on the literals:
|
|
66
|
+
|
|
67
|
+
```ts
|
|
68
|
+
const style = {
|
|
69
|
+
borderColor: (isDark ? ("#fff" as const) : ("#000" as const)),
|
|
70
|
+
};
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
✅ Or extract a shared style record so each value is a known literal:
|
|
74
|
+
|
|
75
|
+
```ts
|
|
76
|
+
const COLORS = { light: "#fff", dark: "#000" } as const;
|
|
77
|
+
const style = { borderColor: COLORS[isDark ? "dark" : "light"] };
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
The same trap fires on every `MosaicColor`-typed field reached via a ternary
|
|
81
|
+
(`borderColor`, `fillColor`, …). Fix at the literal site, never by widening.
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
# MosaicPlacementProps
|
|
2
|
+
|
|
3
|
+
> **Source of truth:** [https://github.com/m0saic-project/m0saic-packages/blob/main/packages/types/src/source/source.ts#L99-188`](https://github.com/m0saic-project/m0saic-packages/blob/main/packages/types/src/source/source.ts); runtime validation in https://github.com/m0saic-project/m0saic-packages/blob/main/packages/platform/src/mosaic/validate/validateSources.ts; cover-crop math in the render engine source (not published).
|
|
4
|
+
|
|
5
|
+
## The union
|
|
6
|
+
|
|
7
|
+
`MosaicPlacementProps = MosaicPlacementContain | MosaicPlacementCover`
|
|
8
|
+
(`source.ts:188`) — a discriminated union on `fit`. Fields that are meaningless
|
|
9
|
+
on one fit are declared `?: never` on that variant, so misuse is a **compile
|
|
10
|
+
error**, not a silent no-op:
|
|
11
|
+
|
|
12
|
+
| Field | Contain (`fit?: "contain"`, default) | Cover (`fit: "cover"`) |
|
|
13
|
+
|---|---|---|
|
|
14
|
+
| `inset` | ✅ (`source.ts:128`, on the shared base) | ✅ |
|
|
15
|
+
| `padding` | ✅ (`source.ts:152`) | ❌ `padding?: never` (:166) |
|
|
16
|
+
| `hAlign` / `vAlign` | ✅ (:133-134) | ❌ `hAlign?: never` / `vAlign?: never` (:162-163) |
|
|
17
|
+
| `focusX` / `focusY` | ❌ `focusX?: never` / `focusY?: never` (:155-156) | ✅ (:177, :185) |
|
|
18
|
+
|
|
19
|
+
Defaults when omitted: `fit="contain"`, `hAlign="center"`, `vAlign="middle"`,
|
|
20
|
+
no `inset`, no `padding`; on cover, focus defaults to centered (`0.5`).
|
|
21
|
+
|
|
22
|
+
## Boxes: tile rect → inset → padding → content box
|
|
23
|
+
|
|
24
|
+
- **Tile rect**: the rectangle the layout engine allocates to the source.
|
|
25
|
+
- **`inset?: MosaicBoxFrac`** (both fits): shrinks the destination rect **before**
|
|
26
|
+
fit math. Role: the **slot-level gutter**, typically owned by the composing
|
|
27
|
+
layer. Fractions of **tile size**.
|
|
28
|
+
- **`padding?: MosaicBoxFrac`** (contain-only): extra dead space reserved
|
|
29
|
+
**inside** the inset box, before contain math. Role: content-level breathing
|
|
30
|
+
room, composing with (not fighting) the slot's `inset`. Fractions of the
|
|
31
|
+
**inset box** (= of tile size when `inset` is unset; the engine resolves
|
|
32
|
+
`padLeft = insetBox.width * padding.left`). Cover forbids it — "cover +
|
|
33
|
+
padding would create intentional dead space" — enforced by the type AND the
|
|
34
|
+
`*_COVER_PADDING` validator (`validateSources.ts:561`).
|
|
35
|
+
- **`MosaicBoxFrac`** (`source.ts:85-97`): `0.02` (all sides) | `{x, y}`
|
|
36
|
+
(symmetric per axis) | `{top, right, bottom, left}` (per-side overrides win).
|
|
37
|
+
All fractions 0..1; validators require each side in `[0, 0.49]`.
|
|
38
|
+
- **Content box**: the tile rect after `inset` (and, for contain, `padding`).
|
|
39
|
+
|
|
40
|
+
⚠️ **Both are applied BEFORE the per-tile effects chain.** Quoting the `inset`
|
|
41
|
+
JSDoc (`source.ts:117-121`): "Applied before the per-tile effects chain — the
|
|
42
|
+
content's buffer IS the shrunk box, so effects that need headroom
|
|
43
|
+
(`effects.rotate`) clip against it. Rotation headroom must be real geometry
|
|
44
|
+
instead: a nested child whose declared `size` equals the padded box (see
|
|
45
|
+
watermark page-mode `buildPaddedInstanceChild`)."
|
|
46
|
+
|
|
47
|
+
## Contain
|
|
48
|
+
|
|
49
|
+
Uniform scale, aspect preserved, so the media fits entirely inside the content
|
|
50
|
+
box. Leftover per axis: `leftoverX = contentW - scaledW`, `leftoverY = contentH - scaledH`.
|
|
51
|
+
|
|
52
|
+
Alignment maps to factors — `left/top = 0`, `center/middle = 0.5` (default),
|
|
53
|
+
`right/bottom = 1` — and placement is:
|
|
54
|
+
|
|
55
|
+
```
|
|
56
|
+
x = contentX + (leftoverX > 0 ? leftoverX * hFactor(hAlign) : 0)
|
|
57
|
+
y = contentY + (leftoverY > 0 ? leftoverY * vFactor(vAlign) : 0)
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
**Exact-fit invariant (still observable, and required):** on an axis with zero
|
|
61
|
+
leftover, varying the alignment MUST NOT change output — byte-identical frames.
|
|
62
|
+
This is the surviving observability check now that cover-side misuse is
|
|
63
|
+
unrepresentable.
|
|
64
|
+
|
|
65
|
+
## Cover
|
|
66
|
+
|
|
67
|
+
Uniform scale, aspect preserved, so the media fully covers the content box;
|
|
68
|
+
overflow is cropped to exactly `contentW × contentH`; placed at
|
|
69
|
+
`(contentX, contentY)`. No leftover space, so alignment is unrepresentable.
|
|
70
|
+
|
|
71
|
+
### Crop anchor: `focusX` / `focusY`
|
|
72
|
+
|
|
73
|
+
```ts
|
|
74
|
+
placement: { fit: "cover", focusX?: number, focusY?: number } // 0..1 each
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
**Offset per axis = `(scaled - content) * focus`.** `0` keeps the leading edge
|
|
78
|
+
(left/top), `0.5` centers (default), `1` keeps the trailing edge. Motivating
|
|
79
|
+
case: portrait photos put faces near the top — `focusY: 0` keeps the head.
|
|
80
|
+
|
|
81
|
+
- **Byte-identity guarantee.** Focus absent or exactly `0.5` emits the exact
|
|
82
|
+
legacy `(in_w-W)/2` string — helper `coverCropOffsetExpr`
|
|
83
|
+
(the render engine source (not published)). Existing goldens do not move.
|
|
84
|
+
- **Enforced twice.** Type level: `focusX?: never` on contain. Runtime:
|
|
85
|
+
`*_FOCUS_REQUIRES_COVER` (https://github.com/m0saic-project/m0saic-packages/blob/main/packages/platform/src/mosaic/validate/validateSources.ts#L575)
|
|
86
|
+
plus range checks `*_INVALID_FOCUSX` / `*_INVALID_FOCUSY` in `[0, 1]`
|
|
87
|
+
(`validateFocus`, `validateSources.ts:502`). Note these live in **platform**,
|
|
88
|
+
not core.
|
|
89
|
+
- **Also steered by focus:** `zoomInPercent`'s zoom crop on cover tiles
|
|
90
|
+
(contain + zoom stays centered), and `doc.backgroundImage`'s cover fill.
|
|
91
|
+
- **Not `effects.camera`.** Same focus vocabulary, different layer: the camera
|
|
92
|
+
is the animatable zoom/pan downstream on the already-cropped content box;
|
|
93
|
+
placement focus is the static, plan-time fit-crop anchor. They compose.
|
|
94
|
+
- **Web parity:** `GeometricPreview` maps cover focus to CSS
|
|
95
|
+
`object-position: ${fx*100}% ${fy*100}%` — identical percentage math.
|
|
96
|
+
|
|
97
|
+
## Historical note
|
|
98
|
+
|
|
99
|
+
Pre-v2 versions of this spec carried "for `fit="cover"`, `hAlign`/`vAlign`
|
|
100
|
+
MUST NOT affect output" invariants. The union now declares those fields `never`
|
|
101
|
+
on cover (verified against `source.ts` 2026-07-27), so the misuse is
|
|
102
|
+
unrepresentable and those invariants are moot — only the contain exact-fit
|
|
103
|
+
invariant above remains testable.
|
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
# Prop bindings — the rect that SHOWS a prop is its handle in Make
|
|
2
|
+
|
|
3
|
+
A template marks the source that DISPLAYS a prop with `source.editor.binding`
|
|
4
|
+
(or `bindings`, several leaves on one rect). Make resolves them per render and
|
|
5
|
+
turns the rect into the prop's canvas handle: double-click / a badge to edit,
|
|
6
|
+
a file dropped on it to fill a media slot. Bindings are re-resolved on every
|
|
7
|
+
render and keyed by the PROP — never by a stableKey, which changes whenever the
|
|
8
|
+
geometry does.
|
|
9
|
+
|
|
10
|
+
One predicate decides what is bindable: `classifyBindableProp` in
|
|
11
|
+
`@m0saic/platform` (`mosaic/propBindings`). Template tests
|
|
12
|
+
(`resolvePropBindings` in `@m0saic/template-utils`) and the Make page both use
|
|
13
|
+
it, so a template can never offer an edit the page refuses, or vice versa.
|
|
14
|
+
`bindProps` / `bindPropPath` THROW at author time for what the platform would
|
|
15
|
+
silently drop — a template test should never reach the silent path.
|
|
16
|
+
|
|
17
|
+
## The wire shape
|
|
18
|
+
|
|
19
|
+
```ts
|
|
20
|
+
binding: {
|
|
21
|
+
propKey: string; // dotted path as propsSchema nests it: "label", "titles.title"
|
|
22
|
+
index?: number; // ONE element of string[] / number[] / media[]
|
|
23
|
+
path?: Array<string | number>; // route into a json / list / array value: [2, "title"]
|
|
24
|
+
kind?: "string" | "number" | "color" | "rect" | "media"; // required on structured leaves
|
|
25
|
+
layer?: number; // which text layer shows the leaf (multi-layer text)
|
|
26
|
+
onClear?: "remove-element" | "unset-leaf";
|
|
27
|
+
seedDraft?: string;
|
|
28
|
+
companion?: boolean;
|
|
29
|
+
range?: { start: number; end: number }; // one line of a multi-line string leaf
|
|
30
|
+
focus?: { start: number; end: number }; // the token the rect shows, inside range
|
|
31
|
+
}
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
Rules:
|
|
35
|
+
|
|
36
|
+
- `propKey` is exactly as `propsSchema` nests it; every segment past the
|
|
37
|
+
first walks a `type: "group"` prop's `fields`.
|
|
38
|
+
- Basic lists (`string[]`, `number[]`, `media[]`) bind ONE element — a
|
|
39
|
+
non-negative integer `index`. The list itself is never bindable.
|
|
40
|
+
- Structured props (`json` / `list` / `array`) bind a LEAF: `path` from the
|
|
41
|
+
prop's value plus an explicit `kind` (the schema has no per-leaf type, and an
|
|
42
|
+
empty leaf must still be an "add" handle). `index` is shorthand for a leading
|
|
43
|
+
numeric segment.
|
|
44
|
+
- `meta.ui.hidden` props, booleans, enum / connection-option strings (closed
|
|
45
|
+
pickers) and whole lists are never bindable. A stray `index` on a scalar is
|
|
46
|
+
ignored.
|
|
47
|
+
- Bind even when the value is empty — the rect is then a handle to ADD.
|
|
48
|
+
|
|
49
|
+
## Kinds
|
|
50
|
+
|
|
51
|
+
| prop / leaf | binding | kind | Make handle |
|
|
52
|
+
|---|---|---|---|
|
|
53
|
+
| free-text `string` (or one `string[]` element, or a structured leaf) | `bindProp(src, key[, i])` / `bindPropPath(src, key, path, "string")` | `"string"` | T — the inline text form |
|
|
54
|
+
| colour-valued string (`isColor` / `colorPicker`) | same | `"color"` | swatch — a picker |
|
|
55
|
+
| `number` (or one `number[]` element, or a leaf) | same, `"number"` | `"number"` | 123 — the inline form |
|
|
56
|
+
| `json` with `picker: "regions"`, `regions.max: 1` | `bindPropRect(src, key)` — no path | `"rect"` | move glyph — the regions draw session on the cell |
|
|
57
|
+
| `media`, one `media[]` element, or a leaf holding a path | `bindProp(src, key[, i])` / `bindPropPath(…, "media")` | `"media"` | picture glyph — a DROP target + the media chip |
|
|
58
|
+
|
|
59
|
+
Order the entries the way a double-click should read them: `fields[0]` is the
|
|
60
|
+
primary action (a plain double-click); the badge row offers every kind.
|
|
61
|
+
|
|
62
|
+
## Authoring helpers (`@m0saic/template-utils`)
|
|
63
|
+
|
|
64
|
+
- `bindProp(src, propKey, index?)` — the basic case.
|
|
65
|
+
- `bindProps(src, entries)` — several leaves on one rect (a multi-layer text,
|
|
66
|
+
a cell that moves AND edits AND takes a drop). Writes `editor.bindings`,
|
|
67
|
+
which wins over the single `binding`.
|
|
68
|
+
- `bindPropPath(src, propKey, path, kind, { onClear?, seedDraft? })` — one
|
|
69
|
+
structured leaf.
|
|
70
|
+
- `bindPropRect(src, propKey)` — a one-region regions picker edited in place.
|
|
71
|
+
- `bindPropRange(src, propKey, index, range, focus?)` — one line of a
|
|
72
|
+
multi-line string prop (a code state, a lyric block); offsets index the RAW
|
|
73
|
+
prop string.
|
|
74
|
+
- `stripPropBindings(doc)` — a host composing ANOTHER template's render drops
|
|
75
|
+
its bindings (they name the inner template's props).
|
|
76
|
+
- `tag(src, label)` composes either way round with all of them.
|
|
77
|
+
|
|
78
|
+
## `onClear` — what an EMPTY commit means
|
|
79
|
+
|
|
80
|
+
```ts
|
|
81
|
+
{ propKey: "days", path: [rowIndex, "day"], kind: "number", onClear: "remove-element" }
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
- `"remove-element"` — splice the element at the leading numeric path segment
|
|
85
|
+
(the whole row, or one element of a basic list) out of its array. Needs an
|
|
86
|
+
`index` or a numeric first `path` segment.
|
|
87
|
+
- `"unset-leaf"` — delete the leaf key from its object, or unset a basic prop
|
|
88
|
+
so its default shows again. Not for array-element leaves (no key).
|
|
89
|
+
- Absent — numbers and colours refuse an empty commit; strings write `""`;
|
|
90
|
+
a scalar media clears to `""`; a `media[]` element refuses (a hole in a media
|
|
91
|
+
list is not a value the engine can open).
|
|
92
|
+
|
|
93
|
+
Editor rules: "empty" is blank for numbers / colours, `""` for text. An empty
|
|
94
|
+
commit against an already-empty leaf writes nothing. In a multi-field session a
|
|
95
|
+
row removal supersedes its siblings' writes; rows go highest index first. The
|
|
96
|
+
panel stays the primary place rows are deleted — canvas clear is an accelerator.
|
|
97
|
+
|
|
98
|
+
## `seedDraft` — prefill an EMPTY leaf from the rect's context
|
|
99
|
+
|
|
100
|
+
```ts
|
|
101
|
+
{ propKey: "days", path: [len, "day"], kind: "number", onClear: "remove-element", seedDraft: String(info.day) }
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
- Applies only when the bound leaf reads EMPTY / absent; a leaf with a value
|
|
105
|
+
ignores it. A DRAFT: shown pre-selected (Enter alone accepts it), written on
|
|
106
|
+
commit like typed text, never applied by itself. Deleting it and committing
|
|
107
|
+
empty still runs `onClear`.
|
|
108
|
+
- Blur commits too, so an untouched seeded form that loses focus WRITES the
|
|
109
|
+
seed — mind this when seeding.
|
|
110
|
+
- Wire shape is a string; a blank seed, or a non-numeric seed on a number
|
|
111
|
+
leaf, throws at author time; `rect` and `media` leaves take no seed.
|
|
112
|
+
- Focus lands on the FIRST field: order a seeded field first when the seed is
|
|
113
|
+
the whole suggestion.
|
|
114
|
+
|
|
115
|
+
## `kind: "media"` — a rect that takes a dropped file
|
|
116
|
+
|
|
117
|
+
A media-bound rect is a DROP target on the Make canvas (a file dragged onto it
|
|
118
|
+
writes the path) with a picture-glyph handle that opens the media chip —
|
|
119
|
+
`Choose file…` / `Replace…` · `Remove` (when clearable) · "or drop a file on
|
|
120
|
+
the cell". A file in flight lights every media slot; a slot whose prop refuses
|
|
121
|
+
the kind lights muted.
|
|
122
|
+
|
|
123
|
+
- What may land is the PROP's say: `meta.control.accept` (`["image","video"]`)
|
|
124
|
+
and `meta.control.extensions`. Judged by MIME while in flight, by extension
|
|
125
|
+
at drop.
|
|
126
|
+
- `media[]`: index `< length` replaces, `=== length` appends (an add handle),
|
|
127
|
+
`> length` is refused. A media list is never padded with `""`.
|
|
128
|
+
- A tile pairs handles freely — the drop-calendar's facecam is
|
|
129
|
+
`bindProps(src, [{ propKey: "facecamRegion", kind: "rect" }, { propKey: "facecam" }])`:
|
|
130
|
+
move it, or drop on it. Put the MEDIA entry first when a double-click should
|
|
131
|
+
mean "replace the picture".
|
|
132
|
+
|
|
133
|
+
### A drop commits the tile's seeded companions
|
|
134
|
+
|
|
135
|
+
When a file lands, every EMPTY sibling leaf on the tile that carries a
|
|
136
|
+
`seedDraft` is written with its seed — the rect's meaning travels with the
|
|
137
|
+
gesture (the clicked date; the slot number). Leaves with a value, or empty
|
|
138
|
+
leaves without a seed, are untouched.
|
|
139
|
+
|
|
140
|
+
`companion: true` marks a seeded leaf ONLY the drop fills:
|
|
141
|
+
|
|
142
|
+
```ts
|
|
143
|
+
{ propKey: "days", path: [row, "teaser"], kind: "number", seedDraft: String(teasers.length + 1), companion: true }
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
It never shows in the text form (a blur-commit there would write the seed on
|
|
147
|
+
an unrelated edit) and never earns a badge. Needs a kept `seedDraft`; refused
|
|
148
|
+
on media / rect leaves. The drop-calendar date cell, in full — one drop births
|
|
149
|
+
a pictured row:
|
|
150
|
+
|
|
151
|
+
```ts
|
|
152
|
+
bindProps(cell, [
|
|
153
|
+
{ propKey: "days", path: [n, "day"], kind: "number", onClear: "remove-element", seedDraft: String(day) },
|
|
154
|
+
{ propKey: "days", path: [n, "title"], kind: "string" },
|
|
155
|
+
{ propKey: "teasers", index: teasers.length, kind: "media" },
|
|
156
|
+
{ propKey: "days", path: [n, "teaser"], kind: "number", seedDraft: String(teasers.length + 1), companion: true },
|
|
157
|
+
])
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
## Starter media — the slot before the footage
|
|
161
|
+
|
|
162
|
+
A template that wants the slot present (and droppable) before the user has a
|
|
163
|
+
file renders a stand-in while the prop is empty. Two sources:
|
|
164
|
+
|
|
165
|
+
1. **Bring your own** — media bundled next to the template
|
|
166
|
+
(`assets/*.png|mp4|svg`, mirrored by copy-assets), built with
|
|
167
|
+
`fileAsset(resolve(__dirname, "assets"), name, kind)` from
|
|
168
|
+
`@m0saic/template-utils/dist/m0saic/assetPath` (asar-safe).
|
|
169
|
+
2. **Included defaults** — `starterMedia(role)` from
|
|
170
|
+
`@m0saic/template-utils/dist/media/defaultMedia` (node-only deep import):
|
|
171
|
+
roles `portrait` `landscape` `square` `facecam` `avatar` `logo`, one small
|
|
172
|
+
viewBox-only SVG each (`assets/starter/`, minted by
|
|
173
|
+
`tools/mint-starter-svg.mjs`). Returns `null` when the file is not on disk —
|
|
174
|
+
degrade to the template's own stand-in, never an error mosaic.
|
|
175
|
+
|
|
176
|
+
An `.svg` file asset is rasterized to a PNG at plan time by the engine, and
|
|
177
|
+
Make's preview still-extractor rasterizes it the same way before ffmpeg sees
|
|
178
|
+
it — ffmpeg itself has no svg decoder, so never hand an svg path to it directly.
|
|
179
|
+
A still stand-in in a video slot keeps a no-footage render a poster (PNG);
|
|
180
|
+
motion arrives with the real clip.
|
|
181
|
+
|
|
182
|
+
Make marks an EXISTING slot with no path (a starter showing) with a persistent
|
|
183
|
+
dashed outline and an "Add …" glyph. An add handle (`teasers[len]`, an unborn
|
|
184
|
+
row) is not a slot and stays quiet — otherwise every empty date cell would light.
|
|
185
|
+
|
|
186
|
+
## What Make does with them (the host side, for orientation)
|
|
187
|
+
|
|
188
|
+
- Badge row on hover: one glyph per kind (T · 123 · swatch · move · picture),
|
|
189
|
+
collapsing to one dot on a tile under ~60 on-screen px. A click on a glyph is
|
|
190
|
+
a double-click WITH that intent.
|
|
191
|
+
- Text / number / colour → the inline form over the rect (Enter / blur commit,
|
|
192
|
+
Esc cancels, an unchanged draft writes nothing). Rect → the regions draw
|
|
193
|
+
session seeded from the painted cell. Media → the chip; a drop needs no click.
|
|
194
|
+
- Every write goes through the ONE prop writer, so it survives every other
|
|
195
|
+
edit and re-renders the template; the panel field updates because it IS the
|
|
196
|
+
prop.
|
|
197
|
+
- The double-clicked tile is often chrome stacked over the bound rect (gridlines,
|
|
198
|
+
glass) — Make looks THROUGH the stack under the pointer for the topmost bound
|
|
199
|
+
tile, for clicks and drops alike.
|
|
200
|
+
|
|
201
|
+
First implementers: `@m0saic-dev/creator/drop-calendar/v1` (every kind, the
|
|
202
|
+
companion, the facecam slot); `@m0saic/alpine/*` (text / number / colour);
|
|
203
|
+
`@m0saic-dev/music/lyric-video/v1` (ranges).
|