diff --git a/.yarn/cache/@radix-ui-react-compose-refs-npm-1.1.1-2480de3ef9-1be82f9f7f.zip b/.yarn/cache/@radix-ui-react-compose-refs-npm-1.1.1-2480de3ef9-1be82f9f7f.zip new file mode 100644 index 000000000..2c1add06b Binary files /dev/null and b/.yarn/cache/@radix-ui-react-compose-refs-npm-1.1.1-2480de3ef9-1be82f9f7f.zip differ diff --git a/.yarn/cache/@radix-ui-react-slot-npm-1.1.1-23892fb17a-5b1ee5100d.zip b/.yarn/cache/@radix-ui-react-slot-npm-1.1.1-23892fb17a-5b1ee5100d.zip new file mode 100644 index 000000000..182b98df2 Binary files /dev/null and b/.yarn/cache/@radix-ui-react-slot-npm-1.1.1-23892fb17a-5b1ee5100d.zip differ diff --git a/.yarn/cache/@stackflow-plugin-basic-ui-npm-1.10.1-374048b116-82a73aae9b.zip b/.yarn/cache/@stackflow-plugin-basic-ui-npm-1.10.1-374048b116-82a73aae9b.zip deleted file mode 100644 index b3638210a..000000000 Binary files a/.yarn/cache/@stackflow-plugin-basic-ui-npm-1.10.1-374048b116-82a73aae9b.zip and /dev/null differ diff --git a/.yarn/cache/@stackflow-plugin-basic-ui-npm-1.11.1-f8d3581a48-cad8cf26ae.zip b/.yarn/cache/@stackflow-plugin-basic-ui-npm-1.11.1-f8d3581a48-cad8cf26ae.zip new file mode 100644 index 000000000..a08e5b500 Binary files /dev/null and b/.yarn/cache/@stackflow-plugin-basic-ui-npm-1.11.1-f8d3581a48-cad8cf26ae.zip differ diff --git a/.yarn/cache/@stackflow-react-ui-core-npm-1.2.1-69bb7564b0-24a4872672.zip b/.yarn/cache/@stackflow-react-ui-core-npm-1.2.1-69bb7564b0-24a4872672.zip new file mode 100644 index 000000000..1a4dcaa47 Binary files /dev/null and b/.yarn/cache/@stackflow-react-ui-core-npm-1.2.1-69bb7564b0-24a4872672.zip differ diff --git a/docs/components/example/app-screen-preview.tsx b/docs/components/example/app-screen-preview.tsx new file mode 100644 index 000000000..edb2187d3 --- /dev/null +++ b/docs/components/example/app-screen-preview.tsx @@ -0,0 +1,47 @@ +"use client"; + +import { Flex } from "@/registry/ui/layout"; +import { IconBellFill } from "@daangn/react-monochrome-icon"; +import type { ActivityComponentType } from "@stackflow/react/future"; +import { + AppBar, + AppScreen, + CloseButton, + IconButton, + Left, + Right, + Title, +} from "seed-design/ui/app-screen"; + +declare module "@stackflow/config" { + interface Register { + AppScreenPreview: unknown; + } +} + +const AppScreenPreviewActivity: ActivityComponentType<"AppScreenPreview"> = () => { + return ( + + + + + Preview + + + + + + + } + > + + Preview + + + ); +}; + +export default AppScreenPreviewActivity; diff --git a/docs/components/example/app-screen-transparent-bar.tsx b/docs/components/example/app-screen-transparent-bar.tsx new file mode 100644 index 000000000..c4f0d74c5 --- /dev/null +++ b/docs/components/example/app-screen-transparent-bar.tsx @@ -0,0 +1,47 @@ +"use client"; + +import { Flex } from "@/registry/ui/layout"; +import { IconBellFill } from "@daangn/react-monochrome-icon"; +import type { ActivityComponentType } from "@stackflow/react/future"; +import { + AppBar, + AppScreen, + CloseButton, + IconButton, + Left, + Right, + Title, +} from "seed-design/ui/app-screen"; + +declare module "@stackflow/config" { + interface Register { + AppScreenTransparentBar: unknown; + } +} + +const AppScreenTransparentBarActivity: ActivityComponentType<"AppScreenTransparentBar"> = () => { + return ( + + + + + Transparent Bar + + + + + + + } + > + + Penguin + + + ); +}; + +export default AppScreenTransparentBarActivity; diff --git a/docs/components/example/index.json b/docs/components/example/index.json index 54fc81b47..66d327fc6 100644 --- a/docs/components/example/index.json +++ b/docs/components/example/index.json @@ -39,6 +39,8 @@ "actionable-inline-banner-with-icon": "import { ActionableInlineBanner, InlineBannerDescription } from \"seed-design/ui/inline-banner\";\nimport { IconILowercaseSerifCircleFill } from \"@daangn/react-monochrome-icon\";\n\nexport default function ActionableInlineBannerWithIcon() {\n return (\n window.alert(\"Hello World\")}\n variant=\"informativeWeak\"\n icon={}\n >\n 다른 사람과 예약된 물품이 있어요.\n \n );\n}", "alert-dialog-default-activity": "import * as React from \"react\";\n\nimport { AppScreen } from \"@stackflow/plugin-basic-ui\";\nimport { type ActivityComponentType, useStepFlow, useStack } from \"@stackflow/react/future\";\n\nimport { ActionButton } from \"seed-design/ui/action-button\";\nimport { AlertDialog as UIAlertDialog } from \"seed-design/ui/alert-dialog\";\n\ndeclare module \"@stackflow/config\" {\n interface Register {\n AlertDialogDefault: {\n alert: boolean;\n };\n }\n}\n\nconst AlertDialogDefaultActivity: ActivityComponentType<\"AlertDialogDefault\"> = ({ params }) => {\n const { alert } = params;\n const stack = useStack();\n const { pushStep, popStep } = useStepFlow(\"AlertDialogDefault\");\n\n const appBarLeft = () =>
Left
;\n const appBarRight = () =>
Right
;\n\n const onInteractOutside = () => {\n popStep();\n };\n\n const onButtonClick = () => {\n pushStep({\n alert: true,\n });\n };\n\n const mainActivitySteps = stack.activities[0].steps;\n\n return (\n \n \n Open\n {alert && (\n \n )}\n \n\n \n \n Steps\n \n {mainActivitySteps.map((step) => (\n \n ))}\n \n\n \n popStep}>Back\n \n \n );\n};\n\nexport default AlertDialogDefaultActivity;\n\nAlertDialogDefaultActivity.displayName = \"AlertDialogDefaultActivity\";", "alert-dialog-preview": "import * as React from \"react\";\n\nimport { AlertDialog } from \"seed-design/ui/alert-dialog\";\n\nexport default function AlertDialogPreview() {\n return ;\n}", + "app-screen-preview": "import { Flex } from \"@/registry/ui/layout\";\nimport { IconBellFill } from \"@daangn/react-monochrome-icon\";\nimport type { ActivityComponentType } from \"@stackflow/react/future\";\nimport {\n AppBar,\n AppScreen,\n CloseButton,\n IconButton,\n Left,\n Right,\n Title,\n} from \"seed-design/ui/app-screen\";\n\ndeclare module \"@stackflow/config\" {\n interface Register {\n AppScreenPreview: unknown;\n }\n}\n\nconst AppScreenPreviewActivity: ActivityComponentType<\"AppScreenPreview\"> = () => {\n return (\n \n \n \n \n Preview\n \n \n \n \n \n \n }\n >\n \n Preview\n \n \n );\n};\n\nexport default AppScreenPreviewActivity;", + "app-screen-transparent-bar": "import { Flex } from \"@/registry/ui/layout\";\nimport { IconBellFill } from \"@daangn/react-monochrome-icon\";\nimport type { ActivityComponentType } from \"@stackflow/react/future\";\nimport {\n AppBar,\n AppScreen,\n CloseButton,\n IconButton,\n Left,\n Right,\n Title,\n} from \"seed-design/ui/app-screen\";\n\ndeclare module \"@stackflow/config\" {\n interface Register {\n AppScreenTransparentBar: unknown;\n }\n}\n\nconst AppScreenTransparentBarActivity: ActivityComponentType<\"AppScreenTransparentBar\"> = () => {\n return (\n \n \n \n \n Transparent Bar\n \n \n \n \n \n \n }\n >\n \n \"Penguin\"\n \n \n );\n};\n\nexport default AppScreenTransparentBarActivity;", "avatar-badge": "import { IdentityPlaceholder } from \"seed-design/ui/identity-placeholder\";\nimport { Avatar, AvatarBadge, AvatarFallback, AvatarImage } from \"seed-design/ui/avatar\";\n\nexport default function AvatarWithBadge() {\n return (\n \n \n \n \n \n \n
\n \n \n );\n}", "avatar-preview": "import { Flex } from \"seed-design/ui/layout\";\nimport { IdentityPlaceholder } from \"seed-design/ui/identity-placeholder\";\nimport { Avatar, AvatarBadge, AvatarFallback, AvatarImage } from \"seed-design/ui/avatar\";\n\nexport default function AvatarPreview() {\n return (\n \n \n \n \n \n \n \n
\n \n \n \n \n \n \n \n \n \n );\n}", "avatar-size": "import { Flex } from \"seed-design/ui/layout\";\nimport { Avatar, AvatarFallback, AvatarImage } from \"seed-design/ui/avatar\";\n\nexport default function AvatarSize() {\n return (\n \n \n \n L\n \n \n \n L\n \n \n \n L\n \n \n \n L\n \n \n \n L\n \n \n \n L\n \n \n \n L\n \n \n );\n}", diff --git a/docs/components/stackflow/ActivityLayout.tsx b/docs/components/stackflow/ActivityLayout.tsx index b82bf1bad..d4400ac92 100644 --- a/docs/components/stackflow/ActivityLayout.tsx +++ b/docs/components/stackflow/ActivityLayout.tsx @@ -1,7 +1,6 @@ import { AppScreen } from "@stackflow/plugin-basic-ui"; import { - IconChevronDownFill, IconChevronDownLine, IconDot3HorizontalChatbubbleLeftLine, IconGearLine, diff --git a/docs/components/stackflow/Stackflow.tsx b/docs/components/stackflow/Stackflow.tsx index ff557b377..cb8f0811b 100644 --- a/docs/components/stackflow/Stackflow.tsx +++ b/docs/components/stackflow/Stackflow.tsx @@ -22,7 +22,7 @@ export const Stackflow: React.FC = ({ Activity }) => { return (
+ +## 설치 + + + +## Props + +### AppScreen + + + +### AppBar + + + +## 예제 + +### Transparent app bar + + \ No newline at end of file diff --git a/docs/public/__registry__/ui/app-screen.json b/docs/public/__registry__/ui/app-screen.json new file mode 100644 index 000000000..d281d0285 --- /dev/null +++ b/docs/public/__registry__/ui/app-screen.json @@ -0,0 +1,13 @@ +{ + "name": "app-screen", + "dependencies": [ + "@seed-design/stackflow" + ], + "registries": [ + { + "name": "app-screen.tsx", + "type": "ui", + "content": "\"use client\";\n\nimport \"@seed-design/stylesheet/screen.css\";\nimport \"@seed-design/stylesheet/topNavigation.css\";\n\nimport {\n IconChevronLeftLine,\n IconXmarkLine,\n} from \"@daangn/react-monochrome-icon\";\nimport {\n AppBar as SeedAppBar,\n AppScreen as SeedAppScreen,\n} from \"@seed-design/stackflow\";\nimport { useActions, useActivity } from \"@stackflow/react\";\nimport { forwardRef, useCallback } from \"react\";\n\nexport type AppBarProps = SeedAppBar.RootProps;\n\nexport type AppScreenProps = SeedAppScreen.RootProps;\n\nexport const AppBar = SeedAppBar.Root;\n\nexport const Left = SeedAppBar.Left;\n\nexport const Right = SeedAppBar.Right;\n\nexport const Title = SeedAppBar.Title;\n\nexport const IconButton = SeedAppBar.IconButton;\n\nexport const BackButton = forwardRef<\n HTMLButtonElement,\n SeedAppBar.IconButtonProps\n>(({ children = , onClick, ...otherProps }, ref) => {\n const activity = useActivity();\n const actions = useActions();\n\n const handleOnClick = useCallback(\n (e: React.MouseEvent) => {\n onClick?.(e);\n\n if (!e.defaultPrevented) {\n actions.pop();\n }\n },\n [actions],\n );\n\n if (!activity) {\n return null;\n }\n if (activity.isRoot) {\n return null;\n }\n\n return (\n \n {children}\n \n );\n});\nBackButton.displayName = \"BackButton\";\n\nexport const CloseButton = forwardRef<\n HTMLButtonElement,\n SeedAppBar.IconButtonProps\n>(({ children = , onClick, ...otherProps }, ref) => {\n const activity = useActivity();\n\n const handleOnClick = useCallback(\n (e: React.MouseEvent) => {\n onClick?.(e);\n\n if (!e.defaultPrevented) {\n // you can do something here\n }\n },\n [],\n );\n\n const isRoot = !activity || activity.isRoot;\n\n if (!isRoot) {\n return null;\n }\n\n return (\n \n {children}\n \n );\n});\nCloseButton.displayName = \"CloseButton\";\n\nexport const AppScreen = forwardRef(\n ({ children, ...otherProps }, ref) => {\n return (\n \n \n {children}\n \n \n );\n },\n);\nAppScreen.displayName = \"AppScreen\";\n" + } + ] +} \ No newline at end of file diff --git a/docs/public/__registry__/ui/index.json b/docs/public/__registry__/ui/index.json index 22c1b3d54..66e81b6c0 100644 --- a/docs/public/__registry__/ui/index.json +++ b/docs/public/__registry__/ui/index.json @@ -1,4 +1,13 @@ [ + { + "name": "app-screen", + "files": [ + "ui:app-screen.tsx" + ], + "dependencies": [ + "@seed-design/stackflow" + ] + }, { "name": "alert-dialog", "innerDependencies": [ diff --git a/docs/public/penguin.webp b/docs/public/penguin.webp new file mode 100644 index 000000000..059474dfb Binary files /dev/null and b/docs/public/penguin.webp differ diff --git a/docs/public/rootage/components/top-navigation.json b/docs/public/rootage/components/top-navigation.json new file mode 100644 index 000000000..638f91d35 --- /dev/null +++ b/docs/public/rootage/components/top-navigation.json @@ -0,0 +1,75 @@ +{ + "kind": "ComponentSpec", + "metadata": { + "id": "top-navigation", + "name": "Top Navigation" + }, + "data": { + "theme=cupertino": { + "enabled": { + "root": { + "minHeight": "44px", + "paddingX": "$unit.s4" + }, + "title": { + "fontSize": "$font-size.s6-static", + "fontWeight": "$font-weight.bold" + }, + "icon": { + "size": "24px", + "targetSize": "40px" + } + } + }, + "theme=android": { + "enabled": { + "root": { + "minHeight": "56px", + "paddingX": "$unit.s4" + }, + "title": { + "fontSize": "$font-size.s6-static", + "fontWeight": "$font-weight.bold" + }, + "icon": { + "size": "24px", + "targetSize": "40px" + } + } + }, + "tone=layer": { + "enabled": { + "root": { + "color": "$color.bg.layer-default" + }, + "title": { + "color": "$color.fg.neutral" + }, + "icon": { + "color": "$color.fg.neutral" + } + } + }, + "tone=transparent": { + "enabled": { + "root": { + "color": "#00000000" + }, + "title": { + "color": "$color.fg.static-white" + }, + "icon": { + "color": "$color.fg.static-white" + } + } + }, + "divider=true": { + "enabled": { + "root": { + "strokeColor": "$color.stroke.neutral-muted", + "strokeWidth": "1px" + } + } + } + } +} \ No newline at end of file diff --git a/docs/public/rootage/font-size.json b/docs/public/rootage/font-size.json index d03bd8f0c..1aaf5d30e 100644 --- a/docs/public/rootage/font-size.json +++ b/docs/public/rootage/font-size.json @@ -56,6 +56,11 @@ "values": { "default": "1.625rem" } + }, + "$font-size.s6-static": { + "values": { + "default": "18px" + } } } } diff --git a/docs/registry/registry-ui.ts b/docs/registry/registry-ui.ts index 8b1ad0717..b214d3d7a 100644 --- a/docs/registry/registry-ui.ts +++ b/docs/registry/registry-ui.ts @@ -10,6 +10,11 @@ import tabsPkg from "@seed-design/react-tabs/package.json"; import popoverPkg from "@seed-design/react-popover/package.json"; export const registryUI: RegistryUI = [ + { + name: "app-screen", + files: ["ui:app-screen.tsx"], + dependencies: ["@seed-design/stackflow"], + }, { name: "alert-dialog", innerDependencies: ["action-button"], diff --git a/docs/registry/ui/app-screen.tsx b/docs/registry/ui/app-screen.tsx new file mode 100644 index 000000000..3e6a64d7c --- /dev/null +++ b/docs/registry/ui/app-screen.tsx @@ -0,0 +1,118 @@ +"use client"; + +import "@seed-design/stylesheet/screen.css"; +import "@seed-design/stylesheet/topNavigation.css"; + +import { + IconChevronLeftLine, + IconXmarkLine, +} from "@daangn/react-monochrome-icon"; +import { + AppBar as SeedAppBar, + AppScreen as SeedAppScreen, +} from "@seed-design/stackflow"; +import { useActions, useActivity } from "@stackflow/react"; +import { forwardRef, useCallback } from "react"; + +export type AppBarProps = SeedAppBar.RootProps; + +export type AppScreenProps = SeedAppScreen.RootProps; + +export const AppBar = SeedAppBar.Root; + +export const Left = SeedAppBar.Left; + +export const Right = SeedAppBar.Right; + +export const Title = SeedAppBar.Title; + +export const IconButton = SeedAppBar.IconButton; + +export const BackButton = forwardRef< + HTMLButtonElement, + SeedAppBar.IconButtonProps +>(({ children = , onClick, ...otherProps }, ref) => { + const activity = useActivity(); + const actions = useActions(); + + const handleOnClick = useCallback( + (e: React.MouseEvent) => { + onClick?.(e); + + if (!e.defaultPrevented) { + actions.pop(); + } + }, + [actions], + ); + + if (!activity) { + return null; + } + if (activity.isRoot) { + return null; + } + + return ( + + {children} + + ); +}); +BackButton.displayName = "BackButton"; + +export const CloseButton = forwardRef< + HTMLButtonElement, + SeedAppBar.IconButtonProps +>(({ children = , onClick, ...otherProps }, ref) => { + const activity = useActivity(); + + const handleOnClick = useCallback( + (e: React.MouseEvent) => { + onClick?.(e); + + if (!e.defaultPrevented) { + // you can do something here + } + }, + [], + ); + + const isRoot = !activity || activity.isRoot; + + if (!isRoot) { + return null; + } + + return ( + + {children} + + ); +}); +CloseButton.displayName = "CloseButton"; + +export const AppScreen = forwardRef( + ({ children, ...otherProps }, ref) => { + return ( + + + {children} + + + ); + }, +); +AppScreen.displayName = "AppScreen"; diff --git a/examples/stackflow-spa/package.json b/examples/stackflow-spa/package.json index 44bd5b871..4c398d6a8 100644 --- a/examples/stackflow-spa/package.json +++ b/examples/stackflow-spa/package.json @@ -16,10 +16,11 @@ "@seed-design/react-tabs": "0.0.0-alpha-20241209060641", "@seed-design/react-text-field": "0.0.0-alpha-20241030023710", "@seed-design/recipe": "0.0.0-alpha-20241212122822", + "@seed-design/stackflow": "0.0.0", "@seed-design/stylesheet": "3.0.0-alpha-20241212122822", "@seed-design/vars": "0.0.0", "@stackflow/core": "^1.1.0", - "@stackflow/plugin-basic-ui": "^1.10.1", + "@stackflow/plugin-basic-ui": "^1.11.1", "@stackflow/plugin-history-sync": "^1.7.0", "@stackflow/plugin-renderer-basic": "^1.1.13", "@stackflow/react": "^1.4.1", diff --git a/examples/stackflow-spa/src/activities/ActivityHome.tsx b/examples/stackflow-spa/src/activities/ActivityHome.tsx index a2e7e4580..3ed3b4929 100644 --- a/examples/stackflow-spa/src/activities/ActivityHome.tsx +++ b/examples/stackflow-spa/src/activities/ActivityHome.tsx @@ -21,6 +21,7 @@ const ActivityHome: ActivityComponentType = () => { push("ActivityActionChip", {})} title="ActionChip" /> push("ActivityControlChip", {})} title="ControlChip" /> push("ActivityHelpBubble", {})} title="HelpBubble" /> + push("ActivityTransparentBar", {})} title="TransparentBar" />
diff --git a/examples/stackflow-spa/src/activities/ActivityTransparentBar.tsx b/examples/stackflow-spa/src/activities/ActivityTransparentBar.tsx new file mode 100644 index 000000000..7fd4c7621 --- /dev/null +++ b/examples/stackflow-spa/src/activities/ActivityTransparentBar.tsx @@ -0,0 +1,49 @@ +import type { ActivityComponentType } from "@stackflow/react"; +import { + AppBar, + BackButton, + IconButton, + Left, + Right, + Title, +} from "../design-system/stackflow/AppBar"; +import { AppScreen } from "../design-system/stackflow/AppScreen"; + +import { IconBellLine } from "@daangn/react-monochrome-icon"; +import img from "../assets/peng.jpeg"; +import { theme } from "../stackflow/theme"; + +const ActivityTransparentBar: ActivityComponentType = () => { + return ( + + + + + 야옹 + + + + + + + + + + + + + + + + } + theme={theme} + > + penguin +
+ + ); +}; + +export default ActivityTransparentBar; diff --git a/examples/stackflow-spa/src/assets/peng.jpeg b/examples/stackflow-spa/src/assets/peng.jpeg new file mode 100644 index 000000000..b4e12a16f Binary files /dev/null and b/examples/stackflow-spa/src/assets/peng.jpeg differ diff --git a/examples/stackflow-spa/src/design-system/stackflow/AppBar.tsx b/examples/stackflow-spa/src/design-system/stackflow/AppBar.tsx new file mode 100644 index 000000000..5ce700ae8 --- /dev/null +++ b/examples/stackflow-spa/src/design-system/stackflow/AppBar.tsx @@ -0,0 +1,87 @@ +import "@seed-design/stylesheet/topNavigation.css"; + +import { IconChevronLeftLine, IconXmarkLine } from "@daangn/react-monochrome-icon"; +import { AppBar as SeedAppBar, type AppBarIconButtonProps } from "@seed-design/stackflow"; +import { useActions, useActivity } from "@stackflow/react"; +import { forwardRef, useCallback } from "react"; + +export const AppBar = SeedAppBar.Root; + +export const Left = SeedAppBar.Left; + +export const Right = SeedAppBar.Right; + +export const Title = SeedAppBar.Title; + +export const IconButton = SeedAppBar.IconButton; + +export const BackButton = forwardRef( + ({ children = , onClick, ...otherProps }, ref) => { + const activity = useActivity(); + const actions = useActions(); + + const handleOnClick = useCallback( + (e: React.MouseEvent) => { + onClick?.(e); + + if (!e.defaultPrevented) { + actions.pop(); + } + }, + [actions], + ); + + if (!activity) { + return null; + } + if (activity.isRoot) { + return null; + } + + return ( + + {children} + + ); + }, +); +BackButton.displayName = "BackButton"; + +export const CloseButton = forwardRef( + ({ children = , onClick, ...otherProps }, ref) => { + const activity = useActivity(); + + const handleOnClick = useCallback((e: React.MouseEvent) => { + onClick?.(e); + + if (!e.defaultPrevented) { + // you can do something here + } + }, []); + + const isRoot = !activity || activity.isRoot; + + if (!isRoot) { + return null; + } + + return ( + + {children} + + ); + }, +); +CloseButton.displayName = "CloseButton"; diff --git a/examples/stackflow-spa/src/design-system/stackflow/AppScreen.tsx b/examples/stackflow-spa/src/design-system/stackflow/AppScreen.tsx new file mode 100644 index 000000000..e473ff018 --- /dev/null +++ b/examples/stackflow-spa/src/design-system/stackflow/AppScreen.tsx @@ -0,0 +1,17 @@ +import "@seed-design/stylesheet/screen.css"; + +import { AppScreen as SeedAppScreen, type AppScreenProps } from "@seed-design/stackflow"; +import { forwardRef } from "react"; + +export const AppScreen = forwardRef( + ({ children, ...otherProps }, ref) => { + return ( + + + {children} + + + ); + }, +); +AppScreen.displayName = "AppScreen"; diff --git a/examples/stackflow-spa/src/global.css b/examples/stackflow-spa/src/global.css index ac7a227ec..22fc4b3cf 100644 --- a/examples/stackflow-spa/src/global.css +++ b/examples/stackflow-spa/src/global.css @@ -1,3 +1,5 @@ :root { - font-family: system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif; -} \ No newline at end of file + font-family: system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen, Ubuntu, + Cantarell, "Open Sans", "Helvetica Neue", sans-serif; + background-color: var(--seed-v3-color-bg-layer-default); +} diff --git a/examples/stackflow-spa/src/stackflow/Stack.tsx b/examples/stackflow-spa/src/stackflow/Stack.tsx index 6592a7ab2..ba396ad31 100644 --- a/examples/stackflow-spa/src/stackflow/Stack.tsx +++ b/examples/stackflow-spa/src/stackflow/Stack.tsx @@ -1,11 +1,13 @@ -import { IconBack, basicUIPlugin } from "@stackflow/plugin-basic-ui"; +import { vars } from "@seed-design/vars"; +import { basicUIPlugin } from "@stackflow/plugin-basic-ui"; import { historySyncPlugin } from "@stackflow/plugin-history-sync"; import { basicRendererPlugin } from "@stackflow/plugin-renderer-basic"; import { stackflow } from "@stackflow/react"; -import { vars } from "@seed-design/vars"; import React from "react"; +import { IconChevronLeftLine } from "@daangn/react-monochrome-icon"; import ActivityNotFound from "../activities/ActivityNotFound"; +import { theme } from "./theme"; /** * Stackflow는 웹뷰 내에서 Stack Navigation UI를 도와주는 도구에요. @@ -14,10 +16,6 @@ import ActivityNotFound from "../activities/ActivityNotFound"; * GitHub: https://github.com/daangn/stackflow */ -const theme = /iphone|ipad|ipod/i.test(window.navigator.userAgent.toLowerCase()) - ? "cupertino" - : "android"; - const { Stack, useFlow, useStepFlow } = stackflow({ activities: { ActivityHome: React.lazy(() => import("../activities/ActivityHome")), @@ -25,6 +23,7 @@ const { Stack, useFlow, useStepFlow } = stackflow({ ActivityActionChip: React.lazy(() => import("../activities/ActivityActionChip")), ActivityControlChip: React.lazy(() => import("../activities/ActivityControlChip")), ActivityHelpBubble: React.lazy(() => import("../activities/ActivityHelpBubble")), + ActivityTransparentBar: React.lazy(() => import("../activities/ActivityTransparentBar")), ActivityNotFound, }, plugins: [ @@ -33,12 +32,13 @@ const { Stack, useFlow, useStepFlow } = stackflow({ appBar: { borderColor: vars.$color.stroke.neutral, closeButton: { - renderIcon: () => , + renderIcon: () => , }, iconColor: vars.$color.fg.neutral, textColor: vars.$color.fg.neutral, }, backgroundColor: vars.$color.bg.layerDefault, + dimBackgroundColor: vars.$color.bg.overlay, theme, }), historySyncPlugin({ @@ -49,6 +49,7 @@ const { Stack, useFlow, useStepFlow } = stackflow({ ActivityActionChip: "/action-chip", ActivityControlChip: "/control-chip", ActivityHelpBubble: "/help-bubble", + ActivityTransparentBar: "/transparent-bar", ActivityNotFound: "/404", }, }), diff --git a/examples/stackflow-spa/src/stackflow/theme.ts b/examples/stackflow-spa/src/stackflow/theme.ts new file mode 100644 index 000000000..ac548f6d8 --- /dev/null +++ b/examples/stackflow-spa/src/stackflow/theme.ts @@ -0,0 +1,3 @@ +export const theme = /iphone|ipad|ipod/i.test(window.navigator.userAgent.toLowerCase()) + ? "cupertino" + : "android"; diff --git a/packages/recipe-generator/preset/src/index.ts b/packages/recipe-generator/preset/src/index.ts index 4fc7134d0..06b5f68b6 100644 --- a/packages/recipe-generator/preset/src/index.ts +++ b/packages/recipe-generator/preset/src/index.ts @@ -17,6 +17,7 @@ import inlineBanner from "./inline-banner.recipe"; import progressCircle from "./progress-circle.recipe"; import radio from "./radio.recipe"; import reactionButton from "./reaction-button.recipe"; +import screen from "./screen.recipe"; import segmentedControl from "./segmented-control.recipe"; import selectBoxGroup from "./select-box-group.recipe"; import skeleton from "./skeleton"; @@ -26,6 +27,7 @@ import tabs from "./tabs.recipe"; import textButton from "./text-button.recipe"; import textField from "./text-field.recipe"; import toggleButton from "./toggle-button.recipe"; +import topNavigation from "./top-navigation.recipe"; const recipes = { avatar, @@ -45,6 +47,7 @@ const recipes = { segmentedControl, selectBoxGroup, switch: switchRecipe, + screen, helpBubble, identityPlaceholder, inlineBanner, @@ -56,6 +59,7 @@ const recipes = { skeleton, textButton, textField, + topNavigation, }; export default recipes; diff --git a/packages/recipe-generator/preset/src/screen.recipe.ts b/packages/recipe-generator/preset/src/screen.recipe.ts new file mode 100644 index 000000000..396fa37a6 --- /dev/null +++ b/packages/recipe-generator/preset/src/screen.recipe.ts @@ -0,0 +1,129 @@ +import { vars } from "@seed-design/vars"; +import { topNavigation } from "@seed-design/vars/component"; +import { defineRecipe } from "./helper"; + +const MIN_SAFE_AREA_INSET_TOP = "0px"; // TODO: turn into public interface + +const screen = defineRecipe({ + name: "screen", + slots: ["root", "layer", "dim", "edge"], + base: { + root: { + position: "absolute", + width: "100%", + height: "100%", + left: 0, + right: 0, + overflow: "hidden", + + "&[data-transition-state=exit-done]": { + transform: "translate3d(100%, 0, 0)", + }, + }, + dim: { + zIndex: "var(--z-index-dim)", + + position: "absolute", + width: "100%", + left: 0, + right: 0, + opacity: 0, + + transition: `transform ${vars.$duration.s6}, opacity ${vars.$duration.s6}`, + + "&:is([data-transition-state=enter-active], [data-transition-state=enter-done])": { + opacity: 1, + }, + "&:is([data-transition-state=exit-active], [data-transition-state=exit-done])": { + opacity: 0, + }, + }, + layer: { + zIndex: "var(--z-index-layer)", + + position: "absolute", + width: "100%", + height: "100%", + left: 0, + right: 0, + overflowY: "scroll", + WebkitOverflowScrolling: "touch", + "&::-webkit-scrollbar": { + display: "none", + }, + + backgroundColor: vars.$color.bg.layerDefault, + transition: `transform ${vars.$duration.s6}, opacity ${vars.$duration.s6}`, + }, + edge: { + zIndex: "var(--z-index-edge)", + + position: "absolute", + width: "20px", + height: "100%", + left: 0, + right: 0, + }, + }, + variants: { + theme: { + cupertino: { + root: { + "--app-bar-height": topNavigation.themeCupertino.enabled.root.minHeight, + }, + dim: { + height: "100%", + background: vars.$color.bg.overlay, + }, + layer: { + transform: "translate3d(100%, 0, 0)", + "&:is([data-transition-state=enter-active], [data-transition-state=enter-done])": { + transform: "translate3d(0, 0, 0)", + }, + }, + }, + android: { + root: { + "--app-bar-height": topNavigation.themeAndroid.enabled.root.minHeight, + }, + dim: { + height: "10rem", + background: `linear-gradient(${vars.$color.bg.overlay}, rgba(0, 0, 0, 0))`, + }, + layer: { + opacity: 0, + transform: "translate3d(0, 10rem, 0)", + "&:is([data-transition-state=enter-active], [data-transition-state=enter-done])": { + opacity: 1, + transform: "translate3d(0, 0, 0)", + }, + }, + }, + }, + hasAppBar: { + true: { + root: { + "--app-bar-margin": "var(--app-bar-height)", + + "@supports (padding: max(0px)) and (padding: constant(safe-area-inset-top))": { + "--app-bar-margin": `calc(var(--app-bar-height) + max(${MIN_SAFE_AREA_INSET_TOP}, constant(safe-area-inset-top)))`, + }, + "@supports (padding: max(0px)) and (padding: env(safe-area-inset-top))": { + "--app-bar-margin": `calc(var(--app-bar-height) + max(${MIN_SAFE_AREA_INSET_TOP}, env(safe-area-inset-top)))`, + }, + }, + layer: { + boxSizing: "border-box", + transition: `transform ${vars.$duration.s6}, opacity ${vars.$duration.s6}`, // TODO: add heightTransitionDuration + height: "100%", + }, + edge: { + top: "var(--app-bar-height)", + height: "calc(100% - var(--app-bar-height))", + }, + }, + }, + }, +}); + +export default screen; diff --git a/packages/recipe-generator/preset/src/top-navigation.recipe.ts b/packages/recipe-generator/preset/src/top-navigation.recipe.ts new file mode 100644 index 000000000..4a67c4ce5 --- /dev/null +++ b/packages/recipe-generator/preset/src/top-navigation.recipe.ts @@ -0,0 +1,226 @@ +import { topNavigation as vars } from "@seed-design/vars/component"; +import { defineRecipe } from "./helper"; + +const MIN_SAFE_AREA_INSET_TOP = "0px"; // TODO: turn into public interface +const COLOR_TRANSITION_DURATION = "0s"; + +const topNavigation = defineRecipe({ + name: "topNavigation", + slots: [ + "root", + "safeArea", + "container", + "left", + "right", + "title", + "titleMain", + "titleEdge", + "titleText", + "iconButton", + "icon", + ], + base: { + root: { + zIndex: "var(--z-index-app-bar)", + + position: "absolute", + boxSizing: "content-box", + width: "100%", + // TODO: do we need to set overflow? + + "&[data-transition-state=exit-active]": { + transform: "translate3d(100%, 0, 0)", + transition: `background-color ${COLOR_TRANSITION_DURATION}, box-shadow ${COLOR_TRANSITION_DURATION}, transform 0s`, + }, + }, + safeArea: { + height: `max(${MIN_SAFE_AREA_INSET_TOP}, env(safe-area-inset-top))`, + }, + container: { + display: "flex", + alignItems: "flex-end", + // TODO: do we need to set overflow? + // TODO: add heightTransitionDuration + }, + left: { + display: "flex", + alignItems: "center", + height: "100%", + }, + right: { + display: "flex", + alignItems: "center", + height: "100%", + marginLeft: "auto", + }, + iconButton: { + display: "flex", + alignItems: "center", + justifyContent: "center", + }, + icon: { + display: "inline-block", + flexShrink: 0, + }, + title: { + display: "flex", + alignItems: "center", + flex: 1, + height: "100%", + }, + titleMain: { + // width is calculated in js + // TODO: add heightTransitionDuration + transition: `color ${COLOR_TRANSITION_DURATION}`, + }, + titleEdge: { + appearance: "none", + border: 0, + padding: 0, + background: "none", + position: "absolute", + top: 0, + cursor: "pointer", + left: "50%", + height: "20px", + transform: "translate(-50%)", + maxWidth: "5rem", + display: "none", + }, + titleText: { + whiteSpace: "nowrap", + overflow: "hidden", + textOverflow: "ellipsis", + width: "100%", + }, + }, + variants: { + theme: { + cupertino: { + container: { + height: vars.themeCupertino.enabled.root.minHeight, + paddingInline: vars.themeCupertino.enabled.root.paddingX, + '[data-stackflow-activity-is-active="false"] &': { + opacity: "calc(pow(var(--stackflow-swipe-back-ratio, 1), 2))", + }, + '[data-stackflow-activity-is-active="true"] &': { + opacity: "calc(1 - pow(var(--stackflow-swipe-back-ratio, 0), 2))", + }, + }, + iconButton: { + width: vars.themeCupertino.enabled.icon.targetSize, + height: vars.themeCupertino.enabled.icon.targetSize, + + "&:first-child": { + marginLeft: `calc(-1 * (${vars.themeCupertino.enabled.icon.targetSize} - ${vars.themeCupertino.enabled.icon.size}) / 2)`, + }, + "&:last-child": { + marginRight: `calc(-1 * (${vars.themeCupertino.enabled.icon.targetSize} - ${vars.themeCupertino.enabled.icon.size}) / 2)`, + }, + }, + icon: { + width: vars.themeCupertino.enabled.icon.size, + height: vars.themeCupertino.enabled.icon.size, + '[data-stackflow-activity-is-active="true"] &[data-transition-state="enter-active"]': { + opacity: 1, + }, + '[data-stackflow-activity-is-active="true"] &[data-transition-state="enter-done"]': { + opacity: 1, + }, + }, + titleMain: { + position: "absolute", + display: "flex", + alignItems: "center", + justifyContent: "center", + textAlign: "center", + height: "100%", + left: "50%", + transform: "translate(-50%)", + top: `max(${MIN_SAFE_AREA_INSET_TOP}, env(safe-area-inset-top))`, + }, + titleText: { + fontSize: vars.themeCupertino.enabled.title.fontSize, + fontWeight: vars.themeCupertino.enabled.title.fontWeight, + }, + titleEdge: { + display: "block", + }, + }, + android: { + root: { + opacity: 0, + transform: "translate3d(0, 160px, 0)", + transition: `background-color ${COLOR_TRANSITION_DURATION}, box-shadow ${COLOR_TRANSITION_DURATION}, transform 300ms`, // TODO: define duration in rootage + + "&:is([data-transition-state=enter-active], [data-transition-state=enter-done])": { + opacity: 1, + transform: "translate3d(0, 0, 0)", + }, + }, + container: { + height: vars.themeAndroid.enabled.root.minHeight, + paddingInline: vars.themeAndroid.enabled.root.paddingX, + }, + iconButton: { + width: vars.themeAndroid.enabled.icon.targetSize, + height: vars.themeAndroid.enabled.icon.targetSize, + + "&:first-child": { + marginLeft: `calc(-1 * (${vars.themeAndroid.enabled.icon.targetSize} - ${vars.themeAndroid.enabled.icon.size}) / 2)`, + }, + "&:last-child": { + marginRight: `calc(-1 * (${vars.themeAndroid.enabled.icon.targetSize} - ${vars.themeAndroid.enabled.icon.size}) / 2)`, + }, + }, + icon: { + width: vars.themeAndroid.enabled.icon.size, + height: vars.themeAndroid.enabled.icon.size, + }, + titleMain: { + width: "100%", + justifyContent: "flex-start", + paddingLeft: "16px", + boxSizing: "border-box", + }, + titleText: { + fontSize: vars.themeAndroid.enabled.title.fontSize, + fontWeight: vars.themeAndroid.enabled.title.fontWeight, + }, + }, + }, + tone: { + layer: { + root: { + backgroundColor: vars.toneLayer.enabled.root.color, + }, + icon: { + color: vars.toneLayer.enabled.icon.color, + }, + titleMain: { + color: vars.toneLayer.enabled.title.color, + }, + }, + transparent: { + root: { + backgroundColor: vars.toneTransparent.enabled.root.color, + }, + icon: { + color: vars.toneTransparent.enabled.icon.color, + }, + titleMain: { + color: vars.toneTransparent.enabled.title.color, + }, + }, + }, + border: { + true: { + root: { + boxShadow: `inset 0px calc(-1 * ${vars.dividerTrue.enabled.root.strokeWidth}) 0 ${vars.dividerTrue.enabled.root.strokeColor}`, + }, + }, + }, + }, +}); + +export default topNavigation; diff --git a/packages/recipe/lib/screen.d.ts b/packages/recipe/lib/screen.d.ts new file mode 100644 index 000000000..bdad3ac6a --- /dev/null +++ b/packages/recipe/lib/screen.d.ts @@ -0,0 +1,18 @@ +interface ScreenVariant { + theme: "cupertino" | "android"; +hasAppBar: boolean; +} + +type ScreenVariantMap = { + [key in keyof ScreenVariant]: Array; +}; + +export type ScreenVariantProps = Partial; + +export type ScreenSlotName = "root" | "layer" | "dim" | "edge"; + +export const screenVariantMap: ScreenVariantMap; + +export function screen( + props?: ScreenVariantProps, +): Record; \ No newline at end of file diff --git a/packages/recipe/lib/screen.mjs b/packages/recipe/lib/screen.mjs new file mode 100644 index 000000000..7f0506a25 --- /dev/null +++ b/packages/recipe/lib/screen.mjs @@ -0,0 +1,47 @@ +import { createClassName } from "./className.mjs"; + +const screenSlotNames = [ + [ + "root", + "screen__root" + ], + [ + "layer", + "screen__layer" + ], + [ + "dim", + "screen__dim" + ], + [ + "edge", + "screen__edge" + ] +]; + +const defaultVariant = {}; + +const compoundVariants = []; + +export const screenVariantMap = { + "theme": [ + "cupertino", + "android" + ], + "hasAppBar": [ + "true" + ] +}; + +export const screenVariantKeys = Object.keys(screenVariantMap); + +export function screen(props) { + return Object.fromEntries( + screenSlotNames.map(([slot, className]) => { + return [ + slot, + createClassName(className, { ...defaultVariant, ...props }, compoundVariants), + ]; + }), + ); +} \ No newline at end of file diff --git a/packages/recipe/lib/topNavigation.d.ts b/packages/recipe/lib/topNavigation.d.ts new file mode 100644 index 000000000..2ebc4eb1e --- /dev/null +++ b/packages/recipe/lib/topNavigation.d.ts @@ -0,0 +1,19 @@ +interface TopNavigationVariant { + theme: "cupertino" | "android"; +tone: "layer" | "transparent"; +border: boolean; +} + +type TopNavigationVariantMap = { + [key in keyof TopNavigationVariant]: Array; +}; + +export type TopNavigationVariantProps = Partial; + +export type TopNavigationSlotName = "root" | "safeArea" | "container" | "left" | "right" | "title" | "titleMain" | "titleEdge" | "titleText" | "iconButton" | "icon"; + +export const topNavigationVariantMap: TopNavigationVariantMap; + +export function topNavigation( + props?: TopNavigationVariantProps, +): Record; \ No newline at end of file diff --git a/packages/recipe/lib/topNavigation.mjs b/packages/recipe/lib/topNavigation.mjs new file mode 100644 index 000000000..f8736eeed --- /dev/null +++ b/packages/recipe/lib/topNavigation.mjs @@ -0,0 +1,79 @@ +import { createClassName } from "./className.mjs"; + +const topNavigationSlotNames = [ + [ + "root", + "topNavigation__root" + ], + [ + "safeArea", + "topNavigation__safeArea" + ], + [ + "container", + "topNavigation__container" + ], + [ + "left", + "topNavigation__left" + ], + [ + "right", + "topNavigation__right" + ], + [ + "title", + "topNavigation__title" + ], + [ + "titleMain", + "topNavigation__titleMain" + ], + [ + "titleEdge", + "topNavigation__titleEdge" + ], + [ + "titleText", + "topNavigation__titleText" + ], + [ + "iconButton", + "topNavigation__iconButton" + ], + [ + "icon", + "topNavigation__icon" + ] +]; + +const defaultVariant = {}; + +const compoundVariants = []; + +export const topNavigationVariantMap = { + "theme": [ + "cupertino", + "android" + ], + "tone": [ + "layer", + "transparent" + ], + "border": [ + "true" + ] +}; + +export const topNavigationVariantKeys = Object.keys(topNavigationVariantMap); + +export function topNavigation(props) { + return Object.fromEntries( + topNavigationSlotNames.map(([slot, className]) => { + return [ + slot, + createClassName(className, { ...defaultVariant, ...props }, compoundVariants), + ]; + }), + ); +} \ No newline at end of file diff --git a/packages/rootage/artifacts/components/schema.json b/packages/rootage/artifacts/components/schema.json index c964f93b8..f80d931ee 100644 --- a/packages/rootage/artifacts/components/schema.json +++ b/packages/rootage/artifacts/components/schema.json @@ -347,6 +347,7 @@ { "const": "$font-size.s8", "title": "$font-size.s8", "description": "default: 1.375rem", "markdownDescription": "- default: `1.375rem`" }, { "const": "$font-size.s9", "title": "$font-size.s9", "description": "default: 1.5rem", "markdownDescription": "- default: `1.5rem`" }, { "const": "$font-size.s10", "title": "$font-size.s10", "description": "default: 1.625rem", "markdownDescription": "- default: `1.625rem`" }, + { "const": "$font-size.s6-static", "title": "$font-size.s6-static", "description": "default: 18px", "markdownDescription": "- default: `18px`" }, { "const": "$font-weight.regular", "title": "$font-weight.regular", "description": "default: 400", "markdownDescription": "- default: `400`" }, { "const": "$font-weight.medium", "title": "$font-weight.medium", "description": "default: 500", "markdownDescription": "- default: `500`" }, { "const": "$font-weight.bold", "title": "$font-weight.bold", "description": "default: 700", "markdownDescription": "- default: `700`" }, diff --git a/packages/rootage/artifacts/components/top-navigation.yaml b/packages/rootage/artifacts/components/top-navigation.yaml new file mode 100644 index 000000000..0087c06af --- /dev/null +++ b/packages/rootage/artifacts/components/top-navigation.yaml @@ -0,0 +1,50 @@ +# yaml-language-server: $schema=./schema.json +# slot= +kind: ComponentSpec +metadata: + id: top-navigation + name: Top Navigation +data: + theme=cupertino: + enabled: + root: + minHeight: 44px + paddingX: $unit.s4 + title: + fontSize: $font-size.s6-static + fontWeight: $font-weight.bold + icon: + size: 24px + targetSize: 40px + theme=android: + enabled: + root: + minHeight: 56px + paddingX: $unit.s4 + title: + fontSize: $font-size.s6-static + fontWeight: $font-weight.bold + icon: + size: 24px + targetSize: 40px + tone=layer: + enabled: + root: + color: $color.bg.layer-default + title: + color: $color.fg.neutral + icon: + color: $color.fg.neutral + tone=transparent: + enabled: + root: + color: "#00000000" + title: + color: $color.fg.static-white + icon: + color: $color.fg.static-white + divider=true: + enabled: + root: + strokeColor: $color.stroke.neutral-muted + strokeWidth: 1px diff --git a/packages/rootage/artifacts/font-size.yaml b/packages/rootage/artifacts/font-size.yaml index 027b37285..96039da0a 100644 --- a/packages/rootage/artifacts/font-size.yaml +++ b/packages/rootage/artifacts/font-size.yaml @@ -35,3 +35,6 @@ data: $font-size.s10: values: default: 1.625rem # 26px ÷ 16 + $font-size.s6-static: + values: + default: 18px diff --git a/packages/stackflow/.gitignore b/packages/stackflow/.gitignore new file mode 100644 index 000000000..12c18d4ed --- /dev/null +++ b/packages/stackflow/.gitignore @@ -0,0 +1 @@ +/lib/ diff --git a/packages/stackflow/package.json b/packages/stackflow/package.json new file mode 100644 index 000000000..a9acef570 --- /dev/null +++ b/packages/stackflow/package.json @@ -0,0 +1,53 @@ +{ + "name": "@seed-design/stackflow", + "version": "0.0.0", + "repository": { + "type": "git", + "url": "git+https://github.com/daangn/seed-design.git", + "directory": "packages/stackflow" + }, + "sideEffects": false, + "exports": { + ".": { + "types": "./lib/index.d.ts", + "require": "./lib/index.js", + "import": "./lib/index.mjs" + }, + "./package.json": "./package.json" + }, + "main": "./lib/index.js", + "files": [ + "lib", + "src" + ], + "scripts": { + "prepack": "yarn build", + "clean": "rm -rf lib", + "build": "nanobundle build" + }, + "dependencies": { + "@radix-ui/react-compose-refs": "^1.1.1", + "@radix-ui/react-slot": "^1.1.1", + "@seed-design/dom-utils": "0.0.0-alpha-20241030023710", + "clsx": "^2.1.1" + }, + "devDependencies": { + "nanobundle": "^1.6.0" + }, + "peerDependencies": { + "@seed-design/recipe": "*", + "@stackflow/react": ">=1.4.1", + "@stackflow/react-ui-core": ">=1.2.1", + "react": ">=18.0.0", + "react-dom": ">=18.0.0" + }, + "publishConfig": { + "access": "public" + }, + "ultra": { + "concurrent": [ + "dev", + "build" + ] + } +} diff --git a/packages/stackflow/src/AppBar.namespace.ts b/packages/stackflow/src/AppBar.namespace.ts new file mode 100644 index 000000000..0f233ca96 --- /dev/null +++ b/packages/stackflow/src/AppBar.namespace.ts @@ -0,0 +1,15 @@ +export { + AppBarIconButton as IconButton, + AppBarLeft as Left, + AppBarRight as Right, + AppBarRoot as Root, + AppBarTitle as Title, +} from "./AppBar"; + +export type { + AppBarIconButtonProps as IconButtonProps, + AppBarLeftProps as LeftProps, + AppBarRightProps as RightProps, + AppBarProps as RootProps, + AppBarTitleProps as TitleProps, +} from "./AppBar"; diff --git a/packages/stackflow/src/AppBar.tsx b/packages/stackflow/src/AppBar.tsx new file mode 100644 index 000000000..94d8a8695 --- /dev/null +++ b/packages/stackflow/src/AppBar.tsx @@ -0,0 +1,148 @@ +import { composeRefs } from "@radix-ui/react-compose-refs"; +import { Slot } from "@radix-ui/react-slot"; +import { type TopNavigationVariantProps, topNavigation } from "@seed-design/recipe/topNavigation"; +import { useAppBarTitleMaxWidth } from "@stackflow/react-ui-core"; +import clsx from "clsx"; +import { createContext, forwardRef, useContext, useMemo, useRef } from "react"; +import { useAppScreenContext } from "./useAppScreen"; + +const StyleContext = createContext | null>(null); + +function useStyleContext() { + const context = useContext(StyleContext); + if (!context) { + throw new Error("useStyleContext must be used within a AppBar"); + } + + return context; +} + +export interface AppBarIconButtonProps extends React.ButtonHTMLAttributes {} + +export const AppBarIconButton = forwardRef( + ({ children, className, ...props }, ref) => { + const { dataProps } = useAppScreenContext(); + const classNames = useStyleContext(); + + return ( + + ); + }, +); +AppBarIconButton.displayName = "IconButton"; + +export interface AppBarLeftProps extends React.HTMLAttributes {} + +export const AppBarLeft = forwardRef( + ({ children, className, ...otherProps }, ref) => { + const classNames = useStyleContext(); + const { dataProps } = useAppScreenContext(); + + return ( +
+ {children} +
+ ); + }, +); +AppBarLeft.displayName = "AppBarLeft"; + +export interface AppBarRightProps extends React.HTMLAttributes {} + +export const AppBarRight = forwardRef( + ({ children, className, ...otherProps }, ref) => { + const classNames = useStyleContext(); + const { dataProps } = useAppScreenContext(); + + return ( +
+ {children} +
+ ); + }, +); +AppBarRight.displayName = "AppBarRight"; + +export interface AppBarTitleProps extends React.HTMLAttributes {} + +export const AppBarTitle = forwardRef( + ({ children, className, ...otherProps }, ref) => { + const { theme, dataProps, appBarEdgeProps, refs } = useAppScreenContext(); + const innerRef = useRef(null); + const { maxWidth } = useAppBarTitleMaxWidth({ + outerRef: refs.appBar, + innerRef: innerRef, + enable: theme === "cupertino", + }); + const classNames = useStyleContext(); + + return ( +
+
+ {typeof children === "string" ? ( +
+ {children} +
+ ) : ( + + {children} + + )} +
+
+ ); + }, +); +AppBarTitle.displayName = "AppBarTitle"; + +export interface AppBarProps + extends React.HTMLAttributes, + Omit {} + +export const AppBarRoot = forwardRef( + ({ border = true, tone = "layer", children, ...otherProps }, ref) => { + const { theme, refs, dataProps } = useAppScreenContext(); + + const classNames = useMemo(() => topNavigation({ theme, border, tone }), [theme, border, tone]); + + return ( +
+
+
+ {children} +
+
+ ); + }, +); +AppBarRoot.displayName = "AppBarRoot"; diff --git a/packages/stackflow/src/AppScreen.namespace.ts b/packages/stackflow/src/AppScreen.namespace.ts new file mode 100644 index 000000000..22c30dd54 --- /dev/null +++ b/packages/stackflow/src/AppScreen.namespace.ts @@ -0,0 +1,13 @@ +export { + AppScreenDim as Dim, + AppScreenEdge as Edge, + AppScreenLayer as Layer, + AppScreenRoot as Root, +} from "./AppScreen"; + +export type { + AppScreenDimProps as DimProps, + AppScreenEdgeProps as EdgeProps, + AppScreenLayerProps as LayerProps, + AppScreenProps as RootProps, +} from "./AppScreen"; diff --git a/packages/stackflow/src/AppScreen.tsx b/packages/stackflow/src/AppScreen.tsx new file mode 100644 index 000000000..d8660b4db --- /dev/null +++ b/packages/stackflow/src/AppScreen.tsx @@ -0,0 +1,111 @@ +import { composeRefs } from "@radix-ui/react-compose-refs"; +import { type ScreenVariantProps, screen } from "@seed-design/recipe/screen"; +import { createContext, forwardRef, useContext, useMemo } from "react"; +import { AppScreenProvider, useAppScreen, useAppScreenContext } from "./useAppScreen"; + +const StyleContext = createContext | null>(null); + +function useStyleContext() { + const context = useContext(StyleContext); + if (!context) { + throw new Error("useStyleContext must be used within a AppScreen"); + } + + return context; +} + +export interface AppScreenDimProps extends React.HTMLAttributes {} + +export const AppScreenDim = forwardRef( + ({ className, ...otherProps }, ref) => { + const { refs, dimProps } = useAppScreenContext(); + const classNames = useStyleContext(); + + return ( +
+ ); + }, +); +AppScreenDim.displayName = "AppScreenDim"; + +export interface AppScreenEdgeProps extends React.HTMLAttributes {} + +export const AppScreenEdge = forwardRef( + ({ className, ...otherProps }, ref) => { + const { refs, edgeProps } = useAppScreenContext(); + const classNames = useStyleContext(); + + return ( +
+ ); + }, +); +AppScreenEdge.displayName = "AppScreenEdge"; + +export interface AppScreenLayerProps extends React.HTMLAttributes {} + +export const AppScreenLayer = forwardRef( + ({ className, ...otherProps }, ref) => { + const { refs, layerProps } = useAppScreenContext(); + const classNames = useStyleContext(); + + return ( +
+ ); + }, +); +AppScreenLayer.displayName = "AppScreenLayer"; + +export interface AppScreenProps + extends React.HTMLAttributes, + Omit { + preventSwipeBack?: boolean; + appBar?: React.ReactNode; +} + +export const AppScreenRoot = forwardRef( + ({ preventSwipeBack, appBar, theme, children }, ref) => { + const hasAppBar = !!appBar; + const api = useAppScreen({ + theme, + preventSwipeBack, + activityEnterStyle: undefined, // TODO: Implement activityEnterStyle + modalPresentationStyle: undefined, // TODO: Implement modalPresentationStyle + hasAppBar, + }); + const { refs, rootProps } = api; + const classNames = useMemo(() => screen({ theme, hasAppBar }), [theme, hasAppBar]); + + return ( +
+ + + {appBar} + {children} + + +
+ ); + }, +); +AppScreenRoot.displayName = "AppScreenRoot"; diff --git a/packages/stackflow/src/index.ts b/packages/stackflow/src/index.ts new file mode 100644 index 000000000..776bbc41e --- /dev/null +++ b/packages/stackflow/src/index.ts @@ -0,0 +1,32 @@ +export { + AppBarRoot, + AppBarIconButton, + AppBarLeft, + AppBarRight, + AppBarTitle, +} from "./AppBar"; + +export type { + AppBarIconButtonProps, + AppBarLeftProps, + AppBarProps, + AppBarRightProps, + AppBarTitleProps, +} from "./AppBar"; + +export { + AppScreenRoot, + AppScreenDim, + AppScreenEdge, + AppScreenLayer, +} from "./AppScreen"; + +export type { + AppScreenDimProps, + AppScreenEdgeProps, + AppScreenLayerProps, + AppScreenProps, +} from "./AppScreen"; + +export * as AppBar from "./AppBar.namespace"; +export * as AppScreen from "./AppScreen.namespace"; diff --git a/packages/stackflow/src/useAppScreen.tsx b/packages/stackflow/src/useAppScreen.tsx new file mode 100644 index 000000000..7ab2bb786 --- /dev/null +++ b/packages/stackflow/src/useAppScreen.tsx @@ -0,0 +1,224 @@ +import { useActions, useStack } from "@stackflow/react"; +import { createContext, useContext, useMemo, useRef } from "react"; + +import type { ActivityTransitionState } from "@stackflow/core"; +import { + useLazy, + useMounted, + useNullableActivity, + useStyleEffectHide, + useStyleEffectOffset, + useStyleEffectSwipeBack, + useZIndexBase, +} from "@stackflow/react-ui-core"; + +const OFFSET_PX_ANDROID = 32; +const OFFSET_PX_CUPERTINO = 80; + +function getZIndexStyle(props: { + base: number; + theme?: "android" | "cupertino"; + hasAppBar: boolean; + modalPresentationStyle?: "fullScreen" | undefined; + activityEnterStyle?: "slideInLeft" | undefined; +}) { + const { base, theme, hasAppBar, modalPresentationStyle, activityEnterStyle } = props; + + if (theme === "cupertino") { + return { + "--z-index-dim": base + (modalPresentationStyle === "fullScreen" ? 2 : 0), + "--z-index-layer": base + (hasAppBar && modalPresentationStyle !== "fullScreen" ? 2 : 3), // FIXME: transparent backswipe에서 appBar 순서로 인해 2로 설정. 1로 되돌려야 함. + "--z-index-edge": base + 4, + "--z-index-app-bar": base + 7, + } as React.CSSProperties; + } + + return { + "--z-index-dim": base, + "--z-index-layer": base + (activityEnterStyle === "slideInLeft" ? 1 : 3), + "--z-index-edge": base + 4, + "--z-index-app-bar": base + (activityEnterStyle === "slideInLeft" ? 7 : 4), + } as React.CSSProperties; +} + +export function useAppScreen(props: { + theme?: "android" | "cupertino"; + modalPresentationStyle?: "fullScreen" | undefined; + activityEnterStyle?: "slideInLeft" | undefined; + preventSwipeBack?: boolean; + hasAppBar: boolean; +}) { + const { theme, preventSwipeBack, hasAppBar } = props; + + const stack = useStack(); + const activity = useNullableActivity(); + const mounted = useMounted(); + + const { pop } = useActions(); + + const appScreenRef = useRef(null); + const dimRef = useRef(null); + const layerRef = useRef(null); + const edgeRef = useRef(null); + const appBarRef = useRef(null); + + const modalPresentationStyle = theme === "cupertino" ? props.modalPresentationStyle : undefined; + const activityEnterStyle = theme === "android" ? props.activityEnterStyle : undefined; + const isSwipeBackPrevented = preventSwipeBack || modalPresentationStyle === "fullScreen"; + + const transitionState = activity?.transitionState ?? "enter-done"; + const lazyTransitionState = useLazy(transitionState); + const transitionDuration = stack ? `${stack.transitionDuration}ms` : "0ms"; + const computedTransitionDuration = + stack?.globalTransitionState === "loading" ? transitionDuration : "0ms"; + + useStyleEffectHide({ + refs: [appScreenRef], + }); + useStyleEffectOffset({ + refs: + theme === "cupertino" || activityEnterStyle === "slideInLeft" + ? [layerRef] + : [layerRef, appBarRef], + offsetStyles: + theme === "cupertino" + ? { + transform: `translate3d(-${OFFSET_PX_CUPERTINO}px, 0, 0)`, + opacity: "1", + } + : activityEnterStyle === "slideInLeft" + ? { + transform: "translate3d(-50%, 0, 0)", + opacity: "0", + } + : { + transform: `translate3d(0, -${OFFSET_PX_ANDROID}px, 0)`, + opacity: "1", + }, + transitionDuration: computedTransitionDuration, + hasEffect: modalPresentationStyle !== "fullScreen", + }); + useStyleEffectSwipeBack({ + dimRef, + edgeRef, + paperRef: layerRef, + appBarRef, + offset: OFFSET_PX_CUPERTINO, + transitionDuration: transitionDuration, + preventSwipeBack: isSwipeBackPrevented || theme !== "cupertino", + getActivityTransitionState() { + const $layer = layerRef.current; + const $appScreen = $layer?.parentElement; + + if (!$appScreen) { + return null; + } + + const transitionState = $appScreen.dataset["transition-state"]; + + if (transitionState) { + return transitionState as ActivityTransitionState; + } + + return null; + }, + onSwipeEnd({ swiped }) { + if (swiped) { + pop(); + } + }, + }); + + const zIndexBase = useZIndexBase(); + const zIndexStyle = useMemo( + () => + getZIndexStyle({ + base: zIndexBase, + theme, + hasAppBar, + modalPresentationStyle, + activityEnterStyle, + }), + [zIndexBase, theme, hasAppBar, modalPresentationStyle, activityEnterStyle], + ); + + const dataProps = useMemo( + () => ({ + "data-transition-state": + transitionState === "enter-done" || transitionState === "exit-done" + ? transitionState + : lazyTransitionState, + "data-stackflow-activity-is-active": mounted ? activity?.isActive : undefined, + }), + [transitionState, lazyTransitionState, mounted, activity?.isActive], + ); + + return useMemo( + () => ({ + theme, + activity, + scroll: ({ top }: { top: number }) => { + layerRef.current?.scroll({ + top, + behavior: "smooth", + }); + }, + refs: { + appScreen: appScreenRef, + dim: dimRef, + layer: layerRef, + edge: edgeRef, + appBar: appBarRef, + }, + dataProps, + rootProps: { + ...dataProps, + "data-stackflow-activity-id": mounted ? activity?.id : undefined, + style: zIndexStyle, + } as React.HTMLAttributes, + dimProps: { + ...dataProps, + style: { + display: activityEnterStyle !== "slideInLeft" ? undefined : "none", + }, + } as React.HTMLAttributes, + layerProps: { + ...dataProps, + } as React.HTMLAttributes, + edgeProps: { + ...dataProps, + style: { + display: + !activity?.isRoot && theme === "cupertino" && !isSwipeBackPrevented + ? undefined + : "none", + }, + } as React.HTMLAttributes, + appBarEdgeProps: { + ...dataProps, + onClick: (e) => { + if (!e.defaultPrevented) { + layerRef.current?.scroll({ + top: 0, + behavior: "smooth", + }); + } + }, + } as React.HTMLAttributes, + }), + [theme, activity, zIndexStyle, isSwipeBackPrevented, activityEnterStyle, dataProps, mounted], + ); +} + +const AppScreenContext = createContext | null>(null); + +export const AppScreenProvider = AppScreenContext.Provider; + +export function useAppScreenContext() { + const context = useContext(AppScreenContext); + if (!context) { + throw new Error("useAppScreen must be used within a AppScreen"); + } + + return context; +} diff --git a/packages/stackflow/tsconfig.json b/packages/stackflow/tsconfig.json new file mode 100644 index 000000000..20bc8e032 --- /dev/null +++ b/packages/stackflow/tsconfig.json @@ -0,0 +1,11 @@ +{ + "compilerOptions": { + "target": "ESNext", + "module": "ESNext", + "moduleResolution": "Bundler", + "rootDir": "src", + "outDir": "lib", + "jsx": "react-jsx" + }, + "include": ["src"] +} diff --git a/packages/stylesheet/screen.css b/packages/stylesheet/screen.css new file mode 100644 index 000000000..c337a8d7b --- /dev/null +++ b/packages/stylesheet/screen.css @@ -0,0 +1,101 @@ +.screen__root { + position: absolute; + width: 100%; + height: 100%; + left: 0; + right: 0; + overflow: hidden; +} +.screen__root[data-transition-state=exit-done] { + transform: translate3d(100%, 0, 0); +} +.screen__dim { + z-index: var(--z-index-dim); + position: absolute; + width: 100%; + left: 0; + right: 0; + opacity: 0; + transition: transform var(--seed-v3-duration-s6), opacity var(--seed-v3-duration-s6); +} +.screen__dim:is([data-transition-state=enter-active], [data-transition-state=enter-done]) { + opacity: 1; +} +.screen__dim:is([data-transition-state=exit-active], [data-transition-state=exit-done]) { + opacity: 0; +} +.screen__layer { + z-index: var(--z-index-layer); + position: absolute; + width: 100%; + height: 100%; + left: 0; + right: 0; + overflow-y: scroll; + -webkit-overflow-scrolling: touch; +} +.screen__layer::-webkit-scrollbar { + display: none; +} +.screen__layer { + background-color: var(--seed-v3-color-bg-layer-default); + transition: transform var(--seed-v3-duration-s6), opacity var(--seed-v3-duration-s6); +} +.screen__edge { + z-index: var(--z-index-edge); + position: absolute; + width: 20px; + height: 100%; + left: 0; + right: 0; +} +.screen__root--theme_cupertino { + --app-bar-height: 44px; +} +.screen__dim--theme_cupertino { + height: 100%; + background: var(--seed-v3-color-bg-overlay); +} +.screen__layer--theme_cupertino { + transform: translate3d(100%, 0, 0); +} +.screen__layer--theme_cupertino:is([data-transition-state=enter-active], [data-transition-state=enter-done]) { + transform: translate3d(0, 0, 0); +} +.screen__root--theme_android { + --app-bar-height: 56px; +} +.screen__dim--theme_android { + height: 10rem; + background: linear-gradient(var(--seed-v3-color-bg-overlay), rgba(0, 0, 0, 0)); +} +.screen__layer--theme_android { + opacity: 0; + transform: translate3d(0, 10rem, 0); +} +.screen__layer--theme_android:is([data-transition-state=enter-active], [data-transition-state=enter-done]) { + opacity: 1; + transform: translate3d(0, 0, 0); +} +.screen__root--hasAppBar_true { + --app-bar-margin: var(--app-bar-height); +} +@supports (padding: max(0px)) and (padding: constant(safe-area-inset-top)) { + .screen__root--hasAppBar_true { + --app-bar-margin: calc(var(--app-bar-height) + max(0px, constant(safe-area-inset-top))); + } +} +@supports (padding: max(0px)) and (padding: env(safe-area-inset-top)) { + .screen__root--hasAppBar_true { + --app-bar-margin: calc(var(--app-bar-height) + max(0px, env(safe-area-inset-top))); + } +} +.screen__layer--hasAppBar_true { + box-sizing: border-box; + transition: transform var(--seed-v3-duration-s6), opacity var(--seed-v3-duration-s6); + height: 100%; +} +.screen__edge--hasAppBar_true { + top: var(--app-bar-height); + height: calc(100% - var(--app-bar-height)); +} \ No newline at end of file diff --git a/packages/stylesheet/token.css b/packages/stylesheet/token.css index 991e5deb5..bad227503 100644 --- a/packages/stylesheet/token.css +++ b/packages/stylesheet/token.css @@ -27,6 +27,7 @@ --seed-v3-font-size-s8: 1.375rem; --seed-v3-font-size-s9: 1.5rem; --seed-v3-font-size-s10: 1.625rem; + --seed-v3-font-size-s6-static: 18px; --seed-v3-font-weight-regular: 400; --seed-v3-font-weight-medium: 500; --seed-v3-font-weight-bold: 700; diff --git a/packages/stylesheet/topNavigation.css b/packages/stylesheet/topNavigation.css new file mode 100644 index 000000000..f08f27e7f --- /dev/null +++ b/packages/stylesheet/topNavigation.css @@ -0,0 +1,172 @@ +.topNavigation__root { + z-index: var(--z-index-app-bar); + position: absolute; + box-sizing: content-box; + width: 100%; +} +.topNavigation__root[data-transition-state=exit-active] { + transform: translate3d(100%, 0, 0); + transition: background-color 0s, box-shadow 0s, transform 0s; +} +.topNavigation__safeArea { + height: max(0px, env(safe-area-inset-top)); +} +.topNavigation__container { + display: flex; + align-items: flex-end; +} +.topNavigation__left { + display: flex; + align-items: center; + height: 100%; +} +.topNavigation__right { + display: flex; + align-items: center; + height: 100%; + margin-left: auto; +} +.topNavigation__iconButton { + display: flex; + align-items: center; + justify-content: center; +} +.topNavigation__icon { + display: inline-block; + flex-shrink: 0; +} +.topNavigation__title { + display: flex; + align-items: center; + flex: 1; + height: 100%; +} +.topNavigation__titleMain { + transition: color 0s; +} +.topNavigation__titleEdge { + appearance: none; + border: 0; + padding: 0; + background: none; + position: absolute; + top: 0; + cursor: pointer; + left: 50%; + height: 20px; + transform: translate(-50%); + max-width: 5rem; + display: none; +} +.topNavigation__titleText { + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + width: 100%; +} +.topNavigation__container--theme_cupertino { + height: 44px; + padding-inline: var(--seed-v3-unit-s4); +} +[data-stackflow-activity-is-active="false"] .topNavigation__container--theme_cupertino { + opacity: calc(pow(var(--stackflow-swipe-back-ratio, 1), 2)); +} +[data-stackflow-activity-is-active="true"] .topNavigation__container--theme_cupertino { + opacity: calc(1 - pow(var(--stackflow-swipe-back-ratio, 0), 2)); +} +.topNavigation__iconButton--theme_cupertino { + width: 40px; + height: 40px; +} +.topNavigation__iconButton--theme_cupertino:first-child { + margin-left: calc(-1 * (40px - 24px) / 2); +} +.topNavigation__iconButton--theme_cupertino:last-child { + margin-right: calc(-1 * (40px - 24px) / 2); +} +.topNavigation__icon--theme_cupertino { + width: 24px; + height: 24px; +} +[data-stackflow-activity-is-active="true"] .topNavigation__icon--theme_cupertino[data-transition-state="enter-active"] { + opacity: 1; +} +[data-stackflow-activity-is-active="true"] .topNavigation__icon--theme_cupertino[data-transition-state="enter-done"] { + opacity: 1; +} +.topNavigation__titleMain--theme_cupertino { + position: absolute; + display: flex; + align-items: center; + justify-content: center; + text-align: center; + height: 100%; + left: 50%; + transform: translate(-50%); + top: max(0px, env(safe-area-inset-top)); +} +.topNavigation__titleText--theme_cupertino { + font-size: var(--seed-v3-font-size-s6-static); + font-weight: var(--seed-v3-font-weight-bold); +} +.topNavigation__titleEdge--theme_cupertino { + display: block; +} +.topNavigation__root--theme_android { + opacity: 0; + transform: translate3d(0, 160px, 0); + transition: background-color 0s, box-shadow 0s, transform 300ms; +} +.topNavigation__root--theme_android:is([data-transition-state=enter-active], [data-transition-state=enter-done]) { + opacity: 1; + transform: translate3d(0, 0, 0); +} +.topNavigation__container--theme_android { + height: 56px; + padding-inline: var(--seed-v3-unit-s4); +} +.topNavigation__iconButton--theme_android { + width: 40px; + height: 40px; +} +.topNavigation__iconButton--theme_android:first-child { + margin-left: calc(-1 * (40px - 24px) / 2); +} +.topNavigation__iconButton--theme_android:last-child { + margin-right: calc(-1 * (40px - 24px) / 2); +} +.topNavigation__icon--theme_android { + width: 24px; + height: 24px; +} +.topNavigation__titleMain--theme_android { + width: 100%; + justify-content: flex-start; + padding-left: 16px; + box-sizing: border-box; +} +.topNavigation__titleText--theme_android { + font-size: var(--seed-v3-font-size-s6-static); + font-weight: var(--seed-v3-font-weight-bold); +} +.topNavigation__root--tone_layer { + background-color: var(--seed-v3-color-bg-layer-default); +} +.topNavigation__icon--tone_layer { + color: var(--seed-v3-color-fg-neutral); +} +.topNavigation__titleMain--tone_layer { + color: var(--seed-v3-color-fg-neutral); +} +.topNavigation__root--tone_transparent { + background-color: #00000000; +} +.topNavigation__icon--tone_transparent { + color: var(--seed-v3-color-fg-static-white); +} +.topNavigation__titleMain--tone_transparent { + color: var(--seed-v3-color-fg-static-white); +} +.topNavigation__root--border_true { + box-shadow: inset 0px calc(-1 * 1px) 0 var(--seed-v3-color-stroke-neutral-muted); +} \ No newline at end of file diff --git a/packages/vars/lib/component/index.d.ts b/packages/vars/lib/component/index.d.ts index 20c58e3c3..fa26611eb 100644 --- a/packages/vars/lib/component/index.d.ts +++ b/packages/vars/lib/component/index.d.ts @@ -26,4 +26,5 @@ export { vars as tablist } from "./tablist"; export { vars as textButton } from "./text-button"; export { vars as textField } from "./text-field"; export { vars as toggleButton } from "./toggle-button"; -export { vars as typography } from "./typography"; \ No newline at end of file +export { vars as topNavigation } from "./top-navigation"; +export { vars as typography } from "./typography"; diff --git a/packages/vars/lib/component/index.mjs b/packages/vars/lib/component/index.mjs index 2a1093f38..ebe300829 100644 --- a/packages/vars/lib/component/index.mjs +++ b/packages/vars/lib/component/index.mjs @@ -26,4 +26,5 @@ export { vars as tablist } from "./tablist.mjs"; export { vars as textButton } from "./text-button.mjs"; export { vars as textField } from "./text-field.mjs"; export { vars as toggleButton } from "./toggle-button.mjs"; +export { vars as topNavigation } from "./top-navigation.mjs"; export { vars as typography } from "./typography.mjs"; \ No newline at end of file diff --git a/packages/vars/lib/component/top-navigation.d.ts b/packages/vars/lib/component/top-navigation.d.ts new file mode 100644 index 000000000..e885513ea --- /dev/null +++ b/packages/vars/lib/component/top-navigation.d.ts @@ -0,0 +1,68 @@ +export declare const vars: { + "themeCupertino": { + "enabled": { + "root": { + "minHeight": "44px", + "paddingX": "var(--seed-v3-unit-s4)" + }, + "title": { + "fontSize": "var(--seed-v3-font-size-s6-static)", + "fontWeight": "var(--seed-v3-font-weight-bold)" + }, + "icon": { + "size": "24px", + "targetSize": "40px" + } + } + }, + "themeAndroid": { + "enabled": { + "root": { + "minHeight": "56px", + "paddingX": "var(--seed-v3-unit-s4)" + }, + "title": { + "fontSize": "var(--seed-v3-font-size-s6-static)", + "fontWeight": "var(--seed-v3-font-weight-bold)" + }, + "icon": { + "size": "24px", + "targetSize": "40px" + } + } + }, + "toneLayer": { + "enabled": { + "root": { + "color": "var(--seed-v3-color-bg-layer-default)" + }, + "title": { + "color": "var(--seed-v3-color-fg-neutral)" + }, + "icon": { + "color": "var(--seed-v3-color-fg-neutral)" + } + } + }, + "toneTransparent": { + "enabled": { + "root": { + "color": "#00000000" + }, + "title": { + "color": "var(--seed-v3-color-fg-static-white)" + }, + "icon": { + "color": "var(--seed-v3-color-fg-static-white)" + } + } + }, + "dividerTrue": { + "enabled": { + "root": { + "strokeColor": "var(--seed-v3-color-stroke-neutral-muted)", + "strokeWidth": "1px" + } + } + } +} \ No newline at end of file diff --git a/packages/vars/lib/component/top-navigation.mjs b/packages/vars/lib/component/top-navigation.mjs new file mode 100644 index 000000000..9ffaddbfe --- /dev/null +++ b/packages/vars/lib/component/top-navigation.mjs @@ -0,0 +1,68 @@ +export const vars = { + "themeCupertino": { + "enabled": { + "root": { + "minHeight": "44px", + "paddingX": "var(--seed-v3-unit-s4)" + }, + "title": { + "fontSize": "var(--seed-v3-font-size-s6-static)", + "fontWeight": "var(--seed-v3-font-weight-bold)" + }, + "icon": { + "size": "24px", + "targetSize": "40px" + } + } + }, + "themeAndroid": { + "enabled": { + "root": { + "minHeight": "56px", + "paddingX": "var(--seed-v3-unit-s4)" + }, + "title": { + "fontSize": "var(--seed-v3-font-size-s6-static)", + "fontWeight": "var(--seed-v3-font-weight-bold)" + }, + "icon": { + "size": "24px", + "targetSize": "40px" + } + } + }, + "toneLayer": { + "enabled": { + "root": { + "color": "var(--seed-v3-color-bg-layer-default)" + }, + "title": { + "color": "var(--seed-v3-color-fg-neutral)" + }, + "icon": { + "color": "var(--seed-v3-color-fg-neutral)" + } + } + }, + "toneTransparent": { + "enabled": { + "root": { + "color": "#00000000" + }, + "title": { + "color": "var(--seed-v3-color-fg-static-white)" + }, + "icon": { + "color": "var(--seed-v3-color-fg-static-white)" + } + } + }, + "dividerTrue": { + "enabled": { + "root": { + "strokeColor": "var(--seed-v3-color-stroke-neutral-muted)", + "strokeWidth": "1px" + } + } + } +} \ No newline at end of file diff --git a/packages/vars/lib/font-size.d.ts b/packages/vars/lib/font-size.d.ts index f11246774..8cb4911b4 100644 --- a/packages/vars/lib/font-size.d.ts +++ b/packages/vars/lib/font-size.d.ts @@ -7,4 +7,5 @@ export declare const s6 = "var(--seed-v3-font-size-s6)"; export declare const s7 = "var(--seed-v3-font-size-s7)"; export declare const s8 = "var(--seed-v3-font-size-s8)"; export declare const s9 = "var(--seed-v3-font-size-s9)"; -export declare const s10 = "var(--seed-v3-font-size-s10)"; \ No newline at end of file +export declare const s10 = "var(--seed-v3-font-size-s10)"; +export declare const s6Static = "var(--seed-v3-font-size-s6-static)"; \ No newline at end of file diff --git a/packages/vars/lib/font-size.mjs b/packages/vars/lib/font-size.mjs index 85f7a23a9..31af9bd07 100644 --- a/packages/vars/lib/font-size.mjs +++ b/packages/vars/lib/font-size.mjs @@ -7,4 +7,5 @@ export const s6 = "var(--seed-v3-font-size-s6)"; export const s7 = "var(--seed-v3-font-size-s7)"; export const s8 = "var(--seed-v3-font-size-s8)"; export const s9 = "var(--seed-v3-font-size-s9)"; -export const s10 = "var(--seed-v3-font-size-s10)"; \ No newline at end of file +export const s10 = "var(--seed-v3-font-size-s10)"; +export const s6Static = "var(--seed-v3-font-size-s6-static)"; \ No newline at end of file diff --git a/yarn.lock b/yarn.lock index 61e66f524..a307fedac 100644 --- a/yarn.lock +++ b/yarn.lock @@ -6113,6 +6113,19 @@ __metadata: languageName: node linkType: hard +"@radix-ui/react-compose-refs@npm:1.1.1, @radix-ui/react-compose-refs@npm:^1.1.1": + version: 1.1.1 + resolution: "@radix-ui/react-compose-refs@npm:1.1.1" + peerDependencies: + "@types/react": "*" + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + "@types/react": + optional: true + checksum: 10/1be82f9f7fab96cc10f167a2e4f976e0135a63d473334f664c06f02af13bc5ea1994cb0505f89ed190d756cb65d57506721c030908af07e49b9e3cfd36044f33 + languageName: node + linkType: hard + "@radix-ui/react-context@npm:1.1.0": version: 1.1.0 resolution: "@radix-ui/react-context@npm:1.1.0" @@ -6477,6 +6490,21 @@ __metadata: languageName: node linkType: hard +"@radix-ui/react-slot@npm:^1.1.1": + version: 1.1.1 + resolution: "@radix-ui/react-slot@npm:1.1.1" + dependencies: + "@radix-ui/react-compose-refs": "npm:1.1.1" + peerDependencies: + "@types/react": "*" + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + "@types/react": + optional: true + checksum: 10/5b1ee5100da356c8f9f56cd7ca273838a373fa3808f0f909b1e132c4f734282571cb666e86a548831ee82a62240e126d43379994285a9b030fd34ea43538b5e2 + languageName: node + linkType: hard + "@radix-ui/react-tabs@npm:^1.1.1": version: 1.1.1 resolution: "@radix-ui/react-tabs@npm:1.1.1" @@ -7938,10 +7966,11 @@ __metadata: "@seed-design/react-tabs": "npm:0.0.0-alpha-20241209060641" "@seed-design/react-text-field": "npm:0.0.0-alpha-20241030023710" "@seed-design/recipe": "npm:0.0.0-alpha-20241212122822" + "@seed-design/stackflow": "npm:0.0.0" "@seed-design/stylesheet": "npm:3.0.0-alpha-20241212122822" "@seed-design/vars": "npm:0.0.0" "@stackflow/core": "npm:^1.1.0" - "@stackflow/plugin-basic-ui": "npm:^1.10.1" + "@stackflow/plugin-basic-ui": "npm:^1.11.1" "@stackflow/plugin-history-sync": "npm:^1.7.0" "@stackflow/plugin-renderer-basic": "npm:^1.1.13" "@stackflow/react": "npm:^1.4.1" @@ -7961,6 +7990,24 @@ __metadata: languageName: unknown linkType: soft +"@seed-design/stackflow@npm:0.0.0, @seed-design/stackflow@workspace:packages/stackflow": + version: 0.0.0-use.local + resolution: "@seed-design/stackflow@workspace:packages/stackflow" + dependencies: + "@radix-ui/react-compose-refs": "npm:^1.1.1" + "@radix-ui/react-slot": "npm:^1.1.1" + "@seed-design/dom-utils": "npm:0.0.0-alpha-20241030023710" + clsx: "npm:^2.1.1" + nanobundle: "npm:^1.6.0" + peerDependencies: + "@seed-design/recipe": "*" + "@stackflow/react": ">=1.4.1" + "@stackflow/react-ui-core": ">=1.2.1" + react: ">=18.0.0" + react-dom: ">=18.0.0" + languageName: unknown + linkType: soft + "@seed-design/stylesheet@npm:3.0.0-alpha-20241212122822, @seed-design/stylesheet@workspace:packages/stylesheet": version: 0.0.0-use.local resolution: "@seed-design/stylesheet@workspace:packages/stylesheet" @@ -8276,11 +8323,11 @@ __metadata: languageName: node linkType: hard -"@stackflow/plugin-basic-ui@npm:^1.10.1": - version: 1.10.1 - resolution: "@stackflow/plugin-basic-ui@npm:1.10.1" +"@stackflow/plugin-basic-ui@npm:^1.11.1": + version: 1.11.1 + resolution: "@stackflow/plugin-basic-ui@npm:1.11.1" dependencies: - "@stackflow/react-ui-core": "npm:^1.1.2" + "@stackflow/react-ui-core": "npm:^1.2.1" "@vanilla-extract/css": "npm:^1.15.3" "@vanilla-extract/dynamic": "npm:^2.1.1" "@vanilla-extract/private": "npm:^1.0.5" @@ -8290,7 +8337,7 @@ __metadata: "@stackflow/react": ^1.3.2-canary.0 "@types/react": ">=16.8.0" react: ">=16.8.0" - checksum: 10/82a73aae9b0f57183118d702f0dedccf47c2746ef21de79803abbf084479cc08415bdfec8f305a38f133404a15263bde2a76287908a649130894811a47cc747e + checksum: 10/cad8cf26ae0d4c7262f65c7e772b0a80e36567c8b6bb717cce1650955b096504d0e43b0d7b5d87339f6f081282a7860d5e99ada433311a76a604f675c779d746 languageName: node linkType: hard @@ -8335,6 +8382,18 @@ __metadata: languageName: node linkType: hard +"@stackflow/react-ui-core@npm:^1.2.1": + version: 1.2.1 + resolution: "@stackflow/react-ui-core@npm:1.2.1" + peerDependencies: + "@stackflow/core": ^1.1.0-canary.0 + "@stackflow/react": ^1.3.2-canary.0 + "@types/react": ">=16.8.0" + react: ">=16.8.0" + checksum: 10/24a487267225b58e23eeef5d213df6a4778d3e3139e3608c5ee7434750ae64da0f7ad788697db455630037c4629588c4f42f775d4cf903aa16c7a5e2cfb169ee + languageName: node + linkType: hard + "@stackflow/react@npm:^1.4.0": version: 1.4.0 resolution: "@stackflow/react@npm:1.4.0"