@beastmode-develeap/beastmode 0.1.22 → 0.1.24

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.
Files changed (2) hide show
  1. package/dist/web/board.html +183 -21
  2. package/package.json +1 -1
@@ -1211,6 +1211,79 @@ input[type="range"]::-webkit-slider-thumb {
1211
1211
  .kanban-card[data-overlay="stuck"] { animation: none; }
1212
1212
  }
1213
1213
 
1214
+ /* Epic group nesting */
1215
+ .epic-group {
1216
+ margin-bottom: 8px;
1217
+ border: 1px solid var(--border);
1218
+ border-radius: 8px;
1219
+ overflow: hidden;
1220
+ background: var(--bg-input);
1221
+ }
1222
+ .epic-group-header {
1223
+ display: flex;
1224
+ align-items: center;
1225
+ gap: 8px;
1226
+ padding: 6px 10px;
1227
+ cursor: pointer;
1228
+ user-select: none;
1229
+ background: var(--bg-card);
1230
+ border-bottom: 1px solid var(--border);
1231
+ transition: background 0.15s;
1232
+ font-size: 12px;
1233
+ }
1234
+ .epic-group-header:hover {
1235
+ background: var(--surface2, rgba(255,255,255,0.05));
1236
+ }
1237
+ .epic-group-collapsed .epic-group-header {
1238
+ border-bottom: none;
1239
+ }
1240
+ .epic-group-chevron {
1241
+ font-size: 10px;
1242
+ color: var(--text-muted);
1243
+ width: 14px;
1244
+ text-align: center;
1245
+ flex-shrink: 0;
1246
+ }
1247
+ .epic-group-color {
1248
+ width: 6px;
1249
+ height: 6px;
1250
+ border-radius: 50%;
1251
+ flex-shrink: 0;
1252
+ }
1253
+ .epic-group-name {
1254
+ font-weight: 600;
1255
+ color: var(--text-secondary);
1256
+ white-space: nowrap;
1257
+ overflow: hidden;
1258
+ text-overflow: ellipsis;
1259
+ flex: 1;
1260
+ min-width: 0;
1261
+ }
1262
+ .epic-group-count {
1263
+ font-size: 10px;
1264
+ color: var(--text-muted);
1265
+ background: var(--surface2, rgba(255,255,255,0.08));
1266
+ padding: 1px 7px;
1267
+ border-radius: 8px;
1268
+ font-weight: 500;
1269
+ flex-shrink: 0;
1270
+ }
1271
+ .epic-group-body {
1272
+ padding: 4px;
1273
+ }
1274
+ .epic-group-body .kanban-card {
1275
+ margin-bottom: 4px;
1276
+ }
1277
+ .epic-group-body .kanban-card:last-child {
1278
+ margin-bottom: 0;
1279
+ }
1280
+ .epic-parent-card {
1281
+ border-left: 3px dashed var(--accent);
1282
+ }
1283
+ .epic-group-collapsed .epic-group-body {
1284
+ display: none;
1285
+ }
1286
+
1214
1287
  /* Board stats bar */
