@beastmode-develeap/beastmode 0.1.107 → 0.1.109

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-002742-2ab080a";</script>
18
+ <script>window.__BUILD_STAMP__ = "20260418-075248-39e5267";</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">
@@ -50,6 +50,10 @@
50
50
  --accent-subtle: rgba(245, 166, 35, 0.12);
51
51
  --accent-glow: rgba(245, 166, 35, 0.2);
52
52
 
53
+ /* Swimlane colors */
54
+ --purple: #a78bfa;
55
+ --orange: #f97316;
56
+
53
57
  /* Semantic */
54
58
  --success: #34d399;
55
59
  --success-subtle: rgba(52, 211, 153, 0.12);
@@ -97,6 +101,8 @@
97
101
  --accent-hover: #E5961A;
98
102
  --accent-subtle: rgba(245, 166, 35, 0.10);
99
103
  --accent-glow: rgba(245, 166, 35, 0.18);
104
+ --purple: #7c3aed;
105
+ --orange: #ea580c;
100
106
  --success: #059669;
101
107
  --success-subtle: rgba(5, 150, 105, 0.08);
102
108
  --danger: #dc2626;
@@ -1285,6 +1291,163 @@ input[type="range"]::-webkit-slider-thumb {
1285
1291
  display: none;
1286
1292
  }
1287
1293
 
1294
+ /* ================================================================
1295
+ PIPELINE VIEW
1296
+ ================================================================ */
1297
+
1298
+ .pipeline-view {
1299
+ display: flex;
1300
+ flex-direction: column;
1301
+ gap: 16px;
1302
+ padding: 0 0 24px 0;
1303
+ }
1304
+
1305
+ .pipeline-swimlane {
1306
+ background: var(--bg-card);
1307
+ border: 1px solid var(--border);
1308
+ border-radius: var(--radius-lg);
1309
+ overflow: hidden;
1310
+ }
1311
+
1312
+ .pipeline-swimlane-collapsed .pipeline-swimlane-body {
1313
+ display: none;
1314
+ }
1315
+
1316
+ .pipeline-swimlane-empty {
1317
+ opacity: 0.5;
1318
+ }
1319
+
1320
+ .pipeline-swimlane-header {
1321
+ display: flex;
1322
+ align-items: center;
1323
+ gap: 10px;
1324
+ padding: 12px 16px;
1325
+ background: var(--surface-elevated);
1326
+ border-bottom: 1px solid var(--border);
1327
+ border-left: 4px solid var(--lane-color, var(--text-muted));
1328
+ cursor: pointer;
1329
+ user-select: none;
1330
+ transition: background 0.15s ease;
1331
+ }
1332
+
1333
+ .pipeline-swimlane-collapsed .pipeline-swimlane-header {
1334
+ border-bottom: none;
1335
+ }
1336
+
1337
+ .pipeline-swimlane-header:hover {
1338
+ background: var(--bg-card-hover);
1339
+ }
1340
+
1341
+ .pipeline-swimlane-chevron {
1342
+ font-size: 10px;
1343
+ color: var(--text-muted);
1344
+ width: 14px;
1345
+ text-align: center;
1346
+ }
1347
+
1348
+ .pipeline-swimlane-color {
1349
+ width: 8px;
1350
+ height: 8px;
1351
+ border-radius: 50%;
1352
+ flex-shrink: 0;
1353
+ }
1354
+
1355
+ .pipeline-swimlane-label {
1356
+ font-size: 14px;
1357
+ font-weight: 600;
1358
+ color: var(--text);
1359
+ flex: 1;
1360
+ letter-spacing: 0.3px;
1361
+ }
1362
+
1363
+ .pipeline-swimlane-count {
1364
+ background: var(--lane-color, var(--accent));
1365
+ color: #1a1a1a;
1366
+ border-radius: 10px;
1367
+ padding: 1px 8px;
1368
+ font-size: 11px;
1369
+ font-weight: 700;
1370
+ min-width: 22px;
1371
+ text-align: center;
1372
+ }
1373
+
1374
+ .pipeline-swimlane-count-zero {
1375
+ background: var(--surface-elevated);
1376
+ color: var(--text-muted);
1377
+ }
1378
+
1379
+ .pipeline-swimlane-body {
1380
+ display: flex;
1381
+ gap: 0;
1382
+ overflow-x: auto;
1383
+ padding: 0;
1384
+ }
1385
+
1386
+ .pipeline-swimlane-body::-webkit-scrollbar { height: 6px; }
1387
+ .pipeline-swimlane-body::-webkit-scrollbar-track { background: transparent; }
1388
+ .pipeline-swimlane-body::-webkit-scrollbar-thumb { background: var(--border); border-radius: 3px; }
1389
+ .pipeline-swimlane-body::-webkit-scrollbar-thumb:hover { background: var(--text-muted); }
1390
+
1391
+ .pipeline-column {
1392
+ min-width: 200px;
1393
+ max-width: 240px;
1394
+ flex: 1;
1395
+ border-right: 1px solid var(--border-subtle);
1396
+ display: flex;
1397
+ flex-direction: column;
1398
+ }
1399
+
1400
+ .pipeline-column:last-child {
1401
+ border-right: none;
1402
+ }
1403
+
1404
+ .pipeline-column.drag-over {
1405
+ border: 2px dashed var(--accent);
1406
+ background: var(--accent-subtle);
1407
+ box-shadow: var(--shadow-glow);
1408
+ }
1409
+
1410
+ .pipeline-column-header {
1411
+ padding: 10px 12px;
1412
+ border-top: 3px solid var(--col-color, var(--text-muted));
1413
+ background: var(--bg-card);
1414
+ font-size: 11px;
1415
+ font-weight: 600;
1416
+ color: var(--text-secondary);
1417
+ display: flex;
1418
+ justify-content: space-between;
1419
+ align-items: center;
1420
+ text-transform: uppercase;
1421
+ letter-spacing: 0.3px;
1422
+ cursor: pointer;
1423
+ user-select: none;
1424
+ white-space: nowrap;
1425
+ }
1426
+
1427
+ .pipeline-column-items {
1428
+ padding: 6px;
1429
+ flex: 1;
1430
+ min-height: 40px;
1431
+ max-height: 400px;
1432
+ overflow-y: auto;
1433
+ }
1434
+
1435
+ @media (max-width: 900px) {
1436
+ .pipeline-swimlane-body {
1437
+ flex-direction: column;
1438
+ overflow-x: hidden;
1439
+ }
1440
+ .pipeline-column {
1441
+ max-width: 100%;
1442
+ min-width: 100%;
1443
+ border-right: none;
1444
+ border-bottom: 1px solid var(--border-subtle);
1445
+ }
1446
+ .pipeline-column:last-child {
1447
+ border-bottom: none;
1448
+ }
1449
+ }
1450
+
1288
1451
  /* Board stats bar */
