@naeil/ui 0.1.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/README.md +36 -0
- package/messages/en.json +221 -0
- package/messages/ja.json +221 -0
- package/messages/ko.json +221 -0
- package/package.json +73 -0
- package/postcss.config.mjs +7 -0
- package/src/app/[locale]/(content)/baby-agent/page.tsx +30 -0
- package/src/app/[locale]/(content)/blog/page.tsx +68 -0
- package/src/app/[locale]/(content)/cc/page.tsx +216 -0
- package/src/app/[locale]/(content)/design/page.tsx +703 -0
- package/src/app/[locale]/(content)/layout.tsx +7 -0
- package/src/app/[locale]/(content)/pkm/page.tsx +31 -0
- package/src/app/[locale]/(content)/projects/page.tsx +100 -0
- package/src/app/[locale]/(content)/sa/page.tsx +179 -0
- package/src/app/[locale]/(landing)/page.tsx +157 -0
- package/src/app/[locale]/layout.tsx +72 -0
- package/src/app/[locale]/login/actions.ts +10 -0
- package/src/app/[locale]/login/oauth-buttons.tsx +78 -0
- package/src/app/[locale]/login/page.tsx +85 -0
- package/src/app/auth/callback/route.ts +78 -0
- package/src/app/favicon.ico +0 -0
- package/src/app/globals.css +32 -0
- package/src/app/layout.tsx +22 -0
- package/src/app/sitemap.ts +26 -0
- package/src/components/accent-picker.tsx +62 -0
- package/src/components/auth-slot.tsx +96 -0
- package/src/components/footer-wrapper.tsx +8 -0
- package/src/components/footer.tsx +193 -0
- package/src/components/hero-scene.tsx +948 -0
- package/src/components/hero-section.tsx +57 -0
- package/src/components/locale-switcher.tsx +74 -0
- package/src/components/logo.tsx +51 -0
- package/src/components/nav-server-wrapper.tsx +22 -0
- package/src/components/nav-wrapper.tsx +45 -0
- package/src/components/nav.tsx +550 -0
- package/src/components/paraglider-cursor.tsx +112 -0
- package/src/components/project-layout.tsx +194 -0
- package/src/components/theme-provider.tsx +10 -0
- package/src/components/theme-toggle-icon.tsx +62 -0
- package/src/components/theme-toggle.tsx +39 -0
- package/src/components/typography.tsx +27 -0
- package/src/components/ui/avatar.tsx +109 -0
- package/src/components/ui/badge.tsx +47 -0
- package/src/components/ui/button.tsx +65 -0
- package/src/components/ui/card.tsx +106 -0
- package/src/components/ui/dialog.tsx +158 -0
- package/src/components/ui/dropdown-menu.tsx +257 -0
- package/src/components/ui/input.tsx +21 -0
- package/src/components/ui/sonner.tsx +54 -0
- package/src/components/workflow-diagram.tsx +414 -0
- package/src/i18n/config.ts +8 -0
- package/src/i18n/request.ts +16 -0
- package/src/i18n/routing.ts +12 -0
- package/src/lib/supabase/client.ts +18 -0
- package/src/lib/supabase/middleware.ts +42 -0
- package/src/lib/supabase/server.ts +37 -0
- package/src/lib/utils.ts +6 -0
- package/src/proxy.ts +62 -0
- package/src/tokens/.gitkeep +0 -0
- package/src/tokens/color.json +56 -0
- package/src/tokens/radius.json +9 -0
- package/src/tokens/semantic.json +81 -0
- package/src/tokens/shadow.json +7 -0
- package/src/tokens/spacing.json +16 -0
- package/src/tokens/typography.json +34 -0
- package/src/types/i18n.d.ts +6 -0
- package/src/utils/.gitkeep +0 -0
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useRef, useState, useEffect } from "react";
|
|
4
|
+
import { HeroScene } from "@/components/hero-scene";
|
|
5
|
+
import { ParagliderCursor } from "@/components/paraglider-cursor";
|
|
6
|
+
|
|
7
|
+
export function HeroSection() {
|
|
8
|
+
const heroRef = useRef<HTMLElement>(null);
|
|
9
|
+
const [inView, setInView] = useState(true);
|
|
10
|
+
|
|
11
|
+
useEffect(() => {
|
|
12
|
+
const el = heroRef.current;
|
|
13
|
+
if (!el || typeof IntersectionObserver === "undefined") return;
|
|
14
|
+
|
|
15
|
+
const observer = new IntersectionObserver(
|
|
16
|
+
([entry]) => setInView(entry.isIntersecting),
|
|
17
|
+
{ rootMargin: "200px 0px", threshold: 0 },
|
|
18
|
+
);
|
|
19
|
+
observer.observe(el);
|
|
20
|
+
return () => observer.disconnect();
|
|
21
|
+
}, []);
|
|
22
|
+
|
|
23
|
+
return (
|
|
24
|
+
<section
|
|
25
|
+
ref={heroRef}
|
|
26
|
+
className="relative flex min-h-screen flex-col items-center justify-center overflow-hidden px-6"
|
|
27
|
+
>
|
|
28
|
+
{/* 3D Scene — background */}
|
|
29
|
+
<div className="pointer-events-none absolute inset-0 flex items-center justify-center opacity-60">
|
|
30
|
+
<HeroScene active={inView} />
|
|
31
|
+
</div>
|
|
32
|
+
|
|
33
|
+
{/* Paraglider follows mouse — desktop only */}
|
|
34
|
+
<div className="hidden md:block">
|
|
35
|
+
<ParagliderCursor containerRef={heroRef} />
|
|
36
|
+
</div>
|
|
37
|
+
|
|
38
|
+
{/* Scroll hint */}
|
|
39
|
+
<div className="absolute bottom-8 left-1/2 z-10 -translate-x-1/2 animate-bounce">
|
|
40
|
+
<svg
|
|
41
|
+
width="20"
|
|
42
|
+
height="20"
|
|
43
|
+
viewBox="0 0 24 24"
|
|
44
|
+
fill="none"
|
|
45
|
+
stroke="currentColor"
|
|
46
|
+
strokeWidth="1.5"
|
|
47
|
+
strokeLinecap="round"
|
|
48
|
+
strokeLinejoin="round"
|
|
49
|
+
className="text-muted-foreground/50"
|
|
50
|
+
>
|
|
51
|
+
<path d="M7 13l5 5 5-5" />
|
|
52
|
+
<path d="M7 6l5 5 5-5" />
|
|
53
|
+
</svg>
|
|
54
|
+
</div>
|
|
55
|
+
</section>
|
|
56
|
+
);
|
|
57
|
+
}
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
DropdownMenu,
|
|
5
|
+
DropdownMenuTrigger,
|
|
6
|
+
DropdownMenuContent,
|
|
7
|
+
DropdownMenuItem,
|
|
8
|
+
} from "./ui/dropdown-menu";
|
|
9
|
+
|
|
10
|
+
interface LocaleSwitcherProps {
|
|
11
|
+
/** Available locales */
|
|
12
|
+
locales: Array<{ code: string; short: string; full: string }>;
|
|
13
|
+
/** Current locale */
|
|
14
|
+
currentLocale: string;
|
|
15
|
+
/** Called when user selects a new locale */
|
|
16
|
+
onLocaleChange: (locale: string) => void;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function LocaleSwitcher({
|
|
20
|
+
locales,
|
|
21
|
+
currentLocale,
|
|
22
|
+
onLocaleChange,
|
|
23
|
+
}: LocaleSwitcherProps) {
|
|
24
|
+
const current = locales.find((l) => l.code === currentLocale);
|
|
25
|
+
|
|
26
|
+
return (
|
|
27
|
+
<DropdownMenu>
|
|
28
|
+
<DropdownMenuTrigger asChild>
|
|
29
|
+
<button
|
|
30
|
+
className="text-muted-foreground hover:text-foreground flex items-center gap-1.5 text-sm transition-colors focus:outline-none"
|
|
31
|
+
aria-label={`Change language — current: ${current?.short ?? currentLocale}`}
|
|
32
|
+
>
|
|
33
|
+
<GlobeIcon />
|
|
34
|
+
{current?.short ?? currentLocale.toUpperCase()}
|
|
35
|
+
</button>
|
|
36
|
+
</DropdownMenuTrigger>
|
|
37
|
+
<DropdownMenuContent align="end" className="min-w-[120px]">
|
|
38
|
+
{locales.map((l) => (
|
|
39
|
+
<DropdownMenuItem
|
|
40
|
+
key={l.code}
|
|
41
|
+
onClick={() => onLocaleChange(l.code)}
|
|
42
|
+
className={l.code === currentLocale ? "font-medium" : ""}
|
|
43
|
+
>
|
|
44
|
+
<span className="text-muted-foreground mr-2 w-5 text-xs">
|
|
45
|
+
{l.short}
|
|
46
|
+
</span>
|
|
47
|
+
{l.full}
|
|
48
|
+
</DropdownMenuItem>
|
|
49
|
+
))}
|
|
50
|
+
</DropdownMenuContent>
|
|
51
|
+
</DropdownMenu>
|
|
52
|
+
);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function GlobeIcon() {
|
|
56
|
+
return (
|
|
57
|
+
<svg
|
|
58
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
59
|
+
width="14"
|
|
60
|
+
height="14"
|
|
61
|
+
viewBox="0 0 24 24"
|
|
62
|
+
fill="none"
|
|
63
|
+
stroke="currentColor"
|
|
64
|
+
strokeWidth="1.5"
|
|
65
|
+
strokeLinecap="round"
|
|
66
|
+
strokeLinejoin="round"
|
|
67
|
+
className="shrink-0"
|
|
68
|
+
>
|
|
69
|
+
<circle cx="12" cy="12" r="10" />
|
|
70
|
+
<path d="M12 2a14.5 14.5 0 0 0 0 20 14.5 14.5 0 0 0 0-20" />
|
|
71
|
+
<path d="M2 12h20" />
|
|
72
|
+
</svg>
|
|
73
|
+
);
|
|
74
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
export function Logo({
|
|
2
|
+
size = 20,
|
|
3
|
+
mono = false,
|
|
4
|
+
className,
|
|
5
|
+
}: {
|
|
6
|
+
size?: number;
|
|
7
|
+
mono?: boolean;
|
|
8
|
+
className?: string;
|
|
9
|
+
}) {
|
|
10
|
+
const id = `naeil-logo-${size}`;
|
|
11
|
+
return (
|
|
12
|
+
<svg
|
|
13
|
+
width={size}
|
|
14
|
+
height={size}
|
|
15
|
+
viewBox="0 0 24 24"
|
|
16
|
+
fill="none"
|
|
17
|
+
aria-label="naeil"
|
|
18
|
+
className={className}
|
|
19
|
+
>
|
|
20
|
+
<path
|
|
21
|
+
d="M6 4 L6 16 L20 16"
|
|
22
|
+
stroke="currentColor"
|
|
23
|
+
strokeWidth="2"
|
|
24
|
+
strokeLinecap="square"
|
|
25
|
+
strokeLinejoin="miter"
|
|
26
|
+
/>
|
|
27
|
+
{mono ? (
|
|
28
|
+
<circle cx="15" cy="16" r="3.5" fill="currentColor" mask={`url(#${id})`} />
|
|
29
|
+
) : (
|
|
30
|
+
<circle cx="15" cy="16" r="3.5" fill={`url(#${id}-grad)`} mask={`url(#${id})`} />
|
|
31
|
+
)}
|
|
32
|
+
<defs>
|
|
33
|
+
{!mono && (
|
|
34
|
+
<linearGradient
|
|
35
|
+
id={`${id}-grad`}
|
|
36
|
+
x1="15"
|
|
37
|
+
y1="12.5"
|
|
38
|
+
x2="15"
|
|
39
|
+
y2="16"
|
|
40
|
+
>
|
|
41
|
+
<stop stopColor="#eab308" />
|
|
42
|
+
<stop offset="1" stopColor="#dc2626" />
|
|
43
|
+
</linearGradient>
|
|
44
|
+
)}
|
|
45
|
+
<mask id={id}>
|
|
46
|
+
<rect x="0" y="0" width="24" height="16" fill="white" />
|
|
47
|
+
</mask>
|
|
48
|
+
</defs>
|
|
49
|
+
</svg>
|
|
50
|
+
);
|
|
51
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { createClient } from "@/lib/supabase/server";
|
|
2
|
+
import { NavWrapper } from "./nav-wrapper";
|
|
3
|
+
|
|
4
|
+
export async function NavServerWrapper() {
|
|
5
|
+
const supabase = await createClient();
|
|
6
|
+
const {
|
|
7
|
+
data: { user },
|
|
8
|
+
} = await supabase.auth.getUser();
|
|
9
|
+
|
|
10
|
+
const authUser = user
|
|
11
|
+
? {
|
|
12
|
+
id: user.id,
|
|
13
|
+
name:
|
|
14
|
+
(user.user_metadata?.full_name as string) ||
|
|
15
|
+
(user.user_metadata?.name as string) ||
|
|
16
|
+
undefined,
|
|
17
|
+
email: user.email,
|
|
18
|
+
}
|
|
19
|
+
: null;
|
|
20
|
+
|
|
21
|
+
return <NavWrapper user={authUser} />;
|
|
22
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { Nav } from "./nav";
|
|
4
|
+
import { ThemeToggleIcon } from "./theme-toggle-icon";
|
|
5
|
+
import { LocaleSwitcher } from "./locale-switcher";
|
|
6
|
+
import { AuthSlot } from "./auth-slot";
|
|
7
|
+
import { Link, usePathname, useRouter } from "@/i18n/routing";
|
|
8
|
+
import { useLocale } from "next-intl";
|
|
9
|
+
import { type Locale } from "@/i18n/config";
|
|
10
|
+
|
|
11
|
+
const LOCALE_LIST = [
|
|
12
|
+
{ code: "en", short: "EN", full: "English" },
|
|
13
|
+
{ code: "ko", short: "KO", full: "한국어" },
|
|
14
|
+
{ code: "ja", short: "JA", full: "日本語" },
|
|
15
|
+
];
|
|
16
|
+
|
|
17
|
+
interface NavWrapperProps {
|
|
18
|
+
user: { id: string; name?: string; email?: string } | null;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function NavWrapper({ user }: NavWrapperProps) {
|
|
22
|
+
const pathname = usePathname();
|
|
23
|
+
const locale = useLocale();
|
|
24
|
+
const router = useRouter();
|
|
25
|
+
|
|
26
|
+
return (
|
|
27
|
+
<Nav
|
|
28
|
+
Link={Link}
|
|
29
|
+
usePathname={() => pathname}
|
|
30
|
+
showBlog={true}
|
|
31
|
+
toolbarSlot={
|
|
32
|
+
<>
|
|
33
|
+
<ThemeToggleIcon />
|
|
34
|
+
<LocaleSwitcher
|
|
35
|
+
locales={LOCALE_LIST}
|
|
36
|
+
currentLocale={locale}
|
|
37
|
+
onLocaleChange={(l) => router.replace(pathname, { locale: l as Locale })}
|
|
38
|
+
/>
|
|
39
|
+
<div className="h-4 w-px bg-border/40" />
|
|
40
|
+
<AuthSlot user={user} />
|
|
41
|
+
</>
|
|
42
|
+
}
|
|
43
|
+
/>
|
|
44
|
+
);
|
|
45
|
+
}
|