| 1 | import { useMemo, type CSSProperties } from "react"; |
| 2 | import { deriveCodeReadabilityPalette, type CodeReadabilityPalette } from "../lib/codeReadability"; |
| 3 | import { themePackKind, type ThemePackView } from "../lib/themePack"; |
| 4 | import { baseStyleForPreview, themePreviewPalette } from "../lib/themePreviewPalette"; |
| 5 | |
| 6 | export function themePreviewPaneAlpha(pack: ThemePackView | null, scene: "home" | "task"): number { |
| 7 | // Live pane transparency is guarded by data-theme-has-bg. Keep token-only |
| 8 | // and base previews opaque so the isolated preview matches the applied UI. |
| 9 | if (pack?.hasBackground !== true) return 1; |
| 10 | const paneAlpha = scene === "home" |
| 11 | ? pack.background?.paneOpacity ?? 0.50 |
| 12 | : pack.taskBackground?.paneOpacity ?? pack.background?.paneOpacity ?? 0.68; |
| 13 | return Math.min(1, Math.max(0, paneAlpha)); |
| 14 | } |
| 15 | |
| 16 | export function themePreviewCodePalette( |
| 17 | pack: ThemePackView | null, |
| 18 | mode: "light" | "dark", |
| 19 | ): CodeReadabilityPalette { |
| 20 | const tokens = mode === "light" ? pack?.tokens?.light : pack?.tokens?.dark; |
| 21 | return deriveCodeReadabilityPalette(mode, baseStyleForPreview(pack), tokens || {}); |
| 22 | } |
| 23 | |
| 24 | /** Isolated mini Reasonix surface for gallery detail — does not touch :root. */ |
| 25 | export function ThemePreviewSurface({ |
| 26 | pack, |
| 27 | mode, |
| 28 | scene, |
| 29 | variant = "full", |
| 30 | }: { |
| 31 | pack: ThemePackView | null; |
| 32 | mode: "light" | "dark"; |
| 33 | scene: "home" | "task"; |
| 34 | variant?: "full" | "thumbnail"; |
| 35 | }) { |
| 36 | const isBasePreview = Boolean(pack && themePackKind(pack) === "base"); |
| 37 | const baseStyle = baseStyleForPreview(pack); |
| 38 | const style = useMemo(() => { |
| 39 | const palette = themePreviewPalette(pack, mode); |
| 40 | const tokens = mode === "light" ? pack?.tokens?.light : pack?.tokens?.dark; |
| 41 | const code = themePreviewCodePalette(pack, mode); |
| 42 | const chat = tokens?.chat || palette.bg; |
| 43 | const sceneBackground = scene === "task" ? pack?.taskBackground || pack?.background : pack?.background; |
| 44 | const focusX = sceneBackground?.focusX ?? 0.72; |
| 45 | const focusY = sceneBackground?.focusY ?? 0.45; |
| 46 | const opacity = |
| 47 | scene === "home" |
| 48 | ? pack?.background?.homeOpacity ?? 1 |
| 49 | : pack?.taskBackground?.opacity ?? pack?.background?.taskOpacity ?? 0.28; |
| 50 | const overlay = scene === "task" |
| 51 | ? pack?.taskBackground?.overlayStrength ?? pack?.background?.overlayStrength ?? 0.62 |
| 52 | : pack?.background?.overlayStrength ?? 0.62; |
| 53 | const boundedPaneAlpha = themePreviewPaneAlpha(pack, scene); |
| 54 | const paneCardOffset = scene === "home" ? 0.26 : 0.14; |
| 55 | return { |
| 56 | ["--tp-bg" as string]: palette.bg, |
| 57 | ["--tp-panel" as string]: palette.panel, |
| 58 | ["--tp-sidebar" as string]: palette.sidebar, |
| 59 | ["--tp-fg" as string]: palette.fg, |
| 60 | ["--tp-fg-dim" as string]: palette.fgDim, |
| 61 | ["--tp-accent" as string]: palette.accent, |
| 62 | ["--tp-accent-fg" as string]: palette.accentFg, |
| 63 | ["--tp-border" as string]: palette.border, |
| 64 | ["--tp-radius" as string]: palette.radius, |
| 65 | ["--tp-chat" as string]: chat, |
| 66 | ["--tp-code-bg" as string]: code.background, |
| 67 | ["--tp-code-border" as string]: code.border, |
| 68 | ["--tp-code-fg" as string]: code.foreground, |
| 69 | ["--tp-hl-keyword" as string]: code.keyword, |
| 70 | ["--tp-hl-string" as string]: code.string, |
| 71 | ["--tp-hl-number" as string]: code.number, |
| 72 | ["--tp-hl-comment" as string]: code.comment, |
| 73 | ["--tp-hl-func" as string]: code.function, |
| 74 | ["--tp-hl-type" as string]: code.type, |
| 75 | ["--tp-add-fg" as string]: code.addition, |
| 76 | ["--tp-del-fg" as string]: code.deletion, |
| 77 | ["--tp-code-add-bg" as string]: code.additionBackground, |
| 78 | ["--tp-code-del-bg" as string]: code.deletionBackground, |
| 79 | ["--tp-focus-x" as string]: `${focusX * 100}%`, |
| 80 | ["--tp-focus-y" as string]: `${focusY * 100}%`, |
| 81 | ["--tp-bg-opacity" as string]: String(opacity), |
| 82 | ["--tp-overlay" as string]: String(overlay), |
| 83 | ["--tp-pane-shell-pct" as string]: `${Math.min((boundedPaneAlpha + 0.08) * 100, 100)}%`, |
| 84 | ["--tp-pane-card-pct" as string]: `${Math.min((boundedPaneAlpha + paneCardOffset) * 100, 100)}%`, |
| 85 | } as CSSProperties; |
| 86 | }, [pack, mode, scene]); |
| 87 | |
| 88 | const bgUrl = isBasePreview |
| 89 | ? "" |
| 90 | : scene === "task" |
| 91 | ? pack?.taskBackgroundUrl || pack?.previewUrl || pack?.backgroundUrl || "" |
| 92 | : pack?.previewUrl || pack?.backgroundUrl || ""; |
| 93 | |
| 94 | return ( |
| 95 | <div |
| 96 | className="theme-preview-surface" |
| 97 | data-mode={mode} |
| 98 | data-scene={scene} |
| 99 | data-preview-kind={isBasePreview ? "base" : "pack"} |
| 100 | data-base-style={baseStyle} |
| 101 | data-variant={variant} |
| 102 | style={style} |
| 103 | > |
| 104 | {bgUrl ? ( |
| 105 | <div |
| 106 | className="theme-preview-surface__bg" |
| 107 | style={{ backgroundImage: `url("${bgUrl}")` }} |
| 108 | aria-hidden="true" |
| 109 | /> |
| 110 | ) : ( |
| 111 | <div |
| 112 | className={`theme-preview-surface__bg ${isBasePreview ? "theme-preview-surface__bg--base" : "theme-preview-surface__bg--swatch"}`} |
| 113 | aria-hidden="true" |
| 114 | /> |
| 115 | )} |
| 116 | <div className="theme-preview-surface__overlay" aria-hidden="true" /> |
| 117 | <div className="theme-preview-surface__chrome"> |
| 118 | <aside className="theme-preview-surface__side"> |
| 119 | <div className="theme-preview-surface__logo">R</div> |
| 120 | <div className="theme-preview-surface__nav" /> |
| 121 | <div className="theme-preview-surface__nav theme-preview-surface__nav--dim" /> |
| 122 | <div className="theme-preview-surface__nav theme-preview-surface__nav--dim" /> |
| 123 | </aside> |
| 124 | <main className="theme-preview-surface__main"> |
| 125 | <div className="theme-preview-surface__card"> |
| 126 | <div className="theme-preview-surface__title" /> |
| 127 | <div className="theme-preview-surface__line" /> |
| 128 | <div className="theme-preview-surface__line theme-preview-surface__line--short" /> |
| 129 | <div className="theme-preview-surface__cta" /> |
| 130 | </div> |
| 131 | {variant === "full" ? ( |
| 132 | <div className="theme-preview-surface__code-island"> |
| 133 | <div className="theme-preview-surface__code-label">CODE + DIFF</div> |
| 134 | <code className="theme-preview-surface__code-line"> |
| 135 | <span className="theme-preview-surface__syntax--keyword">const</span>{" "} |
| 136 | <span className="theme-preview-surface__syntax--func">optimize</span>{" = "} |
| 137 | <span className="theme-preview-surface__syntax--type">async</span>{" ("} |
| 138 | <span className="theme-preview-surface__syntax--number">3</span>{") => "} |
| 139 | <span className="theme-preview-surface__syntax--string">"clear"</span> |
| 140 | <span className="theme-preview-surface__syntax--comment"> // readable</span> |
| 141 | </code> |
| 142 | <div className="theme-preview-surface__diff-line theme-preview-surface__diff-line--del"> |
| 143 | <span>−</span><span>opacity: 0.32</span> |
| 144 | </div> |
| 145 | <div className="theme-preview-surface__diff-line theme-preview-surface__diff-line--add"> |
| 146 | <span>+</span><span>codeBg: opaque</span> |
| 147 | </div> |
| 148 | </div> |
| 149 | ) : null} |
| 150 | </main> |
| 151 | </div> |
| 152 | </div> |
| 153 | ); |
| 154 | } |
| 155 |