@beastmode-develeap/beastmode 0.1.213 → 0.1.215

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-231045-f21d1ff";</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);
@@ -3474,6 +3563,167 @@ input[type="range"]::-webkit-slider-thumb {
3474
3563
  }
3475
3564
  .btn-secondary:hover { background: var(--surface-elevated); border-color: var(--text-muted); }
3476
3565
 
3566
+ /* ================================================================
3567
+ ONBOARDING TOUR & CHECKLIST
3568
+ ================================================================ */
3569
+
3570
+ .tour-backdrop {
3571
+ position: fixed;
3572
+ inset: 0;
3573
+ background: rgba(0, 0, 0, 0.6);
3574
+ z-index: 9000;
3575
+ animation: fadeIn 0.2s ease;
3576
+ }
3577
+ .tour-spotlight {
3578
+ position: fixed;
3579
+ z-index: 9001;
3580
+ border-radius: 8px;
3581
+ box-shadow: 0 0 0 9999px rgba(0, 0, 0, 0.6);
3582
+ pointer-events: none;
3583
+ transition: top 0.3s ease, left 0.3s ease, width 0.3s ease, height 0.3s ease;
3584
+ }
3585
+ .tour-tooltip {
3586
+ position: fixed;
3587
+ z-index: 9002;
3588
+ background: var(--bg-card);
3589
+ border: 1px solid var(--border);
3590
+ border-radius: 12px;
3591
+ padding: 24px;
3592
+ max-width: 380px;
3593
+ width: 90vw;
3594
+ box-shadow: 0 8px 32px rgba(0, 0, 0, 0.4);
3595
+ animation: slideUp 0.25s ease;
3596
+ }
3597
+ .tour-tooltip h3 {
3598
+ font-size: 18px;
3599
+ font-weight: 600;
3600
+ color: var(--text);
3601
+ margin: 0 0 8px 0;
3602
+ }
3603
+ .tour-tooltip p {
3604
+ font-size: 14px;
3605
+ color: var(--text-secondary);
3606
+ line-height: 1.5;
3607
+ margin: 0 0 20px 0;
3608
+ }
3609
+ .tour-step-indicator {
3610
+ display: flex;
3611
+ gap: 6px;
3612
+ margin-bottom: 16px;
3613
+ }
3614
+ .tour-step-dot {
3615
+ width: 8px;
3616
+ height: 8px;
3617
+ border-radius: 50%;
3618
+ background: var(--border);
3619
+ }
3620
+ .tour-step-dot.active {
3621
+ background: var(--accent);
3622
+ }
3623
+ .tour-actions {
3624
+ display: flex;
3625
+ justify-content: space-between;
3626
+ align-items: center;
3627
+ }
3628
+ .tour-actions .tour-skip {
3629
+ background: none;
3630
+ border: none;
3631
+ color: var(--text-muted);
3632
+ cursor: pointer;
3633
+ font-size: 13px;
3634
+ font-family: var(--font-sans);
3635
+ text-decoration: underline;
3636
+ padding: 0;
3637
+ }
3638
+ .tour-actions .tour-skip:hover { color: var(--text); }
3639
+ .tour-actions-right { display: flex; gap: 8px; }
3640
+
3641
+ .onboarding-checklist {
3642
+ background: var(--bg-card);
3643
+ border: 1px solid var(--border);
3644
+ border-radius: 12px;
3645
+ padding: 20px;
3646
+ margin-bottom: 16px;
3647
+ }
3648
+ .onboarding-checklist h3 {
3649
+ font-size: 15px;
3650
+ font-weight: 600;
3651
+ color: var(--text);
3652
+ margin: 0 0 4px 0;
3653
+ }
3654
+ .onboarding-checklist .subtitle {
3655
+ font-size: 13px;
3656
+ color: var(--text-muted);
3657
+ margin: 0 0 16px 0;
3658
+ }
3659
+ .checklist-item {
3660
+ display: flex;
3661
+ align-items: center;
3662
+ gap: 10px;
3663
+ padding: 8px 0;
3664
+ font-size: 14px;
3665
+ color: var(--text-secondary);
3666
+ }
3667
+ .checklist-item.completed {
3668
+ color: var(--text-muted);
3669
+ text-decoration: line-through;
3670
+ }
3671
+ .checklist-check {
3672
+ width: 20px;
3673
+ height: 20px;
3674
+ border-radius: 50%;
3675
+ border: 2px solid var(--border);
3676
+ display: flex;
3677
+ align-items: center;
3678
+ justify-content: center;
3679
+ flex-shrink: 0;
3680
+ font-size: 12px;
3681
+ line-height: 1;
3682
+ }
3683
+ .checklist-check.done {
3684
+ background: var(--success);
3685
+ border-color: var(--success);
3686
+ color: #fff;
3687
+ }
3688
+ .onboarding-progress {
3689
+ height: 4px;
3690
+ background: var(--border);
3691
+ border-radius: 2px;
3692
+ margin: 16px 0 12px;
3693
+ overflow: hidden;
3694
+ }
3695
+ .onboarding-progress-fill {
3696
+ height: 100%;
3697
+ background: var(--accent);
3698
+ border-radius: 2px;
3699
+ transition: width 0.4s ease;
3700
+ }
3701
+ .onboarding-tour-btn {
3702
+ width: 100%;
3703
+ margin-top: 12px;
3704
+ }
3705
+ .onboarding-dismiss {
3706
+ display: block;
3707
+ text-align: center;
3708
+ margin-top: 8px;
3709
+ background: none;
3710
+ border: none;
3711
+ color: var(--text-muted);
3712
+ font-size: 12px;
3713
+ cursor: pointer;
3714
+ font-family: var(--font-sans);
3715
+ width: 100%;
3716
+ padding: 4px 0;
3717
+ }
3718
+ .onboarding-dismiss:hover { color: var(--text); }
3719
+
3720
+ @media (max-width: 900px) {
3721
+ .tour-tooltip {
3722
+ max-width: 90vw;
3723
+ padding: 20px;
3724
+ }
3725
+ }
3726
+
3477
3727
  /* ================================================================
3478
3728
  COSTS PAGE
3479
3729
  ================================================================ */
