claude-code-kanban 4.29.0 → 4.30.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/public/index.html CHANGED
@@ -28,6 +28,7 @@
28
28
  <script defer src="https://cdn.jsdelivr.net/gh/highlightjs/cdn-release@11.12.0/build/highlight.min.js" integrity="sha384-wjfDDhOPPdjtva8vWBhWeVprSpmxisEu5aYT3q1JyACqXpdKpo3PWZTMVq24MBix" crossorigin="anonymous"></script>
29
29
  <script defer src="https://cdn.jsdelivr.net/npm/mermaid@12.0.0/dist/mermaid.min.js" integrity="sha384-xzghz1GQ5u9HCpVskeDPqMsdogD1yvuMQbEK53+wi+G70+6J1AG0L2cfi9PHjDWI" crossorigin="anonymous"></script>
30
30
  <script>window.__STORAGE_NS__ = null;</script>
31
+ <script defer src="/project-match.js"></script>
31
32
  <script defer src="/app.js"></script>
32
33
  </head>
33
34
  <body class="booting">
@@ -120,24 +121,37 @@
120
121
  </svg>
121
122
  </span>
122
123
  <div id="filter-menu" class="filter-menu" onclick="event.stopPropagation()">
123
- <div class="filter-row">
124
- <select id="project-filter" class="filter-dropdown" onchange="filterByProject(this.value)" aria-label="Filter by project">
125
- <option value="">All Projects</option>
126
- </select>
127
- </div>
128
- <div class="filter-row">
129
- <select id="session-filter" class="filter-dropdown" onchange="filterBySessions(this.value)" aria-label="Filter by session status">
130
- <option value="all">All Sessions</option>
131
- <option value="active">Active Only</option>
132
- </select>
124
+ <div class="pf-field">
125
+ <svg class="pf-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
126
+ <path d="M3 7a2 2 0 0 1 2-2h4l2 2h8a2 2 0 0 1 2 2v8a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z"/>
127
+ </svg>
128
+ <input
129
+ id="project-filter"
130
+ type="text"
131
+ class="pf-input"
132
+ placeholder="Any project"
133
+ autocomplete="off"
134
+ spellcheck="false"
135
+ role="combobox"
136
+ aria-expanded="true"
137
+ aria-controls="project-filter-options"
138
+ aria-autocomplete="list"
139
+ aria-label="Project. Type part of a path, or pick a project."
140
+ oninput="typeProjectFilter(this.value)"
141
+ />
142
+ <button
143
+ id="project-recent-btn"
144
+ type="button"
145
+ class="pf-recent"
146
+ onclick="toggleRecentProjects()"
147
+ aria-pressed="false"
148
+ title="Projects with activity in the last 24 hours"
149
+ >24h</button>
133
150
  </div>
134
- <div class="filter-row">
135
- <select id="session-limit" class="filter-dropdown" onchange="changeSessionLimit(this.value)" aria-label="Number of sessions to show">
136
- <option value="10">Show 10</option>
137
- <option value="20">Show 20</option>
138
- <option value="50">Show 50</option>
139
- <option value="all">Show All</option>
140
- </select>
151
+ <ul id="project-filter-options" class="pf-list" role="listbox" aria-label="Projects"></ul>
152
+ <div class="pf-seg" role="radiogroup" aria-label="Sessions">
153
+ <button type="button" role="radio" data-session-filter="active" onclick="filterBySessions('active')">Active</button>
154
+ <button type="button" role="radio" data-session-filter="all" onclick="filterBySessions('all')">All sessions</button>
141
155
  </div>
142
156
  </div>
143
157
  </div>
@@ -188,11 +202,20 @@
188
202
  </svg>
189
203
  <p id="boot-verb">Clawding…</p>
190
204
  </div>
