@1agh/maude 0.58.2 → 0.59.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/apps/studio/annotations-bindings.ts +83 -4
- package/apps/studio/annotations-layer.tsx +49 -15
- package/apps/studio/api.ts +6 -1
- package/apps/studio/bin/_fetch-asset.mjs +169 -5
- package/apps/studio/bin/_import-asset.mjs +90 -0
- package/apps/studio/bin/_import-figma.mjs +1775 -0
- package/apps/studio/bin/_perf-probe-safari.mjs +332 -0
- package/apps/studio/bin/_perf-probe.mjs +228 -0
- package/apps/studio/bin/_perf-shared.mjs +345 -0
- package/apps/studio/bin/_video-playwright.mjs +103 -7
- package/apps/studio/bin/import-figma.sh +47 -0
- package/apps/studio/bin/perf.sh +228 -0
- package/apps/studio/bin/read-annotations.mjs +11 -1
- package/apps/studio/bin/smoke.sh +49 -5
- package/apps/studio/bun.lock +16 -22
- package/apps/studio/canvas-edit.ts +29 -5
- package/apps/studio/canvas-lib.tsx +148 -6
- package/apps/studio/client/app.jsx +196 -38
- package/apps/studio/client/export-center.jsx +42 -4
- package/apps/studio/client/panels/CloudBar.jsx +92 -1
- package/apps/studio/client/panels/FigmaImportPanel.jsx +264 -0
- package/apps/studio/client/panels/GitPanel.jsx +26 -6
- package/apps/studio/client/panels/SettingsPanel.jsx +181 -0
- package/apps/studio/client/panels/SetupChecklist.jsx +26 -2
- package/apps/studio/client/panels/SyncPanel.jsx +229 -0
- package/apps/studio/client/panels/TimelinePanel.jsx +31 -3
- package/apps/studio/client/panels/timeline-comp-target.js +101 -0
- package/apps/studio/client/panels/timeline-parse.js +3 -3
- package/apps/studio/client/styles/3-shell-maude.css +37 -0
- package/apps/studio/client/styles/4-components.css +134 -0
- package/apps/studio/clip-ops.ts +93 -17
- package/apps/studio/cloud/endpoints.ts +78 -10
- package/apps/studio/cloud/renew.ts +183 -0
- package/apps/studio/context.ts +2 -1
- package/apps/studio/dist/client.bundle.js +1231 -1231
- package/apps/studio/dist/runtime/@remotion_media.js +56 -136
- package/apps/studio/dist/runtime/@remotion_player.js +18 -18
- package/apps/studio/dist/runtime/@remotion_transitions.js +9 -9
- package/apps/studio/dist/runtime/@remotion_transitions_clock-wipe.js +1 -1
- package/apps/studio/dist/runtime/remotion.js +12 -12
- package/apps/studio/dist/styles.css +1 -1
- package/apps/studio/exporters/_browser-bundles.ts +20 -6
- package/apps/studio/exporters/_runtime.ts +19 -0
- package/apps/studio/exporters/degraded.ts +92 -0
- package/apps/studio/exporters/index.ts +5 -0
- package/apps/studio/exporters/jobs.ts +19 -0
- package/apps/studio/exporters/unsupported-media.ts +170 -0
- package/apps/studio/exporters/video-encode-lib.ts +35 -6
- package/apps/studio/exporters/video-render-lib.ts +6 -0
- package/apps/studio/exporters/video.ts +72 -1
- package/apps/studio/figma/assets.test.ts +464 -0
- package/apps/studio/figma/assets.ts +452 -0
- package/apps/studio/figma/client.test.ts +395 -0
- package/apps/studio/figma/client.ts +513 -0
- package/apps/studio/figma/codegen-client.test.ts +276 -0
- package/apps/studio/figma/codegen-client.ts +509 -0
- package/apps/studio/figma/codegen-fonts.test.ts +103 -0
- package/apps/studio/figma/codegen-fonts.ts +195 -0
- package/apps/studio/figma/codegen-values.test.ts +179 -0
- package/apps/studio/figma/codegen-values.ts +270 -0
- package/apps/studio/figma/comments-to-strokes.test.ts +194 -0
- package/apps/studio/figma/comments-to-strokes.ts +173 -0
- package/apps/studio/figma/endpoints.ts +273 -0
- package/apps/studio/figma/fig-decode.test.ts +702 -0
- package/apps/studio/figma/fig-decode.ts +617 -0
- package/apps/studio/figma/fig-kiwi.ts +410 -0
- package/apps/studio/figma/fig-zip.ts +270 -0
- package/apps/studio/figma/from-codegen.test.ts +408 -0
- package/apps/studio/figma/from-codegen.ts +1103 -0
- package/apps/studio/figma/sanitize.test.ts +325 -0
- package/apps/studio/figma/sanitize.ts +407 -0
- package/apps/studio/figma/style-map.ts +352 -0
- package/apps/studio/figma/tailwind-map.test.ts +142 -0
- package/apps/studio/figma/tailwind-map.ts +545 -0
- package/apps/studio/figma/to-artboard.test.ts +808 -0
- package/apps/studio/figma/to-artboard.ts +701 -0
- package/apps/studio/figma/to-render.test.ts +180 -0
- package/apps/studio/figma/to-render.ts +328 -0
- package/apps/studio/figma/to-strokes-roundtrip.test.ts +152 -0
- package/apps/studio/figma/to-strokes.test.ts +705 -0
- package/apps/studio/figma/to-strokes.ts +749 -0
- package/apps/studio/figma/to-tokens.test.ts +321 -0
- package/apps/studio/figma/to-tokens.ts +305 -0
- package/apps/studio/figma/types.ts +544 -0
- package/apps/studio/figma/url.test.ts +167 -0
- package/apps/studio/figma/url.ts +160 -0
- package/apps/studio/http.ts +176 -0
- package/apps/studio/sync/asset-push.ts +432 -0
- package/apps/studio/sync/connection-state.ts +82 -3
- package/apps/studio/sync/hub-link.ts +63 -7
- package/apps/studio/sync/hubs-config.ts +31 -3
- package/apps/studio/sync/index.ts +286 -27
- package/apps/studio/sync/migrate-flat-fallback.ts +121 -0
- package/apps/studio/sync/presentation.ts +45 -1
- package/apps/studio/sync/status.ts +18 -0
- package/apps/studio/sync/supervisor.ts +5 -1
- package/apps/studio/sync/workspace-signin.ts +7 -3
- package/apps/studio/test/annotations-bindings.test.ts +150 -12
- package/apps/studio/test/canvas-create-api.test.ts +4 -1
- package/apps/studio/test/canvas-origin-gate.test.ts +17 -0
- package/apps/studio/test/capture-determinism-shape.test.ts +135 -0
- package/apps/studio/test/clip-addressing.test.ts +6 -1
- package/apps/studio/test/clip-ops.test.ts +5 -1
- package/apps/studio/test/cloud-endpoints.test.ts +96 -0
- package/apps/studio/test/cloud-renew.test.ts +205 -0
- package/apps/studio/test/cloud-shell-surfaces.test.ts +11 -2
- package/apps/studio/test/exporters/degraded-propagation.test.ts +123 -0
- package/apps/studio/test/exporters/unsupported-media.test.ts +123 -0
- package/apps/studio/test/fetch-asset-gate.test.ts +189 -0
- package/apps/studio/test/figma-explode.test.ts +438 -0
- package/apps/studio/test/figma-provenance.test.ts +108 -0
- package/apps/studio/test/figma-routes.test.ts +294 -0
- package/apps/studio/test/fixtures/perf-canvas.mjs +201 -0
- package/apps/studio/test/git-cloud-posture.test.ts +50 -0
- package/apps/studio/test/hub-link.test.ts +11 -0
- package/apps/studio/test/import-figma.test.ts +667 -0
- package/apps/studio/test/sync-asset-push.test.ts +567 -0
- package/apps/studio/test/sync-connection-state.test.ts +79 -0
- package/apps/studio/test/sync-hubs-config.test.ts +5 -0
- package/apps/studio/test/sync-migrate-flat-fallback.test.ts +98 -0
- package/apps/studio/test/sync-panel-surface.test.ts +90 -0
- package/apps/studio/test/sync-path-pull.test.ts +63 -0
- package/apps/studio/test/sync-presentation.test.ts +77 -0
- package/apps/studio/test/sync-runtime.test.ts +316 -1
- package/apps/studio/test/sync-status.test.ts +28 -0
- package/apps/studio/test/timeline-comp-target.test.ts +139 -0
- package/apps/studio/test/video-comp.test.ts +104 -2
- package/apps/studio/test/video-encode-lib.test.ts +63 -0
- package/apps/studio/test/workspace-containment.test.ts +1 -0
- package/apps/studio/use-artboard-drag.tsx +37 -3
- package/apps/studio/video-comp.tsx +121 -6
- package/apps/studio/whats-new.json +98 -0
- package/apps/studio/workspace-mode.ts +4 -0
- package/cli/commands/design.mjs +15 -0
- package/cli/commands/kg.mjs +8 -1
- package/cli/commands/kg.test.mjs +24 -0
- package/cli/lib/figma-codegen-reachability.test.mjs +104 -0
- package/cli/lib/figma-import-controls.test.mjs +70 -0
- package/package.json +8 -8
- package/plugins/flow/.claude-plugin/config.schema.json +3 -3
|
@@ -17,18 +17,69 @@ import {
|
|
|
17
17
|
rotatePoint,
|
|
18
18
|
type Stroke,
|
|
19
19
|
strokeBBox,
|
|
20
|
+
strokeHitTest,
|
|
20
21
|
strokeRotation,
|
|
21
22
|
} from './annotations-model.ts';
|
|
22
23
|
|
|
23
|
-
/**
|
|
24
|
+
/**
|
|
25
|
+
* The largest bbox a TEXT stroke may present as a bind target, PER AXIS.
|
|
26
|
+
*
|
|
27
|
+
* A `TextStroke` has no stored `w`/`h` — `strokeBBox` SYNTHESIZES one from the
|
|
28
|
+
* content (`max(8, longest * fontSize * 0.55)` wide). So a long single line can
|
|
29
|
+
* project a bind strip tens of thousands of px wide, and `bindCandidate` scans
|
|
30
|
+
* topmost-first with no area preference: one such stroke would silently become a
|
|
31
|
+
* board-wide magnet that steals every endpoint the user drags. Text bigger than
|
|
32
|
+
* a plausible label is therefore not offered as a host at all (DDR-216 D9).
|
|
33
|
+
*/
|
|
34
|
+
export const MAX_TEXT_BIND_W = 1200;
|
|
35
|
+
export const MAX_TEXT_BIND_H = 480;
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Hosts an arrow endpoint can attach to (shapes + cards, FigJam set).
|
|
39
|
+
*
|
|
40
|
+
* **Widened to `text` + `section` (DDR-216 D9).** FigJam binds connectors to
|
|
41
|
+
* anything, and on the only real board measured 2/2 connectors had at least one
|
|
42
|
+
* endpoint Maude could not bind — so an imported connector degraded to a frozen
|
|
43
|
+
* line. It is also a plain native gap: you could not attach an arrow to a text
|
|
44
|
+
* label or a section, independent of import.
|
|
45
|
+
*
|
|
46
|
+
* Three deliberate exclusions, each closing a regression the naive widening
|
|
47
|
+
* would have introduced:
|
|
48
|
+
*
|
|
49
|
+
* 1. **ANCHORED text is still not bindable.** `bindCandidate`/`anchorPoint`/
|
|
50
|
+
* `recomputeBoundArrows` call `strokeBBox(s)` WITHOUT the anchors map, and
|
|
51
|
+
* for a text stroke with `anchorId` that returns `null`. Admitting it would
|
|
52
|
+
* mint a bind that `anchorPoint` can never resolve — and `applyEnd` would
|
|
53
|
+
* keep it (it only strips when the host stops being bindable), freezing the
|
|
54
|
+
* arrow at stale coordinates forever. That is strictly worse than today's
|
|
55
|
+
* honest "strip the bind, freeze the endpoint". Anchored text lives inside a
|
|
56
|
+
* host shape which is itself bindable, so nothing is lost: bind to the shape.
|
|
57
|
+
* 2. **Oversized text is not offered** — see `MAX_TEXT_BIND_W`/`_H`.
|
|
58
|
+
* 3. **Groups are not bindable by construction** — Maude groups are a flat
|
|
59
|
+
* `groupIds[]` tag array, not addressable objects. A group-targeted import
|
|
60
|
+
* endpoint falls back to the group's geometric bbox and is reported.
|
|
61
|
+
*/
|
|
24
62
|
export function isBindable(s: Stroke): boolean {
|
|
25
|
-
|
|
63
|
+
if (
|
|
26
64
|
s.tool === 'rect' ||
|
|
27
65
|
s.tool === 'ellipse' ||
|
|
28
66
|
s.tool === 'polygon' ||
|
|
29
67
|
s.tool === 'sticky' ||
|
|
30
68
|
s.tool === 'image'
|
|
31
|
-
)
|
|
69
|
+
) {
|
|
70
|
+
return true;
|
|
71
|
+
}
|
|
72
|
+
if (s.tool === 'section') return true;
|
|
73
|
+
if (s.tool === 'text') {
|
|
74
|
+
if (s.anchorId) return false; // (1) — no resolvable bbox without the anchors map
|
|
75
|
+
const bb = strokeBBox(s);
|
|
76
|
+
// Per-AXIS, not an area product: an area cap is satisfiable by a very wide,
|
|
77
|
+
// very short strip, which is precisely the board-wide magnet shape. 4 000
|
|
78
|
+
// chars at the fontSize floor of 8 gives w ≈ 17 600 × h ≈ 9.6 — area 168 960,
|
|
79
|
+
// comfortably under a 640×480 product (post-implementation review F8).
|
|
80
|
+
return !!bb && bb.w > 0 && bb.h > 0 && bb.w <= MAX_TEXT_BIND_W && bb.h <= MAX_TEXT_BIND_H; // (2)
|
|
81
|
+
}
|
|
82
|
+
return false;
|
|
32
83
|
}
|
|
33
84
|
|
|
34
85
|
/** Bind proximity threshold in world px at zoom 1 (Excalidraw's 15 px). */
|
|
@@ -78,6 +129,20 @@ export function bindCandidate(
|
|
|
78
129
|
) {
|
|
79
130
|
continue;
|
|
80
131
|
}
|
|
132
|
+
// A SECTION is a large container, and the natural gesture is to draw one
|
|
133
|
+
// AROUND existing notes — which puts it later in the array, i.e. topmost.
|
|
134
|
+
// Testing its raw bbox would make the whole interior a magnet, so every
|
|
135
|
+
// arrow drawn between two stickies inside a section would bind to the
|
|
136
|
+
// section instead. `strokeHitTest` already encodes the right rule (a section
|
|
137
|
+
// is grabbed by its BORDER or its label chip; the interior stays
|
|
138
|
+
// click-through), so defer to it rather than inventing a second geometry.
|
|
139
|
+
//
|
|
140
|
+
// Deliberately NOT "prefer the smallest containing bbox": containment here
|
|
141
|
+
// is over a bbox INFLATED by `threshold`, so smallest-area lets an 8×8 dot
|
|
142
|
+
// 14 px AWAY beat the large rect the pointer is actually inside — and it
|
|
143
|
+
// inverts the visual invariant, letting a shape hidden behind an opaque one
|
|
144
|
+
// win a bind the user cannot see happening (DDR-216 D9 fix 4).
|
|
145
|
+
if (s.tool === 'section' && !strokeHitTest(s, wx, wy, threshold)) continue;
|
|
81
146
|
return {
|
|
82
147
|
hostId: s.id,
|
|
83
148
|
nx: snapMagnet(clamp01((wx - bb.x) / bb.w)),
|
|
@@ -155,7 +220,21 @@ export function recomputeBoundArrows(strokes: readonly Stroke[]): Stroke[] {
|
|
|
155
220
|
}
|
|
156
221
|
}
|
|
157
222
|
const pt = anchorPoint(host, effective.nx, effective.ny);
|
|
158
|
-
if (!pt)
|
|
223
|
+
if (!pt) {
|
|
224
|
+
// The host is bindable but its bbox is unresolvable right now (e.g. a
|
|
225
|
+
// text stroke whose anchors map isn't reachable from here). Returning
|
|
226
|
+
// silently would KEEP a bind that can never be honoured — the arrow
|
|
227
|
+
// would freeze at stale coordinates permanently, across save/load,
|
|
228
|
+
// while still claiming to be attached. Strip it instead: the arrow
|
|
229
|
+
// survives unbound with its endpoint frozen, which is the same honest
|
|
230
|
+
// degradation a deleted host already gets. (DDR-216 D9 fix 2 — inert
|
|
231
|
+
// before the `isBindable` widening, load-bearing after it.)
|
|
232
|
+
const copy: ArrowStroke = { ...next };
|
|
233
|
+
delete copy[end === 'start' ? 'startBind' : 'endBind'];
|
|
234
|
+
next = copy;
|
|
235
|
+
mutated = true;
|
|
236
|
+
return;
|
|
237
|
+
}
|
|
159
238
|
if (end === 'start') {
|
|
160
239
|
if (next.x1 !== pt[0] || next.y1 !== pt[1]) {
|
|
161
240
|
next = { ...next, x1: pt[0], y1: pt[1] };
|
|
@@ -154,7 +154,13 @@ import {
|
|
|
154
154
|
} from './annotations-snap.ts';
|
|
155
155
|
import { arrowPrimitives, type SvgPrimitive } from './canvas-arrowheads.ts';
|
|
156
156
|
import { IconLineThick, IconLineThin } from './canvas-icons.tsx';
|
|
157
|
-
import {
|
|
157
|
+
import {
|
|
158
|
+
countRender,
|
|
159
|
+
getLiveViewport,
|
|
160
|
+
useLiveViewport,
|
|
161
|
+
useViewportControllerContext,
|
|
162
|
+
useWorldRefContext,
|
|
163
|
+
} from './canvas-lib.tsx';
|
|
158
164
|
import { buildAnnotationStrokesRecord } from './commands/annotation-strokes-command.ts';
|
|
159
165
|
import { ensureMenuStyles as ensureCtxMenuStyles } from './context-menu.tsx';
|
|
160
166
|
import { crossedDragThreshold, type Tool } from './input-router.tsx';
|
|
@@ -948,6 +954,7 @@ function scaleStrokeInGroup(
|
|
|
948
954
|
// Component
|
|
949
955
|
|
|
950
956
|
export function AnnotationsLayer() {
|
|
957
|
+
countRender('annotationRenders');
|
|
951
958
|
ensureAnnotStyles();
|
|
952
959
|
const { tool, setTool, sticky, tools, shapeKind } = useToolMode();
|
|
953
960
|
const theme = useCanvasChromeTheme();
|
|
@@ -1011,8 +1018,22 @@ export function AnnotationsLayer() {
|
|
|
1011
1018
|
// endpoint free). Tracked here because endStroke (pointerup) carries no
|
|
1012
1019
|
// modifier state of its own.
|
|
1013
1020
|
const cmdHeldRef = useRef(false);
|
|
1014
|
-
const
|
|
1015
|
-
|
|
1021
|
+
const vpFallbackRef = useRef(vp);
|
|
1022
|
+
vpFallbackRef.current = vp;
|
|
1023
|
+
// Reads the LIVE viewport, not the published one. Publishing is settle-only
|
|
1024
|
+
// (gesture-static React), so mirroring the published value here would freeze
|
|
1025
|
+
// this ref for the whole gesture and every zoom-scaled threshold computed
|
|
1026
|
+
// from it — bind distance, snap tolerance, hit-test slop, sticker placement —
|
|
1027
|
+
// would silently use the pre-gesture zoom. Shaped as a ref so the ~40 existing
|
|
1028
|
+
// `vpRef.current` call sites keep working unchanged.
|
|
1029
|
+
const vpRef = useMemo(
|
|
1030
|
+
() => ({
|
|
1031
|
+
get current() {
|
|
1032
|
+
return getLiveViewport() ?? vpFallbackRef.current;
|
|
1033
|
+
},
|
|
1034
|
+
}),
|
|
1035
|
+
[]
|
|
1036
|
+
);
|
|
1016
1037
|
const visibilityCtx = useAnnotationsVisibility();
|
|
1017
1038
|
const chrome = useChromeVisibility();
|
|
1018
1039
|
// Presentation Mode hides annotations without mutating the user's own
|
|
@@ -1655,7 +1676,9 @@ export function AnnotationsLayer() {
|
|
|
1655
1676
|
|
|
1656
1677
|
const screenToWorld = useCallback(
|
|
1657
1678
|
(cx: number, cy: number): [number, number] => {
|
|
1658
|
-
|
|
1679
|
+
// Live: this runs at event time (drop/placement), where the published
|
|
1680
|
+
// viewport can be a whole gesture behind.
|
|
1681
|
+
const v = vpRef.current ?? { x: 0, y: 0, zoom: 1 };
|
|
1659
1682
|
const z = v.zoom || 1;
|
|
1660
1683
|
return [(cx - v.x) / z, (cy - v.y) / z];
|
|
1661
1684
|
},
|
|
@@ -1826,7 +1849,7 @@ export function AnnotationsLayer() {
|
|
|
1826
1849
|
|
|
1827
1850
|
const eraseAt = useCallback(
|
|
1828
1851
|
(wx: number, wy: number) => {
|
|
1829
|
-
const zoom =
|
|
1852
|
+
const zoom = vpRef.current?.zoom || 1;
|
|
1830
1853
|
const tol = 8 / zoom;
|
|
1831
1854
|
const prev = strokesRef.current;
|
|
1832
1855
|
for (let i = prev.length - 1; i >= 0; i--) {
|
|
@@ -4750,8 +4773,15 @@ function AddTextHint({ strokes, hintId }: { strokes: readonly Stroke[]; hintId:
|
|
|
4750
4773
|
* marquee/drag handler yields).
|
|
4751
4774
|
*/
|
|
4752
4775
|
function ConnectorDots({ stroke }: { stroke: Stroke }) {
|
|
4753
|
-
|
|
4754
|
-
|
|
4776
|
+
// LIVE zoom, deliberately not the published one. These are the counter-scaled
|
|
4777
|
+
// chrome elements (connector dots, selection bbox, section-label chips): they
|
|
4778
|
+
// must hold a constant SCREEN size while the world scales. Publishing is
|
|
4779
|
+
// settle-only since the gesture-static change, so reading published zoom here
|
|
4780
|
+
// would let this chrome grow and shrink with the world through a pinch and
|
|
4781
|
+
// then jump back at settle — the visible fidelity-pop a previous LOD attempt
|
|
4782
|
+
// was reverted for. These components render only for selected/hovered
|
|
4783
|
+
// strokes, so tracking the gesture frame-by-frame stays cheap.
|
|
4784
|
+
const zoom = useLiveViewport().zoom || 1;
|
|
4755
4785
|
if (!isBindable(stroke)) return null;
|
|
4756
4786
|
const center = strokeCenter(stroke);
|
|
4757
4787
|
if (!center) return null;
|
|
@@ -4942,8 +4972,9 @@ function SelectionHalo({
|
|
|
4942
4972
|
anchorsById: Map<string, AnchorHost>;
|
|
4943
4973
|
multi: boolean;
|
|
4944
4974
|
}) {
|
|
4945
|
-
|
|
4946
|
-
|
|
4975
|
+
// Counter-scaled chrome — must hold a constant screen size while the world
|
|
4976
|
+
// scales, so it needs the live zoom, not the settle-cadence published one.
|
|
4977
|
+
const zoom = useLiveViewport().zoom || 1;
|
|
4947
4978
|
const bbox = strokeBBox(stroke, anchorsById);
|
|
4948
4979
|
if (!bbox) return null;
|
|
4949
4980
|
// T17 + post-Wave-2 fix — annotation halo idioms:
|
|
@@ -4990,8 +5021,9 @@ function AnnotGroupBbox({
|
|
|
4990
5021
|
selectedStrokes: readonly Stroke[];
|
|
4991
5022
|
anchorsById: Map<string, AnchorHost>;
|
|
4992
5023
|
}) {
|
|
4993
|
-
|
|
4994
|
-
|
|
5024
|
+
// Counter-scaled chrome — must hold a constant screen size while the world
|
|
5025
|
+
// scales, so it needs the live zoom, not the settle-cadence published one.
|
|
5026
|
+
const zoom = useLiveViewport().zoom || 1;
|
|
4995
5027
|
if (selectedStrokes.length < 2) return null;
|
|
4996
5028
|
let xMin = Number.POSITIVE_INFINITY;
|
|
4997
5029
|
let yMin = Number.POSITIVE_INFINITY;
|
|
@@ -5225,8 +5257,9 @@ function SectionLabelChip({
|
|
|
5225
5257
|
y: number;
|
|
5226
5258
|
hitMode: 'visiblePainted' | 'none';
|
|
5227
5259
|
}) {
|
|
5228
|
-
|
|
5229
|
-
|
|
5260
|
+
// Counter-scaled chrome — must hold a constant screen size while the world
|
|
5261
|
+
// scales, so it needs the live zoom, not the settle-cadence published one.
|
|
5262
|
+
const zoom = useLiveViewport().zoom || 1;
|
|
5230
5263
|
const fontSize = SECTION_LABEL_FONT / zoom;
|
|
5231
5264
|
const chipH = SECTION_LABEL_H / zoom;
|
|
5232
5265
|
const gap = 4 / zoom;
|
|
@@ -5279,8 +5312,9 @@ function SectionTitleEditor({
|
|
|
5279
5312
|
onCommit: (text: string, fmt?: EditorFmt) => void;
|
|
5280
5313
|
onCancel: () => void;
|
|
5281
5314
|
}) {
|
|
5282
|
-
|
|
5283
|
-
|
|
5315
|
+
// Counter-scaled chrome — must hold a constant screen size while the world
|
|
5316
|
+
// scales, so it needs the live zoom, not the settle-cadence published one.
|
|
5317
|
+
const zoom = useLiveViewport().zoom || 1;
|
|
5284
5318
|
const fontSize = SECTION_LABEL_FONT / zoom;
|
|
5285
5319
|
const chipH = SECTION_LABEL_H / zoom;
|
|
5286
5320
|
const gap = 4 / zoom;
|
package/apps/studio/api.ts
CHANGED
|
@@ -5260,7 +5260,12 @@ export function createApi(ctx: Context, hooks: ApiHooks): Api {
|
|
|
5260
5260
|
// `loadCanvasMeta` call the DDR-093 DS-map loop below already makes for
|
|
5261
5261
|
// non-path-owned (`ui/`) canvases — no extra I/O for the common case.
|
|
5262
5262
|
const canvasKinds: Record<string, string> = {};
|
|
5263
|
-
|
|
5263
|
+
// DDR-216 D7 adds `imported-figma`. The badge deliberately reads as
|
|
5264
|
+
// THIRD-PARTY CONTENT rather than as provenance-therefore-trustworthy: a
|
|
5265
|
+
// clean "imported from Figma" stamp otherwise makes the most
|
|
5266
|
+
// attacker-influenced artifact in the tree look like real design work, to a
|
|
5267
|
+
// human AND to `design-system-keeper` / the critic panel.
|
|
5268
|
+
const NOTABLE_KINDS = new Set(['reconstructed-experimental', 'imported-figma']);
|
|
5264
5269
|
const defaultDs = cfg.defaultDesignSystem || cfg.designSystems?.[0]?.name || 'project';
|
|
5265
5270
|
// A file under `system/<folder>/` belongs to the DS that owns that folder —
|
|
5266
5271
|
// path-authoritative, because specimens/ui_kits rarely carry a sidecar
|
|
@@ -39,9 +39,9 @@
|
|
|
39
39
|
import { execFileSync } from 'node:child_process';
|
|
40
40
|
import { createHash } from 'node:crypto';
|
|
41
41
|
import { lookup } from 'node:dns/promises';
|
|
42
|
-
import { existsSync, mkdirSync, readFileSync, renameSync, rmSync } from 'node:fs';
|
|
42
|
+
import { existsSync, mkdirSync, readFileSync, realpathSync, renameSync, rmSync } from 'node:fs';
|
|
43
43
|
import { isIP } from 'node:net';
|
|
44
|
-
import { join, resolve, sep } from 'node:path';
|
|
44
|
+
import { basename, dirname, join, resolve, sep } from 'node:path';
|
|
45
45
|
import { pathToFileURL } from 'node:url';
|
|
46
46
|
|
|
47
47
|
// A real browser UA — some CDNs (wikimedia) 403 a bare curl/UA; the memory
|
|
@@ -202,7 +202,7 @@ export function classifyAddress(addr) {
|
|
|
202
202
|
* FetchAssetError (code 3) on anything else — non-https scheme, embedded
|
|
203
203
|
* credentials, missing host.
|
|
204
204
|
*/
|
|
205
|
-
export function parseHttpsTarget(rawUrl) {
|
|
205
|
+
export function parseHttpsTarget(rawUrl, opts = {}) {
|
|
206
206
|
let u;
|
|
207
207
|
try {
|
|
208
208
|
u = new URL(rawUrl);
|
|
@@ -221,9 +221,71 @@ export function parseHttpsTarget(rawUrl) {
|
|
|
221
221
|
if (!Number.isInteger(port) || port < 1 || port > 65535) {
|
|
222
222
|
throw new FetchAssetError(3, `bad port ${u.port}`);
|
|
223
223
|
}
|
|
224
|
+
// DDR-216 D4 — an OPTIONAL host allowlist, at the gate rather than at the
|
|
225
|
+
// caller. Absent (the default) = today's unrestricted behaviour, so the
|
|
226
|
+
// existing moodboard/research callers are untouched. The Figma lane always
|
|
227
|
+
// passes it.
|
|
228
|
+
//
|
|
229
|
+
// Matching is exact-or-DOTTED-SUFFIX, never a bare `endsWith`: `endsWith(
|
|
230
|
+
// 'figma.com')` admits `evil-figma.com`, and `endsWith('.amazonaws.com')`
|
|
231
|
+
// admits every S3 bucket on earth.
|
|
232
|
+
//
|
|
233
|
+
// This is a REACH control (which endpoint), never a CONTENT control — the
|
|
234
|
+
// Figma render bucket is a SHARED object store any account can write into,
|
|
235
|
+
// so the byte-level sniff below is what actually protects the tree. And it
|
|
236
|
+
// NARROWS the IP gate, never replaces it: an allowlisted hostname that
|
|
237
|
+
// resolves to 127.0.0.1 is still refused by `resolveSafeIp`.
|
|
238
|
+
if (opts.allowHosts?.length) {
|
|
239
|
+
const h = host.toLowerCase();
|
|
240
|
+
const ok = opts.allowHosts.some((suffix) => {
|
|
241
|
+
const s = String(suffix).toLowerCase().replace(/^\./, '');
|
|
242
|
+
return h === s || h.endsWith(`.${s}`);
|
|
243
|
+
});
|
|
244
|
+
if (!ok) throw new FetchAssetError(3, `host not in this lane's allowlist: ${safeHostLabel(h)}`);
|
|
245
|
+
}
|
|
246
|
+
// DDR-216 D4 — the Figma lane pins 443. Named as NEW logic rather than
|
|
247
|
+
// assumed: this function accepts 1–65535 by default and always has.
|
|
248
|
+
if (opts.pinPort443 && port !== 443) {
|
|
249
|
+
throw new FetchAssetError(3, `this lane requires port 443 (got ${port})`);
|
|
250
|
+
}
|
|
224
251
|
return { host, port };
|
|
225
252
|
}
|
|
226
253
|
|
|
254
|
+
/**
|
|
255
|
+
* A hostname reduced to a charset-validated token before it can be printed.
|
|
256
|
+
*
|
|
257
|
+
* DDR-216 D10: this verb's output is read BY an agent, so an upstream-controlled
|
|
258
|
+
* string must never appear verbatim in a message. A DNS label charset is narrow
|
|
259
|
+
* enough to be safe and specific enough to diagnose.
|
|
260
|
+
*/
|
|
261
|
+
function safeHostLabel(host) {
|
|
262
|
+
const cleaned = String(host)
|
|
263
|
+
.toLowerCase()
|
|
264
|
+
.replace(/[^a-z0-9.-]/g, '');
|
|
265
|
+
return cleaned.slice(0, 253) || '(unprintable)';
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
/**
|
|
269
|
+
* The `--raw-out` accept set: everything `sniffImageExt` accepts, PLUS SVG.
|
|
270
|
+
*
|
|
271
|
+
* `sniffImageExt` is deliberately NOT modified — it is the type gate the
|
|
272
|
+
* moodboard lane depends on, and a standing test asserts it still returns null
|
|
273
|
+
* for `<svg`/`<?xml`. The SVG probe below reuses the exact leading-token shape
|
|
274
|
+
* `_import-asset.mjs`'s own `svgPreParseReject` uses, so a payload accepted here
|
|
275
|
+
* is one that lane will actually attempt to parse (and then sanitize).
|
|
276
|
+
*/
|
|
277
|
+
export function sniffStagedKind(bytes) {
|
|
278
|
+
const raster = sniffImageExt(bytes);
|
|
279
|
+
if (raster) return raster;
|
|
280
|
+
// Text probe on the first bytes only — never decode the whole body to guess.
|
|
281
|
+
const head = Buffer.from(bytes.subarray(0, 256)).toString('utf8');
|
|
282
|
+
// Deliberately NARROWER than `svgPreParseReject`'s own leading-token set: a
|
|
283
|
+
// bare `<!--` prologue is legal SVG but it is also what lets an arbitrary text
|
|
284
|
+
// payload masquerade as one (review F2), and no Figma export starts with it.
|
|
285
|
+
// The full DDR-167 lane still validates properly downstream.
|
|
286
|
+
return /^\s*(<\?xml|<svg)/i.test(head) ? 'svg' : null;
|
|
287
|
+
}
|
|
288
|
+
|
|
227
289
|
// ── image sniff + naming ─────────────────────────────────────────────────────
|
|
228
290
|
|
|
229
291
|
/**
|
|
@@ -408,10 +470,91 @@ export async function fetchAsset({
|
|
|
408
470
|
designRootRel = '.design',
|
|
409
471
|
maxBytes = DEFAULT_MAX_BYTES,
|
|
410
472
|
maxTime = DEFAULT_MAX_TIME,
|
|
473
|
+
allowHosts,
|
|
474
|
+
pinPort443 = false,
|
|
475
|
+
rawOut = null,
|
|
476
|
+
rawRoot = null,
|
|
411
477
|
}) {
|
|
412
|
-
const { host, port } = parseHttpsTarget(url);
|
|
478
|
+
const { host, port } = parseHttpsTarget(url, { allowHosts, pinPort443 });
|
|
413
479
|
const pinIp = await resolveSafeIp(host);
|
|
414
480
|
|
|
481
|
+
// ── `--raw-out` (DDR-216 D11) ──────────────────────────────────────────────
|
|
482
|
+
//
|
|
483
|
+
// Download under the FULL network gate (IP classification, DNS pin, redirect
|
|
484
|
+
// ban, size/time caps, host allowlist) but write to a CALLER-SUPPLIED path
|
|
485
|
+
// instead of `assets/`, and do NOT content-address or name the file.
|
|
486
|
+
//
|
|
487
|
+
// Why this exists: D11 composes two already-reviewed gates rather than
|
|
488
|
+
// widening one. A Figma vector export is an SVG, and this helper's raster-only
|
|
489
|
+
// sniff is load-bearing for its OTHER caller (the DDR-147 moodboard lane,
|
|
490
|
+
// where URLs are research-harvested and there is no allowlist at all). So the
|
|
491
|
+
// Figma lane stages the bytes here and hands them to `_import-asset.mjs`'s
|
|
492
|
+
// DDR-167 SVG lane (allowlist DOM-sanitize → SVGO validity gate) before
|
|
493
|
+
// anything is named or lands in the versioned tree.
|
|
494
|
+
//
|
|
495
|
+
// IT STILL SNIFFS. `--raw-out` must never mean "skip the type gate" — that
|
|
496
|
+
// would turn the one reviewable downloader into an unsniffed one (HTML, JS,
|
|
497
|
+
// polyglots, archives) for the next caller who copies it. It sniffs against
|
|
498
|
+
// an EXTENDED accept set; `sniffImageExt` itself is untouched, which is what
|
|
499
|
+
// keeps the standing "sniffImageExt still rejects SVG" test literally true.
|
|
500
|
+
if (rawOut) {
|
|
501
|
+
// CONTAINMENT (post-implementation review F2). Without this, `--raw-out`
|
|
502
|
+
// was an arbitrary-file-write primitive on the ONE helper whose whole job
|
|
503
|
+
// is to be the safe downloader: `resolve()` + `renameSync()` with no root
|
|
504
|
+
// relationship, no charset assertion, and a type gate a leading `<!--`
|
|
505
|
+
// satisfies. An agent that can run `maude design fetch-asset` (ACP
|
|
506
|
+
// default-allows `Bash(maude:*)`) could have written remote bytes over
|
|
507
|
+
// `CLAUDE.md` — durable prompt injection.
|
|
508
|
+
//
|
|
509
|
+
// So the mode now REQUIRES the caller to declare the directory it owns, and
|
|
510
|
+
// the target must resolve inside it. The Figma lane passes its per-run
|
|
511
|
+
// staging dir; there is no way to express "anywhere".
|
|
512
|
+
if (!rawRoot) {
|
|
513
|
+
throw new FetchAssetError(2, '--raw-out requires --raw-root (the directory the caller owns)');
|
|
514
|
+
}
|
|
515
|
+
// BOTH sides get realpath'd, or neither. On macOS `/var` and `/tmp` are
|
|
516
|
+
// themselves OS-level symlinks (`-> /private/var`, `-> /private/tmp`), so
|
|
517
|
+
// realpath'ing only the root compares `/private/var/…` against `/var/…` and
|
|
518
|
+
// rejects EVERY legitimate staging path — which is exactly how this shipped
|
|
519
|
+
// and why the first real import skipped all 29 of its assets. Same trap
|
|
520
|
+
// DDR-172 Decision 1 documents for its own symlink check.
|
|
521
|
+
//
|
|
522
|
+
// The target does not exist yet, so realpath its PARENT (which does — the
|
|
523
|
+
// caller made the staging dir) and rebuild the leaf onto it.
|
|
524
|
+
const rootAbs = realpathSync(resolve(rawRoot));
|
|
525
|
+
const requested = resolve(rawOut);
|
|
526
|
+
const parentAbs = realpathSync(dirname(requested));
|
|
527
|
+
const outAbs = join(parentAbs, basename(requested));
|
|
528
|
+
if (outAbs !== rootAbs && !outAbs.startsWith(rootAbs + sep)) {
|
|
529
|
+
throw new FetchAssetError(6, '--raw-out must resolve inside --raw-root');
|
|
530
|
+
}
|
|
531
|
+
// Never overwrite: a staged download is always a fresh file, so an existing
|
|
532
|
+
// target means either a collision or an attempt to clobber something.
|
|
533
|
+
if (existsSync(outAbs)) {
|
|
534
|
+
throw new FetchAssetError(6, '--raw-out target already exists');
|
|
535
|
+
}
|
|
536
|
+
const tmpRaw = `${outAbs}.part`;
|
|
537
|
+
try {
|
|
538
|
+
curlDownload({ url, host, port, pinIp, tmpAbs: tmpRaw, maxBytes, maxTime });
|
|
539
|
+
let data;
|
|
540
|
+
try {
|
|
541
|
+
data = readFileSync(tmpRaw);
|
|
542
|
+
} catch {
|
|
543
|
+
throw new FetchAssetError(4, 'download produced no file');
|
|
544
|
+
}
|
|
545
|
+
if (data.length === 0) throw new FetchAssetError(4, 'downloaded empty body');
|
|
546
|
+
if (data.length > maxBytes) throw new FetchAssetError(4, `file exceeds ${maxBytes} bytes`);
|
|
547
|
+
const kind = sniffStagedKind(data);
|
|
548
|
+
if (!kind) {
|
|
549
|
+
throw new FetchAssetError(5, 'not a png/jpg/gif/webp/svg payload (HTML/script rejected)');
|
|
550
|
+
}
|
|
551
|
+
renameSync(tmpRaw, outAbs);
|
|
552
|
+
return { ref: null, path: outAbs, name: null, bytes: data.length, ext: kind };
|
|
553
|
+
} finally {
|
|
554
|
+
rmSync(tmpRaw, { force: true });
|
|
555
|
+
}
|
|
556
|
+
}
|
|
557
|
+
|
|
415
558
|
const { assetsDir } = containedAssetPath(root, designRootRel, 'placeholder.png');
|
|
416
559
|
mkdirSync(assetsDir, { recursive: true });
|
|
417
560
|
const tmpAbs = join(
|
|
@@ -476,6 +619,21 @@ function parseArgv(argv) {
|
|
|
476
619
|
case '--max-time':
|
|
477
620
|
out.maxTime = Number(argv[++i]);
|
|
478
621
|
break;
|
|
622
|
+
// DDR-216 D4/D11 — all three default to today's behaviour, so every
|
|
623
|
+
// existing caller of this helper is untouched.
|
|
624
|
+
case '--allow-host':
|
|
625
|
+
if (!out.allowHosts) out.allowHosts = [];
|
|
626
|
+
out.allowHosts.push(argv[++i]);
|
|
627
|
+
break;
|
|
628
|
+
case '--pin-port-443':
|
|
629
|
+
out.pinPort443 = true;
|
|
630
|
+
break;
|
|
631
|
+
case '--raw-out':
|
|
632
|
+
out.rawOut = argv[++i];
|
|
633
|
+
break;
|
|
634
|
+
case '--raw-root':
|
|
635
|
+
out.rawRoot = argv[++i];
|
|
636
|
+
break;
|
|
479
637
|
case '--json':
|
|
480
638
|
out.json = true;
|
|
481
639
|
break;
|
|
@@ -540,8 +698,14 @@ async function main() {
|
|
|
540
698
|
designRootRel: opts.designRoot,
|
|
541
699
|
maxBytes: opts.maxBytes,
|
|
542
700
|
maxTime: opts.maxTime,
|
|
701
|
+
...(opts.allowHosts ? { allowHosts: opts.allowHosts } : {}),
|
|
702
|
+
...(opts.pinPort443 ? { pinPort443: true } : {}),
|
|
703
|
+
...(opts.rawOut ? { rawOut: opts.rawOut } : {}),
|
|
704
|
+
...(opts.rawRoot ? { rawRoot: opts.rawRoot } : {}),
|
|
543
705
|
});
|
|
544
|
-
|
|
706
|
+
// `--raw-out` has no canvas ref (it deliberately writes outside `assets/`),
|
|
707
|
+
// so print the staged path instead of a null.
|
|
708
|
+
process.stdout.write(opts.json ? `${JSON.stringify(r)}\n` : `${r.ref ?? r.path}\n`);
|
|
545
709
|
} catch (err) {
|
|
546
710
|
process.stderr.write(`fetch-asset: ${err.message}\n`);
|
|
547
711
|
process.exit(err instanceof FetchAssetError ? err.code : 1);
|
|
@@ -203,7 +203,25 @@ const ALLOWED_PLAIN_ATTRS = new Set([
|
|
|
203
203
|
'stop-color',
|
|
204
204
|
'stop-opacity',
|
|
205
205
|
'patternUnits',
|
|
206
|
+
// Required by spec for pattern CONTENT, and we were stripping it. A
|
|
207
|
+
// `<pattern>` whose children are authored in the 0..1 unit box — which is what
|
|
208
|
+
// `<use transform="scale(0.00097 0.00075)">` (1/1028 × 1/1331) means — is read
|
|
209
|
+
// in user space without it, so its content collapses to about a pixel.
|
|
210
|
+
//
|
|
211
|
+
// HONEST SCOPE: this is an allowlist bug, NOT the cause of Figma's raster
|
|
212
|
+
// fills failing to appear. That was chased here first and the diagnosis was
|
|
213
|
+
// wrong: Figma's OWN UNTOUCHED export, fetched straight from `/v1/images` and
|
|
214
|
+
// loaded directly, does not paint its `<pattern>`/`<use>`/`<image
|
|
215
|
+
// xlink:href="data:…">` chain either. Allowing this attribute fixes nothing
|
|
216
|
+
// user-visible; it just stops us corrupting a pattern that would otherwise
|
|
217
|
+
// have worked. Same enum-valued, no-URL, no-script risk class as
|
|
218
|
+
// `patternUnits` directly above.
|
|
219
|
+
'patternContentUnits',
|
|
206
220
|
'patternTransform',
|
|
221
|
+
// Presentational text metrics. Their absence is not fatal the way the pattern
|
|
222
|
+
// one is, but it silently drifts imported type away from the source.
|
|
223
|
+
'letter-spacing',
|
|
224
|
+
'xml:space',
|
|
207
225
|
'version',
|
|
208
226
|
]);
|
|
209
227
|
|
|
@@ -623,6 +641,78 @@ export async function importSvg(svgText, { root, designRootRel = '.design' }) {
|
|
|
623
641
|
* only the rasterize step is blocked, so this fails loud there rather than
|
|
624
642
|
* pretending the whole pipeline is unavailable.
|
|
625
643
|
*/
|
|
644
|
+
|
|
645
|
+
/**
|
|
646
|
+
* The execution canary for MANY sanitized SVGs, in ONE browser session.
|
|
647
|
+
*
|
|
648
|
+
* Same coverage as `runSvgExecutionCanary`, same probe, same hard network
|
|
649
|
+
* denial, same directly-navigated document — the only thing that changes is
|
|
650
|
+
* that the session is created once instead of once per file.
|
|
651
|
+
*
|
|
652
|
+
* Why it exists: measured on a real 6-page product file, a per-file canary cost
|
|
653
|
+
* ~4.5 s each, so a normal design system's icon set (530 distinct vector
|
|
654
|
+
* renders after component dedupe) would have taken ~40 minutes and forced the
|
|
655
|
+
* import to drop most of its artwork against an asset-count cap. The canary is
|
|
656
|
+
* not what needed relaxing — the session-per-file was.
|
|
657
|
+
*
|
|
658
|
+
* Returns the set of indices that TRIPPED, so the caller refuses exactly those
|
|
659
|
+
* and keeps the rest. A tripped canary is still a hard refusal for that file.
|
|
660
|
+
*/
|
|
661
|
+
export async function runSvgExecutionCanaryBatch(sanitizedSvgs, { timeoutMs = 15_000 } = {}) {
|
|
662
|
+
const tripped = new Set();
|
|
663
|
+
if (sanitizedSvgs.length === 0) return tripped;
|
|
664
|
+
const dir = mkdtempSync(join(tmpdir(), 'maude-import-svg-batch-'));
|
|
665
|
+
const probePath = join(dir, 'probe.js');
|
|
666
|
+
try {
|
|
667
|
+
writeFileSync(probePath, 'window.__MAUDE_IMPORT_CANARY__ = false;\n');
|
|
668
|
+
const paths = sanitizedSvgs.map((svg, i) => {
|
|
669
|
+
const p = join(dir, `candidate-${i}.svg`);
|
|
670
|
+
writeFileSync(p, svg);
|
|
671
|
+
return p;
|
|
672
|
+
});
|
|
673
|
+
await withSandboxedRender(
|
|
674
|
+
async (session) => {
|
|
675
|
+
for (let i = 0; i < paths.length; i += 1) {
|
|
676
|
+
session.open(pathToFileURL(paths[i]).href);
|
|
677
|
+
// The probe re-runs on EVERY navigation in this session (it is
|
|
678
|
+
// registered as an init script), so the flag is per-document — a
|
|
679
|
+
// clean file after a tripped one still reads false.
|
|
680
|
+
if (session.eval('window.__MAUDE_IMPORT_CANARY__ === true').trim() === 'true') {
|
|
681
|
+
tripped.add(i);
|
|
682
|
+
}
|
|
683
|
+
}
|
|
684
|
+
},
|
|
685
|
+
{ timeoutMs, initScriptPath: probePath }
|
|
686
|
+
);
|
|
687
|
+
} finally {
|
|
688
|
+
rmSync(dir, { recursive: true, force: true });
|
|
689
|
+
}
|
|
690
|
+
return tripped;
|
|
691
|
+
}
|
|
692
|
+
|
|
693
|
+
/**
|
|
694
|
+
* Sanitize + canary + write MANY SVGs. The batched counterpart of `importSvg`.
|
|
695
|
+
* Returns one result per input, or `null` where the file was refused.
|
|
696
|
+
*/
|
|
697
|
+
export async function importSvgBatch(svgTexts, { root, designRootRel = '.design' }) {
|
|
698
|
+
const sanitized = svgTexts.map((t) => {
|
|
699
|
+
try {
|
|
700
|
+
return sanitizeSvgAllowlist(t);
|
|
701
|
+
} catch {
|
|
702
|
+
return null;
|
|
703
|
+
}
|
|
704
|
+
});
|
|
705
|
+
const live = [];
|
|
706
|
+
for (let i = 0; i < sanitized.length; i += 1) if (sanitized[i] !== null) live.push(i);
|
|
707
|
+
|
|
708
|
+
const tripped = await runSvgExecutionCanaryBatch(live.map((i) => sanitized[i]));
|
|
709
|
+
return sanitized.map((svg, i) => {
|
|
710
|
+
if (svg === null) return null;
|
|
711
|
+
if (tripped.has(live.indexOf(i))) return null;
|
|
712
|
+
return writeContainedAsset(root, designRootRel, Buffer.from(svg, 'utf8'), 'svg');
|
|
713
|
+
});
|
|
714
|
+
}
|
|
715
|
+
|
|
626
716
|
export async function importPdf(inputPath, { root, designRootRel = '.design' }) {
|
|
627
717
|
const buffer = readPdfCapped(inputPath);
|
|
628
718
|
const pageCount = await getPdfPageCountIsolated(buffer);
|