Skip to content

Conversation

Copy link
Contributor

Copilot AI commented Jan 7, 2026

Adds the ability to copy an icon's SVG markup to clipboard from the preview website, in addition to the existing copy-icon-name functionality.

Changes

  • src/template/preview.hbs: Add two icon buttons at the bottom of each card for copying icon name and SVG
  • scripts/embed-svg-data.js: New build script that embeds all SVG content into the HTML as a JS object
  • package.json: Add embed-svg-data script to build pipeline

How it works

SVG data is embedded at build time via a placeholder pattern (matching the existing metadata embedding approach). Each icon card displays two buttons on hover:

  • Copy icon (left): Uses the copy codicon to copy the icon name to clipboard
  • File-code icon (right): Uses the file-code codicon to copy the SVG markup to clipboard
Original prompt

Can you please add a feature to the website so when you click on an icon, you will also have the option to copy the SVG for the icon to your clipboard (on top of the existing copy icon name to clipboard)?


✨ Let Copilot coding agent set things up for you — coding agent works faster and does higher quality work when set up for your repo.

Co-authored-by: hawkticehurst <39639992+hawkticehurst@users.noreply.github.com>
Copilot AI changed the title [WIP] Add feature to copy SVG to clipboard for icons Add copy SVG to clipboard option on icon click Jan 7, 2026
Copilot AI requested a review from hawkticehurst January 7, 2026 22:53
@hawkticehurst
Copy link
Member

@copilot for the UI changes can we actually instead show a two icon buttons at the bottom of each card that let's you copy SVG or copy name?

Use this screenshot as a reference for the design:

Screenshot 2026-01-07 at 2 56 50 PM

You should use the default codicons copy icon for copying the icon name, and I'll leave it to your discretion to pick an icon from the codicons library that makes sense for copying SVG

Copilot AI and others added 2 commits January 7, 2026 23:07
Co-authored-by: hawkticehurst <39639992+hawkticehurst@users.noreply.github.com>
Co-authored-by: hawkticehurst <39639992+hawkticehurst@users.noreply.github.com>
Copy link
Contributor Author

Copilot AI commented Jan 7, 2026

@copilot for the UI changes can we actually instead show a two icon buttons at the bottom of each card that let's you copy SVG or copy name?

Use this screenshot as a reference for the design:
...

Updated the UI to show two icon buttons at the bottom of each card that appear on hover (commit 2d1da31):

  • Copy icon (left): Uses the copy codicon to copy the icon name
  • File-code icon (right): Uses the file-code codicon to copy the SVG markup

Updated UI with icon buttons

@hawkticehurst hawkticehurst marked this pull request as ready for review January 8, 2026 07:13
@hawkticehurst hawkticehurst requested review from Copilot and removed request for hawkticehurst and mrleemurray January 8, 2026 07:13
@vs-code-engineering vs-code-engineering bot added this to the January 2026 milestone Jan 8, 2026
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR adds the ability to copy an icon's SVG markup to clipboard from the preview website. The implementation embeds all SVG content into the HTML at build time as a JavaScript object, with a fallback to fetch individual SVG files if needed.

Key Changes:

  • New copy functionality with two buttons per icon: one for copying the icon name and one for copying the SVG markup
  • Build-time SVG data embedding via a new script that follows the existing metadata embedding pattern
  • Updated UI with hover-triggered copy buttons instead of click-on-icon behavior

Reviewed changes

Copilot reviewed 3 out of 3 changed files in this pull request and generated 9 comments.

File Description
src/template/preview.hbs Added CSS styles for copy buttons, HTML structure for dual-button interface, and JavaScript event handlers for copying icon names and SVG content with clipboard API and fallback support
scripts/embed-svg-data.js New build script that reads all SVG files from src/icons and embeds their content into the HTML file as a JavaScript object
package.json Integrated the embed-svg-data script into the build pipeline after embed-metadata
Comments suppressed due to low confidence (1)

