@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.
Files changed (70) hide show
  1. package/dist/cjs/bundle.css +65 -0
  2. package/dist/cjs/bundle.js +9261 -3
  3. package/dist/cjs/bundle.js.map +1 -1
  4. package/dist/cjs/types/components/Footer/Footer.d.ts +21 -0
  5. package/dist/cjs/types/components/Footer/Footer.stories.d.ts +45 -0
  6. package/dist/cjs/types/components/Footer/index.d.ts +2 -0
  7. package/dist/cjs/types/components/Icon/Icon.d.ts +1 -1
  8. package/dist/cjs/types/components/Icon/Icon.stories.d.ts +9 -1
  9. package/dist/cjs/types/components/Navbar/Navbar.d.ts +5 -0
  10. package/dist/cjs/types/components/Navbar/Navbar.stories.d.ts +14 -0
  11. package/dist/cjs/types/components/PasswordInput/PasswordInput.d.ts +19 -0
  12. package/dist/cjs/types/components/PasswordInput/PasswordInput.stories.d.ts +395 -0
  13. package/dist/cjs/types/components/PasswordInput/index.d.ts +2 -0
  14. package/dist/cjs/types/icons/index.d.ts +1 -0
  15. package/dist/cjs/types/icons/lucideIconNames.d.ts +9 -0
  16. package/dist/cjs/types/index.d.ts +7 -1
  17. package/dist/cjs/types/utils/colors.d.ts +330 -0
  18. package/dist/components/Footer/Footer.js +11 -0
  19. package/dist/components/Footer/Footer.stories.js +34 -0
  20. package/dist/components/Footer/index.js +2 -0
  21. package/dist/components/Icon/Icon.js +28 -11
  22. package/dist/components/Icon/Icon.stories.js +39 -0
  23. package/dist/components/Navbar/Navbar.js +18 -4
  24. package/dist/components/Navbar/Navbar.stories.js +16 -9
  25. package/dist/components/PasswordInput/PasswordInput.js +36 -0
  26. package/dist/components/PasswordInput/PasswordInput.stories.js +67 -0
  27. package/dist/components/PasswordInput/index.js +1 -0
  28. package/dist/esm/bundle.css +65 -0
  29. package/dist/esm/bundle.js +9261 -3
  30. package/dist/esm/bundle.js.map +1 -1
  31. package/dist/esm/types/components/Footer/Footer.d.ts +21 -0
  32. package/dist/esm/types/components/Footer/Footer.stories.d.ts +45 -0
  33. package/dist/esm/types/components/Footer/index.d.ts +2 -0
  34. package/dist/esm/types/components/Icon/Icon.d.ts +1 -1
  35. package/dist/esm/types/components/Icon/Icon.stories.d.ts +9 -1
  36. package/dist/esm/types/components/Navbar/Navbar.d.ts +5 -0
  37. package/dist/esm/types/components/Navbar/Navbar.stories.d.ts +14 -0
  38. package/dist/esm/types/components/PasswordInput/PasswordInput.d.ts +19 -0
  39. package/dist/esm/types/components/PasswordInput/PasswordInput.stories.d.ts +395 -0
  40. package/dist/esm/types/components/PasswordInput/index.d.ts +2 -0
  41. package/dist/esm/types/icons/index.d.ts +1 -0
  42. package/dist/esm/types/icons/lucideIconNames.d.ts +9 -0
  43. package/dist/esm/types/index.d.ts +7 -1
  44. package/dist/esm/types/utils/colors.d.ts +330 -0
  45. package/dist/icons/index.js +1 -0
  46. package/dist/icons/lucideIconNames.js +12 -0
  47. package/dist/index.d.ts +386 -2
  48. package/dist/index.js +4 -0
  49. package/dist/src/theme/global.css +117 -24
  50. package/dist/utils/colors.js +369 -0
  51. package/package.json +2 -1
  52. package/src/components/Footer/Footer.stories.tsx +119 -0
  53. package/src/components/Footer/Footer.tsx +122 -0
  54. package/src/components/Footer/index.ts +3 -0
  55. package/src/components/Icon/Icon.stories.tsx +89 -0
  56. package/src/components/Icon/Icon.tsx +44 -23
  57. package/src/components/Navbar/Navbar.stories.tsx +109 -55
  58. package/src/components/Navbar/Navbar.tsx +41 -3
  59. package/src/components/PasswordInput/PasswordInput.stories.tsx +111 -0
  60. package/src/components/PasswordInput/PasswordInput.tsx +50 -0
  61. package/src/components/PasswordInput/index.ts +2 -0
  62. package/src/icons/index.ts +1 -0
  63. package/src/icons/lucideIconNames.ts +14 -0
  64. package/src/index.ts +15 -1
  65. package/src/theme/themes/skyller/typography.css +24 -24
  66. package/src/theme/tokens/baseline.css +1 -0
  67. package/src/theme/tokens/components/footer.css +9 -0
  68. package/src/theme/tokens/components/navbar.css +2 -1
  69. package/src/types/lucide-react.d.ts +5 -0
  70. 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;
@@ -0,0 +1,3 @@
1
+ import Footer from "./Footer";
2
+
3
+ export { 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
- | "primary"
13
- | "secondary"
14
- | "success"
15
- | "tertiary"
16
- | "info"
17
- | "warning"
18
- | "error"
19
- | "inherit";
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
- const loadIcon = async () => {
40
- const icon = getIcon(type, name, variant);
41
- if (icon) {
42
- setIconComponent(() => icon);
43
- } else {
44
- console.warn(`Icon "${name}" from "${type}" not found.`);
45
- }
46
- };
47
-
48
- loadIcon();
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 (!IconComponent)
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
- <svg
54
- className={cn(iconVariants({ color, size }), className)}
55
- {...props}
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
- console.log("args ", args);
29
- const props: typeof args = {
30
- ...args,
31
- };
32
- return (
33
- <div className="flex flex-row gap-4 w-full">
34
- <Navbar
35
- leftNav={
36
- <ul className="flex gap-2">
37
- <li>link 1</li>
38
- <li>link 2</li>
39
- <li>link 3</li>
40
- </ul>
41
- }
42
- center={<>Center text</>}
43
- rightNav={
44
- <ul className="flex gap-2">
45
- <li>sss 1</li>
46
- <li>ddd 2</li>
47
- <li>vvvv 3</li>
48
- </ul>
49
- }
50
- />
51
- </div>
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
- console.log("args ", args);
60
- const props: typeof args = {
61
- ...args,
62
- };
63
- return (
64
- <div className="flex flex-row gap-4 w-full">
65
- <Navbar
66
- className="px-8"
67
- leftNav={
68
- <ul className="flex gap-2">
69
- <li>link 1</li>
70
- <li>link 2</li>
71
- <li>link 3</li>
72
- </ul>
73
- }
74
- center={<>Center text</>}
75
- rightNav={
76
- <ul className="flex gap-2">
77
- <li>sss 1</li>
78
- <li>ddd 2</li>
79
- <li>vvvv 3</li>
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 bg-[rgb(var(--navbar-bg-color))] text-[rgb(var(--navbar-text-color))] border-b-[rgb(var(--navbar-border-color))]",
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
  },