@ugurdemirel/landcraft 0.1.1 → 0.1.3
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 +104 -4
- package/dist/index.js +1411 -1124
- package/dist/index.js.map +1 -1
- package/dist/styles.css +3721 -5900
- package/package.json +5 -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 +25 -12
- 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";
|
|
@@ -49,7 +49,11 @@ export interface MegaMenuProps extends Omit<HTMLAttributes<HTMLElement>, "title"
|
|
|
49
49
|
/** Rendered at the right side of the bar (desktop) and inside the mobile panel. */
|
|
50
50
|
actions?: ReactNode;
|
|
51
51
|
cta?: ReactNode;
|
|
52
|
+
/** Optional language switcher rendered on the right (desktop) and beside the menu button (mobile). */
|
|
53
|
+
languageSwitcher?: ReactNode;
|
|
52
54
|
sticky?: boolean;
|
|
55
|
+
/** 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. */
|
|
56
|
+
LinkComponent?: ElementType;
|
|
53
57
|
}
|
|
54
58
|
|
|
55
59
|
const triggerStyles = {
|
|
@@ -70,11 +74,14 @@ export const MegaMenu = ({
|
|
|
70
74
|
items,
|
|
71
75
|
actions,
|
|
72
76
|
cta,
|
|
77
|
+
languageSwitcher,
|
|
73
78
|
sticky = true,
|
|
79
|
+
LinkComponent,
|
|
74
80
|
onKeyDown,
|
|
75
81
|
...props
|
|
76
82
|
}: MegaMenuProps) => {
|
|
77
83
|
const ids = useId();
|
|
84
|
+
const Link = LinkComponent ?? "a";
|
|
78
85
|
const [openIndex, setOpenIndex] = useState<number | null>(null);
|
|
79
86
|
const [mobileOpen, setMobileOpen] = useState(false);
|
|
80
87
|
const [mobileExpanded, setMobileExpanded] = useState<number | null>(null);
|
|
@@ -166,7 +173,7 @@ export const MegaMenu = ({
|
|
|
166
173
|
<ul className={cn(column.title && "mt-3", "space-y-0.5")}>
|
|
167
174
|
{column.links.map((link) => (
|
|
168
175
|
<li key={link.href}>
|
|
169
|
-
<
|
|
176
|
+
<Link
|
|
170
177
|
href={link.href}
|
|
171
178
|
onClick={closeAll}
|
|
172
179
|
className={cn(
|
|
@@ -189,7 +196,7 @@ export const MegaMenu = ({
|
|
|
189
196
|
{link.description}
|
|
190
197
|
</span>
|
|
191
198
|
) : null}
|
|
192
|
-
</
|
|
199
|
+
</Link>
|
|
193
200
|
</li>
|
|
194
201
|
))}
|
|
195
202
|
</ul>
|
|
@@ -206,7 +213,7 @@ export const MegaMenu = ({
|
|
|
206
213
|
: undefined;
|
|
207
214
|
|
|
208
215
|
return (
|
|
209
|
-
<
|
|
216
|
+
<Link
|
|
210
217
|
href={featured.href}
|
|
211
218
|
onClick={closeAll}
|
|
212
219
|
className={cn(
|
|
@@ -237,7 +244,7 @@ export const MegaMenu = ({
|
|
|
237
244
|
aria-hidden
|
|
238
245
|
className="pointer-events-none absolute -right-8 -top-8 h-28 w-28 rounded-full opacity-20 blur-2xl bg-black/40"
|
|
239
246
|
/>
|
|
240
|
-
</
|
|
247
|
+
</Link>
|
|
241
248
|
);
|
|
242
249
|
};
|
|
243
250
|
|
|
@@ -255,9 +262,9 @@ export const MegaMenu = ({
|
|
|
255
262
|
{...props}
|
|
256
263
|
>
|
|
257
264
|
<nav className={bar} onMouseLeave={closePanels} aria-label="Mega menu">
|
|
258
|
-
<
|
|
265
|
+
<Link href={brandHref} className="shrink-0">
|
|
259
266
|
{brandNode}
|
|
260
|
-
</
|
|
267
|
+
</Link>
|
|
261
268
|
|
|
262
269
|
<ul className="hidden items-center gap-1 lg:flex">
|
|
263
270
|
{items.map((item, i) => {
|
|
@@ -304,14 +311,14 @@ export const MegaMenu = ({
|
|
|
304
311
|
/>
|
|
305
312
|
</button>
|
|
306
313
|
) : (
|
|
307
|
-
<
|
|
314
|
+
<Link href={item.href ?? "#"} className={triggerClasses}>
|
|
308
315
|
{item.label}
|
|
309
316
|
{item.badge ? (
|
|
310
317
|
<span className="rounded-full bg-primary-soft px-1.5 py-0.5 text-[10px] font-semibold tracking-wide text-primary">
|
|
311
318
|
{item.badge}
|
|
312
319
|
</span>
|
|
313
320
|
) : null}
|
|
314
|
-
</
|
|
321
|
+
</Link>
|
|
315
322
|
)}
|
|
316
323
|
</li>
|
|
317
324
|
);
|
|
@@ -319,10 +326,15 @@ export const MegaMenu = ({
|
|
|
319
326
|
</ul>
|
|
320
327
|
|
|
321
328
|
<div className="hidden items-center gap-3 lg:flex">
|
|
329
|
+
{languageSwitcher}
|
|
322
330
|
{actions}
|
|
323
331
|
{cta}
|
|
324
332
|
</div>
|
|
325
333
|
|
|
334
|
+
{languageSwitcher ? (
|
|
335
|
+
<div className="flex items-center lg:hidden">{languageSwitcher}</div>
|
|
336
|
+
) : null}
|
|
337
|
+
|
|
326
338
|
<button
|
|
327
339
|
type="button"
|
|
328
340
|
aria-label={mobileOpen ? "Close menu" : "Open menu"}
|
|
@@ -350,7 +362,8 @@ export const MegaMenu = ({
|
|
|
350
362
|
aria-label={`${item.label} menu`}
|
|
351
363
|
onMouseEnter={() => setOpenIndex(i)}
|
|
352
364
|
className={cn(
|
|
353
|
-
"absolute
|
|
365
|
+
"absolute top-full z-50 pt-2 transition-all duration-150",
|
|
366
|
+
variant === "floating" ? "inset-x-0 mx-auto w-full max-w-5xl" : "inset-x-0",
|
|
354
367
|
open
|
|
355
368
|
? "visible translate-y-0 opacity-100"
|
|
356
369
|
: "pointer-events-none invisible -translate-y-1 opacity-0",
|
|
@@ -439,7 +452,7 @@ export const MegaMenu = ({
|
|
|
439
452
|
</li>
|
|
440
453
|
) : (
|
|
441
454
|
<li key={item.label} className="px-5">
|
|
442
|
-
<
|
|
455
|
+
<Link
|
|
443
456
|
href={item.href ?? "#"}
|
|
444
457
|
onClick={() => setMobileOpen(false)}
|
|
445
458
|
className={cn(
|
|
@@ -450,7 +463,7 @@ export const MegaMenu = ({
|
|
|
450
463
|
)}
|
|
451
464
|
>
|
|
452
465
|
{item.label}
|
|
453
|
-
</
|
|
466
|
+
</Link>
|
|
454
467
|
</li>
|
|
455
468
|
),
|
|
456
469
|
)}
|
|
@@ -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
|
-
}
|