@ugurdemirel/landcraft 0.1.5 → 0.1.9

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.
Files changed (138) hide show
  1. package/package.json +4 -4
  2. package/src/components/Badge.test.tsx +0 -55
  3. package/src/components/Badge.tsx +0 -68
  4. package/src/components/Blog/Blog.test.tsx +0 -64
  5. package/src/components/Blog/BlogCard.tsx +0 -12
  6. package/src/components/Blog/BlogSection.tsx +0 -95
  7. package/src/components/Blog/index.ts +0 -8
  8. package/src/components/Blog/parts.tsx +0 -38
  9. package/src/components/Blog/types.ts +0 -42
  10. package/src/components/Blog/variants/Card.tsx +0 -62
  11. package/src/components/Blog/variants/Row.tsx +0 -41
  12. package/src/components/Button.test.tsx +0 -91
  13. package/src/components/Button.tsx +0 -113
  14. package/src/components/CTA/CTA.test.tsx +0 -53
  15. package/src/components/CTA/CTA.tsx +0 -12
  16. package/src/components/CTA/index.ts +0 -2
  17. package/src/components/CTA/parts.tsx +0 -26
  18. package/src/components/CTA/types.ts +0 -12
  19. package/src/components/CTA/variants/Inverse.tsx +0 -50
  20. package/src/components/CTA/variants/Panel.tsx +0 -60
  21. package/src/components/CTA/variants/Surface.tsx +0 -34
  22. package/src/components/Card.test.tsx +0 -49
  23. package/src/components/Card.tsx +0 -80
  24. package/src/components/Container.test.tsx +0 -52
  25. package/src/components/Container.tsx +0 -86
  26. package/src/components/FAQ/FAQ.test.tsx +0 -54
  27. package/src/components/FAQ/FAQ.tsx +0 -48
  28. package/src/components/FAQ/index.ts +0 -2
  29. package/src/components/FAQ/parts.tsx +0 -74
  30. package/src/components/FAQ/types.ts +0 -18
  31. package/src/components/FAQ/variants/Accordion.tsx +0 -46
  32. package/src/components/FAQ/variants/Cards.tsx +0 -40
  33. package/src/components/FAQ/variants/Split.tsx +0 -44
  34. package/src/components/Features/FeatureCard.tsx +0 -44
  35. package/src/components/Features/FeatureGrid.tsx +0 -14
  36. package/src/components/Features/Features.test.tsx +0 -49
  37. package/src/components/Features/index.ts +0 -3
  38. package/src/components/Features/parts.ts +0 -15
  39. package/src/components/Features/types.ts +0 -25
  40. package/src/components/Features/variants/Bento.tsx +0 -32
  41. package/src/components/Features/variants/Columns.tsx +0 -21
  42. package/src/components/Features/variants/EditorialRows.tsx +0 -38
  43. package/src/components/Footer/Footer.test.tsx +0 -49
  44. package/src/components/Footer/Footer.tsx +0 -11
  45. package/src/components/Footer/index.ts +0 -2
  46. package/src/components/Footer/parts.tsx +0 -101
  47. package/src/components/Footer/types.ts +0 -32
  48. package/src/components/Footer/variants/Classic.tsx +0 -59
  49. package/src/components/Footer/variants/Editorial.tsx +0 -47
  50. package/src/components/Footer/variants/Minimal.tsx +0 -33
  51. package/src/components/Hero/Hero.test.tsx +0 -71
  52. package/src/components/Hero/Hero.tsx +0 -12
  53. package/src/components/Hero/index.ts +0 -2
  54. package/src/components/Hero/parts.tsx +0 -29
  55. package/src/components/Hero/types.ts +0 -20
  56. package/src/components/Hero/variants/Centered.tsx +0 -54
  57. package/src/components/Hero/variants/Split.tsx +0 -53
  58. package/src/components/Hero/variants/Statement.tsx +0 -60
  59. package/src/components/LanguageSwitcher/LanguageSwitcher.test.tsx +0 -64
  60. package/src/components/LanguageSwitcher/LanguageSwitcher.tsx +0 -37
  61. package/src/components/LanguageSwitcher/index.ts +0 -6
  62. package/src/components/LanguageSwitcher/parts.tsx +0 -144
  63. package/src/components/LanguageSwitcher/types.ts +0 -40
  64. package/src/components/LanguageSwitcher/useLanguageSwitcher.ts +0 -23
  65. package/src/components/LanguageSwitcher/variants/Dropdown.tsx +0 -88
  66. package/src/components/LanguageSwitcher/variants/Modal.tsx +0 -56
  67. package/src/components/LogoCloud/LogoCloud.test.tsx +0 -35
  68. package/src/components/LogoCloud/LogoCloud.tsx +0 -14
  69. package/src/components/LogoCloud/index.ts +0 -2
  70. package/src/components/LogoCloud/parts.tsx +0 -32
  71. package/src/components/LogoCloud/types.ts +0 -16
  72. package/src/components/LogoCloud/variants/Marquee.tsx +0 -30
  73. package/src/components/LogoCloud/variants/Quiet.tsx +0 -26
  74. package/src/components/LogoCloud/variants/Strip.tsx +0 -25
  75. package/src/components/MegaMenu/MegaMenu.test.tsx +0 -89
  76. package/src/components/MegaMenu/MegaMenu.tsx +0 -454
  77. package/src/components/MegaMenu/index.ts +0 -9
  78. package/src/components/MegaMenu/types.ts +0 -56
  79. package/src/components/Modal.test.tsx +0 -80
  80. package/src/components/Modal.tsx +0 -125
  81. package/src/components/Navbar/Navbar.test.tsx +0 -91
  82. package/src/components/Navbar/Navbar.tsx +0 -185
  83. package/src/components/Navbar/index.ts +0 -2
  84. package/src/components/Navbar/types.ts +0 -30
  85. package/src/components/Newsletter/Newsletter.test.tsx +0 -45
  86. package/src/components/Newsletter/Newsletter.tsx +0 -12
  87. package/src/components/Newsletter/index.ts +0 -2
  88. package/src/components/Newsletter/parts.tsx +0 -23
  89. package/src/components/Newsletter/types.ts +0 -12
  90. package/src/components/Newsletter/useNewsletterForm.ts +0 -27
  91. package/src/components/Newsletter/variants/Card.tsx +0 -62
  92. package/src/components/Newsletter/variants/Inline.tsx +0 -54
  93. package/src/components/Newsletter/variants/Underline.tsx +0 -52
  94. package/src/components/Pricing/Pricing.test.tsx +0 -67
  95. package/src/components/Pricing/Pricing.tsx +0 -37
  96. package/src/components/Pricing/index.ts +0 -2
  97. package/src/components/Pricing/parts.tsx +0 -132
  98. package/src/components/Pricing/types.ts +0 -28
  99. package/src/components/Pricing/variants/Bento.tsx +0 -72
  100. package/src/components/Pricing/variants/Cards.tsx +0 -79
  101. package/src/components/Pricing/variants/Compact.tsx +0 -69
  102. package/src/components/Prose.test.tsx +0 -49
  103. package/src/components/Prose.tsx +0 -55
  104. package/src/components/Section.test.tsx +0 -51
  105. package/src/components/Section.tsx +0 -106
  106. package/src/components/Slot.test.tsx +0 -45
  107. package/src/components/Slot.tsx +0 -78
  108. package/src/components/Stats/Stats.test.tsx +0 -47
  109. package/src/components/Stats/Stats.tsx +0 -16
  110. package/src/components/Stats/index.ts +0 -2
  111. package/src/components/Stats/parts.tsx +0 -29
  112. package/src/components/Stats/types.ts +0 -23
  113. package/src/components/Stats/variants/Cells.tsx +0 -34
  114. package/src/components/Stats/variants/Editorial.tsx +0 -41
  115. package/src/components/Stats/variants/Hairline.tsx +0 -31
  116. package/src/components/Stats/variants/Ticker.tsx +0 -36
  117. package/src/components/Testimonials/TestimonialCard.tsx +0 -61
  118. package/src/components/Testimonials/Testimonials.test.tsx +0 -48
  119. package/src/components/Testimonials/Testimonials.tsx +0 -14
  120. package/src/components/Testimonials/index.ts +0 -3
  121. package/src/components/Testimonials/parts.ts +0 -7
  122. package/src/components/Testimonials/types.ts +0 -24
  123. package/src/components/Testimonials/variants/Carousel.tsx +0 -94
  124. package/src/components/Testimonials/variants/Grid.tsx +0 -19
  125. package/src/components/Testimonials/variants/Marquee.tsx +0 -28
  126. package/src/icons.tsx +0 -290
  127. package/src/index.ts +0 -120
  128. package/src/styles/index.css +0 -18
  129. package/src/test/setup.ts +0 -7
  130. package/src/utils/cn.test.ts +0 -21
  131. package/src/utils/cn.ts +0 -6
  132. package/src/utils/contrast.test.ts +0 -71
  133. package/src/utils/contrast.ts +0 -107
  134. package/src/utils/iso2Flag.test.ts +0 -26
  135. package/src/utils/iso2Flag.ts +0 -26
  136. package/src/utils/useTokenForeground.test.ts +0 -50
  137. package/src/utils/useTokenForeground.ts +0 -39
  138. /package/{src/styles → dist}/theme.css +0 -0
