- THEME INDEX
+ {t("themeIndexKicker")}
{collection === "builtIn"
? t("builtInThemes")
@@ -172,17 +193,33 @@ export function ThemesView({
{themes.length} {t("themesCount")}
-
- {themes.map((theme, index) => (
+
+ {filteredThemes.length === 0 && (
+
+
+ {t("noThemeMatches")}
+ {t("searchAgainDetail")}
+ setSearchQuery("")}>
+ {t("clearSearch")}
+
+
+ )}
+ {filteredThemes.map((theme) => (
item.id === theme.id) + 1}
locale={locale}
active={selectedTheme.id === theme.id}
live={liveThemeId === theme.id}
resolveAsset={resolveAsset}
onSelect={() => {
+ setBrowsedThemeId(theme.id);
onSelect(theme);
setCompactDetailOpen(true);
}}
@@ -194,62 +231,24 @@ export function ThemesView({
))}
-
- setCompactDetailOpen(false)}
- >
-
- {t("backToThemes")}
-
-
-
- {busy
- ? t("applying")
- : selectedThemeIsLive
- ? t("liveInCodex")
- : t("previewOnly")}
-
-
-
-
-
- THEME {String(Math.max(1, selectedIndex)).padStart(2, "0")} /{" "}
- {localized.name.toUpperCase()}
-
-
{localized.name}
-
{localized.description}
-
-
- {t("performance")}
- {performance}
-
-
- {t("interactive")}
-
- {selectedTheme.scene.entities.length ? "Pointer" : "—"}
-
-
-
-
- onEdit(selectedTheme)}
- >
- {collection === "builtIn"
- ? t("customizeCopy")
- : t("editTheme")}
-
-
-
-
-
+
setCompactDetailOpen(false)}
+ onEdit={() => onEdit(browsedTheme)}
+ />
)}
diff --git a/apps/desktop/src/lib/companion-placement-modes.test.ts b/apps/desktop/src/lib/companion-placement-modes.test.ts
new file mode 100644
index 0000000..559f045
--- /dev/null
+++ b/apps/desktop/src/lib/companion-placement-modes.test.ts
@@ -0,0 +1,48 @@
+import { describe, expect, it } from "vitest";
+import {
+ applyCompanionPlacementMode,
+ resolveCompanionPlacementMode,
+} from "./companion-placement-modes";
+
+describe("companion placement modes", () => {
+ it("distinguishes composer, free, and imported custom attachments", () => {
+ expect(resolveCompanionPlacementMode(null)).toBe("free");
+ expect(
+ resolveCompanionPlacementMode({
+ target: "composer",
+ edge: "top",
+ align: 0.8,
+ offset: { x: 0, y: 3 },
+ }),
+ ).toBe("composer");
+ expect(
+ resolveCompanionPlacementMode({
+ target: "thread-summary",
+ edge: "top",
+ align: 0.5,
+ offset: { x: 0, y: 0 },
+ }),
+ ).toBe("custom");
+ });
+
+ it("preserves the free-position fallback while restoring package snapping", () => {
+ const packageAttachment = {
+ target: "composer" as const,
+ edge: "top" as const,
+ align: 0.72,
+ offset: { x: 4, y: 2 },
+ };
+ const current = {
+ anchor: { x: 63, y: 58 },
+ attachment: null,
+ };
+
+ expect(applyCompanionPlacementMode("free", current)).toEqual(current);
+ expect(
+ applyCompanionPlacementMode("composer", current, packageAttachment),
+ ).toEqual({
+ anchor: current.anchor,
+ attachment: packageAttachment,
+ });
+ });
+});
diff --git a/apps/desktop/src/lib/companion-placement-modes.ts b/apps/desktop/src/lib/companion-placement-modes.ts
new file mode 100644
index 0000000..977cad0
--- /dev/null
+++ b/apps/desktop/src/lib/companion-placement-modes.ts
@@ -0,0 +1,51 @@
+import type { EntityAttachment } from "@codex-styler/theme-core";
+
+export type CompanionPlacementMode = "composer" | "free" | "custom";
+export type SelectableCompanionPlacementMode = Exclude<
+ CompanionPlacementMode,
+ "custom"
+>;
+
+export interface CompanionPlacementValue {
+ anchor: { x: number; y: number };
+ attachment: EntityAttachment | null;
+}
+
+const defaultComposerAttachment: EntityAttachment = {
+ target: "composer",
+ edge: "top",
+ align: 0.82,
+ offset: { x: 0, y: 3 },
+};
+
+export function resolveCompanionPlacementMode(
+ attachment: EntityAttachment | null | undefined,
+): CompanionPlacementMode {
+ if (!attachment) return "free";
+ return attachment.target === "composer" ? "composer" : "custom";
+}
+
+export function applyCompanionPlacementMode(
+ mode: SelectableCompanionPlacementMode,
+ current: CompanionPlacementValue,
+ packageAttachment?: EntityAttachment,
+): CompanionPlacementValue {
+ if (mode === "free") {
+ return {
+ anchor: { ...current.anchor },
+ attachment: null,
+ };
+ }
+
+ const preferredAttachment =
+ current.attachment?.target === "composer"
+ ? current.attachment
+ : packageAttachment?.target === "composer"
+ ? packageAttachment
+ : defaultComposerAttachment;
+
+ return {
+ anchor: { ...current.anchor },
+ attachment: structuredClone(preferredAttachment),
+ };
+}
diff --git a/apps/desktop/src/lib/entity-placement.test.ts b/apps/desktop/src/lib/entity-placement.test.ts
new file mode 100644
index 0000000..f6fbfd0
--- /dev/null
+++ b/apps/desktop/src/lib/entity-placement.test.ts
@@ -0,0 +1,80 @@
+import { describe, expect, it } from "vitest";
+import {
+ clampEntityCenter,
+ resolveAttachedEntityPosition,
+ resolveFreeEntityAnchor,
+ resolveFreeEntityPosition,
+} from "./entity-placement";
+
+describe("entity placement", () => {
+ it("keeps the complete companion inside the viewport safe area", () => {
+ expect(clampEntityCenter(318, 160, 320)).toBe(232);
+ expect(
+ resolveFreeEntityPosition(
+ { x: 318, y: 238 },
+ { width: 160, height: 120 },
+ { width: 320, height: 240 },
+ ),
+ ).toEqual({ x: 232, y: 172 });
+ });
+
+ it("returns stable center percentages after a size-aware drag", () => {
+ expect(
+ resolveFreeEntityAnchor(
+ { x: -20, y: 280 },
+ { width: 80, height: 120 },
+ { width: 400, height: 300 },
+ ),
+ ).toEqual({ x: 12, y: 77.33333333333333 });
+ });
+
+ it("clamps top and bottom attachments without breaking their edge model", () => {
+ const target = { x: 260, y: 210, width: 80, height: 48 };
+ const entity = { width: 140, height: 160 };
+ const viewport = { width: 360, height: 280 };
+
+ expect(
+ resolveAttachedEntityPosition(
+ target,
+ {
+ target: "composer",
+ edge: "top",
+ align: 1,
+ offset: { x: 12, y: 3 },
+ },
+ entity,
+ viewport,
+ ),
+ ).toEqual({ x: 282, y: 213 });
+
+ expect(
+ resolveAttachedEntityPosition(
+ target,
+ {
+ target: "composer",
+ edge: "bottom",
+ align: 0,
+ offset: { x: -20, y: 3 },
+ },
+ entity,
+ viewport,
+ ),
+ ).toEqual({ x: 240, y: 112 });
+ });
+
+ it("uses the most visible edge when an oversized companion cannot fit", () => {
+ expect(
+ resolveAttachedEntityPosition(
+ { x: 20, y: 100, width: 200, height: 40 },
+ {
+ target: "composer",
+ edge: "top",
+ align: 0.5,
+ offset: { x: 0, y: 0 },
+ },
+ { width: 500, height: 360 },
+ { width: 320, height: 240 },
+ ),
+ ).toEqual({ x: 160, y: 232 });
+ });
+});
diff --git a/apps/desktop/src/lib/entity-placement.ts b/apps/desktop/src/lib/entity-placement.ts
new file mode 100644
index 0000000..e7284ec
--- /dev/null
+++ b/apps/desktop/src/lib/entity-placement.ts
@@ -0,0 +1,114 @@
+import type { EntityAttachment } from "@codex-styler/theme-core";
+
+export interface EntityDimensions {
+ width: number;
+ height: number;
+}
+
+export interface EntityViewport {
+ width: number;
+ height: number;
+}
+
+export interface EntityPoint {
+ x: number;
+ y: number;
+}
+
+export interface EntityTargetRect extends EntityPoint {
+ width: number;
+ height: number;
+}
+
+export const ENTITY_SAFE_INSET = 8;
+
+function finiteOr(value: number, fallback: number) {
+ return Number.isFinite(value) ? value : fallback;
+}
+
+export function clampEntityCenter(
+ value: number,
+ extent: number,
+ viewportExtent: number,
+ inset = ENTITY_SAFE_INSET,
+) {
+ const viewport = Math.max(0, finiteOr(viewportExtent, 0));
+ const halfExtent = Math.max(0, finiteOr(extent, 0)) / 2;
+ const safeInset = Math.max(0, finiteOr(inset, ENTITY_SAFE_INSET));
+ const minimum = safeInset + halfExtent;
+ const maximum = viewport - safeInset - halfExtent;
+
+ if (minimum > maximum) return viewport / 2;
+ return Math.max(minimum, Math.min(maximum, finiteOr(value, viewport / 2)));
+}
+
+export function resolveFreeEntityPosition(
+ point: EntityPoint,
+ entity: EntityDimensions,
+ viewport: EntityViewport,
+ inset = ENTITY_SAFE_INSET,
+): EntityPoint {
+ return {
+ x: clampEntityCenter(point.x, entity.width, viewport.width, inset),
+ y: clampEntityCenter(point.y, entity.height, viewport.height, inset),
+ };
+}
+
+export function resolveFreeEntityAnchor(
+ point: EntityPoint,
+ entity: EntityDimensions,
+ viewport: EntityViewport,
+ inset = ENTITY_SAFE_INSET,
+): EntityPoint {
+ const safe = resolveFreeEntityPosition(point, entity, viewport, inset);
+ return {
+ x: viewport.width > 0 ? (safe.x / viewport.width) * 100 : 50,
+ y: viewport.height > 0 ? (safe.y / viewport.height) * 100 : 50,
+ };
+}
+
+function clampAttachedEdge(
+ value: number,
+ entityHeight: number,
+ viewportHeight: number,
+ edge: EntityAttachment["edge"],
+ inset: number,
+) {
+ const height = Math.max(0, finiteOr(entityHeight, 0));
+ const viewport = Math.max(0, finiteOr(viewportHeight, 0));
+ const safeInset = Math.max(0, finiteOr(inset, ENTITY_SAFE_INSET));
+ const minimum = edge === "top" ? safeInset + height : safeInset;
+ const maximum =
+ edge === "top" ? viewport - safeInset : viewport - safeInset - height;
+
+ if (minimum > maximum) {
+ return edge === "top" ? viewport - safeInset : safeInset;
+ }
+ return Math.max(minimum, Math.min(maximum, finiteOr(value, minimum)));
+}
+
+export function resolveAttachedEntityPosition(
+ target: EntityTargetRect,
+ attachment: EntityAttachment,
+ entity: EntityDimensions,
+ viewport: EntityViewport,
+ inset = ENTITY_SAFE_INSET,
+): EntityPoint {
+ const edgeY =
+ attachment.edge === "bottom" ? target.y + target.height : target.y;
+ return {
+ x: clampEntityCenter(
+ target.x + target.width * attachment.align + attachment.offset.x,
+ entity.width,
+ viewport.width,
+ inset,
+ ),
+ y: clampAttachedEdge(
+ edgeY + attachment.offset.y,
+ entity.height,
+ viewport.height,
+ attachment.edge,
+ inset,
+ ),
+ };
+}
diff --git a/apps/desktop/src/lib/i18n.ts b/apps/desktop/src/lib/i18n.ts
index 3ce3590..9815402 100644
--- a/apps/desktop/src/lib/i18n.ts
+++ b/apps/desktop/src/lib/i18n.ts
@@ -10,6 +10,15 @@ const messages = {
companions: "Companions",
library: "My Themes",
settings: "Settings",
+ homeKicker: "Workspace control",
+ themeLibraryKicker: "Visual system library",
+ companionLibraryKicker: "Independent scene entities",
+ companionLibraryDescription:
+ "Place companions by dragging. Recommended pairings are optional, and themes and companions always stay independent.",
+ companionDraftsKicker: "Autosaved creator projects",
+ settingsKicker: "Local preferences",
+ themeIndexKicker: "Theme index",
+ themeLabel: "Theme",
homeTitle: "Your Codex, composed in one place.",
homeDescription:
"See the active visual system, companion, connection state, and safest next action without navigating the library.",
@@ -29,6 +38,12 @@ const messages = {
themeLibrary: "Theme library",
themeLibraryDetail:
"Browse complete visual systems, manage your local themes, or start a new direction.",
+ searchThemes: "Search themes",
+ searchCompanions: "Search companions",
+ clearSearch: "Clear search",
+ noThemeMatches: "No themes match this search.",
+ noCompanionMatches: "No companions match this search.",
+ searchAgainDetail: "Try a different name, description, or tag.",
builtInThemes: "Built-in",
myThemes: "My Themes",
newTheme: "New theme",
@@ -44,6 +59,20 @@ const messages = {
allThemes: "All themes",
featured: "Featured",
interactive: "Interactive",
+ visualCoverage: "Styled in Codex",
+ visualCoverageSemanticDetail:
+ "Enhanced mode coordinates the complete interface, not only the backdrop.",
+ visualCoverageSafeDetail:
+ "This theme intentionally keeps styling isolated to the scene.",
+ effectBackground: "Scene",
+ effectSurfaces: "Surfaces",
+ effectControls: "Controls",
+ effectIcons: "Icons",
+ effectTypography: "Typography",
+ effectMotion: "Motion",
+ effectReadability: "Readability",
+ inspectEffect: "Inspect this effect",
+ effectUnavailable: "Not included in this theme mode",
apply: "Apply to Codex",
applyChanges: "Apply changes",
startAndApply: "Start and apply",
@@ -56,6 +85,9 @@ const messages = {
selected: "Selected",
preview: "Preview",
previewOnly: "Preview",
+ compareAppearance: "Compare appearance",
+ styledAppearance: "Styled",
+ officialAppearance: "Official",
liveSetup: "Live setup",
selectedSetup: "Selected setup",
liveInCodex: "Live in Codex",
@@ -149,7 +181,7 @@ const messages = {
automaticDetectionRestored: "Automatic Codex detection restored",
appearance: "Appearance",
background: "Background",
- surfaces: "Surfaces",
+ surfaces: "Interface system",
motion: "Motion",
motionLayer: "Transitions, parallax and response",
motionStyle: "Motion style",
@@ -159,6 +191,18 @@ const messages = {
motionExpressive: "Expressive",
motionDescription:
"Motion recipes tune the whole theme while Reduce Motion always takes priority.",
+ previewMotion: "Preview motion",
+ previewMotionPlaying: "Previewing motion…",
+ motionPreviewDescription:
+ "Runs one guided cursor sweep with the theme's real parallax values. Nothing is saved.",
+ motionPreviewReduced:
+ "Reduce Motion is active, so pointer tracking and parallax stay still.",
+ motionPreviewDisabled:
+ "Choose Calm, Fluid, or Expressive to preview a responsive theme.",
+ motionPreviewOfficial:
+ "Switch to Current edit or Saved to preview authored motion.",
+ motionPreviewOfficialLibrary:
+ "Switch to Styled to preview the theme's authored motion.",
parallaxDepth: "Parallax depth",
companion: "Companion",
noCompanion: "No companion",
@@ -221,6 +265,14 @@ const messages = {
radius: "Corner radius",
motionIntensity: "Motion intensity",
companionSize: "Companion size",
+ companionPlacement: "Size & placement",
+ companionPlacementDetail:
+ "Choose a stable placement mode or drag in the preview. Changes update Codex immediately while connected.",
+ placementMode: "Placement mode",
+ customAttachment: "Package attachment",
+ packageDefault: "Package default",
+ customized: "Customized",
+ resetCompanionPlacement: "Reset size & placement",
livePreview: "Live preview",
safePreview: "Semantic preview · no Codex files changed",
layers: "Scene layers",
@@ -263,17 +315,26 @@ const messages = {
deleteLayer: "Delete layer",
blendMode: "Blend mode",
previewScenario: "Preview scenario",
+ previewControls: "Preview controls",
previewVersion: "Preview version",
previewCurrent: "Current edit",
previewSaved: "Saved",
+ previewOfficial: "Official",
viewingSaved: "Viewing saved",
+ viewingOfficial: "Viewing official Codex",
noSavedDifference: "Make an edit before comparing with the saved version.",
savedPreviewReadOnly: "Saved preview",
savedPreviewReadOnlyDetail: "Controls show your current edit.",
+ officialPreviewReadOnly: "Official Codex reference",
+ officialPreviewReadOnlyDetail:
+ "Background, companion, and theme styling are temporarily hidden for comparison.",
returnToCurrent: "Return",
previewHome: "Home",
previewTask: "Task & composer",
+ previewChanges: "Changes & diff",
+ previewTerminal: "Terminal & output",
previewSettings: "Settings",
+ previewComponents: "Components & states",
previewDialog: "Dialog",
previewRightPanel: "Right panel",
liveEffectMapped: "Live effect mapped",
@@ -286,7 +347,7 @@ const messages = {
sceneEffectDetail:
"Layer opacity, blend, and depth update the backdrop in every view.",
surfaceEffectDetail:
- "Panels, controls, icons, and layout update together after applying in Enhanced mode.",
+ "Panels, controls, selected and disabled states, status feedback, diffs, icons, and layout update together in Enhanced mode.",
motionEffectDetail:
"Move the pointer over the preview to inspect response. Reduce Motion still takes priority.",
pairingEffectDetail:
@@ -302,11 +363,26 @@ const messages = {
resetThemeBody:
"Restore this draft to the saved theme. You can undo the action afterward.",
codexPreview: "Codex preview",
- contrastProtected: "Contrast protected",
- contrastProtectedBody:
- "Text colors are automatically corrected to meet readable contrast on theme surfaces.",
+ visualSafety: "Visual safety",
+ visualSafetyReady: "Verified",
+ visualSafetyAttention: "Review",
+ visualSafetyReadyDetail: "Final rendered colors pass all seven checks.",
+ visualSafetyAttentionDetail:
+ "One or more rendered roles need stronger separation.",
+ visualSafeguardActive: "render-time safeguard active",
+ visualSafeguardsActive: "render-time safeguards active",
+ visualPrimaryText: "Primary text",
+ visualSecondaryText: "Secondary text",
+ visualIcons: "Interface icons",
+ visualStatusFeedback: "Status feedback",
+ visualDiffFeedback: "Code changes",
+ visualAccentContent: "Accent controls",
+ visualBoundaries: "Component boundaries",
+ visualSurfaceGuard: "Image surface guard",
+ visualSafetyRuntimeDetail:
+ "Styler preserves the authored palette, then makes the smallest required render-time correction for the actual UI surface.",
backgroundLayer: "Image and atmosphere",
- surfaceLayer: "Panels, cards and controls",
+ surfaceLayer: "Layout, materials, icons and controls",
importBackground: "Import image",
imageAdaptiveTitle: "Adaptive visual direction",
imageAdaptiveBody:
@@ -316,10 +392,60 @@ const messages = {
adaptiveCinematic: "Cinematic",
adaptiveSoft: "Soft",
adaptiveVivid: "Vivid",
+ surfaceTreatment: "Interface direction",
+ surfaceTreatmentDescription:
+ "Choose a composed direction first so layout, icons, and detail stay coordinated.",
+ treatmentRefined: "Refined",
+ treatmentRefinedDescription: "Keeps the native rhythm with quieter depth.",
+ treatmentEditorial: "Editorial",
+ treatmentEditorialDescription:
+ "Builds clearer hierarchy with themed controls.",
+ treatmentImmersive: "Immersive",
+ treatmentImmersiveDescription:
+ "Puts the scene first with expressive detail.",
+ surfaceMaterial: "Surface material",
+ surfaceMaterialDescription:
+ "Control how solid, layered, or translucent focused panels feel.",
+ materialSolid: "Solid",
+ materialSolidDescription: "Opaque, crisp surfaces with no glass blur.",
+ materialLayered: "Layered",
+ materialLayeredDescription: "Balanced depth with clearer focused panels.",
+ materialFrosted: "Frosted",
+ materialFrostedDescription:
+ "Translucent surfaces with stronger background diffusion.",
+ fineTune: "Fine tune",
+ fineTuneDescription:
+ "Open the individual layout, icon, color, opacity, blur, and geometry controls.",
+ adaptiveInteractionChrome: "Interaction details stay coordinated",
+ adaptiveInteractionChromeDetail:
+ "Selection, focus rings, caret, links, and scrollbars remain readable while following the theme accent.",
+ surfaceColor: "Surface color",
+ borderColor: "Border color",
+ colorHarmony: "Component color harmony",
+ colorHarmonyDescription:
+ "Choose how strongly panels and controls separate while preserving readable text.",
+ harmonyAutomatic: "Automatic",
+ harmonyAutomaticDetail:
+ "Follows the theme detail treatment and adapts as base colors change.",
+ harmonyTonal: "Tonal harmony",
+ harmonyTonalDetail:
+ "Keeps panels and controls close to the surface color for a quieter workspace.",
+ harmonyContrast: "Clear hierarchy",
+ harmonyContrastDetail:
+ "Adds stronger surface separation for dense panels, code, and controls.",
+ authoredColorHarmony: "Designed component palette",
+ authoredColorHarmonyDetail:
+ "This theme includes individually tuned component colors.",
+ focusSurfaceOpacity: "Focused panel opacity",
+ surfaceBlur: "Glass blur",
layoutTreatment: "Workspace layout",
layoutNative: "Native rhythm",
layoutEditorial: "Editorial",
layoutImmersive: "Immersive",
+ visualComposition: "Composition",
+ geometryPrecise: "Precise",
+ geometryBalanced: "Balanced",
+ geometrySoft: "Soft",
iconTreatment: "Icon treatment",
iconNative: "Native",
iconContained: "Contained",
@@ -329,7 +455,7 @@ const messages = {
decorationSubtle: "Subtle",
decorationExpressive: "Expressive",
semanticControlsHint:
- "Layout, icons, and details apply in Enhanced mode. Conservative mode keeps only the isolated scene.",
+ "Enhanced mode applies the complete interface system. Conservative mode keeps only the isolated scene.",
importTheme: "Import theme",
exportTheme: "Export selected",
noLocalThemes: "Your imported and duplicated themes will appear here.",
@@ -484,7 +610,7 @@ const messages = {
companionUpdated: "Companion updated",
codexQuitFailed:
"Codex did not close. Save your work, close it manually, and try again.",
- version: "Beta 6",
+ version: "Beta 7",
},
"zh-CN": {
unofficial: "非官方开源项目",
@@ -494,6 +620,15 @@ const messages = {
companions: "互动伙伴",
library: "我的主题",
settings: "设置",
+ homeKicker: "工作区控制",
+ themeLibraryKicker: "视觉系统库",
+ companionLibraryKicker: "独立场景实体",
+ companionLibraryDescription:
+ "可通过拖拽调整伙伴位置;默认搭配仅供参考,主题与互动伙伴始终可以独立选择。",
+ companionDraftsKicker: "自动保存的创作工程",
+ settingsKicker: "本地偏好设置",
+ themeIndexKicker: "主题索引",
+ themeLabel: "主题",
homeTitle: "在一个地方,掌握你的 Codex 外观。",
homeDescription:
"直接查看当前视觉系统、互动伙伴、连接状态与下一步操作,无需先理解主题库结构。",
@@ -513,6 +648,12 @@ const messages = {
themeLibrary: "主题库",
themeLibraryDetail:
"浏览完整视觉系统、管理本地主题,或开始一个全新的视觉方向。",
+ searchThemes: "搜索主题",
+ searchCompanions: "搜索互动伙伴",
+ clearSearch: "清除搜索",
+ noThemeMatches: "没有符合当前搜索的主题。",
+ noCompanionMatches: "没有符合当前搜索的互动伙伴。",
+ searchAgainDetail: "可以尝试其他名称、描述或标签。",
builtInThemes: "内置主题",
myThemes: "我的主题",
newTheme: "新建主题",
@@ -528,6 +669,19 @@ const messages = {
allThemes: "全部主题",
featured: "精选",
interactive: "互动",
+ visualCoverage: "在 Codex 中的装扮范围",
+ visualCoverageSemanticDetail:
+ "增强模式会统一整个界面,而不只是替换背景图。",
+ visualCoverageSafeDetail: "此主题有意只在隔离的场景层中生效。",
+ effectBackground: "场景",
+ effectSurfaces: "界面表面",
+ effectControls: "控件状态",
+ effectIcons: "图标",
+ effectTypography: "排版节奏",
+ effectMotion: "动态",
+ effectReadability: "可读性",
+ inspectEffect: "查看这项效果",
+ effectUnavailable: "当前主题模式不包含这项效果",
apply: "应用到 Codex",
applyChanges: "应用更改",
startAndApply: "启动并应用",
@@ -540,6 +694,9 @@ const messages = {
selected: "已选择",
preview: "预览",
previewOnly: "预览",
+ compareAppearance: "效果对比",
+ styledAppearance: "主题效果",
+ officialAppearance: "官方界面",
liveSetup: "Codex 当前搭配",
selectedSetup: "待应用搭配",
liveInCodex: "已在 Codex 中生效",
@@ -628,7 +785,7 @@ const messages = {
automaticDetectionRestored: "已恢复 Codex 自动检测",
appearance: "外观",
background: "背景",
- surfaces: "表面",
+ surfaces: "界面系统",
motion: "动态",
motionLayer: "过渡、视差与交互响应",
motionStyle: "动态风格",
@@ -638,6 +795,15 @@ const messages = {
motionExpressive: "强化",
motionDescription:
"动态方案会统一调整整个主题;系统的“减少动态”设置始终拥有最高优先级。",
+ previewMotion: "预演动态",
+ previewMotionPlaying: "正在预演动态…",
+ motionPreviewDescription:
+ "使用主题的真实视差参数完成一次引导式光标移动;不会保存任何更改。",
+ motionPreviewReduced:
+ "当前已启用“减少动态”,因此光标跟随与视差会保持静止。",
+ motionPreviewDisabled: "选择“平静”“流畅”或“强化”后即可预演响应效果。",
+ motionPreviewOfficial: "切换到“当前编辑”或“已保存”后即可预演主题动态。",
+ motionPreviewOfficialLibrary: "切换到“主题效果”后即可预演主题动态。",
parallaxDepth: "视差深度",
companion: "互动伙伴",
noCompanion: "不使用伙伴",
@@ -699,6 +865,14 @@ const messages = {
radius: "圆角",
motionIntensity: "动态强度",
companionSize: "伙伴尺寸",
+ companionPlacement: "尺寸与位置",
+ companionPlacementDetail:
+ "可选择稳定的定位方式,也可直接在预览中拖拽;Codex 已连接时会即时同步调整。",
+ placementMode: "定位方式",
+ customAttachment: "资源包自定义吸附",
+ packageDefault: "资源包默认值",
+ customized: "已自定义",
+ resetCompanionPlacement: "恢复默认尺寸与位置",
livePreview: "实时预览",
safePreview: "语义预览 · 不修改 Codex 文件",
layers: "场景图层",
@@ -741,17 +915,26 @@ const messages = {
deleteLayer: "删除图层",
blendMode: "混合模式",
previewScenario: "预览场景",
+ previewControls: "预览控制",
previewVersion: "预览版本",
previewCurrent: "当前编辑",
previewSaved: "已保存",
+ previewOfficial: "官方界面",
viewingSaved: "正在查看已保存版本",
+ viewingOfficial: "正在查看官方 Codex",
noSavedDifference: "修改一项设置后即可与已保存版本比较。",
savedPreviewReadOnly: "已保存预览",
savedPreviewReadOnlyDetail: "控件仍显示当前编辑值。",
+ officialPreviewReadOnly: "官方 Codex 参照",
+ officialPreviewReadOnlyDetail:
+ "暂时隐藏背景、伙伴与主题样式,便于直接比较改造幅度。",
returnToCurrent: "返回编辑",
previewHome: "主页",
previewTask: "任务与输入框",
+ previewChanges: "更改与差异",
+ previewTerminal: "终端与输出",
previewSettings: "设置",
+ previewComponents: "组件与状态",
previewDialog: "弹窗",
previewRightPanel: "右侧面板",
liveEffectMapped: "已映射实时效果",
@@ -762,7 +945,7 @@ const messages = {
backgroundEffectDetail: "图片、氛围与遮罩独立于 Codex 控件安全渲染。",
sceneEffectDetail: "图层透明度、混合方式与深度会在所有场景更新。",
surfaceEffectDetail:
- "应用后,面板、控件、图标与布局会在增强模式下一起更新。",
+ "增强模式会统一更新面板、控件、选中与禁用状态、状态反馈、diff、图标和布局。",
motionEffectDetail: "在预览中移动光标可检查响应;“减少动态”设置始终优先。",
pairingEffectDetail:
"只为新用户保存搭配建议,不会替换当前伙伴,也不会改变此预览。",
@@ -776,11 +959,25 @@ const messages = {
resetTheme: "已恢复为已保存版本,可使用撤销返回。",
resetThemeBody: "将当前草稿恢复为已保存主题,操作完成后仍可撤销。",
codexPreview: "Codex 主题预览",
- contrastProtected: "可读性保护已开启",
- contrastProtectedBody:
- "文字颜色会根据主题表面自动校正,确保达到可读对比度。",
+ visualSafety: "视觉安全验收",
+ visualSafetyReady: "已验证",
+ visualSafetyAttention: "需检查",
+ visualSafetyReadyDetail: "最终渲染颜色已通过七项检查。",
+ visualSafetyAttentionDetail: "部分最终渲染角色需要更清晰的层次。",
+ visualSafeguardActive: "项渲染保护已生效",
+ visualSafeguardsActive: "项渲染保护已生效",
+ visualPrimaryText: "主要文字",
+ visualSecondaryText: "次要文字",
+ visualIcons: "界面图标",
+ visualStatusFeedback: "状态反馈",
+ visualDiffFeedback: "代码变更",
+ visualAccentContent: "强调色控件",
+ visualBoundaries: "组件边界",
+ visualSurfaceGuard: "图片表面保护",
+ visualSafetyRuntimeDetail:
+ "Styler 会保留作者色板,并针对文字所在的实际界面表面,仅在渲染时做满足可读性所需的最小校正。",
backgroundLayer: "图片与氛围",
- surfaceLayer: "面板、卡片与控件",
+ surfaceLayer: "布局、材质、图标与控件",
importBackground: "导入图片",
imageAdaptiveTitle: "自适应视觉方向",
imageAdaptiveBody:
@@ -790,10 +987,53 @@ const messages = {
adaptiveCinematic: "电影感",
adaptiveSoft: "柔和",
adaptiveVivid: "鲜明",
+ surfaceTreatment: "界面装扮方案",
+ surfaceTreatmentDescription:
+ "先选择一套完整方向,让布局、图标和细节保持协调,再按需微调。",
+ treatmentRefined: "精修",
+ treatmentRefinedDescription: "保留原生节奏,用克制层次统一界面。",
+ treatmentEditorial: "编辑式",
+ treatmentEditorialDescription: "强化信息层级和主题化控件。",
+ treatmentImmersive: "沉浸式",
+ treatmentImmersiveDescription: "让场景成为主角并提高细节表现。",
+ surfaceMaterial: "表面材质",
+ surfaceMaterialDescription:
+ "控制普通表面与聚焦面板的实体感、层次和通透程度。",
+ materialSolid: "实色",
+ materialSolidDescription: "不使用玻璃模糊,呈现清晰稳定的实体表面。",
+ materialLayered: "层叠",
+ materialLayeredDescription: "保留适度通透感,同时加强聚焦面板层次。",
+ materialFrosted: "雾面",
+ materialFrostedDescription: "使用更明显的背景扩散与半透明表面。",
+ fineTune: "细节微调",
+ fineTuneDescription:
+ "按需展开布局、图标、颜色、不透明度、模糊与几何等单项控制。",
+ adaptiveInteractionChrome: "交互细节会自动协调",
+ adaptiveInteractionChromeDetail:
+ "文字选择、焦点环、输入光标、链接与滚动条会保持清晰,并跟随主题强调色。",
+ surfaceColor: "表面颜色",
+ borderColor: "边框颜色",
+ colorHarmony: "组件色彩协调",
+ colorHarmonyDescription:
+ "选择面板与控件的层次强度,同时持续保护文字可读性。",
+ harmonyAutomatic: "自动协调",
+ harmonyAutomaticDetail: "跟随主题细节风格,并随基础颜色自动调整。",
+ harmonyTonal: "柔和同色",
+ harmonyTonalDetail: "让面板与控件接近表面色,形成安静统一的工作区。",
+ harmonyContrast: "清晰层次",
+ harmonyContrastDetail: "强化密集面板、代码区域和控件之间的层级区分。",
+ authoredColorHarmony: "主题精调组件色板",
+ authoredColorHarmonyDetail: "该主题包含逐项设计的组件颜色。",
+ focusSurfaceOpacity: "聚焦面板不透明度",
+ surfaceBlur: "玻璃模糊",
layoutTreatment: "工作区布局",
layoutNative: "原生节奏",
layoutEditorial: "编辑式",
layoutImmersive: "沉浸式",
+ visualComposition: "构图与几何",
+ geometryPrecise: "精确利落",
+ geometryBalanced: "平衡",
+ geometrySoft: "圆润",
iconTreatment: "图标处理",
iconNative: "原生",
iconContained: "容器化",
@@ -803,7 +1043,7 @@ const messages = {
decorationSubtle: "克制",
decorationExpressive: "强化",
semanticControlsHint:
- "布局、图标和细节在增强模式下生效;保守模式只保留隔离的场景层。",
+ "增强模式会应用完整界面系统;保守模式只保留隔离的场景层。",
importTheme: "导入主题",
exportTheme: "导出所选主题",
noLocalThemes: "导入或复制的主题会出现在这里。",
@@ -948,7 +1188,7 @@ const messages = {
themeDeleted: "主题已删除",
companionUpdated: "互动伙伴已更新",
codexQuitFailed: "Codex 未能关闭。请保存工作、手动退出后再试。",
- version: "Beta 6",
+ version: "Beta 7",
},
} as const;
diff --git a/apps/desktop/src/lib/preview-entity-layout.test.ts b/apps/desktop/src/lib/preview-entity-layout.test.ts
new file mode 100644
index 0000000..0653214
--- /dev/null
+++ b/apps/desktop/src/lib/preview-entity-layout.test.ts
@@ -0,0 +1,29 @@
+import { describe, expect, it } from "vitest";
+import {
+ previewEntityDimensions,
+ resolvePreviewEntityScale,
+} from "./preview-entity-layout";
+
+describe("preview entity layout", () => {
+ it("keeps authored pixels at the 100% Codex reference height", () => {
+ expect(resolvePreviewEntityScale(760)).toBe(1);
+ expect(previewEntityDimensions(136, 136, 760)).toEqual({
+ scale: 1,
+ width: 136,
+ height: 136,
+ });
+ });
+
+ it("preserves visual occupancy in miniature manager previews", () => {
+ expect(previewEntityDimensions(136, 170, 380)).toEqual({
+ scale: 0.5,
+ width: 68,
+ height: 85,
+ });
+ });
+
+ it("does not enlarge above 100% or disappear in very small previews", () => {
+ expect(resolvePreviewEntityScale(1_200)).toBe(1);
+ expect(resolvePreviewEntityScale(80)).toBe(0.2);
+ });
+});
diff --git a/apps/desktop/src/lib/preview-entity-layout.ts b/apps/desktop/src/lib/preview-entity-layout.ts
new file mode 100644
index 0000000..47411af
--- /dev/null
+++ b/apps/desktop/src/lib/preview-entity-layout.ts
@@ -0,0 +1,31 @@
+const CODEX_REFERENCE_VIEWPORT_HEIGHT = 760;
+const MINIMUM_PREVIEW_SCALE = 0.2;
+
+/**
+ * Companion sizes are authored in Codex pixels. Manager previews render a
+ * miniature Codex viewport, so raw package pixels must be scaled against the
+ * same 1280 x 760 reference used by the editor's 100% preview.
+ */
+export function resolvePreviewEntityScale(viewportHeight: number) {
+ if (!Number.isFinite(viewportHeight) || viewportHeight <= 0) return 1;
+ return Math.min(
+ 1,
+ Math.max(
+ MINIMUM_PREVIEW_SCALE,
+ viewportHeight / CODEX_REFERENCE_VIEWPORT_HEIGHT,
+ ),
+ );
+}
+
+export function previewEntityDimensions(
+ sourceWidth: number,
+ sourceHeight: number,
+ viewportHeight: number,
+) {
+ const scale = resolvePreviewEntityScale(viewportHeight);
+ return {
+ scale,
+ width: sourceWidth * scale,
+ height: sourceHeight * scale,
+ };
+}
diff --git a/apps/desktop/src/lib/resource-search.test.ts b/apps/desktop/src/lib/resource-search.test.ts
new file mode 100644
index 0000000..1b2f5f4
--- /dev/null
+++ b/apps/desktop/src/lib/resource-search.test.ts
@@ -0,0 +1,37 @@
+import { describe, expect, it } from "vitest";
+import {
+ matchesResourceSearch,
+ normalizeResourceSearchText,
+} from "./resource-search";
+
+describe("resource search", () => {
+ it("normalizes case, spacing, and diacritics", () => {
+ expect(normalizeResourceSearchText(" Café STUDIO ")).toBe(
+ "cafe studio",
+ );
+ });
+
+ it("matches across all indexed resource fields", () => {
+ expect(
+ matchesResourceSearch("garden", [
+ "Quiet Garden",
+ "A calm workspace",
+ "nature image",
+ ]),
+ ).toBe(true);
+ expect(
+ matchesResourceSearch("cinematic", [
+ "Quiet Garden",
+ "A calm workspace",
+ "nature image",
+ ]),
+ ).toBe(false);
+ expect(
+ matchesResourceSearch("garden nature", [
+ "Quiet Garden",
+ "A calm workspace",
+ "nature image",
+ ]),
+ ).toBe(true);
+ });
+});
diff --git a/apps/desktop/src/lib/resource-search.ts b/apps/desktop/src/lib/resource-search.ts
new file mode 100644
index 0000000..0e46e27
--- /dev/null
+++ b/apps/desktop/src/lib/resource-search.ts
@@ -0,0 +1,20 @@
+export function normalizeResourceSearchText(value: string): string {
+ return value
+ .normalize("NFKD")
+ .replace(/\p{M}/gu, "")
+ .toLocaleLowerCase()
+ .trim();
+}
+
+export function matchesResourceSearch(
+ query: string,
+ fields: Array
,
+): boolean {
+ const normalizedQuery = normalizeResourceSearchText(query);
+ if (!normalizedQuery) return true;
+
+ const haystack = normalizeResourceSearchText(fields.filter(Boolean).join(" "));
+ return normalizedQuery
+ .split(/\s+/)
+ .every((term) => haystack.includes(term));
+}
diff --git a/apps/desktop/src/lib/runtime.ts b/apps/desktop/src/lib/runtime.ts
index 4ca9d44..a27dfcc 100644
--- a/apps/desktop/src/lib/runtime.ts
+++ b/apps/desktop/src/lib/runtime.ts
@@ -345,7 +345,7 @@ export async function checkForUpdates(
locale: "en" | "zh-CN",
): Promise {
if (!isTauri()) {
- return { currentVersion: "0.2.0-beta.6", update: null };
+ return { currentVersion: "0.2.0-beta.7", update: null };
}
return invoke("check_for_updates", { locale });
}
diff --git a/apps/desktop/src/lib/storage.test.ts b/apps/desktop/src/lib/storage.test.ts
index 4766bb9..e976268 100644
--- a/apps/desktop/src/lib/storage.test.ts
+++ b/apps/desktop/src/lib/storage.test.ts
@@ -68,17 +68,30 @@ describe("workspace UI preferences", () => {
focusMode: false,
themeEditorLayersWidth: 900,
themeEditorInspectorWidth: 20,
- themeEditorPreviewScenario: "settings",
+ themeEditorPreviewScenario: "components",
});
expect(loadWorkspaceUiPreferences()).toMatchObject({
focusMode: false,
themeEditorLayersWidth: 320,
themeEditorInspectorWidth: 264,
- themeEditorPreviewScenario: "settings",
+ themeEditorPreviewScenario: "components",
});
});
+ it("persists dedicated changes and terminal preview scenarios", () => {
+ saveWorkspaceUiPreferences({
+ focusMode: true,
+ themeEditorLayersWidth: 220,
+ themeEditorInspectorWidth: 300,
+ themeEditorPreviewScenario: "terminal",
+ });
+
+ expect(loadWorkspaceUiPreferences().themeEditorPreviewScenario).toBe(
+ "terminal",
+ );
+ });
+
it("falls back from corrupt non-finite widths and unknown scenarios", () => {
localStorage.setItem(
"codex-styler.ui.v1",
diff --git a/apps/desktop/src/lib/storage.ts b/apps/desktop/src/lib/storage.ts
index 8657eea..5b696c9 100644
--- a/apps/desktop/src/lib/storage.ts
+++ b/apps/desktop/src/lib/storage.ts
@@ -10,7 +10,14 @@ export type ManagerAppearance = "system" | "light" | "dark";
export type RuntimeStrategy = "enhanced" | "conservative";
export type CompanionMode = "theme-default" | "custom" | "disabled";
export type PreviewScenario =
- "home" | "task" | "settings" | "dialog" | "right-panel";
+ | "home"
+ | "task"
+ | "changes"
+ | "terminal"
+ | "settings"
+ | "components"
+ | "dialog"
+ | "right-panel";
export interface WorkspaceUiPreferences {
focusMode: boolean;
@@ -247,9 +254,16 @@ function finiteClamped(
}
function isPreviewScenario(value: unknown): value is PreviewScenario {
- return ["home", "task", "settings", "dialog", "right-panel"].includes(
- String(value),
- );
+ return [
+ "home",
+ "task",
+ "changes",
+ "terminal",
+ "settings",
+ "components",
+ "dialog",
+ "right-panel",
+ ].includes(String(value));
}
export function saveWorkspaceUiPreferences(
diff --git a/apps/desktop/src/lib/theme-color-harmony.test.ts b/apps/desktop/src/lib/theme-color-harmony.test.ts
new file mode 100644
index 0000000..a1fbfdc
--- /dev/null
+++ b/apps/desktop/src/lib/theme-color-harmony.test.ts
@@ -0,0 +1,41 @@
+import { builtinThemes, validateTheme } from "@codex-styler/theme-core";
+import { describe, expect, it } from "vitest";
+import { contrastRatio } from "./contrast";
+import {
+ assignThemeColorHarmony,
+ createThemeColorHarmonyPalette,
+ resolveThemeColorHarmonyMode,
+} from "./theme-color-harmony";
+
+describe("theme color harmony", () => {
+ it("builds distinct tonal and contrast component palettes", () => {
+ const variant = structuredClone(builtinThemes[0].variants.dark);
+ variant.appearance.palette = undefined;
+ const tonal = createThemeColorHarmonyPalette(variant, "tonal");
+ const contrast = createThemeColorHarmonyPalette(variant, "contrast");
+
+ expect(tonal.controlActive).not.toBe(contrast.controlActive);
+ expect(tonal.surfaceRaised).not.toBe(contrast.surfaceRaised);
+ expect(
+ contrastRatio(tonal.onAccent!, variant.appearance.accent),
+ ).toBeGreaterThanOrEqual(4.5);
+ expect(
+ contrastRatio(contrast.onAccent!, variant.appearance.accent),
+ ).toBeGreaterThanOrEqual(4.5);
+ });
+
+ it("recognizes generated recipes while preserving authored palettes", () => {
+ const theme = structuredClone(builtinThemes[0]);
+
+ expect(resolveThemeColorHarmonyMode(theme.variants.dark)).toBe("authored");
+ assignThemeColorHarmony(theme, "dark", "tonal");
+ expect(resolveThemeColorHarmonyMode(theme.variants.dark)).toBe("tonal");
+ expect(validateTheme(theme)).toEqual({ ok: true, issues: [] });
+ assignThemeColorHarmony(theme, "dark", "contrast");
+ expect(resolveThemeColorHarmonyMode(theme.variants.dark)).toBe("contrast");
+ expect(validateTheme(theme)).toEqual({ ok: true, issues: [] });
+ assignThemeColorHarmony(theme, "dark", "automatic");
+ expect(resolveThemeColorHarmonyMode(theme.variants.dark)).toBe("automatic");
+ expect(theme.variants.dark.appearance.palette).toBeUndefined();
+ });
+});
diff --git a/apps/desktop/src/lib/theme-color-harmony.ts b/apps/desktop/src/lib/theme-color-harmony.ts
new file mode 100644
index 0000000..e0e030a
--- /dev/null
+++ b/apps/desktop/src/lib/theme-color-harmony.ts
@@ -0,0 +1,153 @@
+import type {
+ ThemeDefinition,
+ ThemeSemanticPalette,
+ ThemeVariant,
+ ThemeVariantName,
+} from "@codex-styler/theme-core";
+import { adaptiveReadableColor, mixColors } from "./contrast";
+
+export type ThemeColorHarmonyId = "automatic" | "tonal" | "contrast";
+export type ThemeColorHarmonyMode = ThemeColorHarmonyId | "authored";
+
+const recipeStrength = {
+ tonal: {
+ canvas: 0.28,
+ raised: 0.055,
+ overlay: 0.09,
+ sunken: 0.16,
+ control: 0.075,
+ hover: 0.12,
+ active: 0.18,
+ borderSubtle: 0.62,
+ borderStrong: 0.18,
+ },
+ contrast: {
+ canvas: 0.16,
+ raised: 0.1,
+ overlay: 0.16,
+ sunken: 0.24,
+ control: 0.12,
+ hover: 0.2,
+ active: 0.28,
+ borderSubtle: 0.78,
+ borderStrong: 0.36,
+ },
+} as const;
+
+function samePalette(
+ left: ThemeSemanticPalette | undefined,
+ right: ThemeSemanticPalette | undefined,
+): boolean {
+ if (!left || !right) return left === right;
+ const keys = new Set([...Object.keys(left), ...Object.keys(right)]);
+ return [...keys].every(
+ (key) =>
+ left[key as keyof ThemeSemanticPalette] ===
+ right[key as keyof ThemeSemanticPalette],
+ );
+}
+
+export function createThemeColorHarmonyPalette(
+ variant: ThemeVariant,
+ recipe: Exclude,
+): ThemeSemanticPalette {
+ const { appearance, background } = variant;
+ const strength = recipeStrength[recipe];
+ const surface = appearance.surface;
+ const accent = appearance.accent;
+
+ return {
+ canvas: mixColors(background.color, surface, strength.canvas),
+ surfaceRaised: mixColors(surface, accent, strength.raised),
+ surfaceOverlay: mixColors(surface, accent, strength.overlay),
+ surfaceSunken: mixColors(surface, background.color, strength.sunken),
+ control: mixColors(surface, accent, strength.control),
+ controlHover: mixColors(surface, accent, strength.hover),
+ controlActive: mixColors(surface, accent, strength.active),
+ textTertiary: adaptiveReadableColor(
+ appearance.mutedText,
+ [surface, mixColors(surface, accent, strength.active)],
+ 3,
+ ),
+ onAccent: adaptiveReadableColor(surface, accent, 4.5),
+ borderSubtle: mixColors(surface, appearance.border, strength.borderSubtle),
+ borderStrong: mixColors(
+ appearance.border,
+ appearance.text,
+ strength.borderStrong,
+ ),
+ focus: accent,
+ };
+}
+
+export function resolveThemeColorHarmonyMode(
+ variant: ThemeVariant,
+): ThemeColorHarmonyMode {
+ if (!variant.appearance.palette) return "automatic";
+ if (
+ samePalette(
+ variant.appearance.palette,
+ createThemeColorHarmonyPalette(variant, "tonal"),
+ )
+ ) {
+ return "tonal";
+ }
+ if (
+ samePalette(
+ variant.appearance.palette,
+ createThemeColorHarmonyPalette(variant, "contrast"),
+ )
+ ) {
+ return "contrast";
+ }
+ return "authored";
+}
+
+export function assignThemeColorHarmony(
+ theme: ThemeDefinition,
+ variantName: ThemeVariantName,
+ recipe: ThemeColorHarmonyId,
+) {
+ const variant = theme.variants[variantName];
+ variant.appearance.palette =
+ recipe === "automatic"
+ ? undefined
+ : createThemeColorHarmonyPalette(variant, recipe);
+}
+
+export function themeColorHarmonySwatches(
+ variant: ThemeVariant,
+ recipe: ThemeColorHarmonyMode,
+): [string, string, string] {
+ if (recipe === "authored") {
+ const palette = variant.appearance.palette ?? {};
+ return [
+ palette.surfaceRaised ?? variant.appearance.surface,
+ palette.controlActive ?? variant.appearance.accent,
+ variant.appearance.accent,
+ ];
+ }
+ if (recipe === "automatic") {
+ const strength =
+ variant.appearance.decorations === "expressive"
+ ? 0.28
+ : variant.appearance.decorations === "subtle"
+ ? 0.18
+ : 0.12;
+ return [
+ variant.appearance.surface,
+ mixColors(
+ variant.appearance.surface,
+ variant.appearance.accent,
+ strength,
+ ),
+ variant.appearance.accent,
+ ];
+ }
+ const palette = createThemeColorHarmonyPalette(variant, recipe);
+ return [
+ palette.surfaceRaised ?? variant.appearance.surface,
+ palette.controlActive ?? variant.appearance.accent,
+ variant.appearance.accent,
+ ];
+}
diff --git a/apps/desktop/src/lib/theme-contrast.test.ts b/apps/desktop/src/lib/theme-contrast.test.ts
index 62fba5d..88ab16d 100644
--- a/apps/desktop/src/lib/theme-contrast.test.ts
+++ b/apps/desktop/src/lib/theme-contrast.test.ts
@@ -31,6 +31,18 @@ describe("theme contrast system", () => {
expect(system.quietSurfaceOpacity).toBeLessThan(0.64);
});
+ it("uses authored focus opacity for dialogs and other raised surfaces", () => {
+ const theme = structuredClone(nocturneStudio);
+ theme.variants.dark.appearance.focusOpacity = 0.97;
+
+ const system = resolveThemeContrast(theme, "dark");
+
+ expect(system.strongSurfaceOpacity).toBeGreaterThanOrEqual(0.97);
+ expect(system.strongSurfaceOpacity).toBeGreaterThan(
+ system.quietSurfaceOpacity,
+ );
+ });
+
it("keeps every built-in variant readable on its quiet and strong UI surfaces", () => {
for (const theme of builtinThemes) {
for (const variant of ["light", "dark"] as const) {
diff --git a/apps/desktop/src/lib/theme-contrast.ts b/apps/desktop/src/lib/theme-contrast.ts
index a576e2e..0e3758a 100644
--- a/apps/desktop/src/lib/theme-contrast.ts
+++ b/apps/desktop/src/lib/theme-contrast.ts
@@ -18,6 +18,7 @@ export interface ThemeContrastSystem {
textPrimary: string;
textSecondary: string;
textTertiary: string;
+ /** Foreground tone selected for the composite UI surfaces. */
tone: "light" | "dark";
}
@@ -114,6 +115,7 @@ export function resolveThemeContrast(
0.98,
Math.max(
authoredSurfaceOpacity,
+ appearance.focusOpacity,
quietSurfaceOpacity + (imageBacked ? 0.12 : 0.08),
),
);
diff --git a/apps/desktop/src/lib/theme-draft.test.ts b/apps/desktop/src/lib/theme-draft.test.ts
new file mode 100644
index 0000000..6a6a334
--- /dev/null
+++ b/apps/desktop/src/lib/theme-draft.test.ts
@@ -0,0 +1,32 @@
+import { describe, expect, it } from "vitest";
+import { builtinThemes } from "@codex-styler/theme-core";
+import { assignThemeVariantField } from "./theme-draft";
+
+describe("theme draft field updates", () => {
+ it("rebuilds component harmony after a base appearance color changes", () => {
+ const draft = structuredClone(
+ builtinThemes.find(
+ (theme) => theme.id === "codex-styler.gilded-grandeur",
+ )!,
+ );
+ expect(draft.variants.dark.appearance.palette).toBeDefined();
+
+ assignThemeVariantField(draft, "dark", "appearance", "accent", "#f4c95d");
+
+ expect(draft.variants.dark.appearance.accent).toBe("#f4c95d");
+ expect(draft.variants.dark.appearance.palette).toBeUndefined();
+ });
+
+ it("preserves an authored component palette for non-color adjustments", () => {
+ const draft = structuredClone(
+ builtinThemes.find(
+ (theme) => theme.id === "codex-styler.gilded-grandeur",
+ )!,
+ );
+ const authoredPalette = draft.variants.dark.appearance.palette;
+
+ assignThemeVariantField(draft, "dark", "appearance", "radius", 16);
+
+ expect(draft.variants.dark.appearance.palette).toEqual(authoredPalette);
+ });
+});
diff --git a/apps/desktop/src/lib/theme-draft.ts b/apps/desktop/src/lib/theme-draft.ts
new file mode 100644
index 0000000..951138d
--- /dev/null
+++ b/apps/desktop/src/lib/theme-draft.ts
@@ -0,0 +1,37 @@
+import type {
+ ThemeDefinition,
+ ThemeVariantName,
+} from "@codex-styler/theme-core";
+
+export type EditableThemeVariantSection =
+ "background" | "appearance" | "motion";
+
+const paletteAnchorFields = new Set([
+ "accent",
+ "surface",
+ "border",
+ "text",
+ "mutedText",
+]);
+
+/**
+ * Updates a cloned draft in place. Detailed component colors are derived from
+ * the five appearance anchors, so retaining them after an anchor edit would
+ * leave Codex panels, controls, and status colors on the previous palette.
+ */
+export function assignThemeVariantField(
+ theme: ThemeDefinition,
+ variant: ThemeVariantName,
+ section: EditableThemeVariantSection,
+ key: string,
+ value: number | string,
+) {
+ const target = theme.variants[variant][section] as unknown as Record<
+ string,
+ number | string | object | undefined
+ >;
+ target[key] = value;
+ if (section === "appearance" && paletteAnchorFields.has(key)) {
+ target.palette = undefined;
+ }
+}
diff --git a/apps/desktop/src/lib/theme-effects.test.ts b/apps/desktop/src/lib/theme-effects.test.ts
new file mode 100644
index 0000000..8a0714a
--- /dev/null
+++ b/apps/desktop/src/lib/theme-effects.test.ts
@@ -0,0 +1,145 @@
+import { describe, expect, it } from "vitest";
+import {
+ gildedGrandeur,
+ merryBigTop,
+ nativeRefined,
+} from "@codex-styler/theme-core";
+import {
+ resolveThemeEffectCoverage,
+ resolveThemeMotionProfile,
+ resolveThemeVisualPersonality,
+} from "./theme-effects";
+
+describe("resolveThemeEffectCoverage", () => {
+ it("reports the semantic systems a theme can actually style", () => {
+ const coverage = resolveThemeEffectCoverage(nativeRefined, "dark");
+
+ expect(coverage.find((effect) => effect.id === "background")?.active).toBe(
+ true,
+ );
+ expect(coverage.find((effect) => effect.id === "surfaces")?.active).toBe(
+ true,
+ );
+ expect(coverage.find((effect) => effect.id === "readability")?.active).toBe(
+ true,
+ );
+ expect(coverage.find((effect) => effect.id === "typography")?.active).toBe(
+ true,
+ );
+ });
+
+ it("does not promise semantic component styling for safe themes", () => {
+ const theme = structuredClone(nativeRefined);
+ theme.compatibility.codex.mode = "safe";
+
+ const coverage = resolveThemeEffectCoverage(theme, "dark");
+
+ expect(coverage.find((effect) => effect.id === "background")?.active).toBe(
+ true,
+ );
+ expect(coverage.find((effect) => effect.id === "surfaces")?.active).toBe(
+ false,
+ );
+ expect(coverage.find((effect) => effect.id === "controls")?.active).toBe(
+ false,
+ );
+ expect(coverage.find((effect) => effect.id === "typography")?.active).toBe(
+ false,
+ );
+
+ theme.variants.dark.motion.parallax = 0;
+ theme.scene.layers.forEach((layer) => {
+ layer.parallax = 0;
+ });
+ expect(
+ resolveThemeEffectCoverage(theme, "dark").find(
+ (effect) => effect.id === "motion",
+ )?.active,
+ ).toBe(false);
+ });
+
+ it("reports UI motion even when background parallax is disabled", () => {
+ const theme = structuredClone(nativeRefined);
+ theme.variants.dark.motion.intensity = 0.58;
+ theme.variants.dark.motion.parallax = 0;
+ theme.scene.layers.forEach((layer) => {
+ layer.parallax = 0;
+ });
+
+ expect(
+ resolveThemeEffectCoverage(theme, "dark").find(
+ (effect) => effect.id === "motion",
+ )?.active,
+ ).toBe(true);
+ });
+});
+
+describe("resolveThemeMotionProfile", () => {
+ it.each([
+ [-1, "still", 0, 0],
+ [0.2, "calm", 140, 1],
+ [0.55, "fluid", 190, 2],
+ [0.9, "expressive", 240, 3],
+ [4, "expressive", 240, 3],
+ ] as const)(
+ "maps intensity %s to the %s motion tier",
+ (intensity, character, durationMs, hoverLiftPx) => {
+ expect(resolveThemeMotionProfile(intensity)).toMatchObject({
+ character,
+ durationMs,
+ hoverLiftPx,
+ });
+ },
+ );
+});
+
+describe("resolveThemeVisualPersonality", () => {
+ it("derives architectural and soft treatments from portable controls", () => {
+ expect(resolveThemeVisualPersonality(gildedGrandeur, "dark")).toEqual({
+ layout: "editorial",
+ geometry: "precise",
+ material: "frosted",
+ iconStyle: "themed",
+ decorations: "expressive",
+ typography: "editorial",
+ motion: "fluid",
+ });
+ expect(resolveThemeVisualPersonality(merryBigTop, "dark")).toEqual({
+ layout: "immersive",
+ geometry: "soft",
+ material: "frosted",
+ iconStyle: "themed",
+ decorations: "expressive",
+ typography: "cinematic",
+ motion: "expressive",
+ });
+ });
+
+ it("keeps low-motion native themes restrained", () => {
+ const theme = structuredClone(nativeRefined);
+ theme.variants.dark.motion.intensity = 0;
+
+ expect(resolveThemeVisualPersonality(theme, "dark")).toMatchObject({
+ layout: "native",
+ geometry: "balanced",
+ typography: "balanced",
+ motion: "still",
+ });
+ });
+
+ it.each([
+ [{ surfaceOpacity: 0.96, focusOpacity: 0.98, focusBlur: 0 }, "solid"],
+ [{ surfaceOpacity: 0.88, focusOpacity: 0.94, focusBlur: 10 }, "layered"],
+ [{ surfaceOpacity: 0.78, focusOpacity: 0.9, focusBlur: 20 }, "frosted"],
+ ] as const)(
+ "derives a %s surface recipe as %s material",
+ (appearance, expectedMaterial) => {
+ const theme = structuredClone(nativeRefined);
+ Object.assign(theme.variants.dark.appearance, appearance);
+
+ expect(resolveThemeVisualPersonality(theme, "dark").material).toBe(
+ expectedMaterial,
+ );
+ },
+ );
+});
diff --git a/apps/desktop/src/lib/theme-effects.ts b/apps/desktop/src/lib/theme-effects.ts
new file mode 100644
index 0000000..3f395ba
--- /dev/null
+++ b/apps/desktop/src/lib/theme-effects.ts
@@ -0,0 +1,168 @@
+import type {
+ ThemeDefinition,
+ ThemeVariantName,
+} from "@codex-styler/theme-core";
+
+export type ThemeEffectId =
+ | "background"
+ | "surfaces"
+ | "controls"
+ | "icons"
+ | "typography"
+ | "motion"
+ | "readability";
+
+export interface ThemeEffectCoverage {
+ id: ThemeEffectId;
+ active: boolean;
+}
+
+export type ThemeGeometry = "precise" | "balanced" | "soft";
+export type ThemeMaterialCharacter = "solid" | "layered" | "frosted";
+export type ThemeMotionCharacter = "still" | "calm" | "fluid" | "expressive";
+export type ThemeTypographyCharacter = "balanced" | "editorial" | "cinematic";
+
+export interface ThemeMotionProfile {
+ character: ThemeMotionCharacter;
+ durationMs: number;
+ hoverLiftPx: number;
+ pressScale: number;
+ overlayOpacity: number;
+ overlayOffsetPx: number;
+ overlayScale: number;
+}
+
+export interface ThemeVisualPersonality {
+ layout: "native" | "editorial" | "immersive";
+ geometry: ThemeGeometry;
+ material: ThemeMaterialCharacter;
+ iconStyle: "native" | "contained" | "themed";
+ decorations: "none" | "subtle" | "expressive";
+ typography: ThemeTypographyCharacter;
+ motion: ThemeMotionCharacter;
+}
+
+/**
+ * Converts the portable intensity control into restrained, geometry-safe
+ * interaction parameters. These thresholds are mirrored by the standalone
+ * injected runtime; neither implementation is allowed to change layout size.
+ */
+export function resolveThemeMotionProfile(
+ intensity: number,
+): ThemeMotionProfile {
+ const normalized = Math.max(0, Math.min(1, intensity));
+ if (normalized <= 0.05) {
+ return {
+ character: "still",
+ durationMs: 0,
+ hoverLiftPx: 0,
+ pressScale: 1,
+ overlayOpacity: 1,
+ overlayOffsetPx: 0,
+ overlayScale: 1,
+ };
+ }
+ if (normalized < 0.4) {
+ return {
+ character: "calm",
+ durationMs: 140,
+ hoverLiftPx: 1,
+ pressScale: 0.995,
+ overlayOpacity: 0.96,
+ overlayOffsetPx: 4,
+ overlayScale: 0.995,
+ };
+ }
+ if (normalized < 0.72) {
+ return {
+ character: "fluid",
+ durationMs: 190,
+ hoverLiftPx: 2,
+ pressScale: 0.99,
+ overlayOpacity: 0.9,
+ overlayOffsetPx: 7,
+ overlayScale: 0.985,
+ };
+ }
+ return {
+ character: "expressive",
+ durationMs: 240,
+ hoverLiftPx: 3,
+ pressScale: 0.985,
+ overlayOpacity: 0.84,
+ overlayOffsetPx: 10,
+ overlayScale: 0.975,
+ };
+}
+
+/**
+ * Turns portable theme controls into a stable, user-facing visual character.
+ * The same thresholds are mirrored by the injected runtime so custom themes
+ * inherit the treatment without theme-id-specific CSS.
+ */
+export function resolveThemeVisualPersonality(
+ theme: ThemeDefinition,
+ variant: ThemeVariantName,
+): ThemeVisualPersonality {
+ const visual = theme.variants[variant];
+ const radius = visual.appearance.radius;
+ const motion = resolveThemeMotionProfile(visual.motion.intensity);
+ const materialOpacity = Math.min(
+ visual.appearance.surfaceOpacity,
+ visual.appearance.focusOpacity,
+ );
+ const material =
+ visual.appearance.focusBlur <= 4 && materialOpacity >= 0.93
+ ? "solid"
+ : visual.appearance.focusBlur >= 16 || materialOpacity <= 0.82
+ ? "frosted"
+ : "layered";
+
+ return {
+ layout: visual.appearance.layout ?? "native",
+ geometry: radius <= 11 ? "precise" : radius >= 17 ? "soft" : "balanced",
+ material,
+ iconStyle: visual.appearance.iconStyle ?? "native",
+ decorations: visual.appearance.decorations ?? "none",
+ typography:
+ visual.appearance.layout === "editorial"
+ ? "editorial"
+ : visual.appearance.layout === "immersive"
+ ? "cinematic"
+ : "balanced",
+ motion: motion.character,
+ };
+}
+
+/**
+ * Describes only effects that the runtime can actually deliver. Keeping this
+ * derived from the portable theme data prevents the library from advertising
+ * editor controls that are not mapped into Codex.
+ */
+export function resolveThemeEffectCoverage(
+ theme: ThemeDefinition,
+ variant: ThemeVariantName,
+): ThemeEffectCoverage[] {
+ const visual = theme.variants[variant];
+ const semantic = theme.compatibility.codex.mode === "semantic";
+ const hasMotionIntensity =
+ resolveThemeMotionProfile(visual.motion.intensity).character !== "still";
+ const hasSceneMotion =
+ (visual.motion.parallax ?? 0) > 0 &&
+ theme.scene.layers.some((layer) => Math.abs(layer.parallax) > 0);
+ const hasMotion = hasMotionIntensity && (semantic || hasSceneMotion);
+
+ return [
+ { id: "background", active: true },
+ { id: "surfaces", active: semantic },
+ { id: "controls", active: semantic },
+ {
+ id: "icons",
+ active:
+ semantic && (visual.appearance.iconStyle ?? "native") !== "native",
+ },
+ { id: "typography", active: semantic },
+ { id: "motion", active: hasMotion },
+ { id: "readability", active: semantic },
+ ];
+}
diff --git a/apps/desktop/src/lib/theme-preview-palette.test.ts b/apps/desktop/src/lib/theme-preview-palette.test.ts
new file mode 100644
index 0000000..752d966
--- /dev/null
+++ b/apps/desktop/src/lib/theme-preview-palette.test.ts
@@ -0,0 +1,255 @@
+import { describe, expect, it } from "vitest";
+import { builtinThemes, type ThemeAppearance } from "@codex-styler/theme-core";
+import { contrastRatio } from "./contrast";
+import {
+ resolveThemeContrast,
+ type ThemeContrastSystem,
+} from "./theme-contrast";
+import {
+ resolveThemePreviewPalette,
+ themeFeedbackBackgrounds,
+} from "./theme-preview-palette";
+
+const contrastSystem: ThemeContrastSystem = {
+ hasImageBackdrop: false,
+ quietSurfaceOpacity: 0.8,
+ strongSurfaceOpacity: 0.92,
+ quietBackgrounds: ["#1b1d22"],
+ strongBackgrounds: ["#1b1d22"],
+ textPrimary: "#f2f4f8",
+ textSecondary: "#c2c6ce",
+ textTertiary: "#9da2ad",
+ tone: "light",
+};
+
+function appearance(
+ decorations: ThemeAppearance["decorations"],
+): ThemeAppearance {
+ return {
+ accent: "#78a6ff",
+ surface: "#1b1d22",
+ surfaceOpacity: 0.88,
+ text: "#f2f4f8",
+ mutedText: "#c2c6ce",
+ border: "#3a3f49",
+ radius: 14,
+ focusOpacity: 0.94,
+ focusBlur: 12,
+ decorations,
+ };
+}
+
+describe("theme preview semantic palette", () => {
+ it("increases accent depth for expressive component treatments", () => {
+ const subtle = resolveThemePreviewPalette(
+ appearance("subtle"),
+ "#101217",
+ contrastSystem,
+ );
+ const expressive = resolveThemePreviewPalette(
+ appearance("expressive"),
+ "#101217",
+ contrastSystem,
+ );
+
+ expect(expressive.controlActive).not.toBe(subtle.controlActive);
+ expect(expressive.surfaceOverlay).not.toBe(subtle.surfaceOverlay);
+ });
+
+ it("preserves a readable authored surface and repairs unsafe ones", () => {
+ const authored = appearance("subtle");
+ authored.palette = {
+ surfaceRaised: "#252a34",
+ surfaceOverlay: "#ffffff",
+ };
+ const palette = resolveThemePreviewPalette(
+ authored,
+ "#101217",
+ contrastSystem,
+ );
+
+ expect(palette.surfaceRaised).toBe("#252a34");
+ expect(palette.surfaceOverlay).not.toBe("#ffffff");
+ });
+
+ it("always supplies readable text for accent controls", () => {
+ const palette = resolveThemePreviewPalette(
+ appearance("expressive"),
+ "#101217",
+ contrastSystem,
+ );
+
+ expect(contrastRatio(palette.onAccent, "#78a6ff")).toBeGreaterThanOrEqual(
+ 4.5,
+ );
+ });
+
+ it("preserves boundary hierarchy without allowing borders to disappear", () => {
+ const unsafe = appearance("subtle");
+ unsafe.border = unsafe.surface;
+ unsafe.palette = {
+ borderSubtle: unsafe.surface,
+ borderStrong: unsafe.surface,
+ };
+ const palette = resolveThemePreviewPalette(
+ unsafe,
+ "#101217",
+ contrastSystem,
+ );
+ const surfaces = [
+ palette.surface,
+ palette.surfaceRaised,
+ palette.surfaceOverlay,
+ palette.surfaceSunken,
+ palette.control,
+ palette.controlHover,
+ palette.controlActive,
+ ];
+
+ expect(
+ Math.min(...surfaces.map((item) => contrastRatio(palette.border, item))),
+ ).toBeGreaterThanOrEqual(1.49);
+ expect(
+ Math.min(
+ ...surfaces.map((item) => contrastRatio(palette.borderStrong, item)),
+ ),
+ ).toBeGreaterThanOrEqual(2.99);
+ });
+
+ it("keeps themed icons legible without snapping every icon to plain text", () => {
+ const themed = appearance("expressive");
+ themed.iconStyle = "themed";
+ themed.accent = "#36506F";
+ const palette = resolveThemePreviewPalette(
+ themed,
+ "#101217",
+ contrastSystem,
+ );
+ const surfaces = [
+ palette.surface,
+ palette.surfaceRaised,
+ palette.surfaceOverlay,
+ palette.control,
+ palette.controlHover,
+ palette.controlActive,
+ ];
+
+ expect(palette.icon).not.toBe(contrastSystem.textSecondary);
+ expect(palette.iconEmphasis).not.toBe(contrastSystem.textPrimary);
+ expect(
+ Math.min(...surfaces.map((item) => contrastRatio(palette.icon, item))),
+ ).toBeGreaterThanOrEqual(2.99);
+ expect(
+ Math.min(
+ ...surfaces.map((item) => contrastRatio(palette.iconEmphasis, item)),
+ ),
+ ).toBeGreaterThanOrEqual(2.99);
+ });
+
+ it("keeps functional and diff colors readable on focused surfaces", () => {
+ const palette = resolveThemePreviewPalette(
+ appearance("expressive"),
+ "#101217",
+ contrastSystem,
+ );
+
+ const surfaces = [
+ palette.surface,
+ palette.surfaceRaised,
+ palette.surfaceOverlay,
+ palette.surfaceSunken,
+ palette.control,
+ palette.controlHover,
+ palette.controlActive,
+ ];
+ [
+ palette.success,
+ palette.warning,
+ palette.danger,
+ palette.info,
+ palette.added,
+ palette.modified,
+ palette.deleted,
+ ].forEach((color) => {
+ expect(
+ Math.min(
+ ...[...surfaces, ...themeFeedbackBackgrounds(color, surfaces)].map(
+ (background) => contrastRatio(color, background),
+ ),
+ ),
+ ).toBeGreaterThanOrEqual(4.49);
+ });
+ });
+
+ it("keeps every authored built-in component palette readable and intact", () => {
+ for (const theme of builtinThemes) {
+ for (const variant of ["light", "dark"] as const) {
+ const appearance = theme.variants[variant].appearance;
+ const authored = appearance.palette!;
+ const contrast = resolveThemeContrast(theme, variant);
+ const palette = resolveThemePreviewPalette(
+ appearance,
+ theme.variants[variant].background.color,
+ contrast,
+ );
+
+ const authoredSurfaces = [
+ ["canvas", palette.canvas, authored.canvas],
+ ["raised", palette.surfaceRaised, authored.surfaceRaised],
+ ["overlay", palette.surfaceOverlay, authored.surfaceOverlay],
+ ["sunken", palette.surfaceSunken, authored.surfaceSunken],
+ ["control", palette.control, authored.control],
+ ["control hover", palette.controlHover, authored.controlHover],
+ ["control active", palette.controlActive, authored.controlActive],
+ ] as const;
+ authoredSurfaces.forEach(([label, resolved, expected]) => {
+ expect(resolved, `${theme.id} ${variant} ${label}`).toBe(expected);
+ });
+ [palette.icon, palette.iconEmphasis].forEach((color) => {
+ expect(
+ Math.min(
+ ...[
+ palette.surface,
+ palette.surfaceRaised,
+ palette.surfaceOverlay,
+ palette.surfaceSunken,
+ palette.control,
+ palette.controlHover,
+ palette.controlActive,
+ ].map((background) => contrastRatio(color, background)),
+ ),
+ `${theme.id} ${variant} icon color`,
+ ).toBeGreaterThanOrEqual(2.99);
+ });
+ [
+ palette.success,
+ palette.warning,
+ palette.danger,
+ palette.info,
+ palette.added,
+ palette.modified,
+ palette.deleted,
+ ].forEach((color) => {
+ const functionalSurfaces = [
+ palette.surface,
+ palette.surfaceRaised,
+ palette.surfaceOverlay,
+ palette.surfaceSunken,
+ palette.control,
+ palette.controlHover,
+ palette.controlActive,
+ ];
+ expect(
+ Math.min(
+ ...[
+ ...functionalSurfaces,
+ ...themeFeedbackBackgrounds(color, functionalSurfaces),
+ ].map((background) => contrastRatio(color, background)),
+ ),
+ `${theme.id} ${variant} functional color`,
+ ).toBeGreaterThanOrEqual(4.49);
+ });
+ }
+ }
+ });
+});
diff --git a/apps/desktop/src/lib/theme-preview-palette.ts b/apps/desktop/src/lib/theme-preview-palette.ts
new file mode 100644
index 0000000..1c1c1ce
--- /dev/null
+++ b/apps/desktop/src/lib/theme-preview-palette.ts
@@ -0,0 +1,269 @@
+import type { ThemeAppearance } from "@codex-styler/theme-core";
+import { adaptiveReadableColor, minimumContrast, mixColors } from "./contrast";
+import type { ThemeContrastSystem } from "./theme-contrast";
+
+export interface ThemePreviewPalette {
+ canvas: string;
+ surface: string;
+ surfaceRaised: string;
+ surfaceOverlay: string;
+ surfaceSunken: string;
+ control: string;
+ controlHover: string;
+ controlActive: string;
+ textPrimary: string;
+ textSecondary: string;
+ textTertiary: string;
+ icon: string;
+ iconEmphasis: string;
+ accent: string;
+ onAccent: string;
+ border: string;
+ borderSubtle: string;
+ borderStrong: string;
+ focus: string;
+ success: string;
+ warning: string;
+ danger: string;
+ info: string;
+ added: string;
+ modified: string;
+ deleted: string;
+}
+
+export type ThemeFeedbackRole =
+ "success" | "warning" | "danger" | "info" | "added" | "modified" | "deleted";
+
+// These are the actual foreground-bearing treatments in the Codex runtime:
+// inline diff marks use 11%, while status and validation surfaces use 16%.
+// Stronger 23–30% editor fills carry normal text and are checked by the base
+// text contract instead of forcing a status hue to fight its own background.
+export const themeFeedbackTintAmounts = [0.11, 0.16] as const;
+
+const profileStrength = {
+ none: {
+ raised: 0.04,
+ overlay: 0.07,
+ control: 0.05,
+ controlHover: 0.08,
+ controlActive: 0.12,
+ },
+ subtle: {
+ raised: 0.06,
+ overlay: 0.1,
+ control: 0.08,
+ controlHover: 0.13,
+ controlActive: 0.18,
+ },
+ expressive: {
+ raised: 0.1,
+ overlay: 0.16,
+ control: 0.12,
+ controlHover: 0.2,
+ controlActive: 0.28,
+ },
+} as const;
+
+export function preferredThemeIconColor(
+ appearance: ThemeAppearance,
+ contrastSystem: ThemeContrastSystem,
+ emphasis = false,
+): string {
+ const style = appearance.iconStyle ?? "native";
+ const base = emphasis
+ ? contrastSystem.textPrimary
+ : contrastSystem.textSecondary;
+ if (style === "native") return base;
+
+ const accentAmount =
+ style === "themed" ? (emphasis ? 0.72 : 0.58) : emphasis ? 0.5 : 0.36;
+ return mixColors(base, appearance.accent, accentAmount);
+}
+
+export function preferredThemeFeedbackColor(
+ appearance: ThemeAppearance,
+ contrastSystem: ThemeContrastSystem,
+ role: ThemeFeedbackRole,
+): string {
+ const custom = appearance.palette ?? {};
+ const defaults =
+ contrastSystem.tone === "light"
+ ? {
+ success: "#4BC47D",
+ warning: "#E7A645",
+ danger: "#F06A67",
+ info: "#83C3FF",
+ }
+ : {
+ success: "#197A43",
+ warning: "#9A5B12",
+ danger: "#B93232",
+ info: "#1F5F99",
+ };
+
+ if (role === "success") return custom.success ?? defaults.success;
+ if (role === "warning") return custom.warning ?? defaults.warning;
+ if (role === "danger") return custom.danger ?? defaults.danger;
+ if (role === "info") return custom.info ?? appearance.accent;
+ if (role === "added")
+ return custom.added ?? custom.success ?? defaults.success;
+ if (role === "modified")
+ return custom.modified ?? custom.warning ?? defaults.warning;
+ return custom.deleted ?? custom.danger ?? defaults.danger;
+}
+
+export function themeFeedbackBackgrounds(
+ foreground: string,
+ surfaces: string[],
+): string[] {
+ return surfaces.flatMap((surface) =>
+ themeFeedbackTintAmounts.map((amount) =>
+ mixColors(surface, foreground, amount),
+ ),
+ );
+}
+
+function adaptiveFeedbackColor(preferred: string, surfaces: string[]): string {
+ let resolved = adaptiveReadableColor(preferred, surfaces, 4.5);
+ for (let iteration = 0; iteration < 8; iteration += 1) {
+ const next = adaptiveReadableColor(
+ resolved,
+ [...surfaces, ...themeFeedbackBackgrounds(resolved, surfaces)],
+ 4.5,
+ );
+ if (next === resolved) break;
+ resolved = next;
+ }
+ return resolved;
+}
+
+export function resolveThemePreviewPalette(
+ appearance: ThemeAppearance,
+ backgroundColor: string,
+ contrastSystem: ThemeContrastSystem,
+): ThemePreviewPalette {
+ const custom = appearance.palette ?? {};
+ const strength =
+ profileStrength[appearance.decorations ?? "none"] ?? profileStrength.none;
+ const readableSurface = (candidate: string) =>
+ minimumContrast(contrastSystem.textPrimary, [candidate]) >= 4.5 &&
+ minimumContrast(contrastSystem.textSecondary, [candidate]) >= 4.5 &&
+ minimumContrast(contrastSystem.textTertiary, [candidate]) >= 3;
+ const safeSurface = (candidate: string | undefined, fallback: string) => {
+ if (candidate && readableSurface(candidate)) return candidate;
+ return readableSurface(fallback) ? fallback : appearance.surface;
+ };
+ const tint = (amount: number) =>
+ mixColors(appearance.surface, appearance.accent, amount);
+ const safeForeground = (candidate: string | undefined, fallback: string) =>
+ adaptiveReadableColor(
+ candidate ?? fallback,
+ contrastSystem.strongBackgrounds,
+ 4.5,
+ );
+
+ const canvas = safeSurface(custom.canvas, backgroundColor);
+ const surfaceRaised = safeSurface(
+ custom.surfaceRaised,
+ tint(strength.raised),
+ );
+ const surfaceOverlay = safeSurface(
+ custom.surfaceOverlay,
+ tint(strength.overlay),
+ );
+ const surfaceSunken = safeSurface(
+ custom.surfaceSunken,
+ mixColors(appearance.surface, backgroundColor, 0.12),
+ );
+ const control = safeSurface(custom.control, tint(strength.control));
+ const controlHover = safeSurface(
+ custom.controlHover,
+ tint(strength.controlHover),
+ );
+ const controlActive = safeSurface(
+ custom.controlActive,
+ tint(strength.controlActive),
+ );
+ const boundaryBackgrounds = [
+ appearance.surface,
+ surfaceRaised,
+ surfaceOverlay,
+ surfaceSunken,
+ control,
+ controlHover,
+ controlActive,
+ ];
+ // Keep low-emphasis separators quiet while guaranteeing that regular and
+ // strong component boundaries remain visible on every derived surface.
+ const border = adaptiveReadableColor(
+ appearance.border,
+ boundaryBackgrounds,
+ 1.5,
+ );
+ const borderSubtle = adaptiveReadableColor(
+ custom.borderSubtle ?? mixColors(appearance.surface, border, 0.72),
+ boundaryBackgrounds,
+ 1.25,
+ );
+ const borderStrong = adaptiveReadableColor(
+ custom.borderStrong ?? mixColors(border, contrastSystem.textPrimary, 0.3),
+ boundaryBackgrounds,
+ custom.borderStrong ? 3 : 2.5,
+ );
+ const icon = adaptiveReadableColor(
+ preferredThemeIconColor(appearance, contrastSystem),
+ boundaryBackgrounds,
+ 3,
+ );
+ const iconEmphasis = adaptiveReadableColor(
+ preferredThemeIconColor(appearance, contrastSystem, true),
+ boundaryBackgrounds,
+ 3,
+ );
+ const feedbackBackgrounds = boundaryBackgrounds;
+ const feedbackColor = (role: ThemeFeedbackRole) =>
+ adaptiveFeedbackColor(
+ preferredThemeFeedbackColor(appearance, contrastSystem, role),
+ feedbackBackgrounds,
+ );
+ const success = feedbackColor("success");
+ const warning = feedbackColor("warning");
+ const danger = feedbackColor("danger");
+ const info = feedbackColor("info");
+
+ return {
+ canvas,
+ surface: appearance.surface,
+ surfaceRaised,
+ surfaceOverlay,
+ surfaceSunken,
+ control,
+ controlHover,
+ controlActive,
+ textPrimary: contrastSystem.textPrimary,
+ textSecondary: contrastSystem.textSecondary,
+ textTertiary: contrastSystem.textTertiary,
+ icon,
+ iconEmphasis,
+ accent: appearance.accent,
+ onAccent: adaptiveReadableColor(
+ custom.onAccent ?? appearance.surface,
+ appearance.accent,
+ 4.5,
+ ),
+ border,
+ borderSubtle,
+ borderStrong,
+ focus: safeForeground(
+ custom.focus ?? appearance.accent,
+ contrastSystem.tone === "light" ? "#83C3FF" : "#1F5F99",
+ ),
+ success,
+ warning,
+ danger,
+ info,
+ added: feedbackColor("added"),
+ modified: feedbackColor("modified"),
+ deleted: feedbackColor("deleted"),
+ };
+}
diff --git a/apps/desktop/src/lib/theme-visual-quality.test.ts b/apps/desktop/src/lib/theme-visual-quality.test.ts
new file mode 100644
index 0000000..63384a8
--- /dev/null
+++ b/apps/desktop/src/lib/theme-visual-quality.test.ts
@@ -0,0 +1,80 @@
+import { describe, expect, it } from "vitest";
+import { builtinThemes } from "@codex-styler/theme-core";
+import { resolveThemeVisualQuality } from "./theme-visual-quality";
+
+describe("theme visual quality", () => {
+ it("verifies the final rendered roles for every built-in variant", () => {
+ for (const theme of builtinThemes) {
+ for (const variant of ["light", "dark"] as const) {
+ const report = resolveThemeVisualQuality(theme, variant);
+
+ report.checks.forEach((check) => {
+ expect(
+ check.ratio,
+ `${theme.id} ${variant} ${check.id}`,
+ ).toBeGreaterThanOrEqual(check.minimum - 0.01);
+ });
+ }
+ }
+ });
+
+ it("reports render-time repairs without mutating authored theme colors", () => {
+ const theme = structuredClone(builtinThemes[0]);
+ const appearance = theme.variants.dark.appearance;
+ const authored = {
+ text: appearance.text,
+ mutedText: appearance.mutedText,
+ border: appearance.border,
+ };
+ appearance.text = appearance.surface;
+ appearance.mutedText = appearance.surface;
+ appearance.border = appearance.surface;
+
+ const report = resolveThemeVisualQuality(theme, "dark");
+
+ expect(report.protectedCount).toBeGreaterThanOrEqual(3);
+ expect(appearance.text).toBe(appearance.surface);
+ expect(appearance.mutedText).toBe(appearance.surface);
+ expect(appearance.border).toBe(appearance.surface);
+ expect(authored.text).not.toBe(appearance.text);
+ expect(authored.mutedText).not.toBe(appearance.mutedText);
+ expect(authored.border).not.toBe(appearance.border);
+ });
+
+ it("exposes the effective image surface guard", () => {
+ const theme = structuredClone(
+ builtinThemes.find((item) => item.id.includes("nocturne"))!,
+ );
+ theme.variants.dark.appearance.surfaceOpacity = 0.4;
+
+ const report = resolveThemeVisualQuality(theme, "dark");
+
+ expect(report.hasImageBackdrop).toBe(true);
+ expect(report.effectiveSurfaceOpacity).toBeGreaterThan(
+ report.authoredSurfaceOpacity,
+ );
+ });
+
+ it("reports unsafe authored status and diff hues as protected", () => {
+ const theme = structuredClone(builtinThemes[0]);
+ const appearance = theme.variants.dark.appearance;
+ appearance.palette = {
+ ...appearance.palette,
+ success: appearance.surface,
+ added: appearance.surface,
+ };
+
+ const report = resolveThemeVisualQuality(theme, "dark");
+ const status = report.checks.find(
+ (check) => check.id === "status-feedback",
+ );
+ const diff = report.checks.find((check) => check.id === "diff-feedback");
+
+ expect(status).toMatchObject({ minimum: 4.5, protected: true });
+ expect(diff).toMatchObject({ minimum: 4.5, protected: true });
+ expect(status!.ratio).toBeGreaterThanOrEqual(4.49);
+ expect(diff!.ratio).toBeGreaterThanOrEqual(4.49);
+ expect(appearance.palette.success).toBe(appearance.surface);
+ expect(appearance.palette.added).toBe(appearance.surface);
+ });
+});
diff --git a/apps/desktop/src/lib/theme-visual-quality.ts b/apps/desktop/src/lib/theme-visual-quality.ts
new file mode 100644
index 0000000..2974608
--- /dev/null
+++ b/apps/desktop/src/lib/theme-visual-quality.ts
@@ -0,0 +1,157 @@
+import type {
+ ThemeDefinition,
+ ThemeVariantName,
+} from "@codex-styler/theme-core";
+import { minimumContrast } from "./contrast";
+import { resolveThemeContrast } from "./theme-contrast";
+import {
+ preferredThemeIconColor,
+ preferredThemeFeedbackColor,
+ resolveThemePreviewPalette,
+ themeFeedbackBackgrounds,
+ type ThemeFeedbackRole,
+} from "./theme-preview-palette";
+
+export type ThemeVisualQualityCheckId =
+ | "primary-text"
+ | "secondary-text"
+ | "icons"
+ | "status-feedback"
+ | "diff-feedback"
+ | "accent-content"
+ | "boundaries";
+
+export interface ThemeVisualQualityCheck {
+ id: ThemeVisualQualityCheckId;
+ ratio: number;
+ minimum: number;
+ protected: boolean;
+}
+
+export interface ThemeVisualQualityReport {
+ checks: ThemeVisualQualityCheck[];
+ protectedCount: number;
+ hasImageBackdrop: boolean;
+ authoredSurfaceOpacity: number;
+ effectiveSurfaceOpacity: number;
+}
+
+export function resolveThemeVisualQuality(
+ theme: ThemeDefinition,
+ variant: ThemeVariantName,
+): ThemeVisualQualityReport {
+ const visual = theme.variants[variant];
+ const contrast = resolveThemeContrast(theme, variant);
+ const palette = resolveThemePreviewPalette(
+ visual.appearance,
+ visual.background.color,
+ contrast,
+ );
+ const semanticSurfaces = [
+ ...contrast.quietBackgrounds,
+ ...contrast.strongBackgrounds,
+ palette.canvas,
+ palette.surfaceRaised,
+ palette.surfaceOverlay,
+ palette.surfaceSunken,
+ palette.control,
+ palette.controlHover,
+ palette.controlActive,
+ ];
+ const boundarySurfaces = [
+ visual.appearance.surface,
+ palette.surfaceRaised,
+ palette.surfaceOverlay,
+ palette.surfaceSunken,
+ palette.control,
+ palette.controlHover,
+ palette.controlActive,
+ ];
+ const authoredBoundaryPalette = visual.appearance.palette;
+ const boundariesProtected =
+ palette.border !== visual.appearance.border ||
+ (Boolean(authoredBoundaryPalette?.borderSubtle) &&
+ palette.borderSubtle !== authoredBoundaryPalette?.borderSubtle) ||
+ (Boolean(authoredBoundaryPalette?.borderStrong) &&
+ palette.borderStrong !== authoredBoundaryPalette?.borderStrong);
+ const feedbackRatio = (roles: ThemeFeedbackRole[]) =>
+ Math.min(
+ ...roles.map((role) => {
+ const foreground = palette[role];
+ return minimumContrast(foreground, [
+ ...boundarySurfaces,
+ ...themeFeedbackBackgrounds(foreground, boundarySurfaces),
+ ]);
+ }),
+ );
+ const feedbackProtected = (roles: ThemeFeedbackRole[]) =>
+ roles.some(
+ (role) =>
+ palette[role] !==
+ preferredThemeFeedbackColor(visual.appearance, contrast, role),
+ );
+ const checks: ThemeVisualQualityCheck[] = [
+ {
+ id: "primary-text",
+ ratio: minimumContrast(palette.textPrimary, semanticSurfaces),
+ minimum: 4.5,
+ protected: palette.textPrimary !== visual.appearance.text,
+ },
+ {
+ id: "secondary-text",
+ ratio: minimumContrast(palette.textSecondary, semanticSurfaces),
+ minimum: 4.5,
+ protected: palette.textSecondary !== visual.appearance.mutedText,
+ },
+ {
+ id: "icons",
+ ratio: Math.min(
+ minimumContrast(palette.icon, boundarySurfaces),
+ minimumContrast(palette.iconEmphasis, boundarySurfaces),
+ ),
+ minimum: 3,
+ protected:
+ palette.icon !== preferredThemeIconColor(visual.appearance, contrast) ||
+ palette.iconEmphasis !==
+ preferredThemeIconColor(visual.appearance, contrast, true),
+ },
+ {
+ id: "status-feedback",
+ ratio: feedbackRatio(["success", "warning", "danger", "info"]),
+ minimum: 4.5,
+ protected: feedbackProtected(["success", "warning", "danger", "info"]),
+ },
+ {
+ id: "diff-feedback",
+ ratio: feedbackRatio(["added", "modified", "deleted"]),
+ minimum: 4.5,
+ protected: feedbackProtected(["added", "modified", "deleted"]),
+ },
+ {
+ id: "accent-content",
+ ratio: minimumContrast(palette.onAccent, [palette.accent]),
+ minimum: 4.5,
+ protected:
+ palette.onAccent !==
+ (visual.appearance.palette?.onAccent ?? visual.appearance.surface),
+ },
+ {
+ id: "boundaries",
+ ratio: minimumContrast(palette.border, boundarySurfaces),
+ minimum: 1.5,
+ protected: boundariesProtected,
+ },
+ ];
+
+ return {
+ checks,
+ protectedCount:
+ checks.filter((check) => check.protected).length +
+ (contrast.quietSurfaceOpacity > visual.appearance.surfaceOpacity + 0.005
+ ? 1
+ : 0),
+ hasImageBackdrop: contrast.hasImageBackdrop,
+ authoredSurfaceOpacity: visual.appearance.surfaceOpacity,
+ effectiveSurfaceOpacity: contrast.quietSurfaceOpacity,
+ };
+}
diff --git a/apps/desktop/src/lib/use-guided-motion-preview.ts b/apps/desktop/src/lib/use-guided-motion-preview.ts
new file mode 100644
index 0000000..8cb8aae
--- /dev/null
+++ b/apps/desktop/src/lib/use-guided-motion-preview.ts
@@ -0,0 +1,35 @@
+import { useCallback, useEffect, useRef, useState } from "react";
+
+const DEFAULT_PREVIEW_DURATION_MS = 1_500;
+
+export function useGuidedMotionPreview(
+ durationMs = DEFAULT_PREVIEW_DURATION_MS,
+) {
+ const timerRef = useRef(null);
+ const [revision, setRevision] = useState(0);
+ const [playing, setPlaying] = useState(false);
+
+ const stop = useCallback(() => {
+ if (timerRef.current !== null) {
+ window.clearTimeout(timerRef.current);
+ timerRef.current = null;
+ }
+ setPlaying(false);
+ }, []);
+
+ const play = useCallback(() => {
+ if (timerRef.current !== null) {
+ window.clearTimeout(timerRef.current);
+ }
+ setPlaying(true);
+ setRevision((current) => current + 1);
+ timerRef.current = window.setTimeout(() => {
+ timerRef.current = null;
+ setPlaying(false);
+ }, durationMs);
+ }, [durationMs]);
+
+ useEffect(() => stop, [stop]);
+
+ return { revision, playing, play, stop };
+}
diff --git a/apps/desktop/src/main.tsx b/apps/desktop/src/main.tsx
index 5c2e66b..6095143 100644
--- a/apps/desktop/src/main.tsx
+++ b/apps/desktop/src/main.tsx
@@ -12,6 +12,7 @@ import "./styles/creator-beta3.css";
import "./styles/interaction-surfaces.css";
import "./styles/typography.css";
import "./styles/experience-beta6.css";
+import "./styles/scroll-surfaces.css";
createRoot(document.getElementById("root")!).render(
diff --git a/apps/desktop/src/runtime-injection.test.ts b/apps/desktop/src/runtime-injection.test.ts
index a4d4f73..52f8a32 100644
--- a/apps/desktop/src/runtime-injection.test.ts
+++ b/apps/desktop/src/runtime-injection.test.ts
@@ -1,8 +1,10 @@
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
-import { nativeRefined } from "@codex-styler/theme-core";
+import { nativeRefined, type SceneEntity } from "@codex-styler/theme-core";
import runtimeSource from "../src-tauri/src/runtime.js?raw";
-import { contrastRatio } from "./lib/contrast";
+import { contrastRatio, minimumContrast } from "./lib/contrast";
import { resolveThemeContrast } from "./lib/theme-contrast";
+import { assignThemeColorHarmony } from "./lib/theme-color-harmony";
+import { resolveThemePreviewPalette } from "./lib/theme-preview-palette";
import { codexFixture, portalFixture } from "./test/fixtures/codex-dom";
interface InjectedRuntime {
@@ -25,18 +27,84 @@ interface InjectedRuntime {
restore: () => void;
}
+interface InjectedRuntimeInternals {
+ semanticPalette: (
+ appearance: (typeof nativeRefined)["variants"]["light"]["appearance"],
+ background: (typeof nativeRefined)["variants"]["light"]["background"],
+ contrastSystem: ReturnType,
+ ) => ReturnType;
+}
+
function runtime(): InjectedRuntime {
return (
window as typeof window & { __CODEX_STYLER_RUNTIME__: InjectedRuntime }
).__CODEX_STYLER_RUNTIME__;
}
+function runtimeInternals(): InjectedRuntimeInternals {
+ return (
+ window as typeof window & {
+ __CODEX_STYLER_RUNTIME_INTERNALS__: InjectedRuntimeInternals;
+ }
+ ).__CODEX_STYLER_RUNTIME_INTERNALS__;
+}
+
+const TEST_IMAGE =
+ "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAA" +
+ "DUlEQVR42mNk+M/wHwAF/gL+X2NDWQAAAABJRU5ErkJggg==";
+
+function testSpriteEntity(overrides: Partial = {}): SceneEntity {
+ return {
+ id: "test-companion",
+ name: "Test companion",
+ renderer: {
+ type: "sprite-atlas",
+ asset: TEST_IMAGE,
+ pages: [TEST_IMAGE],
+ columns: 2,
+ rows: 2,
+ framesPerPage: 4,
+ frameWidth: 1,
+ frameHeight: 1,
+ directions: 4,
+ frameCount: 4,
+ poses: [
+ { id: "pose-up", angle: 0, frame: 0 },
+ { id: "pose-right", angle: 90, frame: 1 },
+ { id: "pose-down", angle: 180, frame: 2 },
+ { id: "pose-left", angle: 270, frame: 3 },
+ ],
+ idleClips: [
+ {
+ id: "blink",
+ poseIds: ["pose-up"],
+ frames: [{ frame: 0, durationMs: 80 }],
+ minimumDelayMs: 500,
+ maximumDelayMs: 1000,
+ },
+ ],
+ neutralFrame: 0,
+ reducedMotionFrame: 0,
+ transitionFps: 30,
+ normalization: "preserve",
+ alphaThreshold: 12,
+ },
+ behaviors: ["idle", "look-at-pointer", "reduce-motion-fallback"],
+ anchor: { x: 80, y: 70 },
+ size: 80,
+ opacity: 1,
+ ...overrides,
+ };
+}
+
describe("injected compatibility runtime", () => {
beforeEach(() => {
document.head.innerHTML = "";
document.body.innerHTML = "";
delete (window as unknown as Record)
.__CODEX_STYLER_RUNTIME__;
+ delete (window as unknown as Record)
+ .__CODEX_STYLER_RUNTIME_INTERNALS__;
vi.stubGlobal("requestAnimationFrame", (callback: FrameRequestCallback) => {
callback(performance.now() + 20);
return 1;
@@ -47,6 +115,8 @@ describe("injected compatibility runtime", () => {
afterEach(() => {
runtime().restore();
+ delete (window as unknown as Record)
+ .__CODEX_STYLER_RUNTIME_INTERNALS__;
vi.restoreAllMocks();
vi.unstubAllGlobals();
});
@@ -70,12 +140,136 @@ describe("injected compatibility runtime", () => {
window as typeof window & {
__CODEX_STYLER_RUNTIME__: { version: number; restore: () => void };
}
- ).__CODEX_STYLER_RUNTIME__ = { version: 8, restore };
+ ).__CODEX_STYLER_RUNTIME__ = { version: 20, restore };
Function(runtimeSource)();
expect(restore).toHaveBeenCalledOnce();
- expect(runtime().version).toBe(18);
+ expect(runtime().version).toBe(36);
+ });
+
+ it.each([
+ [{ surfaceOpacity: 0.96, focusOpacity: 0.98, focusBlur: 0 }, "solid"],
+ [{ surfaceOpacity: 0.88, focusOpacity: 0.94, focusBlur: 10 }, "layered"],
+ [{ surfaceOpacity: 0.78, focusOpacity: 0.9, focusBlur: 20 }, "frosted"],
+ ] as const)(
+ "derives the %s surface recipe as %s material",
+ async (appearance, expectedMaterial) => {
+ const theme = structuredClone(nativeRefined);
+ Object.assign(theme.variants.dark.appearance, appearance);
+
+ await runtime().apply(theme, "dark", "compatibility");
+
+ expect(document.documentElement.dataset.codexStylerMaterial).toBe(
+ expectedMaterial,
+ );
+ },
+ );
+
+ it.each([
+ ["native", "balanced"],
+ ["editorial", "editorial"],
+ ["immersive", "cinematic"],
+ ] as const)(
+ "derives the %s layout as %s typography without extending the theme format",
+ async (layout, expectedTypography) => {
+ const theme = structuredClone(nativeRefined);
+ theme.variants.dark.appearance.layout = layout;
+
+ await runtime().apply(theme, "dark", "compatibility");
+
+ expect(document.documentElement.dataset.codexStylerTypography).toBe(
+ expectedTypography,
+ );
+ },
+ );
+
+ it.each(["light", "dark"] as const)(
+ "keeps the %s preview palette identical to the injected runtime",
+ (variant) => {
+ const theme = structuredClone(nativeRefined);
+ const visual = theme.variants[variant];
+ const contrastSystem = resolveThemeContrast(theme, variant);
+ const previewPalette = resolveThemePreviewPalette(
+ visual.appearance,
+ visual.background.color,
+ contrastSystem,
+ );
+
+ expect(
+ runtimeInternals().semanticPalette(
+ visual.appearance,
+ visual.background,
+ contrastSystem,
+ ),
+ ).toEqual(previewPalette);
+ },
+ );
+
+ it.each(["tonal", "contrast"] as const)(
+ "keeps the %s color harmony identical in preview and runtime",
+ (recipe) => {
+ const theme = structuredClone(nativeRefined);
+ assignThemeColorHarmony(theme, "dark", recipe);
+ const visual = theme.variants.dark;
+ const contrastSystem = resolveThemeContrast(theme, "dark");
+
+ expect(
+ runtimeInternals().semanticPalette(
+ visual.appearance,
+ visual.background,
+ contrastSystem,
+ ),
+ ).toEqual(
+ resolveThemePreviewPalette(
+ visual.appearance,
+ visual.background.color,
+ contrastSystem,
+ ),
+ );
+ },
+ );
+
+ it("derives functional colors from the actual surface tone instead of the variant label", () => {
+ const theme = structuredClone(nativeRefined);
+ const visual = theme.variants.light;
+ visual.background.color = "#090B10";
+ visual.background.overlay = "#090B10";
+ visual.background.overlayOpacity = 1;
+ visual.appearance.surface = "#151820";
+ visual.appearance.text = "#F4F6FA";
+ visual.appearance.mutedText = "#C2C8D2";
+ visual.appearance.palette = {
+ ...visual.appearance.palette,
+ success: "#14311F",
+ warning: "#3A2910",
+ };
+ const contrastSystem = resolveThemeContrast(theme, "light");
+ const previewPalette = resolveThemePreviewPalette(
+ visual.appearance,
+ visual.background.color,
+ contrastSystem,
+ );
+ const injectedPalette = runtimeInternals().semanticPalette(
+ visual.appearance,
+ visual.background,
+ contrastSystem,
+ );
+
+ expect(contrastSystem.tone).toBe("light");
+ expect(injectedPalette).toEqual(previewPalette);
+ expect(
+ minimumContrast(
+ injectedPalette.success,
+ contrastSystem.strongBackgrounds,
+ ),
+ ).toBeGreaterThanOrEqual(4.49);
+ expect(
+ minimumContrast(
+ injectedPalette.warning,
+ contrastSystem.strongBackgrounds,
+ ),
+ ).toBeGreaterThanOrEqual(4.49);
});
it("renders validated scene layers and updates parallax without blocking Codex", async () => {
@@ -116,6 +310,9 @@ describe("injected compatibility runtime", () => {
);
expect(gradient?.style.transform).toContain("translate(");
+ window.dispatchEvent(new Event("blur"));
+ expect(gradient?.style.transform).toBe("");
+
runtime().restore();
expect(document.querySelector(".cs-layer")).toBeNull();
});
@@ -221,6 +418,13 @@ describe("injected compatibility runtime", () => {
expect(document.documentElement.dataset.codexStylerPage).toBe("home");
expect(document.documentElement.dataset.codexStylerLayout).toBe("native");
expect(document.documentElement.dataset.codexStylerIcons).toBe("contained");
+ expect(document.documentElement.dataset.codexStylerGeometry).toBe(
+ "balanced",
+ );
+ expect(document.documentElement.dataset.codexStylerTypography).toBe(
+ "balanced",
+ );
+ expect(document.documentElement.dataset.codexStylerMotion).toBe("fluid");
const stylesheet = document.getElementById("codex-styler-runtime-style");
expect(stylesheet?.textContent).toContain("main.main-surface article");
expect(stylesheet?.textContent).toContain(
@@ -262,6 +466,63 @@ describe("injected compatibility runtime", () => {
expect(stylesheet?.textContent).toContain(
"main.main-surface > header:not(.app-header-tint)",
);
+ expect(stylesheet?.textContent).toContain(
+ '[role="tab"][aria-selected="true"]',
+ );
+ expect(stylesheet?.textContent).toContain(":focus-visible");
+ expect(stylesheet?.textContent).toContain("accent-color:");
+ expect(stylesheet?.textContent).toContain('[aria-disabled="true"]');
+ expect(stylesheet?.textContent).toContain('[aria-invalid="true"]');
+ expect(stylesheet?.textContent).toContain('[data-state="active"]');
+ expect(stylesheet?.textContent).toContain(
+ "progress::-webkit-progress-value",
+ );
+ expect(stylesheet?.textContent).toContain("::-webkit-scrollbar-thumb");
+ expect(
+ stylesheet?.textContent?.match(/scrollbar-width: thin/g),
+ ).toHaveLength(1);
+ expect(stylesheet?.textContent).toContain(
+ "body > [data-codex-styler-overlay-root] *::-webkit-scrollbar-thumb",
+ );
+ expect(stylesheet?.textContent).toContain(
+ "--codex-styler-scrollbar-thumb: color-mix(in srgb, var(--codex-styler-accent)",
+ );
+ expect(stylesheet?.textContent).toContain(
+ 'data-codex-styler-decorations="expressive"',
+ );
+ expect(stylesheet?.textContent).toContain(
+ '[contenteditable="true"] {\n caret-color:',
+ );
+ expect(stylesheet?.textContent).toContain(':is(a, [role="link"]):hover');
+ expect(stylesheet?.textContent).toContain("::target-text");
+ expect(stylesheet?.textContent).toContain("[data-sonner-toast]");
+ expect(stylesheet?.textContent).toContain('[role="tooltip"]');
+ expect(stylesheet?.textContent).toContain(":not(pre) > code");
+ expect(stylesheet?.textContent).toContain(
+ "body > [data-codex-styler-app-root] table",
+ );
+ expect(stylesheet?.textContent).toContain(
+ "body > [data-codex-styler-app-root] details",
+ );
+ expect(stylesheet?.textContent).toContain(
+ '[data-message-author-role="user"]',
+ );
+ expect(stylesheet?.textContent).toContain(
+ "var(--codex-styler-control-active)",
+ );
+ expect(stylesheet?.textContent).toContain("details[open] > summary");
+ expect(stylesheet?.textContent).toContain(
+ "body > [data-codex-styler-app-root] ins",
+ );
+ expect(stylesheet?.textContent).toContain(
+ "body > [data-codex-styler-app-root] del",
+ );
+ expect(stylesheet?.textContent).toContain(":is(samp, output)");
+ expect(stylesheet?.textContent).toContain('[role="progressbar"]');
+ expect(stylesheet?.textContent).toContain('[role="treeitem"]');
+ expect(stylesheet?.textContent).toContain('[role="gridcell"]');
+ expect(stylesheet?.textContent).toContain("fieldset");
+ expect(stylesheet?.textContent).toContain("figcaption");
const appHeaderRule = stylesheet?.textContent
?.split("header.app-header-tint {")[1]
?.split("}")[0];
@@ -274,16 +535,92 @@ describe("injected compatibility runtime", () => {
'data-codex-styler-layout="immersive"] main.main-surface',
);
expect(stylesheet?.textContent).toContain(
- 'data-codex-styler-icons="contained"] aside.app-shell-left-panel button > svg:first-child',
+ 'data-codex-styler-icons="contained"] body > [data-codex-styler-app-root] :is(button, [role="button"], [role="tab"], [role="menuitem"], [role="option"])',
+ );
+ expect(stylesheet?.textContent).toContain(
+ 'data-codex-styler-icons="themed"] body > [data-codex-styler-overlay-root] :is(button, [role="button"], [role="tab"], [role="menuitem"], [role="option"])',
+ );
+ expect(stylesheet?.textContent).toContain(
+ "color: var(--codex-styler-icon) !important",
+ );
+ expect(stylesheet?.textContent).toContain(
+ "color: var(--codex-styler-icon-emphasis) !important",
+ );
+ expect(stylesheet?.textContent).toContain(
+ ':not([data-variant="destructive"]):not([data-tone="danger"]):not([data-state="error"]):not([data-brand])',
);
expect(stylesheet?.textContent).toContain(
'data-codex-styler-decorations="subtle"] main.main-surface',
);
+ expect(stylesheet?.textContent).toContain(
+ 'data-codex-styler-page="settings"] body > [data-codex-styler-app-root] .main-surface',
+ );
+ expect(stylesheet?.textContent).toContain(
+ 'data-codex-styler-decorations="expressive"] :is(\n .composer-surface-chrome',
+ );
+ expect(stylesheet?.textContent).toContain(
+ 'data-codex-styler-geometry="precise"] body > [data-codex-styler-app-root] [role="tablist"]',
+ );
+ expect(stylesheet?.textContent).toContain(
+ 'data-codex-styler-geometry="soft"] body > [data-codex-styler-app-root] [role="tab"]',
+ );
+ expect(stylesheet?.textContent).toContain(
+ 'data-codex-styler-material="solid"]',
+ );
+ expect(stylesheet?.textContent).toContain(
+ 'data-codex-styler-material="frosted"]',
+ );
+ expect(stylesheet?.textContent).toContain(
+ "background: var(--codex-styler-material-raised) !important",
+ );
+ expect(stylesheet?.textContent).toContain(
+ "background: var(--codex-styler-material-overlay) !important",
+ );
+ expect(stylesheet?.textContent).toContain(
+ "background: var(--codex-styler-material-sunken) !important",
+ );
+ expect(stylesheet?.textContent).toContain(
+ 'data-codex-styler-typography="editorial"]',
+ );
+ expect(stylesheet?.textContent).toContain(
+ "line-height: var(--codex-styler-content-leading) !important",
+ );
+ const typographyTreatments = stylesheet?.textContent
+ ?.split("/* Theme typography is content-scoped")[1]
+ ?.split(
+ 'html[data-codex-styler][data-codex-styler-mode="semantic"] body > [data-codex-styler-app-root] :is(fieldset',
+ )[0];
+ expect(typographyTreatments).toBeDefined();
+ expect(typographyTreatments).toContain("[data-message-author-role]");
+ expect(typographyTreatments).toContain(".ProseMirror");
+ expect(typographyTreatments).toContain("html:lang(zh)");
+ expect(typographyTreatments).not.toMatch(
+ /(?:^|\n)\s*(?:font-family|font-size|width|height|padding|margin)\s*:/,
+ );
+ expect(stylesheet?.textContent).toContain(
+ "--codex-styler-motion-duration: 190ms",
+ );
const iconTreatments = stylesheet?.textContent
- ?.split("/* Icon treatments preserve")[1]
+ ?.split("/*\n * Icon treatments are semantic")[1]
?.split("/* Full home composition")[0];
+ expect(iconTreatments).toBeDefined();
expect(iconTreatments).not.toMatch(
- /(?:^|\n)\s*(?:width|height|padding|background)\s*:/,
+ /(?:^|\n)\s*(?:width|height|padding|background|stroke-width)\s*:/,
+ );
+ expect(iconTreatments).toContain("box-shadow: 0 0 0 3px");
+ const decorationTreatments = stylesheet?.textContent
+ ?.split("/* Decoration depth is intentionally geometry-safe")[1]
+ ?.split(
+ 'html[data-codex-styler][data-codex-styler-mode="semantic"] ::selection',
+ )[0];
+ expect(decorationTreatments).toBeDefined();
+ expect(decorationTreatments).toContain(
+ '[data-pip-obstacle="thread-summary-panel"]',
+ );
+ expect(decorationTreatments).toContain('[role="alertdialog"]');
+ expect(decorationTreatments).toContain('[role="tabpanel"]');
+ expect(decorationTreatments).not.toMatch(
+ /(?:^|\n)\s*(?:position|display|width|height|z-index|inset)\s*:/,
);
expect(stylesheet?.textContent).not.toContain("body > div:first-child");
expect(stylesheet?.textContent).toContain(
@@ -426,6 +763,12 @@ describe("injected compatibility runtime", () => {
controlHover: "#303741",
success: "#52C982",
};
+ const contrastSystem = resolveThemeContrast(theme, "dark");
+ const previewPalette = resolveThemePreviewPalette(
+ theme.variants.dark.appearance,
+ theme.variants.dark.background.color,
+ contrastSystem,
+ );
await runtime().apply(theme, "dark", "developer");
@@ -438,7 +781,15 @@ describe("injected compatibility runtime", () => {
"--codex-styler-control-hover: #303741",
);
expect(stylesheet?.textContent).toContain(
- "--codex-styler-success: #52C982",
+ `--codex-styler-success: ${previewPalette.success}`,
+ );
+ expect(stylesheet?.textContent).toContain("--codex-styler-icon:");
+ expect(stylesheet?.textContent).toContain("--codex-styler-icon-emphasis:");
+ expect(stylesheet?.textContent).toContain(
+ "--color-icon-primary: var(--codex-styler-icon-emphasis)",
+ );
+ expect(stylesheet?.textContent).toContain(
+ "--color-background-status-success: color-mix(in srgb, var(--codex-styler-success) 16%, var(--codex-styler-surface))",
);
});
@@ -448,6 +799,7 @@ describe("injected compatibility runtime", () => {
const image = "data:image/png;base64,iVBORw0KGgo=";
delete theme.variants.light.background.image;
theme.variants.light.appearance.surfaceOpacity = 0.3;
+ theme.variants.light.appearance.focusOpacity = 0.97;
theme.variants.light.appearance.palette = {
surfaceRaised: theme.variants.light.appearance.text,
control: theme.variants.light.appearance.text,
@@ -484,6 +836,14 @@ describe("injected compatibility runtime", () => {
expect(stylesheet?.textContent).toContain(
`${Math.round(contrastSystem.quietSurfaceOpacity * 100)}%, transparent) !important`,
);
+ expect(stylesheet?.textContent).toContain(
+ `${Math.round(contrastSystem.strongSurfaceOpacity * 100)}%, transparent) !important`,
+ );
+ expect(contrastSystem.strongSurfaceOpacity).toBeGreaterThanOrEqual(0.97);
+ expect(stylesheet?.textContent).toContain("scrollbar-width: thin");
+ expect(stylesheet?.textContent).toContain(
+ "::-webkit-scrollbar-thumb:hover",
+ );
expect(stylesheet?.textContent).toContain("color-scheme: light !important");
expect(stylesheet?.textContent).toContain(
'[class~="text-token-text-primary"]',
@@ -510,6 +870,66 @@ describe("injected compatibility runtime", () => {
expect(document.documentElement).not.toHaveAttribute(
"data-codex-styler-contrast",
);
+ expect(document.documentElement).not.toHaveAttribute(
+ "data-codex-styler-geometry",
+ );
+ expect(document.documentElement).not.toHaveAttribute(
+ "data-codex-styler-material",
+ );
+ expect(document.documentElement).not.toHaveAttribute(
+ "data-codex-styler-typography",
+ );
+ expect(document.documentElement).not.toHaveAttribute(
+ "data-codex-styler-motion",
+ );
+ });
+
+ it("maps theme intensity to geometry-safe runtime motion profiles", async () => {
+ document.body.innerHTML = codexFixture("task");
+ const theme = structuredClone(nativeRefined);
+ theme.variants.dark.motion.intensity = 0.9;
+
+ await runtime().apply(theme, "dark", "developer");
+
+ const stylesheet = document.getElementById("codex-styler-runtime-style");
+ expect(document.documentElement).toHaveAttribute(
+ "data-codex-styler-motion",
+ "expressive",
+ );
+ expect(stylesheet?.textContent).toContain(
+ "--codex-styler-motion-duration: 240ms",
+ );
+ expect(stylesheet?.textContent).toContain(
+ "--codex-styler-motion-lift: 3px",
+ );
+ expect(stylesheet?.textContent).toContain(
+ "@keyframes codex-styler-surface-enter",
+ );
+ const overlayMotion = stylesheet?.textContent
+ ?.split("@keyframes codex-styler-surface-enter")[1]
+ ?.split('[role="tooltip"]')[0];
+ expect(overlayMotion).toBeDefined();
+ expect(overlayMotion).toContain(
+ "opacity: var(--codex-styler-motion-overlay-opacity)",
+ );
+ expect(overlayMotion).not.toContain("transform:");
+ expect(stylesheet?.textContent).toContain(
+ '[data-codex-styler-motion="fluid"]',
+ );
+ expect(stylesheet?.textContent).toContain("animation: none !important");
+
+ theme.variants.dark.motion.intensity = 0;
+ await runtime().apply(theme, "dark", "developer");
+ expect(document.documentElement).toHaveAttribute(
+ "data-codex-styler-motion",
+ "still",
+ );
+ expect(
+ document.getElementById("codex-styler-runtime-style")?.textContent,
+ ).toContain("--codex-styler-motion-duration: 0ms");
+ expect(
+ document.getElementById("codex-styler-runtime-style")?.textContent,
+ ).toContain("transform: translateX(0px)");
});
it("repairs a stale Codex foreground without dropping the semantic theme", async () => {
@@ -652,54 +1072,8 @@ describe("injected compatibility runtime", () => {
clearRect: vi.fn(),
drawImage: vi.fn(),
} as unknown as CanvasRenderingContext2D);
- const image =
- "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAA" +
- "DUlEQVR42mNk+M/wHwAF/gL+X2NDWQAAAABJRU5ErkJggg==";
const theme = structuredClone(nativeRefined);
- const entity = {
- id: "test-companion",
- name: "Test companion",
- renderer: {
- type: "sprite-atlas" as const,
- asset: image,
- pages: [image],
- columns: 2,
- rows: 2,
- framesPerPage: 4,
- frameWidth: 1,
- frameHeight: 1,
- directions: 4,
- frameCount: 4,
- poses: [
- { id: "pose-up", angle: 0, frame: 0 },
- { id: "pose-right", angle: 90, frame: 1 },
- { id: "pose-down", angle: 180, frame: 2 },
- { id: "pose-left", angle: 270, frame: 3 },
- ],
- idleClips: [
- {
- id: "blink",
- poseIds: ["pose-up"],
- frames: [{ frame: 0, durationMs: 80 }],
- minimumDelayMs: 500,
- maximumDelayMs: 1000,
- },
- ],
- neutralFrame: 0,
- reducedMotionFrame: 0,
- transitionFps: 30,
- normalization: "preserve" as const,
- alphaThreshold: 12,
- },
- behaviors: [
- "idle" as const,
- "look-at-pointer" as const,
- "reduce-motion-fallback" as const,
- ],
- anchor: { x: 80, y: 70 },
- size: 80,
- opacity: 1,
- };
+ const entity = testSpriteEntity();
theme.scene.entities = [entity];
const outcome = await runtime().apply(theme, "dark", "compatibility", 5);
@@ -715,4 +1089,88 @@ describe("injected compatibility runtime", () => {
document.getElementById("codex-styler-entity-root")?.childElementCount,
).toBe(0);
});
+
+ it("keeps free and attached companions inside a size-aware safe area", async () => {
+ vi.stubGlobal("matchMedia", () => ({
+ matches: false,
+ addEventListener: vi.fn(),
+ removeEventListener: vi.fn(),
+ }));
+ vi.spyOn(window, "innerWidth", "get").mockReturnValue(360);
+ vi.spyOn(window, "innerHeight", "get").mockReturnValue(280);
+ vi.spyOn(HTMLCanvasElement.prototype, "getContext").mockReturnValue({
+ clearRect: vi.fn(),
+ drawImage: vi.fn(),
+ } as unknown as CanvasRenderingContext2D);
+
+ const theme = structuredClone(nativeRefined);
+ const entity = testSpriteEntity({
+ anchor: { x: 99, y: 99 },
+ size: 140,
+ });
+ theme.scene.entities = [entity];
+ await runtime().apply(theme, "dark", "compatibility", 5);
+
+ const canvas = document.querySelector(
+ "#codex-styler-entity-root canvas",
+ );
+ expect(canvas).toHaveStyle({ left: "282px", top: "202px" });
+
+ const composer = document.createElement("div");
+ composer.className = "composer-surface-chrome";
+ let targetX = 260;
+ vi.spyOn(composer, "getBoundingClientRect").mockImplementation(
+ () =>
+ ({
+ x: targetX,
+ y: 210,
+ left: targetX,
+ top: 210,
+ right: targetX + 80,
+ bottom: 258,
+ width: 80,
+ height: 48,
+ toJSON: () => ({}),
+ }) as DOMRect,
+ );
+ document.body.appendChild(composer);
+ runtime().updateEntity(
+ {
+ ...entity,
+ attachment: {
+ target: "composer",
+ edge: "top",
+ align: 1,
+ offset: { x: 12, y: 3 },
+ },
+ },
+ 6,
+ );
+ const attachedCanvas = document.querySelector(
+ "#codex-styler-entity-root canvas",
+ );
+ expect(attachedCanvas).toHaveStyle({ left: "282px", top: "213px" });
+
+ targetX = 20;
+ document.dispatchEvent(new Event("scroll"));
+ expect(attachedCanvas).toHaveStyle({ left: "112px", top: "213px" });
+
+ composer.remove();
+ runtime().updateEntity(
+ {
+ ...entity,
+ attachment: {
+ target: "composer",
+ edge: "top",
+ align: 1,
+ offset: { x: 12, y: 3 },
+ },
+ },
+ 7,
+ );
+ const fallbackCanvas = document.querySelector(
+ "#codex-styler-entity-root canvas",
+ );
+ expect(fallbackCanvas).toHaveStyle({ left: "282px", top: "202px" });
+ });
});
diff --git a/apps/desktop/src/styles.css b/apps/desktop/src/styles.css
index 18b2c7f..ca1139d 100644
--- a/apps/desktop/src/styles.css
+++ b/apps/desktop/src/styles.css
@@ -1,4 +1,5 @@
:root {
+ color-scheme: light;
font-family:
-apple-system, BlinkMacSystemFont, "Segoe UI Variable Text", "SF Pro Text",
"PingFang SC", "Microsoft YaHei", sans-serif;
@@ -24,6 +25,7 @@
@media (prefers-color-scheme: dark) {
:root:not([data-appearance="light"]) {
+ color-scheme: dark;
color: #f3f4f6;
background: #111216;
--shell: #111216;
@@ -42,6 +44,7 @@
}
:root[data-appearance="dark"] {
+ color-scheme: dark;
color: #f3f4f6;
background: #111216;
--shell: #111216;
@@ -225,9 +228,9 @@ button:disabled {
transform 160ms ease;
}
-.app-nav__item:hover {
+.app-nav__item:hover:not(.app-nav__item--active) {
color: var(--ink);
- background: color-mix(in srgb, var(--shell-elevated) 74%, transparent);
+ background: color-mix(in srgb, var(--shell-elevated) 58%, transparent);
}
.app-nav__item:active {
@@ -239,6 +242,11 @@ button:disabled {
background: var(--accent-soft);
}
+.app-nav__item--active:hover {
+ color: var(--accent-ink);
+ background: var(--accent-soft);
+}
+
.app-nav__item span {
font-size: 12.5px;
font-weight: 590;
@@ -1234,7 +1242,7 @@ button:disabled {
height: 100%;
min-height: 390px;
overflow: hidden;
- border: 1px solid rgba(255, 255, 255, 0.12);
+ border: 1px solid var(--preview-border-strong);
border-radius: 4px 14px 14px 4px;
color: var(--preview-text);
background: var(--preview-bg);
@@ -1243,6 +1251,8 @@ button:disabled {
}
.workspace-preview__backdrop {
+ --preview-parallax-scale: 1.035;
+
position: absolute;
z-index: -3;
inset: -12px;
@@ -1273,7 +1283,8 @@ button:disabled {
z-index: 1;
inset: 0;
display: grid;
- grid-template-columns: 26% 1fr;
+ grid-template-columns: 24% minmax(0, 1fr);
+ grid-template-rows: minmax(0, 1fr);
}
.workspace-sidebar {
@@ -1281,17 +1292,58 @@ button:disabled {
display: flex;
flex-direction: column;
min-width: 0;
- padding: 14px 10px 10px;
- border-right: 1px solid
- color-mix(in srgb, var(--preview-border) 70%, transparent);
+ padding: 10px 9px 9px;
+ border-right: 1px solid var(--preview-border-subtle);
background: color-mix(
in srgb,
- var(--preview-surface) calc(var(--preview-surface-strong-opacity) * 100%),
+ var(--preview-surface-raised)
+ calc(var(--preview-surface-strong-opacity) * 100%),
transparent
);
backdrop-filter: blur(16px) saturate(1.05);
}
+.workspace-sidebar__brand {
+ display: flex;
+ height: 29px;
+ align-items: center;
+ gap: 4px;
+ padding: 0 6px;
+ color: var(--preview-text);
+}
+
+.workspace-sidebar__brand strong {
+ font-size: 10px;
+ font-weight: 690;
+ letter-spacing: -0.02em;
+}
+
+.workspace-sidebar__brand svg {
+ color: var(--preview-muted);
+}
+
+.workspace-sidebar__primary {
+ display: grid;
+ gap: 2px;
+ margin-top: 5px;
+}
+
+.workspace-sidebar__primary span {
+ display: flex;
+ min-height: 25px;
+ align-items: center;
+ gap: 7px;
+ padding: 0 7px;
+ border-radius: 6px;
+ color: var(--preview-tertiary);
+ font-size: 7px;
+}
+
+.workspace-sidebar__primary span.is-active {
+ color: var(--preview-text);
+ background: color-mix(in srgb, var(--preview-control-hover) 78%, transparent);
+}
+
.workspace-preview[data-layout="editorial"] .workspace-sidebar,
.workspace-preview[data-layout="immersive"] .workspace-sidebar {
margin: 5px 0 5px 5px;
@@ -1299,7 +1351,8 @@ button:disabled {
border-radius: calc(var(--preview-radius) + 3px);
background: color-mix(
in srgb,
- var(--preview-surface) calc(var(--preview-surface-strong-opacity) * 96%),
+ var(--preview-surface-raised)
+ calc(var(--preview-surface-strong-opacity) * 96%),
transparent
);
box-shadow: 8px 0 24px rgba(0, 0, 0, 0.08);
@@ -1313,7 +1366,7 @@ button:disabled {
.workspace-traffic {
display: flex;
gap: 4px;
- height: 18px;
+ height: 14px;
align-items: flex-start;
padding: 1px 3px;
}
@@ -1363,11 +1416,30 @@ button:disabled {
background: color-mix(in srgb, var(--preview-accent) 48%, #24362c);
}
-.workspace-preview[data-icon-style="contained"] .workspace-sidebar svg,
-.workspace-preview[data-icon-style="contained"] .workspace-note__icon svg {
- color: var(--preview-accent);
+.workspace-preview[data-icon-style="contained"]
+ :is(
+ .workspace-sidebar button,
+ .workspace-sidebar__primary span,
+ .workspace-sidebar__footer,
+ .workspace-header__navigation span,
+ .workspace-header__actions > span,
+ .workspace-header-action,
+ .workspace-context-tabs button,
+ .workspace-settings-state nav span,
+ .workspace-component-actions button,
+ .workspace-component-input,
+ .workspace-composer button,
+ .workspace-right-panel__heading,
+ .workspace-dialog-layer > div
+ )
+ svg {
+ overflow: visible;
+ border-radius: max(3px, calc(var(--preview-radius) * 0.28));
+ color: var(--preview-icon);
+ box-shadow: 0 0 0 3px
+ color-mix(in srgb, var(--preview-accent) 10%, transparent);
filter: drop-shadow(
- 0 2px 4px color-mix(in srgb, var(--preview-accent) 22%, transparent)
+ 0 2px 4px color-mix(in srgb, var(--preview-accent) 20%, transparent)
);
}
@@ -1388,21 +1460,33 @@ button:disabled {
}
.workspace-preview[data-icon-style="themed"]
- :is(.workspace-new-task, .workspace-sidebar__footer)
- > svg,
-.workspace-preview[data-icon-style="themed"] .workspace-note__icon {
- color: color-mix(in srgb, var(--preview-accent) 86%, var(--preview-text));
+ :is(
+ .workspace-sidebar button,
+ .workspace-sidebar__primary span,
+ .workspace-sidebar__footer,
+ .workspace-header__navigation span,
+ .workspace-header__actions > span,
+ .workspace-header-action,
+ .workspace-context-tabs button,
+ .workspace-settings-state nav span,
+ .workspace-component-actions button,
+ .workspace-component-input,
+ .workspace-composer button,
+ .workspace-right-panel__heading,
+ .workspace-dialog-layer > div
+ )
+ svg {
+ overflow: visible;
+ border-radius: 999px;
+ color: var(--preview-icon-emphasis);
+ box-shadow:
+ 0 0 0 3px color-mix(in srgb, var(--preview-accent) 13%, transparent),
+ 0 0 12px color-mix(in srgb, var(--preview-accent) 18%, transparent);
filter: drop-shadow(
- 0 3px 6px color-mix(in srgb, var(--preview-accent) 32%, transparent)
+ 0 3px 6px color-mix(in srgb, var(--preview-accent) 30%, transparent)
);
}
-.workspace-preview[data-icon-style="themed"]
- :is(.workspace-new-task, .workspace-sidebar__footer)
- > svg {
- stroke-width: 2.25px;
-}
-
.workspace-preview[data-decorations="subtle"] .workspace-main {
box-shadow:
inset 0 1px color-mix(in srgb, var(--preview-accent) 14%, transparent),
@@ -1427,7 +1511,7 @@ button:disabled {
margin: 7px 1px;
border: 1px solid color-mix(in srgb, var(--preview-border) 75%, transparent);
border-radius: 7px;
- background: color-mix(in srgb, var(--preview-surface) 58%, transparent);
+ background: color-mix(in srgb, var(--preview-control) 78%, transparent);
}
.workspace-section-label {
@@ -1455,20 +1539,37 @@ button:disabled {
.workspace-task--active {
color: var(--preview-text);
- background: color-mix(in srgb, var(--preview-surface) 76%, transparent);
+ background: color-mix(in srgb, var(--preview-control-hover) 82%, transparent);
}
.workspace-sidebar__footer {
- height: 25px;
+ height: 27px;
margin-top: auto;
padding: 0 7px;
color: var(--preview-muted);
}
+.workspace-sidebar__footer > span:nth-child(2) {
+ overflow: hidden;
+ flex: 1;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+}
+
+.workspace-sidebar__user {
+ display: grid;
+ width: 17px;
+ height: 17px;
+ place-items: center;
+ border: 1px solid color-mix(in srgb, var(--preview-border) 78%, transparent);
+ border-radius: 50%;
+ background: color-mix(in srgb, var(--preview-control) 84%, transparent);
+}
+
.workspace-main {
display: grid;
min-width: 0;
- grid-template-rows: 43px 1fr auto;
+ grid-template-rows: 43px minmax(0, 1fr) auto;
}
.workspace-preview[data-layout="editorial"] .workspace-main,
@@ -1497,20 +1598,59 @@ button:disabled {
}
.workspace-header {
- display: flex;
+ display: grid;
align-items: center;
- justify-content: space-between;
- padding: 0 15px;
+ grid-template-columns: minmax(58px, 0.55fr) minmax(0, 1.4fr) minmax(
+ 92px,
+ 0.75fr
+ );
+ gap: 8px;
+ padding: 0 9px;
border-bottom: 1px solid
color-mix(in srgb, var(--preview-border) 66%, transparent);
background: color-mix(in srgb, var(--preview-surface) 28%, transparent);
backdrop-filter: blur(10px);
}
-.workspace-header > div {
+.workspace-header__navigation,
+.workspace-header__title,
+.workspace-header__actions {
display: flex;
align-items: center;
- gap: 8px;
+ min-width: 0;
+ gap: 6px;
+}
+
+.workspace-header__navigation span,
+.workspace-header__actions > span:not(.workspace-header-action) {
+ display: grid;
+ width: 20px;
+ height: 20px;
+ place-items: center;
+ border-radius: 5px;
+ color: var(--preview-muted);
+}
+
+.workspace-header__title {
+ justify-content: center;
+}
+
+.workspace-header__title strong {
+ overflow: hidden;
+ font-size: 7.5px;
+ font-weight: 640;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+}
+
+.workspace-header__title > svg,
+.workspace-header__actions > svg {
+ flex: none;
+ color: var(--preview-muted);
+}
+
+.workspace-header__actions {
+ justify-content: flex-end;
}
.workspace-header strong {
@@ -1530,15 +1670,79 @@ button:disabled {
padding: 5px 7px;
border: 1px solid color-mix(in srgb, var(--preview-border) 65%, transparent);
border-radius: 6px;
- background: color-mix(in srgb, var(--preview-surface) 58%, transparent);
+ background: color-mix(in srgb, var(--preview-control) 82%, transparent);
font-size: 6.5px;
}
+.workspace-content-shell {
+ display: grid;
+ min-width: 0;
+ min-height: 0;
+ grid-template-columns: minmax(0, 1fr);
+ overflow: hidden;
+}
+
+.workspace-content-shell--with-panel {
+ grid-template-columns: minmax(0, 1fr) minmax(124px, 31%);
+}
+
+.workspace-primary-surface {
+ display: grid;
+ min-width: 0;
+ min-height: 0;
+ grid-template-rows: minmax(0, 1fr);
+ overflow: hidden;
+}
+
+.workspace-primary-surface[data-has-context-tabs="true"] {
+ grid-template-rows: 29px minmax(0, 1fr);
+}
+
+.workspace-context-tabs {
+ display: flex;
+ align-items: flex-end;
+ gap: 4px;
+ padding: 0 14px;
+ border-bottom: 1px solid
+ color-mix(in srgb, var(--preview-border) 56%, transparent);
+ background: color-mix(
+ in srgb,
+ var(--preview-surface-sunken) 72%,
+ transparent
+ );
+}
+
+.workspace-context-tabs span {
+ position: relative;
+ display: grid;
+ height: 28px;
+ place-items: center;
+ padding-inline: 9px;
+ color: var(--preview-muted);
+ font-size: 6.5px;
+}
+
+.workspace-context-tabs span.is-active {
+ color: var(--preview-text);
+ background: color-mix(in srgb, var(--preview-accent) 7%, transparent);
+}
+
+.workspace-context-tabs span.is-active::after {
+ position: absolute;
+ right: 7px;
+ bottom: 0;
+ left: 7px;
+ height: 2px;
+ border-radius: 999px 999px 0 0;
+ background: var(--preview-accent);
+ content: "";
+}
+
.workspace-conversation {
display: flex;
flex-direction: column;
gap: 20px;
- padding: 32px clamp(24px, 6vw, 64px);
+ padding: 24px clamp(18px, 5vw, 52px);
}
.workspace-note,
@@ -1548,17 +1752,32 @@ button:disabled {
}
.workspace-note {
+ position: relative;
+ align-items: flex-start;
padding: 11px;
border: 1px solid color-mix(in srgb, var(--preview-border) 65%, transparent);
border-radius: var(--preview-radius);
background: color-mix(
in srgb,
- var(--preview-surface) calc(var(--preview-surface-strong-opacity) * 86%),
+ var(--preview-surface-raised)
+ calc(var(--preview-surface-strong-opacity) * 86%),
transparent
);
backdrop-filter: blur(11px);
}
+.workspace-note__status {
+ margin-left: auto;
+ padding: 3px 5px;
+ border: 1px solid color-mix(in srgb, var(--preview-accent) 28%, transparent);
+ border-radius: 999px;
+ color: color-mix(in srgb, var(--preview-accent) 72%, var(--preview-text));
+ background: color-mix(in srgb, var(--preview-accent) 9%, transparent);
+ font-size: 5.5px;
+ font-weight: 650;
+ white-space: nowrap;
+}
+
.workspace-preview[data-layout="editorial"] .workspace-note,
.workspace-preview[data-layout="immersive"] .workspace-note {
border-left: 3px solid var(--preview-accent);
@@ -1569,7 +1788,8 @@ button:disabled {
border-radius: calc(var(--preview-radius) + 5px);
background: color-mix(
in srgb,
- var(--preview-surface) calc(var(--preview-surface-strong-opacity) * 96%),
+ var(--preview-surface-overlay)
+ calc(var(--preview-surface-strong-opacity) * 96%),
transparent
);
}
@@ -1616,7 +1836,30 @@ button:disabled {
padding: 9px 10px;
border: 1px solid color-mix(in srgb, var(--preview-border) 50%, transparent);
border-radius: 7px;
- background: color-mix(in srgb, var(--preview-surface) 44%, transparent);
+ background: color-mix(
+ in srgb,
+ var(--preview-surface-sunken) 84%,
+ transparent
+ );
+}
+
+.workspace-code-lines__header {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ margin: -2px 0 2px;
+ color: var(--preview-muted);
+}
+
+.workspace-code-lines__header code {
+ color: var(--preview-text);
+ font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
+ font-size: 5.5px;
+}
+
+.workspace-code-lines__header small {
+ color: color-mix(in srgb, var(--preview-accent) 68%, var(--preview-text));
+ font-size: 5px;
}
.workspace-code-lines i {
@@ -1636,7 +1879,7 @@ button:disabled {
}
.workspace-composer {
- padding: 0 clamp(24px, 6vw, 64px) 25px;
+ padding: 0 clamp(18px, 5vw, 52px) 18px;
}
.workspace-composer__field {
@@ -1650,7 +1893,8 @@ button:disabled {
color: var(--preview-muted);
background: color-mix(
in srgb,
- var(--preview-surface) calc(var(--preview-surface-strong-opacity) * 96%),
+ var(--preview-surface-overlay)
+ calc(var(--preview-surface-strong-opacity) * 96%),
transparent
);
box-shadow: 0 10px 25px rgba(0, 0, 0, 0.08);
@@ -1658,6 +1902,11 @@ button:disabled {
font-size: 7.5px;
}
+.workspace-composer__field .workspace-composer__add {
+ color: var(--preview-muted);
+ background: transparent;
+}
+
.workspace-preview[data-decorations="expressive"] .workspace-composer__field {
position: relative;
border-color: color-mix(
@@ -1692,8 +1941,8 @@ button:disabled {
place-items: center;
border: 0;
border-radius: 8px;
- color: white;
- background: color-mix(in srgb, var(--preview-accent) 74%, #26352d);
+ color: var(--preview-on-accent);
+ background: var(--preview-accent);
}
.scene-entity {
@@ -1732,6 +1981,8 @@ button:disabled {
position: absolute;
inset: 0;
display: block;
+ width: 100%;
+ height: 100%;
}
.scene-entity__image {
@@ -2254,6 +2505,54 @@ button:disabled {
line-height: 1.45;
}
+.motion-preview-action {
+ display: grid;
+ grid-template-columns: minmax(0, 1fr);
+ gap: 6px;
+ margin: -3px 0 16px;
+ padding: 9px;
+ border: 1px solid var(--line);
+ border-radius: 9px;
+ background: color-mix(in srgb, var(--shell-soft) 72%, transparent);
+}
+
+.motion-preview-action button {
+ display: inline-flex;
+ width: 100%;
+ min-height: 34px;
+ align-items: center;
+ justify-content: center;
+ gap: 7px;
+ border: 1px solid color-mix(in srgb, var(--accent) 28%, var(--line));
+ border-radius: 8px;
+ color: var(--accent-ink);
+ background: color-mix(in srgb, var(--accent-soft) 74%, var(--shell-elevated));
+ cursor: pointer;
+ font-size: 8.5px;
+ font-weight: 650;
+}
+
+.motion-preview-action button:hover:not(:disabled) {
+ border-color: color-mix(in srgb, var(--accent) 50%, var(--line));
+ background: color-mix(in srgb, var(--accent-soft) 90%, var(--shell-elevated));
+}
+
+.motion-preview-action button:focus-visible {
+ outline: 2px solid var(--focus-ring);
+ outline-offset: 2px;
+}
+
+.motion-preview-action button:disabled {
+ cursor: default;
+ opacity: 0.52;
+}
+
+.motion-preview-action small {
+ color: var(--ink-faint);
+ font-size: 8px;
+ line-height: 1.45;
+}
+
.motion-recipes {
display: grid;
grid-template-columns: 1fr 1fr;
@@ -2368,33 +2667,6 @@ button:disabled {
cursor: pointer;
}
-.contrast-note {
- display: flex;
- align-items: flex-start;
- gap: 8px;
- margin-top: 8px;
- padding: 10px;
- border: 1px solid var(--line);
- border-radius: 9px;
- color: var(--accent-ink);
- background: color-mix(in srgb, var(--accent-soft) 55%, transparent);
-}
-
-.contrast-note span {
- display: flex;
- flex-direction: column;
- gap: 3px;
-}
-
-.contrast-note strong {
- font-size: 9px;
-}
-.contrast-note small {
- color: var(--ink-muted);
- font-size: 8px;
- line-height: 1.45;
-}
-
.inspector-help {
display: flex;
align-items: center;
@@ -5203,6 +5475,16 @@ button:disabled {
object-fit: contain;
}
+.alignment-stage__compiled-preview {
+ display: block;
+ width: 100%;
+ height: 100%;
+}
+
+.alignment-stage__compiled-preview image {
+ pointer-events: none;
+}
+
.shared-crop-box {
position: absolute;
border: 1px solid #6ea2ff;
@@ -5597,7 +5879,8 @@ button:disabled {
outline: none;
}
-.pointer-test-companion img {
+.pointer-test-companion img,
+.pointer-test-companion svg {
width: 100%;
height: 100%;
object-fit: contain;
@@ -7156,22 +7439,13 @@ button:disabled {
margin-top: 1px;
}
-.alignment-readiness__heading p,
-.alignment-readiness__issue {
+.alignment-readiness__heading p {
margin: 3px 0 0;
color: var(--ink-faint);
font-size: 8px;
line-height: 1.45;
}
-.alignment-readiness__issue {
- padding: 7px 8px;
- border: 1px solid color-mix(in srgb, #d97706 24%, var(--line));
- border-radius: 8px;
- color: color-mix(in srgb, #d97706 72%, var(--ink-muted));
- background: color-mix(in srgb, #d97706 6%, transparent);
-}
-
.alignment-metrics {
display: grid;
grid-template-columns: repeat(3, minmax(0, 1fr));
@@ -7214,11 +7488,114 @@ button:disabled {
color: color-mix(in srgb, #d97706 76%, var(--ink));
}
-.alignment-readiness .creator-inspector-action {
- position: static;
+.alignment-recommendation {
+ display: grid;
+ gap: 10px;
+ padding: 11px;
+ border: 1px solid color-mix(in srgb, var(--accent) 26%, var(--line));
+ border-radius: 11px;
+ background: color-mix(in srgb, var(--accent) 7%, var(--shell-elevated));
+}
+
+.alignment-recommendation__copy {
+ display: grid;
+ gap: 4px;
+}
+
+.alignment-recommendation__copy > span {
+ color: var(--accent-strong);
+ font-size: 7px;
+ font-weight: 780;
+ letter-spacing: 0.1em;
+ text-transform: uppercase;
+}
+
+.alignment-recommendation__copy > strong,
+.alignment-recommendation--ready strong {
+ color: var(--ink);
+ font-size: 10px;
+ line-height: 1.3;
+}
+
+.alignment-recommendation__copy > p,
+.alignment-recommendation--ready p {
+ margin: 0;
+ color: var(--ink-muted);
+ font-size: 8px;
+ line-height: 1.5;
+}
+
+.alignment-recommendation > .button {
width: 100%;
+ min-height: 36px;
+ justify-content: center;
+}
+
+.alignment-recommendation--ready {
+ grid-template-columns: 22px minmax(0, 1fr);
+ align-items: start;
+ border-color: color-mix(in srgb, #16a34a 24%, var(--line));
+ background: color-mix(in srgb, #16a34a 6%, var(--shell-elevated));
+}
+
+.alignment-recommendation--ready > svg {
+ margin-top: 1px;
+ color: color-mix(in srgb, #16a34a 72%, var(--ink));
+}
+
+.alignment-recommendation--ready > div {
+ display: grid;
+ gap: 3px;
+}
+
+.alignment-secondary-tools {
+ border-top: 1px solid var(--line);
+ padding-top: 2px;
+}
+
+.alignment-secondary-tools > summary {
+ display: flex;
+ min-height: 32px;
+ cursor: pointer;
+ list-style: none;
+ align-items: center;
+ justify-content: space-between;
+ gap: 8px;
+ color: var(--ink-muted);
+ font-size: 8px;
+ font-weight: 680;
+}
+
+.alignment-secondary-tools > summary::-webkit-details-marker {
+ display: none;
+}
+
+.alignment-secondary-tools > summary svg {
+ transition: transform 180ms ease;
+}
+
+.alignment-secondary-tools[open] > summary svg {
+ transform: rotate(180deg);
+}
+
+.alignment-secondary-tools__actions {
+ display: grid;
+ grid-template-columns: repeat(2, minmax(0, 1fr));
+ gap: 7px;
+ padding-top: 5px;
+}
+
+.alignment-secondary-tools__actions .button {
+ min-width: 0;
min-height: 34px;
- margin: 0;
+ padding-inline: 8px;
+ font-size: 8px;
+}
+
+@media (prefers-reduced-motion: reduce) {
+ .alignment-secondary-tools > summary svg {
+ transition: none;
+ }
}
.creator-section-heading {
@@ -7263,6 +7640,105 @@ button:disabled {
font-size: 8px;
}
+.alignment-scale-control {
+ display: grid;
+ gap: 9px;
+ margin-top: 11px;
+ padding: 11px;
+ border: 1px solid var(--line);
+ border-radius: 10px;
+ background: color-mix(in srgb, var(--shell-soft) 82%, var(--shell-elevated));
+}
+
+.alignment-scale-control__heading,
+.alignment-scale-control__slider {
+ display: flex;
+ min-width: 0;
+ align-items: center;
+ justify-content: space-between;
+ gap: 10px;
+}
+
+.alignment-scale-control__heading > span {
+ display: grid;
+ min-width: 0;
+ gap: 2px;
+}
+
+.alignment-scale-control__heading strong {
+ color: var(--ink);
+ font-size: 9px;
+}
+
+.alignment-scale-control__heading small,
+.alignment-scale-control > p {
+ color: var(--ink-faint);
+ font-size: 7.5px;
+ line-height: 1.45;
+}
+
+.alignment-scale-control__heading > label {
+ display: grid;
+ width: 68px;
+ flex: 0 0 auto;
+ grid-template-columns: minmax(0, 1fr) auto;
+ align-items: center;
+ overflow: hidden;
+ border: 1px solid var(--line-strong);
+ border-radius: 8px;
+ background: var(--shell-elevated);
+}
+
+.alignment-scale-control__heading input[type="number"] {
+ width: 100%;
+ height: 31px;
+ padding: 0 3px 0 9px;
+ border: 0;
+ color: var(--ink);
+ background: transparent;
+ font-size: 9px;
+ font-variant-numeric: tabular-nums;
+ outline: 0;
+}
+
+.alignment-scale-control__heading label > span {
+ padding-right: 8px;
+ color: var(--ink-faint);
+ font-size: 8px;
+}
+
+.alignment-scale-control__slider > input[type="range"] {
+ min-width: 0;
+ flex: 1 1 auto;
+}
+
+.alignment-scale-control__slider .button {
+ min-width: 68px;
+ min-height: 31px;
+ padding-inline: 8px;
+ font-size: 8px;
+}
+
+.alignment-scale-control > p {
+ margin: 0;
+}
+
+.alignment-scale-control > .alignment-scale-control__warning {
+ display: flex;
+ align-items: flex-start;
+ gap: 6px;
+ padding: 7px 8px;
+ border: 1px solid color-mix(in srgb, #d97706 25%, var(--line));
+ border-radius: 8px;
+ color: color-mix(in srgb, #d97706 72%, var(--ink-muted));
+ background: color-mix(in srgb, #d97706 7%, transparent);
+}
+
+.alignment-scale-control__warning svg {
+ flex: 0 0 auto;
+ margin-top: 1px;
+}
+
.alignment-ground-control {
margin-top: 10px;
padding-top: 10px;
diff --git a/apps/desktop/src/styles/creator-beta3.css b/apps/desktop/src/styles/creator-beta3.css
index e9487de..aaf554d 100644
--- a/apps/desktop/src/styles/creator-beta3.css
+++ b/apps/desktop/src/styles/creator-beta3.css
@@ -150,7 +150,6 @@
overflow-x: auto;
border-bottom: 1px solid var(--border-subtle);
background: var(--surface-subtle);
- scrollbar-width: thin;
}
.creator-step {
@@ -352,6 +351,411 @@
align-self: start;
}
+.creator-behavior-audition {
+ display: grid;
+ gap: 10px;
+ padding: 12px;
+ border: 1px solid color-mix(in srgb, var(--accent) 24%, var(--border-subtle));
+ border-radius: var(--radius-sm);
+ background: color-mix(in srgb, var(--accent) 4%, var(--surface-base));
+}
+
+.creator-behavior-audition__heading {
+ display: grid;
+ grid-template-columns: auto minmax(0, 1fr) auto;
+ align-items: start;
+ gap: 8px;
+}
+
+.creator-behavior-audition__heading > div {
+ display: grid;
+ min-width: 0;
+ gap: 2px;
+}
+
+.creator-behavior-audition__heading h3,
+.creator-behavior-audition__heading p,
+.creator-behavior-audition__hint {
+ margin: 0;
+}
+
+.creator-behavior-audition__heading h3 {
+ color: var(--text-primary);
+ font-size: var(--type-body-sm);
+}
+
+.creator-behavior-audition__heading p,
+.creator-behavior-audition__hint {
+ color: var(--text-secondary);
+ font-size: var(--type-caption);
+ line-height: 1.45;
+}
+
+.creator-behavior-audition__icon {
+ display: grid;
+ width: 26px;
+ height: 26px;
+ place-items: center;
+ border-radius: 8px;
+ color: var(--accent);
+ background: color-mix(in srgb, var(--accent) 12%, transparent);
+}
+
+.creator-behavior-audition__pose {
+ display: grid;
+ min-width: 78px;
+ gap: 2px;
+ padding: 5px 7px;
+ border: 1px solid var(--border-subtle);
+ border-radius: var(--radius-xs);
+ background: color-mix(in srgb, var(--surface-subtle) 76%, transparent);
+}
+
+.creator-behavior-audition__pose small {
+ color: var(--text-tertiary);
+ font-size: 10px;
+ letter-spacing: 0.04em;
+ text-transform: uppercase;
+}
+
+.creator-behavior-audition__pose strong {
+ color: var(--text-primary);
+ font-size: var(--type-caption);
+ font-variant-numeric: tabular-nums;
+}
+
+.creator-behavior-audition__controls {
+ display: grid;
+ grid-template-columns: minmax(0, 1fr) auto;
+ align-items: end;
+ gap: 8px;
+}
+
+.creator-behavior-audition__field {
+ display: grid;
+ min-width: 0;
+ gap: 5px;
+ color: var(--text-secondary);
+ font-size: var(--type-caption);
+ font-weight: 650;
+}
+
+.creator-behavior-audition__field .creator-select-control,
+.creator-behavior-audition__field select {
+ width: 100%;
+ min-width: 0;
+}
+
+.creator-behavior-audition__controls > .button {
+ min-width: 124px;
+}
+
+.creator-behavior-audition__status {
+ display: flex;
+ min-width: 0;
+ align-items: center;
+ justify-content: space-between;
+ gap: 8px;
+ padding-top: 2px;
+ color: var(--text-tertiary);
+ font-size: var(--type-caption);
+ font-variant-numeric: tabular-nums;
+}
+
+.creator-behavior-audition__status span:first-child {
+ overflow: hidden;
+ min-width: 0;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+}
+
+.creator-behavior-audition__status strong {
+ color: var(--text-primary);
+}
+
+.creator-behavior-audition__empty {
+ display: grid;
+ gap: 3px;
+ padding: 9px;
+ border: 1px dashed var(--border-subtle);
+ border-radius: var(--radius-xs);
+ color: var(--text-secondary);
+ background: color-mix(in srgb, var(--surface-subtle) 62%, transparent);
+ font-size: var(--type-caption);
+ line-height: 1.45;
+}
+
+.creator-behavior-audition__empty strong {
+ color: var(--text-primary);
+}
+
+@media (max-width: 1080px) {
+ .creator-behavior-audition__heading {
+ grid-template-columns: auto minmax(0, 1fr);
+ }
+
+ .creator-behavior-audition__pose {
+ grid-column: 2;
+ justify-self: start;
+ }
+
+ .creator-behavior-audition__controls {
+ grid-template-columns: minmax(0, 1fr);
+ }
+
+ .creator-behavior-audition__controls > .button {
+ width: 100%;
+ }
+}
+
+.edge-quality-review {
+ display: grid;
+ gap: 10px;
+ padding: 12px;
+ border: 1px solid color-mix(in srgb, #d97706 26%, var(--border-subtle));
+ border-radius: var(--radius-sm);
+ background: color-mix(in srgb, #d97706 5%, var(--surface-base));
+ outline: none;
+}
+
+.edge-quality-review:focus-visible {
+ box-shadow: 0 0 0 2px var(--focus-ring);
+}
+
+.edge-quality-review--complete {
+ border-color: color-mix(in srgb, #16a36a 28%, var(--border-subtle));
+ background: color-mix(in srgb, #16a36a 5%, var(--surface-base));
+}
+
+.edge-quality-review--attention {
+ border-color: color-mix(in srgb, #d97706 42%, var(--border-subtle));
+ background: color-mix(in srgb, #d97706 7%, var(--surface-base));
+}
+
+.edge-quality-review__heading {
+ display: grid;
+ grid-template-columns: auto minmax(0, 1fr) auto;
+ align-items: start;
+ gap: 8px;
+}
+
+.edge-quality-review__heading > div {
+ display: grid;
+ min-width: 0;
+ gap: 2px;
+}
+
+.edge-quality-review__heading h3,
+.edge-quality-review__heading p {
+ margin: 0;
+}
+
+.edge-quality-review__heading p {
+ color: var(--text-secondary);
+ font-size: var(--type-caption);
+ line-height: 1.45;
+}
+
+.edge-quality-review__heading > strong {
+ color: var(--text-secondary);
+ font-size: var(--type-caption);
+ font-variant-numeric: tabular-nums;
+}
+
+.edge-quality-review__icon {
+ display: grid;
+ width: 26px;
+ height: 26px;
+ place-items: center;
+ border-radius: 8px;
+ color: #d97706;
+ background: color-mix(in srgb, #d97706 12%, transparent);
+}
+
+.edge-quality-review--complete .edge-quality-review__icon {
+ color: #16a36a;
+ background: color-mix(in srgb, #16a36a 12%, transparent);
+}
+
+.edge-quality-review__backdrops {
+ display: grid;
+ grid-template-columns: repeat(3, minmax(0, 1fr));
+ gap: 5px;
+}
+
+.edge-quality-review__backdrops button {
+ display: grid;
+ min-width: 0;
+ min-height: 34px;
+ grid-template-columns: auto minmax(0, 1fr) auto;
+ align-items: center;
+ gap: 6px;
+ padding: 5px 7px;
+ border: 1px solid var(--border-subtle);
+ border-radius: var(--radius-xs);
+ color: var(--text-secondary);
+ background: var(--surface-subtle);
+ cursor: pointer;
+ font: inherit;
+ font-size: var(--type-caption);
+ text-align: left;
+ transition:
+ color var(--motion-standard) var(--ease-standard),
+ border-color var(--motion-standard) var(--ease-standard),
+ background var(--motion-standard) var(--ease-standard);
+}
+
+.edge-quality-review__backdrops button:hover,
+.edge-quality-review__backdrops button.is-active {
+ color: var(--text-primary);
+ border-color: color-mix(in srgb, var(--accent) 42%, var(--border-strong));
+ background: var(--surface-hover);
+}
+
+.edge-quality-review__backdrops button:focus-visible,
+.edge-quality-review__repair:focus-visible {
+ outline: 2px solid var(--focus-ring);
+ outline-offset: 2px;
+}
+
+.edge-quality-review__backdrops button > span {
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+}
+
+.edge-quality-review__backdrops button > svg {
+ color: #16a36a;
+}
+
+.edge-quality-review__confirm {
+ width: 100%;
+}
+
+.edge-quality-review__automatic {
+ display: grid;
+ grid-template-columns: auto minmax(0, 1fr);
+ align-items: start;
+ gap: 7px;
+ padding: 8px;
+ border: 1px solid var(--border-subtle);
+ border-radius: var(--radius-xs);
+ color: var(--text-secondary);
+ background: color-mix(in srgb, var(--surface-subtle) 72%, transparent);
+}
+
+.edge-quality-review__automatic > span {
+ display: grid;
+ min-height: 18px;
+ place-items: center;
+ color: var(--text-tertiary);
+}
+
+.edge-quality-review__automatic[data-status="ready"] > span {
+ color: #16a36a;
+}
+
+.edge-quality-review--attention
+ .edge-quality-review__automatic[data-status="ready"]
+ > span {
+ color: #d97706;
+}
+
+.edge-quality-review__automatic > div,
+.edge-quality-review__automatic strong,
+.edge-quality-review__automatic small {
+ display: block;
+ min-width: 0;
+}
+
+.edge-quality-review__automatic strong {
+ color: var(--text-primary);
+ font-size: var(--type-caption);
+ line-height: 1.35;
+}
+
+.edge-quality-review__automatic small {
+ margin-top: 2px;
+ color: var(--text-tertiary);
+ font-size: var(--type-caption);
+ line-height: 1.4;
+}
+
+.edge-quality-review__issues {
+ display: grid;
+ gap: 5px;
+}
+
+.edge-quality-review__issues button {
+ display: grid;
+ min-height: 40px;
+ grid-template-columns: minmax(0, 1fr) auto;
+ align-items: center;
+ gap: 8px;
+ padding: 7px 8px;
+ border: 1px solid color-mix(in srgb, #d97706 28%, var(--border-subtle));
+ border-radius: var(--radius-xs);
+ color: var(--text-secondary);
+ background: var(--surface-subtle);
+ cursor: pointer;
+ font: inherit;
+ text-align: left;
+ transition:
+ border-color var(--motion-standard) var(--ease-standard),
+ background var(--motion-standard) var(--ease-standard);
+}
+
+.edge-quality-review__issues button:hover {
+ border-color: color-mix(in srgb, #d97706 48%, var(--border-strong));
+ background: var(--surface-hover);
+}
+
+.edge-quality-review__issues button:focus-visible {
+ outline: 2px solid var(--focus-ring);
+ outline-offset: 2px;
+}
+
+.edge-quality-review__issues button span,
+.edge-quality-review__issues button strong,
+.edge-quality-review__issues button small {
+ display: block;
+ min-width: 0;
+}
+
+.edge-quality-review__issues button strong {
+ overflow: hidden;
+ color: var(--text-primary);
+ font-size: var(--type-caption);
+ line-height: 1.35;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+}
+
+.edge-quality-review__issues button small {
+ margin-top: 2px;
+ color: var(--text-tertiary);
+ font-size: var(--type-caption);
+}
+
+.edge-quality-review__repair {
+ display: inline-flex;
+ width: max-content;
+ min-height: 30px;
+ align-items: center;
+ gap: 6px;
+ padding: 0;
+ border: 0;
+ color: var(--text-secondary);
+ background: transparent;
+ cursor: pointer;
+ font: inherit;
+ font-size: var(--type-caption);
+}
+
+.edge-quality-review__repair:hover {
+ color: var(--text-primary);
+}
+
.creator-readiness-action {
display: grid;
width: 100%;
@@ -462,7 +866,8 @@
@media (max-width: 1080px) {
.creator-header {
- grid-template-columns: auto minmax(0, 1fr);
+ grid-template-columns: 150px minmax(180px, 1fr) auto;
+ gap: 14px;
}
.creator-header p {
@@ -470,8 +875,8 @@
}
.creator-history {
- grid-column: 1 / -1;
- flex-wrap: wrap;
+ grid-column: auto;
+ flex-wrap: nowrap;
}
.creator-history .button {
@@ -480,6 +885,10 @@
padding-inline: 10px;
}
+ .creator-header > .button:first-child {
+ min-width: 150px;
+ }
+
.creator-restore-card {
grid-template-columns: auto minmax(0, 1fr);
}
diff --git a/apps/desktop/src/styles/editor.css b/apps/desktop/src/styles/editor.css
index f8ff3f8..586c5da 100644
--- a/apps/desktop/src/styles/editor.css
+++ b/apps/desktop/src/styles/editor.css
@@ -302,6 +302,11 @@
box-shadow: inset 0 2px 0 color-mix(in srgb, var(--accent) 52%, transparent);
}
+.canvas-stage[data-preview-version="official"] {
+ box-shadow: inset 0 2px 0
+ color-mix(in srgb, var(--text-tertiary) 46%, transparent);
+}
+
.canvas-stage[data-preview-zoom="actual"] {
display: block;
overflow: auto;
@@ -475,6 +480,597 @@
outline-offset: 2px;
}
+.inspector-subsection-heading {
+ display: grid;
+ gap: 3px;
+ margin: 0 14px 9px;
+}
+
+.inspector-subsection-heading strong,
+.inspector-subsection-label {
+ color: var(--text-primary);
+ font-size: var(--type-meta);
+ font-weight: 680;
+}
+
+.inspector-subsection-heading span {
+ color: var(--text-tertiary);
+ font-size: var(--type-caption);
+ line-height: 1.42;
+}
+
+.surface-treatments {
+ display: grid;
+ gap: 6px;
+ margin: 0 14px 16px;
+}
+
+.surface-treatments button {
+ display: grid;
+ min-height: 48px;
+ grid-template-columns: minmax(0, 1fr) 16px;
+ align-items: center;
+ gap: 9px;
+ padding: 8px 10px;
+ border: 1px solid var(--border-subtle);
+ border-radius: var(--radius-md);
+ color: var(--text-secondary);
+ background: var(--surface-subtle);
+ cursor: pointer;
+ text-align: left;
+ transition:
+ color var(--motion-standard) var(--ease-standard),
+ border-color var(--motion-standard) var(--ease-standard),
+ background var(--motion-standard) var(--ease-standard),
+ box-shadow var(--motion-standard) var(--ease-standard);
+}
+
+.surface-treatments button > span {
+ display: grid;
+ min-width: 0;
+ gap: 2px;
+}
+
+.surface-treatments button strong {
+ color: inherit;
+ font-size: var(--type-meta);
+ font-weight: 680;
+}
+
+.surface-treatments button small {
+ overflow: hidden;
+ color: var(--text-tertiary);
+ font-size: var(--type-caption);
+ line-height: 1.35;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+}
+
+.surface-treatments button:hover {
+ color: var(--text-primary);
+ border-color: var(--border-strong);
+ background: var(--surface-hover);
+}
+
+.surface-treatments button.is-active {
+ color: var(--accent-ink);
+ border-color: color-mix(in srgb, var(--accent) 42%, var(--border-subtle));
+ background: color-mix(in srgb, var(--accent-soft) 72%, var(--surface-base));
+ box-shadow: inset 3px 0 var(--accent);
+}
+
+.surface-treatments button.is-active small {
+ color: color-mix(in srgb, var(--accent-ink) 64%, var(--text-tertiary));
+}
+
+.surface-treatments button:focus-visible {
+ outline: 2px solid var(--focus-ring);
+ outline-offset: 2px;
+}
+
+.surface-materials {
+ display: grid;
+ gap: 6px;
+ margin: 0 14px 16px;
+}
+
+.surface-materials button {
+ display: grid;
+ min-height: 54px;
+ grid-template-columns: minmax(0, 1fr) 16px;
+ align-items: center;
+ gap: 9px;
+ padding: 9px 10px;
+ border: 1px solid var(--border-subtle);
+ border-radius: var(--radius-md);
+ color: var(--text-secondary);
+ background: var(--surface-subtle);
+ cursor: pointer;
+ text-align: left;
+ transition:
+ color var(--motion-standard) var(--ease-standard),
+ border-color var(--motion-standard) var(--ease-standard),
+ background var(--motion-standard) var(--ease-standard),
+ box-shadow var(--motion-standard) var(--ease-standard);
+}
+
+.surface-materials button > span {
+ display: grid;
+ min-width: 0;
+ gap: 2px;
+}
+
+.surface-materials button strong {
+ color: inherit;
+ font-size: var(--type-meta);
+ font-weight: 680;
+}
+
+.surface-materials button small {
+ display: -webkit-box;
+ overflow: hidden;
+ color: var(--text-tertiary);
+ font-size: var(--type-caption);
+ line-height: 1.35;
+ -webkit-box-orient: vertical;
+ -webkit-line-clamp: 2;
+}
+
+.surface-materials button:hover {
+ color: var(--text-primary);
+ border-color: var(--border-strong);
+ background: var(--surface-hover);
+}
+
+.surface-materials button.is-active {
+ color: var(--accent-ink);
+ border-color: color-mix(in srgb, var(--accent) 42%, var(--border-subtle));
+ background: color-mix(in srgb, var(--accent-soft) 72%, var(--surface-base));
+ box-shadow: inset 3px 0 var(--accent);
+}
+
+.surface-materials button.is-active small {
+ color: color-mix(in srgb, var(--accent-ink) 64%, var(--text-tertiary));
+}
+
+.surface-materials button:focus-visible {
+ outline: 2px solid var(--focus-ring);
+ outline-offset: 2px;
+}
+
+.color-harmony-picker {
+ display: grid;
+ gap: 6px;
+ margin: 0 14px 12px;
+}
+
+.color-harmony-picker > button {
+ display: grid;
+ min-height: 54px;
+ grid-template-columns: 44px minmax(0, 1fr) 16px;
+ align-items: center;
+ gap: 9px;
+ padding: 8px 10px;
+ border: 1px solid var(--border-subtle);
+ border-radius: var(--radius-md);
+ color: var(--text-secondary);
+ background: var(--surface-subtle);
+ cursor: pointer;
+ text-align: left;
+ transition:
+ color var(--motion-standard) var(--ease-standard),
+ border-color var(--motion-standard) var(--ease-standard),
+ background var(--motion-standard) var(--ease-standard),
+ box-shadow var(--motion-standard) var(--ease-standard);
+}
+
+.color-harmony-picker > button:hover {
+ color: var(--text-primary);
+ border-color: var(--border-strong);
+ background: var(--surface-hover);
+}
+
+.color-harmony-picker > button.is-active {
+ color: var(--accent-ink);
+ border-color: color-mix(in srgb, var(--accent) 42%, var(--border-subtle));
+ background: color-mix(in srgb, var(--accent-soft) 72%, var(--surface-base));
+ box-shadow: inset 3px 0 var(--accent);
+}
+
+.color-harmony-picker > button:focus-visible {
+ outline: 2px solid var(--focus-ring);
+ outline-offset: 2px;
+}
+
+.color-harmony-picker > button > span:not(.color-harmony-picker__swatches) {
+ display: grid;
+ min-width: 0;
+ gap: 2px;
+}
+
+.color-harmony-picker strong {
+ color: inherit;
+ font-size: var(--type-meta);
+ font-weight: 680;
+}
+
+.color-harmony-picker small {
+ display: -webkit-box;
+ overflow: hidden;
+ color: var(--text-tertiary);
+ font-size: var(--type-caption);
+ line-height: 1.35;
+ -webkit-box-orient: vertical;
+ -webkit-line-clamp: 2;
+}
+
+.color-harmony-picker > button.is-active small {
+ color: color-mix(in srgb, var(--accent-ink) 64%, var(--text-tertiary));
+}
+
+.color-harmony-picker__swatches {
+ display: grid;
+ width: 40px;
+ height: 30px;
+ grid-template-columns: repeat(3, 1fr);
+ gap: 2px;
+ overflow: hidden;
+ padding: 3px;
+ border: 1px solid color-mix(in srgb, var(--border-strong) 82%, transparent);
+ border-radius: 9px;
+ background: var(--surface-base);
+ box-shadow: var(--shadow-control);
+}
+
+.color-harmony-picker__swatches i {
+ min-width: 0;
+ border-radius: 4px;
+ background: var(--harmony-swatch);
+ box-shadow: inset 0 0 0 1px rgb(255 255 255 / 7%);
+}
+
+.semantic-harmony-status {
+ display: grid;
+ min-width: 0;
+ grid-template-columns: 16px minmax(0, 1fr);
+ align-items: start;
+ gap: 8px;
+ margin: 2px 14px 16px;
+ padding: 10px;
+ border: 1px solid var(--border-subtle);
+ border-radius: var(--radius-md);
+ color: var(--text-secondary);
+ background: var(--surface-subtle);
+}
+
+.semantic-harmony-status[data-authored="true"] {
+ border-color: color-mix(in srgb, var(--accent) 28%, var(--border-subtle));
+ background: color-mix(in srgb, var(--accent-soft) 38%, var(--surface-subtle));
+}
+
+.semantic-harmony-status > svg {
+ margin-top: 2px;
+ color: var(--accent-ink);
+}
+
+.semantic-harmony-status > span {
+ display: grid;
+ min-width: 0;
+ gap: 3px;
+}
+
+.semantic-harmony-status strong {
+ color: var(--text-primary);
+ font-size: var(--type-meta);
+ font-weight: 670;
+}
+
+.semantic-harmony-status small {
+ color: var(--text-tertiary);
+ font-size: var(--type-caption);
+ line-height: 1.45;
+}
+
+.interface-system-status {
+ display: grid;
+ min-width: 0;
+ grid-template-columns: 16px minmax(0, 1fr);
+ align-items: start;
+ gap: 8px;
+ margin: 0 14px 14px;
+ padding: 10px;
+ border: 1px solid color-mix(in srgb, var(--accent) 24%, var(--border-subtle));
+ border-radius: var(--radius-md);
+ color: var(--accent-ink);
+ background: color-mix(in srgb, var(--accent-soft) 42%, var(--surface-subtle));
+}
+
+.interface-system-status > svg {
+ margin-top: 2px;
+}
+
+.interface-system-status > span {
+ display: grid;
+ min-width: 0;
+ gap: 3px;
+}
+
+.interface-system-status strong {
+ color: var(--text-primary);
+ font-size: var(--type-meta);
+ font-weight: 670;
+}
+
+.interface-system-status small {
+ color: var(--text-tertiary);
+ font-size: var(--type-caption);
+ line-height: 1.45;
+}
+
+.visual-quality {
+ --visual-quality-tone: var(--success, #3aa675);
+ --visual-quality-ink: #2f956d;
+
+ margin: 0 14px 16px;
+ overflow: clip;
+ border: 1px solid
+ color-mix(in srgb, var(--visual-quality-tone) 30%, var(--border-subtle));
+ border-radius: var(--radius-md);
+ background: color-mix(
+ in srgb,
+ var(--visual-quality-tone) 5%,
+ var(--surface-subtle)
+ );
+}
+
+.visual-quality[data-status="attention"] {
+ --visual-quality-tone: #d68a2f;
+ --visual-quality-ink: #b66d18;
+
+ border-color: color-mix(
+ in srgb,
+ var(--visual-quality-tone) 34%,
+ var(--border-subtle)
+ );
+ background: color-mix(
+ in srgb,
+ var(--visual-quality-tone) 6%,
+ var(--surface-subtle)
+ );
+}
+
+.visual-quality > summary {
+ display: grid;
+ min-height: 58px;
+ grid-template-columns: 24px minmax(0, 1fr) auto 16px;
+ align-items: center;
+ gap: 8px;
+ padding: 9px 10px;
+ color: var(--text-secondary);
+ cursor: pointer;
+ list-style: none;
+}
+
+.visual-quality > summary::-webkit-details-marker {
+ display: none;
+}
+
+.visual-quality > summary:hover {
+ background: color-mix(in srgb, var(--surface-hover) 66%, transparent);
+}
+
+.visual-quality > summary:focus-visible {
+ outline: 2px solid var(--focus-ring);
+ outline-offset: -2px;
+}
+
+.visual-quality__icon {
+ display: grid;
+ width: 24px;
+ height: 24px;
+ place-items: center;
+ border-radius: 8px;
+ color: var(--visual-quality-ink);
+ background: color-mix(in srgb, var(--visual-quality-tone) 13%, transparent);
+}
+
+.visual-quality > summary > span:nth-child(2) {
+ display: grid;
+ min-width: 0;
+ gap: 2px;
+}
+
+.visual-quality > summary strong {
+ color: var(--text-primary);
+ font-size: var(--type-meta);
+ font-weight: 680;
+}
+
+.visual-quality > summary small {
+ color: var(--text-tertiary);
+ font-size: var(--type-caption);
+ line-height: 1.4;
+}
+
+.visual-quality > summary em {
+ padding: 3px 6px;
+ border-radius: 999px;
+ color: var(--visual-quality-ink);
+ background: color-mix(in srgb, var(--visual-quality-tone) 11%, transparent);
+ font-size: 8px;
+ font-style: normal;
+ font-weight: 720;
+}
+
+.visual-quality > summary > svg {
+ transition: transform var(--motion-standard) var(--ease-standard);
+}
+
+.visual-quality[open] > summary > svg {
+ transform: rotate(90deg);
+}
+
+.visual-quality__details {
+ display: grid;
+ gap: 10px;
+ padding: 10px;
+ border-top: 1px solid var(--border-subtle);
+ background: color-mix(in srgb, var(--surface-base) 72%, transparent);
+}
+
+.visual-quality__checks {
+ display: grid;
+ grid-template-columns: repeat(2, minmax(0, 1fr));
+ gap: 6px;
+}
+
+.visual-quality__checks > div,
+.visual-quality__guard {
+ display: flex;
+ min-width: 0;
+ min-height: 30px;
+ align-items: center;
+ justify-content: space-between;
+ gap: 6px;
+ padding: 6px 8px;
+ border: 1px solid var(--border-subtle);
+ border-radius: var(--radius-sm);
+ background: var(--surface-subtle);
+}
+
+.visual-quality__checks span {
+ display: inline-flex;
+ min-width: 0;
+ align-items: center;
+ gap: 5px;
+ overflow: hidden;
+ color: var(--text-secondary);
+ font-size: var(--type-caption);
+ text-overflow: ellipsis;
+ white-space: nowrap;
+}
+
+.visual-quality__checks svg {
+ flex: none;
+ color: var(--visual-quality-ink);
+}
+
+.visual-quality__checks strong,
+.visual-quality__guard strong {
+ flex: none;
+ color: var(--text-primary);
+ font-size: var(--type-caption);
+ font-variant-numeric: tabular-nums;
+}
+
+.visual-quality__checks > div[data-protected="true"] {
+ border-color: color-mix(in srgb, var(--accent) 24%, var(--border-subtle));
+}
+
+.visual-quality__guard {
+ color: var(--text-secondary);
+ font-size: var(--type-caption);
+}
+
+.visual-quality__details > p {
+ margin: 0;
+ color: var(--text-tertiary);
+ font-size: var(--type-caption);
+ line-height: 1.5;
+}
+
+.inspector-disclosure {
+ margin: 0 14px 16px;
+ overflow: clip;
+ border: 1px solid var(--border-subtle);
+ border-radius: var(--radius-md);
+ background: var(--surface-subtle);
+}
+
+.inspector-disclosure > summary {
+ display: grid;
+ min-height: 50px;
+ grid-template-columns: minmax(0, 1fr) 18px;
+ align-items: center;
+ gap: 9px;
+ padding: 8px 10px;
+ color: var(--text-secondary);
+ cursor: pointer;
+ list-style: none;
+ transition:
+ color var(--motion-standard) var(--ease-standard),
+ background var(--motion-standard) var(--ease-standard);
+}
+
+.inspector-disclosure > summary::-webkit-details-marker {
+ display: none;
+}
+
+.inspector-disclosure > summary > span {
+ display: grid;
+ min-width: 0;
+ gap: 2px;
+}
+
+.inspector-disclosure > summary strong {
+ color: var(--text-primary);
+ font-size: var(--type-meta);
+ font-weight: 680;
+}
+
+.inspector-disclosure > summary small {
+ color: var(--text-tertiary);
+ font-size: var(--type-caption);
+ line-height: 1.4;
+}
+
+.inspector-disclosure > summary > svg {
+ transition: transform var(--motion-standard) var(--ease-standard);
+}
+
+.inspector-disclosure > summary:hover {
+ color: var(--text-primary);
+ background: var(--surface-hover);
+}
+
+.inspector-disclosure > summary:focus-visible {
+ outline: 2px solid var(--focus-ring);
+ outline-offset: -2px;
+}
+
+.inspector-disclosure[open] > summary {
+ border-bottom: 1px solid var(--border-subtle);
+ color: var(--accent-ink);
+ background: color-mix(in srgb, var(--accent-soft) 46%, var(--surface-base));
+}
+
+.inspector-disclosure[open] > summary > svg {
+ transform: rotate(90deg);
+}
+
+.inspector-disclosure__content {
+ display: grid;
+ gap: 0;
+ padding: 14px 0 2px;
+ background: var(--surface-base);
+}
+
+.inspector-disclosure__content > .ui-select-field,
+.inspector-disclosure__content > .field,
+.inspector-disclosure__content > .semantic-harmony-status {
+ margin-right: 12px;
+ margin-left: 12px;
+}
+
+.inspector-disclosure__content > .ui-select-field {
+ margin-bottom: 12px;
+}
+
+.inspector-subsection-label {
+ margin: 0 14px 10px;
+ padding-top: 2px;
+}
+
@media (max-width: 1120px) {
.editor-focus-toggle span,
.inspector-toggle span,
@@ -495,6 +1091,10 @@
display: none;
}
+ .editor-layout--inspector-open .canvas-toolbar {
+ padding-right: 296px;
+ }
+
.panel-resizer--inspector {
display: none;
}
diff --git a/apps/desktop/src/styles/experience-beta6.css b/apps/desktop/src/styles/experience-beta6.css
index c3f1bbc..d0ad350 100644
--- a/apps/desktop/src/styles/experience-beta6.css
+++ b/apps/desktop/src/styles/experience-beta6.css
@@ -209,7 +209,6 @@
overflow-x: hidden;
overflow-y: auto;
overscroll-behavior: contain;
- scrollbar-gutter: stable;
}
.onboarding--guided .modal-close {
@@ -642,7 +641,32 @@
}
}
-@media (max-width: 980px) {
+@media (min-width: 1180px) {
+ :root {
+ --sidebar: 224px;
+ }
+
+ .app-sidebar {
+ padding-inline: 16px;
+ }
+
+ .app-nav__item {
+ height: 42px;
+ border-radius: 10px;
+ }
+
+ .home-page,
+ .page--themes,
+ .companions-page {
+ width: min(1260px, calc(100% - 36px));
+ }
+
+ .configuration-dock {
+ width: min(1080px, calc(100% - 36px));
+ }
+}
+
+@media (max-width: 1060px) {
.page-heading {
gap: 18px;
}
@@ -663,15 +687,20 @@
display: none;
}
+ .theme-library-workspace--detail {
+ min-height: 386px;
+ flex-basis: 386px;
+ }
+
.theme-library-workspace--detail .featured-theme {
display: grid;
- min-height: 360px;
- grid-template-columns: minmax(0, 1.35fr) minmax(250px, 0.65fr);
- grid-template-rows: auto minmax(320px, 1fr);
+ min-height: 382px;
+ grid-template-columns: minmax(0, 1fr);
+ grid-template-rows: auto minmax(218px, 1fr) auto;
}
.theme-library-workspace--detail .featured-theme__preview {
- min-height: 320px;
+ min-height: 218px;
grid-row: 2;
grid-column: 1;
}
@@ -682,19 +711,51 @@
}
.theme-library-workspace--detail .featured-theme__copy {
- display: flex;
- max-height: none;
- flex-direction: column;
- justify-content: center;
- grid-row: 2;
- grid-column: 2;
- overflow-y: auto;
- padding: 18px;
+ display: grid;
+ max-height: 128px;
+ grid-template-columns: minmax(300px, 0.9fr) minmax(0, 1.1fr);
+ grid-template-rows: auto auto;
+ grid-row: 3;
+ grid-column: 1;
+ overflow-y: hidden;
+ padding: 8px 14px 7px;
+ border-top: 1px solid var(--border-subtle);
+ border-left: 0;
+ }
+
+ .theme-library-workspace--detail .theme-detail-card__identity > p {
+ min-height: 0;
+ -webkit-line-clamp: 2;
+ }
+
+ .theme-library-workspace--detail .theme-facts {
+ padding: 0 0 0 14px;
border-top: 0;
border-left: 1px solid var(--border-subtle);
}
+ .theme-library-workspace--detail .theme-detail-card__title-row {
+ align-items: center;
+ flex-direction: row;
+ }
+
+ .theme-library-workspace--detail .theme-detail-card__title-row h2 {
+ font-size: 20px;
+ }
+
+ .theme-library-workspace--detail
+ .theme-detail-card__title-row
+ .secondary-button {
+ width: auto;
+ }
+
+ .theme-library-workspace--detail .theme-effect-summary {
+ grid-template-columns: minmax(0, 1fr);
+ }
+
.companions-layout--detail {
+ min-height: 400px;
+ flex-basis: 400px;
grid-template-areas: "detail";
}
@@ -703,7 +764,76 @@
}
.companions-layout--detail .companion-preview-panel {
- display: flex;
+ display: grid;
+ grid-template-columns: minmax(0, 1fr);
+ grid-template-rows: auto minmax(218px, 1fr) auto;
+ }
+
+ .companions-layout--detail .compact-detail-back {
+ grid-row: 1;
+ grid-column: 1 / -1;
+ }
+
+ .companions-layout--detail .companion-preview-stage {
+ min-width: 0;
+ grid-row: 2;
+ grid-column: 1;
+ }
+
+ .companions-layout--detail .companion-detail-summary {
+ min-width: 0;
+ min-height: 0;
+ align-items: stretch;
+ align-self: stretch;
+ justify-content: flex-start;
+ gap: 12px;
+ grid-row: 3;
+ grid-column: 1;
+ overflow: hidden;
+ padding: 9px 14px 10px;
+ border-top: 1px solid var(--border-subtle);
+ border-left: 0;
+ }
+
+ .companions-layout--detail .companion-detail-summary > div:first-child > p {
+ display: -webkit-box;
+ overflow: hidden;
+ -webkit-box-orient: vertical;
+ -webkit-line-clamp: 2;
+ }
+
+ .companions-layout--detail .companion-detail-summary__side {
+ gap: 8px;
+ }
+
+ .companions-layout--detail .companion-detail-summary__badges {
+ flex-wrap: nowrap;
+ }
+
+ .companions-layout--detail .companion-detail-summary__badges span {
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+ }
+
+ .companions-layout--detail .companion-placement-controls {
+ gap: 6px;
+ padding-top: 7px;
+ }
+
+ .companions-layout--detail .companion-placement-modes button {
+ min-height: 30px;
+ }
+
+ .companions-layout--detail .companion-detail-summary__actions {
+ flex-wrap: nowrap;
+ }
+
+ .companions-layout--detail .companion-detail-summary__actions button {
+ min-width: 0;
+ flex: 1 1 auto;
+ overflow: hidden;
+ text-overflow: ellipsis;
}
.configuration-dock {
@@ -776,6 +906,88 @@
.home-current .workspace-preview {
min-height: 286px;
}
+
+ .page--themes,
+ .companions-page {
+ padding-top: 20px;
+ padding-bottom: 16px;
+ }
+
+ .page--themes > .page-heading,
+ .companions-page > .page-heading {
+ margin-bottom: 12px;
+ }
+
+ .page--themes > .resource-library-toolbar,
+ .companions-page > .companions-toolbar {
+ margin-bottom: 10px;
+ }
+}
+
+@media (max-width: 1060px) and (max-height: 760px) {
+ .theme-library-workspace--detail,
+ .companions-layout--detail {
+ min-height: clamp(330px, calc(100dvh - 330px), 400px);
+ flex-basis: clamp(330px, calc(100dvh - 330px), 400px);
+ }
+
+ .theme-library-workspace--detail .featured-theme {
+ min-height: 326px;
+ grid-template-rows: auto minmax(184px, 1fr) auto;
+ }
+
+ .theme-library-workspace--detail .featured-theme__preview {
+ min-height: 184px;
+ }
+
+ .theme-library-workspace--detail .featured-theme__copy {
+ max-height: 120px;
+ grid-template-columns: minmax(300px, 0.9fr) minmax(0, 1.1fr);
+ }
+
+ .companions-layout--detail .companion-preview-panel {
+ grid-template-rows: auto minmax(220px, 1fr) auto;
+ }
+
+ .companions-layout--detail .companion-detail-summary {
+ grid-template-columns: minmax(150px, 0.55fr) minmax(0, 1.45fr);
+ gap: 10px;
+ padding: 6px 12px 7px;
+ }
+
+ .companions-layout--detail .companion-detail-summary > div:first-child > p {
+ display: none;
+ }
+
+ .companions-layout--detail .companion-detail-summary__side {
+ grid-template-areas: "badges placement";
+ grid-template-columns: auto minmax(0, 1fr);
+ align-items: center;
+ gap: 8px;
+ }
+
+ .companions-layout--detail
+ .companion-detail-summary__badges
+ span:first-child:not(:last-child) {
+ display: none;
+ }
+
+ .companions-layout--detail .companion-placement-controls {
+ grid-template-areas: "modes size";
+ grid-template-columns: minmax(180px, 1fr) minmax(150px, 0.86fr);
+ align-items: center;
+ gap: 8px;
+ padding-top: 0;
+ border-top: 0;
+ }
+
+ .companions-layout--detail .companion-placement-controls__heading {
+ display: none;
+ }
+
+ .companions-layout--detail .companion-detail-summary__actions {
+ display: none;
+ }
}
.toast {
diff --git a/apps/desktop/src/styles/interaction-surfaces.css b/apps/desktop/src/styles/interaction-surfaces.css
index f6f5737..6c723b2 100644
--- a/apps/desktop/src/styles/interaction-surfaces.css
+++ b/apps/desktop/src/styles/interaction-surfaces.css
@@ -1,81 +1,8 @@
/*
- * Cross-platform interaction chrome. WebView defaults differ substantially
- * between macOS and Windows, so every scroll surface and native form control
- * is normalized here instead of being patched page by page.
+ * Cross-platform native form chrome. Scroll ownership and scrollbar visuals
+ * live in scroll-surfaces.css so later workspace styles cannot override them.
*/
-* {
- scrollbar-color: var(--scrollbar-thumb) var(--scrollbar-track);
- scrollbar-width: thin;
-}
-
-*::-webkit-scrollbar {
- width: var(--scrollbar-size);
- height: var(--scrollbar-size);
-}
-
-*::-webkit-scrollbar-track,
-*::-webkit-scrollbar-track-piece,
-*::-webkit-scrollbar-corner {
- background: var(--scrollbar-track);
-}
-
-*::-webkit-scrollbar-thumb {
- min-width: 34px;
- min-height: 34px;
- border: 3px solid transparent;
- border-radius: 999px;
- background: var(--scrollbar-thumb);
- background-clip: padding-box;
-}
-
-*::-webkit-scrollbar-thumb:hover {
- background: var(--scrollbar-thumb-hover);
- background-clip: padding-box;
-}
-
-*::-webkit-scrollbar-thumb:active {
- background: var(--scrollbar-thumb-active);
- background-clip: padding-box;
-}
-
-*::-webkit-scrollbar-button {
- display: none;
- width: 0;
- height: 0;
-}
-
-:where(
- .app-main,
- .app-main__viewport,
- .theme-list,
- .companion-list,
- .layer-stack,
- .inspector,
- .creator-inspector-panel,
- .creator-frame-strip__items,
- .new-theme-existing-list,
- .new-theme-options,
- .onboarding,
- .onboarding-content,
- .update-dialog__notes,
- .diagnostics-dialog,
- .diagnostics-dialog pre,
- .atlas-grid-preview,
- .canvas-stage[data-preview-zoom="actual"]
-) {
- overscroll-behavior: contain;
-}
-
-:where(
- .creator-steps,
- .timeline-thumbs,
- .creator-frame-strip__items
-)::-webkit-scrollbar {
- width: var(--scrollbar-size-compact);
- height: var(--scrollbar-size-compact);
-}
-
/* Selects retain native keyboard and assistive technology behavior while the
* closed control follows the product design system on both platforms. */
:where(.ui-select, .creator-select-control):focus-within select {
diff --git a/apps/desktop/src/styles/libraries.css b/apps/desktop/src/styles/libraries.css
index 5bffdfb..748c934 100644
--- a/apps/desktop/src/styles/libraries.css
+++ b/apps/desktop/src/styles/libraries.css
@@ -1,13 +1,177 @@
.page--themes,
.companions-page {
+ --resource-library-min-height: 380px;
+
+ display: flex;
width: min(1240px, calc(100% - 44px));
- padding-bottom: 32px;
+ min-height: 100%;
+ box-sizing: border-box;
+ flex-direction: column;
+ padding-top: 28px;
+ padding-bottom: 28px;
+}
+
+.page--themes > .page-heading,
+.page--themes > .resource-library-toolbar,
+.companions-page > .page-heading,
+.companions-page > .companions-toolbar,
+.companions-page > .companion-projects {
+ flex: none;
+}
+
+.page--themes > .page-heading,
+.companions-page > .page-heading {
+ margin-bottom: 18px;
+}
+
+.page--themes > .resource-library-toolbar,
+.companions-page > .companions-toolbar {
+ margin-bottom: 16px;
+}
+
+.resource-library-toolbar {
+ display: flex;
+ min-height: 42px;
+ align-items: center;
+ justify-content: space-between;
+ gap: 12px;
+ margin: 0 0 16px;
+ padding: 3px;
+ border: 1px solid var(--border-subtle);
+ border-radius: var(--radius-md);
+ background: var(--surface-subtle);
+}
+
+.resource-library-toolbar .theme-collection-tabs {
+ flex: none;
+ margin: 0;
+ padding: 0;
+ border: 0;
+ background: transparent;
+}
+
+.library-search-field {
+ display: grid;
+ width: min(280px, 42%);
+ min-height: 34px;
+ grid-template-columns: auto minmax(0, 1fr) auto auto;
+ align-items: center;
+ gap: 7px;
+ padding: 0 7px 0 10px;
+ border: 1px solid transparent;
+ border-radius: calc(var(--radius-md) - 3px);
+ color: var(--text-tertiary);
+ background: var(--surface-base);
+ transition:
+ border-color var(--motion-micro) var(--ease-standard),
+ box-shadow var(--motion-micro) var(--ease-standard),
+ color var(--motion-micro) var(--ease-standard);
+}
+
+.library-search-field:focus-within {
+ color: var(--text-secondary);
+ border-color: color-mix(in srgb, var(--accent) 48%, var(--border-strong));
+ box-shadow: 0 0 0 3px color-mix(in srgb, var(--accent) 11%, transparent);
+}
+
+.library-search-field input {
+ min-width: 0;
+ width: 100%;
+ padding: 0;
+ border: 0;
+ outline: 0;
+ color: var(--text-primary);
+ background: transparent;
+ font: inherit;
+ font-size: var(--type-caption);
+}
+
+.library-search-field input::placeholder {
+ color: var(--text-tertiary);
+}
+
+.library-search-field input::-webkit-search-cancel-button {
+ display: none;
+}
+
+.library-search-field output {
+ color: var(--text-tertiary);
+ font-size: 9px;
+ font-variant-numeric: tabular-nums;
+}
+
+.library-search-field button {
+ display: grid;
+ width: 24px;
+ height: 24px;
+ place-items: center;
+ padding: 0;
+ border: 0;
+ border-radius: 6px;
+ color: var(--text-tertiary);
+ background: transparent;
+ cursor: pointer;
+}
+
+.library-search-field button:hover {
+ color: var(--text-primary);
+ background: var(--surface-hover);
+}
+
+.library-search-field button:focus-visible {
+ outline: 2px solid var(--focus-ring);
+ outline-offset: 1px;
+}
+
+.library-filter-empty {
+ display: grid;
+ min-height: 100%;
+ place-content: center;
+ justify-items: center;
+ gap: 6px;
+ padding: 24px;
+ color: var(--text-tertiary);
+ text-align: center;
+}
+
+.library-filter-empty strong {
+ color: var(--text-primary);
+ font-size: var(--type-label);
+}
+
+.library-filter-empty span {
+ max-width: 260px;
+ color: var(--text-secondary);
+ font-size: var(--type-caption);
+ line-height: 1.45;
+}
+
+.library-filter-empty button {
+ min-height: 30px;
+ margin-top: 4px;
+ padding: 0 10px;
+ border: 1px solid var(--border-subtle);
+ border-radius: var(--radius-sm);
+ color: var(--text-secondary);
+ background: var(--surface-base);
+ cursor: pointer;
+ font: inherit;
+ font-size: var(--type-caption);
+ font-weight: 650;
+}
+
+.library-filter-empty button:hover {
+ color: var(--text-primary);
+ border-color: var(--border-strong);
+ background: var(--surface-hover);
}
.theme-library-workspace {
display: grid;
- height: clamp(360px, calc(100dvh - 360px), 520px);
- min-height: 0;
+ width: 100%;
+ min-height: var(--resource-library-min-height);
+ max-height: 550px;
+ flex: 1 1 var(--resource-library-min-height);
grid-template-columns: minmax(360px, 0.88fr) minmax(540px, 1.35fr);
gap: 18px;
align-items: stretch;
@@ -125,98 +289,593 @@
box-shadow: var(--shadow-flyout);
}
-.theme-row__menu button {
- display: flex;
- width: 100%;
- min-height: var(--control-compact);
+.theme-row__menu button {
+ display: flex;
+ width: 100%;
+ min-height: var(--control-compact);
+ align-items: center;
+ gap: 8px;
+ padding: 0 9px;
+ border: 0;
+ color: var(--text-secondary);
+ background: transparent;
+ cursor: pointer;
+ text-align: left;
+ font-size: 10px;
+}
+
+.theme-row__menu button:hover {
+ color: var(--text-primary);
+ background: var(--surface-hover);
+}
+
+.theme-row__menu button.is-danger {
+ color: #c54848;
+}
+
+.theme-row:nth-last-child(-n + 2) .theme-row__menu {
+ top: auto;
+ bottom: calc(100% + 4px);
+}
+
+.theme-library-workspace .featured-theme {
+ container-type: inline-size;
+ height: 100%;
+ min-height: 0;
+ grid-template-columns: minmax(0, 1fr);
+ grid-template-rows: minmax(280px, 1.65fr) auto;
+ overflow: hidden;
+ border-radius: var(--radius-lg);
+ box-shadow: var(--shadow-panel);
+}
+
+.theme-library-workspace .featured-theme__preview {
+ position: relative;
+ isolation: isolate;
+ display: grid;
+ min-height: 280px;
+ grid-template-rows: minmax(0, 1fr);
+ overflow: hidden;
+ padding: 12px;
+ border-radius: var(--radius-lg) var(--radius-lg) 0 0;
+}
+
+.featured-theme__preview > .theme-preview-controls {
+ position: absolute;
+ z-index: 30;
+ top: 12px;
+ right: 12px;
+ display: grid;
+ justify-items: end;
+ gap: 7px;
+ pointer-events: none;
+}
+
+.theme-preview-controls__trigger {
+ display: flex;
+ min-height: 32px;
+ align-items: center;
+ gap: 7px;
+ padding: 0 10px;
+ border: 1px solid rgb(255 255 255 / 15%);
+ border-radius: 9px;
+ color: rgb(255 255 255 / 88%);
+ background: rgb(12 14 18 / 68%);
+ cursor: pointer;
+ font-size: var(--type-caption);
+ font-weight: 650;
+ box-shadow: 0 8px 22px rgb(0 0 0 / 16%);
+ backdrop-filter: blur(14px) saturate(1.08);
+ opacity: 0;
+ pointer-events: none;
+ transform: translateY(-4px);
+ transition:
+ opacity var(--motion-micro) var(--ease-standard),
+ color var(--motion-micro) var(--ease-standard),
+ border-color var(--motion-micro) var(--ease-standard),
+ background var(--motion-micro) var(--ease-standard),
+ transform var(--motion-micro) var(--ease-standard);
+}
+
+.featured-theme__preview:hover .theme-preview-controls__trigger,
+.theme-preview-controls:focus-within .theme-preview-controls__trigger,
+.theme-preview-controls[data-open="true"] .theme-preview-controls__trigger {
+ opacity: 1;
+ pointer-events: auto;
+ transform: translateY(0);
+}
+
+.theme-preview-controls__trigger:hover,
+.theme-preview-controls[data-open="true"] .theme-preview-controls__trigger {
+ border-color: rgb(255 255 255 / 25%);
+ background: rgb(12 14 18 / 82%);
+}
+
+.theme-preview-controls[data-open="true"]
+ .theme-preview-controls__trigger
+ > svg:last-child {
+ transform: rotate(180deg);
+}
+
+.theme-preview-controls__trigger > svg:last-child {
+ transition: transform var(--motion-micro) var(--ease-standard);
+}
+
+.theme-preview-controls__panel {
+ position: absolute;
+ top: calc(100% + 7px);
+ right: 0;
+ display: grid;
+ width: min(350px, calc(100vw - 72px));
+ min-width: 0;
+ grid-template-columns: minmax(176px, 1fr) auto;
+ align-items: end;
+ gap: 10px;
+ padding: 11px;
+ border: 1px solid rgb(255 255 255 / 15%);
+ border-radius: 12px;
+ color: rgb(255 255 255 / 90%);
+ background: rgb(12 14 18 / 86%);
+ box-shadow: 0 16px 44px rgb(0 0 0 / 28%);
+ backdrop-filter: blur(18px) saturate(1.08);
+ pointer-events: auto;
+ animation: theme-preview-controls-in var(--motion-standard)
+ var(--ease-emphasized) both;
+}
+
+@media (hover: none) {
+ .theme-preview-controls__trigger {
+ opacity: 1;
+ pointer-events: auto;
+ transform: translateY(0);
+ }
+}
+
+.theme-preview-controls__panel[hidden] {
+ display: none;
+}
+
+.theme-preview-controls__panel .ui-select-field__label,
+.theme-preview-controls__comparison > span {
+ color: rgb(255 255 255 / 62%);
+}
+
+.theme-preview-controls__panel .ui-select select {
+ border-color: rgb(255 255 255 / 14%);
+ color: rgb(255 255 255 / 90%);
+ background: rgb(255 255 255 / 7%);
+}
+
+.theme-preview-controls__panel .ui-select > svg {
+ color: rgb(255 255 255 / 58%);
+}
+
+.theme-preview-controls__comparison {
+ display: grid;
+ gap: 7px;
+}
+
+.theme-preview-controls__comparison > span {
+ font-size: 11px;
+ font-weight: 620;
+}
+
+.theme-preview-controls__comparison > div {
+ display: flex;
+ gap: 3px;
+ padding: 3px;
+ border: 1px solid rgb(255 255 255 / 14%);
+ border-radius: 9px;
+ background: rgb(255 255 255 / 6%);
+}
+
+.theme-preview-controls__comparison button {
+ min-height: 30px;
+ padding: 0 10px;
+ border: 0;
+ border-radius: 6px;
+ color: rgb(255 255 255 / 62%);
+ background: transparent;
+ cursor: pointer;
+ font-size: var(--type-caption);
+ font-weight: 650;
+}
+
+.theme-preview-controls__comparison button.is-active {
+ color: #17191d;
+ background: rgb(255 255 255 / 92%);
+ box-shadow: 0 2px 8px rgb(0 0 0 / 18%);
+}
+
+.theme-preview-controls__motion {
+ grid-column: 1 / -1;
+ display: flex;
+ min-width: 0;
+ min-height: 42px;
+ align-items: center;
+ gap: 9px;
+ padding: 7px 10px;
+ border: 1px solid rgb(255 255 255 / 14%);
+ border-radius: 9px;
+ color: rgb(255 255 255 / 88%);
+ background: rgb(255 255 255 / 6%);
+ cursor: pointer;
+ text-align: left;
+ transition:
+ border-color var(--motion-micro) var(--ease-standard),
+ background var(--motion-micro) var(--ease-standard);
+}
+
+.theme-preview-controls__motion > svg {
+ flex: none;
+ color: rgb(255 255 255 / 76%);
+}
+
+.theme-preview-controls__motion > span {
+ display: grid;
+ min-width: 0;
+ gap: 1px;
+}
+
+.theme-preview-controls__motion strong {
+ font-size: var(--type-caption);
+ font-weight: 680;
+}
+
+.theme-preview-controls__motion small {
+ overflow: hidden;
+ color: rgb(255 255 255 / 56%);
+ font-size: 10px;
+ line-height: 1.35;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+}
+
+.theme-preview-controls__motion:hover:not(:disabled) {
+ border-color: rgb(255 255 255 / 24%);
+ background: rgb(255 255 255 / 10%);
+}
+
+.theme-preview-controls__motion:focus-visible {
+ outline: 2px solid rgb(255 255 255 / 78%);
+ outline-offset: 2px;
+}
+
+.theme-preview-controls__motion:disabled {
+ cursor: default;
+ opacity: 0.5;
+}
+
+@keyframes theme-preview-controls-in {
+ from {
+ opacity: 0;
+ transform: translateY(-8px) scale(0.985);
+ }
+
+ to {
+ opacity: 1;
+ transform: translateY(0) scale(1);
+ }
+}
+
+.theme-library-workspace .featured-theme__preview .workspace-preview {
+ position: relative;
+ z-index: 1;
+ grid-row: 1;
+ width: 100%;
+ height: 100%;
+ min-height: 0;
+ border-radius: calc(var(--radius-lg) - 4px);
+ transform: none;
+}
+
+.theme-library-workspace .featured-theme__copy {
+ container-name: theme-detail-copy;
+ container-type: inline-size;
+ display: grid;
+ min-height: 0;
+ grid-template-columns: minmax(280px, 1.08fr) minmax(0, 0.92fr);
+ grid-template-rows: auto auto;
+ align-items: center;
+ gap: 6px 16px;
+ overflow-x: hidden;
+ overflow-y: hidden;
+ padding: 8px 14px 7px;
+}
+
+.theme-detail-card__identity {
+ display: grid;
+ min-width: 0;
+ grid-column: 1;
+ gap: 3px;
+}
+
+.theme-detail-card__identity > span {
+ color: var(--text-tertiary);
+ font-size: var(--type-caption);
+ font-weight: 700;
+ letter-spacing: 0.13em;
+ text-transform: uppercase;
+}
+
+.theme-detail-card__title-row {
+ display: flex;
+ min-width: 0;
+ align-items: center;
+ justify-content: space-between;
+ gap: 12px;
+}
+
+.theme-library-workspace .theme-detail-card__title-row h2 {
+ min-width: 0;
+ margin: 0;
+ overflow: hidden;
+ font-size: 21px;
+ line-height: 1.1;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+}
+
+.theme-detail-card__title-row .secondary-button {
+ min-height: 32px;
+ flex: none;
+ padding-inline: 10px;
+}
+
+.theme-library-workspace .theme-detail-card__identity > p {
+ display: block;
+ min-width: 0;
+ height: 18px;
+ margin: 0;
+ overflow: hidden;
+ color: var(--text-secondary);
+ line-height: 18px;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+}
+
+.theme-library-workspace .theme-detail-card__identity {
+ grid-column: 1;
+}
+
+.theme-library-workspace .theme-facts {
+ min-width: 0;
+ align-self: stretch;
+ grid-row: 1;
+ grid-column: 2;
+ margin: 0;
+ padding: 0 0 0 14px;
+ border-top: 0;
+ border-bottom: 0;
+ border-left: 1px solid var(--border-subtle);
+}
+
+.theme-library-workspace .theme-personality {
+ grid-template-columns: repeat(2, minmax(0, 1fr));
+ gap: 6px 10px;
+}
+
+.theme-library-workspace .theme-personality > div {
+ min-width: 0;
+ justify-content: flex-start;
+}
+
+.theme-library-workspace .theme-personality strong {
+ display: -webkit-box;
+ overflow: hidden;
+ line-height: 1.25;
+ overflow-wrap: anywhere;
+ -webkit-box-orient: vertical;
+ -webkit-line-clamp: 2;
+}
+
+.theme-library-workspace .configuration-state,
+.theme-library-workspace .theme-effect-summary {
+ grid-column: 1 / -1;
+}
+
+.theme-library-workspace .theme-effect-summary {
+ display: grid;
+ min-height: 26px;
+ grid-template-columns: minmax(0, 1fr);
+ align-items: center;
+ gap: 4px;
+ margin-top: 0;
+ padding: 3px 5px;
+}
+
+.theme-library-workspace .theme-effect-summary__copy {
+ display: none;
+}
+
+.theme-library-workspace .theme-effect-summary__list {
+ flex-wrap: nowrap;
+ justify-content: flex-start;
+ border: 0;
+ border-radius: 0;
+ background: transparent;
+}
+
+.theme-library-workspace .theme-effect-summary__list > button {
+ min-height: 21px;
+ padding: 2px 6px;
+}
+
+.theme-effect-summary {
+ display: flex;
+ min-width: 0;
+ align-items: center;
+ justify-content: space-between;
+ gap: 14px;
+ padding: 10px 12px;
+ border: 1px solid var(--border-subtle);
+ border-radius: var(--radius-md);
+ background: color-mix(in srgb, var(--surface-subtle) 78%, transparent);
+}
+
+.theme-effect-summary__copy {
+ display: flex;
+ min-width: 180px;
+ align-items: flex-start;
+ gap: 9px;
+}
+
+.theme-effect-summary__copy > svg {
+ flex: none;
+ margin-top: 2px;
+ color: var(--accent);
+}
+
+.theme-effect-summary__copy > span {
+ display: flex;
+ min-width: 0;
+ flex-direction: column;
+ gap: 2px;
+}
+
+.theme-effect-summary__copy strong {
+ color: var(--text-primary);
+ font-size: var(--type-meta);
+}
+
+.theme-effect-summary__copy small {
+ max-width: 390px;
+ color: var(--text-tertiary);
+ font-size: var(--type-caption);
+ line-height: 1.4;
+}
+
+.theme-effect-summary__list {
+ display: flex;
+ min-width: 0;
+ flex-wrap: wrap;
+ justify-content: flex-end;
+ gap: 5px;
+}
+
+.theme-effect-summary__list > button {
+ appearance: none;
+ display: inline-flex;
+ min-height: 23px;
align-items: center;
- gap: 8px;
- padding: 0 9px;
- border: 0;
- color: var(--text-secondary);
- background: transparent;
+ gap: 5px;
+ padding: 4px 7px;
+ border: 1px solid var(--border-subtle);
+ border-radius: 999px;
+ color: var(--text-tertiary);
+ background: var(--surface-base);
+ font-size: var(--type-caption);
+ font-family: inherit;
+ white-space: nowrap;
cursor: pointer;
- text-align: left;
- font-size: 10px;
}
-.theme-row__menu button:hover {
- color: var(--text-primary);
- background: var(--surface-hover);
+.theme-effect-summary__list i {
+ width: 5px;
+ height: 5px;
+ border-radius: 999px;
+ background: var(--text-tertiary);
+ opacity: 0.45;
}
-.theme-row__menu button.is-danger {
- color: #c54848;
+.theme-effect-summary__list > button[data-active] {
+ color: var(--text-secondary);
+ border-color: color-mix(in srgb, var(--accent) 22%, var(--border-subtle));
+ background: color-mix(in srgb, var(--accent) 6%, var(--surface-base));
}
-.theme-row:nth-last-child(-n + 2) .theme-row__menu {
- top: auto;
- bottom: calc(100% + 4px);
+.theme-effect-summary__list > button[data-active] i {
+ background: var(--accent);
+ box-shadow: 0 0 0 3px color-mix(in srgb, var(--accent) 10%, transparent);
+ opacity: 1;
}
-.theme-library-workspace .featured-theme {
- height: 100%;
- min-height: 0;
- grid-template-columns: 1fr;
- grid-template-rows: minmax(280px, 1fr) auto;
- border-radius: var(--radius-lg);
- box-shadow: var(--shadow-panel);
+.theme-effect-summary__list > button[data-active]:hover,
+.theme-effect-summary__list > button[data-current] {
+ color: var(--text-primary);
+ border-color: color-mix(in srgb, var(--accent) 44%, var(--border-subtle));
+ background: color-mix(in srgb, var(--accent) 11%, var(--surface-base));
}
-.theme-library-workspace .featured-theme__preview {
- --library-preview-scale: 0.75;
- min-height: 280px;
- overflow: hidden;
- padding: 14px;
+.theme-effect-summary__list > button[data-current] {
+ box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--accent) 15%, transparent);
}
-.theme-library-workspace .featured-theme__preview .workspace-preview {
- width: calc(100% / var(--library-preview-scale));
- height: calc(100% / var(--library-preview-scale));
- min-height: 340px;
- transform: scale(var(--library-preview-scale));
- transform-origin: top left;
+.theme-effect-summary__list > button:focus-visible {
+ outline: 2px solid var(--focus-ring);
+ outline-offset: 2px;
}
-.theme-library-workspace .featured-theme__copy {
- display: grid;
- grid-template-columns: minmax(0, 1fr) auto;
- gap: 8px 24px;
- padding: 22px 24px;
+.theme-effect-summary__list > button:disabled {
+ cursor: default;
+ opacity: 0.58;
}
-.theme-library-workspace .featured-theme__copy > span,
-.theme-library-workspace .featured-theme__copy > h2,
-.theme-library-workspace .featured-theme__copy > p {
- grid-column: 1;
-}
+@container theme-detail-copy (max-width: 620px) {
+ .theme-detail-card__title-row {
+ gap: 8px;
+ }
-.theme-library-workspace .featured-theme__copy h2 {
- margin: 3px 0;
- font-size: 26px;
-}
+ .theme-detail-card__title-row .secondary-button {
+ max-width: 148px;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+ }
-.theme-library-workspace .theme-facts {
- grid-row: 1 / span 3;
- grid-column: 2;
- min-width: 170px;
- align-self: stretch;
- margin: 0;
- padding: 0 0 0 22px;
- border-top: 0;
- border-bottom: 0;
- border-left: 1px solid var(--border-subtle);
+ .theme-library-workspace .theme-facts {
+ padding-left: 12px;
+ }
+
+ .theme-effect-summary__copy {
+ display: none;
+ }
+
+ .theme-library-workspace .theme-effect-summary {
+ grid-template-columns: minmax(0, 1fr);
+ }
+
+ .theme-effect-summary__list {
+ gap: 4px;
+ justify-content: flex-start;
+ }
+
+ .theme-effect-summary__list > button {
+ min-height: 21px;
+ padding: 2px 6px;
+ }
}
-.theme-library-workspace .configuration-state,
-.theme-library-workspace .featured-theme__copy .button-row {
- grid-column: 1 / -1;
+@container theme-detail-copy (max-width: 380px) {
+ .theme-detail-card__identity > span {
+ letter-spacing: 0.09em;
+ }
+
+ .theme-detail-card__title-row {
+ align-items: stretch;
+ flex-direction: column;
+ }
+
+ .theme-detail-card__title-row .secondary-button {
+ width: 100%;
+ max-width: none;
+ }
+
+ .theme-personality {
+ gap: 8px 10px;
+ }
+
+ .theme-effect-summary__list > button {
+ font-size: 9px;
+ }
}
.companions-layout {
display: grid;
- height: clamp(360px, calc(100dvh - 360px), 520px);
- min-height: 0;
+ width: 100%;
+ min-height: var(--resource-library-min-height);
+ max-height: 540px;
+ flex: 1 1 var(--resource-library-min-height);
grid-template-areas: "master detail";
grid-template-columns: minmax(350px, 0.82fr) minmax(520px, 1.38fr);
gap: 18px;
@@ -236,6 +895,8 @@
}
.companions-layout .companion-preview-panel {
+ container-name: companion-detail;
+ container-type: inline-size;
display: flex;
min-height: 0;
flex-direction: column;
@@ -250,25 +911,40 @@
.companions-layout .companion-preview-panel .workspace-preview {
min-height: 0;
flex: 1;
- border: 0;
- border-radius: 0;
+ border: 1px solid var(--preview-border-strong);
+ border-radius: calc(var(--radius-lg) - 4px);
}
-.companion-detail-summary {
+.companion-preview-stage {
+ position: relative;
display: flex;
- min-height: 112px;
+ min-height: 0;
+ flex: 1;
+ margin: 9px;
+}
+
+.companion-preview-stage .drag-hint {
+ right: auto;
+ left: 14px;
+}
+
+.companion-detail-summary {
+ display: grid;
+ min-height: 104px;
+ flex: 0 0 auto;
align-items: center;
- justify-content: space-between;
- gap: 20px;
- padding: 17px 20px;
+ grid-template-columns: minmax(170px, 0.72fr) minmax(360px, 1.28fr);
+ gap: 16px;
+ padding: 11px 16px 12px;
border-top: 1px solid var(--border-subtle);
}
.companion-detail-summary > div:first-child {
min-width: 0;
+ flex: 1 1 auto;
}
-.companion-detail-summary span {
+.companion-detail-summary > div:first-child > span {
color: var(--text-tertiary);
font-size: 8px;
font-weight: 700;
@@ -276,7 +952,7 @@
text-transform: uppercase;
}
-.companion-detail-summary strong {
+.companion-detail-summary > div:first-child > strong {
display: block;
margin-top: 5px;
font-size: 18px;
@@ -290,33 +966,170 @@
line-height: 1.5;
}
+.companion-placement-controls {
+ display: grid;
+ width: 100%;
+ grid-template-areas:
+ "heading heading"
+ "modes size";
+ grid-template-columns: minmax(150px, 1.1fr) minmax(138px, 0.9fr);
+ gap: 6px 8px;
+ padding-top: 6px;
+ border-top: 1px solid var(--border-subtle);
+}
+
+.companion-placement-controls__heading,
+.companion-placement-controls__row {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ gap: 14px;
+}
+
+.companion-placement-controls__heading {
+ grid-area: heading;
+}
+
+.companion-placement-controls__heading strong {
+ margin: 0;
+ color: var(--text-secondary);
+ font-size: var(--type-caption);
+}
+
+.companion-placement-controls__heading small {
+ color: var(--text-tertiary);
+ font-size: var(--type-caption);
+ font-weight: 650;
+}
+
+.companion-size-control {
+ min-width: 0;
+ width: 100%;
+ margin: 0;
+}
+
+.companion-placement-modes {
+ grid-area: modes;
+ display: grid;
+ width: 100%;
+ grid-template-columns: repeat(2, minmax(0, 1fr));
+ gap: 5px;
+ padding: 3px;
+ border: 1px solid var(--border-subtle);
+ border-radius: var(--radius-sm);
+ background: var(--surface-subtle);
+}
+
+.companion-placement-modes button {
+ display: inline-flex;
+ min-width: 0;
+ min-height: 32px;
+ align-items: center;
+ justify-content: center;
+ gap: 6px;
+ padding: 5px 8px;
+ border: 0;
+ border-radius: calc(var(--radius-sm) - 3px);
+ color: var(--text-tertiary);
+ background: transparent;
+ cursor: pointer;
+ font: inherit;
+ font-size: var(--type-caption);
+ font-weight: 650;
+ white-space: nowrap;
+ transition:
+ color var(--motion-micro) var(--ease-standard),
+ background var(--motion-micro) var(--ease-standard),
+ box-shadow var(--motion-micro) var(--ease-standard);
+}
+
+.companion-placement-modes button:hover {
+ color: var(--text-secondary);
+ background: var(--surface-hover);
+}
+
+.companion-placement-modes button[aria-pressed="true"] {
+ color: var(--text-primary);
+ background: var(--surface-base);
+ box-shadow: var(--shadow-control);
+}
+
+.companion-placement-modes button:focus-visible {
+ outline: 2px solid var(--focus-ring);
+ outline-offset: 1px;
+}
+
+.companion-placement-modes > span {
+ grid-column: 1 / -1;
+ padding: 3px 5px 2px;
+ color: var(--text-tertiary);
+ font-size: 10px;
+ text-align: center;
+}
+
+.companion-placement-controls__row {
+ grid-area: size;
+ display: grid;
+ grid-template-columns: minmax(54px, 1fr) auto auto;
+ gap: 7px;
+}
+
+.companion-placement-controls__row output {
+ min-width: 40px;
+ color: var(--text-secondary);
+ font-size: var(--type-caption);
+ font-variant-numeric: tabular-nums;
+ text-align: right;
+}
+
+.companion-placement-reset {
+ min-height: 30px;
+ padding-inline: 8px;
+ font-size: var(--type-caption);
+ white-space: nowrap;
+}
+
.companion-detail-summary__badges {
display: flex;
flex: none;
- flex-wrap: wrap;
- justify-content: flex-end;
- gap: 6px;
+ flex-direction: row;
+ flex-wrap: nowrap;
+ justify-content: flex-start;
+ gap: 5px;
}
.companion-detail-summary__badges span {
- min-height: 24px;
- padding: 5px 8px;
+ min-height: 22px;
+ padding: 4px 7px;
border: 1px solid var(--border-subtle);
border-radius: 999px;
background: var(--surface-subtle);
}
.companion-detail-summary__side {
- display: flex;
- min-width: min(100%, 310px);
- flex: 0 0 auto;
- flex-direction: column;
- align-items: flex-end;
- gap: 10px;
+ display: grid;
+ min-width: 0;
+ width: 100%;
+ grid-template-areas:
+ "badges"
+ "placement"
+ "actions";
+ grid-template-columns: minmax(0, 1fr);
+ align-items: start;
+ gap: 7px 9px;
+}
+
+.companion-detail-summary__side .companion-placement-controls {
+ grid-area: placement;
+}
+
+.companion-detail-summary__side .companion-detail-summary__badges {
+ grid-area: badges;
}
.companion-detail-summary__actions {
display: flex;
+ grid-area: actions;
flex-wrap: wrap;
justify-content: flex-end;
gap: 6px;
@@ -329,6 +1142,28 @@
white-space: nowrap;
}
+@container companion-detail (max-width: 620px) {
+ .companion-detail-summary {
+ grid-template-columns: minmax(0, 1fr);
+ gap: 7px;
+ }
+
+ .companion-detail-summary > div:first-child {
+ display: grid;
+ grid-template-columns: auto minmax(0, 1fr);
+ align-items: baseline;
+ gap: 8px;
+ }
+
+ .companion-detail-summary > div:first-child > strong {
+ margin-top: 0;
+ }
+
+ .companion-detail-summary > div:first-child > p {
+ display: none;
+ }
+}
+
@media (max-width: 1100px) {
.companion-projects__list {
grid-template-columns: repeat(2, minmax(0, 1fr));
@@ -338,18 +1173,19 @@
grid-template-columns: minmax(330px, 0.85fr) minmax(440px, 1.15fr);
}
- .theme-library-workspace .featured-theme__copy {
- display: flex;
- flex-direction: column;
- }
-
.theme-library-workspace .theme-facts {
width: 100%;
min-width: 0;
- padding: 12px 0;
- border-top: 1px solid var(--border-subtle);
- border-bottom: 1px solid var(--border-subtle);
- border-left: 0;
+ grid-template-columns: repeat(2, minmax(0, 1fr));
+ }
+
+ .theme-effect-summary {
+ align-items: flex-start;
+ flex-direction: column;
+ }
+
+ .theme-effect-summary__list {
+ justify-content: flex-start;
}
.companions-layout {
@@ -357,7 +1193,15 @@
}
}
-@media (max-width: 980px) {
+@media (max-width: 1060px) {
+ .resource-library-toolbar {
+ gap: 8px;
+ }
+
+ .library-search-field {
+ width: min(260px, 46%);
+ }
+
.companion-projects__list {
grid-template-columns: minmax(0, 1fr);
}
@@ -370,6 +1214,52 @@
display: none;
}
+ .theme-library-workspace--detail .theme-personality {
+ grid-template-columns: repeat(2, minmax(0, 1fr));
+ }
+
+ .theme-library-workspace--detail .theme-personality strong {
+ overflow: visible;
+ text-overflow: clip;
+ white-space: normal;
+ }
+
+ .theme-library-workspace--detail .theme-detail-card__title-row h2 {
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+ }
+
+ .theme-library-workspace--detail .theme-detail-card__identity > p {
+ display: block;
+ height: 20px;
+ overflow: hidden;
+ line-height: 20px;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+ -webkit-line-clamp: unset;
+ }
+
+ .theme-library-workspace--detail .theme-effect-summary {
+ min-height: 0;
+ gap: 4px;
+ margin-top: 3px;
+ padding: 4px 5px;
+ }
+
+ .theme-library-workspace--detail .theme-effect-summary__copy {
+ display: none;
+ }
+
+ .theme-library-workspace--detail .theme-effect-summary__list {
+ gap: 4px;
+ }
+
+ .theme-library-workspace--detail .theme-effect-summary__list > button {
+ min-height: 21px;
+ padding: 2px 6px;
+ }
+
.theme-library-master .theme-list {
max-height: none;
}
@@ -404,7 +1294,26 @@
}
}
+@media (max-width: 700px) {
+ .resource-library-toolbar {
+ align-items: stretch;
+ flex-direction: column;
+ }
+
+ .library-search-field {
+ width: 100%;
+ }
+}
+
@media (max-height: 760px) {
+ .page--themes,
+ .companions-page {
+ --resource-library-min-height: 300px;
+
+ padding-top: 24px;
+ padding-bottom: 20px;
+ }
+
.section-heading--compact {
min-height: 60px;
padding-block: 10px;
@@ -420,45 +1329,73 @@
}
.theme-library-workspace .featured-theme {
- grid-template-rows: minmax(220px, 1fr) auto;
+ grid-template-rows: minmax(216px, 1.65fr) auto;
}
.theme-library-workspace .featured-theme__preview {
- --library-preview-scale: 0.56;
- min-height: 220px;
+ min-height: 216px;
}
.theme-library-workspace:not(.theme-library-workspace--detail)
.featured-theme__copy {
- max-height: 140px;
- gap: 4px 16px;
- overflow: hidden;
- padding: 12px 16px;
+ max-height: 166px;
+ gap: 3px 14px;
+ overflow-x: hidden;
+ overflow-y: hidden;
+ padding: 8px 14px 6px;
}
.theme-library-workspace:not(.theme-library-workspace--detail)
- .featured-theme__copy
+ .theme-detail-card__title-row
h2 {
margin-block: 0;
- font-size: 22px;
+ font-size: 20px;
}
.theme-library-workspace:not(.theme-library-workspace--detail)
- .featured-theme__copy
+ .theme-detail-card__identity
> p {
- display: -webkit-box;
+ display: block;
+ height: 20px;
overflow: hidden;
- -webkit-box-orient: vertical;
- -webkit-line-clamp: 1;
+ line-height: 20px;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+ -webkit-line-clamp: unset;
}
.theme-library-workspace:not(.theme-library-workspace--detail) .theme-facts {
- min-width: 150px;
- padding-left: 16px;
+ min-width: 190px;
+ padding-left: 14px;
+ }
+
+ .theme-library-workspace:not(.theme-library-workspace--detail)
+ .theme-effect-summary {
+ min-height: 29px;
+ grid-template-columns: minmax(0, 1fr);
+ margin-top: 1px;
+ padding: 3px 5px;
+ }
+
+ .theme-library-workspace:not(.theme-library-workspace--detail)
+ .theme-effect-summary__copy {
+ display: none;
+ }
+
+ .theme-library-workspace:not(.theme-library-workspace--detail)
+ .theme-effect-summary__list {
+ flex-wrap: nowrap;
+ }
+
+ .theme-library-workspace:not(.theme-library-workspace--detail)
+ .theme-effect-summary__list
+ > button {
+ min-height: 21px;
+ padding: 2px 6px;
}
.companion-detail-summary {
min-height: 96px;
- padding-block: 13px;
+ padding-block: 9px 10px;
}
}
diff --git a/apps/desktop/src/styles/primitives.css b/apps/desktop/src/styles/primitives.css
index 61f3bfb..e60c431 100644
--- a/apps/desktop/src/styles/primitives.css
+++ b/apps/desktop/src/styles/primitives.css
@@ -166,6 +166,13 @@ input.visually-hidden[type="file"][aria-hidden="true"] {
grid-template-columns: 0 minmax(0, 1fr);
}
+.app[data-focus-workspace="true"] .app-main {
+ /* The hidden top bar must not leave its auto track behind. Without this,
+ * the editor height is derived from its contents and the remaining viewport
+ * becomes an unexplained empty strip below the workbench. */
+ grid-template-rows: minmax(0, 1fr);
+}
+
.app[data-focus-workspace="true"] .app-sidebar {
width: var(--sidebar);
pointer-events: none;
@@ -179,5 +186,6 @@ input.visually-hidden[type="file"][aria-hidden="true"] {
.app[data-focus-workspace="true"] .editor-page,
.app[data-focus-workspace="true"] .companion-creator {
- min-height: 100vh;
+ height: 100%;
+ min-height: 0;
}
diff --git a/apps/desktop/src/styles/scroll-surfaces.css b/apps/desktop/src/styles/scroll-surfaces.css
new file mode 100644
index 0000000..f5c2ce7
--- /dev/null
+++ b/apps/desktop/src/styles/scroll-surfaces.css
@@ -0,0 +1,273 @@
+/*
+ * Scroll surfaces are application layout, not browser decoration.
+ * This file is deliberately loaded last so WKWebView and WebView2 receive the
+ * same final chrome even when a feature stylesheet changes overflow behavior.
+ */
+
+html,
+body,
+#root,
+* {
+ scrollbar-color: var(--scrollbar-thumb) transparent;
+ scrollbar-width: thin;
+}
+
+*::-webkit-scrollbar {
+ width: var(--scrollbar-size);
+ height: var(--scrollbar-size);
+ background-color: transparent;
+}
+
+*::-webkit-scrollbar-track,
+*::-webkit-scrollbar-track-piece,
+*::-webkit-scrollbar-corner {
+ border: 0;
+ background-color: transparent;
+ box-shadow: none;
+}
+
+*::-webkit-scrollbar-thumb {
+ min-width: 28px;
+ min-height: 28px;
+ border: 2px solid transparent;
+ border-radius: 999px;
+ background-color: var(--scrollbar-thumb);
+ background-clip: padding-box;
+}
+
+*::-webkit-scrollbar-thumb:hover {
+ background-color: var(--scrollbar-thumb-hover);
+ background-clip: padding-box;
+}
+
+*::-webkit-scrollbar-thumb:active {
+ background-color: var(--scrollbar-thumb-active);
+ background-clip: padding-box;
+}
+
+*::-webkit-scrollbar-button,
+*::-webkit-scrollbar-button:single-button {
+ display: none;
+ width: 0;
+ height: 0;
+}
+
+:is(
+ [data-scroll-surface],
+ .app-main__viewport,
+ .theme-list,
+ .companion-list,
+ .layer-stack,
+ .inspector,
+ .creator-workbench,
+ .creator-frame-strip__items,
+ .new-theme-existing-list,
+ .new-theme-options,
+ .onboarding,
+ .onboarding-content,
+ .update-dialog__notes,
+ .diagnostics-dialog,
+ .diagnostics-dialog pre,
+ .atlas-grid-preview,
+ .canvas-stage[data-preview-zoom="actual"],
+ .featured-theme__copy,
+ .timeline-thumbs,
+ .creator-steps
+) {
+ min-width: 0;
+ min-height: 0;
+ overscroll-behavior: contain;
+ scrollbar-gutter: auto;
+}
+
+:is(
+ [data-scroll-surface="horizontal"],
+ .creator-steps,
+ .timeline-thumbs,
+ .creator-frame-strip__items
+)::-webkit-scrollbar {
+ width: var(--scrollbar-size-compact);
+ height: var(--scrollbar-size-compact);
+}
+
+:is(
+ [data-scroll-surface="horizontal"],
+ .creator-steps,
+ .timeline-thumbs,
+ .creator-frame-strip__items
+)::-webkit-scrollbar-thumb {
+ border-width: 1px;
+}
+
+/* Full workspaces own their scrolling. The shell viewport must not create a
+ * second rail beside editor panels or the Companion Studio workbench. */
+.app-main__viewport:is([data-view="editor"], [data-view="companion-editor"]) {
+ overflow: hidden;
+}
+
+/* The theme editor is a fixed professional workspace. Long inspector forms
+ * scroll inside their own column; they must never determine the canvas height
+ * or stretch the 1280×760 Codex preview into a tall, misleading viewport. */
+.app-main__viewport[data-view="editor"] .editor-page {
+ width: 100%;
+ height: 100%;
+ min-height: 0;
+ overflow: hidden;
+}
+
+.app-main__viewport[data-view="editor"] .editor-header {
+ flex: 0 0 auto;
+}
+
+.app-main__viewport[data-view="editor"]
+ :is(.editor-layout, .layers-panel, .editor-canvas, .canvas-stage) {
+ min-height: 0;
+}
+
+.app-main__viewport[data-view="editor"] .editor-layout,
+.app-main__viewport[data-view="editor"] .editor-canvas,
+.app-main__viewport[data-view="editor"] .canvas-stage {
+ overflow: hidden;
+}
+
+.app-main__viewport[data-view="editor"] .layers-panel {
+ overflow: hidden;
+}
+
+.app-main__viewport[data-view="editor"] .layer-stack {
+ min-height: 0;
+ flex: 1 1 auto;
+ overflow-x: hidden;
+ overflow-y: auto;
+}
+
+.app-main__viewport[data-view="editor"]
+ .canvas-stage:not([data-preview-zoom="actual"])
+ .workspace-preview {
+ width: min(100%, 920px);
+ height: auto;
+ min-height: 0;
+ aspect-ratio: 1.62;
+}
+
+.app-main__viewport[data-view="companion-editor"] .companion-creator {
+ display: flex;
+ width: 100%;
+ height: 100%;
+ min-height: 0;
+ flex-direction: column;
+ margin: 0;
+ overflow: hidden;
+}
+
+.companion-creator :where(.creator-header, .creator-steps, .creator-footer) {
+ flex: 0 0 auto;
+}
+
+.companion-creator .creator-steps {
+ margin: 0;
+ overflow-x: auto;
+ overflow-y: hidden;
+}
+
+.companion-creator .creator-error,
+.companion-creator .creator-progress {
+ flex: 0 0 auto;
+ margin: 10px 18px 0;
+}
+
+.companion-creator .creator-workbench {
+ flex: 1 1 auto;
+ margin: 0;
+ padding: 16px 18px;
+ overflow-x: hidden;
+ overflow-y: auto;
+ border: 0;
+ border-radius: 0;
+ background: var(--surface-canvas);
+ box-shadow: none;
+}
+
+.companion-creator .creator-stage {
+ flex: 0 0 auto;
+ border: 1px solid var(--border-subtle);
+ border-radius: var(--radius-lg) var(--radius-lg) 0 0;
+ background: var(--surface-base);
+}
+
+.companion-creator .creator-stage:only-child {
+ border-radius: var(--radius-lg);
+}
+
+/* The workbench is the vertical owner. Inspector rails inside it made the
+ * same stage require two different wheel targets and clipped right controls. */
+.companion-creator .creator-inspector-panel {
+ max-height: none;
+ overflow: visible;
+}
+
+.companion-creator .creator-frame-strip {
+ border: 1px solid var(--border-subtle);
+ border-top: 0;
+ border-radius: 0 0 var(--radius-lg) var(--radius-lg);
+}
+
+.companion-creator .creator-frame-strip__items {
+ padding: 3px 3px 9px;
+}
+
+.companion-creator .timeline-thumbs {
+ padding: 5px 2px 9px;
+}
+
+/* A Codex preview is a self-contained theme surface. Its scroll chrome must
+ * follow the preview accent instead of inheriting the manager appearance. */
+.workspace-preview,
+.workspace-preview * {
+ scrollbar-color: color-mix(
+ in srgb,
+ var(--preview-accent) 42%,
+ var(--preview-border)
+ )
+ transparent;
+}
+
+.workspace-preview *::-webkit-scrollbar {
+ width: 7px;
+ height: 7px;
+}
+
+.workspace-preview *::-webkit-scrollbar-thumb {
+ min-width: 20px;
+ min-height: 20px;
+ border-width: 2px;
+ background-color: color-mix(
+ in srgb,
+ var(--preview-accent) 42%,
+ var(--preview-border)
+ );
+}
+
+.workspace-preview[data-decorations="expressive"] *::-webkit-scrollbar-thumb {
+ background-color: color-mix(
+ in srgb,
+ var(--preview-accent) 62%,
+ var(--preview-border)
+ );
+}
+
+.companion-creator .creator-footer {
+ position: relative;
+ z-index: 18;
+ bottom: auto;
+}
+
+@media (max-width: 1100px) {
+ .companion-creator .creator-steps {
+ padding-inline: 12px;
+ }
+
+ .companion-creator .creator-step {
+ min-width: 90px;
+ }
+}
diff --git a/apps/desktop/src/styles/settings.css b/apps/desktop/src/styles/settings.css
index 3625815..8ff3f8d 100644
--- a/apps/desktop/src/styles/settings.css
+++ b/apps/desktop/src/styles/settings.css
@@ -39,7 +39,7 @@
@media (max-width: 1120px) {
.settings-layout .settings-group--split {
- grid-template-columns: minmax(170px, 1fr) minmax(160px, 220px);
+ grid-template-columns: minmax(170px, 1fr) minmax(220px, 280px);
}
}
diff --git a/apps/desktop/src/styles/tokens.css b/apps/desktop/src/styles/tokens.css
index dcbbdd8..a30fc0b 100644
--- a/apps/desktop/src/styles/tokens.css
+++ b/apps/desktop/src/styles/tokens.css
@@ -46,10 +46,10 @@
--shadow-flyout: 0 18px 48px rgba(8, 10, 14, 0.22);
/* Scroll surfaces are part of the application chrome, not browser chrome. */
- --scrollbar-size: 10px;
- --scrollbar-size-compact: 8px;
- --scrollbar-thumb: color-mix(in srgb, var(--ink) 18%, transparent);
- --scrollbar-thumb-hover: color-mix(in srgb, var(--ink) 32%, transparent);
+ --scrollbar-size: 8px;
+ --scrollbar-size-compact: 6px;
+ --scrollbar-thumb: color-mix(in srgb, var(--ink) 22%, transparent);
+ --scrollbar-thumb-hover: color-mix(in srgb, var(--ink) 38%, transparent);
--scrollbar-thumb-active: color-mix(
in srgb,
var(--accent) 54%,
@@ -71,3 +71,23 @@
--motion-standard: 0ms;
--motion-panel: 0ms;
}
+
+/*
+ * Comfortable desktop density.
+ *
+ * The 960 px workspace is deliberately compact, but carrying the same scale
+ * into a full desktop window makes the manager read like a reduced mock-up.
+ * Raise only the semantic type tokens at wide widths so resource libraries,
+ * settings and both editors gain hierarchy without changing preview geometry
+ * or destabilising the compact layout.
+ */
+@media (min-width: 1180px) {
+ :root {
+ --type-caption: 11.5px;
+ --type-meta: 13px;
+ --type-body: 14.5px;
+ --type-label: 16px;
+ --type-section: 19px;
+ --type-page: 34px;
+ }
+}
diff --git a/apps/desktop/src/styles/typography.css b/apps/desktop/src/styles/typography.css
index bce9fb0..b778b9b 100644
--- a/apps/desktop/src/styles/typography.css
+++ b/apps/desktop/src/styles/typography.css
@@ -131,7 +131,7 @@
font-size: var(--type-caption);
}
-.companion-detail-summary strong {
+.companion-detail-summary > div:first-child > strong {
font-size: var(--type-section);
}
@@ -222,6 +222,31 @@
font-size: var(--type-body);
}
+@media (min-width: 1180px) {
+ .app-brand strong {
+ font-size: 17px;
+ letter-spacing: -0.25px;
+ }
+
+ .theme-library-workspace .featured-theme__copy h2 {
+ font-size: 26px;
+ line-height: 1.08;
+ }
+
+ .companion-detail-summary > div:first-child > strong {
+ font-size: 20px;
+ }
+
+ .editor-header h1,
+ .creator-header h1 {
+ font-size: 23px;
+ }
+
+ .home-actions small {
+ font-size: 12.5px;
+ }
+}
+
.creator-segmented button,
.creator-inspector-actions .button,
.video-range-editor__actions button,
diff --git a/apps/desktop/src/styles/workspaces.css b/apps/desktop/src/styles/workspaces.css
index 6c00dbb..9b58ea8 100644
--- a/apps/desktop/src/styles/workspaces.css
+++ b/apps/desktop/src/styles/workspaces.css
@@ -39,10 +39,20 @@
}
.home-current__facts {
+ grid-template-columns: minmax(88px, 0.72fr) minmax(0, 1.28fr);
margin: 15px 0;
padding: 12px 0;
}
+.home-current__facts dd {
+ display: -webkit-box;
+ overflow: hidden;
+ line-height: 1.35;
+ white-space: normal;
+ -webkit-box-orient: vertical;
+ -webkit-line-clamp: 2;
+}
+
.home-actions > button {
min-height: clamp(94px, 10vh, 112px);
padding: 14px 16px;
@@ -97,9 +107,318 @@
.workspace-main {
position: relative;
+ min-height: 0;
+ overflow: hidden;
+}
+
+/* Surface material is derived from the existing opacity and blur controls.
+ * It changes paint, light diffusion and elevation only; component geometry,
+ * hit targets and Codex layout remain untouched. */
+.workspace-preview {
+ --preview-material-raised: color-mix(
+ in srgb,
+ var(--preview-surface-raised) 92%,
+ transparent
+ );
+ --preview-material-overlay: color-mix(
+ in srgb,
+ var(--preview-surface-overlay) 96%,
+ transparent
+ );
+ --preview-material-sunken: color-mix(
+ in srgb,
+ var(--preview-surface-sunken) 92%,
+ transparent
+ );
+ --preview-material-border: var(--preview-border);
+ --preview-material-filter: blur(var(--preview-focus-blur));
+ --preview-material-raised-shadow: 0 10px 26px rgba(0, 0, 0, 0.08);
+ --preview-material-overlay-shadow: 0 18px 42px rgba(0, 0, 0, 0.16);
+}
+
+.workspace-preview[data-material="solid"] {
+ --preview-material-raised: var(--preview-surface-raised);
+ --preview-material-overlay: var(--preview-surface-overlay);
+ --preview-material-sunken: var(--preview-surface-sunken);
+ --preview-material-border: var(--preview-border-strong);
+ --preview-material-filter: none;
+ --preview-material-raised-shadow: 0 5px 14px rgba(0, 0, 0, 0.07);
+ --preview-material-overlay-shadow: 0 12px 28px rgba(0, 0, 0, 0.14);
+}
+
+.workspace-preview[data-material="frosted"] {
+ --preview-material-raised: linear-gradient(
+ 145deg,
+ color-mix(in srgb, var(--preview-surface-raised) 86%, transparent),
+ color-mix(in srgb, var(--preview-surface-overlay) 70%, transparent)
+ );
+ --preview-material-overlay: linear-gradient(
+ 145deg,
+ color-mix(in srgb, var(--preview-surface-overlay) 88%, transparent),
+ color-mix(in srgb, var(--preview-surface-raised) 68%, transparent)
+ );
+ --preview-material-sunken: color-mix(
+ in srgb,
+ var(--preview-surface-sunken) 82%,
+ transparent
+ );
+ --preview-material-border: color-mix(
+ in srgb,
+ var(--preview-accent) 22%,
+ var(--preview-border)
+ );
+ --preview-material-filter: saturate(1.14) blur(var(--preview-focus-blur));
+ --preview-material-raised-shadow:
+ inset 0 1px color-mix(in srgb, var(--preview-text) 7%, transparent),
+ 0 14px 34px rgba(0, 0, 0, 0.12);
+ --preview-material-overlay-shadow:
+ inset 0 1px color-mix(in srgb, var(--preview-text) 9%, transparent),
+ 0 22px 52px rgba(0, 0, 0, 0.2);
+}
+
+.workspace-preview__chrome {
+ min-width: 0;
+ min-height: 0;
+ overflow: hidden;
+}
+
+.workspace-conversation {
+ min-height: 0;
+ overflow: hidden;
+}
+
+.workspace-context-tabs :is(span, button) {
+ position: relative;
+ display: grid;
+ height: 28px;
+ place-items: center;
+ padding-inline: 9px;
+ border: 0;
+ border-radius: 0;
+ color: var(--preview-tertiary);
+ background: transparent;
+ font: inherit;
+ font-size: 6.5px;
+}
+
+.workspace-context-tabs button {
+ cursor: pointer;
+}
+
+.workspace-context-tabs :is(span, button).is-active {
+ color: var(--preview-text);
+ background: color-mix(in srgb, var(--preview-accent) 7%, transparent);
+}
+
+.workspace-context-tabs :is(span, button).is-active::after {
+ position: absolute;
+ right: 7px;
+ bottom: 0;
+ left: 7px;
+ height: 2px;
+ border-radius: 999px 999px 0 0;
+ background: var(--preview-accent);
+ content: "";
+}
+
+.workspace-context-tabs button:focus-visible {
+ z-index: 1;
+ outline: 1px solid var(--preview-focus);
+ outline-offset: -2px;
+}
+
+.workspace-preview
+ :is(
+ .workspace-context-tabs :is(span, button),
+ .workspace-component-tabs span,
+ .workspace-right-panel__tabs span,
+ .workspace-component-card,
+ .workspace-task-plan,
+ .workspace-task-tool,
+ .workspace-task-changes
+ ) {
+ transition:
+ color var(--preview-motion-duration) ease,
+ background-color var(--preview-motion-duration) ease,
+ border-color var(--preview-motion-duration) ease,
+ box-shadow var(--preview-motion-duration) ease,
+ transform var(--preview-motion-duration) ease;
+}
+
+/* Motion character is a visual-system property, not a layout recipe. Keep
+ * movement on paint-only properties so the preview and Codex runtime retain
+ * identical geometry at every intensity. */
+.workspace-preview[role="group"]
+ :is(
+ .workspace-task,
+ .workspace-context-tabs button,
+ .workspace-component-actions button,
+ .workspace-composer button,
+ .workspace-dialog-actions button
+ ) {
+ transition:
+ color var(--preview-motion-duration) ease,
+ background-color var(--preview-motion-duration) ease,
+ border-color var(--preview-motion-duration) ease,
+ box-shadow var(--preview-motion-duration) ease,
+ transform var(--preview-motion-duration) ease;
+ transform: translateY(0) scale(1);
+ transform-origin: center;
+}
+
+@media (hover: hover) and (pointer: fine) {
+ .workspace-preview[role="group"]:not([data-motion-character="still"])
+ :is(
+ .workspace-task,
+ .workspace-context-tabs button,
+ .workspace-component-actions button,
+ .workspace-composer button,
+ .workspace-dialog-actions button
+ ):not(:disabled):hover {
+ transform: translateY(calc(-1 * var(--preview-motion-lift))) scale(1);
+ }
+}
+
+.workspace-preview[role="group"]:not([data-motion-character="still"])
+ :is(
+ .workspace-task,
+ .workspace-context-tabs button,
+ .workspace-component-actions button,
+ .workspace-composer button,
+ .workspace-dialog-actions button
+ ):not(:disabled):active {
+ transform: translateY(0) scale(var(--preview-motion-press-scale));
+}
+
+.workspace-preview[data-motion-character="fluid"]
+ :is(.workspace-dialog-layer > div, .workspace-right-panel),
+.workspace-preview[data-motion-character="expressive"]
+ :is(.workspace-dialog-layer > div, .workspace-right-panel) {
+ animation: preview-surface-enter var(--preview-motion-duration)
+ cubic-bezier(0.2, 0.72, 0.2, 1) both;
+}
+
+@keyframes preview-surface-enter {
+ from {
+ opacity: var(--preview-motion-overlay-opacity);
+ transform: translateY(var(--preview-motion-overlay-offset))
+ scale(var(--preview-motion-overlay-scale));
+ }
+
+ to {
+ opacity: 1;
+ transform: translateY(0) scale(1);
+ }
+}
+
+.workspace-preview[data-geometry="precise"]
+ .workspace-context-tabs
+ :is(span, button).is-active::after {
+ right: 0;
+ left: 0;
+ border-radius: 0;
+}
+
+.workspace-preview[data-geometry="precise"]
+ :is(
+ .workspace-component-card,
+ .workspace-task-plan,
+ .workspace-task-tool,
+ .workspace-task-changes,
+ .workspace-right-panel
+ ) {
+ border-left-color: color-mix(
+ in srgb,
+ var(--preview-accent) 52%,
+ var(--preview-border)
+ );
+ box-shadow:
+ inset 2px 0 color-mix(in srgb, var(--preview-accent) 34%, transparent),
+ 0 10px 24px rgba(0, 0, 0, 0.1);
+}
+
+.workspace-preview[data-geometry="soft"] .workspace-context-tabs {
+ gap: 2px;
+ padding: 3px;
+ border-bottom-color: transparent;
+ border-radius: 999px;
+ background: color-mix(
+ in srgb,
+ var(--preview-surface-sunken) 88%,
+ transparent
+ );
+}
+
+.workspace-preview :is(input, textarea, [contenteditable="true"]),
+.workspace-component-caret {
+ caret-color: var(--preview-accent);
+}
+
+.workspace-preview ::selection,
+.workspace-component-selection {
+ color: var(--preview-text);
+ background: color-mix(in srgb, var(--preview-accent) 28%, transparent);
+ text-shadow: none;
+}
+
+.workspace-preview :is(a, [role="link"]),
+.workspace-component-link {
+ color: color-mix(in srgb, var(--preview-info) 78%, var(--preview-text));
+ text-decoration-color: color-mix(
+ in srgb,
+ var(--preview-accent) 58%,
+ transparent
+ );
+ text-underline-offset: 2px;
+}
+
+.workspace-preview[data-geometry="soft"]
+ .workspace-context-tabs
+ :is(span, button) {
+ height: 22px;
+ border-radius: 999px;
+}
+
+.workspace-preview[data-geometry="soft"]
+ .workspace-context-tabs
+ :is(span, button).is-active {
+ box-shadow: 0 5px 14px
+ color-mix(in srgb, var(--preview-accent) 16%, transparent);
+}
+
+.workspace-preview[data-geometry="soft"]
+ .workspace-context-tabs
+ :is(span, button).is-active::after {
+ display: none;
+}
+
+.workspace-preview[data-geometry="soft"]
+ :is(.workspace-component-tabs, .workspace-right-panel__tabs) {
+ border-radius: 999px;
+}
+
+.workspace-preview[data-geometry="soft"]
+ :is(.workspace-component-tabs span, .workspace-right-panel__tabs span) {
+ border-radius: 999px;
+}
+
+.workspace-preview[data-geometry="soft"]
+ :is(
+ .workspace-component-card,
+ .workspace-task-plan,
+ .workspace-task-tool,
+ .workspace-task-changes,
+ .workspace-right-panel
+ ) {
+ box-shadow:
+ inset 0 1px color-mix(in srgb, var(--preview-text) 6%, transparent),
+ 0 14px 30px rgba(0, 0, 0, 0.14),
+ 0 0 0 3px color-mix(in srgb, var(--preview-accent) 5%, transparent);
}
.workspace-preview__scene-layer {
+ --preview-parallax-scale: 1.015;
+
position: absolute;
z-index: 1;
inset: -12px;
@@ -109,6 +428,61 @@
transform: scale(1.015);
}
+.workspace-motion-cursor {
+ position: absolute;
+ z-index: 8;
+ top: 50%;
+ left: 50%;
+ display: grid;
+ width: 23px;
+ height: 23px;
+ place-items: center;
+ border: 1px solid color-mix(in srgb, var(--preview-accent) 55%, white 12%);
+ border-radius: 50%;
+ color: var(--preview-on-accent);
+ background: color-mix(in srgb, var(--preview-accent) 84%, transparent);
+ opacity: 0;
+ pointer-events: none;
+ transform: translate(-50%, -50%);
+ transition: opacity 120ms ease;
+ box-shadow: 0 5px 18px
+ color-mix(in srgb, var(--preview-accent) 28%, transparent);
+}
+
+.workspace-preview[data-motion-preview="playing"] .workspace-motion-cursor {
+ opacity: 0.92;
+}
+
+@media (prefers-reduced-motion: reduce) {
+ .workspace-preview {
+ --preview-motion-duration: 0ms !important;
+ --preview-motion-lift: 0px !important;
+ --preview-motion-press-scale: 1 !important;
+ --preview-motion-overlay-opacity: 1 !important;
+ --preview-motion-overlay-offset: 0px !important;
+ --preview-motion-overlay-scale: 1 !important;
+ }
+
+ .workspace-preview
+ :is(
+ .workspace-task,
+ .workspace-context-tabs button,
+ .workspace-component-actions button,
+ .workspace-composer button,
+ .workspace-dialog-actions button,
+ .workspace-dialog-layer > div,
+ .workspace-right-panel
+ ) {
+ animation: none !important;
+ transition: none !important;
+ transform: none !important;
+ }
+
+ .workspace-motion-cursor {
+ display: none;
+ }
+}
+
.workspace-preview__scene-layer--gradient {
background:
linear-gradient(
@@ -163,155 +537,1337 @@
border: 1px solid var(--preview-border);
border-radius: calc(var(--preview-radius) * 0.7);
color: var(--preview-muted);
- background: color-mix(in srgb, var(--preview-surface) 82%, transparent);
+ background: color-mix(in srgb, var(--preview-control) 84%, transparent);
font-size: 8px;
text-align: left;
}
-.workspace-settings-state {
+.workspace-task-activity {
display: grid;
- width: min(500px, 90%);
- min-height: 220px;
+ width: min(500px, 92%);
margin: auto;
- grid-template-columns: 120px minmax(0, 1fr);
- overflow: hidden;
- border: 1px solid var(--preview-border);
- border-radius: var(--preview-radius);
- background: color-mix(in srgb, var(--preview-surface) 90%, transparent);
+ gap: 8px;
+ color: var(--preview-text);
}
-.workspace-settings-state nav {
- display: flex;
- flex-direction: column;
- gap: 4px;
- padding: 12px 9px;
- border-right: 1px solid var(--preview-border);
+.workspace-task-prompt {
+ width: min(78%, 390px);
+ justify-self: end;
+ padding: 9px 11px;
+ border: 1px solid
+ color-mix(in srgb, var(--preview-accent) 34%, var(--preview-border));
+ border-inline-end: 3px solid var(--preview-accent);
+ border-radius: calc(var(--preview-radius) * 0.74);
+ background: color-mix(
+ in srgb,
+ var(--preview-control-active) 84%,
+ transparent
+ );
+ box-shadow: 0 8px 22px rgba(0, 0, 0, 0.09);
}
-.workspace-settings-state nav span {
+.workspace-task-prompt > span {
+ display: block;
+ margin-bottom: 3px;
+ color: color-mix(in srgb, var(--preview-accent) 72%, var(--preview-text));
+ font-size: 5px;
+ font-weight: 750;
+ letter-spacing: 0.09em;
+}
+
+.workspace-task-prompt p {
+ margin: 0;
+ font-size: 6.5px;
+ line-height: 1.5;
+}
+
+.workspace-task-plan,
+.workspace-task-tool,
+.workspace-task-changes {
+ border: 1px solid var(--preview-material-border);
+ border-radius: calc(var(--preview-radius) * 0.7);
+ background: var(--preview-material-raised);
+ box-shadow: var(--preview-material-raised-shadow);
+ backdrop-filter: var(--preview-material-filter);
+}
+
+.workspace-task-plan {
+ padding: 10px 11px 9px;
+}
+
+.workspace-task-plan > header,
+.workspace-task-plan > header > span,
+.workspace-task-tool > summary,
+.workspace-task-tool > summary > span,
+.workspace-task-tool__body > span,
+.workspace-task-changes,
+.workspace-task-changes > span:not(.workspace-task-changes__icon) {
display: flex;
- min-height: 28px;
align-items: center;
- gap: 7px;
- padding: 0 8px;
- border-radius: 6px;
- color: var(--preview-muted);
- font-size: 7px;
}
-.workspace-settings-state nav span.is-active {
- color: var(--preview-text);
- background: color-mix(in srgb, var(--preview-accent) 15%, transparent);
+.workspace-task-plan > header,
+.workspace-task-tool > summary {
+ justify-content: space-between;
+ gap: 10px;
}
-.workspace-settings-state > div {
- display: flex;
- flex-direction: column;
- gap: 11px;
- padding: 20px;
+.workspace-task-plan > header > span,
+.workspace-task-tool > summary > span,
+.workspace-task-tool__body > span {
+ gap: 5px;
}
-.workspace-settings-state > div strong {
- margin-bottom: 6px;
- font-size: 11px;
+.workspace-task-plan strong,
+.workspace-task-tool strong,
+.workspace-task-changes strong {
+ font-size: 6.6px;
+ font-weight: 670;
+}
+
+.workspace-task-plan header svg,
+.workspace-task-tool summary svg,
+.workspace-task-changes__icon {
+ color: var(--preview-info);
+}
+
+.workspace-task-plan header small,
+.workspace-task-tool summary small {
+ color: var(--preview-muted);
+ font-size: 5.5px;
}
-.workspace-settings-state > div span {
+.workspace-task-plan progress {
display: block;
- height: 34px;
- border: 1px solid var(--preview-border);
- border-radius: 7px;
- background: color-mix(in srgb, var(--preview-surface) 82%, transparent);
+ width: 100%;
+ height: 3px;
+ margin: 8px 0;
+ overflow: hidden;
+ border: 0;
+ border-radius: 999px;
+ accent-color: var(--preview-accent);
+ background: color-mix(in srgb, var(--preview-border) 58%, transparent);
}
-.workspace-right-panel {
- position: absolute;
- z-index: 5;
- top: 10px;
- right: 10px;
- bottom: 10px;
- width: min(190px, 34%);
- padding: 13px;
- border: 1px solid var(--preview-border);
- border-radius: calc(var(--preview-radius) * 0.85);
- background: color-mix(in srgb, var(--preview-surface) 94%, transparent);
- box-shadow: 0 14px 30px rgba(0, 0, 0, 0.18);
- backdrop-filter: blur(var(--preview-focus-blur));
+.workspace-task-plan progress::-webkit-progress-bar {
+ border-radius: inherit;
+ background: color-mix(in srgb, var(--preview-border) 58%, transparent);
+}
+
+.workspace-task-plan progress::-webkit-progress-value {
+ border-radius: inherit;
+ background: var(--preview-accent);
+}
+
+.workspace-task-plan__steps {
+ display: grid;
+ grid-template-columns: repeat(3, minmax(0, 1fr));
+ gap: 5px;
}
-.workspace-right-panel > div {
+.workspace-task-plan__steps span {
display: flex;
+ min-width: 0;
align-items: center;
- gap: 7px;
- margin-bottom: 16px;
+ gap: 4px;
+ padding: 5px 6px;
+ border: 1px solid color-mix(in srgb, var(--preview-border) 64%, transparent);
+ border-radius: 5px;
+ color: var(--preview-muted);
+ background: color-mix(
+ in srgb,
+ var(--preview-surface-sunken) 70%,
+ transparent
+ );
+ font-size: 5.3px;
+ line-height: 1.25;
}
-.workspace-right-panel strong {
- font-size: 9px;
+.workspace-task-plan__steps span.is-complete {
+ color: color-mix(in srgb, var(--preview-success) 72%, var(--preview-text));
}
-.workspace-right-panel > span {
- display: block;
- height: 35px;
- margin-top: 7px;
- border: 1px solid var(--preview-border);
- border-radius: 7px;
- background: color-mix(in srgb, var(--preview-surface) 78%, transparent);
+.workspace-task-plan__steps span.is-active {
+ border-color: color-mix(
+ in srgb,
+ var(--preview-accent) 40%,
+ var(--preview-border)
+ );
+ color: var(--preview-text);
+ background: color-mix(in srgb, var(--preview-accent) 9%, transparent);
}
-.workspace-dialog-layer {
- position: absolute;
- z-index: 8;
- inset: 0;
+.workspace-task-tool {
+ overflow: hidden;
+}
+
+.workspace-task-tool > summary {
+ min-height: 31px;
+ padding: 0 10px;
+ list-style: none;
+ cursor: default;
+}
+
+.workspace-task-tool > summary::-webkit-details-marker {
+ display: none;
+}
+
+.workspace-task-tool[open] > summary {
+ border-bottom: 1px solid
+ color-mix(in srgb, var(--preview-border) 62%, transparent);
+}
+
+.workspace-task-tool__body {
display: grid;
- place-items: center;
- background: rgba(8, 10, 13, 0.48);
- backdrop-filter: blur(2px);
+ grid-template-columns: minmax(0, 1fr) auto;
+ align-items: center;
+ gap: 10px;
+ padding: 8px 10px;
+ background: var(--preview-material-sunken);
}
-.workspace-dialog-layer > div {
- width: min(300px, 62%);
- padding: 18px;
- border: 1px solid var(--preview-border);
- border-radius: var(--preview-radius);
+.workspace-task-tool__body code {
+ min-width: 0;
+ overflow: hidden;
color: var(--preview-text);
- background: color-mix(in srgb, var(--preview-surface) 96%, transparent);
- box-shadow: 0 18px 42px rgba(0, 0, 0, 0.26);
+ font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
+ font-size: 5.4px;
+ text-overflow: ellipsis;
+ white-space: nowrap;
}
-.workspace-dialog-layer strong,
-.workspace-dialog-layer p {
- display: block;
+.workspace-task-tool__body > span {
+ color: var(--preview-success);
+ font-size: 5.4px;
+ white-space: nowrap;
}
-.workspace-dialog-layer strong {
- margin: 10px 0 5px;
- font-size: 11px;
+.workspace-task-changes {
+ position: relative;
+ min-height: 42px;
+ gap: 8px;
+ padding: 7px 10px;
+ overflow: hidden;
}
-.workspace-dialog-layer p {
- margin: 0;
+.workspace-task-changes__icon {
+ display: grid;
+ width: 23px;
+ height: 23px;
+ place-items: center;
+ flex: none;
+ border-radius: 7px;
+ background: color-mix(in srgb, var(--preview-accent) 13%, transparent);
+}
+
+.workspace-task-changes > span:not(.workspace-task-changes__icon) {
+ min-width: 0;
+ flex: 1;
+ flex-direction: column;
+ align-items: flex-start;
+ gap: 2px;
+}
+
+.workspace-task-changes small {
color: var(--preview-muted);
- font-size: 8px;
- line-height: 1.5;
+ font-size: 5.3px;
}
-.workspace-dialog-layer > div > span {
- display: flex;
- justify-content: flex-end;
+.workspace-task-changes em {
+ flex: none;
+ padding: 3px 5px;
+ border-radius: 5px;
+ font-size: 5.5px;
+ font-style: normal;
+ font-weight: 700;
+}
+
+.workspace-task-changes em[data-change="added"] {
+ color: var(--preview-added);
+ background: color-mix(in srgb, var(--preview-added) 10%, transparent);
+}
+
+.workspace-task-changes em[data-change="deleted"] {
+ color: var(--preview-deleted);
+ background: color-mix(in srgb, var(--preview-deleted) 10%, transparent);
+}
+
+.workspace-task-changes__mark {
+ position: absolute;
+ top: 5px;
+ right: 5px;
+ color: color-mix(in srgb, var(--preview-muted) 34%, transparent);
+}
+
+.workspace-preview[data-layout="editorial"] .workspace-task-plan,
+.workspace-preview[data-layout="editorial"] .workspace-task-tool,
+.workspace-preview[data-layout="editorial"] .workspace-task-changes,
+.workspace-preview[data-layout="immersive"] .workspace-task-plan,
+.workspace-preview[data-layout="immersive"] .workspace-task-tool,
+.workspace-preview[data-layout="immersive"] .workspace-task-changes {
+ border-inline-start: 2px solid
+ color-mix(in srgb, var(--preview-accent) 68%, var(--preview-border));
+}
+
+.workspace-preview[data-decorations="expressive"] .workspace-task-plan,
+.workspace-preview[data-decorations="expressive"] .workspace-task-tool,
+.workspace-preview[data-decorations="expressive"] .workspace-task-changes {
+ border-color: color-mix(
+ in srgb,
+ var(--preview-accent) 34%,
+ var(--preview-border)
+ );
+ box-shadow:
+ 0 12px 28px rgba(0, 0, 0, 0.12),
+ inset 0 1px color-mix(in srgb, var(--preview-accent) 12%, transparent);
+}
+
+/*
+ * Theme typography changes reading rhythm without replacing Codex fonts,
+ * changing font sizes, or altering component geometry. This keeps CJK and
+ * code rendering stable while letting layout treatments carry through the
+ * actual content surface.
+ */
+.workspace-preview[data-typography="balanced"] {
+ --preview-content-leading: 1.5;
+ --preview-heading-leading: 1.28;
+ --preview-heading-weight: 650;
+ --preview-heading-tracking: -0.008em;
+}
+
+.workspace-preview[data-typography="editorial"] {
+ --preview-content-leading: 1.58;
+ --preview-heading-leading: 1.2;
+ --preview-heading-weight: 690;
+ --preview-heading-tracking: -0.018em;
+}
+
+.workspace-preview[data-typography="cinematic"] {
+ --preview-content-leading: 1.64;
+ --preview-heading-leading: 1.16;
+ --preview-heading-weight: 720;
+ --preview-heading-tracking: -0.026em;
+}
+
+.workspace-preview[data-typography]
+ :is(
+ .workspace-task-prompt p,
+ .workspace-task-tool__body > span,
+ .workspace-task-changes > span:not(.workspace-task-changes__icon)
+ ) {
+ line-height: var(--preview-content-leading);
+}
+
+.workspace-preview[data-typography]
+ :is(
+ .workspace-task-plan strong,
+ .workspace-task-tool strong,
+ .workspace-task-changes strong
+ ) {
+ font-weight: var(--preview-heading-weight);
+ line-height: var(--preview-heading-leading);
+ letter-spacing: var(--preview-heading-tracking);
+}
+
+.workspace-preview[data-typography]:lang(zh)
+ :is(
+ .workspace-task-plan strong,
+ .workspace-task-tool strong,
+ .workspace-task-changes strong
+ ) {
+ letter-spacing: normal;
+}
+
+.workspace-preview--compact .workspace-task-activity {
gap: 6px;
- margin-top: 14px;
}
-.workspace-dialog-layer i {
- display: block;
- width: 58px;
- height: 26px;
+.workspace-preview--compact .workspace-task-prompt {
+ padding: 7px 9px;
+}
+
+.workspace-preview--compact .workspace-task-plan {
+ padding: 8px 9px 7px;
+}
+
+.workspace-preview--compact .workspace-task-plan__steps span:nth-child(2) {
+ display: none;
+}
+
+.workspace-preview--compact .workspace-task-plan__steps {
+ grid-template-columns: repeat(2, minmax(0, 1fr));
+}
+
+.workspace-change-review,
+.workspace-terminal-activity {
+ display: grid;
+ width: min(530px, 94%);
+ margin: auto;
+ overflow: hidden;
+ border: 1px solid var(--preview-material-border);
+ border-radius: var(--preview-radius);
+ color: var(--preview-text);
+ background: var(--preview-material-overlay);
+ box-shadow: var(--preview-material-overlay-shadow);
+ backdrop-filter: var(--preview-material-filter);
+}
+
+.workspace-change-review__heading,
+.workspace-change-review__heading > span,
+.workspace-change-review__summary,
+.workspace-change-review__summary > span,
+.workspace-terminal-activity__heading,
+.workspace-terminal-activity__heading > span,
+.workspace-terminal-activity__heading > small,
+.workspace-terminal-activity__summary,
+.workspace-terminal-activity__summary > span {
+ display: flex;
+ align-items: center;
+}
+
+.workspace-change-review__heading,
+.workspace-change-review__summary,
+.workspace-terminal-activity__heading,
+.workspace-terminal-activity__summary {
+ justify-content: space-between;
+ gap: 10px;
+}
+
+.workspace-change-review__heading,
+.workspace-terminal-activity__heading {
+ min-height: 36px;
+ padding: 0 11px;
+ border-bottom: 1px solid
+ color-mix(in srgb, var(--preview-border) 68%, transparent);
+ background: color-mix(
+ in srgb,
+ var(--preview-surface-raised) 82%,
+ transparent
+ );
+}
+
+.workspace-change-review__heading > span,
+.workspace-terminal-activity__heading > span,
+.workspace-terminal-activity__heading > small,
+.workspace-change-review__summary > span,
+.workspace-terminal-activity__summary > span {
+ gap: 5px;
+}
+
+.workspace-change-review__heading strong,
+.workspace-terminal-activity__heading strong {
+ font-size: 7px;
+ font-weight: 680;
+}
+
+.workspace-change-review__heading svg,
+.workspace-terminal-activity__heading svg {
+ color: var(--preview-info);
+}
+
+.workspace-change-review__heading small,
+.workspace-terminal-activity__heading small {
+ color: var(--preview-muted);
+ font-size: 5.5px;
+}
+
+.workspace-change-review__body {
+ display: grid;
+ min-height: 150px;
+ grid-template-columns: 132px minmax(0, 1fr);
+}
+
+.workspace-change-files {
+ display: flex;
+ flex-direction: column;
+ gap: 4px;
+ padding: 8px;
+ border-right: 1px solid
+ color-mix(in srgb, var(--preview-border) 62%, transparent);
+ background: var(--preview-material-sunken);
+}
+
+.workspace-change-files > span {
+ display: flex;
+ min-height: 28px;
+ align-items: center;
+ gap: 5px;
+ padding: 0 7px;
+ border: 1px solid transparent;
+ border-radius: 6px;
+ color: var(--preview-muted);
+ font-size: 5.5px;
+}
+
+.workspace-change-files > span.is-active {
+ border-color: color-mix(
+ in srgb,
+ var(--preview-accent) 32%,
+ var(--preview-border)
+ );
+ color: var(--preview-text);
+ background: color-mix(in srgb, var(--preview-accent) 10%, transparent);
+}
+
+.workspace-change-files i {
+ min-width: 0;
+ flex: 1;
+ overflow: hidden;
+ font-style: normal;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+}
+
+.workspace-change-files small {
+ flex: none;
+ color: var(--preview-added);
+ font-size: 5px;
+}
+
+.workspace-change-diff {
+ min-width: 0;
+ padding: 9px 10px;
+ background: var(--preview-material-sunken);
+}
+
+.workspace-change-diff > header {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ gap: 8px;
+ margin-bottom: 7px;
+ padding-bottom: 7px;
+ border-bottom: 1px solid
+ color-mix(in srgb, var(--preview-border) 58%, transparent);
+ color: var(--preview-muted);
+ font-size: 5.4px;
+}
+
+.workspace-change-diff > header > span {
+ display: flex;
+ align-items: center;
+ gap: 5px;
+ color: var(--preview-text);
+}
+
+.workspace-change-diff code {
+ display: block;
+ min-height: 21px;
+ padding: 4px 7px;
+ color: var(--preview-muted);
+ font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
+ font-size: 5.3px;
+ line-height: 1.35;
+}
+
+.workspace-change-diff code[data-change="added"] {
+ color: var(--preview-added);
+ background: color-mix(in srgb, var(--preview-added) 11%, transparent);
+}
+
+.workspace-change-diff code[data-change="deleted"] {
+ color: var(--preview-deleted);
+ background: color-mix(in srgb, var(--preview-deleted) 11%, transparent);
+}
+
+.workspace-change-review__summary,
+.workspace-terminal-activity__summary {
+ min-height: 34px;
+ padding: 0 11px;
+ border-top: 1px solid
+ color-mix(in srgb, var(--preview-border) 62%, transparent);
+ color: var(--preview-success);
+ font-size: 5.5px;
+}
+
+.workspace-change-review__summary em {
+ padding: 3px 5px;
+ border-radius: 4px;
+ font-size: 5.3px;
+ font-style: normal;
+ font-weight: 700;
+}
+
+.workspace-change-review__summary em[data-change="added"] {
+ color: var(--preview-added);
+ background: color-mix(in srgb, var(--preview-added) 10%, transparent);
+}
+
+.workspace-change-review__summary em[data-change="deleted"] {
+ color: var(--preview-deleted);
+ background: color-mix(in srgb, var(--preview-deleted) 10%, transparent);
+}
+
+.workspace-terminal-tabs {
+ display: flex;
+ min-height: 30px;
+ align-items: flex-end;
+ gap: 3px;
+ padding: 0 10px;
+ border-bottom: 1px solid
+ color-mix(in srgb, var(--preview-border) 58%, transparent);
+ background: var(--preview-material-sunken);
+}
+
+.workspace-terminal-tabs span {
+ position: relative;
+ display: grid;
+ height: 29px;
+ place-items: center;
+ padding: 0 8px;
+ color: var(--preview-muted);
+ font-size: 5.5px;
+}
+
+.workspace-terminal-tabs span.is-active {
+ color: var(--preview-text);
+}
+
+.workspace-terminal-tabs span.is-active::after {
+ position: absolute;
+ right: 6px;
+ bottom: 0;
+ left: 6px;
+ height: 2px;
+ border-radius: 999px 999px 0 0;
+ background: var(--preview-accent);
+ content: "";
+}
+
+.workspace-terminal-tabs em {
+ margin: auto 0 auto auto;
+ color: var(--preview-muted);
+ font-size: 5px;
+ font-style: normal;
+}
+
+.workspace-terminal-output {
+ display: flex;
+ min-height: 154px;
+ margin: 0;
+ padding: 11px 13px;
+ flex-direction: column;
+ gap: 7px;
+ border: 0;
+ border-radius: 0;
+ color: var(--preview-muted);
+ background: var(--preview-material-sunken);
+ font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
+ font-size: 5.8px;
+ line-height: 1.35;
+}
+
+.workspace-terminal-output :is(code, samp) {
+ color: inherit;
+ font: inherit;
+}
+
+.workspace-terminal-output code {
+ color: var(--preview-text);
+}
+
+.workspace-terminal-output span {
+ color: var(--preview-success);
+}
+
+.workspace-terminal-output span[data-kind="prompt"] {
+ color: var(--preview-info);
+}
+
+.workspace-terminal-activity__summary small {
+ color: var(--preview-muted);
+ font-size: 5.3px;
+}
+
+.workspace-preview[data-decorations="expressive"]
+ :is(.workspace-change-review, .workspace-terminal-activity) {
+ border-color: color-mix(
+ in srgb,
+ var(--preview-accent) 38%,
+ var(--preview-border)
+ );
+ box-shadow:
+ 0 22px 54px rgba(0, 0, 0, 0.18),
+ inset 0 1px color-mix(in srgb, var(--preview-accent) 13%, transparent);
+}
+
+.workspace-preview--compact .workspace-change-review__body {
+ grid-template-columns: 100px minmax(0, 1fr);
+}
+
+.workspace-preview--compact .workspace-change-files > span {
+ padding: 0 5px;
+}
+
+.workspace-preview--compact .workspace-change-files small {
+ display: none;
+}
+
+.workspace-settings-state {
+ display: grid;
+ width: min(500px, 90%);
+ min-height: 220px;
+ margin: auto;
+ grid-template-columns: 120px minmax(0, 1fr);
+ overflow: hidden;
+ border: 1px solid var(--preview-material-border);
+ border-radius: var(--preview-radius);
+ background: var(--preview-material-overlay);
+ box-shadow: var(--preview-material-overlay-shadow);
+ backdrop-filter: var(--preview-material-filter);
+}
+
+.workspace-settings-state nav {
+ display: flex;
+ flex-direction: column;
+ gap: 4px;
+ padding: 12px 9px;
+ border-right: 1px solid var(--preview-border);
+}
+
+.workspace-settings-state nav span {
+ display: flex;
+ min-height: 28px;
+ align-items: center;
+ gap: 7px;
+ padding: 0 8px;
+ border-radius: 6px;
+ color: var(--preview-muted);
+ font-size: 7px;
+}
+
+.workspace-settings-state nav span.is-active {
+ color: var(--preview-text);
+ background: color-mix(in srgb, var(--preview-accent) 15%, transparent);
+}
+
+.workspace-settings-state > div {
+ display: flex;
+ flex-direction: column;
+ gap: 11px;
+ padding: 20px;
+}
+
+.workspace-settings-state > div strong {
+ margin-bottom: 6px;
+ font-size: 11px;
+}
+
+.workspace-settings-state > div > span {
+ display: block;
+ height: 34px;
border: 1px solid var(--preview-border);
border-radius: 7px;
+ background: color-mix(in srgb, var(--preview-control) 88%, transparent);
+}
+
+.workspace-setting-row {
+ display: flex !important;
+ align-items: center;
+ justify-content: space-between;
+ gap: 8px;
+ padding: 6px 8px;
+}
+
+.workspace-setting-row i {
+ display: flex;
+ min-width: 0;
+ flex-direction: column;
+ gap: 2px;
+ font-style: normal;
+}
+
+.workspace-setting-row b {
+ color: var(--preview-text);
+ font-size: 6.5px;
+ font-weight: 650;
+}
+
+.workspace-setting-row small {
+ color: var(--preview-muted);
+ font-size: 5.5px;
+}
+
+.workspace-setting-row em {
+ flex: none;
+ padding: 3px 5px;
+ border: 1px solid var(--preview-border);
+ border-radius: 5px;
+ color: var(--preview-muted);
+ font-size: 5.5px;
+ font-style: normal;
+}
+
+.workspace-setting-row em.is-switch {
+ position: relative;
+ width: 23px;
+ height: 13px;
+ padding: 0;
+ border-color: color-mix(in srgb, var(--preview-accent) 55%, transparent);
+ border-radius: 999px;
+ background: color-mix(in srgb, var(--preview-accent) 56%, transparent);
+}
+
+.workspace-setting-row em.is-switch::after {
+ position: absolute;
+ top: 2px;
+ right: 2px;
+ width: 7px;
+ height: 7px;
+ border-radius: 50%;
+ background: var(--preview-text);
+ content: "";
+}
+
+.workspace-components-state {
+ display: flex;
+ width: min(560px, 92%);
+ margin: auto;
+ flex-direction: column;
+ gap: 8px;
+ padding: 14px;
+ border: 1px solid var(--preview-material-border);
+ border-radius: var(--preview-radius);
+ color: var(--preview-text);
+ background: var(--preview-material-overlay);
+ box-shadow: var(--preview-material-overlay-shadow);
+ backdrop-filter: var(--preview-material-filter);
+}
+
+.workspace-components-state__heading,
+.workspace-component-diff__heading,
+.workspace-component-diff__heading > span {
+ display: flex;
+ align-items: center;
+}
+
+.workspace-components-state__heading,
+.workspace-component-diff__heading {
+ justify-content: space-between;
+ gap: 10px;
+}
+
+.workspace-components-state__heading > span {
+ display: flex;
+ min-width: 0;
+ flex-direction: column;
+ gap: 2px;
+}
+
+.workspace-components-state__heading strong {
+ font-size: 10px;
+}
+
+.workspace-components-state__heading small,
+.workspace-component-diff__heading small {
+ color: var(--preview-muted);
+ font-size: 6px;
+}
+
+.workspace-components-state__heading em {
+ flex: none;
+ padding: 4px 6px;
+ border: 1px solid color-mix(in srgb, var(--preview-accent) 38%, transparent);
+ border-radius: 999px;
+ color: var(--preview-info);
+ background: color-mix(in srgb, var(--preview-accent) 9%, transparent);
+ font-size: 5.5px;
+ font-style: normal;
+ font-weight: 750;
+ letter-spacing: 0.09em;
+}
+
+.workspace-component-tabs {
+ display: grid;
+ grid-template-columns: repeat(3, 1fr);
+ padding: 3px;
+ border: 1px solid var(--preview-border);
+ border-radius: 7px;
+ background: color-mix(
+ in srgb,
+ var(--preview-surface-sunken) 90%,
+ transparent
+ );
+}
+
+.workspace-component-tabs span {
+ padding: 5px 7px;
+ border-radius: 5px;
+ color: var(--preview-muted);
+ font-size: 6px;
+ text-align: center;
+}
+
+.workspace-component-tabs span.is-active {
+ color: var(--preview-text);
+ background: var(--preview-control-active);
+ box-shadow: inset 0 -1px var(--preview-accent);
+}
+
+.workspace-component-grid {
+ display: grid;
+ grid-template-columns: repeat(2, minmax(0, 1fr));
+ gap: 7px;
+}
+
+.workspace-component-card {
+ min-width: 0;
+ padding: 10px;
+ border: 1px solid var(--preview-material-border);
+ border-radius: calc(var(--preview-radius) * 0.66);
+ background: var(--preview-material-raised);
+ box-shadow: var(--preview-material-raised-shadow);
+ backdrop-filter: var(--preview-material-filter);
+}
+
+.workspace-component-card > strong,
+.workspace-component-diff__heading strong {
+ font-size: 7px;
+}
+
+.workspace-component-actions {
+ display: flex;
+ gap: 5px;
+ margin: 8px 0;
+}
+
+.workspace-component-actions button {
+ min-width: 0;
+ height: 24px;
+ flex: 1;
+ border: 1px solid var(--preview-border);
+ border-radius: 6px;
+ color: var(--preview-text);
+ background: var(--preview-control);
+ font-size: 5.5px;
+}
+
+.workspace-component-actions button.is-primary {
+ border-color: var(--preview-accent);
+ color: var(--preview-on-accent);
+ background: var(--preview-accent);
+ box-shadow: 0 5px 12px
+ color-mix(in srgb, var(--preview-accent) 24%, transparent);
+}
+
+.workspace-component-actions button:disabled {
+ color: var(--preview-muted);
+ background: color-mix(in srgb, var(--preview-control) 48%, transparent);
+ opacity: 0.58;
+}
+
+.workspace-component-input {
+ display: flex;
+ height: 27px;
+ align-items: center;
+ gap: 6px;
+ padding: 0 7px;
+ border: 1px solid
+ color-mix(in srgb, var(--preview-focus) 54%, var(--preview-border));
+ border-radius: 6px;
+ color: var(--preview-muted);
+ background: var(--preview-control);
+ box-shadow: 0 0 0 2px
+ color-mix(in srgb, var(--preview-focus) 10%, transparent);
+ font-size: 5.5px;
+}
+
+.workspace-component-input span {
+ min-width: 0;
+ flex: 1;
+}
+
+.workspace-component-caret {
+ width: 1px;
+ height: 11px;
+ flex: none;
+ background: var(--preview-accent);
+ box-shadow: 0 0 6px color-mix(in srgb, var(--preview-accent) 30%, transparent);
+}
+
+.workspace-component-input kbd {
+ padding: 2px 4px;
+ border: 1px solid var(--preview-border);
+ border-radius: 4px;
+ color: var(--preview-muted);
+ background: var(--preview-surface-sunken);
+ font: inherit;
+}
+
+.workspace-component-interactions {
+ display: flex;
+ min-width: 0;
+ align-items: center;
+ gap: 7px;
+ margin-top: 7px;
+ font-size: 5.5px;
+}
+
+.workspace-component-selection {
+ padding: 2px 4px;
+ border-radius: 3px;
+}
+
+.workspace-component-link {
+ margin-left: auto;
+ text-decoration: underline;
+}
+
+.workspace-component-scroll-sample {
+ display: grid;
+ width: 28px;
+ height: 14px;
+ flex: none;
+ gap: 2px;
+ overflow-x: hidden;
+ overflow-y: scroll;
+ padding-right: 2px;
+}
+
+.workspace-component-scroll-sample > i {
+ display: block;
+ height: 5px;
+ border-radius: 999px;
+ background: color-mix(in srgb, var(--preview-muted) 34%, transparent);
+}
+
+.workspace-component-status {
+ display: flex;
+ min-height: 23px;
+ align-items: center;
+ gap: 6px;
+ margin-top: 5px;
+ padding: 0 7px;
+ border: 1px solid currentColor;
+ border-radius: 6px;
+ font-size: 5.8px;
+}
+
+.workspace-component-status.is-success {
+ color: var(--preview-success);
+ background: color-mix(in srgb, var(--preview-success) 10%, transparent);
+}
+
+.workspace-component-status.is-warning {
+ color: var(--preview-warning);
+ background: color-mix(in srgb, var(--preview-warning) 10%, transparent);
+}
+
+.workspace-component-status.is-danger {
+ color: var(--preview-danger);
+ background: color-mix(in srgb, var(--preview-danger) 10%, transparent);
+}
+
+.workspace-component-depth__heading {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ gap: 8px;
+}
+
+.workspace-component-depth__heading small {
+ color: var(--preview-info);
+ font-size: 5.5px;
+}
+
+.workspace-component-depth__stack {
+ position: relative;
+ display: grid;
+ min-height: 76px;
+ margin-top: 8px;
+ place-items: center;
+}
+
+.workspace-component-depth__stack span {
+ position: absolute;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ border: 1px solid var(--preview-border);
+ color: var(--preview-muted);
+ font-size: 5.5px;
+}
+
+.workspace-component-depth__stack span[data-surface="canvas"] {
+ inset: 0;
+ align-items: flex-end;
+ justify-content: flex-start;
+ padding: 6px;
+ border-style: dashed;
+ border-radius: 7px;
+ background: var(--preview-bg);
+}
+
+.workspace-component-depth__stack span[data-surface="raised"] {
+ width: 72%;
+ height: 52px;
+ border-radius: 7px;
+ color: var(--preview-text);
+ background: var(--preview-material-raised);
+ box-shadow: var(--preview-material-raised-shadow);
+ backdrop-filter: var(--preview-material-filter);
+}
+
+.workspace-component-depth__stack span[data-surface="overlay"] {
+ width: 44%;
+ height: 30px;
+ border-color: color-mix(
+ in srgb,
+ var(--preview-accent) 30%,
+ var(--preview-border)
+ );
+ border-radius: 6px;
+ color: var(--preview-text);
+ background: var(--preview-material-overlay);
+ box-shadow: var(--preview-material-overlay-shadow);
+ backdrop-filter: var(--preview-material-filter);
+}
+
+.workspace-component-diff {
+ grid-column: 1 / -1;
+ padding: 8px 10px 9px;
+ background: var(--preview-material-sunken);
+}
+
+.workspace-component-diff__heading {
+ margin-bottom: 5px;
+}
+
+.workspace-component-diff__heading > span {
+ gap: 5px;
+}
+
+.workspace-component-diff code {
+ display: block;
+ margin: 2px 0;
+ padding: 3px 6px;
+ border-radius: 4px;
+ color: var(--preview-muted);
+ font-size: 5.5px;
+ line-height: 1.35;
+}
+
+.workspace-component-diff code[data-change="added"] {
+ color: var(--preview-added);
+ background: color-mix(in srgb, var(--preview-added) 10%, transparent);
+}
+
+.workspace-component-diff code[data-change="modified"] {
+ color: var(--preview-modified);
+ background: color-mix(in srgb, var(--preview-modified) 10%, transparent);
+}
+
+.workspace-component-diff code[data-change="deleted"] {
+ color: var(--preview-deleted);
+ background: color-mix(in srgb, var(--preview-deleted) 10%, transparent);
+}
+
+.workspace-preview[data-decorations="expressive"] .workspace-component-card {
+ border-color: color-mix(
+ in srgb,
+ var(--preview-accent) 28%,
+ var(--preview-border)
+ );
+ box-shadow: 0 8px 18px rgba(0, 0, 0, 0.12);
+}
+
+.workspace-right-panel {
+ position: relative;
+ z-index: 5;
+ width: auto;
+ min-width: 0;
+ margin: 8px 8px 8px 0;
+ padding: 11px;
+ overflow: hidden;
+ border: 1px solid var(--preview-material-border);
+ border-radius: calc(var(--preview-radius) * 0.72);
+ background: var(--preview-material-overlay);
+ box-shadow: var(--preview-material-overlay-shadow);
+ backdrop-filter: var(--preview-material-filter);
+}
+
+.workspace-right-panel__heading {
+ display: flex;
+ align-items: center;
+ gap: 7px;
+ margin-bottom: 16px;
+}
+
+.workspace-right-panel strong {
+ font-size: 9px;
+}
+
+.workspace-right-panel__tabs {
+ display: grid;
+ margin: 0 0 8px !important;
+ grid-template-columns: 1fr 1fr;
+ gap: 3px !important;
+ padding: 3px;
+ border: 1px solid var(--preview-border);
+ border-radius: 6px;
+ background: color-mix(
+ in srgb,
+ var(--preview-surface-sunken) 88%,
+ transparent
+ );
+}
+
+.workspace-right-panel__tabs span {
+ padding: 4px;
+ border-radius: 4px;
+ color: var(--preview-muted);
+ font-size: 5.5px;
+ text-align: center;
+}
+
+.workspace-right-panel__tabs span.is-active {
+ color: var(--preview-text);
+ background: color-mix(in srgb, var(--preview-accent) 14%, transparent);
+ box-shadow: inset 0 -1px var(--preview-accent);
+}
+
+.workspace-right-panel__row {
+ display: flex;
+ min-height: 30px;
+ align-items: center;
+ justify-content: space-between;
+ gap: 6px;
+ margin-top: 6px;
+ padding: 0 7px;
+ border: 1px solid var(--preview-border);
+ border-radius: 6px;
+ background: color-mix(in srgb, var(--preview-control) 88%, transparent);
+}
+
+.workspace-right-panel__row i,
+.workspace-right-panel__row small {
+ overflow: hidden;
+ font-size: 5.5px;
+ font-style: normal;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+}
+
+.workspace-right-panel__row small {
+ color: color-mix(in srgb, var(--preview-accent) 72%, var(--preview-text));
+}
+
+.workspace-dialog-layer {
+ position: absolute;
+ z-index: 8;
+ inset: 0;
+ display: grid;
+ place-items: center;
+ background: rgba(8, 10, 13, 0.48);
+ backdrop-filter: blur(2px);
+}
+
+.workspace-dialog-layer > div {
+ width: min(300px, 62%);
+ padding: 18px;
+ border: 1px solid var(--preview-material-border);
+ border-radius: var(--preview-radius);
+ color: var(--preview-text);
+ background: var(--preview-material-overlay);
+ box-shadow: var(--preview-material-overlay-shadow);
+ backdrop-filter: var(--preview-material-filter);
+}
+
+.workspace-dialog-layer strong,
+.workspace-dialog-layer p {
+ display: block;
+}
+
+.workspace-dialog-layer strong {
+ margin: 10px 0 5px;
+ font-size: 11px;
+}
+
+.workspace-dialog-layer p {
+ margin: 0;
+ color: var(--preview-muted);
+ font-size: 8px;
+ line-height: 1.5;
+}
+
+.workspace-dialog-actions {
+ display: flex;
+ justify-content: flex-end;
+ gap: 6px;
+ margin-top: 14px;
+}
+
+.workspace-dialog-actions button {
+ min-width: 58px;
+ height: 26px;
+ border: 1px solid var(--preview-border);
+ border-radius: 7px;
+ color: var(--preview-muted);
+ background: color-mix(in srgb, var(--preview-control) 90%, transparent);
+ font-size: 6px;
+}
+
+/* The detail treatment must remain visible outside the main conversation.
+ These rules only change material depth; focus-surface geometry and stacking
+ remain owned by their scenario components. */
+.workspace-preview[data-decorations="subtle"]
+ :is(
+ .workspace-settings-state,
+ .workspace-right-panel,
+ .workspace-dialog-layer > div
+ ) {
+ border-color: color-mix(
+ in srgb,
+ var(--preview-accent) 28%,
+ var(--preview-border)
+ );
+ box-shadow:
+ inset 0 1px color-mix(in srgb, var(--preview-accent) 12%, transparent),
+ 0 16px 40px rgba(0, 0, 0, 0.14),
+ 0 0 0 2px color-mix(in srgb, var(--preview-accent) 5%, transparent);
+}
+
+.workspace-preview[data-decorations="expressive"]
+ :is(
+ .workspace-settings-state,
+ .workspace-right-panel,
+ .workspace-dialog-layer > div
+ ) {
+ border-color: color-mix(
+ in srgb,
+ var(--preview-accent) 48%,
+ var(--preview-border)
+ );
+ box-shadow:
+ inset 0 2px color-mix(in srgb, var(--preview-accent) 20%, transparent),
+ 0 24px 58px rgba(0, 0, 0, 0.2),
+ 0 0 0 3px color-mix(in srgb, var(--preview-accent) 8%, transparent);
+ backdrop-filter: saturate(1.14) blur(var(--preview-focus-blur));
+}
+
+.workspace-preview[data-decorations="expressive"]
+ .workspace-settings-state
+ > div,
+.workspace-preview[data-decorations="expressive"] .workspace-right-panel__tabs {
+ background: color-mix(
+ in srgb,
+ var(--preview-surface-raised) 84%,
+ transparent
+ );
+ box-shadow: inset 3px 0
+ color-mix(in srgb, var(--preview-accent) 58%, var(--preview-border));
+}
+
+.workspace-preview[data-decorations="expressive"] .workspace-dialog-layer {
+ background: color-mix(
+ in srgb,
+ var(--preview-accent) 5%,
+ rgba(8, 10, 13, 0.52)
+ );
}
-.workspace-dialog-layer i:last-child {
+.workspace-dialog-actions button:last-child {
border-color: var(--preview-accent);
+ color: var(--preview-on-accent);
background: var(--preview-accent);
}
diff --git a/apps/desktop/src/test/fixtures/codex-dom.ts b/apps/desktop/src/test/fixtures/codex-dom.ts
index ff2dfc4..2a2b1d3 100644
--- a/apps/desktop/src/test/fixtures/codex-dom.ts
+++ b/apps/desktop/src/test/fixtures/codex-dom.ts
@@ -23,10 +23,25 @@ export function codexFixture(page: CodexFixturePage): string {
`
: `
-
- Conversation
- Supporting context
-
+
+
+ Refine the task experience
+
+
+ Implementation plan
+ Two of three steps complete
+
+
+ Run checks
+ pnpm testAll checks passed
+
+ legacy surface semantic surface
+
+ File Change
+ runtime.js Updated
+
+
+
diff --git a/apps/site/package.json b/apps/site/package.json
index 0c7c36a..986aefe 100644
--- a/apps/site/package.json
+++ b/apps/site/package.json
@@ -1,6 +1,6 @@
{
"name": "@codex-styler/site",
- "version": "0.2.0-beta.6",
+ "version": "0.2.0-beta.7",
"private": true,
"type": "module",
"scripts": {
diff --git a/apps/site/src/components/LandingPage.astro b/apps/site/src/components/LandingPage.astro
index 46de1aa..e00689a 100644
--- a/apps/site/src/components/LandingPage.astro
+++ b/apps/site/src/components/LandingPage.astro
@@ -7,9 +7,9 @@ const zh = locale === "zh-CN";
const base = import.meta.env.BASE_URL.replace(/\/$/, "");
const docsBase = base + (zh ? "/zh-cn/docs/" : "/docs/");
const repo = "https://github.com/xuhuanstudio/codex-styler";
-const releaseTag = "v0.2.0-beta.6";
-const macAsset = "Codex-Styler_0.2.0-beta.6_aarch64-unsigned.dmg";
-const windowsAsset = "Codex-Styler_0.2.0-beta.6_x64-unsigned-setup.exe";
+const releaseTag = "v0.2.0-beta.7";
+const macAsset = "Codex-Styler_0.2.0-beta.7_aarch64-unsigned.dmg";
+const windowsAsset = "Codex-Styler_0.2.0-beta.7_x64-unsigned-setup.exe";
const macDownloadUrl = `${repo}/releases/download/${releaseTag}/${macAsset}`;
const windowsDownloadUrl = `${repo}/releases/download/${releaseTag}/${windowsAsset}`;
const copy = zh
diff --git a/apps/site/src/data/docs.ts b/apps/site/src/data/docs.ts
index 430045f..0286d4d 100644
--- a/apps/site/src/data/docs.ts
+++ b/apps/site/src/data/docs.ts
@@ -310,7 +310,7 @@ export const docs: DocPage[] = [
{
heading: "Current Beta targets",
paragraphs: [
- "Unsigned Beta installers are available for macOS on Apple Silicon and Windows 11 x64. macOS has the current real-device evidence; Windows Beta 6 is produced and structurally checked in CI but explicitly awaits community validation. The RC gate requires at least two complete independent device reports per platform.",
+ "Unsigned Beta installers are available for macOS on Apple Silicon and Windows 11 x64. macOS has the current real-device evidence; Windows Beta 7 is produced and structurally checked in CI but explicitly awaits community validation. The RC gate requires at least two complete independent device reports per platform.",
],
},
{
@@ -324,7 +324,7 @@ export const docs: DocPage[] = [
{
heading: "当前 Beta 目标",
paragraphs: [
- "当前提供未签名的 Apple Silicon macOS 与 Windows 11 x64 Beta 安装包。macOS 已有现阶段真实设备证据;Windows Beta 6 由 CI 构建并完成结构校验,但明确等待社区实机验证。进入 RC 前,两个平台都至少需要两份独立的完整实机报告。",
+ "当前提供未签名的 Apple Silicon macOS 与 Windows 11 x64 Beta 安装包。macOS 已有现阶段真实设备证据;Windows Beta 7 由 CI 构建并完成结构校验,但明确等待社区实机验证。进入 RC 前,两个平台都至少需要两份独立的完整实机报告。",
],
},
{
diff --git a/apps/site/src/layouts/BaseLayout.astro b/apps/site/src/layouts/BaseLayout.astro
index 1b18046..74abf33 100644
--- a/apps/site/src/layouts/BaseLayout.astro
+++ b/apps/site/src/layouts/BaseLayout.astro
@@ -19,7 +19,7 @@ const english = new URL(base + path, Astro.site);
const chinese = new URL(base + "/zh-cn" + path, Astro.site);
const socialImage = new URL(base + image, Astro.site);
const repo = "https://github.com/xuhuanstudio/codex-styler";
-const releaseTag = "v0.2.0-beta.6";
+const releaseTag = "v0.2.0-beta.7";
const downloadUrl = `${repo}/releases/tag/${releaseTag}`;
const isChinese = locale === "zh-CN";
const structuredData = {
@@ -28,7 +28,7 @@ const structuredData = {
name: "Codex Styler",
applicationCategory: "DeveloperApplication",
operatingSystem: "macOS 13 or later; Windows 11",
- softwareVersion: "0.2.0-beta.6",
+ softwareVersion: "0.2.0-beta.7",
description,
url: canonical.toString(),
downloadUrl,
diff --git a/docs/architecture.md b/docs/architecture.md
index 4bb0e40..25039f2 100644
--- a/docs/architecture.md
+++ b/docs/architecture.md
@@ -26,6 +26,18 @@ Tauri command boundary ── theme package validator
Theme packages are portable data. They cannot invoke Tauri commands, execute scripts, request remote resources, or reference arbitrary local paths.
+## Desktop frontend boundaries
+
+The desktop frontend follows vertical feature boundaries rather than placing product logic in page components:
+
+- **App shell** owns navigation, session wiring, global dialogs, and command dispatch. It does not render resource-specific detail experiences.
+- **Feature views** own collection selection, filtering, and the selected resource id. A view coordinates list and detail components but does not implement their visual behavior.
+- **Detail and workspace components** own only local preview or editing interaction. They receive explicit domain data and callbacks; they do not invoke Tauri or the injected runtime directly.
+- **Shared UI primitives** own repeated controls such as library tabs, search, status, dialogs, and fields. Themes and companions must not fork equivalent interaction patterns.
+- **Domain helpers and reducers** remain pure. Runtime, archive, and filesystem effects stay behind command modules so async revisions and recovery rules can be tested independently.
+
+Large modules are reduced by extracting one complete behavior boundary at a time. An extraction must preserve user-visible behavior, retain or add a boundary test, and pass the real desktop fixture before adjacent features are moved. This keeps maintenance work reviewable and avoids replacing one monolith with many state-leaking fragments.
+
## Managed Codex lifecycle
1. Detect the installed Codex application and whether a verified Codex UI process is already running. A custom path is a validated fallback.
diff --git a/docs/creating-companions.md b/docs/creating-companions.md
index 4b0ced8..d231a6e 100644
--- a/docs/creating-companions.md
+++ b/docs/creating-companions.md
@@ -18,7 +18,7 @@ Supported video input depends on the system WebView. MP4 or MOV with H.264 is th
4. **Align** — inspect opaque bounds, set one shared crop and ground line, then use baseline translation only. Never resize individual frames.
5. **Calibrate** — select a frame and point the direction dial toward the pointer position that should use it. The dial, angle number, anchor, and timeline are bidirectionally linked. Set at least four anchors; eight is recommended.
6. **Motions** — mark blink, breathing, or gesture ranges, their playback direction, speed, delay, and compatible direction poses. Exclude damaged or unrelated frames.
-7. **Test & Save** — move a real pointer, resize the simulated composer, drag and snap the companion, inspect reduced motion, then build and re-import the package.
+7. **Test & Save** — move a real pointer, resize the simulated composer, drag and snap the companion, inspect reduced motion, follow any automated edge-risk links back to the affected cleanup frame, review the final pixels on black, white, and theme-colored surfaces, then build and re-import the package. The local scan is advisory: it does not replace the visual review or block packaging by itself.
## Direction calibration
diff --git a/docs/product-quality-plan.md b/docs/product-quality-plan.md
index cc6692c..57462c0 100644
--- a/docs/product-quality-plan.md
+++ b/docs/product-quality-plan.md
@@ -209,6 +209,55 @@ working control behind another page, revert it.
- disconnected theme, variant, and companion selection now communicates
through the selected row and live setup bar instead of redundant Toasts that
could cover the next action. Save, apply, recovery, and error Toasts remain.
+- final companion output now includes a guided black, white, and theme-surface
+ edge review for color spill, halos, broken transparency, and residual
+ background pixels. Review evidence is stored with a compact output
+ fingerprint and automatically expires after any pixel, crop, scale,
+ inclusion, or alignment change.
+- edge review now shares the compiler's exact crop, baseline, scale, and canvas
+ renderer and performs a bounded local scan across behavior-defining and
+ representative frames. Advisory findings identify likely retained
+ backgrounds, canvas contact, isolated residue, and sampled-color spill, then
+ return the user to an affected cleanup frame. Heuristic findings never block
+ packaging or replace the explicit three-surface review.
+- semantic theme rendering now protects component boundaries as part of the
+ same surface-aware color contract used for text, status colors, and accent
+ controls. Authored border hues are retained when safe and move only as far
+ as required to preserve subtle, regular, and strong interface hierarchy;
+- the Theme Editor replaces its generic contrast promise with an expandable
+ visual safety result derived from the exact preview/runtime palette. It shows
+ primary and secondary text, accent-control, boundary, and image-surface guard
+ evidence without mutating the authored theme data.
+- contained and themed icon treatments now use dedicated surface-aware icon
+ roles instead of raw accent/text mixing. Their non-layout visual plates keep
+ the original SVG size, padding, and stroke geometry intact, while preview,
+ injected Codex tokens, and visual-safety evidence share the same result.
+- status, validation, and inline diff foregrounds are now validated against
+ both the component surface and the tinted feedback surface they actually
+ render on. Success, warning, error, info, added, modified, and deleted roles
+ retain authored hues when safe and receive the smallest necessary neutral
+ correction otherwise; the Theme Editor exposes status and code-change
+ evidence from the same preview/runtime contract.
+- theme layout now carries a bounded typography rhythm into Codex content.
+ Balanced, editorial, and cinematic treatments coordinate heading weight,
+ heading spacing, and prose leading without replacing Codex fonts, resizing
+ text, or changing component geometry; CJK headings keep neutral tracking and
+ the library preview uses the same derived personality as the runtime.
+- theme surface controls now derive solid, layered, and frosted material
+ behavior without extending the public theme format. Preview and runtime use
+ the same raised, overlay, and sunken roles across cards, composer, code,
+ settings, dialogs, menus, and the right panel while preserving Codex layout,
+ hit targets, and component geometry.
+- theme motion intensity now resolves to still, calm, fluid, or expressive
+ interaction profiles in both preview and runtime. The profiles coordinate
+ geometry-safe control feedback, native overlay entry, scene parallax, and
+ reduced-motion fallback without changing Codex measurements or hit targets.
+- the final companion stage now auditions direction selection and saved idle
+ motion as one runtime behavior. The inspector exposes the active direction,
+ only offers clips assigned to that direction sector, uses compiled playback
+ timing, and cancels the motion as soon as the pointer moves. Compiler pose
+ assignments use the same angular sectors, so interpolated poses no longer
+ make a valid idle motion practically unreachable after installation.
## RC — reliability proof
diff --git a/package.json b/package.json
index eaad154..cfcbbb6 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "codex-styler",
- "version": "0.2.0-beta.6",
+ "version": "0.2.0-beta.7",
"private": true,
"description": "Unofficial, open-source Codex theme editor and Codex skin creator for OpenAI Codex Desktop.",
"license": "Apache-2.0",
diff --git a/packages/theme-core/package.json b/packages/theme-core/package.json
index ce2dc8c..29a7201 100644
--- a/packages/theme-core/package.json
+++ b/packages/theme-core/package.json
@@ -1,6 +1,6 @@
{
"name": "@codex-styler/theme-core",
- "version": "0.2.0-beta.6",
+ "version": "0.2.0-beta.7",
"private": true,
"type": "module",
"exports": {
diff --git a/packages/theme-core/src/builtins.ts b/packages/theme-core/src/builtins.ts
index 0e9c86c..eee8aca 100644
--- a/packages/theme-core/src/builtins.ts
+++ b/packages/theme-core/src/builtins.ts
@@ -1,6 +1,10 @@
import type { ThemeDefinition, ThemeVariant } from "./types";
import { THEME_FORMAT } from "./types";
+type BuiltinSemanticPalette = Required<
+ NonNullable
+>;
+
function variant(
background: string,
surface: string,
@@ -51,7 +55,7 @@ function variant(
function detailedVariant(
base: ThemeVariant,
- palette: NonNullable,
+ palette: BuiltinSemanticPalette,
motion: Partial,
radius = base.appearance.radius,
): ThemeVariant {
@@ -84,29 +88,79 @@ export const nativeRefined: ThemeDefinition = {
},
},
variants: {
- light: variant(
- "#e9e7e1",
- "#f7f5ef",
- "#20201e",
- "#6e6c66",
- "#c9c5ba",
- "#446f63",
- "#e9e7e1",
- 0.06,
- 0.88,
- { layout: "native", iconStyle: "contained", decorations: "subtle" },
+ light: detailedVariant(
+ variant(
+ "#e9e7e1",
+ "#f7f5ef",
+ "#20201e",
+ "#605e58",
+ "#c9c5ba",
+ "#446f63",
+ "#e9e7e1",
+ 0.06,
+ 0.88,
+ { layout: "native", iconStyle: "contained", decorations: "subtle" },
+ ),
+ {
+ canvas: "#e9e7e1",
+ surfaceRaised: "#fffdf7",
+ surfaceOverlay: "#f1eee6",
+ surfaceSunken: "#e5e2da",
+ control: "#eeeae1",
+ controlHover: "#e4e0d6",
+ controlActive: "#d5ddd8",
+ textTertiary: "#6d6a63",
+ onAccent: "#f7fbf8",
+ borderSubtle: "#d8d4ca",
+ borderStrong: "#918d83",
+ focus: "#446f63",
+ success: "#2f704f",
+ warning: "#87570f",
+ danger: "#a33f3a",
+ info: "#315f85",
+ added: "#2f704f",
+ modified: "#87570f",
+ deleted: "#a33f3a",
+ },
+ { intensity: 0.5, parallax: 3, targetFps: 30 },
+ 12,
),
- dark: variant(
- "#151716",
- "#202321",
- "#f1eee6",
- "#a7aaa5",
- "#3a3e3b",
- "#8dc9b3",
- "#0c0e0d",
- 0.18,
- 0.9,
- { layout: "native", iconStyle: "contained", decorations: "subtle" },
+ dark: detailedVariant(
+ variant(
+ "#151716",
+ "#202321",
+ "#f1eee6",
+ "#b3b8b2",
+ "#3a3e3b",
+ "#8dc9b3",
+ "#0c0e0d",
+ 0.18,
+ 0.9,
+ { layout: "native", iconStyle: "contained", decorations: "subtle" },
+ ),
+ {
+ canvas: "#151716",
+ surfaceRaised: "#292d2a",
+ surfaceOverlay: "#303531",
+ surfaceSunken: "#171a18",
+ control: "#2a2e2b",
+ controlHover: "#343a36",
+ controlActive: "#3d4a43",
+ textTertiary: "#949b95",
+ onAccent: "#10251e",
+ borderSubtle: "#303632",
+ borderStrong: "#5c665f",
+ focus: "#8dc9b3",
+ success: "#75c89c",
+ warning: "#e0b35e",
+ danger: "#e27b75",
+ info: "#82b9df",
+ added: "#75c89c",
+ modified: "#e0b35e",
+ deleted: "#e27b75",
+ },
+ { intensity: 0.5, parallax: 3, targetFps: 30 },
+ 12,
),
},
scene: {
@@ -164,30 +218,88 @@ export const nocturneStudio: ThemeDefinition = {
},
},
variants: {
- light: variant(
- "#d6d3cc",
- "#f0ede6",
- "#24231f",
- "#67645c",
- "#b8b3a8",
- "#a35d37",
- "#ddd6ca",
- 0.16,
- 0.82,
- { layout: "editorial", iconStyle: "themed", decorations: "expressive" },
+ light: detailedVariant(
+ variant(
+ "#d6d3cc",
+ "#f0ede6",
+ "#24231f",
+ "#514d44",
+ "#b8b3a8",
+ "#a35d37",
+ "#ddd6ca",
+ 0.16,
+ 0.82,
+ {
+ layout: "editorial",
+ iconStyle: "themed",
+ decorations: "expressive",
+ },
+ ),
+ {
+ canvas: "#d6d3cc",
+ surfaceRaised: "#faf7f0",
+ surfaceOverlay: "#ece6dc",
+ surfaceSunken: "#d8d3ca",
+ control: "#e8e1d6",
+ controlHover: "#dccfc1",
+ controlActive: "#cfbea9",
+ textTertiary: "#6d6359",
+ onAccent: "#fff8ef",
+ borderSubtle: "#c8c0b5",
+ borderStrong: "#86796c",
+ focus: "#9b5431",
+ success: "#2d6f50",
+ warning: "#86550e",
+ danger: "#a43e38",
+ info: "#2f6389",
+ added: "#2d6f50",
+ modified: "#86550e",
+ deleted: "#a43e38",
+ },
+ { intensity: 0.42, parallax: 6, targetFps: 30 },
+ 12,
),
dark: {
- ...variant(
- "#090b0d",
- "#14181a",
- "#f5efe6",
- "#a9a49b",
- "#34383a",
- "#e9a066",
- "#071014",
- 0.42,
- 0.72,
- { layout: "editorial", iconStyle: "themed", decorations: "expressive" },
+ ...detailedVariant(
+ variant(
+ "#090b0d",
+ "#14181a",
+ "#f5efe6",
+ "#b8b2aa",
+ "#34383a",
+ "#e9a066",
+ "#071014",
+ 0.42,
+ 0.72,
+ {
+ layout: "editorial",
+ iconStyle: "themed",
+ decorations: "expressive",
+ },
+ ),
+ {
+ canvas: "#090b0d",
+ surfaceRaised: "#1c2225",
+ surfaceOverlay: "#222a2d",
+ surfaceSunken: "#0e1214",
+ control: "#20272a",
+ controlHover: "#2a3336",
+ controlActive: "#343f42",
+ textTertiary: "#999c99",
+ onAccent: "#241207",
+ borderSubtle: "#2b3235",
+ borderStrong: "#596166",
+ focus: "#e9a066",
+ success: "#70bd8d",
+ warning: "#e1ae5b",
+ danger: "#e47a72",
+ info: "#7fb7de",
+ added: "#70bd8d",
+ modified: "#e1ae5b",
+ deleted: "#e47a72",
+ },
+ { intensity: 0.48, parallax: 8, targetFps: 30 },
+ 12,
),
background: {
color: "#090b0d",
@@ -270,17 +382,46 @@ export const quietGarden: ThemeDefinition = {
},
variants: {
light: {
- ...variant(
- "#d9dfd2",
- "#f2f1e8",
- "#263128",
- "#687269",
- "#b9c2b5",
- "#56745b",
- "#dbe3d6",
- 0.18,
- 0.76,
- { layout: "immersive", iconStyle: "themed", decorations: "expressive" },
+ ...detailedVariant(
+ variant(
+ "#d9dfd2",
+ "#f2f1e8",
+ "#263128",
+ "#4f5d51",
+ "#b9c2b5",
+ "#56745b",
+ "#dbe3d6",
+ 0.18,
+ 0.76,
+ {
+ layout: "immersive",
+ iconStyle: "themed",
+ decorations: "expressive",
+ },
+ ),
+ {
+ canvas: "#d9dfd2",
+ surfaceRaised: "#faf9f0",
+ surfaceOverlay: "#e7ebe0",
+ surfaceSunken: "#dce3d7",
+ control: "#e5e9df",
+ controlHover: "#d8dfd4",
+ controlActive: "#cbd7ca",
+ textTertiary: "#687269",
+ onAccent: "#fbfff8",
+ borderSubtle: "#c8d0c5",
+ borderStrong: "#7f8d80",
+ focus: "#56745b",
+ success: "#2f7047",
+ warning: "#7b5a0c",
+ danger: "#a4443d",
+ info: "#376686",
+ added: "#2f7047",
+ modified: "#7b5a0c",
+ deleted: "#a4443d",
+ },
+ { intensity: 0.34, parallax: 7, targetFps: 30 },
+ 18,
),
background: {
color: "#d9dfd2",
@@ -293,17 +434,46 @@ export const quietGarden: ThemeDefinition = {
},
},
dark: {
- ...variant(
- "#101612",
- "#18211b",
- "#edf2e8",
- "#a5afa5",
- "#364339",
- "#9fc29a",
- "#0d1510",
- 0.34,
- 0.68,
- { layout: "immersive", iconStyle: "themed", decorations: "expressive" },
+ ...detailedVariant(
+ variant(
+ "#101612",
+ "#18211b",
+ "#edf2e8",
+ "#b4bdb5",
+ "#364339",
+ "#9fc29a",
+ "#0d1510",
+ 0.34,
+ 0.68,
+ {
+ layout: "immersive",
+ iconStyle: "themed",
+ decorations: "expressive",
+ },
+ ),
+ {
+ canvas: "#101612",
+ surfaceRaised: "#202b23",
+ surfaceOverlay: "#263229",
+ surfaceSunken: "#111914",
+ control: "#243028",
+ controlHover: "#2e3d33",
+ controlActive: "#394a3e",
+ textTertiary: "#8f9b91",
+ onAccent: "#102015",
+ borderSubtle: "#2c392f",
+ borderStrong: "#59695d",
+ focus: "#9fc29a",
+ success: "#74c58b",
+ warning: "#dfb45c",
+ danger: "#df7d76",
+ info: "#83b8d9",
+ added: "#74c58b",
+ modified: "#dfb45c",
+ deleted: "#df7d76",
+ },
+ { intensity: 0.38, parallax: 8, targetFps: 30 },
+ 18,
),
background: {
color: "#101612",
diff --git a/packages/theme-core/tests/validation.test.ts b/packages/theme-core/tests/validation.test.ts
index da6d55d..1e5819d 100644
--- a/packages/theme-core/tests/validation.test.ts
+++ b/packages/theme-core/tests/validation.test.ts
@@ -51,7 +51,18 @@ describe("built-in themes", () => {
expect(JSON.stringify(theme)).not.toContain("--color-token");
});
- it("ships the two expressive themes with complete light and dark palettes", () => {
+ it("ships every built-in theme with a complete component-level palette", () => {
+ for (const theme of builtinThemes) {
+ for (const variant of Object.values(theme.variants)) {
+ expect(
+ Object.keys(variant.appearance.palette ?? {}),
+ `${theme.id} semantic palette`,
+ ).toHaveLength(19);
+ }
+ }
+ });
+
+ it("ships the two expressive themes with image-backed light and dark variants", () => {
const expressiveThemeIds = new Set([
"codex-styler.gilded-grandeur",
"codex-styler.merry-big-top",
@@ -63,10 +74,6 @@ describe("built-in themes", () => {
for (const theme of expressiveThemes) {
for (const variant of Object.values(theme.variants)) {
expect(variant.background.image, theme.id).toBeDefined();
- expect(
- Object.keys(variant.appearance.palette ?? {}),
- `${theme.id} semantic palette`,
- ).toHaveLength(19);
expect(variant.appearance.iconStyle).toBe("themed");
expect(variant.appearance.decorations).toBe("expressive");
}