Skip to content

Commit bfe0366

Browse files
authored
feat: add copy button for copying error name + message (#76)
1 parent d7352d4 commit bfe0366

File tree

3 files changed

+97
-0
lines changed

3 files changed

+97
-0
lines changed

src/public/error_info/script.js

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
function copyErrorMessage(button) {
2+
const errorText = button.dataset.errorText;
3+
4+
navigator.clipboard.writeText(errorText)
5+
.then(() => {
6+
button.classList.add('copied');
7+
setTimeout(() => button.classList.remove('copied'), 2000);
8+
})
9+
.catch(() => {
10+
button.classList.add('copied');
11+
setTimeout(() => button.classList.remove('copied'), 2000);
12+
});
13+
}

src/public/error_info/style.css

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
gap: 12px;
2323
-webkit-font-smoothing: antialiased;
2424
-moz-osx-font-smoothing: grayscale;
25+
position: relative;
2526
}
2627
#error-message svg {
2728
margin-top: 1.5px;
@@ -48,6 +49,68 @@
4849
color: var(--links-fg);
4950
}
5051

52+
#copy-error-btn {
53+
background: transparent;
54+
border: 1px solid var(--border);
55+
border-radius: 6px;
56+
padding: 8px;
57+
cursor: pointer;
58+
display: flex;
59+
align-items: center;
60+
justify-content: center;
61+
margin-left: auto;
62+
color: var(--text-fg);
63+
transition: all 0.2s ease;
64+
opacity: 0.7;
65+
position: relative;
66+
}
67+
68+
#copy-error-btn:hover {
69+
opacity: 1;
70+
background: var(--bg-hover);
71+
border-color: var(--border-hover);
72+
}
73+
74+
#copy-error-btn:active {
75+
transform: scale(0.95);
76+
}
77+
78+
#copy-error-btn.copied {
79+
background: var(--success-bg);
80+
color: var(--success-fg);
81+
border-color: var(--success-border);
82+
opacity: 1;
83+
}
84+
85+
#copy-error-btn.copied::after {
86+
content: 'Copied!';
87+
position: absolute;
88+
top: -35px;
89+
left: 50%;
90+
transform: translateX(-50%);
91+
background: var(--slate-12);
92+
color: var(--slate-1);
93+
padding: 4px 8px;
94+
border-radius: 4px;
95+
font-size: 12px;
96+
white-space: nowrap;
97+
opacity: 0;
98+
animation: copyFeedback 2s ease-in-out forwards;
99+
}
100+
101+
102+
@keyframes copyFeedback {
103+
0% { opacity: 0; transform: translateX(-50%) translateY(5px); }
104+
10% { opacity: 1; transform: translateX(-50%) translateY(0); }
105+
90% { opacity: 1; transform: translateX(-50%) translateY(0); }
106+
100% { opacity: 0; transform: translateX(-50%) translateY(-5px); }
107+
}
108+
109+
#copy-error-btn svg {
110+
width: 16px;
111+
height: 16px;
112+
}
113+
51114
@media (min-width: 1024px) {
52115
#error-hint {
53116
align-items: center;

src/templates/error_info/main.ts

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,12 +16,24 @@ const ERROR_ICON_SVG = `<svg xmlns="http://www.w3.org/2000/svg" aria-hidden="tru
1616

1717
const HINT_ICON_SVG = `<svg xmlns="http://www.w3.org/2000/svg" aria-hidden="true" width="24" height="24" fill="none"><path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="m21 2-1 1M3 2l1 1m17 13-1-1M3 16l1-1m5 3h6m-5 3h4M12 3C8 3 5.952 4.95 6 8c.023 1.487.5 2.5 1.5 3.5S9 13 9 15h6c0-2 .5-2.5 1.5-3.5h0c1-1 1.477-2.013 1.5-3.5.048-3.05-2-5-6-5Z"/></svg>`
1818

19+
const COPY_ICON_SVG = `<svg xmlns="http://www.w3.org/2000/svg" aria-hidden="true" width="16" height="16" fill="none" viewBox="0 0 24 24"><path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M16 4h2a2 2 0 0 1 2 2v14a2 2 0 0 1-2 2H6a2 2 0 0 1-2-2V6a2 2 0 0 1 2-2h2m8 0V2a2 2 0 0 0-2-2H10a2 2 0 0 0-2 2v2m8 0H8"/></svg>`
20+
21+
function htmlAttributeEscape(value: string): string {
22+
return value
23+
.replace(/&/g, '&amp;')
24+
.replace(/"/g, '&quot;')
25+
.replace(/'/g, '&#x27;')
26+
.replace(/</g, '&lt;')
27+
.replace(/>/g, '&gt;')
28+
}
29+
1930
/**
2031
* Displays the error info including the response status text,
2132
* error name, error message and the hint.
2233
*/
2334
export class ErrorInfo extends BaseComponent<ErrorInfoProps> {
2435
cssFile = new URL('./error_info/style.css', publicDirURL)
36+
jsFile = new URL('./error_info/script.js', publicDirURL)
2537

2638
/**
2739
* The toHTML method is used to output the HTML for the
@@ -38,6 +50,15 @@ export class ErrorInfo extends BaseComponent<ErrorInfoProps> {
3850
<h2 id="error-message">
3951
<span>${ERROR_ICON_SVG}</span>
4052
<span>${props.error.message}</span>
53+
<button
54+
id="copy-error-btn"
55+
data-error-text="${htmlAttributeEscape(`${props.error.name}: ${props.error.message}`)}"
56+
onclick="copyErrorMessage(this)"
57+
title="Copy error message"
58+
aria-label="Copy error message to clipboard"
59+
>
60+
${COPY_ICON_SVG}
61+
</button>
4162
</h2>
4263
${
4364
props.error.hint

0 commit comments

Comments
 (0)