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:
+1
-1
@@ -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,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": {
|
||||
|
||||
@@ -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,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,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",
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user