export type BrandKitLogoVariant = "primary" | "secondary" | "mark"; export type BrandKitLogo = { id: string; name: string; variant: BrandKitLogoVariant; usage: string; source: "upload" | "moteva-mark"; dataUrl?: string; }; export type BrandKitColor = { id: string; name: string; hex: string; usage: string; }; export type BrandKitColorGroup = { id: string; name: string; colors: BrandKitColor[]; }; export type BrandKitReferenceImage = { id: string; name: string; description: string; source: "upload" | "moteva-board"; dataUrl?: string; }; export type BrandKitFontFile = { name: string; format: string; size: number; dataUrl: string; }; export type BrandKitTypography = { id: string; label: string; family: string; weight: string; sizes: string; description: string; fontFile?: BrandKitFontFile; }; export type BrandKit = { version: 1; id: string; name: string; brandName: string; tagline: string; summary: string; audience: string; positioning: string; personality: string; voice: { traits: string; guidance: string; sample: string; }; logos: BrandKitLogo[]; coverImage?: BrandKitReferenceImage; colorGroups: BrandKitColorGroup[]; typography: BrandKitTypography[]; referenceImages: BrandKitReferenceImage[]; visual: { keywords: string; composition: string; imagery: string; dos: string; donts: string; prompt: string; }; applyToNewProjects: boolean; createdAt: string; updatedAt: string; }; export type BrandKitLocale = "zh-CN" | "en-US"; export function createBrandKit(name: string, locale: BrandKitLocale = "zh-CN"): BrandKit { const now = new Date().toISOString(); return { version: 1, id: createBrandKitId("brand-kit"), name, brandName: "", tagline: "", summary: "", audience: "", positioning: "", personality: "", voice: { traits: "", guidance: "", sample: "" }, logos: [], coverImage: undefined, colorGroups: createDefaultColorGroups(locale), typography: createDefaultTypography(locale), referenceImages: [], visual: { keywords: "", composition: "", imagery: "", dos: "", donts: "", prompt: "" }, applyToNewProjects: false, createdAt: now, updatedAt: now }; } export function createMotevaBrandKitExample(locale: BrandKitLocale): BrandKit { const kit = createBrandKit(locale === "zh-CN" ? "Moteva 核心品牌" : "Moteva Core Brand", locale); const chinese = locale === "zh-CN"; return { ...kit, id: "moteva-brand-kit-example", brandName: "Moteva", tagline: chinese ? "让设计进入流动状态" : "Keep every idea in motion", summary: chinese ? "Moteva 是面向创意团队与独立创作者的 AI 设计伙伴,把模糊想法转化为清晰、可继续编辑的视觉成果。" : "Moteva is an AI design partner for creative teams and independent makers, turning early ideas into clear, editable visual work.", audience: chinese ? "需要快速探索方向、同时重视品牌一致性的设计师、市场团队与创业者。" : "Designers, marketing teams, and founders who need to explore quickly without losing brand consistency.", positioning: chinese ? "不是一次性的图片生成器,而是能够理解上下文、延续判断并共同完成设计的工作空间。" : "Not a one-off image generator, but a workspace that understands context, carries decisions forward, and helps finish the design.", personality: chinese ? "清晰、敏锐、克制、协作、持续进化" : "Clear, perceptive, restrained, collaborative, evolving", voice: { traits: chinese ? "直接但不生硬;专业但不炫技;主动给出下一步" : "Direct, never abrupt; expert, never showy; always offers a useful next step", guidance: chinese ? "先说结论,再补充判断依据。使用具体动词和可执行建议,避免空泛口号、夸张承诺与过度技术化表达。" : "Lead with the outcome, then add the reasoning. Use concrete verbs and actionable guidance. Avoid vague slogans, inflated claims, and needless technical language.", sample: chinese ? "把复杂的设计问题,变成下一步可执行的选择。" : "Turn a complex design problem into the next clear decision." }, logos: [ { id: "moteva-example-logo", name: chinese ? "Moteva 主标志" : "Moteva primary mark", variant: "primary", usage: chinese ? "用于产品、品牌页与浅色背景;四周保留一个标志宽度的安全空间。" : "Use across product and brand surfaces on light backgrounds. Keep one mark-width of clear space.", source: "moteva-mark" } ], coverImage: { id: "moteva-example-cover", name: chinese ? "Moteva 品牌封面" : "Moteva brand cover", description: chinese ? "用于品牌套件列表的识别封面。" : "Recognition cover for the Brand Kit library.", source: "moteva-board" }, colorGroups: [ createColorGroup("primary", chinese ? "主色" : "Primary", [ createColor("ink", chinese ? "墨黑" : "Ink", "#0B0B0C", chinese ? "主文字、品牌标志、关键动作" : "Primary text, brand mark, and key actions") ]), createColorGroup("secondary", chinese ? "辅助色" : "Secondary", [ createColor("action", chinese ? "行动蓝" : "Action Blue", "#2F66F6", chinese ? "链接、焦点与高价值交互" : "Links, focus, and high-value interactions") ]), createColorGroup("accent", chinese ? "强调色" : "Accent", [ createColor("signal", chinese ? "信号青柠" : "Signal Lime", "#DDFB70", chinese ? "智能能力、完成状态与少量强调" : "Intelligence cues, completion, and sparing emphasis") ]), createColorGroup("neutral", chinese ? "中性色" : "Neutral", [ createColor("paper", chinese ? "纸白" : "Paper", "#F7F7F5", chinese ? "画布、留白与安静背景" : "Canvas, negative space, and quiet backgrounds"), createColor("soft-gray", chinese ? "雾灰" : "Mist", "#E8E9E5", chinese ? "分隔、次级表面与安静层级" : "Dividers, secondary surfaces, and quiet hierarchy") ]) ], typography: [ createTypography(chinese ? "标题字体" : "Heading type", "Avenir Next", "700", "28 / 40 / 56", chinese ? "标题与关键结论,使用紧凑层级和充足留白。" : "Headlines and key conclusions with compact hierarchy and generous space."), createTypography(chinese ? "正文字体" : "Body type", "Inter", "500", "12 / 14 / 16 / 20", chinese ? "产品界面、正文与说明,优先保证清晰阅读。" : "Product UI, body copy, and guidance with clarity first."), createTypography(chinese ? "强调字体" : "Accent type", "IBM Plex Mono", "500", "11 / 12", chinese ? "模型名称、参数与系统状态,少量使用。" : "Model names, parameters, and system states, used sparingly.") ], referenceImages: [ { id: "moteva-example-reference", name: chinese ? "Moteva 产品视觉" : "Moteva product visual", description: chinese ? "高留白的真实设计画布;黑白作为结构,信号青柠只标记智能能力和关键状态。" : "A spacious real design canvas. Black and white provide structure; signal lime marks intelligence and key states only.", source: "moteva-board" } ], visual: { keywords: chinese ? "精准、流动、克制、技术感、人性化" : "Precise, fluid, restrained, technical, human", composition: chinese ? "用一个明确主视觉建立重心;辅助信息按严格网格排列。先用尺度、留白和对齐创造层级,再考虑装饰。" : "Build focus around one decisive visual moment. Arrange supporting information on a disciplined grid. Create hierarchy with scale, space, and alignment before decoration.", imagery: chinese ? "展示真实设计成果和可检查的细节。图像保持清晰、自然、具有方向性;避免只提供氛围而无法看清主体。" : "Show real design outcomes and inspectable details. Imagery should feel clear, natural, and directed, never atmospheric at the expense of the subject.", dos: chinese ? "使用黑白基底;把信号青柠用于智能能力提示;保持内容优先;让动效解释状态变化。" : "Use a black-and-white foundation; reserve signal lime for intelligence cues; keep content first; let motion explain state changes.", donts: chinese ? "避免装饰性渐变、漂浮光球、泛化图库人物、过圆卡片和无意义的营销式大标题。不要用颜色替代清晰的信息层级。" : "Avoid decorative gradients, floating glow shapes, generic stock people, overly rounded cards, and empty marketing-scale headlines. Do not use color in place of hierarchy.", prompt: chinese ? "为 Moteva 创建设计:以清晰、克制、持续流动为核心。使用墨黑与纸白作为基底,信号青柠仅作少量智能提示;突出一个主视觉,以严格网格、充足留白和可检查的真实细节保持专业感。" : "Create a design for Moteva around clarity, restraint, and continuous motion. Use ink and paper as the foundation with signal lime only for intelligence cues. Establish one visual focus, then use a disciplined grid, generous space, and inspectable real detail." }, applyToNewProjects: true }; } export function cloneBrandKit(source: BrandKit, name: string): BrandKit { const now = new Date().toISOString(); return { ...source, id: createBrandKitId("brand-kit"), name, logos: source.logos.map((logo) => ({ ...logo, id: createBrandKitId("logo") })), coverImage: source.coverImage ? { ...source.coverImage, id: createBrandKitId("cover") } : undefined, colorGroups: source.colorGroups.map((group) => ({ ...group, id: createBrandKitId("color-group"), colors: group.colors.map((color) => ({ ...color, id: createBrandKitId("color") })) })), typography: source.typography.map((item) => ({ ...item, id: createBrandKitId("type"), fontFile: item.fontFile ? { ...item.fontFile } : undefined })), referenceImages: source.referenceImages.map((image) => ({ ...image, id: createBrandKitId("reference") })), applyToNewProjects: false, createdAt: now, updatedAt: now }; } export function brandKitCompletion(kit: BrandKit) { const foundations = [kit.brandName, kit.summary, kit.audience, kit.positioning, kit.personality].filter(hasText).length / 5; const voice = [kit.voice.traits, kit.voice.guidance, kit.voice.sample].filter(hasText).length / 3; const logo = kit.logos.length > 0 ? 1 : 0; const colors = Math.min(brandKitColors(kit).length / 4, 1); const typography = Math.min(kit.typography.filter((item) => hasText(item.family) || Boolean(item.fontFile)).length / 3, 1); const visual = [kit.visual.keywords, kit.visual.composition, kit.visual.imagery, kit.visual.dos, kit.visual.donts, kit.visual.prompt].filter(hasText).length / 6; return Math.round((foundations * 0.28 + voice * 0.13 + logo * 0.13 + colors * 0.14 + typography * 0.14 + visual * 0.18) * 100); } export function brandKitPrompt(kit: BrandKit) { const sections = [ promptSection("Brand", [kit.brandName, kit.tagline, kit.summary]), promptSection("Audience", [kit.audience]), promptSection("Positioning and personality", [kit.positioning, kit.personality]), promptSection("Voice", [kit.voice.traits, kit.voice.guidance, kit.voice.sample]), promptSection( "Logo rules", kit.logos.map((logo) => [logo.name, logo.usage].filter(hasText).join(": ")) ), promptSection("Brand cover", kit.coverImage ? [kit.coverImage.name, kit.coverImage.description] : []), promptSection( "Color system", kit.colorGroups.flatMap((group) => group.colors.map((color) => `${group.name} / ${color.name || "Color"} ${color.hex}${color.usage ? `: ${color.usage}` : ""}`)) ), promptSection( "Typography", kit.typography.map((item) => [item.label, item.family, item.fontFile?.name ?? "", item.weight, item.sizes, item.description].filter(hasText).join(" | ")) ), promptSection( "Reference images", kit.referenceImages.map((image) => [image.name, image.description].filter(hasText).join(": ")) ), promptSection("Visual direction", [kit.visual.keywords, kit.visual.composition, kit.visual.imagery]), promptSection("Use", [kit.visual.dos]), promptSection("Avoid", [kit.visual.donts]), promptSection("Reusable direction", [kit.visual.prompt]) ].filter(hasText); return [`Use this Brand Kit as a binding creative system for the project: ${kit.name}.`, ...sections].join("\n\n"); } export function createBrandKitColor(): BrandKitColor { return createColor(createBrandKitId("color"), "", "#111111", ""); } export function createBrandKitColorGroup(name: string): BrandKitColorGroup { return createColorGroup(createBrandKitId("color-group"), name, []); } export function createBrandKitTypography(label: string): BrandKitTypography { return createTypography(label); } export function createBrandKitLogo(file: File, dataUrl: string): BrandKitLogo { return { id: createBrandKitId("logo"), name: file.name.replace(/\.[^.]+$/, ""), variant: "primary", usage: "", source: "upload", dataUrl }; } export function createBrandKitFontFile(file: File, dataUrl: string): BrandKitFontFile { const extension = file.name.split(".").pop()?.toUpperCase() || "FONT"; return { name: file.name, format: extension, size: file.size, dataUrl }; } export function createBrandKitReferenceImage(file: File, dataUrl: string): BrandKitReferenceImage { return { id: createBrandKitId("reference"), name: file.name.replace(/\.[^.]+$/, ""), description: "", source: "upload", dataUrl }; } export function withBrandKitDefaults(kit: BrandKit): BrandKit { const referenceImages = (kit as BrandKit & { referenceImages?: unknown }).referenceImages; const coverImage = (kit as BrandKit & { coverImage?: unknown }).coverImage; const raw = kit as BrandKit & { colorGroups?: unknown; colors?: unknown }; const colorGroups = Array.isArray(raw.colorGroups) ? (raw.colorGroups as BrandKitColorGroup[]) : Array.isArray(raw.colors) && raw.colors.length > 0 ? [createColorGroup("migrated-colors", "Colors", raw.colors as BrandKitColor[])] : []; return { ...kit, coverImage: coverImage && typeof coverImage === "object" ? (coverImage as BrandKitReferenceImage) : undefined, colorGroups, typography: kit.typography.map(normalizeTypography), referenceImages: Array.isArray(referenceImages) ? (referenceImages as BrandKitReferenceImage[]) : [], applyToNewProjects: kit.applyToNewProjects === true }; } export function brandKitColors(kit: BrandKit) { return kit.colorGroups.flatMap((group) => group.colors); } export function isBrandKit(value: unknown): value is BrandKit { if (!isRecord(value)) return false; const candidate = value as Partial; const voice = candidate.voice; const visual = candidate.visual; const legacyColors = (candidate as Partial & { colors?: unknown }).colors; return ( candidate.version === 1 && typeof candidate.id === "string" && typeof candidate.name === "string" && typeof candidate.brandName === "string" && typeof candidate.tagline === "string" && typeof candidate.summary === "string" && typeof candidate.audience === "string" && typeof candidate.positioning === "string" && typeof candidate.personality === "string" && isRecord(voice) && typeof voice.traits === "string" && typeof voice.guidance === "string" && typeof voice.sample === "string" && Array.isArray(candidate.logos) && candidate.logos.every(isStoredLogo) && (candidate.coverImage === undefined || isStoredReferenceImage(candidate.coverImage)) && (Array.isArray(candidate.colorGroups) ? candidate.colorGroups.every(isStoredColorGroup) : Array.isArray(legacyColors) && legacyColors.every(isStoredColor)) && Array.isArray(candidate.typography) && candidate.typography.every(isStoredTypography) && (candidate.referenceImages === undefined || (Array.isArray(candidate.referenceImages) && candidate.referenceImages.every(isStoredReferenceImage))) && isRecord(visual) && typeof visual.keywords === "string" && typeof visual.composition === "string" && typeof visual.imagery === "string" && typeof visual.dos === "string" && typeof visual.donts === "string" && typeof visual.prompt === "string" && typeof candidate.createdAt === "string" && typeof candidate.updatedAt === "string" ); } function isStoredLogo(value: unknown) { return ( isRecord(value) && typeof value.id === "string" && typeof value.name === "string" && typeof value.usage === "string" && (value.variant === "primary" || value.variant === "secondary" || value.variant === "mark") && (value.source === "upload" || value.source === "moteva-mark") && (value.dataUrl === undefined || typeof value.dataUrl === "string") ); } function isStoredColorGroup(value: unknown) { return isRecord(value) && typeof value.id === "string" && typeof value.name === "string" && Array.isArray(value.colors) && value.colors.every(isStoredColor); } function isStoredColor(value: unknown) { return isRecord(value) && typeof value.id === "string" && typeof value.name === "string" && typeof value.hex === "string" && typeof value.usage === "string"; } function isStoredTypography(value: unknown) { return ( isRecord(value) && typeof value.id === "string" && typeof value.family === "string" && typeof value.weight === "string" && (value.label === undefined || typeof value.label === "string") && (value.sizes === undefined || typeof value.sizes === "string") && (value.description === undefined || typeof value.description === "string") && (value.fontFile === undefined || isStoredFontFile(value.fontFile)) ); } function isStoredReferenceImage(value: unknown) { return ( isRecord(value) && typeof value.id === "string" && typeof value.name === "string" && typeof value.description === "string" && (value.source === "upload" || value.source === "moteva-board") && (value.dataUrl === undefined || typeof value.dataUrl === "string") ); } function isStoredFontFile(value: unknown) { return ( isRecord(value) && typeof value.name === "string" && typeof value.format === "string" && typeof value.size === "number" && Number.isFinite(value.size) && value.size >= 0 && typeof value.dataUrl === "string" ); } function isRecord(value: unknown): value is Record { return Boolean(value) && typeof value === "object"; } function createBrandKitId(prefix: string) { const random = typeof crypto !== "undefined" && "randomUUID" in crypto ? crypto.randomUUID() : `${Date.now()}-${Math.random().toString(16).slice(2)}`; return `${prefix}-${random}`; } function createColor(id: string, name: string, hex: string, usage: string): BrandKitColor { return { id, name, hex, usage }; } function createColorGroup(id: string, name: string, colors: BrandKitColor[]): BrandKitColorGroup { return { id, name, colors }; } function createTypography(label: string, family = "", weight = "", sizes = "", description = ""): BrandKitTypography { return { id: createBrandKitId("type"), label, family, weight, sizes, description }; } function createDefaultColorGroups(locale: BrandKitLocale) { const chinese = locale === "zh-CN"; return [ createBrandKitColorGroup(chinese ? "主色" : "Primary"), createBrandKitColorGroup(chinese ? "辅助色" : "Secondary"), createBrandKitColorGroup(chinese ? "强调色" : "Accent"), createBrandKitColorGroup(chinese ? "中性色" : "Neutral") ]; } function createDefaultTypography(locale: BrandKitLocale) { const chinese = locale === "zh-CN"; return [ createBrandKitTypography(chinese ? "标题字体" : "Heading type"), createBrandKitTypography(chinese ? "正文字体" : "Body type"), createBrandKitTypography(chinese ? "强调字体" : "Accent type") ]; } function normalizeTypography(item: BrandKitTypography) { const legacy = item as BrandKitTypography & { role?: string; usage?: string; sizes?: string; description?: string; label?: string }; const fallbackLabels: Record = { heading: "Heading type", body: "Body type", accent: "Accent type" }; return { ...item, label: legacy.label || fallbackLabels[legacy.role ?? ""] || "Typography", sizes: legacy.sizes || "", description: legacy.description || legacy.usage || "" }; } function promptSection(title: string, values: string[]) { const content = values.map((value) => value.trim()).filter(Boolean); return content.length > 0 ? `${title}:\n${content.join("\n")}` : ""; } function hasText(value: string) { return value.trim().length > 0; }