-
+
+
{schema.description}
+
+
{schema.jsonLdCode}
@@ -115,7 +115,7 @@ export function SchemaDisplay({ schemas, onToast, className }: SchemaDisplayProp
href={schema.documentationUrl}
target="_blank"
rel="noopener noreferrer"
- className="mt-2 inline-flex items-center gap-1 text-body-12 text-accent-blue hover:underline"
+ className="mt-2 inline-flex items-center gap-1 text-body-12 text-brand hover:underline"
>
Documentation
@@ -127,20 +127,20 @@ export function SchemaDisplay({ schemas, onToast, className }: SchemaDisplayProp
return (
-
+
Recommended Schema Markup
{required.length > 0 && (
- Required
+ Required
{required.map(renderSchemaCard)}
)}
{optional.length > 0 && (
- Optional
+ Optional
{optional.map(renderSchemaCard)}
)}
diff --git a/app/src/components/SummaryCard.tsx b/app/src/components/SummaryCard.tsx
index 07c1e73..ead78a7 100644
--- a/app/src/components/SummaryCard.tsx
+++ b/app/src/components/SummaryCard.tsx
@@ -20,16 +20,9 @@ function getArrowCircleClass(passed: number, total: number): string {
return "arrow-circle-warning";
}
-// SVG Triangle icons - larger per Figma spec (~16px)
function TriangleUp({ className }: { className?: string }) {
return (
-
+
);
@@ -37,13 +30,7 @@ function TriangleUp({ className }: { className?: string }) {
function TriangleDown({ className }: { className?: string }) {
return (
-
+
);
@@ -57,44 +44,33 @@ export function SummaryCard({ category, onClick, className }: SummaryCardProps)
{/* Header Row */}
-
-
- {category.label}
-
+
+
{category.label}
{category.passed}/{category.total} passed
- {/* Arrow circle - matches badge color, 35px, black icon */}
-
- {/* Individual Check Items - all text #c7c7c7, 18px, line-height 130% */}
-
+ {/* Individual Check Items */}
+
{category.checks.map((check) => (
{check.status === "pass" ? (
-
+
) : (
-
+
)}
-
- {check.title}
-
+ {check.title}
))}
diff --git a/app/src/components/ui/Badge.tsx b/app/src/components/ui/Badge.tsx
index 4a550a7..7ccff86 100644
--- a/app/src/components/ui/Badge.tsx
+++ b/app/src/components/ui/Badge.tsx
@@ -17,15 +17,16 @@ export function Badge({ status, priority, className }: BadgeProps) {
? "Medium"
: "Low Priority";
+ const isError = status === "fail" && priority === "high";
+ const isWarning = status === "warning" || (status === "fail" && priority !== "high");
+
return (
diff --git a/app/src/components/ui/Button.tsx b/app/src/components/ui/Button.tsx
index 0e59865..6a17b6e 100644
--- a/app/src/components/ui/Button.tsx
+++ b/app/src/components/ui/Button.tsx
@@ -21,15 +21,15 @@ export function Button({
return (
(
({ label, className, id, ...props }, ref) => {
const inputId = id || label?.toLowerCase().replace(/\s+/g, "-");
return (
-
+
{label && (
-
+
{label}
)}
@@ -22,7 +19,7 @@ export const Input = forwardRef(
ref={ref}
id={inputId}
className={cn(
- "w-full rounded-[10px] border border-[#717171] bg-bg-500 p-[14px] text-[18px] leading-[1.3] text-text-primary shadow-[0px_1px_2px_0px_rgba(10,13,20,0.03)] placeholder:text-text-secondary outline-none focus:ring-1 focus:ring-accent-blue transition-shadow",
+ "w-full rounded-input border border-border bg-surface px-3.5 py-3 text-body text-ink shadow-card outline-none transition placeholder:text-faint focus:border-brand focus:ring-2 focus:ring-brand/30",
className,
)}
{...props}
diff --git a/app/src/components/ui/Logo.tsx b/app/src/components/ui/Logo.tsx
new file mode 100644
index 0000000..3ca3d83
--- /dev/null
+++ b/app/src/components/ui/Logo.tsx
@@ -0,0 +1,58 @@
+import { cn } from "@/lib/utils";
+
+interface MarkProps {
+ size?: number;
+ className?: string;
+}
+
+/**
+ * Optia brand mark — an open score-gauge ring with an upward arrow,
+ * evoking "optimize → score rising". Uses the fixed indigo→sky brand
+ * gradient in both themes. Mirrors the extension icon art.
+ */
+export function OptiaMark({ size = 28, className }: MarkProps) {
+ const gid = "optia-mark-grad";
+ return (
+
+
+
+
+
+
+
+ {/* Gauge ring, open at the bottom */}
+
+ {/* Upward arrow */}
+
+
+ );
+}
+
+export function OptiaWordmark({ className }: { className?: string }) {
+ return (
+
+
+ Optia
+
+ );
+}
diff --git a/app/src/components/ui/ScoreGauge.tsx b/app/src/components/ui/ScoreGauge.tsx
index 32dfac7..8b0246f 100644
--- a/app/src/components/ui/ScoreGauge.tsx
+++ b/app/src/components/ui/ScoreGauge.tsx
@@ -8,16 +8,29 @@ interface ScoreGaugeProps {
className?: string;
}
-function getScoreTierColor(score: number): string {
- if (score >= 70) return "#A2FFB4"; // Green
- if (score >= 40) return "#FFDD64"; // Yellow
- return "#FF4343"; // Red
+type Tier = "good" | "warn" | "poor";
+
+function getTier(score: number): Tier {
+ if (score >= 70) return "good";
+ if (score >= 40) return "warn";
+ return "poor";
}
+const STROKE: Record = {
+ good: "stroke-good",
+ warn: "stroke-warn",
+ poor: "stroke-poor",
+};
+const TEXT: Record = {
+ good: "text-good",
+ warn: "text-warn",
+ poor: "text-poor",
+};
+
export function ScoreGauge({
score,
- size = 244,
- strokeWidth = 12,
+ size = 216,
+ strokeWidth = 14,
className,
}: ScoreGaugeProps) {
const [displayScore, setDisplayScore] = useState(0);
@@ -25,7 +38,7 @@ export function ScoreGauge({
const circumference = 2 * Math.PI * radius;
const offset = circumference - (displayScore / 100) * circumference;
- const tierColor = getScoreTierColor(score);
+ const tier = getTier(score);
useEffect(() => {
let frame: number;
@@ -47,59 +60,50 @@ export function ScoreGauge({
return (
- {/* Background track circle - BG 300 (#787878), no rounded ends */}
+ {/* Track */}
- {/* Glow effect layer - blurred arc behind the main arc, subtle opacity */}
+ {/* Glow */}
- {/* Main score arc */}
+ {/* Main arc */}
- {/* Score number - 60px, bold, tier color */}
{displayScore}
- {/* SEO Score label - 16px, 600 weight, white */}
-
- SEO Score
-
+ SEO Score
);
diff --git a/app/src/components/ui/Select.tsx b/app/src/components/ui/Select.tsx
index 57b4d65..edab9bb 100644
--- a/app/src/components/ui/Select.tsx
+++ b/app/src/components/ui/Select.tsx
@@ -15,12 +15,9 @@ export function Select({
}: SelectProps) {
const selectId = id || label?.toLowerCase().replace(/\s+/g, "-");
return (
-
+
{label && (
-
+
{label}
)}
@@ -28,7 +25,7 @@ export function Select({
))}
-
+
);
diff --git a/app/src/components/ui/ThemeToggle.tsx b/app/src/components/ui/ThemeToggle.tsx
new file mode 100644
index 0000000..1672e2f
--- /dev/null
+++ b/app/src/components/ui/ThemeToggle.tsx
@@ -0,0 +1,23 @@
+import { Moon, Sun } from "lucide-react";
+import { useTheme } from "@/lib/theme";
+import { cn } from "@/lib/utils";
+
+export function ThemeToggle({ className }: { className?: string }) {
+ const { theme, toggle } = useTheme();
+ const isDark = theme === "dark";
+
+ return (
+
+ {isDark ? : }
+
+ );
+}
diff --git a/app/src/components/ui/Toast.tsx b/app/src/components/ui/Toast.tsx
index 016abf6..e25538c 100644
--- a/app/src/components/ui/Toast.tsx
+++ b/app/src/components/ui/Toast.tsx
@@ -28,13 +28,13 @@ export function Toast({ message, visible, onClose, duration = 3000 }: ToastProps
return (
-
- {message}
-
+
+ {message}
+
diff --git a/app/src/components/ui/Toggle.tsx b/app/src/components/ui/Toggle.tsx
index 43b68bd..6b9cc70 100644
--- a/app/src/components/ui/Toggle.tsx
+++ b/app/src/components/ui/Toggle.tsx
@@ -15,24 +15,24 @@ export function Toggle({ checked, onChange, label, id }: ToggleProps) {
onChange(e.target.checked)}
/>
- {label &&
{label} }
+ {label &&
{label} }
);
}
diff --git a/app/src/components/ui/Tooltip.tsx b/app/src/components/ui/Tooltip.tsx
index 52c3735..7f19e18 100644
--- a/app/src/components/ui/Tooltip.tsx
+++ b/app/src/components/ui/Tooltip.tsx
@@ -18,9 +18,9 @@ export function Tooltip({ content, children, className }: TooltipProps) {
>
{children}
{show && (
-
diff --git a/app/src/content/highlighter.ts b/app/src/content/highlighter.ts
index 31f707e..2af12e3 100644
--- a/app/src/content/highlighter.ts
+++ b/app/src/content/highlighter.ts
@@ -1,5 +1,5 @@
-const HIGHLIGHT_CLASS = "seo-copilot-highlight";
-const STYLE_ID = "seo-copilot-styles";
+const HIGHLIGHT_CLASS = "optia-highlight";
+const STYLE_ID = "optia-styles";
function injectStyles() {
if (document.getElementById(STYLE_ID)) return;
@@ -7,7 +7,7 @@ function injectStyles() {
style.id = STYLE_ID;
style.textContent = `
.${HIGHLIGHT_CLASS} {
- outline: 3px solid #FF4343 !important;
+ outline: 3px solid #DC2626 !important;
outline-offset: 2px;
position: relative;
}
@@ -16,7 +16,7 @@ function injectStyles() {
position: absolute;
top: -24px;
left: 0;
- background: #FF4343;
+ background: #DC2626;
color: white;
font-size: 11px;
padding: 2px 6px;
diff --git a/app/src/lib/theme.ts b/app/src/lib/theme.ts
new file mode 100644
index 0000000..4e8abed
--- /dev/null
+++ b/app/src/lib/theme.ts
@@ -0,0 +1,44 @@
+import { useCallback, useEffect, useState } from "react";
+import { getStorageItem, setStorageItem } from "./storage";
+
+export type Theme = "light" | "dark";
+
+const THEME_KEY = "theme";
+
+/** Apply (or remove) the `.dark` class on the document root. */
+export function applyTheme(theme: Theme): void {
+ if (typeof document === "undefined") return;
+ document.documentElement.classList.toggle("dark", theme === "dark");
+}
+
+/** Read the persisted theme (default light) and apply it. */
+export async function loadTheme(): Promise
{
+ const stored = await getStorageItem(THEME_KEY);
+ const theme: Theme = stored === "dark" ? "dark" : "light";
+ applyTheme(theme);
+ return theme;
+}
+
+/**
+ * Theme hook: light-first, persisted in chrome.storage.local.
+ * The visual source of truth is the `.dark` class on ; this hook
+ * tracks it for UI state (e.g. the toggle icon).
+ */
+export function useTheme() {
+ const [theme, setTheme] = useState("light");
+
+ useEffect(() => {
+ loadTheme().then(setTheme);
+ }, []);
+
+ const toggle = useCallback(() => {
+ setTheme((prev) => {
+ const next: Theme = prev === "dark" ? "light" : "dark";
+ applyTheme(next);
+ void setStorageItem(THEME_KEY, next);
+ return next;
+ });
+ }, []);
+
+ return { theme, toggle };
+}
diff --git a/app/src/options/Options.tsx b/app/src/options/Options.tsx
index 4a059d6..a6c8829 100644
--- a/app/src/options/Options.tsx
+++ b/app/src/options/Options.tsx
@@ -1,5 +1,8 @@
import { useState, useEffect } from "react";
+import { ChevronDown } from "lucide-react";
import { SUPPORTED_LANGUAGES } from "@/lib/languages";
+import { OptiaWordmark } from "@/components/ui/Logo";
+import { ThemeToggle } from "@/components/ui/ThemeToggle";
const languages = SUPPORTED_LANGUAGES.map((lang) => ({
value: lang.code,
@@ -30,60 +33,68 @@ export function Options() {
};
return (
-
-
- Settings
-
-
-
-
-
- OpenAI API key
-
-
setApiKey(e.target.value)}
- className="rounded-lg border border-[#717171] bg-bg-500 p-3 text-sm text-text-primary placeholder:text-text-secondary outline-none focus:ring-1 focus:ring-accent-blue"
- />
+
+
+
+
+
-
-
- Default language
-
- setLanguage(e.target.value)}
- className="rounded-lg border border-[#717171] bg-bg-500 p-3 text-sm text-text-primary outline-none focus:ring-1 focus:ring-accent-blue"
- >
- {languages.map((lang) => (
-
- {lang.label}
-
- ))}
-
-
+
+
Settings
+
+ Connect OpenAI to unlock AI-powered recommendations.
+
+
+
+
+
+ OpenAI API key
+
+
setApiKey(e.target.value)}
+ className="rounded-input border border-border bg-surface px-3.5 py-3 text-body text-ink shadow-card placeholder:text-faint outline-none transition focus:border-brand focus:ring-2 focus:ring-brand/30"
+ />
+
+ Stored locally in your browser — only ever sent directly to OpenAI.
+
+
-
- Save
-
+
+
+ Default language
+
+
+ setLanguage(e.target.value)}
+ className="w-full appearance-none rounded-input border border-border bg-surface px-3.5 py-3 pr-10 text-body text-ink shadow-card outline-none transition focus:border-brand focus:ring-2 focus:ring-brand/30"
+ >
+ {languages.map((lang) => (
+
+ {lang.label}
+
+ ))}
+
+
+
+
- {saved && (
-
Settings saved.
- )}
+
+ Save
+
+
+ {saved &&
Settings saved.
}
+
+
);
diff --git a/app/src/options/index.html b/app/src/options/index.html
index 33c1900..e5ea93d 100644
--- a/app/src/options/index.html
+++ b/app/src/options/index.html
@@ -3,7 +3,7 @@
-
AI SEO Copilot — Settings
+
Optia — Settings
diff --git a/app/src/options/main.tsx b/app/src/options/main.tsx
index 7a87003..6d740d8 100644
--- a/app/src/options/main.tsx
+++ b/app/src/options/main.tsx
@@ -1,8 +1,12 @@
import React from "react";
import ReactDOM from "react-dom/client";
import { Options } from "./Options";
+import { loadTheme } from "@/lib/theme";
import "@/styles/globals.css";
+// Apply persisted theme (light default) before first paint settles.
+void loadTheme();
+
ReactDOM.createRoot(document.getElementById("root")!).render(
diff --git a/app/src/sidepanel/App.tsx b/app/src/sidepanel/App.tsx
index e6e121a..e80e706 100644
--- a/app/src/sidepanel/App.tsx
+++ b/app/src/sidepanel/App.tsx
@@ -114,7 +114,7 @@ async function sendToServiceWorker(
for (let attempt = 0; attempt <= retries; attempt++) {
if (attempt > 0) {
// Wait before retry to give service worker time to wake up
- console.log(`[AI SEO Copilot] Retrying service worker message (attempt ${attempt + 1})`);
+ console.log(`[Optia] Retrying service worker message (attempt ${attempt + 1})`);
await new Promise((r) => setTimeout(r, 500));
}
@@ -188,7 +188,7 @@ async function extractPageData(tabId: number): Promise {
if (swResponse.data) return swResponse.data;
// Fallback: direct executeScript from side panel (may hang in CRXJS dev mode)
- console.warn("[AI SEO Copilot] Service worker failed, trying direct executeScript:", swResponse.error);
+ console.warn("[Optia] Service worker failed, trying direct executeScript:", swResponse.error);
try {
return await directExecuteScript(tabId);
} catch (directErr) {
@@ -463,7 +463,7 @@ export default function App() {
setView("score");
} catch (error) {
- console.error("[AI SEO Copilot] Analysis failed:", error);
+ console.error("[Optia] Analysis failed:", error);
setError(
error instanceof Error ? error.message : "Analysis failed",
);
@@ -473,7 +473,7 @@ export default function App() {
return (
-
+
{view === "setup" &&
}
{view === "loading" &&
}
diff --git a/app/src/sidepanel/index.html b/app/src/sidepanel/index.html
index b501b1c..7829969 100644
--- a/app/src/sidepanel/index.html
+++ b/app/src/sidepanel/index.html
@@ -3,7 +3,7 @@
-
AI SEO Copilot
+
Optia
diff --git a/app/src/sidepanel/main.tsx b/app/src/sidepanel/main.tsx
index 30fb52f..6de5c2e 100644
--- a/app/src/sidepanel/main.tsx
+++ b/app/src/sidepanel/main.tsx
@@ -1,8 +1,12 @@
import React from "react";
import { createRoot } from "react-dom/client";
import App from "./App";
+import { loadTheme } from "@/lib/theme";
import "@/styles/globals.css";
+// Apply persisted theme (light default) before first paint settles.
+void loadTheme();
+
createRoot(document.getElementById("root")!).render(
diff --git a/app/src/sidepanel/pages/LoadingPage.tsx b/app/src/sidepanel/pages/LoadingPage.tsx
index 0077d83..ec36623 100644
--- a/app/src/sidepanel/pages/LoadingPage.tsx
+++ b/app/src/sidepanel/pages/LoadingPage.tsx
@@ -1,19 +1,37 @@
-import { Loader2 } from "lucide-react";
-
export function LoadingPage() {
return (
-
-
+
+ {/* Animated score-ring placeholder */}
+
+
+
+
+
+ Scoring
+
+
-
Analyzing your page
-
- Extracting SEO data and running checks...
+
Analyzing your page
+
+ Extracting SEO data and running checks…
-
-
+
+ {/* Skeleton category cards */}
+
);
diff --git a/app/src/sidepanel/pages/ScorePage.tsx b/app/src/sidepanel/pages/ScorePage.tsx
index da6459b..864a5bb 100644
--- a/app/src/sidepanel/pages/ScorePage.tsx
+++ b/app/src/sidepanel/pages/ScorePage.tsx
@@ -1,22 +1,17 @@
import { useEffect, useRef } from "react";
-import { ArrowLeft, AlertTriangle } from "lucide-react";
+import { ArrowLeft, AlertTriangle, Sparkles } from "lucide-react";
import confetti from "canvas-confetti";
import { ScoreGauge } from "@/components/ui/ScoreGauge";
import { SummaryCard } from "@/components/SummaryCard";
import { Footer } from "@/components/Footer";
+import { ThemeToggle } from "@/components/ui/ThemeToggle";
+import { OptiaWordmark } from "@/components/ui/Logo";
import { useStore } from "@/lib/store";
import type { CheckCategory } from "@/types/seo";
-// SVG Triangle icons - larger and more prominent per Figma spec
function TriangleUpIcon({ className }: { className?: string }) {
return (
-
+
);
@@ -24,13 +19,7 @@ function TriangleUpIcon({ className }: { className?: string }) {
function TriangleDownIcon({ className }: { className?: string }) {
return (
-
+
);
@@ -44,9 +33,10 @@ export function ScorePage() {
if (analysis && analysis.overallScore === 100 && !confettiFired.current) {
confettiFired.current = true;
confetti({
- particleCount: 100,
- spread: 70,
+ particleCount: 120,
+ spread: 75,
origin: { y: 0.6 },
+ colors: ["#4F46E5", "#0EA5E9", "#16A34A"],
});
}
}, [analysis]);
@@ -57,12 +47,20 @@ export function ScorePage() {
setActiveCategory(category);
};
+ const isPerfect = analysis.overallScore === 100;
+
return (
-
- {/* Back Button — directly on bg-900 */}
+
+ {/* App header */}
+
+
+ {/* Back / new analysis */}
New Analysis
@@ -71,9 +69,9 @@ export function ScorePage() {
{/* JS-rendered / fetch warning banner */}
{analysis.pageData.fetchWarnings &&
analysis.pageData.fetchWarnings.length > 0 && (
-
-
-
+
+
+
{analysis.pageData.fetchWarnings.map((w, i) => (
{w}
))}
@@ -81,33 +79,35 @@ export function ScorePage() {
)}
- {/* Score Circle Card */}
-
+ {/* Score hero */}
+
-
{analysis.scoreLabel}
-
- {analysis.scoreDescription}
-
+
{analysis.scoreLabel}
+
{analysis.scoreDescription}
- {/* Passed/To-Improve Summary Bar */}
+ {isPerfect && (
+
+
+ Perfect score — every check passed!
+
+ )}
+
+ {/* Passed / to-improve summary */}
-
-
- {analysis.totalPassed} passed
+
+
+ {analysis.totalPassed} passed
-
-
- {analysis.totalFailed} to improve
+
+
+ {analysis.totalFailed} to improve
- {/* Summary Cards */}
+ {/* Category breakdown */}
{analysis.categories.map((cat) => (
- {/* Footer — pinned to bottom */}
+ {/* Footer */}
diff --git a/app/src/sidepanel/pages/SetupPage.tsx b/app/src/sidepanel/pages/SetupPage.tsx
index 5e9ee7a..2d149a3 100644
--- a/app/src/sidepanel/pages/SetupPage.tsx
+++ b/app/src/sidepanel/pages/SetupPage.tsx
@@ -3,6 +3,8 @@ import { Input } from "@/components/ui/Input";
import { Select } from "@/components/ui/Select";
import { Toggle } from "@/components/ui/Toggle";
import { Button } from "@/components/ui/Button";
+import { ThemeToggle } from "@/components/ui/ThemeToggle";
+import { OptiaWordmark } from "@/components/ui/Logo";
import { Footer } from "@/components/Footer";
import { useStore } from "@/lib/store";
import { SUPPORTED_LANGUAGES } from "@/lib/languages";
@@ -43,16 +45,6 @@ const isDevMode =
/**
* Hook that watches an input element for programmatic value changes
* (autofill, paste tools, browser automation) and syncs back to React state.
- *
- * Programmatic fill tools often set the native `value` property directly and
- * dispatch an `input` event without clearing the field first. Because React
- * controlled inputs continuously write state back to the DOM, the dispatched
- * event's `target.value` ends up being the *old* React state concatenated
- * with the new text.
- *
- * This hook intercepts the native `value` setter so that any external write
- * is immediately forwarded to the provided `onValueChange` callback, keeping
- * React state in sync and preventing duplication.
*/
function useProgrammaticInputSync(
onValueChange: (value: string) => void,
@@ -65,7 +57,6 @@ function useProgrammaticInputSync(
const input = ref.current;
if (!input) return;
- // Get the native value setter from the HTMLInputElement prototype.
const nativeDescriptor = Object.getOwnPropertyDescriptor(
HTMLInputElement.prototype,
"value",
@@ -74,25 +65,18 @@ function useProgrammaticInputSync(
const nativeSetter = nativeDescriptor.set;
- // Replace the value setter on this specific element instance.
- // When a programmatic tool sets `input.value = "..."`, this fires
- // our callback so React state stays in sync before any subsequent
- // input events are dispatched.
Object.defineProperty(input, "value", {
configurable: true,
get() {
return nativeDescriptor.get?.call(this) ?? "";
},
set(newValue: string) {
- // Call the original native setter first to actually update the DOM.
nativeSetter.call(this, newValue);
- // Sync value back to React state.
onValueChangeRef.current(newValue);
},
});
return () => {
- // Restore original behaviour by removing the instance override.
delete (input as unknown as Record).value;
};
}, []);
@@ -154,178 +138,171 @@ export function SetupPage({ onAnalyze }: SetupPageProps) {
const secondaryKeywordsLength = settings.secondaryKeywords.length;
return (
-
- {/* Main card */}
-
-
- {/* Title with settings gear */}
-
-
- Set up your SEO analysis
-
+
+ {/* App header */}
+
+
+ {/* Inline settings panel */}
+ {showSettings && (
+
+
+ Settings
setShowSettings(!showSettings)}
- className="absolute right-0 rounded-full p-1.5 text-text-secondary transition-colors hover:bg-bg-500 hover:text-text-primary"
- aria-label="Settings"
+ onClick={() => setShowSettings(false)}
+ className="rounded-full p-1 text-muted transition-colors hover:bg-surface-2 hover:text-ink"
+ aria-label="Close settings"
>
-
+
- {/* Inline settings panel */}
- {showSettings && (
-
-
- Settings
- setShowSettings(false)}
- className="rounded-full p-1 text-text-secondary transition-colors hover:bg-bg-700 hover:text-text-primary"
- aria-label="Close settings"
- >
-
-
-
+
+
+ OpenAI API key
+
+ setLocalApiKey(e.target.value)}
+ className="w-full rounded-input border border-border bg-surface px-3.5 py-2.5 text-body text-ink placeholder:text-faint outline-none transition focus:border-brand focus:ring-2 focus:ring-brand/30"
+ />
+
-
-
- OpenAI API key
-
- setLocalApiKey(e.target.value)}
- className="w-full rounded-[10px] border border-[#717171] bg-bg-700 p-[10px] text-[16px] text-text-primary placeholder:text-text-secondary outline-none focus:ring-1 focus:ring-accent-blue transition-shadow"
- />
-
+
setSettings({ language: e.target.value })}
+ />
- setSettings({ language: e.target.value })}
- />
+
+ Save
+
-
- Save
-
-
- {settingsSaved && (
- Settings saved.
- )}
-
- )}
-
- {/* Dev mode URL field */}
- {isDevMode && (
-
setSettings({ targetUrl: e.target.value })}
- onBlur={handleUrlBlur}
- />
- )}
+ {settingsSaved &&
Settings saved.
}
+
+ )}
+
+ {/* Main card */}
+
+
Set up your SEO analysis
- {/* Main keyword */}
+ {/* Dev mode URL field */}
+ {isDevMode && (
setSettings({ keyword: e.target.value })}
+ ref={urlRef}
+ label="Page URL to analyze"
+ type="url"
+ placeholder="https://example.com"
+ value={settings.targetUrl}
+ onChange={(e) => setSettings({ targetUrl: e.target.value })}
+ onBlur={handleUrlBlur}
/>
+ )}
- {/* Divider */}
-
-
- {/* Advanced Analysis section */}
-
-
-
- Advanced Analysis
-
- optional
-
-
-
- Get smarter, page-specific recommendations based on your page context.
-
-
setSettings({ advancedMode: checked })}
- />
-
+ {/* Main keyword */}
+
setSettings({ keyword: e.target.value })}
+ />
+
+ {/* Divider */}
+
+
+ {/* Advanced Analysis section */}
+
+
+ Advanced Analysis
+
+ optional
+
+
+
+ Get smarter, page-specific recommendations based on your page context.
+
+
setSettings({ advancedMode: checked })}
+ />
+
+
- {/* Advanced fields */}
- {settings.advancedMode && (
- <>
-
setSettings({ pageType: e.target.value })}
- />
+ {/* Advanced fields */}
+ {settings.advancedMode && (
+ <>
+ setSettings({ pageType: e.target.value })}
+ />
-
-
-
- Secondary keywords
-
-
- ({secondaryKeywordsLength}/2000 characters)
-
-
-
+ >
+ )}
{/* Error message */}
{error && (
-
+
{error}
)}
- {/* Button — centered pill, not full-width */}
+ {/* Primary action */}
-
+
Optimize my SEO
- {/* Footer — pinned to bottom */}
+ {/* Footer */}
diff --git a/app/src/sidepanel/pages/SubscoresPage.tsx b/app/src/sidepanel/pages/SubscoresPage.tsx
index a9236e7..f25b311 100644
--- a/app/src/sidepanel/pages/SubscoresPage.tsx
+++ b/app/src/sidepanel/pages/SubscoresPage.tsx
@@ -20,7 +20,7 @@ import type { SEOCheck } from "@/types/seo";
// Triangle icons for summary pill
function TriangleUpIcon({ className }: { className?: string }) {
return (
-
+
);
@@ -28,7 +28,7 @@ function TriangleUpIcon({ className }: { className?: string }) {
function TriangleDownIcon({ className }: { className?: string }) {
return (
-
+
);
@@ -76,8 +76,6 @@ export function SubscoresPage() {
}
: undefined;
- // Stub that rejects immediately when no API key is configured.
- // The buttons will be disabled, but this provides a safe fallback.
const rejectNoKey = () => Promise.reject(new Error("No API key configured"));
const renderCheckRecommendation = (check: SEOCheck) => {
@@ -189,32 +187,31 @@ export function SubscoresPage() {
};
return (
-
+
{/* Main card */}
-
+
{/* Header: back arrow + centered title */}
setActiveCategory(null)}
- className="absolute left-0 text-text-secondary hover:text-white transition-colors"
+ aria-label="Back"
+ className="absolute left-0 inline-flex h-9 w-9 items-center justify-center rounded-full text-muted transition-colors hover:bg-surface-2 hover:text-ink"
>
-
+
-
- {category.label}
-
+
{category.label}
{/* Summary pill */}
-
-
- {category.passed} passed
+
+
+ {category.passed} passed
-
-
- {failed} to improve
+
+
+ {failed} to improve
diff --git a/app/src/styles/globals.css b/app/src/styles/globals.css
index aed642b..450a992 100644
--- a/app/src/styles/globals.css
+++ b/app/src/styles/globals.css
@@ -2,125 +2,207 @@
@tailwind components;
@tailwind utilities;
+/* ============================================================
+ Optia — "Signal" theme tokens (RGB triplets for Tailwind alpha)
+ Light is the default; .dark on flips the palette.
+ ============================================================ */
:root {
- --bg-900: #1a1a1a;
- --bg-700: #323232;
- --bg-500: #444444;
- --bg-300: #787878;
- --text-primary: #ffffff;
- --text-secondary: #c7c7c7;
- --red: #ff4343;
- --red-light: #ff8484;
- --green: #a2ffb4;
- --yellow: #ffdd64;
- --yellow-light: #ffea9e;
- --accent-blue: #1a72f5;
+ --canvas: 247 248 250;
+ --surface: 255 255 255;
+ --surface-2: 241 245 249;
+ --surface-3: 233 238 245;
+ --border: 226 232 240;
+ --border-strong: 203 213 225;
+ --ink: 15 23 42;
+ --muted: 100 116 139;
+ --faint: 148 163 184;
+
+ --brand: 79 70 229; /* indigo 600 */
+ --brand-hover: 67 56 202; /* indigo 700 */
+ --brand-fg: 255 255 255;
+ --brand-tint: 238 242 255; /* indigo 50 */
+
+ --accent: 14 165 233; /* sky 500 */
+ --accent-tint: 224 242 254;
+
+ --good: 22 163 74;
+ --good-tint: 220 252 231;
+ --warn: 217 119 6;
+ --warn-tint: 254 243 199;
+ --poor: 220 38 38;
+ --poor-tint: 254 226 226;
+
+ color-scheme: light;
+}
+
+.dark {
+ --canvas: 11 17 32; /* slate 950 */
+ --surface: 17 24 39; /* slate 900 */
+ --surface-2: 30 41 59; /* slate 800 */
+ --surface-3: 39 52 73;
+ --border: 36 48 68;
+ --border-strong: 51 65 85;
+ --ink: 241 245 249;
+ --muted: 148 163 184;
+ --faint: 100 116 139;
+
+ --brand: 99 102 241; /* indigo 500 */
+ --brand-hover: 129 140 248;/* indigo 400 */
+ --brand-fg: 255 255 255;
+ --brand-tint: 30 27 75; /* indigo 950 */
+
+ --accent: 56 189 248; /* sky 400 */
+ --accent-tint: 12 74 110;
+
+ --good: 52 211 153;
+ --good-tint: 6 78 59;
+ --warn: 251 191 36;
+ --warn-tint: 120 53 15;
+ --poor: 248 113 113;
+ --poor-tint: 127 29 29;
+
+ color-scheme: dark;
}
@layer base {
+ html {
+ background-color: rgb(var(--canvas));
+ }
+
body {
- @apply bg-bg-900 text-text-primary font-sans antialiased;
+ @apply bg-canvas text-ink font-sans antialiased;
margin: 0;
- min-width: 380px;
+ min-width: 360px;
}
* {
box-sizing: border-box;
}
-}
-@layer components {
- /* Stroke colors from Figma */
- :root {
- --stroke-bg-700: #3e3e3e;
- --stroke-bg-300: #787878;
+ ::selection {
+ background-color: rgb(var(--brand) / 0.18);
}
- /* Score frame outer container */
+ /* Accessible, brand-tinted focus ring on keyboard nav */
+ :focus-visible {
+ outline: none;
+ box-shadow: 0 0 0 3px rgb(var(--brand) / 0.35);
+ border-radius: 6px;
+ }
+
+ /* Subtle, themed scrollbars inside the side panel */
+ * {
+ scrollbar-width: thin;
+ scrollbar-color: rgb(var(--border-strong)) transparent;
+ }
+ *::-webkit-scrollbar {
+ width: 10px;
+ height: 10px;
+ }
+ *::-webkit-scrollbar-thumb {
+ background-color: rgb(var(--border-strong));
+ border-radius: 999px;
+ border: 3px solid transparent;
+ background-clip: content-box;
+ }
+}
+
+@layer components {
+ /* Outer score frame container */
.score-frame {
- @apply rounded-card-lg border border-bg-500 bg-bg-900;
+ @apply rounded-card-lg border border-border bg-surface shadow-card;
max-width: 715px;
}
- /* Score circle area card - 2px border, symmetric 32px 20px padding */
+ /* Hero card holding the score gauge */
.score-circle-card {
- @apply rounded-score bg-bg-700;
- border: 2px solid var(--stroke-bg-700);
- padding: 32px 20px;
+ @apply relative overflow-hidden rounded-score border border-border bg-surface shadow-pop;
+ padding: 28px 20px 24px;
+ }
+ /* Soft brand glow behind the gauge */
+ .score-circle-card::before {
+ content: "";
+ position: absolute;
+ inset: -40% 0 auto 0;
+ height: 220px;
+ background: radial-gradient(
+ 60% 80% at 50% 0%,
+ rgb(var(--brand) / 0.14),
+ transparent 70%
+ );
+ pointer-events: none;
}
- /* Summarybox card - 2px border */
+ /* Generic content card */
.summarybox-card {
- @apply rounded-card-lg bg-bg-700;
- border: 2px solid var(--stroke-bg-700);
- padding: 20px 24px 23px 24px;
+ @apply rounded-card-lg border border-border bg-surface shadow-card;
+ padding: 18px 20px 20px;
}
- /* Passed/To-Improve summary pill container */
+ /* Passed / to-improve summary bar */
.summary-pill {
- @apply flex items-center justify-between;
- background: #444444;
- border: 1px solid #444444;
- border-radius: 41px;
- padding: 12px 31px;
- min-width: 300px;
+ @apply flex items-center justify-between rounded-pill border border-border bg-surface-2;
+ padding: 10px 22px;
+ min-width: 280px;
}
- /* Status badge pill - Figma spec: 16px/400, letter-spacing -0.896px, line-height 125% */
+ /* Status badge pills — tinted bg + matching text (modern, high-contrast) */
.status-badge {
- @apply inline-flex items-center gap-1;
- font-size: 16px;
- font-weight: 400;
- letter-spacing: -0.896px;
- line-height: 125%;
- padding: 8px 12px;
- border-radius: 27px;
- border: 1px solid var(--stroke-bg-300);
- color: #000000;
+ @apply inline-flex items-center gap-1.5 rounded-pill font-semibold;
+ font-size: 13px;
+ line-height: 1;
+ letter-spacing: -0.01em;
+ padding: 7px 12px;
+ border: 1px solid transparent;
}
-
.status-badge-success {
- @apply status-badge;
- background-color: #A2FFB4;
- box-shadow: 0 2px 6.6px 0 rgba(162, 255, 180, 0.26);
+ @apply status-badge bg-good-tint text-good;
+ border-color: rgb(var(--good) / 0.25);
}
-
.status-badge-warning {
- @apply status-badge;
- background-color: #FFDD64;
- box-shadow: 0 2px 6.6px 0 rgba(255, 221, 100, 0.26);
+ @apply status-badge bg-warn-tint text-warn;
+ border-color: rgb(var(--warn) / 0.3);
}
-
.status-badge-error {
- @apply status-badge;
- background-color: #FF4343;
- box-shadow: 0 2px 6.6px 0 rgba(255, 67, 67, 0.26);
+ @apply status-badge bg-poor-tint text-poor;
+ border-color: rgb(var(--poor) / 0.25);
}
- /* Arrow icon circle - matches badge color */
+ /* Circular arrow/icon chip — matches badge tint */
.arrow-circle {
- @apply flex items-center justify-center;
- width: 35px;
- height: 35px;
- border-radius: 27px;
- border: 1px solid var(--stroke-bg-300);
+ @apply flex items-center justify-center rounded-full;
+ width: 34px;
+ height: 34px;
+ border: 1px solid transparent;
}
-
.arrow-circle-success {
- @apply arrow-circle;
- background-color: #A2FFB4;
- box-shadow: 0 2px 6.6px 0 rgba(162, 255, 180, 0.26);
+ @apply arrow-circle bg-good-tint text-good;
+ border-color: rgb(var(--good) / 0.25);
}
-
.arrow-circle-warning {
- @apply arrow-circle;
- background-color: #FFDD64;
- box-shadow: 0 2px 6.6px 0 rgba(255, 221, 100, 0.26);
+ @apply arrow-circle bg-warn-tint text-warn;
+ border-color: rgb(var(--warn) / 0.3);
}
-
.arrow-circle-error {
- @apply arrow-circle;
- background-color: #FF4343;
- box-shadow: 0 2px 6.6px 0 rgba(255, 67, 67, 0.26);
+ @apply arrow-circle bg-poor-tint text-poor;
+ border-color: rgb(var(--poor) / 0.25);
+ }
+
+ /* Loading skeleton shimmer */
+ .skeleton {
+ @apply relative overflow-hidden rounded-card bg-surface-2;
+ }
+ .skeleton::after {
+ content: "";
+ position: absolute;
+ inset: 0;
+ transform: translateX(-100%);
+ background: linear-gradient(
+ 90deg,
+ transparent,
+ rgb(var(--ink) / 0.06),
+ transparent
+ );
+ animation: shimmer 1.4s infinite;
}
}
diff --git a/app/tailwind.config.ts b/app/tailwind.config.ts
index 7c33600..510817d 100644
--- a/app/tailwind.config.ts
+++ b/app/tailwind.config.ts
@@ -1,49 +1,118 @@
import type { Config } from "tailwindcss";
+/**
+ * Optia design system — "Signal" theme.
+ * Colors are driven by CSS variables (RGB triplets) defined in src/styles/globals.css,
+ * so every token is theme-aware (light default, .dark variant) and supports Tailwind
+ * opacity modifiers (e.g. bg-brand/20).
+ */
+const v = (name: string) => `rgb(var(${name}) /
)`;
+
export default {
+ darkMode: "class",
content: ["./src/**/*.{ts,tsx,html}"],
theme: {
extend: {
colors: {
- bg: {
- 900: "#1A1A1A",
- 700: "#323232",
- 500: "#444444",
- 300: "#787878",
+ // --- Signal semantic tokens ---
+ canvas: v("--canvas"),
+ surface: {
+ DEFAULT: v("--surface"),
+ 2: v("--surface-2"),
+ 3: v("--surface-3"),
},
- text: {
- primary: "#FFFFFF",
- secondary: "#C7C7C7",
+ border: {
+ DEFAULT: v("--border"),
+ strong: v("--border-strong"),
},
- red: {
- DEFAULT: "#FF4343",
- light: "#FF8484",
+ ink: v("--ink"),
+ muted: v("--muted"),
+ faint: v("--faint"),
+ brand: {
+ DEFAULT: v("--brand"),
+ hover: v("--brand-hover"),
+ fg: v("--brand-fg"),
+ tint: v("--brand-tint"),
},
- green: {
- DEFAULT: "#A2FFB4",
+ accent: {
+ DEFAULT: v("--accent"),
+ tint: v("--accent-tint"),
+ // legacy alias used across the codebase
+ blue: v("--brand"),
},
- yellow: {
- DEFAULT: "#FFDD64",
- light: "#FFEA9E",
+ good: { DEFAULT: v("--good"), tint: v("--good-tint") },
+ warn: { DEFAULT: v("--warn"), tint: v("--warn-tint") },
+ poor: { DEFAULT: v("--poor"), tint: v("--poor-tint") },
+
+ // --- Backward-compatible aliases (old token names -> new themed values) ---
+ bg: {
+ 900: v("--canvas"),
+ 700: v("--surface"),
+ 500: v("--surface-2"),
+ 300: v("--border-strong"),
},
- accent: {
- blue: "#1A72F5",
+ text: {
+ primary: v("--ink"),
+ secondary: v("--muted"),
},
+ green: { DEFAULT: v("--good") },
+ red: { DEFAULT: v("--poor"), light: v("--poor") },
+ yellow: { DEFAULT: v("--warn"), light: v("--warn-tint") },
+ },
+ fontFamily: {
+ sans: [
+ "Inter",
+ "ui-sans-serif",
+ "system-ui",
+ "-apple-system",
+ "Segoe UI",
+ "Roboto",
+ "Helvetica Neue",
+ "Arial",
+ "sans-serif",
+ ],
},
fontSize: {
- h1: ["28px", { lineHeight: "110%", fontWeight: "500" }],
- h2: ["20px", { lineHeight: "120%", fontWeight: "600" }],
- body: ["18px", { lineHeight: "130%", fontWeight: "400" }],
- "body-semibold": ["18px", { lineHeight: "130%", fontWeight: "600" }],
- "body-16": ["16px", { lineHeight: "130%", fontWeight: "400" }],
- "body-12": ["12px", { lineHeight: "130%", fontWeight: "400" }],
- button: ["16px", { lineHeight: "20px", fontWeight: "500" }],
+ display: ["40px", { lineHeight: "104%", fontWeight: "700", letterSpacing: "-0.02em" }],
+ h1: ["24px", { lineHeight: "112%", fontWeight: "600", letterSpacing: "-0.015em" }],
+ h2: ["18px", { lineHeight: "120%", fontWeight: "600", letterSpacing: "-0.01em" }],
+ body: ["15px", { lineHeight: "150%", fontWeight: "400" }],
+ "body-semibold": ["15px", { lineHeight: "150%", fontWeight: "600" }],
+ "body-16": ["15px", { lineHeight: "150%", fontWeight: "400" }],
+ "body-12": ["12px", { lineHeight: "140%", fontWeight: "400" }],
+ button: ["14px", { lineHeight: "20px", fontWeight: "600" }],
+ label: ["11px", { lineHeight: "120%", fontWeight: "600", letterSpacing: "0.06em" }],
+ caption: ["12px", { lineHeight: "140%", fontWeight: "500" }],
},
borderRadius: {
- card: "12px",
- "card-lg": "14px",
- input: "8px",
- score: "20px",
+ input: "10px",
+ card: "14px",
+ "card-lg": "18px",
+ score: "24px",
+ pill: "999px",
+ },
+ boxShadow: {
+ card: "0 1px 2px rgb(15 23 42 / 0.04), 0 1px 3px rgb(15 23 42 / 0.06)",
+ pop: "0 4px 12px rgb(15 23 42 / 0.08), 0 2px 4px rgb(15 23 42 / 0.04)",
+ brand: "0 6px 18px -4px rgb(var(--brand) / 0.45)",
+ "focus-ring": "0 0 0 3px rgb(var(--brand) / 0.35)",
+ },
+ keyframes: {
+ "fade-in": {
+ from: { opacity: "0", transform: "translateY(4px)" },
+ to: { opacity: "1", transform: "translateY(0)" },
+ },
+ "scale-in": {
+ from: { opacity: "0", transform: "scale(0.96)" },
+ to: { opacity: "1", transform: "scale(1)" },
+ },
+ shimmer: {
+ "100%": { transform: "translateX(100%)" },
+ },
+ },
+ animation: {
+ "fade-in": "fade-in 0.25s ease-out both",
+ "scale-in": "scale-in 0.22s ease-out both",
},
},
},
diff --git a/docs/chrome-web-store-listing.md b/docs/chrome-web-store-listing.md
index d303f5f..51412ff 100644
--- a/docs/chrome-web-store-listing.md
+++ b/docs/chrome-web-store-listing.md
@@ -2,7 +2,7 @@
## Extension Name
-AI SEO Copilot
+Optia
## Short Description
@@ -14,7 +14,7 @@ Productivity
## Detailed Description
-AI SEO Copilot analyzes any web page for SEO issues and gives you a clear score with actionable recommendations — powered by AI.
+Optia analyzes any web page for SEO issues and gives you a clear score with actionable recommendations — powered by AI.
Enter your target keyword, click "Optimize my SEO," and instantly see what's working and what needs improvement across your titles, meta descriptions, headings, images, links, structured data, and more. Each check is labeled by priority (High, Medium) so you know exactly where to focus first.
@@ -46,7 +46,7 @@ Full privacy policy: https://github.com/die-Manufaktur/AISEOC-Chrome-Extension/b
**FREE & OPEN SOURCE**
-AI SEO Copilot is completely free to use and open source. View the source code, report issues, or contribute on GitHub:
+Optia is completely free to use and open source. View the source code, report issues, or contribute on GitHub:
https://github.com/die-Manufaktur/AISEOC-Chrome-Extension
## Privacy Policy URL
diff --git a/docs/launch-social-posts.md b/docs/launch-social-posts.md
index 3e1bec6..5079b34 100644
--- a/docs/launch-social-posts.md
+++ b/docs/launch-social-posts.md
@@ -4,7 +4,7 @@
### Post 1 — Announcement
-I just shipped my first Chrome extension: AI SEO Copilot.
+I just shipped my first Chrome extension: Optia.
It analyzes any web page for SEO issues and gives you a score from 0–100 with actionable recommendations. Enter your target keyword, and it shows you exactly what's working and what needs fixing across titles, meta descriptions, headings, images, links, structured data, and more.
@@ -20,7 +20,7 @@ It's free, open source, and privacy-first — all analysis happens locally in yo
### Post 2 — Behind the Build
-Shipped something new today: AI SEO Copilot, a free Chrome extension for on-page SEO analysis.
+Shipped something new today: Optia, a free Chrome extension for on-page SEO analysis.
What it does:
• Instant SEO score (0–100) with priority-labeled issues
@@ -43,7 +43,7 @@ If you work with SEO or just want to improve your site's search visibility, give
### r/SEO
-**Title:** I built a free, open-source Chrome extension for on-page SEO analysis — AI SEO Copilot
+**Title:** I built a free, open-source Chrome extension for on-page SEO analysis — Optia
**Body:**
@@ -86,7 +86,7 @@ Would love feedback from this community — what checks would you want to see ad
Hey Webflow community,
-I built a Chrome extension called AI SEO Copilot that I've been using to optimize my Webflow projects. Thought I'd share it here since it's free and might be useful for others.
+I built a Chrome extension called Optia that I've been using to optimize my Webflow projects. Thought I'd share it here since it's free and might be useful for others.
**What it does:**
- Analyzes any page for SEO issues and gives you a 0–100 score
@@ -111,11 +111,11 @@ It's open source if anyone wants to check out the code or contribute: https://gi
### r/SideProject
-**Title:** I shipped my first Chrome extension — AI SEO Copilot (free, open source)
+**Title:** I shipped my first Chrome extension — Optia (free, open source)
**Body:**
-Just launched AI SEO Copilot on the Chrome Web Store.
+Just launched Optia on the Chrome Web Store.
**The problem:** I was tired of using multiple SEO tools that either required paid accounts or sent my data to their servers just to get basic on-page recommendations.
@@ -146,7 +146,7 @@ Would love feedback — what features would make this more useful for you?
## Twitter/X
-🚀 Just shipped AI SEO Copilot — a free Chrome extension for on-page SEO analysis.
+🚀 Just shipped Optia — a free Chrome extension for on-page SEO analysis.
Enter your keyword, get a 0–100 score, and see exactly what to fix across titles, meta, headings, images, and structured data.
@@ -158,7 +158,7 @@ AI-powered suggestions (bring your own OpenAI key). Privacy-first. Open source.
## Webflow Community
-**Title:** Free Chrome Extension for SEO Analysis — AI SEO Copilot
+**Title:** Free Chrome Extension for SEO Analysis — Optia
**Body:**
@@ -166,7 +166,7 @@ Hey everyone,
I built a Chrome extension that I've been using alongside my Webflow projects and wanted to share it with the community.
-**AI SEO Copilot** analyzes any web page for SEO issues and gives you a clear score with actionable recommendations.
+**Optia** analyzes any web page for SEO issues and gives you a clear score with actionable recommendations.
**How it works:**
1. Open any page (including your published Webflow site)
diff --git a/package.json b/package.json
index 3474657..30b4a1e 100644
--- a/package.json
+++ b/package.json
@@ -1,5 +1,5 @@
{
- "name": "ai-seo-copilot-extension",
+ "name": "optia-extension",
"version": "0.1.0",
"description": "Chrome extension for SEO analysis with AI-powered recommendations",
"private": true,