diff --git a/apps/manager/src/api/index.ts b/apps/manager/src/api/index.ts new file mode 100644 index 00000000..d5bc710c --- /dev/null +++ b/apps/manager/src/api/index.ts @@ -0,0 +1 @@ +export * from './mutations'; diff --git a/apps/manager/src/api/mutations/index.ts b/apps/manager/src/api/mutations/index.ts new file mode 100644 index 00000000..2aa12e28 --- /dev/null +++ b/apps/manager/src/api/mutations/index.ts @@ -0,0 +1 @@ +export * from './useLogin'; diff --git a/apps/manager/src/api/mutations/useLogin.ts b/apps/manager/src/api/mutations/useLogin.ts new file mode 100644 index 00000000..19255035 --- /dev/null +++ b/apps/manager/src/api/mutations/useLogin.ts @@ -0,0 +1,12 @@ +import { fetcher, useMutation } from '@hcc/api-base'; + +type Request = { + email: string; + password: string; +}; + +export const postLogin = (request: Request) => { + return fetcher.post('manager/login', { json: request }); +}; + +export const useLogin = () => useMutation({ mutationFn: postLogin }); diff --git a/apps/manager/src/api/queries/index.ts b/apps/manager/src/api/queryKey.ts similarity index 100% rename from apps/manager/src/api/queries/index.ts rename to apps/manager/src/api/queryKey.ts diff --git a/apps/manager/src/app/auth/login/login-form.tsx b/apps/manager/src/app/auth/login/login-form.tsx new file mode 100644 index 00000000..3a4142cf --- /dev/null +++ b/apps/manager/src/app/auth/login/login-form.tsx @@ -0,0 +1,60 @@ +'use client'; + +import { Button, Input, toast } from '@hcc/ui'; +import { useRouter } from 'next/navigation'; +import type { FormEvent } from 'react'; +import { useLogin } from '~/api'; +import { ROUTES } from '~/constants/routes'; + +export const LoginForm = () => { + const router = useRouter(); + const { mutate } = useLogin(); + + const handleSubmit = (event: FormEvent) => { + event.preventDefault(); + const formData = new FormData(event.currentTarget); + const email = formData.get('email') as string; + const password = formData.get('password') as string; + + mutate( + { email, password }, + { + onSuccess: () => { + router.push(ROUTES.HOME); + }, + onError: error => { + if (error instanceof Error) { + toast.error(error.name); + } else { + toast.error('아이디 또는 비밀번호 오류'); + } + }, + }, + ); + }; + + return ( +
+ + + +
+ ); +}; diff --git a/apps/manager/src/app/auth/login/page.tsx b/apps/manager/src/app/auth/login/page.tsx index 2ffb4d78..bfa902f4 100644 --- a/apps/manager/src/app/auth/login/page.tsx +++ b/apps/manager/src/app/auth/login/page.tsx @@ -1,4 +1,5 @@ import { Badge } from '@hcc/ui'; +import { LoginForm } from './login-form'; const Page = () => { return ( @@ -9,11 +10,12 @@ const Page = () => {
manager - + 매니저 용 -
+ +
); }; diff --git a/apps/manager/src/app/layout.tsx b/apps/manager/src/app/layout.tsx index 8e912676..762464fa 100644 --- a/apps/manager/src/app/layout.tsx +++ b/apps/manager/src/app/layout.tsx @@ -1,11 +1,13 @@ +import '@hcc/ui/styles.css'; +import '~/styles/globals.css'; + +import { Toaster } from '@hcc/ui'; import { Analytics } from '@vercel/analytics/next'; import type { Metadata } from 'next'; import type { PropsWithChildren } from 'react'; import { Layout } from '~/components/layout'; import { Pretendard } from './_fonts'; import { Provider } from './provider'; -import '~/styles/globals.css'; -import '@hcc/ui/styles.css'; export const metadata: Metadata = { title: '훕치치 매니저', @@ -20,6 +22,7 @@ const RootLayout = ({ children }: PropsWithChildren) => { {children} + ); diff --git a/packages/api-base/build.js b/packages/api-base/build.js deleted file mode 100644 index de80fca8..00000000 --- a/packages/api-base/build.js +++ /dev/null @@ -1,38 +0,0 @@ -import esbuild from 'esbuild'; -import pkg from './package.json' with { type: 'json' }; - -const sharedConfig = { - entryPoints: ['./src/index.ts'], - bundle: true, - write: true, - treeShaking: true, - minify: true, - sourcemap: false, - target: ['es2020'], - external: [ - 'react', - 'react-dom', - ...Object.keys(pkg.dependencies || {}), - ...Object.keys(pkg.peerDependencies || {}), - ], - jsx: 'automatic', - jsxImportSource: 'react', -}; - -esbuild - .build({ - ...sharedConfig, - outfile: './dist/index.js', - format: 'esm', - platform: 'neutral', - }) - .catch(() => process.exit(1)); - -esbuild - .build({ - ...sharedConfig, - outfile: './dist/index.cjs', - format: 'cjs', - platform: 'neutral', - }) - .catch(() => process.exit(1)); diff --git a/packages/api-base/package.json b/packages/api-base/package.json index 0abfcda4..590c5e7e 100644 --- a/packages/api-base/package.json +++ b/packages/api-base/package.json @@ -3,8 +3,6 @@ "version": "0.0.1", "private": true, "scripts": { - "build": "rm -rf dist && node build.js && tsc", - "dev": "node build.js --watch", "start": "next start", "lint": "biome lint .", "format": "biome format --write ." @@ -15,21 +13,10 @@ "biome format --write" ] }, - "sideEffects": false, "type": "module", - "main": "./dist/index.cjs", - "module": "./dist/index.js", - "types": "./dist/index.d.ts", - "exports": { - ".": { - "types": "./dist/index.d.ts", - "import": "./dist/index.js", - "require": "./dist/index.cjs" - } - }, - "files": [ - "dist" - ], + "main": "./src/index.ts", + "module": "./src/index.ts", + "types": "./src/index.ts", "peerDependencies": { "react": "^19.1.1", "react-dom": "^19.1.1" diff --git a/packages/ui/package.json b/packages/ui/package.json index 189a605c..e6533345 100644 --- a/packages/ui/package.json +++ b/packages/ui/package.json @@ -45,7 +45,10 @@ "typescript": "^5.9.2" }, "dependencies": { + "@hcc/icons": "workspace:*", "@radix-ui/react-slot": "^1.2.3", - "clsx": "^2.1.1" + "clsx": "^2.1.1", + "sonner": "^2.0.7", + "ts-pattern": "^5.8.0" } } diff --git a/packages/ui/src/badge/Badge.tsx b/packages/ui/src/badge/Badge.tsx index fa2c0484..e3ef61d6 100644 --- a/packages/ui/src/badge/Badge.tsx +++ b/packages/ui/src/badge/Badge.tsx @@ -1,12 +1,13 @@ import { clsx } from 'clsx'; import { type ComponentProps, type CSSProperties, forwardRef } from 'react'; -import { color } from '../token'; +import { match } from 'ts-pattern'; +import { colors, type ResponsiveFontSize } from '../token'; import { Typography } from '../typography'; import styles from './Badge.module.css'; -type BadgeSize = 'small' | 'medium' | 'large'; +type BadgeSize = 'sm' | 'md' | 'lg'; -type BadgeVariant = 'default' | 'danger' | 'success'; +type BadgeVariant = 'default' | 'danger' | 'primary'; export interface BadgeProps extends ComponentProps<'div'> { size?: BadgeSize; @@ -14,7 +15,7 @@ export interface BadgeProps extends ComponentProps<'div'> { } export const Badge = forwardRef( - ({ className, children, size = 'medium', variant = 'default', style: _style, ...props }, ref) => { + ({ className, children, size = 'md', variant = 'default', style: _style, ...props }, ref) => { const backgroundColor = getBackgroundColor(variant); const padding = getPadding(size); const style = { @@ -30,7 +31,13 @@ export const Badge = forwardRef( return (
- + {children}
@@ -38,46 +45,30 @@ export const Badge = forwardRef( }, ); -const getPadding = (size: BadgeSize) => { - switch (size) { - case 'small': - return '6px 8px'; - case 'medium': - return '8px 12px'; - case 'large': - return '10px 16px'; - } -}; +const getPadding = (size: BadgeSize) => + match(size) + .with('sm', () => '4px 8px') + .with('md', () => '6px 12px') + .with('lg', () => '8px 16px') + .exhaustive(); -const getFontSize = (size: BadgeSize) => { - switch (size) { - case 'small': - return 12; - case 'medium': - return 14; - case 'large': - return 18; - } -}; +const getFontSize = (size: BadgeSize) => + match(size) + .with('sm', () => 12) + .with('md', () => 14) + .with('lg', () => 16) + .exhaustive(); -const getFontColor = (variant: BadgeVariant) => { - switch (variant) { - case 'default': - return color.neutral600; - case 'danger': - return color.white; - case 'success': - return color.primary600; - } -}; +const getFontColor = (variant: BadgeVariant) => + match(variant) + .with('default', () => colors.neutral600) + .with('danger', () => colors.white) + .with('primary', () => colors.primary600) + .exhaustive(); -const getBackgroundColor = (variant: BadgeVariant) => { - switch (variant) { - case 'default': - return color.neutral100; - case 'danger': - return color.danger600; - case 'success': - return color.primary100; - } -}; +const getBackgroundColor = (variant: BadgeVariant) => + match(variant) + .with('default', () => colors.neutral100) + .with('danger', () => colors.danger600) + .with('primary', () => colors.primary100) + .exhaustive(); diff --git a/packages/ui/src/button/Button.module.css b/packages/ui/src/button/Button.module.css new file mode 100644 index 00000000..2d090840 --- /dev/null +++ b/packages/ui/src/button/Button.module.css @@ -0,0 +1,40 @@ +.button { + display: inline-flex; + align-items: center; + justify-content: center; + cursor: pointer; + transition: background-color 0.15s ease-in-out; + + height: var(--hcc-button-height); + color: var(--hcc-button-font-color); + font-size: var(--hcc-button-font-size); + font-weight: var(--hcc-button-font-weight); + border: var(--hcc-button-border); + border-radius: var(--hcc-button-border-radius); + background-color: var(--hcc-button-bg-color); + + --hcc-button-height: inherit; + --hcc-button-font-color: inherit; + --hcc-button-font-size: inherit; + --hcc-button-font-weight: inherit; + --hcc-button-border: inherit; + --hcc-button-border-radius: inherit; + --hcc-button-bg-color: inherit; +} + +.button:focus-visible { + outline: 2px solid currentColor; + outline-offset: 2px; +} + +.disabled { + opacity: 0.5; + cursor: not-allowed; + pointer-events: none; +} + +.button:hover { + background-color: var(--hcc-button-bg-hover-color); + + --hcc-button-bg-hover-color: inherit; +} diff --git a/packages/ui/src/button/Button.tsx b/packages/ui/src/button/Button.tsx new file mode 100644 index 00000000..51be1900 --- /dev/null +++ b/packages/ui/src/button/Button.tsx @@ -0,0 +1,119 @@ +import { Slot } from '@radix-ui/react-slot'; +import { clsx } from 'clsx'; +import { type ComponentProps, type CSSProperties, forwardRef } from 'react'; +import { match } from 'ts-pattern'; +import { colors, fontWeight as fontWeightToken } from '../token'; +import styles from './Button.module.css'; + +type ButtonSize = 'xs' | 'sm' | 'md' | 'lg' | 'xl'; + +type ButtonColor = 'black' | 'primary' | 'danger'; + +type ButtonVariant = 'solid' | 'subtle' | 'ghost'; + +export interface ButtonProps extends ComponentProps<'button'> { + asChild?: boolean; + size?: ButtonSize; + color?: ButtonColor; + variant?: ButtonVariant; + disabled?: boolean; +} + +export const Button = forwardRef( + ( + { + asChild, + className, + children, + disabled, + size = 'md', + color = 'primary', + variant = 'solid', + style: _style, + ...props + }, + ref, + ) => { + const Comp = asChild ? Slot : 'button'; + + const style = { + ..._style, + ...getColorStyle(color, variant), + '--hcc-button-height': `${getHeight(size)}px`, + '--hcc-button-font-size': `${getFontSize(size)}px`, + '--hcc-button-font-weight': fontWeightToken[getFontWeight(size)], + '--hcc-button-border-radius': '8px', + } as CSSProperties; + + return ( + + {children} + + ); + }, +); + +const getHeight = (size: ButtonSize) => + match(size) + .with('xs', () => 28) + .with('sm', () => 36) + .with('md', () => 44) + .with('lg', () => 52) + .with('xl', () => 60) + .exhaustive(); + +const getFontSize = (size: ButtonSize) => + match(size) + .with('xs', () => 12) + .with('sm', () => 14) + .with('md', () => 14) + .with('lg', () => 16) + .with('xl', () => 18) + .exhaustive(); + +const getFontWeight = (size: ButtonSize): keyof typeof fontWeightToken => + match(size) + .with('xs', 'sm', () => 'medium' as const) + .with('md', 'lg', 'xl', () => 'semibold' as const) + .exhaustive(); + +const getColorStyle = (color: ButtonColor, variant: ButtonVariant) => { + const fontColor = match([color, variant]) + .with(['black', 'solid'], ['primary', 'solid'], ['danger', 'solid'], () => colors.white) + .with(['black', 'subtle'], ['black', 'ghost'], () => colors.neutral900) + .with(['primary', 'subtle'], ['primary', 'ghost'], () => colors.primary600) + .with(['danger', 'subtle'], ['danger', 'ghost'], () => colors.danger600) + .exhaustive(); + + const backgroundColor = match([color, variant]) + .with(['black', 'solid'], () => colors.neutral900) + .with(['black', 'subtle'], () => colors.neutral50) + .with(['primary', 'solid'], () => colors.primary600) + .with(['primary', 'subtle'], () => colors.primary100) + .with(['danger', 'solid'], () => colors.danger600) + .with(['danger', 'subtle'], () => colors.danger100) + .otherwise(() => colors.transparent); + + const backgroundHoverColor = match([color, variant]) + .with(['black', 'solid'], () => colors.neutral700) + .with(['black', 'subtle'], () => colors.neutral100) + .with(['black', 'ghost'], () => colors.neutral50) + .with(['primary', 'solid'], () => colors.primary700) + .with(['primary', 'subtle'], () => colors.primary200) + .with(['primary', 'ghost'], () => colors.primary50) + .with(['danger', 'solid'], () => colors.danger700) + .with(['danger', 'subtle'], () => colors.danger200) + .with(['danger', 'ghost'], () => colors.danger50) + .otherwise(() => colors.transparent); + + return { + '--hcc-button-font-color': fontColor, + '--hcc-button-bg-color': backgroundColor, + '--hcc-button-bg-hover-color': backgroundHoverColor, + }; +}; diff --git a/packages/ui/src/button/index.ts b/packages/ui/src/button/index.ts new file mode 100644 index 00000000..8b166a86 --- /dev/null +++ b/packages/ui/src/button/index.ts @@ -0,0 +1 @@ +export * from './Button'; diff --git a/packages/ui/src/index.ts b/packages/ui/src/index.ts index f3a90f60..342abfe2 100644 --- a/packages/ui/src/index.ts +++ b/packages/ui/src/index.ts @@ -1,3 +1,7 @@ export * from './badge'; +export * from './button'; +export * from './input'; +export * from './select'; +export * from './toast'; export * from './token'; export * from './typography'; diff --git a/packages/ui/src/input/Input.module.css b/packages/ui/src/input/Input.module.css new file mode 100644 index 00000000..bd1f406e --- /dev/null +++ b/packages/ui/src/input/Input.module.css @@ -0,0 +1,68 @@ +.wrapper { + position: relative; + display: flex; + text-align: start; + overflow: hidden; + + height: var(--hcc-input-height); + border-radius: var(--hcc-input-border-radius); + outline: var(--hcc-input-outline); + + --hcc-input-height: inherit; + --hcc-input-padding-inline: inherit; + --hcc-input-placeholder-color: inherit; + --hcc-input-font-size: inherit; + --hcc-input-font-weight: inherit; + --hcc-input-line-height: inherit; + --hcc-input-border-radius: inherit; + --hcc-input-outline: inherit; + --hcc-input-padding-top: inherit; + --hcc-label-position: inherit; +} + +.input { + width: 100%; + height: 100%; + display: flex; + align-items: center; + text-align: inherit; + padding-top: var(--hcc-input-padding-top); + + color: inherit; + outline: 1px solid transparent; + border: none; + background: transparent; + + padding-inline: var(--hcc-input-padding-inline); + font-size: var(--hcc-input-font-size); + font-weight: var(--hcc-input-font-weight); + line-height: var(--hcc-input-line-height); +} + +.input::placeholder { + color: var(--hcc-input-placeholder-color); +} + +.wrapper:has(.label) .input::placeholder { + color: transparent; +} + +.label { + position: absolute; + left: var(--hcc-input-padding-inline); + top: 50%; + transform: translateY(-50%); + pointer-events: none; + transition: top 0.15s ease, transform 0.15s ease, font-size 0.15s ease; + + color: var(--hcc-input-placeholder-color); + font-size: var(--hcc-input-font-size); + font-weight: var(--hcc-input-font-weight); + line-height: var(--hcc-input-line-height); +} + +.wrapper:focus-within .label, .input:not(:placeholder-shown) ~ .label { + top: var(--hcc-label-position); + transform: translateY(0); + font-size: calc(var(--hcc-input-font-size) * 0.75); +} diff --git a/packages/ui/src/input/Input.tsx b/packages/ui/src/input/Input.tsx new file mode 100644 index 00000000..b4cd4870 --- /dev/null +++ b/packages/ui/src/input/Input.tsx @@ -0,0 +1,104 @@ +import { clsx } from 'clsx'; +import { type ComponentProps, type CSSProperties, forwardRef } from 'react'; +import { match } from 'ts-pattern'; +import { + colors, + type FontWeight, + fontWeight as fontWeightToken, + type LineHeight, + lineHeight as lineHeightToken, +} from '../token'; +import styles from './Input.module.css'; + +type InputSize = 'xs' | 'sm' | 'md' | 'lg' | 'xl'; + +export interface InputProps extends Omit, 'size'> { + size?: InputSize; + weight?: FontWeight; + lineHeight?: LineHeight; +} + +export const Input = forwardRef( + ( + { + id, + className, + children, + placeholder, + size = 'md', + weight = 'medium', + lineHeight = 'normal', + style: _style, + ...props + }, + ref, + ) => { + const isLabelVisible = size === 'lg' || size === 'xl'; + + const style = { + ..._style, + '--hcc-input-height': `${getHeight(size)}px`, + '--hcc-input-padding-inline': `${getPadding(size)}px`, + '--hcc-input-placeholder-color': colors.neutral400, + '--hcc-input-font-size': `${getFontSize(size)}px`, + '--hcc-input-font-weight': fontWeightToken[weight], + '--hcc-input-line-height': lineHeightToken[lineHeight], + '--hcc-input-border-radius': '8px', + '--hcc-input-outline': `1px solid ${colors.neutral100}`, + '--hcc-input-padding-top': isLabelVisible ? `${getInputPaddingTop(size)}px` : '0px', + '--hcc-label-position': isLabelVisible ? `${getLabelPosition(size)}px` : '0px', + } as CSSProperties; + + return ( +
+ + {isLabelVisible && ( + + )} + {children} +
+ ); + }, +); + +Input.displayName = 'Input'; + +const getHeight = (size: InputSize) => + match(size) + .with('xs', () => 28) + .with('sm', () => 36) + .with('md', () => 44) + .with('lg', () => 52) + .with('xl', () => 60) + .exhaustive(); + +const getPadding = (size: InputSize) => + match(size) + .with('xs', () => 8) + .with('sm', () => 10) + .with('md', () => 14) + .with('lg', 'xl', () => 18) + .exhaustive(); + +const getFontSize = (size: InputSize) => + match(size) + .with('xs', () => 12) + .with('sm', () => 14) + .with('md', 'lg', 'xl', () => 16) + .exhaustive(); + +const getInputPaddingTop = (size: InputSize) => + match(size) + .with('xs', 'sm', 'md', () => 0) + .with('lg', () => 10) + .with('xl', () => 18) + .exhaustive(); + +const getLabelPosition = (size: InputSize) => + match(size) + .with('xs', 'sm', 'md', () => 0) + .with('lg', () => 5) + .with('xl', () => 9) + .exhaustive(); diff --git a/packages/ui/src/input/index.ts b/packages/ui/src/input/index.ts new file mode 100644 index 00000000..ba9fe7eb --- /dev/null +++ b/packages/ui/src/input/index.ts @@ -0,0 +1 @@ +export * from './Input'; diff --git a/packages/ui/src/select/Select.module.css b/packages/ui/src/select/Select.module.css new file mode 100644 index 00000000..e1020f00 --- /dev/null +++ b/packages/ui/src/select/Select.module.css @@ -0,0 +1,69 @@ +.wrapper { + position: relative; + display: flex; + text-align: start; + overflow: hidden; + + height: var(--hcc-select-height); + border-radius: var(--hcc-select-border-radius); + outline: var(--hcc-select-outline); + + --hcc-select-height: inherit; + --hcc-select-padding-inline: inherit; + --hcc-select-placeholder-color: inherit; + --hcc-select-font-size: inherit; + --hcc-select-font-weight: inherit; + --hcc-select-line-height: inherit; + --hcc-select-border-radius: inherit; + --hcc-select-outline: inherit; + --hcc-select-padding-top: inherit; + --hcc-select-label-top: inherit; +} + +.select { + width: 100%; + height: 100%; + display: flex; + align-items: center; + text-align: inherit; + padding-top: var(--hcc-select-padding-top); + + color: inherit; + outline: 1px solid transparent; + border: none; + background: transparent; + appearance: none; + + padding-inline: var(--hcc-select-padding-inline); + font-size: var(--hcc-select-font-size); + font-weight: var(--hcc-select-font-weight); + line-height: var(--hcc-select-line-height); +} + +.select:invalid { + color: var(--hcc-select-placeholder-color); +} + +.wrapper:has(.label) .select:invalid { + color: transparent; +} + +.label { + position: absolute; + left: var(--hcc-select-padding-inline); + top: 50%; + transform: translateY(-50%); + pointer-events: none; + transition: top 0.15s ease, transform 0.15s ease, font-size 0.15s ease; + + color: var(--hcc-select-placeholder-color); + font-size: var(--hcc-select-font-size); + font-weight: var(--hcc-select-font-weight); + line-height: var(--hcc-select-line-height); +} + +.wrapper:has(.select:valid) .label { + top: var(--hcc-select-label-top); + transform: translateY(0); + font-size: calc(var(--hcc-select-font-size) * 0.75); +} diff --git a/packages/ui/src/select/Select.tsx b/packages/ui/src/select/Select.tsx new file mode 100644 index 00000000..96d9c2b1 --- /dev/null +++ b/packages/ui/src/select/Select.tsx @@ -0,0 +1,120 @@ +import { clsx } from 'clsx'; +import { type ComponentProps, type CSSProperties, forwardRef } from 'react'; +import { match } from 'ts-pattern'; +import { + colors, + type FontWeight, + fontWeight as fontWeightToken, + type LineHeight, + lineHeight as lineHeightToken, + type ResponsiveFontSize, +} from '../token'; +import styles from './Select.module.css'; + +type SelectSize = 'xs' | 'sm' | 'md' | 'lg' | 'xl'; + +export interface SelectProps extends Omit, 'size'> { + size?: SelectSize; + fontSize?: ResponsiveFontSize; + weight?: FontWeight; + lineHeight?: LineHeight; + placeholder?: string; +} + +export const Select = forwardRef( + ( + { + id, + className, + children, + placeholder, + size = 'md', + fontSize = 16, + weight = 'medium', + lineHeight = 'normal', + style: _style, + ...props + }, + ref, + ) => { + const isLabelVisible = size === 'lg' || size === 'xl'; + + const style = { + ..._style, + '--hcc-select-height': `${getHeight(size)}px`, + '--hcc-select-padding-inline': `${getPadding(size)}px`, + '--hcc-select-placeholder-color': colors.neutral400, + '--hcc-select-font-size': `${getFontSize(size)}px`, + '--hcc-select-font-weight': fontWeightToken[weight], + '--hcc-select-line-height': lineHeightToken[lineHeight], + '--hcc-select-border-radius': '8px', + '--hcc-select-outline': `1px solid ${colors.neutral100}`, + '--hcc-select-padding-top': isLabelVisible ? getSelectPaddingTop(size) : '0px', + '--hcc-select-label-top': isLabelVisible ? getLabelTop(size) : '0px', + } as CSSProperties; + + return ( +
+ + + {isLabelVisible && placeholder && ( + + )} +
+ ); + }, +); + +Select.displayName = 'Select'; + +const getHeight = (size: SelectSize) => + match(size) + .with('xs', () => 28) + .with('sm', () => 36) + .with('md', () => 44) + .with('lg', () => 52) + .with('xl', () => 60) + .exhaustive(); + +const getPadding = (size: SelectSize) => + match(size) + .with('xs', () => 8) + .with('sm', () => 10) + .with('md', () => 14) + .with('lg', 'xl', () => 18) + .exhaustive(); + +const getFontSize = (size: SelectSize) => + match(size) + .with('xs', () => 12) + .with('sm', () => 14) + .with('md', 'lg', 'xl', () => 16) + .exhaustive(); + +const getSelectPaddingTop = (size: SelectSize) => + match(size) + .with('xs', 'sm', 'md', () => 0) + .with('lg', () => 10) + .with('xl', () => 18) + .exhaustive(); + +const getLabelTop = (size: SelectSize) => + match(size) + .with('xs', 'sm', 'md', () => 0) + .with('lg', () => 5) + .with('xl', () => 9) + .exhaustive(); diff --git a/packages/ui/src/select/index.ts b/packages/ui/src/select/index.ts new file mode 100644 index 00000000..7868ecba --- /dev/null +++ b/packages/ui/src/select/index.ts @@ -0,0 +1 @@ +export * from './Select'; diff --git a/packages/ui/src/toast/Toast.module.css b/packages/ui/src/toast/Toast.module.css new file mode 100644 index 00000000..ebd9473f --- /dev/null +++ b/packages/ui/src/toast/Toast.module.css @@ -0,0 +1,17 @@ +.toast { + padding: 15px !important; + font-size: 16px !important; + border-color: transparent !important; + border-radius: 8px !important; + gap: 12px !important; +} + +.toast [data-icon] { + width: 24px !important; + height: 24px !important; +} + +.toast [data-content] { + flex: 1; + gap: 0 !important; +} \ No newline at end of file diff --git a/packages/ui/src/toast/Toast.tsx b/packages/ui/src/toast/Toast.tsx new file mode 100644 index 00000000..6133c86a --- /dev/null +++ b/packages/ui/src/toast/Toast.tsx @@ -0,0 +1,26 @@ +import { CheckCircleIcon, ErrorIcon } from '@hcc/icons'; +import type { CSSProperties } from 'react'; +import { Toaster as BaseToaster, toast } from 'sonner'; +import { colors } from '../token'; +import styles from './Toast.module.css'; + +export const Toaster = () => { + return ( + , + error: , + }} + /> + ); +}; + +export { toast }; diff --git a/packages/ui/src/toast/index.ts b/packages/ui/src/toast/index.ts new file mode 100644 index 00000000..1b794ee7 --- /dev/null +++ b/packages/ui/src/toast/index.ts @@ -0,0 +1 @@ +export * from './Toast'; diff --git a/packages/ui/src/token/color.ts b/packages/ui/src/token/color.ts index 4dea5d7c..a730a340 100644 --- a/packages/ui/src/token/color.ts +++ b/packages/ui/src/token/color.ts @@ -1,12 +1,42 @@ -export const color = { - white: '#FFFFFF', - black: '#000000', +export const colors = { + white: 'oklch(100% 0 0)', + black: 'oklch(0% 0 0)', + transparent: 'transparent', + primary50: '#F8FBFF', primary100: '#F2F8FF', + primary200: '#E0EDFF', + primary300: '#C7DBFF', + primary400: '#A3C4FF', + primary500: '#4D9FFF', primary600: '#007AFF', + primary700: '#0056CC', + primary800: '#003D99', + primary900: '#002466', + danger50: '#FFF8F7', + danger100: '#FFF0EF', + danger200: '#FFE0DE', + danger300: '#FFC7C4', + danger400: '#FFA3A0', + danger500: '#FE7A7A', danger600: '#FC5555', + danger700: '#E63939', + danger800: '#CC2626', + danger900: '#991A1A', - neutral100: '#F5F5F5', - neutral600: '#525252', -}; + green600: '#1ADA3B', + + neutral50: 'oklch(98.5% 0 0)', + neutral100: 'oklch(97% 0 0)', + neutral200: 'oklch(92.2% 0 0)', + neutral300: 'oklch(87% 0 0)', + neutral400: 'oklch(70.8% 0 0)', + neutral500: 'oklch(55.6% 0 0)', + neutral600: 'oklch(43.9% 0 0)', + neutral700: 'oklch(37.1% 0 0)', + neutral800: 'oklch(26.9% 0 0)', + neutral900: 'oklch(20.5% 0 0)', + + toast: '#374553', +} as const; diff --git a/packages/ui/src/token/typography.ts b/packages/ui/src/token/typography.ts index 422ddb21..ae85db6d 100644 --- a/packages/ui/src/token/typography.ts +++ b/packages/ui/src/token/typography.ts @@ -15,9 +15,9 @@ export const fontSize = { export const fontWeight = { regular: 400, medium: 500, - semiBold: 600, + semibold: 600, bold: 700, - extraBold: 800, + extrabold: 800, black: 900, } as const; @@ -29,3 +29,30 @@ export const lineHeight = { relaxed: 1.625, loose: 2, } as const; + +export type FontSize = keyof typeof fontSize; + +export type ResponsiveFontSize = + | FontSize + | [FontSize, FontSize?, FontSize?] + | { base: FontSize; tablet?: FontSize; pc?: FontSize }; + +export type FontWeight = keyof typeof fontWeight; + +export type LineHeight = keyof typeof lineHeight; + +export const parseResponsiveFontSize = (fontSize: ResponsiveFontSize) => { + let base: FontSize | undefined; + let tablet: FontSize | undefined; + let pc: FontSize | undefined; + + if (Array.isArray(fontSize)) { + [base, tablet, pc] = fontSize; + } else if (typeof fontSize === 'object' && fontSize !== null) { + ({ base, tablet, pc } = fontSize); + } else { + base = fontSize; + } + + return { base, tablet, pc }; +}; diff --git a/packages/ui/src/typography/Typography.module.css b/packages/ui/src/typography/Typography.module.css index e25900bc..1ed20fa7 100644 --- a/packages/ui/src/typography/Typography.module.css +++ b/packages/ui/src/typography/Typography.module.css @@ -18,6 +18,6 @@ @media (min-width: 1024px) { .typography { - font-size: var(--hcc-desktop-typography-font-size, var(--hcc-tablet-typography-font-size, var(--hcc-typography-font-size))); + font-size: var(--hcc-pc-typography-font-size, var(--hcc-tablet-typography-font-size, var(--hcc-typography-font-size))); } } diff --git a/packages/ui/src/typography/Typography.tsx b/packages/ui/src/typography/Typography.tsx index 6db18c31..b6c2a178 100644 --- a/packages/ui/src/typography/Typography.tsx +++ b/packages/ui/src/typography/Typography.tsx @@ -1,27 +1,20 @@ import { Slot } from '@radix-ui/react-slot'; -import { clsx } from 'clsx'; +import { clsx as cn } from 'clsx'; import { type ComponentProps, type CSSProperties, forwardRef } from 'react'; import { + type FontWeight, fontSize as fontSizeToken, fontWeight as fontWeightToken, + type LineHeight, lineHeight as lineHeightToken, + parseResponsiveFontSize, + type ResponsiveFontSize, } from '../token'; import styles from './Typography.module.css'; -export type FontSize = keyof typeof fontSizeToken; - -export type ResponsiveFontSize = - | FontSize - | [FontSize, FontSize?, FontSize?] - | { base: FontSize; tablet?: FontSize; desktop?: FontSize }; - -export type FontWeight = keyof typeof fontWeightToken; - -export type LineHeight = keyof typeof lineHeightToken; - export interface TypographyProps extends ComponentProps<'p'> { asChild?: boolean; - size?: ResponsiveFontSize; + fontSize?: ResponsiveFontSize; weight?: FontWeight; lineHeight?: LineHeight; } @@ -32,7 +25,7 @@ export const Typography = forwardRef( asChild, className, color, - size = 16, + fontSize = 16, weight = 'regular', lineHeight = 'normal', style: _style, @@ -41,18 +34,7 @@ export const Typography = forwardRef( ref, ) => { const Comp = asChild ? Slot : 'p'; - - let base: FontSize | undefined; - let tablet: FontSize | undefined; - let desktop: FontSize | undefined; - - if (Array.isArray(size)) { - [base, tablet, desktop] = size; - } else if (typeof size === 'object') { - ({ base, tablet, desktop } = size); - } else { - base = size; - } + const { base, tablet, pc } = parseResponsiveFontSize(fontSize); const style = { ..._style, @@ -61,15 +43,13 @@ export const Typography = forwardRef( ...(tablet !== undefined && { '--hcc-tablet-typography-font-size': `${fontSizeToken[tablet]}px`, }), - ...(desktop !== undefined && { - '--hcc-desktop-typography-font-size': `${fontSizeToken[desktop]}px`, + ...(pc !== undefined && { + '--hcc-pc-typography-font-size': `${fontSizeToken[pc]}px`, }), '--hcc-typography-font-weight': fontWeightToken[weight], '--hcc-typography-line-height': lineHeightToken[lineHeight], } as CSSProperties; - return ( - - ); + return ; }, ); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index a9b567d1..9a4fbb16 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -194,6 +194,9 @@ importers: packages/ui: dependencies: + '@hcc/icons': + specifier: workspace:* + version: link:../icons '@radix-ui/react-slot': specifier: ^1.2.3 version: 1.2.3(@types/react@19.1.9)(react@19.1.1) @@ -206,6 +209,12 @@ importers: react-dom: specifier: ^19.1.1 version: 19.1.1(react@19.1.1) + sonner: + specifier: ^2.0.7 + version: 2.0.7(react-dom@19.1.1(react@19.1.1))(react@19.1.1) + ts-pattern: + specifier: ^5.8.0 + version: 5.8.0 devDependencies: '@hcc/typescript-config': specifier: workspace:* @@ -1475,6 +1484,12 @@ packages: resolution: {integrity: sha512-bSiSngZ/jWeX93BqeIAbImyTbEihizcwNjFoRUIY/T1wWQsfsm2Vw1agPKylXvQTU7iASGdHhyqRlqQzfz+Htg==} engines: {node: '>=18'} + sonner@2.0.7: + resolution: {integrity: sha512-W6ZN4p58k8aDKA4XPcx2hpIQXBRAgyiWVkYhT7CvK6D3iAu7xjvVyhQHg2/iaKJZ1XVJ4r7XuwGL+WGEK37i9w==} + peerDependencies: + react: ^18.0.0 || ^19.0.0 || ^19.0.0-rc + react-dom: ^18.0.0 || ^19.0.0 || ^19.0.0-rc + source-map-js@1.2.1: resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} engines: {node: '>=0.10.0'} @@ -1541,6 +1556,9 @@ packages: resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==} engines: {node: '>=8.0'} + ts-pattern@5.8.0: + resolution: {integrity: sha512-kIjN2qmWiHnhgr5DAkAafF9fwb0T5OhMVSWrm8XEdTFnX6+wfXwYOFjeF86UZ54vduqiR7BfqScFmXSzSaH8oA==} + tslib@2.8.1: resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==} @@ -2683,6 +2701,11 @@ snapshots: ansi-styles: 6.2.1 is-fullwidth-code-point: 5.0.0 + sonner@2.0.7(react-dom@19.1.1(react@19.1.1))(react@19.1.1): + dependencies: + react: 19.1.1 + react-dom: 19.1.1(react@19.1.1) + source-map-js@1.2.1: {} split2@4.2.0: {} @@ -2737,6 +2760,8 @@ snapshots: dependencies: is-number: 7.0.0 + ts-pattern@5.8.0: {} + tslib@2.8.1: {} turbo-darwin-64@2.5.5: