fix(browser-extension): 1.9.4 — monochrome brand pass + on-page chat config

@hanzo/brand is strictly monochrome (primaryColor #FFFFFF, no chromatic
accents). Stripped every blue/violet/emerald/amber/red literal across
the in-page overlay, sidebar, and popup; replaced with a luminance scale
keyed off white. Status states (success/warn/error) now distinguished
by opacity, not hue.

On-page overlay (content-script):
- Replaces the brittle `looksLikePromiseResult` blue button with a white
  primary action (#fff bg / #0a0a0b text) per brand guidelines.
- Composer is now sticky-bottom: messages flex/scroll, composer
  flex-shrink: 0, panel min-height: 0 lets the layout collapse cleanly.
- Adds a pin-mode toggle in the header. `mode=push` reflows page content
  (sets html.hanzo-overlay-push-{side} → margin-{side}: var(width));
  `mode=overlay` floats over the page (default).
- Adds setMode / setWidth / setEnabled message handlers and persists all
  three in chrome.storage.sync alongside the existing overlaySide.
- Picks up overlayWidth (compact 320 / default 420 / wide 560) via
  --hanzo-overlay-width CSS var, used by both the panel itself and the
  push-mode html margin.

Popup:
- "Open Chat" honours overlayEnabled — falls back to standalone tab
  when disabled (or when active tab is privileged).
- Adds On-page chat panel checkbox + Width picker (Compact/Default/Wide)
  alongside the existing Pin Left/Right.

CSS / brand cleanup:
- sidebar.css usage bars (.bar-blue/.bar-violet/...) → white opacity scale.
- model-hub badges (.badge-cloud/.badge-local/.badge-hub) → mono-luminance.
- inspector-result element-{tag,id,class} → mono-luminance instead of
  blue/violet/green syntax tints.
- --success / --warning / --error in both stylesheets → white shades.
This commit is contained in:
Hanzo AI
2026-05-08 10:50:20 -07:00
parent cfd53c8142
commit 1781862fb1
9 changed files with 365 additions and 101 deletions
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "hanzo-ai-monorepo",
"version": "1.9.3",
"version": "1.9.4",
"private": true,
"description": "Hanzo AI Development Platform Monorepo",
"license": "MIT",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@hanzo/browser-extension",
"version": "1.9.3",
"version": "1.9.4",
"description": "Hanzo AI Browser Extension",
"main": "dist/background.js",
"scripts": {
+228 -50
View File
@@ -115,8 +115,10 @@ class HanzoContentScript {
sendResponse({ success: true });
return true;
case 'page.overlay.toggle':
if (request.side === 'left' || request.side === 'right') {
pageOverlay.setSide(request.side);
if (request.side === 'left' || request.side === 'right') pageOverlay.setSide(request.side);
if (request.mode === 'push' || request.mode === 'overlay') pageOverlay.setMode(request.mode);
if (request.width === 'compact' || request.width === 'default' || request.width === 'wide') {
pageOverlay.setWidth(request.width);
}
sendResponse({
success: true,
@@ -124,8 +126,10 @@ class HanzoContentScript {
});
return true;
case 'page.overlay.show':
if (request.side === 'left' || request.side === 'right') {
pageOverlay.setSide(request.side);
if (request.side === 'left' || request.side === 'right') pageOverlay.setSide(request.side);
if (request.mode === 'push' || request.mode === 'overlay') pageOverlay.setMode(request.mode);
if (request.width === 'compact' || request.width === 'default' || request.width === 'wide') {
pageOverlay.setWidth(request.width);
}
pageOverlay.show();
sendResponse({ success: true, open: true });
@@ -135,7 +139,12 @@ class HanzoContentScript {
sendResponse({ success: true, open: false });
return true;
case 'page.overlay.status':
sendResponse({ success: true, open: pageOverlay.isOpen(), side: pageOverlay.getSide() });
sendResponse({
success: true,
open: pageOverlay.isOpen(),
side: pageOverlay.getSide(),
mode: pageOverlay.getMode(),
});
return true;
case 'page.overlay.setSide':
if (request.side === 'left' || request.side === 'right') {
@@ -145,6 +154,30 @@ class HanzoContentScript {
sendResponse({ success: false, error: 'side must be "left" or "right"' });
}
return true;
case 'page.overlay.setMode':
if (request.mode === 'push' || request.mode === 'overlay') {
pageOverlay.setMode(request.mode);
sendResponse({ success: true, mode: request.mode });
} else {
sendResponse({ success: false, error: 'mode must be "push" or "overlay"' });
}
return true;
case 'page.overlay.setWidth':
if (request.width === 'compact' || request.width === 'default' || request.width === 'wide') {
pageOverlay.setWidth(request.width);
sendResponse({ success: true, width: request.width });
} else {
sendResponse({ success: false, error: 'width must be compact|default|wide' });
}
return true;
case 'page.overlay.setEnabled':
if (typeof request.enabled === 'boolean') {
pageOverlay.setEnabled(request.enabled);
sendResponse({ success: true, enabled: request.enabled });
} else {
sendResponse({ success: false, error: 'enabled must be boolean' });
}
return true;
// --- Popup Tool Actions ---
case 'tool.picker.start':
@@ -768,11 +801,22 @@ interface OverlayElementContext {
html: string;
}
type OverlayMode = 'overlay' | 'push';
type OverlayWidth = 'compact' | 'default' | 'wide';
const OVERLAY_WIDTH_PX: Record<OverlayWidth, number> = {
compact: 320,
default: 420,
wide: 560,
};
class HanzoPageOverlay {
private mounted = false;
private enabled = false;
private open = false;
private side: 'left' | 'right' = 'right';
private mode: OverlayMode = 'overlay';
private width: OverlayWidth = 'default';
private sending = false;
private watchMode = false;
private editMode = false;
@@ -799,23 +843,55 @@ class HanzoPageOverlay {
this.injectStyles();
this.mount();
this.bindGlobalEvents();
this.restoreSide();
this.restorePrefs();
}
private restoreSide(): void {
private restorePrefs(): void {
try {
const ss = browser?.storage?.sync;
const ss = (typeof browser !== 'undefined' ? browser : (chrome as any))?.storage?.sync;
if (!ss?.get) return;
ss.get(['overlaySide']).then((v: any) => {
const stored = v?.overlaySide;
if (stored === 'left' || stored === 'right') {
this.side = stored;
this.root?.setAttribute('data-side', stored);
const promised = ss.get(['overlaySide', 'overlayMode', 'overlayWidth']);
const apply = (v: any) => {
if (v?.overlaySide === 'left' || v?.overlaySide === 'right') {
this.side = v.overlaySide;
this.root?.setAttribute('data-side', this.side);
}
}).catch(() => {});
if (v?.overlayMode === 'push' || v?.overlayMode === 'overlay') {
this.mode = v.overlayMode;
this.root?.setAttribute('data-mode', this.mode);
}
if (v?.overlayWidth === 'compact' || v?.overlayWidth === 'default' || v?.overlayWidth === 'wide') {
this.width = v.overlayWidth;
this.applyWidth();
}
this.applyPushIfOpen();
};
if (promised && typeof promised.then === 'function') {
promised.then(apply).catch(() => {});
} else {
ss.get(['overlaySide', 'overlayMode', 'overlayWidth'], apply);
}
} catch { /* storage may be unavailable */ }
}
private applyWidth(): void {
const px = OVERLAY_WIDTH_PX[this.width] ?? OVERLAY_WIDTH_PX.default;
if (this.root) {
this.root.style.setProperty('--hanzo-overlay-width', `${px}px`);
this.root.style.width = `min(${px}px, 100vw)`;
}
document.documentElement.style.setProperty('--hanzo-overlay-width', `${px}px`);
}
private applyPushIfOpen(): void {
const html = document.documentElement;
// Always strip first so we never stack margins.
html.classList.remove('hanzo-overlay-push', 'hanzo-overlay-push-left', 'hanzo-overlay-push-right');
if (this.open && this.enabled && this.mode === 'push') {
html.classList.add('hanzo-overlay-push', `hanzo-overlay-push-${this.side}`);
}
}
toggle(): boolean {
if (!this.enabled || !this.open) {
this.show();
@@ -833,13 +909,42 @@ class HanzoPageOverlay {
return this.side;
}
getMode(): OverlayMode {
return this.mode;
}
setSide(side: 'left' | 'right'): void {
this.side = side;
this.root?.setAttribute('data-side', side);
this.applyPushIfOpen();
this.persist({ overlaySide: side });
}
setMode(mode: OverlayMode): void {
this.mode = mode;
this.root?.setAttribute('data-mode', mode);
this.applyPushIfOpen();
this.persist({ overlayMode: mode });
}
setWidth(width: OverlayWidth): void {
this.width = width;
this.applyWidth();
this.persist({ overlayWidth: width });
}
setEnabled(enabled: boolean): void {
this.enabled = enabled;
this.root?.setAttribute('data-enabled', enabled ? 'true' : 'false');
if (!enabled) this.hide();
this.persist({ overlayEnabled: enabled });
}
private persist(values: Record<string, unknown>): void {
try {
// Persist asynchronously; callers don't need to await.
browser?.storage?.sync?.set({ overlaySide: side });
} catch { /* storage may be unavailable in some contexts */ }
const ss = (typeof browser !== 'undefined' ? browser : (chrome as any))?.storage?.sync;
ss?.set?.(values);
} catch { /* storage may be unavailable */ }
}
show(): void {
@@ -848,6 +953,7 @@ class HanzoPageOverlay {
this.open = true;
this.root?.setAttribute('data-enabled', 'true');
this.root?.setAttribute('data-open', 'true');
this.applyPushIfOpen();
this.loadModels();
this.focusInput();
}
@@ -855,6 +961,7 @@ class HanzoPageOverlay {
hide(): void {
this.open = false;
this.root?.setAttribute('data-open', 'false');
this.applyPushIfOpen();
this.updateStatus('');
this.clearHighlight();
this.setWatchMode(false);
@@ -872,19 +979,19 @@ class HanzoPageOverlay {
const style = document.createElement('style');
style.id = 'hanzo-page-overlay-styles';
style.textContent = `
/* Edge-pinned sidebar panel — no FAB, opened from extension popup. */
/* Edge-pinned sidebar panel — no FAB, opened from extension popup.
Hanzo brand: monochrome (white primary, neutral surfaces). No blue. */
#hanzo-page-overlay-root {
position: fixed;
top: 0;
bottom: 0;
right: 0;
width: min(420px, 100vw);
width: min(var(--hanzo-overlay-width, 420px), 100vw);
z-index: 2147483645;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
pointer-events: none;
}
/* User-configurable side. Set data-side="left" on the root to flip. */
#hanzo-page-overlay-root[data-side="left"] {
right: auto;
left: 0;
@@ -894,12 +1001,24 @@ class HanzoPageOverlay {
box-sizing: border-box;
}
/* Push mode: when the user pins the panel as a dock, html shifts so
page content reflows beside it instead of being covered. Applied to
<html> from setMode('push') + open. */
html.hanzo-overlay-push.hanzo-overlay-push-right {
margin-right: var(--hanzo-overlay-width, 420px) !important;
transition: margin-right 0.18s ease;
}
html.hanzo-overlay-push.hanzo-overlay-push-left {
margin-left: var(--hanzo-overlay-width, 420px) !important;
transition: margin-left 0.18s ease;
}
.hanzo-page-panel {
width: 100%;
height: 100%;
border-left: 1px solid rgba(255, 255, 255, 0.16);
background: linear-gradient(180deg, rgba(11, 13, 18, 0.97), rgba(7, 8, 12, 0.97));
color: #eef2ff;
background: linear-gradient(180deg, rgba(10, 10, 11, 0.97), rgba(7, 7, 8, 0.97));
color: #f5f5f5;
overflow: hidden;
box-shadow: -22px 0 48px rgba(0, 0, 0, 0.45);
transform: translateX(100%);
@@ -907,6 +1026,7 @@ class HanzoPageOverlay {
pointer-events: none;
display: flex;
flex-direction: column;
min-height: 0;
transition: transform 0.18s ease, opacity 0.18s ease;
}
@@ -928,14 +1048,16 @@ class HanzoPageOverlay {
align-items: center;
justify-content: space-between;
padding: 12px 14px;
border-bottom: 1px solid rgba(255, 255, 255, 0.12);
border-bottom: 1px solid rgba(255, 255, 255, 0.10);
background: linear-gradient(180deg, rgba(255, 255, 255, 0.03), transparent);
flex-shrink: 0;
}
.hanzo-page-brand {
font-weight: 700;
letter-spacing: 0.2px;
font-size: 13px;
color: #f5f5f5;
}
.hanzo-page-header-actions {
@@ -948,16 +1070,22 @@ class HanzoPageOverlay {
border: 1px solid rgba(255, 255, 255, 0.2);
border-radius: 9999px;
padding: 4px 10px;
background: rgba(255, 255, 255, 0.02);
color: #d6deff;
background: transparent;
color: rgba(245, 245, 245, 0.85);
cursor: pointer;
font-size: 11px;
transition: background 0.15s, color 0.15s, border-color 0.15s;
}
.hanzo-chip-btn:hover {
background: rgba(255, 255, 255, 0.06);
color: #fff;
}
.hanzo-chip-btn[data-active="true"] {
border-color: rgba(56, 189, 248, 0.62);
background: rgba(14, 116, 144, 0.3);
color: #e0f2fe;
border-color: rgba(255, 255, 255, 0.7);
background: rgba(255, 255, 255, 0.10);
color: #fff;
}
.hanzo-icon-btn {
@@ -966,9 +1094,28 @@ class HanzoPageOverlay {
border-radius: 8px;
border: 1px solid rgba(255, 255, 255, 0.18);
background: transparent;
color: #e5e7eb;
color: #f5f5f5;
cursor: pointer;
font-size: 16px;
font-size: 14px;
display: inline-flex;
align-items: center;
justify-content: center;
padding: 0;
transition: background 0.15s, border-color 0.15s;
}
.hanzo-icon-btn:hover {
background: rgba(255, 255, 255, 0.06);
}
.hanzo-icon-btn[data-active="true"] {
background: rgba(255, 255, 255, 0.12);
border-color: rgba(255, 255, 255, 0.5);
}
.hanzo-icon-btn svg {
width: 14px;
height: 14px;
}
.hanzo-page-meta {
@@ -977,11 +1124,12 @@ class HanzoPageOverlay {
grid-template-columns: 1fr auto;
gap: 8px;
align-items: center;
flex-shrink: 0;
}
.hanzo-page-context {
font-size: 11px;
color: #a5b4fc;
color: rgba(245, 245, 245, 0.65);
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
@@ -989,11 +1137,12 @@ class HanzoPageOverlay {
.hanzo-page-status {
font-size: 11px;
color: #93c5fd;
color: rgba(245, 245, 245, 0.55);
}
.hanzo-model-row {
padding: 0 14px 8px 14px;
flex-shrink: 0;
}
.hanzo-model-row select {
@@ -1001,15 +1150,16 @@ class HanzoPageOverlay {
border-radius: 8px;
border: 1px solid rgba(255, 255, 255, 0.16);
background: rgba(255, 255, 255, 0.03);
color: #f1f5f9;
color: #f5f5f5;
padding: 7px 9px;
font-size: 12px;
}
/* The chat scroller: takes ALL remaining vertical space so the
composer below stays sticky against the bottom of the panel. */
.hanzo-page-messages {
flex: 1;
min-height: 200px;
max-height: 370px;
flex: 1 1 auto;
min-height: 0;
overflow-y: auto;
padding: 8px 14px 12px 14px;
display: flex;
@@ -1028,23 +1178,27 @@ class HanzoPageOverlay {
.hanzo-page-msg.user {
align-self: flex-end;
max-width: 88%;
background: rgba(59, 130, 246, 0.2);
border: 1px solid rgba(59, 130, 246, 0.4);
background: rgba(255, 255, 255, 0.10);
border: 1px solid rgba(255, 255, 255, 0.18);
}
.hanzo-page-msg.assistant {
align-self: flex-start;
max-width: 92%;
background: rgba(255, 255, 255, 0.05);
border: 1px solid rgba(255, 255, 255, 0.12);
background: rgba(255, 255, 255, 0.04);
border: 1px solid rgba(255, 255, 255, 0.10);
}
/* Composer pinned to bottom of the panel. flex-shrink: 0 prevents
it from collapsing when the messages scroller grows. */
.hanzo-page-composer {
border-top: 1px solid rgba(255, 255, 255, 0.1);
border-top: 1px solid rgba(255, 255, 255, 0.10);
padding: 10px 12px;
display: flex;
gap: 8px;
align-items: flex-end;
background: rgba(0, 0, 0, 0.35);
flex-shrink: 0;
}
.hanzo-page-composer textarea {
@@ -1055,21 +1209,31 @@ class HanzoPageOverlay {
border-radius: 10px;
border: 1px solid rgba(255, 255, 255, 0.17);
background: rgba(255, 255, 255, 0.03);
color: #f8fafc;
color: #f5f5f5;
padding: 9px 11px;
font-size: 12px;
}
/* Primary action: WHITE per @hanzo/brand monochrome guidelines. */
.hanzo-page-composer button {
min-width: 68px;
height: 38px;
border-radius: 10px;
border: 1px solid rgba(37, 99, 235, 0.65);
background: linear-gradient(180deg, rgba(37, 99, 235, 0.95), rgba(30, 64, 175, 0.95));
color: #eff6ff;
border: 1px solid #ffffff;
background: #ffffff;
color: #0a0a0b;
font-weight: 600;
font-size: 12px;
cursor: pointer;
transition: background 0.12s, transform 0.08s;
}
.hanzo-page-composer button:hover:not(:disabled) {
background: #f5f5f5;
}
.hanzo-page-composer button:active:not(:disabled) {
transform: translateY(1px);
}
.hanzo-page-composer button:disabled {
@@ -1081,13 +1245,13 @@ class HanzoPageOverlay {
position: fixed;
z-index: 2147483644;
pointer-events: none;
border: 2px solid rgba(56, 189, 248, 0.95);
border: 2px solid rgba(255, 255, 255, 0.95);
border-radius: 6px;
box-shadow: 0 0 0 2px rgba(14, 116, 144, 0.2);
box-shadow: 0 0 0 2px rgba(255, 255, 255, 0.2);
}
[data-hanzo-inline-editing="true"] {
outline: 2px solid rgba(251, 191, 36, 0.92) !important;
outline: 2px solid rgba(255, 255, 255, 0.92) !important;
outline-offset: 2px !important;
}
`;
@@ -1102,14 +1266,21 @@ class HanzoPageOverlay {
this.root.setAttribute('data-open', 'false');
this.root.setAttribute('data-enabled', 'false');
this.root.setAttribute('data-side', this.side);
this.root.setAttribute('data-mode', this.mode);
this.applyWidth();
this.root.innerHTML = `
<div class="hanzo-page-panel" role="dialog" aria-label="Hanzo Page Assistant">
<div class="hanzo-page-header">
<div class="hanzo-page-brand">Hanzo Overlay</div>
<div class="hanzo-page-brand">Hanzo</div>
<div class="hanzo-page-header-actions">
<button class="hanzo-chip-btn" data-role="watch" data-active="false" title="Watch elements on hover">Watch</button>
<button class="hanzo-chip-btn" data-role="edit" data-active="false" title="Click any element to edit text">Edit</button>
<button class="hanzo-icon-btn" data-role="close" title="Minimize">×</button>
<button class="hanzo-icon-btn" data-role="pin-mode" title="Toggle: pin (push content) vs overlay (float over page)">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true">
<path d="M9 4v16M3 4h12a4 4 0 0 1 4 4v8a4 4 0 0 1-4 4H3z"/>
</svg>
</button>
<button class="hanzo-icon-btn" data-role="close" title="Hide overlay">×</button>
</div>
</div>
<div class="hanzo-page-meta">
@@ -1147,6 +1318,13 @@ class HanzoPageOverlay {
this.statusEl = this.root.querySelector('.hanzo-page-status');
this.root.querySelector('[data-role="close"]')?.addEventListener('click', () => this.hide());
const pinModeBtn = this.root.querySelector('[data-role="pin-mode"]');
pinModeBtn?.setAttribute('data-active', this.mode === 'push' ? 'true' : 'false');
pinModeBtn?.addEventListener('click', () => {
const next: OverlayMode = this.mode === 'push' ? 'overlay' : 'push';
this.setMode(next);
pinModeBtn.setAttribute('data-active', next === 'push' ? 'true' : 'false');
});
this.watchBtn?.addEventListener('click', () => this.setWatchMode(!this.watchMode));
this.editBtn?.addEventListener('click', () => this.setEditMode(!this.editMode));
this.sendBtn?.addEventListener('click', () => this.askPageQuestion());
+1 -1
View File
@@ -1,7 +1,7 @@
{
"manifest_version": 3,
"name": "Hanzo AI",
"version": "1.9.3",
"version": "1.9.4",
"description": "AI-powered dev assistant — click-to-code navigation, source maps, WebGPU AI, and MCP integration for Claude Code.",
"permissions": [
"activeTab",
+1 -1
View File
@@ -1,7 +1,7 @@
{
"manifest_version": 3,
"name": "Hanzo AI",
"version": "1.9.3",
"version": "1.9.4",
"description": "AI-powered dev assistant — click-to-code navigation, source maps, WebGPU AI, and MCP integration for Claude Code.",
"permissions": [
"activeTab",
+4 -3
View File
@@ -13,9 +13,10 @@
--border-strong: rgba(255,255,255,0.16);
--glass: rgba(255,255,255,0.04);
--glass-strong: rgba(255,255,255,0.07);
--success: #22c55e;
--warning: #eab308;
--error: #ef4444;
/* Status: monochrome by hue per @hanzo/brand (white luminance scale). */
--success: #fafafa;
--warning: #d4d4d4;
--error: rgba(255,255,255,0.55);
--link: #a3a3a3;
--link-hover: #fafafa;
--radius: 10px;
+29 -8
View File
@@ -57,20 +57,41 @@
<div class="divider"></div>
</div>
<!-- Actions: single unified Open Chat button + side picker.
Both old buttons (Open Chat Panel, Toggle On-Page Overlay)
did the same thing in different words; unified here. -->
<!-- Actions: single Open Chat button + on-page surface settings. -->
<button id="open-panel" class="btn btn-secondary" style="margin-bottom: 8px;">
<svg class="icon" viewBox="0 0 24 24" fill="none" stroke="currentColor">
<path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z"/>
</svg>
Open Chat
</button>
<div class="side-picker" style="display:flex;gap:6px;margin-bottom:12px;font-size:11px;align-items:center;">
<span style="color:var(--text-muted,#888);">Pin:</span>
<button id="pin-left" class="btn btn-ghost" style="flex:1;padding:4px 8px;font-size:11px;">Left</button>
<button id="pin-right" class="btn btn-ghost" style="flex:1;padding:4px 8px;font-size:11px;" data-active="true">Right</button>
<span style="color:var(--text-muted,#888);font-size:10px;">FF: View → Sidebar → Hanzo AI for native</span>
<!-- On-page chat config -->
<div class="onpage-config" style="display:flex;flex-direction:column;gap:6px;margin-bottom:12px;font-size:11px;">
<div class="feature-toggle" style="display:flex;justify-content:space-between;align-items:center;">
<label style="display:flex;align-items:center;gap:6px;cursor:pointer;">
<input type="checkbox" id="overlay-enabled" checked>
<span>On-page chat panel</span>
</label>
<span style="color:var(--text-muted,#888);font-size:10px;">overlay vs new tab</span>
</div>
<div id="overlay-side-row" class="side-picker" style="display:flex;gap:6px;align-items:center;">
<span style="color:var(--text-muted,#888);">Pin:</span>
<button id="pin-left" class="btn btn-ghost" style="flex:1;padding:4px 8px;font-size:11px;">Left</button>
<button id="pin-right" class="btn btn-ghost" style="flex:1;padding:4px 8px;font-size:11px;" data-active="true">Right</button>
</div>
<div id="overlay-width-row" style="display:flex;gap:6px;align-items:center;">
<span style="color:var(--text-muted,#888);">Width:</span>
<button class="btn btn-ghost width-btn" data-width="compact" style="flex:1;padding:4px 8px;font-size:11px;">Compact</button>
<button class="btn btn-ghost width-btn" data-width="default" style="flex:1;padding:4px 8px;font-size:11px;" data-active="true">Default</button>
<button class="btn btn-ghost width-btn" data-width="wide" style="flex:1;padding:4px 8px;font-size:11px;">Wide</button>
</div>
<span style="color:var(--text-muted,#888);font-size:10px;line-height:1.4;">
FF native sidebar: View → Sidebar → Hanzo AI.<br>
Disable to fall back to a standalone tab — useful on sites with strict CSP.
</span>
</div>
<div class="divider"></div>
+77 -21
View File
@@ -37,6 +37,25 @@ document.addEventListener('DOMContentLoaded', () => {
const openPageOverlay = document.getElementById('open-page-overlay'); // legacy id, may not exist
const pinLeftBtn = document.getElementById('pin-left');
const pinRightBtn = document.getElementById('pin-right');
const overlayEnabledCheckbox = document.getElementById('overlay-enabled') as HTMLInputElement | null;
const widthBtns = Array.from(document.querySelectorAll('.width-btn')) as HTMLButtonElement[];
const overlaySideRow = document.getElementById('overlay-side-row');
const overlayWidthRow = document.getElementById('overlay-width-row');
type OverlayWidth = 'compact' | 'default' | 'wide';
function syncSettingsUi(enabled: boolean, side: 'left' | 'right', width: OverlayWidth) {
if (overlayEnabledCheckbox) overlayEnabledCheckbox.checked = enabled;
pinLeftBtn?.setAttribute('data-active', side === 'left' ? 'true' : 'false');
pinRightBtn?.setAttribute('data-active', side === 'right' ? 'true' : 'false');
widthBtns.forEach((b) => b.setAttribute('data-active', b.getAttribute('data-width') === width ? 'true' : 'false'));
// Grey out side/width when on-page overlay is disabled.
[overlaySideRow, overlayWidthRow].forEach((row) => {
if (!row) return;
(row as HTMLElement).style.opacity = enabled ? '1' : '0.5';
(row as HTMLElement).style.pointerEvents = enabled ? '' : 'none';
});
}
// Status dots
const zapStatus = document.getElementById('zap-status');
@@ -109,47 +128,84 @@ document.addEventListener('DOMContentLoaded', () => {
// --- Open chat ---
// Default surface: in-page edge-pinned overlay (content-script). Honours
// the user's `overlaySide` (left|right) preference. Firefox users who
// prefer the OS-native sidebar can also reach the same chat surface via
// View → Sidebar → Hanzo AI (registered via sidebar_action in
// manifest-firefox.json).
// the user's `overlayEnabled`, `overlaySide`, and `overlayWidth` prefs.
// When `overlayEnabled` is false (or the active tab is privileged), opens
// the chat surface in a standalone tab. Firefox users can also reach it
// via View → Sidebar → Hanzo AI (registered via sidebar_action).
function openSidebarTab() {
chrome.tabs.create({ url: chrome.runtime.getURL('sidebar.html') });
window.close();
}
openPanel?.addEventListener('click', () => {
chrome.storage?.sync?.get?.(['overlaySide'], (vals) => {
chrome.storage?.sync?.get?.(['overlayEnabled', 'overlaySide', 'overlayWidth'], (vals) => {
const enabled = vals?.overlayEnabled !== false;
const side = vals?.overlaySide === 'left' ? 'left' : 'right';
const width: OverlayWidth = vals?.overlayWidth === 'compact'
? 'compact' : vals?.overlayWidth === 'wide' ? 'wide' : 'default';
if (!enabled) { openSidebarTab(); return; }
chrome.tabs.query({ active: true, currentWindow: true }, (tabs) => {
const activeTab = tabs[0];
const target = isInjectableTab(activeTab) ? activeTab : null;
if (!target?.id) {
// Fallback: open the chat surface in a regular tab so the popup
// button still does something on privileged (chrome:// / about:)
// pages where content scripts can't run.
chrome.tabs.create({ url: chrome.runtime.getURL('sidebar.html') });
window.close();
return;
}
chrome.runtime.sendMessage({ action: 'page.overlay.show', tabId: target.id, side }, () => {
window.close();
});
if (!target?.id) { openSidebarTab(); return; }
chrome.runtime.sendMessage({
action: 'page.overlay.show',
tabId: target.id,
side,
width,
}, () => window.close());
});
});
});
function setActiveSide(side: 'left' | 'right') {
chrome.storage?.sync?.set?.({ overlaySide: side });
pinLeftBtn?.setAttribute('data-active', side === 'left' ? 'true' : 'false');
pinRightBtn?.setAttribute('data-active', side === 'right' ? 'true' : 'false');
chrome.storage?.sync?.set?.({ overlaySide: side });
chrome.tabs.query({ active: true, currentWindow: true }, (tabs) => {
const id = tabs[0]?.id;
if (id) chrome.runtime.sendMessage({ action: 'page.overlay.setSide', tabId: id, side });
});
}
function setActiveWidth(width: OverlayWidth) {
chrome.storage?.sync?.set?.({ overlayWidth: width });
widthBtns.forEach((b) => b.setAttribute('data-active', b.getAttribute('data-width') === width ? 'true' : 'false'));
chrome.tabs.query({ active: true, currentWindow: true }, (tabs) => {
const id = tabs[0]?.id;
if (id) chrome.runtime.sendMessage({ action: 'page.overlay.setWidth', tabId: id, width });
});
}
function setOverlayEnabled(enabled: boolean) {
chrome.storage?.sync?.set?.({ overlayEnabled: enabled });
chrome.tabs.query({ active: true, currentWindow: true }, (tabs) => {
const id = tabs[0]?.id;
if (id) chrome.runtime.sendMessage({ action: 'page.overlay.setEnabled', tabId: id, enabled });
});
chrome.storage?.sync?.get?.(['overlaySide', 'overlayWidth'], (vals) => {
const side = vals?.overlaySide === 'left' ? 'left' : 'right';
const width: OverlayWidth = vals?.overlayWidth === 'compact'
? 'compact' : vals?.overlayWidth === 'wide' ? 'wide' : 'default';
syncSettingsUi(enabled, side, width);
});
}
pinLeftBtn?.addEventListener('click', () => setActiveSide('left'));
pinRightBtn?.addEventListener('click', () => setActiveSide('right'));
// Restore active-pin highlighting from storage.
chrome.storage?.sync?.get?.(['overlaySide'], (vals) => {
widthBtns.forEach((b) => b.addEventListener('click', () => {
const w = b.getAttribute('data-width') as OverlayWidth | null;
if (w === 'compact' || w === 'default' || w === 'wide') setActiveWidth(w);
}));
overlayEnabledCheckbox?.addEventListener('change', (e) => {
setOverlayEnabled((e.target as HTMLInputElement).checked);
});
// Initial UI sync from storage.
chrome.storage?.sync?.get?.(['overlayEnabled', 'overlaySide', 'overlayWidth'], (vals) => {
const enabled = vals?.overlayEnabled !== false;
const side = vals?.overlaySide === 'left' ? 'left' : 'right';
pinLeftBtn?.setAttribute('data-active', side === 'left' ? 'true' : 'false');
pinRightBtn?.setAttribute('data-active', side === 'right' ? 'true' : 'false');
const width: OverlayWidth = vals?.overlayWidth === 'compact'
? 'compact' : vals?.overlayWidth === 'wide' ? 'wide' : 'default';
syncSettingsUi(enabled, side, width);
});
function isInjectableTab(tab: chrome.tabs.Tab | undefined): boolean {
+23 -15
View File
@@ -1,4 +1,6 @@
/* Hanzo AI Browser Extension Sidebar — aligned with @hanzo/ui dark theme */
/* Hanzo AI Browser Extension Sidebar — Hanzo brand: strictly monochrome.
See ~/work/hanzo/brand/brand.json — primaryColor #FFFFFF, no chromatic
accents. Status states are conveyed by intensity, not hue. */
:root {
--bg-primary: #0a0a0a;
@@ -15,9 +17,11 @@
--accent-dim: rgba(255,255,255,0.06);
--accent-glow: rgba(255,255,255,0.03);
--success: #22c55e;
--warning: #eab308;
--error: #ef4444;
/* Status: monochrome by hue, distinguished by opacity / weight.
Brand is strict monochrome, so success/warn/error use luminance. */
--success: #fafafa;
--warning: #d4d4d4;
--error: rgba(255,255,255,0.55);
--border: rgba(255,255,255,0.10);
--border-strong: rgba(255,255,255,0.16);
@@ -1046,16 +1050,18 @@ html {
word-break: break-all;
}
/* Element-syntax tinting — kept monochrome via opacity, not hue. */
.inspector-result .element-tag {
color: #7cc4f8;
color: #fafafa;
font-weight: 600;
}
.inspector-result .element-id {
color: #c792ea;
color: rgba(255,255,255,0.78);
}
.inspector-result .element-class {
color: #c3e88d;
color: rgba(255,255,255,0.62);
}
.inspector-result .element-size {
@@ -1681,11 +1687,13 @@ select option {
min-width: 0;
}
.bar-blue { background: #3B82F6; }
.bar-violet { background: #8B5CF6; }
.bar-emerald { background: #10B981; }
.bar-amber { background: #F59E0B; }
.bar-red { background: #EF4444; }
/* Usage bars: monochrome scale (brightest → dimmest) per @hanzo/brand.
Class names kept for compatibility with existing markup. */
.bar-blue { background: rgba(255,255,255,0.95); }
.bar-violet { background: rgba(255,255,255,0.75); }
.bar-emerald { background: rgba(255,255,255,0.55); }
.bar-amber { background: rgba(255,255,255,0.40); }
.bar-red { background: rgba(255,255,255,0.25); }
.usage-bar-group {
margin-top: 8px;
@@ -1761,9 +1769,9 @@ select option {
letter-spacing: 0.05em;
white-space: nowrap;
}
.badge-cloud { background: rgba(59,130,246,0.15); color: #60A5FA; }
.badge-local { background: rgba(16,185,129,0.15); color: #34D399; }
.badge-hub { background: rgba(139,92,246,0.15); color: #A78BFA; }
.badge-cloud { background: rgba(255,255,255,0.10); color: rgba(255,255,255,0.92); }
.badge-local { background: rgba(255,255,255,0.06); color: rgba(255,255,255,0.78); }
.badge-hub { background: rgba(255,255,255,0.04); color: rgba(255,255,255,0.65); }
.model-hub-actions {
display: flex;