partforge 0.27.0 → 0.31.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/README.md +16 -4
- package/docs/AUTHORING-PARTS.md +99 -7
- package/docs/ERROR-PATTERNS.md +1 -1
- package/package.json +5 -2
- package/src/app-bracket.js +5 -0
- package/src/app-demo.js +5 -0
- package/src/app-faceted-vase.js +5 -0
- package/src/app-filleted-box.js +5 -0
- package/src/app-hull-sweep.js +5 -0
- package/src/app-nameplate.js +5 -0
- package/src/app-planter.js +5 -0
- package/src/app-text-smoke.js +5 -0
- package/src/framework/app.css +85 -39
- package/src/framework/chrome.css +180 -0
- package/src/framework/debug-overlay.js +16 -1
- package/src/framework/download.js +14 -6
- package/src/framework/geometry/manifold-backend.js +10 -16
- package/src/framework/geometry/mesh-stl.js +27 -0
- package/src/framework/geometry/occt-backend.js +292 -92
- package/src/framework/geometry/pose.js +47 -0
- package/src/framework/mount.js +17 -3
- package/src/framework/rail-state.js +73 -0
- package/src/framework/rail.js +321 -0
- package/src/framework/tokens.css +14 -1
package/README.md
CHANGED
|
@@ -99,10 +99,11 @@ const runtime = mount(part, {
|
|
|
99
99
|
createWorker,
|
|
100
100
|
elements: {
|
|
101
101
|
viewer, controls, // canvas host + param-panel host
|
|
102
|
+
rail, // full-height resizable/collapsible controls rail
|
|
102
103
|
status: { status, busy, phase }, // status chrome
|
|
103
104
|
tabs, // view-tab segmented control
|
|
104
105
|
exports: { stl, step, threeMf }, // export buttons
|
|
105
|
-
chrome: { pause, reframe, theme },
|
|
106
|
+
chrome: { pause, reframe, theme, railToggle }, // viewer buttons + rail collapse/restore
|
|
106
107
|
},
|
|
107
108
|
onBuild: ({ status, ms, error }) => {}, // per accepted build: "success" | "error"
|
|
108
109
|
onPick: ({ selection, label, prompt, token }) => {}, // programmatic click-to-select
|
|
@@ -112,9 +113,20 @@ runtime.dispose(); // stops loops, workers, observers, listeners; frees GPU
|
|
|
112
113
|
```
|
|
113
114
|
|
|
114
115
|
Every `elements` entry defaults to the legacy global ID (`#app`, `#controls`,
|
|
115
|
-
`#status`/`#busy`/`#phase`, `#part`,
|
|
116
|
-
`#
|
|
117
|
-
|
|
116
|
+
`#panel` for `rail`, `#status`/`#busy`/`#phase`, `#part`,
|
|
117
|
+
`#download`/`#download-step`/`#download-3mf`,
|
|
118
|
+
`#pause`/`#reframe`/`#theme`/`#rail-toggle`), so a classic host page needs no
|
|
119
|
+
changes. The viewer sizes from its container via ResizeObserver — no window
|
|
120
|
+
coupling.
|
|
121
|
+
|
|
122
|
+
`rail` is the full-height controls rail introduced by the resizable-panel
|
|
123
|
+
layout (`docs/superpowers/specs/2026-07-26-controls-rail-layout-design.md`);
|
|
124
|
+
`chrome.railToggle` is its optional collapse/restore button. Both are optional
|
|
125
|
+
— a host that lays out the framework itself (no rail markup) gets a no-op.
|
|
126
|
+
The rail's resize seam is positioned against `rail.parentElement` by default,
|
|
127
|
+
so **the rail must be a direct child of the positioned `.pf-shell`** unless
|
|
128
|
+
the host also supplies `elements.shell` to point at the real containing block
|
|
129
|
+
(e.g. when a wrapper div sits between them, as is common in a React layout).
|
|
118
130
|
|
|
119
131
|
`onPick` arms click-to-select permanently: `label` is the feature label (falling
|
|
120
132
|
back to the sub-part label/name) for compact UI, `prompt` is the LLM-ready
|
package/docs/AUTHORING-PARTS.md
CHANGED
|
@@ -30,8 +30,9 @@ OCCT-only fillet/chamfer/shell ops.
|
|
|
30
30
|
`http://localhost:5173/<your-part>.html`.
|
|
31
31
|
|
|
32
32
|
That's the whole loop. The chrome (panel, tabs, viewer, export buttons) is shared —
|
|
33
|
-
your HTML is
|
|
34
|
-
|
|
33
|
+
your HTML is structural markup only and carries no CSS (the framework supplies it via
|
|
34
|
+
`framework/app.css`, imported by `mount`). See "Wiring a part into a runnable app"
|
|
35
|
+
below for what that markup must contain.
|
|
35
36
|
|
|
36
37
|
---
|
|
37
38
|
|
|
@@ -336,6 +337,13 @@ their hash folds every shape-affecting argument (each `loft` ring's points/`z`/`
|
|
|
336
337
|
profile's segment specs from `roundedProfile`, and the tessellation from `twist`), so
|
|
337
338
|
changing any of them is a fresh cache node while an identical rebuild is a hit.
|
|
338
339
|
|
|
340
|
+
This holds on **both backends** — and on OCCT, `translate`/`rotate` are additionally
|
|
341
|
+
*pose-lazy*: the backend re-poses the cached solid's cached tessellation instead of
|
|
342
|
+
re-running any B-rep work. A parameter that only feeds a final placement rotation (a
|
|
343
|
+
lid's open angle, an exploded-view offset) therefore re-drags in ~0 ms even on the
|
|
344
|
+
slow exact kernel — keep such transforms as the last ops in `build` (or in `place`)
|
|
345
|
+
rather than baking them into the geometry earlier.
|
|
346
|
+
|
|
339
347
|
---
|
|
340
348
|
|
|
341
349
|
## Parameters: the control-panel schema
|
|
@@ -688,37 +696,121 @@ stylesheet). `mount` looks up these element IDs:
|
|
|
688
696
|
| `#download-step` / `#download` / `#download-3mf` | STEP / STL / 3MF export buttons |
|
|
689
697
|
| `#status`, `#busy`, `#phase` | status line + busy overlay |
|
|
690
698
|
| `#viewbar` with `#pause` / `#reframe` / `#cutaway` / `#theme` | optional viewer controls (omit any you don't want) |
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
699
|
+
| `#panel` | the full-height controls rail (`class="pf-rail"`); programmatic hosts pass `elements.rail` instead |
|
|
700
|
+
| `#rail-toggle` | optional — collapses/restores the rail; resolved the same way as `#pause`/`#theme` |
|
|
701
|
+
|
|
702
|
+
Copy `demo.html` and change the title, the panel heading, and the `<script src>`. Two
|
|
703
|
+
workers are spawned from your one worker entry (`name` = `"manifold"` for preview/STL/3MF,
|
|
704
|
+
`"occt"` for STEP — handled for you).
|
|
705
|
+
|
|
706
|
+
**The markup convention (`demo.html` is the canonical copy-me page):** `<body>` carries
|
|
707
|
+
`class="pf-shell"`, the flex row that lays the viewer column next to the rail. `#app`
|
|
708
|
+
(`class="pf-stage"`) *is* that viewer column, and now contains the floating chrome
|
|
709
|
+
(`#topbar`, `#viewbar`, `#busy`) as absolutely-positioned siblings of the canvas, not
|
|
710
|
+
page-level overlays. `#panel` (`class="pf-rail"`) is a full-height rail docked to the
|
|
711
|
+
right edge, split into three children — `.pf-rail-head` / `.pf-rail-body` /
|
|
712
|
+
`.pf-rail-foot` — of which head and foot are flex-fixed and only the body scrolls: put
|
|
713
|
+
your heading in the head and the download row in the foot so the export buttons never
|
|
714
|
+
scroll out of reach. The rail's drag/collapse seam is created by `rail.js` itself; don't
|
|
715
|
+
add markup for it. This isn't decorative — get the head/body/foot split wrong and either
|
|
716
|
+
the export buttons scroll away or a tall parameter list pushes them off-screen. See
|
|
717
|
+
`docs/superpowers/specs/2026-07-26-controls-rail-layout-design.md` for why the rail is
|
|
718
|
+
shaped this way (resize/collapse behavior, breakpoints, the design rationale).
|
|
719
|
+
|
|
720
|
+
**Keyboard (the seam is `role="separator"`, focusable, `tabIndex=0`):**
|
|
721
|
+
|
|
722
|
+
| Key | Action |
|
|
723
|
+
|---|---|
|
|
724
|
+
| ← | widen the rail 16px (64px with Shift). No-op while collapsed. |
|
|
725
|
+
| → | narrow the rail 16px (64px with Shift), clamped at the 240px minimum — never collapses. No-op while collapsed. |
|
|
726
|
+
| Home | jump to the 240px minimum, animated. Reopens even while collapsed. |
|
|
727
|
+
| End | jump to the clamped maximum (half the shell, capped at 560px), animated. Reopens even while collapsed. |
|
|
728
|
+
| Enter / Space | toggle collapse — collapses if open; reopens at the remembered width if collapsed. |
|
|
729
|
+
| double-click (on the seam) | reset to the 288px default, animated, and opens if collapsed. |
|
|
730
|
+
|
|
731
|
+
Arrow keys move the **separator**, not the pane — standard `role="separator"`
|
|
732
|
+
semantics, and why ← *widens* a right-hand rail. `Cmd`/`Ctrl`/`Alt` held with an
|
|
733
|
+
arrow key passes through untouched (those are OS/browser-reserved combos, e.g.
|
|
734
|
+
back navigation or window-switching); `Shift` alone still applies the larger
|
|
735
|
+
step. Collapsed, the two arrow keys are deliberate no-ops rather than a reopen
|
|
736
|
+
gesture — reopening would otherwise silently discard the remembered width and
|
|
737
|
+
clamp to the minimum, and "press an arrow, get narrower" reads backwards for a
|
|
738
|
+
rail that's already shut. Home/End and Enter/Space/double-click are exempt from
|
|
739
|
+
that rule and always reopen, since jumping to an explicit width or toggling is
|
|
740
|
+
an unambiguous, deliberate gesture either way. A held arrow-key repeat
|
|
741
|
+
suppresses the 150ms width transition for the whole repeat window (not just one
|
|
742
|
+
keydown), matching what happens during a drag.
|
|
743
|
+
|
|
744
|
+
Legacy id-only markup (predating this class scheme) still renders: `app.css` keeps
|
|
745
|
+
`:not(.pf-*)` fallbacks (`#app:not(.pf-stage)`, `#panel:not(.pf-rail)`, and
|
|
746
|
+
placement-only ones for `#topbar`/`#viewbar`) that reproduce the old floating-card
|
|
747
|
+
look — `:not()` rather than a plain id rule because an id selector outranks a class. New
|
|
748
|
+
apps should still use the classed markup above; the fallback exists for pages that
|
|
749
|
+
predate it, not as a second supported style.
|
|
750
|
+
|
|
751
|
+
A host that builds its own DOM instead of using `mount`'s markup (e.g. an editor
|
|
752
|
+
embedding the viewer/rail inside a larger UI) can adopt the same layout by importing
|
|
753
|
+
**`partforge/chrome.css`** directly — it's deliberately class-based and id-free for that
|
|
754
|
+
reason. It expects `partforge/tokens.css` to already be loaded for its `--pf-*` custom
|
|
755
|
+
properties, and expects the host to size `.pf-shell` itself (`mount`'s own `app.css`,
|
|
756
|
+
which `@import`s both, does both of these for you already).
|
|
694
757
|
|
|
695
758
|
`#cutaway` is optional viewer chrome. When present, it toggles an interactive
|
|
696
759
|
section plane whose exposed faces are hatched; changing views resets it. Cutaway
|
|
697
760
|
is viewer-only and never changes STL, STEP, or 3MF exports. Hosts that omit the
|
|
698
761
|
button get no cutaway UI.
|
|
699
762
|
|
|
700
|
-
Programmatic hosts can provide the same optional
|
|
701
|
-
ID by passing
|
|
763
|
+
Programmatic hosts can provide the same optional controls, including the rail toggle,
|
|
764
|
+
without relying on an ID by passing them beside the other chrome references — and can
|
|
765
|
+
pass the rail itself as `elements.rail` instead of relying on `#panel`:
|
|
702
766
|
|
|
703
767
|
```js
|
|
704
768
|
mount(part, {
|
|
705
769
|
createWorker,
|
|
706
770
|
elements: {
|
|
771
|
+
rail,
|
|
707
772
|
chrome: {
|
|
708
773
|
pause,
|
|
709
774
|
reframe,
|
|
710
775
|
cutaway,
|
|
711
776
|
theme,
|
|
777
|
+
railToggle,
|
|
712
778
|
},
|
|
713
779
|
},
|
|
714
780
|
});
|
|
715
781
|
```
|
|
716
782
|
|
|
783
|
+
`rail`/`chrome.railToggle` are both optional; a host with no rail markup gets a
|
|
784
|
+
no-op (the resize/collapse behavior below simply doesn't attach). **Constraint:**
|
|
785
|
+
the rail element must be a direct child of the positioned `.pf-shell` — the
|
|
786
|
+
resize seam is created and positioned against `rail.parentElement` by default,
|
|
787
|
+
so an extra wrapper div between them (common in a React layout) puts the seam
|
|
788
|
+
against the wrong ancestor and silently breaks `[data-pf-dragging] .pf-stage`.
|
|
789
|
+
A host that can't make the rail a direct child of `.pf-shell` must also pass
|
|
790
|
+
`elements.shell` pointing at the real positioned ancestor:
|
|
791
|
+
|
|
792
|
+
```js
|
|
793
|
+
mount(part, {
|
|
794
|
+
createWorker,
|
|
795
|
+
elements: { rail, shell, chrome: { railToggle } },
|
|
796
|
+
});
|
|
797
|
+
```
|
|
798
|
+
|
|
717
799
|
> Production deploy compiles only the pages listed in `build.rollupOptions.input`
|
|
718
800
|
> (currently the landing gallery + the demo part pages). Other root `*.html` files are
|
|
719
801
|
> **dev-only** (Vite serves any root HTML in `npm run dev`) unless added there. To also
|
|
720
802
|
> ship one, add it to `build.rollupOptions.input` in `vite.config.js`.
|
|
721
803
|
|
|
804
|
+
**Styling hooks:** the rail/stage layout and palette are both plain `--pf-*` custom
|
|
805
|
+
properties from `partforge/tokens.css`, overridable on `:root` (or
|
|
806
|
+
`:root[data-theme="light"]`) without touching `chrome.css`. Layout/shape tokens added
|
|
807
|
+
alongside the rail: `--pf-sans`, `--pf-rail-w`, `--pf-rail-pad`, `--pf-radius-control`,
|
|
808
|
+
`--pf-radius-pill`, `--pf-shadow-float`, `--pf-shadow-rail`. The dev demos self-host
|
|
809
|
+
Geist and Geist Mono (`@fontsource-variable/geist(-mono)`, a `devDependency`, imported
|
|
810
|
+
from each `app-<part>.js` — see `src/app-demo.js`) so a standalone forge looks like the
|
|
811
|
+
finished product; the published library ships no font files, and a consumer that loads
|
|
812
|
+
none falls through `--pf-sans`/`--pf-mono` to system stacks by design.
|
|
813
|
+
|
|
722
814
|
### Developing against a local (linked) partforge
|
|
723
815
|
|
|
724
816
|
A normal `npm install partforge` needs no extra config. But if you `npm link` a local
|
package/docs/ERROR-PATTERNS.md
CHANGED
|
@@ -47,7 +47,7 @@ test parses every one; keep prose like this as plain paragraphs):
|
|
|
47
47
|
- **Cause:** replicad transforms and booleans (`translate`/`rotate`/`mirror`/`cut`/…) consume their operand — the input solid is deleted and a new one returned.
|
|
48
48
|
- **Fix:** Never reuse a solid after transforming it; take a `.clone()` first when you need the original again. See [AUTHORING-PARTS.md](AUTHORING-PARTS.md) § "Geometry: the kernel / `Solid` API" (the `s.clone()` row).
|
|
49
49
|
|
|
50
|
-
The framework itself rebuilds each sub-part fresh per job and applies `place` once, which avoids the problem — follow the same pattern in your own code.
|
|
50
|
+
The framework itself rebuilds each sub-part fresh per job and applies `place` once, which avoids the problem — follow the same pattern in your own code. (Since the OCCT solid cache landed, the in-repo backend clones internally before every consuming replicad call, so wrapped `Solid`s effectively have value semantics and this crash should no longer reproduce through the kernel API — but the portable rule stands: per KERNEL-CONTRACT.md a backend MAY consume, so a part must still not rely on reuse.)
|
|
51
51
|
|
|
52
52
|
## probe-routed-to-occt
|
|
53
53
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "partforge",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.31.0",
|
|
4
4
|
"description": "Turn a declarative part definition into a parametric-CAD web app (three.js + Manifold/Replicad). Requires a Vite-based consumer.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -28,7 +28,8 @@
|
|
|
28
28
|
"./lint": "./src/lint.js",
|
|
29
29
|
"./derive": "./src/framework/derive.js",
|
|
30
30
|
"./testing": "./src/testing.js",
|
|
31
|
-
"./tokens.css": "./src/framework/tokens.css"
|
|
31
|
+
"./tokens.css": "./src/framework/tokens.css",
|
|
32
|
+
"./chrome.css": "./src/framework/chrome.css"
|
|
32
33
|
},
|
|
33
34
|
"bin": {
|
|
34
35
|
"partforge": "./bin/cli.js"
|
|
@@ -54,6 +55,8 @@
|
|
|
54
55
|
"three": "^0.184.0"
|
|
55
56
|
},
|
|
56
57
|
"devDependencies": {
|
|
58
|
+
"@fontsource-variable/geist": "^5.3.0",
|
|
59
|
+
"@fontsource-variable/geist-mono": "^5.3.0",
|
|
57
60
|
"happy-dom": "^20.10.6",
|
|
58
61
|
"playwright": "^1.49.0",
|
|
59
62
|
"vite": "^8.0.16",
|
package/src/app-bracket.js
CHANGED
|
@@ -1,3 +1,8 @@
|
|
|
1
|
+
// Self-hosted Geist + Geist Mono for the dev demos, so a standalone forge looks
|
|
2
|
+
// like the product. Dev-only: --pf-sans/--pf-mono fall back to system stacks for
|
|
3
|
+
// any consumer that doesn't load them (spec §2.2).
|
|
4
|
+
import "@fontsource-variable/geist";
|
|
5
|
+
import "@fontsource-variable/geist-mono";
|
|
1
6
|
import part from "./parts/bracket.js";
|
|
2
7
|
import { mount } from "./framework/index.js";
|
|
3
8
|
|
package/src/app-demo.js
CHANGED
|
@@ -1,3 +1,8 @@
|
|
|
1
|
+
// Self-hosted Geist + Geist Mono for the dev demos, so a standalone forge looks
|
|
2
|
+
// like the product. Dev-only: --pf-sans/--pf-mono fall back to system stacks for
|
|
3
|
+
// any consumer that doesn't load them (spec §2.2).
|
|
4
|
+
import "@fontsource-variable/geist";
|
|
5
|
+
import "@fontsource-variable/geist-mono";
|
|
1
6
|
import demoPart from "./parts/demo.js";
|
|
2
7
|
import { mount } from "./framework/index.js";
|
|
3
8
|
|
package/src/app-faceted-vase.js
CHANGED
|
@@ -1,3 +1,8 @@
|
|
|
1
|
+
// Self-hosted Geist + Geist Mono for the dev demos, so a standalone forge looks
|
|
2
|
+
// like the product. Dev-only: --pf-sans/--pf-mono fall back to system stacks for
|
|
3
|
+
// any consumer that doesn't load them (spec §2.2).
|
|
4
|
+
import "@fontsource-variable/geist";
|
|
5
|
+
import "@fontsource-variable/geist-mono";
|
|
1
6
|
import vasePart from "./parts/faceted-vase.js";
|
|
2
7
|
import { mount } from "./framework/index.js";
|
|
3
8
|
|
package/src/app-filleted-box.js
CHANGED
|
@@ -1,3 +1,8 @@
|
|
|
1
|
+
// Self-hosted Geist + Geist Mono for the dev demos, so a standalone forge looks
|
|
2
|
+
// like the product. Dev-only: --pf-sans/--pf-mono fall back to system stacks for
|
|
3
|
+
// any consumer that doesn't load them (spec §2.2).
|
|
4
|
+
import "@fontsource-variable/geist";
|
|
5
|
+
import "@fontsource-variable/geist-mono";
|
|
1
6
|
import part from "./parts/filleted-box.js";
|
|
2
7
|
import { mount } from "./framework/index.js";
|
|
3
8
|
|
package/src/app-hull-sweep.js
CHANGED
|
@@ -1,3 +1,8 @@
|
|
|
1
|
+
// Self-hosted Geist + Geist Mono for the dev demos, so a standalone forge looks
|
|
2
|
+
// like the product. Dev-only: --pf-sans/--pf-mono fall back to system stacks for
|
|
3
|
+
// any consumer that doesn't load them (spec §2.2).
|
|
4
|
+
import "@fontsource-variable/geist";
|
|
5
|
+
import "@fontsource-variable/geist-mono";
|
|
1
6
|
import part from "./parts/hull-sweep.js";
|
|
2
7
|
import { mount } from "./framework/index.js";
|
|
3
8
|
|
package/src/app-nameplate.js
CHANGED
|
@@ -1,3 +1,8 @@
|
|
|
1
|
+
// Self-hosted Geist + Geist Mono for the dev demos, so a standalone forge looks
|
|
2
|
+
// like the product. Dev-only: --pf-sans/--pf-mono fall back to system stacks for
|
|
3
|
+
// any consumer that doesn't load them (spec §2.2).
|
|
4
|
+
import "@fontsource-variable/geist";
|
|
5
|
+
import "@fontsource-variable/geist-mono";
|
|
1
6
|
import part from "./parts/nameplate.js";
|
|
2
7
|
import { mount } from "./framework/index.js";
|
|
3
8
|
|
package/src/app-planter.js
CHANGED
|
@@ -1,3 +1,8 @@
|
|
|
1
|
+
// Self-hosted Geist + Geist Mono for the dev demos, so a standalone forge looks
|
|
2
|
+
// like the product. Dev-only: --pf-sans/--pf-mono fall back to system stacks for
|
|
3
|
+
// any consumer that doesn't load them (spec §2.2).
|
|
4
|
+
import "@fontsource-variable/geist";
|
|
5
|
+
import "@fontsource-variable/geist-mono";
|
|
1
6
|
import planterPart from "./parts/planter.js";
|
|
2
7
|
import { mount } from "./framework/index.js";
|
|
3
8
|
|
package/src/app-text-smoke.js
CHANGED
|
@@ -1,3 +1,8 @@
|
|
|
1
|
+
// Self-hosted Geist + Geist Mono for the dev demos, so a standalone forge looks
|
|
2
|
+
// like the product. Dev-only: --pf-sans/--pf-mono fall back to system stacks for
|
|
3
|
+
// any consumer that doesn't load them (spec §2.2).
|
|
4
|
+
import "@fontsource-variable/geist";
|
|
5
|
+
import "@fontsource-variable/geist-mono";
|
|
1
6
|
import part from "./parts/text-smoke.js";
|
|
2
7
|
import { mount } from "./framework/index.js";
|
|
3
8
|
|
package/src/framework/app.css
CHANGED
|
@@ -6,42 +6,63 @@
|
|
|
6
6
|
accent, shadcn-shaped radii/shadows/focus rings — plain CSS on --pf-*.
|
|
7
7
|
See docs/AUTHORING-PARTS.md. */
|
|
8
8
|
@import "./tokens.css"; /* palette + light overrides; also exported standalone as partforge/tokens.css */
|
|
9
|
+
@import "./chrome.css"; /* reusable layout; also exported as partforge/chrome.css */
|
|
9
10
|
|
|
10
11
|
* { box-sizing: border-box; }
|
|
11
12
|
html, body { margin: 0; height: 100%; overflow: hidden;
|
|
12
|
-
font: 13px/1.4 -
|
|
13
|
-
|
|
13
|
+
font: 13px/1.4 var(--pf-sans); }
|
|
14
|
+
/* Legacy fallback: a page whose markup predates .pf-stage keeps the old
|
|
15
|
+
full-window viewer. :not() rather than a plain #app rule because an id would
|
|
16
|
+
outrank .pf-stage's positioning and win the cascade. */
|
|
17
|
+
#app:not(.pf-stage) { position: fixed; inset: 0; background: var(--pf-bg); }
|
|
14
18
|
canvas { display: block; }
|
|
15
19
|
|
|
16
|
-
|
|
20
|
+
/* Legacy fallback: the pre-rail floating card, for markup without .pf-rail. */
|
|
21
|
+
#panel:not(.pf-rail) {
|
|
17
22
|
position: fixed; top: 12px; left: 12px; width: 256px;
|
|
18
23
|
max-height: calc(100vh - 24px); overflow-y: auto; z-index: 10;
|
|
19
24
|
background: var(--pf-surface); border: 1px solid var(--pf-border); border-radius: 16px;
|
|
20
25
|
padding: 14px; color: var(--pf-text);
|
|
21
|
-
|
|
22
|
-
panes read as siblings (was the smaller shadow-md). */
|
|
23
|
-
box-shadow: 0 10px 15px -3px rgb(0 0 0 / .1), 0 4px 6px -4px rgb(0 0 0 / .1);
|
|
26
|
+
box-shadow: var(--pf-shadow-float);
|
|
24
27
|
}
|
|
28
|
+
/* Legacy fallback only: pre-rail markup has a bare <h1>/.sub directly inside
|
|
29
|
+
#panel, with no .pf-rail-head wrapper, so chrome.css's classed
|
|
30
|
+
.pf-rail-head h1/.sub rule (the single source of truth for that typography)
|
|
31
|
+
can't reach it — chrome.css is deliberately id-free (see test/tokens.test.js)
|
|
32
|
+
so this id-scoped restatement has to live here instead. Classed markup never
|
|
33
|
+
hits this rule (its h1 is inside .pf-rail-head, already styled by
|
|
34
|
+
chrome.css); keep the two in sync if the typography ever changes. */
|
|
25
35
|
#panel h1 { font-size: 14px; margin: 0 0 2px; color: var(--pf-text-strong); letter-spacing: -0.01em; }
|
|
26
36
|
#panel .sub {
|
|
27
37
|
font-family: var(--pf-mono); color: var(--pf-muted); font-size: 10px;
|
|
28
38
|
letter-spacing: 0.04em; text-transform: uppercase;
|
|
29
|
-
margin: 0 0
|
|
39
|
+
margin: 2px 0 0;
|
|
30
40
|
}
|
|
31
41
|
|
|
32
42
|
.seg { display: flex; gap: 2px; margin-bottom: 12px;
|
|
33
|
-
background: var(--pf-surface-2); border-radius:
|
|
43
|
+
background: var(--pf-surface-2); border-radius: var(--pf-radius-control); padding: 3px; }
|
|
34
44
|
.seg button {
|
|
35
|
-
flex: 1; padding: 6px 0; border: 0; border-radius:
|
|
45
|
+
flex: 1; padding: 6px 0; border: 0; border-radius: var(--pf-radius-control);
|
|
36
46
|
background: transparent; color: var(--pf-muted); cursor: pointer;
|
|
37
47
|
font-family: var(--pf-mono); font-size: 11px; letter-spacing: 0.02em;
|
|
38
48
|
}
|
|
39
49
|
.seg button:hover { color: var(--pf-text-2); }
|
|
40
50
|
.seg button.on { background: var(--pf-accent); color: var(--pf-on-accent); }
|
|
41
51
|
|
|
52
|
+
/* Full-bleed rows: the divider spans the rail's whole width while the content
|
|
53
|
+
sits at the rail's own padding, so each slider gains the ~22px the old box
|
|
54
|
+
border + padding used to take from both sides. */
|
|
42
55
|
.section {
|
|
43
|
-
|
|
44
|
-
|
|
56
|
+
padding: 11px var(--pf-rail-pad);
|
|
57
|
+
}
|
|
58
|
+
/* Divider BETWEEN visible sections. `~` walks all preceding siblings, and a
|
|
59
|
+
relevance-hidden section (.section-hidden, display:none) fails the :not(),
|
|
60
|
+
so the topmost VISIBLE section never draws a hairline under the rail header.
|
|
61
|
+
Do not simplify this to `.section:first-child { border-top: 0 }` — that
|
|
62
|
+
matches DOM position rather than visibility, and leaves a stray divider
|
|
63
|
+
floating at the top whenever applyRelevance hides the first section. */
|
|
64
|
+
.section:not(.section-hidden) ~ .section:not(.section-hidden) {
|
|
65
|
+
border-top: 1px solid var(--pf-border);
|
|
45
66
|
}
|
|
46
67
|
.sec-title {
|
|
47
68
|
font-family: var(--pf-mono); font-size: 10px; font-weight: 600;
|
|
@@ -49,13 +70,13 @@ canvas { display: block; }
|
|
|
49
70
|
}
|
|
50
71
|
select.preset {
|
|
51
72
|
width: 100%; background: var(--pf-input-bg); color: var(--pf-text-2);
|
|
52
|
-
border: 1px solid var(--pf-border); border-radius:
|
|
73
|
+
border: 1px solid var(--pf-border); border-radius: var(--pf-radius-control); padding: 7px 9px;
|
|
53
74
|
font-family: var(--pf-mono); font-size: 11px;
|
|
54
75
|
}
|
|
55
76
|
.feat { display: flex; align-items: center; gap: 8px; margin: 6px 0;
|
|
56
77
|
color: var(--pf-text-2); cursor: pointer; }
|
|
57
78
|
.feat input { cursor: pointer; accent-color: var(--pf-accent); }
|
|
58
|
-
.feat-group { margin: 2px 0 8px; padding-left: 10px; border-left:
|
|
79
|
+
.feat-group { margin: 2px 0 8px; padding-left: 10px; border-left: 1px solid var(--pf-border); }
|
|
59
80
|
.feat-group.hidden { display: none; }
|
|
60
81
|
.adv-toggle {
|
|
61
82
|
margin-top: 8px; padding: 4px 0; width: 100%; border: 0; border-radius: 6px;
|
|
@@ -74,7 +95,7 @@ select.preset {
|
|
|
74
95
|
.row .num {
|
|
75
96
|
width: 54px; text-align: right; font-family: var(--pf-mono); font-size: 12px; font-variant-numeric: tabular-nums;
|
|
76
97
|
background: var(--pf-input-bg); color: var(--pf-text-strong);
|
|
77
|
-
border: 1px solid var(--pf-border); border-radius:
|
|
98
|
+
border: 1px solid var(--pf-border); border-radius: var(--pf-radius-control); padding: 3px 6px;
|
|
78
99
|
}
|
|
79
100
|
.row .num:focus { outline: none; border-color: var(--pf-accent);
|
|
80
101
|
box-shadow: 0 0 0 3px color-mix(in oklab, var(--pf-accent) 35%, transparent); }
|
|
@@ -84,7 +105,7 @@ select.preset {
|
|
|
84
105
|
.text-input {
|
|
85
106
|
display: block; width: 100%; font: 12px/1.4 var(--pf-mono);
|
|
86
107
|
background: var(--pf-input-bg); color: var(--pf-text-strong);
|
|
87
|
-
border: 1px solid var(--pf-border); border-radius:
|
|
108
|
+
border: 1px solid var(--pf-border); border-radius: var(--pf-radius-control); padding: 6px 8px;
|
|
88
109
|
}
|
|
89
110
|
textarea.text-input { min-height: 64px; resize: vertical; }
|
|
90
111
|
.text-input:focus { outline: none; border-color: var(--pf-accent);
|
|
@@ -97,12 +118,12 @@ input[type="range"]::-moz-range-track { height: 3px; border-radius: 2px; backgro
|
|
|
97
118
|
input[type="range"]::-moz-range-progress { height: 3px; border-radius: 2px; background: var(--pf-accent); }
|
|
98
119
|
input[type="range"]::-webkit-slider-thumb {
|
|
99
120
|
-webkit-appearance: none; appearance: none; width: 14px; height: 14px; margin-top: -5.5px;
|
|
100
|
-
border-radius: 50%; background: var(--pf-accent); border: 2px solid var(--pf-surface
|
|
121
|
+
border-radius: 50%; background: var(--pf-accent); border: 2px solid var(--pf-surface); box-shadow: 0 0 0 1px var(--pf-accent);
|
|
101
122
|
transition: box-shadow .12s ease;
|
|
102
123
|
}
|
|
103
124
|
input[type="range"]::-moz-range-thumb {
|
|
104
125
|
width: 14px; height: 14px; border-radius: 50%; background: var(--pf-accent);
|
|
105
|
-
border: 2px solid var(--pf-surface
|
|
126
|
+
border: 2px solid var(--pf-surface); box-shadow: 0 0 0 1px var(--pf-accent);
|
|
106
127
|
}
|
|
107
128
|
input[type="range"]:hover::-webkit-slider-thumb { box-shadow: 0 0 0 5px color-mix(in oklab, var(--pf-accent) 35%, transparent); }
|
|
108
129
|
input[type="range"]:focus-visible { outline: none; }
|
|
@@ -110,7 +131,7 @@ input[type="range"]:focus-visible::-webkit-slider-thumb { box-shadow: 0 0 0 5px
|
|
|
110
131
|
input[type="range"]:focus-visible::-moz-range-thumb { box-shadow: 0 0 0 5px color-mix(in oklab, var(--pf-accent) 35%, transparent); }
|
|
111
132
|
|
|
112
133
|
button.action {
|
|
113
|
-
width: 100%; margin-top: 8px; padding: 9px; border: 0; border-radius:
|
|
134
|
+
width: 100%; margin-top: 8px; padding: 9px; border: 0; border-radius: var(--pf-radius-control);
|
|
114
135
|
background: var(--pf-accent); color: var(--pf-on-accent); font-weight: 500; cursor: pointer;
|
|
115
136
|
}
|
|
116
137
|
button.action:hover:not(:disabled) { background: color-mix(in oklab, var(--pf-accent) 90%, black); }
|
|
@@ -125,7 +146,7 @@ button.action:disabled { opacity: .5; cursor: default; }
|
|
|
125
146
|
}
|
|
126
147
|
.dl-row { display: flex; gap: 6px; }
|
|
127
148
|
.dl-row button {
|
|
128
|
-
flex: 1; padding: 8px 0; border: 1px solid var(--pf-border); border-radius:
|
|
149
|
+
flex: 1; padding: 8px 0; border: 1px solid var(--pf-border); border-radius: var(--pf-radius-control);
|
|
129
150
|
background: transparent; color: var(--pf-text-2);
|
|
130
151
|
font-family: var(--pf-mono); font-weight: 600; font-size: 11px; letter-spacing: 0.06em; cursor: pointer;
|
|
131
152
|
}
|
|
@@ -143,28 +164,28 @@ button.action:focus-visible, .adv-toggle:focus-visible, #viewbar button:focus-vi
|
|
|
143
164
|
box-shadow: 0 0 0 3px color-mix(in oklab, var(--pf-accent) 35%, transparent);
|
|
144
165
|
}
|
|
145
166
|
|
|
146
|
-
/* part tabs
|
|
147
|
-
#topbar {
|
|
148
|
-
position: fixed; top: 12px; left: 50%; transform: translateX(-50%);
|
|
149
|
-
z-index: 15;
|
|
150
|
-
}
|
|
167
|
+
/* part tabs (placement: .pf-float-tabs; legacy markup keeps the old float) */
|
|
168
|
+
#topbar:not(.pf-float-tabs) { position: fixed; top: 12px; left: 50%; transform: translateX(-50%); z-index: 15; }
|
|
151
169
|
#topbar .seg {
|
|
152
170
|
margin: 0; padding: 4px; background: var(--pf-surface); border: 1px solid var(--pf-border);
|
|
153
|
-
border-radius:
|
|
154
|
-
box-shadow:
|
|
171
|
+
border-radius: var(--pf-radius-pill);
|
|
172
|
+
box-shadow: var(--pf-shadow-float);
|
|
155
173
|
}
|
|
156
174
|
#topbar .seg button { min-width: 70px; padding: 7px 10px; }
|
|
157
175
|
|
|
158
|
-
/* viewer controls
|
|
176
|
+
/* viewer controls. APPEARANCE is ungated: partforge-cloud re-anchors #viewbar's
|
|
177
|
+
position in sandbox.css but inherits this pill chrome, so gating it on a class
|
|
178
|
+
the cloud never sets would strip the editor's viewbar. PLACEMENT comes from
|
|
179
|
+
.pf-float-viewbar; the :not() keeps legacy markup's old top-right float. */
|
|
159
180
|
#viewbar {
|
|
160
|
-
position: fixed; top: 12px; right: 12px; z-index: 15;
|
|
161
181
|
display: flex; gap: 4px; padding: 4px;
|
|
162
182
|
background: var(--pf-surface); border: 1px solid var(--pf-border);
|
|
163
|
-
border-radius:
|
|
164
|
-
box-shadow:
|
|
183
|
+
border-radius: var(--pf-radius-pill);
|
|
184
|
+
box-shadow: var(--pf-shadow-float);
|
|
165
185
|
}
|
|
186
|
+
#viewbar:not(.pf-float-viewbar) { position: fixed; top: 12px; right: 12px; z-index: 15; }
|
|
166
187
|
#viewbar button {
|
|
167
|
-
width: 34px; height: 34px; border: 0; border-radius:
|
|
188
|
+
width: 34px; height: 34px; border: 0; border-radius: var(--pf-radius-control);
|
|
168
189
|
background: transparent; color: var(--pf-muted-2); cursor: pointer;
|
|
169
190
|
font-size: 15px; line-height: 1;
|
|
170
191
|
display: flex; align-items: center; justify-content: center;
|
|
@@ -177,16 +198,36 @@ button.action:focus-visible, .adv-toggle:focus-visible, #viewbar button:focus-vi
|
|
|
177
198
|
#viewbar button:hover { color: var(--pf-text); background: var(--pf-surface-2); }
|
|
178
199
|
#viewbar button.on { background: var(--pf-accent); color: var(--pf-on-accent); }
|
|
179
200
|
|
|
201
|
+
/* At very narrow widths the full-size pill (5 icon buttons + cutaway's
|
|
202
|
+
Flip/Reset, ~314px) is wider than a small phone's viewport minus the
|
|
203
|
+
stage's 12px margins, and .pf-shell { overflow: hidden } clips the excess
|
|
204
|
+
off its left edge (caught by scripts/check-app.mjs's containment check).
|
|
205
|
+
Shrink the pill instead of letting it wrap — a two-row pill would need its
|
|
206
|
+
own anchoring rework, while shrinking is a pure appearance change. */
|
|
207
|
+
@media (max-width: 360px) {
|
|
208
|
+
#viewbar { gap: 3px; }
|
|
209
|
+
#viewbar button { width: 30px; height: 30px; font-size: 13px; }
|
|
210
|
+
#viewbar .pf-cutaway-actions { gap: 3px; }
|
|
211
|
+
#viewbar .pf-cutaway-actions button { min-width: 44px; padding: 0 6px; }
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
/* Legacy id-only markup only: classed markup's viewbar lives inside .pf-stage
|
|
215
|
+
(bottom-right, see chrome.css's .pf-float-viewbar) so it never meets the
|
|
216
|
+
top-left floating #panel card. Legacy markup still floats #viewbar top-right
|
|
217
|
+
and #panel top-left (both `position: fixed`), which collide once the window
|
|
218
|
+
gets narrow enough — restore the old narrow-width rule that moves #viewbar
|
|
219
|
+
to bottom-centre and gives #panel room, scoped with :not(.pf-*) so it can't
|
|
220
|
+
affect the new layout. */
|
|
180
221
|
@media (max-width: 680px) {
|
|
181
|
-
#panel { max-height: calc(100vh - 80px); }
|
|
182
|
-
#viewbar {
|
|
222
|
+
#panel:not(.pf-rail) { max-height: calc(100vh - 80px); }
|
|
223
|
+
#viewbar:not(.pf-float-viewbar) {
|
|
183
224
|
top: auto; right: auto; bottom: 12px; left: 50%;
|
|
184
225
|
transform: translateX(-50%);
|
|
185
226
|
}
|
|
186
227
|
}
|
|
187
228
|
|
|
188
229
|
#busy {
|
|
189
|
-
position:
|
|
230
|
+
position: absolute; inset: 0; z-index: 20; pointer-events: none;
|
|
190
231
|
display: none; flex-direction: column; align-items: center;
|
|
191
232
|
justify-content: center; gap: 14px;
|
|
192
233
|
}
|
|
@@ -211,8 +252,8 @@ button.action:focus-visible, .adv-toggle:focus-visible, #viewbar button:focus-vi
|
|
|
211
252
|
.popover {
|
|
212
253
|
position: fixed; z-index: 50; max-width: 280px; max-height: 50vh; overflow: auto;
|
|
213
254
|
background: var(--pf-surface); color: var(--pf-text); border: 1px solid var(--pf-border);
|
|
214
|
-
border-radius:
|
|
215
|
-
box-shadow:
|
|
255
|
+
border-radius: var(--pf-radius-pill); padding: 10px 12px;
|
|
256
|
+
box-shadow: var(--pf-shadow-float);
|
|
216
257
|
font-size: 12px; line-height: 1.5;
|
|
217
258
|
}
|
|
218
259
|
.popover[hidden] { display: none; }
|
|
@@ -246,12 +287,12 @@ button.action:focus-visible, .adv-toggle:focus-visible, #viewbar button:focus-vi
|
|
|
246
287
|
/* request-a-pick: agent prompt banner, floated top-centre well below the part tabs,
|
|
247
288
|
laid out like a chat message (avatar + text). Slides/fades in when shown; themed. */
|
|
248
289
|
#pf-pick-banner {
|
|
249
|
-
position: fixed; top: 78px; left: 50
|
|
290
|
+
position: fixed; top: 78px; left: calc(50% - var(--pf-rail-w) / 2); transform: translateX(-50%);
|
|
250
291
|
z-index: 30; max-width: min(56ch, calc(100vw - 24px));
|
|
251
292
|
padding: 10px 36px 10px 12px;
|
|
252
293
|
background: var(--pf-surface); color: var(--pf-text);
|
|
253
|
-
border: 1px solid var(--pf-border); border-radius:
|
|
254
|
-
box-shadow:
|
|
294
|
+
border: 1px solid var(--pf-border); border-radius: var(--pf-radius-pill);
|
|
295
|
+
box-shadow: var(--pf-shadow-float);
|
|
255
296
|
font-size: 12px; line-height: 1.45;
|
|
256
297
|
animation: pf-pick-in .18s ease;
|
|
257
298
|
}
|
|
@@ -264,6 +305,11 @@ button.action:focus-visible, .adv-toggle:focus-visible, #viewbar button:focus-vi
|
|
|
264
305
|
#pf-pick-banner .pf-pick-msg { display: flex; flex-direction: column; gap: 1px; min-width: 0; }
|
|
265
306
|
#pf-pick-banner .pf-pick-label { color: var(--pf-muted); font-size: 11px; }
|
|
266
307
|
#pf-pick-banner .pf-pick-prompt { color: var(--pf-text-strong); font-weight: 600; }
|
|
308
|
+
/* Below the rail's breakpoint the rail stacks under the viewer, so the stage is
|
|
309
|
+
full width again and the banner centres on the window. */
|
|
310
|
+
@media (max-width: 719px) {
|
|
311
|
+
#pf-pick-banner { left: 50%; }
|
|
312
|
+
}
|
|
267
313
|
#pf-pick-close {
|
|
268
314
|
position: absolute; top: 6px; right: 6px; appearance: none;
|
|
269
315
|
width: 20px; height: 20px; padding: 0;
|