slidemind 0.3.19 → 0.4.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/.claude-plugin/plugin.json +1 -1
- package/.codex-plugin/plugin.json +1 -1
- package/AGENTS.md +591 -15
- package/README.md +202 -7
- package/bin/slidemind.mjs +50 -2
- package/package.json +2 -2
- package/skills/create-slides/SKILL.md +484 -886
- package/skills/search/SKILL.md +46 -23
- package/src/bridge/build-and-push.mjs +548 -73
- package/src/bridge/bundle.mjs +100 -31
- package/src/bridge/capture.mjs +14 -17
- package/src/bridge/client/brands.mjs +157 -18
- package/src/bridge/client/engine-routing.mjs +48 -13
- package/src/bridge/client/engine-search.mjs +24 -1
- package/src/bridge/client/engine-slides.mjs +1 -1
- package/src/bridge/client/render.mjs +27 -1
- package/src/bridge/client/transport.mjs +499 -23
- package/src/bridge/client.mjs +10 -0
- package/src/bridge/config.mjs +67 -6
- package/src/bridge/engine-download.mjs +0 -10
- package/src/bridge/graph-auth.mjs +55 -3
- package/src/bridge/image-intent-envelope.mjs +239 -0
- package/src/bridge/package-preview.mjs +67 -0
- package/src/bridge/sharepoint.mjs +914 -0
- package/src/builders/asset-fetch.mjs +203 -0
- package/src/builders/block-dividers.mjs +83 -0
- package/src/builders/build.mjs +171 -87
- package/src/builders/card-height.mjs +294 -0
- package/src/builders/chart-hole.mjs +183 -0
- package/src/builders/chart-jsx.mjs +410 -0
- package/src/builders/chart-labels.mjs +326 -0
- package/src/builders/concept-geometry.mjs +370 -0
- package/src/builders/concept-intent.mjs +200 -0
- package/src/builders/concept-layout.mjs +79 -0
- package/src/builders/concept-lines.mjs +509 -0
- package/src/builders/concept-lists.mjs +270 -0
- package/src/builders/concept-marks.mjs +84 -0
- package/src/builders/concept-relationships.mjs +12 -0
- package/src/builders/concept-spec.mjs +645 -0
- package/src/builders/concept-text.mjs +140 -0
- package/src/builders/concept-waves.mjs +57 -0
- package/src/builders/copy-fit.mjs +139 -0
- package/src/builders/design-tokens.mjs +347 -0
- package/src/builders/draft-check.mjs +552 -340
- package/src/builders/draft-lines.mjs +369 -0
- package/src/builders/edit-route.mjs +29 -0
- package/src/builders/figure-sketch.mjs +375 -0
- package/src/builders/fold-sets.mjs +639 -0
- package/src/builders/gate-server.mjs +9 -4
- package/src/builders/gate-session.js +273 -12
- package/src/builders/gate-shell.js +1494 -0
- package/src/builders/image-intent-check.mjs +308 -0
- package/src/builders/image-use-reconcile.mjs +107 -0
- package/src/builders/kit-helpers.mjs +121 -0
- package/src/builders/layout-names.mjs +32 -0
- package/src/builders/legacy-reference-uses.mjs +179 -0
- package/src/builders/line-fit.mjs +224 -0
- package/src/builders/line-native.mjs +895 -0
- package/src/builders/line-relations.mjs +230 -0
- package/src/builders/lint-fix.mjs +117 -9
- package/src/builders/lint.mjs +25 -20
- package/src/builders/list-jsx.mjs +88 -0
- package/src/builders/placement.mjs +133 -0
- package/src/builders/plan-checks.mjs +135 -0
- package/src/builders/plan-comments.mjs +19 -0
- package/src/builders/plan-handlers.mjs +95 -0
- package/src/builders/plan-markdown-edit.mjs +3 -3
- package/src/builders/plan-overlay.js +1530 -1608
- package/src/builders/plan.mjs +689 -186
- package/src/builders/preset-definitions.json +1 -0
- package/src/builders/preset-eval.mjs +244 -0
- package/src/builders/preview.html +51 -120
- package/src/builders/red-thread-backbone.mjs +135 -36
- package/src/builders/repair.mjs +205 -0
- package/src/builders/repeat-groups.mjs +540 -0
- package/src/builders/resolve-skill.mjs +5 -4
- package/src/builders/review-overlay.js +296 -293
- package/src/builders/review.mjs +305 -84
- package/src/builders/rule-shift.mjs +96 -0
- package/src/builders/seat.mjs +354 -0
- package/src/builders/settle-driver.mjs +236 -0
- package/src/builders/settle.mjs +1448 -0
- package/src/builders/slide-prep.mjs +67 -0
- package/src/builders/spec-assets.mjs +236 -0
- package/src/builders/spec-match.mjs +56 -0
- package/src/builders/stack.mjs +413 -0
- package/src/builders/storyboard-markdown.mjs +70 -27
- package/src/builders/storyboard-schema.mjs +54 -114
- package/src/builders/text-blocks.mjs +212 -0
- package/src/builders/text-card-fit.mjs +552 -0
- package/src/builders/text-stack.mjs +276 -0
- package/src/builders/transcribe.mjs +1069 -0
- package/src/cli/assets.mjs +164 -0
- package/src/cli/common.mjs +173 -12
- package/src/cli/concept-spec-support.mjs +76 -0
- package/src/cli/concepts.mjs +1002 -103
- package/src/cli/deck.mjs +401 -65
- package/src/cli/diagnose.mjs +4 -2
- package/src/cli/engine-helpers.mjs +17 -10
- package/src/cli/engine-results.mjs +76 -3
- package/src/cli/help.mjs +232 -28
- package/src/cli/install.mjs +9 -0
- package/src/cli/maintenance.mjs +104 -0
- package/src/cli/read.mjs +2 -1
- package/src/cli/ready.mjs +6 -3
- package/src/cli/record.mjs +522 -0
- package/src/cli/remote.mjs +818 -0
- package/src/cli/revision.mjs +686 -0
- package/src/cli/search.mjs +30 -11
- package/src/cli/session.mjs +56 -13
- package/src/cli/setup.mjs +46 -2
- package/src/cli/status.mjs +142 -33
- package/src/cli/tasks.mjs +246 -0
- package/src/cli/transcribe.mjs +854 -0
- package/src/cli/workspace-ui.mjs +260 -0
- package/src/cli/workspace.mjs +212 -34
- package/src/install-channel.mjs +64 -7
- package/src/lib/adopt.mjs +346 -25
- package/src/lib/concept-spec-binding.mjs +96 -0
- package/src/lib/conversions.mjs +70 -6
- package/src/lib/convert-folded.mjs +107 -0
- package/src/lib/deck-paths.mjs +418 -0
- package/src/lib/durable-ids.mjs +47 -0
- package/src/lib/edit-operations.mjs +316 -0
- package/src/lib/edit-state.mjs +1244 -0
- package/src/lib/edit-transitions.mjs +120 -0
- package/src/lib/extensions.mjs +501 -0
- package/src/lib/image-assets.mjs +465 -0
- package/src/lib/image-derivatives.mjs +234 -0
- package/src/lib/image-intent-fingerprint.mjs +70 -0
- package/src/lib/image-intent-tags.mjs +176 -0
- package/src/lib/image-intent.mjs +802 -0
- package/src/lib/image-use-overrides.mjs +279 -0
- package/src/lib/inline-components.mjs +613 -0
- package/src/lib/lint-converted.mjs +68 -4
- package/src/lib/merge-json.mjs +163 -0
- package/src/lib/overwrite-guard.mjs +3 -3
- package/src/lib/presentation-context.mjs +531 -0
- package/src/lib/preservation-tags.mjs +157 -0
- package/src/lib/preservation-verdict.mjs +1342 -0
- package/src/lib/publish.mjs +1557 -0
- package/src/lib/rebase.mjs +945 -0
- package/src/lib/reconcile-ack.mjs +4 -6
- package/src/lib/reconcile.mjs +62 -24
- package/src/lib/record-client.mjs +175 -0
- package/src/lib/record-state.mjs +151 -0
- package/src/lib/remote.mjs +661 -0
- package/src/lib/revision.mjs +1210 -0
- package/src/lib/slide-guids.mjs +4 -6
- package/src/lib/slide-preservation.mjs +423 -0
- package/src/lib/slide-selection.mjs +9 -0
- package/src/lib/workspace-transaction.mjs +117 -0
- package/src/pptx/content-digest.mjs +334 -0
- package/src/pptx/deck-assemble.mjs +1278 -0
- package/src/pptx/fill/engine.mjs +2 -3
- package/src/pptx/fill/freeform.mjs +6 -3
- package/src/pptx/ooxml.mjs +20 -0
- package/src/pptx/package-evidence.mjs +383 -0
- package/src/pptx/tags.mjs +163 -11
- package/src/update-check.mjs +22 -4
- package/src/workspace/adapters.mjs +222 -0
- package/src/workspace/auth.mjs +256 -0
- package/src/workspace/autostart.mjs +204 -0
- package/src/workspace/daemon.mjs +360 -0
- package/src/workspace/discovery.mjs +180 -0
- package/src/workspace/edit.mjs +167 -0
- package/src/workspace/events.mjs +52 -0
- package/src/workspace/gate-client.mjs +290 -0
- package/src/workspace/handback.mjs +73 -0
- package/src/workspace/lifecycle.mjs +635 -0
- package/src/workspace/operations.mjs +200 -0
- package/src/workspace/pick-folder.mjs +152 -0
- package/src/workspace/registry.mjs +359 -0
- package/src/workspace/remote-links.mjs +397 -0
- package/src/workspace/remote-status.mjs +359 -0
- package/src/workspace/routes.mjs +1331 -0
- package/src/workspace/server.mjs +159 -0
- package/src/workspace/sync.mjs +1119 -0
- package/src/workspace/task-actions.mjs +215 -0
- package/src/workspace/tasks.mjs +507 -0
- package/src/workspace/ui/app.js +1886 -0
- package/src/workspace/ui/index.html +141 -0
- package/src/workspace/ui/plan-bridge.js +120 -0
- package/src/workspace/ui/sharepoint.svg +59 -0
- package/src/workspace/ui/styles.css +423 -0
- package/src/builders/visual-families.mjs +0 -46
|
@@ -1,63 +1,56 @@
|
|
|
1
|
-
// Plan sign-off overlay — the CLI's plan-review gate,
|
|
2
|
-
//
|
|
3
|
-
//
|
|
4
|
-
//
|
|
5
|
-
//
|
|
1
|
+
// Plan sign-off overlay — the CLI's plan-review gate, laid out like the
|
|
2
|
+
// preview-review gate (review-overlay.js): the storyline as a slide RAIL on the
|
|
3
|
+
// left, the selected slide's storyboard on a fitted STAGE in the middle, its
|
|
4
|
+
// layout gallery + details in an ASIDE on the right, and the decision composer
|
|
5
|
+
// pinned at the bottom. Dormant unless `window.__SLIDEMIND_PLAN_REVIEW__` is
|
|
6
|
+
// set (only the `slidemind plan` local server injects it), and dormant inside
|
|
7
|
+
// an iframe / thumbnail render, so nothing runs on an ordinary page.
|
|
6
8
|
//
|
|
7
|
-
// The
|
|
8
|
-
//
|
|
9
|
-
//
|
|
10
|
-
//
|
|
11
|
-
//
|
|
12
|
-
// (contenteditable) directly. Both paths edit the SAME markdown string, mapped
|
|
13
|
-
// back through the plan modules exposed on window.__SLIDEMIND_PLAN__ (bundled
|
|
14
|
-
// alongside window.__SLIDEMIND_RENDER_MD__ by plan.mjs).
|
|
9
|
+
// The chrome (bar, buttons, strips, hand-off dialog, veil, rail styling) is the
|
|
10
|
+
// SHARED gate shell (gate-shell.js → window.__SLIDEMIND_SHELL__); the session
|
|
11
|
+
// (ledger, rounds, heartbeat) is the shared gate session (gate-session.js).
|
|
12
|
+
// This file owns only what is plan-specific: the storyline edits, the
|
|
13
|
+
// storyboard canvas, the layout pick, the composer and the decision POST.
|
|
15
14
|
//
|
|
16
|
-
//
|
|
17
|
-
//
|
|
18
|
-
//
|
|
19
|
-
//
|
|
20
|
-
//
|
|
21
|
-
// session with its anchor and status (sent → addressed).
|
|
15
|
+
// A plan that carries ```storyboard-canvas``` fences renders as rail + stage +
|
|
16
|
+
// aside; a plain-markdown plan renders its blocks inline-editable
|
|
17
|
+
// (contenteditable) as one document. Both paths edit the SAME markdown string,
|
|
18
|
+
// mapped back through the plan modules on window.__SLIDEMIND_PLAN__ (bundled
|
|
19
|
+
// by plan.mjs).
|
|
22
20
|
//
|
|
23
21
|
// TWO MODES, not "reviewing vs. locked out":
|
|
24
22
|
// - PARKED (the command is blocking on this tab): everything is live. Send
|
|
25
23
|
// resolves the gate at once (decision "comments"); Approve resolves it as
|
|
26
24
|
// "approve"; direct edits and story iteration are allowed.
|
|
27
|
-
// - WORKING (the agent is revising): the plan stays readable and the
|
|
25
|
+
// - WORKING (the agent is revising): the plan stays readable and the rail
|
|
28
26
|
// stays navigable; the composer, direct edits, story iteration and Approve
|
|
29
|
-
// are off (the displayed round is already superseded).
|
|
30
|
-
//
|
|
31
|
-
//
|
|
32
|
-
// heartbeat and reloads itself when the next `slidemind plan` run presents
|
|
33
|
-
// the revised round.
|
|
27
|
+
// are off (the displayed round is already superseded). Nothing is queued
|
|
28
|
+
// behind the agent's back: the tab polls the heartbeat and reloads itself
|
|
29
|
+
// when the next `slidemind plan` run presents the revised round.
|
|
34
30
|
//
|
|
35
|
-
// A round transition is detected from the SERVER's per-run nonce (CFG.round)
|
|
36
|
-
//
|
|
37
|
-
// flips "sent" ledger rows to "addressed", restores the selected tab + the
|
|
31
|
+
// A round transition is detected from the SERVER's per-run nonce (CFG.round).
|
|
32
|
+
// It flips "sent" ledger rows to "addressed", restores the selected slide + the
|
|
38
33
|
// unsent draft, shows a transient banner, and diffs the cached previous plan
|
|
39
|
-
// body per slide to badge the changed
|
|
40
|
-
// COSMETIC only; it never decides whether a round arrived.
|
|
41
|
-
// fires when the arriving round parks (a plain refresh of a sent round stays
|
|
42
|
-
// silent).
|
|
34
|
+
// body per slide to badge the changed rail cards "updated" — the body diff is
|
|
35
|
+
// COSMETIC only; it never decides whether a round arrived.
|
|
43
36
|
//
|
|
44
|
-
//
|
|
45
|
-
// the
|
|
46
|
-
//
|
|
47
|
-
//
|
|
48
|
-
//
|
|
49
|
-
//
|
|
50
|
-
//
|
|
51
|
-
// Branding is lifted verbatim from review-overlay.js: the embossed "raised
|
|
52
|
-
// slide" logo (inline-SVG fallback when no per-brand orb PNG is bundled), the
|
|
53
|
-
// drifting "slide"/"mind" wordmark, Poppins/Urbanist type, Lucide icons, and
|
|
54
|
-
// the neutral dark product palette.
|
|
37
|
+
// The decision POSTs {decision, comments[], editedPlan, operationId,
|
|
38
|
+
// expectedHash} through the shell's decision wire (SHELL.decide) so the
|
|
39
|
+
// `slidemind plan` command — or, inside the workspace page, the agent's waiting
|
|
40
|
+
// request — gets it. A refusal never drops what the reviewer wrote: a busy deck
|
|
41
|
+
// re-sends it, a newer version reloads with the drafts kept, and the card edits
|
|
42
|
+
// that have not been sent yet are persisted per plan version (EDITS_KEY), so a
|
|
43
|
+
// reload or a crash brings them back.
|
|
55
44
|
|
|
56
45
|
(function () {
|
|
57
46
|
var CFG = window.__SLIDEMIND_PLAN_REVIEW__;
|
|
58
47
|
if (!CFG) return;
|
|
59
|
-
//
|
|
60
|
-
|
|
48
|
+
// The copy of this page saved at the deck root: a browser cannot write back
|
|
49
|
+
// to a file, so the gate stays dormant and the plan reads as a document.
|
|
50
|
+
if (CFG.readOnly) return;
|
|
51
|
+
// Dormant inside an iframe / thumbnail render — except the workspace page,
|
|
52
|
+
// which frames the LIVE gate on purpose (CFG.embedded).
|
|
53
|
+
if (window.top !== window.self && !CFG.embedded) return;
|
|
61
54
|
if (new URLSearchParams(location.search).get("thumb") === "1") return;
|
|
62
55
|
|
|
63
56
|
// The bundled plan modules (schema / storyboard-markdown / backbone / edit /
|
|
@@ -67,22 +60,23 @@
|
|
|
67
60
|
var APPROVE_CLOSE_DELAY_MS = 2400; // long enough to read the build animation
|
|
68
61
|
var WAITING_TITLE = "⏳ Agent revising — slidemind";
|
|
69
62
|
// The plan body of the round that is on screen, cached for the NEXT round's
|
|
70
|
-
// per-slide diff. Its own key
|
|
71
|
-
//
|
|
72
|
-
//
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
// server-nonce round transition, the round-ready announce and the heartbeat
|
|
78
|
-
// poll — one implementation, shared with the preview gate.
|
|
63
|
+
// per-slide diff. Its own key: ~60 KB, written ONCE at boot.
|
|
64
|
+
// The storage namespace: inside the workspace the registry id (two decks may
|
|
65
|
+
// share a name, and one tab's sessionStorage serves every deck it frames);
|
|
66
|
+
// standalone the deck name, as before.
|
|
67
|
+
var DECK_KEY = CFG.embedded && CFG.deckId ? "id:" + CFG.deckId : CFG.deck || "";
|
|
68
|
+
var BODY_KEY = "slidemind-plan-body:" + DECK_KEY;
|
|
69
|
+
|
|
79
70
|
var SES = window.__SLIDEMIND_GATE__.createGateSession({
|
|
80
71
|
name: "plan",
|
|
81
|
-
deck:
|
|
72
|
+
deck: DECK_KEY,
|
|
82
73
|
storage: sessionStorage,
|
|
83
74
|
fetch: function (u, o) {
|
|
84
75
|
return fetch(u, o);
|
|
85
76
|
},
|
|
77
|
+
endpoint: CFG.endpoint || "/plan-review",
|
|
78
|
+
embedded: !!CFG.embedded,
|
|
79
|
+
subject: CFG.subjectHash || null, // the content version this page shows (rides in ctx)
|
|
86
80
|
round: CFG.round || "",
|
|
87
81
|
service: "slidemind-plan",
|
|
88
82
|
heartbeat: CFG.heartbeat || "/__plan/heartbeat",
|
|
@@ -92,455 +86,370 @@
|
|
|
92
86
|
notifyTitle: "slidemind — plan ready",
|
|
93
87
|
notifyBody: "The revised plan is ready for your review. Click to jump back to the tab.",
|
|
94
88
|
});
|
|
95
|
-
var BANNER_MS = 7000; // how long the "Updated
|
|
96
|
-
var BEAT_DRAG_THRESHOLD = 4; // px before a
|
|
89
|
+
var BANNER_MS = 7000; // how long the "Updated by your agent" banner stays
|
|
90
|
+
var BEAT_DRAG_THRESHOLD = 4; // px before a rail card press becomes a drag
|
|
97
91
|
var BLOCK_DRAG_THRESHOLD = 3; // px before a block press becomes a drag
|
|
98
92
|
|
|
99
93
|
// ---------- state ----------
|
|
100
|
-
var submitted = false;
|
|
101
|
-
var approveArmed = false; // inline "discard unsent feedback?" confirm state
|
|
102
94
|
var working = false; // the agent is revising → edits, composer and approve off
|
|
103
95
|
var baseline = CFG.planBody || ""; // the served plan body; edits diff against it
|
|
104
|
-
var ledger = []; // every comment of this session: {…, status:sent|addressed}
|
|
96
|
+
var ledger = []; // every comment of this session: {…, status:draft|sent|addressed}
|
|
105
97
|
var roundNo = 1; // this tab's round counter (sessionStorage, not server state)
|
|
106
|
-
var changedSlides = {}; //
|
|
98
|
+
var changedSlides = {}; // slide number → true ("updated" badges this round)
|
|
107
99
|
var bannerText = null; // transient round-arrival banner, null once dismissed
|
|
108
100
|
var commentScope = "slide"; // composer scope control: "slide" | "story"
|
|
101
|
+
var pendingBullet = false; // a content point being typed, not in the record yet
|
|
109
102
|
|
|
110
103
|
// The single source of truth the whole overlay renders from. `value` is the
|
|
111
|
-
// frontmatter-less plan body
|
|
112
|
-
//
|
|
113
|
-
//
|
|
114
|
-
|
|
104
|
+
// frontmatter-less plan body; `selectedIndex` is the slide on stage;
|
|
105
|
+
// `storyDraft` is a staged red-thread restructure (null when none), which
|
|
106
|
+
// blocks Approve and turns "send" into a story-iteration round.
|
|
107
|
+
// `design` is a cached design the reviewer picked (its presFP), null while
|
|
108
|
+
// the deck's own design is shown; it rides with the answer, like the edits.
|
|
109
|
+
var S = { value: baseline, selectedIndex: 0, storyDraft: null, design: null };
|
|
110
|
+
// The bound design's palette; a picked design's comes from CFG.designs.
|
|
111
|
+
var boundLayouts = Array.isArray(CFG.layouts) ? CFG.layouts : [];
|
|
112
|
+
// The plan text before the last design switch moved its layouts, and right
|
|
113
|
+
// after: switching again over an untouched remap starts from `before`, so
|
|
114
|
+
// A → B → A gives back exactly the plan the reviewer had.
|
|
115
|
+
var designUndo = null; // { before, from, after }
|
|
115
116
|
|
|
116
117
|
var feedbackText = ""; // the composer input, kept across composer rebuilds
|
|
117
|
-
var editingKey = null; // the
|
|
118
|
+
var editingKey = null; // the rail card whose move text is in rename mode
|
|
118
119
|
var newSlideCounter = 0; // unique keys for inserted slides
|
|
119
120
|
var focusAfterRender = null; // an element to focus once a render settles
|
|
120
|
-
|
|
121
|
-
var bar, wsEl, surfaceEl, composerEl, commentEl;
|
|
122
|
-
var editedBadge, approveBtn, sendBtn, hintEl;
|
|
123
121
|
var workingNote = null; // headline of the working status strip
|
|
124
122
|
var pendingComments = []; // the batch the next decision POST carries
|
|
125
123
|
var pendingEdited = null; // the edited plan markdown riding with it
|
|
126
|
-
var
|
|
124
|
+
var arrivedAfterRevising = false; // this load is the round the reviewer was waiting for
|
|
125
|
+
var askedAgainUnchanged = false; // …and the agent asked again without changing anything
|
|
127
126
|
|
|
128
|
-
|
|
129
|
-
|
|
127
|
+
var wsEl, surfaceEl, composerEl, commentEl;
|
|
128
|
+
var editedBadge, approveBtn, sendBtn, hintEl, barTools;
|
|
129
|
+
|
|
130
|
+
// The shared chrome: bar, strips, hand-off dialog, veil, buttons.
|
|
131
|
+
var SHELL = window.__SLIDEMIND_SHELL__.createGateShell({
|
|
132
|
+
CFG: CFG,
|
|
133
|
+
session: SES,
|
|
134
|
+
service: "slidemind-plan",
|
|
135
|
+
what: { revising: "the plan", approved: "Plan" },
|
|
136
|
+
isWorking: function () {
|
|
137
|
+
return working;
|
|
138
|
+
},
|
|
139
|
+
workingNote: function () {
|
|
140
|
+
return workingNote;
|
|
141
|
+
},
|
|
142
|
+
onSaveCtx: function () {
|
|
143
|
+
saveCtx();
|
|
144
|
+
},
|
|
145
|
+
onChange: function () {
|
|
146
|
+
// an answer started or ended: the cards lock / unlock with it
|
|
147
|
+
if (locked() !== surfaceLocked) renderSurface();
|
|
148
|
+
refreshChrome();
|
|
149
|
+
},
|
|
150
|
+
onDecided: function (decision, ack, body) {
|
|
151
|
+
decided(decision, body);
|
|
152
|
+
},
|
|
153
|
+
onDecisionFailed: function () {
|
|
154
|
+
refreshChrome();
|
|
155
|
+
},
|
|
156
|
+
onAlreadyAnswered: function (a) {
|
|
157
|
+
enterWorking(a && a.action === "approve" ? "Already approved." : a && a.action ? "Comments already sent." : "Already answered in another window.");
|
|
158
|
+
},
|
|
159
|
+
onApprovalPickedUp: function () {
|
|
160
|
+
showApproved();
|
|
161
|
+
},
|
|
162
|
+
onLost: function () {
|
|
163
|
+
startNextRoundPolling(); // a re-run of the plan gate reloads this tab, drafts and all
|
|
164
|
+
},
|
|
165
|
+
// Unsent card edits are the one thing a reload into a newer version would
|
|
166
|
+
// drop (a comment being typed is kept, but the reload would yank the field
|
|
167
|
+
// from under the cursor): the newer version waits for both.
|
|
168
|
+
reloadBlocker: function () {
|
|
169
|
+
if (working) return null;
|
|
170
|
+
if (planWasEdited() || S.storyDraft || S.design) return "edits";
|
|
171
|
+
var a = document.activeElement;
|
|
172
|
+
if (a && (a.isContentEditable || (a === commentEl && commentEl.value.trim()))) return "typing";
|
|
173
|
+
return null;
|
|
174
|
+
},
|
|
175
|
+
reloadIntoNewRound: function () {
|
|
176
|
+
SES.write(EDITS_KEY, null); // reached only without edits, or by the reviewer's own "Reload"
|
|
177
|
+
saveLedger();
|
|
178
|
+
saveCtx();
|
|
179
|
+
SES.markRoundReadyAndReload();
|
|
180
|
+
},
|
|
181
|
+
});
|
|
182
|
+
var el = SHELL.el;
|
|
183
|
+
var ICONS = SHELL.ICONS;
|
|
184
|
+
|
|
185
|
+
// Read-only surface: working mode — and an answer on its way (a busy deck or
|
|
186
|
+
// a stopped workspace re-sends it) — lock the plan text: the request carries
|
|
187
|
+
// the edits made BEFORE it, so an edit made meanwhile would ride on nothing
|
|
188
|
+
// and be wiped when it lands. Navigation (rail selection) and the composer
|
|
189
|
+
// stay live regardless.
|
|
130
190
|
function locked() {
|
|
131
|
-
return working;
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
// ---------- brand assets (identical to review-overlay.js) ----------
|
|
135
|
-
var LOGO_SVG =
|
|
136
|
-
'<svg xmlns="http://www.w3.org/2000/svg" viewBox="160 160 960 540" preserveAspectRatio="none">' +
|
|
137
|
-
"<defs>" +
|
|
138
|
-
'<linearGradient id="rimTop" gradientUnits="userSpaceOnUse" x1="160" y1="0" x2="1120" y2="0">' +
|
|
139
|
-
'<stop offset="0%" stop-color="#d0e4ff" stop-opacity="0.85"/>' +
|
|
140
|
-
'<stop offset="35%" stop-color="#fff" stop-opacity="0.5"/>' +
|
|
141
|
-
'<stop offset="100%" stop-color="#fff" stop-opacity="0"/></linearGradient>' +
|
|
142
|
-
'<linearGradient id="rimLeft" gradientUnits="userSpaceOnUse" x1="0" y1="160" x2="0" y2="700">' +
|
|
143
|
-
'<stop offset="0%" stop-color="#d0e4ff" stop-opacity="0.6"/>' +
|
|
144
|
-
'<stop offset="40%" stop-color="#fff" stop-opacity="0.28"/>' +
|
|
145
|
-
'<stop offset="100%" stop-color="#fff" stop-opacity="0"/></linearGradient>' +
|
|
146
|
-
'<linearGradient id="rimBottom" gradientUnits="userSpaceOnUse" x1="1120" y1="0" x2="160" y2="0">' +
|
|
147
|
-
'<stop offset="0%" stop-color="#000" stop-opacity="0.55"/>' +
|
|
148
|
-
'<stop offset="60%" stop-color="#000" stop-opacity="0.2"/>' +
|
|
149
|
-
'<stop offset="100%" stop-color="#000" stop-opacity="0"/></linearGradient>' +
|
|
150
|
-
'<linearGradient id="rimRight" gradientUnits="userSpaceOnUse" x1="0" y1="700" x2="0" y2="160">' +
|
|
151
|
-
'<stop offset="0%" stop-color="#000" stop-opacity="0.45"/>' +
|
|
152
|
-
'<stop offset="60%" stop-color="#000" stop-opacity="0.16"/>' +
|
|
153
|
-
'<stop offset="100%" stop-color="#000" stop-opacity="0"/></linearGradient>' +
|
|
154
|
-
'<linearGradient id="faceFill" gradientUnits="userSpaceOnUse" x1="160" y1="160" x2="1120" y2="700">' +
|
|
155
|
-
'<stop offset="0%" stop-color="#fff" stop-opacity="0.08"/>' +
|
|
156
|
-
'<stop offset="50%" stop-color="#808080" stop-opacity="0.02"/>' +
|
|
157
|
-
'<stop offset="100%" stop-color="#000" stop-opacity="0.10"/></linearGradient>' +
|
|
158
|
-
"</defs>" +
|
|
159
|
-
'<rect x="160" y="160" width="960" height="540" rx="40" ry="40" fill="url(#faceFill)"/>' +
|
|
160
|
-
'<path d="M 160,200 A 40,40 0 0 1 200,160 L 1080,160 A 40,40 0 0 1 1120,200" fill="none" stroke="url(#rimTop)" stroke-width="3" stroke-linecap="round"/>' +
|
|
161
|
-
'<path d="M 160,200 L 160,660 A 40,40 0 0 0 200,700" fill="none" stroke="url(#rimLeft)" stroke-width="2.4" stroke-linecap="round"/>' +
|
|
162
|
-
'<path d="M 1120,660 A 40,40 0 0 1 1080,700 L 200,700 A 40,40 0 0 1 160,660" fill="none" stroke="url(#rimBottom)" stroke-width="3" stroke-linecap="round"/>' +
|
|
163
|
-
'<path d="M 1120,660 L 1120,200 A 40,40 0 0 0 1080,160" fill="none" stroke="url(#rimRight)" stroke-width="2.4" stroke-linecap="round"/>' +
|
|
164
|
-
'<rect x="160" y="160" width="960" height="540" rx="40" ry="40" fill="none" stroke="#fff" stroke-opacity="0.14" stroke-width="1.4"/>' +
|
|
165
|
-
"</svg>";
|
|
166
|
-
|
|
167
|
-
// Give a copy of the logo unique gradient ids so two instances (header + done
|
|
168
|
-
// veil) can coexist without id collisions.
|
|
169
|
-
function logoWithIds(suffix) {
|
|
170
|
-
return LOGO_SVG.replace(/id="([^"]+)"/g, 'id="$1' + suffix + '"').replace(
|
|
171
|
-
/url\(#([^)]+)\)/g,
|
|
172
|
-
"url(#$1" + suffix + ")",
|
|
173
|
-
);
|
|
191
|
+
return working || SHELL.decisionPending();
|
|
174
192
|
}
|
|
193
|
+
var surfaceLocked = false; // what the surface was last rendered as
|
|
175
194
|
|
|
176
|
-
//
|
|
177
|
-
var ICONS = {
|
|
178
|
-
check:
|
|
179
|
-
'<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><path d="M20 6 9 17l-5-5"/></svg>',
|
|
180
|
-
send:
|
|
181
|
-
'<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="m22 2-7 20-4-9-9-4Z"/><path d="M22 2 11 13"/></svg>',
|
|
182
|
-
sparkles:
|
|
183
|
-
'<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M9.937 15.5A2 2 0 0 0 8.5 14.063l-6.135-1.582a.5.5 0 0 1 0-.962L8.5 9.936A2 2 0 0 0 9.937 8.5l1.582-6.135a.5.5 0 0 1 .962 0L14.063 8.5A2 2 0 0 0 15.5 9.937l6.135 1.582a.5.5 0 0 1 0 .962L15.5 14.063a2 2 0 0 0-1.437 1.437l-1.582 6.135a.5.5 0 0 1-.962 0z"/><path d="M20 3v4"/><path d="M22 5h-4"/><path d="M4 17v2"/><path d="M5 18H3"/></svg>',
|
|
184
|
-
plus:
|
|
185
|
-
'<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.4" stroke-linecap="round" stroke-linejoin="round"><path d="M5 12h14"/><path d="M12 5v14"/></svg>',
|
|
186
|
-
x:
|
|
187
|
-
'<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M18 6 6 18"/><path d="m6 6 12 12"/></svg>',
|
|
188
|
-
ruler:
|
|
189
|
-
'<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M21.174 6.812a1 1 0 0 0-3.986-3.987L3.842 16.174a2 2 0 0 0-.5.83l-1.321 4.352a.5.5 0 0 0 .623.622l4.353-1.32a2 2 0 0 0 .83-.497z"/><path d="m15 5 4 4"/></svg>',
|
|
190
|
-
};
|
|
191
|
-
|
|
192
|
-
// ---------- styles (chrome copied from review-overlay.js; the editor/preview
|
|
193
|
-
// split is replaced by the storyboard dossier — plan-markdown.css ported and
|
|
194
|
-
// remapped onto the overlay's dark --sm-* palette; the two structural hues
|
|
195
|
-
// #3565d4 (blueprint blue) and #b41830 (storyline red) are literals) ----------
|
|
195
|
+
// ---------- styles (plan-specific; the chrome comes from the shell) ----------
|
|
196
196
|
var CSS =
|
|
197
|
-
"
|
|
198
|
-
"@keyframes sm-drift-b{0%,100%{transform:translateX(5px)}50%{transform:translateX(-5px)}}" +
|
|
199
|
-
"@keyframes sm-bar-in{from{transform:translateY(-100%);opacity:0}to{transform:none;opacity:1}}" +
|
|
200
|
-
"@keyframes sm-veil-in{from{opacity:0}to{opacity:1}}" +
|
|
201
|
-
"@keyframes sm-rise{from{opacity:0;transform:translateY(14px)}to{opacity:1;transform:none}}" +
|
|
202
|
-
"@keyframes sm-pulse{0%,100%{opacity:.45}50%{opacity:.85}}" +
|
|
203
|
-
":root{--sm-bg:var(--deck-chrome-bg,#111113);--sm-surface:var(--deck-chrome-bg-2,#161618);" +
|
|
204
|
-
"--sm-border:var(--deck-chrome-border,#2b2b30);--sm-fg:var(--deck-chrome-fg,#ededed);" +
|
|
205
|
-
"--sm-dim:var(--deck-chrome-dim,#9a9aa0);--sm-muted:rgba(255,255,255,.05);--sm-accent:#a9c2ff;" +
|
|
206
|
-
"--sm-success:#2ecc71;--sm-danger:#f87171;--sm-warn:#f6c454;" +
|
|
207
|
-
"--sm-font:'Poppins',ui-sans-serif,system-ui,sans-serif;--sm-word:'Urbanist',var(--sm-font);" +
|
|
208
|
-
/* the two structural hues, brightened against the dark surface */
|
|
209
|
-
"--plan-annotation:color-mix(in srgb,#3565d4 60%,var(--sm-fg));" +
|
|
197
|
+
":root{--plan-annotation:color-mix(in srgb,#3565d4 60%,var(--sm-fg));" +
|
|
210
198
|
"--plan-annotation-line:color-mix(in srgb,#3565d4 52%,transparent);" +
|
|
211
199
|
"--plan-annotation-wash:color-mix(in srgb,#3565d4 12%,transparent);" +
|
|
212
200
|
"--plan-story:color-mix(in srgb,#b41830 72%,var(--sm-fg));" +
|
|
213
201
|
"--plan-story-line:color-mix(in srgb,#b41830 45%,transparent);}" +
|
|
214
|
-
"
|
|
215
|
-
/*
|
|
216
|
-
".sm-
|
|
217
|
-
"
|
|
218
|
-
"
|
|
219
|
-
|
|
220
|
-
".sm-
|
|
221
|
-
".
|
|
222
|
-
".
|
|
223
|
-
".sm-logo-img{position:absolute;inset:0;width:100%;height:100%;object-fit:cover;display:block;}" +
|
|
224
|
-
".sm-word{display:flex;flex-direction:column;line-height:.98;font-family:var(--sm-word);" +
|
|
225
|
-
"font-weight:300;font-size:13px;letter-spacing:.46em;color:var(--sm-fg);}" +
|
|
226
|
-
".sm-word b{font-weight:300;display:block;will-change:transform;}" +
|
|
227
|
-
".sm-word .top{animation:sm-drift-a 20s ease-in-out infinite;}" +
|
|
228
|
-
".sm-word .bot{margin-left:.18em;animation:sm-drift-b 20s ease-in-out infinite;}" +
|
|
229
|
-
".sm-sep{width:1px;height:28px;background:var(--sm-border);}" +
|
|
230
|
-
".sm-meta{display:flex;flex-direction:column;gap:2px;min-width:0;}" +
|
|
231
|
-
".sm-meta .t{font-weight:600;font-size:13px;display:flex;align-items:center;gap:8px;}" +
|
|
232
|
-
".sm-meta .h{font-size:11.5px;color:var(--sm-dim);}" +
|
|
233
|
-
".sm-edited{font-size:10px;font-weight:600;letter-spacing:.06em;text-transform:uppercase;color:var(--sm-warn);" +
|
|
234
|
-
"border:1px solid rgba(246,196,84,.4);border-radius:999px;padding:1px 7px;}" +
|
|
235
|
-
".sm-grow{flex:1;}" +
|
|
236
|
-
/* buttons */
|
|
237
|
-
".sm-btn{display:inline-flex;align-items:center;gap:7px;border:1px solid transparent;border-radius:9px;" +
|
|
238
|
-
"padding:8px 14px;font-family:var(--sm-font);font-weight:600;font-size:12.5px;cursor:pointer;" +
|
|
239
|
-
"transition:transform .14s ease,background .14s ease,box-shadow .14s ease,border-color .14s ease;white-space:nowrap;}" +
|
|
240
|
-
".sm-btn svg{width:15px;height:15px;flex-shrink:0;}" +
|
|
241
|
-
/* workspace: one scrollable document surface + composer dock */
|
|
242
|
-
".sm-ws{position:fixed;left:0;right:0;top:var(--deck-chrome-h,62px);bottom:0;z-index:2147483540;" +
|
|
243
|
-
"display:flex;flex-direction:column;background:var(--sm-bg);}" +
|
|
244
|
-
".sm-ws-surface{flex:1;min-height:0;overflow-y:auto;background:var(--sm-bg);padding:22px 22px 30px;}" +
|
|
245
|
-
/* working mode: read-only, NOT locked out — the surface stays scrollable and
|
|
246
|
-
the strip stays clickable; the dashed frame is the read-only signal */
|
|
247
|
-
".sm-ws.is-waiting .plan-dossier{border-style:dashed;}" +
|
|
248
|
-
/* round-arrival banner */
|
|
249
|
-
".sm-round-banner{display:flex;align-items:center;gap:9px;max-width:1120px;margin:0 auto 14px;padding:9px 13px;" +
|
|
250
|
-
"border:1px solid color-mix(in srgb,var(--sm-accent) 35%,transparent);border-radius:10px;" +
|
|
251
|
-
"background:color-mix(in srgb,var(--sm-accent) 9%,transparent);color:var(--sm-dim);font-size:12.5px;line-height:1.5;" +
|
|
252
|
-
"animation:sm-rise .45s cubic-bezier(.2,.7,.2,1) both;}" +
|
|
253
|
-
".sm-round-banner svg{width:15px;height:15px;flex-shrink:0;color:var(--sm-accent);}" +
|
|
254
|
-
".sm-round-banner b{color:var(--sm-fg);font-weight:600;}" +
|
|
255
|
-
/* markdown document (Mode A + the storyboard headings share this scale) */
|
|
256
|
-
".plan-md-root{max-width:min(1120px,100%);margin:0 auto;line-height:1.65;font-size:15.5px;color:var(--sm-fg);}" +
|
|
257
|
-
".plan-md-root--storyboards{max-width:100%;display:flex;flex-direction:column;gap:0;}" +
|
|
258
|
-
".plan-md-root h1,.plan-md-root h2,.plan-md-root h3,.plan-md-root h4,.plan-md-root h5,.plan-md-root h6{" +
|
|
259
|
-
"color:var(--sm-fg);font-weight:600;line-height:1.28;margin:1.6em 0 .5em;}" +
|
|
202
|
+
"html,body{overflow:hidden;}" +
|
|
203
|
+
/* the workspace: surface + composer under the bar */
|
|
204
|
+
".sm-ws{position:fixed;left:0;right:0;top:var(--deck-chrome-h,62px);bottom:0;display:flex;flex-direction:column;" +
|
|
205
|
+
"background:var(--sm-bg);color:var(--sm-fg);font-family:var(--sm-font);font-size:13px;}" +
|
|
206
|
+
".sm-ws-surface{flex:1;min-height:0;display:flex;flex-direction:column;}" +
|
|
207
|
+
/* Mode A: one scrolling document */
|
|
208
|
+
".sm-doc{flex:1;min-height:0;overflow-y:auto;padding:28px 28px 40px;}" +
|
|
209
|
+
".plan-md-root{max-width:760px;margin:0 auto;line-height:1.65;font-size:15.5px;color:var(--sm-fg);}" +
|
|
210
|
+
".plan-md-root h1,.plan-md-root h2,.plan-md-root h3,.plan-md-root h4,.plan-md-root h5,.plan-md-root h6{color:var(--sm-fg);font-weight:600;line-height:1.28;margin:1.6em 0 .5em;}" +
|
|
260
211
|
".plan-md-root h1{font-size:1.95em;margin-top:.2em;}" +
|
|
261
212
|
".plan-md-root h2{font-size:1.5em;padding-bottom:.24em;border-bottom:1px solid var(--sm-border);}" +
|
|
262
213
|
".plan-md-root h3{font-size:1.22em;}.plan-md-root h4{font-size:1.05em;}" +
|
|
263
214
|
".plan-md-root h5,.plan-md-root h6{font-size:.95em;color:var(--sm-dim);}" +
|
|
264
|
-
".plan-md-root p{margin:.7em 0;}" +
|
|
265
|
-
".plan-md-root ul
|
|
266
|
-
".plan-md-root
|
|
267
|
-
".plan-md-root
|
|
268
|
-
".plan-md-root blockquote{margin:.95em 0;padding:.45em 1em;border-left:3px solid var(--sm-border);" +
|
|
269
|
-
"color:var(--sm-dim);background:var(--sm-muted);border-radius:0 6px 6px 0;}" +
|
|
270
|
-
".plan-md-root code{font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;font-size:.88em;" +
|
|
271
|
-
"background:var(--sm-muted);padding:.12em .42em;border-radius:5px;}" +
|
|
215
|
+
".plan-md-root p{margin:.7em 0;}.plan-md-root ul,.plan-md-root ol{margin:.6em 0;padding-left:1.5em;}" +
|
|
216
|
+
".plan-md-root ul{list-style:disc;}.plan-md-root ol{list-style:decimal;}.plan-md-root li{margin:.3em 0;}" +
|
|
217
|
+
".plan-md-root blockquote{margin:.95em 0;padding:.45em 1em;border-left:3px solid var(--sm-border);color:var(--sm-dim);background:var(--sm-muted);border-radius:0 6px 6px 0;}" +
|
|
218
|
+
".plan-md-root code{font-family:var(--sm-mono);font-size:.88em;background:var(--sm-muted);padding:.12em .42em;border-radius:5px;}" +
|
|
272
219
|
".plan-md-root pre{background:#0c0c0e;border:1px solid var(--sm-border);border-radius:10px;padding:14px 16px;overflow-x:auto;line-height:1.5;}" +
|
|
273
220
|
".plan-md-root pre code{background:none;padding:0;font-size:.86em;}" +
|
|
274
|
-
".plan-md-root strong{color:var(--sm-fg);font-weight:600;}.plan-md-root em{font-style:italic;}" +
|
|
275
|
-
".plan-md-root a{color:var(--sm-accent);}" +
|
|
221
|
+
".plan-md-root strong{color:var(--sm-fg);font-weight:600;}.plan-md-root em{font-style:italic;}.plan-md-root a{color:var(--sm-accent);}" +
|
|
276
222
|
".plan-md-root [contenteditable=\"true\"]{border:0;background:transparent;border-radius:0;box-shadow:none;caret-color:currentColor;outline:none;}" +
|
|
277
|
-
|
|
278
|
-
/*
|
|
279
|
-
|
|
280
|
-
"
|
|
281
|
-
|
|
282
|
-
".
|
|
283
|
-
"
|
|
284
|
-
".
|
|
285
|
-
".
|
|
286
|
-
|
|
287
|
-
".
|
|
288
|
-
"
|
|
289
|
-
"
|
|
290
|
-
".plan-
|
|
291
|
-
"
|
|
292
|
-
/* storyline tab strip */
|
|
293
|
-
".plan-storyline{margin:0;}" +
|
|
294
|
-
".plan-storyline-head{display:flex;flex-wrap:wrap;align-items:flex-start;justify-content:space-between;gap:10px 20px;padding:16px 20px 12px;}" +
|
|
295
|
-
".plan-storyline-head-text{display:flex;flex:1 1 240px;flex-direction:column;gap:3px;min-width:0;}" +
|
|
296
|
-
/* deck-level layout knob: one pick binds every slide's layout */
|
|
297
|
-
".plan-storyline-deck-layout{display:flex;flex:0 1 280px;flex-direction:column;gap:4px;min-width:200px;}" +
|
|
298
|
-
".plan-storyline-deck-layout-label{color:var(--sm-dim);font-size:9px;font-weight:700;letter-spacing:.18em;line-height:1.2;text-transform:uppercase;}" +
|
|
299
|
-
".plan-storyline-deck-layout .plan-storyboard-layout-picker{margin:0;}" +
|
|
300
|
-
".plan-storyline-deck-layout .plan-storyboard-layout-menu{left:auto;right:0;min-width:340px;}" +
|
|
301
|
-
".plan-storyline-label{color:var(--plan-story);font-size:9px;font-weight:700;letter-spacing:.18em;line-height:1.2;text-transform:uppercase;}" +
|
|
302
|
-
".plan-storyline-track{display:flex;flex-wrap:wrap;align-items:stretch;margin:0;padding:4px 14px 10px;list-style:none;" +
|
|
303
|
-
"border-bottom:1px solid var(--sm-border);row-gap:2px;}" +
|
|
304
|
-
/* grid: numeral + move text share a baseline row; the layout chip fills the
|
|
305
|
-
second row and pins to the bottom, so cards in a row stay one height */
|
|
306
|
-
".plan-storyline-beat{position:relative;display:grid;flex:1 1 150px;min-width:132px;max-width:260px;grid-template-columns:auto minmax(0,1fr);grid-template-rows:auto 1fr;" +
|
|
307
|
-
"align-items:baseline;column-gap:9px;padding:8px 8px 9px;border:0;border-radius:8px;background:transparent;cursor:pointer;transition:background-color 130ms ease;}" +
|
|
308
|
-
".plan-storyline-beat:hover{background:var(--sm-muted);}" +
|
|
309
|
-
".plan-storyline-beat--active{background:color-mix(in srgb,var(--sm-fg) 8%,transparent);}" +
|
|
310
|
-
".plan-storyline-beat--draggable{touch-action:none;}" +
|
|
311
|
-
".plan-storyline-beat--dragging{z-index:20;cursor:grabbing;background:var(--sm-surface);box-shadow:0 16px 36px rgba(0,0,0,.5);transition:none;}" +
|
|
312
|
-
".plan-storyline-beat--editing{cursor:text;}" +
|
|
313
|
-
".plan-storyline-beat-slide{grid-row:1;grid-column:1;color:color-mix(in srgb,var(--sm-dim) 55%,transparent);" +
|
|
314
|
-
"font-family:Georgia,'Times New Roman',serif;font-size:21px;font-weight:400;line-height:1;transition:color 130ms ease;}" +
|
|
315
|
-
".plan-storyline-beat--active .plan-storyline-beat-slide{color:var(--plan-story);}" +
|
|
316
|
-
".plan-storyline-beat-text{grid-row:1;grid-column:2;display:-webkit-box;min-width:0;overflow:hidden;color:var(--sm-dim);font-size:12px;" +
|
|
317
|
-
"font-weight:500;line-height:1.35;-webkit-box-orient:vertical;-webkit-line-clamp:2;}" +
|
|
318
|
-
".plan-storyline-beat--active .plan-storyline-beat-text{color:var(--sm-fg);font-weight:600;}" +
|
|
319
|
-
".plan-storyline-beat-text[contenteditable=\"true\"]{display:block;border-radius:4px;cursor:text;outline:none;-webkit-line-clamp:unset;}" +
|
|
320
|
-
".plan-storyboard-layout-picker--beat{grid-row:2;grid-column:2;align-self:end;min-width:0;margin:5px 0 0;max-width:100%;}" +
|
|
321
|
-
".plan-storyboard-layout-picker--beat .plan-storyboard-layout-current{width:auto;max-width:100%;gap:6px;border-color:transparent;border-radius:5px;padding:2px 4px;margin-left:-4px;color:var(--sm-dim);font-size:10.5px;font-weight:500;}" +
|
|
322
|
-
".plan-storyboard-layout-picker--beat .plan-storyboard-layout-current:not(:disabled):hover{border-color:var(--sm-line);}" +
|
|
323
|
-
".plan-storyboard-layout-picker--beat .plan-storyboard-layout-current>.plan-storyboard-layout-thumb{width:36px;height:20px;}" +
|
|
324
|
-
".plan-storyboard-layout-picker--beat .plan-storyboard-layout-menu{right:auto;min-width:320px;}" +
|
|
325
|
-
/* "updated" badge — this slide changed in the round that just arrived */
|
|
326
|
-
".plan-storyline-beat-updated{position:absolute;top:-6px;left:6px;border-radius:999px;padding:0 6px;" +
|
|
327
|
-
"background:color-mix(in srgb,var(--sm-accent) 22%,var(--sm-bg));color:var(--sm-accent);" +
|
|
328
|
-
"border:1px solid color-mix(in srgb,var(--sm-accent) 45%,transparent);" +
|
|
329
|
-
"font-size:8.5px;font-weight:700;letter-spacing:.08em;text-transform:uppercase;line-height:1.6;}" +
|
|
330
|
-
".plan-storyline-gap{position:relative;display:flex;flex:none;align-items:center;justify-content:center;width:22px;}" +
|
|
331
|
-
".plan-storyline-gap[data-active=\"true\"]::before{position:absolute;top:18%;bottom:18%;left:50%;width:3px;border-radius:2px;background:var(--plan-story);content:\"\";transform:translateX(-50%);}" +
|
|
332
|
-
".plan-storyline-insert{position:relative;display:flex;align-items:center;justify-content:center;width:18px;height:18px;" +
|
|
333
|
-
"border:1px solid var(--sm-border);border-radius:999px;background:var(--sm-surface);color:var(--sm-dim);cursor:pointer;" +
|
|
334
|
-
"opacity:0;padding:0;pointer-events:none;transform:scale(.85);transition:opacity 140ms ease,transform 140ms ease,background-color 140ms ease,border-color 140ms ease,color 140ms ease;}" +
|
|
223
|
+
/* the dossier: rail | stage | aside */
|
|
224
|
+
/* A wider rail than the preview gate's: these cards carry a story beat, not
|
|
225
|
+
a filename, and the beat is what the reviewer navigates by. */
|
|
226
|
+
".sm-plan{--sm-rail-w:272px;flex:1;min-height:0;display:grid;grid-template-columns:var(--sm-rail-w) minmax(0,1fr);}" +
|
|
227
|
+
/* the header's deck-wide choices, each with its name: Design, Slide Layouts */
|
|
228
|
+
".sm-bar-tools{display:flex;align-items:center;gap:16px;min-width:0;flex:0 1 auto;}" +
|
|
229
|
+
".sm-bar-tools:empty{display:none;}" +
|
|
230
|
+
".sm-bar-tool{display:flex;align-items:center;gap:8px;min-width:0;}" +
|
|
231
|
+
".sm-bar-tool-label{flex:none;font-size:10px;font-weight:600;letter-spacing:.08em;text-transform:uppercase;color:var(--sm-dim);}" +
|
|
232
|
+
".sm-bar-tool .plan-design-picker,.sm-bar-tool .plan-storyboard-layout-picker{min-width:0;max-width:280px;}" +
|
|
233
|
+
"@media (max-width:760px){.sm-bar-tool-label{display:none;}}" +
|
|
234
|
+
".sm-rail-gap{position:relative;height:10px;margin:-2px 0 0;display:flex;align-items:center;justify-content:center;}" +
|
|
235
|
+
".sm-rail-gap[data-active=\"true\"]::before{content:\"\";position:absolute;left:8%;right:8%;top:50%;height:3px;border-radius:2px;background:var(--plan-story);transform:translateY(-50%);}" +
|
|
236
|
+
".plan-storyline-insert{position:relative;z-index:2;display:flex;align-items:center;justify-content:center;width:18px;height:18px;border:1px solid var(--sm-border);border-radius:999px;" +
|
|
237
|
+
"background:var(--sm-surface);color:var(--sm-dim);cursor:pointer;opacity:0;padding:0;transform:scale(.85);transition:opacity 140ms ease,transform 140ms ease,background-color 140ms ease,border-color 140ms ease,color 140ms ease;}" +
|
|
335
238
|
".plan-storyline-insert svg{width:12px;height:12px;}" +
|
|
336
|
-
".
|
|
239
|
+
".sm-rail-gap:hover .plan-storyline-insert,.plan-storyline-insert:focus-visible{opacity:1;transform:scale(1);}" +
|
|
337
240
|
".plan-storyline-insert:hover{background:var(--plan-story);border-color:var(--plan-story);color:var(--sm-bg);transform:scale(1.1);}" +
|
|
338
|
-
".
|
|
339
|
-
"width:
|
|
340
|
-
"
|
|
241
|
+
".sm-rail-add{display:flex;align-items:center;justify-content:center;gap:6px;width:100%;margin-top:4px;padding:8px;border:1px dashed var(--sm-border);border-radius:8px;background:transparent;color:var(--sm-dim);font:500 11.5px var(--sm-font);cursor:pointer;}" +
|
|
242
|
+
".sm-rail-add svg{width:12px;height:12px;}.sm-rail-add:hover{color:var(--sm-fg);border-color:#3a3a40;background:var(--sm-muted);}" +
|
|
243
|
+
".plan-beat{touch-action:none;}" +
|
|
244
|
+
".plan-beat.is-dragging{z-index:20;cursor:grabbing;box-shadow:0 16px 36px rgba(0,0,0,.5);transition:none;}" +
|
|
245
|
+
".plan-beat.is-editing{cursor:text;}" +
|
|
246
|
+
".plan-beat .sm-thumb-name[contenteditable=\"true\"]{display:block;-webkit-line-clamp:unset;border-radius:3px;cursor:text;outline:none;background:var(--sm-muted);padding:1px 3px;margin:-1px -3px;}" +
|
|
247
|
+
".plan-beat.is-active .sm-thumb-number{color:var(--plan-story);font-weight:600;}" +
|
|
248
|
+
".plan-storyline-beat-updated{position:absolute;top:-6px;left:8px;z-index:2;border-radius:999px;padding:0 6px;background:color-mix(in srgb,var(--sm-accent) 22%,var(--sm-bg));color:var(--sm-accent);" +
|
|
249
|
+
"border:1px solid color-mix(in srgb,var(--sm-accent) 45%,transparent);font-size:8.5px;font-weight:700;letter-spacing:.08em;text-transform:uppercase;line-height:1.6;}" +
|
|
250
|
+
".plan-storyline-beat-remove{position:absolute;top:-7px;right:-7px;z-index:2;display:inline-flex;align-items:center;justify-content:center;width:19px;height:19px;border:1px solid var(--sm-border);border-radius:999px;" +
|
|
251
|
+
"background:var(--sm-surface);box-shadow:0 2px 6px rgba(0,0,0,.35);color:var(--sm-dim);cursor:pointer;opacity:0;padding:0;pointer-events:none;transition:opacity 120ms ease;}" +
|
|
341
252
|
".plan-storyline-beat-remove svg{width:12px;height:12px;}" +
|
|
342
|
-
".plan-
|
|
343
|
-
".plan-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
".
|
|
347
|
-
".
|
|
348
|
-
"
|
|
349
|
-
".
|
|
350
|
-
|
|
351
|
-
"
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
".
|
|
357
|
-
".
|
|
358
|
-
"
|
|
359
|
-
"color:var(--sm-
|
|
360
|
-
".
|
|
361
|
-
"
|
|
362
|
-
".
|
|
363
|
-
"
|
|
364
|
-
"
|
|
365
|
-
"
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
".plan-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
".plan-
|
|
374
|
-
".
|
|
375
|
-
"
|
|
376
|
-
"
|
|
377
|
-
".plan-
|
|
378
|
-
".plan-
|
|
379
|
-
".plan-
|
|
380
|
-
".plan-
|
|
381
|
-
"
|
|
382
|
-
"
|
|
383
|
-
".plan-
|
|
384
|
-
".plan-
|
|
385
|
-
".plan-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
"
|
|
390
|
-
".plan-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
"
|
|
394
|
-
".plan-
|
|
395
|
-
".plan-
|
|
396
|
-
"
|
|
397
|
-
".plan-
|
|
398
|
-
"
|
|
399
|
-
".plan-
|
|
400
|
-
".plan-
|
|
401
|
-
".plan-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
".plan-
|
|
405
|
-
".plan-
|
|
406
|
-
".plan-
|
|
407
|
-
".plan-
|
|
408
|
-
".plan-
|
|
409
|
-
".plan-
|
|
410
|
-
".plan-
|
|
411
|
-
".plan-
|
|
412
|
-
".plan-
|
|
413
|
-
".plan-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
".plan-
|
|
417
|
-
".plan-
|
|
418
|
-
".plan-
|
|
419
|
-
".plan-
|
|
420
|
-
".plan-
|
|
253
|
+
".plan-beat:hover .plan-storyline-beat-remove{opacity:1;pointer-events:auto;}" +
|
|
254
|
+
".plan-beat.is-dragging .plan-storyline-beat-remove,.plan-beat.is-editing .plan-storyline-beat-remove,.plan-storyline-beat-remove:disabled{opacity:0;pointer-events:none;}" +
|
|
255
|
+
/* the mini storyboard on a rail card */
|
|
256
|
+
".sm-mini{position:absolute;inset:0;}" +
|
|
257
|
+
".sm-mini-chrome{position:absolute;inset:0;width:100%;height:100%;object-fit:cover;}" +
|
|
258
|
+
".sm-mini-block{position:absolute;border:1px dashed color-mix(in srgb,#3565d4 70%,transparent);border-radius:2px;background:rgba(53,101,212,.14);}" +
|
|
259
|
+
".sm-mini-block[data-emphasis=\"primary\"]{border-style:solid;background:rgba(53,101,212,.22);}" +
|
|
260
|
+
".sm-mini-empty{position:absolute;inset:0;display:flex;align-items:center;justify-content:center;color:#8a8a90;font-size:10px;background:#f4f4f5;}" +
|
|
261
|
+
/* the stage: the selected storyboard, fitted to the column */
|
|
262
|
+
".sm-stage{container-type:size;min-width:0;min-height:0;display:flex;flex-direction:column;align-items:center;justify-content:center;padding:16px 22px;overflow:auto;background:#0a0a0b;}" +
|
|
263
|
+
/* The PANEL is the subject, not the slide. Letting the slide fill the
|
|
264
|
+
column made a wide grey field around a small box, which read as "look
|
|
265
|
+
at the slide" when the job is "correct the text" — so the slide is
|
|
266
|
+
capped to a reading size and the panel takes a large fraction OF it. */
|
|
267
|
+
".sm-stage-fit{width:min(100%,980px,calc((100cqh - 96px) * 16 / 9));min-width:min(100%,360px);}" +
|
|
268
|
+
".sm-stage-head{display:flex;align-items:baseline;justify-content:space-between;gap:12px;margin-bottom:8px;font-size:11px;font-weight:600;text-transform:uppercase;letter-spacing:.04em;color:var(--sm-dim);}" +
|
|
269
|
+
".sm-stage-head .t{min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;}" +
|
|
270
|
+
".sm-stage-head .t b{color:var(--sm-fg);text-transform:none;letter-spacing:0;font-weight:500;}" +
|
|
271
|
+
".sm-round-banner{display:flex;align-items:center;gap:9px;margin:0 0 12px;padding:9px 13px;border:1px solid color-mix(in srgb,var(--sm-accent) 35%,transparent);border-radius:10px;" +
|
|
272
|
+
"background:color-mix(in srgb,var(--sm-accent) 9%,transparent);color:var(--sm-dim);font-size:12.5px;line-height:1.5;animation:sm-rise .45s cubic-bezier(.2,.7,.2,1) both;}" +
|
|
273
|
+
".sm-round-banner svg{width:15px;height:15px;flex-shrink:0;color:var(--sm-accent);}.sm-round-banner b{color:var(--sm-fg);font-weight:600;}" +
|
|
274
|
+
".plan-dossier-new-slide{display:flex;align-items:flex-start;gap:10px;border:1.5px dashed var(--plan-story-line);border-radius:10px;background:color-mix(in srgb,#b41830 6%,transparent);color:var(--sm-dim);font-size:13px;line-height:1.5;padding:22px 18px;}" +
|
|
275
|
+
".plan-dossier-new-slide svg{width:14px;height:14px;flex:none;margin-top:2px;color:var(--plan-story);}" +
|
|
276
|
+
".plan-dossier-new-slide p{margin:0;}.plan-dossier-new-slide strong{color:var(--sm-fg);font-weight:600;}" +
|
|
277
|
+
/* the slide on stage: the layout's own render with its slot geometry on
|
|
278
|
+
top, a light veil pushing it back, and ONE panel floating over both. The
|
|
279
|
+
panel is the only thing on the stage with elevation — that is what makes
|
|
280
|
+
it read as an overlay ON the slide rather than content IN it. */
|
|
281
|
+
".plan-stage-frame{position:relative;width:100%;container-type:inline-size;}" +
|
|
282
|
+
/* The slide itself: the shared embossed card treatment, and the only
|
|
283
|
+
element here that clips — the panel above it has a menu that escapes. */
|
|
284
|
+
".plan-stage-slide{position:relative;overflow:hidden;border-radius:12px;background:#fff;}" +
|
|
285
|
+
".slide-emboss{box-shadow:0 1px 2px rgba(0,0,0,.5),0 10px 26px rgba(0,0,0,.42),inset 0 1px 0 rgba(255,255,255,.07);}" +
|
|
286
|
+
".plan-stage-chrome{display:block;width:100%;aspect-ratio:16/9;object-fit:cover;border:0;background:#fff;}" +
|
|
287
|
+
".plan-stage-chrome--blank{background:#16161a;border:1px solid var(--sm-border);}" +
|
|
288
|
+
".plan-stage-slots{position:absolute;inset:0;pointer-events:none;}" +
|
|
289
|
+
".plan-stage-slot{position:absolute;border:1px solid rgba(15,23,42,.45);border-radius:3px;background:rgba(255,255,255,.1);box-shadow:0 0 0 1px rgba(255,255,255,.45);}" +
|
|
290
|
+
".plan-stage-slot-label{position:absolute;top:-1px;left:-1px;border-radius:2px 0 3px 0;background:rgba(15,23,42,.72);padding:0 5px;color:#fff;font-size:8px;font-weight:700;letter-spacing:.05em;text-transform:uppercase;}" +
|
|
291
|
+
".sm-mini-slots .plan-stage-slot{border-width:.5px;box-shadow:none;}" +
|
|
292
|
+
".sm-mini-slots .plan-stage-slot-label{display:none;}" +
|
|
293
|
+
".sm-mini-slots .plan-stage-zone{border-width:1px;}" +
|
|
294
|
+
".plan-stage-zone{position:absolute;border:1.5px dashed rgba(53,101,212,.7);border-radius:4px;background:rgba(53,101,212,.07);}" +
|
|
295
|
+
".plan-stage-veil{position:absolute;inset:0;background:rgba(9,11,16,.22);pointer-events:none;}" +
|
|
296
|
+
".plan-stage-card{position:absolute;top:50%;left:50%;transform:translate(-50%,-50%);display:flex;width:84%;max-height:88%;flex-direction:column;gap:clamp(10px,1.7cqw,18px);" +
|
|
297
|
+
/* Glass, not a lid: the slide stays readable through it, so the reviewer
|
|
298
|
+
can see the layout they are choosing copy for. The blur is what keeps
|
|
299
|
+
text legible over a busy render — opacity alone would not. */
|
|
300
|
+
"border:1px solid color-mix(in srgb,var(--sm-fg) 18%,transparent);border-radius:12px;background:color-mix(in srgb,var(--sm-surface) 76%,transparent);box-shadow:inset 0 1px 0 color-mix(in srgb,var(--sm-fg) 11%,transparent),0 2px 6px rgba(0,0,0,.3),0 26px 64px rgba(0,0,0,.46);padding:clamp(16px,2.4cqw,26px) clamp(16px,2.6cqw,28px) clamp(12px,1.8cqw,20px);backdrop-filter:blur(16px) saturate(115%);}" +
|
|
301
|
+
"@supports not (backdrop-filter:blur(1px)){.plan-stage-card{background:color-mix(in srgb,var(--sm-surface) 97%,transparent);}}" +
|
|
302
|
+
/* label gutter + content column: the label names which KIND of thing this
|
|
303
|
+
is, which is why it earns the space */
|
|
304
|
+
".plan-stage-row{display:grid;min-height:0;margin-left:clamp(-9px,-.8cqw,-5px);grid-template-columns:calc(clamp(68px,10.5cqw,104px) - clamp(-9px,-.8cqw,-5px)) minmax(0,1fr);gap:4px clamp(10px,1.6cqw,18px);}" +
|
|
305
|
+
".plan-stage-label{color:var(--sm-dim);font-size:clamp(10.5px,1.25cqw,12.5px);font-weight:500;line-height:1.5;padding-top:clamp(4px,.7cqw,7px);}" +
|
|
306
|
+
".plan-stage-beat{color:var(--sm-fg);font-size:clamp(14px,2.25cqw,21px);font-weight:600;line-height:1.3;outline:none;white-space:pre-wrap;word-break:break-word;}" +
|
|
307
|
+
".plan-stage-points{display:flex;min-height:0;flex-direction:column;gap:2px;overflow-y:auto;scrollbar-width:none;}" +
|
|
308
|
+
".plan-stage-points::-webkit-scrollbar{display:none;}" +
|
|
309
|
+
".plan-stage-bullets{display:flex;flex-direction:column;gap:clamp(2px,.5cqw,6px);margin:0;padding:0;list-style:none;}" +
|
|
310
|
+
".plan-stage-bullet{display:flex;align-items:flex-start;gap:8px;color:var(--sm-fg);font-size:clamp(12px,1.55cqw,15px);line-height:1.5;}" +
|
|
311
|
+
".plan-stage-bullet::before{flex:none;color:var(--plan-story);content:\"\\2022\";font-weight:700;line-height:inherit;}" +
|
|
312
|
+
".plan-stage-bullet-text{min-width:0;flex:1;outline:none;white-space:pre-wrap;word-break:break-word;}" +
|
|
313
|
+
/* These two fields ARE the gate. At rest they read as copy; the moment a
|
|
314
|
+
pointer crosses them they read as inputs, and focus draws a real ring. */
|
|
315
|
+
".plan-stage-beat[contenteditable=\"true\"],.plan-stage-bullet-text[contenteditable=\"true\"]{border-radius:6px;cursor:text;padding:clamp(3px,.5cqw,6px) clamp(5px,.8cqw,9px);transition:background 120ms ease,box-shadow 120ms ease;}" +
|
|
316
|
+
".plan-stage-beat[contenteditable=\"true\"]:hover,.plan-stage-bullet-text[contenteditable=\"true\"]:hover{background:var(--sm-muted);}" +
|
|
317
|
+
".plan-stage-beat:focus,.plan-stage-bullet-text:focus{background:color-mix(in srgb,var(--sm-fg) 8%,transparent);box-shadow:0 0 0 1px color-mix(in srgb,var(--sm-fg) 26%,transparent);}" +
|
|
318
|
+
".plan-stage-bullet-remove{display:inline-flex;flex:none;align-items:center;justify-content:center;width:16px;height:16px;margin-top:2px;border:0;border-radius:999px;background:transparent;color:var(--sm-dim);cursor:pointer;opacity:0;padding:0;transition:opacity 120ms ease;}" +
|
|
319
|
+
".plan-stage-bullet-remove svg{width:11px;height:11px;}" +
|
|
320
|
+
".plan-stage-bullet:hover .plan-stage-bullet-remove,.plan-stage-bullet:focus-within .plan-stage-bullet-remove{opacity:1;}" +
|
|
321
|
+
".plan-stage-bullet-remove:hover{background:var(--sm-muted);color:var(--sm-fg);}" +
|
|
322
|
+
".plan-stage-bullet-add{display:inline-flex;align-self:flex-start;align-items:center;gap:5px;margin-top:5px;border:0;border-radius:6px;background:transparent;color:var(--sm-dim);cursor:pointer;font-family:var(--sm-font);font-size:clamp(11.5px,1.35cqw,13.5px);font-weight:500;padding:clamp(3px,.5cqw,6px) clamp(5px,.8cqw,9px);}" +
|
|
323
|
+
".plan-stage-bullet-add svg{width:11px;height:11px;}" +
|
|
324
|
+
".plan-stage-bullet-add:hover{color:var(--sm-fg);}" +
|
|
325
|
+
/* the slide's approved image uses: read-only, one row per use — the file
|
|
326
|
+
the user handed over, shown, next to what it is FOR in plain words */
|
|
327
|
+
".plan-stage-uses{display:flex;min-height:0;flex-direction:column;gap:clamp(5px,.8cqw,9px);overflow-y:auto;scrollbar-width:none;}" +
|
|
328
|
+
".plan-stage-uses::-webkit-scrollbar{display:none;}" +
|
|
329
|
+
".plan-stage-use{display:flex;align-items:flex-start;gap:clamp(7px,1.1cqw,11px);}" +
|
|
330
|
+
".plan-stage-use-thumb{flex:none;width:clamp(38px,5.4cqw,54px);height:clamp(26px,3.7cqw,37px);border:1px solid var(--sm-border);border-radius:5px;background:var(--sm-muted);object-fit:contain;}" +
|
|
331
|
+
".plan-stage-use-body{min-width:0;flex:1;display:flex;flex-direction:column;gap:1px;}" +
|
|
332
|
+
".plan-stage-use-what{color:var(--sm-fg);font-size:clamp(11.5px,1.45cqw,14px);font-weight:600;line-height:1.4;}" +
|
|
333
|
+
".plan-stage-use-name{color:var(--sm-dim);font-weight:500;}" +
|
|
334
|
+
".plan-stage-use-instruction{color:var(--sm-fg);font-size:clamp(11px,1.35cqw,13.5px);line-height:1.45;word-break:break-word;}" +
|
|
335
|
+
".plan-stage-use-framing{color:var(--sm-dim);font-size:clamp(10.5px,1.2cqw,12.5px);line-height:1.45;}" +
|
|
336
|
+
".plan-stage-foot{display:grid;flex:none;align-items:center;margin-left:clamp(-9px,-.8cqw,-5px);grid-template-columns:calc(clamp(68px,10.5cqw,104px) - clamp(-9px,-.8cqw,-5px)) minmax(0,1fr);gap:clamp(10px,1.6cqw,18px);border-top:1px solid color-mix(in srgb,var(--sm-fg) 12%,transparent);padding-top:clamp(10px,1.5cqw,16px);}" +
|
|
337
|
+
".plan-stage-foot .plan-stage-label{padding-top:0;}" +
|
|
338
|
+
".plan-stage-card .plan-stage-layout{width:auto;max-width:100%;margin:0;}" +
|
|
339
|
+
/* A real button, not a caption: bordered, filled, and it lifts on hover. */
|
|
340
|
+
".plan-stage-card .plan-stage-layout .plan-storyboard-layout-current{width:auto;gap:9px;border-color:color-mix(in srgb,var(--sm-fg) 26%,transparent);border-radius:8px;background:color-mix(in srgb,var(--sm-fg) 7%,transparent);padding:clamp(4px,.6cqw,7px) clamp(9px,1.1cqw,13px) clamp(4px,.6cqw,7px) clamp(4px,.6cqw,7px);font-size:clamp(11.5px,1.35cqw,13.5px);font-weight:600;}" +
|
|
341
|
+
".plan-stage-card .plan-stage-layout .plan-storyboard-layout-current:not(:disabled):hover{border-color:color-mix(in srgb,var(--sm-fg) 42%,transparent);background:color-mix(in srgb,var(--sm-fg) 12%,transparent);}" +
|
|
342
|
+
".plan-stage-card .plan-stage-layout .plan-storyboard-layout-thumb{width:clamp(34px,4.4cqw,46px);height:clamp(19px,2.5cqw,26px);}" +
|
|
343
|
+
/* a narrow panel has no room for a gutter: the label sits above its content */
|
|
344
|
+
"@container (max-width:620px){" +
|
|
345
|
+
".plan-stage-card{width:90%;gap:12px;}" +
|
|
346
|
+
".plan-stage-row,.plan-stage-foot{grid-template-columns:minmax(0,1fr);gap:2px;}" +
|
|
347
|
+
".plan-stage-foot .plan-stage-layout{margin-top:4px;}" +
|
|
348
|
+
".plan-stage-beat{font-size:15px;}" +
|
|
349
|
+
"}" +
|
|
350
|
+
".plan-storyboard-warnings{margin:.75em 0 0;padding-left:1.2em;font-size:.86em;color:var(--sm-dim);}" +
|
|
351
|
+
".plan-storyboard-error{margin:0;border:1px solid var(--sm-danger);border-radius:8px;background:color-mix(in srgb,var(--sm-danger) 10%,transparent);padding:12px;}" +
|
|
352
|
+
".plan-storyboard-error p{margin:0 0 8px;font-weight:600;}.plan-storyboard-error pre{margin:0;overflow-x:auto;font-size:.82em;color:var(--sm-dim);}" +
|
|
353
|
+
".plan-storyboard-layout-unknown{display:inline-block;border-radius:999px;background:rgba(217,75,75,.14);padding:1px 7px;color:#e07272;font-size:10px;font-weight:650;}" +
|
|
354
|
+
".plan-storyboard-layout-purpose{color:var(--sm-dim);font-size:11px;line-height:1.35;margin:0 0 6px;}" +
|
|
355
|
+
".plan-storyboard-draft-note{display:flex;align-items:center;justify-content:center;gap:7px;margin:10px 0 0;color:var(--sm-dim);font-size:11.5px;line-height:1.4;}" +
|
|
356
|
+
".plan-storyboard-draft-note svg{width:14px;height:14px;flex:none;}" +
|
|
357
|
+
".plan-storyboard-draft-note p{margin:0;}.plan-storyboard-draft-note strong{color:var(--sm-fg);font-weight:600;}" +
|
|
358
|
+
/* the layout picker: the deck default in the header, the slide pick on the card */
|
|
359
|
+
".plan-storyboard-layout-picker{position:relative;margin:0;}" +
|
|
360
|
+
".plan-storyboard-layout-current{display:flex;align-items:center;gap:8px;width:100%;border:1px solid var(--sm-border);border-radius:8px;background:transparent;padding:5px 8px;color:var(--sm-fg);font-size:11.5px;font-weight:500;text-align:left;cursor:pointer;font-family:inherit;}" +
|
|
421
361
|
".plan-storyboard-layout-current:disabled{cursor:default;opacity:.85;}" +
|
|
422
362
|
".plan-storyboard-layout-current:not(:disabled):hover{background:rgba(255,255,255,.05);}" +
|
|
423
|
-
".plan-storyboard-layout-thumb{width:
|
|
363
|
+
".plan-storyboard-layout-thumb{width:36px;height:20px;flex:none;border:1px solid var(--sm-border);border-radius:3px;object-fit:cover;background:#fff;}" +
|
|
424
364
|
".plan-storyboard-layout-name{min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;}" +
|
|
425
|
-
|
|
426
|
-
".plan-storyboard-layout-
|
|
427
|
-
"
|
|
365
|
+
/* the same drawn chevron as the comment target: a "▾" glyph rendered as a dot */
|
|
366
|
+
".plan-storyboard-layout-chevron{display:flex;flex:none;margin-left:auto;color:var(--sm-dim);transition:transform .15s ease;}.plan-storyboard-layout-chevron svg{width:10px;height:6px;}" +
|
|
367
|
+
"[aria-expanded=\"true\"]>.plan-storyboard-layout-chevron{transform:rotate(180deg);}" +
|
|
368
|
+
".plan-storyboard-layout-menu{position:absolute;z-index:30;top:calc(100% + 4px);left:0;min-width:280px;max-height:320px;overflow:auto;border:1px solid var(--sm-border);border-radius:10px;background:var(--sm-surface);box-shadow:0 10px 32px rgba(0,0,0,.45);padding:4px;}" +
|
|
428
369
|
".plan-storyboard-layout-option{display:flex;align-items:center;gap:8px;width:100%;border:0;border-radius:7px;background:transparent;padding:6px;color:var(--sm-fg);text-align:left;cursor:pointer;font-family:inherit;}" +
|
|
429
|
-
".plan-storyboard-layout-option:hover{background:rgba(255,255,255,.06);}" +
|
|
430
|
-
".plan-storyboard-layout-option
|
|
431
|
-
".plan-storyboard-layout-thumb--option{width:72px;height:41px;}" +
|
|
370
|
+
".plan-storyboard-layout-option:hover{background:rgba(255,255,255,.06);}.plan-storyboard-layout-option--selected{background:rgba(255,255,255,.09);}" +
|
|
371
|
+
".plan-storyboard-layout-thumb--option{width:64px;height:36px;}" +
|
|
432
372
|
".plan-storyboard-layout-option-text{display:flex;min-width:0;flex-direction:column;gap:1px;}" +
|
|
433
|
-
".plan-storyboard-layout-option-text .plan-storyboard-layout-name{font-size:
|
|
434
|
-
".plan-storyboard-layout-purpose{overflow:hidden;color:var(--sm-dim);font-size:11px;line-height:1.3;display:-webkit-box;-webkit-box-orient:vertical;-webkit-line-clamp:2;}" +
|
|
373
|
+
".plan-storyboard-layout-option-text .plan-storyboard-layout-name{font-size:12px;font-weight:600;}" +
|
|
435
374
|
".plan-storyboard-layout-canvas{color:var(--sm-dim);font-size:10px;font-variant-numeric:tabular-nums;}" +
|
|
436
|
-
|
|
437
|
-
".plan-
|
|
438
|
-
".plan-
|
|
439
|
-
".plan-
|
|
440
|
-
".plan-
|
|
441
|
-
".plan-storyboard-
|
|
442
|
-
"
|
|
443
|
-
".plan-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
"
|
|
447
|
-
".
|
|
448
|
-
"border:1px solid
|
|
375
|
+
/* the design picker: name, then the design's accent colours */
|
|
376
|
+
".plan-design-picker{position:relative;margin:0;}" +
|
|
377
|
+
".plan-design-current,.plan-design-option{display:flex;align-items:center;gap:8px;width:100%;border:1px solid var(--sm-border);border-radius:8px;background:transparent;padding:6px 8px;color:var(--sm-fg);font:500 11.5px var(--sm-font);text-align:left;cursor:pointer;}" +
|
|
378
|
+
".plan-design-current:disabled{cursor:default;opacity:.85;}" +
|
|
379
|
+
".plan-design-current:not(:disabled):hover{background:rgba(255,255,255,.05);}" +
|
|
380
|
+
".plan-design-current .plan-storyboard-layout-chevron{margin-left:0;}" +
|
|
381
|
+
".plan-design-name{min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;}" +
|
|
382
|
+
".plan-design-dots{display:inline-flex;flex:none;gap:3px;margin-left:auto;}" +
|
|
383
|
+
".plan-design-dot{display:block;width:10px;height:10px;border-radius:50%;box-shadow:inset 0 0 0 1px rgba(255,255,255,.16);}" +
|
|
384
|
+
/* a design the backend serves that this machine has not downloaded yet */
|
|
385
|
+
".plan-design-get{flex:none;margin-left:auto;color:var(--sm-dim);font-size:10.5px;font-weight:500;}" +
|
|
386
|
+
".plan-design-current.is-busy{cursor:progress;}.plan-design-current.is-busy .plan-design-name{color:var(--sm-dim);}" +
|
|
387
|
+
".plan-design-menu{position:absolute;z-index:30;top:calc(100% + 4px);left:0;min-width:100%;max-height:320px;overflow:auto;border:1px solid var(--sm-border);border-radius:10px;background:var(--sm-surface);box-shadow:0 10px 32px rgba(0,0,0,.45);padding:4px;}" +
|
|
388
|
+
".plan-design-option{border:0;border-radius:7px;padding:8px;font-size:12px;}" +
|
|
389
|
+
".plan-design-option:hover,.plan-design-option:focus-visible{background:rgba(255,255,255,.06);outline:none;}.plan-design-option.is-selected{background:rgba(255,255,255,.09);}" +
|
|
390
|
+
/* designs not on this computer yet: last, smaller and quieter, under one divider */
|
|
391
|
+
".plan-design-option.is-remote{padding:4px 8px;font-size:11px;color:var(--sm-dim);}" +
|
|
392
|
+
".plan-design-option.is-first-remote{position:relative;margin-top:9px;}" +
|
|
393
|
+
".plan-design-option.is-first-remote::before{content:'';position:absolute;left:8px;right:8px;top:-5px;border-top:1px solid var(--sm-border);}" +
|
|
394
|
+
/* small screens: the rail becomes a horizontal strip above the stage */
|
|
395
|
+
"@media (max-width:960px){" +
|
|
396
|
+
".sm-plan{display:block;overflow-y:auto;}" +
|
|
397
|
+
".sm-rail{border-right:0;border-bottom:1px solid var(--sm-border);}" +
|
|
398
|
+
".sm-rail-list{display:flex;align-items:flex-start;overflow-x:auto;overflow-y:hidden;padding:4px 12px 10px;}" +
|
|
399
|
+
".sm-rail-gap{width:12px;height:auto;align-self:stretch;margin:0;flex:none;}" +
|
|
400
|
+
".sm-rail-gap[data-active=\"true\"]::before{left:50%;right:auto;top:8%;bottom:8%;height:auto;width:3px;transform:translateX(-50%);}" +
|
|
401
|
+
".sm-thumb{width:150px;flex:none;margin:0;}" +
|
|
402
|
+
".sm-rail-add{width:auto;flex:none;align-self:center;margin:0 0 0 6px;white-space:nowrap;}" +
|
|
403
|
+
".sm-stage{container-type:normal;min-height:0;overflow:visible;justify-content:flex-start;padding:14px;}" +
|
|
404
|
+
".sm-stage-fit{width:100%;}" +
|
|
405
|
+
"}" +
|
|
406
|
+
/* the composer: the unsent comments, then ONE row — what the comment is
|
|
407
|
+
about, the comment itself, Send (only once there is something to send)
|
|
408
|
+
and Approve */
|
|
409
|
+
".sm-composer{flex-shrink:0;display:flex;flex-direction:column;gap:8px;padding:12px 16px;border-top:1px solid var(--sm-border);background:var(--sm-surface);font-family:var(--sm-font);}" +
|
|
410
|
+
".sm-composer[hidden]{display:none;}" +
|
|
411
|
+
".sm-story-banner{display:flex;align-items:center;gap:8px;padding:7px 10px;font-size:12px;line-height:1.5;border:1px solid color-mix(in srgb,#b41830 30%,transparent);border-radius:9px;background:color-mix(in srgb,#b41830 8%,transparent);}" +
|
|
449
412
|
".sm-story-banner svg{width:14px;height:14px;flex-shrink:0;color:var(--plan-story);}" +
|
|
450
|
-
".sm-story-banner .txt{flex:1;min-width:0;color:var(--sm-dim);}" +
|
|
451
|
-
".sm-story-
|
|
452
|
-
".sm-story-discard{flex-shrink:0;border:0;background:transparent;color:var(--sm-dim);font-family:var(--sm-font);font-weight:500;" +
|
|
453
|
-
"font-size:12px;padding:4px 8px;border-radius:6px;cursor:pointer;transition:background .12s,color .12s;}" +
|
|
413
|
+
".sm-story-banner .txt{flex:1;min-width:0;color:var(--sm-dim);}.sm-story-banner .txt b{color:var(--sm-fg);font-weight:600;}" +
|
|
414
|
+
".sm-story-discard{flex-shrink:0;border:0;background:transparent;color:var(--sm-dim);font-family:var(--sm-font);font-weight:500;font-size:12px;padding:4px 8px;border-radius:6px;cursor:pointer;transition:background .12s,color .12s;}" +
|
|
454
415
|
".sm-story-discard:hover{background:var(--sm-muted);color:var(--sm-fg);}" +
|
|
455
|
-
|
|
456
|
-
".sm-
|
|
457
|
-
"
|
|
458
|
-
".sm-
|
|
459
|
-
".sm-
|
|
460
|
-
".sm-
|
|
461
|
-
".sm-
|
|
462
|
-
"
|
|
463
|
-
".sm-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
".sm-
|
|
467
|
-
".sm-
|
|
468
|
-
".sm-
|
|
469
|
-
".sm-
|
|
470
|
-
"
|
|
471
|
-
"
|
|
472
|
-
".sm-
|
|
473
|
-
"
|
|
474
|
-
".sm-
|
|
475
|
-
".sm-
|
|
476
|
-
"
|
|
477
|
-
".sm-
|
|
478
|
-
".sm-
|
|
479
|
-
"
|
|
480
|
-
".sm-
|
|
481
|
-
".sm-
|
|
482
|
-
".sm-send
|
|
483
|
-
".sm-send
|
|
484
|
-
|
|
485
|
-
".sm-send-pill{display:inline-flex;align-items:center;gap:7px;height:38px;flex-shrink:0;border:0;border-radius:999px;" +
|
|
486
|
-
"padding:0 16px;background:var(--sm-accent);color:#0f1220;font-family:var(--sm-font);font-weight:600;font-size:12.5px;cursor:pointer;white-space:nowrap;" +
|
|
487
|
-
"transition:transform .14s ease,box-shadow .14s ease,opacity .14s ease;}" +
|
|
488
|
-
".sm-send-pill svg{width:15px;height:15px;}" +
|
|
416
|
+
".sm-drafts{display:flex;flex-direction:column;gap:3px;max-height:128px;overflow-y:auto;margin:0;padding:0;list-style:none;}" +
|
|
417
|
+
".sm-draft{display:flex;align-items:center;gap:10px;padding:4px 4px 4px 11px;border-radius:8px;background:var(--sm-muted);font-size:12.5px;line-height:1.45;}" +
|
|
418
|
+
".sm-draft-anchor{flex:none;color:var(--sm-dim);font-size:11.5px;font-weight:600;white-space:nowrap;}" +
|
|
419
|
+
".sm-draft-text{flex:1;min-width:0;overflow:hidden;color:var(--sm-fg);text-overflow:ellipsis;white-space:nowrap;}" +
|
|
420
|
+
".sm-draft-del{flex:none;display:flex;align-items:center;justify-content:center;width:22px;height:22px;border:0;border-radius:50%;background:transparent;color:var(--sm-dim);cursor:pointer;}" +
|
|
421
|
+
".sm-draft-del svg{width:12px;height:12px;}.sm-draft-del:hover{background:rgba(255,255,255,.08);color:var(--sm-fg);}" +
|
|
422
|
+
".sm-composer-row{display:flex;align-items:flex-end;gap:8px;}" +
|
|
423
|
+
".sm-chat-box{flex:1;min-width:0;display:flex;align-items:flex-end;gap:4px;padding:4px 4px 4px 5px;border:1px solid var(--sm-border);border-radius:14px;background:#0c0c0e;transition:border-color .15s;}" +
|
|
424
|
+
".sm-chat-box:focus-within{border-color:var(--sm-accent);}" +
|
|
425
|
+
/* what the comment is about: a small menu of its own — a native select
|
|
426
|
+
opens the system's list, white on this dark page */
|
|
427
|
+
".sm-target-wrap{position:relative;flex:none;}" +
|
|
428
|
+
".sm-target{display:inline-flex;align-items:center;gap:7px;height:30px;max-width:170px;margin:0;border:0;border-radius:10px;padding:0 10px;background:var(--sm-muted);color:var(--sm-fg);font:600 11.5px var(--sm-font);cursor:pointer;transition:background .12s;}" +
|
|
429
|
+
".sm-target:hover,.sm-target[aria-expanded=\"true\"]{background:rgba(255,255,255,.09);}.sm-target:focus-visible{outline:2px solid var(--sm-accent);outline-offset:1px;}" +
|
|
430
|
+
".sm-target-label{min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;}" +
|
|
431
|
+
".sm-target-chevron{display:flex;flex:none;color:var(--sm-dim);transition:transform .15s ease;}.sm-target-chevron svg{width:10px;height:6px;}" +
|
|
432
|
+
".sm-target[aria-expanded=\"true\"] .sm-target-chevron{transform:rotate(180deg);}" +
|
|
433
|
+
".sm-target-menu{position:absolute;z-index:40;left:-5px;bottom:calc(100% + 10px);width:max-content;min-width:220px;max-width:min(340px,calc(100vw - 32px));padding:4px;" +
|
|
434
|
+
"border:1px solid var(--sm-border);border-radius:12px;background:var(--sm-surface-2);box-shadow:0 -12px 36px rgba(0,0,0,.5);animation:sm-pop-in .14s ease;}" +
|
|
435
|
+
".sm-target-option{display:flex;align-items:center;gap:10px;width:100%;border:0;border-radius:8px;background:transparent;padding:7px 9px;color:var(--sm-fg);font-family:var(--sm-font);text-align:left;cursor:pointer;}" +
|
|
436
|
+
".sm-target-option:hover,.sm-target-option:focus-visible{background:rgba(255,255,255,.06);outline:none;}.sm-target-option.is-selected{background:rgba(169,194,255,.1);}" +
|
|
437
|
+
".sm-target-text{display:flex;flex:1;min-width:0;flex-direction:column;gap:1px;}" +
|
|
438
|
+
".sm-target-option-label{font-size:12px;font-weight:600;}" +
|
|
439
|
+
".sm-target-option-detail{overflow:hidden;color:var(--sm-dim);font-size:11px;text-overflow:ellipsis;white-space:nowrap;}" +
|
|
440
|
+
".sm-target-check{display:flex;flex:none;width:14px;color:var(--sm-accent);}.sm-target-check svg{width:14px;height:14px;}" +
|
|
441
|
+
".sm-chat-ta{flex:1;min-width:0;min-height:30px;max-height:150px;resize:none;border:0;background:transparent;padding:5px 8px;color:var(--sm-fg);font:13px/1.5 var(--sm-font);outline:none;}" +
|
|
442
|
+
".sm-chat-ta::placeholder{color:#6b6b72;}" +
|
|
443
|
+
".sm-send-pill,.sm-approve{display:inline-flex;align-items:center;gap:7px;height:40px;flex-shrink:0;border:0;border-radius:999px;padding:0 17px;font:600 12.5px var(--sm-font);cursor:pointer;white-space:nowrap;transition:transform .14s ease,box-shadow .14s ease,background .14s ease,opacity .14s ease;}" +
|
|
444
|
+
".sm-send-pill svg,.sm-approve svg{width:15px;height:15px;}" +
|
|
445
|
+
".sm-send-pill{background:var(--sm-accent);color:#0f1220;}" +
|
|
489
446
|
".sm-send-pill:hover{transform:translateY(-1px);box-shadow:0 8px 22px rgba(169,194,255,.35);}" +
|
|
490
|
-
".sm-
|
|
491
|
-
".sm-composer-sep{width:1px;align-self:stretch;background:var(--sm-border);}" +
|
|
492
|
-
".sm-approve{display:inline-flex;flex-direction:column;align-items:center;gap:1px;border:0;border-radius:12px;padding:7px 16px;" +
|
|
493
|
-
"background:#f4f4f5;color:#0f0f0f;cursor:pointer;font-family:var(--sm-font);transition:transform .14s ease,background .14s ease,box-shadow .14s ease;white-space:nowrap;}" +
|
|
447
|
+
".sm-approve{background:#f4f4f5;color:#0f0f0f;}.sm-approve svg{color:var(--sm-success);}" +
|
|
494
448
|
".sm-approve:hover{background:#fff;transform:translateY(-1px);box-shadow:0 6px 18px rgba(0,0,0,.45);}" +
|
|
495
|
-
|
|
496
|
-
".sm-approve
|
|
497
|
-
".sm-approve .sub{font-size:9.5px;font-weight:500;color:#61616a;}" +
|
|
498
|
-
".sm-approve.is-armed{background:#f6c454;color:#241a00;}" +
|
|
499
|
-
".sm-approve.is-armed .main svg{color:#241a00;}" +
|
|
500
|
-
".sm-approve.is-off{opacity:.45;cursor:not-allowed;pointer-events:none;}" +
|
|
501
|
-
/* waiting state */
|
|
502
|
-
"@keyframes sm-rotate{to{transform:rotate(360deg)}}" +
|
|
503
|
-
".sm-wait{display:flex;align-items:center;gap:12px;padding:7px 2px;font-size:12.5px;color:var(--sm-dim);line-height:1.5;}" +
|
|
504
|
-
".sm-wait b{color:var(--sm-fg);font-weight:600;}" +
|
|
505
|
-
".sm-spin{width:17px;height:17px;border-radius:50%;border:2px solid var(--sm-border);border-top-color:var(--sm-accent);animation:sm-rotate .9s linear infinite;flex-shrink:0;}" +
|
|
506
|
-
/* done veil */
|
|
507
|
-
".sm-veil{position:fixed;inset:0;z-index:2147483647;display:flex;flex-direction:column;align-items:center;justify-content:center;gap:30px;" +
|
|
508
|
-
"font-family:var(--sm-font);color:var(--sm-fg);background:radial-gradient(ellipse at center,rgba(16,16,18,.93),rgba(5,5,7,.98));" +
|
|
509
|
-
"-webkit-backdrop-filter:blur(7px);backdrop-filter:blur(7px);animation:sm-veil-in .4s ease;}" +
|
|
510
|
-
".sm-veil .sm-logo{width:188px;height:106px;border-radius:14px;animation:sm-rise .6s cubic-bezier(.2,.7,.2,1) both;}" +
|
|
511
|
-
".sm-veil .word{display:flex;flex-direction:column;gap:6px;font-family:var(--sm-word);font-weight:300;font-size:36px;letter-spacing:.6em;text-align:center;animation:sm-rise .6s .06s cubic-bezier(.2,.7,.2,1) both;}" +
|
|
512
|
-
".sm-veil .word b{font-weight:300;}" +
|
|
513
|
-
".sm-veil .word .top{animation:sm-drift-a 18s ease-in-out infinite;}" +
|
|
514
|
-
".sm-veil .word .bot{margin-left:.3em;animation:sm-drift-b 18s ease-in-out infinite;}" +
|
|
515
|
-
".sm-veil .status{display:flex;align-items:center;gap:10px;font-size:18px;font-weight:600;animation:sm-rise .6s .12s cubic-bezier(.2,.7,.2,1) both;}" +
|
|
516
|
-
".sm-veil .status svg{width:24px;height:24px;color:var(--sm-success);}" +
|
|
517
|
-
".sm-veil .sub{color:var(--sm-dim);font-size:13.5px;animation:sm-rise .6s .18s cubic-bezier(.2,.7,.2,1) both;}" +
|
|
518
|
-
".sm-veil .sub.sm-pulse{animation:sm-rise .6s .18s cubic-bezier(.2,.7,.2,1) both,sm-pulse 2s .9s ease-in-out infinite;}";
|
|
449
|
+
/* off: still there, so it is clear Approve comes back once the comments are sent */
|
|
450
|
+
".sm-send-pill.is-off,.sm-approve.is-off{opacity:.35;cursor:not-allowed;transform:none;box-shadow:none;}";
|
|
519
451
|
|
|
520
452
|
// ---------- helpers ----------
|
|
521
|
-
function el(tag, cls, html) {
|
|
522
|
-
var e = document.createElement(tag);
|
|
523
|
-
if (cls) e.className = cls;
|
|
524
|
-
if (html != null) e.innerHTML = html;
|
|
525
|
-
return e;
|
|
526
|
-
}
|
|
527
|
-
function wordmarkHtml() {
|
|
528
|
-
return '<b class="top">slide</b><b class="bot">mind</b>';
|
|
529
|
-
}
|
|
530
|
-
// Genuine path: the add-in's per-brand orb PNG (CFG.logoMark). Fallback: the
|
|
531
|
-
// inline embossed slide glyph — no CLI-side orb re-creation.
|
|
532
|
-
function logoNode(cls, idSuffix) {
|
|
533
|
-
var box = el("div", cls);
|
|
534
|
-
if (CFG.logoMark) {
|
|
535
|
-
var img = el("img", "sm-logo-img");
|
|
536
|
-
img.src = CFG.logoMark;
|
|
537
|
-
img.alt = "slidemind";
|
|
538
|
-
box.appendChild(img);
|
|
539
|
-
return box;
|
|
540
|
-
}
|
|
541
|
-
box.appendChild(el("div", "sm-logo-svg", idSuffix ? logoWithIds(idSuffix) : LOGO_SVG));
|
|
542
|
-
return box;
|
|
543
|
-
}
|
|
544
453
|
function clamp(v, min, max) {
|
|
545
454
|
return Math.min(Math.max(v, min), max);
|
|
546
455
|
}
|
|
@@ -564,16 +473,48 @@
|
|
|
564
473
|
/* headless / detached — ignore */
|
|
565
474
|
}
|
|
566
475
|
}
|
|
476
|
+
function pctOf(v) {
|
|
477
|
+
return (v * 100).toFixed(2) + "%";
|
|
478
|
+
}
|
|
479
|
+
function copyOf(o) {
|
|
480
|
+
var d = {};
|
|
481
|
+
for (var k in o) d[k] = o[k];
|
|
482
|
+
return d;
|
|
483
|
+
}
|
|
484
|
+
function layoutOptions() {
|
|
485
|
+
var d = shownDesign();
|
|
486
|
+
return d && !d.current ? d.layouts || [] : boundLayouts;
|
|
487
|
+
}
|
|
488
|
+
function designOptions() {
|
|
489
|
+
return Array.isArray(CFG.designs) ? CFG.designs : [];
|
|
490
|
+
}
|
|
491
|
+
// The design the page shows: the picked one, else the deck's own (null when
|
|
492
|
+
// the deck is bound to none this machine has cached).
|
|
493
|
+
function shownDesign() {
|
|
494
|
+
var all = designOptions();
|
|
495
|
+
for (var i = 0; i < all.length; i++) {
|
|
496
|
+
if (S.design ? all[i].presFP === S.design : all[i].current) return all[i];
|
|
497
|
+
}
|
|
498
|
+
return null;
|
|
499
|
+
}
|
|
500
|
+
function findLayout(componentName) {
|
|
501
|
+
return layoutOptions().find(function (o) {
|
|
502
|
+
return o.componentName === componentName;
|
|
503
|
+
}) || null;
|
|
504
|
+
}
|
|
567
505
|
|
|
568
506
|
// ---------- session (shared with the preview gate via gate-session.js) ------
|
|
569
507
|
function saveLedger() {
|
|
570
508
|
SES.saveLedger(ledger);
|
|
571
509
|
}
|
|
572
|
-
// The small round context the NEXT round restores from: the selected
|
|
573
|
-
// the unsent draft (the server nonce is added
|
|
574
|
-
// plan body is NOT in here — see BODY_KEY.
|
|
510
|
+
// The small round context the NEXT round restores from: the selected slide,
|
|
511
|
+
// the unsent draft and the shell's hand-off state (the server nonce is added
|
|
512
|
+
// by the shared session). The plan body is NOT in here — see BODY_KEY.
|
|
575
513
|
function ctxFields() {
|
|
576
|
-
|
|
514
|
+
var f = { round: roundNo, selectedIndex: S.selectedIndex, draft: feedbackText, working: working };
|
|
515
|
+
var shell = SHELL.ctxFields();
|
|
516
|
+
for (var k in shell) f[k] = shell[k];
|
|
517
|
+
return f;
|
|
577
518
|
}
|
|
578
519
|
function saveCtx() {
|
|
579
520
|
SES.saveCtx(ctxFields());
|
|
@@ -583,12 +524,53 @@
|
|
|
583
524
|
SES.saveCtxSoon(ctxFields());
|
|
584
525
|
}
|
|
585
526
|
|
|
527
|
+
// Card edits that are not sent yet, keyed by the plan version they were made
|
|
528
|
+
// on (CFG.subjectHash, else a hash of the served body): a reload of the same
|
|
529
|
+
// version brings them back; edits on an older version are offered for copying
|
|
530
|
+
// instead of being applied to text they were not written against. Written on
|
|
531
|
+
// each committed edit (blur / drop), never per keystroke.
|
|
532
|
+
var EDITS_KEY = "slidemind-plan-edits:" + DECK_KEY;
|
|
533
|
+
var editsRestored = false; // this load brought unsent edits back
|
|
534
|
+
var staleEdits = null; // edits made on an older version: { value }
|
|
535
|
+
function textHash(t) {
|
|
536
|
+
var h = 0x811c9dc5;
|
|
537
|
+
for (var i = 0; i < t.length; i++) {
|
|
538
|
+
h ^= t.charCodeAt(i);
|
|
539
|
+
h = Math.imul(h, 0x01000193) >>> 0;
|
|
540
|
+
}
|
|
541
|
+
return ("0000000" + h.toString(16)).slice(-8) + ":" + t.length;
|
|
542
|
+
}
|
|
543
|
+
function editsBase() {
|
|
544
|
+
return CFG.subjectHash || "body:" + textHash(baseline);
|
|
545
|
+
}
|
|
546
|
+
function saveEdits() {
|
|
547
|
+
if (S.value !== baseline || S.storyDraft || S.design) SES.write(EDITS_KEY, { base: editsBase(), value: S.value, storyDraft: S.storyDraft || null, design: S.design || null });
|
|
548
|
+
else SES.write(EDITS_KEY, null);
|
|
549
|
+
}
|
|
550
|
+
function restoreEdits() {
|
|
551
|
+
var saved = SES.read(EDITS_KEY, null);
|
|
552
|
+
if (!saved || typeof saved !== "object" || typeof saved.value !== "string") return;
|
|
553
|
+
if (saved.base === editsBase()) {
|
|
554
|
+
S.value = saved.value;
|
|
555
|
+
S.storyDraft = saved.storyDraft || null;
|
|
556
|
+
// a picked design survives a reload only while it is still on offer
|
|
557
|
+
S.design = designOptions().some(function (d) {
|
|
558
|
+
return d.presFP === saved.design && !d.current;
|
|
559
|
+
})
|
|
560
|
+
? saved.design
|
|
561
|
+
: null;
|
|
562
|
+
editsRestored = S.value !== baseline || !!S.storyDraft || !!S.design;
|
|
563
|
+
} else {
|
|
564
|
+
staleEdits = { value: saved.value };
|
|
565
|
+
SES.write(EDITS_KEY, null);
|
|
566
|
+
}
|
|
567
|
+
}
|
|
568
|
+
|
|
586
569
|
// Per-slide segment diff between the previous round's plan body and this one,
|
|
587
570
|
// keyed by the slide NUMBER the `### Slide N` heading declares — never by the
|
|
588
571
|
// heading's position. Real decks number non-contiguously (2,3,4,7,8,9), so a
|
|
589
|
-
// positional key badged the wrong
|
|
590
|
-
// section, so trailing prose
|
|
591
|
-
// swept into the last slide and badge it for an unrelated edit.
|
|
572
|
+
// positional key badged the wrong card. Each chunk also STOPS at the next
|
|
573
|
+
// `## ` section, so trailing prose can't be swept into the last slide.
|
|
592
574
|
function slideChunks(md) {
|
|
593
575
|
var out = {};
|
|
594
576
|
String(md || "")
|
|
@@ -596,8 +578,6 @@
|
|
|
596
578
|
.forEach(function (part) {
|
|
597
579
|
var m = /^###\s+Slide\s+(\d+)/.exec(part);
|
|
598
580
|
if (!m) return;
|
|
599
|
-
// Cut at the next level-2 heading (the chunk's own `###` line is at
|
|
600
|
-
// index 0, so only a later `## ` can match).
|
|
601
581
|
var stop = part.search(/\n##\s+/);
|
|
602
582
|
var body = stop > 0 ? part.slice(0, stop) : part;
|
|
603
583
|
out[Number(m[1])] = body.trim();
|
|
@@ -618,117 +598,112 @@
|
|
|
618
598
|
// Runs before the first render so the surface is built in the right mode.
|
|
619
599
|
function restoreSession() {
|
|
620
600
|
var prevBody = SES.read(BODY_KEY, null);
|
|
621
|
-
|
|
601
|
+
// A changed nonce while the agent still revises this page's comments is not
|
|
602
|
+
// the next round (SHELL.holdsRound) — nothing it was handed is answered yet.
|
|
603
|
+
var st = SES.restore(CFG.ledgerSeed, { hold: SHELL.holdsRound });
|
|
622
604
|
ledger = st.ledger;
|
|
623
605
|
roundNo = st.roundNo;
|
|
624
606
|
working = st.working;
|
|
625
|
-
if (st.newRound) {
|
|
607
|
+
if (st.newRound && !st.unchanged) {
|
|
626
608
|
// Cosmetics only: WHICH slides moved is a content question, so it stays a
|
|
627
609
|
// body diff — the round transition itself came from the server nonce.
|
|
628
610
|
changedSlides = diffSlides(prevBody, baseline);
|
|
629
|
-
bannerText = "Updated
|
|
611
|
+
bannerText = "Updated by your agent";
|
|
630
612
|
}
|
|
631
|
-
//
|
|
613
|
+
// Same bytes, asked again: never "updated", never "worked in".
|
|
614
|
+
askedAgainUnchanged = !!(st.unchanged && st.hadSent);
|
|
615
|
+
// Reviewer context survives the round: selection + draft + hand-off state.
|
|
632
616
|
if (st.ctx) {
|
|
633
617
|
if (typeof st.ctx.selectedIndex === "number") S.selectedIndex = st.ctx.selectedIndex;
|
|
634
618
|
if (typeof st.ctx.draft === "string") feedbackText = st.ctx.draft;
|
|
635
619
|
}
|
|
620
|
+
arrivedAfterRevising = SHELL.restoreCtx(st.ctx, working, st.newRound);
|
|
621
|
+
if (!working) restoreEdits();
|
|
636
622
|
saveLedger();
|
|
637
623
|
saveCtx();
|
|
638
624
|
// Once per page load — the served body never changes while this page lives.
|
|
639
|
-
|
|
625
|
+
// A held load keeps the body the reviewer commented on: the "updated"
|
|
626
|
+
// badges of the real next round compare against THAT.
|
|
627
|
+
if (!st.held) SES.write(BODY_KEY, baseline);
|
|
640
628
|
}
|
|
641
629
|
|
|
642
630
|
// ---------- chrome (top bar) ----------
|
|
643
631
|
function buildChrome() {
|
|
644
|
-
bar =
|
|
645
|
-
bar.setAttribute("data-sm-overlay", "1");
|
|
646
|
-
|
|
647
|
-
var brand = el("div", "sm-brand");
|
|
648
|
-
brand.appendChild(logoNode("sm-logo"));
|
|
649
|
-
brand.appendChild(el("div", "sm-word", wordmarkHtml()));
|
|
650
|
-
bar.appendChild(brand);
|
|
651
|
-
bar.appendChild(el("div", "sm-sep"));
|
|
652
|
-
|
|
653
|
-
var meta = el("div", "sm-meta");
|
|
654
|
-
var title = el("div", "t");
|
|
655
|
-
title.appendChild(document.createTextNode("Plan review"));
|
|
632
|
+
var bar = SHELL.buildBar("Plan review", "");
|
|
656
633
|
editedBadge = el("span", "sm-edited", "edited");
|
|
657
634
|
editedBadge.style.display = "none";
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
bar.
|
|
663
|
-
bar.appendChild(el("div", "sm-grow"));
|
|
664
|
-
|
|
665
|
-
document.body.appendChild(bar);
|
|
635
|
+
bar.titleEl.appendChild(editedBadge);
|
|
636
|
+
hintEl = bar.hintEl;
|
|
637
|
+
// the deck-wide choices sit next to the title, each named: the design and the body slides' layout
|
|
638
|
+
barTools = el("div", "sm-bar-tools");
|
|
639
|
+
bar.bar.insertBefore(barTools, bar.grow);
|
|
666
640
|
}
|
|
667
641
|
function refreshChrome() {
|
|
668
|
-
if (editedBadge) editedBadge.style.display = planWasEdited() ? "" : "none";
|
|
642
|
+
if (editedBadge) editedBadge.style.display = planWasEdited() || S.design ? "" : "none";
|
|
669
643
|
if (hintEl) {
|
|
670
644
|
hintEl.textContent = working
|
|
671
|
-
?
|
|
672
|
-
|
|
645
|
+
? CFG.embedded
|
|
646
|
+
? "Read on and browse the slides · you can comment again when your agent asks"
|
|
647
|
+
: "The agent is revising · read on and browse the slides — commenting and editing resume with the next round"
|
|
648
|
+
: SHELL.notAskedYet()
|
|
649
|
+
? "Your agent is still working on this plan · it asks for your review when it is ready"
|
|
650
|
+
: SHELL.alreadyApproved()
|
|
651
|
+
? "Approved · comment only if something must still change"
|
|
652
|
+
: "Pick a slide on the left · drag to reorder, click its title to rename · edit its story, points and layout on the card";
|
|
673
653
|
}
|
|
674
654
|
|
|
675
|
-
|
|
676
|
-
var
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
var
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
}
|
|
693
|
-
} else if (S.storyDraft) {
|
|
694
|
-
// Approve is blocked until the staged storyline is sent or discarded.
|
|
695
|
-
approveArmed = false;
|
|
696
|
-
approveBtn.classList.remove("is-armed");
|
|
697
|
-
approveBtn.classList.add("is-off");
|
|
698
|
-
if (main) main.textContent = "Approve plan";
|
|
699
|
-
if (sub) {
|
|
700
|
-
sub.style.display = "";
|
|
701
|
-
sub.textContent = "Send or discard your storyline changes first";
|
|
702
|
-
}
|
|
703
|
-
} else {
|
|
704
|
-
approveBtn.classList.remove("is-off");
|
|
705
|
-
if (approveArmed && hasFeedback) {
|
|
706
|
-
approveBtn.classList.add("is-armed");
|
|
707
|
-
if (main) main.textContent = "Feedback not sent — approve anyway?";
|
|
708
|
-
if (sub) sub.style.display = "none";
|
|
709
|
-
} else {
|
|
710
|
-
approveArmed = false;
|
|
711
|
-
approveBtn.classList.remove("is-armed");
|
|
712
|
-
if (main) main.textContent = "Approve plan";
|
|
713
|
-
if (sub) {
|
|
714
|
-
sub.style.display = "";
|
|
715
|
-
sub.textContent = "the agent builds the slides next";
|
|
716
|
-
}
|
|
717
|
-
}
|
|
655
|
+
// the in-flight latch: one answer at a time, a double click cannot send twice
|
|
656
|
+
var sending = SHELL.decisionPending() ? SHELL.state.pending : null;
|
|
657
|
+
var total = draftComments().length + (commentText() ? 1 : 0);
|
|
658
|
+
// Send appears once there is something to send — a comment typed or
|
|
659
|
+
// listed, or a storyline change — and stays while that answer is on its way.
|
|
660
|
+
if (sendBtn) {
|
|
661
|
+
var canSend = !working && (!!S.storyDraft || total > 0);
|
|
662
|
+
sendBtn.style.display = canSend || (sending && sending.decision === "comments") ? "" : "none";
|
|
663
|
+
sendBtn.classList.toggle("is-off", !!sending);
|
|
664
|
+
var n = sendBtn.querySelector(".n");
|
|
665
|
+
if (n) {
|
|
666
|
+
n.textContent =
|
|
667
|
+
sending && sending.decision === "comments"
|
|
668
|
+
? "Sending…"
|
|
669
|
+
: S.storyDraft
|
|
670
|
+
? "Send changes"
|
|
671
|
+
: "Send " + total + (total === 1 ? " comment" : " comments");
|
|
718
672
|
}
|
|
719
673
|
}
|
|
720
|
-
}
|
|
721
674
|
|
|
722
|
-
|
|
675
|
+
if (!approveBtn) return;
|
|
676
|
+
// Why Approve is off — said on hover, never as one more line on the page.
|
|
677
|
+
// A comment and an approval never go out together: anything typed or
|
|
678
|
+
// listed turns Approve off until it is sent or removed.
|
|
679
|
+
var why = null;
|
|
680
|
+
if (working) why = CFG.embedded ? "Your answer is saved. You can approve the next version when your agent asks." : "The agent is revising — approve the next round";
|
|
681
|
+
else if (sending) why = "Sending your answer…";
|
|
682
|
+
else if (S.storyDraft) why = "Send or discard your storyline changes first";
|
|
683
|
+
else if (SHELL.notAskedYet()) why = "Your agent asks for your approval when the plan is ready";
|
|
684
|
+
else if (SHELL.alreadyApproved()) why = "This plan is approved";
|
|
685
|
+
else if (total > 0) why = "Send or remove your " + (total === 1 ? "comment" : "comments") + " first";
|
|
686
|
+
approveBtn.classList.toggle("is-off", !!why);
|
|
687
|
+
approveBtn.setAttribute("aria-disabled", why ? "true" : "false");
|
|
688
|
+
approveBtn.title = why || approveTip();
|
|
689
|
+
var label = approveBtn.querySelector(".n");
|
|
690
|
+
if (label) label.textContent = sending && sending.decision === "approve" ? "Approving…" : SHELL.alreadyApproved() ? "Plan approved" : "Approve plan";
|
|
691
|
+
}
|
|
692
|
+
|
|
693
|
+
// What Approve does, on hover. In the workspace the click also copies the
|
|
694
|
+
// message the reviewer pastes to their agent — the dialog after the click is
|
|
695
|
+
// where that is said; the button itself stays two words (doc 21).
|
|
696
|
+
function approveTip() {
|
|
697
|
+
if (!CFG.embedded) return "Approve the plan — the agent builds the slides next";
|
|
698
|
+
return "Approve the plan. You then paste a message into your agent chat.";
|
|
699
|
+
}
|
|
700
|
+
|
|
701
|
+
// ---------- workspace: surface + composer ----------
|
|
723
702
|
function buildWorkspace() {
|
|
724
703
|
wsEl = el("div", "sm-o sm-ws");
|
|
725
704
|
wsEl.setAttribute("data-sm-overlay", "1");
|
|
726
|
-
|
|
727
705
|
surfaceEl = el("div", "sm-ws-surface");
|
|
728
|
-
// Delegated inline-edit commit for Mode A rendered markdown blocks.
|
|
729
|
-
// to the persistent surface so a Mode-B re-render (which replaces the
|
|
730
|
-
// surface's children) keeps these live. commitRenderedEdit is a no-op for
|
|
731
|
-
// anything that isn't a stamped, editable, non-storyboard block.
|
|
706
|
+
// Delegated inline-edit commit for Mode A rendered markdown blocks.
|
|
732
707
|
surfaceEl.addEventListener(
|
|
733
708
|
"blur",
|
|
734
709
|
function (e) {
|
|
@@ -742,7 +717,7 @@
|
|
|
742
717
|
if (e.key !== "Enter" || e.shiftKey) return;
|
|
743
718
|
var t = e.target;
|
|
744
719
|
if (!(t instanceof HTMLElement)) return;
|
|
745
|
-
if (t.closest(".plan
|
|
720
|
+
if (t.closest(".sm-plan")) return;
|
|
746
721
|
if (!t.closest("[data-md-line][data-md-kind]")) return;
|
|
747
722
|
e.preventDefault();
|
|
748
723
|
commitRenderedEdit(t);
|
|
@@ -751,12 +726,10 @@
|
|
|
751
726
|
true,
|
|
752
727
|
);
|
|
753
728
|
wsEl.appendChild(surfaceEl);
|
|
754
|
-
document.body.appendChild(wsEl);
|
|
755
|
-
|
|
756
729
|
composerEl = el("div", "sm-composer");
|
|
757
730
|
composerEl.setAttribute("data-sm-overlay", "1");
|
|
758
731
|
wsEl.appendChild(composerEl);
|
|
759
|
-
|
|
732
|
+
document.body.appendChild(wsEl);
|
|
760
733
|
renderSurface();
|
|
761
734
|
renderComposer();
|
|
762
735
|
}
|
|
@@ -767,7 +740,7 @@
|
|
|
767
740
|
if (locked()) return;
|
|
768
741
|
if (!(target instanceof HTMLElement)) return;
|
|
769
742
|
var editable = target.closest("[data-md-line][data-md-kind]");
|
|
770
|
-
if (!editable || editable.closest(".plan
|
|
743
|
+
if (!editable || editable.closest(".sm-plan")) return;
|
|
771
744
|
var next = PLAN.replaceRenderedMarkdownBlock({
|
|
772
745
|
markdown: S.value,
|
|
773
746
|
line: Number(editable.dataset.mdLine),
|
|
@@ -776,21 +749,21 @@
|
|
|
776
749
|
});
|
|
777
750
|
if (next !== S.value) {
|
|
778
751
|
S.value = next;
|
|
779
|
-
|
|
752
|
+
saveEdits(); // an unsent edit survives a reload of this version
|
|
780
753
|
refreshChrome();
|
|
781
754
|
}
|
|
782
755
|
}
|
|
783
756
|
|
|
784
757
|
// ---------- surface render ----------
|
|
785
|
-
// A plan with storyboard fences renders as
|
|
786
|
-
// markdown renders inline-editable.
|
|
787
|
-
// committed model change (
|
|
788
|
-
//
|
|
758
|
+
// A plan with storyboard fences renders as rail + stage + aside; otherwise the
|
|
759
|
+
// plain markdown renders inline-editable. The dossier re-renders in full on
|
|
760
|
+
// every committed model change (edits commit on blur / pointerup, so nothing
|
|
761
|
+
// is focused at commit time).
|
|
789
762
|
function renderSurface() {
|
|
790
763
|
if (!surfaceEl) return;
|
|
764
|
+
surfaceLocked = locked();
|
|
791
765
|
surfaceEl.innerHTML = "";
|
|
792
766
|
focusAfterRender = null;
|
|
793
|
-
if (bannerText) surfaceEl.appendChild(roundBanner());
|
|
794
767
|
var segments = PLAN.splitPlanMarkdownIntoSegments(S.value);
|
|
795
768
|
var hasStoryboards = segments.some(function (s) {
|
|
796
769
|
return s.type === "storyboard";
|
|
@@ -803,20 +776,15 @@
|
|
|
803
776
|
}
|
|
804
777
|
}
|
|
805
778
|
|
|
806
|
-
// Transient "Updated · Round N" banner.
|
|
807
|
-
//
|
|
808
|
-
// can't be mistaken for a permanent state.
|
|
779
|
+
// Transient "Updated · Round N" banner. Self-dismisses so it can't be
|
|
780
|
+
// mistaken for a permanent state.
|
|
809
781
|
function roundBanner() {
|
|
810
782
|
var count = Object.keys(changedSlides).length;
|
|
811
783
|
var node = el("div", "sm-round-banner");
|
|
812
784
|
node.innerHTML =
|
|
813
785
|
ICONS.sparkles +
|
|
814
|
-
"<span><b>" +
|
|
815
|
-
|
|
816
|
-
"</b> " +
|
|
817
|
-
(count
|
|
818
|
-
? count + (count === 1 ? " slide changed" : " slides changed") + " — marked “updated” on the strip."
|
|
819
|
-
: "The plan was revised.") +
|
|
786
|
+
"<span><b>" + bannerText + "</b> " +
|
|
787
|
+
(count ? count + (count === 1 ? " slide changed" : " slides changed") + " — marked “updated” on the left." : "The plan was revised.") +
|
|
820
788
|
"</span>";
|
|
821
789
|
setTimeout(function () {
|
|
822
790
|
bannerText = null;
|
|
@@ -826,7 +794,9 @@
|
|
|
826
794
|
}
|
|
827
795
|
|
|
828
796
|
function renderModeA(segments) {
|
|
797
|
+
var doc = el("div", "sm-doc");
|
|
829
798
|
var root = el("div", "plan-md-root");
|
|
799
|
+
if (bannerText) root.appendChild(roundBanner());
|
|
830
800
|
segments.forEach(function (seg) {
|
|
831
801
|
if (seg.type !== "markdown") return;
|
|
832
802
|
var html = PLAN.renderPlanMarkdown(seg.text, { lineOffset: seg.lineOffset });
|
|
@@ -835,20 +805,21 @@
|
|
|
835
805
|
holder.innerHTML = html;
|
|
836
806
|
while (holder.firstChild) root.appendChild(holder.firstChild);
|
|
837
807
|
});
|
|
838
|
-
|
|
808
|
+
doc.appendChild(root);
|
|
809
|
+
surfaceEl.appendChild(doc);
|
|
839
810
|
}
|
|
840
811
|
|
|
841
|
-
// The story beats
|
|
842
|
-
//
|
|
843
|
-
//
|
|
844
|
-
//
|
|
845
|
-
// declares, which is what a "This slide" comment is anchored to
|
|
846
|
-
// reads the same number the reviewer saw).
|
|
812
|
+
// The story beats: the backbone (a body slide's `## Red thread` move, a frame
|
|
813
|
+
// slide's key message), else the moves — but only when they match the boards
|
|
814
|
+
// one-to-one, so a drifted red thread never renders phantom slides —
|
|
815
|
+
// otherwise each board's key message. Each beat carries the slide number its
|
|
816
|
+
// board declares, which is what a "This slide" comment is anchored to.
|
|
847
817
|
function planBeats() {
|
|
848
818
|
var boards = PLAN.splitPlanMarkdownIntoSegments(S.value).filter(function (s) {
|
|
849
819
|
return s.type === "storyboard";
|
|
850
820
|
});
|
|
851
|
-
var
|
|
821
|
+
var backbone = PLAN.parsePlanBackbone(S.value);
|
|
822
|
+
var messages = backbone ? backbone.messages : PLAN.extractRedThreadMessages(S.value);
|
|
852
823
|
if (messages.length !== boards.length) {
|
|
853
824
|
messages = boards.map(function (seg, i) {
|
|
854
825
|
return (seg.data && seg.data.keyMessage) || "Slide " + ((seg.data && seg.data.slide) || i + 1);
|
|
@@ -860,7 +831,7 @@
|
|
|
860
831
|
});
|
|
861
832
|
}
|
|
862
833
|
|
|
863
|
-
// ---------- dossier
|
|
834
|
+
// ---------- dossier: rail | stage | aside ----------
|
|
864
835
|
function renderDossier(segments) {
|
|
865
836
|
var storyboardSegments = segments.filter(function (s) {
|
|
866
837
|
return s.type === "storyboard";
|
|
@@ -875,77 +846,37 @@
|
|
|
875
846
|
});
|
|
876
847
|
var committed = PLAN.backboneFromMessages(beatMessages);
|
|
877
848
|
var slides = S.storyDraft || committed;
|
|
878
|
-
var
|
|
879
|
-
var canEdit = !locked() && backboneEditable;
|
|
849
|
+
var canEdit = !locked() && PLAN.parsePlanBackbone(S.value) !== null;
|
|
880
850
|
|
|
881
851
|
if (S.selectedIndex >= slides.length) S.selectedIndex = slides.length - 1;
|
|
882
852
|
if (S.selectedIndex < 0) S.selectedIndex = 0;
|
|
883
853
|
var activeIndex = S.selectedIndex;
|
|
884
854
|
|
|
885
|
-
// A staged draft resolves a
|
|
886
|
-
// new slides (originalIndex null) have no board yet.
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
var oi = slides[
|
|
890
|
-
|
|
891
|
-
} else {
|
|
892
|
-
activeSegment = storyboardSegments[activeIndex];
|
|
893
|
-
}
|
|
894
|
-
|
|
895
|
-
var dossier = el("div", "plan-dossier");
|
|
896
|
-
var root = el("div", "plan-md-root plan-md-root--storyboards");
|
|
897
|
-
dossier.appendChild(root);
|
|
898
|
-
|
|
899
|
-
root.appendChild(
|
|
900
|
-
renderStrip(slides, committed, beatMessages, storyboardData, storyboardSegments, canEdit, activeIndex),
|
|
901
|
-
);
|
|
902
|
-
|
|
903
|
-
var body = el("div", "plan-dossier-body");
|
|
904
|
-
if (activeSegment) {
|
|
905
|
-
if (activeSegment.data) {
|
|
906
|
-
body.appendChild(renderCanvas(activeSegment));
|
|
907
|
-
} else {
|
|
908
|
-
var err = el("div", "plan-storyboard-error");
|
|
909
|
-
err.appendChild(el("p", null, "Storyboard block could not be rendered."));
|
|
910
|
-
var pre = el("pre");
|
|
911
|
-
var codeNode = document.createElement("code");
|
|
912
|
-
codeNode.textContent = activeSegment.raw || "";
|
|
913
|
-
pre.appendChild(codeNode);
|
|
914
|
-
err.appendChild(pre);
|
|
915
|
-
body.appendChild(err);
|
|
916
|
-
}
|
|
917
|
-
} else {
|
|
918
|
-
// A staged new slide (or a beat with no board yet): the agent builds its
|
|
919
|
-
// storyboard once the story round is sent.
|
|
920
|
-
var placeholder = el("div", "plan-dossier-new-slide");
|
|
921
|
-
placeholder.innerHTML =
|
|
922
|
-
ICONS.ruler +
|
|
923
|
-
"<p><strong>Storyline edited.</strong> Sending asks the agent to build storyboards and content for your new and renamed slides — add comments below if you like.</p>";
|
|
924
|
-
body.appendChild(placeholder);
|
|
855
|
+
// A staged draft resolves a card to the board of the slide it originally
|
|
856
|
+
// was; new slides (originalIndex null) have no board yet.
|
|
857
|
+
function segmentFor(index) {
|
|
858
|
+
if (!S.storyDraft) return storyboardSegments[index];
|
|
859
|
+
var oi = slides[index] ? slides[index].originalIndex : undefined;
|
|
860
|
+
return oi === null || oi === undefined ? undefined : storyboardSegments[oi];
|
|
925
861
|
}
|
|
862
|
+
var activeSegment = segmentFor(activeIndex);
|
|
926
863
|
|
|
927
|
-
var
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
body.appendChild(note);
|
|
932
|
-
|
|
933
|
-
root.appendChild(body);
|
|
934
|
-
surfaceEl.appendChild(dossier);
|
|
864
|
+
var plan = el("div", "sm-plan");
|
|
865
|
+
plan.appendChild(renderRail(slides, beatMessages, storyboardData, segmentFor, canEdit, activeIndex));
|
|
866
|
+
plan.appendChild(renderStage(activeSegment, activeIndex, slides));
|
|
867
|
+
surfaceEl.appendChild(plan);
|
|
935
868
|
}
|
|
936
869
|
|
|
937
|
-
// ----------
|
|
938
|
-
function
|
|
939
|
-
var
|
|
940
|
-
var head = el("div", "
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
head.appendChild(renderDeckLayoutKnob(storyboardData));
|
|
945
|
-
section.appendChild(head);
|
|
870
|
+
// ---------- the rail: one card per slide, in story order ----------
|
|
871
|
+
function renderRail(slides, committedMessages, storyboardData, segmentFor, canEdit, activeIndex) {
|
|
872
|
+
var rail = el("aside", "sm-rail");
|
|
873
|
+
var head = el("div", "sm-rail-head", "<span>Storyline</span>");
|
|
874
|
+
head.appendChild(el("span", "n", "")).textContent = slides.length + (slides.length === 1 ? " slide" : " slides");
|
|
875
|
+
rail.appendChild(head);
|
|
876
|
+
renderBarTools(storyboardData);
|
|
946
877
|
|
|
947
|
-
var
|
|
948
|
-
|
|
878
|
+
var list = el("ol", "sm-rail-list");
|
|
879
|
+
rail.appendChild(list);
|
|
949
880
|
|
|
950
881
|
var cardEls = [];
|
|
951
882
|
var gapEls = [];
|
|
@@ -958,20 +889,31 @@
|
|
|
958
889
|
else gapEls[i].removeAttribute("data-active");
|
|
959
890
|
}
|
|
960
891
|
}
|
|
892
|
+
// Gap index nearest the pointer: each card contributes its leading edge
|
|
893
|
+
// (the gap before it) and trailing edge (the gap after it), on whichever
|
|
894
|
+
// axis the rail flows.
|
|
961
895
|
function nearestGap(cx, cy) {
|
|
962
896
|
var best = 0;
|
|
963
897
|
var bd = Infinity;
|
|
898
|
+
var horizontal = list.scrollWidth > list.clientWidth + 1 || getComputedStyle(list).display === "flex";
|
|
964
899
|
for (var i = 0; i < slides.length; i++) {
|
|
965
900
|
var card = cardEls[i];
|
|
966
901
|
if (!card) continue;
|
|
967
902
|
var r = card.getBoundingClientRect();
|
|
968
|
-
var
|
|
969
|
-
|
|
903
|
+
var before, after;
|
|
904
|
+
if (horizontal) {
|
|
905
|
+
var dy = cy - (r.top + r.height / 2);
|
|
906
|
+
before = (cx - r.left) * (cx - r.left) + dy * dy;
|
|
907
|
+
after = (cx - r.right) * (cx - r.right) + dy * dy;
|
|
908
|
+
} else {
|
|
909
|
+
var dx = cx - (r.left + r.width / 2);
|
|
910
|
+
before = (cy - r.top) * (cy - r.top) + dx * dx;
|
|
911
|
+
after = (cy - r.bottom) * (cy - r.bottom) + dx * dx;
|
|
912
|
+
}
|
|
970
913
|
if (before < bd) {
|
|
971
914
|
bd = before;
|
|
972
915
|
best = i;
|
|
973
916
|
}
|
|
974
|
-
var after = (cx - r.right) * (cx - r.right) + dy * dy;
|
|
975
917
|
if (after < bd) {
|
|
976
918
|
bd = after;
|
|
977
919
|
best = i + 1;
|
|
@@ -980,32 +922,34 @@
|
|
|
980
922
|
return best;
|
|
981
923
|
}
|
|
982
924
|
|
|
983
|
-
//
|
|
984
|
-
//
|
|
925
|
+
// Only a RENAME stages a draft: a renamed move says the slide's job in the
|
|
926
|
+
// story changed, which is the agent's to re-answer. Reorders, removals and
|
|
927
|
+
// INSERTS apply now. An insert used to stage too, which left the new slide
|
|
928
|
+
// with no record at all — the stage had nothing to render, so its beat,
|
|
929
|
+
// points and layout could not be touched until a whole agent round had run.
|
|
985
930
|
function beatCommit(next) {
|
|
986
|
-
var
|
|
987
|
-
return s.originalIndex !== null && committedMessages[s.originalIndex]
|
|
931
|
+
var renamed = next.some(function (s) {
|
|
932
|
+
return s.originalIndex !== null && committedMessages[s.originalIndex] !== s.message;
|
|
988
933
|
});
|
|
989
|
-
if (
|
|
934
|
+
if (renamed) {
|
|
935
|
+
S.storyDraft = next;
|
|
936
|
+
} else {
|
|
990
937
|
S.storyDraft = null;
|
|
991
938
|
var applied = PLAN.applyPlanBackbone(S.value, next);
|
|
992
939
|
if (applied) S.value = applied;
|
|
993
|
-
} else {
|
|
994
|
-
S.storyDraft = next;
|
|
995
940
|
}
|
|
996
|
-
|
|
941
|
+
saveEdits();
|
|
997
942
|
refreshChrome();
|
|
998
943
|
renderComposer();
|
|
999
944
|
renderSurface();
|
|
1000
945
|
}
|
|
1001
946
|
function renameSlide(index, message) {
|
|
1002
947
|
var cur = slides[index];
|
|
948
|
+
editingKey = null;
|
|
1003
949
|
if (!cur || cur.message === message) {
|
|
1004
|
-
editingKey = null;
|
|
1005
950
|
renderSurface();
|
|
1006
951
|
return;
|
|
1007
952
|
}
|
|
1008
|
-
editingKey = null;
|
|
1009
953
|
beatCommit(
|
|
1010
954
|
slides.map(function (s, i) {
|
|
1011
955
|
return i === index ? { key: s.key, originalIndex: s.originalIndex, message: message } : s;
|
|
@@ -1015,11 +959,7 @@
|
|
|
1015
959
|
function insertSlideAt(position) {
|
|
1016
960
|
newSlideCounter += 1;
|
|
1017
961
|
var next = slides.slice();
|
|
1018
|
-
next.splice(position, 0, {
|
|
1019
|
-
key: "new-" + newSlideCounter,
|
|
1020
|
-
originalIndex: null,
|
|
1021
|
-
message: "New slide",
|
|
1022
|
-
});
|
|
962
|
+
next.splice(position, 0, { key: "new-" + newSlideCounter, originalIndex: null, message: "New slide" });
|
|
1023
963
|
S.selectedIndex = position;
|
|
1024
964
|
beatCommit(next);
|
|
1025
965
|
}
|
|
@@ -1044,7 +984,7 @@
|
|
|
1044
984
|
}
|
|
1045
985
|
|
|
1046
986
|
function gapAt(position) {
|
|
1047
|
-
var li = el("li", "
|
|
987
|
+
var li = el("li", "sm-rail-gap");
|
|
1048
988
|
li.setAttribute("aria-hidden", canEdit ? "false" : "true");
|
|
1049
989
|
if (canEdit) {
|
|
1050
990
|
var btn = el("button", "plan-storyline-insert", ICONS.plus);
|
|
@@ -1061,35 +1001,34 @@
|
|
|
1061
1001
|
}
|
|
1062
1002
|
|
|
1063
1003
|
slides.forEach(function (item, index) {
|
|
1064
|
-
|
|
1065
|
-
|
|
1004
|
+
list.appendChild(gapAt(index));
|
|
1005
|
+
var segment = segmentFor(index);
|
|
1006
|
+
var data = segment && segment.data;
|
|
1066
1007
|
var li = el(
|
|
1067
1008
|
"li",
|
|
1068
|
-
"plan-
|
|
1069
|
-
(index === activeIndex ? " plan-storyline-beat--active" : "") +
|
|
1070
|
-
(canEdit ? " plan-storyline-beat--draggable" : "") +
|
|
1071
|
-
(editingKey === item.key ? " plan-storyline-beat--editing" : ""),
|
|
1009
|
+
"sm-thumb plan-beat" + (index === activeIndex ? " is-active" : "") + (editingKey === item.key ? " is-editing" : ""),
|
|
1072
1010
|
);
|
|
1073
1011
|
cardEls[index] = li;
|
|
1012
|
+
li.setAttribute("aria-current", index === activeIndex ? "true" : "false");
|
|
1074
1013
|
|
|
1075
1014
|
var numeral = S.storyDraft ? index + 1 : (storyboardData[index] && storyboardData[index].slide) || index + 1;
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
// "updated" badge: this tab's slide changed in the round that just landed.
|
|
1082
|
-
// Keyed by the SLIDE NUMBER shown on the tab (`numeral`, straight off the
|
|
1083
|
-
// board data) — the same source the diff keys by, so non-contiguous decks
|
|
1084
|
-
// (2,3,4,7,8,9) badge the tab that actually moved. A staged storyline
|
|
1085
|
-
// renumbers positionally, so badges are suppressed while one is open.
|
|
1015
|
+
// "updated" badge: this card's slide changed in the round that just
|
|
1016
|
+
// landed. Keyed by the SLIDE NUMBER (`numeral`, straight off the board
|
|
1017
|
+
// data) — the same source the diff keys by. A staged storyline renumbers
|
|
1018
|
+
// positionally, so badges are suppressed while one is open.
|
|
1086
1019
|
if (!S.storyDraft && changedSlides[numeral]) {
|
|
1087
1020
|
var upd = el("span", "plan-storyline-beat-updated", "updated");
|
|
1088
1021
|
upd.title = "This slide changed in the latest round";
|
|
1089
1022
|
li.appendChild(upd);
|
|
1090
1023
|
}
|
|
1091
1024
|
|
|
1092
|
-
|
|
1025
|
+
li.appendChild(renderMini(data));
|
|
1026
|
+
|
|
1027
|
+
var meta = el("div", "sm-thumb-meta");
|
|
1028
|
+
var num = el("span", "sm-thumb-number");
|
|
1029
|
+
num.textContent = String(numeral).padStart(2, "0");
|
|
1030
|
+
meta.appendChild(num);
|
|
1031
|
+
var textEl = el("div", "sm-thumb-name");
|
|
1093
1032
|
textEl.setAttribute("role", "textbox");
|
|
1094
1033
|
textEl.textContent = item.message;
|
|
1095
1034
|
var isEditing = canEdit && editingKey === item.key;
|
|
@@ -1121,28 +1060,15 @@
|
|
|
1121
1060
|
}
|
|
1122
1061
|
});
|
|
1123
1062
|
}
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
var
|
|
1128
|
-
var
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
onPick: function (componentName) {
|
|
1134
|
-
var d = {};
|
|
1135
|
-
for (var k in chipSegment.data) d[k] = chipSegment.data[k];
|
|
1136
|
-
d.layout = componentName;
|
|
1137
|
-
d.layoutSelection = "explicit";
|
|
1138
|
-
d.layoutKey = (CFG.layouts || []).find(function(o) { return o.componentName === componentName; })?.layoutKey;
|
|
1139
|
-
delete d.layoutReason;
|
|
1140
|
-
commitStoryboard(chipSegment, d);
|
|
1141
|
-
},
|
|
1142
|
-
});
|
|
1143
|
-
chip.setAttribute("data-no-drag", "");
|
|
1144
|
-
li.appendChild(chip);
|
|
1145
|
-
}
|
|
1063
|
+
meta.appendChild(textEl);
|
|
1064
|
+
li.appendChild(meta);
|
|
1065
|
+
|
|
1066
|
+
var sub = el("div", "sm-thumb-sub");
|
|
1067
|
+
var chosen = data && findLayout(data.layout);
|
|
1068
|
+
sub.textContent = data
|
|
1069
|
+
? (chosen && chosen.nativeLayoutName) || data.layout || "No layout chosen"
|
|
1070
|
+
: "Storyboard follows once sent";
|
|
1071
|
+
li.appendChild(sub);
|
|
1146
1072
|
|
|
1147
1073
|
if (canEdit) {
|
|
1148
1074
|
var rm = el("button", "plan-storyline-beat-remove", ICONS.x);
|
|
@@ -1161,19 +1087,12 @@
|
|
|
1161
1087
|
}
|
|
1162
1088
|
|
|
1163
1089
|
// Pointer: drag ≥ threshold reorders; a click selects; a click on the
|
|
1164
|
-
// active
|
|
1090
|
+
// active card's title (when editable) enters rename mode.
|
|
1165
1091
|
li.addEventListener("pointerdown", function (e) {
|
|
1166
1092
|
if (e.button !== 0) return;
|
|
1167
1093
|
if (editingKey === item.key) return;
|
|
1168
1094
|
if (e.target.closest && e.target.closest("[data-no-drag]")) return;
|
|
1169
|
-
stripDrag = {
|
|
1170
|
-
index: index,
|
|
1171
|
-
key: item.key,
|
|
1172
|
-
pointerId: e.pointerId,
|
|
1173
|
-
ox: e.clientX,
|
|
1174
|
-
oy: e.clientY,
|
|
1175
|
-
moved: false,
|
|
1176
|
-
};
|
|
1095
|
+
stripDrag = { index: index, key: item.key, pointerId: e.pointerId, ox: e.clientX, oy: e.clientY, moved: false, onTitle: e.target === textEl };
|
|
1177
1096
|
try {
|
|
1178
1097
|
li.setPointerCapture(e.pointerId);
|
|
1179
1098
|
} catch {
|
|
@@ -1187,7 +1106,7 @@
|
|
|
1187
1106
|
var dy = e.clientY - stripDrag.oy;
|
|
1188
1107
|
if (!stripDrag.moved && Math.hypot(dx, dy) < BEAT_DRAG_THRESHOLD) return;
|
|
1189
1108
|
stripDrag.moved = true;
|
|
1190
|
-
li.classList.add("
|
|
1109
|
+
li.classList.add("is-dragging");
|
|
1191
1110
|
li.style.transform = "translate(" + dx + "px," + dy + "px)";
|
|
1192
1111
|
dropGap = nearestGap(e.clientX, e.clientY);
|
|
1193
1112
|
paintGaps();
|
|
@@ -1203,10 +1122,11 @@
|
|
|
1203
1122
|
var gap = dropGap;
|
|
1204
1123
|
var idx = stripDrag.index;
|
|
1205
1124
|
var key = stripDrag.key;
|
|
1125
|
+
var onTitle = stripDrag.onTitle;
|
|
1206
1126
|
stripDrag = null;
|
|
1207
1127
|
dropGap = null;
|
|
1208
1128
|
li.style.transform = "";
|
|
1209
|
-
li.classList.remove("
|
|
1129
|
+
li.classList.remove("is-dragging");
|
|
1210
1130
|
paintGaps();
|
|
1211
1131
|
if (moved) {
|
|
1212
1132
|
if (gap !== null) reorderTo(idx, gap);
|
|
@@ -1217,7 +1137,7 @@
|
|
|
1217
1137
|
saveCtx();
|
|
1218
1138
|
renderSurface();
|
|
1219
1139
|
renderComposer(); // the scope control names the newly selected slide
|
|
1220
|
-
} else if (canEdit) {
|
|
1140
|
+
} else if (canEdit && onTitle) {
|
|
1221
1141
|
editingKey = key;
|
|
1222
1142
|
renderSurface();
|
|
1223
1143
|
}
|
|
@@ -1228,389 +1148,381 @@
|
|
|
1228
1148
|
stripDrag = null;
|
|
1229
1149
|
dropGap = null;
|
|
1230
1150
|
li.style.transform = "";
|
|
1231
|
-
li.classList.remove("
|
|
1151
|
+
li.classList.remove("is-dragging");
|
|
1232
1152
|
paintGaps();
|
|
1233
1153
|
});
|
|
1234
|
-
|
|
1235
|
-
track.appendChild(li);
|
|
1236
|
-
});
|
|
1237
|
-
track.appendChild(gapAt(slides.length));
|
|
1238
|
-
|
|
1239
|
-
return section;
|
|
1240
|
-
}
|
|
1241
|
-
|
|
1242
|
-
// ---------- storyboard canvas ----------
|
|
1243
|
-
function fittedFontSize(value, variant) {
|
|
1244
|
-
var len = value.trim().length;
|
|
1245
|
-
if (variant === "element") return Math.max(8.5, 12.5 - Math.max(0, len - 14) * 0.22);
|
|
1246
|
-
return Math.max(8, 13 - Math.max(0, len - 12) * 0.3);
|
|
1247
|
-
}
|
|
1248
|
-
function compactSingleLine(text, variant) {
|
|
1249
|
-
var lim = variant === "element" ? PLAN.MAX_DESIGN_ELEMENT_LENGTH : PLAN.MAX_CONTENT_LENGTH;
|
|
1250
|
-
return text
|
|
1251
|
-
.replace(/\u00a0/g, " ")
|
|
1252
|
-
.replace(/\s+/g, " ")
|
|
1253
|
-
.trim()
|
|
1254
|
-
.slice(0, lim);
|
|
1255
|
-
}
|
|
1256
|
-
function notesToText(notes) {
|
|
1257
|
-
return notes && notes.length ? notes.join("\n") : "";
|
|
1258
|
-
}
|
|
1259
|
-
function textToNotes(text) {
|
|
1260
|
-
var notes = text
|
|
1261
|
-
.replace(/\u00a0/g, " ")
|
|
1262
|
-
.split(/\r?\n/)
|
|
1263
|
-
.map(function (l) {
|
|
1264
|
-
return l.trim();
|
|
1265
|
-
})
|
|
1266
|
-
.filter(Boolean);
|
|
1267
|
-
return notes.length ? notes : undefined;
|
|
1268
|
-
}
|
|
1269
|
-
function updateBlock(data, blockId, patch) {
|
|
1270
|
-
var blocks = data.blocks.map(function (b) {
|
|
1271
|
-
if (b.id !== blockId) return b;
|
|
1272
|
-
var next = {};
|
|
1273
|
-
for (var k in b) next[k] = b[k];
|
|
1274
|
-
for (var p in patch) next[p] = patch[p];
|
|
1275
|
-
return next;
|
|
1154
|
+
list.appendChild(li);
|
|
1276
1155
|
});
|
|
1277
|
-
|
|
1278
|
-
|
|
1279
|
-
|
|
1280
|
-
|
|
1281
|
-
|
|
1282
|
-
|
|
1283
|
-
|
|
1156
|
+
list.appendChild(gapAt(slides.length));
|
|
1157
|
+
if (canEdit) {
|
|
1158
|
+
var add = el("li");
|
|
1159
|
+
var addBtn = el("button", "sm-rail-add", ICONS.plus + "<span>Add slide</span>");
|
|
1160
|
+
addBtn.type = "button";
|
|
1161
|
+
addBtn.onclick = function () {
|
|
1162
|
+
insertSlideAt(slides.length);
|
|
1163
|
+
};
|
|
1164
|
+
add.appendChild(addBtn);
|
|
1165
|
+
list.appendChild(add);
|
|
1166
|
+
}
|
|
1167
|
+
// Keep the selected card in view (the preview rail does the same).
|
|
1168
|
+
setTimeout(function () {
|
|
1169
|
+
var active = cardEls[activeIndex];
|
|
1170
|
+
if (active && active.scrollIntoView) active.scrollIntoView({ block: "nearest", inline: "nearest" });
|
|
1171
|
+
}, 0);
|
|
1172
|
+
return rail;
|
|
1284
1173
|
}
|
|
1285
1174
|
|
|
1286
|
-
//
|
|
1287
|
-
|
|
1288
|
-
|
|
1289
|
-
|
|
1290
|
-
if (
|
|
1291
|
-
|
|
1292
|
-
|
|
1293
|
-
|
|
1294
|
-
|
|
1175
|
+
// The rail card's picture: the render of the layout this slide sits on.
|
|
1176
|
+
function renderMini(data) {
|
|
1177
|
+
var box = el("div", "sm-thumb-preview");
|
|
1178
|
+
var mini = el("div", "sm-mini");
|
|
1179
|
+
if (!data) {
|
|
1180
|
+
mini.appendChild(el("div", "sm-mini-empty", "new"));
|
|
1181
|
+
box.appendChild(mini);
|
|
1182
|
+
return box;
|
|
1183
|
+
}
|
|
1184
|
+
var selected = selectedPlanLayout(data);
|
|
1185
|
+
if (selected && selected.previewUrl) {
|
|
1186
|
+
var img = el("img", "sm-mini-chrome");
|
|
1187
|
+
img.src = selected.previewUrl;
|
|
1188
|
+
img.alt = "";
|
|
1189
|
+
img.loading = "lazy";
|
|
1190
|
+
mini.appendChild(img);
|
|
1191
|
+
} else {
|
|
1192
|
+
mini.appendChild(el("div", "sm-mini-empty", ""));
|
|
1193
|
+
}
|
|
1194
|
+
// Same slot treatment as the stage: scanning the rail should say how each
|
|
1195
|
+
// slide is SHAPED, not just what it is called.
|
|
1196
|
+
mini.appendChild(renderSlotGeometry(selected, "sm-mini-slots"));
|
|
1197
|
+
box.appendChild(mini);
|
|
1198
|
+
return box;
|
|
1295
1199
|
}
|
|
1296
|
-
|
|
1297
|
-
|
|
1200
|
+
|
|
1201
|
+
// ---------- the stage: the slide, fitted ----------
|
|
1202
|
+
// The bound layout's own chrome render with its slot geometry on top, a light
|
|
1203
|
+
// veil pushing it back, and ONE panel floating over both: the story beat, the
|
|
1204
|
+
// 3-5 key points, the layout pick. That is the whole of what the gate asks a
|
|
1205
|
+
// human — the storyboard canvas it replaced was design input nobody reviewed.
|
|
1206
|
+
//
|
|
1207
|
+
// The slide is CONTEXT and the panel is the SUBJECT, so the panel is centred
|
|
1208
|
+
// and is the only thing on the stage with elevation. Pinning it to the
|
|
1209
|
+
// layout's content region made it read as part of the slide, which is the one
|
|
1210
|
+
// thing it must not be; the layout's shape is carried by its own geometry.
|
|
1211
|
+
var MAX_BULLETS = 5;
|
|
1212
|
+
|
|
1213
|
+
function renderStage(segment, activeIndex, slides) {
|
|
1214
|
+
var stage = el("section", "sm-stage");
|
|
1215
|
+
var fit = el("div", "sm-stage-fit");
|
|
1216
|
+
if (bannerText) fit.appendChild(roundBanner());
|
|
1217
|
+
if (segment) {
|
|
1218
|
+
if (segment.data) fit.appendChild(renderSlideCard(segment, segment.data));
|
|
1219
|
+
else {
|
|
1220
|
+
var err = el("div", "plan-storyboard-error");
|
|
1221
|
+
err.appendChild(el("p", null, "Slide plan could not be read."));
|
|
1222
|
+
var pre = el("pre");
|
|
1223
|
+
var codeNode = document.createElement("code");
|
|
1224
|
+
codeNode.textContent = segment.raw || "";
|
|
1225
|
+
pre.appendChild(codeNode);
|
|
1226
|
+
err.appendChild(pre);
|
|
1227
|
+
fit.appendChild(err);
|
|
1228
|
+
}
|
|
1229
|
+
} else {
|
|
1230
|
+
// A staged new slide (or a beat with no record yet): the agent fills it
|
|
1231
|
+
// once the story round is sent.
|
|
1232
|
+
var head = el("div", "sm-stage-head");
|
|
1233
|
+
head.appendChild(el("span", "t", "")).textContent = "Slide " + (activeIndex + 1) + " · " + ((slides[activeIndex] && slides[activeIndex].message) || "");
|
|
1234
|
+
fit.appendChild(head);
|
|
1235
|
+
var placeholder = el("div", "plan-dossier-new-slide");
|
|
1236
|
+
placeholder.innerHTML =
|
|
1237
|
+
ICONS.ruler +
|
|
1238
|
+
"<p><strong>Storyline edited.</strong> Sending asks the agent to draft the content for your new and renamed slides — add comments below if you like.</p>";
|
|
1239
|
+
fit.appendChild(placeholder);
|
|
1240
|
+
}
|
|
1241
|
+
fit.appendChild(draftNote());
|
|
1242
|
+
stage.appendChild(fit);
|
|
1243
|
+
return stage;
|
|
1298
1244
|
}
|
|
1299
1245
|
|
|
1300
|
-
function
|
|
1301
|
-
var
|
|
1302
|
-
|
|
1303
|
-
|
|
1304
|
-
|
|
1305
|
-
|
|
1306
|
-
|
|
1307
|
-
|
|
1308
|
-
|
|
1309
|
-
|
|
1310
|
-
|
|
1246
|
+
function renderSlideCard(segment, data) {
|
|
1247
|
+
var frame = el("div", "plan-stage-frame");
|
|
1248
|
+
var selected = selectedPlanLayout(data);
|
|
1249
|
+
// The slide card: the same embossed physical object the add-in uses for a
|
|
1250
|
+
// slide anywhere else. The clip lives HERE, not on the frame, because the
|
|
1251
|
+
// panel's layout menu has to escape it.
|
|
1252
|
+
var slide = el("div", "plan-stage-slide slide-emboss");
|
|
1253
|
+
if (selected && selected.previewUrl) {
|
|
1254
|
+
var chrome = el("img", "plan-stage-chrome");
|
|
1255
|
+
chrome.src = selected.previewUrl;
|
|
1256
|
+
chrome.alt = "";
|
|
1257
|
+
slide.appendChild(chrome);
|
|
1258
|
+
} else {
|
|
1259
|
+
slide.appendChild(el("div", "plan-stage-chrome plan-stage-chrome--blank"));
|
|
1311
1260
|
}
|
|
1312
|
-
|
|
1313
|
-
|
|
1314
|
-
|
|
1315
|
-
|
|
1316
|
-
var
|
|
1317
|
-
|
|
1318
|
-
|
|
1319
|
-
|
|
1320
|
-
|
|
1321
|
-
|
|
1322
|
-
|
|
1323
|
-
|
|
1261
|
+
slide.appendChild(renderSlotGeometry(selected));
|
|
1262
|
+
slide.appendChild(el("div", "plan-stage-veil"));
|
|
1263
|
+
frame.appendChild(slide);
|
|
1264
|
+
|
|
1265
|
+
var card = el("div", "plan-stage-card");
|
|
1266
|
+
|
|
1267
|
+
var beatRow = el("div", "plan-stage-row");
|
|
1268
|
+
beatRow.appendChild(el("span", "plan-stage-label", "Story beat"));
|
|
1269
|
+
beatRow.appendChild(
|
|
1270
|
+
renderDetailField("plan-stage-beat", "Story beat", data.keyMessage || "", false, function (next) {
|
|
1271
|
+
var d = copyOf(data);
|
|
1272
|
+
if (next) d.keyMessage = next;
|
|
1273
|
+
else delete d.keyMessage;
|
|
1274
|
+
commitStoryboard(segment, d);
|
|
1275
|
+
}),
|
|
1276
|
+
);
|
|
1277
|
+
card.appendChild(beatRow);
|
|
1278
|
+
|
|
1279
|
+
var bullets = data.contentNotes || data.depthNotes || [];
|
|
1280
|
+
function writeBullets(next) {
|
|
1281
|
+
var d = {};
|
|
1282
|
+
for (var k in data) if (k !== "contentNotes" && k !== "depthNotes") d[k] = data[k];
|
|
1283
|
+
if (next.length) d.contentNotes = next;
|
|
1284
|
+
commitStoryboard(segment, d);
|
|
1324
1285
|
}
|
|
1325
|
-
|
|
1326
|
-
|
|
1327
|
-
|
|
1328
|
-
|
|
1329
|
-
|
|
1330
|
-
|
|
1331
|
-
|
|
1332
|
-
|
|
1333
|
-
|
|
1334
|
-
|
|
1335
|
-
|
|
1286
|
+
var pointsRow = el("div", "plan-stage-row");
|
|
1287
|
+
pointsRow.appendChild(el("span", "plan-stage-label", "Key points"));
|
|
1288
|
+
var points = el("div", "plan-stage-points");
|
|
1289
|
+
var list = el("ul", "plan-stage-bullets");
|
|
1290
|
+
bullets.forEach(function (bullet, index) {
|
|
1291
|
+
var li = el("li", "plan-stage-bullet");
|
|
1292
|
+
li.appendChild(
|
|
1293
|
+
renderDetailField("plan-stage-bullet-text", "Key points", bullet, false, function (next) {
|
|
1294
|
+
writeBullets(
|
|
1295
|
+
bullets.reduce(function (acc, b, at) {
|
|
1296
|
+
if (at !== index) acc.push(b);
|
|
1297
|
+
else if (next) acc.push(next);
|
|
1298
|
+
return acc;
|
|
1299
|
+
}, []),
|
|
1300
|
+
);
|
|
1301
|
+
}),
|
|
1302
|
+
);
|
|
1303
|
+
if (!locked()) {
|
|
1304
|
+
var rm = document.createElement("button");
|
|
1305
|
+
rm.type = "button";
|
|
1306
|
+
rm.className = "plan-stage-bullet-remove";
|
|
1307
|
+
rm.title = "Remove this point";
|
|
1308
|
+
rm.setAttribute("aria-label", "Remove this point");
|
|
1309
|
+
rm.innerHTML = ICONS.x;
|
|
1310
|
+
rm.addEventListener("click", function () {
|
|
1311
|
+
writeBullets(
|
|
1312
|
+
bullets.filter(function (_, at) {
|
|
1313
|
+
return at !== index;
|
|
1314
|
+
}),
|
|
1315
|
+
);
|
|
1316
|
+
});
|
|
1317
|
+
li.appendChild(rm);
|
|
1318
|
+
}
|
|
1319
|
+
list.appendChild(li);
|
|
1336
1320
|
});
|
|
1337
|
-
|
|
1338
|
-
|
|
1339
|
-
|
|
1340
|
-
|
|
1341
|
-
|
|
1342
|
-
|
|
1343
|
-
|
|
1344
|
-
|
|
1345
|
-
|
|
1346
|
-
|
|
1347
|
-
// frame units per stage unit.
|
|
1348
|
-
function frameMapFor(data) {
|
|
1349
|
-
var selected = selectedPlanLayout(data);
|
|
1350
|
-
var zone = selected && selected.canvasBounds;
|
|
1351
|
-
// A zero-size zone would make the per-pixel scale infinite and a drag
|
|
1352
|
-
// write NaN coordinates — treat it as no zone.
|
|
1353
|
-
if (!zone || !(zone.w > 0) || !(zone.h > 0)) {
|
|
1354
|
-
return { x: 0, y: 0, sx: 1 / PLAN.STORYBOARD_WIDTH, sy: 1 / PLAN.STORYBOARD_HEIGHT };
|
|
1321
|
+
if (pendingBullet) {
|
|
1322
|
+
var draftLi = el("li", "plan-stage-bullet");
|
|
1323
|
+
var draftField = renderDetailField("plan-stage-bullet-text", "One short point", "", false, function (next) {
|
|
1324
|
+
pendingBullet = false;
|
|
1325
|
+
if (next) writeBullets(bullets.concat([next]));
|
|
1326
|
+
else render();
|
|
1327
|
+
});
|
|
1328
|
+
draftLi.appendChild(draftField);
|
|
1329
|
+
list.appendChild(draftLi);
|
|
1330
|
+
focusAfterRender = draftField;
|
|
1355
1331
|
}
|
|
1356
|
-
|
|
1357
|
-
|
|
1358
|
-
|
|
1359
|
-
|
|
1360
|
-
|
|
1361
|
-
|
|
1362
|
-
|
|
1363
|
-
|
|
1364
|
-
|
|
1365
|
-
|
|
1366
|
-
|
|
1367
|
-
|
|
1368
|
-
function stagePerPixel(map, width) {
|
|
1369
|
-
var height = (width * PLAN.STORYBOARD_HEIGHT) / PLAN.STORYBOARD_WIDTH;
|
|
1370
|
-
return { x: 1 / (width * map.sx), y: 1 / (height * map.sy) };
|
|
1371
|
-
}
|
|
1372
|
-
|
|
1373
|
-
function renderBlock(frameEl, block, data, segment, canRemove) {
|
|
1374
|
-
var node = el("div", "plan-storyboard-block" + (locked() ? "" : " plan-storyboard-block--movable"));
|
|
1375
|
-
node.setAttribute("data-emphasis", block.emphasis || "secondary");
|
|
1376
|
-
var map = frameMapFor(data);
|
|
1377
|
-
placeBlock(node, map, block.x, block.y, block.w, block.h);
|
|
1378
|
-
|
|
1379
|
-
function change(patch) {
|
|
1380
|
-
commitStoryboard(segment, updateBlock(data, block.id, patch));
|
|
1332
|
+
points.appendChild(list);
|
|
1333
|
+
if (!locked() && !pendingBullet && bullets.length < MAX_BULLETS) {
|
|
1334
|
+
var add = document.createElement("button");
|
|
1335
|
+
add.type = "button";
|
|
1336
|
+
add.className = "plan-stage-bullet-add";
|
|
1337
|
+
add.innerHTML = ICONS.plus;
|
|
1338
|
+
add.appendChild(document.createTextNode("Add a point"));
|
|
1339
|
+
add.addEventListener("click", function () {
|
|
1340
|
+
pendingBullet = true;
|
|
1341
|
+
render();
|
|
1342
|
+
});
|
|
1343
|
+
points.appendChild(add);
|
|
1381
1344
|
}
|
|
1345
|
+
pointsRow.appendChild(points);
|
|
1346
|
+
card.appendChild(pointsRow);
|
|
1382
1347
|
|
|
1383
|
-
|
|
1384
|
-
|
|
1385
|
-
|
|
1386
|
-
|
|
1387
|
-
|
|
1388
|
-
|
|
1389
|
-
|
|
1390
|
-
|
|
1348
|
+
var usesRow = renderImageUses(data.assetUses);
|
|
1349
|
+
if (usesRow) card.appendChild(usesRow);
|
|
1350
|
+
|
|
1351
|
+
var foot = el("div", "plan-stage-foot");
|
|
1352
|
+
// Same label column as the two fields above, so the layout reads as a third
|
|
1353
|
+
// thing this gate lets you change rather than a caption on the panel.
|
|
1354
|
+
foot.appendChild(el("span", "plan-stage-label", "Change layout"));
|
|
1355
|
+
foot.appendChild(
|
|
1356
|
+
renderLayoutPicker(data.layout, {
|
|
1357
|
+
className: "plan-stage-layout",
|
|
1358
|
+
ariaLabel: "Choose the slide's layout",
|
|
1359
|
+
onPick: function (componentName) {
|
|
1360
|
+
var d = copyOf(data);
|
|
1361
|
+
d.layout = componentName;
|
|
1362
|
+
d.layoutSelection = "explicit";
|
|
1363
|
+
d.layoutKey = (findLayout(componentName) || {}).layoutKey;
|
|
1364
|
+
delete d.layoutReason;
|
|
1365
|
+
commitStoryboard(segment, d);
|
|
1366
|
+
},
|
|
1391
1367
|
}),
|
|
1392
1368
|
);
|
|
1369
|
+
card.appendChild(foot);
|
|
1370
|
+
|
|
1371
|
+
frame.appendChild(card);
|
|
1372
|
+
return frame;
|
|
1373
|
+
}
|
|
1374
|
+
|
|
1375
|
+
// ---------- the slide's image uses (read-only) ----------
|
|
1376
|
+
// What a reviewer signs off is not "there is an image" but WHICH image and
|
|
1377
|
+
// what it is for, so the row shows the file, names the obligation in plain
|
|
1378
|
+
// language and quotes the agent's instruction verbatim. Framing is shown only
|
|
1379
|
+
// when it is not the default (`contain`, centred, uncropped) — a line saying
|
|
1380
|
+
// the default back is noise.
|
|
1381
|
+
//
|
|
1382
|
+
// Read-only ON PURPOSE. There is deliberately no role picker (design decision
|
|
1383
|
+
// 3): a correction is a comment like every other change to the plan, and the
|
|
1384
|
+
// agent rewrites the use.
|
|
1385
|
+
// Keyed by fidelity AND scope, not composed from a shared stem: the add-in
|
|
1386
|
+
// ships the same sentences in de/fr, where the "whole slide" / "one element"
|
|
1387
|
+
// qualifier does not sit where English puts it. One key per case is what lets
|
|
1388
|
+
// both clients say the same thing to the same user.
|
|
1389
|
+
var USE_WORDS = {
|
|
1390
|
+
original: "Placed as the image itself",
|
|
1391
|
+
"reconstruct:slide": "Rebuilt as editable slide content — whole slide",
|
|
1392
|
+
"reconstruct:element": "Rebuilt as editable slide content — one element",
|
|
1393
|
+
reference: "Visual reference only",
|
|
1394
|
+
};
|
|
1393
1395
|
|
|
1394
|
-
|
|
1395
|
-
|
|
1396
|
-
|
|
1397
|
-
rm.setAttribute("data-no-drag", "");
|
|
1398
|
-
rm.title = "Remove container";
|
|
1399
|
-
rm.setAttribute("aria-label", "Remove container");
|
|
1400
|
-
rm.addEventListener("pointerdown", function (e) {
|
|
1401
|
-
e.stopPropagation();
|
|
1402
|
-
});
|
|
1403
|
-
rm.addEventListener("click", function () {
|
|
1404
|
-
if (data.blocks.length <= 1) return;
|
|
1405
|
-
commitStoryboard(
|
|
1406
|
-
segment,
|
|
1407
|
-
withBlocks(
|
|
1408
|
-
data,
|
|
1409
|
-
data.blocks.filter(function (b) {
|
|
1410
|
-
return b.id !== block.id;
|
|
1411
|
-
}),
|
|
1412
|
-
),
|
|
1413
|
-
);
|
|
1414
|
-
});
|
|
1415
|
-
node.appendChild(rm);
|
|
1396
|
+
function useWords(use) {
|
|
1397
|
+
if (use.fidelity === "reconstruct") {
|
|
1398
|
+
return USE_WORDS["reconstruct:" + use.reconstructionScope] || USE_WORDS["reconstruct:element"];
|
|
1416
1399
|
}
|
|
1400
|
+
return USE_WORDS[use.fidelity] || use.fidelity;
|
|
1401
|
+
}
|
|
1417
1402
|
|
|
1418
|
-
|
|
1419
|
-
|
|
1420
|
-
|
|
1421
|
-
|
|
1422
|
-
rz.type = "button";
|
|
1423
|
-
rz.setAttribute("data-no-drag", "");
|
|
1424
|
-
rz.title = "Drag to resize";
|
|
1425
|
-
rz.setAttribute("aria-label", "Drag to resize");
|
|
1426
|
-
wireBlockResize(rz, frameEl, block, map, change);
|
|
1427
|
-
node.appendChild(rz);
|
|
1403
|
+
function imageAssetById(assetId) {
|
|
1404
|
+
var all = (CFG && CFG.imageAssets) || [];
|
|
1405
|
+
for (var i = 0; i < all.length; i++) {
|
|
1406
|
+
if (all[i] && all[i].assetId === assetId) return all[i];
|
|
1428
1407
|
}
|
|
1429
|
-
return
|
|
1408
|
+
return null;
|
|
1430
1409
|
}
|
|
1431
1410
|
|
|
1432
|
-
function
|
|
1433
|
-
|
|
1434
|
-
|
|
1435
|
-
|
|
1436
|
-
|
|
1437
|
-
|
|
1438
|
-
|
|
1439
|
-
|
|
1440
|
-
|
|
1441
|
-
|
|
1442
|
-
|
|
1443
|
-
|
|
1444
|
-
};
|
|
1445
|
-
}
|
|
1446
|
-
function wireBlockDrag(node, frameEl, block, map, change) {
|
|
1447
|
-
var st = null;
|
|
1448
|
-
node.addEventListener("pointerdown", function (e) {
|
|
1449
|
-
if (locked() || e.button !== 0) return;
|
|
1450
|
-
if (e.target.closest && e.target.closest(".plan-storyboard-field, [data-no-drag]")) return;
|
|
1451
|
-
var rect = frameEl.getBoundingClientRect();
|
|
1452
|
-
if (!rect || rect.width <= 0) return;
|
|
1453
|
-
st = {
|
|
1454
|
-
pointerId: e.pointerId,
|
|
1455
|
-
ox: e.clientX,
|
|
1456
|
-
oy: e.clientY,
|
|
1457
|
-
sx: block.x,
|
|
1458
|
-
sy: block.y,
|
|
1459
|
-
scale: stagePerPixel(map, rect.width),
|
|
1460
|
-
moved: false,
|
|
1461
|
-
};
|
|
1462
|
-
try {
|
|
1463
|
-
node.setPointerCapture(e.pointerId);
|
|
1464
|
-
} catch {
|
|
1465
|
-
/* headless */
|
|
1466
|
-
}
|
|
1467
|
-
});
|
|
1468
|
-
node.addEventListener("pointermove", function (e) {
|
|
1469
|
-
if (!st || st.pointerId !== e.pointerId) return;
|
|
1470
|
-
var dx = e.clientX - st.ox;
|
|
1471
|
-
var dy = e.clientY - st.oy;
|
|
1472
|
-
if (!st.moved && Math.hypot(dx, dy) < BLOCK_DRAG_THRESHOLD) return;
|
|
1473
|
-
st.moved = true;
|
|
1474
|
-
node.classList.add("plan-storyboard-block--dragging");
|
|
1475
|
-
var p = clampPos(block, st.sx + dx * st.scale.x, st.sy + dy * st.scale.y);
|
|
1476
|
-
placeBlock(node, map, p.x, p.y, block.w, block.h);
|
|
1477
|
-
});
|
|
1478
|
-
function end(e) {
|
|
1479
|
-
if (!st || st.pointerId !== e.pointerId) return;
|
|
1480
|
-
try {
|
|
1481
|
-
node.releasePointerCapture(e.pointerId);
|
|
1482
|
-
} catch {
|
|
1483
|
-
/* ignore */
|
|
1484
|
-
}
|
|
1485
|
-
var s = st;
|
|
1486
|
-
st = null;
|
|
1487
|
-
if (!s.moved) {
|
|
1488
|
-
node.classList.remove("plan-storyboard-block--dragging");
|
|
1489
|
-
return;
|
|
1490
|
-
}
|
|
1491
|
-
var p = clampPos(block, s.sx + (e.clientX - s.ox) * s.scale.x, s.sy + (e.clientY - s.oy) * s.scale.y);
|
|
1492
|
-
var x = Math.round(p.x);
|
|
1493
|
-
var y = Math.round(p.y);
|
|
1494
|
-
if (x !== block.x || y !== block.y) change({ x: x, y: y });
|
|
1495
|
-
else node.classList.remove("plan-storyboard-block--dragging");
|
|
1411
|
+
function useFramingText(use) {
|
|
1412
|
+
// Only an original is framed, and only a non-default framing is worth a
|
|
1413
|
+
// line: `contain`, centred and uncropped says nothing back to the reviewer.
|
|
1414
|
+
// The reconstruct scope is part of the label above, not a framing fact.
|
|
1415
|
+
if (use.fidelity !== "original") return "";
|
|
1416
|
+
var bits = [];
|
|
1417
|
+
if (use.crop) {
|
|
1418
|
+
bits.push(
|
|
1419
|
+
"Cropped to " +
|
|
1420
|
+
Math.round(use.crop.w * 100) + "% × " + Math.round(use.crop.h * 100) +
|
|
1421
|
+
"% of the source",
|
|
1422
|
+
);
|
|
1496
1423
|
}
|
|
1497
|
-
|
|
1498
|
-
|
|
1499
|
-
|
|
1500
|
-
|
|
1501
|
-
|
|
1502
|
-
|
|
1503
|
-
|
|
1504
|
-
|
|
1505
|
-
|
|
1506
|
-
|
|
1507
|
-
|
|
1508
|
-
|
|
1509
|
-
var
|
|
1510
|
-
if (
|
|
1511
|
-
|
|
1512
|
-
|
|
1513
|
-
|
|
1514
|
-
|
|
1515
|
-
|
|
1516
|
-
|
|
1517
|
-
scale: stagePerPixel(map, rect.width),
|
|
1518
|
-
moved: false,
|
|
1519
|
-
};
|
|
1520
|
-
try {
|
|
1521
|
-
handle.setPointerCapture(e.pointerId);
|
|
1522
|
-
} catch {
|
|
1523
|
-
/* headless */
|
|
1424
|
+
if (use.fit === "cover") bits.push("Framed to fill (cover)");
|
|
1425
|
+
if (use.slot) bits.push("In the " + use.slot + " slot");
|
|
1426
|
+
return bits.join(" · ");
|
|
1427
|
+
}
|
|
1428
|
+
|
|
1429
|
+
function renderImageUses(uses) {
|
|
1430
|
+
if (!uses || !uses.length) return null;
|
|
1431
|
+
var row = el("div", "plan-stage-row");
|
|
1432
|
+
row.appendChild(el("span", "plan-stage-label", uses.length === 1 ? "Image" : "Images"));
|
|
1433
|
+
var list = el("div", "plan-stage-uses");
|
|
1434
|
+
uses.forEach(function (use) {
|
|
1435
|
+
var asset = imageAssetById(use.assetId);
|
|
1436
|
+
var item = el("div", "plan-stage-use");
|
|
1437
|
+
if (asset && asset.previewUrl) {
|
|
1438
|
+
var thumb = el("img", "plan-stage-use-thumb");
|
|
1439
|
+
thumb.src = asset.previewUrl;
|
|
1440
|
+
thumb.alt = "";
|
|
1441
|
+
item.appendChild(thumb);
|
|
1442
|
+
} else {
|
|
1443
|
+
item.appendChild(el("div", "plan-stage-use-thumb"));
|
|
1524
1444
|
}
|
|
1525
|
-
|
|
1526
|
-
|
|
1527
|
-
|
|
1528
|
-
|
|
1529
|
-
|
|
1530
|
-
|
|
1531
|
-
|
|
1532
|
-
|
|
1533
|
-
|
|
1534
|
-
|
|
1535
|
-
|
|
1536
|
-
|
|
1537
|
-
|
|
1538
|
-
if (
|
|
1539
|
-
|
|
1540
|
-
|
|
1541
|
-
|
|
1542
|
-
/* ignore */
|
|
1445
|
+
var body = el("div", "plan-stage-use-body");
|
|
1446
|
+
var what = el("div", "plan-stage-use-what");
|
|
1447
|
+
what.textContent = useWords(use);
|
|
1448
|
+
// The file the user handed over — the only way to tell two screenshots
|
|
1449
|
+
// apart when both thumbnails look alike.
|
|
1450
|
+
var name = el("span", "plan-stage-use-name", "");
|
|
1451
|
+
name.textContent = " · " + ((asset && asset.displayName) || use.assetId);
|
|
1452
|
+
what.appendChild(name);
|
|
1453
|
+
body.appendChild(what);
|
|
1454
|
+
var instruction = el("div", "plan-stage-use-instruction");
|
|
1455
|
+
instruction.textContent = use.instruction || "";
|
|
1456
|
+
body.appendChild(instruction);
|
|
1457
|
+
var framing = useFramingText(use);
|
|
1458
|
+
if (framing) {
|
|
1459
|
+
var fr = el("div", "plan-stage-use-framing");
|
|
1460
|
+
fr.textContent = framing;
|
|
1461
|
+
body.appendChild(fr);
|
|
1543
1462
|
}
|
|
1544
|
-
|
|
1545
|
-
|
|
1546
|
-
|
|
1547
|
-
|
|
1548
|
-
|
|
1549
|
-
|
|
1550
|
-
|
|
1463
|
+
item.appendChild(body);
|
|
1464
|
+
list.appendChild(item);
|
|
1465
|
+
});
|
|
1466
|
+
row.appendChild(list);
|
|
1467
|
+
return row;
|
|
1468
|
+
}
|
|
1469
|
+
|
|
1470
|
+
// The layout's own slots over its render: how a reviewer sees WHICH layout
|
|
1471
|
+
// they are on without reading its name. Read-only, pointer-through, and
|
|
1472
|
+
// under the veil so the panel stays the brightest thing on the stage.
|
|
1473
|
+
function renderSlotGeometry(layout, extraClass) {
|
|
1474
|
+
var slots = el("div", "plan-stage-slots" + (extraClass ? " " + extraClass : ""));
|
|
1475
|
+
slots.setAttribute("aria-hidden", "true");
|
|
1476
|
+
if (!layout) return slots;
|
|
1477
|
+
function place(node, rect) {
|
|
1478
|
+
node.style.left = rect.x * 100 + "%";
|
|
1479
|
+
node.style.top = rect.y * 100 + "%";
|
|
1480
|
+
node.style.width = rect.w * 100 + "%";
|
|
1481
|
+
node.style.height = rect.h * 100 + "%";
|
|
1482
|
+
slots.appendChild(node);
|
|
1551
1483
|
}
|
|
1552
|
-
|
|
1553
|
-
|
|
1554
|
-
if (
|
|
1484
|
+
(layout.placeholders || []).forEach(function (ph) {
|
|
1485
|
+
var box = el("div", "plan-stage-slot");
|
|
1486
|
+
if (ph.label) box.appendChild(el("span", "plan-stage-slot-label", "")).textContent = ph.label;
|
|
1487
|
+
place(box, ph);
|
|
1555
1488
|
});
|
|
1489
|
+
(layout.contentAreas || []).forEach(function (area) {
|
|
1490
|
+
place(el("div", "plan-stage-zone"), area);
|
|
1491
|
+
});
|
|
1492
|
+
return slots;
|
|
1556
1493
|
}
|
|
1557
1494
|
|
|
1558
|
-
function
|
|
1559
|
-
var
|
|
1560
|
-
|
|
1561
|
-
|
|
1562
|
-
|
|
1563
|
-
|
|
1564
|
-
if (!locked()) {
|
|
1565
|
-
field.contentEditable = "true";
|
|
1566
|
-
field.spellcheck = true;
|
|
1567
|
-
}
|
|
1568
|
-
// Commit-once guard: a commit re-renders the surface and detaches this
|
|
1569
|
-
// focused field, firing a second blur \u2014 which must not re-splice the stale
|
|
1570
|
-
// segment (double-commit \u2192 corrupted fences on the single-line beat field).
|
|
1571
|
-
var committed = false;
|
|
1572
|
-
function commit() {
|
|
1573
|
-
if (committed) return;
|
|
1574
|
-
committed = true;
|
|
1575
|
-
var normalized = (field.textContent || "").replace(/\u00a0/g, " ").trim();
|
|
1576
|
-
field.textContent = normalized;
|
|
1577
|
-
if (normalized !== value) onCommit(normalized);
|
|
1578
|
-
}
|
|
1579
|
-
field.addEventListener("blur", commit);
|
|
1580
|
-
if (!multiline) {
|
|
1581
|
-
// Enter blurs; the blur handler is the single committer.
|
|
1582
|
-
field.addEventListener("keydown", function (e) {
|
|
1583
|
-
if (e.key !== "Enter") return;
|
|
1584
|
-
e.preventDefault();
|
|
1585
|
-
field.blur();
|
|
1586
|
-
});
|
|
1587
|
-
}
|
|
1588
|
-
return field;
|
|
1495
|
+
function draftNote() {
|
|
1496
|
+
var note = el("div", "plan-storyboard-draft-note");
|
|
1497
|
+
note.innerHTML =
|
|
1498
|
+
ICONS.ruler +
|
|
1499
|
+
"<p><strong>Draft plan.</strong> The finished slides follow your corporate design.</p>";
|
|
1500
|
+
return note;
|
|
1589
1501
|
}
|
|
1590
1502
|
|
|
1591
|
-
// Layout picker: current choice (chrome thumbnail + name) plus a dropdown of
|
|
1592
|
-
// the bundle's layouts (CFG.layouts; empty → read-only). Used per slide in
|
|
1593
|
-
// the details aside and once for the deck-level knob; `opts.onPick` owns the
|
|
1594
|
-
// commit. All labels/names go through textContent — bundle strings never
|
|
1595
|
-
// reach innerHTML.
|
|
1596
|
-
// One short line per option instead of the codegen's geometry dump: the
|
|
1597
|
-
// count is what a human picks on; the exact rectangles are drawn on the
|
|
1598
|
-
// storyboard frame once the layout is chosen.
|
|
1599
1503
|
function contentAreaSummary(option) {
|
|
1600
1504
|
var n = option.contentAreas ? option.contentAreas.length : 0;
|
|
1601
1505
|
if (!n && option.canvas) n = (option.canvas.match(/(?:^|;)\s*\d+\./g) || []).length || 1;
|
|
1602
1506
|
if (!n) return "Chrome only";
|
|
1603
1507
|
return n === 1 ? "1 content area" : n + " content areas";
|
|
1604
1508
|
}
|
|
1509
|
+
function usesSummary(option) {
|
|
1510
|
+
var u = option.selection;
|
|
1511
|
+
if (!u) return "";
|
|
1512
|
+
if (u.currentCount) return u.currentCount + " uses in this presentation";
|
|
1513
|
+
if (u.usage && u.usage.count) return u.usage.count + " observed uses";
|
|
1514
|
+
return "";
|
|
1515
|
+
}
|
|
1605
1516
|
|
|
1517
|
+
// ---------- the compact deck-level layout knob (rail head) ----------
|
|
1518
|
+
// Dropdown of the bundle's layouts: one pick binds EVERY body board's
|
|
1519
|
+
// `layout` in one commit; a mixed deck reads "Mixed". Read-only without a
|
|
1520
|
+
// bundle. Bundle strings only ever reach textContent.
|
|
1606
1521
|
function renderLayoutPicker(value, opts) {
|
|
1607
|
-
var options =
|
|
1608
|
-
if (opts.bodyOnly) options = options.filter(function(o) { return o.canvasBounds || o.contentAreas
|
|
1522
|
+
var options = layoutOptions();
|
|
1523
|
+
if (opts.bodyOnly) options = options.filter(function (o) { return o.canvasBounds || (o.contentAreas && o.contentAreas.length); });
|
|
1609
1524
|
var root = el("div", "plan-storyboard-layout-picker" + (opts.className ? " " + opts.className : ""));
|
|
1610
|
-
var selected =
|
|
1611
|
-
for (var i = 0; i < options.length; i++) {
|
|
1612
|
-
if (options[i].componentName === value) selected = options[i];
|
|
1613
|
-
}
|
|
1525
|
+
var selected = findLayout(value);
|
|
1614
1526
|
var canPick = !locked() && options.length > 0;
|
|
1615
1527
|
|
|
1616
1528
|
var current = document.createElement("button");
|
|
@@ -1623,51 +1535,37 @@
|
|
|
1623
1535
|
thumb.className = "plan-storyboard-layout-thumb";
|
|
1624
1536
|
thumb.src = selected.previewUrl;
|
|
1625
1537
|
thumb.alt = "";
|
|
1626
|
-
thumb.setAttribute("aria-hidden", "true");
|
|
1627
1538
|
current.appendChild(thumb);
|
|
1628
1539
|
}
|
|
1629
1540
|
var nameSpan = el("span", "plan-storyboard-layout-name");
|
|
1630
1541
|
nameSpan.textContent = (selected && selected.nativeLayoutName) || value || opts.placeholder || "No layout chosen";
|
|
1631
1542
|
current.appendChild(nameSpan);
|
|
1632
|
-
if (value && options.length > 0 && !selected) {
|
|
1633
|
-
var badge = el("span", "plan-storyboard-layout-unknown");
|
|
1634
|
-
badge.textContent = "Not in this brand";
|
|
1635
|
-
current.appendChild(badge);
|
|
1636
|
-
}
|
|
1637
1543
|
if (canPick) {
|
|
1638
|
-
var chevron = el("span", "plan-storyboard-layout-chevron",
|
|
1544
|
+
var chevron = el("span", "plan-storyboard-layout-chevron", CHEVRON);
|
|
1639
1545
|
chevron.setAttribute("aria-hidden", "true");
|
|
1640
1546
|
current.appendChild(chevron);
|
|
1641
1547
|
}
|
|
1642
1548
|
root.appendChild(current);
|
|
1643
|
-
|
|
1644
1549
|
if (!canPick) return root;
|
|
1645
1550
|
|
|
1646
1551
|
var menu = null;
|
|
1647
1552
|
function closeMenu() {
|
|
1648
|
-
if (menu)
|
|
1649
|
-
|
|
1650
|
-
|
|
1651
|
-
|
|
1652
|
-
}
|
|
1553
|
+
if (!menu) return;
|
|
1554
|
+
menu.remove();
|
|
1555
|
+
menu = null;
|
|
1556
|
+
document.removeEventListener("pointerdown", onOutside);
|
|
1653
1557
|
}
|
|
1654
1558
|
function onOutside(e) {
|
|
1655
1559
|
if (!root.contains(e.target)) closeMenu();
|
|
1656
1560
|
}
|
|
1657
1561
|
current.addEventListener("click", function () {
|
|
1658
|
-
if (menu)
|
|
1659
|
-
closeMenu();
|
|
1660
|
-
return;
|
|
1661
|
-
}
|
|
1562
|
+
if (menu) return closeMenu();
|
|
1662
1563
|
menu = el("div", "plan-storyboard-layout-menu");
|
|
1663
1564
|
menu.setAttribute("role", "listbox");
|
|
1664
1565
|
options.forEach(function (option) {
|
|
1665
1566
|
var btn = document.createElement("button");
|
|
1666
1567
|
btn.type = "button";
|
|
1667
|
-
btn.className =
|
|
1668
|
-
option.componentName === value
|
|
1669
|
-
? "plan-storyboard-layout-option plan-storyboard-layout-option--selected"
|
|
1670
|
-
: "plan-storyboard-layout-option";
|
|
1568
|
+
btn.className = "plan-storyboard-layout-option" + (option.componentName === value ? " plan-storyboard-layout-option--selected" : "");
|
|
1671
1569
|
btn.setAttribute("role", "option");
|
|
1672
1570
|
btn.setAttribute("aria-selected", option.componentName === value ? "true" : "false");
|
|
1673
1571
|
if (option.previewUrl) {
|
|
@@ -1675,23 +1573,14 @@
|
|
|
1675
1573
|
t.className = "plan-storyboard-layout-thumb plan-storyboard-layout-thumb--option";
|
|
1676
1574
|
t.src = option.previewUrl;
|
|
1677
1575
|
t.alt = "";
|
|
1678
|
-
t.setAttribute("aria-hidden", "true");
|
|
1679
1576
|
btn.appendChild(t);
|
|
1680
1577
|
}
|
|
1681
1578
|
var text = el("span", "plan-storyboard-layout-option-text");
|
|
1682
1579
|
var n = el("span", "plan-storyboard-layout-name");
|
|
1683
1580
|
n.textContent = option.nativeLayoutName || option.componentName;
|
|
1684
|
-
if (options.filter(function (other) { return other.nativeLayoutName === option.nativeLayoutName; }).length > 1 && option.selection?.masterName) n.textContent += " · " + option.selection.masterName;
|
|
1685
|
-
if (option.selection) n.title = option.selection.masterName || "";
|
|
1686
1581
|
text.appendChild(n);
|
|
1687
|
-
if (option.purpose) {
|
|
1688
|
-
var p = el("span", "plan-storyboard-layout-purpose");
|
|
1689
|
-
p.textContent = option.purpose;
|
|
1690
|
-
text.appendChild(p);
|
|
1691
|
-
}
|
|
1692
1582
|
var c = el("span", "plan-storyboard-layout-canvas");
|
|
1693
|
-
c.textContent = contentAreaSummary(option);
|
|
1694
|
-
if (option.selection) { var u = option.selection; c.textContent += u.currentCount ? " · " + u.currentCount + " uses in this presentation" : u.usage.count ? " · " + u.usage.count + " observed uses" : ""; }
|
|
1583
|
+
c.textContent = [contentAreaSummary(option), usesSummary(option)].filter(Boolean).join(" · ");
|
|
1695
1584
|
text.appendChild(c);
|
|
1696
1585
|
btn.appendChild(text);
|
|
1697
1586
|
btn.addEventListener("click", function () {
|
|
@@ -1706,356 +1595,360 @@
|
|
|
1706
1595
|
return root;
|
|
1707
1596
|
}
|
|
1708
1597
|
|
|
1709
|
-
//
|
|
1710
|
-
|
|
1598
|
+
// ---------- the header's deck-wide choices: the design, then the body slides' layout ----------
|
|
1599
|
+
function renderBarTools(storyboardData) {
|
|
1600
|
+
if (!barTools) return;
|
|
1601
|
+
barTools.textContent = "";
|
|
1602
|
+
var designPicker = renderDesignPicker();
|
|
1603
|
+
if (designPicker) barTools.appendChild(barTool("Design", designPicker));
|
|
1604
|
+
barTools.appendChild(barTool("Slide Layouts", renderDeckLayoutKnob(storyboardData)));
|
|
1605
|
+
}
|
|
1606
|
+
function barTool(label, control) {
|
|
1607
|
+
var group = el("div", "sm-bar-tool");
|
|
1608
|
+
group.appendChild(el("span", "sm-bar-tool-label", "")).textContent = label;
|
|
1609
|
+
group.appendChild(control);
|
|
1610
|
+
return group;
|
|
1611
|
+
}
|
|
1711
1612
|
function renderDeckLayoutKnob(storyboardData) {
|
|
1712
|
-
var wrap = el("div", "plan-storyline-deck-layout");
|
|
1713
|
-
wrap.appendChild(el("span", "plan-storyline-deck-layout-label", "Body defaults"));
|
|
1714
1613
|
var seen = {};
|
|
1715
1614
|
var distinct = 0;
|
|
1716
1615
|
for (var i = 0; i < storyboardData.length; i++) {
|
|
1616
|
+
if (storyboardData[i].role === "frame") continue;
|
|
1717
1617
|
var name = storyboardData[i].layout || "";
|
|
1718
1618
|
if (!seen[name]) {
|
|
1719
1619
|
seen[name] = true;
|
|
1720
1620
|
distinct++;
|
|
1721
1621
|
}
|
|
1722
1622
|
}
|
|
1723
|
-
var common = distinct === 1
|
|
1724
|
-
|
|
1725
|
-
|
|
1726
|
-
|
|
1727
|
-
|
|
1728
|
-
|
|
1729
|
-
|
|
1730
|
-
|
|
1731
|
-
},
|
|
1732
|
-
}),
|
|
1733
|
-
);
|
|
1734
|
-
if ((CFG.layouts || []).some(function(o) { return o.selection?.counterpartKey; })) {
|
|
1735
|
-
["light", "dark"].forEach(function(tone) {
|
|
1736
|
-
var button = el("button", "plan-storyboard-layout-current", tone === "dark" ? "Dark variants" : "Light variants");
|
|
1737
|
-
button.type = "button";
|
|
1738
|
-
button.addEventListener("click", function() { commitPlan(PLAN.setStoryboardStyle(S.value, tone, CFG.layouts || [])); });
|
|
1739
|
-
wrap.appendChild(button);
|
|
1740
|
-
});
|
|
1741
|
-
}
|
|
1742
|
-
return wrap;
|
|
1743
|
-
}
|
|
1744
|
-
|
|
1745
|
-
function renderDetails(data, segment) {
|
|
1746
|
-
var aside = el("aside", "plan-storyboard-detail");
|
|
1747
|
-
var roleSelect = document.createElement("select");
|
|
1748
|
-
roleSelect.setAttribute("aria-label", "Slide role");
|
|
1749
|
-
["body", "cover", "agenda", "divider", "closing"].forEach(function(role) {
|
|
1750
|
-
var option = document.createElement("option"); option.value = role; option.textContent = role;
|
|
1751
|
-
option.selected = role === (data.role === "frame" ? data.frameKind : "body"); roleSelect.appendChild(option);
|
|
1752
|
-
});
|
|
1753
|
-
roleSelect.addEventListener("change", function() { var d = Object.assign({}, data);
|
|
1754
|
-
d.role = roleSelect.value === "body" ? "body" : "frame";
|
|
1755
|
-
d.frameKind = d.role === "frame" ? roleSelect.value : undefined;
|
|
1756
|
-
commitStoryboard(segment, d);
|
|
1623
|
+
var common = distinct === 1 ? Object.keys(seen)[0] : undefined;
|
|
1624
|
+
return renderLayoutPicker(common || undefined, {
|
|
1625
|
+
placeholder: distinct > 1 ? "Mixed layouts" : undefined,
|
|
1626
|
+
ariaLabel: "Set the layout for every body slide",
|
|
1627
|
+
bodyOnly: true,
|
|
1628
|
+
onPick: function (componentName) {
|
|
1629
|
+
commitPlan(PLAN.setStoryboardLayoutForAll(S.value, componentName, layoutOptions()));
|
|
1630
|
+
},
|
|
1757
1631
|
});
|
|
1758
|
-
aside.appendChild(roleSelect);
|
|
1759
|
-
if (data.layoutReason) aside.appendChild(el("div", "plan-storyboard-layout-purpose", data.layoutReason));
|
|
1760
|
-
aside.appendChild(el("div", "plan-storyboard-detail-label", "Layout"));
|
|
1761
|
-
aside.appendChild(
|
|
1762
|
-
renderLayoutPicker(data.layout, {
|
|
1763
|
-
onPick: function (componentName) {
|
|
1764
|
-
var d = {};
|
|
1765
|
-
for (var k in data) d[k] = data[k];
|
|
1766
|
-
d.layout = componentName;
|
|
1767
|
-
d.layoutSelection = "explicit";
|
|
1768
|
-
d.layoutKey = (CFG.layouts || []).find(function(o) { return o.componentName === componentName; })?.layoutKey;
|
|
1769
|
-
delete d.layoutReason;
|
|
1770
|
-
commitStoryboard(segment, d);
|
|
1771
|
-
},
|
|
1772
|
-
}),
|
|
1773
|
-
);
|
|
1774
|
-
aside.appendChild(el("div", "plan-storyboard-detail-label", "Story beat"));
|
|
1775
|
-
aside.appendChild(
|
|
1776
|
-
renderDetailField(
|
|
1777
|
-
"plan-storyboard-key-message",
|
|
1778
|
-
"Story beat",
|
|
1779
|
-
data.keyMessage || "",
|
|
1780
|
-
false,
|
|
1781
|
-
function (next) {
|
|
1782
|
-
if (next) {
|
|
1783
|
-
var d = {};
|
|
1784
|
-
for (var k in data) d[k] = data[k];
|
|
1785
|
-
d.keyMessage = next;
|
|
1786
|
-
commitStoryboard(segment, d);
|
|
1787
|
-
} else {
|
|
1788
|
-
var d2 = {};
|
|
1789
|
-
for (var k2 in data) if (k2 !== "keyMessage") d2[k2] = data[k2];
|
|
1790
|
-
commitStoryboard(segment, d2);
|
|
1791
|
-
}
|
|
1792
|
-
},
|
|
1793
|
-
),
|
|
1794
|
-
);
|
|
1795
|
-
aside.appendChild(el("div", "plan-storyboard-detail-label", "Content draft"));
|
|
1796
|
-
var currentContent = notesToText(data.contentNotes);
|
|
1797
|
-
aside.appendChild(
|
|
1798
|
-
renderDetailField(
|
|
1799
|
-
"plan-storyboard-detail-text",
|
|
1800
|
-
"Content draft",
|
|
1801
|
-
currentContent,
|
|
1802
|
-
true,
|
|
1803
|
-
function (next) {
|
|
1804
|
-
var notes = textToNotes(next);
|
|
1805
|
-
if (notesToText(notes) === currentContent) return;
|
|
1806
|
-
var d = {};
|
|
1807
|
-
for (var k in data) if (k !== "contentNotes" && k !== "depthNotes") d[k] = data[k];
|
|
1808
|
-
if (notes) d.contentNotes = notes;
|
|
1809
|
-
commitStoryboard(segment, d);
|
|
1810
|
-
},
|
|
1811
|
-
),
|
|
1812
|
-
);
|
|
1813
|
-
return aside;
|
|
1814
1632
|
}
|
|
1815
1633
|
|
|
1816
|
-
//
|
|
1817
|
-
//
|
|
1818
|
-
//
|
|
1819
|
-
|
|
1820
|
-
|
|
1821
|
-
|
|
1822
|
-
|
|
1823
|
-
|
|
1634
|
+
// ---------- a dropdown menu (the comment target, the design picker) ----------
|
|
1635
|
+
// Drawn here rather than a native <select>, whose open list is the system's
|
|
1636
|
+
// own — white on this dark page. Opens on click (or an arrow key), closes on
|
|
1637
|
+
// a pick, an outside press, Tab or Escape; the arrow keys walk the options.
|
|
1638
|
+
// `render(node, item, selected)` fills an option; strings reach textContent.
|
|
1639
|
+
var CHEVRON = '<svg viewBox="0 0 10 6" aria-hidden="true"><path d="M1 1l4 4 4-4" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/></svg>';
|
|
1640
|
+
function attachMenu(root, trigger, spec) {
|
|
1641
|
+
var menu = null;
|
|
1642
|
+
trigger.setAttribute("aria-haspopup", "listbox");
|
|
1643
|
+
trigger.setAttribute("aria-expanded", "false");
|
|
1644
|
+
function options() {
|
|
1645
|
+
return menu ? Array.prototype.slice.call(menu.querySelectorAll('[role="option"]')) : [];
|
|
1824
1646
|
}
|
|
1825
|
-
|
|
1826
|
-
|
|
1827
|
-
|
|
1828
|
-
|
|
1829
|
-
|
|
1830
|
-
|
|
1831
|
-
|
|
1832
|
-
function renderZoneOverlay(zone, areas) {
|
|
1833
|
-
var zones = el("div", "plan-storyboard-zones");
|
|
1834
|
-
zones.setAttribute("aria-hidden", "true");
|
|
1835
|
-
function wash(style) {
|
|
1836
|
-
var d = el("div", "plan-storyboard-zone-wash");
|
|
1837
|
-
for (var k in style) d.style[k] = style[k];
|
|
1838
|
-
zones.appendChild(d);
|
|
1647
|
+
function close(refocus) {
|
|
1648
|
+
if (!menu) return;
|
|
1649
|
+
menu.remove();
|
|
1650
|
+
menu = null;
|
|
1651
|
+
trigger.setAttribute("aria-expanded", "false");
|
|
1652
|
+
document.removeEventListener("pointerdown", onOutside, true);
|
|
1653
|
+
if (refocus) trigger.focus();
|
|
1839
1654
|
}
|
|
1840
|
-
|
|
1841
|
-
|
|
1842
|
-
wash({ left: "0", top: pctOf(zone.y), width: pctOf(zone.x), height: pctOf(zone.h) });
|
|
1843
|
-
wash({
|
|
1844
|
-
left: pctOf(zone.x + zone.w),
|
|
1845
|
-
top: pctOf(zone.y),
|
|
1846
|
-
right: "0",
|
|
1847
|
-
height: pctOf(zone.h),
|
|
1848
|
-
});
|
|
1849
|
-
// One rectangle per writable area; the washes above cover the union bbox
|
|
1850
|
-
// and the gutters between areas stay unmarked on purpose.
|
|
1851
|
-
for (var i = 0; i < areas.length; i++) {
|
|
1852
|
-
var area = areas[i];
|
|
1853
|
-
var rect = el("div", "plan-storyboard-zone-canvas");
|
|
1854
|
-
rect.style.left = pctOf(area.x);
|
|
1855
|
-
rect.style.top = pctOf(area.y);
|
|
1856
|
-
rect.style.width = pctOf(area.w);
|
|
1857
|
-
rect.style.height = pctOf(area.h);
|
|
1858
|
-
var chip = el("span", "plan-storyboard-zone-label");
|
|
1859
|
-
chip.textContent = areas.length > 1 ? "Content area " + (i + 1) : "Content area";
|
|
1860
|
-
rect.appendChild(chip);
|
|
1861
|
-
zones.appendChild(rect);
|
|
1655
|
+
function onOutside(e) {
|
|
1656
|
+
if (!root.contains(e.target)) close(false);
|
|
1862
1657
|
}
|
|
1863
|
-
|
|
1864
|
-
|
|
1865
|
-
|
|
1866
|
-
|
|
1867
|
-
|
|
1868
|
-
|
|
1869
|
-
|
|
1870
|
-
|
|
1871
|
-
|
|
1872
|
-
|
|
1873
|
-
|
|
1874
|
-
|
|
1875
|
-
|
|
1876
|
-
|
|
1877
|
-
|
|
1878
|
-
|
|
1879
|
-
box.style.height = pctOf(slot.h);
|
|
1880
|
-
box.appendChild(el("span", "plan-storyboard-image-caption", "Image idea"));
|
|
1881
|
-
var input = document.createElement("textarea");
|
|
1882
|
-
input.className = "plan-storyboard-image-input";
|
|
1883
|
-
input.value = value;
|
|
1884
|
-
input.maxLength = PLAN.MAX_IMAGE_PROMPT_LENGTH;
|
|
1885
|
-
input.placeholder = "Describe the image for this slot — leave empty to let the AI decide";
|
|
1886
|
-
input.disabled = locked();
|
|
1887
|
-
input.setAttribute("aria-label", "Image idea " + slot.key);
|
|
1888
|
-
input.addEventListener("blur", function () {
|
|
1889
|
-
var trimmed = input.value.trim().slice(0, PLAN.MAX_IMAGE_PROMPT_LENGTH);
|
|
1890
|
-
if (trimmed === value.trim()) return;
|
|
1891
|
-
var next = {};
|
|
1892
|
-
for (var k in data.imagePrompts || {}) next[k] = data.imagePrompts[k];
|
|
1893
|
-
if (trimmed) next[slot.key] = trimmed;
|
|
1894
|
-
else delete next[slot.key];
|
|
1895
|
-
var rest = {};
|
|
1896
|
-
for (var k2 in data) if (k2 !== "imagePrompts") rest[k2] = data[k2];
|
|
1897
|
-
if (Object.keys(next).length) rest.imagePrompts = next;
|
|
1898
|
-
commitStoryboard(segment, rest);
|
|
1658
|
+
function open() {
|
|
1659
|
+
menu = el("div", spec.menuClass);
|
|
1660
|
+
menu.setAttribute("role", "listbox");
|
|
1661
|
+
spec.items.forEach(function (item) {
|
|
1662
|
+
var selected = !!spec.isSelected(item);
|
|
1663
|
+
var btn = document.createElement("button");
|
|
1664
|
+
btn.type = "button";
|
|
1665
|
+
btn.className = spec.optionClass + (selected ? " is-selected" : "");
|
|
1666
|
+
btn.setAttribute("role", "option");
|
|
1667
|
+
btn.setAttribute("aria-selected", selected ? "true" : "false");
|
|
1668
|
+
spec.render(btn, item, selected);
|
|
1669
|
+
btn.addEventListener("click", function () {
|
|
1670
|
+
close(true);
|
|
1671
|
+
spec.onPick(item);
|
|
1672
|
+
});
|
|
1673
|
+
menu.appendChild(btn);
|
|
1899
1674
|
});
|
|
1900
|
-
|
|
1901
|
-
|
|
1675
|
+
root.appendChild(menu);
|
|
1676
|
+
trigger.setAttribute("aria-expanded", "true");
|
|
1677
|
+
document.addEventListener("pointerdown", onOutside, true);
|
|
1678
|
+
var all = options();
|
|
1679
|
+
var first = menu.querySelector('[aria-selected="true"]') || all[0];
|
|
1680
|
+
if (first) first.focus();
|
|
1681
|
+
}
|
|
1682
|
+
trigger.addEventListener("click", function () {
|
|
1683
|
+
if (menu) close(true);
|
|
1684
|
+
else open();
|
|
1685
|
+
});
|
|
1686
|
+
root.addEventListener("keydown", function (e) {
|
|
1687
|
+
if (!menu) {
|
|
1688
|
+
if (e.target === trigger && (e.key === "ArrowDown" || e.key === "ArrowUp")) {
|
|
1689
|
+
e.preventDefault();
|
|
1690
|
+
open();
|
|
1691
|
+
}
|
|
1692
|
+
return;
|
|
1693
|
+
}
|
|
1694
|
+
if (e.key === "Escape") {
|
|
1695
|
+
e.preventDefault();
|
|
1696
|
+
e.stopPropagation();
|
|
1697
|
+
close(true);
|
|
1698
|
+
} else if (e.key === "Tab") {
|
|
1699
|
+
close(false);
|
|
1700
|
+
} else if (e.key === "ArrowDown" || e.key === "ArrowUp" || e.key === "Home" || e.key === "End") {
|
|
1701
|
+
e.preventDefault();
|
|
1702
|
+
var all = options();
|
|
1703
|
+
var at = all.indexOf(document.activeElement);
|
|
1704
|
+
var next = e.key === "Home" ? 0 : e.key === "End" ? all.length - 1 : e.key === "ArrowDown" ? Math.min(at + 1, all.length - 1) : Math.max(at - 1, 0);
|
|
1705
|
+
if (all[next]) all[next].focus();
|
|
1706
|
+
}
|
|
1902
1707
|
});
|
|
1903
|
-
return layer;
|
|
1904
1708
|
}
|
|
1905
1709
|
|
|
1906
|
-
|
|
1907
|
-
|
|
1908
|
-
|
|
1909
|
-
|
|
1910
|
-
|
|
1911
|
-
|
|
1912
|
-
|
|
1913
|
-
|
|
1914
|
-
|
|
1915
|
-
|
|
1916
|
-
|
|
1917
|
-
|
|
1918
|
-
|
|
1919
|
-
|
|
1920
|
-
|
|
1921
|
-
|
|
1922
|
-
|
|
1923
|
-
|
|
1924
|
-
|
|
1925
|
-
|
|
1926
|
-
|
|
1927
|
-
|
|
1928
|
-
|
|
1929
|
-
|
|
1930
|
-
|
|
1931
|
-
|
|
1932
|
-
|
|
1933
|
-
|
|
1934
|
-
|
|
1935
|
-
|
|
1936
|
-
|
|
1937
|
-
|
|
1938
|
-
|
|
1939
|
-
|
|
1940
|
-
|
|
1941
|
-
|
|
1942
|
-
);
|
|
1943
|
-
|
|
1944
|
-
|
|
1945
|
-
|
|
1946
|
-
|
|
1947
|
-
|
|
1948
|
-
|
|
1949
|
-
|
|
1950
|
-
|
|
1951
|
-
|
|
1952
|
-
|
|
1953
|
-
|
|
1954
|
-
|
|
1955
|
-
grid.setAttribute("aria-hidden", "true");
|
|
1956
|
-
frame.appendChild(grid);
|
|
1710
|
+
// ---------- the design picker (rail head, above the layout default) ----------
|
|
1711
|
+
// Every design cached on this machine, each shown as its name and accent
|
|
1712
|
+
// colours. A pick swaps the layout palette and moves every slide whose layout
|
|
1713
|
+
// the new design lacks onto its nearest equivalent (remapStoryboardLayouts);
|
|
1714
|
+
// the deck is rebound only when the answer is sent. Bundle strings reach
|
|
1715
|
+
// textContent only, and a colour must be a plain hex to reach a style.
|
|
1716
|
+
function designSwatches(design) {
|
|
1717
|
+
var dots = el("span", "plan-design-dots");
|
|
1718
|
+
dots.setAttribute("aria-hidden", "true");
|
|
1719
|
+
(design.accents || []).forEach(function (hex) {
|
|
1720
|
+
if (!/^#[0-9a-f]{6}$/i.test(String(hex))) return;
|
|
1721
|
+
var dot = el("i", "plan-design-dot");
|
|
1722
|
+
dot.style.background = hex;
|
|
1723
|
+
dots.appendChild(dot);
|
|
1724
|
+
});
|
|
1725
|
+
return dots;
|
|
1726
|
+
}
|
|
1727
|
+
function designLabel(node, design) {
|
|
1728
|
+
var name = el("span", "plan-design-name");
|
|
1729
|
+
name.textContent = design ? design.name : "Choose a design";
|
|
1730
|
+
node.appendChild(name);
|
|
1731
|
+
if (design && design.cached === false) node.appendChild(el("span", "plan-design-get", "")).textContent = "Download";
|
|
1732
|
+
else if (design) node.appendChild(designSwatches(design));
|
|
1733
|
+
}
|
|
1734
|
+
// The design picker stays a dropdown while the plan can change — even when
|
|
1735
|
+
// the one design on offer is already picked: the rest of the menu (every
|
|
1736
|
+
// design a connected backend serves) loads after the page does (loadDesigns),
|
|
1737
|
+
// and a design this machine has not downloaded yet is fetched when picked
|
|
1738
|
+
// (downloadDesign), then applied like any other.
|
|
1739
|
+
var designPickerEl = null; // the picker on screen, swapped when its state changes
|
|
1740
|
+
var designBusy = null; // the design being downloaded, null otherwise
|
|
1741
|
+
var designsAsked = false; // the backend's part of the menu was asked for
|
|
1742
|
+
function renderDesignPicker() {
|
|
1743
|
+
var options = designOptions();
|
|
1744
|
+
if (!options.length && !CFG.designsUrl) return null;
|
|
1745
|
+
var shown = shownDesign();
|
|
1746
|
+
var canPick = !locked() && !designBusy;
|
|
1747
|
+
var root = el("div", "plan-design-picker");
|
|
1748
|
+
var current = document.createElement("button");
|
|
1749
|
+
current.type = "button";
|
|
1750
|
+
current.className = "plan-design-current" + (designBusy ? " is-busy" : "");
|
|
1751
|
+
current.setAttribute("aria-label", "Choose the deck's design");
|
|
1752
|
+
current.disabled = !canPick;
|
|
1753
|
+
if (designBusy) current.appendChild(el("span", "plan-design-name", "")).textContent = "Downloading " + designBusy.name + "…";
|
|
1754
|
+
else designLabel(current, shown);
|
|
1755
|
+
if (canPick) {
|
|
1756
|
+
var chevron = el("span", "plan-storyboard-layout-chevron", CHEVRON);
|
|
1757
|
+
chevron.setAttribute("aria-hidden", "true");
|
|
1758
|
+
current.appendChild(chevron);
|
|
1957
1759
|
}
|
|
1958
|
-
|
|
1959
|
-
|
|
1960
|
-
if (
|
|
1961
|
-
|
|
1962
|
-
|
|
1963
|
-
|
|
1964
|
-
|
|
1965
|
-
|
|
1966
|
-
|
|
1967
|
-
|
|
1968
|
-
|
|
1969
|
-
|
|
1970
|
-
|
|
1971
|
-
|
|
1972
|
-
|
|
1760
|
+
root.appendChild(current);
|
|
1761
|
+
designPickerEl = root;
|
|
1762
|
+
if (!canPick) return root;
|
|
1763
|
+
attachMenu(root, current, {
|
|
1764
|
+
menuClass: "plan-design-menu",
|
|
1765
|
+
optionClass: "plan-design-option",
|
|
1766
|
+
items: options, // read when the menu opens: designs that arrive later are in it
|
|
1767
|
+
isSelected: function (design) {
|
|
1768
|
+
return design === shown;
|
|
1769
|
+
},
|
|
1770
|
+
render: function (node, design) {
|
|
1771
|
+
designLabel(node, design);
|
|
1772
|
+
// not on this computer yet: a quieter, smaller row under a divider
|
|
1773
|
+
if (design.cached === false) {
|
|
1774
|
+
node.classList.add("is-remote");
|
|
1775
|
+
var first = null;
|
|
1776
|
+
for (var i = 0; i < options.length; i++) {
|
|
1777
|
+
if (options[i].cached === false) {
|
|
1778
|
+
first = options[i];
|
|
1779
|
+
break;
|
|
1780
|
+
}
|
|
1781
|
+
}
|
|
1782
|
+
if (design === first) node.classList.add("is-first-remote");
|
|
1973
1783
|
}
|
|
1974
|
-
|
|
1784
|
+
},
|
|
1785
|
+
onPick: function (design) {
|
|
1786
|
+
if (design.cached === false) downloadDesign(design);
|
|
1787
|
+
else pickDesign(design.presFP);
|
|
1788
|
+
},
|
|
1789
|
+
});
|
|
1790
|
+
return root;
|
|
1791
|
+
}
|
|
1792
|
+
function refreshDesignPicker() {
|
|
1793
|
+
var old = designPickerEl;
|
|
1794
|
+
if (!old || !old.parentNode) return;
|
|
1795
|
+
var next = renderDesignPicker();
|
|
1796
|
+
if (next) old.parentNode.replaceChild(next, old);
|
|
1797
|
+
}
|
|
1798
|
+
// Once per page: the designs a connected backend serves that this machine has
|
|
1799
|
+
// not downloaded. A backend that does not answer leaves the menu as it is.
|
|
1800
|
+
function loadDesigns() {
|
|
1801
|
+
if (!CFG.designsUrl || designsAsked) return;
|
|
1802
|
+
designsAsked = true;
|
|
1803
|
+
if (!Array.isArray(CFG.designs)) CFG.designs = [];
|
|
1804
|
+
fetch(CFG.designsUrl, { credentials: "same-origin" })
|
|
1805
|
+
.then(function (r) {
|
|
1806
|
+
return r.ok ? r.json() : null;
|
|
1807
|
+
})
|
|
1808
|
+
.then(function (j) {
|
|
1809
|
+
if (!j || !Array.isArray(j.designs)) return;
|
|
1810
|
+
var all = designOptions();
|
|
1811
|
+
var have = {};
|
|
1812
|
+
all.forEach(function (d) {
|
|
1813
|
+
have[d.presFP] = true;
|
|
1814
|
+
});
|
|
1815
|
+
var added = 0;
|
|
1816
|
+
j.designs.forEach(function (d) {
|
|
1817
|
+
if (d && typeof d.presFP === "string" && typeof d.name === "string" && !have[d.presFP]) {
|
|
1818
|
+
all.push({ presFP: d.presFP, name: d.name, accents: [], current: false, cached: false });
|
|
1819
|
+
added++;
|
|
1820
|
+
}
|
|
1821
|
+
});
|
|
1822
|
+
if (!added) return;
|
|
1823
|
+
// what this machine has first, what would still have to be downloaded last
|
|
1824
|
+
all.sort(function (a, b) {
|
|
1825
|
+
var ra = a.cached === false ? 1 : 0;
|
|
1826
|
+
var rb = b.cached === false ? 1 : 0;
|
|
1827
|
+
return ra - rb || String(a.name).localeCompare(String(b.name));
|
|
1828
|
+
});
|
|
1829
|
+
refreshDesignPicker();
|
|
1830
|
+
})
|
|
1831
|
+
.catch(function () {
|
|
1832
|
+
/* the menu stays at what this machine has */
|
|
1975
1833
|
});
|
|
1976
|
-
|
|
1977
|
-
|
|
1978
|
-
|
|
1979
|
-
|
|
1980
|
-
|
|
1981
|
-
|
|
1982
|
-
|
|
1983
|
-
|
|
1984
|
-
|
|
1985
|
-
|
|
1986
|
-
|
|
1987
|
-
|
|
1988
|
-
|
|
1989
|
-
|
|
1834
|
+
}
|
|
1835
|
+
// A picked design this machine does not have yet: the workspace downloads it
|
|
1836
|
+
// (POST, carrying the gate token like the answer does), and its entry —
|
|
1837
|
+
// palette, layouts — replaces the bare one before it is applied.
|
|
1838
|
+
function downloadDesign(design) {
|
|
1839
|
+
if (designBusy || locked() || !CFG.designsUrl) return;
|
|
1840
|
+
designBusy = design;
|
|
1841
|
+
SHELL.clearNotice("design");
|
|
1842
|
+
refreshDesignPicker();
|
|
1843
|
+
var url = CFG.designsUrl + "/" + encodeURIComponent(design.presFP) + "?t=" + encodeURIComponent(CFG.designsGate || "");
|
|
1844
|
+
fetch(url, { method: "POST", credentials: "same-origin", headers: { "Content-Type": "application/json" }, body: "{}" })
|
|
1845
|
+
.then(function (r) {
|
|
1846
|
+
return r
|
|
1847
|
+
.json()
|
|
1848
|
+
.catch(function () {
|
|
1849
|
+
return null;
|
|
1990
1850
|
})
|
|
1991
|
-
|
|
1992
|
-
|
|
1993
|
-
|
|
1994
|
-
|
|
1995
|
-
|
|
1996
|
-
|
|
1997
|
-
|
|
1998
|
-
|
|
1999
|
-
|
|
2000
|
-
|
|
2001
|
-
|
|
2002
|
-
|
|
2003
|
-
|
|
2004
|
-
|
|
2005
|
-
|
|
2006
|
-
|
|
2007
|
-
]);
|
|
2008
|
-
commitStoryboard(segment, withBlocks(data, nextBlocks));
|
|
1851
|
+
.then(function (j) {
|
|
1852
|
+
return r.ok && j && j.design && j.design.presFP === design.presFP ? j.design : null;
|
|
1853
|
+
});
|
|
1854
|
+
})
|
|
1855
|
+
.then(function (fresh) {
|
|
1856
|
+
designBusy = null;
|
|
1857
|
+
if (!fresh) throw new Error("no design");
|
|
1858
|
+
var all = designOptions();
|
|
1859
|
+
for (var i = 0; i < all.length; i++) if (all[i].presFP === fresh.presFP) all[i] = fresh;
|
|
1860
|
+
pickDesign(fresh.presFP); // re-renders the surface, this picker with it
|
|
1861
|
+
refreshDesignPicker(); // …or not, when the pick changed nothing
|
|
1862
|
+
})
|
|
1863
|
+
.catch(function () {
|
|
1864
|
+
designBusy = null;
|
|
1865
|
+
refreshDesignPicker();
|
|
1866
|
+
SHELL.showNotice("design", { tone: "warn", title: "That design could not be downloaded", text: "Check your connection, then pick it again.", dismiss: true });
|
|
2009
1867
|
});
|
|
2010
|
-
|
|
1868
|
+
}
|
|
1869
|
+
function pickDesign(presFP) {
|
|
1870
|
+
if (locked()) return;
|
|
1871
|
+
var target = null;
|
|
1872
|
+
designOptions().forEach(function (d) {
|
|
1873
|
+
if (d.presFP === presFP) target = d;
|
|
1874
|
+
});
|
|
1875
|
+
if (!target || target === shownDesign()) return;
|
|
1876
|
+
if (!designUndo || S.value !== designUndo.after) designUndo = { before: S.value, from: layoutOptions() };
|
|
1877
|
+
S.design = target.current ? null : presFP;
|
|
1878
|
+
var next = PLAN.remapStoryboardLayouts(designUndo.before, layoutOptions(), designUndo.from);
|
|
1879
|
+
designUndo.after = next;
|
|
1880
|
+
if (next === S.value) {
|
|
1881
|
+
saveEdits(); // the layouts all exist in both designs: only the pick changed
|
|
1882
|
+
refreshChrome();
|
|
1883
|
+
renderSurface();
|
|
1884
|
+
} else {
|
|
1885
|
+
commitPlan(next);
|
|
2011
1886
|
}
|
|
2012
|
-
|
|
2013
|
-
|
|
1887
|
+
}
|
|
2014
1888
|
|
|
2015
|
-
|
|
2016
|
-
|
|
2017
|
-
|
|
2018
|
-
|
|
1889
|
+
function commitPlan(nextValue) {
|
|
1890
|
+
if (locked()) return;
|
|
1891
|
+
S.value = nextValue;
|
|
1892
|
+
saveEdits();
|
|
1893
|
+
refreshChrome();
|
|
1894
|
+
renderSurface();
|
|
1895
|
+
}
|
|
1896
|
+
function commitStoryboard(segment, nextData) {
|
|
1897
|
+
commitPlan(PLAN.replaceStoryboardSegment(S.value, segment, nextData));
|
|
1898
|
+
}
|
|
2019
1899
|
|
|
2020
|
-
|
|
2021
|
-
|
|
2022
|
-
|
|
1900
|
+
function renderDetailField(className, ariaLabel, value, multiline, onCommit) {
|
|
1901
|
+
var field = el("div", className);
|
|
1902
|
+
field.setAttribute("role", "textbox");
|
|
1903
|
+
field.setAttribute("aria-label", ariaLabel);
|
|
1904
|
+
if (multiline) field.setAttribute("aria-multiline", "true");
|
|
1905
|
+
field.textContent = value;
|
|
1906
|
+
if (!locked()) {
|
|
1907
|
+
field.contentEditable = "true";
|
|
1908
|
+
field.spellcheck = true;
|
|
2023
1909
|
}
|
|
2024
|
-
|
|
2025
|
-
|
|
2026
|
-
|
|
2027
|
-
|
|
2028
|
-
|
|
2029
|
-
|
|
2030
|
-
|
|
2031
|
-
|
|
2032
|
-
|
|
2033
|
-
|
|
2034
|
-
"No content area on this layout — pick a layout with a content body";
|
|
2035
|
-
noticeLayer.appendChild(notice);
|
|
2036
|
-
frame.appendChild(noticeLayer);
|
|
1910
|
+
// Commit-once guard: a commit re-renders the surface and detaches this
|
|
1911
|
+
// focused field, firing a second blur \u2014 which must not re-splice the stale
|
|
1912
|
+
// segment (double-commit \u2192 corrupted fences on the single-line beat field).
|
|
1913
|
+
var committed = false;
|
|
1914
|
+
function commit() {
|
|
1915
|
+
if (committed) return;
|
|
1916
|
+
committed = true;
|
|
1917
|
+
var normalized = (field.textContent || "").replace(/\u00a0/g, " ").trim();
|
|
1918
|
+
field.textContent = normalized;
|
|
1919
|
+
if (normalized !== value) onCommit(normalized);
|
|
2037
1920
|
}
|
|
2038
|
-
|
|
2039
|
-
|
|
2040
|
-
|
|
2041
|
-
|
|
2042
|
-
|
|
2043
|
-
|
|
2044
|
-
|
|
2045
|
-
segment.warnings.forEach(function (w) {
|
|
2046
|
-
var li = document.createElement("li");
|
|
2047
|
-
li.textContent = w;
|
|
2048
|
-
ul.appendChild(li);
|
|
1921
|
+
field.addEventListener("blur", commit);
|
|
1922
|
+
if (!multiline) {
|
|
1923
|
+
// Enter blurs; the blur handler is the single committer.
|
|
1924
|
+
field.addEventListener("keydown", function (e) {
|
|
1925
|
+
if (e.key !== "Enter") return;
|
|
1926
|
+
e.preventDefault();
|
|
1927
|
+
field.blur();
|
|
2049
1928
|
});
|
|
2050
|
-
figure.appendChild(ul);
|
|
2051
1929
|
}
|
|
2052
|
-
return
|
|
1930
|
+
return field;
|
|
1931
|
+
}
|
|
1932
|
+
|
|
1933
|
+
// The planned layout, when the record names one the bundle has: its chrome
|
|
1934
|
+
// render grounds the stage and its content region places the slide card.
|
|
1935
|
+
function selectedPlanLayout(data) {
|
|
1936
|
+
var options = layoutOptions();
|
|
1937
|
+
if (!data.layout) return null;
|
|
1938
|
+
for (var i = 0; i < options.length; i++) {
|
|
1939
|
+
if (options[i].componentName === data.layout) return options[i];
|
|
1940
|
+
}
|
|
1941
|
+
return null;
|
|
1942
|
+
}
|
|
1943
|
+
|
|
1944
|
+
function pctOf(v) {
|
|
1945
|
+
return (v * 100).toFixed(2) + "%";
|
|
2053
1946
|
}
|
|
2054
1947
|
|
|
2055
1948
|
// ---------- comment ledger ----------
|
|
2056
1949
|
// The anchor a comment written right now would carry. "This slide" reads the
|
|
2057
|
-
// SELECTED
|
|
2058
|
-
//
|
|
1950
|
+
// SELECTED rail card; without storyboards there is nothing to anchor to, so
|
|
1951
|
+
// everything is whole-story.
|
|
2059
1952
|
function currentAnchor() {
|
|
2060
1953
|
var beats = planBeats();
|
|
2061
1954
|
if (!beats.length || commentScope !== "slide") {
|
|
@@ -2071,7 +1964,7 @@
|
|
|
2071
1964
|
return "c-" + Date.now() + "-" + Math.random().toString(16).slice(2);
|
|
2072
1965
|
}
|
|
2073
1966
|
}
|
|
2074
|
-
function newComment(text, forceScope) {
|
|
1967
|
+
function newComment(text, forceScope, status) {
|
|
2075
1968
|
var a = forceScope === "story" ? { scope: "story", slideIndex: null, slideHeading: null } : currentAnchor();
|
|
2076
1969
|
return {
|
|
2077
1970
|
id: newId(),
|
|
@@ -2081,74 +1974,93 @@
|
|
|
2081
1974
|
slideHeading: a.slideHeading,
|
|
2082
1975
|
text: text,
|
|
2083
1976
|
at: new Date().toISOString(),
|
|
2084
|
-
status: "sent",
|
|
1977
|
+
status: status || "sent",
|
|
2085
1978
|
};
|
|
2086
1979
|
}
|
|
1980
|
+
// Comments queue up as drafts (Enter); ONE send carries the batch — the
|
|
1981
|
+
// reviewer reads the whole plan, notes everything, then answers once (every
|
|
1982
|
+
// answer is one paste to the agent, so one batch is one round trip).
|
|
1983
|
+
function draftComments() {
|
|
1984
|
+
return ledger.filter(function (c) {
|
|
1985
|
+
return c.status === "draft";
|
|
1986
|
+
});
|
|
1987
|
+
}
|
|
1988
|
+
function addDraft() {
|
|
1989
|
+
if (working) return;
|
|
1990
|
+
var typed = commentText();
|
|
1991
|
+
if (!typed) return;
|
|
1992
|
+
ledger.push(newComment(typed, null, "draft"));
|
|
1993
|
+
feedbackText = "";
|
|
1994
|
+
if (commentEl) commentEl.value = "";
|
|
1995
|
+
saveLedger();
|
|
1996
|
+
saveCtx();
|
|
1997
|
+
renderComposer();
|
|
1998
|
+
if (commentEl) commentEl.focus();
|
|
1999
|
+
}
|
|
2000
|
+
function deleteDraft(c) {
|
|
2001
|
+
var i = ledger.indexOf(c);
|
|
2002
|
+
if (i >= 0) ledger.splice(i, 1);
|
|
2003
|
+
saveLedger();
|
|
2004
|
+
renderComposer();
|
|
2005
|
+
}
|
|
2087
2006
|
function anchorLabel(c) {
|
|
2088
2007
|
if (c.scope !== "slide") return "Whole story";
|
|
2089
2008
|
return "Slide " + (c.slideIndex == null ? "?" : c.slideIndex);
|
|
2090
2009
|
}
|
|
2091
|
-
|
|
2092
|
-
|
|
2093
|
-
|
|
2094
|
-
|
|
2095
|
-
|
|
2096
|
-
|
|
2097
|
-
|
|
2098
|
-
|
|
2099
|
-
|
|
2100
|
-
|
|
2101
|
-
|
|
2102
|
-
|
|
2103
|
-
|
|
2104
|
-
|
|
2105
|
-
|
|
2106
|
-
|
|
2107
|
-
|
|
2108
|
-
|
|
2109
|
-
|
|
2010
|
+
// The comments not sent yet — the only ones the composer shows. Sent and
|
|
2011
|
+
// worked-in comments stay in the ledger (the next round marks them) but not
|
|
2012
|
+
// on screen: what the reviewer needs here is what this answer will carry.
|
|
2013
|
+
function draftList(drafts) {
|
|
2014
|
+
var list = el("ul", "sm-drafts");
|
|
2015
|
+
drafts.forEach(function (c) {
|
|
2016
|
+
var row = el("li", "sm-draft");
|
|
2017
|
+
row.appendChild(el("span", "sm-draft-anchor", "")).textContent = anchorLabel(c);
|
|
2018
|
+
var text = row.appendChild(el("span", "sm-draft-text", ""));
|
|
2019
|
+
text.textContent = c.text;
|
|
2020
|
+
text.title = c.text;
|
|
2021
|
+
if (!SHELL.inFlight(c.id)) {
|
|
2022
|
+
var del = el("button", "sm-draft-del", ICONS.close);
|
|
2023
|
+
del.type = "button";
|
|
2024
|
+
del.title = "Remove this comment";
|
|
2025
|
+
del.setAttribute("aria-label", "Remove this comment");
|
|
2026
|
+
del.onclick = function () {
|
|
2027
|
+
deleteDraft(c);
|
|
2028
|
+
};
|
|
2029
|
+
row.appendChild(del);
|
|
2030
|
+
}
|
|
2031
|
+
list.appendChild(row);
|
|
2110
2032
|
});
|
|
2111
|
-
return
|
|
2112
|
-
}
|
|
2113
|
-
|
|
2114
|
-
// ----------
|
|
2115
|
-
//
|
|
2116
|
-
//
|
|
2117
|
-
//
|
|
2033
|
+
return list;
|
|
2034
|
+
}
|
|
2035
|
+
|
|
2036
|
+
// ---------- the composer ----------
|
|
2037
|
+
// One row: what the comment is about, the comment, Send (only once there is
|
|
2038
|
+
// something to send) and Approve (off while there is). Rebuilt on structural
|
|
2039
|
+
// changes (a draft added / removed, a storyline staged, the selected slide),
|
|
2040
|
+
// NOT on every keystroke (keystrokes only call refreshChrome); the typed text
|
|
2041
|
+
// survives a rebuild via feedbackText.
|
|
2042
|
+
//
|
|
2043
|
+
// While the agent has the next move (WORKING) the composer is gone: the
|
|
2044
|
+
// answer is saved, the hand-off dialog said what to do with it, and the bar's
|
|
2045
|
+
// hint says commenting opens again — nothing to type into, nothing greyed out.
|
|
2118
2046
|
function renderComposer() {
|
|
2119
2047
|
if (!composerEl) return;
|
|
2120
2048
|
composerEl.innerHTML = "";
|
|
2121
|
-
|
|
2049
|
+
composerEl.hidden = working;
|
|
2122
2050
|
if (working) {
|
|
2123
|
-
|
|
2124
|
-
|
|
2125
|
-
|
|
2126
|
-
note.innerHTML =
|
|
2127
|
-
"<b>" +
|
|
2128
|
-
(workingNote || "Comments sent.") +
|
|
2129
|
-
"</b> The agent is revising the plan — this tab refreshes when the next " +
|
|
2130
|
-
"round is ready. Keep reading and navigating; comment again on that round.";
|
|
2131
|
-
status.appendChild(note);
|
|
2132
|
-
composerEl.appendChild(status);
|
|
2133
|
-
// The permission prompt was raised at the send click (a user gesture, the
|
|
2134
|
-
// only context a browser shows it in); once granted, say so.
|
|
2135
|
-
(notifyPermPromise || Promise.resolve(SES.notifyPermission())).then(function (perm) {
|
|
2136
|
-
if (perm === "granted") {
|
|
2137
|
-
note.innerHTML += " You'll get a notification when the next round needs you.";
|
|
2138
|
-
}
|
|
2139
|
-
});
|
|
2051
|
+
commentEl = sendBtn = approveBtn = null;
|
|
2052
|
+
refreshChrome();
|
|
2053
|
+
return;
|
|
2140
2054
|
}
|
|
2141
2055
|
|
|
2142
|
-
if (
|
|
2056
|
+
if (S.storyDraft) {
|
|
2143
2057
|
var banner = el("div", "sm-story-banner");
|
|
2144
|
-
banner.innerHTML =
|
|
2145
|
-
ICONS.sparkles +
|
|
2146
|
-
'<span class="txt"><b>Storyline edited.</b> Sending asks the agent to build storyboards and content for your new and renamed slides — add comments below if you like.</span>';
|
|
2058
|
+
banner.innerHTML = ICONS.sparkles + '<span class="txt"><b>Storyline changed.</b> Send it and your agent fills in the new and renamed slides.</span>';
|
|
2147
2059
|
var discard = el("button", "sm-story-discard", "Discard changes");
|
|
2148
2060
|
discard.type = "button";
|
|
2149
2061
|
discard.onclick = function () {
|
|
2150
2062
|
S.storyDraft = null;
|
|
2151
|
-
|
|
2063
|
+
saveEdits();
|
|
2152
2064
|
refreshChrome();
|
|
2153
2065
|
renderComposer();
|
|
2154
2066
|
renderSurface();
|
|
@@ -2157,88 +2069,92 @@
|
|
|
2157
2069
|
composerEl.appendChild(banner);
|
|
2158
2070
|
}
|
|
2159
2071
|
|
|
2160
|
-
|
|
2072
|
+
var drafts = draftComments();
|
|
2073
|
+
if (drafts.length) composerEl.appendChild(draftList(drafts));
|
|
2074
|
+
|
|
2075
|
+
var row = el("div", "sm-composer-row");
|
|
2076
|
+
var box = el("div", "sm-chat-box");
|
|
2161
2077
|
|
|
2162
|
-
//
|
|
2163
|
-
//
|
|
2078
|
+
// What the comment is about: the slide on stage, or the whole story. A plan
|
|
2079
|
+
// with no slides to anchor to is whole-story only, and shows no choice.
|
|
2164
2080
|
var beats = planBeats();
|
|
2165
2081
|
if (!beats.length) commentScope = "story";
|
|
2166
2082
|
if (beats.length) {
|
|
2167
|
-
var meta = el("div", "sm-composer-meta");
|
|
2168
|
-
meta.appendChild(el("span", "sm-scope-label", "Comment on"));
|
|
2169
|
-
var scopeBox = el("div", "sm-scope");
|
|
2170
2083
|
var active = clamp(S.selectedIndex, 0, beats.length - 1);
|
|
2171
|
-
[
|
|
2172
|
-
{ key: "slide", label: "Slide " + beats[active].slide },
|
|
2173
|
-
{ key: "story", label: "Whole story" },
|
|
2174
|
-
]
|
|
2175
|
-
|
|
2176
|
-
|
|
2177
|
-
|
|
2178
|
-
|
|
2179
|
-
|
|
2084
|
+
var targets = [
|
|
2085
|
+
{ key: "slide", label: "Slide " + beats[active].slide, detail: beats[active].message },
|
|
2086
|
+
{ key: "story", label: "Whole story", detail: "The storyline as a whole" },
|
|
2087
|
+
];
|
|
2088
|
+
var wrap = el("div", "sm-target-wrap");
|
|
2089
|
+
var target = el("button", "sm-target");
|
|
2090
|
+
target.type = "button";
|
|
2091
|
+
target.setAttribute("aria-label", "What your comment is about");
|
|
2092
|
+
target.title = "What your comment is about";
|
|
2093
|
+
var targetLabel = el("span", "sm-target-label");
|
|
2094
|
+
targetLabel.textContent = targets[commentScope === "story" ? 1 : 0].label;
|
|
2095
|
+
target.appendChild(targetLabel);
|
|
2096
|
+
target.appendChild(el("span", "sm-target-chevron", CHEVRON));
|
|
2097
|
+
wrap.appendChild(target);
|
|
2098
|
+
attachMenu(wrap, target, {
|
|
2099
|
+
menuClass: "sm-target-menu",
|
|
2100
|
+
optionClass: "sm-target-option",
|
|
2101
|
+
items: targets,
|
|
2102
|
+
isSelected: function (t) {
|
|
2103
|
+
return t.key === commentScope;
|
|
2104
|
+
},
|
|
2105
|
+
render: function (node, t, selected) {
|
|
2106
|
+
var text = el("span", "sm-target-text");
|
|
2107
|
+
text.appendChild(el("span", "sm-target-option-label")).textContent = t.label;
|
|
2108
|
+
text.appendChild(el("span", "sm-target-option-detail")).textContent = t.detail;
|
|
2109
|
+
node.appendChild(text);
|
|
2110
|
+
node.appendChild(el("span", "sm-target-check", selected ? ICONS.check : ""));
|
|
2111
|
+
},
|
|
2112
|
+
onPick: function (t) {
|
|
2113
|
+
commentScope = t.key;
|
|
2114
|
+
targetLabel.textContent = t.label;
|
|
2180
2115
|
if (commentEl) commentEl.focus();
|
|
2181
|
-
}
|
|
2182
|
-
scopeBox.appendChild(b);
|
|
2116
|
+
},
|
|
2183
2117
|
});
|
|
2184
|
-
|
|
2185
|
-
composerEl.appendChild(meta);
|
|
2118
|
+
box.appendChild(wrap);
|
|
2186
2119
|
}
|
|
2187
2120
|
|
|
2188
|
-
var row = el("div", "sm-composer-row");
|
|
2189
2121
|
commentEl = el("textarea", "sm-chat-ta");
|
|
2190
2122
|
commentEl.rows = 1;
|
|
2191
|
-
commentEl.placeholder = working
|
|
2192
|
-
? "The agent is revising — comment on the next round."
|
|
2193
|
-
: "What should change? Leave a note, or edit the plan directly.";
|
|
2194
|
-
commentEl.disabled = working;
|
|
2195
2123
|
commentEl.value = feedbackText;
|
|
2124
|
+
commentEl.placeholder = drafts.length ? "Add another comment…" : "What should change? Press Enter to add it.";
|
|
2125
|
+
commentEl.setAttribute("aria-label", "Your comment");
|
|
2196
2126
|
commentEl.addEventListener("input", function () {
|
|
2197
2127
|
feedbackText = commentEl.value;
|
|
2198
|
-
approveArmed = false; // typing more feedback disarms a pending approve-confirm
|
|
2199
2128
|
commentEl.style.height = "auto";
|
|
2200
2129
|
commentEl.style.height = Math.min(commentEl.scrollHeight, 150) + "px";
|
|
2201
2130
|
saveDraftSoon(); // an unsent draft survives the next round's reload
|
|
2202
2131
|
refreshChrome();
|
|
2203
2132
|
});
|
|
2204
2133
|
commentEl.addEventListener("keydown", function (e) {
|
|
2205
|
-
if ((e.metaKey || e.ctrlKey) && e.key === "Enter") requestChanges();
|
|
2134
|
+
if ((e.metaKey || e.ctrlKey) && e.key === "Enter") return requestChanges();
|
|
2135
|
+
if (e.key === "Enter" && !e.shiftKey) {
|
|
2136
|
+
e.preventDefault();
|
|
2137
|
+
addDraft();
|
|
2138
|
+
}
|
|
2206
2139
|
});
|
|
2207
|
-
|
|
2140
|
+
box.appendChild(commentEl);
|
|
2141
|
+
row.appendChild(box);
|
|
2208
2142
|
|
|
2209
|
-
|
|
2210
|
-
|
|
2211
|
-
|
|
2212
|
-
sendBtn.type = "button";
|
|
2213
|
-
sendBtn.title = "Iterate on the story";
|
|
2214
|
-
} else {
|
|
2215
|
-
sendBtn = el("button", "sm-send", ICONS.send);
|
|
2216
|
-
sendBtn.type = "button";
|
|
2217
|
-
sendBtn.title = working ? "The agent is revising — send on the next round" : "Send feedback";
|
|
2218
|
-
sendBtn.setAttribute("aria-label", sendBtn.title);
|
|
2219
|
-
sendBtn.disabled = working;
|
|
2220
|
-
}
|
|
2143
|
+
sendBtn = el("button", "sm-send-pill", (S.storyDraft ? ICONS.sparkles : ICONS.send) + '<span class="n">Send</span>');
|
|
2144
|
+
sendBtn.type = "button";
|
|
2145
|
+
sendBtn.title = S.storyDraft ? "Send your storyline changes and comments" : "Send your comments (⌘/Ctrl+Enter)";
|
|
2221
2146
|
sendBtn.onclick = requestChanges;
|
|
2222
2147
|
row.appendChild(sendBtn);
|
|
2223
2148
|
|
|
2224
|
-
|
|
2225
|
-
|
|
2226
|
-
approveBtn = el("button", "sm-approve");
|
|
2149
|
+
approveBtn = el("button", "sm-approve", ICONS.check + '<span class="n">Approve plan</span>');
|
|
2227
2150
|
approveBtn.type = "button";
|
|
2228
|
-
approveBtn.innerHTML =
|
|
2229
|
-
'<span class="main">' +
|
|
2230
|
-
ICONS.check +
|
|
2231
|
-
"<span>Approve plan</span></span>" +
|
|
2232
|
-
'<span class="sub">the agent builds the slides next</span>';
|
|
2233
2151
|
approveBtn.onclick = function () {
|
|
2234
2152
|
if (working) return; // superseded round
|
|
2153
|
+
if (SHELL.decisionPending()) return; // one answer at a time
|
|
2154
|
+
if (SHELL.alreadyApproved()) return; // already approved and nobody asks again
|
|
2155
|
+
if (SHELL.notAskedYet()) return; // the agent is still working on it
|
|
2235
2156
|
if (S.storyDraft) return; // blocked while a storyline draft is staged
|
|
2236
|
-
|
|
2237
|
-
if (commentText() && !approveArmed) {
|
|
2238
|
-
approveArmed = true;
|
|
2239
|
-
refreshChrome();
|
|
2240
|
-
return;
|
|
2241
|
-
}
|
|
2157
|
+
if (draftComments().length || commentText()) return; // comments go out or away first
|
|
2242
2158
|
submit("approve");
|
|
2243
2159
|
};
|
|
2244
2160
|
row.appendChild(approveBtn);
|
|
@@ -2249,7 +2165,7 @@
|
|
|
2249
2165
|
|
|
2250
2166
|
// ---------- working mode (the agent is revising) ----------
|
|
2251
2167
|
// The tab neither veils over nor freezes: the plan stays readable and the
|
|
2252
|
-
//
|
|
2168
|
+
// rail stays navigable; the composer, the editing affordances and Approve go
|
|
2253
2169
|
// away until the next round. Heartbeat polling waits for the agent's next
|
|
2254
2170
|
// `slidemind plan` round, which reloads this same tab.
|
|
2255
2171
|
function enterWorking(note) {
|
|
@@ -2264,97 +2180,85 @@
|
|
|
2264
2180
|
}
|
|
2265
2181
|
|
|
2266
2182
|
// ---------- submit ----------
|
|
2267
|
-
// Send: resolves the gate right now with the
|
|
2183
|
+
// Send: resolves the gate right now with the listed comments (plus a staged
|
|
2268
2184
|
// storyline restructure as a whole-story instruction). Off while WORKING.
|
|
2269
2185
|
function requestChanges() {
|
|
2270
2186
|
if (working) return;
|
|
2271
|
-
|
|
2187
|
+
if (SHELL.decisionPending()) return; // the answer on its way carries no late additions
|
|
2188
|
+
addDraft(); // whatever is still typed goes with the batch
|
|
2189
|
+
var drafts = draftComments();
|
|
2272
2190
|
var batch = [];
|
|
2273
2191
|
if (S.storyDraft) {
|
|
2274
2192
|
var staged = PLAN.stageStoryIteration(S.value, S.storyDraft);
|
|
2275
2193
|
if (!staged) return;
|
|
2276
2194
|
// A storyline restructure is a whole-story instruction by construction.
|
|
2277
|
-
batch =
|
|
2278
|
-
newComment(typed ? staged.feedback + "\n\n" + typed : staged.feedback, "story"),
|
|
2279
|
-
]);
|
|
2195
|
+
batch = [newComment(staged.feedback, "story")].concat(drafts);
|
|
2280
2196
|
pendingEdited = staged.editedPlanMarkdown;
|
|
2281
2197
|
} else {
|
|
2282
|
-
if (!
|
|
2283
|
-
batch =
|
|
2198
|
+
if (!drafts.length) return;
|
|
2199
|
+
batch = drafts;
|
|
2284
2200
|
pendingEdited = planWasEdited() ? S.value : null;
|
|
2285
2201
|
}
|
|
2286
2202
|
pendingComments = batch;
|
|
2287
2203
|
// Ask for notification permission HERE — the click is a user gesture, the
|
|
2288
|
-
// only context where the browser will actually show the prompt.
|
|
2289
|
-
|
|
2204
|
+
// only context where the browser will actually show the prompt. Standalone
|
|
2205
|
+
// only: in the workspace a permission prompt is friction on the click that
|
|
2206
|
+
// copies the message, and nothing waits in the background there.
|
|
2207
|
+
if (!CFG.embedded) SES.requestNotifyPermission();
|
|
2290
2208
|
submit("comments");
|
|
2291
2209
|
}
|
|
2292
|
-
|
|
2293
|
-
|
|
2294
|
-
|
|
2295
|
-
saveCtx();
|
|
2296
|
-
}
|
|
2210
|
+
// One answer at a time (SHELL.decide holds the in-flight latch and carries
|
|
2211
|
+
// the operationId + the plan version it answers). Nothing here changes until
|
|
2212
|
+
// the server SAVED it: decided() below is the success path.
|
|
2297
2213
|
function submit(decision) {
|
|
2298
|
-
if (
|
|
2299
|
-
|
|
2300
|
-
var payload = {
|
|
2214
|
+
if (SHELL.decisionPending()) return;
|
|
2215
|
+
SHELL.decide({
|
|
2301
2216
|
decision: decision,
|
|
2302
2217
|
comments: decision === "comments" ? pendingComments.map(SES.wire) : [],
|
|
2303
2218
|
editedPlan: decision === "comments" ? pendingEdited : planWasEdited() ? S.value : null,
|
|
2304
|
-
|
|
2305
|
-
|
|
2306
|
-
|
|
2307
|
-
headers: { "Content-Type": "application/json" },
|
|
2308
|
-
body: JSON.stringify(payload),
|
|
2309
|
-
})
|
|
2310
|
-
.then(function (res) {
|
|
2311
|
-
if (res.status === 409) {
|
|
2312
|
-
// Another tab already decided this run — this round is stale. The
|
|
2313
|
-
// typed text stays in the input (restored on the next round's reload)
|
|
2314
|
-
// so it isn't lost; disarm the submit latch — leaving it armed made
|
|
2315
|
-
// every later send a silent no-op. A staged story restructure is
|
|
2316
|
-
// dropped deliberately: its editedPlanMarkdown was computed against
|
|
2317
|
-
// the superseded plan and applying it to the next round would corrupt
|
|
2318
|
-
// it (same call the add-in dialog makes on round arrival).
|
|
2319
|
-
S.storyDraft = null;
|
|
2320
|
-
submitted = false;
|
|
2321
|
-
enterWorking("Round superseded.");
|
|
2322
|
-
return;
|
|
2323
|
-
}
|
|
2324
|
-
if (!res.ok) throw new Error("gate refused the decision");
|
|
2325
|
-
if (decision === "approve") {
|
|
2326
|
-
showApproved();
|
|
2327
|
-
return;
|
|
2328
|
-
}
|
|
2329
|
-
pendingComments.forEach(function (c) {
|
|
2330
|
-
c.status = "sent";
|
|
2331
|
-
if (ledger.indexOf(c) < 0) ledger.push(c);
|
|
2332
|
-
});
|
|
2333
|
-
saveLedger();
|
|
2334
|
-
clearDraft();
|
|
2335
|
-
// The staged restructure is now the agent's job — keeping it staged
|
|
2336
|
-
// would show the reviewer a storyline the served plan no longer claims.
|
|
2337
|
-
S.storyDraft = null;
|
|
2338
|
-
enterWorking("Comments sent.");
|
|
2339
|
-
})
|
|
2340
|
-
.catch(function () {
|
|
2341
|
-
submitted = false;
|
|
2342
|
-
alert("Could not reach the slidemind plan server — is the terminal command still running?");
|
|
2343
|
-
});
|
|
2219
|
+
design: S.design,
|
|
2220
|
+
});
|
|
2221
|
+
refreshChrome();
|
|
2344
2222
|
}
|
|
2345
|
-
|
|
2346
|
-
|
|
2347
|
-
|
|
2348
|
-
|
|
2349
|
-
|
|
2350
|
-
|
|
2351
|
-
|
|
2352
|
-
|
|
2353
|
-
|
|
2223
|
+
// The server saved the answer. The rows it carried are sent — read off the
|
|
2224
|
+
// request itself, so an answer re-sent after a reload marks the right rows —
|
|
2225
|
+
// and the edits that rode with it are the server's now.
|
|
2226
|
+
function decided(decision, body) {
|
|
2227
|
+
baseline = S.value;
|
|
2228
|
+
if (S.design) {
|
|
2229
|
+
// the server rebound the deck: the picked design is its own from here on
|
|
2230
|
+
designOptions().forEach(function (d) {
|
|
2231
|
+
if (d.current) d.layouts = boundLayouts;
|
|
2232
|
+
d.current = d.presFP === S.design;
|
|
2233
|
+
if (d.current) boundLayouts = d.layouts || [];
|
|
2234
|
+
});
|
|
2235
|
+
S.design = null;
|
|
2236
|
+
}
|
|
2237
|
+
designUndo = null;
|
|
2238
|
+
SES.write(EDITS_KEY, null);
|
|
2239
|
+
if (decision === "approve") {
|
|
2240
|
+
showApproved();
|
|
2241
|
+
return;
|
|
2242
|
+
}
|
|
2243
|
+
((body && body.comments) || []).forEach(function (w) {
|
|
2244
|
+
var row = null;
|
|
2245
|
+
for (var i = 0; i < ledger.length; i++) if (ledger[i].id === w.id) row = ledger[i];
|
|
2246
|
+
if (row) row.status = "sent";
|
|
2247
|
+
else ledger.push(Object.assign({}, w, { status: "sent" })); // the generated story instruction
|
|
2248
|
+
});
|
|
2249
|
+
saveLedger();
|
|
2250
|
+
// The staged restructure is now the agent's job — keeping it staged
|
|
2251
|
+
// would show the reviewer a storyline the served plan no longer claims.
|
|
2252
|
+
S.storyDraft = null;
|
|
2253
|
+
enterWorking(CFG.embedded ? "Comments saved." : "Comments sent.");
|
|
2354
2254
|
}
|
|
2255
|
+
|
|
2355
2256
|
// The shared heartbeat poll (gate-session.js) with this gate's two endings:
|
|
2356
|
-
// a retired tab simply says where the review went.
|
|
2257
|
+
// a retired tab simply says where the review went. Standalone only — in the
|
|
2258
|
+
// workspace page the shell's monitor watches the round for the page's whole
|
|
2259
|
+
// life, and the page never "ends".
|
|
2357
2260
|
function startNextRoundPolling() {
|
|
2261
|
+
if (CFG.embedded) return;
|
|
2358
2262
|
SES.startNextRoundPolling({
|
|
2359
2263
|
onEnded: showSessionEnded,
|
|
2360
2264
|
// A retired tab still holding an unsent draft must not discard it: the
|
|
@@ -2369,58 +2273,37 @@
|
|
|
2369
2273
|
|
|
2370
2274
|
// ---------- done veil ----------
|
|
2371
2275
|
function showVeil(statusHtml, subText, subPulse) {
|
|
2372
|
-
|
|
2373
|
-
if (wsEl) wsEl.style.display = "none";
|
|
2374
|
-
var existing = document.querySelector(".sm-veil[data-sm-overlay]");
|
|
2375
|
-
if (existing && existing.parentNode) existing.parentNode.removeChild(existing);
|
|
2376
|
-
var veil = el("div", "sm-o sm-veil");
|
|
2377
|
-
veil.setAttribute("data-sm-overlay", "1");
|
|
2378
|
-
veil.appendChild(logoNode("sm-logo", "-v"));
|
|
2379
|
-
veil.appendChild(el("div", "word", wordmarkHtml()));
|
|
2380
|
-
veil.appendChild(el("div", "status", statusHtml));
|
|
2381
|
-
veil.appendChild(el("div", "sub" + (subPulse ? " sm-pulse" : ""), subText));
|
|
2382
|
-
document.body.appendChild(veil);
|
|
2276
|
+
return SHELL.showVeil(statusHtml, subText, subPulse, [wsEl]);
|
|
2383
2277
|
}
|
|
2278
|
+
// Embedded, the veil says the agent HAS the approval — only once the wire
|
|
2279
|
+
// proved it (delivered, or picked up later). Until then the approval is saved
|
|
2280
|
+
// and the strip + hand-off say what is still missing.
|
|
2384
2281
|
function showApproved() {
|
|
2385
|
-
|
|
2386
|
-
|
|
2387
|
-
|
|
2388
|
-
|
|
2389
|
-
|
|
2390
|
-
|
|
2282
|
+
if (CFG.embedded) {
|
|
2283
|
+
if (SHELL.state.phase !== "approved") {
|
|
2284
|
+
enterWorking("Plan approved.");
|
|
2285
|
+
return;
|
|
2286
|
+
}
|
|
2287
|
+
// working, so a reload (the approval itself may move the round) restores
|
|
2288
|
+
// this state instead of a fresh "approved — nothing to do" page
|
|
2289
|
+
if (!working) enterWorking("Plan approved.");
|
|
2290
|
+
var picked = SHELL.state.pickedUpAt;
|
|
2291
|
+
showVeil(
|
|
2292
|
+
ICONS.check + "<span>Plan approved</span>",
|
|
2293
|
+
picked ? "Your agent picked it up at " + SHELL.hhmm(picked) + " and is building the slides now." : "Your agent has it and is building the slides now.",
|
|
2294
|
+
true,
|
|
2295
|
+
);
|
|
2296
|
+
return;
|
|
2297
|
+
}
|
|
2298
|
+
showVeil(ICONS.check + "<span>Plan approved</span>", "The agent is building your slides now — you can close this tab.", true);
|
|
2299
|
+
setTimeout(SHELL.requestTabClose, APPROVE_CLOSE_DELAY_MS);
|
|
2391
2300
|
}
|
|
2392
2301
|
function showSessionEnded() {
|
|
2393
2302
|
showVeil("<span>Session ended</span>", "You can close this tab.");
|
|
2394
2303
|
}
|
|
2395
2304
|
function showSuperseded() {
|
|
2396
2305
|
document.title = "Plan review moved — slidemind";
|
|
2397
|
-
showVeil(
|
|
2398
|
-
"<span>Continued in a new tab</span>",
|
|
2399
|
-
"The next plan round opened in a fresh browser tab — you can close this one.",
|
|
2400
|
-
);
|
|
2401
|
-
}
|
|
2402
|
-
|
|
2403
|
-
// ---------- assets ----------
|
|
2404
|
-
function injectAssets() {
|
|
2405
|
-
var style = el("style");
|
|
2406
|
-
style.id = "sm-plan-style";
|
|
2407
|
-
style.textContent = CSS;
|
|
2408
|
-
document.head.appendChild(style);
|
|
2409
|
-
|
|
2410
|
-
var pre1 = el("link");
|
|
2411
|
-
pre1.rel = "preconnect";
|
|
2412
|
-
pre1.href = "https://fonts.googleapis.com";
|
|
2413
|
-
var pre2 = el("link");
|
|
2414
|
-
pre2.rel = "preconnect";
|
|
2415
|
-
pre2.href = "https://fonts.gstatic.com";
|
|
2416
|
-
pre2.crossOrigin = "anonymous";
|
|
2417
|
-
var fonts = el("link");
|
|
2418
|
-
fonts.rel = "stylesheet";
|
|
2419
|
-
fonts.href =
|
|
2420
|
-
"https://fonts.googleapis.com/css2?family=Poppins:wght@300;400;500;600&family=Urbanist:wght@300;400&display=swap";
|
|
2421
|
-
document.head.appendChild(pre1);
|
|
2422
|
-
document.head.appendChild(pre2);
|
|
2423
|
-
document.head.appendChild(fonts);
|
|
2306
|
+
showVeil("<span>Continued in a new tab</span>", "The next plan round opened in a fresh browser tab — you can close this one.");
|
|
2424
2307
|
}
|
|
2425
2308
|
|
|
2426
2309
|
// ---------- wire up ----------
|
|
@@ -2439,19 +2322,58 @@
|
|
|
2439
2322
|
var flagged = SES.consumeRoundFlag();
|
|
2440
2323
|
restoreSession();
|
|
2441
2324
|
|
|
2442
|
-
injectAssets();
|
|
2325
|
+
SHELL.injectAssets(CSS);
|
|
2443
2326
|
buildChrome();
|
|
2444
2327
|
buildWorkspace();
|
|
2328
|
+
refreshChrome();
|
|
2329
|
+
loadDesigns(); // the rest of the design menu, after the page is up
|
|
2445
2330
|
|
|
2446
2331
|
if (working) {
|
|
2447
2332
|
// A refresh of a round the reviewer already sent: keep waiting quietly.
|
|
2448
2333
|
if (wsEl) wsEl.classList.add("is-waiting");
|
|
2449
2334
|
document.title = WAITING_TITLE;
|
|
2450
2335
|
startNextRoundPolling();
|
|
2336
|
+
if (CFG.embedded) SHELL.renderRevising(); // a refresh mid-revision keeps the strip
|
|
2337
|
+
if (CFG.embedded && SHELL.state.phase === "approved") showApproved(); // the agent has the approval
|
|
2451
2338
|
} else {
|
|
2452
2339
|
// Parked round: the agent is waiting on the reviewer — announce it.
|
|
2453
2340
|
SES.announceNewRound(flagged);
|
|
2341
|
+
if (SHELL.alreadyApproved()) SHELL.showApprovedBanner("Your agent builds the slides from it. If something must still change, leave a comment.");
|
|
2342
|
+
if (SHELL.askedAgain()) SHELL.showApprovedBanner("Approve again to let it continue, or leave a comment if something must still change.", "Your agent asks you to confirm the plan again");
|
|
2343
|
+
if (SHELL.notAskedYet()) SHELL.showNotAskedBanner("Your agent is still writing the plan. Read along if you like — this page updates by itself when it asks you.");
|
|
2344
|
+
if (CFG.embedded && askedAgainUnchanged && CFG.asked) {
|
|
2345
|
+
SHELL.showAskedAgainUnchanged();
|
|
2346
|
+
} else if (CFG.embedded && arrivedAfterRevising && CFG.asked) {
|
|
2347
|
+
var n = Object.keys(changedSlides).length;
|
|
2348
|
+
SHELL.showArrived(n ? n + (n === 1 ? " slide changed" : " slides changed") + " — your comments were worked in. Have another look." : "Your comments were worked in. Have another look.");
|
|
2349
|
+
}
|
|
2350
|
+
if (editsRestored) {
|
|
2351
|
+
SHELL.showNotice("edits", { tone: "info", title: "Your unsent edits are back", text: "They are not sent yet — send or approve when you are ready.", dismiss: true });
|
|
2352
|
+
}
|
|
2353
|
+
if (staleEdits) {
|
|
2354
|
+
var old = staleEdits.value;
|
|
2355
|
+
SHELL.showNotice("edits", {
|
|
2356
|
+
tone: "warn",
|
|
2357
|
+
title: "Your agent changed the plan after your last unsent edits",
|
|
2358
|
+
text: "They were made on the older version, so they are not applied here. Copy them if you want to reuse a part.",
|
|
2359
|
+
dismiss: true,
|
|
2360
|
+
actions: [
|
|
2361
|
+
{
|
|
2362
|
+
label: "Copy my edited version",
|
|
2363
|
+
onClick: function () {
|
|
2364
|
+
try {
|
|
2365
|
+
navigator.clipboard.writeText(old);
|
|
2366
|
+
} catch {
|
|
2367
|
+
/* no clipboard in this frame */
|
|
2368
|
+
}
|
|
2369
|
+
},
|
|
2370
|
+
},
|
|
2371
|
+
],
|
|
2372
|
+
});
|
|
2373
|
+
}
|
|
2454
2374
|
}
|
|
2375
|
+
// The hand-over in progress, the next-load note, an answer on its way.
|
|
2376
|
+
SHELL.resume();
|
|
2455
2377
|
}
|
|
2456
2378
|
|
|
2457
2379
|
if (document.readyState === "loading") {
|