191
- <div id="no-session" class="empty-state">
192
- <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5">
193
- <path d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2"/>
194
- </svg>
195
- <p>Select a session to view tasks</p>
205
+ <div id="no-session" class="empty-state idle-board">
206
+ <div class="idle-ghost" aria-hidden="true">
207
+ <div><span></span><span></span><span></span></div>
208
+ <div><span></span><span></span></div>
209
+ <div><span></span><span></span><span></span><span></span></div>
210
+ </div>
211
+ <div class="idle-glow" aria-hidden="true"><span></span><span></span></div>
212
+ <div class="idle-content">
213
+ <div class="idle-clawd" aria-hidden="true">
214
+ <svg class="clawd" viewBox="1 0 16 10" shape-rendering="crispEdges"><use href="#clawd-shape"/></svg>
215
+ </div>
216
+ <p class="idle-title">Select a session to view tasks</p>
217
+ <div class="idle-hint sp-hint"><kbd>↑</kbd><kbd>↓</kbd> move <span>·</span> <kbd>Enter</kbd> open <span>·</span> <kbd>?</kbd> shortcuts</div>
218
+ </div>
196
219
  </div>
197
220
 
198
221
  <div id="session-view" class="session-view">
@@ -213,7 +236,7 @@
213
236
  <polyline points="4 17 10 11 4 5"/><line x1="12" y1="19" x2="20" y2="19"/>
214
237
  </svg>
215
238
  </button>
216
- <button id="message-toggle" class="icon-btn" onclick="toggleMessagePanel()" title="Session log (L)" aria-label="Toggle session log">
239
+ <button id="message-toggle" class="icon-btn" onclick="toggleMessagePanel()" title="Session log (Shift+L)" aria-label="Toggle session log">
217
240
  <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
218
241
  <path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z"/>
219
242
  </svg>
@@ -0,0 +1,27 @@
1
+ // Loaded by the page as a plain script and by server.js through require, so the sidebar and
2
+ // the API (and the CLI, which sends `project=`) filter projects by one rule.
3
+ const projectMatch = (() => {
4
+ function normalizeProjectPath(p) {
5
+ return p.toLowerCase().replace(/\\/g, '/').replace(/\/+$/, '');
6
+ }
7
+ function isExactProjectPath(normalized) {
8
+ return /^([a-z]:)?\//.test(normalized);
9
+ }
10
+ function isExactProjectFilter(query) {
11
+ return isExactProjectPath(normalizeProjectPath(query.trim()));
12
+ }
13
+ // An absolute path selects that one project, so the hub's scope does not also pull in
14
+ // `app-2` next to `app`; any other text matches a part of the path.
15
+ function projectMatcher(query) {
16
+ const q = normalizeProjectPath(query.trim());
17
+ const exact = isExactProjectPath(q);
18
+ return (project) => {
19
+ if (!project) return false;
20
+ const p = normalizeProjectPath(project);
21
+ return exact ? p === q : p.includes(q);
22
+ };
23
+ }
24
+ return { normalizeProjectPath, isExactProjectPath, isExactProjectFilter, projectMatcher };
25
+ })();
26
+
27
+ if (typeof module === 'object' && module.exports) module.exports = projectMatch;
package/public/style.css CHANGED
@@ -456,14 +456,14 @@ body::before {
456
456
  gap: 8px;
457
457
  padding: 14px 16px 10px 20px;
458
458
  font-size: 11px;
459
- font-weight: 500;
460
- text-transform: uppercase;
461
- letter-spacing: 0.12em;
462
459
  color: var(--text-muted);
463
460
  }
464
461
 
465
462
  .section-header-label {
466
463
  flex: 0 0 auto;
464
+ font-weight: 500;
465
+ text-transform: uppercase;
466
+ letter-spacing: 0.12em;
467
467
  }
468
468
 
