@beastmode-develeap/beastmode 0.1.212 → 0.1.214

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.
@@ -15,7 +15,7 @@
15
15
  }
16
16
  </script>
17
17
  <!--BOARD_DATA-->
18
- <script>window.__BUILD_STAMP__ = "20260509-145929-6395b56";</script>
18
+ <script>window.__BUILD_STAMP__ = "20260509-230834-09eb714";</script>
19
19
  <link rel="preconnect" href="https://fonts.googleapis.com">
20
20
  <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
21
21
  <link href="https://fonts.googleapis.com/css2?family=IBM+Plex+Sans:wght@300;400;500;600;700&family=JetBrains+Mono:wght@400;500;600;700&display=swap" rel="stylesheet">
@@ -1688,6 +1688,29 @@ input[type="range"]::-webkit-slider-thumb {
1688
1688
  color: #f97316;
1689
1689
  border: 1px solid rgba(249, 115, 22, 0.4);
1690
1690
  }
1691
+ .badge-superseded {
1692
+ background: rgba(100, 116, 139, 0.2);
1693
+ color: #94a3b8;
1694
+ border: 1px solid rgba(100, 116, 139, 0.4);
1695
+ text-decoration: line-through;
1696
+ }
1697
+ .kanban-card[data-overlay="superseded"] {
1698
+ opacity: 0.6;
1699
+ }
1700
+ .kanban-card[data-overlay="superseded"] .card-title {
1701
+ text-decoration: line-through;
1702
+ color: var(--text-muted);
1703
+ }
1704
+ .badge-successor {
1705
+ background: rgba(100, 116, 139, 0.15);
1706
+ color: #94a3b8;
1707
+ cursor: pointer;
1708
+ text-decoration: none;
1709
+ }
1710
+ .badge-successor:hover {
1711
+ background: rgba(100, 116, 139, 0.3);
1712
+ color: #cbd5e1;
1713
+ }
1691
1714
 
1692
1715
  /* Card-level visual treatment for overlay statuses */
