Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -25,11 +25,13 @@
},
"devDependencies": {
"@biomejs/biome": "2.4.6",
"@preact/preset-vite": "^2.10.5",
"@testing-library/preact": "^3.2.4",
"@types/chrome": "^0.1.37",
"@types/vscode": "^1.118.0",
"@vscode/codicons": "^0.0.45",
"happy-dom": "^20.9.0",
"preact": "^10.29.1",
"typescript": "^5.9.3",
"vite": "^6.3.5",
"vitest": "^4.1.5"
Expand Down
10 changes: 6 additions & 4 deletions packages/vscode-ext/src/webview/csp.js
Original file line number Diff line number Diff line change
Expand Up @@ -21,9 +21,11 @@
* Builds the Content-Security-Policy string for an OctoClock webview.
*
* Directives:
* default-src 'none' — deny everything not listed below
* script-src 'nonce-<N>' — only scripts carrying the matching nonce run
* style-src cspSource nonce — stylesheets from localResourceRoots + <style nonce>
* default-src 'none' — deny everything not listed below
* script-src 'nonce-<N>' src — nonce covers the <script> entry tag;
* src covers Vite bundle files loaded from
* localResourceRoots (dist/)
* style-src cspSource nonce — stylesheets from localResourceRoots + <style nonce>
* img-src cspSource https: data:
* — local icons, remote GitHub avatars, data URIs
* font-src cspSource — codicons font from localResourceRoots
Expand All @@ -39,7 +41,7 @@ export function buildCsp(nonce, webview) {
const src = webview.cspSource;
return [
`default-src 'none'`,
`script-src 'nonce-${nonce}'`,
`script-src 'nonce-${nonce}' ${src}`,
`style-src ${src} 'nonce-${nonce}'`,
`img-src ${src} https: data:`,
`font-src ${src}`,
Expand Down
219 changes: 219 additions & 0 deletions packages/vscode-ext/src/webview/shared/components.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,219 @@
/**
* OctoClock — Shared Webview Component Styles
*
* Layout primitives and component classes shared across all sidebar and
* dashboard panels. Loaded via <link> in every panel's html.js shell, so
* none of these rules need to be repeated in per-panel <style nonce> blocks.
*
* What lives here:
* - Reset / body defaults
* - Codicon icon utility (.codicon — NOT @font-face, which needs a runtime
* URI and must stay in each panel's nonce'd <style> block)
* - Row layout primitives (.row, .row-icon, .row-lbl, .row-act, .row-always)
* - Icon button (.ib)
* - Filter bar (.filter-bar, .filter-input, .filter-tabs, .ftab)
* - Empty / loading state (.no-results)
* - Search highlight (mark)
*
* What does NOT live here (stays in per-panel <style nonce>):
* - @font-face for codicons — needs webview.asWebviewUri() at runtime
* - Panel-specific accent rows (e.g. .timer-row, .branch-row)
* - Panel-specific widget styles (.dot, .timer, .btn-stop, .bpill, etc.)
*/

/* ── Reset ─────────────────────────────────────── */

*,
*::before,
*::after {
box-sizing: border-box;
margin: 0;
padding: 0;
}

body {
font-family: var(--oc-font-ui);
font-size: 13px;
color: var(--oc-fg);
background: transparent;
line-height: 1;
}

/* ── Codicon font utility ────────────────────────
Note: @font-face is declared per-panel because it references a
webview URI that is only available at runtime. */

.codicon {
font: normal normal 16px/1 codicon;
display: inline-block;
}

/* ── Row layout ──────────────────────────────────
Base row used by every panel for tree-view style list items. */

.row {
display: flex;
align-items: center;
height: 22px;
padding: 0 6px 0 20px;
gap: 5px;
user-select: none;
}

.row.h28 {
height: 28px;
}

.row:hover {
background: var(--oc-hover-bg);
}

.row-icon {
flex-shrink: 0;
width: 16px;
text-align: center;
font-size: 14px;
}

.row-lbl {
flex: 1;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
font-size: 13px;
color: var(--oc-fg);
}

.row-lbl.dim {
color: var(--oc-muted);
}

/* Shown on hover only — used for action buttons in a row */
.row-act {
display: none;
align-items: center;
gap: 2px;
flex-shrink: 0;
}

.row:hover .row-act {
display: flex;
}

/* Always-visible right slot — used when buttons must be visible at all times */
.row-always {
display: flex;
align-items: center;
gap: 4px;
flex-shrink: 0;
}

/* Muted helper colours */
.dim-fg {
color: var(--oc-desc);
}

/* ── Icon button (.ib) ───────────────────────────
Small square button for inline actions (play, stop, open-link…). */

.ib {
display: inline-flex;
align-items: center;
justify-content: center;
width: 20px;
height: 20px;
background: none;
border: none;
border-radius: 3px;
color: var(--oc-fg);
cursor: pointer;
padding: 0;
}

.ib:hover {
background: var(--oc-hover-bg);
}

.ib:disabled {
opacity: 0.35;
cursor: default;
}

.ib .codicon {
font-size: 14px;
}

/* ── Filter bar ──────────────────────────────────
Search input + tab switcher used by My Issues and future list panels. */

.filter-bar {
padding: 4px 8px;
display: flex;
flex-direction: column;
gap: 4px;
}

.filter-input {
width: 100%;
font-size: 12px;
font-family: var(--oc-font-ui);
color: var(--oc-input-fg);
background: var(--oc-input-bg);
border: 1px solid var(--oc-input-border);
border-radius: 2px;
padding: 3px 6px;
outline: none;
}

.filter-input:focus {
border-color: var(--oc-focus);
}

.filter-input::placeholder {
color: var(--oc-input-placeholder);
}

.filter-tabs {
display: flex;
gap: 3px;
}

.ftab {
font-size: 11px;
font-family: var(--oc-font-ui);
background: none;
border: 1px solid transparent;
border-radius: 2px;
color: var(--oc-tab-inactive-fg);
cursor: pointer;
padding: 2px 7px;
}

.ftab:hover {
background: var(--oc-hover-bg);
}

.ftab.on {
color: var(--oc-fg);
border-color: var(--oc-btn-sec-border);
background: var(--oc-card-bg);
}

/* ── Empty / loading state ───────────────────────
Shown when a list has no items to display. */

.no-results {
padding: 8px 20px;
font-size: 12px;
color: var(--oc-muted);
}

/* ── Search highlight ────────────────────────────
Applied by IssueList's highlight() function via dangerouslySetInnerHTML. */

mark {
background: rgba(226, 192, 141, 0.25);
color: inherit;
border-radius: 2px;
padding: 0 1px;
}
29 changes: 29 additions & 0 deletions packages/vscode-ext/src/webview/shared/hooks/useVscodeMessage.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
// packages/vscode-ext/src/webview/shared/hooks/useVscodeMessage.js
//
// Subscribes to VS Code webview `message` events and calls the handler
// whenever a message of the given type arrives.
//
// Usage:
// useVscodeMessage('timerState', (msg) => setState(msg));
//
// The handler is stable across renders — no need to memoize at the call site.

import { useEffect } from 'preact/hooks';

/**
* @template T
* @param {string} type - The message type to listen for.
* @param {(message: T) => void} handler - Called for every matching message.
*/
export function useVscodeMessage(type, handler) {
useEffect(() => {
const listener = (/** @type {MessageEvent} */ event) => {
if (event.data?.type === type) {
handler(event.data);
}
};
window.addEventListener('message', listener);
return () => window.removeEventListener('message', listener);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [type]);
}
74 changes: 74 additions & 0 deletions packages/vscode-ext/src/webview/sidebar/active-timer/html.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
// packages/vscode-ext/src/webview/sidebar/active-timer/html.js
//
// Generates the HTML shell for the Active Timer webview panel.
//
// The shell is a minimal page that:
// 1. Loads tokens.css for --oc-* design tokens.
// 2. Defines the codicons @font-face and resets.
// 3. Mounts a <div id="app"> for the Preact bundle.
// 4. Loads the Vite bundle as a nonce-tagged ES module.

import * as vscode from 'vscode';
import { buildCsp } from '../../csp.js';
import { getNonce } from '../../nonce.js';

/**
* @param {import('vscode').Webview} webview
* @param {import('vscode').Uri} extensionUri
* @returns {string}
*/
export function getHtml(webview, extensionUri) {
const nonce = getNonce();
const csp = buildCsp(nonce, webview);

const tokensUri = webview.asWebviewUri(
vscode.Uri.joinPath(extensionUri, 'dist', 'webview', 'tokens.css'),
);
const componentsUri = webview.asWebviewUri(
vscode.Uri.joinPath(extensionUri, 'dist', 'webview', 'shared', 'components.css'),
);
const codiconsUri = webview.asWebviewUri(
vscode.Uri.joinPath(extensionUri, 'dist', 'fonts', 'codicon.ttf'),
);
const appUri = webview.asWebviewUri(
vscode.Uri.joinPath(extensionUri, 'dist', 'webview', 'sidebar', 'active-timer', 'app.js'),
);

return /* html */ `<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="Content-Security-Policy" content="${csp}">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link rel="stylesheet" href="${tokensUri}">
<link rel="stylesheet" href="${componentsUri}">
<style nonce="${nonce}">
/* codicons @font-face must stay here — needs a runtime webview URI */
@font-face { font-family: 'codicon'; src: url('${codiconsUri}') format('truetype'); }

/* ── Active Timer: panel-specific ── */
.icon-desc { color: var(--oc-desc); }
.icon-muted { color: var(--oc-muted); }
.row-lbl.dim { font-style: italic; }

.row.timer-row { background: rgba(0,122,204,.08); border-left: 2px solid rgba(0,122,204,.45); padding-left: 18px; }
.row.timer-row:hover { background: rgba(0,122,204,.13); }

.dot { width: 7px; height: 7px; border-radius: 50%; flex-shrink: 0; background: var(--oc-timer); }
@keyframes pulse { 0%, 100% { opacity: 1; } 50% { opacity: .25; } }
.dot.pulse { animation: pulse 1.8s ease-in-out infinite; }

.timer { font-family: var(--oc-font-mono); font-size: 12px; font-weight: 600; color: var(--oc-timer); letter-spacing: .03em; flex-shrink: 0; }

.btn-stop { display: inline-flex; align-items: center; gap: 3px; font-size: 11px; font-family: var(--oc-font-ui); background: rgba(244,135,113,.1); color: var(--oc-danger); border: 1px solid rgba(244,135,113,.2); border-radius: 2px; padding: 0 6px; height: 20px; cursor: pointer; flex-shrink: 0; }
.btn-stop:hover { background: rgba(244,135,113,.2); }
.btn-stop .codicon { font-size: 12px; }
</style>
<title>Active Timer</title>
</head>
<body>
<div id="app"></div>
<script type="module" nonce="${nonce}" src="${appUri}"></script>
</body>
</html>`;
}
Loading
Loading