|
1 | 1 | import React, { useEffect, useState } from 'react'; |
| 2 | +import { Button } from '@components/ui/button'; |
| 3 | +import { Share2Icon, CheckIcon } from 'lucide-react'; |
| 4 | +// Removed ToggleGroup in favor of Button to match Share style |
| 5 | +import { Alert, AlertDescription, AlertTitle } from '@components/ui/alert'; |
| 6 | +import { AlertCircleIcon } from 'lucide-react'; |
2 | 7 | import './ResultPanel.css'; |
3 | 8 |
|
4 | 9 | export interface Diagnostic { |
@@ -209,47 +214,77 @@ export const ResultPanel: React.FC<ResultPanelProps> = props => { |
209 | 214 | }); |
210 | 215 | }, [astTree]); |
211 | 216 |
|
| 217 | + // Share button state and handler |
| 218 | + const [shareCopied, setShareCopied] = useState(false); |
| 219 | + async function copyShareUrl() { |
| 220 | + try { |
| 221 | + const url = window.location.href; |
| 222 | + await copyToClipboard(url); |
| 223 | + setShareCopied(true); |
| 224 | + window.setTimeout(() => setShareCopied(false), 1500); |
| 225 | + } catch (e) { |
| 226 | + console.warn('Share failed', e); |
| 227 | + } |
| 228 | + } |
| 229 | + |
212 | 230 | return ( |
213 | 231 | <div className="result-panel"> |
214 | 232 | <div className="result-header"> |
215 | | - <div className="result-tabs"> |
216 | | - <div |
217 | | - className={`result-tab ${activeTab === 'lint' ? 'active' : ''}`} |
| 233 | + <div className="flex items-center gap-2"> |
| 234 | + <Button |
| 235 | + type="button" |
| 236 | + variant={activeTab === 'lint' ? 'default' : 'outline'} |
| 237 | + size="sm" |
218 | 238 | onClick={() => setActiveTab('lint')} |
219 | | - title="Errors" |
| 239 | + aria-pressed={activeTab === 'lint'} |
220 | 240 | > |
221 | 241 | Errors |
222 | | - </div> |
223 | | - <div |
224 | | - className={`result-tab ${activeTab === 'ast' ? 'active' : ''}`} |
| 242 | + </Button> |
| 243 | + <Button |
| 244 | + type="button" |
| 245 | + variant={activeTab === 'ast' ? 'default' : 'outline'} |
| 246 | + size="sm" |
225 | 247 | onClick={() => setActiveTab('ast')} |
226 | | - title="AST" |
| 248 | + aria-pressed={activeTab === 'ast'} |
227 | 249 | > |
228 | | - AST(tsgo) |
229 | | - </div> |
| 250 | + AST |
| 251 | + </Button> |
| 252 | + </div> |
| 253 | + <div className="result-actions"> |
| 254 | + <Button |
| 255 | + type="button" |
| 256 | + variant="outline" |
| 257 | + size="sm" |
| 258 | + onClick={() => copyShareUrl()} |
| 259 | + title={shareCopied ? 'Copied link' : 'Copy shareable link'} |
| 260 | + > |
| 261 | + {shareCopied ? ( |
| 262 | + <CheckIcon className="size-4" /> |
| 263 | + ) : ( |
| 264 | + <Share2Icon className="size-4" /> |
| 265 | + )} |
| 266 | + {shareCopied ? 'Copied' : 'Share'} |
| 267 | + </Button> |
230 | 268 | </div> |
231 | 269 | </div> |
232 | 270 |
|
233 | 271 | {initialized ? ( |
234 | 272 | <div className="result-content"> |
235 | 273 | {error && ( |
236 | | - <div className="error-message"> |
237 | | - <div className="error-icon">⚠️</div> |
238 | | - <div className="error-text"> |
239 | | - <strong>Error:</strong> {error} |
240 | | - </div> |
241 | | - </div> |
| 274 | + <Alert variant="destructive"> |
| 275 | + <AlertCircleIcon /> |
| 276 | + <AlertTitle>Error</AlertTitle> |
| 277 | + <AlertDescription>{error}</AlertDescription> |
| 278 | + </Alert> |
242 | 279 | )} |
243 | 280 |
|
244 | 281 | {!error && activeTab === 'lint' && ( |
245 | 282 | <div className="lint-results"> |
246 | 283 | {diagnostics.length === 0 ? ( |
247 | | - <div className="success-message"> |
248 | | - <div className="success-icon">✅</div> |
249 | | - <div className="success-text"> |
250 | | - <strong>No issues found!</strong> Your code looks good. |
251 | | - </div> |
252 | | - </div> |
| 284 | + <Alert> |
| 285 | + <AlertTitle>No issues found!</AlertTitle> |
| 286 | + <AlertDescription>Your code looks good.</AlertDescription> |
| 287 | + </Alert> |
253 | 288 | ) : ( |
254 | 289 | <div className="diagnostics-list"> |
255 | 290 | {diagnostics.map((diagnostic, index) => ( |
@@ -308,3 +343,24 @@ export const ResultPanel: React.FC<ResultPanelProps> = props => { |
308 | 343 | </div> |
309 | 344 | ); |
310 | 345 | }; |
| 346 | + |
| 347 | +function copyToClipboard(text: string) { |
| 348 | + if (navigator.clipboard?.writeText) |
| 349 | + return navigator.clipboard.writeText(text); |
| 350 | + return new Promise<void>((resolve, reject) => { |
| 351 | + try { |
| 352 | + const ta = document.createElement('textarea'); |
| 353 | + ta.value = text; |
| 354 | + ta.setAttribute('readonly', ''); |
| 355 | + ta.style.position = 'absolute'; |
| 356 | + ta.style.left = '-9999px'; |
| 357 | + document.body.appendChild(ta); |
| 358 | + ta.select(); |
| 359 | + document.execCommand('copy'); |
| 360 | + document.body.removeChild(ta); |
| 361 | + resolve(); |
| 362 | + } catch (e) { |
| 363 | + reject(e); |
| 364 | + } |
| 365 | + }); |
| 366 | +} |
0 commit comments