@floless/app 0.83.0 → 0.85.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/dist/floless-server.cjs +188 -89
- package/dist/templates/steel-model.flo +6 -0
- package/dist/templates/vectorize.flo +6 -0
- package/dist/web/analytics.js +31 -0
- package/dist/web/app.css +19 -2
- package/dist/web/aware.js +167 -17
- package/dist/web/index.html +13 -2
- package/dist/web/steel-3d-view.js +10 -0
- package/dist/web/steel-editor.html +5 -0
- package/dist/web/vector-editor.html +29 -7
- package/dist/web/workspaces.js +283 -37
- package/package.json +1 -1
package/dist/web/app.css
CHANGED
|
@@ -3322,10 +3322,13 @@ body {
|
|
|
3322
3322
|
.mode-switch button.active { background:var(--accent-soft); color:var(--accent-bright); }
|
|
3323
3323
|
|
|
3324
3324
|
/* Mode gating: Workflows chrome hides wholesale in Workspaces (hidden, never disabled).
|
|
3325
|
-
≡ keeps only app-wide items; Dashboard is Workflows-scoped (spec §2).
|
|
3325
|
+
≡ keeps only app-wide items; Dashboard is Workflows-scoped (spec §2).
|
|
3326
|
+
#guide-beacon is NOT hidden here — it's the ONE shared control that serves both modes:
|
|
3327
|
+
aware.js repaints it as the Workspaces document-guide (drop → review → approve → export)
|
|
3328
|
+
for the open project, and toggles its `hidden` attribute in JS (hidden on the landing). */
|
|
3326
3329
|
.app.mode-workspaces .controls > label,
|
|
3327
3330
|
.app.mode-workspaces .wf-combo, .app.mode-workspaces #wf-update, .app.mode-workspaces #wf-forked,
|
|
3328
|
-
.app.mode-workspaces #
|
|
3331
|
+
.app.mode-workspaces #run-state,
|
|
3329
3332
|
.app.mode-workspaces #restore-btn, .app.mode-workspaces #customize-btn,
|
|
3330
3333
|
.app.mode-workspaces #compile-btn, .app.mode-workspaces #sim-btn,
|
|
3331
3334
|
.app.mode-workspaces #run-btn, .app.mode-workspaces #stop-run-btn,
|
|
@@ -3378,10 +3381,24 @@ body {
|
|
|
3378
3381
|
color:var(--text-muted); font-size:12px; width:100%; text-align:center; padding:12px; }
|
|
3379
3382
|
.ws-seed:hover { background:var(--accent-soft); border-color:var(--accent); }
|
|
3380
3383
|
.ws-seed .plus { font-size:24px; color:var(--accent); line-height:1; }
|
|
3384
|
+
/* Drop-a-drawing seed card (vectorize): a click-to-pick + drag/drop target. .drag-over lights it up
|
|
3385
|
+
while a file hovers (same intent as the editor's .canvas-drop-target.active). */
|
|
3386
|
+
.ws-seed-drop.drag-over { background:var(--accent-soft); border-color:var(--accent); border-style:solid; }
|
|
3387
|
+
.ws-seed .ws-seed-headline { font-size:12.5px; font-weight:600; color:var(--text); line-height:1.35; }
|
|
3388
|
+
.ws-seed .ws-seed-sub { font-size:11px; color:var(--text-dim); line-height:1.4; max-width:220px; }
|
|
3381
3389
|
.ws-empty-note { color:var(--text-dim); font-size:12px; margin-top:10px; }
|
|
3382
3390
|
|
|
3383
3391
|
/* open project */
|
|
3384
3392
|
.ws-project { flex:1; min-height:0; display:flex; flex-direction:column; }
|
|
3393
|
+
/* Source-step placeholder — a filter-less workspace app (vectorize) shows this centered pane where
|
|
3394
|
+
steel loads its filter iframe. Muted + baseline-faithful; guides forward (not a dead control).
|
|
3395
|
+
The [hidden] guard is load-bearing: display:flex would otherwise override the hidden attribute. */
|
|
3396
|
+
.ws-source-placeholder { flex:1; min-height:0; display:flex; align-items:center; justify-content:center; padding:24px; }
|
|
3397
|
+
.ws-source-placeholder[hidden] { display:none; }
|
|
3398
|
+
.ws-source-inner { max-width:440px; text-align:center; }
|
|
3399
|
+
.ws-source-ico { font-size:34px; color:var(--accent); display:block; margin-bottom:12px; line-height:1; }
|
|
3400
|
+
.ws-source-title { font-size:14px; font-weight:600; color:var(--text); margin-bottom:6px; }
|
|
3401
|
+
.ws-source-sub { font-size:12px; color:var(--text-dim); line-height:1.55; }
|
|
3385
3402
|
.ws-crumb-row { padding:12px 14px 8px; display:flex; justify-content:space-between; align-items:center; }
|
|
3386
3403
|
.ws-crumb { font-size:12px; color:var(--text-muted); }
|
|
3387
3404
|
.ws-crumb-link { background:none; border:none; padding:0; color:var(--text-muted); font-size:12px; cursor:pointer; }
|
package/dist/web/aware.js
CHANGED
|
@@ -575,7 +575,66 @@
|
|
|
575
575
|
// The beacon pulses while a workflow is new to the user.
|
|
576
576
|
function beaconIsNew(id) { const g = guideFor(id); return !g.firstRunDone && !g.dismissed; }
|
|
577
577
|
|
|
578
|
+
// ── Workspaces variant of the SAME beacon ───────────────────────────────────
|
|
579
|
+
// Workspaces is the DOCUMENT archetype (drop → review → approve → export), not the
|
|
580
|
+
// pipeline archetype (inputs → compile → run). One beacon element, two content
|
|
581
|
+
// sources: in Workspaces mode with a project open, this layer drives #guide-beacon +
|
|
582
|
+
// #guide-modal instead of the workflow layer. It REUSES the LS_GUIDE store and its
|
|
583
|
+
// get/set helpers — no parallel system — but keys its record under `ws:<app>` so a
|
|
584
|
+
// Workflows Run of an app (firstRunDone) never calms its Workspaces beacon and vice
|
|
585
|
+
// versa (vectorize is BOTH a pipeline app and a workspace app). "New" here calms on the
|
|
586
|
+
// first Approve for the app, mirroring how the workflow beacon calms on the first Run.
|
|
587
|
+
// workspaces.js owns the project state, so it hands us a small snapshot (app id + which
|
|
588
|
+
// lifecycle signals are true) via flolessBridge; we own the chrome, glyphs, and copy.
|
|
589
|
+
let wsGuideCtx = null; // { app, name, displayName, approved, exported } | null (name = the open project's name)
|
|
590
|
+
const wsGuideKey = (app) => `ws:${app}`;
|
|
591
|
+
const inWorkspacesMode = () => { const a = document.getElementById('app'); return !!(a && a.classList.contains('mode-workspaces')); };
|
|
592
|
+
// The Workspaces beacon is active only in Workspaces mode with a project open (a ctx set).
|
|
593
|
+
const wsGuideActive = () => inWorkspacesMode() && !!wsGuideCtx;
|
|
594
|
+
function wsBeaconIsNew(app) { const g = guideFor(wsGuideKey(app)); return !g.approveDone && !g.dismissed; }
|
|
595
|
+
|
|
596
|
+
// The document-lifecycle checklist. Approve/Export carry an honest boolean from project
|
|
597
|
+
// state; "Review" borrows Approve's boolean (you can't sign off without having looked —
|
|
598
|
+
// Approve sits next to the editor), never a faked "opened the tab" signal; "Drop" is
|
|
599
|
+
// done whenever the guide is open (a project only exists after a drawing was dropped —
|
|
600
|
+
// a cancelled/failed drop is rolled back, never left as an empty project). The current
|
|
601
|
+
// step = the first not-done row, so the arrow lands on the next real action.
|
|
602
|
+
function workspaceGuideSteps(ctx) {
|
|
603
|
+
return [
|
|
604
|
+
{ label: 'Drop a drawing', note: 'PDF or image — vectors come out editable', done: true },
|
|
605
|
+
{ label: 'Review the vectors', note: 'check layers, isolate low-confidence traces', done: !!ctx.approved },
|
|
606
|
+
{ label: 'Approve', note: 'signs off this version, unlocks Export', done: !!ctx.approved },
|
|
607
|
+
{ label: 'Export SVG', note: 'clean vectors, take them anywhere', done: !!ctx.exported },
|
|
608
|
+
];
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
// Paint #guide-beacon for the open workspace project (called by paintBeacon when in
|
|
612
|
+
// Workspaces mode). Same glyphs/label/pulse dialect as the workflow beacon; names the
|
|
613
|
+
// open PROJECT (not the app id), matching the crumb/picker.
|
|
614
|
+
function paintWorkspaceBeacon() {
|
|
615
|
+
const $b = document.getElementById('guide-beacon');
|
|
616
|
+
if (!$b) return;
|
|
617
|
+
const ctx = wsGuideCtx;
|
|
618
|
+
if (!ctx) { $b.hidden = true; $b.classList.remove('is-new', 'spotlit'); return; }
|
|
619
|
+
const isNew = wsBeaconIsNew(ctx.app);
|
|
620
|
+
$b.hidden = false;
|
|
621
|
+
$b.classList.remove('spotlit');
|
|
622
|
+
$b.classList.toggle('is-new', isNew);
|
|
623
|
+
const ico = $b.querySelector('.guide-beacon-ico');
|
|
624
|
+
const label = $b.querySelector('.guide-beacon-label');
|
|
625
|
+
if (ico) ico.textContent = isNew ? '✦' : '?';
|
|
626
|
+
if (label) label.textContent = isNew ? 'Start here' : 'Guide';
|
|
627
|
+
// No "workflow"/"Run" vocabulary — this is a document project.
|
|
628
|
+
$b.dataset.tip = isNew
|
|
629
|
+
? 'New here? See what this project does and how to take it from drawing to export.'
|
|
630
|
+
: 'Open the guide for this project.';
|
|
631
|
+
$b.setAttribute('aria-label', (isNew ? 'Start here — guide for ' : 'Guide for ') + (ctx.name || ctx.displayName || ctx.app));
|
|
632
|
+
}
|
|
633
|
+
|
|
578
634
|
function paintBeacon(app) {
|
|
635
|
+
// In Workspaces mode the beacon belongs to the open project, not the last-loaded
|
|
636
|
+
// workflow — hand off to the workspace layer (which hides it when no project is open).
|
|
637
|
+
if (inWorkspacesMode()) { paintWorkspaceBeacon(); return; }
|
|
579
638
|
const $b = document.getElementById('guide-beacon');
|
|
580
639
|
if (!$b) return;
|
|
581
640
|
if (!app) { $b.hidden = true; $b.classList.remove('is-new', 'spotlit'); return; }
|
|
@@ -624,30 +683,66 @@
|
|
|
624
683
|
return steps;
|
|
625
684
|
}
|
|
626
685
|
|
|
686
|
+
// Shared checklist renderer — shape carries state (✓ done · → current · ☐ to-do), never
|
|
687
|
+
// colour alone. The current step is the first not-done row. Labels/notes are authored
|
|
688
|
+
// constants but still pass escapeHtml (defence-in-depth; the security hook forbids raw
|
|
689
|
+
// string→DOM). Both the workflow and workspace guides render through this.
|
|
690
|
+
function guideStepsHtml(steps) {
|
|
691
|
+
const curIdx = steps.findIndex((s) => !s.done);
|
|
692
|
+
return `<ol class="guide-steps" role="list">${steps.map((s, i) => {
|
|
693
|
+
const state = s.done ? 'done' : i === curIdx ? 'current' : 'todo';
|
|
694
|
+
const box = s.done ? '✓' : i === curIdx ? '→' : '☐';
|
|
695
|
+
const cur = i === curIdx ? ' aria-current="step"' : '';
|
|
696
|
+
return `<li class="guide-step ${state}"${cur}><span class="guide-box" aria-hidden="true">${box}</span>` +
|
|
697
|
+
`<span class="guide-step-text"><span class="guide-step-label">${escapeHtml(s.label)}</span> ` +
|
|
698
|
+
`<span class="guide-step-note">— ${escapeHtml(s.note)}</span></span>` +
|
|
699
|
+
`<span class="sr-only">${s.done ? '(done)' : i === curIdx ? '(next step)' : '(to do)'}</span></li>`;
|
|
700
|
+
}).join('')}</ol>`;
|
|
701
|
+
}
|
|
702
|
+
|
|
703
|
+
// The Workspaces guide body — the document life-cycle, not node mechanics. No
|
|
704
|
+
// "Compile"/"Run"/"Save" language (those pipeline verbs are absent here; the verb is
|
|
705
|
+
// Approve). "Walk me through it" is a Workflows relay (terminal AI + the workflow's own
|
|
706
|
+
// skill), so it's hidden for a workspace — the beacon is a self-contained checklist here.
|
|
707
|
+
// The title is the open project's user-authored name → set via textContent only (never
|
|
708
|
+
// into innerHTML); the body innerHTML is authored constants + escaped step labels/notes.
|
|
709
|
+
function renderWorkspaceGuide() {
|
|
710
|
+
const ctx = wsGuideCtx;
|
|
711
|
+
const $body = document.getElementById('guide-body');
|
|
712
|
+
const $title = document.getElementById('guide-title');
|
|
713
|
+
const $eyebrow = document.getElementById('guide-eyebrow');
|
|
714
|
+
const $sub = document.querySelector('#guide-modal .modal-sub');
|
|
715
|
+
if (!ctx || !$body) return;
|
|
716
|
+
if ($title) $title.textContent = ctx.name || ctx.displayName || ctx.app;
|
|
717
|
+
if ($eyebrow) $eyebrow.textContent = wsBeaconIsNew(ctx.app) ? '✦ Start here' : '✦ Project guide';
|
|
718
|
+
if ($sub) $sub.textContent = 'What this project does, and how to take it from drawing to export.';
|
|
719
|
+
const descHtml = `<div class="guide-desc"><p>Drop in a drawing — a PDF, scan, or photo — and it comes back as clean, editable 2D vectors. Anything the trace wasn’t sure about gets flagged for you to check before you approve and export.</p></div>`;
|
|
720
|
+
const stepsHtml = guideStepsHtml(workspaceGuideSteps(ctx));
|
|
721
|
+
$body.innerHTML =
|
|
722
|
+
`<div class="guide-section-label">What this does</div>` + descHtml +
|
|
723
|
+
`<div class="guide-section-label">How to go through it</div>` + stepsHtml;
|
|
724
|
+
}
|
|
725
|
+
|
|
627
726
|
function renderGuide() {
|
|
727
|
+
// In Workspaces mode with a project open, the guide describes the document life-cycle.
|
|
728
|
+
if (wsGuideActive()) { renderWorkspaceGuide(); return; }
|
|
628
729
|
const app = currentId && apps.get(currentId);
|
|
629
730
|
const $body = document.getElementById('guide-body');
|
|
630
731
|
const $title = document.getElementById('guide-title');
|
|
631
732
|
const $eyebrow = document.getElementById('guide-eyebrow');
|
|
733
|
+
const $sub = document.querySelector('#guide-modal .modal-sub');
|
|
632
734
|
if (!app || !$body) return;
|
|
633
735
|
if ($title) $title.textContent = app.displayName || app.id;
|
|
634
736
|
if ($eyebrow) $eyebrow.textContent = beaconIsNew(app.id) ? '✦ Start here' : '✦ Workflow guide';
|
|
737
|
+
// Restore the workflow sub in case a prior Workspaces open overwrote it.
|
|
738
|
+
if ($sub) $sub.textContent = 'What this workflow does, and how to go through it.';
|
|
635
739
|
const desc = (app.description || '').trim();
|
|
636
740
|
const descHtml = desc
|
|
637
741
|
? `<div class="guide-desc">${desc.split(/\n{2,}/).map((p) => `<p>${mdInline(escapeHtml(p.trim()))}</p>`).join('')}</div>`
|
|
638
742
|
: `<div class="guide-desc guide-desc-empty"><p>A ${app.nodes.length}-step workflow. Set its inputs, Compile to freeze the approved lock, then Run it against your live host.</p></div>`;
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
const stepsHtml =
|
|
642
|
-
const state = s.done ? 'done' : i === curIdx ? 'current' : 'todo';
|
|
643
|
-
// Shape carries state, not colour alone: ✓ done · → current · ☐ to-do.
|
|
644
|
-
const box = s.done ? '✓' : i === curIdx ? '→' : '☐';
|
|
645
|
-
const cur = i === curIdx ? ' aria-current="step"' : '';
|
|
646
|
-
return `<li class="guide-step ${state}"${cur}><span class="guide-box" aria-hidden="true">${box}</span>` +
|
|
647
|
-
`<span class="guide-step-text"><span class="guide-step-label">${escapeHtml(s.label)}</span> ` +
|
|
648
|
-
`<span class="guide-step-note">— ${escapeHtml(s.note)}</span></span>` +
|
|
649
|
-
`<span class="sr-only">${s.done ? '(done)' : i === curIdx ? '(next step)' : '(to do)'}</span></li>`;
|
|
650
|
-
}).join('')}</ol>`;
|
|
743
|
+
// Static authored strings + escapeHtml on every dynamic value (app.description, step
|
|
744
|
+
// label/note) — no raw user string reaches the DOM (the established guide pattern).
|
|
745
|
+
const stepsHtml = guideStepsHtml(guideSteps(app));
|
|
651
746
|
const walkHint = `<div class="guide-walk-hint">A bigger workflow? <strong>Walk me through it</strong> hands off to your terminal AI for a step-by-step tour using this workflow's own skill.</div>`;
|
|
652
747
|
$body.innerHTML =
|
|
653
748
|
`<div class="guide-section-label">What this does</div>` + descHtml +
|
|
@@ -655,9 +750,15 @@
|
|
|
655
750
|
}
|
|
656
751
|
|
|
657
752
|
function openGuide() {
|
|
658
|
-
|
|
753
|
+
const ws = wsGuideActive();
|
|
754
|
+
if (!ws && !currentId) { showToast('open a workflow first', 'warn'); return; }
|
|
659
755
|
renderGuide();
|
|
660
756
|
const $m = document.getElementById('guide-modal');
|
|
757
|
+
// "Walk me through it →" relays to the terminal AI using a WORKFLOW's own skill — a
|
|
758
|
+
// pipeline concept with no document-workspace analog. Hide it (never disable — an
|
|
759
|
+
// absent control, not a dead one) for the self-contained workspace guide.
|
|
760
|
+
const $walk = document.getElementById('guide-walk');
|
|
761
|
+
if ($walk) $walk.hidden = ws;
|
|
661
762
|
showModal($m);
|
|
662
763
|
// Focus the dialog shell (not a button) so the checklist is read first; Tab then
|
|
663
764
|
// moves to Got it → Walk me through it. Focus returns to the beacon on close.
|
|
@@ -665,9 +766,13 @@
|
|
|
665
766
|
if (shell) { try { shell.focus(); } catch { /* not focusable */ } }
|
|
666
767
|
}
|
|
667
768
|
// Closing the guide (Got it / backdrop / Esc) marks it seen → the beacon stops
|
|
668
|
-
// pulsing even before a first run (the user has looked; don't nag).
|
|
769
|
+
// pulsing even before a first run/approve (the user has looked; don't nag). In
|
|
770
|
+
// Workspaces mode this marks the project's app workspace-guide key, not the workflow.
|
|
669
771
|
function closeGuide() {
|
|
670
|
-
if (
|
|
772
|
+
if (wsGuideActive()) {
|
|
773
|
+
setGuide(wsGuideKey(wsGuideCtx.app), { dismissed: true });
|
|
774
|
+
paintWorkspaceBeacon();
|
|
775
|
+
} else if (currentId) {
|
|
671
776
|
setGuide(currentId, { dismissed: true });
|
|
672
777
|
const app = apps.get(currentId);
|
|
673
778
|
if (app) paintBeacon(app);
|
|
@@ -4660,7 +4765,14 @@
|
|
|
4660
4765
|
? `\nRevised drawing${req.snapshots.length > 1 ? 's' : ''} (read these): ${req.snapshots.join(', ')}`
|
|
4661
4766
|
: '';
|
|
4662
4767
|
const note = req.instruction ? `\nMy note: ${req.instruction}` : '';
|
|
4663
|
-
|
|
4768
|
+
const post = `Then POST /api/projects/${req.project}/revision-read with { contract, message: "<plain-English what changed>", requestId: "${req.id}" }. The server derives the base version + source provenance from this queued request, records a "revision-read" version, and clears the request — do NOT DELETE the request yourself. If you can't read the set, POST /api/projects/${req.project}/revision-requests/${req.id}/fail with { error }.`;
|
|
4769
|
+
// App-aware: the target contract shape differs per workspace app. A vectorize project holds a
|
|
4770
|
+
// drawing.vector/v1 (clean editable 2D linework), NOT a steel takeoff — instructing the AI to
|
|
4771
|
+
// read steel members would produce the wrong contract for the vector editor (spec §3.3g).
|
|
4772
|
+
if (req.appId === 'vectorize') {
|
|
4773
|
+
return `In floless project "${req.project}" (app "${req.appId}"), a REVISED drawing was attached — re-read (vectorize) it into the SAME project as a new version (do NOT create a new project). Vectorize the drawing per the floless-app-vectorize skill into a drawing.vector/v1 contract: emit clean, editable 2D vector geometry (sheets → elements: lines / polylines / curves / text, each on its layer), NOT steel members or a takeoff. A vector PDF is read deterministically (exact geometry, no guessing); a photo, scan or hand sketch is traced by a fenced vision extraction that flags every uncertain stroke (low confidence) for the user's review. ${post}${snaps}${note}`;
|
|
4774
|
+
}
|
|
4775
|
+
return `In floless project "${req.project}" (app "${req.appId}"), a REVISED drawing set was attached — re-read it into the SAME project as a new version (do NOT create a new project). Read the drawings per the floless-app-steel-takeoff skill, compose the updated steel.takeoff/v1 contract. First GET /api/contract/${req.appId}?project=${req.project} to read the CURRENT contract and PRESERVE each still-present member's id AND its sheet label (mint a NEW id only for a genuinely new member; drop the id of a member the revision deleted) so the version comparison matches members — do NOT renumber the whole model. ${post}${snaps}${note}`;
|
|
4664
4776
|
}
|
|
4665
4777
|
return '';
|
|
4666
4778
|
}
|
|
@@ -4688,7 +4800,10 @@
|
|
|
4688
4800
|
function markedInstruction(req) {
|
|
4689
4801
|
const body = instructionFor(req);
|
|
4690
4802
|
if (!body) return '';
|
|
4691
|
-
|
|
4803
|
+
// revision-read's skill is app-specific: steel takeoff by default, floless-app-vectorize for a
|
|
4804
|
+
// vector project — keep the clipboard marker in step with the app-aware instruction body above.
|
|
4805
|
+
let skill = REQUEST_SKILL[req.type] || 'floless-app-workflows';
|
|
4806
|
+
if (req.type === 'revision-read' && req.appId === 'vectorize') skill = 'floless-app-vectorize';
|
|
4692
4807
|
const base = (typeof location !== 'undefined' && location.origin) ? location.origin : 'http://127.0.0.1:4317';
|
|
4693
4808
|
// A revision-read is cleared by its own /revision-read POST — a separate DELETE would 404. Every
|
|
4694
4809
|
// other type is applied-then-DELETEd by the terminal AI.
|
|
@@ -6459,6 +6574,41 @@
|
|
|
6459
6574
|
instructionFor,
|
|
6460
6575
|
markedInstruction, // marked (paste-safe) form for clipboard copies — panels.js uses it (#73)
|
|
6461
6576
|
formModal, // the shared styled prompt/form modal — workspaces.js reuses it (never a native prompt)
|
|
6577
|
+
// ── Workspaces guide beacon (the SAME #guide-beacon/#guide-modal, document variant) ──
|
|
6578
|
+
// workspaces.js owns the open project + its lifecycle state; aware.js owns the beacon
|
|
6579
|
+
// chrome. workspaces.js calls setWorkspaceGuide() with a small snapshot whenever the
|
|
6580
|
+
// open project (or its approve/export state) changes, clearWorkspaceGuide() on close/
|
|
6581
|
+
// Workflows-mode, and markWorkspaceApproved() the first time an Approve succeeds (which
|
|
6582
|
+
// calms the beacon, mirroring markFirstRunDone for a workflow's first Run).
|
|
6583
|
+
setWorkspaceGuide(ctx) {
|
|
6584
|
+
// ctx: { app, name, displayName?, approved, exported }. Null/!app → treat as clear.
|
|
6585
|
+
wsGuideCtx = (ctx && ctx.app) ? { ...ctx } : null;
|
|
6586
|
+
if (inWorkspacesMode()) {
|
|
6587
|
+
paintWorkspaceBeacon();
|
|
6588
|
+
const $m = document.getElementById('guide-modal');
|
|
6589
|
+
if ($m && $m.classList.contains('show') && wsGuideCtx) renderWorkspaceGuide();
|
|
6590
|
+
}
|
|
6591
|
+
},
|
|
6592
|
+
clearWorkspaceGuide() {
|
|
6593
|
+
wsGuideCtx = null;
|
|
6594
|
+
// Repaint through the mode-correct painter. In Workspaces mode paintWorkspaceBeacon() hides the
|
|
6595
|
+
// (now-cleared) workspace beacon. On the mode-switch-BACK path we're already in Workflows mode,
|
|
6596
|
+
// so repaint the WORKFLOW beacon for the current app instead of no-op'ing — otherwise the stale
|
|
6597
|
+
// workspace label/tooltip/is-new lingers until the next workflow load. paintBeacon(null) is safe
|
|
6598
|
+
// (it just hides the workflow beacon when no app is open).
|
|
6599
|
+
if (inWorkspacesMode()) paintWorkspaceBeacon();
|
|
6600
|
+
else paintBeacon(currentId ? apps.get(currentId) : null);
|
|
6601
|
+
},
|
|
6602
|
+
markWorkspaceApproved(app) {
|
|
6603
|
+
if (!app || guideFor(wsGuideKey(app)).approveDone) return;
|
|
6604
|
+
setGuide(wsGuideKey(app), { approveDone: true });
|
|
6605
|
+
if (wsGuideCtx && wsGuideCtx.app === app) {
|
|
6606
|
+
wsGuideCtx.approved = true;
|
|
6607
|
+
if (inWorkspacesMode()) paintWorkspaceBeacon();
|
|
6608
|
+
const $m = document.getElementById('guide-modal');
|
|
6609
|
+
if ($m && $m.classList.contains('show')) renderWorkspaceGuide();
|
|
6610
|
+
}
|
|
6611
|
+
},
|
|
6462
6612
|
};
|
|
6463
6613
|
|
|
6464
6614
|
// ── boot ──────────────────────────────────────────────────────────────────────
|
package/dist/web/index.html
CHANGED
|
@@ -80,7 +80,7 @@
|
|
|
80
80
|
</button>
|
|
81
81
|
<span class="ctl-sep" aria-hidden="true"></span>
|
|
82
82
|
<button id="ws-approve" type="button" class="primary"
|
|
83
|
-
data-tip="Locks in this version
|
|
83
|
+
data-tip="Locks in this version — your edits already auto-save; this is the sign-off.">✓ Approve</button>
|
|
84
84
|
</div>
|
|
85
85
|
<!-- Per-workflow onboarding beacon. Pulses "✨ Start here" while this workflow is
|
|
86
86
|
new to the user (no first Run completed, not dismissed); calms to a quiet
|
|
@@ -173,7 +173,7 @@
|
|
|
173
173
|
<!-- Drawings step: a shell toolbar band + revision-read status card ABOVE the filter iframe
|
|
174
174
|
(chrome around it, never inside — the filter owns its own layout). Shown only on Drawings. -->
|
|
175
175
|
<div class="ws-drawings-bar" id="ws-drawings-bar" hidden>
|
|
176
|
-
<span class="ws-drawings-bar-label">
|
|
176
|
+
<span class="ws-drawings-bar-label">Source</span>
|
|
177
177
|
<span class="ws-drawings-bar-sep"></span>
|
|
178
178
|
<button type="button" id="ws-attach-revision" class="btn-mini" data-tip="Attach a revised drawing set — your terminal AI reads it into a new version">⤒ Attach revised drawings…</button>
|
|
179
179
|
<input type="file" id="ws-revision-file" accept=".pdf,image/*" multiple hidden>
|
|
@@ -183,6 +183,16 @@
|
|
|
183
183
|
Same-origin like #contract-editor-frame (they call /api/contract directly). -->
|
|
184
184
|
<iframe id="ws-frame-model" class="ws-frame" title="Project model editor" hidden></iframe>
|
|
185
185
|
<iframe id="ws-frame-drawings" class="ws-frame" title="Project drawings & filter" hidden></iframe>
|
|
186
|
+
<!-- Source-step placeholder: a workspace app with NO filter iframe (vectorize) shows this
|
|
187
|
+
where steel shows its filter. The ws-drawings-bar (attach/replace to re-vectorize) sits
|
|
188
|
+
above; this pane explains the Source step. Toggled by setStep() in workspaces.js. -->
|
|
189
|
+
<div class="ws-source-placeholder" id="ws-source-placeholder" hidden>
|
|
190
|
+
<div class="ws-source-inner">
|
|
191
|
+
<span class="ws-source-ico" aria-hidden="true">▨</span>
|
|
192
|
+
<div class="ws-source-title">This project's source drawing</div>
|
|
193
|
+
<div class="ws-source-sub">The vectors on the Vectors tab were read from the drawing you imported. Drop a revised drawing in the bar above to re-vectorize it as a new version.</div>
|
|
194
|
+
</div>
|
|
195
|
+
</div>
|
|
186
196
|
<!-- Exports = a SHELL-rendered pane (not an iframe): export cards over the project's own
|
|
187
197
|
contract. workspaces.js fills it on step-switch (renderExports). -->
|
|
188
198
|
<div class="ws-exports" id="ws-exports" hidden></div>
|
|
@@ -828,6 +838,7 @@
|
|
|
828
838
|
nothing project-specific (spec §2). Populated per-open in workspaces.js. -->
|
|
829
839
|
<div class="menu ws-proj-menu" id="ws-proj-menu" role="menu" hidden></div>
|
|
830
840
|
|
|
841
|
+
<script src="analytics.js"></script>
|
|
831
842
|
<script src="app.js"></script>
|
|
832
843
|
<script src="renderers.js"></script>
|
|
833
844
|
<script src="aware.js"></script>
|
|
@@ -1493,6 +1493,16 @@ function onKey(e) {
|
|
|
1493
1493
|
if (!renderer || !canvasEl || canvasEl.style.display === 'none') return; // only when 3D is active
|
|
1494
1494
|
const ae = document.activeElement; if (ae && /^(INPUT|SELECT|TEXTAREA)$/.test(ae.tagName)) return;
|
|
1495
1495
|
if (pending && pending.clipDrag && e.key === 'Escape') { e.preventDefault(); const p = pending; if (p.plane) p.clip.point = p.prePoint; else p.clip.box.copy(p.preBox); rebuildClipPlanes(p.clip); applyClips(); renderClipGizmo(); pending = dragging = null; if (controls) controls.enabled = true; if (readout) readout.style.display = 'none'; return; } // Esc mid-drag → undo the handle move
|
|
1496
|
+
if (pending && e.key === 'Escape' && (pending.epDrag || pending.origMm)) { // Esc mid-drag → cancel a member move / endpoint stretch and revert to the pre-drag state (no commit)
|
|
1497
|
+
e.preventDefault();
|
|
1498
|
+
if (pending.mesh && pending.meshPos0) pending.mesh.position.copy(pending.meshPos0); // put a live-moved (or Alt-vertical) member mesh back
|
|
1499
|
+
const wasEp = pending.epDrag;
|
|
1500
|
+
pending = dragging = null; dragEp = null;
|
|
1501
|
+
if (controls) controls.enabled = true;
|
|
1502
|
+
if (marker) marker.visible = false; if (readout) readout.style.display = 'none'; if (epPreview) epPreview.visible = false; if (epMovedLine) epMovedLine.visible = false; clearCopyGhost();
|
|
1503
|
+
if (wasEp) rebuildEndpoints(); // restore the dragged endpoint dot to its original spot
|
|
1504
|
+
return;
|
|
1505
|
+
}
|
|
1496
1506
|
if (wpMode && e.key === 'Escape') { e.preventDefault(); if (wpMode === '3pt' && wpDraft && wpDraft.length) { wpDraft.pop(); updateStatusChip(); } else { wpMode = null; wpDraft = null; marker.visible = false; canvasEl.style.cursor = 'default'; reflectWpBar(); updateStatusChip(); } return; } // Esc steps a 3pt pick back, else disarms the set-plane mode
|
|
1497
1507
|
if (insertMode && e.key === 'Escape') { e.preventDefault(); setInsertMode(false); if (api && api.toast) api.toast('Insert cancelled'); return; } // Esc disarms the detail-placement pick
|
|
1498
1508
|
if (basePickCol && e.key === 'Escape') { e.preventDefault(); setBasePickMode(null); if (api && api.toast) api.toast('Trim cancelled'); return; } // Esc disarms the Mode B column-base pick
|
|
@@ -3112,6 +3112,11 @@ addEventListener('keydown',e=>{
|
|
|
3112
3112
|
if(e.key==='Escape'&&rfiOpen()){closeRFI();return;}
|
|
3113
3113
|
if(e.key==='Escape'&&confOpen()){closeConf();return;}
|
|
3114
3114
|
if(e.key==='Escape'&&snapMenuOpen()){closeSnapMenu();return;}
|
|
3115
|
+
if(e.key==='Escape'&&drag){e.preventDefault();const pre=drag.pre,wasGrid=drag.type==='gridline'; // Esc mid-drag → cancel the command and revert to the pre-drag state (no undo entry committed)
|
|
3116
|
+
if(drag.type==='draw'&&drag.line)drag.line.remove();else if(drag.type==='marquee'&&drag.rect)drag.rect.remove();
|
|
3117
|
+
drag=null;snapClear();epPrevClear();gridReadoutHide();if(wasGrid||!anyToolActive())snapOnlyClear2d(); // a gridline drag's single-shot snap override is always cleared (it can run with the grid panel open, so don't gate on anyToolActive) — matches the normal release
|
|
3118
|
+
if(pre)apply(pre);else render(); // apply() restores the snapshot + re-renders (2D+3D); draw/marquee have no snapshot, just refresh
|
|
3119
|
+
return;}
|
|
3115
3120
|
if(e.key==='Escape'&&!view3d&&snapOnly){snapOnlyClear2d();return;} // own Esc rung: 1st Esc drops the snap override, the next cancels the tool/draft (mirrors the set-axes two-step)
|
|
3116
3121
|
if(e.key==='Home'){e.preventDefault();if(view3d&&window.Steel3DView)window.Steel3DView.frameAll();else fitToWindow();return;}
|
|
3117
3122
|
if(!view3d&&!inForm&&(((e.key==='z'||e.key==='Z')&&e.altKey)||(e.key===' '&&e.shiftKey))){e.preventDefault();zoomToSelection();return;} // 2D zoom-to-selected (Tekla Shift+Space / Alt+Z); 3D handles its own (steel-3d-view onKey)
|
|
@@ -68,6 +68,9 @@
|
|
|
68
68
|
#toast.show{display:flex}
|
|
69
69
|
#toast .ghost{height:24px;padding:0 8px;background:transparent;border:1px solid var(--line);color:var(--mut);border-radius:5px;font-size:11px}
|
|
70
70
|
#toast .ghost:hover{color:var(--text);border-color:var(--brand)}
|
|
71
|
+
/* Themed tooltip — replaces native title= so no OS-default tooltip leaks the dark theme. */
|
|
72
|
+
#tooltip{position:fixed;z-index:80;background:var(--panel);border:1px solid var(--line);border-radius:6px;padding:5px 8px;font-size:11.5px;line-height:1.35;color:var(--text);pointer-events:none;max-width:260px;box-shadow:0 8px 22px rgba(0,0,0,.5);opacity:0;transition:opacity .12s}
|
|
73
|
+
#tooltip.show{opacity:1}
|
|
71
74
|
</style>
|
|
72
75
|
</head>
|
|
73
76
|
<body>
|
|
@@ -76,7 +79,7 @@
|
|
|
76
79
|
<span class="stat" id="title">—</span>
|
|
77
80
|
<span class="spacer"></span>
|
|
78
81
|
<span class="stat"><b id="nEl">0</b> elements · <b id="nTx">0</b> text</span>
|
|
79
|
-
<span id="saveStat"
|
|
82
|
+
<span id="saveStat" data-tip="Edits save to the app's contract automatically" hidden>Auto-save on</span>
|
|
80
83
|
<button class="primary" id="exportBtn" disabled>Export SVG</button>
|
|
81
84
|
</header>
|
|
82
85
|
<div id="wrap">
|
|
@@ -84,7 +87,7 @@
|
|
|
84
87
|
<svg id="svg" xmlns="http://www.w3.org/2000/svg"></svg>
|
|
85
88
|
<div id="state" class="show"><div class="spin"></div><div id="stateMsg">Loading drawing…</div></div>
|
|
86
89
|
<div id="zoombar">
|
|
87
|
-
<button class="ghost" id="fitBtn"
|
|
90
|
+
<button class="ghost" id="fitBtn" data-tip="Fit to view">Fit</button>
|
|
88
91
|
<input type="range" id="zoom" min="10" max="800" value="100" aria-label="Zoom">
|
|
89
92
|
<span id="zpct">100%</span>
|
|
90
93
|
</div>
|
|
@@ -120,7 +123,12 @@
|
|
|
120
123
|
const svg = $('svg');
|
|
121
124
|
const params = new URLSearchParams(location.search);
|
|
122
125
|
const app = params.get('app');
|
|
123
|
-
|
|
126
|
+
// Workspaces: a project-keyed contract lives at projects/<id>/contract.json. Empty PROJECT =
|
|
127
|
+
// legacy app-keyed behaviour (byte-for-byte today's URLs). PROJECT_QS is appended to every
|
|
128
|
+
// /api/contract call so two vectorize projects of the same app never share a draft (spec §4.3).
|
|
129
|
+
const PROJECT = params.get('project') || '';
|
|
130
|
+
const PROJECT_QS = PROJECT ? ('?project=' + encodeURIComponent(PROJECT)) : '';
|
|
131
|
+
const src = params.get('src') || (app ? ('/api/contract/' + encodeURIComponent(app) + PROJECT_QS) : '/vector-example.json');
|
|
124
132
|
const WEAK = 0.5;
|
|
125
133
|
|
|
126
134
|
let sheet = null;
|
|
@@ -307,7 +315,7 @@
|
|
|
307
315
|
let saveQueue = Promise.resolve(true);
|
|
308
316
|
async function doPut() {
|
|
309
317
|
try {
|
|
310
|
-
const res = await fetch('/api/contract/' + encodeURIComponent(app), {
|
|
318
|
+
const res = await fetch('/api/contract/' + encodeURIComponent(app) + PROJECT_QS, {
|
|
311
319
|
method: 'PUT', headers: { 'content-type': 'application/json' }, body: JSON.stringify(C),
|
|
312
320
|
});
|
|
313
321
|
setSaved(res.ok ? 'ok' : 'err');
|
|
@@ -382,14 +390,14 @@
|
|
|
382
390
|
for (const el of weak) {
|
|
383
391
|
const row = document.createElement('div'); row.className = 'wrow'; row.dataset.id = el.id || '';
|
|
384
392
|
const lbl = document.createElement('span'); lbl.className = 'lbl'; lbl.textContent = elLabel(el);
|
|
385
|
-
lbl.
|
|
393
|
+
lbl.dataset.tip = 'Show this trace on the drawing';
|
|
386
394
|
lbl.addEventListener('click', () => locateOnCanvas(el.id));
|
|
387
395
|
const pct = document.createElement('span'); pct.className = 'pct'; pct.textContent = Math.round((el.confidence || 0) * 100) + '%';
|
|
388
396
|
const ok = document.createElement('button'); ok.className = 'ghost'; ok.textContent = 'Accept';
|
|
389
|
-
ok.
|
|
397
|
+
ok.dataset.tip = 'Mark this trace as correct — clears its low-confidence flag.';
|
|
390
398
|
ok.addEventListener('click', () => acceptEl(el.id));
|
|
391
399
|
const del = document.createElement('button'); del.className = 'danger'; del.textContent = 'Delete';
|
|
392
|
-
del.
|
|
400
|
+
del.dataset.tip = 'Remove this trace from the drawing (Undo available).';
|
|
393
401
|
del.addEventListener('click', () => deleteEl(el.id));
|
|
394
402
|
row.append(lbl, pct, ok, del); box.appendChild(row);
|
|
395
403
|
}
|
|
@@ -461,6 +469,20 @@
|
|
|
461
469
|
|
|
462
470
|
window.addEventListener('resize', () => { if (sheet) syncZoom(); });
|
|
463
471
|
})();
|
|
472
|
+
|
|
473
|
+
// Themed tooltips (replaces native title=) — one shared element, shown on data-tip hover after a
|
|
474
|
+
// short delay, positioned below the control and clamped to the viewport (same handler as the steel
|
|
475
|
+
// editor's 3D toolbar). Reads data-tip via getAttribute, so JS-set el.dataset.tip works too.
|
|
476
|
+
(function(){ const tip=document.createElement('div'); tip.id='tooltip'; document.body.appendChild(tip); let t=null;
|
|
477
|
+
function show(el){ const txt=el.getAttribute('data-tip'); if(!txt) return; tip.textContent=txt; tip.classList.add('show');
|
|
478
|
+
const r=el.getBoundingClientRect(), tw=tip.offsetWidth, th=tip.offsetHeight;
|
|
479
|
+
let x=Math.max(6,Math.min(r.left+r.width/2-tw/2, innerWidth-tw-6)), y=r.bottom+6; if(y+th>innerHeight-6) y=r.top-th-6;
|
|
480
|
+
tip.style.left=x+'px'; tip.style.top=y+'px'; }
|
|
481
|
+
function hide(){ clearTimeout(t); tip.classList.remove('show'); }
|
|
482
|
+
document.addEventListener('pointerover', e=>{ const el=e.target.closest('[data-tip]'); if(!el) return; clearTimeout(t); t=setTimeout(()=>show(el),400); });
|
|
483
|
+
document.addEventListener('pointerout', e=>{ if(e.target.closest('[data-tip]')) hide(); });
|
|
484
|
+
document.addEventListener('pointerdown', hide, true); // a click hides the tip immediately
|
|
485
|
+
})();
|
|
464
486
|
</script>
|
|
465
487
|
</body>
|
|
466
488
|
</html>
|