@bakapiano/ccsm 0.5.0 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/config.js +1 -0
- package/lib/focus.js +90 -14
- package/lib/labels.js +49 -0
- package/lib/workspace.js +8 -4
- package/package.json +1 -1
- package/public/app.js +556 -97
- package/public/favicon.svg +18 -0
- package/public/index.html +135 -23
- package/public/styles.css +464 -29
- package/server.js +28 -1
package/public/styles.css
CHANGED
|
@@ -61,6 +61,7 @@
|
|
|
61
61
|
}
|
|
62
62
|
|
|
63
63
|
* { box-sizing: border-box; margin: 0; padding: 0; }
|
|
64
|
+
[hidden] { display: none !important; }
|
|
64
65
|
|
|
65
66
|
html, body {
|
|
66
67
|
background: var(--bg);
|
|
@@ -124,10 +125,9 @@ html, body {
|
|
|
124
125
|
width: 32px;
|
|
125
126
|
height: 32px;
|
|
126
127
|
flex: 0 0 32px;
|
|
127
|
-
background:
|
|
128
|
-
color: var(--bg-elev);
|
|
129
|
-
border-radius: var(--r-sm);
|
|
128
|
+
background: transparent; /* SVG draws its own terminal window */
|
|
130
129
|
}
|
|
130
|
+
.brand-mark svg { display: block; }
|
|
131
131
|
.brand-name {
|
|
132
132
|
font-size: 19px;
|
|
133
133
|
font-weight: 600;
|
|
@@ -155,7 +155,8 @@ html, body {
|
|
|
155
155
|
align-items: center;
|
|
156
156
|
gap: var(--s-3);
|
|
157
157
|
width: 100%;
|
|
158
|
-
padding:
|
|
158
|
+
padding: 0 10px;
|
|
159
|
+
min-height: 36px; /* uniform across nav + util items */
|
|
159
160
|
border-radius: var(--r-sm);
|
|
160
161
|
cursor: pointer;
|
|
161
162
|
color: var(--ink-mid);
|
|
@@ -185,6 +186,30 @@ html, body {
|
|
|
185
186
|
background: var(--accent);
|
|
186
187
|
border-radius: 2px;
|
|
187
188
|
}
|
|
189
|
+
|
|
190
|
+
/* "Has unsaved changes" indicator — small accent dot next to the label */
|
|
191
|
+
.nav-item.has-changes::after {
|
|
192
|
+
content: "";
|
|
193
|
+
position: absolute;
|
|
194
|
+
right: 10px;
|
|
195
|
+
top: 50%;
|
|
196
|
+
transform: translateY(-50%);
|
|
197
|
+
width: 7px;
|
|
198
|
+
height: 7px;
|
|
199
|
+
border-radius: 50%;
|
|
200
|
+
background: var(--accent);
|
|
201
|
+
box-shadow: 0 0 0 0 rgba(196, 95, 63, 0.45);
|
|
202
|
+
animation: dirty-pulse 2s ease-in-out infinite;
|
|
203
|
+
}
|
|
204
|
+
@keyframes dirty-pulse {
|
|
205
|
+
0%, 100% { box-shadow: 0 0 0 0 rgba(196, 95, 63, 0.45); }
|
|
206
|
+
50% { box-shadow: 0 0 0 5px rgba(196, 95, 63, 0); }
|
|
207
|
+
}
|
|
208
|
+
.sidebar[data-collapsed="true"] .nav-item.has-changes::after {
|
|
209
|
+
right: auto;
|
|
210
|
+
top: 6px;
|
|
211
|
+
left: 28px;
|
|
212
|
+
}
|
|
188
213
|
.nav-icon {
|
|
189
214
|
display: inline-flex;
|
|
190
215
|
width: 18px;
|
|
@@ -324,6 +349,61 @@ html, body {
|
|
|
324
349
|
.ph-val { color: var(--ink-mid); }
|
|
325
350
|
.ph-divider { color: var(--ink-faint); }
|
|
326
351
|
|
|
352
|
+
/* Server status pill — "● online v0.5.0" / "● offline" */
|
|
353
|
+
.server-status {
|
|
354
|
+
display: inline-flex;
|
|
355
|
+
align-items: center;
|
|
356
|
+
gap: 6px;
|
|
357
|
+
padding: 2px 9px 2px 7px;
|
|
358
|
+
border-radius: 999px;
|
|
359
|
+
background: var(--bg);
|
|
360
|
+
border: 1px solid var(--border);
|
|
361
|
+
font-family: var(--mono);
|
|
362
|
+
font-size: 10.5px;
|
|
363
|
+
letter-spacing: 0.02em;
|
|
364
|
+
color: var(--ink-mid);
|
|
365
|
+
transition: background .15s ease, border-color .15s ease, color .15s ease;
|
|
366
|
+
cursor: default;
|
|
367
|
+
}
|
|
368
|
+
.server-status .status-pulse {
|
|
369
|
+
width: 6px;
|
|
370
|
+
height: 6px;
|
|
371
|
+
border-radius: 50%;
|
|
372
|
+
background: var(--ink-faint);
|
|
373
|
+
flex: 0 0 6px;
|
|
374
|
+
position: relative;
|
|
375
|
+
}
|
|
376
|
+
.server-status[data-state="online"] {
|
|
377
|
+
border-color: rgba(74, 138, 74, 0.35);
|
|
378
|
+
background: rgba(74, 138, 74, 0.06);
|
|
379
|
+
color: var(--green);
|
|
380
|
+
}
|
|
381
|
+
.server-status[data-state="online"] .status-pulse {
|
|
382
|
+
background: var(--green);
|
|
383
|
+
animation: server-pulse 2.2s ease-in-out infinite;
|
|
384
|
+
}
|
|
385
|
+
.server-status[data-state="offline"] {
|
|
386
|
+
border-color: rgba(183, 63, 63, 0.4);
|
|
387
|
+
background: rgba(183, 63, 63, 0.06);
|
|
388
|
+
color: var(--red);
|
|
389
|
+
}
|
|
390
|
+
.server-status[data-state="offline"] .status-pulse {
|
|
391
|
+
background: var(--red);
|
|
392
|
+
}
|
|
393
|
+
.server-status[data-state="connecting"] {
|
|
394
|
+
border-color: rgba(196, 137, 43, 0.4);
|
|
395
|
+
background: rgba(196, 137, 43, 0.06);
|
|
396
|
+
color: var(--yellow);
|
|
397
|
+
}
|
|
398
|
+
.server-status[data-state="connecting"] .status-pulse {
|
|
399
|
+
background: var(--yellow);
|
|
400
|
+
animation: server-pulse 1s ease-in-out infinite;
|
|
401
|
+
}
|
|
402
|
+
@keyframes server-pulse {
|
|
403
|
+
0%, 100% { box-shadow: 0 0 0 0 currentColor; }
|
|
404
|
+
50% { box-shadow: 0 0 0 4px transparent; }
|
|
405
|
+
}
|
|
406
|
+
|
|
327
407
|
.content {
|
|
328
408
|
flex: 1;
|
|
329
409
|
display: flex;
|
|
@@ -360,9 +440,48 @@ html, body {
|
|
|
360
440
|
padding: var(--s-4) var(--s-6) var(--s-3);
|
|
361
441
|
border-bottom: 1px solid var(--border-soft);
|
|
362
442
|
display: flex;
|
|
363
|
-
justify-content:
|
|
364
|
-
align-items:
|
|
365
|
-
gap: var(--s-
|
|
443
|
+
justify-content: flex-start;
|
|
444
|
+
align-items: center;
|
|
445
|
+
gap: var(--s-3);
|
|
446
|
+
}
|
|
447
|
+
/* Make the whole header clickable to fold (only when card is foldable) */
|
|
448
|
+
.card[data-fold-key] .card-head {
|
|
449
|
+
cursor: pointer;
|
|
450
|
+
user-select: none;
|
|
451
|
+
transition: background .12s ease;
|
|
452
|
+
}
|
|
453
|
+
.card[data-fold-key] .card-head:hover {
|
|
454
|
+
background: var(--bg);
|
|
455
|
+
}
|
|
456
|
+
.card[data-collapsed] .card-head { border-bottom-color: transparent; }
|
|
457
|
+
.card-titles { flex: 1; min-width: 0; }
|
|
458
|
+
|
|
459
|
+
/* Fold toggle button in card head */
|
|
460
|
+
.card-fold {
|
|
461
|
+
appearance: none;
|
|
462
|
+
background: transparent;
|
|
463
|
+
border: 0;
|
|
464
|
+
padding: 4px;
|
|
465
|
+
margin: 0;
|
|
466
|
+
cursor: pointer;
|
|
467
|
+
color: var(--ink-muted);
|
|
468
|
+
display: inline-flex;
|
|
469
|
+
align-items: center;
|
|
470
|
+
justify-content: center;
|
|
471
|
+
border-radius: 4px;
|
|
472
|
+
transition: color .12s ease, background .12s ease, transform .25s cubic-bezier(.4, 0, .2, 1);
|
|
473
|
+
line-height: 0;
|
|
474
|
+
flex: 0 0 auto;
|
|
475
|
+
}
|
|
476
|
+
.card-fold:hover {
|
|
477
|
+
color: var(--ink);
|
|
478
|
+
background: var(--bg);
|
|
479
|
+
}
|
|
480
|
+
.card[data-collapsed] .card-fold {
|
|
481
|
+
transform: rotate(-90deg);
|
|
482
|
+
}
|
|
483
|
+
.card[data-collapsed] .card-body {
|
|
484
|
+
display: none;
|
|
366
485
|
}
|
|
367
486
|
.card-titles { min-width: 0; }
|
|
368
487
|
.card-title {
|
|
@@ -394,20 +513,34 @@ html, body {
|
|
|
394
513
|
Page-level inline actions (above the cards on a tab)
|
|
395
514
|
───────────────────────────────────────────────────────────── */
|
|
396
515
|
|
|
516
|
+
/* CTA banner — visually distinct from the data cards: subtle accent tint,
|
|
517
|
+
no card shadow, dashed-ish bottom rule, no rounded corners as heavy.
|
|
518
|
+
Reads as "tip / shortcut" rather than another data section. */
|
|
397
519
|
.page-actions {
|
|
398
520
|
display: flex;
|
|
399
521
|
align-items: center;
|
|
400
522
|
justify-content: space-between;
|
|
401
523
|
gap: var(--s-4);
|
|
402
524
|
padding: var(--s-3) var(--s-5);
|
|
403
|
-
background:
|
|
404
|
-
border: 1px solid
|
|
405
|
-
border-radius: var(--r-
|
|
406
|
-
box-shadow:
|
|
525
|
+
background: linear-gradient(180deg, rgba(196,95,63,0.06), rgba(196,95,63,0.02));
|
|
526
|
+
border: 1px solid rgba(196,95,63,0.18);
|
|
527
|
+
border-radius: var(--r-sm);
|
|
528
|
+
box-shadow: none;
|
|
529
|
+
position: relative;
|
|
530
|
+
}
|
|
531
|
+
.page-actions::before {
|
|
532
|
+
content: "";
|
|
533
|
+
position: absolute;
|
|
534
|
+
left: 0; top: 8px; bottom: 8px;
|
|
535
|
+
width: 2px;
|
|
536
|
+
background: var(--accent);
|
|
537
|
+
border-radius: 0 2px 2px 0;
|
|
407
538
|
}
|
|
408
539
|
.page-actions-hint {
|
|
409
540
|
font-size: 13px;
|
|
410
|
-
color: var(--
|
|
541
|
+
color: var(--accent-deep);
|
|
542
|
+
font-weight: 500;
|
|
543
|
+
padding-left: var(--s-2);
|
|
411
544
|
}
|
|
412
545
|
.page-actions .action svg { stroke-width: 2; }
|
|
413
546
|
|
|
@@ -417,10 +550,15 @@ html, body {
|
|
|
417
550
|
|
|
418
551
|
.table-scroll {
|
|
419
552
|
overflow-x: auto;
|
|
420
|
-
/*
|
|
553
|
+
/* tactile: scroll-padding so the actions column edges aren't flush against
|
|
554
|
+
the viewport when you scroll right to reach the buttons */
|
|
555
|
+
scroll-padding-right: var(--s-6);
|
|
421
556
|
}
|
|
422
|
-
.table-scroll .data { min-width: 760px; }
|
|
423
557
|
.table-scroll::-webkit-scrollbar { height: 8px; }
|
|
558
|
+
.table-scroll .data {
|
|
559
|
+
min-width: 960px;
|
|
560
|
+
table-layout: auto;
|
|
561
|
+
}
|
|
424
562
|
|
|
425
563
|
.data {
|
|
426
564
|
width: 100%;
|
|
@@ -443,11 +581,39 @@ html, body {
|
|
|
443
581
|
white-space: nowrap;
|
|
444
582
|
}
|
|
445
583
|
.data thead th.num { text-align: right; }
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
.data thead th.col-
|
|
584
|
+
/* shrink-to-fit pattern: width:1% with nowrap → the cell only takes the
|
|
585
|
+
space its content needs, the title / path cells get the remainder */
|
|
586
|
+
.data thead th.col-mark { width: 28px; padding-left: 0; padding-right: 0; }
|
|
587
|
+
.data thead th.col-star { width: 28px; padding-left: 0; padding-right: 0; }
|
|
588
|
+
.data thead th.col-actions { width: 1%; white-space: nowrap; }
|
|
589
|
+
.data thead th.num { width: 1%; white-space: nowrap; }
|
|
590
|
+
.data tbody td.num { white-space: nowrap; }
|
|
591
|
+
.data tbody td:last-child { white-space: nowrap; }
|
|
449
592
|
.data thead th:first-child { padding-left: var(--s-6); }
|
|
450
|
-
.data thead th:last-child
|
|
593
|
+
.data thead th:last-child { padding-right: var(--s-6); }
|
|
594
|
+
|
|
595
|
+
/* ── Sticky actions column ──
|
|
596
|
+
The last column (Focus/Resume/Continue buttons) stays pinned to the
|
|
597
|
+
right edge of the .table-scroll container while horizontal scroll happens
|
|
598
|
+
underneath. Backgrounds must be solid because sticky cells overlay the
|
|
599
|
+
content scrolling beneath them. */
|
|
600
|
+
.data thead th:last-child,
|
|
601
|
+
.data tbody td:last-child {
|
|
602
|
+
position: sticky;
|
|
603
|
+
right: 0;
|
|
604
|
+
z-index: 1;
|
|
605
|
+
/* fade the seam into the cell so it doesn't look like a hard cut */
|
|
606
|
+
box-shadow: -10px 0 10px -8px rgba(26, 24, 21, 0.08);
|
|
607
|
+
}
|
|
608
|
+
.data thead th:last-child {
|
|
609
|
+
background: var(--bg); /* matches thead bg */
|
|
610
|
+
}
|
|
611
|
+
.data tbody td:last-child {
|
|
612
|
+
background: var(--bg-elev); /* matches card bg */
|
|
613
|
+
}
|
|
614
|
+
.data tbody tr:hover td:last-child {
|
|
615
|
+
background: var(--bg); /* match row hover */
|
|
616
|
+
}
|
|
451
617
|
|
|
452
618
|
.data tbody tr {
|
|
453
619
|
border-bottom: 1px solid var(--border-soft);
|
|
@@ -519,13 +685,21 @@ html, body {
|
|
|
519
685
|
}
|
|
520
686
|
}
|
|
521
687
|
|
|
522
|
-
/* Composite cells
|
|
523
|
-
|
|
688
|
+
/* Composite cells — title and path are fixed-width with overflow ellipsis
|
|
689
|
+
on the inner spans, so the actions column is always visible without
|
|
690
|
+
horizontal scroll at typical viewport widths. Long titles / paths
|
|
691
|
+
truncate with ellipsis (hover shows full value via title attribute). */
|
|
692
|
+
.title-cell {
|
|
693
|
+
width: 300px;
|
|
694
|
+
max-width: 300px;
|
|
695
|
+
min-width: 0;
|
|
696
|
+
}
|
|
524
697
|
.title-cell .title-row {
|
|
525
698
|
display: flex;
|
|
526
699
|
align-items: center;
|
|
527
700
|
gap: 6px;
|
|
528
701
|
min-width: 0;
|
|
702
|
+
width: 100%;
|
|
529
703
|
}
|
|
530
704
|
.title-cell .primary {
|
|
531
705
|
color: var(--ink);
|
|
@@ -543,13 +717,18 @@ html, body {
|
|
|
543
717
|
color: var(--ink-muted);
|
|
544
718
|
letter-spacing: 0.02em;
|
|
545
719
|
margin-top: 2px;
|
|
720
|
+
white-space: nowrap;
|
|
721
|
+
overflow: hidden;
|
|
722
|
+
text-overflow: ellipsis;
|
|
546
723
|
}
|
|
547
724
|
|
|
548
725
|
.path-cell {
|
|
549
726
|
font-family: var(--mono);
|
|
550
727
|
font-size: 11.5px;
|
|
551
728
|
color: var(--ink-mid);
|
|
552
|
-
|
|
729
|
+
width: 260px;
|
|
730
|
+
max-width: 260px;
|
|
731
|
+
min-width: 0;
|
|
553
732
|
white-space: nowrap;
|
|
554
733
|
overflow: hidden;
|
|
555
734
|
text-overflow: ellipsis;
|
|
@@ -573,8 +752,8 @@ html, body {
|
|
|
573
752
|
}
|
|
574
753
|
|
|
575
754
|
/* Star button (favorite toggle) — sits inline next to the title text.
|
|
576
|
-
Outline by default
|
|
577
|
-
|
|
755
|
+
Outline by default, brightens on hover, fills with the accent color
|
|
756
|
+
when starred. */
|
|
578
757
|
.star-btn {
|
|
579
758
|
appearance: none;
|
|
580
759
|
background: transparent;
|
|
@@ -582,7 +761,7 @@ html, body {
|
|
|
582
761
|
padding: 2px;
|
|
583
762
|
margin: 0;
|
|
584
763
|
cursor: pointer;
|
|
585
|
-
color: var(--
|
|
764
|
+
color: var(--ink-muted);
|
|
586
765
|
display: inline-flex;
|
|
587
766
|
align-items: center;
|
|
588
767
|
justify-content: center;
|
|
@@ -590,30 +769,67 @@ html, body {
|
|
|
590
769
|
transition: color .12s ease, background .12s ease, transform .15s ease;
|
|
591
770
|
line-height: 0;
|
|
592
771
|
flex: 0 0 auto;
|
|
593
|
-
opacity: 0.55;
|
|
594
772
|
}
|
|
595
773
|
.data tbody tr:hover .star-btn {
|
|
596
|
-
|
|
597
|
-
color: var(--ink-faint);
|
|
774
|
+
color: var(--ink-mid);
|
|
598
775
|
}
|
|
599
776
|
.star-btn:hover {
|
|
600
777
|
color: var(--accent) !important;
|
|
601
778
|
background: var(--accent-softer);
|
|
602
|
-
opacity: 1 !important;
|
|
603
779
|
}
|
|
604
780
|
.star-btn:active { transform: scale(0.88); }
|
|
605
781
|
.star-btn.is-fav {
|
|
606
782
|
color: var(--accent);
|
|
607
|
-
opacity: 1;
|
|
608
783
|
}
|
|
609
784
|
.star-btn svg { display: block; }
|
|
610
785
|
|
|
786
|
+
/* Rename (pencil) button — inline next to title, only visible on row hover
|
|
787
|
+
so it doesn't compete with the star or distract from the title. When a
|
|
788
|
+
label exists (`has-label`), it stays subtly visible so the user knows
|
|
789
|
+
the title is overridden. */
|
|
790
|
+
.rename-btn {
|
|
791
|
+
appearance: none;
|
|
792
|
+
background: transparent;
|
|
793
|
+
border: 0;
|
|
794
|
+
padding: 2px;
|
|
795
|
+
margin: 0;
|
|
796
|
+
cursor: pointer;
|
|
797
|
+
color: var(--ink-faint);
|
|
798
|
+
opacity: 0;
|
|
799
|
+
display: inline-flex;
|
|
800
|
+
align-items: center;
|
|
801
|
+
justify-content: center;
|
|
802
|
+
border-radius: 4px;
|
|
803
|
+
transition: opacity .12s ease, color .12s ease, background .12s ease, transform .15s ease;
|
|
804
|
+
line-height: 0;
|
|
805
|
+
flex: 0 0 auto;
|
|
806
|
+
}
|
|
807
|
+
.data tbody tr:hover .rename-btn {
|
|
808
|
+
opacity: 1;
|
|
809
|
+
color: var(--ink-muted);
|
|
810
|
+
}
|
|
811
|
+
.rename-btn:hover {
|
|
812
|
+
color: var(--accent) !important;
|
|
813
|
+
background: var(--accent-softer);
|
|
814
|
+
}
|
|
815
|
+
.rename-btn:active { transform: scale(0.88); }
|
|
816
|
+
.rename-btn.has-label {
|
|
817
|
+
opacity: 0.8;
|
|
818
|
+
color: var(--accent);
|
|
819
|
+
}
|
|
820
|
+
.rename-btn svg { display: block; }
|
|
821
|
+
|
|
611
822
|
/* Title with icon glyph */
|
|
612
823
|
.card-title .title-icon {
|
|
613
824
|
color: var(--accent);
|
|
614
825
|
margin-right: 6px;
|
|
615
826
|
vertical-align: -2px;
|
|
616
827
|
}
|
|
828
|
+
.card-title .title-icon-after {
|
|
829
|
+
margin-right: 0;
|
|
830
|
+
margin-left: 6px;
|
|
831
|
+
vertical-align: -1px;
|
|
832
|
+
}
|
|
617
833
|
|
|
618
834
|
/* Favorites empty state — sits inside the card body (not generic table empty) */
|
|
619
835
|
#favoritesEmpty {
|
|
@@ -1173,6 +1389,225 @@ input[type="checkbox"]:checked::after {
|
|
|
1173
1389
|
.toast.error { border-left-color: var(--red); }
|
|
1174
1390
|
.toast.ok { border-left-color: var(--green); }
|
|
1175
1391
|
|
|
1392
|
+
/* ─────────────────────────────────────────────────────────────
|
|
1393
|
+
FAB · floating action button (bottom-right "+ new session")
|
|
1394
|
+
───────────────────────────────────────────────────────────── */
|
|
1395
|
+
|
|
1396
|
+
.fab {
|
|
1397
|
+
position: fixed;
|
|
1398
|
+
bottom: var(--s-6);
|
|
1399
|
+
right: var(--s-6);
|
|
1400
|
+
z-index: 90;
|
|
1401
|
+
width: 52px;
|
|
1402
|
+
height: 52px;
|
|
1403
|
+
border-radius: 50%;
|
|
1404
|
+
background: var(--accent);
|
|
1405
|
+
color: var(--bg-elev);
|
|
1406
|
+
border: 0;
|
|
1407
|
+
cursor: pointer;
|
|
1408
|
+
display: inline-flex;
|
|
1409
|
+
align-items: center;
|
|
1410
|
+
justify-content: center;
|
|
1411
|
+
box-shadow:
|
|
1412
|
+
0 8px 24px -6px rgba(196, 95, 63, 0.5),
|
|
1413
|
+
0 2px 6px -1px rgba(26, 24, 21, 0.15);
|
|
1414
|
+
transition: background .12s ease, transform .15s ease, box-shadow .15s ease;
|
|
1415
|
+
}
|
|
1416
|
+
.fab:hover {
|
|
1417
|
+
background: var(--accent-deep);
|
|
1418
|
+
transform: translateY(-1px) scale(1.04);
|
|
1419
|
+
box-shadow:
|
|
1420
|
+
0 12px 28px -6px rgba(196, 95, 63, 0.6),
|
|
1421
|
+
0 4px 10px -2px rgba(26, 24, 21, 0.2);
|
|
1422
|
+
}
|
|
1423
|
+
.fab:active { transform: scale(0.96); }
|
|
1424
|
+
|
|
1425
|
+
/* ─────────────────────────────────────────────────────────────
|
|
1426
|
+
Modal dialog
|
|
1427
|
+
───────────────────────────────────────────────────────────── */
|
|
1428
|
+
|
|
1429
|
+
.modal-backdrop {
|
|
1430
|
+
position: fixed;
|
|
1431
|
+
inset: 0;
|
|
1432
|
+
z-index: 200;
|
|
1433
|
+
background: rgba(26, 24, 21, 0.42);
|
|
1434
|
+
backdrop-filter: blur(4px);
|
|
1435
|
+
-webkit-backdrop-filter: blur(4px);
|
|
1436
|
+
display: flex;
|
|
1437
|
+
align-items: center;
|
|
1438
|
+
justify-content: center;
|
|
1439
|
+
padding: var(--s-6);
|
|
1440
|
+
animation: backdrop-in .18s ease;
|
|
1441
|
+
}
|
|
1442
|
+
@keyframes backdrop-in { from { opacity: 0; } to { opacity: 1; } }
|
|
1443
|
+
|
|
1444
|
+
.modal {
|
|
1445
|
+
background: var(--bg-elev);
|
|
1446
|
+
border: 1px solid var(--border);
|
|
1447
|
+
border-radius: var(--r-md);
|
|
1448
|
+
width: min(560px, 100%);
|
|
1449
|
+
max-height: 90vh;
|
|
1450
|
+
display: flex;
|
|
1451
|
+
flex-direction: column;
|
|
1452
|
+
overflow: hidden;
|
|
1453
|
+
box-shadow:
|
|
1454
|
+
0 24px 64px -16px rgba(26, 24, 21, 0.35),
|
|
1455
|
+
0 4px 12px -2px rgba(26, 24, 21, 0.15);
|
|
1456
|
+
animation: modal-in .22s cubic-bezier(.4, 0, .2, 1);
|
|
1457
|
+
}
|
|
1458
|
+
@keyframes modal-in {
|
|
1459
|
+
from { opacity: 0; transform: translateY(12px) scale(0.98); }
|
|
1460
|
+
to { opacity: 1; transform: translateY(0) scale(1); }
|
|
1461
|
+
}
|
|
1462
|
+
|
|
1463
|
+
.modal-head {
|
|
1464
|
+
display: flex;
|
|
1465
|
+
align-items: center;
|
|
1466
|
+
justify-content: space-between;
|
|
1467
|
+
padding: var(--s-4) var(--s-6);
|
|
1468
|
+
border-bottom: 1px solid var(--border-soft);
|
|
1469
|
+
}
|
|
1470
|
+
.modal-head h2 {
|
|
1471
|
+
font-size: 16px;
|
|
1472
|
+
font-weight: 600;
|
|
1473
|
+
color: var(--ink);
|
|
1474
|
+
}
|
|
1475
|
+
.modal-close {
|
|
1476
|
+
appearance: none;
|
|
1477
|
+
background: transparent;
|
|
1478
|
+
border: 0;
|
|
1479
|
+
padding: 4px;
|
|
1480
|
+
margin: -4px;
|
|
1481
|
+
cursor: pointer;
|
|
1482
|
+
color: var(--ink-muted);
|
|
1483
|
+
border-radius: 4px;
|
|
1484
|
+
display: inline-flex;
|
|
1485
|
+
transition: color .12s ease, background .12s ease;
|
|
1486
|
+
}
|
|
1487
|
+
.modal-close:hover { color: var(--ink); background: var(--bg); }
|
|
1488
|
+
|
|
1489
|
+
.modal-body {
|
|
1490
|
+
padding: var(--s-5) var(--s-6);
|
|
1491
|
+
overflow-y: auto;
|
|
1492
|
+
flex: 1;
|
|
1493
|
+
}
|
|
1494
|
+
.modal-hint {
|
|
1495
|
+
font-size: 13px;
|
|
1496
|
+
color: var(--ink-muted);
|
|
1497
|
+
margin-bottom: var(--s-4);
|
|
1498
|
+
}
|
|
1499
|
+
.modal-hint code {
|
|
1500
|
+
font-family: var(--mono);
|
|
1501
|
+
font-size: 11.5px;
|
|
1502
|
+
background: var(--bg);
|
|
1503
|
+
padding: 1px 5px;
|
|
1504
|
+
border-radius: 4px;
|
|
1505
|
+
border: 1px solid var(--border-soft);
|
|
1506
|
+
}
|
|
1507
|
+
|
|
1508
|
+
.modal-foot {
|
|
1509
|
+
display: flex;
|
|
1510
|
+
justify-content: flex-end;
|
|
1511
|
+
gap: var(--s-3);
|
|
1512
|
+
padding: var(--s-3) var(--s-6);
|
|
1513
|
+
border-top: 1px solid var(--border-soft);
|
|
1514
|
+
background: var(--bg);
|
|
1515
|
+
}
|
|
1516
|
+
|
|
1517
|
+
.repos-inline-config {
|
|
1518
|
+
margin: var(--s-3) 0;
|
|
1519
|
+
border: 1px solid var(--border);
|
|
1520
|
+
border-radius: var(--r-sm);
|
|
1521
|
+
background: var(--bg);
|
|
1522
|
+
}
|
|
1523
|
+
.repos-inline-config summary {
|
|
1524
|
+
cursor: pointer;
|
|
1525
|
+
padding: 8px var(--s-3);
|
|
1526
|
+
font-size: 12.5px;
|
|
1527
|
+
color: var(--ink-mid);
|
|
1528
|
+
font-weight: 500;
|
|
1529
|
+
user-select: none;
|
|
1530
|
+
}
|
|
1531
|
+
.repos-inline-config summary::marker { color: var(--accent); }
|
|
1532
|
+
.repos-inline-config summary:hover { color: var(--ink); }
|
|
1533
|
+
.repos-inline-config[open] summary { border-bottom: 1px solid var(--border); }
|
|
1534
|
+
.repos-inline-body {
|
|
1535
|
+
padding: var(--s-3);
|
|
1536
|
+
background: var(--bg-elev);
|
|
1537
|
+
border-radius: 0 0 var(--r-sm) var(--r-sm);
|
|
1538
|
+
}
|
|
1539
|
+
.repos-inline-actions {
|
|
1540
|
+
display: flex;
|
|
1541
|
+
gap: var(--s-3);
|
|
1542
|
+
align-items: center;
|
|
1543
|
+
margin-top: var(--s-3);
|
|
1544
|
+
}
|
|
1545
|
+
|
|
1546
|
+
/* Unsaved-changes banner in Configure tab — sticky-ish callout above the
|
|
1547
|
+
settings card. Mirrors the accent CTA banner pattern but louder. */
|
|
1548
|
+
.dirty-banner {
|
|
1549
|
+
display: flex;
|
|
1550
|
+
align-items: center;
|
|
1551
|
+
gap: var(--s-3);
|
|
1552
|
+
padding: var(--s-3) var(--s-5);
|
|
1553
|
+
background: linear-gradient(180deg, rgba(196,95,63,0.12), rgba(196,95,63,0.06));
|
|
1554
|
+
border: 1px solid var(--accent);
|
|
1555
|
+
border-radius: var(--r-sm);
|
|
1556
|
+
color: var(--accent-deep);
|
|
1557
|
+
font-size: 13px;
|
|
1558
|
+
font-weight: 500;
|
|
1559
|
+
position: sticky;
|
|
1560
|
+
top: var(--s-3);
|
|
1561
|
+
z-index: 10;
|
|
1562
|
+
box-shadow: 0 4px 16px -8px rgba(196, 95, 63, 0.4);
|
|
1563
|
+
animation: banner-in .25s cubic-bezier(.4, 0, .2, 1);
|
|
1564
|
+
}
|
|
1565
|
+
@keyframes banner-in {
|
|
1566
|
+
from { opacity: 0; transform: translateY(-6px); }
|
|
1567
|
+
to { opacity: 1; transform: translateY(0); }
|
|
1568
|
+
}
|
|
1569
|
+
.dirty-banner .dirty-dot {
|
|
1570
|
+
width: 8px;
|
|
1571
|
+
height: 8px;
|
|
1572
|
+
border-radius: 50%;
|
|
1573
|
+
background: var(--accent);
|
|
1574
|
+
flex: 0 0 8px;
|
|
1575
|
+
box-shadow: 0 0 0 0 rgba(196, 95, 63, 0.5);
|
|
1576
|
+
animation: dirty-pulse 2s ease-in-out infinite;
|
|
1577
|
+
}
|
|
1578
|
+
.dirty-banner .dirty-text { flex: 1; }
|
|
1579
|
+
|
|
1580
|
+
/* Save button "pulse" when there are unsaved changes */
|
|
1581
|
+
.action.primary.is-dirty {
|
|
1582
|
+
animation: save-pulse 1.6s ease-in-out infinite;
|
|
1583
|
+
}
|
|
1584
|
+
@keyframes save-pulse {
|
|
1585
|
+
0%, 100% { box-shadow: 0 0 0 0 rgba(196, 95, 63, 0.4); }
|
|
1586
|
+
50% { box-shadow: 0 0 0 6px rgba(196, 95, 63, 0); }
|
|
1587
|
+
}
|
|
1588
|
+
|
|
1589
|
+
/* Ad-hoc dialog variant (confirm / prompt) — narrower than the main modal */
|
|
1590
|
+
.modal-dialog { width: min(440px, 100%); }
|
|
1591
|
+
.modal-dialog .modal-head { padding: var(--s-4) var(--s-5) var(--s-3); border-bottom: 0; }
|
|
1592
|
+
.modal-dialog .modal-head h2 {
|
|
1593
|
+
font-size: 14.5px;
|
|
1594
|
+
font-weight: 600;
|
|
1595
|
+
color: var(--ink);
|
|
1596
|
+
line-height: 1.4;
|
|
1597
|
+
}
|
|
1598
|
+
.modal-dialog .modal-body { padding: 0 var(--s-5) var(--s-4); }
|
|
1599
|
+
.dialog-msg {
|
|
1600
|
+
font-size: 13.5px;
|
|
1601
|
+
color: var(--ink-mid);
|
|
1602
|
+
line-height: 1.55;
|
|
1603
|
+
}
|
|
1604
|
+
.modal-dialog .modal-foot { padding: var(--s-3) var(--s-5); }
|
|
1605
|
+
.modal-dialog input[type="text"] {
|
|
1606
|
+
width: 100%;
|
|
1607
|
+
max-width: none;
|
|
1608
|
+
margin-top: var(--s-2);
|
|
1609
|
+
}
|
|
1610
|
+
|
|
1176
1611
|
/* ─────────────────────────────────────────────────────────────
|
|
1177
1612
|
Small utilities
|
|
1178
1613
|
───────────────────────────────────────────────────────────── */
|
package/server.js
CHANGED
|
@@ -6,6 +6,7 @@ const express = require('express');
|
|
|
6
6
|
|
|
7
7
|
const { listSessions, listRecentSessions, findSessionMetadata } = require('./lib/sessions');
|
|
8
8
|
const { listFavorites, addFavorite, removeFavorite, loadFavorites } = require('./lib/favorites');
|
|
9
|
+
const { loadLabels, setLabel, removeLabel } = require('./lib/labels');
|
|
9
10
|
const { loadConfig, saveConfig, DATA_DIR } = require('./lib/config');
|
|
10
11
|
const {
|
|
11
12
|
saveSnapshot,
|
|
@@ -105,6 +106,29 @@ app.delete('/api/favorites/:sessionId', asyncH(async (req, res) => {
|
|
|
105
106
|
res.json({ removed });
|
|
106
107
|
}));
|
|
107
108
|
|
|
109
|
+
// ---- labels (rename overrides) ----
|
|
110
|
+
// Custom display titles keyed by sessionId. Empty body / empty label is
|
|
111
|
+
// treated as a delete.
|
|
112
|
+
app.get('/api/labels', asyncH(async (_req, res) => {
|
|
113
|
+
const labels = await loadLabels();
|
|
114
|
+
res.json({ labels });
|
|
115
|
+
}));
|
|
116
|
+
|
|
117
|
+
app.put('/api/labels/:sessionId', asyncH(async (req, res) => {
|
|
118
|
+
const label = req.body && req.body.label;
|
|
119
|
+
if (!label || !String(label).trim()) {
|
|
120
|
+
const removed = await removeLabel(req.params.sessionId);
|
|
121
|
+
return res.json({ removed });
|
|
122
|
+
}
|
|
123
|
+
const saved = await setLabel(req.params.sessionId, label);
|
|
124
|
+
res.json({ label: saved });
|
|
125
|
+
}));
|
|
126
|
+
|
|
127
|
+
app.delete('/api/labels/:sessionId', asyncH(async (req, res) => {
|
|
128
|
+
const removed = await removeLabel(req.params.sessionId);
|
|
129
|
+
res.json({ removed });
|
|
130
|
+
}));
|
|
131
|
+
|
|
108
132
|
// ---- config ----
|
|
109
133
|
|
|
110
134
|
app.get('/api/config', asyncH(async (_req, res) => {
|
|
@@ -340,11 +364,13 @@ app.post('/api/sessions/:sessionId/focus', asyncH(async (req, res) => {
|
|
|
340
364
|
const sessions = await listSessions();
|
|
341
365
|
const s = sessions.find((x) => x.sessionId === sessionId);
|
|
342
366
|
if (!s) return res.status(404).json({ error: `session ${sessionId} not live` });
|
|
367
|
+
const cfg = await loadConfig();
|
|
343
368
|
const result = await focusBySession({
|
|
344
369
|
pid: s.pid,
|
|
345
370
|
sessionId: s.sessionId,
|
|
346
371
|
title: s.title,
|
|
347
372
|
cwd: s.cwd,
|
|
373
|
+
moveToCenter: !!cfg.focusMovesToCenter,
|
|
348
374
|
});
|
|
349
375
|
res.json({ session: { pid: s.pid, sessionId: s.sessionId, cwd: s.cwd, title: s.title }, ...result });
|
|
350
376
|
}));
|
|
@@ -353,7 +379,8 @@ app.post('/api/sessions/:sessionId/focus', asyncH(async (req, res) => {
|
|
|
353
379
|
app.get('/api/terminals', (_req, res) => res.json({ terminals: listTerminalKinds() }));
|
|
354
380
|
|
|
355
381
|
// ---- health ----
|
|
356
|
-
|
|
382
|
+
const pkg = require('./package.json');
|
|
383
|
+
app.get('/api/health', (_req, res) => res.json({ ok: true, pid: process.pid, version: pkg.version, name: pkg.name }));
|
|
357
384
|
|
|
358
385
|
// ---- auto-snapshot scheduler ----
|
|
359
386
|
let snapshotTimer = null;
|