@galda/cli 0.10.101 → 0.10.102
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/app/index.html +137 -36
- package/package.json +1 -1
package/app/index.html
CHANGED
|
@@ -3793,7 +3793,7 @@ function buildReviewDigest({ goals, tasks } = {}){
|
|
|
3793
3793
|
for (const g of goals ?? []) {
|
|
3794
3794
|
if (g?.status !== 'review') continue;
|
|
3795
3795
|
const goalTasks = (tasks ?? []).filter((t) => t.goalId === g.id);
|
|
3796
|
-
const proofTask = goalTasks
|
|
3796
|
+
const proofTask = latestProofTask(goalTasks);
|
|
3797
3797
|
const checkLine = (g.reviewSummary?.check || g.plan?.[0] || g.text || '').replace(/\s+/g, ' ').trim().slice(0, 66);
|
|
3798
3798
|
// Mirrors lib.mjs isPlanReview: a plan awaiting approval reads "PLAN — approve
|
|
3799
3799
|
// to execute" and carries its plan body so the reviewer sees the plan in place.
|
|
@@ -9247,7 +9247,7 @@ function peraData(){
|
|
|
9247
9247
|
const gs = (state.goals || []).filter((g) => g.projectId === state.active);
|
|
9248
9248
|
const ts = (state.tasks || []).filter((t) => t.projectId === state.active);
|
|
9249
9249
|
return buildReviewDigest({ goals: gs, tasks: ts }).map((r) => {
|
|
9250
|
-
const pt = ts.
|
|
9250
|
+
const pt = latestProofTask(ts.filter((t) => r.goalIds.includes(t.goalId)), true);
|
|
9251
9251
|
return { ...r, proofTaskId: pt?.id ?? null, proofImg: pt?.proof?.png ?? pt?.proof?.gif ?? null };
|
|
9252
9252
|
});
|
|
9253
9253
|
}
|
|
@@ -9360,6 +9360,8 @@ function closeSummaryPera(){ $('summaryOverlay').classList.remove('show'); }
|
|
|
9360
9360
|
// settles it (see saSync's "returned to review" reset).
|
|
9361
9361
|
// ============================================================================
|
|
9362
9362
|
const SA = { open: false, items: [], diffs: {}, actLog: {}, msgs: {}, convOpen: new Set(), shotBox: {}, detOpen: new Set(), diffOpen: new Set(), attachments: {}, focusGid: null, focusScrolled: false, focusTimer: null, cur: null, solo: null, lastMarkup: null };
|
|
9363
|
+
// Same convention as the existing ?dev=1 devbar flag — off unless someone opts in by URL.
|
|
9364
|
+
const SA_DEV = new URLSearchParams(location.search).get('dev') === '1';
|
|
9363
9365
|
function saReplyAttachmentsHtml(goalId){
|
|
9364
9366
|
return (SA.attachments[goalId] || []).map((a, i) => `<span class="sa-replythumb"><img src="${esc(a.url)}" alt="Attached image"><button type="button" data-saarm="${goalId}:${i}" aria-label="Remove image">×</button></span>`).join('');
|
|
9365
9367
|
}
|
|
@@ -9394,6 +9396,17 @@ function latestGoalOutcomeTask(goalId, tasks){
|
|
|
9394
9396
|
}
|
|
9395
9397
|
function taskAttemptId(t){ return t?.attemptId || (t?.id != null ? `task-${t.id}` : null); }
|
|
9396
9398
|
function taskSortTime(t){ return Date.parse(t?.finishedAt || t?.startedAt || t?.createdAt || 0) || 0; }
|
|
9399
|
+
// The proof a reviewer is being asked to look at is the NEWEST one. A reply re-runs
|
|
9400
|
+
// the work and takes its own shot, so the goal accumulates several. Every call site
|
|
9401
|
+
// used to be `tasks.find((t) => t.proof)`, which returns the OLDEST — the card then
|
|
9402
|
+
// showed the first attempt's screenshot underneath the latest attempt's words
|
|
9403
|
+
// (Result already sorts by time via latestGoalOutcomeTask). `image` = the call site
|
|
9404
|
+
// renders a thumbnail and needs a png/gif, not a proof that only carries a video.
|
|
9405
|
+
function latestProofTask(tasks, image = false){
|
|
9406
|
+
return (tasks || [])
|
|
9407
|
+
.filter((t) => t.proof && (!image || t.proof.png || t.proof.gif))
|
|
9408
|
+
.sort((a, b) => taskSortTime(b) - taskSortTime(a))[0] || null;
|
|
9409
|
+
}
|
|
9397
9410
|
function latestGoalRunForOutcome(goalId, tasks, outcome){
|
|
9398
9411
|
if (outcome?.run) return { taskId: outcome.id, attemptId: taskAttemptId(outcome), inherited: false, ...outcome.run };
|
|
9399
9412
|
const runs = (tasks || []).filter((t) => t.goalId === goalId && t.run)
|
|
@@ -9424,7 +9437,7 @@ function saRowFromDigest(r, gs, ts){
|
|
|
9424
9437
|
// here so the card never calls that state a generic failure.
|
|
9425
9438
|
const interruptedTask = [...ts].filter((t) => r.goalIds.includes(t.goalId) && t.status === 'interrupted')
|
|
9426
9439
|
.sort((a, b) => taskSortTime(b) - taskSortTime(a))[0] || null;
|
|
9427
|
-
const pt = ts.
|
|
9440
|
+
const pt = latestProofTask(ts.filter((t) => r.goalIds.includes(t.goalId)), true);
|
|
9428
9441
|
// "What we did" (H23 §2, replaces the old Plan/Review-thread folds): one bullet
|
|
9429
9442
|
// per finished requirement task's own report; falls back to the goal's plan
|
|
9430
9443
|
// steps when nothing has finished yet (e.g. a plan awaiting approval — the plan
|
|
@@ -9463,9 +9476,9 @@ function saRowFromDigest(r, gs, ts){
|
|
|
9463
9476
|
attempt: g?.reviewAttempt ?? null,
|
|
9464
9477
|
unmappedChangedFiles: outcome?.unmappedChangedFiles ?? [],
|
|
9465
9478
|
requirementVerification: outcome?.requirementVerification ?? null,
|
|
9466
|
-
requirementCoverage: ts.filter((t) => t.goalId === g?.id && (t.detail || t.title)).sort((a, b) => (a.num ?? 0) - (b.num ?? 0)).map((t) => ({ text: String(t.reply ? t.detail : t.title).replace(/\s+/g, ' ').trim(), taskStatus: t.status, evidenceStatus: t.requirementEvidence?.requirements?.[0]?.status ?? null })),
|
|
9479
|
+
requirementCoverage: ts.filter((t) => t.goalId === g?.id && (t.detail || t.title)).sort((a, b) => (a.num ?? 0) - (b.num ?? 0)).map((t) => ({ text: String((t.reply ? t.detail : t.title) ?? t.title ?? '').replace(/\s+/g, ' ').trim(), taskStatus: t.status, evidenceStatus: t.requirementEvidence?.requirements?.[0]?.status ?? null })),
|
|
9467
9480
|
requirementEvidence: outcome?.requirementEvidence?.requirements ?? [],
|
|
9468
|
-
requirementCoverage: ts.filter((t) => t.goalId === g?.id && (t.detail || t.title) && (!t.reply || !t.replyFrom || t.replyFrom === 'you')).sort((a, b) => (a.num ?? 0) - (b.num ?? 0)).map((t) => ({ text: String(t.reply ? t.detail : t.title).replace(/\s+/g, ' ').trim(), status: t.status })),
|
|
9481
|
+
requirementCoverage: ts.filter((t) => t.goalId === g?.id && (t.detail || t.title) && (!t.reply || !t.replyFrom || t.replyFrom === 'you')).sort((a, b) => (a.num ?? 0) - (b.num ?? 0)).map((t) => ({ text: String((t.reply ? t.detail : t.title) ?? t.title ?? '').replace(/\s+/g, ' ').trim(), status: t.status })),
|
|
9469
9482
|
did, siblings, risk: g?.risk ?? null,
|
|
9470
9483
|
verificationInfraError: g?.verificationInfraError ?? r.verificationInfraError ?? null,
|
|
9471
9484
|
prNote: g?.prNote ?? null,
|
|
@@ -9573,7 +9586,7 @@ function saRowFromDigest(r, gs, ts){
|
|
|
9573
9586
|
// the goal + its tasks exactly as buildReviewDigest does for a review goal.
|
|
9574
9587
|
function saSoloDigestRow(g, ts){
|
|
9575
9588
|
const goalTasks = (ts ?? []).filter((t) => t.goalId === g.id);
|
|
9576
|
-
const proofTask = goalTasks
|
|
9589
|
+
const proofTask = latestProofTask(goalTasks);
|
|
9577
9590
|
const checkLine = (g.reviewSummary?.check || g.plan?.[0] || g.text || '').replace(/\s+/g, ' ').trim().slice(0, 66);
|
|
9578
9591
|
return {
|
|
9579
9592
|
goalIds: [g.id], reviewNumber: goalReviewNumber({ goal: g, tasks: ts }), checkLine,
|
|
@@ -10404,26 +10417,37 @@ function saRender(){
|
|
|
10404
10417
|
const S = $('seeall'); if (!S) return;
|
|
10405
10418
|
// SSE, diff fetches and action responses rebuild the dialog. Remember the
|
|
10406
10419
|
// review body's position before replacing it so reading never jumps to top.
|
|
10420
|
+
// A raw scrollTop number is not enough: if a new review card lands ABOVE the
|
|
10421
|
+
// one being read (saSync pushes it earlier in SA.items), restoring the same
|
|
10422
|
+
// scrollTop lands on different content — the card visually jumps ("ガクッ").
|
|
10423
|
+
// Anchor on the card currently at/near the top of the viewport instead, and
|
|
10424
|
+
// after the rebuild re-align that same card back to the same offset.
|
|
10407
10425
|
const previousBody = S.querySelector('.sa-body');
|
|
10408
10426
|
const previousScrollTop = previousBody?.scrollTop ?? 0;
|
|
10409
|
-
|
|
10410
|
-
//
|
|
10411
|
-
//
|
|
10412
|
-
//
|
|
10413
|
-
//
|
|
10414
|
-
//
|
|
10415
|
-
|
|
10416
|
-
// above it grew or shrank.
|
|
10427
|
+
// Preserve scroll even while a focus-flash is in flight: a new incoming review
|
|
10428
|
+
// (saSync from SSE) used to skip this whenever SA.focusGid was still set, snapping
|
|
10429
|
+
// the body back to scrollTop 0 and jerking the person's read position. The focus
|
|
10430
|
+
// block below only force-scrolls the focused card to the top ONCE per flash window
|
|
10431
|
+
// (SA.focusScrolled — see below), so it no longer fights this restore on later
|
|
10432
|
+
// renders within that window.
|
|
10433
|
+
const restoreScroll = Boolean(previousBody);
|
|
10417
10434
|
let anchorGid = null, anchorOffset = 0;
|
|
10435
|
+
// No `previousScrollTop > 0` guard here on purpose (task-917): right after opening,
|
|
10436
|
+
// or right after the focus-flash auto-scroll lands a card at the top, scrollTop IS 0
|
|
10437
|
+
// while a real card is still on screen. Skipping the anchor capture at exactly that
|
|
10438
|
+
// scrollTop silently let a newly-inserted card take over position 0 with no scroll
|
|
10439
|
+
// adjustment — the card someone was looking at got replaced under them without the
|
|
10440
|
+
// scrollbar moving at all, which read as the exact same "ガクッ" jump the anchor
|
|
10441
|
+
// logic below was built to prevent. Anchor whenever there is a previous body, full stop.
|
|
10418
10442
|
if (restoreScroll) {
|
|
10419
|
-
const
|
|
10420
|
-
|
|
10421
|
-
|
|
10422
|
-
const
|
|
10423
|
-
for (const el of previousBody.querySelectorAll('.sa-item')) {
|
|
10443
|
+
const bodyRect = previousBody.getBoundingClientRect();
|
|
10444
|
+
const midY = bodyRect.top + previousBody.clientHeight / 2;
|
|
10445
|
+
const cards = [...previousBody.querySelectorAll('.sa-item[data-gid]')];
|
|
10446
|
+
const anchor = cards.find((el) => {
|
|
10424
10447
|
const r = el.getBoundingClientRect();
|
|
10425
|
-
|
|
10426
|
-
}
|
|
10448
|
+
return r.top <= midY && r.bottom >= midY;
|
|
10449
|
+
}) || cards.find((el) => el.getBoundingClientRect().bottom > bodyRect.top);
|
|
10450
|
+
if (anchor) { anchorGid = anchor.dataset.gid; anchorOffset = anchor.getBoundingClientRect().top - bodyRect.top; }
|
|
10427
10451
|
}
|
|
10428
10452
|
const focusedReplyGoal = document.activeElement?.matches?.('#seeall .sa-chat')
|
|
10429
10453
|
? document.activeElement.dataset.goal
|
|
@@ -10438,17 +10462,51 @@ function saRender(){
|
|
|
10438
10462
|
const curIdx = pending.findIndex((x) => x.goalId === SA.cur);
|
|
10439
10463
|
const posLabel = solo ? '' : (pending.length && curIdx >= 0 ? `${curIdx + 1} / ${pending.length}` : '');
|
|
10440
10464
|
const hdKeys = solo ? '↑↓ MOVE · ESC CLOSE' : '↑↓ MOVE · A APPROVE · D DISMISS · ESC CLOSE';
|
|
10441
|
-
|
|
10465
|
+
// Manual repro aid for the "ガクッ" scroll-jump bug (task-870): behind ?dev=1 only,
|
|
10466
|
+
// never in a normal session. Lets a person scroll into a review card and then trigger
|
|
10467
|
+
// a synthetic "new review just arrived" render the same way saSync does mid-read.
|
|
10468
|
+
const devInjectBtn = SA_DEV ? '<button class="sa-x" data-saact="devinject:0" title="Insert a fake review above the current scroll (dev only)">+TEST</button>' : '';
|
|
10469
|
+
const markup = `<div class="sa-panel"><div class="sa-hd"><span class="t">${hdTitle}</span><span class="c">${c}</span><span class="sa-count">${posLabel}</span><span class="keys">${hdKeys}</span>${devInjectBtn}<button class="sa-x" data-saact="close:0">✕</button></div>
|
|
10442
10470
|
<div class="sa-body">${SA.items.length ? SA.items.map((it, i) => saItemHtml(it, i)).join('') : '<div class="sa-empty">Nothing waiting for review.</div>'}</div></div>`;
|
|
10443
10471
|
// Most SSE goal/task events are unrelated to the card the person is reading.
|
|
10444
10472
|
// Replacing #seeall with identical markup destroyed and recreated every node,
|
|
10445
10473
|
// producing a blank flash and resetting browser-owned state. Keep the current
|
|
10446
10474
|
// dialog DOM when its semantic markup did not change; the board behind it can
|
|
10447
10475
|
// still refresh normally.
|
|
10448
|
-
if (SA.lastMarkup === markup && S.querySelector('.sa-panel'))
|
|
10476
|
+
if (SA.lastMarkup === markup && S.querySelector('.sa-panel')) {
|
|
10477
|
+
// A focus change does not alter the card markup, but it still has a visual
|
|
10478
|
+
// side effect: move the newly focused card before paint. Handle that on the
|
|
10479
|
+
// existing DOM instead of returning early and waiting for the later rAF.
|
|
10480
|
+
if (SA.focusGid != null && !SA.focusScrolled) {
|
|
10481
|
+
const el = S.querySelector(`.sa-item[data-gid="${SA.focusGid}"]`);
|
|
10482
|
+
if (el) {
|
|
10483
|
+
saScrollCardToTop(el);
|
|
10484
|
+
SA.focusScrolled = true;
|
|
10485
|
+
el.classList.add('sa-flash');
|
|
10486
|
+
if (!SA.focusTimer) SA.focusTimer = setTimeout(() => { SA.focusGid = null; SA.focusTimer = null; }, 1400);
|
|
10487
|
+
}
|
|
10488
|
+
}
|
|
10489
|
+
return;
|
|
10490
|
+
}
|
|
10449
10491
|
SA.lastMarkup = markup;
|
|
10450
10492
|
S.innerHTML = markup;
|
|
10451
10493
|
const body = S.querySelector('.sa-body');
|
|
10494
|
+
let focusPlacedNow = false;
|
|
10495
|
+
// H14's "scroll the focused card to top" used to always run one requestAnimationFrame
|
|
10496
|
+
// after this point. That's still required for the very FIRST render of a fresh
|
|
10497
|
+
// saOpen(): #seeall is still `display:none` here (the .open class is added by the
|
|
10498
|
+
// caller right after saRender() returns), so getBoundingClientRect() on anything
|
|
10499
|
+
// inside is all-zero and a synchronous scroll here would be a no-op.
|
|
10500
|
+
// But once the panel is already open (S.classList has 'open'), a LATER render
|
|
10501
|
+
// arriving mid-flash (saSync/devinject, task-909) can and must place the card
|
|
10502
|
+
// synchronously here, in the same pass as the anchor restore below — deferring it
|
|
10503
|
+
// to the next rAF left a one-frame window where the browser could paint with the
|
|
10504
|
+
// OLD (pre-focus) scrollTop, then snap to the correct position a frame later: a
|
|
10505
|
+
// visible double-jump ("ガクッ") the first time a card arrived during that window.
|
|
10506
|
+
if (body && S.classList.contains('open') && SA.focusGid != null && !SA.focusScrolled) {
|
|
10507
|
+
const el = body.querySelector(`.sa-item[data-gid="${SA.focusGid}"]`);
|
|
10508
|
+
if (el) { saScrollCardToTop(el); SA.focusScrolled = true; focusPlacedNow = true; }
|
|
10509
|
+
}
|
|
10452
10510
|
// Once the person scrolls, their position wins over the short opening-focus
|
|
10453
10511
|
// animation. Programmatic opening scroll fires this too, which is fine: the
|
|
10454
10512
|
// first placement happened and later SSE ticks should preserve it.
|
|
@@ -10464,12 +10522,16 @@ function saRender(){
|
|
|
10464
10522
|
// also what actually grows a replied-to card's height, so the scroll anchor
|
|
10465
10523
|
// below must run AFTER it, not before (2026-07-29 jolt fix).
|
|
10466
10524
|
if (body && restoreScroll) {
|
|
10467
|
-
|
|
10468
|
-
|
|
10469
|
-
|
|
10470
|
-
body.
|
|
10471
|
-
|
|
10472
|
-
|
|
10525
|
+
// An explicit focus move wins for this render. Later SSE renders have
|
|
10526
|
+
// focusScrolled=true and return to the normal reading-anchor preservation.
|
|
10527
|
+
if (!focusPlacedNow) {
|
|
10528
|
+
const anchorEl = anchorGid != null ? body.querySelector(`.sa-item[data-gid="${anchorGid}"]`) : null;
|
|
10529
|
+
if (anchorEl) {
|
|
10530
|
+
const newOffset = anchorEl.getBoundingClientRect().top - body.getBoundingClientRect().top;
|
|
10531
|
+
body.scrollTop += (newOffset - anchorOffset);
|
|
10532
|
+
} else {
|
|
10533
|
+
body.scrollTop = previousScrollTop;
|
|
10534
|
+
}
|
|
10473
10535
|
}
|
|
10474
10536
|
}
|
|
10475
10537
|
for (const det of S.querySelectorAll('[data-detkey]')) {
|
|
@@ -10502,17 +10564,18 @@ function saRender(){
|
|
|
10502
10564
|
if (im.complete && im.naturalWidth === 0) drop();
|
|
10503
10565
|
else im.addEventListener('error', drop, { once: true });
|
|
10504
10566
|
}
|
|
10505
|
-
// H14: if a single-review focus is pending,
|
|
10506
|
-
//
|
|
10507
|
-
//
|
|
10508
|
-
//
|
|
10509
|
-
//
|
|
10510
|
-
//
|
|
10567
|
+
// H14: if a single-review focus is pending, flash a blue ring on its card. The
|
|
10568
|
+
// scroll-to-top placement already happened synchronously above whenever the panel
|
|
10569
|
+
// was visible at render time; this rAF is the fallback for the still-`display:none`
|
|
10570
|
+
// first render of saOpen() (SA.focusScrolled stays false above, since the panel
|
|
10571
|
+
// isn't open yet), plus it always adds the flash class and starts the self-clear
|
|
10572
|
+
// timer, neither of which affect scroll position. Self-clears after ~1.4s (outlasts
|
|
10573
|
+
// the localhost diff fetch) so live SSE ticks stop yanking the view.
|
|
10511
10574
|
if (SA.focusGid != null) {
|
|
10512
10575
|
requestAnimationFrame(() => {
|
|
10513
10576
|
const el = S.querySelector(`.sa-item[data-gid="${SA.focusGid}"]`);
|
|
10514
10577
|
if (!el) return;
|
|
10515
|
-
saScrollCardToTop(el);
|
|
10578
|
+
if (!SA.focusScrolled) { saScrollCardToTop(el); SA.focusScrolled = true; }
|
|
10516
10579
|
el.classList.add('sa-flash');
|
|
10517
10580
|
if (!SA.focusTimer) SA.focusTimer = setTimeout(() => { SA.focusGid = null; SA.focusTimer = null; }, 1400);
|
|
10518
10581
|
});
|
|
@@ -10852,6 +10915,15 @@ $('seeall').addEventListener('click', async (e) => {
|
|
|
10852
10915
|
// #reviewOverlay sits above #seeall, and a folded goal shows read-only there since
|
|
10853
10916
|
// canReview=false hides Approve/Dismiss), or restore it via /unfold. Handled before the
|
|
10854
10917
|
// SA.items[i] lookup below.
|
|
10918
|
+
// Dev-only repro aid (task-870, gated by SA_DEV/?dev=1): simulate a new review card
|
|
10919
|
+
// arriving mid-read the same way saSync splices one in from SSE, so the scroll-jump
|
|
10920
|
+
// fix above can be checked by hand without waiting for a real worker to finish.
|
|
10921
|
+
if (k === 'devinject') {
|
|
10922
|
+
if (!SA_DEV) return;
|
|
10923
|
+
SA.items.unshift(saDevSeedItem());
|
|
10924
|
+
saRender();
|
|
10925
|
+
return;
|
|
10926
|
+
}
|
|
10855
10927
|
if (k === 'foldopen') return openReviewChecklist(Number(v));
|
|
10856
10928
|
if (k === 'foldundo') {
|
|
10857
10929
|
let r; try { r = await fetch(withKey(`/api/goals/${v}/unfold`), { method: 'POST' }); }
|
|
@@ -11503,6 +11575,35 @@ $('toggle').onclick = () => { $('side').classList.toggle('collapsed'); rememberS
|
|
|
11503
11575
|
// V3-C dev escape hatch (Masa決定 2026-07-07): flagship hides the top bar; ?dev=1
|
|
11504
11576
|
// (or localStorage devbar=1) shows it there for development (layout/theme switchers).
|
|
11505
11577
|
if (new URLSearchParams(location.search).get('dev') === '1' || localStorage.getItem('devbar') === '1') document.body.classList.add('devbar');
|
|
11578
|
+
// Manual repro aid for the scroll-jump-on-new-review bug (task-870), ?dev=1 only: a
|
|
11579
|
+
// floating button that opens the Review dialog pre-loaded with several synthetic cards
|
|
11580
|
+
// (no real worker/PR needed), so the "+TEST" button inside the dialog (see saRender) has
|
|
11581
|
+
// something to scroll through. Never shown outside an explicit ?dev=1 session.
|
|
11582
|
+
let saDevSeq = 0;
|
|
11583
|
+
function saDevSeedItem(){
|
|
11584
|
+
saDevSeq++;
|
|
11585
|
+
const n = saDevSeq;
|
|
11586
|
+
return {
|
|
11587
|
+
goalId: -n, ids: [-n], st: 0, goal: {}, title: `Test review #${n}`,
|
|
11588
|
+
report: `Synthetic review card #${n} — for manually checking the scroll position stays put when a new card is inserted (task-870).`,
|
|
11589
|
+
pr: null, testResult: null, tokenOpt: null,
|
|
11590
|
+
metaFiles: [`fixtures/dev-review-${n}.mjs +${n} −${n}`], metaAdd: `+${n}`, metaDel: `−${n}`,
|
|
11591
|
+
};
|
|
11592
|
+
}
|
|
11593
|
+
if (SA_DEV) {
|
|
11594
|
+
const btn = document.createElement('button');
|
|
11595
|
+
btn.textContent = 'Open test review (dev)';
|
|
11596
|
+
btn.title = 'Opens the Review dialog with fake cards so the scroll-jump fix (task-870) can be checked by hand: scroll down, then press +TEST in the dialog header.';
|
|
11597
|
+
btn.style.cssText = 'position:fixed;right:16px;bottom:16px;z-index:9999;padding:8px 12px;border-radius:8px;border:1px solid var(--line,#444);background:var(--panel,#222);color:var(--ink,#eee);cursor:pointer;font:12px/1.2 inherit';
|
|
11598
|
+
btn.addEventListener('click', () => {
|
|
11599
|
+
SA.open = true; SA.solo = null; SA.detOpen = new Set(); SA.focusGid = null;
|
|
11600
|
+
SA.items = Array.from({ length: 6 }, () => saDevSeedItem());
|
|
11601
|
+
SA.cur = SA.items[0].goalId;
|
|
11602
|
+
saRender();
|
|
11603
|
+
$('seeall').classList.add('open');
|
|
11604
|
+
});
|
|
11605
|
+
document.body.appendChild(btn);
|
|
11606
|
+
}
|
|
11506
11607
|
// Feedback 7: layout switcher — apply + persist the chosen arrangement.
|
|
11507
11608
|
function applyLayout(){
|
|
11508
11609
|
document.body.dataset.layout = state.layout || 'current';
|
package/package.json
CHANGED