@esic-lab/data-core-ui 0.0.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 (59) hide show
  1. package/.storybook/main.ts +12 -0
  2. package/.storybook/preview.ts +16 -0
  3. package/.storybook/vitest.setup.ts +7 -0
  4. package/LICENSE +21 -0
  5. package/package.json +52 -0
  6. package/postcss.config.js +5 -0
  7. package/src/Button/GhostButton/GhostButton.stories.tsx +20 -0
  8. package/src/Button/GhostButton/GhostButton.tsx +26 -0
  9. package/src/Button/PrimaryButton/PrimaryButton.stories.tsx +21 -0
  10. package/src/Button/PrimaryButton/PrimaryButton.tsx +35 -0
  11. package/src/Button/SecondaryButton/SecondaryButton.stories.tsx +16 -0
  12. package/src/Button/SecondaryButton/SecondaryButton.tsx +26 -0
  13. package/src/Checkbox/Checkbox/Checkbox.stories.tsx +17 -0
  14. package/src/Checkbox/Checkbox/Checkbox.tsx +27 -0
  15. package/src/Checkbox/CheckboxGroup/CheckboxGroup.stories.tsx +35 -0
  16. package/src/Checkbox/CheckboxGroup/CheckboxGroup.tsx +23 -0
  17. package/src/Loader/Loader/Loader.stories.tsx +16 -0
  18. package/src/Loader/Loader/Loader.tsx +12 -0
  19. package/src/NavBar/MenuNavBar/MenuNavBar.stories.tsx +85 -0
  20. package/src/NavBar/MenuNavBar/MenuNavBar.tsx +43 -0
  21. package/src/NavBar/TopNavBar/TopNavBar.stories.tsx +16 -0
  22. package/src/NavBar/TopNavBar/TopNavBar.tsx +24 -0
  23. package/src/Radio/Radio/Radio.stories.tsx +17 -0
  24. package/src/Radio/Radio/Radio.tsx +15 -0
  25. package/src/Radio/RadioGroup/RadioGroup.stories.tsx +23 -0
  26. package/src/Radio/RadioGroup/RadioGroup.tsx +27 -0
  27. package/src/Switch/Switch/Switch.stories.tsx +18 -0
  28. package/src/Switch/Switch/Switch.tsx +25 -0
  29. package/src/assets/STO-logo.svg +92 -0
  30. package/src/index.css +260 -0
  31. package/src/index.ts +24 -0
  32. package/stories/Button.stories.ts +49 -0
  33. package/stories/Button.tsx +33 -0
  34. package/stories/Configure.mdx +364 -0
  35. package/stories/Header.stories.ts +29 -0
  36. package/stories/Header.tsx +47 -0
  37. package/stories/Page.stories.ts +28 -0
  38. package/stories/Page.tsx +69 -0
  39. package/stories/assets/accessibility.png +0 -0
  40. package/stories/assets/accessibility.svg +1 -0
  41. package/stories/assets/addon-library.png +0 -0
  42. package/stories/assets/assets.png +0 -0
  43. package/stories/assets/avif-test-image.avif +0 -0
  44. package/stories/assets/context.png +0 -0
  45. package/stories/assets/discord.svg +1 -0
  46. package/stories/assets/docs.png +0 -0
  47. package/stories/assets/figma-plugin.png +0 -0
  48. package/stories/assets/github.svg +1 -0
  49. package/stories/assets/share.png +0 -0
  50. package/stories/assets/styling.png +0 -0
  51. package/stories/assets/testing.png +0 -0
  52. package/stories/assets/theming.png +0 -0
  53. package/stories/assets/tutorials.svg +1 -0
  54. package/stories/assets/youtube.svg +1 -0
  55. package/stories/button.css +30 -0
  56. package/stories/header.css +32 -0
  57. package/stories/page.css +68 -0
  58. package/tsconfig.json +16 -0
  59. package/vitest.config.js +35 -0
