Frontend drops 15 files no page reaches (issue #139, BUG-1480)
Unreachable from every app entry and frontend/scripts (dynamic imports counted): chart-basics-tab, error-toast, seven unused shadcn ui files (skeleton among them), three birth-time helpers only tests used, the motion provider shell (it imported motion/react statically), and the two empty people/account stores. Tests that only exercised them are removed or trimmed, each with old/new/reason notes. Unreachable files 18 -> 3 (all kept on purpose); npm test names drop exactly the 4 removed and 1 renamed test. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
b9a5415e71
commit
2fbce95536
@@ -1,5 +1,10 @@
|
||||
# 印度占星 Skill 更新日志
|
||||
|
||||
## 2026-10-28 — 前端删掉 15 个没人用的文件
|
||||
|
||||
- 删掉 15 个没有任何页面用到的前端文件(约 700 行),其中包括一个会把动效库带回首屏的旧外壳和一个与「不出现骨架屏」规定冲突的组件。用户看到的内容不变。
|
||||
- Skill 版本不变(BUG-1480,issue #139)。
|
||||
|
||||
## 2026-10-28 — VedAstro 限流或不可用时,不再每轮重等
|
||||
|
||||
- VedAstro 被限流或暂时拿不到数据时,同一张盘当天只问一次;之后的提问直接说明「外部校验暂不可用」,不再每轮多等 0.4~1.5 秒。第二天会重新问。
|
||||
|
||||
@@ -20203,3 +20203,18 @@
|
||||
- 关联:BUG-734(健康探测走缓存)、BUG-727 / BUG-301(前台快照)。
|
||||
- 复发自:无
|
||||
- 修复版本:分支 `codex/vedastro-negative-day-20261028`。
|
||||
|
||||
## BUG-1480 | 前端留着 15 个没有任何页面用到的文件
|
||||
|
||||
- 状态:resolved(代码 + 回归测试;待部署)
|
||||
- 首次发现 / 最近更新:2026-10-28 / 2026-10-28(issue #118 代码体检第 24 条 → #139)
|
||||
- 影响面:`frontend/src/`;不影响任何页面(这些文件从任何入口都不可达)。
|
||||
- 现象:从所有页面、布局、接口、错误页入口和 `frontend/scripts/` 出发(计入动态 import),有 18 个文件不可达。其中 `lib/motion/provider.tsx` 静态引用 `motion/react`,将来谁一引用 Motion 就回到首屏;`ui/skeleton` 与 DESIGN「揭幕后不得出现骨架」冲突,留着容易被误用。
|
||||
- 根因:改版和组件库生成后留下的残余,只被测试引用或没人引用。
|
||||
- 修复:删除 15 个:`components/chart-page/chart-basics-tab.tsx`、`components/error-toast.tsx`、`components/ui/{avatar,empty,field,kbd,separator,skeleton,switch}.tsx`、`lib/birth-time-choice-question-model.ts`、`lib/birth-time-guided-terminal.ts`、`lib/birth-time-guided-turn-identity.ts`、`lib/motion/provider.tsx`、`stores/account-store.ts`、`stores/people-store.ts`。保留 `app/forbidden.tsx`(Next 的特殊入口文件)以及进度记录里写过理由的 `reader-appendix-language.ts`、`personal-report-longform-download.ts`。删前对每个文件用 `git grep -- tests/ frontend/` 核对,Python 源码契约测试里没有引用。
|
||||
- 测试变化(只少被删模块的用例):删除 `birth-time-guide-flow.test.ts`(1 条,只测 `guidedTurnIdentity`);`birth-time-choice-question.test.ts` 删 2 条(click intent submits primary and unknown options immediately、unmatched requests clarification without creating scoring evidence);`birth-time-guided-review-fixes.test.ts` 删 1 条(dynamic medium terminal preserves its candidate range without direct adoption),另 1 条去掉 `guidedTerminalPath` 断言;`birth-time-rectification-contract.test.ts` 1 条去掉 `guidedTerminalPath` 断言;`ui-kit-stores.test.ts` 去掉两个空骨架 store 的断言(Home 不引用它们的断言保留);`motion-presets.test.ts`「LazyMotion loads domAnimation asynchronously and honors reduced motion」改名为「the home and the layout never import motion or a motion shell statically」,只留首页与 layout 的静态引用约束。改动处都写了原值 / 新值 / 原因。
|
||||
- 验证:不可达文件 18 → 3(上面保留的 3 个)。全量前端测试与 staging 逐条比对:少的名字正好是上面 4 条删除和 1 条改名,没有新增失败。tsc 0、lint 0 error。删的都是不可达文件,打包内容不变。
|
||||
- 防复发:删除前跑可达性检查并 `git grep -- tests/ frontend/`;只被测试引用的模块连同测试一起删。
|
||||
- 关联:前端死代码清理(frontend-dead-code 进度记录)。
|
||||
- 复发自:无
|
||||
- 修复版本:分支 `codex/frontend-dead-files-20261028`。
|
||||
|
||||
@@ -1,20 +0,0 @@
|
||||
import type { ChartViewOk } from "@/lib/chart-view-contract";
|
||||
import { formatDegree } from "./chart-format";
|
||||
|
||||
export function ChartBasicsTab({ view }: { view: ChartViewOk }) {
|
||||
return (
|
||||
<div className="chart-page-basics">
|
||||
<div className="chart-page-basics-grid">
|
||||
{view.vedic.planets.map((planet) => (
|
||||
<article key={planet.name} className="chart-page-planet-card">
|
||||
<h3>{planet.label}</h3>
|
||||
<p>本命 {planet.signLabel} {formatDegree(planet.degreeInSign)} · 第{planet.house}宫</p>
|
||||
<p>{planet.nakshatra} 第{planet.pada}足 · 星宿主 {planet.nakshatraLord}</p>
|
||||
<p>D9 {planet.d9SignLabel ?? "未返回"}</p>
|
||||
{planet.symbol ? <p className="chart-page-symbol">{planet.symbol}</p> : null}
|
||||
</article>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,17 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect } from "react";
|
||||
|
||||
import { notifyError } from "@/lib/notify";
|
||||
|
||||
/**
|
||||
* An error a flow keeps in state, said once as a toast whenever it changes to
|
||||
* a new message (2026-09-30: no inline error bars). The message is the toast's
|
||||
* id, so a remount repeats nothing on screen. Renders nothing.
|
||||
*/
|
||||
export function ErrorToast({ message }: Readonly<{ message: string | null | undefined }>) {
|
||||
useEffect(() => {
|
||||
if (message) notifyError(message, { id: `error:${message}` });
|
||||
}, [message]);
|
||||
return null;
|
||||
}
|
||||
@@ -1,108 +0,0 @@
|
||||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
import { Avatar as AvatarPrimitive } from "@base-ui/react/avatar"
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
function Avatar({
|
||||
className,
|
||||
size = "default",
|
||||
...props
|
||||
}: AvatarPrimitive.Root.Props & {
|
||||
size?: "default" | "sm" | "lg"
|
||||
}) {
|
||||
return (
|
||||
<AvatarPrimitive.Root
|
||||
data-slot="avatar"
|
||||
data-size={size}
|
||||
className={cn(
|
||||
"group/avatar relative flex size-8 shrink-0 rounded-full select-none after:absolute after:inset-0 after:rounded-full after:border after:border-border after:mix-blend-darken data-[size=lg]:size-10 data-[size=sm]:size-6 dark:after:mix-blend-lighten",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function AvatarImage({ className, ...props }: AvatarPrimitive.Image.Props) {
|
||||
return (
|
||||
<AvatarPrimitive.Image
|
||||
data-slot="avatar-image"
|
||||
className={cn(
|
||||
"aspect-square size-full rounded-full object-cover",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function AvatarFallback({
|
||||
className,
|
||||
...props
|
||||
}: AvatarPrimitive.Fallback.Props) {
|
||||
return (
|
||||
<AvatarPrimitive.Fallback
|
||||
data-slot="avatar-fallback"
|
||||
className={cn(
|
||||
"flex size-full items-center justify-center rounded-full bg-muted text-sm text-muted-foreground group-data-[size=sm]/avatar:text-xs",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function AvatarBadge({ className, ...props }: React.ComponentProps<"span">) {
|
||||
return (
|
||||
<span
|
||||
data-slot="avatar-badge"
|
||||
className={cn(
|
||||
"absolute right-0 bottom-0 z-10 inline-flex items-center justify-center rounded-full bg-primary text-primary-foreground bg-blend-color ring-2 ring-background select-none",
|
||||
"group-data-[size=sm]/avatar:size-2 group-data-[size=sm]/avatar:[&>svg]:hidden",
|
||||
"group-data-[size=default]/avatar:size-2.5 group-data-[size=default]/avatar:[&>svg]:size-2",
|
||||
"group-data-[size=lg]/avatar:size-3 group-data-[size=lg]/avatar:[&>svg]:size-2",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function AvatarGroup({ className, ...props }: React.ComponentProps<"div">) {
|
||||
return (
|
||||
<div
|
||||
data-slot="avatar-group"
|
||||
className={cn(
|
||||
"group/avatar-group flex -space-x-2 *:data-[slot=avatar]:ring-2 *:data-[slot=avatar]:ring-background",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function AvatarGroupCount({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<"div">) {
|
||||
return (
|
||||
<div
|
||||
data-slot="avatar-group-count"
|
||||
className={cn(
|
||||
"relative flex size-8 shrink-0 items-center justify-center rounded-full bg-muted text-sm text-muted-foreground ring-2 ring-background group-has-data-[size=lg]/avatar-group:size-10 group-has-data-[size=sm]/avatar-group:size-6 [&>svg]:size-4 group-has-data-[size=lg]/avatar-group:[&>svg]:size-5 group-has-data-[size=sm]/avatar-group:[&>svg]:size-3",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export {
|
||||
Avatar,
|
||||
AvatarImage,
|
||||
AvatarFallback,
|
||||
AvatarGroup,
|
||||
AvatarGroupCount,
|
||||
AvatarBadge,
|
||||
}
|
||||
@@ -1,103 +0,0 @@
|
||||
import { cva, type VariantProps } from "class-variance-authority"
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
function Empty({ className, ...props }: React.ComponentProps<"div">) {
|
||||
return (
|
||||
<div
|
||||
data-slot="empty"
|
||||
className={cn(
|
||||
"flex w-full min-w-0 flex-1 flex-col items-center justify-center gap-4 rounded-xl border-dashed p-6 text-center text-balance",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function EmptyHeader({ className, ...props }: React.ComponentProps<"div">) {
|
||||
return (
|
||||
<div
|
||||
data-slot="empty-header"
|
||||
className={cn("flex max-w-sm flex-col items-center gap-2", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
const emptyMediaVariants = cva(
|
||||
"mb-2 flex shrink-0 items-center justify-center [&_svg]:pointer-events-none [&_svg]:shrink-0",
|
||||
{
|
||||
variants: {
|
||||
variant: {
|
||||
default: "bg-transparent",
|
||||
icon: "flex size-8 shrink-0 items-center justify-center rounded-lg bg-muted text-foreground [&_svg:not([class*='size-'])]:size-4",
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
variant: "default",
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
function EmptyMedia({
|
||||
className,
|
||||
variant = "default",
|
||||
...props
|
||||
}: React.ComponentProps<"div"> & VariantProps<typeof emptyMediaVariants>) {
|
||||
return (
|
||||
<div
|
||||
data-slot="empty-icon"
|
||||
data-variant={variant}
|
||||
className={cn(emptyMediaVariants({ variant, className }))}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function EmptyTitle({ className, ...props }: React.ComponentProps<"div">) {
|
||||
return (
|
||||
<div
|
||||
data-slot="empty-title"
|
||||
className={cn(
|
||||
"text-sm font-medium tracking-tight",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function EmptyDescription({ className, ...props }: React.ComponentProps<"p">) {
|
||||
return (
|
||||
<div
|
||||
data-slot="empty-description"
|
||||
className={cn(
|
||||
"text-sm/relaxed text-muted-foreground [&>a]:underline [&>a]:underline-offset-4 [&>a:hover]:text-primary",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function EmptyContent({ className, ...props }: React.ComponentProps<"div">) {
|
||||
return (
|
||||
<div
|
||||
data-slot="empty-content"
|
||||
className={cn(
|
||||
"flex w-full max-w-sm min-w-0 flex-col items-center gap-2.5 text-sm text-balance",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export {
|
||||
Empty,
|
||||
EmptyHeader,
|
||||
EmptyTitle,
|
||||
EmptyDescription,
|
||||
EmptyContent,
|
||||
EmptyMedia,
|
||||
}
|
||||
@@ -1,238 +0,0 @@
|
||||
"use client"
|
||||
|
||||
import { useMemo } from "react"
|
||||
import { cva, type VariantProps } from "class-variance-authority"
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
import { Label } from "@/components/ui/label"
|
||||
import { Separator } from "@/components/ui/separator"
|
||||
|
||||
function FieldSet({ className, ...props }: React.ComponentProps<"fieldset">) {
|
||||
return (
|
||||
<fieldset
|
||||
data-slot="field-set"
|
||||
className={cn(
|
||||
"flex flex-col gap-4 has-[>[data-slot=checkbox-group]]:gap-3 has-[>[data-slot=radio-group]]:gap-3",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function FieldLegend({
|
||||
className,
|
||||
variant = "legend",
|
||||
...props
|
||||
}: React.ComponentProps<"legend"> & { variant?: "legend" | "label" }) {
|
||||
return (
|
||||
<legend
|
||||
data-slot="field-legend"
|
||||
data-variant={variant}
|
||||
className={cn(
|
||||
"mb-1.5 font-medium data-[variant=label]:text-sm data-[variant=legend]:text-base",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function FieldGroup({ className, ...props }: React.ComponentProps<"div">) {
|
||||
return (
|
||||
<div
|
||||
data-slot="field-group"
|
||||
className={cn(
|
||||
"group/field-group @container/field-group flex w-full flex-col gap-5 data-[slot=checkbox-group]:gap-3 *:data-[slot=field-group]:gap-4",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
const fieldVariants = cva(
|
||||
"group/field flex w-full gap-2 data-[invalid=true]:text-destructive",
|
||||
{
|
||||
variants: {
|
||||
orientation: {
|
||||
vertical: "flex-col *:w-full [&>.sr-only]:w-auto",
|
||||
horizontal:
|
||||
"flex-row items-center has-[>[data-slot=field-content]]:items-start *:data-[slot=field-label]:flex-auto has-[>[data-slot=field-content]]:[&>[role=checkbox],[role=radio]]:mt-px",
|
||||
responsive:
|
||||
"flex-col *:w-full @md/field-group:flex-row @md/field-group:items-center @md/field-group:*:w-auto @md/field-group:has-[>[data-slot=field-content]]:items-start @md/field-group:*:data-[slot=field-label]:flex-auto [&>.sr-only]:w-auto @md/field-group:has-[>[data-slot=field-content]]:[&>[role=checkbox],[role=radio]]:mt-px",
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
orientation: "vertical",
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
function Field({
|
||||
className,
|
||||
orientation = "vertical",
|
||||
...props
|
||||
}: React.ComponentProps<"div"> & VariantProps<typeof fieldVariants>) {
|
||||
return (
|
||||
<div
|
||||
role="group"
|
||||
data-slot="field"
|
||||
data-orientation={orientation}
|
||||
className={cn(fieldVariants({ orientation }), className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function FieldContent({ className, ...props }: React.ComponentProps<"div">) {
|
||||
return (
|
||||
<div
|
||||
data-slot="field-content"
|
||||
className={cn(
|
||||
"group/field-content flex flex-1 flex-col gap-0.5 leading-snug",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function FieldLabel({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof Label>) {
|
||||
return (
|
||||
<Label
|
||||
data-slot="field-label"
|
||||
className={cn(
|
||||
"group/field-label peer/field-label flex w-fit gap-2 leading-snug group-data-[disabled=true]/field:opacity-50 has-data-checked:border-primary/30 has-data-checked:bg-primary/5 has-[>[data-slot=field]]:rounded-lg has-[>[data-slot=field]]:border has-[>[data-slot=field]]:not-has-[:disabled,[data-disabled]]:hover:bg-muted/50 has-[>[data-slot=field]]:has-[:focus-visible]:border-ring has-[>[data-slot=field]]:has-[:focus-visible]:ring-3 has-[>[data-slot=field]]:has-[:focus-visible]:ring-ring/50 *:data-[slot=field]:p-2.5 dark:has-data-checked:border-primary/20 dark:has-data-checked:bg-primary/10",
|
||||
"has-[>[data-slot=field]]:w-full has-[>[data-slot=field]]:flex-col",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function FieldTitle({ className, ...props }: React.ComponentProps<"div">) {
|
||||
return (
|
||||
<div
|
||||
data-slot="field-label"
|
||||
className={cn(
|
||||
"flex w-fit items-center gap-2 text-sm font-medium group-data-[disabled=true]/field:opacity-50",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function FieldDescription({ className, ...props }: React.ComponentProps<"p">) {
|
||||
return (
|
||||
<p
|
||||
data-slot="field-description"
|
||||
className={cn(
|
||||
"text-left text-sm leading-normal font-normal text-muted-foreground group-has-data-horizontal/field:text-balance [[data-variant=legend]+&]:-mt-1.5",
|
||||
"last:mt-0 nth-last-2:-mt-1",
|
||||
"[&>a]:underline [&>a]:underline-offset-4 [&>a:hover]:text-primary",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function FieldSeparator({
|
||||
children,
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<"div"> & {
|
||||
children?: React.ReactNode
|
||||
}) {
|
||||
return (
|
||||
<div
|
||||
data-slot="field-separator"
|
||||
data-content={!!children}
|
||||
className={cn(
|
||||
"relative -my-2 h-5 text-sm group-data-[variant=outline]/field-group:-mb-2",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<Separator className="absolute inset-0 top-1/2" />
|
||||
{children && (
|
||||
<span
|
||||
className="relative mx-auto block w-fit bg-background px-2 text-muted-foreground"
|
||||
data-slot="field-separator-content"
|
||||
>
|
||||
{children}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function FieldError({
|
||||
className,
|
||||
children,
|
||||
errors,
|
||||
...props
|
||||
}: React.ComponentProps<"div"> & {
|
||||
errors?: Array<{ message?: string } | undefined>
|
||||
}) {
|
||||
const content = useMemo(() => {
|
||||
if (children) {
|
||||
return children
|
||||
}
|
||||
|
||||
if (!errors?.length) {
|
||||
return null
|
||||
}
|
||||
|
||||
const uniqueErrors = [
|
||||
...new Map(errors.map((error) => [error?.message, error])).values(),
|
||||
]
|
||||
|
||||
if (uniqueErrors?.length == 1) {
|
||||
return uniqueErrors[0]?.message
|
||||
}
|
||||
|
||||
return (
|
||||
<ul className="ml-4 flex list-disc flex-col gap-1">
|
||||
{uniqueErrors.map(
|
||||
(error, index) =>
|
||||
error?.message && <li key={index}>{error.message}</li>
|
||||
)}
|
||||
</ul>
|
||||
)
|
||||
}, [children, errors])
|
||||
|
||||
if (!content) {
|
||||
return null
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
role="alert"
|
||||
data-slot="field-error"
|
||||
className={cn("text-sm font-normal text-destructive", className)}
|
||||
{...props}
|
||||
>
|
||||
{content}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export {
|
||||
Field,
|
||||
FieldLabel,
|
||||
FieldDescription,
|
||||
FieldError,
|
||||
FieldGroup,
|
||||
FieldLegend,
|
||||
FieldSeparator,
|
||||
FieldSet,
|
||||
FieldContent,
|
||||
FieldTitle,
|
||||
}
|
||||
@@ -1,26 +0,0 @@
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
function Kbd({ className, ...props }: React.ComponentProps<"kbd">) {
|
||||
return (
|
||||
<kbd
|
||||
data-slot="kbd"
|
||||
className={cn(
|
||||
"pointer-events-none inline-flex h-5 w-fit min-w-5 items-center justify-center gap-1 rounded-sm bg-muted px-1 font-sans text-xs font-medium text-muted-foreground select-none in-data-[slot=tooltip-content]:bg-background/20 in-data-[slot=tooltip-content]:text-background dark:in-data-[slot=tooltip-content]:bg-background/10 [&_svg:not([class*='size-'])]:size-3",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function KbdGroup({ className, ...props }: React.ComponentProps<"div">) {
|
||||
return (
|
||||
<kbd
|
||||
data-slot="kbd-group"
|
||||
className={cn("inline-flex items-center gap-1", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export { Kbd, KbdGroup }
|
||||
@@ -1,24 +0,0 @@
|
||||
"use client"
|
||||
|
||||
import { Separator as SeparatorPrimitive } from "@base-ui/react/separator"
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
function Separator({
|
||||
className,
|
||||
orientation = "horizontal",
|
||||
...props
|
||||
}: SeparatorPrimitive.Props) {
|
||||
return (
|
||||
<SeparatorPrimitive
|
||||
data-slot="separator"
|
||||
orientation={orientation}
|
||||
className={cn(
|
||||
"shrink-0 bg-border data-horizontal:h-px data-horizontal:w-full data-vertical:w-px data-vertical:self-stretch",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export { Separator }
|
||||
@@ -1,13 +0,0 @@
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
function Skeleton({ className, ...props }: React.ComponentProps<"div">) {
|
||||
return (
|
||||
<div
|
||||
data-slot="skeleton"
|
||||
className={cn("animate-pulse rounded-md bg-muted", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export { Skeleton }
|
||||
@@ -1,31 +0,0 @@
|
||||
"use client"
|
||||
|
||||
import { Switch as SwitchPrimitive } from "@base-ui/react/switch"
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
function Switch({
|
||||
className,
|
||||
size = "default",
|
||||
...props
|
||||
}: SwitchPrimitive.Root.Props & {
|
||||
size?: "sm" | "default"
|
||||
}) {
|
||||
return (
|
||||
<SwitchPrimitive.Root
|
||||
data-slot="switch"
|
||||
data-size={size}
|
||||
className={cn(
|
||||
"peer group/switch relative inline-flex shrink-0 items-center rounded-full border border-transparent transition-all outline-none group-has-[:focus-visible]/field-label:border-transparent group-has-[:focus-visible]/field-label:ring-0 after:absolute after:-inset-x-3 after:-inset-y-2 focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-ring/50 aria-invalid:border-destructive aria-invalid:ring-3 aria-invalid:ring-destructive/20 data-[size=default]:h-[18.4px] data-[size=default]:w-[32px] data-[size=sm]:h-[14px] data-[size=sm]:w-[24px] dark:aria-invalid:border-destructive/50 dark:aria-invalid:ring-destructive/40 data-checked:bg-primary data-unchecked:bg-input dark:data-unchecked:bg-input/80 data-disabled:cursor-not-allowed data-disabled:opacity-50",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<SwitchPrimitive.Thumb
|
||||
data-slot="switch-thumb"
|
||||
className="pointer-events-none block rounded-full bg-background ring-0 transition-transform group-data-[size=default]/switch:size-4 group-data-[size=sm]/switch:size-3 group-data-[size=default]/switch:data-checked:translate-x-[calc(100%-2px)] group-data-[size=sm]/switch:data-checked:translate-x-[calc(100%-2px)] dark:data-checked:bg-primary-foreground group-data-[size=default]/switch:data-unchecked:translate-x-0 group-data-[size=sm]/switch:data-unchecked:translate-x-0 dark:data-unchecked:bg-foreground"
|
||||
/>
|
||||
</SwitchPrimitive.Root>
|
||||
)
|
||||
}
|
||||
|
||||
export { Switch }
|
||||
@@ -1,50 +0,0 @@
|
||||
import type {
|
||||
PublicDynamicChoiceQuestion,
|
||||
TimeRange,
|
||||
} from "./birth-time-dynamic-choice.ts";
|
||||
|
||||
type ChoiceOption = PublicDynamicChoiceQuestion["options"][number];
|
||||
|
||||
export type ChoiceQuestionGroups = {
|
||||
readonly primary: readonly ChoiceOption[];
|
||||
readonly unknown: ChoiceOption;
|
||||
readonly unmatched: ChoiceOption;
|
||||
};
|
||||
|
||||
export type ChoiceSelectionIntent = {
|
||||
readonly kind: "submit";
|
||||
readonly optionId: string;
|
||||
readonly effective: boolean;
|
||||
};
|
||||
|
||||
export class ChoiceQuestionModelError extends Error {
|
||||
readonly name = "ChoiceQuestionModelError";
|
||||
}
|
||||
|
||||
export function choiceQuestionGroups(
|
||||
question: PublicDynamicChoiceQuestion,
|
||||
): ChoiceQuestionGroups {
|
||||
const primary = question.options.filter((option) => option.kind === "primary");
|
||||
const unknown = question.options.find((option) => option.kind === "unknown");
|
||||
const unmatched = question.options.find((option) => option.kind === "unmatched");
|
||||
if (!unknown || !unmatched) {
|
||||
throw new ChoiceQuestionModelError("Dynamic question is missing a reserved choice");
|
||||
}
|
||||
return { primary, unknown, unmatched };
|
||||
}
|
||||
|
||||
export function choiceSelectionIntent(option: ChoiceOption): ChoiceSelectionIntent {
|
||||
return {
|
||||
kind: "submit",
|
||||
optionId: option.optionId,
|
||||
effective: option.kind === "primary",
|
||||
};
|
||||
}
|
||||
|
||||
export function normalizeUnmatchedNote(note: string): string {
|
||||
return Array.from(note.trim()).slice(0, 240).join("");
|
||||
}
|
||||
|
||||
export function rangeLabel(range: TimeRange): string {
|
||||
return `${range.startTime}—${range.endTime}`;
|
||||
}
|
||||
@@ -1,29 +0,0 @@
|
||||
import type { JourneyClientResponse } from "./birth-time-journey-response-schema.ts";
|
||||
|
||||
export type GuidedTerminalPath =
|
||||
| {
|
||||
readonly kind: "edit_birth_time_details";
|
||||
readonly preservesCase: true;
|
||||
readonly appliesCandidateTime: false;
|
||||
}
|
||||
| {
|
||||
readonly kind: "complete_with_candidate";
|
||||
readonly time: string;
|
||||
readonly preservesCase: true;
|
||||
readonly appliesCandidateTime: true;
|
||||
};
|
||||
|
||||
export function guidedTerminalPath(journey: JourneyClientResponse): GuidedTerminalPath | null {
|
||||
const kind = journey.nextAction.kind;
|
||||
if (journey.journeyProtocol === "dynamic-choice-v2"
|
||||
&& (kind === "present_low_result" || kind === "present_medium_result" || kind === "candidate_saved")) {
|
||||
return {
|
||||
kind: "edit_birth_time_details",
|
||||
preservesCase: true,
|
||||
appliesCandidateTime: false,
|
||||
};
|
||||
}
|
||||
return kind === "present_low_result" || kind === "candidate_saved"
|
||||
? { kind: "edit_birth_time_details", preservesCase: true, appliesCandidateTime: false }
|
||||
: null;
|
||||
}
|
||||
@@ -1,3 +0,0 @@
|
||||
export function guidedTurnIdentity(turnVersion: number, questionId: string): string {
|
||||
return `${turnVersion}:${questionId}`;
|
||||
}
|
||||
@@ -1,16 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import { LazyMotion, MotionConfig } from "motion/react";
|
||||
import type { ReactNode } from "react";
|
||||
|
||||
const loadDomAnimation = () =>
|
||||
import("motion/react").then((module) => module.domAnimation);
|
||||
|
||||
/** Not mounted in K0. Callers use `m` from motion/react inside this tree. */
|
||||
export function MotionProvider({ children }: { children: ReactNode }) {
|
||||
return (
|
||||
<LazyMotion features={loadDomAnimation} strict>
|
||||
<MotionConfig reducedMotion="user">{children}</MotionConfig>
|
||||
</LazyMotion>
|
||||
);
|
||||
}
|
||||
@@ -1,12 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import { create } from "zustand";
|
||||
|
||||
/** Skeleton. Account state moves here in a later issue, not in K0. */
|
||||
type AccountState = {
|
||||
placeholder: true;
|
||||
};
|
||||
|
||||
export const useAccountStore = create<AccountState>(() => ({
|
||||
placeholder: true,
|
||||
}));
|
||||
@@ -1,12 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import { create } from "zustand";
|
||||
|
||||
/** Skeleton. People-archive state moves here in a later issue, not in K0. */
|
||||
type PeopleState = {
|
||||
placeholder: true;
|
||||
};
|
||||
|
||||
export const usePeopleStore = create<PeopleState>(() => ({
|
||||
placeholder: true,
|
||||
}));
|
||||
@@ -1,10 +1,5 @@
|
||||
import assert from "node:assert/strict";
|
||||
import test from "node:test";
|
||||
import {
|
||||
choiceQuestionGroups,
|
||||
choiceSelectionIntent,
|
||||
normalizeUnmatchedNote,
|
||||
} from "../src/lib/birth-time-choice-question-model.ts";
|
||||
import type { PublicDynamicChoiceQuestion } from "../src/lib/birth-time-dynamic-choice.ts";
|
||||
import {
|
||||
advanceDynamicBirthTimePreview,
|
||||
@@ -22,28 +17,6 @@ const question: PublicDynamicChoiceQuestion = {
|
||||
],
|
||||
};
|
||||
|
||||
test("click intent submits primary and unknown options immediately", () => {
|
||||
const groups = choiceQuestionGroups(question);
|
||||
|
||||
assert.deepEqual(groups.primary.map((option) => option.optionId), ["early", "late"]);
|
||||
assert.deepEqual(choiceSelectionIntent(groups.primary[0]), {
|
||||
kind: "submit", optionId: "early", effective: true,
|
||||
});
|
||||
assert.deepEqual(choiceSelectionIntent(groups.unknown), {
|
||||
kind: "submit", optionId: "unknown", effective: false,
|
||||
});
|
||||
});
|
||||
|
||||
test("unmatched requests clarification without creating scoring evidence", () => {
|
||||
const groups = choiceQuestionGroups(question);
|
||||
|
||||
assert.deepEqual(choiceSelectionIntent(groups.unmatched), {
|
||||
kind: "submit", optionId: "unmatched", effective: false,
|
||||
});
|
||||
assert.equal(normalizeUnmatchedNote(" 大约是在搬家之后 "), "大约是在搬家之后");
|
||||
assert.equal(normalizeUnmatchedNote("甲".repeat(260)).length, 240);
|
||||
});
|
||||
|
||||
test("preview choices expose real local interaction states without authentication", () => {
|
||||
const start = dynamicBirthTimePreview();
|
||||
const nextQuestion = advanceDynamicBirthTimePreview(start, { kind: "select", optionId: "earlier" });
|
||||
|
||||
@@ -1,11 +0,0 @@
|
||||
import assert from "node:assert/strict";
|
||||
import test from "node:test";
|
||||
import { guidedTurnIdentity } from "../src/lib/birth-time-guided-turn-identity.ts";
|
||||
|
||||
test("persisted questions receive stable identities without leaking prior input", () => {
|
||||
assert.equal(guidedTurnIdentity(3, "education_entry"), "3:education_entry");
|
||||
assert.notEqual(
|
||||
guidedTurnIdentity(3, "education_entry"),
|
||||
guidedTurnIdentity(4, "relationship_entry"),
|
||||
);
|
||||
});
|
||||
@@ -7,7 +7,6 @@ import {
|
||||
scheduleCancellableStart,
|
||||
} from "../src/lib/birth-time-guided-effect-coordinator.ts";
|
||||
import { confirmReviewedBirthTimeDraft } from "../src/lib/birth-time-guided-draft-confirmation.ts";
|
||||
import { guidedTerminalPath } from "../src/lib/birth-time-guided-terminal.ts";
|
||||
import { parseJourneyResponse } from "../src/lib/birth-time-journey-client.ts";
|
||||
import { dynamicBirthTimePreview } from "../src/lib/birth-time-dynamic-preview.ts";
|
||||
import { guidedBirthTimePreview } from "../src/lib/birth-time-guided-preview.ts";
|
||||
@@ -45,26 +44,11 @@ test("low without a result and saved medium both return to declared-time editing
|
||||
const saved = guidedBirthTimePreview("birth-time-rectification-saved");
|
||||
|
||||
assert.equal(dynamicLow.candidateResult, null);
|
||||
assert.deepEqual(guidedTerminalPath(nullResultLow), {
|
||||
kind: "edit_birth_time_details",
|
||||
preservesCase: true,
|
||||
appliesCandidateTime: false,
|
||||
});
|
||||
assert.deepEqual(guidedTerminalPath(saved), {
|
||||
kind: "edit_birth_time_details",
|
||||
preservesCase: true,
|
||||
appliesCandidateTime: false,
|
||||
});
|
||||
});
|
||||
|
||||
test("dynamic medium terminal preserves its candidate range without direct adoption", () => {
|
||||
const medium = dynamicBirthTimePreview("medium");
|
||||
|
||||
assert.deepEqual(guidedTerminalPath(medium), {
|
||||
kind: "edit_birth_time_details",
|
||||
preservesCase: true,
|
||||
appliesCandidateTime: false,
|
||||
});
|
||||
// 原值:另断言 guidedTerminalPath(nullResultLow / saved) 都回到改出生时间
|
||||
// 新值:只断言这两种状态仍能解析
|
||||
// 原因:issue #139 guidedTerminalPath 只被测试引用,产品里没有调用方,已删除
|
||||
assert.equal(nullResultLow.nextAction.kind, "present_low_result");
|
||||
assert.ok(saved.snapshot);
|
||||
});
|
||||
|
||||
test("candidate completion hook cannot invoke the legacy endpoint for dynamic turns", () => {
|
||||
|
||||
@@ -4,7 +4,6 @@ import {
|
||||
guidedBirthTimePreview,
|
||||
isGuidedBirthTimePreview,
|
||||
} from "../src/lib/birth-time-guided-preview.ts";
|
||||
import { guidedTerminalPath } from "../src/lib/birth-time-guided-terminal.ts";
|
||||
|
||||
/* Legacy pre-dynamic-choice contract assertions intentionally omitted. */
|
||||
/*
|
||||
@@ -74,9 +73,5 @@ test("low-confidence preview mirrors the persisted dynamic terminal state", () =
|
||||
assert.ok(low.candidateResult);
|
||||
assert.equal(low.candidateResult.winningSegment?.representativeTime, "05:21");
|
||||
assert.equal(low.nextAction.resultId, low.candidateResult.resultId);
|
||||
assert.deepEqual(guidedTerminalPath(low), {
|
||||
kind: "edit_birth_time_details",
|
||||
preservesCase: true,
|
||||
appliesCandidateTime: false,
|
||||
});
|
||||
// 原值:另断言 guidedTerminalPath(low) 回到改出生时间;原因:issue #139 该函数只被测试引用,已删除
|
||||
});
|
||||
|
||||
@@ -3,9 +3,7 @@ import { readFileSync } from "node:fs";
|
||||
import test from "node:test";
|
||||
|
||||
import { easeOut, easeSpring, motionPresets } from "../src/lib/motion/presets.ts";
|
||||
import { MotionProvider } from "../src/lib/motion/provider.tsx";
|
||||
|
||||
const providerSource = readFileSync(new URL("../src/lib/motion/provider.tsx", import.meta.url), "utf8");
|
||||
const pageSource = readFileSync(new URL("../src/app/(app)/page.tsx", import.meta.url), "utf8");
|
||||
const layoutSource = readFileSync(new URL("../src/app/layout.tsx", import.meta.url), "utf8");
|
||||
|
||||
@@ -66,13 +64,10 @@ test("every site-motion row is a preset and nothing else is invented", () => {
|
||||
}
|
||||
});
|
||||
|
||||
test("LazyMotion loads domAnimation asynchronously and honors reduced motion", () => {
|
||||
assert.equal(typeof MotionProvider, "function");
|
||||
assert.match(providerSource, /LazyMotion/);
|
||||
assert.match(providerSource, /MotionConfig/);
|
||||
assert.match(providerSource, /reducedMotion="user"/);
|
||||
assert.match(providerSource, /import\("motion\/react"\)\.then\(\(module\) => module\.domAnimation\)/);
|
||||
assert.doesNotMatch(providerSource, /domMax/);
|
||||
test("the home and the layout never import motion or a motion shell statically", () => {
|
||||
// 原值:本条还断言 lib/motion/provider.tsx(LazyMotion 外壳)的写法
|
||||
// 新值:该外壳没有任何产品引用,issue #139 已删除;只留首页与 layout 的静态引用约束
|
||||
// 原因:留着它,将来一引用 Motion 就回到首屏
|
||||
// 原值:首页不得引用 motion/react、动效外壳和任何一个 store。
|
||||
// 新值:首页仍不得静态引用 motion 或动效外壳;可以引用会话 store,不得引用另外三个。
|
||||
// 原因:K2 把对话状态放进会话 store。动效仍按需加载,不进首页同步包。
|
||||
|
||||
@@ -2,8 +2,6 @@ import assert from "node:assert/strict";
|
||||
import { readFileSync } from "node:fs";
|
||||
import test from "node:test";
|
||||
|
||||
import { useAccountStore } from "../src/stores/account-store.ts";
|
||||
import { usePeopleStore } from "../src/stores/people-store.ts";
|
||||
import { useSessionStore } from "../src/stores/session-store.ts";
|
||||
import { useUiStore } from "../src/stores/ui-store.ts";
|
||||
|
||||
@@ -15,8 +13,7 @@ test("K0 store skeletons exist and Home() does not move state into them", () =>
|
||||
// 原因:K2 把对话状态放进会话 store,Home() 的 useState 从 33 降到 25。另外三个还没接上。
|
||||
assert.equal(useSessionStore.getState().placeholder, true);
|
||||
assert.equal(typeof useSessionStore.getState().setSessions, "function");
|
||||
assert.equal(usePeopleStore.getState().placeholder, true);
|
||||
assert.equal(useAccountStore.getState().placeholder, true);
|
||||
// 原值:人物、账户两个 store 也是空骨架;新值:两者从未接上,issue #139 已删除
|
||||
assert.equal(useUiStore.getState().placeholder, true);
|
||||
assert.match(home, /useSessionStore/);
|
||||
assert.doesNotMatch(home, /usePeopleStore|useAccountStore|useUiStore/);
|
||||
|
||||
Reference in New Issue
Block a user