From 5a67877dfe4ee847c0332adcfa95eb7368c932dc Mon Sep 17 00:00:00 2001 From: Ned Twigg Date: Mon, 18 May 2026 23:34:45 -0700 Subject: [PATCH 1/9] Group modal stories --- lib/src/stories/ExternalLinkDialog.stories.tsx | 2 +- lib/src/stories/KillModal.stories.tsx | 2 +- lib/src/stories/UpdateDebugDialog.stories.tsx | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/src/stories/ExternalLinkDialog.stories.tsx b/lib/src/stories/ExternalLinkDialog.stories.tsx index 676557a..44695de 100644 --- a/lib/src/stories/ExternalLinkDialog.stories.tsx +++ b/lib/src/stories/ExternalLinkDialog.stories.tsx @@ -24,7 +24,7 @@ function DialogStory({ uri, displayText }: { uri: string; displayText: string }) } const meta: Meta = { - title: 'Components/ExternalLinkDialog', + title: 'Modals/ExternalLinkDialog', component: DialogStory, argTypes: { uri: { control: 'text' }, diff --git a/lib/src/stories/KillModal.stories.tsx b/lib/src/stories/KillModal.stories.tsx index 40d7881..8f0a5c8 100644 --- a/lib/src/stories/KillModal.stories.tsx +++ b/lib/src/stories/KillModal.stories.tsx @@ -18,7 +18,7 @@ function KillModal({ char = 'G', onCancel, exit }: { char?: string; onCancel?: ( } const meta: Meta = { - title: 'Components/KillModal', + title: 'Modals/KillModal', component: KillModal, argTypes: { char: { control: 'text' }, diff --git a/lib/src/stories/UpdateDebugDialog.stories.tsx b/lib/src/stories/UpdateDebugDialog.stories.tsx index 77da4cc..f780189 100644 --- a/lib/src/stories/UpdateDebugDialog.stories.tsx +++ b/lib/src/stories/UpdateDebugDialog.stories.tsx @@ -42,7 +42,7 @@ const BODY = [ ].join('\n'); const meta: Meta = { - title: 'Components/UpdateDebugDialog', + title: 'Modals/UpdateDebugDialog', component: UpdateDebugDialogStory, }; From 722fbc1243e9b63456712733acbf4fef686aac3a Mon Sep 17 00:00:00 2001 From: Ned Twigg Date: Mon, 18 May 2026 23:44:13 -0700 Subject: [PATCH 2/9] Unify modal primitives --- lib/src/components/ExternalLinkDialog.tsx | 13 +- lib/src/components/KillConfirm.tsx | 14 ++- lib/src/components/design.tsx | 28 ++++- lib/src/stories/KillModal.stories.tsx | 10 +- standalone/src/UpdateDebugDialog.tsx | 142 ++++++++++++---------- 5 files changed, 123 insertions(+), 84 deletions(-) diff --git a/lib/src/components/ExternalLinkDialog.tsx b/lib/src/components/ExternalLinkDialog.tsx index c90cf3e..5a76ca4 100644 --- a/lib/src/components/ExternalLinkDialog.tsx +++ b/lib/src/components/ExternalLinkDialog.tsx @@ -1,11 +1,11 @@ import { useRef } from 'react'; -import { ProhibitIcon, WarningOctagonIcon, XIcon } from '@phosphor-icons/react'; +import { ProhibitIcon, WarningOctagonIcon } from '@phosphor-icons/react'; import type { DisplayMatchVerdict, ExternalUriDecision } from '../lib/external-links'; import { + ModalCloseButton, ModalOverlay, ModalSurface, modalActionButton, - modalIconButton, useModalFocusTrap, } from './design'; @@ -98,14 +98,7 @@ export function ExternalLinkDialog({ )} - + {/* Bordered nested box: explicit exception to the bg-only chrome rule diff --git a/lib/src/components/KillConfirm.tsx b/lib/src/components/KillConfirm.tsx index b3b19e2..5ea4500 100644 --- a/lib/src/components/KillConfirm.tsx +++ b/lib/src/components/KillConfirm.tsx @@ -20,8 +20,17 @@ export function randomKillChar(): string { export function KillConfirmCard({ char, onCancel, exit }: { char: string; onCancel?: () => void; exit?: KillExit }) { return ( - -

Confirm kill

+ +

+ Confirm kill +

); } - diff --git a/lib/src/components/design.tsx b/lib/src/components/design.tsx index 9cd64c1..2c90e57 100644 --- a/lib/src/components/design.tsx +++ b/lib/src/components/design.tsx @@ -1,7 +1,8 @@ import { clsx } from 'clsx'; import { tv, type VariantProps } from 'tailwind-variants'; +import { XIcon } from '@phosphor-icons/react'; import { forwardRef, useEffect, useLayoutEffect, useState } from 'react'; -import type { CSSProperties, HTMLAttributes, ReactNode, RefObject } from 'react'; +import type { ButtonHTMLAttributes, CSSProperties, HTMLAttributes, ReactNode, RefObject } from 'react'; // App-wide type scale, color strategy, and chrome conventions: see // docs/specs/theme.md and AGENTS.md. @@ -83,6 +84,7 @@ export const modalSurface = tv({ base: 'rounded-lg border border-border bg-surface-raised font-mono text-foreground shadow-lg', variants: { padding: { + none: 'p-0', compact: 'p-3', default: 'p-4', spacious: 'px-6 py-4', @@ -118,6 +120,30 @@ export const modalIconButton = tv({ base: 'shrink-0 rounded p-0.5 text-muted transition-colors hover:bg-foreground/10 hover:text-foreground focus-visible:outline focus-visible:outline-1 focus-visible:outline-focus-ring', }); +export type ModalCloseButtonProps = ButtonHTMLAttributes; + +export const ModalCloseButton = forwardRef( + function ModalCloseButton({ + children, + className, + type = 'button', + ...props + }, ref) { + const ariaLabel = props['aria-label'] ?? 'Close'; + return ( + + ); + }, +); + export function useMeasuredElementRect(element: HTMLElement | null): ModalRect | null { const [rect, setRect] = useState(null); diff --git a/lib/src/stories/KillModal.stories.tsx b/lib/src/stories/KillModal.stories.tsx index 8f0a5c8..9bea198 100644 --- a/lib/src/stories/KillModal.stories.tsx +++ b/lib/src/stories/KillModal.stories.tsx @@ -1,18 +1,20 @@ import type { Meta, StoryObj } from '@storybook/react'; +import { useState } from 'react'; import { KillConfirmCard, type KillExit } from '../components/KillConfirm'; +import { ModalOverlay } from '../components/design'; function KillModal({ char = 'G', onCancel, exit }: { char?: string; onCancel?: () => void; exit?: KillExit }) { + const [frameEl, setFrameEl] = useState(null); return ( -
+
{/* Simulated terminal content behind the overlay */}
user@dormouse:~$ npm run build
Building project...
- {/* Kill confirmation overlay — positioned over the pane */} -
+ -
+
); } diff --git a/standalone/src/UpdateDebugDialog.tsx b/standalone/src/UpdateDebugDialog.tsx index 6b794f1..c563936 100644 --- a/standalone/src/UpdateDebugDialog.tsx +++ b/standalone/src/UpdateDebugDialog.tsx @@ -1,5 +1,11 @@ import { useEffect, useRef, useState } from 'react'; -import { XIcon } from '@phosphor-icons/react'; +import { + ModalCloseButton, + ModalOverlay, + ModalSurface, + modalActionButton, + useModalFocusTrap, +} from '../../lib/src/components/design'; import { openIssueSearch } from './updater'; interface UpdateDebugDialogProps { @@ -10,14 +16,17 @@ interface UpdateDebugDialogProps { } export function UpdateDebugDialog({ open, onClose, failure, body }: UpdateDebugDialogProps) { - const dialogRef = useRef(null); + const dialogRef = useRef(null); + const closeButtonRef = useRef(null); const [copied, setCopied] = useState(false); + useModalFocusTrap(dialogRef, { + initialFocusRef: closeButtonRef, + onEscape: onClose, + }); + useEffect(() => { - const dialog = dialogRef.current; - if (!dialog) return; - if (open && !dialog.open) dialog.showModal(); - if (!open && dialog.open) dialog.close(); + if (open) closeButtonRef.current?.focus(); }, [open]); useEffect(() => { @@ -42,74 +51,75 @@ export function UpdateDebugDialog({ open, onClose, failure, body }: UpdateDebugD const errorPreview = failure.error ?? ''; - return ( - -
- Update failed - -
+ if (!open) return null; -
-
-

- We couldn't install v{failure.version}. The error was: -

-
-            {errorPreview || '(no error captured)'}
-          
+ return ( + + +
+

+ Update failed +

+
-
-

1. Search existing reports

-

- Someone may have already hit this — a quick search saves a duplicate report. -

- -
+
+
+

+ We couldn't install v{failure.version}. The error was: +

+
+              {errorPreview || '(no error captured)'}
+            
+
-
-

2. File a new bug

-

- If you can't find an existing bug,{' '} +

+

1. Search existing reports

+

+ Someone may have already hit this. A quick search saves a duplicate report. +

- {copied && — copied!} - {' '}and paste it into a new issue. -

-