diff --git a/README.md b/README.md index 503a900..8e0b46f 100644 --- a/README.md +++ b/README.md @@ -23,10 +23,10 @@ Clay annotates rendered HTML with `data-uri` attributes on every component, page - **SEO tab** — title / meta / og / twitter / JSON-LD with a Twitter + Facebook card preview and lints (length, missing image, duplicate `

`, etc.) - **Recently viewed components** persisted across sessions, with one-click jump back - **Resizable + dockable panel** — drag the inner edges (or the inner-corner grabber) to resize width _and_ height; choose any of four corners or a full-height left/right side dock -- **Toggleable component outlines** (button in the header, h shortcut) with a configurable opacity +- **Refined highlight modes** — _Off_, _Selection only_ (default, like Chrome DevTools' element inspector), _Editable only_, or _All components_. Single accent color with state-based opacity instead of the old multi-color dashed rainbow. Selected component carries a labelled top-left badge. Switch modes from the panel header dropdown or with the h shortcut. - **Auto / light / dark themes** that respond to OS theme changes live - **Keyboard shortcuts** with a ? overlay listing every binding -- **Options page** for theme, dock side + width, environment hosts, highlight intensity, shortcut toggle, and recents history size +- **Options page** for theme, dock side + width, environment hosts, highlight mode + intensity, shortcut toggle, and recents history size - **Floating Clay button (FAB)** on every Clay page — collapsed/idle state of the panel is a small circular button anchored to the user's preferred corner, with a live component-count badge. Click it to expand the full panel. The standard browser-extension chrome pattern (Sentry / Hotjar / Crisp / Intercom). - **Smart popup**: friendly "Not a Clay page" popup on non-Clay pages; on Clay pages the toolbar icon mounts/unmounts the entire extension as an escape hatch - **Toolbar badge** shows the count of Clay components on the current page (cleared on navigation) @@ -75,7 +75,7 @@ Reload the extension in `chrome://extensions` after switching between `dev` and | Resize the panel | Drag the inner vertical / horizontal edge — or the inner-corner grabber for both at once | | Copy URI | Press y then c (component) or p (page) | | Open URI in new tab | Press o then c or p | -| Toggle outlines on page | Press h or click the eye icon in the header | +| Cycle highlight mode | Press h (off → selection → editable → all) or pick from the eye-icon dropdown | | Cycle environment | Click the `env: …` pill at the bottom of the Inspect tab | | Show shortcut overlay | Press ? | | Toggle FAB ↔ panel | Press [ or click the collapse button / the FAB | @@ -124,7 +124,7 @@ src/ │ ├── components/ # Tabs, tree, JSON viewer, diff, breadcrumb… │ └── hooks/ # Drag, theme, shortcuts, selection ├── popup/ # "Not a Clay page" popup (active until a page sends CLAY_DETECTED) -├── options/ # Full options page (env hosts, dock + width, intensity, recents, shortcuts) +├── options/ # Full options page (env hosts, dock + width, highlight mode + intensity, recents, shortcuts) └── lib/ # Pure utilities ├── clay-uri.ts # URI parsing + buildUrl/buildEditorUrl/buildShareLink + copy-as helpers ├── clipboard.ts # Modern + legacy clipboard diff --git a/package.json b/package.json index c7424ff..639b89a 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "clay-slip", - "version": "2.0.2", + "version": "2.1.0", "description": "Modern devtools for Clay CMS pages: visualize component boundaries, inspect data, and navigate the page/layout hierarchy.", "private": true, "type": "module", diff --git a/src/content/highlighter.ts b/src/content/highlighter.ts index ec37aea..a601f76 100644 --- a/src/content/highlighter.ts +++ b/src/content/highlighter.ts @@ -1,34 +1,65 @@ /** - * Manages outline highlighting for Clay components in the host page. - * Uses a single style element scoped via data-attribute selectors so we never - * mutate inline styles on host elements. + * Manages the visual outlines Clay Slip paints over host-page components. + * + * Design rules — read first + * ------------------------- + * 1. **Outline only.** We never set background, border, or position on host + * elements except via two pseudo-elements (annotation dot + selection + * label badge), each gated behind a tiny `position: relative` opt-in. CSS + * `outline` is layout-neutral and overrides cleanly with `!important`. + * + * 2. **One accent color.** Old versions used a six-color rainbow keyed off a + * component's sibling order. It looked busy and carried no real meaning + * (siblings aren't more or less important than each other). The only + * thing the highlight needs to communicate is **state**: + * ambient – "this is a Clay component" + * hover – "this is what you'd select right now" + * selected – "this is what the panel is currently inspecting" + * State is encoded with width + opacity of a single accent, not with hue. + * + * 3. **Mode-gated ambient.** The "every component outlined all the time" + * look turns the page into caution-tape soup on busy layouts. We default + * to `selection` mode, where ambient outlines are off entirely. The user + * opts up to `editable` (only `[data-editable]`) or `all` (every + * component) when they want the bird's-eye view. + * + * 4. **Z-order budget.** All highlight effects live near the top of the + * z-axis (just below the panel itself). We use 2147483645/6 — one short + * of the int max — so panel UI can still sit on top with 2147483647. */ +import type { HighlightMode } from '@/lib/types'; + const STYLE_ID = 'clay-slip-highlight-styles'; + const HIGHLIGHT_ATTR = 'data-clay-slip-color'; const SELECTED_ATTR = 'data-clay-slip-selected'; const HOVER_ATTR = 'data-clay-slip-hover'; const ANNOTATED_ATTR = 'data-clay-slip-annotated'; const MATCH_ATTR = 'data-clay-slip-match'; const FILTER_MODE_ATTR = 'data-clay-slip-filtering'; +const LABEL_ATTR = 'data-clay-slip-label'; +const MODE_ATTR = 'data-clay-slip-mode'; + const OPACITY_VAR = '--clay-slip-outline-opacity'; const DEFAULT_OPACITY = 0.85; -interface PaletteEntry { - readonly r: number; - readonly g: number; - readonly b: number; - readonly style: string; - readonly width: number; -} +/** + * Single accent color for ambient/hover/selected outlines. + * Picked to be readable against both light and dark editorial designs and + * to *not* visually clash with typical brand reds, magentas, or oranges (the + * colors most likely to appear in a Clay site's content). RGB triple is + * inlined into rgba() expressions since CSS `outline` only takes a single + * color and we vary opacity per state. + */ +const ACCENT_RGB = '37, 99, 235'; // tailwind blue-600 -const PALETTE: ReadonlyArray = [ - { r: 221, g: 161, b: 161, style: 'solid', width: 2 }, - { r: 221, g: 221, b: 161, style: 'dashed', width: 3 }, - { r: 176, g: 221, b: 161, style: 'dotted', width: 4 }, - { r: 161, g: 221, b: 221, style: 'solid', width: 5 }, - { r: 161, g: 161, b: 221, style: 'dashed', width: 4 }, - { r: 221, g: 160, b: 221, style: 'double', width: 4 }, -]; +/** Element width/opacity tokens per state. Tweak these together. */ +const TOKENS = { + ambient: { width: 1, alpha: 0.18, offset: -1 }, + hover: { width: 2, alpha: 0.7, offset: -2 }, + selected: { width: 2, alpha: 1, offset: -2 }, + match: { width: 2, alpha: 0.95, offset: -2 }, +} as const; export function installHighlightStyles(): void { if (document.getElementById(STYLE_ID)) return; @@ -37,48 +68,132 @@ export function installHighlightStyles(): void { style.textContent = buildStyleSheet(); document.head.appendChild(style); setHighlightOpacity(DEFAULT_OPACITY); + // Keep the previously-active mode if one was set before mount; otherwise + // start in 'selection' mode (also the prefs default), which keeps the page + // pristine until the user opts up. + if (!document.documentElement.hasAttribute(MODE_ATTR)) { + setHighlightMode('selection'); + } } function buildStyleSheet(): string { - const colorRules = PALETTE.map( - (p, i) => - `[${HIGHLIGHT_ATTR}="${i}"]{outline:${p.width}px ${p.style} rgba(${p.r},${p.g},${p.b},var(${OPACITY_VAR},${DEFAULT_OPACITY})) !important;outline-offset:-${p.width}px !important;}` - ).join('\n'); + // We use the var() for opacity so the user's "intensity" slider can scale + // every outline at once without re-emitting the stylesheet. + const o = (alpha: number) => + `rgba(${ACCENT_RGB}, calc(${alpha} * var(${OPACITY_VAR}, ${DEFAULT_OPACITY})))`; return ` - ${colorRules} - [${HOVER_ATTR}]{outline:3px solid rgba(255,175,58,var(${OPACITY_VAR},${DEFAULT_OPACITY})) !important;outline-offset:-3px !important;} - [${SELECTED_ATTR}]{outline:5px solid rgba(226,44,44,var(${OPACITY_VAR},${DEFAULT_OPACITY})) !important;outline-offset:-5px !important;} - [${ANNOTATED_ATTR}]{position:relative;} - [${ANNOTATED_ATTR}]::before{ - content:"";position:absolute;top:4px;right:4px;width:10px;height:10px;border-radius:50%; - background:rgba(245,158,11,0.95);box-shadow:0 0 0 2px rgba(255,255,255,0.85); - pointer-events:none;z-index:2147483646; + /* ── Ambient outlines: gated by html[data-clay-slip-mode] ──────────── + Mode 'all' → every [data-uri] gets a 1px ghost outline. + Mode 'editable' → only [data-editable] does. + Mode 'selection' or 'off' → no rule matches; nothing painted. */ + html[${MODE_ATTR}="all"] [${HIGHLIGHT_ATTR}], + html[${MODE_ATTR}="editable"] [${HIGHLIGHT_ATTR}][data-editable] { + outline: ${TOKENS.ambient.width}px solid ${o(TOKENS.ambient.alpha)} !important; + outline-offset: ${TOKENS.ambient.offset}px !important; + } + + /* Hover and selection always render regardless of mode (otherwise + click-to-inspect would be invisible in 'off'). */ + [${HOVER_ATTR}] { + outline: ${TOKENS.hover.width}px solid ${o(TOKENS.hover.alpha)} !important; + outline-offset: ${TOKENS.hover.offset}px !important; + } + + [${SELECTED_ATTR}] { + outline: ${TOKENS.selected.width}px solid ${o(TOKENS.selected.alpha)} !important; + outline-offset: ${TOKENS.selected.offset}px !important; + position: relative; + } + + /* Selection label — a small pill in the top-left of the selected box + reading the component name from the data-clay-slip-label attribute. + Sits *outside* the box when there's room above it, otherwise tucks + inside via translateY(0). The negative-then-clamp trick keeps the + label visible at the very top of the page where translateY(-100%) + would scroll out of view. */ + [${SELECTED_ATTR}][${LABEL_ATTR}]::before { + content: attr(${LABEL_ATTR}); + position: absolute; + top: 0; + left: 0; + transform: translateY(-100%); + background: rgb(${ACCENT_RGB}); + color: #ffffff; + font: 500 11px/1.4 ui-sans-serif, system-ui, -apple-system, Segoe UI, Roboto, sans-serif; + padding: 2px 6px; + border-radius: 3px 3px 3px 0; + white-space: nowrap; + max-width: 240px; + overflow: hidden; + text-overflow: ellipsis; + pointer-events: none; + z-index: 2147483646; + box-shadow: 0 1px 2px rgba(0, 0, 0, 0.15); + } + + /* Annotation dot — kept from the previous iteration, retuned to amber + so it doesn't blend with the new blue accent. */ + [${ANNOTATED_ATTR}] { position: relative; } + [${ANNOTATED_ATTR}]::after { + content: ""; + position: absolute; + top: 4px; + right: 4px; + width: 8px; + height: 8px; + border-radius: 50%; + background: rgba(245, 158, 11, 0.95); + box-shadow: 0 0 0 2px rgba(255, 255, 255, 0.85); + pointer-events: none; + z-index: 2147483646; + } + + /* Find-on-page filter mode: dim non-matches, highlight matches. Match + outline uses an emerald green so it reads as a *different signal* + than the regular accent. */ + html[${FILTER_MODE_ATTR}] [data-uri]:not([${MATCH_ATTR}]) { + opacity: 0.25 !important; + transition: opacity 0.12s; + } + [${MATCH_ATTR}] { + outline: ${TOKENS.match.width}px solid + rgba(34, 197, 94, calc(${TOKENS.match.alpha} * var(${OPACITY_VAR}, ${DEFAULT_OPACITY}))) !important; + outline-offset: ${TOKENS.match.offset}px !important; } - html[${FILTER_MODE_ATTR}] [data-uri]:not([${MATCH_ATTR}]){opacity:0.25 !important;transition:opacity 0.12s;} - [${MATCH_ATTR}]{outline:3px solid rgba(34,197,94,var(${OPACITY_VAR},${DEFAULT_OPACITY})) !important;outline-offset:-3px !important;} `; } -export function applyHighlights(elements: HTMLElement[]): void { - let lastParent: ParentNode | null = null; - let colorIdx = 0; - for (const el of elements) { - if (lastParent && el.parentNode !== lastParent) { - colorIdx = (colorIdx + 1) % PALETTE.length; - } - el.setAttribute(HIGHLIGHT_ATTR, String(colorIdx)); - lastParent = el.parentNode; +/** + * Tag every component element so the ambient + state CSS selectors have + * something to target. Also stashes a human-readable label on the element + * so the selection badge can read it via `attr()`. + * + * The previous implementation also encoded a per-sibling color index here. + * That was the source of the rainbow look and we drop it — `[data-clay-slip-color]` + * is now just a presence flag. + */ +export function applyHighlights( + elements: readonly HTMLElement[], + labels?: readonly string[] +): void { + for (let i = 0; i < elements.length; i++) { + const el = elements[i]; + if (!el) continue; + el.setAttribute(HIGHLIGHT_ATTR, ''); + const label = labels?.[i]; + if (label) el.setAttribute(LABEL_ATTR, label); } } -export function clearHighlights(elements: HTMLElement[]): void { +export function clearHighlights(elements: readonly HTMLElement[]): void { for (const el of elements) { el.removeAttribute(HIGHLIGHT_ATTR); el.removeAttribute(SELECTED_ATTR); el.removeAttribute(HOVER_ATTR); el.removeAttribute(ANNOTATED_ATTR); el.removeAttribute(MATCH_ATTR); + el.removeAttribute(LABEL_ATTR); } document.documentElement.removeAttribute(FILTER_MODE_ATTR); } @@ -93,10 +208,29 @@ export function setHovered(prev: HTMLElement | null, next: HTMLElement | null): if (next) next.setAttribute(HOVER_ATTR, ''); } -export function setHighlightingEnabled(enabled: boolean): void { - const style = document.getElementById(STYLE_ID) as HTMLStyleElement | null; - if (!style) return; - style.disabled = !enabled; +/** + * Switch the global ambient-outline mode by flipping a single attribute on + * ``. The attribute is the join key the stylesheet uses for its mode + * gating, so this is the only function that needs to run when mode changes. + * + * `'off'` removes the attribute entirely so nothing matches the ambient + * rule — slightly cheaper than carrying an explicit `[mode="off"]` selector + * around, and means no ambient rule can ever fire by accident. + */ +export function setHighlightMode(mode: HighlightMode): void { + if (mode === 'off') { + document.documentElement.setAttribute(MODE_ATTR, 'off'); + } else { + document.documentElement.setAttribute(MODE_ATTR, mode); + } +} + +export function getHighlightMode(): HighlightMode { + const attr = document.documentElement.getAttribute(MODE_ATTR); + if (attr === 'off' || attr === 'selection' || attr === 'editable' || attr === 'all') { + return attr; + } + return 'selection'; } export function setHighlightOpacity(opacity: number): void { diff --git a/src/content/index.ts b/src/content/index.ts index 2f13ae5..97e006b 100644 --- a/src/content/index.ts +++ b/src/content/index.ts @@ -17,7 +17,13 @@ function send(message: RuntimeMessage): void { function paintAndSync(): number { installHighlightStyles(); const components = readComponents(); - applyHighlights(components.map((c) => c.element)); + applyHighlights( + components.map((c) => c.element), + // Selection badge label — passed down so the CSS pseudo-element can + // surface it via attr(). Falling back to component name keeps the + // badge useful when an instance ID isn't present. + components.map((c) => c.displayName || c.name) + ); useStore.getState().setComponents(components); return components.length; } diff --git a/src/content/panel/App.tsx b/src/content/panel/App.tsx index bd9cc17..6a32db6 100644 --- a/src/content/panel/App.tsx +++ b/src/content/panel/App.tsx @@ -2,7 +2,7 @@ import { useEffect, useRef } from 'react'; import { loadPreferences, onPreferencesChanged } from '@/lib/storage'; import { listAnnotations, onAnnotationsChanged } from '@/lib/annotations'; import { loadRecents, onRecentsChanged, pushRecent } from '@/lib/recents'; -import { setAnnotatedUris, setHighlightingEnabled, setHighlightOpacity } from '../highlighter'; +import { setAnnotatedUris, setHighlightMode, setHighlightOpacity } from '../highlighter'; import { useStore } from './store'; import { useDraggable } from './hooks/useDraggable'; import { useKeyboardShortcuts } from './hooks/useKeyboardShortcuts'; @@ -32,7 +32,7 @@ export function App() { const panelWidth = useStore((s) => s.preferences.panelWidth); const panelHeight = useStore((s) => s.preferences.panelHeight); const highlightOpacity = useStore((s) => s.preferences.highlightOpacity); - const highlightEnabled = useStore((s) => s.highlightEnabled); + const highlightMode = useStore((s) => s.preferences.highlightMode); const setPrefs = useStore((s) => s.setPreferences); const setRecents = useStore((s) => s.setRecents); const setAnnotations = useStore((s) => s.setAnnotations); @@ -68,8 +68,8 @@ export function App() { }, [highlightOpacity]); useEffect(() => { - setHighlightingEnabled(highlightEnabled); - }, [highlightEnabled]); + setHighlightMode(highlightMode); + }, [highlightMode]); // Sync the annotation dot indicators on the page whenever either set changes. useEffect(() => { diff --git a/src/content/panel/components/Header.tsx b/src/content/panel/components/Header.tsx index 4462566..543d281 100644 --- a/src/content/panel/components/Header.tsx +++ b/src/content/panel/components/Header.tsx @@ -2,6 +2,7 @@ import type { Ref } from 'react'; import clayIconUrl from '@/assets/clay-icon.png?inline'; import type { RuntimeMessage } from '@/lib/types'; import { Icon } from './Icon'; +import { HighlightModeMenu } from './HighlightModeMenu'; import { useStore } from '../store'; interface HeaderProps { @@ -12,8 +13,6 @@ export function Header({ ref }: HeaderProps) { const page = useStore((s) => s.page); const toggleCollapsed = useStore((s) => s.toggleCollapsed); const toggleShortcuts = useStore((s) => s.toggleShortcuts); - const toggleHighlights = useStore((s) => s.toggleHighlights); - const highlightEnabled = useStore((s) => s.highlightEnabled); const componentCount = useStore((s) => s.components.length); const openOptions = () => { @@ -36,15 +35,7 @@ export function Header({ ref }: HeaderProps) { {page.isPublished ? 'Published' : 'Draft'} )} - + + ))} + + + ); +} + +/** Header real estate is tight, so we abbreviate the label next to the icon. */ +function compactLabel(mode: HighlightMode): string { + switch (mode) { + case 'off': + return 'Off'; + case 'selection': + return 'Sel'; + case 'editable': + return 'Edit'; + case 'all': + return 'All'; + } +} diff --git a/src/content/panel/hooks/useKeyboardShortcuts.ts b/src/content/panel/hooks/useKeyboardShortcuts.ts index 8e16906..099f8ef 100644 --- a/src/content/panel/hooks/useKeyboardShortcuts.ts +++ b/src/content/panel/hooks/useKeyboardShortcuts.ts @@ -37,7 +37,7 @@ export function useKeyboardShortcuts(): void { selected, toggleShortcuts, toggleCollapsed, - toggleHighlights, + cycleHighlightMode, pushToast, setActiveTab, } = state; @@ -61,7 +61,7 @@ export function useKeyboardShortcuts(): void { } if ((e.key === 'h' || e.key === 'H') && !e.metaKey && !e.ctrlKey && !e.altKey) { e.preventDefault(); - toggleHighlights(); + cycleHighlightMode(); return; } if ((e.key === 't' || e.key === 'T') && !e.metaKey && !e.ctrlKey) { @@ -119,7 +119,7 @@ export function useKeyboardShortcuts(): void { export const SHORTCUTS = [ { keys: ['?'], description: 'Show this shortcut overlay' }, { keys: ['['], description: 'Collapse / expand the panel' }, - { keys: ['h'], description: 'Toggle component outlines' }, + { keys: ['h'], description: 'Cycle highlight mode (off → selection → editable → all)' }, { keys: ['i'], description: 'Switch to Inspect tab' }, { keys: ['t'], description: 'Switch to Tree tab' }, { keys: ['y', 'p'], description: 'Copy current page URI' }, diff --git a/src/content/panel/store.ts b/src/content/panel/store.ts index 01b035c..5ac4511 100644 --- a/src/content/panel/store.ts +++ b/src/content/panel/store.ts @@ -3,10 +3,12 @@ import type { Annotation, ClayComponentInfo, ClayPageInfo, + HighlightMode, RecentComponent, UserPreferences, } from '@/lib/types'; -import { DEFAULT_PREFERENCES } from '@/lib/types'; +import { DEFAULT_PREFERENCES, HIGHLIGHT_MODE_ORDER } from '@/lib/types'; +import { savePreferences } from '@/lib/storage'; import { readPageInfo } from '../page-info'; export type PanelTab = 'inspect' | 'tree' | 'json' | 'diff' | 'seo' | 'notes'; @@ -32,7 +34,6 @@ interface StoreState { find: FindState; activeTab: PanelTab; showShortcuts: boolean; - highlightEnabled: boolean; preferences: UserPreferences; toasts: ToastMessage[]; recents: RecentComponent[]; @@ -47,7 +48,17 @@ interface StoreState { setFind: (next: FindState) => void; setActiveTab: (tab: PanelTab) => void; toggleShortcuts: () => void; - toggleHighlights: () => void; + /** + * Update the ambient-outline mode. Persists to chrome.storage.sync so + * the choice survives reloads and propagates to other tabs. + */ + setHighlightMode: (mode: HighlightMode) => void; + /** + * Cycle through the four modes in `HIGHLIGHT_MODE_ORDER`. Bound to the + * `h` keyboard shortcut for muscle memory with the old "toggle outlines" + * behavior. + */ + cycleHighlightMode: () => void; setPreferences: (next: Partial) => void; setRecents: (next: RecentComponent[]) => void; setAnnotations: (next: Annotation[]) => void; @@ -70,7 +81,6 @@ export const useStore = create()((set) => ({ find: { query: '', index: 0 }, activeTab: 'inspect', showShortcuts: false, - highlightEnabled: true, preferences: DEFAULT_PREFERENCES, toasts: [], recents: [], @@ -85,7 +95,16 @@ export const useStore = create()((set) => ({ setFind: (find) => set({ find }), setActiveTab: (activeTab) => set({ activeTab }), toggleShortcuts: () => set((s) => ({ showShortcuts: !s.showShortcuts })), - toggleHighlights: () => set((s) => ({ highlightEnabled: !s.highlightEnabled })), + setHighlightMode: (mode) => { + set((s) => ({ preferences: { ...s.preferences, highlightMode: mode } })); + void savePreferences({ highlightMode: mode }); + }, + cycleHighlightMode: () => { + const current = useStore.getState().preferences.highlightMode; + const idx = HIGHLIGHT_MODE_ORDER.indexOf(current); + const next = HIGHLIGHT_MODE_ORDER[(idx + 1) % HIGHLIGHT_MODE_ORDER.length] ?? 'selection'; + useStore.getState().setHighlightMode(next); + }, setPreferences: (prefs) => set((s) => ({ preferences: { ...s.preferences, ...prefs } })), setRecents: (recents) => set({ recents }), setAnnotations: (annotations) => diff --git a/src/content/panel/styles.css b/src/content/panel/styles.css index 2201cf4..90bb1fa 100644 --- a/src/content/panel/styles.css +++ b/src/content/panel/styles.css @@ -188,6 +188,103 @@ color: var(--cs-text-subtle); } +/* ── Highlight-mode menu (header dropdown) ───────────────────────────── + Built on a native
for click-outside / keyboard / a11y for free. + The summary doubles as an .cs-icon-btn so it visually blends with the + sibling header buttons; the popover sits absolutely positioned beneath. */ +.cs-mode-menu { + position: relative; +} + +.cs-mode-menu > summary { + list-style: none; + cursor: pointer; + gap: 4px; +} + +.cs-mode-menu > summary::-webkit-details-marker { + display: none; +} + +.cs-mode-trigger-label { + font-size: 11px; + font-weight: 500; + color: inherit; + letter-spacing: 0.01em; +} + +.cs-mode-popover { + position: absolute; + top: calc(100% + 6px); + right: 0; + min-width: 240px; + background: var(--cs-bg); + border: 1px solid var(--cs-border); + border-radius: 8px; + box-shadow: + 0 1px 2px rgba(0, 0, 0, 0.06), + 0 8px 24px rgba(0, 0, 0, 0.08); + padding: 6px; + z-index: 10; +} + +.cs-mode-popover-header { + font-size: 10px; + text-transform: uppercase; + letter-spacing: 0.06em; + color: var(--cs-text-subtle); + padding: 4px 8px 6px; +} + +.cs-mode-option { + display: flex; + gap: 8px; + width: 100%; + background: transparent; + border: none; + text-align: left; + padding: 6px 8px; + border-radius: 6px; + cursor: pointer; + color: var(--cs-text); + font: inherit; +} + +.cs-mode-option:hover { + background: var(--cs-bg-hover); +} + +.cs-mode-option-active { + background: var(--cs-accent-bg); +} + +.cs-mode-option-bullet { + flex: 0 0 auto; + width: 12px; + text-align: center; + color: var(--cs-accent); + font-size: 10px; + line-height: 1.5; +} + +.cs-mode-option-text { + display: flex; + flex-direction: column; + gap: 1px; + min-width: 0; +} + +.cs-mode-option-label { + font-size: 12px; + font-weight: 500; +} + +.cs-mode-option-help { + font-size: 11px; + color: var(--cs-text-muted); + line-height: 1.35; +} + .cs-tabs { display: flex; gap: 0; diff --git a/src/lib/types.ts b/src/lib/types.ts index 842bcf7..4693786 100644 --- a/src/lib/types.ts +++ b/src/lib/types.ts @@ -32,6 +32,42 @@ export type PanelPosition = | 'left-side' | 'right-side'; +/** + * Controls *which* components show an ambient outline on the page. + * Hover and selection always render their own highlight regardless of mode + * (otherwise click-to-inspect would be invisible). + * + * - `off` – no ambient outlines at all. Hover + selection still highlight. + * - `selection` – ambient is off; only the hovered/selected element is outlined. + * This is the new default — feels closest to Chrome DevTools. + * - `editable` – ambient outlines on `[data-editable]` components only. + * Useful for editorial / PM workflows. + * - `all` – ambient outlines on every component. + * The "give me the bird's-eye view of structure" mode. + */ +export type HighlightMode = 'off' | 'selection' | 'editable' | 'all'; + +export const HIGHLIGHT_MODE_ORDER: readonly HighlightMode[] = [ + 'off', + 'selection', + 'editable', + 'all', +]; + +export const HIGHLIGHT_MODE_LABELS: Readonly> = { + off: 'Off', + selection: 'Selection only', + editable: 'Editable only', + all: 'All components', +}; + +export const HIGHLIGHT_MODE_DESCRIPTIONS: Readonly> = { + off: 'No outlines anywhere. The panel still works for inspection.', + selection: 'Only the component you hover or click gets an outline.', + editable: 'Subtle outlines on every editable component.', + all: 'Subtle outlines on every Clay component.', +}; + export interface UserPreferences { readonly theme: 'auto' | 'light' | 'dark'; readonly panelPosition: PanelPosition; @@ -39,6 +75,7 @@ export interface UserPreferences { readonly panelHeight: number; readonly defaultEnvironment: Environment; readonly environments: EnvironmentHosts; + readonly highlightMode: HighlightMode; readonly highlightOpacity: number; readonly enableShortcuts: boolean; readonly maxRecentComponents: number; @@ -86,6 +123,7 @@ export const DEFAULT_PREFERENCES: UserPreferences = { panelHeight: 540, defaultEnvironment: 'prod', environments: DEFAULT_ENVIRONMENT_HOSTS, + highlightMode: 'selection', highlightOpacity: 0.85, enableShortcuts: true, maxRecentComponents: 20, diff --git a/src/options/Options.tsx b/src/options/Options.tsx index 159776c..a2fe77d 100644 --- a/src/options/Options.tsx +++ b/src/options/Options.tsx @@ -7,10 +7,14 @@ import { DEFAULT_PREFERENCES, ENVIRONMENT_LABELS, ENVIRONMENT_ORDER, + HIGHLIGHT_MODE_DESCRIPTIONS, + HIGHLIGHT_MODE_LABELS, + HIGHLIGHT_MODE_ORDER, SITE_ENV_LABELS, SITE_ENV_ORDER, type Environment, type EnvironmentHosts, + type HighlightMode, type PanelPosition, type SiteEnv, type SiteHostMapping, @@ -162,11 +166,32 @@ export function Options() { /> + +