@rovula/ui 0.1.0 → 0.1.1
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/dist/cjs/bundle.css +65 -0
- package/dist/cjs/bundle.js +9261 -3
- package/dist/cjs/bundle.js.map +1 -1
- package/dist/cjs/types/components/Footer/Footer.d.ts +21 -0
- package/dist/cjs/types/components/Footer/Footer.stories.d.ts +45 -0
- package/dist/cjs/types/components/Footer/index.d.ts +2 -0
- package/dist/cjs/types/components/Icon/Icon.d.ts +1 -1
- package/dist/cjs/types/components/Icon/Icon.stories.d.ts +9 -1
- package/dist/cjs/types/components/Navbar/Navbar.d.ts +5 -0
- package/dist/cjs/types/components/Navbar/Navbar.stories.d.ts +14 -0
- package/dist/cjs/types/components/PasswordInput/PasswordInput.d.ts +19 -0
- package/dist/cjs/types/components/PasswordInput/PasswordInput.stories.d.ts +395 -0
- package/dist/cjs/types/components/PasswordInput/index.d.ts +2 -0
- package/dist/cjs/types/icons/index.d.ts +1 -0
- package/dist/cjs/types/icons/lucideIconNames.d.ts +9 -0
- package/dist/cjs/types/index.d.ts +7 -1
- package/dist/cjs/types/utils/colors.d.ts +330 -0
- package/dist/components/Footer/Footer.js +11 -0
- package/dist/components/Footer/Footer.stories.js +34 -0
- package/dist/components/Footer/index.js +2 -0
- package/dist/components/Icon/Icon.js +28 -11
- package/dist/components/Icon/Icon.stories.js +39 -0
- package/dist/components/Navbar/Navbar.js +18 -4
- package/dist/components/Navbar/Navbar.stories.js +16 -9
- package/dist/components/PasswordInput/PasswordInput.js +36 -0
- package/dist/components/PasswordInput/PasswordInput.stories.js +67 -0
- package/dist/components/PasswordInput/index.js +1 -0
- package/dist/esm/bundle.css +65 -0
- package/dist/esm/bundle.js +9261 -3
- package/dist/esm/bundle.js.map +1 -1
- package/dist/esm/types/components/Footer/Footer.d.ts +21 -0
- package/dist/esm/types/components/Footer/Footer.stories.d.ts +45 -0
- package/dist/esm/types/components/Footer/index.d.ts +2 -0
- package/dist/esm/types/components/Icon/Icon.d.ts +1 -1
- package/dist/esm/types/components/Icon/Icon.stories.d.ts +9 -1
- package/dist/esm/types/components/Navbar/Navbar.d.ts +5 -0
- package/dist/esm/types/components/Navbar/Navbar.stories.d.ts +14 -0
- package/dist/esm/types/components/PasswordInput/PasswordInput.d.ts +19 -0
- package/dist/esm/types/components/PasswordInput/PasswordInput.stories.d.ts +395 -0
- package/dist/esm/types/components/PasswordInput/index.d.ts +2 -0
- package/dist/esm/types/icons/index.d.ts +1 -0
- package/dist/esm/types/icons/lucideIconNames.d.ts +9 -0
- package/dist/esm/types/index.d.ts +7 -1
- package/dist/esm/types/utils/colors.d.ts +330 -0
- package/dist/icons/index.js +1 -0
- package/dist/icons/lucideIconNames.js +12 -0
- package/dist/index.d.ts +386 -2
- package/dist/index.js +4 -0
- package/dist/src/theme/global.css +117 -24
- package/dist/utils/colors.js +369 -0
- package/package.json +2 -1
- package/src/components/Footer/Footer.stories.tsx +119 -0
- package/src/components/Footer/Footer.tsx +122 -0
- package/src/components/Footer/index.ts +3 -0
- package/src/components/Icon/Icon.stories.tsx +89 -0
- package/src/components/Icon/Icon.tsx +44 -23
- package/src/components/Navbar/Navbar.stories.tsx +109 -55
- package/src/components/Navbar/Navbar.tsx +41 -3
- package/src/components/PasswordInput/PasswordInput.stories.tsx +111 -0
- package/src/components/PasswordInput/PasswordInput.tsx +50 -0
- package/src/components/PasswordInput/index.ts +2 -0
- package/src/icons/index.ts +1 -0
- package/src/icons/lucideIconNames.ts +14 -0
- package/src/index.ts +15 -1
- package/src/theme/themes/skyller/typography.css +24 -24
- package/src/theme/tokens/baseline.css +1 -0
- package/src/theme/tokens/components/footer.css +9 -0
- package/src/theme/tokens/components/navbar.css +2 -1
- package/src/types/lucide-react.d.ts +5 -0
- package/src/utils/colors.ts +383 -0
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import type { Meta, StoryObj } from "@storybook/react";
|
|
3
|
+
|
|
4
|
+
import Footer from "./Footer";
|
|
5
|
+
|
|
6
|
+
const meta = {
|
|
7
|
+
title: "Components/Footer",
|
|
8
|
+
component: Footer,
|
|
9
|
+
tags: ["autodocs"],
|
|
10
|
+
parameters: {
|
|
11
|
+
layout: "fullscreen",
|
|
12
|
+
},
|
|
13
|
+
decorators: [
|
|
14
|
+
(Story) => (
|
|
15
|
+
<div className="p-5 w-full h-screen flex flex-col">
|
|
16
|
+
<div className="flex-1">Page content</div>
|
|
17
|
+
<Story />
|
|
18
|
+
</div>
|
|
19
|
+
),
|
|
20
|
+
],
|
|
21
|
+
} satisfies Meta<typeof Footer>;
|
|
22
|
+
|
|
23
|
+
export default meta;
|
|
24
|
+
|
|
25
|
+
export const Default = {
|
|
26
|
+
args: {},
|
|
27
|
+
render: (args) => (
|
|
28
|
+
<Footer
|
|
29
|
+
leftNav={
|
|
30
|
+
<ul className="flex gap-2">
|
|
31
|
+
<li>© 2024 Company</li>
|
|
32
|
+
<li>Privacy</li>
|
|
33
|
+
<li>Terms</li>
|
|
34
|
+
</ul>
|
|
35
|
+
}
|
|
36
|
+
center={<>Footer center</>}
|
|
37
|
+
rightNav={
|
|
38
|
+
<ul className="flex gap-2">
|
|
39
|
+
<li>Link 1</li>
|
|
40
|
+
<li>Link 2</li>
|
|
41
|
+
<li>Link 3</li>
|
|
42
|
+
</ul>
|
|
43
|
+
}
|
|
44
|
+
/>
|
|
45
|
+
),
|
|
46
|
+
} satisfies StoryObj;
|
|
47
|
+
|
|
48
|
+
export const WithCopyright = {
|
|
49
|
+
args: {},
|
|
50
|
+
render: (args) => (
|
|
51
|
+
<Footer
|
|
52
|
+
copyright="© 2024 Company. All rights reserved."
|
|
53
|
+
leftNav={
|
|
54
|
+
<ul className="flex gap-2">
|
|
55
|
+
<li>Privacy</li>
|
|
56
|
+
<li>Terms</li>
|
|
57
|
+
</ul>
|
|
58
|
+
}
|
|
59
|
+
rightNav={
|
|
60
|
+
<ul className="flex gap-2">
|
|
61
|
+
<li>Help</li>
|
|
62
|
+
<li>Contact</li>
|
|
63
|
+
</ul>
|
|
64
|
+
}
|
|
65
|
+
/>
|
|
66
|
+
),
|
|
67
|
+
} satisfies StoryObj;
|
|
68
|
+
|
|
69
|
+
export const Simple = {
|
|
70
|
+
args: {},
|
|
71
|
+
render: (args) => (
|
|
72
|
+
<Footer
|
|
73
|
+
variant="simple"
|
|
74
|
+
copyright="© 2024 Company. Powered by Rovula."
|
|
75
|
+
/>
|
|
76
|
+
),
|
|
77
|
+
} satisfies StoryObj;
|
|
78
|
+
|
|
79
|
+
export const Transparent = {
|
|
80
|
+
args: {},
|
|
81
|
+
render: (args) => (
|
|
82
|
+
<div className="flex flex-col w-full min-h-screen bg-primary-5">
|
|
83
|
+
<div className="flex-1 p-8">Page content</div>
|
|
84
|
+
<Footer
|
|
85
|
+
variant="transparent"
|
|
86
|
+
copyright="© 2024 Company"
|
|
87
|
+
leftNav={
|
|
88
|
+
<ul className="flex gap-2">
|
|
89
|
+
<li>Privacy</li>
|
|
90
|
+
<li>Terms</li>
|
|
91
|
+
</ul>
|
|
92
|
+
}
|
|
93
|
+
rightNav={
|
|
94
|
+
<ul className="flex gap-2">
|
|
95
|
+
<li>Help</li>
|
|
96
|
+
<li>Contact</li>
|
|
97
|
+
</ul>
|
|
98
|
+
}
|
|
99
|
+
/>
|
|
100
|
+
</div>
|
|
101
|
+
),
|
|
102
|
+
} satisfies StoryObj;
|
|
103
|
+
|
|
104
|
+
export const Custom = {
|
|
105
|
+
args: {},
|
|
106
|
+
render: (args) => (
|
|
107
|
+
<Footer
|
|
108
|
+
className="px-8"
|
|
109
|
+
copyright="© 2024 Company"
|
|
110
|
+
center={<>Powered by Rovula</>}
|
|
111
|
+
rightNav={
|
|
112
|
+
<ul className="flex gap-2">
|
|
113
|
+
<li>Help</li>
|
|
114
|
+
<li>Contact</li>
|
|
115
|
+
</ul>
|
|
116
|
+
}
|
|
117
|
+
/>
|
|
118
|
+
),
|
|
119
|
+
} satisfies StoryObj;
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
import { cn } from "@/utils/cn";
|
|
2
|
+
import React, { FC, ReactNode } from "react";
|
|
3
|
+
|
|
4
|
+
export type FooterVariant = "default" | "simple" | "transparent";
|
|
5
|
+
|
|
6
|
+
export type FooterProps = {
|
|
7
|
+
/** Layout variant: default (3-column) | simple (centered) | transparent (no bg) */
|
|
8
|
+
variant?: FooterVariant;
|
|
9
|
+
/** Copyright text - renders in left (default) or center (simple) */
|
|
10
|
+
copyright?: ReactNode;
|
|
11
|
+
position?: "static" | "sticky";
|
|
12
|
+
children?: ReactNode;
|
|
13
|
+
leftNav?: ReactNode;
|
|
14
|
+
rightNav?: ReactNode;
|
|
15
|
+
center?: ReactNode;
|
|
16
|
+
container?: boolean;
|
|
17
|
+
className?: string;
|
|
18
|
+
containerClassName?: string;
|
|
19
|
+
leftNavClassName?: string;
|
|
20
|
+
centerClassName?: string;
|
|
21
|
+
rightNavClassName?: string;
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
const Footer: FC<FooterProps> = ({
|
|
25
|
+
children,
|
|
26
|
+
className,
|
|
27
|
+
variant = "default",
|
|
28
|
+
copyright,
|
|
29
|
+
center,
|
|
30
|
+
leftNav,
|
|
31
|
+
rightNav,
|
|
32
|
+
position,
|
|
33
|
+
container = false,
|
|
34
|
+
containerClassName,
|
|
35
|
+
leftNavClassName,
|
|
36
|
+
centerClassName,
|
|
37
|
+
rightNavClassName,
|
|
38
|
+
}) => {
|
|
39
|
+
const isSimple = variant === "simple";
|
|
40
|
+
const isTransparent = variant === "transparent";
|
|
41
|
+
|
|
42
|
+
const defaultLeft = leftNav || copyright;
|
|
43
|
+
const defaultCenter = isSimple ? copyright ?? center : center;
|
|
44
|
+
const defaultRight = isSimple ? null : rightNav;
|
|
45
|
+
|
|
46
|
+
return (
|
|
47
|
+
<footer
|
|
48
|
+
className={cn(
|
|
49
|
+
"relative w-full px-4 py-6 box-border overflow-hidden typography-subtitile2 border-solid border-t-2 text-[var(--footer-text-color)] border-t-[var(--footer-border-color)]",
|
|
50
|
+
isSimple ? "h-[var(--footer-height-simple)]" : "h-[var(--footer-height)]",
|
|
51
|
+
{ position },
|
|
52
|
+
className
|
|
53
|
+
)}
|
|
54
|
+
>
|
|
55
|
+
{/* Default bg (z:-10) + overlay for className override - hidden when transparent */}
|
|
56
|
+
{!isTransparent && (
|
|
57
|
+
<div
|
|
58
|
+
className="absolute inset-0 -z-10 bg-[var(--footer-bg-color)]"
|
|
59
|
+
aria-hidden
|
|
60
|
+
/>
|
|
61
|
+
)}
|
|
62
|
+
<div
|
|
63
|
+
className={cn("absolute inset-0 -z-[5] pointer-events-none", className)}
|
|
64
|
+
aria-hidden
|
|
65
|
+
/>
|
|
66
|
+
<div
|
|
67
|
+
className={cn(
|
|
68
|
+
"relative mx-auto flex h-full items-center",
|
|
69
|
+
isSimple ? "justify-center" : "justify-between",
|
|
70
|
+
{ container },
|
|
71
|
+
containerClassName
|
|
72
|
+
)}
|
|
73
|
+
>
|
|
74
|
+
{children ?? (
|
|
75
|
+
isSimple ? (
|
|
76
|
+
<div className={cn("text-center", centerClassName)}>
|
|
77
|
+
{defaultCenter}
|
|
78
|
+
</div>
|
|
79
|
+
) : (
|
|
80
|
+
<>
|
|
81
|
+
<nav
|
|
82
|
+
className={cn(
|
|
83
|
+
"flex w-1/2 items-center gap-x-[var(--footer-gap)] text-xl",
|
|
84
|
+
leftNavClassName
|
|
85
|
+
)}
|
|
86
|
+
>
|
|
87
|
+
{copyright && leftNav ? (
|
|
88
|
+
<>
|
|
89
|
+
<span className="flex-shrink-0">{copyright}</span>
|
|
90
|
+
{leftNav}
|
|
91
|
+
</>
|
|
92
|
+
) : (
|
|
93
|
+
defaultLeft
|
|
94
|
+
)}
|
|
95
|
+
</nav>
|
|
96
|
+
|
|
97
|
+
<div
|
|
98
|
+
className={cn(
|
|
99
|
+
"flex flex-shrink-0 flex-wrap justify-center",
|
|
100
|
+
centerClassName
|
|
101
|
+
)}
|
|
102
|
+
>
|
|
103
|
+
{defaultCenter}
|
|
104
|
+
</div>
|
|
105
|
+
|
|
106
|
+
<nav
|
|
107
|
+
className={cn(
|
|
108
|
+
"flex w-1/2 justify-end gap-x-[var(--footer-gap)] text-xl",
|
|
109
|
+
rightNavClassName
|
|
110
|
+
)}
|
|
111
|
+
>
|
|
112
|
+
{defaultRight}
|
|
113
|
+
</nav>
|
|
114
|
+
</>
|
|
115
|
+
)
|
|
116
|
+
)}
|
|
117
|
+
</div>
|
|
118
|
+
</footer>
|
|
119
|
+
);
|
|
120
|
+
};
|
|
121
|
+
|
|
122
|
+
export default Footer;
|
|
@@ -368,6 +368,95 @@ export const PreviewHeroIcon = {
|
|
|
368
368
|
},
|
|
369
369
|
} satisfies StoryObj;
|
|
370
370
|
|
|
371
|
+
const LUCIDE_DESIGNER_ICONS = [
|
|
372
|
+
"user",
|
|
373
|
+
"lock",
|
|
374
|
+
"eye-closed",
|
|
375
|
+
"eye",
|
|
376
|
+
"circle-check",
|
|
377
|
+
"circle-x",
|
|
378
|
+
"circle-alert",
|
|
379
|
+
"sliders-horizontal",
|
|
380
|
+
"calendar",
|
|
381
|
+
"search",
|
|
382
|
+
"check",
|
|
383
|
+
"triangle-alert",
|
|
384
|
+
];
|
|
385
|
+
|
|
386
|
+
export const PreviewLucideIcon = {
|
|
387
|
+
args: {},
|
|
388
|
+
render: (args) => (
|
|
389
|
+
<div className="grid grid-cols-1 gap-4 w-full h-full">
|
|
390
|
+
<div className="flex flex-col justify-start gap-4 w-full h-full">
|
|
391
|
+
<h4>Lucide icons (designer set)</h4>
|
|
392
|
+
<p className="text-sm text-gray-500">
|
|
393
|
+
Names from <a href="https://lucide.dev/icons" target="_blank" rel="noreferrer" className="underline">lucide.dev/icons</a>. Use <code>getLucideIconNames()</code> for full list.
|
|
394
|
+
</p>
|
|
395
|
+
{LUCIDE_DESIGNER_ICONS.map((iconName) => (
|
|
396
|
+
<div key={iconName} className="flex flex-row gap-6 items-center">
|
|
397
|
+
<Icon {...args} type="lucide" name={iconName} variant="outline" size="sm" />
|
|
398
|
+
<Icon {...args} type="lucide" name={iconName} variant="outline" size="md" />
|
|
399
|
+
<Icon {...args} type="lucide" name={iconName} variant="outline" size="lg" />
|
|
400
|
+
<p className="ml-4 font-mono text-sm">{iconName}</p>
|
|
401
|
+
</div>
|
|
402
|
+
))}
|
|
403
|
+
</div>
|
|
404
|
+
</div>
|
|
405
|
+
),
|
|
406
|
+
} satisfies StoryObj;
|
|
407
|
+
|
|
408
|
+
export const LucideIconBrowser = {
|
|
409
|
+
args: {},
|
|
410
|
+
render: () => {
|
|
411
|
+
const [names, setNames] = React.useState<string[]>([]);
|
|
412
|
+
const [filter, setFilter] = React.useState("");
|
|
413
|
+
const [loading, setLoading] = React.useState(true);
|
|
414
|
+
|
|
415
|
+
React.useEffect(() => {
|
|
416
|
+
import("@/icons").then(({ getLucideIconNames }) => {
|
|
417
|
+
getLucideIconNames().then((n) => {
|
|
418
|
+
setNames(n.sort());
|
|
419
|
+
setLoading(false);
|
|
420
|
+
});
|
|
421
|
+
});
|
|
422
|
+
}, []);
|
|
423
|
+
|
|
424
|
+
const filtered = filter
|
|
425
|
+
? names.filter((n) => n.toLowerCase().includes(filter.toLowerCase())).slice(0, 80)
|
|
426
|
+
: names.slice(0, 50);
|
|
427
|
+
|
|
428
|
+
return (
|
|
429
|
+
<div className="flex flex-col gap-4 p-4 max-h-[80vh] overflow-auto">
|
|
430
|
+
<h4>Lucide icon names ({names.length} total)</h4>
|
|
431
|
+
<input
|
|
432
|
+
type="text"
|
|
433
|
+
placeholder="Search icons..."
|
|
434
|
+
value={filter}
|
|
435
|
+
onChange={(e) => setFilter(e.target.value)}
|
|
436
|
+
className="px-3 py-2 border rounded w-64"
|
|
437
|
+
/>
|
|
438
|
+
{loading ? (
|
|
439
|
+
<p>Loading...</p>
|
|
440
|
+
) : (
|
|
441
|
+
<div className="grid grid-cols-[repeat(auto-fill,minmax(140px,1fr))] gap-2">
|
|
442
|
+
{filtered.map((name) => (
|
|
443
|
+
<div
|
|
444
|
+
key={name}
|
|
445
|
+
className="flex flex-col items-center gap-1 p-2 border rounded hover:bg-gray-50"
|
|
446
|
+
>
|
|
447
|
+
<Icon type="lucide" name={name} size="md" />
|
|
448
|
+
<span className="font-mono text-xs truncate w-full text-center">
|
|
449
|
+
{name}
|
|
450
|
+
</span>
|
|
451
|
+
</div>
|
|
452
|
+
))}
|
|
453
|
+
</div>
|
|
454
|
+
)}
|
|
455
|
+
</div>
|
|
456
|
+
);
|
|
457
|
+
},
|
|
458
|
+
} satisfies StoryObj;
|
|
459
|
+
|
|
371
460
|
export const PreviewMaterialIcon = {
|
|
372
461
|
args: {
|
|
373
462
|
// variant: "outline",
|
|
@@ -7,21 +7,23 @@ import { iconVariants } from "./Icon.styles";
|
|
|
7
7
|
|
|
8
8
|
export type IconProps = {
|
|
9
9
|
name: string;
|
|
10
|
-
type?: "heroicons" | "material" | "custom";
|
|
10
|
+
type?: "heroicons" | "material" | "lucide" | "custom";
|
|
11
11
|
color?:
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
12
|
+
| "primary"
|
|
13
|
+
| "secondary"
|
|
14
|
+
| "success"
|
|
15
|
+
| "tertiary"
|
|
16
|
+
| "info"
|
|
17
|
+
| "warning"
|
|
18
|
+
| "error"
|
|
19
|
+
| "inherit";
|
|
20
20
|
variant?: "solid" | "outline";
|
|
21
21
|
size?: "sm" | "md" | "lg" | "inherit";
|
|
22
22
|
className?: string;
|
|
23
23
|
} & React.SVGProps<SVGSVGElement>;
|
|
24
24
|
|
|
25
|
+
const LUCIDE_SIZE = { sm: 16, md: 24, lg: 32 } as const;
|
|
26
|
+
|
|
25
27
|
const Icon: React.FC<IconProps> = ({
|
|
26
28
|
name,
|
|
27
29
|
type = "heroicons",
|
|
@@ -36,25 +38,44 @@ const Icon: React.FC<IconProps> = ({
|
|
|
36
38
|
> | null>(null);
|
|
37
39
|
|
|
38
40
|
React.useEffect(() => {
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
41
|
+
if (type === "lucide") {
|
|
42
|
+
import("lucide-react/dynamicIconImports").then(({ default: dynamicIconImports }) => {
|
|
43
|
+
const loader = dynamicIconImports[name as keyof typeof dynamicIconImports];
|
|
44
|
+
if (loader) {
|
|
45
|
+
loader().then((m: { default: React.ComponentType<React.SVGProps<SVGSVGElement>>; }) => {
|
|
46
|
+
setIconComponent(() => m.default);
|
|
47
|
+
});
|
|
48
|
+
} else {
|
|
49
|
+
console.warn(`Lucide icon "${name}" not found.`);
|
|
50
|
+
}
|
|
51
|
+
});
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
const icon = getIcon(type, name, variant);
|
|
55
|
+
if (icon) setIconComponent(() => icon);
|
|
56
|
+
else console.warn(`Icon "${name}" from "${type}" not found.`);
|
|
49
57
|
}, [name, type, variant]);
|
|
50
58
|
|
|
51
|
-
if (
|
|
59
|
+
if (type === "lucide") {
|
|
60
|
+
if (!IconComponent) {
|
|
61
|
+
return <span className={cn(iconVariants({ color, size }), className)} />;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const sizeNum = size === "inherit" ? 24 : LUCIDE_SIZE[size];
|
|
52
65
|
return (
|
|
53
|
-
<
|
|
54
|
-
|
|
55
|
-
{
|
|
66
|
+
<IconComponent
|
|
67
|
+
{...(props as any)}
|
|
68
|
+
size={sizeNum}
|
|
69
|
+
strokeWidth={sizeNum <= 16 ? 1.5 : 2}
|
|
70
|
+
absoluteStrokeWidth
|
|
71
|
+
shapeRendering="geometricPrecision"
|
|
72
|
+
className={cn(iconVariants({ color, size }), "stroke-current", className)}
|
|
56
73
|
/>
|
|
57
74
|
);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
if (!IconComponent)
|
|
78
|
+
return <svg className={cn(iconVariants({ color, size }), className)} {...props} />;
|
|
58
79
|
|
|
59
80
|
return (
|
|
60
81
|
<IconComponent
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import React from "react";
|
|
2
2
|
import type { Meta, StoryObj } from "@storybook/react";
|
|
3
3
|
|
|
4
|
-
import { Checkbox } from "../Checkbox/Checkbox";
|
|
5
4
|
import Navbar from "./Navbar";
|
|
5
|
+
import { Footer } from "../Footer";
|
|
6
6
|
|
|
7
7
|
const meta = {
|
|
8
8
|
title: "Components/Navbar",
|
|
@@ -24,63 +24,117 @@ export default meta;
|
|
|
24
24
|
|
|
25
25
|
export const Default = {
|
|
26
26
|
args: {},
|
|
27
|
-
render: (args) =>
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
</
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
27
|
+
render: (args) => (
|
|
28
|
+
<div className="flex flex-row gap-4 w-full">
|
|
29
|
+
<Navbar
|
|
30
|
+
leftNav={
|
|
31
|
+
<ul className="flex gap-2">
|
|
32
|
+
<li>link 1</li>
|
|
33
|
+
<li>link 2</li>
|
|
34
|
+
<li>link 3</li>
|
|
35
|
+
</ul>
|
|
36
|
+
}
|
|
37
|
+
center={<>Center text</>}
|
|
38
|
+
rightNav={
|
|
39
|
+
<ul className="flex gap-2">
|
|
40
|
+
<li>sss 1</li>
|
|
41
|
+
<li>ddd 2</li>
|
|
42
|
+
<li>vvvv 3</li>
|
|
43
|
+
</ul>
|
|
44
|
+
}
|
|
45
|
+
/>
|
|
46
|
+
</div>
|
|
47
|
+
),
|
|
48
|
+
} satisfies StoryObj;
|
|
49
|
+
|
|
50
|
+
export const Transparent = {
|
|
51
|
+
args: {},
|
|
52
|
+
render: (args) => (
|
|
53
|
+
<div className="flex flex-col w-full min-h-screen bg-primary-5">
|
|
54
|
+
<Navbar
|
|
55
|
+
variant="transparent"
|
|
56
|
+
position="sticky"
|
|
57
|
+
scrollShadow
|
|
58
|
+
leftNav={
|
|
59
|
+
<ul className="flex gap-2">
|
|
60
|
+
<li>link 1</li>
|
|
61
|
+
<li>link 2</li>
|
|
62
|
+
</ul>
|
|
63
|
+
}
|
|
64
|
+
center={<>Transparent Navbar</>}
|
|
65
|
+
rightNav={
|
|
66
|
+
<ul className="flex gap-2">
|
|
67
|
+
<li>Menu 1</li>
|
|
68
|
+
<li>Menu 2</li>
|
|
69
|
+
</ul>
|
|
70
|
+
}
|
|
71
|
+
/>
|
|
72
|
+
<div className="flex-1 p-8">Scroll down to see shadow</div>
|
|
73
|
+
<Footer variant="transparent" copyright="© 2024" />
|
|
74
|
+
</div>
|
|
75
|
+
),
|
|
76
|
+
} satisfies StoryObj;
|
|
77
|
+
|
|
78
|
+
export const WithScrollShadow = {
|
|
79
|
+
args: {},
|
|
80
|
+
parameters: {
|
|
81
|
+
layout: "fullscreen",
|
|
53
82
|
},
|
|
83
|
+
decorators: [
|
|
84
|
+
(Story) => (
|
|
85
|
+
<div className="w-full h-[200vh]">
|
|
86
|
+
<div className="h-screen flex flex-col">
|
|
87
|
+
<Story />
|
|
88
|
+
<div className="flex-1 p-8 text-center">
|
|
89
|
+
Scroll down to see navbar shadow
|
|
90
|
+
</div>
|
|
91
|
+
</div>
|
|
92
|
+
</div>
|
|
93
|
+
),
|
|
94
|
+
],
|
|
95
|
+
render: (args) => (
|
|
96
|
+
<Navbar
|
|
97
|
+
position="sticky"
|
|
98
|
+
scrollShadow
|
|
99
|
+
leftNav={
|
|
100
|
+
<ul className="flex gap-2">
|
|
101
|
+
<li>link 1</li>
|
|
102
|
+
<li>link 2</li>
|
|
103
|
+
</ul>
|
|
104
|
+
}
|
|
105
|
+
center={<>Sticky + Scroll Shadow</>}
|
|
106
|
+
rightNav={
|
|
107
|
+
<ul className="flex gap-2">
|
|
108
|
+
<li>Menu 1</li>
|
|
109
|
+
<li>Menu 2</li>
|
|
110
|
+
</ul>
|
|
111
|
+
}
|
|
112
|
+
/>
|
|
113
|
+
),
|
|
54
114
|
} satisfies StoryObj;
|
|
55
115
|
|
|
56
116
|
export const Custom = {
|
|
57
117
|
args: {},
|
|
58
|
-
render: (args) =>
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
</
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
</ul>
|
|
81
|
-
}
|
|
82
|
-
/>
|
|
83
|
-
</div>
|
|
84
|
-
);
|
|
85
|
-
},
|
|
118
|
+
render: (args) => (
|
|
119
|
+
<div className="flex flex-row gap-4 w-full">
|
|
120
|
+
<Navbar
|
|
121
|
+
className="px-8"
|
|
122
|
+
leftNav={
|
|
123
|
+
<ul className="flex gap-2">
|
|
124
|
+
<li>link 1</li>
|
|
125
|
+
<li>link 2</li>
|
|
126
|
+
<li>link 3</li>
|
|
127
|
+
</ul>
|
|
128
|
+
}
|
|
129
|
+
center={<>Center text</>}
|
|
130
|
+
rightNav={
|
|
131
|
+
<ul className="flex gap-2">
|
|
132
|
+
<li>sss 1</li>
|
|
133
|
+
<li>ddd 2</li>
|
|
134
|
+
<li>vvvv 3</li>
|
|
135
|
+
</ul>
|
|
136
|
+
}
|
|
137
|
+
/>
|
|
138
|
+
</div>
|
|
139
|
+
),
|
|
86
140
|
} satisfies StoryObj;
|
|
@@ -1,7 +1,13 @@
|
|
|
1
1
|
import { cn } from "@/utils/cn";
|
|
2
|
-
import React, { FC, ReactNode } from "react";
|
|
2
|
+
import React, { FC, ReactNode, useEffect, useState } from "react";
|
|
3
|
+
|
|
4
|
+
export type NavbarVariant = "default" | "transparent";
|
|
3
5
|
|
|
4
6
|
export type NavbarProps = {
|
|
7
|
+
/** Appearance: default (solid bg) | transparent */
|
|
8
|
+
variant?: NavbarVariant;
|
|
9
|
+
/** Show shadow when page is scrolled (works with position="sticky") */
|
|
10
|
+
scrollShadow?: boolean;
|
|
5
11
|
position?: "static" | "sticky";
|
|
6
12
|
children?: ReactNode;
|
|
7
13
|
leftNav?: ReactNode;
|
|
@@ -15,9 +21,13 @@ export type NavbarProps = {
|
|
|
15
21
|
rightNavClassName?: string;
|
|
16
22
|
};
|
|
17
23
|
|
|
24
|
+
const SCROLL_THRESHOLD = 4;
|
|
25
|
+
|
|
18
26
|
const Navbar: FC<NavbarProps> = ({
|
|
19
27
|
children,
|
|
20
28
|
className,
|
|
29
|
+
variant = "default",
|
|
30
|
+
scrollShadow = false,
|
|
21
31
|
center,
|
|
22
32
|
leftNav,
|
|
23
33
|
rightNav,
|
|
@@ -28,17 +38,45 @@ const Navbar: FC<NavbarProps> = ({
|
|
|
28
38
|
centerClassName,
|
|
29
39
|
rightNavClassName,
|
|
30
40
|
}) => {
|
|
41
|
+
const [isScrolled, setIsScrolled] = useState(false);
|
|
42
|
+
|
|
43
|
+
useEffect(() => {
|
|
44
|
+
if (!scrollShadow || typeof window === "undefined") return;
|
|
45
|
+
|
|
46
|
+
const handleScroll = () => {
|
|
47
|
+
setIsScrolled(window.scrollY > SCROLL_THRESHOLD);
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
handleScroll(); // init
|
|
51
|
+
window.addEventListener("scroll", handleScroll, { passive: true });
|
|
52
|
+
return () => window.removeEventListener("scroll", handleScroll);
|
|
53
|
+
}, [scrollShadow]);
|
|
54
|
+
|
|
55
|
+
const isTransparent = variant === "transparent";
|
|
56
|
+
|
|
31
57
|
return (
|
|
32
58
|
<header
|
|
33
59
|
className={cn(
|
|
34
|
-
"w-full px-4 py-6 h-[var(--navbar-height)] box-border overflow-hidden typography-subtitile2 border-solid border-b-2
|
|
60
|
+
"relative w-full px-4 py-6 h-[var(--navbar-height)] box-border overflow-hidden typography-subtitile2 border-solid border-b-2 text-[rgb(var(--navbar-text-color))] border-b-[rgb(var(--navbar-border-color))] transition-shadow duration-200",
|
|
35
61
|
{ position },
|
|
62
|
+
scrollShadow && isScrolled && "shadow-[var(--navbar-shadow-scrolled)]",
|
|
36
63
|
className
|
|
37
64
|
)}
|
|
38
65
|
>
|
|
66
|
+
{/* Default bg (z:-10) + overlay for className override - hidden when transparent */}
|
|
67
|
+
{!isTransparent && (
|
|
68
|
+
<div
|
|
69
|
+
className="absolute inset-0 -z-10 bg-[rgb(var(--navbar-bg-color))]"
|
|
70
|
+
aria-hidden
|
|
71
|
+
/>
|
|
72
|
+
)}
|
|
73
|
+
<div
|
|
74
|
+
className={cn("absolute inset-0 -z-[5] pointer-events-none", className)}
|
|
75
|
+
aria-hidden
|
|
76
|
+
/>
|
|
39
77
|
<div
|
|
40
78
|
className={cn(
|
|
41
|
-
"mx-auto flex h-full justify-between items-center",
|
|
79
|
+
"relative mx-auto flex h-full justify-between items-center",
|
|
42
80
|
{
|
|
43
81
|
container,
|
|
44
82
|
},
|