@@ -0,0 +1,12 @@
1
+ import type { StorybookConfig } from "@storybook/react-vite";
2
+
3
+ const config: StorybookConfig = {
4
+ stories: ["../src/**/*.stories.@(ts|tsx|js|jsx|mdx)"],
5
+ addons: ["@storybook/addon-links", "@storybook/addon-essentials", "@storybook/addon-interactions"],
6
+ framework: {
7
+ name: "@storybook/react-vite",
8
+ options: {},
9
+ },
10
+ };
11
+
12
+ export default config;
@@ -0,0 +1,16 @@
1
+ import type { Preview } from "@storybook/react";
2
+ import "../src/index.css";
3
+
4
+ const preview: Preview = {
5
+ parameters: {
6
+ actions: { argTypesRegex: "^on[A-Z].*" },
7
+ controls: {
8
+ matchers: {
9
+ color: /(background|color)$/i,
10
+ date: /Date$/,
11
+ },
12
+ },
13
+ },
14
+ };
15
+
16
+ export default preview;
@@ -0,0 +1,7 @@
1
+ import * as a11yAddonAnnotations from "@storybook/addon-a11y/preview";
2
+ import { setProjectAnnotations } from '@storybook/react-vite';
3
+ import * as projectAnnotations from './preview';
4
+
5
+ // This is an important step to apply the right configuration when testing your stories.
6
+ // More info at: https://storybook.js.org/docs/api/portable-stories/portable-stories-vitest#setprojectannotations
7
+ setProjectAnnotations([a11yAddonAnnotations, projectAnnotations]);
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 ESIC Lab
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/package.json ADDED
@@ -0,0 +1,52 @@
1
+ {
2
+ "name": "@esic-lab/data-core-ui",
3
+ "version": "0.0.2",
4
+ "main": "index.js",
5
+ "devDependencies": {
6
+ "@chromatic-com/storybook": "^4.1.1",
7
+ "@iconify/react": "^6.0.1",
8
+ "@storybook/addon-a11y": "^9.1.3",
9
+ "@storybook/addon-docs": "^9.1.3",
10
+ "@storybook/addon-styling-webpack": "^2.0.0",
11
+ "@storybook/addon-vitest": "^9.1.3",
12
+ "@storybook/react-vite": "^9.1.3",
13
+ "@tailwindcss/postcss": "^4.1.12",
14
+ "@types/node": "^24.3.0",
15
+ "@types/react": "^19.1.11",
16
+ "@types/react-dom": "^19.1.8",
17
+ "@vitest/browser": "^3.2.4",
18
+ "@vitest/coverage-v8": "^3.2.4",
19
+ "playwright": "^1.55.0",
20
+ "postcss": "^8.5.6",
21
+ "prop-types": "^15.8.1",
22
+ "storybook": "^9.1.3",
23
+ "tailwindcss": "^4.1.12",
24
+ "ts-node": "^10.9.2",
25
+ "typescript": "^5.9.2",
26
+ "vitest": "^3.2.4"
27
+ },
28
+ "scripts": {
29
+ "test": "echo \"Error: no test specified\" && exit 1",
30
+ "storybook": "storybook dev -p 6006",
31
+ "build-storybook": "storybook build"
32
+ },
33
+ "keywords": [],
34
+ "author": "",
35
+ "license": "MIT",
36
+ "description": "STO-core-UI",
37
+ "dependencies": {
38
+ "@tabler/icons-react": "^3.34.1",
39
+ "classnames": "^2.5.1"
40
+ },
41
+ "publishConfig": {
42
+ "access": "public"
43
+ },
44
+ "repository": {
45
+ "type": "git",
46
+ "url": "git+https://github.com/ESICLab/STO-core-UI.git"
47
+ },
48
+ "bugs": {
49
+ "url": "https://github.com/ESICLab/STO-core-UI/issues"
50
+ },
51
+ "homepage": "https://github.com/ESICLab/STO-core-UI#readme"
52
+ }
@@ -0,0 +1,5 @@
1
+ module.exports = {
2
+ plugins: {
3
+ '@tailwindcss/postcss': {},
4
+ },
5
+ }
@@ -0,0 +1,20 @@
1
+ import type { Meta, StoryObj } from "@storybook/react";
2
+
3
+ import { GhostButton } from "./GhostButton";
4
+
5
+ import { IconPlus } from "@tabler/icons-react";
6
+
7
+ const meta = {
8
+ component: GhostButton,
9
+ } satisfies Meta<typeof GhostButton>;
10
+
11
+ export default meta;
12
+ type Story = StoryObj<typeof meta>;
13
+
14
+ export const Default: Story = {
15
+ args: {
16
+ title: "สร้างโครงการ",
17
+ onClick: () => alert("onClick"),
18
+ iconLeft: <IconPlus size={16} />,
19
+ },
20
+ };
@@ -0,0 +1,26 @@
1
+ type ColorScale = 50 | 100 | 200 | 300 | 400 | 500 | 600 | 700 | 800 | 900;
2
+ type BaseColor = "primary" | "gray" | "green" | "red" | "yellow" | "blue";
3
+ type ThemeColor = `bg-${BaseColor}-${ColorScale}`;
4
+
5
+ interface GhostButtonProps {
6
+ title: string;
7
+ onClick: () => void;
8
+ iconLeft?: React.ReactNode;
9
+ iconRight?: React.ReactNode;
10
+ }
11
+
12
+ //TODO: disabled button, on hover, loading
13
+ export function GhostButton({ title, onClick, iconLeft, iconRight }: GhostButtonProps) {
14
+ return (
15
+ <button
16
+ className={`flex justify-center w-full h-[42px] rounded-[6px] p-[10px] cursor-pointer bg-[#E9E9E9]`}
17
+ onClick={onClick}
18
+ >
19
+ <div className="flex justify-center items-center gap-[10px]">
20
+ {iconLeft && <div>{iconLeft}</div>}
21
+ {title}
22
+ {iconRight && <div>{iconRight}</div>}
23
+ </div>
24
+ </button>
25
+ );
26
+ }
@@ -0,0 +1,21 @@
1
+ import type { Meta, StoryObj } from "@storybook/react";
2
+
3
+ import { PrimaryButton } from "./PrimaryButton";
4
+
5
+ import { IconPlus } from "@tabler/icons-react";
6
+
7
+ const meta = {
8
+ component: PrimaryButton,
9
+ } satisfies Meta<typeof PrimaryButton>;
10
+
11
+ export default meta;
12
+ type Story = StoryObj<typeof meta>;
13
+
14
+ export const Default: Story = {
15
+ args: {
16
+ title: "สร้างโครงการ",
17
+ onClick: () => alert("onClick"),
18
+ iconLeft: <IconPlus size={16} />,
19
+ textColor: "white",
20
+ },
21
+ };
@@ -0,0 +1,35 @@
1
+ type ColorScale = 50 | 100 | 200 | 300 | 400 | 500 | 600 | 700 | 800 | 900;
2
+ type BaseColor = "primary" | "gray" | "green" | "red" | "yellow" | "blue";
3
+ type ThemeColor = `bg-${BaseColor}-${ColorScale}`;
4
+
5
+ interface PrimaryButtonProps {
6
+ title: string;
7
+ onClick: () => void;
8
+ iconLeft?: React.ReactNode;
9
+ iconRight?: React.ReactNode;
10
+ bgColor?: ThemeColor;
11
+ textColor?: "white" | "black";
12
+ }
13
+
14
+ //TODO: disabled button, on hover, loading
15
+ export function PrimaryButton({
16
+ title,
17
+ onClick,
18
+ iconLeft,
19
+ iconRight,
20
+ bgColor = "bg-primary-500",
21
+ textColor = "black",
22
+ }: PrimaryButtonProps) {
23
+ return (
24
+ <button
25
+ className={`flex justify-center w-full h-[42px] rounded-[6px] p-[10px] cursor-pointer ${bgColor} text-${textColor}`}
26
+ onClick={onClick}
27
+ >
28
+ <div className="flex justify-center items-center gap-[10px]">
29
+ {iconLeft && <div>{iconLeft}</div>}
30
+ {title}
31
+ {iconRight && <div>{iconRight}</div>}
32
+ </div>
33
+ </button>
34
+ );
35
+ }
@@ -0,0 +1,16 @@
1
+ import type { Meta, StoryObj } from "@storybook/react";
2
+
3
+ import { SecondaryButton } from "./SecondaryButton";
4
+
5
+ const meta = {
6
+ component: SecondaryButton,
7
+ } satisfies Meta<typeof SecondaryButton>;
8
+
9
+ export default meta;
10
+ type Story = StoryObj<typeof SecondaryButton>;
11
+
12
+ export const Default: Story = {
13
+ render: () => {
14
+ return <SecondaryButton title="Demo" onClick={() => alert("onClick")} />;
15
+ },
16
+ };
@@ -0,0 +1,26 @@
1
+ type ColorScale = 50 | 100 | 200 | 300 | 400 | 500 | 600 | 700 | 800 | 900;
2
+ type BaseColor = "primary" | "gray" | "green" | "red" | "yellow" | "blue";
3
+ type ThemeColor = `bg-${BaseColor}-${ColorScale}`;
4
+
5
+ interface SecondaryButtonProps {
6
+ title: string;
7
+ onClick: () => void;
8
+ iconLeft?: React.ReactNode;
9
+ iconRight?: React.ReactNode;
10
+ }
11
+
12
+ //TODO: disabled button, on hover
13
+ export function SecondaryButton({ title, onClick, iconLeft, iconRight }: SecondaryButtonProps) {
14
+ return (
15
+ <button
16
+ className={`flex justify-center w-full h-[42px] rounded-[6px] p-[10px] border-[1px] border-black cursor-pointer bg-white text-black`}
17
+ onClick={onClick}
18
+ >
19
+ <div className="flex justify-center items-center gap-[10px]">
20
+ {iconLeft && <div>{iconLeft}</div>}
21
+ {title}
22
+ {iconRight && <div>{iconRight}</div>}
23
+ </div>
24
+ </button>
25
+ );
26
+ }
@@ -0,0 +1,17 @@
1
+ import type { Meta, StoryObj } from "@storybook/react";
2
+ import { Checkbox } from "./Checkbox";
3
+ import { useState } from "react";
4
+
5
+ const meta = {
6
+ component: Checkbox,
7
+ } satisfies Meta<typeof Checkbox>;
8
+
9
+ export default meta;
10
+ type Story = StoryObj<typeof Checkbox>;
11
+
12
+ export const Default: Story = {
13
+ render: () => {
14
+ const [checked, setChecked] = useState(false);
15
+ return <Checkbox checked={checked} onChange={setChecked} label="Remember me" />;
16
+ },
17
+ };
@@ -0,0 +1,27 @@
1
+ import { IconCheck } from "@tabler/icons-react";
2
+
3
+ interface CheckboxProps {
4
+ label?: string;
5
+ checked: boolean;
6
+ onChange: (checked: boolean) => void;
7
+ }
8
+
9
+ export function Checkbox({ label, checked, onChange }: CheckboxProps) {
10
+ return (
11
+ <div className="flex gap-[10px]">
12
+ <div
13
+ className={`flex justify-center items-center border-[1px] border-black w-[24px] h-[24px] rounded-[8px] cursor-pointer transition-colors duration-100
14
+ ${checked ? "bg-black text-white" : "bg-white text-black"}`}
15
+ onClick={() => onChange(!checked)}
16
+ >
17
+ <span
18
+ className={`flex justify-center items-center transition-transform duration-150
19
+ ${checked ? "scale-100 opacity-100" : "scale-0 opacity-0"}`}
20
+ >
21
+ <IconCheck size={20} />
22
+ </span>
23
+ </div>
24
+ {label && <p>{label}</p>}
25
+ </div>
26
+ );
27
+ }
@@ -0,0 +1,35 @@
1
+ import type { Meta, StoryObj } from "@storybook/react-vite";
2
+
3
+ import { CheckboxGroup } from "./CheckboxGroup";
4
+ import { useState } from "react";
5
+
6
+ const meta = {
7
+ component: CheckboxGroup,
8
+ } satisfies Meta<typeof CheckboxGroup>;
9
+
10
+ export default meta;
11
+
12
+ type Story = StoryObj<typeof CheckboxGroup>;
13
+
14
+ export const Default: Story = {
15
+ render: () => {
16
+ const [options, setOptions] = useState([
17
+ { checked: false, label: "Apple" },
18
+ { checked: true, label: "Banana" },
19
+ { checked: false, label: "Cherry" },
20
+ ]);
21
+
22
+ const onChange = (label: string) => {
23
+ setOptions((prev) => prev.map((opt) => (opt.label === label ? { ...opt, checked: !opt.checked } : opt)));
24
+ };
25
+
26
+ const selectedItem = options.filter((opt) => opt.checked).map((item) => item.label);
27
+
28
+ return (
29
+ <div>
30
+ <CheckboxGroup options={options} onChange={onChange} alignment="vertical" />
31
+ <p>Selected items: {selectedItem.join(", ")} </p>
32
+ </div>
33
+ );
34
+ },
35
+ };
@@ -0,0 +1,23 @@
1
+ import React from "react";
2
+ import { Checkbox } from "../Checkbox/Checkbox";
3
+
4
+ interface CheckboxOption {
5
+ checked: boolean;
6
+ label: string;
7
+ }
8
+
9
+ interface CheckboxGroupProps {
10
+ options: CheckboxOption[];
11
+ onChange: (label: string) => void;
12
+ alignment?: "horizontal" | "vertical";
13
+ }
14
+
15
+ export function CheckboxGroup({ options, onChange, alignment = "vertical" }: CheckboxGroupProps) {
16
+ return (
17
+ <div className={`flex gap-4 ${alignment === "vertical" ? "flex-col" : ""}`}>
18
+ {options.map((opt) => (
19
+ <Checkbox key={opt.label} checked={opt.checked} label={opt.label} onChange={() => onChange(opt.label)} />
20
+ ))}
21
+ </div>
22
+ );
23
+ }
@@ -0,0 +1,16 @@
1
+ import type { Meta, StoryObj } from "@storybook/react";
2
+
3
+ import { Loader } from "./Loader";
4
+
5
+ const meta = {
6
+ component: Loader,
7
+ } satisfies Meta<typeof Loader>;
8
+
9
+ export default meta;
10
+ type Story = StoryObj<typeof Loader>;
11
+
12
+ export const Default: Story = {
13
+ render: () => {
14
+ return <Loader />;
15
+ },
16
+ };
@@ -0,0 +1,12 @@
1
+ interface LoaderProps {
2
+ size?: number;
3
+ }
4
+
5
+ export function Loader({ size = 25 }: LoaderProps) {
6
+ return (
7
+ <div
8
+ style={{ width: size, height: size }}
9
+ className="border-4 border-black border-t-transparent border-solid rounded-full animate-spin"
10
+ ></div>
11
+ );
12
+ }
@@ -0,0 +1,85 @@
1
+ import type { Meta, StoryObj } from "@storybook/react";
2
+ import { MenuNavBar } from "./MenuNavBar";
3
+
4
+ import { Icon } from "@iconify/react";
5
+
6
+ import {
7
+ IconLayoutDashboard,
8
+ IconLayoutDashboardFilled,
9
+ IconFolder,
10
+ IconFolderFilled,
11
+ IconSquareCheck,
12
+ IconSquareCheckFilled,
13
+ IconCalendarMonth,
14
+ IconFileAnalytics,
15
+ IconSettings,
16
+ IconHelp,
17
+ IconCalendarMonthFilled,
18
+ IconFileAnalyticsFilled,
19
+ } from "@tabler/icons-react";
20
+
21
+ const meta = {
22
+ component: MenuNavBar,
23
+ } satisfies Meta<typeof MenuNavBar>;
24
+
25
+ export default meta;
26
+ type Story = StoryObj<typeof MenuNavBar>;
27
+
28
+ export const Default: Story = {
29
+ render: () => {
30
+ const MENU = [
31
+ {
32
+ title: "ภาพรวม",
33
+ subMenus: [
34
+ {
35
+ title: "แดชบอร์ด",
36
+ icon: <IconLayoutDashboard />,
37
+ iconActive: <IconLayoutDashboardFilled />,
38
+ path: "/dashboard",
39
+ },
40
+ { title: "โครงการ", icon: <IconFolder />, iconActive: <IconFolderFilled />, path: "/project" },
41
+ ],
42
+ },
43
+ {
44
+ title: "การจัดการ",
45
+ subMenus: [
46
+ {
47
+ title: "สิ่งที่ต้องทำ",
48
+ icon: <IconSquareCheck />,
49
+ iconActive: <IconSquareCheckFilled />,
50
+ path: "/todo",
51
+ customNode: (
52
+ <div
53
+ className="flex justify-center items-center w-[30px] h-[30px] rounded-[6px] bg-red-500 text-white
54
+ group-hover:bg-white group-hover:text-primary-500 group-hover:border-[1px] group-hover:border-primary-500
55
+ group-active:bg-red-500 group-active:text-white btn-small"
56
+ >
57
+ 5
58
+ </div>
59
+ ),
60
+ },
61
+ {
62
+ title: "กำหนดการ",
63
+ icon: <IconCalendarMonth />,
64
+ iconActive: <IconCalendarMonthFilled />,
65
+ path: "/calendar",
66
+ },
67
+ { title: "รายงาน", icon: <IconFileAnalytics />, iconActive: <IconFileAnalyticsFilled />, path: "/report" },
68
+ { title: "ทีม", icon: <Icon icon="ri:team-line" />, iconActive: <Icon icon="ri:team-fill" />, path: "/team" },
69
+ ],
70
+ },
71
+ {
72
+ title: "ตั้งค่า",
73
+ subMenus: [
74
+ { title: "ตั้งค่า", icon: <IconSettings />, path: "/setting" },
75
+ { title: "ศูนย์ความช่วยเหลือ", icon: <IconHelp />, path: "/help" },
76
+ ],
77
+ },
78
+ ];
79
+ return (
80
+ <div className="w-[242px]">
81
+ <MenuNavBar menus={MENU} onClick={(v) => console.log(v)} />
82
+ </div>
83
+ );
84
+ },
85
+ };
@@ -0,0 +1,43 @@
1
+ interface Menu {
2
+ title: string;
3
+ subMenus: {
4
+ title: string;
5
+ icon?: React.ReactNode;
6
+ iconActive?: React.ReactNode;
7
+ path: string;
8
+ customNode?: React.ReactNode;
9
+ }[];
10
+ }
11
+
12
+ interface MenuNavBarProps {
13
+ menus: Menu[];
14
+ onClick: (path: string) => void;
15
+ }
16
+
17
+ export function MenuNavBar({ menus, onClick }: MenuNavBarProps) {
18
+ return (
19
+ <div className="w-full h-full p-[10px]">
20
+ {menus?.map((menu, index) => (
21
+ <div key={`menu_${menu.title}`} className={`p-[10px] ${index !== 0 ? "mt-[10px]" : ""}`}>
22
+ <p className="p-[10px] w-[202px] h-[47px] subtitle-1">{menu.title}</p>
23
+ {menu?.subMenus.map((subMenu) => (
24
+ <div
25
+ key={`sub_${subMenu.title}`}
26
+ className="group flex justify-center items-center gap-[10px] p-[10px] w-[202px] h-[47px] rounded-[6px] subtitle-2 cursor-pointer hover:bg-red-100 active:bg-primary-500 hover:text-white active:text-white"
27
+ onClick={() => onClick(subMenu.path)}
28
+ >
29
+ <span className="flex justify-center items-center w-[24px] h-[24px] text-[20px]">
30
+ {subMenu.icon && (
31
+ <span className={`block ${subMenu.iconActive ? "group-active:hidden" : ""}`}>{subMenu.icon}</span>
32
+ )}
33
+ {subMenu.iconActive && <span className="hidden group-active:block">{subMenu.iconActive}</span>}
34
+ </span>
35
+ {subMenu.title}
36
+ <span className="flex ml-auto">{subMenu.customNode && subMenu.customNode}</span>
37
+ </div>
38
+ ))}
39
+ </div>
40
+ ))}
41
+ </div>
42
+ );
43
+ }
@@ -0,0 +1,16 @@
1
+ import type { Meta, StoryObj } from "@storybook/react";
2
+
3
+ import { TopNavBar } from "./TopNavBar";
4
+
5
+ const meta = {
6
+ component: TopNavBar,
7
+ } satisfies Meta<typeof TopNavBar>;
8
+
9
+ export default meta;
10
+ type Story = StoryObj<typeof TopNavBar>;
11
+
12
+ export const Default: Story = {
13
+ render: () => {
14
+ return <TopNavBar onClickNoti={() => console.log("noti")} />;
15
+ },
16
+ };
@@ -0,0 +1,24 @@
1
+ import STOLogo from "../../assets/STO-logo.svg";
2
+ import { IconBellRinging } from "@tabler/icons-react";
3
+
4
+ interface TopNavBarProps {
5
+ onClickNoti: () => void;
6
+ }
7
+
8
+ export function TopNavBar({ onClickNoti }: TopNavBarProps) {
9
+ return (
10
+ <div className="w-full h-full flex">
11
+ <div className="flex items-center gap-[20px] p-[10px]">
12
+ <img src={STOLogo} />
13
+ <p className="subtitle-1">Project Management</p>
14
+ </div>
15
+ <div className="flex items-center ml-auto gap-[20px] p-[10px]">
16
+ <div>Search</div>
17
+ <div>
18
+ <IconBellRinging onClick={onClickNoti} className="cursor-pointer" />
19
+ </div>
20
+ <div className="w-[40px] h-[40px] bg-gray-400 rounded-full cursor-pointer"></div>
21
+ </div>
22
+ </div>
23
+ );
24
+ }
@@ -0,0 +1,17 @@
1
+ import type { Meta, StoryObj } from "@storybook/react";
2
+ import { Radio } from "./Radio";
3
+ import { useState } from "react";
4
+
5
+ const meta = {
6
+ component: Radio,
7
+ } satisfies Meta<typeof Radio>;
8
+
9
+ export default meta;
10
+ type Story = StoryObj<typeof Radio>;
11
+
12
+ export const Default: Story = {
13
+ render: () => {
14
+ const [selected, setSelected] = useState(false);
15
+ return <Radio selected={selected} onChange={setSelected} />;
16
+ },
17
+ };
@@ -0,0 +1,15 @@
1
+ interface RadioProps {
2
+ selected: boolean;
3
+ onChange: (selected: boolean) => void;
4
+ }
5
+
6
+ export function Radio({ selected, onChange }: RadioProps) {
7
+ return (
8
+ <div
9
+ className="flex justify-center items-center w-[16px] h-[16px] border-[1px] border-black rounded-full cursor-pointer"
10
+ onClick={() => onChange(!selected)}
11
+ >
12
+ {selected && <div className={`bg-black w-[10px] h-[10px] rounded-full transition-all duration-300`} />}
13
+ </div>
14
+ );
15
+ }
@@ -0,0 +1,23 @@
1
+ import type { Meta, StoryObj } from "@storybook/react";
2
+ import { useState } from "react";
3
+ import { RadioGroup } from "./RadioGroup";
4
+
5
+ const meta = {
6
+ component: RadioGroup,
7
+ } satisfies Meta<typeof RadioGroup>;
8
+
9
+ export default meta;
10
+ type Story = StoryObj<typeof RadioGroup>;
11
+
12
+ export const Default: Story = {
13
+ render: () => {
14
+ const options = [
15
+ { value: "1", label: "Option 1" },
16
+ { value: "2", label: "Option 2" },
17
+ { value: "3", label: "Option 3" },
18
+ ];
19
+
20
+ const [selected, setSelected] = useState("1");
21
+ return <RadioGroup onChange={setSelected} options={options} value={selected} alignment="vertical" />;
22
+ },
23
+ };
@@ -0,0 +1,27 @@
1
+ import { Radio } from "../Radio/Radio";
2
+
3
+ interface RadioOption {
4
+ value: string;
5
+ label: string;
6
+ }
7
+
8
+ interface RadioGroupProps {
9
+ options: RadioOption[];
10
+ value: string;
11
+ onChange: (value: string) => void;
12
+ alignment?: "vertical" | "horizontal";
13
+ }
14
+
15
+ //TODO: ALIGNMENT
16
+ export function RadioGroup({ options, value, onChange, alignment = "horizontal" }: RadioGroupProps) {
17
+ return (
18
+ <div className={`flex gap-2 ${alignment === "vertical" ? "flex-col" : ""}`}>
19
+ {options.map((opt) => (
20
+ <label key={opt.value} className="flex items-center gap-2 cursor-pointer">
21
+ <Radio selected={value === opt.value} onChange={() => onChange(opt.value)} />
22
+ <span>{opt.label}</span>
23
+ </label>
24
+ ))}
25
+ </div>
26
+ );
27
+ }