diff --git a/docs/components/example/index.json b/docs/components/example/index.json index 0c1c125a7..edf5a3e12 100644 --- a/docs/components/example/index.json +++ b/docs/components/example/index.json @@ -108,10 +108,10 @@ "inline-banner-with-icon": "import { InlineBanner } from \"seed-design/ui/inline-banner\";\nimport { IconILowercaseSerifCircleFill } from \"@daangn/react-monochrome-icon\";\n\nexport default function InlineBannerWithIcon() {\n return (\n }>\n 다른 사람과 예약된 물품이 있어요.\n \n );\n}", "inline-banner-with-link": "import { InlineBanner } from \"seed-design/ui/inline-banner\";\n\nexport default function InlineBannerWithLink() {\n return (\n {} }}>\n 다른 사람과 예약된 물품이 있어요.\n \n );\n}", "inline-banner-with-title-text": "import { InlineBanner } from \"seed-design/ui/inline-banner\";\n\nexport default function InlineBannerWithTitleText() {\n return (\n \n 다른 사람과 예약된 물품이 있어요.\n \n );\n}", - "segmented-control-fixed-width": "import { SegmentedControl, SegmentedControlOption } from \"seed-design/ui/segmented-control\";\n\nexport default function SegmentedControlFixedWidth() {\n return (\n \n New\n Hot\n \n );\n}", - "segmented-control-long-label-fixed-width": "import { SegmentedControl, SegmentedControlOption } from \"seed-design/ui/segmented-control\";\n\nexport default function SegmentedControlLongLabelFixedWidth() {\n return (\n \n 가격 높은 순\n 할인율 높은 순\n 인기 많은 순\n \n );\n}", - "segmented-control-long-label": "import { SegmentedControl, SegmentedControlOption } from \"seed-design/ui/segmented-control\";\n\nexport default function SegmentedControlLongLabel() {\n return (\n \n 가격 높은 순\n 할인율 높은 순\n 인기 많은 순\n \n );\n}", - "segmented-control-preview": "import { useState } from \"react\";\nimport { SegmentedControl, SegmentedControlOption } from \"seed-design/ui/segmented-control\";\n\nexport default function SegmentedControlPreview() {\n const options = [\"New\", \"Hot\"];\n const [value, setValue] = useState(\"New\");\n\n return (\n
\n \n {options.map((option) => (\n \n {option}\n \n ))}\n \n
Selected value: {value}
\n
\n );\n}", + "segmented-control-fixed-width": "import { SegmentedControl, SegmentedControlTrigger } from \"seed-design/ui/segmented-control\";\n\nexport default function SegmentedControlFixedWidth() {\n return (\n \n New\n Hot\n \n );\n}", + "segmented-control-long-label-fixed-width": "import { SegmentedControl, SegmentedControlTrigger } from \"seed-design/ui/segmented-control\";\n\nexport default function SegmentedControlLongLabelFixedWidth() {\n return (\n \n 가격 높은 순\n 할인율 높은 순\n 인기 많은 순\n \n );\n}", + "segmented-control-long-label": "import { SegmentedControl, SegmentedControlTrigger } from \"seed-design/ui/segmented-control\";\n\nexport default function SegmentedControlLongLabel() {\n return (\n \n 가격 높은 순\n 할인율 높은 순\n 인기 많은 순\n \n );\n}", + "segmented-control-preview": "import { useState } from \"react\";\nimport { SegmentedControl, SegmentedControlTrigger } from \"seed-design/ui/segmented-control\";\n\nexport default function SegmentedControlPreview() {\n const options = [\"New\", \"Hot\"];\n const [value, setValue] = useState(\"New\");\n\n return (\n
\n \n {options.map((option) => (\n \n {option}\n \n ))}\n \n
Selected value: {value}
\n
\n );\n}", "skeleton-wave-activity": "import type { ActivityComponentType } from \"@stackflow/react/future\";\nimport type * as React from \"react\";\n\nimport Layout from \"@/components/stackflow/ActivityLayout\";\nimport { Skeleton } from \"seed-design/ui/skeleton\";\nimport {\n useSkeletonDuration,\n useIsRealLoading,\n useSkeletonLoading,\n useSkeletonTimingFunction,\n useSkeletonInitTransitionDuration,\n useSkeletonGradient,\n} from \"@/stores/skeleton\";\n\ndeclare module \"@stackflow/config\" {\n interface Register {\n SkeletonWave: unknown;\n }\n}\n\nconst Fallback = () => {\n return (\n
\n \n
\n \n \n \n
\n
\n );\n};\n\nconst SkeletonWaveActivity: ActivityComponentType<\"SkeletonWave\"> = () => {\n const isLoading = useSkeletonLoading();\n const isRealLoading = useIsRealLoading();\n const animationDuration = useSkeletonDuration();\n const animationTiming = useSkeletonTimingFunction();\n const initTransitionDuration = useSkeletonInitTransitionDuration();\n const gradient = useSkeletonGradient();\n\n return (\n \n \n {isLoading ? isRealLoading && :
content
}\n \n
\n );\n};\n\nexport default SkeletonWaveActivity;\n\nSkeletonWaveActivity.displayName = \"SkeletonWaveActivity\";", "switch-disabled": "import { Switch } from \"seed-design/ui/switch\";\n\nexport default function SwitchDisabled() {\n return (\n
\n \n \n
\n );\n}", "switch-medium": "import { useState } from \"react\";\nimport { Switch } from \"seed-design/ui/switch\";\n\nexport default function SwitchMedium() {\n const [isChecked, setIsChecked] = useState(false);\n\n return (\n \n );\n}", @@ -126,7 +126,7 @@ "tabs-size-medium": "import { Tabs, TabContent, TabContentList, TabTrigger, TabTriggerList } from \"seed-design/ui/tabs\";\n\nexport default function TabsSizeMedium() {\n return (\n
\n \n \n 라벨1\n 라벨2\n 라벨3\n \n \n \n Content 1\n \n \n Content 2\n \n \n Content 3\n \n \n \n
\n );\n}\n\nconst Content = (props: React.PropsWithChildren) => {\n return (\n \n {props.children}\n \n );\n};", "tabs-size-small": "import { Tabs, TabContent, TabContentList, TabTrigger, TabTriggerList } from \"seed-design/ui/tabs\";\n\nexport default function TabsSizeSmall() {\n return (\n
\n \n \n 라벨1\n 라벨2\n 라벨3\n \n \n \n Content 1\n \n \n Content 2\n \n \n Content 3\n \n \n \n
\n );\n}\n\nconst Content = (props: React.PropsWithChildren) => {\n return (\n \n {props.children}\n \n );\n};", "tabs-standalone": "import { useState } from \"react\";\nimport { Tabs, TabTrigger, TabTriggerList } from \"seed-design/ui/tabs\";\n\nexport default function TabsStandalone() {\n const [activeTab, setActiveTab] = useState(\"1\");\n\n return (\n
\n \n \n 라벨1\n 라벨2\n 라벨3\n \n {activeTab === \"1\" && (\n
\n Content 1\n
\n )}\n {activeTab === \"2\" && (\n
\n Content 2\n
\n )}\n {activeTab === \"3\" && (\n
\n Content 3\n
\n )}\n
\n
\n );\n}\n\nconst Content = (props: React.PropsWithChildren) => {\n return (\n \n {props.children}\n \n );\n};", - "tabs-swipeable": "import { Tabs, TabContent, TabContentList, TabTrigger, TabTriggerList } from \"seed-design/ui/tabs\";\n\nexport default function TabsSwipeable() {\n return (\n
\n \n \n 라벨1\n 라벨2\n 라벨3\n \n \n \n Content 1\n \n \n Content 2\n \n \n Content 3\n \n \n \n
\n );\n}\n\nconst Content = (props: React.PropsWithChildren) => {\n return (\n \n {props.children}\n \n );\n};", + "tabs-swipeable": "import { Tabs, TabContent, TabContentList, TabTrigger, TabTriggerList } from \"seed-design/ui/tabs\";\n\nexport default function TabsSwipeable() {\n return (\n
\n \n \n 라벨1\n 라벨2\n 라벨3\n \n \n \n Content 1\n \n \n Content 2\n \n \n Content 3\n \n \n \n
\n );\n}\n\nconst Content = (props: React.PropsWithChildren) => {\n return (\n \n {props.children}\n \n );\n};", "text-button-brand": "import { IconPlusCircleLine } from \"@daangn/react-monochrome-icon\";\nimport { TextButton } from \"seed-design/ui/text-button\";\n\nexport default function TextButtonBrand() {\n return (\n }>\n 추가\n \n );\n}", "text-button-danger": "import { IconTrashcanLine } from \"@daangn/react-monochrome-icon\";\nimport { TextButton } from \"seed-design/ui/text-button\";\n\nexport default function TextButtonDanger() {\n return (\n }>\n 삭제\n \n );\n}", "text-button-disabled": "import { IconPenHorizlineLine } from \"@daangn/react-monochrome-icon\";\nimport { TextButton } from \"seed-design/ui/text-button\";\n\nexport default function TextButtonPreview() {\n return (\n }>\n 새 글\n \n );\n}", diff --git a/docs/components/example/segmented-control-fixed-width.tsx b/docs/components/example/segmented-control-fixed-width.tsx index b3dae961f..79f85bf0a 100644 --- a/docs/components/example/segmented-control-fixed-width.tsx +++ b/docs/components/example/segmented-control-fixed-width.tsx @@ -1,10 +1,10 @@ -import { SegmentedControl, SegmentedControlOption } from "seed-design/ui/segmented-control"; +import { SegmentedControl, SegmentedControlTrigger } from "seed-design/ui/segmented-control"; export default function SegmentedControlFixedWidth() { return ( - - New - Hot + + New + Hot ); } diff --git a/docs/components/example/segmented-control-long-label-fixed-width.tsx b/docs/components/example/segmented-control-long-label-fixed-width.tsx index 33295c7ca..f836992b9 100644 --- a/docs/components/example/segmented-control-long-label-fixed-width.tsx +++ b/docs/components/example/segmented-control-long-label-fixed-width.tsx @@ -1,11 +1,11 @@ -import { SegmentedControl, SegmentedControlOption } from "seed-design/ui/segmented-control"; +import { SegmentedControl, SegmentedControlTrigger } from "seed-design/ui/segmented-control"; export default function SegmentedControlLongLabelFixedWidth() { return ( - - 가격 높은 순 - 할인율 높은 순 - 인기 많은 순 + + 가격 높은 순 + 할인율 높은 순 + 인기 많은 순 ); } diff --git a/docs/components/example/segmented-control-long-label.tsx b/docs/components/example/segmented-control-long-label.tsx index 3803253f7..b476a1d4e 100644 --- a/docs/components/example/segmented-control-long-label.tsx +++ b/docs/components/example/segmented-control-long-label.tsx @@ -1,11 +1,11 @@ -import { SegmentedControl, SegmentedControlOption } from "seed-design/ui/segmented-control"; +import { SegmentedControl, SegmentedControlTrigger } from "seed-design/ui/segmented-control"; export default function SegmentedControlLongLabel() { return ( - - 가격 높은 순 - 할인율 높은 순 - 인기 많은 순 + + 가격 높은 순 + 할인율 높은 순 + 인기 많은 순 ); } diff --git a/docs/components/example/segmented-control-preview.tsx b/docs/components/example/segmented-control-preview.tsx index d79a07b9c..04c7cc226 100644 --- a/docs/components/example/segmented-control-preview.tsx +++ b/docs/components/example/segmented-control-preview.tsx @@ -1,7 +1,7 @@ "use client"; import { useState } from "react"; -import { SegmentedControl, SegmentedControlOption } from "seed-design/ui/segmented-control"; +import { SegmentedControl, SegmentedControlTrigger } from "seed-design/ui/segmented-control"; export default function SegmentedControlPreview() { const options = ["New", "Hot"]; @@ -11,9 +11,9 @@ export default function SegmentedControlPreview() {
{options.map((option) => ( - + {option} - + ))}
Selected value: {value}
diff --git a/docs/content/docs/react/components/segmented-control.mdx b/docs/content/docs/react/components/segmented-control.mdx index 0940d0607..8863ff012 100644 --- a/docs/content/docs/react/components/segmented-control.mdx +++ b/docs/content/docs/react/components/segmented-control.mdx @@ -18,18 +18,18 @@ description: 설명 # TODO name="SegmentedControlProps" /> -### `SegmentedControlOption` +### `SegmentedControlTrigger` ## 예제 ### 최소 너비보다 넓은 옵션 레이블 -Pill 형태의 `SegmentControlOption` 한 개는 86px의 최소 너비를 가져요. 제공한 옵션 중 이 너비를 초과하는 옵션이 있다면, 가장 긴 옵션의 너비에 모든 옵션의 너비가 맞춰져요. +Pill 형태의 `SegmentControlTrigger` 한 개는 86px의 최소 너비를 가져요. 제공한 옵션 중 이 너비를 초과하는 옵션이 있다면, 가장 긴 옵션의 너비에 모든 옵션의 너비가 맞춰져요. diff --git a/docs/public/__registry__/ui/segmented-control.json b/docs/public/__registry__/ui/segmented-control.json index aa28fc75a..c5c1c36d4 100644 --- a/docs/public/__registry__/ui/segmented-control.json +++ b/docs/public/__registry__/ui/segmented-control.json @@ -1,13 +1,11 @@ { "name": "segmented-control", - "dependencies": [ - "@seed-design/react-tabs@alpha" - ], + "dependencies": ["@seed-design/react-tabs@alpha"], "registries": [ { "name": "segmented-control.tsx", "type": "ui", - "content": "\"use client\";\n\nimport \"@seed-design/stylesheet/segmentedControl.css\";\nimport {\n useTabs,\n type TriggerProps,\n type UseTabsProps,\n} from \"@seed-design/react-tabs\";\nimport * as React from \"react\";\nimport clsx from \"clsx\";\nimport {\n segmentedControl,\n type SegmentedControlVariantProps,\n} from \"@seed-design/recipe/segmentedControl\";\nimport type { Assign } from \"../util/types\";\nexport interface SegmentedControlProps extends SegmentedControlVariantProps {}\n\nconst TabsContext = React.createContext<{\n api: ReturnType;\n} | null>(null);\n\nconst useTabsContext = () => {\n const context = React.useContext(TabsContext);\n if (!context)\n throw new Error(\n \"SegmentedControlOption cannot be rendered outside the SegmentedControl\",\n );\n\n return context;\n};\n\nexport interface SegmentedControlProps\n extends SegmentedControlVariantProps,\n Pick {}\n\ntype ReactSegmentedControlProps = SegmentedControlProps &\n Assign, UseTabsProps>;\n\nexport const SegmentedControl = React.forwardRef<\n // HTMLFieldSetElement,\n HTMLDivElement,\n ReactSegmentedControlProps\n>(({ className, children, style, ...otherProps }, ref) => {\n const api = useTabs(otherProps);\n const { tabTriggerListProps, triggerSize, tabIndicatorProps } = api;\n\n const { left, width } = triggerSize;\n\n // TODO: value/defaultvalue 없는 경우 첫 번째 아이템으로 default (tabs 참고)\n\n const classNames = segmentedControl();\n\n return (\n \n {children}\n \n
\n );\n});\nSegmentedControl.displayName = \"SegmentedControl\";\n\nexport interface SegmentedControlOptionProps\n extends SegmentedControlVariantProps,\n Omit {}\n\ntype ReactSegmentedControlOptionProps = Assign<\n React.HTMLAttributes,\n SegmentedControlOptionProps\n>;\n\nexport const SegmentedControlOption = React.forwardRef<\n HTMLButtonElement,\n ReactSegmentedControlOptionProps\n>(({ className, children, value, ...otherProps }, ref) => {\n const {\n api: { getTabTriggerProps },\n } = useTabsContext();\n\n const { rootProps, labelProps } = getTabTriggerProps({ value });\n\n const classNames = segmentedControl();\n\n return (\n \n
\n {children}\n
\n
\n {children}\n
\n \n );\n});\n\nSegmentedControlOption.displayName = \"SegmentedControlOption\";\n" + "content": "\"use client\";\n\nimport \"@seed-design/stylesheet/segmentedControl.css\";\nimport {\n useTabs,\n type TriggerProps,\n type UseTabsProps,\n} from \"@seed-design/react-tabs\";\nimport * as React from \"react\";\nimport clsx from \"clsx\";\nimport {\n segmentedControl,\n type SegmentedControlVariantProps,\n} from \"@seed-design/recipe/segmentedControl\";\nimport type { Assign } from \"../util/types\";\nexport interface SegmentedControlProps extends SegmentedControlVariantProps {}\n\nconst TabsContext = React.createContext<{\n api: ReturnType;\n} | null>(null);\n\nconst useTabsContext = () => {\n const context = React.useContext(TabsContext);\n if (!context)\n throw new Error(\n \"SegmentedControlOption cannot be rendered outside the SegmentedControl\",\n );\n\n return context;\n};\n\nexport interface SegmentedControlProps\n extends SegmentedControlVariantProps,\n Pick {}\n\ntype ReactSegmentedControlProps = SegmentedControlProps &\n Assign, UseTabsProps>;\n\nexport const SegmentedControl = React.forwardRef<\n // HTMLFieldSetElement,\n HTMLDivElement,\n ReactSegmentedControlProps\n>(({ className, children, style, ...otherProps }, ref) => {\n const api = useTabs(otherProps);\n const { tabIndicatorProps, rootProps } = api;\n\n // TODO: value/defaultvalue 없는 경우 첫 번째 아이템으로 default (tabs 참고)\n\n const classNames = segmentedControl();\n\n return (\n \n {children}\n \n \n );\n});\nSegmentedControl.displayName = \"SegmentedControl\";\n\nexport interface SegmentedControlTriggerProps\n extends SegmentedControlVariantProps,\n Omit {}\n\ntype ReactSegmentedControlTriggerProps = Assign<\n React.HTMLAttributes,\n SegmentedControlTriggerProps\n>;\n\nexport const SegmentedControlTrigger = React.forwardRef<\n HTMLButtonElement,\n ReactSegmentedControlTriggerProps\n>(({ className, children, value, ...otherProps }, ref) => {\n const {\n api: { getTabTriggerProps },\n } = useTabsContext();\n\n const { rootProps, labelProps } = getTabTriggerProps({ value });\n const classNames = segmentedControl();\n\n return (\n \n
\n {children}\n
\n
\n {children}\n
\n \n );\n});\n\nSegmentedControlTrigger.displayName = \"SegmentedControlOption\";\n" } ] -} \ No newline at end of file +} diff --git a/docs/public/__registry__/ui/tabs.json b/docs/public/__registry__/ui/tabs.json index 314e569e3..2c53e3f64 100644 --- a/docs/public/__registry__/ui/tabs.json +++ b/docs/public/__registry__/ui/tabs.json @@ -7,7 +7,7 @@ { "name": "tabs.tsx", "type": "ui", - "content": "\"use client\";\n\nimport clsx from \"clsx\";\nimport * as React from \"react\";\nimport {\n useTabs,\n useSwipeable,\n type UseTabsProps,\n type TriggerProps,\n type ContentProps,\n useLazyContents,\n type UseLazyContentsProps,\n} from \"@seed-design/react-tabs\";\nimport { tabs } from \"@seed-design/recipe/tabs\";\nimport { tab } from \"@seed-design/recipe/tab\";\n\nimport \"@seed-design/stylesheet/tabs.css\";\nimport \"@seed-design/stylesheet/tab.css\";\n\ntype Assign = Omit & U;\n\ninterface TabsContextValue {\n api: ReturnType & ReturnType;\n classNames: ReturnType;\n shouldRender: (value: string) => boolean;\n\n /**\n * @default false\n */\n isSwipeable: boolean;\n\n layout: \"fill\" | \"hug\";\n size: \"small\" | \"medium\";\n}\n\nconst TabsContext = React.createContext(null);\n\nconst useTabsContext = () => {\n const context = React.useContext(TabsContext);\n if (!context) {\n throw new Error(\"Tabs cannot be rendered outside the Tabs\");\n }\n return context;\n};\n\nexport interface TabsProps\n extends Assign, UseTabsProps>,\n Omit {\n /**\n * @default \"hug\"\n */\n layout?: \"fill\" | \"hug\";\n\n /**\n * @default \"small\"\n */\n size?: \"small\" | \"medium\";\n}\n\nexport const Tabs = React.forwardRef(\n (props, ref) => {\n const {\n className,\n lazyMode,\n isLazy,\n isSwipeable = false,\n layout = \"hug\",\n size = \"small\",\n } = props;\n const useTabsProps = useTabs(props);\n const useSwipeableProps = useSwipeable({\n isSwipeable,\n onSwipeLeftToRight: useTabsProps.movePrev,\n onSwipeRightToLeft: useTabsProps.moveNext,\n });\n const classNames = tabs({\n layout,\n });\n const { rootProps, value } = useTabsProps;\n const { shouldRender } = useLazyContents({\n currentValue: value,\n lazyMode,\n isLazy,\n });\n const api = {\n ...useTabsProps,\n ...useSwipeableProps,\n };\n\n return (\n \n \n {props.children}\n \n \n );\n },\n);\nTabs.displayName = \"Tabs\";\n\nexport const TabTriggerList = React.forwardRef<\n HTMLDivElement,\n React.HTMLAttributes\n>(({ className, children, ...otherProps }, ref) => {\n const { api, classNames } = useTabsContext();\n const { tabTriggerListProps, triggerSize } = api;\n const { left } = triggerSize;\n const { triggerList } = classNames;\n\n const containerRef = React.useRef(null);\n React.useImperativeHandle(ref, () => containerRef.current as HTMLDivElement);\n\n React.useEffect(() => {\n if (containerRef.current) {\n containerRef.current?.scrollTo({\n // NOTE: 27px is half of tab's min-width\n left: left - 27,\n behavior: \"smooth\",\n });\n }\n }, [left]);\n\n return (\n \n {children}\n \n \n );\n});\nTabTriggerList.displayName = \"TabTriggerList\";\n\nexport interface TabTriggerProps\n extends Assign, TriggerProps> {\n /**\n * @default false\n */\n alert?: boolean;\n}\n\nexport const TabTrigger = React.forwardRef(\n (\n { className, children, value, isDisabled, alert = false, ...otherProps },\n ref,\n ) => {\n const { api, layout, size } = useTabsContext();\n const { getTabTriggerProps } = api;\n const { label, notification, root } = tab({\n size,\n layout,\n });\n const { rootProps, notificationProps, labelProps } = getTabTriggerProps({\n value,\n isDisabled,\n });\n\n return (\n \n \n {children}\n {alert &&
}\n \n \n );\n },\n);\nTabTrigger.displayName = \"TabTrigger\";\n\nexport const TabContentList = React.forwardRef<\n HTMLDivElement,\n React.HTMLAttributes\n>(({ className, children, ...otherProps }, ref) => {\n const { api, classNames, isSwipeable } = useTabsContext();\n const {\n tabContentListProps,\n tabContentCameraProps,\n getDragProps,\n currentTabEnabledIndex,\n swipeMoveX,\n swipeStatus,\n tabEnabledCount,\n } = api;\n const { contentList, contentCamera } = classNames;\n const dragProps = getDragProps();\n\n const getCameraTranslateX = () => {\n const MODIFIER = 5;\n\n const currentContentOffsetX = currentTabEnabledIndex * 100;\n\n if (swipeMoveX > 0 && currentTabEnabledIndex === 0) {\n return `calc(-${currentContentOffsetX}% + ${swipeMoveX / MODIFIER}px)`;\n }\n\n if (swipeMoveX < 0 && currentTabEnabledIndex === tabEnabledCount - 1) {\n return `calc(-${currentContentOffsetX}% + ${swipeMoveX / MODIFIER}px)`;\n }\n\n return `calc(-${currentContentOffsetX}% + ${swipeMoveX}px)`;\n };\n\n return (\n \n \n {children}\n
\n \n );\n});\nTabContentList.displayName = \"TabContentList\";\n\nexport const TabContent = React.forwardRef<\n HTMLDivElement,\n Assign, ContentProps>\n>(({ className, children, value, ...otherProps }, ref) => {\n const { api, classNames, shouldRender } = useTabsContext();\n const { getTabContentProps } = api;\n const { content } = classNames;\n const tabContentProps = getTabContentProps({ value });\n const isRender = shouldRender(value);\n\n return (\n \n {isRender && children}\n \n );\n});\nTabContent.displayName = \"TabContent\";\n\nconst TabIndicator = React.forwardRef<\n HTMLDivElement,\n React.HTMLAttributes\n>(({ className, ...otherProps }, ref) => {\n const { api, classNames, isSwipeable, layout } = useTabsContext();\n const {\n tabIndicatorProps,\n triggerSize,\n currentTabIndex,\n swipeMoveX,\n tabCount,\n swipeStatus,\n } = api;\n const { indicator } = classNames;\n const { left: triggerLeft, width: triggerWidth } = triggerSize;\n\n const getLeft = () => {\n const MODIFIER = layout === \"hug\" ? 10 : 5;\n const GUTTER = layout === \"fill\" ? 16 : 0;\n\n // 양끝 탭에서 스와이프로 인한 이동은 MODIFIER를 5배로 늘려서 완전 조금 이동하도록 함\n if (\n (swipeMoveX > 0 && currentTabIndex === 0) ||\n (swipeMoveX < 0 && currentTabIndex === tabCount - 1)\n ) {\n return `calc(${GUTTER}px + ${triggerLeft}px - ${swipeMoveX / (MODIFIER * 5)}px)`;\n }\n\n return `calc(${GUTTER}px + ${triggerLeft}px - ${swipeMoveX / MODIFIER}px)`;\n };\n\n const getWidth = () => {\n const GUTTER = 16;\n\n if (layout === \"hug\") {\n return triggerWidth;\n }\n\n return triggerWidth - GUTTER * 2;\n };\n\n const leftTransition =\n isSwipeable && swipeStatus === \"idle\"\n ? \"left 0.2s cubic-bezier(0.15, 0.3, 0.25, 1)\"\n : \"\";\n const widthTransition = \"width 0.2s cubic-bezier(0.15, 0.3, 0.25, 1)\";\n const transitions = [leftTransition, widthTransition]\n .filter(Boolean)\n .join(\", \");\n\n return (\n \n );\n});\nTabIndicator.displayName = \"TabIndicator\";\n" + "content": "\"use client\";\n\nimport clsx from \"clsx\";\nimport * as React from \"react\";\nimport {\n useTabs,\n useSwipeable,\n type UseTabsProps,\n type TriggerProps,\n type ContentProps,\n useLazyContents,\n type UseLazyContentsProps,\n} from \"@seed-design/react-tabs\";\nimport { tabs } from \"@seed-design/recipe/tabs\";\nimport { tab } from \"@seed-design/recipe/tab\";\n\nimport \"@seed-design/stylesheet/tabs.css\";\nimport \"@seed-design/stylesheet/tab.css\";\n\ntype Assign = Omit & U;\n\ninterface TabsContextValue {\n api: ReturnType & ReturnType;\n classNames: ReturnType;\n shouldRender: (value: string) => boolean;\n\n /**\n * @default false\n */\n isSwipeable: boolean;\n\n layout: \"fill\" | \"hug\";\n size: \"small\" | \"medium\";\n}\n\nconst TabsContext = React.createContext(null);\n\nconst useTabsContext = () => {\n const context = React.useContext(TabsContext);\n if (!context) {\n throw new Error(\"Tabs cannot be rendered outside the Tabs\");\n }\n return context;\n};\n\nexport interface TabsProps\n extends Assign, UseTabsProps>,\n Omit {\n /**\n * @default \"hug\"\n */\n layout?: \"fill\" | \"hug\";\n\n /**\n * @default \"small\"\n */\n size?: \"small\" | \"medium\";\n}\n\nexport const Tabs = React.forwardRef(\n (props, ref) => {\n const {\n className,\n lazyMode,\n isLazy,\n isSwipeable = false,\n layout = \"hug\",\n size = \"small\",\n } = props;\n const useTabsProps = useTabs(props);\n const useSwipeableProps = useSwipeable({\n isSwipeable,\n onSwipeLeftToRight: useTabsProps.movePrev,\n onSwipeRightToLeft: useTabsProps.moveNext,\n });\n const classNames = tabs({\n layout,\n });\n const { rootProps, value } = useTabsProps;\n const { shouldRender } = useLazyContents({\n currentValue: value,\n lazyMode,\n isLazy,\n });\n const api = {\n ...useTabsProps,\n ...useSwipeableProps,\n };\n\n return (\n \n \n {props.children}\n \n \n );\n },\n);\nTabs.displayName = \"Tabs\";\n\nexport const TabTriggerList = React.forwardRef<\n HTMLDivElement,\n React.HTMLAttributes\n>(({ className, children, ...otherProps }, ref) => {\n const { api, classNames } = useTabsContext();\n const { tabTriggerListProps, triggerSize } = api;\n const { left } = triggerSize;\n const { triggerList } = classNames;\n\n const containerRef = React.useRef(null);\n React.useImperativeHandle(ref, () => containerRef.current as HTMLDivElement);\n\n React.useEffect(() => {\n if (containerRef.current) {\n containerRef.current?.scrollTo({\n // NOTE: 27px is half of tab's min-width\n left: left - 27,\n behavior: \"smooth\",\n });\n }\n }, [left]);\n\n return (\n \n {children}\n \n \n );\n});\nTabTriggerList.displayName = \"TabTriggerList\";\n\nexport interface TabTriggerProps\n extends Assign, TriggerProps> {\n /**\n * @default false\n */\n alert?: boolean;\n}\n\nexport const TabTrigger = React.forwardRef(\n (\n { className, children, value, isDisabled, alert = false, ...otherProps },\n ref,\n ) => {\n const { api, layout, size } = useTabsContext();\n const { getTabTriggerProps } = api;\n const { label, notification, root } = tab({\n size,\n layout,\n });\n const { rootProps, notificationProps, labelProps } = getTabTriggerProps({\n value,\n isDisabled,\n });\n\n return (\n \n \n {children}\n {alert &&
}\n \n \n );\n },\n);\nTabTrigger.displayName = \"TabTrigger\";\n\nexport const TabContentList = React.forwardRef<\n HTMLDivElement,\n React.HTMLAttributes\n>(({ className, children, ...otherProps }, ref) => {\n const { api, classNames, isSwipeable } = useTabsContext();\n const {\n tabContentListProps,\n tabContentCameraProps,\n getDragProps,\n currentTabEnabledIndex,\n swipeMoveX,\n swipeStatus,\n tabEnabledCount,\n } = api;\n const { contentList, contentCamera } = classNames;\n const dragProps = getDragProps();\n\n const getCameraTranslateX = () => {\n const MODIFIER = 5;\n\n const isSide =\n currentTabEnabledIndex === 0 ||\n currentTabEnabledIndex === tabEnabledCount - 1;\n const swipeOffset = isSide ? swipeMoveX / MODIFIER : swipeMoveX;\n\n return `calc(var(--seed-design-tab-index) * var(--seed-design-tab-camera-width) * -1px + ${swipeOffset}px)`;\n };\n\n return (\n \n \n {children}\n
\n \n );\n});\nTabContentList.displayName = \"TabContentList\";\n\nexport const TabContent = React.forwardRef<\n HTMLDivElement,\n Assign, ContentProps>\n>(({ className, children, value, ...otherProps }, ref) => {\n const { api, classNames, shouldRender } = useTabsContext();\n const { getTabContentProps } = api;\n const { content } = classNames;\n const tabContentProps = getTabContentProps({ value });\n const isRender = shouldRender(value);\n\n return (\n \n {isRender && children}\n \n );\n});\nTabContent.displayName = \"TabContent\";\n\nconst TabIndicator = React.forwardRef<\n HTMLDivElement,\n React.HTMLAttributes\n>(({ className, ...otherProps }, ref) => {\n const { api, classNames } = useTabsContext();\n const { tabIndicatorProps } = api;\n const { indicator } = classNames;\n\n return (\n \n );\n});\nTabIndicator.displayName = \"TabIndicator\";\n" } ] } \ No newline at end of file diff --git a/docs/registry/ui/segmented-control.tsx b/docs/registry/ui/segmented-control.tsx index 560ab8e56..3b3f3e281 100644 --- a/docs/registry/ui/segmented-control.tsx +++ b/docs/registry/ui/segmented-control.tsx @@ -42,9 +42,7 @@ export const SegmentedControl = React.forwardRef< ReactSegmentedControlProps >(({ className, children, style, ...otherProps }, ref) => { const api = useTabs(otherProps); - const { tabTriggerListProps, triggerSize, tabIndicatorProps } = api; - - const { left, width } = triggerSize; + const { tabIndicatorProps, rootProps } = api; // TODO: value/defaultvalue 없는 경우 첫 번째 아이템으로 default (tabs 참고) @@ -56,10 +54,18 @@ export const SegmentedControl = React.forwardRef< ...style, // XXX: tabCount 썼을 때 hydration 문제 gridTemplateColumns: `repeat(${React.Children.count(children)}, 1fr)`, + ...{ + "--seed-design-segmented-control-indicator-left": + "var(--seed-design-tab-indicator-left)", + "--seed-design-segmented-control-indicator-width": + "var(--seed-design-tab-indicator-width)", + "--seed-design-segmented-control-index": + "var(--seed-design-tab-index)", + }, }} className={clsx(classNames.root, className)} ref={ref} - {...tabTriggerListProps} + {...rootProps} {...otherProps} > {children} @@ -67,32 +73,30 @@ export const SegmentedControl = React.forwardRef< aria-hidden className={classNames.selectedIndicator} {...tabIndicatorProps} - style={{ left, width }} /> ); }); SegmentedControl.displayName = "SegmentedControl"; -export interface SegmentedControlOptionProps +export interface SegmentedControlTriggerProps extends SegmentedControlVariantProps, Omit {} -type ReactSegmentedControlOptionProps = Assign< +type ReactSegmentedControlTriggerProps = Assign< React.HTMLAttributes, - SegmentedControlOptionProps + SegmentedControlTriggerProps >; -export const SegmentedControlOption = React.forwardRef< +export const SegmentedControlTrigger = React.forwardRef< HTMLButtonElement, - ReactSegmentedControlOptionProps + ReactSegmentedControlTriggerProps >(({ className, children, value, ...otherProps }, ref) => { const { api: { getTabTriggerProps }, } = useTabsContext(); const { rootProps, labelProps } = getTabTriggerProps({ value }); - const classNames = segmentedControl(); return ( @@ -102,7 +106,7 @@ export const SegmentedControlOption = React.forwardRef< {...rootProps} {...otherProps} > -
+
{children}
@@ -112,4 +116,4 @@ export const SegmentedControlOption = React.forwardRef< ); }); -SegmentedControlOption.displayName = "SegmentedControlOption"; +SegmentedControlTrigger.displayName = "SegmentedControlOption"; diff --git a/docs/stories/SegmentedControl.stories.tsx b/docs/stories/SegmentedControl.stories.tsx index 0dd32dbab..7d7938509 100644 --- a/docs/stories/SegmentedControl.stories.tsx +++ b/docs/stories/SegmentedControl.stories.tsx @@ -2,7 +2,7 @@ import type { Meta, StoryObj } from "@storybook/react"; import { SegmentedControl, - SegmentedControlOption, + SegmentedControlTrigger, type SegmentedControlProps, } from "seed-design/ui/segmented-control"; @@ -18,9 +18,9 @@ const Component = () => { return ( {values.map((value) => ( - + {value} - + ))} ); diff --git a/packages/react-headless/tabs/src/useTabs.ts b/packages/react-headless/tabs/src/useTabs.ts index 1eec6c4b9..813a7d62c 100644 --- a/packages/react-headless/tabs/src/useTabs.ts +++ b/packages/react-headless/tabs/src/useTabs.ts @@ -105,24 +105,12 @@ export function useTabs(props: UseTabsProps) { } = props; const updateIndicatorStyle = React.useCallback(() => { - const getLeft = () => { - const GUTTER = layout === "fill" ? 16 : 0; - return GUTTER + triggerSize.left; - }; - - const getWidth = () => { - const GUTTER = 16; - - if (layout === "hug") return triggerSize.width; - return triggerSize.width - GUTTER * 2; - }; - const rootEl = dom.getRootEl(id); if (rootEl) { - rootEl.style.setProperty("--seed-design-tab-indicator-left", `${getLeft()}px`); - rootEl.style.setProperty("--seed-design-tab-indicator-width", `${getWidth()}px`); + rootEl.style.setProperty("--seed-design-tab-indicator-left", `${triggerSize.left}px`); + rootEl.style.setProperty("--seed-design-tab-indicator-width", `${triggerSize.width}px`); } - }, [layout, triggerSize, id]); + }, [triggerSize, id]); const updateCameraStyle = React.useCallback(() => { const rootEl = dom.getRootEl(id); diff --git a/packages/recipe-generator/preset/src/segmented-control.recipe.ts b/packages/recipe-generator/preset/src/segmented-control.recipe.ts index 5e6e1d955..12b7882b2 100644 --- a/packages/recipe-generator/preset/src/segmented-control.recipe.ts +++ b/packages/recipe-generator/preset/src/segmented-control.recipe.ts @@ -110,6 +110,9 @@ const segmentedControl = defineRecipe({ willChange: "left, width", // XXX: 임의 transition: "left 0.2s, width 0.2s", + + left: "var(--seed-design-segmented-control-indicator-left, 0px)", + width: "var(--seed-design-segmented-control-indicator-width, 0px)", }, }, variants: {}, diff --git a/packages/recipe-generator/preset/src/tabs.recipe.ts b/packages/recipe-generator/preset/src/tabs.recipe.ts index ba987d0b8..b930aa1eb 100644 --- a/packages/recipe-generator/preset/src/tabs.recipe.ts +++ b/packages/recipe-generator/preset/src/tabs.recipe.ts @@ -58,12 +58,20 @@ const tabs = defineRecipe({ padding: `0px ${vars.layoutFill.enabled.root.paddingX}`, justifyContent: "space-around", }, + indicator: { + left: "calc(var(--seed-design-tab-indicator-left, 0px) + 16px)", + width: "calc(var(--seed-design-tab-indicator-width, 0px) - 32px)", + }, }, hug: { triggerList: { padding: `0px ${vars.layoutHug.enabled.root.paddingX}`, justifyContent: "flex-start", }, + indicator: { + left: "calc(var(--seed-design-tab-indicator-left, 0px))", + width: "calc(var(--seed-design-tab-indicator-width, 0px))", + }, }, }, }, diff --git a/packages/stylesheet/segmentedControl.css b/packages/stylesheet/segmentedControl.css index e36bee8db..373abe291 100644 --- a/packages/stylesheet/segmentedControl.css +++ b/packages/stylesheet/segmentedControl.css @@ -68,4 +68,6 @@ box-shadow: 0 1px 6px rgba(0, 0, 0, 5%); will-change: left, width; transition: left 0.2s, width 0.2s; + left: var(--seed-design-segmented-control-indicator-left, 0px); + width: var(--seed-design-segmented-control-indicator-width, 0px); } \ No newline at end of file diff --git a/packages/stylesheet/tabs.css b/packages/stylesheet/tabs.css index abd51b261..080f1a8be 100644 --- a/packages/stylesheet/tabs.css +++ b/packages/stylesheet/tabs.css @@ -43,7 +43,15 @@ padding: 0px 0px; justify-content: space-around; } +.tabs__indicator--layout_fill { + left: calc(var(--seed-design-tab-indicator-left, 0px) + 16px); + width: calc(var(--seed-design-tab-indicator-width, 0px) - 32px); +} .tabs__triggerList--layout_hug { padding: 0px var(--seed-v3-unit-x4); justify-content: flex-start; +} +.tabs__indicator--layout_hug { + left: calc(var(--seed-design-tab-indicator-left, 0px)); + width: calc(var(--seed-design-tab-indicator-width, 0px)); } \ No newline at end of file