src/template/preview.hbs:673

  • The fetch promise chain doesn't check if the response is successful before calling response.text(). If the fetch returns a 404 or other error status, the SVG content might be an error page HTML. Add a check for response.ok before processing the response.
                fetch('icons/' + iconName + '.svg')
                    .then(response => response.text())
                    .then(svg => {
                        copyToClipboard(svg, iconName + ' (SVG)');

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

function copyToClipboard(text, displayText) {
if (navigator.clipboard && navigator.clipboard.writeText) {
navigator.clipboard.writeText(text).then(function() {
notificationText.innerHTML = displayText;
Copy link

Copilot AI Jan 8, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The notification message uses the icon name directly in innerHTML without sanitization. While icon names are likely controlled data from the build process, if there's any possibility of user-controlled data in icon names, this could be an XSS vulnerability. Consider using textContent instead of innerHTML for displaying the icon name.

Copilot uses AI. Check for mistakes.
copyToClipboard(svgData[iconName], iconName + ' (SVG)');
} else {
// Fallback: try to fetch SVG if not in embedded data
fetch('icons/' + iconName + '.svg')
Copy link

Copilot AI Jan 8, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The fetch fallback for SVG data uses a relative path 'icons/' + iconName + '.svg' but there's no verification that this path is correct or that the icons directory is available at this location in the deployed site. Consider documenting where these files should be located or ensuring the build process copies them to the expected location.

This issue also appears in the following locations of the same file:

  • line 670

Copilot uses AI. Check for mistakes.
Comment on lines +265 to +272
<div class="copy-buttons">
<button class="copy-btn copy-name-btn" title="Copy icon name" aria-label="Copy icon name">
<i class="codicon codicon-copy" aria-hidden="true"></i>
</button>
<button class="copy-btn copy-svg-btn" title="Copy SVG" aria-label="Copy SVG">
<i class="codicon codicon-file-code" aria-hidden="true"></i>
</button>
</div>
Copy link

Copilot AI Jan 8, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The copy buttons are placed inside the .icon div but outside any semantic grouping. Consider wrapping the buttons in a container with role="group" and aria-label="Copy actions" to provide better context for screen reader users about what these buttons do in relation to the icon.

Copilot uses AI. Check for mistakes.
Comment on lines +655 to +681
copyNameBtn.onclick = function(e){
e.stopPropagation();
let iconName = this.closest('.icon').getAttribute('data-name');
copyToClipboard(iconName, iconName);
};
// Copy SVG button
let copySvgBtn = icon.querySelector('.copy-svg-btn');
copySvgBtn.onclick = function(e){
e.stopPropagation();
let iconName = this.closest('.icon').getAttribute('data-name');
if (svgData[iconName]) {
copyToClipboard(svgData[iconName], iconName + ' (SVG)');
} else {
// Fallback: try to fetch SVG if not in embedded data
fetch('icons/' + iconName + '.svg')
.then(response => response.text())
.then(svg => {
copyToClipboard(svg, iconName + ' (SVG)');
})
.catch(error => {
console.warn('Could not fetch SVG for ' + iconName, error);
notificationText.innerHTML = 'SVG not available';
animateNotification();
});
}
};
Copy link

Copilot AI Jan 8, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The code doesn't check if the querySelector calls return null before assigning onclick handlers. If the template structure changes or an icon doesn't have the expected buttons, this will throw an error. Add null checks before assigning the onclick handlers.

Suggested change
copyNameBtn.onclick = function(e){
e.stopPropagation();
let iconName = this.closest('.icon').getAttribute('data-name');
copyToClipboard(iconName, iconName);
};
// Copy SVG button
let copySvgBtn = icon.querySelector('.copy-svg-btn');
copySvgBtn.onclick = function(e){
e.stopPropagation();
let iconName = this.closest('.icon').getAttribute('data-name');
if (svgData[iconName]) {
copyToClipboard(svgData[iconName], iconName + ' (SVG)');
} else {
// Fallback: try to fetch SVG if not in embedded data
fetch('icons/' + iconName + '.svg')
.then(response => response.text())
.then(svg => {
copyToClipboard(svg, iconName + ' (SVG)');
})
.catch(error => {
console.warn('Could not fetch SVG for ' + iconName, error);
notificationText.innerHTML = 'SVG not available';
animateNotification();
});
}
};
if (copyNameBtn) {
copyNameBtn.onclick = function(e){
e.stopPropagation();
let iconName = this.closest('.icon').getAttribute('data-name');
copyToClipboard(iconName, iconName);
};
}
// Copy SVG button
let copySvgBtn = icon.querySelector('.copy-svg-btn');
if (copySvgBtn) {
copySvgBtn.onclick = function(e){
e.stopPropagation();
let iconName = this.closest('.icon').getAttribute('data-name');
if (svgData[iconName]) {
copyToClipboard(svgData[iconName], iconName + ' (SVG)');
} else {
// Fallback: try to fetch SVG if not in embedded data
fetch('icons/' + iconName + '.svg')
.then(response => response.text())
.then(svg => {
copyToClipboard(svg, iconName + ' (SVG)');
})
.catch(error => {
console.warn('Could not fetch SVG for ' + iconName, error);
notificationText.innerHTML = 'SVG not available';
animateNotification();
});
}
};
}

Copilot uses AI. Check for mistakes.
display: inline-block;
margin: 8px;
}
Copy link

Copilot AI Jan 8, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The removal of '.icon:hover { cursor: pointer; }' means icons no longer show a pointer cursor on hover, which might reduce discoverability of the copy functionality. Consider adding 'cursor: pointer' to the '.icon:hover .copy-buttons' selector or keeping a hover indicator on the icon itself.

Suggested change
.icon:hover {
cursor: pointer;
}

Copilot uses AI. Check for mistakes.
console.error('HTML file not found:', htmlPath);
process.exit(1);
}

Copy link

Copilot AI Jan 8, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The script checks if the HTML file exists but doesn't verify that the icons directory exists before attempting to read it. This could result in a cryptic error if the directory is missing. Consider adding a check similar to the HTML file verification.

Suggested change
if (!fs.existsSync(iconsDir)) {
console.error('Icons directory not found:', iconsDir);
process.exit(1);
}

Copilot uses AI. Check for mistakes.
bpasero
bpasero previously approved these changes Jan 8, 2026
alexr00
alexr00 previously approved these changes Jan 8, 2026
function copyToClipboard(text, displayText) {
if (navigator.clipboard && navigator.clipboard.writeText) {
navigator.clipboard.writeText(text).then(function() {
notificationText.innerHTML = displayText;

Check failure

Code scanning / CodeQL

DOM text reinterpreted as HTML High

DOM text
is reinterpreted as HTML without escaping meta-characters.
DOM text
is reinterpreted as HTML without escaping meta-characters.

Copilot Autofix

AI 2 days ago

In general, the fix is to avoid interpreting untrusted strings as HTML. Instead of assigning potentially tainted text to element.innerHTML, use element.textContent (or an equivalent safe text API) so that any HTML meta-characters are treated as plain text rather than markup.

Concretely, in src/template/preview.hbs, the function copyToClipboard uses notificationText.innerHTML = displayText; in three places (success in the modern clipboard path, success in the fallback, and in the SVG fetch error path). We should change all of these assignments to use notificationText.textContent = displayText;. This preserves the existing behavior of displaying the same text to the user while preventing any embedded HTML inside displayText (or the literal 'SVG not available') from being interpreted as markup. No additional methods or imports are needed; textContent is a standard DOM property. This single change addresses both reported alert variants because both ultimately use displayText in the same sink.

Suggested changeset 1
src/template/preview.hbs

Autofix patch

Autofix patch
Run the following command in your local git repository to apply this patch
cat << 'EOF' | git apply
diff --git a/src/template/preview.hbs b/src/template/preview.hbs
--- a/src/template/preview.hbs
+++ b/src/template/preview.hbs
@@ -629,7 +629,7 @@
     function copyToClipboard(text, displayText) {
         if (navigator.clipboard && navigator.clipboard.writeText) {
             navigator.clipboard.writeText(text).then(function() {
-                notificationText.innerHTML = displayText;
+                notificationText.textContent = displayText;
                 animateNotification();
             }).catch(function() {
                 // Fallback to old method
@@ -637,7 +637,7 @@
                 copier.select();
                 copier.setSelectionRange(0, 99999);
                 document.execCommand('copy');
-                notificationText.innerHTML = displayText;
+                notificationText.textContent = displayText;
                 animateNotification();
             });
         } else {
@@ -645,7 +645,7 @@
             copier.select();
             copier.setSelectionRange(0, 99999);
             document.execCommand('copy');
-            notificationText.innerHTML = displayText;
+            notificationText.textContent = displayText;
             animateNotification();
         }
     }
@@ -678,7 +678,7 @@
                     })
                     .catch(error => {
                         console.warn('Could not fetch SVG for ' + iconName, error);
-                        notificationText.innerHTML = 'SVG not available';
+                        notificationText.textContent = 'SVG not available';
                         animateNotification();
                     });
             }
EOF
@@ -629,7 +629,7 @@
function copyToClipboard(text, displayText) {
if (navigator.clipboard && navigator.clipboard.writeText) {
navigator.clipboard.writeText(text).then(function() {
notificationText.innerHTML = displayText;
notificationText.textContent = displayText;
animateNotification();
}).catch(function() {
// Fallback to old method
@@ -637,7 +637,7 @@
copier.select();
copier.setSelectionRange(0, 99999);
document.execCommand('copy');
notificationText.innerHTML = displayText;
notificationText.textContent = displayText;
animateNotification();
});
} else {
@@ -645,7 +645,7 @@
copier.select();
copier.setSelectionRange(0, 99999);
document.execCommand('copy');
notificationText.innerHTML = displayText;
notificationText.textContent = displayText;
animateNotification();
}
}
@@ -678,7 +678,7 @@
})
.catch(error => {
console.warn('Could not fetch SVG for ' + iconName, error);
notificationText.innerHTML = 'SVG not available';
notificationText.textContent = 'SVG not available';
animateNotification();
});
}
Copilot is powered by AI and may make mistakes. Always verify output.
copier.select();
copier.setSelectionRange(0, 99999);
document.execCommand('copy');
notificationText.innerHTML = displayText;

Check failure

Code scanning / CodeQL

DOM text reinterpreted as HTML High

DOM text
is reinterpreted as HTML without escaping meta-characters.
DOM text
is reinterpreted as HTML without escaping meta-characters.

Copilot Autofix

AI 2 days ago

General approach: Avoid interpreting untrusted text as HTML. Instead of assigning displayText (which may come from data-name) to notificationText.innerHTML, assign it to notificationText.textContent so that any meta-characters are treated as literal text rather than markup. This preserves the visual behavior (showing a notification with the icon name or message) while eliminating the XSS sink.

Detailed best fix: In copyToClipboard, all three assignments to notificationText.innerHTML = displayText; should be changed to notificationText.textContent = displayText;. Likewise, in the SVG fetch catch handler, notificationText.innerHTML = 'SVG not available'; should be changed to notificationText.textContent = 'SVG not available';. These are all in src/template/preview.hbs around lines 631–641 and 679–682. No additional functions or imports are needed; textContent is a standard DOM property and behaves the same across modern browsers. This change does not alter the logic except to prevent HTML parsing of the message text.

Suggested changeset 1
src/template/preview.hbs

Autofix patch

Autofix patch
Run the following command in your local git repository to apply this patch
cat << 'EOF' | git apply
diff --git a/src/template/preview.hbs b/src/template/preview.hbs
--- a/src/template/preview.hbs
+++ b/src/template/preview.hbs
@@ -629,7 +629,7 @@
     function copyToClipboard(text, displayText) {
         if (navigator.clipboard && navigator.clipboard.writeText) {
             navigator.clipboard.writeText(text).then(function() {
-                notificationText.innerHTML = displayText;
+                notificationText.textContent = displayText;
                 animateNotification();
             }).catch(function() {
                 // Fallback to old method
@@ -637,7 +637,7 @@
                 copier.select();
                 copier.setSelectionRange(0, 99999);
                 document.execCommand('copy');
-                notificationText.innerHTML = displayText;
+                notificationText.textContent = displayText;
                 animateNotification();
             });
         } else {
@@ -645,7 +645,7 @@
             copier.select();
             copier.setSelectionRange(0, 99999);
             document.execCommand('copy');
-            notificationText.innerHTML = displayText;
+            notificationText.textContent = displayText;
             animateNotification();
         }
     }
@@ -678,7 +678,7 @@
                     })
                     .catch(error => {
                         console.warn('Could not fetch SVG for ' + iconName, error);
-                        notificationText.innerHTML = 'SVG not available';
+                        notificationText.textContent = 'SVG not available';
                         animateNotification();
                     });
             }
