@galda/cli 0.10.122 → 0.10.124
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 +614 -107
- package/package.json +1 -1
package/app/index.html
CHANGED
|
@@ -237,8 +237,11 @@
|
|
|
237
237
|
.composer-wrap{flex:0 0 auto;padding:8px 22px 18px}
|
|
238
238
|
.composer{position:relative;max-width:640px;margin:0 auto;display:flex;flex-direction:column;gap:8px;border:1px solid var(--hair2);
|
|
239
239
|
border-radius:16px;background:var(--glass);backdrop-filter:blur(22px);padding:10px 12px}
|
|
240
|
+
/* max-height reads the same --compmax dial the JS auto-grow uses. It was a second,
|
|
241
|
+
independent 120 here, so raising the JS cap did nothing — the box stayed at 120 while
|
|
242
|
+
the dial said 280 (measured). One number, two consumers. */
|
|
240
243
|
.composer textarea{background:transparent;border:0;resize:none;color:var(--ink);font:inherit;font-size:14px;
|
|
241
|
-
line-height:1.5;outline:none;max-height:120px;padding:2px 2px 0}
|
|
244
|
+
line-height:1.5;outline:none;max-height:var(--compmax,120px);padding:2px 2px 0}
|
|
242
245
|
.composer textarea::placeholder{color:var(--ink3)}
|
|
243
246
|
/* slash-command palette — floats above the whole composer (Claude-Code style).
|
|
244
247
|
Reuses the .appmenu/.skillmenu popover language: 1px hairline, tokens, no
|
|
@@ -1292,6 +1295,79 @@
|
|
|
1292
1295
|
body[data-layout="flagship"] .gearpop{position:fixed;top:120px;right:auto;width:380px;max-height:calc(100vh - 160px);border-radius:14px;
|
|
1293
1296
|
background:color-mix(in srgb,var(--panel) 74%,transparent);
|
|
1294
1297
|
-webkit-backdrop-filter:blur(28px) saturate(1.35);backdrop-filter:blur(28px) saturate(1.35)}
|
|
1298
|
+
/* ── Settings = a centred two-column dialog (CDO 2026-08-02, pattern C) ────────────
|
|
1299
|
+
Four sections that each hold ten-plus controls are a sidebar's job, not a tab
|
|
1300
|
+
strip's: the 380px column beside the rail was showing them 380px at a time, and
|
|
1301
|
+
the panel covered the feed while doing it. Same parts, new frame — the tab
|
|
1302
|
+
buttons keep their data-gctab wiring, the sticky Save/Cancel footer keeps its
|
|
1303
|
+
shape, and everything below 760px still gets the bottom sheet further down.
|
|
1304
|
+
Scoped to flagship only: the legacy anchored popover in other layouts is
|
|
1305
|
+
untouched (it is 320px wide and a left rail would ruin it).
|
|
1306
|
+
No scrim (Masa 2026-08-02): a dimmed board made opening Settings feel like a modal
|
|
1307
|
+
interruption. The dialog just appears — its own shadow separates it from the board,
|
|
1308
|
+
and outside-tap/Esc already close it.
|
|
1309
|
+
ONE HEIGHT for every section (Masa 2026-08-02): the panel used to resize as you moved
|
|
1310
|
+
between Theme (long) and MCP (short), so the thing under your cursor jumped. The frame
|
|
1311
|
+
is fixed and the content column scrolls inside it. */
|
|
1312
|
+
body[data-layout="flagship"] .gearpop.open{display:flex;flex-direction:row;gap:22px;
|
|
1313
|
+
/* Straddles the rail rather than sitting dead centre (Masa 2026-08-02): landing on top
|
|
1314
|
+
of the rail says "this belongs to the gear you just pressed", and it leaves the feed
|
|
1315
|
+
— the thing you were reading — visible on the right instead of split in half. */
|
|
1316
|
+
top:50%;left:96px;transform:translateY(-50%);right:auto;bottom:auto;
|
|
1317
|
+
width:min(880px,calc(100vw - 128px));max-width:none;height:min(720px,86vh);max-height:86vh;
|
|
1318
|
+
border-radius:16px;padding:18px 20px 0;
|
|
1319
|
+
overflow:hidden; /* the .gbody column scrolls, not the frame */
|
|
1320
|
+
/* Lifted off the board instead of matching it: on the dark themes --panel is very near
|
|
1321
|
+
black, so a black dialog on a black board read as a hole rather than a surface. The
|
|
1322
|
+
mix is ink-based, so it lightens in the dark and stays white on the light themes. */
|
|
1323
|
+
background:color-mix(in srgb,var(--ink) 5%,rgb(from var(--panel) r g b));
|
|
1324
|
+
-webkit-backdrop-filter:none;backdrop-filter:none;
|
|
1325
|
+
box-shadow:0 24px 70px rgba(0,0,0,.42),0 0 0 1px var(--hair2)}
|
|
1326
|
+
body[data-layout="flagship"] .gearpop .gnav{flex:0 0 158px;display:flex;flex-direction:column;gap:2px;
|
|
1327
|
+
border-right:1px solid var(--hair);padding:2px 12px 12px 0}
|
|
1328
|
+
body[data-layout="flagship"] .gearpop .gnav .ggrp{font:600 9px/1 var(--mono);letter-spacing:.12em;
|
|
1329
|
+
text-transform:uppercase;color:var(--ink3);padding:14px 10px 6px}
|
|
1330
|
+
body[data-layout="flagship"] .gearpop .gnav .ggrp:first-child{padding-top:2px}
|
|
1331
|
+
body[data-layout="flagship"] .gearpop .gnav button{border:0;background:none;text-align:left;
|
|
1332
|
+
padding:8px 10px;border-radius:8px;cursor:pointer;font:500 12.5px/1 var(--ui);color:var(--ink2)}
|
|
1333
|
+
body[data-layout="flagship"] .gearpop .gnav button:hover{color:var(--ink)} /* glyph only (§5.5) */
|
|
1334
|
+
body[data-layout="flagship"] .gearpop .gnav button.on{background:var(--hover);color:var(--ink);font-weight:600}
|
|
1335
|
+
body[data-layout="flagship"] .gearpop .gcol{flex:1;min-width:0;display:flex;flex-direction:column}
|
|
1336
|
+
/* padding-bottom: the note used to end flush against the frame's edge with nowhere for
|
|
1337
|
+
the eye to land (Masa 2026-08-02). Also the scroll's own breathing room. */
|
|
1338
|
+
body[data-layout="flagship"] .gearpop .gbody{flex:1;min-height:0;overflow-y:auto;scrollbar-width:thin;padding-bottom:26px}
|
|
1339
|
+
body[data-layout="flagship"] .gearpop .gbody > h4:first-child{margin-top:2px}
|
|
1340
|
+
body[data-layout="flagship"] .gearpop .cfoot{margin:14px -20px 0;padding:14px 20px;bottom:0;border-radius:0;
|
|
1341
|
+
background:color-mix(in srgb,var(--ink) 5%,rgb(from var(--panel) r g b))}
|
|
1342
|
+
/* ONE COLUMN (Masa 2026-08-02). This was two-across, and reading it was worse, not
|
|
1343
|
+
better: a settings row is label + description + control on one line, so pairing two
|
|
1344
|
+
of them puts four things on a line and the eye stops knowing which control belongs to
|
|
1345
|
+
which label. The width now buys long descriptions that no longer wrap, which is what
|
|
1346
|
+
it should have bought in the first place. The class stays a grid so the group keeps
|
|
1347
|
+
one place to change. */
|
|
1348
|
+
.gearpop .gtwo{display:grid;grid-template-columns:1fr}
|
|
1349
|
+
/* The free-tier Upgrade CTA is a bare button, not a .srow — as a full-width grid child
|
|
1350
|
+
it read as a banner. .bx-up-set pins width:100%, right in the 320px popover, wrong
|
|
1351
|
+
here. Keep it the size of its label, on the right edge where every other control in
|
|
1352
|
+
this list sits (Masa 2026-08-02) — a lone left-aligned button broke the column of
|
|
1353
|
+
chips it belongs to. */
|
|
1354
|
+
.gearpop .gtwo > .bx-cta{justify-self:end;align-self:center;margin:9px 0;width:auto}
|
|
1355
|
+
/* ── FEEDBACK: the founder's note, last thing in SETTINGS (Masa 2026-08-02) ────────
|
|
1356
|
+
A letter, not a settings row: hairline above, mono label, plain prose, one mail
|
|
1357
|
+
button. No fill, no badge, no icon block (§5.5). It lives inside the scrolling
|
|
1358
|
+
body so it never covers the sticky Save/Cancel. */
|
|
1359
|
+
.gearpop .gfeed{border-top:1px solid var(--hair);margin-top:13px;padding-top:13px}
|
|
1360
|
+
.gearpop .gfeed.gsolo{border-top:0;margin-top:2px;padding-top:0}
|
|
1361
|
+
.gearpop .gfeed.gsolo p{font-size:13px;line-height:1.8;max-width:56ch;margin-bottom:13px}
|
|
1362
|
+
.gearpop .gfeed.gsolo .gsig{font-size:12px;margin-bottom:18px}
|
|
1363
|
+
.gearpop .gfeed.gsolo .gmail{height:32px;padding:0 15px;font-size:12.5px}
|
|
1364
|
+
.gearpop .gfeed h4{margin:0 0 8px}
|
|
1365
|
+
.gearpop .gfeed p{font-size:11.5px;line-height:1.75;color:var(--ink2);margin:0 0 9px;max-width:54ch}
|
|
1366
|
+
.gearpop .gfeed .gsig{font-size:11px;color:var(--ink3);margin:0 0 11px}
|
|
1367
|
+
.gearpop .gfeed .gmail{display:inline-flex;align-items:center;gap:6px;height:27px;padding:0 12px;
|
|
1368
|
+
border-radius:8px;background:var(--chipbg);color:var(--ink);text-decoration:none;font:500 11.5px/1 var(--mono)}
|
|
1369
|
+
.gearpop .gfeed .gmail:hover{color:var(--ink);background:color-mix(in srgb,var(--ink) 10%,rgb(from var(--panel) r g b))}
|
|
1370
|
+
.gearpop .gfeed .gmail svg{width:12px;height:12px;opacity:.55;fill:none;stroke:currentColor;stroke-width:1.4}
|
|
1295
1371
|
/* settings = EDIT SESSION (HANDOFF-v45 §3, flagship .cfoot/.mcpbox port): sticky footer,
|
|
1296
1372
|
always visible over the internal scroll — Save = ink-solid persists, Cancel = ghost
|
|
1297
1373
|
restores the open-time snapshot. */
|
|
@@ -1333,14 +1409,6 @@
|
|
|
1333
1409
|
.gearpop .srow.dis{opacity:.38;pointer-events:none}
|
|
1334
1410
|
.gearpop .srow .sl{flex:1;min-width:0;font-size:12.5px;color:var(--ink)}
|
|
1335
1411
|
.gearpop .srow .sd{display:block;font-size:10.5px;color:var(--ink3);margin-top:2px}
|
|
1336
|
-
/* Project rules read-only view (§1.5a): a quiet framed panel under its row —
|
|
1337
|
-
mono filename label + the file body, scrollable. Reuses --canvas/--hair so it
|
|
1338
|
-
matches the MCP box; no new surface invented. */
|
|
1339
|
-
.gearpop .rulesview{margin:2px 0 9px;display:flex;flex-direction:column;gap:8px}
|
|
1340
|
-
.gearpop .rulesfile{background:var(--canvas);border-radius:8px;overflow:hidden}
|
|
1341
|
-
.gearpop .rulesname{font:600 9px/1 var(--mono);letter-spacing:.08em;text-transform:uppercase;color:var(--ink3);padding:8px 10px 0}
|
|
1342
|
-
.gearpop .rulestext{margin:6px 0 0;padding:0 10px 10px;max-height:220px;overflow:auto;font:400 11px/1.6 var(--mono);color:var(--ink2);white-space:pre-wrap;word-break:break-word}
|
|
1343
|
-
.gearpop .rulesempty{color:var(--ink3);font-style:italic}
|
|
1344
1412
|
/* MCP tab: live status dot + connect command + quiet explanation */
|
|
1345
1413
|
.gearpop .mcpstat{display:flex;align-items:center;gap:8px;font:600 11px/1 var(--ui);color:var(--ink);padding:2px 0 10px}
|
|
1346
1414
|
.gearpop .mcpstat i{width:7px;height:7px;border-radius:50%;background:var(--green);box-shadow:0 0 6px rgba(var(--green-rgb),.5)}
|
|
@@ -1690,7 +1758,12 @@
|
|
|
1690
1758
|
#fsRoot.lanefold .lanetg{left:50%;margin-left:-14px} /* no rotation (v45 §4) */
|
|
1691
1759
|
#fsRoot.lanefold .fslane{flex-basis:44px;padding:18px 7px}
|
|
1692
1760
|
#fsRoot.lanefold .fslane>*:not(.lanetg){display:none}
|
|
1693
|
-
|
|
1761
|
+
/* The grip sits on the edge that faces the chat. It was pinned to the lane's LEFT,
|
|
1762
|
+
which was right while the lane lived on the right of the chat — once the board took
|
|
1763
|
+
the middle (swapmid) that edge faces the rail and the board could not be widened at
|
|
1764
|
+
all from its own right side (Masa 2026-08-03: 右側の幅が変えられない). */
|
|
1765
|
+
#fsRoot .lanegrip{position:absolute;left:-3px;right:auto;top:0;bottom:0;width:7px;cursor:col-resize;z-index:2}
|
|
1766
|
+
#fsRoot.swapmid .lanegrip{left:auto;right:-3px}
|
|
1694
1767
|
#fsRoot .lanegrip::after{content:"";position:absolute;left:3px;top:50%;transform:translateY(-50%);width:2px;height:34px;border-radius:2px;background:var(--hair2);opacity:0;transition:opacity .15s}
|
|
1695
1768
|
#fsRoot .fslane:hover .lanegrip::after{opacity:1}
|
|
1696
1769
|
#fsRoot .lanebody{display:flex;flex-direction:column;min-height:0;flex:1;overflow-y:auto;overflow-x:hidden;scrollbar-width:none}
|
|
@@ -1732,7 +1805,81 @@
|
|
|
1732
1805
|
/* ---- kanban board: fills the SAME lane slot the To Do list uses (var(--lanew-board), independent
|
|
1733
1806
|
from the list's own var(--lanew)) — swapping List⇄Board never resizes the lane or the chat.
|
|
1734
1807
|
5+ columns don't fit the lane width, so it scrolls horizontally in place instead of growing. ---- */
|
|
1735
|
-
|
|
1808
|
+
/* Unset = the board fills the row and the CHAT's min-width is the only thing that
|
|
1809
|
+
stops it (Masa 2026-08-03: 既定を kanban に・添付画像くらいの幅で). Letting flexbox
|
|
1810
|
+
hold the floor beats computing a width in JS: the boot-order guess came out 60px
|
|
1811
|
+
wide and the composer's button row clipped by 72px, and a JS correction loop then
|
|
1812
|
+
overshot the other way (lane 41% of the window at 1280). --lanew-board is still the
|
|
1813
|
+
one dial the drag grip writes, so a dragged width keeps winning. */
|
|
1814
|
+
#fsRoot .fslane.boardmode{flex-basis:var(--lanew-board,100%);flex-shrink:1;min-width:280px}
|
|
1815
|
+
/* 460, not the 440 the JS constant claims. Measured by walking the chat's width in 4px
|
|
1816
|
+
steps and watching .crow overflow: it clears at 452 and still spills 12px at 440, so
|
|
1817
|
+
the old figure had drifted from whatever the composer's button row costs today. 460
|
|
1818
|
+
leaves a little slack. Below this those chips clip off the card's edge. It only bites
|
|
1819
|
+
in board mode — in List the chat is ~848px and nowhere near it. .fscenter sits BEFORE
|
|
1820
|
+
.fslane in the DOM, so no sibling selector reaches it; the state is mirrored onto
|
|
1821
|
+
#fsRoot instead. .fscenter's own min-width:0 stays untouched — that one is load-bearing
|
|
1822
|
+
(it is what stops a wide lane from overflowing the page). */
|
|
1823
|
+
#fsRoot.boardmode .fscenter{min-width:460px}
|
|
1824
|
+
|
|
1825
|
+
/* ── Board view puts the composer UNDER the board (Masa 2026-08-03, variant b) ─────
|
|
1826
|
+
The board took the middle, which left the composer in the 460px right-hand column
|
|
1827
|
+
where it read cramped. It now spans the board's own width, below it, punching through
|
|
1828
|
+
the lane boundary — near where it sat before the board moved.
|
|
1829
|
+
It has to be a DOM move, not just CSS: #fsCompwrap's parent .fscenter is itself
|
|
1830
|
+
position:relative AND overflow:hidden, so an absolutely-positioned composer anchored
|
|
1831
|
+
to the chat column and got clipped by it (measured: host at 1160-1620 on a 1440px
|
|
1832
|
+
window — off-screen and cropped). #fsCompBar is a direct child of #fsRoot instead. */
|
|
1833
|
+
/* How tall the textarea may grow before it scrolls. It was a hard 120 in three
|
|
1834
|
+
separate places; now one dial, draggable by the handle above the card and reset by
|
|
1835
|
+
double-clicking it (Masa 2026-08-03: 自由にかえる・デフォルトに戻す). */
|
|
1836
|
+
:root{--compmax:120px}
|
|
1837
|
+
/* No visible grab line (Masa 2026-08-03: いらなそう). The top edge of the composer is
|
|
1838
|
+
simply a resize zone — the cursor says so on hover, which is how the rail and lane
|
|
1839
|
+
grips already work. Full width so it is easy to land on. */
|
|
1840
|
+
.comphandle{display:none}
|
|
1841
|
+
body[data-layout="flagship"] .comphandle{display:block;height:10px;margin:0 0 -6px;width:100%;
|
|
1842
|
+
cursor:ns-resize;position:relative;touch-action:none}
|
|
1843
|
+
#fsRoot .fscompbar{display:none}
|
|
1844
|
+
/* Drop targets while the composer is being dragged (§5.5: a line, not a filled box). */
|
|
1845
|
+
#fsRoot.compdragging .fscompbar,#fsRoot.compdragging .fscenter{outline:1px dashed var(--hair2);outline-offset:-4px;border-radius:12px}
|
|
1846
|
+
#fsRoot.compdragging .comp-over{outline-color:var(--ink3)}
|
|
1847
|
+
/* Once the composer moves to the chat this bar is empty, so it collapses to zero height
|
|
1848
|
+
and there is nothing left to drop on — the composer could go one way and never come
|
|
1849
|
+
back (Masa 2026-08-03: kanban に戻せなくなってる). It only takes up room WHILE a drag
|
|
1850
|
+
is in progress, so an empty box is never sitting there the rest of the time (§1.5-3). */
|
|
1851
|
+
#fsRoot.compdragging .fscompbar{min-height:72px}
|
|
1852
|
+
/* While it is being carried: it follows the cursor, sits above everything, and stops
|
|
1853
|
+
receiving hits so the column under the pointer is what gets highlighted. */
|
|
1854
|
+
#composerWrap.compdrag-lift{position:relative;z-index:40;pointer-events:none;
|
|
1855
|
+
box-shadow:0 18px 44px rgba(0,0,0,.42);opacity:.94;transition:none}
|
|
1856
|
+
body.compdrag-noselect,body.compdrag-noselect *{user-select:none!important;-webkit-user-select:none!important}
|
|
1857
|
+
#fsRoot.compdragging .fscompbar::after{content:"Composer";position:absolute;left:0;right:0;top:50%;
|
|
1858
|
+
transform:translateY(-50%);text-align:center;font:600 10px/1 var(--mono);letter-spacing:.12em;
|
|
1859
|
+
text-transform:uppercase;color:var(--ink3);pointer-events:none}
|
|
1860
|
+
#fsRoot.compdragging .fscompbar:has(#composerWrap)::after{content:none}
|
|
1861
|
+
/* Moving it: grab the composer anywhere that is not something you can type in or press
|
|
1862
|
+
(Masa 2026-08-03: どこを掴めるか分からない). The cursor is the whole affordance — no
|
|
1863
|
+
extra handle to find. */
|
|
1864
|
+
body[data-layout="flagship"] #fsRoot.boardmode #composerWrap{cursor:grab}
|
|
1865
|
+
body[data-layout="flagship"] #fsRoot.boardmode #composerWrap:active{cursor:grabbing}
|
|
1866
|
+
/* Only the textarea is named. Forcing cursor:pointer on every button in here overrode
|
|
1867
|
+
the ones that deliberately say something else — the folder row goes cursor:default
|
|
1868
|
+
while a chooser is already open so it stops advertising itself as pressable, and this
|
|
1869
|
+
rule was louder than that (caught by folder-picker-single-dialog). Buttons already
|
|
1870
|
+
carry their own cursor; leave them alone. */
|
|
1871
|
+
body[data-layout="flagship"] #fsRoot.boardmode #composerWrap textarea{cursor:text}
|
|
1872
|
+
#fsRoot.boardmode .fscompbar{display:block;position:absolute;z-index:6;
|
|
1873
|
+
left:18px;right:18px;bottom:14px} /* 18px = .fslane's own side padding */
|
|
1874
|
+
/* The board stops above the composer. The reserve is the bar's MEASURED height, not a
|
|
1875
|
+
constant: the box is user-resizable now, so any fixed number is wrong the moment
|
|
1876
|
+
someone drags it (and 136 already left the board's scrollbar 7px underneath). */
|
|
1877
|
+
#fsRoot.boardmode .fslane{padding-bottom:calc(var(--compbarh,124px) + 20px)}
|
|
1878
|
+
@media (max-width: 760px){
|
|
1879
|
+
#fsRoot.boardmode .fscompbar{display:none}
|
|
1880
|
+
#fsRoot.boardmode .fslane{padding-bottom:0}
|
|
1881
|
+
}
|
|
1882
|
+
#fsRoot.boardmode .fscompbar #composerWrap{width:100%;margin:0;max-width:none}
|
|
1736
1883
|
#fsRoot .fslane.boardmode #fsTodoSec .tl,#fsRoot .fslane.boardmode #fsTodoSec .doneline,#fsRoot .fslane.boardmode #fsTodoSec #fsDonelist{display:none}
|
|
1737
1884
|
/* Board mode = board ONLY (Masa 2026-07-17): the Review section — proof thumbnails, PR
|
|
1738
1885
|
links, approve/dismiss, and its "N ready" summary line — is spoken by the board's own
|
|
@@ -1747,7 +1894,14 @@
|
|
|
1747
1894
|
container-box position (measured identical, 63px both), but the glyph sits differently inside that box. Scoped here, not on <body>, to avoid
|
|
1748
1895
|
a global typography change. */
|
|
1749
1896
|
#fsRoot .fslane.boardmode #fsBoard{display:flex}
|
|
1750
|
-
|
|
1897
|
+
/* The horizontal scrollbar is a hint, not furniture (Masa 2026-08-03): the track keeps
|
|
1898
|
+
its 8px so nothing shifts, but the thumb is transparent until you actually scroll —
|
|
1899
|
+
.sbon is added on scroll and dropped ~1.4s after the last one. */
|
|
1900
|
+
#fsBoard{scrollbar-width:none}
|
|
1901
|
+
#fsBoard.sbon{scrollbar-width:thin;scrollbar-color:var(--hair2) transparent}
|
|
1902
|
+
#fsBoard::-webkit-scrollbar{height:8px}
|
|
1903
|
+
#fsBoard::-webkit-scrollbar-thumb{background:transparent;border-radius:6px;transition:background .25s}
|
|
1904
|
+
#fsBoard.sbon::-webkit-scrollbar-thumb{background:var(--hair2)}
|
|
1751
1905
|
.fsbcol{flex:0 0 236px;max-height:100%;display:flex;flex-direction:column;min-height:0} /* 236 = flagship .labcol (was 200, a prod-side narrowing that made the board read cramped vs the Artifact) */
|
|
1752
1906
|
.fsbcolhd{display:flex;align-items:center;gap:8px;padding:1px 4px 9px;flex:0 0 auto}
|
|
1753
1907
|
.fsbdot{width:7px;height:7px;border-radius:50%;background:var(--stg,var(--ink3));flex:0 0 auto;box-shadow:0 0 0 3px color-mix(in srgb,var(--stg,#fff) 14%,transparent)}
|
|
@@ -1858,6 +2012,19 @@
|
|
|
1858
2012
|
#fsRoot .qadd.editing{opacity:1;color:var(--ink);cursor:text}
|
|
1859
2013
|
#fsRoot .qadd input{all:unset;display:block;min-width:0;flex:1;color:var(--ink);font:inherit;line-height:18px}
|
|
1860
2014
|
#fsRoot .qadd input::placeholder{color:var(--ink3)}
|
|
2015
|
+
/* A card that has been accepted but not started: faint until the engine picks it up. */
|
|
2016
|
+
#fsRoot .lbc.is-tentative{opacity:.5}
|
|
2017
|
+
/* The board's own add row. It reads and behaves like the To Do list's .qadd — before
|
|
2018
|
+
this the inline field kept the browser's default chrome and landed as a white box on
|
|
2019
|
+
a dark board (Masa 2026-08-03: 白いダイアログでるけどダサい). all:unset is the same
|
|
2020
|
+
trick .qadd uses; the row is the field, there is no second surface. */
|
|
2021
|
+
#fsRoot .fsbadd{all:unset;box-sizing:border-box;display:flex;align-items:center;gap:9px;width:100%;
|
|
2022
|
+
padding:6px 4px 6px 8px;border-radius:8px;color:var(--ink3);font-size:11.5px;cursor:pointer;
|
|
2023
|
+
opacity:.45;transition:opacity .15s,color .15s}
|
|
2024
|
+
#fsRoot .fsbadd:hover{opacity:1;color:var(--ink)}
|
|
2025
|
+
#fsRoot .fsbadd.editing{opacity:1;color:var(--ink);cursor:text}
|
|
2026
|
+
#fsRoot .fsbadd input{all:unset;display:block;min-width:0;flex:1;color:var(--ink);font:inherit;line-height:18px}
|
|
2027
|
+
#fsRoot .fsbadd input::placeholder{color:var(--ink3)}
|
|
1861
2028
|
#fsRoot .lateridle{display:flex;align-items:center;gap:9px;margin:5px 0 2px;padding:9px 10px;border:1px solid var(--hair);border-radius:8px;color:var(--ink2);font-size:10.5px;line-height:1.45}
|
|
1862
2029
|
#fsRoot .lateridle .msg{flex:1;min-width:0}
|
|
1863
2030
|
#fsRoot .lateridle button{height:26px;padding:0 10px;border:1px solid var(--hair2);border-radius:7px;background:var(--panel);color:var(--ink);font:600 10px/1 var(--mono);cursor:pointer;white-space:nowrap}
|
|
@@ -2987,6 +3154,16 @@
|
|
|
2987
3154
|
body[data-layout="flagship"] .gearpop{left:8px!important;right:8px!important;top:auto!important;
|
|
2988
3155
|
bottom:calc(env(safe-area-inset-bottom) + 8px)!important;width:auto!important;max-width:none!important;
|
|
2989
3156
|
max-height:82dvh!important;border-radius:16px;padding-top:8px}
|
|
3157
|
+
/* The desktop dialog above centres with a transform and lays its nav out sideways —
|
|
3158
|
+
both have to be undone here or the sheet floats off-centre with a 158px rail. */
|
|
3159
|
+
body[data-layout="flagship"] .gearpop.open{transform:none!important;flex-direction:column!important;
|
|
3160
|
+
padding:8px 13px 0!important;gap:0!important}
|
|
3161
|
+
body[data-layout="flagship"] .gearpop .gnav{flex:0 0 auto;flex-direction:row;gap:6px;overflow-x:auto;
|
|
3162
|
+
scrollbar-width:none;border-right:0;border-bottom:1px solid var(--hair);padding:0 0 8px;margin-bottom:10px}
|
|
3163
|
+
body[data-layout="flagship"] .gearpop .gnav::-webkit-scrollbar{display:none}
|
|
3164
|
+
body[data-layout="flagship"] .gearpop .gnav .ggrp{display:none} /* group labels need a column to sit in */
|
|
3165
|
+
body[data-layout="flagship"] .gearpop .gnav button{white-space:nowrap;min-height:38px;padding:0 12px;font-size:13px}
|
|
3166
|
+
body[data-layout="flagship"] .gearpop .cfoot{margin:14px -13px 0;padding:12px 13px}
|
|
2990
3167
|
body[data-layout="flagship"] .gearpop::before{content:"";position:sticky;top:0;z-index:2;display:block;
|
|
2991
3168
|
width:38px;height:5px;margin:0 auto 12px;border-radius:3px;background:var(--hair2)}
|
|
2992
3169
|
body[data-layout="flagship"] .gearpop .csw{width:30px;height:30px}
|
|
@@ -3013,6 +3190,7 @@
|
|
|
3013
3190
|
/* Top bar = chromeless on mobile for ALL themes (Masa 2026-07-18: Midnight で上に変なフレーム
|
|
3014
3191
|
が出ていた → Gradient と同じく枠/面なしに). Gradient already did this; extend to every theme. */
|
|
3015
3192
|
body[data-layout="flagship"] #fsRoot .fsrail{background:transparent;box-shadow:none}
|
|
3193
|
+
body[data-layout="flagship"] #fsRoot.boardmode .fscenter{min-width:0} /* stacked here — no floor to hold */
|
|
3016
3194
|
/* Masa: on mobile drop the drag-to-swap grip (⠿) — it collides with the lane close ✕ and
|
|
3017
3195
|
there's nothing to drag on a phone. Keep the ✕. */
|
|
3018
3196
|
body[data-layout="flagship"] #fsRoot .panelgrip{display:none}
|
|
@@ -3141,6 +3319,8 @@
|
|
|
3141
3319
|
<div id="tobottom" title="Back to latest">↓</div>
|
|
3142
3320
|
<div class="errbar" id="errbar"></div>
|
|
3143
3321
|
<div class="composer-wrap" id="composerWrap">
|
|
3322
|
+
<!-- Drag to set how tall the box may grow; double-click to put it back (Masa 2026-08-03). -->
|
|
3323
|
+
<div class="comphandle" id="compHandle" title="Drag to resize · double-click to reset"></div>
|
|
3144
3324
|
<div class="queue" id="queue"></div>
|
|
3145
3325
|
<!-- Context row (Masa 2026-07-22: 「claude の表記に合わせて」). The Claude app
|
|
3146
3326
|
puts separate rounded chips above the box — repo · branch|worktree · add —
|
|
@@ -3255,12 +3435,6 @@
|
|
|
3255
3435
|
same right column as the fold toggle above it; the menu opens BELOW the gear. -->
|
|
3256
3436
|
<div class="lbrow"><span class="lb">Projects</span>
|
|
3257
3437
|
<button class="ghostbtn morebtn" id="fsMoreBtn" title="Settings"><svg viewBox="0 0 24 24"><circle cx="12" cy="12" r="3.2"/><path d="M19.4 15a1.7 1.7 0 00.34 1.87l.06.06a2 2 0 11-2.83 2.83l-.06-.06a1.7 1.7 0 00-1.87-.34 1.7 1.7 0 00-1 1.56V21a2 2 0 11-4 0v-.09a1.7 1.7 0 00-1-1.56 1.7 1.7 0 00-1.87.34l-.06.06a2 2 0 11-2.83-2.83l.06-.06a1.7 1.7 0 00.34-1.87 1.7 1.7 0 00-1.56-1H3a2 2 0 110-4h.09a1.7 1.7 0 001.56-1 1.7 1.7 0 00-.34-1.87l-.06-.06a2 2 0 112.83-2.83l.06.06a1.7 1.7 0 001.87.34h.09a1.7 1.7 0 001-1.56V3a2 2 0 114 0v.09a1.7 1.7 0 001 1.56h.09a1.7 1.7 0 001.87-.34l.06-.06a2 2 0 112.83 2.83l-.06.06a1.7 1.7 0 00-.34 1.87v.09a1.7 1.7 0 001.56 1H21a2 2 0 110 4h-.09a1.7 1.7 0 00-1.56 1z"/></svg></button>
|
|
3258
|
-
<div class="moremenu" id="fsMoreMenu">
|
|
3259
|
-
<button class="ghostbtn" id="fsCust">Theme</button>
|
|
3260
|
-
<button class="ghostbtn" id="fsConn">Connect MCP</button>
|
|
3261
|
-
<button class="ghostbtn" id="fsHist">History</button>
|
|
3262
|
-
<button class="ghostbtn" id="fsSet">Settings</button>
|
|
3263
|
-
</div>
|
|
3264
3438
|
</div>
|
|
3265
3439
|
<button class="fshamb" id="fsHamburger" title="Open To Do and Review"><svg viewBox="0 0 24 24"><path d="M5 7h14M5 12h14M5 17h14"/></svg><span class="fshamb-lb">Review</span><span class="fshamb-n" id="fsHambN"></span></button>
|
|
3266
3440
|
<div id="fsProjects"></div>
|
|
@@ -3322,6 +3496,10 @@
|
|
|
3322
3496
|
</span>
|
|
3323
3497
|
<div class="fswait" id="fsWait"><!-- waiting badge: "N Needs you / N Review" beside the toggle, board mode only (renderFsWait) --></div>
|
|
3324
3498
|
<div class="lanegrip" id="fsGrip" title="Drag to resize"></div>
|
|
3499
|
+
<!-- Board-view composer host (Masa 2026-08-03). It lives INSIDE .fslane so its width
|
|
3500
|
+
is the board's width by construction — no second copy of that number to keep in
|
|
3501
|
+
step, and it follows the grip when the board is resized. -->
|
|
3502
|
+
<div class="fscompbar" id="fsCompBar"></div>
|
|
3325
3503
|
<!-- mobile-only sheet header (H25②): grab handle + close X; display:none on desktop -->
|
|
3326
3504
|
<div class="lanemtop"><span class="lanemgrab"></span><button class="lanemx" id="fsLaneClose" title="Close" aria-label="Close">✕</button></div>
|
|
3327
3505
|
<div class="lanebody">
|
|
@@ -3565,7 +3743,7 @@ const savedTheme = (() => {
|
|
|
3565
3743
|
if (v !== s) localStorage.setItem('theme', v);
|
|
3566
3744
|
return v;
|
|
3567
3745
|
})();
|
|
3568
|
-
const state = { repos: null, repoHome: '', /* GET /api/repos — the folders a project can point at (Context Bar) */ projects: [], goals: [], tasks: [], act: {}, actAt: {}, todos: {}, chatTurns: {}, active: localStorage.getItem('sel:project') || 'default', editing: null, attach: [], connectCommand: '', models: ['sonnet'], agentModels: null, agentEfforts: null, agents: ['claude-code', 'codex'], auth: null, expanded: new Set(), openThreads: new Set(), queueOrder: {}, queueWaits: {}, externalActivity: [], selectedGoal: null, composerDetachedGoal: null, workflowColumns: {}, wfDraft: null, reviewDefinitions: {}, rdDraft: null, runningCounts: {}, parallelLimits: null, hostReadiness: [], connectedApp: localStorage.getItem('sel:agent') || 'claude-code', reviewOpenGoal: null, reviewApprovals: {}, goalDetailOpen: null, qreplyOpen: new Set(), logCollapsed: localStorage.getItem('logCollapsed') !== '0', logEntries: [], layout: localStorage.getItem('layout') || 'flagship', theme: savedTheme, boardskin: localStorage.getItem('boardskin') || 'auto', summaryPattern: Number(localStorage.getItem('summaryPattern')) || 3, peraSpot: 0, skill: null, skillsCache: null,
|
|
3746
|
+
const state = { repos: null, repoHome: '', /* GET /api/repos — the folders a project can point at (Context Bar) */ projects: [], goals: [], tasks: [], act: {}, actAt: {}, todos: {}, chatTurns: {}, active: localStorage.getItem('sel:project') || 'default', editing: null, attach: [], connectCommand: '', models: ['sonnet'], agentModels: null, agentEfforts: null, agents: ['claude-code', 'codex'], auth: null, expanded: new Set(), openThreads: new Set(), queueOrder: {}, queueWaits: {}, externalActivity: [], selectedGoal: null, composerDetachedGoal: null, workflowColumns: {}, wfDraft: null, reviewDefinitions: {}, rdDraft: null, runningCounts: {}, parallelLimits: null, hostReadiness: [], connectedApp: localStorage.getItem('sel:agent') || 'claude-code', reviewOpenGoal: null, reviewApprovals: {}, goalDetailOpen: null, qreplyOpen: new Set(), logCollapsed: localStorage.getItem('logCollapsed') !== '0', logEntries: [], layout: localStorage.getItem('layout') || 'flagship', theme: savedTheme, boardskin: localStorage.getItem('boardskin') || 'auto', summaryPattern: Number(localStorage.getItem('summaryPattern')) || 3, peraSpot: 0, skill: null, skillsCache: null,
|
|
3569
3747
|
// per-message send overrides set via slash-commands (reset after each send,
|
|
3570
3748
|
// except model which is sticky in localStorage). palette = open command list.
|
|
3571
3749
|
// Scoped per project (msgByProject) so a skill/mode picked in one project can't leak
|
|
@@ -4001,6 +4179,14 @@ function saReviewTitle(g){
|
|
|
4001
4179
|
// reviewSummary.title, so they fall back to the request text unchanged (no regression).
|
|
4002
4180
|
return String(g?.reviewSummary?.title || g?.text || g?.plan?.[0] || g?.reviewSummary?.changed || '').replace(/\s+/g, ' ').trim().slice(0, 90);
|
|
4003
4181
|
}
|
|
4182
|
+
// A reply(follow-up) task's own title IS the reply text ("ok, fix the header too"), not a
|
|
4183
|
+
// task name — showing it as "Doing" reads like a new, unrelated task started (Masa
|
|
4184
|
+
// 2026-07-24). The To Do panel's Doing row already fell back to the goal's own name for a
|
|
4185
|
+
// reply task; the "Now doing" header did not, so the same goal read two different names in
|
|
4186
|
+
// the two places that are supposed to mirror each other. One function, both call sites.
|
|
4187
|
+
function fsDoingName(t, goal){
|
|
4188
|
+
return t.reply ? (goal?.reviewSummary?.check || goal?.plan?.[0] || goal?.text || t.title) : t.title;
|
|
4189
|
+
}
|
|
4004
4190
|
// H23 (2026-07-18) evidence-kind detection. flagship's demo data carries an explicit
|
|
4005
4191
|
// `kind` field; real goals/tasks have no such field, so the review card derives one
|
|
4006
4192
|
// from real signals it already has (a captured proof image, whether the changed files
|
|
@@ -5252,6 +5438,30 @@ function ensureLayoutAnchors(){
|
|
|
5252
5438
|
if (el && !el._anchor) { const a = document.createComment('home:' + id); el.parentNode.insertBefore(a, el); el._anchor = a; }
|
|
5253
5439
|
}
|
|
5254
5440
|
}
|
|
5441
|
+
// Board view can host the composer in a full-width bar under both columns (?cmp trial).
|
|
5442
|
+
// appendChild is guarded on a real parent change so a focused #input is never detached
|
|
5443
|
+
// mid-typing — same rule positionLayout follows.
|
|
5444
|
+
// Where the composer lives. In board view it defaults to the bar under the board; drag
|
|
5445
|
+
// its handle onto the chat column (or back) to move it, and that choice is remembered
|
|
5446
|
+
// (Masa 2026-08-03). Outside board view there is only one place for it.
|
|
5447
|
+
// Narrow screens stack the columns, and the bar is absolutely positioned inside the lane —
|
|
5448
|
+
// putting the composer there pushed it clean off the right of a 390px phone (measured at
|
|
5449
|
+
// x=414..682 on a 390px viewport, with the column's own "New" unreachable too). Phones keep
|
|
5450
|
+
// the composer where it has always been.
|
|
5451
|
+
function composerIsStacked(){
|
|
5452
|
+
return window.matchMedia('(max-width: 760px)').matches;
|
|
5453
|
+
}
|
|
5454
|
+
function composerHost(){
|
|
5455
|
+
const bar = document.getElementById('fsCompBar');
|
|
5456
|
+
const chat = document.getElementById('fsCompwrap');
|
|
5457
|
+
const board = document.getElementById('fsRoot')?.classList.contains('boardmode');
|
|
5458
|
+
if (!bar || !board || composerIsStacked()) return chat;
|
|
5459
|
+
return localStorage.getItem('compHome') === 'chat' ? chat : bar;
|
|
5460
|
+
}
|
|
5461
|
+
function placeComposer(){
|
|
5462
|
+
const host = composerHost(), cw = document.getElementById('composerWrap');
|
|
5463
|
+
if (host && cw && cw.parentNode !== host) host.appendChild(cw);
|
|
5464
|
+
}
|
|
5255
5465
|
function positionLayout(){
|
|
5256
5466
|
ensureLayoutAnchors();
|
|
5257
5467
|
// The settings panel escapes the hidden top bar in flagship (.top{display:none} would
|
|
@@ -5268,7 +5478,7 @@ function positionLayout(){
|
|
|
5268
5478
|
// Composer (queue + input + FREE LIMIT banner) + transient #errbar live at the bottom of
|
|
5269
5479
|
// the ONE center card — desktop and mobile alike (全面つなげた下地: feed scrolls above,
|
|
5270
5480
|
// composer sits on the same connected surface, the queue tray shares that 下地).
|
|
5271
|
-
const host =
|
|
5481
|
+
const host = composerHost(), cw = $('composerWrap');
|
|
5272
5482
|
if (host && cw && cw.parentNode !== host) host.appendChild(cw);
|
|
5273
5483
|
const eb = $('errbar');
|
|
5274
5484
|
if (host && eb && eb.parentNode !== host) host.appendChild(eb);
|
|
@@ -6844,7 +7054,11 @@ const fsUI = {
|
|
|
6844
7054
|
band: Number(localStorage.getItem('fsBand') ?? 0), // theme-6 band choice
|
|
6845
7055
|
revMore: false, // "+N more" fold on the review list (§1.5-8)
|
|
6846
7056
|
todoFold: false, doneOpen: false, rejOpen: false, todoMore: false,
|
|
6847
|
-
|
|
7057
|
+
// Board is the DEFAULT (Masa 2026-08-03: 明らかに kanban view の方がフックが強い).
|
|
7058
|
+
// Only an explicit 'list' — a choice the person made and we stored — opts out; an
|
|
7059
|
+
// empty localStorage means board, so a first visit lands on the view that shows
|
|
7060
|
+
// what Galda is (columns of work moving) instead of a single list.
|
|
7061
|
+
todoView: localStorage.getItem('fsTodoView') === 'list' ? 'list' : 'board',
|
|
6848
7062
|
logOpen: new Set(), logFocus: null, // "View log" targets
|
|
6849
7063
|
repsOpen: new Set(), // reply-group folds
|
|
6850
7064
|
};
|
|
@@ -7018,7 +7232,7 @@ function renderFsFeed(){
|
|
|
7018
7232
|
nd.hidden = false;
|
|
7019
7233
|
nd.innerHTML = `<span class="nd-lb">Doing</span><span class="nd-run"></span>`
|
|
7020
7234
|
+ (n != null ? `<span class="gno nd-no" data-n="${n}">#${n}</span>` : '')
|
|
7021
|
-
+ `<span class="nd-nm">${esc(String(t
|
|
7235
|
+
+ `<span class="nd-nm">${esc(String(fsDoingName(t, g) ?? '').replace(/\s+/g, ' ').slice(0, 90))}</span>`
|
|
7022
7236
|
+ `<span class="nd-pct">${pct}%</span><div class="nd-bar"><i style="width:${pct}%"></i></div>`;
|
|
7023
7237
|
} else { nd.hidden = true; nd.innerHTML = ''; }
|
|
7024
7238
|
}
|
|
@@ -7264,6 +7478,8 @@ function renderFsLane(){
|
|
|
7264
7478
|
const ae = document.activeElement;
|
|
7265
7479
|
if (ae && lane.contains(ae) && ['INPUT', 'TEXTAREA'].includes(ae.tagName)) return;
|
|
7266
7480
|
lane.classList.toggle('boardmode', fsUI.todoView === 'board');
|
|
7481
|
+
lane.closest('#fsRoot')?.classList.toggle('boardmode', fsUI.todoView === 'board');
|
|
7482
|
+
placeComposer();
|
|
7267
7483
|
syncFsTvSeg();
|
|
7268
7484
|
renderFsReview(); renderFsTodo();
|
|
7269
7485
|
if (fsUI.todoView === 'board') renderFsBoard();
|
|
@@ -7284,6 +7500,12 @@ function fsBoardBucket(goal, tasks){
|
|
|
7284
7500
|
if (s === 'done' || s === 'reverted' || s === 'skipped') return 'done'; // terminal; skipped is finished, not in flight (it used to fall through to the doing default)
|
|
7285
7501
|
if (s === 'rejected') return 'rejected'; // Reject = closed. Parked in the list's own Dismissed fold; the board drops it entirely (a closed todo must not linger in Doing — Masa 2026-07-21). Both 'folded' and 'rejected' used to fall through to the doing default.
|
|
7286
7502
|
if (s === 'stacked' || s === 'sending') return 'todo';
|
|
7503
|
+
// NOT moved to 'todo' here, though it was tried (Masa 2026-08-03: キューに入ってる時に
|
|
7504
|
+
// kanban から消える): 'planning' means the agent is already running, and the board must
|
|
7505
|
+
// place a goal in the same column the List does (Masa 2026-07-19, board-list-align).
|
|
7506
|
+
// Breaking that to keep a card visible traded one inconsistency for a worse one. What
|
|
7507
|
+
// actually fixes the disappearance is upstream — the card is inserted locally the moment
|
|
7508
|
+
// you press Enter, and 'running' without startedAt (genuinely queued) already stays here.
|
|
7287
7509
|
if (s === 'running' && !goal?.startedAt) return 'todo'; // goalChip: planned, nothing started yet
|
|
7288
7510
|
// Otherwise the goal is in flight (planning / retesting / running-and-started). BUT a
|
|
7289
7511
|
// goal whose status stuck 'running' after every task already finished is NOT moving —
|
|
@@ -7324,8 +7546,14 @@ function renderFsBoard(){
|
|
|
7324
7546
|
// (#input) — the real create path (no editable rows / no per-column status invention like the
|
|
7325
7547
|
// flagship mock; a new goal enters via the composer). Earlier this was To-Do-only, which read
|
|
7326
7548
|
// as "position is off" against the Artifact (Masa 2026-07-18).
|
|
7327
|
-
|
|
7328
|
-
|
|
7549
|
+
// Only To Do and Backlog take a + (Masa 2026-08-03). The other columns are states the
|
|
7550
|
+
// work arrives in — you cannot put something straight into Doing or Review by asking for
|
|
7551
|
+
// it — so a + there offered an action that does not exist.
|
|
7552
|
+
const CAN_ADD = new Set(['todo', 'later']);
|
|
7553
|
+
const col = (name, stg, count, html, stage) => {
|
|
7554
|
+
const plus = CAN_ADD.has(stage);
|
|
7555
|
+
return `<section class="fsbcol" data-stage="${stage}" style="--stg:${stg}"><div class="fsbcolhd"><span class="fsbdot"></span><span class="fsbcolnm">${name}</span>${stage === 'doing' ? `<button class="fsbcpause" data-fspause="1" aria-pressed="${paused ? 'true' : 'false'}" title="${paused ? 'Resume' : 'Pause'}">${paused ? '<svg viewBox="0 0 24 24"><path d="M8 5v14l11-7z"/></svg>' : '<svg viewBox="0 0 24 24"><rect x="6" y="5" width="4" height="14" rx="1"/><rect x="14" y="5" width="4" height="14" rx="1"/></svg>'}</button>` : ''}<span class="fsbcnt">${count}</span>${plus ? `<button class="fsbcadd" data-fsnew="1" title="New goal"><svg viewBox="0 0 24 24"><path d="M12 5v14M5 12h14"/></svg></button>` : ''}</div><div class="fsbbody">${html || '<div class="fsbempty">—</div>'}</div>${plus ? `<div class="fsbadd" data-fsnew="1" role="button" tabindex="0" title="${stage === 'later' ? 'New Backlog goal' : 'New goal'}"><span class="paplus"></span><span>${stage === 'later' ? 'New' : 'New'}</span></div>` : ''}</section>`;
|
|
7556
|
+
};
|
|
7329
7557
|
// Depth card (CDO, Masa 2026-07-17): #NN + title, colour on the dot only, and a continuous
|
|
7330
7558
|
// stroke + n/m ONLY while the goal is actually in flight. The column already names the
|
|
7331
7559
|
// status, so the card never repeats it. Stopped/failed carry NO bar (a dead bar would claim
|
|
@@ -7380,6 +7608,10 @@ function renderFsBoard(){
|
|
|
7380
7608
|
else if (healthyRun) dot = 'run';
|
|
7381
7609
|
// To Do (idle) and Later carry no dot — the column already says it. Quiet = dimmer title.
|
|
7382
7610
|
const quiet = bkt === 'todo' || bkt === 'done' || bkt === 'later';
|
|
7611
|
+
// Accepted, but the engine has not started it: it stays put and reads faintly rather
|
|
7612
|
+
// than looking as settled as work that is really queued behind a slot (Masa 2026-08-03:
|
|
7613
|
+
// 未確定なら薄く表示されててほしい).
|
|
7614
|
+
const tentative = g.local === true || g.status === 'planning' || (g.status === 'running' && !g.startedAt);
|
|
7383
7615
|
const n = goalNo.get(g.id);
|
|
7384
7616
|
let h = '<div class="lbc-hd">'
|
|
7385
7617
|
+ (dot ? `<span class="lbc-dot ${dot}"></span>` : '')
|
|
@@ -7405,7 +7637,7 @@ function renderFsBoard(){
|
|
|
7405
7637
|
h += `<div class="lbc-meter"><span class="bar"><i style="width:${Math.round(done / total * 100)}%"></i></span><span class="frac">${done}/${total}</span></div>`;
|
|
7406
7638
|
}
|
|
7407
7639
|
}
|
|
7408
|
-
return `<div class="lbc${quiet ? ' is-quiet' : ''}" data-fsgoal="${g.id}">${h}</div>`;
|
|
7640
|
+
return `<div class="lbc${quiet ? ' is-quiet' : ''}${tentative ? ' is-tentative' : ''}" data-fsgoal="${g.id}">${h}</div>`;
|
|
7409
7641
|
};
|
|
7410
7642
|
const no = (g) => goalNo.get(g.id) ?? Infinity;
|
|
7411
7643
|
const bucket = { todo: [], doing: [], needs: [], review: [], done: [], later: [], rejected: [] };
|
|
@@ -7431,8 +7663,23 @@ function renderFsBoard(){
|
|
|
7431
7663
|
+ (bucket.needs.length ? col('Needs you', '#ffca16', bucket.needs.length, bucket.needs.map(goalCard).join(''), 'needs') : '')
|
|
7432
7664
|
+ col('Review', 'var(--review)', reviewDigest.length, reviewDigest.map(reviewCard).join(''), 'review')
|
|
7433
7665
|
+ col('Done', 'var(--ink3)', bucket.done.length, bucket.done.slice(0, 40).map(goalCard).join(''), 'done')
|
|
7434
|
-
+ col('
|
|
7435
|
-
|
|
7666
|
+
+ col('Backlog', 'var(--ink3)', bucket.later.length, bucket.later.map(goalCard).join(''), 'later');
|
|
7667
|
+
// + and New ADD something here (Masa 2026-08-03: + をおしてタスクを追加できるように).
|
|
7668
|
+
// They used to only focus the composer, which is a box on the other side of the screen —
|
|
7669
|
+
// measured on Masa's own window: activeElement really was #input, not readOnly, not
|
|
7670
|
+
// disabled, so it "worked", and nothing whatsoever happened where he was looking.
|
|
7671
|
+
// fsInlineNewRow + fsCreateGoal are the same pair the List view has always used for its
|
|
7672
|
+
// own To Do / Backlog rows; nothing new is invented here.
|
|
7673
|
+
for (const b of el.querySelectorAll('[data-fsnew]')) {
|
|
7674
|
+
const colEl = b.closest('.fsbcol');
|
|
7675
|
+
const stage = colEl?.dataset.stage;
|
|
7676
|
+
const anchor = colEl?.querySelector('.fsbadd[data-fsnew]') || b;
|
|
7677
|
+
b.onclick = (e) => {
|
|
7678
|
+
e.stopPropagation();
|
|
7679
|
+
const later = stage === 'later';
|
|
7680
|
+
fsInlineNewRow(anchor, later ? 'Add to Backlog…' : 'Add to To Do…', (text) => fsCreateGoal(text, later));
|
|
7681
|
+
};
|
|
7682
|
+
}
|
|
7436
7683
|
for (const b of el.querySelectorAll('[data-fspause]')) b.onclick = togglePauseActive; // Doing列の一時停止/再生
|
|
7437
7684
|
for (const c of el.querySelectorAll('[data-fsgoal]')) c.onclick = (e) => {
|
|
7438
7685
|
if (e.target.closest('button, a')) return;
|
|
@@ -7775,7 +8022,7 @@ function renderFsTodo(){
|
|
|
7775
8022
|
const doingRows =
|
|
7776
8023
|
running.filter((t) => !pausedGoalIds.has(t.goalId)).map((t) => {
|
|
7777
8024
|
const goal = pgoals.find((g) => g.id === t.goalId);
|
|
7778
|
-
const title = t
|
|
8025
|
+
const title = fsDoingName(t, goal);
|
|
7779
8026
|
return `<div class="drow"><div class="trow now" data-fsgoalrow="${t.goalId}"><span class="st run"></span>${noSpan(t.goalId)}<span class="tt">${tt(title)}</span><span class="stword run">Running</span><span class="pct num">${progressFor(t)}%</span>${fsActs(t.goalId)}</div>
|
|
7780
8027
|
<div class="tbar"><i style="width:${progressFor(t)}%"></i></div></div>`;
|
|
7781
8028
|
}).join('')
|
|
@@ -7870,8 +8117,8 @@ function renderFsTodo(){
|
|
|
7870
8117
|
${(parkedRows || upRows) ? `<span class="tlcap"${(doingRows || paused) || needsRows || rejected.length ? '' : ' style="padding-top:0"'}>Next up</span>${parkedRows}${upRows}` : ''}
|
|
7871
8118
|
${queued.length > NCAP ? `<div class="morefold" id="fsTodoMore">${fsUI.todoMore ? 'less' : `+${queued.length - NCAP} more`}<span class="g">›</span></div>` : ''}
|
|
7872
8119
|
<div class="qadd" data-fsnew="1" role="button" tabindex="0" title="New goal"><span class="paplus"></span><span>New</span></div>
|
|
7873
|
-
<span class="tlcap">
|
|
7874
|
-
<div class="qadd lateradd" data-fslaternew="1" role="button" tabindex="0" title="New
|
|
8120
|
+
<span class="tlcap">Backlog</span>
|
|
8121
|
+
<div class="qadd lateradd" data-fslaternew="1" role="button" tabindex="0" title="New Backlog goal"><span class="paplus"></span><span>New</span></div>
|
|
7875
8122
|
${laterRows}
|
|
7876
8123
|
</div>
|
|
7877
8124
|
${doneAll.length ? `<div class="doneline${fsUI.doneOpen ? ' open' : ''}" id="fsDoneLine">✓ Done<span class="num">${doneAll.length}</span><span class="fold">▾</span></div>
|
|
@@ -7883,7 +8130,7 @@ function renderFsTodo(){
|
|
|
7883
8130
|
const laterAdd = el.querySelector('.qadd[data-fslaternew]');
|
|
7884
8131
|
if (laterAdd) laterAdd.onclick = (e) => {
|
|
7885
8132
|
e.stopPropagation();
|
|
7886
|
-
fsInlineNewRow(laterAdd, 'Add to
|
|
8133
|
+
fsInlineNewRow(laterAdd, 'Add to Backlog…', (text) => fsCreateGoal(text, true));
|
|
7887
8134
|
};
|
|
7888
8135
|
const dl = el.querySelector('#fsDoneLine');
|
|
7889
8136
|
if (dl) dl.onclick = () => { fsUI.doneOpen = !fsUI.doneOpen; dl.classList.toggle('open', fsUI.doneOpen); el.querySelector('#fsDonelist').hidden = !fsUI.doneOpen; };
|
|
@@ -7971,17 +8218,34 @@ function renderFsTodo(){
|
|
|
7971
8218
|
catch { showErr('Cannot delete a running/finished goal — reply to append instead'); return; }
|
|
7972
8219
|
if (!r.ok) showErr('Cannot delete a running/finished goal — reply to append instead'); else await refresh(); };
|
|
7973
8220
|
}
|
|
8221
|
+
// The card appears the moment you press Enter, in the column you typed it into, faint —
|
|
8222
|
+
// then the server's real goal replaces it (Masa 2026-08-03: 通信通さず・キューに入る感じで).
|
|
8223
|
+
// Before this the whole thing waited on the round-trip, so the row you had just typed
|
|
8224
|
+
// simply was not there for as long as the POST took, which read as "it disappeared".
|
|
8225
|
+
// A negative id can never collide with the server's, and `local` marks it as not-yet-real.
|
|
8226
|
+
let localGoalSeq = 0;
|
|
7974
8227
|
async function fsCreateGoal(text, pending){
|
|
8228
|
+
const tempId = -(++localGoalSeq);
|
|
8229
|
+
const ghost = {
|
|
8230
|
+
id: tempId, projectId: state.active, text,
|
|
8231
|
+
status: pending ? 'pending' : 'stacked', // Backlog / To Do — the columns they were typed into
|
|
8232
|
+
createdAt: new Date().toISOString(), local: true,
|
|
8233
|
+
};
|
|
8234
|
+
state.goals.push(ghost);
|
|
8235
|
+
render();
|
|
8236
|
+
const drop = () => { const i = state.goals.findIndex((g) => g.id === tempId); if (i >= 0) state.goals.splice(i, 1); };
|
|
7975
8237
|
const r = await fetch(withKey('/api/tasks'), { method: 'POST', headers: { 'content-type': 'application/json' },
|
|
7976
8238
|
body: JSON.stringify({ projectId: state.active, text, pr: 'auto', model: state.stickyModel, effort: state.stickyEffort, agent: state.connectedApp, mode: 'auto', pending }) });
|
|
7977
8239
|
if (r.status === 402) {
|
|
7978
8240
|
const data = await r.json().catch(() => ({}));
|
|
8241
|
+
drop(); render();
|
|
7979
8242
|
showBillingBlocked(data.blocked);
|
|
7980
8243
|
return false;
|
|
7981
8244
|
}
|
|
7982
|
-
if (!r.ok) { showErr(pending ? '
|
|
8245
|
+
if (!r.ok) { drop(); render(); showErr(pending ? 'Backlog add failed.' : 'To Do add failed.'); return false; }
|
|
7983
8246
|
clearBillingBlocked();
|
|
7984
8247
|
const goal = await r.json();
|
|
8248
|
+
drop(); // the real one takes its place
|
|
7985
8249
|
if (goal?.id) upsert(state.goals, goal);
|
|
7986
8250
|
render();
|
|
7987
8251
|
return true;
|
|
@@ -8315,13 +8579,22 @@ function renderClawdHero(){
|
|
|
8315
8579
|
e.stopPropagation();
|
|
8316
8580
|
fsUI.todoView = b.dataset.fstv; localStorage.setItem('fsTodoView', fsUI.todoView);
|
|
8317
8581
|
$('fsLane').classList.toggle('boardmode', fsUI.todoView === 'board');
|
|
8582
|
+
$('fsRoot').classList.toggle('boardmode', fsUI.todoView === 'board');
|
|
8583
|
+
window.__fsApplyMidOrder?.();
|
|
8584
|
+
placeComposer();
|
|
8318
8585
|
syncFsTvSeg();
|
|
8319
8586
|
if (fsUI.todoView === 'board') renderFsBoard();
|
|
8320
8587
|
};
|
|
8321
8588
|
syncFsTvSeg();
|
|
8322
8589
|
tgUpdate();
|
|
8323
|
-
|
|
8324
|
-
|
|
8590
|
+
// One press, one dialog (Masa 2026-08-02). The gear used to open a four-item menu
|
|
8591
|
+
// (Theme / Connect MCP / History / Settings) whose only job was to pick a tab — and the
|
|
8592
|
+
// dialog now carries those same four in its own left nav, so the menu was asking people
|
|
8593
|
+
// to choose twice. Press the gear, get Settings; switch inside if you want something else.
|
|
8594
|
+
$('fsMoreBtn').onclick = (e) => {
|
|
8595
|
+
e.stopPropagation();
|
|
8596
|
+
if ($('gearPop').classList.contains('open')) custCancel(); else custOpen();
|
|
8597
|
+
};
|
|
8325
8598
|
const closeMobilePanels = () => {
|
|
8326
8599
|
document.body.classList.remove('laneopen', 'projectsopen');
|
|
8327
8600
|
};
|
|
@@ -8349,11 +8622,7 @@ function renderClawdHero(){
|
|
|
8349
8622
|
// v46 (HANDOFF-v45 §3): the menu entries deep-link into the 4-tab settings panel —
|
|
8350
8623
|
// Theme→THEME / Connect MCP→MCP / History→HISTORY / Settings→SETTINGS.
|
|
8351
8624
|
// stopPropagation: the document click-away handler otherwise closes it in the same event.
|
|
8352
|
-
|
|
8353
|
-
$('fsConn').onclick = (e) => { e.stopPropagation(); $('fsMoreMenu').classList.remove('open'); custOpen('mcp'); };
|
|
8354
|
-
$('fsHist').onclick = (e) => { e.stopPropagation(); $('fsMoreMenu').classList.remove('open'); custOpen('history'); };
|
|
8355
|
-
$('fsSet').onclick = (e) => { e.stopPropagation(); $('fsMoreMenu').classList.remove('open'); custOpen('settings'); };
|
|
8356
|
-
// parallel-runs switcher (§1.5-1/-7): same toggle+click-away shape as fsMoreMenu above.
|
|
8625
|
+
// parallel-runs switcher (§1.5-1/-7): toggle + click-away, the shape the rail's own menus use.
|
|
8357
8626
|
// Row content is rebuilt by renderFsFeed() (running list changes live); the open/closed
|
|
8358
8627
|
// class here is left alone by that rebuild so the popover doesn't snap shut mid-render.
|
|
8359
8628
|
$('fsRunSwitch').onclick = (e) => { e.stopPropagation(); $('fsRunMenu').classList.toggle('open'); };
|
|
@@ -8367,7 +8636,15 @@ function renderClawdHero(){
|
|
|
8367
8636
|
// minReadableCenterW, given however wide the lane currently is — no more flat 280px ceiling
|
|
8368
8637
|
// that stopped rightward drags well short of the window edge (Masa 2026-07-16: "もっと広げ
|
|
8369
8638
|
// られるようにできる?"). Floors at 72 (the min) on a narrow/laned-out window.
|
|
8370
|
-
|
|
8639
|
+
// Two things were wrong here once the board took the middle (Masa 2026-08-03: 左レーンの
|
|
8640
|
+
// 幅を調整できない). It subtracted (a) the LIST view's 640px reading floor even in board
|
|
8641
|
+
// view, where the floor is minCenterWBoard, and (b) the lane's CURRENT width — but the
|
|
8642
|
+
// lane is flex-shrink:1, so it yields when the rail grows. Together they made railMax()
|
|
8643
|
+
// return the 72px floor on any normal window, so every drag collapsed the rail instead
|
|
8644
|
+
// of resizing it (measured at 1440: 1440-52-760-640 = -12 → clamped to 72).
|
|
8645
|
+
// Reserve the lane's MINIMUM instead, and the floor that actually applies.
|
|
8646
|
+
const centreFloor = () => (lane.classList.contains('boardmode') ? minCenterWBoard : minReadableCenterW);
|
|
8647
|
+
const railMax = () => Math.max(72, window.innerWidth - 28 - 24 - 280 - centreFloor());
|
|
8371
8648
|
const saved = Number(localStorage.getItem('fsLaneW'));
|
|
8372
8649
|
const savedRail = Number(localStorage.getItem('fsRailW'));
|
|
8373
8650
|
const restoreLane = saved >= 280 && saved <= 540;
|
|
@@ -8391,10 +8668,45 @@ function renderClawdHero(){
|
|
|
8391
8668
|
// those chips get clipped off the edge of the composer card (Masa 2026-07-16 repro:
|
|
8392
8669
|
// dragged the board grip wide on a 1280px-wide window, "Medium"+send were cut off).
|
|
8393
8670
|
// 440 is the smallest value that doesn't clip on a real laptop width.
|
|
8394
|
-
|
|
8671
|
+
// 460: measured, and the same number the CSS floor uses (#fsRoot.boardmode .fscenter).
|
|
8672
|
+
// They were 440 here and 460 there, so a drag could store a width the CSS then refused —
|
|
8673
|
+
// the lane stayed put while localStorage said otherwise.
|
|
8674
|
+
const minCenterWBoard = 460;
|
|
8395
8675
|
const boardMax = () => Math.max(280, window.innerWidth - 28 - 24
|
|
8396
8676
|
- (document.querySelector('#fsRoot .fsrail')?.getBoundingClientRect().width || defaultRailW) - minCenterWBoard);
|
|
8677
|
+
// Which panel sits next to the rail. In board view the board takes it (Masa 2026-08-03:
|
|
8678
|
+
// 中央レーンに kanban) — the columns are the thing to look at, and the chat reads fine
|
|
8679
|
+
// on the right. An explicit drag (fsMidLeft) always wins over this.
|
|
8680
|
+
const applyMidOrder = () => {
|
|
8681
|
+
const chosen = localStorage.getItem('fsMidLeft');
|
|
8682
|
+
root.classList.toggle('swapmid', chosen ? chosen === 'lane' : fsUI.todoView === 'board');
|
|
8683
|
+
};
|
|
8684
|
+
applyMidOrder();
|
|
8685
|
+
window.__fsApplyMidOrder = applyMidOrder;
|
|
8397
8686
|
const savedBoard = Number(localStorage.getItem('fsLaneWBoard'));
|
|
8687
|
+
// Nobody has dragged it yet → open at boardMax(), not the old 372px stub (Masa
|
|
8688
|
+
// 2026-08-03: 添付画像くらい widely). boardMax() is already the honest ceiling — it
|
|
8689
|
+
// reserves the rail's REAL width plus the 440px the composer needs before its own
|
|
8690
|
+
// chips clip — so "as wide as allowed" is the widest the board can be while the
|
|
8691
|
+
// chat still works. Measured 780px on a 1440px window, which is the width in the
|
|
8692
|
+
// screenshot this came from. It follows the window because it is recomputed on
|
|
8693
|
+
// resize for as long as the width is unsaved; the first drag pins it for good.
|
|
8694
|
+
// Nobody has dragged the board yet → open it as wide as it is ALLOWED to be, not at
|
|
8695
|
+
// the old 372px stub (Masa 2026-08-03). Two things had to be measured rather than
|
|
8696
|
+
// reasoned about:
|
|
8697
|
+
// 1. boardMax() reads the rail's real width, but at boot the rail is not laid out
|
|
8698
|
+
// yet, so it fell back to defaultRailW and returned ~60px too much (measured:
|
|
8699
|
+
// lane 840 where the arithmetic wanted 780).
|
|
8700
|
+
// 2. That surplus came straight out of the chat, which dropped to 380px — and the
|
|
8701
|
+
// composer's own button row then overflowed its container by 72px, which is
|
|
8702
|
+
// exactly the clipping minCenterWBoard exists to prevent (main, for comparison:
|
|
8703
|
+
// lane 372 / chat 848 / overflow 0).
|
|
8704
|
+
// So the width is not computed once and trusted. A ResizeObserver watches the chat
|
|
8705
|
+
// column and gives width back to it whenever it falls under the floor — self-correcting
|
|
8706
|
+
// whatever the boot order turns out to be. The first drag writes fsLaneWBoard and the
|
|
8707
|
+
// observer stands down for good.
|
|
8708
|
+
// Unsaved → set nothing: the CSS fallback (100% minus the chat's min-width) sizes it,
|
|
8709
|
+
// and it follows the window with no listener to keep in step.
|
|
8398
8710
|
if (savedBoard >= 280 && savedBoard <= boardMax()) root.style.setProperty('--lanew-board', `${savedBoard}px`);
|
|
8399
8711
|
let sx = 0, sw = 0, drag = false, boarding = false;
|
|
8400
8712
|
grip.addEventListener('mousedown', (e) => {
|
|
@@ -8404,7 +8716,10 @@ function renderClawdHero(){
|
|
|
8404
8716
|
document.addEventListener('mousemove', (e) => {
|
|
8405
8717
|
if (!drag) return;
|
|
8406
8718
|
const max = boarding ? boardMax() : 540;
|
|
8407
|
-
|
|
8719
|
+
// Grip on the right edge (swapmid) → dragging right widens; on the left edge the
|
|
8720
|
+
// sign flips. Same gesture either way: pull the edge you are holding.
|
|
8721
|
+
const dir = root.classList.contains('swapmid') ? (e.clientX - sx) : (sx - e.clientX);
|
|
8722
|
+
const w = Math.max(280, Math.min(max, sw + dir));
|
|
8408
8723
|
root.style.setProperty(boarding ? '--lanew-board' : '--lanew', `${w}px`);
|
|
8409
8724
|
});
|
|
8410
8725
|
document.addEventListener('mouseup', () => {
|
|
@@ -8475,6 +8790,10 @@ function renderClawdHero(){
|
|
|
8475
8790
|
const side = sideFor(p, e.clientX);
|
|
8476
8791
|
const wantLeft = side === 'left' ? dragged : (dragged === 'center' ? 'lane' : 'center');
|
|
8477
8792
|
root.classList.toggle('swapmid', wantLeft === 'lane');
|
|
8793
|
+
// Persisted from here on (Masa 2026-08-03). It used to be session-only, which was
|
|
8794
|
+
// harmless while the default never moved — now that board mode puts the lane on
|
|
8795
|
+
// the left by default, an unsaved drag back would be undone by the next reload.
|
|
8796
|
+
localStorage.setItem('fsMidLeft', wantLeft);
|
|
8478
8797
|
});
|
|
8479
8798
|
}
|
|
8480
8799
|
})();
|
|
@@ -11961,7 +12280,7 @@ function acceptPalette(){
|
|
|
11961
12280
|
$('input').value = stripLeadingCmd($('input').value);
|
|
11962
12281
|
closePalette();
|
|
11963
12282
|
}
|
|
11964
|
-
const ta = $('input'); ta.style.height = 'auto'; ta.style.height = Math.min(ta.scrollHeight,
|
|
12283
|
+
const ta = $('input'); ta.style.height = 'auto'; ta.style.height = Math.min(ta.scrollHeight, composerMaxH()) + 'px'; ta.focus();
|
|
11965
12284
|
return true;
|
|
11966
12285
|
}
|
|
11967
12286
|
// Close the palette on any click outside the composer.
|
|
@@ -12086,10 +12405,159 @@ $('input').addEventListener('keydown', (e) => {
|
|
|
12086
12405
|
// Up-arrow on an empty input recalls the last sent goal for editing.
|
|
12087
12406
|
if (e.key === 'ArrowUp' && !state.palette && !$('input').value && _lastSend.text) {
|
|
12088
12407
|
e.preventDefault(); const ta = $('input'); ta.value = _lastSend.text;
|
|
12089
|
-
ta.style.height = 'auto'; ta.style.height = Math.min(ta.scrollHeight,
|
|
12408
|
+
ta.style.height = 'auto'; ta.style.height = Math.min(ta.scrollHeight, composerMaxH()) + 'px';
|
|
12090
12409
|
}
|
|
12091
12410
|
});
|
|
12092
|
-
|
|
12411
|
+
// The textarea grows to a cap; the cap is the dial. Default 120 = the value that was
|
|
12412
|
+
// hard-coded in three places before this. Clamped to a floor that still shows one line
|
|
12413
|
+
// and a ceiling of half the window so the board never disappears behind the composer.
|
|
12414
|
+
const COMP_MAX_DEFAULT = 120;
|
|
12415
|
+
function composerMaxH(){
|
|
12416
|
+
const v = parseInt(getComputedStyle(document.documentElement).getPropertyValue('--compmax'), 10);
|
|
12417
|
+
return Number.isFinite(v) && v > 0 ? v : COMP_MAX_DEFAULT;
|
|
12418
|
+
}
|
|
12419
|
+
function setComposerMax(px, persist){
|
|
12420
|
+
const max = Math.round(Math.max(56, Math.min(px, Math.round(window.innerHeight * 0.5))));
|
|
12421
|
+
document.documentElement.style.setProperty('--compmax', `${max}px`);
|
|
12422
|
+
if (persist) localStorage.setItem('compMax', String(max));
|
|
12423
|
+
const ta = document.getElementById('input');
|
|
12424
|
+
if (ta) { ta.style.height = 'auto'; ta.style.height = Math.min(ta.scrollHeight, max) + 'px'; }
|
|
12425
|
+
}
|
|
12426
|
+
// Keep --compbarh in step with what the composer bar actually measures, so the board's
|
|
12427
|
+
// bottom reserve follows it (including while it is being dragged taller).
|
|
12428
|
+
// The board's scrollbar shows itself while you scroll and for a moment after.
|
|
12429
|
+
// Crossing the phone/desktop boundary changes where the composer belongs.
|
|
12430
|
+
(function watchComposerBreakpoint(){
|
|
12431
|
+
const mq = window.matchMedia('(max-width: 760px)');
|
|
12432
|
+
const on = () => { try { placeComposer(); } catch {} };
|
|
12433
|
+
mq.addEventListener ? mq.addEventListener('change', on) : mq.addListener(on);
|
|
12434
|
+
})();
|
|
12435
|
+
(function boardScrollbarHint(){
|
|
12436
|
+
const board = document.getElementById('fsBoard'); if (!board) return;
|
|
12437
|
+
let t = null;
|
|
12438
|
+
board.addEventListener('scroll', () => {
|
|
12439
|
+
board.classList.add('sbon');
|
|
12440
|
+
clearTimeout(t); t = setTimeout(() => board.classList.remove('sbon'), 1400);
|
|
12441
|
+
}, { passive: true });
|
|
12442
|
+
})();
|
|
12443
|
+
(function trackComposerBarHeight(){
|
|
12444
|
+
const bar = document.getElementById('fsCompBar'); if (!bar) return;
|
|
12445
|
+
const sync = () => {
|
|
12446
|
+
// 0 is a real answer, not a glitch to skip: when the composer is dragged over to the
|
|
12447
|
+
// chat this bar is empty, and the board should get that space back instead of holding
|
|
12448
|
+
// a 125px gap under itself for a box that is no longer there.
|
|
12449
|
+
const h = Math.round(bar.getBoundingClientRect().height);
|
|
12450
|
+
document.documentElement.style.setProperty('--compbarh', `${h}px`);
|
|
12451
|
+
};
|
|
12452
|
+
if (window.ResizeObserver) new ResizeObserver(sync).observe(bar);
|
|
12453
|
+
window.addEventListener('resize', sync);
|
|
12454
|
+
sync();
|
|
12455
|
+
})();
|
|
12456
|
+
// Drag the composer between "under the board" and "under Implementing". The handle is
|
|
12457
|
+
// the grab point — the same one that resizes it — so there is one thing to reach for.
|
|
12458
|
+
// A plain HTML5 drag with its own MIME type, like the panel-swap drag already in the rail,
|
|
12459
|
+
// so it can never be confused with a file drop or a card drag.
|
|
12460
|
+
(function initComposerMove(){
|
|
12461
|
+
const cw = document.getElementById('composerWrap'); if (!cw) return;
|
|
12462
|
+
const root = () => document.getElementById('fsRoot');
|
|
12463
|
+
// The whole COLUMN is the target, not the thin bar (Masa 2026-08-03: 移動がうまくいかない
|
|
12464
|
+
// 時がある). Letting go over the board itself — the obvious place to aim for when the
|
|
12465
|
+
// idea is "put it under the kanban" — landed outside the 125px bar and did nothing.
|
|
12466
|
+
const zones = () => [
|
|
12467
|
+
[document.querySelector('#fsRoot .fslane'), 'board', document.getElementById('fsCompBar')],
|
|
12468
|
+
[document.querySelector('#fsRoot .fscenter'), 'chat', document.getElementById('fsCompwrap')],
|
|
12469
|
+
].filter(([el]) => el);
|
|
12470
|
+
|
|
12471
|
+
// Pointer events, not HTML5 drag-and-drop. The DnD version LOOKED right — dragstart
|
|
12472
|
+
// fired and the outlines appeared — but nothing moved in a real browser, and the
|
|
12473
|
+
// synthetic DragEvents I tested with only proved the handlers were wired (Masa: 枠はでる
|
|
12474
|
+
// けど移動が出来ない). The rail and lane grips here have always been mousedown/mousemove/
|
|
12475
|
+
// mouseup; this matches them, and a real mouse can be driven through it in a test.
|
|
12476
|
+
let dragging = false, armed = false, sx = 0, sy = 0, hot = null;
|
|
12477
|
+
const THRESHOLD = 4; // a click on the card must not read as a drag
|
|
12478
|
+
|
|
12479
|
+
const zoneUnder = (x, y) => {
|
|
12480
|
+
for (const z of zones()) {
|
|
12481
|
+
const r = z[0].getBoundingClientRect();
|
|
12482
|
+
if (x >= r.left && x <= r.right && y >= r.top && y <= r.bottom) return z;
|
|
12483
|
+
}
|
|
12484
|
+
return null;
|
|
12485
|
+
};
|
|
12486
|
+
const paint = (target) => {
|
|
12487
|
+
for (const z of zones()) {
|
|
12488
|
+
z[0].classList.toggle('comp-over', z[0] === target);
|
|
12489
|
+
z[2]?.classList.toggle('comp-over', z[0] === target);
|
|
12490
|
+
}
|
|
12491
|
+
};
|
|
12492
|
+
const stop = () => {
|
|
12493
|
+
dragging = armed = false; hot = null;
|
|
12494
|
+
root()?.classList.remove('compdragging');
|
|
12495
|
+
document.body.classList.remove('compdrag-noselect');
|
|
12496
|
+
cw.classList.remove('compdrag-lift');
|
|
12497
|
+
cw.style.transform = '';
|
|
12498
|
+
paint(null);
|
|
12499
|
+
};
|
|
12500
|
+
|
|
12501
|
+
cw.addEventListener('mousedown', (e) => {
|
|
12502
|
+
if (e.button !== 0) return;
|
|
12503
|
+
if (!root()?.classList.contains('boardmode')) return; // nowhere to move it to
|
|
12504
|
+
if (e.target.closest('textarea,button,select,input,a,[contenteditable],#compHandle')) return;
|
|
12505
|
+
armed = true; sx = e.clientX; sy = e.clientY;
|
|
12506
|
+
// Without this the browser starts a text selection from the card and sweeps it across
|
|
12507
|
+
// everything the pointer passes (Masa 2026-08-03: テキストが広範囲で選択される).
|
|
12508
|
+
e.preventDefault();
|
|
12509
|
+
});
|
|
12510
|
+
document.addEventListener('mousemove', (e) => {
|
|
12511
|
+
if (!armed) return;
|
|
12512
|
+
const dx = e.clientX - sx, dy = e.clientY - sy;
|
|
12513
|
+
if (!dragging) {
|
|
12514
|
+
if (Math.abs(dx) + Math.abs(dy) < THRESHOLD) return;
|
|
12515
|
+
dragging = true;
|
|
12516
|
+
root().classList.add('compdragging');
|
|
12517
|
+
document.body.classList.add('compdrag-noselect');
|
|
12518
|
+
cw.classList.add('compdrag-lift');
|
|
12519
|
+
}
|
|
12520
|
+
e.preventDefault();
|
|
12521
|
+
// It follows the cursor. Without this there was no way to tell it had been picked up
|
|
12522
|
+
// (Masa: 掴めてるかわからない) — the lane grip moves its edge, so this moves the card.
|
|
12523
|
+
cw.style.transform = `translate(${Math.round(dx)}px, ${Math.round(dy)}px)`;
|
|
12524
|
+
hot = zoneUnder(e.clientX, e.clientY);
|
|
12525
|
+
paint(hot ? hot[0] : null);
|
|
12526
|
+
});
|
|
12527
|
+
document.addEventListener('mouseup', () => {
|
|
12528
|
+
if (!armed) return;
|
|
12529
|
+
const home = dragging && hot ? hot[1] : null;
|
|
12530
|
+
stop();
|
|
12531
|
+
if (!home) return;
|
|
12532
|
+
localStorage.setItem('compHome', home);
|
|
12533
|
+
placeComposer();
|
|
12534
|
+
});
|
|
12535
|
+
})();
|
|
12536
|
+
(function initComposerResize(){
|
|
12537
|
+
const saved = Number(localStorage.getItem('compMax'));
|
|
12538
|
+
if (saved >= 56) setComposerMax(saved, false);
|
|
12539
|
+
const h = document.getElementById('compHandle'); if (!h) return;
|
|
12540
|
+
let sy = 0, sh = 0, dragging = false;
|
|
12541
|
+
const down = (e) => { dragging = true; sy = (e.touches ? e.touches[0].clientY : e.clientY);
|
|
12542
|
+
sh = composerMaxH(); e.preventDefault(); };
|
|
12543
|
+
const move = (e) => { if (!dragging) return;
|
|
12544
|
+
const y = (e.touches ? e.touches[0].clientY : e.clientY);
|
|
12545
|
+
setComposerMax(sh + (sy - y), false); }; // drag UP = taller
|
|
12546
|
+
const up = () => { if (!dragging) return; dragging = false;
|
|
12547
|
+
localStorage.setItem('compMax', String(composerMaxH())); };
|
|
12548
|
+
h.addEventListener('mousedown', down); h.addEventListener('touchstart', down, { passive: false });
|
|
12549
|
+
document.addEventListener('mousemove', move); document.addEventListener('touchmove', move, { passive: false });
|
|
12550
|
+
document.addEventListener('mouseup', up); document.addEventListener('touchend', up);
|
|
12551
|
+
// Double-click puts it back — the same gesture the project rows already use for "undo
|
|
12552
|
+
// my fiddling", and the handle says so in its title.
|
|
12553
|
+
h.addEventListener('dblclick', () => {
|
|
12554
|
+
localStorage.removeItem('compMax');
|
|
12555
|
+
document.documentElement.style.removeProperty('--compmax');
|
|
12556
|
+
const ta = document.getElementById('input');
|
|
12557
|
+
if (ta) { ta.style.height = 'auto'; ta.style.height = Math.min(ta.scrollHeight, COMP_MAX_DEFAULT) + 'px'; }
|
|
12558
|
+
});
|
|
12559
|
+
})();
|
|
12560
|
+
$('input').addEventListener('input', () => { const ta = $('input'); ta.style.height = 'auto'; ta.style.height = Math.min(ta.scrollHeight, composerMaxH()) + 'px'; localStorage.setItem('draft', ta.value); refreshPalette(); });
|
|
12093
12561
|
$('input').addEventListener('paste', (e) => {
|
|
12094
12562
|
for (const item of e.clipboardData?.items ?? []) {
|
|
12095
12563
|
if (item.type.startsWith('image/')) { e.preventDefault(); uploadFile(item.getAsFile()); }
|
|
@@ -12762,11 +13230,38 @@ function applyCust(){
|
|
|
12762
13230
|
// histHTML/setHTML/mcpHTML port). custTab is session UI state, never persisted; switching
|
|
12763
13231
|
// tabs re-renders the panel WITHOUT touching the edit-session snapshot (custSnap) and
|
|
12764
13232
|
// without closing. =====
|
|
12765
|
-
let custTab = '
|
|
13233
|
+
let custTab = 'settings'; // the gear opens on Settings (Masa 2026-08-02): themes are a once-a-month errand, settings are the reason people press it
|
|
12766
13234
|
function gearTabsHTML(){
|
|
12767
13235
|
return `<div class="ctabs">${[['theme', 'THEME'], ['history', 'HISTORY'], ['settings', 'SETTINGS'], ['mcp', 'MCP']]
|
|
12768
13236
|
.map(([k, l]) => `<button class="ctab${custTab === k ? ' on' : ''}" data-gctab="${k}">${l}</button>`).join('')}</div>`;
|
|
12769
13237
|
}
|
|
13238
|
+
// Flagship = the centred dialog (CDO pattern C): the four tabs become a left rail,
|
|
13239
|
+
// grouped by what they are actually about instead of sitting in one flat strip.
|
|
13240
|
+
// Same data-gctab contract as the tab buttons, so wireGearCommon needs no change.
|
|
13241
|
+
// Theme sits LAST (Masa 2026-08-02): it is the section people visit least, and it was
|
|
13242
|
+
// taking the top slot — the position a reader reads first — from the one they came for.
|
|
13243
|
+
const GEAR_NAV = [
|
|
13244
|
+
['Project', [['settings', 'Settings'], ['history', 'History'], ['mcp', 'MCP']]],
|
|
13245
|
+
// Its own entry under MCP (Masa 2026-08-02) as well as the note at the foot of Settings:
|
|
13246
|
+
// a person who has decided to say something should not have to scroll a settings list to
|
|
13247
|
+
// find where. The heading is Contact, not Feedback — a section labelled the same as its
|
|
13248
|
+
// only item is a label for nothing.
|
|
13249
|
+
['Contact', [['feedback', 'Feedback']]],
|
|
13250
|
+
['Appearance', [['theme', 'Theme']]],
|
|
13251
|
+
];
|
|
13252
|
+
function gearNavHTML(){
|
|
13253
|
+
return `<nav class="gnav">${GEAR_NAV.map(([grp, items]) =>
|
|
13254
|
+
`<div class="ggrp">${grp}</div>` + items.map(([k, l]) =>
|
|
13255
|
+
`<button class="${custTab === k ? 'on' : ''}" data-gctab="${k}">${l}</button>`).join('')).join('')}</nav>`;
|
|
13256
|
+
}
|
|
13257
|
+
// The dialog only exists in flagship; every other layout keeps the anchored popover
|
|
13258
|
+
// and its top tab strip untouched.
|
|
13259
|
+
function gearIsDialog(){ return state.layout === 'flagship'; }
|
|
13260
|
+
function gearShell(html){
|
|
13261
|
+
return gearIsDialog()
|
|
13262
|
+
? gearNavHTML() + `<div class="gcol"><div class="gbody">${html}</div></div>`
|
|
13263
|
+
: gearTabsHTML() + html;
|
|
13264
|
+
}
|
|
12770
13265
|
// HISTORY (v46): approve/dismiss history from goal state — deterministic, zero LLM.
|
|
12771
13266
|
// HONEST DATA NOTE: the engine stamps no decidedAt on approve/dismiss (they only flip
|
|
12772
13267
|
// goal.status — approveGoal/dismissGoal in lib.mjs), and a dismissed goal goes BACK to
|
|
@@ -12834,20 +13329,51 @@ function gearSetHTML(){
|
|
|
12834
13329
|
: acct + freeUsage;
|
|
12835
13330
|
const st = state.stability;
|
|
12836
13331
|
const stability = st ? `<div class="srow"><div class="sl">Verification health<span class="sd">Last 24h · implementation ${st.implementationFailures} · environment ${st.verificationInfrastructure} · inconclusive ${st.timeoutsOrInconclusive} · worker holds ${st.workerHolds}</span></div><button class="optchip" data-gstability="1">Refresh</button></div>` : `<div class="srow"><div class="sl">Verification health<span class="sd">Loading…</span></div></div>`;
|
|
12837
|
-
|
|
12838
|
-
<div class="srow"><div class="sl">Language<span class="sd">Review checklist language for this project</span></div>
|
|
13332
|
+
const langRow = `<div class="srow"><div class="sl">Language<span class="sd">Review checklist language for this project</span></div>
|
|
12839
13333
|
<button class="optchip${lang === 'auto' ? ' on' : ''}" data-gslang="auto">Auto</button>
|
|
12840
13334
|
<button class="optchip${lang === 'en' ? ' on' : ''}" data-gslang="en">EN</button>
|
|
12841
|
-
<button class="optchip${lang === 'ja' ? ' on' : ''}" data-gslang="ja">JA</button></div
|
|
12842
|
-
|
|
12843
|
-
|
|
12844
|
-
|
|
12845
|
-
|
|
12846
|
-
|
|
12847
|
-
|
|
12848
|
-
|
|
12849
|
-
|
|
12850
|
-
|
|
13335
|
+
<button class="optchip${lang === 'ja' ? ' on' : ''}" data-gslang="ja">JA</button></div>`;
|
|
13336
|
+
const aboutRow = `<div class="srow"><div class="sl">About Galda<span class="sd">What it is, what the agent can touch, and what leaves this machine</span></div>
|
|
13337
|
+
<button class="optchip" data-gabout="1">Open</button></div>`;
|
|
13338
|
+
const dataRow = `<div class="srow dis"><div class="sl">Data folder<span class="sd">Runs are stored in the engine's chat-runs folder on the host</span></div></div>`;
|
|
13339
|
+
// Grouped headings + two-across rows (CDO pattern C, Masa 2026-08-02). The flat list
|
|
13340
|
+
// gave a reader no way to tell "settings for this project" from "settings for this
|
|
13341
|
+
// account" — the names were already there in the rows, only the structure was missing.
|
|
13342
|
+
//
|
|
13343
|
+
// A group with nothing in it renders nothing, heading included (§1.5-3 no empty box):
|
|
13344
|
+
// remoteAccessRow/shareUsageDataRow/hostReadinessRow all return '' when the engine has
|
|
13345
|
+
// not configured them, and on such a host the heading alone would be a label for air.
|
|
13346
|
+
const group = (title, rows) => {
|
|
13347
|
+
const body = rows.filter(Boolean).join('');
|
|
13348
|
+
return body ? `<h4>${title}</h4><div class="gtwo">${body}</div>` : '';
|
|
13349
|
+
};
|
|
13350
|
+
return group('Project', [langRow, remoteAccessRow(), shareUsageDataRow()])
|
|
13351
|
+
+ group('Account', [billingRow])
|
|
13352
|
+
// "System", not "This machine": hostReadinessRow() already labels itself
|
|
13353
|
+
// "This machine" (and a test asserts that exact label), so the heading would
|
|
13354
|
+
// have been the same words twice, three rows apart.
|
|
13355
|
+
+ group('System', [stability, aboutRow, hostReadinessRow(), dataRow])
|
|
13356
|
+
+ gearFeedbackHTML();
|
|
13357
|
+
}
|
|
13358
|
+
// The founder's note (Masa 2026-08-02). It is the last thing in SETTINGS because it
|
|
13359
|
+
// is the one row that asks something of the reader rather than offering a control,
|
|
13360
|
+
// and because a person who got this far has already formed an opinion.
|
|
13361
|
+
//
|
|
13362
|
+
// The honest limitation is the point of the copy, not an apology: few users so far
|
|
13363
|
+
// means workflows we have not met, and saying so is what earns the reply. Keep the
|
|
13364
|
+
// promise in it something Leon can actually keep.
|
|
13365
|
+
// `solo` = the Contact tab, where the note is the whole screen rather than the last thing
|
|
13366
|
+
// under a list: no hairline above it (there is nothing above it to separate from) and room
|
|
13367
|
+
// to breathe. Same words either way — one place to change them.
|
|
13368
|
+
function gearFeedbackHTML({ solo = false } = {}){
|
|
13369
|
+
return `<div class="gfeed${solo ? ' gsolo' : ''}">
|
|
13370
|
+
<h4>Feedback</h4>
|
|
13371
|
+
<p>Galda is early. Not many people are using it yet, so there are workflows it handles badly — quite possibly parts of yours.</p>
|
|
13372
|
+
<p>If it doesn't fit the way you work, write to me. I'd rather hear it straight and make it fit than guess.</p>
|
|
13373
|
+
<p class="gsig">— Leon, co-founder</p>
|
|
13374
|
+
<a class="gmail" href="mailto:hello@galda.app?subject=Galda%20feedback">
|
|
13375
|
+
<svg viewBox="0 0 16 16"><rect x="1.5" y="3.5" width="13" height="9" rx="1.5"/><path d="M2 4.5l6 4 6-4"/></svg>hello@galda.app</a>
|
|
13376
|
+
</div>`;
|
|
12851
13377
|
}
|
|
12852
13378
|
// The npx moment, moved to where the intent is.
|
|
12853
13379
|
//
|
|
@@ -13040,36 +13566,16 @@ function hostReadinessRow(){
|
|
|
13040
13566
|
const detail = (missing.length ? missing : checks).map((c) => c.detail).join(' · ');
|
|
13041
13567
|
return `<div class="srow${missing.length ? '' : ' dis'}"><div class="sl">This machine<span class="sd">${esc(detail)}</span></div></div>`;
|
|
13042
13568
|
}
|
|
13043
|
-
|
|
13044
|
-
|
|
13045
|
-
|
|
13046
|
-
|
|
13047
|
-
|
|
13048
|
-
|
|
13049
|
-
|
|
13050
|
-
|
|
13051
|
-
|
|
13052
|
-
|
|
13053
|
-
<div class="srow"><div class="sl">Project rules<span class="sd">What this project tells the AI — read-only · ${esc(names)}</span></div>
|
|
13054
|
-
<button class="optchip${state.rulesOpen ? ' on' : ''}" data-grules="1">${state.rulesOpen ? 'Hide' : 'View'}</button></div>
|
|
13055
|
-
${view}`;
|
|
13056
|
-
}
|
|
13057
|
-
// Fetch the active project's rules files once (cached per project), then rebuild
|
|
13058
|
-
// the settings panel so the row appears. The browser can't read the host FS, so
|
|
13059
|
-
// this is the only way in — GET /api/projects/:id/rules (engine, §1.5a).
|
|
13060
|
-
let rulesFetching = null;
|
|
13061
|
-
async function loadProjectRules(){
|
|
13062
|
-
const pid = state.active;
|
|
13063
|
-
if (state.projectRules[pid] || rulesFetching === pid) return;
|
|
13064
|
-
rulesFetching = pid;
|
|
13065
|
-
try {
|
|
13066
|
-
const r = await fetch(withKey(`/api/projects/${pid}/rules`));
|
|
13067
|
-
state.projectRules[pid] = r.ok ? await r.json() : { files: [] };
|
|
13068
|
-
} catch { state.projectRules[pid] = { files: [] }; }
|
|
13069
|
-
finally { rulesFetching = null; }
|
|
13070
|
-
// Only rebuild if the settings tab is still the one on screen.
|
|
13071
|
-
if ($('gearPop') && custTab === 'settings') buildGearPop();
|
|
13072
|
-
}
|
|
13569
|
+
// NO "Project rules" row (Masa 2026-08-02). It used to print CLAUDE.md / AGENTS.md
|
|
13570
|
+
// read-only in Settings. What ONE-CONVERSATION §1.5a actually decided was a NEGATIVE:
|
|
13571
|
+
// Galda must not grow its own "how we work" field, because the same rules would then
|
|
13572
|
+
// live in two places and drift — the repo already holds the answer. That decision does
|
|
13573
|
+
// not need a control to be honoured. The viewer was the incidental part, and it showed
|
|
13574
|
+
// a person their own file, which they can open in an editor. Removed.
|
|
13575
|
+
//
|
|
13576
|
+
// The engine keeps collectProjectRules (workerRunContext still names the files it found)
|
|
13577
|
+
// and its GET /api/projects/:id/rules, which nothing calls now — engine is a separate
|
|
13578
|
+
// lane, so that endpoint is a follow-up, not a silent edit from here.
|
|
13073
13579
|
// MCP (v46): connection status dot + connectCommand + COPY + quiet explanation.
|
|
13074
13580
|
// The dot is the app's own live engine link (SSE up/down) — the only real signal here.
|
|
13075
13581
|
function gearMcpHTML(){
|
|
@@ -13086,7 +13592,7 @@ function buildGearPop(){
|
|
|
13086
13592
|
// custRender, the sticky Save/Cancel footer belongs to THEME (the live-preview tab);
|
|
13087
13593
|
// the edit session itself stays open across tabs (Esc/outside still Cancel).
|
|
13088
13594
|
if (custTab === 'history') {
|
|
13089
|
-
pop.innerHTML =
|
|
13595
|
+
pop.innerHTML = gearShell(gearHistHTML());
|
|
13090
13596
|
wireGearCommon(pop);
|
|
13091
13597
|
for (const b of pop.querySelectorAll('[data-ghmore]')) b.onclick = () => {
|
|
13092
13598
|
const r = b.closest('.hrow'); const ex = r.classList.toggle('ex'); b.textContent = ex ? 'Less' : 'More';
|
|
@@ -13099,7 +13605,7 @@ function buildGearPop(){
|
|
|
13099
13605
|
return;
|
|
13100
13606
|
}
|
|
13101
13607
|
if (custTab === 'settings') {
|
|
13102
|
-
pop.innerHTML =
|
|
13608
|
+
pop.innerHTML = gearShell(gearSetHTML());
|
|
13103
13609
|
wireGearCommon(pop);
|
|
13104
13610
|
for (const b of pop.querySelectorAll('[data-gslang]')) b.onclick = () => saveLang(b.dataset.gslang);
|
|
13105
13611
|
for (const b of pop.querySelectorAll('[data-grelay]')) b.onclick = () => saveRelayPref(b.dataset.grelay === 'on');
|
|
@@ -13124,18 +13630,20 @@ function buildGearPop(){
|
|
|
13124
13630
|
catch {}
|
|
13125
13631
|
finally { await refresh(); if ($('gearPop')) buildGearPop(); }
|
|
13126
13632
|
};
|
|
13127
|
-
const rulesBtn = pop.querySelector('[data-grules]');
|
|
13128
|
-
if (rulesBtn) rulesBtn.onclick = () => { state.rulesOpen = !state.rulesOpen; buildGearPop(); };
|
|
13129
13633
|
const stabilityBtn = pop.querySelector('[data-gstability]');
|
|
13130
13634
|
if (stabilityBtn) stabilityBtn.onclick = () => { state.stability = null; buildGearPop(); loadStability(); };
|
|
13131
|
-
loadProjectRules();
|
|
13132
13635
|
if (!state.stability) loadStability();
|
|
13133
13636
|
if (!state.relayPref) loadRelayPref();
|
|
13134
13637
|
if (!state.analyticsPref) loadAnalyticsPref();
|
|
13135
13638
|
return;
|
|
13136
13639
|
}
|
|
13640
|
+
if (custTab === 'feedback') {
|
|
13641
|
+
pop.innerHTML = gearShell(gearFeedbackHTML({ solo: true }));
|
|
13642
|
+
wireGearCommon(pop);
|
|
13643
|
+
return;
|
|
13644
|
+
}
|
|
13137
13645
|
if (custTab === 'mcp') {
|
|
13138
|
-
pop.innerHTML =
|
|
13646
|
+
pop.innerHTML = gearShell(gearMcpHTML());
|
|
13139
13647
|
wireGearCommon(pop);
|
|
13140
13648
|
const mc2 = pop.querySelector('[data-gmcpcopy]'); if (mc2) mc2.onclick = () => {
|
|
13141
13649
|
navigator.clipboard?.writeText(state.connectCommand || '').then(() => { mc2.textContent = 'COPIED'; setTimeout(() => { mc2.textContent = 'COPY'; }, 1500); });
|
|
@@ -13206,7 +13714,7 @@ function buildGearPop(){
|
|
|
13206
13714
|
const th = state.theme || 'gradient';
|
|
13207
13715
|
const sec = (on, html) => `<div class="gsec${on ? '' : ' dis'}">${html}</div>`;
|
|
13208
13716
|
// FONT exploration section retired (v45 §1): the brand fonts are final — no chips.
|
|
13209
|
-
pop.innerHTML =
|
|
13717
|
+
pop.innerHTML = gearShell(
|
|
13210
13718
|
`<h4>THEME</h4><div class="swrow">${themeBtns}</div>` +
|
|
13211
13719
|
`<h4>BOARD</h4><div class="swrow">${boardChips}</div>` +
|
|
13212
13720
|
sec(th === 'gradient', `<h4>COLOR</h4><div class="swrow">${arcSw}</div>` +
|
|
@@ -13224,7 +13732,7 @@ function buildGearPop(){
|
|
|
13224
13732
|
(cust.btn === 0 ? `<h4>RING</h4><div class="swrow">${ringSw}</div>` : '') +
|
|
13225
13733
|
// LOGO row removed (v45 §3: code-fixed) · MCP CONNECT moved to the MCP tab (v46)
|
|
13226
13734
|
// sticky footer (v45 §3, flagship .cfoot): Save = persist NOW / Cancel = snapshot restore
|
|
13227
|
-
`<div class="cfoot"><button class="cbtn cancel" data-gccancel="1">Cancel</button><button class="cbtn save" data-gcsave="1">Save</button></div
|
|
13735
|
+
`<div class="cfoot"><button class="cbtn cancel" data-gccancel="1">Cancel</button><button class="cbtn save" data-gcsave="1">Save</button></div>`);
|
|
13228
13736
|
wireGearCommon(pop);
|
|
13229
13737
|
// EDIT SESSION (v45 §3): every handler below is LIVE PREVIEW ONLY — mutate cust/state,
|
|
13230
13738
|
// apply to the DOM, re-render the pop. localStorage is written ONLY in custSave().
|
|
@@ -13256,16 +13764,14 @@ function buildGearPop(){
|
|
|
13256
13764
|
// live previews that never persist and never close the panel; Save is the only moment
|
|
13257
13765
|
// localStorage is written; Cancel / Esc / outside-tap restore the snapshot fully. =====
|
|
13258
13766
|
let custSnap = null;
|
|
13259
|
-
function custPlace(){ // flagship:
|
|
13260
|
-
|
|
13261
|
-
|
|
13262
|
-
|
|
13263
|
-
|
|
13264
|
-
} else pop.style.left = '';
|
|
13767
|
+
function custPlace(){ // flagship: centred dialog (CDO pattern C) — CSS owns the position now,
|
|
13768
|
+
// so the measured rail edge that used to be written here must be cleared, or the inline
|
|
13769
|
+
// left would fight left:50% and the dialog would sit off-centre. Other layouts keep the
|
|
13770
|
+
// .gearwrap anchor and never had an inline left to begin with.
|
|
13771
|
+
$('gearPop').style.left = '';
|
|
13265
13772
|
}
|
|
13266
13773
|
function custOpen(tab){
|
|
13267
|
-
custTab = tab || '
|
|
13268
|
-
state.rulesOpen = false; // rules panel starts collapsed each time the gear opens
|
|
13774
|
+
custTab = tab || 'settings'; // v46 deep-link (flagship __custOpen(tab)): menu entries land on their tab
|
|
13269
13775
|
custSnap = { cust: { ...cust }, theme: state.theme, band: fsUI.band };
|
|
13270
13776
|
buildGearPop(); custPlace();
|
|
13271
13777
|
$('gearPop').classList.add('open');
|
|
@@ -13320,15 +13826,16 @@ document.addEventListener('keydown', (e) => { if (e.key === 'Escape' && $('gearP
|
|
|
13320
13826
|
$('gearBtn').onclick = (e) => { e.stopPropagation(); const p = $('gearPop'); if (p.classList.contains('open')) custCancel(); else custOpen(); };
|
|
13321
13827
|
// outside-tap = Cancel (v45 §3). e.composedPath() is frozen at dispatch, so a swatch that
|
|
13322
13828
|
// buildGearPop() re-rendered (detached) mid-click still reads as INSIDE the pop —
|
|
13323
|
-
// selections never close the panel.
|
|
13324
|
-
//
|
|
13829
|
+
// selections never close the panel. #fsMoreBtn (the rail gear) is excluded so its own
|
|
13830
|
+
// click is never read as an outside tap — otherwise pressing it would close and reopen the
|
|
13831
|
+
// dialog in a single press. #deleteConfirmOverlay is excluded
|
|
13325
13832
|
// too (Masa指示 2026-07-16): it's a separate top-level overlay a settings row can open
|
|
13326
13833
|
// (e.g. Log out) — without this, confirming/cancelling it read as an outside tap and
|
|
13327
13834
|
// closed the settings panel underneath it as a surprise side effect.
|
|
13328
13835
|
document.addEventListener('click', (e) => {
|
|
13329
13836
|
const p = $('gearPop'); if (!p.classList.contains('open')) return;
|
|
13330
13837
|
if (e.composedPath().includes(p)) return;
|
|
13331
|
-
if (e.target.closest('.gearwrap') || e.target.closest('#
|
|
13838
|
+
if (e.target.closest('.gearwrap') || e.target.closest('#fsMoreBtn') || e.target.closest('#deleteConfirmOverlay')) return;
|
|
13332
13839
|
custCancel();
|
|
13333
13840
|
});
|
|
13334
13841
|
// Banff BACKDROP free upload (v3 §6.5): file → dataURL → #themebg. Persists via
|
package/package.json
CHANGED