@alpic-ai/ui 0.0.0-dev.9f2fa8e → 0.0.0-dev.9f6b80d
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/dist/components/avatar.d.mts +1 -1
- package/dist/components/button.d.mts +3 -1
- package/dist/components/button.mjs +17 -3
- package/dist/components/form.d.mts +36 -1
- package/dist/components/form.mjs +111 -1
- package/dist/components/page-loader.d.mts +11 -0
- package/dist/components/page-loader.mjs +122 -0
- package/dist/components/shimmer-text.d.mts +12 -0
- package/dist/components/shimmer-text.mjs +22 -0
- package/dist/components/sidebar.mjs +4 -4
- package/dist/components/spinner.d.mts +1 -1
- package/dist/components/table.d.mts +10 -1
- package/dist/components/table.mjs +4 -4
- package/dist/components/tabs.mjs +4 -4
- package/dist/components/task-progress.d.mts +27 -0
- package/dist/components/task-progress.mjs +66 -0
- package/dist/components/wizard.d.mts +34 -0
- package/dist/components/wizard.mjs +46 -0
- package/package.json +12 -12
- package/src/components/button.tsx +10 -0
- package/src/components/combobox.tsx +18 -6
- package/src/components/form.tsx +161 -0
- package/src/components/page-loader.tsx +59 -0
- package/src/components/shimmer-text.tsx +23 -0
- package/src/components/sidebar.tsx +4 -4
- package/src/components/table.tsx +17 -4
- package/src/components/tabs.tsx +4 -4
- package/src/components/task-progress.tsx +107 -0
- package/src/components/wizard.tsx +69 -0
- package/src/hooks/use-copy-to-clipboard.ts +6 -2
- package/src/stories/button.stories.tsx +23 -1
- package/src/stories/form.stories.tsx +64 -2
- package/src/stories/table.stories.tsx +2 -2
- package/src/stories/tabs.stories.tsx +4 -2
- package/src/stories/task-progress.stories.tsx +81 -0
- package/src/stories/wizard.stories.tsx +64 -0
- package/src/styles/tokens.css +193 -4
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
/*
|
|
4
|
+
* Wizard family — primitives for multi-step flows.
|
|
5
|
+
*
|
|
6
|
+
* - WizardSteps — vertical step rail (controlled by activeIdx + onSelect)
|
|
7
|
+
* - WizardProgress — step counter + progress bar
|
|
8
|
+
*
|
|
9
|
+
* Consumers compose these inside whatever container they need (sticky aside, modal, etc.).
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import type * as React from "react";
|
|
13
|
+
|
|
14
|
+
import { cn } from "../lib/cn";
|
|
15
|
+
import { TabsNav, TabsNavList, TabsNavTrigger } from "./tabs";
|
|
16
|
+
|
|
17
|
+
interface WizardStep {
|
|
18
|
+
id: string;
|
|
19
|
+
label: string;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
interface WizardStepsProps {
|
|
23
|
+
steps: readonly WizardStep[];
|
|
24
|
+
activeIdx: number;
|
|
25
|
+
onSelect: (idx: number) => void;
|
|
26
|
+
ariaLabel?: string;
|
|
27
|
+
className?: string;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function WizardSteps({ steps, activeIdx, onSelect, ariaLabel = "Wizard steps", className }: WizardStepsProps) {
|
|
31
|
+
return (
|
|
32
|
+
<TabsNav orientation="vertical" aria-label={ariaLabel} className={className}>
|
|
33
|
+
<TabsNavList>
|
|
34
|
+
{steps.map((step, idx) => (
|
|
35
|
+
<TabsNavTrigger key={step.id} active={idx === activeIdx} asChild>
|
|
36
|
+
<button type="button" onClick={() => onSelect(idx)} className="w-full justify-start text-left">
|
|
37
|
+
{step.label}
|
|
38
|
+
</button>
|
|
39
|
+
</TabsNavTrigger>
|
|
40
|
+
))}
|
|
41
|
+
</TabsNavList>
|
|
42
|
+
</TabsNav>
|
|
43
|
+
);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
interface WizardProgressProps extends React.ComponentProps<"div"> {
|
|
47
|
+
current: number;
|
|
48
|
+
total: number;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function WizardProgress({ current, total, className, ...props }: WizardProgressProps) {
|
|
52
|
+
const percent = total > 0 ? Math.round((current / total) * 100) : 0;
|
|
53
|
+
return (
|
|
54
|
+
<div className={cn("flex flex-col gap-1.5 px-2", className)} {...props}>
|
|
55
|
+
<div className="text-muted-foreground flex items-center justify-between text-xs">
|
|
56
|
+
<span>
|
|
57
|
+
Step {current}/{total}
|
|
58
|
+
</span>
|
|
59
|
+
<span>{percent}%</span>
|
|
60
|
+
</div>
|
|
61
|
+
<div className="bg-muted h-1.5 overflow-hidden rounded-full">
|
|
62
|
+
<div className="bg-primary h-full transition-all" style={{ width: `${percent}%` }} />
|
|
63
|
+
</div>
|
|
64
|
+
</div>
|
|
65
|
+
);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export type { WizardStep };
|
|
69
|
+
export { WizardProgress, WizardSteps };
|
|
@@ -8,7 +8,9 @@ export function useCopyToClipboard({ resetDelay = 2000 }: { resetDelay?: number
|
|
|
8
8
|
|
|
9
9
|
useEffect(() => {
|
|
10
10
|
return () => {
|
|
11
|
-
if (timeoutRef.current)
|
|
11
|
+
if (timeoutRef.current) {
|
|
12
|
+
clearTimeout(timeoutRef.current);
|
|
13
|
+
}
|
|
12
14
|
};
|
|
13
15
|
}, []);
|
|
14
16
|
|
|
@@ -16,7 +18,9 @@ export function useCopyToClipboard({ resetDelay = 2000 }: { resetDelay?: number
|
|
|
16
18
|
(text: string) => {
|
|
17
19
|
navigator.clipboard.writeText(text).then(() => {
|
|
18
20
|
setIsCopied(true);
|
|
19
|
-
if (timeoutRef.current)
|
|
21
|
+
if (timeoutRef.current) {
|
|
22
|
+
clearTimeout(timeoutRef.current);
|
|
23
|
+
}
|
|
20
24
|
timeoutRef.current = setTimeout(() => setIsCopied(false), resetDelay);
|
|
21
25
|
});
|
|
22
26
|
},
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { Plus } from "lucide-react";
|
|
1
|
+
import { ArrowRight, Plus, Sparkles } from "lucide-react";
|
|
2
2
|
|
|
3
3
|
import { Button } from "../components/button";
|
|
4
4
|
|
|
@@ -323,6 +323,28 @@ export const AllVariants = () => {
|
|
|
323
323
|
</Button>
|
|
324
324
|
</div>
|
|
325
325
|
|
|
326
|
+
{/* ── CTA (animated gradient ring) ────────────────────────────────── */}
|
|
327
|
+
<span className={SECTION_HEADER}>CTA — animated</span>
|
|
328
|
+
<p className="type-text-xs text-muted-foreground -mt-2 max-w-md">
|
|
329
|
+
Hover to rotate the conic gradient around the border and ignite the soft halo.
|
|
330
|
+
</p>
|
|
331
|
+
|
|
332
|
+
<div className="flex items-center gap-6">
|
|
333
|
+
<Button variant="cta" iconTrailing={<ArrowRight />}>
|
|
334
|
+
Get started
|
|
335
|
+
</Button>
|
|
336
|
+
<Button variant="cta" icon={<Sparkles />} iconTrailing={<ArrowRight />}>
|
|
337
|
+
Launch server
|
|
338
|
+
</Button>
|
|
339
|
+
<Button variant="cta">Deploy now</Button>
|
|
340
|
+
<Button variant="cta" iconTrailing={<ArrowRight />} disabled>
|
|
341
|
+
Disabled
|
|
342
|
+
</Button>
|
|
343
|
+
<Button variant="cta" iconTrailing={<ArrowRight />} loading>
|
|
344
|
+
Deploying
|
|
345
|
+
</Button>
|
|
346
|
+
</div>
|
|
347
|
+
|
|
326
348
|
{/* ── asChild ─────────────────────────────────────────────────────── */}
|
|
327
349
|
<span className={SECTION_HEADER}>asChild</span>
|
|
328
350
|
|
|
@@ -1,6 +1,17 @@
|
|
|
1
1
|
import type { Story } from "@ladle/react";
|
|
2
2
|
import { useForm } from "react-hook-form";
|
|
3
|
-
|
|
3
|
+
|
|
4
|
+
import {
|
|
5
|
+
CheckboxField,
|
|
6
|
+
ChecklistField,
|
|
7
|
+
Form,
|
|
8
|
+
FormFields,
|
|
9
|
+
FormHeader,
|
|
10
|
+
InputField,
|
|
11
|
+
RadioField,
|
|
12
|
+
SelectField,
|
|
13
|
+
TextareaField,
|
|
14
|
+
} from "../components/form";
|
|
4
15
|
|
|
5
16
|
/* ── Types ───────────────────────────────────────────────────────────────── */
|
|
6
17
|
|
|
@@ -8,6 +19,9 @@ interface CreateProjectForm {
|
|
|
8
19
|
email: string;
|
|
9
20
|
role: string;
|
|
10
21
|
description: string;
|
|
22
|
+
visibility: string;
|
|
23
|
+
capabilities: string[];
|
|
24
|
+
acceptTerms: boolean;
|
|
11
25
|
}
|
|
12
26
|
|
|
13
27
|
const roles = [
|
|
@@ -16,11 +30,31 @@ const roles = [
|
|
|
16
30
|
{ value: "viewer", label: "Viewer" },
|
|
17
31
|
];
|
|
18
32
|
|
|
33
|
+
const visibilities = [
|
|
34
|
+
{ value: "public", label: "Public" },
|
|
35
|
+
{ value: "private", label: "Private" },
|
|
36
|
+
{ value: "internal", label: "Internal" },
|
|
37
|
+
];
|
|
38
|
+
|
|
39
|
+
const capabilities = [
|
|
40
|
+
{ value: "read", label: "Read" },
|
|
41
|
+
{ value: "write", label: "Write" },
|
|
42
|
+
{ value: "deploy", label: "Deploy" },
|
|
43
|
+
{ value: "admin", label: "Admin" },
|
|
44
|
+
];
|
|
45
|
+
|
|
19
46
|
/* ── Composed Form ───────────────────────────────────────────────────────── */
|
|
20
47
|
|
|
21
48
|
function ComposedForm() {
|
|
22
49
|
const form = useForm<CreateProjectForm>({
|
|
23
|
-
defaultValues: {
|
|
50
|
+
defaultValues: {
|
|
51
|
+
email: "",
|
|
52
|
+
role: "",
|
|
53
|
+
description: "",
|
|
54
|
+
visibility: "",
|
|
55
|
+
capabilities: [],
|
|
56
|
+
acceptTerms: false,
|
|
57
|
+
},
|
|
24
58
|
});
|
|
25
59
|
|
|
26
60
|
const onSubmit = (data: CreateProjectForm) => {
|
|
@@ -64,6 +98,34 @@ function ComposedForm() {
|
|
|
64
98
|
placeholder="A short description of your project..."
|
|
65
99
|
required
|
|
66
100
|
/>
|
|
101
|
+
|
|
102
|
+
<RadioField
|
|
103
|
+
control={form.control}
|
|
104
|
+
name="visibility"
|
|
105
|
+
rules={{ required: "Pick a visibility." }}
|
|
106
|
+
label="Visibility"
|
|
107
|
+
description="Who can see and access this project."
|
|
108
|
+
options={visibilities}
|
|
109
|
+
required
|
|
110
|
+
/>
|
|
111
|
+
|
|
112
|
+
<ChecklistField
|
|
113
|
+
control={form.control}
|
|
114
|
+
name="capabilities"
|
|
115
|
+
rules={{ validate: (value) => (value?.length ? true : "Pick at least one capability.") }}
|
|
116
|
+
label="Capabilities"
|
|
117
|
+
description="Select all that apply."
|
|
118
|
+
options={capabilities}
|
|
119
|
+
required
|
|
120
|
+
/>
|
|
121
|
+
|
|
122
|
+
<CheckboxField
|
|
123
|
+
control={form.control}
|
|
124
|
+
name="acceptTerms"
|
|
125
|
+
rules={{ validate: (value) => (value ? true : "You must accept the terms.") }}
|
|
126
|
+
label="I accept the terms and conditions"
|
|
127
|
+
description="Required to create the project."
|
|
128
|
+
/>
|
|
67
129
|
</FormFields>
|
|
68
130
|
|
|
69
131
|
<button
|
|
@@ -190,8 +190,8 @@ export const AllVariants: Story = () => (
|
|
|
190
190
|
</TableRow>
|
|
191
191
|
</TableHeader>
|
|
192
192
|
<TableBody>
|
|
193
|
-
{USERS.slice(0, 3).map((user,
|
|
194
|
-
<TableRow key={user.id} data-state={
|
|
193
|
+
{USERS.slice(0, 3).map((user, index) => (
|
|
194
|
+
<TableRow key={user.id} data-state={index === 1 ? "selected" : undefined}>
|
|
195
195
|
<TableCell className="font-medium text-foreground">{user.name}</TableCell>
|
|
196
196
|
<TableCell className="text-subtle-foreground">{user.role}</TableCell>
|
|
197
197
|
<TableCell className="text-subtle-foreground">{user.email}</TableCell>
|
|
@@ -18,9 +18,11 @@ const navTabs = [
|
|
|
18
18
|
|
|
19
19
|
const sideNavTabs = [
|
|
20
20
|
{ id: "general", label: "General" },
|
|
21
|
+
{ id: "build-settings", label: "Build settings" },
|
|
22
|
+
{ id: "environment-variables", label: "Environment variables" },
|
|
23
|
+
{ id: "domains", label: "Domains" },
|
|
21
24
|
{ id: "authentication", label: "Authentication" },
|
|
22
|
-
{ id: "
|
|
23
|
-
{ id: "billing", label: "Billing" },
|
|
25
|
+
{ id: "marketplace", label: "Marketplace" },
|
|
24
26
|
];
|
|
25
27
|
|
|
26
28
|
function useHashRoute(fallback: string) {
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import type { Story } from "@ladle/react";
|
|
2
|
+
import { useEffect, useState } from "react";
|
|
3
|
+
|
|
4
|
+
import { TaskProgress, type TaskProgressStep } from "../components/task-progress";
|
|
5
|
+
|
|
6
|
+
const SECTION_HEADER = "type-text-xs font-medium text-subtle-foreground uppercase tracking-wide pt-4";
|
|
7
|
+
|
|
8
|
+
const beaconSteps: TaskProgressStep[] = [
|
|
9
|
+
{ id: "init", label: "Initialize MCP connection", status: "done" },
|
|
10
|
+
{ id: "fetch", label: "Fetch tools and resources", status: "done" },
|
|
11
|
+
{ id: "compat", label: "Check ChatGPT & Claude.ai compatibility", status: "done" },
|
|
12
|
+
];
|
|
13
|
+
|
|
14
|
+
const partialSteps: TaskProgressStep[] = [
|
|
15
|
+
{ id: "a", label: "Reading project metadata", status: "done" },
|
|
16
|
+
{ id: "b", label: "Fetching MCP server manifest", status: "running" },
|
|
17
|
+
{ id: "c", label: "Preparing your submission", status: "pending" },
|
|
18
|
+
];
|
|
19
|
+
|
|
20
|
+
const allDoneSteps: TaskProgressStep[] = [
|
|
21
|
+
{ id: "a", label: "Reading project metadata", status: "done" },
|
|
22
|
+
{ id: "b", label: "Fetching MCP server manifest", status: "done" },
|
|
23
|
+
{ id: "c", label: "Preparing your submission", status: "done" },
|
|
24
|
+
];
|
|
25
|
+
|
|
26
|
+
const STEP_INTERVAL_MS = 1200;
|
|
27
|
+
|
|
28
|
+
function AnimatedExample() {
|
|
29
|
+
const [activeIdx, setActiveIdx] = useState(0);
|
|
30
|
+
|
|
31
|
+
const labels = ["Reading project metadata", "Fetching MCP server manifest", "Preparing your submission"];
|
|
32
|
+
|
|
33
|
+
useEffect(() => {
|
|
34
|
+
if (activeIdx >= labels.length) {
|
|
35
|
+
return;
|
|
36
|
+
}
|
|
37
|
+
const timeoutId = setTimeout(() => setActiveIdx((prev) => prev + 1), STEP_INTERVAL_MS);
|
|
38
|
+
return () => clearTimeout(timeoutId);
|
|
39
|
+
}, [activeIdx, labels.length]);
|
|
40
|
+
|
|
41
|
+
const steps: TaskProgressStep[] = labels.map((label, idx) => ({
|
|
42
|
+
id: String(idx),
|
|
43
|
+
label,
|
|
44
|
+
status: idx < activeIdx ? "done" : idx === activeIdx ? "running" : "pending",
|
|
45
|
+
}));
|
|
46
|
+
const allDone = activeIdx >= labels.length;
|
|
47
|
+
|
|
48
|
+
return <TaskProgress steps={steps} trailingLabel={allDone ? "Almost there…" : undefined} />;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export const AllVariants: Story = () => (
|
|
52
|
+
<div className="flex flex-col gap-8 p-8 max-w-[640px]">
|
|
53
|
+
<div>
|
|
54
|
+
<p className={SECTION_HEADER}>In-progress (running step in the middle)</p>
|
|
55
|
+
<div className="mt-4">
|
|
56
|
+
<TaskProgress steps={partialSteps} />
|
|
57
|
+
</div>
|
|
58
|
+
</div>
|
|
59
|
+
|
|
60
|
+
<div>
|
|
61
|
+
<p className={SECTION_HEADER}>All done</p>
|
|
62
|
+
<div className="mt-4">
|
|
63
|
+
<TaskProgress steps={allDoneSteps} />
|
|
64
|
+
</div>
|
|
65
|
+
</div>
|
|
66
|
+
|
|
67
|
+
<div>
|
|
68
|
+
<p className={SECTION_HEADER}>All done + trailing "Running checks…" row</p>
|
|
69
|
+
<div className="mt-4">
|
|
70
|
+
<TaskProgress steps={beaconSteps} trailingLabel="Running checks…" />
|
|
71
|
+
</div>
|
|
72
|
+
</div>
|
|
73
|
+
|
|
74
|
+
<div>
|
|
75
|
+
<p className={SECTION_HEADER}>Animated (timer-driven, mirrors the submission-prefill flow)</p>
|
|
76
|
+
<div className="mt-4">
|
|
77
|
+
<AnimatedExample />
|
|
78
|
+
</div>
|
|
79
|
+
</div>
|
|
80
|
+
</div>
|
|
81
|
+
);
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import type { Story } from "@ladle/react";
|
|
2
|
+
import { useState } from "react";
|
|
3
|
+
|
|
4
|
+
import { WizardProgress, type WizardStep, WizardSteps } from "../components/wizard";
|
|
5
|
+
|
|
6
|
+
const SECTION_HEADER = "type-text-xs font-medium text-muted-foreground uppercase tracking-wide pt-4";
|
|
7
|
+
|
|
8
|
+
const steps: WizardStep[] = [
|
|
9
|
+
{ id: "overview", label: "Overview" },
|
|
10
|
+
{ id: "branding", label: "Branding & metadata" },
|
|
11
|
+
{ id: "auth", label: "Authentication" },
|
|
12
|
+
{ id: "tools", label: "Tools & test cases" },
|
|
13
|
+
{ id: "review", label: "Review & submit" },
|
|
14
|
+
];
|
|
15
|
+
|
|
16
|
+
export const AllVariants: Story = () => {
|
|
17
|
+
const [activeIdx, setActiveIdx] = useState(1);
|
|
18
|
+
|
|
19
|
+
return (
|
|
20
|
+
<div className="flex flex-col gap-10 p-8">
|
|
21
|
+
{/* Full rail (steps + progress) */}
|
|
22
|
+
<div>
|
|
23
|
+
<p className={SECTION_HEADER}>Full rail — steps + progress</p>
|
|
24
|
+
<div className="mt-4 flex gap-6">
|
|
25
|
+
<aside className="basis-56 shrink-0 flex flex-col gap-4 self-start">
|
|
26
|
+
<WizardSteps steps={steps} activeIdx={activeIdx} onSelect={setActiveIdx} ariaLabel="Submission steps" />
|
|
27
|
+
<WizardProgress current={activeIdx + 1} total={steps.length} />
|
|
28
|
+
</aside>
|
|
29
|
+
<div className="flex-1 rounded-md border p-4">
|
|
30
|
+
<p className="type-text-sm text-muted-foreground">Content for step "{steps[activeIdx]?.label}"</p>
|
|
31
|
+
</div>
|
|
32
|
+
</div>
|
|
33
|
+
</div>
|
|
34
|
+
|
|
35
|
+
{/* Steps only */}
|
|
36
|
+
<div>
|
|
37
|
+
<p className={SECTION_HEADER}>Steps only</p>
|
|
38
|
+
<div className="mt-4 max-w-56">
|
|
39
|
+
<WizardSteps steps={steps} activeIdx={activeIdx} onSelect={setActiveIdx} />
|
|
40
|
+
</div>
|
|
41
|
+
</div>
|
|
42
|
+
|
|
43
|
+
{/* Progress only — various positions */}
|
|
44
|
+
<div>
|
|
45
|
+
<p className={SECTION_HEADER}>Progress — first step</p>
|
|
46
|
+
<div className="mt-4 max-w-56">
|
|
47
|
+
<WizardProgress current={1} total={5} />
|
|
48
|
+
</div>
|
|
49
|
+
</div>
|
|
50
|
+
<div>
|
|
51
|
+
<p className={SECTION_HEADER}>Progress — mid-flow</p>
|
|
52
|
+
<div className="mt-4 max-w-56">
|
|
53
|
+
<WizardProgress current={3} total={5} />
|
|
54
|
+
</div>
|
|
55
|
+
</div>
|
|
56
|
+
<div>
|
|
57
|
+
<p className={SECTION_HEADER}>Progress — complete</p>
|
|
58
|
+
<div className="mt-4 max-w-56">
|
|
59
|
+
<WizardProgress current={5} total={5} />
|
|
60
|
+
</div>
|
|
61
|
+
</div>
|
|
62
|
+
</div>
|
|
63
|
+
);
|
|
64
|
+
};
|
package/src/styles/tokens.css
CHANGED
|
@@ -39,6 +39,9 @@
|
|
|
39
39
|
|
|
40
40
|
--color-ring: #f22b79; /* Figma: focus-ring */
|
|
41
41
|
|
|
42
|
+
/* cta — decorative gradient accent, used with --color-primary in the CTA button ring */
|
|
43
|
+
--color-cta-accent: #6eece7; /* Figma: CTA border accent (cyan) */
|
|
44
|
+
|
|
42
45
|
/* sidebar */
|
|
43
46
|
--color-sidebar: #f8fafa; /* Figma: bg-secondary-subtle */
|
|
44
47
|
--color-sidebar-foreground: #3a4848; /* Figma: fg-secondary */
|
|
@@ -110,6 +113,24 @@
|
|
|
110
113
|
--animate-beacon-ring-pulse: beacon-ring-pulse 2.2s ease-in-out infinite;
|
|
111
114
|
}
|
|
112
115
|
|
|
116
|
+
@keyframes shimmer-text {
|
|
117
|
+
from {
|
|
118
|
+
background-position: 100% center;
|
|
119
|
+
}
|
|
120
|
+
to {
|
|
121
|
+
background-position: 0% center;
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
@keyframes alpic-ride {
|
|
126
|
+
0% {
|
|
127
|
+
transform: translate(0px, 0px);
|
|
128
|
+
}
|
|
129
|
+
100% {
|
|
130
|
+
transform: translate(237px, -64px);
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
113
134
|
@keyframes accordion-down {
|
|
114
135
|
from {
|
|
115
136
|
height: 0;
|
|
@@ -143,14 +164,166 @@
|
|
|
143
164
|
@keyframes beacon-ring-pulse {
|
|
144
165
|
0%,
|
|
145
166
|
100% {
|
|
146
|
-
opacity: 0.
|
|
167
|
+
opacity: 0.55;
|
|
147
168
|
transform: scale(1);
|
|
148
|
-
filter: drop-shadow(0 0 0 rgba(59, 130, 246, 0));
|
|
149
169
|
}
|
|
150
170
|
50% {
|
|
151
|
-
opacity:
|
|
171
|
+
opacity: 0.9;
|
|
152
172
|
transform: scale(1.02);
|
|
153
|
-
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/* ─── CTA button — animated conic gradient ring ───────────────────────────── */
|
|
177
|
+
|
|
178
|
+
@property --cta-angle {
|
|
179
|
+
syntax: "<angle>";
|
|
180
|
+
inherits: false;
|
|
181
|
+
initial-value: 135deg;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
@keyframes cta-rotate {
|
|
185
|
+
to {
|
|
186
|
+
--cta-angle: 495deg;
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
.button-cta {
|
|
191
|
+
position: relative;
|
|
192
|
+
isolation: isolate;
|
|
193
|
+
/* light mode: whisper-of-gradient surface tint + soft rose drop-shadow */
|
|
194
|
+
background-image: linear-gradient(
|
|
195
|
+
135deg,
|
|
196
|
+
color-mix(in oklab, var(--color-primary) 5%, transparent) 0%,
|
|
197
|
+
color-mix(in oklab, var(--color-cta-accent) 5%, transparent) 100%
|
|
198
|
+
);
|
|
199
|
+
box-shadow:
|
|
200
|
+
0 6px 24px -10px color-mix(in oklab, var(--color-primary) 38%, transparent),
|
|
201
|
+
0 2px 6px -4px color-mix(in oklab, var(--color-cta-accent) 30%, transparent);
|
|
202
|
+
transition:
|
|
203
|
+
box-shadow 400ms ease,
|
|
204
|
+
transform 300ms ease,
|
|
205
|
+
filter 300ms ease;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
@media (hover: hover) {
|
|
209
|
+
.button-cta:hover {
|
|
210
|
+
box-shadow:
|
|
211
|
+
0 10px 30px -8px color-mix(in oklab, var(--color-primary) 52%, transparent),
|
|
212
|
+
0 3px 10px -3px color-mix(in oklab, var(--color-cta-accent) 40%, transparent);
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
/* dark mode: solid inverted surface, no tint or shadow — let the halo do the work */
|
|
217
|
+
.dark .button-cta {
|
|
218
|
+
background-image: none;
|
|
219
|
+
box-shadow: none;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
.button-cta::before,
|
|
223
|
+
.button-cta::after {
|
|
224
|
+
content: "";
|
|
225
|
+
position: absolute;
|
|
226
|
+
inset: 0;
|
|
227
|
+
border-radius: inherit;
|
|
228
|
+
pointer-events: none;
|
|
229
|
+
/* Always "running" in browser terms, but paused at rest — freezes at current
|
|
230
|
+
angle on unhover instead of snapping back. */
|
|
231
|
+
animation: cta-rotate 3.2s linear infinite;
|
|
232
|
+
animation-play-state: paused;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
/* Gradient ring (masked so only the border shows) */
|
|
236
|
+
.button-cta::before {
|
|
237
|
+
padding: 1.5px;
|
|
238
|
+
background: conic-gradient(
|
|
239
|
+
from var(--cta-angle),
|
|
240
|
+
var(--color-cta-accent) 0deg,
|
|
241
|
+
var(--color-primary) 150deg,
|
|
242
|
+
var(--color-cta-accent) 300deg,
|
|
243
|
+
var(--color-cta-accent) 360deg
|
|
244
|
+
);
|
|
245
|
+
-webkit-mask:
|
|
246
|
+
linear-gradient(#000 0 0) content-box,
|
|
247
|
+
linear-gradient(#000 0 0);
|
|
248
|
+
-webkit-mask-composite: xor;
|
|
249
|
+
mask-composite: exclude;
|
|
250
|
+
transition: filter 400ms ease;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
/* Blurred glow halo behind the button — subtle in light, bolder in dark */
|
|
254
|
+
.button-cta::after {
|
|
255
|
+
z-index: -1;
|
|
256
|
+
background: conic-gradient(
|
|
257
|
+
from var(--cta-angle),
|
|
258
|
+
var(--color-cta-accent) 0deg,
|
|
259
|
+
var(--color-primary) 150deg,
|
|
260
|
+
var(--color-cta-accent) 300deg,
|
|
261
|
+
var(--color-cta-accent) 360deg
|
|
262
|
+
);
|
|
263
|
+
filter: blur(12px);
|
|
264
|
+
opacity: 0.05;
|
|
265
|
+
transition: opacity 400ms ease;
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
.dark .button-cta::after {
|
|
269
|
+
opacity: 0.14;
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
@media (hover: hover) {
|
|
273
|
+
.button-cta:hover::before,
|
|
274
|
+
.button-cta:hover::after {
|
|
275
|
+
animation-play-state: running;
|
|
276
|
+
}
|
|
277
|
+
.button-cta:hover::before {
|
|
278
|
+
filter: saturate(1.15) brightness(1.05);
|
|
279
|
+
}
|
|
280
|
+
.button-cta:hover::after {
|
|
281
|
+
opacity: 0.18;
|
|
282
|
+
}
|
|
283
|
+
.dark .button-cta:hover::after {
|
|
284
|
+
opacity: 0.32;
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
.button-cta:focus-visible::before,
|
|
289
|
+
.button-cta:focus-visible::after {
|
|
290
|
+
animation-play-state: running;
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
.button-cta:disabled::before,
|
|
294
|
+
.button-cta:disabled::after,
|
|
295
|
+
[aria-busy="true"].button-cta::before,
|
|
296
|
+
[aria-busy="true"].button-cta::after {
|
|
297
|
+
animation-play-state: paused;
|
|
298
|
+
}
|
|
299
|
+
.button-cta:disabled::after {
|
|
300
|
+
opacity: 0;
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
@media (prefers-reduced-motion: reduce) {
|
|
304
|
+
.button-cta::before,
|
|
305
|
+
.button-cta::after {
|
|
306
|
+
animation: none;
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
/* Icon slide on hover (applied to [data-cta-icon-trailing]) */
|
|
311
|
+
.button-cta [data-cta-icon-trailing] {
|
|
312
|
+
transition: transform 300ms cubic-bezier(0.2, 0.8, 0.2, 1);
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
@media (hover: hover) {
|
|
316
|
+
.button-cta:hover [data-cta-icon-trailing] {
|
|
317
|
+
transform: translateX(2px);
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
@media (prefers-reduced-motion: reduce) {
|
|
322
|
+
.button-cta [data-cta-icon-trailing] {
|
|
323
|
+
transition: none;
|
|
324
|
+
}
|
|
325
|
+
.button-cta:hover [data-cta-icon-trailing] {
|
|
326
|
+
transform: none;
|
|
154
327
|
}
|
|
155
328
|
}
|
|
156
329
|
|
|
@@ -191,6 +364,9 @@
|
|
|
191
364
|
|
|
192
365
|
--color-ring: #f22b79; /* Figma: focus-ring */
|
|
193
366
|
|
|
367
|
+
/* cta — decorative gradient accent, used with --color-primary in the CTA button ring */
|
|
368
|
+
--color-cta-accent: #6eece7; /* Figma: CTA border accent (cyan) */
|
|
369
|
+
|
|
194
370
|
/* sidebar */
|
|
195
371
|
--color-sidebar: #0c1c1c; /* Figma: bg-secondary */
|
|
196
372
|
--color-sidebar-foreground: #90a4a4; /* Figma: fg-secondary */
|
|
@@ -249,6 +425,14 @@
|
|
|
249
425
|
|
|
250
426
|
@custom-variant dark (&:where(.dark, .dark *));
|
|
251
427
|
|
|
428
|
+
:root {
|
|
429
|
+
--gradient-sidebar: linear-gradient(0deg, #c9e2e280 0%, #ffffff 70%); /* Figma: bg-nav-gradiant-light */
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
.dark {
|
|
433
|
+
--gradient-sidebar: linear-gradient(0deg, #213535 0%, #121e1e 70%); /* Figma: bg-nav-gradiant-dark */
|
|
434
|
+
}
|
|
435
|
+
|
|
252
436
|
@layer base {
|
|
253
437
|
* {
|
|
254
438
|
@apply border-border shadow-shadow;
|
|
@@ -302,6 +486,11 @@
|
|
|
302
486
|
|
|
303
487
|
/* ─── Type preset utilities ───────────────────────────────────────────────── */
|
|
304
488
|
|
|
489
|
+
@utility bg-sidebar-surface {
|
|
490
|
+
background-color: var(--color-background);
|
|
491
|
+
background-image: var(--gradient-sidebar);
|
|
492
|
+
}
|
|
493
|
+
|
|
305
494
|
@utility type-display-2xl {
|
|
306
495
|
font-family: var(--font-display);
|
|
307
496
|
font-size: var(--font-size-display-2xl);
|