EOF
@@ -629,7 +629,7 @@
function copyToClipboard(text, displayText) {
if (navigator.clipboard && navigator.clipboard.writeText) {
navigator.clipboard.writeText(text).then(function() {
notificationText.innerHTML = displayText;
notificationText.textContent = displayText;
animateNotification();
}).catch(function() {
// Fallback to old method
@@ -637,7 +637,7 @@
copier.select();
copier.setSelectionRange(0, 99999);
document.execCommand('copy');
notificationText.innerHTML = displayText;
notificationText.textContent = displayText;
animateNotification();
});
} else {
@@ -645,7 +645,7 @@
copier.select();
copier.setSelectionRange(0, 99999);
document.execCommand('copy');
notificationText.innerHTML = displayText;
notificationText.textContent = displayText;
animateNotification();
}
}
@@ -678,7 +678,7 @@
})
.catch(error => {
console.warn('Could not fetch SVG for ' + iconName, error);
notificationText.innerHTML = 'SVG not available';
notificationText.textContent = 'SVG not available';
animateNotification();
});
}
Copilot is powered by AI and may make mistakes. Always verify output.
document.execCommand('copy');
notificationText.innerHTML = name;
notificationText.innerHTML = displayText;

Check failure

Code scanning / CodeQL

DOM text reinterpreted as HTML High