1693
1716
  .kanban-card[data-overlay="stuck"] {
@@ -2161,6 +2184,95 @@ input[type="range"]::-webkit-slider-thumb {
2161
2184
  .filter-toggle:hover { border-color: var(--text-muted); color: var(--text); }
2162
2185
  .filter-toggle.active { border-color: var(--accent); color: var(--accent); background: var(--accent-subtle); }
2163
2186
 
2187
+ /* Sort toggle button — mirrors filter-toggle */
2188
+ .sort-toggle {
2189
+ display: inline-flex;
2190
+ align-items: center;
2191
+ gap: 6px;
2192
+ padding: 0 14px;
2193
+ height: 36px;
2194
+ background: var(--bg-card);
2195
+ border: 1px solid var(--border);
2196
+ border-radius: var(--radius-sm);
2197
+ color: var(--text-secondary);
2198
+ font-size: 13px;
2199
+ font-family: var(--font-sans);
2200
+ cursor: pointer;
2201
+ transition: all 0.15s;
2202
+ white-space: nowrap;
2203
+ position: relative;
2204
+ }
2205
+ .sort-toggle:hover { border-color: var(--text-muted); color: var(--text); }
2206
+ .sort-toggle.active { border-color: var(--accent); color: var(--accent); background: var(--accent-subtle); }
2207
+
2208
+ .sort-active-label {
2209
+ font-size: 11px;
2210
+ font-weight: 600;
2211
+ color: var(--accent);
2212
+ margin-left: 2px;
2213
+ }
2214
+
2215
+ /* Sort dropdown */
2216
+ .sort-dropdown {
2217
+ position: absolute;
2218
+ top: 100%;
2219
+ right: 0;
2220
+ margin-top: 4px;
2221
+ background: var(--bg-card);
2222
+ border: 1px solid var(--border);
2223
+ border-radius: var(--radius-sm);
2224
+ box-shadow: 0 4px 12px rgba(0,0,0,0.15);
2225
+ z-index: 100;
2226
+ min-width: 180px;
2227
+ padding: 4px;
2228
+ }
2229
+
2230
+ .sort-options {
2231
+ display: flex;
2232
+ flex-direction: column;
2233
+ }
2234
+
2235
+ .sort-option {
2236
+ display: flex;
2237
+ align-items: center;
2238
+ justify-content: space-between;
2239
+ padding: 8px 12px;
2240
+ background: none;
2241
+ border: none;
2242
+ color: var(--text-secondary);
2243
+ font-size: 13px;
2244
+ font-family: var(--font-sans);
2245
+ cursor: pointer;
2246
+ border-radius: var(--radius-sm);
2247
+ transition: background 0.1s;
2248
+ width: 100%;
2249
+ text-align: left;
2250
+ }
2251
+ .sort-option:hover { background: var(--bg-hover); color: var(--text); }
2252
+ .sort-option.active { color: var(--accent); font-weight: 600; }
2253
+
2254
+ .sort-dir-icon {
2255
+ font-size: 14px;
2256
+ font-weight: 700;
2257
+ color: var(--accent);
2258
+ }
2259
+
2260
+ .sort-clear {
2261
+ display: block;
2262
+ width: 100%;
2263
+ padding: 8px 12px;
2264
+ background: none;
2265
+ border: none;
2266
+ border-top: 1px solid var(--border);
2267
+ color: var(--text-muted);
2268
+ font-size: 12px;
2269
+ font-family: var(--font-sans);
2270
+ cursor: pointer;
2271
+ text-align: center;
2272
+ margin-top: 4px;
2273
+ }
2274
+ .sort-clear:hover { color: var(--danger); }
2275
+
2164
2276
  .view-toggle {
2165
2277
  display: inline-flex;
2166
2278
  border: 1px solid var(--border);
@@ -3829,6 +3941,7 @@ function Icon({ name, size = 18, className = '' }) {
3829
3941
  lightbulb: html`<path d="M8 1a5 5 0 00-3 9c.5.6.8 1.2 1 1.8V13h4v-1.2c.2-.6.5-1.2 1-1.8A5 5 0 008 1z"/><line x1="6" y1="13.5" x2="10" y2="13.5"/><line x1="6.5" y1="15" x2="9.5" y2="15"/>`,
3830
3942
  help: html`<circle cx="8" cy="8" r="6.5"/><path d="M6 6.5a2 2 0 013.7 1c0 1.5-1.7 1.5-1.7 3"/><circle cx="8" cy="12" r="0.5" fill="currentColor" stroke="none"/>`,
3831
3943
  filter: html`<polygon points="1.5,2 14.5,2 9.5,8.5 9.5,13 6.5,14.5 6.5,8.5"/>`,
3944
+ 'arrow-up-down': html`<path d="M4 6l4-4 4 4"/><path d="M4 10l4 4 4-4"/>`,
3832
3945
  costs: html`<circle cx="8" cy="8" r="6.5"/><path d="M8 3.5v1m0 7v1M6 10.5c0 .8.9 1.5 2 1.5s2-.7 2-1.5S9.1 9 8 9s-2-.7-2-1.5S6.9 6 8 6s2 .7 2 1.5"/>`,
3833
3946
  };
3834
3947
 
@@ -4382,7 +4495,9 @@ function isOverlayStatus(status) {
4382
4495
  const overlays = (typeof PIPELINE_CONFIG !== 'undefined' && PIPELINE_CONFIG && PIPELINE_CONFIG.overlay_statuses)
4383
4496
  ? PIPELINE_CONFIG.overlay_statuses
4384
4497
  : ['Stuck', 'Awaiting Input'];
4385
- return overlays.includes(status);
4498
+ if (overlays.includes(status)) return true;
4499
+ if (status === 'Superseded') return true;
4500
+ return false;
4386
4501
  }
4387
4502
 
4388
4503
  function overlayBadgeClass(status) {
@@ -4390,6 +4505,7 @@ function overlayBadgeClass(status) {
4390
4505
  const s = status.toLowerCase();
4391
4506
  if (s === 'stuck') return 'badge-overlay-stuck';
4392
4507
  if (s === 'awaiting input') return 'badge-overlay-awaiting';
4508
+ if (s === 'superseded') return 'badge-superseded';
4393
4509
  return '';
4394
4510
  }
4395
4511
 
@@ -4398,6 +4514,7 @@ function overlayBadgeLabel(status) {
4398
4514
  const s = status.toLowerCase();
4399
4515
  if (s === 'stuck') return '\u26A0 Stuck';
4400
4516
  if (s === 'awaiting input') return '\u23F3 Awaiting Input';
4517
+ if (s === 'superseded') return 'Superseded';
4401
4518
  return status;
4402
4519
  }
4403
4520
 
@@ -4406,6 +4523,7 @@ function overlayKey(status) {
4406
4523
  const s = status.toLowerCase();
4407
4524
  if (s === 'stuck') return 'stuck';
4408
4525
  if (s === 'awaiting input') return 'awaiting-input';
4526
+ if (s === 'superseded') return 'superseded';
4409
4527
  return null;
4410
4528
  }
4411
4529
 
@@ -4414,6 +4532,7 @@ function overlayTooltip(status) {
4414
4532
  const s = status.toLowerCase();
4415
4533
  if (s === 'stuck') return 'Task is stuck after max retries. Comment \'reset\' to restart or investigate the failure.';
4416
4534
  if (s === 'awaiting input') return 'BeastMode needs your answer to a question before continuing. Check the task updates.';
4535
+ if (s === 'superseded') return 'This item has been superseded by another item. It is terminal and cannot be reactivated.';
4417
4536
  return '';
4418
4537
  }
4419
4538
 
@@ -4566,6 +4685,32 @@ function partitionByEpic(columnItems, allItems) {
4566
4685
  groups[epicId].epicName = groups[epicId].epic.name;
4567
4686
  groups[epicId].epicStatus = groups[epicId].epic.status;
4568
4687
  }
4688
+ const allChildren = allItems.filter(i => String(i.parent_epic) === String(epicId));
4689
+ let doneCount = 0;
4690
+ let supersededCount = 0;
4691
+ let activeCount = 0;
4692
+ let mostCommonSuccessor = null;
4693
+ const successorCounts = {};
4694
+ for (const child of allChildren) {
4695
+ if (child.status === 'Done') {
4696
+ doneCount++;
4697
+ } else if (child.status === 'Superseded') {
4698
+ supersededCount++;
4699
+ const sby = child.extra && child.extra.superseded_by;
4700
+ if (sby) {
4701
+ successorCounts[sby] = (successorCounts[sby] || 0) + 1;
4702
+ }
4703
+ } else {
4704
+ activeCount++;
4705
+ }
4706
+ }
4707
+ if (supersededCount > 0) {
4708
+ let maxCount = 0;
4709
+ for (const [id, count] of Object.entries(successorCounts)) {
4710
+ if (count > maxCount) { maxCount = count; mostCommonSuccessor = id; }
4711
+ }
4712
+ }
4713
+ groups[epicId].breakdown = { doneCount, supersededCount, activeCount, mostCommonSuccessor };
4569
4714
  }
4570
4715
 
4571
4716
  const sortedGroupKeys = Object.keys(groups).sort((a, b) => {
@@ -5419,7 +5564,23 @@ function ItemDetailSidebar({ item, onClose, onStatusChange, selectedProject, all
5419
5564
  <aside class="detail-sidebar open" ref=${sidebarRef}>
5420
5565
  <div class="detail-resize-handle" onMouseDown=${onResizeStart}></div>
5421
5566
  <div class="detail-header">
5422
- <h3>${item.name || item.title}</h3>
5567
+ <h3 style=${item.status === 'Superseded' ? 'text-decoration:line-through;color:var(--text-muted);' : ''}>${item.name || item.title}</h3>
5568
+ ${item.status === 'Superseded' && html`
5569
+ <div class="detail-superseded-info" data-testid="sidebar-superseded-info"
5570
+ style="display:flex;align-items:center;gap:8px;padding:4px 0 8px;font-size:13px;color:var(--text-muted);">
5571
+ <span class="badge-superseded" style="font-size:11px;padding:2px 8px;border-radius:4px;">Superseded</span>
5572
+ ${item.extra && item.extra.superseded_by && html`
5573
+ <span>\u2192</span>
5574
+ <a href="#" data-testid="sidebar-successor-link"
5575
+ style="color:var(--accent);text-decoration:none;font-family:var(--font-mono);font-size:12px;"
5576
+ onClick=${(e) => {
5577
+ e.preventDefault();
5578
+ const target = allItems && allItems.find ? allItems.find(i => String(i.id) === String(item.extra.superseded_by)) : null;
5579
+ if (target && onSelectItem) onSelectItem(target);
5580
+ }}>#${item.extra.superseded_by}</a>
5581
+ `}
5582
+ </div>
5583
+ `}
5423
5584
  <button class="detail-close" onClick=${onClose}>\u00d7</button>
5424
5585
  </div>
5425
5586
  <div class="detail-meta">
@@ -5824,8 +5985,7 @@ function PipelineView({
5824
5985
  selectedProject,
5825
5986
  setSelectedItem,
5826
5987
  deleteItem,
5827
- columnSorts,
5828
- cycleSort,
5988
+ globalSort,
5829
5989
  sortColumnItems,
5830
5990
  costsByItem,
5831
5991
  envVerifyByItem,
@@ -5849,6 +6009,15 @@ function PipelineView({
5849
6009
  <div class="card-title" style="cursor:pointer;" onClick=${() => setSelectedItem(item)} title=${item.name || item.title}>${item.name || item.title}</div>
5850
6010
  <div class="card-footer">
5851
6011
  ${isOverlayStatus(item.status) && html`<span class=${'card-badge badge-overlay ' + overlayBadgeClass(item.status)} title=${overlayTooltip(item.status)}>${overlayBadgeLabel(item.status)}</span>`}
6012
+ ${item.extra && item.extra.superseded_by && html`<a class="card-badge badge-successor"
6013
+ href="#" data-testid="card-successor-link"
6014
+ title=${'Superseded by #' + item.extra.superseded_by}
6015
+ onClick=${(e) => {
6016
+ e.preventDefault();
6017
+ e.stopPropagation();
6018
+ const target = filteredItems.find(i => String(i.id) === String(item.extra.superseded_by));
6019
+ if (target) setSelectedItem(target);
6020
+ }}>→ #${item.extra.superseded_by}</a>`}
5852
6021
  ${!isParentEpic && item.parent_epic && html`<span class="card-badge badge-epic">epic:${item.parent_epic}</span>`}
5853
6022
  ${item.priority && html`<span class=${'card-badge ' + priorityBadgeClass(item.priority)}>${item.priority}</span>`}
5854
6023
  ${item.task_type && html`<span class="card-badge badge-type">${item.task_type}</span>`}
@@ -5915,8 +6084,7 @@ function PipelineView({
5915
6084
  ${columns.map(colId => {
5916
6085
  const meta = getColumnMeta(colId);
5917
6086
  const rawColItems = laneItemList.filter(item => getItemColumn(item, columns) === colId);
5918
- const colItems = sortColumnItems(rawColItems, columnSorts[colId] || '');
5919
- const sortMode = columnSorts[colId] || '';
6087
+ const colItems = sortColumnItems(rawColItems, globalSort);
5920
6088
  return html`
5921
6089
  <div class="pipeline-column"
5922
6090
  data-testid=${'pipeline-col-' + colId}
@@ -5927,9 +6095,8 @@ function PipelineView({
5927
6095
  onDrop=${e => onDrop(e, colId)}>
5928
6096
  <div class="pipeline-column-header"
5929
6097
  style=${'--col-color: ' + meta.color}
5930
- title=${meta.tooltip}
5931
- onClick=${() => cycleSort(colId)}>
5932
- <span>${meta.label}${sortMode && html`<span class="sort-indicator">${sortMode === 'priority' ? '\u25BC' : '\u25B2'}</span>`}</span>
6098
+ title=${meta.tooltip}>
6099
+ <span>${meta.label}</span>
5933
6100
  <span class=${'kanban-count' + (colItems.length === 0 ? ' kanban-count-zero' : '')}>${colItems.length}</span>
5934
6101
  </div>
5935
6102
  <div class="pipeline-column-items">
@@ -5957,6 +6124,19 @@ function PipelineView({
5957
6124
  <span class="epic-group-color" style="background: var(--accent)"></span>
5958
6125
  <span class="epic-group-name">${group.epicName}</span>
5959
6126
  <span class="epic-group-count">${totalCount}</span>
6127
+ ${(() => {
6128
+ const bd = group.breakdown;
6129
+ if (!bd || (bd.doneCount === 0 && bd.supersededCount === 0)) return null;
6130
+ const parts = [];
6131
+ if (bd.doneCount > 0) parts.push(bd.doneCount + ' Done');
6132
+ if (bd.supersededCount > 0) parts.push(bd.supersededCount + ' Superseded');
6133
+ if (bd.activeCount > 0) parts.push(bd.activeCount + ' Active');
6134
+ return html`<span class="epic-group-breakdown" data-testid="epic-group-breakdown"
6135
+ style="font-size:10px;color:var(--text-muted);margin-left:4px;">(${parts.join(', ')}${bd.mostCommonSuccessor ? html` → <a href="#" data-testid="epic-breakdown-successor-link" style="color:var(--accent);text-decoration:none;" onClick=${(e) => {
6136
+ e.preventDefault();
6137
+ e.stopPropagation();
6138
+ }}>#${bd.mostCommonSuccessor}</a>` : ''})</span>`;
6139
+ })()}
5960
6140
  </div>
5961
6141
  <div class="epic-group-body" id=${egBodyId}>
5962
6142
  ${group.epic && renderCard(group.epic, meta.status, true)}
@@ -6167,7 +6347,14 @@ function BoardPage({ selectedProject }) {
6167
6347
  const allKeys = ((window.PIPELINE_CONFIG && window.PIPELINE_CONFIG.swimlanes) || []).map(s => s.key);
6168
6348
  return new Set(allKeys);
6169
6349
  });
6170
- const [columnSorts, setColumnSorts] = useState({});
6350
+ const [globalSort, setGlobalSort] = useState(() => {
6351
+ try {
6352
+ const saved = localStorage.getItem('beastmode-sort-mode');
6353
+ if (saved) return JSON.parse(saved);
6354
+ } catch {}
6355
+ return { field: '', direction: 'asc' };
6356
+ });
6357
+ const [sortOpen, setSortOpen] = useState(false);
6171
6358
  const [epicCollapseKey, setEpicCollapseKey] = useState(0);
6172
6359
  const [costsByItem, setCostsByItem] = useState({});
6173
6360
  const [envVerifyByItem, setEnvVerifyByItem] = useState({});
@@ -6606,6 +6793,25 @@ function BoardPage({ selectedProject }) {
6606
6793
  } catch {}
6607
6794
  }, [activeSwimlanesSet]);
6608
6795
 
6796
+ // Persist sort preference to localStorage whenever it changes.
6797
+ useEffect(() => {
6798
+ localStorage.setItem('beastmode-sort-mode', JSON.stringify(globalSort));
6799
+ }, [globalSort]);
6800
+
6801
+ // Close sort dropdown when clicking outside.
6802
+ useEffect(() => {
6803
+ if (!sortOpen) return;
6804
+ const handler = (e) => {
6805
+ const dropdown = document.querySelector('.sort-dropdown');
6806
+ const toggle = document.querySelector('[data-testid="sort-toggle"]');
6807
+ if (dropdown && !dropdown.contains(e.target) && toggle && !toggle.contains(e.target)) {
6808
+ setSortOpen(false);
6809
+ }
6810
+ };
6811
+ document.addEventListener('click', handler, true);
6812
+ return () => document.removeEventListener('click', handler, true);
6813
+ }, [sortOpen]);
6814
+
6609
6815
  // Expose swimlane state for external scenario verification.
6610
6816
  useEffect(() => {
6611
6817
  const swimlanes = (window.PIPELINE_CONFIG && window.PIPELINE_CONFIG.swimlanes) || [];
@@ -6687,28 +6893,32 @@ function BoardPage({ selectedProject }) {
6687
6893
  const activeFilterCount = _baseFilterCount + (_swimlaneFilterActive ? 1 : 0);
6688
6894
 
6689
6895
  // ── Column sorting ──
6896
+ const SORT_LABELS = {
6897
+ priority: 'Priority',
6898
+ name: 'Name',
6899
+ created: 'Created',
6900
+ updated: 'Updated',
6901
+ status_age: 'Time in Status',
6902
+ };
6690
6903
  const PRIORITY_ORDER = { 'Critical': 0, 'High': 1, 'Medium': 2, 'Low': 3, '': 4 };
6691
- const sortColumnItems = (colItems, mode) => {
6692
- if (!mode) return colItems; // default: keep API order (newest first)
6904
+ const sortColumnItems = (colItems, sort) => {
6905
+ if (!sort || !sort.field) return colItems;
6906
+ const dir = sort.direction === 'desc' ? -1 : 1;
6693
6907
  const sorted = [...colItems];
6694
- if (mode === 'priority') {
6695
- sorted.sort((a, b) => (PRIORITY_ORDER[a.priority || ''] ?? 4) - (PRIORITY_ORDER[b.priority || ''] ?? 4));
6696
- } else if (mode === 'name') {
6697
- sorted.sort((a, b) => (a.name || a.title || '').localeCompare(b.name || b.title || ''));
6908
+ if (sort.field === 'priority') {
6909
+ sorted.sort((a, b) => dir * ((PRIORITY_ORDER[a.priority || ''] ?? 4) - (PRIORITY_ORDER[b.priority || ''] ?? 4)));
6910
+ } else if (sort.field === 'name') {
6911
+ sorted.sort((a, b) => dir * (a.name || a.title || '').localeCompare(b.name || b.title || ''));
6912
+ } else if (sort.field === 'created') {
6913
+ sorted.sort((a, b) => dir * ((a.created_at || '').localeCompare(b.created_at || '')));
6914
+ } else if (sort.field === 'updated') {
6915
+ sorted.sort((a, b) => dir * ((a.updated_at || '').localeCompare(b.updated_at || '')));
6916
+ } else if (sort.field === 'status_age') {
6917
+ sorted.sort((a, b) => dir * ((a.status_changed_at || a.updated_at || '').localeCompare(b.status_changed_at || b.updated_at || '')));
6698
6918
  }
6699
6919
  return sorted;
6700
6920
  };
6701
6921
 
6702
- const cycleSort = (colId) => {
6703
- setColumnSorts(prev => {
6704
- const current = prev[colId] || '';
6705
- const next = current === '' ? 'priority' : current === 'priority' ? 'name' : '';
6706
- const copy = { ...prev };
6707
- if (next) copy[colId] = next; else delete copy[colId];
6708
- return copy;
6709
- });
6710
- };
6711
-
6712
6922
  // Unique project IDs and parent epics for filter dropdowns
6713
6923
  const uniqueProjects = [...new Set(items.map(i => i.project_id).filter(Boolean))];
6714
6924
  const uniqueEpics = [...new Set(items.map(i => i.parent_epic).filter(Boolean))].map(String);
@@ -6794,6 +7004,46 @@ function BoardPage({ selectedProject }) {
6794
7004
  ${activeFilterCount > 0 && html`<span class="filter-active-count">${activeFilterCount}</span>`}
6795
7005
  </button>
6796
7006
 
7007
+ <div style="position: relative;">
7008
+ <button class=${'sort-toggle' + (sortOpen || globalSort.field ? ' active' : '')}
7009
+ onClick=${() => setSortOpen(v => !v)}
7010
+ data-testid="sort-toggle">
7011
+ <${Icon} name="arrow-up-down" size=${13} />
7012
+ Sort
7013
+ ${globalSort.field && html`<span class="sort-active-label">${SORT_LABELS[globalSort.field]}</span>`}
7014
+ </button>
7015
+ ${sortOpen && html`
7016
+ <div class="sort-dropdown" data-testid="sort-dropdown">
7017
+ <div class="sort-options">
7018
+ ${Object.entries(SORT_LABELS).map(([key, label]) => html`
7019
+ <button key=${key}
7020
+ class=${'sort-option' + (globalSort.field === key ? ' active' : '')}
7021
+ data-testid=${'sort-option-' + key}
7022
+ onClick=${() => {
7023
+ if (globalSort.field === key) {
7024
+ setGlobalSort(s => ({ ...s, direction: s.direction === 'asc' ? 'desc' : 'asc' }));
7025
+ } else {
7026
+ const defaultDir = (key === 'created' || key === 'updated') ? 'desc' : 'asc';
7027
+ setGlobalSort({ field: key, direction: defaultDir });
7028
+ }
7029
+ }}>
7030
+ <span>${label}</span>
7031
+ ${globalSort.field === key && html`
7032
+ <span class="sort-dir-icon">${globalSort.direction === 'asc' ? '↑' : '↓'}</span>
7033
+ `}
7034
+ </button>
7035
+ `)}
7036
+ </div>
7037
+ ${globalSort.field && html`
7038
+ <button class="sort-clear" data-testid="sort-clear"
7039
+ onClick=${() => { setGlobalSort({ field: '', direction: 'asc' }); setSortOpen(false); }}>
7040
+ Clear sort
7041
+ </button>
7042
+ `}
7043
+ </div>
7044
+ `}
7045
+ </div>
7046
+
6797
7047
  <button class="btn btn-primary" onClick=${() => setShowCreateDialog(true)}>
6798
7048
  <${Icon} name="plus" size=${14} />
6799
7049
  New Task
@@ -6891,8 +7141,7 @@ function BoardPage({ selectedProject }) {
6891
7141
  selectedProject=${selectedProject}
6892
7142
  setSelectedItem=${setSelectedItem}
6893
7143
  deleteItem=${deleteItem}
6894
- columnSorts=${columnSorts}
6895
- cycleSort=${cycleSort}
7144
+ globalSort=${globalSort}
6896
7145
  sortColumnItems=${sortColumnItems}
6897
7146
  costsByItem=${costsByItem}
6898
7147
  envVerifyByItem=${envVerifyByItem}
@@ -6927,15 +7176,14 @@ function BoardPage({ selectedProject }) {
6927
7176
  if (col.also && col.also.includes(i.status)) return true;
6928
7177
  return false;
6929
7178
  });
6930
- const colItems = sortColumnItems(rawColItems, columnSorts[col.id] || '');
6931
- const sortMode = columnSorts[col.id] || '';
7179
+ const colItems = sortColumnItems(rawColItems, globalSort);
6932
7180
  return html`
6933
7181
  <div class="kanban-column" key=${col.id}
6934
7182
  onDragOver=${e => onDragOver(e, col.id)}
6935
7183
  onDragLeave=${onDragLeave}
6936
7184
  onDrop=${e => onDrop(e, col.id)}>
6937
- <div class="kanban-column-header" style=${'--col-color: ' + col.color} title=${col.tooltip} onClick=${() => cycleSort(col.id)}>
6938
- <span>${col.label}${sortMode && html`<span class="sort-indicator">${sortMode === 'priority' ? '\u25BC' : '\u25B2'}</span>`}</span>
7185
+ <div class="kanban-column-header" style=${'--col-color: ' + col.color} title=${col.tooltip}>
7186
+ <span>${col.label}</span>
6939
7187
  <span class=${'kanban-count' + (colItems.length === 0 ? ' kanban-count-zero' : '')}>${colItems.length}</span>
6940
7188
  </div>
6941
7189
  <div class="kanban-items">
@@ -6957,6 +7205,15 @@ function BoardPage({ selectedProject }) {
6957
7205
  <div class="card-title" style="cursor:pointer;" onClick=${() => setSelectedItem(item)} title=${item.name || item.title}>${item.name || item.title}</div>
6958
7206
  <div class="card-footer">
6959
7207
  ${isOverlayStatus(item.status) && html`<span class=${'card-badge badge-overlay ' + overlayBadgeClass(item.status)} title=${overlayTooltip(item.status)}>${overlayBadgeLabel(item.status)}</span>`}
7208
+ ${item.extra && item.extra.superseded_by && html`<a class="card-badge badge-successor"
7209
+ href="#" data-testid="card-successor-link"
7210
+ title=${'Superseded by #' + item.extra.superseded_by}
7211
+ onClick=${(e) => {
7212
+ e.preventDefault();
7213
+ e.stopPropagation();
7214
+ const target = swimlaneFilteredItems.find(i => String(i.id) === String(item.extra.superseded_by));
7215
+ if (target) setSelectedItem(target);
7216
+ }}>→ #${item.extra.superseded_by}</a>`}
6960
7217
  ${!isParentEpic && item.parent_epic && html`<span class="card-badge badge-epic">epic:${item.parent_epic}</span>`}
6961
7218
  ${item.priority && html`<span class=${'card-badge ' + priorityBadgeClass(item.priority)}>${item.priority}</span>`}
6962
7219
  ${item.task_type && html`<span class="card-badge badge-type">${item.task_type}</span>`}
@@ -7006,6 +7263,19 @@ function BoardPage({ selectedProject }) {
7006
7263
  <span class="epic-group-color" style="background: var(--accent)"></span>
7007
7264
  <span class="epic-group-name">${group.epicName}</span>
7008
7265
  <span class="epic-group-count">${totalCount}</span>
7266
+ ${(() => {
7267
+ const bd = group.breakdown;
7268
+ if (!bd || (bd.doneCount === 0 && bd.supersededCount === 0)) return null;
7269
+ const parts = [];
7270
+ if (bd.doneCount > 0) parts.push(bd.doneCount + ' Done');
7271
+ if (bd.supersededCount > 0) parts.push(bd.supersededCount + ' Superseded');
7272
+ if (bd.activeCount > 0) parts.push(bd.activeCount + ' Active');
7273
+ return html`<span class="epic-group-breakdown" data-testid="epic-group-breakdown"
7274
+ style="font-size:10px;color:var(--text-muted);margin-left:4px;">(${parts.join(', ')}${bd.mostCommonSuccessor ? html` → <a href="#" data-testid="epic-breakdown-successor-link" style="color:var(--accent);text-decoration:none;" onClick=${(e) => {
7275
+ e.preventDefault();
7276
+ e.stopPropagation();
7277
+ }}>#${bd.mostCommonSuccessor}</a>` : ''})</span>`;
7278
+ })()}
7009
7279
  </div>
7010
7280
  <div class="epic-group-body" id=${bodyId}>
7011
7281
  ${group.epic && renderCard(group.epic, true)}
@@ -1 +1 @@
1
- 6395b562346253985efd5667de1e2809baa36bf0
1
+ 09eb71470e076891f6ed9304e081cc9372bad7fc
@@ -1 +1 @@
1
- 20260509-145929-6395b56
1
+ 20260509-230834-09eb714
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@beastmode-develeap/beastmode",
3
- "version": "0.1.212",
3
+ "version": "0.1.214",
4
4
  "description": "BeastMode Dark Factory — turn intent into verified software",
5
5
  "type": "module",
6
6
  "bin": {