Files
Jyotisha/frontend/src/lib/people-archive-view.ts
T
Jesse_ChenandClaude Opus 5.5 2981da6fbc fix(people,ephemeris): style /people and make the ephemeris readable (BUG-1039)
- /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
2026-09-26 06:58:41 +08:00

77 lines
3.3 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* 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;
}