|
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