From 7c0aad8d10ff8de656945f3756668d0f0e108a7e Mon Sep 17 00:00:00 2001 From: myusername Date: Wed, 13 May 2026 11:49:23 +0530 Subject: [PATCH] feat: Remade using predefined primitives --- src/layouts/MainLayout/BaseLayout.tsx | 11 +- src/shared/Button/index.ts | 2 - .../AvatarMenu/AvatarMenu.stories.tsx | 41 +++++ .../composites/AvatarMenu/AvatarMenu.tsx | 153 ++++++++++++++++++ .../SidebarNavItem/SidebarNavItem.stories.tsx | 0 .../SidebarNavItem/SidebarNavItem.tsx | 2 +- .../{ => composites}/SidebarNavItem/index.ts | 0 .../primitives/Avatar/Avatar.stories.tsx | 77 +++++++++ src/shared/primitives/Avatar/Avatar.tsx | 136 ++++++++++++++++ src/shared/primitives/Avatar/index.ts | 2 + .../{ => primitives}/Badge/Badge.stories.tsx | 0 src/shared/{ => primitives}/Badge/Badge.tsx | 0 src/shared/{ => primitives}/Badge/index.ts | 0 .../Button/Button.stories.tsx | 0 src/shared/{ => primitives}/Button/Button.tsx | 14 +- src/shared/primitives/Button/index.ts | 2 + .../primitives/Switch/Switch.stories.tsx | 57 +++++++ src/shared/primitives/Switch/Switch.tsx | 104 ++++++++++++ src/shared/primitives/Switch/index.ts | 3 + .../Tooltip/Tooltip.stories.tsx | 0 .../{ => primitives}/Tooltip/Tooltip.tsx | 2 +- src/shared/{ => primitives}/Tooltip/index.ts | 0 22 files changed, 597 insertions(+), 9 deletions(-) delete mode 100644 src/shared/Button/index.ts create mode 100644 src/shared/composites/AvatarMenu/AvatarMenu.stories.tsx create mode 100644 src/shared/composites/AvatarMenu/AvatarMenu.tsx rename src/shared/{ => composites}/SidebarNavItem/SidebarNavItem.stories.tsx (100%) rename src/shared/{ => composites}/SidebarNavItem/SidebarNavItem.tsx (97%) rename src/shared/{ => composites}/SidebarNavItem/index.ts (100%) create mode 100644 src/shared/primitives/Avatar/Avatar.stories.tsx create mode 100644 src/shared/primitives/Avatar/Avatar.tsx create mode 100644 src/shared/primitives/Avatar/index.ts rename src/shared/{ => primitives}/Badge/Badge.stories.tsx (100%) rename src/shared/{ => primitives}/Badge/Badge.tsx (100%) rename src/shared/{ => primitives}/Badge/index.ts (100%) rename src/shared/{ => primitives}/Button/Button.stories.tsx (100%) rename src/shared/{ => primitives}/Button/Button.tsx (85%) create mode 100644 src/shared/primitives/Button/index.ts create mode 100644 src/shared/primitives/Switch/Switch.stories.tsx create mode 100644 src/shared/primitives/Switch/Switch.tsx create mode 100644 src/shared/primitives/Switch/index.ts rename src/shared/{ => primitives}/Tooltip/Tooltip.stories.tsx (100%) rename src/shared/{ => primitives}/Tooltip/Tooltip.tsx (98%) rename src/shared/{ => primitives}/Tooltip/index.ts (100%) diff --git a/src/layouts/MainLayout/BaseLayout.tsx b/src/layouts/MainLayout/BaseLayout.tsx index cafdb9e..2e0c20d 100644 --- a/src/layouts/MainLayout/BaseLayout.tsx +++ b/src/layouts/MainLayout/BaseLayout.tsx @@ -1,5 +1,6 @@ -import { Tooltip } from '@/shared/Tooltip' -import ThemeToggleSwitch from '@/ThemeToggleSwitch' +import AvatarMenu from '@/shared/composites/AvatarMenu/AvatarMenu' +import { Tooltip } from '@/shared/primitives/Tooltip' +// import ThemeToggleSwitch from '@/ThemeToggleSwitch' import React, { Suspense } from 'react' import { Outlet } from 'react-router-dom' @@ -12,7 +13,11 @@ const BaseLayout: React.FC = () => {
- } content="Change theme" position="left" /> + } + content="Settings" + position="left" + />
diff --git a/src/shared/Button/index.ts b/src/shared/Button/index.ts deleted file mode 100644 index a957269..0000000 --- a/src/shared/Button/index.ts +++ /dev/null @@ -1,2 +0,0 @@ -export { Button } from './Button' -export type { ButtonProps } from './Button' \ No newline at end of file diff --git a/src/shared/composites/AvatarMenu/AvatarMenu.stories.tsx b/src/shared/composites/AvatarMenu/AvatarMenu.stories.tsx new file mode 100644 index 0000000..ad2f4bd --- /dev/null +++ b/src/shared/composites/AvatarMenu/AvatarMenu.stories.tsx @@ -0,0 +1,41 @@ +import type { Meta, StoryObj } from '@storybook/react-vite' + +import { fn } from 'storybook/test' + +import { AvatarMenu } from './AvatarMenu' + +const meta = { + title: 'Shared/Composites/AvatarMenu', + + component: AvatarMenu, + + tags: ['autodocs'], + + args: { + onEditProfile: fn(), + + onPreferences: fn(), + + onSecurity: fn(), + + onLogout: fn(), + }, +} satisfies Meta + +export default meta + +type Story = StoryObj + +export const Default: Story = { + args: { + name: 'Admin Mod', + }, +} + +export const WithImage: Story = { + args: { + name: 'Admin Mod', + + src: 'https://i.pravatar.cc/150?img=12', + }, +} diff --git a/src/shared/composites/AvatarMenu/AvatarMenu.tsx b/src/shared/composites/AvatarMenu/AvatarMenu.tsx new file mode 100644 index 0000000..46f04f5 --- /dev/null +++ b/src/shared/composites/AvatarMenu/AvatarMenu.tsx @@ -0,0 +1,153 @@ +import { useEffect, useRef, useState } from 'react' + +import { Avatar } from '@/shared/primitives/Avatar' +import { Switch } from '@/shared/primitives/Switch/Switch' +import { + ArrowRightStartOnRectangleIcon, + PencilSquareIcon, + // Cog6ToothIcon, + MoonIcon, + SunIcon, + AdjustmentsHorizontalIcon, + LockClosedIcon, + // UserCircleIcon, + // ShieldCheckIcon, +} from '@heroicons/react/24/outline' +import { useTheme } from '@/shared/theme' +import { Button } from '@/shared/primitives/Button' +export interface AvatarMenuProps { + name: string + + src?: string + + onEditProfile?: () => void + + onPreferences?: () => void + + onSecurity?: () => void + + onLogout?: () => void +} + +export function AvatarMenu({ + name, + src, + + onEditProfile = () => {}, + onPreferences = () => {}, + onSecurity = () => {}, + onLogout = () => {}, +}: AvatarMenuProps) { + const [open, setOpen] = useState(false) + const { themeName, setTheme } = useTheme() + const ref = useRef(null) + + useEffect(() => { + const handleClickOutside = (e: MouseEvent) => { + if (ref.current && !ref.current.contains(e.target as Node)) { + setOpen(false) + } + } + + document.addEventListener('mousedown', handleClickOutside) + + return () => { + document.removeEventListener('mousedown', handleClickOutside) + } + }, []) + + return ( +
+ {/* Trigger */} + + + {/* Dropdown */} + {open && ( +
+ {/* Header */} +
+
{name}
+ +
Moderator account
+
+ + {/* Items */} + + + +
+ + +
+ + +
+ )} +
+ ) +} + +export default AvatarMenu diff --git a/src/shared/SidebarNavItem/SidebarNavItem.stories.tsx b/src/shared/composites/SidebarNavItem/SidebarNavItem.stories.tsx similarity index 100% rename from src/shared/SidebarNavItem/SidebarNavItem.stories.tsx rename to src/shared/composites/SidebarNavItem/SidebarNavItem.stories.tsx diff --git a/src/shared/SidebarNavItem/SidebarNavItem.tsx b/src/shared/composites/SidebarNavItem/SidebarNavItem.tsx similarity index 97% rename from src/shared/SidebarNavItem/SidebarNavItem.tsx rename to src/shared/composites/SidebarNavItem/SidebarNavItem.tsx index 1527b78..7023112 100644 --- a/src/shared/SidebarNavItem/SidebarNavItem.tsx +++ b/src/shared/composites/SidebarNavItem/SidebarNavItem.tsx @@ -1,6 +1,6 @@ import React from 'react' import { cva } from 'class-variance-authority' -import { Badge } from '../Badge' +import { Badge } from '../../primitives/Badge' // import { NavLink } from 'react-router-dom' const sidebarNavItem = cva( diff --git a/src/shared/SidebarNavItem/index.ts b/src/shared/composites/SidebarNavItem/index.ts similarity index 100% rename from src/shared/SidebarNavItem/index.ts rename to src/shared/composites/SidebarNavItem/index.ts diff --git a/src/shared/primitives/Avatar/Avatar.stories.tsx b/src/shared/primitives/Avatar/Avatar.stories.tsx new file mode 100644 index 0000000..6c6aa95 --- /dev/null +++ b/src/shared/primitives/Avatar/Avatar.stories.tsx @@ -0,0 +1,77 @@ +import type { Meta, StoryObj } from '@storybook/react-vite' + +import { Avatar } from './Avatar' + +const meta = { + title: 'Shared/Primitives/Avatar', + + component: Avatar, + + tags: ['autodocs'], +} satisfies Meta + +export default meta + +type Story = StoryObj + +export const Image: Story = { + args: { + name: 'Admin Mod', + + src: 'https://i.pravatar.cc/150?img=12', + }, +} + +export const InitialsFallback: Story = { + args: { + name: 'Admin Mod', + }, +} + +export const Online: Story = { + args: { + name: 'Admin Mod', + + showOnline: true, + }, +} + +export const ExtraSmall: Story = { + args: { + name: 'Admin Mod', + + size: 'xs', + }, +} + +export const Small: Story = { + args: { + name: 'Admin Mod', + + size: 'sm', + }, +} + +export const Medium: Story = { + args: { + name: 'Admin Mod', + + size: 'md', + }, +} + +export const Large: Story = { + args: { + name: 'Admin Mod', + + size: 'lg', + }, +} + +export const BrokenImageFallback: Story = { + args: { + name: 'Admin Mod', + + src: '/broken-image.png', + }, +} diff --git a/src/shared/primitives/Avatar/Avatar.tsx b/src/shared/primitives/Avatar/Avatar.tsx new file mode 100644 index 0000000..3ebd6c8 --- /dev/null +++ b/src/shared/primitives/Avatar/Avatar.tsx @@ -0,0 +1,136 @@ +import { useMemo, useState } from 'react' + +import { cva } from 'class-variance-authority' + +export interface AvatarProps { + name: string + + src?: string + + size?: 'xs' | 'sm' | 'md' | 'lg' + + showOnline?: boolean + + className?: string +} + +const avatar = cva( + ` + relative + inline-flex + items-center + justify-center + rounded-md + select-none + shrink-0 + font-medium + `, + { + variants: { + size: { + xs: 'w-5 h-5 text-[9px]', + sm: 'w-7 h-7 text-xs', + md: 'w-9 h-9 text-sm ', + lg: 'w-12 h-12 text-base', + }, + }, + + defaultVariants: { + size: 'md', + }, + } +) + +const onlineDot = cva( + ` + absolute + rounded-full + border-2 + border-bg-primary + bg-text-success + + `, + { + variants: { + size: { + xs: 'w-1.5 h-1.5 bottom-0 right-0', + sm: 'w-2 h-2 bottom-0 right-0', + md: 'w-2.5 h-2.5 bottom-0 right-0', + lg: 'w-3 h-3 bottom-0.5 right-0.5', + }, + }, + + defaultVariants: { + size: 'md', + }, + } +) + +const fallbackPalette = [ + 'bg-bg-info text-text-info border border-border-info', + 'bg-bg-warning text-text-warning border border-border-warning', + 'bg-bg-success text-text-success border border-border-success', +] + +function getInitials(name: string) { + const parts = name.trim().split(/\s+/) + + if (parts.length === 1) { + return parts[0]?.[0]?.toUpperCase() ?? '?' + } + + return ((parts[0]?.[0] ?? '') + (parts[parts.length - 1]?.[0] ?? '')).toUpperCase() +} + +export function Avatar({ + name, + src, + size = 'md', + showOnline = false, + className = '', +}: AvatarProps) { + const [failedSrc, setFailedSrc] = useState(null) + + const initials = useMemo(() => getInitials(name), [name]) + + const fallbackColor = useMemo(() => { + const index = name.charCodeAt(0) % fallbackPalette.length + + return fallbackPalette[index] + }, [name]) + + const showImage = Boolean(src) && failedSrc !== src + + return ( +
+ {showImage ? ( + {name} { + if (src) { + setFailedSrc(src) + } + }} + /> + ) : ( +
+ {initials} +
+ )} + + {showOnline && } +
+ ) +} + +export default Avatar diff --git a/src/shared/primitives/Avatar/index.ts b/src/shared/primitives/Avatar/index.ts new file mode 100644 index 0000000..11f8055 --- /dev/null +++ b/src/shared/primitives/Avatar/index.ts @@ -0,0 +1,2 @@ +export { Avatar } from './Avatar' +export type { AvatarProps } from './Avatar' diff --git a/src/shared/Badge/Badge.stories.tsx b/src/shared/primitives/Badge/Badge.stories.tsx similarity index 100% rename from src/shared/Badge/Badge.stories.tsx rename to src/shared/primitives/Badge/Badge.stories.tsx diff --git a/src/shared/Badge/Badge.tsx b/src/shared/primitives/Badge/Badge.tsx similarity index 100% rename from src/shared/Badge/Badge.tsx rename to src/shared/primitives/Badge/Badge.tsx diff --git a/src/shared/Badge/index.ts b/src/shared/primitives/Badge/index.ts similarity index 100% rename from src/shared/Badge/index.ts rename to src/shared/primitives/Badge/index.ts diff --git a/src/shared/Button/Button.stories.tsx b/src/shared/primitives/Button/Button.stories.tsx similarity index 100% rename from src/shared/Button/Button.stories.tsx rename to src/shared/primitives/Button/Button.stories.tsx diff --git a/src/shared/Button/Button.tsx b/src/shared/primitives/Button/Button.tsx similarity index 85% rename from src/shared/Button/Button.tsx rename to src/shared/primitives/Button/Button.tsx index 1d7a4a7..4c0a341 100644 --- a/src/shared/Button/Button.tsx +++ b/src/shared/primitives/Button/Button.tsx @@ -25,24 +25,33 @@ const button = cva( info: 'bg-bg-info text-text-info border border-border-info cursor-pointer', - ghost: 'bg-transparent text-text-secondary border border-transparent cursor-pointer', + ghost: + 'bg-transparent rounded-md overflow-hidden text-text-secondary border border-transparent cursor-pointer', }, size: { sm: 'px-3 py-1.5 text-sm', md: 'px-4 py-2 text-sm', lg: 'px-5 py-3 text-base', + sd: 'p-0 text-sm ', }, fullWidth: { true: 'w-full', }, + + textAlign: { + left: 'justify-start text-left', + center: 'justify-center text-center', + right: 'justify-end text-right', + }, }, defaultVariants: { variant: 'primary', size: 'md', fullWidth: false, + textAlign: 'left', }, } ) @@ -71,7 +80,7 @@ export function Button({ isLoading = false, isDisabled = false, - + textAlign, leftIcon, rightIcon, @@ -87,6 +96,7 @@ export function Button({ variant, size, fullWidth, + textAlign, className, })} disabled={disabled} diff --git a/src/shared/primitives/Button/index.ts b/src/shared/primitives/Button/index.ts new file mode 100644 index 0000000..d7f8c69 --- /dev/null +++ b/src/shared/primitives/Button/index.ts @@ -0,0 +1,2 @@ +export { Button } from './Button' +export type { ButtonProps } from './Button' diff --git a/src/shared/primitives/Switch/Switch.stories.tsx b/src/shared/primitives/Switch/Switch.stories.tsx new file mode 100644 index 0000000..39a7fa2 --- /dev/null +++ b/src/shared/primitives/Switch/Switch.stories.tsx @@ -0,0 +1,57 @@ +import type { Meta, StoryObj } from '@storybook/react-vite' + +import { fn } from 'storybook/test' + +import { Switch } from './Switch' + +const meta = { + title: 'Shared/Primitives/Switch', + + component: Switch, + + tags: ['autodocs'], + + argTypes: { + checked: { + control: 'boolean', + }, + + disabled: { + control: 'boolean', + }, + }, + + args: { + onChange: fn(), + }, +} satisfies Meta + +export default meta + +type Story = StoryObj + +export const Off: Story = { + args: { + checked: false, + }, +} + +export const On: Story = { + args: { + checked: true, + }, +} + +export const DisabledOff: Story = { + args: { + checked: false, + disabled: true, + }, +} + +export const DisabledOn: Story = { + args: { + checked: true, + disabled: true, + }, +} diff --git a/src/shared/primitives/Switch/Switch.tsx b/src/shared/primitives/Switch/Switch.tsx new file mode 100644 index 0000000..2333515 --- /dev/null +++ b/src/shared/primitives/Switch/Switch.tsx @@ -0,0 +1,104 @@ +import { cva } from 'class-variance-authority' + +export interface SwitchProps { + checked: boolean + + onChange?: (checked: boolean) => void + + disabled?: boolean + + className?: string +} + +const switchTrack = cva( + ` + relative + inline-flex + h-6 + w-11 + shrink-0 + cursor-pointer + rounded-full + border + transition-colors + duration-200 + focus:outline-none + `, + { + variants: { + checked: { + true: ` + bg-bg-secondary + border-border-secondary + `, + + false: ` + bg-bg-tertiary + border-border-secondary + `, + }, + + disabled: { + true: ` + opacity-50 + cursor-not-allowed + `, + + false: '', + }, + }, + } +) + +const switchThumb = cva( + ` + pointer-events-none + inline-block + h-5 + w-5 + transform + rounded-full + bg-white + shadow-sm + ring-0 + transition-transform + duration-200 + `, + { + variants: { + checked: { + true: 'translate-x-5 translate-y-0.25', + false: 'translate-x-0.5 translate-y-0.25', + }, + }, + } +) + +export function Switch({ checked, onChange, disabled = false, className = '' }: SwitchProps) { + return ( + + ) +} + +export default Switch diff --git a/src/shared/primitives/Switch/index.ts b/src/shared/primitives/Switch/index.ts new file mode 100644 index 0000000..73fed20 --- /dev/null +++ b/src/shared/primitives/Switch/index.ts @@ -0,0 +1,3 @@ +export { Switch } from './Switch' + +export type { SwitchProps } from './Switch' diff --git a/src/shared/Tooltip/Tooltip.stories.tsx b/src/shared/primitives/Tooltip/Tooltip.stories.tsx similarity index 100% rename from src/shared/Tooltip/Tooltip.stories.tsx rename to src/shared/primitives/Tooltip/Tooltip.stories.tsx diff --git a/src/shared/Tooltip/Tooltip.tsx b/src/shared/primitives/Tooltip/Tooltip.tsx similarity index 98% rename from src/shared/Tooltip/Tooltip.tsx rename to src/shared/primitives/Tooltip/Tooltip.tsx index 9d697d4..888d126 100644 --- a/src/shared/Tooltip/Tooltip.tsx +++ b/src/shared/primitives/Tooltip/Tooltip.tsx @@ -180,7 +180,7 @@ export function Tooltip({ : position === 'bottom' ? 'translate(-50%, 0)' : position === 'left' - ? 'translate(-100%, -5%)' + ? 'translate(-100%, -45%)' : 'translate(0, -45%)', }} > diff --git a/src/shared/Tooltip/index.ts b/src/shared/primitives/Tooltip/index.ts similarity index 100% rename from src/shared/Tooltip/index.ts rename to src/shared/primitives/Tooltip/index.ts