partforge 0.50.0 → 0.51.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 +4 -0
- package/package.json +1 -1
- package/src/framework/chrome.css +68 -0
- package/src/framework/mount.js +33 -3
- package/src/framework/rail.js +19 -2
package/README.md
CHANGED
|
@@ -114,6 +114,10 @@ runtime.setHostPane("rail"); // narrow layout only: show just the controls
|
|
|
114
114
|
// built-in tab bar. null hands selection back.
|
|
115
115
|
runtime.setActive(false); // park the viewer: stop the render loop, release the
|
|
116
116
|
// drawing buffer. setActive(true) restores both.
|
|
117
|
+
runtime.attachTooltips([{ element: myButton }]); // host chrome buttons join the mount's
|
|
118
|
+
// shared hover tooltip (label = the button's title or
|
|
119
|
+
// aria-label, or a per-entry getLabel()); returns
|
|
120
|
+
// { sync, hide, detach }, auto-detached on dispose()
|
|
117
121
|
const off = runtime.onContextLost(() => {}); // WebGL context loss; returns an unsubscribe
|
|
118
122
|
runtime.dispose(); // stops loops, workers, observers, listeners; frees GPU resources
|
|
119
123
|
```
|
package/package.json
CHANGED
package/src/framework/chrome.css
CHANGED
|
@@ -79,6 +79,74 @@
|
|
|
79
79
|
.pf-rail-head, .pf-rail-foot { flex: none; }
|
|
80
80
|
.pf-rail-head { padding: 12px var(--pf-rail-pad); border-bottom: 1px solid var(--pf-border); }
|
|
81
81
|
.pf-rail-foot { padding: 12px var(--pf-rail-pad); border-top: 1px solid var(--pf-border); }
|
|
82
|
+
|
|
83
|
+
/* Foot ACTIONS, the counterpart to app.css's `#viewbar button`: any button a
|
|
84
|
+
host puts in the foot inherits the shared chrome, so a host-drawn control
|
|
85
|
+
sits beside a built-in one without restating the metrics — or, more to the
|
|
86
|
+
point, without re-deriving the states. Both hosts had independently written
|
|
87
|
+
the same padding/radius/mono stack, and only one of them had remembered to
|
|
88
|
+
exclude :disabled from :hover, so a dead button still lit up under the
|
|
89
|
+
pointer in the other. That bug is unreachable from here.
|
|
90
|
+
|
|
91
|
+
Buttons only; the ROW is the host's. A foot can hold a bare row
|
|
92
|
+
(partforge-cloud) or a labelled group (the demo pages' .dl/.dl-head), and a
|
|
93
|
+
container rule here would have to pick one. This mirrors the viewbar only
|
|
94
|
+
as far as the analogy holds: there, the pill itself is a single known
|
|
95
|
+
element partforge can own outright.
|
|
96
|
+
|
|
97
|
+
Base is the quiet outline treatment, because that is the safe thing to
|
|
98
|
+
inherit by accident; the loud one is opt-in via .pf-primary. Equal
|
|
99
|
+
specificity to app.css's `.dl-row button`, which is imported after this
|
|
100
|
+
file and therefore still wins — the legacy download row renders exactly as
|
|
101
|
+
before, and can drop .dl-row whenever it likes. */
|
|
102
|
+
.pf-rail-foot button {
|
|
103
|
+
flex: 1;
|
|
104
|
+
padding: 8px 0;
|
|
105
|
+
border: 1px solid var(--pf-border);
|
|
106
|
+
border-radius: var(--pf-radius-control);
|
|
107
|
+
background: transparent;
|
|
108
|
+
color: var(--pf-text-2);
|
|
109
|
+
font-family: var(--pf-mono);
|
|
110
|
+
font-weight: 600;
|
|
111
|
+
font-size: 11px;
|
|
112
|
+
letter-spacing: 0.06em;
|
|
113
|
+
cursor: pointer;
|
|
114
|
+
}
|
|
115
|
+
.pf-rail-foot button:hover:not(:disabled) {
|
|
116
|
+
border-color: var(--pf-accent);
|
|
117
|
+
color: var(--pf-text-strong);
|
|
118
|
+
}
|
|
119
|
+
.pf-rail-foot button:disabled { opacity: .45; cursor: default; }
|
|
120
|
+
|
|
121
|
+
/* The foot's main call to action — accent-filled, so it reads as the primary
|
|
122
|
+
thing to do with a finished part. */
|
|
123
|
+
.pf-rail-foot button.pf-primary {
|
|
124
|
+
border-color: var(--pf-accent);
|
|
125
|
+
background: var(--pf-accent);
|
|
126
|
+
color: var(--pf-on-accent);
|
|
127
|
+
}
|
|
128
|
+
.pf-rail-foot button.pf-primary:hover:not(:disabled) {
|
|
129
|
+
background: color-mix(in oklab, var(--pf-accent) 88%, #000);
|
|
130
|
+
border-color: color-mix(in oklab, var(--pf-accent) 88%, #000);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/* A square icon-only action sitting beside the primary one. Sized to match
|
|
134
|
+
the viewbar's icon buttons, and flex-none so the primary action keeps the
|
|
135
|
+
remaining width rather than the two splitting it evenly. */
|
|
136
|
+
.pf-rail-foot button.pf-icon {
|
|
137
|
+
flex: 0 0 auto;
|
|
138
|
+
width: 34px;
|
|
139
|
+
display: inline-flex;
|
|
140
|
+
align-items: center;
|
|
141
|
+
justify-content: center;
|
|
142
|
+
color: var(--pf-muted);
|
|
143
|
+
}
|
|
144
|
+
.pf-rail-foot button.pf-icon:hover:not(:disabled) {
|
|
145
|
+
background: var(--pf-surface-2);
|
|
146
|
+
border-color: var(--pf-border);
|
|
147
|
+
color: var(--pf-text-2);
|
|
148
|
+
}
|
|
149
|
+
|
|
82
150
|
.pf-rail-body {
|
|
83
151
|
flex: 1;
|
|
84
152
|
min-height: 0;
|
package/src/framework/mount.js
CHANGED
|
@@ -5,7 +5,7 @@ import { attachViewerControls } from "./viewer-controls.js";
|
|
|
5
5
|
import { attachCutawayControls } from "./cutaway-controls.js";
|
|
6
6
|
import { attachRail } from "./rail.js";
|
|
7
7
|
import { attachMobileTabs } from "./mobile-tabs.js";
|
|
8
|
-
import { createTooltipPresenter } from "./tooltip.js";
|
|
8
|
+
import { createTooltipPresenter, attachButtonTooltips } from "./tooltip.js";
|
|
9
9
|
import { loadCamera } from "./view-state.js";
|
|
10
10
|
import { buildControls } from "./controls.js";
|
|
11
11
|
import { relevantParamKeys } from "./param-deps.js";
|
|
@@ -29,7 +29,11 @@ import { resolveDefaultView } from "./default-view.js";
|
|
|
29
29
|
|
|
30
30
|
// The mount handle, factored out so its shape is unit-testable without booting
|
|
31
31
|
// the full mount() pipeline (WASM + workers + DOM).
|
|
32
|
-
|
|
32
|
+
// The default no-op tooltip binding, so a host can hold on to whatever
|
|
33
|
+
// attachTooltips returned without caring whether this mount resolved one.
|
|
34
|
+
const NOOP_TOOLTIP_BINDING = { sync: () => {}, hide: () => {}, detach: () => {} };
|
|
35
|
+
|
|
36
|
+
export function makeHandle({ ready, dispose, viewer, setParams, listExportableParts, exportParts, setHostPane, animation, getView, setView, captureView, attachTooltips }) {
|
|
33
37
|
return {
|
|
34
38
|
ready, dispose, setParams,
|
|
35
39
|
// Part-declared animation playback (spec 2026-08-02): animations are
|
|
@@ -60,6 +64,12 @@ export function makeHandle({ ready, dispose, viewer, setParams, listExportablePa
|
|
|
60
64
|
// (partforge-cloud does, at the window level). Defaulted to a no-op so the
|
|
61
65
|
// handle's shape never depends on whether this mount resolved a rail.
|
|
62
66
|
setHostPane: setHostPane ?? (() => {}),
|
|
67
|
+
// Join host-owned chrome buttons to this mount's shared hover tooltip, so
|
|
68
|
+
// a host's own viewbar/rail-foot buttons match the built-in ones. Entries
|
|
69
|
+
// are [{ element, getLabel? }] (label falls back to the button's
|
|
70
|
+
// title/aria-label); returns { sync, hide, detach }. Same no-op default
|
|
71
|
+
// stance as setHostPane above.
|
|
72
|
+
attachTooltips: attachTooltips ?? (() => NOOP_TOOLTIP_BINDING),
|
|
63
73
|
};
|
|
64
74
|
}
|
|
65
75
|
|
|
@@ -116,6 +126,15 @@ function createCleanupStack() {
|
|
|
116
126
|
// runtime.setHostPane("rail"); // narrow layout only: show just the controls
|
|
117
127
|
// // rail ('stage' | 'rail'), suppressing the
|
|
118
128
|
// // built-in tab bar. null hands selection back.
|
|
129
|
+
// runtime.attachTooltips([{ element: myButton }]); // host chrome buttons join the
|
|
130
|
+
// // mount's shared hover tooltip (the viewbar one).
|
|
131
|
+
// // Label = the button's title (or aria-label), or a
|
|
132
|
+
// // per-entry getLabel(); the title attribute is
|
|
133
|
+
// // absorbed while attached so it can't double up as a
|
|
134
|
+
// // native tooltip, and restored on detach. Returns
|
|
135
|
+
// // { sync, hide, detach } — call sync() after you
|
|
136
|
+
// // toggle a button's disabled state. Detached
|
|
137
|
+
// // automatically on dispose().
|
|
119
138
|
// runtime.setActive(false); // park the viewer: stop the render loop and release
|
|
120
139
|
// // both large GPU allocations (the drawing buffer and
|
|
121
140
|
// // the cached capture target). For a host that hides the
|
|
@@ -203,7 +222,7 @@ export function mount(part, { createWorker, elements = {}, onBuild, onPick, onDo
|
|
|
203
222
|
cleanup.defer(() => cutawayChrome.detach());
|
|
204
223
|
// Resizable/collapsible controls rail. No-ops when the host lays out the
|
|
205
224
|
// framework itself (no #panel / no elements.rail).
|
|
206
|
-
const railChrome = attachRail({ rail: els.rail, toggle: els.chrome.railToggle, shell: els.shell });
|
|
225
|
+
const railChrome = attachRail({ rail: els.rail, toggle: els.chrome.railToggle, shell: els.shell, tooltip });
|
|
207
226
|
cleanup.defer(() => railChrome.detach());
|
|
208
227
|
// Narrow-layout pane tabs. Below RAIL_NARROW_BREAKPOINT the rail cannot sit
|
|
209
228
|
// beside the viewer, so exactly one pane shows and this bar picks it. Same
|
|
@@ -639,8 +658,19 @@ export function mount(part, { createWorker, elements = {}, onBuild, onPick, onDo
|
|
|
639
658
|
}
|
|
640
659
|
};
|
|
641
660
|
|
|
661
|
+
// Host chrome buttons joining the mount's shared tooltip (the one the
|
|
662
|
+
// viewbar and cutaway buttons already use). Bindings are detached by
|
|
663
|
+
// dispose() via the cleanup stack; a host detaching earlier is fine —
|
|
664
|
+
// attachButtonTooltips.detach is idempotent.
|
|
665
|
+
const attachHostTooltips = (entries) => {
|
|
666
|
+
const binding = attachButtonTooltips(tooltip, entries);
|
|
667
|
+
cleanup.defer(() => binding.detach());
|
|
668
|
+
return binding;
|
|
669
|
+
};
|
|
670
|
+
|
|
642
671
|
return makeHandle({
|
|
643
672
|
ready, dispose, viewer, setParams,
|
|
673
|
+
attachTooltips: attachHostTooltips,
|
|
644
674
|
setHostPane: paneTabs.setHostPane,
|
|
645
675
|
getView: view, // () => tabsCtl.current()
|
|
646
676
|
setView: (name) => tabsCtl.select(name),
|
package/src/framework/rail.js
CHANGED
|
@@ -2,6 +2,7 @@ import {
|
|
|
2
2
|
RAIL_DEFAULT_WIDTH, RAIL_MIN_WIDTH, RAIL_NARROW_BREAKPOINT,
|
|
3
3
|
clampRailWidth, railMaxWidth, readRailPref, resolveRailDrag, writeRailPref,
|
|
4
4
|
} from "./rail-state.js";
|
|
5
|
+
import { attachButtonTooltips } from "./tooltip.js";
|
|
5
6
|
|
|
6
7
|
const KEY_STEP = 16;
|
|
7
8
|
const KEY_STEP_SHIFT = 64;
|
|
@@ -75,7 +76,11 @@ function safeStorage() {
|
|
|
75
76
|
//
|
|
76
77
|
// Everything is optional. With no rail this returns a no-op, so hosts that lay
|
|
77
78
|
// the framework out themselves (see embed-test.html) are unaffected.
|
|
78
|
-
|
|
79
|
+
//
|
|
80
|
+
// `tooltip` is the mount's shared presenter (tooltip.js). With it, the toggle's
|
|
81
|
+
// Hide/Show-controls label renders as the same anchored pf-hover-tip the
|
|
82
|
+
// viewbar buttons use; without it, the label falls back to a native title.
|
|
83
|
+
export function attachRail({ rail, toggle, shell = rail?.parentElement, storage = safeStorage(), tooltip } = {}) {
|
|
79
84
|
if (!rail || !shell) {
|
|
80
85
|
// No rail to resolve in this document: --pf-rail-w still defaults to 288px
|
|
81
86
|
// from tokens.css, but nothing is reserving that space, so anything that
|
|
@@ -107,6 +112,12 @@ export function attachRail({ rail, toggle, shell = rail?.parentElement, storage
|
|
|
107
112
|
toggle.replaceChildren(svg);
|
|
108
113
|
toggleChevron = chevron;
|
|
109
114
|
}
|
|
115
|
+
// Attached BEFORE the first apply() below writes an aria-label, so the
|
|
116
|
+
// binding's original-attribute capture (what its detach() restores) sees the
|
|
117
|
+
// host's own markup, not our first label.
|
|
118
|
+
const tooltipBinding = toggle && tooltip
|
|
119
|
+
? attachButtonTooltips(tooltip, [{ element: toggle }])
|
|
120
|
+
: null;
|
|
110
121
|
|
|
111
122
|
const seam = document.createElement("div");
|
|
112
123
|
seam.className = "pf-rail-seam";
|
|
@@ -157,8 +168,11 @@ export function attachRail({ rail, toggle, shell = rail?.parentElement, storage
|
|
|
157
168
|
const label = collapsed ? "Show controls" : "Hide controls";
|
|
158
169
|
toggle.setAttribute("aria-expanded", String(!collapsed));
|
|
159
170
|
toggle.setAttribute("aria-label", label);
|
|
160
|
-
|
|
171
|
+
// The shared tooltip reads the aria-label at show time, so a native
|
|
172
|
+
// title would double up as a second, competing tooltip.
|
|
173
|
+
if (!tooltipBinding) toggle.title = label;
|
|
161
174
|
toggle.classList.toggle("on", collapsed);
|
|
175
|
+
tooltipBinding?.sync();
|
|
162
176
|
}
|
|
163
177
|
if (persist) writeRailPref(state, storage);
|
|
164
178
|
}
|
|
@@ -324,6 +338,9 @@ export function attachRail({ rail, toggle, shell = rail?.parentElement, storage
|
|
|
324
338
|
shell.removeAttribute("data-pf-dragging");
|
|
325
339
|
rail.removeAttribute("inert");
|
|
326
340
|
root.style.removeProperty("--pf-rail-w");
|
|
341
|
+
// Before the attribute restore below: the binding's own detach rewrites
|
|
342
|
+
// title/aria-label from its capture, and ours must win.
|
|
343
|
+
tooltipBinding?.detach();
|
|
327
344
|
if (toggle) {
|
|
328
345
|
toggle.innerHTML = toggleOriginal.html;
|
|
329
346
|
toggle.title = toggleOriginal.title;
|