@nebutra/brand 2.0.0 → 3.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -77,7 +77,7 @@ const accent = colors.accent[500]; // "#0BF1C3" 云毓青
77
77
  const gradient = colors.gradient.primary; // "linear-gradient(135deg, #0033FE 0%, #00A2E9 50%, #0BF1C3 100%)"
78
78
 
79
79
  // Typography
80
- const fontCn = typography.fontFamily.cn; // Noto Sans SC
80
+ const fontCn = typography.fontFamily.cn; // MiSans
81
81
  const fontEn = typography.fontFamily.en; // Geist
82
82
  ```
83
83
 
@@ -169,10 +169,13 @@ linear-gradient(135deg, #0033FE 0%, #00A2E9 50%, #0BF1C3 100%)
169
169
 
170
170
  ## Typography (品牌字体)
171
171
 
172
- ### 中文 - Noto Sans SC
172
+ ### 中文 - MiSans
173
173
  - Light (300) / Regular (400) / Medium (500) / DemiBold (600) / Bold (700)
174
174
 
175
- ### 英文 - Geist
175
+ ### 英文标题 - DM Sans
176
+ - 标题 500(`--font-weight-heading`),可变字体 100–1000
177
+
178
+ ### 英文正文与界面 - Geist
176
179
  - Regular (400) / Medium (500) / SemiBold (600) / Bold (700)
177
180
 
178
181
  ## Logo Usage Rules (标志使用规范)
@@ -22,7 +22,6 @@ var brand = {
22
22
  api: "api.nebutra.com",
23
23
  auth: "auth.nebutra.com",
24
24
  sso: "sso.nebutra.com",
25
- docs: "docs.nebutra.com",
26
25
  studio: "studio.nebutra.com",
27
26
  cdn: "cdn.nebutra.com",
28
27
  router: "router.nebutra.com",
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/metadata.ts","../../src/metadata-helpers.ts","../../src/components/Logo.tsx"],"sourcesContent":["/**\n * Nebutra Brand Metadata\n * Central source of truth for brand identity\n *\n * AUTO-GENERATED by `pnpm brand:apply` from `scripts/brand-types.ts`\n * (DEFAULT_BRAND) or a sibling `brand.config.ts` if present. Do NOT\n * edit by hand — your edits will be wiped on the next brand:apply.\n *\n * To rebrand, edit DEFAULT_BRAND in scripts/brand-types.ts and run\n * `pnpm brand:apply`. To extend the shape, add a field to BrandConfig,\n * set its value in DEFAULT_BRAND, then read it from this writer.\n *\n * Pipeline guard: tests/architecture/brand-metadata-drift.test.ts\n * asserts `metadata.ts` is byte-identical to what brand:apply emits.\n */\n\nexport const brand = {\n name: \"Nebutra\",\n nameCn: \"云毓智能\",\n nameFull: \"无锡云毓智能科技有限公司\",\n nameFullEn: \"Wuxi Nebutra Intelligence Technology Co., Ltd.\",\n\n tagline: \"Ship AI products, not boilerplate.\",\n taglineCn: \"AI原生·快速出海·即刻交付\",\n description:\n \"Production-ready Next.js monorepo template for AI SaaS products. Auth, billing, multi-tenancy, AI services, design system, and enterprise infrastructure — pre-configured.\",\n descriptionCn:\n \"面向AI创业者的一体化SaaS基础设施模板,覆盖认证、计费、多租户、AI服务与设计系统,开箱即产品\",\n\n story: {\n concept:\n \"Logo以首字母N的基础造型概念为主要设计框架,通过几何正负空间构建隐形'N',形成近似六边形的稳定结构\",\n colorMeaning: \"蓝绿渐变体现未来感与科技锋芒,'云'代表云端平台,'毓'寓意孕育与转化\",\n values: [\"AI Native\", \"Ship Fast\", \"Open by Default\", \"Global-Ready\", \"Enterprise-Grade\"],\n missionStatement:\n \"Help AI founders and SaaS teams go from idea to production 10x faster by providing the infrastructure layer they shouldn't have to build.\",\n },\n\n domains: {\n landing: \"nebutra.com\",\n app: \"app.nebutra.com\",\n api: \"api.nebutra.com\",\n auth: \"auth.nebutra.com\",\n sso: \"sso.nebutra.com\",\n docs: \"docs.nebutra.com\",\n studio: \"studio.nebutra.com\",\n cdn: \"cdn.nebutra.com\",\n router: \"router.nebutra.com\",\n forge: \"forge.nebutra.com\",\n design: \"design.nebutra.com\",\n status: \"status.nebutra.com\",\n open: \"open.nebutra.com\",\n admin: \"admin.nebutra.com\",\n analytics: \"analytics.nebutra.com\",\n pebble: \"pebble.nebutra.com\",\n carina: \"carina.nebutra.com\",\n kuanlan: \"kuanlan.nebutra.com\",\n origin: \"origin.nebutra.com\",\n },\n\n social: {\n twitter: \"https://twitter.com/nebutra\",\n github: \"https://github.com/nebutra\",\n discord: \"https://discord.gg/nebutra\",\n linkedin: \"https://linkedin.com/company/nebutra\",\n },\n} as const;\n\n/**\n * Brand Colors — full VI palette + brand gradients + semantic colors.\n * Aligned with @nebutra/design-tokens DTCG SSOT (tokens/core.json).\n */\nexport const colors = {\n primary: {\n \"50\": \"#f0f4ff\",\n \"100\": \"#dbe4ff\",\n \"200\": \"#bac8ff\",\n \"300\": \"#91a7ff\",\n \"400\": \"#5c7cfa\",\n \"500\": \"#0033FE\",\n \"600\": \"#002ad4\",\n \"700\": \"#0021ab\",\n \"800\": \"#001882\",\n \"900\": \"#000f59\",\n \"950\": \"#000830\",\n },\n accent: {\n \"50\": \"#e6fff8\",\n \"100\": \"#b3ffec\",\n \"200\": \"#80ffe0\",\n \"300\": \"#4dfcd4\",\n \"400\": \"#1af7c8\",\n \"500\": \"#0BF1C3\",\n \"600\": \"#09c9a3\",\n \"700\": \"#07a183\",\n \"800\": \"#057963\",\n \"900\": \"#035143\",\n \"950\": \"#012923\",\n },\n neutral: {\n \"0\": \"#ffffff\",\n \"50\": \"#f8fafc\",\n \"100\": \"#f1f5f9\",\n \"200\": \"#e2e8f0\",\n \"300\": \"#cbd5e1\",\n \"400\": \"#94a3b8\",\n \"500\": \"#64748b\",\n \"600\": \"#475569\",\n \"700\": \"#334155\",\n \"800\": \"#1e293b\",\n \"900\": \"#0f172a\",\n \"950\": \"#020617\",\n },\n white: \"#FFFFFF\",\n black: \"#000000\",\n gradient: {\n primary: \"linear-gradient(135deg, #0033FE 0%, #00A2E9 50%, #0BF1C3 100%)\",\n primaryReverse: \"linear-gradient(135deg, #0BF1C3 0%, #00A2E9 50%, #0033FE 100%)\",\n primaryVertical: \"linear-gradient(180deg, #0033FE 0%, #00A2E9 50%, #0BF1C3 100%)\",\n primaryRadial: \"radial-gradient(circle, #0BF1C3 0%, #00A2E9 50%, #0033FE 100%)\",\n },\n p3Overrides: {\n primary500: \"color(display-p3 0.03 0.19 0.99)\",\n accent500: \"color(display-p3 0.07 0.94 0.79)\",\n },\n success: \"#22c55e\",\n warning: \"#f59e0b\",\n error: \"#ef4444\",\n info: \"#0033FE\",\n} as const;\n\n/**\n * Typography — \"Precision Stack\" (Geist + Noto Sans SC + Geist Mono).\n */\nexport const typography = {\n fontFamily: {\n en: '\"Geist\", -apple-system, BlinkMacSystemFont, \"Segoe UI\", system-ui, sans-serif',\n cn: '\"Noto Sans SC\", \"PingFang SC\", \"Microsoft YaHei\", sans-serif',\n sans: '\"Geist\", \"Noto Sans SC\", \"PingFang SC\", \"Microsoft YaHei\", -apple-system, BlinkMacSystemFont, system-ui, sans-serif',\n mono: '\"Geist Mono\", ui-monospace, SFMono-Regular, \"SF Mono\", Menlo, monospace',\n display: '\"Geist\", \"Noto Sans SC\", sans-serif',\n heading: '\"Geist\", \"Noto Sans SC\", sans-serif',\n brandPrint: '\"Noto Sans SC\", \"PingFang SC\", sans-serif',\n },\n cssVars: {\n sans: \"var(--font-geist-sans)\",\n mono: \"var(--font-geist-mono)\",\n cnSans: \"var(--font-noto-sc)\",\n },\n fontWeight: {\n thin: 100,\n extraLight: 200,\n light: 300,\n normal: 400,\n medium: 500,\n semibold: 600,\n bold: 700,\n extraBold: 800,\n black: 900,\n },\n letterSpacing: {\n display: \"-0.04em\",\n heading: \"-0.03em\",\n subheading: \"-0.02em\",\n body: \"0em\",\n caption: \"0.01em\",\n mono: \"0em\",\n },\n lineHeight: {\n none: 1,\n tight: 1.2,\n snug: 1.4,\n normal: 1.6,\n relaxed: 1.75,\n loose: 2,\n },\n} as const;\n\n/**\n * Logo asset paths (relative to package), dual-edition structure:\n * - classic (v1.0): preferred for UX / product surfaces\n * - compliant (v2.0): required for commercial / legal contexts\n */\nexport const logoAssets = {\n classic: {\n default: \"assets/logo/logo-color.svg\",\n color: \"assets/logo/logo-color.svg\",\n inverse: \"assets/logo/logo-inverse.svg\",\n mono: \"assets/logo/logo-mono.svg\",\n en: \"assets/logo/logo-en.svg\",\n zh: \"assets/logo/logo-zh.svg\",\n zhEn: \"assets/logo/logo-zh-en.svg\",\n horizontalEn: \"assets/logo/logo-horizontal-en.svg\",\n horizontalZh: \"assets/logo/logo-horizontal-zh.svg\",\n verticalEn: \"assets/logo/logo-vertical-en.svg\",\n verticalZh: \"assets/logo/logo-vertical-zh.svg\",\n },\n compliant: {\n default: \"assets/logo-compliant/logo-color.svg\",\n color: \"assets/logo-compliant/logo-color.svg\",\n inverse: \"assets/logo-compliant/logo-inverse.svg\",\n mono: \"assets/logo-compliant/logo-mono.svg\",\n en: \"assets/logo-compliant/logo-en.svg\",\n zh: \"assets/logo-compliant/logo-zh.svg\",\n zhEn: \"assets/logo-compliant/logo-zh-en.svg\",\n horizontalEn: \"assets/logo-compliant/logo-horizontal-en.svg\",\n horizontalZh: \"assets/logo-compliant/logo-horizontal-zh.svg\",\n verticalEn: \"assets/logo-compliant/logo-vertical-en.svg\",\n verticalZh: \"assets/logo-compliant/logo-vertical-zh.svg\",\n horizontalEnMono: \"assets/logo-compliant/logo-horizontal-en-mono.svg\",\n horizontalZhMono: \"assets/logo-compliant/logo-horizontal-zh-mono.svg\",\n verticalEnMono: \"assets/logo-compliant/logo-vertical-en-mono.svg\",\n verticalZhMono: \"assets/logo-compliant/logo-vertical-zh-mono.svg\",\n },\n} as const;\n\n/**\n * Font asset paths (relative to package).\n */\nexport const fontAssets = {\n poppins: {\n thin: \"assets/fonts/poppins/Poppins-Thin.otf\",\n thinItalic: \"assets/fonts/poppins/Poppins-ThinItalic.otf\",\n extraLight: \"assets/fonts/poppins/Poppins-ExtraLight.otf\",\n extraLightItalic: \"assets/fonts/poppins/Poppins-ExtraLightItalic.otf\",\n light: \"assets/fonts/poppins/Poppins-Light.otf\",\n lightItalic: \"assets/fonts/poppins/Poppins-LightItalic.otf\",\n regular: \"assets/fonts/poppins/Poppins-Regular.otf\",\n italic: \"assets/fonts/poppins/Poppins-Italic.otf\",\n medium: \"assets/fonts/poppins/Poppins-Medium.otf\",\n mediumItalic: \"assets/fonts/poppins/Poppins-MediumItalic.otf\",\n semiBold: \"assets/fonts/poppins/Poppins-SemiBold.otf\",\n semiBoldItalic: \"assets/fonts/poppins/Poppins-SemiBoldItalic.otf\",\n bold: \"assets/fonts/poppins/Poppins-Bold.otf\",\n boldItalic: \"assets/fonts/poppins/Poppins-BoldItalic.otf\",\n extraBold: \"assets/fonts/poppins/Poppins-ExtraBold.otf\",\n extraBoldItalic: \"assets/fonts/poppins/Poppins-ExtraBoldItalic.otf\",\n black: \"assets/fonts/poppins/Poppins-Black.otf\",\n blackItalic: \"assets/fonts/poppins/Poppins-BlackItalic.otf\",\n },\n} as const;\n\n/**\n * Favicon assets.\n */\nexport const faviconAssets = {\n ico: \"assets/favicon/favicon.ico\",\n svg: \"assets/favicon/favicon.svg\",\n apple: \"assets/favicon/apple-touch-icon.png\",\n android192: \"assets/favicon/android-chrome-192x192.png\",\n android512: \"assets/favicon/android-chrome-512x512.png\",\n} as const;\n\n/**\n * OG Image dimensions.\n */\nexport const ogImageDimensions = {\n default: {\n width: 1200,\n height: 630,\n },\n twitter: {\n width: 1200,\n height: 600,\n },\n square: {\n width: 1200,\n height: 1200,\n },\n} as const;\n\nexport type BrandColors = typeof colors;\nexport type BrandTypography = typeof typography;\nexport type LogoAssets = typeof logoAssets;\n","/**\n * @nebutra/brand — metadata-helpers\n *\n * Pure functions deriving Next.js Metadata, JSON-LD objects, PWA manifest,\n * and feed channel headers from the single `brand` + `colors` SSOT.\n *\n * Zero hardcoded brand-name literals — every string is derived from `brand`\n * or `colors`. The arch drift guard `tests/architecture/brand-metadata-drift.test.ts`\n * asserts this invariant at CI time.\n *\n * NOTE: `next` is a TYPE-ONLY import here. The package declares `next` as an\n * optional peer dep + devDep (catalog:). skipLibCheck:true is set in tsconfig.json.\n * Adding `import \"server-only\"` is intentionally omitted: this file is consumed\n * by both server and client render paths in tests; the CALLER must enforce\n * server-only boundaries if needed.\n *\n * NOTE: getSiteUrl(service) in this module is DISTINCT from the zero-param\n * getSiteUrl() in apps/landing/src/lib/seo/site-routes.ts — different\n * modules, no collision. The local one remains the landing URL util.\n */\n\nimport type { Metadata, MetadataRoute } from \"next\";\nimport { brand, colors } from \"./metadata\";\n\n/** Services that have a canonical domain in brand.domains */\nexport type BrandService = keyof typeof brand.domains;\n\n/** Absolute origin from brand.domains only (no env hijack). */\nexport function getBrandOrigin(service: BrandService): string {\n const host = brand.domains[service];\n if (!host?.trim()) {\n throw new Error(`brand.domains.${String(service)} is empty — run brand:apply`);\n }\n return `https://${host.replace(/^https?:\\/\\//, \"\").replace(/\\/+$/, \"\")}`;\n}\n\n/**\n * Public object on `nebutra-assets` via `cdn.nebutra.com`.\n * R2_PUBLIC_URL / NEXT_PUBLIC_R2_PUBLIC_URL may override the origin for dogfood.\n */\nexport function publicAssetUrl(key: string, base?: string): string {\n const origin = (\n base ||\n process.env.NEXT_PUBLIC_R2_PUBLIC_URL ||\n process.env.R2_PUBLIC_URL ||\n getBrandOrigin(\"cdn\")\n ).replace(/\\/+$/, \"\");\n const path = key.replace(/^\\/+/, \"\");\n if (!path || path.includes(\"..\") || path.includes(\"\\\\\")) {\n throw new Error(\"invalid_public_asset_key\");\n }\n return `${origin}/${path}`;\n}\n\n/**\n * Bare query flag that keeps a signed-in visitor on the marketing homepage\n * (`?home`, no value). Apex `/` without it is a product launcher.\n */\nexport const MARKETING_HOME_PARAM = \"home\";\n\n/**\n * Marketing homepage path. Always includes the `?home` skip — that is the\n * only signal landing honours. `getBrandOrigin(\"landing\")` is the launcher.\n *\n * `defaultLocale` is the landing path locale that lives on `/` (today `en`).\n * Pass it from `@nebutra/i18n` so brand does not import the i18n package.\n */\nexport function getMarketingHomePath(options?: {\n locale?: string;\n defaultLocale?: string;\n}): string {\n const defaultLocale = options?.defaultLocale?.trim() || \"en\";\n const locale = options?.locale?.trim();\n const path = locale && locale !== defaultLocale ? `/${locale}` : \"/\";\n return path === \"/\" ? `/?${MARKETING_HOME_PARAM}` : `${path}?${MARKETING_HOME_PARAM}`;\n}\n\n/** Absolute marketing homepage. Same contract as `getMarketingHomePath`. */\nexport function getMarketingHomeUrl(options?: { locale?: string; defaultLocale?: string }): string {\n const origin = getBrandOrigin(\"landing\");\n return new URL(getMarketingHomePath(options), `${origin}/`).toString();\n}\n\n/** Cookie parent domain from landing apex (e.g. `.example.com`). */\nexport function getBrandCookieDomain(): string {\n const apex = brand.domains.landing.replace(/^https?:\\/\\//, \"\").replace(/\\/+$/, \"\");\n if (!apex) return \"\";\n return apex.startsWith(\".\") ? apex : `.${apex}`;\n}\n\n/**\n * Canonical base URL. NEXT_PUBLIC_SITE_URL only overrides landing.\n */\nexport function getSiteUrl(service: BrandService = \"landing\"): string {\n if (service === \"landing\") {\n const envUrl = process.env[\"NEXT_PUBLIC_SITE_URL\"];\n if (envUrl && envUrl.trim() !== \"\") {\n return envUrl.trim().replace(/\\/+$/, \"\");\n }\n }\n return getBrandOrigin(service);\n}\n\n/** Production multi-app public URL map (dogfood brand.domains). */\nexport function getBrandPublicUrls() {\n return {\n siteUrl: getBrandOrigin(\"landing\"),\n appUrl: getBrandOrigin(\"app\"),\n apiUrl: getBrandOrigin(\"api\"),\n authUrl: getBrandOrigin(\"auth\"),\n ssoUrl: getBrandOrigin(\"sso\"),\n docsUrl: getBrandOrigin(\"docs\"),\n routerUrl: getBrandOrigin(\"router\"),\n forgeUrl: getBrandOrigin(\"forge\"),\n designUrl: getBrandOrigin(\"design\"),\n statusUrl: getBrandOrigin(\"status\"),\n openUrl: getBrandOrigin(\"open\"),\n studioUrl: getBrandOrigin(\"studio\"),\n cdnUrl: getBrandOrigin(\"cdn\"),\n analyticsUrl: getBrandOrigin(\"analytics\"),\n pebbleUrl: getBrandOrigin(\"pebble\"),\n carinaUrl: getBrandOrigin(\"carina\"),\n cookieDomain: getBrandCookieDomain(),\n } as const;\n}\n\n/**\n * Product mailbox derived from landing apex — rebrand-safe.\n * e.g. getBrandEmail(\"contact\") → contact@example.com\n */\nexport function getBrandEmail(localPart: string): string {\n const apex = brand.domains.landing\n .replace(/^https?:\\/\\//, \"\")\n .replace(/\\/+$/, \"\")\n .replace(/^www\\./, \"\");\n if (!apex) {\n throw new Error(\"brand.domains.landing is empty — run brand:apply\");\n }\n const local = localPart.trim().replace(/@.*$/, \"\");\n if (!local) {\n throw new Error(\"getBrandEmail requires a non-empty local part\");\n }\n return `${local}@${apex}`;\n}\n\n/** Default transactional From header: `Brand <noreply@apex>`. */\nexport function getBrandMailFrom(): string {\n return `${brand.name} <${getBrandEmail(\"noreply\")}>`;\n}\n\n// ─── Metadata ────────────────────────────────────────────────────────────────\n\nexport interface SiteMetadataOptions {\n readonly service: BrandService;\n readonly title?: string;\n readonly description?: string;\n}\n\n/**\n * Returns a partial Next.js Metadata object suitable for spreading into a\n * layout.tsx export const metadata. Callers may override any field.\n */\nexport function getSiteMetadata(opts: SiteMetadataOptions): Metadata {\n const siteUrl = getSiteUrl(opts.service);\n const title = opts.title ?? brand.name;\n const description = opts.description ?? brand.tagline;\n\n return {\n applicationName: brand.name,\n title: {\n default: title,\n template: `%s — ${brand.name}`,\n },\n description,\n metadataBase: new URL(siteUrl),\n authors: [{ name: brand.name, url: siteUrl }],\n creator: brand.name,\n publisher: brand.name,\n openGraph: {\n type: \"website\",\n siteName: brand.name,\n title,\n description,\n url: siteUrl,\n },\n twitter: {\n card: \"summary_large_image\",\n site: brand.social.twitter.replace(\"https://twitter.com/\", \"@\"),\n title,\n description,\n },\n };\n}\n\n// ─── JSON-LD types ─────────────────────────────────────────────────────────────\n\nexport interface OrganizationJsonLd {\n \"@context\": \"https://schema.org\";\n \"@type\": \"Organization\";\n \"@id\": string;\n name: string;\n alternateName: string;\n url: string;\n sameAs: string[];\n [key: string]: unknown;\n}\n\nexport interface WebSiteJsonLd {\n \"@context\": \"https://schema.org\";\n \"@type\": \"WebSite\";\n \"@id\": string;\n name: string;\n url: string;\n description: string;\n publisher: { \"@id\": string };\n inLanguage: string[];\n [key: string]: unknown;\n}\n\nexport interface SoftwareApplicationJsonLd {\n \"@context\": \"https://schema.org\";\n \"@type\": \"SoftwareApplication\";\n name: string;\n applicationCategory: string;\n url: string;\n author: { \"@id\": string };\n [key: string]: unknown;\n}\n\nexport interface FeedChannelMeta {\n title: string;\n link: string;\n description: string;\n feedUrl: string;\n language: string;\n}\n\n// ─── JSON-LD builders ────────────────────────────────────────────────────────\n\n/**\n * Base Organization JSON-LD — company-specific fields\n * (foundingDate, address, contactPoint) are intentionally omitted here.\n * Merge them at the call site to avoid hard-coding values here.\n */\nexport function buildOrganizationJsonLd(): OrganizationJsonLd {\n const siteUrl = getSiteUrl(\"landing\");\n const id = `${siteUrl}/#organization`;\n\n const sameAs = [\n brand.social.github,\n brand.social.twitter.replace(\"https://twitter.com/\", \"https://x.com/\"),\n brand.social.linkedin,\n brand.social.discord,\n ].filter((url) => url.trim() !== \"\");\n\n return {\n \"@context\": \"https://schema.org\",\n \"@type\": \"Organization\",\n \"@id\": id,\n name: brand.name,\n alternateName: brand.nameCn,\n url: siteUrl,\n sameAs,\n };\n}\n\nexport function buildWebSiteJsonLd(): WebSiteJsonLd {\n const siteUrl = getSiteUrl(\"landing\");\n const org = buildOrganizationJsonLd();\n\n return {\n \"@context\": \"https://schema.org\",\n \"@type\": \"WebSite\",\n \"@id\": `${siteUrl}/#website`,\n name: brand.name,\n url: siteUrl,\n description: brand.tagline,\n publisher: { \"@id\": org[\"@id\"] },\n inLanguage: [\"en\", \"zh\"],\n };\n}\n\nexport function buildSoftwareApplicationJsonLd(): SoftwareApplicationJsonLd {\n const org = buildOrganizationJsonLd();\n const githubPath = brand.social.github.replace(\"https://github.com/\", \"\");\n\n return {\n \"@context\": \"https://schema.org\",\n \"@type\": \"SoftwareApplication\",\n name: `${brand.name} Sailor`,\n applicationCategory: \"DeveloperApplication\",\n url: `https://github.com/${githubPath}`,\n author: { \"@id\": org[\"@id\"] },\n };\n}\n\n// ─── PWA Manifest ────────────────────────────────────────────────────────────\n\n/**\n * Returns a base PWA manifest derived from brand + colors.\n * background_color and theme_color use the palette's canonical hex values\n * (NOT CSS variables — service workers do not resolve var()).\n *\n * CONTRAST CAVEAT: theme_color = colors.primary[\"500\"] assumes a dark\n * background context. If a rebrand sets a light primary, update this value.\n */\nexport function buildPwaManifest(): MetadataRoute.Manifest {\n return {\n name: `${brand.name} — ${brand.tagline}`,\n short_name: brand.name,\n description: brand.description,\n start_url: \"/\",\n display: \"standalone\",\n background_color: colors.neutral[\"950\"],\n theme_color: colors.primary[\"500\"],\n orientation: \"portrait-primary\",\n scope: \"/\",\n lang: \"en\",\n categories: [\"business\", \"productivity\", \"utilities\"],\n icons: [\n {\n src: \"/favicon.ico\",\n sizes: \"any\",\n type: \"image/x-icon\",\n },\n {\n src: \"/android-chrome-192x192.png\",\n sizes: \"192x192\",\n type: \"image/png\",\n purpose: \"maskable\",\n },\n {\n src: \"/android-chrome-512x512.png\",\n sizes: \"512x512\",\n type: \"image/png\",\n purpose: \"maskable\",\n },\n {\n src: \"/android-chrome-192x192.png\",\n sizes: \"192x192\",\n type: \"image/png\",\n purpose: \"any\",\n },\n {\n src: \"/android-chrome-512x512.png\",\n sizes: \"512x512\",\n type: \"image/png\",\n purpose: \"any\",\n },\n ],\n };\n}\n\n// ─── Feed ────────────────────────────────────────────────────────────────────\n\n/**\n * Returns the shared channel-level fields for RSS and Atom feeds.\n * Item-level links are built at the call site using the version string.\n */\nexport function buildFeedChannelMeta(): FeedChannelMeta {\n const siteUrl = getSiteUrl(\"landing\");\n\n return {\n title: `${brand.name} Changelog`,\n link: `${siteUrl}/changelog`,\n description: brand.tagline,\n feedUrl: `${siteUrl}/api/changelog`,\n language: \"en-us\",\n };\n}\n","\"use client\";\n\nimport { brand } from \"../metadata\";\nimport { publicAssetUrl } from \"../metadata-helpers\";\n\nexport type LogoVariant =\n | \"color\"\n | \"inverse\"\n | \"mono\"\n | \"en\"\n | \"zh\"\n | \"zh-en\"\n | \"horizontal-en\"\n | \"horizontal-zh\"\n | \"vertical-en\"\n | \"vertical-zh\"\n | \"horizontal-en-mono\"\n | \"horizontal-zh-mono\"\n | \"vertical-en-mono\"\n | \"vertical-zh-mono\";\n\n/**\n * Logo edition: classic (v1.0) or compliant (v2.0)\n *\n * - classic: 经典版,\"毓\"字更美观,用于用户体验场景 (App/网站/产品)\n * - compliant: 合规版,\"毓\"字符合商标规范,用于商务场景 (法律/商标/正式文件)\n */\nexport type LogoEdition = \"classic\" | \"compliant\";\n\n/** Variants that only exist in the compliant edition */\nconst COMPLIANT_ONLY_VARIANTS: ReadonlySet<LogoVariant> = new Set([\n \"horizontal-en-mono\",\n \"horizontal-zh-mono\",\n \"vertical-en-mono\",\n \"vertical-zh-mono\",\n]);\n\nexport interface LogoProps {\n /**\n * Logo variant\n * @default \"en\" (Nebutra with English wordmark)\n */\n variant?: LogoVariant;\n\n /**\n * Logo edition\n * - \"classic\" (default): v1.0 经典版,更美观\n * - \"compliant\": v2.0 合规版,符合商标规范\n *\n * Mono combination variants automatically use compliant edition.\n * @default \"classic\"\n */\n edition?: LogoEdition;\n\n /**\n * Logo size (width in pixels)\n * @default 120\n */\n size?: number;\n\n /**\n * Custom className\n */\n className?: string;\n\n /**\n * Invert colors for dark backgrounds.\n * When true, black becomes white (useful for dark mode).\n *\n * Note: For pure white logomark, use variant=\"inverse\" instead.\n */\n inverted?: boolean;\n}\n\n/**\n * Resolve the CDN object for a logo variant + edition combination.\n * Mono combination variants always route to the compliant directory.\n * `public/` copies from `pnpm brand:sync` stay as seed, not consumption.\n */\nexport function logoPublicSrc(variant: LogoVariant, edition: LogoEdition): string {\n const dir =\n edition === \"compliant\" || COMPLIANT_ONLY_VARIANTS.has(variant)\n ? \"brand/logo-compliant\"\n : \"brand/logo\";\n return publicAssetUrl(`${dir}/logo-${variant}.svg`);\n}\n\n/**\n * Aspect ratios for different logo variants\n * width:height ratio (e.g. 5.25 means width is 5.25x height)\n */\nconst ASPECT_RATIOS: Record<LogoVariant, number> = {\n color: 1.07, // 535.71 x 500 ≈ 1.07\n inverse: 1.07, // 535.71 x 500 ≈ 1.07\n mono: 1.07, // Same as color/inverse\n en: 5.25, // 544.21 x 103.74 ≈ 5.25 (horizontal text)\n zh: 3.5, // Estimated for Chinese wordmark\n \"zh-en\": 4.0, // Estimated for bilingual\n \"horizontal-en\": 5.25, // Similar to \"en\"\n \"horizontal-zh\": 4.5, // Estimated\n \"vertical-en\": 0.8, // Taller than wide\n \"vertical-zh\": 0.8, // Taller than wide\n \"horizontal-en-mono\": 5.25, // Same as horizontal-en\n \"horizontal-zh-mono\": 4.5, // Same as horizontal-zh\n \"vertical-en-mono\": 0.8, // Same as vertical-en\n \"vertical-zh-mono\": 0.8, // Same as vertical-zh\n};\n\n/**\n * Nebutra Logo Component\n *\n * Renders the official Nebutra brand logo from SVG assets (fixed VI fills).\n * Browser src is `https://cdn.nebutra.com/brand/logo{,-compliant}/…`.\n * `pnpm brand:sync` still copies seed files into each app `public/`.\n *\n * For product chrome that must recolor with Brand Package skins (roles.brand),\n * prefer LogomarkSVG / LogoEnSVG with default `text-brand-mark` instead of\n * raster VI color SVGs or `text-primary` (CTA / roles.action).\n *\n * @example\n * ```tsx\n * // Logomark only (square icon)\n * <Logo variant=\"inverse\" size={40} />\n *\n * // Full logo with English wordmark\n * <Logo variant=\"en\" size={150} />\n *\n * // Chinese wordmark\n * <Logo variant=\"zh\" size={120} />\n * ```\n */\nexport function Logo({\n variant = \"en\",\n edition = \"classic\",\n size = 120,\n className,\n inverted = false,\n}: LogoProps) {\n const aspectRatio = ASPECT_RATIOS[variant];\n const height = Math.round(size / aspectRatio);\n const src = logoPublicSrc(variant, edition);\n\n // For dark backgrounds, invert black to white\n const filterStyle = inverted ? { filter: \"brightness(0) invert(1)\" } : {};\n\n return (\n <img\n src={src}\n alt={brand.name}\n width={size}\n height={height}\n className={className}\n style={{ width: size, height, ...filterStyle }}\n />\n );\n}\n\n/**\n * Logomark only (the abstract icon without text)\n *\n * This uses the \"color\" or \"inverse\" variant which is just the icon.\n *\n * @example\n * ```tsx\n * <Logomark variant=\"inverse\" size={32} />\n * ```\n */\nexport function Logomark({\n size = 32,\n className,\n variant = \"color\",\n edition = \"classic\",\n inverted = false,\n}: {\n size?: number;\n className?: string;\n variant?: \"color\" | \"inverse\" | \"mono\";\n edition?: LogoEdition;\n inverted?: boolean;\n}) {\n const src = logoPublicSrc(variant, edition);\n\n // For dark backgrounds, invert black to white (unless using \"inverse\" variant which is already white)\n const filterStyle =\n inverted && variant !== \"inverse\" ? { filter: \"brightness(0) invert(1)\" } : {};\n\n return (\n <img\n src={src}\n alt={brand.name}\n width={size}\n height={size}\n className={className}\n style={{ width: size, height: size, ...filterStyle }}\n />\n );\n}\n\n/**\n * Wordmark only (text \"Nebutra\" without the icon)\n *\n * Uses the \"en\" variant which has the full wordmark.\n * For icon + text, use Logo with variant=\"en\".\n */\nexport function Wordmark({\n size = 100,\n className,\n variant = \"en\",\n edition = \"classic\",\n inverted = false,\n}: {\n size?: number;\n className?: string;\n variant?: \"en\" | \"zh\" | \"zh-en\";\n edition?: LogoEdition;\n inverted?: boolean;\n}) {\n const aspectRatio = ASPECT_RATIOS[variant];\n const height = Math.round(size / aspectRatio);\n const src = logoPublicSrc(variant, edition);\n\n const filterStyle = inverted ? { filter: \"brightness(0) invert(1)\" } : {};\n\n return (\n <img\n src={src}\n alt={brand.name}\n width={size}\n height={height}\n className={className}\n style={{ width: size, height, ...filterStyle }}\n />\n );\n}\n\nexport default Logo;\n"],"mappings":";;;AAgBO,IAAM,QAAQ;AAAA,EACnB,MAAM;AAAA,EACN,QAAQ;AAAA,EACR,UAAU;AAAA,EACV,YAAY;AAAA,EAEZ,SAAS;AAAA,EACT,WAAW;AAAA,EACX,aACE;AAAA,EACF,eACE;AAAA,EAEF,OAAO;AAAA,IACL,SACE;AAAA,IACF,cAAc;AAAA,IACd,QAAQ,CAAC,aAAa,aAAa,mBAAmB,gBAAgB,kBAAkB;AAAA,IACxF,kBACE;AAAA,EACJ;AAAA,EAEA,SAAS;AAAA,IACP,SAAS;AAAA,IACT,KAAK;AAAA,IACL,KAAK;AAAA,IACL,MAAM;AAAA,IACN,KAAK;AAAA,IACL,MAAM;AAAA,IACN,QAAQ;AAAA,IACR,KAAK;AAAA,IACL,QAAQ;AAAA,IACR,OAAO;AAAA,IACP,QAAQ;AAAA,IACR,QAAQ;AAAA,IACR,MAAM;AAAA,IACN,OAAO;AAAA,IACP,WAAW;AAAA,IACX,QAAQ;AAAA,IACR,QAAQ;AAAA,IACR,SAAS;AAAA,IACT,QAAQ;AAAA,EACV;AAAA,EAEA,QAAQ;AAAA,IACN,SAAS;AAAA,IACT,QAAQ;AAAA,IACR,SAAS;AAAA,IACT,UAAU;AAAA,EACZ;AACF;;;ACtCO,SAAS,eAAe,SAA+B;AAC5D,QAAM,OAAO,MAAM,QAAQ,OAAO;AAClC,MAAI,CAAC,MAAM,KAAK,GAAG;AACjB,UAAM,IAAI,MAAM,iBAAiB,OAAO,OAAO,CAAC,kCAA6B;AAAA,EAC/E;AACA,SAAO,WAAW,KAAK,QAAQ,gBAAgB,EAAE,EAAE,QAAQ,QAAQ,EAAE,CAAC;AACxE;AAMO,SAAS,eAAe,KAAa,MAAuB;AACjE,QAAM,UACJ,QACA,QAAQ,IAAI,6BACZ,QAAQ,IAAI,iBACZ,eAAe,KAAK,GACpB,QAAQ,QAAQ,EAAE;AACpB,QAAM,OAAO,IAAI,QAAQ,QAAQ,EAAE;AACnC,MAAI,CAAC,QAAQ,KAAK,SAAS,IAAI,KAAK,KAAK,SAAS,IAAI,GAAG;AACvD,UAAM,IAAI,MAAM,0BAA0B;AAAA,EAC5C;AACA,SAAO,GAAG,MAAM,IAAI,IAAI;AAC1B;;;AC8FI;AApHJ,IAAM,0BAAoD,oBAAI,IAAI;AAAA,EAChE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF,CAAC;AA4CM,SAAS,cAAc,SAAsB,SAA8B;AAChF,QAAM,MACJ,YAAY,eAAe,wBAAwB,IAAI,OAAO,IAC1D,yBACA;AACN,SAAO,eAAe,GAAG,GAAG,SAAS,OAAO,MAAM;AACpD;AAMA,IAAM,gBAA6C;AAAA,EACjD,OAAO;AAAA;AAAA,EACP,SAAS;AAAA;AAAA,EACT,MAAM;AAAA;AAAA,EACN,IAAI;AAAA;AAAA,EACJ,IAAI;AAAA;AAAA,EACJ,SAAS;AAAA;AAAA,EACT,iBAAiB;AAAA;AAAA,EACjB,iBAAiB;AAAA;AAAA,EACjB,eAAe;AAAA;AAAA,EACf,eAAe;AAAA;AAAA,EACf,sBAAsB;AAAA;AAAA,EACtB,sBAAsB;AAAA;AAAA,EACtB,oBAAoB;AAAA;AAAA,EACpB,oBAAoB;AAAA;AACtB;AAyBO,SAAS,KAAK;AAAA,EACnB,UAAU;AAAA,EACV,UAAU;AAAA,EACV,OAAO;AAAA,EACP;AAAA,EACA,WAAW;AACb,GAAc;AACZ,QAAM,cAAc,cAAc,OAAO;AACzC,QAAM,SAAS,KAAK,MAAM,OAAO,WAAW;AAC5C,QAAM,MAAM,cAAc,SAAS,OAAO;AAG1C,QAAM,cAAc,WAAW,EAAE,QAAQ,0BAA0B,IAAI,CAAC;AAExE,SACE;AAAA,IAAC;AAAA;AAAA,MACC;AAAA,MACA,KAAK,MAAM;AAAA,MACX,OAAO;AAAA,MACP;AAAA,MACA;AAAA,MACA,OAAO,EAAE,OAAO,MAAM,QAAQ,GAAG,YAAY;AAAA;AAAA,EAC/C;AAEJ;AAYO,SAAS,SAAS;AAAA,EACvB,OAAO;AAAA,EACP;AAAA,EACA,UAAU;AAAA,EACV,UAAU;AAAA,EACV,WAAW;AACb,GAMG;AACD,QAAM,MAAM,cAAc,SAAS,OAAO;AAG1C,QAAM,cACJ,YAAY,YAAY,YAAY,EAAE,QAAQ,0BAA0B,IAAI,CAAC;AAE/E,SACE;AAAA,IAAC;AAAA;AAAA,MACC;AAAA,MACA,KAAK,MAAM;AAAA,MACX,OAAO;AAAA,MACP,QAAQ;AAAA,MACR;AAAA,MACA,OAAO,EAAE,OAAO,MAAM,QAAQ,MAAM,GAAG,YAAY;AAAA;AAAA,EACrD;AAEJ;AAQO,SAAS,SAAS;AAAA,EACvB,OAAO;AAAA,EACP;AAAA,EACA,UAAU;AAAA,EACV,UAAU;AAAA,EACV,WAAW;AACb,GAMG;AACD,QAAM,cAAc,cAAc,OAAO;AACzC,QAAM,SAAS,KAAK,MAAM,OAAO,WAAW;AAC5C,QAAM,MAAM,cAAc,SAAS,OAAO;AAE1C,QAAM,cAAc,WAAW,EAAE,QAAQ,0BAA0B,IAAI,CAAC;AAExE,SACE;AAAA,IAAC;AAAA;AAAA,MACC;AAAA,MACA,KAAK,MAAM;AAAA,MACX,OAAO;AAAA,MACP;AAAA,MACA;AAAA,MACA,OAAO,EAAE,OAAO,MAAM,QAAQ,GAAG,YAAY;AAAA;AAAA,EAC/C;AAEJ;AAEA,IAAO,eAAQ;","names":[]}
1
+ {"version":3,"sources":["../../src/metadata.ts","../../src/metadata-helpers.ts","../../src/components/Logo.tsx"],"sourcesContent":["/**\n * Nebutra Brand Metadata\n * Central source of truth for brand identity\n *\n * AUTO-GENERATED by `pnpm brand:apply` from `scripts/brand-types.ts`\n * (DEFAULT_BRAND) or a sibling `brand.config.ts` if present. Do NOT\n * edit by hand — your edits will be wiped on the next brand:apply.\n *\n * To rebrand, edit DEFAULT_BRAND in scripts/brand-types.ts and run\n * `pnpm brand:apply`. To extend the shape, add a field to BrandConfig,\n * set its value in DEFAULT_BRAND, then read it from this writer.\n *\n * Pipeline guard: tests/architecture/brand-metadata-drift.test.ts\n * asserts `metadata.ts` is byte-identical to what brand:apply emits.\n */\n\nexport const brand = {\n name: \"Nebutra\",\n nameCn: \"云毓智能\",\n nameFull: \"无锡云毓智能科技有限公司\",\n nameFullEn: \"Wuxi Nebutra Intelligence Technology Co., Ltd.\",\n\n tagline: \"Ship AI products, not boilerplate.\",\n taglineCn: \"AI原生·快速出海·即刻交付\",\n description:\n \"Production-ready Next.js monorepo template for AI SaaS products. Auth, billing, multi-tenancy, AI services, design system, and enterprise infrastructure — pre-configured.\",\n descriptionCn:\n \"面向AI创业者的一体化SaaS基础设施模板,覆盖认证、计费、多租户、AI服务与设计系统,开箱即产品\",\n\n story: {\n concept:\n \"Logo以首字母N的基础造型概念为主要设计框架,通过几何正负空间构建隐形'N',形成近似六边形的稳定结构\",\n colorMeaning: \"蓝绿渐变体现未来感与科技锋芒,'云'代表云端平台,'毓'寓意孕育与转化\",\n values: [\"AI Native\", \"Ship Fast\", \"Open by Default\", \"Global-Ready\", \"Enterprise-Grade\"],\n missionStatement:\n \"Help AI founders and SaaS teams go from idea to production 10x faster by providing the infrastructure layer they shouldn't have to build.\",\n },\n\n domains: {\n landing: \"nebutra.com\",\n app: \"app.nebutra.com\",\n api: \"api.nebutra.com\",\n auth: \"auth.nebutra.com\",\n sso: \"sso.nebutra.com\",\n studio: \"studio.nebutra.com\",\n cdn: \"cdn.nebutra.com\",\n router: \"router.nebutra.com\",\n forge: \"forge.nebutra.com\",\n design: \"design.nebutra.com\",\n status: \"status.nebutra.com\",\n open: \"open.nebutra.com\",\n admin: \"admin.nebutra.com\",\n analytics: \"analytics.nebutra.com\",\n pebble: \"pebble.nebutra.com\",\n carina: \"carina.nebutra.com\",\n kuanlan: \"kuanlan.nebutra.com\",\n origin: \"origin.nebutra.com\",\n },\n\n social: {\n twitter: \"https://twitter.com/nebutra\",\n github: \"https://github.com/nebutra\",\n discord: \"https://discord.gg/nebutra\",\n linkedin: \"https://linkedin.com/company/nebutra\",\n },\n} as const;\n\n/**\n * Brand Colors — full VI palette + brand gradients + semantic colors.\n * Aligned with @nebutra/design-tokens DTCG SSOT (tokens/core.json).\n */\nexport const colors = {\n primary: {\n \"50\": \"#f0f4ff\",\n \"100\": \"#dbe4ff\",\n \"200\": \"#bac8ff\",\n \"300\": \"#91a7ff\",\n \"400\": \"#5c7cfa\",\n \"500\": \"#0033FE\",\n \"600\": \"#002ad4\",\n \"700\": \"#0021ab\",\n \"800\": \"#001882\",\n \"900\": \"#000f59\",\n \"950\": \"#000830\",\n },\n accent: {\n \"50\": \"#e6fff8\",\n \"100\": \"#b3ffec\",\n \"200\": \"#80ffe0\",\n \"300\": \"#4dfcd4\",\n \"400\": \"#1af7c8\",\n \"500\": \"#0BF1C3\",\n \"600\": \"#09c9a3\",\n \"700\": \"#07a183\",\n \"800\": \"#057963\",\n \"900\": \"#035143\",\n \"950\": \"#012923\",\n },\n neutral: {\n \"0\": \"#ffffff\",\n \"50\": \"#f9fafb\",\n \"100\": \"#f3f4f6\",\n \"200\": \"#e4e5e8\",\n \"300\": \"#d2d4d8\",\n \"400\": \"#9fa1a5\",\n \"500\": \"#717377\",\n \"600\": \"#515357\",\n \"700\": \"#3f4044\",\n \"800\": \"#26272a\",\n \"900\": \"#18191c\",\n \"950\": \"#0b0c0d\",\n },\n white: \"#FFFFFF\",\n black: \"#000000\",\n gradient: {\n primary: \"linear-gradient(135deg, #0033FE 0%, #00A2E9 50%, #0BF1C3 100%)\",\n primaryReverse: \"linear-gradient(135deg, #0BF1C3 0%, #00A2E9 50%, #0033FE 100%)\",\n primaryVertical: \"linear-gradient(180deg, #0033FE 0%, #00A2E9 50%, #0BF1C3 100%)\",\n primaryRadial: \"radial-gradient(circle, #0BF1C3 0%, #00A2E9 50%, #0033FE 100%)\",\n },\n p3Overrides: {\n primary500: \"color(display-p3 0.03 0.19 0.99)\",\n accent500: \"color(display-p3 0.07 0.94 0.79)\",\n },\n success: \"#22c55e\",\n warning: \"#f59e0b\",\n error: \"#ef4444\",\n info: \"#0033FE\",\n} as const;\n\n/**\n * Typography. fontFamily is read from design-tokens core.json:fontFamily by\n * brand:apply — change faces there, not here.\n */\nexport const typography = {\n fontFamily: {\n en: '\"Geist\", -apple-system, BlinkMacSystemFont, system-ui, sans-serif',\n cn: '\"MiSans\", \"PingFang SC\", \"Microsoft YaHei\", sans-serif',\n sans: '\"Geist\", \"MiSans\", \"PingFang SC\", \"Microsoft YaHei\", -apple-system, BlinkMacSystemFont, system-ui, sans-serif',\n mono: '\"Geist Mono\", ui-monospace, SFMono-Regular, \"SF Mono\", Menlo, monospace',\n display: '\"DM Sans\", \"MiSans\", \"PingFang SC\", sans-serif',\n heading: '\"DM Sans\", \"MiSans\", \"PingFang SC\", sans-serif',\n },\n fontWeight: {\n thin: 100,\n extraLight: 200,\n light: 300,\n normal: 400,\n medium: 500,\n semibold: 600,\n bold: 700,\n extraBold: 800,\n black: 900,\n },\n letterSpacing: {\n display: \"-0.04em\",\n heading: \"-0.03em\",\n subheading: \"-0.02em\",\n body: \"0em\",\n caption: \"0.01em\",\n mono: \"0em\",\n },\n lineHeight: {\n none: 1,\n tight: 1.2,\n snug: 1.4,\n normal: 1.6,\n relaxed: 1.75,\n loose: 2,\n },\n} as const;\n\n/**\n * Logo asset paths (relative to package), dual-edition structure:\n * - classic (v1.0): preferred for UX / product surfaces\n * - compliant (v2.0): required for commercial / legal contexts\n */\nexport const logoAssets = {\n classic: {\n default: \"assets/logo/logo-color.svg\",\n color: \"assets/logo/logo-color.svg\",\n inverse: \"assets/logo/logo-inverse.svg\",\n mono: \"assets/logo/logo-mono.svg\",\n en: \"assets/logo/logo-en.svg\",\n zh: \"assets/logo/logo-zh.svg\",\n zhEn: \"assets/logo/logo-zh-en.svg\",\n horizontalEn: \"assets/logo/logo-horizontal-en.svg\",\n horizontalZh: \"assets/logo/logo-horizontal-zh.svg\",\n verticalEn: \"assets/logo/logo-vertical-en.svg\",\n verticalZh: \"assets/logo/logo-vertical-zh.svg\",\n },\n compliant: {\n default: \"assets/logo-compliant/logo-color.svg\",\n color: \"assets/logo-compliant/logo-color.svg\",\n inverse: \"assets/logo-compliant/logo-inverse.svg\",\n mono: \"assets/logo-compliant/logo-mono.svg\",\n en: \"assets/logo-compliant/logo-en.svg\",\n zh: \"assets/logo-compliant/logo-zh.svg\",\n zhEn: \"assets/logo-compliant/logo-zh-en.svg\",\n horizontalEn: \"assets/logo-compliant/logo-horizontal-en.svg\",\n horizontalZh: \"assets/logo-compliant/logo-horizontal-zh.svg\",\n verticalEn: \"assets/logo-compliant/logo-vertical-en.svg\",\n verticalZh: \"assets/logo-compliant/logo-vertical-zh.svg\",\n horizontalEnMono: \"assets/logo-compliant/logo-horizontal-en-mono.svg\",\n horizontalZhMono: \"assets/logo-compliant/logo-horizontal-zh-mono.svg\",\n verticalEnMono: \"assets/logo-compliant/logo-vertical-en-mono.svg\",\n verticalZhMono: \"assets/logo-compliant/logo-vertical-zh-mono.svg\",\n },\n} as const;\n\n/**\n * Font asset paths (relative to package).\n */\nexport const fontAssets = {\n poppins: {\n thin: \"assets/fonts/poppins/Poppins-Thin.otf\",\n thinItalic: \"assets/fonts/poppins/Poppins-ThinItalic.otf\",\n extraLight: \"assets/fonts/poppins/Poppins-ExtraLight.otf\",\n extraLightItalic: \"assets/fonts/poppins/Poppins-ExtraLightItalic.otf\",\n light: \"assets/fonts/poppins/Poppins-Light.otf\",\n lightItalic: \"assets/fonts/poppins/Poppins-LightItalic.otf\",\n regular: \"assets/fonts/poppins/Poppins-Regular.otf\",\n italic: \"assets/fonts/poppins/Poppins-Italic.otf\",\n medium: \"assets/fonts/poppins/Poppins-Medium.otf\",\n mediumItalic: \"assets/fonts/poppins/Poppins-MediumItalic.otf\",\n semiBold: \"assets/fonts/poppins/Poppins-SemiBold.otf\",\n semiBoldItalic: \"assets/fonts/poppins/Poppins-SemiBoldItalic.otf\",\n bold: \"assets/fonts/poppins/Poppins-Bold.otf\",\n boldItalic: \"assets/fonts/poppins/Poppins-BoldItalic.otf\",\n extraBold: \"assets/fonts/poppins/Poppins-ExtraBold.otf\",\n extraBoldItalic: \"assets/fonts/poppins/Poppins-ExtraBoldItalic.otf\",\n black: \"assets/fonts/poppins/Poppins-Black.otf\",\n blackItalic: \"assets/fonts/poppins/Poppins-BlackItalic.otf\",\n },\n} as const;\n\n/**\n * Favicon assets.\n */\nexport const faviconAssets = {\n ico: \"assets/favicon/favicon.ico\",\n svg: \"assets/favicon/favicon.svg\",\n apple: \"assets/favicon/apple-touch-icon.png\",\n android192: \"assets/favicon/android-chrome-192x192.png\",\n android512: \"assets/favicon/android-chrome-512x512.png\",\n} as const;\n\n/**\n * OG Image dimensions.\n */\nexport const ogImageDimensions = {\n default: {\n width: 1200,\n height: 630,\n },\n twitter: {\n width: 1200,\n height: 600,\n },\n square: {\n width: 1200,\n height: 1200,\n },\n} as const;\n\nexport type BrandColors = typeof colors;\nexport type BrandTypography = typeof typography;\nexport type LogoAssets = typeof logoAssets;\n","/**\n * @nebutra/brand — metadata-helpers\n *\n * Pure functions deriving Next.js Metadata, JSON-LD objects, PWA manifest,\n * and feed channel headers from the single `brand` + `colors` SSOT.\n *\n * Zero hardcoded brand-name literals — every string is derived from `brand`\n * or `colors`. The arch drift guard `tests/architecture/brand-metadata-drift.test.ts`\n * asserts this invariant at CI time.\n *\n * NOTE: `next` is a TYPE-ONLY import here. The package declares `next` as an\n * optional peer dep + devDep (catalog:). skipLibCheck:true is set in tsconfig.json.\n * Adding `import \"server-only\"` is intentionally omitted: this file is consumed\n * by both server and client render paths in tests; the CALLER must enforce\n * server-only boundaries if needed.\n *\n * NOTE: getSiteUrl(service) in this module is DISTINCT from the zero-param\n * getSiteUrl() in apps/landing/src/lib/seo/site-routes.ts — different\n * modules, no collision. The local one remains the landing URL util.\n */\n\nimport type { Metadata, MetadataRoute } from \"next\";\nimport { brand, colors } from \"./metadata\";\n\n/** Services that have a canonical domain in brand.domains */\nexport type BrandService = keyof typeof brand.domains;\n\n/** Absolute origin from brand.domains only (no env hijack). */\nexport function getBrandOrigin(service: BrandService): string {\n const host = brand.domains[service];\n if (!host?.trim()) {\n throw new Error(`brand.domains.${String(service)} is empty — run brand:apply`);\n }\n return `https://${host.replace(/^https?:\\/\\//, \"\").replace(/\\/+$/, \"\")}`;\n}\n\n/**\n * Public object on `nebutra-assets` via `cdn.nebutra.com`.\n * R2_PUBLIC_URL / NEXT_PUBLIC_R2_PUBLIC_URL may override the origin for dogfood.\n */\n/**\n * Where a product's documentation lives: a path on that product's own host.\n *\n * There is no `docs` entry in `brand.domains` — deliberately. A single docs\n * subdomain meant a product's own host was never where its documentation\n * lived: pebble's docs sat at docs.<domain>/pebble/* while\n * pebble.<domain>/docs/* redirected to them. One value cannot express \"each\n * product serves its own\", so the key is gone and the path is derived.\n *\n * Defaults to the marketing site, which carries the product docs. Pass a\n * service to get that product's: getDocsUrl(\"pebble\") → https://pebble.…/docs.\n */\nexport function getDocsUrl(service: BrandService = \"landing\", path = \"\"): string {\n const suffix = path.replace(/^\\/+/, \"\");\n return `${getBrandOrigin(service)}/docs${suffix ? `/${suffix}` : \"\"}`;\n}\n\n/**\n * The origin publicAssetUrl() resolves against — for CSP directives. A page\n * that loads public assets (images, the MiSans subsets) must allow exactly\n * this origin, or the browser blocks the request with no error on the page:\n * font-src 'self' blocked every MiSans face on nebutra.com until 2026-09-26.\n */\nexport function publicAssetOrigin(base?: string): string {\n return new URL(publicAssetUrl(\"x\", base)).origin;\n}\n\nexport function publicAssetUrl(key: string, base?: string): string {\n const origin = (\n base ||\n process.env.NEXT_PUBLIC_R2_PUBLIC_URL ||\n process.env.R2_PUBLIC_URL ||\n getBrandOrigin(\"cdn\")\n ).replace(/\\/+$/, \"\");\n const path = key.replace(/^\\/+/, \"\");\n if (!path || path.includes(\"..\") || path.includes(\"\\\\\")) {\n throw new Error(\"invalid_public_asset_key\");\n }\n return `${origin}/${path}`;\n}\n\n/**\n * Bare query flag that keeps a signed-in visitor on the marketing homepage\n * (`?home`, no value). Apex `/` without it is a product launcher.\n */\nexport const MARKETING_HOME_PARAM = \"home\";\n\n/**\n * Marketing homepage path. Always includes the `?home` skip — that is the\n * only signal landing honours. `getBrandOrigin(\"landing\")` is the launcher.\n *\n * `defaultLocale` is the landing path locale that lives on `/` (today `en`).\n * Pass it from `@nebutra/i18n` so brand does not import the i18n package.\n */\nexport function getMarketingHomePath(options?: {\n locale?: string;\n defaultLocale?: string;\n}): string {\n const defaultLocale = options?.defaultLocale?.trim() || \"en\";\n const locale = options?.locale?.trim();\n const path = locale && locale !== defaultLocale ? `/${locale}` : \"/\";\n return path === \"/\" ? `/?${MARKETING_HOME_PARAM}` : `${path}?${MARKETING_HOME_PARAM}`;\n}\n\n/** Absolute marketing homepage. Same contract as `getMarketingHomePath`. */\nexport function getMarketingHomeUrl(options?: { locale?: string; defaultLocale?: string }): string {\n const origin = getBrandOrigin(\"landing\");\n return new URL(getMarketingHomePath(options), `${origin}/`).toString();\n}\n\n/** Cookie parent domain from landing apex (e.g. `.example.com`). */\nexport function getBrandCookieDomain(): string {\n const apex = brand.domains.landing.replace(/^https?:\\/\\//, \"\").replace(/\\/+$/, \"\");\n if (!apex) return \"\";\n return apex.startsWith(\".\") ? apex : `.${apex}`;\n}\n\n/**\n * Canonical base URL. NEXT_PUBLIC_SITE_URL only overrides landing.\n */\nexport function getSiteUrl(service: BrandService = \"landing\"): string {\n if (service === \"landing\") {\n const envUrl = process.env[\"NEXT_PUBLIC_SITE_URL\"];\n if (envUrl && envUrl.trim() !== \"\") {\n return envUrl.trim().replace(/\\/+$/, \"\");\n }\n }\n return getBrandOrigin(service);\n}\n\n/** Production multi-app public URL map (dogfood brand.domains). */\nexport function getBrandPublicUrls() {\n return {\n siteUrl: getBrandOrigin(\"landing\"),\n appUrl: getBrandOrigin(\"app\"),\n apiUrl: getBrandOrigin(\"api\"),\n authUrl: getBrandOrigin(\"auth\"),\n ssoUrl: getBrandOrigin(\"sso\"),\n docsUrl: getDocsUrl(),\n routerUrl: getBrandOrigin(\"router\"),\n forgeUrl: getBrandOrigin(\"forge\"),\n designUrl: getBrandOrigin(\"design\"),\n statusUrl: getBrandOrigin(\"status\"),\n openUrl: getBrandOrigin(\"open\"),\n studioUrl: getBrandOrigin(\"studio\"),\n cdnUrl: getBrandOrigin(\"cdn\"),\n analyticsUrl: getBrandOrigin(\"analytics\"),\n pebbleUrl: getBrandOrigin(\"pebble\"),\n carinaUrl: getBrandOrigin(\"carina\"),\n cookieDomain: getBrandCookieDomain(),\n } as const;\n}\n\n/**\n * Product mailbox derived from landing apex — rebrand-safe.\n * e.g. getBrandEmail(\"contact\") → contact@example.com\n */\nexport function getBrandEmail(localPart: string): string {\n const apex = brand.domains.landing\n .replace(/^https?:\\/\\//, \"\")\n .replace(/\\/+$/, \"\")\n .replace(/^www\\./, \"\");\n if (!apex) {\n throw new Error(\"brand.domains.landing is empty — run brand:apply\");\n }\n const local = localPart.trim().replace(/@.*$/, \"\");\n if (!local) {\n throw new Error(\"getBrandEmail requires a non-empty local part\");\n }\n return `${local}@${apex}`;\n}\n\n/** Default transactional From header: `Brand <noreply@apex>`. */\nexport function getBrandMailFrom(): string {\n return `${brand.name} <${getBrandEmail(\"noreply\")}>`;\n}\n\n// ─── Metadata ────────────────────────────────────────────────────────────────\n\nexport interface SiteMetadataOptions {\n readonly service: BrandService;\n readonly title?: string;\n readonly description?: string;\n}\n\n/**\n * Returns a partial Next.js Metadata object suitable for spreading into a\n * layout.tsx export const metadata. Callers may override any field.\n */\nexport function getSiteMetadata(opts: SiteMetadataOptions): Metadata {\n const siteUrl = getSiteUrl(opts.service);\n const title = opts.title ?? brand.name;\n const description = opts.description ?? brand.tagline;\n\n return {\n applicationName: brand.name,\n title: {\n default: title,\n template: `%s — ${brand.name}`,\n },\n description,\n metadataBase: new URL(siteUrl),\n authors: [{ name: brand.name, url: siteUrl }],\n creator: brand.name,\n publisher: brand.name,\n openGraph: {\n type: \"website\",\n siteName: brand.name,\n title,\n description,\n url: siteUrl,\n },\n twitter: {\n card: \"summary_large_image\",\n site: brand.social.twitter.replace(\"https://twitter.com/\", \"@\"),\n title,\n description,\n },\n };\n}\n\n// ─── JSON-LD types ─────────────────────────────────────────────────────────────\n\nexport interface OrganizationJsonLd {\n \"@context\": \"https://schema.org\";\n \"@type\": \"Organization\";\n \"@id\": string;\n name: string;\n alternateName: string;\n url: string;\n sameAs: string[];\n [key: string]: unknown;\n}\n\nexport interface WebSiteJsonLd {\n \"@context\": \"https://schema.org\";\n \"@type\": \"WebSite\";\n \"@id\": string;\n name: string;\n url: string;\n description: string;\n publisher: { \"@id\": string };\n inLanguage: string[];\n [key: string]: unknown;\n}\n\nexport interface SoftwareApplicationJsonLd {\n \"@context\": \"https://schema.org\";\n \"@type\": \"SoftwareApplication\";\n name: string;\n applicationCategory: string;\n url: string;\n author: { \"@id\": string };\n [key: string]: unknown;\n}\n\nexport interface FeedChannelMeta {\n title: string;\n link: string;\n description: string;\n feedUrl: string;\n language: string;\n}\n\n// ─── JSON-LD builders ────────────────────────────────────────────────────────\n\n/**\n * Base Organization JSON-LD — company-specific fields\n * (foundingDate, address, contactPoint) are intentionally omitted here.\n * Merge them at the call site to avoid hard-coding values here.\n */\nexport function buildOrganizationJsonLd(): OrganizationJsonLd {\n const siteUrl = getSiteUrl(\"landing\");\n const id = `${siteUrl}/#organization`;\n\n const sameAs = [\n brand.social.github,\n brand.social.twitter.replace(\"https://twitter.com/\", \"https://x.com/\"),\n brand.social.linkedin,\n brand.social.discord,\n ].filter((url) => url.trim() !== \"\");\n\n return {\n \"@context\": \"https://schema.org\",\n \"@type\": \"Organization\",\n \"@id\": id,\n name: brand.name,\n alternateName: brand.nameCn,\n url: siteUrl,\n sameAs,\n };\n}\n\nexport function buildWebSiteJsonLd(): WebSiteJsonLd {\n const siteUrl = getSiteUrl(\"landing\");\n const org = buildOrganizationJsonLd();\n\n return {\n \"@context\": \"https://schema.org\",\n \"@type\": \"WebSite\",\n \"@id\": `${siteUrl}/#website`,\n name: brand.name,\n url: siteUrl,\n description: brand.tagline,\n publisher: { \"@id\": org[\"@id\"] },\n inLanguage: [\"en\", \"zh\"],\n };\n}\n\nexport function buildSoftwareApplicationJsonLd(): SoftwareApplicationJsonLd {\n const org = buildOrganizationJsonLd();\n const githubPath = brand.social.github.replace(\"https://github.com/\", \"\");\n\n return {\n \"@context\": \"https://schema.org\",\n \"@type\": \"SoftwareApplication\",\n name: `${brand.name} Sailor`,\n applicationCategory: \"DeveloperApplication\",\n url: `https://github.com/${githubPath}`,\n author: { \"@id\": org[\"@id\"] },\n };\n}\n\n// ─── PWA Manifest ────────────────────────────────────────────────────────────\n\n/**\n * Returns a base PWA manifest derived from brand + colors.\n * background_color and theme_color use the palette's canonical hex values\n * (NOT CSS variables — service workers do not resolve var()).\n *\n * CONTRAST CAVEAT: theme_color = colors.primary[\"500\"] assumes a dark\n * background context. If a rebrand sets a light primary, update this value.\n */\nexport function buildPwaManifest(): MetadataRoute.Manifest {\n return {\n name: `${brand.name} — ${brand.tagline}`,\n short_name: brand.name,\n description: brand.description,\n start_url: \"/\",\n display: \"standalone\",\n background_color: colors.neutral[\"950\"],\n theme_color: colors.primary[\"500\"],\n orientation: \"portrait-primary\",\n scope: \"/\",\n lang: \"en\",\n categories: [\"business\", \"productivity\", \"utilities\"],\n icons: [\n {\n src: \"/favicon.ico\",\n sizes: \"any\",\n type: \"image/x-icon\",\n },\n {\n src: \"/android-chrome-192x192.png\",\n sizes: \"192x192\",\n type: \"image/png\",\n purpose: \"maskable\",\n },\n {\n src: \"/android-chrome-512x512.png\",\n sizes: \"512x512\",\n type: \"image/png\",\n purpose: \"maskable\",\n },\n {\n src: \"/android-chrome-192x192.png\",\n sizes: \"192x192\",\n type: \"image/png\",\n purpose: \"any\",\n },\n {\n src: \"/android-chrome-512x512.png\",\n sizes: \"512x512\",\n type: \"image/png\",\n purpose: \"any\",\n },\n ],\n };\n}\n\n// ─── Feed ────────────────────────────────────────────────────────────────────\n\n/**\n * Returns the shared channel-level fields for RSS and Atom feeds.\n * Item-level links are built at the call site using the version string.\n */\nexport function buildFeedChannelMeta(): FeedChannelMeta {\n const siteUrl = getSiteUrl(\"landing\");\n\n return {\n title: `${brand.name} Changelog`,\n link: `${siteUrl}/changelog`,\n description: brand.tagline,\n feedUrl: `${siteUrl}/api/changelog`,\n language: \"en-us\",\n };\n}\n","\"use client\";\n\nimport { brand } from \"../metadata\";\nimport { publicAssetUrl } from \"../metadata-helpers\";\n\nexport type LogoVariant =\n | \"color\"\n | \"inverse\"\n | \"mono\"\n | \"en\"\n | \"zh\"\n | \"zh-en\"\n | \"horizontal-en\"\n | \"horizontal-zh\"\n | \"vertical-en\"\n | \"vertical-zh\"\n | \"horizontal-en-mono\"\n | \"horizontal-zh-mono\"\n | \"vertical-en-mono\"\n | \"vertical-zh-mono\";\n\n/**\n * Logo edition: classic (v1.0) or compliant (v2.0)\n *\n * - classic: 经典版,\"毓\"字更美观,用于用户体验场景 (App/网站/产品)\n * - compliant: 合规版,\"毓\"字符合商标规范,用于商务场景 (法律/商标/正式文件)\n */\nexport type LogoEdition = \"classic\" | \"compliant\";\n\n/** Variants that only exist in the compliant edition */\nconst COMPLIANT_ONLY_VARIANTS: ReadonlySet<LogoVariant> = new Set([\n \"horizontal-en-mono\",\n \"horizontal-zh-mono\",\n \"vertical-en-mono\",\n \"vertical-zh-mono\",\n]);\n\nexport interface LogoProps {\n /**\n * Logo variant\n * @default \"en\" (Nebutra with English wordmark)\n */\n variant?: LogoVariant;\n\n /**\n * Logo edition\n * - \"classic\" (default): v1.0 经典版,更美观\n * - \"compliant\": v2.0 合规版,符合商标规范\n *\n * Mono combination variants automatically use compliant edition.\n * @default \"classic\"\n */\n edition?: LogoEdition;\n\n /**\n * Logo size (width in pixels)\n * @default 120\n */\n size?: number;\n\n /**\n * Custom className\n */\n className?: string;\n\n /**\n * Invert colors for dark backgrounds.\n * When true, black becomes white (useful for dark mode).\n *\n * Note: For pure white logomark, use variant=\"inverse\" instead.\n */\n inverted?: boolean;\n}\n\n/**\n * Resolve the CDN object for a logo variant + edition combination.\n * Mono combination variants always route to the compliant directory.\n * `public/` copies from `pnpm brand:sync` stay as seed, not consumption.\n */\nexport function logoPublicSrc(variant: LogoVariant, edition: LogoEdition): string {\n const dir =\n edition === \"compliant\" || COMPLIANT_ONLY_VARIANTS.has(variant)\n ? \"brand/logo-compliant\"\n : \"brand/logo\";\n return publicAssetUrl(`${dir}/logo-${variant}.svg`);\n}\n\n/**\n * Aspect ratios for different logo variants\n * width:height ratio (e.g. 5.25 means width is 5.25x height)\n */\nconst ASPECT_RATIOS: Record<LogoVariant, number> = {\n color: 1.07, // 535.71 x 500 ≈ 1.07\n inverse: 1.07, // 535.71 x 500 ≈ 1.07\n mono: 1.07, // Same as color/inverse\n en: 5.25, // 544.21 x 103.74 ≈ 5.25 (horizontal text)\n zh: 3.5, // Estimated for Chinese wordmark\n \"zh-en\": 4.0, // Estimated for bilingual\n \"horizontal-en\": 5.25, // Similar to \"en\"\n \"horizontal-zh\": 4.5, // Estimated\n \"vertical-en\": 0.8, // Taller than wide\n \"vertical-zh\": 0.8, // Taller than wide\n \"horizontal-en-mono\": 5.25, // Same as horizontal-en\n \"horizontal-zh-mono\": 4.5, // Same as horizontal-zh\n \"vertical-en-mono\": 0.8, // Same as vertical-en\n \"vertical-zh-mono\": 0.8, // Same as vertical-zh\n};\n\n/**\n * Nebutra Logo Component\n *\n * Renders the official Nebutra brand logo from SVG assets (fixed VI fills).\n * Browser src is `https://cdn.nebutra.com/brand/logo{,-compliant}/…`.\n * `pnpm brand:sync` still copies seed files into each app `public/`.\n *\n * For product chrome that must recolor with Brand Package skins (roles.brand),\n * prefer LogomarkSVG / LogoEnSVG with default `text-brand-mark` instead of\n * raster VI color SVGs or `text-primary` (CTA / roles.action).\n *\n * @example\n * ```tsx\n * // Logomark only (square icon)\n * <Logo variant=\"inverse\" size={40} />\n *\n * // Full logo with English wordmark\n * <Logo variant=\"en\" size={150} />\n *\n * // Chinese wordmark\n * <Logo variant=\"zh\" size={120} />\n * ```\n */\nexport function Logo({\n variant = \"en\",\n edition = \"classic\",\n size = 120,\n className,\n inverted = false,\n}: LogoProps) {\n const aspectRatio = ASPECT_RATIOS[variant];\n const height = Math.round(size / aspectRatio);\n const src = logoPublicSrc(variant, edition);\n\n // For dark backgrounds, invert black to white\n const filterStyle = inverted ? { filter: \"brightness(0) invert(1)\" } : {};\n\n return (\n <img\n src={src}\n alt={brand.name}\n width={size}\n height={height}\n className={className}\n style={{ width: size, height, ...filterStyle }}\n />\n );\n}\n\n/**\n * Logomark only (the abstract icon without text)\n *\n * This uses the \"color\" or \"inverse\" variant which is just the icon.\n *\n * @example\n * ```tsx\n * <Logomark variant=\"inverse\" size={32} />\n * ```\n */\nexport function Logomark({\n size = 32,\n className,\n variant = \"color\",\n edition = \"classic\",\n inverted = false,\n}: {\n size?: number;\n className?: string;\n variant?: \"color\" | \"inverse\" | \"mono\";\n edition?: LogoEdition;\n inverted?: boolean;\n}) {\n const src = logoPublicSrc(variant, edition);\n\n // For dark backgrounds, invert black to white (unless using \"inverse\" variant which is already white)\n const filterStyle =\n inverted && variant !== \"inverse\" ? { filter: \"brightness(0) invert(1)\" } : {};\n\n return (\n <img\n src={src}\n alt={brand.name}\n width={size}\n height={size}\n className={className}\n style={{ width: size, height: size, ...filterStyle }}\n />\n );\n}\n\n/**\n * Wordmark only (text \"Nebutra\" without the icon)\n *\n * Uses the \"en\" variant which has the full wordmark.\n * For icon + text, use Logo with variant=\"en\".\n */\nexport function Wordmark({\n size = 100,\n className,\n variant = \"en\",\n edition = \"classic\",\n inverted = false,\n}: {\n size?: number;\n className?: string;\n variant?: \"en\" | \"zh\" | \"zh-en\";\n edition?: LogoEdition;\n inverted?: boolean;\n}) {\n const aspectRatio = ASPECT_RATIOS[variant];\n const height = Math.round(size / aspectRatio);\n const src = logoPublicSrc(variant, edition);\n\n const filterStyle = inverted ? { filter: \"brightness(0) invert(1)\" } : {};\n\n return (\n <img\n src={src}\n alt={brand.name}\n width={size}\n height={height}\n className={className}\n style={{ width: size, height, ...filterStyle }}\n />\n );\n}\n\nexport default Logo;\n"],"mappings":"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","names":[]}
package/dist/index.d.ts CHANGED
@@ -1,7 +1,7 @@
1
1
  export { Logo, LogoEdition, LogoProps, LogoVariant, Logomark, Wordmark, logoPublicSrc } from './components/Logo.js';