DOM text
is reinterpreted as HTML without escaping meta-characters.
DOM text
is reinterpreted as HTML without escaping meta-characters.

Copilot Autofix

AI 2 days ago

In general, to fix “DOM text reinterpreted as HTML” issues, avoid passing untrusted strings to APIs that interpret them as HTML (innerHTML, outerHTML, jQuery’s html(), etc.). Instead, insert them as plain text using textContent, innerText, or appropriate escaping, so that any meta-characters are rendered literally rather than executed.

For this specific code, the displayText argument to copyToClipboard is derived from data-name and then used for user-visible notifications only. There is no requirement here to support HTML formatting in the notification; it should just display plain text like the icon name or “SVG not available”. Therefore, the best fix is to replace notificationText.innerHTML = displayText; with notificationText.textContent = displayText; in all places where displayText (or a string built from it) is shown. This preserves all current behavior (user still sees the same message, clipboard functionality is unchanged), but ensures that any special characters in data-name are not interpreted as HTML.

Concretely, in src/template/preview.hbs:

  • In the copyToClipboard function:
    • On the success path of navigator.clipboard.writeText, change notificationText.innerHTML = displayText; to notificationText.textContent = displayText;.
    • On the catch (fallback) path, change the second notificationText.innerHTML = displayText; similarly.
    • In the branch for environments without navigator.clipboard, change notificationText.innerHTML = displayText; likewise.
  • In the fetch error handler in the SVG copy code, change notificationText.innerHTML = 'SVG not available'; to notificationText.textContent = 'SVG not available';.

