@@ -600,53 +660,27 @@ function FindingSection({ finding: f }: { finding: FindingEntry }) {
function CommentSection({
finding,
form,
+ you,
}: {
finding: FindingEntry;
form: GateFormState;
+ you?: string;
}) {
const key = commentKey(finding.id);
const label = `Comment on ${finding.title}`;
const { edited } = commentState(finding.comment!, form.texts[key], true);
return (
-
-
+ form.clearText(key)}
- />
- )
- }
- badge={
- edited && (
- <>
- Edited
-
- form.clearText(key)}
- />
- >
- )
- }
+ lead={ }
onChange={t => form.setText(key, t)}
+ onReset={() => form.clearText(key)}
/>
-
+
);
}
@@ -662,6 +696,7 @@ export function FindingCard({
mrRef,
mrRevision,
pins,
+ you,
}: {
finding: FindingEntry;
checked: boolean;
@@ -671,6 +706,8 @@ export function FindingCard({
mrRef?: string;
mrRevision?: string;
pins: FilePin[];
+ /** The reviewer's username, for the avatar beside the comment. */
+ you?: string;
}) {
return (
{head}
-
-
- {checked && (
-
- {hasDiffLine(f) ? (
-
- ) : (
-
- )}
-
-
- )}
-
-
+
+ {hasDiffLine(f) ? (
+
+ ) : (
+
+ )}
+
+
);
}
@@ -759,7 +777,7 @@ export function SummaryCard({
label="Summary line"
draft={draft}
value={form.texts[key]}
- variant="filled"
+ variant="plain"
onChange={t => form.setText(key, t)}
/>
diff --git a/apps/board/src/client/board/ReviewGateSheet.stories.tsx b/apps/board/src/client/board/ReviewGateSheet.stories.tsx
deleted file mode 100644
index fba0b3cd93..0000000000
--- a/apps/board/src/client/board/ReviewGateSheet.stories.tsx
+++ /dev/null
@@ -1,1006 +0,0 @@
-import { useEffect, type ReactNode } from 'react';
-import type { Meta, StoryObj } from '@storybook/react-vite';
-
-import { gateDraftKey } from '@mattstack/gate-kit/react';
-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 type { GateQuestion, GateRow } from '../../gates/store.ts';
-import type { BoardMRWithReview } from '../types.ts';
-import { DiffSourceContext, type DiffSource } from './diff-source.ts';
-import { useGateForm } from './GateForm.tsx';
-import { ReviewGateSheet } from './ReviewGateSheet.tsx';
-
-/**
- * Storybook coverage for the full-screen review gate sheet (design doc
- * `docs/superpowers/specs/2026-09-18-review-gate-redesign-design.md` §4):
- * six invented findings across two chunked questions plus a clean-review
- * (outcome-only) variant. The fixtures carry review@1 (gate-level) and
- * findings@1 (per-chunk) contexts, the shapes `readReviewGate` joins. The
- * sheet fills the viewport itself, so the stage only needs the theme
- * provider, not DecisionQueueModal.stories.tsx's tall padded stage.
- */
-function Stage({
- scheme,
- children,
-}: {
- scheme: 'light' | 'dark';
- children: ReactNode;
-}) {
- return (
-
- {children}
-
- );
-}
-
-const stage = (
- Story: () => ReactNode,
- context: { globals: { scheme?: string } }
-) => (
-
-
-
-);
-
-const meta = {
- title: 'Gates/Board/ReviewGateSheet',
- decorators: [stage],
- parameters: { layout: 'fullscreen' },
-} satisfies Meta;
-
-export default meta;
-
-type Story = StoryObj
;
-
-registerTheme(tuiTheme);
-
-function question(
- id: string,
- label: string,
- multi: boolean,
- options: GateQuestion['options'],
- context?: string
-): GateQuestion {
- return { id, label, multi, options, ...(context ? { context } : {}) };
-}
-
-const boardMr = {
- iid: 31,
- title: 'themed gate controls',
- webUrl: 'https://gitlab.example.com/acme/widgets/-/merge_requests/31',
- author: { id: 'gitlab:7', username: 'paul', name: 'Paul', avatarUrl: null },
- sourceBranch: 'board-28-themed-gate-controls',
- targetBranch: 'main',
- createdAt: '2026-09-17T09:00:00.000Z',
- diff: { additions: 84, deletions: 21, filesChanged: 6 },
-} as unknown as BoardMRWithReview;
-
-const ctx = (v: unknown) => JSON.stringify(v);
-const findingsCtx = (...findings: object[]) =>
- ctx({ 'gate-ctx': 'findings@1', findings });
-const F = {
- f1: {
- id: 'f1',
- severity: 'critical',
- title: 'SQL built from unsanitized input',
- file: 'lib/db/query.ts:42',
- fix: 'parameterize the query',
- body: 'The search handler interpolates the raw `q` parameter into the WHERE clause, so a crafted query string reaches the database as SQL.',
- },
- f2: {
- id: 'f2',
- severity: 'important',
- title: 'Missing null check on response',
- file: 'lib/api/client.ts:88',
- fix: 'guard before dereferencing',
- body: 'A 204 from the upstream returns no body, and `data.items` is read before anything checks that `data` exists.',
- },
- f3: {
- id: 'f3',
- severity: 'important',
- title: 'Inconsistent error wording',
- file: 'lib/errors.ts:15',
- fix: 'align with the style guide',
- body: 'Two of the new messages end in a period and one does not; the style guide asks for none.',
- },
- f4: {
- id: 'f4',
- severity: 'minor',
- title: 'Unused import',
- file: 'lib/utils.ts:3',
- fix: 'drop the dead import',
- body: '`debounce` is imported and never called.',
- },
- f5: {
- id: 'f5',
- severity: 'important',
- title: 'Retry loop lacks backoff',
- file: 'lib/retry.ts:41',
- fix: 'add exponential backoff',
- body: 'The loop retries immediately up to five times, which turns one upstream blip into a burst of six requests.',
- },
- f6: {
- id: 'f6',
- severity: 'minor',
- title: 'Inconsistent spacing',
- file: 'lib/format.ts:9',
- fix: 'run prettier',
- body: 'The new block mixes two- and four-space indents.',
- },
-};
-
-const findingsQuestions = [
- question(
- 'findings-1',
- 'Post which findings to !31?',
- true,
- [
- {
- value: 'f1',
- label: '[Critical] SQL built from unsanitized input',
- description: 'lib/db/query.ts:42 · parameterize the query',
- },
- {
- value: 'f2',
- label: '[Important] Missing null check on response',
- description:
- 'lib/api/client.ts:88 · guard before dereferencing · kind:bug',
- },
- {
- value: 'f3',
- label: '[Important] Inconsistent error wording',
- description: 'lib/errors.ts:15 · align with the style guide',
- },
- {
- value: 'f4',
- label: '[Minor] Unused import',
- description: 'lib/utils.ts:3 · drop the dead import',
- },
- ],
- findingsCtx(F.f1, F.f2, F.f3, F.f4)
- ),
- question(
- 'findings-2',
- 'Post which findings to !31?',
- true,
- [
- {
- value: 'f5',
- label: '[Important] Retry loop lacks backoff',
- description:
- 'lib/retry.ts:41 · add exponential backoff · kind:suggestion',
- },
- {
- value: 'f6',
- label: '[Minor] Inconsistent spacing',
- description: 'lib/format.ts:9 · run prettier',
- },
- ],
- findingsCtx(F.f5, F.f6)
- ),
- question('outcome', 'Verdict on !31', false, [
- {
- value: 'approve',
- label: 'approve (recommended)',
- description:
- 'Approve !31 and post the selected findings as inline threads.',
- },
- {
- value: 'comment',
- label: 'comment',
- description:
- 'Post the selected findings without a verdict; the important ones stay open.',
- },
- ]),
-];
-
-const sixFindingGate: GateRow = {
- gateId: 'sheet-six-findings',
- subject: 'mr:gitlab.example.com/acme/widgets/-/merge_requests/31',
- kind: 'review-post',
- label: 'review',
- status: 'open',
- openedAt: 1788962100000,
- context: ctx({
- 'gate-ctx': 'review@1',
- reviewer: 'renee',
- readiness: 'with-fixes',
- summary: 'One critical injection risk; everything else is polish.',
- findings: { critical: 1, important: 3, minor: 2 },
- }),
- questions: findingsQuestions,
- origin: { paneId: 'pane-1', worktree: 'widgets' },
-};
-
-const reportJson = JSON.stringify({
- findings: [],
- depth:
- 'verify. jest 120/120 green; typecheck noise was codegen, not defects.',
- strengths: [
- {
- lead: 'Retry loop now covers the abort path',
- detail: 'lib/retry.ts:58, matches the earlier thread',
- },
- { lead: 'Config validation reads clean', detail: 'lib/config.ts:12-40' },
- ],
- checks: [
- { tag: 'PASS', text: 'jest suite (120/120)' },
- { tag: 'PASS', text: 'typecheck' },
- { tag: 'N/A', text: 'visual regression (no UI touched)' },
- ],
- notes: ['Consider a follow-up MR to backfill retry tests for the old path.'],
-});
-
-// Captured once at module scope, before any story stubs it: the restore
-// target every fetchStub decorator falls back to and resets to on cleanup.
-const REAL_FETCH = globalThis.fetch;
-
-/** Installs a fetch stub for the story's mount only and restores the real
- fetch on unmount, rather than clobbering globalThis.fetch permanently.
- `handler` answers `/review/*` urls; anything it returns undefined for
- falls through to the real fetch. */
-function fetchStub(
- handler: (url: string) => Response | Promise | undefined
-) {
- return function FetchStubDecorator(Story: () => ReactNode) {
- // Installed during render, not in an effect: the sheet's own mount
- // effect fetches report.json, and child effects run before this
- // decorator's would, so an effect-installed stub arrives too late.
- globalThis.fetch = (async (
- input: RequestInfo | URL,
- init?: RequestInit
- ) => {
- const url = typeof input === 'string' ? input : input.toString();
- const stubbed = await handler(url);
- if (stubbed !== undefined) return stubbed;
- return REAL_FETCH(input as RequestInfo, init);
- }) as typeof fetch;
- useEffect(
- () => () => {
- globalThis.fetch = REAL_FETCH;
- },
- []
- );
- return ;
- };
-}
-
-const noop = () => {};
-
-function seedDraft(
- gateId: string,
- draft: {
- selections: Record;
- notes: Record;
- item: string | null;
- }
-) {
- localStorage.setItem(gateDraftKey(gateId), JSON.stringify(draft));
-}
-
-function Host({ gate, mr }: { gate: GateRow; mr: BoardMRWithReview }) {
- const form = useGateForm(gate, noop);
- return (
-
- );
-}
-
-// --- SixFindings -------------------------------------------------------
-
-/** The full record cluster: report.json resolves with every optional array
- populated, so `/review/report.json` is mocked in rather than left to hit
- the network from Storybook's iframe. */
-export const SixFindings: Story = {
- decorators: [
- fetchStub(url =>
- url.startsWith('/review/report.json')
- ? new Response(reportJson, {
- status: 200,
- headers: { 'content-type': 'application/json' },
- })
- : undefined
- ),
- ],
- render: () => ,
-};
-
-// --- UnselectedSome ------------------------------------------------------
-
-/** Mid-triage: three findings unchecked, so the tally and submit label both
- read a partial count -- the draft seed drives the same localStorage key
- useGateForm reads on mount. */
-const partialGate: GateRow = { ...sixFindingGate, gateId: 'sheet-partial' };
-
-export const UnselectedSome: Story = {
- decorators: [
- fetchStub(url =>
- url.includes('/review/')
- ? new Response('no structured review yet', { status: 404 })
- : undefined
- ),
- ],
- render: () => {
- seedDraft(partialGate.gateId, {
- selections: { findings: ['f1', 'f3', 'f5'], outcome: 'comment' },
- notes: {},
- item: null,
- });
- return ;
- },
-};
-
-// --- CleanReview -----------------------------------------------------------
-
-/** No findings at all: the outcome question rides alone, so the sheet skips
- the findings head/list entirely and shows only the MR card, the (absent)
- record cluster's full-report fallback, and the verdict. */
-const cleanGate: GateRow = {
- gateId: 'sheet-clean',
- subject: 'mr:gitlab.example.com/acme/widgets/-/merge_requests/46',
- kind: 'review-post',
- label: 'review',
- status: 'open',
- openedAt: 1788964320000,
- context: ctx({
- 'gate-ctx': 'review@1',
- readiness: 'yes',
- summary: 'Nothing flagged worth a thread.',
- findings: {},
- }),
- questions: [
- question('outcome', 'Verdict on !46', false, [
- {
- value: 'approve',
- label: 'approve (recommended)',
- description: 'Approve !46; nothing was flagged worth a thread.',
- },
- {
- value: 'comment',
- label: 'comment',
- description: 'Leave a comment without approving.',
- },
- ]),
- ],
- origin: { paneId: 'pane-1', worktree: 'widgets' },
-};
-
-const cleanMr = { ...boardMr, iid: 46 } as BoardMRWithReview;
-
-export const CleanReview: Story = {
- decorators: [
- fetchStub(url =>
- url.includes('/review/')
- ? new Response('no structured review yet', { status: 404 })
- : undefined
- ),
- ],
- render: () => ,
-};
-
-// --- ReReview --------------------------------------------------------------
-
-/** Round 2: the decision card's meta line carries the reviewer, the round,
- and the addressed/still-open tally, and three carried-over findings each
- show their own disposition pill. */
-const reReviewGate: GateRow = {
- gateId: 'sheet-re-review',
- subject: 'mr:gitlab.example.com/acme/widgets/-/merge_requests/31',
- kind: 'review-post',
- label: 'review',
- status: 'open',
- openedAt: 1788965520000,
- context: ctx({
- 'gate-ctx': 'review@1',
- reviewer: 'renee',
- readiness: 'with-fixes',
- summary: 'Two important findings carried over; the rest is polish.',
- findings: { important: 2, minor: 1 },
- round: 2,
- re_review: true,
- prior: { addressed: 3, still_open: 1 },
- }),
- questions: [
- question(
- 'findings-1',
- 'Post which findings to !31?',
- true,
- [
- findingsQuestions[0]!.options[1]!,
- findingsQuestions[1]!.options[0]!,
- findingsQuestions[1]!.options[1]!,
- ],
- findingsCtx(
- { ...F.f2, disposition: 'still-open' },
- { ...F.f5, disposition: 'new' },
- { ...F.f6, disposition: 'addressed-check' }
- )
- ),
- findingsQuestions[2]!,
- ],
- origin: { paneId: 'pane-1', worktree: 'widgets' },
-};
-
-export const ReReview: Story = {
- decorators: [
- fetchStub(url =>
- url.includes('/review/')
- ? new Response('no structured review yet', { status: 404 })
- : undefined
- ),
- ],
- render: () => ,
-};
-
-// --- Later rounds: earlier threads, new findings, skipped findings --------
-
-const no404 = fetchStub(url =>
- url.includes('/review/')
- ? new Response('no structured review yet', { status: 404 })
- : undefined
-);
-
-function carryQuestion(
- n: number,
- thread: string,
- file: string,
- defaults: { post: boolean; resolve: boolean },
- carry: {
- round: number;
- call: 'fixed' | 'not-fixed' | 'pushback-accepted' | 'pushback-rejected';
- original: string;
- authorReply?: string;
- note?: string;
- reply: string;
- }
-): GateQuestion {
- return question(
- `thread-${n}`,
- file,
- true,
- [
- {
- value: `post:${thread}`,
- label: defaults.post ? 'Post reply (Recommended)' : 'Post reply',
- description: 'post this reply to the thread',
- },
- {
- value: `resolve:${thread}`,
- label: defaults.resolve ? 'Resolve (Recommended)' : 'Resolve',
- description: 'resolve the thread when the review is submitted',
- },
- ],
- ctx({ 'gate-ctx': 'carryover@1', thread, file, ...carry })
- );
-}
-
-const threadFixed = carryQuestion(
- 1,
- 'd-query',
- 'lib/db/query.ts:42',
- { post: true, resolve: true },
- {
- round: 1,
- call: 'fixed',
- original:
- '**issue:** the search handler interpolates the raw `q` parameter into the WHERE clause, so a crafted query string reaches the database as SQL.',
- authorReply:
- 'Switched to a parameterized query in 4c1e9a2 and added a test with a quote in the search term.',
- note: 'verified: query.ts:42 now binds $1, and the new test covers a quoted term',
- reply: "Thanks, that's exactly the fix. Resolving.",
- }
-);
-
-const threadNotFixed = carryQuestion(
- 2,
- 'd-retry',
- 'lib/retry.ts:41',
- { post: true, resolve: false },
- {
- round: 2,
- call: 'not-fixed',
- original:
- '**issue:** the loop retries immediately up to five times, which turns one upstream blip into a burst of six requests.',
- authorReply: 'Added exponential backoff in 7d2f310.',
- note: 'the backoff landed, but an aborted request still goes round the loop: retry.ts:58 never checks signal.aborted',
- reply:
- 'The backoff looks good. One gap left: an aborted request still goes round the loop, since nothing checks `signal.aborted` before the next try (`retry.ts:58`).',
- }
-);
-
-const threadPushbackAccepted = carryQuestion(
- 3,
- 'd-errors',
- 'lib/errors.ts:15',
- { post: true, resolve: true },
- {
- round: 2,
- call: 'pushback-accepted',
- original:
- '**suggestion:** two of the new messages end in a period and one does not; the style guide asks for none.',
- authorReply:
- "These mirror the upstream API's error strings word for word, so support can search for them. I'd rather keep them identical.",
- note: 'fair: the strings are copied verbatim from the upstream API, and matching them is the better rule here',
- reply:
- 'Makes sense, matching the API exactly is the better call. Resolving.',
- }
-);
-
-const threadPushbackRejected = carryQuestion(
- 4,
- 'd-client',
- 'lib/api/client.ts:88',
- { post: true, resolve: false },
- {
- round: 1,
- call: 'pushback-rejected',
- original:
- '**issue:** a 204 from the upstream returns no body, and `data.items` is read before anything checks that `data` exists.',
- authorReply: 'The upstream never sends a 204 for this endpoint.',
- note: 'the upstream docs list a 204 for an empty page, and client.test.ts has no case for it',
- reply:
- "The upstream docs list a 204 for an empty page (`GET /items?page=`), so I think the guard still earns its place. Happy to be wrong if you've seen otherwise.",
- }
-);
-
-const threadNoReply = carryQuestion(
- 5,
- 'd-cache-ttl',
- 'lib/cache.ts:9',
- { post: true, resolve: false },
- {
- round: 2,
- call: 'not-fixed',
- original:
- '**question:** is a 24h TTL intended here? The other caches in this module use 5 minutes.',
- note: 'no reply and no change at cache.ts:9',
- reply: 'Bumping this one: is the 24h TTL intended?',
- }
-);
-
-const NEW = {
- n1: {
- id: 'n1',
- severity: 'important',
- title: 'Cache key ignores the tenant',
- file: 'lib/cache.ts:23',
- fix: 'prefix the key with the tenant id',
- body: 'Two tenants asking for the same page share one cache entry, so the second one is served the first tenant’s results.',
- disposition: 'new',
- },
- n2: {
- id: 'n2',
- severity: 'minor',
- title: 'Debug log prints the whole session object',
- file: 'lib/auth/session.ts:61',
- fix: 'log the session id only',
- body: 'The new `log.debug` call serializes the full session, refresh token included.',
- disposition: 'new',
- },
-};
-
-const newFindingsQuestion = question(
- 'findings-1',
- 'Post which findings to !31?',
- true,
- [
- {
- value: 'n1',
- label: '[Important] Cache key ignores the tenant',
- description: 'lib/cache.ts:23 · prefix the key with the tenant id',
- },
- {
- value: 'n2',
- label: '[Minor] Debug log prints the whole session object',
- description: 'lib/auth/session.ts:61 · log the session id only',
- },
- ],
- findingsCtx(NEW.n1, NEW.n2)
-);
-
-const skippedQuestion = question(
- 'skipped-1',
- 'Bring back which skipped findings?',
- true,
- [
- {
- value: 'restore:s1',
- label: '[Minor] Unused import',
- description: 'lib/utils.ts:3 · skipped in round 1',
- },
- {
- value: 'restore:s2',
- label: '[Minor] Inconsistent spacing',
- description: 'lib/format.ts:9 · skipped in round 1 · code changed since',
- },
- {
- value: 'restore:s3',
- label: '[Important] Config defaults live in two files',
- description: 'lib/config.ts:12 · skipped in round 2',
- },
- ],
- ctx({
- 'gate-ctx': 'skipped@1',
- skipped: [
- {
- id: 's1',
- round: 1,
- severity: 'minor',
- title: 'Unused import',
- file: 'lib/utils.ts:3',
- },
- {
- id: 's2',
- round: 1,
- severity: 'minor',
- title: 'Inconsistent spacing',
- file: 'lib/format.ts:9',
- changed: true,
- },
- {
- id: 's3',
- round: 2,
- severity: 'important',
- title: 'Config defaults live in two files',
- file: 'lib/config.ts:12',
- },
- ],
- })
-);
-
-const commentFirst = question('outcome', 'Verdict on !31', false, [
- {
- value: 'comment',
- label: 'comment (recommended)',
- description:
- 'Submit as a Comment review: the replies, resolves and new findings post together.',
- },
- {
- value: 'approve',
- label: 'approve',
- description: 'Submit the review and approve !31.',
- },
-]);
-
-const roundThreeGate: GateRow = {
- gateId: 'sheet-round-three',
- subject: 'mr:gitlab.example.com/acme/widgets/-/merge_requests/31',
- kind: 'review-post',
- label: 'review',
- status: 'open',
- openedAt: 1788969120000,
- context: ctx({
- 'gate-ctx': 'review@1',
- reviewer: 'renee',
- readiness: 'with-fixes',
- summary:
- 'The injection fix and the backoff both landed. An aborted request still retries, the 204 guard is still missing, and the new cache layer shares entries across tenants.',
- findings: { important: 1, minor: 1 },
- round: 3,
- re_review: true,
- }),
- questions: [
- threadFixed,
- threadNotFixed,
- threadPushbackAccepted,
- threadPushbackRejected,
- threadNoReply,
- newFindingsQuestion,
- skippedQuestion,
- commentFirst,
- ],
- origin: { paneId: 'pane-1', worktree: 'widgets' },
-};
-
-/** Round 3: five earlier threads, one per call the agent can make (plus a
- thread the author never answered), two new findings, and three findings
- skipped in earlier rounds, collapsed. Fixed and accepted pushback start
- on reply + resolve; the rest start on reply only. */
-export const RoundThree: Story = {
- decorators: [no404],
- render: () => ,
-};
-
-const bringingBackGate: GateRow = {
- ...roundThreeGate,
- gateId: 'sheet-round-three-bringing-back',
-};
-
-/** The same round with one skipped finding ticked to come back: the skipped
- row opens on its own, its hint and the submit label count it, and the
- rest stay off the MR. */
-export const RoundThreeBringingOneBack: Story = {
- decorators: [no404],
- render: () => {
- seedDraft(bringingBackGate.gateId, {
- selections: { skipped: ['restore:s2'] },
- notes: {},
- item: null,
- });
- return ;
- },
-};
-
-const settledGate: GateRow = {
- gateId: 'sheet-round-two-settled',
- subject: 'mr:gitlab.example.com/acme/widgets/-/merge_requests/31',
- kind: 'review-post',
- label: 'review',
- status: 'open',
- openedAt: 1788970320000,
- context: ctx({
- 'gate-ctx': 'review@1',
- reviewer: 'renee',
- readiness: 'yes',
- summary:
- 'Both asks from round 1 are settled and nothing new turned up. Ready to approve.',
- findings: {},
- round: 2,
- re_review: true,
- }),
- questions: [
- threadFixed,
- threadPushbackAccepted,
- question('outcome', 'Verdict on !31', false, [
- {
- value: 'approve',
- label: 'approve (recommended)',
- description: 'Submit the replies and resolves, and approve !31.',
- },
- {
- value: 'comment',
- label: 'comment',
- description: 'Submit the replies and resolves without approving.',
- },
- ]),
- ],
- origin: { paneId: 'pane-1', worktree: 'widgets' },
-};
-
-/** Round 2 where everything from round 1 is settled: the earlier threads
- start on reply + resolve, the new-findings section says nothing turned
- up, and approve is the recommendation. */
-export const RoundTwoAllSettled: Story = {
- decorators: [no404],
- render: () => ,
-};
-
-// --- DraftedReReview -----------------------------------------------------
-
-const draftedNewFindings = question(
- 'findings-1',
- 'Post which findings to !31?',
- true,
- newFindingsQuestion.options,
- findingsCtx(
- {
- ...NEW.n1,
- comment:
- 'Two tenants asking for the same page share one entry here, so the second one gets the first tenant’s results. Could the key start with the tenant id?\n\n```ts\nconst key = `${tenantId}:${page}`;\n```',
- },
- {
- ...NEW.n2,
- comment:
- 'Small one: this `log.debug` serializes the whole session, refresh token included. Logging `session.id` would be enough.',
- }
- )
-);
-
-const draftedReReviewGate: GateRow = {
- ...roundThreeGate,
- gateId: 'sheet-drafted-re-review',
- context: ctx({
- ...(JSON.parse(roundThreeGate.context!) as Record),
- editable: true,
- summaryComments: {
- comment:
- 'Thanks for the quick turnaround. Two earlier threads still need a look, and two new things turned up inline.',
- approve:
- 'Thanks for the quick turnaround. The rest are small, so approving; the inline notes can land in a follow-up.',
- },
- }),
- questions: roundThreeGate.questions.map(q =>
- q.id === 'findings-1' ? draftedNewFindings : q
- ),
-};
-
-/** A drafted round 3: the earlier threads keep their reply composers, each
- with Post reply and Resolve thread in its footer, then the new findings
- as drafted cards. No diff source is wired, so each card's code shows its
- file header only. */
-export const DraftedReReview: Story = {
- decorators: [no404],
- render: () => ,
-};
-
-// --- DraftedFirstRound ---------------------------------------------------
-
-const draftedFirstRoundFindings = question(
- 'findings-1',
- 'Post which findings to !31?',
- true,
- [
- {
- value: 'f1',
- label: '[Critical] SQL built from unsanitized input',
- description: 'lib/db/query.ts:42 · parameterize the query',
- },
- {
- value: 'f5',
- label: '[Important] Retry loop lacks backoff',
- description: 'lib/retry.ts:41 · add exponential backoff',
- },
- {
- value: 'n-readme',
- label: '[Minor] README still names the old folder',
- description: 'README.md · mention the new path',
- },
- {
- value: 'n-evidence',
- label: '[Minor] No run for the --changed flag',
- description: 'evidence · one quick run',
- },
- ],
- findingsCtx(
- {
- ...F.f1,
- comment:
- '**issue:** the search handler drops the raw `q` straight into the `WHERE` clause, so a crafted query string reaches the database as SQL. could this use a bound parameter?\n\n```ts\ndb.query("SELECT * FROM items WHERE name = $1", [q]);\n```',
- },
- {
- ...F.f5,
- comment:
- '**suggestion:** the loop retries immediately up to five times, so one upstream blip turns into six requests. something like `Math.min(200 * 2 ** attempt, 5000)` would back it off.',
- },
- {
- id: 'n-readme',
- severity: 'minor',
- title: 'README still names the old folder',
- file: 'README.md',
- fix: 'mention the new path',
- body: 'The README still says the scanner reads `lib/jobs`, and nothing mentions the move to `lib/tasks`.',
- comment:
- '**suggestion:** the `README.md` still says the scanner reads `lib/jobs`. 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 the move?',
- },
- {
- id: 'n-evidence',
- severity: 'minor',
- title: 'No run for the --changed flag',
- fix: 'one quick run',
- body: 'Every AC has a run in the evidence except `--changed` picking up a file under `lib/tasks/**`.',
- comment:
- '**suggestion:** thanks for the extra evidence, that covers it nicely. the one AC without a run is `--changed` picking up a file under `lib/tasks/**`... would you mind grabbing one quick run of that?',
- }
- )
-);
-
-const draftedFirstRoundGate: GateRow = {
- ...sixFindingGate,
- gateId: 'sheet-drafted-first-round',
- context: ctx({
- ...(JSON.parse(sixFindingGate.context!) as Record),
- findings: { critical: 1, important: 1, minor: 2 },
- editable: true,
- summaryComments: {
- approve:
- 'Left a few comments, the injection one is the main one. Approving once that lands.',
- comment: 'Left a few comments, the injection one is the main one.',
- },
- }),
- questions: [
- draftedFirstRoundFindings,
- question('outcome', 'Verdict on !31', false, [
- {
- value: 'comment',
- label: 'comment (recommended)',
- description: 'Post the selected findings without a verdict.',
- },
- {
- value: 'approve',
- label: 'approve',
- description: 'Approve !31 and post the selected findings.',
- },
- ]),
- ],
-};
-
-const FIRST_ROUND_PATCHES: Record = {
- 'lib/db/query.ts': `@@ -35,12 +35,14 @@ export async function searchItems(
- ) {
- const db = await pool.connect();
- const q = req.query.q ?? '';
- const limit = Number(req.query.limit ?? 20);
-
-- const rows = await db.query('SELECT * FROM items LIMIT $1', [limit]);
-+ const rows = await db.query(
-+ \`SELECT * FROM items
-+ WHERE name LIKE '%\${q}%'
-+ LIMIT \${limit}\`
-+ );
- return rows.map(toItem);
- }
-
- export async function itemById(id: string) {`,
- 'lib/retry.ts': `@@ -34,12 +34,14 @@ export async function withRetry(
- job: () => Promise,
- attempts = 5
- ): Promise {
-- return job();
-+ for (let attempt = 0; ; attempt++) {
-+ try {
-+ return await job();
-+ } catch (err) {
-+ if (attempt >= attempts) throw err;
-+ log.warn('retrying', { attempt });
-+ }
-+ }
- }
-
- export function isRetryable(err: unknown) {`,
-};
-
-/** Invented patches for the two files the drafted findings sit on. */
-const firstRoundDiffs: DiffSource = {
- fetchFileDiff: async ({ file }) =>
- FIRST_ROUND_PATCHES[file]
- ? {
- path: file,
- patch: FIRST_ROUND_PATCHES[file]!,
- forge: 'gitlab',
- webUrl: `${boardMr.webUrl}/diffs`,
- }
- : null,
- loadView: () => import('./DiffView.tsx'),
-};
-
-/** A drafted first round with click-to-edit comments: two findings on a
- diff line, two that fold into the summary comment. Every comment reads
- rendered and opens raw on a click; a folded one edits in the summary or
- in its own card. */
-export const DraftedFirstRound: Story = {
- // A real gate's report.json carries the record cluster; without it the
- // column falls back to the Full report disclosure, which real gates
- // rarely show.
- decorators: [
- fetchStub(url =>
- url.startsWith('/review/report.json')
- ? new Response(reportJson, {
- status: 200,
- headers: { 'content-type': 'application/json' },
- })
- : undefined
- ),
- ],
- render: () => (
-
-
-
- ),
-};
diff --git a/apps/board/src/client/board/ReviewGateSheet.tsx b/apps/board/src/client/board/ReviewGateSheet.tsx
index a2fe453c96..0281bffc82 100644
--- a/apps/board/src/client/board/ReviewGateSheet.tsx
+++ b/apps/board/src/client/board/ReviewGateSheet.tsx
@@ -1,4 +1,5 @@
-import { useEffect, useMemo, useRef, useState } from 'react';
+import { useEffect, useMemo, useState } from 'react';
+import { ChartColumn, ListChecks, ScanSearch } from 'lucide-react';
import {
collapseChunks,
@@ -21,13 +22,15 @@ import { Disclosure, DisclosureHead } from './Disclosure.tsx';
import type { Disposition, FindingEntry, FindingSeverity } from './gate-ctx.ts';
import type { GateFormState } from './GateForm.tsx';
import { GateSheet, type GateSheetQueue } from './GateSheet.tsx';
+import { GateTimeline, type RoundEntry } from './GateTimeline.tsx';
import {
CircleCheckFilledIcon,
PencilLineIcon,
SearchCheckIcon,
} from './icons.tsx';
import { MrCard } from './MrCard.tsx';
-import { mrRef } from './MrLinks.tsx';
+import { mrRef, onGitHub } from './MrLinks.tsx';
+import { usePeople } from './people.ts';
import {
attachDraftEdits,
draftEdits,
@@ -44,7 +47,7 @@ import {
import { FindingCard, SummaryCard, type FilePin } from './ReviewDraftCard.tsx';
import {
CarryoverCard,
- carryoverTally,
+ CarryoverTally,
carryoverTexts,
FindingAnchor,
SkippedEarlier,
@@ -53,7 +56,8 @@ import { useSheetKeys } from './sheet-keys.ts';
import {
KeyCap,
reserveDock,
- RoundHeading,
+ SheetEnd,
+ SheetHeading,
SheetLost,
SUBMIT_KEYS,
} from './SheetParts.tsx';
@@ -207,13 +211,51 @@ const DISPOSITION: Record<
function severityGroups(
findings: FindingEntry[]
): Array<[FindingSeverity, FindingEntry[]]> {
- return SEVERITY_ORDER.map(
- s =>
- [s, findings.filter(f => f.severity === s)] as [
- FindingSeverity,
- FindingEntry[],
- ]
- ).filter(([, items]) => items.length > 0);
+ // Inside a severity, the findings on the code come first and the ones
+ // with no diff line after, each keeping the agent's order.
+ return SEVERITY_ORDER.map(s => {
+ const items = findings.filter(f => f.severity === s);
+ return [
+ s,
+ [...items.filter(hasDiffLine), ...items.filter(f => !hasDiffLine(f))],
+ ] as [FindingSeverity, FindingEntry[]];
+ }).filter(([, items]) => items.length > 0);
+}
+
+/** The rounds behind a review gate, from what each recorded round posted.
+ A round that approved needs no reply; any other is taken as answered,
+ since a later round exists. */
+function reviewHistory(
+ round: number,
+ outcomes: readonly ('comment' | 'approve')[],
+ you: string | undefined,
+ authorId: string | undefined,
+ author: string
+): RoundEntry[] {
+ return Array.from({ length: round }, (_, i): RoundEntry => {
+ if (i === round - 1)
+ return {
+ round: i + 1,
+ events: [{ id: you, text: 'You, reviewing', now: 'review' }],
+ };
+ const outcome = outcomes[i];
+ const posted = {
+ id: you,
+ text:
+ outcome === 'approve'
+ ? 'You approved'
+ : outcome === 'comment'
+ ? 'You commented'
+ : 'You reviewed',
+ };
+ return {
+ round: i + 1,
+ events:
+ outcome === 'approve'
+ ? [posted]
+ : [posted, { id: authorId, text: `${author} replied` }],
+ };
+ });
}
/** The drafts the poster falls back to, or null when the gate does not
@@ -299,6 +341,7 @@ function ReviewGateSheet({
onClose,
onContinue,
onFocusPane,
+ self,
}: {
gate: GateRow;
mr?: BoardMRWithReview;
@@ -308,6 +351,8 @@ function ReviewGateSheet({
/** Retires a gate answered elsewhere from the queue. */
onContinue: () => void;
onFocusPane: (mr: BoardMRWithReview, domain: GateDomain) => void;
+ /** The seat's username: the reviewer when the gate names none. */
+ self?: string | null;
}) {
const data = useMemo(() => readReviewGate(gate), [gate]);
const { questions, groups } = useMemo(
@@ -331,6 +376,7 @@ function ReviewGateSheet({
const skippedName = skippedQuestion?.id;
const outcomeName = outcomeQuestion?.id;
const [mainTab, setMainTab] = useState<'findings' | 'report'>('findings');
+ const people = usePeople(mr);
const skippedEntries = useMemo(
() =>
(skippedQuestion?.options ?? []).flatMap(o => {
@@ -514,26 +560,12 @@ function ReviewGateSheet({
mrRef={mr && mrRef(mr)}
mrRevision={mr?.updatedAt ?? undefined}
pins={f.file ? (pinsByFile.get(anchorPath(f.file)) ?? []) : []}
+ you={you}
head={
{tick}
{title}
{pills}
- {!hasDiffLine(f) && (
-
- in summary
-
- )}
{SEVERITY_LABEL[f.severity]}
@@ -623,22 +655,39 @@ function ReviewGateSheet({
if (!data) return null;
const { review } = data;
+ const author = mr ? people.short(mr.author.username) : 'the author';
+ const you = review.reviewer ?? self ?? undefined;
const meta = reviewMeta(review);
- const roundSummary = [
- carryovers.length > 0
- ? `${carryovers.length} earlier ${carryovers.length === 1 ? 'thread' : 'threads'}`
- : null,
- findings.length > 0
- ? `${findings.length} new ${findings.length === 1 ? 'finding' : 'findings'}`
- : carryovers.length > 0
- ? 'nothing new'
- : null,
- skippedEntries.length > 0
- ? `${skippedEntries.length} skipped earlier`
- : null,
- ]
- .filter(Boolean)
- .join(' · ');
+
+ const contextCard = (
+
+
decision context
+
+ {readinessProse(review.readiness)}
+
+
+
+ {review.summary}
+
+
+ {meta &&
{meta}
}
+ {SEVERITY_ORDER.some(s => review.findings[s] > 0) && (
+
+ {SEVERITY_ORDER.filter(s => review.findings[s] > 0).map(s => (
+
+ {SEVERITY_LABEL[s]}
+ {review.findings[s]}
+
+ ))}
+
+ )}
+
+ );
return (
+ }
+ title={`Review ${author}'s ${mr && onGitHub(mr) ? 'PR' : 'MR'}`}
+ aside={
+
+ }
+ />
{(
[
- ['findings', 'Findings', findings.length],
- ['report', 'Report', undefined],
+ ['findings', 'Findings', ListChecks, findings.length],
+ ['report', 'Report', ChartColumn, undefined],
] as const
- ).map(([id, label, count]) => (
+ ).map(([id, label, TabIcon, count]) => (
setMainTab(id)}
>
+
{label}
{count !== undefined && (
{count}
@@ -698,22 +769,23 @@ function ReviewGateSheet({
{mainTab === 'findings' && (
<>
- {review.round !== undefined && (
-
- )}
{carryovers.length > 0 && (
<>
- Your earlier threads ({carryovers.length})
-
-
- {carryoverTally(carryovers)}
+ Earlier threads ({carryovers.length})
+
{carryovers.map(c => (
-
+
))}
>
@@ -803,9 +875,17 @@ function ReviewGateSheet({
}
/>
)}
+ You've reached the end of the findings.
>
)}
- {mainTab === 'report' && (record || checks.length > 0) && (
+ {mainTab === 'report' && (
+
+ {mr && }
+ {contextCard}
+ {checks.length > 0 && }
+
+ )}
+ {mainTab === 'report' && record && (
{strengths.length > 0 && (
@@ -859,32 +939,6 @@ function ReviewGateSheet({
)}
- {checks.length > 0 && (
-
-
checks
-
- {checks.map((ch, i) => (
-
-
- {ch.tag}
-
- {ch.text}
-
- ))}
-
-
- )}
)}
{mainTab === 'report' && !record && (
@@ -898,39 +952,7 @@ function ReviewGateSheet({
<>
{mr &&
}
-
-
- decision context
-
-
- {readinessProse(review.readiness)}
-
-
-
- {review.summary}
-
-
- {meta &&
{meta}
}
- {SEVERITY_ORDER.some(s => review.findings[s] > 0) && (
-
- {SEVERITY_ORDER.filter(s => review.findings[s] > 0).map(
- s => (
-
- {SEVERITY_LABEL[s]}
-
- {review.findings[s]}
-
-
- )
- )}
-
- )}
-
+ {contextCard}
{checks.length > 0 &&
}
diff --git a/apps/board/src/client/board/ReviewRoundParts.tsx b/apps/board/src/client/board/ReviewRoundParts.tsx
index cf5f31d5cd..a882d8474c 100644
--- a/apps/board/src/client/board/ReviewRoundParts.tsx
+++ b/apps/board/src/client/board/ReviewRoundParts.tsx
@@ -1,25 +1,22 @@
import { useState } from 'react';
import type { GateSelections } from '@mattstack/gate-kit';
-import { Markdown } from '@mattstack/tui-kit';
-import { CommentComposer } from './CommentComposer.tsx';
import { Disclosure, DisclosureHead } from './Disclosure.tsx';
import type { CarryoverCall, SkippedEntry } from './gate-ctx.ts';
import type { GateFormState } from './GateForm.tsx';
-import { SearchCheckIcon } from './icons.tsx';
import { sendableTexts } from './respond-post.ts';
-import { PostResolveChoice } from './RespondCards.tsx';
import { SEVERITY_LABEL, type CarryoverPick } from './review-gate.ts';
+import { ThreadReplyCard } from './ThreadReplyCard.tsx';
/** Every label names who has to act; none says "open" or "unresolved". */
export const CARRYOVER_CALL: Record<
CarryoverCall,
{ text: string; hue: 'green' | 'amber' | 'accent' }
> = {
- fixed: { text: 'fixed by author', hue: 'green' },
- 'not-fixed': { text: 'waiting on author', hue: 'amber' },
- 'pushback-accepted': { text: 'author pushed back · accept', hue: 'accent' },
- 'pushback-rejected': { text: 'author pushed back · hold firm', hue: 'amber' },
+ fixed: { text: 'Fixed', hue: 'green' },
+ 'not-fixed': { text: 'Waiting on author', hue: 'amber' },
+ 'pushback-accepted': { text: 'Pushback accepted', hue: 'accent' },
+ 'pushback-rejected': { text: 'Holding firm', hue: 'amber' },
};
export function isPosting(
@@ -54,119 +51,93 @@ export function carryoverTexts(
);
}
-/** "2 fixed · 1 pushback accepted · 1 waiting on author" */
-export function carryoverTally(picks: CarryoverPick[]): string {
- let fixed = 0;
- let accepted = 0;
- let waiting = 0;
- for (const p of picks) {
- if (p.carry.call === 'fixed') fixed++;
- else if (p.carry.call === 'pushback-accepted') accepted++;
- else waiting++;
- }
- return [
- fixed > 0 ? `${fixed} fixed` : null,
- accepted > 0 ? `${accepted} pushback accepted` : null,
- waiting > 0 ? `${waiting} waiting on author` : null,
- ]
- .filter(Boolean)
- .join(' · ');
-}
+const TALLY_ORDER: CarryoverCall[] = [
+ 'fixed',
+ 'pushback-accepted',
+ 'not-fixed',
+ 'pushback-rejected',
+];
-function Quote({
- k,
- who,
- text,
-}: {
- k: string;
- who: 'you' | 'author';
- text: string;
-}) {
+/** The earlier threads counted by status, each status as its card shows it:
+ the same dot, word and colour, then its count. */
+export function CarryoverTally({ picks }: { picks: CarryoverPick[] }) {
+ const counts = new Map();
+ for (const p of picks)
+ counts.set(p.carry.call, (counts.get(p.carry.call) ?? 0) + 1);
return (
-
+
+ {TALLY_ORDER.filter(c => counts.has(c)).map(c => (
+
+ {CARRYOVER_CALL[c].text}
+ {counts.get(c)}
+
+ ))}
+
);
}
/** One of the reviewer's earlier threads: what they wrote, what the author
- said back, the agent's check, and the reply this round posts, with the
- respond gate's own post/hold and resolve controls. */
+ said back, the agent's check, and the reply this round posts. */
export function CarryoverCard({
pick,
form,
+ author,
+ you,
}: {
pick: CarryoverPick;
form: GateFormState;
+ /** The MR author's name, who the reviewer's thread is with. */
+ author: string;
+ /** The reviewer's username, for the avatar beside the reply. */
+ you?: string;
}) {
const { carry } = pick;
const posting = isPosting(pick, form.selections);
- const call = CARRYOVER_CALL[carry.call];
return (
-
-
- {pick.label}
-
- {call.text}
-
-
-
-
- {carry.authorReply ? (
-
- ) : (
-
- The author hasn't replied in this thread.
-
- )}
- {carry.note && (
-
form.toggleMulti(pick.name, pick.post, on)}
+ resolve={
+ pick.resolve
+ ? {
+ on: isResolving(pick, form.selections),
+ onChange: on => form.toggleMulti(pick.name, pick.resolve!, on),
}
- >
-
-
-
- agent's check
- {carry.note}
-
- )}
-
form.setText(pick.name, t)}
- onReset={() => form.clearText(pick.name)}
- controls={ }
- />
-
-
+ : undefined
+ }
+ onChange={t => form.setText(pick.name, t)}
+ onReset={() => form.clearText(pick.name)}
+ />
);
}
diff --git a/apps/board/src/client/board/SheetParts.tsx b/apps/board/src/client/board/SheetParts.tsx
index dc7574aa0e..d94a24b4bf 100644
--- a/apps/board/src/client/board/SheetParts.tsx
+++ b/apps/board/src/client/board/SheetParts.tsx
@@ -1,4 +1,7 @@
import type { ReactNode } from 'react';
+import { useEffect, useRef, useState } from 'react';
+import { CheckCheck } from 'lucide-react';
+import { domAnimation, LazyMotion, m, MotionConfig } from 'motion/react';
import type { GateItemDisplay } from '@mattstack/gate-kit/react';
import { Button, Chip, Markdown } from '@mattstack/tui-kit';
@@ -274,31 +277,24 @@ function SheetLost({
);
}
-/** The round a review or respond gate is on, as the first thing the main
- column says, with a one-line account of what this round holds. */
-function RoundHeading({
- round,
- revise,
- summary,
+/** A review or respond sheet's sticky band: what the gate asks of you on
+ the left, where it sits in the review loop at the right edge. */
+function SheetHeading({
+ icon,
+ title,
+ aside,
}: {
- round?: number;
- /** Re-adjudications past the round, shown after it. */
- revise?: number;
- summary?: string;
+ icon: ReactNode;
+ title: string;
+ aside?: ReactNode;
}) {
- const text = [
- round !== undefined ? `Round ${round}` : undefined,
- revise !== undefined
- ? `${round !== undefined ? 'revise' : 'Revise'} ${revise}`
- : undefined,
- ]
- .filter(Boolean)
- .join(' · ');
- if (!text) return null;
return (
-
{text}
- {summary &&
{summary} }
+
+ {icon}
+ {title}
+
+ {aside &&
{aside}
}
);
}
@@ -326,9 +322,98 @@ export {
Note,
ProseContext,
reserveDock,
- RoundHeading,
+ SheetHeading,
SheetLost,
SheetRows,
SUBMIT_KEYS,
};
export type { ChoiceState, RowChip, SheetRow };
+
+/** The nearest ancestor that scrolls: the findings column on a wide
+ screen, the whole sheet once it stacks. */
+function scrollParent(el: HTMLElement): HTMLElement | null {
+ for (let p = el.parentElement; p; p = p.parentElement) {
+ const y = getComputedStyle(p).overflowY;
+ if (y === 'auto' || y === 'scroll') return p;
+ }
+ return null;
+}
+
+/** A sheet list's last line. It rises in as it scrolls into view, the check
+ popping a beat after, and resets when it leaves, so every arrival at
+ the bottom plays it again. */
+export function SheetEnd({ children }: { children: ReactNode }) {
+ const slot = useRef(null);
+ // Shown only when the list overflows their scroller; the line's own
+ // height is left out of that sum, so it never shows just to scroll to
+ // itself.
+ const [overflows, setOverflows] = useState(false);
+ useEffect(() => {
+ const el = slot.current;
+ const scroller = el && scrollParent(el);
+ if (!el || !scroller) return;
+ const measure = () =>
+ setOverflows(
+ scroller.scrollHeight - el.offsetHeight > scroller.clientHeight + 1
+ );
+ const sizes = new ResizeObserver(measure);
+ const watch = () => {
+ sizes.disconnect();
+ sizes.observe(scroller);
+ for (const child of scroller.children) sizes.observe(child);
+ };
+ const children = new MutationObserver(watch);
+ children.observe(scroller, { childList: true });
+ watch();
+ measure();
+ return () => {
+ sizes.disconnect();
+ children.disconnect();
+ };
+ }, []);
+ return (
+
+ {overflows && (
+
+
+
+
+
+
+ {children}
+
+
+
+ )}
+
+ );
+}
diff --git a/apps/board/src/client/board/ThreadReplyCard.tsx b/apps/board/src/client/board/ThreadReplyCard.tsx
new file mode 100644
index 0000000000..08ea3bb810
--- /dev/null
+++ b/apps/board/src/client/board/ThreadReplyCard.tsx
@@ -0,0 +1,178 @@
+import type { ReactNode } from 'react';
+
+import { Chip, ICONS } from '@mattstack/tui-kit';
+import { commentState } from './CommentComposer.tsx';
+import { CommentMarkdown } from './CommentMarkdown.tsx';
+import {
+ AgentCheck,
+ Answer,
+ CardTitle,
+ CommentArea,
+ DraftField,
+ GateCard,
+ SaidBox,
+ StatusMark,
+ ThreadCode,
+ type AgentVerdict,
+ type StatusHue,
+} from './GateParts.tsx';
+import { CommentAuthor } from './PersonLead.tsx';
+
+export interface ThreadMessage {
+ /** Absent on the viewer's own message, which needs no name. */
+ who?: string;
+ /** The username their invader is generated from. */
+ whoId?: string;
+ text: string;
+}
+
+export interface ThreadReplyCardProps {
+ /** The thread's file:line, or "General thread". */
+ label: string;
+ status: { text: string; hue: StatusHue };
+ /** The fix commit a respond reply points at. */
+ sha?: string;
+ /** The review round the thread was opened in. */
+ round?: number;
+ /** Beside the title, as the plan card shows the thread's severity. */
+ tag?: ReactNode;
+ /** `conversation`: the thread reads as who said what above the reply (a
+ re-review). `stacked`: the agent's check, the comment being answered
+ in its box, and the reply hung under it (a respond post). */
+ layout?: 'conversation' | 'stacked';
+ /** The MR, for the code a file:line thread was left on. */
+ mr?: { url?: string; ref?: string; revision?: string };
+ messages: ThreadMessage[];
+ /** Your username, for the invader beside your reply. */
+ selfId?: string;
+ /** Said in place of the other side's message when they never replied. */
+ silence?: string;
+ check?: { note: string; verdict: AgentVerdict };
+ draft: string;
+ value: string | undefined;
+ posting: boolean;
+ onPosting: (on: boolean) => void;
+ /** Absent when the thread cannot be resolved (an Overview comment). */
+ resolve?: { on: boolean; onChange: (on: boolean) => void };
+ onChange: (text: string) => void;
+ onReset: () => void;
+}
+
+function ResolveTick({
+ label,
+ resolve,
+}: {
+ label: string;
+ resolve: NonNullable;
+}) {
+ return (
+
+ resolve.onChange(e.currentTarget.checked)}
+ />
+ Resolve thread
+
+ );
+}
+
+/** A reply on a review thread, in the finding card: the head's tick posts
+ the reply, and the reply is the click-to-edit comment. Unticked, the
+ card folds to its head, which keeps the resolve tick so a thread can
+ close without a reply. */
+export function ThreadReplyCard(p: ThreadReplyCardProps) {
+ const { edited } = commentState(p.draft, p.value, true);
+ const label = `Reply on ${p.label}`;
+ const stacked = p.layout === 'stacked';
+ const said = stacked ? p.messages.find(m => m.who) : undefined;
+ const field = (
+ }
+ footer={p.resolve && }
+ onChange={p.onChange}
+ onReset={p.onReset}
+ />
+ );
+ return (
+
+
+ {p.round !== undefined && (
+
+ Round {p.round}
+
+ )}
+
+ {p.tag}
+
+ {p.sha && {p.sha}}
+
+ >
+ }
+ aside={
+ !p.posting &&
+ p.resolve &&
+ }
+ >
+
+ {!stacked && (p.messages.length > 0 || p.silence) && (
+
+ {p.messages.map((msg, i) => (
+
+ ))}
+ {p.silence && (
+
+
+ {p.silence}
+
+
+ )}
+
+ )}
+
+ {p.check && }
+ {said && (
+
+ )}
+
+ {said ?
{field} : field}
+
+
+
+ );
+}
+
+function Message({ who, text }: ThreadMessage): ReactNode {
+ return (
+
+ {who &&
{who} }
+
+ {text}
+
+
+ );
+}
+
+export type { AgentVerdict };
diff --git a/apps/board/src/client/board/__tests__/comment-field-dom.test.tsx b/apps/board/src/client/board/__tests__/comment-field-dom.test.tsx
deleted file mode 100644
index 32db8a14d3..0000000000
--- a/apps/board/src/client/board/__tests__/comment-field-dom.test.tsx
+++ /dev/null
@@ -1,336 +0,0 @@
-/** Every comment field (a review comment, a respond reply, the summary)
- carries Write and Preview tabs: Preview renders the same text as
- Markdown, and in Write a Tab indents by two spaces while Shift-Tab and
- Escape still leave the field. */
-
-import React, { useState } from 'react';
-import { GlobalRegistrator } from '@happy-dom/global-registrator';
-import { afterAll, afterEach, beforeEach, expect, test } from 'bun:test';
-import { createRoot, type Root } from 'react-dom/client';
-
-import { CommentComposer } from '../CommentComposer.tsx';
-import { DiffSourceContext, type DiffSource } from '../diff-source.ts';
-
-GlobalRegistrator.register({ url: 'http://localhost/' });
-
-afterAll(async () => {
- await GlobalRegistrator.unregister();
-});
-
-(
- globalThis as unknown as { IS_REACT_ACT_ENVIRONMENT: boolean }
-).IS_REACT_ACT_ENVIRONMENT = true;
-
-let container: HTMLDivElement;
-let root: Root;
-let latest: string | undefined;
-
-beforeEach(() => {
- container = document.createElement('div');
- document.body.appendChild(container);
- root = createRoot(container);
- latest = undefined;
-});
-
-afterEach(async () => {
- await React.act(async () => root.unmount());
- container.remove();
-});
-
-function Host({ draft }: { draft: string }) {
- const [value, setValue] = useState(undefined);
- return (
-
- {
- latest = t;
- setValue(t);
- }}
- onReset={() => setValue(undefined)}
- />
-
- );
-}
-
-const box = () =>
- container.querySelector('textarea.tui-composer-input');
-const tab = (name: string) =>
- [...container.querySelectorAll('.tui-field-tab')].find(
- b => b.textContent === name
- )!;
-const key = (el: Element, init: KeyboardEventInit) =>
- React.act(async () => {
- el.dispatchEvent(new KeyboardEvent('keydown', { bubbles: true, ...init }));
- });
-
-test('the tabs are Write and Preview, with Write selected', async () => {
- await React.act(async () => root.render( ));
- const list = container.querySelector('.tui-field-tabs')!;
- expect(list.getAttribute('aria-label')).toBe('Comment on finding 1: view');
- expect(
- [...list.querySelectorAll('.tui-field-tab')].map(t => [
- t.textContent,
- t.getAttribute('aria-pressed'),
- ])
- ).toEqual([
- ['Write', 'true'],
- ['Preview', 'false'],
- ]);
- expect(box()).not.toBeNull();
-});
-
-test('Preview renders the text as Markdown, and Write brings the field back', async () => {
- await React.act(async () =>
- root.render( )
- );
- await React.act(async () => tab('Preview').click());
- expect(box()).toBeNull();
- const preview = container.querySelector('.tui-field-preview')!;
- expect(preview.getAttribute('aria-label')).toBe(
- 'Comment on finding 1: preview'
- );
- expect(preview.querySelector('code')!.textContent).toBe('requireActual');
- expect([...preview.querySelectorAll('li')].map(li => li.textContent)).toEqual(
- ['one', 'two']
- );
- expect(tab('Preview').getAttribute('aria-pressed')).toBe('true');
- await React.act(async () => tab('Write').click());
- expect(box()!.value).toBe('Use `requireActual` here:\n\n- one\n- two');
-});
-
-test('Tab in the field inserts two spaces at the caret and stays in the field', async () => {
- await React.act(async () => root.render( ));
- const el = box()!;
- el.focus();
- el.setSelectionRange(1, 1);
- await key(el, { key: 'Tab' });
- expect(latest).toBe('a b');
- expect(box()!.value).toBe('a b');
- expect(document.activeElement).toBe(box());
- expect(box()!.selectionStart).toBe(3);
-});
-
-test('Tab replaces a selection with the two spaces', async () => {
- await React.act(async () => root.render( ));
- const el = box()!;
- el.focus();
- el.setSelectionRange(1, 3);
- await key(el, { key: 'Tab' });
- expect(latest).toBe('a d');
-});
-
-test('Shift-Tab is left to the browser, so it leaves the field', async () => {
- await React.act(async () => root.render( ));
- const el = box()!;
- const ev = new KeyboardEvent('keydown', {
- key: 'Tab',
- shiftKey: true,
- bubbles: true,
- cancelable: true,
- });
- await React.act(async () => {
- el.dispatchEvent(ev);
- });
- expect(ev.defaultPrevented).toBe(false);
- expect(latest).toBeUndefined();
-});
-
-test("Escape in the field moves focus to the field's own Write tab, inside the sheet", async () => {
- await React.act(async () => root.render( ));
- const el = box()!;
- el.focus();
- await key(el, { key: 'Escape' });
- expect(document.activeElement).toBe(tab('Write'));
- expect(
- container.querySelector('[role="dialog"]')!.contains(document.activeElement)
- ).toBe(true);
-});
-
-test('reset while in Preview goes back to Write and focuses the field', async () => {
- await React.act(async () => root.render( ));
- const el = box()!;
- el.focus();
- el.setSelectionRange(1, 1);
- await key(el, { key: 'Tab' });
- await React.act(async () => tab('Preview').click());
- expect(box()).toBeNull();
- await React.act(async () =>
- container
- .querySelector(
- 'button[aria-label="Comment on finding 1: reset to draft"]'
- )!
- .click()
- );
- expect(tab('Write').getAttribute('aria-pressed')).toBe('true');
- expect(box()!.value).toBe('ab');
- expect(document.activeElement).toBe(box());
-});
-
-test('a read-only held field takes no Tab', async () => {
- await React.act(async () =>
- root.render(
- (latest = t)}
- onReset={() => {}}
- />
- )
- );
- const el = box()!;
- await key(el, { key: 'Tab' });
- expect(latest).toBeUndefined();
-});
-
-test('a strip needs a kicker or a where: the types refuse neither', () => {
- const bare = (
- // @ts-expect-error neither kicker nor where names the strip
- {}}
- onReset={() => {}}
- />
- );
- expect(bare).toBeDefined();
-});
-
-const RICH = [
- 'Small one: `requireActual` drops `__esModule`.',
- '',
- '- [x] spread it',
- '- set it back',
- '',
- '> the old mock read it directly',
- '',
- '| file | change |',
- '| --- | --- |',
- '| a.ts | +2 |',
- '',
- '```ts',
- 'const mod = jest.requireActual("./config");',
- '```',
- '',
- '```suggestion:-0+0',
- 'export default { ...mod, __esModule: true };',
- '```',
-].join('\n');
-
-async function preview(draft: string, diffs?: DiffSource) {
- const host = ;
- await React.act(async () =>
- root.render(
- diffs ? (
-
- {host}
-
- ) : (
- host
- )
- )
- );
- await React.act(async () => tab('Preview').click());
- return container.querySelector('.tui-field-preview')!;
-}
-
-test('Preview reads like the comment will: inline code, task list, quote, table', async () => {
- const panel = await preview(RICH);
- expect(panel.querySelector('p code')!.textContent).toBe('requireActual');
- expect(
- panel.querySelector('.task-list-item input')!.checked
- ).toBe(true);
- expect(panel.querySelector('blockquote')!.textContent).toContain(
- 'the old mock read it directly'
- );
- expect(panel.querySelector('td')!.textContent).toBe('a.ts');
-});
-
-test('a fenced block shows its language over the code, and plain without a highlighter', async () => {
- const panel = await preview(RICH);
- const block = panel.querySelector(
- '.tui-md-code[data-lang="ts"]'
- )!;
- expect(block.querySelector('.tui-md-code-head')!.textContent).toBe(
- 'TypeScript'
- );
- expect(block.querySelector('pre code')!.textContent).toBe(
- 'const mod = jest.requireActual("./config");'
- );
- expect(block.querySelector('.tui-md-code-hl')).toBeNull();
-});
-
-test('a fence named after an Object.prototype key shows that name, not the prototype', async () => {
- const panel = await preview('```__proto__\nx\n```\n\n```constructor\ny\n```');
- expect(
- [...panel.querySelectorAll('.tui-md-code-head')].map(h => h.textContent)
- ).toEqual(['__proto__', 'constructor']);
-});
-
-test('a suggestion fence reads as a suggested change', async () => {
- const panel = await preview(RICH);
- const block = panel.querySelector(
- '.tui-md-code[data-suggestion]'
- )!;
- expect(block.querySelector('.tui-md-code-head')!.textContent).toBe(
- 'Suggested change'
- );
- expect(block.textContent).toContain('__esModule: true');
-});
-
-test('with a source that brings the renderer, a fence is highlighted in its language', async () => {
- const seen: string[] = [];
- const diffs: DiffSource = {
- fetchFileDiff: async () => null,
- loadView: async () =>
- ({
- default: () => null,
- CodeBlock: ({ lang, code }: { lang: string; code: string }) => {
- seen.push(lang);
- return {code} ;
- },
- }) as unknown as typeof import('../DiffView.tsx'),
- };
- const panel = await preview(RICH, diffs);
- await React.act(async () => {
- await new Promise(r => setTimeout(r, 0));
- });
- expect(panel.querySelector('[data-highlighted="ts"]')!.textContent).toBe(
- 'const mod = jest.requireActual("./config");'
- );
- expect(seen).toEqual(['ts']);
- expect(
- panel.querySelector('.tui-md-code[data-suggestion] [data-highlighted]')
- ).toBeNull();
-});
-
-test('empty text previews as a muted note', async () => {
- const panel = await preview(' ');
- expect(panel.querySelector('.tui-field-preview-empty')!.textContent).toBe(
- 'Nothing to preview'
- );
-});
-
-test('Preview keeps at least the Write field height', async () => {
- Object.defineProperty(HTMLTextAreaElement.prototype, 'offsetHeight', {
- configurable: true,
- get: () => 120,
- });
- try {
- const panel = await preview('one line');
- expect(panel.style.getPropertyValue('--field-h')).toBe('120px');
- } finally {
- delete (HTMLTextAreaElement.prototype as { offsetHeight?: number })
- .offsetHeight;
- }
-});
diff --git a/apps/board/src/client/board/__tests__/person-round.test.ts b/apps/board/src/client/board/__tests__/person-round.test.ts
new file mode 100644
index 0000000000..3c20eb5cac
--- /dev/null
+++ b/apps/board/src/client/board/__tests__/person-round.test.ts
@@ -0,0 +1,36 @@
+import { describe, expect, test } from 'bun:test';
+
+import type { BoardMRWithReview } from '../../types.ts';
+import { personRound } from '../RespondGateHeader.tsx';
+
+const mr = (m: object) => m as unknown as BoardMRWithReview;
+
+describe("a respond gate's own response round", () => {
+ test("a gate opened during this person's live run reads the run's round", () => {
+ const m = mr({
+ respond: { person: 'renee', startedAt: 100, responseRound: 3 },
+ respondRounds: { renee: 3 },
+ });
+ expect(personRound(150, 'renee', m)).toBe(3);
+ });
+
+ test("another person's live run never lends its round", () => {
+ const m = mr({
+ respond: { person: 'john', startedAt: 100, responseRound: 3 },
+ respondRounds: { john: 3, renee: 1 },
+ });
+ expect(personRound(50, 'renee', m)).toBe(1);
+ });
+
+ test('a gate parked from before the live run reads the recorded count', () => {
+ const m = mr({
+ respond: { person: 'renee', startedAt: 100, responseRound: 3 },
+ respondRounds: { renee: 3 },
+ });
+ expect(personRound(50, 'renee', m)).toBe(3);
+ });
+
+ test('a person with no recorded round has none', () => {
+ expect(personRound(50, 'renee', mr({}))).toBeUndefined();
+ });
+});
diff --git a/apps/board/src/client/board/__tests__/respond-header-dom.test.tsx b/apps/board/src/client/board/__tests__/respond-header-dom.test.tsx
index 36eb3b8d27..3e6526a3a5 100644
--- a/apps/board/src/client/board/__tests__/respond-header-dom.test.tsx
+++ b/apps/board/src/client/board/__tests__/respond-header-dom.test.tsx
@@ -134,32 +134,54 @@ const planAtRound = (round: number) =>
const withRespond = (respond: Record) =>
({ ...MR, respond }) as unknown as BoardMRWithReview;
-test('the sheet heading splits the wrapper round like the header: response round, then revises', async () => {
+const roundChip = () => $('.tui-tl .tui-tl-round-chip')!;
+
+test('the sheet heading names the person; its timeline carries the response round, the rail the revises', async () => {
await renderModal(
planAtRound(3),
withRespond({ status: 'triaging', person: 'john', responseRound: 2 })
);
- expect($('.tui-sheet-round-n')!.textContent).toBe('Round 2 \u00b7 revise 1');
+ expect($('.tui-sheet-round-n')!.textContent).toBe('Respond to john');
+ expect(roundChip().textContent).toBe('Round 2');
expect($('.tui-sheet-context-meta')!.textContent).toContain(
'round 2 \u00b7 revise 1'
);
});
-test('the sheet heading stays out of a first round with no revises, and shows revises alone', async () => {
- const first = withRespond({
- status: 'triaging',
- person: 'john',
- responseRound: 1,
- });
- await renderModal(planAtRound(1), first);
- expect($('.tui-sheet-round-n')).toBeNull();
- await renderModal(planAtRound(2), first);
- expect($('.tui-sheet-round-n')!.textContent).toBe('Revise 1');
+test('the round chip opens the earlier rounds only once there is more than one', async () => {
+ await renderModal(
+ planAtRound(2),
+ withRespond({ status: 'triaging', person: 'john', responseRound: 1 })
+ );
+ expect(roundChip().tagName).toBe('SPAN');
+ expect(roundChip().textContent).toBe('Round 1');
+ await renderModal(
+ planAtRound(1),
+ withRespond({ status: 'triaging', person: 'john', responseRound: 2 })
+ );
+ expect(roundChip().tagName).toBe('BUTTON');
+ expect(roundChip().hasAttribute('data-menu')).toBe(true);
+ expect(roundChip().textContent).toBe('Round 2');
});
-test('the sheet heading reads the wrapper round when no response round is known', async () => {
+test('the timeline reads the wrapper round when no response round is known', async () => {
await renderModal(planAtRound(3), withRespond({ status: 'triaging' }));
- expect($('.tui-sheet-round-n')!.textContent).toBe('Round 3');
+ expect(roundChip().textContent).toBe('Round 3');
+});
+
+test('a plan gate sits on Plan in the timeline: the review done, then your response', async () => {
+ await renderModal(planAtRound(1), withRespond({ status: 'triaging' }));
+ const steps = [...document.body.querySelectorAll('.tui-tl .tui-tl-step')].map(
+ li => [li.textContent, li.getAttribute('data-state')]
+ );
+ expect(steps).toEqual([
+ ['renee reviewed', 'done'],
+ ['Plan', 'current'],
+ ['Fix', 'next'],
+ ['Post', 'next'],
+ ]);
+ expect($('.tui-tl-response-label')!.textContent).toBe('Your response');
+ expect($('.tui-sheet-main .tui-phase')).toBeNull();
});
test('an open plan@1 gate opens the respond sheet: threads left; MR card, decision context, status and responses right', async () => {
@@ -186,12 +208,15 @@ test('an open plan@1 gate opens the respond sheet: threads left; MR card, decisi
expect($('.tui-sheet-main .tui-mr-card')).toBeNull();
expect($('.tui-sheet-rail [data-card="responses"]')).not.toBeNull();
expect($('.tui-sheet-rail [data-card="mr-status"]')).not.toBeNull();
+ expect($('.tui-sheet-main .tui-sheet-round-n')!.textContent).toBe(
+ 'Respond to renee'
+ );
expect(
document.body.querySelectorAll(
- '.tui-sheet-main .tui-respond-list > .tui-gate-question'
+ '.tui-sheet-main .tui-respond-list > .tui-plan-card'
)
).toHaveLength(1);
- expect($('.tui-sheet-main .tui-thread-card')).not.toBeNull();
+ expect($('.tui-sheet-main .tui-plan-said')).not.toBeNull();
expect($('.tui-triage-sheet')).toBeNull();
const text = document.body.textContent ?? '';
expect(text).not.toContain('gate-ctx');
diff --git a/apps/board/src/client/board/__tests__/respond-sheet-dom.test.tsx b/apps/board/src/client/board/__tests__/respond-sheet-dom.test.tsx
index eaf9a0241b..0936ac208c 100644
--- a/apps/board/src/client/board/__tests__/respond-sheet-dom.test.tsx
+++ b/apps/board/src/client/board/__tests__/respond-sheet-dom.test.tsx
@@ -8,6 +8,7 @@
import React from 'react';
import { GlobalRegistrator } from '@happy-dom/global-registrator';
import { afterAll, afterEach, beforeEach, expect, test } from 'bun:test';
+import { MotionGlobalConfig } from 'motion/react';
import { createRoot, type Root } from 'react-dom/client';
import type { GateRow } from '../../../gates/store.ts';
@@ -19,6 +20,13 @@ import {
} from './fake-resize-observer.ts';
GlobalRegistrator.register({ url: 'http://localhost/' });
+// happy-dom's Web Animations stub rejects a cancelled animation, which motion
+// raises as an unhandled error when a card folds; without `animate` motion
+// falls back to its own frame loop.
+delete (Element.prototype as { animate?: unknown }).animate;
+// Folds finish at once, so an unticked card's body leaves the DOM in the
+// same act() rather than after an exit animation.
+MotionGlobalConfig.skipAnimations = true;
afterAll(async () => {
await GlobalRegistrator.unregister();
@@ -192,25 +200,51 @@ async function render(row: GateRow, mr: BoardMRWithReview = MR) {
const $ = (selector: string) => document.body.querySelector(selector);
+const planCards = () => [
+ ...document.body.querySelectorAll('.tui-plan-card'),
+];
+
+/** A plan thread is picked on its card: the head's tick keeps it (unticked
+ is skip), and the switch inside chooses fix or reply. Any other choice
+ is an input carrying its value. */
async function pick(value: string) {
const input = $(`input[value="${value}"]`) as HTMLInputElement | null;
- if (!input) throw new Error(`no choice ${value}`);
+ if (input) {
+ await React.act(async () => {
+ input.click();
+ });
+ return;
+ }
+ const m = /^(fix|reply|skip):t(\d+)$/.exec(value);
+ const card = m ? planCards()[Number(m[2]) - 1] : undefined;
+ if (!m || !card) throw new Error(`no choice ${value}`);
+ const tick = card.querySelector(
+ '.tui-reply-head input[type="checkbox"]'
+ )!;
+ if ((m[1] === 'skip') === tick.checked)
+ await React.act(async () => {
+ tick.click();
+ });
+ if (m[1] === 'skip') return;
+ const label = m[1] === 'fix' ? 'Fix the code' : 'Reply only';
+ const seg = [
+ ...card.querySelectorAll(
+ '[data-part="segmented-option"]'
+ ),
+ ].find(b => b.textContent === label);
+ if (!seg) throw new Error(`no switch ${label} for ${value}`);
await React.act(async () => {
- input.click();
+ seg.click();
});
}
-async function type(label: string, text: string) {
- const input = $(`input[aria-label="${label}"]`) as HTMLInputElement;
- await React.act(async () => {
- const setter = Object.getOwnPropertyDescriptor(
- HTMLInputElement.prototype,
- 'value'
- )!.set!;
- setter.call(input, text);
- input.dispatchEvent(new Event('input', { bubbles: true }));
- });
-}
+/** A note no longer has a control on the plan card; one restored from a
+ saved draft still rides the answer. */
+const seedNotes = (gateId: string, notes: Record) =>
+ localStorage.setItem(
+ `gate-kit:draft:${gateId}`,
+ JSON.stringify({ selections: {}, notes, texts: {}, item: null })
+ );
const submit = () => $('.tui-sheet-submit') as HTMLButtonElement;
@@ -223,38 +257,85 @@ async function clickSubmit() {
const answer = () => posts.find(p => p.url === '/gate/answer')?.body;
-test('every thread renders at once in the main column', async () => {
+test('every thread renders at once in the main column, each on its recommended pick', async () => {
await render(planGate());
expect(
- document.body.querySelectorAll('.tui-respond-list > .tui-gate-question')
+ document.body.querySelectorAll('.tui-respond-list > .tui-plan-card')
).toHaveLength(2);
- expect($('.tui-sheet-list-tally')!.textContent).toBe('0 of 2 decided');
+ expect($('.tui-sheet-list-tally')!.textContent).toBe('2 of 2 decided');
});
-test("each thread's choices form one named radio group that holds only its radios", async () => {
+test("each thread's card is named by its thread and holds one fix-or-reply switch", async () => {
await render(planGate());
- const groups = [...document.body.querySelectorAll('[role="radiogroup"]')];
- expect(groups.map(g => g.getAttribute('aria-label'))).toEqual(
+ const cards = planCards();
+ expect(cards.map(c => c.getAttribute('aria-label'))).toEqual(
planGate()
.questions.slice(0, 2)
.map(q => q.label)
);
- for (const g of groups) {
- expect(g.querySelectorAll('input[type="radio"]').length).toBe(3);
- expect(g.querySelector('input:not([type="radio"]), textarea')).toBeNull();
+ for (const card of cards) {
+ const label = card.getAttribute('aria-label');
+ expect(
+ card
+ .querySelector('.tui-reply-head input[type="checkbox"]')!
+ .getAttribute('aria-label')
+ ).toBe(`${label}: respond`);
+ const groups = card.querySelectorAll('[role="group"]');
+ expect(groups).toHaveLength(1);
+ expect(groups[0]!.getAttribute('aria-label')).toBe(
+ `Fix or reply on ${label}`
+ );
+ expect(
+ [...groups[0]!.querySelectorAll('button')].map(b => b.textContent)
+ ).toEqual(['Fix the code', 'Reply only']);
+ expect(card.querySelector('input[type="radio"], textarea')).toBeNull();
}
});
-test('submit stays disabled until every thread is decided', async () => {
+test('every thread opens decided, so submit is live from the start', async () => {
await render(planGate());
- expect(submit().disabled).toBe(true);
+ expect(submit().disabled).toBe(false);
+ expect(submit().textContent).toBe('submit · 2 fixes');
await pick('reply:t1');
- expect(submit().disabled).toBe(true);
await pick('reply:t2');
expect(submit().disabled).toBe(false);
expect(submit().textContent).toBe('submit · 2 replies');
});
+test('reset on the plan sheet puts every thread back on its recommended pick', async () => {
+ await render(planGate());
+ await pick('reply:t1');
+ await pick('skip:t2');
+ await click($('.tui-sheet-reset'));
+ expect($('.tui-sheet-list-tally')!.textContent).toBe('2 of 2 decided');
+ expect(submit().disabled).toBe(false);
+ expect(submit().textContent).toBe('submit · 2 fixes');
+});
+
+test('a thread the agent recommends skipping opens skipped, and reset skips it again', async () => {
+ const gate = planGate();
+ const q2 = gate.questions[1]!;
+ gate.questions[1] = {
+ ...q2,
+ options: [
+ { value: 'reply:t2', label: 'reply' },
+ { value: 'fix:t2', label: 'fix' },
+ { value: 'skip:t2', label: 'skip (Recommended)' },
+ ],
+ };
+ await render(gate);
+ const tick = () =>
+ planCards()[1]!.querySelector(
+ '.tui-reply-head input[type="checkbox"]'
+ ) as HTMLInputElement;
+ expect(tick().checked).toBe(false);
+ expect(submit().textContent).toBe('submit · 1 fix · 1 skip');
+ await pick('fix:t2');
+ await click($('.tui-sheet-reset'));
+ expect(tick().checked).toBe(false);
+ expect(submit().textContent).toBe('submit · 1 fix · 1 skip');
+});
+
async function pressSubmit() {
await React.act(async () => {
$('.tui-gate-sheet')!.dispatchEvent(
@@ -268,19 +349,22 @@ async function pressSubmit() {
});
}
-test('Cmd+Enter submits once every thread is decided, and not before', async () => {
- await render(planGate());
+test('Cmd+Enter submits, and not while submit is off', async () => {
+ const gate = planGate();
+ gate.questions[2] = { ...gate.questions[2]!, options: ['approve', 'revise'] };
+ await render(gate);
await pick('reply:t1');
+ await pick('reply:t2');
await pressSubmit();
expect(answer()).toBeUndefined();
- await pick('reply:t2');
+ await pick('approve');
await pressSubmit();
expect(answer()).toEqual({
gateId: 'g-plan',
answers: {
'thread-1': 'reply:t1',
'thread-2': 'reply:t2',
- 'code-changes': 'skip',
+ 'code-changes': 'approve',
},
});
});
@@ -364,11 +448,11 @@ test('withdrawing the only fix drops code-changes back to the sentinel', async (
});
});
-test("a thread's note travels trimmed with its pick", async () => {
+test("a thread's restored note travels trimmed with its pick", async () => {
+ seedNotes('g-plan', { 'thread-1': ' say which test ' });
await render(planGate());
await pick('reply:t1');
await pick('reply:t2');
- await type('Note for queue/enqueue.ts:10', ' say which test ');
await clickSubmit();
expect(answer()).toMatchObject({
answers: {
@@ -427,7 +511,7 @@ test('sending the plan back needs a reason and posts revise with it', async () =
gateId: 'g-plan',
answers: {
'thread-1': 'fix:t1',
- 'thread-2': 'reply:t2',
+ 'thread-2': 'fix:t2',
'code-changes': {
value: 'revise',
note: 'split the fix into its own MR',
@@ -458,7 +542,7 @@ test('cancel leaves send-back mode and restores the submit', async () => {
expect(
$('textarea[aria-label="What should the new plan change?"]')
).toBeNull();
- expect(submit().textContent).toBe('submit');
+ expect(submit().textContent).toBe('submit · 2 fixes');
});
test('a replies gate offers no send-back action', async () => {
@@ -643,11 +727,22 @@ const withFixPlan = (text?: string) => {
return { ...MR, gates: [plan] } as unknown as BoardMRWithReview;
};
+/** Every card on the post step in order: a reply card, or a plan thread
+ with nothing for this gate to decide. */
const postCards = () => [
- ...document.body.querySelectorAll('[data-step="post"]'),
+ ...document.body.querySelectorAll(
+ '.tui-respond-list > article.tui-reply-card, .tui-respond-list > [data-step="post"]'
+ ),
];
+/** A reply card's post tick is its head's, and its resolve tick sits in
+ the field's footer, or in the head once the card folds; a card whose
+ reply did not fit the gate keeps the plain ticks. */
const control = (card: HTMLElement, value: 'post' | 'resolve') =>
- card.querySelector(`input[value="${value}"]`);
+ card.querySelector(
+ `input[aria-label="${card.getAttribute('aria-label')}: ${
+ value === 'post' ? 'post reply' : 'resolve thread'
+ }"]`
+ ) ?? card.querySelector(`input[value="${value}"]`);
/** Holding a reply is unticking its post box; a held reply stays held. */
const holdReply = (card: HTMLElement) =>
React.act(async () => {
@@ -750,51 +845,42 @@ test('without its plan gate, each per-thread question still draws its reply and
expect(cards[0]!.textContent).toContain(
'Fixed -- a.ts:1 now guards the retry.'
);
- expect(cards[0]!.querySelector('[data-outcome]')?.textContent).toBe('fixed');
+ expect(cards[0]!.querySelector('.tui-reply-status')?.textContent).toBe(
+ 'Fixedab12cd3'
+ );
expect(control(cards[1]!, 'resolve')?.checked).toBe(false);
expect($('.tui-sheet-list-tally')!.textContent).toBe('2 of 2 posting');
});
-test('without its plan gate, each thread is one composer frame: strip, field, footer ticks', async () => {
+test('without its plan gate, each thread is one reply card: head tick and status, your reply, resolve in its footer', async () => {
await render(perThreadPostGate());
const [fix] = postCards();
- expect(fix!.getAttribute('data-frame')).toBe('composer');
+ expect(fix!.tagName).toBe('ARTICLE');
expect(fix!.querySelector('.tui-thread-card')).toBeNull();
- expect(fix!.querySelector('.tui-gate-question-head')).toBeNull();
- const strip = fix!.querySelector('.tui-composer-strip')!;
- const where = strip.querySelector('.tui-composer-where')!;
- expect(where.textContent).toBe('a.ts:1');
- expect(where.hasAttribute('data-mono')).toBe(true);
- expect(strip.querySelector('[data-outcome]')!.textContent).toBe('fixed');
- expect(strip.querySelector('.tui-composer-meta')!.textContent).toBe(
- 'posts as you'
+ const head = fix!.querySelector('.tui-reply-head')!;
+ const title = head.querySelector('.tui-reply-title')!;
+ expect(title.textContent).toBe('a.ts:1');
+ expect(title.hasAttribute('data-file')).toBe(true);
+ expect(head.querySelector('.tui-reply-status')!.textContent).toBe(
+ 'Fixedab12cd3'
);
- const foot = fix!.querySelector('.tui-composer .tui-composer-foot')!;
- expect(
- [...foot.querySelectorAll('.tui-post-tick')].map(t => t.textContent)
- ).toEqual(['Post reply', 'Resolve thread']);
- expect(foot.querySelector('input[value="post"]')).toBe(control(fix!, 'post'));
+ expect(head.querySelector('input')).toBe(control(fix!, 'post'));
+ expect(fix!.querySelector('.tui-cte-lead')!.textContent).toBe('You:');
+ const foot = fix!.querySelector('.tui-cte-footer')!;
+ expect(foot.textContent).toBe('Resolve thread');
+ expect(foot.querySelector('input')).toBe(control(fix!, 'resolve'));
});
-test('an unticked Post greys the strip and says the reply is not posting', async () => {
+test('an unticked post tick folds the card to its head, which keeps the resolve tick', async () => {
await render(perThreadPostGate());
const card = () => postCards()[1]!;
await holdReply(card());
- const frame = card().querySelector('.tui-composer')!;
- expect(frame.hasAttribute('data-held')).toBe(true);
- expect(frame.querySelector('.tui-composer-meta')!.textContent).toBe(
- 'not posting'
- );
- expect(card().querySelector('.tui-composer-where')!.textContent).toBe(
- 'b.ts:2'
- );
- const box = replyBox(card())!;
- expect(box.readOnly).toBe(true);
- const named = box
- .getAttribute('aria-describedby')!
- .split(' ')
- .map(id => document.getElementById(id)?.textContent);
- expect(named).toContain('not posting');
+ expect(card().hasAttribute('data-off')).toBe(true);
+ expect(control(card(), 'post')!.checked).toBe(false);
+ expect(card().querySelector('.tui-cte')).toBeNull();
+ const resolve = control(card(), 'resolve')!;
+ expect(resolve.closest('.tui-reply-head')).not.toBeNull();
+ expect(resolve.checked).toBe(false);
});
/** The same gate after `gate-ctx.sh fit` went over budget: every context,
@@ -865,7 +951,7 @@ test('each resolve checkbox is named by its thread', async () => {
postCards()
.slice(0, 2)
.map(c => control(c, 'resolve')!.getAttribute('aria-label'))
- ).toEqual(['a.ts:1: resolve', 'b.ts:2: resolve']);
+ ).toEqual(['a.ts:1: resolve thread', 'b.ts:2: resolve thread']);
});
test('a dock question on a per-thread gate joins the submit label once picked', async () => {
@@ -967,12 +1053,14 @@ test('holding an Overview comment reply posts nothing for it', async () => {
});
const listTitle = () => $('.tui-sheet-list-title')!.textContent;
+const sheetTitle = () => $('.tui-sheet-round-n')!.textContent;
const contextLead = () =>
$('.tui-sheet-context-card .tui-id-card-lead')!.textContent;
test('a post sheet mixing threads and Overview comments says both in its heading', async () => {
await render(overviewPostGate());
- expect(listTitle()).toBe('Post replies on 3 threads and comments from renee');
+ expect(listTitle()).toBe('Post replies on 3 threads and comments');
+ expect(sheetTitle()).toBe('Respond to renee');
expect(contextLead()).toBe('renee reviewed your merge request');
});
@@ -990,7 +1078,8 @@ function commentsOnlyPlanGate(): GateRow {
test('someone who only commented: the heading counts comments and the lead says commented', async () => {
await render(commentsOnlyPlanGate());
- expect(listTitle()).toBe('Respond to 2 comments from renee');
+ expect(listTitle()).toBe('2 comments');
+ expect(sheetTitle()).toBe('Respond to renee');
expect(contextLead()).toBe('renee commented on your merge request');
expect($('[data-chip="threads"]')!.textContent).toBe('2 comments');
});
@@ -999,7 +1088,7 @@ test('one Overview comment beside a thread reads as threads and comments, review
const gate = planGate();
gate.questions[1] = { ...gate.questions[1]!, label: 'Overview comment' };
await render(gate);
- expect(listTitle()).toBe('Respond to 2 threads and comments from renee');
+ expect(listTitle()).toBe('2 threads and comments');
expect($('[data-chip="threads"]')!.textContent).toBe(
'2 threads and comments'
);
@@ -1020,7 +1109,8 @@ test('the sheet names the person the live respond run answers, by the name the b
{ username: 'john', name: 'John Ward', openThreads: 2, openComments: 0 },
])
);
- expect(listTitle()).toBe('Respond to 2 threads from John');
+ expect(sheetTitle()).toBe('Respond to John');
+ expect(listTitle()).toBe('2 threads');
expect(contextLead()).toBe('John reviewed your merge request');
});
@@ -1034,13 +1124,15 @@ test("a parked gate from an earlier run names its own reviewer, not the live run
startedAt: Date.now(),
},
} as unknown as BoardMRWithReview);
- expect(listTitle()).toBe('Respond to 2 threads from renee');
+ expect(sheetTitle()).toBe('Respond to renee');
+ expect(listTitle()).toBe('2 threads');
expect(contextLead()).toBe('renee reviewed your merge request');
});
test('a post sheet names the live run person too, and falls back to the handle', async () => {
await render(perThreadPostGate(), respondingTo('john'));
- expect(listTitle()).toBe('Post replies on 2 threads from john');
+ expect(sheetTitle()).toBe('Respond to john');
+ expect(listTitle()).toBe('Post replies on 2 threads');
expect(contextLead()).toBe('john reviewed your merge request');
});
@@ -1096,11 +1188,25 @@ test('an emptied reply on a posting thread disables submit', async () => {
expect(submit().disabled).toBe(true);
});
+/** The reply's field: its text area while open, else the rendered reply
+ that opens it on a click. */
const replyBox = (card: HTMLElement) =>
card.querySelector('textarea.tui-composer-input');
-const button = (card: HTMLElement, text: string) =>
- [...card.querySelectorAll('button')].find(b => b.textContent === text);
-async function typeInto(el: HTMLTextAreaElement, text: string) {
+const replyView = (card: HTMLElement) =>
+ card.querySelector('.tui-cte-view');
+const replyText = (card: HTMLElement) =>
+ replyBox(card)?.value ?? replyView(card)?.textContent ?? null;
+const isEdited = (card: HTMLElement) =>
+ card.querySelector('[data-section="comment"][data-edited]') !== null;
+async function openReply(card: HTMLElement) {
+ if (!replyBox(card))
+ await React.act(async () => {
+ replyView(card)!.click();
+ });
+ return replyBox(card)!;
+}
+async function typeInto(card: HTMLElement, text: string) {
+ const el = await openReply(card);
await React.act(async () => {
Object.getOwnPropertyDescriptor(
HTMLTextAreaElement.prototype,
@@ -1110,49 +1216,48 @@ async function typeInto(el: HTMLTextAreaElement, text: string) {
});
}
-test('the reply is a text area seeded with the draft, ready to type into', async () => {
+test('the reply reads as the draft under your name, and a click opens it seeded with the draft', async () => {
await render(perThreadPostGate(), withFixPlan());
const reply = postCards()[1]!;
- expect(reply.querySelector('button[aria-label$="edit reply"]')).toBeNull();
- expect(replyBox(reply)!.value).toBe('The delay is fixed by design.');
- expect(replyBox(reply)!.readOnly).toBe(false);
- expect(reply.querySelector('.tui-composer-meta')!.textContent).toBe(
- 'posts as you'
- );
+ expect(replyBox(reply)).toBeNull();
+ expect(replyView(reply)!.textContent).toBe('The delay is fixed by design.');
+ expect(replyView(reply)!.hasAttribute('aria-readonly')).toBe(false);
+ expect(reply.querySelector('.tui-cte-lead')!.textContent).toBe('You:');
+ const box = await openReply(reply);
+ expect(box.value).toBe('The delay is fixed by design.');
+ expect(document.activeElement).toBe(box);
});
-test('a changed reply shows the edited chip and posts; reset to draft clears both', async () => {
+test('a changed reply is marked edited and posts; reset to draft clears both', async () => {
await render(perThreadPostGate(), withFixPlan());
const reply = postCards()[1]!;
- await typeInto(
- replyBox(reply)!,
- 'The delay is fixed by design; see retry.ts.'
- );
- expect(reply.querySelector('.tui-composer[data-edited]')).not.toBeNull();
- expect(reply.querySelector('.tui-composer-meta')!.textContent).toBe(
- 'edited·reset'
- );
- await React.act(async () => button(reply, 'reset')!.click());
- expect(reply.querySelector('.tui-composer[data-edited]')).toBeNull();
- expect(replyBox(reply)!.value).toBe('The delay is fixed by design.');
+ await typeInto(reply, 'The delay is fixed by design; see retry.ts.');
+ expect(isEdited(reply)).toBe(true);
+ const reset = reply.querySelector(
+ 'button[aria-label="Reply on b.ts:2: reset to draft"]'
+ )!;
+ expect(reset.textContent).toBe('Reset to draft');
+ await React.act(async () => reset.click());
+ expect(isEdited(reply)).toBe(false);
+ expect(replyText(reply)).toBe('The delay is fixed by design.');
expect(document.activeElement).toBe(replyBox(reply));
});
test('an edit survives hold and back to post, and is what posts', async () => {
await render(perThreadPostGate(), withFixPlan());
const reply = postCards()[1]!;
- await typeInto(replyBox(reply)!, 'Kept on purpose.');
+ await typeInto(reply, 'Kept on purpose.');
const post = control(reply, 'post')!;
await React.act(async () => {
post.focus();
post.click();
});
- expect(replyBox(reply)!.readOnly).toBe(true);
- expect(replyBox(reply)!.value).toBe('The delay is fixed by design.');
+ expect(reply.hasAttribute('data-off')).toBe(true);
+ expect(reply.querySelector('.tui-cte')).toBeNull();
expect(document.activeElement).toBe(post);
await React.act(async () => control(reply, 'post')!.click());
- expect(replyBox(reply)!.readOnly).toBe(false);
- expect(replyBox(reply)!.value).toBe('Kept on purpose.');
+ expect(replyText(reply)).toBe('Kept on purpose.');
+ expect(isEdited(reply)).toBe(true);
await clickSubmit();
expect(
(answer() as { answers: Record }).answers['thread-2']
@@ -1165,25 +1270,26 @@ test('an edit survives hold and back to post, and is what posts', async () => {
test('an emptied reply says so on its card', async () => {
await render(perThreadPostGate(), withFixPlan());
const reply = postCards()[1]!;
- await typeInto(replyBox(reply)!, ' ');
- expect(reply.textContent).toContain('the reply is empty');
+ await typeInto(reply, ' ');
+ expect(reply.textContent).toContain('the comment is empty');
expect(submit().disabled).toBe(true);
const hint = replyBox(reply)!.getAttribute('aria-describedby');
expect(hint && document.getElementById(hint)?.textContent).toBe(
- 'the reply is empty'
+ 'the comment is empty'
);
+ expect(replyBox(reply)!.getAttribute('aria-invalid')).toBe('true');
});
test('the dock reset restores the picks but keeps an edited reply', async () => {
await render(perThreadPostGate(), withFixPlan());
const reply = postCards()[1]!;
- await typeInto(replyBox(reply)!, 'Kept on purpose.');
+ await typeInto(reply, 'Kept on purpose.');
await holdReply(reply);
await click($('.tui-sheet-reset'));
const again = postCards()[1]!;
expect(control(again, 'post')!.checked).toBe(true);
- expect(again.querySelector('.tui-composer[data-edited]')).not.toBeNull();
- expect(replyBox(again)!.value).toBe('Kept on purpose.');
+ expect(isEdited(again)).toBe(true);
+ expect(replyText(again)).toBe('Kept on purpose.');
await clickSubmit();
expect(
(answer() as { answers: Record }).answers['thread-2']
@@ -1197,7 +1303,7 @@ test('the dock says why submit is off while a posting reply is empty', async ()
const dock = () => $('.tui-sheet-dock')!.textContent;
expect(dock()).not.toContain(DOCK_EMPTY);
const reply = postCards()[1]!;
- await typeInto(replyBox(reply)!, '');
+ await typeInto(reply, '');
expect(dock()).toContain(DOCK_EMPTY);
await holdReply(reply);
expect(dock()).not.toContain(DOCK_EMPTY);
@@ -1207,7 +1313,7 @@ test('the dock says why submit is off while a posting reply is empty', async ()
test('the reply controls are named by their thread', async () => {
await render(perThreadPostGate(), withFixPlan());
const reply = postCards()[1]!;
- await typeInto(replyBox(reply)!, 'Kept on purpose.');
+ await typeInto(reply, 'Kept on purpose.');
expect(
[...reply.querySelectorAll('.tui-composer-act')].map(b =>
b.getAttribute('aria-label')
@@ -1215,28 +1321,31 @@ test('the reply controls are named by their thread', async () => {
).toEqual(['Reply on b.ts:2: reset to draft']);
expect(replyBox(reply)!.getAttribute('aria-label')).toBe('Reply on b.ts:2');
expect(replyBox(reply)!.hasAttribute('aria-describedby')).toBe(false);
+ expect(control(reply, 'post')!.getAttribute('aria-label')).toBe(
+ 'b.ts:2: post reply'
+ );
});
-test('an edit is marked in the reply strip only, never in the card head', async () => {
+test('an edit is marked in the reply field only, never in the card head', async () => {
await render(perThreadPostGate(), withFixPlan());
const fix = postCards()[0]!;
- await typeInto(replyBox(fix)!, 'Fixed, and bounded.');
- expect(fix.querySelector('.tui-composer[data-edited]')).not.toBeNull();
- expect(
- fix.querySelector('.tui-gate-question-head')!.textContent
- ).not.toContain('edited');
+ await typeInto(fix, 'Fixed, and bounded.');
+ expect(isEdited(fix)).toBe(true);
+ const head = fix.querySelector('.tui-reply-head')!;
+ expect(head.textContent).not.toContain('dited');
+ expect(head.querySelector('[data-edited]')).toBeNull();
});
test('Escape in a field leaves the field, not the sheet; a second Escape reaches the sheet', async () => {
await render(perThreadPostGate(), withFixPlan());
- const box = replyBox(postCards()[1]!)!;
+ const card = postCards()[1]!;
+ const box = await openReply(card);
const esc = (el: Element) =>
React.act(async () => {
el.dispatchEvent(
new KeyboardEvent('keydown', { key: 'Escape', bubbles: true })
);
});
- box.focus();
// The sheet's Escape listener sits on the document; counting what reaches
// it keeps the test free of the layer stack other files leave behind.
let reached = 0;
@@ -1246,11 +1355,10 @@ test('Escape in a field leaves the field, not the sheet; a second Escape reaches
document.addEventListener('keydown', onDoc);
try {
await esc(box);
- expect(document.activeElement?.className).toBe('tui-field-tab');
- expect(document.activeElement?.textContent).toBe('Write');
- expect(box.closest('.tui-composer')!.contains(document.activeElement)).toBe(
- true
+ expect(card.querySelector('.tui-cte')!.hasAttribute('data-editing')).toBe(
+ false
);
+ expect(document.activeElement).toBe(replyView(card));
expect(reached).toBe(0);
expect(closes).toBe(0);
await esc(document.body);
@@ -1260,14 +1368,14 @@ test('Escape in a field leaves the field, not the sheet; a second Escape reaches
}
});
-test('a held reply names its held strip to assistive tech', async () => {
+test('a held reply tells assistive tech through its unticked post tick', async () => {
await render(perThreadPostGate(), withFixPlan());
const reply = postCards()[1]!;
await holdReply(reply);
- const id = replyBox(reply)!.getAttribute('aria-describedby');
- expect(id && document.getElementById(id)?.textContent).toBe(
- 'drafted reply · not posting'
- );
+ const tick = control(reply, 'post')!;
+ expect(tick.getAttribute('aria-label')).toBe('b.ts:2: post reply');
+ expect(tick.checked).toBe(false);
+ expect(reply.getAttribute('aria-label')).toBe('b.ts:2');
});
test('a card whose reply did not fit the gate offers no edit', async () => {
@@ -1281,32 +1389,24 @@ test('a card whose reply did not fit the gate offers no edit', async () => {
expect(unfit!.textContent).toContain(
'The reply text did not fit the gate; read it in the pane.'
);
- expect(replyBox(unfit!)).toBeNull();
- expect(replyBox(fit!)!.readOnly).toBe(false);
+ expect(unfit!.querySelector('.tui-cte')).toBeNull();
+ expect(replyView(fit!)!.hasAttribute('aria-readonly')).toBe(false);
});
test('without its plan gate, an edited reply is still marked edited', async () => {
await render(perThreadPostGate());
const reply = postCards()[1]!;
- await typeInto(replyBox(reply)!, 'Kept on purpose.');
- expect(reply.querySelector('.tui-composer-meta')!.textContent).toBe(
- 'edited·reset'
- );
+ await typeInto(reply, 'Kept on purpose.');
+ expect(isEdited(reply)).toBe(true);
});
-const planCards = () => [
- ...document.body.querySelectorAll(
- 'section[data-gate-ctx="thread"]'
- ),
-];
-
test('a reply pick on the plan sheet can be edited and answers with its text', async () => {
await render(planGate());
await pick('reply:t1');
await pick('reply:t2');
const card = planCards()[0]!;
- await typeInto(replyBox(card)!, 'fixed in the next push, with a test.');
- expect(card.querySelector('.tui-composer[data-edited]')).not.toBeNull();
+ await typeInto(card, 'fixed in the next push, with a test.');
+ expect(isEdited(card)).toBe(true);
await clickSubmit();
expect(answer()).toEqual({
gateId: 'g-plan',
@@ -1326,10 +1426,11 @@ test('a fix or skip pick offers no edit, and an edit made under reply is not sen
await pick('reply:t1');
await pick('skip:t2');
const card = planCards()[0]!;
- await typeInto(replyBox(card)!, 'an edit');
+ await typeInto(card, 'an edit');
await pick('fix:t1');
- expect(replyBox(card)!.readOnly).toBe(true);
- expect(replyBox(planCards()[1]!)!.readOnly).toBe(true);
+ expect(card.querySelector('.tui-cte')).toBeNull();
+ expect(planCards()[1]!.hasAttribute('data-off')).toBe(true);
+ expect(planCards()[1]!.querySelector('.tui-cte')).toBeNull();
await clickSubmit();
expect(
(answer() as { answers: Record }).answers['thread-1']
@@ -1354,11 +1455,12 @@ test('the dock says what happens next on the plan sheet', async () => {
expect(submit().textContent).toBe('submit · 2 fixes');
});
-test('the plan dock waits for every thread to be picked before saying what comes next', async () => {
+test('the plan dock says what comes next from the first paint, every thread on its pick', async () => {
await render(planGate());
- expect($('.tui-sheet-dock-next')).toBeNull();
+ expect($('.tui-sheet-dock-next')!.textContent).toBe(
+ 'Next, 2 fixes get implemented, then you approve 2 replies before anything posts.'
+ );
await pick('reply:t1');
- expect($('.tui-sheet-dock-next')).toBeNull();
await pick('reply:t2');
expect($('.tui-sheet-dock-next')!.textContent).toBe('Next, 2 replies post.');
});
@@ -1366,30 +1468,60 @@ test('the plan dock waits for every thread to be picked before saying what comes
const caption = (card: HTMLElement) =>
card.querySelector('.tui-composer-k')!.textContent;
-test('a plan card shows only what will post: a fix pick shows the draft, and reply brings the edit back', async () => {
+test('a plan card shows only what will post: a fix pick says what gets fixed, and reply brings the edit back', async () => {
await render(planGate());
const card = planCards()[0]!;
- expect(caption(card)).toBe('drafted reply');
+ expect(card.querySelector('.tui-plan-fix')!.textContent).toBe('fix');
+ expect(card.querySelector('.tui-cte')).toBeNull();
await pick('reply:t1');
await pick('reply:t2');
- expect(caption(card)).toBe('will post as reply');
- await typeInto(replyBox(card)!, 'fixed in the next push, with a test.');
+ expect(card.querySelector('.tui-plan-fix')).toBeNull();
+ expect(card.querySelector('.tui-cte-lead')!.textContent).toBe('You:');
+ expect(replyText(card)).toBe('fixed, with a test.');
+ await typeInto(card, 'fixed in the next push, with a test.');
await pick('fix:t1');
- expect(caption(card)).toBe('drafted reply');
- expect(replyBox(card)!.value).toBe('fixed, with a test.');
- expect(card.querySelector('.tui-composer[data-edited]')).toBeNull();
+ expect(card.querySelector('.tui-plan-fix')!.textContent).toBe('fix');
+ expect(card.querySelector('.tui-cte')).toBeNull();
+ expect(isEdited(card)).toBe(false);
await pick('reply:t1');
- expect(caption(card)).toBe('will post as reply');
- expect(replyBox(card)!.value).toBe('fixed in the next push, with a test.');
- expect(card.querySelector('.tui-composer[data-edited]')).not.toBeNull();
+ expect(replyText(card)).toBe('fixed in the next push, with a test.');
+ expect(isEdited(card)).toBe(true);
});
-test('a held post-step reply is captioned as a draft, not as posting', async () => {
+test('a reply pick with no drafted words says it is drafted at posting', async () => {
+ const gate = planGate();
+ gate.questions[1] = {
+ ...gate.questions[1]!,
+ context: JSON.stringify({
+ 'gate-ctx': 'thread@1',
+ author: 'renee',
+ severity: 'blocking',
+ claim: { summary: 'claim 2' },
+ verdict: { call: 'valid' },
+ reply: { kind: 'direction', text: 'say the retry is bounded now.' },
+ }),
+ };
+ await render(gate);
+ await pick('reply:t2');
+ const card = planCards()[1]!;
+ expect(card.querySelector('.tui-cte')).toBeNull();
+ const later = card.querySelector('.tui-plan-later')!;
+ expect(later.textContent).toBe('Drafted at posting');
+ expect(later.querySelector('button')!.getAttribute('aria-label')).toContain(
+ 'drafted during the Post step'
+ );
+});
+
+test('a held post-step reply folds its card; posting it again opens the reply', async () => {
await render(perThreadPostGate(), withFixPlan());
const reply = postCards()[1]!;
- expect(caption(reply)).toBe('will post as reply');
+ expect(replyText(reply)).toBe('The delay is fixed by design.');
await holdReply(reply);
- expect(caption(reply)).toBe('drafted reply · not posting');
+ expect(reply.hasAttribute('data-off')).toBe(true);
+ expect(reply.querySelector('.tui-cte')).toBeNull();
+ await React.act(async () => control(reply, 'post')!.click());
+ expect(reply.hasAttribute('data-off')).toBe(false);
+ expect(replyText(reply)).toBe('The delay is fixed by design.');
});
test('a fix pick beside an edited reply approves bare and wraps the reply', async () => {
@@ -1397,7 +1529,7 @@ test('a fix pick beside an edited reply approves bare and wraps the reply', asyn
await pick('reply:t1');
await pick('fix:t2');
const card = planCards()[0]!;
- await typeInto(replyBox(card)!, 'fixed in the next push, with a test.');
+ await typeInto(card, 'fixed in the next push, with a test.');
await clickSubmit();
expect(answer()).toEqual({
gateId: 'g-plan',
@@ -1417,7 +1549,7 @@ test('sending the plan back carries no edited text and stays live', async () =>
await pick('reply:t1');
await pick('reply:t2');
const card = planCards()[0]!;
- await typeInto(replyBox(card)!, 'fixed in the next push, with a test.');
+ await typeInto(card, 'fixed in the next push, with a test.');
await click($('.tui-sheet-revise'));
await typeArea('What should the new plan change?', 'split the fix');
expect(submit().disabled).toBe(false);
@@ -1439,7 +1571,7 @@ test('a revise answered in the dock sends no edited text and is not blocked by a
await pick('reply:t1');
await pick('reply:t2');
const card = planCards()[0]!;
- await typeInto(replyBox(card)!, ' ');
+ await typeInto(card, ' ');
await pick('approve');
expect(submit().disabled).toBe(true);
expect(document.body.textContent).toContain(
@@ -1449,7 +1581,7 @@ test('a revise answered in the dock sends no edited text and is not blocked by a
expect(submit().disabled).toBe(false);
expect(document.body.textContent).not.toContain('a reply is empty:');
expect($('.tui-sheet-dock-next')).toBeNull();
- await typeInto(replyBox(card)!, 'fixed in the next push, with a test.');
+ await typeInto(card, 'fixed in the next push, with a test.');
await clickSubmit();
expect(answer()).toEqual({
gateId: 'g-plan',
@@ -1466,7 +1598,7 @@ test('an emptied reply pick blocks submit with a reason; send-back mode ignores
await pick('reply:t1');
await pick('reply:t2');
const card = planCards()[0]!;
- await typeInto(replyBox(card)!, ' ');
+ await typeInto(card, ' ');
expect(submit().disabled).toBe(true);
expect(document.body.textContent).toContain(
'a reply is empty: write it or skip the thread'
@@ -1476,50 +1608,48 @@ test('an emptied reply pick blocks submit with a reason; send-back mode ignores
});
async function editThenHold(card: HTMLElement) {
- await typeInto(replyBox(card)!, 'Kept on purpose.');
+ await typeInto(card, 'Kept on purpose.');
await holdReply(card);
}
-test('a held post card shows the draft without the chip; post brings the edit back', async () => {
+test('a held post card drops its edited mark; post brings the edit back', async () => {
await render(perThreadPostGate(), withFixPlan());
const reply = postCards()[1]!;
await editThenHold(reply);
- expect(caption(reply)).toBe('drafted reply · not posting');
- expect(replyBox(reply)!.value).toBe('The delay is fixed by design.');
- expect(reply.querySelector('.tui-composer[data-edited]')).toBeNull();
+ expect(reply.hasAttribute('data-off')).toBe(true);
+ expect(isEdited(reply)).toBe(false);
await React.act(async () => control(reply, 'post')!.click());
- expect(caption(reply)).toBe('will post as reply');
- expect(replyBox(reply)!.value).toBe('Kept on purpose.');
- expect(reply.querySelector('.tui-composer[data-edited]')).not.toBeNull();
+ expect(replyText(reply)).toBe('Kept on purpose.');
+ expect(isEdited(reply)).toBe(true);
});
-test('without its plan gate, a held post card also shows the draft without the chip', async () => {
+test('without its plan gate, a held post card also drops its edited mark until posted again', async () => {
await render(perThreadPostGate());
const reply = postCards()[1]!;
await editThenHold(reply);
- expect(replyBox(reply)!.value).toBe('The delay is fixed by design.');
- expect(reply.querySelector('.tui-composer[data-edited]')).toBeNull();
+ expect(isEdited(reply)).toBe(false);
await React.act(async () => control(reply, 'post')!.click());
- expect(reply.querySelector('.tui-composer[data-edited]')).not.toBeNull();
+ expect(replyText(reply)).toBe('Kept on purpose.');
+ expect(isEdited(reply)).toBe(true);
});
const dockNext = () => $('.tui-sheet-dock-next')!.textContent;
-test('a reply pick with a note is an override: you approve it at gate 2', async () => {
+test('a reply pick with a restored note is an override: you approve it at gate 2', async () => {
+ seedNotes('g-plan', { 'thread-1': ' say which test ' });
await render(planGate());
await pick('reply:t1');
await pick('reply:t2');
- await type('Note for queue/enqueue.ts:10', ' say which test ');
expect(dockNext()).toBe('Next, you approve 1 reply before anything posts.');
});
-test('a reply pick with an edit and a note posts its edit: a plain reply', async () => {
+test('a reply pick with an edit and a restored note posts its edit: a plain reply', async () => {
+ seedNotes('g-plan', { 'thread-1': 'say which test' });
await render(planGate());
await pick('reply:t1');
await pick('skip:t2');
const card = planCards()[0]!;
- await typeInto(replyBox(card)!, 'fixed in the next push, with a test.');
- await type('Note for queue/enqueue.ts:10', 'say which test');
+ await typeInto(card, 'fixed in the next push, with a test.');
expect(dockNext()).toBe('Next, 1 reply posts.');
});
@@ -1580,8 +1710,10 @@ test('a reply-only thread edited at Gate 1 shows its edit, marked in its strip',
test('a reply override is offered at Gate 2 with its redraft and its controls', async () => {
await render(perThreadPostGate(), withFixPlan());
const card = postCards()[1]!;
- expect(outcomeOf(card)).toBe('reply only');
- expect(replyBox(card)!.value).toBe('The delay is fixed by design.');
+ expect(card.querySelector('.tui-reply-status')!.textContent).toBe(
+ 'Reply only'
+ );
+ expect(replyText(card)).toBe('The delay is fixed by design.');
expect(control(card, 'post')!.checked).toBe(true);
expect(control(card, 'resolve')!.checked).toBe(false);
});
@@ -1596,14 +1728,14 @@ test('holding everything with no reply-only thread still reads hold all', async
});
test('the plan dock says nothing about what comes next while a reply is empty', async () => {
+ seedNotes('g-plan', { 'thread-1': 'say which test' });
await render(planGate());
await pick('reply:t1');
await pick('reply:t2');
const card = planCards()[0]!;
- await typeInto(replyBox(card)!, ' ');
- await type('Note for queue/enqueue.ts:10', 'say which test');
+ await typeInto(card, ' ');
expect($('.tui-sheet-dock-next')).toBeNull();
- await typeInto(replyBox(card)!, 'fixed in the next push, with a test.');
+ await typeInto(card, 'fixed in the next push, with a test.');
expect(dockNext()).toBe('Next, 2 replies post.');
});
diff --git a/apps/board/src/client/board/__tests__/respond-thread-card-dom.test.tsx b/apps/board/src/client/board/__tests__/respond-thread-card-dom.test.tsx
index c99c98b833..bc91844e38 100644
--- a/apps/board/src/client/board/__tests__/respond-thread-card-dom.test.tsx
+++ b/apps/board/src/client/board/__tests__/respond-thread-card-dom.test.tsx
@@ -11,7 +11,6 @@ import { afterAll, afterEach, beforeEach, expect, test } from 'bun:test';
import { createRoot, type Root } from 'react-dom/client';
import type { GateRow } from '../../../gates/store.ts';
-import { CommentComposer } from '../CommentComposer.tsx';
import { DecisionQueueModal } from '../DecisionQueueModal.tsx';
GlobalRegistrator.register({ url: 'http://localhost/' });
@@ -225,7 +224,7 @@ test('each verdict call renders its own word and data-call', async () => {
).toEqual(calls.map(c => [...c]));
});
-test('reply kinds: verbatim and direction label their box, none renders no box', async () => {
+test('reply kinds: verbatim labels its box, a direction reads as the reply after the fix, none renders neither', async () => {
await renderGate(
gate({
questions: [
@@ -250,14 +249,16 @@ test('reply kinds: verbatim and direction label their box, none renders no box',
expect(cards[0]!.querySelector('.tui-composer-k')!.textContent).toBe(
'will post as reply'
);
- expect(
- cards[1]!.querySelector('.tui-composer')!.getAttribute('data-kind')
- ).toBe('direction');
- expect(cards[1]!.querySelector('.tui-composer-k')!.textContent).toBe(
- 'reply direction'
+ expect(cards[1]!.querySelector('.tui-composer')).toBeNull();
+ const direction = cards[1]!.querySelector('.tui-thread-direction')!;
+ expect(direction.querySelector('.tui-thread-direction-k')!.textContent).toBe(
+ 'Reply after the fix'
);
- expect(cards[1]!.textContent).toContain('Explain the retry contract');
+ expect(
+ direction.querySelector('.tui-thread-direction-text')!.textContent
+ ).toContain('Explain the retry contract');
expect(cards[2]!.querySelector('.tui-composer')).toBeNull();
+ expect(cards[2]!.querySelector('.tui-thread-direction')).toBeNull();
});
test("the fix option's description renders as its choice subtitle", async () => {
@@ -279,89 +280,3 @@ test('a thread context missing a required field renders as prose', async () => {
).toBeNull();
expect(container.querySelector('.tui-gate-question-context')).not.toBeNull();
});
-
-test('CommentComposer is a framed text area seeded with the draft', async () => {
- await React.act(async () => {
- root.render(
- {}}
- onReset={() => {}}
- />
- );
- });
- expect(container.querySelector('.tui-composer-k')!.textContent).toBe(
- 'will post as comment'
- );
- expect(container.querySelector('.tui-composer-meta')!.textContent).toBe(
- 'posts as you'
- );
- const box = container.querySelector(
- 'textarea[aria-label="Comment on finding 1"]'
- )!;
- expect(box.value).toBe('tighten this');
- expect(box.readOnly).toBe(false);
-});
-
-test('CommentComposer says edited, offers reset, and names an empty edit', async () => {
- let resets = 0;
- const draw = (value: string) =>
- root.render(
- {}}
- onReset={() => resets++}
- />
- );
- await React.act(async () => draw('y'));
- expect(container.querySelector('.tui-composer-meta')!.textContent).toBe(
- 'edited·reset'
- );
- await React.act(async () =>
- container
- .querySelector(
- 'button[aria-label="Reply on a.ts:1: reset to draft"]'
- )!
- .click()
- );
- expect(resets).toBe(1);
- await React.act(async () => draw(' '));
- const box = container.querySelector('textarea')!;
- const hint = box.getAttribute('aria-describedby');
- expect(hint && document.getElementById(hint)?.textContent).toBe(
- 'the reply is empty'
- );
-});
-
-test('a held CommentComposer shows the draft read-only under its held strip', async () => {
- await React.act(async () => {
- root.render(
- {}}
- onReset={() => {}}
- />
- );
- });
- expect(container.querySelector('.tui-composer-k')!.textContent).toBe(
- 'drafted reply · not posting'
- );
- expect(container.querySelector('.tui-composer-meta')).toBeNull();
- const box = container.querySelector('textarea')!;
- expect(box.value).toBe('the draft');
- expect(box.readOnly).toBe(true);
-});
diff --git a/apps/board/src/client/board/__tests__/review-gate-sheet-dom.test.tsx b/apps/board/src/client/board/__tests__/review-gate-sheet-dom.test.tsx
index 67f6121557..1a0569589a 100644
--- a/apps/board/src/client/board/__tests__/review-gate-sheet-dom.test.tsx
+++ b/apps/board/src/client/board/__tests__/review-gate-sheet-dom.test.tsx
@@ -606,12 +606,16 @@ test("a malformed report.json renders the sheet without throwing and skips the r
const labels = [
...container.querySelectorAll('.tui-review-record-label'),
].map(l => l.textContent);
- // strengths and notes are dropped whole (not arrays); the checks show in
- // the Report tab's card as well as the rail's.
- expect(labels).toEqual(['depth', 'checks']);
+ // strengths and notes are dropped whole (not arrays); the checks leave
+ // the record for their own card, in the Report tab as in the rail.
+ expect(labels).toEqual(['depth']);
// The one malformed check entry (missing `text`) is dropped, the
- // well-typed one stays and shows up in the rail card.
- expect(container.textContent).toContain('real check');
+ // well-typed one stays in the Report tab's checks card.
+ const checks = container.querySelector('.tui-sheet-main .tui-review-checks')!;
+ expect(checks.textContent).toContain('real check');
+ expect(checks.querySelector('.tui-review-checks-tally')!.textContent).toBe(
+ '1 fail'
+ );
expect(container.textContent).not.toContain('not an array');
});
@@ -1006,25 +1010,42 @@ describe('a later round', () => {
return { container, submitted };
}
+ const fileFor = (call: string) => EARLIER[earlierIndex(call)]!.file;
+
const cardFor = (scope: Element, call: string) =>
- scope
- .querySelector(`[data-call="${call}"]`)!
- .closest('[data-step="carryover"]')!;
+ scope.querySelector(
+ `article.tui-reply-card[aria-label="${fileFor(call)}"]`
+ )!;
+
+ const tickFor = (scope: Element, call: string, value: 'post' | 'resolve') =>
+ cardFor(scope, call).querySelector(
+ `input[aria-label="${fileFor(call)}: ${
+ value === 'post' ? 'post reply' : 'resolve thread'
+ }"]`
+ );
+
+ const earlierCards = (scope: Element) => [
+ ...scope.querySelectorAll(
+ '.tui-respond-list > article.tui-reply-card'
+ ),
+ ];
async function untick(
scope: Element,
call: string,
value: 'post' | 'resolve'
) {
- const box = cardFor(scope, call).querySelector(
- `input[value="${value}"]`
- )!;
+ const box = tickFor(scope, call, value)!;
if (!box.checked) throw new Error(`${call} ${value} is not ticked`);
await React.act(async () => box.click());
}
async function editReply(scope: Element, call: string, text: string) {
- const box = cardFor(scope, call).querySelector(
+ const card = cardFor(scope, call);
+ await React.act(async () => {
+ card.querySelector('.tui-cte-view')!.click();
+ });
+ const box = card.querySelector(
'textarea.tui-composer-input'
)!;
await React.act(async () => {
@@ -1066,29 +1087,43 @@ describe('a later round', () => {
await click(box);
}
- test('the main column opens with the round and what it holds', () => {
+ test('the main column opens with the review heading, its round on the timeline', () => {
const { container } = renderSheet(roundThreeGate());
const heading = container.querySelector('.tui-sheet-round')!;
expect(heading.querySelector('.tui-sheet-round-n')!.textContent).toBe(
- 'Round 3'
+ "Review Paul's MR"
);
+ const chip = heading.querySelector('.tui-tl-round-chip')!;
+ expect(chip.textContent).toBe('Round 3');
+ expect(chip.hasAttribute('data-menu')).toBe(true);
expect(
- heading.querySelector('.tui-sheet-round-summary')!.textContent
- ).toMatch(/earlier threads? · \d+ new finding/);
+ [...heading.querySelectorAll('.tui-tl-step')].map(li => [
+ li.textContent,
+ li.getAttribute('data-state'),
+ ])
+ ).toEqual([
+ ['Your review', 'current'],
+ ["Paul's response", 'next'],
+ ]);
+ expect(heading.querySelector('.tui-sheet-round-summary')).toBeNull();
});
test('each earlier thread is a card naming who acts', () => {
const { container } = renderSheet(roundThreeGate());
- const cards = [...container.querySelectorAll('[data-step="carryover"]')];
- expect(cards.length).toBeGreaterThan(0);
- const chips = cards.map(
- c => c.querySelector('.tui-thread-outcome')!.textContent
+ const cards = earlierCards(container);
+ expect(cards.map(c => c.getAttribute('aria-label'))).toEqual(
+ EARLIER.map(c => c.file)
+ );
+ const marks = cards.map(
+ c => c.querySelector('.tui-reply-head .tui-reply-status')!.textContent
);
- for (const chip of chips) expect(chip).not.toMatch(/open|unresolved/i);
- expect(chips).toContain('fixed by author');
- expect(chips).toContain('waiting on author');
- expect(chips).toContain('author pushed back · accept');
- expect(chips).toContain('author pushed back · hold firm');
+ for (const mark of marks) expect(mark).not.toMatch(/open|unresolved/i);
+ expect(marks).toEqual([
+ 'Fixed',
+ 'Waiting on author',
+ 'Pushback accepted',
+ 'Holding firm',
+ ]);
});
test('neither the round heading nor the earlier threads heading says open or unresolved', () => {
@@ -1099,46 +1134,41 @@ describe('a later round', () => {
].find(h =>
h
.querySelector('.tui-sheet-list-title')
- ?.textContent?.startsWith('Your earlier threads')
+ ?.textContent?.startsWith('Earlier threads')
)!.textContent!;
expect(round).toContain('Round 3');
- expect(earlier).toContain(`Your earlier threads (${EARLIER.length})`);
+ expect(earlier).toContain(`Earlier threads (${EARLIER.length})`);
for (const text of [round, earlier])
expect(text).not.toMatch(/open|unresolved/i);
});
test('a thread the author never answered says so', () => {
const { container } = renderSheet(roundThreeGate({ dropAuthorReplyOn: 0 }));
- expect(container.querySelector('.tui-thread-nothing')!.textContent).toBe(
- "The author hasn't replied in this thread."
- );
const silent = cardFor(container, EARLIER[0]!.call);
- expect([...silent.querySelectorAll('.tui-thread-nothing')]).toHaveLength(1);
- const answered = [
- ...container.querySelectorAll('[data-step="carryover"]'),
- ].filter(c => c !== silent);
+ const said = [...silent.querySelectorAll('.tui-reply-silence')];
+ expect(said).toHaveLength(1);
+ expect(said[0]!.textContent).toBe('No reply from author');
+ const answered = earlierCards(container).filter(c => c !== silent);
expect(answered).toHaveLength(EARLIER.length - 1);
for (const card of answered)
- expect(card.querySelector('.tui-thread-nothing')).toBeNull();
+ expect(card.querySelector('.tui-reply-silence')).toBeNull();
});
test('a fixed thread starts with post and resolve ticked; a waiting one with post only', () => {
const { container } = renderSheet(roundThreeGate());
- const ticks = (call: string) => {
- const card = container
- .querySelector(`[data-call="${call}"]`)!
- .closest('[data-step="carryover"]')!;
- return [
- ...card.querySelectorAll('input[type="checkbox"]'),
- ].map(i => [i.value, i.checked]);
- };
+ const ticks = (call: string) =>
+ [
+ ...cardFor(container, call).querySelectorAll(
+ 'input[type="checkbox"]'
+ ),
+ ].map(i => [i.getAttribute('aria-label'), i.checked]);
expect(ticks('fixed')).toEqual([
- ['post', true],
- ['resolve', true],
+ [`${fileFor('fixed')}: post reply`, true],
+ [`${fileFor('fixed')}: resolve thread`, true],
]);
expect(ticks('not-fixed')).toEqual([
- ['post', true],
- ['resolve', false],
+ [`${fileFor('not-fixed')}: post reply`, true],
+ [`${fileFor('not-fixed')}: resolve thread`, false],
]);
});
@@ -1593,11 +1623,14 @@ describe('a gate whose poster honors edited drafts', () => {
expect(container.querySelector('.tui-review-summary')).toBeNull();
});
- test('on a later round the tabs lead, then the round band, then the findings list', async () => {
+ test('the heading leads, then the tabs, then the findings list', async () => {
await render(draftsGate({ round: 2 }));
const main = container.querySelector('.tui-sheet-main')!;
- expect(main.firstElementChild?.getAttribute('role')).toBe('tablist');
- expect(main.children[1]?.className).toBe('tui-sheet-round');
+ expect(main.firstElementChild?.className).toBe('tui-sheet-round');
+ expect(
+ main.firstElementChild?.querySelector('.tui-tl-round-chip')?.textContent
+ ).toBe('Round 2');
+ expect(main.children[1]?.getAttribute('role')).toBe('tablist');
expect(
main.children[2]?.querySelector('.tui-sheet-list-title')?.textContent
).toBe('Post which findings to !31?');
@@ -1615,25 +1648,18 @@ describe('a gate whose poster honors edited drafts', () => {
).toBeNull();
});
- test('a no-diff-line finding wears an in-summary badge in its head, with no code and a comment of its own', async () => {
+ test('a no-diff-line finding shows what it found in place of code, with a comment of its own and no in-summary chip', async () => {
await render(draftsGate());
const item = itemFor('Inconsistent error wording');
expect(item.querySelector('[data-section="code"]')).toBeNull();
- const badge = item.querySelector(
- '.tui-review-card-head .tui-review-in-summary'
- )!;
- expect(badge.textContent).toBe('in summary');
- expect(badge.getAttribute('title')).toBeTruthy();
expect(
- badge.compareDocumentPosition(item.querySelector('.tui-review-sev')!) &
- Node.DOCUMENT_POSITION_FOLLOWING
- ).toBeTruthy();
+ item.querySelector('[data-section="finding"] .tui-review-finding-said')!
+ .textContent
+ ).toBe(
+ 'Inconsistent error wording: the full finding text, never truncated.'
+ );
expect(item.querySelector('.tui-review-code-file')).toBeNull();
- expect(
- itemFor('SQL built from unsanitized input').querySelector(
- '.tui-review-in-summary'
- )
- ).toBeNull();
+ expect(container.querySelector('.tui-review-in-summary')).toBeNull();
expect(valueOf(sectionOf(item, 'Comment'))).toContain(COMMENT.f3);
expect(
container.querySelector('.tui-review-dock-summary')!.textContent
diff --git a/apps/board/src/client/board/gate-stories.fixtures.ts b/apps/board/src/client/board/gate-stories.fixtures.ts
new file mode 100644
index 0000000000..b9340eaf63
--- /dev/null
+++ b/apps/board/src/client/board/gate-stories.fixtures.ts
@@ -0,0 +1,889 @@
+import type { GateQuestion, GateRow } from '../../gates/store.ts';
+import type { BoardMRWithReview } from '../types.ts';
+import type { DiffSource, FileDiff } from './diff-source.ts';
+
+/**
+ * The four gates Storybook shows, rebuilt from real review-post,
+ * respond-plan and respond-post rows: question ids and counts, option
+ * labels and descriptions, gate-ctx shapes, meta and origin keep the real
+ * rows' structure, while every name, ticket, MR, path and piece of domain
+ * wording is invented. This repo is public, so nothing here may be copied
+ * from a live board.
+ */
+
+export interface StoryGate {
+ gate: GateRow;
+ mr: BoardMRWithReview;
+ /** Where a drafted review's code sections get their diffs. */
+ diffs?: DiffSource;
+}
+
+export const PEOPLE: ReadonlyMap = new Map([
+ ['pat', 'Pat Lin'],
+ ['renee', 'Renee Park'],
+ ['dana', 'Dana Ortiz'],
+]);
+
+const minutesAgo = (m: number) => Date.now() - m * 60_000;
+const ctx = (v: unknown) => JSON.stringify(v);
+
+function opt(value: string, label: string, description?: string) {
+ return description === undefined
+ ? { value, label }
+ : { value, label, description };
+}
+
+function question(
+ id: string,
+ label: string,
+ multi: boolean,
+ options: GateQuestion['options'],
+ context?: string
+): GateQuestion {
+ return { id, label, multi, options, ...(context ? { context } : {}) };
+}
+
+const FORGE = 'https://gitlab.example.com/acme/webapp/-/merge_requests';
+const TREES = '/Users/pat/worktrees/gl-acme-webapp';
+const mrSubject = (iid: number) => `mr:${FORGE}/${iid}`;
+
+function boardMr(
+ iid: number,
+ title: string,
+ author: string,
+ sourceBranch: string,
+ diff: [number, number, number],
+ approvals: [given: number, required: number]
+): BoardMRWithReview {
+ const [given, required] = approvals;
+ return {
+ iid,
+ title,
+ webUrl: `${FORGE}/${iid}`,
+ author: {
+ id: `gitlab:${author.length + iid}`,
+ username: author,
+ name: PEOPLE.get(author) ?? author,
+ avatarUrl: null,
+ },
+ sourceBranch,
+ targetBranch: 'main',
+ isDraft: false,
+ createdAt: new Date(minutesAgo(60 * 26)).toISOString(),
+ diff: { additions: diff[0], deletions: diff[1], filesChanged: diff[2] },
+ pipelineState: 'passed',
+ behindTarget: null,
+ reviews: {
+ required,
+ given,
+ remaining: Math.max(required - given, 0),
+ isApproved: given >= required,
+ approvedBy: [],
+ reviewers: [],
+ totalAssigned: 0,
+ haveActed: given,
+ havePending: 0,
+ haveNotStarted: 0,
+ },
+ blockers: {
+ isDraft: false,
+ hasConflicts: false,
+ needsRebase: false,
+ pipelineFailing: false,
+ pipelineRunning: false,
+ awaitingApprovals: given < required,
+ },
+ } as unknown as BoardMRWithReview;
+}
+
+// --- Review: Dana's MR, reviewed by me -------------------------------------
+
+const TILE_MR = boardMr(
+ 826,
+ 'ACME-3420: cache forecast tiles per region',
+ 'dana',
+ 'acme-3420-forecast-tile-cache',
+ [184, 41, 5],
+ [0, 1]
+);
+
+const CACHE = 'packages/forecast/src/tiles/tileCache.ts';
+const CACHE_TEST = 'packages/forecast/src/tiles/tileCache.test.ts';
+
+const TILE_FINDINGS = {
+ f1: {
+ id: 'f1',
+ severity: 'critical',
+ title: 'Cache key drops the region',
+ body: '`tileKey` builds the key from zoom, x and y only. Two regions asking for the same tile coordinates share one entry, so the second region is served the first region’s forecast until the entry expires.',
+ file: `${CACHE}:31`,
+ fix: 'Put the region first in the key and add a two-region test.',
+ kind: 'issue',
+ comment:
+ '**issue:** the key here is zoom/x/y only, so two regions asking for the same tile share one entry and the second one gets the first region’s forecast. could the region go first?\n\n```ts\nconst tileKey = (t: TileRequest) => `${t.region}:${t.z}/${t.x}/${t.y}`;\n```',
+ },
+ f2: {
+ id: 'f2',
+ severity: 'important',
+ title: 'An expired tile is served after a failed refetch',
+ body: 'When the refetch throws, the catch returns `entry.tile` without checking `expiresAt`, so a tile from yesterday keeps rendering with no stale marker.',
+ file: `${CACHE}:58`,
+ fix: 'Return the stale tile flagged as stale, or rethrow once it is past a hard limit.',
+ kind: 'question',
+ comment:
+ '**question:** if the refetch throws, this hands back `entry.tile` no matter how old it is. is that on purpose? a tile from yesterday would keep rendering with nothing saying it’s stale.',
+ },
+ f3: {
+ id: 'f3',
+ severity: 'important',
+ title: 'Evidence stops at the unit tests',
+ body: 'The tests prove the cache in isolation, but nothing in the description shows the map reading cached tiles across a region switch, which is the AC the ticket leads with.',
+ fix: 'Add a before/after capture of a region switch on the forecast map.',
+ kind: 'suggestion',
+ comment:
+ '**suggestion:** would you mind adding a capture of a region switch on the forecast map? the tests cover the cache itself, but the first AC is about what the map shows.',
+ },
+ f4: {
+ id: 'f4',
+ severity: 'minor',
+ title: 'Eviction never runs below the size cap',
+ body: '`evict` only runs when `size > maxEntries`, so expired entries sit in memory until the cache fills.',
+ file: `${CACHE}:66`,
+ fix: 'Drop expired entries on read as well.',
+ kind: 'issue',
+ comment:
+ '**issue (non-blocking):** expired entries only leave when the cache is full, so a quiet session keeps every old tile around. dropping them on read would cover it.',
+ },
+ f5: {
+ id: 'f5',
+ severity: 'minor',
+ title: 'Two new branches have no test',
+ body: 'Nothing covers the refetch failure path or the eviction of an expired entry.',
+ fix: 'Add a case per branch.',
+ kind: 'suggestion',
+ comment:
+ '**suggestion (non-blocking):** the refetch failure and the expired eviction don’t have a test yet. a case each would pin them.',
+ },
+ f6: {
+ id: 'f6',
+ severity: 'minor',
+ title: 'Test helper falls back to the first tile',
+ body: '`findTile(tiles, key) ?? tiles[0]` quietly tests the wrong tile if the fixture drifts.',
+ file: `${CACHE_TEST}:108`,
+ fix: 'Assert the tile exists instead of falling back.',
+ kind: 'nitpick',
+ comment:
+ '**nitpick:** the `?? tiles[0]` fallback would quietly test the wrong tile if the fixture changes. an assert would fail loudly instead.',
+ },
+ f7: {
+ id: 'f7',
+ severity: 'minor',
+ title: 'Description lists the radar layer as a consumer',
+ body: 'The radar layer reads tiles through its own loader and never touches this cache.',
+ fix: 'Drop the radar layer from the consumer list.',
+ kind: 'nitpick',
+ comment:
+ '**nitpick:** the description lists the radar layer as a consumer, but it goes through its own loader. worth dropping so nobody goes looking.',
+ },
+};
+
+const findingOption = (f: (typeof TILE_FINDINGS)[keyof typeof TILE_FINDINGS]) =>
+ opt(
+ f.id,
+ `[${f.severity[0]!.toUpperCase()}${f.severity.slice(1)}] ${f.title}`,
+ `${'file' in f ? f.file : 'not inline-anchorable (MR description)'} · ${f.fix} · kind:${f.kind}`
+ );
+
+const tileOrigin = {
+ presentation: 'form' as const,
+ surface: 'board',
+ tabId: 'w41:t1',
+ worktree: `${TREES}/juniper`,
+ paneId: 'w41:p1',
+ runId: '20261027-142817-249f-11804',
+};
+
+const reviewMeta = {
+ label: 'review gate !826',
+ headline: 'Your review of !826 is ready',
+ summary: 'Pick what to post',
+};
+
+const { f1, f2, f3, f4, f5, f6, f7 } = TILE_FINDINGS;
+
+export const reviewFirstRound: StoryGate = {
+ mr: TILE_MR,
+ diffs: {
+ fetchFileDiff: async ({ file }) => TILE_DIFFS[file] ?? null,
+ loadView: () => import('./DiffView.tsx'),
+ },
+ gate: {
+ gateId: 'story-review-first-round',
+ subject: mrSubject(826),
+ kind: 'review-post',
+ label: 'review gate !826',
+ status: 'open',
+ openedAt: minutesAgo(8),
+ meta: reviewMeta,
+ domain: 'review',
+ origin: tileOrigin,
+ context: ctx({
+ 'gate-ctx': 'review@1',
+ readiness: 'with-fixes',
+ summary:
+ 'The cache does what the ticket asks for one region, and the tests are red on main and green here. The key drops the region, so a second region reads the first one’s tiles; that one blocks.',
+ findings: { critical: 1, important: 2, minor: 4 },
+ editable: true,
+ summaryComments: {
+ comment: 'Left a few comments! The region key one is blocking for me.',
+ approve:
+ 'Looks good to me, approving. Left a few inline notes to pick up on the way.',
+ },
+ }),
+ questions: [
+ question(
+ 'findings-1',
+ 'Post which findings to !826?',
+ true,
+ [f1, f2, f3, f4].map(findingOption),
+ ctx({ 'gate-ctx': 'findings@1', findings: [f1, f2, f3, f4] })
+ ),
+ question(
+ 'findings-2',
+ 'Post which findings to !826?',
+ true,
+ [f5, f6, f7].map(findingOption),
+ ctx({ 'gate-ctx': 'findings@1', findings: [f5, f6, f7] })
+ ),
+ question(
+ 'outcome',
+ 'Verdict on !826: ready once the cache key carries the region',
+ false,
+ [
+ opt(
+ 'comment',
+ 'Comment (recommended)',
+ 'post the picked findings and leave approval for after the key fix'
+ ),
+ opt(
+ 'approve',
+ 'Approve',
+ 'post the picked findings and approve now despite the key bug'
+ ),
+ ]
+ ),
+ ],
+ },
+};
+
+// Invented code for the two files the findings sit on, as GitLab's MR diffs
+// API returns them: per-file hunks with no file headers.
+const CACHE_PATCH = `@@ -24,8 +24,14 @@ export interface TileRequest {
+ z: number;
+ x: number;
+ y: number;
+ }
+
++const TTL_MS = 15 * 60_000;
++
++const tileKey = (t: TileRequest) => \`\${t.z}/\${t.x}/\${t.y}\`;
++
++const entries = new Map();
++
+ export async function getTile(t: TileRequest): Promise {
+ const key = tileKey(t);
+ const entry = entries.get(key);
+@@ -44,13 +50,26 @@ export async function getTile(t: TileRequest): Promise {
+ if (entry && entry.expiresAt > Date.now()) return entry.tile;
+
+ try {
+ const tile = await fetchTile(t);
+- return tile;
++ entries.set(key, { tile, expiresAt: Date.now() + TTL_MS });
++ evict();
++ return tile;
+ } catch (err) {
+- throw err;
++ if (entry) return entry.tile;
++ throw err;
+ }
+ }
+
++const maxEntries = 400;
++
++function evict() {
++ if (entries.size <= maxEntries) return;
++ const oldest = [...entries.entries()]
++ .sort(([, a], [, b]) => a.expiresAt - b.expiresAt)
++ .slice(0, entries.size - maxEntries);
++ for (const [key] of oldest) entries.delete(key);
++}
++
+ export function clearTiles() {
+ entries.clear();
+ }
+`;
+
+const CACHE_TEST_PATCH = `@@ -98,6 +98,23 @@ describe('getTile', () => {
+ expect(fetchTile).toHaveBeenCalledTimes(1);
+ });
+
++ it('serves a cached tile inside the TTL', async () => {
++ const tiles = await loadFixtureTiles();
++ const key = '6/12/18';
++ fetchTile.mockResolvedValueOnce(findTile(tiles, key));
++ await getTile({ region: 'north', z: 6, x: 12, y: 18 });
++ const again = await getTile({ region: 'north', z: 6, x: 12, y: 18 });
++ expect(fetchTile).toHaveBeenCalledTimes(1);
++ expect(again).toBe(findTile(tiles, key) ?? tiles[0]);
++ });
++
++ it('evicts the oldest tiles past the cap', async () => {
++ for (let i = 0; i < 401; i++)
++ await getTile({ region: 'north', z: 6, x: i, y: 0 });
++ await getTile({ region: 'north', z: 6, x: 0, y: 0 });
++ expect(fetchTile).toHaveBeenCalledTimes(402);
++ });
++
+ it('rethrows when nothing is cached', async () => {
+ fetchTile.mockRejectedValueOnce(new Error('offline'));
+ await expect(getTile(req)).rejects.toThrow('offline');
+`;
+
+const TILE_DIFFS: Record = {
+ [CACHE]: {
+ path: CACHE,
+ patch: CACHE_PATCH,
+ forge: 'gitlab',
+ webUrl: `${FORGE}/826/diffs#tileCache.ts`,
+ },
+ [CACHE_TEST]: {
+ path: CACHE_TEST,
+ patch: CACHE_TEST_PATCH,
+ forge: 'gitlab',
+ webUrl: `${FORGE}/826/diffs#tileCache.test.ts`,
+ },
+};
+
+const carryover = (
+ n: number,
+ thread: string,
+ label: string,
+ defaults: { post: boolean; resolve: boolean },
+ carry: {
+ round: number;
+ call: 'fixed' | 'not-fixed' | 'pushback-accepted' | 'pushback-rejected';
+ original: string;
+ reply: string;
+ file?: string;
+ authorReply?: string;
+ note?: string;
+ }
+) =>
+ question(
+ `thread-${n}`,
+ label,
+ true,
+ [
+ opt(
+ `post:${thread}`,
+ defaults.post ? 'Post reply (recommended)' : 'Post reply'
+ ),
+ opt(
+ `resolve:${thread}`,
+ defaults.resolve ? 'Resolve thread (recommended)' : 'Resolve thread'
+ ),
+ ],
+ ctx({ 'gate-ctx': 'carryover@1', thread, ...carry })
+ );
+
+const TILE_THREADS = {
+ key: '5d1c0e7a93b24f6a8c0e1f2d3b4a5c6d7e8f9a01',
+ stale: 'a07e4c2b9d1f3e5a7c9b0d2f4e6a8c1b3d5f7e92',
+ evidence: 'c3f9e1d7b5a3c1e9f7d5b3a1c9e7f5d3b1a9c8e4',
+};
+
+const N1 = {
+ id: 'n1',
+ severity: 'minor',
+ title: 'Stale flag is never cleared on a fresh fetch',
+ body: 'The new `stale` flag is set when the refetch fails, but the success path builds a fresh entry without it, so it only clears by accident.',
+ file: `${CACHE}:54`,
+ fix: 'Set stale: false explicitly on the success path.',
+ kind: 'nitpick',
+ disposition: 'new',
+ comment:
+ '**nitpick:** the success path relies on the new entry not having `stale` at all. an explicit `stale: false` would say it out loud.',
+};
+
+/** Round 2 of the same review: the key fix landed, the stale-tile answer
+ was a fair pushback, the evidence is still missing, and one small new
+ finding turned up. */
+export const reviewReReview: StoryGate = {
+ mr: {
+ ...TILE_MR,
+ diff: { additions: 213, deletions: 44, filesChanged: 6 },
+ review: { status: 'done', rounds: 1, outcomes: ['comment'] },
+ },
+ diffs: reviewFirstRound.diffs!,
+ gate: {
+ gateId: 'story-review-re-review',
+ subject: mrSubject(826),
+ kind: 'review-post',
+ label: 'review gate !826',
+ status: 'open',
+ openedAt: minutesAgo(5),
+ meta: reviewMeta,
+ domain: 'review',
+ origin: { ...tileOrigin, runId: '20261028-103324-937e-11805' },
+ context: ctx({
+ 'gate-ctx': 'review@1',
+ readiness: 'with-fixes',
+ summary:
+ 'The region key is fixed and pinned by a two-region test. The stale-tile pushback holds up. The region-switch evidence is still missing, which is the one thing left before approval.',
+ findings: { minor: 1 },
+ round: 2,
+ re_review: true,
+ prior: { addressed: 2, still_open: 1 },
+ editable: true,
+ summaryComments: {
+ comment:
+ 'Thanks for the updates! Replied on the threads; the evidence is the last thing for me.',
+ approve:
+ 'Looks good to me, thanks for the quick turnaround! Replied on the threads.',
+ },
+ }),
+ questions: [
+ carryover(
+ 1,
+ TILE_THREADS.key,
+ `${CACHE}:31`,
+ { post: true, resolve: true },
+ {
+ round: 1,
+ call: 'fixed',
+ file: `${CACHE}:31`,
+ original: f1.comment,
+ authorReply:
+ 'good catch. the region goes first in the key now, and there’s a test that asks two regions for the same tile.',
+ note: 'tileKey (tileCache.ts:31) now starts with region; tileCache.test.ts:130 asks north and south for 6/12/18 and expects two fetches.',
+ reply: 'thanks, that’s exactly it. resolving.',
+ }
+ ),
+ carryover(
+ 2,
+ TILE_THREADS.stale,
+ `${CACHE}:58`,
+ { post: true, resolve: true },
+ {
+ round: 1,
+ call: 'pushback-accepted',
+ file: `${CACHE}:58`,
+ original: f2.comment,
+ authorReply:
+ 'that’s on purpose: an old forecast beats a blank map when the tile service is down. i added a `stale` flag so the map can dim it, and the map work is ACME-3431.',
+ note: 'the stale flag lands on the entry (tileCache.ts:61) and ACME-3431 covers dimming; serving old tiles during an outage is the documented product call.',
+ reply:
+ 'fair, an old forecast is better than a blank map. the flag covers what i was after.',
+ }
+ ),
+ carryover(
+ 3,
+ TILE_THREADS.evidence,
+ 'General thread',
+ { post: true, resolve: false },
+ {
+ round: 1,
+ call: 'not-fixed',
+ original: f3.comment,
+ note: 'the description still has no capture of a region switch; no reply on the thread.',
+ reply:
+ 'bumping this one: could you grab a quick capture of a region switch on the forecast map?',
+ }
+ ),
+ question(
+ 'findings-1',
+ 'Post which findings to !826?',
+ true,
+ [
+ opt(
+ 'n1',
+ '[Minor] Stale flag is never cleared on a fresh fetch',
+ `${CACHE}:54 · Set stale: false explicitly on the success path. · kind:nitpick`
+ ),
+ ],
+ ctx({ 'gate-ctx': 'findings@1', findings: [N1] })
+ ),
+ question(
+ 'outcome',
+ 'Verdict on !826: ready once the evidence is up',
+ false,
+ [
+ opt(
+ 'comment',
+ 'Comment (recommended)',
+ 'Posts the thread replies and the new finding, and leaves approval for later.'
+ ),
+ opt(
+ 'approve',
+ 'Approve',
+ 'Posts the thread replies and the new finding, and approves the MR.'
+ ),
+ ]
+ ),
+ ],
+ },
+};
+
+// --- Respond: my MR, reviewed by Renee -------------------------------------
+
+const PIN_MR = boardMr(
+ 831,
+ 'ACME-3436: keep station pins when the map re-centers',
+ 'pat',
+ 'acme-3436-keep-station-pins',
+ [97, 22, 4],
+ [0, 1]
+);
+
+const PIN_THREADS = {
+ t1: '7b2e9c4a1d6f8e3b5a0c2d4f6e8a1b3c5d7e9f02',
+ t2: 'e4a1c7b3d9f5e2a8c6b0d4f1e7a3c9b5d2f8e613',
+ t3: '2c8f4a6e1b9d3f7a5c0e8b2d6f4a1c9e3b7d5f24',
+ t4: '9d3b7f1e5a2c8d4b6f0a3e9c1d7b5f2a8e4c6d35',
+ t5: 'f6a0d2c8e4b1f7a3d9c5e2b8f0a6d4c1e7b3f946',
+};
+
+const PIN_FILES = {
+ t1: 'StationPins.tsx:48',
+ t2: 'useMapCenter.ts:22',
+ t3: 'StationPins.test.tsx:91',
+ t4: 'stationLayer.ts:130',
+ t5: 'General thread',
+};
+
+const PIN_PLAN = {
+ t1: {
+ fix: 'Key the pin list by station id instead of the array index.',
+ claim:
+ 'the pins are keyed by index, so a re-center that reorders stations remounts every pin and drops the open popover.',
+ note: 'confirmed: StationPins.tsx:48 keys by index, and the station list is re-sorted by distance on every re-center.',
+ reply:
+ 'pins are keyed by station id now, so a re-center that reorders them keeps the open popover.',
+ },
+ t2: {
+ fix: 'Read the center from the map’s moveend event instead of a 200ms poll.',
+ claim:
+ 'polling the center every 200ms keeps a timer running while the map is idle; the map already fires `moveend`.',
+ note: 'confirmed: useMapCenter.ts:22 sets an interval with no idle check, and moveend fires after every pan and zoom.',
+ reply: 'switched to `moveend`, so nothing runs while the map is idle.',
+ },
+ t3: {
+ fix: 'Add a test that re-centers with a popover open and expects it to stay open.',
+ claim:
+ 'the tests only check pin count; nothing covers the bug the ticket is about, a popover closing on re-center.',
+ note: 'confirmed: StationPins.test.tsx has no popover assertion.',
+ reply:
+ 'added a test that opens a popover, re-centers, and expects it to stay open. it fails on main.',
+ },
+ t4: {
+ fix: 'Drop the unused cluster radius export.',
+ claim: '`CLUSTER_RADIUS` is exported but nothing imports it.',
+ note: 'confirmed: no import of CLUSTER_RADIUS anywhere in the repo.',
+ reply: 'dropped it.',
+ },
+};
+
+const planThread = (
+ n: number,
+ key: keyof typeof PIN_PLAN,
+ severity: 'blocking' | 'non-blocking'
+) =>
+ question(
+ `thread-${n}`,
+ PIN_FILES[key],
+ false,
+ [
+ opt(
+ `reply:${PIN_THREADS[key]}`,
+ 'Reply only',
+ 'Post the drafted reply and change no code.'
+ ),
+ opt(
+ `fix:${PIN_THREADS[key]}`,
+ 'Fix the code (Recommended)',
+ PIN_PLAN[key].fix
+ ),
+ opt(
+ `skip:${PIN_THREADS[key]}`,
+ 'Skip this thread',
+ 'Post nothing and change nothing for this thread.'
+ ),
+ ],
+ ctx({
+ 'gate-ctx': 'thread@1',
+ author: 'renee',
+ severity,
+ claim: { summary: PIN_PLAN[key].claim },
+ verdict: { call: 'valid', note: PIN_PLAN[key].note },
+ reply: { kind: 'direction', text: PIN_PLAN[key].reply },
+ })
+ );
+
+const pinOrigin = {
+ presentation: 'form' as const,
+ surface: 'board',
+ tabId: 'w44:t1',
+ worktree: `${TREES}/hazel`,
+ paneId: 'w44:p1',
+ runId: '20261028-091512-4c7d-11806',
+};
+
+// Invented code for the respond threads. Respond threads name their file
+// by basename, so the story's diff source answers to that.
+const pinDiff = (file: string, patch: string): FileDiff => ({
+ path: file,
+ patch,
+ forge: 'gitlab',
+ webUrl: `${FORGE}/831/diffs#${file}`,
+});
+
+const PLAN_PATCHES: Record = {
+ 'StationPins.tsx': `@@ -44,6 +44,7 @@ export function StationPins({ stations }: Props) {
+ const map = useMap();
+
+ return (
+
++ {stations.map((s, i) => )}
+
+ );
+`,
+ 'useMapCenter.ts': `@@ -19,5 +19,6 @@ import { useInterval } from './useInterval';
+
+ export function useMapCenter(map: MapView) {
+ const [center, setCenter] = useState(map.center);
++ useInterval(() => setCenter(map.center), 200);
+ return center;
+ }
+`,
+ 'StationPins.test.tsx': `@@ -88,5 +88,8 @@ describe('StationPins', () => {
+ });
+
++ it('renders a pin per station', () => {
++ render( );
++ expect(screen.getAllByRole('button')).toHaveLength(fixtures.length);
+ });
+`,
+ 'stationLayer.ts': `@@ -127,4 +127,5 @@ export function stationLayer(stations: Station[]) {
+ return source;
+ }
+
++export const CLUSTER_RADIUS = 48;
+`,
+};
+
+const POST_PATCHES: Record = {
+ ...PLAN_PATCHES,
+ 'StationPins.tsx': `@@ -44,6 +44,7 @@ export function StationPins({ stations }: Props) {
+ const map = useMap();
+
+ return (
+
++ {stations.map(s => )}
+
+ );
+`,
+};
+
+const pinDiffs = (patches: Record): DiffSource => ({
+ fetchFileDiff: async ({ file }) =>
+ patches[file] ? pinDiff(file, patches[file]!) : null,
+ loadView: () => import('./DiffView.tsx'),
+});
+
+export const respondPlan: StoryGate = {
+ diffs: pinDiffs(PLAN_PATCHES),
+ mr: PIN_MR,
+ gate: {
+ gateId: 'story-respond-plan',
+ subject: mrSubject(831),
+ kind: 'respond-plan',
+ label: 'respond gate !831',
+ status: 'open',
+ openedAt: minutesAgo(9),
+ meta: { label: 'respond gate !831' },
+ domain: 'respond',
+ origin: pinOrigin,
+ context: ctx({
+ 'gate-ctx': 'plan@1',
+ reviewer: 'renee',
+ round: 1,
+ threads: { total: 5, blocking: 1 },
+ adjudication: '4 fix, 1 reply',
+ }),
+ questions: [
+ planThread(1, 't1', 'blocking'),
+ planThread(2, 't2', 'non-blocking'),
+ planThread(3, 't3', 'non-blocking'),
+ planThread(4, 't4', 'non-blocking'),
+ question(
+ 'thread-5',
+ PIN_FILES.t5,
+ false,
+ [
+ opt(
+ `reply:${PIN_THREADS.t5}`,
+ 'Reply only (Recommended)',
+ 'Post the drafted reply and change no code.'
+ ),
+ opt(
+ `fix:${PIN_THREADS.t5}`,
+ 'Fix the code',
+ 'Nothing to change; the thread asks for a re-review ping.'
+ ),
+ opt(
+ `skip:${PIN_THREADS.t5}`,
+ 'Skip this thread',
+ 'Post nothing and change nothing for this thread.'
+ ),
+ ],
+ ctx({
+ 'gate-ctx': 'thread@1',
+ author: 'renee',
+ severity: 'none',
+ claim: {
+ summary:
+ 'Left a few comments, the index key one is the main one. Ping me when it’s ready for another look.',
+ },
+ verdict: {
+ call: 'valid',
+ note: 'process only; reply once threads 1-4 are in.',
+ },
+ reply: { kind: 'verbatim', text: 'Ready for another look!' },
+ })
+ ),
+ question('code-changes', 'Approve the proposed code changes?', false, [
+ opt('approve', 'Approve the changes'),
+ opt('revise', 'Revise the proposal'),
+ opt('skip', 'Skip code changes'),
+ ]),
+ ],
+ },
+};
+
+const postThread = (
+ n: number,
+ key: keyof typeof PIN_THREADS,
+ reply: { verb: 'reply' | 'fix'; text: string; sha?: string },
+ resolvable = true
+) =>
+ question(
+ `thread-${n}`,
+ PIN_FILES[key],
+ true,
+ [
+ opt(
+ `post:${PIN_THREADS[key]}`,
+ 'Post this reply (Recommended)',
+ 'Post this reply to the thread.'
+ ),
+ ...(resolvable
+ ? [
+ opt(
+ `resolve:${PIN_THREADS[key]}`,
+ 'Resolve the thread (Recommended)',
+ 'Resolve the thread after its reply.'
+ ),
+ ]
+ : []),
+ ],
+ ctx({
+ 'gate-ctx': 'reply@1',
+ thread: PIN_THREADS[key],
+ file: PIN_FILES[key],
+ ...reply,
+ })
+ );
+
+/** The same MR after the fixes landed: four fix replies with their commits,
+ and the re-review ping on the Overview thread, which GitLab cannot
+ resolve. */
+export const respondPost: StoryGate = {
+ diffs: pinDiffs(POST_PATCHES),
+ // The answered plan gate rides on the MR, as on the board, so each reply
+ // card shows the reviewer's comment above it.
+ mr: {
+ ...PIN_MR,
+ // A second response to Renee: her first review commented.
+ respond: { status: 'drafting', responseRound: 2 },
+ respondOutcomes: { renee: ['comment', 'comment'] },
+ gates: [
+ {
+ ...respondPlan.gate,
+ gateId: 'story-respond-plan-answered',
+ status: 'answered',
+ answers: {
+ 'thread-1': `fix:${PIN_THREADS.t1}`,
+ 'thread-2': `fix:${PIN_THREADS.t2}`,
+ 'thread-3': `fix:${PIN_THREADS.t3}`,
+ 'thread-4': `fix:${PIN_THREADS.t4}`,
+ 'thread-5': `reply:${PIN_THREADS.t5}`,
+ 'code-changes': 'approve',
+ },
+ answeredBy: 'board',
+ answeredAt: minutesAgo(8),
+ },
+ ],
+ } as BoardMRWithReview,
+ gate: {
+ gateId: 'story-respond-post',
+ subject: mrSubject(831),
+ kind: 'respond-post',
+ label: 'respond gate !831',
+ status: 'open',
+ openedAt: minutesAgo(4),
+ meta: { label: 'respond gate !831' },
+ domain: 'respond',
+ origin: pinOrigin,
+ context: ctx({
+ 'gate-ctx': 'post@1',
+ reviewer: 'renee',
+ round: 1,
+ replies: 5,
+ fixes: [
+ { sha: '3e7a9c1b5d2' },
+ { sha: '8f2d4b6a0c9' },
+ { sha: 'b5c1e7d3a9f' },
+ { sha: '0a6e2c8f4b1' },
+ ],
+ }),
+ questions: [
+ postThread(1, 't1', {
+ verb: 'fix',
+ sha: '3e7a9c1b5d2',
+ text: 'pins are keyed by station id now, so a re-center that reorders them keeps the open popover.',
+ }),
+ postThread(2, 't2', {
+ verb: 'fix',
+ sha: '8f2d4b6a0c9',
+ text: 'switched to `moveend`, so nothing runs while the map is idle. the hook also stops listening on unmount now, which the poll never did.',
+ }),
+ postThread(3, 't3', {
+ verb: 'fix',
+ sha: 'b5c1e7d3a9f',
+ text: 'added a test that opens a popover, re-centers, and expects it to stay open. it fails on main.',
+ }),
+ postThread(4, 't4', {
+ verb: 'fix',
+ sha: '0a6e2c8f4b1',
+ text: 'dropped it.',
+ }),
+ postThread(
+ 5,
+ 't5',
+ { verb: 'reply', text: 'Ready for another look!' },
+ false
+ ),
+ ],
+ },
+};
diff --git a/apps/board/src/client/types.ts b/apps/board/src/client/types.ts
index 2865c652dc..c6ae30effc 100644
--- a/apps/board/src/client/types.ts
+++ b/apps/board/src/client/types.ts
@@ -59,6 +59,8 @@ export interface ReviewInfo {
noPack?: boolean;
/** The highest review round the board has recorded for this MR. */
rounds?: number;
+ /** What each recorded round posted, in round order. */
+ outcomes?: Array<'comment' | 'approve'>;
}
export interface RespondInfo {
status: RespondStatus;
diff --git a/apps/board/src/data.ts b/apps/board/src/data.ts
index 34748ef75d..7c819fb806 100644
--- a/apps/board/src/data.ts
+++ b/apps/board/src/data.ts
@@ -57,6 +57,9 @@ export type BoardMR = MRDashboardProps & {
respondTargets?: RespondTarget[];
/** Highest respond round per username, from the rounds table. */
respondRounds?: Record;
+ /** What each person had posted when each of their response rounds began,
+ in round order; null for a round recorded before this was kept. */
+ respondOutcomes?: Record>;
pipelineState: PipelineState;
/** Draft (unfinished) MR. Only your own drafts reach the board, so this also
means "yours, and not marked ready yet" -- see buildBoard. */
diff --git a/apps/board/src/respond-rounds.ts b/apps/board/src/respond-rounds.ts
index 9f1a6f4bb6..fc9dcef338 100644
--- a/apps/board/src/respond-rounds.ts
+++ b/apps/board/src/respond-rounds.ts
@@ -1,13 +1,26 @@
import { Database } from 'bun:sqlite';
import { getStateDb } from './state/index.ts';
+import { personApproved } from './view.ts';
+
+/** What the person had posted on the MR when a response round began. */
+export type ReviewerOutcome = 'comment' | 'approve';
+
+/** What the person has posted on the MR now, as a response round records it. */
+export function reviewerOutcome(
+ mr: Parameters[0],
+ person: string
+): ReviewerOutcome {
+ return personApproved(mr, person) ? 'approve' : 'comment';
+}
/** Records the next response round for this person and returns it (1 on the first). */
export function recordRespondRound(
mrUrl: string,
person: string,
now: number = Date.now(),
- db: Database = getStateDb()
+ db: Database = getStateDb(),
+ outcome?: ReviewerOutcome
): number {
return db.transaction(() => {
const row = db
@@ -17,8 +30,8 @@ export function recordRespondRound(
.get(mrUrl, person) as { round: number };
const round = row.round + 1;
db.run(
- 'INSERT INTO respond_rounds (mr_url, person, round, started_at) VALUES (?, ?, ?, ?)',
- [mrUrl, person, round, now]
+ 'INSERT INTO respond_rounds (mr_url, person, round, started_at, outcome) VALUES (?, ?, ?, ?, ?)',
+ [mrUrl, person, round, now, outcome ?? null]
);
return round;
})();
@@ -55,13 +68,48 @@ export function readRespondRounds(
return out;
}
+/** Each person's recorded outcomes per MR, in round order; null for a
+ round recorded before outcomes were. */
+export function readRespondOutcomes(
+ db: Database = getStateDb()
+): Map>> {
+ const rows = db
+ .query(
+ 'SELECT mr_url, person, outcome FROM respond_rounds ORDER BY mr_url, person, round'
+ )
+ .all() as Array<{ mr_url: string; person: string; outcome: string | null }>;
+ const out = new Map>>();
+ for (const r of rows) {
+ const people = out.get(r.mr_url) ?? {};
+ (people[r.person] ??= []).push(r.outcome as ReviewerOutcome | null);
+ out.set(r.mr_url, people);
+ }
+ return out;
+}
+
export function attachRespondRounds(
mrs: T[],
- rounds: Map>
-): Array }> {
- return mrs.map(mr =>
- mr.webUrl && rounds.has(mr.webUrl)
- ? { ...mr, respondRounds: rounds.get(mr.webUrl) }
- : mr
- );
+ rounds: Map>,
+ outcomes: Map<
+ string,
+ Record>
+ > = new Map()
+): Array<
+ T & {
+ respondRounds?: Record;
+ respondOutcomes?: Record>;
+ }
+> {
+ return mrs.map(mr => {
+ if (!mr.webUrl) return mr;
+ const r = rounds.get(mr.webUrl);
+ const o = outcomes.get(mr.webUrl);
+ return r || o
+ ? {
+ ...mr,
+ ...(r ? { respondRounds: r } : {}),
+ ...(o ? { respondOutcomes: o } : {}),
+ }
+ : mr;
+ });
}
diff --git a/apps/board/src/review-rounds.ts b/apps/board/src/review-rounds.ts
index ed5e78d92f..f95f571f1d 100644
--- a/apps/board/src/review-rounds.ts
+++ b/apps/board/src/review-rounds.ts
@@ -110,6 +110,22 @@ export function latestRounds(db: Database = getStateDb()): Map {
return new Map(rows.map(r => [r.mr_url, r.round]));
}
+/** What each MR's recorded rounds posted, in round order. */
+export function roundOutcomes(
+ db: Database = getStateDb()
+): Map {
+ const rows = db
+ .query('SELECT mr_url, outcome FROM review_rounds ORDER BY mr_url, round')
+ .all() as Array<{ mr_url: string; outcome: string }>;
+ const out = new Map();
+ for (const r of rows) {
+ const list = out.get(r.mr_url) ?? [];
+ list.push(r.outcome as ReviewOutcome);
+ out.set(r.mr_url, list);
+ }
+ return out;
+}
+
/** What a later round needs: every finding skipped so far that no round has
brought back, and every thread confirmed fixed and left to the author. */
export function ledgerView(rounds: ReviewRound[]): LedgerView {
diff --git a/apps/board/src/review-state.ts b/apps/board/src/review-state.ts
index 45499829d6..cb72e46084 100644
--- a/apps/board/src/review-state.ts
+++ b/apps/board/src/review-state.ts
@@ -257,13 +257,26 @@ export function dropPrunedReviewState(
export function attachReviews(
mrs: T[],
reviews: Map,
- latestRounds: ReadonlyMap = new Map()
-): Array {
+ latestRounds: ReadonlyMap = new Map(),
+ roundOutcomes: ReadonlyMap = new Map()
+): Array<
+ T & {
+ review?: ReviewState & { rounds?: number; outcomes?: ReviewOutcome[] };
+ }
+> {
return mrs.map(mr => {
const review = mr.webUrl ? reviews.get(mr.webUrl) : undefined;
if (!review) return mr;
const rounds = latestRounds.get(mr.webUrl!);
- return { ...mr, review: rounds ? { ...review, rounds } : review };
+ const outcomes = roundOutcomes.get(mr.webUrl!);
+ return {
+ ...mr,
+ review: {
+ ...review,
+ ...(rounds ? { rounds } : {}),
+ ...(outcomes ? { outcomes } : {}),
+ },
+ };
});
}
diff --git a/apps/board/src/server.ts b/apps/board/src/server.ts
index 29da95b2c5..df517ad1da 100644
--- a/apps/board/src/server.ts
+++ b/apps/board/src/server.ts
@@ -7,13 +7,13 @@ import {
type EventBridgeRule,
} from '@mattstack/app-server/event-bridge';
import { shellHandoff } from '@mattstack/app-server/shell-handoff';
-import type { MRDetail, PullRequest } from '@mattstack/glance';
import {
GitLabProvider,
NoteMutator,
parseRepoId,
ReadBackFailedError,
} from '@mattstack/glance';
+import type { MRDetail, PullRequest } from '@mattstack/glance';
import {
eventsHead,
eventsList,
@@ -282,8 +282,10 @@ import { launchReopen, type ReopenIo } from './reopen-launch.ts';
import {
attachRespondRounds,
deleteRespondRound,
+ readRespondOutcomes,
readRespondRounds,
recordRespondRound,
+ reviewerOutcome,
} from './respond-rounds.ts';
import {
attachResponds,
@@ -301,7 +303,7 @@ import {
} from './respond-state.ts';
import { respondTargets } from './respond-targets.ts';
import { launchReReview, reviewLaunchForTab } from './review-launch.ts';
-import { dropRounds, latestRounds } from './review-rounds.ts';
+import { dropRounds, latestRounds, roundOutcomes } from './review-rounds.ts';
import {
attachReviews,
parseReviewRequestBody,
@@ -1120,10 +1122,11 @@ async function withRowState(mrs: BoardMR[]) {
const withReviewState = attachDoctors(
attachRespondRounds(
attachResponds(
- attachReviews(mrs, readReviewStates(), latestRounds()),
+ attachReviews(mrs, readReviewStates(), latestRounds(), roundOutcomes()),
readRespondStates()
),
- readRespondRounds()
+ readRespondRounds(),
+ readRespondOutcomes()
),
readDoctorStates()
);
@@ -2784,7 +2787,13 @@ const httpServer = Bun.serve({
return new Response(dedup.reason, { status: 409 });
}
const responseRound = person
- ? recordRespondRound(parsed.mrUrl, person)
+ ? recordRespondRound(
+ parsed.mrUrl,
+ person,
+ undefined,
+ undefined,
+ reviewerOutcome(mr, person)
+ )
: undefined;
const statePath = respondFilePath(parsed.mrUrl);
writeRespondState(statePath, {
diff --git a/apps/board/src/state/db.ts b/apps/board/src/state/db.ts
index d4e86072e5..fe03cb49c2 100644
--- a/apps/board/src/state/db.ts
+++ b/apps/board/src/state/db.ts
@@ -8,7 +8,7 @@ import { getKvValue } from './kv-blob.ts';
import { importLegacyState } from './legacy-import.ts';
export type DbFlavor = 'server' | 'cli';
-export const SCHEMA_VERSION = 5;
+export const SCHEMA_VERSION = 6;
const BUSY_TIMEOUT_MS: Record = { server: 250, cli: 5000 };
const MIGRATION_BUSY_TIMEOUT_MS = 5000;
@@ -154,6 +154,15 @@ const MIGRATIONS: Migration[] = [
// v5: one row per response round to one person, so each person's round
// count survives the MR's single respond lane being rewritten per launch.
db => db.run(V5_SCHEMA),
+ // v6: what the person had posted when a response round began (comment or
+ // approve), so the respond gate can list each earlier round's review.
+ db => {
+ const cols = db.query('PRAGMA table_info(respond_rounds)').all() as Array<{
+ name: string;
+ }>;
+ if (!cols.some(c => c.name === 'outcome'))
+ db.run('ALTER TABLE respond_rounds ADD COLUMN outcome TEXT');
+ },
];
// SCHEMA_VERSION is the public constant other modules reason about;
diff --git a/apps/board/src/style.css b/apps/board/src/style.css
index 5b808e7d6f..dead0da63a 100644
--- a/apps/board/src/style.css
+++ b/apps/board/src/style.css
@@ -3592,7 +3592,7 @@ a.tui-id-card-ref:focus-visible {
align-items: center;
gap: 16px;
flex-shrink: 0;
- padding: 14px 20px;
+ padding: 8px 20px;
border-bottom: 1px solid var(--border);
}
.tui-gate-sheet-lead {
@@ -3609,9 +3609,9 @@ a.tui-id-card-ref:focus-visible {
min-width: 0;
}
.tui-gate-sheet-title {
- font-weight: 700;
- font-size: 20px;
- color: var(--text-1);
+ font-weight: 600;
+ font-size: 15px;
+ color: var(--text-2);
}
.tui-gate-sheet-actions {
display: flex;
@@ -3658,7 +3658,7 @@ a.tui-id-card-ref:focus-visible {
}
.tui-gate-queue-pos {
line-height: 1.2;
- font-size: var(--gate-font-meta);
+ font-size: 11.5px;
font-variant-numeric: tabular-nums;
color: var(--text-2);
white-space: nowrap;
@@ -3786,7 +3786,7 @@ a.tui-id-card-ref:focus-visible {
z-index: 2;
display: flex;
flex-wrap: wrap;
- align-items: baseline;
+ align-items: center;
gap: 4px 12px;
margin: -22px -28px 28px -26px;
padding: 16px 28px 16px 26px;
@@ -3806,6 +3806,261 @@ a.tui-id-card-ref:focus-visible {
font-size: var(--gate-font-body);
color: var(--text-2);
}
+.tui-sheet-round-aside {
+ align-self: center;
+ margin-left: auto;
+}
+.tui-sheet-round-n {
+ display: flex;
+ align-items: center;
+ gap: 8px;
+}
+.tui-sheet-round-n > svg {
+ flex-shrink: 0;
+ color: var(--text-3);
+}
+/* Where a gate sits in the review loop. A step's colour says what it was:
+ accent for a review that commented, ok for an approval, purple for the
+ response (the board's commented, approved and comments-resolved pills).
+ A line takes the colour of the step it leaves. */
+.tui-tl,
+.tui-tl-response-steps {
+ display: flex;
+ align-items: center;
+ margin: 0;
+ padding: 0;
+ list-style: none;
+}
+.tui-tl:has(.tui-tl-response) {
+ align-items: flex-end;
+}
+.tui-tl:has(.tui-tl-response) > li:not(.tui-tl-response) {
+ margin-bottom: 8px;
+}
+.tui-tl > li,
+.tui-tl-response-steps > li {
+ display: flex;
+ align-items: center;
+ gap: 7px;
+ min-height: 26px;
+ font-size: 12.5px;
+ font-weight: 500;
+ white-space: nowrap;
+ color: var(--text-3);
+}
+.tui-tl > li + li::before,
+.tui-tl-response-steps > li + li::before {
+ content: '';
+ width: 24px;
+ height: 2px;
+ /* 8px clear on both sides: the item's own 7px gap makes up the right. */
+ margin: 0 1px 0 8px;
+ border-radius: 1px;
+ background: var(--border-soft);
+}
+.tui-tl-response-steps > li + li::before {
+ width: 20px;
+}
+.tui-tl > li + .tui-tl-response {
+ position: relative;
+ margin-left: 40px;
+}
+.tui-tl > li + .tui-tl-response::before {
+ position: absolute;
+ right: calc(100% + 8px);
+ top: calc(50% + 5px);
+ margin: 0;
+}
+.tui-tl-round-chip {
+ display: inline-flex;
+ align-items: center;
+ gap: 4px;
+ height: 24px;
+ padding: 0 8px;
+ border: 0;
+ border-radius: 6px;
+ background: color-mix(in srgb, var(--fg) 6%, var(--card));
+ font: inherit;
+ font-size: 13px;
+ font-weight: 600;
+ color: var(--text-1);
+}
+.tui-tl-round-chip[data-menu] {
+ cursor: pointer;
+}
+.tui-tl-round-chip[data-menu]:hover,
+.tui-tl-round-chip[data-menu][data-popup-open] {
+ background: color-mix(in srgb, var(--fg) 10%, var(--card));
+}
+.tui-tl-round-chip > svg {
+ color: var(--text-3);
+}
+/* The round menu: a label column, then who did what, rounds ruled apart. */
+.tui-tl-history {
+ display: flex;
+ flex-direction: column;
+ gap: 14px;
+ width: 300px;
+ padding: 16px;
+}
+.tui-tl-history-round {
+ display: flex;
+ gap: 12px;
+}
+.tui-tl-history-round + .tui-tl-history-round {
+ padding-top: 14px;
+ border-top: 1px solid var(--border-soft);
+}
+.tui-tl-history-n {
+ flex: 0 0 60px;
+ line-height: 20px;
+ font-size: 12px;
+ font-weight: 500;
+ color: var(--text-3);
+}
+.tui-tl-history-round[data-current] .tui-tl-history-n {
+ font-weight: 700;
+ color: var(--text-1);
+}
+.tui-tl-history ul {
+ display: flex;
+ flex-direction: column;
+ gap: 8px;
+ margin: 0;
+ padding: 0;
+ list-style: none;
+}
+.tui-tl-history li {
+ display: flex;
+ align-items: center;
+ gap: 8px;
+ font-size: 13px;
+ color: var(--text-1);
+}
+/* Holds a line's place when its person is unknown, so the text still aligns. */
+.tui-tl-history-blank {
+ flex-shrink: 0;
+ width: 20px;
+ height: 20px;
+}
+.tui-tl-history li[data-now] {
+ font-weight: 600;
+}
+.tui-tl-history li[data-now='review'] {
+ color: var(--text-accent);
+}
+.tui-tl-history li[data-now='response'] {
+ color: var(--text-purple);
+}
+.tui-tl-mark {
+ box-sizing: border-box;
+ display: inline-grid;
+ place-items: center;
+ flex-shrink: 0;
+ width: 22px;
+ height: 22px;
+ border: 1.5px solid var(--border);
+ border-radius: 50%;
+ background: var(--card);
+}
+.tui-tl-step[data-state='done'] {
+ color: var(--text-1);
+}
+.tui-tl-step[aria-current] {
+ font-weight: 700;
+ color: var(--tl-text);
+}
+.tui-tl-step[data-phase='review'] {
+ --tl-fill: var(--fill-accent);
+ --tl-text: var(--text-accent);
+}
+.tui-tl-step[data-outcome='approve'] {
+ --tl-fill: var(--fill-ok);
+ --tl-text: var(--text-ok);
+}
+.tui-tl-step[data-phase='response'] {
+ --tl-fill: var(--fill-purple);
+ --tl-text: var(--text-purple);
+}
+.tui-tl-step[data-phase='review'] .tui-tl-mark {
+ width: 26px;
+ height: 26px;
+}
+.tui-tl-step[data-phase='review']:is([data-state='done'], [aria-current])
+ .tui-tl-mark {
+ border: 1px solid color-mix(in srgb, var(--tl-fill) 28%, transparent);
+ background: color-mix(in srgb, var(--tl-fill) 18%, var(--card));
+ color: var(--tl-text);
+}
+.tui-tl-step[data-phase='response']:is([data-state='done'], [aria-current])
+ .tui-tl-mark {
+ border-color: color-mix(in srgb, var(--tl-fill) 45%, transparent);
+ color: var(--tl-text);
+}
+.tui-tl-response-steps > [data-state='next'] .tui-tl-mark {
+ border-color: color-mix(in srgb, var(--fill-purple) 35%, transparent);
+}
+.tui-tl-mark > span {
+ /* Even, so it sits whole pixels in from either edge of an even mark. */
+ width: 10px;
+ height: 10px;
+ border-radius: 50%;
+ background: var(--tl-fill);
+}
+.tui-tl-step[aria-current] .tui-tl-mark {
+ position: relative;
+}
+.tui-tl-step[aria-current] .tui-tl-mark::after {
+ content: '';
+ position: absolute;
+ inset: -1px;
+ border-radius: 50%;
+ animation: tui-tl-here 2.4s ease-out infinite;
+}
+@keyframes tui-tl-here {
+ 0% {
+ box-shadow: 0 0 0 0 color-mix(in srgb, var(--tl-fill) 35%, transparent);
+ }
+ 70%,
+ 100% {
+ box-shadow: 0 0 0 6px transparent;
+ }
+}
+@media (prefers-reduced-motion: reduce) {
+ .tui-tl-step[aria-current] .tui-tl-mark::after {
+ animation: none;
+ }
+}
+.tui-tl-step[data-outcome='comment'] + li::before {
+ background: var(--fill-accent);
+}
+.tui-tl-step[data-outcome='approve'] + li::before {
+ background: var(--fill-ok);
+}
+.tui-tl-response-steps > [data-state='done'] + li::before {
+ background: var(--fill-purple);
+}
+/* Plan, Fix and Post are one phase: a purple wash, named on its top edge. */
+.tui-tl-response {
+ position: relative;
+ padding: 18px 18px 8px;
+ border-radius: 8px;
+ background: color-mix(in srgb, var(--fill-purple) 10%, transparent);
+}
+.tui-tl-response-label {
+ position: absolute;
+ top: 0;
+ left: 50%;
+ translate: -50% -50%;
+ padding: 2px 7px;
+ border: 1px solid color-mix(in srgb, var(--fill-purple) 35%, transparent);
+ border-radius: 4px;
+ background: var(--card);
+ font-size: 9.5px;
+ font-weight: 600;
+ line-height: 1.2;
+ color: var(--text-purple);
+}
.tui-sheet-round + .tui-sheet-list-head {
margin-top: 0;
}
@@ -3824,6 +4079,13 @@ a.tui-id-card-ref:focus-visible {
.tui-sheet-list-title {
font-weight: 700;
}
+.tui-sheet-list-head:has(+ .tui-sheet-list-lede) {
+ margin-bottom: 4px;
+}
+.tui-sheet-list-lede {
+ margin: 0 0 12px;
+ color: var(--text-2);
+}
.tui-sheet-list-tally {
color: var(--text-3);
font-size: var(--gate-font-meta);
@@ -4135,6 +4397,7 @@ a.tui-id-card-ref:focus-visible {
margin-top: 0;
}
.tui-review-card-head .tui-review-finding-title {
+ flex: 0 1 auto;
font-size: 14px;
font-weight: 600;
}
@@ -4143,16 +4406,16 @@ a.tui-id-card-ref:focus-visible {
align-items: center;
gap: 6px;
flex-shrink: 0;
- font-size: 12px;
- font-weight: 500;
- color: var(--text-3);
+ font-size: 13px;
+ font-weight: 600;
+ color: var(--text-2);
}
.tui-review-sev::before {
content: '';
- width: 8px;
- height: 8px;
+ width: 9px;
+ height: 9px;
border-radius: 50%;
- background: var(--text-4);
+ background: var(--text-3);
}
.tui-review-sev[data-sev='critical'] {
color: var(--text-bad-small);
@@ -4308,11 +4571,27 @@ a.tui-id-card-ref:focus-visible {
border-bottom: 1px solid var(--border-soft);
}
.tui-review-code-path {
+ display: flex;
min-width: 0;
font-family: var(--font-mono);
font-size: 12px;
+ white-space: nowrap;
color: var(--text-1);
- overflow-wrap: anywhere;
+}
+/* The folders shrink first and lose their start: right-to-left puts the
+ ellipsis on the left, and the inner run keeps the path in order. */
+.tui-review-code-dir {
+ min-width: 0;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ direction: rtl;
+ text-align: left;
+}
+.tui-review-code-name {
+ flex: none;
+}
+.tui-review-code-file > :not(.tui-review-code-path) {
+ flex-shrink: 0;
}
/* The anchor's line reads like the file name in a finding's anchor. */
.tui-review-code-line {
@@ -4351,62 +4630,359 @@ a.tui-id-card-ref:focus-visible {
/* Inside a drafted card the card is the box: the code loses its own frame
and its file bar spans the card. The snippet, not the whole block, opens
the full diff. */
-.tui-review-finding .tui-review-section[data-section='code'] {
+:is(.tui-review-finding, .tui-reply-card)
+ .tui-review-section[data-section='code'] {
padding: 0;
border-top: none;
}
-.tui-review-finding .tui-review-code,
-.tui-review-finding .tui-review-code:hover {
- border: none;
- border-radius: 0;
- cursor: default;
+:is(.tui-review-finding, .tui-reply-card) .tui-review-code,
+:is(.tui-review-finding, .tui-reply-card) .tui-review-code:hover {
+ border: none;
+ border-radius: 0;
+ cursor: default;
+}
+:is(.tui-review-finding, .tui-reply-card) .tui-review-code-file {
+ padding: 6px 14px;
+ background: var(--panel);
+}
+:is(.tui-review-finding, .tui-reply-card)
+ .tui-review-code[data-folded]
+ .tui-review-code-file {
+ border-bottom: none;
+}
+.tui-review-code-snippet {
+ cursor: pointer;
+}
+.tui-review-code-fold {
+ display: inline-flex;
+ margin: -3px;
+ padding: 3px;
+ border: none;
+ border-radius: 4px;
+ background: none;
+ color: var(--text-3);
+ cursor: pointer;
+ transition:
+ transform 120ms,
+ background-color 120ms,
+ color 120ms;
+}
+@media (hover: hover) {
+ .tui-review-code-fold:hover {
+ background: color-mix(in srgb, var(--fg) 8%, transparent);
+ color: var(--text-1);
+ }
+}
+.tui-review-code[data-folded] .tui-review-code-fold {
+ transform: rotate(-90deg);
+}
+.tui-review-code-fold:focus-visible {
+ outline: 2px solid var(--fill-accent);
+ outline-offset: 2px;
+ border-radius: 2px;
+}
+.tui-review-code-more {
+ margin-left: auto;
+}
+.tui-review-code-more[data-reveal] {
+ opacity: 0;
+ transition: opacity 120ms;
+}
+.tui-review-section[data-section='code']:hover
+ .tui-review-code-more[data-reveal],
+.tui-review-code-more[data-reveal]:focus-visible {
+ opacity: 1;
+}
+.tui-review-finding .tui-review-section[data-section='comment'] {
+ position: relative;
+ padding: 16px 14px 14px;
+ background: var(--card-well);
+}
+.tui-review-finding .tui-review-section[data-section='comment']:last-child {
+ border-radius: 0 0 7px 7px;
+}
+
+/* A reply on a review thread: the finding card's header, comment area and
+ corners, with the thread as who said what in between. */
+.tui-reply-card .tui-review-section[data-section='comment'] {
+ position: relative;
+ padding: 16px 14px 14px;
+ background: var(--card);
+ border-radius: 0 0 7px 7px;
+}
+/* Your reply sits on a shaded band across the card's foot, as GitLab's
+ reply does; the field on it stays a plain card. */
+.tui-reply-band {
+ margin: 0 -14px -14px;
+ /* Room above the field for the edit chip that rides its top edge. */
+ padding: 18px 14px 14px;
+ border-radius: 0 0 7px 7px;
+ background: color-mix(in srgb, var(--fg) 3%, var(--card));
+}
+/* The elbow reaches from the comment above, past the band's top edge. */
+.tui-reply-band > .tui-plan-answer {
+ margin-top: 0;
+}
+.tui-reply-band > .tui-plan-answer::before {
+ top: -31px;
+ height: 51px;
+}
+.tui-reply-band > .tui-plan-answer:has(.tui-cte[data-editing])::before {
+ height: 76px;
+}
+.tui-reply-head {
+ padding: 0;
+}
+.tui-reply-head-label {
+ flex: 1;
+ min-width: 0;
+ align-items: center;
+ margin: 0;
+ padding: 10px 14px;
+ border-radius: inherit;
+}
+.tui-reply-head-label:focus-within {
+ background: none;
+ outline: none;
+}
+.tui-reply-head-label .tui-gate-choice-input {
+ margin-top: 0;
+}
+.tui-reply-head .tui-reply-resolve {
+ padding-right: 14px;
+}
+.tui-reply-title[data-file] {
+ font-family: var(--font-mono);
+ font-size: 13px;
+ font-weight: 400;
+}
+.tui-reply-card[data-off] .tui-reply-title {
+ color: var(--text-3);
+}
+.tui-reply-status {
+ display: inline-flex;
+ margin-left: auto;
+ align-items: center;
+ gap: 6px;
+ flex-shrink: 0;
+ font-size: 12px;
+ font-weight: 500;
+ color: var(--text-3);
+}
+.tui-reply-status::before {
+ content: '';
+ width: 7px;
+ height: 7px;
+ border-radius: 50%;
+ background: currentColor;
+}
+.tui-reply-status[data-hue='green'] {
+ color: var(--text-ok);
+}
+.tui-reply-status[data-hue='amber'] {
+ color: var(--text-warn);
+}
+.tui-reply-status[data-hue='accent'] {
+ color: var(--text-accent);
+}
+.tui-reply-sha {
+ font-family: var(--font-mono);
+ font-size: 12px;
+ color: var(--text-accent-small);
+}
+/* The thread reads top to bottom on one left edge: each message is a name
+ line over its words, and the agent's check closes it under a hairline. */
+.tui-reply-thread {
+ display: flex;
+ flex-direction: column;
+ gap: 16px;
+ padding: 14px 14px 16px;
+}
+.tui-reply-msg {
+ display: flex;
+ flex-direction: column;
+ gap: 4px;
+}
+/* A reply sits indented under the comment it answers, joined by a rounded
+ elbow that drops from the comment and turns into the reply's name. */
+.tui-reply-msg + .tui-reply-msg {
+ position: relative;
+ padding-left: 24px;
+}
+.tui-reply-msg + .tui-reply-msg::before {
+ content: '';
+ position: absolute;
+ left: 6px;
+ top: -12px;
+ width: 12px;
+ height: 21px;
+ border-left: 1.5px solid var(--border);
+ border-bottom: 1.5px solid var(--border);
+ border-bottom-left-radius: 8px;
+}
+.tui-reply-name {
+ font-size: 13px;
+ font-weight: 600;
+ line-height: 1.3;
+ color: var(--text-1);
+}
+.tui-reply-round {
+ margin-left: 10px;
+ vertical-align: 1px;
+}
+.tui-reply-text {
+ font-size: 14px;
+ line-height: 1.5;
+ color: var(--text-1);
+}
+.tui-reply-silence {
+ align-items: flex-start;
+}
+/* The footer hangs from the field's bottom-right edge as a small tinted
+ tab, set in from the corner, with a soft shade where it meets the field.
+ The field keeps its own border, corners and focus ring. */
+.tui-cte-footer {
+ display: flex;
+ align-items: center;
+ width: max-content;
+ margin: 0 12px 0 auto;
+ border-radius: 0 0 6px 6px;
+ background: color-mix(in srgb, var(--fg) 8%, var(--card));
+ box-shadow: inset 0 3px 3px -3px color-mix(in srgb, black 18%, transparent);
+ transition: background-color 200ms ease;
+}
+/* While writing, the helper line shares the tab's row: the line takes the
+ left and the tab keeps the right, so neither pushes the other down. */
+.tui-cte-wrap:has(> .tui-cte-footer) {
+ display: flex;
+ flex-wrap: wrap;
+ align-items: flex-start;
+}
+.tui-cte-wrap:has(> .tui-cte-footer) > .tui-cte {
+ flex: 0 0 100%;
+}
+.tui-cte-wrap:has(> .tui-cte-footer) > .tui-cte[data-editing] ~ .tui-cte-hint {
+ flex: 1;
+ order: 1;
+}
+.tui-cte-wrap:has(> .tui-cte-footer) > .tui-cte-footer {
+ order: 2;
+}
+/* Ticked, the tab washes blue: this thread resolves when the review posts. */
+.tui-cte-footer:has(.tui-gate-choice-input:checked) {
+ background: color-mix(in srgb, var(--fill-accent) 13%, var(--card));
+}
+/* The whole tab is the tick's label: a deeper tint under the pointer and
+ deeper still while pressed. */
+.tui-cte-footer:hover {
+ background: color-mix(in srgb, var(--fg) 12%, var(--card));
+}
+.tui-cte-footer:active {
+ background: color-mix(in srgb, var(--fg) 16%, var(--card));
+}
+.tui-cte-footer:has(.tui-gate-choice-input:checked):hover {
+ background: color-mix(in srgb, var(--fill-accent) 19%, var(--card));
+}
+.tui-cte-footer:has(.tui-gate-choice-input:checked):active {
+ background: color-mix(in srgb, var(--fill-accent) 25%, var(--card));
+}
+/* The footer's tick is a quiet line, not a second field. */
+.tui-cte-footer .tui-reply-resolve {
+ flex-direction: row-reverse;
+ padding: 3px 14px 4px;
+ user-select: none;
+ gap: 6px;
+ font-size: 12px;
+ color: var(--text-1);
+}
+.tui-cte-footer .tui-gate-choice-input {
+ width: 13px;
+ height: 13px;
+}
+/* The findings close on a quiet line with room under it, so the last card
+ never sits against the end of the scroll. */
+.tui-sheet-end {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ gap: 12px;
+ padding: 44px 0 64px;
+ font-size: 15px;
+ color: var(--text-3);
+}
+.tui-sheet-end p {
+ margin: 0;
}
-.tui-review-finding .tui-review-code-file {
- padding: 6px 14px;
+.tui-sheet-end-icon {
+ display: inline-grid;
+ place-items: center;
+ width: 40px;
+ height: 40px;
+ border-radius: 50%;
+ border: 1px solid var(--border-soft);
background: var(--panel);
+ box-shadow: inset 0 1px 3px color-mix(in srgb, black 12%, transparent);
+ color: var(--text-3);
}
-.tui-review-finding .tui-review-code[data-folded] .tui-review-code-file {
- border-bottom: none;
+/* The agent's verdict opens the comment area, the reason for the reply
+ below it; the gap clears the field's pill on its top border. */
+.tui-reply-card .tui-review-section[data-section='comment'] > .tui-reply-check {
+ margin-bottom: 6px;
}
-.tui-review-code-snippet {
- cursor: pointer;
+/* The agent's check is a kit button; a click opens what it found. */
+.tui-reply-check {
+ display: flex;
+ flex-direction: column;
+ align-items: flex-start;
}
-.tui-review-code-fold {
+.tui-reply-check-chevron {
display: inline-flex;
- padding: 0;
- border: none;
- background: none;
- color: var(--text-3);
- cursor: pointer;
- transition: transform 120ms;
+ margin-left: 2px;
+ transition: transform 150ms;
}
-.tui-review-code[data-folded] .tui-review-code-fold {
- transform: rotate(-90deg);
+.tui-reply-check-chevron[data-open] {
+ transform: rotate(90deg);
}
-.tui-review-code-fold:focus-visible {
- outline: 2px solid var(--fill-accent);
- outline-offset: 2px;
- border-radius: 2px;
+/* The note hangs from the button on a rule in its tone, dropped from
+ under the button's icon. Only the disclosure's height animates. */
+.tui-reply-check {
+ --check-line: var(--fill-warn);
}
-.tui-review-code-more {
- margin-left: auto;
+.tui-reply-check[data-verdict='verified'] {
+ --check-line: var(--fill-ok);
}
-.tui-review-code-more[data-reveal] {
- opacity: 0;
- transition: opacity 120ms;
+.tui-reply-check[data-verdict='accepted'] {
+ --check-line: var(--fill-accent);
}
-.tui-review-section[data-section='code']:hover
- .tui-review-code-more[data-reveal],
-.tui-review-code-more[data-reveal]:focus-visible {
- opacity: 1;
+.tui-reply-check > .tui-disclosure {
+ align-self: stretch;
}
-.tui-review-finding .tui-review-section[data-section='comment'] {
- position: relative;
- padding: 16px 14px 14px;
- background: var(--panel);
+.tui-reply-check .tui-disclosure .tui-disclosure-body {
+ padding-top: 0;
}
-.tui-review-finding .tui-review-section[data-section='comment']:last-child {
- border-radius: 0 0 7px 7px;
+.tui-reply-check-note {
+ margin: 0 0 0 13px;
+ padding: 8px 0 2px 14px;
+ border-left: 2px solid var(--check-line);
+ font-size: 14px;
+ line-height: 1.5;
+ color: var(--text-1);
+}
+.tui-reply-resolve {
+ display: inline-flex;
+ align-items: center;
+ gap: 8px;
+ align-self: flex-start;
+ font-size: 13px;
+ color: var(--text-1);
+ cursor: pointer;
+}
+.tui-reply-resolve .tui-gate-choice-input[data-checked],
+.tui-reply-head-label .tui-gate-choice-input[data-checked] {
+ background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='white' stroke-width='3.5' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='m5 12.5 4.5 4.5L19 7.5'/%3E%3C/svg%3E");
+ background-repeat: no-repeat;
+ background-position: center;
+ background-size: 11px 11px;
}
/* The diff draws in a shadow root; these reach it as inherited custom
properties. */
@@ -5011,7 +5587,7 @@ a.tui-id-card-ref:focus-visible {
/* At rest the hint is a pill riding the field's top border, in the
field's top padding, so it never covers the text; it eases in with the
pointer. The section it sits in is its positioning box. */
-.tui-cte[data-variant='plain']:not([data-editing]) + .tui-cte-hint {
+.tui-cte[data-variant='plain']:not([data-editing]) ~ .tui-cte-hint {
position: absolute;
top: -10px;
right: 12px;
@@ -5026,16 +5602,14 @@ a.tui-id-card-ref:focus-visible {
line-height: 1;
color: var(--text-accent);
background: color-mix(in srgb, var(--fill-accent) 12%, var(--card));
- box-shadow:
- 0 0 0 1px color-mix(in srgb, var(--fill-accent) 14%, transparent),
- 0 1px 3px color-mix(in srgb, var(--fg) 8%, transparent);
+ box-shadow: 0 0 0 1px color-mix(in srgb, var(--fill-accent) 14%, transparent);
opacity: 0;
transform: translateY(3px) scale(0.94);
pointer-events: none;
transition: all 150ms cubic-bezier(0.4, 0, 0.2, 1);
}
/* Write's hint tucks just under the field. */
-.tui-cte[data-variant='plain'][data-editing] + .tui-cte-hint {
+.tui-cte[data-variant='plain'][data-editing] ~ .tui-cte-hint {
margin-top: 5px;
}
.tui-cte-hint svg {
@@ -5044,14 +5618,23 @@ a.tui-id-card-ref:focus-visible {
flex-shrink: 0;
}
@media (hover: hover) {
- .tui-cte[data-variant='plain']:not([data-editing]):hover + .tui-cte-hint {
+ .tui-cte[data-variant='plain']:not([data-editing]):hover ~ .tui-cte-hint,
+ .tui-cte[data-variant='plain']:not([data-editing]) ~ .tui-cte-hint:hover {
opacity: 1;
transform: none;
+ pointer-events: auto;
+ cursor: pointer;
+ }
+ .tui-cte[data-variant='plain']:not([data-editing]) ~ .tui-cte-hint:hover {
+ background: color-mix(in srgb, var(--fill-accent) 20%, var(--card));
}
}
+.tui-cte[data-variant='plain']:not([data-editing]) ~ .tui-cte-hint:active {
+ background: color-mix(in srgb, var(--fill-accent) 28%, var(--card));
+}
/* The flash mounts already shown, so it eases in from the hidden pose. */
.tui-cte[data-variant='plain']:not([data-editing])
- + .tui-cte-hint[data-flash='shown'] {
+ ~ .tui-cte-hint[data-flash='shown'] {
opacity: 1;
transform: none;
@starting-style {
@@ -5061,12 +5644,10 @@ a.tui-id-card-ref:focus-visible {
}
/* "Comment updated" is a done state, so it takes the ok wash, not the
accent the edit cue wears; it keeps it through the fade. */
-.tui-cte[data-variant='plain']:not([data-editing]) + .tui-cte-hint[data-flash] {
+.tui-cte[data-variant='plain']:not([data-editing]) ~ .tui-cte-hint[data-flash] {
color: var(--text-ok);
background: color-mix(in srgb, var(--fill-ok) 12%, var(--card));
- box-shadow:
- 0 0 0 1px color-mix(in srgb, var(--fill-ok) 18%, transparent),
- 0 1px 3px color-mix(in srgb, var(--fg) 8%, transparent);
+ box-shadow: 0 0 0 1px color-mix(in srgb, var(--fill-ok) 18%, transparent);
}
@media (hover: hover) {
.tui-cte[data-variant='filled']:not([data-editing]):hover {
@@ -6603,18 +7184,31 @@ a.tui-id-card-ref:focus-visible {
}
}
-/* The review sheet's main column sits a step lighter than the page, so its
+/* The review and respond sheets' main column sits a step lighter than the page, so its
white cards separate softly. */
-.tui-sheet-main[data-sheet='review'] {
+.tui-sheet-main:is([data-sheet='review'], [data-sheet='respond']) {
+ /* A card's lower well sits a step darker than the page around it. */
+ --card-well: color-mix(in srgb, var(--fg) 4.5%, var(--card));
background: var(--panel);
}
/* On a wide display the findings stop at a readable measure, centred in
- the column, with the rail still pinned right; 1120px keeps a 12px mono
- diff line near 150 characters. */
-.tui-sheet-main[data-sheet='review'] > * {
- max-width: 1120px;
+ the column, with the rail still pinned right; 1040px keeps a 12px mono
+ diff line near 140 characters without stretching the prose. */
+.tui-sheet-main:is([data-sheet='review'], [data-sheet='respond']) > * {
+ max-width: 1040px;
margin-inline: auto;
}
+/* The round band spans the whole column; its padding lines its text up
+ with the capped content below. */
+@media (min-width: 721px) {
+ .tui-sheet-main:is([data-sheet='review'], [data-sheet='respond'])
+ > .tui-sheet-round {
+ max-width: none;
+ margin-inline: -26px -28px;
+ padding-inline: calc(26px + max(0px, (100% - 1040px) / 2))
+ calc(28px + max(0px, (100% - 1040px) / 2));
+ }
+}
/* The summary comment in the dock: two lines at rest, whole when open. */
.tui-review-dock-summary {
@@ -6639,10 +7233,6 @@ a.tui-id-card-ref:focus-visible {
-webkit-box-orient: vertical;
overflow: hidden;
}
-.tui-review-dock-summary .tui-cte[data-variant='filled']:not([data-editing]) {
- border-color: var(--border-soft);
- background: var(--card);
-}
/* The rail's checks: a tally beside the title, then every check. */
.tui-review-checks {
@@ -6674,6 +7264,14 @@ a.tui-id-card-ref:focus-visible {
.tui-review-tier-pills:has(.tui-review-sev) {
gap: 14px;
}
+/* The earlier threads' tally: the cards' own status marks, in a row. */
+.tui-carry-tally {
+ gap: 14px;
+ margin-top: 0;
+}
+.tui-carry-tally > .tui-reply-status {
+ margin-left: 0;
+}
.tui-review-sev-count {
margin-left: 2px;
font-weight: 400;
@@ -6688,11 +7286,8 @@ a.tui-id-card-ref:focus-visible {
outline-offset: 2px;
}
-/* A finding with no diff line says where its comment goes in the head;
- its comment then follows the head straight on, under the head's line. */
-.tui-review-in-summary {
- flex-shrink: 0;
-}
+/* A finding with no diff line: its comment follows the head straight on,
+ under the head's line. */
.tui-review-card-head
+ .tui-review-card-body
> .tui-review-section:first-child {
@@ -6836,26 +7431,6 @@ a.tui-id-card-ref:focus-visible {
/* The info mark beside a code file's path, and the agent's finding it
opens on a click. */
-.tui-review-info {
- display: inline-flex;
- align-items: center;
- padding: 2px;
- border: none;
- border-radius: 4px;
- background: none;
- color: var(--text-3);
- cursor: pointer;
- transition: all 150ms cubic-bezier(0.4, 0, 0.2, 1);
-}
-.tui-review-info:hover,
-.tui-review-info[data-popup-open] {
- color: var(--text-accent);
- background: color-mix(in srgb, var(--fill-accent) 10%, transparent);
-}
-.tui-review-info:focus-visible {
- outline: 2px solid var(--fill-accent);
- outline-offset: 1px;
-}
.tui-review-info-card {
display: flex;
flex-direction: column;
@@ -6871,6 +7446,15 @@ a.tui-id-card-ref:focus-visible {
font-weight: 500;
color: var(--text-3);
}
+/* The suggested fix follows the finding under a hairline, same label. */
+.tui-review-info-fix {
+ display: flex;
+ flex-direction: column;
+ gap: 6px;
+ margin-top: 4px;
+ padding-top: 10px;
+ border-top: 1px solid var(--border-soft);
+}
/* The code under its file bar; motion eases its height on fold. */
.tui-review-code-body {
@@ -6896,3 +7480,246 @@ a.tui-id-card-ref:focus-visible {
0 18px 0 0 var(--surface-wash-fg-4-card);
}
}
+
+/* The snippet's stand-in while it loads: seven quiet bars at the diff's
+ line pitch, pulsing gently. */
+.tui-review-code-loading {
+ display: flex;
+ flex-direction: column;
+ gap: 9.2px;
+ padding: 9px 14px 9px 52px;
+}
+.tui-review-code-loading > span {
+ height: 10px;
+ border-radius: 3px;
+ background: color-mix(in srgb, var(--fg) 6%, transparent);
+ animation: tui-code-loading 1.2s ease-in-out infinite alternate;
+}
+@keyframes tui-code-loading {
+ from {
+ opacity: 0.55;
+ }
+ to {
+ opacity: 1;
+ }
+}
+@media (prefers-reduced-motion: reduce) {
+ .tui-review-code-loading > span {
+ animation: none;
+ }
+}
+
+/* The Report tab opens on the same MR and decision context cards as the
+ rail. */
+.tui-review-report-context {
+ display: flex;
+ flex-direction: column;
+ gap: 12px;
+ margin-bottom: 16px;
+}
+/* The rail's MR card sits on the panel there; on the panel-toned main
+ column it takes the card ground so it doesn't vanish into it. */
+.tui-review-report-context .tui-id-card {
+ border-color: var(--border);
+ background: var(--card);
+}
+.tui-review-tabs .tui-tab > svg {
+ margin-right: 6px;
+}
+
+/* A fix's reply direction: what the reply will say once the fix lands,
+ quoted under a small label rather than framed like a field. */
+.tui-thread-direction {
+ display: flex;
+ flex-direction: column;
+ gap: 4px;
+ padding-left: 12px;
+ border-left: 2px solid var(--border);
+}
+.tui-thread-direction-k {
+ font-size: 12px;
+ font-weight: 600;
+ color: var(--text-3);
+}
+.tui-thread-direction-text {
+ font-size: 14px;
+ line-height: 1.5;
+ color: var(--text-2);
+}
+
+/* A plan thread in the review card (B43): the verdict and the fix or
+ reply switch, the reviewer's comment in a box, then what answers it. */
+.tui-plan-card .tui-review-section[data-section='comment'] {
+ gap: 12px;
+}
+.tui-plan-verdict {
+ display: flex;
+ align-items: flex-start;
+ justify-content: space-between;
+ gap: 12px;
+}
+.tui-plan-verdict > .tui-reply-check {
+ flex: 1;
+ min-width: 0;
+}
+.tui-plan-switch {
+ flex-shrink: 0;
+ padding: 3px;
+ border-radius: 8px;
+}
+.tui-plan-switch [data-part='segmented-option'] {
+ flex: none;
+ gap: 6px;
+ white-space: nowrap;
+ padding: 5px 12px;
+ border-radius: 6px;
+ font-size: 13px;
+}
+.tui-plan-switch [data-part='segmented-option'][data-active] {
+ font-weight: 600;
+}
+.tui-plan-switch [data-part='segmented-option'][data-active] svg {
+ color: var(--text-accent-small);
+}
+.tui-plan-said {
+ display: flex;
+ flex-direction: column;
+ gap: 4px;
+ padding: 10px 12px;
+ border: 1px solid var(--border-soft);
+ border-radius: 6px;
+ background: var(--card);
+ font-size: 14px;
+ line-height: 1.5;
+ color: var(--text-1);
+}
+.tui-plan-said-who {
+ font-size: 13px;
+ font-weight: 600;
+}
+.tui-comment-author {
+ display: inline-flex;
+ align-items: center;
+ gap: 7px;
+}
+/* The whole invader on a filled tile. */
+.tui-comment-avatar {
+ display: inline-flex;
+ flex-shrink: 0;
+ width: 20px;
+ height: 20px;
+ border-radius: 3px;
+ overflow: hidden;
+ background: color-mix(
+ in srgb,
+ var(--avatar-hue, var(--text-3)) 10%,
+ var(--card)
+ );
+ box-shadow: inset 0 0 0 1px
+ color-mix(in srgb, var(--avatar-hue, var(--text-3)) 18%, var(--card));
+}
+.tui-comment-avatar svg {
+ width: 100%;
+ height: 100%;
+ transform: scale(0.78);
+}
+.tui-plan-fix {
+ display: flex;
+ align-items: flex-start;
+ gap: 8px;
+ margin: 0;
+ padding-left: 10px;
+ font-size: 14px;
+ line-height: 1.5;
+ color: var(--text-1);
+}
+.tui-plan-fix > svg {
+ flex-shrink: 0;
+ margin-top: 2px;
+ color: var(--text-accent);
+}
+/* What answers the comment hangs under it on a rounded elbow that turns
+ into the answer's name line. */
+.tui-plan-answer {
+ position: relative;
+ /* Room for the click-to-edit chip, which rides 10px above the field. */
+ margin-top: 6px;
+ padding-left: 28px;
+}
+.tui-plan-answer::before {
+ content: '';
+ position: absolute;
+ left: 10px;
+ top: -18px;
+ width: 15px;
+ height: 38px;
+ border-left: 1.5px solid var(--border);
+ border-bottom: 1.5px solid var(--border);
+ border-bottom-left-radius: 8px;
+ /* Matches the edit heading's slide in ClickToEditField (SWAP). */
+ transition: height 150ms cubic-bezier(0.4, 0, 0.2, 1);
+}
+/* Editing opens a heading row above the field; the elbow follows the
+ field's name line down past it. */
+.tui-plan-answer:has(.tui-cte[data-editing])::before {
+ height: 63px;
+}
+/* Your reply carries a faint accent wash so it reads apart from what it
+ answers; editing turns the field back into a plain one. A finding's own
+ comment, with its code, stays plain. */
+.tui-plan-card .tui-plan-answer .tui-cte:not([data-editing]) {
+ border-color: color-mix(in srgb, var(--fill-accent) 22%, var(--border));
+ background: color-mix(in srgb, var(--fill-accent) 5%, var(--card));
+}
+.tui-plan-later {
+ display: flex;
+ align-items: center;
+ gap: 6px;
+ align-self: flex-start;
+ width: fit-content;
+ margin: 0;
+ padding: 10px 12px;
+ border: 1px dashed var(--border);
+ border-radius: 6px;
+ background: var(--card);
+ font-size: 14px;
+ line-height: 1.5;
+ color: var(--text-2);
+}
+.tui-plan-later > [data-part='tooltip'] {
+ display: flex;
+}
+.tui-plan-later-info {
+ display: inline-flex;
+ /* The text's ink sits below its line box's centre. */
+ translate: 0 1px;
+ padding: 2px;
+ border: 0;
+ border-radius: 4px;
+ background: none;
+ color: var(--text-3);
+ cursor: pointer;
+}
+.tui-plan-later-info:hover,
+.tui-plan-later-info:focus-visible {
+ color: var(--text-1);
+}
+/* The name line inside a comment field, like the reviewer's name in theirs. */
+.tui-cte-lead[data-editable] {
+ cursor: text;
+}
+.tui-cte-lead {
+ padding: 10px 12px 0;
+ font-size: 13px;
+ font-weight: 600;
+ color: var(--text-1);
+}
+.tui-cte[data-variant='plain']
+ .tui-cte-lead
+ ~ *
+ :is(.tui-cte-view, .tui-cte-input) {
+ padding-top: 2px;
+}
+.tui-stacked-area {
+ gap: 12px;
+}
diff --git a/apps/board/src/view.ts b/apps/board/src/view.ts
index 59231b483a..5c611d01fc 100644
--- a/apps/board/src/view.ts
+++ b/apps/board/src/view.ts
@@ -621,6 +621,26 @@ function ageBucket(
return { label: 'Older', order: 9 };
}
+/** Whether a person approved the MR: listed among its approvers, or an
+ assigned reviewer whose state reads approved. Someone who approved
+ without being assigned shows only in the approvers, and GitHub's
+ reviewers never carry a state. */
+export function personApproved(
+ mr:
+ | {
+ reviews?: Partial>;
+ }
+ | undefined,
+ person: string
+): boolean {
+ const who = person.toLowerCase();
+ const reviews = mr?.reviews;
+ if (reviews?.approvedBy?.some(u => u.username.toLowerCase() === who))
+ return true;
+ const r = reviews?.reviewers?.find(x => x.username.toLowerCase() === who);
+ return getReviewDisplayState(r?.reviewState ?? null) === 'approved';
+}
+
/** Every assigned reviewer has approved. */
function allReviewersApproved(mr: BoardMR): boolean {
const reviewers = mr.reviews.reviewers ?? [];
diff --git a/apps/board/tests/decision-queue-context-layout.test.ts b/apps/board/tests/decision-queue-context-layout.test.ts
index fc3b014864..98d5b66227 100644
--- a/apps/board/tests/decision-queue-context-layout.test.ts
+++ b/apps/board/tests/decision-queue-context-layout.test.ts
@@ -207,7 +207,7 @@ test('laptop: a respond gate lists every thread at full height beside a docked s
document: { querySelectorAll(s: string): ArrayLike };
};
return Array.from(
- document.querySelectorAll('.tui-respond-list .tui-thread-card')
+ document.querySelectorAll('.tui-respond-list > .tui-plan-card')
).map(c => c.scrollHeight <= c.clientHeight + 1);
});
expect(cards.length).toBeGreaterThan(1);
@@ -269,36 +269,42 @@ test('queue nav: previous is disabled at the first gate and walks back from the
await page.context().close();
}, 30_000);
-test('the recommended choice is highlighted only until something in its question is picked', async () => {
+test("a plan card opens on the agent's pick as the switch's lit tile, and a click moves the tile", async () => {
const page = await openQueue(LAPTOP);
await nextUntil(page, '.tui-respond-sheet');
- const tinted = () =>
- page.evaluate(() => {
+ const SWITCH =
+ '.tui-respond-list > .tui-plan-card:first-child [data-part="segmented-option"]';
+ const tiles = () =>
+ page.evaluate(selector => {
const { document } = globalThis as unknown as {
- document: { querySelectorAll(s: string): ArrayLike };
+ document: {
+ querySelectorAll(
+ s: string
+ ): ArrayLike<{ hasAttribute(a: string): boolean }>;
+ };
};
const win = globalThis as unknown as {
getComputedStyle(el: unknown): { backgroundColor: string };
};
- return Array.from(
- document.querySelectorAll(
- '.tui-respond-list > .tui-gate-question:first-child .tui-gate-choice[data-recommended]'
- )
- ).map(
- c => win.getComputedStyle(c).backgroundColor !== 'rgba(0, 0, 0, 0)'
- );
- });
- expect(await tinted()).toEqual([true]);
- await page
- .locator(
- '.tui-respond-list > .tui-gate-question:first-child .tui-gate-choice:not([data-recommended])'
- )
- .first()
- .click();
- // The choice background transitions, so wait out the fade before reading.
- for (let i = 0; i < 20 && (await tinted())[0]; i++)
+ return Array.from(document.querySelectorAll(selector)).map(c => ({
+ active: c.hasAttribute('data-active'),
+ lit: win.getComputedStyle(c).backgroundColor !== 'rgba(0, 0, 0, 0)',
+ }));
+ }, SWITCH);
+ const before = await tiles();
+ expect(before).toHaveLength(2);
+ expect(before.filter(t => t.active)).toHaveLength(1);
+ const idle = before.findIndex(t => !t.active);
+ await page.locator(SWITCH).nth(idle).click();
+ // The tile's background transitions, so wait out the fade before reading.
+ let after = await tiles();
+ for (let i = 0; i < 20 && (!after[idle]!.lit || after[1 - idle]!.lit); i++) {
await page.waitForTimeout(50);
- expect(await tinted()).toEqual([false]);
+ after = await tiles();
+ }
+ expect(after.map(t => t.active)).toEqual(before.map(t => !t.active));
+ expect(after[idle]!.lit).toBe(true);
+ expect(after[1 - idle]!.lit).toBe(false);
await page.context().close();
}, 30_000);
diff --git a/apps/deck/core/generated/board.css b/apps/deck/core/generated/board.css
index 32548d5d16..a891f2acec 100644
--- a/apps/deck/core/generated/board.css
+++ b/apps/deck/core/generated/board.css
@@ -1 +1 @@
-@layer soribashi.tokens,soribashi.recipes,soribashi.utilities;@layer soribashi.tokens{:root{--buncss-light:initial;--buncss-dark: ;color-scheme:light;--color-amber-500:var(--buncss-light,#ef5f00)var(--buncss-dark,#f76b15);--color-amber-hover:var(--buncss-light,#d65704)var(--buncss-dark,#ff801f);--color-amber-onFill:var(--buncss-light,#fff)var(--buncss-dark,#fff);--color-amber-text:var(--buncss-light,#cc4e00)var(--buncss-dark,#ffa057);--color-amber-textSmall:var(--buncss-light,#582d1d)var(--buncss-dark,#ffe0c2);--color-amber-textVivid:var(--buncss-light,#cc4e00)var(--buncss-dark,#ffa057);--color-blue-500:var(--buncss-light,#3e63dd)var(--buncss-dark,#3e63dd);--color-blue-hover:var(--buncss-light,#3358d4)var(--buncss-dark,#5472e4);--color-blue-onFill:var(--buncss-light,#fff)var(--buncss-dark,#fff);--color-blue-text:var(--buncss-light,#3a5bc7)var(--buncss-dark,#9eb1ff);--color-blue-textSmall:var(--buncss-light,#1f2d5c)var(--buncss-dark,#d6e1ff);--color-blue-textVivid:var(--buncss-light,#3a5bc7)var(--buncss-dark,#9eb1ff);--color-cyan-500:var(--buncss-light,#0797b9)var(--buncss-dark,#00a2c7);--color-cyan-hover:var(--buncss-light,#0a89a7)var(--buncss-dark,#23afd0);--color-cyan-onFill:var(--buncss-light,#fff)var(--buncss-dark,#fff);--color-cyan-text:var(--buncss-light,#107d98)var(--buncss-dark,#4ccce6);--color-cyan-textSmall:var(--buncss-light,#0d3c48)var(--buncss-dark,#b6ecf7);--color-cyan-textVivid:var(--buncss-light,#107d98)var(--buncss-dark,#4ccce6);--color-dot-bad:var(--buncss-light,#e93d82)var(--buncss-dark,#e93d82);--color-dot-ok:var(--buncss-light,#0d9b8a)var(--buncss-dark,#12a594);--color-dot-warn:var(--buncss-light,#ef5f00)var(--buncss-dark,#f76b15);--color-gold-500:var(--buncss-light,#ffc53d)var(--buncss-dark,#ffc53d);--color-gold-hover:var(--buncss-light,#ffba18)var(--buncss-dark,#ffd60a);--color-gold-onFill:var(--buncss-light,#1c2024)var(--buncss-dark,#1c2024);--color-gold-text:var(--buncss-light,#ab6400)var(--buncss-dark,#ffca16);--color-gold-textSmall:var(--buncss-light,#4f3422)var(--buncss-dark,#ffe7b3);--color-gold-textVivid:var(--buncss-light,#ab6400)var(--buncss-dark,#ffca16);--color-gray-accentText:var(--buncss-light,#3a5bc7)var(--buncss-dark,#9eb1ff);--color-gray-badgeText:var(--buncss-light,#60646c)var(--buncss-dark,#b0b4ba);--color-gray-fg:var(--buncss-light,#1c2024)var(--buncss-dark,#edeef0);--color-gray-muted:var(--buncss-light,#8b8d98)var(--buncss-dark,#696e77);--color-gray-mutedOnCard:var(--buncss-light,#60646c)var(--buncss-dark,#b0b4ba);--color-gray-mutedText:var(--buncss-light,#60646c)var(--buncss-dark,#b0b4ba);--color-gray-okText:var(--buncss-light,#008573)var(--buncss-dark,#0bd8b6);--color-gray-redText:var(--buncss-light,#cb1d63)var(--buncss-dark,#ff92ad);--color-gray-warnText:var(--buncss-light,#cc4e00)var(--buncss-dark,#ffa057);--color-green-500:var(--buncss-light,#0d9b8a)var(--buncss-dark,#12a594);--color-green-hover:var(--buncss-light,#0f8c7e)var(--buncss-dark,#0eb39e);--color-green-onFill:var(--buncss-light,#fff)var(--buncss-dark,#fff);--color-green-text:var(--buncss-light,#008573)var(--buncss-dark,#0bd8b6);--color-green-textSmall:var(--buncss-light,#0d3d38)var(--buncss-dark,#adf0dd);--color-green-textVivid:var(--buncss-light,#008573)var(--buncss-dark,#0bd8b6);--color-ground-1:var(--buncss-light,#fff)var(--buncss-dark,#111113);--color-ground-2:var(--buncss-light,#f9f9fb)var(--buncss-dark,#18191b);--color-ground-3:var(--buncss-light,#f0f0f3)var(--buncss-dark,#212225);--color-ground-4:var(--buncss-light,#e8e8ec)var(--buncss-dark,#272a2d);--color-ink-1:var(--buncss-light,#1c2024)var(--buncss-dark,#edeef0);--color-ink-2:var(--buncss-light,#60646c)var(--buncss-dark,#b0b4ba);--color-ink-3:var(--buncss-light,#60646c)var(--buncss-dark,#b0b4ba);--color-ink-4:var(--buncss-light,#60646c)var(--buncss-dark,#b0b4ba);--color-line-border:var(--buncss-light,#cdced6)var(--buncss-dark,#43484e);--color-line-control:var(--buncss-light,#b9bbc6)var(--buncss-dark,#696e77);--color-line-controlEdgeOnCard:var(--buncss-light,#b9bbc6)var(--buncss-dark,#696e77);--color-line-edgeOnCard:var(--buncss-light,#cdced6)var(--buncss-dark,#696e77);--color-line-grid:var(--buncss-light,#343b580d)var(--buncss-dark,#7aa2f70f);--color-line-soft:var(--buncss-light,#d9d9e0)var(--buncss-dark,#363a3f);--color-line-softOnCard:var(--buncss-light,#d9d9e0)var(--buncss-dark,#43484e);--color-purple-500:var(--buncss-light,#8e4ec6)var(--buncss-dark,#8e4ec6);--color-purple-hover:var(--buncss-light,#8347b9)var(--buncss-dark,#9a5cd0);--color-purple-onFill:var(--buncss-light,#fff)var(--buncss-dark,#fff);--color-purple-text:var(--buncss-light,#8145b5)var(--buncss-dark,#d19dff);--color-purple-textSmall:var(--buncss-light,#402060)var(--buncss-dark,#ecd9fa);--color-purple-textVivid:var(--buncss-light,#8145b5)var(--buncss-dark,#d19dff);--color-red-500:var(--buncss-light,#e93d82)var(--buncss-dark,#e93d82);--color-red-hover:var(--buncss-light,#df3478)var(--buncss-dark,#ee518a);--color-red-onFill:var(--buncss-light,#fff)var(--buncss-dark,#fff);--color-red-text:var(--buncss-light,#cb1d63)var(--buncss-dark,#ff92ad);--color-red-textSmall:var(--buncss-light,#621639)var(--buncss-dark,#fdd3e8);--color-red-textVivid:var(--buncss-light,#cb1d63)var(--buncss-dark,#ff92ad);--color-rule-1:var(--buncss-light,#b9bbc6)var(--buncss-dark,#696e77);--color-rule-2:var(--buncss-light,#cdced6)var(--buncss-dark,#43484e);--color-rule-3:var(--buncss-light,#d9d9e0)var(--buncss-dark,#363a3f);--color-surface-bg:var(--buncss-light,#f0f0f3)var(--buncss-dark,#111113);--color-surface-card:var(--buncss-light,#fff)var(--buncss-dark,#212225);--color-surface-chrome:var(--buncss-light,#f0f0f3)var(--buncss-dark,#18191b);--color-surface-inset:var(--buncss-light,#f0f0f3)var(--buncss-dark,#111113);--color-surface-overlay:var(--buncss-light,#f9f9fb)var(--buncss-dark,#212225);--color-surface-panel:var(--buncss-light,#f9f9fb)var(--buncss-dark,#18191b);--color-surface-raised:var(--buncss-light,#e8e8ec)var(--buncss-dark,#272a2d);--radius-lg:8px;--radius-md:6px;--radius-pill:999px;--radius-px2:2px;--radius-px5:5px;--radius-px7:7px;--radius-round:50%;--radius-sm:4px;--radius-xl:10px;--radius-xs:3px;--spacing-lg:.7rem;--spacing-md:.6rem;--spacing-px10:10px;--spacing-px12:12px;--spacing-px14:14px;--spacing-px2:2px;--spacing-px3:3px;--spacing-px4:4px;--spacing-px5:5px;--spacing-px6:6px;--spacing-px7:7px;--spacing-px8:8px;--spacing-px9:9px;--spacing-rem100:1rem;--spacing-rem120:1.2rem;--spacing-rem140:1.4rem;--spacing-rem150:1.5rem;--spacing-rem175:1.75rem;--spacing-rem180:1.8rem;--spacing-rem20:.2rem;--spacing-rem35:.35rem;--spacing-rem40:.4rem;--spacing-rem50:.5rem;--spacing-rem55:.55rem;--spacing-rem75:.75rem;--spacing-rem80:.8rem;--spacing-rem95:.95rem;--spacing-sm:.45rem;--spacing-xl:.9rem;--spacing-xs:.3rem;--spacing-xxl:1.1rem;--spacing-xxs:.15rem;--font-size-base:13.5px;--font-size-lg:.85rem;--font-size-md:.76rem;--font-size-px10:10px;--font-size-px11:11px;--font-size-px12:12px;--font-size-px13:13px;--font-size-px9:9px;--font-size-rem100:1rem;--font-size-rem105:1.05rem;--font-size-rem60:.6rem;--font-size-rem62:.62rem;--font-size-rem65:.65rem;--font-size-rem68:.68rem;--font-size-rem72:.72rem;--font-size-rem74:.74rem;--font-size-rem75:.75rem;--font-size-rem78:.78rem;--font-size-rem80:.8rem;--font-size-rem82:.82rem;--font-size-rem90:.9rem;--font-size-rem95:.95rem;--font-size-sm:.7rem;--font-size-xl:.92rem;--font-size-xs:.66rem;--font-size-xxl:1.15rem;--font-size-xxs:.55rem;--font-family-mono:"JetBrains Mono",ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;--font-family-sans:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,Helvetica,Arial,sans-serif;--line-height-base:1.55;--line-height-snug:1.4;--shadow-card:0 2px 5px -1px var(--buncss-light,#10182829)var(--buncss-dark,#0009);--shadow-drawer:-6px 0 32px #0000004d;--shadow-drawer-left:4px 0 28px #0000004d;--shadow-menu:0 10px 30px #00000047,0 2px 8px #0000002e;--shadow-modal:0 12px 40px #00000040;--shadow-toast:0 6px 20px #00000040;--breakpoint-2xl:96rem;--breakpoint-3xl:120rem;--breakpoint-lg:64rem;--breakpoint-md:48rem;--breakpoint-sm:40rem;--breakpoint-xl:80rem;--breakpoint-xs:24rem;--text-accentText:var(--color-gray-accentText);--text-badText:var(--color-gray-redText);--text-muted:var(--color-gray-mutedText);--text-muted-on-card:var(--color-gray-mutedOnCard);--text-primary:var(--color-gray-fg);--surface-canvas:var(--color-surface-bg);--surface-card:var(--color-surface-card);--surface-chrome:var(--color-surface-chrome);--surface-inset:var(--color-surface-inset);--surface-overlay:var(--color-surface-overlay);--surface-panel:var(--color-surface-panel);--surface-raised:var(--color-surface-raised);--surface-wash-accent-14:color-mix(in srgb,var(--accent)14%,transparent);--surface-wash-accent-16:color-mix(in srgb,var(--accent)16%,transparent);--surface-wash-accent-7:color-mix(in srgb,var(--accent)7%,transparent);--surface-wash-accent-fg-70:color-mix(in srgb,var(--accent)70%,var(--fg));--surface-wash-amber-45:color-mix(in srgb,var(--amber)45%,transparent);--surface-wash-amber-7:color-mix(in srgb,var(--amber)7%,transparent);--surface-wash-bg-55:color-mix(in srgb,var(--bg)55%,transparent);--surface-wash-cyan-border-45:color-mix(in srgb,var(--cyan)45%,var(--border));--surface-wash-cyan-panel-38:color-mix(in srgb,var(--cyan)38%,var(--panel));--surface-wash-fg-4-card:color-mix(in srgb,var(--fg)4%,var(--card));--surface-wash-fg-5:color-mix(in srgb,var(--fg)5%,transparent);--surface-wash-fg-8:color-mix(in srgb,var(--fg)8%,transparent);--surface-wash-panel-100:color-mix(in srgb,var(--panel)100%,transparent);--surface-wash-panel-55:color-mix(in srgb,var(--panel)55%,transparent);--surface-wash-panel-70:color-mix(in srgb,var(--panel)70%,transparent);--surface-wash-panel-88:color-mix(in srgb,var(--panel)88%,transparent);--surface-wash-panel-94:color-mix(in srgb,var(--panel)94%,transparent);--border-control:var(--color-line-control);--border-control-on-card:var(--color-line-controlEdgeOnCard);--border-default:var(--color-line-border);--border-on-card:var(--color-line-edgeOnCard);--border-soft:var(--color-line-soft);--border-soft-on-card:var(--color-line-softOnCard);--bg:var(--surface-canvas);--panel:var(--surface-panel);--card:var(--surface-card);--chrome:var(--surface-chrome);--fg:var(--text-primary);--muted:var(--color-gray-muted);--muted-text:var(--text-muted);--accent-text:var(--text-accentText);--red-text:var(--text-badText);--border:var(--border-default);--page:var(--surface-canvas);--raised:var(--surface-raised);--surface-1:var(--color-ground-1);--surface-2:var(--color-ground-2);--surface-3:var(--color-ground-3);--surface-4:var(--color-ground-4);--text-1:var(--color-ink-1);--text-2:var(--color-ink-2);--text-3:var(--color-ink-3);--text-4:var(--color-ink-4);--line-1:var(--color-rule-1);--line-2:var(--color-rule-2);--line-3:var(--color-rule-3);--fill-accent:var(--color-blue-500);--fill-accent-hover:var(--color-blue-hover);--fill-ok:var(--color-green-500);--fill-ok-hover:var(--color-green-hover);--fill-bad:var(--color-red-500);--fill-bad-hover:var(--color-red-hover);--fill-warn:var(--color-amber-500);--fill-warn-hover:var(--color-amber-hover);--fill-purple:var(--color-purple-500);--fill-purple-hover:var(--color-purple-hover);--fill-cyan:var(--color-cyan-500);--fill-cyan-hover:var(--color-cyan-hover);--fill-gold:var(--color-gold-500);--fill-gold-hover:var(--color-gold-hover);--on-fill-accent:var(--color-blue-onFill);--on-fill-ok:var(--color-green-onFill);--on-fill-bad:var(--color-red-onFill);--on-fill-warn:var(--color-amber-onFill);--on-fill-purple:var(--color-purple-onFill);--on-fill-cyan:var(--color-cyan-onFill);--on-fill-gold:var(--color-gold-onFill);--text-accent:var(--color-blue-text);--text-accent-small:var(--color-blue-textSmall);--text-accent-vivid:var(--color-blue-textVivid);--text-ok:var(--color-green-text);--text-ok-small:var(--color-green-textSmall);--text-ok-vivid:var(--color-green-textVivid);--text-bad:var(--color-red-text);--text-bad-small:var(--color-red-textSmall);--text-bad-vivid:var(--color-red-textVivid);--text-warn:var(--color-amber-text);--text-warn-small:var(--color-amber-textSmall);--text-warn-vivid:var(--color-amber-textVivid);--text-purple:var(--color-purple-text);--text-purple-small:var(--color-purple-textSmall);--text-purple-vivid:var(--color-purple-textVivid);--text-cyan:var(--color-cyan-text);--text-cyan-small:var(--color-cyan-textSmall);--text-cyan-vivid:var(--color-cyan-textVivid);--text-gold:var(--color-gold-text);--text-gold-small:var(--color-gold-textSmall);--text-gold-vivid:var(--color-gold-textVivid);--accent:var(--color-blue-500);--green:var(--color-green-500);--red:var(--color-red-500);--amber:var(--color-amber-500);--purple:var(--color-purple-500);--cyan:var(--color-cyan-500);--gold:var(--color-gold-500);--grid-line:var(--color-line-grid);--dot-ok:var(--color-dot-ok);--dot-warn:var(--color-dot-warn);--dot-bad:var(--color-dot-bad);--font-mono:var(--font-family-mono);--font-sans:var(--font-family-sans);--font-numeric:tabular-nums;--type-display:var(--font-size-rem100);--type-title:var(--font-size-rem90);--type-body:var(--font-size-lg);--type-meta:var(--font-size-rem78);--type-small:var(--font-size-sm);--type-micro:var(--font-size-rem62);--terminal-bg:#111113;--terminal-fg:#edeef0;--terminal-border:#43484e}.dark{--buncss-light: ;--buncss-dark:initial;color-scheme:dark}@property --radius-lg{syntax: ""; inherits: true; initial-value: 8px;}@property --radius-md{syntax: ""; inherits: true; initial-value: 6px;}@property --radius-pill{syntax: ""; inherits: true; initial-value: 999px;}@property --radius-px2{syntax: ""; inherits: true; initial-value: 2px;}@property --radius-px5{syntax: ""; inherits: true; initial-value: 5px;}@property --radius-px7{syntax: ""; inherits: true; initial-value: 7px;}@property --radius-round{syntax: ""; inherits: true; initial-value: 50%;}@property --radius-sm{syntax: ""; inherits: true; initial-value: 4px;}@property --radius-xl{syntax: ""; inherits: true; initial-value: 10px;}@property --radius-xs{syntax: ""; inherits: true; initial-value: 3px;}@property --spacing-lg{syntax: ""; inherits: true; initial-value: .7rem;}@property --spacing-md{syntax: ""; inherits: true; initial-value: .6rem;}@property --spacing-px10{syntax: ""; inherits: true; initial-value: 10px;}@property --spacing-px12{syntax: ""; inherits: true; initial-value: 12px;}@property --spacing-px14{syntax: ""; inherits: true; initial-value: 14px;}@property --spacing-px2{syntax: ""; inherits: true; initial-value: 2px;}@property --spacing-px3{syntax: ""; inherits: true; initial-value: 3px;}@property --spacing-px4{syntax: ""; inherits: true; initial-value: 4px;}@property --spacing-px5{syntax: ""; inherits: true; initial-value: 5px;}@property --spacing-px6{syntax: ""; inherits: true; initial-value: 6px;}@property --spacing-px7{syntax: ""; inherits: true; initial-value: 7px;}@property --spacing-px8{syntax: ""; inherits: true; initial-value: 8px;}@property --spacing-px9{syntax: ""; inherits: true; initial-value: 9px;}@property --spacing-rem100{syntax: ""; inherits: true; initial-value: 1rem;}@property --spacing-rem120{syntax: ""; inherits: true; initial-value: 1.2rem;}@property --spacing-rem140{syntax: ""; inherits: true; initial-value: 1.4rem;}@property --spacing-rem150{syntax: ""; inherits: true; initial-value: 1.5rem;}@property --spacing-rem175{syntax: ""; inherits: true; initial-value: 1.75rem;}@property --spacing-rem180{syntax: ""; inherits: true; initial-value: 1.8rem;}@property --spacing-rem20{syntax: ""; inherits: true; initial-value: .2rem;}@property --spacing-rem35{syntax: ""; inherits: true; initial-value: .35rem;}@property --spacing-rem40{syntax: ""; inherits: true; initial-value: .4rem;}@property --spacing-rem50{syntax: ""; inherits: true; initial-value: .5rem;}@property --spacing-rem55{syntax: ""; inherits: true; initial-value: .55rem;}@property --spacing-rem75{syntax: ""; inherits: true; initial-value: .75rem;}@property --spacing-rem80{syntax: ""; inherits: true; initial-value: .8rem;}@property --spacing-rem95{syntax: ""; inherits: true; initial-value: .95rem;}@property --spacing-sm{syntax: ""; inherits: true; initial-value: .45rem;}@property --spacing-xl{syntax: ""; inherits: true; initial-value: .9rem;}@property --spacing-xs{syntax: ""; inherits: true; initial-value: .3rem;}@property --spacing-xxl{syntax: ""; inherits: true; initial-value: 1.1rem;}@property --spacing-xxs{syntax: ""; inherits: true; initial-value: .15rem;}@property --font-size-base{syntax: ""; inherits: true; initial-value: 13.5px;}@property --font-size-lg{syntax: ""; inherits: true; initial-value: .85rem;}@property --font-size-md{syntax: ""; inherits: true; initial-value: .76rem;}@property --font-size-px10{syntax: ""; inherits: true; initial-value: 10px;}@property --font-size-px11{syntax: ""; inherits: true; initial-value: 11px;}@property --font-size-px12{syntax: ""; inherits: true; initial-value: 12px;}@property --font-size-px13{syntax: ""; inherits: true; initial-value: 13px;}@property --font-size-px9{syntax: ""; inherits: true; initial-value: 9px;}@property --font-size-rem100{syntax: ""; inherits: true; initial-value: 1rem;}@property --font-size-rem105{syntax: ""; inherits: true; initial-value: 1.05rem;}@property --font-size-rem60{syntax: ""; inherits: true; initial-value: .6rem;}@property --font-size-rem62{syntax: ""; inherits: true; initial-value: .62rem;}@property --font-size-rem65{syntax: ""; inherits: true; initial-value: .65rem;}@property --font-size-rem68{syntax: ""; inherits: true; initial-value: .68rem;}@property --font-size-rem72{syntax: ""; inherits: true; initial-value: .72rem;}@property --font-size-rem74{syntax: ""; inherits: true; initial-value: .74rem;}@property --font-size-rem75{syntax: ""; inherits: true; initial-value: .75rem;}@property --font-size-rem78{syntax: ""; inherits: true; initial-value: .78rem;}@property --font-size-rem80{syntax: ""; inherits: true; initial-value: .8rem;}@property --font-size-rem82{syntax: ""; inherits: true; initial-value: .82rem;}@property --font-size-rem90{syntax: ""; inherits: true; initial-value: .9rem;}@property --font-size-rem95{syntax: ""; inherits: true; initial-value: .95rem;}@property --font-size-sm{syntax: ""; inherits: true; initial-value: .7rem;}@property --font-size-xl{syntax: ""; inherits: true; initial-value: .92rem;}@property --font-size-xs{syntax: ""; inherits: true; initial-value: .66rem;}@property --font-size-xxl{syntax: ""; inherits: true; initial-value: 1.15rem;}@property --font-size-xxs{syntax: ""; inherits: true; initial-value: .55rem;}}@font-face{font-family:JetBrains Mono;font-style:normal;font-weight:100 800;font-display:swap;src:url(data:font/woff2;base64,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)format(woff2)}body{-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.board{font-family:var(--font-sans);color:var(--text-1);width:min(100% - 2rem,96rem);margin-inline-start:auto;margin-inline-end:auto}body{background:var(--page);background-image:linear-gradient(var(--grid-line)1px,transparent 1px),linear-gradient(90deg,var(--grid-line)1px,transparent 1px);background-size:28px 28px;margin:0}.board-header{display:flex;justify-content:space-between;align-items: center}.board-title{display:flex;align-items: center;gap:var(--spacing-md)}.board-logo{display:block;flex:none;width:32px;height:32px}.board-subline{color:var(--text-2);margin:var(--spacing-lg)0 var(--spacing-rem150)}.subline-rest{margin-left:var(--spacing-xl)}.header-actions{display:flex;align-items: center;gap:var(--spacing-xs)}.settings-modal{overflow:hidden;width:min(920px,94vw);max-width:min(920px,94vw)}.settings-frame{display:block;width:calc(100% + var(--spacing-xxl));margin:.6rem calc(-1*var(--spacing-xxl))0 0;background:0 0;border:0;max-height:calc(80vh - 4.5rem)}.mt-6{margin-top:1.5rem}.section-title{display:flex;align-items: center;gap:var(--spacing-sm)}.section-title [data-part=tooltip]{line-height:0}.site-mark{display:inline-flex;width:var(--spacing-rem120);height:var(--spacing-rem120);border-radius:var(--radius-sm);vertical-align:middle;margin-right:var(--spacing-sm);flex:none;justify-content:center;align-items: center}.site-mark-letter{background:var(--surface-wash-fg-8);color:var(--text-4);font-family:var(--font-sans);font-size:var(--font-size-xs);font-weight:500}.tunnel-badge{display:inline-flex;align-items: center;gap:var(--spacing-xs);margin-right:var(--spacing-sm);font:inherit;color:var(--text-2);cursor:pointer;background:0 0;border:none;padding:0}.tunnel-label{font-size:var(--font-size-sm)}.tunnel-badge:hover .tunnel-label{color:var(--text-1)}.unstyled{color:inherit;text-decoration:none}.muted{color:var(--text-4);text-decoration:none}.modal-form .muted{color:var(--text-2)}.t-bad{color:var(--text-bad-vivid)}.t-ok{color:var(--text-ok-vivid)}.t-warn{color:var(--text-warn)}.live-source .live-source-name{font-family:var(--font-mono)}.live-mark{color:var(--text-ok-vivid)}.live-go-mark{color:var(--text-2)}.live-failed{all:unset;cursor:pointer;border-radius:var(--radius-sm,4px)}.live-failed:focus-visible{outline:2px solid var(--text-accent-small);outline-offset:2px}.t-accent{color:var(--text-accent-small)}.live-modal{width:min(30rem,100%);max-width:min(30rem,100%)}.live-modal-title{display:flex;gap:var(--spacing-xxs);flex-direction:column}.live-modal-title .muted{font-weight:400;font-size:var(--font-size-px13);color:var(--text-2)}.live-body{display:flex;gap:var(--spacing-md);padding:var(--spacing-rem150);flex-direction:column}.live-body .muted{color:var(--text-2)}.live-footer{justify-content:flex-end}.live-footer .footer-start{margin-right:auto}.live-help{display:flex;gap:var(--spacing-xxs);flex-direction:column}.live-help:empty{display:none}.live-help p{font-size:var(--font-size-px12);margin:0}.live-modal kbd{font-family:var(--font-mono);font-size:var(--font-size-px12)}.live-detail{display:inline-flex;align-items: center;gap:var(--spacing-sm)}.live-log{padding:var(--spacing-md);background:var(--surface-inset);border:1px solid var(--border-on-card);border-radius:var(--radius-md);color:var(--text-2);font-family:var(--font-mono);font-size:var(--font-size-px12);white-space:pre-wrap;margin:0}.site-name{display:inline-flex;align-items: center;gap:var(--spacing-xxs);vertical-align:middle}.site-name a{font-size:var(--font-size-md)}.cell-tag{margin-left:var(--spacing-sm)}.site-name a.unstyled{transition:color .12s}.site-name a.unstyled:hover{color:var(--text-accent-small)}.site-name a.unstyled:focus-visible{color:var(--text-accent-small)}.public-link{display:inline-flex;margin-left:var(--spacing-sm);border-radius:var(--radius-sm);color:var(--text-accent-small);background:color-mix(in srgb,var(--fill-accent)12%,transparent);justify-content:center;align-items: center;width:24px;height:24px;transition:color .12s,background .12s}.public-link:hover{color:var(--text-accent-small);background:color-mix(in srgb,var(--fill-accent)20%,transparent)}.public-link:focus-visible{color:var(--text-accent-small);background:color-mix(in srgb,var(--fill-accent)20%,transparent)}.preflight-issue{display:flex;gap:var(--spacing-xxs);white-space:normal;flex-direction:column;align-items: flex-start;max-width:22rem}.preflight-issue code{white-space:normal;word-break:break-word;font-size:.75em}.dev-link-input{display:inline-flex;align-items: center;gap:var(--spacing-xs);width:100%}.dev-link-input [data-part=field]{flex:1;min-width:0}.dev-link-input [data-part=field-input]{width:100%}.modal-form{display:flex;gap:var(--spacing-md);flex-direction:column}.modal-footer{display:flex;justify-content:flex-end;gap:var(--spacing-sm);margin-top:var(--spacing-md)}[data-part=table-cell] [data-part=switch]{vertical-align:middle}[data-part=statusdot]{margin-right:var(--spacing-sm)}.row-actions{display:flex;justify-content:flex-end;align-items: center;gap:var(--spacing-xxs)}.row-actions .dev-link-input{flex:1;min-width:0}.row-icon{color:var(--text-2)}.row-actions [data-part=button].t-warn{color:var(--text-warn-vivid)}.row-gear{opacity:0;transition:opacity .12s,border-color .12s,background .12s,color .12s,transform 80ms}@media (hover:none){.row-gear{opacity:1}}@media (hover:hover){.apps-grid [data-part=table-row]:hover .row-gear{opacity:1}}.apps-grid [data-part=table-row]:has(:focus-visible) .row-gear{opacity:1}@media (prefers-reduced-motion:reduce){.row-gear{transition:none}}.version-cell{display:inline-flex;align-items: center;gap:var(--spacing-xs)}.version-sha{font-family:var(--font-mono)}.version-sha.t-warn{color:var(--text-warn-small)}.apps-grid .col-gap{padding-left:calc(var(--spacing-md) + var(--spacing-lg))}.col-ident{font-family:var(--font-mono)}.apps-grid [data-part=table]{table-layout:fixed}.apps-panel{background:var(--card);border:1px solid var(--border-on-card);border-radius:var(--radius-lg);overflow:hidden;--muted:var(--text-muted-on-card);--border:var(--border-on-card);--border-soft:var(--border-soft-on-card)}.apps-grid{padding:2px 0 var(--spacing-sm)}.apps-grid [data-part=table-cell]:first-child,.apps-grid thead:not(.head-hidden) [data-part=table-headcell]:first-child{padding-left:calc(var(--spacing-lg) + var(--spacing-md))}.apps-grid [data-part=table-cell]:last-child,.apps-grid thead:not(.head-hidden) [data-part=table-headcell]:last-child{padding-right:calc(var(--spacing-lg) + var(--spacing-md))}.update-strip{display:flex;align-items: center;gap:var(--spacing-px10);padding:var(--spacing-px14)calc(var(--spacing-lg) + var(--spacing-md));background:var(--surface-wash-amber-7);border-bottom:1px solid var(--border-soft-on-card);font-size:var(--font-size-px13);line-height:var(--line-height-snug)}.update-strip-icon{width:var(--spacing-rem100);height:var(--spacing-rem100);color:var(--text-warn-vivid);flex:none}.update-strip-text{flex:1}.apps-grid [data-part=table-row]:last-child [data-part=table-cell]{border-bottom:none}.apps-grid thead.head-hidden th{color:#0000;overflow:hidden;border:0;height:0;padding:0;font-size:0;line-height:0}.railway-globe{display:inline-flex;margin-left:var(--spacing-xs);align-items: center}.railway-globe.railway-live{color:var(--text-accent-small)}.railway-globe.railway-deploying,.railway-globe.railway-verifying{color:var(--text-warn-small)}.railway-globe.railway-error{color:var(--text-bad-small)}.app-settings-modal{width:min(1000px,100%);max-width:min(1000px,100%)}.app-settings-modal,.live-modal{font-size:var(--font-size-base);padding:0}.app-settings-modal.app-settings-reduced{width:min(640px,100%);max-width:min(640px,100%)}.app-settings-overlay{backdrop-filter:none;padding-top:14.5vh}.app-settings-modal [data-part=button]:not([data-icon-only]),.live-modal [data-part=button]:not([data-icon-only]){padding-block:var(--spacing-px2);font-size:var(--font-size-px13)}.app-settings-modal [data-part=button][data-variant=default],.app-settings-modal [data-part=copybutton],.live-modal [data-part=button][data-variant=default]{background:var(--surface-card)}.app-settings-modal [data-part=button][data-variant=default]:hover:not(:disabled),.live-modal [data-part=button][data-variant=default]:hover:not(:disabled){background:var(--surface-wash-fg-4-card)}.app-settings-modal [data-part=field-input]{background:var(--surface-inset)}.app-settings-modal [data-part=modal-head],.live-modal [data-part=modal-head]{position:sticky;z-index:1;gap:var(--spacing-md);padding:calc(var(--spacing-rem100) + var(--spacing-px4))var(--spacing-rem150);background:var(--panel);border-bottom:1px solid var(--border);top:0}.app-settings-modal [data-part=modal-title],.live-modal [data-part=modal-title]{letter-spacing:normal;color:var(--text-1);flex:1;min-width:0;font-weight:400}.settings-head{display:flex;align-items: center;gap:var(--spacing-lg)}.settings-head .site-mark{width:calc(var(--spacing-rem100)*2.5);height:calc(var(--spacing-rem100)*2.5);border-radius:var(--radius-lg);margin-right:0}.settings-head .site-mark-letter{font-size:calc(var(--font-size-xs)*2)}.settings-identity{display:flex;gap:var(--spacing-xxs);flex-direction:column;min-width:0}.settings-name-line{display:flex;align-items: center;gap:var(--spacing-sm)}.settings-name,.live-modal-title{font-size:calc(var(--font-size-rem100)*1.25);font-weight:700}.settings-url{display:inline-flex;align-items:baseline;gap:var(--spacing-xs);font-family:var(--font-mono);font-size:var(--font-size-px13);color:var(--text-accent);text-decoration:none}.settings-url:hover{text-decoration:underline}.settings-url.muted{color:var(--text-2)}.settings-head-actions{display:flex;align-items: center;gap:var(--spacing-sm);flex:none;margin-left:auto}.settings-pill [data-part=badge]{gap:var(--spacing-md);padding:var(--spacing-xxs)var(--spacing-md);border-radius:var(--radius-pill);font-size:var(--font-size-px13)}.settings-pill-dot{width:var(--spacing-sm);height:var(--spacing-sm);border-radius:var(--radius-round);background:currentColor}.settings-pill[data-tone=ok] .settings-pill-dot{background:var(--dot-ok)}.settings-pill[data-tone=bad] .settings-pill-dot{background:var(--dot-bad)}.settings-pill[data-tone=warn] .settings-pill-dot{background:var(--dot-warn)}.settings-pill-detail{font-family:var(--font-mono)}.settings-issues{display:flex;gap:var(--spacing-sm);padding:var(--spacing-rem150)var(--spacing-rem150)0;flex-direction:column}.settings-issues [data-part=alert]{padding:var(--spacing-md)var(--spacing-lg);border:1px solid color-mix(in srgb,var(--text-bad)45%,transparent);border-radius:var(--radius-lg)}.settings-issue{display:flex;align-items: center;gap:var(--spacing-sm)}.settings-issue [data-part=icon]{color:var(--text-bad-vivid);flex:none}.settings-grid{display:grid;grid-template-columns:1fr 1fr;align-items: start}.settings-col,.settings-stack{display:flex;gap:var(--spacing-rem150);padding:var(--spacing-rem150);flex-direction:column;min-width:0}.settings-col:not(:last-child){border-right:1px solid var(--border)}.app-settings-reduced .settings-grid{grid-template-columns:1fr}@media (max-width:760px){.settings-grid{grid-template-columns:1fr}.settings-col:not(:last-child){border-right:none;border-bottom:1px solid var(--border)}}.settings-block{display:flex;gap:var(--spacing-lg);flex-direction:column;min-width:0}.settings-heading{font-size:var(--font-size-lg);color:var(--text-1);margin:0;font-weight:700}.settings-note{color:var(--text-2);font-size:var(--font-size-px12)}.settings-mono{font-family:var(--font-mono)}.app-settings-modal{--settings-label-col:calc(var(--spacing-rem150)*4.5)}.settings-block-head{display:flex;gap:var(--spacing-xxs);flex-direction:column}.settings-block-head .settings-note{margin:0}.settings-facts{display:grid;grid-template-columns:var(--settings-label-col)minmax(0,1fr);align-items: center;gap:var(--spacing-rem80)0;margin:0}.settings-facts dt{display:flex;align-items: center;gap:var(--spacing-xs);color:var(--text-2)}.settings-facts dd{overflow-wrap:anywhere;min-width:0;margin:0}.settings-deployed{display:flex;align-items: center;gap:var(--spacing-sm);flex-wrap:wrap}.settings-override{display:flex;justify-content:space-between;align-items: center;gap:var(--spacing-sm)}.settings-actions{display:flex;align-items: center;gap:var(--spacing-sm);flex-wrap:wrap}.settings-actions .command-labeled[data-intent=warn]{background:var(--surface-wash-amber-7)}.settings-actions .command-labeled[data-intent=warn]:hover:not(:disabled){background:var(--button-hover)}.settings-live-source{display:flex;gap:var(--spacing-xs);align-items: center}.live-procs{list-style:none;border:1px solid var(--border-on-card);border-radius:var(--radius-lg);margin:0;padding:0}.live-proc{display:flex;align-items: center;gap:var(--spacing-md);padding:var(--spacing-sm)var(--spacing-md)}.live-proc-item+.live-proc-item{border-top:1px solid var(--border-on-card)}.live-proc-log{margin:0 var(--spacing-md)var(--spacing-md);overflow:auto;white-space:pre-wrap;max-height:12rem}.live-proc-kind{width:3rem;font-weight:500}.live-proc-cmd{font-size:var(--font-size-px11);overflow:hidden;text-overflow:ellipsis;white-space:nowrap;color:var(--text-2);flex:1;min-width:0}.settings-actions-end{display:flex;gap:var(--spacing-xs);margin-left:auto}.settings-inline-form{display:flex;align-items: flex-start;gap:var(--spacing-sm)}.settings-inline-form [data-part=field]{width:calc(var(--spacing-rem150)*5)}.settings-toggle-row{display:flex;justify-content:space-between;align-items: center;gap:var(--spacing-lg)}.settings-toggle-text{display:flex;gap:var(--spacing-xxs);flex-direction:column;min-width:0}.settings-toggle-label{display:inline-flex;align-items: center;gap:var(--spacing-xs);color:var(--text-1)}.settings-form{display:grid;grid-template-columns:var(--settings-label-col)minmax(0,1fr);gap:var(--spacing-sm)0}.settings-form [data-part=field]{display:grid;grid-column:1/-1;grid-template-columns:subgrid;align-items: center;row-gap:var(--spacing-xs);font-size:inherit}.settings-form [data-part=field-label]{color:var(--text-2);font-size:inherit}.settings-form [data-part=field-input]{font-family:var(--font-mono)}.settings-form [data-part=field-error]{grid-column:2}.settings-form-actions{display:flex;grid-column:1/-1;justify-content:flex-end}.settings-block [data-part=field]{font-size:inherit}.settings-block [data-part=field-input]{padding:var(--spacing-px4)var(--spacing-sm)}.settings-link-input [data-part=field-input]{font-family:var(--font-mono)}.settings-help{display:inline-flex;vertical-align:middle}.settings-help-icon{display:inline-flex;color:var(--text-2);font:inherit;cursor:help;border-radius:var(--radius-round);background:0 0;border:none;margin:0;padding:0}.settings-help-icon:focus-visible{outline:var(--spacing-px2)solid var(--fill-accent);outline-offset:var(--spacing-px2)}.settings-errors-empty,.settings-errors-tail{background:var(--bg);border:1px solid var(--border);border-radius:var(--radius-lg);margin:0}.settings-errors-empty{display:flex;justify-content:center;align-items: center;gap:var(--spacing-sm);padding:var(--spacing-rem175)var(--spacing-xl);color:var(--text-2);font-size:var(--font-size-md);text-align:center}.settings-errors-tail{padding:var(--spacing-md)var(--spacing-lg);color:var(--text-bad);font-family:var(--font-mono);font-size:var(--font-size-px12);line-height:var(--line-height-base);white-space:pre-wrap;word-break:break-word;overflow-y:auto;max-height:240px}.settings-errors-foot{display:flex;justify-content:space-between;align-items: center;gap:var(--spacing-sm)}.settings-errors-foot [data-part=copybutton]{gap:var(--spacing-xs);color:var(--text-1);font:inherit;font-size:var(--font-size-px13)}.settings-list{display:flex;list-style:none;background:var(--card);border:1px solid var(--border);border-radius:var(--radius-lg);flex-direction:column;margin:0;padding:0}.settings-item{display:grid;grid-template-columns:auto minmax(0,1fr)auto;align-items: center;gap:var(--spacing-sm)var(--spacing-md);padding:var(--spacing-px10)var(--spacing-lg)}.settings-item+.settings-item{border-top:1px solid var(--border-on-card)}.settings-item-icon{display:inline-flex;color:var(--text-2)}.settings-item-text{display:flex;gap:var(--spacing-xxs);flex-direction:column;min-width:0}.settings-item-end{display:flex;justify-content:flex-end;align-items: center}.settings-item-actions{display:flex;align-items: center;gap:var(--spacing-xs)}.settings-item-body{display:flex;align-items: flex-start;gap:var(--spacing-sm);grid-column:2/-1;flex-direction:column;min-width:0}.settings-item-body>[data-part=alert],.settings-grow{align-self:stretch}.settings-grow [data-part=field]{flex:1;width:auto}.settings-remote{display:flex;justify-content:space-between;align-items: center;gap:var(--spacing-sm);align-self:stretch}.settings-remote-status{display:inline-flex;align-items: center;gap:var(--spacing-xs);font-size:var(--font-size-px13)}.settings-remote-status[data-tone=ok]{color:var(--text-ok)}.settings-remote-status[data-tone=ok] .settings-pill-dot{background:var(--dot-ok)}.settings-remote-status[data-tone=warn]{color:var(--text-warn)}.settings-remote-status[data-tone=warn] .settings-pill-dot{background:var(--dot-warn)}.settings-remote-status[data-tone=bad]{color:var(--text-bad)}.settings-remote-status[data-tone=bad] .settings-pill-dot{background:var(--dot-bad)}.settings-item-body [data-part=segmented-option]{padding:var(--spacing-xs)var(--spacing-md);font-size:var(--font-size-px13);color:var(--text-2)}.settings-item-body [data-part=segmented-option][data-active]{background:var(--surface-wash-fg-8);color:var(--text-1)}.settings-chips{display:flex;gap:var(--spacing-xs);flex-wrap:wrap}.settings-chip{gap:var(--spacing-xs);padding:var(--spacing-xxs)var(--spacing-sm);border-radius:var(--radius-md);font-family:inherit;font-size:var(--font-size-px12);color:var(--text-1)}.settings-chip-remove{display:inline-flex;color:var(--text-2);cursor:pointer;border-radius:var(--radius-round);background:0 0;border:none;padding:0}.settings-chip-remove [data-part=icon]{width:var(--spacing-px12);height:var(--spacing-px12)}.settings-chip-remove:hover{color:var(--text-1)}.settings-chip-remove:focus-visible{outline:var(--spacing-px2)solid var(--fill-accent);outline-offset:1px}.settings-open-note{display:flex;align-items: flex-start;gap:var(--spacing-sm);padding:var(--spacing-md)var(--spacing-lg);background:var(--surface-wash-amber-7);border:1px solid color-mix(in srgb,var(--text-warn)45%,transparent);border-radius:var(--radius-lg);color:var(--text-1);font-size:var(--font-size-px12);margin:0}.settings-open-note [data-part=icon]{color:var(--text-warn-vivid);flex:none}.settings-footer{position:sticky;display:flex;align-items: center;gap:var(--spacing-md);padding:var(--spacing-lg)var(--spacing-rem150);background:var(--card);border-top:1px solid var(--border);bottom:0}.settings-footer-note{margin-left:auto}@layer soribashi.recipes{.root_jT5pOw{display:flex;gap:var(--spacing-xs);color:var(--text-1);background:var(--sb-alert-bg);border-left:2px solid var(--sb-alert-color);border-radius:var(--radius-md);padding:var(--spacing-sm)var(--spacing-lg);flex-direction:column}.command_jT5pOw{font-family:var(--font-mono);font-size:var(--font-size-xs);background:var(--panel);padding:var(--spacing-sm);border-radius:var(--radius-md);overflow-x:auto;white-space:pre-wrap;margin:0}}@layer soribashi.recipes{.root_so3lDg{display:inline-flex;align-items: center;gap:var(--spacing-xxs);font-family:var(--font-sans);font-size:var(--font-size-xs);line-height:var(--line-height-base);font-variant-numeric:var(--font-numeric);color:var(--sb-badge-color);background:var(--sb-badge-bg);border:1px solid var(--sb-badge-border);border-radius:var(--radius-pill);padding:0 var(--spacing-sm);white-space:nowrap}.root_so3lDg>[data-part=spinner]{margin-inline-end:var(--spacing-xxs)}}@layer soribashi.recipes{.root_0gPzkQ{display:inline-block;width:round(var(--sb-spinner-size),2px);height:round(var(--sb-spinner-size),2px);will-change:transform;box-sizing:border-box;aspect-ratio:1;vertical-align:middle;flex-shrink:0}.root_0gPzkQ svg{display:block;overflow:visible;width:100%;height:100%}}@layer soribashi.recipes{[data-part=spinner]{animation:1s linear infinite sb-spinner-spin}@keyframes sb-spinner-spin{to{transform:rotate(360deg)}}}@layer soribashi.recipes{.root_30t2rQ{display:inline-flex;justify-content:center;align-items: center;gap:var(--spacing-xs);font-family:var(--font-sans);font-size:var(--font-size-sm);line-height:var(--line-height-base);border:1px solid var(--button-border);border-radius:var(--radius-md);padding:var(--spacing-xs)var(--spacing-md);cursor:pointer;background:var(--button-bg);color:var(--button-color);transition:border-color .12s,background .12s,color .12s,transform 80ms}.root_30t2rQ:hover:not(:disabled){background:var(--button-hover)}.root_30t2rQ:focus-visible{outline:1px solid var(--fill-accent);outline-offset:1px}.root_30t2rQ:disabled{opacity:.55;cursor:default}.root_30t2rQ:active:not(:disabled){transform:translateY(1px)}.root_30t2rQ[hidden]{display:none}.root_30t2rQ[data-size=sm]{font-size:var(--font-size-xs);padding:var(--spacing-xxs)var(--spacing-sm)}.root_30t2rQ[data-size=lg]{font-size:var(--font-size-rem80);padding:var(--spacing-px5)var(--spacing-px12)}.root_30t2rQ[data-variant=filled]{font-weight:500}.root_30t2rQ[data-icon-only]{padding:var(--spacing-xs);aspect-ratio:1;min-width:var(--spacing-rem175);min-height:var(--spacing-rem175)}.root_30t2rQ[data-variant=default][data-intent=bad]{color:var(--sb-button-bad-color);border-color:var(--fill-bad)}}@layer soribashi.recipes{.root_TGYYvw{display:inline-flex;align-items: center;gap:var(--spacing-px4);padding:1px var(--spacing-px6);border:1px solid var(--border);border-radius:var(--radius-sm);font-size:var(--font-size-px11);white-space:nowrap;background:var(--chip-bg,transparent);color:var(--chip-color,inherit);font-family:inherit;font-weight:inherit;line-height:inherit;text-align:inherit;text-decoration:none;flex-shrink:0;margin:0}.root_TGYYvw[data-variant=outline]{border-color:var(--chip-border,currentColor)}.root_TGYYvw[data-dimmed]{opacity:var(--sb-chip-dimmed-opacity)}.root_TGYYvw[data-uppercase]{text-transform:uppercase;letter-spacing:var(--sb-chip-uppercase-tracking);font-size:var(--font-size-xxs);font-weight:500}@supports (text-box: trim-both cap alphabetic){.root_TGYYvw[data-uppercase]{padding-block:var(--spacing-px4)}.root_TGYYvw[data-uppercase] .label_TGYYvw{text-box:trim-both cap alphabetic}}.root_TGYYvw:-webkit-any(button,a){cursor:pointer}.root_TGYYvw:-moz-any(button,a){cursor:pointer}.root_TGYYvw:is(button,a){cursor:pointer}.root_TGYYvw:-webkit-any(button,a):hover{background:var(--chip-hover,var(--card))}.root_TGYYvw:-moz-any(button,a):hover{background:var(--chip-hover,var(--card))}.root_TGYYvw:is(button,a):hover{background:var(--chip-hover,var(--card))}.root_TGYYvw:-webkit-any(button,a):focus-visible{outline:2px solid var(--fill-accent);outline-offset:2px}.root_TGYYvw:-moz-any(button,a):focus-visible{outline:2px solid var(--fill-accent);outline-offset:2px}.root_TGYYvw:is(button,a):focus-visible{outline:2px solid var(--fill-accent);outline-offset:2px}.icon_TGYYvw{display:inline-flex;flex-shrink:0;align-items: center;line-height:1}}@layer soribashi.recipes{[data-part=chip][data-pulse]{animation-name:chip-pulse;animation-duration:var(--sb-chip-pulse-period);animation-timing-function:ease-in-out;animation-iteration-count:infinite}@media (prefers-reduced-motion:reduce){[data-part=chip][data-pulse]{animation-name:none}}@keyframes chip-pulse{0%,to{opacity:1}50%{opacity:.5}}}@layer soribashi.recipes{.root_Puve9g{flex-shrink:0}}@layer soribashi.recipes{.overlay_gUyvOg{position:fixed;z-index:100;display:flex;padding:var(--sb-modal-overlay-pad-y)var(--spacing-rem100);background:var(--surface-wash-bg-55);backdrop-filter:blur(2px);justify-content:center;align-items: flex-start;inset:0}.root_gUyvOg{box-sizing:border-box;max-width:var(--sb-modal-max-w);max-height:var(--sb-modal-max-h);overflow-y:auto;border:1px solid var(--border);border-radius:var(--radius-xl);background:var(--panel);padding:var(--spacing-rem100)var(--spacing-xxl)var(--spacing-rem120);box-shadow:var(--shadow-modal);width:100%}.head_gUyvOg{display:flex;justify-content:space-between;align-items: center}.title_gUyvOg{letter-spacing:var(--sb-modal-title-tracking);color:var(--text-accent-small);font-weight:700}.close_gUyvOg{color:var(--text-2);cursor:pointer;font-size:var(--font-size-rem90);padding:var(--spacing-rem20)var(--spacing-rem40);border-radius:var(--radius-px5);background:0 0;border:none}.close_gUyvOg:hover{color:var(--text-1);background:var(--card)}.close_gUyvOg:focus-visible{outline:2px solid var(--fill-accent);outline-offset:2px}}@layer soribashi.recipes{.body_-s5fvg{padding:var(--spacing-md)0;font-size:var(--font-size-sm);color:var(--text-1)}.foot_-s5fvg{display:flex;justify-content:flex-end;gap:var(--spacing-sm)}}@layer soribashi.recipes{.positioner_TfyK5Q{z-index:200}.root_TfyK5Q{box-sizing:border-box;min-width:var(--sb-contextmenu-min-w);padding:var(--spacing-px4);background:var(--card);border:1px solid var(--border);border-radius:var(--radius-lg);box-shadow:var(--shadow-menu);font-size:var(--font-size-px13);outline:none}.label_TfyK5Q{padding:var(--spacing-px5)var(--spacing-px8)var(--spacing-px6);font-family:var(--font-sans);font-size:var(--font-size-px11);color:var(--text-4);border-bottom:1px solid var(--border-soft);margin-bottom:var(--spacing-px4)}.item_TfyK5Q{box-sizing:border-box;display:flex;justify-content:space-between;align-items: center;gap:var(--spacing-px12);padding:var(--spacing-px7)var(--spacing-px8);border-radius:var(--radius-px5);color:var(--text-1);font:inherit;text-align:left;cursor:pointer;outline:none;background:0 0;border:none;width:100%}.item_TfyK5Q[data-highlighted]:not([data-disabled]),.item_TfyK5Q[data-popup-open]{background:var(--surface-wash-accent-16)}.item_TfyK5Q[data-disabled]:focus-visible:not(:hover){background:var(--surface-wash-accent-16)}.item_TfyK5Q[data-disabled]{color:var(--muted);cursor:not-allowed}.hint_TfyK5Q{font-family:var(--font-sans);font-size:var(--font-size-px10);color:var(--text-4)}.item_TfyK5Q[data-disabled] .hint_TfyK5Q{font-style:italic}.item_TfyK5Q[data-disabled] .chevron_TfyK5Q{color:inherit}.separator_TfyK5Q{box-sizing:border-box;border-top:1px solid var(--border-soft);margin:var(--spacing-px4)var(--spacing-px2);height:1px}.submenu_TfyK5Q{box-sizing:border-box;min-width:var(--sb-contextmenu-min-w);padding:var(--spacing-px4);background:var(--card);border:1px solid var(--border);border-radius:var(--radius-lg);box-shadow:var(--shadow-menu);font-size:var(--font-size-px13);outline:none}.chevron_TfyK5Q{display:flex;color:var(--text-4)}.row_TfyK5Q{display:flex;gap:var(--spacing-px4)}.row_TfyK5Q>.item_TfyK5Q{flex:1;justify-content:center;width:auto}}@layer soribashi.recipes{[data-part=contextmenu]{animation:90ms ease-out contextmenu-in}@keyframes contextmenu-in{0%{opacity:0;transform:scale(.97)translateY(-2px)}to{opacity:1;transform:none}}}@layer soribashi.recipes{.root_V4wrdw{display:inline-flex;padding:var(--spacing-xs)var(--spacing-sm);cursor:pointer;border-radius:var(--radius-md);background:var(--panel);border:1px solid var(--border);color:var(--text-3);justify-content:center;align-items: center}.root_V4wrdw:hover{color:var(--text-1);border-color:var(--fill-accent)}.root_V4wrdw[data-copied]{color:var(--text-ok-small);border-color:var(--fill-ok)}.root_V4wrdw:focus-visible{outline:2px solid var(--fill-accent);outline-offset:2px}}@layer soribashi.recipes{.overlay_QCiRPg{position:fixed;display:flex;background:var(--surface-wash-bg-55);backdrop-filter:blur(2px);inset:0}.overlay_QCiRPg[data-side=right]{z-index:100;cursor:default;white-space:normal;justify-content:flex-end}.overlay_QCiRPg[data-side=left]{z-index:90}.root_QCiRPg{box-sizing:border-box;width:var(--sb-sidedrawer-w);overflow-y:auto;background:var(--panel);display:flex;flex-direction:column;height:100%}.root_QCiRPg[data-side=right]{border-left:1px solid var(--border);box-shadow:var(--shadow-drawer)}.root_QCiRPg[data-side=left]{border-right:1px solid var(--border);padding:var(--spacing-px14);gap:var(--spacing-px14);box-shadow:var(--shadow-drawer-left)}}@layer soribashi.recipes{[data-part=sidedrawer]{animation:.16s ease-out sidedrawer-in}[data-part=sidedrawer][data-side=right]{--sb-sidedrawer-slide-x:100%}[data-part=sidedrawer][data-side=left]{--sb-sidedrawer-slide-x:-100%}[data-part=sidedrawer][data-closing]{animation:.12s ease-in forwards sidedrawer-out}@keyframes sidedrawer-in{0%{transform:translateX(var(--sb-sidedrawer-slide-x))}to{transform:none}}@keyframes sidedrawer-out{to{transform:translateX(var(--sb-sidedrawer-slide-x))}}@media (prefers-reduced-motion:reduce){[data-part=sidedrawer],[data-part=sidedrawer][data-closing]{animation:none}}}@layer soribashi.recipes{.frame_Z0A9GA{display:flex;flex-direction:column;flex:auto;min-height:0}.nav_Z0A9GA{display:flex;gap:var(--spacing-rem40);padding:var(--spacing-rem75)var(--spacing-rem100)var(--spacing-rem50);border-bottom:1px solid var(--border);flex-direction:column;flex-shrink:0}.back_Z0A9GA{color:var(--text-4);cursor:pointer;font:inherit;font-size:var(--font-size-sm);background:0 0;border:none;align-self: flex-start;padding:0}.back_Z0A9GA:hover{text-decoration:underline}.back_Z0A9GA:focus-visible{outline:2px solid var(--fill-accent);outline-offset:2px}.navRow_Z0A9GA{display:flex;justify-content:space-between;align-items: center;gap:var(--spacing-rem50)}.title_Z0A9GA{font-weight:700;font-size:var(--font-size-xl);color:var(--text-1);overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.navActions_Z0A9GA{display:flex;align-items: center;gap:var(--spacing-rem40);flex-shrink:0}.navAction_Z0A9GA{color:var(--text-accent-small);cursor:pointer;font:inherit;padding:var(--spacing-rem20)var(--spacing-rem40);border-radius:var(--radius-px5);background:0 0;border:none;font-weight:500}.navAction_Z0A9GA:hover:not(:disabled){background:var(--surface-wash-accent-14)}.navAction_Z0A9GA:focus-visible{outline:2px solid var(--fill-accent);outline-offset:2px}.navAction_Z0A9GA:disabled{opacity:.55;cursor:default}.close_Z0A9GA{color:var(--text-2);cursor:pointer;font-size:var(--font-size-rem90);padding:var(--spacing-rem20)var(--spacing-rem40);border-radius:var(--radius-px5);background:0 0;border:none}.close_Z0A9GA:hover{color:var(--text-1);background:var(--card)}.close_Z0A9GA:focus-visible{outline:2px solid var(--fill-accent);outline-offset:2px}.header_Z0A9GA{padding:var(--spacing-rem50)var(--spacing-rem100);border-bottom:1px solid var(--border-soft);flex-shrink:0}.content_Z0A9GA{overflow-y:auto;padding:var(--spacing-rem100);flex:auto;min-height:0}}@layer soribashi.recipes{.root_ricmaA{display:flex;gap:var(--spacing-sm);font-family:var(--font-sans);font-size:var(--font-size-sm);color:var(--text-1);flex-direction:column}.label_ricmaA{color:var(--text-4);font-size:var(--font-size-xs)}.input_ricmaA{background:var(--panel);color:var(--text-1);border:1px solid var(--border);border-radius:var(--radius-md);padding:var(--spacing-sm);font-family:inherit;font-size:inherit}.input_ricmaA:focus-visible{outline:1px solid var(--fill-accent);outline-offset:1px}.input_ricmaA:where(textarea){resize:vertical}.error_ricmaA{color:var(--text-bad-small);font-size:var(--font-size-xs)}.option_ricmaA{display:flex;gap:var(--spacing-sm);align-items: center}}@layer soribashi.recipes{.root_RnlAng{display:inline-flex;align-items: center;gap:var(--spacing-sm);font-family:var(--font-sans);font-size:var(--font-size-sm);color:var(--text-1);cursor:pointer}.control_RnlAng{appearance:none;width:var(--spacing-rem180);height:var(--spacing-rem100);border:1px solid var(--border);border-radius:var(--radius-pill);background:var(--sb-switch-bg-off);position:relative;cursor:pointer;margin:0;transition:background .12s,border-color .12s}.control_RnlAng:after{content:"";position:absolute;top:calc((var(--spacing-rem100) - var(--spacing-rem75) - var(--spacing-px2))/2);left:var(--spacing-px2);width:var(--spacing-rem75);height:var(--spacing-rem75);border-radius:var(--radius-round);background:var(--muted);transition:left .12s,background .12s}.control_RnlAng:checked{background:var(--sb-switch-bg-on);border-color:var(--fill-accent)}.control_RnlAng:checked:after{left:calc(100% - var(--spacing-rem75) - var(--spacing-px2));background:var(--fill-accent)}.control_RnlAng:focus-visible{outline:1px solid var(--fill-accent);outline-offset:1px}.control_RnlAng:disabled{opacity:.55;cursor:default}}@layer soribashi.recipes{.root_yG8YKA{border:1px solid var(--border);border-radius:var(--radius-lg);background:var(--panel);overflow:hidden;font-family:var(--font-sans);font-size:var(--font-size-sm);color:var(--text-1)}.list_yG8YKA{list-style:none;margin:0;padding:0}.footer_yG8YKA{padding:var(--spacing-sm)var(--spacing-md);color:var(--text-4);font-size:var(--font-size-xs);margin:0}.item_yG8YKA{border-bottom:1px solid var(--border-soft)}.item_yG8YKA:last-child{border-bottom:none}.label_yG8YKA{min-width:0}.value_yG8YKA{color:var(--text-4);font-variant-numeric:var(--font-numeric)}.trailing_yG8YKA{display:inline-flex;align-items: center;gap:var(--spacing-xs)}.chevron_yG8YKA{color:var(--text-4)}.navButton_yG8YKA{display:flex;justify-content:space-between;align-items: center;gap:var(--spacing-sm);padding:var(--spacing-sm)var(--spacing-md);font:inherit;color:inherit;text-align:left;cursor:pointer;background:0 0;border:none;width:100%;margin:0;transition:background .12s}.actionButton_yG8YKA{display:flex;justify-content:flex-start;align-items: center;gap:var(--spacing-sm);padding:var(--spacing-sm)var(--spacing-md);font:inherit;color:inherit;text-align:left;cursor:pointer;background:0 0;border:none;width:100%;margin:0;transition:background .12s}.actionButton_yG8YKA[data-intent=accent]{color:var(--text-accent-small)}.actionButton_yG8YKA[data-intent=bad]{color:var(--text-bad-small)}.actionButton_yG8YKA[data-centered]{text-align:center;justify-content:center}.navButton_yG8YKA:hover:not(:disabled),.actionButton_yG8YKA:hover:not(:disabled){background:var(--surface-wash-fg-5)}.navButton_yG8YKA:focus-visible{outline:1px solid var(--fill-accent);outline-offset:1px}.actionButton_yG8YKA:focus-visible{outline:1px solid var(--fill-accent);outline-offset:1px}.navButton_yG8YKA:disabled,.actionButton_yG8YKA:disabled{opacity:.55;cursor:default}@media (prefers-reduced-motion:reduce){.navButton_yG8YKA,.actionButton_yG8YKA{transition:none}}.toggleRow_yG8YKA,.factRow_yG8YKA{display:flex;justify-content:space-between;align-items: center;gap:var(--spacing-sm);padding:var(--spacing-sm)var(--spacing-md)}.inputRow_yG8YKA{padding:var(--spacing-sm)var(--spacing-md)}}@layer soribashi.recipes{.root_QopY9g{font-family:var(--font-sans);font-size:var(--font-size-rem95);color:var(--text-1);overflow-wrap:anywhere;line-height:1.6}.root_QopY9g h1,.root_QopY9g h2,.root_QopY9g h3,.root_QopY9g h4{margin:var(--sb-markdown-heading-margin-top)0 var(--sb-markdown-heading-margin-bottom);font-weight:700;line-height:1.25}.root_QopY9g h1{font-size:var(--sb-markdown-h1-size);padding-bottom:var(--sb-markdown-heading-underline-pad);border-bottom:1px solid var(--border)}.root_QopY9g h2{font-size:var(--sb-markdown-h2-size);padding-bottom:var(--sb-markdown-heading-underline-pad);border-bottom:1px solid var(--border)}.root_QopY9g h3{font-size:var(--sb-markdown-h3-size)}.root_QopY9g h4{font-size:var(--sb-markdown-h4-size)}.root_QopY9g h1:first-child,.root_QopY9g h2:first-child,.root_QopY9g h3:first-child{margin-top:0}.root_QopY9g p,.root_QopY9g ul,.root_QopY9g ol{margin:var(--sb-markdown-block-margin)0}.root_QopY9g ul,.root_QopY9g ol{padding-left:var(--sb-markdown-list-indent)}.root_QopY9g li{margin:var(--sb-markdown-list-item-margin)0}.root_QopY9g a{color:var(--text-accent)}.root_QopY9g code{font-family:var(--font-mono);font-size:var(--sb-markdown-code-size);background:var(--card);padding:var(--sb-markdown-code-pad-block)var(--sb-markdown-code-pad-inline);border-radius:var(--radius-sm)}.root_QopY9g pre{background:var(--card);border:1px solid var(--border);border-radius:var(--radius-lg);padding:var(--spacing-lg)var(--spacing-xl);overflow-x:auto;margin:var(--sb-markdown-pre-margin)0}.root_QopY9g pre code{background:0 0;padding:0}.root_QopY9g blockquote{margin:var(--sb-markdown-blockquote-margin)0;padding:var(--sb-markdown-blockquote-pad-block)var(--sb-markdown-blockquote-pad-inline);color:var(--text-2);border-left:var(--sb-markdown-blockquote-border-w)solid var(--border)}.root_QopY9g table{border-collapse:collapse;margin:var(--sb-markdown-table-margin)0;display:block;overflow-x:auto}.root_QopY9g th,.root_QopY9g td{border:1px solid var(--border);padding:var(--sb-markdown-cell-pad-block)var(--sb-markdown-cell-pad-inline);text-align:left}.root_QopY9g hr{border:none;border-top:1px solid var(--border);margin:var(--sb-markdown-hr-margin)0}}@layer soribashi.recipes{.root_EZIvCA{position:relative;border:1px solid var(--border);border-radius:var(--radius-lg);background:var(--surface-wash-panel-88);padding:var(--spacing-xxl)0 var(--spacing-xs);margin:var(--spacing-rem150)0 var(--spacing-rem180)}.title_EZIvCA{position:absolute;top:var(--panel-title-offset);left:var(--spacing-xl);background:var(--page);padding:0 var(--spacing-rem50);font-size:var(--font-size-rem74);letter-spacing:var(--panel-title-tracking);color:var(--text-accent-small);cursor:pointer;display:inline-flex;align-items: center;gap:var(--panel-title-gap);border:0;font-family:inherit;font-weight:700}.title_EZIvCA:hover{text-decoration:underline}.title_EZIvCA:focus-visible{outline:2px solid var(--fill-accent);outline-offset:2px;border-radius:var(--radius-px2)}.caret_EZIvCA{display:inline-flex;opacity:.7;align-items: center;transition:transform .12s}.caret_EZIvCA svg{width:var(--spacing-px12);height:var(--spacing-px12)}.root_EZIvCA:not([data-collapsed]) .caret_EZIvCA{transform:rotate(90deg)}.count_EZIvCA{color:var(--text-4);font-weight:400}.root_EZIvCA[data-collapsed]{margin:var(--spacing-rem75)0;background:0 0;border:0;padding:0}.root_EZIvCA[data-collapsed] .title_EZIvCA{box-sizing:border-box;position:static;display:flex;text-align:left;padding:var(--spacing-rem40)var(--spacing-xl);background:var(--surface-wash-panel-70);border:1px solid var(--border);border-radius:var(--radius-md);width:100%}.root_EZIvCA[data-collapsed] .title_EZIvCA:hover{background:var(--surface-wash-panel-100)}}@layer soribashi.recipes{.root_EgffBA{position:fixed;z-index:200;width:0;height:0;top:0;left:0}.popup_EgffBA{box-sizing:border-box;max-width:var(--available-width);overflow:auto;color:var(--text-1);font-family:var(--font-sans);padding:var(--spacing-px4);background:var(--card);border:1px solid var(--border);border-radius:var(--radius-lg);box-shadow:var(--shadow-menu);font-size:var(--font-size-px13);outline:none;width:max-content}}@layer soribashi.recipes{.root_fWmRbg{display:flex;max-height:var(--sb-scrollpane-max);overflow:hidden;box-sizing:border-box;border:1px solid var(--border-soft);border-radius:var(--radius-xl);background:var(--card);padding:var(--spacing-rem100)var(--spacing-xxl)var(--spacing-xxl);font-family:var(--font-sans);font-size:var(--type-meta);flex-direction:column;min-width:0;min-height:0}.head_fWmRbg{margin:calc(-1*var(--spacing-rem100))calc(-1*var(--spacing-xxl))var(--spacing-rem55);padding:var(--spacing-md)var(--spacing-xxl);font-size:var(--type-title);font-weight:500;line-height:var(--line-height-snug);color:var(--text-accent);background:var(--surface-wash-fg-4-card);border-bottom:1px solid var(--border-soft);border-radius:calc(var(--radius-xl) - 1px)calc(var(--radius-xl) - 1px)0 0;flex-shrink:0}.body_fWmRbg{overflow-y:auto;margin-right:calc(-1*var(--spacing-xxl));padding-right:var(--spacing-xxl);scrollbar-width:thin;scrollbar-color:var(--border-on-card)transparent;min-height:0}}@layer soribashi.recipes{.root_1GCN5Q{display:flex;gap:var(--spacing-px6);flex-direction:column}.label_1GCN5Q{color:var(--text-1);font-family:var(--font-sans);font-size:var(--font-size-px13);font-weight:500}.trigger_1GCN5Q{box-sizing:border-box;display:flex;align-items: center;gap:var(--spacing-px6);padding:var(--spacing-px6)var(--spacing-px10);background:var(--card);color:var(--text-1);border:1px solid var(--border-control);border-radius:var(--radius-md);font-family:var(--font-mono);font-size:var(--font-size-px13);cursor:default;width:100%}.trigger_1GCN5Q[data-popup-open]{border-color:var(--fill-accent)}.value_1GCN5Q{text-align:left;flex:1}.chevron_1GCN5Q{color:var(--text-2);display:flex}.portal_1GCN5Q{position:fixed;z-index:300;width:0;height:0;top:0;left:0}.popup_1GCN5Q{box-sizing:border-box;width:var(--anchor-width);max-height:min(var(--sb-searchselect-max-h),var(--available-height));display:flex;background:var(--card);color:var(--text-1);border:1px solid var(--border);border-radius:var(--radius-lg);box-shadow:var(--shadow-menu);overflow:hidden;outline:none;flex-direction:column}.searchBox_1GCN5Q{box-sizing:border-box;display:flex;align-items: center;gap:var(--spacing-px8);margin:var(--spacing-px8);padding:var(--spacing-px6)var(--spacing-px10);background:var(--inset);border:1px solid var(--border-control);border-radius:var(--radius-md)}.searchBox_1GCN5Q:focus-within{outline:1px solid var(--fill-accent);outline-offset:1px}.searchIcon_1GCN5Q{color:var(--text-2);display:flex}.search_1GCN5Q{color:var(--text-1);font-family:var(--font-sans);font-size:var(--font-size-px13);outline:none;background:0 0;border:none;flex:1;min-width:0;margin:0;padding:0}.list_1GCN5Q{overflow-y:auto;overscroll-behavior:contain;padding-bottom:var(--spacing-px4)}.groupLabel_1GCN5Q{padding:var(--spacing-px8)var(--spacing-px14)var(--spacing-px4);color:var(--text-2);font-family:var(--font-sans);font-size:var(--font-size-px11);letter-spacing:var(--sb-searchselect-tracking);text-transform:uppercase;font-weight:500}.item_1GCN5Q{display:flex;align-items: center;gap:var(--spacing-px8);min-height:var(--sb-searchselect-item-h);padding:0 var(--spacing-px14);font-family:var(--font-mono);font-size:var(--font-size-px13);cursor:default;outline:none}.item_1GCN5Q[data-highlighted]{background:var(--sb-searchselect-hover)}.itemIcon_1GCN5Q{color:var(--text-2);display:flex}.itemLabel_1GCN5Q{flex:1}.itemDetail_1GCN5Q{color:var(--text-2);font-family:var(--font-sans);font-size:var(--font-size-px12)}.check_1GCN5Q{color:var(--text-accent);display:flex}.empty_1GCN5Q{padding:var(--spacing-px10)var(--spacing-px14);color:var(--text-2);font-size:var(--font-size-px13)}.empty_1GCN5Q:empty{padding:0}.footer_1GCN5Q{display:flex;gap:var(--spacing-px14);padding:var(--spacing-px8)var(--spacing-px14);background:var(--panel);border-top:1px solid var(--line-3);color:var(--text-2);font-family:var(--font-sans);font-size:var(--font-size-px12)}}@layer soribashi.recipes{.root_AuavJw{display:inline-flex;border:1px solid var(--border);border-radius:var(--radius-md);overflow:hidden;background:var(--panel)}.option_AuavJw{display:inline-flex;padding:var(--spacing-xs)var(--spacing-sm);cursor:pointer;color:var(--text-3);background:0 0;border:none;justify-content:center;align-items: center;font-weight:400}.option_AuavJw+.option_AuavJw{border-left:1px solid var(--border)}.option_AuavJw[data-active]{background:var(--fill-accent);color:var(--on-fill-accent);font-weight:500}.option_AuavJw:not([data-active]):hover{color:var(--text-1)}.option_AuavJw:focus-visible{outline:2px solid var(--fill-accent);outline-offset:2px}.root_AuavJw[data-text] .option_AuavJw{padding:var(--spacing-xs)var(--spacing-rem55);font-size:var(--sb-segmented-text-size)}.root_AuavJw[data-soft]{display:flex;gap:var(--spacing-px2);padding:var(--spacing-px2);background:var(--surface-inset);border:none}.root_AuavJw[data-soft] .option_AuavJw{border-radius:var(--radius-px5);flex:1}.root_AuavJw[data-soft] .option_AuavJw+.option_AuavJw{border-left:none}.root_AuavJw[data-soft] .option_AuavJw[data-active]{background:var(--card);color:var(--text-1);box-shadow:var(--shadow-card)}}@layer soribashi.recipes{.root_HD_UaQ{display:inline-flex;width:var(--sb-selectbox-size);height:var(--sb-selectbox-size);font-family:inherit;font-weight:inherit;font-size:var(--font-size-rem105);cursor:pointer;border-radius:var(--radius-px5);color:var(--text-2);opacity:.5;background:0 0;border:none;flex:none;justify-content:center;align-items: center;padding:0;transition:opacity .12s,color .12s,background .12s;line-height:1}.root_HD_UaQ:hover{opacity:1;color:var(--text-1);background:var(--surface-wash-accent-14)}.root_HD_UaQ[data-checked]{opacity:1;color:var(--text-accent)}.root_HD_UaQ:focus-visible{outline:2px solid var(--fill-accent);outline-offset:2px}}@layer soribashi.recipes{.root_1fs1zQ{flex-shrink:0;align-self: center}.dot_1fs1zQ{font-size:var(--font-size-rem100);display:inline-block;vertical-align:middle;position:relative;color:var(--sd-color);line-height:1;top:-2px}.card_1fs1zQ{box-sizing:border-box;position:fixed;z-index:300;white-space:pre-line;font-family:var(--font-sans);font-size:var(--font-size-rem74);text-align:left;background:var(--panel);color:var(--text-1);border:1px solid var(--border);border-radius:var(--radius-md);padding:var(--spacing-sm)var(--spacing-lg);pointer-events:none;width:max-content;max-width:320px;font-weight:400;line-height:1.5}}@layer soribashi.recipes{.root_PXbu4g{overflow-x:auto;overflow-y:hidden;scrollbar-width:thin;scrollbar-color:var(--border)transparent}.root_PXbu4g::-webkit-scrollbar{height:var(--spacing-px8);width:var(--spacing-px8)}.root_PXbu4g::-webkit-scrollbar-track{background:0 0}.root_PXbu4g::-webkit-scrollbar-thumb{background:var(--muted);border-radius:var(--radius-pill)}.table_PXbu4g{border-collapse:collapse;font-family:var(--font-sans);font-size:var(--font-size-sm);width:100%}.headcell_PXbu4g{color:var(--text-4);text-transform:uppercase;font-size:var(--font-size-xs);letter-spacing:var(--sb-table-tracking);text-align:left;padding:var(--spacing-xs)var(--spacing-md);border-bottom:1px solid var(--border)}.cell_PXbu4g{padding:var(--spacing-sm)var(--spacing-md);border-bottom:1px solid var(--border-soft);white-space:nowrap;vertical-align:middle;font-variant-numeric:var(--font-numeric)}.cell_PXbu4g[data-align=end]{text-align:right}.row_PXbu4g:hover{background:var(--surface-wash-fg-5)}}@layer soribashi.recipes{.root_8hsw9A{position:fixed;right:var(--sb-toasthost-offset);bottom:var(--sb-toasthost-offset);z-index:210;display:flex;gap:var(--spacing-px8);pointer-events:none;flex-direction:column}.toast_8hsw9A{box-sizing:border-box;padding:var(--spacing-px9)var(--font-size-px13);background:var(--card);border:1px solid var(--border);border-left:var(--spacing-px3)solid var(--fill-accent);border-radius:0 var(--radius-px7)var(--radius-px7)0;box-shadow:var(--shadow-toast);color:var(--text-1);font-size:var(--font-size-px13);min-width:240px;max-width:320px}.toast_8hsw9A[data-state],.toast_8hsw9A[data-icon]{display:flex;align-items: center;gap:var(--spacing-px8)}.status_8hsw9A{display:inline-flex;color:var(--text-accent);flex-shrink:0}.toast_8hsw9A[data-icon] .status_8hsw9A{padding:var(--spacing-px5);border-radius:var(--radius-round);background:var(--surface-wash-accent-14)}.toast_8hsw9A[data-state=done]{border-left-color:var(--fill-ok)}.toast_8hsw9A[data-state=done] .status_8hsw9A{color:var(--text-ok)}.toast_8hsw9A[data-state=failed]{border-left-color:var(--fill-bad)}.toast_8hsw9A[data-state=failed] .status_8hsw9A{color:var(--text-bad)}}@layer soribashi.recipes{[data-part=toasthost-toast]{animation:.25s cubic-bezier(.51,.3,0,1.21) toasthost-in}[data-part=toasthost-toast][data-leaving]{overflow:hidden;pointer-events:none;animation:.25s cubic-bezier(.51,.3,0,1.21) forwards toasthost-out,.25s linear forwards toasthost-collapse}@keyframes toasthost-in{0%{opacity:0;transform:translate(100%)}to{opacity:1;transform:translate(0)}}@keyframes toasthost-out{0%{opacity:1;transform:translate(0)}to{opacity:0;transform:translate(100%)}}@keyframes toasthost-collapse{0%{max-height:320px}to{max-height:0;padding-block:0;border-block-width:0;margin-bottom:calc(-1*var(--spacing-px8))}}[data-part=toasthost-toast]{transition:border-left-color .2s ease-out}[data-part=toasthost-toast][data-state=done] [data-part=toasthost-status],[data-part=toasthost-toast][data-state=failed] [data-part=toasthost-status]{animation:.22s ease-out toasthost-check}@keyframes toasthost-check{0%{opacity:0;transform:scale(.6)}to{opacity:1;transform:none}}}@layer soribashi.recipes{.root_6ouFMg{position:relative;display:inline-block}.card_6ouFMg{box-sizing:border-box;position:fixed;z-index:300;white-space:pre-line;max-width:var(--sb-tooltip-max-w);font-family:var(--font-sans);font-size:var(--font-size-px12);text-align:left;background:var(--sb-tooltip-bg);color:var(--card);border-radius:var(--radius-md);padding:var(--spacing-px6)var(--spacing-px10);box-shadow:var(--sb-tooltip-shadow);transform-origin:var(--tooltip-arrow-x,50%)top;pointer-events:none;border:0;width:max-content;font-weight:500;line-height:1.4}.card_6ouFMg:before{content:"";position:absolute;top:calc(-1*var(--sb-tooltip-arrow-h));left:clamp(var(--radius-md),calc(var(--tooltip-arrow-x,50%) - var(--sb-tooltip-arrow-w)/2),calc(100% - var(--sb-tooltip-arrow-w) - var(--radius-md)));width:var(--sb-tooltip-arrow-w);height:var(--sb-tooltip-arrow-h);background:var(--sb-tooltip-bg);clip-path:var(--sb-tooltip-arrow-clip)}}@layer soribashi.recipes{[data-part=tooltip-card]{animation:.14s cubic-bezier(.2,.8,.3,1) tooltip-in}@media (prefers-reduced-motion:reduce){[data-part=tooltip-card]{animation:none}}@keyframes tooltip-in{0%{opacity:0;transform:var(--sb-tooltip-enter-from)}}}
+@layer soribashi.tokens,soribashi.recipes,soribashi.utilities;@layer soribashi.tokens{:root{--buncss-light:initial;--buncss-dark: ;color-scheme:light;--color-amber-500:var(--buncss-light,#ef5f00)var(--buncss-dark,#f76b15);--color-amber-hover:var(--buncss-light,#d65704)var(--buncss-dark,#ff801f);--color-amber-onFill:var(--buncss-light,#fff)var(--buncss-dark,#fff);--color-amber-text:var(--buncss-light,#cc4e00)var(--buncss-dark,#ffa057);--color-amber-textSmall:var(--buncss-light,#582d1d)var(--buncss-dark,#ffe0c2);--color-amber-textVivid:var(--buncss-light,#cc4e00)var(--buncss-dark,#ffa057);--color-blue-500:var(--buncss-light,#3e63dd)var(--buncss-dark,#3e63dd);--color-blue-hover:var(--buncss-light,#3358d4)var(--buncss-dark,#5472e4);--color-blue-onFill:var(--buncss-light,#fff)var(--buncss-dark,#fff);--color-blue-text:var(--buncss-light,#3a5bc7)var(--buncss-dark,#9eb1ff);--color-blue-textSmall:var(--buncss-light,#1f2d5c)var(--buncss-dark,#d6e1ff);--color-blue-textVivid:var(--buncss-light,#3a5bc7)var(--buncss-dark,#9eb1ff);--color-cyan-500:var(--buncss-light,#0797b9)var(--buncss-dark,#00a2c7);--color-cyan-hover:var(--buncss-light,#0a89a7)var(--buncss-dark,#23afd0);--color-cyan-onFill:var(--buncss-light,#fff)var(--buncss-dark,#fff);--color-cyan-text:var(--buncss-light,#107d98)var(--buncss-dark,#4ccce6);--color-cyan-textSmall:var(--buncss-light,#0d3c48)var(--buncss-dark,#b6ecf7);--color-cyan-textVivid:var(--buncss-light,#107d98)var(--buncss-dark,#4ccce6);--color-dot-bad:var(--buncss-light,#e93d82)var(--buncss-dark,#e93d82);--color-dot-ok:var(--buncss-light,#0d9b8a)var(--buncss-dark,#12a594);--color-dot-warn:var(--buncss-light,#ef5f00)var(--buncss-dark,#f76b15);--color-gold-500:var(--buncss-light,#ffc53d)var(--buncss-dark,#ffc53d);--color-gold-hover:var(--buncss-light,#ffba18)var(--buncss-dark,#ffd60a);--color-gold-onFill:var(--buncss-light,#1c2024)var(--buncss-dark,#1c2024);--color-gold-text:var(--buncss-light,#ab6400)var(--buncss-dark,#ffca16);--color-gold-textSmall:var(--buncss-light,#4f3422)var(--buncss-dark,#ffe7b3);--color-gold-textVivid:var(--buncss-light,#ab6400)var(--buncss-dark,#ffca16);--color-gray-accentText:var(--buncss-light,#3a5bc7)var(--buncss-dark,#9eb1ff);--color-gray-badgeText:var(--buncss-light,#60646c)var(--buncss-dark,#b0b4ba);--color-gray-fg:var(--buncss-light,#1c2024)var(--buncss-dark,#edeef0);--color-gray-muted:var(--buncss-light,#8b8d98)var(--buncss-dark,#696e77);--color-gray-mutedOnCard:var(--buncss-light,#60646c)var(--buncss-dark,#b0b4ba);--color-gray-mutedText:var(--buncss-light,#60646c)var(--buncss-dark,#b0b4ba);--color-gray-okText:var(--buncss-light,#008573)var(--buncss-dark,#0bd8b6);--color-gray-redText:var(--buncss-light,#cb1d63)var(--buncss-dark,#ff92ad);--color-gray-warnText:var(--buncss-light,#cc4e00)var(--buncss-dark,#ffa057);--color-green-500:var(--buncss-light,#0d9b8a)var(--buncss-dark,#12a594);--color-green-hover:var(--buncss-light,#0f8c7e)var(--buncss-dark,#0eb39e);--color-green-onFill:var(--buncss-light,#fff)var(--buncss-dark,#fff);--color-green-text:var(--buncss-light,#008573)var(--buncss-dark,#0bd8b6);--color-green-textSmall:var(--buncss-light,#0d3d38)var(--buncss-dark,#adf0dd);--color-green-textVivid:var(--buncss-light,#008573)var(--buncss-dark,#0bd8b6);--color-ground-1:var(--buncss-light,#fff)var(--buncss-dark,#111113);--color-ground-2:var(--buncss-light,#f9f9fb)var(--buncss-dark,#18191b);--color-ground-3:var(--buncss-light,#f0f0f3)var(--buncss-dark,#212225);--color-ground-4:var(--buncss-light,#e8e8ec)var(--buncss-dark,#272a2d);--color-ink-1:var(--buncss-light,#1c2024)var(--buncss-dark,#edeef0);--color-ink-2:var(--buncss-light,#60646c)var(--buncss-dark,#b0b4ba);--color-ink-3:var(--buncss-light,#60646c)var(--buncss-dark,#b0b4ba);--color-ink-4:var(--buncss-light,#60646c)var(--buncss-dark,#b0b4ba);--color-line-border:var(--buncss-light,#cdced6)var(--buncss-dark,#43484e);--color-line-control:var(--buncss-light,#b9bbc6)var(--buncss-dark,#696e77);--color-line-controlEdgeOnCard:var(--buncss-light,#b9bbc6)var(--buncss-dark,#696e77);--color-line-edgeOnCard:var(--buncss-light,#cdced6)var(--buncss-dark,#696e77);--color-line-grid:var(--buncss-light,#343b580d)var(--buncss-dark,#7aa2f70f);--color-line-soft:var(--buncss-light,#d9d9e0)var(--buncss-dark,#363a3f);--color-line-softOnCard:var(--buncss-light,#d9d9e0)var(--buncss-dark,#43484e);--color-purple-500:var(--buncss-light,#8e4ec6)var(--buncss-dark,#8e4ec6);--color-purple-hover:var(--buncss-light,#8347b9)var(--buncss-dark,#9a5cd0);--color-purple-onFill:var(--buncss-light,#fff)var(--buncss-dark,#fff);--color-purple-text:var(--buncss-light,#8145b5)var(--buncss-dark,#d19dff);--color-purple-textSmall:var(--buncss-light,#402060)var(--buncss-dark,#ecd9fa);--color-purple-textVivid:var(--buncss-light,#8145b5)var(--buncss-dark,#d19dff);--color-red-500:var(--buncss-light,#e93d82)var(--buncss-dark,#e93d82);--color-red-hover:var(--buncss-light,#df3478)var(--buncss-dark,#ee518a);--color-red-onFill:var(--buncss-light,#fff)var(--buncss-dark,#fff);--color-red-text:var(--buncss-light,#cb1d63)var(--buncss-dark,#ff92ad);--color-red-textSmall:var(--buncss-light,#621639)var(--buncss-dark,#fdd3e8);--color-red-textVivid:var(--buncss-light,#cb1d63)var(--buncss-dark,#ff92ad);--color-rule-1:var(--buncss-light,#b9bbc6)var(--buncss-dark,#696e77);--color-rule-2:var(--buncss-light,#cdced6)var(--buncss-dark,#43484e);--color-rule-3:var(--buncss-light,#d9d9e0)var(--buncss-dark,#363a3f);--color-surface-bg:var(--buncss-light,#f0f0f3)var(--buncss-dark,#111113);--color-surface-card:var(--buncss-light,#fff)var(--buncss-dark,#212225);--color-surface-chrome:var(--buncss-light,#f0f0f3)var(--buncss-dark,#18191b);--color-surface-inset:var(--buncss-light,#f0f0f3)var(--buncss-dark,#111113);--color-surface-overlay:var(--buncss-light,#f9f9fb)var(--buncss-dark,#212225);--color-surface-panel:var(--buncss-light,#f9f9fb)var(--buncss-dark,#18191b);--color-surface-raised:var(--buncss-light,#e8e8ec)var(--buncss-dark,#272a2d);--radius-lg:8px;--radius-md:6px;--radius-pill:999px;--radius-px2:2px;--radius-px5:5px;--radius-px7:7px;--radius-round:50%;--radius-sm:4px;--radius-xl:10px;--radius-xs:3px;--spacing-lg:.7rem;--spacing-md:.6rem;--spacing-px10:10px;--spacing-px12:12px;--spacing-px14:14px;--spacing-px2:2px;--spacing-px3:3px;--spacing-px4:4px;--spacing-px5:5px;--spacing-px6:6px;--spacing-px7:7px;--spacing-px8:8px;--spacing-px9:9px;--spacing-rem100:1rem;--spacing-rem120:1.2rem;--spacing-rem140:1.4rem;--spacing-rem150:1.5rem;--spacing-rem175:1.75rem;--spacing-rem180:1.8rem;--spacing-rem20:.2rem;--spacing-rem35:.35rem;--spacing-rem40:.4rem;--spacing-rem50:.5rem;--spacing-rem55:.55rem;--spacing-rem75:.75rem;--spacing-rem80:.8rem;--spacing-rem95:.95rem;--spacing-sm:.45rem;--spacing-xl:.9rem;--spacing-xs:.3rem;--spacing-xxl:1.1rem;--spacing-xxs:.15rem;--font-size-base:13.5px;--font-size-lg:.85rem;--font-size-md:.76rem;--font-size-px10:10px;--font-size-px11:11px;--font-size-px12:12px;--font-size-px13:13px;--font-size-px9:9px;--font-size-rem100:1rem;--font-size-rem105:1.05rem;--font-size-rem60:.6rem;--font-size-rem62:.62rem;--font-size-rem65:.65rem;--font-size-rem68:.68rem;--font-size-rem72:.72rem;--font-size-rem74:.74rem;--font-size-rem75:.75rem;--font-size-rem78:.78rem;--font-size-rem80:.8rem;--font-size-rem82:.82rem;--font-size-rem90:.9rem;--font-size-rem95:.95rem;--font-size-sm:.7rem;--font-size-xl:.92rem;--font-size-xs:.66rem;--font-size-xxl:1.15rem;--font-size-xxs:.55rem;--font-family-mono:"JetBrains Mono",ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;--font-family-sans:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,Helvetica,Arial,sans-serif;--line-height-base:1.55;--line-height-snug:1.4;--shadow-card:0 2px 5px -1px var(--buncss-light,#10182829)var(--buncss-dark,#0009);--shadow-drawer:-6px 0 32px #0000004d;--shadow-drawer-left:4px 0 28px #0000004d;--shadow-menu:0 10px 30px #00000047,0 2px 8px #0000002e;--shadow-modal:0 12px 40px #00000040;--shadow-toast:0 6px 20px #00000040;--breakpoint-2xl:96rem;--breakpoint-3xl:120rem;--breakpoint-lg:64rem;--breakpoint-md:48rem;--breakpoint-sm:40rem;--breakpoint-xl:80rem;--breakpoint-xs:24rem;--text-accentText:var(--color-gray-accentText);--text-badText:var(--color-gray-redText);--text-muted:var(--color-gray-mutedText);--text-muted-on-card:var(--color-gray-mutedOnCard);--text-primary:var(--color-gray-fg);--surface-canvas:var(--color-surface-bg);--surface-card:var(--color-surface-card);--surface-chrome:var(--color-surface-chrome);--surface-inset:var(--color-surface-inset);--surface-overlay:var(--color-surface-overlay);--surface-panel:var(--color-surface-panel);--surface-raised:var(--color-surface-raised);--surface-wash-accent-14:color-mix(in srgb,var(--accent)14%,transparent);--surface-wash-accent-16:color-mix(in srgb,var(--accent)16%,transparent);--surface-wash-accent-7:color-mix(in srgb,var(--accent)7%,transparent);--surface-wash-accent-fg-70:color-mix(in srgb,var(--accent)70%,var(--fg));--surface-wash-amber-45:color-mix(in srgb,var(--amber)45%,transparent);--surface-wash-amber-7:color-mix(in srgb,var(--amber)7%,transparent);--surface-wash-bg-55:color-mix(in srgb,var(--bg)55%,transparent);--surface-wash-cyan-border-45:color-mix(in srgb,var(--cyan)45%,var(--border));--surface-wash-cyan-panel-38:color-mix(in srgb,var(--cyan)38%,var(--panel));--surface-wash-fg-4-card:color-mix(in srgb,var(--fg)4%,var(--card));--surface-wash-fg-5:color-mix(in srgb,var(--fg)5%,transparent);--surface-wash-fg-8:color-mix(in srgb,var(--fg)8%,transparent);--surface-wash-panel-100:color-mix(in srgb,var(--panel)100%,transparent);--surface-wash-panel-55:color-mix(in srgb,var(--panel)55%,transparent);--surface-wash-panel-70:color-mix(in srgb,var(--panel)70%,transparent);--surface-wash-panel-88:color-mix(in srgb,var(--panel)88%,transparent);--surface-wash-panel-94:color-mix(in srgb,var(--panel)94%,transparent);--border-control:var(--color-line-control);--border-control-on-card:var(--color-line-controlEdgeOnCard);--border-default:var(--color-line-border);--border-on-card:var(--color-line-edgeOnCard);--border-soft:var(--color-line-soft);--border-soft-on-card:var(--color-line-softOnCard);--bg:var(--surface-canvas);--panel:var(--surface-panel);--card:var(--surface-card);--chrome:var(--surface-chrome);--fg:var(--text-primary);--muted:var(--color-gray-muted);--muted-text:var(--text-muted);--accent-text:var(--text-accentText);--red-text:var(--text-badText);--border:var(--border-default);--page:var(--surface-canvas);--raised:var(--surface-raised);--surface-1:var(--color-ground-1);--surface-2:var(--color-ground-2);--surface-3:var(--color-ground-3);--surface-4:var(--color-ground-4);--text-1:var(--color-ink-1);--text-2:var(--color-ink-2);--text-3:var(--color-ink-3);--text-4:var(--color-ink-4);--line-1:var(--color-rule-1);--line-2:var(--color-rule-2);--line-3:var(--color-rule-3);--fill-accent:var(--color-blue-500);--fill-accent-hover:var(--color-blue-hover);--fill-ok:var(--color-green-500);--fill-ok-hover:var(--color-green-hover);--fill-bad:var(--color-red-500);--fill-bad-hover:var(--color-red-hover);--fill-warn:var(--color-amber-500);--fill-warn-hover:var(--color-amber-hover);--fill-purple:var(--color-purple-500);--fill-purple-hover:var(--color-purple-hover);--fill-cyan:var(--color-cyan-500);--fill-cyan-hover:var(--color-cyan-hover);--fill-gold:var(--color-gold-500);--fill-gold-hover:var(--color-gold-hover);--on-fill-accent:var(--color-blue-onFill);--on-fill-ok:var(--color-green-onFill);--on-fill-bad:var(--color-red-onFill);--on-fill-warn:var(--color-amber-onFill);--on-fill-purple:var(--color-purple-onFill);--on-fill-cyan:var(--color-cyan-onFill);--on-fill-gold:var(--color-gold-onFill);--text-accent:var(--color-blue-text);--text-accent-small:var(--color-blue-textSmall);--text-accent-vivid:var(--color-blue-textVivid);--text-ok:var(--color-green-text);--text-ok-small:var(--color-green-textSmall);--text-ok-vivid:var(--color-green-textVivid);--text-bad:var(--color-red-text);--text-bad-small:var(--color-red-textSmall);--text-bad-vivid:var(--color-red-textVivid);--text-warn:var(--color-amber-text);--text-warn-small:var(--color-amber-textSmall);--text-warn-vivid:var(--color-amber-textVivid);--text-purple:var(--color-purple-text);--text-purple-small:var(--color-purple-textSmall);--text-purple-vivid:var(--color-purple-textVivid);--text-cyan:var(--color-cyan-text);--text-cyan-small:var(--color-cyan-textSmall);--text-cyan-vivid:var(--color-cyan-textVivid);--text-gold:var(--color-gold-text);--text-gold-small:var(--color-gold-textSmall);--text-gold-vivid:var(--color-gold-textVivid);--accent:var(--color-blue-500);--green:var(--color-green-500);--red:var(--color-red-500);--amber:var(--color-amber-500);--purple:var(--color-purple-500);--cyan:var(--color-cyan-500);--gold:var(--color-gold-500);--grid-line:var(--color-line-grid);--dot-ok:var(--color-dot-ok);--dot-warn:var(--color-dot-warn);--dot-bad:var(--color-dot-bad);--font-mono:var(--font-family-mono);--font-sans:var(--font-family-sans);--font-numeric:tabular-nums;--type-display:var(--font-size-rem100);--type-title:var(--font-size-rem90);--type-body:var(--font-size-lg);--type-meta:var(--font-size-rem78);--type-small:var(--font-size-sm);--type-micro:var(--font-size-rem62);--terminal-bg:#111113;--terminal-fg:#edeef0;--terminal-border:#43484e}.dark{--buncss-light: ;--buncss-dark:initial;color-scheme:dark}@property --radius-lg{syntax: ""; inherits: true; initial-value: 8px;}@property --radius-md{syntax: ""; inherits: true; initial-value: 6px;}@property --radius-pill{syntax: ""; inherits: true; initial-value: 999px;}@property --radius-px2{syntax: ""; inherits: true; initial-value: 2px;}@property --radius-px5{syntax: ""; inherits: true; initial-value: 5px;}@property --radius-px7{syntax: ""; inherits: true; initial-value: 7px;}@property --radius-round{syntax: ""; inherits: true; initial-value: 50%;}@property --radius-sm{syntax: ""; inherits: true; initial-value: 4px;}@property --radius-xl{syntax: ""; inherits: true; initial-value: 10px;}@property --radius-xs{syntax: ""; inherits: true; initial-value: 3px;}@property --spacing-lg{syntax: ""; inherits: true; initial-value: .7rem;}@property --spacing-md{syntax: ""; inherits: true; initial-value: .6rem;}@property --spacing-px10{syntax: ""; inherits: true; initial-value: 10px;}@property --spacing-px12{syntax: ""; inherits: true; initial-value: 12px;}@property --spacing-px14{syntax: ""; inherits: true; initial-value: 14px;}@property --spacing-px2{syntax: ""; inherits: true; initial-value: 2px;}@property --spacing-px3{syntax: ""; inherits: true; initial-value: 3px;}@property --spacing-px4{syntax: ""; inherits: true; initial-value: 4px;}@property --spacing-px5{syntax: ""; inherits: true; initial-value: 5px;}@property --spacing-px6{syntax: ""; inherits: true; initial-value: 6px;}@property --spacing-px7{syntax: ""; inherits: true; initial-value: 7px;}@property --spacing-px8{syntax: ""; inherits: true; initial-value: 8px;}@property --spacing-px9{syntax: ""; inherits: true; initial-value: 9px;}@property --spacing-rem100{syntax: ""; inherits: true; initial-value: 1rem;}@property --spacing-rem120{syntax: ""; inherits: true; initial-value: 1.2rem;}@property --spacing-rem140{syntax: ""; inherits: true; initial-value: 1.4rem;}@property --spacing-rem150{syntax: ""; inherits: true; initial-value: 1.5rem;}@property --spacing-rem175{syntax: ""; inherits: true; initial-value: 1.75rem;}@property --spacing-rem180{syntax: ""; inherits: true; initial-value: 1.8rem;}@property --spacing-rem20{syntax: ""; inherits: true; initial-value: .2rem;}@property --spacing-rem35{syntax: ""; inherits: true; initial-value: .35rem;}@property --spacing-rem40{syntax: ""; inherits: true; initial-value: .4rem;}@property --spacing-rem50{syntax: ""; inherits: true; initial-value: .5rem;}@property --spacing-rem55{syntax: ""; inherits: true; initial-value: .55rem;}@property --spacing-rem75{syntax: ""; inherits: true; initial-value: .75rem;}@property --spacing-rem80{syntax: ""; inherits: true; initial-value: .8rem;}@property --spacing-rem95{syntax: ""; inherits: true; initial-value: .95rem;}@property --spacing-sm{syntax: ""; inherits: true; initial-value: .45rem;}@property --spacing-xl{syntax: ""; inherits: true; initial-value: .9rem;}@property --spacing-xs{syntax: ""; inherits: true; initial-value: .3rem;}@property --spacing-xxl{syntax: ""; inherits: true; initial-value: 1.1rem;}@property --spacing-xxs{syntax: ""; inherits: true; initial-value: .15rem;}@property --font-size-base{syntax: ""; inherits: true; initial-value: 13.5px;}@property --font-size-lg{syntax: ""; inherits: true; initial-value: .85rem;}@property --font-size-md{syntax: ""; inherits: true; initial-value: .76rem;}@property --font-size-px10{syntax: ""; inherits: true; initial-value: 10px;}@property --font-size-px11{syntax: ""; inherits: true; initial-value: 11px;}@property --font-size-px12{syntax: ""; inherits: true; initial-value: 12px;}@property --font-size-px13{syntax: ""; inherits: true; initial-value: 13px;}@property --font-size-px9{syntax: ""; inherits: true; initial-value: 9px;}@property --font-size-rem100{syntax: ""; inherits: true; initial-value: 1rem;}@property --font-size-rem105{syntax: ""; inherits: true; initial-value: 1.05rem;}@property --font-size-rem60{syntax: ""; inherits: true; initial-value: .6rem;}@property --font-size-rem62{syntax: ""; inherits: true; initial-value: .62rem;}@property --font-size-rem65{syntax: ""; inherits: true; initial-value: .65rem;}@property --font-size-rem68{syntax: ""; inherits: true; initial-value: .68rem;}@property --font-size-rem72{syntax: ""; inherits: true; initial-value: .72rem;}@property --font-size-rem74{syntax: ""; inherits: true; initial-value: .74rem;}@property --font-size-rem75{syntax: ""; inherits: true; initial-value: .75rem;}@property --font-size-rem78{syntax: ""; inherits: true; initial-value: .78rem;}@property --font-size-rem80{syntax: ""; inherits: true; initial-value: .8rem;}@property --font-size-rem82{syntax: ""; inherits: true; initial-value: .82rem;}@property --font-size-rem90{syntax: ""; inherits: true; initial-value: .9rem;}@property --font-size-rem95{syntax: ""; inherits: true; initial-value: .95rem;}@property --font-size-sm{syntax: ""; inherits: true; initial-value: .7rem;}@property --font-size-xl{syntax: ""; inherits: true; initial-value: .92rem;}@property --font-size-xs{syntax: ""; inherits: true; initial-value: .66rem;}@property --font-size-xxl{syntax: ""; inherits: true; initial-value: 1.15rem;}@property --font-size-xxs{syntax: ""; inherits: true; initial-value: .55rem;}}@font-face{font-family:JetBrains Mono;font-style:normal;font-weight:100 800;font-display:swap;src:url(data:font/woff2;base64,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)format(woff2)}body{-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.board{font-family:var(--font-sans);color:var(--text-1);width:min(100% - 2rem,96rem);margin-inline-start:auto;margin-inline-end:auto}body{background:var(--page);background-image:linear-gradient(var(--grid-line)1px,transparent 1px),linear-gradient(90deg,var(--grid-line)1px,transparent 1px);background-size:28px 28px;margin:0}.board-header{display:flex;justify-content:space-between;align-items: center}.board-title{display:flex;align-items: center;gap:var(--spacing-md)}.board-logo{display:block;flex:none;width:32px;height:32px}.board-subline{color:var(--text-2);margin:var(--spacing-lg)0 var(--spacing-rem150)}.subline-rest{margin-left:var(--spacing-xl)}.header-actions{display:flex;align-items: center;gap:var(--spacing-xs)}.settings-modal{overflow:hidden;width:min(920px,94vw);max-width:min(920px,94vw)}.settings-frame{display:block;width:calc(100% + var(--spacing-xxl));margin:.6rem calc(-1*var(--spacing-xxl))0 0;background:0 0;border:0;max-height:calc(80vh - 4.5rem)}.mt-6{margin-top:1.5rem}.section-title{display:flex;align-items: center;gap:var(--spacing-sm)}.section-title [data-part=tooltip]{line-height:0}.site-mark{display:inline-flex;width:var(--spacing-rem120);height:var(--spacing-rem120);border-radius:var(--radius-sm);vertical-align:middle;margin-right:var(--spacing-sm);flex:none;justify-content:center;align-items: center}.site-mark-letter{background:var(--surface-wash-fg-8);color:var(--text-4);font-family:var(--font-sans);font-size:var(--font-size-xs);font-weight:500}.tunnel-badge{display:inline-flex;align-items: center;gap:var(--spacing-xs);margin-right:var(--spacing-sm);font:inherit;color:var(--text-2);cursor:pointer;background:0 0;border:none;padding:0}.tunnel-label{font-size:var(--font-size-sm)}.tunnel-badge:hover .tunnel-label{color:var(--text-1)}.unstyled{color:inherit;text-decoration:none}.muted{color:var(--text-4);text-decoration:none}.modal-form .muted{color:var(--text-2)}.t-bad{color:var(--text-bad-vivid)}.t-ok{color:var(--text-ok-vivid)}.t-warn{color:var(--text-warn)}.live-source .live-source-name{font-family:var(--font-mono)}.live-mark{color:var(--text-ok-vivid)}.live-go-mark{color:var(--text-2)}.live-failed{all:unset;cursor:pointer;border-radius:var(--radius-sm,4px)}.live-failed:focus-visible{outline:2px solid var(--text-accent-small);outline-offset:2px}.t-accent{color:var(--text-accent-small)}.live-modal{width:min(30rem,100%);max-width:min(30rem,100%)}.live-modal-title{display:flex;gap:var(--spacing-xxs);flex-direction:column}.live-modal-title .muted{font-weight:400;font-size:var(--font-size-px13);color:var(--text-2)}.live-body{display:flex;gap:var(--spacing-md);padding:var(--spacing-rem150);flex-direction:column}.live-body .muted{color:var(--text-2)}.live-footer{justify-content:flex-end}.live-footer .footer-start{margin-right:auto}.live-help{display:flex;gap:var(--spacing-xxs);flex-direction:column}.live-help:empty{display:none}.live-help p{font-size:var(--font-size-px12);margin:0}.live-modal kbd{font-family:var(--font-mono);font-size:var(--font-size-px12)}.live-detail{display:inline-flex;align-items: center;gap:var(--spacing-sm)}.live-log{padding:var(--spacing-md);background:var(--surface-inset);border:1px solid var(--border-on-card);border-radius:var(--radius-md);color:var(--text-2);font-family:var(--font-mono);font-size:var(--font-size-px12);white-space:pre-wrap;margin:0}.site-name{display:inline-flex;align-items: center;gap:var(--spacing-xxs);vertical-align:middle}.site-name a{font-size:var(--font-size-md)}.cell-tag{margin-left:var(--spacing-sm)}.site-name a.unstyled{transition:color .12s}.site-name a.unstyled:hover{color:var(--text-accent-small)}.site-name a.unstyled:focus-visible{color:var(--text-accent-small)}.public-link{display:inline-flex;margin-left:var(--spacing-sm);border-radius:var(--radius-sm);color:var(--text-accent-small);background:color-mix(in srgb,var(--fill-accent)12%,transparent);justify-content:center;align-items: center;width:24px;height:24px;transition:color .12s,background .12s}.public-link:hover{color:var(--text-accent-small);background:color-mix(in srgb,var(--fill-accent)20%,transparent)}.public-link:focus-visible{color:var(--text-accent-small);background:color-mix(in srgb,var(--fill-accent)20%,transparent)}.preflight-issue{display:flex;gap:var(--spacing-xxs);white-space:normal;flex-direction:column;align-items: flex-start;max-width:22rem}.preflight-issue code{white-space:normal;word-break:break-word;font-size:.75em}.dev-link-input{display:inline-flex;align-items: center;gap:var(--spacing-xs);width:100%}.dev-link-input [data-part=field]{flex:1;min-width:0}.dev-link-input [data-part=field-input]{width:100%}.modal-form{display:flex;gap:var(--spacing-md);flex-direction:column}.modal-footer{display:flex;justify-content:flex-end;gap:var(--spacing-sm);margin-top:var(--spacing-md)}[data-part=table-cell] [data-part=switch]{vertical-align:middle}[data-part=statusdot]{margin-right:var(--spacing-sm)}.row-actions{display:flex;justify-content:flex-end;align-items: center;gap:var(--spacing-xxs)}.row-actions .dev-link-input{flex:1;min-width:0}.row-icon{color:var(--text-2)}.row-actions [data-part=button].t-warn{color:var(--text-warn-vivid)}.row-gear{opacity:0;transition:opacity .12s,border-color .12s,background .12s,color .12s,transform 80ms}@media (hover:none){.row-gear{opacity:1}}@media (hover:hover){.apps-grid [data-part=table-row]:hover .row-gear{opacity:1}}.apps-grid [data-part=table-row]:has(:focus-visible) .row-gear{opacity:1}@media (prefers-reduced-motion:reduce){.row-gear{transition:none}}.version-cell{display:inline-flex;align-items: center;gap:var(--spacing-xs)}.version-sha{font-family:var(--font-mono)}.version-sha.t-warn{color:var(--text-warn-small)}.apps-grid .col-gap{padding-left:calc(var(--spacing-md) + var(--spacing-lg))}.col-ident{font-family:var(--font-mono)}.apps-grid [data-part=table]{table-layout:fixed}.apps-panel{background:var(--card);border:1px solid var(--border-on-card);border-radius:var(--radius-lg);overflow:hidden;--muted:var(--text-muted-on-card);--border:var(--border-on-card);--border-soft:var(--border-soft-on-card)}.apps-grid{padding:2px 0 var(--spacing-sm)}.apps-grid [data-part=table-cell]:first-child,.apps-grid thead:not(.head-hidden) [data-part=table-headcell]:first-child{padding-left:calc(var(--spacing-lg) + var(--spacing-md))}.apps-grid [data-part=table-cell]:last-child,.apps-grid thead:not(.head-hidden) [data-part=table-headcell]:last-child{padding-right:calc(var(--spacing-lg) + var(--spacing-md))}.update-strip{display:flex;align-items: center;gap:var(--spacing-px10);padding:var(--spacing-px14)calc(var(--spacing-lg) + var(--spacing-md));background:var(--surface-wash-amber-7);border-bottom:1px solid var(--border-soft-on-card);font-size:var(--font-size-px13);line-height:var(--line-height-snug)}.update-strip-icon{width:var(--spacing-rem100);height:var(--spacing-rem100);color:var(--text-warn-vivid);flex:none}.update-strip-text{flex:1}.apps-grid [data-part=table-row]:last-child [data-part=table-cell]{border-bottom:none}.apps-grid thead.head-hidden th{color:#0000;overflow:hidden;border:0;height:0;padding:0;font-size:0;line-height:0}.railway-globe{display:inline-flex;margin-left:var(--spacing-xs);align-items: center}.railway-globe.railway-live{color:var(--text-accent-small)}.railway-globe.railway-deploying,.railway-globe.railway-verifying{color:var(--text-warn-small)}.railway-globe.railway-error{color:var(--text-bad-small)}.app-settings-modal{width:min(1000px,100%);max-width:min(1000px,100%)}.app-settings-modal,.live-modal{font-size:var(--font-size-base);padding:0}.app-settings-modal.app-settings-reduced{width:min(640px,100%);max-width:min(640px,100%)}.app-settings-overlay{backdrop-filter:none;padding-top:14.5vh}.app-settings-modal [data-part=button]:not([data-icon-only]),.live-modal [data-part=button]:not([data-icon-only]){padding-block:var(--spacing-px2);font-size:var(--font-size-px13)}.app-settings-modal [data-part=button][data-variant=default],.app-settings-modal [data-part=copybutton],.live-modal [data-part=button][data-variant=default]{background:var(--surface-card)}.app-settings-modal [data-part=button][data-variant=default]:hover:not(:disabled),.live-modal [data-part=button][data-variant=default]:hover:not(:disabled){background:var(--surface-wash-fg-4-card)}.app-settings-modal [data-part=field-input]{background:var(--surface-inset)}.app-settings-modal [data-part=modal-head],.live-modal [data-part=modal-head]{position:sticky;z-index:1;gap:var(--spacing-md);padding:calc(var(--spacing-rem100) + var(--spacing-px4))var(--spacing-rem150);background:var(--panel);border-bottom:1px solid var(--border);top:0}.app-settings-modal [data-part=modal-title],.live-modal [data-part=modal-title]{letter-spacing:normal;color:var(--text-1);flex:1;min-width:0;font-weight:400}.settings-head{display:flex;align-items: center;gap:var(--spacing-lg)}.settings-head .site-mark{width:calc(var(--spacing-rem100)*2.5);height:calc(var(--spacing-rem100)*2.5);border-radius:var(--radius-lg);margin-right:0}.settings-head .site-mark-letter{font-size:calc(var(--font-size-xs)*2)}.settings-identity{display:flex;gap:var(--spacing-xxs);flex-direction:column;min-width:0}.settings-name-line{display:flex;align-items: center;gap:var(--spacing-sm)}.settings-name,.live-modal-title{font-size:calc(var(--font-size-rem100)*1.25);font-weight:700}.settings-url{display:inline-flex;align-items:baseline;gap:var(--spacing-xs);font-family:var(--font-mono);font-size:var(--font-size-px13);color:var(--text-accent);text-decoration:none}.settings-url:hover{text-decoration:underline}.settings-url.muted{color:var(--text-2)}.settings-head-actions{display:flex;align-items: center;gap:var(--spacing-sm);flex:none;margin-left:auto}.settings-pill [data-part=badge]{gap:var(--spacing-md);padding:var(--spacing-xxs)var(--spacing-md);border-radius:var(--radius-pill);font-size:var(--font-size-px13)}.settings-pill-dot{width:var(--spacing-sm);height:var(--spacing-sm);border-radius:var(--radius-round);background:currentColor}.settings-pill[data-tone=ok] .settings-pill-dot{background:var(--dot-ok)}.settings-pill[data-tone=bad] .settings-pill-dot{background:var(--dot-bad)}.settings-pill[data-tone=warn] .settings-pill-dot{background:var(--dot-warn)}.settings-pill-detail{font-family:var(--font-mono)}.settings-issues{display:flex;gap:var(--spacing-sm);padding:var(--spacing-rem150)var(--spacing-rem150)0;flex-direction:column}.settings-issues [data-part=alert]{padding:var(--spacing-md)var(--spacing-lg);border:1px solid color-mix(in srgb,var(--text-bad)45%,transparent);border-radius:var(--radius-lg)}.settings-issue{display:flex;align-items: center;gap:var(--spacing-sm)}.settings-issue [data-part=icon]{color:var(--text-bad-vivid);flex:none}.settings-grid{display:grid;grid-template-columns:1fr 1fr;align-items: start}.settings-col,.settings-stack{display:flex;gap:var(--spacing-rem150);padding:var(--spacing-rem150);flex-direction:column;min-width:0}.settings-col:not(:last-child){border-right:1px solid var(--border)}.app-settings-reduced .settings-grid{grid-template-columns:1fr}@media (max-width:760px){.settings-grid{grid-template-columns:1fr}.settings-col:not(:last-child){border-right:none;border-bottom:1px solid var(--border)}}.settings-block{display:flex;gap:var(--spacing-lg);flex-direction:column;min-width:0}.settings-heading{font-size:var(--font-size-lg);color:var(--text-1);margin:0;font-weight:700}.settings-note{color:var(--text-2);font-size:var(--font-size-px12)}.settings-mono{font-family:var(--font-mono)}.app-settings-modal{--settings-label-col:calc(var(--spacing-rem150)*4.5)}.settings-block-head{display:flex;gap:var(--spacing-xxs);flex-direction:column}.settings-block-head .settings-note{margin:0}.settings-facts{display:grid;grid-template-columns:var(--settings-label-col)minmax(0,1fr);align-items: center;gap:var(--spacing-rem80)0;margin:0}.settings-facts dt{display:flex;align-items: center;gap:var(--spacing-xs);color:var(--text-2)}.settings-facts dd{overflow-wrap:anywhere;min-width:0;margin:0}.settings-deployed{display:flex;align-items: center;gap:var(--spacing-sm);flex-wrap:wrap}.settings-override{display:flex;justify-content:space-between;align-items: center;gap:var(--spacing-sm)}.settings-actions{display:flex;align-items: center;gap:var(--spacing-sm);flex-wrap:wrap}.settings-actions .command-labeled[data-intent=warn]{background:var(--surface-wash-amber-7)}.settings-actions .command-labeled[data-intent=warn]:hover:not(:disabled){background:var(--button-hover)}.settings-live-source{display:flex;gap:var(--spacing-xs);align-items: center}.live-procs{list-style:none;border:1px solid var(--border-on-card);border-radius:var(--radius-lg);margin:0;padding:0}.live-proc{display:flex;align-items: center;gap:var(--spacing-md);padding:var(--spacing-sm)var(--spacing-md)}.live-proc-item+.live-proc-item{border-top:1px solid var(--border-on-card)}.live-proc-log{margin:0 var(--spacing-md)var(--spacing-md);overflow:auto;white-space:pre-wrap;max-height:12rem}.live-proc-kind{width:3rem;font-weight:500}.live-proc-cmd{font-size:var(--font-size-px11);overflow:hidden;text-overflow:ellipsis;white-space:nowrap;color:var(--text-2);flex:1;min-width:0}.settings-actions-end{display:flex;gap:var(--spacing-xs);margin-left:auto}.settings-inline-form{display:flex;align-items: flex-start;gap:var(--spacing-sm)}.settings-inline-form [data-part=field]{width:calc(var(--spacing-rem150)*5)}.settings-toggle-row{display:flex;justify-content:space-between;align-items: center;gap:var(--spacing-lg)}.settings-toggle-text{display:flex;gap:var(--spacing-xxs);flex-direction:column;min-width:0}.settings-toggle-label{display:inline-flex;align-items: center;gap:var(--spacing-xs);color:var(--text-1)}.settings-form{display:grid;grid-template-columns:var(--settings-label-col)minmax(0,1fr);gap:var(--spacing-sm)0}.settings-form [data-part=field]{display:grid;grid-column:1/-1;grid-template-columns:subgrid;align-items: center;row-gap:var(--spacing-xs);font-size:inherit}.settings-form [data-part=field-label]{color:var(--text-2);font-size:inherit}.settings-form [data-part=field-input]{font-family:var(--font-mono)}.settings-form [data-part=field-error]{grid-column:2}.settings-form-actions{display:flex;grid-column:1/-1;justify-content:flex-end}.settings-block [data-part=field]{font-size:inherit}.settings-block [data-part=field-input]{padding:var(--spacing-px4)var(--spacing-sm)}.settings-link-input [data-part=field-input]{font-family:var(--font-mono)}.settings-help{display:inline-flex;vertical-align:middle}.settings-help-icon{display:inline-flex;color:var(--text-2);font:inherit;cursor:help;border-radius:var(--radius-round);background:0 0;border:none;margin:0;padding:0}.settings-help-icon:focus-visible{outline:var(--spacing-px2)solid var(--fill-accent);outline-offset:var(--spacing-px2)}.settings-errors-empty,.settings-errors-tail{background:var(--bg);border:1px solid var(--border);border-radius:var(--radius-lg);margin:0}.settings-errors-empty{display:flex;justify-content:center;align-items: center;gap:var(--spacing-sm);padding:var(--spacing-rem175)var(--spacing-xl);color:var(--text-2);font-size:var(--font-size-md);text-align:center}.settings-errors-tail{padding:var(--spacing-md)var(--spacing-lg);color:var(--text-bad);font-family:var(--font-mono);font-size:var(--font-size-px12);line-height:var(--line-height-base);white-space:pre-wrap;word-break:break-word;overflow-y:auto;max-height:240px}.settings-errors-foot{display:flex;justify-content:space-between;align-items: center;gap:var(--spacing-sm)}.settings-errors-foot [data-part=copybutton]{gap:var(--spacing-xs);color:var(--text-1);font:inherit;font-size:var(--font-size-px13)}.settings-list{display:flex;list-style:none;background:var(--card);border:1px solid var(--border);border-radius:var(--radius-lg);flex-direction:column;margin:0;padding:0}.settings-item{display:grid;grid-template-columns:auto minmax(0,1fr)auto;align-items: center;gap:var(--spacing-sm)var(--spacing-md);padding:var(--spacing-px10)var(--spacing-lg)}.settings-item+.settings-item{border-top:1px solid var(--border-on-card)}.settings-item-icon{display:inline-flex;color:var(--text-2)}.settings-item-text{display:flex;gap:var(--spacing-xxs);flex-direction:column;min-width:0}.settings-item-end{display:flex;justify-content:flex-end;align-items: center}.settings-item-actions{display:flex;align-items: center;gap:var(--spacing-xs)}.settings-item-body{display:flex;align-items: flex-start;gap:var(--spacing-sm);grid-column:2/-1;flex-direction:column;min-width:0}.settings-item-body>[data-part=alert],.settings-grow{align-self:stretch}.settings-grow [data-part=field]{flex:1;width:auto}.settings-remote{display:flex;justify-content:space-between;align-items: center;gap:var(--spacing-sm);align-self:stretch}.settings-remote-status{display:inline-flex;align-items: center;gap:var(--spacing-xs);font-size:var(--font-size-px13)}.settings-remote-status[data-tone=ok]{color:var(--text-ok)}.settings-remote-status[data-tone=ok] .settings-pill-dot{background:var(--dot-ok)}.settings-remote-status[data-tone=warn]{color:var(--text-warn)}.settings-remote-status[data-tone=warn] .settings-pill-dot{background:var(--dot-warn)}.settings-remote-status[data-tone=bad]{color:var(--text-bad)}.settings-remote-status[data-tone=bad] .settings-pill-dot{background:var(--dot-bad)}.settings-item-body [data-part=segmented-option]{padding:var(--spacing-xs)var(--spacing-md);font-size:var(--font-size-px13);color:var(--text-2)}.settings-item-body [data-part=segmented-option][data-active]{background:var(--surface-wash-fg-8);color:var(--text-1)}.settings-chips{display:flex;gap:var(--spacing-xs);flex-wrap:wrap}.settings-chip{gap:var(--spacing-xs);padding:var(--spacing-xxs)var(--spacing-sm);border-radius:var(--radius-md);font-family:inherit;font-size:var(--font-size-px12);color:var(--text-1)}.settings-chip-remove{display:inline-flex;color:var(--text-2);cursor:pointer;border-radius:var(--radius-round);background:0 0;border:none;padding:0}.settings-chip-remove [data-part=icon]{width:var(--spacing-px12);height:var(--spacing-px12)}.settings-chip-remove:hover{color:var(--text-1)}.settings-chip-remove:focus-visible{outline:var(--spacing-px2)solid var(--fill-accent);outline-offset:1px}.settings-open-note{display:flex;align-items: flex-start;gap:var(--spacing-sm);padding:var(--spacing-md)var(--spacing-lg);background:var(--surface-wash-amber-7);border:1px solid color-mix(in srgb,var(--text-warn)45%,transparent);border-radius:var(--radius-lg);color:var(--text-1);font-size:var(--font-size-px12);margin:0}.settings-open-note [data-part=icon]{color:var(--text-warn-vivid);flex:none}.settings-footer{position:sticky;display:flex;align-items: center;gap:var(--spacing-md);padding:var(--spacing-lg)var(--spacing-rem150);background:var(--card);border-top:1px solid var(--border);bottom:0}.settings-footer-note{margin-left:auto}@layer soribashi.recipes{.root_jT5pOw{display:flex;gap:var(--spacing-xs);color:var(--text-1);background:var(--sb-alert-bg);border-left:2px solid var(--sb-alert-color);border-radius:var(--radius-md);padding:var(--spacing-sm)var(--spacing-lg);flex-direction:column}.command_jT5pOw{font-family:var(--font-mono);font-size:var(--font-size-xs);background:var(--panel);padding:var(--spacing-sm);border-radius:var(--radius-md);overflow-x:auto;white-space:pre-wrap;margin:0}}@layer soribashi.recipes{.root_so3lDg{display:inline-flex;align-items: center;gap:var(--spacing-xxs);font-family:var(--font-sans);font-size:var(--font-size-xs);line-height:var(--line-height-base);font-variant-numeric:var(--font-numeric);color:var(--sb-badge-color);background:var(--sb-badge-bg);border:1px solid var(--sb-badge-border);border-radius:var(--radius-pill);padding:0 var(--spacing-sm);white-space:nowrap}.root_so3lDg>[data-part=spinner]{margin-inline-end:var(--spacing-xxs)}}@layer soribashi.recipes{.root_0gPzkQ{display:inline-block;width:round(var(--sb-spinner-size),2px);height:round(var(--sb-spinner-size),2px);will-change:transform;box-sizing:border-box;aspect-ratio:1;vertical-align:middle;flex-shrink:0}.root_0gPzkQ svg{display:block;overflow:visible;width:100%;height:100%}}@layer soribashi.recipes{[data-part=spinner]{animation:1s linear infinite sb-spinner-spin}@keyframes sb-spinner-spin{to{transform:rotate(360deg)}}}@layer soribashi.recipes{.root_30t2rQ{display:inline-flex;justify-content:center;align-items: center;gap:var(--spacing-xs);font-family:var(--font-sans);font-size:var(--font-size-sm);line-height:var(--line-height-base);border:1px solid var(--button-border);border-radius:var(--radius-md);padding:var(--spacing-xs)var(--spacing-md);cursor:pointer;background:var(--button-bg);color:var(--button-color);transition:border-color .12s,background .12s,color .12s,transform 80ms}.root_30t2rQ:hover:not(:disabled){background:var(--button-hover)}.root_30t2rQ:focus-visible{outline:1px solid var(--fill-accent);outline-offset:1px}.root_30t2rQ:disabled{opacity:.55;cursor:default}.root_30t2rQ:active:not(:disabled){transform:translateY(1px)}.root_30t2rQ[hidden]{display:none}.root_30t2rQ[data-size=sm]{font-size:var(--font-size-xs);padding:var(--spacing-xxs)var(--spacing-sm)}.root_30t2rQ[data-size=lg]{font-size:var(--font-size-rem80);padding:var(--spacing-px5)var(--spacing-px12)}.root_30t2rQ[data-variant=filled]{font-weight:500}.root_30t2rQ[data-icon-only]{padding:var(--spacing-xs);aspect-ratio:1;min-width:var(--spacing-rem175);min-height:var(--spacing-rem175)}.root_30t2rQ[data-variant=default][data-intent=bad]{color:var(--sb-button-bad-color);border-color:var(--fill-bad)}}@layer soribashi.recipes{.root_TGYYvw{display:inline-flex;align-items: center;gap:var(--spacing-px4);padding:1px var(--spacing-px6);border:1px solid var(--border);border-radius:var(--radius-sm);font-size:var(--font-size-px11);white-space:nowrap;background:var(--chip-bg,transparent);color:var(--chip-color,inherit);font-family:inherit;font-weight:inherit;line-height:inherit;text-align:inherit;text-decoration:none;flex-shrink:0;margin:0}.root_TGYYvw[data-variant=outline]{border-color:var(--chip-border,currentColor)}.root_TGYYvw[data-dimmed]{opacity:var(--sb-chip-dimmed-opacity)}.root_TGYYvw[data-uppercase]{text-transform:uppercase;letter-spacing:var(--sb-chip-uppercase-tracking);font-size:var(--font-size-xxs);font-weight:500}@supports (text-box: trim-both cap alphabetic){.root_TGYYvw[data-uppercase]{padding-block:var(--spacing-px4)}.root_TGYYvw[data-uppercase] .label_TGYYvw{text-box:trim-both cap alphabetic}}.root_TGYYvw:-webkit-any(button,a){cursor:pointer}.root_TGYYvw:-moz-any(button,a){cursor:pointer}.root_TGYYvw:is(button,a){cursor:pointer}.root_TGYYvw:-webkit-any(button,a):hover{background:var(--chip-hover,var(--card))}.root_TGYYvw:-moz-any(button,a):hover{background:var(--chip-hover,var(--card))}.root_TGYYvw:is(button,a):hover{background:var(--chip-hover,var(--card))}.root_TGYYvw:-webkit-any(button,a):focus-visible{outline:2px solid var(--fill-accent);outline-offset:2px}.root_TGYYvw:-moz-any(button,a):focus-visible{outline:2px solid var(--fill-accent);outline-offset:2px}.root_TGYYvw:is(button,a):focus-visible{outline:2px solid var(--fill-accent);outline-offset:2px}.icon_TGYYvw{display:inline-flex;flex-shrink:0;align-items: center;line-height:1}}@layer soribashi.recipes{[data-part=chip][data-pulse]{animation-name:chip-pulse;animation-duration:var(--sb-chip-pulse-period);animation-timing-function:ease-in-out;animation-iteration-count:infinite}@media (prefers-reduced-motion:reduce){[data-part=chip][data-pulse]{animation-name:none}}@keyframes chip-pulse{0%,to{opacity:1}50%{opacity:.5}}}@layer soribashi.recipes{.root_Puve9g{flex-shrink:0}}@layer soribashi.recipes{.overlay_gUyvOg{position:fixed;z-index:100;display:flex;padding:var(--sb-modal-overlay-pad-y)var(--spacing-rem100);background:var(--surface-wash-bg-55);backdrop-filter:blur(2px);justify-content:center;align-items: flex-start;inset:0}.root_gUyvOg{box-sizing:border-box;max-width:var(--sb-modal-max-w);max-height:var(--sb-modal-max-h);overflow-y:auto;border:1px solid var(--border);border-radius:var(--radius-xl);background:var(--panel);padding:var(--spacing-rem100)var(--spacing-xxl)var(--spacing-rem120);box-shadow:var(--shadow-modal);width:100%}.head_gUyvOg{display:flex;justify-content:space-between;align-items: center}.title_gUyvOg{letter-spacing:var(--sb-modal-title-tracking);color:var(--text-accent-small);font-weight:700}.close_gUyvOg{color:var(--text-2);cursor:pointer;font-size:var(--font-size-rem90);padding:var(--spacing-rem20)var(--spacing-rem40);border-radius:var(--radius-px5);background:0 0;border:none}.close_gUyvOg:hover{color:var(--text-1);background:var(--card)}.close_gUyvOg:focus-visible{outline:2px solid var(--fill-accent);outline-offset:2px}}@layer soribashi.recipes{.body_-s5fvg{padding:var(--spacing-md)0;font-size:var(--font-size-sm);color:var(--text-1)}.foot_-s5fvg{display:flex;justify-content:flex-end;gap:var(--spacing-sm)}}@layer soribashi.recipes{.positioner_TfyK5Q{z-index:200}.root_TfyK5Q{box-sizing:border-box;min-width:var(--sb-contextmenu-min-w);padding:var(--spacing-px4);background:var(--card);border:1px solid var(--border);border-radius:var(--radius-lg);box-shadow:var(--shadow-menu);font-size:var(--font-size-px13);outline:none}.label_TfyK5Q{padding:var(--spacing-px5)var(--spacing-px8)var(--spacing-px6);font-family:var(--font-sans);font-size:var(--font-size-px11);color:var(--text-4);border-bottom:1px solid var(--border-soft);margin-bottom:var(--spacing-px4)}.item_TfyK5Q{box-sizing:border-box;display:flex;justify-content:space-between;align-items: center;gap:var(--spacing-px12);padding:var(--spacing-px7)var(--spacing-px8);border-radius:var(--radius-px5);color:var(--text-1);font:inherit;text-align:left;cursor:pointer;outline:none;background:0 0;border:none;width:100%}.item_TfyK5Q[data-highlighted]:not([data-disabled]),.item_TfyK5Q[data-popup-open]{background:var(--surface-wash-accent-16)}.item_TfyK5Q[data-disabled]:focus-visible:not(:hover){background:var(--surface-wash-accent-16)}.item_TfyK5Q[data-disabled]{color:var(--muted);cursor:not-allowed}.hint_TfyK5Q{font-family:var(--font-sans);font-size:var(--font-size-px10);color:var(--text-4)}.item_TfyK5Q[data-disabled] .hint_TfyK5Q{font-style:italic}.item_TfyK5Q[data-disabled] .chevron_TfyK5Q{color:inherit}.separator_TfyK5Q{box-sizing:border-box;border-top:1px solid var(--border-soft);margin:var(--spacing-px4)var(--spacing-px2);height:1px}.submenu_TfyK5Q{box-sizing:border-box;min-width:var(--sb-contextmenu-min-w);padding:var(--spacing-px4);background:var(--card);border:1px solid var(--border);border-radius:var(--radius-lg);box-shadow:var(--shadow-menu);font-size:var(--font-size-px13);outline:none}.chevron_TfyK5Q{display:flex;color:var(--text-4)}.row_TfyK5Q{display:flex;gap:var(--spacing-px4)}.row_TfyK5Q>.item_TfyK5Q{flex:1;justify-content:center;width:auto}}@layer soribashi.recipes{[data-part=contextmenu]{animation:90ms ease-out contextmenu-in}@keyframes contextmenu-in{0%{opacity:0;transform:scale(.97)translateY(-2px)}to{opacity:1;transform:none}}}@layer soribashi.recipes{.root_V4wrdw{display:inline-flex;padding:var(--spacing-xs)var(--spacing-sm);cursor:pointer;border-radius:var(--radius-md);background:var(--panel);border:1px solid var(--border);color:var(--text-3);justify-content:center;align-items: center}.root_V4wrdw:hover{color:var(--text-1);border-color:var(--fill-accent)}.root_V4wrdw[data-copied]{color:var(--text-ok-small);border-color:var(--fill-ok)}.root_V4wrdw:focus-visible{outline:2px solid var(--fill-accent);outline-offset:2px}}@layer soribashi.recipes{.overlay_QCiRPg{position:fixed;display:flex;background:var(--surface-wash-bg-55);backdrop-filter:blur(2px);inset:0}.overlay_QCiRPg[data-side=right]{z-index:100;cursor:default;white-space:normal;justify-content:flex-end}.overlay_QCiRPg[data-side=left]{z-index:90}.root_QCiRPg{box-sizing:border-box;width:var(--sb-sidedrawer-w);overflow-y:auto;background:var(--panel);display:flex;flex-direction:column;height:100%}.root_QCiRPg[data-side=right]{border-left:1px solid var(--border);box-shadow:var(--shadow-drawer)}.root_QCiRPg[data-side=left]{border-right:1px solid var(--border);padding:var(--spacing-px14);gap:var(--spacing-px14);box-shadow:var(--shadow-drawer-left)}}@layer soribashi.recipes{[data-part=sidedrawer]{animation:.16s ease-out sidedrawer-in}[data-part=sidedrawer][data-side=right]{--sb-sidedrawer-slide-x:100%}[data-part=sidedrawer][data-side=left]{--sb-sidedrawer-slide-x:-100%}[data-part=sidedrawer][data-closing]{animation:.12s ease-in forwards sidedrawer-out}@keyframes sidedrawer-in{0%{transform:translateX(var(--sb-sidedrawer-slide-x))}to{transform:none}}@keyframes sidedrawer-out{to{transform:translateX(var(--sb-sidedrawer-slide-x))}}@media (prefers-reduced-motion:reduce){[data-part=sidedrawer],[data-part=sidedrawer][data-closing]{animation:none}}}@layer soribashi.recipes{.frame_Z0A9GA{display:flex;flex-direction:column;flex:auto;min-height:0}.nav_Z0A9GA{display:flex;gap:var(--spacing-rem40);padding:var(--spacing-rem75)var(--spacing-rem100)var(--spacing-rem50);border-bottom:1px solid var(--border);flex-direction:column;flex-shrink:0}.back_Z0A9GA{color:var(--text-4);cursor:pointer;font:inherit;font-size:var(--font-size-sm);background:0 0;border:none;align-self: flex-start;padding:0}.back_Z0A9GA:hover{text-decoration:underline}.back_Z0A9GA:focus-visible{outline:2px solid var(--fill-accent);outline-offset:2px}.navRow_Z0A9GA{display:flex;justify-content:space-between;align-items: center;gap:var(--spacing-rem50)}.title_Z0A9GA{font-weight:700;font-size:var(--font-size-xl);color:var(--text-1);overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.navActions_Z0A9GA{display:flex;align-items: center;gap:var(--spacing-rem40);flex-shrink:0}.navAction_Z0A9GA{color:var(--text-accent-small);cursor:pointer;font:inherit;padding:var(--spacing-rem20)var(--spacing-rem40);border-radius:var(--radius-px5);background:0 0;border:none;font-weight:500}.navAction_Z0A9GA:hover:not(:disabled){background:var(--surface-wash-accent-14)}.navAction_Z0A9GA:focus-visible{outline:2px solid var(--fill-accent);outline-offset:2px}.navAction_Z0A9GA:disabled{opacity:.55;cursor:default}.close_Z0A9GA{color:var(--text-2);cursor:pointer;font-size:var(--font-size-rem90);padding:var(--spacing-rem20)var(--spacing-rem40);border-radius:var(--radius-px5);background:0 0;border:none}.close_Z0A9GA:hover{color:var(--text-1);background:var(--card)}.close_Z0A9GA:focus-visible{outline:2px solid var(--fill-accent);outline-offset:2px}.header_Z0A9GA{padding:var(--spacing-rem50)var(--spacing-rem100);border-bottom:1px solid var(--border-soft);flex-shrink:0}.content_Z0A9GA{overflow-y:auto;padding:var(--spacing-rem100);flex:auto;min-height:0}}@layer soribashi.recipes{.root_ricmaA{display:flex;gap:var(--spacing-sm);font-family:var(--font-sans);font-size:var(--font-size-sm);color:var(--text-1);flex-direction:column}.label_ricmaA{color:var(--text-4);font-size:var(--font-size-xs)}.input_ricmaA{background:var(--panel);color:var(--text-1);border:1px solid var(--border);border-radius:var(--radius-md);padding:var(--spacing-sm);font-family:inherit;font-size:inherit}.input_ricmaA:focus-visible{outline:1px solid var(--fill-accent);outline-offset:1px}.input_ricmaA:where(textarea){resize:vertical}.error_ricmaA{color:var(--text-bad-small);font-size:var(--font-size-xs)}.option_ricmaA{display:flex;gap:var(--spacing-sm);align-items: center}}@layer soribashi.recipes{.root_RnlAng{display:inline-flex;align-items: center;gap:var(--spacing-sm);font-family:var(--font-sans);font-size:var(--font-size-sm);color:var(--text-1);cursor:pointer}.control_RnlAng{appearance:none;width:var(--spacing-rem180);height:var(--spacing-rem100);border:1px solid var(--border);border-radius:var(--radius-pill);background:var(--sb-switch-bg-off);position:relative;cursor:pointer;margin:0;transition:background .12s,border-color .12s}.control_RnlAng:after{content:"";position:absolute;top:calc((var(--spacing-rem100) - var(--spacing-rem75) - var(--spacing-px2))/2);left:var(--spacing-px2);width:var(--spacing-rem75);height:var(--spacing-rem75);border-radius:var(--radius-round);background:var(--muted);transition:left .12s,background .12s}.control_RnlAng:checked{background:var(--sb-switch-bg-on);border-color:var(--fill-accent)}.control_RnlAng:checked:after{left:calc(100% - var(--spacing-rem75) - var(--spacing-px2));background:var(--fill-accent)}.control_RnlAng:focus-visible{outline:1px solid var(--fill-accent);outline-offset:1px}.control_RnlAng:disabled{opacity:.55;cursor:default}}@layer soribashi.recipes{.root_yG8YKA{border:1px solid var(--border);border-radius:var(--radius-lg);background:var(--panel);overflow:hidden;font-family:var(--font-sans);font-size:var(--font-size-sm);color:var(--text-1)}.list_yG8YKA{list-style:none;margin:0;padding:0}.footer_yG8YKA{padding:var(--spacing-sm)var(--spacing-md);color:var(--text-4);font-size:var(--font-size-xs);margin:0}.item_yG8YKA{border-bottom:1px solid var(--border-soft)}.item_yG8YKA:last-child{border-bottom:none}.label_yG8YKA{min-width:0}.value_yG8YKA{color:var(--text-4);font-variant-numeric:var(--font-numeric)}.trailing_yG8YKA{display:inline-flex;align-items: center;gap:var(--spacing-xs)}.chevron_yG8YKA{color:var(--text-4)}.navButton_yG8YKA{display:flex;justify-content:space-between;align-items: center;gap:var(--spacing-sm);padding:var(--spacing-sm)var(--spacing-md);font:inherit;color:inherit;text-align:left;cursor:pointer;background:0 0;border:none;width:100%;margin:0;transition:background .12s}.actionButton_yG8YKA{display:flex;justify-content:flex-start;align-items: center;gap:var(--spacing-sm);padding:var(--spacing-sm)var(--spacing-md);font:inherit;color:inherit;text-align:left;cursor:pointer;background:0 0;border:none;width:100%;margin:0;transition:background .12s}.actionButton_yG8YKA[data-intent=accent]{color:var(--text-accent-small)}.actionButton_yG8YKA[data-intent=bad]{color:var(--text-bad-small)}.actionButton_yG8YKA[data-centered]{text-align:center;justify-content:center}.navButton_yG8YKA:hover:not(:disabled),.actionButton_yG8YKA:hover:not(:disabled){background:var(--surface-wash-fg-5)}.navButton_yG8YKA:focus-visible{outline:1px solid var(--fill-accent);outline-offset:1px}.actionButton_yG8YKA:focus-visible{outline:1px solid var(--fill-accent);outline-offset:1px}.navButton_yG8YKA:disabled,.actionButton_yG8YKA:disabled{opacity:.55;cursor:default}@media (prefers-reduced-motion:reduce){.navButton_yG8YKA,.actionButton_yG8YKA{transition:none}}.toggleRow_yG8YKA,.factRow_yG8YKA{display:flex;justify-content:space-between;align-items: center;gap:var(--spacing-sm);padding:var(--spacing-sm)var(--spacing-md)}.inputRow_yG8YKA{padding:var(--spacing-sm)var(--spacing-md)}}@layer soribashi.recipes{.root_QopY9g{font-family:var(--font-sans);font-size:var(--font-size-rem95);color:var(--text-1);overflow-wrap:anywhere;line-height:1.6}.root_QopY9g h1,.root_QopY9g h2,.root_QopY9g h3,.root_QopY9g h4{margin:var(--sb-markdown-heading-margin-top)0 var(--sb-markdown-heading-margin-bottom);font-weight:700;line-height:1.25}.root_QopY9g h1{font-size:var(--sb-markdown-h1-size);padding-bottom:var(--sb-markdown-heading-underline-pad);border-bottom:1px solid var(--border)}.root_QopY9g h2{font-size:var(--sb-markdown-h2-size);padding-bottom:var(--sb-markdown-heading-underline-pad);border-bottom:1px solid var(--border)}.root_QopY9g h3{font-size:var(--sb-markdown-h3-size)}.root_QopY9g h4{font-size:var(--sb-markdown-h4-size)}.root_QopY9g h1:first-child,.root_QopY9g h2:first-child,.root_QopY9g h3:first-child{margin-top:0}.root_QopY9g p,.root_QopY9g ul,.root_QopY9g ol{margin:var(--sb-markdown-block-margin)0}.root_QopY9g ul,.root_QopY9g ol{padding-left:var(--sb-markdown-list-indent)}.root_QopY9g li{margin:var(--sb-markdown-list-item-margin)0}.root_QopY9g a{color:var(--text-accent)}.root_QopY9g code{font-family:var(--font-mono);font-size:var(--sb-markdown-code-size);background:var(--card);padding:var(--sb-markdown-code-pad-block)var(--sb-markdown-code-pad-inline);border-radius:var(--radius-sm)}.root_QopY9g pre{background:var(--card);border:1px solid var(--border);border-radius:var(--radius-lg);padding:var(--spacing-lg)var(--spacing-xl);overflow-x:auto;margin:var(--sb-markdown-pre-margin)0}.root_QopY9g pre code{background:0 0;padding:0}.root_QopY9g blockquote{margin:var(--sb-markdown-blockquote-margin)0;padding:var(--sb-markdown-blockquote-pad-block)var(--sb-markdown-blockquote-pad-inline);color:var(--text-2);border-left:var(--sb-markdown-blockquote-border-w)solid var(--border)}.root_QopY9g table{border-collapse:collapse;margin:var(--sb-markdown-table-margin)0;display:block;overflow-x:auto}.root_QopY9g th,.root_QopY9g td{border:1px solid var(--border);padding:var(--sb-markdown-cell-pad-block)var(--sb-markdown-cell-pad-inline);text-align:left}.root_QopY9g hr{border:none;border-top:1px solid var(--border);margin:var(--sb-markdown-hr-margin)0}}@layer soribashi.recipes{.root_EZIvCA{position:relative;border:1px solid var(--border);border-radius:var(--radius-lg);background:var(--surface-wash-panel-88);padding:var(--spacing-xxl)0 var(--spacing-xs);margin:var(--spacing-rem150)0 var(--spacing-rem180)}.title_EZIvCA{position:absolute;top:var(--panel-title-offset);left:var(--spacing-xl);background:var(--page);padding:0 var(--spacing-rem50);font-size:var(--font-size-rem74);letter-spacing:var(--panel-title-tracking);color:var(--text-accent-small);cursor:pointer;display:inline-flex;align-items: center;gap:var(--panel-title-gap);border:0;font-family:inherit;font-weight:700}.title_EZIvCA:hover{text-decoration:underline}.title_EZIvCA:focus-visible{outline:2px solid var(--fill-accent);outline-offset:2px;border-radius:var(--radius-px2)}.caret_EZIvCA{display:inline-flex;opacity:.7;align-items: center;transition:transform .12s}.caret_EZIvCA svg{width:var(--spacing-px12);height:var(--spacing-px12)}.root_EZIvCA:not([data-collapsed]) .caret_EZIvCA{transform:rotate(90deg)}.count_EZIvCA{color:var(--text-4);font-weight:400}.root_EZIvCA[data-collapsed]{margin:var(--spacing-rem75)0;background:0 0;border:0;padding:0}.root_EZIvCA[data-collapsed] .title_EZIvCA{box-sizing:border-box;position:static;display:flex;text-align:left;padding:var(--spacing-rem40)var(--spacing-xl);background:var(--surface-wash-panel-70);border:1px solid var(--border);border-radius:var(--radius-md);width:100%}.root_EZIvCA[data-collapsed] .title_EZIvCA:hover{background:var(--surface-wash-panel-100)}}@layer soribashi.recipes{.root_EgffBA{position:fixed;z-index:200;width:0;height:0;top:0;left:0}.popup_EgffBA{box-sizing:border-box;max-width:var(--available-width);overflow:auto;color:var(--text-1);font-family:var(--font-sans);padding:var(--spacing-px4);background:var(--card);border:1px solid var(--border);border-radius:var(--radius-lg);box-shadow:var(--shadow-menu);font-size:var(--font-size-px13);outline:none;transform-origin:var(--transform-origin);width:max-content;transition:opacity .15s,transform .15s}.popup_EgffBA[data-starting-style],.popup_EgffBA[data-ending-style]{opacity:0;transform:scale(.96)}@media (prefers-reduced-motion:reduce){.popup_EgffBA{transition:none}}}@layer soribashi.recipes{.root_fWmRbg{display:flex;max-height:var(--sb-scrollpane-max);overflow:hidden;box-sizing:border-box;border:1px solid var(--border-soft);border-radius:var(--radius-xl);background:var(--card);padding:var(--spacing-rem100)var(--spacing-xxl)var(--spacing-xxl);font-family:var(--font-sans);font-size:var(--type-meta);flex-direction:column;min-width:0;min-height:0}.head_fWmRbg{margin:calc(-1*var(--spacing-rem100))calc(-1*var(--spacing-xxl))var(--spacing-rem55);padding:var(--spacing-md)var(--spacing-xxl);font-size:var(--type-title);font-weight:500;line-height:var(--line-height-snug);color:var(--text-accent);background:var(--surface-wash-fg-4-card);border-bottom:1px solid var(--border-soft);border-radius:calc(var(--radius-xl) - 1px)calc(var(--radius-xl) - 1px)0 0;flex-shrink:0}.body_fWmRbg{overflow-y:auto;margin-right:calc(-1*var(--spacing-xxl));padding-right:var(--spacing-xxl);scrollbar-width:thin;scrollbar-color:var(--border-on-card)transparent;min-height:0}}@layer soribashi.recipes{.root_1GCN5Q{display:flex;gap:var(--spacing-px6);flex-direction:column}.label_1GCN5Q{color:var(--text-1);font-family:var(--font-sans);font-size:var(--font-size-px13);font-weight:500}.trigger_1GCN5Q{box-sizing:border-box;display:flex;align-items: center;gap:var(--spacing-px6);padding:var(--spacing-px6)var(--spacing-px10);background:var(--card);color:var(--text-1);border:1px solid var(--border-control);border-radius:var(--radius-md);font-family:var(--font-mono);font-size:var(--font-size-px13);cursor:default;width:100%}.trigger_1GCN5Q[data-popup-open]{border-color:var(--fill-accent)}.value_1GCN5Q{text-align:left;flex:1}.chevron_1GCN5Q{color:var(--text-2);display:flex}.portal_1GCN5Q{position:fixed;z-index:300;width:0;height:0;top:0;left:0}.popup_1GCN5Q{box-sizing:border-box;width:var(--anchor-width);max-height:min(var(--sb-searchselect-max-h),var(--available-height));display:flex;background:var(--card);color:var(--text-1);border:1px solid var(--border);border-radius:var(--radius-lg);box-shadow:var(--shadow-menu);overflow:hidden;outline:none;flex-direction:column}.searchBox_1GCN5Q{box-sizing:border-box;display:flex;align-items: center;gap:var(--spacing-px8);margin:var(--spacing-px8);padding:var(--spacing-px6)var(--spacing-px10);background:var(--inset);border:1px solid var(--border-control);border-radius:var(--radius-md)}.searchBox_1GCN5Q:focus-within{outline:1px solid var(--fill-accent);outline-offset:1px}.searchIcon_1GCN5Q{color:var(--text-2);display:flex}.search_1GCN5Q{color:var(--text-1);font-family:var(--font-sans);font-size:var(--font-size-px13);outline:none;background:0 0;border:none;flex:1;min-width:0;margin:0;padding:0}.list_1GCN5Q{overflow-y:auto;overscroll-behavior:contain;padding-bottom:var(--spacing-px4)}.groupLabel_1GCN5Q{padding:var(--spacing-px8)var(--spacing-px14)var(--spacing-px4);color:var(--text-2);font-family:var(--font-sans);font-size:var(--font-size-px11);letter-spacing:var(--sb-searchselect-tracking);text-transform:uppercase;font-weight:500}.item_1GCN5Q{display:flex;align-items: center;gap:var(--spacing-px8);min-height:var(--sb-searchselect-item-h);padding:0 var(--spacing-px14);font-family:var(--font-mono);font-size:var(--font-size-px13);cursor:default;outline:none}.item_1GCN5Q[data-highlighted]{background:var(--sb-searchselect-hover)}.itemIcon_1GCN5Q{color:var(--text-2);display:flex}.itemLabel_1GCN5Q{flex:1}.itemDetail_1GCN5Q{color:var(--text-2);font-family:var(--font-sans);font-size:var(--font-size-px12)}.check_1GCN5Q{color:var(--text-accent);display:flex}.empty_1GCN5Q{padding:var(--spacing-px10)var(--spacing-px14);color:var(--text-2);font-size:var(--font-size-px13)}.empty_1GCN5Q:empty{padding:0}.footer_1GCN5Q{display:flex;gap:var(--spacing-px14);padding:var(--spacing-px8)var(--spacing-px14);background:var(--panel);border-top:1px solid var(--line-3);color:var(--text-2);font-family:var(--font-sans);font-size:var(--font-size-px12)}}@layer soribashi.recipes{.root_AuavJw{display:inline-flex;border:1px solid var(--border);border-radius:var(--radius-md);overflow:hidden;background:var(--panel)}.option_AuavJw{display:inline-flex;padding:var(--spacing-xs)var(--spacing-sm);cursor:pointer;color:var(--text-3);background:0 0;border:none;justify-content:center;align-items: center;font-weight:400}.option_AuavJw+.option_AuavJw{border-left:1px solid var(--border)}.option_AuavJw[data-active]{background:var(--fill-accent);color:var(--on-fill-accent);font-weight:500}.option_AuavJw:not([data-active]):hover{color:var(--text-1)}.option_AuavJw:focus-visible{outline:2px solid var(--fill-accent);outline-offset:2px}.root_AuavJw[data-text] .option_AuavJw{padding:var(--spacing-xs)var(--spacing-rem55);font-size:var(--sb-segmented-text-size)}.root_AuavJw[data-soft]{display:flex;gap:var(--spacing-px2);padding:var(--spacing-px2);background:var(--surface-inset);border:none}.root_AuavJw[data-soft] .option_AuavJw{border-radius:var(--radius-px5);flex:1}.root_AuavJw[data-soft] .option_AuavJw+.option_AuavJw{border-left:none}.root_AuavJw[data-soft] .option_AuavJw[data-active]{background:var(--card);color:var(--text-1);box-shadow:var(--shadow-card)}}@layer soribashi.recipes{.root_HD_UaQ{display:inline-flex;width:var(--sb-selectbox-size);height:var(--sb-selectbox-size);font-family:inherit;font-weight:inherit;font-size:var(--font-size-rem105);cursor:pointer;border-radius:var(--radius-px5);color:var(--text-2);opacity:.5;background:0 0;border:none;flex:none;justify-content:center;align-items: center;padding:0;transition:opacity .12s,color .12s,background .12s;line-height:1}.root_HD_UaQ:hover{opacity:1;color:var(--text-1);background:var(--surface-wash-accent-14)}.root_HD_UaQ[data-checked]{opacity:1;color:var(--text-accent)}.root_HD_UaQ:focus-visible{outline:2px solid var(--fill-accent);outline-offset:2px}}@layer soribashi.recipes{.root_1fs1zQ{flex-shrink:0;align-self: center}.dot_1fs1zQ{font-size:var(--font-size-rem100);display:inline-block;vertical-align:middle;position:relative;color:var(--sd-color);line-height:1;top:-2px}.card_1fs1zQ{box-sizing:border-box;position:fixed;z-index:300;white-space:pre-line;font-family:var(--font-sans);font-size:var(--font-size-rem74);text-align:left;background:var(--panel);color:var(--text-1);border:1px solid var(--border);border-radius:var(--radius-md);padding:var(--spacing-sm)var(--spacing-lg);pointer-events:none;width:max-content;max-width:320px;font-weight:400;line-height:1.5}}@layer soribashi.recipes{.root_PXbu4g{overflow-x:auto;overflow-y:hidden;scrollbar-width:thin;scrollbar-color:var(--border)transparent}.root_PXbu4g::-webkit-scrollbar{height:var(--spacing-px8);width:var(--spacing-px8)}.root_PXbu4g::-webkit-scrollbar-track{background:0 0}.root_PXbu4g::-webkit-scrollbar-thumb{background:var(--muted);border-radius:var(--radius-pill)}.table_PXbu4g{border-collapse:collapse;font-family:var(--font-sans);font-size:var(--font-size-sm);width:100%}.headcell_PXbu4g{color:var(--text-4);text-transform:uppercase;font-size:var(--font-size-xs);letter-spacing:var(--sb-table-tracking);text-align:left;padding:var(--spacing-xs)var(--spacing-md);border-bottom:1px solid var(--border)}.cell_PXbu4g{padding:var(--spacing-sm)var(--spacing-md);border-bottom:1px solid var(--border-soft);white-space:nowrap;vertical-align:middle;font-variant-numeric:var(--font-numeric)}.cell_PXbu4g[data-align=end]{text-align:right}.row_PXbu4g:hover{background:var(--surface-wash-fg-5)}}@layer soribashi.recipes{.root_8hsw9A{position:fixed;right:var(--sb-toasthost-offset);bottom:var(--sb-toasthost-offset);z-index:210;display:flex;gap:var(--spacing-px8);pointer-events:none;flex-direction:column}.toast_8hsw9A{box-sizing:border-box;padding:var(--spacing-px9)var(--font-size-px13);background:var(--card);border:1px solid var(--border);border-left:var(--spacing-px3)solid var(--fill-accent);border-radius:0 var(--radius-px7)var(--radius-px7)0;box-shadow:var(--shadow-toast);color:var(--text-1);font-size:var(--font-size-px13);min-width:240px;max-width:320px}.toast_8hsw9A[data-state],.toast_8hsw9A[data-icon]{display:flex;align-items: center;gap:var(--spacing-px8)}.status_8hsw9A{display:inline-flex;color:var(--text-accent);flex-shrink:0}.toast_8hsw9A[data-icon] .status_8hsw9A{padding:var(--spacing-px5);border-radius:var(--radius-round);background:var(--surface-wash-accent-14)}.toast_8hsw9A[data-state=done]{border-left-color:var(--fill-ok)}.toast_8hsw9A[data-state=done] .status_8hsw9A{color:var(--text-ok)}.toast_8hsw9A[data-state=failed]{border-left-color:var(--fill-bad)}.toast_8hsw9A[data-state=failed] .status_8hsw9A{color:var(--text-bad)}}@layer soribashi.recipes{[data-part=toasthost-toast]{animation:.25s cubic-bezier(.51,.3,0,1.21) toasthost-in}[data-part=toasthost-toast][data-leaving]{overflow:hidden;pointer-events:none;animation:.25s cubic-bezier(.51,.3,0,1.21) forwards toasthost-out,.25s linear forwards toasthost-collapse}@keyframes toasthost-in{0%{opacity:0;transform:translate(100%)}to{opacity:1;transform:translate(0)}}@keyframes toasthost-out{0%{opacity:1;transform:translate(0)}to{opacity:0;transform:translate(100%)}}@keyframes toasthost-collapse{0%{max-height:320px}to{max-height:0;padding-block:0;border-block-width:0;margin-bottom:calc(-1*var(--spacing-px8))}}[data-part=toasthost-toast]{transition:border-left-color .2s ease-out}[data-part=toasthost-toast][data-state=done] [data-part=toasthost-status],[data-part=toasthost-toast][data-state=failed] [data-part=toasthost-status]{animation:.22s ease-out toasthost-check}@keyframes toasthost-check{0%{opacity:0;transform:scale(.6)}to{opacity:1;transform:none}}}@layer soribashi.recipes{.root_6ouFMg{position:relative;display:inline-block}.card_6ouFMg{box-sizing:border-box;position:fixed;z-index:300;white-space:pre-line;max-width:var(--sb-tooltip-max-w);font-family:var(--font-sans);font-size:var(--font-size-px12);text-align:left;background:var(--sb-tooltip-bg);color:var(--card);border-radius:var(--radius-md);padding:var(--spacing-px6)var(--spacing-px10);box-shadow:var(--sb-tooltip-shadow);transform-origin:var(--tooltip-arrow-x,50%)top;pointer-events:none;border:0;width:max-content;font-weight:500;line-height:1.4}.card_6ouFMg:before{content:"";position:absolute;top:calc(-1*var(--sb-tooltip-arrow-h));left:clamp(var(--radius-md),calc(var(--tooltip-arrow-x,50%) - var(--sb-tooltip-arrow-w)/2),calc(100% - var(--sb-tooltip-arrow-w) - var(--radius-md)));width:var(--sb-tooltip-arrow-w);height:var(--sb-tooltip-arrow-h);background:var(--sb-tooltip-bg);clip-path:var(--sb-tooltip-arrow-clip)}}@layer soribashi.recipes{[data-part=tooltip-card]{animation:.14s cubic-bezier(.2,.8,.3,1) tooltip-in}@media (prefers-reduced-motion:reduce){[data-part=tooltip-card]{animation:none}}@keyframes tooltip-in{0%{opacity:0;transform:var(--sb-tooltip-enter-from)}}}
diff --git a/apps/deck/core/generated/board.js b/apps/deck/core/generated/board.js
index bc57c977e1..1543cdef28 100644
--- a/apps/deck/core/generated/board.js
+++ b/apps/deck/core/generated/board.js
@@ -249,7 +249,7 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
To allow this value:
1. Extend the component's vocabulary:
${e}.extend({ vocabulary: { ${r}: (cur) => defineVocabulary([...cur.values, ${JSON.stringify(o)}]) } })
- 2. Include that entry in createTheme({ components: [...] }) and call registerTheme(theme).`)}var Q5=L(tt(),1),W5=L(pe(),1);function xa(e){return function(o){let a=W5.forwardRef((l,c)=>{let u={...o};for(let m in l)if(l[m]!==void 0)u[m]=l[m];return Q5.jsx(e,{ref:c,...u})});if(a.displayName=`WithProps(${e.displayName??e.name??"Component"})`,e.extend!==void 0)a.extend=e.extend;return a.withProps=xa(a),a}}function MS(e){let r=(e.variants?.length??0)>0,o=J5.forwardRef((a,l)=>{let c=Wa(e.name,e.defaults??null,a),u=Ys(c);Xs(e.name,e.vocabularyAxes??[],u.rest,e.variants);let p=e.vars?(g,x)=>e.vars(g,x):(g,x)=>ga(g,e.name,x,r),m=Gl({name:e.name,classes:e.classes,className:u.rest.className,style:u.rest.style,classNames:u.rest.classNames,styles:u.rest.styles,vars:u.rest.vars,attributes:u.rest.attributes,unstyled:u.rest.unstyled,dataAttrs:pu(e.vocabularyAxes??[],r,u.rest),props:u.rest,varsResolver:p,stylePropsStyle:u.rootStyle,stylePropsClassName:u.rootClassName}),h=e.render({props:u.rest,getStyles:m,ref:l});return u.styleNode?AS.jsxs(AS.Fragment,{children:[u.styleNode,h]}):h});return o.displayName=e.name,vu(o,{builder:"defineComponent",name:e.name,slots:e.selectors,parts:[],vocabularyAxes:e.vocabularyAxes??[],variants:e.variants??[],defaults:e.defaults??{}}),o.__vocabularyAxes=e.vocabularyAxes??[],o.classes=e.classes,o.withProps=xa(o),o.extend=qs(e.name),o}var Mm=L(tt(),1),es=L(pe(),1);var zS=L(pe(),1);function e6(e){let r=zS.createContext(null);return[r,()=>{let a=zS.useContext(r);if(a===null)throw Error(e);return a}]}function Am(e){return e.charAt(0).toUpperCase()+e.slice(1)}function LQ(e,r){if(e===r)return!0;let o=Object.keys(e);if(o.length!==Object.keys(r).length)return!1;return o.every((a)=>Object.is(e[a],r[a]))}function t6(e){let r=es.useRef(e);if(!LQ(r.current,e))r.current=e;return r.current}function n6(e,r){return new Proxy({},{get(){throw Error(`<${e}.${Am(r)}> must be inside <${e}>`)}})}function IS(e){if(!e.parts.root)throw Error(`defineCompound("${e.name}") requires parts.root`);if(e.parts.root.polymorphic)throw Error(`defineCompound("${e.name}") root part cannot be polymorphic; declare polymorphism on a child part instead.`);let[r]=e6(`${e.name} parts must be inside <${e.name}>`),o=es.forwardRef(function(u,p){let m=Wa(e.name,e.defaults??null,u),h=Ys(m);Xs(e.name,e.vocabularyAxes??[],h.rest,e.variants);let g=Gl({name:e.name,classes:e.classes,className:h.rest.className,style:h.rest.style,classNames:h.rest.classNames,styles:h.rest.styles,vars:h.rest.vars,attributes:h.rest.attributes,unstyled:h.rest.unstyled,dataAttrs:pu(e.vocabularyAxes??[],(e.variants?.length??0)>0,h.rest),props:h.rest,varsResolver:e.vars?(H,V)=>e.vars(H,V):void 0,stylePropsStyle:h.rootStyle,stylePropsClassName:h.rootClassName}),x=h.rest.variant,y=t6(e.context?e.context(h.rest):{}),b=Zs(),{children:w,...R}=m,_=t6(R),k=es.useMemo(()=>({variant:x,getStyles:g,ctxExtras:y}),[x,y,b,_]),I=(H)=>g(H?.part??"root",H?{active:H.active,variant:H.variant,className:H.className,style:H.style,classNames:H.classNames,styles:H.styles}:void 0),O=Mm.jsx(r.Provider,{value:k,children:e.parts.root.render({props:h.rest,getStyles:I,ctx:{variant:x,...y},children:h.rest.children,ref:p})});return h.styleNode?Mm.jsxs(Mm.Fragment,{children:[h.styleNode,O]}):O});o.displayName=e.name;let a=Object.keys(e.parts);vu(o,{builder:"defineCompound",name:e.name,slots:e.slotKeys??[...a,...Object.keys(e.classes??{}).filter((c)=>!a.includes(c))],parts:a,vocabularyAxes:e.vocabularyAxes??[],variants:e.variants??[],defaults:e.defaults??{}}),o.__vocabularyAxes=e.vocabularyAxes??[],o.classes=e.classes,o.withProps=xa(o),o.extend=qs(e.name);let l={};for(let[c,u]of Object.entries(e.parts)){if(c==="root")continue;let p=`${e.name}${Am(c)}`;if(u.polymorphic===!0){let g=u,x=es.forwardRef(function(b,w){let R=es.useContext(r),_=Wa(p,g.defaults??null,b);Xs(p,e.vocabularyAxes??[],_,e.variants);let{as:k,render:I,...O}=_,H=k??g.defaultElement,V=(Z)=>{if(R===null)throw Error(`<${e.name}.${Am(c)}> must be inside <${e.name}>`);let q=Z?.part??c,Q=q===c,P=O;return R.getStyles(q,{className:Q?Z?.className??P.className:Z?.className,style:Q?Z?.style??P.style:Z?.style,classNames:P.classNames,styles:P.styles,callClassNames:Z?.classNames,callStyles:Z?.styles,vars:P.vars,attributes:P.attributes,unstyled:P.unstyled,dataAttrs:Z?.dataAttrs,active:Z?.active,variant:Z?.variant,themeName:p})},D=R===null?n6(e.name,c):{variant:R.variant,...R.ctxExtras};return g.render({Element:H,props:O,getStyles:V,ctx:D,children:O.children,ref:w})});x.displayName=p,x.extend=qs(p),l[Am(c)]=x;continue}let h=es.forwardRef(function(x,y){let b=es.useContext(r),w=Wa(p,u.defaults??null,x);Xs(p,e.vocabularyAxes??[],w);let R=(O)=>{if(b===null)throw Error(`<${e.name}.${Am(c)}> must be inside <${e.name}>`);let H=O?.part??c,V=H===c,D=w;return b.getStyles(H,{className:V?O?.className??D.className:O?.className,style:V?O?.style??D.style:O?.style,classNames:D.classNames,styles:D.styles,callClassNames:O?.classNames,callStyles:O?.styles,vars:D.vars,attributes:D.attributes,unstyled:D.unstyled,dataAttrs:O?.dataAttrs,active:O?.active,variant:O?.variant,themeName:p})},_=b===null?n6(e.name,c):{variant:b.variant,...b.ctxExtras},{render:k,...I}=w;return u.render({props:I,getStyles:R,ctx:_,children:I.children,ref:y})});h.displayName=p,h.extend=qs(p),l[Am(c)]=h}return Object.assign(o,l),o}var NS=L(tt(),1),r6=L(pe(),1);function PS(e){let r=(e.variants?.length??0)>0,o=r6.forwardRef((a,l)=>{let c=Wa(e.name,e.defaults??null,a),u=Ys(c);Xs(e.name,e.vocabularyAxes??[],u.rest,e.variants);let p=e.vars?(g,x)=>e.vars(g,x):(g,x)=>ga(g,e.name,x,r),m=Gl({name:e.name,classes:e.classes,className:u.rest.className,style:u.rest.style,classNames:u.rest.classNames,styles:u.rest.styles,vars:u.rest.vars,attributes:u.rest.attributes,unstyled:u.rest.unstyled,dataAttrs:pu(e.vocabularyAxes??[],r,u.rest),props:u.rest,varsResolver:p,stylePropsStyle:u.rootStyle,stylePropsClassName:u.rootClassName}),h=e.render({props:u.rest,getStyles:m,ref:l});return u.styleNode?NS.jsxs(NS.Fragment,{children:[u.styleNode,h]}):h});return o.displayName=e.name,vu(o,{builder:"defineGenericComponent",name:e.name,slots:e.selectors,parts:[],vocabularyAxes:e.vocabularyAxes??[],variants:e.variants??[],defaults:e.defaults??{}}),o.__vocabularyAxes=e.vocabularyAxes??[],o.classes=e.classes,o.extend=qs(e.name),o.withProps=xa(o),o}var DS=L(tt(),1),o6=L(pe(),1);function LS(e){let r=(e.variants?.length??0)>0,o=o6.forwardRef((a,l)=>{let c=Wa(e.name,e.defaults??null,a),{as:u,...p}=c,m=u??e.defaultElement,h=Ys(p);Xs(e.name,e.vocabularyAxes??[],h.rest,e.variants);let g=e.vars?(b,w)=>e.vars(b,w):(b,w)=>ga(b,e.name,w,r),x=Gl({name:e.name,classes:e.classes,className:h.rest.className,style:h.rest.style,classNames:h.rest.classNames,styles:h.rest.styles,vars:h.rest.vars,attributes:h.rest.attributes,unstyled:h.rest.unstyled,dataAttrs:pu(e.vocabularyAxes??[],r,h.rest),props:h.rest,varsResolver:g,stylePropsStyle:h.rootStyle,stylePropsClassName:h.rootClassName}),y=e.render({Element:m,props:h.rest,getStyles:x,ref:l});return h.styleNode?DS.jsxs(DS.Fragment,{children:[h.styleNode,y]}):y});return o.displayName=e.name,vu(o,{builder:"definePolymorphicComponent",name:e.name,slots:e.selectors,parts:[],vocabularyAxes:e.vocabularyAxes??[],variants:e.variants??[],defaults:e.defaults??{}}),o.__vocabularyAxes=e.vocabularyAxes??[],o.classes=e.classes,o.withProps=xa(o),o.extend=qs(e.name),o}function Pb(e){Y5(),MO("__global__",{size:e.vocabulary.size,intent:e.vocabulary.intent,variant:e.vocabulary.variant});for(let[r,o]of Object.entries(e.components)){let a=o.vocabulary;if(a)MO(r,{...a.size!==void 0&&{size:a.size},...a.intent!==void 0&&{intent:a.intent},...a.variant!==void 0&&{variant:a.variant}})}}function BS(){return{defineComponent:MS,definePolymorphicComponent:LS,defineCompound:IS,defineGenericComponent:PS}}var BQ=L(pe(),1);var HQ=L(pe(),1);function va(...e){return(r)=>{let o=[];for(let a of e){if(a===null||a===void 0)continue;if(typeof a==="function"){let l=a(r);if(typeof l==="function")o.push(l)}else a.current=r}if(o.length>0)return()=>{for(let a of o)try{a()}catch{}}}}var $Q=L(pe(),1);var i6=L(tt(),1);function zm(e){return i6.jsx(ES.Provider,{value:e.theme,children:e.children})}var ts=L(pe(),1);function a6(e,r){let o={...e};for(let a of Object.keys(r)){let l=e[a],c=r[a];if(a==="className")o[a]=O3(l,c);else if(a==="style")o[a]={...l,...c};else if(a.startsWith("on")&&typeof l==="function"&&typeof c==="function")o[a]=(...u)=>{l(...u),c(...u)};else if(c!==void 0)o[a]=c}return o}var jQ=ts.forwardRef(function(r,o){let{children:a,...l}=r;if(ts.Children.count(a)>1)ts.Children.only(a);if(!ts.isValidElement(a)){if(a!=null)console.error("[soribashi] expects a single React element child; received a non-element child and rendered nothing.");return null}if(a.type===ts.Fragment)throw Error("Slot does not accept React.Fragment children. Pass a single element instead.");let u=a,p=UQ(u),m=a6(l,u.props);if(o!==null&&o!==void 0)m.ref=p?va(o,p):o;else if(p)m.ref=p;return ts.cloneElement(u,m)});function UQ(e){let r=Object.getOwnPropertyDescriptor(e.props,"ref")?.get,o=r&&"isReactWarning"in r&&r.isReactWarning;if(o)return e.ref;if(r=Object.getOwnPropertyDescriptor(e,"ref")?.get,o=r&&"isReactWarning"in r&&r.isReactWarning,o)return e.props.ref;return e.props.ref??e.ref}var s6={blue:{"500":"#3e63dd",hover:"#3358d4",onFill:"#ffffff",text:"#3a5bc7",textSmall:"#1f2d5c",textVivid:"#3a5bc7"},green:{"500":"#0d9b8a",hover:"color-mix(in srgb, #0d9b8a 88%, #1c2024)",onFill:"#ffffff",text:"#008573",textSmall:"#0d3d38",textVivid:"#008573"},red:{"500":"#e93d82",hover:"#df3478",onFill:"#ffffff",text:"#cb1d63",textSmall:"#621639",textVivid:"#cb1d63"},amber:{"500":"#ef5f00",hover:"color-mix(in srgb, #ef5f00 88%, #1c2024)",onFill:"#ffffff",text:"#cc4e00",textSmall:"#582d1d",textVivid:"#cc4e00"},purple:{"500":"#8e4ec6",hover:"#8347b9",onFill:"#ffffff",text:"#8145b5",textSmall:"#402060",textVivid:"#8145b5"},cyan:{"500":"#0797b9",hover:"color-mix(in srgb, #0797b9 88%, #1c2024)",onFill:"#ffffff",text:"#107d98",textSmall:"#0d3c48",textVivid:"#107d98"},gold:{"500":"#ffc53d",hover:"#ffba18",onFill:"#1c2024",text:"#ab6400",textSmall:"#4f3422",textVivid:"#ab6400"},ground:{"1":"#ffffff","2":"#f9f9fb","3":"#f0f0f3","4":"#e8e8ec"},ink:{"1":"#1c2024","2":"#60646c","3":"#60646c","4":"#60646c"},rule:{"1":"#b9bbc6","2":"#cdced6","3":"#d9d9e0"},gray:{fg:"#1c2024",muted:"#8b8d98",mutedText:"#60646c",accentText:"#3a5bc7",okText:"#008573",warnText:"#cc4e00",badgeText:"#60646c",redText:"#cb1d63",mutedOnCard:"#60646c"},surface:{bg:"#f0f0f3",panel:"#f9f9fb",card:"#ffffff",chrome:"#f0f0f3",inset:"#f0f0f3",overlay:"#f9f9fb",raised:"#e8e8ec"},line:{border:"#cdced6",soft:"#d9d9e0",grid:"rgba(52, 59, 88, 0.05)",control:"#b9bbc6",edgeOnCard:"#cdced6",controlEdgeOnCard:"#b9bbc6",softOnCard:"#d9d9e0"},dot:{ok:"#0d9b8a",warn:"#ef5f00",bad:"#e93d82"}},l6={blue:{"500":"#3e63dd",hover:"#5472e4",onFill:"#ffffff",text:"#9eb1ff",textSmall:"#d6e1ff",textVivid:"#9eb1ff"},green:{"500":"#12a594",hover:"#0eb39e",onFill:"#ffffff",text:"#0bd8b6",textSmall:"#adf0dd",textVivid:"#0bd8b6"},red:{"500":"#e93d82",hover:"#ee518a",onFill:"#ffffff",text:"#ff92ad",textSmall:"#fdd3e8",textVivid:"#ff92ad"},amber:{"500":"#f76b15",hover:"#ff801f",onFill:"#ffffff",text:"#ffa057",textSmall:"#ffe0c2",textVivid:"#ffa057"},purple:{"500":"#8e4ec6",hover:"#9a5cd0",onFill:"#ffffff",text:"#d19dff",textSmall:"#ecd9fa",textVivid:"#d19dff"},cyan:{"500":"#00a2c7",hover:"#23afd0",onFill:"#ffffff",text:"#4ccce6",textSmall:"#b6ecf7",textVivid:"#4ccce6"},gold:{"500":"#ffc53d",hover:"#ffd60a",onFill:"#1c2024",text:"#ffca16",textSmall:"#ffe7b3",textVivid:"#ffca16"},ground:{"1":"#111113","2":"#18191b","3":"#212225","4":"#272a2d"},ink:{"1":"#edeef0","2":"#b0b4ba","3":"#b0b4ba","4":"#b0b4ba"},rule:{"1":"#696e77","2":"#43484e","3":"#363a3f"},gray:{fg:"#edeef0",muted:"#696e77",mutedText:"#b0b4ba",accentText:"#9eb1ff",okText:"#0bd8b6",warnText:"#ffa057",badgeText:"#b0b4ba",redText:"#ff92ad",mutedOnCard:"#b0b4ba"},surface:{bg:"#111113",panel:"#18191b",card:"#212225",chrome:"#18191b",inset:"#111113",overlay:"#212225",raised:"#272a2d"},line:{border:"#43484e",soft:"#363a3f",grid:"rgba(122, 162, 247, 0.06)",control:"#696e77",edgeOnCard:"#696e77",controlEdgeOnCard:"#696e77",softOnCard:"#43484e"},dot:{ok:"#12a594",warn:"#f76b15",bad:"#e93d82"}},c6={mono:'"JetBrains Mono", ui-monospace, SFMono-Regular, Menlo, Consolas, monospace',sans:'-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif'},u6="1.55",f6={card:"0 2px 5px -1px light-dark(rgb(16 24 40 / 16%), rgb(0 0 0 / 60%))"};var qQ={accent:"blue",ok:"green",warn:"amber",bad:"red",cyan:"cyan",purple:"purple",muted:"gray"},GQ={accent:"var(--text-accent)",ok:"var(--text-ok)",warn:"var(--text-warn)",bad:"var(--text-bad)",cyan:"var(--text-cyan)",purple:"var(--text-purple)",muted:"var(--text-2)"},YQ={accent:"var(--text-accent-small)",ok:"var(--text-ok-small)",warn:"var(--text-warn-small)",bad:"var(--text-bad-small)",cyan:"var(--text-cyan-small)",purple:"var(--text-purple-small)",muted:"var(--text-4)"};function XQ(e,r,o){if(r==="light")return YQ[o]??e;if(r==="outline"||r==="subtle")return GQ[o]??e;return e}var d6=({intent:e,variant:r})=>{let o=qQ[e]??"blue",a=o==="gray"?"var(--color-gray-muted)":`var(--color-${o}-500)`,l=Ob(a,r);if(r==="filled"){let u=o==="gray";l={...l,color:u?"light-dark(var(--text-1), #ffffff)":`var(--color-${o}-onFill)`,hover:u?`color-mix(in srgb, ${a} 88%, var(--fg))`:`var(--color-${o}-hover)`,border:"transparent"}}let c=XQ(a,r,e);return c===a?l:{...l,color:c}};var KQ={size:Vs(["xs","sm","md","lg","xl"]),intent:Vs(["accent","ok","warn","bad","cyan","purple","muted"]),variant:Vs(["default","filled","light","outline","subtle"])},QQ=l6,Db=_m({name:"tui-kit",darkMode:{selector:".dark"},intentResolver:d6,vocabulary:KQ,tokens:{colors:s6,radius:{xs:"3px",sm:"4px",md:"6px",lg:"8px",xl:"10px",round:"50%",pill:"999px",px2:"2px",px5:"5px",px7:"7px"},spacing:{xxs:"0.15rem",xs:"0.3rem",sm:"0.45rem",md:"0.6rem",lg:"0.7rem",xl:"0.9rem",xxl:"1.1rem",rem20:"0.2rem",rem35:"0.35rem",rem40:"0.4rem",rem50:"0.5rem",rem55:"0.55rem",rem75:"0.75rem",rem80:"0.8rem",rem95:"0.95rem",rem100:"1rem",rem120:"1.2rem",rem140:"1.4rem",rem150:"1.5rem",rem175:"1.75rem",rem180:"1.8rem",px2:"2px",px3:"3px",px4:"4px",px5:"5px",px6:"6px",px7:"7px",px8:"8px",px9:"9px",px10:"10px",px12:"12px",px14:"14px"},fontSize:{base:"13.5px",xxs:"0.55rem",xs:"0.66rem",sm:"0.7rem",md:"0.76rem",lg:"0.85rem",xl:"0.92rem",xxl:"1.15rem",rem60:"0.6rem",rem65:"0.65rem",rem68:"0.68rem",rem72:"0.72rem",rem74:"0.74rem",rem75:"0.75rem",rem78:"0.78rem",rem80:"0.8rem",rem82:"0.82rem",rem62:"0.62rem",rem90:"0.9rem",rem95:"0.95rem",rem100:"1rem",rem105:"1.05rem",px9:"9px",px10:"10px",px11:"11px",px12:"12px",px13:"13px"},fontFamily:c6,lineHeight:{base:u6,snug:"1.4"},shadow:{card:f6.card,menu:"0 10px 30px rgba(0, 0, 0, 0.28), 0 2px 8px rgba(0, 0, 0, 0.18)",modal:"0 12px 40px rgba(0, 0, 0, 0.25)",toast:"0 6px 20px rgba(0, 0, 0, 0.25)",drawer:"-6px 0 32px rgba(0, 0, 0, 0.3)","drawer-left":"4px 0 28px rgba(0, 0, 0, 0.3)"}},dark:{colors:QQ},semanticTokens:{defaults:!1,surface:{canvas:"colors.surface.bg",panel:"colors.surface.panel",card:"colors.surface.card",chrome:"colors.surface.chrome",inset:"colors.surface.inset",overlay:"colors.surface.overlay",raised:"colors.surface.raised","wash-bg-55":"color-mix(in srgb, var(--bg) 55%, transparent)","wash-panel-55":"color-mix(in srgb, var(--panel) 55%, transparent)","wash-panel-70":"color-mix(in srgb, var(--panel) 70%, transparent)","wash-panel-88":"color-mix(in srgb, var(--panel) 88%, transparent)","wash-panel-94":"color-mix(in srgb, var(--panel) 94%, transparent)","wash-panel-100":"color-mix(in srgb, var(--panel) 100%, transparent)","wash-accent-7":"color-mix(in srgb, var(--accent) 7%, transparent)","wash-accent-14":"color-mix(in srgb, var(--accent) 14%, transparent)","wash-accent-16":"color-mix(in srgb, var(--accent) 16%, transparent)","wash-accent-fg-70":"color-mix(in srgb, var(--accent) 70%, var(--fg))","wash-amber-7":"color-mix(in srgb, var(--amber) 7%, transparent)","wash-amber-45":"color-mix(in srgb, var(--amber) 45%, transparent)","wash-fg-5":"color-mix(in srgb, var(--fg) 5%, transparent)","wash-fg-8":"color-mix(in srgb, var(--fg) 8%, transparent)","wash-cyan-panel-38":"color-mix(in srgb, var(--cyan) 38%, var(--panel))","wash-cyan-border-45":"color-mix(in srgb, var(--cyan) 45%, var(--border))","wash-fg-4-card":"color-mix(in srgb, var(--fg) 4%, var(--card))"},text:{primary:"colors.gray.fg",muted:"colors.gray.mutedText",accentText:"colors.gray.accentText",badText:"colors.gray.redText","muted-on-card":"colors.gray.mutedOnCard"},border:{default:"colors.line.border",control:"colors.line.control",soft:"colors.line.soft","on-card":"colors.line.edgeOnCard","control-on-card":"colors.line.controlEdgeOnCard","soft-on-card":"colors.line.softOnCard"}}});Pb(Db);var ai=L(pe(),1);var HS=L(tt(),1);var{defineComponent:_t,definePolymorphicComponent:p6,defineCompound:m6,defineGenericComponent:IO}=BS();var h6={root:"root_jT5pOw",command:"command_jT5pOw"};var eW=["root","command"],NO={root:"alert",command:"alert-command"},tW={ok:"var(--green)",bad:"var(--red)"},Wo=_t({name:"Alert",selectors:eW,classes:h6,vars:(e,r)=>{let o=tW[r.intent];return{root:{"--sb-alert-color":o,"--sb-alert-bg":`color-mix(in srgb, ${o} 10%, transparent)`}}},render:({props:e,getStyles:r,ref:o})=>{let{intent:a,command:l,children:c,classNames:u,styles:p,vars:m,attributes:h,unstyled:g,...x}=e;return HS.jsxs("div",{ref:o,...x,...r("root"),role:"alert","data-part":NO.root,"data-intent":a,children:[c,l===void 0?null:HS.jsx("pre",{...r("command"),"data-part":NO.command,children:l})]})}}),nW=Wo.extend({});var b6=L(tt(),1);var g6={root:"root_so3lDg"};var oW=["root"],y6={root:"badge"},iW={ok:"var(--green)",warn:"var(--amber)",bad:"var(--red)",muted:"var(--muted)"},mr=_t({name:"Badge",selectors:oW,classes:g6,vars:(e,r)=>{let o=r.intent??"muted",a=iW[o];return{root:{"--sb-badge-color":o==="muted"?"var(--text-muted)":`var(--text-${o}-small)`,"--sb-badge-bg":`color-mix(in srgb, ${a} 12%, transparent)`,"--sb-badge-border":`color-mix(in srgb, ${a} 40%, transparent)`}}},render:({props:e,getStyles:r,ref:o})=>{let{intent:a,children:l,classNames:c,styles:u,vars:p,attributes:m,unstyled:h,...g}=e;return b6.jsx("span",{ref:o,...g,...r("root"),"data-part":y6.root,"data-intent":a??"muted",children:l})}}),aW=mr.extend({});var jS=L(tt(),1);var $S=L(tt(),1);var x6={root:"root_0gPzkQ"};var lW=["root"],v6={root:"spinner"},cW={xs:"calc(0.85em + 4px)",sm:"calc(1em + 4px)",md:"16px"},Wr=_t({name:"Spinner",selectors:lW,classes:x6,vars:(e,r)=>({root:{"--sb-spinner-size":cW[r.size??"sm"]}}),render:({props:e,getStyles:r,ref:o})=>{let{size:a,classNames:l,styles:c,vars:u,attributes:p,unstyled:m,...h}=e;return $S.jsx("span",{ref:o,"aria-hidden":!0,...h,...r("root"),"data-part":v6.root,"data-size":a??"sm",children:$S.jsx("svg",{viewBox:"0 0 24 24",fill:"none",stroke:"currentColor",strokeWidth:"2.5",strokeLinecap:"round",children:$S.jsx("path",{d:"M21 12a9 9 0 1 1-6.219-8.56"})})})}}),uW=Wr.extend({});var S6={root:"root_30t2rQ"};var dW=["root"],w6={root:"button"},pW=["default","filled","light","outline","subtle"],mW=["intent","variant","size"],hW={"light|muted":{bg:"color-mix(in srgb, var(--fg) 8%, transparent)",color:"var(--fg)",hover:"color-mix(in srgb, var(--fg) 13%, transparent)",border:"transparent"},"light|accent":{bg:"color-mix(in srgb, var(--accent) 14%, transparent)",color:"var(--text-accent-small)",hover:"color-mix(in srgb, var(--accent) 22%, transparent)",border:"transparent"},"subtle|muted":{color:"var(--fg)",hover:"color-mix(in srgb, var(--fg) 6%, transparent)",border:"transparent"}},mt=_t({name:"Button",selectors:dW,vocabularyAxes:mW,variants:pW,classes:S6,vars:(e,r)=>{let o=ga(e,"Button",r,!0),a=hW[`${r.variant}|${r.intent}`];return{root:{...o.root,"--sb-button-bad-color":"var(--text-bad)",...a&&{"--button-bg":a.bg??o.root?.["--button-bg"],"--button-color":a.color,"--button-hover":a.hover,"--button-border":a.border}}}},defaults:{intent:"accent",variant:"default",size:"md"},render:({props:e,getStyles:r,ref:o})=>{let{children:a,busy:l,iconOnly:c,disabled:u,type:p,intent:m,variant:h,size:g,classNames:x,styles:y,vars:b,attributes:w,unstyled:R,..._}=e;if(ha()&&c&&!_["aria-label"])console.warn("tui-kit Button: iconOnly requires an aria-label");return jS.jsxs("button",{ref:o,type:p??"button",disabled:u||l||void 0,..._,...r("root"),"data-part":w6.root,"data-icon-only":c||void 0,"aria-busy":l||void 0,children:[l&&jS.jsx(Wr,{size:c?"md":"xs"}),a]})}}),gW=mt.extend({});var Lb=L(tt(),1);var E6={root:"root_TGYYvw",label:"label_TGYYvw",icon:"icon_TGYYvw"};var yW=["root","icon","label"],US={root:"chip",icon:"chip-icon",label:"chip-label"},xW=["outline","subtle"],vW=["intent","variant"],SW={"--sb-chip-pulse-period":"1.4s","--sb-chip-dimmed-opacity":"0.7","--sb-chip-uppercase-tracking":"0.06em"},wu=p6({name:"Chip",defaultElement:"span",vocabularyAxes:vW,selectors:yW,variants:xW,classes:E6,defaults:{intent:"accent",variant:"outline"},vars:(e,r)=>({root:{...ga(e,"Chip",r,!0).root??{},...SW}}),render:({Element:e,props:r,getStyles:o,ref:a})=>{let{children:l,icon:c,pulse:u,dimmed:p,uppercase:m,intent:h,variant:g,classNames:x,styles:y,vars:b,attributes:w,unstyled:R,..._}=r,k=e==="button"?{type:"button",..._}:_;return Lb.jsxs(e,{ref:a,...k,...o("root"),"data-part":US.root,"data-pulse":u||void 0,"data-dimmed":p||void 0,"data-uppercase":m||void 0,children:[c!=null&&Lb.jsx("span",{...o("icon"),"data-part":US.icon,"aria-hidden":!0,children:c}),m?Lb.jsx("span",{...o("label"),"data-part":US.label,children:l}):l]})}}),wW=wu.extend({});var dd=L(tt(),1),VS=L(pe(),1);var fd=L(tt(),1);var Sa=L(pe(),1);var Bb=[];function PO(e){return Bb.push(e),()=>{let r=Bb.lastIndexOf(e);if(r>=0)Bb.splice(r,1)}}function DO(){Bb[Bb.length-1]?.()}var Hb=0,R6="";function LO(e){if(Hb===0)R6=e.overflow,e.overflow="hidden";Hb++}function BO(e){if(Hb=Math.max(0,Hb-1),Hb===0)e.overflow=R6}var HO=0,Im=null;function Eu(e){let r=Sa.useRef(e);r.current=e,Sa.useEffect(()=>{let o=PO(()=>r.current());if(HO++,!Im)Im=(a)=>{if(a.key==="Escape")DO()},document.addEventListener("keydown",Im);return()=>{if(o(),HO--,HO===0&&Im)document.removeEventListener("keydown",Im),Im=null}},[])}function $b(){Sa.useEffect(()=>(LO(document.body.style),()=>{BO(document.body.style)}),[])}var k6=3500,EW=2000,RW=30000;function $O(){let[e,r]=Sa.useState([]),o=Sa.useRef(0),a=Sa.useCallback((u,p)=>setTimeout(()=>r((m)=>m.filter((h)=>h.id!==u)),p),[]),l=Sa.useCallback((u,p)=>{let m=++o.current;r((h)=>[...h,{id:m,text:u,...p?.icon?{icon:p.icon}:{}}]),a(m,k6)},[a]),c=Sa.useCallback((u)=>{let p=++o.current;r((y)=>[...y,{id:p,text:u,state:"pending"}]);let m=!1,h=!0,g=setTimeout(()=>{h=!1,r((y)=>y.filter((b)=>b.id!==p))},RW),x=(y,b)=>{if(m)return;if(m=!0,clearTimeout(g),h)r((w)=>w.map((R)=>R.id===p?{id:p,...y}:R));else p=++o.current,r((w)=>[...w,{id:p,...y}]);a(p,b)};return{done:(y)=>x({text:y,state:"done"},EW),fail:(y)=>x({text:y,state:"failed"},k6)}},[a]);return{toasts:e,addToast:l,startToast:c}}var $n=L(tt(),1);var C6={root:"root_Puve9g"};var CW=["root"],TW="icon",ot=_t({name:"Icon",selectors:CW,classes:C6,render:({props:e,getStyles:r,ref:o})=>{let{d:a,circle:l,classNames:c,styles:u,vars:p,attributes:m,unstyled:h,...g}=e;return $n.jsxs("svg",{ref:o,viewBox:"0 0 24 24",width:"14",height:"14",fill:"none",stroke:"currentColor",strokeWidth:"2",strokeLinecap:"round",strokeLinejoin:"round","aria-hidden":!0,...g,...r("root"),"data-part":TW,children:[l&&$n.jsx("circle",{cx:"12",cy:"12",r:"4"}),$n.jsx("path",{d:a})]})}}),fn={rows:$n.jsx(ot,{d:"M3 6h18M3 12h18M3 18h18"}),grid:$n.jsx(ot,{d:"M3 3h7v7H3zM14 3h7v7h-7zM3 14h7v7H3zM14 14h7v7h-7z"}),light:$n.jsx(ot,{circle:!0,d:"M12 2v2m0 16v2M4.9 4.9l1.4 1.4m11.4 11.4 1.4 1.4M2 12h2m16 0h2M4.9 19.1l1.4-1.4M17.7 6.3l1.4-1.4"}),dark:$n.jsx(ot,{d:"M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z"}),system:$n.jsx(ot,{d:"M2 4h20v12H2zM8 20h8m-4-4v4"}),menu:$n.jsx(ot,{d:"M3 6h18M3 12h18M3 18h18"}),close:$n.jsx(ot,{d:"M6 6l12 12M18 6L6 18"}),refresh:$n.jsx(ot,{d:"M23 4v6h-6M1 20v-6h6M3.51 9a9 9 0 0 1 14.85-3.36L23 10M1 14l4.64 4.36A9 9 0 0 0 20.49 15"}),people:$n.jsx(ot,{d:"M16 21v-2a4 4 0 0 0-4-4H6a4 4 0 0 0-4 4v2M9 11a4 4 0 1 0 0-8 4 4 0 0 0 0 8M22 21v-2a4 4 0 0 0-3-3.87M16 3.13a4 4 0 0 1 0 7.75"}),settings:$n.jsx(ot,{circle:!0,d:"M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 1 1-2.83 2.83l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 0 1-4 0v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 1 1-2.83-2.83l.06-.06a1.65 1.65 0 0 0 .33-1.82 1.65 1.65 0 0 0-1.51-1H3a2 2 0 0 1 0-4h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 1 1 2.83-2.83l.06.06a1.65 1.65 0 0 0 1.82.33H9a1.65 1.65 0 0 0 1-1.51V3a2 2 0 0 1 4 0v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 1 1 2.83 2.83l-.06.06a1.65 1.65 0 0 0-.33 1.82V9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 0 1 0 4h-.09a1.65 1.65 0 0 0-1.51 1z"}),plus:$n.jsx(ot,{d:"M5 12h14M12 5v14"}),"external-link":$n.jsx(ot,{d:"M15 3h6v6M10 14 21 3M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6"}),"triangle-alert":$n.jsx(ot,{d:"m21.73 18-8-14a2 2 0 0 0-3.48 0l-8 14A2 2 0 0 0 4 21h16a2 2 0 0 0 1.73-3M12 9v4M12 17h.01"}),"circle-check":$n.jsx(ot,{d:"M2 12a10 10 0 1 0 20 0a10 10 0 1 0-20 0M9 12l2 2 4-4"}),"file-warning":$n.jsx(ot,{d:"M6 22a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h8a2.4 2.4 0 0 1 1.704.706l3.588 3.588A2.4 2.4 0 0 1 20 8v12a2 2 0 0 1-2 2zM12 9v4M12 17h.01"}),"refresh-cw":$n.jsx(ot,{d:"M3 12a9 9 0 0 1 9-9 9.75 9.75 0 0 1 6.74 2.74L21 8M21 3v5h-5M21 12a9 9 0 0 1-9 9 9.75 9.75 0 0 1-6.74-2.74L3 16M8 16H3v5"}),pencil:$n.jsx(ot,{d:"M21.174 6.812a1 1 0 0 0-3.986-3.987L3.842 16.174a2 2 0 0 0-.5.83l-1.321 4.352a.5.5 0 0 0 .623.622l4.353-1.32a2 2 0 0 0 .83-.497zM15 5l4 4"}),"trash-2":$n.jsx(ot,{d:"M10 11v6M14 11v6M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6M3 6h18M8 6V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"}),"lock-keyhole":$n.jsx(ot,{d:"M11 16a1 1 0 1 0 2 0a1 1 0 1 0-2 0M5 10L19 10A2 2 0 0 1 21 12L21 20A2 2 0 0 1 19 22L5 22A2 2 0 0 1 3 20L3 12A2 2 0 0 1 5 10ZM7 10V7a5 5 0 0 1 10 0v3"}),"user-round-check":$n.jsx(ot,{d:"M2 21a8 8 0 0 1 13.292-6M5 8a5 5 0 1 0 10 0a5 5 0 1 0-10 0M16 19l2 2 4-4"}),"rotate-ccw":$n.jsx(ot,{d:"M3 12a9 9 0 1 0 9-9 9.75 9.75 0 0 0-6.74 2.74L3 8M3 3v5h5"}),"chevron-right":$n.jsx(ot,{d:"m9 18 6-6-6-6"})},jO="M9 9h10v10H9zM5 15H4a1 1 0 0 1-1-1V4a1 1 0 0 1 1-1h10a1 1 0 0 1 1 1v1",ud="M20 6 9 17l-5-5",UO="M6 6l12 12M18 6L6 18",_W=ot.extend({});var T6={overlay:"overlay_gUyvOg",root:"root_gUyvOg",head:"head_gUyvOg",title:"title_gUyvOg",close:"close_gUyvOg"};var AW=["root","overlay","head","title","close"],Nm={root:"modal",overlay:"modal-overlay",head:"modal-head",title:"modal-title",close:"modal-close"},FS={overlayPadY:"8vh",maxWidth:"420px",maxHeight:"80vh",titleTracking:"0.02em"},yi=_t({name:"Modal",selectors:AW,classes:T6,vars:()=>({overlay:{"--sb-modal-overlay-pad-y":FS.overlayPadY},root:{"--sb-modal-max-w":FS.maxWidth,"--sb-modal-max-h":FS.maxHeight},title:{"--sb-modal-title-tracking":FS.titleTracking}}),render:({props:e,getStyles:r,ref:o})=>{let{title:a,ariaLabel:l,onClose:c,closeGlyph:u,overlayClassName:p,children:m,classNames:h,styles:g,vars:x,attributes:y,unstyled:b,...w}=e;return Eu(c),$b(),fd.jsx("div",{...r("overlay",{className:p}),"data-part":Nm.overlay,onClick:c,children:fd.jsxs("div",{ref:o,role:"dialog","aria-modal":!0,...w,...r("root"),"data-part":Nm.root,"aria-label":l,onClick:(R)=>R.stopPropagation(),children:[fd.jsxs("div",{...r("head"),"data-part":Nm.head,children:[fd.jsx("span",{...r("title"),"data-part":Nm.title,children:a}),fd.jsx("button",{type:"button",...r("close"),"data-part":Nm.close,onClick:c,"aria-label":"close",children:u??fn.close})]}),m]})})}}),MW=yi.extend({});var _6={body:"body_-s5fvg",foot:"foot_-s5fvg"};var IW=["root","body","foot"],FO={root:"confirmdialog",body:"confirmdialog-body",foot:"confirmdialog-foot"},ZS=_t({name:"ConfirmDialog",selectors:IW,classes:_6,render:({props:e,getStyles:r,ref:o})=>{let{open:a,title:l,onConfirm:c,onCancel:u,confirmLabel:p,cancelLabel:m,intent:h,confirmVariant:g,ariaLabel:x,children:y}=e,b=VS.useRef(null);if(VS.useEffect(()=>{if(a)b.current?.focus()},[a]),!a)return null;let w=x??(typeof l==="string"?l:"");return dd.jsxs(yi,{ref:o,...r("root"),title:l,ariaLabel:w,onClose:u,children:[dd.jsx("div",{...r("body"),"data-part":FO.body,children:y}),dd.jsxs("div",{...r("foot"),"data-part":FO.foot,children:[dd.jsx(mt,{ref:b,size:"sm",onClick:u,children:m??"Cancel"}),dd.jsx(mt,{size:"sm",intent:h??"bad",variant:g,onClick:c,children:p})]})]})}}),NW=ZS.extend({});var Zn=L(tt(),1);var qS=L(pe(),1),GS=qS.createContext(void 0);GS.displayName="MenuPositionerContext";function Pm(e){let r=qS.useContext(GS);if(r===void 0&&!e)throw Error("Base UI: MenuPositionerContext is missing. MenuPositioner parts must be placed within .");return r}var YS=L(pe(),1),XS=YS.createContext(void 0);XS.displayName="MenuRootContext";function wo(e){let r=YS.useContext(XS);if(r===void 0&&!e)throw Error("Base UI: MenuRootContext is missing. Menu parts must be placed within .");return r}var Yl=L(pe(),1);var A6=L(pe(),1),O6={};function cn(e,r){let o=A6.useRef(O6);if(o.current===O6)o.current=e(r);return o}function xi(e,r,o,a){let l=cn(z6).current;if(PW(l,e,r,o,a))I6(l,[e,r,o,a]);return l.callback}function M6(e){let r=cn(z6).current;if(DW(r,e))I6(r,e);return r.callback}function z6(){return{callback:null,cleanup:null,refs:[]}}function PW(e,r,o,a,l){return e.refs[0]!==r||e.refs[1]!==o||e.refs[2]!==a||e.refs[3]!==l}function DW(e,r){return e.refs.length!==r.length||e.refs.some((o,a)=>o!==r[a])}function I6(e,r){if(e.refs=r,r.every((o)=>o==null)){e.callback=null;return}e.callback=(o)=>{if(e.cleanup)e.cleanup(),e.cleanup=null;if(o!=null){let a=Array(r.length).fill(null);for(let l=0;l{for(let l=0;l=e}function VO(e){if(!P6.isValidElement(e))return null;let r=e,o=r.props;return(Dm(19)?o?.ref:r.ref)??null}function jb(e,r){if(e&&!r)return e;if(!e&&r)return r;if(e||r)return{...e,...r};return}var ZO;ZO=new Set;function KS(e,r){return function(...a){{let l=a.join(" "),c=r?`${r}: ${l}`:l,u=`${e}:${c}`;if(!ZO.has(u))if(ZO.add(u),e==="warn")console.warn(c);else console.error(c)}}}var QS=KS("warn","Base UI");function an(){}var vi=Object.freeze([]),en=Object.freeze({});function D6(e,r){let o={};for(let a in e){let l=e[a];if(r?.hasOwnProperty(a)){let c=r[a](l);if(c!=null)Object.assign(o,c);continue}if(l===!0)o[`data-${a.toLowerCase()}`]="";else if(l)o[`data-${a.toLowerCase()}`]=l.toString()}return o}function L6(e,r){return typeof e==="function"?e(r):e}function B6(e,r){return typeof e==="function"?e(r):e}var qO={};function so(e,r,o,a,l){if(!o&&!a&&!l&&!e)return WS(r);let c=WS(e);if(r)c=Ub(c,r);if(o)c=Ub(c,o);if(a)c=Ub(c,a);if(l)c=Ub(c,l);return c}function H6(e){if(e.length===0)return qO;if(e.length===1)return WS(e[0]);let r=WS(e[0]);for(let o=1;o