2
2
  import * as react_jsx_runtime from 'react/jsx-runtime';
3
3
  export { BrandColors, BrandTypography, LogoAssets, brand, colors, faviconAssets, fontAssets, logoAssets, ogImageDimensions, typography } from './metadata.js';
4
- export { BrandService, FeedChannelMeta, MARKETING_HOME_PARAM, OrganizationJsonLd, SiteMetadataOptions, SoftwareApplicationJsonLd, WebSiteJsonLd, buildFeedChannelMeta, buildOrganizationJsonLd, buildPwaManifest, buildSoftwareApplicationJsonLd, buildWebSiteJsonLd, getBrandCookieDomain, getBrandEmail, getBrandMailFrom, getBrandOrigin, getBrandPublicUrls, getMarketingHomePath, getMarketingHomeUrl, getSiteMetadata, getSiteUrl, publicAssetUrl } from './metadata-helpers.js';
4
+ export { BrandService, FeedChannelMeta, MARKETING_HOME_PARAM, OrganizationJsonLd, SiteMetadataOptions, SoftwareApplicationJsonLd, WebSiteJsonLd, buildFeedChannelMeta, buildOrganizationJsonLd, buildPwaManifest, buildSoftwareApplicationJsonLd, buildWebSiteJsonLd, getBrandCookieDomain, getBrandEmail, getBrandMailFrom, getBrandOrigin, getBrandPublicUrls, getDocsUrl, getMarketingHomePath, getMarketingHomeUrl, getSiteMetadata, getSiteUrl, publicAssetUrl } from './metadata-helpers.js';
5
5
  export { EASTER_EGG_REGISTRY, EasterEggEntry, MILESTONE_COPY_PACK, MilestoneCopyEntry, MilestoneId, NebutraMicrocopy, SupportedLocale, getMilestoneCopy } from './microcopy.js';
