chore: seal Design Vision v1 and monorepo scaffold
Archive the differentiated YuXinGu product docs, AI engineering system, design contract, and Go/Vue scaffold. Next execution prioritizes Cece-parity over early innovation (see .ai/product/STRATEGY.md). Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"name": "@yuxingu/sdk",
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"main": "./src/index.ts",
|
||||
"types": "./src/index.ts",
|
||||
"exports": {
|
||||
".": "./src/index.ts"
|
||||
},
|
||||
"dependencies": {
|
||||
"@yuxingu/types": "0.1.0"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
import type { ApiResponse } from '@yuxingu/types'
|
||||
|
||||
/** Platform adapters so H5 and mini-program share one client. */
|
||||
export interface RequestOptions {
|
||||
method?: 'GET' | 'POST' | 'PUT' | 'DELETE'
|
||||
path: string
|
||||
body?: unknown
|
||||
headers?: Record<string, string>
|
||||
}
|
||||
|
||||
export interface ClientAdapters {
|
||||
/** Perform HTTP and return parsed JSON envelope. */
|
||||
request: <T>(opts: RequestOptions) => Promise<ApiResponse<T>>
|
||||
getToken?: () => string | null | Promise<string | null>
|
||||
}
|
||||
|
||||
export interface CreateClientOptions {
|
||||
baseURL: string
|
||||
adapters: ClientAdapters
|
||||
}
|
||||
|
||||
/**
|
||||
* createClient builds a typed API facade.
|
||||
* Side effect: network via adapters.request.
|
||||
*/
|
||||
export function createClient(opts: CreateClientOptions) {
|
||||
const { baseURL, adapters } = opts
|
||||
|
||||
async function call<T>(path: string, init?: Omit<RequestOptions, 'path'>): Promise<T> {
|
||||
const token = adapters.getToken ? await adapters.getToken() : null
|
||||
const headers: Record<string, string> = { ...(init?.headers || {}) }
|
||||
if (token) headers.Authorization = `Bearer ${token}`
|
||||
|
||||
const res = await adapters.request<T>({
|
||||
method: init?.method || 'GET',
|
||||
path: joinURL(baseURL, path),
|
||||
body: init?.body,
|
||||
headers,
|
||||
})
|
||||
if (res.code !== 0) {
|
||||
throw new Error(res.message || `api error ${res.code}`)
|
||||
}
|
||||
return res.data as T
|
||||
}
|
||||
|
||||
return {
|
||||
healthz: () => call<{ status: string }>('/api/v1/healthz'),
|
||||
ping: () => call<{ pong: boolean }>('/api/v1/ping'),
|
||||
}
|
||||
}
|
||||
|
||||
/** Browser fetch adapter for user-h5. */
|
||||
export function createBrowserAdapters(): ClientAdapters {
|
||||
return {
|
||||
request: async <T>({ method = 'GET', path, body, headers }) => {
|
||||
const res = await fetch(path, {
|
||||
method,
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
...(headers || {}),
|
||||
},
|
||||
body: body === undefined ? undefined : JSON.stringify(body),
|
||||
})
|
||||
return (await res.json()) as ApiResponse<T>
|
||||
},
|
||||
getToken: () => localStorage.getItem('yxg_token'),
|
||||
}
|
||||
}
|
||||
|
||||
function joinURL(base: string, path: string): string {
|
||||
if (path.startsWith('http')) return path
|
||||
const b = base.replace(/\/$/, '')
|
||||
const p = path.startsWith('/') ? path : `/${path}`
|
||||
return `${b}${p}`
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
{
|
||||
"name": "@yuxingu/types",
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"main": "./src/index.ts",
|
||||
"types": "./src/index.ts",
|
||||
"exports": {
|
||||
".": "./src/index.ts"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
/** Unified API envelope from apps/api. */
|
||||
export interface ApiResponse<T = unknown> {
|
||||
code: number
|
||||
message: string
|
||||
data?: T
|
||||
}
|
||||
|
||||
/** User profile used by decode / relation features. */
|
||||
export interface Profile {
|
||||
id: string
|
||||
label: string
|
||||
year: number
|
||||
month: number
|
||||
day: number
|
||||
relation: 'self' | 'other'
|
||||
}
|
||||
|
||||
/** Minimal scale list item. */
|
||||
export interface ScaleSummary {
|
||||
slug: string
|
||||
name: string
|
||||
category: string
|
||||
questionCount: number
|
||||
description?: string
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"name": "@yuxingu/ui",
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"main": "./src/tokens.css",
|
||||
"exports": {
|
||||
"./tokens.css": "./src/tokens.css"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,79 @@
|
||||
/* Shared design tokens — source of truth for H5 / mini-program / future clients
|
||||
* Contract: .ai/design/design-system.md
|
||||
*/
|
||||
:root {
|
||||
/* ── Semantic color (prefer in new UI) ── */
|
||||
--color-primary: #e54d42;
|
||||
--color-primary-hover: #d44338;
|
||||
--color-primary-soft: #ffe4e4;
|
||||
--color-primary-disabled: #f5b5b0;
|
||||
|
||||
--color-bg-start: #ffd1c7;
|
||||
--color-bg-end: #ffc8b5;
|
||||
--color-bg-sheet: #fff9f7;
|
||||
|
||||
--color-surface: #ffffff;
|
||||
--color-text-primary: #333333;
|
||||
--color-text-secondary: #999999;
|
||||
--color-text-tertiary: #bbbbbb;
|
||||
--color-border: #f0f0f0;
|
||||
--color-border-strong: #eeeeee;
|
||||
--color-input-bg: #fdfaf8;
|
||||
|
||||
--color-accent-gold: #c8923a;
|
||||
--color-accent-gold-soft: #fff3d6;
|
||||
--color-accent-blue: #4a90e2;
|
||||
--color-accent-blue-soft: #e3eeff;
|
||||
--color-accent-green: #5cb85c;
|
||||
--color-accent-green-soft: #e0f6e4;
|
||||
--color-accent-orange: #e8985a;
|
||||
--color-accent-purple: #8b5cf6;
|
||||
|
||||
--color-success: #5cb85c;
|
||||
--color-warning: #e8985a;
|
||||
--color-error: #e54d42;
|
||||
--color-info: #4a90e2;
|
||||
|
||||
/* ── Spacing ── */
|
||||
--spacing-2xs: 4px;
|
||||
--spacing-xs: 8px;
|
||||
--spacing-sm: 12px;
|
||||
--spacing-md: 16px;
|
||||
--spacing-lg: 24px;
|
||||
--spacing-xl: 32px;
|
||||
--spacing-2xl: 48px;
|
||||
--spacing-3xl: 64px;
|
||||
|
||||
/* ── Radius ── */
|
||||
--radius-sm: 10px;
|
||||
--radius-md: 12px;
|
||||
--radius-lg: 16px;
|
||||
--radius-xl: 20px;
|
||||
--radius-pill: 999px;
|
||||
|
||||
/* ── Shadow / layout ── */
|
||||
--shadow-card: 0 2px 10px rgba(0, 0, 0, 0.04);
|
||||
--shadow-nav: 0 -2px 8px rgba(0, 0, 0, 0.03);
|
||||
--layout-max-user: 430px;
|
||||
--layout-nav-h: 56px;
|
||||
|
||||
/* ── Motion ── */
|
||||
--duration-fast: 150ms;
|
||||
--duration-normal: 200ms;
|
||||
--duration-slow: 300ms;
|
||||
|
||||
/* ── Legacy aliases (do not remove; migrate gradually) ── */
|
||||
--yxg-pri: var(--color-primary);
|
||||
--yxg-blue: var(--color-accent-blue);
|
||||
--yxg-green: var(--color-accent-green);
|
||||
--yxg-purple: var(--color-accent-purple);
|
||||
--yxg-orange: var(--color-accent-orange);
|
||||
--yxg-gold: var(--color-accent-gold);
|
||||
--yxg-text: var(--color-text-primary);
|
||||
--yxg-sub: var(--color-text-secondary);
|
||||
--yxg-bg-start: var(--color-bg-start);
|
||||
--yxg-bg-end: var(--color-bg-end);
|
||||
--yxg-radius: var(--radius-lg);
|
||||
--yxg-nav-h: var(--layout-nav-h);
|
||||
--yxg-max-w: var(--layout-max-user);
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
{
|
||||
"name": "@yuxingu/utils",
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"main": "./src/index.ts",
|
||||
"types": "./src/index.ts",
|
||||
"exports": {
|
||||
".": "./src/index.ts"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
/** Clamp a number into [min, max]. */
|
||||
export function clamp(n: number, min: number, max: number): number {
|
||||
return Math.min(max, Math.max(min, n))
|
||||
}
|
||||
|
||||
/** Pad date part to 2 digits. */
|
||||
export function pad2(n: number): string {
|
||||
return String(n).padStart(2, '0')
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate calendar-ish birth inputs.
|
||||
* Side effect: none. Returns error message or null if ok.
|
||||
*/
|
||||
export function validateBirth(year: number, month: number, day: number): string | null {
|
||||
if (!Number.isFinite(year) || year < 1900 || year > 2100) return '年份无效'
|
||||
if (month < 1 || month > 12) return '月份无效'
|
||||
if (day < 1 || day > 31) return '日期无效'
|
||||
return null
|
||||
}
|
||||
Reference in New Issue
Block a user