11import { useRef } from 'react' ;
2- import { ProhibitIcon , WarningOctagonIcon , XIcon } from '@phosphor-icons/react' ;
2+ import { ProhibitIcon , WarningOctagonIcon } from '@phosphor-icons/react' ;
33import type { DisplayMatchVerdict , ExternalUriDecision } from '../lib/external-links' ;
44import {
5- ModalOverlay ,
6- ModalSurface ,
5+ ModalCloseButton ,
6+ ModalFrame ,
7+ ModalReviewBlock ,
78 modalActionButton ,
8- modalIconButton ,
9- useModalFocusTrap ,
109} from './design' ;
1110
12- export interface ExternalLinkDialogRequest {
11+ export interface ExternalLinkModalRequest {
1312 uri : string ;
1413 displayText : string ;
1514 verdict : DisplayMatchVerdict ;
@@ -41,16 +40,15 @@ function schemePrefix(scheme: string, uri: string): string {
4140 return uri . slice ( scheme . length + 1 ) . startsWith ( '//' ) ? `${ scheme } ://` : `${ scheme } :` ;
4241}
4342
44- export function ExternalLinkDialog ( {
43+ export function ExternalLinkModal ( {
4544 request,
4645 onCancel,
4746 onConfirm,
4847} : {
49- request : ExternalLinkDialogRequest ;
48+ request : ExternalLinkModalRequest ;
5049 onCancel : ( ) => void ;
5150 onConfirm : ( ) => void ;
5251} ) {
53- const dialogRef = useRef < HTMLDivElement > ( null ) ;
5452 const primaryButtonRef = useRef < HTMLButtonElement > ( null ) ;
5553 const secondaryButtonRef = useRef < HTMLButtonElement > ( null ) ;
5654
@@ -63,111 +61,99 @@ export function ExternalLinkDialog({
6361 ? pickOpenButtonNoun ( openableDecision . scheme , openableDecision . uri )
6462 : 'URL' ;
6563
66- useModalFocusTrap ( dialogRef , {
67- // Deceptive case: focus the copy action so a default Enter doesn't dismiss
68- // silently. Everywhere else: focus the safe affordance (Cancel/Close).
69- initialFocusRef : isDeceptive ? primaryButtonRef : secondaryButtonRef ,
70- onEscape : onCancel ,
71- } ) ;
72-
7364 const handleCopy = ( ) => {
7465 void navigator . clipboard . writeText ( request . uri ) ;
7566 onCancel ( ) ;
7667 } ;
7768
7869 return (
79- < ModalOverlay zIndex = { 9999 } backdrop = "strong" className = "px-4 py-6" >
80- < ModalSurface
81- ref = { dialogRef }
82- role = "dialog"
83- aria-modal = "true"
84- aria-labelledby = "external-link-dialog-title"
85- elevation = "modal"
86- className = "w-full max-w-[34rem]"
87- >
88- < div className = "flex items-start gap-3" >
89- < h2
90- id = "external-link-dialog-title"
91- className = "min-w-0 flex-1 text-sm leading-5 text-foreground"
92- >
93- { isDeceptive ? (
94- < DeceptiveTitle displayText = { request . displayText } />
95- ) : blockedDecision ? (
96- < BlockedTitle reason = { blockedDecision . reason } />
97- ) : (
98- < OpenTitle verdict = { verdict } displayText = { request . displayText } />
99- ) }
100- </ h2 >
101- < button
102- type = "button"
103- aria-label = "Close"
104- className = { modalIconButton ( ) }
105- onClick = { onCancel }
106- >
107- < XIcon size = { 13 } weight = "bold" />
108- </ button >
109- </ div >
110-
111- { /* Bordered nested box: explicit exception to the bg-only chrome rule
112- in DESIGN.md. The URL is the literal artifact the user is being
113- asked to scrutinize, and a framed box reads better than a bare
114- bg-shift in this high-stakes context. */ }
115- < div className = "mt-3 max-h-40 overflow-auto whitespace-pre-wrap break-all rounded border border-border bg-app-bg px-2.5 py-2 text-sm leading-relaxed text-foreground" >
116- { displayUri }
117- </ div >
118-
119- < div className = "mt-4 flex justify-end gap-2 text-xs" >
70+ < ModalFrame
71+ titleId = "external-link-modal-title"
72+ layer = "critical"
73+ backdrop = "strong"
74+ elevation = "modal"
75+ overlayClassName = "px-4 py-6"
76+ className = "w-full max-w-[34rem]"
77+ // Deceptive case: focus the copy action so a default Enter doesn't dismiss
78+ // silently. Everywhere else: focus the safe affordance (Cancel/Close).
79+ initialFocusRef = { isDeceptive ? primaryButtonRef : secondaryButtonRef }
80+ onEscape = { onCancel }
81+ >
82+ < div className = "flex items-start gap-3" >
83+ < h2
84+ id = "external-link-modal-title"
85+ className = "min-w-0 flex-1 text-sm leading-5 text-foreground"
86+ >
12087 { isDeceptive ? (
121- < >
122- < button
123- ref = { secondaryButtonRef }
124- type = "button"
125- onClick = { onCancel }
126- className = { `${ modalActionButton ( { tone : 'secondary' } ) } min-w-[5rem]` }
127- >
128- Close
129- </ button >
130- < button
131- ref = { primaryButtonRef }
132- type = "button"
133- onClick = { handleCopy }
134- className = { modalActionButton ( { tone : 'primary' } ) }
135- >
136- Copy deceptive URL to clipboard
137- </ button >
138- </ >
139- ) : openableDecision ? (
140- < >
141- < button
142- ref = { secondaryButtonRef }
143- type = "button"
144- onClick = { onCancel }
145- className = { `${ modalActionButton ( { tone : 'secondary' } ) } min-w-[5rem]` }
146- >
147- Cancel
148- </ button >
149- < button
150- ref = { primaryButtonRef }
151- type = "button"
152- onClick = { onConfirm }
153- className = { `${ modalActionButton ( { tone : 'primary' } ) } min-w-[5rem]` }
154- >
155- { 'Open ' } { buttonNoun }
156- </ button >
157- </ >
88+ < DeceptiveTitle displayText = { request . displayText } />
89+ ) : blockedDecision ? (
90+ < BlockedTitle reason = { blockedDecision . reason } />
15891 ) : (
92+ < OpenTitle verdict = { verdict } displayText = { request . displayText } />
93+ ) }
94+ </ h2 >
95+ < ModalCloseButton onClick = { onCancel } />
96+ </ div >
97+
98+ { /* Bordered nested box: explicit exception to the bg-only chrome rule
99+ in DESIGN.md. The URL is the literal artifact the user is being
100+ asked to scrutinize, and a framed box reads better than a bare
101+ bg-shift in this high-stakes context. */ }
102+ < ModalReviewBlock className = "mt-3" wrap = "breakAll" >
103+ { displayUri }
104+ </ ModalReviewBlock >
105+
106+ < div className = "mt-4 flex justify-end gap-2 text-xs" >
107+ { isDeceptive ? (
108+ < >
159109 < button
160110 ref = { secondaryButtonRef }
161111 type = "button"
162112 onClick = { onCancel }
163- className = { `${ modalActionButton ( { tone : 'primary ' } ) } min-w-[6rem ]` }
113+ className = { `${ modalActionButton ( { tone : 'secondary ' } ) } min-w-[5rem ]` }
164114 >
165115 Close
166116 </ button >
167- ) }
168- </ div >
169- </ ModalSurface >
170- </ ModalOverlay >
117+ < button
118+ ref = { primaryButtonRef }
119+ type = "button"
120+ onClick = { handleCopy }
121+ className = { modalActionButton ( { tone : 'primary' } ) }
122+ >
123+ Copy deceptive URL to clipboard
124+ </ button >
125+ </ >
126+ ) : openableDecision ? (
127+ < >
128+ < button
129+ ref = { secondaryButtonRef }
130+ type = "button"
131+ onClick = { onCancel }
132+ className = { `${ modalActionButton ( { tone : 'secondary' } ) } min-w-[5rem]` }
133+ >
134+ Cancel
135+ </ button >
136+ < button
137+ ref = { primaryButtonRef }
138+ type = "button"
139+ onClick = { onConfirm }
140+ className = { `${ modalActionButton ( { tone : 'primary' } ) } min-w-[5rem]` }
141+ >
142+ { 'Open ' } { buttonNoun }
143+ </ button >
144+ </ >
145+ ) : (
146+ < button
147+ ref = { secondaryButtonRef }
148+ type = "button"
149+ onClick = { onCancel }
150+ className = { `${ modalActionButton ( { tone : 'primary' } ) } min-w-[6rem]` }
151+ >
152+ Close
153+ </ button >
154+ ) }
155+ </ div >
156+ </ ModalFrame >
171157 ) ;
172158}
173159
0 commit comments