@kalikayi/shared 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/package.json +46 -0
- package/src/components/ads/ad-banner.tsx +65 -0
- package/src/components/ads/tool-promo-banner.tsx +73 -0
- package/src/components/layout/footer.tsx +119 -0
- package/src/components/layout/header.tsx +75 -0
- package/src/components/layout/mobile-nav.tsx +139 -0
- package/src/components/layout/sub-header.tsx +113 -0
- package/src/components/seo/json-ld.tsx +81 -0
- package/src/components/theme/accent-picker.tsx +63 -0
- package/src/components/theme/theme-provider.tsx +17 -0
- package/src/components/theme/theme-toggle.tsx +40 -0
- package/src/hooks/use-copy-to-clipboard.ts +23 -0
- package/src/index.ts +43 -0
- package/src/lib/kalikayi-tools.ts +35 -0
- package/src/lib/utils.ts +6 -0
- package/src/styles/theme-base.css +159 -0
- package/src/types/index.ts +38 -0
package/package.json
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@kalikayi/shared",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Shared components, hooks, and utilities for the Kalikayi ecosystem",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "./src/index.ts",
|
|
7
|
+
"types": "./src/index.ts",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": "./src/index.ts",
|
|
10
|
+
"./styles/theme-base.css": "./src/styles/theme-base.css"
|
|
11
|
+
},
|
|
12
|
+
"files": [
|
|
13
|
+
"src"
|
|
14
|
+
],
|
|
15
|
+
"scripts": {
|
|
16
|
+
"typecheck": "tsc --noEmit"
|
|
17
|
+
},
|
|
18
|
+
"peerDependencies": {
|
|
19
|
+
"lucide-react": ">=0.400.0",
|
|
20
|
+
"next": "^14.0.0 || ^15.0.0 || ^16.0.0",
|
|
21
|
+
"next-themes": ">=0.3.0",
|
|
22
|
+
"react": "^18.0.0 || ^19.0.0",
|
|
23
|
+
"react-dom": "^18.0.0 || ^19.0.0"
|
|
24
|
+
},
|
|
25
|
+
"dependencies": {
|
|
26
|
+
"clsx": "^2.1.0",
|
|
27
|
+
"tailwind-merge": "^3.0.0"
|
|
28
|
+
},
|
|
29
|
+
"devDependencies": {
|
|
30
|
+
"@types/node": "^25.3.5",
|
|
31
|
+
"@types/react": "^19.0.0",
|
|
32
|
+
"@types/react-dom": "^19.0.0",
|
|
33
|
+
"typescript": "^5.7.0"
|
|
34
|
+
},
|
|
35
|
+
"license": "MIT",
|
|
36
|
+
"repository": {
|
|
37
|
+
"type": "git",
|
|
38
|
+
"url": "https://github.com/sunilkalikayi/kalikayi-hub"
|
|
39
|
+
},
|
|
40
|
+
"keywords": [
|
|
41
|
+
"kalikayi",
|
|
42
|
+
"shared",
|
|
43
|
+
"components",
|
|
44
|
+
"next.js"
|
|
45
|
+
]
|
|
46
|
+
}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useEffect, useRef } from "react";
|
|
4
|
+
import { cn } from "../../lib/utils";
|
|
5
|
+
|
|
6
|
+
const slotHeights: Record<string, string> = {
|
|
7
|
+
"top-leaderboard": "min-h-[90px]",
|
|
8
|
+
"in-content": "min-h-[250px]",
|
|
9
|
+
"bottom-rectangle": "min-h-[280px]",
|
|
10
|
+
sidebar: "min-h-[250px]",
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
interface AdBannerProps {
|
|
14
|
+
slot?: string;
|
|
15
|
+
format?: "auto" | "rectangle" | "horizontal" | "vertical";
|
|
16
|
+
className?: string;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function AdBanner({ slot, format = "auto", className }: AdBannerProps) {
|
|
20
|
+
const adRef = useRef<HTMLDivElement>(null);
|
|
21
|
+
const adsenseId = typeof window !== "undefined"
|
|
22
|
+
? process.env.NEXT_PUBLIC_ADSENSE_ID
|
|
23
|
+
: undefined;
|
|
24
|
+
const height = slot ? (slotHeights[slot] ?? "min-h-[90px]") : "min-h-[90px]";
|
|
25
|
+
|
|
26
|
+
useEffect(() => {
|
|
27
|
+
if (!adsenseId || !slot) return;
|
|
28
|
+
try {
|
|
29
|
+
const w = window as Window & { adsbygoogle?: unknown[] };
|
|
30
|
+
(w.adsbygoogle = w.adsbygoogle ?? []).push({});
|
|
31
|
+
} catch {
|
|
32
|
+
// AdSense not loaded
|
|
33
|
+
}
|
|
34
|
+
}, [adsenseId, slot]);
|
|
35
|
+
|
|
36
|
+
// Dev placeholder or no slot/adsenseId
|
|
37
|
+
if (!adsenseId || !slot) {
|
|
38
|
+
return (
|
|
39
|
+
<div
|
|
40
|
+
className={cn(
|
|
41
|
+
"flex items-center justify-center rounded-lg border border-dashed border-border/50 bg-muted/30 text-xs text-muted-foreground/40",
|
|
42
|
+
height,
|
|
43
|
+
className
|
|
44
|
+
)}
|
|
45
|
+
aria-hidden="true"
|
|
46
|
+
>
|
|
47
|
+
Ad: {slot || "placeholder"}
|
|
48
|
+
</div>
|
|
49
|
+
);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// Production AdSense
|
|
53
|
+
return (
|
|
54
|
+
<div ref={adRef} className={cn("ad-container overflow-hidden", height, className)} aria-hidden="true">
|
|
55
|
+
<ins
|
|
56
|
+
className="adsbygoogle"
|
|
57
|
+
style={{ display: "block" }}
|
|
58
|
+
data-ad-client={adsenseId}
|
|
59
|
+
data-ad-slot={slot}
|
|
60
|
+
data-ad-format={format}
|
|
61
|
+
data-full-width-responsive="true"
|
|
62
|
+
/>
|
|
63
|
+
</div>
|
|
64
|
+
);
|
|
65
|
+
}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { ExternalLink } from "lucide-react";
|
|
4
|
+
import { getLiveTools } from "../../lib/kalikayi-tools";
|
|
5
|
+
import { cn } from "../../lib/utils";
|
|
6
|
+
|
|
7
|
+
interface ToolPromoBannerProps {
|
|
8
|
+
variant: "horizontal" | "card-grid";
|
|
9
|
+
excludeTool?: string;
|
|
10
|
+
className?: string;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function ToolPromoBanner({ variant, excludeTool, className }: ToolPromoBannerProps) {
|
|
14
|
+
const tools = getLiveTools().filter((t) => t.name !== excludeTool);
|
|
15
|
+
|
|
16
|
+
if (tools.length === 0) return null;
|
|
17
|
+
|
|
18
|
+
if (variant === "horizontal") {
|
|
19
|
+
return (
|
|
20
|
+
<div
|
|
21
|
+
className={cn(
|
|
22
|
+
"rounded-xl border border-primary/20 bg-primary/5 p-4",
|
|
23
|
+
className
|
|
24
|
+
)}
|
|
25
|
+
>
|
|
26
|
+
<p className="mb-3 text-xs font-semibold uppercase tracking-wider text-primary">
|
|
27
|
+
More Free Tools by Kalikayi
|
|
28
|
+
</p>
|
|
29
|
+
<div className="flex flex-wrap gap-3">
|
|
30
|
+
{tools.map((tool) => (
|
|
31
|
+
<a
|
|
32
|
+
key={tool.name}
|
|
33
|
+
href={tool.href}
|
|
34
|
+
target="_blank"
|
|
35
|
+
rel="noopener noreferrer"
|
|
36
|
+
className="flex items-center gap-1.5 rounded-lg border border-border/50 bg-background/60 px-3 py-2 text-sm font-medium text-foreground hover:border-primary/30 hover:bg-primary/5"
|
|
37
|
+
>
|
|
38
|
+
{tool.name}
|
|
39
|
+
<ExternalLink className="h-3 w-3 text-muted-foreground" />
|
|
40
|
+
</a>
|
|
41
|
+
))}
|
|
42
|
+
</div>
|
|
43
|
+
</div>
|
|
44
|
+
);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
return (
|
|
48
|
+
<div className={className}>
|
|
49
|
+
<p className="mb-4 text-xs font-semibold uppercase tracking-wider text-primary">
|
|
50
|
+
Explore More Free Tools
|
|
51
|
+
</p>
|
|
52
|
+
<div className="grid gap-3 sm:grid-cols-2">
|
|
53
|
+
{tools.map((tool) => (
|
|
54
|
+
<a
|
|
55
|
+
key={tool.name}
|
|
56
|
+
href={tool.href}
|
|
57
|
+
target="_blank"
|
|
58
|
+
rel="noopener noreferrer"
|
|
59
|
+
className="rounded-lg border border-border/50 p-4 hover:border-primary/30 hover:shadow-md"
|
|
60
|
+
>
|
|
61
|
+
<div className="flex items-center justify-between">
|
|
62
|
+
<div>
|
|
63
|
+
<p className="font-semibold text-foreground">{tool.name}</p>
|
|
64
|
+
<p className="text-sm text-muted-foreground">{tool.tagline}</p>
|
|
65
|
+
</div>
|
|
66
|
+
<ExternalLink className="h-4 w-4 text-muted-foreground" />
|
|
67
|
+
</div>
|
|
68
|
+
</a>
|
|
69
|
+
))}
|
|
70
|
+
</div>
|
|
71
|
+
</div>
|
|
72
|
+
);
|
|
73
|
+
}
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
import { Github, Linkedin, Globe } from "lucide-react";
|
|
2
|
+
|
|
3
|
+
const QUICK_LINKS = [
|
|
4
|
+
{ href: "https://kalikayi.com/tools", label: "Tools" },
|
|
5
|
+
{ href: "https://kalikayi.com/blog", label: "Blog" },
|
|
6
|
+
{ href: "https://kalikayi.com/about", label: "About" },
|
|
7
|
+
{ href: "https://kalikayi.com/contact", label: "Contact" },
|
|
8
|
+
{ href: "https://kalikayi.com/creator", label: "Creator" },
|
|
9
|
+
];
|
|
10
|
+
|
|
11
|
+
const LEGAL_LINKS = [
|
|
12
|
+
{ href: "https://kalikayi.com/privacy-policy", label: "Privacy Policy" },
|
|
13
|
+
{ href: "https://kalikayi.com/terms", label: "Terms of Service" },
|
|
14
|
+
{ href: "https://kalikayi.com/disclaimer", label: "Disclaimer" },
|
|
15
|
+
];
|
|
16
|
+
|
|
17
|
+
const SOCIAL = {
|
|
18
|
+
github: "https://github.com/sunilkalikayi",
|
|
19
|
+
linkedin: "https://linkedin.com/in/sunilkalikayi",
|
|
20
|
+
portfolio: "https://sunilkalikayi.vercel.app/",
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
export function Footer() {
|
|
24
|
+
const year = new Date().getFullYear();
|
|
25
|
+
|
|
26
|
+
return (
|
|
27
|
+
<footer className="border-t border-border/30 bg-card/50">
|
|
28
|
+
<div className="mx-auto max-w-7xl px-6 py-12">
|
|
29
|
+
<div className="grid gap-8 sm:grid-cols-2 lg:grid-cols-4">
|
|
30
|
+
{/* Brand */}
|
|
31
|
+
<div className="space-y-3">
|
|
32
|
+
<h3 className="text-lg font-bold text-primary">Kalikayi</h3>
|
|
33
|
+
<p className="text-sm leading-relaxed text-muted-foreground">
|
|
34
|
+
Free online tools that just work. No sign-up, no paywalls, no tricks.
|
|
35
|
+
</p>
|
|
36
|
+
</div>
|
|
37
|
+
|
|
38
|
+
{/* Quick Links */}
|
|
39
|
+
<div className="space-y-3">
|
|
40
|
+
<h4 className="text-sm font-semibold">Quick Links</h4>
|
|
41
|
+
<nav className="flex flex-col gap-2">
|
|
42
|
+
{QUICK_LINKS.map((link) => (
|
|
43
|
+
<a
|
|
44
|
+
key={link.href}
|
|
45
|
+
href={link.href}
|
|
46
|
+
className="text-sm text-muted-foreground transition-colors hover:text-foreground"
|
|
47
|
+
>
|
|
48
|
+
{link.label}
|
|
49
|
+
</a>
|
|
50
|
+
))}
|
|
51
|
+
</nav>
|
|
52
|
+
</div>
|
|
53
|
+
|
|
54
|
+
{/* Legal */}
|
|
55
|
+
<div className="space-y-3">
|
|
56
|
+
<h4 className="text-sm font-semibold">Legal</h4>
|
|
57
|
+
<nav className="flex flex-col gap-2">
|
|
58
|
+
{LEGAL_LINKS.map((link) => (
|
|
59
|
+
<a
|
|
60
|
+
key={link.href}
|
|
61
|
+
href={link.href}
|
|
62
|
+
className="text-sm text-muted-foreground transition-colors hover:text-foreground"
|
|
63
|
+
>
|
|
64
|
+
{link.label}
|
|
65
|
+
</a>
|
|
66
|
+
))}
|
|
67
|
+
</nav>
|
|
68
|
+
</div>
|
|
69
|
+
|
|
70
|
+
{/* Connect */}
|
|
71
|
+
<div className="space-y-3">
|
|
72
|
+
<h4 className="text-sm font-semibold">Connect</h4>
|
|
73
|
+
<div className="flex gap-3">
|
|
74
|
+
<a
|
|
75
|
+
href={SOCIAL.github}
|
|
76
|
+
target="_blank"
|
|
77
|
+
rel="noopener noreferrer"
|
|
78
|
+
className="text-muted-foreground transition-colors hover:text-foreground"
|
|
79
|
+
aria-label="GitHub"
|
|
80
|
+
>
|
|
81
|
+
<Github size={20} />
|
|
82
|
+
</a>
|
|
83
|
+
<a
|
|
84
|
+
href={SOCIAL.linkedin}
|
|
85
|
+
target="_blank"
|
|
86
|
+
rel="noopener noreferrer"
|
|
87
|
+
className="text-muted-foreground transition-colors hover:text-foreground"
|
|
88
|
+
aria-label="LinkedIn"
|
|
89
|
+
>
|
|
90
|
+
<Linkedin size={20} />
|
|
91
|
+
</a>
|
|
92
|
+
<a
|
|
93
|
+
href={SOCIAL.portfolio}
|
|
94
|
+
target="_blank"
|
|
95
|
+
rel="noopener noreferrer"
|
|
96
|
+
className="text-muted-foreground transition-colors hover:text-foreground"
|
|
97
|
+
aria-label="Portfolio"
|
|
98
|
+
>
|
|
99
|
+
<Globe size={20} />
|
|
100
|
+
</a>
|
|
101
|
+
</div>
|
|
102
|
+
</div>
|
|
103
|
+
</div>
|
|
104
|
+
|
|
105
|
+
{/* Separator */}
|
|
106
|
+
<div className="my-8 h-px bg-border/30" />
|
|
107
|
+
|
|
108
|
+
<div className="flex flex-col items-center justify-between gap-2 text-center sm:flex-row">
|
|
109
|
+
<p className="text-xs text-muted-foreground">
|
|
110
|
+
© {year} Kalikayi. All rights reserved.
|
|
111
|
+
</p>
|
|
112
|
+
<p className="text-xs text-muted-foreground">
|
|
113
|
+
Made with care for everyone
|
|
114
|
+
</p>
|
|
115
|
+
</div>
|
|
116
|
+
</div>
|
|
117
|
+
</footer>
|
|
118
|
+
);
|
|
119
|
+
}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import Link from "next/link";
|
|
4
|
+
import { usePathname } from "next/navigation";
|
|
5
|
+
import { AccentPicker } from "../theme/accent-picker";
|
|
6
|
+
import { ThemeToggle } from "../theme/theme-toggle";
|
|
7
|
+
import { MobileNav } from "./mobile-nav";
|
|
8
|
+
import { cn } from "../../lib/utils";
|
|
9
|
+
import type { SubHeaderConfig } from "../../types";
|
|
10
|
+
|
|
11
|
+
const NAV_LINKS = [
|
|
12
|
+
{ href: "https://kalikayi.com", label: "Home" },
|
|
13
|
+
{ href: "https://kalikayi.com/tools", label: "Tools" },
|
|
14
|
+
{ href: "https://kalikayi.com/blog", label: "Blog" },
|
|
15
|
+
{ href: "https://kalikayi.com/about", label: "About" },
|
|
16
|
+
{ href: "https://kalikayi.com/contact", label: "Contact" },
|
|
17
|
+
];
|
|
18
|
+
|
|
19
|
+
interface HeaderProps {
|
|
20
|
+
subHeaderConfig?: SubHeaderConfig;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function Header({ subHeaderConfig }: HeaderProps) {
|
|
24
|
+
const pathname = usePathname();
|
|
25
|
+
|
|
26
|
+
return (
|
|
27
|
+
<header className="sticky top-0 z-50 w-full border-b border-border/30 bg-background/80 backdrop-blur-xl">
|
|
28
|
+
<div className="mx-auto flex h-14 max-w-7xl items-center justify-between px-6">
|
|
29
|
+
{/* Logo */}
|
|
30
|
+
<a
|
|
31
|
+
href="https://kalikayi.com"
|
|
32
|
+
className="text-xl font-bold tracking-tight text-primary"
|
|
33
|
+
>
|
|
34
|
+
Kalikayi
|
|
35
|
+
</a>
|
|
36
|
+
|
|
37
|
+
{/* Desktop nav */}
|
|
38
|
+
<nav className="hidden items-center gap-1 md:flex">
|
|
39
|
+
{NAV_LINKS.map((link) => {
|
|
40
|
+
const isActive =
|
|
41
|
+
link.href === "https://kalikayi.com"
|
|
42
|
+
? pathname === "/"
|
|
43
|
+
: pathname.startsWith(new URL(link.href).pathname);
|
|
44
|
+
return (
|
|
45
|
+
<a
|
|
46
|
+
key={link.href}
|
|
47
|
+
href={link.href}
|
|
48
|
+
className={cn(
|
|
49
|
+
"relative px-3 py-2 text-sm font-medium transition-colors",
|
|
50
|
+
isActive
|
|
51
|
+
? "text-primary"
|
|
52
|
+
: "text-muted-foreground hover:text-foreground"
|
|
53
|
+
)}
|
|
54
|
+
>
|
|
55
|
+
{link.label}
|
|
56
|
+
{isActive && (
|
|
57
|
+
<span className="absolute inset-x-3 -bottom-[13px] h-0.5 rounded-full bg-primary" />
|
|
58
|
+
)}
|
|
59
|
+
</a>
|
|
60
|
+
);
|
|
61
|
+
})}
|
|
62
|
+
</nav>
|
|
63
|
+
|
|
64
|
+
{/* Right side controls */}
|
|
65
|
+
<div className="flex items-center gap-2">
|
|
66
|
+
<div className="hidden items-center gap-2 md:flex">
|
|
67
|
+
<AccentPicker />
|
|
68
|
+
<ThemeToggle />
|
|
69
|
+
</div>
|
|
70
|
+
<MobileNav subHeaderConfig={subHeaderConfig} />
|
|
71
|
+
</div>
|
|
72
|
+
</div>
|
|
73
|
+
</header>
|
|
74
|
+
);
|
|
75
|
+
}
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useState } from "react";
|
|
4
|
+
import Link from "next/link";
|
|
5
|
+
import { usePathname } from "next/navigation";
|
|
6
|
+
import { Menu, X } from "lucide-react";
|
|
7
|
+
import { AccentPicker } from "../theme/accent-picker";
|
|
8
|
+
import { ThemeToggle } from "../theme/theme-toggle";
|
|
9
|
+
import { cn } from "../../lib/utils";
|
|
10
|
+
import type { SubHeaderConfig } from "../../types";
|
|
11
|
+
|
|
12
|
+
const HUB_NAV_LINKS = [
|
|
13
|
+
{ href: "https://kalikayi.com", label: "Home" },
|
|
14
|
+
{ href: "https://kalikayi.com/tools", label: "Tools" },
|
|
15
|
+
{ href: "https://kalikayi.com/blog", label: "Blog" },
|
|
16
|
+
{ href: "https://kalikayi.com/about", label: "About" },
|
|
17
|
+
{ href: "https://kalikayi.com/contact", label: "Contact" },
|
|
18
|
+
];
|
|
19
|
+
|
|
20
|
+
interface MobileNavProps {
|
|
21
|
+
subHeaderConfig?: SubHeaderConfig;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function MobileNav({ subHeaderConfig }: MobileNavProps) {
|
|
25
|
+
const [open, setOpen] = useState(false);
|
|
26
|
+
const pathname = usePathname();
|
|
27
|
+
|
|
28
|
+
return (
|
|
29
|
+
<>
|
|
30
|
+
<button
|
|
31
|
+
className="inline-flex h-9 w-9 items-center justify-center rounded-md text-muted-foreground transition-colors hover:bg-accent hover:text-foreground md:hidden"
|
|
32
|
+
onClick={() => setOpen(true)}
|
|
33
|
+
aria-label="Open menu"
|
|
34
|
+
>
|
|
35
|
+
<Menu className="h-5 w-5" />
|
|
36
|
+
</button>
|
|
37
|
+
|
|
38
|
+
{/* Overlay */}
|
|
39
|
+
{open && (
|
|
40
|
+
<div className="fixed inset-0 z-50 md:hidden">
|
|
41
|
+
{/* Backdrop */}
|
|
42
|
+
<div
|
|
43
|
+
className="fixed inset-0 bg-background/80 backdrop-blur-sm"
|
|
44
|
+
onClick={() => setOpen(false)}
|
|
45
|
+
/>
|
|
46
|
+
|
|
47
|
+
{/* Sheet */}
|
|
48
|
+
<div className="fixed inset-y-0 right-0 w-[280px] border-l border-border/30 bg-background p-0 shadow-lg">
|
|
49
|
+
{/* Header */}
|
|
50
|
+
<div className="flex items-center justify-between border-b border-border/30 px-6 py-4">
|
|
51
|
+
<span className="font-bold text-primary">Kalikayi</span>
|
|
52
|
+
<button
|
|
53
|
+
className="inline-flex h-8 w-8 items-center justify-center rounded-md text-muted-foreground hover:bg-accent hover:text-foreground"
|
|
54
|
+
onClick={() => setOpen(false)}
|
|
55
|
+
aria-label="Close menu"
|
|
56
|
+
>
|
|
57
|
+
<X className="h-4 w-4" />
|
|
58
|
+
</button>
|
|
59
|
+
</div>
|
|
60
|
+
|
|
61
|
+
{/* Hub nav */}
|
|
62
|
+
<nav className="flex flex-col gap-1 p-4">
|
|
63
|
+
<p className="mb-1 px-4 text-xs font-semibold uppercase tracking-wider text-muted-foreground/60">
|
|
64
|
+
Kalikayi
|
|
65
|
+
</p>
|
|
66
|
+
{HUB_NAV_LINKS.map((link) => (
|
|
67
|
+
<a
|
|
68
|
+
key={link.href}
|
|
69
|
+
href={link.href}
|
|
70
|
+
onClick={() => setOpen(false)}
|
|
71
|
+
className={cn(
|
|
72
|
+
"rounded-lg px-4 py-2.5 text-sm font-medium transition-colors",
|
|
73
|
+
"text-muted-foreground hover:bg-accent hover:text-foreground"
|
|
74
|
+
)}
|
|
75
|
+
>
|
|
76
|
+
{link.label}
|
|
77
|
+
</a>
|
|
78
|
+
))}
|
|
79
|
+
</nav>
|
|
80
|
+
|
|
81
|
+
{/* Tool nav */}
|
|
82
|
+
{subHeaderConfig && (
|
|
83
|
+
<nav className="flex flex-col gap-1 border-t border-border/30 p-4">
|
|
84
|
+
<p className="mb-1 px-4 text-xs font-semibold uppercase tracking-wider text-muted-foreground/60">
|
|
85
|
+
{subHeaderConfig.name}
|
|
86
|
+
</p>
|
|
87
|
+
|
|
88
|
+
{subHeaderConfig.dropdowns?.map((dropdown) => (
|
|
89
|
+
<div key={dropdown.label}>
|
|
90
|
+
<p className="px-4 py-1.5 text-xs font-medium text-primary">
|
|
91
|
+
{dropdown.label}
|
|
92
|
+
</p>
|
|
93
|
+
{dropdown.items.map((item) => (
|
|
94
|
+
<Link
|
|
95
|
+
key={item.href}
|
|
96
|
+
href={item.href}
|
|
97
|
+
onClick={() => setOpen(false)}
|
|
98
|
+
className="block rounded-lg px-6 py-2 text-sm text-muted-foreground hover:bg-accent hover:text-foreground"
|
|
99
|
+
>
|
|
100
|
+
{item.label}
|
|
101
|
+
</Link>
|
|
102
|
+
))}
|
|
103
|
+
</div>
|
|
104
|
+
))}
|
|
105
|
+
|
|
106
|
+
{subHeaderConfig.navLinks?.map((link) => {
|
|
107
|
+
const isActive = pathname === link.href || pathname.startsWith(link.href + "/");
|
|
108
|
+
return (
|
|
109
|
+
<Link
|
|
110
|
+
key={link.href}
|
|
111
|
+
href={link.href}
|
|
112
|
+
onClick={() => setOpen(false)}
|
|
113
|
+
className={cn(
|
|
114
|
+
"rounded-lg px-4 py-2.5 text-sm font-medium transition-colors",
|
|
115
|
+
isActive
|
|
116
|
+
? "bg-primary/10 text-primary"
|
|
117
|
+
: "text-muted-foreground hover:bg-accent hover:text-foreground"
|
|
118
|
+
)}
|
|
119
|
+
>
|
|
120
|
+
{link.label}
|
|
121
|
+
</Link>
|
|
122
|
+
);
|
|
123
|
+
})}
|
|
124
|
+
</nav>
|
|
125
|
+
)}
|
|
126
|
+
|
|
127
|
+
{/* Controls */}
|
|
128
|
+
<div className="mt-auto border-t border-border/30 px-6 py-4">
|
|
129
|
+
<div className="flex items-center justify-between">
|
|
130
|
+
<AccentPicker />
|
|
131
|
+
<ThemeToggle />
|
|
132
|
+
</div>
|
|
133
|
+
</div>
|
|
134
|
+
</div>
|
|
135
|
+
</div>
|
|
136
|
+
)}
|
|
137
|
+
</>
|
|
138
|
+
);
|
|
139
|
+
}
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useState, useRef, useEffect } from "react";
|
|
4
|
+
import Link from "next/link";
|
|
5
|
+
import { usePathname } from "next/navigation";
|
|
6
|
+
import { ChevronDown } from "lucide-react";
|
|
7
|
+
import { cn } from "../../lib/utils";
|
|
8
|
+
import type { SubHeaderConfig } from "../../types";
|
|
9
|
+
|
|
10
|
+
interface SubHeaderProps {
|
|
11
|
+
config: SubHeaderConfig;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function SubHeader({ config }: SubHeaderProps) {
|
|
15
|
+
const pathname = usePathname();
|
|
16
|
+
const [openDropdown, setOpenDropdown] = useState<string | null>(null);
|
|
17
|
+
const dropdownRef = useRef<HTMLDivElement>(null);
|
|
18
|
+
|
|
19
|
+
useEffect(() => {
|
|
20
|
+
function handleClickOutside(e: MouseEvent) {
|
|
21
|
+
if (dropdownRef.current && !dropdownRef.current.contains(e.target as Node)) {
|
|
22
|
+
setOpenDropdown(null);
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
document.addEventListener("mousedown", handleClickOutside);
|
|
26
|
+
return () => document.removeEventListener("mousedown", handleClickOutside);
|
|
27
|
+
}, []);
|
|
28
|
+
|
|
29
|
+
return (
|
|
30
|
+
<div className="sticky top-14 z-40 w-full border-b border-border/20 bg-card/50 backdrop-blur-lg">
|
|
31
|
+
<div className="mx-auto flex h-12 max-w-7xl items-center gap-6 px-6" ref={dropdownRef}>
|
|
32
|
+
{/* Tool logo + name */}
|
|
33
|
+
<Link
|
|
34
|
+
href={config.href}
|
|
35
|
+
className="flex items-center gap-2 text-sm font-bold text-foreground"
|
|
36
|
+
>
|
|
37
|
+
{config.logo}
|
|
38
|
+
<span>{config.name}</span>
|
|
39
|
+
</Link>
|
|
40
|
+
|
|
41
|
+
{/* Desktop nav */}
|
|
42
|
+
<nav className="hidden items-center gap-1 md:flex">
|
|
43
|
+
{/* Dropdowns */}
|
|
44
|
+
{config.dropdowns?.map((dropdown) => (
|
|
45
|
+
<div key={dropdown.label} className="relative">
|
|
46
|
+
<button
|
|
47
|
+
className={cn(
|
|
48
|
+
"flex items-center gap-1 rounded-md px-3 py-1.5 text-sm font-medium transition-colors",
|
|
49
|
+
openDropdown === dropdown.label
|
|
50
|
+
? "text-primary"
|
|
51
|
+
: "text-muted-foreground hover:text-foreground"
|
|
52
|
+
)}
|
|
53
|
+
onClick={() =>
|
|
54
|
+
setOpenDropdown(
|
|
55
|
+
openDropdown === dropdown.label ? null : dropdown.label
|
|
56
|
+
)
|
|
57
|
+
}
|
|
58
|
+
>
|
|
59
|
+
{dropdown.label}
|
|
60
|
+
<ChevronDown
|
|
61
|
+
className={cn(
|
|
62
|
+
"h-3.5 w-3.5 transition-transform",
|
|
63
|
+
openDropdown === dropdown.label && "rotate-180"
|
|
64
|
+
)}
|
|
65
|
+
/>
|
|
66
|
+
</button>
|
|
67
|
+
{openDropdown === dropdown.label && (
|
|
68
|
+
<div className="absolute left-0 top-full z-50 mt-1 w-64 rounded-lg border border-border/50 bg-popover p-2 shadow-lg">
|
|
69
|
+
{dropdown.items.map((item) => (
|
|
70
|
+
<Link
|
|
71
|
+
key={item.href}
|
|
72
|
+
href={item.href}
|
|
73
|
+
onClick={() => setOpenDropdown(null)}
|
|
74
|
+
className="block rounded-md px-3 py-2 text-sm text-muted-foreground hover:bg-accent hover:text-foreground"
|
|
75
|
+
>
|
|
76
|
+
<span className="font-medium text-foreground">
|
|
77
|
+
{item.label}
|
|
78
|
+
</span>
|
|
79
|
+
{item.description && (
|
|
80
|
+
<span className="mt-0.5 block text-xs text-muted-foreground">
|
|
81
|
+
{item.description}
|
|
82
|
+
</span>
|
|
83
|
+
)}
|
|
84
|
+
</Link>
|
|
85
|
+
))}
|
|
86
|
+
</div>
|
|
87
|
+
)}
|
|
88
|
+
</div>
|
|
89
|
+
))}
|
|
90
|
+
|
|
91
|
+
{/* Nav links */}
|
|
92
|
+
{config.navLinks?.map((link) => {
|
|
93
|
+
const isActive = pathname === link.href || pathname.startsWith(link.href + "/");
|
|
94
|
+
return (
|
|
95
|
+
<Link
|
|
96
|
+
key={link.href}
|
|
97
|
+
href={link.href}
|
|
98
|
+
className={cn(
|
|
99
|
+
"rounded-md px-3 py-1.5 text-sm font-medium transition-colors",
|
|
100
|
+
isActive
|
|
101
|
+
? "text-primary"
|
|
102
|
+
: "text-muted-foreground hover:text-foreground"
|
|
103
|
+
)}
|
|
104
|
+
>
|
|
105
|
+
{link.label}
|
|
106
|
+
</Link>
|
|
107
|
+
);
|
|
108
|
+
})}
|
|
109
|
+
</nav>
|
|
110
|
+
</div>
|
|
111
|
+
</div>
|
|
112
|
+
);
|
|
113
|
+
}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import type { FAQ, BreadcrumbItem } from "../../types";
|
|
2
|
+
|
|
3
|
+
interface JsonLdProps {
|
|
4
|
+
data: Record<string, unknown>;
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
export function JsonLd({ data }: JsonLdProps) {
|
|
8
|
+
return (
|
|
9
|
+
<script
|
|
10
|
+
type="application/ld+json"
|
|
11
|
+
dangerouslySetInnerHTML={{ __html: JSON.stringify(data) }}
|
|
12
|
+
/>
|
|
13
|
+
);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function WebAppJsonLd({
|
|
17
|
+
name,
|
|
18
|
+
url,
|
|
19
|
+
description,
|
|
20
|
+
}: {
|
|
21
|
+
name: string;
|
|
22
|
+
url: string;
|
|
23
|
+
description: string;
|
|
24
|
+
}) {
|
|
25
|
+
return (
|
|
26
|
+
<JsonLd
|
|
27
|
+
data={{
|
|
28
|
+
"@context": "https://schema.org",
|
|
29
|
+
"@type": "WebApplication",
|
|
30
|
+
name,
|
|
31
|
+
url,
|
|
32
|
+
description,
|
|
33
|
+
applicationCategory: "UtilityApplication",
|
|
34
|
+
operatingSystem: "All",
|
|
35
|
+
offers: {
|
|
36
|
+
"@type": "Offer",
|
|
37
|
+
price: "0",
|
|
38
|
+
priceCurrency: "USD",
|
|
39
|
+
},
|
|
40
|
+
}}
|
|
41
|
+
/>
|
|
42
|
+
);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export function FAQJsonLd({ faqs }: { faqs: FAQ[] }) {
|
|
46
|
+
if (faqs.length === 0) return null;
|
|
47
|
+
|
|
48
|
+
return (
|
|
49
|
+
<JsonLd
|
|
50
|
+
data={{
|
|
51
|
+
"@context": "https://schema.org",
|
|
52
|
+
"@type": "FAQPage",
|
|
53
|
+
mainEntity: faqs.map((faq) => ({
|
|
54
|
+
"@type": "Question",
|
|
55
|
+
name: faq.question,
|
|
56
|
+
acceptedAnswer: {
|
|
57
|
+
"@type": "Answer",
|
|
58
|
+
text: faq.answer,
|
|
59
|
+
},
|
|
60
|
+
})),
|
|
61
|
+
}}
|
|
62
|
+
/>
|
|
63
|
+
);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export function BreadcrumbJsonLd({ items }: { items: BreadcrumbItem[] }) {
|
|
67
|
+
return (
|
|
68
|
+
<JsonLd
|
|
69
|
+
data={{
|
|
70
|
+
"@context": "https://schema.org",
|
|
71
|
+
"@type": "BreadcrumbList",
|
|
72
|
+
itemListElement: items.map((item, i) => ({
|
|
73
|
+
"@type": "ListItem",
|
|
74
|
+
position: i + 1,
|
|
75
|
+
name: item.name,
|
|
76
|
+
item: item.url,
|
|
77
|
+
})),
|
|
78
|
+
}}
|
|
79
|
+
/>
|
|
80
|
+
);
|
|
81
|
+
}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useEffect, useState } from "react";
|
|
4
|
+
import { cn } from "../../lib/utils";
|
|
5
|
+
|
|
6
|
+
const ACCENTS = [
|
|
7
|
+
{ name: "teal", color: "oklch(0.55 0.15 165)" },
|
|
8
|
+
{ name: "blue", color: "oklch(0.55 0.20 260)" },
|
|
9
|
+
{ name: "purple", color: "oklch(0.55 0.22 290)" },
|
|
10
|
+
{ name: "orange", color: "oklch(0.65 0.18 55)" },
|
|
11
|
+
{ name: "rose", color: "oklch(0.55 0.20 10)" },
|
|
12
|
+
] as const;
|
|
13
|
+
|
|
14
|
+
type AccentName = (typeof ACCENTS)[number]["name"];
|
|
15
|
+
|
|
16
|
+
function getStoredAccent(): AccentName {
|
|
17
|
+
if (typeof window === "undefined") return "teal";
|
|
18
|
+
return (localStorage.getItem("accent") as AccentName) || "teal";
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function applyAccent(accent: AccentName) {
|
|
22
|
+
const root = document.documentElement;
|
|
23
|
+
if (accent === "teal") {
|
|
24
|
+
root.removeAttribute("data-accent");
|
|
25
|
+
} else {
|
|
26
|
+
root.setAttribute("data-accent", accent);
|
|
27
|
+
}
|
|
28
|
+
localStorage.setItem("accent", accent);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function AccentPicker() {
|
|
32
|
+
const [active, setActive] = useState<AccentName>("teal");
|
|
33
|
+
|
|
34
|
+
useEffect(() => {
|
|
35
|
+
const stored = getStoredAccent();
|
|
36
|
+
setActive(stored);
|
|
37
|
+
applyAccent(stored);
|
|
38
|
+
}, []);
|
|
39
|
+
|
|
40
|
+
return (
|
|
41
|
+
<div className="flex items-center gap-1.5" role="radiogroup" aria-label="Accent color">
|
|
42
|
+
{ACCENTS.map(({ name, color }) => (
|
|
43
|
+
<button
|
|
44
|
+
key={name}
|
|
45
|
+
role="radio"
|
|
46
|
+
aria-checked={active === name}
|
|
47
|
+
aria-label={`${name} accent`}
|
|
48
|
+
className={cn(
|
|
49
|
+
"h-5 w-5 rounded-full border-2 transition-transform duration-200 hover:scale-110",
|
|
50
|
+
active === name
|
|
51
|
+
? "border-foreground scale-110"
|
|
52
|
+
: "border-transparent"
|
|
53
|
+
)}
|
|
54
|
+
style={{ backgroundColor: color }}
|
|
55
|
+
onClick={() => {
|
|
56
|
+
setActive(name);
|
|
57
|
+
applyAccent(name);
|
|
58
|
+
}}
|
|
59
|
+
/>
|
|
60
|
+
))}
|
|
61
|
+
</div>
|
|
62
|
+
);
|
|
63
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { ThemeProvider as NextThemesProvider } from "next-themes";
|
|
4
|
+
import type { ReactNode } from "react";
|
|
5
|
+
|
|
6
|
+
export function ThemeProvider({ children }: { children: ReactNode }) {
|
|
7
|
+
return (
|
|
8
|
+
<NextThemesProvider
|
|
9
|
+
attribute="class"
|
|
10
|
+
defaultTheme="system"
|
|
11
|
+
enableSystem
|
|
12
|
+
disableTransitionOnChange={false}
|
|
13
|
+
>
|
|
14
|
+
{children}
|
|
15
|
+
</NextThemesProvider>
|
|
16
|
+
);
|
|
17
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useTheme } from "next-themes";
|
|
4
|
+
import { useEffect, useState } from "react";
|
|
5
|
+
import { Moon, Sun } from "lucide-react";
|
|
6
|
+
|
|
7
|
+
export function ThemeToggle() {
|
|
8
|
+
const { resolvedTheme, setTheme } = useTheme();
|
|
9
|
+
const [mounted, setMounted] = useState(false);
|
|
10
|
+
|
|
11
|
+
useEffect(() => setMounted(true), []);
|
|
12
|
+
|
|
13
|
+
if (!mounted) {
|
|
14
|
+
return (
|
|
15
|
+
<button
|
|
16
|
+
className="inline-flex h-9 w-9 items-center justify-center rounded-md text-muted-foreground opacity-50"
|
|
17
|
+
disabled
|
|
18
|
+
aria-label="Toggle theme"
|
|
19
|
+
>
|
|
20
|
+
<Sun className="h-[18px] w-[18px]" />
|
|
21
|
+
</button>
|
|
22
|
+
);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const isDark = resolvedTheme === "dark";
|
|
26
|
+
|
|
27
|
+
return (
|
|
28
|
+
<button
|
|
29
|
+
className="inline-flex h-9 w-9 items-center justify-center rounded-md text-muted-foreground transition-colors hover:bg-accent hover:text-foreground"
|
|
30
|
+
onClick={() => setTheme(isDark ? "light" : "dark")}
|
|
31
|
+
aria-label={isDark ? "Switch to light mode" : "Switch to dark mode"}
|
|
32
|
+
>
|
|
33
|
+
{isDark ? (
|
|
34
|
+
<Sun className="h-[18px] w-[18px]" />
|
|
35
|
+
) : (
|
|
36
|
+
<Moon className="h-[18px] w-[18px]" />
|
|
37
|
+
)}
|
|
38
|
+
</button>
|
|
39
|
+
);
|
|
40
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useState, useCallback } from "react";
|
|
4
|
+
|
|
5
|
+
export function useCopyToClipboard(resetDelay = 2000) {
|
|
6
|
+
const [copied, setCopied] = useState(false);
|
|
7
|
+
|
|
8
|
+
const copy = useCallback(
|
|
9
|
+
async (text: string) => {
|
|
10
|
+
try {
|
|
11
|
+
await navigator.clipboard.writeText(text);
|
|
12
|
+
setCopied(true);
|
|
13
|
+
setTimeout(() => setCopied(false), resetDelay);
|
|
14
|
+
return true;
|
|
15
|
+
} catch {
|
|
16
|
+
return false;
|
|
17
|
+
}
|
|
18
|
+
},
|
|
19
|
+
[resetDelay]
|
|
20
|
+
);
|
|
21
|
+
|
|
22
|
+
return { copy, copied };
|
|
23
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
// Types
|
|
2
|
+
export type {
|
|
3
|
+
FAQ,
|
|
4
|
+
BreadcrumbItem,
|
|
5
|
+
KalikayiTool,
|
|
6
|
+
NavLink,
|
|
7
|
+
NavDropdown,
|
|
8
|
+
SubHeaderConfig,
|
|
9
|
+
} from "./types";
|
|
10
|
+
|
|
11
|
+
// Lib
|
|
12
|
+
export { cn } from "./lib/utils";
|
|
13
|
+
export {
|
|
14
|
+
kalikayiTools,
|
|
15
|
+
getLiveTools,
|
|
16
|
+
getRandomTools,
|
|
17
|
+
} from "./lib/kalikayi-tools";
|
|
18
|
+
|
|
19
|
+
// SEO
|
|
20
|
+
export {
|
|
21
|
+
JsonLd,
|
|
22
|
+
WebAppJsonLd,
|
|
23
|
+
FAQJsonLd,
|
|
24
|
+
BreadcrumbJsonLd,
|
|
25
|
+
} from "./components/seo/json-ld";
|
|
26
|
+
|
|
27
|
+
// Ads
|
|
28
|
+
export { AdBanner } from "./components/ads/ad-banner";
|
|
29
|
+
export { ToolPromoBanner } from "./components/ads/tool-promo-banner";
|
|
30
|
+
|
|
31
|
+
// Theme
|
|
32
|
+
export { ThemeProvider } from "./components/theme/theme-provider";
|
|
33
|
+
export { AccentPicker } from "./components/theme/accent-picker";
|
|
34
|
+
export { ThemeToggle } from "./components/theme/theme-toggle";
|
|
35
|
+
|
|
36
|
+
// Layout
|
|
37
|
+
export { Header } from "./components/layout/header";
|
|
38
|
+
export { SubHeader } from "./components/layout/sub-header";
|
|
39
|
+
export { MobileNav } from "./components/layout/mobile-nav";
|
|
40
|
+
export { Footer } from "./components/layout/footer";
|
|
41
|
+
|
|
42
|
+
// Hooks
|
|
43
|
+
export { useCopyToClipboard } from "./hooks/use-copy-to-clipboard";
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import type { KalikayiTool } from "../types";
|
|
2
|
+
|
|
3
|
+
export const kalikayiTools: KalikayiTool[] = [
|
|
4
|
+
{
|
|
5
|
+
name: "FreeResumePick",
|
|
6
|
+
tagline: "Free resume builder & ATS scanner",
|
|
7
|
+
href: "https://kalikayi.com/tools/free-resume-pick",
|
|
8
|
+
icon: "FileText",
|
|
9
|
+
isLive: true,
|
|
10
|
+
},
|
|
11
|
+
{
|
|
12
|
+
name: "FreeTypingBoard",
|
|
13
|
+
tagline: "Free typing tutor with 30+ lessons",
|
|
14
|
+
href: "https://kalikayi.com/tools/free-typing-board",
|
|
15
|
+
icon: "Keyboard",
|
|
16
|
+
isLive: true,
|
|
17
|
+
},
|
|
18
|
+
{
|
|
19
|
+
name: "FreeCalcKit",
|
|
20
|
+
tagline: "50+ free calculators & converters",
|
|
21
|
+
href: "https://freecalckit.com",
|
|
22
|
+
icon: "Calculator",
|
|
23
|
+
isLive: true,
|
|
24
|
+
},
|
|
25
|
+
];
|
|
26
|
+
|
|
27
|
+
export function getLiveTools(): KalikayiTool[] {
|
|
28
|
+
return kalikayiTools.filter((tool) => tool.isLive);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function getRandomTools(exclude?: string, count = 3): KalikayiTool[] {
|
|
32
|
+
const live = getLiveTools().filter((t) => t.name !== exclude);
|
|
33
|
+
const shuffled = [...live].sort(() => Math.random() - 0.5);
|
|
34
|
+
return shuffled.slice(0, count);
|
|
35
|
+
}
|
package/src/lib/utils.ts
ADDED
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
/* ========================================
|
|
2
|
+
KALIKAYI BASE THEME — OKLCH Color System
|
|
3
|
+
5 accents (Teal, Blue, Purple, Orange, Rose) x 2 modes (Light/Dark)
|
|
4
|
+
======================================== */
|
|
5
|
+
|
|
6
|
+
/* ========================================
|
|
7
|
+
BASE LIGHT THEME (Teal accent — default)
|
|
8
|
+
======================================== */
|
|
9
|
+
:root {
|
|
10
|
+
--radius: 0.625rem;
|
|
11
|
+
--background: oklch(0.985 0.002 250);
|
|
12
|
+
--foreground: oklch(0.13 0.03 265);
|
|
13
|
+
--card: oklch(0.97 0.003 250);
|
|
14
|
+
--card-foreground: oklch(0.13 0.03 265);
|
|
15
|
+
--popover: oklch(0.985 0.002 250);
|
|
16
|
+
--popover-foreground: oklch(0.13 0.03 265);
|
|
17
|
+
--primary: oklch(0.55 0.15 165);
|
|
18
|
+
--primary-foreground: oklch(0.98 0.005 165);
|
|
19
|
+
--secondary: oklch(0.95 0.005 250);
|
|
20
|
+
--secondary-foreground: oklch(0.2 0.03 265);
|
|
21
|
+
--muted: oklch(0.95 0.005 250);
|
|
22
|
+
--muted-foreground: oklch(0.45 0.03 260);
|
|
23
|
+
--accent: oklch(0.93 0.01 250);
|
|
24
|
+
--accent-foreground: oklch(0.2 0.03 265);
|
|
25
|
+
--destructive: oklch(0.577 0.245 27.325);
|
|
26
|
+
--border: oklch(0.9 0.01 255);
|
|
27
|
+
--input: oklch(0.9 0.01 255);
|
|
28
|
+
--ring: oklch(0.55 0.15 165);
|
|
29
|
+
--glow: oklch(0.55 0.15 165);
|
|
30
|
+
--glow-muted: oklch(0.55 0.15 165 / 15%);
|
|
31
|
+
--chart-1: oklch(0.646 0.222 41.116);
|
|
32
|
+
--chart-2: oklch(0.6 0.118 184.704);
|
|
33
|
+
--chart-3: oklch(0.398 0.07 227.392);
|
|
34
|
+
--chart-4: oklch(0.828 0.189 84.429);
|
|
35
|
+
--chart-5: oklch(0.769 0.188 70.08);
|
|
36
|
+
--sidebar: oklch(0.984 0.003 247.858);
|
|
37
|
+
--sidebar-foreground: oklch(0.129 0.042 264.695);
|
|
38
|
+
--sidebar-primary: oklch(0.208 0.042 265.755);
|
|
39
|
+
--sidebar-primary-foreground: oklch(0.984 0.003 247.858);
|
|
40
|
+
--sidebar-accent: oklch(0.968 0.007 247.896);
|
|
41
|
+
--sidebar-accent-foreground: oklch(0.208 0.042 265.755);
|
|
42
|
+
--sidebar-border: oklch(0.929 0.013 255.508);
|
|
43
|
+
--sidebar-ring: oklch(0.704 0.04 256.788);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/* ========================================
|
|
47
|
+
BASE DARK THEME (Teal accent — default)
|
|
48
|
+
======================================== */
|
|
49
|
+
.dark {
|
|
50
|
+
--background: oklch(0.07 0.01 260);
|
|
51
|
+
--foreground: oklch(0.96 0.005 250);
|
|
52
|
+
--card: oklch(0.12 0.015 260);
|
|
53
|
+
--card-foreground: oklch(0.96 0.005 250);
|
|
54
|
+
--popover: oklch(0.12 0.015 260);
|
|
55
|
+
--popover-foreground: oklch(0.96 0.005 250);
|
|
56
|
+
--primary: oklch(0.8 0.17 165);
|
|
57
|
+
--primary-foreground: oklch(0.07 0.01 260);
|
|
58
|
+
--secondary: oklch(0.17 0.02 260);
|
|
59
|
+
--secondary-foreground: oklch(0.96 0.005 250);
|
|
60
|
+
--muted: oklch(0.17 0.02 260);
|
|
61
|
+
--muted-foreground: oklch(0.73 0.015 255);
|
|
62
|
+
--accent: oklch(0.17 0.02 260);
|
|
63
|
+
--accent-foreground: oklch(0.96 0.005 250);
|
|
64
|
+
--destructive: oklch(0.704 0.191 22.216);
|
|
65
|
+
--border: oklch(1 0 0 / 14%);
|
|
66
|
+
--input: oklch(1 0 0 / 18%);
|
|
67
|
+
--ring: oklch(0.8 0.17 165);
|
|
68
|
+
--glow: oklch(0.8 0.17 165);
|
|
69
|
+
--glow-muted: oklch(0.8 0.17 165 / 18%);
|
|
70
|
+
--chart-1: oklch(0.488 0.243 264.376);
|
|
71
|
+
--chart-2: oklch(0.696 0.17 162.48);
|
|
72
|
+
--chart-3: oklch(0.769 0.188 70.08);
|
|
73
|
+
--chart-4: oklch(0.627 0.265 303.9);
|
|
74
|
+
--chart-5: oklch(0.645 0.246 16.439);
|
|
75
|
+
--sidebar: oklch(0.1 0.015 260);
|
|
76
|
+
--sidebar-foreground: oklch(0.93 0.008 250);
|
|
77
|
+
--sidebar-primary: oklch(0.72 0.19 165);
|
|
78
|
+
--sidebar-primary-foreground: oklch(0.93 0.008 250);
|
|
79
|
+
--sidebar-accent: oklch(0.15 0.02 260);
|
|
80
|
+
--sidebar-accent-foreground: oklch(0.93 0.008 250);
|
|
81
|
+
--sidebar-border: oklch(1 0 0 / 8%);
|
|
82
|
+
--sidebar-ring: oklch(0.72 0.19 165);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/* ========================================
|
|
86
|
+
ACCENT: BLUE
|
|
87
|
+
======================================== */
|
|
88
|
+
[data-accent="blue"] {
|
|
89
|
+
--primary: oklch(0.55 0.20 260);
|
|
90
|
+
--primary-foreground: oklch(0.98 0.005 260);
|
|
91
|
+
--ring: oklch(0.55 0.20 260);
|
|
92
|
+
--glow: oklch(0.55 0.20 260);
|
|
93
|
+
--glow-muted: oklch(0.55 0.20 260 / 15%);
|
|
94
|
+
}
|
|
95
|
+
[data-accent="blue"].dark,
|
|
96
|
+
.dark[data-accent="blue"] {
|
|
97
|
+
--primary: oklch(0.70 0.18 260);
|
|
98
|
+
--primary-foreground: oklch(0.07 0.01 260);
|
|
99
|
+
--ring: oklch(0.70 0.18 260);
|
|
100
|
+
--glow: oklch(0.70 0.18 260);
|
|
101
|
+
--glow-muted: oklch(0.70 0.18 260 / 18%);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/* ========================================
|
|
105
|
+
ACCENT: PURPLE
|
|
106
|
+
======================================== */
|
|
107
|
+
[data-accent="purple"] {
|
|
108
|
+
--primary: oklch(0.55 0.22 290);
|
|
109
|
+
--primary-foreground: oklch(0.98 0.005 290);
|
|
110
|
+
--ring: oklch(0.55 0.22 290);
|
|
111
|
+
--glow: oklch(0.55 0.22 290);
|
|
112
|
+
--glow-muted: oklch(0.55 0.22 290 / 15%);
|
|
113
|
+
}
|
|
114
|
+
[data-accent="purple"].dark,
|
|
115
|
+
.dark[data-accent="purple"] {
|
|
116
|
+
--primary: oklch(0.70 0.20 290);
|
|
117
|
+
--primary-foreground: oklch(0.07 0.01 260);
|
|
118
|
+
--ring: oklch(0.70 0.20 290);
|
|
119
|
+
--glow: oklch(0.70 0.20 290);
|
|
120
|
+
--glow-muted: oklch(0.70 0.20 290 / 18%);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/* ========================================
|
|
124
|
+
ACCENT: ORANGE
|
|
125
|
+
======================================== */
|
|
126
|
+
[data-accent="orange"] {
|
|
127
|
+
--primary: oklch(0.65 0.18 55);
|
|
128
|
+
--primary-foreground: oklch(0.98 0.005 55);
|
|
129
|
+
--ring: oklch(0.65 0.18 55);
|
|
130
|
+
--glow: oklch(0.65 0.18 55);
|
|
131
|
+
--glow-muted: oklch(0.65 0.18 55 / 15%);
|
|
132
|
+
}
|
|
133
|
+
[data-accent="orange"].dark,
|
|
134
|
+
.dark[data-accent="orange"] {
|
|
135
|
+
--primary: oklch(0.78 0.16 55);
|
|
136
|
+
--primary-foreground: oklch(0.07 0.01 260);
|
|
137
|
+
--ring: oklch(0.78 0.16 55);
|
|
138
|
+
--glow: oklch(0.78 0.16 55);
|
|
139
|
+
--glow-muted: oklch(0.78 0.16 55 / 18%);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/* ========================================
|
|
143
|
+
ACCENT: ROSE
|
|
144
|
+
======================================== */
|
|
145
|
+
[data-accent="rose"] {
|
|
146
|
+
--primary: oklch(0.55 0.20 10);
|
|
147
|
+
--primary-foreground: oklch(0.98 0.005 10);
|
|
148
|
+
--ring: oklch(0.55 0.20 10);
|
|
149
|
+
--glow: oklch(0.55 0.20 10);
|
|
150
|
+
--glow-muted: oklch(0.55 0.20 10 / 15%);
|
|
151
|
+
}
|
|
152
|
+
[data-accent="rose"].dark,
|
|
153
|
+
.dark[data-accent="rose"] {
|
|
154
|
+
--primary: oklch(0.72 0.18 10);
|
|
155
|
+
--primary-foreground: oklch(0.07 0.01 260);
|
|
156
|
+
--ring: oklch(0.72 0.18 10);
|
|
157
|
+
--glow: oklch(0.72 0.18 10);
|
|
158
|
+
--glow-muted: oklch(0.72 0.18 10 / 18%);
|
|
159
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import type { ReactNode } from "react";
|
|
2
|
+
|
|
3
|
+
export interface FAQ {
|
|
4
|
+
question: string;
|
|
5
|
+
answer: string;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export interface BreadcrumbItem {
|
|
9
|
+
name: string;
|
|
10
|
+
url: string;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export interface KalikayiTool {
|
|
14
|
+
name: string;
|
|
15
|
+
tagline: string;
|
|
16
|
+
href: string;
|
|
17
|
+
icon: string;
|
|
18
|
+
isLive: boolean;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export interface NavLink {
|
|
22
|
+
href: string;
|
|
23
|
+
label: string;
|
|
24
|
+
description?: string;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export interface NavDropdown {
|
|
28
|
+
label: string;
|
|
29
|
+
items: NavLink[];
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export interface SubHeaderConfig {
|
|
33
|
+
name: string;
|
|
34
|
+
href: string;
|
|
35
|
+
logo: ReactNode;
|
|
36
|
+
navLinks?: NavLink[];
|
|
37
|
+
dropdowns?: NavDropdown[];
|
|
38
|
+
}
|