Skip to content

Commit 1afaa5a

Browse files
authored
Add New Icons to Webview (#87)
1 parent 4b063e9 commit 1afaa5a

13 files changed

Lines changed: 180 additions & 86 deletions

File tree

client/.vscodeignore

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,12 @@ vsc-extension-quickstart.md
1111
# Build artifacts
1212
out/**
1313
node_modules/**
14+
!node_modules/
15+
!node_modules/@vscode/
16+
!node_modules/@vscode/codicons/
17+
!node_modules/@vscode/codicons/dist/
18+
!node_modules/@vscode/codicons/dist/codicon.css
19+
!node_modules/@vscode/codicons/dist/codicon.ttf
1420
*.vsix
1521

1622
# Test files

client/package-lock.json

Lines changed: 12 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

client/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -178,6 +178,7 @@
178178
"webpack-cli": "^5.1.4"
179179
},
180180
"dependencies": {
181+
"@vscode/codicons": "^0.0.45",
181182
"ftp": "^0.3.10",
182183
"get-port": "^5.1.1",
183184
"jsonc-parser": "^0.4.2",

client/src/webview/clipboard.ts

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
const COPY_BUTTON_RESET_MS = 2000;
2+
3+
export async function copyToClipboard(button: HTMLButtonElement, text: string) {
4+
const originalTitle = button.getAttribute('title');
5+
6+
try {
7+
button.disabled = true;
8+
await navigator.clipboard.writeText(text);
9+
button.setAttribute('title', 'Copied!');
10+
} catch (e) {
11+
button.setAttribute('title', 'Copy failed');
12+
} finally {
13+
setTimeout(() => {
14+
if (originalTitle !== null) {
15+
button.setAttribute('title', originalTitle);
16+
} else {
17+
button.removeAttribute('title');
18+
}
19+
button.disabled = false;
20+
}, COPY_BUTTON_RESET_MS);
21+
}
22+
}

client/src/webview/diagram.ts

Lines changed: 8 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,12 @@
11
import type { LJStateMachine } from "../types/fsm";
2+
import { copyToClipboard } from "./clipboard";
23

34
// constants
45
const MIN_ZOOM = 0.2;
56
const MAX_ZOOM = 5;
67
const ZOOM_BUTTON_FACTOR = 1.5;
78
const SCROLL_ZOOM_IN_FACTOR = 1.05;
89
const SCROLL_ZOOM_OUT_FACTOR = 0.95;
9-
const COPY_TIMEOUT_MS = 2000;
1010

1111
// state variables
1212
let zoomLevel = 1;
@@ -51,18 +51,15 @@ export function createMermaidDiagram(sm: LJStateMachine | undefined, orientation
5151
// add transitions
5252
transitionMap.forEach((labels, key) => {
5353
const [from, to] = key.split('|');
54-
const mergedLabel = labels.join(', ');
54+
const mergedLabel = labels.join('<br/>');
5555
lines.push(` ${from} --> ${to} : ${mergedLabel}`);
5656
});
5757

5858
return lines.join('\n');
5959
}
6060

6161
function getTransitionLabel(label: string, preCond?: string | null, postCond?: string | null, showConditions = false): string {
62-
if (!showConditions) {
63-
return escapeMermaidLabel(label);
64-
}
65-
62+
if (!showConditions) return escapeMermaidLabel(label);
6663
return [
6764
getConditionLabel('pre', preCond),
6865
escapeMermaidLabel(label),
@@ -75,9 +72,8 @@ function getInitialTransitionLabel(postCond?: string | null, showConditions = fa
7572
}
7673

7774
function getConditionLabel(kind: 'pre' | 'post', cond?: string | null): string {
78-
if (!cond) {
79-
return '';
80-
}
75+
if (!cond) return '';
76+
8177
return `<span class="state-cond state-cond-${kind}">${escapeMermaidLabel(cond)}</span>`;
8278
}
8379

@@ -101,6 +97,8 @@ export async function renderMermaidDiagram(document: any, window: any) {
10197
await mermaid.run({ nodes: mermaidElements });
10298
applyTransform(document);
10399
registerPanListeners(document);
100+
const diagramContainer = document.querySelector('.diagram-container') as HTMLElement | null;
101+
if (diagramContainer) diagramContainer.style.minHeight = '';
104102
} catch (e) {
105103
console.error('Failed to render Mermaid diagram:', e);
106104
}
@@ -241,20 +239,5 @@ export function registerPanListeners(document: any) {
241239
}
242240

243241
export async function copyDiagramToClipboard(target: any, diagram: string) {
244-
const title = target.getAttribute('title');
245-
try {
246-
target.disabled = true;
247-
await navigator.clipboard.writeText(diagram);
248-
target.classList.add('copied');
249-
target.setAttribute('title', 'Copied!');
250-
} catch (e) {
251-
target.setAttribute('title', 'Copy failed');
252-
} finally {
253-
// reset button after timeout
254-
setTimeout(() => {
255-
target.setAttribute('title', title);
256-
target.classList.remove('copied');
257-
target.disabled = false;
258-
}, COPY_TIMEOUT_MS);
259-
}
242+
await copyToClipboard(target, diagram);
260243
}

client/src/webview/html.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ export function getHtml(webview: vscode.Webview, extensionUri: vscode.Uri): stri
1313
const nonce = Date.now().toString();
1414
const cspSource = webview.cspSource;
1515
const scriptUri = webview.asWebviewUri(vscode.Uri.joinPath(extensionUri, "media", "webview.js"));
16+
const codiconsUri = webview.asWebviewUri(vscode.Uri.joinPath(extensionUri, "node_modules", "@vscode", "codicons", "dist", "codicon.css"));
1617
return /*html*/ `
1718
<!DOCTYPE html>
1819
<html lang="en">
@@ -21,8 +22,9 @@ export function getHtml(webview: vscode.Webview, extensionUri: vscode.Uri): stri
2122
<meta name="viewport" content="width=device-width, initial-scale=1.0">
2223
<meta
2324
http-equiv="Content-Security-Policy"
24-
content="default-src 'none'; style-src ${cspSource} 'unsafe-inline'; script-src 'nonce-${nonce}' https://cdn.jsdelivr.net; connect-src https://cdn.jsdelivr.net;"
25+
content="default-src 'none'; style-src ${cspSource} 'unsafe-inline'; font-src ${cspSource}; script-src 'nonce-${nonce}' https://cdn.jsdelivr.net; connect-src https://cdn.jsdelivr.net;"
2526
>
27+
<link href="${codiconsUri}" rel="stylesheet">
2628
<style>${getStyles()}</style>
2729
</head>
2830
<body>

client/src/webview/icons.ts

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
export function renderCodicon(name: string, className = ""): string {
2+
const classes = ["codicon", `codicon-${name}`, className].filter(Boolean).join(" ");
3+
return `<span class="${classes}" aria-hidden="true"></span>`;
4+
}
5+
6+
type CodiconButtonOptions = {
7+
id?: string;
8+
className?: string;
9+
title: string;
10+
ariaLabel?: string;
11+
attributes?: string;
12+
disabled?: boolean;
13+
};
14+
15+
export function renderCodiconButton(iconName: string, options: CodiconButtonOptions): string {
16+
const classes = ["icon-button", options.className].filter(Boolean).join(" ");
17+
const id = options.id ? ` id="${options.id}"` : "";
18+
const ariaLabel = ` aria-label="${options.ariaLabel ?? options.title}"`;
19+
const attributes = options.attributes ? ` ${options.attributes}` : "";
20+
const disabled = options.disabled ? " disabled" : "";
21+
22+
return `<button${id} class="${classes}" title="${options.title}"${ariaLabel}${attributes}${disabled} type="button">${renderCodicon(iconName)}</button>`;
23+
}

client/src/webview/script.ts

Lines changed: 19 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,12 @@ export function getScript(vscode: VSCodeApi, document: Document, window: Window)
5454
root.addEventListener('click', (e: MouseEvent) => {
5555
const target = e.target instanceof Element ? e.target : null;
5656
if (!target) return;
57+
const iconButton = target.closest?.('.icon-button');
58+
if (iconButton && !(iconButton as HTMLButtonElement).disabled) {
59+
iconButton.classList.remove('icon-button-pop');
60+
void (iconButton as HTMLElement).offsetWidth;
61+
iconButton.classList.add('icon-button-pop');
62+
}
5763

5864
// context section toggle
5965
const contextToggleButton = target.closest?.('.context-toggle-btn');
@@ -77,7 +83,8 @@ export function getScript(vscode: VSCodeApi, document: Document, window: Window)
7783

7884
const icon = contextToggleButton.querySelector('.context-toggle-icon');
7985
if (icon) {
80-
icon.textContent = nextExpanded ? '▾' : '▸';
86+
icon.classList.toggle('codicon-triangle-down', nextExpanded);
87+
icon.classList.toggle('codicon-triangle-right', !nextExpanded);
8188
}
8289

8390
return;
@@ -143,7 +150,7 @@ export function getScript(vscode: VSCodeApi, document: Document, window: Window)
143150
}
144151

145152
// toggle diagram orientation
146-
if (target.id === 'diagram-orientation-btn') {
153+
if (target.closest?.('#diagram-orientation-btn')) {
147154
e.stopPropagation();
148155
diagramOrientation = diagramOrientation === "TB" ? "LR" : "TB";
149156
resetZoom(document);
@@ -152,40 +159,42 @@ export function getScript(vscode: VSCodeApi, document: Document, window: Window)
152159
}
153160

154161
// toggle diagram conditions
155-
if (target.id === 'diagram-conditions-btn') {
162+
const diagramConditionsButton = target.closest?.('#diagram-conditions-btn');
163+
if (diagramConditionsButton) {
156164
e.stopPropagation();
157-
if ((target as HTMLButtonElement).disabled) return;
165+
if ((diagramConditionsButton as HTMLButtonElement).disabled) return;
158166
showDiagramConditions = !showDiagramConditions;
159167
updateView();
160168
return;
161169
}
162170

163171
// zoom in
164-
if (target.id === 'zoom-in-btn') {
172+
if (target.closest?.('#zoom-in-btn')) {
165173
e.stopPropagation();
166174
zoomIn(document);
167175
return;
168176
}
169177

170178
// zoom out
171-
if (target.id === 'zoom-out-btn') {
179+
if (target.closest?.('#zoom-out-btn')) {
172180
e.stopPropagation();
173181
zoomOut(document);
174182
return;
175183
}
176184

177185
// reset zoom
178-
if (target.id === 'zoom-reset-btn') {
186+
if (target.closest?.('#zoom-reset-btn')) {
179187
e.stopPropagation();
180188
resetZoom(document);
181189
return;
182190
}
183191

184192
// copy diagram source
185-
if (target.id === 'copy-diagram-btn') {
193+
const copyDiagramButton = target.closest?.('#copy-diagram-btn');
194+
if (copyDiagramButton) {
186195
e.stopPropagation();
187196
if (!currentDiagram) return
188-
copyDiagramToClipboard(target, currentDiagram);
197+
copyDiagramToClipboard(copyDiagramButton, currentDiagram);
189198
return;
190199
}
191200

@@ -315,7 +324,7 @@ export function getScript(vscode: VSCodeApi, document: Document, window: Window)
315324
case 'fsm': {
316325
const diagram = createMermaidDiagram(stateMachine, diagramOrientation, showDiagramConditions);
317326
currentDiagram = diagram;
318-
root.innerHTML = renderStateMachineView(stateMachine, diagram, diagramOrientation, showDiagramConditions);
327+
renderStateMachineView(root, stateMachine, diagram, diagramOrientation, showDiagramConditions);
319328
if (stateMachine) renderMermaidDiagram(document, window);
320329
break;
321330
}

0 commit comments

Comments
 (0)