feat: discipline buildpulse today commands
This commit is contained in:
+1
-1
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "buildpulse",
|
"name": "buildpulse",
|
||||||
"private": true,
|
"private": true,
|
||||||
"version": "0.4.2",
|
"version": "0.4.3",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"api": "node --env-file=../.env server/index.mjs",
|
"api": "node --env-file=../.env server/index.mjs",
|
||||||
|
|||||||
+117
-22
@@ -148,6 +148,7 @@ function App() {
|
|||||||
const [showManualFeatureEditor, setShowManualFeatureEditor] = useState(false)
|
const [showManualFeatureEditor, setShowManualFeatureEditor] = useState(false)
|
||||||
const [showManualParkingEditor, setShowManualParkingEditor] = useState(false)
|
const [showManualParkingEditor, setShowManualParkingEditor] = useState(false)
|
||||||
const [showManualPulseEditor, setShowManualPulseEditor] = useState(false)
|
const [showManualPulseEditor, setShowManualPulseEditor] = useState(false)
|
||||||
|
const [todayCommandDrawerOpen, setTodayCommandDrawerOpen] = useState(false)
|
||||||
const hasHydratedRemote = useRef(false)
|
const hasHydratedRemote = useRef(false)
|
||||||
const initialLocalStateRef = useRef(appState)
|
const initialLocalStateRef = useRef(appState)
|
||||||
const triageStatusRef = useRef<HTMLDivElement | null>(null)
|
const triageStatusRef = useRef<HTMLDivElement | null>(null)
|
||||||
@@ -1531,7 +1532,7 @@ function App() {
|
|||||||
<div className="app-shell">
|
<div className="app-shell">
|
||||||
<header className="mobile-shell-header card">
|
<header className="mobile-shell-header card">
|
||||||
<div>
|
<div>
|
||||||
<p className="eyebrow">BuildPulse v0.4.2</p>
|
<p className="eyebrow">BuildPulse v0.4.3</p>
|
||||||
<h1>BuildPulse</h1>
|
<h1>BuildPulse</h1>
|
||||||
<p className="hero-goal compact-goal">
|
<p className="hero-goal compact-goal">
|
||||||
<strong>Current goal:</strong> {appState.project.current_goal || 'Classify new ideas before they become work.'}
|
<strong>Current goal:</strong> {appState.project.current_goal || 'Classify new ideas before they become work.'}
|
||||||
@@ -2429,15 +2430,122 @@ function App() {
|
|||||||
<h2>Capture → Brief → Build</h2>
|
<h2>Capture → Brief → Build</h2>
|
||||||
<p>One decision at a time. Capture the idea, pick the next move, hand it to an agent.</p>
|
<p>One decision at a time. Capture the idea, pick the next move, hand it to an agent.</p>
|
||||||
</div>
|
</div>
|
||||||
<div className="button-inline-row">
|
</div>
|
||||||
<button type="button" className="primary-triage-button" onClick={() => openTriage()}>
|
|
||||||
+ Add Idea
|
<div className="today-command-bar" aria-label="Today primary workflow">
|
||||||
|
<button type="button" className="primary-triage-button today-primary-command" onClick={() => openTriage()}>
|
||||||
|
Capture new idea
|
||||||
</button>
|
</button>
|
||||||
<button type="button" className="ghost" onClick={() => setShowManualFeatureEditor((current) => !current)}>
|
<button
|
||||||
Manual
|
type="button"
|
||||||
|
className="ghost today-drawer-toggle"
|
||||||
|
aria-expanded={todayCommandDrawerOpen}
|
||||||
|
aria-controls="today-command-drawer"
|
||||||
|
onClick={() => setTodayCommandDrawerOpen((current) => !current)}
|
||||||
|
>
|
||||||
|
{todayCommandDrawerOpen ? 'Hide commands' : 'Commands'}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{todayCommandDrawerOpen && (
|
||||||
|
<div id="today-command-drawer" className="today-command-drawer" aria-label="Secondary Today commands">
|
||||||
|
<div>
|
||||||
|
<p className="eyebrow">Build</p>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="drawer-command primary"
|
||||||
|
disabled={!focusFeature}
|
||||||
|
onClick={() => {
|
||||||
|
if (!focusFeature) return
|
||||||
|
setTodayCommandDrawerOpen(false)
|
||||||
|
openHandoffPreview(focusFeature.id, 'OpenClaw')
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Start AI session
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="drawer-command"
|
||||||
|
disabled={!focusFeature}
|
||||||
|
onClick={() => {
|
||||||
|
if (!focusFeature) return
|
||||||
|
setTodayCommandDrawerOpen(false)
|
||||||
|
openFeatureResultCapture(focusFeature.id)
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Record result
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="drawer-command"
|
||||||
|
disabled={!focusFeature}
|
||||||
|
onClick={() => {
|
||||||
|
if (!focusFeature) return
|
||||||
|
setTodayCommandDrawerOpen(false)
|
||||||
|
beginFeatureEdit(focusFeature)
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Focus details
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<p className="eyebrow">Sort</p>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="drawer-command"
|
||||||
|
onClick={() => {
|
||||||
|
setTodayCommandDrawerOpen(false)
|
||||||
|
setShowManualFeatureEditor((current) => !current)
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Manual feature
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="drawer-command"
|
||||||
|
onClick={() => {
|
||||||
|
setTodayCommandDrawerOpen(false)
|
||||||
|
setActiveTab('parking-lot')
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Parked ideas
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="drawer-command"
|
||||||
|
onClick={() => {
|
||||||
|
setTodayCommandDrawerOpen(false)
|
||||||
|
setActiveTab('pulse-log')
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Pulse history
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<p className="eyebrow">Ship</p>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="drawer-command"
|
||||||
|
onClick={() => {
|
||||||
|
setTodayCommandDrawerOpen(false)
|
||||||
|
setActiveTab('roadmap')
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Release view
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="drawer-command"
|
||||||
|
onClick={() => {
|
||||||
|
setTodayCommandDrawerOpen(false)
|
||||||
|
setActiveTab('export')
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Session handoff
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
<div className="today-grid">
|
<div className="today-grid">
|
||||||
<article className="focus-panel today-focus-card">
|
<article className="focus-panel today-focus-card">
|
||||||
@@ -2451,17 +2559,7 @@ function App() {
|
|||||||
<span className="pill">{focusFeature.status}</span>
|
<span className="pill">{focusFeature.status}</span>
|
||||||
<span className="pill">{columnLabels[focusFeature.column]}</span>
|
<span className="pill">{columnLabels[focusFeature.column]}</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="button-inline-row sticky-action-row">
|
<p className="today-card-note">Use Commands for build, result, and detail actions.</p>
|
||||||
<button type="button" className="primary small" onClick={() => openHandoffPreview(focusFeature.id, 'OpenClaw')}>
|
|
||||||
Start AI Session
|
|
||||||
</button>
|
|
||||||
<button type="button" className="ghost small" onClick={() => openFeatureResultCapture(focusFeature.id)}>
|
|
||||||
Record Result
|
|
||||||
</button>
|
|
||||||
<button type="button" className="ghost small" onClick={() => beginFeatureEdit(focusFeature)}>
|
|
||||||
Details
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</>
|
</>
|
||||||
) : (
|
) : (
|
||||||
<p>No focus feature yet. Add an idea and let triage decide if it deserves to become work.</p>
|
<p>No focus feature yet. Add an idea and let triage decide if it deserves to become work.</p>
|
||||||
@@ -2477,17 +2575,14 @@ function App() {
|
|||||||
<small>{selectedReleaseBlockers.length ? `${selectedReleaseBlockers.length} required item(s) still open` : 'No required blockers detected'}</small>
|
<small>{selectedReleaseBlockers.length ? `${selectedReleaseBlockers.length} required item(s) still open` : 'No required blockers detected'}</small>
|
||||||
<small>{selectedRelease?.forbidden_feature_titles.length ? `Forbidden right now: ${selectedRelease.forbidden_feature_titles.slice(0, 3).join(', ')}` : 'No forbidden-work list yet'}</small>
|
<small>{selectedRelease?.forbidden_feature_titles.length ? `Forbidden right now: ${selectedRelease.forbidden_feature_titles.slice(0, 3).join(', ')}` : 'No forbidden-work list yet'}</small>
|
||||||
</div>
|
</div>
|
||||||
<button type="button" className="ghost small" onClick={() => setActiveTab('roadmap')}>Release</button>
|
<p className="today-card-note">Release controls live in Commands.</p>
|
||||||
</article>
|
</article>
|
||||||
|
|
||||||
<article className="focus-panel today-decision-card">
|
<article className="focus-panel today-decision-card">
|
||||||
<p className="eyebrow">Needs decision</p>
|
<p className="eyebrow">Needs decision</p>
|
||||||
<h3>{duplicateParkingGroups.length ? `${duplicateParkingGroups.length} duplicate idea group${duplicateParkingGroups.length === 1 ? '' : 's'}` : 'No obvious duplicate pile-ups'}</h3>
|
<h3>{duplicateParkingGroups.length ? `${duplicateParkingGroups.length} duplicate idea group${duplicateParkingGroups.length === 1 ? '' : 's'}` : 'No obvious duplicate pile-ups'}</h3>
|
||||||
<p>{appState.parking_lot.length} parked · {appState.pulses.length} pulses</p>
|
<p>{appState.parking_lot.length} parked · {appState.pulses.length} pulses</p>
|
||||||
<div className="button-inline-row">
|
<p className="today-card-note">Decision controls live in Commands.</p>
|
||||||
<button type="button" className="ghost small" onClick={() => setActiveTab('parking-lot')}>Ideas</button>
|
|
||||||
<button type="button" className="ghost small" onClick={() => setActiveTab('pulse-log')}>History</button>
|
|
||||||
</div>
|
|
||||||
</article>
|
</article>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|||||||
@@ -2722,3 +2722,81 @@ select {
|
|||||||
line-height: 1.12;
|
line-height: 1.12;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* v0.4.3 — interaction discipline: one obvious Today path, secondary commands in a drawer. */
|
||||||
|
.today-command-bar {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: minmax(0, 1fr) auto;
|
||||||
|
gap: 0.7rem;
|
||||||
|
align-items: stretch;
|
||||||
|
margin: 1rem 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.today-primary-command {
|
||||||
|
min-height: 3rem;
|
||||||
|
justify-content: center;
|
||||||
|
font-size: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.today-drawer-toggle {
|
||||||
|
min-width: 9rem;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.today-command-drawer {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||||
|
gap: 0.75rem;
|
||||||
|
margin: -0.2rem 0 1rem;
|
||||||
|
padding: 0.75rem;
|
||||||
|
border: 1px solid rgba(125, 211, 252, 0.18);
|
||||||
|
border-radius: 12px;
|
||||||
|
background: rgba(3, 7, 18, 0.72);
|
||||||
|
}
|
||||||
|
|
||||||
|
.drawer-command {
|
||||||
|
width: 100%;
|
||||||
|
justify-content: flex-start;
|
||||||
|
margin-top: 0.45rem;
|
||||||
|
border-color: rgba(148, 163, 184, 0.16);
|
||||||
|
background: rgba(148, 163, 184, 0.06);
|
||||||
|
}
|
||||||
|
|
||||||
|
.drawer-command.primary {
|
||||||
|
justify-content: center;
|
||||||
|
border-color: rgba(125, 211, 252, 0.5);
|
||||||
|
background: linear-gradient(135deg, rgba(14, 165, 233, 0.92), rgba(59, 130, 246, 0.92));
|
||||||
|
color: #f8fbff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.drawer-command:disabled {
|
||||||
|
cursor: not-allowed;
|
||||||
|
opacity: 0.42;
|
||||||
|
}
|
||||||
|
|
||||||
|
.today-card-note {
|
||||||
|
margin-top: 0.75rem;
|
||||||
|
color: #8fa0bb;
|
||||||
|
font-size: 0.82rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 760px) {
|
||||||
|
.today-command-bar {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
gap: 0.45rem;
|
||||||
|
margin: 0.75rem 0 0.55rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.today-drawer-toggle {
|
||||||
|
width: 100%;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.today-command-drawer {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
gap: 0.55rem;
|
||||||
|
margin: 0 0 0.45rem;
|
||||||
|
padding: 0.6rem;
|
||||||
|
border-radius: 9px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user