@ugurdemirel/landcraft 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/src/index.ts ADDED
@@ -0,0 +1,111 @@
1
+ export { Button, type ButtonProps, type ButtonVariant, type ButtonSize } from "./components/Button";
2
+ export { Badge, type BadgeProps, type BadgeVariant, type BadgeSize } from "./components/Badge";
3
+ export {
4
+ Card,
5
+ CardHeader,
6
+ CardTitle,
7
+ CardDescription,
8
+ CardContent,
9
+ CardFooter,
10
+ type CardProps,
11
+ type CardHeaderProps,
12
+ type CardVariant,
13
+ } from "./components/Card";
14
+ export {
15
+ Section,
16
+ SectionHeader,
17
+ type SectionProps,
18
+ type SectionHeaderProps,
19
+ type EyebrowStyle,
20
+ } from "./components/Section";
21
+ export { Hero, type HeroProps, type HeroMeta } from "./components/Hero";
22
+ export { Navbar, type NavbarProps, type NavLink } from "./components/Navbar";
23
+ export {
24
+ MegaMenu,
25
+ type MegaMenuProps,
26
+ type MegaMenuItem,
27
+ type MegaMenuColumn,
28
+ type MegaMenuLink,
29
+ type MegaMenuFeatured,
30
+ } from "./components/MegaMenu";
31
+ export { LogoCloud, type LogoCloudProps, type LogoItem } from "./components/LogoCloud";
32
+ export {
33
+ FeatureCard,
34
+ FeatureGrid,
35
+ type FeatureCardProps,
36
+ type FeatureGridProps,
37
+ type FeatureItem,
38
+ } from "./components/Features";
39
+ export { Stats, type StatsProps, type Stat } from "./components/Stats";
40
+ export { Pricing, type PricingProps, type Plan } from "./components/Pricing";
41
+ export {
42
+ Testimonials,
43
+ TestimonialCard,
44
+ type TestimonialsProps,
45
+ type TestimonialCardProps,
46
+ type Testimonial,
47
+ } from "./components/Testimonials";
48
+ export { CTA, type CTAProps } from "./components/CTA";
49
+ export { FAQ, type FAQProps, type FaqItem } from "./components/FAQ";
50
+ export { Footer, type FooterProps, type FooterColumn, type FooterLink } from "./components/Footer";
51
+ export { Newsletter, type NewsletterProps } from "./components/Newsletter";
52
+ export { Prose, ProseLead, type ProseProps, type ProseSize } from "./components/Prose";
53
+ export {
54
+ BlogCard,
55
+ BlogSection,
56
+ type BlogCardProps,
57
+ type BlogPost,
58
+ type BlogAuthor,
59
+ type BlogSectionProps,
60
+ } from "./components/Blog";
61
+ export {
62
+ Container,
63
+ Stack,
64
+ type ContainerProps,
65
+ type ContainerSize,
66
+ type StackProps,
67
+ type StackGap,
68
+ } from "./components/Container";
69
+
70
+ export { cn } from "./utils/cn";
71
+ export { getContrastText, type ColorInput } from "./utils/contrast";
72
+ export { useTokenForeground } from "./utils/useTokenForeground";
73
+
74
+ export {
75
+ ArrowRight,
76
+ ArrowUpRight,
77
+ ArrowDown,
78
+ ArrowUp,
79
+ Check,
80
+ Plus,
81
+ X,
82
+ Menu,
83
+ ChevronDown,
84
+ ChevronRight,
85
+ ChevronLeft,
86
+ Zap,
87
+ ShieldCheck,
88
+ Layers,
89
+ Smartphone,
90
+ Gauge,
91
+ BarChart,
92
+ Users,
93
+ Star,
94
+ Quote,
95
+ Globe,
96
+ Mail,
97
+ Lock,
98
+ Clock,
99
+ DollarSign,
100
+ Code,
101
+ Terminal,
102
+ Send,
103
+ Play,
104
+ ExternalLink,
105
+ Sliders,
106
+ Database,
107
+ Box,
108
+ Cpu,
109
+ Palette,
110
+ Rocket,
111
+ } from "./icons";
@@ -0,0 +1,87 @@
1
+ @import url("https://fonts.googleapis.com/css2?family=Space+Grotesk:wght@400;500;600;700&family=DM+Sans:opsz,wght@9..40,400;9..40,500;9..40,600&display=swap");
2
+
3
+ @tailwind base;
4
+ @tailwind components;
5
+ @tailwind utilities;
6
+
7
+ @layer base {
8
+ :root {
9
+ /* ── Brand ─────────────────────────────────────────────── */
10
+ --color-primary: 79 70 229; /* indigo-600 */
11
+ --color-primary-hover: 67 56 202; /* indigo-700 */
12
+ --color-primary-soft: 238 242 255; /* indigo-50 */
13
+ --color-on-primary: 255 255 255;
14
+
15
+ --color-secondary: 15 15 15; /* ink */
16
+ --color-secondary-hover: 38 38 38;
17
+ --color-on-secondary: 250 250 250;
18
+
19
+ --color-accent: 5 150 105; /* emerald-600 */
20
+ --color-accent-hover: 4 120 87;
21
+ --color-on-accent: 255 255 255;
22
+
23
+ /* ── Surfaces & text (warm paper, not pure white) ──────── */
24
+ --color-background: 250 250 250;
25
+ --color-foreground: 17 17 17;
26
+ --color-muted-foreground: 97 97 97;
27
+ --color-surface: 255 255 255;
28
+ --color-surface-strong: 244 244 242;
29
+ --color-border: 229 229 228;
30
+ --color-ring: 79 70 229;
31
+ --color-danger: 217 45 32;
32
+ --color-danger-soft: 255 241 242;
33
+
34
+ /* ── Type & shape ──────────────────────────────────────── */
35
+ --font-display: "Space Grotesk", ui-sans-serif, system-ui, -apple-system, "Segoe UI", sans-serif;
36
+ --font-sans: "DM Sans", ui-sans-serif, system-ui, -apple-system, "Segoe UI", sans-serif;
37
+
38
+ --radius-sm: 0.375rem;
39
+ --radius-md: 0.625rem;
40
+ --radius-lg: 0.875rem;
41
+ --radius-xl: 1.125rem;
42
+ --radius-2xl: 1.5rem;
43
+ --radius-3xl: 2rem;
44
+
45
+ /* ── Elevation ─────────────────────────────────────────── */
46
+ --shadow-soft: 0 1px 2px rgb(17 17 17 / 0.04), 0 2px 8px rgb(17 17 17 / 0.04);
47
+ --shadow-raised: 0 2px 4px rgb(17 17 17 / 0.05), 0 12px 32px rgb(17 17 17 / 0.08);
48
+ --shadow-overlay: 0 24px 64px -12px rgb(17 17 17 / 0.18);
49
+ }
50
+
51
+ html {
52
+ -webkit-tap-highlight-color: transparent;
53
+ scroll-behavior: smooth;
54
+ }
55
+
56
+ body {
57
+ @apply bg-background text-foreground font-sans antialiased;
58
+ }
59
+
60
+ ::selection {
61
+ background-color: rgb(var(--color-primary) / 0.18);
62
+ }
63
+ }
64
+
65
+ @layer utilities {
66
+ /* Subtle film grain for dark/statement surfaces */
67
+ .grain {
68
+ background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='160' height='160'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.85' numOctaves='3' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23n)' opacity='0.45'/%3E%3C/svg%3E");
69
+ }
70
+
71
+ .animate-marquee {
72
+ animation: marquee 38s linear infinite;
73
+ }
74
+
75
+ .mask-fade-x {
76
+ mask-image: linear-gradient(to right, transparent, black 12%, black 88%, transparent);
77
+ }
78
+ }
79
+
80
+ @keyframes marquee {
81
+ from {
82
+ transform: translateX(0);
83
+ }
84
+ to {
85
+ transform: translateX(-50%);
86
+ }
87
+ }
@@ -0,0 +1,6 @@
1
+ import { clsx, type ClassValue } from "clsx";
2
+ import { twMerge } from "tailwind-merge";
3
+
4
+ export function cn(...inputs: ClassValue[]) {
5
+ return twMerge(clsx(inputs));
6
+ }
@@ -0,0 +1,107 @@
1
+ /**
2
+ * Contrast utilities.
3
+ *
4
+ * The library ships paired semantic tokens (e.g. --color-primary /
5
+ * --color-on-primary) so explicit pairings stay safe. For the few places where
6
+ * a *dynamic* color paints a background (a `customColor` prop, a token-based
7
+ * highlight card...) we pick the text color that yields the **best WCAG
8
+ * contrast ratio** at runtime.
9
+ *
10
+ * Inputs may be hex colors, bare RGB channels ("21 128 61"), rgb() functions
11
+ * (`rgb(21 128 61 / 0.95)`) or CSS variable references
12
+ * (`rgb(var(--color-primary) / 0.85)`). Variable references are resolved
13
+ * against the live stylesheet before computing luminance, so a re-themed
14
+ * palette never produces a low-contrast combination.
15
+ */
16
+
17
+ export type ColorInput = string;
18
+
19
+ function readVariable(name: string): string {
20
+ if (typeof window === "undefined") return "";
21
+ return window
22
+ .getComputedStyle(document.documentElement)
23
+ .getPropertyValue(name)
24
+ .trim();
25
+ }
26
+
27
+ /** Resolve `var(--x)` references against the live stylesheet. */
28
+ function resolveVars(input: string): string {
29
+ let str = input;
30
+ const refs = str.match(/var\(--[\w-]+/g) ?? [];
31
+ for (const ref of refs) {
32
+ const name = ref.replace(/^var\(/, "");
33
+ const resolved = readVariable(name);
34
+ if (resolved) str = str.split(ref).join(resolved);
35
+ }
36
+ return str;
37
+ }
38
+
39
+ interface RGB {
40
+ r: number;
41
+ g: number;
42
+ b: number;
43
+ }
44
+
45
+ function channels(color: ColorInput): RGB | null {
46
+ let str = color.trim();
47
+
48
+ if (/^#([0-9a-f]{3}){1,2}$/i.test(str)) {
49
+ if (str.length === 4) {
50
+ str =
51
+ "#" +
52
+ str
53
+ .slice(1)
54
+ .split("")
55
+ .map((c) => c + c)
56
+ .join("");
57
+ }
58
+ return {
59
+ r: parseInt(str.slice(1, 3), 16),
60
+ g: parseInt(str.slice(3, 5), 16),
61
+ b: parseInt(str.slice(5, 7), 16),
62
+ };
63
+ }
64
+
65
+ const resolved = resolveVars(str);
66
+
67
+ // Normalize rgb()/rgba() separators ("r g b / a", "r, g, b") and bare channels.
68
+ const normalized = resolved.replace(/[()/;,]/g, " ");
69
+ const parts = normalized.match(/-?\d+(\.\d+)?/g)?.map(Number) ?? [];
70
+ if (parts.length < 3) return null;
71
+ // Alpha (4th component) is ignored: these colors paint opaque surfaces and
72
+ // mixing against an unknown backdrop can't be modelled reliably.
73
+ const [r, g, b] = parts;
74
+ return {
75
+ r: Math.max(0, Math.min(255, Math.round(r))),
76
+ g: Math.max(0, Math.min(255, Math.round(g))),
77
+ b: Math.max(0, Math.min(255, Math.round(b))),
78
+ };
79
+ }
80
+
81
+ function relativeLuminance(color: ColorInput): number {
82
+ const c = channels(color);
83
+ if (!c) return 0;
84
+ const lin = [c.r, c.g, c.b]
85
+ .map((v) => v / 255)
86
+ .map((v) => (v <= 0.03928 ? v / 12.92 : Math.pow((v + 0.055) / 1.055, 2.4)));
87
+ return 0.2126 * lin[0] + 0.7152 * lin[1] + 0.0722 * lin[2];
88
+ }
89
+
90
+ function contrastRatio(lumA: number, lumB: number): number {
91
+ const [hi, lo] = lumA > lumB ? [lumA, lumB] : [lumB, lumA];
92
+ return (hi + 0.05) / (lo + 0.05);
93
+ }
94
+
95
+ /**
96
+ * Returns the more readable of the two candidate text colors for a given
97
+ * background. Both candidates are evaluated with the WCAG contrast formula and
98
+ * the winning (highest ratio) color is returned — not a fixed luminance
99
+ * breakpoint — so mid-luminance backgrounds (amber, pastels, light violet…)
100
+ * always fall on the correct side.
101
+ */
102
+ export function getContrastText(bg: ColorInput, light = "#ffffff", dark = "#0f172a"): string {
103
+ const lumBg = relativeLuminance(bg);
104
+ const lumLight = relativeLuminance(light);
105
+ const lumDark = relativeLuminance(dark);
106
+ return contrastRatio(lumBg, lumLight) >= contrastRatio(lumBg, lumDark) ? light : dark;
107
+ }
@@ -0,0 +1,38 @@
1
+ import { useEffect, useState } from "react";
2
+ import { getContrastText } from "./contrast";
3
+
4
+ function readChannel(variable: string): string {
5
+ if (typeof window === "undefined") return "";
6
+ return window
7
+ .getComputedStyle(document.documentElement)
8
+ .getPropertyValue(variable)
9
+ .trim();
10
+ }
11
+
12
+ /**
13
+ * Reads a `--color-*` token from the live stylesheet and returns a text color
14
+ * that is always readable against it. Recomputes whenever the palette changes
15
+ * at runtime (e.g. the Storybook theme switcher or an app-level theme swap).
16
+ */
17
+ export function useTokenForeground(variable: string, fallback = "#ffffff"): string {
18
+ const [text, setText] = useState<string>(() => {
19
+ const value = readChannel(variable);
20
+ return value ? getContrastText(value) : fallback;
21
+ });
22
+
23
+ useEffect(() => {
24
+ const update = () => {
25
+ const value = readChannel(variable);
26
+ setText(value ? getContrastText(value) : fallback);
27
+ };
28
+ update();
29
+ const observer = new MutationObserver(update);
30
+ observer.observe(document.documentElement, {
31
+ attributes: true,
32
+ attributeFilter: ["style", "class"],
33
+ });
34
+ return () => observer.disconnect();
35
+ }, [variable, fallback]);
36
+
37
+ return text;
38
+ }
@@ -0,0 +1,121 @@
1
+ /**
2
+ * Tailwind preset for @ugurdemirel/landcraft.
3
+ *
4
+ * Every design decision (color, type, radius, shadow) is driven by CSS custom
5
+ * properties so consumers can re-skin the whole library from outside without
6
+ * touching a single component.
7
+ *
8
+ * Colors store space-separated RGB channels so Tailwind's `<alpha-value>`
9
+ * opacity syntax keeps working (e.g. `text-primary/60`).
10
+ */
11
+ /** @type {import('tailwindcss').Config} */
12
+ module.exports = {
13
+ theme: {
14
+ extend: {
15
+ colors: {
16
+ primary: "rgb(var(--color-primary) / <alpha-value>)",
17
+ "primary-hover": "rgb(var(--color-primary-hover) / <alpha-value>)",
18
+ "primary-soft": "rgb(var(--color-primary-soft) / <alpha-value>)",
19
+ "on-primary": "rgb(var(--color-on-primary) / <alpha-value>)",
20
+ secondary: "rgb(var(--color-secondary) / <alpha-value>)",
21
+ "secondary-hover": "rgb(var(--color-secondary-hover) / <alpha-value>)",
22
+ "on-secondary": "rgb(var(--color-on-secondary) / <alpha-value>)",
23
+ accent: "rgb(var(--color-accent) / <alpha-value>)",
24
+ "accent-hover": "rgb(var(--color-accent-hover) / <alpha-value>)",
25
+ "on-accent": "rgb(var(--color-on-accent) / <alpha-value>)",
26
+ background: "rgb(var(--color-background) / <alpha-value>)",
27
+ foreground: "rgb(var(--color-foreground) / <alpha-value>)",
28
+ "muted-foreground": "rgb(var(--color-muted-foreground) / <alpha-value>)",
29
+ surface: "rgb(var(--color-surface) / <alpha-value>)",
30
+ "surface-strong": "rgb(var(--color-surface-strong) / <alpha-value>)",
31
+ border: "rgb(var(--color-border) / <alpha-value>)",
32
+ ring: "rgb(var(--color-ring) / <alpha-value>)",
33
+ danger: "rgb(var(--color-danger) / <alpha-value>)",
34
+ "danger-soft": "rgb(var(--color-danger-soft) / <alpha-value>)",
35
+ },
36
+ fontFamily: {
37
+ sans: ["var(--font-sans)"],
38
+ display: ["var(--font-display)"],
39
+ },
40
+ borderRadius: {
41
+ sm: "var(--radius-sm)",
42
+ md: "var(--radius-md)",
43
+ lg: "var(--radius-lg)",
44
+ xl: "var(--radius-xl)",
45
+ "2xl": "var(--radius-2xl)",
46
+ "3xl": "var(--radius-3xl)",
47
+ full: "9999px",
48
+ },
49
+ boxShadow: {
50
+ soft: "var(--shadow-soft)",
51
+ raised: "var(--shadow-raised)",
52
+ overlay: "var(--shadow-overlay)",
53
+ },
54
+ borderColor: {
55
+ DEFAULT: "rgb(var(--color-border) / <alpha-value>)",
56
+ },
57
+ ringColor: {
58
+ DEFAULT: "rgb(var(--color-ring) / <alpha-value>)",
59
+ },
60
+ typography: {
61
+ DEFAULT: {
62
+ css: {
63
+ "--tw-prose-body": "rgb(var(--color-foreground))",
64
+ "--tw-prose-headings": "rgb(var(--color-foreground))",
65
+ "--tw-prose-lead": "rgb(var(--color-muted-foreground))",
66
+ "--tw-prose-links": "rgb(var(--color-primary))",
67
+ "--tw-prose-bold": "rgb(var(--color-foreground))",
68
+ "--tw-prose-counters": "rgb(var(--color-muted-foreground))",
69
+ "--tw-prose-bullets": "rgb(var(--color-muted-foreground))",
70
+ "--tw-prose-hr": "rgb(var(--color-border))",
71
+ "--tw-prose-quotes": "rgb(var(--color-foreground))",
72
+ "--tw-prose-quote-borders": "rgb(var(--color-primary))",
73
+ "--tw-prose-captions": "rgb(var(--color-muted-foreground))",
74
+ "--tw-prose-code": "rgb(var(--color-foreground))",
75
+ "--tw-prose-pre-code": "rgb(var(--color-on-secondary))",
76
+ "--tw-prose-pre-bg": "rgb(var(--color-secondary))",
77
+ "--tw-prose-th-borders": "rgb(var(--color-border))",
78
+ "--tw-prose-td-borders": "rgb(var(--color-border))",
79
+ "h1, h2, h3, h4, h5, h6": {
80
+ fontFamily: "var(--font-display)",
81
+ fontWeight: "600",
82
+ letterSpacing: "-0.02em",
83
+ },
84
+ a: {
85
+ textDecorationThickness: "1px",
86
+ textUnderlineOffset: "3px",
87
+ fontWeight: "500",
88
+ },
89
+ blockquote: {
90
+ fontStyle: "normal",
91
+ },
92
+ "figure figcaption": {
93
+ fontStyle: "normal",
94
+ },
95
+ "pre": {
96
+ border: "1px solid rgb(var(--color-border))",
97
+ borderRadius: "var(--radius-xl)",
98
+ backgroundColor: "rgb(var(--color-secondary))",
99
+ color: "rgb(var(--color-on-secondary))",
100
+ },
101
+ code: {
102
+ backgroundColor: "rgb(var(--color-surface-strong))",
103
+ borderRadius: "var(--radius-md)",
104
+ padding: "0.125rem 0.3125rem",
105
+ fontWeight: "500",
106
+ "&::before, &::after": {
107
+ content: "none",
108
+ },
109
+ },
110
+ "pre code": {
111
+ backgroundColor: "transparent",
112
+ padding: "0",
113
+ borderRadius: "0",
114
+ },
115
+ },
116
+ },
117
+ },
118
+ },
119
+ },
120
+ plugins: [require("@tailwindcss/typography")],
121
+ };