@zmzai/theme 0.4.1 → 0.5.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/package.json
CHANGED
package/src/components/index.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
// @zmzai/theme — Components barrel (
|
|
1
|
+
// @zmzai/theme — Components barrel (54 components)
|
|
2
2
|
|
|
3
3
|
// === Basic ===
|
|
4
4
|
export * from "./button";
|
|
@@ -76,6 +76,7 @@ export * from "./card-3d";
|
|
|
76
76
|
|
|
77
77
|
// === Utilities ===
|
|
78
78
|
export * from "./terminal";
|
|
79
|
+
export * from "./landing";
|
|
79
80
|
export * from "./keyboard";
|
|
80
81
|
export * from "./notch";
|
|
81
82
|
export * from "./sticky-banner";
|
|
@@ -0,0 +1,392 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
useEffect,
|
|
5
|
+
useRef,
|
|
6
|
+
useState,
|
|
7
|
+
type AnchorHTMLAttributes,
|
|
8
|
+
type ButtonHTMLAttributes,
|
|
9
|
+
type ReactNode,
|
|
10
|
+
} from "react";
|
|
11
|
+
|
|
12
|
+
import { cn } from "../../utils/cn";
|
|
13
|
+
import { Navbar, type NavbarProps } from "../navbar";
|
|
14
|
+
import "./landing.css";
|
|
15
|
+
|
|
16
|
+
/* ════════════════════════════════════════════════════════════
|
|
17
|
+
Landing kit — open-design.ai 风格落地页组件。
|
|
18
|
+
样式见 landing.css(token 由消费端 @theme 提供)。
|
|
19
|
+
════════════════════════════════════════════════════════════ */
|
|
20
|
+
|
|
21
|
+
/* ── LandingButton — 胶囊按钮(solid/outline),href 时渲染 a ── */
|
|
22
|
+
type LandingButtonBase = {
|
|
23
|
+
variant?: "solid" | "outline";
|
|
24
|
+
/** 尾部滑入箭头 → */
|
|
25
|
+
arrow?: boolean;
|
|
26
|
+
href?: string;
|
|
27
|
+
className?: string;
|
|
28
|
+
children?: ReactNode;
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
export type LandingButtonProps = LandingButtonBase &
|
|
32
|
+
Omit<AnchorHTMLAttributes<HTMLAnchorElement>, keyof LandingButtonBase> &
|
|
33
|
+
Omit<ButtonHTMLAttributes<HTMLButtonElement>, keyof LandingButtonBase>;
|
|
34
|
+
|
|
35
|
+
export function LandingButton({
|
|
36
|
+
variant = "solid",
|
|
37
|
+
arrow = false,
|
|
38
|
+
href,
|
|
39
|
+
className,
|
|
40
|
+
children,
|
|
41
|
+
...rest
|
|
42
|
+
}: LandingButtonProps) {
|
|
43
|
+
const cls = cn("landing-btn", variant === "solid" ? "btn-solid" : "btn-outline", className);
|
|
44
|
+
const inner = (
|
|
45
|
+
<>
|
|
46
|
+
{children}
|
|
47
|
+
{arrow && <span className="arrow-slide">→</span>}
|
|
48
|
+
</>
|
|
49
|
+
);
|
|
50
|
+
if (href) {
|
|
51
|
+
return (
|
|
52
|
+
<a href={href} className={cls} {...(rest as AnchorHTMLAttributes<HTMLAnchorElement>)}>
|
|
53
|
+
{inner}
|
|
54
|
+
</a>
|
|
55
|
+
);
|
|
56
|
+
}
|
|
57
|
+
return (
|
|
58
|
+
<button type="button" className={cls} {...(rest as ButtonHTMLAttributes<HTMLButtonElement>)}>
|
|
59
|
+
{inner}
|
|
60
|
+
</button>
|
|
61
|
+
);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/* ── PillTabs — 胶囊标签栏 ── */
|
|
65
|
+
export interface PillTabItem {
|
|
66
|
+
value: string;
|
|
67
|
+
label: ReactNode;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export interface PillTabsProps {
|
|
71
|
+
items: PillTabItem[];
|
|
72
|
+
value: string;
|
|
73
|
+
onValueChange: (value: string) => void;
|
|
74
|
+
className?: string;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export function PillTabs({ items, value, onValueChange, className }: PillTabsProps) {
|
|
78
|
+
return (
|
|
79
|
+
<div className={cn("flex flex-wrap gap-2", className)} role="tablist">
|
|
80
|
+
{items.map((item) => (
|
|
81
|
+
<button
|
|
82
|
+
key={item.value}
|
|
83
|
+
type="button"
|
|
84
|
+
role="tab"
|
|
85
|
+
aria-selected={item.value === value}
|
|
86
|
+
className={cn("tab-pill", item.value === value && "active")}
|
|
87
|
+
onClick={() => onValueChange(item.value)}
|
|
88
|
+
>
|
|
89
|
+
{item.label}
|
|
90
|
+
</button>
|
|
91
|
+
))}
|
|
92
|
+
</div>
|
|
93
|
+
);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/* ── StepList — 工作流步骤条(点击切换) ── */
|
|
97
|
+
export interface StepEntry {
|
|
98
|
+
num: string;
|
|
99
|
+
title: string;
|
|
100
|
+
desc?: string;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
export interface StepListProps {
|
|
104
|
+
steps: StepEntry[];
|
|
105
|
+
active: number;
|
|
106
|
+
onSelect: (index: number) => void;
|
|
107
|
+
className?: string;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
export function StepList({ steps, active, onSelect, className }: StepListProps) {
|
|
111
|
+
return (
|
|
112
|
+
<div className={cn("flex flex-col", className)}>
|
|
113
|
+
{steps.map((s, i) => (
|
|
114
|
+
<div
|
|
115
|
+
key={s.num}
|
|
116
|
+
className={cn("step-item", i === active && "active")}
|
|
117
|
+
onClick={() => onSelect(i)}
|
|
118
|
+
>
|
|
119
|
+
<span className="step-num">{s.num}</span>
|
|
120
|
+
<div>
|
|
121
|
+
<div className="font-semibold">{s.title}</div>
|
|
122
|
+
{s.desc && (
|
|
123
|
+
<div className={cn("text-xs mt-0.5", i === active ? "text-paper/70" : "text-muted")}>
|
|
124
|
+
{s.desc}
|
|
125
|
+
</div>
|
|
126
|
+
)}
|
|
127
|
+
</div>
|
|
128
|
+
</div>
|
|
129
|
+
))}
|
|
130
|
+
</div>
|
|
131
|
+
);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/* ── WorkflowFrame — 视觉卡框架 + 浮动 step badge ── */
|
|
135
|
+
export interface WorkflowFrameProps {
|
|
136
|
+
/** 左上角浮动徽章文案(如 "step 1") */
|
|
137
|
+
badge?: string;
|
|
138
|
+
className?: string;
|
|
139
|
+
children?: ReactNode;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
export function WorkflowFrame({ badge, className, children }: WorkflowFrameProps) {
|
|
143
|
+
return (
|
|
144
|
+
<div className={cn("workflow-frame", className)}>
|
|
145
|
+
{badge && <span className="step-badge">{badge}</span>}
|
|
146
|
+
<div className="workflow-frame-inner">{children}</div>
|
|
147
|
+
</div>
|
|
148
|
+
);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/* ── Pipeline — 管线 breadcrumb(节点 + 箭头,current 高亮) ── */
|
|
152
|
+
export interface PipelineProps {
|
|
153
|
+
items: string[];
|
|
154
|
+
/** 高亮节点下标 */
|
|
155
|
+
current?: number;
|
|
156
|
+
className?: string;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
export function Pipeline({ items, current, className }: PipelineProps) {
|
|
160
|
+
return (
|
|
161
|
+
<div className={cn("pipeline", className)}>
|
|
162
|
+
{items.map((node, i) => (
|
|
163
|
+
<span key={node} className="contents">
|
|
164
|
+
<span className={cn("pipeline-node", i === current && "current")}>{node}</span>
|
|
165
|
+
{i < items.length - 1 && <span className="pipeline-arrow">→</span>}
|
|
166
|
+
</span>
|
|
167
|
+
))}
|
|
168
|
+
</div>
|
|
169
|
+
);
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/* ── WhyCard — 特性卡(dark 变体:一暗多亮) ── */
|
|
173
|
+
export interface WhyCardProps {
|
|
174
|
+
dark?: boolean;
|
|
175
|
+
icon?: ReactNode;
|
|
176
|
+
title: ReactNode;
|
|
177
|
+
className?: string;
|
|
178
|
+
children?: ReactNode;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
export function WhyCard({ dark = false, icon, title, className, children }: WhyCardProps) {
|
|
182
|
+
return (
|
|
183
|
+
<div className={cn("why-card", dark && "dark", className)}>
|
|
184
|
+
{icon && <div className="why-card-icon">{icon}</div>}
|
|
185
|
+
<div className="text-lg font-bold">{title}</div>
|
|
186
|
+
{children}
|
|
187
|
+
</div>
|
|
188
|
+
);
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/* ── Chip — 集成/标签胶囊 ── */
|
|
192
|
+
export interface ChipProps {
|
|
193
|
+
/** 左侧 accent 圆点 */
|
|
194
|
+
dot?: boolean;
|
|
195
|
+
href?: string;
|
|
196
|
+
className?: string;
|
|
197
|
+
children?: ReactNode;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
export function Chip({ dot = true, href, className, children }: ChipProps) {
|
|
201
|
+
const cls = cn("chip", className);
|
|
202
|
+
const inner = (
|
|
203
|
+
<>
|
|
204
|
+
{dot && <span className="chip-dot" />}
|
|
205
|
+
{children}
|
|
206
|
+
</>
|
|
207
|
+
);
|
|
208
|
+
if (href) {
|
|
209
|
+
return (
|
|
210
|
+
<a href={href} className={cls}>
|
|
211
|
+
{inner}
|
|
212
|
+
</a>
|
|
213
|
+
);
|
|
214
|
+
}
|
|
215
|
+
return <span className={cls}>{inner}</span>;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
/* ── CheckList — 绿色勾选清单(boxed 带边框卡) ── */
|
|
219
|
+
export interface CheckListProps {
|
|
220
|
+
items: ReactNode[];
|
|
221
|
+
boxed?: boolean;
|
|
222
|
+
className?: string;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
export function CheckList({ items, boxed = true, className }: CheckListProps) {
|
|
226
|
+
return (
|
|
227
|
+
<div className={cn(boxed && "border border-line bg-paper p-8", className)}>
|
|
228
|
+
{items.map((item, i) => (
|
|
229
|
+
<div key={i} className="check-row">
|
|
230
|
+
<span className="check-mark">✓</span>
|
|
231
|
+
<span className="text-ink-2">{item}</span>
|
|
232
|
+
</div>
|
|
233
|
+
))}
|
|
234
|
+
</div>
|
|
235
|
+
);
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
/* ── FaqAccordion — FAQ 手风琴(各项独立开合) ── */
|
|
239
|
+
export interface FaqEntry {
|
|
240
|
+
q: string;
|
|
241
|
+
a: ReactNode;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
export interface FaqAccordionProps {
|
|
245
|
+
items: FaqEntry[];
|
|
246
|
+
className?: string;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
function FaqItem({ q, a }: FaqEntry) {
|
|
250
|
+
const [open, setOpen] = useState(false);
|
|
251
|
+
return (
|
|
252
|
+
<div className={cn("faq-item", open && "open")}>
|
|
253
|
+
<button className="faq-question" onClick={() => setOpen(!open)}>
|
|
254
|
+
{q}
|
|
255
|
+
<span className="faq-icon">+</span>
|
|
256
|
+
</button>
|
|
257
|
+
<div className="faq-answer">
|
|
258
|
+
<p className="text-sm leading-relaxed text-ink-2">{a}</p>
|
|
259
|
+
</div>
|
|
260
|
+
</div>
|
|
261
|
+
);
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
export function FaqAccordion({ items, className }: FaqAccordionProps) {
|
|
265
|
+
return (
|
|
266
|
+
<div className={className}>
|
|
267
|
+
{items.map((item) => (
|
|
268
|
+
<FaqItem key={item.q} q={item.q} a={item.a} />
|
|
269
|
+
))}
|
|
270
|
+
</div>
|
|
271
|
+
);
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
/* ── StatusBadge — live 脉冲 / building 流光 / planned 虚线 ── */
|
|
275
|
+
export type LandingStatus = "live" | "building" | "planned";
|
|
276
|
+
|
|
277
|
+
export interface StatusBadgeProps {
|
|
278
|
+
status: LandingStatus;
|
|
279
|
+
/** 状态文案(消费端决定措辞,如 已上线 / 建设中) */
|
|
280
|
+
label: string;
|
|
281
|
+
className?: string;
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
export function StatusBadge({ status, label, className }: StatusBadgeProps) {
|
|
285
|
+
if (status === "live") {
|
|
286
|
+
return (
|
|
287
|
+
<span
|
|
288
|
+
className={cn(
|
|
289
|
+
"inline-flex items-center gap-1.5 font-mono text-[10px] tracking-widest uppercase text-success",
|
|
290
|
+
className,
|
|
291
|
+
)}
|
|
292
|
+
>
|
|
293
|
+
<span className="relative flex size-1.5">
|
|
294
|
+
<span className="status-pulse absolute inset-0 rounded-full bg-success" />
|
|
295
|
+
<span className="relative size-1.5 rounded-full bg-success" />
|
|
296
|
+
</span>
|
|
297
|
+
{label}
|
|
298
|
+
</span>
|
|
299
|
+
);
|
|
300
|
+
}
|
|
301
|
+
if (status === "building") {
|
|
302
|
+
return (
|
|
303
|
+
<span
|
|
304
|
+
className={cn(
|
|
305
|
+
"inline-flex items-center gap-1.5 font-mono text-[10px] tracking-widest uppercase text-accent",
|
|
306
|
+
className,
|
|
307
|
+
)}
|
|
308
|
+
>
|
|
309
|
+
<span className="status-progress w-6 h-1 bg-accent/20 text-accent" />
|
|
310
|
+
{label}
|
|
311
|
+
</span>
|
|
312
|
+
);
|
|
313
|
+
}
|
|
314
|
+
return (
|
|
315
|
+
<span
|
|
316
|
+
className={cn(
|
|
317
|
+
"inline-flex items-center gap-1.5 font-mono text-[10px] tracking-widest uppercase text-muted",
|
|
318
|
+
className,
|
|
319
|
+
)}
|
|
320
|
+
>
|
|
321
|
+
<span className="status-dashed size-2.5" />
|
|
322
|
+
{label}
|
|
323
|
+
</span>
|
|
324
|
+
);
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
/* ── CountUp — 进入视口后从 0 计数 ── */
|
|
328
|
+
export interface CountUpProps {
|
|
329
|
+
value: number;
|
|
330
|
+
suffix?: string;
|
|
331
|
+
duration?: number;
|
|
332
|
+
className?: string;
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
export function CountUp({ value, suffix = "", duration = 1200, className }: CountUpProps) {
|
|
336
|
+
const ref = useRef<HTMLSpanElement>(null);
|
|
337
|
+
const [display, setDisplay] = useState(0);
|
|
338
|
+
const started = useRef(false);
|
|
339
|
+
|
|
340
|
+
useEffect(() => {
|
|
341
|
+
const el = ref.current;
|
|
342
|
+
if (!el) return;
|
|
343
|
+
const obs = new IntersectionObserver(
|
|
344
|
+
([entry]) => {
|
|
345
|
+
if (entry.isIntersecting && !started.current) {
|
|
346
|
+
started.current = true;
|
|
347
|
+
const start = performance.now();
|
|
348
|
+
const tick = (now: number) => {
|
|
349
|
+
const t = Math.min((now - start) / duration, 1);
|
|
350
|
+
const eased = 1 - Math.pow(1 - t, 3);
|
|
351
|
+
setDisplay(Math.round(eased * value));
|
|
352
|
+
if (t < 1) requestAnimationFrame(tick);
|
|
353
|
+
};
|
|
354
|
+
requestAnimationFrame(tick);
|
|
355
|
+
}
|
|
356
|
+
},
|
|
357
|
+
{ threshold: 0.5 },
|
|
358
|
+
);
|
|
359
|
+
obs.observe(el);
|
|
360
|
+
return () => obs.disconnect();
|
|
361
|
+
}, [value, duration]);
|
|
362
|
+
|
|
363
|
+
return (
|
|
364
|
+
<span ref={ref} className={cn("count-num", className)}>
|
|
365
|
+
{display}
|
|
366
|
+
{suffix}
|
|
367
|
+
</span>
|
|
368
|
+
);
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
/* ── NavShell — 吸顶胶囊导航:顶部全宽,滚动后收缩为浮动胶囊 ── */
|
|
372
|
+
export interface NavShellProps extends NavbarProps {
|
|
373
|
+
/** 触发浮动形态的滚动距离(px) */
|
|
374
|
+
floatingThreshold?: number;
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
export function NavShell({ floatingThreshold = 16, className, ...navbar }: NavShellProps) {
|
|
378
|
+
const [floating, setFloating] = useState(false);
|
|
379
|
+
|
|
380
|
+
useEffect(() => {
|
|
381
|
+
const onScroll = () => setFloating(window.scrollY > floatingThreshold);
|
|
382
|
+
onScroll();
|
|
383
|
+
window.addEventListener("scroll", onScroll, { passive: true });
|
|
384
|
+
return () => window.removeEventListener("scroll", onScroll);
|
|
385
|
+
}, [floatingThreshold]);
|
|
386
|
+
|
|
387
|
+
return (
|
|
388
|
+
<div className={cn("nav-shell", floating && "floating")}>
|
|
389
|
+
<Navbar className={cn("site-nav static! border-b-0!", className)} {...navbar} />
|
|
390
|
+
</div>
|
|
391
|
+
);
|
|
392
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
export {
|
|
2
|
+
LandingButton,
|
|
3
|
+
PillTabs,
|
|
4
|
+
StepList,
|
|
5
|
+
WorkflowFrame,
|
|
6
|
+
Pipeline,
|
|
7
|
+
WhyCard,
|
|
8
|
+
Chip,
|
|
9
|
+
CheckList,
|
|
10
|
+
FaqAccordion,
|
|
11
|
+
StatusBadge,
|
|
12
|
+
CountUp,
|
|
13
|
+
NavShell,
|
|
14
|
+
} from "./Landing";
|
|
15
|
+
export type {
|
|
16
|
+
LandingButtonProps,
|
|
17
|
+
PillTabItem,
|
|
18
|
+
PillTabsProps,
|
|
19
|
+
StepEntry,
|
|
20
|
+
StepListProps,
|
|
21
|
+
WorkflowFrameProps,
|
|
22
|
+
PipelineProps,
|
|
23
|
+
WhyCardProps,
|
|
24
|
+
ChipProps,
|
|
25
|
+
CheckListProps,
|
|
26
|
+
FaqEntry,
|
|
27
|
+
FaqAccordionProps,
|
|
28
|
+
LandingStatus,
|
|
29
|
+
StatusBadgeProps,
|
|
30
|
+
CountUpProps,
|
|
31
|
+
NavShellProps,
|
|
32
|
+
} from "./Landing";
|
|
@@ -0,0 +1,432 @@
|
|
|
1
|
+
/* @zmzai/theme — Landing kit
|
|
2
|
+
open-design.ai 风格落地页组件样式。
|
|
3
|
+
依赖消费端 @theme token:--color-paper/ink/ink-2/muted/line/accent/accent-ink/
|
|
4
|
+
surface/radius-*/ease-out/ease-out-expo/font-mono/font-serif。 */
|
|
5
|
+
|
|
6
|
+
/* ── Capsule buttons(solid/outline 胶囊按钮) ── */
|
|
7
|
+
.btn-outline {
|
|
8
|
+
display: inline-flex;
|
|
9
|
+
align-items: center;
|
|
10
|
+
gap: 0.5rem;
|
|
11
|
+
padding: 0.75rem 1.5rem;
|
|
12
|
+
border: 1.5px solid var(--color-ink);
|
|
13
|
+
border-radius: var(--radius-pill);
|
|
14
|
+
font-size: 0.9375rem;
|
|
15
|
+
font-weight: 500;
|
|
16
|
+
color: var(--color-ink);
|
|
17
|
+
background: transparent;
|
|
18
|
+
cursor: pointer;
|
|
19
|
+
transition: all 200ms var(--ease-out);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
.btn-outline:hover {
|
|
23
|
+
background: var(--color-ink);
|
|
24
|
+
color: var(--color-paper);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
.btn-solid {
|
|
28
|
+
display: inline-flex;
|
|
29
|
+
align-items: center;
|
|
30
|
+
gap: 0.5rem;
|
|
31
|
+
padding: 0.75rem 1.5rem;
|
|
32
|
+
border: 1.5px solid var(--color-ink);
|
|
33
|
+
border-radius: var(--radius-pill);
|
|
34
|
+
font-size: 0.9375rem;
|
|
35
|
+
font-weight: 500;
|
|
36
|
+
color: var(--color-paper);
|
|
37
|
+
background: var(--color-ink);
|
|
38
|
+
cursor: pointer;
|
|
39
|
+
transition: all 200ms var(--ease-out);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
.btn-solid:hover {
|
|
43
|
+
background: oklch(0.30 0.010 30);
|
|
44
|
+
border-color: oklch(0.30 0.010 30);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/* Arrow slide on hover(按钮/链接内的 → 箭头位移) */
|
|
48
|
+
.arrow-slide {
|
|
49
|
+
display: inline-block;
|
|
50
|
+
transition: transform var(--animate-dur-base, 250ms) var(--ease-out-expo);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
.group:hover .arrow-slide,
|
|
54
|
+
.landing-btn:hover .arrow-slide {
|
|
55
|
+
transform: translateX(4px);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/* ── Tab pills(胶囊标签栏) ── */
|
|
59
|
+
.tab-pill {
|
|
60
|
+
display: inline-flex;
|
|
61
|
+
align-items: center;
|
|
62
|
+
gap: 0.5rem;
|
|
63
|
+
padding: 0.5rem 1rem;
|
|
64
|
+
border-radius: var(--radius-pill);
|
|
65
|
+
font-size: 0.8125rem;
|
|
66
|
+
font-weight: 500;
|
|
67
|
+
border: 1px solid var(--color-line);
|
|
68
|
+
background: var(--color-paper);
|
|
69
|
+
color: var(--color-muted);
|
|
70
|
+
cursor: pointer;
|
|
71
|
+
transition: all 200ms var(--ease-out);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
.tab-pill:hover {
|
|
75
|
+
border-color: var(--color-muted);
|
|
76
|
+
color: var(--color-ink);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
.tab-pill.active {
|
|
80
|
+
background: var(--color-ink);
|
|
81
|
+
color: var(--color-paper);
|
|
82
|
+
border-color: var(--color-ink);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/* ── Step sidebar(工作流步骤条) ── */
|
|
86
|
+
.step-item {
|
|
87
|
+
display: flex;
|
|
88
|
+
align-items: center;
|
|
89
|
+
gap: 0.75rem;
|
|
90
|
+
padding: 0.75rem 1rem;
|
|
91
|
+
border-radius: var(--radius-pill);
|
|
92
|
+
font-size: 0.875rem;
|
|
93
|
+
color: var(--color-muted);
|
|
94
|
+
cursor: pointer;
|
|
95
|
+
transition: all 200ms var(--ease-out);
|
|
96
|
+
margin-bottom: 0.25rem;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
.step-item:hover {
|
|
100
|
+
color: var(--color-ink);
|
|
101
|
+
background: var(--color-surface);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
.step-item.active {
|
|
105
|
+
background: var(--color-ink);
|
|
106
|
+
color: var(--color-paper);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
.step-num {
|
|
110
|
+
font-family: var(--font-mono);
|
|
111
|
+
font-size: 0.75rem;
|
|
112
|
+
font-weight: 600;
|
|
113
|
+
width: 1.5rem;
|
|
114
|
+
text-align: center;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/* ── Product card(产品卡片,hover 上浮) ── */
|
|
118
|
+
.product-card {
|
|
119
|
+
border: 1px solid var(--color-line);
|
|
120
|
+
background: var(--color-paper);
|
|
121
|
+
transition: transform 300ms var(--ease-out-expo),
|
|
122
|
+
box-shadow 300ms var(--ease-out-expo),
|
|
123
|
+
border-color 300ms var(--ease-out);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
.product-card:hover {
|
|
127
|
+
transform: translateY(-4px);
|
|
128
|
+
box-shadow: 0 16px 48px -12px oklch(0.18 0.011 40 / 0.12);
|
|
129
|
+
border-color: var(--color-muted);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/* ── Stat value(大字统计数字) ── */
|
|
133
|
+
.stat-value {
|
|
134
|
+
font-family: var(--font-serif);
|
|
135
|
+
font-size: 2.5rem;
|
|
136
|
+
font-weight: 700;
|
|
137
|
+
line-height: 1;
|
|
138
|
+
letter-spacing: -0.03em;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/* Count-up number */
|
|
142
|
+
.count-num {
|
|
143
|
+
font-variant-numeric: tabular-nums;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/* ── FAQ accordion ── */
|
|
147
|
+
.faq-item {
|
|
148
|
+
border-bottom: 1px solid var(--color-line);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
.faq-question {
|
|
152
|
+
display: flex;
|
|
153
|
+
align-items: center;
|
|
154
|
+
justify-content: space-between;
|
|
155
|
+
width: 100%;
|
|
156
|
+
padding: 1.25rem 0;
|
|
157
|
+
font-size: 1rem;
|
|
158
|
+
font-weight: 600;
|
|
159
|
+
text-align: left;
|
|
160
|
+
cursor: pointer;
|
|
161
|
+
background: none;
|
|
162
|
+
border: none;
|
|
163
|
+
color: var(--color-ink);
|
|
164
|
+
transition: color 200ms var(--ease-out);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
.faq-question:hover {
|
|
168
|
+
color: var(--color-accent);
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
.faq-icon {
|
|
172
|
+
font-size: 1.25rem;
|
|
173
|
+
transition: transform 300ms var(--ease-out);
|
|
174
|
+
color: var(--color-muted);
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
.faq-item.open .faq-icon {
|
|
178
|
+
transform: rotate(45deg);
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
.faq-answer {
|
|
182
|
+
max-height: 0;
|
|
183
|
+
overflow: hidden;
|
|
184
|
+
transition: max-height 400ms var(--ease-out-expo),
|
|
185
|
+
padding 400ms var(--ease-out-expo);
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
.faq-item.open .faq-answer {
|
|
189
|
+
max-height: 200px;
|
|
190
|
+
padding-bottom: 1.25rem;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
/* ── Workflow screenshot frame ── */
|
|
194
|
+
.workflow-frame {
|
|
195
|
+
border: 1px solid var(--color-line);
|
|
196
|
+
border-radius: var(--radius-xl);
|
|
197
|
+
overflow: hidden;
|
|
198
|
+
box-shadow: 0 24px 64px -16px oklch(0.15 0.010 30 / 0.12);
|
|
199
|
+
position: relative;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
.workflow-frame-inner {
|
|
203
|
+
background: var(--color-surface);
|
|
204
|
+
padding: 1.5rem;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
/* Step badge — floats over workflow frame */
|
|
208
|
+
.step-badge {
|
|
209
|
+
position: absolute;
|
|
210
|
+
top: 12px;
|
|
211
|
+
left: 12px;
|
|
212
|
+
z-index: 2;
|
|
213
|
+
padding: 0.25rem 0.75rem;
|
|
214
|
+
border-radius: var(--radius-pill);
|
|
215
|
+
background: var(--color-accent);
|
|
216
|
+
color: var(--color-accent-ink);
|
|
217
|
+
font-family: var(--font-mono);
|
|
218
|
+
font-size: 0.6875rem;
|
|
219
|
+
font-weight: 600;
|
|
220
|
+
letter-spacing: 0.08em;
|
|
221
|
+
text-transform: uppercase;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
/* ── Pipeline breadcrumb(想法 → relay → … → 交付) ── */
|
|
225
|
+
.pipeline {
|
|
226
|
+
display: flex;
|
|
227
|
+
flex-wrap: wrap;
|
|
228
|
+
align-items: center;
|
|
229
|
+
gap: 0.5rem;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
.pipeline-node {
|
|
233
|
+
padding: 0.375rem 0.875rem;
|
|
234
|
+
border-radius: var(--radius-pill);
|
|
235
|
+
border: 1px solid var(--color-line);
|
|
236
|
+
font-family: var(--font-mono);
|
|
237
|
+
font-size: 0.75rem;
|
|
238
|
+
color: var(--color-muted);
|
|
239
|
+
background: var(--color-paper);
|
|
240
|
+
transition: all 200ms var(--ease-out);
|
|
241
|
+
white-space: nowrap;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
.pipeline-node.current {
|
|
245
|
+
border-color: var(--color-accent);
|
|
246
|
+
background: var(--color-accent);
|
|
247
|
+
color: var(--color-accent-ink);
|
|
248
|
+
font-weight: 600;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
.pipeline-arrow {
|
|
252
|
+
color: var(--color-line);
|
|
253
|
+
font-size: 0.75rem;
|
|
254
|
+
user-select: none;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
/* ── Narrative paragraph(行高 2 的叙事段落,b 关键词加深) ── */
|
|
258
|
+
.narrative {
|
|
259
|
+
font-size: 1.0625rem;
|
|
260
|
+
line-height: 2;
|
|
261
|
+
color: var(--color-ink-2);
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
.narrative b {
|
|
265
|
+
color: var(--color-ink);
|
|
266
|
+
font-weight: 600;
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
/* ── Why feature cards(dark 变体:一暗多亮) ── */
|
|
270
|
+
.why-card {
|
|
271
|
+
border: 1px solid var(--color-line);
|
|
272
|
+
background: var(--color-surface);
|
|
273
|
+
padding: 1.75rem;
|
|
274
|
+
display: flex;
|
|
275
|
+
flex-direction: column;
|
|
276
|
+
gap: 0.75rem;
|
|
277
|
+
transition: transform 300ms var(--ease-out-expo),
|
|
278
|
+
border-color 300ms var(--ease-out);
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
.why-card:hover {
|
|
282
|
+
transform: translateY(-3px);
|
|
283
|
+
border-color: var(--color-muted);
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
.why-card.dark {
|
|
287
|
+
background: var(--color-ink);
|
|
288
|
+
color: var(--color-paper);
|
|
289
|
+
border-color: var(--color-ink);
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
.why-card-icon {
|
|
293
|
+
width: 40px;
|
|
294
|
+
height: 40px;
|
|
295
|
+
border-radius: var(--radius-lg);
|
|
296
|
+
border: 1px solid var(--color-line);
|
|
297
|
+
display: flex;
|
|
298
|
+
align-items: center;
|
|
299
|
+
justify-content: center;
|
|
300
|
+
color: var(--color-accent);
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
.why-card.dark .why-card-icon {
|
|
304
|
+
border-color: oklch(1 0 0 / 0.2);
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
/* ── Integration chips ── */
|
|
308
|
+
.chip {
|
|
309
|
+
display: inline-flex;
|
|
310
|
+
align-items: center;
|
|
311
|
+
gap: 0.5rem;
|
|
312
|
+
padding: 0.5rem 1rem;
|
|
313
|
+
border: 1px solid var(--color-line);
|
|
314
|
+
border-radius: var(--radius-pill);
|
|
315
|
+
font-family: var(--font-mono);
|
|
316
|
+
font-size: 0.8125rem;
|
|
317
|
+
color: var(--color-ink-2);
|
|
318
|
+
background: var(--color-paper);
|
|
319
|
+
transition: all 200ms var(--ease-out);
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
.chip:hover {
|
|
323
|
+
border-color: var(--color-accent);
|
|
324
|
+
color: var(--color-ink);
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
.chip-dot {
|
|
328
|
+
width: 6px;
|
|
329
|
+
height: 6px;
|
|
330
|
+
border-radius: 50%;
|
|
331
|
+
background: var(--color-accent);
|
|
332
|
+
flex-shrink: 0;
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
/* ── Checklist — 绿色勾选行 ── */
|
|
336
|
+
.check-row {
|
|
337
|
+
display: flex;
|
|
338
|
+
align-items: flex-start;
|
|
339
|
+
gap: 0.75rem;
|
|
340
|
+
padding: 0.875rem 0;
|
|
341
|
+
border-bottom: 1px solid var(--color-line);
|
|
342
|
+
font-size: 0.9375rem;
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
.check-row:last-child {
|
|
346
|
+
border-bottom: none;
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
.check-mark {
|
|
350
|
+
flex-shrink: 0;
|
|
351
|
+
width: 20px;
|
|
352
|
+
height: 20px;
|
|
353
|
+
border-radius: 50%;
|
|
354
|
+
background: var(--color-accent);
|
|
355
|
+
color: var(--color-accent-ink);
|
|
356
|
+
display: flex;
|
|
357
|
+
align-items: center;
|
|
358
|
+
justify-content: center;
|
|
359
|
+
font-size: 0.6875rem;
|
|
360
|
+
font-weight: 700;
|
|
361
|
+
margin-top: 1px;
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
/* ── Status badge(live 脉冲 / building 流光 / planned 虚线) ── */
|
|
365
|
+
.status-pulse {
|
|
366
|
+
position: relative;
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
.status-pulse::after {
|
|
370
|
+
content: "";
|
|
371
|
+
position: absolute;
|
|
372
|
+
inset: -3px;
|
|
373
|
+
border-radius: 50%;
|
|
374
|
+
background: currentColor;
|
|
375
|
+
opacity: 0;
|
|
376
|
+
animation: landingStatusPulse 2s ease-out infinite;
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
@keyframes landingStatusPulse {
|
|
380
|
+
0% { opacity: 0.5; transform: scale(0.8); }
|
|
381
|
+
100% { opacity: 0; transform: scale(2.2); }
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
.status-progress {
|
|
385
|
+
overflow: hidden;
|
|
386
|
+
border-radius: 9999px;
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
.status-progress::after {
|
|
390
|
+
content: "";
|
|
391
|
+
display: block;
|
|
392
|
+
height: 100%;
|
|
393
|
+
width: 40%;
|
|
394
|
+
background: currentColor;
|
|
395
|
+
opacity: 0.5;
|
|
396
|
+
animation: landingStatusProgress 1.8s ease-in-out infinite;
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
@keyframes landingStatusProgress {
|
|
400
|
+
0% { transform: translateX(-120%); }
|
|
401
|
+
100% { transform: translateX(380%); }
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
.status-dashed {
|
|
405
|
+
border: 1.5px dashed currentColor;
|
|
406
|
+
border-radius: 50%;
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
/* ── Floating pill nav — 顶部全宽吸顶,滚动后收缩为浮动胶囊 ── */
|
|
410
|
+
.nav-shell {
|
|
411
|
+
position: sticky;
|
|
412
|
+
top: 0;
|
|
413
|
+
z-index: 40;
|
|
414
|
+
transition: padding 300ms var(--ease-out);
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
.nav-shell .site-nav {
|
|
418
|
+
transition: border-radius 300ms var(--ease-out),
|
|
419
|
+
box-shadow 300ms var(--ease-out),
|
|
420
|
+
border-color 300ms var(--ease-out);
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
.nav-shell.floating {
|
|
424
|
+
padding: 10px 12px;
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
.nav-shell.floating .site-nav {
|
|
428
|
+
border-radius: var(--radius-pill);
|
|
429
|
+
border-color: transparent;
|
|
430
|
+
box-shadow: 0 8px 32px -8px oklch(0.15 0.010 30 / 0.18),
|
|
431
|
+
0 1px 3px oklch(0.15 0.010 30 / 0.06);
|
|
432
|
+
}
|