partforge 0.103.0 → 0.105.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/docs/AUTHORING-PARTS.md
CHANGED
|
@@ -2057,8 +2057,17 @@ see "Wiring a part into a runnable app", above, for its full signature):
|
|
|
2057
2057
|
**The `allow` list, and what bypasses it.** All three controls take the same
|
|
2058
2058
|
`allow` list already documented for `"font"` in the control-types table above
|
|
2059
2059
|
(`"https"` — the default; `"gstatic"`, font-only, `https://fonts.gstatic.com`
|
|
2060
|
-
exactly; `"asset"`, a `pfc-asset://` token the host has stored for this part
|
|
2061
|
-
|
|
2060
|
+
exactly; `"asset"`, a `pfc-asset://` token the host has stored for this part;
|
|
2061
|
+
`"tree"`, vector-only, a `pfc-tree://` token naming artwork that lives as a
|
|
2062
|
+
**file inside the part itself** rather than in host storage), gating what a
|
|
2063
|
+
**param-supplied** value may be.
|
|
2064
|
+
|
|
2065
|
+
`"tree"` is vector-only by construction, not by omission: a part's files are
|
|
2066
|
+
text, so a `partforge-vector` JSON document can live in one and a PNG or a
|
|
2067
|
+
font cannot. Its payoff is that the artwork is versioned *with* the part —
|
|
2068
|
+
a host that stores parts as a file tree keeps the document in the same
|
|
2069
|
+
snapshot as the code that reads it, so publishing, history and undo carry the
|
|
2070
|
+
two together instead of leaving a param pointing at storage that moved on. It never restricts a source an
|
|
2062
2071
|
author writes into `images`/`fonts`/`vectors` themselves — that's code, not
|
|
2063
2072
|
user input.
|
|
2064
2073
|
|
|
@@ -2338,14 +2347,17 @@ yourself (e.g. to download from a different origin) instead of partforge's own D
|
|
|
2338
2347
|
where `ImageAsset` is `{ id, label, url, width, height, thumbUrl }`. With no
|
|
2339
2348
|
provider a `type: "image"` control degrades to a URL field.
|
|
2340
2349
|
|
|
2341
|
-
- `onAssetUpload(blob, { kind, filename }) → Promise<string>` — the drop
|
|
2342
|
-
shared by `"image"`/`"vector"`/`"font"` controls (see "Getting files
|
|
2343
|
-
part", below) calls this with the converted artifact after a drop,
|
|
2344
|
-
file-picker choice, and writes whatever it resolves to into the
|
|
2345
|
-
`blob` is the CONVERTED artifact — a PNG, a partforge-vector JSON blob,
|
|
2346
|
-
original file for a font — never the user's raw drop; `kind` is
|
|
2347
|
-
`"vector"`, or `"font"
|
|
2348
|
-
|
|
2350
|
+
- `onAssetUpload(blob, { kind, key, filename }) → Promise<string>` — the drop
|
|
2351
|
+
target shared by `"image"`/`"vector"`/`"font"` controls (see "Getting files
|
|
2352
|
+
into a part", below) calls this with the converted artifact after a drop,
|
|
2353
|
+
paste, or file-picker choice, and writes whatever it resolves to into the
|
|
2354
|
+
param. `blob` is the CONVERTED artifact — a PNG, a partforge-vector JSON blob,
|
|
2355
|
+
or the original file for a font — never the user's raw drop; `kind` is
|
|
2356
|
+
`"image"`, `"vector"`, or `"font"`; `key` is the param the drop landed on,
|
|
2357
|
+
which is what lets a host give each control a stable destination of its own
|
|
2358
|
+
rather than reconciling uploads against the part afterwards. Must resolve to
|
|
2359
|
+
a non-empty source string (an `https:` URL, or a host-defined `pfc-asset:` or
|
|
2360
|
+
`pfc-tree:` token); anything else is treated
|
|
2349
2361
|
as a contract violation and reported through the control's own error line,
|
|
2350
2362
|
not written into the param. Omit it and the converted bytes land straight in
|
|
2351
2363
|
the param instead — the path a host that cannot fetch URLs (the
|
package/package.json
CHANGED
|
@@ -237,7 +237,16 @@ export function attachAnimationControls(viewer, part, {
|
|
|
237
237
|
const nextAnimBtn = btn("pf-anim-page", "›", "Next animation");
|
|
238
238
|
bar.append(prevAnimBtn, pick, title);
|
|
239
239
|
const infoSlot = el("span", "pf-anim-info");
|
|
240
|
-
|
|
240
|
+
// Inline SVGs, not the ▶/⏸ characters: U+23F8 takes its EMOJI presentation
|
|
241
|
+
// on iOS, so the pause glyph rendered as a color sticker in an otherwise
|
|
242
|
+
// monochrome bar. Both glyphs are PERSISTENT children toggled by the
|
|
243
|
+
// data-playing attribute (app.css) — the WebKit click-loss rule below
|
|
244
|
+
// forbids replacing the button's children while a press may be down, and an
|
|
245
|
+
// attribute write never eats a click.
|
|
246
|
+
const playBtn = btn("pf-anim-play", null, "Play animation");
|
|
247
|
+
playBtn.innerHTML =
|
|
248
|
+
'<svg class="pf-anim-glyph-play" viewBox="0 0 24 24" width="14" height="14" fill="currentColor" aria-hidden="true"><path d="M7 4.5v15l13-7.5z"/></svg>'
|
|
249
|
+
+ '<svg class="pf-anim-glyph-pause" viewBox="0 0 24 24" width="14" height="14" fill="currentColor" aria-hidden="true"><path d="M7 4.5h3.4v15H7zM13.6 4.5H17v15h-3.6z"/></svg>';
|
|
241
250
|
const scrubWrap = el("span", "pf-anim-scrub-wrap");
|
|
242
251
|
const scrub = document.createElement("input");
|
|
243
252
|
scrub.type = "range";
|
|
@@ -404,10 +413,12 @@ export function attachAnimationControls(viewer, part, {
|
|
|
404
413
|
// syncUi() runs on every playback frame. Two rules keep that from eating the
|
|
405
414
|
// user's clicks, and they are independent:
|
|
406
415
|
//
|
|
407
|
-
// 1.
|
|
408
|
-
//
|
|
409
|
-
//
|
|
410
|
-
//
|
|
416
|
+
// 1. The play button's glyph flip is an ATTRIBUTE toggle over two persistent
|
|
417
|
+
// SVG children (app.css keys which one shows on [data-playing]) — its
|
|
418
|
+
// children are never touched after construction. This is the one that
|
|
419
|
+
// matters, because it holds even when the glyph legitimately changes
|
|
420
|
+
// under a held finger — playback ending mid-press. (The bubble's text
|
|
421
|
+
// still goes through textSetter, the text-node form of the same rule.)
|
|
411
422
|
// 2. Each element has its own renderer that leaves early when its own value
|
|
412
423
|
// is unchanged, so a playing transport touches only what actually moved:
|
|
413
424
|
// the scrubber's value, and aria-valuetext when it crosses a reporting
|
|
@@ -420,13 +431,12 @@ export function attachAnimationControls(viewer, part, {
|
|
|
420
431
|
// for. Measured in WebKit, any press held >= 40ms lost it (a real click is
|
|
421
432
|
// ~100ms; a synthetic 0ms one survives, which is why automated clicking never
|
|
422
433
|
// saw it). Reset was never affected: nothing rewrites that button per frame.
|
|
423
|
-
const setPlayGlyph = textSetter(playBtn);
|
|
424
434
|
let shownActive = null;
|
|
425
435
|
|
|
426
436
|
function renderPlayButton(active) {
|
|
427
437
|
if (active === shownActive) return;
|
|
428
438
|
shownActive = active;
|
|
429
|
-
|
|
439
|
+
playBtn.toggleAttribute("data-playing", active);
|
|
430
440
|
const label = active ? "Pause animation" : "Play animation";
|
|
431
441
|
playBtn.setAttribute("aria-label", label);
|
|
432
442
|
playBtn.title = label;
|
package/src/framework/app.css
CHANGED
|
@@ -737,6 +737,14 @@ button.action:focus-visible, .adv-toggle:focus-visible, .sec-title:focus-visible
|
|
|
737
737
|
cursor: pointer; font-size: 13px; padding: 2px 4px;
|
|
738
738
|
}
|
|
739
739
|
.pf-anim-bar button:hover { color: var(--pf-text-2); }
|
|
740
|
+
/* Play/pause is two PERSISTENT inline SVGs toggled by [data-playing] — never
|
|
741
|
+
the ▶/⏸ characters (U+23F8 renders as a color emoji on iOS) and never a
|
|
742
|
+
child swap (WebKit drops a click whose press started on a replaced node —
|
|
743
|
+
see animation-controls.js). */
|
|
744
|
+
.pf-anim-play { display: inline-flex; align-items: center; justify-content: center; }
|
|
745
|
+
.pf-anim-play .pf-anim-glyph-pause { display: none; }
|
|
746
|
+
.pf-anim-play[data-playing] .pf-anim-glyph-play { display: none; }
|
|
747
|
+
.pf-anim-play[data-playing] .pf-anim-glyph-pause { display: block; }
|
|
740
748
|
.pf-anim-title, .pf-anim-pick {
|
|
741
749
|
font-family: var(--pf-mono); font-size: 11px; color: var(--pf-text-2);
|
|
742
750
|
}
|
|
@@ -845,6 +853,20 @@ button.action:focus-visible, .adv-toggle:focus-visible, .sec-title:focus-visible
|
|
|
845
853
|
.pf-anim-scrub { min-height: 44px; }
|
|
846
854
|
}
|
|
847
855
|
|
|
856
|
+
/* On a phone the full transport is a two-row player squatting on a small
|
|
857
|
+
stage. Collapse it to the decisions that matter — which animation, and
|
|
858
|
+
play/pause: the timeline, reset and ⓘ stand down below the narrow
|
|
859
|
+
breakpoint. WIDTH-keyed, deliberately not the coarse-pointer condition the
|
|
860
|
+
target-size block above shares: a coarse-pointer iPad has the room and
|
|
861
|
+
keeps the full transport; only the small screen loses the player bar.
|
|
862
|
+
Playback itself still visits every chapter; scrubbing wants a real
|
|
863
|
+
screen. */
|
|
864
|
+
@media (max-width: 719px) {
|
|
865
|
+
.pf-anim-bar .pf-anim-scrub-wrap,
|
|
866
|
+
.pf-anim-bar .pf-anim-reset,
|
|
867
|
+
.pf-anim-bar .pf-anim-info { display: none; }
|
|
868
|
+
}
|
|
869
|
+
|
|
848
870
|
/* Legacy id-only markup only: classed markup's viewbar lives inside .pf-stage
|
|
849
871
|
(bottom-right, see chrome.css's .pf-float-viewbar) so it never meets the
|
|
850
872
|
top-left floating #panel card. Legacy markup still floats #viewbar top-right
|
|
@@ -96,7 +96,7 @@ function el(tag, className, text) {
|
|
|
96
96
|
// The click path is dropped rather than hidden, deliberately: an invisible
|
|
97
97
|
// overlay that still swallowed clicks would eat the button underneath it, which
|
|
98
98
|
// is the one affordance ambient mode exists to protect.
|
|
99
|
-
export function makeFileDrop({ kind, onSource, onError, onAssetUpload, ambient = false }) {
|
|
99
|
+
export function makeFileDrop({ kind, key, onSource, onError, onAssetUpload, ambient = false }) {
|
|
100
100
|
const row = rowFor(kind);
|
|
101
101
|
const wrap = el("div", ambient ? "file-drop file-drop-ambient" : "file-drop");
|
|
102
102
|
if (!ambient) {
|
|
@@ -219,7 +219,14 @@ export function makeFileDrop({ kind, onSource, onError, onAssetUpload, ambient =
|
|
|
219
219
|
|
|
220
220
|
if (onAssetUpload) {
|
|
221
221
|
try {
|
|
222
|
-
|
|
222
|
+
// `key` is the PARAM the drop landed on. A host needs it to give a
|
|
223
|
+
// control a stable destination of its own — partforge-cloud derives
|
|
224
|
+
// one file path per key, so re-dropping replaces that control's
|
|
225
|
+
// artwork in place instead of accumulating files it must later
|
|
226
|
+
// reconcile against the part. Purely informational to partforge:
|
|
227
|
+
// the hook still answers with a source string and nothing here
|
|
228
|
+
// reads the key back.
|
|
229
|
+
const source = await onAssetUpload(artifact.blob, { kind, key, filename: file.name });
|
|
223
230
|
if (stale()) return;
|
|
224
231
|
// A host hook that resolves to anything but a non-empty string —
|
|
225
232
|
// `undefined`, an object, `""` — is a contract violation, not a
|
|
@@ -329,6 +336,7 @@ export function mountDrop(kind, { params, node, onAssetUpload, onChange, onCommi
|
|
|
329
336
|
|
|
330
337
|
const drop = makeFileDrop({
|
|
331
338
|
kind,
|
|
339
|
+
key: node.key,
|
|
332
340
|
ambient,
|
|
333
341
|
onAssetUpload,
|
|
334
342
|
onSource: (source) => {
|
|
@@ -52,6 +52,14 @@ import { unwrapModule } from "./asset-resolve.js";
|
|
|
52
52
|
export const VECTOR_ALLOW_DEFAULT = ["https"];
|
|
53
53
|
|
|
54
54
|
const ASSET_SCHEME = "pfc-asset:";
|
|
55
|
+
// Artwork that lives as a FILE IN THE PART ITSELF, addressed by its path
|
|
56
|
+
// rather than by a host-side storage id. Vector-only, and the asymmetry is
|
|
57
|
+
// structural rather than an omission: a part's files are text, so a JSON
|
|
58
|
+
// document can live in one and a PNG cannot. A host that stores parts as a
|
|
59
|
+
// file tree (partforge-cloud is the motivating case) resolves this scheme
|
|
60
|
+
// against that tree; partforge itself only decides whether a param may carry
|
|
61
|
+
// it, exactly as it does for `pfc-asset:`.
|
|
62
|
+
const TREE_SCHEME = "pfc-tree:";
|
|
55
63
|
|
|
56
64
|
// The "unset" vector source. An empty value declares NO artwork for that
|
|
57
65
|
// name — mirrors isNoFontSource/isNoImageSource exactly. Never a source to
|
|
@@ -94,6 +102,7 @@ export function vectorSourceAllowed(source, allow = VECTOR_ALLOW_DEFAULT) {
|
|
|
94
102
|
// "pfc-asset://" must not pass, and neither must a lookalike host.
|
|
95
103
|
if (kind === "https" && u.protocol === "https:") return true;
|
|
96
104
|
if (kind === "asset" && u.protocol === ASSET_SCHEME) return true;
|
|
105
|
+
if (kind === "tree" && u.protocol === TREE_SCHEME) return true;
|
|
97
106
|
}
|
|
98
107
|
return false;
|
|
99
108
|
}
|