@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.
Files changed (94) hide show
  1. package/dist/cjs/bundle.css +129 -0
  2. package/dist/cjs/bundle.js +9255 -3
  3. package/dist/cjs/bundle.js.map +1 -1
  4. package/dist/cjs/types/components/Dropdown/Dropdown.stories.d.ts +1 -0
  5. package/dist/cjs/types/components/Footer/Footer.d.ts +21 -0
  6. package/dist/cjs/types/components/Footer/Footer.stories.d.ts +45 -0
  7. package/dist/cjs/types/components/Footer/index.d.ts +2 -0
  8. package/dist/cjs/types/components/Icon/Icon.d.ts +1 -1
  9. package/dist/cjs/types/components/Icon/Icon.stories.d.ts +9 -1
  10. package/dist/cjs/types/components/InputFilter/InputFilter.stories.d.ts +1 -0
  11. package/dist/cjs/types/components/MaskedTextInput/MaskedTextInput.d.ts +1 -0
  12. package/dist/cjs/types/components/MaskedTextInput/MaskedTextInput.stories.d.ts +2 -0
  13. package/dist/cjs/types/components/Navbar/Navbar.d.ts +5 -0
  14. package/dist/cjs/types/components/Navbar/Navbar.stories.d.ts +14 -0
  15. package/dist/cjs/types/components/PasswordInput/PasswordInput.d.ts +19 -0
  16. package/dist/cjs/types/components/PasswordInput/PasswordInput.stories.d.ts +396 -0
  17. package/dist/cjs/types/components/PasswordInput/index.d.ts +2 -0
  18. package/dist/cjs/types/components/Search/Search.stories.d.ts +1 -0
  19. package/dist/cjs/types/components/TextInput/TextInput.d.ts +2 -0
  20. package/dist/cjs/types/components/TextInput/TextInput.stories.d.ts +10 -0
  21. package/dist/cjs/types/components/TextInput/TextInput.styles.d.ts +15 -0
  22. package/dist/cjs/types/icons/index.d.ts +1 -0
  23. package/dist/cjs/types/icons/lucideIconNames.d.ts +9 -0
  24. package/dist/cjs/types/index.d.ts +7 -1
  25. package/dist/cjs/types/utils/colors.d.ts +330 -0
  26. package/dist/components/Footer/Footer.js +11 -0
  27. package/dist/components/Footer/Footer.stories.js +34 -0
  28. package/dist/components/Footer/index.js +2 -0
  29. package/dist/components/Icon/Icon.js +28 -11
  30. package/dist/components/Icon/Icon.stories.js +39 -0
  31. package/dist/components/Navbar/Navbar.js +18 -4
  32. package/dist/components/Navbar/Navbar.stories.js +16 -9
  33. package/dist/components/PasswordInput/PasswordInput.js +36 -0
  34. package/dist/components/PasswordInput/PasswordInput.stories.js +67 -0
  35. package/dist/components/PasswordInput/index.js +1 -0
  36. package/dist/components/TextArea/TextArea.styles.js +1 -1
  37. package/dist/components/TextInput/TextInput.js +33 -24
  38. package/dist/components/TextInput/TextInput.stories.js +14 -2
  39. package/dist/components/TextInput/TextInput.styles.js +25 -10
  40. package/dist/esm/bundle.css +129 -0
  41. package/dist/esm/bundle.js +9255 -3
  42. package/dist/esm/bundle.js.map +1 -1
  43. package/dist/esm/types/components/Dropdown/Dropdown.stories.d.ts +1 -0
  44. package/dist/esm/types/components/Footer/Footer.d.ts +21 -0
  45. package/dist/esm/types/components/Footer/Footer.stories.d.ts +45 -0
  46. package/dist/esm/types/components/Footer/index.d.ts +2 -0
  47. package/dist/esm/types/components/Icon/Icon.d.ts +1 -1
  48. package/dist/esm/types/components/Icon/Icon.stories.d.ts +9 -1
  49. package/dist/esm/types/components/InputFilter/InputFilter.stories.d.ts +1 -0
  50. package/dist/esm/types/components/MaskedTextInput/MaskedTextInput.d.ts +1 -0
  51. package/dist/esm/types/components/MaskedTextInput/MaskedTextInput.stories.d.ts +2 -0
  52. package/dist/esm/types/components/Navbar/Navbar.d.ts +5 -0
  53. package/dist/esm/types/components/Navbar/Navbar.stories.d.ts +14 -0
  54. package/dist/esm/types/components/PasswordInput/PasswordInput.d.ts +19 -0
  55. package/dist/esm/types/components/PasswordInput/PasswordInput.stories.d.ts +396 -0
  56. package/dist/esm/types/components/PasswordInput/index.d.ts +2 -0
  57. package/dist/esm/types/components/Search/Search.stories.d.ts +1 -0
  58. package/dist/esm/types/components/TextInput/TextInput.d.ts +2 -0
  59. package/dist/esm/types/components/TextInput/TextInput.stories.d.ts +10 -0
  60. package/dist/esm/types/components/TextInput/TextInput.styles.d.ts +15 -0
  61. package/dist/esm/types/icons/index.d.ts +1 -0
  62. package/dist/esm/types/icons/lucideIconNames.d.ts +9 -0
  63. package/dist/esm/types/index.d.ts +7 -1
  64. package/dist/esm/types/utils/colors.d.ts +330 -0
  65. package/dist/icons/index.js +1 -0
  66. package/dist/icons/lucideIconNames.js +12 -0
  67. package/dist/index.d.ts +389 -2
  68. package/dist/index.js +4 -0
  69. package/dist/src/theme/global.css +200 -24
  70. package/dist/utils/colors.js +369 -0
  71. package/package.json +2 -1
  72. package/src/components/Footer/Footer.stories.tsx +119 -0
  73. package/src/components/Footer/Footer.tsx +122 -0
  74. package/src/components/Footer/index.ts +3 -0
  75. package/src/components/Icon/Icon.stories.tsx +89 -0
  76. package/src/components/Icon/Icon.tsx +44 -23
  77. package/src/components/Navbar/Navbar.stories.tsx +109 -55
  78. package/src/components/Navbar/Navbar.tsx +41 -3
  79. package/src/components/PasswordInput/PasswordInput.stories.tsx +111 -0
  80. package/src/components/PasswordInput/PasswordInput.tsx +50 -0
  81. package/src/components/PasswordInput/index.ts +2 -0
  82. package/src/components/TextArea/TextArea.styles.ts +1 -1
  83. package/src/components/TextInput/TextInput.stories.tsx +60 -2
  84. package/src/components/TextInput/TextInput.styles.ts +36 -19
  85. package/src/components/TextInput/TextInput.tsx +83 -55
  86. package/src/icons/index.ts +1 -0
  87. package/src/icons/lucideIconNames.ts +14 -0
  88. package/src/index.ts +15 -1
  89. package/src/theme/themes/skyller/typography.css +24 -24
  90. package/src/theme/tokens/baseline.css +1 -0
  91. package/src/theme/tokens/components/footer.css +9 -0
  92. package/src/theme/tokens/components/navbar.css +2 -1
  93. package/src/types/lucide-react.d.ts +5 -0
  94. 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
- | "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
  },
@@ -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;
@@ -0,0 +1,2 @@
1
+ export { default } from "./PasswordInput";
2
+ export type { PasswordInputProps } from "./PasswordInput";
@@ -203,7 +203,7 @@ export const clearIconVariant = cva(
203
203
  );
204
204
 
205
205
  export const helperTextVariant = cva(
206
- ["text-small1 flex flex-row items-center gap-1"],
206
+ ["typography-small1 flex flex-row items-center gap-1"],
207
207
  {
208
208
  variants: {
209
209
  size: {