@rovula/ui 0.1.0 → 0.1.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cjs/bundle.css +129 -0
- package/dist/cjs/bundle.js +9255 -3
- package/dist/cjs/bundle.js.map +1 -1
- package/dist/cjs/types/components/Dropdown/Dropdown.stories.d.ts +1 -0
- 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/InputFilter/InputFilter.stories.d.ts +1 -0
- package/dist/cjs/types/components/MaskedTextInput/MaskedTextInput.d.ts +1 -0
- package/dist/cjs/types/components/MaskedTextInput/MaskedTextInput.stories.d.ts +2 -0
- 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 +396 -0
- package/dist/cjs/types/components/PasswordInput/index.d.ts +2 -0
- package/dist/cjs/types/components/Search/Search.stories.d.ts +1 -0
- package/dist/cjs/types/components/TextInput/TextInput.d.ts +2 -0
- package/dist/cjs/types/components/TextInput/TextInput.stories.d.ts +10 -0
- package/dist/cjs/types/components/TextInput/TextInput.styles.d.ts +15 -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/components/TextArea/TextArea.styles.js +1 -1
- package/dist/components/TextInput/TextInput.js +33 -24
- package/dist/components/TextInput/TextInput.stories.js +14 -2
- package/dist/components/TextInput/TextInput.styles.js +25 -10
- package/dist/esm/bundle.css +129 -0
- package/dist/esm/bundle.js +9255 -3
- package/dist/esm/bundle.js.map +1 -1
- package/dist/esm/types/components/Dropdown/Dropdown.stories.d.ts +1 -0
- 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/InputFilter/InputFilter.stories.d.ts +1 -0
- package/dist/esm/types/components/MaskedTextInput/MaskedTextInput.d.ts +1 -0
- package/dist/esm/types/components/MaskedTextInput/MaskedTextInput.stories.d.ts +2 -0
- 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 +396 -0
- package/dist/esm/types/components/PasswordInput/index.d.ts +2 -0
- package/dist/esm/types/components/Search/Search.stories.d.ts +1 -0
- package/dist/esm/types/components/TextInput/TextInput.d.ts +2 -0
- package/dist/esm/types/components/TextInput/TextInput.stories.d.ts +10 -0
- package/dist/esm/types/components/TextInput/TextInput.styles.d.ts +15 -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 +389 -2
- package/dist/index.js +4 -0
- package/dist/src/theme/global.css +200 -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/components/TextArea/TextArea.styles.ts +1 -1
- package/src/components/TextInput/TextInput.stories.tsx +60 -2
- package/src/components/TextInput/TextInput.styles.ts +36 -19
- package/src/components/TextInput/TextInput.tsx +83 -55
- 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
|
@@ -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
|
},
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import type { Meta, StoryObj } from "@storybook/react";
|
|
3
|
+
import PasswordInput from "./PasswordInput";
|
|
4
|
+
|
|
5
|
+
const meta = {
|
|
6
|
+
title: "Components/PasswordInput",
|
|
7
|
+
component: PasswordInput,
|
|
8
|
+
tags: ["autodocs"],
|
|
9
|
+
parameters: {
|
|
10
|
+
layout: "fullscreen",
|
|
11
|
+
},
|
|
12
|
+
decorators: [
|
|
13
|
+
(Story) => (
|
|
14
|
+
<div className="p-5 flex w-full bg-[rgb(var(--base-bg-2))]">
|
|
15
|
+
<Story />
|
|
16
|
+
</div>
|
|
17
|
+
),
|
|
18
|
+
],
|
|
19
|
+
} satisfies Meta<typeof PasswordInput>;
|
|
20
|
+
|
|
21
|
+
export default meta;
|
|
22
|
+
|
|
23
|
+
export const Default = {
|
|
24
|
+
args: {
|
|
25
|
+
label: "Password",
|
|
26
|
+
placeholder: "Enter your password",
|
|
27
|
+
fullwidth: true,
|
|
28
|
+
},
|
|
29
|
+
render: (args) => (
|
|
30
|
+
<div className="flex flex-col gap-4 w-full max-w-md">
|
|
31
|
+
<PasswordInput id="1" size="lg" {...args} />
|
|
32
|
+
<PasswordInput id="2" size="md" {...args} />
|
|
33
|
+
<PasswordInput id="3" size="sm" {...args} />
|
|
34
|
+
</div>
|
|
35
|
+
),
|
|
36
|
+
} satisfies StoryObj;
|
|
37
|
+
|
|
38
|
+
export const WithHelperText = {
|
|
39
|
+
args: {
|
|
40
|
+
label: "Password",
|
|
41
|
+
placeholder: "Enter your password",
|
|
42
|
+
helperText: "Must be at least 8 characters",
|
|
43
|
+
fullwidth: true,
|
|
44
|
+
},
|
|
45
|
+
render: (args) => (
|
|
46
|
+
<div className="w-full max-w-md">
|
|
47
|
+
<PasswordInput id="1" size="md" {...args} />
|
|
48
|
+
</div>
|
|
49
|
+
),
|
|
50
|
+
} satisfies StoryObj;
|
|
51
|
+
|
|
52
|
+
export const WithError = {
|
|
53
|
+
args: {
|
|
54
|
+
label: "Password",
|
|
55
|
+
placeholder: "Enter your password",
|
|
56
|
+
error: true,
|
|
57
|
+
errorMessage: "Password is required",
|
|
58
|
+
fullwidth: true,
|
|
59
|
+
},
|
|
60
|
+
render: (args) => (
|
|
61
|
+
<div className="w-full max-w-md">
|
|
62
|
+
<PasswordInput id="1" size="md" {...args} />
|
|
63
|
+
</div>
|
|
64
|
+
),
|
|
65
|
+
} satisfies StoryObj;
|
|
66
|
+
|
|
67
|
+
export const Disabled = {
|
|
68
|
+
args: {
|
|
69
|
+
label: "Password",
|
|
70
|
+
value: "secret123",
|
|
71
|
+
disabled: true,
|
|
72
|
+
fullwidth: true,
|
|
73
|
+
},
|
|
74
|
+
render: (args) => (
|
|
75
|
+
<div className="w-full max-w-md">
|
|
76
|
+
<PasswordInput id="1" size="md" {...args} />
|
|
77
|
+
</div>
|
|
78
|
+
),
|
|
79
|
+
} satisfies StoryObj;
|
|
80
|
+
|
|
81
|
+
export const CustomIcons = {
|
|
82
|
+
args: {
|
|
83
|
+
label: "Password",
|
|
84
|
+
placeholder: "Enter your password",
|
|
85
|
+
fullwidth: true,
|
|
86
|
+
},
|
|
87
|
+
render: (args) => (
|
|
88
|
+
<div className="w-full max-w-md">
|
|
89
|
+
<PasswordInput
|
|
90
|
+
{...args}
|
|
91
|
+
id="1"
|
|
92
|
+
hideIcon={<span className="text-sm">👁</span>}
|
|
93
|
+
showIcon={<span className="text-sm">🙈</span>}
|
|
94
|
+
/>
|
|
95
|
+
</div>
|
|
96
|
+
),
|
|
97
|
+
} satisfies StoryObj;
|
|
98
|
+
|
|
99
|
+
export const WithoutToggle = {
|
|
100
|
+
args: {
|
|
101
|
+
label: "Password",
|
|
102
|
+
placeholder: "Enter your password",
|
|
103
|
+
showToggle: false,
|
|
104
|
+
fullwidth: true,
|
|
105
|
+
},
|
|
106
|
+
render: (args) => (
|
|
107
|
+
<div className="w-full max-w-md">
|
|
108
|
+
<PasswordInput id="1" size="md" {...args} />
|
|
109
|
+
</div>
|
|
110
|
+
),
|
|
111
|
+
} satisfies StoryObj;
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import React, { forwardRef, useImperativeHandle, useRef, useState } from "react";
|
|
2
|
+
import TextInput, { InputProps } from "../TextInput/TextInput";
|
|
3
|
+
import Icon from "../Icon/Icon";
|
|
4
|
+
|
|
5
|
+
export type PasswordInputProps = Omit<InputProps, "type"> & {
|
|
6
|
+
/** Show toggle visibility button. Default: true */
|
|
7
|
+
showToggle?: boolean;
|
|
8
|
+
/** Icon when password is hidden (click to reveal). Default: Icon eye (lucide) */
|
|
9
|
+
hideIcon?: React.ReactNode;
|
|
10
|
+
/** Icon when password is visible (click to hide). Default: Icon eye-off (lucide) */
|
|
11
|
+
showIcon?: React.ReactNode;
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
export const PasswordInput = forwardRef<HTMLInputElement, PasswordInputProps>(
|
|
15
|
+
({ showToggle = true, hasClearIcon = false, hideIcon, showIcon, endIcon, renderEndIcon, onClickEndIcon, ...props }, ref) => {
|
|
16
|
+
const inputRef = useRef<HTMLInputElement>(null);
|
|
17
|
+
const [visible, setVisible] = useState(false);
|
|
18
|
+
|
|
19
|
+
useImperativeHandle(ref, () => inputRef?.current as HTMLInputElement);
|
|
20
|
+
|
|
21
|
+
const handleToggleVisibility = () => {
|
|
22
|
+
if (props.disabled) return;
|
|
23
|
+
setVisible((v) => !v);
|
|
24
|
+
inputRef.current?.focus();
|
|
25
|
+
onClickEndIcon?.();
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
const toggleIcon = visible
|
|
29
|
+
? showIcon ?? <Icon name="eye" type="lucide" size="inherit" className="size-full stroke-input-default-stroke hover:stroke-input-active-stroke" aria-label="Hide password" />
|
|
30
|
+
: hideIcon ?? <Icon name="eye-closed" type="lucide" size="inherit" className="size-full stroke-input-default-stroke hover:stroke-input-active-stroke" aria-label="Show password" />;
|
|
31
|
+
|
|
32
|
+
return (
|
|
33
|
+
<TextInput
|
|
34
|
+
iconMode="flat"
|
|
35
|
+
{...props}
|
|
36
|
+
ref={inputRef}
|
|
37
|
+
type={visible ? "text" : "password"}
|
|
38
|
+
hasClearIcon={hasClearIcon}
|
|
39
|
+
endIcon={showToggle ? toggleIcon : endIcon}
|
|
40
|
+
renderEndIcon={renderEndIcon}
|
|
41
|
+
onClickEndIcon={showToggle ? handleToggleVisibility : onClickEndIcon}
|
|
42
|
+
autoComplete={props.autoComplete ?? "current-password"}
|
|
43
|
+
/>
|
|
44
|
+
);
|
|
45
|
+
}
|
|
46
|
+
);
|
|
47
|
+
|
|
48
|
+
PasswordInput.displayName = "PasswordInput";
|
|
49
|
+
|
|
50
|
+
export default PasswordInput;
|