diff --git a/apps/wow-docs/app/page.tsx b/apps/wow-docs/app/page.tsx index d89460d2..a3e944cb 100644 --- a/apps/wow-docs/app/page.tsx +++ b/apps/wow-docs/app/page.tsx @@ -10,6 +10,10 @@ import RadioButton from "wowds-ui/RadioButton"; import RadioGroup from "wowds-ui/RadioGroup"; import SearchBar from "wowds-ui/SearchBar"; import Switch from "wowds-ui/Switch"; +import Tabs from "wowds-ui/Tabs"; +import TabsContent from "wowds-ui/TabsContent"; +import TabsItem from "wowds-ui/TabsItem"; +import TabsList from "wowds-ui/TabsList"; const Home = () => { return ( @@ -43,6 +47,22 @@ const Home = () => { + + + 첫번째첫번째첫번째첫번째 + 두 번째 + 세 번쨰 + + + 첫번째 탭 + + + 두번째 탭 + + + 세번째 탭 + + ); }; diff --git a/apps/wow-docs/styled-system/tokens/index.js b/apps/wow-docs/styled-system/tokens/index.js index 930662c7..a6ec6779 100644 --- a/apps/wow-docs/styled-system/tokens/index.js +++ b/apps/wow-docs/styled-system/tokens/index.js @@ -343,6 +343,10 @@ const tokens = { value: 10, variable: "var(--z-index-dropdown)", }, + "zIndex.overlay": { + value: 9999, + variable: "var(--z-index-overlay)", + }, "shadows.blue": { value: "0px 4px 8px 0px rgba(16, 43, 74, 0.2)", variable: "var(--shadows-blue)", diff --git a/apps/wow-docs/styled-system/tokens/tokens.d.ts b/apps/wow-docs/styled-system/tokens/tokens.d.ts index 24b9c593..45319136 100644 --- a/apps/wow-docs/styled-system/tokens/tokens.d.ts +++ b/apps/wow-docs/styled-system/tokens/tokens.d.ts @@ -86,6 +86,7 @@ export type Token = | "borderWidths.button" | "borderWidths.arrow" | "zIndex.dropdown" + | "zIndex.overlay" | "shadows.blue" | "shadows.mono" | "breakpoints.xs" @@ -323,7 +324,7 @@ export type RadiusToken = "sm" | "md" | "full"; export type BorderWidthToken = "button" | "arrow"; -export type ZIndexToken = "dropdown"; +export type ZIndexToken = "dropdown" | "overlay"; export type ShadowToken = "blue" | "mono"; diff --git a/apps/wow-docs/styled-system/types/prop-type.d.ts b/apps/wow-docs/styled-system/types/prop-type.d.ts index 195159ce..bfcf5a77 100644 --- a/apps/wow-docs/styled-system/types/prop-type.d.ts +++ b/apps/wow-docs/styled-system/types/prop-type.d.ts @@ -679,7 +679,9 @@ export interface UtilityValues { | "body3" | "label1" | "label2" - | "label3"; + | "label3" + | "header1" + | "header2"; } type WithColorOpacityModifier = T extends string ? `${T}/${string}` : T; diff --git a/packages/scripts/generateBuildConfig.ts b/packages/scripts/generateBuildConfig.ts index c9bed9e1..613353d5 100644 --- a/packages/scripts/generateBuildConfig.ts +++ b/packages/scripts/generateBuildConfig.ts @@ -26,17 +26,22 @@ const excludedComponents = [ "CollectionContext", "DropDownOptionList", "pickerComponents", + "ToastContext", ]; +// 추가할 컴포넌트 목록 +const includedComponents = ["useToast"]; + const getFilteredComponentFiles = async (directoryPath: string) => { const files = await fs.readdir(directoryPath, { recursive: true }); return files.filter( (file) => - file.endsWith(".tsx") && - !file.includes("test") && - !file.includes("stories") && - !excludedComponents.some((excluded) => file.includes(excluded)) + (file.endsWith(".tsx") && + !file.includes("test") && + !file.includes("stories") && + !excludedComponents.some((excluded) => file.includes(excluded))) || + includedComponents.some((included) => file.includes(included)) ); }; diff --git a/packages/scripts/generateReactComponentFromSvg.ts b/packages/scripts/generateReactComponentFromSvg.ts index a08f184c..ad7ec5db 100644 --- a/packages/scripts/generateReactComponentFromSvg.ts +++ b/packages/scripts/generateReactComponentFromSvg.ts @@ -138,7 +138,9 @@ const generateExportFile = async (components: string[]) => { ) .join("\n"); - await fs.writeFile(EXPORT_FILE_PATH, exportFileContent); + const resolvedExportFileContent = `export * from "../types/Icon.ts";\n${exportFileContent}`; + + await fs.writeFile(EXPORT_FILE_PATH, resolvedExportFileContent); }; (async () => { diff --git a/packages/wow-icons/CHANGELOG.md b/packages/wow-icons/CHANGELOG.md index cecf3299..4885acd9 100644 --- a/packages/wow-icons/CHANGELOG.md +++ b/packages/wow-icons/CHANGELOG.md @@ -1,5 +1,13 @@ # wowds-icons +## 0.1.5 + +### Patch Changes + +- 3682ddd: Avatar 컴포넌트를 추가합니다. +- 185475a: Icon 공통 타입을 내보내기합니다. +- b7f51d2: Header 컴포넌트를 추가합니다. + ## 0.1.4 ### Patch Changes diff --git a/packages/wow-icons/package.json b/packages/wow-icons/package.json index 447ce8c2..5eead9b1 100644 --- a/packages/wow-icons/package.json +++ b/packages/wow-icons/package.json @@ -1,6 +1,6 @@ { "name": "wowds-icons", - "version": "0.1.4", + "version": "0.1.5", "description": "", "repository": { "type": "git", @@ -16,7 +16,6 @@ "package.json" ], "type": "module", - "types": "./dist/index.d.ts", "exports": { ".": { "types": "./dist/component/index.d.ts", diff --git a/packages/wow-icons/src/component/BlueAvatar.tsx b/packages/wow-icons/src/component/BlueAvatar.tsx new file mode 100644 index 00000000..6aee0b4d --- /dev/null +++ b/packages/wow-icons/src/component/BlueAvatar.tsx @@ -0,0 +1,83 @@ +import { forwardRef } from "react"; + +import type { IconProps } from "@/types/Icon.ts"; + +const BlueAvatar = forwardRef( + ( + { + className, + width = "100", + height = "100", + viewBox = "0 0 100 100", + ...rest + }, + ref + ) => { + return ( + + + + + + + + + + + + + + + + + + + ); + } +); + +BlueAvatar.displayName = "BlueAvatar"; +export default BlueAvatar; diff --git a/packages/wow-icons/src/component/GdscLogo.tsx b/packages/wow-icons/src/component/GdscLogo.tsx new file mode 100644 index 00000000..cba980ef --- /dev/null +++ b/packages/wow-icons/src/component/GdscLogo.tsx @@ -0,0 +1,51 @@ +import { forwardRef } from "react"; + +import type { IconProps } from "@/types/Icon.ts"; + +const GdscLogo = forwardRef( + ( + { className, width = "49", height = "24", viewBox = "0 0 49 24", ...rest }, + ref + ) => { + return ( + + + + + + + + + + + + + + ); + } +); + +GdscLogo.displayName = "GdscLogo"; +export default GdscLogo; diff --git a/packages/wow-icons/src/component/GreenAvatar.tsx b/packages/wow-icons/src/component/GreenAvatar.tsx new file mode 100644 index 00000000..b3a465b5 --- /dev/null +++ b/packages/wow-icons/src/component/GreenAvatar.tsx @@ -0,0 +1,87 @@ +import { forwardRef } from "react"; + +import type { IconProps } from "@/types/Icon.ts"; + +const GreenAvatar = forwardRef( + ( + { + className, + width = "100", + height = "100", + viewBox = "0 0 100 100", + ...rest + }, + ref + ) => { + return ( + + + + + + + + + + + + + + + + + + + ); + } +); + +GreenAvatar.displayName = "GreenAvatar"; +export default GreenAvatar; diff --git a/packages/wow-icons/src/component/RedAvatar.tsx b/packages/wow-icons/src/component/RedAvatar.tsx new file mode 100644 index 00000000..542a5ffd --- /dev/null +++ b/packages/wow-icons/src/component/RedAvatar.tsx @@ -0,0 +1,84 @@ +import { forwardRef } from "react"; + +import type { IconProps } from "@/types/Icon.ts"; + +const RedAvatar = forwardRef( + ( + { + className, + width = "100", + height = "100", + viewBox = "0 0 100 100", + ...rest + }, + ref + ) => { + return ( + + + + + + + + + + + + + + + + + + + + + ); + } +); + +RedAvatar.displayName = "RedAvatar"; +export default RedAvatar; diff --git a/packages/wow-icons/src/component/YellowAvatar.tsx b/packages/wow-icons/src/component/YellowAvatar.tsx new file mode 100644 index 00000000..606be0bb --- /dev/null +++ b/packages/wow-icons/src/component/YellowAvatar.tsx @@ -0,0 +1,91 @@ +import { forwardRef } from "react"; + +import type { IconProps } from "@/types/Icon.ts"; + +const YellowAvatar = forwardRef( + ( + { + className, + width = "100", + height = "100", + viewBox = "0 0 100 100", + ...rest + }, + ref + ) => { + return ( + + + + + + + + + + + + + + + + + + + + ); + } +); + +YellowAvatar.displayName = "YellowAvatar"; +export default YellowAvatar; diff --git a/packages/wow-icons/src/component/index.ts b/packages/wow-icons/src/component/index.ts index 11ab8e7f..238767d8 100644 --- a/packages/wow-icons/src/component/index.ts +++ b/packages/wow-icons/src/component/index.ts @@ -1,14 +1,20 @@ +export * from "../types/Icon.ts"; +export { default as BlueAvatar } from "./BlueAvatar.tsx"; export { default as Calendar } from "./Calendar.tsx"; export { default as Check } from "./Check.tsx"; export { default as Close } from "./Close.tsx"; export { default as DownArrow } from "./DownArrow.tsx"; export { default as Edit } from "./Edit.tsx"; +export { default as GdscLogo } from "./GdscLogo.tsx"; +export { default as GreenAvatar } from "./GreenAvatar.tsx"; export { default as Help } from "./Help.tsx"; export { default as LeftArrow } from "./LeftArrow.tsx"; export { default as Link } from "./Link.tsx"; export { default as Plus } from "./Plus.tsx"; +export { default as RedAvatar } from "./RedAvatar.tsx"; export { default as Reload } from "./Reload.tsx"; export { default as RightArrow } from "./RightArrow.tsx"; export { default as Search } from "./Search.tsx"; export { default as Trash } from "./Trash.tsx"; export { default as Warn } from "./Warn.tsx"; +export { default as YellowAvatar } from "./YellowAvatar.tsx"; diff --git a/packages/wow-icons/src/svg/blue-avatar.svg b/packages/wow-icons/src/svg/blue-avatar.svg new file mode 100644 index 00000000..49658c71 --- /dev/null +++ b/packages/wow-icons/src/svg/blue-avatar.svg @@ -0,0 +1,18 @@ + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/packages/wow-icons/src/svg/gdsc-logo.svg b/packages/wow-icons/src/svg/gdsc-logo.svg new file mode 100644 index 00000000..6cc817bb --- /dev/null +++ b/packages/wow-icons/src/svg/gdsc-logo.svg @@ -0,0 +1,13 @@ + + + + + + + + + + + + + \ No newline at end of file diff --git a/packages/wow-icons/src/svg/green-avatar.svg b/packages/wow-icons/src/svg/green-avatar.svg new file mode 100644 index 00000000..5e0cfaf4 --- /dev/null +++ b/packages/wow-icons/src/svg/green-avatar.svg @@ -0,0 +1,18 @@ + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/packages/wow-icons/src/svg/red-avatar.svg b/packages/wow-icons/src/svg/red-avatar.svg new file mode 100644 index 00000000..26bf9bb3 --- /dev/null +++ b/packages/wow-icons/src/svg/red-avatar.svg @@ -0,0 +1,20 @@ + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/packages/wow-icons/src/svg/yellow-avatar.svg b/packages/wow-icons/src/svg/yellow-avatar.svg new file mode 100644 index 00000000..0dd1a239 --- /dev/null +++ b/packages/wow-icons/src/svg/yellow-avatar.svg @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/packages/wow-theme/CHANGELOG.md b/packages/wow-theme/CHANGELOG.md index 8a2f24f6..731f3003 100644 --- a/packages/wow-theme/CHANGELOG.md +++ b/packages/wow-theme/CHANGELOG.md @@ -1,5 +1,12 @@ # wowds-theme +## 0.1.4 + +### Patch Changes + +- 185475a: zIndex 토큰을 추가합니다. +- b7f51d2: Header 컴포넌트를 추가합니다. + ## 0.1.3 ### Patch Changes diff --git a/packages/wow-theme/package.json b/packages/wow-theme/package.json index 1a7fe353..72f23b8e 100644 --- a/packages/wow-theme/package.json +++ b/packages/wow-theme/package.json @@ -1,6 +1,6 @@ { "name": "wowds-theme", - "version": "0.1.3", + "version": "0.1.4", "type": "module", "repository": { "type": "git", diff --git a/packages/wow-theme/src/tokens/typography.ts b/packages/wow-theme/src/tokens/typography.ts index 55b7fb30..7a952cdc 100644 --- a/packages/wow-theme/src/tokens/typography.ts +++ b/packages/wow-theme/src/tokens/typography.ts @@ -38,4 +38,10 @@ export const textStyles = defineTextStyles({ label3: { value: typography.label3, }, + header1: { + value: typography.header1, + }, + header2: { + value: typography.header2, + }, }); diff --git a/packages/wow-theme/src/tokens/zIndex.ts b/packages/wow-theme/src/tokens/zIndex.ts index 879fc4ab..628e6787 100644 --- a/packages/wow-theme/src/tokens/zIndex.ts +++ b/packages/wow-theme/src/tokens/zIndex.ts @@ -5,4 +5,7 @@ export const zIndex = defineTokens.zIndex({ dropdown: { value: wowZIndex.dropdown, }, + overlay: { + value: wowZIndex.overlay, + }, }); diff --git a/packages/wow-tokens/CHANGELOG.md b/packages/wow-tokens/CHANGELOG.md index 9f24f7d6..9e64a36a 100644 --- a/packages/wow-tokens/CHANGELOG.md +++ b/packages/wow-tokens/CHANGELOG.md @@ -1,5 +1,12 @@ # wowds-tokens +## 0.1.4 + +### Patch Changes + +- 185475a: zIndex 토큰을 추가합니다. +- b7f51d2: Header 컴포넌트를 추가합니다. + ## 0.1.3 ### Patch Changes diff --git a/packages/wow-tokens/package.json b/packages/wow-tokens/package.json index fafa8808..85bc0480 100644 --- a/packages/wow-tokens/package.json +++ b/packages/wow-tokens/package.json @@ -1,6 +1,6 @@ { "name": "wowds-tokens", - "version": "0.1.3", + "version": "0.1.4", "description": "", "repository": { "type": "git", diff --git a/packages/wow-tokens/src/typography.ts b/packages/wow-tokens/src/typography.ts index a643c400..fc8d9703 100644 --- a/packages/wow-tokens/src/typography.ts +++ b/packages/wow-tokens/src/typography.ts @@ -79,3 +79,15 @@ export const label3 = { lineHeight: "100%", fontWeight: 600, }; + +export const header1 = { + fontSize: "1.3rem", + lineHeight: "130%", + fontWeight: 700, +}; + +export const header2 = { + fontSize: "0.9rem", + lineHeight: "130%", + fontWeight: 400, +}; diff --git a/packages/wow-tokens/src/zIndex.ts b/packages/wow-tokens/src/zIndex.ts index fb2a7f3f..79e30bc3 100644 --- a/packages/wow-tokens/src/zIndex.ts +++ b/packages/wow-tokens/src/zIndex.ts @@ -1 +1,2 @@ export const dropdown = 10; +export const overlay = 9999; diff --git a/packages/wow-ui/.storybook/main.ts b/packages/wow-ui/.storybook/main.ts index 35682223..e6c80f20 100644 --- a/packages/wow-ui/.storybook/main.ts +++ b/packages/wow-ui/.storybook/main.ts @@ -36,6 +36,28 @@ const config: StorybookConfig = { "@styled-system": path.resolve(__dirname, "../styled-system"), }; } + + if (!config.module || !config.module.rules) { + return config; + } + + config.module.rules = [ + ...config.module.rules.map((rule) => { + if (!rule || rule === "...") { + return rule; + } + + if (rule.test && /svg/.test(String(rule.test))) { + return { ...rule, exclude: /\.svg$/i }; + } + return rule; + }), + { + test: /\.svg$/, + use: ["@svgr/webpack"], + }, + ]; + return config; }, }; diff --git a/packages/wow-ui/CHANGELOG.md b/packages/wow-ui/CHANGELOG.md index 9c09b59c..eeef1d82 100644 --- a/packages/wow-ui/CHANGELOG.md +++ b/packages/wow-ui/CHANGELOG.md @@ -1,5 +1,18 @@ # wowds-ui +## 0.1.17 + +### Patch Changes + +- 7b19f05: Tab 컴포넌트를 구현합니다. +- 3682ddd: Avatar 컴포넌트를 추가합니다. +- 185475a: Toast 컴포넌트를 추가합니다. +- b7f51d2: Header 컴포넌트를 추가합니다. +- Updated dependencies [3682ddd] +- Updated dependencies [185475a] +- Updated dependencies [b7f51d2] + - wowds-icons@0.1.5 + ## 0.1.16 ### Patch Changes diff --git a/packages/wow-ui/package.json b/packages/wow-ui/package.json index dad39e09..468be460 100644 --- a/packages/wow-ui/package.json +++ b/packages/wow-ui/package.json @@ -1,6 +1,6 @@ { "name": "wowds-ui", - "version": "0.1.16", + "version": "0.1.17", "description": "", "author": "gdsc-hongik", "repository": { @@ -20,6 +20,21 @@ "type": "module", "exports": { "./styles.css": "./dist/styles.css", + "./ToastProvider": { + "types": "./dist/components/Toast/ToastProvider.d.ts", + "require": "./dist/ToastProvider.cjs", + "import": "./dist/ToastProvider.js" + }, + "./Toast": { + "types": "./dist/components/Toast/index.d.ts", + "require": "./dist/Toast.cjs", + "import": "./dist/Toast.js" + }, + "./useToast": { + "types": "./dist/components/Toast/useToast.d.ts", + "require": "./dist/useToast.cjs", + "import": "./dist/useToast.js" + }, "./TextField": { "types": "./dist/components/TextField/index.d.ts", "require": "./dist/TextField.cjs", @@ -65,6 +80,26 @@ "require": "./dist/Tr.cjs", "import": "./dist/Tr.js" }, + "./Tabs": { + "types": "./dist/components/Tabs/index.d.ts", + "require": "./dist/Tabs.cjs", + "import": "./dist/Tabs.js" + }, + "./TabsContent": { + "types": "./dist/components/Tabs/TabsContent.d.ts", + "require": "./dist/TabsContent.cjs", + "import": "./dist/TabsContent.js" + }, + "./TabsItem": { + "types": "./dist/components/Tabs/TabsItem.d.ts", + "require": "./dist/TabsItem.cjs", + "import": "./dist/TabsItem.js" + }, + "./TabsList": { + "types": "./dist/components/Tabs/TabsList.d.ts", + "require": "./dist/TabsList.cjs", + "import": "./dist/TabsList.js" + }, "./Switch": { "types": "./dist/components/Switch/index.d.ts", "require": "./dist/Switch.cjs", @@ -135,6 +170,11 @@ "require": "./dist/MultiGroup.cjs", "import": "./dist/MultiGroup.js" }, + "./Header": { + "types": "./dist/components/Header/index.d.ts", + "require": "./dist/Header.cjs", + "import": "./dist/Header.js" + }, "./DropDownOption": { "types": "./dist/components/DropDown/DropDownOption.d.ts", "require": "./dist/DropDownOption.cjs", @@ -169,6 +209,11 @@ "types": "./dist/components/Box/index.d.ts", "require": "./dist/Box.cjs", "import": "./dist/Box.js" + }, + "./Avatar": { + "types": "./dist/components/Avatar/index.d.ts", + "require": "./dist/Avatar.cjs", + "import": "./dist/Avatar.js" } }, "keywords": [], @@ -210,12 +255,14 @@ "plop": "^4.0.1", "rollup-plugin-peer-deps-external": "^2.2.4", "storybook": "^8.1.9", - "typescript": "^5.3.3" + "typescript": "^5.3.3", + "@types/uuid": "^10.0.0" }, "dependencies": { "clsx": "^2.1.1", "lottie-react": "^2.4.0", "react-day-picker": "^9.0.8", + "uuid": "^10.0.0", "wowds-icons": "workspace:^" }, "peerDependencies": { diff --git a/packages/wow-ui/rollup.config.js b/packages/wow-ui/rollup.config.js index e9210f7a..7bb9dfa3 100644 --- a/packages/wow-ui/rollup.config.js +++ b/packages/wow-ui/rollup.config.js @@ -20,6 +20,9 @@ process.env.BABEL_ENV = "production"; export default { input: { + ToastProvider: "./src/components/Toast/ToastProvider", + Toast: "./src/components/Toast", + useToast: "./src/components/Toast/useToast", TextField: "./src/components/TextField", TextButton: "./src/components/TextButton", Tag: "./src/components/Tag", @@ -29,6 +32,10 @@ export default { Th: "./src/components/Table/Th", Thead: "./src/components/Table/Thead", Tr: "./src/components/Table/Tr", + Tabs: "./src/components/Tabs", + TabsContent: "./src/components/Tabs/TabsContent", + TabsItem: "./src/components/Tabs/TabsItem", + TabsList: "./src/components/Tabs/TabsList", Switch: "./src/components/Switch", Stepper: "./src/components/Stepper", BlueSpinner: "./src/components/Spinner/BlueSpinner", @@ -43,6 +50,7 @@ export default { TimePicker: "./src/components/Picker/TimePicker", Pagination: "./src/components/Pagination", MultiGroup: "./src/components/MultiGroup", + Header: "./src/components/Header", DropDownOption: "./src/components/DropDown/DropDownOption", DropDown: "./src/components/DropDown", Divider: "./src/components/Divider", @@ -50,6 +58,7 @@ export default { Checkbox: "./src/components/Checkbox", Button: "./src/components/Button", Box: "./src/components/Box", + Avatar: "./src/components/Avatar", }, output: [ { diff --git a/packages/wow-ui/src/assets/lottie/blueSpinner.json b/packages/wow-ui/src/assets/lotties/blueSpinner.json similarity index 100% rename from packages/wow-ui/src/assets/lottie/blueSpinner.json rename to packages/wow-ui/src/assets/lotties/blueSpinner.json diff --git a/packages/wow-ui/src/assets/lottie/rainbowSpinner.json b/packages/wow-ui/src/assets/lotties/rainbowSpinner.json similarity index 100% rename from packages/wow-ui/src/assets/lottie/rainbowSpinner.json rename to packages/wow-ui/src/assets/lotties/rainbowSpinner.json diff --git a/packages/wow-ui/src/components/Avatar/Avatar.stories.tsx b/packages/wow-ui/src/components/Avatar/Avatar.stories.tsx new file mode 100644 index 00000000..bcedd31c --- /dev/null +++ b/packages/wow-ui/src/components/Avatar/Avatar.stories.tsx @@ -0,0 +1,119 @@ +import type { Meta, StoryObj } from "@storybook/react"; + +import Avatar from "@/components/Avatar/index.tsx"; + +const meta = { + title: "UI/Avatar", + component: Avatar, + parameters: { + componentSubtitle: "아바타 컴포넌트", + }, + tags: ["autodocs"], + argTypes: { + size: { + control: { type: "radio" }, + options: ["sm", "lg"], + description: "아바타의 크기입니다.", + table: { + defaultValue: { summary: "lg" }, + type: { + summary: "sm | lg", + }, + }, + }, + variant: { + control: { type: "radio" }, + options: ["blue", "green", "yellow", "red"], + description: "아바타의 색상입니다.", + table: { + defaultValue: { summary: "blue" }, + type: { + summary: "blue | green | yellow | red", + }, + }, + }, + ImageComponent: { + control: false, + description: "아바타에 표시할 이미지 컴포넌트입니다.", + table: { + type: { summary: "ElementType" }, + }, + }, + imageUrl: { + control: { type: "text" }, + description: "아바타에 표시할 이미지의 URL입니다.", + table: { + type: { summary: "string" }, + }, + }, + username: { + control: { type: "text" }, + description: "아바타 옆에 표시할 사용자 이름입니다.", + table: { + type: { summary: "string" }, + }, + }, + orientation: { + control: { type: "radio" }, + options: ["left", "right"], + description: + "사용자 이름 레이블의 방향입니다. size가 'sm'인 경우 지정할 수 있습니다.", + table: { + defaultValue: { summary: "right" }, + type: { summary: "left | right" }, + }, + }, + asProp: { + control: false, + description: "렌더링할 HTML 요소나 React 컴포넌트입니다.", + table: { + defaultValue: { summary: "div" }, + type: { summary: "React.ElementType" }, + }, + }, + ref: { + description: "'ref'를 사용하여 컴포넌트에 직접 접근할 수 있습니다.", + }, + }, +} satisfies Meta; + +export default meta; + +type Story = StoryObj; + +export const Default: Story = { + args: {}, +}; + +export const Small: Story = { + args: { + size: "sm", + }, +}; + +export const WithImage: Story = { + args: { + imageUrl: "https://i.ibb.co/2hz6hNP/2024-06-06-11-52-22.png", + }, +}; + +export const LargeWithUsername: Story = { + args: { + username: "김홍익 님", + }, +}; + +export const SmallWithUsernameOrientationLeft: Story = { + args: { + username: "김홍익 님", + size: "sm", + orientation: "left", + }, +}; + +export const SmallWithUsernameOrientationRight: Story = { + args: { + username: "김홍익 님", + size: "sm", + }, +}; diff --git a/packages/wow-ui/src/components/Avatar/index.tsx b/packages/wow-ui/src/components/Avatar/index.tsx new file mode 100644 index 00000000..018c6134 --- /dev/null +++ b/packages/wow-ui/src/components/Avatar/index.tsx @@ -0,0 +1,163 @@ +import { cva } from "@styled-system/css"; +import type { ElementType, PropsWithChildren, ReactNode } from "react"; +import { forwardRef } from "react"; +import { BlueAvatar, GreenAvatar, RedAvatar, YellowAvatar } from "wowds-icons"; + +import type { + PolymorphicComponentProps, + PolymorphicComponentPropsWithRef, + PolymorphicRef, +} from "@/types/polymorphic.ts"; + +/** + * @description Avatar 컴포넌트는 사용자 프로필을 나타내는 컴포넌트입니다. + * + * @param {("sm" | "lg")} [size="lg"] - 아바타의 크기. + * @param {("blue" | "green" | "yellow" | "red")} [variant="blue"] - 아바타의 색상. + * @param {ElementType} [ImageComponent] - 아바타에 표시할 이미지 컴포넌트. + * @param {string} [imageUrl] - 아바타에 표시할 이미지의 URL. + * @param {string} [username] - 아바타 옆에 표시할 사용자 이름. 사용자 이름이 제공되면 레이블로 표시됨. + * @param {("left" | "right")} [orientation="right"] - 사용자 이름 레이블의 방향. size가 'sm'인 경우 지정 가능. + * @param {React.ElementType} [asProp="div"] - 렌더링할 HTML 요소나 React 컴포넌트. 기본값은 "div". + */ + +type AvatarSizeType = "sm" | "lg"; + +export interface _AvatarProps { + size?: AvatarSizeType; + variant?: "blue" | "green" | "yellow" | "red"; + ImageComponent?: ElementType; + imageUrl?: string; + username?: string; + orientation?: "left" | "right"; +} + +type AvatarProps = PolymorphicComponentProps< + T, + _AvatarProps +> & + PropsWithChildren; + +type AvatarComponent = ( + props: PolymorphicComponentPropsWithRef> +) => ReactNode; + +const Avatar: AvatarComponent & { displayName?: string } = forwardRef( + ( + { + asProp, + size = "lg", + variant = "blue", + ImageComponent, + imageUrl, + username, + orientation = "right", + ...rest + }: AvatarProps, + ref?: PolymorphicRef + ) => { + const Component = asProp || "div"; + const AvatarComponent = avatarMap[variant]; + + return ( + + {ImageComponent ? ( + + ) : imageUrl ? ( + avatar + ) : ( + + )} + + + ); + } +); + +Avatar.displayName = "Avatar"; +export default Avatar; + +const avatarMap = { + blue: BlueAvatar, + green: GreenAvatar, + red: RedAvatar, + yellow: YellowAvatar, +}; + +const avatarContainerStyle = cva({ + base: { + height: "fit-content", + width: "fit-content", + display: "flex", + justifyContent: "center", + alignItems: "center", + }, + variants: { + size: { + sm: { + gap: 8, + }, + lg: { + gap: 12, + }, + }, + orientation: { + default: { + flexDirection: "column", + }, + left: { + flexDirection: "row-reverse", + }, + right: { + flexDirection: "row", + }, + }, + }, +}); + +const avatarSizeStyle = cva({ + base: { + borderRadius: "50%", + }, + variants: { + size: { + sm: { + width: 32, + height: 32, + }, + lg: { + width: 100, + height: 100, + }, + }, + }, +}); + +const avatarLabelStyle = cva({ + variants: { + size: { + sm: { + textStyle: "label1", + }, + lg: { + textStyle: "h1", + }, + }, + }, +}); diff --git a/packages/wow-ui/src/components/Checkbox/index.tsx b/packages/wow-ui/src/components/Checkbox/index.tsx index 6e8169b7..325cd822 100644 --- a/packages/wow-ui/src/components/Checkbox/index.tsx +++ b/packages/wow-ui/src/components/Checkbox/index.tsx @@ -116,7 +116,7 @@ const Checkbox = forwardRef( })} {...inputProps} value={value} - onClick={() => handleClick(value)} + onChange={() => handleClick(value)} /> {checked && ( ; + +export default meta; + +export const Default: StoryObj = { + args: {}, +}; + +export const Login: StoryObj = { + args: { + variant: "login", + }, +}; + +export const Username: StoryObj = { + args: { + variant: "username", + username: "김홍익", + }, +}; diff --git a/packages/wow-ui/src/components/Header/index.tsx b/packages/wow-ui/src/components/Header/index.tsx new file mode 100644 index 00000000..f505748e --- /dev/null +++ b/packages/wow-ui/src/components/Header/index.tsx @@ -0,0 +1,115 @@ +/** + * @description 헤더 컴포넌트입니다. + * 사이트 로고와 로그인 또는 사용자 이름 표시 기능을 포함합니다. + * + * @template T + * @param {"username" | "login" | "none"} [variant] - Header 종류. + * - "username": 사용자 이름을 표시. + * - "login": 로그인 버튼을 표시. + * - "none": 아무것도 표시하지 않음. + * @param {T extends "username" ? string : never} [username] - variant가 "username"인 경우 표시되는 오른쪽 요소. 사용자가 로그인한 경우 사용자 이름을 표시함. + * @param {T extends "login" ? () => void : never} [onClick] - 로그인 버튼 클릭 시 호출되는 함수. + */ + +import { css } from "@styled-system/css"; +import { Flex } from "@styled-system/jsx"; +import { GdscLogo } from "wowds-icons"; + +import Button from "@/components/Button"; + +type RightElementType = "username" | "login" | "none"; + +export interface HeaderProps { + variant?: T; + username?: T extends "username" ? string : never; + onClick?: T extends "login" ? () => void : never; +} + +const Header = ({ + variant = "none", + username, + onClick, +}: HeaderProps) => { + return ( +
+
+
+ + +

GDSC

+

Hongik Univ.

+
+
+
+ {variant === "login" && ( + + )} + {variant === "username" && ( +
+ {username} 님 +
+ )} +
+
+
+ ); +}; + +export default Header; + +const headerStyle = css({ + borderBottomStyle: "solid", + borderBottomWidth: 1, + borderBottomColor: "outline", + height: "54px", + width: "100%", + display: "flex", + alignItems: "center", + xsToLg: { + paddingX: "16px", + height: "66px", + }, +}); + +const headerContentStyle = css({ + width: "988px", + display: "flex", + justifyContent: "space-between", + alignItems: "center", + margin: "auto", +}); + +const leftElementContainerStyle = css({ + display: "flex", + gap: "xs", + alignItems: "center", +}); + +const titleStyle = css({ + fontFamily: "Product Sans", + textStyle: "header1", +}); + +const subTitleStyle = css({ + fontFamily: "Product Sans", + color: "primary", + textStyle: "header2", +}); + +const usernameTextStyle = css({ + textStyle: "label1", +}); + +const logoTextContainerStyle = css({ + lg: { + gap: "xs", + alignItems: "center", + }, + xsToLg: { + flexDirection: "column", + justifyContent: "flex-start", + alignItems: "space-between", + }, +}); diff --git a/packages/wow-ui/src/components/Spinner/BlueSpinner.tsx b/packages/wow-ui/src/components/Spinner/BlueSpinner.tsx index 354d26b6..c2be21f5 100644 --- a/packages/wow-ui/src/components/Spinner/BlueSpinner.tsx +++ b/packages/wow-ui/src/components/Spinner/BlueSpinner.tsx @@ -2,7 +2,7 @@ import type { LottieComponentProps } from "lottie-react"; import Lottie from "lottie-react"; import type { CSSProperties } from "react"; -import blueSpinner from "@/assets/lottie/blueSpinner.json"; +import blueSpinner from "@/assets/lotties/blueSpinner.json"; /** * @description 블루 스피너 컴포넌트입니다. diff --git a/packages/wow-ui/src/components/Spinner/RainbowSpinner.tsx b/packages/wow-ui/src/components/Spinner/RainbowSpinner.tsx index 5020d788..e1731fca 100644 --- a/packages/wow-ui/src/components/Spinner/RainbowSpinner.tsx +++ b/packages/wow-ui/src/components/Spinner/RainbowSpinner.tsx @@ -2,7 +2,7 @@ import type { LottieComponentProps } from "lottie-react"; import Lottie from "lottie-react"; import type { CSSProperties } from "react"; -import rainbowSpinner from "@/assets/lottie/rainbowSpinner.json"; +import rainbowSpinner from "@/assets/lotties/rainbowSpinner.json"; /** * @description 레인보우 스피너 컴포넌트입니다. Lottie 애니메이션을 사용하여 스피너를 표시합니다. diff --git a/packages/wow-ui/src/components/Switch/index.tsx b/packages/wow-ui/src/components/Switch/index.tsx index 46af598c..b36631e8 100644 --- a/packages/wow-ui/src/components/Switch/index.tsx +++ b/packages/wow-ui/src/components/Switch/index.tsx @@ -106,7 +106,7 @@ const Switch = forwardRef( ref={ref} type="checkbox" value={value} - onClick={() => handleClick(value)} + onChange={() => handleClick(value)} {...inputProps} /> diff --git a/packages/wow-ui/src/components/Tabs/Tabs.stories.tsx b/packages/wow-ui/src/components/Tabs/Tabs.stories.tsx new file mode 100644 index 00000000..23020448 --- /dev/null +++ b/packages/wow-ui/src/components/Tabs/Tabs.stories.tsx @@ -0,0 +1,196 @@ +import type { Meta, StoryObj } from "@storybook/react"; +import { useState } from "react"; + +import type { TabsProps } from "@/components/Tabs"; +import Tabs from "@/components/Tabs"; +import TabsContent from "@/components/Tabs/TabsContent"; +import TabsItem from "@/components/Tabs/TabsItem"; +import TabsList from "@/components/Tabs/TabsList"; + +const meta: Meta = { + title: "UI/Tabs", + component: Tabs, + tags: ["autodocs"], + parameters: { + componentSubtitle: "탭을 통해 콘텐츠를 선택할 수 있는 컴포넌트입니다.", + docs: { + description: { + component: + "TabsList 로 TabsItem을 감싸서 탭 트리거를 관리하고 TabsContent 로 탭 콘텐츠를 관리합니다.", + }, + }, + a11y: { + config: { + rules: [{ id: "color-contrast", enabled: false }], + }, + }, + }, + argTypes: { + children: { + description: "TabsList,TabsItem,TabsContent 를 children 으로 받습니다.", + table: { + type: { summary: "ReactNode" }, + }, + control: false, + }, + value: { + description: "현재 선택된 탭의 값을 나타냅니다.", + table: { + type: { summary: "string" }, + }, + control: "text", + }, + defaultValue: { + description: "초기 선택된 탭 값을 나타냅니다.", + table: { + type: { summary: "string" }, + }, + control: "text", + }, + onChange: { + description: "탭 값이 변경될 때 호출되는 함수입니다.", + table: { + type: { summary: "(value: string) => void" }, + }, + action: "changed", + }, + label: { + description: "각 탭을 구분할 수 있는 레이블입니다.", + table: { + type: { summary: "string" }, + }, + control: "text", + }, + style: { + description: "탭의 커스텀 스타일을 설정합니다.", + table: { + type: { summary: "CSSProperties" }, + defaultValue: { summary: "{}" }, + }, + control: false, + }, + className: { + description: "탭에 전달하는 커스텀 클래스를 설정합니다.", + table: { + type: { summary: "string" }, + }, + control: { + type: "text", + }, + }, + }, +} satisfies Meta; + +export default meta; + +type Story = StoryObj; + +export const Primary: Story = { + args: { + children: ( + <> + + Tab 1 + Tab 2 + + Tab 1 Content + Tab 2 Content + + ), + defaultValue: "tab1", + }, + parameters: { + docs: { + description: { + story: "기본적인 탭 컴포넌트입니다. 탭 1과 탭 2가 제공됩니다.", + }, + }, + }, +}; + +export const WithDefaultValue: Story = { + args: { + children: ( + <> + + Tab 1 + Tab 2 + Tab 3 + + Tab 1 Content + Tab 2 Content + Tab 3 Content + + ), + defaultValue: "tab2", + }, + parameters: { + docs: { + description: { + story: + "초기 값으로 두 번째 탭이 선택된 상태로 시작하는 컴포넌트입니다.", + }, + }, + }, +}; + +const ControlledTabsComponent = () => { + const [selectedTab, setSelectedTab] = useState("tab1"); + + const handleChange = (value: string) => { + setSelectedTab(value); + }; + + return ( + + + Tab 1 + Tab 2 + Tab 3 + + Tab 1 Content + Tab 2 Content + Tab 3 Content + + ); +}; + +export const ControlledValue: Story = { + render: () => , + parameters: { + docs: { + description: { + story: "외부 상태에 따라 제어되는 탭 컴포넌트입니다.", + }, + }, + }, +}; + +export const ManyTabs: Story = { + args: { + children: ( + <> + + {Array.from({ length: 10 }, (_, index) => ( + + Tab {index + 1} + + ))} + + {Array.from({ length: 10 }, (_, index) => ( + + Tab {index + 1} Content + + ))} + + ), + defaultValue: "tab1", + }, + parameters: { + docs: { + description: { + story: "여러 개의 탭을 가진 탭 컴포넌트입니다.", + }, + }, + }, +}; diff --git a/packages/wow-ui/src/components/Tabs/Tabs.test.tsx b/packages/wow-ui/src/components/Tabs/Tabs.test.tsx new file mode 100644 index 00000000..32b9c992 --- /dev/null +++ b/packages/wow-ui/src/components/Tabs/Tabs.test.tsx @@ -0,0 +1,87 @@ +import type { RenderResult } from "@testing-library/react"; +import { render, waitFor } from "@testing-library/react"; +import { userEvent } from "@testing-library/user-event"; + +import type { TabsProps } from "@/components/Tabs"; +import Tabs from "@/components/Tabs"; +import TabsContent from "@/components/Tabs/TabsContent"; +import TabsItem from "@/components/Tabs/TabsItem"; +import TabsList from "@/components/Tabs/TabsList"; + +describe("Tabs component", () => { + const uncontrolledTabs = (props: Partial = {}): RenderResult => { + return render( + + + Tab 1 + Tab 2 + + Tab 1 Content + Tab 2 Content + + ); + }; + + const controlledTabs = (props: Partial = {}): RenderResult => { + return render( + + + Tab 1 + Tab 2 + + Tab 1 Content + Tab 2 Content + + ); + }; + test("renders correctly with default value", async () => { + const { getByText } = uncontrolledTabs(); + expect(getByText("Tab 1 Content")).toBeVisible(); + }); + + test("switches content when clicking on tab triggers", async () => { + const { getByText } = uncontrolledTabs(); + await userEvent.click(getByText("Tab 2")); + await waitFor(() => { + expect(getByText("Tab 2 Content")).toBeVisible(); + }); + }); + + test("calls onChange when the tab is changed", async () => { + const handleChange = jest.fn(); + const { getByText } = controlledTabs({ + value: "tab1", + onChange: handleChange, + }); + await userEvent.click(getByText("Tab 2")); + expect(handleChange).toHaveBeenCalledWith("tab2"); + }); + + test("can navigate between tabs using keyboard (ArrowRight)", async () => { + const { getByText } = uncontrolledTabs(); + const tab1 = getByText("Tab 1"); + const tab2 = getByText("Tab 2"); + + tab1.focus(); + await userEvent.keyboard("{ArrowRight}"); + expect(tab2).toHaveFocus(); + + await waitFor(() => { + expect(getByText("Tab 2 Content")).toBeVisible(); + }); + }); + + test("can navigate between tabs using keyboard (ArrowLeft)", async () => { + const { getByText } = uncontrolledTabs(); + const tab1 = getByText("Tab 1"); + const tab2 = getByText("Tab 2"); + + tab1.focus(); + await userEvent.keyboard("{ArrowLeft}"); + expect(tab2).toHaveFocus(); + + await waitFor(() => { + expect(getByText("Tab 2 Content")).toBeVisible(); + }); + }); +}); diff --git a/packages/wow-ui/src/components/Tabs/TabsContent.tsx b/packages/wow-ui/src/components/Tabs/TabsContent.tsx new file mode 100644 index 00000000..ce6234a5 --- /dev/null +++ b/packages/wow-ui/src/components/Tabs/TabsContent.tsx @@ -0,0 +1,43 @@ +"use client"; + +import type { PropsWithChildren } from "react"; +import { forwardRef } from "react"; + +import type { DefaultProps } from "@/types/DefaultProps"; + +import { useTabsContext } from "./contexts/TabsContext"; + +/** + * @description TabsContent 컴포넌트는 각 Tab에 해당하는 콘텐츠입니다. + * @param {string} value - TabTrigger의 value와 일치하는 값입니다. + * @param {string} [className] - TabsContent에 전달할 커스텀 클래스. + * @param {CSSProperties} [style] - TabsContent에 전달할 커스텀 스타일. + * @param {ComponentPropsWithoutRef} rest 렌더링된 요소 또는 컴포넌트에 전달할 추가 props. + * @param {ComponentPropsWithRef["ref"]} ref 렌더링된 요소 또는 컴포넌트에 연결할 ref. + * @param {ReactNode} children - TabsContent의 자식 요소. + */ +interface TabsContentProps extends PropsWithChildren, DefaultProps { + value: string; +} + +const TabsContent = forwardRef( + ({ value: tabValue, children }: TabsContentProps, ref) => { + const { value, label } = useTabsContext(); + const selected = tabValue === value; + if (!selected) return null; + + return ( +
+ {children} +
+ ); + } +); + +export default TabsContent; diff --git a/packages/wow-ui/src/components/Tabs/TabsItem.tsx b/packages/wow-ui/src/components/Tabs/TabsItem.tsx new file mode 100644 index 00000000..92e15ca2 --- /dev/null +++ b/packages/wow-ui/src/components/Tabs/TabsItem.tsx @@ -0,0 +1,100 @@ +"use client"; + +import { cva } from "@styled-system/css"; +import { clsx } from "clsx"; +import type { ButtonHTMLAttributes, PropsWithChildren } from "react"; +import { forwardRef, useEffect, useRef } from "react"; + +import { useMergeRefs } from "@/hooks/useMergeRefs"; +import type { DefaultProps } from "@/types/DefaultProps"; + +import { useCollectionContext } from "./contexts/CollectionContext"; +import { useTabsContext } from "./contexts/TabsContext"; + +/** + * @description TabsItem 컴포넌트는 각 Tab 컴포넌트입니다. + * @param {string} value - TabsContent의 value와 일치하는 값입니다. + * @param {ReactNode} children - TabsContent 자식 요소. + * @param {string} [className] - TabsItem에 전달할 커스텀 클래스. + * @param {CSSProperties} [style] - TabsItem에 전달할 커스텀 스타일. + * @param {ComponentPropsWithoutRef} rest 렌더링된 요소 또는 컴포넌트에 전달할 추가 props. + * @param {ComponentPropsWithRef["ref"]} ref 렌더링된 요소 또는 컴포넌트에 연결할 ref. + * @param {ReactNode} children - TabsItem의 자식 요소. + */ +interface TabsItemProps + extends PropsWithChildren, + DefaultProps, + ButtonHTMLAttributes { + value: string; +} + +const TabsItem = forwardRef( + ({ value, children, className, ...rest }: TabsItemProps, ref) => { + const { value: selectedValue, setSelectedValue, label } = useTabsContext(); + const selected = selectedValue === value; + + const handleClickTabTrigger = () => { + setSelectedValue(value); + }; + + const { values } = useCollectionContext(); + const internalButtonRef = useRef(null); + const buttonRef = useMergeRefs(ref, internalButtonRef); + + useEffect(() => { + values.add(value); + if (selected && internalButtonRef.current) { + internalButtonRef.current.focus(); + } + }, [values, selected, value]); + + return ( + + ); + } +); +export default TabsItem; + +const tabItemStyle = cva({ + base: { + textStyle: "label1", + paddingY: "sm", + paddingX: "14px", + borderBottom: "1px solid", + borderColor: "outline", + color: "sub", + outline: "none", + cursor: "pointer", + whiteSpace: "pre", + xsToSm: { + display: "flex", + flexGrow: 1, + justifyContent: "center", + alignItems: "center", + }, + }, + variants: { + type: { + selected: { + color: "primary", + borderColor: "primary", + }, + default: {}, + }, + }, +}); diff --git a/packages/wow-ui/src/components/Tabs/TabsList.tsx b/packages/wow-ui/src/components/Tabs/TabsList.tsx new file mode 100644 index 00000000..ed02fee9 --- /dev/null +++ b/packages/wow-ui/src/components/Tabs/TabsList.tsx @@ -0,0 +1,98 @@ +"use client"; + +import { css } from "@styled-system/css"; +import { Flex } from "@styled-system/jsx"; +import { + type KeyboardEvent, + type PropsWithChildren, + useCallback, + useEffect, +} from "react"; + +import { useCollectionContext } from "./contexts/CollectionContext"; +import { useTabsContext } from "./contexts/TabsContext"; + +/** + * @description TabsList 컴포넌트는 TabsItem 컴포넌트들을 관리합니다. + */ +const TabsList = ({ children }: PropsWithChildren) => { + const { + label, + setSelectedValue, + value: selectedValue, + isControlled, + } = useTabsContext(); + + const { values } = useCollectionContext(); + + useEffect(() => { + if (!isControlled && !selectedValue && values.size > 0) { + setSelectedValue(values.values().next().value); + } + }, []); + + const updateFocusedValue = useCallback( + (direction: number) => { + const valuesArray = Array.from(values); + const currentIndex = valuesArray.indexOf(selectedValue ?? ""); + const nextIndex = + (currentIndex + direction + valuesArray.length) % valuesArray.length; + setSelectedValue(valuesArray[nextIndex] ?? ""); + }, + [setSelectedValue, selectedValue, values] + ); + + const handleArrowNavigation = useCallback( + (direction: number, event: KeyboardEvent) => { + updateFocusedValue(direction); + event.preventDefault(); + }, + [updateFocusedValue] + ); + + const handleKeyDown = useCallback( + (event: KeyboardEvent) => { + const { key } = event; + + if (key === "ArrowRight") { + handleArrowNavigation(1, event); + } else if (key === "ArrowLeft") { + handleArrowNavigation(-1, event); + } + }, + [handleArrowNavigation] + ); + + return ( + + {children} + + ); +}; + +export default TabsList; + +const tabsListStyle = css({ + overflowX: "scroll", + scrollBehavior: "smooth", + _scrollbar: { + width: "65px", + height: "2px", + }, + _scrollbarThumb: { + width: "65px", + height: "2px", + borderRadius: "sm", + backgroundColor: "outline", + }, + _scrollbarTrack: { + marginTop: "2px", + marginBottom: "2px", + }, +}); diff --git a/packages/wow-ui/src/components/Tabs/contexts/CollectionContext.tsx b/packages/wow-ui/src/components/Tabs/contexts/CollectionContext.tsx new file mode 100644 index 00000000..f0ce0217 --- /dev/null +++ b/packages/wow-ui/src/components/Tabs/contexts/CollectionContext.tsx @@ -0,0 +1,25 @@ +"use client"; + +import type { PropsWithChildren } from "react"; +import { createContext } from "react"; + +import useSafeContext from "@/hooks/useSafeContext"; + +interface CollectionContextProps { + values: Set; +} + +const CollectionContext = createContext(null); + +export const useCollectionContext = () => { + const context = useSafeContext(CollectionContext); + return context; +}; + +export const CollectionProvider = ({ children }: PropsWithChildren) => { + return ( + () }}> + {children} + + ); +}; diff --git a/packages/wow-ui/src/components/Tabs/contexts/TabsContext.ts b/packages/wow-ui/src/components/Tabs/contexts/TabsContext.ts new file mode 100644 index 00000000..60259df4 --- /dev/null +++ b/packages/wow-ui/src/components/Tabs/contexts/TabsContext.ts @@ -0,0 +1,19 @@ +"use client"; + +import { createContext } from "react"; + +import useSafeContext from "@/hooks/useSafeContext"; + +interface TabsContextProps { + value: string; + setSelectedValue: (value: string) => void; + label?: string; + isControlled: boolean; +} + +export const TabsContext = createContext(null); + +export const useTabsContext = () => { + const context = useSafeContext(TabsContext); + return context; +}; diff --git a/packages/wow-ui/src/components/Tabs/index.tsx b/packages/wow-ui/src/components/Tabs/index.tsx new file mode 100644 index 00000000..50edce44 --- /dev/null +++ b/packages/wow-ui/src/components/Tabs/index.tsx @@ -0,0 +1,70 @@ +"use client"; + +import { css } from "@styled-system/css"; +import { clsx } from "clsx"; +import type { PropsWithChildren } from "react"; +import { useRef, useState } from "react"; + +import { CollectionProvider } from "@/components/Tabs/contexts/CollectionContext"; +import { TabsContext } from "@/components/Tabs/contexts/TabsContext"; +import type { DefaultProps } from "@/types/DefaultProps"; + +/** + * @description Tabs 컴포넌트는 탭을 통해 콘텐츠를 선택할 수 있는 컴포넌트입니다. + * @param {string} [defaultValue] - 탭의 기본값입니다. + * @param {string} [value] - 현재 선택된 탭의 값입니다. + * @param {string} [label] - 각 탭을 구분할 수 있는 레이블입니다. + * @param {(value: string) => void} [onChange] - 탭이 변경될 때 호출되는 함수입니다. + * @param {CSSProperties} [style] - 탭 컴포넌트의 커스텀 스타일. + * @param {string} [className] - 탭 컴포넌트에 전달할 커스텀 클래스. + * @param {ReactNode} children - 탭의 자식 요소. + */ +export interface TabsProps extends PropsWithChildren, DefaultProps { + defaultValue?: string; + value?: string; + label?: string; + onChange?: (value: string) => void; +} +const Tabs = ({ + defaultValue, + value: valueProp, + label = "default-tab", + children, + onChange, + className, + style, +}: TabsProps) => { + const [selectedValue, setSelectedValue] = useState(defaultValue ?? ""); + const isControlled = useRef(valueProp !== undefined).current; + + const handleSelect = (selectedValue: string) => { + if (!isControlled) { + setSelectedValue(selectedValue); + return; + } + if (onChange) { + onChange(selectedValue); + } + }; + + return ( +
+ + {children} + +
+ ); +}; + +export default Tabs; + +const tabsContainerStyle = css({ + width: "100%", +}); diff --git a/packages/wow-ui/src/components/Toast/Toast.stories.tsx b/packages/wow-ui/src/components/Toast/Toast.stories.tsx new file mode 100644 index 00000000..11451a3f --- /dev/null +++ b/packages/wow-ui/src/components/Toast/Toast.stories.tsx @@ -0,0 +1,173 @@ +import type { Meta, StoryObj } from "@storybook/react"; +import { useEffect } from "react"; +import { Warn } from "wowds-icons"; + +import Button from "@/components/Button"; + +import Toast from "."; +import ToastProvider from "./ToastProvider"; +import useToast from "./useToast"; + +const meta: Meta = { + title: "UI/Toast", + component: Toast, + tags: ["autodocs"], + parameters: { + componentSubtitle: "Toast 컴포넌트", + a11y: { + config: { + rules: [{ id: "color-contrast", enabled: false }], + }, + }, + docs: { + description: { + component: + "토스트가 필요한 레이아웃에서 children을 ToastProvider로 감싸 사용합니다.", + }, + }, + }, + decorators: [ + (Story) => ( + + + + ), + ], + argTypes: { + text: { + description: "Toast에 들어갈 메인 텍스트를 나타냅니다.", + control: { type: "text" }, + }, + subText: { + description: "Toast에 들어갈 보조 텍스트를 나타냅니다.", + control: { type: "text" }, + }, + rightIcon: { + description: "Toast의 우측에 들어갈 아이콘을 나타냅니다.", + table: { + type: { summary: "none | close | arrow" }, + defaultValue: { summary: "none" }, + }, + control: "radio", + options: ["none", "close", "arrow"], + }, + id: { + description: "Toast 컴포넌트의 id를 나타냅니다.", + control: false, + }, + onClickArrowIcon: { + description: + "Toast 컴포넌트의 화살표 아이콘을 클릭했을 때 호출되는 함수를 나타냅니다.", + control: false, + }, + onRemove: { + description: "Toast 컴포넌트가 닫힌 이후 호출되는 함수를 나타냅니다.", + control: false, + }, + showLeftIcon: { + description: "Toast 좌측에 들어갈 아이콘의 노출 여부를 나타냅니다.", + control: "boolean", + }, + toastDuration: { + description: "Toast가 보여지는 시간(ms)을 나타냅니다.", + control: { type: "number" }, + }, + style: { + description: "Toast에 커스텀 스타일을 적용하기 위한 객체를 나타냅니다.", + control: false, + }, + className: { + description: "Toast에 커스텀 클래스를 적용하기 위한 문자열을 나타냅니다.", + control: false, + }, + }, +} satisfies Meta; + +export default meta; + +type Story = StoryObj; + +export const Default: Story = { + args: { + id: "1", + text: "Text", + subText: "subtext", + toastDuration: 60 * 60 * 1000, + }, +}; + +export const WithTrigger = () => { + const { toast } = useToast(); + + return ( + + ); +}; + +export const WithCloseIcon = () => { + const { toast } = useToast(); + useEffect(() => { + toast({ + text: "Text", + subText: "subtext", + rightIcon: "close", + }); + }, []); +}; + +export const WithArrowIcon = () => { + const { toast } = useToast(); + useEffect(() => { + toast({ + text: "Text", + subText: "subtext", + rightIcon: "arrow", + }); + }, []); +}; + +export const WithLeftIcon = () => { + const { toast } = useToast(); + useEffect(() => { + toast({ + text: "Text", + subText: "subtext", + showLeftIcon: true, + }); + }, []); +}; + +export const WithLeftAndArrowIcons = () => { + const { toast } = useToast(); + useEffect(() => { + toast({ + text: "Text", + subText: "subtext", + showLeftIcon: true, + rightIcon: "arrow", + }); + }, []); +}; + +export const TwoLines = () => { + const { toast } = useToast(); + useEffect(() => { + toast({ + showLeftIcon: true, + text: "TextTextTextTextTextTextTextTextTextTextTextTextTextTextTextText", + }); + }, []); +}; + +export const Slow = () => { + const { toast } = useToast(); + useEffect(() => { + toast({ + text: "Text", + subText: "subtext", + toastDuration: 5000, + }); + }, []); +}; diff --git a/packages/wow-ui/src/components/Toast/ToastContext.ts b/packages/wow-ui/src/components/Toast/ToastContext.ts new file mode 100644 index 00000000..aff6f0f2 --- /dev/null +++ b/packages/wow-ui/src/components/Toast/ToastContext.ts @@ -0,0 +1,22 @@ +import { createContext } from "react"; + +import useSafeContext from "@/hooks/useSafeContext"; + +import type { ToastProps } from "."; + +interface ToastContextProps { + toasts: ToastProps[]; + addToast: ( + toast: Omit & Partial> + ) => void; + removeToast: (id: string) => void; +} + +export const ToastContext = createContext( + undefined +); + +export const useToastContext = () => { + const context = useSafeContext(ToastContext); + return context; +}; diff --git a/packages/wow-ui/src/components/Toast/ToastProvider.tsx b/packages/wow-ui/src/components/Toast/ToastProvider.tsx new file mode 100644 index 00000000..aa766870 --- /dev/null +++ b/packages/wow-ui/src/components/Toast/ToastProvider.tsx @@ -0,0 +1,63 @@ +"use client"; + +import { Flex } from "@styled-system/jsx"; +import type { ReactNode } from "react"; +import { useState } from "react"; +import { v4 as uuidv4 } from "uuid"; + +import type { ToastProps } from "."; +import Toast from "."; +import { ToastContext } from "./ToastContext"; + +interface ToastProviderProps { + children: ReactNode; + idPattern?: RegExp; +} + +const ToastProvider = ({ children, idPattern }: ToastProviderProps) => { + const [toasts, setToasts] = useState([]); + + const addToast = ( + props: Omit & Partial> + ) => { + const newToast = { + ...props, + id: props.id || uuidv4(), + }; + + setToasts((prev) => [...prev, newToast]); + }; + + const removeToast = (id: string) => { + setToasts((prev) => prev.filter((toast) => toast.id !== id)); + }; + + const filteredToasts = idPattern + ? toasts.filter((toast) => idPattern.test(toast.id)) + : toasts; + + return ( + + {toasts.length > 0 && ( + + {filteredToasts?.map((toast: ToastProps) => ( + + ))} + + )} + {children} + + ); +}; + +export default ToastProvider; diff --git a/packages/wow-ui/src/components/Toast/index.tsx b/packages/wow-ui/src/components/Toast/index.tsx new file mode 100644 index 00000000..efd10216 --- /dev/null +++ b/packages/wow-ui/src/components/Toast/index.tsx @@ -0,0 +1,131 @@ +"use client"; + +import { css } from "@styled-system/css"; +import type { FlexProps } from "@styled-system/jsx"; +import { Flex, styled } from "@styled-system/jsx"; +import type { CSSProperties } from "react"; +import { forwardRef, useEffect, useState } from "react"; +import type { IconProps } from "wowds-icons"; +import { Close, RightArrow, Warn } from "wowds-icons"; + +import useToast from "./useToast"; + +/** + * @description 토스트 컴포넌트입니다. + * + * @param {string} id - 토스트 컴포넌트의 id. + * @param {"default"|"close"|"arrow"} [type] - 토스트 컴포넌트의 타입. + * @param {string} text - 토스트 컴포넌트의 메인 텍스트. + * @param {ReactNode} icon - 토스트 컴포넌트의 좌측에 들어갈 아이콘. + * @param {()=>void} [onClickArrowIcon] - 화살표 아이콘을 클릭했을 때 호출되는 함수. + * @param {()=>void} [onRemove] - 토스트 컴포넌트가 닫히고 나서 호출되는 함수. + * @param {string} [subText] - 토스트 컴포넌트의 보조 텍스트. + * @param {string} [toastDuration] - 토스트 컴포넌트의 보여지는 시간. + * @param {CSSProperties} [style] - 커스텀 스타일을 적용하기 위한 객체. + * @param {string} [className] - 커스텀 클래스를 적용하기 위한 문자열. + */ + +export interface ToastProps extends FlexProps { + id: string; + showLeftIcon?: boolean; + rightIcon?: "none" | "close" | "arrow"; + text: string; + onClickArrowIcon?: () => void; + onRemove?: () => void; + subText?: string; + toastDuration?: number; + style?: CSSProperties; + className?: string; +} + +const Toast = forwardRef( + ({ + id, + text, + subText, + onClickArrowIcon, + onRemove, + rightIcon = "none", + showLeftIcon = false, + toastDuration, + ...rest + }: ToastProps) => { + const TOAST_DURATION = toastDuration || 2000; + const ANIMATION_DURATION = 200; + const { removeToast } = useToast(); + + const RightIcon = (props: IconProps) => { + if (rightIcon === "close") + return removeToast(id)} {...props} />; + else if (rightIcon === "arrow") + return ; + return null; + }; + + const [opacity, setOpacity] = useState(0.2); + + useEffect(() => { + setOpacity(1); + const timeoutForRemove = setTimeout(() => { + removeToast(id); + onRemove?.(); + }, TOAST_DURATION); + + const timeoutForVisible = setTimeout(() => { + setOpacity(0); + }, TOAST_DURATION - ANIMATION_DURATION); + + return () => { + clearTimeout(timeoutForRemove); + clearTimeout(timeoutForVisible); + }; + }, [id, removeToast]); + + return ( + + + {showLeftIcon && } + + + {text} + + {subText && ( + + {subText} + + )} + + + + + ); + } +); + +const toastContainerStyle = css({ + width: "22.375rem", + height: "fit-content", + padding: "0.75rem 1rem", + + borderRadius: "md", + + background: "backgroundDimmer", + backdropFilter: "blur(30px)", + boxShadow: "mono", +}); + +Toast.displayName = "Toast"; +export default Toast; diff --git a/packages/wow-ui/src/components/Toast/useToast.ts b/packages/wow-ui/src/components/Toast/useToast.ts new file mode 100644 index 00000000..31c618d7 --- /dev/null +++ b/packages/wow-ui/src/components/Toast/useToast.ts @@ -0,0 +1,12 @@ +import { useToastContext } from "./ToastContext"; + +const useToast = () => { + const { addToast, removeToast } = useToastContext(); + + return { + toast: addToast, + removeToast, + }; +}; + +export default useToast; diff --git a/packages/wow-ui/src/hooks/useMergeRefs.ts b/packages/wow-ui/src/hooks/useMergeRefs.ts new file mode 100644 index 00000000..11b535d8 --- /dev/null +++ b/packages/wow-ui/src/hooks/useMergeRefs.ts @@ -0,0 +1,13 @@ +import type { MutableRefObject, Ref } from "react"; + +export function useMergeRefs(...refs: (Ref | null)[]) { + return (value: T | null) => { + refs.forEach((ref) => { + if (typeof ref === "function") { + ref(value); + } else if (ref !== null && typeof ref === "object") { + (ref as MutableRefObject).current = value; + } + }); + }; +} diff --git a/packages/wow-ui/src/types/DefaultProps.ts b/packages/wow-ui/src/types/DefaultProps.ts new file mode 100644 index 00000000..ace10e37 --- /dev/null +++ b/packages/wow-ui/src/types/DefaultProps.ts @@ -0,0 +1,11 @@ +import type { CSSProperties } from "react"; + +/** + * @description 컴포넌트에 전달한 기본적으로 전달한 props 입니다. + * @property {string} className - 컴포넌트에 전달할 커스텀 클래스명입니다. + * @property {CSSProperties} style - 컴포넌트에 전달할 커스텀 스타일입니다. + */ +export interface DefaultProps { + className?: string; + style?: CSSProperties; +} diff --git a/packages/wow-ui/src/types/index.ts b/packages/wow-ui/src/types/index.ts index 2d701f74..e904c893 100644 --- a/packages/wow-ui/src/types/index.ts +++ b/packages/wow-ui/src/types/index.ts @@ -3,9 +3,9 @@ export type { ButtonElementType, MenuButtonProps, ToggleButtonProps, -} from "./Button"; +} from "./button.ts"; export type { PolymorphicComponentProps, PolymorphicComponentPropsWithRef, PolymorphicRef, -} from "./Polymorphic"; +} from "./polymorphic.ts"; diff --git a/packages/wow-ui/src/types/Polymorphic.ts b/packages/wow-ui/src/types/polymorphic.ts similarity index 74% rename from packages/wow-ui/src/types/Polymorphic.ts rename to packages/wow-ui/src/types/polymorphic.ts index 72c7681c..f9dedd89 100644 --- a/packages/wow-ui/src/types/Polymorphic.ts +++ b/packages/wow-ui/src/types/polymorphic.ts @@ -13,12 +13,12 @@ export type PolymorphicRef = export type PolymorphicComponentPropsWithRef< C extends ElementType, - Props = {}, -> = Props & { ref?: PolymorphicRef }; + ComponentProps = {}, +> = ComponentProps & { ref?: PolymorphicRef }; export type PolymorphicComponentProps< T extends ElementType, - Props = {}, + ComponentProps = {}, > = AsProps & ComponentPropsWithoutRef & - PolymorphicComponentPropsWithRef; + PolymorphicComponentPropsWithRef; diff --git a/packages/wow-ui/styled-system/styles.css b/packages/wow-ui/styled-system/styles.css index 49dc1210..655517da 100644 --- a/packages/wow-ui/styled-system/styles.css +++ b/packages/wow-ui/styled-system/styles.css @@ -169,104 +169,443 @@ progress { :-moz-focusring { outline: auto; } -[hidden]:not(#\#):not(#\#):not(#\#):not(#\#):not(#\#):not(#\#):not(#\#) { +[hidden]:not(#\#):not(#\#):not(#\#):not(#\#):not(#\#) { display: none !important; } +body:not(#\#) { + div: border-box; + button: border-box; +} :where(:root, :host):not(#\#):not(#\#) { + --colors-red-50: #fdeceb; + --colors-red-100: #fbd9d7; + --colors-red-150: #f9c7c2; + --colors-red-200: #f7b4ae; + --colors-red-300: #f28e86; --colors-red-400: #ee695d; --colors-red-500: #ea4335; + --colors-red-600: #bb362a; + --colors-red-700: #8c2820; + --colors-red-800: #5e1b15; + --colors-red-850: #461410; + --colors-red-900: #2f0d0b; + --colors-red-950: #170705; + --colors-blue-50: #ebf4fe; --colors-blue-100: #d7e9fd; + --colors-blue-150: #c3ddfd; + --colors-blue-200: #afd2fc; + --colors-blue-300: #86bcfa; --colors-blue-400: #5ea5f9; + --colors-blue-500: #368ff7; + --colors-blue-600: #2b72c6; + --colors-blue-700: #205694; + --colors-blue-800: #163963; + --colors-blue-850: #102b4a; + --colors-blue-900: #0b1d31; + --colors-blue-950: #050e19; + --colors-yellow-50: #fef7e6; + --colors-yellow-100: #feeecc; + --colors-yellow-150: #fde6b3; + --colors-yellow-200: #fddd99; + --colors-yellow-300: #fbcd66; + --colors-yellow-400: #fabc33; + --colors-yellow-500: #f9ab00; + --colors-yellow-600: #c78900; + --colors-yellow-700: #956700; + --colors-yellow-800: #644400; + --colors-yellow-850: #4b3300; + --colors-yellow-900: #322200; + --colors-yellow-950: #191100; + --colors-green-50: #ebf6ee; + --colors-green-100: #d6eedd; + --colors-green-150: #c2e5cb; + --colors-green-200: #aedcba; + --colors-green-300: #85cb98; + --colors-green-400: #5db975; + --colors-green-500: #34a853; + --colors-green-600: #2a8642; + --colors-green-700: #1f6532; + --colors-green-800: #154321; + --colors-green-850: #103219; + --colors-green-900: #0a2211; + --colors-green-950: #051108; + --colors-mono-50: #f7f7f7; + --colors-mono-100: #f0f0f0; + --colors-mono-150: #e8e8e8; + --colors-mono-200: #e1e1e1; + --colors-mono-300: #d1d1d1; + --colors-mono-400: #c2c2c2; + --colors-mono-500: #b3b3b3; + --colors-mono-600: #8f8f8f; + --colors-mono-700: #6b6b6b; + --colors-mono-800: #484848; + --colors-mono-900: #242424; + --colors-mono-950: #121212; + --colors-white: #ffffff; + --colors-black: #000000; --colors-primary: #368ff7; + --colors-success: #2a8642; + --colors-error: #bb362a; + --colors-background-normal: #ffffff; + --colors-background-alternative: #f7f7f7; + --colors-background-dimmer: rgba(0, 0, 0, 0.8); + --colors-sub: #6b6b6b; + --colors-outline: #c2c2c2; + --colors-text-black: #121212; + --colors-text-white: #ffffff; --colors-dark-disabled: #c2c2c2; + --colors-light-disabled: #e1e1e1; + --colors-blue-hover: #2b72c6; + --colors-mono-hover: #121212; + --colors-elevated-hover: rgba(16, 43, 74, 0.2); --colors-blue-pressed: #5ea5f9; --colors-blue-background-pressed: #ebf4fe; + --colors-mono-background-pressed: #f7f7f7; + --colors-shadow-small: rgba(0, 0, 0, 0.1); + --colors-shadow-medium: rgba(0, 0, 0, 0.2); + --colors-blue-shadow: rgba(16, 43, 74, 0.2); + --colors-discord: #5566fb; + --colors-github: #000000; + --colors-secondary-yellow: #f9ab00; + --colors-secondary-green: #34a853; + --colors-secondary-red: #ea4335; + --colors-error-background: #fbd9d7; + --colors-blue-disabled: #d7e9fd; + --colors-text-blue-disabled: #afd2fc; +} +.text_blue\.50:not(#\#):not(#\#):not(#\#):not(#\#) { + color: var(--colors-blue-50); +} +.text_blue\.100:not(#\#):not(#\#):not(#\#):not(#\#) { + color: var(--colors-blue-100); +} +.text_blue\.150:not(#\#):not(#\#):not(#\#):not(#\#) { + color: var(--colors-blue-150); +} +.text_blue\.200:not(#\#):not(#\#):not(#\#):not(#\#) { + color: var(--colors-blue-200); +} +.text_blue\.300:not(#\#):not(#\#):not(#\#):not(#\#) { + color: var(--colors-blue-300); +} +.text_blue\.400:not(#\#):not(#\#):not(#\#):not(#\#) { + color: var(--colors-blue-400); +} +.text_blue\.500:not(#\#):not(#\#):not(#\#):not(#\#) { + color: var(--colors-blue-500); +} +.text_blue\.600:not(#\#):not(#\#):not(#\#):not(#\#) { + color: var(--colors-blue-600); +} +.text_blue\.700:not(#\#):not(#\#):not(#\#):not(#\#) { + color: var(--colors-blue-700); +} +.text_blue\.800:not(#\#):not(#\#):not(#\#):not(#\#) { + color: var(--colors-blue-800); +} +.text_blue\.850:not(#\#):not(#\#):not(#\#):not(#\#) { + color: var(--colors-blue-850); +} +.text_blue\.900:not(#\#):not(#\#):not(#\#):not(#\#) { + color: var(--colors-blue-900); +} +.text_blue\.950:not(#\#):not(#\#):not(#\#):not(#\#) { + color: var(--colors-blue-950); +} +.text_yellow\.50:not(#\#):not(#\#):not(#\#):not(#\#) { + color: var(--colors-yellow-50); +} +.text_yellow\.100:not(#\#):not(#\#):not(#\#):not(#\#) { + color: var(--colors-yellow-100); +} +.text_yellow\.150:not(#\#):not(#\#):not(#\#):not(#\#) { + color: var(--colors-yellow-150); +} +.text_yellow\.200:not(#\#):not(#\#):not(#\#):not(#\#) { + color: var(--colors-yellow-200); +} +.text_yellow\.300:not(#\#):not(#\#):not(#\#):not(#\#) { + color: var(--colors-yellow-300); +} +.text_yellow\.400:not(#\#):not(#\#):not(#\#):not(#\#) { + color: var(--colors-yellow-400); +} +.text_yellow\.500:not(#\#):not(#\#):not(#\#):not(#\#) { + color: var(--colors-yellow-500); +} +.text_yellow\.600:not(#\#):not(#\#):not(#\#):not(#\#) { + color: var(--colors-yellow-600); +} +.text_yellow\.700:not(#\#):not(#\#):not(#\#):not(#\#) { + color: var(--colors-yellow-700); +} +.text_yellow\.800:not(#\#):not(#\#):not(#\#):not(#\#) { + color: var(--colors-yellow-800); +} +.text_yellow\.850:not(#\#):not(#\#):not(#\#):not(#\#) { + color: var(--colors-yellow-850); +} +.text_yellow\.900:not(#\#):not(#\#):not(#\#):not(#\#) { + color: var(--colors-yellow-900); +} +.text_yellow\.950:not(#\#):not(#\#):not(#\#):not(#\#) { + color: var(--colors-yellow-950); +} +.text_green\.50:not(#\#):not(#\#):not(#\#):not(#\#) { + color: var(--colors-green-50); +} +.text_green\.100:not(#\#):not(#\#):not(#\#):not(#\#) { + color: var(--colors-green-100); +} +.text_green\.150:not(#\#):not(#\#):not(#\#):not(#\#) { + color: var(--colors-green-150); +} +.text_green\.200:not(#\#):not(#\#):not(#\#):not(#\#) { + color: var(--colors-green-200); +} +.text_green\.300:not(#\#):not(#\#):not(#\#):not(#\#) { + color: var(--colors-green-300); +} +.text_green\.400:not(#\#):not(#\#):not(#\#):not(#\#) { + color: var(--colors-green-400); +} +.text_green\.500:not(#\#):not(#\#):not(#\#):not(#\#) { + color: var(--colors-green-500); +} +.text_green\.600:not(#\#):not(#\#):not(#\#):not(#\#) { + color: var(--colors-green-600); +} +.text_green\.700:not(#\#):not(#\#):not(#\#):not(#\#) { + color: var(--colors-green-700); +} +.text_green\.800:not(#\#):not(#\#):not(#\#):not(#\#) { + color: var(--colors-green-800); +} +.text_green\.850:not(#\#):not(#\#):not(#\#):not(#\#) { + color: var(--colors-green-850); +} +.text_green\.900:not(#\#):not(#\#):not(#\#):not(#\#) { + color: var(--colors-green-900); +} +.text_green\.950:not(#\#):not(#\#):not(#\#):not(#\#) { + color: var(--colors-green-950); +} +.text_red\.50:not(#\#):not(#\#):not(#\#):not(#\#) { + color: var(--colors-red-50); +} +.text_red\.100:not(#\#):not(#\#):not(#\#):not(#\#) { + color: var(--colors-red-100); +} +.text_red\.150:not(#\#):not(#\#):not(#\#):not(#\#) { + color: var(--colors-red-150); +} +.text_red\.200:not(#\#):not(#\#):not(#\#):not(#\#) { + color: var(--colors-red-200); +} +.text_red\.300:not(#\#):not(#\#):not(#\#):not(#\#) { + color: var(--colors-red-300); +} +.text_red\.400:not(#\#):not(#\#):not(#\#):not(#\#) { + color: var(--colors-red-400); +} +.text_red\.500:not(#\#):not(#\#):not(#\#):not(#\#) { + color: var(--colors-red-500); +} +.text_red\.600:not(#\#):not(#\#):not(#\#):not(#\#) { + color: var(--colors-red-600); +} +.text_red\.700:not(#\#):not(#\#):not(#\#):not(#\#) { + color: var(--colors-red-700); +} +.text_red\.800:not(#\#):not(#\#):not(#\#):not(#\#) { + color: var(--colors-red-800); +} +.text_red\.850:not(#\#):not(#\#):not(#\#):not(#\#) { + color: var(--colors-red-850); +} +.text_red\.900:not(#\#):not(#\#):not(#\#):not(#\#) { + color: var(--colors-red-900); +} +.text_red\.950:not(#\#):not(#\#):not(#\#):not(#\#) { + color: var(--colors-red-950); +} +.text_mono\.50:not(#\#):not(#\#):not(#\#):not(#\#) { + color: var(--colors-mono-50); +} +.text_mono\.100:not(#\#):not(#\#):not(#\#):not(#\#) { + color: var(--colors-mono-100); +} +.text_mono\.150:not(#\#):not(#\#):not(#\#):not(#\#) { + color: var(--colors-mono-150); +} +.text_mono\.200:not(#\#):not(#\#):not(#\#):not(#\#) { + color: var(--colors-mono-200); +} +.text_mono\.300:not(#\#):not(#\#):not(#\#):not(#\#) { + color: var(--colors-mono-300); +} +.text_mono\.400:not(#\#):not(#\#):not(#\#):not(#\#) { + color: var(--colors-mono-400); +} +.text_mono\.500:not(#\#):not(#\#):not(#\#):not(#\#) { + color: var(--colors-mono-500); +} +.text_mono\.600:not(#\#):not(#\#):not(#\#):not(#\#) { + color: var(--colors-mono-600); +} +.text_mono\.700:not(#\#):not(#\#):not(#\#):not(#\#) { + color: var(--colors-mono-700); +} +.text_mono\.800:not(#\#):not(#\#):not(#\#):not(#\#) { + color: var(--colors-mono-800); +} +.text_mono\.850:not(#\#):not(#\#):not(#\#):not(#\#) { + color: mono.850; +} +.text_mono\.900:not(#\#):not(#\#):not(#\#):not(#\#) { + color: var(--colors-mono-900); +} +.text_mono\.950:not(#\#):not(#\#):not(#\#):not(#\#) { + color: var(--colors-mono-950); +} +.text_white:not(#\#):not(#\#):not(#\#):not(#\#) { + color: var(--colors-white); +} +.text_black:not(#\#):not(#\#):not(#\#):not(#\#) { + color: var(--colors-black); +} +.text_whiteOpacity\.20:not(#\#):not(#\#):not(#\#):not(#\#) { + color: whiteOpacity.20; +} +.text_whiteOpacity\.40:not(#\#):not(#\#):not(#\#):not(#\#) { + color: whiteOpacity.40; +} +.text_whiteOpacity\.60:not(#\#):not(#\#):not(#\#):not(#\#) { + color: whiteOpacity.60; +} +.text_whiteOpacity\.80:not(#\#):not(#\#):not(#\#):not(#\#) { + color: whiteOpacity.80; +} +.text_blackOpacity\.20:not(#\#):not(#\#):not(#\#):not(#\#) { + color: blackOpacity.20; +} +.text_blackOpacity\.40:not(#\#):not(#\#):not(#\#):not(#\#) { + color: blackOpacity.40; +} +.text_blackOpacity\.60:not(#\#):not(#\#):not(#\#):not(#\#) { + color: blackOpacity.60; +} +.text_blackOpacity\.80:not(#\#):not(#\#):not(#\#):not(#\#) { + color: blackOpacity.80; +} +.text_primary:not(#\#):not(#\#):not(#\#):not(#\#) { + color: var(--colors-primary); +} +.text_success:not(#\#):not(#\#):not(#\#):not(#\#) { + color: var(--colors-success); +} +.text_error:not(#\#):not(#\#):not(#\#):not(#\#) { + color: var(--colors-error); +} +.text_backgroundNormal:not(#\#):not(#\#):not(#\#):not(#\#) { + color: var(--colors-background-normal); +} +.text_backgroundAlternative:not(#\#):not(#\#):not(#\#):not(#\#) { + color: var(--colors-background-alternative); +} +.text_backgroundDimmer:not(#\#):not(#\#):not(#\#):not(#\#) { + color: var(--colors-background-dimmer); +} +.text_errorBackground:not(#\#):not(#\#):not(#\#):not(#\#) { + color: var(--colors-error-background); +} +.text_sub:not(#\#):not(#\#):not(#\#):not(#\#) { + color: var(--colors-sub); +} +.text_outline:not(#\#):not(#\#):not(#\#):not(#\#) { + color: var(--colors-outline); } -.textStyle_body2:not(#\#):not(#\#):not(#\#):not(#\#) { - letter-spacing: -0.00875rem; - font-size: 0.875rem; - line-height: 160%; - font-weight: 500; +.text_textBlack:not(#\#):not(#\#):not(#\#):not(#\#) { + color: var(--colors-text-black); } -.bg_blue\.100:not(#\#):not(#\#):not(#\#):not(#\#):not(#\#) { - background: var(--colors-blue-100); +.text_textWhite:not(#\#):not(#\#):not(#\#):not(#\#) { + color: var(--colors-text-white); } -.px_4:not(#\#):not(#\#):not(#\#):not(#\#):not(#\#) { - padding-inline: 4px; +.text_darkDisabled:not(#\#):not(#\#):not(#\#):not(#\#) { + color: var(--colors-dark-disabled); } -.py_3:not(#\#):not(#\#):not(#\#):not(#\#):not(#\#) { - padding-block: 3px; +.text_lightDisabled:not(#\#):not(#\#):not(#\#):not(#\#) { + color: var(--colors-light-disabled); } -.rounded_md:not(#\#):not(#\#):not(#\#):not(#\#):not(#\#) { - border-radius: md; +.text_blueDisabled:not(#\#):not(#\#):not(#\#):not(#\#) { + color: var(--colors-blue-disabled); } -.bg_red\.400:not(#\#):not(#\#):not(#\#):not(#\#):not(#\#) { - background: var(--colors-red-400); +.text_textBlueDisabled:not(#\#):not(#\#):not(#\#):not(#\#) { + color: var(--colors-text-blue-disabled); } -.w_20:not(#\#):not(#\#):not(#\#):not(#\#):not(#\#) { - width: 20px; +.text_blueHover:not(#\#):not(#\#):not(#\#):not(#\#) { + color: var(--colors-blue-hover); } -.h_20:not(#\#):not(#\#):not(#\#):not(#\#):not(#\#) { - height: 20px; +.text_monoHover:not(#\#):not(#\#):not(#\#):not(#\#) { + color: var(--colors-mono-hover); } -.rounded_9999:not(#\#):not(#\#):not(#\#):not(#\#):not(#\#) { - border-radius: 9999px; +.text_elevatedHover:not(#\#):not(#\#):not(#\#):not(#\#) { + color: var(--colors-elevated-hover); } -.d_flex:not(#\#):not(#\#):not(#\#):not(#\#):not(#\#) { - display: flex; +.text_bluePressed:not(#\#):not(#\#):not(#\#):not(#\#) { + color: var(--colors-blue-pressed); } -.bg_blueBackgroundPressed:not(#\#):not(#\#):not(#\#):not(#\#):not(#\#) { - background: var(--colors-blue-background-pressed); +.text_blueBackgroundPressed:not(#\#):not(#\#):not(#\#):not(#\#) { + color: var(--colors-blue-background-pressed); } -.w_10:not(#\#):not(#\#):not(#\#):not(#\#):not(#\#) { - width: 10px; +.text_monoBackgroundPressed:not(#\#):not(#\#):not(#\#):not(#\#) { + color: var(--colors-mono-background-pressed); } -.h_10:not(#\#):not(#\#):not(#\#):not(#\#):not(#\#) { - height: 10px; +.text_shadowSmall:not(#\#):not(#\#):not(#\#):not(#\#) { + color: var(--colors-shadow-small); } -.bg_primary:not(#\#):not(#\#):not(#\#):not(#\#):not(#\#) { - background: var(--colors-primary); +.text_shadowMedium:not(#\#):not(#\#):not(#\#):not(#\#) { + color: var(--colors-shadow-medium); } -.gap_8:not(#\#):not(#\#):not(#\#):not(#\#):not(#\#) { - gap: 8px; +.text_blueShadow:not(#\#):not(#\#):not(#\#):not(#\#) { + color: var(--colors-blue-shadow); } -.d_none:not(#\#):not(#\#):not(#\#):not(#\#):not(#\#) { - display: none; +.text_discord:not(#\#):not(#\#):not(#\#):not(#\#) { + color: var(--colors-discord); } -.gap_0\.5rem:not(#\#):not(#\#):not(#\#):not(#\#):not(#\#) { - gap: 0.5rem; +.text_github:not(#\#):not(#\#):not(#\#):not(#\#) { + color: var(--colors-github); } -.font_Inter:not(#\#):not(#\#):not(#\#):not(#\#):not(#\#) { - font-family: Inter; +.text_secondaryYellow:not(#\#):not(#\#):not(#\#):not(#\#) { + color: var(--colors-secondary-yellow); } -.border-w_1:not(#\#):not(#\#):not(#\#):not(#\#):not(#\#) { - border-width: 1px; +.text_secondaryGreen:not(#\#):not(#\#):not(#\#):not(#\#) { + color: var(--colors-secondary-green); } -.items_center:not(#\#):not(#\#):not(#\#):not(#\#):not(#\#) { - align-items: center; +.text_secondaryRed:not(#\#):not(#\#):not(#\#):not(#\#) { + color: var(--colors-secondary-red); } -.justify_center:not(#\#):not(#\#):not(#\#):not(#\#):not(#\#) { - justify-content: center; +.text_blueGradientDark:not(#\#):not(#\#):not(#\#):not(#\#) { + color: blueGradientDark; } -.border_primary:not(#\#):not(#\#):not(#\#):not(#\#):not(#\#) { - border-color: var(--colors-primary); +.text_blueGradientLight:not(#\#):not(#\#):not(#\#):not(#\#) { + color: blueGradientLight; } -.border_darkDisabled:not(#\#):not(#\#):not(#\#):not(#\#):not(#\#) { - border-color: var(--colors-dark-disabled); +.text_redGradientDark:not(#\#):not(#\#):not(#\#):not(#\#) { + color: redGradientDark; } -.border_bluePressed:not(#\#):not(#\#):not(#\#):not(#\#):not(#\#) { - border-color: var(--colors-blue-pressed); +.text_redGradientLight:not(#\#):not(#\#):not(#\#):not(#\#) { + color: redGradientLight; } -.flex_column:not(#\#):not(#\#):not(#\#):not(#\#):not(#\#) { - flex-direction: column; +.text_greenGradientDark:not(#\#):not(#\#):not(#\#):not(#\#) { + color: greenGradientDark; } -.hover\:bg_blue\.400:is(:hover, [data-hover]):not(#\#):not(#\#):not(#\#):not( - #\# - ):not(#\#) { - background: var(--colors-blue-400); +.text_greenGradientLight:not(#\#):not(#\#):not(#\#):not(#\#) { + color: greenGradientLight; } -.hover\:bg_red\.500:is(:hover, [data-hover]):not(#\#):not(#\#):not(#\#):not( - #\# - ):not(#\#) { - background: var(--colors-red-500); +.text_yellowGradientDark:not(#\#):not(#\#):not(#\#):not(#\#) { + color: yellowGradientDark; } +.text_yellowGradientLight:not(#\#):not(#\#):not(#\#):not(#\#) { + color: yellowGradientLight; +} \ No newline at end of file diff --git a/packages/wow-ui/styled-system/tokens/index.js b/packages/wow-ui/styled-system/tokens/index.js index 930662c7..a6ec6779 100644 --- a/packages/wow-ui/styled-system/tokens/index.js +++ b/packages/wow-ui/styled-system/tokens/index.js @@ -343,6 +343,10 @@ const tokens = { value: 10, variable: "var(--z-index-dropdown)", }, + "zIndex.overlay": { + value: 9999, + variable: "var(--z-index-overlay)", + }, "shadows.blue": { value: "0px 4px 8px 0px rgba(16, 43, 74, 0.2)", variable: "var(--shadows-blue)", diff --git a/packages/wow-ui/styled-system/tokens/tokens.d.ts b/packages/wow-ui/styled-system/tokens/tokens.d.ts index 24b9c593..45319136 100644 --- a/packages/wow-ui/styled-system/tokens/tokens.d.ts +++ b/packages/wow-ui/styled-system/tokens/tokens.d.ts @@ -86,6 +86,7 @@ export type Token = | "borderWidths.button" | "borderWidths.arrow" | "zIndex.dropdown" + | "zIndex.overlay" | "shadows.blue" | "shadows.mono" | "breakpoints.xs" @@ -323,7 +324,7 @@ export type RadiusToken = "sm" | "md" | "full"; export type BorderWidthToken = "button" | "arrow"; -export type ZIndexToken = "dropdown"; +export type ZIndexToken = "dropdown" | "overlay"; export type ShadowToken = "blue" | "mono"; diff --git a/packages/wow-ui/styled-system/types/prop-type.d.ts b/packages/wow-ui/styled-system/types/prop-type.d.ts index 195159ce..bfcf5a77 100644 --- a/packages/wow-ui/styled-system/types/prop-type.d.ts +++ b/packages/wow-ui/styled-system/types/prop-type.d.ts @@ -679,7 +679,9 @@ export interface UtilityValues { | "body3" | "label1" | "label2" - | "label3"; + | "label3" + | "header1" + | "header2"; } type WithColorOpacityModifier = T extends string ? `${T}/${string}` : T; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index fd92978b..f80dc987 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -268,6 +268,9 @@ importers: react-day-picker: specifier: ^9.0.8 version: 9.0.8(react@18.2.0) + uuid: + specifier: ^10.0.0 + version: 10.0.0 wowds-icons: specifier: workspace:^ version: link:../wow-icons @@ -323,6 +326,9 @@ importers: '@types/react-dom': specifier: ^18.2.19 version: 18.2.19 + '@types/uuid': + specifier: ^10.0.0 + version: 10.0.0 axe-playwright: specifier: ^2.0.1 version: 2.0.1(playwright@1.45.0) @@ -5448,6 +5454,10 @@ packages: resolution: {integrity: sha512-dqId9J8K/vGi5Zr7oo212BGii5m3q5Hxlkwy3WpYuKPklmBEvsbMYYyLxAQpSffdLl/gdW0XUpKWFvYmyoWCoQ==} dev: true + /@types/uuid@10.0.0: + resolution: {integrity: sha512-7gqG38EyHgyP1S+7+xomFtL+ZNHcKv6DwNaCZmJmo1vgMugyF3TCnXVg4t1uk89mLNwnLtnY3TpOpCOyp1/xHQ==} + dev: true + /@types/uuid@9.0.8: resolution: {integrity: sha512-jg+97EGIcY9AGHJJRaaPVgetKDsrTgbRjQ5Msgjh/DQKEFl0DtyRr/VCOyD1T2R1MNeWPK/u7JoGhlDZnKBAfA==} dev: true @@ -15661,6 +15671,11 @@ packages: engines: {node: '>= 0.4.0'} dev: true + /uuid@10.0.0: + resolution: {integrity: sha512-8XkAphELsDnEGrDxUOHB3RGvXz6TeuYSGEZBOjtTtPm2lwhGBjLgOzLHB63IUWfBpNucQjND6d3AOudO+H3RWQ==} + hasBin: true + dev: false + /uuid@8.3.2: resolution: {integrity: sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==} hasBin: true