feat: add bundled OpenCode skills and refine module flow
Some checks failed
Electron E2E / Electron E2E (macos-latest) (push) Has been cancelled
Electron E2E / Electron E2E (ubuntu-latest) (push) Has been cancelled
Electron E2E / Electron E2E (windows-latest) (push) Has been cancelled

This commit is contained in:
inman
2026-08-14 18:15:55 +08:00
parent 88f9ee8708
commit c809002180
31 changed files with 1153 additions and 241 deletions

View File

@@ -1,22 +1,10 @@
import { useEffect, useRef, useState } from 'react';
import { ChevronDown } from 'lucide-react';
import { useLocation, useNavigate } from 'react-router-dom';
import { DisclosureContent } from '@/components/ui/disclosure';
import { cn } from '@/lib/utils';
import { aiModules, getAiModuleForPath, type AiModuleId } from '@/lib/ai-modules';
const moduleToneClasses: Record<AiModuleId, { active: string }> = {
programming: { active: 'bg-brand-soft' },
painting: { active: 'bg-brand-soft' },
learning: { active: 'bg-brand-soft' },
robot: { active: 'bg-brand-soft' },
};
import { AI_MODULE_SELECTION_PATH, aiModules, getAiModuleForPath } from '@/lib/ai-modules';
export function ModuleSwitcher({ sidebarCollapsed, compact = false }: { sidebarCollapsed: boolean; compact?: boolean }) {
const location = useLocation();
const navigate = useNavigate();
const [open, setOpen] = useState(false);
const switcherRef = useRef<HTMLDivElement | null>(null);
const activeModuleId = getAiModuleForPath(location.pathname);
const activeModule = aiModules.find((module) => module.id === activeModuleId) ?? aiModules[0];
const getModuleLabel = (module: typeof aiModules[number]) => module.switcherLabel
@@ -24,88 +12,24 @@ export function ModuleSwitcher({ sidebarCollapsed, compact = false }: { sidebarC
const activeModuleLabel = getModuleLabel(activeModule);
const compactTrigger = compact && !sidebarCollapsed;
useEffect(() => {
if (!open) return undefined;
const handleMouseDown = (event: MouseEvent) => {
const target = event.target;
if (target instanceof Node && !switcherRef.current?.contains(target)) setOpen(false);
};
const handleKeyDown = (event: KeyboardEvent) => {
if (event.key === 'Escape') setOpen(false);
};
document.addEventListener('mousedown', handleMouseDown);
document.addEventListener('keydown', handleKeyDown);
return () => {
document.removeEventListener('mousedown', handleMouseDown);
document.removeEventListener('keydown', handleKeyDown);
};
}, [open]);
return (
<div ref={switcherRef} data-testid="sidebar-module-switcher" className="relative">
<div data-testid="sidebar-module-switcher" className="relative">
<button
type="button"
data-testid="sidebar-module-switcher-trigger"
aria-label={`${activeModuleLabel}`}
aria-expanded={open}
aria-controls="sidebar-module-switcher-menu"
onClick={() => setOpen((current) => !current)}
aria-label="返回首页"
title="返回首页"
onClick={() => navigate(AI_MODULE_SELECTION_PATH)}
className={cn(
'motion-press items-center bg-transparent text-left font-semibold text-foreground transition-[background-color,color,transform] hover:bg-surface-subtle active:scale-[0.985]',
compactTrigger
? 'inline-flex min-h-9 w-fit translate-y-2 justify-start gap-1 rounded-xl px-2 text-xs'
: 'flex min-h-11 w-full justify-between gap-2 rounded-2xl px-3 py-2 text-sm',
open && 'bg-surface-subtle',
? 'inline-flex min-h-9 w-fit translate-y-2 justify-start rounded-xl p-2 text-xs'
: 'flex min-h-11 w-full justify-between gap-2 rounded-2xl p-3 text-sm',
sidebarCollapsed ? 'justify-center px-0' : '',
)}
>
{!sidebarCollapsed ? <span className="min-w-0 whitespace-nowrap">{activeModuleLabel}</span> : null}
{!sidebarCollapsed ? <ChevronDown className={cn(compactTrigger ? 'h-3.5 w-3.5' : 'h-4 w-4', 'shrink-0 transition-transform duration-200 ease-out', open && 'rotate-180')} /> : null}
</button>
<DisclosureContent
open={open}
id="sidebar-module-switcher-menu"
role="menu"
data-testid="sidebar-module-switcher-menu"
className={cn(
'absolute top-[calc(100%+0.5rem)] z-50 rounded-2xl border border-border/80 bg-background p-2 text-foreground shadow-float',
sidebarCollapsed ? 'left-0 w-64' : compactTrigger ? 'right-0 min-w-56' : 'inset-x-0',
)}
innerClassName="grid gap-1.5"
>
<div className="px-2 py-1 text-[11px] font-semibold text-muted-foreground">选择模块</div>
{aiModules.map((module) => {
const active = module.id === activeModuleId;
const tone = moduleToneClasses[module.id];
const moduleLabel = getModuleLabel(module);
return (
<button
key={module.id}
type="button"
role="menuitem"
data-testid={`sidebar-module-${module.id}`}
aria-label={`${moduleLabel}${module.enabled ? '' : ',即将上线'}`}
aria-current={active ? 'page' : undefined}
disabled={!module.enabled}
onClick={() => {
setOpen(false);
if (module.route) navigate(module.route);
}}
className={cn(
'flex min-h-9 w-full items-center rounded-md border px-2 py-1.5 text-left text-xs font-semibold text-foreground transition-[background-color,color,transform,opacity] hover:bg-background active:scale-[0.985]',
active ? `border-brand/20 ${tone.active} shadow-soft` : 'border-transparent',
!module.enabled && 'cursor-not-allowed border-foreground/15 bg-surface-subtle text-muted-foreground/70 opacity-70 hover:bg-surface-subtle',
)}
>
<span className="min-w-0 flex-1 whitespace-nowrap">
<span className="block whitespace-nowrap">{moduleLabel}</span>
{!module.enabled ? <span className="mt-0.5 block text-[10px] font-medium opacity-80">{module.subtitle}</span> : null}
</span>
</button>
);
})}
</DisclosureContent>
</div>
);
}

View File

@@ -11,6 +11,7 @@ import { agentAvatarOptions } from '@/lib/agent-avatars';
import { formatModelRefLabel, type ConfiguredModelOption } from '@/lib/model-options';
import { cn } from '@/lib/utils';
import type { ProjectAgentConfig } from '../../../shared/project-config';
import { DEFAULT_PROJECT_AGENT_SKILL_IDS } from '../../../shared/opencode-skills';
export type AgentCreationSkillOption = {
id: string;
@@ -81,6 +82,7 @@ export function AgentCreationDialog({
}: AgentCreationDialogProps) {
const editing = Boolean(agent);
const [input, setInput] = useState<AgentCreationInput>(() => createInitialInput(modelOptions, agent));
const defaultSkillsAppliedRef = useRef(false);
const [submitting, setSubmitting] = useState(false);
const [skillQuery, setSkillQuery] = useState('');
const [avatarPickerOpen, setAvatarPickerOpen] = useState(false);
@@ -88,11 +90,25 @@ export function AgentCreationDialog({
const avatarUploadInputRef = useRef<HTMLInputElement | null>(null);
useEffect(() => {
if (!open) return;
if (!open) {
defaultSkillsAppliedRef.current = false;
return;
}
defaultSkillsAppliedRef.current = editing;
setInput(createInitialInput(modelOptions, agent));
setSkillQuery('');
setAvatarPickerOpen(false);
}, [agent, modelOptions, open]);
}, [agent, editing, modelOptions, open]);
useEffect(() => {
if (!open || editing || defaultSkillsAppliedRef.current) return;
const defaultSkillIds = DEFAULT_PROJECT_AGENT_SKILL_IDS.filter((skillId) => skills.some((skill) => skill.id === skillId));
if (defaultSkillIds.length === 0) return;
setInput((current) => current.skillIds.length > 0
? current
: { ...current, skillIds: [...defaultSkillIds] });
defaultSkillsAppliedRef.current = true;
}, [editing, open, skills]);
const handleAvatarFileChange = async (event: ChangeEvent<HTMLInputElement>) => {
const file = event.target.files?.[0];

View File

@@ -41,7 +41,7 @@ export const aiModules: readonly AiModuleDefinition[] = [
subtitle: 'AI 学习',
description: '用顶尖的方法解锁万物规律',
route: null,
enabled: false,
enabled: true,
Icon: Sigma,
},
{

View File

@@ -8,6 +8,18 @@ const SKILL_DISPLAY_BY_ID: Record<string, SkillDisplayInfo> = {
name: '开发浏览器',
description: '打开、查看或调试本地及公网网页,读取 Console、Network、DOM 和样式信息。',
},
'frontend-slides': {
name: '项目演示',
description: '把项目内容整理成可播放的 16:9 HTML 幻灯片,不生成 PPTX 或云端发布。',
},
grilling: {
name: '方案质询',
description: '在重要工作开始前逐项澄清目标、范围和关键取舍,确认后再执行。',
},
'planning-with-files': {
name: '项目规划',
description: '为复杂任务维护可恢复的计划、发现和进展,记录在项目的 .niancode/agent-planning/ 中。',
},
};
export function getSkillDisplayInfo(skillId: string): SkillDisplayInfo {

View File

@@ -219,6 +219,39 @@ const courseSkills: Skill[] = [
appliesTo: ['需要网页调试的项目伙伴'],
statusLabel: '平台内置 · 可手动选择',
},
{
id: 'grilling',
name: '方案质询',
description: '在重要工作开始前逐项澄清目标、范围和关键取舍,确认后再执行。',
prompt: '当任务涉及重要实现或设计决策时,先逐项确认关键选择,不要在未确认前修改项目。',
inputs: ['用户目标', '范围与约束', '验收标准'],
outputs: ['已确认的方案', '主要取舍', '剩余不确定性'],
guardrails: ['每轮只问一个问题', '先查代码事实', '确认前不执行实现'],
appliesTo: ['需要澄清复杂需求的项目伙伴'],
statusLabel: '平台内置 · 默认勾选',
},
{
id: 'planning-with-files',
name: '项目规划',
description: '为复杂任务维护可恢复的计划、发现和进展,记录在项目的 .niancode/agent-planning/ 中。',
prompt: '复杂任务开始前在 .niancode/agent-planning/ 建立并持续更新 task_plan.md、findings.md 和 progress.md。',
inputs: ['复杂任务目标', '阶段与成功标准', '代码和验证结果'],
outputs: ['可恢复的计划', '研究发现', '进展与验证记录'],
guardrails: ['简单任务不创建规划文件', '不记录密钥和敏感数据', '遵守项目 AGENTS.md'],
appliesTo: ['需要持续推进复杂任务的项目伙伴'],
statusLabel: '平台内置 · 默认勾选',
},
{
id: 'frontend-slides',
name: '项目演示',
description: '把项目内容整理成可播放的 16:9 HTML 幻灯片,不生成 PPTX 或云端发布。',
prompt: '当用户准备汇报、结题展示或项目介绍时,主动把真实项目内容整理成可播放的本地 HTML 幻灯片,并用浏览器检查布局和素材。',
inputs: ['项目目标与受众', '功能截图或素材', '想讲清楚的过程与结果'],
outputs: ['16:9 HTML 演示', '风格草图', '浏览器验收结果'],
guardrails: ['只生成本地 HTML', '不生成 PPTX', '不部署或上传到云端', '不编造项目数据'],
appliesTo: ['需要结题展示的项目伙伴'],
statusLabel: '平台内置 · 可手动选择',
},
];
const knowledgeFiles: KnowledgeFile[] = [

View File

@@ -1,4 +1,3 @@
import { ArrowRight } from 'lucide-react';
import { useNavigate } from 'react-router-dom';
import moduleCanvasImage from '@/assets/module-canvas-neon.jpg';
import moduleGameImage from '@/assets/module-game.jpg';
@@ -70,7 +69,8 @@ export function ModuleSelection({ authRequired = true }: { authRequired?: boolea
!module.enabled && 'module-option-card-disabled',
)}
>
<div className="module-option-card-surface relative z-10 flex h-full w-full overflow-hidden rounded-lg bg-background">
<span className="module-option-card-frame" aria-hidden="true" />
<div className="module-option-card-surface relative flex h-full w-full rounded-lg bg-background">
<div className="module-option-card-image relative shrink-0">
<img
src={content.image}
@@ -78,18 +78,14 @@ export function ModuleSelection({ authRequired = true }: { authRequired?: boolea
className={cn('module-option-card-image-media h-full w-full object-cover', !module.enabled && 'grayscale')}
draggable="false"
/>
{module.enabled ? (
<span className="module-option-card-arrow absolute flex items-center justify-center rounded-full bg-background/90 text-foreground shadow-sm">
<ArrowRight className="module-option-card-arrow-icon" strokeWidth={2.6} />
</span>
) : null}
</div>
<div className="module-option-card-content min-h-0 flex-1">
<div className="module-option-card-content relative min-h-0 flex-1">
<p className="module-option-card-title font-medium tracking-[-0.02em]">{content.label}</p>
<p className="module-option-card-description font-medium text-muted-foreground">{module.description}</p>
{!module.enabled ? <p className="module-option-card-status font-semibold"></p> : null}
</div>
</div>
<span className="module-option-card-hit-area" aria-hidden="true" />
</button>
);
})}

View File

@@ -87,8 +87,8 @@ function DrawerFrame({ title, desc, onClose, children, closeLabel = '关闭', cl
.from('.drawer-motion-header', { x: 8, duration: 0.22 })
.from('.drawer-motion-body > *', { y: 6, duration: 0.22, stagger: 0.035 }, '-=0.12');
}, { scope: drawerRef });
const closeButton = <Button type="button" variant="ghost" size="icon" onClick={onClose} aria-label={closeLabel} title={closeLabel}>{closeIcon === 'back' ? <ArrowLeft className="h-4 w-4" /> : <X className="h-4 w-4" />}</Button>;
return <div ref={drawerRef} className="flex min-h-0 flex-1 flex-col"><div className="glass-surface drawer-motion-header flex items-start justify-between gap-3 border-b border-border/70 bg-background/80 px-5 py-4"><div><SheetTitle className="text-lg font-semibold">{title}</SheetTitle><SheetDescription className="mt-1 text-sm font-medium text-muted-foreground">{desc}</SheetDescription></div>{closeIcon === 'back' ? closeButton : <SheetClose asChild>{closeButton}</SheetClose>}</div><div className="drawer-motion-body min-h-0 flex-1 overflow-auto p-5">{children}</div></div>;
const closeButton = <Button type="button" variant="ghost" size="icon" onClick={onClose} aria-label={closeLabel} title={closeLabel} className="relative z-10 shrink-0">{closeIcon === 'back' ? <ArrowLeft className="h-4 w-4" /> : <X className="h-4 w-4" />}</Button>;
return <div ref={drawerRef} className="flex min-h-0 flex-1 flex-col"><div className="glass-surface drawer-motion-header flex items-center justify-between gap-3 border-b border-border/70 bg-background/80 px-5 py-4"><div className="min-w-0 flex-1"><SheetTitle className="text-lg font-semibold">{title}</SheetTitle><SheetDescription className="mt-1 text-sm font-medium text-muted-foreground">{desc}</SheetDescription></div>{closeIcon === 'back' ? closeButton : <SheetClose asChild>{closeButton}</SheetClose>}</div><div className="drawer-motion-body min-h-0 flex-1 overflow-auto p-5">{children}</div></div>;
}
function SkillDetail({ skill }: { skill: SkillInfo }) {

View File

@@ -445,14 +445,23 @@ html[data-ui-language='ru'] {
}
}
/* Adapted from Uiverse.io card by Pravins01 (odd-fly-66). The card keeps the
* original pseudo-element rotation and blur animation, with Makelore colors. */
/* Adapted from Uiverse.io card by Smit-Prajapati (smart-liger-5). The
* original glass layers are softened for Makelore's horizontal module cards
* and single light visual system. */
.module-option-card {
--module-option-frame-orange: 237 114 47;
--module-option-inner-gap-y: 0.0625rem;
--module-option-inner-gap-x: 0.0625rem;
--module-option-plate-inset: 0;
--module-option-motion-duration: 280ms;
--module-option-motion-ease: cubic-bezier(0.16, 1, 0.3, 1);
position: relative;
z-index: 0;
overflow: visible;
height: clamp(7rem, 11vw, 8.5rem);
min-width: 0;
perspective: 1100px;
transform-style: preserve-3d;
}
/* The module chooser uses a single vertical rail so each horizontal card has
@@ -470,54 +479,128 @@ html[data-ui-language='ru'] {
}
.module-option-card-surface {
position: absolute;
display: flex;
min-height: 100%;
min-height: 0;
flex-direction: row;
align-items: stretch;
border: 1px solid hsl(var(--border) / 0.75);
box-shadow: 0 1px 2px hsl(220 20% 16% / 0.035), 0 12px 30px hsl(220 20% 16% / 0.045);
}
.module-option-card::before {
position: absolute;
top: -1%;
left: -1%;
z-index: -10;
content: '';
width: 102%;
height: 102%;
margin: auto;
border-radius: 10px;
background: linear-gradient(-45deg, hsl(var(--brand)) 0%, hsl(var(--brand-blue)) 100%);
overflow: visible;
pointer-events: none;
transition: transform 600ms cubic-bezier(0.175, 0.885, 0.32, 1.275);
z-index: 1;
inset: var(--module-option-plate-inset);
width: auto;
height: auto;
border: 1px solid transparent;
border-radius: calc(0.75rem - var(--module-option-inner-gap-x));
background: hsl(var(--background));
box-shadow:
0 1px 0 hsl(var(--background) / 0.96),
0 0.5rem 1rem hsl(var(--foreground) / 0.07);
-webkit-backdrop-filter: none;
backdrop-filter: none;
-webkit-backface-visibility: hidden;
backface-visibility: hidden;
will-change: transform;
transform: none;
transform-style: preserve-3d;
transition:
background 260ms ease,
border-color 220ms ease,
box-shadow 260ms ease,
-webkit-backdrop-filter 260ms ease,
backdrop-filter 260ms ease,
transform var(--module-option-motion-duration) var(--module-option-motion-ease),
opacity 260ms ease;
}
.module-option-card::after {
.module-option-card-frame {
position: absolute;
inset: 0;
z-index: -1;
content: '';
background: linear-gradient(-45deg, hsl(var(--brand)) 0%, hsl(var(--brand-blue)) 100%);
transform: translate3d(0, 0, 0) scale(0.95);
filter: blur(20px);
z-index: 0;
display: none;
border: 0;
border-radius: 0.75rem;
background: rgb(var(--module-option-frame-orange));
box-shadow:
0 1px 2px hsl(220 20% 16% / 0.035),
0 12px 30px hsl(220 20% 16% / 0.045);
pointer-events: none;
-webkit-backface-visibility: hidden;
backface-visibility: hidden;
will-change: transform;
transform-origin: center center;
transform-style: preserve-3d;
transform: none;
transition:
transform var(--module-option-motion-duration) var(--module-option-motion-ease);
}
.module-option-card-hit-area {
position: absolute;
inset: 0;
z-index: 20;
pointer-events: auto;
background: transparent;
}
.module-option-card-surface * {
pointer-events: none;
}
.module-option-card:hover::after,
.module-option-card:focus-visible::after {
filter: blur(30px);
.module-option-card:hover .module-option-card-frame,
.module-option-card:focus-visible .module-option-card-frame {
transform: rotate3d(1, 1, 0, 16deg) translate3d(0.5rem, 0.2rem, 0);
}
.module-option-card:hover .module-option-card-surface,
.module-option-card:focus-visible .module-option-card-surface {
background: linear-gradient(
140deg,
hsl(var(--background) / 0.98) 0%,
hsl(var(--background) / 0.82) 52%,
hsl(var(--background) / 0.94) 100%
);
border-color: rgb(255 255 255);
-webkit-backdrop-filter: blur(14px) saturate(1.35);
backdrop-filter: blur(14px) saturate(1.35);
box-shadow:
0 0 0 1px hsl(var(--brand) / 0.24),
0 0 1.25rem hsl(var(--brand) / 0.2),
0 0 2.5rem hsl(var(--brand) / 0.12),
0 0.9rem 1.5rem hsl(var(--foreground) / 0.1);
transform: rotate3d(1, 1, 0, 16deg) translate3d(0.25rem, -0.25rem, 28px);
}
.module-option-card-image {
width: clamp(7rem, 22%, 10rem);
height: 100%;
flex: 0 0 clamp(7rem, 22%, 10rem);
width: clamp(7.125rem, calc(26% - var(--module-option-inner-gap-x)), 10.625rem);
height: calc(100% - var(--module-option-inner-gap-y) - var(--module-option-inner-gap-y));
flex: 0 0 clamp(7.125rem, calc(26% - var(--module-option-inner-gap-x)), 10.625rem);
align-self: center;
margin: var(--module-option-inner-gap-y) 0 var(--module-option-inner-gap-y) var(--module-option-inner-gap-x);
overflow: hidden;
border-radius: 0.75rem 0 0 0.75rem;
-webkit-backface-visibility: hidden;
backface-visibility: hidden;
will-change: transform;
transform: none;
transform-style: preserve-3d;
transition: transform var(--module-option-motion-duration) var(--module-option-motion-ease);
}
.module-option-card-image-media {
transition: transform 320ms cubic-bezier(0.22, 1, 0.36, 1);
display: block;
border-radius: inherit;
-webkit-backface-visibility: hidden;
backface-visibility: hidden;
image-rendering: auto;
transform: translate3d(0, 0, 0);
will-change: transform;
transition: transform 240ms var(--module-option-motion-ease);
}
.module-option-card:hover .module-option-card-image,
.module-option-card:focus-visible .module-option-card-image {
transform: translate3d(-0.08rem, -0.08rem, 48px) scale(1.015);
}
.module-option-card-content {
@@ -528,61 +611,112 @@ html[data-ui-language='ru'] {
justify-content: center;
padding: clamp(0.75rem, 1.6vw, 1.25rem);
padding-right: clamp(2.75rem, 5vw, 4rem);
transform: none;
transform-style: preserve-3d;
transition: transform var(--module-option-motion-duration) var(--module-option-motion-ease);
}
.module-option-card-content > * {
position: relative;
transform-style: preserve-3d;
transition: transform var(--module-option-motion-duration) var(--module-option-motion-ease);
}
.module-option-card:hover .module-option-card-content,
.module-option-card:focus-visible .module-option-card-content {
transform: translate3d(0, 0, 44px);
}
.module-option-card-title {
font-size: clamp(1rem, 1.8vw, 1.35rem);
line-height: 1.25;
transform: none;
transition-delay: 0ms;
}
.module-option-card:hover .module-option-card-title,
.module-option-card:focus-visible .module-option-card-title {
transform: translate3d(0, 0, 54px);
transition-delay: 40ms;
}
.module-option-card-description {
margin-top: clamp(0.375rem, 0.7vw, 0.625rem);
font-size: clamp(0.75rem, 1.1vw, 0.95rem);
line-height: clamp(1.1rem, 1.5vw, 1.5rem);
transform: none;
transition-delay: 0ms;
}
.module-option-card:hover .module-option-card-description,
.module-option-card:focus-visible .module-option-card-description {
transform: translate3d(0, 0, 24px);
transition-delay: 60ms;
}
.module-option-card-status {
margin-top: clamp(0.375rem, 0.7vw, 0.625rem);
font-size: clamp(0.625rem, 0.75vw, 0.75rem);
line-height: 1.25;
transform: none;
transition-delay: 0ms;
}
.module-option-card-arrow {
top: clamp(0.5rem, 1vw, 0.75rem);
right: clamp(0.5rem, 1vw, 0.75rem);
width: clamp(1.75rem, 2.8vw, 2rem);
height: clamp(1.75rem, 2.8vw, 2rem);
}
.module-option-card-arrow-icon {
width: clamp(1rem, 1.8vw, 1.25rem);
height: clamp(1rem, 1.8vw, 1.25rem);
.module-option-card:hover .module-option-card-status,
.module-option-card:focus-visible .module-option-card-status {
transform: translate3d(0, 0, 20px);
transition-delay: 80ms;
}
.module-option-card.module-option-card-disabled::before,
.module-option-card.module-option-card-disabled::after {
background: hsl(var(--muted-foreground) / 0.28);
filter: none;
display: none;
}
.module-option-card.module-option-card-disabled:hover::before,
.module-option-card.module-option-card-disabled:focus-visible::before {
.module-option-card.module-option-card-disabled:hover .module-option-card-surface,
.module-option-card.module-option-card-disabled:focus-visible .module-option-card-surface {
transform: none;
}
.module-option-card.module-option-card-disabled:hover::after,
.module-option-card.module-option-card-disabled:focus-visible::after {
filter: none;
.module-option-card.module-option-card-disabled:hover,
.module-option-card.module-option-card-disabled:focus-visible {
box-shadow:
0 1px 2px hsl(220 20% 16% / 0.035),
0 12px 30px hsl(220 20% 16% / 0.045);
transform: none;
}
.module-option-card:hover::before,
.module-option-card:focus-visible::before {
transform: rotate(-1deg) scaleX(1.01) scaleY(1.04);
.module-option-card.module-option-card-disabled .module-option-card-frame {
border: 0;
background: linear-gradient(135deg, hsl(var(--brand) / 0.18) 0%, hsl(var(--brand-soft) / 0.34) 100%);
box-shadow:
0 1px 2px hsl(220 20% 16% / 0.035),
0 12px 30px hsl(220 20% 16% / 0.045);
}
.module-option-card.module-option-card-disabled:hover .module-option-card-frame,
.module-option-card.module-option-card-disabled:focus-visible .module-option-card-frame {
transform: none;
}
.module-option-card.module-option-card-disabled:hover .module-option-card-image,
.module-option-card.module-option-card-disabled:focus-visible .module-option-card-image,
.module-option-card.module-option-card-disabled:hover .module-option-card-content,
.module-option-card.module-option-card-disabled:focus-visible .module-option-card-content {
transform: none;
}
.module-option-card.module-option-card-disabled .module-option-card-content > * {
transform: none;
}
.module-option-card.module-option-card-disabled .module-option-card-surface {
background: transparent;
}
.module-option-card:hover .module-option-card-image-media,
.module-option-card:focus-visible .module-option-card-image-media {
transform: scale(1.04);
transform: scale(1.015);
}
.module-option-card:disabled:hover .module-option-card-image-media,
@@ -751,13 +885,28 @@ html[data-ui-language='ru'] {
transform: none;
}
.module-option-card::before {
.module-option-card:hover .module-option-card-frame,
.module-option-card:focus-visible .module-option-card-frame,
.module-option-card:hover .module-option-card-surface,
.module-option-card:focus-visible .module-option-card-surface {
transform: none;
}
.module-option-card:hover::after,
.module-option-card:focus-visible::after {
filter: blur(20px);
.module-option-card:hover .module-option-card-image,
.module-option-card:focus-visible .module-option-card-image,
.module-option-card:hover .module-option-card-content,
.module-option-card:focus-visible .module-option-card-content {
transform: none;
}
.module-option-card:hover .module-option-card-content > *,
.module-option-card:focus-visible .module-option-card-content > * {
transform: none;
}
.module-option-card:hover .module-option-card-image-media,
.module-option-card:focus-visible .module-option-card-image-media {
transform: none;
}
.brand-spark {