@ugurdemirel/landcraft 0.1.0 → 0.1.2
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 +103 -19
- package/dist/index.cjs +7 -1
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.ts +102 -4
- package/dist/index.js +1336 -1053
- package/dist/index.js.map +1 -1
- package/dist/styles.css +3721 -5900
- package/package.json +9 -7
- package/src/components/Blog.tsx +18 -10
- package/src/components/Button.tsx +27 -8
- package/src/components/Footer.tsx +53 -13
- package/src/components/LanguageSwitcher.tsx +223 -0
- package/src/components/MegaMenu.tsx +15 -11
- package/src/components/Modal.tsx +124 -0
- package/src/components/Navbar.tsx +20 -8
- package/src/components/Slot.tsx +78 -0
- package/src/index.ts +9 -0
- package/src/styles/index.css +2 -71
- package/src/styles/theme.css +242 -0
- package/src/utils/iso2Flag.ts +26 -0
- package/tailwind.preset.cjs +0 -121
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { useId, useState, type HTMLAttributes, type ReactNode } from "react";
|
|
1
|
+
import { useId, useState, type ElementType, type HTMLAttributes, type ReactNode } from "react";
|
|
2
2
|
import { cn } from "../utils/cn";
|
|
3
3
|
import { getContrastText } from "../utils/contrast";
|
|
4
4
|
import { ChevronDown, Menu, X } from "../icons";
|
|
@@ -50,6 +50,8 @@ export interface MegaMenuProps extends Omit<HTMLAttributes<HTMLElement>, "title"
|
|
|
50
50
|
actions?: ReactNode;
|
|
51
51
|
cta?: ReactNode;
|
|
52
52
|
sticky?: boolean;
|
|
53
|
+
/** Component used to render brand, trigger and panel links. Defaults to `<a>`. Pass your router's `<Link>` (Next.js, Remix, React Router…) for framework-aware navigation. */
|
|
54
|
+
LinkComponent?: ElementType;
|
|
53
55
|
}
|
|
54
56
|
|
|
55
57
|
const triggerStyles = {
|
|
@@ -71,10 +73,12 @@ export const MegaMenu = ({
|
|
|
71
73
|
actions,
|
|
72
74
|
cta,
|
|
73
75
|
sticky = true,
|
|
76
|
+
LinkComponent,
|
|
74
77
|
onKeyDown,
|
|
75
78
|
...props
|
|
76
79
|
}: MegaMenuProps) => {
|
|
77
80
|
const ids = useId();
|
|
81
|
+
const Link = LinkComponent ?? "a";
|
|
78
82
|
const [openIndex, setOpenIndex] = useState<number | null>(null);
|
|
79
83
|
const [mobileOpen, setMobileOpen] = useState(false);
|
|
80
84
|
const [mobileExpanded, setMobileExpanded] = useState<number | null>(null);
|
|
@@ -166,7 +170,7 @@ export const MegaMenu = ({
|
|
|
166
170
|
<ul className={cn(column.title && "mt-3", "space-y-0.5")}>
|
|
167
171
|
{column.links.map((link) => (
|
|
168
172
|
<li key={link.href}>
|
|
169
|
-
<
|
|
173
|
+
<Link
|
|
170
174
|
href={link.href}
|
|
171
175
|
onClick={closeAll}
|
|
172
176
|
className={cn(
|
|
@@ -189,7 +193,7 @@ export const MegaMenu = ({
|
|
|
189
193
|
{link.description}
|
|
190
194
|
</span>
|
|
191
195
|
) : null}
|
|
192
|
-
</
|
|
196
|
+
</Link>
|
|
193
197
|
</li>
|
|
194
198
|
))}
|
|
195
199
|
</ul>
|
|
@@ -206,7 +210,7 @@ export const MegaMenu = ({
|
|
|
206
210
|
: undefined;
|
|
207
211
|
|
|
208
212
|
return (
|
|
209
|
-
<
|
|
213
|
+
<Link
|
|
210
214
|
href={featured.href}
|
|
211
215
|
onClick={closeAll}
|
|
212
216
|
className={cn(
|
|
@@ -237,7 +241,7 @@ export const MegaMenu = ({
|
|
|
237
241
|
aria-hidden
|
|
238
242
|
className="pointer-events-none absolute -right-8 -top-8 h-28 w-28 rounded-full opacity-20 blur-2xl bg-black/40"
|
|
239
243
|
/>
|
|
240
|
-
</
|
|
244
|
+
</Link>
|
|
241
245
|
);
|
|
242
246
|
};
|
|
243
247
|
|
|
@@ -255,9 +259,9 @@ export const MegaMenu = ({
|
|
|
255
259
|
{...props}
|
|
256
260
|
>
|
|
257
261
|
<nav className={bar} onMouseLeave={closePanels} aria-label="Mega menu">
|
|
258
|
-
<
|
|
262
|
+
<Link href={brandHref} className="shrink-0">
|
|
259
263
|
{brandNode}
|
|
260
|
-
</
|
|
264
|
+
</Link>
|
|
261
265
|
|
|
262
266
|
<ul className="hidden items-center gap-1 lg:flex">
|
|
263
267
|
{items.map((item, i) => {
|
|
@@ -304,14 +308,14 @@ export const MegaMenu = ({
|
|
|
304
308
|
/>
|
|
305
309
|
</button>
|
|
306
310
|
) : (
|
|
307
|
-
<
|
|
311
|
+
<Link href={item.href ?? "#"} className={triggerClasses}>
|
|
308
312
|
{item.label}
|
|
309
313
|
{item.badge ? (
|
|
310
314
|
<span className="rounded-full bg-primary-soft px-1.5 py-0.5 text-[10px] font-semibold tracking-wide text-primary">
|
|
311
315
|
{item.badge}
|
|
312
316
|
</span>
|
|
313
317
|
) : null}
|
|
314
|
-
</
|
|
318
|
+
</Link>
|
|
315
319
|
)}
|
|
316
320
|
</li>
|
|
317
321
|
);
|
|
@@ -439,7 +443,7 @@ export const MegaMenu = ({
|
|
|
439
443
|
</li>
|
|
440
444
|
) : (
|
|
441
445
|
<li key={item.label} className="px-5">
|
|
442
|
-
<
|
|
446
|
+
<Link
|
|
443
447
|
href={item.href ?? "#"}
|
|
444
448
|
onClick={() => setMobileOpen(false)}
|
|
445
449
|
className={cn(
|
|
@@ -450,7 +454,7 @@ export const MegaMenu = ({
|
|
|
450
454
|
)}
|
|
451
455
|
>
|
|
452
456
|
{item.label}
|
|
453
|
-
</
|
|
457
|
+
</Link>
|
|
454
458
|
</li>
|
|
455
459
|
),
|
|
456
460
|
)}
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
import { forwardRef, useEffect, useId, useRef, type HTMLAttributes, type ReactNode } from "react";
|
|
2
|
+
import { cn } from "../utils/cn";
|
|
3
|
+
import { X } from "../icons";
|
|
4
|
+
|
|
5
|
+
export interface ModalProps extends Omit<HTMLAttributes<HTMLDivElement>, "title"> {
|
|
6
|
+
open: boolean;
|
|
7
|
+
onClose: () => void;
|
|
8
|
+
/** Dialog heading (required for a11y; auto-linked via `aria-labelledby`). */
|
|
9
|
+
title?: ReactNode;
|
|
10
|
+
description?: ReactNode;
|
|
11
|
+
/** Rendered below the content, e.g. actions. */
|
|
12
|
+
footer?: ReactNode;
|
|
13
|
+
/** Content width. Defaults to `md`. */
|
|
14
|
+
size?: "sm" | "md" | "lg";
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const sizes: Record<NonNullable<ModalProps["size"]>, string> = {
|
|
18
|
+
sm: "max-w-sm",
|
|
19
|
+
md: "max-w-lg",
|
|
20
|
+
lg: "max-w-2xl",
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Accessible dialog built on the HTML Popover API.
|
|
25
|
+
*
|
|
26
|
+
* `popover="auto"` renders in the top layer and gives light-dismiss (outside
|
|
27
|
+
* click + `Escape`), automatic focus movement and restoration for free, so no
|
|
28
|
+
* portal, focus trap or key handlers are needed. State stays controlled via
|
|
29
|
+
* `open` / `onClose`.
|
|
30
|
+
*/
|
|
31
|
+
export const Modal = forwardRef<HTMLDivElement, ModalProps>(
|
|
32
|
+
({ className, open, onClose, title, description, footer, size = "md", children, ...props }, ref) => {
|
|
33
|
+
const titleId = useId();
|
|
34
|
+
const descriptionId = useId();
|
|
35
|
+
const internalRef = useRef<HTMLDivElement | null>(null);
|
|
36
|
+
const onCloseRef = useRef(onClose);
|
|
37
|
+
|
|
38
|
+
useEffect(() => {
|
|
39
|
+
onCloseRef.current = onClose;
|
|
40
|
+
}, [onClose]);
|
|
41
|
+
|
|
42
|
+
useEffect(() => {
|
|
43
|
+
const el = internalRef.current;
|
|
44
|
+
if (!el) return;
|
|
45
|
+
if (!el.hasAttribute("popover")) el.setAttribute("popover", "auto");
|
|
46
|
+
}, []);
|
|
47
|
+
|
|
48
|
+
useEffect(() => {
|
|
49
|
+
const el = internalRef.current;
|
|
50
|
+
if (!el) return;
|
|
51
|
+
if (open) {
|
|
52
|
+
if (!el.matches(":popover-open")) el.showPopover();
|
|
53
|
+
document.body.style.overflow = "hidden";
|
|
54
|
+
} else {
|
|
55
|
+
if (el.matches(":popover-open")) el.hidePopover();
|
|
56
|
+
document.body.style.overflow = "";
|
|
57
|
+
}
|
|
58
|
+
}, [open]);
|
|
59
|
+
|
|
60
|
+
useEffect(() => {
|
|
61
|
+
const el = internalRef.current;
|
|
62
|
+
if (!el) return;
|
|
63
|
+
const onToggle = (event: Event) => {
|
|
64
|
+
if ((event as ToggleEvent).newState === "closed") onCloseRef.current?.();
|
|
65
|
+
};
|
|
66
|
+
el.addEventListener("toggle", onToggle);
|
|
67
|
+
return () => el.removeEventListener("toggle", onToggle);
|
|
68
|
+
}, []);
|
|
69
|
+
|
|
70
|
+
useEffect(
|
|
71
|
+
() => () => {
|
|
72
|
+
document.body.style.overflow = "";
|
|
73
|
+
},
|
|
74
|
+
[],
|
|
75
|
+
);
|
|
76
|
+
|
|
77
|
+
return (
|
|
78
|
+
<div
|
|
79
|
+
ref={(node) => {
|
|
80
|
+
internalRef.current = node;
|
|
81
|
+
if (typeof ref === "function") ref(node);
|
|
82
|
+
else if (ref) ref.current = node;
|
|
83
|
+
}}
|
|
84
|
+
role="dialog"
|
|
85
|
+
aria-modal="true"
|
|
86
|
+
aria-labelledby={title ? titleId : undefined}
|
|
87
|
+
aria-describedby={description ? descriptionId : undefined}
|
|
88
|
+
className={cn(
|
|
89
|
+
"modal-anim modal-backdrop fixed inset-0 z-[100] m-auto h-fit max-h-[calc(100vh-2rem)] w-fit max-w-[calc(100vw-2rem)] overflow-y-auto rounded-2xl border border-border bg-surface p-6 text-foreground shadow-overlay",
|
|
90
|
+
sizes[size],
|
|
91
|
+
className,
|
|
92
|
+
)}
|
|
93
|
+
{...props}
|
|
94
|
+
>
|
|
95
|
+
{title ? (
|
|
96
|
+
<h2
|
|
97
|
+
id={titleId}
|
|
98
|
+
className="pr-8 font-display text-lg font-semibold tracking-tight text-foreground"
|
|
99
|
+
>
|
|
100
|
+
{title}
|
|
101
|
+
</h2>
|
|
102
|
+
) : null}
|
|
103
|
+
{description ? (
|
|
104
|
+
<p id={descriptionId} className="mt-1.5 text-sm leading-6 text-muted-foreground">
|
|
105
|
+
{description}
|
|
106
|
+
</p>
|
|
107
|
+
) : null}
|
|
108
|
+
<button
|
|
109
|
+
type="button"
|
|
110
|
+
aria-label="Close dialog"
|
|
111
|
+
onClick={onClose}
|
|
112
|
+
className="absolute right-4 top-4 inline-flex h-8 w-8 cursor-pointer items-center justify-center rounded-md text-muted-foreground transition-colors duration-150 hover:bg-surface-strong hover:text-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
|
|
113
|
+
>
|
|
114
|
+
<X className="h-4 w-4" />
|
|
115
|
+
</button>
|
|
116
|
+
<div className={title || description ? "mt-4" : "pt-8"}>{children}</div>
|
|
117
|
+
{footer ? (
|
|
118
|
+
<div className="mt-6 flex flex-wrap items-center justify-end gap-3">{footer}</div>
|
|
119
|
+
) : null}
|
|
120
|
+
</div>
|
|
121
|
+
);
|
|
122
|
+
},
|
|
123
|
+
);
|
|
124
|
+
Modal.displayName = "Modal";
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { useState, type HTMLAttributes, type ReactNode } from "react";
|
|
1
|
+
import { useState, type ElementType, type HTMLAttributes, type ReactNode } from "react";
|
|
2
2
|
import { cn } from "../utils/cn";
|
|
3
3
|
import { Menu, X } from "../icons";
|
|
4
4
|
|
|
@@ -21,8 +21,12 @@ export interface NavbarProps extends Omit<HTMLAttributes<HTMLElement>, "title">
|
|
|
21
21
|
brandHref?: string;
|
|
22
22
|
links: NavLink[];
|
|
23
23
|
actions?: ReactNode;
|
|
24
|
-
sticky?: boolean;
|
|
25
24
|
cta?: ReactNode;
|
|
25
|
+
/** Optional language switcher rendered on the right (desktop) and beside the menu button (mobile). */
|
|
26
|
+
languageSwitcher?: ReactNode;
|
|
27
|
+
sticky?: boolean;
|
|
28
|
+
/** Component used to render the brand and nav links. Defaults to `<a>`. Pass your router's `<Link>` (Next.js, Remix, React Router…) for framework-aware navigation. */
|
|
29
|
+
LinkComponent?: ElementType;
|
|
26
30
|
}
|
|
27
31
|
|
|
28
32
|
const linkStyles = {
|
|
@@ -43,11 +47,14 @@ export const Navbar = ({
|
|
|
43
47
|
links,
|
|
44
48
|
actions,
|
|
45
49
|
cta,
|
|
50
|
+
languageSwitcher,
|
|
46
51
|
sticky = true,
|
|
52
|
+
LinkComponent,
|
|
47
53
|
...props
|
|
48
54
|
}: NavbarProps) => {
|
|
49
55
|
const [open, setOpen] = useState(false);
|
|
50
56
|
const inverse = variant === "inverse";
|
|
57
|
+
const Link = LinkComponent ?? "a";
|
|
51
58
|
|
|
52
59
|
const shell = cn(
|
|
53
60
|
"w-full transition-colors duration-200",
|
|
@@ -99,14 +106,14 @@ export const Navbar = ({
|
|
|
99
106
|
return (
|
|
100
107
|
<header className={cn(shell, variant === "floating" && "px-3 sm:px-6", className)} {...props}>
|
|
101
108
|
<nav className={bar} aria-label="Main navigation">
|
|
102
|
-
<
|
|
109
|
+
<Link href={brandHref} className="shrink-0">
|
|
103
110
|
{brandNode}
|
|
104
|
-
</
|
|
111
|
+
</Link>
|
|
105
112
|
|
|
106
113
|
<ul className="hidden items-center gap-7 md:flex">
|
|
107
114
|
{links.map((link) => (
|
|
108
115
|
<li key={link.href}>
|
|
109
|
-
<
|
|
116
|
+
<Link
|
|
110
117
|
href={link.href}
|
|
111
118
|
className={cn(
|
|
112
119
|
"text-sm font-medium transition-colors duration-150",
|
|
@@ -114,16 +121,21 @@ export const Navbar = ({
|
|
|
114
121
|
)}
|
|
115
122
|
>
|
|
116
123
|
{link.label}
|
|
117
|
-
</
|
|
124
|
+
</Link>
|
|
118
125
|
</li>
|
|
119
126
|
))}
|
|
120
127
|
</ul>
|
|
121
128
|
|
|
122
129
|
<div className="hidden items-center gap-3 md:flex">
|
|
130
|
+
{languageSwitcher}
|
|
123
131
|
{actions}
|
|
124
132
|
{cta}
|
|
125
133
|
</div>
|
|
126
134
|
|
|
135
|
+
{languageSwitcher ? (
|
|
136
|
+
<div className="flex items-center md:hidden">{languageSwitcher}</div>
|
|
137
|
+
) : null}
|
|
138
|
+
|
|
127
139
|
<button
|
|
128
140
|
type="button"
|
|
129
141
|
aria-label={open ? "Close menu" : "Open menu"}
|
|
@@ -155,7 +167,7 @@ export const Navbar = ({
|
|
|
155
167
|
<ul className={cn("flex flex-col py-4", variant !== "floating" && "px-5")}>
|
|
156
168
|
{links.map((link) => (
|
|
157
169
|
<li key={link.href}>
|
|
158
|
-
<
|
|
170
|
+
<Link
|
|
159
171
|
href={link.href}
|
|
160
172
|
onClick={() => setOpen(false)}
|
|
161
173
|
className={cn(
|
|
@@ -166,7 +178,7 @@ export const Navbar = ({
|
|
|
166
178
|
)}
|
|
167
179
|
>
|
|
168
180
|
{link.label}
|
|
169
|
-
</
|
|
181
|
+
</Link>
|
|
170
182
|
</li>
|
|
171
183
|
))}
|
|
172
184
|
{cta ? (
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import {
|
|
2
|
+
Children,
|
|
3
|
+
cloneElement,
|
|
4
|
+
forwardRef,
|
|
5
|
+
isValidElement,
|
|
6
|
+
type HTMLAttributes,
|
|
7
|
+
type ReactElement,
|
|
8
|
+
type ReactNode,
|
|
9
|
+
type Ref,
|
|
10
|
+
} from "react";
|
|
11
|
+
import { cn } from "../utils/cn";
|
|
12
|
+
|
|
13
|
+
export interface SlotProps extends Omit<HTMLAttributes<HTMLElement>, "ref"> {
|
|
14
|
+
children?: ReactNode;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
type AnyProps = Record<string, any>;
|
|
18
|
+
|
|
19
|
+
function mergeProps(slotProps: AnyProps, childProps: AnyProps): AnyProps {
|
|
20
|
+
const overrideProps: AnyProps = { ...childProps };
|
|
21
|
+
|
|
22
|
+
for (const propName in childProps) {
|
|
23
|
+
const slotPropValue = slotProps[propName];
|
|
24
|
+
const childPropValue = childProps[propName];
|
|
25
|
+
const isHandler = /^on[A-Z]/.test(propName);
|
|
26
|
+
|
|
27
|
+
if (isHandler) {
|
|
28
|
+
if (slotPropValue && childPropValue) {
|
|
29
|
+
overrideProps[propName] = (...args: any[]) => {
|
|
30
|
+
childPropValue(...args);
|
|
31
|
+
slotPropValue(...args);
|
|
32
|
+
};
|
|
33
|
+
} else if (slotPropValue) {
|
|
34
|
+
overrideProps[propName] = slotPropValue;
|
|
35
|
+
}
|
|
36
|
+
} else if (propName === "style") {
|
|
37
|
+
overrideProps[propName] = { ...slotPropValue, ...childPropValue };
|
|
38
|
+
} else if (propName === "className") {
|
|
39
|
+
overrideProps[propName] = cn(slotPropValue, childPropValue);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
return { ...slotProps, ...overrideProps };
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function useComposedRefs(...refs: Array<Ref<unknown> | undefined>) {
|
|
47
|
+
return (node: unknown) => {
|
|
48
|
+
for (const ref of refs) {
|
|
49
|
+
if (typeof ref === "function") {
|
|
50
|
+
ref(node);
|
|
51
|
+
} else if (ref != null) {
|
|
52
|
+
(ref as { current: unknown }).current = node;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Merges its props onto a single child element instead of rendering its own
|
|
60
|
+
* DOM node. Used with the `asChild` prop so consumers can render a framework
|
|
61
|
+
* router `<Link>` (Next.js, Remix, React Router…) with a component's styling.
|
|
62
|
+
*/
|
|
63
|
+
export const Slot = forwardRef<HTMLElement, SlotProps>((props, forwardedRef) => {
|
|
64
|
+
const { children, ...slotProps } = props;
|
|
65
|
+
|
|
66
|
+
if (!isValidElement(children)) {
|
|
67
|
+
return null;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const child = Children.only(children) as ReactElement & { ref?: Ref<unknown> };
|
|
71
|
+
const composedRef = useComposedRefs(forwardedRef, child.ref);
|
|
72
|
+
|
|
73
|
+
return cloneElement(child, {
|
|
74
|
+
...mergeProps(slotProps as AnyProps, child.props as AnyProps),
|
|
75
|
+
ref: composedRef,
|
|
76
|
+
});
|
|
77
|
+
});
|
|
78
|
+
Slot.displayName = "Slot";
|
package/src/index.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
export { Button, type ButtonProps, type ButtonVariant, type ButtonSize } from "./components/Button";
|
|
2
|
+
export { Slot, type SlotProps } from "./components/Slot";
|
|
2
3
|
export { Badge, type BadgeProps, type BadgeVariant, type BadgeSize } from "./components/Badge";
|
|
3
4
|
export {
|
|
4
5
|
Card,
|
|
@@ -48,6 +49,13 @@ export {
|
|
|
48
49
|
export { CTA, type CTAProps } from "./components/CTA";
|
|
49
50
|
export { FAQ, type FAQProps, type FaqItem } from "./components/FAQ";
|
|
50
51
|
export { Footer, type FooterProps, type FooterColumn, type FooterLink } from "./components/Footer";
|
|
52
|
+
export {
|
|
53
|
+
LanguageSwitcher,
|
|
54
|
+
type LanguageSwitcherProps,
|
|
55
|
+
type LanguageSwitcherOption,
|
|
56
|
+
type LanguageOption,
|
|
57
|
+
} from "./components/LanguageSwitcher";
|
|
58
|
+
export { Modal, type ModalProps } from "./components/Modal";
|
|
51
59
|
export { Newsletter, type NewsletterProps } from "./components/Newsletter";
|
|
52
60
|
export { Prose, ProseLead, type ProseProps, type ProseSize } from "./components/Prose";
|
|
53
61
|
export {
|
|
@@ -70,6 +78,7 @@ export {
|
|
|
70
78
|
export { cn } from "./utils/cn";
|
|
71
79
|
export { getContrastText, type ColorInput } from "./utils/contrast";
|
|
72
80
|
export { useTokenForeground } from "./utils/useTokenForeground";
|
|
81
|
+
export { iso2ToFlagEmoji } from "./utils/iso2Flag";
|
|
73
82
|
|
|
74
83
|
export {
|
|
75
84
|
ArrowRight,
|
package/src/styles/index.css
CHANGED
|
@@ -1,53 +1,8 @@
|
|
|
1
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
|
-
@
|
|
4
|
-
@tailwind components;
|
|
5
|
-
@tailwind utilities;
|
|
2
|
+
@import "tailwindcss";
|
|
3
|
+
@import "./theme.css";
|
|
6
4
|
|
|
7
5
|
@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
6
|
html {
|
|
52
7
|
-webkit-tap-highlight-color: transparent;
|
|
53
8
|
scroll-behavior: smooth;
|
|
@@ -61,27 +16,3 @@
|
|
|
61
16
|
background-color: rgb(var(--color-primary) / 0.18);
|
|
62
17
|
}
|
|
63
18
|
}
|
|
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
|
-
}
|