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
+
+
+
+
+
+
+ }
+ >
+
+
+
+
+ );
+};
+
+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 \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 \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}
+ >
+
+
+
+ );
+};
+
+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 (
+
+ );
+ },
+);
+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"