Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
88 commits
Select commit Hold shift + click to select a range
07361cc
board: map a click in rendered markdown to its source offset
m4ttheweric Oct 28, 2026
ce53416
board: click-to-edit comment field, first pass in Storybook
m4ttheweric Oct 28, 2026
71c7126
board: tint a summary field while it edits
m4ttheweric Oct 28, 2026
367f278
board: a summary field edits on the neutral wash, not green
m4ttheweric Oct 28, 2026
33a3869
board story: stripe the summary line, more room between summary fields
m4ttheweric Oct 28, 2026
12fb177
board: summary fields hug their text; the space between them is margin
m4ttheweric Oct 28, 2026
7ffccee
board: ease the click-to-edit field's change over 150ms
m4ttheweric Oct 28, 2026
11aaafe
board: add motion 14.2.0
m4ttheweric Oct 28, 2026
eea04f3
board: crossfade read and write with motion, ease the height; roomier…
m4ttheweric Oct 28, 2026
d7e11ba
board story: diff-line findings edit in the borderless field too
m4ttheweric Oct 28, 2026
d4dfae8
board: a comment's last block drops its bottom margin
m4ttheweric Oct 28, 2026
f342592
board: even the rest-state field's padding around inline code
m4ttheweric Oct 28, 2026
3a1347a
board: balance the rest-state wash on the ink, not the line box
m4ttheweric Oct 28, 2026
28126cb
board: even the rest-state wash around the letters themselves
m4ttheweric Oct 28, 2026
52fd6bf
board: more room under a rest-state comment
m4ttheweric Oct 28, 2026
a7b9640
board: 20px under a rest-state comment
m4ttheweric Oct 28, 2026
b831a83
board: back to 8px under a rest-state comment
m4ttheweric Oct 28, 2026
6547245
board: 8px above and below a rest-state comment
m4ttheweric Oct 28, 2026
29eefae
board: review sheet comments read rendered and edit on a click
m4ttheweric Oct 28, 2026
2fca846
board story: the drafted first round shows its diffs
m4ttheweric Oct 28, 2026
7de0a86
board: a finding's comment edits in a filled well, no stripe
m4ttheweric Oct 28, 2026
47f6e69
board: the summary card holds the summary line only, in the filled field
m4ttheweric Oct 28, 2026
17138c2
board: the summary badge reads review verdict: <verdict>
m4ttheweric Oct 28, 2026
ed97f35
board: the re-review ask is a row in the respond dock
m4ttheweric Oct 28, 2026
4e8d15b
board: drop the review dock's global reset
m4ttheweric Oct 28, 2026
1c6a86f
board: code fences name their language (TypeScript, not ts)
m4ttheweric Oct 28, 2026
5cf295a
design: review gate boards B23-B34 (summary, click to edit, laptop ra…
m4ttheweric Oct 28, 2026
cc9ebcf
board: review cards are one box per finding; summary moves to the dock
m4ttheweric Oct 28, 2026
d608cda
board: a laptop-sized rail: checks fold to a tally, MR card to three …
m4ttheweric Oct 28, 2026
0dbbffc
board: review dock and context match the design
m4ttheweric Oct 28, 2026
992acbf
board: the whole card head toggles its finding; drop the finding mark
m4ttheweric Oct 28, 2026
308436e
board: a finding with no diff line wears an in-summary badge
m4ttheweric Oct 28, 2026
887c110
board: the card head hovers in the finding rows' blue wash
m4ttheweric Oct 28, 2026
774b5f1
board tests: assert the redesigned review gate (click-to-edit, dock s…
m4ttheweric Oct 28, 2026
cb3a7bf
board: a drafted comment is a plain comment box; Escape stays closed
m4ttheweric Oct 28, 2026
fca34de
board: a drafted comment is a field on a grey comment area (B38)
m4ttheweric Oct 28, 2026
ca505e8
board: soften the comment field and its hover hint
m4ttheweric Oct 28, 2026
de5bcac
board: 'Click to edit' is a pill on the comment field's border
m4ttheweric Oct 28, 2026
45185d0
board: the click-to-edit pill wears the accent wash
m4ttheweric Oct 28, 2026
ec097b9
board: the click-to-edit pill gets a pencil, a centered wash and room
m4ttheweric Oct 28, 2026
da9fe66
board: drop the comment field's hover glow; the pill carries hover
m4ttheweric Oct 28, 2026
6648da6
board: keyboard focus on a comment rings its field, not a clipped out…
m4ttheweric Oct 28, 2026
ed61028
board: closing a changed comment flashes 'Comment updated' in its pill
m4ttheweric Oct 28, 2026
2b297e1
board: a click that closes a comment doesn't also open the full diff
m4ttheweric Oct 28, 2026
05a2782
board: closing a comment no longer flashes the click-to-edit pill
m4ttheweric Oct 28, 2026
7996414
board: 'Comment updated' wears the ok wash
m4ttheweric Oct 28, 2026
2f211bc
board: the comment pill transitions all over 150ms
m4ttheweric Oct 28, 2026
1964216
board: the updated flash holds its label for the pill's 150ms fade
m4ttheweric Oct 28, 2026
fd2ab3e
board: the comment pill eases in and out with a small lift
m4ttheweric Oct 28, 2026
c645a42
board: full diff reveals on hovering the code section, not the whole …
m4ttheweric Oct 28, 2026
a47f947
board: an edited draft shows one 'Reset to draft' button, no dotted pair
m4ttheweric Oct 28, 2026
56a7d44
board: tuck the write hint closer under the comment field
m4ttheweric Oct 28, 2026
a22150a
board: a comment card eases its height on reset and edit
m4ttheweric Oct 28, 2026
acb57db
board: a comment's header says it's being edited and holds the reset
m4ttheweric Oct 28, 2026
cdcce69
board: the comment header shows only while writing
m4ttheweric Oct 28, 2026
2cef85c
board: an edited comment wears an 'Edited' pill with its reset at rest
m4ttheweric Oct 28, 2026
a7d2e91
board: polish the edited pill: split state and reset, text clears it
m4ttheweric Oct 28, 2026
e83ad8b
board: the edited pill's reset hovers by deepening, no underline or wash
m4ttheweric Oct 28, 2026
7c0c330
board: the edited pill's reset hovers with a full-half blue wash
m4ttheweric Oct 28, 2026
8754194
board: the edited pill's hairline fades while its reset is washed
m4ttheweric Oct 28, 2026
90ed1db
board: comment code blocks are a frameless grey wash with a floated l…
m4ttheweric Oct 28, 2026
735fa9e
board: highlighted comment code sits in the wash with its label clear
m4ttheweric Oct 28, 2026
9f1aef6
board: comment code wash takes 4px corners
m4ttheweric Oct 28, 2026
f5c938e
board: comment code drops the highlighter's scrollbar gutter
m4ttheweric Oct 28, 2026
c93ffc9
board: review findings cap at 960px, centred in the main column
m4ttheweric Oct 28, 2026
a67c824
board: wrapped inline code closes each piece; findings cap at 1120px
m4ttheweric Oct 28, 2026
fdd9fbf
board: review dock back to its earlier verdict choices, summary kept
m4ttheweric Oct 28, 2026
cf6bacf
board: the dock's summary reads 'Review comment'
m4ttheweric Oct 28, 2026
5d3ff7e
board: unpicked dock choices are white cards on the grey dock
m4ttheweric Oct 28, 2026
df46f6e
board story: drafted first round serves a report, so the record shows
m4ttheweric Oct 28, 2026
a4096dc
board: the review record sits in a card under a 'Review report' heading
m4ttheweric Oct 28, 2026
9a34582
board: the review main column splits into Findings and Report tabs
m4ttheweric Oct 28, 2026
1995033
board: checks move from the rail into the Report tab's card
m4ttheweric Oct 28, 2026
8feed79
board: checks stay in the rail too, beside the Report tab's row
m4ttheweric Oct 28, 2026
5b642bd
board: the rail's checks card lists every check, no fold
m4ttheweric Oct 28, 2026
48ca0a0
board: a finding with no diff line shows the agent's finding in the c…
m4ttheweric Oct 28, 2026
00ff4bc
board: tidy the no-diff finding: one border, full-strength 14px text
m4ttheweric Oct 28, 2026
5474bee
board: the no-diff finding sits on a light blue wash
m4ttheweric Oct 28, 2026
f3d84d1
board: an info mark in the code's file bar opens the agent's finding …
m4ttheweric Oct 28, 2026
723ea64
board: the finding info mark opens on click, with a pointer cursor
m4ttheweric Oct 28, 2026
db0e841
board: folding a finding's code eases its height over 150ms
m4ttheweric Oct 28, 2026
c89d01a
board: drop the findings tally's 'N more below' and its scroll tracking
m4ttheweric Oct 28, 2026
9a88e2a
board: the drafted findings tally reads 'Posting N of M'
m4ttheweric Oct 28, 2026
1756acc
board: ticking a finding eases its card open and closed
m4ttheweric Oct 28, 2026
b1f8d77
board tests: follow the review gate's final UI
m4ttheweric Oct 28, 2026
40f8da7
board: fence labels read own keys only, so __proto__ can't crash a co…
m4ttheweric Oct 28, 2026
a3b84c2
board: a tall dock scrolls inside the rail, submit stuck to its bottom
m4ttheweric Oct 28, 2026
cfca404
board tests: review-code-dom skips motion's animations under happy-dom
m4ttheweric Oct 28, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions apps/board/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
"@mattstack/tui-kit": "workspace:*",
"@pierre/diffs": "catalog:",
"invadrs": "^1.3.1",
"motion": "14.2.0",
"pngjs": "^7.0.0",
"react": "catalog:",
"react-dom": "catalog:",
Expand Down
323 changes: 323 additions & 0 deletions apps/board/src/client/board/ClickToEditField.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,323 @@
import { useState, type ReactNode } from 'react';
import type { Meta, StoryObj } from '@storybook/react-vite';

import { Chip, Markdown } from '@mattstack/tui-kit';
import { registerTheme, SoribashiProvider } from '@mattstack/tui-kit/provider';
import { tuiTheme } from '@mattstack/tui-kit/theme';

import '@mattstack/tui-kit/theme.css';
import '@mattstack/tui-kit/canvas.css';
import '../../style.css';

import { ClickToEditField } from './ClickToEditField.tsx';
import { AgentGlyph, CodeGlyph, CommentGlyph, FileGlyph } from './icons.tsx';

/**
* First build pass of the click-to-edit comment (board.pen B26 and B27):
* every comment reads rendered, a click on its text opens the raw Markdown
* with the caret at the click, and there is no Write/Preview toggle. The
* summary card stacks bare fields so it reads as the one comment the forge
* shows; a comment from a finding edits in the summary or in its own card,
* both on the same text. Invented content only (the repo is public).
*/
function Stage({
scheme,
children,
}: {
scheme: 'light' | 'dark';
children: ReactNode;
}) {
return (
<div
className={scheme === 'dark' ? 'dark' : undefined}
style={{
background: 'var(--page)',
color: 'var(--text-1)',
minHeight: '100vh',
padding: 32,
boxSizing: 'border-box',
}}
>
<SoribashiProvider theme={tuiTheme}>
<div style={{ maxWidth: 860 }}>{children}</div>
</SoribashiProvider>
</div>
);
}

const meta = {
title: 'Gates/Board/Click to edit',
decorators: [
(Story, context) => (
<Stage scheme={context.globals.scheme === 'dark' ? 'dark' : 'light'}>
<Story />
</Stage>
),
],
parameters: { layout: 'fullscreen' },
} satisfies Meta;

export default meta;

type Story = StoryObj<typeof meta>;

registerTheme(tuiTheme);

interface StoryFinding {
id: string;
title: string;
body: string;
comment: string;
file?: { path: string; line: number };
}

const FINDINGS: StoryFinding[] = [
{
id: 'd1',
title: 'retryPolicy retries a job that already failed for good',
body: '`retryPolicy` retries every rejection, including `PermanentJobError`, so a job that can never succeed burns all five attempts.',
comment:
"**issue:** this retries `PermanentJobError` too, so a job that can't succeed eats all five attempts before it fails. could we skip retries for that one?",
file: { path: 'src/jobs/retry-policy.ts', line: 34 },
},
{
id: 'd2',
title: 'The delay never backs off',
body: 'The delay is fixed at `200ms`; the spec asks for exponential backoff capped at 5s.',
comment:
'**suggestion:** the spec wants exponential backoff capped at 5s, and this is a flat `200ms`. something like `Math.min(200 * 2 ** attempt, 5000)` would cover it.',
file: { path: 'src/jobs/retry-policy.ts', line: 41 },
},
{
id: 'f1',
title: 'No run for the --changed flag',
body: 'The AC for `--changed` on files under `src/jobs/**` has no run in the evidence.',
comment:
'**suggestion:** thanks for the extra evidence, that covers it nicely. the one AC without a run is `--changed` picking up a file under `src/jobs/**`... would you mind grabbing one quick run of that?',
},
{
id: 'f2',
title: 'Stale comments and a stale stat name',
body: 'Comments in `scan-jobs.ts` still describe the old single folder, and `stats.reachableInJobs` now counts both.',
comment:
"**nitpick:** a few comments in `scan-jobs.ts` still describe the old single folder: `JobRow.path` (:88) says it's relative to `src/jobs/`. `stats.reachableInJobs` (:140) counts both folders now too, so maybe rename it?",
},
{
id: 'f3',
title: 'README is out of step with the script',
body: 'The README still names the old folder, and nothing mentions the path change.',
comment:
'**suggestion:** the `README.md` still says the scanner reads `src/jobs` (:42). anyone with a local overlay keyed on the old paths will get `merged 0/N` with no hint why. could we add a line about both? see [the overlay docs](https://example.test/docs/overlay).',
},
];

function SummaryAndFindings() {
const [summary, setSummary] = useState(
'Left a few comments, the `retryPolicy` one is the main one.'
);
const [texts, setTexts] = useState<Record<string, string>>(
Object.fromEntries(FINDINGS.map(f => [f.id, f.comment]))
);
const [ticked, setTicked] = useState(new Set(FINDINGS.map(f => f.id)));
const [editing, setEditing] = useState<string | null>(null);
const setText = (id: string) => (t: string) =>
setTexts(prev => ({ ...prev, [id]: t }));
const track = (key: string) => (on: boolean) =>
setEditing(prev => (on ? key : prev === key ? null : prev));
const folded = FINDINGS.filter(f => !f.file && ticked.has(f.id));
return (
<>
<div className="tui-sheet-list-head" style={{ marginTop: 0 }}>
<span className="tui-sheet-list-title">Summary comment</span>
<span
className="tui-sheet-list-tally"
style={{ display: 'inline-flex', alignItems: 'center', gap: 10 }}
>
<Chip intent="accent" variant="outline" uppercase>
for comment
</Chip>
posts on !1182
</span>
</div>
<article className="tui-review-card tui-review-summary">
<section className="tui-review-section">
<div style={{ display: 'flex', flexDirection: 'column', gap: 20 }}>
<ClickToEditField
label="Summary line"
text={summary}
variant="bare"
stripe="always"
hint={false}
onChange={setSummary}
onEditing={track('summary')}
/>
{folded.map(f => (
<ClickToEditField
key={f.id}
label={`Summary comment: ${f.title}`}
text={texts[f.id]!}
variant="bare"
stripe="always"
hint={false}
onChange={setText(f.id)}
onEditing={track(`summary:${f.id}`)}
/>
))}
</div>
{editing?.startsWith('summary') && (
<span className="tui-cte-hint">Markdown supported</span>
)}
</section>
</article>

<div className="tui-sheet-list-head">
<span className="tui-sheet-list-title">Findings</span>
<span className="tui-sheet-list-tally">
{ticked.size} of {FINDINGS.length} selected
</span>
</div>
<div style={{ display: 'flex', flexDirection: 'column', gap: 10 }}>
{FINDINGS.map(f => {
const on = ticked.has(f.id);
return (
<article
key={f.id}
className="tui-review-finding tui-review-card"
data-off={on ? undefined : ''}
>
<label className="tui-review-finding-row tui-review-card-head">
<input
type="checkbox"
className="tui-gate-choice-input"
data-type="checkbox"
data-checked={on ? '' : undefined}
checked={on}
onChange={e =>
setTicked(prev => {
const next = new Set(prev);
if (e.currentTarget.checked) next.add(f.id);
else next.delete(f.id);
return next;
})
}
/>
<span className="tui-review-finding-title">{f.title}</span>
</label>
{on && (
<>
{f.file && (
<section className="tui-review-section" data-section="code">
<div className="tui-review-section-head">
<span
className="tui-review-section-name"
data-hue="muted"
>
<CodeGlyph />
The Code
</span>
</div>
<div className="tui-review-code">
<div className="tui-review-code-file">
<FileGlyph />
<span className="tui-review-code-path">
{f.file.path}
<span className="tui-review-code-line">
:{f.file.line}
</span>
</span>
</div>
</div>
</section>
)}
<section className="tui-review-section">
<div className="tui-review-section-head">
<span
className="tui-review-section-name"
data-hue="accent"
>
<AgentGlyph />
Agent Finding
</span>
</div>
<div className="tui-review-section-body">
<Markdown unstyled linkTargetBlank>
{f.body}
</Markdown>
</div>
</section>
<section className="tui-review-section">
<div className="tui-review-section-head">
<span className="tui-review-section-name" data-hue="ok">
<CommentGlyph />
Comment
</span>
<span className="tui-review-section-meta">
{f.file
? 'drafted by the agent'
: editing === `summary:${f.id}`
? 'editing in summary'
: 'posts in summary'}
</span>
</div>
<ClickToEditField
label={`Comment on ${f.title}`}
text={texts[f.id]!}
variant="filled"
onChange={setText(f.id)}
onEditing={track(`card:${f.id}`)}
/>
</section>
</>
)}
</article>
);
})}
</div>
</>
);
}

export const SummaryWithFindings: Story = {
name: 'Summary and finding cards',
render: () => <SummaryAndFindings />,
};

function OneField() {
const [text, setText] = useState(
[
'**question:** does the retry here back off, or hammer the queue?',
'',
'- `retryPolicy.max` is 5',
'- the delay looks fixed at 200ms',
'',
'```ts',
'await retry(job, { max: 5, delayMs: 200 });',
'```',
'',
'see [the retry notes](https://example.test/retry) for the old numbers.',
].join('\n')
);
return <ClickToEditField label="Comment" text={text} onChange={setText} />;
}

export const SingleField: Story = {
name: 'One boxed field (lists, code, a link)',
render: () => <OneField />,
};

function EmptyField() {
const [text, setText] = useState('');
return (
<ClickToEditField
label="Comment"
text={text}
invalid={text.trim() === ''}
onChange={setText}
/>
);
}

export const Empty: Story = {
name: 'An empty comment',
render: () => <EmptyField />,
};
Loading
Loading