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 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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "partforge",
3
- "version": "0.50.0",
3
+ "version": "0.51.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",
@@ -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;
@@ -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
- export function makeHandle({ ready, dispose, viewer, setParams, listExportableParts, exportParts, setHostPane, animation, getView, setView, captureView }) {
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),
@@ -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
- export function attachRail({ rail, toggle, shell = rail?.parentElement, storage = safeStorage() } = {}) {
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
- toggle.title = label;
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;