@galda/cli 0.10.27 → 0.10.32
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 +886 -105
- package/engine/lib.mjs +43 -4
- package/engine/server.mjs +118 -36
- package/package.json +1 -1
package/app/index.html
CHANGED
|
@@ -654,6 +654,18 @@
|
|
|
654
654
|
.delmodal{width:380px}
|
|
655
655
|
.delpreview{border:1px solid var(--hair2);border-radius:10px;padding:10px 12px;background:var(--paper);
|
|
656
656
|
color:var(--ink);font-size:12.5px;line-height:1.5;max-height:120px;overflow:auto;word-break:break-word}
|
|
657
|
+
/* moment ③ New-project paywall dialog (H22). Same narrow .modal as .delmodal,
|
|
658
|
+
but a one-action layout (Masa 2026-07-17 確定 __dlg(2)): Upgrade full-width,
|
|
659
|
+
"Not now" a quiet underlined link centered below it. */
|
|
660
|
+
.flimdlg{width:380px}
|
|
661
|
+
.flimdlg .fd-lb{font:600 9px/1 var(--mono);letter-spacing:.11em;text-transform:uppercase;color:var(--danger);margin-bottom:8px}
|
|
662
|
+
.flimdlg h3{margin:0 0 8px;font-size:15px;letter-spacing:-.01em;color:var(--ink);text-wrap:balance}
|
|
663
|
+
.flimdlg p{margin:0;font-size:12.5px;color:var(--ink2);line-height:1.65}
|
|
664
|
+
.flimdlg .fd-row{display:flex;flex-direction:column-reverse;gap:10px;align-items:stretch;margin-top:16px}
|
|
665
|
+
.flimdlg .fd-cta{width:100%;height:34px}
|
|
666
|
+
.flimdlg .fd-no{border:0;background:0;height:auto;padding:0;color:var(--ink3);font:500 12px/1 var(--ui);
|
|
667
|
+
text-decoration:underline;text-underline-offset:2px;align-self:center;cursor:pointer}
|
|
668
|
+
.flimdlg .fd-no:hover{color:var(--ink2)}
|
|
657
669
|
.howto{margin-top:14px;border:1px solid var(--hair);border-radius:10px;padding:10px 14px;background:var(--paper)}
|
|
658
670
|
.howto b{font-size:12px;color:var(--ink)}
|
|
659
671
|
.howto ol{margin:6px 0 0;padding-left:18px;font-size:11.5px;color:var(--ink2);line-height:1.8}
|
|
@@ -753,6 +765,47 @@
|
|
|
753
765
|
.revundo:hover{color:var(--ink);border-color:var(--ink3)}
|
|
754
766
|
@keyframes rvpop{0%{transform:scale(0)}70%{transform:scale(1.18)}100%{transform:scale(1)}}
|
|
755
767
|
@keyframes rvdraw{to{stroke-dashoffset:0}}
|
|
768
|
+
/* Upgrade celebration — the full-screen reward shown the instant the user
|
|
769
|
+
returns from a completed Stripe checkout (celebrateUpgrade()). Reuses the
|
|
770
|
+
review check-pop vocabulary (green circle + drawn tick, rvpop) scaled up.
|
|
771
|
+
The scrim is a fixed dark wash (same rgba as .overlay), so title/sub use a
|
|
772
|
+
FIXED light color on purpose: no theme token is guaranteed light here
|
|
773
|
+
(--on-accent and --invink are near-black in several themes, --ink would
|
|
774
|
+
vanish on the scrim in light modes). The check is a WHITE tick on the brand
|
|
775
|
+
green disc (the joyful high-signal success mark; white always reads on the
|
|
776
|
+
always-bright green, unlike a theme token). */
|
|
777
|
+
.upcel{position:fixed;inset:0;z-index:200;display:none;place-items:center;cursor:pointer;
|
|
778
|
+
/* .72 (darker than the .55 dialog scrim) so the fixed-white text keeps strong
|
|
779
|
+
contrast even in the light themes, where a lighter wash would grey out. */
|
|
780
|
+
background:rgba(0,0,0,.72);backdrop-filter:blur(7px)}
|
|
781
|
+
.upcel.show{display:grid;animation:upcelin .3s ease}
|
|
782
|
+
.upcel.out{animation:upcelout .34s ease forwards}
|
|
783
|
+
.upcelcard{display:flex;flex-direction:column;align-items:center;gap:16px;text-align:center;padding:0 24px}
|
|
784
|
+
.upcelchk{width:76px;height:76px;border-radius:50%;display:grid;place-items:center;
|
|
785
|
+
background:var(--green);box-shadow:0 0 34px rgba(var(--green-rgb),.55)}
|
|
786
|
+
.upcelchk svg{width:38px;height:38px;stroke:#fff;stroke-width:3;fill:none;stroke-linecap:round;stroke-linejoin:round}
|
|
787
|
+
.upcelchk svg path{stroke-dasharray:30;stroke-dashoffset:30}
|
|
788
|
+
.upcel.show .upcelchk{animation:rvpop .5s cubic-bezier(.2,1.3,.5,1)}
|
|
789
|
+
.upcel.show .upcelchk svg path{animation:updraw .38s .22s ease forwards}
|
|
790
|
+
.upceltitle{font-size:26px;font-weight:700;letter-spacing:-.01em;color:#fff}
|
|
791
|
+
.upcelsub{font-size:14px;line-height:1.5;max-width:300px;color:rgba(255,255,255,.82)}
|
|
792
|
+
@keyframes upcelin{from{opacity:0}to{opacity:1}}
|
|
793
|
+
@keyframes upcelout{from{opacity:1}to{opacity:0}}
|
|
794
|
+
@keyframes updraw{to{stroke-dashoffset:0}}
|
|
795
|
+
@media (prefers-reduced-motion:reduce){
|
|
796
|
+
.upcel.show,.upcel.out{animation:none}
|
|
797
|
+
.upcel.show .upcelchk,.upcel.show .upcelchk svg path{animation:none}
|
|
798
|
+
.upcelchk svg path{stroke-dashoffset:0}
|
|
799
|
+
}
|
|
800
|
+
/* .upcelburst centers a soft green flash pulse (brand color) on the check so
|
|
801
|
+
the pop reads as a burst of light, not just a scale. */
|
|
802
|
+
.upcelburst{position:relative;display:grid;place-items:center;width:76px;height:76px}
|
|
803
|
+
.upcelburst .upcelchk{position:relative;z-index:1}
|
|
804
|
+
.upcelflash{position:absolute;inset:-10px;border-radius:50%;opacity:0;
|
|
805
|
+
background:radial-gradient(circle, rgba(var(--green-rgb),.6) 0%, rgba(var(--green-rgb),0) 70%)}
|
|
806
|
+
.upcel.show .upcelflash{animation:upcelflash .72s ease-out forwards}
|
|
807
|
+
@keyframes upcelflash{0%{transform:scale(.4);opacity:.95}100%{transform:scale(3);opacity:0}}
|
|
808
|
+
@media (prefers-reduced-motion:reduce){ .upcel.show .upcelflash{animation:none;opacity:0} }
|
|
756
809
|
.reqlist{display:flex;flex-direction:column;gap:12px}
|
|
757
810
|
.reqcard{border:1px solid var(--hair2);border-radius:14px;padding:14px 16px;background:var(--paper);transition:border-color .15s,background-color .15s}
|
|
758
811
|
.reqcard.approved{border-color:rgba(var(--green-rgb),.5);background:rgba(var(--green-rgb),.05)}
|
|
@@ -1095,8 +1148,11 @@
|
|
|
1095
1148
|
the layout starts at the three cards. Frame = 14px all around (flagship .app{inset:14px}).
|
|
1096
1149
|
Dev escape hatch: ?dev=1 (or localStorage devbar=1) adds body.devbar to show the bar. */
|
|
1097
1150
|
body[data-layout="flagship"]:not(.devbar) .top{display:none}
|
|
1098
|
-
body[data-layout="flagship"] #fsRoot{flex:1;min-height:0;display:flex;gap:12px;padding:14px;position:relative;
|
|
1099
|
-
--fscenterw:760px} /*
|
|
1151
|
+
body[data-layout="flagship"] #fsRoot{flex:1;min-width:0;min-height:0;display:flex;gap:12px;padding:14px;position:relative;
|
|
1152
|
+
--fscenterw:760px} /* min-width:0 (Masa 2026-07-16): rail/lane are flex-shrink:0 (fixed basis), so without
|
|
1153
|
+
this #fsRoot's own automatic min-width == their full combined width, and #fsRoot refuses to shrink
|
|
1154
|
+
below that even though .fscenter (flex:1;min-width:0) is perfectly able to — the whole page overflowed
|
|
1155
|
+
horizontally once the lane got wide instead of .fscenter just getting narrow. position:relative so .railgrip (a direct child, kept outside .fsrail's
|
|
1100
1156
|
overflow:hidden — see below) can be placed via left:calc(var(--railw)...) and never get clipped.
|
|
1101
1157
|
single source of truth for the reading column — .fsstream (feed) and
|
|
1102
1158
|
.compwrap (composer) both read this var so they can never drift apart (fix: composer was
|
|
@@ -1219,7 +1275,7 @@
|
|
|
1219
1275
|
color:color-mix(in srgb,var(--ink) 24%,transparent);cursor:pointer;
|
|
1220
1276
|
display:grid;place-items:center;position:absolute;z-index:2;font:inherit}
|
|
1221
1277
|
#fsRoot .railtg:hover,#fsRoot .lanetg:hover{background:transparent;color:color-mix(in srgb,var(--ink) 85%,transparent)}
|
|
1222
|
-
#fsRoot .railtg{top:
|
|
1278
|
+
#fsRoot .railtg{top:12px;right:6px} /* 28px button centers its glyph — lowered ~4px onto the Galda logo's visual baseline (Masa via CDO artifact 2026-07-16, not yet in flagship.html) */
|
|
1223
1279
|
#fsRoot.railfold .railtg{right:50%;margin-right:-14px} /* centred in the folded 44px rail — the arrow, not rotation, signals the opening direction (v45 §4) */
|
|
1224
1280
|
#fsRoot.railfold .fsrail{flex-basis:44px;padding:44px 7px 14px} /* top pad 44px — project dots clear the toggle (v45 §2) */
|
|
1225
1281
|
#fsRoot.railfold .fsrail .lb,#fsRoot.railfold .fsproj span,#fsRoot.railfold .fsproj .num,#fsRoot.railfold .fsprojadd .pa-label{display:none}
|
|
@@ -1365,6 +1421,11 @@
|
|
|
1365
1421
|
outright, its original home) — float it just above the input, matching the
|
|
1366
1422
|
legacy layout's "just above the composer" placement (CDO判定 2026-07-16). */
|
|
1367
1423
|
#fsRoot .compwrap>.errbar{position:absolute;left:50%;bottom:calc(100% + 10px);transform:translateX(-50%)}
|
|
1424
|
+
/* Free-limit banner (H22) spans the composer, not a centered content-width pill
|
|
1425
|
+
like a plain showErr toast: the width rule is scoped to .billing so ordinary
|
|
1426
|
+
errors stay centered pills. left/right:0 stretches it to .compwrap's own box
|
|
1427
|
+
(= composer width), and .bx-ask is display:flex so it fills that. */
|
|
1428
|
+
#fsRoot .compwrap>.errbar.billing{left:0;right:0;transform:none}
|
|
1368
1429
|
#fsRoot.lanefold{--fscenterw:1020px} /* folded space flows to the center (§1.5-7) — one var flip keeps feed+composer in lockstep */
|
|
1369
1430
|
/* adopted composer (#composerWrap moves in — same node, same handlers, focus fix intact) */
|
|
1370
1431
|
body[data-layout="flagship"] #composerWrap{padding:0;border-top:0;max-width:none;width:auto;margin:0}
|
|
@@ -1390,10 +1451,11 @@
|
|
|
1390
1451
|
flagship-only 34px override that used to live here is gone, it was redundant
|
|
1391
1452
|
with (and masked that) the base .send circle was still 32px elsewhere. */
|
|
1392
1453
|
/* ---- right lane: Review (top, §1.5-4) + To Do timeline ---- */
|
|
1393
|
-
#fsRoot .fslane{flex:0 0 var(--lanew,372px);display:flex;flex-direction:column;padding:36px 18px 14px;position:relative;
|
|
1454
|
+
#fsRoot .fslane{flex:0 0 var(--lanew,372px);min-width:0;display:flex;flex-direction:column;padding:36px 18px 14px;position:relative;
|
|
1394
1455
|
transition:flex-basis .28s cubic-bezier(.4,0,.2,1),padding .28s}
|
|
1395
1456
|
#fsRoot .fslane.noanim{transition:none}
|
|
1396
|
-
#fsRoot .lanetg{top:
|
|
1457
|
+
#fsRoot .lanetg{top:12px;left:10px} /* 畳みトグル: 左上・帯 (§6.5 確定 tgpos1 — lane padding-top 36px = the band strip).
|
|
1458
|
+
top:12 (was 9) matches .railtg's Galda-baseline lower (Masa via CDO artifact 2026-07-16, not yet in flagship.html). */
|
|
1397
1459
|
#fsRoot.lanefold .lanetg{left:50%;margin-left:-14px} /* no rotation (v45 §4) */
|
|
1398
1460
|
#fsRoot.lanefold .fslane{flex-basis:44px;padding:18px 7px}
|
|
1399
1461
|
#fsRoot.lanefold .fslane>*:not(.lanetg){display:none}
|
|
@@ -1403,6 +1465,162 @@
|
|
|
1403
1465
|
#fsRoot .lanebody{display:flex;flex-direction:column;min-height:0;flex:1;overflow-y:auto;overflow-x:hidden;scrollbar-width:none}
|
|
1404
1466
|
#fsRoot .lanebody::-webkit-scrollbar{display:none}
|
|
1405
1467
|
#fsRevSec:not(:empty){margin-bottom:12px}
|
|
1468
|
+
/* List view: drop the whole lane content below the fold/list/board toggle row, the same 26px
|
|
1469
|
+
clearance the board gets — the summary/"See all" was crammed right under the toggle (Masa 2026-07-18). */
|
|
1470
|
+
#fsRoot .fslane:not(.boardmode) .lanebody{padding-top:26px}
|
|
1471
|
+
|
|
1472
|
+
/* ---- To Do: List/Board view toggle, in the To Do header — same spot both modes (Masa 2026-07-16) ---- */
|
|
1473
|
+
/* List/Board toggle: a FIXED overlay beside the fold icon, in BOTH modes — matching the
|
|
1474
|
+
Artifact's .labseg.inlane exactly (2026-07-17: it's never inline in the To Do header row
|
|
1475
|
+
in the Artifact, always an absolute sibling of .lanetg; a prior commit put it inside
|
|
1476
|
+
#fsTodoHd's flow instead, which pushed it onto its own row below the fold icon whenever
|
|
1477
|
+
the header text above it wrapped/existed — "まだなおってない" screenshot). .lanetg is
|
|
1478
|
+
top:12/left:10/width:28 (center y=26) → this sits at the same 8px gap the Artifact uses
|
|
1479
|
+
after its own fold icon, vertically centered on that same 26px line. */
|
|
1480
|
+
#fsRoot .fstvseg{position:absolute;top:16px;left:42px;z-index:6;display:inline-flex;gap:2px} /* top/left measured directly off the Artifact's own
|
|
1481
|
+
.labseg.inlane (2026-07-17: opened it locally, getBoundingClientRect()) — centers on .lanetg exactly like the fold icon,
|
|
1482
|
+
left:42 = .lanetg's right edge (10+28=38) + a measured 4px gap (an earlier pass guessed 46/8px without measuring the gap itself). */
|
|
1483
|
+
/* Board mode: the "To Do N" title is redundant once a To Do COLUMN exists among the board's
|
|
1484
|
+
own columns — hide it; the toggle+fold are unaffected since they no longer live inside it
|
|
1485
|
+
(Masa 2026-07-16: 看板の時は上のTo Doを消してよい — the column keeps the name). */
|
|
1486
|
+
#fsRoot .fslane.boardmode #fsTodoHd{margin-bottom:0}
|
|
1487
|
+
#fsRoot .fslane.boardmode #fsTodoHd .fstdlabel{display:none}
|
|
1488
|
+
/* toggle tone matches the CDO Artifact's .labseg button exactly (2026-07-17 checked against
|
|
1489
|
+
the Artifact directly, not a guess): ink3 @ opacity .75 at rest, hover/on = filled
|
|
1490
|
+
background + full ink + opacity 1. This is a DIFFERENT convention from the fold buttons
|
|
1491
|
+
(which are glyph-only, no fill) — the segmented toggle is its own component and the
|
|
1492
|
+
Artifact deliberately fills a background for the selected state; a prior commit "fixed"
|
|
1493
|
+
this to match the fold buttons' no-fill rule, which was the wrong reference. */
|
|
1494
|
+
#fsRoot .fstvb{width:22px;height:20px;padding:0;display:grid;place-items:center;border:0;background:transparent;color:var(--ink3);cursor:pointer;border-radius:6px;opacity:.75;font:inherit} /* 22x20/radius:6 — was 20x18/radius:5, an unmeasured guess ("小さい") */
|
|
1495
|
+
#fsRoot .fstvb:hover{background:var(--hover);color:var(--ink2);opacity:1}
|
|
1496
|
+
#fsRoot .fstvb.on{background:var(--hover-s);color:var(--ink2);opacity:1} /* selected: soft fill + softened glyph (--ink2, not near-black --ink) so it doesn't read heavier than its neighbours (Masa 2026-07-18) */
|
|
1497
|
+
#fsRoot .fstvb:focus:not(:focus-visible){outline:none}
|
|
1498
|
+
#fsRoot .fstvb svg{width:13px;height:13px;fill:none;stroke:currentColor;stroke-width:1.7;stroke-linecap:round;stroke-linejoin:round} /* was 12px, guessed */
|
|
1499
|
+
#fsRoot .fstvb svg rect{fill:none}
|
|
1500
|
+
|
|
1501
|
+
/* ---- kanban board: fills the SAME lane slot the To Do list uses (var(--lanew-board), independent
|
|
1502
|
+
from the list's own var(--lanew)) — swapping List⇄Board never resizes the lane or the chat.
|
|
1503
|
+
5+ columns don't fit the lane width, so it scrolls horizontally in place instead of growing. ---- */
|
|
1504
|
+
#fsRoot .fslane.boardmode{flex-basis:var(--lanew-board,372px)}
|
|
1505
|
+
#fsRoot .fslane.boardmode #fsTodoSec .tl,#fsRoot .fslane.boardmode #fsTodoSec .doneline,#fsRoot .fslane.boardmode #fsTodoSec #fsDonelist{display:none}
|
|
1506
|
+
/* Board mode = board ONLY (Masa 2026-07-17): the Review section — proof thumbnails, PR
|
|
1507
|
+
links, approve/dismiss, and its "N ready" summary line — is spoken by the board's own
|
|
1508
|
+
Review column, so it must not also stack on top of the board. This was the same bug the
|
|
1509
|
+
flagship had: hiding only the To Do list bits (above) left #fsRevSec sitting over the
|
|
1510
|
+
board. #fsRevSec holds both the summary count (.revhd) and the rows, so one rule hides
|
|
1511
|
+
both (production has no separate #sumwrap element). */
|
|
1512
|
+
#fsRoot .fslane.boardmode #fsRevSec{display:none}
|
|
1513
|
+
#fsBoard{display:none;gap:14px;flex:1 1 auto;min-width:0;min-height:0;overflow-x:auto;overflow-y:hidden;padding:0 0 4px 0;margin-top:26px;align-items:flex-start;line-height:1.5} /* 26px measured directly off the Artifact's #labBoard (was 8px — guessed, never checked).
|
|
1514
|
+
line-height:1.5 — the Artifact's <body> sets this globally and every board text element inherits it (measured: .labcolnm computes to 18.75px = 12.5×1.5);
|
|
1515
|
+
this app's <body> doesn't set a global line-height, so the same elements were rendering at the browser's "normal" default instead — same
|
|
1516
|
+
container-box position (measured identical, 63px both), but the glyph sits differently inside that box. Scoped here, not on <body>, to avoid
|
|
1517
|
+
a global typography change. */
|
|
1518
|
+
#fsRoot .fslane.boardmode #fsBoard{display:flex}
|
|
1519
|
+
#fsBoard::-webkit-scrollbar{height:8px}#fsBoard::-webkit-scrollbar-thumb{background:var(--hair);border-radius:6px}
|
|
1520
|
+
.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) */
|
|
1521
|
+
.fsbcolhd{display:flex;align-items:center;gap:8px;padding:1px 4px 9px;flex:0 0 auto}
|
|
1522
|
+
.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)}
|
|
1523
|
+
.fsbcolnm{font-size:12.5px;font-weight:600;color:var(--ink);letter-spacing:-.01em}
|
|
1524
|
+
.fsbcnt{font-family:var(--mono);font-size:10px;color:var(--ink3);font-variant-numeric:tabular-nums;background:var(--surf);border-radius:5px;padding:1px 6px}
|
|
1525
|
+
.fsbcadd{margin-left:auto;width:20px;height:20px;display:grid;place-items:center;border:0;background:none;color:var(--ink3);cursor:pointer;border-radius:5px;opacity:0;transition:opacity .12s} /* header "+" — matches flagship .labcadd (hover-reveal add) */
|
|
1526
|
+
.fsbcol:hover .fsbcadd{opacity:.7}
|
|
1527
|
+
.fsbcadd:hover{opacity:1;color:var(--ink);background:var(--hover)}
|
|
1528
|
+
.fsbcadd svg{width:14px;height:14px;fill:none;stroke:currentColor;stroke-width:2;stroke-linecap:round}
|
|
1529
|
+
.fsbbody{flex:1;min-height:12px;overflow-y:auto;padding:1px 2px 6px;display:flex;flex-direction:column;gap:5px}
|
|
1530
|
+
.fsbbody::-webkit-scrollbar{width:7px}.fsbbody::-webkit-scrollbar-thumb{background:var(--hair);border-radius:6px}
|
|
1531
|
+
/* Board card = Depth (CDO, Masa 2026-07-17): a card is SCANNED, not read — #NN + a
|
|
1532
|
+
2-line title, colour rides the dot (never a fill, §5.5), and a continuous stroke + n/m
|
|
1533
|
+
ONLY while the goal is in flight. The column already names the status, so the card never
|
|
1534
|
+
repeats it. (Replaces the old .fsbrow one-liner that just cloned the list row's shape.) */
|
|
1535
|
+
.lbc{padding:9px 10px;border-radius:8px;display:flex;flex-direction:column;gap:6px;transition:box-shadow .12s,background .12s;line-height:1.35}
|
|
1536
|
+
.lbc:hover{background:var(--hover-s)} /* fill-less (light / quiet) hover */
|
|
1537
|
+
/* Board surface skin (Masa 2026-07-18). DEFAULT "auto" lets the THEME decide, because a white
|
|
1538
|
+
board and a black board want different cards (Masa: unifying them dragged white back to an
|
|
1539
|
+
ugly grey box): dark themes (banff/midnight/cinema) get the Artifact card — a near-black-grey
|
|
1540
|
+
fill + hairline; light themes (studio/gradient) get the fill-less card, held by whitespace +
|
|
1541
|
+
hover, like the To Do list. "card"/"quiet"/"frost" force one look in every theme. Switch via
|
|
1542
|
+
__boardskin() / the Customize gear's BOARD row. */
|
|
1543
|
+
body[data-boardskin="auto"][data-theme="banff"] .lbc,
|
|
1544
|
+
body[data-boardskin="auto"][data-theme="midnight"] .lbc,
|
|
1545
|
+
body[data-boardskin="auto"][data-theme="cinema"] .lbc,
|
|
1546
|
+
body[data-boardskin="card"] .lbc{background:color-mix(in srgb,var(--ink) 5%,transparent);box-shadow:0 0 0 1px var(--hair)}
|
|
1547
|
+
body[data-boardskin="auto"][data-theme="banff"] .lbc:hover,
|
|
1548
|
+
body[data-boardskin="auto"][data-theme="midnight"] .lbc:hover,
|
|
1549
|
+
body[data-boardskin="auto"][data-theme="cinema"] .lbc:hover,
|
|
1550
|
+
body[data-boardskin="card"] .lbc:hover{box-shadow:0 0 0 1px var(--hair2)}
|
|
1551
|
+
body[data-boardskin="quiet"] .lbc{background:transparent;box-shadow:none}
|
|
1552
|
+
body[data-boardskin="quiet"] .lbc:hover{background:var(--hover-s);box-shadow:none}
|
|
1553
|
+
body[data-boardskin="frost"] .lbc{background:var(--glass,var(--panel));-webkit-backdrop-filter:blur(6px);backdrop-filter:blur(6px);box-shadow:0 2px 8px rgba(20,23,31,.10),0 0 0 1px var(--hair2)}
|
|
1554
|
+
.lbc-hd{display:flex;align-items:center;gap:6px;min-height:12px}
|
|
1555
|
+
.lbc-n{font:600 10px/1 var(--mono);color:var(--ink3);font-variant-numeric:tabular-nums;letter-spacing:.02em}
|
|
1556
|
+
.lbc-dot{width:6px;height:6px;border-radius:50%;flex:0 0 6px;background:var(--ink3)}
|
|
1557
|
+
.lbc-dot.run{background:var(--green);animation:lbcpulse 2.4s ease-in-out infinite}
|
|
1558
|
+
/* Status dot palette (Masa 2026-07-18): Needs you = amber #ffca16, Review = steel #8390f2,
|
|
1559
|
+
Doing = green (kept), Done/To Do/Later = grey, Failed = red. Fixed hues (like flagship's
|
|
1560
|
+
board), chosen to sit together as one family anchored on the green. */
|
|
1561
|
+
.lbc-dot.rev{background:#8390f2}
|
|
1562
|
+
.lbc-dot.bad{background:var(--danger)}
|
|
1563
|
+
.lbc-dot.intr{background:#ffca16}
|
|
1564
|
+
.lbc-dot.ok{background:var(--ink3);opacity:.6}
|
|
1565
|
+
@keyframes lbcpulse{0%,100%{opacity:.4}50%{opacity:1}}
|
|
1566
|
+
@media (prefers-reduced-motion:reduce){.lbc-dot.run{animation:none}}
|
|
1567
|
+
.lbc-t{font-size:12.6px;line-height:1.35;color:var(--ink);letter-spacing:-.005em;display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical;overflow:hidden}
|
|
1568
|
+
.lbc.is-quiet .lbc-t{color:var(--ink2)} /* Done / Later / To Do: visible, not shouting */
|
|
1569
|
+
.lbc-w{font:600 8.5px/1 var(--mono);letter-spacing:.11em;text-transform:uppercase;margin-left:auto}
|
|
1570
|
+
.lbc-w.bad{color:var(--danger)} .lbc-w.intr{color:#ffca16}
|
|
1571
|
+
.lbc-meter{display:flex;align-items:center;gap:7px}
|
|
1572
|
+
.lbc-meter .bar{flex:1;height:2px;border-radius:2px;background:color-mix(in srgb,var(--ink) 11%,transparent);overflow:hidden}
|
|
1573
|
+
.lbc-meter .bar i{display:block;height:100%;border-radius:2px;background:var(--green);transition:width .3s}
|
|
1574
|
+
.lbc-meter .frac{font:600 9.5px/1 var(--mono);color:var(--ink3);font-variant-numeric:tabular-nums}
|
|
1575
|
+
.fsbempty{color:var(--ink3);font-size:11px;padding:4px 2px}
|
|
1576
|
+
|
|
1577
|
+
/* ---- Waiting badge (CDO, Masa 2026-07-17): columns stay in work-flow order (left→right =
|
|
1578
|
+
the kanban convention), and the two columns that fall off the right edge at 372px — Needs
|
|
1579
|
+
you and Review, i.e. "your turn" — announce themselves on the toggle's OWN row, so no new
|
|
1580
|
+
vertical space is taken. Zero waiting → nothing drawn. Colour is the dot only, no fill
|
|
1581
|
+
(§5.5); a column already scrolled into view sinks (pointing at what's visible is a lie).
|
|
1582
|
+
Board mode only. ---- */
|
|
1583
|
+
#fsRoot .fswait{position:absolute;top:16px;z-index:6;display:none;align-items:center;gap:10px;height:20px}
|
|
1584
|
+
#fsRoot .fslane.boardmode .fswait{display:flex}
|
|
1585
|
+
#fsRoot .fswait button{display:flex;align-items:center;gap:5px;border:0;background:none;padding:0;cursor:pointer;font:400 11px/1 var(--ui);color:var(--ink2);transition:color .12s,opacity .18s}
|
|
1586
|
+
#fsRoot .fswait button:hover{color:var(--ink)} /* no fill — glyph colour only */
|
|
1587
|
+
#fsRoot .fswait button:focus:not(:focus-visible){outline:none}
|
|
1588
|
+
#fsRoot .fswait button.seen{opacity:.34} /* that column is already in view — nothing to point at */
|
|
1589
|
+
#fsRoot .fswait .wd{width:6px;height:6px;border-radius:50%;flex:0 0 6px;background:var(--stg,var(--ink3))}
|
|
1590
|
+
#fsRoot .fswait .wn{font:600 10px/1 var(--mono);font-variant-numeric:tabular-nums;color:var(--ink)}
|
|
1591
|
+
#fsRoot .fswait button.seen .wn{color:var(--ink2)}
|
|
1592
|
+
|
|
1593
|
+
/* ---- + New (CDO decision 5, Masa 2026-07-17): a quiet add-row where + sits in the ● column
|
|
1594
|
+
and New in the text column — aligned to the row's own grid, not a box shoved right. The
|
|
1595
|
+
shared .paplus glyph is 13px (the same span as the ● dot), with the stroke re-centred inside
|
|
1596
|
+
that box so the GLYPH lines up, not the box (Masa aesthetic: align glyphs, not boxes). Click
|
|
1597
|
+
= focus the composer (#input) — the one real create path, no invented row / server call. On
|
|
1598
|
+
the board it lives ONLY in the To Do column: a new goal is a new to-do; you don't "create"
|
|
1599
|
+
into Done/Review/Doing. ---- */
|
|
1600
|
+
#fsRoot .paplus{width:13px;height:13px;flex:0 0 13px;position:relative;display:inline-block}
|
|
1601
|
+
#fsRoot .paplus::before,#fsRoot .paplus::after{content:"";position:absolute;background:currentColor;border-radius:1px}
|
|
1602
|
+
#fsRoot .paplus::before{left:5.85px;top:2px;width:1.3px;height:9px}
|
|
1603
|
+
#fsRoot .paplus::after{top:5.85px;left:2px;height:1.3px;width:9px}
|
|
1604
|
+
/* list rail add-row — padding-left:8px puts + on the .trow ● (8px pad + 13px ● + 9px gap grid) */
|
|
1605
|
+
#fsRoot .qadd{all:unset;box-sizing:border-box;display:flex;align-items:center;gap:9px;width:100%;padding:6px 4px 6px 8px;border-radius:8px;color:var(--ink3);font-size:11.5px;cursor:pointer;opacity:.45;transition:opacity .15s,color .15s}
|
|
1606
|
+
#fsRoot .qadd:hover{opacity:1;color:var(--ink)}
|
|
1607
|
+
/* board To Do column add-row — 2px body pad + 10px card pad = 12px lines + up with the card's #NN */
|
|
1608
|
+
#fsRoot .fsbadd{margin:5px 2px 0;padding:6px 10px;border-radius:7px;border:0;background:none;color:var(--ink3);font:400 11.5px/1 var(--ui);text-align:left;cursor:pointer;display:flex;align-items:center;gap:9px;opacity:.45;transition:opacity .15s,color .15s}
|
|
1609
|
+
#fsRoot .fsbadd:hover{opacity:1;color:var(--ink2)}
|
|
1610
|
+
|
|
1611
|
+
/* ---- chat ⇄ lane panel swap: grab anywhere in the top ~40px of either panel, drop lands
|
|
1612
|
+
EXACTLY on the side the dashed guide showed. Rail is not part of this (Masa 2026-07-16). ---- */
|
|
1613
|
+
#fsRoot .fscenter{order:1}
|
|
1614
|
+
#fsRoot .fslane{order:2}
|
|
1615
|
+
#fsRoot.swapmid .fscenter{order:2}
|
|
1616
|
+
#fsRoot.swapmid .fslane{order:1}
|
|
1617
|
+
#fsRoot .panelgrip{position:absolute;top:10px;right:10px;width:24px;height:24px;display:grid;place-items:center;border:0;background:transparent;color:var(--ink3);opacity:.4;cursor:grab;border-radius:6px;z-index:5;transition:opacity .12s,background .12s} /* right:10 on BOTH .fscenter and .fslane, measured directly off the Artifact (2026-07-17) — it uses the same uniform 10/10 for both grips, no special-cased offset for #fsBandBtn (which is Cinema-theme-only and the Artifact doesn't dodge it either); a prior right:44px guess for .fscenter was the actual bug ("6点が左にずれてる") */
|
|
1618
|
+
#fsRoot .panelgrip:hover{opacity:1;background:var(--hover)}
|
|
1619
|
+
#fsRoot .panelgrip svg{width:14px;height:14px;fill:currentColor}
|
|
1620
|
+
#fsRoot .panel-over{outline:2px dashed color-mix(in srgb,var(--ink) 22%,transparent);outline-offset:-2px;border-radius:14px}
|
|
1621
|
+
#fsRoot .fscenter.top-armed,#fsRoot .fslane.top-armed{cursor:grab}
|
|
1622
|
+
#fsRoot .fscenter.top-armed:active,#fsRoot .fslane.top-armed:active{cursor:grabbing}
|
|
1623
|
+
#panelDropLine{position:absolute;width:0;display:none;border-left:2px dashed var(--ink2);z-index:20;pointer-events:none;border-radius:2px}
|
|
1406
1624
|
#fsRoot .lhd{display:flex;align-items:baseline;gap:8px;margin-bottom:12px}
|
|
1407
1625
|
#fsRoot .lhd .hd{font-size:13.5px;letter-spacing:-.02em}
|
|
1408
1626
|
#fsRoot .lhd .num{font-size:10px;color:var(--ink3)}
|
|
@@ -1628,8 +1846,8 @@
|
|
|
1628
1846
|
#fsRoot .rcard .rnote{display:block;font:500 9.5px/1.4 var(--mono);color:var(--green);margin-top:3px}
|
|
1629
1847
|
/* To Do timeline: order via tone + labels, no axis line (§1.5-4) */
|
|
1630
1848
|
#fsRoot .tl{position:relative}
|
|
1631
|
-
#fsRoot .tlcap{display:block;font-family:var(--mono);font-size:8.5px;letter-spacing:.1em;color:var(--ink3);text-transform:uppercase;padding:13px 0 5px
|
|
1632
|
-
#fsRoot .trow{position:relative;padding:6px 6px 6px
|
|
1849
|
+
#fsRoot .tlcap{display:block;font-family:var(--mono);font-size:8.5px;letter-spacing:.1em;color:var(--ink3);text-transform:uppercase;padding:13px 0 5px 0} /* left 8->0: sit the DOING/Next up/Later labels on the same spine as the section headings (Masa 2026-07-18) */
|
|
1850
|
+
#fsRoot .trow{position:relative;padding:6px 6px 6px 0;border-radius:8px;font-size:12.5px;color:var(--ink);display:flex;align-items:center;gap:9px} /* left 8->0: row dot lines up with the Review/To Do headings' left edge, not 8px inside it (Masa 2026-07-18) */
|
|
1633
1851
|
#fsRoot .trow .tt{color:color-mix(in srgb,var(--ink) 84%,var(--panel))} /* §13.2 soften To Do row name (not pure black); left chat feed keeps --ink */
|
|
1634
1852
|
#fsRoot .trow:hover{background:var(--hover-s)}
|
|
1635
1853
|
#fsRoot .trow.now{font-weight:500}
|
|
@@ -2018,6 +2236,15 @@
|
|
|
2018
2236
|
#fsRoot .cc-who .cs-n{font-size:12px;font-weight:600;color:var(--ink)}
|
|
2019
2237
|
#fsRoot .cc-who .cs-d{font-family:var(--mono);font-size:9.5px;color:var(--ink3);letter-spacing:.05em}
|
|
2020
2238
|
|
|
2239
|
+
.lanemtop{display:none} /* mobile-only lane sheet header (H25②); shown in the max-width:640 block */
|
|
2240
|
+
/* H23 mobile empty-state hero (flagship .m-hero) — shown only on phones when the
|
|
2241
|
+
column is empty (#fsRoot.clawdhero), replacing the Clawd waiting-hero there. */
|
|
2242
|
+
.m-hero{display:none;flex-direction:column;align-items:center;justify-content:center;text-align:center;flex:1 1 auto;min-height:0;gap:15px;padding:24px 20px}
|
|
2243
|
+
.m-hero h2{font-size:20px;font-weight:600;letter-spacing:-.01em;color:var(--ink);margin:0;text-wrap:balance}
|
|
2244
|
+
.m-hero p{font-size:13px;line-height:1.55;color:var(--ink3);margin:0;max-width:290px}
|
|
2245
|
+
.m-chips{display:flex;flex-wrap:wrap;gap:8px;justify-content:center;margin-top:3px}
|
|
2246
|
+
.m-chip{border:1px solid var(--hair);border-radius:999px;padding:8px 14px;font-size:12.5px;color:var(--ink2);background:transparent;cursor:pointer;font-family:inherit}
|
|
2247
|
+
.m-chip:active{background:var(--hover-s)}
|
|
2021
2248
|
/* ============================================================================
|
|
2022
2249
|
FLAGSHIP · MOBILE (Masa 2026-07-09): app shell. The phone view keeps the
|
|
2023
2250
|
implementing column as the primary screen, fixes the composer to the bottom,
|
|
@@ -2026,8 +2253,8 @@
|
|
|
2026
2253
|
@media (max-width: 640px){
|
|
2027
2254
|
html,body{height:100dvh}
|
|
2028
2255
|
body[data-layout="flagship"]{overflow:hidden}
|
|
2029
|
-
body[data-layout="flagship"] #fsRoot{height:100dvh;flex-direction:column;padding:8px;gap:8px;
|
|
2030
|
-
overflow:hidden;position:relative;--fscenterw:100%}
|
|
2256
|
+
body[data-layout="flagship"] #fsRoot{height:100dvh;flex-direction:column;padding:8px 24px max(14px, calc(env(safe-area-inset-bottom) + 10px));gap:8px;
|
|
2257
|
+
overflow:hidden;position:relative;--fscenterw:100%} /* Masa: side margins — narrower cards (8→18px) */
|
|
2031
2258
|
body[data-layout="flagship"].devbar #fsRoot{padding-top:8px}
|
|
2032
2259
|
body[data-layout="flagship"] #fsRoot .fsrail,
|
|
2033
2260
|
body[data-layout="flagship"] #fsRoot.railfold .fsrail{
|
|
@@ -2059,19 +2286,44 @@
|
|
|
2059
2286
|
body[data-layout="flagship"].projectsopen #fsRoot #fsProjects{display:flex}
|
|
2060
2287
|
body[data-layout="flagship"] #fsRoot .fsproj,
|
|
2061
2288
|
body[data-layout="flagship"] #fsRoot .fsprojadd{width:100%;flex:0 0 auto;white-space:nowrap;min-height:40px}
|
|
2062
|
-
body[data-layout="flagship"] #fsRoot .fscenter{flex:1 1
|
|
2289
|
+
body[data-layout="flagship"] #fsRoot .fscenter{flex:1 1 0;width:100%;min-height:0;padding:12px 12px 12px} /* Masa: bottom padding so the composer isn't flush with the card edge */
|
|
2063
2290
|
body[data-layout="flagship"] #fsRoot .chd{margin-bottom:10px}
|
|
2064
|
-
body[data-layout="flagship"] #fsRoot .fsstream{padding-bottom:
|
|
2291
|
+
body[data-layout="flagship"] #fsRoot .fsstream{padding-bottom:12px} /* composer no longer overlaps (it's a sibling now) */
|
|
2065
2292
|
body[data-layout="flagship"] #fsCompwrap{padding-bottom:0}
|
|
2066
|
-
|
|
2067
|
-
|
|
2068
|
-
|
|
2293
|
+
/* Composer = a SEPARATE card, sibling of the feed card (positionLayout mobile branch puts
|
|
2294
|
+
#composerWrap directly under .fscenter in #fsRoot). #fsRoot's `gap` then shows the base
|
|
2295
|
+
(下地) between the feed and the composer — they no longer merge into one white block.
|
|
2296
|
+
As a normal flex child it inherits #fsRoot's side padding = aligned with the feed, in
|
|
2297
|
+
every theme (no more backdrop-filter containing-block double-inset). */
|
|
2298
|
+
/* Composer sits in-flow at the bottom of the ONE center card (the connected 下地): the feed
|
|
2299
|
+
(.fsstream) scrolls above it, the queue tray + input rest on the card's surface. Not fixed,
|
|
2300
|
+
not a separate card — matches the desktop center column. */
|
|
2301
|
+
body[data-layout="flagship"] #composerWrap{position:static;flex:0 0 auto;max-width:none;width:auto;margin:8px 0 0;padding:0}
|
|
2302
|
+
body[data-layout="flagship"] #composerWrap .composer{box-shadow:none} /* input box sits ON the center card — no separate float shadow */
|
|
2303
|
+
/* Masa: composer was too tall — compact it (ChatGPT-like). Controls + send stay on ONE row
|
|
2304
|
+
(nowrap; the model/agent chips shrink first, the send never shrinks). */
|
|
2305
|
+
body[data-layout="flagship"] #fsRoot .composer{padding:8px 10px;gap:6px;border-radius:14px}
|
|
2306
|
+
body[data-layout="flagship"] #fsRoot .composer textarea{font-size:15px;line-height:1.3;padding:1px 2px 0;max-height:90px}
|
|
2307
|
+
body[data-layout="flagship"] #fsRoot .crow{flex-wrap:nowrap;gap:6px}
|
|
2308
|
+
/* collapse the agent button to its icon (drop the "Claude Code" label) so the row fits
|
|
2309
|
+
one line with the send fully visible; model + effort keep their text. */
|
|
2310
|
+
body[data-layout="flagship"] #fsRoot #appbtn>span{display:none}
|
|
2311
|
+
body[data-layout="flagship"] #fsRoot .crow .appbtn,
|
|
2312
|
+
body[data-layout="flagship"] #fsRoot .crow .deliv{height:26px;flex:0 1 auto;min-width:0;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
|
|
2313
|
+
body[data-layout="flagship"] #fsRoot .crow .imgbtn{flex:0 0 auto}
|
|
2314
|
+
body[data-layout="flagship"] #fsRoot .crow .send{flex:0 0 32px;width:32px;height:32px}
|
|
2069
2315
|
body[data-layout="flagship"] #fsRoot .fslane,
|
|
2070
2316
|
body[data-layout="flagship"] #fsRoot.lanefold .fslane{
|
|
2071
|
-
position:fixed;top:0;right:0;height:100dvh;width:min(
|
|
2072
|
-
padding:
|
|
2073
|
-
transition:transform .28s cubic-bezier(.4,0,.2,1);border-radius:16px 0 0 16px}
|
|
2317
|
+
position:fixed;top:0;right:0;height:100dvh;width:min(78vw,340px);min-width:0;display:flex;flex-direction:column;
|
|
2318
|
+
padding:12px 14px max(14px, calc(env(safe-area-inset-bottom) + 12px));z-index:85;transform:translateX(102%);
|
|
2319
|
+
transition:transform .28s cubic-bezier(.4,0,.2,1);border-radius:16px 0 0 16px} /* H25②: 78vw leaves chat peeking on the left; grab-handle header pins above a scrolling body */
|
|
2074
2320
|
body[data-layout="flagship"].laneopen #fsRoot .fslane{transform:translateX(0)}
|
|
2321
|
+
/* H25②: mobile sheet header — center grab handle + close X, pinned above the scrolling lane body */
|
|
2322
|
+
body[data-layout="flagship"] #fsRoot .fslane .lanemtop{display:flex;align-items:center;justify-content:center;position:relative;flex:0 0 auto;height:22px;margin:0 0 8px}
|
|
2323
|
+
body[data-layout="flagship"] #fsRoot .fslane .lanemgrab{width:36px;height:4px;border-radius:3px;background:var(--hair2)}
|
|
2324
|
+
body[data-layout="flagship"] #fsRoot .fslane .lanemx{position:absolute;right:-2px;top:50%;transform:translateY(-50%);width:32px;height:32px;border:0;border-radius:9px;background:transparent;color:var(--ink3);display:grid;place-items:center;font-size:15px;cursor:pointer}
|
|
2325
|
+
body[data-layout="flagship"] #fsRoot .fslane .lanemx:active{color:var(--ink);background:var(--hover-s)}
|
|
2326
|
+
body[data-layout="flagship"] #fsRoot .fslane .lanebody{flex:1 1 auto;min-height:0;overflow-y:auto;-webkit-overflow-scrolling:touch}
|
|
2075
2327
|
body[data-layout="flagship"] #fsRoot.lanefold .fslane>*:not(.lanetg){display:flex}
|
|
2076
2328
|
body[data-layout="flagship"] #fsRoot .fsdim{display:block;position:fixed;inset:0;background:rgba(9,12,18,.32);
|
|
2077
2329
|
opacity:0;pointer-events:none;transition:opacity .22s;z-index:80}
|
|
@@ -2100,22 +2352,77 @@
|
|
|
2100
2352
|
/* ---- settings/customize popover — anchored via JS inline left (rail.right+8px),
|
|
2101
2353
|
which breaks once the rail is a full-width top bar; !important is the only way
|
|
2102
2354
|
to beat an inline style without touching the JS ---- */
|
|
2103
|
-
|
|
2104
|
-
|
|
2355
|
+
/* H25④: settings/customize popover → bottom sheet (grab handle + bigger tap targets;
|
|
2356
|
+
app classes: .csw swatches / .optchip type chips / .cfoot .cbtn / .moremenu .ghostbtn). */
|
|
2357
|
+
body[data-layout="flagship"] .gearpop{left:8px!important;right:8px!important;top:auto!important;
|
|
2358
|
+
bottom:calc(env(safe-area-inset-bottom) + 8px)!important;width:auto!important;max-width:none!important;
|
|
2359
|
+
max-height:82dvh!important;border-radius:16px;padding-top:8px}
|
|
2360
|
+
body[data-layout="flagship"] .gearpop::before{content:"";position:sticky;top:0;z-index:2;display:block;
|
|
2361
|
+
width:38px;height:5px;margin:0 auto 12px;border-radius:3px;background:var(--hair2)}
|
|
2362
|
+
body[data-layout="flagship"] .gearpop .csw{width:30px;height:30px}
|
|
2363
|
+
body[data-layout="flagship"] .gearpop .optchip{padding:9px 15px;font-size:12.5px}
|
|
2364
|
+
body[data-layout="flagship"] .gearpop .cfoot .cbtn{height:38px;padding:0 20px;font-size:12.5px}
|
|
2365
|
+
body[data-layout="flagship"] #fsRoot .moremenu .ghostbtn{min-height:46px;display:flex;align-items:center;padding:0 14px;font-size:14px;border-radius:10px}
|
|
2366
|
+
/* H23: centered top bar — gear (settings ⚙) left · project name centered + caret · Review (☰) right. */
|
|
2367
|
+
body[data-layout="flagship"] #fsRoot .fsrail{position:relative}
|
|
2368
|
+
body[data-layout="flagship"] #fsRoot .lbrow{order:0;margin-left:0;margin-right:auto}
|
|
2369
|
+
body[data-layout="flagship"] #fsRoot .fshamb{order:9;margin-left:0}
|
|
2370
|
+
body[data-layout="flagship"] #fsRoot .fsprojbtn{position:absolute;left:50%;transform:translateX(-50%);
|
|
2371
|
+
max-width:62%;justify-content:center;margin:0}
|
|
2372
|
+
/* Review/To Do opener (right) = plain ☰ icon (Masa 2026-07-18: ボタン化はやめ、三本線だけ・
|
|
2373
|
+
タップで To Do/Review ドロワー). The base .fshamb rule above already renders the ☰ svg. */
|
|
2374
|
+
body[data-layout="flagship"] #fsRoot .fshamb .fshamb-lb,
|
|
2375
|
+
body[data-layout="flagship"] #fsRoot .fshamb .fshamb-n{display:none}
|
|
2376
|
+
/* Top bar = chromeless on mobile for ALL themes (Masa 2026-07-18: Midnight で上に変なフレーム
|
|
2377
|
+
が出ていた → Gradient と同じく枠/面なしに). Gradient already did this; extend to every theme. */
|
|
2378
|
+
body[data-layout="flagship"] #fsRoot .fsrail{background:transparent;box-shadow:none}
|
|
2379
|
+
/* Masa: on mobile drop the drag-to-swap grip (⠿) — it collides with the lane close ✕ and
|
|
2380
|
+
there's nothing to drag on a phone. Keep the ✕. */
|
|
2381
|
+
body[data-layout="flagship"] #fsRoot .panelgrip{display:none}
|
|
2382
|
+
/* Masa: the QUESTION card text ran under the top-right trash icon at some lengths — reserve
|
|
2383
|
+
the icon's gutter so the label/question never collide with it. */
|
|
2384
|
+
body[data-layout="flagship"] #fsRoot .cc-ask-lb,
|
|
2385
|
+
body[data-layout="flagship"] #fsRoot .cc-ask-q{padding-right:30px}
|
|
2386
|
+
/* H23: mobile empty state → hero (chips), replacing the Clawd waiting-hero on phones. */
|
|
2387
|
+
body[data-layout="flagship"] #fsRoot.clawdhero #fsClawdHero{display:none}
|
|
2388
|
+
body[data-layout="flagship"] #fsRoot.clawdhero .m-hero{display:flex}
|
|
2389
|
+
/* H23: chat-first — hide the "Implementing" header (the feed is the screen); slim the now-doing line. */
|
|
2390
|
+
body[data-layout="flagship"] #fsRoot .chd{display:none}
|
|
2391
|
+
body[data-layout="flagship"] #fsRoot .nowdoing{background:transparent;border:0;box-shadow:none;border-radius:0;
|
|
2392
|
+
margin:0 0 4px;padding:0 2px 10px;gap:9px;border-bottom:1px solid var(--hair)}
|
|
2393
|
+
/* H25③: See-all ledger → scrim + inset panel (a full-bleed sheet let translucent themes show through). */
|
|
2394
|
+
body[data-layout="flagship"] #seeall{background:rgba(9,12,20,.55)}
|
|
2395
|
+
body[data-layout="flagship"] #seeall .sa-panel{width:96%!important;max-width:96%!important;
|
|
2396
|
+
max-height:calc(100dvh - env(safe-area-inset-top) - 22px)!important;
|
|
2397
|
+
margin-top:calc(env(safe-area-inset-top) + 10px)!important;border-radius:16px!important}
|
|
2105
2398
|
}
|
|
2106
|
-
/* Billing UI (H16) — self-serve plan. Tokens only; red is a line/label,
|
|
2107
|
-
a fill (DESIGN-RULES §5.6)
|
|
2108
|
-
|
|
2109
|
-
|
|
2110
|
-
.bx-cta{height:29px;display:inline-flex;align-items:center;justify-content:center;gap:4px;padding:0 13px;border-radius:8px;background:var(--ink);color:var(--
|
|
2399
|
+
/* Billing UI (H16/H22) — self-serve plan. Tokens only; red is a line/label,
|
|
2400
|
+
never a fill (DESIGN-RULES §5.6), and it is --danger (the app's real token —
|
|
2401
|
+
there is no --red here). The CTA text is --invink (defined per theme, always
|
|
2402
|
+
readable on --ink) so it needs no per-theme opaque pin. */
|
|
2403
|
+
.bx-cta{height:29px;display:inline-flex;align-items:center;justify-content:center;gap:4px;padding:0 13px;border-radius:8px;background:var(--ink);color:var(--invink);font:550 11.5px/1 var(--ui);border:0;cursor:pointer}
|
|
2111
2404
|
.bx-cta:hover{opacity:.9}
|
|
2112
|
-
|
|
2113
|
-
|
|
2405
|
+
/* Masa 2026-07-17: tapping Upgrade jumps straight to Stripe's own page — no
|
|
2406
|
+
transition reads as "did that even do anything?". A spinner in the button
|
|
2407
|
+
itself (not a separate overlay) confirms the tap landed during the request
|
|
2408
|
+
round-trip, right up until the redirect actually happens. Reuses the
|
|
2409
|
+
existing @keyframes spin (line ~130) rather than inventing a new one. */
|
|
2410
|
+
.bx-cta.loading{cursor:default;opacity:.85}
|
|
2411
|
+
.bx-spin{display:none}
|
|
2412
|
+
.bx-cta.loading .bx-spin{display:inline-block;width:11px;height:11px;border-radius:50%;border:1.5px solid rgba(255,255,255,.4);border-top-color:#fff;animation:spin .7s linear infinite;margin-right:1px}
|
|
2413
|
+
body[data-theme="banff"] .bx-cta.loading .bx-spin,body[data-theme="glass"] .bx-cta.loading .bx-spin{border-color:rgba(16,16,20,.35);border-top-color:#101014}
|
|
2414
|
+
/* Price = figure only, currency small (DESIGN-RULES §5.5 / flagship .fl-pm):
|
|
2415
|
+
the $ is set to 74% of the figure and optically raised so its top meets the
|
|
2416
|
+
digits' cap line (§5.5 グリフで揃える), not sunk to the baseline. No /mo on
|
|
2417
|
+
the button — the number is the message. This is the ONE place a price is
|
|
2418
|
+
styled; every screen inherits it via fmtPrice(). */
|
|
2419
|
+
.bx-pm{font-family:var(--mono);font-size:10.5px;opacity:.82;margin-left:2px;font-variant-numeric:tabular-nums}
|
|
2420
|
+
.bx-pm .cur{margin-right:.5px;font-size:.74em;vertical-align:.12em}
|
|
2114
2421
|
/* ① free-limit prompt — rendered in #errbar with .billing neutralizing its error fill */
|
|
2115
2422
|
.errbar.billing{background:transparent!important;backdrop-filter:none!important;box-shadow:none!important;border:0!important;padding:0!important}
|
|
2116
|
-
.bx-ask{border:1px solid var(--hair);border-left:2px solid var(--
|
|
2423
|
+
.bx-ask{border:1px solid var(--hair);border-left:2px solid var(--danger);border-radius:11px;padding:10px 13px;display:flex;align-items:center;gap:14px;background:var(--panel)}
|
|
2117
2424
|
.bx-qt{flex:1;text-align:left}
|
|
2118
|
-
.bx-lb{font:600 9px/1 var(--mono);letter-spacing:.11em;text-transform:uppercase;color:var(--
|
|
2425
|
+
.bx-lb{font:600 9px/1 var(--mono);letter-spacing:.11em;text-transform:uppercase;color:var(--danger)}
|
|
2119
2426
|
.bx-q{font-size:13px;color:var(--ink);margin-top:4px}
|
|
2120
2427
|
.bx-act{flex:none}
|
|
2121
2428
|
/* neutral ask (not a limit/error — e.g. the sign-in nudge): same card, --blue
|
|
@@ -2123,6 +2430,13 @@
|
|
|
2123
2430
|
(§7決定ログ「AI確認:色は青系」) rather than inventing a new color. */
|
|
2124
2431
|
.bx-ask.info{border-left-color:var(--blue)}
|
|
2125
2432
|
.bx-ask.info .bx-lb{color:var(--blue)}
|
|
2433
|
+
/* free-tier pending counter in the To Do header (H22): mono figure, dim slash,
|
|
2434
|
+
--danger at the cap. Built only on the free plan (see renderFsTodo), so it
|
|
2435
|
+
needs no display toggle here — it simply replaces the lane-total .num. */
|
|
2436
|
+
.fcount{font-family:var(--mono);font-size:11px;font-variant-numeric:tabular-nums;color:var(--ink3)}
|
|
2437
|
+
.fcount .fc-l{opacity:.62}
|
|
2438
|
+
.fcount.cap{color:var(--danger)}
|
|
2439
|
+
.fcount.cap .fc-l{opacity:.75}
|
|
2126
2440
|
/* ② post-checkout return bar */
|
|
2127
2441
|
.bx-cobar{box-shadow:inset 0 0 0 1px var(--hair2);border-radius:11px;padding:10px 13px;display:flex;align-items:center;gap:10px;font-size:12.5px;margin:0 auto 12px;width:100%;max-width:var(--fscenterw,760px)}
|
|
2128
2442
|
.bx-cobar[hidden]{display:none}
|
|
@@ -2287,7 +2601,7 @@
|
|
|
2287
2601
|
<button class="ghostbtn" id="fsSet">Settings</button>
|
|
2288
2602
|
</div>
|
|
2289
2603
|
</div>
|
|
2290
|
-
<button class="fshamb" id="fsHamburger" title="Open To Do and Review"><svg viewBox="0 0 24 24"><path d="M5 7h14M5 12h14M5 17h14"/></svg></button>
|
|
2604
|
+
<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>
|
|
2291
2605
|
<div id="fsProjects"></div>
|
|
2292
2606
|
</div>
|
|
2293
2607
|
<!-- .railgrip lives here (sibling of .fsrail, not a child — see the CSS comment above)
|
|
@@ -2295,6 +2609,7 @@
|
|
|
2295
2609
|
overflow:hidden (Masa 2026-07-16). -->
|
|
2296
2610
|
<div class="railgrip" id="fsRgrip" title="Drag to resize"></div>
|
|
2297
2611
|
<div class="fscard fscenter">
|
|
2612
|
+
<button class="panelgrip" draggable="true" title="Drag to swap chat ⇄ lane"><svg viewBox="0 0 24 24"><circle cx="9" cy="6" r="1.6"/><circle cx="15" cy="6" r="1.6"/><circle cx="9" cy="12" r="1.6"/><circle cx="15" cy="12" r="1.6"/><circle cx="9" cy="18" r="1.6"/><circle cx="15" cy="18" r="1.6"/></svg></button>
|
|
2298
2613
|
<div id="fsBand"></div>
|
|
2299
2614
|
<button id="fsBandBtn" title="Change the band image"><svg viewBox="0 0 24 24"><rect x="3" y="5" width="18" height="14" rx="2"/><path d="M3 15l5-4 4 3 4-5 5 6"/></svg></button>
|
|
2300
2615
|
<div id="fsBandPop"><div class="bandgrid">
|
|
@@ -2319,6 +2634,8 @@
|
|
|
2319
2634
|
<div class="nowdoing" id="fsNowDoing" hidden></div>
|
|
2320
2635
|
<!-- Clawd HERO (§4.5 初期状態): filled by JS (CLAWD generator), shown via #fsRoot.clawdhero -->
|
|
2321
2636
|
<div id="fsClawdHero" aria-hidden="true"></div>
|
|
2637
|
+
<!-- H23 mobile empty-state hero (flagship .m-hero) — chips fill the composer (#input) -->
|
|
2638
|
+
<div class="m-hero"><h2>What should we build?</h2><p>Describe a goal in plain language. Claude Code runs it, checks its own work, and only calls it done with proof.</p><div class="m-chips"><button class="m-chip">Fix the failing test in lib.mjs</button><button class="m-chip">Add a token-usage counter</button><button class="m-chip">Summarize the open PRs</button></div></div>
|
|
2322
2639
|
<div class="fsstream"><div id="fsFeed"></div></div>
|
|
2323
2640
|
<div class="compwrap" id="fsCompwrap">
|
|
2324
2641
|
<!-- q3 chat-bottom askbar: DOM path for the "flowed away" dynamic rule (§6.5) — hidden;
|
|
@@ -2329,11 +2646,20 @@
|
|
|
2329
2646
|
</div>
|
|
2330
2647
|
<div class="fsdim" id="fsLaneDim"></div>
|
|
2331
2648
|
<div class="fscard fslane" id="fsLane">
|
|
2649
|
+
<button class="panelgrip" draggable="true" title="Drag to swap chat ⇄ lane"><svg viewBox="0 0 24 24"><circle cx="9" cy="6" r="1.6"/><circle cx="15" cy="6" r="1.6"/><circle cx="9" cy="12" r="1.6"/><circle cx="15" cy="12" r="1.6"/><circle cx="9" cy="18" r="1.6"/><circle cx="15" cy="18" r="1.6"/></svg></button>
|
|
2332
2650
|
<button class="lanetg" id="fsLaneTg" title="Fold the lane"></button><!-- panel icon filled by tgUpdate (v45 §4) -->
|
|
2651
|
+
<span class="fstvseg" id="fsTvSeg"><!-- List/Board toggle — fixed beside the fold icon in BOTH modes, matching the Artifact (not inline in the To Do header) -->
|
|
2652
|
+
<button class="fstvb" data-fstv="list" title="List"><svg viewBox="0 0 24 24"><path d="M4 6h16M4 12h16M4 18h10"/></svg></button>
|
|
2653
|
+
<button class="fstvb" data-fstv="board" title="Board"><svg viewBox="0 0 24 24"><rect x="3" y="4" width="5" height="16" rx="1"/><rect x="10" y="4" width="5" height="16" rx="1"/><rect x="17" y="4" width="4" height="16" rx="1"/></svg></button>
|
|
2654
|
+
</span>
|
|
2655
|
+
<div class="fswait" id="fsWait"><!-- waiting badge: "N Needs you / N Review" beside the toggle, board mode only (renderFsWait) --></div>
|
|
2333
2656
|
<div class="lanegrip" id="fsGrip" title="Drag to resize"></div>
|
|
2657
|
+
<!-- mobile-only sheet header (H25②): grab handle + close X; display:none on desktop -->
|
|
2658
|
+
<div class="lanemtop"><span class="lanemgrab"></span><button class="lanemx" id="fsLaneClose" title="Close" aria-label="Close">✕</button></div>
|
|
2334
2659
|
<div class="lanebody">
|
|
2335
2660
|
<div id="fsRevSec"></div>
|
|
2336
2661
|
<div id="fsTodoSec"></div>
|
|
2662
|
+
<div id="fsBoard"></div>
|
|
2337
2663
|
</div>
|
|
2338
2664
|
</div>
|
|
2339
2665
|
</div>
|
|
@@ -2510,6 +2836,21 @@
|
|
|
2510
2836
|
</div>
|
|
2511
2837
|
</div>
|
|
2512
2838
|
</div>
|
|
2839
|
+
<!-- moment ③: the New-project paywall dialog (H22). Reuses the .overlay dim+blur +
|
|
2840
|
+
narrow .modal of the delete confirm (the product's one dimmed dialog family),
|
|
2841
|
+
with a one-action layout: Upgrade full-width, "Not now" a quiet link under it.
|
|
2842
|
+
Title/body/price are filled per language at open (openProjectLimitDialog). -->
|
|
2843
|
+
<div class="overlay" id="flimProjOverlay">
|
|
2844
|
+
<div class="modal flimdlg">
|
|
2845
|
+
<div class="fd-lb">Free limit</div>
|
|
2846
|
+
<h3 id="flimProjTitle"></h3>
|
|
2847
|
+
<p id="flimProjBody"></p>
|
|
2848
|
+
<div class="fd-row">
|
|
2849
|
+
<button class="bx-cta fd-cta" id="flimProjUpgrade"><span class="bx-spin"></span>Upgrade</button>
|
|
2850
|
+
<button class="fd-no" id="flimProjNo">Not now</button>
|
|
2851
|
+
</div>
|
|
2852
|
+
</div>
|
|
2853
|
+
</div>
|
|
2513
2854
|
<script>
|
|
2514
2855
|
// Auth key lives in localStorage (the ?key= from the first visit) so auth
|
|
2515
2856
|
// never depends on cookies — Chrome drops session cookies when the server
|
|
@@ -2536,7 +2877,7 @@ const savedTheme = (() => {
|
|
|
2536
2877
|
if (v !== s) localStorage.setItem('theme', v);
|
|
2537
2878
|
return v;
|
|
2538
2879
|
})();
|
|
2539
|
-
const state = { projects: [], goals: [], tasks: [], act: {}, actAt: {}, todos: {}, chatTurns: {}, active: 'galda2', editing: null, attach: [], connectCommand: '', models: ['sonnet'], agentModels: null, agentEfforts: null, agents: ['claude-code', 'codex'], expanded: new Set(), openThreads: new Set(), queueOrder: {}, externalActivity: [], selectedGoal: null, workflowColumns: {}, wfDraft: null, reviewDefinitions: {}, rdDraft: null, 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, summaryPattern: Number(localStorage.getItem('summaryPattern')) || 3, peraSpot: 0, skill: null, skillsCache: null,
|
|
2880
|
+
const state = { projects: [], goals: [], tasks: [], act: {}, actAt: {}, todos: {}, chatTurns: {}, active: 'galda2', editing: null, attach: [], connectCommand: '', models: ['sonnet'], agentModels: null, agentEfforts: null, agents: ['claude-code', 'codex'], expanded: new Set(), openThreads: new Set(), queueOrder: {}, externalActivity: [], selectedGoal: null, workflowColumns: {}, wfDraft: null, reviewDefinitions: {}, rdDraft: null, 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,
|
|
2540
2881
|
// per-message send overrides set via slash-commands (reset after each send,
|
|
2541
2882
|
// except model which is sticky in localStorage). palette = open command list.
|
|
2542
2883
|
msg: { mode: null, model: null, effort: null, skill: null, later: false, review: false },
|
|
@@ -3884,13 +4225,11 @@ function positionLayout(){
|
|
|
3884
4225
|
// into the center card. Moving the node keeps every handler and the
|
|
3885
4226
|
// input-focus fix (c32fda1): appendChild is guarded so the focused #input
|
|
3886
4227
|
// is never detached mid-typing on SSE-driven renders.
|
|
4228
|
+
// Composer (queue + input + FREE LIMIT banner) + transient #errbar live at the bottom of
|
|
4229
|
+
// the ONE center card — desktop and mobile alike (全面つなげた下地: feed scrolls above,
|
|
4230
|
+
// composer sits on the same connected surface, the queue tray shares that 下地).
|
|
3887
4231
|
const host = $('fsCompwrap'), cw = $('composerWrap');
|
|
3888
4232
|
if (host && cw && cw.parentNode !== host) host.appendChild(cw);
|
|
3889
|
-
// #errbar (showErr/showBillingBlocked/showSignInNudge) lives inside .main,
|
|
3890
|
-
// which flagship hides outright (display:none) — every transient message
|
|
3891
|
-
// silently never rendered. Adopt it beside the composer, same technique as
|
|
3892
|
-
// #composerWrap above; a flagship CSS rule floats it just above the input
|
|
3893
|
-
// (CDO判定 2026-07-16 — same fix as #gearPop's body[data-layout] escape).
|
|
3894
4233
|
const eb = $('errbar');
|
|
3895
4234
|
if (host && eb && eb.parentNode !== host) host.appendChild(eb);
|
|
3896
4235
|
// tasklist / actpanel go back home (they live hidden inside .main here)
|
|
@@ -4912,6 +5251,7 @@ const fsUI = {
|
|
|
4912
5251
|
band: Number(localStorage.getItem('fsBand') ?? 0), // theme-6 band choice
|
|
4913
5252
|
revMore: false, // "+N more" fold on the review list (§1.5-8)
|
|
4914
5253
|
todoFold: false, doneOpen: false, todoMore: false,
|
|
5254
|
+
todoView: localStorage.getItem('fsTodoView') === 'board' ? 'board' : 'list', // To Do: list (default) / kanban board (Masa 2026-07-16)
|
|
4915
5255
|
logOpen: new Set(), logFocus: null, // "View log" targets
|
|
4916
5256
|
repsOpen: new Set(), // reply-group folds
|
|
4917
5257
|
};
|
|
@@ -5198,6 +5538,10 @@ function toggleGoalDetail(goalId){
|
|
|
5198
5538
|
if (state.goalDetailOpen === goalId && overlay.classList.contains('show')) closeGoalDetail();
|
|
5199
5539
|
else openGoalDetail(goalId);
|
|
5200
5540
|
}
|
|
5541
|
+
function syncFsTvSeg(){
|
|
5542
|
+
const seg = $('fsTvSeg'); if (!seg) return;
|
|
5543
|
+
for (const b of seg.querySelectorAll('[data-fstv]')) b.classList.toggle('on', b.dataset.fstv === fsUI.todoView);
|
|
5544
|
+
}
|
|
5201
5545
|
function renderFsLane(){
|
|
5202
5546
|
if (state.layout !== 'flagship') return;
|
|
5203
5547
|
const lane = $('fsLane'); if (!lane) return;
|
|
@@ -5205,7 +5549,153 @@ function renderFsLane(){
|
|
|
5205
5549
|
// bug as the qreplyrow "vanishes mid-type" fix.
|
|
5206
5550
|
const ae = document.activeElement;
|
|
5207
5551
|
if (ae && lane.contains(ae) && ['INPUT', 'TEXTAREA'].includes(ae.tagName)) return;
|
|
5552
|
+
lane.classList.toggle('boardmode', fsUI.todoView === 'board');
|
|
5553
|
+
syncFsTvSeg();
|
|
5208
5554
|
renderFsReview(); renderFsTodo();
|
|
5555
|
+
if (fsUI.todoView === 'board') renderFsBoard();
|
|
5556
|
+
}
|
|
5557
|
+
// Which board column a GOAL belongs in. Follows goalChip()'s rule so the board and
|
|
5558
|
+
// the status chips can never drift; the two columns the chips have no word for
|
|
5559
|
+
// (Later / Needs you) are spelled out here. Pure: goal in, column key out.
|
|
5560
|
+
function fsBoardBucket(goal){
|
|
5561
|
+
const s = goal?.status;
|
|
5562
|
+
if (s === 'pending') return 'later'; // shelved by the composer's "Later"
|
|
5563
|
+
if (s === 'needsInput') return 'needs'; // a clarifying question is waiting on the user
|
|
5564
|
+
if (['blocked', 'failed', 'partial', 'interrupted'].includes(s)) return 'needs'; // stopped/errored — needs YOUR action (retry), like the flagship board which puts .drow.err in Needs you. Not "Doing" — Doing is only what's actively moving (Masa 2026-07-18)
|
|
5565
|
+
if (s === 'review') return 'review';
|
|
5566
|
+
if (s === 'done' || s === 'reverted') return 'done'; // both terminal; REVERTED is not "in flight"
|
|
5567
|
+
if (s === 'stacked' || s === 'sending') return 'todo';
|
|
5568
|
+
if (s === 'running' && !goal.startedAt) return 'todo'; // goalChip: planned, nothing started yet
|
|
5569
|
+
return 'doing'; // planning / running / retesting — actively in flight only
|
|
5570
|
+
}
|
|
5571
|
+
// Kanban board — GOAL-level: one card per goal, never per task.
|
|
5572
|
+
//
|
|
5573
|
+
// A card is one thing the user asked for, and its #NN names that one thing exactly
|
|
5574
|
+
// once (Masa 2026-07-17). Tasks are the AI's decomposition of a single request —
|
|
5575
|
+
// the user didn't write them, doesn't review them one by one (proof and the PR are
|
|
5576
|
+
// per goal), and never says "#14" about one. They are the inside of a card, not
|
|
5577
|
+
// cards. The board used to lay out one card per TASK while #NN kept naming the
|
|
5578
|
+
// GOAL, so a goal split into three tasks put three cards reading "#13" in three
|
|
5579
|
+
// different columns.
|
|
5580
|
+
//
|
|
5581
|
+
// No drag-to-change-status: each status transition is its own gated server endpoint
|
|
5582
|
+
// (approve/dismiss/reverify/activate/…), there's no generic "set status" call to hook a
|
|
5583
|
+
// drag onto, so v1 is a visual overview only. Review already has its own rich section
|
|
5584
|
+
// above (proof thumbnails, PR links, approve/dismiss) — the board doesn't duplicate it.
|
|
5585
|
+
function renderFsBoard(){
|
|
5586
|
+
const el = $('fsBoard'); if (!el) return;
|
|
5587
|
+
const { pgoals, list } = fsBucketTodo();
|
|
5588
|
+
const tt = (s, n) => esc(String(s ?? '').replace(/\s+/g, ' ').slice(0, n ?? 60));
|
|
5589
|
+
const goalNo = new Map();
|
|
5590
|
+
for (const g of pgoals) { const n = goalReviewNumber({ goal: g, tasks: list }); if (n != null) goalNo.set(g.id, n); }
|
|
5591
|
+
// + New in EVERY column, matching the flagship Artifact: a hover-reveal "+" in the header
|
|
5592
|
+
// (.fsbcadd) and a persistent "+ New" at the bottom (.fsbadd). Both just focus the composer
|
|
5593
|
+
// (#input) — the real create path (no editable rows / no per-column status invention like the
|
|
5594
|
+
// flagship mock; a new goal enters via the composer). Earlier this was To-Do-only, which read
|
|
5595
|
+
// as "position is off" against the Artifact (Masa 2026-07-18).
|
|
5596
|
+
const col = (name, stg, count, html, stage) =>
|
|
5597
|
+
`<section class="fsbcol" data-stage="${stage}" style="--stg:${stg}"><div class="fsbcolhd"><span class="fsbdot"></span><span class="fsbcolnm">${name}</span><span class="fsbcnt">${count}</span><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><button class="fsbadd" data-fsnew="1" title="New goal"><span class="paplus"></span>New</button></section>`;
|
|
5598
|
+
// Depth card (CDO, Masa 2026-07-17): #NN + title, colour on the dot only, and a continuous
|
|
5599
|
+
// stroke + n/m ONLY while the goal is actually in flight. The column already names the
|
|
5600
|
+
// status, so the card never repeats it. Stopped/failed carry NO bar (a dead bar would claim
|
|
5601
|
+
// the goal is moving) — a coloured word (Failed/Interrupted) says it instead.
|
|
5602
|
+
const goalTasks = (g) => list.filter((t) => t.goalId === g.id && !t.reply);
|
|
5603
|
+
const goalCard = (g) => {
|
|
5604
|
+
const bkt = fsBoardBucket(g);
|
|
5605
|
+
// errored is status-based now (blocked/failed/partial/interrupted live in the Needs-you column,
|
|
5606
|
+
// not Doing). Doing holds only what's actively moving, so bkt==='doing' ⇒ healthy in flight.
|
|
5607
|
+
const errored = ['blocked', 'failed', 'partial', 'interrupted'].includes(g.status);
|
|
5608
|
+
const intr = g.status === 'interrupted';
|
|
5609
|
+
const healthyRun = bkt === 'doing';
|
|
5610
|
+
let dot = '', word = '';
|
|
5611
|
+
if (errored) { dot = intr ? 'intr' : 'bad'; word = intr ? 'Interrupted' : 'Failed'; } // shown in Needs you, with the word
|
|
5612
|
+
else if (bkt === 'review') dot = 'rev';
|
|
5613
|
+
else if (bkt === 'done') dot = 'ok';
|
|
5614
|
+
else if (bkt === 'needs') dot = 'intr'; // your turn — a clarifying question is waiting
|
|
5615
|
+
else if (healthyRun) dot = 'run';
|
|
5616
|
+
// To Do (idle) and Later carry no dot — the column already says it. Quiet = dimmer title.
|
|
5617
|
+
const quiet = bkt === 'todo' || bkt === 'done' || bkt === 'later';
|
|
5618
|
+
const n = goalNo.get(g.id);
|
|
5619
|
+
let h = '<div class="lbc-hd">'
|
|
5620
|
+
+ (dot ? `<span class="lbc-dot ${dot}"></span>` : '')
|
|
5621
|
+
+ (n != null ? `<span class="lbc-n">#${n}</span>` : '')
|
|
5622
|
+
+ (word ? `<span class="lbc-w ${intr ? 'intr' : 'bad'}">${word}</span>` : '')
|
|
5623
|
+
+ '</div>'
|
|
5624
|
+
+ `<div class="lbc-t">${tt(g.text || g.plan?.[0], 90)}</div>`;
|
|
5625
|
+
// n/m is Doing-only (Masa 2026-07-17): a fraction on a stopped/queued/done card is
|
|
5626
|
+
// residue, not progress — a card showing n/m reads unambiguously as "moving right now".
|
|
5627
|
+
// done/total are the goal's real tasks, so it never invents a number.
|
|
5628
|
+
if (healthyRun) {
|
|
5629
|
+
const gt = goalTasks(g), total = gt.length;
|
|
5630
|
+
if (total) {
|
|
5631
|
+
const done = gt.filter((t) => ['done', 'skipped'].includes(t.status)).length;
|
|
5632
|
+
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>`;
|
|
5633
|
+
}
|
|
5634
|
+
}
|
|
5635
|
+
return `<div class="lbc${quiet ? ' is-quiet' : ''}">${h}</div>`;
|
|
5636
|
+
};
|
|
5637
|
+
const no = (g) => goalNo.get(g.id) ?? Infinity;
|
|
5638
|
+
const bucket = { todo: [], doing: [], needs: [], review: [], done: [], later: [] };
|
|
5639
|
+
for (const g of pgoals) bucket[fsBoardBucket(g)].push(g);
|
|
5640
|
+
for (const k of Object.keys(bucket)) bucket[k].sort((a, b) => k === 'done' ? no(b) - no(a) : no(a) - no(b));
|
|
5641
|
+
// To Do / Doing / Needs you / Review / Done / Later — the original design's columns
|
|
5642
|
+
// (Masa 2026-07-16). "Needs you" = a clarifying question is waiting on the user —
|
|
5643
|
+
// distinct from "blocked" (an error needing retry/archive, which stays in Doing, the
|
|
5644
|
+
// same grouping the List view already uses). Later (backlog) sits LAST, not before Done.
|
|
5645
|
+
el.innerHTML =
|
|
5646
|
+
col('To Do', 'var(--ink3)', bucket.todo.length, bucket.todo.map(goalCard).join(''), 'todo')
|
|
5647
|
+
+ col('Doing', 'var(--green)', bucket.doing.length, bucket.doing.map(goalCard).join(''), 'doing')
|
|
5648
|
+
+ (bucket.needs.length ? col('Needs you', '#ffca16', bucket.needs.length, bucket.needs.map(goalCard).join(''), 'needs') : '')
|
|
5649
|
+
+ col('Review', '#8390f2', bucket.review.length, bucket.review.map(goalCard).join(''), 'review')
|
|
5650
|
+
+ col('Done', 'var(--ink3)', bucket.done.length, bucket.done.slice(0, 40).map(goalCard).join(''), 'done')
|
|
5651
|
+
+ col('Later', 'var(--ink3)', bucket.later.length, bucket.later.map(goalCard).join(''), 'later');
|
|
5652
|
+
for (const b of el.querySelectorAll('[data-fsnew]')) b.onclick = () => $('input')?.focus(); // every + New / header + focuses the composer
|
|
5653
|
+
renderFsWait(bucket);
|
|
5654
|
+
}
|
|
5655
|
+
// Waiting badge (CDO, Masa 2026-07-17 decision 2): the board keeps work-flow column order, so
|
|
5656
|
+
// the "your turn" columns (Needs you, Review) can sit off the right edge at a narrow lane
|
|
5657
|
+
// width. They announce themselves beside the List/Board toggle instead — pure read of the same
|
|
5658
|
+
// buckets the board just rendered (no server call). Colour on the dot only; a column already
|
|
5659
|
+
// in view sinks. Only the columns with items are drawn — zero waiting shows nothing.
|
|
5660
|
+
const FS_WAIT = [['needs', 'Needs you', '#ffca16'], ['review', 'Review', '#8390f2']];
|
|
5661
|
+
function fsWaitPlace(){
|
|
5662
|
+
const bar = $('fsWait'), seg = $('fsTvSeg'), lane = $('fsLane');
|
|
5663
|
+
if (!bar || !seg || !lane) return;
|
|
5664
|
+
const sr = seg.getBoundingClientRect(), lr = lane.getBoundingClientRect();
|
|
5665
|
+
bar.style.left = Math.round(sr.right - lr.left + 14) + 'px'; // measured off the toggle, not guessed
|
|
5666
|
+
}
|
|
5667
|
+
function fsWaitSeen(){
|
|
5668
|
+
const bar = $('fsWait'), board = $('fsBoard');
|
|
5669
|
+
if (!bar || !board) return;
|
|
5670
|
+
const br = board.getBoundingClientRect();
|
|
5671
|
+
for (const b of bar.querySelectorAll('button')) {
|
|
5672
|
+
const col = board.querySelector(`.fsbcol[data-stage="${b.dataset.stage}"]`);
|
|
5673
|
+
if (!col) continue;
|
|
5674
|
+
const cr = col.getBoundingClientRect();
|
|
5675
|
+
b.classList.toggle('seen', cr.left < br.right - 40 && cr.right > br.left + 40);
|
|
5676
|
+
}
|
|
5677
|
+
}
|
|
5678
|
+
function renderFsWait(bucket){
|
|
5679
|
+
const bar = $('fsWait'); if (!bar) return;
|
|
5680
|
+
bar.innerHTML = FS_WAIT.map(([k, label, c]) => bucket[k].length
|
|
5681
|
+
? `<button data-stage="${k}" style="--stg:${c}"><span class="wd"></span><span class="wn">${bucket[k].length}</span>${label}</button>` : '').join('');
|
|
5682
|
+
fsWaitPlace();
|
|
5683
|
+
if (!renderFsWait.__wired) {
|
|
5684
|
+
renderFsWait.__wired = true;
|
|
5685
|
+
bar.addEventListener('click', (e) => {
|
|
5686
|
+
const b = e.target.closest('button'); if (!b) return;
|
|
5687
|
+
const col = $('fsBoard')?.querySelector(`.fsbcol[data-stage="${b.dataset.stage}"]`);
|
|
5688
|
+
if (col) col.scrollIntoView({ behavior: 'smooth', block: 'nearest', inline: 'start' });
|
|
5689
|
+
});
|
|
5690
|
+
const board = $('fsBoard');
|
|
5691
|
+
if (board) {
|
|
5692
|
+
board.addEventListener('scroll', fsWaitSeen, { passive: true });
|
|
5693
|
+
// the lane can also widen a hidden column into view (drag the grip) — repos + re-check
|
|
5694
|
+
if (window.ResizeObserver) new ResizeObserver(() => { fsWaitPlace(); fsWaitSeen(); }).observe(board);
|
|
5695
|
+
}
|
|
5696
|
+
window.addEventListener('resize', () => { fsWaitPlace(); fsWaitSeen(); });
|
|
5697
|
+
}
|
|
5698
|
+
fsWaitSeen();
|
|
5209
5699
|
}
|
|
5210
5700
|
// Review (top of the lane, §1.5-4). Counts are DETERMINISTIC (§1.5-9): computed
|
|
5211
5701
|
// from state via buildReviewDigest — no LLM, no donut, no badges.
|
|
@@ -5288,6 +5778,8 @@ function renderFsReview(){
|
|
|
5288
5778
|
</span></div>
|
|
5289
5779
|
<div class="revlist">${shown.map(card).join('')}${foldedGoals.map(foldCard).join('')}
|
|
5290
5780
|
${rows.length > RCAP ? `<div class="morefold" id="fsRevMore">${fsUI.revMore ? 'less' : `+${rows.length - RCAP} more`}<span class="g">›</span></div>` : ''}</div>`;
|
|
5781
|
+
// Mirror the ready-review count onto the mobile top-bar "Review N" opener (H23).
|
|
5782
|
+
const _hn = document.getElementById('fsHambN'); if (_hn) _hn.textContent = green > 0 ? String(green) : '';
|
|
5291
5783
|
// See all = the Ledger dialog (HANDOFF-v45 §5, replacing the ペライチ);
|
|
5292
5784
|
// Cmd/Ctrl+click = standalone full-page view in a new tab (#seeall route).
|
|
5293
5785
|
// guarded: the meter (and with it See all) is absent while the section holds only
|
|
@@ -5325,26 +5817,36 @@ function renderFsReview(){
|
|
|
5325
5817
|
headers: { 'content-type': 'application/json' }, body: JSON.stringify({ text: '' }) });
|
|
5326
5818
|
if (r && r.ok) await refresh(); else showErr('Dismiss failed.'); };
|
|
5327
5819
|
}
|
|
5328
|
-
// To Do (
|
|
5329
|
-
//
|
|
5330
|
-
function
|
|
5331
|
-
const el = $('fsTodoSec'); if (!el) return;
|
|
5820
|
+
// Shared To Do bucketing (list rows AND the kanban board read the same groups —
|
|
5821
|
+
// single source of truth, see renderFsBoard below). Pure of the DOM: state in, groups out.
|
|
5822
|
+
function fsBucketTodo(){
|
|
5332
5823
|
const pgoals = state.goals.filter((g) => g.projectId === state.active);
|
|
5333
5824
|
const list = state.tasks.filter((t) => t.projectId === state.active);
|
|
5334
5825
|
const order = state.queueOrder[state.active] ?? [];
|
|
5335
5826
|
const byQueue = (a, b) => { const ia = order.indexOf(a.id), ib = order.indexOf(b.id); return (ia < 0 ? 1e9 : ia) - (ib < 0 ? 1e9 : ib) || a.num - b.num; };
|
|
5336
|
-
|
|
5337
|
-
|
|
5338
|
-
|
|
5339
|
-
|
|
5340
|
-
|
|
5827
|
+
return {
|
|
5828
|
+
pgoals, list,
|
|
5829
|
+
running: list.filter((t) => t.status === 'running'),
|
|
5830
|
+
failed: list.filter((t) => ['failed', 'interrupted'].includes(t.status)),
|
|
5831
|
+
blocked: pgoals.filter((g) => g.status === 'blocked'),
|
|
5832
|
+
needsInput: pgoals.filter((g) => g.status === 'needsInput'), // a clarifying question is waiting on the user (board's "Needs you")
|
|
5833
|
+
reviewGoals: pgoals.filter((g) => g.status === 'review'),
|
|
5834
|
+
queued: list.filter((t) => t.status === 'queued' && !t.reply).sort(byQueue),
|
|
5835
|
+
doneAll: list.filter((t) => ['done', 'skipped'].includes(t.status)).sort((a, b) => b.num - a.num),
|
|
5836
|
+
later: pgoals.filter((g) => g.status === 'pending'),
|
|
5837
|
+
};
|
|
5838
|
+
}
|
|
5839
|
+
// To Do (below Review): Doing rows with bar + mono %, Next up in queue order,
|
|
5840
|
+
// error as a small in-row colour element (§1.5-8), quiet Done fold.
|
|
5841
|
+
function renderFsTodo(){
|
|
5842
|
+
const el = $('fsTodoSec'); if (!el) return;
|
|
5843
|
+
const { pgoals, list, running, failed, blocked, queued, doneAll, later } = fsBucketTodo();
|
|
5341
5844
|
const tt = (s, n) => esc(String(s ?? '').replace(/\s+/g, ' ').slice(0, n ?? 70));
|
|
5342
5845
|
// LATER = deliberately shelved goals (composer "Later" send → server status 'pending';
|
|
5343
5846
|
// PRD §8.2 top gap — they were invisible in flagship). Staged disclosure: the cap
|
|
5344
5847
|
// renders only when they exist. Same endpoints as the old layout's data-pstart/pdel:
|
|
5345
5848
|
// Start = POST /api/goals/:id/activate, Delete = DELETE /api/goals/:id (confirm).
|
|
5346
5849
|
// No mono # — pending goals are never planned, so they carry no task number yet.
|
|
5347
|
-
const later = pgoals.filter((g) => g.status === 'pending');
|
|
5348
5850
|
const laterRows = later.map((g) => `<div class="trow up dim"><span class="st"></span><span class="tt">${tt(g.text, 70)}</span>
|
|
5349
5851
|
<span class="acts"><button class="ab txt" data-fspstart="${g.id}" title="Queue it now">Start</button><button class="ab txt" data-fspdel="${g.id}" title="Delete">Delete</button></span></div>`).join('');
|
|
5350
5852
|
// reference numbers (HANDOFF-v45 §11): a #NN prefix on every To Do / Review item's name.
|
|
@@ -5401,17 +5903,31 @@ function renderFsTodo(){
|
|
|
5401
5903
|
// specifically, since that suffix describes the Next-up sublist's own sort, not the
|
|
5402
5904
|
// lane total.
|
|
5403
5905
|
const todoTotal = running.length + failed.length + blocked.length + queued.length + later.length;
|
|
5404
|
-
|
|
5906
|
+
// Free-tier pending counter (H22): on the free plan the header shows the wall
|
|
5907
|
+
// — pending goals out of the 5-slot cap — reddening at the cap and gone once
|
|
5908
|
+
// paid. Reads the SAME numbers the gate enforces (state.billing, never a local
|
|
5909
|
+
// recompute) so the header and the wall can't drift. usage.pending already
|
|
5910
|
+
// excludes review server-side (#128).
|
|
5911
|
+
const _bill = state.billing || {};
|
|
5912
|
+
const _fp = _bill.usage?.pending, _fl = _bill.limits?.pending;
|
|
5913
|
+
const _showFc = _bill.plan !== 'paid' && Number.isFinite(_fp) && Number.isFinite(_fl);
|
|
5914
|
+
const _todoNum = _showFc
|
|
5915
|
+
? `<span class="fcount fstdlabel${_fp >= _fl ? ' cap' : ''}">${_fp}<span class="fc-l">/${_fl}</span></span>`
|
|
5916
|
+
: `<span class="num fstdlabel">${todoTotal}${queued.length ? ' · next-up order' : ''}</span>`;
|
|
5917
|
+
el.innerHTML = `<div class="lhd sechd" id="fsTodoHd"><span class="hd fstdlabel">To Do</span>${_todoNum}<span class="fold fstdlabel">▾</span></div>
|
|
5405
5918
|
<div class="tl">
|
|
5406
5919
|
${doingRows ? `<span class="tlcap" style="padding-top:0">Doing</span>${doingRows}` : ''}
|
|
5407
5920
|
${upRows ? `<span class="tlcap"${doingRows ? '' : ' style="padding-top:0"'}>Next up</span>${upRows}` : ''}
|
|
5408
5921
|
${queued.length > NCAP ? `<div class="morefold" id="fsTodoMore">${fsUI.todoMore ? 'less' : `+${queued.length - NCAP} more`}<span class="g">›</span></div>` : ''}
|
|
5409
5922
|
${laterRows ? `<span class="tlcap">Later</span>${laterRows}` : ''}
|
|
5923
|
+
<button class="qadd" data-fsnew="1" title="New goal"><span class="paplus"></span>New</button>
|
|
5410
5924
|
</div>
|
|
5411
5925
|
${doneAll.length ? `<div class="doneline${fsUI.doneOpen ? ' open' : ''}" id="fsDoneLine">✓ Done<span class="num">${doneAll.length}</span><span class="fold">▾</span></div>
|
|
5412
5926
|
<div id="fsDonelist"${fsUI.doneOpen ? '' : ' hidden'}>${doneAll.slice(0, 3).map((t) => `<div class="trow up dim done"><span class="st ok"></span>${noSpan(t.goalId)}<span class="tt">${tt(t.title)}</span></div>`).join('')}</div>` : ''}`;
|
|
5413
5927
|
el.classList.toggle('folded', !!fsUI.todoFold);
|
|
5414
5928
|
$('fsTodoHd').onclick = () => { fsUI.todoFold = !fsUI.todoFold; el.classList.toggle('folded', fsUI.todoFold); };
|
|
5929
|
+
const qadd = el.querySelector('.qadd[data-fsnew]'); // + New → focus the composer (the real create path)
|
|
5930
|
+
if (qadd) qadd.onclick = () => $('input')?.focus();
|
|
5415
5931
|
const dl = el.querySelector('#fsDoneLine');
|
|
5416
5932
|
if (dl) dl.onclick = () => { fsUI.doneOpen = !fsUI.doneOpen; dl.classList.toggle('open', fsUI.doneOpen); el.querySelector('#fsDonelist').hidden = !fsUI.doneOpen; };
|
|
5417
5933
|
const more = el.querySelector('#fsTodoMore');
|
|
@@ -5524,6 +6040,9 @@ async function createProject(){
|
|
|
5524
6040
|
try {
|
|
5525
6041
|
const r = await fetch(withKey('/api/projects'), { method: 'POST', headers: { 'content-type': 'application/json' },
|
|
5526
6042
|
body: JSON.stringify({ baseProjectId: state.active, name: 'New project' }) });
|
|
6043
|
+
// Free tier allows one project (moment ③): the server 402s the second. Raise
|
|
6044
|
+
// the centered upgrade dialog instead of the generic "failed." toast.
|
|
6045
|
+
if (r.status === 402) { openProjectLimitDialog(); return; }
|
|
5527
6046
|
if (!r.ok) throw new Error();
|
|
5528
6047
|
const body = await r.json();
|
|
5529
6048
|
state.projects = body.projects ?? [...state.projects, body.project].filter(Boolean);
|
|
@@ -5531,8 +6050,22 @@ async function createProject(){
|
|
|
5531
6050
|
document.body.classList.remove('projectsopen');
|
|
5532
6051
|
render();
|
|
5533
6052
|
} catch { showErr('New project failed.'); }
|
|
5534
|
-
if (add) add.disabled = false;
|
|
5535
|
-
}
|
|
6053
|
+
finally { if (add) add.disabled = false; } // the 402 early-return must not leave +New dead
|
|
6054
|
+
}
|
|
6055
|
+
// moment ③ paywall dialog — filled per language from BILLING_COPY, Upgrade shows
|
|
6056
|
+
// the one price format (small $, no /mo), "Not now" just closes.
|
|
6057
|
+
function openProjectLimitDialog(){
|
|
6058
|
+
const c = BILLING_COPY[billingLang()];
|
|
6059
|
+
$('flimProjTitle').textContent = c.projTitle;
|
|
6060
|
+
$('flimProjBody').textContent = c.projBody;
|
|
6061
|
+
$('flimProjNo').textContent = c.notNow;
|
|
6062
|
+
const up = $('flimProjUpgrade');
|
|
6063
|
+
up.disabled = false; up.classList.remove('loading');
|
|
6064
|
+
up.innerHTML = `<span class="bx-spin"></span>Upgrade${priceTag(state.billing?.price)}`;
|
|
6065
|
+
up.onclick = () => startCheckout(up);
|
|
6066
|
+
$('flimProjOverlay').classList.add('show');
|
|
6067
|
+
}
|
|
6068
|
+
function closeProjectLimitDialog(){ $('flimProjOverlay').classList.remove('show'); }
|
|
5536
6069
|
async function deleteProject(p){
|
|
5537
6070
|
if (!p) return;
|
|
5538
6071
|
if (!(await confirmDelete({
|
|
@@ -5720,6 +6253,16 @@ function renderClawdHero(){
|
|
|
5720
6253
|
};
|
|
5721
6254
|
$('fsRailTg').onclick = () => { root.classList.toggle('railfold'); tgUpdate(); fsGearAlignX(); }; // §1.5-7: fold
|
|
5722
6255
|
$('fsLaneTg').onclick = () => { root.classList.toggle('lanefold'); tgUpdate(); };
|
|
6256
|
+
// List/Board toggle — wired once (static, unlike the row it used to live inside which
|
|
6257
|
+
// rebuilds on every renderFsTodo()); syncFsTvSeg() paints .on and is called from renderFsLane.
|
|
6258
|
+
for (const b of $('fsTvSeg').querySelectorAll('[data-fstv]')) b.onclick = (e) => {
|
|
6259
|
+
e.stopPropagation();
|
|
6260
|
+
fsUI.todoView = b.dataset.fstv; localStorage.setItem('fsTodoView', fsUI.todoView);
|
|
6261
|
+
$('fsLane').classList.toggle('boardmode', fsUI.todoView === 'board');
|
|
6262
|
+
syncFsTvSeg();
|
|
6263
|
+
if (fsUI.todoView === 'board') renderFsBoard();
|
|
6264
|
+
};
|
|
6265
|
+
syncFsTvSeg();
|
|
5723
6266
|
tgUpdate();
|
|
5724
6267
|
$('fsMoreBtn').onclick = (e) => { e.stopPropagation(); $('fsMoreMenu').classList.toggle('open'); };
|
|
5725
6268
|
document.addEventListener('click', (e) => { if (!e.target.closest('#fsRoot .lbrow')) $('fsMoreMenu').classList.remove('open'); });
|
|
@@ -5732,6 +6275,12 @@ function renderClawdHero(){
|
|
|
5732
6275
|
document.body.classList.toggle('laneopen');
|
|
5733
6276
|
};
|
|
5734
6277
|
$('fsLaneDim').onclick = closeMobilePanels;
|
|
6278
|
+
{ const lx = document.getElementById('fsLaneClose'); if (lx) lx.onclick = closeMobilePanels; }
|
|
6279
|
+
// Mobile empty-state hero chips (H23): tapping one drafts it into the composer.
|
|
6280
|
+
document.querySelectorAll('.m-chip').forEach((ch) => { ch.onclick = () => {
|
|
6281
|
+
const inp = document.getElementById('input');
|
|
6282
|
+
if (inp) { inp.value = ch.textContent; inp.focus(); inp.dispatchEvent(new Event('input', { bubbles: true })); }
|
|
6283
|
+
}; });
|
|
5735
6284
|
$('fsProjBtn').onclick = (e) => {
|
|
5736
6285
|
e.stopPropagation();
|
|
5737
6286
|
document.body.classList.remove('laneopen');
|
|
@@ -5770,17 +6319,43 @@ function renderClawdHero(){
|
|
|
5770
6319
|
if (restoreLane && canRestoreFlagshipWidths(restoreRail ? savedRail : defaultRailW, saved)) {
|
|
5771
6320
|
root.style.setProperty('--lanew', `${saved}px`);
|
|
5772
6321
|
}
|
|
5773
|
-
|
|
5774
|
-
|
|
6322
|
+
// Board view keeps its OWN width (--lanew-board), independent from the List width
|
|
6323
|
+
// (--lanew) — both start at the same 372px default, each persists once resized
|
|
6324
|
+
// (Masa 2026-07-16: 看板とToDoの横幅は最初同じ・それぞれ保持). List stays capped at
|
|
6325
|
+
// 540 (a To Do list doesn't need more); Board can grow with the window, same
|
|
6326
|
+
// "no flat ceiling" treatment as the rail above (Masa 2026-07-16: もっと広げたい) —
|
|
6327
|
+
// but its live max reserves the rail's CURRENT width (not a flat guess), same
|
|
6328
|
+
// protection railMax() gives center: a wide-open board must not crush the rail's
|
|
6329
|
+
// own content (Masa 2026-07-16: 狭くした時にバグる — rail at its 72px floor overlaps
|
|
6330
|
+
// its own logo/project row; reserving rail's real width keeps it out of that floor).
|
|
6331
|
+
// Board is the star of this view — chat only needs to stay usable, not "comfortably
|
|
6332
|
+
// readable" (that 640px figure is for List/chat-first use). 320px LOOKED right but was
|
|
6333
|
+
// measured wrong: the composer's own button row (.crow — app/model/effort selects + send)
|
|
6334
|
+
// needs ~342px just for its content, and .fscenter's padding adds ~44px more — below ~430
|
|
6335
|
+
// those chips get clipped off the edge of the composer card (Masa 2026-07-16 repro:
|
|
6336
|
+
// dragged the board grip wide on a 1280px-wide window, "Medium"+send were cut off).
|
|
6337
|
+
// 440 is the smallest value that doesn't clip on a real laptop width.
|
|
6338
|
+
const minCenterWBoard = 440;
|
|
6339
|
+
const boardMax = () => Math.max(280, window.innerWidth - 28 - 24
|
|
6340
|
+
- (document.querySelector('#fsRoot .fsrail')?.getBoundingClientRect().width || defaultRailW) - minCenterWBoard);
|
|
6341
|
+
const savedBoard = Number(localStorage.getItem('fsLaneWBoard'));
|
|
6342
|
+
if (savedBoard >= 280 && savedBoard <= boardMax()) root.style.setProperty('--lanew-board', `${savedBoard}px`);
|
|
6343
|
+
let sx = 0, sw = 0, drag = false, boarding = false;
|
|
6344
|
+
grip.addEventListener('mousedown', (e) => {
|
|
6345
|
+
drag = true; sx = e.clientX; boarding = lane.classList.contains('boardmode');
|
|
6346
|
+
sw = lane.getBoundingClientRect().width; lane.classList.add('noanim'); e.preventDefault();
|
|
6347
|
+
});
|
|
5775
6348
|
document.addEventListener('mousemove', (e) => {
|
|
5776
6349
|
if (!drag) return;
|
|
5777
|
-
const
|
|
5778
|
-
|
|
6350
|
+
const max = boarding ? boardMax() : 540;
|
|
6351
|
+
const w = Math.max(280, Math.min(max, sw + (sx - e.clientX)));
|
|
6352
|
+
root.style.setProperty(boarding ? '--lanew-board' : '--lanew', `${w}px`);
|
|
5779
6353
|
});
|
|
5780
6354
|
document.addEventListener('mouseup', () => {
|
|
5781
6355
|
if (!drag) return;
|
|
5782
6356
|
drag = false; lane.classList.remove('noanim');
|
|
5783
|
-
localStorage.setItem('
|
|
6357
|
+
if (boarding) localStorage.setItem('fsLaneWBoard', String(parseInt(root.style.getPropertyValue('--lanew-board')) || 372));
|
|
6358
|
+
else localStorage.setItem('fsLaneW', String(parseInt(root.style.getPropertyValue('--lanew')) || 372));
|
|
5784
6359
|
});
|
|
5785
6360
|
// rail width drag: 72px – railMax() (freely widens, only bounded by keeping the center column
|
|
5786
6361
|
// readable), persisted (flagship .railgrip port, v36 JS; grip hides when folded)
|
|
@@ -5801,6 +6376,52 @@ function renderClawdHero(){
|
|
|
5801
6376
|
localStorage.setItem('fsRailW', String(parseInt(root.style.getPropertyValue('--railw')) || 168)); // H19: default floor 168
|
|
5802
6377
|
if ($('gearPop').classList.contains('open')) custPlace(); // panel sits beside the rail — track the new edge
|
|
5803
6378
|
});
|
|
6379
|
+
// chat ⇄ lane panel swap — grab anywhere in the top ~40px of either panel (not just the
|
|
6380
|
+
// grip icon), drop lands EXACTLY on the side the dashed guide showed (never a blind
|
|
6381
|
+
// toggle, which could disagree with what was shown). Rail is fixed, not part of this
|
|
6382
|
+
// (Masa 2026-07-16).
|
|
6383
|
+
(() => {
|
|
6384
|
+
const center = root.querySelector('.fscenter');
|
|
6385
|
+
if (!center || !lane) return;
|
|
6386
|
+
const TOPZONE = 40;
|
|
6387
|
+
const sideFor = (target, x) => { const r = target.getBoundingClientRect(); return x < r.left + r.width / 2 ? 'left' : 'right'; };
|
|
6388
|
+
const dropLine = document.createElement('div'); dropLine.id = 'panelDropLine'; root.appendChild(dropLine);
|
|
6389
|
+
const showDropLine = (target, side) => {
|
|
6390
|
+
const r = target.getBoundingClientRect(), ar = root.getBoundingClientRect();
|
|
6391
|
+
const edgeX = side === 'left' ? r.left : r.right;
|
|
6392
|
+
dropLine.style.left = `${edgeX - ar.left}px`; dropLine.style.top = `${r.top - ar.top}px`;
|
|
6393
|
+
dropLine.style.height = `${r.height}px`; dropLine.style.display = 'block';
|
|
6394
|
+
};
|
|
6395
|
+
const hideDropLine = () => { dropLine.style.display = 'none'; };
|
|
6396
|
+
document.addEventListener('dragend', hideDropLine);
|
|
6397
|
+
const armTop = (panel, name) => {
|
|
6398
|
+
panel.addEventListener('mousedown', (e) => { const r = panel.getBoundingClientRect(); if (e.clientY - r.top <= TOPZONE) panel.setAttribute('draggable', 'true'); });
|
|
6399
|
+
panel.addEventListener('mousemove', (e) => { const r = panel.getBoundingClientRect(); panel.classList.toggle('top-armed', e.clientY - r.top <= TOPZONE); });
|
|
6400
|
+
panel.addEventListener('mouseleave', () => panel.classList.remove('top-armed'));
|
|
6401
|
+
panel.addEventListener('dragstart', (e) => {
|
|
6402
|
+
if (panel.getAttribute('draggable') !== 'true') return;
|
|
6403
|
+
e.dataTransfer.effectAllowed = 'move'; e.dataTransfer.setData('application/x-panel-swap', name);
|
|
6404
|
+
});
|
|
6405
|
+
const disarm = () => { panel.removeAttribute('draggable'); panel.classList.remove('top-armed'); };
|
|
6406
|
+
panel.addEventListener('dragend', disarm); panel.addEventListener('mouseup', disarm);
|
|
6407
|
+
};
|
|
6408
|
+
armTop(center, 'center'); armTop(lane, 'lane');
|
|
6409
|
+
for (const p of [center, lane]) {
|
|
6410
|
+
p.addEventListener('dragover', (e) => {
|
|
6411
|
+
if (!e.dataTransfer.types.includes('application/x-panel-swap')) return;
|
|
6412
|
+
e.preventDefault(); p.classList.add('panel-over'); showDropLine(p, sideFor(p, e.clientX));
|
|
6413
|
+
});
|
|
6414
|
+
p.addEventListener('dragleave', () => { p.classList.remove('panel-over'); hideDropLine(); });
|
|
6415
|
+
p.addEventListener('drop', (e) => {
|
|
6416
|
+
if (!e.dataTransfer.types.includes('application/x-panel-swap')) return;
|
|
6417
|
+
e.preventDefault(); p.classList.remove('panel-over'); hideDropLine();
|
|
6418
|
+
const dragged = e.dataTransfer.getData('application/x-panel-swap');
|
|
6419
|
+
const side = sideFor(p, e.clientX);
|
|
6420
|
+
const wantLeft = side === 'left' ? dragged : (dragged === 'center' ? 'lane' : 'center');
|
|
6421
|
+
root.classList.toggle('swapmid', wantLeft === 'lane');
|
|
6422
|
+
});
|
|
6423
|
+
}
|
|
6424
|
+
})();
|
|
5804
6425
|
// theme-6 band picker (visual thumbnails, §6.5 T6)
|
|
5805
6426
|
$('fsBandBtn').onclick = (e) => { e.stopPropagation(); $('fsBandPop').classList.toggle('open'); };
|
|
5806
6427
|
document.addEventListener('click', (e) => { if (!e.target.closest('#fsBandPop') && !e.target.closest('#fsBandBtn')) $('fsBandPop').classList.remove('open'); });
|
|
@@ -5853,39 +6474,86 @@ function upsert(arr, item){ const i = arr.findIndex((x) => x.id === item.id); if
|
|
|
5853
6474
|
|
|
5854
6475
|
let errTimer = null;
|
|
5855
6476
|
let authDown = false; // true while the worker-auth banner is up → keep auto-hide timers from clearing it
|
|
6477
|
+
// true while the free-tier paywall is up. Unlike a transient error, "you are
|
|
6478
|
+
// over the free limit" doesn't resolve itself — it persists until the user
|
|
6479
|
+
// upgrades or usage drops, so it must not auto-hide on a fixed timer (Masa
|
|
6480
|
+
// 2026-07-17: a 20s auto-hide + a 15s retry that background-tab throttling can
|
|
6481
|
+
// stretch to ~once/minute meant the banner could vanish for long stretches
|
|
6482
|
+
// with the outbox still silently stuck "Unsent" and no visible explanation —
|
|
6483
|
+
// exactly the "queue fills up, nothing happens" report). Mirrors authDown's
|
|
6484
|
+
// existing guard pattern; cleared only in postOutboxItem once a send actually
|
|
6485
|
+
// succeeds, which is the one signal that's actually true evidence the block
|
|
6486
|
+
// lifted (an upgrade, or the count dropping).
|
|
6487
|
+
let billingBlockedActive = false;
|
|
6488
|
+
function clearBillingBlocked(){
|
|
6489
|
+
if (!billingBlockedActive) return;
|
|
6490
|
+
billingBlockedActive = false;
|
|
6491
|
+
const bar = $('errbar');
|
|
6492
|
+
bar.classList.remove('billing');
|
|
6493
|
+
bar.style.display = 'none';
|
|
6494
|
+
bar.innerHTML = '';
|
|
6495
|
+
}
|
|
5856
6496
|
function showErr(msg){
|
|
5857
6497
|
const bar = $('errbar');
|
|
5858
6498
|
bar.classList.remove('billing'); // shed the paywall styling if it was last used for that
|
|
6499
|
+
billingBlockedActive = false;
|
|
5859
6500
|
bar.textContent = msg; bar.style.display = 'block';
|
|
5860
6501
|
clearTimeout(errTimer);
|
|
5861
6502
|
errTimer = setTimeout(() => { if (authDown) return; bar.style.display = 'none'; }, 6000);
|
|
5862
6503
|
}
|
|
5863
6504
|
|
|
5864
|
-
//
|
|
5865
|
-
//
|
|
5866
|
-
//
|
|
5867
|
-
//
|
|
5868
|
-
const
|
|
5869
|
-
|
|
5870
|
-
|
|
5871
|
-
|
|
6505
|
+
// Free-limit copy (H22, Masa 2026-07-17 確定 · ja+en). The sentence is content,
|
|
6506
|
+
// so it follows the user's language; the mono label and buttons stay English
|
|
6507
|
+
// 汎用名 (DOING / FAILED / Retry). <b> is intentional (rendered as innerHTML from
|
|
6508
|
+
// this fixed internal constant, never user input) — the figure is the message.
|
|
6509
|
+
const BILLING_COPY = {
|
|
6510
|
+
ja: {
|
|
6511
|
+
'pending-goal-limit': '無料プランでは一度に登録できるタスクは <b>5個</b> までです。Proプランでは無制限に登録できます。',
|
|
6512
|
+
'cumulative-goal-limit': '無料プランの <b>20/20</b> タスクをすべて使いました。',
|
|
6513
|
+
projTitle: 'プロジェクトは1つまでです',
|
|
6514
|
+
projBody: '無料プランに含まれるプロジェクトは1つです。アップグレードすると、プロジェクトを無制限に作れます。',
|
|
6515
|
+
notNow: 'Not now',
|
|
6516
|
+
},
|
|
6517
|
+
en: {
|
|
6518
|
+
'pending-goal-limit': 'The free plan holds <b>5 tasks</b> at a time. Pro holds as many as you want.',
|
|
6519
|
+
'cumulative-goal-limit': "You've used all <b>20/20</b> free tasks.",
|
|
6520
|
+
projTitle: 'Free includes one project',
|
|
6521
|
+
projBody: 'Your free plan includes one project. Upgrade to create as many as you need.',
|
|
6522
|
+
notNow: 'Not now',
|
|
6523
|
+
},
|
|
5872
6524
|
};
|
|
6525
|
+
// Which language the paywall speaks. Mirrors foldLangFor's path (the project's
|
|
6526
|
+
// review-language override wins; otherwise match the language the user is
|
|
6527
|
+
// actually writing in — the latest goal in the active project — else default en)
|
|
6528
|
+
// so the wall reads in the same language as everything else in the project.
|
|
6529
|
+
function billingLang(){
|
|
6530
|
+
const l = reviewLangFor(state.active);
|
|
6531
|
+
if (l === 'ja' || l === 'en') return l;
|
|
6532
|
+
const recent = (state.goals || []).filter((g) => g.projectId === state.active).slice(-1)[0];
|
|
6533
|
+
if (recent && /[-ヿ㐀-鿿]/.test(recent.text || '')) return 'ja';
|
|
6534
|
+
return document.documentElement.lang === 'ja' ? 'ja' : 'en';
|
|
6535
|
+
}
|
|
6536
|
+
// The free-limit banner (moments ① pending-goal-limit / ② cumulative-goal-limit):
|
|
6537
|
+
// a bordered card in #errbar, --danger as a 2px left rule only (no fill, §5.6),
|
|
6538
|
+
// with an Upgrade CTA that opens Stripe Checkout. project-limit is NOT a banner —
|
|
6539
|
+
// it is the centered dialog raised from createProject (moment ③). The .billing
|
|
6540
|
+
// class strips #errbar's own error fill so the card stands alone.
|
|
5873
6541
|
function showBillingBlocked(blocked){
|
|
5874
6542
|
const bar = $('errbar');
|
|
5875
|
-
const
|
|
5876
|
-
const
|
|
5877
|
-
// ① the free-limit prompt (design H16): a bordered card, red as a 2px left
|
|
5878
|
-
// rule only (no fill), with an Upgrade CTA that opens Stripe Checkout. The
|
|
5879
|
-
// .billing class strips #errbar's own error fill so the card stands alone.
|
|
6543
|
+
const c = BILLING_COPY[billingLang()];
|
|
6544
|
+
const q = c[blocked?.reason] || c['pending-goal-limit'];
|
|
5880
6545
|
bar.classList.add('billing');
|
|
5881
6546
|
bar.innerHTML = `<div class="bx-ask"><div class="bx-qt"><div class="bx-lb">Free limit</div>`
|
|
5882
|
-
+ `<div class="bx-q">${
|
|
5883
|
-
+ `<div class="bx-act"><button class="bx-cta" id="bxUpgrade"
|
|
6547
|
+
+ `<div class="bx-q">${q}</div></div>`
|
|
6548
|
+
+ `<div class="bx-act"><button class="bx-cta" id="bxUpgrade"><span class="bx-spin"></span>Upgrade${priceTag(state.billing?.price)}</button></div></div>`;
|
|
5884
6549
|
bar.style.display = 'block';
|
|
5885
6550
|
const btn = bar.querySelector('#bxUpgrade');
|
|
5886
|
-
if (btn) btn.onclick = () => startCheckout();
|
|
6551
|
+
if (btn) btn.onclick = () => startCheckout(btn);
|
|
6552
|
+
// No auto-hide: the condition persists until the user acts, so a fixed timer
|
|
6553
|
+
// would just recreate the bug this fixes. clearTimeout only cancels any showErr
|
|
6554
|
+
// 6s timer that happened to be mid-flight on this same shared bar.
|
|
5887
6555
|
clearTimeout(errTimer);
|
|
5888
|
-
|
|
6556
|
+
billingBlockedActive = true;
|
|
5889
6557
|
}
|
|
5890
6558
|
|
|
5891
6559
|
// Worker sign-in banner. The server preflights `claude` at boot (/api/state.auth
|
|
@@ -5937,27 +6605,45 @@ function showSignInNudge(){
|
|
|
5937
6605
|
errTimer = setTimeout(() => { bar.style.display = 'none'; bar.classList.remove('billing'); }, 20000);
|
|
5938
6606
|
}
|
|
5939
6607
|
|
|
5940
|
-
//
|
|
5941
|
-
//
|
|
6608
|
+
// The Stripe-owned price (state.billing.price), never hardcoded. Split into the
|
|
6609
|
+
// currency mark + the figure so the ONE price format (§5.5 / .bx-pm) can render
|
|
6610
|
+
// the mark smaller and optically raised. No interval — /mo is dropped on the
|
|
6611
|
+
// button (the number is the message). Returns null when the price isn't known
|
|
6612
|
+
// yet so the CTA just reads "Upgrade".
|
|
5942
6613
|
function fmtPrice(p){
|
|
5943
|
-
if (!p || p.amount == null) return
|
|
6614
|
+
if (!p || p.amount == null) return null;
|
|
5944
6615
|
const n = p.amount / 100;
|
|
5945
6616
|
const amt = Number.isInteger(n) ? String(n) : n.toFixed(2);
|
|
5946
6617
|
const cur = String(p.currency || 'usd').toLowerCase() === 'usd' ? '$' : (String(p.currency).toUpperCase() + ' ');
|
|
5947
|
-
|
|
5948
|
-
|
|
6618
|
+
return { cur, amt };
|
|
6619
|
+
}
|
|
6620
|
+
// Render a price into the single styled price span (small currency mark, no /mo).
|
|
6621
|
+
// '' when the price is unknown so callers can concatenate it unconditionally.
|
|
6622
|
+
function priceTag(p){
|
|
6623
|
+
const f = fmtPrice(p);
|
|
6624
|
+
return f ? `<span class="bx-pm"><span class="cur">${esc(f.cur)}</span>${esc(f.amt)}</span>` : '';
|
|
5949
6625
|
}
|
|
5950
6626
|
|
|
5951
6627
|
// Upgrade → Stripe Checkout (via the engine proxy, same-origin). On return the
|
|
5952
6628
|
// browser lands back with ?checkout=success|cancel (handled at load).
|
|
5953
|
-
|
|
6629
|
+
// btn (optional): the button that triggered this, so it can show a spinner
|
|
6630
|
+
// for the round trip to /api/checkout — jumping straight to Stripe's own page
|
|
6631
|
+
// with zero feedback reads as "did that even do anything?" (Masa 2026-07-17).
|
|
6632
|
+
// Left spinning through the r.ok/d.url success path on purpose: the page is
|
|
6633
|
+
// about to navigate away to Stripe, so there is nothing to reset back to.
|
|
6634
|
+
async function startCheckout(btn){
|
|
6635
|
+
if (btn) { btn.disabled = true; btn.classList.add('loading'); }
|
|
5954
6636
|
try {
|
|
5955
6637
|
const r = await fetch(withKey('/api/checkout'), { method: 'POST', headers: { 'content-type': 'application/json' },
|
|
5956
6638
|
body: JSON.stringify({ successUrl: location.origin + location.pathname + '?checkout=success', cancelUrl: location.origin + location.pathname + '?checkout=cancel' }) });
|
|
5957
6639
|
const d = await r.json().catch(() => ({}));
|
|
5958
|
-
if (r.ok && d.url) location.href = d.url;
|
|
5959
|
-
|
|
5960
|
-
|
|
6640
|
+
if (r.ok && d.url) { location.href = d.url; return; }
|
|
6641
|
+
if (btn) { btn.disabled = false; btn.classList.remove('loading'); }
|
|
6642
|
+
showErr(d.error || 'Could not start checkout.');
|
|
6643
|
+
} catch {
|
|
6644
|
+
if (btn) { btn.disabled = false; btn.classList.remove('loading'); }
|
|
6645
|
+
showErr('Could not start checkout — check your connection.');
|
|
6646
|
+
}
|
|
5961
6647
|
}
|
|
5962
6648
|
|
|
5963
6649
|
// Manage billing → Stripe Customer Portal (paying users; engine proxy).
|
|
@@ -5970,27 +6656,100 @@ async function openBillingPortal(){
|
|
|
5970
6656
|
} catch { showErr('Could not open billing — check your connection.'); }
|
|
5971
6657
|
}
|
|
5972
6658
|
|
|
5973
|
-
//
|
|
5974
|
-
//
|
|
5975
|
-
//
|
|
5976
|
-
//
|
|
6659
|
+
// The full-screen "you're Pro now" reward. Fires the instant the user returns
|
|
6660
|
+
// from a completed checkout — landing on the success_url IS Stripe's confirmation
|
|
6661
|
+
// that the payment went through, so there's nothing to wait for before rewarding
|
|
6662
|
+
// (the entitlement flip only has to catch up before this fades, ~2.6s). Built in
|
|
6663
|
+
// JS (no static markup), auto-dismisses, click-to-dismiss, reduced-motion aware.
|
|
6664
|
+
// The reward = green disc + WHITE check (the joyful, high-signal success mark,
|
|
6665
|
+
// not a dark glyph) that pops, a soft green flash pulse behind it, and a bright
|
|
6666
|
+
// confetti burst. The palette is a harmonized set of brights (DESIGN-RULES §5.7
|
|
6667
|
+
// motion) — NOT the drab CONFETTI_COLORS (which leans on dark --ink pieces). Kept
|
|
6668
|
+
// stylish: the brand green anchors, confetti is curated brights, not an even
|
|
6669
|
+
// rainbow (DESIGN-RULES §5.5 動き — 報酬モーション). Tune the two constants below.
|
|
6670
|
+
const UPCEL_BRIGHT = ['#3DDC97', '#22C7A9', '#FFC93C', '#FF8A5B', '#FF5FA2', '#7C9CFF', '#B07CFF'];
|
|
6671
|
+
const UPCEL_CONFETTI = { count: 46, ribbon: 0.4, spread: 220 }; // ribbon = fraction rendered as fluttering strips
|
|
6672
|
+
let upcelShown = false;
|
|
6673
|
+
function celebrateUpgrade(){
|
|
6674
|
+
if (upcelShown) return; // once per return, even if handleCheckoutReturn re-runs
|
|
6675
|
+
upcelShown = true;
|
|
6676
|
+
const lang = billingLang();
|
|
6677
|
+
const title = lang === 'ja' ? 'プロになりました' : "You're Pro now";
|
|
6678
|
+
const sub = lang === 'ja' ? '無制限のタスクとプロジェクトが使えます。' : 'Unlimited tasks and projects are unlocked.';
|
|
6679
|
+
const ov = document.createElement('div');
|
|
6680
|
+
ov.className = 'upcel';
|
|
6681
|
+
ov.innerHTML = '<div class="upcelcard"><div class="upcelburst"><span class="upcelflash"></span>'
|
|
6682
|
+
+ '<div class="upcelchk"><svg viewBox="0 0 24 24"><path d="M5 13l4 4L19 7"/></svg></div></div>'
|
|
6683
|
+
+ '<div class="upceltitle"></div><div class="upcelsub"></div></div>';
|
|
6684
|
+
ov.querySelector('.upceltitle').textContent = title; // textContent: copy is data, never markup
|
|
6685
|
+
ov.querySelector('.upcelsub').textContent = sub;
|
|
6686
|
+
document.body.appendChild(ov);
|
|
6687
|
+
requestAnimationFrame(() => ov.classList.add('show'));
|
|
6688
|
+
const reduce = window.matchMedia && window.matchMedia('(prefers-reduced-motion:reduce)').matches;
|
|
6689
|
+
if (!reduce) setTimeout(() => upcelConfetti(UPCEL_CONFETTI), 170); // reduced motion: static check, no confetti
|
|
6690
|
+
let done = false;
|
|
6691
|
+
const close = () => { if (done) return; done = true; ov.classList.add('out'); setTimeout(() => ov.remove(), 360); };
|
|
6692
|
+
ov.addEventListener('click', close);
|
|
6693
|
+
setTimeout(close, 2600);
|
|
6694
|
+
}
|
|
6695
|
+
// Radial confetti burst from the check, bright palette. cfg.ribbon = fraction of
|
|
6696
|
+
// pieces rendered as long fluttering strips; cfg.spread = travel radius.
|
|
6697
|
+
// .confetti-piece carries z-index:9999, so it rides above the .upcel scrim.
|
|
6698
|
+
function upcelConfetti(cfg){
|
|
6699
|
+
const cx = window.innerWidth / 2, cy = window.innerHeight * 0.42;
|
|
6700
|
+
for (let i = 0; i < cfg.count; i++){
|
|
6701
|
+
const el = document.createElement('div');
|
|
6702
|
+
el.className = 'confetti-piece';
|
|
6703
|
+
const ribbon = Math.random() < cfg.ribbon;
|
|
6704
|
+
let w, h;
|
|
6705
|
+
if (ribbon) { w = 4 + Math.random() * 3; h = 13 + Math.random() * 10; }
|
|
6706
|
+
else { const s = 7 + Math.random() * 7; w = s; h = s; }
|
|
6707
|
+
el.style.width = `${w}px`; el.style.height = `${h}px`;
|
|
6708
|
+
el.style.background = UPCEL_BRIGHT[i % UPCEL_BRIGHT.length];
|
|
6709
|
+
el.style.borderRadius = ribbon ? '2px' : (Math.random() < 0.5 ? '50%' : '2px');
|
|
6710
|
+
el.style.left = `${cx}px`; el.style.top = `${cy}px`;
|
|
6711
|
+
el.style.opacity = '1';
|
|
6712
|
+
el.style.transform = 'translate(-50%,-50%) rotate(0deg)';
|
|
6713
|
+
el.style.transition = 'transform 1.1s cubic-bezier(.12,.72,.28,1), opacity 1.1s ease .4s';
|
|
6714
|
+
document.body.appendChild(el);
|
|
6715
|
+
const ang = Math.random() * Math.PI * 2;
|
|
6716
|
+
const dist = 70 + Math.random() * cfg.spread;
|
|
6717
|
+
const dx = Math.cos(ang) * dist, dy = Math.sin(ang) * dist * 0.62 + 115; // burst out, settle down
|
|
6718
|
+
const rot = (Math.random() < 0.5 ? -1 : 1) * (420 + Math.random() * 560);
|
|
6719
|
+
requestAnimationFrame(() => {
|
|
6720
|
+
el.style.transform = `translate(calc(-50% + ${dx}px), calc(-50% + ${dy}px)) rotate(${rot}deg)`;
|
|
6721
|
+
el.style.opacity = '0';
|
|
6722
|
+
});
|
|
6723
|
+
setTimeout(() => el.remove(), 1300);
|
|
6724
|
+
}
|
|
6725
|
+
}
|
|
6726
|
+
// After returning from Stripe Checkout (?checkout=success|cancel), then URL cleanup.
|
|
6727
|
+
// On SUCCESS: lift the paywall UI at once (the #errbar billing banner is imperative,
|
|
6728
|
+
// not state-driven — refresh() alone never clears it, which is why it used to linger
|
|
6729
|
+
// ~5s until the entitlement poll finished) and celebrate. Keep nudging the engine to
|
|
6730
|
+
// re-check entitlement so the PLAN card flips to Paid; the first tick is tightened to
|
|
6731
|
+
// 700ms so the confirmation usually lands while the celebration is still on screen.
|
|
6732
|
+
// On CANCEL: the thin inline bar, unchanged.
|
|
5977
6733
|
function handleCheckoutReturn(){
|
|
5978
6734
|
const c = new URLSearchParams(location.search).get('checkout');
|
|
5979
6735
|
if (!c) return;
|
|
5980
|
-
const center = document.querySelector('#fsRoot .fscenter') || document.body;
|
|
5981
|
-
const bar = document.createElement('div');
|
|
5982
|
-
bar.className = 'bx-cobar ' + (c === 'success' ? 'bx-ok' : 'bx-cxl');
|
|
5983
|
-
bar.innerHTML = `<span class="bx-dot"></span><span class="bx-cotxt">${c === 'success' ? "<b>You're on Paid.</b> Free limits lifted." : "<b>Checkout canceled.</b> Nothing changed."}</span>`;
|
|
5984
|
-
center.insertBefore(bar, center.firstChild);
|
|
5985
6736
|
const url = new URL(location.href); url.searchParams.delete('checkout');
|
|
5986
6737
|
history.replaceState({}, '', url.pathname + (url.searchParams.toString() ? '?' + url.searchParams : ''));
|
|
5987
6738
|
if (c === 'success') {
|
|
6739
|
+
clearBillingBlocked(); // drop the paywall banner immediately (it is not tied to state)
|
|
6740
|
+
celebrateUpgrade();
|
|
5988
6741
|
let tries = 0;
|
|
5989
6742
|
const tick = () => { fetch(withKey('/api/refresh-billing'), { method: 'POST' })
|
|
5990
|
-
.then((r) => r.json()).then((d) => { if (!d?.isPaying && ++tries <
|
|
6743
|
+
.then((r) => r.json()).then((d) => { if (!d?.isPaying && ++tries < 5) setTimeout(tick, 1500); else refresh(); })
|
|
5991
6744
|
.catch(() => {}); };
|
|
5992
|
-
setTimeout(tick,
|
|
6745
|
+
setTimeout(tick, 700);
|
|
6746
|
+
return;
|
|
5993
6747
|
}
|
|
6748
|
+
const center = document.querySelector('#fsRoot .fscenter') || document.body;
|
|
6749
|
+
const bar = document.createElement('div');
|
|
6750
|
+
bar.className = 'bx-cobar bx-cxl';
|
|
6751
|
+
bar.innerHTML = '<span class="bx-dot"></span><span class="bx-cotxt"><b>Checkout canceled.</b> Nothing changed.</span>';
|
|
6752
|
+
center.insertBefore(bar, center.firstChild);
|
|
5994
6753
|
setTimeout(() => bar.remove(), 12000);
|
|
5995
6754
|
}
|
|
5996
6755
|
window.addEventListener('load', handleCheckoutReturn);
|
|
@@ -6329,6 +7088,10 @@ async function postOutboxItem(item){
|
|
|
6329
7088
|
return;
|
|
6330
7089
|
}
|
|
6331
7090
|
if (!r.ok) throw new Error(`HTTP ${r.status}`);
|
|
7091
|
+
// A send just succeeded, which is the one signal that's actually true
|
|
7092
|
+
// evidence the free-tier block lifted (upgrade, or usage dropping below
|
|
7093
|
+
// the limit) — clear the persistent paywall banner if it was still up.
|
|
7094
|
+
clearBillingBlocked();
|
|
6332
7095
|
const goal = await r.json();
|
|
6333
7096
|
if (goal.kind === 'chat') {
|
|
6334
7097
|
// Conversation-first (SLICE 2 + Pattern A H23): a chat/help/settings turn,
|
|
@@ -7299,6 +8062,10 @@ $('delCancelBtn').onclick = () => closeDeleteConfirm(false);
|
|
|
7299
8062
|
$('delConfirmBtn').onclick = () => closeDeleteConfirm(true);
|
|
7300
8063
|
$('deleteConfirmOverlay').addEventListener('click', (e) => { if (e.target === $('deleteConfirmOverlay')) closeDeleteConfirm(_delDismissValue); });
|
|
7301
8064
|
document.addEventListener('keydown', (e) => { if (e.key === 'Escape' && $('deleteConfirmOverlay').classList.contains('show')) closeDeleteConfirm(_delDismissValue); });
|
|
8065
|
+
// moment ③ dialog dismissal: "Not now" link, backdrop click, and Escape all close it.
|
|
8066
|
+
$('flimProjNo').onclick = () => closeProjectLimitDialog();
|
|
8067
|
+
$('flimProjOverlay').addEventListener('click', (e) => { if (e.target === $('flimProjOverlay')) closeProjectLimitDialog(); });
|
|
8068
|
+
document.addEventListener('keydown', (e) => { if (e.key === 'Escape' && $('flimProjOverlay').classList.contains('show')) closeProjectLimitDialog(); });
|
|
7302
8069
|
$('input').addEventListener('keydown', (e) => {
|
|
7303
8070
|
// Palette navigation takes priority while it is open (Claude-Code keys).
|
|
7304
8071
|
if (state.palette && !e.isComposing) {
|
|
@@ -7361,10 +8128,18 @@ applyLayout();
|
|
|
7361
8128
|
// default Gradient (Lavender). Pure token overrides + structural layers.
|
|
7362
8129
|
function applyTheme(){
|
|
7363
8130
|
document.body.dataset.theme = state.theme || 'gradient';
|
|
8131
|
+
document.body.dataset.boardskin = state.boardskin || 'auto'; // board surface skin (card default = the Artifact card) rides with the theme apply
|
|
7364
8132
|
for (const b of document.querySelectorAll('#themesel .laybtn')) b.classList.toggle('on', b.dataset.theme === (state.theme || 'gradient'));
|
|
8133
|
+
for (const b of document.querySelectorAll('#boardskinsel .laybtn')) b.classList.toggle('on', b.dataset.boardskin === (state.boardskin || 'auto'));
|
|
7365
8134
|
window.__glassSync?.(); // Glass is ARCHIVED (§6.5) — never selectable, so this guard keeps the WebGL loop dormant
|
|
7366
8135
|
fsGearAlignX(); // fonts/metrics can shift with the theme — re-zero the gear/toggle delta (v45 §2)
|
|
7367
8136
|
}
|
|
8137
|
+
// Board surface skin — card (the Artifact card, default) / quiet (like the To Do list) / frost. Switchable + persisted.
|
|
8138
|
+
const BOARDSKINS = ['auto', 'card', 'quiet', 'frost'];
|
|
8139
|
+
window.__boardskin = (v) => {
|
|
8140
|
+
const skin = typeof v === 'number' ? (BOARDSKINS[((v | 0) % 4 + 4) % 4]) : (BOARDSKINS.includes(v) ? v : 'auto');
|
|
8141
|
+
state.boardskin = skin; localStorage.setItem('boardskin', skin); applyTheme();
|
|
8142
|
+
};
|
|
7368
8143
|
for (const b of document.querySelectorAll('#themesel .laybtn')) {
|
|
7369
8144
|
b.onclick = () => { state.theme = b.dataset.theme; localStorage.setItem('theme', state.theme); applyTheme(); };
|
|
7370
8145
|
}
|
|
@@ -7997,24 +8772,25 @@ function gearSetHTML(){
|
|
|
7997
8772
|
// so the polished version can drop in without touching the wiring below.
|
|
7998
8773
|
// ③ PLAN card: identity (Sign in with Google / Signed in as X) + plan status.
|
|
7999
8774
|
// Free → usage vs limits + Upgrade; Paid → green dot + Manage (Stripe Portal).
|
|
8000
|
-
const pl = fmtPrice(bill.price);
|
|
8001
8775
|
const u = bill.usage || {}, lim = bill.limits || {};
|
|
8002
|
-
|
|
8003
|
-
|
|
8004
|
-
|
|
8776
|
+
// Account row: signed in (email + Log out) vs anonymous (Sign in with Google).
|
|
8777
|
+
const acct = bill.licensed
|
|
8778
|
+
? `<div class="srow"><div class="sl">Account<span class="sd">Signed in with Google · ${esc(bill.email)}</span></div><button class="optchip" data-gsignout="1">Log out</button></div>`
|
|
8779
|
+
: `<div class="srow"><div class="sl">Account<span class="sd">Sign in with Google to sync your usage across devices — the free tier works without it</span></div>
|
|
8005
8780
|
<button class="optchip" data-gsignin="1">Sign in with Google</button></div>
|
|
8006
8781
|
<div class="srow dis" id="licKeyMsg" style="display:none"></div>`;
|
|
8007
|
-
|
|
8008
|
-
|
|
8009
|
-
|
|
8010
|
-
|
|
8011
|
-
|
|
8012
|
-
billingRow = `<div class="srow"><div class="sl">Account<span class="sd">Signed in with Google · ${esc(bill.email)}</span></div><button class="optchip" data-gsignout="1">Log out</button></div>
|
|
8013
|
-
<div class="srow"><div class="sl">Plan</div><span class="bx-sval">Free</span></div>
|
|
8782
|
+
// The free-tier usage + Upgrade shows on the free plan whether or not the user
|
|
8783
|
+
// has signed in — it mirrors the To Do header counter (H22), which is visible
|
|
8784
|
+
// to anonymous free users too. Sign-in is an extra (sync across devices), not a
|
|
8785
|
+
// gate to see your own usage or upgrade.
|
|
8786
|
+
const freeUsage = `<div class="srow"><div class="sl">Plan</div><span class="bx-sval">Free</span></div>
|
|
8014
8787
|
<div class="srow"><div class="sl">To Dos<span class="sd">pending, this install</span></div><span class="bx-sval bx-num">${u.pending ?? 0} / ${lim.pending ?? 5}</span></div>
|
|
8015
|
-
<div class="srow"><div class="sl">Lifetime</div><span class="bx-sval bx-num">${u.cumulative ?? 0} / ${lim.cumulative ??
|
|
8016
|
-
<button class="bx-cta bx-up-set" data-gupgrade="1"
|
|
8017
|
-
|
|
8788
|
+
<div class="srow"><div class="sl">Lifetime</div><span class="bx-sval bx-num">${u.cumulative ?? 0} / ${lim.cumulative ?? 20}</span></div>
|
|
8789
|
+
<button class="bx-cta bx-up-set" data-gupgrade="1"><span class="bx-spin"></span>Upgrade${priceTag(bill.price)}</button>`;
|
|
8790
|
+
const billingRow = bill.plan === 'paid'
|
|
8791
|
+
? acct + `<div class="srow"><div class="sl">Plan</div><span class="bx-pdot"><i></i>Paid</span></div>
|
|
8792
|
+
<div class="srow"><div class="sl">Billing<span class="sd">manage or cancel in Stripe</span></div><button class="bx-cchip" data-gportal="1">Manage</button></div>`
|
|
8793
|
+
: acct + freeUsage;
|
|
8018
8794
|
return `
|
|
8019
8795
|
<div class="srow"><div class="sl">Language<span class="sd">Review checklist language for this project</span></div>
|
|
8020
8796
|
<button class="optchip${lang === 'auto' ? ' on' : ''}" data-gslang="auto">Auto</button>
|
|
@@ -8063,7 +8839,7 @@ function buildGearPop(){
|
|
|
8063
8839
|
signBtn.disabled = false;
|
|
8064
8840
|
};
|
|
8065
8841
|
const upBtn = pop.querySelector('[data-gupgrade]');
|
|
8066
|
-
if (upBtn) upBtn.onclick = () => startCheckout();
|
|
8842
|
+
if (upBtn) upBtn.onclick = () => startCheckout(upBtn);
|
|
8067
8843
|
const portalBtn = pop.querySelector('[data-gportal]');
|
|
8068
8844
|
if (portalBtn) portalBtn.onclick = () => openBillingPortal();
|
|
8069
8845
|
const signOutBtn = pop.querySelector('[data-gsignout]');
|
|
@@ -8097,6 +8873,9 @@ function buildGearPop(){
|
|
|
8097
8873
|
};
|
|
8098
8874
|
const themeBtns = [['studio', 'Studio'], ['gradient', 'Gradient'], ['banff', 'Banff'], ['midnight', 'Midnight'], ['cinema', 'Cinema']].map(([v, l]) =>
|
|
8099
8875
|
`<button class="thtile${(state.theme || 'gradient') === v ? ' on' : ''}" data-gtheme="${v}"><span class="tp" style="background:${THEME_TP[v]}"></span><span>${l}</span></button>`).join('');
|
|
8876
|
+
// Board surface — Quiet (like the To Do list, default) / Paper (card lifts) / Frost (glass over the wallpaper). Applies in every theme.
|
|
8877
|
+
const boardChips = [['auto', 'Auto'], ['card', 'Card'], ['quiet', 'Quiet'], ['frost', 'Frost']].map(([v, l]) =>
|
|
8878
|
+
`<button class="optchip${(state.boardskin || 'auto') === v ? ' on' : ''}" data-gboardskin="${v}">${l}</button>`).join('');
|
|
8100
8879
|
const grad2 = (p) => `linear-gradient(135deg,${p.join(',')})`;
|
|
8101
8880
|
const arcSw = ARC17.map((p, i) =>
|
|
8102
8881
|
`<button class="sw${cust.arc === i ? ' on' : ''}" data-garc="${i}" title="${ARC_NAMES[i]}" style="background:${grad2(p)}"></button>`).join('');
|
|
@@ -8146,6 +8925,7 @@ function buildGearPop(){
|
|
|
8146
8925
|
// FONT exploration section retired (v45 §1): the brand fonts are final — no chips.
|
|
8147
8926
|
pop.innerHTML = gearTabsHTML() +
|
|
8148
8927
|
`<h4>THEME</h4><div class="swrow">${themeBtns}</div>` +
|
|
8928
|
+
`<h4>BOARD</h4><div class="swrow">${boardChips}</div>` +
|
|
8149
8929
|
sec(th === 'gradient', `<h4>COLOR</h4><div class="swrow">${arcSw}</div>` +
|
|
8150
8930
|
`<h4>SURFACE</h4><div class="swrow">${surfChips}</div>` +
|
|
8151
8931
|
`<h4>TEXTBOX</h4><div class="swrow">${tbChips}</div>`) +
|
|
@@ -8166,6 +8946,7 @@ function buildGearPop(){
|
|
|
8166
8946
|
// EDIT SESSION (v45 §3): every handler below is LIVE PREVIEW ONLY — mutate cust/state,
|
|
8167
8947
|
// apply to the DOM, re-render the pop. localStorage is written ONLY in custSave().
|
|
8168
8948
|
for (const b of pop.querySelectorAll('[data-gtheme]')) b.onclick = () => { state.theme = b.dataset.gtheme; applyTheme(); applyCust(); buildGearPop(); };
|
|
8949
|
+
for (const b of pop.querySelectorAll('[data-gboardskin]')) b.onclick = () => { window.__boardskin(b.dataset.gboardskin); buildGearPop(); };
|
|
8169
8950
|
for (const b of pop.querySelectorAll('[data-garc]')) b.onclick = () => { cust.arc = Number(b.dataset.garc); applyCust(); buildGearPop(); };
|
|
8170
8951
|
for (const b of pop.querySelectorAll('[data-gapanel]')) b.onclick = () => { cust.arcPanel = Number(b.dataset.gapanel); applyCust(); buildGearPop(); };
|
|
8171
8952
|
for (const b of pop.querySelectorAll('[data-garctb]')) b.onclick = () => { cust.arctb = Number(b.dataset.garctb); applyCust(); buildGearPop(); };
|