@beastmode-develeap/beastmode 0.1.124 → 0.1.126

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__ = "20260418-213012-7a72646";</script>
18
+ <script>window.__BUILD_STAMP__ = "20260419-095657-596a382";</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">
@@ -1738,6 +1738,105 @@ input[type="range"]::-webkit-slider-thumb {
1738
1738
  }
1739
1739
  .filter-clear-link:hover { text-decoration: underline; }
1740
1740
 
1741
+ /* Swimlane filter chips (Story 5) */
1742
+ .swimlane-controls-row {
1743
+ display: flex;
1744
+ align-items: center;
1745
+ justify-content: space-between;
1746
+ gap: 12px;
1747
+ padding: 4px 0;
1748
+ }
1749
+ .swimlane-chips {
1750
+ display: flex;
1751
+ flex-wrap: wrap;
1752
+ gap: 6px;
1753
+ padding: 8px 0;
1754
+ margin-bottom: 8px;
1755
+ border-bottom: 1px solid var(--border-subtle);
1756
+ }
1757
+ .swimlane-chip {
1758
+ display: inline-flex;
1759
+ align-items: center;
1760
+ gap: 6px;
1761
+ padding: 4px 12px;
1762
+ border-radius: 16px;
1763
+ font-size: 12px;
1764
+ font-weight: 500;
1765
+ font-family: var(--font-sans);
1766
+ background: var(--bg-input);
1767
+ color: var(--text-muted);
1768
+ border: 1px solid var(--border);
1769
+ cursor: pointer;
1770
+ transition: all 0.15s ease;
1771
+ user-select: none;
1772
+ }
1773
+ .swimlane-chip:hover {
1774
+ border-color: var(--text-muted);
1775
+ color: var(--text-secondary);
1776
+ }
1777
+ .swimlane-chip.active {
1778
+ background: color-mix(in srgb, var(--chip-color, var(--accent)) 12%, transparent);
1779
+ color: var(--chip-color, var(--accent));
1780
+ border-color: color-mix(in srgb, var(--chip-color, var(--accent)) 30%, transparent);
1781
+ }
1782
+ .swimlane-chip-dot {
1783
+ width: 8px;
1784
+ height: 8px;
1785
+ border-radius: 50%;
1786
+ background: var(--chip-color, var(--text-muted));
1787
+ opacity: 0.5;
1788
+ transition: opacity 0.15s ease;
1789
+ }
1790
+ .swimlane-chip.active .swimlane-chip-dot {
1791
+ opacity: 1;
1792
+ }
1793
+ .swimlane-chip-count {
1794
+ font-family: var(--font-mono);
1795
+ font-size: 11px;
1796
+ opacity: 0.7;
1797
+ }
1798
+ .swimlane-chip-all {
1799
+ font-weight: 600;
1800
+ }
1801
+ .swimlane-chip-all.active {
1802
+ background: var(--accent-subtle);
1803
+ color: var(--accent);
1804
+ border-color: rgba(245, 166, 35, 0.3);
1805
+ }
1806
+ @supports not (background: color-mix(in srgb, red 12%, transparent)) {
1807
+ .swimlane-chip.active {
1808
+ background: var(--accent-subtle);
1809
+ color: var(--accent);
1810
+ border-color: rgba(245, 166, 35, 0.3);
1811
+ }
1812
+ }
1813
+ .collapse-all-btn {
1814
+ display: inline-flex;
1815
+ align-items: center;
1816
+ gap: 4px;
1817
+ padding: 4px 10px;
1818
+ border-radius: var(--radius-xs);
1819
+ font-size: 11px;
1820
+ font-weight: 500;
1821
+ font-family: var(--font-sans);
1822
+ background: transparent;
1823
+ color: var(--text-muted);
1824
+ border: 1px solid var(--border);
1825
+ cursor: pointer;
1826
+ transition: all 0.15s ease;
1827
+ }
1828
+ .collapse-all-btn:hover {
1829
+ border-color: var(--text-muted);
1830
+ color: var(--text-secondary);
1831
+ background: var(--bg-input);
1832
+ }
1833
+ @media (max-width: 768px) {
1834
+ .swimlane-controls-row {
1835
+ flex-direction: column;
1836
+ align-items: flex-start;
1837
+ }
1838
+ }
1839
+
1741
1840
  /* Column sort indicator */
