diff --git a/README.md b/README.md
index 488a0cb..5f1ed41 100644
--- a/README.md
+++ b/README.md
@@ -128,13 +128,15 @@ Once a valid configuration has been added, the active environment will be shown
## Contribute
-Got feedback or feature ideas? Open an issue.
+Got feedback or feature ideas? See [CONTRIBUTING.md](./CONTRIBUTING.md) or open an issue.
---
## Resources
- [Cloudinary Documentation](https://cloudinary.com/documentation)
+- [Cloudinary Upload Presets](https://cloudinary.com/documentation/upload_presets)
+- [Cloudinary Admin API](https://cloudinary.com/documentation/admin_api)
---
diff --git a/docs/adding-features.md b/docs/adding-features.md
new file mode 100644
index 0000000..5439ff0
--- /dev/null
+++ b/docs/adding-features.md
@@ -0,0 +1,363 @@
+# Adding Features
+
+This guide explains how to add new functionality to the Cloudinary VS Code Extension.
+
+## Adding a New Command
+
+### 1. Create the Command File
+
+Create a new file in `src/commands/`:
+
+```typescript
+// src/commands/myNewCommand.ts
+import * as vscode from "vscode";
+import { CloudinaryTreeDataProvider } from "../tree/treeDataProvider";
+
+function registerMyNewCommand(
+ context: vscode.ExtensionContext,
+ provider: CloudinaryTreeDataProvider
+) {
+ context.subscriptions.push(
+ vscode.commands.registerCommand("cloudinary.myNewCommand", async () => {
+ // Implementation here
+ vscode.window.showInformationMessage("Command executed!");
+ })
+ );
+}
+
+export default registerMyNewCommand;
+```
+
+### 2. Register the Command
+
+Add to `src/commands/registerCommands.ts`:
+
+```typescript
+import registerMyNewCommand from "./myNewCommand";
+
+function registerAllCommands(
+ context: vscode.ExtensionContext,
+ provider: CloudinaryTreeDataProvider
+) {
+ // ... existing registrations
+ registerMyNewCommand(context, provider);
+}
+```
+
+### 3. Add to Package.json
+
+```json
+{
+ "contributes": {
+ "commands": [
+ {
+ "command": "cloudinary.myNewCommand",
+ "title": "My New Command",
+ "category": "Cloudinary",
+ "icon": "$(symbol-misc)"
+ }
+ ]
+ }
+}
+```
+
+### 4. Add Menu Placement (Optional)
+
+```json
+{
+ "contributes": {
+ "menus": {
+ "view/title": [
+ {
+ "command": "cloudinary.myNewCommand",
+ "when": "view == cloudinaryMediaLibrary",
+ "group": "navigation"
+ }
+ ],
+ "view/item/context": [
+ {
+ "command": "cloudinary.myNewCommand",
+ "when": "viewItem == asset",
+ "group": "inline"
+ }
+ ]
+ }
+ }
+}
+```
+
+## Adding a New Tree Item Type
+
+### 1. Add the Type
+
+In `src/tree/cloudinaryItem.ts`:
+
+```typescript
+export type CloudinaryItemType =
+ | 'asset'
+ | 'folder'
+ | 'loadMore'
+ | 'myNewType'; // Add your type
+```
+
+### 2. Handle in Constructor
+
+```typescript
+else if (type === 'myNewType') {
+ this.contextValue = 'myNewType';
+ this.iconPath = new vscode.ThemeIcon('symbol-misc');
+ this.tooltip = 'My new item type';
+ this.command = {
+ command: 'cloudinary.handleMyNewType',
+ title: 'Handle',
+ arguments: [data],
+ };
+}
+```
+
+### 3. Create Items in Provider
+
+In `src/tree/treeDataProvider.ts`:
+
+```typescript
+const myItem = new CloudinaryItem(
+ 'Item Label',
+ vscode.TreeItemCollapsibleState.None,
+ 'myNewType',
+ { customData: 'value' },
+ this.cloudName!,
+ this.dynamicFolders
+);
+items.push(myItem);
+```
+
+### 4. Add Context Menu (Optional)
+
+```json
+{
+ "contributes": {
+ "menus": {
+ "view/item/context": [
+ {
+ "command": "cloudinary.myCommand",
+ "when": "viewItem == myNewType"
+ }
+ ]
+ }
+ }
+}
+```
+
+## Adding a New Webview
+
+### 1. Create the Command File
+
+```typescript
+// src/commands/myWebview.ts
+import * as vscode from "vscode";
+import { createWebviewDocument, getScriptUri } from "../webview/webviewUtils";
+import { escapeHtml } from "../webview/utils/helpers";
+
+let panel: vscode.WebviewPanel | undefined;
+
+function registerMyWebview(context: vscode.ExtensionContext) {
+ context.subscriptions.push(
+ vscode.commands.registerCommand("cloudinary.openMyWebview", () => {
+ if (panel) {
+ panel.reveal();
+ return;
+ }
+
+ panel = vscode.window.createWebviewPanel(
+ "cloudinaryMyWebview",
+ "My Webview",
+ vscode.ViewColumn.One,
+ {
+ enableScripts: true,
+ localResourceRoots: [
+ vscode.Uri.joinPath(context.extensionUri, "src", "webview", "media"),
+ ],
+ }
+ );
+
+ panel.webview.html = createWebviewDocument({
+ title: "My Webview",
+ webview: panel.webview,
+ extensionUri: context.extensionUri,
+ bodyContent: getContent(),
+ inlineScript: "initCommon();",
+ });
+
+ panel.webview.onDidReceiveMessage((message) => {
+ switch (message.command) {
+ case "doSomething":
+ // Handle message
+ break;
+ }
+ });
+
+ panel.onDidDispose(() => {
+ panel = undefined;
+ });
+ })
+ );
+}
+
+function getContent(): string {
+ return `
+
+
+
+
My Webview
+
Content here
+
+
+
+
+ `;
+}
+
+export default registerMyWebview;
+```
+
+### 2. Add Custom JavaScript (Optional)
+
+Create `src/webview/media/scripts/my-webview.js`:
+
+```javascript
+/**
+ * My Webview functionality.
+ */
+
+function initMyWebview() {
+ const button = document.getElementById('myButton');
+ if (button) {
+ button.addEventListener('click', () => {
+ vscode.postMessage({ command: 'doSomething' });
+ });
+ }
+}
+```
+
+Update the webview to include it:
+
+```typescript
+const myScriptUri = getScriptUri(panel.webview, context.extensionUri, "my-webview.js");
+
+panel.webview.html = createWebviewDocument({
+ // ...
+ additionalScripts: [myScriptUri],
+ inlineScript: "initCommon(); initMyWebview();",
+});
+```
+
+### 3. Register and Add to Package.json
+
+Same as adding a command (see above).
+
+## Adding Configuration Options
+
+### 1. Update Configuration Interface
+
+In `src/config/configUtils.ts`:
+
+```typescript
+export interface CloudinaryEnvironment {
+ apiKey: string;
+ apiSecret: string;
+ uploadPreset?: string;
+ myNewOption?: string; // Add your option
+}
+```
+
+### 2. Use in Code
+
+```typescript
+const myOption = provider.getConfig().myNewOption || 'default';
+```
+
+### 3. Document the Option
+
+Update `docs/configuration.md` with the new option.
+
+## Common Patterns
+
+### Error Handling
+
+```typescript
+import { handleCloudinaryError } from '../utils/cloudinaryErrorHandler';
+
+try {
+ const result = await cloudinary.api.someMethod();
+} catch (err: any) {
+ handleCloudinaryError('Operation failed', err);
+}
+```
+
+### User Input
+
+```typescript
+// Input box
+const query = await vscode.window.showInputBox({
+ placeHolder: 'Enter search term',
+ prompt: 'Search assets by public ID',
+ validateInput: (value) => value ? null : 'Cannot be empty'
+});
+
+if (!query) return; // User cancelled
+
+// Quick pick
+const selected = await vscode.window.showQuickPick(
+ ['Option 1', 'Option 2', 'Option 3'],
+ { placeHolder: 'Select an option' }
+);
+```
+
+### Progress Indicator
+
+```typescript
+await vscode.window.withProgress(
+ {
+ location: vscode.ProgressLocation.Notification,
+ title: 'Loading assets...',
+ cancellable: false,
+ },
+ async (progress) => {
+ progress.report({ increment: 0 });
+ // Do work
+ progress.report({ increment: 50 });
+ // More work
+ progress.report({ increment: 100 });
+ }
+);
+```
+
+### Refresh Tree View
+
+```typescript
+// After modifying data
+provider.refresh(); // Fires _onDidChangeTreeData
+```
+
+## Testing Your Changes
+
+1. **Compile**: `npm run compile`
+2. **Launch**: Press `F5` to open Extension Development Host
+3. **Test**: Verify functionality works as expected
+4. **Reload**: Press `Ctrl+R` / `Cmd+R` after code changes
+
+### Manual Testing Checklist
+
+- [ ] Feature works in light and dark themes
+- [ ] Error cases show user-friendly messages
+- [ ] Keyboard navigation works
+- [ ] No console errors in Developer Tools
+
+## Code Style Guidelines
+
+1. **TypeScript strict mode** - Fix all type errors
+2. **Use existing patterns** - Follow conventions in similar files
+3. **Escape HTML** - Always use `escapeHtml()` for dynamic content
+4. **Use design system** - Use component classes from `components.css`
+5. **Handle errors** - Use `handleCloudinaryError()` for API errors
+6. **Document** - Add JSDoc comments for public functions
diff --git a/docs/architecture.md b/docs/architecture.md
new file mode 100644
index 0000000..015952a
--- /dev/null
+++ b/docs/architecture.md
@@ -0,0 +1,140 @@
+# Architecture
+
+The extension is built on VS Code's extension API and integrates with Cloudinary's Admin and Upload APIs.
+
+### Core Components
+
+```
+┌─────────────────────────────────────────────────────────────┐
+│ VS Code Extension │
+├─────────────────────────────────────────────────────────────┤
+│ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │
+│ │ Tree View │ │ Webviews │ │ Commands │ │
+│ │ (Sidebar) │ │ (Panels) │ │ (Actions) │ │
+│ └──────┬───────┘ └──────┬───────┘ └──────┬───────┘ │
+│ │ │ │ │
+│ └────────────┬────┴────────────────┘ │
+│ │ │
+│ ┌────────────▼────────────┐ │
+│ │ CloudinaryTreeProvider │ │
+│ │ (State & API Layer) │ │
+│ └────────────┬────────────┘ │
+│ │ │
+├──────────────────────┼──────────────────────────────────────┤
+│ │ │
+│ ┌────────────▼────────────┐ │
+│ │ Cloudinary SDK │ │
+│ │ (Node.js v2) │ │
+│ └────────────┬────────────┘ │
+│ │ │
+└──────────────────────┼──────────────────────────────────────┘
+ │
+ ┌───────────▼───────────┐
+ │ Cloudinary APIs │
+ │ - Admin API │
+ │ - Search API │
+ │ - Upload API │
+ └───────────────────────┘
+```
+
+### Key Design Decisions
+
+1. **Configuration via files, not settings** - API secrets are stored in `~/.cloudinary/environments.json`, not VS Code settings (which could be committed to repos)
+
+2. **Singleton provider** - `CloudinaryTreeDataProvider` holds all state and manages API calls
+
+3. **External CSS/JS for webviews** - Styles and scripts are loaded from files, not embedded in HTML strings
+
+4. **Centralized icons** - SVG icons are defined once in `src/webview/icons.ts`
+
+5. **Shared utilities** - Common functions like `escapeHtml` are in `src/webview/utils/helpers.ts`
+
+## Technology Stack
+
+| Component | Technology |
+|-----------|------------|
+| Language | TypeScript (strict mode) |
+| Build | esbuild |
+| Runtime | VS Code Extension Host (Node.js) |
+| API Client | Cloudinary Node.js SDK v2.x |
+| Testing | Mocha + VS Code Test Electron |
+| Linting | ESLint |
+
+## Data Flow
+
+### Configuration Loading
+
+```
+1. Extension activates
+2. Check for workspace config (.cloudinary/environments.json)
+3. Fall back to global config (~/.cloudinary/environments.json)
+4. Validate credentials (reject placeholder values)
+5. Configure Cloudinary SDK
+6. Detect folder mode (dynamic vs fixed)
+```
+
+### Tree View Population
+
+```
+1. VS Code calls provider.getChildren()
+2. Provider checks cache (assetMap)
+3. If not cached, fetch from API
+4. Transform to CloudinaryItem instances
+5. Return items to VS Code
+```
+
+### Webview Communication
+
+```
+Extension Webview
+ │ │
+ │ panel.webview.html = ... │
+ │──────────────────────────────────>│
+ │ │
+ │ vscode.postMessage({...}) │
+ │<──────────────────────────────────│
+ │ │
+ │ panel.webview.postMessage({...})│
+ │──────────────────────────────────>│
+ │ │
+```
+
+## VS Code Integration Points
+
+### Package.json Contributions
+
+| Contribution | Purpose |
+|--------------|---------|
+| `viewsContainers.activitybar` | Cloudinary icon in sidebar |
+| `views.cloudinary` | Tree view registration |
+| `commands` | Command definitions |
+| `menus.view/title` | Tree view title bar buttons |
+| `menus.view/item/context` | Right-click context menu |
+
+### Tree Item Context Values
+
+| Context Value | Description |
+|---------------|-------------|
+| `asset` | Media file (enables copy commands) |
+| `folder` | Directory (enables upload to folder) |
+| `loadMore` | Pagination trigger |
+| `clearSearch` | Clear search results |
+
+## Error Handling
+
+All Cloudinary API errors flow through `handleCloudinaryError()`:
+
+```typescript
+import { handleCloudinaryError } from '../utils/cloudinaryErrorHandler';
+
+try {
+ await cloudinary.search.execute();
+} catch (err: any) {
+ handleCloudinaryError('Failed to search assets', err);
+}
+```
+
+The handler:
+1. Extracts message from various error formats
+2. Shows VS Code error notification
+3. Offers "Open Global Config" action for credential errors
diff --git a/docs/project-structure.md b/docs/project-structure.md
new file mode 100644
index 0000000..4de1ddd
--- /dev/null
+++ b/docs/project-structure.md
@@ -0,0 +1,220 @@
+# Project Structure
+
+This document explains the organization of the Cloudinary VS Code Extension codebase.
+
+## Directory Overview
+
+```
+cloudinary-vscode/
+├── src/ # TypeScript source code
+│ ├── extension.ts # Extension entry point
+│ ├── commands/ # Command implementations
+│ ├── tree/ # Tree view (sidebar)
+│ ├── config/ # Configuration utilities
+│ ├── utils/ # Shared utilities
+│ ├── webview/ # Webview design system
+│ └── test/ # Test files
+├── dist/ # Bundled output (esbuild)
+├── out/ # TypeScript output (for tests)
+├── docs/ # Documentation
+├── resources/ # Static assets (icons)
+├── package.json # Extension manifest
+├── tsconfig.json # TypeScript configuration
+├── esbuild.js # Build script
+└── .eslintrc.json # ESLint configuration
+```
+
+## Source Code (`src/`)
+
+### Entry Point
+
+**`extension.ts`** - Extension lifecycle management
+
+- `activate()` - Called when extension starts
+- Creates `CloudinaryTreeDataProvider`
+- Loads configuration
+- Registers commands and tree view
+
+### Commands (`src/commands/`)
+
+Each file exports a registration function:
+
+| File | Commands | Purpose |
+|------|----------|---------|
+| `registerCommands.ts` | - | Central registration, imports all commands |
+| `previewAsset.ts` | `cloudinary.openAsset` | Asset preview panel |
+| `uploadWidget.ts` | `cloudinary.openUploadWidget`, `cloudinary.uploadToFolder` | Upload panel |
+| `welcomeScreen.ts` | `cloudinary.showWelcome` | Welcome/onboarding screen |
+| `searchAssets.ts` | `cloudinary.searchAssets` | Search by public ID |
+| `copyCommands.ts` | `cloudinary.copyPublicId`, `cloudinary.copySecureUrl` | Clipboard operations |
+| `switchEnvironment.ts` | `cloudinary.switchEnvironment` | Environment switching |
+| `clearSearch.ts` | `cloudinary.clearSearch` | Clear search filter |
+| `viewOptions.ts` | `cloudinary.setResourceFilter` | Filter by type |
+
+### Tree View (`src/tree/`)
+
+| File | Purpose |
+|------|---------|
+| `treeDataProvider.ts` | `TreeDataProvider` implementation, state management, API calls |
+| `cloudinaryItem.ts` | `TreeItem` subclass for assets, folders, and UI elements |
+
+**CloudinaryTreeDataProvider** holds:
+- Credentials (`cloudName`, `apiKey`, `apiSecret`)
+- View state (current folder, search query, filter)
+- Asset cache (`assetMap`)
+- Upload presets
+
+### Configuration (`src/config/`)
+
+| File | Purpose |
+|------|---------|
+| `configUtils.ts` | Load/validate configuration files |
+| `detectFolderMode.ts` | Detect dynamic vs fixed folder mode |
+
+### Utilities (`src/utils/`)
+
+| File | Purpose |
+|------|---------|
+| `cloudinaryErrorHandler.ts` | Consistent error display with VS Code UI |
+| `userAgent.ts` | Generate user agent for API calls |
+
+### Webview System (`src/webview/`)
+
+The webview module provides a design system for building consistent UIs:
+
+```
+src/webview/
+├── index.ts # Public exports
+├── tokens.ts # Design tokens (colors, spacing)
+├── baseStyles.ts # CSS reset, typography
+├── icons.ts # Centralized SVG icons
+├── webviewUtils.ts # HTML generation helpers
+├── components/ # UI components
+│ ├── index.ts # Component exports
+│ ├── button.ts # Button styles
+│ ├── card.ts # Card/panel styles
+│ ├── tabs.ts # Tab navigation
+│ ├── input.ts # Form inputs
+│ ├── dropZone.ts # File upload drop zone
+│ ├── progressBar.ts # Progress indicators
+│ ├── badge.ts # Tags and badges
+│ ├── infoRow.ts # Key-value display
+│ ├── lightbox.ts # Image lightbox
+│ └── layout.ts # Layout components
+├── utils/ # Webview utilities
+│ ├── index.ts # Utility exports
+│ ├── helpers.ts # escapeHtml, formatFileSize, etc.
+│ ├── clipboard.ts # Clipboard functionality
+│ └── messaging.ts # VS Code API wrappers
+├── scripts/ # TypeScript for client-side JS
+│ ├── index.ts
+│ ├── uploadWidget.ts
+│ ├── previewAsset.ts
+│ └── welcomeScreen.ts
+└── media/ # External CSS/JS files
+ ├── styles/
+ │ ├── tokens.css # CSS custom properties
+ │ ├── base.css # Base styles
+ │ └── components.css # Component styles
+ └── scripts/
+ ├── common.js # Shared client-side utilities
+ ├── upload-widget.js # Upload panel functionality
+ └── welcome.js # Welcome screen functionality
+```
+
+See [Webview System](./webview-system.md) for detailed documentation.
+
+## Configuration Files
+
+### `package.json`
+
+Extension manifest defining:
+- Extension metadata (name, version, publisher)
+- Activation events
+- Contributed commands, views, and menus
+- Dependencies
+
+### `tsconfig.json`
+
+TypeScript configuration:
+- Strict mode enabled
+- ES2022 target
+- CommonJS modules (for VS Code)
+
+### `esbuild.js`
+
+Build configuration:
+- Entry: `src/extension.ts`
+- Output: `dist/extension.js`
+- Externals: `vscode` module
+- Source maps enabled
+
+## Output Directories
+
+### `dist/`
+
+Production bundle created by esbuild:
+- `extension.js` - Bundled extension code
+- `extension.js.map` - Source map
+
+### `out/`
+
+TypeScript compilation output (for tests):
+- Mirrors `src/` structure
+- Used by test runner
+
+## Resources
+
+### `resources/`
+
+Static assets:
+- `cloudinary_icon_blue.png` - Extension icon
+- `icon-image.svg`, `icon-video.svg`, `icon-file.svg` - Tree item icons
+
+## Key Patterns
+
+### Command Registration
+
+```typescript
+// src/commands/myCommand.ts
+function registerMyCommand(
+ context: vscode.ExtensionContext,
+ provider: CloudinaryTreeDataProvider
+) {
+ context.subscriptions.push(
+ vscode.commands.registerCommand("cloudinary.myCommand", async () => {
+ // Implementation
+ })
+ );
+}
+export default registerMyCommand;
+```
+
+### Tree Item Creation
+
+```typescript
+new CloudinaryItem(
+ 'Label',
+ vscode.TreeItemCollapsibleState.Collapsed,
+ 'folder', // type
+ { path: '/products' }, // data
+ cloudName,
+ dynamicFolders
+);
+```
+
+### Webview HTML Generation
+
+```typescript
+import { createWebviewDocument, getScriptUri } from "../webview/webviewUtils";
+
+panel.webview.html = createWebviewDocument({
+ title: "My Panel",
+ webview: panel.webview,
+ extensionUri: context.extensionUri,
+ bodyContent: getHtmlContent(),
+ additionalScripts: [getScriptUri(webview, extensionUri, "my-script.js")],
+ inlineScript: "initCommon(); initMyPanel();",
+});
+```
+
diff --git a/docs/setup.md b/docs/setup.md
new file mode 100644
index 0000000..903904a
--- /dev/null
+++ b/docs/setup.md
@@ -0,0 +1,150 @@
+# Development Setup
+
+This guide will help you set up your development environment for contributing to the Cloudinary VS Code Extension.
+
+## Prerequisites
+
+- **Node.js** 18.x or later
+- **npm** 8.x or later
+- **Visual Studio Code** 1.85.0 or later
+- **Git**
+- **Cloudinary account** with API credentials
+
+## Clone and Install
+
+```bash
+# Clone the repository
+git clone https://github.com/cloudinary/cloudinary-vscode.git
+cd cloudinary-vscode
+
+# Install dependencies
+npm install
+```
+
+## Configure Credentials
+
+Create a Cloudinary configuration file:
+
+**macOS/Linux:**
+```bash
+mkdir -p ~/.cloudinary
+```
+
+**Windows:**
+```powershell
+mkdir $env:USERPROFILE\.cloudinary
+```
+
+Create `environments.json` with your credentials:
+
+```json
+{
+ "your-cloud-name": {
+ "apiKey": "your-api-key",
+ "apiSecret": "your-api-secret"
+ }
+}
+```
+
+## Build Commands
+
+| Command | Description |
+|---------|-------------|
+| `npm run compile` | Type check + build |
+| `npm run watch` | Watch mode (auto-rebuild) |
+| `npm run check-types` | TypeScript type checking only |
+| `npm run lint` | Run ESLint |
+| `npm test` | Run tests |
+
+## Running the Extension
+
+### Launch Extension Development Host
+
+1. Open the project in VS Code
+2. Press `F5` (or Run → Start Debugging)
+3. A new VS Code window opens with your extension loaded
+4. Make changes, then press `Ctrl+R` / `Cmd+R` in the dev window to reload
+
+### Setting Breakpoints
+
+1. Set breakpoints in `.ts` files (source maps are enabled)
+2. Launch the Extension Development Host
+3. Trigger the code path you want to debug
+4. VS Code will pause at your breakpoints
+
+## Testing
+
+### Run All Tests
+
+```bash
+npm test
+```
+
+### Manual Testing Checklist
+
+After making changes, verify:
+
+- [ ] Extension activates without errors
+- [ ] Tree view populates with folders/assets
+- [ ] Commands work from command palette
+- [ ] Context menus appear on correct items
+- [ ] Webviews display correctly in light and dark themes
+- [ ] Upload functionality works
+- [ ] Error messages are user-friendly
+
+## Creating a Package
+
+To create a `.vsix` file for local installation:
+
+```bash
+npm run package
+```
+
+This creates `cloudinary-x.x.x.vsix` in the project root.
+
+### Install Locally
+
+1. Open VS Code
+2. Go to Extensions (`Ctrl+Shift+X` / `Cmd+Shift+X`)
+3. Click `...` menu → **Install from VSIX...**
+4. Select the `.vsix` file
+
+## Code Style
+
+- Use TypeScript strict mode
+- Follow existing patterns in the codebase
+- Use `handleCloudinaryError()` for API errors
+- Use the webview design system for UI components
+- Keep dependencies minimal
+
+## Useful VS Code Commands
+
+When debugging the extension:
+
+| Command | Description |
+|---------|-------------|
+| `Developer: Reload Window` | Reload after code changes |
+| `Developer: Toggle Developer Tools` | Open browser dev tools for webviews |
+| `Cloudinary: Show Welcome` | Test the welcome screen |
+| `Cloudinary: Upload` | Test the upload panel |
+
+## Troubleshooting
+
+### Extension Not Loading
+
+1. Check the Debug Console for errors
+2. Verify `main` in package.json points to `dist/extension.js`
+3. Run `npm run compile` to check for TypeScript errors
+
+### Tree View Empty
+
+1. Check credentials in config file
+2. Verify network connectivity
+3. Check Debug Console for API errors
+
+### Webview Blank
+
+1. Open Developer Tools (`Help → Toggle Developer Tools`)
+2. Check Console tab for JavaScript errors
+3. Verify Content Security Policy allows your resources
+
diff --git a/docs/webview-system.md b/docs/webview-system.md
new file mode 100644
index 0000000..6b36580
--- /dev/null
+++ b/docs/webview-system.md
@@ -0,0 +1,331 @@
+# Webview System
+
+The extension uses a modular design system for building webview UIs. CSS and JavaScript are loaded from external files, providing better maintainability and debugging.
+
+## Architecture
+
+```
+src/webview/
+├── media/ # External files loaded at runtime
+│ ├── styles/
+│ │ ├── tokens.css # Design tokens (CSS custom properties)
+│ │ ├── base.css # Reset, typography, utilities
+│ │ └── components.css # Component styles
+│ └── scripts/
+│ ├── common.js # Shared utilities (tabs, copy, collapsibles)
+│ ├── upload-widget.js # Upload-specific functionality
+│ └── welcome.js # Welcome screen functionality
+├── webviewUtils.ts # HTML generation and CSP helpers
+├── icons.ts # Centralized SVG icons
+└── utils/helpers.ts # Shared utilities (escapeHtml, etc.)
+```
+
+## Creating a Webview
+
+### Basic Structure
+
+```typescript
+import { createWebviewDocument, getScriptUri } from "../webview/webviewUtils";
+import { escapeHtml } from "../webview/utils/helpers";
+import { assetIcons, actionIcons } from "../webview/icons";
+
+function openMyPanel(context: vscode.ExtensionContext) {
+ const panel = vscode.window.createWebviewPanel(
+ "myPanelId",
+ "My Panel Title",
+ vscode.ViewColumn.One,
+ {
+ enableScripts: true,
+ localResourceRoots: [
+ vscode.Uri.joinPath(context.extensionUri, "src", "webview", "media"),
+ ],
+ }
+ );
+
+ // Optional: Add custom script
+ const myScriptUri = getScriptUri(panel.webview, context.extensionUri, "my-script.js");
+
+ panel.webview.html = createWebviewDocument({
+ title: "My Panel",
+ webview: panel.webview,
+ extensionUri: context.extensionUri,
+ bodyContent: getMyContent(),
+ bodyClass: "layout-centered", // Optional
+ additionalScripts: [myScriptUri], // Optional
+ inlineScript: "initCommon(); initMyPanel();", // Required
+ });
+
+ // Handle messages from webview
+ panel.webview.onDidReceiveMessage((message) => {
+ // Handle message
+ });
+}
+```
+
+### Key Points
+
+1. **Always call `initCommon()`** in the inline script - this initializes tabs, copy buttons, and collapsibles
+2. **Use `escapeHtml()`** for any dynamic content
+3. **Import icons** from the centralized module
+4. **Specify `localResourceRoots`** to allow loading external files
+
+## CSS Architecture
+
+### Design Tokens (`tokens.css`)
+
+CSS custom properties based on Cloudinary brand and VS Code theming:
+
+```css
+:root {
+ /* Cloudinary Brand Colors */
+ --cld-brand-blue: #3448C5;
+ --cld-sky-blue: #0D9AFF;
+
+ /* Semantic Colors (VS Code with Cloudinary fallbacks) */
+ --color-accent: var(--vscode-textLink-foreground, var(--cld-sky-blue));
+ --color-success: var(--vscode-testing-iconPassed, #60CFB7);
+ --color-error: var(--vscode-testing-iconFailed, #FE5981);
+
+ /* Surfaces */
+ --color-surface: var(--vscode-editor-background);
+ --color-surface-elevated: var(--vscode-editorWidget-background);
+ --color-border: var(--vscode-editorWidget-border);
+
+ /* Spacing */
+ --space-sm: 0.5rem;
+ --space-md: 0.75rem;
+ --space-lg: 1rem;
+ --space-xl: 1.5rem;
+}
+```
+
+### Base Styles (`base.css`)
+
+- CSS reset
+- Typography defaults
+- Flexbox utilities (`.flex`, `.items-center`, `.gap-md`)
+- Spacing utilities (`.mt-lg`, `.mb-md`)
+- Animations (`.animate-fade-in`)
+
+### Component Styles (`components.css`)
+
+Styles for all UI components:
+
+| Component | Classes |
+|-----------|---------|
+| Buttons | `.btn`, `.btn--primary`, `.btn--secondary`, `.btn--sm` |
+| Cards | `.card`, `.card--elevated`, `.card__header`, `.card__body` |
+| Tabs | `.tabs`, `.tabs__nav`, `.tabs__btn`, `.tabs__content` |
+| Inputs | `.input`, `.select`, `.form-group` |
+| Progress | `.progress-bar`, `.upload-queue` |
+| Badges | `.badge`, `.badge--success`, `.meta-tags` |
+| Drop Zone | `.drop-zone`, `.drop-zone--active` |
+| Lightbox | `.lightbox`, `.lightbox__content` |
+
+## JavaScript Architecture
+
+### Common Script (`common.js`)
+
+Shared functionality loaded by all webviews:
+
+```javascript
+// Initialize all common functionality
+function initCommon() {
+ initVSCode(); // acquireVsCodeApi()
+ initTabs(); // Tab switching
+ initCopyButtons(); // Copy to clipboard
+ initCollapsibles();// Expandable sections
+ initLightbox(); // Image lightbox
+}
+
+// Utility functions
+function copyToClipboard(text) { ... }
+function formatFileSize(bytes) { ... }
+function truncateString(str, maxLength) { ... }
+```
+
+### View-Specific Scripts
+
+Each webview can have its own script:
+
+| Script | Purpose | Init Function |
+|--------|---------|---------------|
+| `upload-widget.js` | File uploads, progress, presets | `initUploadWidget(config)` |
+| `welcome.js` | Welcome screen interactions | (auto-init) |
+
+### Initialization Pattern
+
+```javascript
+// In inline script
+initCommon(); // Always first
+initUploadWidget({
+ cloudName: "my-cloud",
+ presets: [...]
+});
+```
+
+## Content Security Policy
+
+The `createWebviewDocument` function generates a secure CSP:
+
+```
+default-src 'none';
+style-src ${webview.cspSource} 'unsafe-inline';
+script-src ${webview.cspSource} 'nonce-${nonce}';
+img-src ${webview.cspSource} https: data:;
+font-src ${webview.cspSource};
+```
+
+- External CSS/JS loaded from `webview.cspSource`
+- Inline scripts require the nonce
+- Images allowed from HTTPS and data URIs
+
+## Icons
+
+Use the centralized icon module:
+
+```typescript
+import { assetIcons, actionIcons } from "../webview/icons";
+
+// Asset type icons
+assetIcons.image("lg") // Large image icon
+assetIcons.video("md") // Medium video icon
+assetIcons.file("sm") // Small file icon
+
+// Action icons
+actionIcons.download("sm")
+actionIcons.copy("md")
+actionIcons.enlarge("md")
+actionIcons.close("md")
+```
+
+Sizes: `sm` (16px), `md` (20px), `lg` (24px), `xl` (48px)
+
+## Utility Functions
+
+Import shared utilities:
+
+```typescript
+import {
+ escapeHtml, // Escape HTML special characters
+ formatFileSize, // Format bytes (e.g., "2.4 MB")
+ truncateString, // Truncate with ellipsis
+ generateId, // Generate unique IDs
+} from "../webview/utils/helpers";
+```
+
+## Component Examples
+
+### Buttons
+
+```html
+
+
+
+```
+
+### Cards
+
+```html
+
+```
+
+### Tabs
+
+```html
+
+
+
+ Info content
+
+
+ Metadata content
+
+
+```
+
+### Collapsibles
+
+```html
+
+
+
+ Hidden content here
+
+
+```
+
+### Form Groups
+
+```html
+
+```
+
+### Info Rows
+
+```html
+
+ File Size
+ 2.4 MB
+
+```
+
+## Webview Communication
+
+### Extension → Webview
+
+```typescript
+panel.webview.postMessage({
+ command: 'update',
+ data: { ... }
+});
+```
+
+### Webview → Extension
+
+```javascript
+// In webview
+const vscode = acquireVsCodeApi();
+vscode.postMessage({
+ command: 'doSomething',
+ data: { ... }
+});
+
+// In extension
+panel.webview.onDidReceiveMessage((message) => {
+ if (message.command === 'doSomething') {
+ // Handle
+ }
+});
+```
+
+## Adding a New Component
+
+1. **Add styles** to `src/webview/media/styles/components.css`
+2. **Add JavaScript** (if needed) to `src/webview/media/scripts/common.js`
+3. **Add TypeScript generator** (if complex) to `src/webview/components/`
+4. **Export** from `src/webview/components/index.ts`
+
+## Debugging Webviews
+
+1. Open Developer Tools: `Help → Toggle Developer Tools`
+2. Select the webview iframe in Elements panel
+3. Check Console for JavaScript errors
+4. Inspect Network tab for failed resource loads
+5. Verify CSP isn't blocking resources
diff --git a/esbuild.js b/esbuild.js
index 5ca1c87..c207238 100644
--- a/esbuild.js
+++ b/esbuild.js
@@ -4,7 +4,8 @@ const production = process.argv.includes("--production");
const watch = process.argv.includes("--watch");
async function main() {
- const ctx = await esbuild.context({
+ // Build extension (Node.js)
+ const extensionCtx = await esbuild.context({
entryPoints: ["src/extension.ts"],
bundle: true,
format: "cjs",
@@ -15,16 +16,31 @@ async function main() {
outfile: "dist/extension.js",
external: ["vscode"],
logLevel: "warning",
- plugins: [
- /* add to the end of plugins array */
- esbuildProblemMatcherPlugin,
+ plugins: [esbuildProblemMatcherPlugin],
+ });
+
+ // Build webview scripts (browser)
+ const webviewCtx = await esbuild.context({
+ entryPoints: [
+ "src/webview/client/preview.ts",
+ "src/webview/client/upload-widget.ts",
+ "src/webview/client/welcome.ts",
],
+ bundle: true,
+ format: "iife",
+ minify: production,
+ sourcemap: !production,
+ sourcesContent: false,
+ platform: "browser",
+ outdir: "media/scripts",
+ logLevel: "warning",
});
+
if (watch) {
- await ctx.watch();
+ await Promise.all([extensionCtx.watch(), webviewCtx.watch()]);
} else {
- await ctx.rebuild();
- await ctx.dispose();
+ await Promise.all([extensionCtx.rebuild(), webviewCtx.rebuild()]);
+ await Promise.all([extensionCtx.dispose(), webviewCtx.dispose()]);
}
}
diff --git a/media/styles/base.css b/media/styles/base.css
new file mode 100644
index 0000000..a706d22
--- /dev/null
+++ b/media/styles/base.css
@@ -0,0 +1,167 @@
+/**
+ * Base styles for Cloudinary VS Code extension webviews.
+ * Reset, typography, and layout utilities.
+ */
+
+/* ========================================
+ CSS Reset
+ ======================================== */
+*,
+*::before,
+*::after {
+ box-sizing: border-box;
+ margin: 0;
+ padding: 0;
+}
+
+html {
+ font-size: 14px;
+ -webkit-font-smoothing: antialiased;
+ -moz-osx-font-smoothing: grayscale;
+}
+
+body {
+ font-family: var(--vscode-font-family, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, sans-serif);
+ font-size: var(--font-md);
+ line-height: 1.5;
+ color: var(--color-text);
+ background-color: var(--color-surface);
+ padding: var(--space-xl);
+}
+
+/* ========================================
+ Typography
+ ======================================== */
+h1, h2, h3, h4, h5, h6 {
+ font-weight: 600;
+ line-height: 1.3;
+ color: var(--color-text);
+}
+
+h1 { font-size: var(--font-xxl); }
+h2 { font-size: var(--font-xl); }
+h3 { font-size: var(--font-lg); }
+
+p {
+ margin-bottom: var(--space-md);
+ line-height: 1.6;
+}
+
+a {
+ color: var(--color-accent);
+ text-decoration: none;
+ transition: color var(--transition-normal);
+}
+
+a:hover {
+ color: var(--color-accent-hover);
+ text-decoration: underline;
+}
+
+code {
+ font-family: var(--vscode-editor-font-family, 'Consolas', 'Courier New', monospace);
+ font-size: 0.9em;
+ background-color: var(--color-surface);
+ padding: 0.1em 0.3em;
+ border-radius: var(--radius-sm);
+}
+
+strong {
+ font-weight: 600;
+}
+
+/* ========================================
+ Layout Utilities
+ ======================================== */
+.hidden {
+ display: none !important;
+}
+
+.text-center { text-align: center; }
+.text-left { text-align: left; }
+.text-right { text-align: right; }
+
+.text-muted {
+ color: var(--color-text-muted);
+}
+
+/* Flexbox utilities */
+.flex { display: flex; }
+.flex-col { flex-direction: column; }
+.flex-wrap { flex-wrap: wrap; }
+.items-center { align-items: center; }
+.items-start { align-items: flex-start; }
+.items-end { align-items: flex-end; }
+.justify-center { justify-content: center; }
+.justify-between { justify-content: space-between; }
+.justify-end { justify-content: flex-end; }
+.gap-xs { gap: var(--space-xs); }
+.gap-sm { gap: var(--space-sm); }
+.gap-md { gap: var(--space-md); }
+.gap-lg { gap: var(--space-lg); }
+.gap-xl { gap: var(--space-xl); }
+
+/* Spacing utilities */
+.mt-sm { margin-top: var(--space-sm); }
+.mt-md { margin-top: var(--space-md); }
+.mt-lg { margin-top: var(--space-lg); }
+.mt-xl { margin-top: var(--space-xl); }
+.mt-xxl { margin-top: var(--space-xxl); }
+
+.mb-sm { margin-bottom: var(--space-sm); }
+.mb-md { margin-bottom: var(--space-md); }
+.mb-lg { margin-bottom: var(--space-lg); }
+.mb-xl { margin-bottom: var(--space-xl); }
+.mb-xxl { margin-bottom: var(--space-xxl); }
+
+/* ========================================
+ Animations
+ ======================================== */
+@keyframes fadeIn {
+ from { opacity: 0; }
+ to { opacity: 1; }
+}
+
+@keyframes slideUp {
+ from {
+ opacity: 0;
+ transform: translateY(8px);
+ }
+ to {
+ opacity: 1;
+ transform: translateY(0);
+ }
+}
+
+@keyframes pulse {
+ 0%, 100% { opacity: 1; }
+ 50% { opacity: 0.6; }
+}
+
+.animate-fade-in {
+ animation: fadeIn var(--transition-slow) ease-out;
+}
+
+.animate-slide-up {
+ animation: slideUp var(--transition-slow) ease-out;
+}
+
+/* ========================================
+ Page Layouts
+ ======================================== */
+.layout-centered {
+ display: flex;
+ justify-content: center;
+ align-items: flex-start;
+ min-height: 100vh;
+}
+
+.container {
+ width: 100%;
+ max-width: 1000px;
+ margin: 0 auto;
+ padding: var(--space-xxl);
+}
+
+.container--sm { max-width: 600px; }
+.container--lg { max-width: 1200px; }
diff --git a/media/styles/components.css b/media/styles/components.css
new file mode 100644
index 0000000..31aa053
--- /dev/null
+++ b/media/styles/components.css
@@ -0,0 +1,1125 @@
+/**
+ * Component styles for Cloudinary VS Code extension webviews.
+ */
+
+/* ========================================
+ Buttons
+ ======================================== */
+.btn {
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ gap: var(--space-sm);
+ font-family: inherit;
+ font-size: var(--font-md);
+ font-weight: 500;
+ line-height: 1;
+ text-decoration: none;
+ white-space: nowrap;
+ cursor: pointer;
+ border: none;
+ border-radius: var(--radius-sm);
+ padding: 0.6rem 1.25rem;
+ transition:
+ background-color var(--transition-normal),
+ color var(--transition-normal),
+ border-color var(--transition-normal),
+ opacity var(--transition-normal);
+}
+
+.btn:focus-visible {
+ outline: 2px solid var(--vscode-focusBorder);
+ outline-offset: 2px;
+}
+
+.btn:disabled {
+ opacity: 0.5;
+ cursor: not-allowed;
+}
+
+/* Primary button */
+.btn--primary {
+ background-color: var(--vscode-button-background);
+ color: var(--vscode-button-foreground);
+}
+
+.btn--primary:hover:not(:disabled) {
+ background-color: var(--vscode-button-hoverBackground);
+}
+
+/* Secondary button */
+.btn--secondary {
+ background-color: var(--vscode-button-secondaryBackground);
+ color: var(--vscode-button-secondaryForeground);
+}
+
+.btn--secondary:hover:not(:disabled) {
+ background-color: var(--vscode-button-secondaryHoverBackground);
+}
+
+/* Ghost button */
+.btn--ghost {
+ background-color: transparent;
+ color: var(--color-text);
+}
+
+.btn--ghost:hover:not(:disabled) {
+ background-color: var(--vscode-toolbar-hoverBackground, rgba(90, 93, 94, 0.31));
+}
+
+/* Button sizes */
+.btn--sm {
+ padding: 0.35rem 0.75rem;
+ font-size: var(--font-sm);
+}
+
+.btn--lg {
+ padding: 0.75rem 1.5rem;
+ font-size: var(--font-lg);
+}
+
+/* Button groups */
+.btn-group {
+ display: flex;
+ flex-wrap: wrap;
+ gap: var(--space-md);
+ margin-top: var(--space-lg);
+}
+
+.btn-group--center { justify-content: center; }
+.btn-group--end { justify-content: flex-end; }
+
+/* ========================================
+ Cards & Panels
+ ======================================== */
+.card {
+ background-color: var(--color-surface-elevated);
+ border: 1px solid var(--color-border);
+ border-radius: var(--radius-lg);
+}
+
+.card--elevated {
+ box-shadow: var(--shadow-lg);
+ border: none;
+}
+
+.card__header {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ gap: var(--space-md);
+ padding: var(--space-lg);
+ border-bottom: 1px solid var(--color-border);
+}
+
+.card__title {
+ margin: 0;
+ font-size: var(--font-xl);
+ font-weight: 600;
+}
+
+.card__body {
+ padding: var(--space-lg);
+}
+
+.card__footer {
+ display: flex;
+ align-items: center;
+ justify-content: flex-end;
+ gap: var(--space-sm);
+ padding: var(--space-md) var(--space-lg);
+ border-top: 1px solid var(--color-border);
+ background-color: var(--color-surface);
+}
+
+/* Card content styles */
+.card h3 {
+ margin: 0 0 var(--space-lg) 0;
+ font-size: var(--font-xl);
+ font-weight: 600;
+}
+
+.card h4 {
+ margin: var(--space-lg) 0 var(--space-md) 0;
+ font-size: var(--font-lg);
+ font-weight: 600;
+}
+
+.card h4:first-child { margin-top: 0; }
+
+.card p {
+ margin: 0 0 var(--space-md) 0;
+ line-height: 1.6;
+}
+
+.card p:last-child { margin-bottom: 0; }
+
+.card ul,
+.card ol {
+ margin: var(--space-md) 0;
+ padding-left: var(--space-xl);
+}
+
+.card li {
+ margin-bottom: var(--space-sm);
+ line-height: 1.5;
+}
+
+.card li:last-child { margin-bottom: 0; }
+
+/* Panel (full-width container) */
+.panel {
+ background-color: var(--color-surface-elevated);
+ border-radius: var(--radius-xl);
+ box-shadow: var(--shadow-lg);
+ padding: var(--space-xl);
+ width: 100%;
+}
+
+.panel--sm { max-width: 400px; }
+.panel--md { max-width: 600px; }
+.panel--lg { max-width: 750px; }
+.panel--xl { max-width: 1000px; }
+
+.panel__title {
+ margin: 0 0 var(--space-lg) 0;
+ font-size: var(--font-xl);
+ font-weight: 600;
+}
+
+/* ========================================
+ Tabs
+ ======================================== */
+.tabs {
+ width: 100%;
+}
+
+.tabs__nav {
+ display: flex;
+ gap: 0;
+ border-bottom: 1px solid var(--color-border);
+ margin-bottom: var(--space-lg);
+ overflow: visible;
+}
+
+.tabs__btn {
+ display: inline-flex;
+ align-items: center;
+ gap: var(--space-sm);
+ padding: 0.65rem 1.25rem;
+ background: none;
+ border: none;
+ font-family: inherit;
+ font-size: var(--font-md);
+ font-weight: 500;
+ color: var(--color-text-muted);
+ cursor: pointer;
+ border-bottom: 2px solid transparent;
+ margin-bottom: -1px;
+ transition:
+ color var(--transition-normal),
+ border-color var(--transition-normal);
+ white-space: nowrap;
+}
+
+.tabs__btn:hover { color: var(--color-text); }
+
+.tabs__btn:focus-visible {
+ outline: 2px solid var(--vscode-focusBorder);
+ outline-offset: -2px;
+}
+
+.tabs__btn.active {
+ color: var(--color-accent);
+ border-bottom-color: var(--color-accent);
+}
+
+.tabs__content {
+ display: none;
+ animation: tabFadeIn var(--transition-slow) ease-out;
+}
+
+.tabs__content.active { display: block; }
+
+@keyframes tabFadeIn {
+ from {
+ opacity: 0;
+ transform: translateY(4px);
+ }
+ to {
+ opacity: 1;
+ transform: translateY(0);
+ }
+}
+
+/* Nested tabs */
+.tabs .tabs { margin-top: var(--space-lg); }
+.tabs .tabs .tabs__nav { margin-bottom: var(--space-md); }
+
+/* ========================================
+ Form Inputs
+ ======================================== */
+.form-group {
+ display: flex;
+ flex-direction: column;
+ gap: var(--space-xs);
+ min-width: 200px;
+}
+
+.form-group__label {
+ font-size: var(--font-sm);
+ font-weight: 600;
+ color: var(--color-text-muted);
+ text-transform: uppercase;
+ letter-spacing: 0.5px;
+}
+
+.form-group__hint {
+ font-size: var(--font-xs);
+ color: var(--color-text-muted);
+ margin-top: var(--space-xs);
+}
+
+.input {
+ width: 100%;
+ background-color: var(--vscode-input-background);
+ color: var(--vscode-input-foreground);
+ border: 1px solid var(--vscode-input-border, var(--color-border));
+ border-radius: var(--radius-sm);
+ padding: 0.5rem 0.75rem;
+ font-family: inherit;
+ font-size: var(--font-md);
+ line-height: 1.4;
+ transition: border-color var(--transition-normal);
+}
+
+.input:focus {
+ outline: none;
+ border-color: var(--vscode-focusBorder);
+}
+
+.input:disabled {
+ opacity: 0.5;
+ cursor: not-allowed;
+}
+
+.input::placeholder {
+ color: var(--vscode-input-placeholderForeground);
+}
+
+.select {
+ width: 100%;
+ background-color: var(--vscode-dropdown-background);
+ color: var(--vscode-dropdown-foreground);
+ border: 1px solid var(--vscode-dropdown-border, var(--color-border));
+ border-radius: var(--radius-sm);
+ padding: 0.5rem 0.75rem;
+ font-family: inherit;
+ font-size: var(--font-md);
+ cursor: pointer;
+ appearance: none;
+ background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 12 12'%3E%3Cpath fill='%23888' d='M2 4l4 4 4-4'/%3E%3C/svg%3E");
+ background-repeat: no-repeat;
+ background-position: right 0.75rem center;
+ padding-right: 2rem;
+}
+
+.select:focus {
+ outline: none;
+ border-color: var(--vscode-focusBorder);
+}
+
+.settings-row {
+ display: flex;
+ gap: var(--space-lg);
+ flex-wrap: wrap;
+}
+
+.settings-row .form-group { flex: 1; }
+
+.setting-card {
+ background-color: var(--color-surface);
+ border: 1px solid var(--color-border);
+ border-radius: var(--radius-md);
+ padding: 0.75rem 1rem;
+}
+
+/* ========================================
+ Badges
+ ======================================== */
+.badge {
+ display: inline-flex;
+ align-items: center;
+ padding: 0.15rem 0.5rem;
+ font-size: var(--font-xs);
+ font-weight: 500;
+ border-radius: var(--radius-sm);
+ background-color: var(--vscode-badge-background);
+ color: var(--vscode-badge-foreground);
+ white-space: nowrap;
+}
+
+.badge--primary {
+ background-color: var(--cld-sky-blue);
+ color: white;
+}
+
+.badge--pill { border-radius: var(--radius-full); }
+
+/* ========================================
+ Info Rows
+ ======================================== */
+.info-row {
+ display: flex;
+ justify-content: space-between;
+ align-items: flex-start;
+ padding: var(--space-sm) 0;
+ border-bottom: 1px solid var(--color-border);
+ gap: var(--space-md);
+}
+
+.info-row:last-child { border-bottom: none; }
+
+.info-row__label {
+ color: var(--color-text-muted);
+ font-size: var(--font-sm);
+ flex-shrink: 0;
+ min-width: 120px;
+}
+
+.info-row__value {
+ font-size: var(--font-sm);
+ text-align: right;
+ word-break: break-word;
+ display: flex;
+ align-items: center;
+ gap: var(--space-sm);
+ flex-wrap: wrap;
+ justify-content: flex-end;
+}
+
+/* URL Items */
+.url-item {
+ margin-bottom: var(--space-lg);
+}
+
+.url-item:last-child { margin-bottom: 0; }
+
+.url-item__label {
+ font-size: var(--font-sm);
+ color: var(--color-text-muted);
+ margin-bottom: var(--space-xs);
+}
+
+.url-item__value {
+ display: flex;
+ align-items: center;
+ gap: var(--space-sm);
+ flex-wrap: wrap;
+}
+
+.url-item__link {
+ font-size: var(--font-sm);
+ word-break: break-all;
+ flex: 1;
+ min-width: 0;
+}
+
+/* ========================================
+ Progress Bars
+ ======================================== */
+.progress {
+ width: 100%;
+ height: 6px;
+ background-color: var(--vscode-progressBar-background, rgba(0, 120, 212, 0.2));
+ border-radius: 3px;
+ overflow: hidden;
+}
+
+.progress__bar {
+ height: 100%;
+ background-color: var(--vscode-progressBar-foreground, #0078d4);
+ border-radius: 3px;
+ transition: width var(--transition-normal) ease-out;
+}
+
+/* Queue items */
+.queue-item {
+ display: flex;
+ align-items: center;
+ gap: var(--space-md);
+ padding: 0.6rem 0.75rem;
+ background: var(--color-surface);
+ border-radius: var(--radius-md);
+ border: 1px solid var(--color-border);
+ margin-bottom: var(--space-sm);
+}
+
+.queue-item:last-child { margin-bottom: 0; }
+
+.queue-item__name {
+ flex-shrink: 0;
+ max-width: 200px;
+ font-size: var(--font-sm);
+ white-space: nowrap;
+ overflow: hidden;
+ text-overflow: ellipsis;
+}
+
+.queue-item__progress {
+ flex: 1;
+ min-width: 100px;
+}
+
+.queue-item__status {
+ flex-shrink: 0;
+ font-size: var(--font-sm);
+ color: var(--color-text-muted);
+ min-width: 80px;
+ text-align: right;
+}
+
+.queue-item--complete .progress__bar { background-color: var(--color-success); }
+.queue-item--complete .queue-item__status { color: var(--color-success); }
+.queue-item--error .progress__bar { background-color: var(--color-error); }
+.queue-item--error .queue-item__status { color: var(--color-error); }
+
+.upload-queue {
+ margin-top: var(--space-lg);
+}
+
+.upload-queue:empty { display: none; }
+
+/* ========================================
+ Drop Zone
+ ======================================== */
+.drop-zone {
+ border: 2px dashed var(--color-border);
+ border-radius: var(--radius-xl);
+ padding: var(--space-xxl) var(--space-xl);
+ text-align: center;
+ background-color: var(--color-surface);
+ transition:
+ border-color var(--transition-normal),
+ background-color var(--transition-normal);
+ cursor: pointer;
+}
+
+.drop-zone:hover,
+.drop-zone.drag-over {
+ border-color: var(--vscode-focusBorder);
+ background-color: rgba(0, 120, 212, 0.05);
+}
+
+.drop-zone.drag-over { border-style: solid; }
+
+.drop-zone__icon {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ margin-bottom: var(--space-md);
+ color: var(--color-accent);
+ opacity: 0.8;
+}
+
+.drop-zone__icon svg {
+ width: 48px;
+ height: 48px;
+}
+
+.drop-zone__text {
+ margin: var(--space-xs) 0;
+ color: var(--color-text-muted);
+ font-size: var(--font-md);
+}
+
+.drop-zone__hint {
+ margin: var(--space-md) 0;
+ font-size: var(--font-sm);
+ color: var(--color-text-muted);
+ opacity: 0.6;
+}
+
+.drop-zone__input { display: none; }
+
+/* ========================================
+ Asset Grid & Cards
+ ======================================== */
+.asset-grid {
+ display: grid;
+ grid-template-columns: repeat(auto-fill, minmax(150px, 1fr));
+ gap: var(--space-lg);
+}
+
+.asset-card {
+ background: var(--color-surface);
+ border: 1px solid var(--color-border);
+ border-radius: var(--radius-lg);
+ padding: 0.6rem;
+ text-align: center;
+ transition:
+ border-color var(--transition-normal),
+ transform var(--transition-normal);
+}
+
+.asset-card:hover {
+ border-color: var(--vscode-focusBorder);
+ transform: translateY(-2px);
+}
+
+.asset-card__thumbnail {
+ position: relative;
+ cursor: pointer;
+}
+
+.asset-card__image {
+ width: 130px;
+ height: 100px;
+ object-fit: cover;
+ border-radius: var(--radius-md);
+ background: var(--color-surface-elevated);
+}
+
+.asset-card__icon {
+ width: 130px;
+ height: 100px;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ background: var(--color-surface-elevated);
+ border-radius: var(--radius-md);
+ color: var(--color-text-muted);
+}
+
+.asset-card__folder {
+ font-size: var(--font-xs);
+ color: var(--color-text-muted);
+ margin: var(--space-xs) 0;
+}
+
+.asset-card__id {
+ font-size: var(--font-xs);
+ color: var(--color-text-muted);
+ margin: var(--space-xs) 0;
+ word-break: break-all;
+ max-height: 2.4em;
+ overflow: hidden;
+}
+
+.asset-card__actions {
+ display: flex;
+ gap: var(--space-xs);
+ justify-content: center;
+ flex-wrap: wrap;
+}
+
+.uploaded-assets {
+ margin-top: var(--space-xl);
+ padding-top: var(--space-xl);
+ border-top: 1px solid var(--color-border);
+}
+
+.uploaded-assets.hidden { display: none; }
+
+.uploaded-assets__header {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ margin-bottom: var(--space-lg);
+}
+
+.uploaded-assets__title {
+ margin: 0;
+ font-size: var(--font-lg);
+ font-weight: 600;
+}
+
+/* ========================================
+ Lightbox & Modal
+ ======================================== */
+.lightbox {
+ display: none;
+ position: fixed;
+ top: 0;
+ left: 0;
+ width: 100%;
+ height: 100%;
+ background: rgba(0, 0, 0, 0.9);
+ z-index: 1000;
+ justify-content: center;
+ align-items: center;
+ padding: var(--space-xxl);
+}
+
+.lightbox.active { display: flex; }
+
+.lightbox__content {
+ max-width: 95%;
+ max-height: 95%;
+ object-fit: contain;
+ border-radius: var(--radius-lg);
+}
+
+.lightbox__close {
+ position: absolute;
+ top: var(--space-lg);
+ right: var(--space-lg);
+ background: rgba(255, 255, 255, 0.1);
+ border: none;
+ border-radius: var(--radius-full);
+ width: 40px;
+ height: 40px;
+ cursor: pointer;
+ color: white;
+ font-size: 1.5rem;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ transition: background-color var(--transition-normal);
+}
+
+.lightbox__close:hover {
+ background: rgba(255, 255, 255, 0.2);
+}
+
+/* ========================================
+ Collapsible
+ ======================================== */
+.collapsible {
+ margin-bottom: var(--space-lg);
+}
+
+.collapsible__header {
+ display: flex;
+ align-items: center;
+ gap: var(--space-sm);
+ padding: var(--space-sm) 0;
+ cursor: pointer;
+ user-select: none;
+ color: var(--color-text-muted);
+ font-size: var(--font-sm);
+ transition: color var(--transition-normal);
+}
+
+.collapsible__header:hover { color: var(--color-text); }
+
+.collapsible__header::before {
+ content: '▶';
+ font-size: 0.6rem;
+ transition: transform var(--transition-normal);
+}
+
+.collapsible__header.expanded::before {
+ transform: rotate(90deg);
+}
+
+.collapsible__content {
+ display: none;
+ padding: var(--space-md) 0;
+}
+
+.collapsible__content.visible { display: block; }
+
+/* ========================================
+ Layout Components
+ ======================================== */
+
+/* Asset header */
+.asset-header {
+ display: flex;
+ align-items: center;
+ gap: var(--space-md);
+ margin-bottom: var(--space-lg);
+ padding-bottom: var(--space-md);
+ border-bottom: 1px solid var(--color-border);
+}
+
+.asset-header__icon {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ color: var(--color-accent);
+ flex-shrink: 0;
+}
+
+.asset-header__icon svg {
+ width: 28px;
+ height: 28px;
+}
+
+.asset-header__content {
+ flex: 1;
+ min-width: 0;
+}
+
+.asset-header__title {
+ margin: 0;
+ font-size: var(--font-lg);
+ font-weight: 600;
+ white-space: nowrap;
+ overflow: hidden;
+ text-overflow: ellipsis;
+}
+
+.asset-header__subtitle {
+ font-size: var(--font-sm);
+ color: var(--color-text-muted);
+ margin-top: 0.2rem;
+ display: flex;
+ align-items: center;
+ gap: var(--space-sm);
+}
+
+/* Hero section */
+.hero {
+ text-align: center;
+ margin-bottom: var(--space-xxl);
+ padding: var(--space-xxl);
+ background: linear-gradient(135deg, var(--cld-brand-blue) 0%, var(--cld-sky-blue) 100%);
+ border-radius: var(--radius-xxl);
+ color: white;
+}
+
+.hero__title {
+ margin: 0;
+ font-size: 2.5rem;
+ font-weight: 700;
+ color: white;
+}
+
+.hero__subtitle {
+ margin: var(--space-sm) 0 0 0;
+ font-size: 1.1rem;
+ opacity: 0.9;
+ color: white;
+}
+
+/* Status card */
+.status-card {
+ background-color: var(--color-surface-elevated);
+ border: 1px solid var(--color-border);
+ border-radius: var(--radius-lg);
+ padding: var(--space-xl);
+ margin-bottom: var(--space-xxl);
+ display: flex;
+ align-items: center;
+ gap: var(--space-lg);
+}
+
+.status-card__icon {
+ font-size: 1.5rem;
+ width: 2.5rem;
+ height: 2.5rem;
+ border-radius: var(--radius-full);
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ color: white;
+ flex-shrink: 0;
+}
+
+.status-card__icon--success { background-color: var(--color-success); }
+.status-card__icon--warning { background-color: var(--cld-pink); }
+
+.status-card__content { flex: 1; }
+
+.status-card__title {
+ font-weight: 600;
+ margin-bottom: var(--space-xs);
+}
+
+.status-card__text {
+ margin: 0;
+ color: var(--color-text-muted);
+}
+
+/* Step list */
+.step-list {
+ display: flex;
+ flex-direction: column;
+ gap: var(--space-xl);
+}
+
+.step {
+ display: flex;
+ align-items: flex-start;
+ gap: var(--space-lg);
+}
+
+.step__number {
+ background-color: var(--cld-brand-blue);
+ color: white;
+ border-radius: var(--radius-full);
+ width: 2rem;
+ height: 2rem;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ font-weight: 600;
+ flex-shrink: 0;
+}
+
+.step__content { flex: 1; }
+
+.step__title {
+ margin: 0 0 var(--space-sm) 0;
+ font-size: var(--font-lg);
+ font-weight: 600;
+}
+
+.step__description {
+ margin: 0 0 var(--space-sm) 0;
+ color: var(--color-text-muted);
+}
+
+/* Feature grid */
+.feature-grid {
+ display: grid;
+ grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
+ gap: var(--space-xl);
+}
+
+.feature-item {
+ background-color: var(--color-surface-elevated);
+ border: 1px solid var(--color-border);
+ border-radius: var(--radius-lg);
+ padding: var(--space-xl);
+ text-align: center;
+ transition: transform var(--transition-normal);
+}
+
+.feature-item:hover { transform: translateY(-2px); }
+
+.feature-item__title {
+ font-size: var(--font-xl);
+ font-weight: 600;
+ margin-bottom: var(--space-sm);
+}
+
+.feature-item__description {
+ color: var(--color-text-muted);
+ font-size: var(--font-md);
+}
+
+/* Highlight box */
+.highlight-box {
+ background: linear-gradient(135deg, var(--cld-brand-blue) 0%, var(--cld-purple) 100%);
+ color: white;
+ padding: var(--space-xxl);
+ border-radius: var(--radius-xxl);
+ margin: var(--space-xxl) 0;
+ text-align: center;
+}
+
+.highlight-box__title {
+ margin: 0 0 var(--space-lg) 0;
+ font-size: 1.5rem;
+ color: white;
+}
+
+.highlight-box__text {
+ margin: 0 0 var(--space-xl) 0;
+ opacity: 0.9;
+ color: white;
+}
+
+/* Info/Warning boxes */
+.info-box {
+ background-color: rgba(72, 196, 216, 0.1);
+ border: 1px solid var(--cld-turquoise);
+ border-radius: var(--radius-md);
+ padding: var(--space-lg);
+ margin: var(--space-lg) 0;
+}
+
+.info-box strong { color: var(--cld-turquoise); }
+
+/* Code block */
+.code-block {
+ background-color: var(--color-surface);
+ border: 1px solid var(--color-border);
+ border-radius: var(--radius-md);
+ padding: var(--space-lg);
+ margin: var(--space-lg) 0;
+ font-family: var(--vscode-editor-font-family, monospace);
+ font-size: var(--font-sm);
+ position: relative;
+ overflow-x: auto;
+}
+
+.code-block pre {
+ margin: 0;
+ white-space: pre-wrap;
+}
+
+.code-block__copy {
+ position: absolute;
+ top: var(--space-sm);
+ right: var(--space-sm);
+}
+
+/* Grid layouts */
+.grid-2 {
+ display: grid;
+ grid-template-columns: repeat(2, 1fr);
+ gap: var(--space-xl);
+}
+
+@media (max-width: 768px) {
+ .grid-2 { grid-template-columns: 1fr; }
+}
+
+/* Preview container */
+.preview-container {
+ position: relative;
+ display: inline-block;
+ width: 100%;
+ margin-bottom: var(--space-lg);
+ background: var(--color-surface);
+ border-radius: var(--radius-lg);
+ overflow: hidden;
+ border: 1px solid var(--color-border);
+}
+
+.preview-container__media {
+ display: block;
+ max-width: 100%;
+ max-height: 250px;
+ width: auto;
+ margin: 0 auto;
+ border-radius: var(--radius-md);
+}
+
+.preview-container__enlarge {
+ position: absolute;
+ top: var(--space-sm);
+ right: var(--space-sm);
+ background: rgba(0, 0, 0, 0.7);
+ border: none;
+ border-radius: var(--radius-md);
+ padding: var(--space-sm);
+ cursor: pointer;
+ color: white;
+ opacity: 0;
+ transition: opacity var(--transition-normal), background var(--transition-normal);
+ display: flex;
+ align-items: center;
+ justify-content: center;
+}
+
+.preview-container:hover .preview-container__enlarge { opacity: 1; }
+.preview-container__enlarge:hover { background: rgba(0, 0, 0, 0.9); }
+
+/* Raw file preview */
+.raw-file-preview {
+ text-align: center;
+ padding: var(--space-xxl);
+ background: var(--color-surface);
+ border-radius: var(--radius-lg);
+ border: 1px solid var(--color-border);
+ margin-bottom: var(--space-lg);
+}
+
+.raw-file-preview__icon {
+ margin-bottom: var(--space-md);
+ color: var(--color-text-muted);
+}
+
+.raw-file-preview__name {
+ font-size: var(--font-md);
+ color: var(--color-text-muted);
+ margin: var(--space-sm) 0;
+ word-break: break-all;
+}
+
+/* Preset details */
+.preset-toggle {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+}
+
+.preset-toggle__btn {
+ background: none;
+ border: none;
+ color: var(--color-accent);
+ cursor: pointer;
+ padding: 0;
+ font-size: var(--font-xs);
+ display: flex;
+ align-items: center;
+ gap: var(--space-xs);
+}
+
+.preset-toggle__btn::before {
+ content: '▶';
+ font-size: 0.55rem;
+ transition: transform var(--transition-normal);
+}
+
+.preset-toggle__btn.expanded::before { transform: rotate(90deg); }
+
+.preset-details {
+ margin-top: var(--space-sm);
+ padding: var(--space-sm);
+ background-color: var(--color-surface);
+ border-radius: var(--radius-sm);
+ font-size: var(--font-xs);
+ font-family: var(--vscode-editor-font-family, monospace);
+ white-space: pre-wrap;
+ max-height: 150px;
+ overflow-y: auto;
+ border: 1px solid var(--color-border);
+ display: none;
+ color: var(--color-text-muted);
+}
+
+.preset-details.visible { display: block; }
+
+/* URL input group */
+.url-input-group {
+ display: flex;
+ gap: var(--space-sm);
+ margin-bottom: var(--space-md);
+}
+
+.url-input-group .input { flex: 1; }
+
+/* Metadata section */
+.meta-section {
+ margin-bottom: var(--space-lg);
+}
+
+.meta-section:last-child { margin-bottom: 0; }
+
+.meta-section__title {
+ font-size: var(--font-sm);
+ font-weight: 600;
+ color: var(--color-text-muted);
+ text-transform: uppercase;
+ letter-spacing: 0.5px;
+ margin-bottom: var(--space-sm);
+}
+
+.meta-section__empty {
+ color: var(--color-text-muted);
+ font-size: var(--font-sm);
+ font-style: italic;
+}
+
+/* Links */
+.link {
+ color: var(--color-accent);
+ cursor: pointer;
+ text-decoration: none;
+ transition: color var(--transition-normal);
+}
+
+.link:hover { text-decoration: underline; }
+
+/* Tags display */
+.meta-tags {
+ display: flex;
+ flex-wrap: wrap;
+ gap: var(--space-sm);
+}
diff --git a/media/styles/tokens.css b/media/styles/tokens.css
new file mode 100644
index 0000000..7e2ac08
--- /dev/null
+++ b/media/styles/tokens.css
@@ -0,0 +1,90 @@
+/**
+ * Design tokens for the Cloudinary VS Code extension webviews.
+ * Based on Cloudinary Brand Guidelines: https://brand-guidelines.cloudinary.com/
+ */
+
+:root {
+ /* ========================================
+ Cloudinary Brand Colors
+ ======================================== */
+ --cld-brand-blue: #3448C5;
+ --cld-sky-blue: #0D9AFF;
+ --cld-grey: #E3E9EF;
+ --cld-aegean: #23436A;
+ --cld-cetacean: #1B295D;
+
+ /* Accent colors */
+ --cld-turquoise: #48C4D8;
+ --cld-pink: #FE5981;
+ --cld-green: #D5FDA1;
+ --cld-teal: #60CFB7;
+ --cld-purple: #A15EE4;
+
+ /* ========================================
+ Semantic Color Mappings
+ Maps VS Code variables with Cloudinary fallbacks
+ ======================================== */
+
+ /* Primary accent - used for links, active states */
+ --color-accent: var(--vscode-textLink-foreground, var(--cld-sky-blue));
+ --color-accent-hover: var(--vscode-textLink-activeForeground, var(--cld-sky-blue));
+
+ /* Status colors */
+ --color-success: var(--vscode-testing-iconPassed, var(--cld-teal));
+ --color-error: var(--vscode-testing-iconFailed, var(--cld-pink));
+ --color-warning: var(--cld-pink);
+ --color-info: var(--cld-turquoise);
+
+ /* Surface colors */
+ --color-surface: var(--vscode-editor-background);
+ --color-surface-elevated: var(--vscode-editorWidget-background);
+ --color-border: var(--vscode-editorWidget-border);
+
+ /* Text colors */
+ --color-text: var(--vscode-editor-foreground);
+ --color-text-muted: var(--vscode-descriptionForeground);
+
+ /* ========================================
+ Spacing Scale
+ ======================================== */
+ --space-xs: 0.25rem;
+ --space-sm: 0.5rem;
+ --space-md: 0.75rem;
+ --space-lg: 1rem;
+ --space-xl: 1.5rem;
+ --space-xxl: 2rem;
+
+ /* ========================================
+ Border Radius
+ ======================================== */
+ --radius-sm: 4px;
+ --radius-md: 6px;
+ --radius-lg: 8px;
+ --radius-xl: 10px;
+ --radius-xxl: 12px;
+ --radius-full: 50%;
+
+ /* ========================================
+ Font Sizes
+ ======================================== */
+ --font-xs: 0.65rem;
+ --font-sm: 0.75rem;
+ --font-md: 0.85rem;
+ --font-lg: 1rem;
+ --font-xl: 1.15rem;
+ --font-xxl: 1.25rem;
+
+ /* ========================================
+ Transitions
+ ======================================== */
+ --transition-fast: 0.1s;
+ --transition-normal: 0.15s;
+ --transition-slow: 0.2s;
+
+ /* ========================================
+ Shadows
+ ======================================== */
+ --shadow-sm: 0 2px 8px rgba(0, 0, 0, 0.15);
+ --shadow-md: 0 4px 12px rgba(0, 0, 0, 0.2);
+ --shadow-lg: 0 4px 20px rgba(0, 0, 0, 0.25);
+}
diff --git a/src/commands/previewAsset.ts b/src/commands/previewAsset.ts
index 68f7ccd..b80687b 100644
--- a/src/commands/previewAsset.ts
+++ b/src/commands/previewAsset.ts
@@ -1,713 +1,336 @@
import * as vscode from "vscode";
+import { createWebviewDocument, getScriptUri } 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, "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);
+ });
+
+ // Get the preview script
+ const previewScriptUri = getScriptUri(
+ panel.webview,
+ context.extensionUri,
+ "preview.js"
+ );
+
+ // Set the HTML content
+ panel.webview.html = createWebviewDocument({
+ title: asset.public_id,
+ webview: panel.webview,
+ extensionUri: context.extensionUri,
+ bodyContent: getPreviewContent(asset),
+ bodyClass: "layout-centered",
+ additionalScripts: [previewScriptUri],
+ });
+
+ // 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 display name (last part of public_id)
- const displayName = asset.public_id.includes('/')
- ? asset.public_id.split('/').pop()
- : asset.public_id;
-
- // SVG icons
- const icons = {
- image: ``,
- video: ``,
- file: ``,
- download: ``
- };
-
- // Determine asset type icon
- const typeIcon = asset.displayType === 'image' ? icons.image : asset.displayType === 'video' ? icons.video : icons.file;
-
- let previewHtml = "";
- let hasEnlarge = false;
-
- if (asset.displayType === "image") {
- hasEnlarge = true;
- previewHtml = `
-
-

-
-
- `;
- } else if (asset.displayType === "video") {
- hasEnlarge = true;
- previewHtml = `
-
- `;
- } else {
- previewHtml = `
-
- `;
- }
-
- panel.webview.html = `
-
-
-
-
-
- ${asset.public_id}
-
-
-
-
-
-