1215
1288
  .board-stats-bar {
1216
1289
  display: flex;
@@ -2734,6 +2807,67 @@ window.overlayKey = overlayKey;
2734
2807
  window.overlayTooltip = overlayTooltip;
2735
2808
  window.getEffectivePipelineColumn = getEffectivePipelineColumn;
2736
2809
 
2810
+ function isEpicGroupCollapsed(epicId) {
2811
+ try {
2812
+ const state = JSON.parse(localStorage.getItem('beastmode-epic-collapse') || '{}');
2813
+ return state[epicId] === true;
2814
+ } catch { return false; }
2815
+ }
2816
+
2817
+ function saveEpicGroupCollapse(epicId, collapsed) {
2818
+ try {
2819
+ const state = JSON.parse(localStorage.getItem('beastmode-epic-collapse') || '{}');
2820
+ state[epicId] = collapsed;
2821
+ localStorage.setItem('beastmode-epic-collapse', JSON.stringify(state));
2822
+ } catch {}
2823
+ }
2824
+
2825
+ function partitionByEpic(columnItems, allItems) {
2826
+ const ungrouped = [];
2827
+ const groups = {};
2828
+
2829
+ for (const item of columnItems) {
2830
+ const epicId = item.parent_epic;
2831
+ if (!epicId) {
2832
+ if (item.task_type === 'epic') {
2833
+ const childrenInCol = columnItems.filter(c => String(c.parent_epic) === String(item.id));
2834
+ if (childrenInCol.length > 0) {
2835
+ groups[item.id] = groups[item.id] || { epic: null, children: [] };
2836
+ groups[item.id].epic = item;
2837
+ continue;
2838
+ }
2839
+ }
2840
+ ungrouped.push(item);
2841
+ } else {
2842
+ groups[epicId] = groups[epicId] || { epic: null, children: [] };
2843
+ groups[epicId].children.push(item);
2844
+ }
2845
+ }
2846
+
2847
+ for (const epicId of Object.keys(groups)) {
2848
+ if (!groups[epicId].epic) {
2849
+ const parentItem = allItems.find(i => String(i.id) === String(epicId));
2850
+ groups[epicId].epicName = parentItem ? parentItem.name : 'Epic #' + epicId;
2851
+ groups[epicId].epicStatus = parentItem ? parentItem.status : null;
2852
+ } else {
2853
+ groups[epicId].epicName = groups[epicId].epic.name;
2854
+ groups[epicId].epicStatus = groups[epicId].epic.status;
2855
+ }
2856
+ }
2857
+
2858
+ const sortedGroupKeys = Object.keys(groups).sort((a, b) => {
2859
+ const idsA = groups[a].children.map(c => Number(c.id));
2860
+ if (groups[a].epic) idsA.push(Number(groups[a].epic.id));
2861
+ const idsB = groups[b].children.map(c => Number(c.id));
2862
+ if (groups[b].epic) idsB.push(Number(groups[b].epic.id));
2863
+ const minA = idsA.length > 0 ? Math.min(...idsA) : Infinity;
2864
+ const minB = idsB.length > 0 ? Math.min(...idsB) : Infinity;
2865
+ return minA - minB;
2866
+ });
2867
+
2868
+ return { ungrouped, groups, sortedGroupKeys };
2869
+ }
2870
+
2737
2871
  // ── HTML Content Renderer (for Monday.com update bodies) ──
2738
2872
 
2739
2873
  function _sanitizeHtml(raw) {
@@ -3289,6 +3423,7 @@ function BoardPage({ selectedProject }) {
3289
3423
  const [filtersOpen, setFiltersOpen] = useState(false);
3290
3424
  const [filters, setFilters] = useState({ priority: '', taskType: '', project: '', dateRange: '', parentEpic: '' });
3291
3425
  const [columnSorts, setColumnSorts] = useState({});
3426
+ const [epicCollapseKey, setEpicCollapseKey] = useState(0);
3292
3427
 
3293
3428
  const fetchItems = useCallback(() => {
3294
3429
  setLoading(true);
@@ -3673,28 +3808,55 @@ function BoardPage({ selectedProject }) {
3673
3808
  <div class="kanban-items">
3674
3809
  ${colItems.length === 0
3675
3810
  ? html`<div class="kanban-drop-zone">No items</div>`
3676
- : colItems.map(item => html`
3677
- <div class="kanban-card" key=${item.id}
3678
- data-status=${col.status}
3679
- data-overlay=${overlayKey(item.status) || ''}
3680
- draggable="true"
3681
- onDragStart=${e => onDragStart(e, item.id)}
3682
- onDragEnd=${onDragEnd}>
3683
- <button class="card-delete" onClick=${(e) => { e.stopPropagation(); deleteItem(item.id); }} title="Delete">
3684
- <${Icon} name="trash" size=${12} />
3685
- </button>
3686
- <div class="card-id">#${item.id}</div>
3687
- <div class="card-title" style="cursor:pointer;" onClick=${() => setSelectedItem(item)}>${item.name || item.title}</div>
3688
- <div class="card-footer">
3689
- ${isOverlayStatus(item.status) && html`<span class=${'card-badge badge-overlay ' + overlayBadgeClass(item.status)} title=${overlayTooltip(item.status)}>${overlayBadgeLabel(item.status)}</span>`}
3690
- ${item.parent_epic && html`<span class="card-badge badge-epic">epic:${item.parent_epic}</span>`}
3691
- ${item.priority && html`<span class=${'card-badge ' + priorityBadgeClass(item.priority)}>${item.priority}</span>`}
3692
- ${item.task_type && html`<span class="card-badge badge-type">${item.task_type}</span>`}
3693
- ${selectedProject === 'all' && item.project_id && html`<span class="card-badge badge-project">${item.project_id}</span>`}
3694
- <span class="card-time">${timeAgo(item.updated_at || item.created_at)}</span>
3811
+ : (() => {
3812
+ const { ungrouped, groups, sortedGroupKeys } = partitionByEpic(colItems, items);
3813
+ const renderCard = (item, isParentEpic) => html`
3814
+ <div class=${'kanban-card' + (isParentEpic ? ' epic-parent-card' : '')} key=${item.id}
3815
+ data-status=${col.status}
3816
+ data-overlay=${overlayKey(item.status) || ''}
3817
+ draggable="true"
3818
+ onDragStart=${e => onDragStart(e, item.id)}
3819
+ onDragEnd=${onDragEnd}>
3820
+ <button class="card-delete" onClick=${(e) => { e.stopPropagation(); deleteItem(item.id); }} title="Delete">
3821
+ <${Icon} name="trash" size=${12} />
3822
+ </button>
3823
+ <div class="card-id">#${item.id}</div>
3824
+ <div class="card-title" style="cursor:pointer;" onClick=${() => setSelectedItem(item)}>${item.name || item.title}</div>
3825
+ <div class="card-footer">
3826
+ ${isOverlayStatus(item.status) && html`<span class=${'card-badge badge-overlay ' + overlayBadgeClass(item.status)} title=${overlayTooltip(item.status)}>${overlayBadgeLabel(item.status)}</span>`}
3827
+ ${!isParentEpic && item.parent_epic && html`<span class="card-badge badge-epic">epic:${item.parent_epic}</span>`}
3828
+ ${item.priority && html`<span class=${'card-badge ' + priorityBadgeClass(item.priority)}>${item.priority}</span>`}
3829
+ ${item.task_type && html`<span class="card-badge badge-type">${item.task_type}</span>`}
3830
+ ${selectedProject === 'all' && item.project_id && html`<span class="card-badge badge-project">${item.project_id}</span>`}
3831
+ <span class="card-time">${timeAgo(item.updated_at || item.created_at)}</span>
3832
+ </div>
3695
3833
  </div>
3696
- </div>
3697
- `)}
3834
+ `;
3835
+ return html`
3836
+ ${ungrouped.map(item => renderCard(item, false))}
3837
+ ${sortedGroupKeys.map(epicId => {
3838
+ const group = groups[epicId];
3839
+ const collapsed = isEpicGroupCollapsed(epicId);
3840
+ const totalCount = (group.epic ? 1 : 0) + group.children.length;
3841
+ const bodyId = 'epic-group-body-' + epicId + '-' + col.id;
3842
+ return html`
3843
+ <div class=${'epic-group' + (collapsed ? ' epic-group-collapsed' : '')} data-epic-id=${epicId} key=${'eg-' + epicId}>
3844
+ <div class="epic-group-header" role="button" aria-expanded=${!collapsed} aria-controls=${bodyId}
3845
+ onClick=${() => { saveEpicGroupCollapse(epicId, !collapsed); setEpicCollapseKey(k => k + 1); }}>
3846
+ <span class="epic-group-chevron">${collapsed ? '\u25BA' : '\u25BC'}</span>
3847
+ <span class="epic-group-color" style="background: var(--accent)"></span>
3848
+ <span class="epic-group-name">${group.epicName}</span>
3849
+ <span class="epic-group-count">${totalCount}</span>
3850
+ </div>
3851
+ <div class="epic-group-body" id=${bodyId}>
3852
+ ${group.epic && renderCard(group.epic, true)}
3853
+ ${group.children.map(item => renderCard(item, false))}
3854
+ </div>
3855
+ </div>
3856
+ `;
3857
+ })}
3858
+ `;
3859
+ })()}
3698
3860
  </div>
3699
3861
  </div>
3700
3862
  `;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@beastmode-develeap/beastmode",
3
- "version": "0.1.22",
3
+ "version": "0.1.24",
4
4
  "description": "BeastMode Dark Factory — turn intent into verified software",
5
5
  "type": "module",
6
6
  "bin": {