Commit a8687e0a by luoqi

feat(web): PAC 主色换成 PANTONE 286 C(#0032A0),teal-* 整体改名 brand-*

## 换色
globals.css 里新增品牌色阶 brand-50…900(同色相 H=221.25 推出),
主色 #0032A0 坐 600 档 —— 全站 `bg-brand-600` 是主按钮、`--primary` 也指这档,
主色必须落这里才算真换了。--primary / --ring 同步改成 hsl(221.25 100% 31.4%)。
实测 `bg-brand-600` 计算值 rgb(0,50,160),与色卡 RGB 0/50/160 一致。

## 为什么顺手改名
没有走"偷偷把 Tailwind 的 teal 调色板改成蓝色"这条捷径 —— 那样类名写着 teal、
渲染出来是蓝的,下一个人读代码会以为自己看错了。品牌色就该叫 brand:
- 263 处 `teal-N` → `brand-N`(28 个文件,纯类名替换)
- tone 色板键 'teal' → 'brand'(labels.ts 的 PERSONA_FEATURE_META 一并改;
  tone 值全在代码里,不来自 DB,改名没有兼容问题)
- 宠物 SVG 里手挑的 3 个十六进制(11 处)按色阶位置 1:1 映射过去,不然吉祥物
  还是青色、跟全站撞色

以后换色只改 globals.css 那 10 行。

web typecheck + 657 tests / 42 suites 通过;本地实测列表/详情/表单全站已是蓝。

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
parent 93b5ccab
Pipeline #3478 failed in 0 seconds
......@@ -92,7 +92,7 @@ function PlanDetailLoader({ planId }: { planId: string }) {
</div>
<div className="absolute inset-0 z-10 flex items-center justify-center bg-white/30">
<div className="flex items-center gap-2 rounded-full bg-white px-3 py-1.5 text-[12px] text-slate-500 shadow-md ring-1 ring-slate-200">
<span className="h-3.5 w-3.5 animate-spin rounded-full border-2 border-teal-500 border-t-transparent" />
<span className="h-3.5 w-3.5 animate-spin rounded-full border-2 border-brand-500 border-t-transparent" />
切换中…
</div>
</div>
......
......@@ -59,7 +59,7 @@ export default function PlansLayout({ children }: { children: React.ReactNode })
<button
type="button"
onClick={() => setRailOpen(true)}
className="fixed left-0 top-1/2 z-50 -translate-y-1/2 rounded-r-lg bg-teal-600 px-1.5 py-3 text-white shadow-lg hover:bg-teal-700 lg:hidden"
className="fixed left-0 top-1/2 z-50 -translate-y-1/2 rounded-r-lg bg-brand-600 px-1.5 py-3 text-white shadow-lg hover:bg-brand-700 lg:hidden"
title="打开患者列表"
>
<span className="flex flex-col items-center gap-1">
......
......@@ -62,7 +62,7 @@ function EntryResolver() {
if (phase === 'resolving') {
return (
<div className="flex h-screen items-center justify-center gap-2 text-sm text-muted-foreground">
<span className="h-4 w-4 animate-spin rounded-full border-2 border-teal-500 border-t-transparent" />
<span className="h-4 w-4 animate-spin rounded-full border-2 border-brand-500 border-t-transparent" />
正在进入召回工作台…
</div>
);
......@@ -74,7 +74,7 @@ function EntryResolver() {
{/* 空态 header —— 内容只有 PAC 品牌 + 用户三件套,无功能项;嵌入宿主时整体隐藏 */}
{!isEmbedded() && (
<header className="flex h-12 flex-none items-center gap-2 border-b border-slate-100 bg-white px-4">
<span className="inline-flex h-7 w-7 items-center justify-center rounded-md bg-teal-600 text-[12px] font-bold text-white">
<span className="inline-flex h-7 w-7 items-center justify-center rounded-md bg-brand-600 text-[12px] font-bold text-white">
PAC
</span>
<h1 className="text-[14px] font-semibold text-slate-900">召回工作台</h1>
......
......@@ -4,7 +4,7 @@
* PAC Design Tokens(design-system.md §2-§4)
*
* 沿用 shadcn 的 HSL var 模式(方便组件级覆盖),但:
* - primary 改成 teal-600(PAC 品牌)
* - primary 改成 brand-600(PAC 品牌 = PANTONE 286 C / #0032A0)
* - border 加深(default shadcn 91.4% 视觉太弱)
* - 阴影整体放轻(Claude / OpenAI 现代风:border 主导,shadow 仅用于"浮起"场景)
* - 字体优先 PingFang(医疗 SaaS 中文场景)
......@@ -27,7 +27,7 @@
--accent-foreground: 222.2 47.4% 11.2%;
/* === PAC 品牌色 === */
--primary: 173 80% 36%; /* teal-600 */
--primary: 221.25 100% 31.4%; /* brand-600 = #0032A0(PANTONE 286 C) */
--primary-foreground: 0 0% 100%;
--secondary: 210 40% 96.1%;
--secondary-foreground: 222.2 47.4% 11.2%;
......@@ -37,13 +37,37 @@
/* === 边框 — 加深(91.4% → 87%,1px 视觉更扎实)=== */
--border: 214 24% 87%;
--input: 214 28% 82%; /* 输入框边再深一档,跟 hover 状态拉开 */
--ring: 173 80% 36%; /* focus ring 与 primary 同色 */
--ring: 221.25 100% 31.4%; /* focus ring 与 primary 同色 */
--radius: 0.5rem;
}
}
@theme inline {
/* ⭐ === PAC 品牌色阶 brand-50…900 ===
*
* 主色 = **PANTONE 286 C / #0032A0**(RGB 0/50/160),坐在 600 档 —— 因为
* 全站 `bg-brand-600` 是主按钮、`--primary` 也指这一档,主色必须落在这里才算"改了主色"。
* 其余档由同色相(H=221.25)调明度/饱和推出:浅档做底纹(brand-50 是最常用的选中/命中底),
* 深档做正文链接(brand-700 用得最多)。
*
* ⚠️ 类名从 `teal-*` 整体改名成 `brand-*`(263 处)—— 不是把 Tailwind 的 teal 调色板
* 偷偷改成蓝的:那样类名写着 teal 渲染出蓝,下一个人读代码会以为自己看错了。
* 品牌色就该叫 brand,换色只改这一段。
*
* 换色只需改下面 10 行;别在组件里写死十六进制(宠物 SVG 是唯一例外,见 pet-body.tsx)。
*/
--color-brand-50: #F0F4FF;
--color-brand-100: #DCE7FE;
--color-brand-200: #BACFFD;
--color-brand-300: #85A9FA;
--color-brand-400: #3B76F7;
--color-brand-500: #0043D6;
--color-brand-600: #0032A0; /* ← 主色 PANTONE 286 C */
--color-brand-700: #002985;
--color-brand-800: #01226A;
--color-brand-900: #031C53;
/* === 颜色 token → 自动生成 bg-{name} text-{name} border-{name} === */
--color-background: hsl(var(--background));
--color-foreground: hsl(var(--foreground));
......
......@@ -121,7 +121,7 @@ export default function PetLabPage() {
{/* 栖息台:离地 ~120px,在可跳高度内(data-pet-perch 机制演示) */}
<div
data-pet-perch
className="fixed bottom-[120px] left-1/2 h-3 w-56 -translate-x-1/2 rounded-full bg-teal-100 shadow-inner"
className="fixed bottom-[120px] left-1/2 h-3 w-56 -translate-x-1/2 rounded-full bg-brand-100 shadow-inner"
title="栖息台(data-pet-perch)"
/>
......
......@@ -80,13 +80,13 @@ function Markdown({ text }: { text: string }) {
li: ({ children }) => <li className="leading-relaxed">{children}</li>,
strong: ({ children }) => <strong className="font-semibold text-slate-900">{children}</strong>,
a: ({ children, href }) => (
<a href={href} className="text-teal-700 underline" target="_blank" rel="noreferrer">
<a href={href} className="text-brand-700 underline" target="_blank" rel="noreferrer">
{children}
</a>
),
hr: () => <hr className="my-3 border-slate-100" />,
blockquote: ({ children }) => (
<blockquote className="my-2 border-l-2 border-teal-300 bg-teal-50/40 py-1 pl-3 text-slate-600">
<blockquote className="my-2 border-l-2 border-brand-300 bg-brand-50/40 py-1 pl-3 text-slate-600">
{children}
</blockquote>
),
......@@ -265,7 +265,7 @@ function ArtifactView({ artifact }: { artifact: Artifact }) {
return (
<div className="overflow-hidden rounded-lg border border-slate-100 bg-white shadow-sm">
<div className="flex items-center gap-1.5 border-b border-slate-100 bg-slate-50/60 px-3 py-1.5">
<LayoutTemplate className="h-3.5 w-3.5 text-teal-600" />
<LayoutTemplate className="h-3.5 w-3.5 text-brand-600" />
<span className="text-[11.5px] font-medium text-slate-500">{artifact.title || '可视化卡片'}</span>
<button
type="button"
......@@ -288,10 +288,10 @@ function ArtifactView({ artifact }: { artifact: Artifact }) {
<div className="absolute inset-0 bg-black/40" onClick={() => setExpanded(false)} />
<div className="relative z-10 flex h-[min(86vh,920px)] w-[min(980px,94vw)] flex-col overflow-hidden rounded-xl border border-slate-100 bg-white shadow-2xl">
<div className="flex flex-none items-center gap-1.5 border-b border-slate-100 bg-slate-50/60 px-3 py-2">
<LayoutTemplate className="h-4 w-4 text-teal-600" />
<LayoutTemplate className="h-4 w-4 text-brand-600" />
<span className="text-[12.5px] font-medium text-slate-600">{artifact.title || '可视化卡片'}</span>
{artifact.streaming && (
<span className="ml-1 inline-flex items-center gap-1 text-[10.5px] text-teal-600">
<span className="ml-1 inline-flex items-center gap-1 text-[10.5px] text-brand-600">
<Loader2 className="h-3 w-3 animate-spin" />
生成中
</span>
......@@ -327,7 +327,7 @@ function ArtifactView({ artifact }: { artifact: Artifact }) {
function GeneratingHint({ label = '生成中' }: { label?: string }) {
return (
<div className="flex items-center gap-1.5 text-[12px] text-slate-400">
<Loader2 className="h-3.5 w-3.5 animate-spin text-teal-500" />
<Loader2 className="h-3.5 w-3.5 animate-spin text-brand-500" />
<span>{label}</span>
</div>
);
......@@ -351,7 +351,7 @@ function MessageView({ message, streaming }: { message: ChatMessage; streaming?:
last && last.kind === 'tool' && last.step.status === 'running' ? toolMeta(last.step.tool).running : '生成中';
return (
<div className="flex gap-2.5">
<span className="mt-0.5 inline-flex h-7 w-7 flex-none items-center justify-center rounded-full bg-teal-600 text-white">
<span className="mt-0.5 inline-flex h-7 w-7 flex-none items-center justify-center rounded-full bg-brand-600 text-white">
<Bot className="h-4 w-4" />
</span>
<div className="min-w-0 flex-1 space-y-2.5 pt-0.5">
......@@ -407,7 +407,7 @@ function ModelSelect({
}}
className={cn(
'flex w-full items-center gap-2 px-3 py-1.5 text-left text-[12.5px] hover:bg-slate-50',
m.value === model ? 'font-medium text-teal-700' : 'text-slate-700',
m.value === model ? 'font-medium text-brand-700' : 'text-slate-700',
)}
>
{m.value === model ? <Check className="h-3.5 w-3.5" /> : <span className="w-3.5" />}
......@@ -577,7 +577,7 @@ export function AssistantChat({
>
<span
className={cn(
'inline-flex flex-none items-center justify-center rounded-lg bg-teal-600 text-white',
'inline-flex flex-none items-center justify-center rounded-lg bg-brand-600 text-white',
isWidget ? 'h-6 w-6' : 'h-7 w-7',
)}
>
......@@ -617,7 +617,7 @@ export function AssistantChat({
<div className="mx-auto max-w-3xl space-y-5 px-4 py-5">
{messages.length === 0 ? (
<div className={cn('flex flex-col items-center justify-center gap-4 text-center', isWidget ? 'py-8' : 'py-20')}>
<span className="inline-flex h-12 w-12 items-center justify-center rounded-2xl bg-teal-600 text-white">
<span className="inline-flex h-12 w-12 items-center justify-center rounded-2xl bg-brand-600 text-white">
<Bot className="h-6 w-6" />
</span>
<div className="max-w-md text-[13.5px] text-slate-500">
......@@ -629,7 +629,7 @@ export function AssistantChat({
key={ex}
type="button"
onClick={() => fire(ex)}
className="rounded-full border border-slate-100 bg-white px-3 py-1.5 text-[11.5px] text-slate-600 transition-colors hover:border-teal-300 hover:bg-teal-50/50 hover:text-teal-700"
className="rounded-full border border-slate-100 bg-white px-3 py-1.5 text-[11.5px] text-slate-600 transition-colors hover:border-brand-300 hover:bg-brand-50/50 hover:text-brand-700"
>
{ex}
</button>
......@@ -651,7 +651,7 @@ export function AssistantChat({
{/* Composer */}
<div className="flex-none border-t border-slate-100 bg-white">
<div className="mx-auto max-w-3xl px-4 py-3">
<div className="flex items-end gap-2 rounded-xl border border-slate-100 bg-white px-3 py-2 transition-all focus-within:border-teal-300 focus-within:ring-2 focus-within:ring-teal-200">
<div className="flex items-end gap-2 rounded-xl border border-slate-100 bg-white px-3 py-2 transition-all focus-within:border-brand-300 focus-within:ring-2 focus-within:ring-brand-200">
<textarea
ref={taRef}
rows={1}
......@@ -679,7 +679,7 @@ export function AssistantChat({
'mb-0.5 inline-flex h-8 w-8 flex-none items-center justify-center rounded-lg transition-colors',
voice === 'live'
? 'animate-pulse bg-rose-100 text-rose-600 ring-1 ring-inset ring-rose-300'
: 'bg-slate-100 text-slate-500 hover:bg-teal-50 hover:text-teal-700',
: 'bg-slate-100 text-slate-500 hover:bg-brand-50 hover:text-brand-700',
)}
>
<Mic className="h-4 w-4" />
......@@ -702,7 +702,7 @@ export function AssistantChat({
title="发送"
className={cn(
'mb-0.5 inline-flex h-8 w-8 flex-none items-center justify-center rounded-lg transition-colors',
input.trim() ? 'bg-teal-600 text-white hover:bg-teal-700' : 'bg-slate-100 text-slate-400',
input.trim() ? 'bg-brand-600 text-white hover:bg-brand-700' : 'bg-slate-100 text-slate-400',
)}
>
<Send className="h-4 w-4" />
......
......@@ -23,7 +23,7 @@ export function IdentityCluster() {
{visibleClinics(user).length} 个诊所 · {roleNameZh(user?.role)}
</div>
</div>
<span className="inline-flex h-8 w-8 flex-none items-center justify-center rounded-full bg-gradient-to-br from-teal-400 to-teal-600 text-[12px] font-bold text-white">
<span className="inline-flex h-8 w-8 flex-none items-center justify-center rounded-full bg-gradient-to-br from-brand-400 to-brand-600 text-[12px] font-bold text-white">
{name.charAt(0).toUpperCase()}
</span>
<button
......
......@@ -35,7 +35,7 @@ import { cn } from '@/lib/utils';
type BrandSlug = 'ruier' | 'ruitai';
const TENANTS: { slug: BrandSlug; nameZh: string; tone: string }[] = [
{ slug: 'ruier', nameZh: '瑞尔', tone: 'border-teal-200 bg-teal-50/40' },
{ slug: 'ruier', nameZh: '瑞尔', tone: 'border-brand-200 bg-brand-50/40' },
{ slug: 'ruitai', nameZh: '瑞泰', tone: 'border-sky-200 bg-sky-50/40' },
];
......@@ -161,7 +161,7 @@ export function MockLoginDialog({ open }: { open: boolean }) {
>
<DialogHeader>
<DialogTitle className="flex items-center gap-2">
<span className="inline-flex h-7 w-7 items-center justify-center rounded-md bg-teal-600 text-[12px] font-bold text-white">
<span className="inline-flex h-7 w-7 items-center justify-center rounded-md bg-brand-600 text-[12px] font-bold text-white">
PAC
</span>
<span>快速登录</span>
......@@ -192,7 +192,7 @@ export function MockLoginDialog({ open }: { open: boolean }) {
className={cn(
'flex flex-col items-start rounded-t-md px-3 py-1.5 text-left transition-colors',
hostTab === t.key
? 'border-b-2 border-teal-600 font-semibold text-teal-700'
? 'border-b-2 border-brand-600 font-semibold text-brand-700'
: 'text-slate-500 hover:text-slate-800',
)}
>
......@@ -228,8 +228,8 @@ export function MockLoginDialog({ open }: { open: boolean }) {
className={cn(
'flex flex-col items-start gap-0.5 rounded-md border px-2.5 py-1.5 text-left transition-all',
role === r.key
? 'border-teal-400 bg-teal-50/60 ring-1 ring-teal-200'
: 'border-slate-100 hover:border-teal-300',
? 'border-brand-400 bg-brand-50/60 ring-1 ring-brand-200'
: 'border-slate-100 hover:border-brand-300',
)}
>
<span className="text-[12.5px] font-semibold text-slate-800">{r.nameZh}</span>
......@@ -270,8 +270,8 @@ export function MockLoginDialog({ open }: { open: boolean }) {
className={cn(
'rounded-full border px-2.5 py-0.5 text-[11px] transition-colors',
tenantFilter === t.k
? 'border-teal-400 bg-teal-50 text-teal-700'
: 'border-slate-100 text-slate-500 hover:border-teal-300',
? 'border-brand-400 bg-brand-50 text-brand-700'
: 'border-slate-100 text-slate-500 hover:border-brand-300',
)}
>
{t.n}
......@@ -281,7 +281,7 @@ export function MockLoginDialog({ open }: { open: boolean }) {
value={query}
onChange={(e) => setQuery(e.target.value)}
placeholder="搜客服姓名…"
className="ml-auto w-40 rounded-md border border-slate-100 px-2.5 py-1 text-[12px] outline-none focus:border-teal-400"
className="ml-auto w-40 rounded-md border border-slate-100 px-2.5 py-1 text-[12px] outline-none focus:border-brand-400"
/>
</div>
......@@ -300,8 +300,8 @@ export function MockLoginDialog({ open }: { open: boolean }) {
className={cn(
'flex w-full items-center justify-between gap-2 rounded-md border px-2.5 py-1.5 text-left transition-all',
loading
? 'border-teal-400 ring-2 ring-teal-200'
: 'border-slate-100 hover:border-teal-400 hover:bg-teal-50/40',
? 'border-brand-400 ring-2 ring-brand-200'
: 'border-slate-100 hover:border-brand-400 hover:bg-brand-50/40',
busy && !loading && 'opacity-50',
)}
>
......@@ -347,8 +347,8 @@ export function MockLoginDialog({ open }: { open: boolean }) {
className={cn(
'group flex flex-col items-start gap-1 rounded-md border bg-white px-3 py-2 text-left transition-all',
loading
? 'border-teal-400 ring-2 ring-teal-200'
: 'border-slate-100 hover:border-teal-400 hover:bg-teal-50/40',
? 'border-brand-400 ring-2 ring-brand-200'
: 'border-slate-100 hover:border-brand-400 hover:bg-brand-50/40',
busy && !loading && 'opacity-50',
)}
>
......@@ -429,8 +429,8 @@ function FridayPanel({
className={cn(
'flex flex-col items-start gap-0.5 rounded-md border px-2.5 py-1.5 text-left transition-all',
role === r.key
? 'border-teal-400 bg-teal-50/60 ring-1 ring-teal-200'
: 'border-slate-100 hover:border-teal-300',
? 'border-brand-400 bg-brand-50/60 ring-1 ring-brand-200'
: 'border-slate-100 hover:border-brand-300',
)}
>
<span className="text-[12.5px] font-semibold text-slate-800">{r.nameZh}</span>
......@@ -465,7 +465,7 @@ function FridayPanel({
value={query}
onChange={(e) => setQuery(e.target.value)}
placeholder="搜品牌 / 诊所名…"
className="mb-2 h-7 w-full rounded-md border border-slate-100 bg-slate-50 px-2 text-[12px] outline-none placeholder:text-slate-400 focus:border-teal-300 focus:bg-white"
className="mb-2 h-7 w-full rounded-md border border-slate-100 bg-slate-50 px-2 text-[12px] outline-none placeholder:text-slate-400 focus:border-brand-300 focus:bg-white"
/>
<div className="max-h-[280px] space-y-2 overflow-y-auto">
{filtered.length === 0 && (
......@@ -480,7 +480,7 @@ function FridayPanel({
type="button"
onClick={() => onPick(role, b.brandId)}
disabled={!!busy}
className="flex w-full items-center justify-between gap-2 rounded-t-md bg-slate-50/60 px-2.5 py-1.5 text-left transition-colors hover:bg-teal-50/60 disabled:opacity-50"
className="flex w-full items-center justify-between gap-2 rounded-t-md bg-slate-50/60 px-2.5 py-1.5 text-left transition-colors hover:bg-brand-50/60 disabled:opacity-50"
>
<span className="truncate text-[12.5px] font-semibold text-slate-800">{b.name}</span>
<span className="flex-none text-[10.5px] text-slate-500">
......@@ -495,7 +495,7 @@ function FridayPanel({
type="button"
onClick={() => onPick(role, c.clinicId)}
disabled={!!busy}
className="rounded-full border border-slate-100 px-2 py-0.5 text-[11px] text-slate-600 transition-colors hover:border-teal-300 hover:bg-teal-50 hover:text-teal-700 disabled:opacity-50"
className="rounded-full border border-slate-100 px-2 py-0.5 text-[11px] text-slate-600 transition-colors hover:border-brand-300 hover:bg-brand-50 hover:text-brand-700 disabled:opacity-50"
>
{c.name}
</button>
......
......@@ -354,9 +354,9 @@ export function PetBody({
)}
{vetFluoride && (
<>
<line x1="45" y1="51" x2="37" y2="41" stroke="#0d9488" strokeWidth="2" strokeLinecap="round" />
<ellipse className="pac-vet-buzz" cx="35" cy="39" rx="2.4" ry="1.6" fill="#5eead4" stroke="#0d9488" strokeWidth="0.6" />
<g fill="#99f6e4" stroke="#5eead4" strokeWidth="0.4">
<line x1="45" y1="51" x2="37" y2="41" stroke="#0032A0" strokeWidth="2" strokeLinecap="round" />
<ellipse className="pac-vet-buzz" cx="35" cy="39" rx="2.4" ry="1.6" fill="#85A9FA" stroke="#0032A0" strokeWidth="0.6" />
<g fill="#BACFFD" stroke="#85A9FA" strokeWidth="0.4">
<circle className="pac-pet-rinse-b" cx="33" cy="36" r="1.1" />
<circle className="pac-pet-rinse-b" cx="37" cy="35" r="1.4" style={{ animationDelay: '0.5s' }} />
</g>
......@@ -366,7 +366,7 @@ export function PetBody({
)}
{/* 思考:头顶冒点点 */}
{thinking && (
<g fill="#0d9488">
<g fill="#0032A0">
<circle className="pac-pet-dot" cx="46" cy="12" r="2" style={{ animationDelay: '0s' }} />
<circle className="pac-pet-dot" cx="52" cy="8" r="2.6" style={{ animationDelay: '0.2s' }} />
<circle className="pac-pet-dot" cx="58" cy="3.5" r="3.2" style={{ animationDelay: '0.4s' }} />
......@@ -377,7 +377,7 @@ export function PetBody({
<>
<g className="pac-pet-scrub">
{/* 刷柄(品牌青) + 刷毛 */}
<rect x="22" y="50" width="20" height="3.6" rx="1.8" fill="#0d9488" transform="rotate(-8 32 52)" />
<rect x="22" y="50" width="20" height="3.6" rx="1.8" fill="#0032A0" transform="rotate(-8 32 52)" />
<rect x="38" y="45.5" width="7" height="6" rx="1.2" fill="#ffffff" stroke="#e2e8f0" strokeWidth="0.8" transform="rotate(-8 41 48)" />
</g>
<g fill="#a5f3fc" stroke="#67e8f9" strokeWidth="0.5">
......@@ -390,9 +390,9 @@ export function PetBody({
{/* 牙线自理:头顶拉一根牙线锯冠缝 */}
{flossing && (
<g className="pac-pet-floss">
<path d="M8 13 Q32 22 56 13" stroke="#5eead4" strokeWidth="1.6" fill="none" strokeLinecap="round" />
<circle cx="8" cy="13" r="2.2" fill="#0d9488" />
<circle cx="56" cy="13" r="2.2" fill="#0d9488" />
<path d="M8 13 Q32 22 56 13" stroke="#85A9FA" strokeWidth="1.6" fill="none" strokeLinecap="round" />
<circle cx="8" cy="13" r="2.2" fill="#0032A0" />
<circle cx="56" cy="13" r="2.2" fill="#0032A0" />
</g>
)}
{/* 闪亮微笑:牙面 ding 两颗星 */}
......@@ -428,7 +428,7 @@ export function PetBody({
)}
{/* 睡觉:Zzz */}
{sleeping && (
<g fill="#0d9488" fontWeight={700} fontFamily="ui-sans-serif">
<g fill="#0032A0" fontWeight={700} fontFamily="ui-sans-serif">
<text className="pac-pet-z" x="46" y="14" fontSize="9" style={{ animationDelay: '0s' }}>z</text>
<text className="pac-pet-z" x="52" y="8" fontSize="12" style={{ animationDelay: '0.6s' }}>Z</text>
</g>
......
......@@ -536,7 +536,7 @@ function ChainListItem({
const activeRing: Record<string, string> = {
rose: 'border-rose-400 bg-rose-50 ring-1 ring-rose-200',
amber: 'border-amber-400 bg-amber-50 ring-1 ring-amber-200',
sky: 'border-teal-400 bg-teal-50 ring-1 ring-teal-200',
sky: 'border-brand-400 bg-brand-50 ring-1 ring-brand-200',
emerald: 'border-emerald-400 bg-emerald-50 ring-1 ring-emerald-200',
slate: 'border-slate-300 bg-slate-50 ring-1 ring-slate-200',
};
......
......@@ -85,14 +85,14 @@ export function CloseOpportunityDialog({
'flex items-center gap-2 rounded-md border px-3 py-2.5 text-left text-[12.5px] transition-colors',
r.key === 'other' && 'col-span-2',
active
? 'border-teal-400 bg-teal-50 font-medium text-teal-800'
: 'border-slate-200 bg-white text-slate-700 hover:border-teal-300 hover:bg-teal-50/50',
? 'border-brand-400 bg-brand-50 font-medium text-brand-800'
: 'border-slate-200 bg-white text-slate-700 hover:border-brand-300 hover:bg-brand-50/50',
)}
>
<span
className={cn(
'flex h-3.5 w-3.5 flex-none items-center justify-center rounded-[3px] border',
active ? 'border-teal-600 bg-teal-600' : 'border-slate-300',
active ? 'border-brand-600 bg-brand-600' : 'border-slate-300',
)}
>
{active && (
......@@ -124,14 +124,14 @@ export function CloseOpportunityDialog({
onChange={(e) => setNote(e.target.value)}
rows={3}
placeholder="请详细描述关闭机会的具体原因,以便后续分析和改进…"
className="w-full resize-y rounded-md border border-slate-200 bg-white px-3 py-2 text-[12.5px] text-slate-900 placeholder:text-slate-400 focus:outline-none focus:ring-2 focus:ring-teal-200 focus:border-teal-400"
className="w-full resize-y rounded-md border border-slate-200 bg-white px-3 py-2 text-[12.5px] text-slate-900 placeholder:text-slate-400 focus:outline-none focus:ring-2 focus:ring-brand-200 focus:border-brand-400"
/>
</div>
)}
{selected.length > 0 && (
<div className="rounded-md border border-teal-100 bg-teal-50/60 px-3 py-2">
<div className="text-[11.5px] font-semibold text-teal-800">
<div className="rounded-md border border-brand-100 bg-brand-50/60 px-3 py-2">
<div className="text-[11.5px] font-semibold text-brand-800">
已选关闭原因 · {selected.length}
</div>
<ul className="mt-0.5 space-y-0.5">
......
......@@ -339,14 +339,14 @@ function ReturnVisitsList({ visits }: { visits: ReturnVisitItem[] }) {
key={i}
className={cn(
'flex items-start gap-2.5 text-[12.5px] py-1 border-b border-slate-50 last:border-0',
latest && 'bg-teal-50/50 -mx-2 px-2 rounded',
latest && 'bg-brand-50/50 -mx-2 px-2 rounded',
)}
>
<span className={cn('flex-none mt-[5px] w-1.5 h-1.5 rounded-full', done ? 'bg-teal-400' : 'bg-slate-300')} />
<span className={cn('flex-none mt-[5px] w-1.5 h-1.5 rounded-full', done ? 'bg-brand-400' : 'bg-slate-300')} />
<div className="flex-1 min-w-0">
<div className="flex items-center gap-1.5 flex-wrap">
{latest && (
<span className="flex-none rounded bg-teal-600 px-1 py-px text-[10px] font-medium text-white">
<span className="flex-none rounded bg-brand-600 px-1 py-px text-[10px] font-medium text-white">
最近一次
</span>
)}
......@@ -356,7 +356,7 @@ function ReturnVisitsList({ visits }: { visits: ReturnVisitItem[] }) {
<span className="font-medium text-slate-700">{v.type ?? '回访'}</span>
{v.status && <span className="text-slate-400">· {v.status}</span>}
{v.taskDate && <span className="text-slate-400 tabular-nums">{v.taskDate}</span>}
{v.treatmentItems && <span className="text-teal-700/70">· {v.treatmentItems}</span>}
{v.treatmentItems && <span className="text-brand-700/70">· {v.treatmentItems}</span>}
</div>
{v.followContent && (
<div className="text-slate-600 leading-snug break-words mt-0.5">{v.followContent}</div>
......
......@@ -324,7 +324,7 @@ function EmrSection({
const tooth = String(tc.tooth_position ?? '');
return (
<li key={tx.id} className="text-[12px] text-slate-700 leading-relaxed">
<span className="px-1.5 py-px mr-1.5 bg-teal-50 text-teal-700 rounded text-[10.5px] font-medium">
<span className="px-1.5 py-px mr-1.5 bg-brand-50 text-brand-700 rounded text-[10.5px] font-medium">
{badgeText}
</span>
<span className="font-medium">{subtype}</span>
......
......@@ -47,7 +47,7 @@ export function factLabel(f: AdaptedFact): FactLabel {
const isPlanned = f.kind === 'planned';
return {
badge: isPlanned ? '计划' : catZh || '治疗',
badgeTone: isPlanned ? 'bg-slate-100 text-slate-500' : 'bg-teal-50 text-teal-700',
badgeTone: isPlanned ? 'bg-slate-100 text-slate-500' : 'bg-brand-50 text-brand-700',
text,
};
}
......@@ -95,7 +95,7 @@ export function factLabel(f: AdaptedFact): FactLabel {
// ⚠️ 下面三类的 f.title 都是 `<中文> <externalId>`(接诊 16432552 / 挂号 xxx / 前台接待 xxx)——
// 直接显示等于把宿主内部 ID 甩给客服。一律改取 content 里有语义的字段,取不到就只留中文词。
case 'encounter_record': {
const tone = { badge: '就诊', badgeTone: 'bg-teal-50 text-teal-700' };
const tone = { badge: '就诊', badgeTone: 'bg-brand-50 text-brand-700' };
const cc = String(c.chief_complaint ?? '').trim() || String(f.summary ?? '').trim();
const doc = String(c.doctor_name ?? '').trim();
if (cc) return { ...tone, text: cc, hover: doc ? `${cc}(${doc}医生)` : cc };
......@@ -105,10 +105,10 @@ export function factLabel(f: AdaptedFact): FactLabel {
const doc = String(c.doctor_name ?? '').trim();
const dept = String(c.department ?? c.department_name ?? '').trim();
const text = [dept, doc ? `${doc}医生` : ''].filter(Boolean).join(' · ') || '挂号';
return { badge: '挂号', badgeTone: 'bg-teal-50 text-teal-700', text };
return { badge: '挂号', badgeTone: 'bg-brand-50 text-brand-700', text };
}
case 'visit_reception_record':
return { badge: '接待', badgeTone: 'bg-teal-50 text-teal-700', text: '前台接待' };
return { badge: '接待', badgeTone: 'bg-brand-50 text-brand-700', text: '前台接待' };
case 'emr_record': {
const tone = { badge: '病历', badgeTone: 'bg-slate-100 text-slate-600' };
// ⚠️ **不能用 f.summary** —— emr parser 写进去的是 `关联接诊:<encounter_id>`(内部 ID),
......
......@@ -536,7 +536,7 @@ const TONE_FALLBACK: ToneCls = { bg: 'bg-slate-100', text: 'text-slate-700', bor
const TONE = {
rose: { bg: 'bg-rose-100', text: 'text-rose-700', border: 'border-rose-200' },
teal: { bg: 'bg-teal-100', text: 'text-teal-700', border: 'border-teal-200' },
brand: { bg: 'bg-brand-100', text: 'text-brand-700', border: 'border-brand-200' },
indigo: { bg: 'bg-indigo-100', text: 'text-indigo-700', border: 'border-indigo-200' },
slate: TONE_FALLBACK,
sky: { bg: 'bg-sky-100', text: 'text-sky-700', border: 'border-sky-200' },
......
......@@ -248,7 +248,7 @@ export const mockPersona = {
key: PersonaFeatureKey.LIFECYCLE_STAGE,
label: '生命周期',
value: '成熟客 · 末诊140天 · 就诊20次',
tone: 'teal',
tone: 'brand',
evidence: ['fact_visit_2026_04'],
data: { stage: 'mature', label: '成熟客', recencyDays: 140, totalVisits: 20 },
},
......
......@@ -311,7 +311,7 @@ export function OutcomeForm({
value={notes}
onChange={(e) => setNotes(e.target.value)}
placeholder="患者反馈、关键点、需告知医生的信息……"
className="flex-1 min-h-[60px] px-2.5 py-1.5 rounded border border-slate-100 bg-white text-[12.5px] resize-none focus:outline-none focus:ring-2 focus:ring-teal-200 focus:border-teal-300"
className="flex-1 min-h-[60px] px-2.5 py-1.5 rounded border border-slate-100 bg-white text-[12.5px] resize-none focus:outline-none focus:ring-2 focus:ring-brand-200 focus:border-brand-300"
/>
</div>
......@@ -329,7 +329,7 @@ export function OutcomeForm({
submitted
? 'bg-emerald-600 text-white'
: canSubmit
? 'bg-teal-600 text-white hover:bg-teal-700'
? 'bg-brand-600 text-white hover:bg-brand-700'
: 'bg-slate-100 text-slate-400 cursor-not-allowed',
)}
>
......
......@@ -155,14 +155,14 @@ function FeatureCard({
ref={cardRef}
className={cn(
'relative rounded-md border p-3 pr-9',
focused ? 'border-teal-300 bg-teal-50/40 ring-1 ring-teal-200' : 'border-slate-100',
focused ? 'border-brand-300 bg-brand-50/40 ring-1 ring-brand-200' : 'border-slate-100',
)}
>
{/* 右上角 ? —— 真 button:可 Tab 聚焦,focus 也能唤出说明卡(触屏/键盘可达) */}
<PersonaFeatureHover featureKey={f.key} value={f.value}>
<button
type="button"
className="absolute top-1.5 right-1.5 inline-flex h-6 w-6 items-center justify-center rounded-full text-slate-400 hover:bg-slate-100 hover:text-slate-700 focus:outline-none focus-visible:ring-2 focus-visible:ring-teal-500"
className="absolute top-1.5 right-1.5 inline-flex h-6 w-6 items-center justify-center rounded-full text-slate-400 hover:bg-slate-100 hover:text-slate-700 focus:outline-none focus-visible:ring-2 focus-visible:ring-brand-500"
aria-label={`${f.label} 怎么算的`}
>
<svg viewBox="0 0 24 24" className="h-3.5 w-3.5" fill="none" stroke="currentColor" strokeWidth="2">
......@@ -211,7 +211,7 @@ function FeatureCard({
<button
type="button"
onClick={() => setShowAll((v) => !v)}
className="text-[10.5px] text-teal-700 hover:underline"
className="text-[10.5px] text-brand-700 hover:underline"
>
{showAll ? '收起' : `展开全部`}
</button>
......
......@@ -44,7 +44,7 @@ export function PersonaFeatureHover({
</div>
{/* 本患者实际取值 */}
{value && (
<div className="rounded bg-teal-50 px-2 py-1.5 text-[11px] leading-relaxed text-teal-800">
<div className="rounded bg-brand-50 px-2 py-1.5 text-[11px] leading-relaxed text-brand-800">
{value}
</div>
)}
......
......@@ -603,7 +603,7 @@ export function PlanDetailApp({
</span>
)}
{focusTreatment && (
<span className="inline-flex items-center rounded px-1.5 py-0.5 text-[10.5px] font-medium bg-teal-50 text-teal-700 border border-teal-100">
<span className="inline-flex items-center rounded px-1.5 py-0.5 text-[10.5px] font-medium bg-brand-50 text-brand-700 border border-brand-100">
目标 · {focusTreatment}
</span>
)}
......@@ -1055,7 +1055,7 @@ function TopBar({
{!embedded && (
<>
<div className="inline-flex flex-none items-center gap-2">
<span className="inline-flex h-7 w-7 items-center justify-center rounded-md bg-teal-600 text-[12px] font-bold text-white">
<span className="inline-flex h-7 w-7 items-center justify-center rounded-md bg-brand-600 text-[12px] font-bold text-white">
PAC
</span>
{/* "疗效保障"标题字在窄屏隐藏,腾出空间给关键信息 */}
......@@ -1132,7 +1132,7 @@ function TopBar({
'inline-flex items-center gap-1.5 rounded-md border px-2 sm:px-2.5 py-1 text-[11.5px] font-medium transition-colors',
refreshing
? 'cursor-not-allowed border-slate-100 bg-slate-50 text-slate-400'
: 'border-slate-100 bg-white text-slate-700 hover:border-teal-300 hover:bg-teal-50 hover:text-teal-700',
: 'border-slate-100 bg-white text-slate-700 hover:border-brand-300 hover:bg-brand-50 hover:text-brand-700',
)}
>
<RefreshCw className={cn('h-3.5 w-3.5', refreshing && 'animate-spin')} />
......@@ -1147,7 +1147,7 @@ function TopBar({
type="button"
onClick={onOpenPotential}
title="查看潜在治疗机会"
className="inline-flex items-center gap-1.5 rounded-md border border-slate-100 bg-white px-2 sm:px-2.5 py-1 text-[11.5px] font-medium text-slate-700 transition-colors hover:border-teal-300 hover:bg-teal-50 hover:text-teal-700"
className="inline-flex items-center gap-1.5 rounded-md border border-slate-100 bg-white px-2 sm:px-2.5 py-1 text-[11.5px] font-medium text-slate-700 transition-colors hover:border-brand-300 hover:bg-brand-50 hover:text-brand-700"
>
<Sparkles className="h-3.5 w-3.5" />
<span className="hidden sm:inline">潜在</span>
......@@ -1158,7 +1158,7 @@ function TopBar({
type="button"
onClick={onCreateAppointment}
title="新建预约"
className="inline-flex items-center gap-1.5 rounded-md border border-slate-100 bg-white px-2 sm:px-2.5 py-1 text-[11.5px] font-medium text-slate-700 transition-colors hover:border-teal-300 hover:bg-teal-50 hover:text-teal-700"
className="inline-flex items-center gap-1.5 rounded-md border border-slate-100 bg-white px-2 sm:px-2.5 py-1 text-[11.5px] font-medium text-slate-700 transition-colors hover:border-brand-300 hover:bg-brand-50 hover:text-brand-700"
>
<CalendarPlus className="h-3.5 w-3.5" />
<span className="hidden sm:inline">预约</span>
......@@ -1170,7 +1170,7 @@ function TopBar({
onClick={onOpenReturnVisit}
onMouseEnter={onHoverReturnVisit}
title="回访"
className="inline-flex items-center gap-1.5 rounded-md bg-teal-600 px-2 sm:px-2.5 py-1 text-[11.5px] font-medium text-white transition-colors hover:bg-teal-700"
className="inline-flex items-center gap-1.5 rounded-md bg-brand-600 px-2 sm:px-2.5 py-1 text-[11.5px] font-medium text-white transition-colors hover:bg-brand-700"
>
<CalendarClock className="h-3.5 w-3.5" />
<span className="hidden sm:inline">回访</span>
......@@ -1373,7 +1373,7 @@ function IdentityCard({
<a
href={originalArchiveUrl}
{...HOST_LINK_PROPS}
className="text-[10.5px] text-teal-700 hover:underline"
className="text-[10.5px] text-brand-700 hover:underline"
>
原始档案 →
</a>
......@@ -1409,7 +1409,7 @@ function IdentityCard({
className={cn(
'inline-flex items-center rounded px-1 text-[10px] font-medium leading-4 ring-1 ring-inset',
patient.phoneVerified
? 'bg-teal-50 text-teal-700 ring-teal-200'
? 'bg-brand-50 text-brand-700 ring-brand-200'
: 'bg-slate-50 text-slate-400 ring-slate-200',
)}
>
......@@ -1421,7 +1421,7 @@ function IdentityCard({
title="复制号码"
className={cn(
'inline-flex items-center justify-center w-5 h-5 rounded transition-colors',
copied ? 'text-emerald-600 bg-emerald-50' : 'text-slate-400 hover:text-teal-700 hover:bg-teal-50',
copied ? 'text-emerald-600 bg-emerald-50' : 'text-slate-400 hover:text-brand-700 hover:bg-brand-50',
)}
>
{copied ? (
......@@ -1493,7 +1493,7 @@ function PersonaTagsCard({
title="画像标签"
meta={features.length > 0 ? `${features.length} 项` : undefined}
action={
<button onClick={onOpenDetail} className="text-[10.5px] text-teal-700 hover:underline">
<button onClick={onOpenDetail} className="text-[10.5px] text-brand-700 hover:underline">
详情 →
</button>
}
......@@ -1534,7 +1534,7 @@ function RecallReasonLine({ visibleReasons }: { visibleReasons: PlanReason[] })
{visibleReasons.length > 1 && (
<HoverCard openDelay={120} closeDelay={80}>
<HoverCardTrigger asChild>
<button className="ml-1.5 align-middle rounded px-1 text-[10.5px] text-slate-400 hover:text-teal-700 hover:bg-slate-100 cursor-default">
<button className="ml-1.5 align-middle rounded px-1 text-[10.5px] text-slate-400 hover:text-brand-700 hover:bg-slate-100 cursor-default">
+{visibleReasons.length - 1}
</button>
</HoverCardTrigger>
......@@ -1596,7 +1596,7 @@ function RecallBriefLine({
}, [planId]);
if (summary) {
// 主题色阶背景:左侧 teal 实条 + 向右渐隐的 teal 底,突出"这通电话的由头"。
// 主题色阶背景:左侧品牌色实条 + 向右渐隐的品牌色底,突出"这通电话的由头"。
// 右侧箭头展开 → 看摘要"压掉"的结构化召回原因(=以前的「为什么召回」,样式沿用 RecallReasonLine)。
const canExpand = visibleReasons.length > 0;
return (
......@@ -1606,15 +1606,15 @@ function RecallBriefLine({
onClick={canExpand ? () => setExpanded((v) => !v) : undefined}
aria-expanded={canExpand ? expanded : undefined}
className={cn(
'flex w-full items-start gap-2 rounded-md border-l-[3px] border-teal-400 bg-gradient-to-r from-teal-50 via-teal-50/60 to-transparent py-1.5 pl-2 pr-2 text-left',
canExpand ? 'cursor-pointer hover:from-teal-100/70' : 'cursor-default',
'flex w-full items-start gap-2 rounded-md border-l-[3px] border-brand-400 bg-gradient-to-r from-brand-50 via-brand-50/60 to-transparent py-1.5 pl-2 pr-2 text-left',
canExpand ? 'cursor-pointer hover:from-brand-100/70' : 'cursor-default',
)}
>
<svg viewBox="0 0 24 24" className="mt-[1px] h-3.5 w-3.5 flex-none text-teal-500" fill="none" stroke="currentColor" strokeWidth="2">
<svg viewBox="0 0 24 24" className="mt-[1px] h-3.5 w-3.5 flex-none text-brand-500" fill="none" stroke="currentColor" strokeWidth="2">
<circle cx="12" cy="12" r="9" />
<path d="M12 8v5M12 16h.01" strokeLinecap="round" />
</svg>
<span className="flex-1 min-w-0 font-medium text-teal-900 leading-snug">{summary}</span>
<span className="flex-1 min-w-0 font-medium text-brand-900 leading-snug">{summary}</span>
{canExpand && (
// 右指箭头(收起)→ 旋转 90° 朝下(展开)
<svg
......@@ -1622,7 +1622,7 @@ function RecallBriefLine({
fill="none"
stroke="currentColor"
strokeWidth="2.5"
className={cn('mt-[1px] h-3.5 w-3.5 flex-none text-teal-500/70 transition-transform', expanded && 'rotate-90')}
className={cn('mt-[1px] h-3.5 w-3.5 flex-none text-brand-500/70 transition-transform', expanded && 'rotate-90')}
>
<path d="M9 6l6 6-6 6" strokeLinecap="round" strokeLinejoin="round" />
</svg>
......@@ -1845,11 +1845,11 @@ function KeyFactsCard({
{/* 「牙位 →」按业务要求隐藏(2026-07-29)。抽屉(kind='teeth')整套留着,
把 onOpenTeeth 传回来就恢复 —— 不删是因为牙位图本身没问题,只是这一屏不需要。 */}
{onOpenTeeth && (
<button onClick={onOpenTeeth} className="text-[10.5px] text-teal-700 hover:underline">
<button onClick={onOpenTeeth} className="text-[10.5px] text-brand-700 hover:underline">
牙位 →
</button>
)}
<button onClick={onOpenDetail} className="text-[10.5px] text-teal-700 hover:underline">
<button onClick={onOpenDetail} className="text-[10.5px] text-brand-700 hover:underline">
详情 →
</button>
</span>
......@@ -1946,7 +1946,7 @@ function TreatmentHistoryCard({
action={
<div className="flex items-center gap-2">
{onOpenMedical && (
<button onClick={onOpenMedical} className="text-[10.5px] text-teal-700 hover:underline">
<button onClick={onOpenMedical} className="text-[10.5px] text-brand-700 hover:underline">
病历快读 →
</button>
)}
......@@ -1955,13 +1955,13 @@ function TreatmentHistoryCard({
<a
href={emrUrl}
{...HOST_LINK_PROPS}
className="text-[10.5px] text-teal-700 hover:underline"
className="text-[10.5px] text-brand-700 hover:underline"
>
原始病历 →
</a>
) : (
onOpenTimeline && (
<button onClick={onOpenTimeline} className="text-[10.5px] text-teal-700 hover:underline">
<button onClick={onOpenTimeline} className="text-[10.5px] text-brand-700 hover:underline">
详情 →
</button>
)
......@@ -1978,7 +1978,7 @@ function TreatmentHistoryCard({
<span className="flex-1 min-w-0 truncate text-slate-800" title={treatmentLabel(latestPlan)}>
{treatmentLabel(latestPlan)}
</span>
<span className="flex-none rounded bg-teal-100 px-1 text-[10px] font-medium text-teal-700">治疗计划</span>
<span className="flex-none rounded bg-brand-100 px-1 text-[10px] font-medium text-brand-700">治疗计划</span>
</div>
)}
{history.length === 0 ? (
......@@ -2028,7 +2028,7 @@ function SuggestionCard({
<circle cx="12" cy="12" r="1.5" fill="currentColor" />
</svg>
),
tone: 'teal',
tone: 'brand',
},
{
label: '归属诊所',
......@@ -2079,14 +2079,14 @@ function SuggestionCard({
{/* 召回冷静期 / 暂缓 — snoozedUntil 未到期时提示客服"到点再跟,别现在打" */}
{plan.snoozedUntil && plan.snoozedUntil.getTime() > Date.now() && (
<div className="flex items-start gap-2 text-[11px] py-0.5 mt-1 bg-teal-50/60 rounded px-1 -mx-1">
<span className="flex-none w-5 h-5 rounded flex items-center justify-center mt-px bg-teal-100 text-teal-700">
<div className="flex items-start gap-2 text-[11px] py-0.5 mt-1 bg-brand-50/60 rounded px-1 -mx-1">
<span className="flex-none w-5 h-5 rounded flex items-center justify-center mt-px bg-brand-100 text-brand-700">
<svg viewBox="0 0 24 24" className="w-3 h-3" fill="none" stroke="currentColor" strokeWidth="1.8" strokeLinecap="round" strokeLinejoin="round">
<circle cx="12" cy="12" r="9" /><path d="M12 7v5l3 2" />
</svg>
</span>
<span className="w-14 flex-none mt-0.5 text-teal-700 font-semibold">暂缓跟进</span>
<span className="font-medium flex-1 leading-snug min-w-0 text-teal-900 break-words">
<span className="w-14 flex-none mt-0.5 text-brand-700 font-semibold">暂缓跟进</span>
<span className="font-medium flex-1 leading-snug min-w-0 text-brand-900 break-words">
{snoozeLabel(plan.snoozedUntil)}
</span>
</div>
......@@ -2105,7 +2105,7 @@ function SuggestionCard({
key={i}
className={cn(
'w-2 h-1.5 rounded-sm',
// 仅"已触达"染色,不预亮"下一格" — 旧版 0/4 时第一格 teal 高亮被误读为已用
// 仅"已触达"染色,不预亮"下一格" — 旧版 0/4 时第一格高亮被误读为已用
i < plan.contactAttempts ? 'bg-amber-500' : 'bg-slate-200',
)}
/>
......@@ -2207,7 +2207,7 @@ function ReturnVisitsCard({
meta={`${visits.length} `}
action={
onOpenDetail ? (
<button onClick={onOpenDetail} className="text-[10.5px] text-teal-700 hover:underline">
<button onClick={onOpenDetail} className="text-[10.5px] text-brand-700 hover:underline">
详情 →
</button>
) : undefined
......@@ -2354,7 +2354,7 @@ function PersonaTagCloud({
type="button"
onClick={() => onSelect?.(f.key)}
aria-label={`${f.label}:${text} —— 查看画像详情`}
className="inline-flex max-w-full items-center gap-1 rounded px-1 py-px text-[10px] leading-4 text-slate-600 ring-1 ring-inset ring-slate-200 hover:bg-slate-50 hover:text-slate-900 hover:ring-slate-300 focus:outline-none focus-visible:ring-2 focus-visible:ring-teal-500"
className="inline-flex max-w-full items-center gap-1 rounded px-1 py-px text-[10px] leading-4 text-slate-600 ring-1 ring-inset ring-slate-200 hover:bg-slate-50 hover:text-slate-900 hover:ring-slate-300 focus:outline-none focus-visible:ring-2 focus-visible:ring-brand-500"
>
{inner}
</button>
......@@ -2415,7 +2415,7 @@ function RegenBtn({
'inline-flex items-center gap-0.5 px-1.5 py-0.5 text-[10.5px] border-r transition-colors',
streaming
? 'text-slate-300 border-slate-100 cursor-not-allowed'
: 'text-slate-500 border-slate-100 hover:text-teal-700 hover:bg-teal-50',
: 'text-slate-500 border-slate-100 hover:text-brand-700 hover:bg-brand-50',
);
return (
<div className="inline-flex flex-none items-stretch rounded border border-slate-100 overflow-hidden">
......@@ -2473,7 +2473,7 @@ function RegenBtn({
'inline-flex items-center gap-1 px-2 py-0.5 text-[10.5px] font-medium transition-colors',
streaming
? 'text-rose-600 bg-rose-50 hover:bg-rose-100'
: 'text-teal-600 hover:text-teal-700 hover:bg-indigo-50',
: 'text-brand-600 hover:text-brand-700 hover:bg-indigo-50',
)}
>
{streaming ? (
......
......@@ -150,7 +150,7 @@ function IssueList({
<div className="font-medium text-slate-600">{it.section || `问题 ${i + 1}`}</div>
<div className="break-words text-slate-500">{it.problem}</div>
{it.fix && (
<div className="mt-0.5 break-words text-teal-600">改法:{it.fix}</div>
<div className="mt-0.5 break-words text-brand-600">改法:{it.fix}</div>
)}
</li>
))}
......
......@@ -27,7 +27,7 @@ export function ScriptMarkdown({ sections, streaming = false }: { sections: Scri
className="w-full flex items-center justify-between gap-2 px-3 py-2 text-left hover:bg-slate-50"
>
<div className="flex items-center gap-2">
<span className="w-5 h-5 rounded text-[10.5px] flex items-center justify-center font-semibold bg-teal-50 text-teal-700">
<span className="w-5 h-5 rounded text-[10.5px] flex items-center justify-center font-semibold bg-brand-50 text-brand-700">
{idx + 1}
</span>
<span className={cn('text-[13px] font-semibold text-slate-900', streaming && 'shimmer-text')}>
......@@ -66,7 +66,7 @@ export function ScriptStepCards({ sections, streaming = false }: { sections: Scr
<div key={sec.id} className="rounded-md border border-slate-100 bg-white p-3 flex flex-col">
<div className="flex items-center justify-between mb-2">
<div className="flex items-center gap-2">
<span className="w-6 h-6 rounded-full bg-teal-600 text-white text-[11px] font-semibold flex items-center justify-center">
<span className="w-6 h-6 rounded-full bg-brand-600 text-white text-[11px] font-semibold flex items-center justify-center">
{i + 1}
</span>
<span className={cn('text-[13px] font-semibold text-slate-900', streaming && 'shimmer-text')}>
......@@ -101,9 +101,9 @@ export function ScriptCopilot({ sections, streaming = false }: { sections: Scrip
className={cn(
'flex-1 min-w-0 text-[11px] font-medium px-2 py-1.5 rounded transition-colors flex items-center justify-center gap-1.5',
i === active
? 'bg-teal-600 text-white shadow-sm'
? 'bg-brand-600 text-white shadow-sm'
: i < active
? 'text-teal-700 hover:bg-white'
? 'text-brand-700 hover:bg-white'
: 'text-slate-500 hover:bg-white',
)}
>
......@@ -113,7 +113,7 @@ export function ScriptCopilot({ sections, streaming = false }: { sections: Scrip
i === active
? 'bg-white/20 text-white'
: i < active
? 'bg-teal-100 text-teal-700'
? 'bg-brand-100 text-brand-700'
: 'bg-slate-200 text-slate-500',
)}
>
......@@ -125,18 +125,18 @@ export function ScriptCopilot({ sections, streaming = false }: { sections: Scrip
</div>
{/* 当前段高亮 */}
<div className="rounded-md border-2 border-teal-200 bg-teal-50/30 p-4 ring-1 ring-teal-100">
<div className="rounded-md border-2 border-brand-200 bg-brand-50/30 p-4 ring-1 ring-brand-100">
<div className="flex items-center justify-between mb-2">
<div className="flex items-center gap-2">
<span className="w-2 h-2 rounded-full bg-teal-500 animate-pulse" />
<span className="text-[11px] text-teal-700 font-semibold uppercase tracking-wide">当前段</span>
<span className="w-2 h-2 rounded-full bg-brand-500 animate-pulse" />
<span className="text-[11px] text-brand-700 font-semibold uppercase tracking-wide">当前段</span>
<span className={cn('text-[14px] font-semibold text-slate-900', streaming && 'shimmer-text')}>
{cur.label}
</span>
</div>
</div>
<MD text={cur.markdown} />
<div className="mt-3 flex items-center justify-between pt-2 border-t border-teal-100">
<div className="mt-3 flex items-center justify-between pt-2 border-t border-brand-100">
<button
disabled={active === 0}
onClick={() => setActive(Math.max(0, active - 1))}
......@@ -150,7 +150,7 @@ export function ScriptCopilot({ sections, streaming = false }: { sections: Scrip
<button
disabled={active === sections.length - 1}
onClick={() => setActive(Math.min(sections.length - 1, active + 1))}
className="text-[12px] text-teal-700 font-medium hover:text-teal-900 disabled:text-slate-300 inline-flex items-center gap-1"
className="text-[12px] text-brand-700 font-medium hover:text-brand-900 disabled:text-slate-300 inline-flex items-center gap-1"
>
下一段 →
</button>
......
......@@ -10,7 +10,7 @@ export const TONE: Record<
string,
{ bg: string; text: string; ring: string; dot: string }
> = {
teal: { bg: 'bg-teal-50', text: 'text-teal-700', ring: 'ring-teal-200', dot: 'bg-teal-500' },
brand: { bg: 'bg-brand-50', text: 'text-brand-700', ring: 'ring-brand-200', dot: 'bg-brand-500' },
indigo: { bg: 'bg-indigo-50', text: 'text-indigo-700', ring: 'ring-indigo-200', dot: 'bg-indigo-500' },
emerald: { bg: 'bg-emerald-50', text: 'text-emerald-700', ring: 'ring-emerald-200', dot: 'bg-emerald-500' },
amber: { bg: 'bg-amber-50', text: 'text-amber-700', ring: 'ring-amber-200', dot: 'bg-amber-500' },
......@@ -25,7 +25,7 @@ export const tone = (t?: string) => TONE[t ?? 'slate'] ?? TONE.slate!;
// Chip
// ──────────────────────────────────────────
/**
* 语义色小标签 — tone: teal/slate/amber/rose/indigo/emerald/sky…,icon 可选,size: xs/sm。工作台高频原语。
* 语义色小标签 — tone: brand/slate/amber/rose/indigo/emerald/sky…,icon 可选,size: xs/sm。工作台高频原语。
*/
export function Chip({
children,
......@@ -230,7 +230,7 @@ export function MD({ text, className }: { text: string; className?: string }) {
out.push(
<blockquote
key={key++}
className="my-2 pl-3 border-l-2 border-teal-300 bg-teal-50/40 py-2 pr-2 rounded-r text-[13px] text-slate-800 leading-relaxed"
className="my-2 pl-3 border-l-2 border-brand-300 bg-brand-50/40 py-2 pr-2 rounded-r text-[13px] text-slate-800 leading-relaxed"
>
{block.map((b, k) => (
<p key={k} className="m-0" dangerouslySetInnerHTML={{ __html: inlineMD(b) }} />
......
......@@ -89,7 +89,7 @@ export function TaskDrawer({ currentPlanId }: { currentPlanId: string }) {
<div className="hidden md:block fixed left-0 top-0 bottom-0 w-3 z-40" onMouseEnter={() => setHover(true)}>
{!open && (
<div className="absolute left-0 top-1/2 -translate-y-1/2 flex flex-col items-center gap-1">
<span className="block w-1 h-12 bg-teal-500/40 rounded-r-full hover:bg-teal-600 transition-colors" />
<span className="block w-1 h-12 bg-brand-500/40 rounded-r-full hover:bg-brand-600 transition-colors" />
<span className="text-[9px] text-slate-400 -rotate-90 origin-center mt-4 whitespace-nowrap tracking-widest pointer-events-none">
我的任务
</span>
......@@ -110,7 +110,7 @@ export function TaskDrawer({ currentPlanId }: { currentPlanId: string }) {
<header className="flex-none px-3 pt-3 pb-2 border-b border-slate-100">
<div className="flex items-center justify-between gap-2 mb-2.5">
<div className="flex items-center gap-2 min-w-0">
<span className="inline-flex items-center justify-center w-7 h-7 rounded-md bg-teal-50 text-teal-700 ring-1 ring-teal-200 flex-none">
<span className="inline-flex items-center justify-center w-7 h-7 rounded-md bg-brand-50 text-brand-700 ring-1 ring-brand-200 flex-none">
<svg viewBox="0 0 24 24" className="w-3.5 h-3.5" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
<path d="M9 11l3 3L22 4 M21 12v7a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h11" />
</svg>
......@@ -125,7 +125,7 @@ export function TaskDrawer({ currentPlanId }: { currentPlanId: string }) {
title={pinned ? '取消固定' : '固定打开'}
className={cn(
'w-6 h-6 rounded inline-flex items-center justify-center transition-colors flex-none',
pinned ? 'bg-teal-600 text-white' : 'text-slate-400 hover:bg-slate-100 hover:text-slate-700',
pinned ? 'bg-brand-600 text-white' : 'text-slate-400 hover:bg-slate-100 hover:text-slate-700',
)}
>
<svg viewBox="0 0 24 24" className={cn('w-3.5 h-3.5 transition-transform', pinned && 'rotate-45')} fill="none" stroke="currentColor" strokeWidth="2">
......@@ -152,7 +152,7 @@ export function TaskDrawer({ currentPlanId }: { currentPlanId: string }) {
placeholder="搜索 姓名 / 手机 / ID(已加载项)"
value={search}
onChange={(e) => setSearch(e.target.value)}
className="w-full pl-7 pr-2.5 py-1.5 rounded-md border border-slate-100 bg-slate-50 text-[12px] text-slate-800 focus:outline-none focus:ring-2 focus:ring-teal-200 focus:border-teal-300 focus:bg-white transition-all"
className="w-full pl-7 pr-2.5 py-1.5 rounded-md border border-slate-100 bg-slate-50 text-[12px] text-slate-800 focus:outline-none focus:ring-2 focus:ring-brand-200 focus:border-brand-300 focus:bg-white transition-all"
/>
</div>
<div className="grid grid-cols-3 gap-1">
......@@ -165,7 +165,7 @@ export function TaskDrawer({ currentPlanId }: { currentPlanId: string }) {
onClick={() => setFilter(t.v)}
className={cn(
'px-1 py-1 rounded inline-flex items-center justify-center gap-1 text-[11px] transition-colors',
filter === t.v ? 'bg-teal-600 text-white font-semibold' : 'bg-slate-100 text-slate-600 hover:bg-slate-200',
filter === t.v ? 'bg-brand-600 text-white font-semibold' : 'bg-slate-100 text-slate-600 hover:bg-slate-200',
)}
>
<span>{t.label}</span>
......@@ -255,7 +255,7 @@ function TaskRow({
className={cn(
'w-full text-left rounded-md border px-2.5 py-2 transition-colors',
active
? 'border-teal-400 bg-teal-50/60 ring-1 ring-teal-100'
? 'border-brand-400 bg-brand-50/60 ring-1 ring-brand-100'
: 'border-slate-100 bg-white hover:border-slate-300 hover:bg-slate-50',
done && 'opacity-70',
)}
......@@ -280,7 +280,7 @@ function TaskRow({
</>
)}
</div>
{active && <div className="mt-1 text-[10px] text-teal-700 font-medium">● 当前查看</div>}
{active && <div className="mt-1 text-[10px] text-brand-700 font-medium">● 当前查看</div>}
</button>
</li>
);
......
......@@ -146,9 +146,9 @@ export function ToothTimeline({ facts }: { facts: AdaptedFact[] }) {
<ToothFactRow key={`${k}-${f.id}`} fact={f} />
))}
{!isWhole && bridgeNote.has(k) && (
<div className="flex items-baseline gap-2 px-2.5 py-1.5 text-[12px] bg-teal-50/40">
<div className="flex items-baseline gap-2 px-2.5 py-1.5 text-[12px] bg-brand-50/40">
<span className="w-[74px] flex-none" />
<span className="flex-none px-1.5 py-px rounded text-[10px] font-medium bg-teal-50 text-teal-700">
<span className="flex-none px-1.5 py-px rounded text-[10px] font-medium bg-brand-50 text-brand-700">
桥体
</span>
<span className="flex-1 min-w-0 text-slate-600 leading-snug">
......
......@@ -202,7 +202,7 @@ export function PatientPickerRail({
className={cn(
'rounded-t-md px-3 py-1.5 text-[12.5px] transition-colors',
view === t.v
? 'border-b-2 border-teal-600 font-semibold text-teal-700'
? 'border-b-2 border-brand-600 font-semibold text-brand-700'
: 'text-slate-500 hover:text-slate-800',
)}
>
......@@ -239,7 +239,7 @@ export function PatientPickerRail({
scheduleKeyword(v);
}}
placeholder="姓名 / 手机 / 病历号"
className="h-7 w-full rounded-md border border-slate-100 bg-slate-50 pl-7 pr-2 text-[12px] outline-none placeholder:text-slate-400 focus:border-teal-300 focus:bg-white"
className="h-7 w-full rounded-md border border-slate-100 bg-slate-50 pl-7 pr-2 text-[12px] outline-none placeholder:text-slate-400 focus:border-brand-300 focus:bg-white"
/>
</div>
<div className="flex items-center gap-1.5">
......@@ -251,7 +251,7 @@ export function PatientPickerRail({
className={cn(
'inline-flex h-6 items-center gap-1 rounded-md border px-1.5 text-[11px] transition-colors',
realPhoneOnly
? 'border-teal-300 bg-teal-50 font-medium text-teal-700'
? 'border-brand-300 bg-brand-50 font-medium text-brand-700'
: 'border-slate-100 bg-white text-slate-500 hover:bg-slate-50',
)}
>
......@@ -282,7 +282,7 @@ export function PatientPickerRail({
key={kv}
type="button"
onClick={() => toggleTag(kv)}
className="inline-flex items-center gap-0.5 rounded-full bg-teal-50 px-1.5 py-0.5 text-[10.5px] text-teal-700 ring-1 ring-inset ring-teal-200 hover:bg-teal-100"
className="inline-flex items-center gap-0.5 rounded-full bg-brand-50 px-1.5 py-0.5 text-[10.5px] text-brand-700 ring-1 ring-inset ring-brand-200 hover:bg-brand-100"
title={`${dim?.nameZh}:${opt?.zh}(点击移除)`}
>
{opt?.zh ?? v}
......@@ -341,7 +341,7 @@ export function PatientPickerRail({
type="button"
data-pet-perch
onClick={loadMore}
className="block w-full py-2 text-center text-[11px] text-teal-700 hover:bg-teal-50/50"
className="block w-full py-2 text-center text-[11px] text-brand-700 hover:bg-brand-50/50"
>
加载更多({items.length}/{total})
</button>
......@@ -384,7 +384,7 @@ function PersonaTagFilter({
className={cn(
'inline-flex h-6 items-center gap-1 rounded-md border px-1.5 text-[11px] transition-colors',
selected.size > 0
? 'border-teal-300 bg-teal-50 font-medium text-teal-700'
? 'border-brand-300 bg-brand-50 font-medium text-brand-700'
: 'border-slate-100 bg-white text-slate-500 hover:bg-slate-50',
)}
>
......@@ -396,7 +396,7 @@ function PersonaTagFilter({
<div className="mb-1 flex items-center justify-between px-1">
<span className="text-[11px] font-medium text-slate-500">按标签圈人</span>
{selected.size > 0 && (
<button type="button" onClick={onClear} className="text-[10.5px] text-teal-700 hover:underline">
<button type="button" onClick={onClear} className="text-[10.5px] text-brand-700 hover:underline">
清空
</button>
)}
......@@ -440,8 +440,8 @@ function PersonaTagFilter({
className={cn(
'rounded-full border px-2 py-0.5 text-[11px] transition-colors',
on
? 'border-teal-300 bg-teal-50 font-medium text-teal-700'
: 'border-slate-100 bg-white text-slate-600 hover:border-teal-200 hover:bg-teal-50/40',
? 'border-brand-300 bg-brand-50 font-medium text-brand-700'
: 'border-slate-100 bg-white text-slate-600 hover:border-brand-200 hover:bg-brand-50/40',
)}
>
{o.zh}
......@@ -491,7 +491,7 @@ function RailClinicFilter({
className={cn(
'inline-flex h-6 w-full items-center justify-between rounded-md border px-1.5 text-[11px] transition-colors',
selected.length
? 'border-teal-300 bg-teal-50 font-medium text-teal-700'
? 'border-brand-300 bg-brand-50 font-medium text-brand-700'
: 'border-slate-100 bg-white text-slate-500 hover:bg-slate-50',
)}
>
......@@ -507,7 +507,7 @@ function RailClinicFilter({
<div className="mb-1 flex items-center justify-between border-b border-slate-100 px-1.5 py-1">
<span className="text-[10.5px] text-slate-400">按诊所筛选(可多选)</span>
{selected.length > 0 && (
<button type="button" onClick={() => onChange([])} className="text-[10.5px] text-teal-600 hover:underline">
<button type="button" onClick={() => onChange([])} className="text-[10.5px] text-brand-600 hover:underline">
清空
</button>
)}
......@@ -516,7 +516,7 @@ function RailClinicFilter({
<label key={o.id} className="flex cursor-pointer items-center gap-2 rounded px-1.5 py-1.5 hover:bg-slate-50">
<input
type="checkbox"
className="h-3.5 w-3.5 accent-teal-600"
className="h-3.5 w-3.5 accent-brand-600"
checked={sel.has(o.id)}
onChange={() => toggle(o.id)}
/>
......@@ -624,7 +624,7 @@ function DoctorPicker({
type="button"
onClick={() => onToggle(kv)}
title="点击移除"
className="rounded-full border border-teal-300 bg-teal-50 px-2 py-0.5 text-[11px] font-medium text-teal-700"
className="rounded-full border border-brand-300 bg-brand-50 px-2 py-0.5 text-[11px] font-medium text-brand-700"
>
{kv.slice(dimId.length + 1)} ×
</button>
......@@ -635,7 +635,7 @@ function DoctorPicker({
value={q}
onChange={(e) => setQ(e.target.value)}
placeholder={doctors.length ? '输入医生姓名搜索' : '暂无医生名单'}
className="w-full rounded border border-slate-200 px-1.5 py-0.5 text-[11px] outline-none placeholder:text-slate-300 focus:border-teal-300"
className="w-full rounded border border-slate-200 px-1.5 py-0.5 text-[11px] outline-none placeholder:text-slate-300 focus:border-brand-300"
/>
{matches.length > 0 && (
<div className="flex flex-wrap gap-1">
......@@ -647,7 +647,7 @@ function DoctorPicker({
onToggle(`${dimId}:${d}`);
setQ('');
}}
className="rounded-full border border-slate-100 bg-white px-2 py-0.5 text-[11px] text-slate-600 hover:border-teal-200 hover:bg-teal-50/40"
className="rounded-full border border-slate-100 bg-white px-2 py-0.5 text-[11px] text-slate-600 hover:border-brand-200 hover:bg-brand-50/40"
>
{d}
</button>
......@@ -711,12 +711,12 @@ function PatientRow({
onKeyDown={(e) => e.key === 'Enter' && onClick()}
className={cn(
'group block w-full cursor-pointer border-b border-slate-100 px-3 py-2 text-left transition-colors',
active ? 'bg-teal-50/70 ring-1 ring-inset ring-teal-200' : 'hover:bg-slate-50',
active ? 'bg-brand-50/70 ring-1 ring-inset ring-brand-200' : 'hover:bg-slate-50',
)}
>
<div className="flex items-center gap-1.5">
{p.lastOutcome && <OutcomeTag outcome={p.lastOutcome} />}
<span className={cn('truncate text-[13px] font-semibold', active ? 'text-teal-800' : 'text-slate-900')}>
<span className={cn('truncate text-[13px] font-semibold', active ? 'text-brand-800' : 'text-slate-900')}>
{p.patient.name ?? p.patient.nameMasked ?? '—'}
</span>
{/* 顺序对齐详情页患者卡:姓名 → 病历号 → 性别·年龄。
......@@ -730,7 +730,7 @@ function PatientRow({
{formatGender(p.patient.gender)} · {p.patient.age ?? '?'}
</span>
{p.patient.phoneVerified && (
<span className="flex-none rounded bg-teal-50 px-1 text-[9.5px] font-medium leading-4 text-teal-700 ring-1 ring-inset ring-teal-200">
<span className="flex-none rounded bg-brand-50 px-1 text-[9.5px] font-medium leading-4 text-brand-700 ring-1 ring-inset ring-brand-200">
</span>
)}
......@@ -776,10 +776,10 @@ function PatientRow({
)}
<span className="ml-auto flex-none" />
{(() => {
// 动作按钮:待认领→「认领」(teal),已认领→「返池」(amber)。二者与状态药丸叠同 grid 格,
// 动作按钮:待认领→「认领」(品牌色),已认领→「返池」(amber)。二者与状态药丸叠同 grid 格,
// 槽位宽/高恒为较大值,hover 只切 visibility(非 display)→ 悬停整行不重排、不位移。
const act = onClaim
? { label: '认领', tone: 'bg-teal-600 hover:bg-teal-700', fn: onClaim }
? { label: '认领', tone: 'bg-brand-600 hover:bg-brand-700', fn: onClaim }
: onRecycle
? { label: '返池', tone: 'bg-amber-500 hover:bg-amber-600', fn: onRecycle }
: null;
......
......@@ -240,7 +240,7 @@ export function PlansListApp() {
<KpiCard tone="amber" icon={<ListChecks className="h-4 w-4" />} label="我的进行中" value={counts.mineAssigned} sub={`共 ${counts.mine} 个工单`} />
<KpiCard tone="rose" icon={<Flame className="h-4 w-4" />} label="召回池余量" value={counts.pool} sub="池中待领取" />
<KpiCard tone="emerald" icon={<ListChecks className="h-4 w-4" />} label="我完成" value={counts.mineCompleted} sub={successRateText(counts)} />
<KpiCard tone="teal" icon={<Waves className="h-4 w-4" />} label="全部计划" value={counts.all} sub="活跃 + 历史" />
<KpiCard tone="brand" icon={<Waves className="h-4 w-4" />} label="全部计划" value={counts.all} sub="活跃 + 历史" />
</div>
<div className="flex-none bg-white">
......@@ -353,7 +353,7 @@ function PageHeader({
<header className="flex flex-none items-center justify-between gap-3 border-b border-slate-100 bg-white px-5 py-3">
<div className="flex min-w-0 items-center gap-3">
<div className="inline-flex items-center gap-2">
<span className="inline-flex h-7 w-7 items-center justify-center rounded-md bg-teal-600 text-[12px] font-bold text-white">PAC</span>
<span className="inline-flex h-7 w-7 items-center justify-center rounded-md bg-brand-600 text-[12px] font-bold text-white">PAC</span>
<span className="text-[13px] font-semibold text-slate-900">疗效保障</span>
</div>
<span className="text-slate-300">/</span>
......@@ -370,7 +370,7 @@ function PageHeader({
<div className="font-medium text-slate-700">{userDisplayName(user)}</div>
<div className="nums">{user?.clinicIds?.length ?? 0} 个诊所 · {roleNameZh(user?.role)}</div>
</div>
<span className="inline-flex h-8 w-8 items-center justify-center rounded-full bg-gradient-to-br from-teal-400 to-teal-600 text-[12px] font-bold text-white">
<span className="inline-flex h-8 w-8 items-center justify-center rounded-full bg-gradient-to-br from-brand-400 to-brand-600 text-[12px] font-bold text-white">
{userDisplayName(user).charAt(0).toUpperCase()}
</span>
<Button
......@@ -399,7 +399,7 @@ const TONE_BG: Record<string, string> = {
amber: 'bg-amber-50 text-amber-700 ring-amber-200',
rose: 'bg-rose-50 text-rose-700 ring-rose-200',
emerald: 'bg-emerald-50 text-emerald-700 ring-emerald-200',
teal: 'bg-teal-50 text-teal-700 ring-teal-200',
brand: 'bg-brand-50 text-brand-700 ring-brand-200',
};
function KpiCard({
......@@ -489,12 +489,12 @@ function ViewTabs({
<span
className={cn(
'inline-flex h-[18px] min-w-[20px] items-center justify-center rounded px-1 text-[10.5px] font-bold nums',
active ? 'bg-teal-600 text-white' : 'bg-slate-100 text-slate-600',
active ? 'bg-brand-600 text-white' : 'bg-slate-100 text-slate-600',
)}
>
{count}
</span>
{active && <span className="absolute -bottom-px left-3 right-3 h-[2px] rounded-full bg-teal-600" />}
{active && <span className="absolute -bottom-px left-3 right-3 h-[2px] rounded-full bg-brand-600" />}
</button>
);
})}
......@@ -537,7 +537,7 @@ function ClinicFilter({
<div className="mb-1 flex items-center justify-between border-b border-slate-100 px-1.5 py-1">
<span className="text-[10.5px] text-slate-400">按诊所筛选(可多选)</span>
{selected.length > 0 && (
<button onClick={() => onChange([])} className="text-[10.5px] text-teal-600 hover:underline">
<button onClick={() => onChange([])} className="text-[10.5px] text-brand-600 hover:underline">
清空
</button>
)}
......@@ -549,7 +549,7 @@ function ClinicFilter({
>
<input
type="checkbox"
className="h-3.5 w-3.5 accent-teal-600"
className="h-3.5 w-3.5 accent-brand-600"
checked={sel.has(o.id)}
onChange={() => toggle(o.id)}
/>
......@@ -649,7 +649,7 @@ function FilterBar({
className={cn(
'inline-flex h-8 items-center gap-1 rounded-md border px-2.5 text-xs transition-colors',
realPhoneOnly
? 'border-teal-300 bg-teal-50 font-medium text-teal-700'
? 'border-brand-300 bg-brand-50 font-medium text-brand-700'
: 'border-slate-100 bg-white text-slate-600 hover:bg-slate-50',
)}
>
......@@ -675,7 +675,7 @@ function FilterBar({
<label className="inline-flex cursor-pointer select-none items-center gap-1.5">
<input
type="checkbox"
className="h-4 w-4 accent-teal-600 cursor-pointer"
className="h-4 w-4 accent-brand-600 cursor-pointer"
checked={allSelected}
ref={(el) => {
if (el) el.indeterminate = !allSelected && someSelected;
......@@ -707,8 +707,8 @@ function BatchBar({
}) {
if (!count) return null;
return (
<div className="flex flex-none items-center gap-3 border-y border-teal-200 bg-teal-50/80 px-5 py-2 backdrop-blur">
<span className="text-[12px] text-teal-800">
<div className="flex flex-none items-center gap-3 border-y border-brand-200 bg-brand-50/80 px-5 py-2 backdrop-blur">
<span className="text-[12px] text-brand-800">
已选 <span className="nums font-bold">{count}</span>
</span>
<span className="text-slate-300">·</span>
......@@ -765,7 +765,7 @@ function PatientPlanCard({
<article
className={cn(
'group relative flex flex-col rounded-lg border bg-white transition-all',
selected ? 'border-teal-400 ring-2 ring-teal-100' : 'border-slate-100 hover:border-slate-300 hover:shadow-sm',
selected ? 'border-brand-400 ring-2 ring-brand-100' : 'border-slate-100 hover:border-slate-300 hover:shadow-sm',
isClosed && 'opacity-75',
)}
>
......@@ -780,7 +780,7 @@ function PatientPlanCard({
>
<input
type="checkbox"
className="h-4 w-4 accent-teal-600 cursor-pointer"
className="h-4 w-4 accent-brand-600 cursor-pointer"
checked={selected}
onChange={onSelect}
onClick={(e) => e.stopPropagation()}
......@@ -805,7 +805,7 @@ function PatientPlanCard({
{p.patient.phoneVerified && (
<span
title="真实手机号(外部对照表已核实)"
className="inline-flex items-center rounded bg-teal-50 px-1 text-[10px] font-medium leading-4 text-teal-700 ring-1 ring-inset ring-teal-200"
className="inline-flex items-center rounded bg-brand-50 px-1 text-[10px] font-medium leading-4 text-brand-700 ring-1 ring-inset ring-brand-200"
>
</span>
......@@ -852,7 +852,7 @@ function PatientPlanCard({
<HoverCardTrigger asChild>
<button
onClick={(e) => e.stopPropagation()}
className="ml-1.5 align-middle rounded px-1 text-[10.5px] text-slate-400 hover:text-teal-700 hover:bg-slate-100 cursor-default"
className="ml-1.5 align-middle rounded px-1 text-[10.5px] text-slate-400 hover:text-brand-700 hover:bg-slate-100 cursor-default"
>
+{p.reasons.length - 1}
</button>
......@@ -897,7 +897,7 @@ function PatientPlanCard({
{isPool && canAssign && (
<button
onClick={onClaim}
className="rounded-md bg-teal-600 px-2.5 py-1 text-[12px] font-semibold text-white transition-colors hover:bg-teal-700"
className="rounded-md bg-brand-600 px-2.5 py-1 text-[12px] font-semibold text-white transition-colors hover:bg-brand-700"
>
认领 →
</button>
......@@ -1001,7 +1001,7 @@ function ScenarioChip({ label, scenario }: { label: string; scenario: string })
const tone =
scenario === 'treatment_initiation_recall'
? 'bg-rose-50 text-rose-700 ring-rose-200'
: 'bg-teal-50 text-teal-700 ring-teal-200';
: 'bg-brand-50 text-brand-700 ring-brand-200';
return <span className={cn('inline-block rounded px-1.5 py-0.5 text-[10.5px] font-medium ring-1 ring-inset', tone)}>{label}</span>;
}
......
......@@ -52,7 +52,7 @@ export function AudioVisualizer({
ref={(el) => {
barsRef.current[i] = el;
}}
className="w-[3px] rounded-full bg-teal-500/90 transition-[height] duration-75 ease-out"
className="w-[3px] rounded-full bg-brand-500/90 transition-[height] duration-75 ease-out"
style={{ height: `${MIN_H}px` }}
/>
))}
......
......@@ -13,7 +13,7 @@ export function RealtimeCoachButton({ active, onClick }: { active: boolean; onCl
'cursor-pointer active:scale-95',
active
? 'bg-rose-500 hover:bg-rose-600 animate-coachPulse'
: 'bg-teal-600 hover:bg-teal-700',
: 'bg-brand-600 hover:bg-brand-700',
)}
>
{active ? (
......
......@@ -37,7 +37,7 @@ export interface ButtonProps
}
/**
* 主操作按钮 — variant: default/secondary/outline/ghost/destructive/link,size: sm/default/lg/icon;teal 主色
* 主操作按钮 — variant: default/secondary/outline/ghost/destructive/link,size: sm/default/lg/icon;PAC 品牌色(brand-600 / #0032A0)
* @category actions
*/
export const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
......
......@@ -19,7 +19,7 @@ const zhFormatters = {
};
/// shadcn 官方 Calendar(react-day-picker v10)—— 全 Tailwind classNames,不引 rdp 默认样式表。
/// 主题走项目 CSS 变量(--primary=teal-600 / --accent / --ring),故选中/今天/焦点自动是 teal
/// 主题走项目 CSS 变量(--primary=brand-600 / --accent / --ring),故选中/今天/焦点自动是品牌色
/// 支持 captionLayout="dropdown"(月/年下拉)与默认 label(左右翻页箭头)两种 caption。
/**
* 日历(react-day-picker 封装)— 单选/范围由 mode 决定。
......
......@@ -7,7 +7,7 @@ import { cn } from '@/lib/utils';
* Input — shadcn-new-york 标准 input,统一全站输入框样式。
*
* 风格:无 shadow(扁平),border-input(82% L,1px 视觉扎实),
* hover 加深 border,focus 切 ring(跟 primary 同色 = teal)。
* hover 加深 border,focus 切 ring(跟 primary 同色 = 品牌色)。
*
* 变体通过 className 覆盖:
* <Input className="font-mono text-xs" placeholder="https://..." />
......
......@@ -6,11 +6,11 @@
import { PersonaFeatureKey, PlanScenario } from './enums';
// ─────────────────────────────────────────────────────────
// Tone — UI 色板键(对应前端 Tailwind 色板 teal/indigo/...)
// Tone — UI 色板键(brand=PAC 主色,其余对应 Tailwind 色板 indigo/amber/...)
// ─────────────────────────────────────────────────────────
export type Tone =
| 'teal'
| 'brand'
| 'indigo'
| 'emerald'
| 'amber'
......@@ -49,7 +49,7 @@ export const planScenarioLabel = (key: string): string =>
* rose ⚠️ 警示 —— 看到要小心/要避让(禁忌、爽约、敏感、急迫)
* amber ⏳ 机会 —— 有事可做(潜在治疗、治疗史这类可切入点)
* indigo 💰 价值 —— 钱与权益
* teal 📈 阶段 —— 客户所处位置
* brand 📈 阶段 —— 客户所处位置
* sky/slate 基础属性 —— 不需要判断,只是背景信息
*/
export const PERSONA_FEATURE_META: Record<string, { label: string; tone: Tone }> = {
......@@ -66,8 +66,8 @@ export const PERSONA_FEATURE_META: Record<string, { label: string; tone: Tone }>
[PersonaFeatureKey.ENTITLEMENT_STATUS]: { label: '权益身份', tone: 'indigo' },
[PersonaFeatureKey.DISCOUNT_ANCHOR]: { label: '折扣锚点', tone: 'indigo' },
// 📈 阶段与关系(转介绍达人是正面信息,同理不该标红)
[PersonaFeatureKey.LIFECYCLE_STAGE]: { label: '生命周期', tone: 'teal' },
[PersonaFeatureKey.REFERRAL_CHAMPION]: { label: '转介绍达人', tone: 'teal' },
[PersonaFeatureKey.LIFECYCLE_STAGE]: { label: '生命周期', tone: 'brand' },
[PersonaFeatureKey.REFERRAL_CHAMPION]: { label: '转介绍达人', tone: 'brand' },
// 基础属性
[PersonaFeatureKey.TIME_PREFERENCE]: { label: '时间偏好', tone: 'sky' },
[PersonaFeatureKey.AGE_BRACKET]: { label: '年龄段', tone: 'sky' },
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment