From 02750142be5874baa09b721d0cd72c79050691b5 Mon Sep 17 00:00:00 2001 From: Nick Bradley Date: Mon, 2 Feb 2026 16:08:00 +0000 Subject: [PATCH 1/5] chore: refactor to remove inline css and js from webviews --- src/commands/previewAsset.ts | 932 ++++--------- src/commands/uploadWidget.ts | 1589 ++++------------------- src/commands/welcomeScreen.ts | 1089 ++++------------ src/webview/baseStyles.ts | 337 +++++ src/webview/components/badge.ts | 334 +++++ src/webview/components/button.ts | 333 +++++ src/webview/components/card.ts | 316 +++++ src/webview/components/dropZone.ts | 440 +++++++ src/webview/components/index.ts | 155 +++ src/webview/components/infoRow.ts | 274 ++++ src/webview/components/input.ts | 420 ++++++ src/webview/components/layout.ts | 630 +++++++++ src/webview/components/lightbox.ts | 565 ++++++++ src/webview/components/progressBar.ts | 308 +++++ src/webview/components/tabs.ts | 302 +++++ src/webview/icons.ts | 179 +++ src/webview/index.ts | 179 +++ src/webview/media/styles/base.css | 167 +++ src/webview/media/styles/components.css | 1125 ++++++++++++++++ src/webview/media/styles/main.css | 8 + src/webview/media/styles/tokens.css | 90 ++ src/webview/scripts/index.ts | 7 + src/webview/scripts/previewAsset.ts | 53 + src/webview/scripts/uploadWidget.ts | 432 ++++++ src/webview/scripts/welcomeScreen.ts | 103 ++ src/webview/tokens.ts | 228 ++++ src/webview/utils/clipboard.ts | 118 ++ src/webview/utils/helpers.ts | 136 ++ src/webview/utils/index.ts | 23 + src/webview/utils/messaging.ts | 104 ++ src/webview/webviewUtils.ts | 148 +++ 31 files changed, 8310 insertions(+), 2814 deletions(-) create mode 100644 src/webview/baseStyles.ts create mode 100644 src/webview/components/badge.ts create mode 100644 src/webview/components/button.ts create mode 100644 src/webview/components/card.ts create mode 100644 src/webview/components/dropZone.ts create mode 100644 src/webview/components/index.ts create mode 100644 src/webview/components/infoRow.ts create mode 100644 src/webview/components/input.ts create mode 100644 src/webview/components/layout.ts create mode 100644 src/webview/components/lightbox.ts create mode 100644 src/webview/components/progressBar.ts create mode 100644 src/webview/components/tabs.ts create mode 100644 src/webview/icons.ts create mode 100644 src/webview/index.ts create mode 100644 src/webview/media/styles/base.css create mode 100644 src/webview/media/styles/components.css create mode 100644 src/webview/media/styles/main.css create mode 100644 src/webview/media/styles/tokens.css create mode 100644 src/webview/scripts/index.ts create mode 100644 src/webview/scripts/previewAsset.ts create mode 100644 src/webview/scripts/uploadWidget.ts create mode 100644 src/webview/scripts/welcomeScreen.ts create mode 100644 src/webview/tokens.ts create mode 100644 src/webview/utils/clipboard.ts create mode 100644 src/webview/utils/helpers.ts create mode 100644 src/webview/utils/index.ts create mode 100644 src/webview/utils/messaging.ts create mode 100644 src/webview/webviewUtils.ts diff --git a/src/commands/previewAsset.ts b/src/commands/previewAsset.ts index 68f7ccd..1bab3bd 100644 --- a/src/commands/previewAsset.ts +++ b/src/commands/previewAsset.ts @@ -1,713 +1,329 @@ import * as vscode from "vscode"; +import { createWebviewDocument } from "../webview/webviewUtils"; +import { escapeHtml, formatFileSize } from "../webview/utils/helpers"; +import { assetIcons, actionIcons } from "../webview/icons"; type AssetData = { - public_id: string, - displayType: "image" | "video" | string, - secure_url: string, - optimized_url: string, - bytes: number, - width: number, - height: number, - filename: string, - format?: string, - resource_type?: string + public_id: string; + displayType: "image" | "video" | string; + secure_url: string; + optimized_url: string; + bytes: number; + width: number; + height: number; + filename: string; + format?: string; + resource_type?: string; + tags?: string[]; + context?: Record; + metadata?: Record; }; /** * Map of open preview panels by public_id. - * Prevents opening multiple panels for the same asset. */ const openPanels: Map = new Map(); +/** + * Get icon for asset type using centralized icons. + */ +function getAssetIcon(type: string): string { + switch (type) { + case "image": + return assetIcons.image("lg"); + case "video": + return assetIcons.video("lg"); + default: + return assetIcons.file("lg"); + } +} + function registerPreview(context: vscode.ExtensionContext) { context.subscriptions.push( - vscode.commands.registerCommand("cloudinary.openAsset", (asset: AssetData) => { - const publicId = asset.public_id; - - // Check if panel for this asset already exists - const existingPanel = openPanels.get(publicId); - if (existingPanel) { - // Reveal the existing panel - existingPanel.reveal(vscode.ViewColumn.One); - return; + vscode.commands.registerCommand( + "cloudinary.openAsset", + (asset: AssetData) => { + const publicId = asset.public_id; + + // Check if panel for this asset already exists + const existingPanel = openPanels.get(publicId); + if (existingPanel) { + existingPanel.reveal(vscode.ViewColumn.One); + return; + } + + // Get short display name for tab + const shortName = asset.public_id.includes("/") + ? asset.public_id.split("/").pop() + : asset.public_id; + + // Create new panel + const panel = vscode.window.createWebviewPanel( + "cloudinaryAssetPreview", + shortName || asset.public_id, + vscode.ViewColumn.One, + { + enableScripts: true, + localResourceRoots: [ + vscode.Uri.joinPath(context.extensionUri, "src", "webview", "media"), + ], + } + ); + + // Set the panel icon based on asset type + const iconFile = + asset.displayType === "image" + ? "icon-image.svg" + : asset.displayType === "video" + ? "icon-video.svg" + : "icon-file.svg"; + panel.iconPath = vscode.Uri.joinPath( + context.extensionUri, + "resources", + iconFile + ); + + // Track this panel + openPanels.set(publicId, panel); + + // Remove from tracking when disposed + panel.onDidDispose(() => { + openPanels.delete(publicId); + }); + + // Set the HTML content + panel.webview.html = createWebviewDocument({ + title: asset.public_id, + webview: panel.webview, + extensionUri: context.extensionUri, + bodyContent: getPreviewContent(asset), + bodyClass: "layout-centered", + inlineScript: "initCommon();", + }); + + // Handle messages from webview + panel.webview.onDidReceiveMessage(async (message) => { + if (message.command === "copyToClipboard" && message.text) { + await vscode.env.clipboard.writeText(message.text); + } + }); } + ) + ); +} - // Get short display name for tab - const shortName = asset.public_id.includes('/') - ? asset.public_id.split('/').pop() - : asset.public_id; - - // Create new panel - const panel = vscode.window.createWebviewPanel( - "cloudinaryAssetPreview", - shortName || asset.public_id, - vscode.ViewColumn.One, - { enableScripts: true } - ); - - // Set the panel icon based on asset type - const iconFile = asset.displayType === 'image' - ? 'icon-image.svg' - : asset.displayType === 'video' - ? 'icon-video.svg' - : 'icon-file.svg'; - panel.iconPath = vscode.Uri.joinPath(context.extensionUri, "resources", iconFile); - - // Track this panel - openPanels.set(publicId, panel); - - // Remove from tracking when disposed - panel.onDidDispose(() => { - openPanels.delete(publicId); - }); - - // Format file size - const formatSize = (bytes: number) => { - if (bytes === 0) { return '0 B'; } - const k = 1024; - const sizes = ['B', 'KB', 'MB', 'GB']; - const i = Math.floor(Math.log(bytes) / Math.log(k)); - return `${parseFloat((bytes / Math.pow(k, i)).toFixed(1))} ${sizes[i]}`; - }; +/** + * Get asset type icon. + */ +function getAssetTypeIcon(type: string): string { + return getAssetIcon(type); +} - // Get display name (last part of public_id) - const displayName = asset.public_id.includes('/') - ? asset.public_id.split('/').pop() - : asset.public_id; +/** + * Build the preview section HTML based on asset type. + */ +function buildPreviewHtml(asset: AssetData): { html: string; hasEnlarge: boolean } { + const displayName = asset.public_id.includes("/") + ? asset.public_id.split("/").pop() + : asset.public_id; + + if (asset.displayType === "image") { + return { + html: ` +
+ ${escapeHtml(asset.public_id)} + +
+ `, + hasEnlarge: true, + }; + } + + if (asset.displayType === "video") { + return { + html: ` +
+ + +
+ `, + hasEnlarge: true, + }; + } + + // Raw file + return { + html: ` +
+
+ ${assetIcons.file("xl")} +
+

${escapeHtml(displayName || "")}

+ + ${actionIcons.download("sm")} Download File + +
+ `, + hasEnlarge: false, + }; +} - // SVG icons - const icons = { - image: ` - - `, - video: ` - - `, - file: ` - - `, - download: ` - - ` - }; +/** + * Build the tags HTML. + */ +function buildTagsHtml(tags?: string[]): string { + if (!Array.isArray(tags) || tags.length === 0) { + return '

No tags

'; + } + return `
${tags.map((t) => `${escapeHtml(t)}`).join("")}
`; +} - // Determine asset type icon - const typeIcon = asset.displayType === 'image' ? icons.image : asset.displayType === 'video' ? icons.video : icons.file; +/** + * Build metadata section HTML. + */ +function buildMetadataHtml(data: Record | undefined, emptyText: string): string { + if (!data || Object.keys(data).length === 0) { + return `

${escapeHtml(emptyText)}

`; + } + return Object.entries(data) + .map( + ([key, value]) => ` +
+ ${escapeHtml(key)} + ${escapeHtml(String(value))} +
+ ` + ) + .join(""); +} - let previewHtml = ""; - let hasEnlarge = false; +/** + * Build the lightbox HTML. + */ +function buildLightboxHtml(asset: AssetData): string { + const content = + asset.displayType === "image" + ? `${escapeHtml(asset.public_id)}` + : ``; + + return ` + + `; +} - if (asset.displayType === "image") { - hasEnlarge = true; - previewHtml = ` -
- ${asset.public_id} - -
- `; - } else if (asset.displayType === "video") { - hasEnlarge = true; - previewHtml = ` -
- - -
- `; - } else { - previewHtml = ` -
-
- - - +/** + * Generate the body content for the asset preview panel. + */ +function getPreviewContent(asset: AssetData): string { + const displayName = asset.public_id.includes("/") + ? asset.public_id.split("/").pop() + : asset.public_id; + + const typeIcon = getAssetTypeIcon(asset.displayType); + const { html: previewHtml, hasEnlarge } = buildPreviewHtml(asset); + const tagsHtml = buildTagsHtml(asset.tags); + const contextHtml = buildMetadataHtml(asset.context, "No context metadata"); + const metadataHtml = buildMetadataHtml(asset.metadata, "No structured metadata"); + const lightboxHtml = hasEnlarge ? buildLightboxHtml(asset) : ""; + + return ` +
+
+ +
+ ${typeIcon} +
+

${escapeHtml(displayName || "")}

+
+ ${escapeHtml((asset.format || asset.displayType || "unknown").toUpperCase())} + ${asset.width && asset.height ? `${asset.width} × ${asset.height}` : ""} + ${asset.bytes ? ` • ${formatFileSize(asset.bytes)}` : ""}
-

${displayName}

- - - - - Download File -
- `; - } - - panel.webview.html = ` - - - - - - ${asset.public_id} - - - -
- -
- ${typeIcon} -
-

${displayName}

-
- ${(asset.format || asset.displayType || 'unknown').toUpperCase()} - ${asset.width && asset.height ? `${asset.width} × ${asset.height}` : ''} - ${asset.bytes ? ` • ${formatSize(asset.bytes)}` : ''} -
-
-
- - - ${previewHtml} - - -