1742
1841
  .kanban-column-header .sort-indicator {
1743
1842
  font-size: 11px;
@@ -3759,6 +3858,12 @@ function saveSwimlaneCollapse(laneKey, collapsed) {
3759
3858
  } catch {}
3760
3859
  }
3761
3860
 
3861
+ // NOTE: `swimlaneColor` is declared earlier in this file (first occurrence
3862
+ // wins). A second duplicate block lived here and broke the whole page with
3863
+ // "Identifier 'swimlaneColor' has already been declared" — fixed 2026-04-19,
3864
+ // visible only in the browser (the UI tooling never exercised the in-page
3865
+ // script so the syntax error slipped past CI).
3866
+
3762
3867
  // ── HTML Content Renderer (for Monday.com update bodies) ──
3763
3868
 
3764
3869
  function _sanitizeHtml(raw) {
@@ -4610,6 +4715,40 @@ function SwimlaneMiniBar({ stat }) {
4610
4715
  `;
4611
4716
  }
4612
4717
 
4718
+ // ── Swimlane Filter Chips + Collapse All (Story 5) ──
4719
+
4720
+ function SwimlaneFilterChips({ activeSwimlanesSet, onToggle, onToggleAll, chipCounts }) {
4721
+ const swimlanes = (window.PIPELINE_CONFIG && window.PIPELINE_CONFIG.swimlanes) || [];
4722
+ const allActive = swimlanes.length > 0 && activeSwimlanesSet.size === swimlanes.length;
4723
+ return html`
4724
+ <div class="swimlane-chips" data-testid="swimlane-filter-chips">
4725
+ <button class=${'swimlane-chip swimlane-chip-all' + (allActive ? ' active' : '')}
4726
+ onClick=${onToggleAll}
4727
+ aria-pressed=${allActive ? 'true' : 'false'}
4728
+ data-testid="swimlane-chip-all">
4729
+ All
4730
+ </button>
4731
+ ${swimlanes.map(lane => {
4732
+ const isActive = activeSwimlanesSet.has(lane.key);
4733
+ const color = swimlaneColor(lane.color);
4734
+ const count = (chipCounts && typeof chipCounts[lane.key] === 'number') ? chipCounts[lane.key] : 0;
4735
+ return html`
4736
+ <button key=${lane.key}
4737
+ class=${'swimlane-chip' + (isActive ? ' active' : '')}
4738
+ style=${'--chip-color: ' + color}
4739
+ onClick=${() => onToggle(lane.key)}
4740
+ aria-pressed=${isActive ? 'true' : 'false'}
4741
+ data-testid=${'swimlane-chip-' + lane.key}>
4742
+ <span class="swimlane-chip-dot"></span>
4743
+ <span>${lane.label}</span>
4744
+ <span class="swimlane-chip-count">${count}</span>
4745
+ </button>
4746
+ `;
4747
+ })}
4748
+ </div>
4749
+ `;
4750
+ }
4751
+
4613
4752
  function BottleneckBadge({ warning }) {
4614
4753
  const title = `${warning.swimlaneLabel}: ${warning.count} items stuck in ${warning.stage}`;
4615
4754
  return html`
@@ -4676,6 +4815,17 @@ function PipelineHealth({ items }) {
4676
4815
  `;
4677
4816
  }
4678
4817
 
4818
+ function CollapseAllButton({ allCollapsed, onToggle }) {
4819
+ return html`
4820
+ <button class="collapse-all-btn"
4821
+ onClick=${onToggle}
4822
+ title=${allCollapsed ? 'Expand all swimlanes' : 'Collapse all swimlanes'}
4823
+ data-testid="collapse-all-btn">
4824
+ ${allCollapsed ? 'Expand All' : 'Collapse All'}
4825
+ </button>
4826
+ `;
4827
+ }
4828
+
4679
4829
  // ── Board Page ──
4680
4830
 
4681
4831
  function BoardPage({ selectedProject }) {
@@ -4683,11 +4833,22 @@ function BoardPage({ selectedProject }) {
4683
4833
  const [loading, setLoading] = useState(true);
4684
4834
  const [error, setError] = useState(null);
4685
4835
  const [dragInfo, setDragInfo] = useState(null);
4836
+ const dragInfoRef = useRef(null);
4686
4837
  const [selectedItem, setSelectedItem] = useState(null);
4687
4838
  const [showCreateDialog, setShowCreateDialog] = useState(false);
4688
4839
  const [searchTerm, setSearchTerm] = useState('');
4689
4840
  const [filtersOpen, setFiltersOpen] = useState(false);
4690
4841
  const [filters, setFilters] = useState({ priority: '', taskType: '', project: '', dateRange: '', parentEpic: '' });
4842
+ const [activeSwimlanesSet, setActiveSwimlanesSet] = useState(() => {
4843
+ try {
4844
+ const saved = JSON.parse(localStorage.getItem('beastmode-swimlane-filter') || 'null');
4845
+ if (Array.isArray(saved) && saved.length > 0) {
4846
+ return new Set(saved);
4847
+ }
4848
+ } catch {}
4849
+ const allKeys = ((window.PIPELINE_CONFIG && window.PIPELINE_CONFIG.swimlanes) || []).map(s => s.key);
4850
+ return new Set(allKeys);
4851
+ });
4691
4852
  const [columnSorts, setColumnSorts] = useState({});
4692
4853
  const [epicCollapseKey, setEpicCollapseKey] = useState(0);
4693
4854
  const [costsByItem, setCostsByItem] = useState({});
@@ -4874,6 +5035,7 @@ function BoardPage({ selectedProject }) {
4874
5035
  const item = items.find(i => String(i.id) === String(id));
4875
5036
  const taskType = (item && item.task_type) ? item.task_type : 'code';
4876
5037
  setDragInfo({ id, taskType });
5038
+ dragInfoRef.current = { id, taskType };
4877
5039
  e.dataTransfer.effectAllowed = 'move';
4878
5040
  try { e.dataTransfer.setData('text/plain', String(id)); } catch {}
4879
5041
  e.target.classList.add('dragging');
@@ -4882,6 +5044,7 @@ function BoardPage({ selectedProject }) {
4882
5044
  const onDragEnd = (e) => {
4883
5045
  e.target.classList.remove('dragging');
4884
5046
  setDragInfo(null);
5047
+ dragInfoRef.current = null;
4885
5048
  document.querySelectorAll('.drag-over, .drag-over-invalid').forEach(el => {
4886
5049
  el.classList.remove('drag-over', 'drag-over-invalid');
4887
5050
  });
@@ -4889,9 +5052,10 @@ function BoardPage({ selectedProject }) {
4889
5052
 
4890
5053
  const onDragOver = (e, targetColumnId) => {
4891
5054
  e.preventDefault();
4892
- if (!dragInfo) return;
5055
+ const di = dragInfoRef.current;
5056
+ if (!di) return;
4893
5057
  const validStages = (typeof getStagesForType === 'function')
4894
- ? getStagesForType(dragInfo.taskType)
5058
+ ? getStagesForType(di.taskType)
4895
5059
  : [];
4896
5060
  const isValid = validStages.indexOf(targetColumnId) !== -1;
4897
5061
  if (isValid) {
@@ -4912,15 +5076,16 @@ function BoardPage({ selectedProject }) {
4912
5076
  const onDrop = async (e, status) => {
4913
5077
  e.preventDefault();
4914
5078
  e.currentTarget.classList.remove('drag-over', 'drag-over-invalid');
4915
- if (!dragInfo) return;
5079
+ const di = dragInfoRef.current;
5080
+ if (!di) return;
4916
5081
  const validStages = (typeof getStagesForType === 'function')
4917
- ? getStagesForType(dragInfo.taskType)
5082
+ ? getStagesForType(di.taskType)
4918
5083
  : [];
4919
5084
  if (validStages.indexOf(status) === -1) return;
4920
- const current = items.find(i => String(i.id) === String(dragInfo.id));
5085
+ const current = items.find(i => String(i.id) === String(di.id));
4921
5086
  if (current && current.status === status) return;
4922
5087
  try {
4923
- await api('PATCH', '/api/board/items/' + dragInfo.id, { status });
5088
+ await api('PATCH', '/api/board/items/' + di.id, { status });
4924
5089
  fetchItems();
4925
5090
  } catch (err) { setError(err.message); }
4926
5091
  };
@@ -4958,8 +5123,62 @@ function BoardPage({ selectedProject }) {
4958
5123
  return true;
4959
5124
  });
4960
5125
 
5126
+ // Persist swimlane filter to localStorage whenever it changes.
5127
+ useEffect(() => {
5128
+ try {
5129
+ localStorage.setItem('beastmode-swimlane-filter', JSON.stringify([...activeSwimlanesSet]));
5130
+ } catch {}
5131
+ }, [activeSwimlanesSet]);
5132
+
5133
+ // Expose swimlane state for external scenario verification.
5134
+ useEffect(() => {
5135
+ const swimlanes = (window.PIPELINE_CONFIG && window.PIPELINE_CONFIG.swimlanes) || [];
5136
+ window.__BEASTMODE_SWIMLANE_FILTER__ = {
5137
+ active: [...activeSwimlanesSet],
5138
+ total: swimlanes.length,
5139
+ };
5140
+ }, [activeSwimlanesSet]);
5141
+
5142
+ const toggleSwimlane = (key) => {
5143
+ setActiveSwimlanesSet(prev => {
5144
+ const next = new Set(prev);
5145
+ if (next.has(key)) {
5146
+ if (next.size > 1) next.delete(key);
5147
+ } else {
5148
+ next.add(key);
5149
+ }
5150
+ return next;
5151
+ });
5152
+ };
5153
+
5154
+ const toggleAllSwimlanes = () => {
5155
+ const allKeys = ((window.PIPELINE_CONFIG && window.PIPELINE_CONFIG.swimlanes) || []).map(s => s.key);
5156
+ setActiveSwimlanesSet(prev => {
5157
+ if (prev.size === allKeys.length) {
5158
+ return new Set(allKeys.slice(0, 1));
5159
+ }
5160
+ return new Set(allKeys);
5161
+ });
5162
+ };
5163
+
5164
+ // Compute chip counts from filteredItems (pre-swimlane) so users can see
5165
+ // how many items each swimlane holds regardless of the current chip state.
5166
+ const chipCounts = {};
5167
+ ((window.PIPELINE_CONFIG && window.PIPELINE_CONFIG.swimlanes) || []).forEach(lane => {
5168
+ const taskTypes = lane.taskTypes || lane.task_types || [];
5169
+ chipCounts[lane.key] = filteredItems.filter(i => taskTypes.includes(i.task_type || 'code')).length;
5170
+ });
5171
+
5172
+ // Apply the swimlane filter on top of the existing filters.
5173
+ const swimlaneFilteredItems = filteredItems.filter(item => {
5174
+ const lane = getSwimlaneForType(item.task_type || 'code');
5175
+ return lane && activeSwimlanesSet.has(lane.key);
5176
+ });
5177
+
4961
5178
  // Active filter count
4962
- const activeFilterCount = [filters.priority, filters.taskType, filters.project, filters.dateRange, filters.parentEpic].filter(Boolean).length;
5179
+ const _baseFilterCount = [filters.priority, filters.taskType, filters.project, filters.dateRange, filters.parentEpic].filter(Boolean).length;
5180
+ const _swimlaneFilterActive = activeSwimlanesSet.size < (((window.PIPELINE_CONFIG && window.PIPELINE_CONFIG.swimlanes) || []).length || 0);
5181
+ const activeFilterCount = _baseFilterCount + (_swimlaneFilterActive ? 1 : 0);
4963
5182
 
4964
5183
  // ── Column sorting ──
4965
5184
  const PRIORITY_ORDER = { 'Critical': 0, 'High': 1, 'Medium': 2, 'Low': 3, '': 4 };
@@ -5020,10 +5239,10 @@ function BoardPage({ selectedProject }) {
5020
5239
  `;
5021
5240
  }
5022
5241
 
5023
- const totalItems = filteredItems.length;
5024
- const activeItems = filteredItems.filter(i => i.status !== 'Done').length;
5025
- const doneItems = filteredItems.filter(i => i.status === 'Done').length;
5026
- const stuckItems = filteredItems.filter(i => i.status === 'Stuck').length;
5242
+ const totalItems = swimlaneFilteredItems.length;
5243
+ const activeItems = swimlaneFilteredItems.filter(i => i.status !== 'Done').length;
5244
+ const doneItems = swimlaneFilteredItems.filter(i => i.status === 'Done').length;
5245
+ const stuckItems = swimlaneFilteredItems.filter(i => i.status === 'Stuck').length;
5027
5246
  const progressPct = totalItems > 0 ? Math.round((doneItems / totalItems) * 100) : 0;
5028
5247
  const isFiltered = searchTerm.length >= 2 || activeFilterCount > 0;
5029
5248
 
@@ -5120,10 +5339,22 @@ function BoardPage({ selectedProject }) {
5120
5339
  `}
5121
5340
 
5122
5341
  ${items.length > 0 && html`<${PipelineHealth} items=${filteredItems} />`}
5342
+ <div class="swimlane-controls-row" data-testid="swimlane-controls">
5343
+ <${SwimlaneFilterChips}
5344
+ activeSwimlanesSet=${activeSwimlanesSet}
5345
+ onToggle=${toggleSwimlane}
5346
+ onToggleAll=${toggleAllSwimlanes}
5347
+ chipCounts=${chipCounts}
5348
+ />
5349
+ <${CollapseAllButton}
5350
+ allCollapsed=${activeSwimlanesSet.size === 1}
5351
+ onToggle=${toggleAllSwimlanes}
5352
+ />
5353
+ </div>
5123
5354
 
5124
5355
  ${viewMode === 'pipeline'
5125
5356
  ? html`<${PipelineView}
5126
- filteredItems=${filteredItems}
5357
+ filteredItems=${swimlaneFilteredItems}
5127
5358
  items=${items}
5128
5359
  onDragStart=${onDragStart}
5129
5360
  onDragEnd=${onDragEnd}
@@ -5162,7 +5393,7 @@ function BoardPage({ selectedProject }) {
5162
5393
  </div>
5163
5394
  <div class="kanban">
5164
5395
  ${KANBAN_COLUMNS.map(col => {
5165
- const rawColItems = filteredItems.filter(i => {
5396
+ const rawColItems = swimlaneFilteredItems.filter(i => {
5166
5397
  if (col.id === 'New') return !i.status || i.status === 'New' || i.status === '';
5167
5398
  if (i.status === col.id) return true;
5168
5399
  if (col.also && col.also.includes(i.status)) return true;
@@ -0,0 +1 @@
1
+ 596a38260632710347fefd052b85af2a64628e87
@@ -1 +1 @@
1
- 20260418-213012-7a72646
1
+ 20260419-095657-596a382
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@beastmode-develeap/beastmode",
3
- "version": "0.1.124",
3
+ "version": "0.1.126",
4
4
  "description": "BeastMode Dark Factory — turn intent into verified software",
5
5
  "type": "module",
6
6
  "bin": {