@bakapiano/ccsm 0.5.0 → 0.8.3

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.
Files changed (70) hide show
  1. package/README.md +172 -38
  2. package/bin/ccsm.js +194 -0
  3. package/lib/config.js +1 -0
  4. package/lib/favorites.js +23 -45
  5. package/lib/focus.js +90 -14
  6. package/lib/jsonStore.js +60 -0
  7. package/lib/labels.js +29 -0
  8. package/lib/webTerminal.js +173 -0
  9. package/lib/workspace.js +8 -4
  10. package/package.json +11 -3
  11. package/public/css/base.css +82 -0
  12. package/public/css/cards.css +149 -0
  13. package/public/css/feedback.css +219 -0
  14. package/public/css/forms.css +282 -0
  15. package/public/css/layout.css +107 -0
  16. package/public/css/modal.css +169 -0
  17. package/public/css/responsive.css +10 -0
  18. package/public/css/sidebar.css +165 -0
  19. package/public/css/tables.css +266 -0
  20. package/public/css/terminals.css +112 -0
  21. package/public/css/tokens.css +63 -0
  22. package/public/css/wco.css +70 -0
  23. package/public/css/widgets.css +204 -0
  24. package/public/favicon.svg +18 -0
  25. package/public/index.html +53 -379
  26. package/public/js/actions.js +87 -0
  27. package/public/js/api.js +103 -0
  28. package/public/js/backend.js +28 -0
  29. package/public/js/components/App.js +45 -0
  30. package/public/js/components/Card.js +24 -0
  31. package/public/js/components/DialogHost.js +45 -0
  32. package/public/js/components/Fab.js +11 -0
  33. package/public/js/components/FavoritesTable.js +81 -0
  34. package/public/js/components/Footer.js +12 -0
  35. package/public/js/components/NewSessionModal.js +142 -0
  36. package/public/js/components/OfflineBanner.js +52 -0
  37. package/public/js/components/PageHead.js +33 -0
  38. package/public/js/components/Pagination.js +27 -0
  39. package/public/js/components/ProgressList.js +32 -0
  40. package/public/js/components/RecentTable.js +68 -0
  41. package/public/js/components/RepoPicker.js +40 -0
  42. package/public/js/components/ReposEditor.js +74 -0
  43. package/public/js/components/ServerStatus.js +18 -0
  44. package/public/js/components/SessionsTable.js +71 -0
  45. package/public/js/components/Sidebar.js +52 -0
  46. package/public/js/components/SnapshotPanel.js +77 -0
  47. package/public/js/components/TerminalView.js +108 -0
  48. package/public/js/components/TitleCell.js +40 -0
  49. package/public/js/components/Toast.js +8 -0
  50. package/public/js/components/WorkspacePicker.js +19 -0
  51. package/public/js/components/WorkspacesGrid.js +41 -0
  52. package/public/js/dialog.js +59 -0
  53. package/public/js/html.js +6 -0
  54. package/public/js/icons.js +114 -0
  55. package/public/js/main.js +81 -0
  56. package/public/js/pages/AboutPage.js +85 -0
  57. package/public/js/pages/ConfigurePage.js +194 -0
  58. package/public/js/pages/LaunchPage.js +117 -0
  59. package/public/js/pages/SessionsPage.js +47 -0
  60. package/public/js/pages/TerminalsPage.js +74 -0
  61. package/public/js/state.js +87 -0
  62. package/public/js/streaming.js +96 -0
  63. package/public/js/toast.js +14 -0
  64. package/public/js/util.js +24 -0
  65. package/public/manifest.webmanifest +14 -0
  66. package/scripts/install.js +111 -0
  67. package/scripts/uninstall.js +56 -0
  68. package/server.js +314 -31
  69. package/public/app.js +0 -894
  70. package/public/styles.css +0 -1204