No new methods, imports, or definitions are required; textContent is a standard DOM property.


Suggested changeset 1
src/template/preview.hbs

Autofix patch

Autofix patch
Run the following command in your local git repository to apply this patch
cat << 'EOF' | git apply
diff --git a/src/template/preview.hbs b/src/template/preview.hbs
--- a/src/template/preview.hbs
+++ b/src/template/preview.hbs
@@ -629,7 +629,7 @@
     function copyToClipboard(text, displayText) {
         if (navigator.clipboard && navigator.clipboard.writeText) {
             navigator.clipboard.writeText(text).then(function() {
-                notificationText.innerHTML = displayText;
+                notificationText.textContent = displayText;
                 animateNotification();
             }).catch(function() {
                 // Fallback to old method
@@ -637,7 +637,7 @@
                 copier.select();
                 copier.setSelectionRange(0, 99999);
                 document.execCommand('copy');
-                notificationText.innerHTML = displayText;
+                notificationText.textContent = displayText;
                 animateNotification();
             });
         } else {
@@ -645,7 +645,7 @@
             copier.select();
             copier.setSelectionRange(0, 99999);
             document.execCommand('copy');
-            notificationText.innerHTML = displayText;
+            notificationText.textContent = displayText;
             animateNotification();
         }
     }
@@ -678,7 +678,7 @@
                     })
                     .catch(error => {
                         console.warn('Could not fetch SVG for ' + iconName, error);
-                        notificationText.innerHTML = 'SVG not available';
+                        notificationText.textContent = 'SVG not available';
                         animateNotification();
                     });
             }
EOF
@@ -629,7 +629,7 @@
function copyToClipboard(text, displayText) {
if (navigator.clipboard && navigator.clipboard.writeText) {
navigator.clipboard.writeText(text).then(function() {
notificationText.innerHTML = displayText;
notificationText.textContent = displayText;
animateNotification();
}).catch(function() {
// Fallback to old method
@@ -637,7 +637,7 @@
copier.select();
copier.setSelectionRange(0, 99999);
document.execCommand('copy');
notificationText.innerHTML = displayText;
notificationText.textContent = displayText;
animateNotification();
});
} else {
@@ -645,7 +645,7 @@
copier.select();
copier.setSelectionRange(0, 99999);
document.execCommand('copy');
notificationText.innerHTML = displayText;
notificationText.textContent = displayText;
animateNotification();
}
}
@@ -678,7 +678,7 @@
})
.catch(error => {
console.warn('Could not fetch SVG for ' + iconName, error);
notificationText.innerHTML = 'SVG not available';
notificationText.textContent = 'SVG not available';
animateNotification();
});
}
Copilot is powered by AI and may make mistakes. Always verify output.
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
@mrleemurray mrleemurray dismissed stale reviews from alexr00 and bpasero via ed35e25 January 13, 2026 11:10
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
@mrleemurray mrleemurray merged commit 67d7817 into main Jan 14, 2026
5 of 6 checks passed
@mrleemurray mrleemurray deleted the copilot/add-copy-svg-feature branch January 14, 2026 17:56
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

6 participants