@beastmode-develeap/beastmode 0.1.213 → 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-151332-bb5cfbc";</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">
@@ -2184,6 +2184,95 @@ input[type="range"]::-webkit-slider-thumb {
2184
2184
  .filter-toggle:hover { border-color: var(--text-muted); color: var(--text); }
2185
2185
  .filter-toggle.active { border-color: var(--accent); color: var(--accent); background: var(--accent-subtle); }
2186
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
+
2187
2276
  .view-toggle {
2188
2277
  display: inline-flex;
2189
2278
  border: 1px solid var(--border);
@@ -3852,6 +3941,7 @@ function Icon({ name, size = 18, className = '' }) {
3852
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"/>`,
3853
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"/>`,
3854
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"/>`,
3855
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"/>`,
3856
3946
  };
3857
3947
 
@@ -5895,8 +5985,7 @@ function PipelineView({
5895
5985
  selectedProject,
5896
5986
  setSelectedItem,
5897
5987
  deleteItem,
5898
- columnSorts,
5899
- cycleSort,
5988
+ globalSort,
5900
5989
  sortColumnItems,
5901
5990
  costsByItem,
5902
5991
  envVerifyByItem,
@@ -5995,8 +6084,7 @@ function PipelineView({
5995
6084
  ${columns.map(colId => {
5996
6085
  const meta = getColumnMeta(colId);
5997
6086
  const rawColItems = laneItemList.filter(item => getItemColumn(item, columns) === colId);
5998
- const colItems = sortColumnItems(rawColItems, columnSorts[colId] || '');
5999
- const sortMode = columnSorts[colId] || '';
6087
+ const colItems = sortColumnItems(rawColItems, globalSort);
6000
6088
  return html`
6001
6089
  <div class="pipeline-column"
6002
6090
  data-testid=${'pipeline-col-' + colId}
@@ -6007,9 +6095,8 @@ function PipelineView({
6007
6095
  onDrop=${e => onDrop(e, colId)}>
6008
6096
  <div class="pipeline-column-header"
6009
6097
  style=${'--col-color: ' + meta.color}
6010
- title=${meta.tooltip}
6011
- onClick=${() => cycleSort(colId)}>
6012
- <span>${meta.label}${sortMode && html`<span class="sort-indicator">${sortMode === 'priority' ? '\u25BC' : '\u25B2'}</span>`}</span>
6098
+ title=${meta.tooltip}>
6099
+ <span>${meta.label}</span>
6013
6100
  <span class=${'kanban-count' + (colItems.length === 0 ? ' kanban-count-zero' : '')}>${colItems.length}</span>
6014
6101
  </div>
6015
6102
  <div class="pipeline-column-items">
@@ -6260,7 +6347,14 @@ function BoardPage({ selectedProject }) {
6260
6347
  const allKeys = ((window.PIPELINE_CONFIG && window.PIPELINE_CONFIG.swimlanes) || []).map(s => s.key);
6261
6348
  return new Set(allKeys);
6262
6349
  });
6263
- 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);
6264
6358
  const [epicCollapseKey, setEpicCollapseKey] = useState(0);
6265
6359
  const [costsByItem, setCostsByItem] = useState({});
6266
6360
  const [envVerifyByItem, setEnvVerifyByItem] = useState({});
@@ -6699,6 +6793,25 @@ function BoardPage({ selectedProject }) {
6699
6793
  } catch {}
6700
6794
  }, [activeSwimlanesSet]);
6701
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
+
6702
6815
  // Expose swimlane state for external scenario verification.
6703
6816
  useEffect(() => {
6704
6817
  const swimlanes = (window.PIPELINE_CONFIG && window.PIPELINE_CONFIG.swimlanes) || [];
@@ -6780,28 +6893,32 @@ function BoardPage({ selectedProject }) {
6780
6893
  const activeFilterCount = _baseFilterCount + (_swimlaneFilterActive ? 1 : 0);
6781
6894
 
6782
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
+ };
6783
6903
  const PRIORITY_ORDER = { 'Critical': 0, 'High': 1, 'Medium': 2, 'Low': 3, '': 4 };
6784
- const sortColumnItems = (colItems, mode) => {
6785
- 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;
6786
6907
  const sorted = [...colItems];
6787
- if (mode === 'priority') {
6788
- sorted.sort((a, b) => (PRIORITY_ORDER[a.priority || ''] ?? 4) - (PRIORITY_ORDER[b.priority || ''] ?? 4));
6789
- } else if (mode === 'name') {
6790
- 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 || '')));
6791
6918
  }
6792
6919
  return sorted;
6793
6920
  };
6794
6921
 
6795
- const cycleSort = (colId) => {
6796
- setColumnSorts(prev => {
6797
- const current = prev[colId] || '';
6798
- const next = current === '' ? 'priority' : current === 'priority' ? 'name' : '';
6799
- const copy = { ...prev };
6800
- if (next) copy[colId] = next; else delete copy[colId];
6801
- return copy;
6802
- });
6803
- };
6804
-
6805
6922
  // Unique project IDs and parent epics for filter dropdowns
6806
6923
  const uniqueProjects = [...new Set(items.map(i => i.project_id).filter(Boolean))];
6807
6924
  const uniqueEpics = [...new Set(items.map(i => i.parent_epic).filter(Boolean))].map(String);
@@ -6887,6 +7004,46 @@ function BoardPage({ selectedProject }) {
6887
7004
  ${activeFilterCount > 0 && html`<span class="filter-active-count">${activeFilterCount}</span>`}
6888
7005
  </button>
6889
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
+
6890
7047
  <button class="btn btn-primary" onClick=${() => setShowCreateDialog(true)}>
6891
7048
  <${Icon} name="plus" size=${14} />
6892
7049
  New Task
@@ -6984,8 +7141,7 @@ function BoardPage({ selectedProject }) {
6984
7141
  selectedProject=${selectedProject}
6985
7142
  setSelectedItem=${setSelectedItem}
6986
7143
  deleteItem=${deleteItem}
6987
- columnSorts=${columnSorts}
6988
- cycleSort=${cycleSort}
7144
+ globalSort=${globalSort}
6989
7145
  sortColumnItems=${sortColumnItems}
6990
7146
  costsByItem=${costsByItem}
6991
7147
  envVerifyByItem=${envVerifyByItem}
@@ -7020,15 +7176,14 @@ function BoardPage({ selectedProject }) {
7020
7176
  if (col.also && col.also.includes(i.status)) return true;
7021
7177
  return false;
7022
7178
  });
7023
- const colItems = sortColumnItems(rawColItems, columnSorts[col.id] || '');
7024
- const sortMode = columnSorts[col.id] || '';
7179
+ const colItems = sortColumnItems(rawColItems, globalSort);
7025
7180
  return html`
7026
7181
  <div class="kanban-column" key=${col.id}
7027
7182
  onDragOver=${e => onDragOver(e, col.id)}
7028
7183
  onDragLeave=${onDragLeave}
7029
7184
  onDrop=${e => onDrop(e, col.id)}>
7030
- <div class="kanban-column-header" style=${'--col-color: ' + col.color} title=${col.tooltip} onClick=${() => cycleSort(col.id)}>
7031
- <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>
7032
7187
  <span class=${'kanban-count' + (colItems.length === 0 ? ' kanban-count-zero' : '')}>${colItems.length}</span>
7033
7188
  </div>
7034
7189
  <div class="kanban-items">
@@ -1 +1 @@
1
- bb5cfbcb14116ecf6845a1f8dd4282def11ac1a5
1
+ 09eb71470e076891f6ed9304e081cc9372bad7fc
@@ -1 +1 @@
1
- 20260509-151332-bb5cfbc
1
+ 20260509-230834-09eb714
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@beastmode-develeap/beastmode",
3
- "version": "0.1.213",
3
+ "version": "0.1.214",
4
4
  "description": "BeastMode Dark Factory — turn intent into verified software",
5
5
  "type": "module",
6
6
  "bin": {