Files
nexu-io--open-design/apps/web/src/components/ContinueInCliButton.tsx
T
wehub-resource-sync 070959e133
landing-page-staging / Deploy landing page to staging (push) Has been skipped
landing-page-ci / Validate landing page (push) Failing after 4s
visual-baseline / Capture visual baselines (push) Has been cancelled
bake-plugin-previews / Bake plugin previews (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 12:00:47 +08:00

87 lines
3.3 KiB
TypeScript

// "Continue in CLI" toolbar action — #451. Three states:
//
// - DESIGN.md missing → disabled with a tooltip pointing at the
// Finalize action so the user learns the workflow rather than
// having the prerequisite hidden.
// - DESIGN.md present + fresh → enabled, plain label.
// - DESIGN.md present + stale → enabled with a warning chip; the
// chip text is canonical per spec §4.6 ("Spec is stale —
// regenerate?"). A "regenerate?" affordance can land in a
// follow-up; v1 keeps the chip text-only so the user can still
// proceed with Continue in CLI from a stale spec if they
// intentionally want the captured intent.
//
// The actual click handler lives in ProjectView (it owns the
// resolvedDir + clipboard + terminal-launch + toast wiring) and is
// passed in as `onClick`. Disabled state short-circuits in the
// component itself.
import type { DesignMdState, DesignMdStaleReason } from '../hooks/useDesignMdState';
const STALE_CHIP_TEXT = 'Spec is stale — regenerate?';
// Round 7 (mrcfps @ useDesignMdState.ts:160): malformed provenance
// timestamps used to silently report fresh; they now surface as a
// distinct chip so the user knows the freshness signal is degraded
// rather than green.
const UNKNOWN_PROVENANCE_CHIP_TEXT = 'Spec freshness unknown — regenerate to refresh signal';
const DISABLED_TOOLTIP = 'Finalize the design package first.';
function chipTextForReason(reason: DesignMdStaleReason): string {
return reason === 'unknown-provenance' ? UNKNOWN_PROVENANCE_CHIP_TEXT : STALE_CHIP_TEXT;
}
export interface ContinueInCliButtonProps {
designMdState: Pick<DesignMdState, 'exists' | 'isStale' | 'staleReason'>;
onClick: () => void | Promise<void>;
}
export function ContinueInCliButton({ designMdState, onClick }: ContinueInCliButtonProps) {
if (!designMdState.exists) {
// Native `<button disabled>` does not fire hover or focus events
// in the browsers we ship against, so a `title` tooltip on the
// disabled button never surfaces — that hides the prerequisite
// guidance that the spec explicitly wanted discoverable. Render
// the help text as a visible sibling instead, plus an
// aria-describedby link so assistive tech announces the same
// explanation when the disabled button gets focused.
return (
<span className="project-actions-button-group">
<button
type="button"
className="project-actions-button project-actions-button-secondary"
disabled
aria-describedby="continue-in-cli-disabled-hint"
>
Continue in CLI
</button>
<span
id="continue-in-cli-disabled-hint"
className="project-actions-disabled-hint"
role="note"
>
{DISABLED_TOOLTIP}
</span>
</span>
);
}
return (
<span className="project-actions-button-group">
<button
type="button"
className="project-actions-button project-actions-button-secondary"
onClick={() => {
void onClick();
}}
>
Continue in CLI
</button>
{designMdState.isStale ? (
<span className="project-actions-chip" role="note" aria-label="Spec staleness">
{chipTextForReason(designMdState.staleReason)}
</span>
) : null}
</span>
);
}