- /people had zero CSS rules for its people-archive classes. Add the approved layout: list -> detail below 1024px (data-view, ?person= history entry so Back returns to the list), two columns at 1024px and above; avatar rows with the current person highlighted, time cards with a rectified state, readable ayanamsa name, three quick actions; delete moves to the bottom of the edit view with unchanged confirm/count logic. - Ephemeris: Chinese-first panchanga names from the brief's table only (panchanga-names-zh.ts, three listed aliases, unknown names stay Sanskrit), coloured 吉/中/凶 tags, N吉/M凶 counts and a fixed-rule day summary (pure function, no model call); the header ask button becomes a bottom hint card; transit rows show glyph, 「行星 在 星座」, house line and degree; the Lahiri note names the subject's own ayanamsa. - API, engine and database unchanged; /people and /ephemeris stay Static. - DESIGN, VOICE, BUG_HISTORY, CHANGELOG, BLOCKED, PROGRESS and a manual checklist updated. Two existing assertions changed, with 原值/新值/原因. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017eEAG8HD3mm8gsKXgk8uU8
77 lines
3.3 KiB
TypeScript
77 lines
3.3 KiB
TypeScript
/**
|
||
* Pure display helpers for /people (TASK-people-ephemeris-ui-20260926 D2–D4).
|
||
* No new facts: every string is read from the stored profile or an existing
|
||
* label table.
|
||
*/
|
||
import { AYANAMSA_OPTIONS, resolveAyanamsa } from "@/lib/ayanamsa";
|
||
import { effectiveBirthDate } from "@/lib/effective-birth-date";
|
||
import { findCity, findProvince, profileBirthTimeLabel } from "@/lib/home-profile";
|
||
import type { Profile } from "@/lib/home-types";
|
||
|
||
/** D4: four existing element tokens; the person's id picks one, stably. */
|
||
export const PEOPLE_AVATAR_TONES = ["fire", "earth", "air", "water"] as const;
|
||
export type PeopleAvatarTone = (typeof PEOPLE_AVATAR_TONES)[number] | "self";
|
||
|
||
export function peopleAvatarTone(id: string, role: "self" | "other"): PeopleAvatarTone {
|
||
if (role === "self") return "self";
|
||
let hash = 0;
|
||
for (const character of id) hash = (hash * 31 + character.codePointAt(0)!) >>> 0;
|
||
return PEOPLE_AVATAR_TONES[hash % PEOPLE_AVATAR_TONES.length];
|
||
}
|
||
|
||
export function peopleDisplayName(profile: Profile, role: "self" | "other"): string {
|
||
return profile.name.trim() || (role === "self" ? "我" : "未命名资料");
|
||
}
|
||
|
||
/** 本人 shows 「我」; everyone else the first character of the name. */
|
||
export function peopleAvatarInitial(profile: Profile, role: "self" | "other"): string {
|
||
if (role === "self") return "我";
|
||
return Array.from(peopleDisplayName(profile, role))[0] ?? "?";
|
||
}
|
||
|
||
function trimCitySuffix(name: string): string {
|
||
return name.length > 2 && name.endsWith("市") ? name.slice(0, -1) : name;
|
||
}
|
||
|
||
/**
|
||
* The city for the one-line list meta. Chinese places come from the location
|
||
* codes; a free-text label keeps its first comma segment, or the city segment
|
||
* of a 「中国 · 省 · 市 · 区」 label. Empty when the place is not complete.
|
||
*/
|
||
export function peopleCityLabel(profile: Profile): string {
|
||
const city = findCity(findProvince(profile.provinceCode), profile.cityCode);
|
||
if (city) return trimCitySuffix(city.name);
|
||
const label = profile.birthPlaceLabel.trim();
|
||
if (!label) return "";
|
||
if (label.includes("·")) {
|
||
const parts = label.split("·").map((part) => part.trim()).filter(Boolean);
|
||
const withoutCountry = parts[0] === "中国" ? parts.slice(1) : parts;
|
||
return trimCitySuffix(withoutCountry[1] ?? withoutCountry[0] ?? "");
|
||
}
|
||
const [first] = label.split(/[,,]/).map((part) => part.trim()).filter(Boolean);
|
||
return first ?? "";
|
||
}
|
||
|
||
/** D2: 「出生日期 · 排盘时间 · 城市」, missing parts dropped. */
|
||
export function peopleRowMeta(profile: Profile): string {
|
||
return [effectiveBirthDate(profile) ?? "", profileBirthTimeLabel(profile), peopleCityLabel(profile)]
|
||
.filter(Boolean)
|
||
.join(" · ");
|
||
}
|
||
|
||
/**
|
||
* D3: a rectification result was adopted or confirmed. The case id is required
|
||
* too: `readProfile` falls back to `confirmed` for any legacy row that simply
|
||
* has a time, and a hospital-record time is not a 校正结果.
|
||
*/
|
||
export function peopleTimeRectified(profile: Profile): boolean {
|
||
return (profile.birthTimeStatus === "accepted" || profile.birthTimeStatus === "confirmed")
|
||
&& profile.rectificationCaseId.trim() !== "";
|
||
}
|
||
|
||
/** D3: 「Raman(默认)」, never the stored lower-case key. */
|
||
export function peopleAyanamsaLabel(profile: Profile): string {
|
||
const value = resolveAyanamsa(profile);
|
||
return AYANAMSA_OPTIONS.find((option) => option.value === value)?.label ?? value;
|
||
}
|