@@ -1,37 +0,0 @@
1
- "use client";
2
- import type { LanguageSwitcherProps } from "./types";
3
- import { useLanguageSwitcher } from "./useLanguageSwitcher";
4
- import { LanguageSwitcherDropdown } from "./variants/Dropdown";
5
- import { LanguageSwitcherModal } from "./variants/Modal";
6
-
7
- export const LanguageSwitcher = ({
8
- option = "dropdown",
9
- languages,
10
- value,
11
- defaultValue,
12
- onChange,
13
- ...props
14
- }: LanguageSwitcherProps) => {
15
- const state = useLanguageSwitcher(languages, value, defaultValue, onChange);
16
-
17
- if (languages.length === 0) return null;
18
-
19
- const variantProps = {
20
- languages,
21
- value,
22
- defaultValue,
23
- onChange,
24
- ...props,
25
- open: state.open,
26
- onOpenChange: state.setOpen,
27
- current: state.current,
28
- currentLang: state.currentLang,
29
- setCurrent: state.setCurrent,
30
- };
31
-
32
- if (option === "modal") {
33
- return <LanguageSwitcherModal {...variantProps} />;
34
- }
35
- return <LanguageSwitcherDropdown {...variantProps} />;
36
- };
37
- LanguageSwitcher.displayName = "LanguageSwitcher";
@@ -1,6 +0,0 @@
1
- export { LanguageSwitcher } from "./LanguageSwitcher";
2
- export type {
3
- LanguageSwitcherProps,
4
- LanguageSwitcherOption,
5
- LanguageOption,
6
- } from "./types";
@@ -1,144 +0,0 @@
1
- "use client";
2
- import { useEffect } from "react";
3
- import { cn } from "../../utils/cn";
4
- import { iso2ToFlagEmoji } from "../../utils/iso2Flag";
5
- import { Check, ChevronDown, Globe } from "../../icons";
6
- import type { LanguageOption } from "./types";
7
-
8
- export function useOutsideClose(
9
- open: boolean,
10
- rootRef: { current: HTMLDivElement | null },
11
- onClose: () => void,
12
- ) {
13
- useEffect(() => {
14
- if (!open) return;
15
- const onPointerDown = (event: PointerEvent) => {
16
- if (rootRef.current && !rootRef.current.contains(event.target as Node)) {
17
- onClose();
18
- }
19
- };
20
- const onKeyDown = (event: KeyboardEvent) => {
21
- if (event.key === "Escape") onClose();
22
- };
23
- document.addEventListener("pointerdown", onPointerDown);
24
- document.addEventListener("keydown", onKeyDown);
25
- return () => {
26
- document.removeEventListener("pointerdown", onPointerDown);
27
- document.removeEventListener("keydown", onKeyDown);
28
- };
29
- }, [open, rootRef, onClose]);
30
- }
31
-
32
- export function renderFlagOrGlobe(
33
- code: string,
34
- { size = "text-sm", globeClass = "text-muted-foreground" }: { size?: string; globeClass?: string } = {},
35
- ) {
36
- const flag = iso2ToFlagEmoji(code);
37
- return flag ? (
38
- <span aria-hidden className={cn(size, "leading-none")}>
39
- {flag}
40
- </span>
41
- ) : (
42
- <Globe className={cn("h-4 w-4 shrink-0", globeClass)} />
43
- );
44
- }
45
-
46
- export function LanguageTrigger({
47
- inverse,
48
- open,
49
- showFlag,
50
- currentLang,
51
- onClick,
52
- ariaProps,
53
- }: {
54
- inverse?: boolean;
55
- open: boolean;
56
- showFlag?: boolean;
57
- currentLang: LanguageOption;
58
- onClick: () => void;
59
- ariaProps: {
60
- haspopup: "listbox" | "dialog";
61
- expanded: boolean;
62
- controls?: string;
63
- };
64
- }) {
65
- return (
66
- <button
67
- type="button"
68
- aria-haspopup={ariaProps.haspopup}
69
- aria-expanded={ariaProps.expanded}
70
- aria-controls={ariaProps.controls}
71
- onClick={onClick}
72
- className={cn(
73
- "inline-flex h-9 cursor-pointer items-center gap-1.5 rounded-lg border px-3 text-[13px] font-medium transition-colors duration-150 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring",
74
- inverse
75
- ? "border-white/10 text-on-secondary hover:bg-white/10"
76
- : "border-border text-foreground hover:bg-surface",
77
- )}
78
- >
79
- {showFlag ? (
80
- renderFlagOrGlobe(currentLang.code, { globeClass: inverse ? "opacity-70" : "text-muted-foreground" })
81
- ) : (
82
- <Globe
83
- className={cn("h-4 w-4", inverse ? "opacity-70" : "text-muted-foreground")}
84
- />
85
- )}
86
- <span className="tracking-tight">{currentLang.label}</span>
87
- <ChevronDown
88
- className={cn(
89
- "h-3.5 w-3.5 transition-transform duration-150",
90
- open && "rotate-180",
91
- inverse ? "opacity-70" : "text-muted-foreground",
92
- )}
93
- />
94
- </button>
95
- );
96
- }
97
-
98
- export function LanguageOptionRow({
99
- lang,
100
- active,
101
- onSelect,
102
- variant,
103
- showFlag,
104
- }: {
105
- lang: LanguageOption;
106
- active: boolean;
107
- onSelect: () => void;
108
- variant: "dropdown" | "modal";
109
- showFlag?: boolean;
110
- }) {
111
- return (
112
- <button
113
- key={lang.code}
114
- type="button"
115
- role="option"
116
- aria-selected={active}
117
- onClick={onSelect}
118
- className={
119
- variant === "modal"
120
- ? cn(
121
- "flex w-full cursor-pointer items-center justify-between gap-3 rounded-lg border px-4 py-3 text-left transition-colors duration-150 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring",
122
- active
123
- ? "border-primary/40 bg-primary-soft text-foreground"
124
- : "border-border text-foreground hover:bg-surface-strong",
125
- )
126
- : cn(
127
- "flex w-full cursor-pointer items-center justify-between gap-3 rounded-md px-3 py-2 text-[13px] font-medium transition-colors duration-150 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring",
128
- active
129
- ? "bg-surface-strong text-foreground"
130
- : "text-muted-foreground hover:bg-surface-strong hover:text-foreground",
131
- )
132
- }
133
- >
134
- <span className={variant === "modal" ? "flex items-center gap-3" : "flex items-center gap-2.5"}>
135
- {showFlag ? renderFlagOrGlobe(lang.code, variant === "modal" ? { size: "text-base" } : {}) : null}
136
- <span className="text-sm font-medium">{lang.label}</span>
137
- </span>
138
- <span className={variant === "modal" ? "flex items-center gap-2 text-xs font-semibold uppercase tracking-wide text-muted-foreground" : undefined}>
139
- {variant === "modal" ? lang.code : null}
140
- {active ? <Check className={variant === "modal" ? "h-4 w-4 text-primary" : "h-3.5 w-3.5 text-primary"} /> : null}
141
- </span>
142
- </button>
143
- );
144
- }
@@ -1,40 +0,0 @@
1
- import type { HTMLAttributes, ReactNode } from "react";
2
-
3
- export interface LanguageOption {
4
- /** Language code (ISO 3166-1 alpha-2, e.g. `"tr"`) — this is the value passed to `onChange` and the flag source. */
5
- code: string;
6
- /** Native display name, e.g. `"Türkçe"`. */
7
- label: string;
8
- }
9
-
10
- export type LanguageSwitcherOption = "dropdown" | "modal";
11
-
12
- export interface LanguageSwitcherProps extends Omit<HTMLAttributes<HTMLDivElement>, "onChange"> {
13
- /** `dropdown` opens the anchored menu; `modal` opens a full dialog via the Modal component. */
14
- option?: LanguageSwitcherOption;
15
- /** Languages to switch between. Required — no built-in default language list. */
16
- languages: LanguageOption[];
17
- /** Controlled current language code. */
18
- value?: string;
19
- /** Uncontrolled initial language code. Defaults to the first language. */
20
- defaultValue?: string;
21
- /** Called with the selected language code. */
22
- onChange?: (code: string) => void;
23
- /** Dropdown menu alignment relative to its trigger. Defaults to `end`. */
24
- align?: "start" | "end";
25
- /** Heading rendered inside the `modal` option. Optional — pass it to show a title. */
26
- modalTitle?: ReactNode;
27
- /** Replace the globe icon with the selected language's flag emoji (and show flags in options). */
28
- showFlag?: boolean;
29
- /** Adapts colors for dark surfaces (inverse Navbar, classic Footer). */
30
- inverse?: boolean;
31
- }
32
-
33
- export interface LanguageSwitcherVariantProps
34
- extends Omit<LanguageSwitcherProps, "option"> {
35
- open: boolean;
36
- onOpenChange: (open: boolean) => void;
37
- current: string;
38
- currentLang: LanguageOption;
39
- setCurrent: (code: string) => void;
40
- }
@@ -1,23 +0,0 @@
1
- "use client";
2
- import { useState } from "react";
3
- import type { LanguageOption } from "./types";
4
-
5
- export function useLanguageSwitcher(
6
- languages: LanguageOption[],
7
- value?: string,
8
- defaultValue?: string,
9
- onChange?: (code: string) => void,
10
- ) {
11
- const [open, setOpen] = useState(false);
12
- const [internal, setInternal] = useState(defaultValue ?? languages[0]?.code ?? "");
13
- const current = value ?? internal;
14
- const currentLang = languages.find((l) => l.code === current) ?? languages[0];
15
-
16
- const setCurrent = (code: string) => {
17
- setInternal(code);
18
- setOpen(false);
19
- onChange?.(code);
20
- };
21
-
22
- return { open, setOpen, current, currentLang, setCurrent };
23
- }
@@ -1,88 +0,0 @@
1
- "use client";
2
- import { useId, useRef, type CSSProperties } from "react";
3
- import { cn } from "../../../utils/cn";
4
- import {
5
- useOutsideClose,
6
- LanguageTrigger,
7
- LanguageOptionRow,
8
- } from "../parts";
9
- import type { LanguageSwitcherVariantProps } from "../types";
10
-
11
- export function LanguageSwitcherDropdown({
12
- className,
13
- languages,
14
- open,
15
- onOpenChange,
16
- current,
17
- currentLang,
18
- setCurrent,
19
- align = "end",
20
- showFlag = false,
21
- inverse = false,
22
- modalTitle,
23
- value,
24
- defaultValue,
25
- onChange,
26
- ...props
27
- }: LanguageSwitcherVariantProps) {
28
- const listboxId = useId();
29
- const anchorName = `--ls-${listboxId.replace(/:/g, "")}`;
30
- const rootRef = useRef<HTMLDivElement>(null);
31
-
32
- useOutsideClose(open, rootRef, () => onOpenChange(false));
33
-
34
- return (
35
- <div
36
- ref={rootRef}
37
- className={cn("relative inline-flex", className)}
38
- {...props}
39
- >
40
- <button
41
- type="button"
42
- style={{ anchorName } as CSSProperties}
43
- onClick={() => onOpenChange(!open)}
44
- >
45
- <LanguageTrigger
46
- inverse={inverse}
47
- open={open}
48
- showFlag={showFlag}
49
- currentLang={currentLang}
50
- onClick={() => onOpenChange(!open)}
51
- ariaProps={{ haspopup: "listbox", expanded: open, controls: listboxId }}
52
- />
53
- </button>
54
-
55
- <div
56
- id={listboxId}
57
- role="listbox"
58
- aria-label="Select language"
59
- style={
60
- {
61
- positionAnchor: anchorName,
62
- top: "anchor(bottom)",
63
- ...(align === "start" ? { left: "anchor(left)" } : { right: "anchor(right)" }),
64
- } as CSSProperties
65
- }
66
- className={cn(
67
- "absolute top-full z-50 mt-2 min-w-[10rem] rounded-lg border border-border bg-surface p-1 shadow-raised transition-all duration-150",
68
- align === "start" ? "left-0" : "right-0",
69
- open
70
- ? "visible translate-y-0 opacity-100"
71
- : "pointer-events-none invisible -translate-y-1 opacity-0",
72
- )}
73
- >
74
- {languages.map((lang) => (
75
- <LanguageOptionRow
76
- key={lang.code}
77
- lang={lang}
78
- active={lang.code === current}
79
- onSelect={() => setCurrent(lang.code)}
80
- variant="dropdown"
81
- showFlag={showFlag}
82
- />
83
- ))}
84
- </div>
85
- </div>
86
- );
87
- }
88
- LanguageSwitcherDropdown.displayName = "LanguageSwitcherDropdown";
@@ -1,56 +0,0 @@
1
- "use client";
2
- import { useRef } from "react";
3
- import { cn } from "../../../utils/cn";
4
- import { Modal } from "../../Modal";
5
- import { LanguageTrigger, LanguageOptionRow } from "../parts";
6
- import type { LanguageSwitcherVariantProps } from "../types";
7
-
8
- export function LanguageSwitcherModal({
9
- className,
10
- languages,
11
- open,
12
- onOpenChange,
13
- current,
14
- currentLang,
15
- setCurrent,
16
- modalTitle,
17
- showFlag = false,
18
- inverse = false,
19
- align,
20
- value,
21
- defaultValue,
22
- onChange,
23
- ...props
24
- }: LanguageSwitcherVariantProps) {
25
- const rootRef = useRef<HTMLDivElement>(null);
26
-
27
- return (
28
- <>
29
- <div ref={rootRef} className={cn("relative inline-flex", className)} {...props}>
30
- <LanguageTrigger
31
- inverse={inverse}
32
- open={open}
33
- showFlag={showFlag}
34
- currentLang={currentLang}
35
- onClick={() => onOpenChange(!open)}
36
- ariaProps={{ haspopup: "dialog", expanded: open }}
37
- />
38
- </div>
39
- <Modal open={open} onClose={() => onOpenChange(false)} title={modalTitle} size="sm">
40
- <div role="listbox" aria-label="Select language" className="grid min-w-[19rem] gap-1.5">
41
- {languages.map((lang) => (
42
- <LanguageOptionRow
43
- key={lang.code}
44
- lang={lang}
45
- active={lang.code === current}
46
- onSelect={() => setCurrent(lang.code)}
47
- variant="modal"
48
- showFlag={showFlag}
49
- />
50
- ))}
51
- </div>
52
- </Modal>
53
- </>
54
- );
55
- }
56
- LanguageSwitcherModal.displayName = "LanguageSwitcherModal";
@@ -1,35 +0,0 @@
1
- import { describe, it, expect } from "vitest";
2
- import { render, screen } from "@testing-library/react";
3
- import { LogoCloud } from "./LogoCloud";
4
-
5
- const logos = [{ name: "Acme" }, { name: "Globex" }];
6
-
7
- describe("LogoCloud", () => {
8
- it("renders the title and logo names (quiet by default)", () => {
9
- render(<LogoCloud title="Teams" logos={logos} />);
10
- expect(screen.getByText("Teams")).toBeInTheDocument();
11
- expect(screen.getByText("Acme")).toBeInTheDocument();
12
- expect(screen.getByText("Globex")).toBeInTheDocument();
13
- });
14
-
15
- it("does not render a title when none is provided", () => {
16
- render(<LogoCloud logos={logos} />);
17
- expect(screen.queryByText("Teams")).not.toBeInTheDocument();
18
- });
19
-
20
- it("renders the marquee option (items duplicated for the loop)", () => {
21
- render(<LogoCloud logos={logos} option="marquee" />);
22
- expect(screen.getAllByText("Acme").length).toBeGreaterThan(1);
23
- });
24
-
25
- it("renders the strip option", () => {
26
- render(<LogoCloud logos={logos} option="strip" />);
27
- expect(screen.getByText("Acme")).toBeInTheDocument();
28
- });
29
-
30
- it("renders a logo image when src is provided", () => {
31
- render(<LogoCloud logos={[{ name: "Acme", src: "/acme.png" }]} />);
32
- const img = screen.getByAltText("Acme");
33
- expect(img).toHaveAttribute("src", "/acme.png");
34
- });
35
- });
@@ -1,14 +0,0 @@
1
- import { forwardRef } from "react";
2
- import type { LogoCloudProps } from "./types";
3
- import { LogoCloudQuiet } from "./variants/Quiet";
4
- import { LogoCloudMarquee } from "./variants/Marquee";
5
- import { LogoCloudStrip } from "./variants/Strip";
6
-
7
- export const LogoCloud = forwardRef<HTMLDivElement, LogoCloudProps>(
8
- ({ option = "quiet", ...props }, ref) => {
9
- if (option === "marquee") return <LogoCloudMarquee ref={ref} {...props} />;
10
- if (option === "strip") return <LogoCloudStrip ref={ref} {...props} />;
11
- return <LogoCloudQuiet ref={ref} {...props} />;
12
- },
13
- );
14
- LogoCloud.displayName = "LogoCloud";
@@ -1,2 +0,0 @@
1
- export { LogoCloud } from "./LogoCloud";
2
- export type { LogoCloudProps, LogoCloudOption, LogoItem } from "./types";
@@ -1,32 +0,0 @@
1
- import { cn } from "../../utils/cn";
2
- import type { LogoItem } from "./types";
3
-
4
- const LogoText = ({ name }: { name: string }) => (
5
- <span className="text-lg font-semibold tracking-tight text-foreground/45 transition-colors duration-200 group-hover:text-foreground">
6
- {name}
7
- </span>
8
- );
9
-
10
- export const LogoMark = ({ logo }: { logo: LogoItem }) =>
11
- logo.src ? (
12
- <img
13
- src={logo.src}
14
- alt={logo.name}
15
- loading="lazy"
16
- className={cn(
17
- "max-h-7 w-auto opacity-50 grayscale transition-all duration-200 group-hover:opacity-90 group-hover:grayscale-0",
18
- "h-7",
19
- )}
20
- />
21
- ) : (
22
- <LogoText name={logo.name} />
23
- );
24
-
25
- export function LogoCloudTitle({ title, className }: { title?: string; className?: string }) {
26
- if (!title) return null;
27
- return (
28
- <p className={cn("mb-8 text-center text-xs font-semibold uppercase tracking-[0.18em] text-muted-foreground", className)}>
29
- {title}
30
- </p>
31
- );
32
- }
@@ -1,16 +0,0 @@
1
- import type { HTMLAttributes } from "react";
2
-
3
- export type LogoCloudOption = "quiet" | "marquee" | "strip";
4
-
5
- export interface LogoItem {
6
- name: string;
7
- src?: string;
8
- href?: string;
9
- }
10
-
11
- export interface LogoCloudProps extends HTMLAttributes<HTMLDivElement> {
12
- logos: LogoItem[];
13
- title?: string;
14
- /** Visual option: quiet list · infinite marquee · hairline strip */
15
- option?: LogoCloudOption;
16
- }
@@ -1,30 +0,0 @@
1
- import { forwardRef, type HTMLAttributes } from "react";
2
- import { cn } from "../../../utils/cn";
3
- import { LogoMark, LogoCloudTitle } from "../parts";
4
- import type { LogoItem } from "../types";
5
-
6
- interface MarqueeProps extends HTMLAttributes<HTMLDivElement> {
7
- logos: LogoItem[];
8
- title?: string;
9
- }
10
-
11
- export const LogoCloudMarquee = forwardRef<HTMLDivElement, MarqueeProps>(
12
- ({ className, logos, title, ...props }, ref) => {
13
- const doubled = [...logos, ...logos];
14
- return (
15
- <div ref={ref} className={cn("w-full overflow-hidden", className)} {...props}>
16
- <LogoCloudTitle title={title} />
17
- <div className="mask-fade-x">
18
- <div className="flex w-max animate-marquee gap-14 pr-14 motion-reduce:animate-none">
19
- {doubled.map((logo, i) => (
20
- <div key={`${logo.name}-${i}`} className="group shrink-0">
21
- <LogoMark logo={logo} />
22
- </div>
23
- ))}
24
- </div>
25
- </div>
26
- </div>
27
- );
28
- },
29
- );
30
- LogoCloudMarquee.displayName = "LogoCloudMarquee";
@@ -1,26 +0,0 @@
1
- import { forwardRef, type HTMLAttributes } from "react";
2
- import { cn } from "../../../utils/cn";
3
- import { LogoMark, LogoCloudTitle } from "../parts";
4
- import type { LogoItem } from "../types";
5
-
6
- interface QuietProps extends HTMLAttributes<HTMLDivElement> {
7
- logos: LogoItem[];
8
- title?: string;
9
- }
10
-
11
- /** quiet — unbounded spread of wordmarks. */
12
- export const LogoCloudQuiet = forwardRef<HTMLDivElement, QuietProps>(
13
- ({ className, logos, title, ...props }, ref) => (
14
- <div ref={ref} className={cn("w-full", className)} {...props}>
15
- <LogoCloudTitle title={title} />
16
- <ul className="flex flex-wrap items-center justify-center gap-x-12 gap-y-6">
17
- {logos.map((logo) => (
18
- <li key={logo.name} className="group">
19
- <LogoMark logo={logo} />
20
- </li>
21
- ))}
22
- </ul>
23
- </div>
24
- ),
25
- );
26
- LogoCloudQuiet.displayName = "LogoCloudQuiet";
@@ -1,25 +0,0 @@
1
- import { forwardRef, type HTMLAttributes } from "react";
2
- import { cn } from "../../../utils/cn";
3
- import { LogoMark, LogoCloudTitle } from "../parts";
4
- import type { LogoItem } from "../types";
5
-
6
- interface StripProps extends HTMLAttributes<HTMLDivElement> {
7
- logos: LogoItem[];
8
- title?: string;
9
- }
10
-
11
- export const LogoCloudStrip = forwardRef<HTMLDivElement, StripProps>(
12
- ({ className, logos, title, ...props }, ref) => (
13
- <div ref={ref} className={cn("w-full", className)} {...props}>
14
- <LogoCloudTitle title={title} />
15
- <ul className="flex flex-wrap items-center justify-center gap-x-10 gap-y-4 rounded-2xl border border-border bg-surface px-6 py-7 sm:gap-x-14">
16
- {logos.map((logo) => (
17
- <li key={logo.name} className="group">
18
- <LogoMark logo={logo} />
19
- </li>
20
- ))}
21
- </ul>
22
- </div>
23
- ),
24
- );
25
- LogoCloudStrip.displayName = "LogoCloudStrip";
@@ -1,89 +0,0 @@
1
- import { describe, it, expect } from "vitest";
2
- import { render, screen, fireEvent } from "@testing-library/react";
3
- import { MegaMenu } from "./MegaMenu";
4
-
5
- const items = [
6
- {
7
- label: "Product",
8
- columns: [
9
- {
10
- title: "Build",
11
- links: [
12
- { label: "API Platform", description: "REST + GraphQL", href: "#api" },
13
- { label: "Workflows", href: "#wf" },
14
- ],
15
- },
16
- ],
17
- featured: { title: "Get started", description: "See it live", cta: "Start", href: "#start" },
18
- },
19
- { label: "Pricing", href: "#pricing" },
20
- { label: "Docs", href: "#docs" },
21
- ];
22
-
23
- function desktopTrigger(container: HTMLElement): HTMLButtonElement {
24
- // The desktop trigger is the one with aria-haspopup; the mobile accordion
25
- // button does not set it, and it is hidden behind `lg:hidden` CSS.
26
- return container.querySelector('button[aria-haspopup="true"]')!;
27
- }
28
-
29
- describe("MegaMenu", () => {
30
- it("renders the brand and plain link items", () => {
31
- render(<MegaMenu brand="Acurio" items={items} />);
32
- expect(screen.getByText("Acurio")).toBeInTheDocument();
33
- // Links render in both the desktop bar and the mobile panel.
34
- const pricingLinks = screen.getAllByRole("link", { name: "Pricing" });
35
- expect(pricingLinks.length).toBeGreaterThan(0);
36
- expect(pricingLinks[0]).toHaveAttribute("href", "#pricing");
37
- expect(screen.getAllByRole("link", { name: "Docs" }).length).toBeGreaterThan(0);
38
- });
39
-
40
- it("renders a trigger button for panel items (with columns/featured)", () => {
41
- const { container } = render(<MegaMenu brand="Acurio" items={items} />);
42
- const trigger = desktopTrigger(container);
43
- expect(trigger).toHaveAttribute("aria-haspopup", "true");
44
- expect(trigger).toHaveAttribute("aria-expanded", "false");
45
- });
46
-
47
- it("reveals the panel (column links + featured card) when the desktop trigger is clicked", () => {
48
- const { container } = render(<MegaMenu brand="Acurio" items={items} />);
49
- fireEvent.click(desktopTrigger(container));
50
-
51
- expect(container.querySelector('[role="region"][aria-label="Product menu"]')).toBeInTheDocument();
52
- // The column and featured links are present in the panel markup.
53
- expect(container.querySelector('a[href="#api"]')).toBeInTheDocument();
54
- expect(container.querySelector('a[href="#wf"]')).toBeInTheDocument();
55
- expect(container.querySelector('a[href="#start"]')).toBeInTheDocument();
56
- });
57
-
58
- it("sets aria-expanded while the panel is open and closes on Escape", () => {
59
- const { container } = render(<MegaMenu brand="Acurio" items={items} />);
60
- const trigger = desktopTrigger(container);
61
- fireEvent.click(trigger);
62
- expect(trigger).toHaveAttribute("aria-expanded", "true");
63
-
64
- fireEvent.keyDown(screen.getByRole("banner"), { key: "Escape" });
65
- expect(trigger).toHaveAttribute("aria-expanded", "false");
66
- });
67
-
68
- it("applies the inverse variant to the header shell", () => {
69
- render(<MegaMenu brand="Acurio" items={items} variant="inverse" />);
70
- expect(screen.getByRole("banner")).toHaveClass("bg-[#101010]/85");
71
- });
72
-
73
- it("toggles the mobile menu via the hamburger", () => {
74
- render(<MegaMenu brand="Acurio" items={items} />);
75
- const openBtn = screen.getByRole("button", { name: "Open menu" });
76
- expect(openBtn).toHaveAttribute("aria-expanded", "false");
77
-
78
- fireEvent.click(openBtn);
79
- expect(screen.getByRole("button", { name: "Close menu" })).toHaveAttribute(
80
- "aria-expanded",
81
- "true",
82
- );
83
- });
84
-
85
- it("does not render sticky when sticky is disabled", () => {
86
- render(<MegaMenu brand="Acurio" items={items} sticky={false} />);
87
- expect(screen.getByRole("banner")).not.toHaveClass("sticky");
88
- });
89
- });