@@ -3852,6 +4102,7 @@ function Icon({ name, size = 18, className = '' }) {
3852
4102
  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
4103
  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
4104
  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"/>`,
4105
+ 'arrow-up-down': html`<path d="M4 6l4-4 4 4"/><path d="M4 10l4 4 4-4"/>`,
3855
4106
  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
4107
  };
3857
4108
 
@@ -3953,7 +4204,7 @@ function Sidebar({ currentHash, factoryName, theme, onToggleTheme, selectedProje
3953
4204
  ];
3954
4205
 
3955
4206
  return html`
3956
- <aside class=${'sidebar' + (mobileOpen ? ' open' : '')}>
4207
+ <aside class=${'sidebar' + (mobileOpen ? ' open' : '')} data-tour="sidebar">
3957
4208
  <div class="sidebar-brand">
3958
4209
  <svg class="brand-logo-svg" viewBox="0 0 400 320" xmlns="http://www.w3.org/2000/svg" role="img" aria-label="BeastMode — Dark Factory">
3959
4210
  <g transform="translate(200,130)">
@@ -4003,7 +4254,8 @@ function Sidebar({ currentHash, factoryName, theme, onToggleTheme, selectedProje
4003
4254
  ${links.map(link => html`
4004
4255
  <li key=${link.path}>
4005
4256
  <a href=${link.path}
4006
- class=${currentHash === link.path || (link.path !== '#/' && currentHash.startsWith(link.path)) ? 'active' : ''}>
4257
+ class=${currentHash === link.path || (link.path !== '#/' && currentHash.startsWith(link.path)) ? 'active' : ''}
4258
+ data-tour=${link.path === '#/help' ? 'help-link' : null}>
4007
4259
  <${Icon} name=${link.icon} />
4008
4260
  ${link.label}
4009
4261
  </a>
@@ -4044,11 +4296,15 @@ function statusBadgeClass(status) {
4044
4296
  function DashboardPage({ selectedProject, onProjectChange }) {
4045
4297
  const [status, setStatus] = useState(null);
4046
4298
  const [recentItems, setRecentItems] = useState([]);
4299
+ const [allItems, setAllItems] = useState([]);
4047
4300
  const [runs, setRuns] = useState([]);
4048
4301
  const [projects, setProjects] = useState([]);
4049
4302
  const [runnerData, setRunnerData] = useState({ count: 0, offline: 0 });
4050
4303
  const [loading, setLoading] = useState(true);
4051
4304
  const [error, setError] = useState(null);
4305
+ const [onboardingDismissed, setOnboardingDismissed] = useState(() => {
4306
+ try { return !!localStorage.getItem('beastmode-onboarding-dismissed'); } catch (_) { return false; }
4307
+ });
4052
4308
 
4053
4309
  useEffect(() => {
4054
4310
  function fetchDashboard() {
@@ -4059,7 +4315,10 @@ function DashboardPage({ selectedProject, onProjectChange }) {
4059
4315
  api('GET', '/api/projects').catch(() => ({ projects: [] })),
4060
4316
  ]).then(([s, boardData, runsData, projectsData]) => {
4061
4317
  setStatus(s);
4062
- const sorted = (boardData.items || [])
4318
+ const allBoardItems = boardData.items || [];
4319
+ setAllItems(allBoardItems);
4320
+ const sorted = allBoardItems
4321
+ .slice()
4063
4322
  .sort((a, b) => new Date(normalizeDt(b.updated_at || b.created_at) || 0) - new Date(normalizeDt(a.updated_at || a.created_at) || 0))
4064
4323
  .slice(0, 10);
4065
4324
  setRecentItems(sorted);
@@ -4246,6 +4505,48 @@ function DashboardPage({ selectedProject, onProjectChange }) {
4246
4505
  </div>
4247
4506
 
4248
4507
  <div class="page-dashboard-sidebar">
4508
+ ${!onboardingDismissed && (() => {
4509
+ const hasProject = projects.length >= 1;
4510
+ const hasItem = allItems.length >= 1;
4511
+ const hasDone = allItems.some(i => (i.status || '').toLowerCase() === 'done');
4512
+ const checks = [
4513
+ { label: 'Add a project', done: hasProject },
4514
+ { label: 'Create your first task', done: hasItem },
4515
+ { label: 'Complete a task', done: hasDone },
4516
+ ];
4517
+ const completedCount = checks.filter(c => c.done).length;
4518
+ const allDone = completedCount === checks.length;
4519
+ const dismiss = () => {
4520
+ try { localStorage.setItem('beastmode-onboarding-dismissed', 'true'); } catch (_) {}
4521
+ setOnboardingDismissed(true);
4522
+ };
4523
+ return html`
4524
+ <div class="onboarding-checklist" data-testid="onboarding-checklist">
4525
+ <h3>Getting Started</h3>
4526
+ <p class="subtitle">Complete these steps to get up and running</p>
4527
+ ${allDone ? html`
4528
+ <p style="font-size:14px;color:var(--success);font-weight:600;margin:8px 0 0;" data-testid="onboarding-congrats">
4529
+ You're all set! BeastMode is ready to build.
4530
+ </p>
4531
+ ` : html`
4532
+ <div class="onboarding-progress">
4533
+ <div class="onboarding-progress-fill" style=${'width:' + ((completedCount / checks.length) * 100) + '%'}></div>
4534
+ </div>
4535
+ ${checks.map((c, i) => html`
4536
+ <div key=${i} class=${'checklist-item' + (c.done ? ' completed' : '')} data-testid=${'checklist-item-' + i}>
4537
+ <div class=${'checklist-check' + (c.done ? ' done' : '')}>${c.done ? '✓' : ''}</div>
4538
+ <span>${c.label}</span>
4539
+ </div>
4540
+ `)}
4541
+ <button class="btn btn-primary btn-pill onboarding-tour-btn" data-testid="onboarding-tour-btn"
4542
+ onClick=${() => { if (typeof window.__bmStartTour === 'function') window.__bmStartTour(); }}>
4543
+ Take the guided tour
4544
+ </button>
4545
+ `}
4546
+ <button class="onboarding-dismiss" data-testid="onboarding-dismiss" onClick=${dismiss}>Dismiss</button>
4547
+ </div>
4548
+ `;
4549
+ })()}
4249
4550
  <div class="card">
4250
4551
  <div class="card-header">
4251
4552
  <h3>Recent Activity</h3>
@@ -5895,8 +6196,7 @@ function PipelineView({
5895
6196
  selectedProject,
5896
6197
  setSelectedItem,
5897
6198
  deleteItem,
5898
- columnSorts,
5899
- cycleSort,
6199
+ globalSort,
5900
6200
  sortColumnItems,
5901
6201
  costsByItem,
5902
6202
  envVerifyByItem,
@@ -5995,8 +6295,7 @@ function PipelineView({
5995
6295
  ${columns.map(colId => {
5996
6296
  const meta = getColumnMeta(colId);
5997
6297
  const rawColItems = laneItemList.filter(item => getItemColumn(item, columns) === colId);
5998
- const colItems = sortColumnItems(rawColItems, columnSorts[colId] || '');
5999
- const sortMode = columnSorts[colId] || '';
6298
+ const colItems = sortColumnItems(rawColItems, globalSort);
6000
6299
  return html`
6001
6300
  <div class="pipeline-column"
6002
6301
  data-testid=${'pipeline-col-' + colId}
@@ -6007,9 +6306,8 @@ function PipelineView({
6007
6306
  onDrop=${e => onDrop(e, colId)}>
6008
6307
  <div class="pipeline-column-header"
6009
6308
  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>
6309
+ title=${meta.tooltip}>
6310
+ <span>${meta.label}</span>
6013
6311
  <span class=${'kanban-count' + (colItems.length === 0 ? ' kanban-count-zero' : '')}>${colItems.length}</span>
6014
6312
  </div>
6015
6313
  <div class="pipeline-column-items">
@@ -6260,7 +6558,14 @@ function BoardPage({ selectedProject }) {
6260
6558
  const allKeys = ((window.PIPELINE_CONFIG && window.PIPELINE_CONFIG.swimlanes) || []).map(s => s.key);
6261
6559
  return new Set(allKeys);
6262
6560
  });
6263
- const [columnSorts, setColumnSorts] = useState({});
6561
+ const [globalSort, setGlobalSort] = useState(() => {
6562
+ try {
6563
+ const saved = localStorage.getItem('beastmode-sort-mode');
6564
+ if (saved) return JSON.parse(saved);
6565
+ } catch {}
6566
+ return { field: '', direction: 'asc' };
6567
+ });
6568
+ const [sortOpen, setSortOpen] = useState(false);
6264
6569
  const [epicCollapseKey, setEpicCollapseKey] = useState(0);
6265
6570
  const [costsByItem, setCostsByItem] = useState({});
6266
6571
  const [envVerifyByItem, setEnvVerifyByItem] = useState({});
@@ -6699,6 +7004,25 @@ function BoardPage({ selectedProject }) {
6699
7004
  } catch {}
6700
7005
  }, [activeSwimlanesSet]);
6701
7006
 
7007
+ // Persist sort preference to localStorage whenever it changes.
7008
+ useEffect(() => {
7009
+ localStorage.setItem('beastmode-sort-mode', JSON.stringify(globalSort));
7010
+ }, [globalSort]);
7011
+
7012
+ // Close sort dropdown when clicking outside.
7013
+ useEffect(() => {
7014
+ if (!sortOpen) return;
7015
+ const handler = (e) => {
7016
+ const dropdown = document.querySelector('.sort-dropdown');
7017
+ const toggle = document.querySelector('[data-testid="sort-toggle"]');
7018
+ if (dropdown && !dropdown.contains(e.target) && toggle && !toggle.contains(e.target)) {
7019
+ setSortOpen(false);
7020
+ }
7021
+ };
7022
+ document.addEventListener('click', handler, true);
7023
+ return () => document.removeEventListener('click', handler, true);
7024
+ }, [sortOpen]);
7025
+
6702
7026
  // Expose swimlane state for external scenario verification.
6703
7027
  useEffect(() => {
6704
7028
  const swimlanes = (window.PIPELINE_CONFIG && window.PIPELINE_CONFIG.swimlanes) || [];
@@ -6780,28 +7104,32 @@ function BoardPage({ selectedProject }) {
6780
7104
  const activeFilterCount = _baseFilterCount + (_swimlaneFilterActive ? 1 : 0);
6781
7105
 
6782
7106
  // ── Column sorting ──
7107
+ const SORT_LABELS = {
7108
+ priority: 'Priority',
7109
+ name: 'Name',
7110
+ created: 'Created',
7111
+ updated: 'Updated',
7112
+ status_age: 'Time in Status',
7113
+ };
6783
7114
  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)
7115
+ const sortColumnItems = (colItems, sort) => {
7116
+ if (!sort || !sort.field) return colItems;
7117
+ const dir = sort.direction === 'desc' ? -1 : 1;
6786
7118
  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 || ''));
7119
+ if (sort.field === 'priority') {
7120
+ sorted.sort((a, b) => dir * ((PRIORITY_ORDER[a.priority || ''] ?? 4) - (PRIORITY_ORDER[b.priority || ''] ?? 4)));
7121
+ } else if (sort.field === 'name') {
7122
+ sorted.sort((a, b) => dir * (a.name || a.title || '').localeCompare(b.name || b.title || ''));
7123
+ } else if (sort.field === 'created') {
7124
+ sorted.sort((a, b) => dir * ((a.created_at || '').localeCompare(b.created_at || '')));
7125
+ } else if (sort.field === 'updated') {
7126
+ sorted.sort((a, b) => dir * ((a.updated_at || '').localeCompare(b.updated_at || '')));
7127
+ } else if (sort.field === 'status_age') {
7128
+ sorted.sort((a, b) => dir * ((a.status_changed_at || a.updated_at || '').localeCompare(b.status_changed_at || b.updated_at || '')));
6791
7129
  }
6792
7130
  return sorted;
6793
7131
  };
6794
7132
 
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
7133
  // Unique project IDs and parent epics for filter dropdowns
6806
7134
  const uniqueProjects = [...new Set(items.map(i => i.project_id).filter(Boolean))];
6807
7135
  const uniqueEpics = [...new Set(items.map(i => i.parent_epic).filter(Boolean))].map(String);
@@ -6864,7 +7192,7 @@ function BoardPage({ selectedProject }) {
6864
7192
 
6865
7193
  ${error && html`<div class="error-msg">${error}</div>`}
6866
7194
 
6867
- <div class="board-header-row">
7195
+ <div class="board-header-row" data-tour="board-header">
6868
7196
  <div class="board-stats-bar">
6869
7197
  <span class="stat-label">${viewMode === 'pipeline' ? 'Pipeline' : 'Board'}</span>
6870
7198
  <span class="stat-item"><strong>${totalItems}</strong>${isFiltered ? '/' + items.length : ''} total</span>
@@ -6887,7 +7215,47 @@ function BoardPage({ selectedProject }) {
6887
7215
  ${activeFilterCount > 0 && html`<span class="filter-active-count">${activeFilterCount}</span>`}
6888
7216
  </button>
6889
7217
 
6890
- <button class="btn btn-primary" onClick=${() => setShowCreateDialog(true)}>
7218
+ <div style="position: relative;">
7219
+ <button class=${'sort-toggle' + (sortOpen || globalSort.field ? ' active' : '')}
7220
+ onClick=${() => setSortOpen(v => !v)}
7221
+ data-testid="sort-toggle">
7222
+ <${Icon} name="arrow-up-down" size=${13} />
7223
+ Sort
7224
+ ${globalSort.field && html`<span class="sort-active-label">${SORT_LABELS[globalSort.field]}</span>`}
7225
+ </button>
7226
+ ${sortOpen && html`
7227
+ <div class="sort-dropdown" data-testid="sort-dropdown">
7228
+ <div class="sort-options">
7229
+ ${Object.entries(SORT_LABELS).map(([key, label]) => html`
7230
+ <button key=${key}
7231
+ class=${'sort-option' + (globalSort.field === key ? ' active' : '')}
7232
+ data-testid=${'sort-option-' + key}
7233
+ onClick=${() => {
7234
+ if (globalSort.field === key) {
7235
+ setGlobalSort(s => ({ ...s, direction: s.direction === 'asc' ? 'desc' : 'asc' }));
7236
+ } else {
7237
+ const defaultDir = (key === 'created' || key === 'updated') ? 'desc' : 'asc';
7238
+ setGlobalSort({ field: key, direction: defaultDir });
7239
+ }
7240
+ }}>
7241
+ <span>${label}</span>
7242
+ ${globalSort.field === key && html`
7243
+ <span class="sort-dir-icon">${globalSort.direction === 'asc' ? '↑' : '↓'}</span>
7244
+ `}
7245
+ </button>
7246
+ `)}
7247
+ </div>
7248
+ ${globalSort.field && html`
7249
+ <button class="sort-clear" data-testid="sort-clear"
7250
+ onClick=${() => { setGlobalSort({ field: '', direction: 'asc' }); setSortOpen(false); }}>
7251
+ Clear sort
7252
+ </button>
7253
+ `}
7254
+ </div>
7255
+ `}
7256
+ </div>
7257
+
7258
+ <button class="btn btn-primary" data-tour="add-item" onClick=${() => setShowCreateDialog(true)}>
6891
7259
  <${Icon} name="plus" size=${14} />
6892
7260
  New Task
6893
7261
  </button>
@@ -6984,8 +7352,7 @@ function BoardPage({ selectedProject }) {
6984
7352
  selectedProject=${selectedProject}
6985
7353
  setSelectedItem=${setSelectedItem}
6986
7354
  deleteItem=${deleteItem}
6987
- columnSorts=${columnSorts}
6988
- cycleSort=${cycleSort}
7355
+ globalSort=${globalSort}
6989
7356
  sortColumnItems=${sortColumnItems}
6990
7357
  costsByItem=${costsByItem}
6991
7358
  envVerifyByItem=${envVerifyByItem}
@@ -7020,15 +7387,14 @@ function BoardPage({ selectedProject }) {
7020
7387
  if (col.also && col.also.includes(i.status)) return true;
7021
7388
  return false;
7022
7389
  });
7023
- const colItems = sortColumnItems(rawColItems, columnSorts[col.id] || '');
7024
- const sortMode = columnSorts[col.id] || '';
7390
+ const colItems = sortColumnItems(rawColItems, globalSort);
7025
7391
  return html`
7026
7392
  <div class="kanban-column" key=${col.id}
7027
7393
  onDragOver=${e => onDragOver(e, col.id)}
7028
7394
  onDragLeave=${onDragLeave}
7029
7395
  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>
7396
+ <div class="kanban-column-header" style=${'--col-color: ' + col.color} title=${col.tooltip}>
7397
+ <span>${col.label}</span>
7032
7398
  <span class=${'kanban-count' + (colItems.length === 0 ? ' kanban-count-zero' : '')}>${colItems.length}</span>
7033
7399
  </div>
7034
7400
  <div class="kanban-items">
@@ -8877,10 +9243,33 @@ function HooksTab() {
8877
9243
  // ================================================================
8878
9244
 
8879
9245
  function HelpPage() {
9246
+ const startTour = () => {
9247
+ navigate('#/board');
9248
+ setTimeout(() => {
9249
+ if (typeof window.__bmStartTour === 'function') window.__bmStartTour();
9250
+ }, 300);
9251
+ };
8880
9252
  return html`
8881
9253
  <div>
8882
9254
  <h2 style="margin:0 0 24px;font-size:22px;font-weight:700;color:var(--text-primary);">Help & Documentation</h2>
8883
9255
 
9256
+ <div style="margin-bottom: 24px; padding: 16px; background: var(--surface-elevated);
9257
+ border: 1px solid var(--border); border-radius: 8px;
9258
+ display: flex; align-items: center; justify-content: space-between;"
9259
+ data-testid="help-tour-card">
9260
+ <div>
9261
+ <div style="font-weight: 600; color: var(--text); margin-bottom: 4px;">
9262
+ Interactive Tour
9263
+ </div>
9264
+ <div style="font-size: 13px; color: var(--text-muted);">
9265
+ Walk through the key features of BeastMode step by step.
9266
+ </div>
9267
+ </div>
9268
+ <button class="btn btn-primary" data-testid="help-start-tour" onClick=${startTour}>
9269
+ Start Tour
9270
+ </button>
9271
+ </div>
9272
+
8884
9273
  <div class="settings-section">
8885
9274
  <h3>Getting Started</h3>
8886
9275
  <div style="line-height:1.8;color:var(--text-secondary);">
@@ -10706,6 +11095,211 @@ function FactoryStatusBar() {
10706
11095
  // App Root
10707
11096
  // ================================================================
10708
11097
 
11098
+ // ================================================================
11099
+ // Onboarding Tour
11100
+ // ================================================================
11101
+
11102
+ const TOUR_STEPS = [
11103
+ {
11104
+ target: null,
11105
+ title: 'Welcome to BeastMode',
11106
+ body: "BeastMode turns your ideas into working software — describe what you want in plain language, and the pipeline writes the spec, code, and tests. Let's take a quick tour.",
11107
+ placement: 'center',
11108
+ },
11109
+ {
11110
+ target: '[data-tour="sidebar"]',
11111
+ title: 'Navigation',
11112
+ body: 'Use the sidebar to move between pages. The Board is where your tasks live. The Dashboard shows factory health and stats.',
11113
+ placement: 'right',
11114
+ },
11115
+ {
11116
+ target: '[data-tour="board-header"]',
11117
+ title: 'Your Task Board',
11118
+ body: 'Tasks flow through the pipeline from left to right: Ready → Working on it → Review → Done. BeastMode handles each stage automatically.',
11119
+ placement: 'bottom',
11120
+ },
11121
+ {
11122
+ target: '[data-tour="add-item"]',
11123
+ title: 'Create a Task',
11124
+ body: 'Click here to create a new task. Describe what you want to build — BeastMode writes the spec, plans the work, and implements it.',
11125
+ placement: 'bottom-start',
11126
+ },
11127
+ {
11128
+ target: '[data-tour="help-link"]',
11129
+ title: 'Need Help?',
11130
+ body: 'Visit the Help page anytime for guides on task types, pipeline stages, and troubleshooting tips.',
11131
+ placement: 'right',
11132
+ },
11133
+ {
11134
+ target: null,
11135
+ title: "You're Ready!",
11136
+ body: 'Start by creating your first task. BeastMode takes it from there. You can restart this tour anytime from the Help page.',
11137
+ placement: 'center',
11138
+ },
11139
+ ];
11140
+
11141
+ function OnboardingTour() {
11142
+ const [currentStep, setCurrentStep] = useState(null);
11143
+ const [targetRect, setTargetRect] = useState(null);
11144
+
11145
+ // Auto-launch on first visit.
11146
+ useEffect(() => {
11147
+ let cancelled = false;
11148
+ try {
11149
+ if (localStorage.getItem('beastmode-tour-completed')) return;
11150
+ } catch (_) { return; }
11151
+ if (!location.hash || location.hash === '#/' || location.hash === '#') {
11152
+ navigate('#/board');
11153
+ }
11154
+ const t = setTimeout(() => {
11155
+ if (!cancelled) setCurrentStep(0);
11156
+ }, 600);
11157
+ return () => { cancelled = true; clearTimeout(t); };
11158
+ }, []);
11159
+
11160
+ // Expose manual launcher globally for Dashboard checklist + Help page button.
11161
+ useEffect(() => {
11162
+ window.__bmStartTour = () => setCurrentStep(0);
11163
+ return () => { try { delete window.__bmStartTour; } catch (_) {} };
11164
+ }, []);
11165
+
11166
+ // Recalculate target rect on step change and on resize.
11167
+ useEffect(() => {
11168
+ if (currentStep === null) {
11169
+ setTargetRect(null);
11170
+ return;
11171
+ }
11172
+ const step = TOUR_STEPS[currentStep];
11173
+ function measure() {
11174
+ if (!step || !step.target) {
11175
+ setTargetRect(null);
11176
+ return;
11177
+ }
11178
+ const el = document.querySelector(step.target);
11179
+ if (!el) {
11180
+ setTargetRect(null);
11181
+ return;
11182
+ }
11183
+ const r = el.getBoundingClientRect();
11184
+ setTargetRect({ top: r.top, left: r.left, right: r.right, bottom: r.bottom, width: r.width, height: r.height });
11185
+ }
11186
+ measure();
11187
+ let timer = null;
11188
+ const onResize = () => {
11189
+ if (timer) clearTimeout(timer);
11190
+ timer = setTimeout(measure, 100);
11191
+ };
11192
+ window.addEventListener('resize', onResize);
11193
+ window.addEventListener('scroll', onResize, true);
11194
+ return () => {
11195
+ window.removeEventListener('resize', onResize);
11196
+ window.removeEventListener('scroll', onResize, true);
11197
+ if (timer) clearTimeout(timer);
11198
+ };
11199
+ }, [currentStep]);
11200
+
11201
+ const completeTour = useCallback(() => {
11202
+ try { localStorage.setItem('beastmode-tour-completed', 'true'); } catch (_) {}
11203
+ setCurrentStep(null);
11204
+ }, []);
11205
+
11206
+ // Escape to skip.
11207
+ useEffect(() => {
11208
+ if (currentStep === null) return;
11209
+ const onKey = (e) => {
11210
+ if (e.key === 'Escape') {
11211
+ e.preventDefault();
11212
+ completeTour();
11213
+ }
11214
+ };
11215
+ document.addEventListener('keydown', onKey);
11216
+ return () => document.removeEventListener('keydown', onKey);
11217
+ }, [currentStep, completeTour]);
11218
+
11219
+ if (currentStep === null) return null;
11220
+
11221
+ const step = TOUR_STEPS[currentStep];
11222
+ const isLast = currentStep === TOUR_STEPS.length - 1;
11223
+ const padding = 8;
11224
+
11225
+ // Spotlight position
11226
+ const spotlightStyle = targetRect ? {
11227
+ top: (targetRect.top - padding) + 'px',
11228
+ left: (targetRect.left - padding) + 'px',
11229
+ width: (targetRect.width + padding * 2) + 'px',
11230
+ height: (targetRect.height + padding * 2) + 'px',
11231
+ } : null;
11232
+
11233
+ // Tooltip position
11234
+ const tooltipWidth = 380;
11235
+ const tooltipApproxHeight = 220;
11236
+ const margin = 16;
11237
+ let tooltipStyle = {};
11238
+ if (!targetRect || step.placement === 'center') {
11239
+ tooltipStyle = {
11240
+ top: '50%',
11241
+ left: '50%',
11242
+ transform: 'translate(-50%, -50%)',
11243
+ };
11244
+ } else if (step.placement === 'right') {
11245
+ let top = targetRect.top + targetRect.height / 2 - tooltipApproxHeight / 2;
11246
+ let left = targetRect.right + margin;
11247
+ top = Math.max(margin, Math.min(top, window.innerHeight - tooltipApproxHeight - margin));
11248
+ left = Math.max(margin, Math.min(left, window.innerWidth - tooltipWidth - margin));
11249
+ tooltipStyle = { top: top + 'px', left: left + 'px' };
11250
+ } else if (step.placement === 'bottom') {
11251
+ let top = targetRect.bottom + margin;
11252
+ let left = targetRect.left + targetRect.width / 2 - tooltipWidth / 2;
11253
+ top = Math.max(margin, Math.min(top, window.innerHeight - tooltipApproxHeight - margin));
11254
+ left = Math.max(margin, Math.min(left, window.innerWidth - tooltipWidth - margin));
11255
+ tooltipStyle = { top: top + 'px', left: left + 'px' };
11256
+ } else if (step.placement === 'bottom-start') {
11257
+ let top = targetRect.bottom + margin;
11258
+ let left = targetRect.left;
11259
+ top = Math.max(margin, Math.min(top, window.innerHeight - tooltipApproxHeight - margin));
11260
+ left = Math.max(margin, Math.min(left, window.innerWidth - tooltipWidth - margin));
11261
+ tooltipStyle = { top: top + 'px', left: left + 'px' };
11262
+ } else {
11263
+ tooltipStyle = { top: '50%', left: '50%', transform: 'translate(-50%, -50%)' };
11264
+ }
11265
+
11266
+ const styleStr = (obj) => Object.entries(obj).map(([k, v]) => {
11267
+ const kebab = k.replace(/[A-Z]/g, m => '-' + m.toLowerCase());
11268
+ return kebab + ':' + v;
11269
+ }).join(';');
11270
+
11271
+ return html`
11272
+ <div>
11273
+ <div class="tour-backdrop" onClick=${completeTour}></div>
11274
+ ${spotlightStyle ? html`<div class="tour-spotlight" style=${styleStr(spotlightStyle)}></div>` : null}
11275
+ <div class="tour-tooltip" data-testid="tour-tooltip" style=${styleStr(tooltipStyle)} onClick=${(e) => e.stopPropagation()}>
11276
+ <div class="tour-step-indicator">
11277
+ ${TOUR_STEPS.map((_, i) => html`
11278
+ <span key=${i} class=${'tour-step-dot' + (i === currentStep ? ' active' : '')}></span>
11279
+ `)}
11280
+ </div>
11281
+ <h3>${step.title}</h3>
11282
+ <p>${step.body}</p>
11283
+ <div class="tour-actions">
11284
+ <button class="tour-skip" data-testid="tour-skip" onClick=${completeTour}>Skip Tour</button>
11285
+ <div class="tour-actions-right">
11286
+ ${currentStep > 0 && html`
11287
+ <button class="btn" data-testid="tour-back" onClick=${() => setCurrentStep(s => s - 1)}>Back</button>
11288
+ `}
11289
+ <button class="btn btn-primary" data-testid="tour-next" onClick=${() => {
11290
+ if (isLast) {
11291
+ completeTour();
11292
+ } else {
11293
+ setCurrentStep(s => s + 1);
11294
+ }
11295
+ }}>${isLast ? 'Get Started' : 'Next'}</button>
11296
+ </div>
11297
+ </div>
11298
+ </div>
11299
+ </div>
11300
+ `;
11301
+ }
11302
+
10709
11303
  function App() {
10710
11304
  const hash = useHash();
10711
11305
  const { theme, toggle: toggleTheme } = useTheme();
@@ -10787,6 +11381,7 @@ function App() {
10787
11381
  <${FactoryStatusBar} />
10788
11382
  ${page}
10789
11383
  </main>
11384
+ <${OnboardingTour} />
10790
11385
  </div>
10791
11386
  `;
10792
11387
  }
@@ -1 +1 @@
1
- bb5cfbcb14116ecf6845a1f8dd4282def11ac1a5
1
+ f21d1ff83dc59d5ca4ea41a6e71c4ede76ee5ac7
@@ -1 +1 @@
1
- 20260509-151332-bb5cfbc
1
+ 20260509-231045-f21d1ff
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.215",
4
4
  "description": "BeastMode Dark Factory — turn intent into verified software",
5
5
  "type": "module",
6
6
  "bin": {