/** * 家书 FamilyBook · 主题与设计令牌(鸿蒙端) * * 对应 Web 端 `assets/css/app.css` 里的 `:root { --paper / --ink / --brand … }`。 * * 配色取自图标:宣纸米 / 砖红封面 / 深褐书脊 / 纸白。 * 原则:扁平、留白充足、字重克制(正文 400,标题 500-600)。 * * ⚠️ 深浅色两套值取自资源文件(`resources/base|dark/element/color.json`), * 这里只做**语义名 → $r 资源**的映射,不硬编码 hex —— * 这样系统切深色时不用改任何页面代码。 */ /** 语义色令牌(全部指向资源,随系统深浅色自动切换) */ export class FbColors { /** 宣纸底(页面背景) */ static readonly paper: Resource = $r('app.color.page_bg'); /** 纸白(卡片背景) */ static readonly surface: Resource = $r('app.color.surface'); /** 略暖的白(次级卡片 / 内嵌块) */ static readonly surfaceAlt: Resource = $r('app.color.surface_alt'); /** 输入框底 */ static readonly inputBg: Resource = $r('app.color.input_bg'); /** 主文字(墨) */ static readonly ink: Resource = $r('app.color.ink'); /** 次级文字 */ static readonly muted: Resource = $r('app.color.muted'); /** 最弱的文字(占位、时间戳) */ static readonly faint: Resource = $r('app.color.faint'); /** 品牌色(砖红)—— 未进具体模块时的默认色 */ static readonly brand: Resource = $r('app.color.brand'); static readonly brand2: Resource = $r('app.color.brand_2'); static readonly brandInk: Resource = $r('app.color.brand_ink'); /** 分割线 */ static readonly line: Resource = $r('app.color.line'); static readonly line2: Resource = $r('app.color.line_2'); /** 语义色:绿=成功 / 红=删除 / 蓝=编辑 / 黄=提醒 */ static readonly green: Resource = $r('app.color.green'); static readonly red: Resource = $r('app.color.red'); static readonly blue: Resource = $r('app.color.blue'); static readonly yellow: Resource = $r('app.color.yellow'); } /** 间距 / 圆角 / 字号令牌(数值常量,vp/fp 由调用处加单位) */ export class FbMetric { /** 页面左右内边距 */ static readonly pagePad: number = 16; /** 卡片内边距 */ static readonly cardPad: number = 16; /** 卡片间距 */ static readonly cardGap: number = 12; /** 大圆角(卡片) */ static readonly radius: number = 16; /** 小圆角(内嵌块 / 输入框) */ static readonly radiusS: number = 10; /** 胶囊圆角 */ static readonly radiusPill: number = 999; /** 正文 */ static readonly fontBody: number = 15; /** 次级信息 */ static readonly fontSmall: number = 13; /** 最小号(时间戳 / 计数) */ static readonly fontTiny: number = 11; /** 卡片标题 */ static readonly fontTitle: number = 17; /** 页面大标题 */ static readonly fontH1: number = 24; /** 顶栏高度 */ static readonly topBarH: number = 56; /** 抽屉宽度 */ static readonly drawerW: number = 264; } /** * 模块主题色 → 客户端可用的三档调色板。 * * 与 Web 端 `Module::cssVars()` 的差别:那边填进 CSS 变量(浏览器按色值渲染), * 这边直接算成 hex 字符串给 ArkUI 用(ArkUI 的 ResourceColor 接受 '#RRGGBB')。 * 算法本身共用(见 `Modules.ets::fbThemeVars`)。 */ export interface FbPalette { /** 模块主色(图标 / 强调) */ main: string; /** 按钮底色 */ brand: string; /** hover / 浅底 */ brand2: string; /** 文字色(深一档,保证在浅底上看得清) */ ink: string; /** 极浅底色(选中行背景) */ wash: string; } /** * 由模块主题色算出整套调色板。 * * `wash` 是「选中行 / 标签底」用的极浅色 —— 做法是把主色降饱和、抬明度到 L≈0.94, * 这样在宣纸米底色上只是一层淡淡的染色,不会抢内容。 */ export function fbPalette(moduleColor: string): FbPalette { const v = fbThemeVarsLocal(moduleColor); return { main: moduleColor, brand: v.brand, brand2: v.brand2, ink: v.ink, wash: fbWashOf(moduleColor) }; } /** 极浅染色底(L≈0.94,饱和度压到 0.35 上限) */ function fbWashOf(hex: string): string { const hsl: number[] = fbHexToHslLocal(hex); const sat: number = Math.min(0.35, hsl[1]); return fbHslToHexLocal(hsl[0], sat, 0.94); } /** 局部副本:避免 Theme 依赖 Modules(两文件都能独立使用) */ function fbThemeVarsLocal(hex: string): FbThemeVarsLocal { const hsl: number[] = fbHexToHslLocal(hex.startsWith('#') ? hex : '#A84A28'); const h: number = hsl[0]; const sat: number = Math.max(0.28, Math.min(0.72, hsl[1])); return { brand: '#' + fbHslToHexLocal(h, sat, 0.42), brand2: '#' + fbHslToHexLocal(h, sat, 0.58), ink: '#' + fbHslToHexLocal(h, sat, 0.28) }; } interface FbThemeVarsLocal { brand: string; brand2: string; ink: string; } function fbHexToHslLocal(hex: string): number[] { const s: string = hex.startsWith('#') ? hex.substring(1) : hex; const r: number = parseInt(s.substring(0, 2), 16) / 255; const g: number = parseInt(s.substring(2, 4), 16) / 255; const b: number = parseInt(s.substring(4, 6), 16) / 255; const mx: number = Math.max(r, g, b); const mn: number = Math.min(r, g, b); const l: number = (mx + mn) / 2; const d: number = mx - mn; if (d === 0) { return [0, 0, l]; } const den: number = 1 - Math.abs(2 * l - 1); const sat: number = den > 1e-9 ? d / den : 0; let h: number; if (mx === r) { h = ((g - b) / d) % 6; } else if (mx === g) { h = ((b - r) / d) + 2; } else { h = ((r - g) / d) + 4; } h = h * 60; if (h < 0) { h = h + 360; } return [h / 360, sat, l]; } function fbHslToHexLocal(h: number, sat: number, l: number): string { const hh: number = h - Math.floor(h); if (sat <= 0) { const v: number = Math.round(l * 255); return fbH2(v) + fbH2(v) + fbH2(v); } const q: number = l < 0.5 ? l * (1 + sat) : l + sat - l * sat; const p: number = 2 * l - q; const conv = (t0: number): number => { let t: number = t0 - Math.floor(t0); if (t < 1 / 6) { return p + (q - p) * 6 * t; } if (t < 1 / 2) { return q; } if (t < 2 / 3) { return p + (q - p) * (2 / 3 - t) * 6; } return p; }; const r: number = Math.max(0, Math.min(255, Math.round(conv(hh + 1 / 3) * 255))); const g: number = Math.max(0, Math.min(255, Math.round(conv(hh) * 255))); const b: number = Math.max(0, Math.min(255, Math.round(conv(hh - 1 / 3) * 255))); return '#' + fbH2(r) + fbH2(g) + fbH2(b); } function fbH2(v: number): string { const t: string = v.toString(16).toUpperCase(); return t.length < 2 ? '0' + t : t; } /** 图标资源名 → Resource(模块图标与通用图标都在这张表里) */ export class FbIcon { /** 取模块图标(key 同 FB_MODULES 的 key) */ static module(key: string): Resource { return fbIconOf('fb_ic_' + key); } /** 取通用图标(search / menu / close / bell / user / gear …) */ static ui(name: string): Resource { return fbIconOf('fb_ic_' + name); } } /** 动态资源名 → Resource(`$r` 需要字面量,这里用 switch 展开) */ function fbIconOf(name: string): Resource { switch (name) { case 'fb_ic_home': return $r('app.media.fb_ic_home'); case 'fb_ic_kin': return $r('app.media.fb_ic_kin'); case 'fb_ic_people': return $r('app.media.fb_ic_people'); case 'fb_ic_movies': return $r('app.media.fb_ic_movies'); case 'fb_ic_trips': return $r('app.media.fb_ic_trips'); case 'fb_ic_calendar': return $r('app.media.fb_ic_calendar'); case 'fb_ic_resources': return $r('app.media.fb_ic_resources'); case 'fb_ic_tasks': return $r('app.media.fb_ic_tasks'); case 'fb_ic_days': return $r('app.media.fb_ic_days'); case 'fb_ic_ledger': return $r('app.media.fb_ic_ledger'); case 'fb_ic_books': return $r('app.media.fb_ic_books'); case 'fb_ic_nav': return $r('app.media.fb_ic_nav'); case 'fb_ic_notes': return $r('app.media.fb_ic_notes'); case 'fb_ic_search': return $r('app.media.fb_ic_search'); case 'fb_ic_menu': return $r('app.media.fb_ic_menu'); case 'fb_ic_close': return $r('app.media.fb_ic_close'); case 'fb_ic_back': return $r('app.media.fb_ic_back'); case 'fb_ic_forward': return $r('app.media.fb_ic_forward'); case 'fb_ic_plus': return $r('app.media.fb_ic_plus'); case 'fb_ic_bell': return $r('app.media.fb_ic_bell'); case 'fb_ic_gear': return $r('app.media.fb_ic_gear'); case 'fb_ic_user': return $r('app.media.fb_ic_user'); case 'fb_ic_chart': return $r('app.media.fb_ic_chart'); case 'fb_ic_check': return $r('app.media.fb_ic_check'); case 'fb_ic_dot': return $r('app.media.fb_ic_dot'); case 'fb_ic_share': return $r('app.media.fb_ic_share'); case 'fb_ic_filter': return $r('app.media.fb_ic_filter'); case 'fb_ic_lock': return $r('app.media.fb_ic_lock'); case 'fb_ic_wallet': return $r('app.media.fb_ic_wallet'); case 'fb_ic_book': return $r('app.media.fb_ic_book'); case 'fb_ic_edit': return $r('app.media.fb_ic_edit'); case 'fb_ic_trash': return $r('app.media.fb_ic_trash'); default: return $r('app.media.fb_ic_dot'); } }