@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.
- package/dist/index.js +6 -1
- package/dist/index.js.map +1 -1
- package/dist/web/board.html +245 -14
- package/dist/web/build-commit.txt +1 -0
- package/dist/web/build-stamp.txt +1 -1
- package/package.json +1 -1
package/dist/web/board.html
CHANGED
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
}
|
|
16
16
|
</script>
|
|
17
17
|
<!--BOARD_DATA-->
|
|
18
|
-
<script>window.__BUILD_STAMP__ = "
|
|
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
|
-
|
|
5055
|
+
const di = dragInfoRef.current;
|
|
5056
|
+
if (!di) return;
|
|
4893
5057
|
const validStages = (typeof getStagesForType === 'function')
|
|
4894
|
-
? getStagesForType(
|
|
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
|
-
|
|
5079
|
+
const di = dragInfoRef.current;
|
|
5080
|
+
if (!di) return;
|
|
4916
5081
|
const validStages = (typeof getStagesForType === 'function')
|
|
4917
|
-
? getStagesForType(
|
|
5082
|
+
? getStagesForType(di.taskType)
|
|
4918
5083
|
: [];
|
|
4919
5084
|
if (validStages.indexOf(status) === -1) return;
|
|
4920
|
-
const current = items.find(i => String(i.id) === String(
|
|
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/' +
|
|
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
|
|
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 =
|
|
5024
|
-
const activeItems =
|
|
5025
|
-
const doneItems =
|
|
5026
|
-
const stuckItems =
|
|
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=${
|
|
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 =
|
|
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
|
package/dist/web/build-stamp.txt
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
|
|
1
|
+
20260419-095657-596a382
|