@@ -0,0 +1,107 @@
1
+ /* App grid · main column · page header · tab panels · footer status line */
2
+
3
+ .app {
4
+ display: grid;
5
+ grid-template-columns: var(--sidebar-w) 1fr;
6
+ min-height: 100vh;
7
+ transition: grid-template-columns .25s cubic-bezier(.4, 0, .2, 1);
8
+ }
9
+ .app:has(.sidebar[data-collapsed="true"]) {
10
+ grid-template-columns: var(--sidebar-w-collapsed) 1fr;
11
+ }
12
+
13
+ .main {
14
+ display: flex;
15
+ flex-direction: column;
16
+ min-width: 0;
17
+ padding: var(--s-8) var(--s-10) var(--s-6);
18
+ gap: var(--s-6);
19
+ }
20
+
21
+ .page-head {
22
+ display: flex;
23
+ align-items: flex-start;
24
+ justify-content: space-between;
25
+ gap: var(--s-8);
26
+ padding-bottom: var(--s-5);
27
+ border-bottom: 1px solid var(--border);
28
+ }
29
+ .page-head-inner { min-width: 0; }
30
+
31
+ .page-title {
32
+ font-size: 26px;
33
+ font-weight: 600;
34
+ letter-spacing: -0.024em;
35
+ color: var(--ink);
36
+ line-height: 1.1;
37
+ }
38
+ .page-subtitle {
39
+ margin-top: 4px;
40
+ font-size: 13.5px;
41
+ color: var(--ink-mid);
42
+ }
43
+
44
+ .page-head-meta {
45
+ display: flex;
46
+ align-items: baseline;
47
+ gap: var(--s-3);
48
+ flex-shrink: 0;
49
+ white-space: nowrap;
50
+ font-family: var(--mono);
51
+ font-size: 11px;
52
+ color: var(--ink-muted);
53
+ }
54
+ .ph-stat { display: inline-flex; gap: 6px; align-items: baseline; }
55
+ .ph-key {
56
+ font-family: var(--body);
57
+ font-size: 10.5px;
58
+ letter-spacing: 0.06em;
59
+ text-transform: uppercase;
60
+ color: var(--ink-faint);
61
+ }
62
+ .ph-val { color: var(--ink-mid); }
63
+ .ph-divider { color: var(--ink-faint); }
64
+
65
+ .content {
66
+ flex: 1;
67
+ display: flex;
68
+ flex-direction: column;
69
+ }
70
+
71
+ .tab-panel {
72
+ display: none;
73
+ flex-direction: column;
74
+ gap: var(--s-6);
75
+ }
76
+ .tab-panel[data-active] {
77
+ display: flex;
78
+ animation: panel-in .35s cubic-bezier(.4, 0, .2, 1);
79
+ }
80
+ @keyframes panel-in {
81
+ from { opacity: 0; transform: translateY(6px); }
82
+ to { opacity: 1; transform: translateY(0); }
83
+ }
84
+
85
+ .footer-status {
86
+ margin-top: auto;
87
+ padding-top: var(--s-4);
88
+ border-top: 1px solid var(--border-soft);
89
+ display: flex;
90
+ flex-wrap: wrap;
91
+ align-items: baseline;
92
+ gap: var(--s-2);
93
+ font-size: 11px;
94
+ color: var(--ink-muted);
95
+ }
96
+ .footer-status .fs-key {
97
+ text-transform: uppercase;
98
+ letter-spacing: 0.08em;
99
+ font-size: 10px;
100
+ color: var(--ink-faint);
101
+ }
102
+ .footer-status .fs-val {
103
+ font-family: var(--mono);
104
+ font-size: 11px;
105
+ color: var(--ink-mid);
106
+ }
107
+ .footer-status .fs-divider { color: var(--ink-faint); margin: 0 var(--s-1); }
@@ -0,0 +1,169 @@
1
+ /* Floating action button · modal backdrop / shell / sections ·
2
+ nested repos-inline-config disclosure · ad-hoc confirm/prompt dialog */
3
+
4
+ .fab {
5
+ position: fixed;
6
+ bottom: var(--s-6);
7
+ right: var(--s-6);
8
+ z-index: 90;
9
+ width: 48px;
10
+ height: 48px;
11
+ border-radius: 50%;
12
+ background: var(--ink);
13
+ color: var(--bg-elev);
14
+ border: 0;
15
+ cursor: pointer;
16
+ display: inline-flex;
17
+ align-items: center;
18
+ justify-content: center;
19
+ box-shadow:
20
+ 0 6px 16px -6px rgba(26, 24, 21, 0.25),
21
+ 0 1px 0 rgba(26, 24, 21, 0.06);
22
+ transition: background .12s ease, transform .15s ease, box-shadow .15s ease;
23
+ }
24
+ .fab:hover {
25
+ background: #000;
26
+ transform: translateY(-1px);
27
+ box-shadow:
28
+ 0 10px 22px -6px rgba(26, 24, 21, 0.26),
29
+ 0 2px 0 rgba(26, 24, 21, 0.08);
30
+ }
31
+ .fab:active { transform: scale(0.96); }
32
+
33
+ .modal-backdrop {
34
+ position: fixed;
35
+ inset: 0;
36
+ z-index: 200;
37
+ background: rgba(26, 24, 21, 0.48);
38
+ display: flex;
39
+ align-items: center;
40
+ justify-content: center;
41
+ padding: var(--s-6);
42
+ animation: backdrop-in .18s ease;
43
+ }
44
+ @keyframes backdrop-in { from { opacity: 0; } to { opacity: 1; } }
45
+
46
+ .modal {
47
+ background: var(--bg-elev);
48
+ border: 1px solid var(--border);
49
+ border-radius: var(--r-md);
50
+ width: min(560px, 100%);
51
+ max-height: 90vh;
52
+ display: flex;
53
+ flex-direction: column;
54
+ overflow: hidden;
55
+ box-shadow:
56
+ 0 24px 64px -16px rgba(26, 24, 21, 0.35),
57
+ 0 4px 12px -2px rgba(26, 24, 21, 0.15);
58
+ animation: modal-in .22s cubic-bezier(.4, 0, .2, 1);
59
+ }
60
+ @keyframes modal-in {
61
+ from { opacity: 0; transform: translateY(12px) scale(0.98); }
62
+ to { opacity: 1; transform: translateY(0) scale(1); }
63
+ }
64
+
65
+ .modal-head {
66
+ display: flex;
67
+ align-items: center;
68
+ justify-content: space-between;
69
+ padding: var(--s-4) var(--s-6);
70
+ border-bottom: 1px solid var(--border-soft);
71
+ }
72
+ .modal-head h2 {
73
+ font-size: 16px;
74
+ font-weight: 600;
75
+ color: var(--ink);
76
+ }
77
+ .modal-close {
78
+ appearance: none;
79
+ background: transparent;
80
+ border: 0;
81
+ padding: 4px;
82
+ margin: -4px;
83
+ cursor: pointer;
84
+ color: var(--ink-muted);
85
+ border-radius: 4px;
86
+ display: inline-flex;
87
+ transition: color .12s ease, background .12s ease;
88
+ }
89
+ .modal-close:hover { color: var(--ink); background: var(--bg); }
90
+
91
+ .modal-body {
92
+ padding: var(--s-5) var(--s-6);
93
+ overflow-y: auto;
94
+ flex: 1;
95
+ }
96
+ .modal-hint {
97
+ font-size: 13px;
98
+ color: var(--ink-muted);
99
+ margin-bottom: var(--s-4);
100
+ }
101
+ .modal-hint code {
102
+ font-family: var(--mono);
103
+ font-size: 11.5px;
104
+ background: var(--bg);
105
+ padding: 1px 5px;
106
+ border-radius: 4px;
107
+ border: 1px solid var(--border-soft);
108
+ }
109
+
110
+ .modal-foot {
111
+ display: flex;
112
+ justify-content: flex-end;
113
+ gap: var(--s-3);
114
+ padding: var(--s-3) var(--s-6);
115
+ border-top: 1px solid var(--border-soft);
116
+ background: var(--bg);
117
+ }
118
+
119
+ /* Nested "Edit repos" disclosure inside the new-session modal */
120
+ .repos-inline-config {
121
+ margin: var(--s-3) 0;
122
+ border: 1px solid var(--border);
123
+ border-radius: var(--r-sm);
124
+ background: var(--bg);
125
+ }
126
+ .repos-inline-config summary {
127
+ cursor: pointer;
128
+ padding: 8px var(--s-3);
129
+ font-size: 12.5px;
130
+ color: var(--ink-mid);
131
+ font-weight: 500;
132
+ user-select: none;
133
+ }
134
+ .repos-inline-config summary::marker { color: var(--ink-mid); }
135
+ .repos-inline-config summary:hover { color: var(--ink); }
136
+ .repos-inline-config[open] summary { border-bottom: 1px solid var(--border); }
137
+ .repos-inline-body {
138
+ padding: var(--s-3);
139
+ background: var(--bg-elev);
140
+ border-radius: 0 0 var(--r-sm) var(--r-sm);
141
+ }
142
+ .repos-inline-actions {
143
+ display: flex;
144
+ gap: var(--s-3);
145
+ align-items: center;
146
+ margin-top: var(--s-3);
147
+ }
148
+
149
+ /* Narrower variant for confirm/prompt — no border under head, tighter padding */
150
+ .modal-dialog { width: min(440px, 100%); }
151
+ .modal-dialog .modal-head { padding: var(--s-4) var(--s-5) var(--s-3); border-bottom: 0; }
152
+ .modal-dialog .modal-head h2 {
153
+ font-size: 14.5px;
154
+ font-weight: 600;
155
+ color: var(--ink);
156
+ line-height: 1.4;
157
+ }
158
+ .modal-dialog .modal-body { padding: 0 var(--s-5) var(--s-4); }
159
+ .modal-dialog .modal-foot { padding: var(--s-3) var(--s-5); }
160
+ .modal-dialog input[type="text"] {
161
+ width: 100%;
162
+ max-width: none;
163
+ margin-top: var(--s-2);
164
+ }
165
+ .dialog-msg {
166
+ font-size: 13.5px;
167
+ color: var(--ink-mid);
168
+ line-height: 1.55;
169
+ }
@@ -0,0 +1,10 @@
1
+ /* Narrow viewports: force-collapse sidebar, single-col config grid */
2
+
3
+ @media (max-width: 900px) {
4
+ .app { grid-template-columns: var(--sidebar-w-collapsed) 1fr !important; }
5
+ .sidebar { padding: var(--s-4) var(--s-2); }
6
+ .brand-name, .nav-label, .nav-badge { opacity: 0; pointer-events: none; }
7
+ .main { padding: var(--s-6) var(--s-5) var(--s-5); }
8
+ .page-head { flex-direction: column; gap: var(--s-3); }
9
+ .config-grid { grid-template-columns: 1fr; }
10
+ }
@@ -0,0 +1,165 @@
1
+ /* Left collapsible sidebar nav · brand mark · util items · collapse toggle */
2
+
3
+ .sidebar {
4
+ position: sticky;
5
+ top: 0;
6
+ height: 100vh;
7
+ background: var(--sidebar-bg);
8
+ border-right: 1px solid var(--border);
9
+ display: flex;
10
+ flex-direction: column;
11
+ padding: var(--s-4) var(--s-3);
12
+ overflow: hidden;
13
+ transition: padding .25s cubic-bezier(.4, 0, .2, 1);
14
+ }
15
+ .sidebar[data-collapsed="true"] {
16
+ padding: var(--s-4) var(--s-2);
17
+ }
18
+
19
+ .sidebar-brand {
20
+ display: flex;
21
+ align-items: center;
22
+ gap: var(--s-2);
23
+ /* Top padding matches .main's so the brand mark sits on the same
24
+ horizontal band as the page-title. */
25
+ padding: var(--s-4) var(--s-2);
26
+ min-height: 56px;
27
+ }
28
+ .brand-mark {
29
+ display: inline-flex;
30
+ align-items: center;
31
+ justify-content: center;
32
+ width: 32px;
33
+ height: 32px;
34
+ flex: 0 0 32px;
35
+ background: transparent;
36
+ }
37
+ .brand-mark svg { display: block; }
38
+ .brand-name {
39
+ font-size: 19px;
40
+ font-weight: 600;
41
+ letter-spacing: -0.02em;
42
+ color: var(--ink);
43
+ white-space: nowrap;
44
+ opacity: 1;
45
+ transition: opacity .15s ease;
46
+ }
47
+ .brand-dot { color: var(--accent); }
48
+ .sidebar[data-collapsed="true"] .brand-name { opacity: 0; pointer-events: none; }
49
+
50
+ .sidebar-nav {
51
+ display: flex;
52
+ flex-direction: column;
53
+ gap: 2px;
54
+ flex: 0 0 auto;
55
+ }
56
+
57
+ .nav-item, .util-item {
58
+ appearance: none;
59
+ background: transparent;
60
+ border: 0;
61
+ display: flex;
62
+ align-items: center;
63
+ gap: var(--s-3);
64
+ width: 100%;
65
+ padding: 0 12px;
66
+ min-height: 42px;
67
+ border-radius: var(--r-sm);
68
+ cursor: pointer;
69
+ color: var(--ink-mid);
70
+ font-family: var(--body);
71
+ font-size: 14.5px;
72
+ font-weight: 500;
73
+ text-align: left;
74
+ transition: background .12s ease, color .12s ease;
75
+ position: relative;
76
+ }
77
+ .nav-item:hover, .util-item:hover {
78
+ background: var(--sidebar-hover);
79
+ color: var(--ink);
80
+ }
81
+ .nav-item[aria-selected="true"] {
82
+ background: var(--sidebar-active);
83
+ color: var(--ink);
84
+ }
85
+
86
+ /* Unsaved-changes dot next to nav label */
87
+ .nav-item.has-changes::after {
88
+ content: "";
89
+ position: absolute;
90
+ right: 10px;
91
+ top: 50%;
92
+ transform: translateY(-50%);
93
+ width: 7px;
94
+ height: 7px;
95
+ border-radius: 50%;
96
+ background: var(--ink);
97
+ box-shadow: 0 0 0 0 rgba(26, 24, 21, 0.30);
98
+ animation: dirty-pulse 2s ease-in-out infinite;
99
+ }
100
+ @keyframes dirty-pulse {
101
+ 0%, 100% { box-shadow: 0 0 0 0 rgba(26, 24, 21, 0.22); }
102
+ 50% { box-shadow: 0 0 0 4px rgba(26, 24, 21, 0); }
103
+ }
104
+ .sidebar[data-collapsed="true"] .nav-item.has-changes::after {
105
+ right: auto;
106
+ top: 6px;
107
+ left: 28px;
108
+ }
109
+
110
+ .nav-icon {
111
+ display: inline-flex;
112
+ width: 20px;
113
+ height: 20px;
114
+ flex: 0 0 20px;
115
+ color: currentColor;
116
+ }
117
+ .nav-icon svg { width: 100%; height: 100%; }
118
+ .nav-label {
119
+ white-space: nowrap;
120
+ opacity: 1;
121
+ transition: opacity .15s ease;
122
+ flex: 1;
123
+ }
124
+ .sidebar[data-collapsed="true"] .nav-label { opacity: 0; pointer-events: none; }
125
+
126
+ .nav-badge {
127
+ font-family: var(--mono);
128
+ font-size: 10.5px;
129
+ background: var(--border-soft);
130
+ color: var(--ink-muted);
131
+ padding: 1px 6px;
132
+ border-radius: 4px;
133
+ font-variant-numeric: tabular-nums;
134
+ opacity: 1;
135
+ transition: opacity .15s ease;
136
+ }
137
+ .sidebar[data-collapsed="true"] .nav-badge { opacity: 0; }
138
+ .nav-item[aria-selected="true"] .nav-badge {
139
+ background: var(--bg-elev);
140
+ color: var(--ink-mid);
141
+ }
142
+
143
+ .sidebar-divider {
144
+ margin: var(--s-3) var(--s-2);
145
+ border-top: 1px solid var(--border);
146
+ }
147
+
148
+ .sidebar-utility {
149
+ display: flex;
150
+ flex-direction: column;
151
+ gap: 2px;
152
+ }
153
+
154
+ .sidebar-foot {
155
+ margin-top: auto;
156
+ padding-top: var(--s-2);
157
+ }
158
+
159
+ /* chevron flips when the sidebar is collapsed; the rest is util-item styling */
160
+ .collapse-toggle .nav-icon {
161
+ transition: transform .25s cubic-bezier(.4, 0, .2, 1);
162
+ }
163
+ .sidebar[data-collapsed="true"] .collapse-toggle .nav-icon {
164
+ transform: rotate(180deg);
165
+ }
@@ -0,0 +1,266 @@
1
+ /* Data tables · sticky actions column · status mark · title/path cells ·
2
+ star + rename inline buttons · row stagger animation */
3
+
4
+ .table-scroll {
5
+ overflow-x: auto;
6
+ /* tactile: scroll-padding so the actions column edges aren't flush
7
+ against the viewport when you scroll right to reach the buttons */
8
+ scroll-padding-right: var(--s-6);
9
+ }
10
+ .table-scroll::-webkit-scrollbar { height: 8px; }
11
+ .table-scroll .data {
12
+ min-width: 960px;
13
+ table-layout: auto;
14
+ }
15
+
16
+ .data {
17
+ width: 100%;
18
+ border-collapse: collapse;
19
+ font-family: var(--body);
20
+ font-size: 13.5px;
21
+ }
22
+
23
+ .data thead th {
24
+ padding: 10px var(--s-5) 10px;
25
+ text-align: left;
26
+ font-family: var(--body);
27
+ font-size: 11px;
28
+ font-weight: 600;
29
+ letter-spacing: 0.04em;
30
+ color: var(--ink-muted);
31
+ border-bottom: 1px solid var(--border);
32
+ background: var(--bg);
33
+ vertical-align: bottom;
34
+ white-space: nowrap;
35
+ }
36
+ .data thead th.num { text-align: right; }
37
+ /* shrink-to-fit pattern: width:1% with nowrap → the cell takes only
38
+ what it needs; title / path cells get the remainder */
39
+ .data thead th.col-mark { width: 28px; padding-left: 0; padding-right: 0; }
40
+ .data thead th.col-star { width: 28px; padding-left: 0; padding-right: 0; }
41
+ .data thead th.col-actions { width: 1%; white-space: nowrap; }
42
+ .data thead th.num { width: 1%; white-space: nowrap; }
43
+ .data tbody td.num { white-space: nowrap; }
44
+ .data tbody td:last-child { white-space: nowrap; }
45
+ .data thead th:first-child { padding-left: var(--s-6); }
46
+ .data thead th:last-child { padding-right: var(--s-6); }
47
+
48
+ /* Last column pins to the right edge of the .table-scroll container during
49
+ horizontal scroll. Backgrounds must be solid because sticky cells overlay
50
+ the content scrolling beneath them. */
51
+ .data thead th:last-child,
52
+ .data tbody td:last-child {
53
+ position: sticky;
54
+ right: 0;
55
+ z-index: 1;
56
+ box-shadow: -10px 0 10px -8px rgba(26, 24, 21, 0.08);
57
+ }
58
+ .data thead th:last-child { background: var(--bg); }
59
+ .data tbody td:last-child { background: var(--bg-elev); }
60
+ .data tbody tr:hover td:last-child { background: var(--bg); }
61
+
62
+ .data tbody tr {
63
+ border-bottom: 1px solid var(--border-soft);
64
+ transition: background .12s ease;
65
+ }
66
+ .data tbody tr:last-child { border-bottom: 0; }
67
+ .data tbody tr:hover { background: var(--bg); }
68
+
69
+ .data tbody td {
70
+ padding: 12px var(--s-5);
71
+ vertical-align: middle;
72
+ color: var(--ink);
73
+ }
74
+ .data tbody td:first-child { padding-left: var(--s-6); }
75
+ .data tbody td:last-child { padding-right: var(--s-6); }
76
+ .data tbody td.num {
77
+ text-align: right;
78
+ font-family: var(--mono);
79
+ font-size: 11.5px;
80
+ color: var(--ink-mid);
81
+ }
82
+
83
+ /* JS adds .no-anim after the first render so 5s auto-refresh doesn't
84
+ restage the stagger and strobe. */
85
+ .data tbody:not(.no-anim) tr { animation: row-in .3s ease-out backwards; }
86
+ .data tbody:not(.no-anim) tr:nth-child(1) { animation-delay: 0ms; }
87
+ .data tbody:not(.no-anim) tr:nth-child(2) { animation-delay: 20ms; }
88
+ .data tbody:not(.no-anim) tr:nth-child(3) { animation-delay: 40ms; }
89
+ .data tbody:not(.no-anim) tr:nth-child(4) { animation-delay: 60ms; }
90
+ .data tbody:not(.no-anim) tr:nth-child(5) { animation-delay: 80ms; }
91
+ .data tbody:not(.no-anim) tr:nth-child(6) { animation-delay: 100ms; }
92
+ .data tbody:not(.no-anim) tr:nth-child(7) { animation-delay: 120ms; }
93
+ .data tbody:not(.no-anim) tr:nth-child(8) { animation-delay: 140ms; }
94
+ .data tbody:not(.no-anim) tr:nth-child(n+9) { animation-delay: 160ms; }
95
+ @keyframes row-in {
96
+ from { opacity: 0; transform: translateY(3px); }
97
+ to { opacity: 1; transform: translateY(0); }
98
+ }
99
+
100
+ /* Status indicator dot — claude-like. Base color is the muted gray so any
101
+ unrecognized status string Claude writes ("compacting", "thinking", a
102
+ transient state, etc.) falls back to "unknown-looking" rather than a
103
+ stark black dot. .busy / .idle override. */
104
+ .status-mark {
105
+ display: inline-block;
106
+ width: 8px;
107
+ height: 8px;
108
+ border-radius: 50%;
109
+ background: currentColor;
110
+ vertical-align: middle;
111
+ position: relative;
112
+ color: var(--ink-faint);
113
+ }
114
+ .status-mark.busy {
115
+ color: var(--blue);
116
+ box-shadow: 0 0 0 0 currentColor;
117
+ animation: pulse 1.5s ease-in-out infinite;
118
+ }
119
+ .status-mark.idle { color: var(--green); }
120
+ .status-mark.unknown { color: var(--ink-faint); }
121
+ @keyframes pulse {
122
+ 0% { box-shadow: 0 0 0 0 rgba(74, 115, 165, 0.4); }
123
+ 70% { box-shadow: 0 0 0 7px rgba(74, 115, 165, 0); }
124
+ 100% { box-shadow: 0 0 0 0 rgba(74, 115, 165, 0); }
125
+ }
126
+
127
+ /* Title and path are fixed-width with overflow ellipsis on the inner
128
+ spans so the sticky actions column is always visible at typical widths;
129
+ long values truncate, the title attr shows the full value on hover. */
130
+ .title-cell {
131
+ width: 300px;
132
+ max-width: 300px;
133
+ min-width: 0;
134
+ }
135
+ .title-cell .title-row {
136
+ display: flex;
137
+ align-items: center;
138
+ gap: 6px;
139
+ min-width: 0;
140
+ width: 100%;
141
+ }
142
+ .title-cell .primary {
143
+ color: var(--ink);
144
+ font-weight: 500;
145
+ white-space: nowrap;
146
+ overflow: hidden;
147
+ text-overflow: ellipsis;
148
+ font-size: 13.5px;
149
+ min-width: 0;
150
+ flex: 0 1 auto;
151
+ }
152
+ .title-cell .secondary {
153
+ font-family: var(--mono);
154
+ font-size: 10.5px;
155
+ color: var(--ink-muted);
156
+ letter-spacing: 0.02em;
157
+ margin-top: 2px;
158
+ white-space: nowrap;
159
+ overflow: hidden;
160
+ text-overflow: ellipsis;
161
+ }
162
+
163
+ .path-cell {
164
+ font-family: var(--mono);
165
+ font-size: 11.5px;
166
+ color: var(--ink-mid);
167
+ width: 260px;
168
+ max-width: 260px;
169
+ min-width: 0;
170
+ white-space: nowrap;
171
+ overflow: hidden;
172
+ text-overflow: ellipsis;
173
+ }
174
+
175
+ .branch-tag {
176
+ display: inline-block;
177
+ font-family: var(--mono);
178
+ font-size: 11px;
179
+ color: var(--ink-mid);
180
+ background: var(--bg);
181
+ padding: 2px 7px;
182
+ border-radius: 4px;
183
+ border: 1px solid var(--border-soft);
184
+ }
185
+
186
+ .row-actions {
187
+ display: flex;
188
+ gap: var(--s-2);
189
+ justify-content: flex-end;
190
+ }
191
+ /* Equalised width so Focus / Continue / Resume align cleanly down the
192
+ actions column (last column is sticky, so width drift is visible).
193
+ .action is inline-flex with justify-content:center, so the icon+text
194
+ cluster sits in the middle. */
195
+ .row-actions .action.small {
196
+ min-width: 96px;
197
+ }
198
+
199
+ /* Star toggle — inline next to the title text. Outline by default;
200
+ filled in ink when pinned. */
201
+ .star-btn {
202
+ appearance: none;
203
+ background: transparent;
204
+ border: 0;
205
+ padding: 2px;
206
+ margin: 0;
207
+ cursor: pointer;
208
+ color: var(--ink-muted);
209
+ display: inline-flex;
210
+ align-items: center;
211
+ justify-content: center;
212
+ border-radius: 4px;
213
+ transition: color .12s ease, background .12s ease, transform .15s ease;
214
+ line-height: 0;
215
+ flex: 0 0 auto;
216
+ }
217
+ .data tbody tr:hover .star-btn { color: var(--ink-mid); }
218
+ .star-btn:hover {
219
+ color: #e3b341 !important;
220
+ background: var(--bg);
221
+ }
222
+ .star-btn:active { transform: scale(0.88); }
223
+ .star-btn.is-fav { color: #e3b341; }
224
+ .star-btn svg { display: block; }
225
+
226
+ /* Rename (pencil) — only visible on row hover so it doesn't compete with
227
+ the star. When a label is set (.has-label), it stays subtly visible to
228
+ indicate the title is overridden. */
229
+ .rename-btn {
230
+ appearance: none;
231
+ background: transparent;
232
+ border: 0;
233
+ padding: 2px;
234
+ margin: 0;
235
+ cursor: pointer;
236
+ color: var(--ink-faint);
237
+ opacity: 0;
238
+ display: inline-flex;
239
+ align-items: center;
240
+ justify-content: center;
241
+ border-radius: 4px;
242
+ transition: opacity .12s ease, color .12s ease, background .12s ease, transform .15s ease;
243
+ line-height: 0;
244
+ flex: 0 0 auto;
245
+ }
246
+ .data tbody tr:hover .rename-btn {
247
+ opacity: 1;
248
+ color: var(--ink-muted);
249
+ }
250
+ .rename-btn:hover {
251
+ color: var(--ink) !important;
252
+ background: var(--bg);
253
+ }
254
+ .rename-btn:active { transform: scale(0.88); }
255
+ .rename-btn.has-label {
256
+ opacity: 0.8;
257
+ color: var(--ink);
258
+ }
259
+ .rename-btn svg { display: block; }
260
+
261
+ #favoritesEmpty {
262
+ padding: var(--s-6) var(--s-6);
263
+ text-align: center;
264
+ font-size: 12.5px;
265
+ color: var(--ink-muted);
266
+ }