@floless/app 0.66.0 → 0.66.1

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.
@@ -53022,7 +53022,7 @@ function appVersion() {
53022
53022
  return resolveVersion({
53023
53023
  isSea: isSea2(),
53024
53024
  sqVersionXml: readSqVersionXml(),
53025
- define: true ? "0.66.0" : void 0,
53025
+ define: true ? "0.66.1" : void 0,
53026
53026
  pkgVersion: readPkgVersion()
53027
53027
  });
53028
53028
  }
@@ -53032,7 +53032,7 @@ function resolveChannel(s) {
53032
53032
  return "dev";
53033
53033
  }
53034
53034
  function appChannel() {
53035
- return resolveChannel({ isSea: isSea2(), define: true ? "0.66.0" : void 0 });
53035
+ return resolveChannel({ isSea: isSea2(), define: true ? "0.66.1" : void 0 });
53036
53036
  }
53037
53037
 
53038
53038
  // workflow-update.ts
@@ -2146,8 +2146,8 @@ function onMove(e) {
2146
2146
  if (pending.epDrag) { onMoveEndpoint(e); return; }
2147
2147
  if (!pending.grab || !pending.origMm) return;
2148
2148
  if (!dragging && Math.hypot(e.clientX - downXY[0], e.clientY - downXY[1]) <= DRAG_TOL_PX) return;
2149
- const canDrag = pending.copy || (api && api.dragMoveEnabled && api.dragMoveEnabled());
2150
- if (!canDrag) { if (!pending._hinted) { pending._hinted = true; if (api && api.dragMoveHint) api.dragMoveHint(); } return; } // drag-move OFF, plain grab → never translate; onUp selects the member (dragging stays false)
2149
+ const canDrag = api && api.dragMoveEnabled && api.dragMoveEnabled(); // the toggle gates BOTH gestures — plain drag (move) AND Ctrl+drag (copy)
2150
+ if (!canDrag) { if (!pending._hinted) { pending._hinted = true; if (api && api.dragMoveHint) api.dragMoveHint(); } return; } // drag-move/copy OFF → never translate; onUp selects the grabbed member (dragging stays false)
2151
2151
  if (!dragging) pending.mode = pending.copy ? 'copy' : (e.altKey ? 'vertical' : 'plan'); // lock the mode at drag start (copy is plan-only; Alt = vertical/elevation)
2152
2152
  dragging = pending;
2153
2153
  if (pending.mode === 'vertical') { onMoveVertical(e); return; }
@@ -2244,7 +2244,7 @@ function onUp(e) {
2244
2244
  return;
2245
2245
  }
2246
2246
  if (!wasDragging) {
2247
- if (p.id && !p.geo && !p.epDrag && !p.copy && moved > DRAG_TOL_PX && api && api.onSelect) { api.onSelect(p.id, false); return; } // a blocked drag (drag-move OFF) selects the GRABBED member, not whatever's under the release point
2247
+ if (p.id && !p.geo && !p.epDrag && moved > DRAG_TOL_PX && api && api.onSelect) { api.onSelect(p.id, p.ctrl); return; } // a blocked drag (drag-move/copy OFF) acts on the GRABBED member, not whatever's under the release point (Ctrl toggles it)
2248
2248
  clickSelect(e.clientX, e.clientY, p.ctrl); return; // click → cycle-pick: member, or a derived part stacked on it at a joint (Ctrl+click = additive select)
2249
2249
  }
2250
2250
  if (p.copy) { // Ctrl+drag → commit a copy at the plan delta (the editor clones + selects); the ghost was the preview
@@ -2343,10 +2343,10 @@ function updateStatusChip() {
2343
2343
  else if (isolatedIds) txt = `Isolated: ${isolatedIds.size} · Show all to restore`;
2344
2344
  // A read-only host (the vector 3D viewer) supplies its own idle hint — the default speaks editing
2345
2345
  // verbs (move/stretch/elevate) a viewer's no-op api would silently ignore. Otherwise the default
2346
- // tells the truth about the drag-move toggle (grab-to-move ON vs click-to-select + Ctrl+drag-copy).
2346
+ // tells the truth about the drag-move/copy toggle (ON = drag-move + Ctrl-drag-copy; OFF = click-select).
2347
2347
  else if (api && typeof api.statusHint === 'function' && api.statusHint()) txt = api.statusHint();
2348
2348
  else { const canDrag = api && api.dragMoveEnabled && api.dragMoveEnabled();
2349
- txt = (canDrag ? 'Drag to move · ' : 'Click to select · Ctrl+drag to copy · ') + 'ends to stretch · Alt+drag elevation · right-drag orbit · dbl-click to zoom in'; }
2349
+ txt = (canDrag ? 'Drag to move · Ctrl+drag to copy · ' : 'Click to select · ') + 'ends to stretch · Alt+drag elevation · right-drag orbit · dbl-click to zoom in'; }
2350
2350
  hoverChip.textContent = txt;
2351
2351
  hoverChip.style.left = (rect.left + rect.width / 2) + 'px'; hoverChip.style.transform = 'translateX(-50%)';
2352
2352
  hoverChip.style.top = (rect.bottom - 30) + 'px';
@@ -98,10 +98,10 @@
98
98
  #moreMenu .msnap-sect{display:none} #moreMenu .msnap-sect.open{display:block}
99
99
  #moreMenu button.msnap{display:flex;align-items:center;gap:0}
100
100
  #moreMenu button.msnap.on{color:var(--text)} /* the switch carries the state — don't also brand the text (reads as an armed tool elsewhere in this menu) */
101
- #moreMenu .mck{position:relative;width:26px;height:14px;margin-right:9px;border-radius:7px;border:1px solid var(--line);background:#0b1220;flex:none;transition:background-color .15s,border-color .15s}
102
- #moreMenu .mck::after{content:'';position:absolute;top:1px;left:1px;width:10px;height:10px;border-radius:50%;background:var(--mut);transition:transform .15s,background-color .15s}
103
- #moreMenu button.msnap.on .mck{background:rgba(59,130,246,.28);border-color:var(--brand)}
104
- #moreMenu button.msnap.on .mck::after{transform:translateX(12px);background:var(--brand)}
101
+ #moreMenu .mck,.cmmenu .mck{position:relative;width:26px;height:14px;margin-right:9px;border-radius:7px;border:1px solid var(--line);background:#0b1220;flex:none;transition:background-color .15s,border-color .15s} /* delicate CSS-only slider switch — shared by the ⋯ Snapping rows and the Move/Copy → Drag-to-move/copy toggle */
102
+ #moreMenu .mck::after,.cmmenu .mck::after{content:'';position:absolute;top:1px;left:1px;width:10px;height:10px;border-radius:50%;background:var(--mut);transition:transform .15s,background-color .15s}
103
+ #moreMenu button.msnap.on .mck,.cmmenu #dragMoveB.on .mck{background:rgba(59,130,246,.28);border-color:var(--brand)}
104
+ #moreMenu button.msnap.on .mck::after,.cmmenu #dragMoveB.on .mck::after{transform:translateX(12px);background:var(--brand)}
105
105
  #moreMenu button.msnap .sg{display:inline-block;width:17px;color:#22d3ee;opacity:.5;flex:none;transition:opacity .15s}
106
106
  #moreMenu button.msnap.on .sg{opacity:1}
107
107
  /* Quick-access snap bar — always-expanded row of glyph toggles, bottom-right of the canvas (both views); reuses the brand-fill "on" toolbar language */
@@ -161,9 +161,9 @@
161
161
  .cmwrap{position:relative;display:inline-flex}
162
162
  #xfB{white-space:nowrap}
163
163
  #xfB.on{background:var(--brand);border-color:var(--brand);color:#fff} /* a transform tool is armed */
164
- .cmmenu .mstate{margin-left:auto;font-size:11px;color:var(--mut);padding-left:12px} /* the Drag-to-move on/off state, right-aligned in its menu row */
165
- .cmmenu #dragMoveB.on .mstate{color:var(--brand)}
166
- .cmmenu #dragMoveB.on{color:var(--text)}
164
+ /* Drag-to-move/copy menu item wears the shared .mck slider switch (rules above, shared with Snapping). */
165
+ .cmmenu #dragMoveB{display:flex;align-items:center;justify-content:flex-start;gap:0}
166
+ .cmmenu #dragMoveB.on{color:var(--text)} /* the switch carries the state — don't also brand the text (reads as an armed tool) */
167
167
  .cmmenu{display:none;position:absolute;left:0;top:calc(100% + 6px);min-width:200px;background:var(--panel);border:1px solid #475569;border-radius:8px;box-shadow:0 8px 24px rgba(0,0,0,.5);padding:4px 0;z-index:30}
168
168
  .cmmenu.open{display:block}
169
169
  .cmmenu button{display:flex;justify-content:space-between;gap:12px;width:100%;text-align:left;background:transparent;border:0;border-radius:0;padding:7px 12px;color:var(--text);white-space:nowrap}
@@ -369,7 +369,7 @@
369
369
  <button id=cpArrB>Copy array…</button>
370
370
  <button id=cpLevelB>Copy to level…</button>
371
371
  <div class=mlabel>Dragging</div>
372
- <button id=dragMoveB role=menuitemcheckbox aria-checked=false title="When ON, left-dragging a member moves it. When OFF (default), a click only selects members can't be nudged by accident (Move and Ctrl+drag-copy still work). Applies to 2D and 3D.">Drag to move<span class=mstate id=dragMoveState>Off</span></button>
372
+ <button id=dragMoveB role=menuitemcheckbox aria-checked=false title="When ON, left-dragging a member moves it and Ctrl+drag copies it. When OFF (default), a drag does neither — a click only selects, so members can't be nudged by accident (the Move and Copy tools still work). Applies to 2D and 3D."><span class=mck aria-hidden=true></span>Drag to move/copy</button>
373
373
  </div>
374
374
  </div>
375
375
  <button id=askAiBtn>Ask AI ▸</button>
@@ -1809,12 +1809,13 @@ function dimRefreshPrev(){if(!dimMode||!dimDraft||!dimLastPtr)return;
1809
1809
  // array N×M. Same shell as the Dimension tool: armed flag routes the pointer handlers, the preview
1810
1810
  // lives in its own <g> (no render() until commit), commits go through edit()/pushUndo. ---
1811
1811
  let cmTool=null,cmArrayMode=false,cmDraft=null,cmAxis='free',cmCount=1,cmCountA=2,cmCountB=2,cmLastPtr=null,cmLastEvt=null;
1812
- // Drag-to-move: OFF by default so a click only selects (no accidental nudges). Persisted per-browser,
1813
- // global (a UI pref, not per-app). Ctrl+drag-copy ignores this it's an explicit gesture.
1812
+ // Drag-to-move/copy: OFF by default so a click only selects (no accidental nudges). Persisted
1813
+ // per-browser, global (a UI pref, not per-app). Gates BOTH drag gesturesleft-drag=move AND
1814
+ // Ctrl+drag=copy; only Ctrl+CLICK (a click, not a drag) still toggles selection when off.
1814
1815
  let dragMoveOn=false; try{dragMoveOn=localStorage.getItem('steel:dragmove:v1')==='1';}catch(_){}
1815
1816
  let dragHintShown=false; try{dragHintShown=localStorage.getItem('steel:dragmovehint:v1')==='1';}catch(_){} // one-time "drag-move is off" teach on the first blocked drag
1816
1817
  function setDragMove(on){dragMoveOn=!!on;document.body.classList.toggle('dragmove',dragMoveOn);
1817
- const b=document.getElementById('dragMoveB');if(b){b.classList.toggle('on',dragMoveOn);b.setAttribute('aria-checked',String(dragMoveOn));const st=document.getElementById('dragMoveState');if(st)st.textContent=dragMoveOn?'On':'Off';}
1818
+ const b=document.getElementById('dragMoveB');if(b){b.classList.toggle('on',dragMoveOn);b.setAttribute('aria-checked',String(dragMoveOn));} // the .mck slider carries the on/off state
1818
1819
  try{localStorage.setItem('steel:dragmove:v1',dragMoveOn?'1':'0');}catch(_){}}
1819
1820
  // cmDraft: {base:[x,y], vA:null} — array mode sets vA=[dx,dy,0] once direction A is picked/typed
1820
1821
  function setCmUi(){document.body.classList.toggle('cmon',!!cmTool);
@@ -2093,7 +2094,7 @@ svg.addEventListener('pointerdown',e=>{if(e.button!==0)return;const t=e.target;
2093
2094
  // Plain click selects on down (so a no-drag click still selects even with drag-move off). Ctrl defers
2094
2095
  // its select to up — a ctrl-DRAG copies, a ctrl-CLICK toggles selection (today's behavior).
2095
2096
  if(!ctrl&&!selIds.has(id)){selIds=new Set([id]);render();}
2096
- const canDrag=ctrl||dragMoveOn; // plain grab only translates when the toggle is on; ctrl always drags (to copy)
2097
+ const canDrag=dragMoveOn; // the toggle gates BOTH gestures: left-drag=move and Ctrl+drag=copy (Ctrl+CLICK still toggles selection below — a click, not a drag)
2097
2098
  const srcIds=(ctrl&&!selIds.has(id))?new Set([id]):new Set(selArr().map(m=>m.id)); // ctrl on an unselected member copies just it; else the whole selection
2098
2099
  const items=[...srcIds].map(mid=>{const m=byId(mid);return{id:mid,o0:m.wp[0].slice(),o1:m.wp[1].slice()};});
2099
2100
  buildSnap(srcIds); // snap the dragged line(s) to OTHER members'/segments' endpoints
@@ -2200,7 +2201,7 @@ svg.addEventListener('pointerup',()=>{if(!drag)return;snapClear();
2200
2201
  drag=null;render();return;}
2201
2202
  if(!drag.armed){ // dragged but drag-move OFF → nothing moved; teach the toggle once
2202
2203
  drag=null;render();
2203
- if(!dragHintShown){dragHintShown=true;try{localStorage.setItem('steel:dragmovehint:v1','1');}catch(_){}toast('Drag-to-move is off — turn it on in the toolbar, or use Move (M)');}
2204
+ if(!dragHintShown){dragHintShown=true;try{localStorage.setItem('steel:dragmovehint:v1','1');}catch(_){}toast('Drag to move/copy is off — turn it on in the Move / Copy menu, or use the Move (M) / Copy (C) tools');}
2204
2205
  return;}
2205
2206
  if(drag.pre&&snapshot()!==drag.pre)pushUndo(drag.pre); // armed move/copy committed (copy always changed geometry via its clones)
2206
2207
  const wasCopy=drag.copy;if(!anyToolActive())snapOnlyClear2d();drag=null;render();if(wasCopy)toast('Copied '+selIds.size+' member'+(selIds.size===1?'':'s'));return;} // a grab move/copy was the operation → single-shot override clears
@@ -2242,7 +2243,7 @@ document.getElementById('mrgB').onclick=()=>{const r=mergeCollinearJS(P.members)
2242
2243
  toast('Merged '+r.mergedAway+' segment'+(r.mergedAway===1?'':'s')+' into '+r.runs+' member'+(r.runs===1?'':'s'));};
2243
2244
  document.getElementById('undoB').onclick=doUndo;
2244
2245
  document.getElementById('redoB').onclick=doRedo;
2245
- document.getElementById('dragMoveB').onclick=()=>{setDragMove(!dragMoveOn);toast(dragMoveOn?'Drag to move: ON — dragging a member moves it':'Drag to move: OFF — a click only selects');};
2246
+ document.getElementById('dragMoveB').onclick=()=>{setDragMove(!dragMoveOn);toast(dragMoveOn?'Drag to move/copy: ON — drag moves, Ctrl+drag copies':'Drag to move/copy: OFF — a click only selects');};
2246
2247
  setDragMove(dragMoveOn); // reflect the persisted state on load (body class + pill)
2247
2248
  addEventListener('keydown',e=>{
2248
2249
  const inForm=/^(INPUT|SELECT|TEXTAREA)$/.test((document.activeElement||{}).tagName);
@@ -2357,7 +2358,7 @@ const view3dApi={
2357
2358
  geoMode:()=>geoMode, // 'el' | 'split' | null — armed Trim/Extend/Split state
2358
2359
  onMoveMember:(id,newWp)=>{edit(()=>{const m=byId(id);if(m)m.wp=newWp;});}, // 3D drag → write wp (undoable, autosaves, syncs 2D/chip/BOM)
2359
2360
  dragMoveEnabled:()=>dragMoveOn, // the 3D view gates its plain member-drag on this
2360
- dragMoveHint:()=>{if(dragHintShown)return;dragHintShown=true;try{localStorage.setItem('steel:dragmovehint:v1','1');}catch(_){}toast('Drag-to-move is off — turn it on in the toolbar, or use Move (M)');},
2361
+ dragMoveHint:()=>{if(dragHintShown)return;dragHintShown=true;try{localStorage.setItem('steel:dragmovehint:v1','1');}catch(_){}toast('Drag to move/copy is off — turn it on in the Move / Copy menu, or use the Move (M) / Copy (C) tools');},
2361
2362
  onCopyDrag3d:(ids,dxMm,dyMm)=>{const k=304.8/((P&&P.pt_per_ft>0)?P.pt_per_ft:1),r6=n=>Math.round(n*1e6)/1e6; // Ctrl+drag copy: mm delta → plan px (Y FLIPPED), clone + translate + select
2362
2363
  const dpx=r6(dxMm/k),dpy=r6(-dyMm/k);
2363
2364
  edit(()=>{const ns=new Set();for(const id of (ids||[])){const src=byId(id);if(!src)continue;const c=cloneMember(src);translateMembers([c],[dpx,dpy,0]);P.members.push(c);ns.add(c.id);}selIds=ns;});
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@floless/app",
3
- "version": "0.66.0",
3
+ "version": "0.66.1",
4
4
  "type": "module",
5
5
  "description": "Thin localhost host for floless.app — serves web/ and shells the aware CLI. No engine, no LLM.",
6
6
  "bin": {