6
6
  import 'next';
7
7
 
@@ -70,14 +70,14 @@ declare const nebutraBlue: {
70
70
  /** Color values */
71
71
  readonly hex: "#0033FE";
72
72
  readonly rgb: {
73
- readonly r: 0;
74
- readonly g: 51;
75
- readonly b: 254;
73
+ r: number;
74
+ g: number;
75
+ b: number;
76
76
  };
77
77
  readonly hsl: {
78
- readonly h: 228;
79
- readonly s: 100;
80
- readonly l: 50;
78
+ h: number;
79
+ s: number;
80
+ l: number;
81
81
  };
82
82
  /** Print color values (if needed) */
83
83
  readonly cmyk: {
@@ -103,14 +103,14 @@ declare const nebutraCyan: {
103
103
  /** Color values */
104
104
  readonly hex: "#0BF1C3";
105
105
  readonly rgb: {
106
- readonly r: 11;
107
- readonly g: 241;
108
- readonly b: 195;
106
+ r: number;
107
+ g: number;
108
+ b: number;
109
109
  };
110
110
  readonly hsl: {
111
- readonly h: 168;
112
- readonly s: 91;
113
- readonly l: 49;
111
+ h: number;
112
+ s: number;
113
+ l: number;
114
114
  };
115
115
  /** Print color values (if needed) */
116
116
  readonly cmyk: {
@@ -135,68 +135,60 @@ declare const brandGradient: {
135
135
  readonly primary: {
136
136
  readonly css: "linear-gradient(135deg, #0033FE 0%, #00A2E9 50%, #0BF1C3 100%)";
137
137
  readonly angle: 135;
138
- readonly stops: readonly [{
139
- readonly color: "#0033FE";
140
- readonly position: 0;
141
- }, {
142
- readonly color: "#00A2E9";
143
- readonly position: 50;
144
- readonly note: "OKLab midpoint";
145
- }, {
146
- readonly color: "#0BF1C3";
147
- readonly position: 100;
148
- }];
138
+ readonly stops: ({
139
+ color: string;
140
+ position: number;
141
+ note: string;
142
+ } | {
143
+ color: string;
144
+ position: number;
145
+ note?: undefined;
146
+ })[];
149
147
  readonly usage: "Logo、Hero区域、VI 品牌资产(产品 CTA 用 solid primary)";
150
148
  };
151
149
  /** Reverse gradient */
152
150
  readonly reverse: {
153
151
  readonly css: "linear-gradient(135deg, #0BF1C3 0%, #00A2E9 50%, #0033FE 100%)";
154
152
  readonly angle: 135;
155
- readonly stops: readonly [{
156
- readonly color: "#0BF1C3";
157
- readonly position: 0;
158
- }, {
159
- readonly color: "#00A2E9";
160
- readonly position: 50;
161
- readonly note: "OKLab midpoint";
162
- }, {
163
- readonly color: "#0033FE";
164
- readonly position: 100;
165
- }];
153
+ readonly stops: ({
154
+ color: string;
155
+ position: number;
156
+ note: string;
157
+ } | {
158
+ color: string;
159
+ position: number;
160
+ note?: undefined;
161
+ })[];
166
162
  readonly usage: "次要元素、hover状态";
167
163
  };
168
164
  /** Vertical gradient */
169
165
  readonly vertical: {
170
166
  readonly css: "linear-gradient(180deg, #0033FE 0%, #00A2E9 50%, #0BF1C3 100%)";
171
167
  readonly angle: 180;
172
- readonly stops: readonly [{
173
- readonly color: "#0033FE";
174
- readonly position: 0;
175
- }, {
176
- readonly color: "#00A2E9";
177
- readonly position: 50;
178
- readonly note: "OKLab midpoint";
179
- }, {
180
- readonly color: "#0BF1C3";
181
- readonly position: 100;
182
- }];
168
+ readonly stops: ({
169
+ color: string;
170
+ position: number;
171
+ note: string;
172
+ } | {
173
+ color: string;
174
+ position: number;
175
+ note?: undefined;
176
+ })[];
183
177
  readonly usage: "垂直布局元素、页面分割";
184
178
  };
185
179
  /** Radial gradient */
186
180
  readonly radial: {
187
181
  readonly css: "radial-gradient(circle, #0BF1C3 0%, #00A2E9 50%, #0033FE 100%)";
188
182
  readonly type: "radial";
189
- readonly stops: readonly [{
190
- readonly color: "#0BF1C3";
191
- readonly position: 0;
192
- }, {
193
- readonly color: "#00A2E9";
194
- readonly position: 50;
195
- readonly note: "OKLab midpoint";
196
- }, {
197
- readonly color: "#0033FE";
198
- readonly position: 100;
199
- }];
183
+ readonly stops: ({
184
+ color: string;
185
+ position: number;
186
+ note: string;
187
+ } | {
188
+ color: string;
189
+ position: number;
190
+ note?: undefined;
191
+ })[];
200
192
  readonly usage: "背景光晕、焦点效果";
201
193
  };
202
194
  };
@@ -322,17 +314,17 @@ declare const nebutraCyanScale: {
322
314
  * The `0` step (white) is omitted to keep the legacy 50–950 scale shape intact.
323
315
  */
324
316
  declare const nebutraNeutralScale: {
325
- readonly 50: "#f8fafc";
326
- readonly 100: "#f1f5f9";
327
- readonly 200: "#e2e8f0";
328
- readonly 300: "#cbd5e1";
329
- readonly 400: "#94a3b8";
330
- readonly 500: "#64748b";
331
- readonly 600: "#475569";
332
- readonly 700: "#334155";
333
- readonly 800: "#1e293b";
334
- readonly 900: "#0f172a";
335
- readonly 950: "#020617";
317
+ readonly 50: "#f9fafb";
318
+ readonly 100: "#f3f4f6";
319
+ readonly 200: "#e4e5e8";
320
+ readonly 300: "#d2d4d8";
321
+ readonly 400: "#9fa1a5";
322
+ readonly 500: "#717377";
323
+ readonly 600: "#515357";
324
+ readonly 700: "#3f4044";
325
+ readonly 800: "#26272a";
326
+ readonly 900: "#18191c";
327
+ readonly 950: "#0b0c0d";
336
328
  };
337
329
  /**
338
330
  * Semantic Color Tokens - 语义化颜色
@@ -342,40 +334,40 @@ declare const nebutraNeutralScale: {
342
334
  declare const semanticColors: {
343
335
  readonly surface: {
344
336
  readonly light: {
345
- readonly default: "#f8fafc";
346
- readonly subtle: "#f1f5f9";
347
- readonly muted: "#e2e8f0";
337
+ readonly default: "#f9fafb";
338
+ readonly subtle: "#f3f4f6";
339
+ readonly muted: "#e4e5e8";
348
340
  };
349
341
  readonly dark: {
350
- readonly default: "#020617";
351
- readonly subtle: "#0f172a";
352
- readonly muted: "#1e293b";
342
+ readonly default: "#0b0c0d";
343
+ readonly subtle: "#18191c";
344
+ readonly muted: "#26272a";
353
345
  };
354
346
  };
355
347
  readonly text: {
356
348
  readonly light: {
357
- readonly primary: "#0f172a";
358
- readonly secondary: "#475569";
359
- readonly muted: "#94a3b8";
349
+ readonly primary: "#18191c";
350
+ readonly secondary: "#515357";
351
+ readonly muted: "#9fa1a5";
360
352
  readonly inverse: "#ffffff";
361
353
  };
362
354
  readonly dark: {
363
- readonly primary: "#f8fafc";
364
- readonly secondary: "#cbd5e1";
365
- readonly muted: "#64748b";
366
- readonly inverse: "#0f172a";
355
+ readonly primary: "#f9fafb";
356
+ readonly secondary: "#d2d4d8";
357
+ readonly muted: "#717377";
358
+ readonly inverse: "#18191c";
367
359
  };
368
360
  };
369
361
  readonly border: {
370
362
  readonly light: {
371
- readonly default: "#e2e8f0";
372
- readonly subtle: "#f1f5f9";
373
- readonly strong: "#cbd5e1";
363
+ readonly default: "#e4e5e8";
364
+ readonly subtle: "#f3f4f6";
365
+ readonly strong: "#d2d4d8";
374
366
  };
375
367
  readonly dark: {
376
- readonly default: "#1e293b";
377
- readonly subtle: "#0f172a";
378
- readonly strong: "#334155";
368
+ readonly default: "#26272a";
369
+ readonly subtle: "#18191c";
370
+ readonly strong: "#3f4044";
379
371
  };
380
372
  };
381
373
  readonly brand: {
@@ -702,8 +694,9 @@ declare const brandGuidelines: {
702
694
  readonly gradient: "linear-gradient(135deg, #0033FE 0%, #00A2E9 50%, #0BF1C3 100%)";
703
695
  };
704
696
  readonly typography: {
705
- readonly cn: "Noto Sans SC";
706
- readonly en: "Geist";
697
+ readonly cn: string;
698
+ readonly en: string;
699
+ readonly heading: string;
707
700
  readonly weights: readonly ["Regular", "Medium", "SemiBold", "Bold"];
708
701
  };
709
702
  };