diff --git a/packages/extension/public/SelectionCommandLogo_dark.png b/packages/extension/public/SelectionCommandLogo_dark.png new file mode 100644 index 00000000..971b15b1 Binary files /dev/null and b/packages/extension/public/SelectionCommandLogo_dark.png differ diff --git a/packages/extension/src/components/global.css b/packages/extension/src/components/global.css index a483a216..31fe6419 100644 --- a/packages/extension/src/components/global.css +++ b/packages/extension/src/components/global.css @@ -18,6 +18,7 @@ &:where([data-sc-theme="dark"], [data-sc-theme="dark"] *) { @slot; } + :host([data-sc-theme="dark"]) & { @slot; } @@ -78,164 +79,193 @@ --animate-marquee2: marquee2 var(--marquee-duration) linear infinite; --animate-spin-slow: spin 3s linear infinite; --animate-popup: popup 0.3s cubic-bezier(0.34, 1.56, 0.64, 1) both; - --animate-onboarding-rise: onboarding-rise 0.7s cubic-bezier(0.16, 1, 0.3, 1) - both; - --animate-onboarding-blur-in: onboarding-blur-in 0.8s - cubic-bezier(0.16, 1, 0.3, 1) both; - --animate-onboarding-blur-out: onboarding-blur-out 0.32s - cubic-bezier(0.4, 0, 1, 1) both; - --animate-onboarding-glow-in: onboarding-glow-in 1.6s - cubic-bezier(0.16, 1, 0.3, 1) both; + --animate-onboarding-rise: onboarding-rise 0.7s cubic-bezier(0.16, 1, 0.3, 1) both; + --animate-onboarding-blur-in: onboarding-blur-in 0.8s cubic-bezier(0.16, 1, 0.3, 1) both; + --animate-onboarding-blur-out: onboarding-blur-out 0.32s cubic-bezier(0.4, 0, 1, 1) both; + --animate-onboarding-glow-in: onboarding-glow-in 1.6s cubic-bezier(0.16, 1, 0.3, 1) both; --animate-onboarding-ring: onboarding-ring 2.4s ease-in-out 0.6s infinite; --animate-onboarding-blink: onboarding-blink 1.5s ease-in-out 0.3s infinite; --animate-onboarding-highlight: onboarding-highlight 4s ease-in-out infinite; - --animate-onboarding-pop: onboarding-pop 0.36s - cubic-bezier(0.34, 1.4, 0.64, 1) both; - --animate-onboarding-pop-2: onboarding-pop-2 0.36s - cubic-bezier(0.34, 1.4, 0.64, 1) both; + --animate-onboarding-pop: onboarding-pop 0.36s cubic-bezier(0.34, 1.4, 0.64, 1) both; + --animate-onboarding-pop-2: onboarding-pop-2 0.36s cubic-bezier(0.34, 1.4, 0.64, 1) both; --animate-onboarding-float: onboarding-float 1.4s ease-in-out infinite; @keyframes accordion-down { from { height: 0; } + to { height: var(--radix-accordion-content-height); } } + @keyframes accordion-up { from { height: var(--radix-accordion-content-height); } + to { height: 0; } } + @keyframes marquee { 0% { transform: translateX(0%); } + 100% { transform: translateX(-100%); } } + @keyframes marquee2 { 0% { transform: translateX(100%); } + 100% { transform: translateX(0%); } } + @keyframes popup { 0% { opacity: 0; transform: scale(0.5) translate(-10px, 10px); } + 100% { opacity: 1; transform: scale(1); } } + @keyframes onboarding-rise { from { opacity: 0; transform: translateY(10px); } + to { opacity: 1; transform: translateY(0); } } + @keyframes onboarding-blur-in { from { opacity: 0; filter: blur(8px); transform: translateY(6px); } + to { opacity: 1; filter: blur(0px); transform: translateY(0); } } + @keyframes onboarding-blur-out { from { opacity: 1; filter: blur(0px); } + to { opacity: 0; filter: blur(8px); } } + @keyframes onboarding-glow-in { from { opacity: 0; transform: scale(0.6); } + to { opacity: 1; transform: scale(1); } } + @keyframes onboarding-ring { + 0%, 100% { box-shadow: 0 0 0 0 var(--onboarding-ring-color); } + 50% { box-shadow: 0 0 0 6px var(--onboarding-ring-color); } } + @keyframes onboarding-blink { + 0%, 100% { opacity: 1; } + 50% { opacity: 0.45; } } + + /* --onboarding-highlight-color lets dark mode use a darker highlight. */ @keyframes onboarding-highlight { + 0%, 10% { - background-color: rgb(191 219 254 / 0); + background-color: transparent; } + 44%, 70% { - background-color: rgb(191 219 254 / 1); + background-color: var(--onboarding-highlight-color, rgb(191 219 254)); } + 82%, 100% { - background-color: rgb(191 219 254 / 0); + background-color: transparent; } } + @keyframes onboarding-pop { 0% { opacity: 0; transform: scale(0.72); } + 100% { opacity: 1; transform: scale(1); } } + @keyframes onboarding-pop-2 { 0% { opacity: 0; transform: scale(0.72) translate(-10px, 10px); } + 100% { opacity: 1; transform: scale(1); } } + @keyframes onboarding-float { + 0%, 100% { translate: 0 0; } + 50% { translate: 0 -4px; } @@ -307,6 +337,7 @@ /* Restore v3 defaults that the v4 preflight changed. */ @layer base { + input::placeholder, textarea::placeholder { color: var(--color-gray-400); @@ -333,6 +364,7 @@ } @layer base { + :host, :root { --background: 0 0% 100%; diff --git a/packages/extension/src/components/onboarding/OnboardingLayout.tsx b/packages/extension/src/components/onboarding/OnboardingLayout.tsx index eefe715a..903139a9 100644 --- a/packages/extension/src/components/onboarding/OnboardingLayout.tsx +++ b/packages/extension/src/components/onboarding/OnboardingLayout.tsx @@ -4,8 +4,7 @@ import { getProgress, showsSkip } from "./onboardingProgress" import { OnboardingGlow } from "./OnboardingGlow" import type { OnboardingBackground } from "./onboardingVariantUi" import type { OnboardingStep, StepPhase } from "@/types/onboarding" - -const ICON_URL = chrome.runtime.getURL("SelectionCommandLogo.png") +import { useLogoUrl } from "./onboardingLogo" type Props = { step: OnboardingStep @@ -29,10 +28,11 @@ export function OnboardingLayout({ onSkip, children, }: Props) { + const logoUrl = useLogoUrl() const progress = getProgress(step, phase) return ( -
+
{background === "glow" ? ( // Fixed to the viewport, like the welcome overlay, so its center // lines up exactly with the overlay's glow during the handoff. @@ -51,7 +51,7 @@ export function OnboardingLayout({
{progress && ( Logo of selection command ))}
- + {progress.current} / {progress.total}
@@ -86,7 +86,7 @@ export function OnboardingLayout({ diff --git a/packages/extension/src/components/onboarding/OnboardingPage.tsx b/packages/extension/src/components/onboarding/OnboardingPage.tsx index a6843cf6..c123341f 100644 --- a/packages/extension/src/components/onboarding/OnboardingPage.tsx +++ b/packages/extension/src/components/onboarding/OnboardingPage.tsx @@ -5,6 +5,7 @@ import { Popup } from "@/components/Popup" import { LinkSelector } from "@/components/LinkSelector" import { useOnboardingState } from "./useOnboardingState" import { useOnboardingVariant } from "./useOnboardingVariant" +import { useTheme, themeContext } from "@/hooks/useTheme" import { OnboardingLayout } from "./OnboardingLayout" import { OnboardingWelcome } from "./OnboardingWelcome" import { StepIntro } from "./steps/StepIntro" @@ -20,15 +21,21 @@ import "@/components/App.css" export function OnboardingPage() { const variant = useOnboardingVariant() + const theme = useTheme(document.documentElement) - // Hold a blank canvas until the A/B assignment is known (normally a single - // chrome.storage.local read, since the service worker assigns on - // install). Rendering a default arm first would flash variant A's INTRO - // screen at variant C users. - if (variant == null) { - return
- } - return + return ( + + {variant == null ? ( + // Hold a blank canvas until the A/B assignment is known (normally a + // single chrome.storage.local read, since the service worker assigns + // on install). Rendering a default arm first would flash variant A's + // INTRO screen at variant C users. +
+ ) : ( + + )} + + ) } function OnboardingFlow({ variant }: { variant: ExperimentVariant }) { diff --git a/packages/extension/src/components/onboarding/OnboardingRail.tsx b/packages/extension/src/components/onboarding/OnboardingRail.tsx index f87bda83..581c722c 100644 --- a/packages/extension/src/components/onboarding/OnboardingRail.tsx +++ b/packages/extension/src/components/onboarding/OnboardingRail.tsx @@ -60,7 +60,9 @@ export function OnboardingRail({ {state === "done" && ( @@ -93,12 +95,16 @@ export function OnboardingRail({ /> )} {state === "current" && ( - - + + )} - + {label}
diff --git a/packages/extension/src/components/onboarding/OnboardingTargetText.tsx b/packages/extension/src/components/onboarding/OnboardingTargetText.tsx index 5ca3c32c..f1f50c6b 100644 --- a/packages/extension/src/components/onboarding/OnboardingTargetText.tsx +++ b/packages/extension/src/components/onboarding/OnboardingTargetText.tsx @@ -53,12 +53,12 @@ export function OnboardingTargetText({
{text} @@ -78,7 +79,7 @@ export function OnboardingTargetText({ diff --git a/packages/extension/src/components/onboarding/OnboardingWelcome.tsx b/packages/extension/src/components/onboarding/OnboardingWelcome.tsx index ad75db20..8fbe5a2e 100644 --- a/packages/extension/src/components/onboarding/OnboardingWelcome.tsx +++ b/packages/extension/src/components/onboarding/OnboardingWelcome.tsx @@ -7,8 +7,7 @@ import { DEFAULT_WELCOME_UI, type OnboardingWelcomeUi, } from "./onboardingVariantUi" - -const ICON_URL = chrome.runtime.getURL("SelectionCommandLogo.png") +import { useLogoUrl } from "./onboardingLogo" /** How long variant C's overlay stays up before advancing on its own. */ export const WELCOME_DURATION_MS = DEFAULT_WELCOME_UI.durationMs @@ -30,6 +29,7 @@ type Props = { // when the user clicks - there is nothing to read here, so making people // wait out the timer would just be a second thing to sit through. export function OnboardingWelcome({ onDone, ui = DEFAULT_WELCOME_UI }: Props) { + const logoUrl = useLogoUrl() const [exiting, setExiting] = useState(false) const timersRef = useRef[]>([]) const finishedRef = useRef(false) @@ -69,7 +69,7 @@ export function OnboardingWelcome({ onDone, ui = DEFAULT_WELCOME_UI }: Props) { aria-label={t("onboarding_welcomeContinue")} data-testid="onboarding-welcome" className={clsx( - "fixed inset-0 z-30 flex cursor-default flex-col items-center justify-center overflow-hidden bg-white", + "outline-none fixed inset-0 z-30 flex cursor-default flex-col items-center justify-center overflow-hidden bg-background", ui.gapClassName, exiting && "animate-onboarding-blur-out motion-reduce:animate-none", )} @@ -87,15 +87,15 @@ export function OnboardingWelcome({ onDone, ui = DEFAULT_WELCOME_UI }: Props) { delay={ui.logoDelay} className={ui.logoClassName} > - + -

+

{t("onboarding_welcomeMessage")}

-

+

{t("onboarding_welcomeIntroduction")}

diff --git a/packages/extension/src/components/onboarding/__tests__/onboardingLogo.test.tsx b/packages/extension/src/components/onboarding/__tests__/onboardingLogo.test.tsx new file mode 100644 index 00000000..1f47bf56 --- /dev/null +++ b/packages/extension/src/components/onboarding/__tests__/onboardingLogo.test.tsx @@ -0,0 +1,30 @@ +import { describe, it, expect, vi } from "vitest" +import { renderHook } from "@testing-library/react" +import { themeContext, type ResolvedTheme } from "@/hooks/useTheme" +import { useLogoUrl } from "../onboardingLogo" + +// The logo URLs are resolved when the module loads, before any test runs. +vi.hoisted(() => { + vi.mocked(chrome.runtime.getURL).mockImplementation( + (path: string) => `chrome-extension://test/${path}`, + ) +}) + +const renderWithTheme = (theme: ResolvedTheme) => + renderHook(() => useLogoUrl(), { + wrapper: ({ children }) => ( + {children} + ), + }) + +describe("useLogoUrl", () => { + it("OL-01: 正常系: ライトテーマでは通常のロゴを返す", () => { + const { result } = renderWithTheme("light") + expect(result.current).toContain("SelectionCommandLogo.png") + }) + + it("OL-02: 正常系: ダークテーマではダーク用のロゴを返す", () => { + const { result } = renderWithTheme("dark") + expect(result.current).toContain("SelectionCommandLogo_dark.png") + }) +}) diff --git a/packages/extension/src/components/onboarding/onboardingLogo.ts b/packages/extension/src/components/onboarding/onboardingLogo.ts new file mode 100644 index 00000000..918aa12e --- /dev/null +++ b/packages/extension/src/components/onboarding/onboardingLogo.ts @@ -0,0 +1,10 @@ +import { useThemeContext } from "@/hooks/useTheme" + +const LOGO_URL = chrome.runtime.getURL("SelectionCommandLogo.png") +// The same logo with the wordmark's lightness inverted (hue kept) and the +// icon untouched, since the original's dark text vanishes on a dark page. +const LOGO_DARK_URL = chrome.runtime.getURL("SelectionCommandLogo_dark.png") + +/** Returns the logo image URL for the current theme. */ +export const useLogoUrl = (): string => + useThemeContext() === "dark" ? LOGO_DARK_URL : LOGO_URL diff --git a/packages/extension/src/components/onboarding/steps/StepAiPromptCommand.tsx b/packages/extension/src/components/onboarding/steps/StepAiPromptCommand.tsx index 5786340c..51e23ebc 100644 --- a/packages/extension/src/components/onboarding/steps/StepAiPromptCommand.tsx +++ b/packages/extension/src/components/onboarding/steps/StepAiPromptCommand.tsx @@ -132,7 +132,7 @@ export function StepAiPromptCommand({ onboarding }: Props) { return (
-

+

2.{" "} {t("Option_commandType_aiPrompt_title")}

@@ -157,7 +157,7 @@ export function StepAiPromptCommand({ onboarding }: Props) { className="flex flex-col items-center gap-4" delay={phaseDelays.message} > -

+

{isValueShown ? ( <> {t("onboarding_step2ValueMessage")} @@ -181,7 +181,7 @@ export function StepAiPromptCommand({ onboarding }: Props) { key="value-submessage" delay={phaseDelays.valueSubmessage} > -

+

{renderMultiline(t("onboarding_step2ValueSubmessage"))}

@@ -236,7 +236,7 @@ export function StepAiPromptCommand({ onboarding }: Props) { type="button" onClick={() => onboarding.goToStep(OnboardingStep.LINK_PREVIEW)} className={cn( - "flex items-center gap-2 min-h-14 rounded-xl bg-sky-950 px-8 text-lg font-semibold text-white shadow-[0_10px_20px_-14px_rgba(15,23,42,.7)] hover:brightness-[1.35]", + "flex items-center gap-2 min-h-14 rounded-xl bg-sky-950 dark:bg-sky-700 px-8 text-lg font-semibold text-white shadow-[0_10px_20px_-14px_rgba(15,23,42,.7)] hover:brightness-[1.35]", !onboarding.ui.showRail && "mt-4", )} > diff --git a/packages/extension/src/components/onboarding/steps/StepComplete.tsx b/packages/extension/src/components/onboarding/steps/StepComplete.tsx index f16302eb..418626de 100644 --- a/packages/extension/src/components/onboarding/steps/StepComplete.tsx +++ b/packages/extension/src/components/onboarding/steps/StepComplete.tsx @@ -33,7 +33,7 @@ export function StepComplete({ onboarding }: Props) {
@@ -43,7 +43,7 @@ export function StepComplete({ onboarding }: Props) { className="flex flex-col items-center gap-5" delay={delays[1]} > -

+

{t("onboarding_completeTitle")}

@@ -52,7 +52,7 @@ export function StepComplete({ onboarding }: Props) { className="flex flex-col items-center gap-5" delay={delays[2]} > -

+

{renderMultiline(t("onboarding_completeBody"))}

@@ -64,7 +64,7 @@ export function StepComplete({ onboarding }: Props) { >