diff --git a/scripts/ais-relay.cjs b/scripts/ais-relay.cjs index 30636b226..150908569 100644 --- a/scripts/ais-relay.cjs +++ b/scripts/ais-relay.cjs @@ -8325,6 +8325,9 @@ Rows: padding 5–8px vertical, 8px horizontal. Section gaps: 8–12px. NEVER us ### Border radius — flat, not rounded Max 4px. NEVER use border-radius > 4px (no 8px, 12px, 16px rounded cards). +### Titles — NEVER duplicate the panel header +The outer panel frame already displays the widget title. NEVER add an h1/h2/h3 or any top-level title element to the widget body. Start content immediately (tabs, rows, stats grid, or a section label). + ### Labels — uppercase monospace Section headers and column labels: font-size: 10px; text-transform: uppercase; letter-spacing: 0.5px; color: var(--text-muted) @@ -8890,7 +8893,8 @@ CSS variables are pre-defined in the iframe: --bg, --surface, --text, --text-sec - ALWAYS use CSS variables for colors — never hardcode hex values like #3b82f6, #1a1a2e, etc. - NEVER override font-family (already set — do not change it) - NEVER use border-radius > 4px -- NEVER use large bold titles (h1/h2/h3) — use .panel-title or section labels only +- NEVER use large bold titles (h1/h2/h3) — use section labels only +- NEVER add a top-level .panel-header or .panel-title — the outer panel frame already shows the widget title; a second header creates an ugly duplicate - Keep row padding tight: 5–8px vertical, 8px horizontal - Numbers/prices: font-variant-numeric: tabular-nums - Positive values: color: var(--green) | Negative values: color: var(--red) @@ -8900,8 +8904,7 @@ CSS variables are pre-defined in the iframe: --bg, --surface, --text, --text-sec ## Pre-defined CSS classes — use these, do NOT reinvent them -Panel structure (copy exactly): -
SECTION TITLELIVE
+Tabs (when content has multiple views — start directly with this, NO .panel-header above it):
diff --git a/src/components/CustomWidgetPanel.ts b/src/components/CustomWidgetPanel.ts index 36927d962..468e960df 100644 --- a/src/components/CustomWidgetPanel.ts +++ b/src/components/CustomWidgetPanel.ts @@ -1,16 +1,9 @@ import { Panel } from './Panel'; import type { CustomWidgetSpec } from '@/services/widget-store'; -import { saveWidget } from '@/services/widget-store'; import { t } from '@/services/i18n'; import { wrapWidgetHtml, wrapProWidgetHtml } from '@/utils/widget-sanitizer'; import { h } from '@/utils/dom-utils'; -const ACCENT_COLORS: Array = [ - '#44ff88', '#ff8844', '#4488ff', '#ff44ff', - '#ffff44', '#ff4444', '#44ffff', '#3b82f6', - null, -]; - export class CustomWidgetPanel extends Panel { private spec: CustomWidgetSpec; @@ -29,17 +22,6 @@ export class CustomWidgetPanel extends Panel { private addHeaderButtons(): void { const closeBtn = this.header.querySelector('.panel-close-btn'); - const colorBtn = h('button', { - className: 'icon-btn widget-color-btn widget-header-btn', - title: t('widgets.changeAccent'), - 'aria-label': t('widgets.changeAccent'), - }); - colorBtn.style.setProperty('background', this.spec.accentColor ?? 'var(--accent)'); - colorBtn.addEventListener('click', (e) => { - e.stopPropagation(); - this.cycleAccentColor(colorBtn); - }); - const chatBtn = h('button', { className: 'icon-btn panel-widget-chat-btn widget-header-btn', title: t('widgets.modifyWithAi'), @@ -63,24 +45,12 @@ export class CustomWidgetPanel extends Panel { } if (closeBtn) { - this.header.insertBefore(colorBtn, closeBtn); this.header.insertBefore(chatBtn, closeBtn); } else { - this.header.appendChild(colorBtn); this.header.appendChild(chatBtn); } } - private cycleAccentColor(btn: HTMLElement): void { - const current = this.spec.accentColor; - const idx = ACCENT_COLORS.indexOf(current); - const next = ACCENT_COLORS[(idx + 1) % ACCENT_COLORS.length] ?? null; - this.spec = { ...this.spec, accentColor: next, updatedAt: Date.now() }; - saveWidget(this.spec); - btn.style.setProperty('background', next ?? 'var(--accent)'); - this.applyAccentColor(); - } - renderWidget(): void { if (this.spec.tier === 'pro') { this.setContent(wrapProWidgetHtml(this.spec.html)); @@ -103,8 +73,6 @@ export class CustomWidgetPanel extends Panel { const titleEl = this.header.querySelector('.panel-title'); if (titleEl) titleEl.textContent = spec.title; this.renderWidget(); - const colorBtn = this.header.querySelector('.widget-color-btn') as HTMLElement | null; - if (colorBtn) colorBtn.style.setProperty('background', spec.accentColor ?? 'var(--accent)'); } getSpec(): CustomWidgetSpec { diff --git a/src/utils/widget-sanitizer.ts b/src/utils/widget-sanitizer.ts index ad31ed70a..737400912 100644 --- a/src/utils/widget-sanitizer.ts +++ b/src/utils/widget-sanitizer.ts @@ -30,12 +30,19 @@ export function sanitizeWidgetHtml(html: string): string { return DOMPurify.sanitize(html, PURIFY_CONFIG) as unknown as string; } +// Strip a leading .panel-header that the agent may generate — the outer +// CustomWidgetPanel frame already displays the title, so a second one is +// always a duplicate. Only the very first element is removed. +function stripLeadingPanelHeader(html: string): string { + return html.replace(/^\s*]*\bclass="panel-header"[^>]*>[\s\S]*?<\/div>\s*/i, ''); +} + export function wrapWidgetHtml(html: string, extraClass = ''): string { const shellClass = ['wm-widget-shell', extraClass].filter(Boolean).join(' '); return `
-
${sanitizeWidgetHtml(html)}
+
${sanitizeWidgetHtml(stripLeadingPanelHeader(html))}
`; @@ -43,6 +50,10 @@ export function wrapWidgetHtml(html: string, extraClass = ''): string { const widgetBodyStore = new Map(); +// Keyed by iframe element — persists HTML across DOM moves so the load listener +// can re-post whenever the browser re-navigates the iframe after a drag. +const iframeHtmlStore = new WeakMap(); + function buildWidgetDoc(bodyContent: string): string { return ` @@ -52,7 +63,8 @@ function buildWidgetDoc(bodyContent: string): string {