1289
1452
  .board-stats-bar {
1290
1453
  display: flex;
@@ -2953,6 +3116,72 @@ function partitionByEpic(columnItems, allItems) {
2953
3116
  return { ungrouped, groups, sortedGroupKeys };
2954
3117
  }
2955
3118
 
3119
+ // ── Pipeline (swimlane) helpers ──
3120
+
3121
+ function computeSwimlaneColumns(laneConfig) {
3122
+ const seen = new Set();
3123
+ const ordered = [];
3124
+ for (const taskType of (laneConfig.taskTypes || laneConfig.task_types || [])) {
3125
+ const stages = getStagesForType(taskType);
3126
+ for (const stage of stages) {
3127
+ if (!seen.has(stage)) {
3128
+ seen.add(stage);
3129
+ ordered.push(stage);
3130
+ }
3131
+ }
3132
+ }
3133
+ return ordered;
3134
+ }
3135
+
3136
+ function assignItemsToSwimlanes(items) {
3137
+ const result = new Map();
3138
+ for (const lane of SWIMLANE_CONFIG) {
3139
+ result.set(lane.key, []);
3140
+ }
3141
+ for (const item of items) {
3142
+ const lane = getSwimlaneForType(item.task_type);
3143
+ if (result.has(lane.key)) {
3144
+ result.get(lane.key).push(item);
3145
+ } else {
3146
+ result.set(lane.key, [item]);
3147
+ }
3148
+ }
3149
+ return result;
3150
+ }
3151
+
3152
+ function getColumnMeta(stageId) {
3153
+ const col = KANBAN_COLUMNS.find(c => c.id === stageId);
3154
+ return col
3155
+ ? { label: col.label, color: col.color, tooltip: col.tooltip, status: col.status }
3156
+ : { label: stageId, color: '#6b7280', tooltip: '', status: 'waiting' };
3157
+ }
3158
+
3159
+ function getItemColumn(item, laneColumns) {
3160
+ if (isOverlayStatus(item.status)) {
3161
+ const effective = getEffectivePipelineColumn(item);
3162
+ return laneColumns.includes(effective) ? effective : laneColumns[0];
3163
+ }
3164
+ if (!item.status || item.status === 'New' || item.status === '') {
3165
+ return 'New';
3166
+ }
3167
+ return laneColumns.includes(item.status) ? item.status : laneColumns[0];
3168
+ }
3169
+
3170
+ function isSwimlaneCollapsed(laneKey) {
3171
+ try {
3172
+ const state = JSON.parse(localStorage.getItem('beastmode-swimlane-collapse') || '{}');
3173
+ return state[laneKey] === true;
3174
+ } catch { return false; }
3175
+ }
3176
+
3177
+ function saveSwimlaneCollapse(laneKey, collapsed) {
3178
+ try {
3179
+ const state = JSON.parse(localStorage.getItem('beastmode-swimlane-collapse') || '{}');
3180
+ state[laneKey] = collapsed;
3181
+ localStorage.setItem('beastmode-swimlane-collapse', JSON.stringify(state));
3182
+ } catch {}
3183
+ }
3184
+
2956
3185
  // ── HTML Content Renderer (for Monday.com update bodies) ──
2957
3186
 
2958
3187
  function _sanitizeHtml(raw) {
@@ -3556,6 +3785,146 @@ function CreateTaskDialog({ onClose, onCreated }) {
3556
3785
  `;
3557
3786
  }
3558
3787
 
3788
+ // ── Pipeline (Swimlane) View ──
3789
+
3790
+ function PipelineView({
3791
+ filteredItems,
3792
+ items,
3793
+ onDragStart,
3794
+ onDragEnd,
3795
+ onDragOver,
3796
+ onDragLeave,
3797
+ onDrop,
3798
+ selectedProject,
3799
+ setSelectedItem,
3800
+ deleteItem,
3801
+ columnSorts,
3802
+ cycleSort,
3803
+ sortColumnItems,
3804
+ }) {
3805
+ const [collapseKey, setCollapseKey] = useState(0);
3806
+ const [, setEpicCollapseKey] = useState(0);
3807
+
3808
+ const laneItems = assignItemsToSwimlanes(filteredItems);
3809
+
3810
+ const renderCard = (item, colStatus, isParentEpic) => html`
3811
+ <div class=${'kanban-card' + (isParentEpic ? ' epic-parent-card' : '')} key=${item.id}
3812
+ data-status=${colStatus}
3813
+ data-overlay=${overlayKey(item.status) || ''}
3814
+ draggable="true"
3815
+ onDragStart=${e => onDragStart(e, item.id)}
3816
+ onDragEnd=${onDragEnd}>
3817
+ <button class="card-delete" onClick=${(e) => { e.stopPropagation(); deleteItem(item.id); }} title="Delete">
3818
+ <${Icon} name="trash" size=${12} />
3819
+ </button>
3820
+ <div class="card-id">#${item.id}</div>
3821
+ <div class="card-title" style="cursor:pointer;" onClick=${() => setSelectedItem(item)}>${item.name || item.title}</div>
3822
+ <div class="card-footer">
3823
+ ${isOverlayStatus(item.status) && html`<span class=${'card-badge badge-overlay ' + overlayBadgeClass(item.status)} title=${overlayTooltip(item.status)}>${overlayBadgeLabel(item.status)}</span>`}
3824
+ ${!isParentEpic && item.parent_epic && html`<span class="card-badge badge-epic">epic:${item.parent_epic}</span>`}
3825
+ ${item.priority && html`<span class=${'card-badge ' + priorityBadgeClass(item.priority)}>${item.priority}</span>`}
3826
+ ${item.task_type && html`<span class="card-badge badge-type">${item.task_type}</span>`}
3827
+ ${selectedProject === 'all' && item.project_id && html`<span class="card-badge badge-project">${item.project_id}</span>`}
3828
+ <span class="card-time">${timeAgo(item.updated_at || item.created_at)}</span>
3829
+ </div>
3830
+ </div>
3831
+ `;
3832
+
3833
+ return html`
3834
+ <div class="pipeline-view" data-testid="pipeline-view">
3835
+ ${SWIMLANE_CONFIG.map(lane => {
3836
+ const columns = computeSwimlaneColumns(lane);
3837
+ const laneItemList = laneItems.get(lane.key) || [];
3838
+ const collapsed = isSwimlaneCollapsed(lane.key);
3839
+ const isEmpty = laneItemList.length === 0;
3840
+ const swimlaneClasses = 'pipeline-swimlane'
3841
+ + (collapsed ? ' pipeline-swimlane-collapsed' : '')
3842
+ + (isEmpty ? ' pipeline-swimlane-empty' : '');
3843
+ const bodyId = 'pipeline-swimlane-body-' + lane.key;
3844
+ return html`
3845
+ <div class=${swimlaneClasses}
3846
+ data-testid=${'pipeline-swimlane-' + lane.key}
3847
+ data-swimlane=${lane.key}
3848
+ key=${'swimlane-' + lane.key}
3849
+ style=${'--lane-color: ' + lane.color}>
3850
+ <div class="pipeline-swimlane-header"
3851
+ role="button"
3852
+ aria-expanded=${!collapsed}
3853
+ aria-controls=${bodyId}
3854
+ onClick=${() => { saveSwimlaneCollapse(lane.key, !collapsed); setCollapseKey(k => k + 1); }}>
3855
+ <span class="pipeline-swimlane-chevron">${collapsed ? '\u25BA' : '\u25BC'}</span>
3856
+ <span class="pipeline-swimlane-color" style=${'background: ' + lane.color}></span>
3857
+ <span class="pipeline-swimlane-label">${lane.label}</span>
3858
+ <span class=${'pipeline-swimlane-count' + (isEmpty ? ' pipeline-swimlane-count-zero' : '')}>${laneItemList.length}</span>
3859
+ </div>
3860
+ <div class="pipeline-swimlane-body" id=${bodyId}>
3861
+ ${columns.map(colId => {
3862
+ const meta = getColumnMeta(colId);
3863
+ const rawColItems = laneItemList.filter(item => getItemColumn(item, columns) === colId);
3864
+ const colItems = sortColumnItems(rawColItems, columnSorts[colId] || '');
3865
+ const sortMode = columnSorts[colId] || '';
3866
+ return html`
3867
+ <div class="pipeline-column"
3868
+ data-testid=${'pipeline-col-' + colId}
3869
+ data-column=${colId}
3870
+ key=${'pc-' + lane.key + '-' + colId}
3871
+ onDragOver=${onDragOver}
3872
+ onDragLeave=${onDragLeave}
3873
+ onDrop=${e => onDrop(e, colId)}>
3874
+ <div class="pipeline-column-header"
3875
+ style=${'--col-color: ' + meta.color}
3876
+ title=${meta.tooltip}
3877
+ onClick=${() => cycleSort(colId)}>
3878
+ <span>${meta.label}${sortMode && html`<span class="sort-indicator">${sortMode === 'priority' ? '\u25BC' : '\u25B2'}</span>`}</span>
3879
+ <span class=${'kanban-count' + (colItems.length === 0 ? ' kanban-count-zero' : '')}>${colItems.length}</span>
3880
+ </div>
3881
+ <div class="pipeline-column-items">
3882
+ ${colItems.length === 0
3883
+ ? html`<div class="kanban-drop-zone">No items</div>`
3884
+ : (() => {
3885
+ const { ungrouped, groups, sortedGroupKeys } = partitionByEpic(colItems, items);
3886
+ return html`
3887
+ ${ungrouped.map(item => renderCard(item, meta.status, false))}
3888
+ ${sortedGroupKeys.map(epicId => {
3889
+ const group = groups[epicId];
3890
+ const epCollapsed = isEpicGroupCollapsed(epicId);
3891
+ const totalCount = (group.epic ? 1 : 0) + group.children.length;
3892
+ const egBodyId = 'pipeline-epic-body-' + epicId + '-' + lane.key + '-' + colId;
3893
+ return html`
3894
+ <div class=${'epic-group' + (epCollapsed ? ' epic-group-collapsed' : '')}
3895
+ data-epic-id=${epicId}
3896
+ key=${'eg-' + epicId}>
3897
+ <div class="epic-group-header"
3898
+ role="button"
3899
+ aria-expanded=${!epCollapsed}
3900
+ aria-controls=${egBodyId}
3901
+ onClick=${() => { saveEpicGroupCollapse(epicId, !epCollapsed); setEpicCollapseKey(k => k + 1); }}>
3902
+ <span class="epic-group-chevron">${epCollapsed ? '\u25BA' : '\u25BC'}</span>
3903
+ <span class="epic-group-color" style="background: var(--accent)"></span>
3904
+ <span class="epic-group-name">${group.epicName}</span>
3905
+ <span class="epic-group-count">${totalCount}</span>
3906
+ </div>
3907
+ <div class="epic-group-body" id=${egBodyId}>
3908
+ ${group.epic && renderCard(group.epic, meta.status, true)}
3909
+ ${group.children.map(item => renderCard(item, meta.status, false))}
3910
+ </div>
3911
+ </div>
3912
+ `;
3913
+ })}
3914
+ `;
3915
+ })()}
3916
+ </div>
3917
+ </div>
3918
+ `;
3919
+ })}
3920
+ </div>
3921
+ </div>
3922
+ `;
3923
+ })}
3924
+ </div>
3925
+ `;
3926
+ }
3927
+
3559
3928
  // ── Board Page ──
3560
3929
 
3561
3930
  function BoardPage({ selectedProject }) {
@@ -3570,6 +3939,13 @@ function BoardPage({ selectedProject }) {
3570
3939
  const [filters, setFilters] = useState({ priority: '', taskType: '', project: '', dateRange: '', parentEpic: '' });
3571
3940
  const [columnSorts, setColumnSorts] = useState({});
3572
3941
  const [epicCollapseKey, setEpicCollapseKey] = useState(0);
3942
+ const viewMode = (() => {
3943
+ try {
3944
+ const params = new URLSearchParams(window.location.search);
3945
+ if (params.get('view') === 'pipeline') return 'pipeline';
3946
+ } catch {}
3947
+ return 'board';
3948
+ })();
3573
3949
 
3574
3950
  const fetchItems = useCallback(() => {
3575
3951
  setLoading(true);
@@ -3911,6 +4287,23 @@ function BoardPage({ selectedProject }) {
3911
4287
  </div>
3912
4288
  `}
3913
4289
 
4290
+ ${viewMode === 'pipeline'
4291
+ ? html`<${PipelineView}
4292
+ filteredItems=${filteredItems}
4293
+ items=${items}
4294
+ onDragStart=${onDragStart}
4295
+ onDragEnd=${onDragEnd}
4296
+ onDragOver=${onDragOver}
4297
+ onDragLeave=${onDragLeave}
4298
+ onDrop=${onDrop}
4299
+ selectedProject=${selectedProject}
4300
+ setSelectedItem=${setSelectedItem}
4301
+ deleteItem=${deleteItem}
4302
+ columnSorts=${columnSorts}
4303
+ cycleSort=${cycleSort}
4304
+ sortColumnItems=${sortColumnItems}
4305
+ />`
4306
+ : html`
3914
4307
  <div class="kanban-wrapper">
3915
4308
  <div class="kanban-scroll-bar">
3916
4309
  <button class="kanban-scroll-btn" onClick=${() => {
@@ -4009,6 +4402,7 @@ function BoardPage({ selectedProject }) {
4009
4402
  })}
4010
4403
  </div>
4011
4404
  </div>
4405
+ `}
4012
4406
 
4013
4407
  ${showCreateDialog && html`<${CreateTaskDialog} onClose=${() => setShowCreateDialog(false)} onCreated=${fetchItems} />`}
4014
4408
  ${selectedItem && html`<${ItemDetailSidebar} item=${selectedItem} onClose=${() => setSelectedItem(null)} onStatusChange=${() => { fetchItems(); setSelectedItem(null); }} />`}
@@ -6660,6 +7054,19 @@ function App() {
6660
7054
  setSelectedProjectRaw(value);
6661
7055
  }, []);
6662
7056
 
7057
+ // If ?view=pipeline or ?view=board is in the URL but no hash route is
7058
+ // set, the router would default to Dashboard. Redirect to #/board so
7059
+ // the board page renders (and BoardPage can read the ?view= param).
7060
+ useEffect(() => {
7061
+ try {
7062
+ const params = new URLSearchParams(window.location.search);
7063
+ const view = params.get('view');
7064
+ if ((view === 'pipeline' || view === 'board') && (!location.hash || location.hash === '#/' || location.hash === '#')) {
7065
+ navigate('#/board');
7066
+ }
7067
+ } catch (_) {}
7068
+ }, []);
7069
+
6663
7070
  const boardData = window.__BOARD_DATA__ || {};
6664
7071
  const factoryName = boardData.factoryName || 'BeastMode Factory';
6665
7072
 
@@ -1 +1 @@
1
- 20260418-002742-2ab080a
1
+ 20260418-075248-39e5267
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@beastmode-develeap/beastmode",
3
- "version": "0.1.107",
3
+ "version": "0.1.109",
4
4
  "description": "BeastMode Dark Factory — turn intent into verified software",
5
5
  "type": "module",
6
6
  "bin": {