469
469
  /* Non-default filters, spelled out in the header so folding them into the popover
@@ -475,9 +475,7 @@ body::before {
475
475
  white-space: nowrap;
476
476
  text-overflow: ellipsis;
477
477
  font-size: 10px;
478
- font-weight: 400;
479
478
  letter-spacing: 0.02em;
480
- text-transform: none;
481
479
  color: var(--accent-text);
482
480
  }
483
481
 
@@ -545,15 +543,6 @@ body::before {
545
543
  display: block;
546
544
  }
547
545
 
548
- .filter-row {
549
- display: flex;
550
- gap: 5px;
551
- }
552
-
553
- .filter-row + .filter-row {
554
- margin-top: 5px;
555
- }
556
-
557
546
  .filter-dropdown {
558
547
  flex: 1;
559
548
  appearance: none;
@@ -577,28 +566,182 @@ body::before {
577
566
  border-color: var(--border);
578
567
  }
579
568
 
580
- /* A filter holding a non-default value tints toward the ember, so it reads as "active"
581
- before you read the value. Border and text only — the fill never floods. */
582
- .filter-dropdown.non-default {
583
- color: var(--accent-text);
584
- border-color: color-mix(in srgb, var(--accent) 40%, transparent);
569
+ .filter-dropdown option {
570
+ background: var(--bg-surface);
571
+ color: var(--text-primary);
585
572
  }
586
573
 
587
- .filter-dropdown.non-default:hover {
588
- border-color: color-mix(in srgb, var(--accent) 60%, transparent);
574
+ .filter-dropdown:focus,
575
+ .pf-field:focus-within {
576
+ outline: none;
577
+ border-color: var(--accent);
578
+ box-shadow: 0 0 0 2px var(--accent-dim);
589
579
  }
590
580
 
591
- .filter-dropdown option {
592
- background: var(--bg-surface);
581
+ /* The project box: path field with the 24h switch set into its right end. */
582
+ .pf-field {
583
+ display: flex;
584
+ align-items: center;
585
+ gap: 6px;
586
+ padding: 0 4px 0 9px;
587
+ background: var(--bg-deep);
588
+ border: 1px solid transparent;
589
+ border-radius: 6px;
590
+ }
591
+
592
+ .pf-icon {
593
+ flex: none;
594
+ width: 14px;
595
+ height: 14px;
596
+ color: var(--text-muted);
597
+ }
598
+
599
+ .pf-input {
600
+ flex: 1;
601
+ min-width: 0;
602
+ padding: 8px 0;
603
+ background: none;
604
+ border: none;
605
+ outline: none;
606
+ font-family: var(--mono);
607
+ font-size: 12px;
593
608
  color: var(--text-primary);
594
609
  }
595
610
 
596
- .filter-dropdown:focus {
611
+ .pf-input::placeholder {
612
+ color: var(--text-muted);
613
+ }
614
+
615
+ .pf-recent {
616
+ flex: none;
617
+ padding: 3px 7px;
618
+ background: none;
619
+ border: 1px solid var(--border);
620
+ border-radius: 4px;
621
+ font-family: var(--mono);
622
+ font-size: 10.5px;
623
+ color: var(--text-muted);
624
+ cursor: pointer;
625
+ }
626
+
627
+ .pf-recent:hover {
628
+ color: var(--text-secondary);
629
+ }
630
+
631
+ .pf-recent[aria-pressed='true'] {
632
+ background: var(--accent-dim);
633
+ border-color: transparent;
634
+ color: var(--accent-text);
635
+ }
636
+
637
+ .pf-recent:focus-visible,
638
+ .pf-seg button:focus-visible {
597
639
  outline: none;
598
- border-color: var(--accent);
599
640
  box-shadow: 0 0 0 2px var(--accent-dim);
600
641
  }
601
642
 
643
+ /* Name first, folder after it, age last. The folder clips from its left end, so the part
644
+ next to the name stays readable. */
645
+ .pf-list {
646
+ list-style: none;
647
+ margin: 4px 0 0;
648
+ padding: 0;
649
+ }
650
+
651
+ .pf-opt {
652
+ position: relative;
653
+ display: grid;
654
+ grid-template-columns: auto minmax(0, 1fr) auto;
655
+ align-items: baseline;
656
+ gap: 8px;
657
+ padding: 5px 8px;
658
+ border-radius: 5px;
659
+ font-family: var(--mono);
660
+ cursor: pointer;
661
+ }
662
+
663
+ .pf-opt:hover,
664
+ .pf-opt.hl {
665
+ background: var(--bg-hover);
666
+ }
667
+
668
+ .pf-name {
669
+ font-size: 12px;
670
+ color: var(--text-primary);
671
+ white-space: nowrap;
672
+ }
673
+
674
+ .pf-dir,
675
+ .pf-age {
676
+ font-size: 10.5px;
677
+ color: var(--text-muted);
678
+ white-space: nowrap;
679
+ }
680
+
681
+ .pf-dir {
682
+ overflow: hidden;
683
+ text-overflow: ellipsis;
684
+ direction: rtl;
685
+ text-align: left;
686
+ }
687
+
688
+ .pf-opt mark {
689
+ background: none;
690
+ color: var(--accent-text);
691
+ text-decoration: underline;
692
+ text-decoration-thickness: 1px;
693
+ text-underline-offset: 3px;
694
+ }
695
+
696
+ .pf-opt[aria-selected='true']::before {
697
+ content: '';
698
+ position: absolute;
699
+ top: 5px;
700
+ bottom: 5px;
701
+ left: 0;
702
+ width: 2px;
703
+ background: var(--accent);
704
+ }
705
+
706
+ .pf-opt[aria-selected='true'] .pf-name {
707
+ color: var(--accent-text);
708
+ }
709
+
710
+ .pf-empty {
711
+ padding: 8px;
712
+ font-size: 11.5px;
713
+ color: var(--text-muted);
714
+ }
715
+
716
+ .pf-seg {
717
+ display: grid;
718
+ grid-template-columns: 1fr 1fr;
719
+ gap: 4px;
720
+ margin-top: 6px;
721
+ padding-top: 6px;
722
+ border-top: 1px solid var(--border);
723
+ }
724
+
725
+ .pf-seg button {
726
+ padding: 6px;
727
+ background: none;
728
+ border: none;
729
+ border-radius: 5px;
730
+ font-family: var(--mono);
731
+ font-size: 11.5px;
732
+ color: var(--text-muted);
733
+ cursor: pointer;
734
+ }
735
+
736
+ .pf-seg button:hover {
737
+ color: var(--text-secondary);
738
+ }
739
+
740
+ .pf-seg button[aria-checked='true'] {
741
+ background: var(--bg-deep);
742
+ color: var(--text-primary);
743
+ }
744
+
602
745
  /* #endregion */
603
746
 
604
747
  /* #region COLLAPSIBLE */
@@ -904,34 +1047,6 @@ body.booting .boot-splash {
904
1047
  text-overflow: ellipsis;
905
1048
  }
906
1049
 
907
- /* inline-flex so only the text takes the click — a full-width row would swallow clicks
908
- meant for the card itself */
909
- .session-goal {
910
- display: inline-flex;
911
- align-items: baseline;
912
- max-width: 100%;
913
- font-size: 10px;
914
- margin-top: var(--card-gap-tight);
915
- white-space: nowrap;
916
- overflow: hidden;
917
- text-overflow: ellipsis;
918
- cursor: pointer;
919
- color: var(--warning);
920
- }
921
- .session-goal .session-goal-icon {
922
- margin-right: 3px;
923
- font-weight: 700;
924
- flex-shrink: 0;
925
- }
926
- .session-goal .session-goal-text {
927
- overflow: hidden;
928
- text-overflow: ellipsis;
929
- }
930
- .session-goal:hover .session-goal-text {
931
- text-decoration: underline;
932
- text-underline-offset: 2px;
933
- }
934
-
935
1050
  .session-item.plan-reveal {
936
1051
  outline: 1.5px solid var(--plan);
937
1052
  background: var(--plan-dim);
@@ -1231,14 +1346,140 @@ body.booting .boot-splash {
1231
1346
  color: var(--text-muted);
1232
1347
  }
1233
1348
 
1234
- .empty-state svg {
1235
- width: 48px;
1236
- height: 48px;
1349
+ .idle-board {
1350
+ position: relative;
1351
+ overflow: hidden;
1352
+ isolation: isolate;
1353
+ }
1354
+
1355
+ .idle-ghost {
1356
+ position: absolute;
1357
+ inset: 0;
1358
+ z-index: -1;
1359
+ display: grid;
1360
+ grid-template-columns: repeat(3, 1fr);
1361
+ gap: 20px;
1362
+ padding: 28px;
1363
+ opacity: 0.55;
1364
+ mask-image: radial-gradient(ellipse 70% 65% at 50% 50%, transparent 25%, #000 75%);
1365
+ }
1366
+
1367
+ .idle-ghost > div {
1368
+ display: flex;
1369
+ flex-direction: column;
1370
+ gap: 10px;
1371
+ padding: 44px 12px 12px;
1372
+ border: 1px dashed var(--border);
1373
+ border-radius: 12px;
1374
+ }
1375
+
1376
+ .idle-ghost span {
1377
+ height: 64px;
1378
+ border: 1px solid var(--border);
1379
+ border-radius: 9px;
1380
+ background: var(--bg-surface);
1381
+ }
1382
+
1383
+ .idle-ghost span:nth-child(2n) {
1384
+ height: 92px;
1385
+ }
1386
+
1387
+ .idle-glow {
1388
+ position: absolute;
1389
+ inset: 0;
1390
+ z-index: -1;
1391
+ pointer-events: none;
1392
+ }
1393
+
1394
+ .idle-glow span {
1395
+ position: absolute;
1396
+ width: 460px;
1397
+ height: 460px;
1398
+ border-radius: 50%;
1399
+ background: radial-gradient(circle, var(--accent-dim), transparent 65%);
1400
+ animation: idle-drift 22s ease-in-out infinite alternate;
1401
+ }
1402
+
1403
+ .idle-glow span:first-child {
1404
+ top: 12%;
1405
+ left: 18%;
1406
+ }
1407
+
1408
+ .idle-glow span + span {
1409
+ right: 14%;
1410
+ bottom: 8%;
1411
+ width: 360px;
1412
+ height: 360px;
1413
+ animation-duration: 28s;
1414
+ animation-direction: alternate-reverse;
1415
+ }
1416
+
1417
+ @keyframes idle-drift {
1418
+ to {
1419
+ transform: translate(90px, 50px) scale(1.15);
1420
+ }
1421
+ }
1422
+
1423
+ .idle-content {
1424
+ display: flex;
1425
+ flex-direction: column;
1426
+ align-items: center;
1427
+ gap: 10px;
1428
+ }
1429
+
1430
+ .idle-clawd {
1431
+ position: relative;
1432
+ padding-bottom: 10px;
1433
+ margin-bottom: 6px;
1434
+ }
1435
+
1436
+ .idle-clawd .clawd {
1437
+ display: block;
1438
+ animation: idle-float 3.2s ease-in-out infinite;
1439
+ }
1440
+
1441
+ .idle-clawd::after {
1442
+ content: '';
1443
+ position: absolute;
1444
+ left: 50%;
1445
+ bottom: 0;
1446
+ width: 44px;
1447
+ height: 6px;
1448
+ margin-left: -22px;
1449
+ border-radius: 50%;
1450
+ background: var(--text-muted);
1451
+ opacity: 0.25;
1452
+ animation: idle-shadow 3.2s ease-in-out infinite;
1453
+ }
1454
+
1455
+ @keyframes idle-float {
1456
+ 50% {
1457
+ transform: translateY(-6px);
1458
+ }
1459
+ }
1460
+
1461
+ @keyframes idle-shadow {
1462
+ 50% {
1463
+ transform: scaleX(0.75);
1464
+ opacity: 0.15;
1465
+ }
1466
+ }
1467
+
1468
+ .idle-title {
1469
+ font-size: 14px;
1470
+ color: var(--text-secondary);
1471
+ }
1472
+
1473
+ .idle-hint span {
1237
1474
  opacity: 0.5;
1238
1475
  }
1239
1476
 
1240
- .empty-state p {
1241
- font-size: 13px;
1477
+ @media (prefers-reduced-motion: reduce) {
1478
+ .idle-glow span,
1479
+ .idle-clawd .clawd,
1480
+ .idle-clawd::after {
1481
+ animation: none;
1482
+ }
1242
1483
  }
1243
1484
 
1244
1485
  /* #endregion */
@@ -1681,12 +1922,6 @@ body.booting .boot-splash {
1681
1922
  color: var(--text-tertiary);
1682
1923
  }
1683
1924
 
1684
- .task-session {
1685
- font-size: 12px;
1686
- color: var(--accent);
1687
- margin-top: 4px;
1688
- }
1689
-
1690
1925
  .task-active {
1691
1926
  display: flex;
1692
1927
  align-items: center;
@@ -2613,35 +2848,6 @@ body.booting .boot-splash {
2613
2848
  border-top: 1px solid var(--border);
2614
2849
  }
2615
2850
 
2616
- .info-goal-card {
2617
- margin: 12px 0;
2618
- padding: 10px 14px;
2619
- background: var(--bg-elevated);
2620
- border: 1px solid var(--border);
2621
- border-radius: 8px;
2622
- }
2623
- .info-goal-head {
2624
- display: flex;
2625
- align-items: center;
2626
- gap: 6px;
2627
- font-size: 11px;
2628
- font-weight: 600;
2629
- text-transform: uppercase;
2630
- letter-spacing: 0.5px;
2631
- color: var(--text-muted);
2632
- }
2633
- .info-goal-icon {
2634
- color: var(--warning);
2635
- }
2636
- .info-goal-text {
2637
- margin-top: 6px;
2638
- font-size: 13px;
2639
- line-height: 1.45;
2640
- color: var(--text-primary);
2641
- white-space: pre-wrap;
2642
- word-break: break-word;
2643
- }
2644
-
2645
2851
  .info-grid {
2646
2852
  display: grid;
2647
2853
  grid-template-columns: auto 1fr auto;
@@ -5749,6 +5955,10 @@ select.form-input option:checked {
5749
5955
  color: var(--text-primary);
5750
5956
  }
5751
5957
 
5958
+ .sp-row:not(.outside) + .sp-row.outside {
5959
+ margin-top: 8px;
5960
+ }
5961
+
5752
5962
  .sp-row.current .sp-name {
5753
5963
  color: var(--accent-text);
5754
5964
  }
@@ -7024,8 +7234,10 @@ pre.mermaid svg {
7024
7234
  cursor: pointer;
7025
7235
  }
7026
7236
 
7027
- .sg-menu-item:hover {
7237
+ .sg-menu-item:hover,
7238
+ .sg-menu-item:focus-visible {
7028
7239
  background: var(--bg-hover);
7240
+ outline: none;
7029
7241
  }
7030
7242
 
7031
7243
  .sg-menu-remove {
package/public/sw.js CHANGED
@@ -1,5 +1,5 @@
1
1
  const CACHE_NAME = 'cc-kanban-v2';
2
- const PRECACHE_URLS = ['/', '/style.css', '/app.js'];
2
+ const PRECACHE_URLS = ['/', '/style.css', '/project-match.js', '/app.js'];
3
3
 
4
4
  let cachePromise = null;
5
5
  function getCache() {