From 169596530839be8b03459c6c42b5240495e3537a Mon Sep 17 00:00:00 2001 From: Daniel Williams Date: Thu, 4 Apr 2024 13:07:09 +0100 Subject: [PATCH] Feat/v8 UI (#564) --- ROADMAP.md | 17 +- examples/expo-example/.storybook/main.ts | 3 +- .../.storybook/storybook.requires.ts | 31 +- .../ActionExample/Actions.stories.tsx | 3 +- .../components/ActionExample/Actions.tsx | 3 +- examples/expo-example/metro.config.js | 35 +- examples/expo-example/package.json | 50 +- lerna.json | 2 +- package.json | 3 +- packages/ondevice-actions/package.json | 8 +- packages/ondevice-backgrounds/package.json | 8 +- packages/ondevice-controls/package.json | 15 +- .../ondevice-controls/src/types/Array.tsx | 3 +- .../ondevice-controls/src/types/Number.tsx | 3 +- .../ondevice-controls/src/types/Object.tsx | 3 +- packages/ondevice-controls/src/types/Text.tsx | 3 +- packages/ondevice-notes/package.json | 12 +- packages/react-native-theming/package.json | 10 +- .../react-native-theming/scripts/patchdts.ts | 10 +- .../src/emotionAugmentation.d.ts | 1 + packages/react-native-theming/src/index.ts | 4 +- packages/react-native-theming/src/newTheme.ts | 242 + packages/react-native-theming/src/theme.ts | 217 +- packages/react-native-ui/package.json | 84 + .../react-native-ui/src/Button.stories.tsx | 134 + packages/react-native-ui/src/Button.tsx | 171 + .../react-native-ui/src/Explorer.stories.tsx | 40 + packages/react-native-ui/src/Explorer.tsx | 38 + packages/react-native-ui/src/IconButton.tsx | 11 + .../react-native-ui/src/Layout.stories.tsx | 33 + packages/react-native-ui/src/Layout.tsx | 103 + .../react-native-ui/src/LayoutProvider.tsx | 90 + .../react-native-ui/src/MobileAddonsPanel.tsx | 164 + .../react-native-ui/src/MobileMenuDrawer.tsx | 82 + packages/react-native-ui/src/Refs.tsx | 82 + packages/react-native-ui/src/Search.tsx | 243 + .../react-native-ui/src/SearchResults.tsx | 245 + .../react-native-ui/src/Sidebar.stories.tsx | 224 + packages/react-native-ui/src/Sidebar.tsx | 137 + packages/react-native-ui/src/Tree.stories.tsx | 131 + packages/react-native-ui/src/Tree.tsx | 387 + .../react-native-ui/src/TreeNode.stories.tsx | 117 + packages/react-native-ui/src/TreeNode.tsx | 145 + packages/react-native-ui/src/constants.ts | 4 + .../src/icon/BottomBarToggleIcon.tsx | 23 + .../react-native-ui/src/icon/CloseIcon.tsx | 23 + .../src/icon/CollapseAllIcon.tsx | 17 + .../react-native-ui/src/icon/CollapseIcon.tsx | 39 + .../src/icon/ComponentIcon.tsx | 14 + .../src/icon/ExpandAllIcon.tsx | 17 + .../src/icon/FaceHappyIcon.tsx | 18 + .../react-native-ui/src/icon/GroupIcon.tsx | 14 + .../react-native-ui/src/icon/MenuIcon.tsx | 17 + .../react-native-ui/src/icon/SearchIcon.tsx | 20 + .../react-native-ui/src/icon/StoryIcon.tsx | 14 + packages/react-native-ui/src/index.tsx | 10 + .../react-native-ui/src/mockdata.large.ts | 25217 ++++++++++++++++ packages/react-native-ui/src/mockdata.ts | 287 + packages/react-native-ui/src/types.ts | 66 + packages/react-native-ui/src/useExpanded.ts | 72 + packages/react-native-ui/src/useLastViewed.ts | 48 + .../react-native-ui/src/util/StoryHash.ts | 244 + packages/react-native-ui/src/util/status.tsx | 70 + packages/react-native-ui/src/util/tree.ts | 91 + packages/react-native-ui/tsconfig.json | 9 + packages/react-native-ui/tsup.config.ts | 14 + .../react-native/buildscripts/gendtsdev.ts | 4 +- packages/react-native/package.json | 28 +- .../__snapshots__/generate.test.js.snap | 72 +- packages/react-native/scripts/generate.js | 18 +- packages/react-native/src/Start.tsx | 28 +- packages/react-native/src/View.tsx | 72 +- .../src/components/OnDeviceUI/OnDeviceUI.tsx | 239 - .../src/components/OnDeviceUI/Panel.tsx | 30 - ...bsolute-positioned-keyboard-aware-view.tsx | 52 - .../components/OnDeviceUI/addons/Addons.tsx | 52 - .../OnDeviceUI/addons/AddonsSkeleton.tsx | 96 - .../src/components/OnDeviceUI/addons/List.tsx | 36 - .../components/OnDeviceUI/addons/Wrapper.tsx | 37 - .../components/OnDeviceUI/addons/index.tsx | 1 - .../src/components/OnDeviceUI/animation.ts | 141 - .../src/components/OnDeviceUI/index.tsx | 1 - .../OnDeviceUI/navigation/Navigation.tsx | 88 - .../OnDeviceUI/navigation/NavigationBar.tsx | 39 - .../navigation/NavigationButton.tsx | 48 - .../OnDeviceUI/navigation/constants.ts | 3 - .../OnDeviceUI/navigation/index.tsx | 1 - .../src/components/Shared/icons.tsx | 68 - .../src/components/Shared/tabs.tsx | 78 - .../StoryListView/StoryListView.tsx | 329 - .../StoryListView/getNestedStories.test.ts | 272 - .../StoryListView/getNestedStories.ts | 103 - .../src/components/StoryListView/index.tsx | 1 - packages/react-native/src/constants.ts | 5 - packages/react-native/src/executeLoadable.ts | 106 - packages/react-native/src/hooks.tsx | 141 +- packages/react-native/src/index.ts | 2 +- .../cli/stories/Button/Button.stories.tsx | 12 +- .../template/cli/stories/Button/Button.tsx | 2 +- .../template/cli/storybook.requires.ts | 35 +- tsconfig.json | 1 + yarn.lock | 5585 ++-- 102 files changed, 32309 insertions(+), 5212 deletions(-) create mode 100644 packages/react-native-theming/src/newTheme.ts create mode 100644 packages/react-native-ui/package.json create mode 100644 packages/react-native-ui/src/Button.stories.tsx create mode 100644 packages/react-native-ui/src/Button.tsx create mode 100644 packages/react-native-ui/src/Explorer.stories.tsx create mode 100644 packages/react-native-ui/src/Explorer.tsx create mode 100644 packages/react-native-ui/src/IconButton.tsx create mode 100644 packages/react-native-ui/src/Layout.stories.tsx create mode 100644 packages/react-native-ui/src/Layout.tsx create mode 100644 packages/react-native-ui/src/LayoutProvider.tsx create mode 100644 packages/react-native-ui/src/MobileAddonsPanel.tsx create mode 100644 packages/react-native-ui/src/MobileMenuDrawer.tsx create mode 100644 packages/react-native-ui/src/Refs.tsx create mode 100644 packages/react-native-ui/src/Search.tsx create mode 100644 packages/react-native-ui/src/SearchResults.tsx create mode 100644 packages/react-native-ui/src/Sidebar.stories.tsx create mode 100644 packages/react-native-ui/src/Sidebar.tsx create mode 100644 packages/react-native-ui/src/Tree.stories.tsx create mode 100644 packages/react-native-ui/src/Tree.tsx create mode 100644 packages/react-native-ui/src/TreeNode.stories.tsx create mode 100644 packages/react-native-ui/src/TreeNode.tsx create mode 100644 packages/react-native-ui/src/constants.ts create mode 100644 packages/react-native-ui/src/icon/BottomBarToggleIcon.tsx create mode 100644 packages/react-native-ui/src/icon/CloseIcon.tsx create mode 100644 packages/react-native-ui/src/icon/CollapseAllIcon.tsx create mode 100644 packages/react-native-ui/src/icon/CollapseIcon.tsx create mode 100644 packages/react-native-ui/src/icon/ComponentIcon.tsx create mode 100644 packages/react-native-ui/src/icon/ExpandAllIcon.tsx create mode 100644 packages/react-native-ui/src/icon/FaceHappyIcon.tsx create mode 100644 packages/react-native-ui/src/icon/GroupIcon.tsx create mode 100644 packages/react-native-ui/src/icon/MenuIcon.tsx create mode 100644 packages/react-native-ui/src/icon/SearchIcon.tsx create mode 100644 packages/react-native-ui/src/icon/StoryIcon.tsx create mode 100644 packages/react-native-ui/src/index.tsx create mode 100644 packages/react-native-ui/src/mockdata.large.ts create mode 100644 packages/react-native-ui/src/mockdata.ts create mode 100644 packages/react-native-ui/src/types.ts create mode 100644 packages/react-native-ui/src/useExpanded.ts create mode 100644 packages/react-native-ui/src/useLastViewed.ts create mode 100644 packages/react-native-ui/src/util/StoryHash.ts create mode 100644 packages/react-native-ui/src/util/status.tsx create mode 100644 packages/react-native-ui/src/util/tree.ts create mode 100644 packages/react-native-ui/tsconfig.json create mode 100644 packages/react-native-ui/tsup.config.ts delete mode 100644 packages/react-native/src/components/OnDeviceUI/OnDeviceUI.tsx delete mode 100644 packages/react-native/src/components/OnDeviceUI/Panel.tsx delete mode 100644 packages/react-native/src/components/OnDeviceUI/absolute-positioned-keyboard-aware-view.tsx delete mode 100644 packages/react-native/src/components/OnDeviceUI/addons/Addons.tsx delete mode 100644 packages/react-native/src/components/OnDeviceUI/addons/AddonsSkeleton.tsx delete mode 100644 packages/react-native/src/components/OnDeviceUI/addons/List.tsx delete mode 100644 packages/react-native/src/components/OnDeviceUI/addons/Wrapper.tsx delete mode 100644 packages/react-native/src/components/OnDeviceUI/addons/index.tsx delete mode 100644 packages/react-native/src/components/OnDeviceUI/animation.ts delete mode 100644 packages/react-native/src/components/OnDeviceUI/index.tsx delete mode 100644 packages/react-native/src/components/OnDeviceUI/navigation/Navigation.tsx delete mode 100644 packages/react-native/src/components/OnDeviceUI/navigation/NavigationBar.tsx delete mode 100644 packages/react-native/src/components/OnDeviceUI/navigation/NavigationButton.tsx delete mode 100644 packages/react-native/src/components/OnDeviceUI/navigation/constants.ts delete mode 100644 packages/react-native/src/components/OnDeviceUI/navigation/index.tsx delete mode 100644 packages/react-native/src/components/Shared/icons.tsx delete mode 100644 packages/react-native/src/components/Shared/tabs.tsx delete mode 100644 packages/react-native/src/components/StoryListView/StoryListView.tsx delete mode 100644 packages/react-native/src/components/StoryListView/getNestedStories.test.ts delete mode 100644 packages/react-native/src/components/StoryListView/getNestedStories.ts delete mode 100644 packages/react-native/src/components/StoryListView/index.tsx delete mode 100644 packages/react-native/src/constants.ts delete mode 100644 packages/react-native/src/executeLoadable.ts diff --git a/ROADMAP.md b/ROADMAP.md index eec067f460..d1c1997192 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -1,12 +1,9 @@ # Roadmap -Now that V6.5 is ready, we're looking to the future. Here's what we're working on next: - -- Firstly stablise the V6.5 release -- Version 7 -- Support story categorisation for more organisation in the story list sidebar -- Experiment with use of metro’s experimental `require.context` to simplify story imports -- Better testing support, potentially support interaction tests with the play function -- Better integration with `@storybook/addon-react-native-web` -- Better way to display addons to avoid shrinking the preview -- … and more \ No newline at end of file +- [x] Create a roadmap +- [ ] UI overhaul + - [ ] Redo theming + - [ ] integrate reanimated and gorhom as new dependencies + - [ ] New storybook/react-native-ui library for ondevice ui components + - [ ] implement new ondevice ui based on storybook v8 mobile design +- [ ] improve controls api implementation to match more closely web api diff --git a/examples/expo-example/.storybook/main.ts b/examples/expo-example/.storybook/main.ts index 84cf8e8f96..c799e5d6d7 100644 --- a/examples/expo-example/.storybook/main.ts +++ b/examples/expo-example/.storybook/main.ts @@ -3,6 +3,7 @@ import { StorybookConfig } from '@storybook/react-native'; const main: StorybookConfig = { stories: [ '../components/**/*.stories.?(ts|tsx|js|jsx)', + '../../../packages/react-native-ui/**/*.stories.?(ts|tsx|js|jsx)', { directory: '../other_components', files: '**/*.stories.?(ts|tsx|js|jsx)', @@ -16,10 +17,10 @@ const main: StorybookConfig = { // '../components/**/*.storiesof.?(ts|tsx|js|jsx)', ], addons: [ - '@storybook/addon-ondevice-notes', '@storybook/addon-ondevice-controls', '@storybook/addon-ondevice-backgrounds', '@storybook/addon-ondevice-actions', + '@storybook/addon-ondevice-notes', ], }; diff --git a/examples/expo-example/.storybook/storybook.requires.ts b/examples/expo-example/.storybook/storybook.requires.ts index 294ad19cb1..b38f22f68a 100644 --- a/examples/expo-example/.storybook/storybook.requires.ts +++ b/examples/expo-example/.storybook/storybook.requires.ts @@ -1,15 +1,11 @@ /* do not change this file, it is auto generated by storybook. */ -import { - start, - prepareStories, - getProjectAnnotations, -} from "@storybook/react-native"; +import { start, updateView } from "@storybook/react-native"; -import "@storybook/addon-ondevice-notes/register"; import "@storybook/addon-ondevice-controls/register"; import "@storybook/addon-ondevice-backgrounds/register"; import "@storybook/addon-ondevice-actions/register"; +import "@storybook/addon-ondevice-notes/register"; const normalizedStories = [ { @@ -25,6 +21,19 @@ const normalizedStories = [ /^\.(?:(?:^|\/|(?:(?:(?!(?:^|\/)\.).)*?)\/)(?!\.)(?=.)[^/]*?\.stories\.(?:ts|tsx|js|jsx)?)$/ ), }, + { + titlePrefix: "", + directory: "../../packages/react-native-ui", + files: "**/*.stories.?(ts|tsx|js|jsx)", + importPathMatcher: + /^\.(?:(?:^|\/|(?:(?:(?!(?:^|\/)\.).)*?)\/)(?!\.)(?=.)[^/]*?\.stories\.(?:ts|tsx|js|jsx)?)$/, + // @ts-ignore + req: require.context( + "../../../packages/react-native-ui", + true, + /^\.(?:(?:^|\/|(?:(?:(?!(?:^|\/)\.).)*?)\/)(?!\.)(?=.)[^/]*?\.stories\.(?:ts|tsx|js|jsx)?)$/ + ), + }, { titlePrefix: "OtherComponents", directory: "./other_components", @@ -62,15 +71,7 @@ if (!global.view) { storyEntries: normalizedStories, }); } else { - const { importMap } = prepareStories({ storyEntries: normalizedStories }); - - global.view._preview.onStoriesChanged({ - importFn: async (importPath: string) => importMap[importPath], - }); - - global.view._preview.onGetProjectAnnotationsChanged({ - getProjectAnnotations: getProjectAnnotations(global.view, annotations), - }); + updateView(global.view, annotations, normalizedStories); } export const view = global.view; diff --git a/examples/expo-example/components/ActionExample/Actions.stories.tsx b/examples/expo-example/components/ActionExample/Actions.stories.tsx index 1588ec7c66..fa47de98d3 100644 --- a/examples/expo-example/components/ActionExample/Actions.stories.tsx +++ b/examples/expo-example/components/ActionExample/Actions.stories.tsx @@ -1,11 +1,12 @@ import type { Meta, StoryObj } from '@storybook/react'; + import { ActionButton } from './Actions'; const meta = { title: 'ActionButton', component: ActionButton, argTypes: { - onPress: { action: 'pressed the button' }, + onPress: { action: 'pressed' }, }, args: { text: 'Press me!', diff --git a/examples/expo-example/components/ActionExample/Actions.tsx b/examples/expo-example/components/ActionExample/Actions.tsx index 769acc6cc3..b26eab37c8 100644 --- a/examples/expo-example/components/ActionExample/Actions.tsx +++ b/examples/expo-example/components/ActionExample/Actions.tsx @@ -1,8 +1,7 @@ -import React from 'react'; import { TouchableOpacity, Text, StyleSheet } from 'react-native'; interface ActionButtonProps { - onPress: () => void; + onPress?: () => void; text: string; } diff --git a/examples/expo-example/metro.config.js b/examples/expo-example/metro.config.js index 5d452dc44c..60748590a4 100644 --- a/examples/expo-example/metro.config.js +++ b/examples/expo-example/metro.config.js @@ -1,44 +1,19 @@ // Learn more https://docs.expo.io/guides/customizing-metro const { getDefaultConfig } = require('expo/metro-config'); -const { mergeConfig } = require('metro-config'); const path = require('path'); const defaultConfig = getDefaultConfig(__dirname); -// const { writeRequires } = require('@storybook/react-native/scripts/loader'); const { generate } = require('@storybook/react-native/scripts/generate'); generate({ configPath: path.resolve(__dirname, './.storybook'), }); -// writeRequires({ -// configPath: path.resolve(__dirname, './.storybook'), -// unstable_useRequireContext: false, -// }); +defaultConfig.transformer.unstable_allowRequireContext = true; -module.exports = (async () => { - return mergeConfig(defaultConfig, { - resolver: { - // unstable_enablePackageExports: true, - disableHierarchicalLookup: true, - unstable_enableSymlinks: true, - resolveRequest: (context, moduleName, platform) => { - const defaultResolveResult = context.resolveRequest(context, moduleName, platform); +defaultConfig.watchFolders.push('../../packages/react-native-ui'); - if ( - process.env.STORYBOOK_ENABLED !== 'true' && - defaultResolveResult?.filePath?.includes?.('.storybook/') - ) { - return { - type: 'empty', - }; - } +// causing breakage :( +// defaultConfig.resolver.disableHierarchicalLookup = true; - return defaultResolveResult; - }, - }, - transformer: { - unstable_allowRequireContext: true, - }, - }); -})(); +module.exports = defaultConfig; diff --git a/examples/expo-example/package.json b/examples/expo-example/package.json index c50f56eafe..2cf58b443d 100644 --- a/examples/expo-example/package.json +++ b/examples/expo-example/package.json @@ -1,6 +1,6 @@ { "name": "expo-example", - "version": "8.0.0-alpha.2", + "version": "8.0.0-alpha.4", "private": true, "main": "index.js", "scripts": { @@ -19,36 +19,41 @@ "test:ci": "jest --runInBand" }, "dependencies": { - "@expo/metro-runtime": "^3.1.1", + "@expo/metro-runtime": "~3.1.3", + "@gorhom/bottom-sheet": "^4", "@react-native-async-storage/async-storage": "1.21.0", "@react-native-community/datetimepicker": "7.6.1", "@react-native-community/slider": "4.4.2", - "@storybook/addon-essentials": "8.0.0-rc.3", - "@storybook/addon-interactions": "8.0.0-rc.3", - "@storybook/addon-links": "8.0.0-rc.3", - "@storybook/addon-ondevice-actions": "^8.0.0-alpha.2", - "@storybook/addon-ondevice-backgrounds": "^8.0.0-alpha.2", - "@storybook/addon-ondevice-controls": "^8.0.0-alpha.2", - "@storybook/addon-ondevice-notes": "^8.0.0-alpha.2", + "@storybook/addon-essentials": "^8", + "@storybook/addon-interactions": "^8", + "@storybook/addon-links": "^8", + "@storybook/addon-ondevice-actions": "^8.0.0-alpha.4", + "@storybook/addon-ondevice-backgrounds": "^8.0.0-alpha.4", + "@storybook/addon-ondevice-controls": "^8.0.0-alpha.4", + "@storybook/addon-ondevice-notes": "^8.0.0-alpha.4", "@storybook/addon-react-native-server": "0.0.5", "@storybook/addon-react-native-web": "^0.0.22", - "@storybook/blocks": "8.0.0-rc.3", - "@storybook/builder-webpack5": "8.0.0-rc.3", - "@storybook/core-common": "8.0.0-rc.3", - "@storybook/docs-tools": "8.0.0-rc.3", + "@storybook/blocks": "^8", + "@storybook/builder-webpack5": "^8", + "@storybook/core-common": "^8", + "@storybook/docs-tools": "^8", "@storybook/global": "^5.0.0", - "@storybook/react": "8.0.0-rc.3", - "@storybook/react-native": "^8.0.0-alpha.2", - "@storybook/react-webpack5": "8.0.0-rc.3", - "@storybook/test": "8.0.0-rc.3", - "expo": "^50.0.2", + "@storybook/react": "^8", + "@storybook/react-native": "^8.0.0-alpha.4", + "@storybook/react-native-theming": "^8.0.0-alpha.4", + "@storybook/react-webpack5": "^8", + "@storybook/test": "^8", + "expo": "~50.0.14", "querystring": "^0.2.1", "react": "18.2.0", "react-dom": "18.2.0", - "react-native": "0.73.2", + "react-native": "0.73.6", + "react-native-gesture-handler": "~2.14.1", + "react-native-reanimated": "~3.6.2", "react-native-safe-area-context": "4.8.2", + "react-native-svg": "14.1.0", "react-native-web": "~0.19.6", - "storybook": "8.0.0-rc.3", + "storybook": "^8", "ws": "^8.16.0" }, "devDependencies": { @@ -61,10 +66,9 @@ "@types/ws": "^8.5.10", "babel-loader": "^9.1.3", "babel-plugin-react-docgen-typescript": "^1.5.1", - "jest": "29.7.0", - "jest-expo": "50.0.1", + "jest": "^29.7.0", + "jest-expo": "~50.0.4", "metro-react-native-babel-preset": "^0.77.0", - "ts-node": "^10.9.1", "typescript": "^5.3.3" }, "jest": { diff --git a/lerna.json b/lerna.json index f1e6f42dca..b644830763 100644 --- a/lerna.json +++ b/lerna.json @@ -1,5 +1,5 @@ { "npmClient": "yarn", "registry": "https://registry.npmjs.org", - "version": "8.0.0-alpha.2" + "version": "8.0.0-alpha.4" } diff --git a/package.json b/package.json index 5b785c78e4..2c92777cde 100644 --- a/package.json +++ b/package.json @@ -42,14 +42,13 @@ "publish:next": "lerna publish from-git --dist-tag next", "test:ci": "lerna run test:ci", "test": "lerna run test", - "check-mismatched-deps": "ts-node ./check-matching-deps.ts" + "check-mismatched-deps": "npx tsx ./check-matching-deps.ts" }, "devDependencies": { "@react-native/eslint-config": "^0.72.1", "cross-env": "^7.0.3", "eslint": "8.24.0", "lerna": "^8.1.2", - "ts-node": "^10.9.1", "typescript": "^5.3.3" }, "resolutions": { diff --git a/packages/ondevice-actions/package.json b/packages/ondevice-actions/package.json index 9ac0b6daef..95088e8da5 100644 --- a/packages/ondevice-actions/package.json +++ b/packages/ondevice-actions/package.json @@ -1,6 +1,6 @@ { "name": "@storybook/addon-ondevice-actions", - "version": "8.0.0-alpha.2", + "version": "8.0.0-alpha.4", "description": "Action Logger addon for react-native storybook", "keywords": [ "storybook" @@ -27,10 +27,10 @@ "prepare": "tsc" }, "dependencies": { - "@storybook/addon-actions": "8.0.0-rc.3", - "@storybook/core-events": "8.0.0-rc.3", + "@storybook/addon-actions": "^8", + "@storybook/core-events": "^8", "@storybook/global": "^5.0.0", - "@storybook/manager-api": "8.0.0-rc.3", + "@storybook/manager-api": "^8", "fast-deep-equal": "^2.0.1" }, "devDependencies": { diff --git a/packages/ondevice-backgrounds/package.json b/packages/ondevice-backgrounds/package.json index 97e07ecd35..469b215026 100644 --- a/packages/ondevice-backgrounds/package.json +++ b/packages/ondevice-backgrounds/package.json @@ -1,6 +1,6 @@ { "name": "@storybook/addon-ondevice-backgrounds", - "version": "8.0.0-alpha.2", + "version": "8.0.0-alpha.4", "description": "A react-native storybook addon to show different backgrounds for your preview", "keywords": [ "addon", @@ -32,9 +32,9 @@ "dev": "tsc --watch" }, "dependencies": { - "@storybook/manager-api": "8.0.0-rc.3", - "@storybook/preview-api": "8.0.0-rc.3", - "@storybook/react-native-theming": "^8.0.0-alpha.2" + "@storybook/manager-api": "^8", + "@storybook/preview-api": "^8", + "@storybook/react-native-theming": "^8.0.0-alpha.4" }, "devDependencies": { "typescript": "^5.3.3" diff --git a/packages/ondevice-controls/package.json b/packages/ondevice-controls/package.json index 87a835808a..0adf0bb74a 100644 --- a/packages/ondevice-controls/package.json +++ b/packages/ondevice-controls/package.json @@ -1,6 +1,6 @@ { "name": "@storybook/addon-ondevice-controls", - "version": "8.0.0-alpha.2", + "version": "8.0.0-alpha.4", "description": "Display storybook controls on your device.", "keywords": [ "addon", @@ -30,12 +30,12 @@ "copyimages": "cross-env-shell cp -r src/components/color-picker/resources dist/components/color-picker/resources" }, "dependencies": { - "@storybook/addon-controls": "8.0.0-rc.3", - "@storybook/channels": "8.0.0-rc.3", - "@storybook/client-logger": "8.0.0-rc.3", - "@storybook/core-events": "8.0.0-rc.3", - "@storybook/manager-api": "8.0.0-rc.3", - "@storybook/react-native-theming": "^8.0.0-alpha.2", + "@storybook/addon-controls": "^8", + "@storybook/channels": "^8", + "@storybook/client-logger": "^8", + "@storybook/core-events": "^8", + "@storybook/manager-api": "^8", + "@storybook/react-native-theming": "^8.0.0-alpha.4", "deep-equal": "^1.0.1", "prop-types": "^15.7.2", "react-native-modal-datetime-picker": "^14.0.0", @@ -47,6 +47,7 @@ "typescript": "^5.3.3" }, "peerDependencies": { + "@gorhom/bottom-sheet": ">=4", "@react-native-community/datetimepicker": "*", "@react-native-community/slider": "*", "react": "*", diff --git a/packages/ondevice-controls/src/types/Array.tsx b/packages/ondevice-controls/src/types/Array.tsx index 272be399da..dad0784350 100644 --- a/packages/ondevice-controls/src/types/Array.tsx +++ b/packages/ondevice-controls/src/types/Array.tsx @@ -3,8 +3,9 @@ import { styled } from '@storybook/react-native-theming'; import { inputStyle } from './common'; import { useResyncValue } from './useResyncValue'; +import { BottomSheetTextInput } from '@gorhom/bottom-sheet'; -const Input = styled.TextInput(({ theme }) => ({ +const Input = styled(BottomSheetTextInput)(({ theme }) => ({ ...inputStyle(theme), })); diff --git a/packages/ondevice-controls/src/types/Number.tsx b/packages/ondevice-controls/src/types/Number.tsx index 24445f0df7..2c1fd53f2c 100644 --- a/packages/ondevice-controls/src/types/Number.tsx +++ b/packages/ondevice-controls/src/types/Number.tsx @@ -5,8 +5,9 @@ import { View } from 'react-native'; import { inputStyle } from './common'; import { useResyncValue } from './useResyncValue'; +import { BottomSheetTextInput } from '@gorhom/bottom-sheet'; -const Input = styled.TextInput<{ showError: boolean }>(({ theme, showError }) => { +const Input = styled(BottomSheetTextInput)<{ showError: boolean }>(({ theme, showError }) => { const style = inputStyle(theme); return { ...style, diff --git a/packages/ondevice-controls/src/types/Object.tsx b/packages/ondevice-controls/src/types/Object.tsx index 691f02f4ef..14612564eb 100644 --- a/packages/ondevice-controls/src/types/Object.tsx +++ b/packages/ondevice-controls/src/types/Object.tsx @@ -4,6 +4,7 @@ import { ViewStyle } from 'react-native'; import { useResyncValue } from './useResyncValue'; import { inputStyle } from './common'; +import { BottomSheetTextInput } from '@gorhom/bottom-sheet'; export interface ObjectProps { arg: { @@ -14,7 +15,7 @@ export interface ObjectProps { isPristine: boolean; } -const Input = styled.TextInput(({ theme }) => ({ +const Input = styled(BottomSheetTextInput)(({ theme }) => ({ ...inputStyle(theme, false), minHeight: 60, })); diff --git a/packages/ondevice-controls/src/types/Text.tsx b/packages/ondevice-controls/src/types/Text.tsx index b261773bba..47bc9e820d 100644 --- a/packages/ondevice-controls/src/types/Text.tsx +++ b/packages/ondevice-controls/src/types/Text.tsx @@ -1,5 +1,6 @@ import React from 'react'; import { styled } from '@storybook/react-native-theming'; +import { BottomSheetTextInput } from '@gorhom/bottom-sheet'; import { inputStyle } from './common'; import { useResyncValue } from './useResyncValue'; @@ -14,7 +15,7 @@ export interface TextProps { isPristine: boolean; } -const Input = styled.TextInput(({ theme }) => ({ +const Input = styled(BottomSheetTextInput)(({ theme }) => ({ ...inputStyle(theme), })); diff --git a/packages/ondevice-notes/package.json b/packages/ondevice-notes/package.json index 2e09f04e34..910f46d077 100644 --- a/packages/ondevice-notes/package.json +++ b/packages/ondevice-notes/package.json @@ -1,6 +1,6 @@ { "name": "@storybook/addon-ondevice-notes", - "version": "8.0.0-alpha.2", + "version": "8.0.0-alpha.4", "description": "Write notes for your react-native Storybook stories.", "keywords": [ "addon", @@ -29,11 +29,11 @@ "dev": "tsc --watch" }, "dependencies": { - "@storybook/client-logger": "8.0.0-rc.3", - "@storybook/core-events": "8.0.0-rc.3", - "@storybook/manager-api": "8.0.0-rc.3", - "@storybook/react-native-theming": "^8.0.0-alpha.2", - "react-native-markdown-display": "6.1.6" + "@storybook/client-logger": "^8", + "@storybook/core-events": "^8", + "@storybook/manager-api": "^8", + "@storybook/react-native-theming": "^8.0.0-alpha.4", + "react-native-markdown-display": "^7.0.2" }, "devDependencies": { "typescript": "^5.3.3" diff --git a/packages/react-native-theming/package.json b/packages/react-native-theming/package.json index e76d1ac437..08d2a38767 100644 --- a/packages/react-native-theming/package.json +++ b/packages/react-native-theming/package.json @@ -1,6 +1,6 @@ { "name": "@storybook/react-native-theming", - "version": "8.0.0-alpha.2", + "version": "8.0.0-alpha.4", "description": "A wrapper library around emotion 11 to provide theming support for react-native storybook", "keywords": [ "react", @@ -21,13 +21,15 @@ "main": "dist/index.js", "types": "dist/index.d.ts", "scripts": { - "dev": "ts-node ./scripts/gendtsdev.ts && tsup --watch", - "prepare": "tsup && ts-node ./scripts/patchdts.ts" + "dev": "npx tsx ./scripts/gendtsdev.ts && tsup --watch", + "prepare": "tsup && npx tsx ./scripts/patchdts.ts" + }, + "dependencies": { + "polished": "^4.3.1" }, "devDependencies": { "@emotion/native": "^11.11.0", "@emotion/react": "^11.11.1", - "ts-node": "^10.9.1", "tsup": "^7.2.0" }, "peerDependencies": { diff --git a/packages/react-native-theming/scripts/patchdts.ts b/packages/react-native-theming/scripts/patchdts.ts index caaf0e3e51..386843eb00 100644 --- a/packages/react-native-theming/scripts/patchdts.ts +++ b/packages/react-native-theming/scripts/patchdts.ts @@ -1,8 +1,12 @@ async function run() { - const { appendFile } = await import('fs/promises'); + const { readFile, writeFile } = await import('fs/promises'); // add a string of text at the end of the dist/index.d.ts file - console.log('Appending to dist/index.d.ts'); - await appendFile('dist/index.d.ts', '\n export interface Theme extends StorybookTheme {}'); + console.log('writing to dist/index.d.ts'); + const contents = await readFile('dist/index.d.ts', 'utf-8'); + await writeFile( + 'dist/index.d.ts', + contents.replace('interface Theme {}', 'export interface Theme extends StorybookTheme {}') + ); } run().catch((e) => { diff --git a/packages/react-native-theming/src/emotionAugmentation.d.ts b/packages/react-native-theming/src/emotionAugmentation.d.ts index 5ea69e4efd..8cc15623b6 100644 --- a/packages/react-native-theming/src/emotionAugmentation.d.ts +++ b/packages/react-native-theming/src/emotionAugmentation.d.ts @@ -5,5 +5,6 @@ import '@emotion/react'; declare module '@emotion/react' { type StorybookTheme = import('./theme').StorybookTheme; + export interface Theme extends StorybookTheme {} } diff --git a/packages/react-native-theming/src/index.ts b/packages/react-native-theming/src/index.ts index 52b2c8d22b..58566d711d 100644 --- a/packages/react-native-theming/src/index.ts +++ b/packages/react-native-theming/src/index.ts @@ -2,7 +2,7 @@ export { theme, darkTheme, StorybookTheme } from './theme'; import styled, { type StyledComponent } from '@emotion/native'; -import { useTheme } from '@emotion/react'; +import { useTheme, withTheme } from '@emotion/react'; import { ThemeProvider } from '@emotion/react'; -export { styled, useTheme, ThemeProvider, StyledComponent }; +export { styled, useTheme, withTheme, ThemeProvider, StyledComponent }; diff --git a/packages/react-native-theming/src/newTheme.ts b/packages/react-native-theming/src/newTheme.ts new file mode 100644 index 0000000000..9731556f8f --- /dev/null +++ b/packages/react-native-theming/src/newTheme.ts @@ -0,0 +1,242 @@ +import type { TextStyle } from 'react-native'; +import { transparentize } from 'polished'; + +export const color = { + // Official color palette + primary: '#FF4785', // coral + secondary: '#029CFD', // ocean + secondaryLighter: 'rgba(2, 157, 253, 0.9)', // ocean + tertiary: '#FAFBFC', + ancillary: '#22a699', + + // Complimentary + orange: '#FC521F', + gold: '#FFAE00', + green: '#66BF3C', + seafoam: '#37D5D3', + purple: '#6F2CAC', + ultraviolet: '#2A0481', + + // Monochrome + lightest: '#FFFFFF', + lighter: '#F7FAFC', + light: '#EEF3F6', + mediumlight: '#ECF4F9', + medium: '#D9E8F2', + mediumdark: '#73828C', + dark: '#5C6870', + darker: '#454E54', + darkest: '#2E3438', + + // For borders + border: 'hsla(203, 50%, 30%, 0.15)', + + // Status + positive: '#66BF3C', + negative: '#FF4400', + warning: '#E69D00', + critical: '#FFFFFF', + + // Text + defaultText: '#2E3438', + inverseText: '#FFFFFF', + positiveText: '#448028', + negativeText: '#D43900', + warningText: '#A15C20', +}; + +export const background = { + app: '#F6F9FC', + bar: color.lightest, + content: color.lightest, + preview: color.lightest, + gridCellSize: 10, + hoverable: transparentize(0.9, color.secondary), // hover state for items in a list + + // Notification, error, and warning backgrounds + positive: '#E1FFD4', + negative: '#FEDED2', + warning: '#FFF5CF', + critical: '#FF4400', +}; + +export const typography = { + weight: { + regular: '400' as TextStyle['fontWeight'], + bold: '700' as TextStyle['fontWeight'], + }, + size: { + s1: 12, + s2: 14, + s3: 16, + m1: 20, + m2: 24, + m3: 28, + l1: 32, + l2: 40, + l3: 48, + code: 90, + }, +}; + +export interface ThemeVars extends ThemeVarsBase, ThemeVarsColors {} + +export interface ThemeVarsPartial extends ThemeVarsBase, Partial {} + +interface ThemeVarsBase { + base: 'light' | 'dark'; +} + +export const light: ThemeVars = { + base: 'light', + + // Storybook-specific color palette + colorPrimary: '#FF4785', // coral + colorSecondary: '#029CFD', // ocean + + // UI + appBg: background.app, + appContentBg: color.lightest, + appPreviewBg: color.lightest, + appBorderColor: color.border, + appBorderRadius: 4, + + // Fonts + + // Text colors + textColor: color.darkest, + textInverseColor: color.lightest, + textMutedColor: color.dark, + + // Toolbar default and active colors + barTextColor: color.mediumdark, + barHoverColor: color.secondary, + barSelectedColor: color.secondary, + barBg: color.lightest, + + // Form colors + buttonBg: background.app, + buttonBorder: color.medium, + booleanBg: color.mediumlight, + booleanSelectedBg: color.lightest, + inputBg: color.lightest, + inputBorder: color.border, + inputTextColor: color.darkest, + inputBorderRadius: 4, +}; + +export interface ThemeVarsColors { + colorPrimary: string; + colorSecondary: string; + + // UI + appBg: string; + appContentBg: string; + appPreviewBg: string; + appBorderColor: string; + appBorderRadius: number; + + // Text colors + textColor: string; + textInverseColor: string; + textMutedColor: string; + + // Toolbar default and active colors + barTextColor: string; + barHoverColor: string; + barSelectedColor: string; + barBg: string; + + // Form colors + buttonBg: string; + buttonBorder: string; + booleanBg: string; + booleanSelectedBg: string; + inputBg: string; + inputBorder: string; + inputTextColor: string; + inputBorderRadius: number; + + brandTitle?: string; + brandUrl?: string; + brandImage?: string; + brandTarget?: string; + + gridCellSize?: number; +} + +export type Color = typeof color; +export type Background = typeof background; +export type Typography = typeof typography; + +export type TextSize = number | string; +export interface Brand { + title: string | undefined; + url: string | null | undefined; + image: string | null | undefined; + target: string | null | undefined; +} + +export interface StorybookThemeWeb { + color: Color; + background: Background; + typography: Typography; + + input: { + border: string; + background: string; + color: string; + borderRadius: number; + }; + + // UI + layoutMargin: number; + appBorderColor: string; + appBorderRadius: number; + + // Toolbar default/active colors + barTextColor: string; + barHoverColor: string; + barSelectedColor: string; + barBg: string; + + brand: Brand; + + [key: string]: any; +} + +export const dark: ThemeVars = { + base: 'dark', + + // Storybook-specific color palette + colorPrimary: '#FF4785', // coral + colorSecondary: '#029CFD', // ocean + + // UI + appBg: '#222425', + appContentBg: '#1B1C1D', + appPreviewBg: color.lightest, + appBorderColor: 'rgba(255,255,255,.1)', + appBorderRadius: 4, + + // Text colors + textColor: '#C9CDCF', + textInverseColor: '#222425', + textMutedColor: '#798186', + + // Toolbar default and active colors + barTextColor: '#798186', + barHoverColor: color.secondary, + barSelectedColor: color.secondary, + barBg: '#292C2E', + + // Form colors + buttonBg: '#222425', + buttonBorder: 'rgba(255,255,255,.1)', + booleanBg: '#222425', + booleanSelectedBg: '#2E3438', + inputBg: '#1B1C1D', + inputBorder: 'rgba(255,255,255,.1)', + inputTextColor: color.lightest, + inputBorderRadius: 4, +}; diff --git a/packages/react-native-theming/src/theme.ts b/packages/react-native-theming/src/theme.ts index cc0a8ccbc8..9e76b92b38 100644 --- a/packages/react-native-theming/src/theme.ts +++ b/packages/react-native-theming/src/theme.ts @@ -1,4 +1,5 @@ -import { ShadowStyleIOS, ViewStyle, TextStyle } from 'react-native'; +import type { ShadowStyleIOS, ViewStyle, TextStyle } from 'react-native'; +import { StorybookThemeWeb, background, color, light, dark } from './newTheme'; type ShadowStyle = ShadowStyleIOS | Pick; @@ -60,7 +61,7 @@ interface ThemeButton { borderRadius: number; } -export interface StorybookTheme { +export interface StorybookTheme extends StorybookThemeWeb { tokens: ThemeTokens; backgroundColor: string; text: { @@ -248,8 +249,123 @@ const textOnDark: StorybookTheme['text'] = { secondaryColor: tokens.color.grey200, linkColor: tokens.color.blue600, }; +export const typography = { + weight: { + regular: '400' as TextStyle['fontWeight'], + bold: '700' as TextStyle['fontWeight'], + }, + size: { + s1: 12, + s2: 14, + s3: 16, + m1: 20, + m2: 24, + m3: 28, + l1: 32, + l2: 40, + l3: 48, + code: 90, + }, +} as const; export const theme: StorybookTheme = { + base: 'light', + textMutedColor: color.dark, + color: { + primary: light.colorPrimary, + secondary: light.colorSecondary, + secondaryLighter: color.secondaryLighter, + tertiary: color.tertiary, + ancillary: color.ancillary, + + // Complimentary + orange: color.orange, + gold: color.gold, + green: color.green, + seafoam: color.seafoam, + purple: color.purple, + ultraviolet: color.ultraviolet, + + // Monochrome + lightest: color.lightest, + lighter: color.lighter, + light: color.light, + mediumlight: color.mediumlight, + medium: color.medium, + mediumdark: color.mediumdark, + dark: color.dark, + darker: color.darker, + darkest: color.darkest, + + // For borders + border: color.border, + + // Status + positive: color.positive, + negative: color.negative, + warning: color.warning, + critical: color.critical, + + defaultText: light.textColor || color.darkest, + inverseText: light.textInverseColor || color.lightest, + positiveText: color.positiveText, + negativeText: color.negativeText, + warningText: color.warningText, + }, + background: { + app: light.appBg, + bar: light.barBg, + content: light.appContentBg, + preview: light.appPreviewBg, + gridCellSize: light.gridCellSize || background.gridCellSize, + hoverable: background.hoverable, + positive: background.positive, + negative: background.negative, + warning: background.warning, + critical: background.critical, + }, + typography: { + weight: typography.weight, + size: typography.size, + }, + + input: { + background: light.inputBg, + border: light.inputBorder, + borderRadius: light.inputBorderRadius, + color: light.inputTextColor, + }, + + button$: { + background: light.buttonBg || light.inputBg, + border: light.buttonBorder || light.inputBorder, + }, + + boolean: { + background: light.booleanBg || light.inputBorder, + selectedBackground: light.booleanSelectedBg || light.inputBg, + }, + + // UI + layoutMargin: 10, + appBorderColor: light.appBorderColor, + appBorderRadius: light.appBorderRadius, + + // Toolbar default/active colors + barTextColor: light.barTextColor, + barHoverColor: light.barHoverColor || light.colorSecondary, + barSelectedColor: light.barSelectedColor || light.colorSecondary, + barBg: light.barBg, + + // Brand logo/text + brand: { + title: light.brandTitle, + url: light.brandUrl, + image: light.brandImage || (light.brandTitle ? null : undefined), + target: light.brandTarget, + }, + + // ONDEVICE tokens, text, backgroundColor: tokens.color.white, @@ -382,6 +498,103 @@ export const theme: StorybookTheme = { }; export const darkTheme: StorybookTheme = { + base: 'dark', + textMutedColor: '#798186', + color: { + primary: dark.colorPrimary, + secondary: dark.colorSecondary, + secondaryLighter: color.secondaryLighter, + tertiary: color.tertiary, + ancillary: color.ancillary, + + // Complimentary + orange: color.orange, + gold: color.gold, + green: color.green, + seafoam: color.seafoam, + purple: color.purple, + ultraviolet: color.ultraviolet, + + // Monochrome + lightest: color.lightest, + lighter: color.lighter, + light: color.light, + mediumlight: color.mediumlight, + medium: color.medium, + mediumdark: color.mediumdark, + dark: color.dark, + darker: color.darker, + darkest: color.darkest, + + // For borders + border: color.border, + + // Status + positive: color.positive, + negative: color.negative, + warning: color.warning, + critical: color.critical, + + defaultText: dark.textColor || color.darkest, + inverseText: dark.textInverseColor || color.lightest, + positiveText: color.positiveText, + negativeText: color.negativeText, + warningText: color.warningText, + }, + background: { + app: dark.appBg, + bar: dark.barBg, + content: dark.appContentBg, + preview: dark.appPreviewBg, + gridCellSize: dark.gridCellSize || background.gridCellSize, + hoverable: background.hoverable, + positive: background.positive, + negative: background.negative, + warning: background.warning, + critical: background.critical, + }, + typography: { + weight: typography.weight, + size: typography.size, + }, + + input: { + background: dark.inputBg, + border: dark.inputBorder, + borderRadius: dark.inputBorderRadius, + color: dark.inputTextColor, + }, + + button$: { + background: dark.buttonBg || dark.inputBg, + border: dark.buttonBorder || dark.inputBorder, + }, + + boolean: { + background: dark.booleanBg || dark.inputBorder, + selectedBackground: dark.booleanSelectedBg || dark.inputBg, + }, + + // UI + layoutMargin: 10, + appBorderColor: dark.appBorderColor, + appBorderRadius: dark.appBorderRadius, + + // Toolbar default/active colors + barTextColor: dark.barTextColor, + barHoverColor: dark.barHoverColor || dark.colorSecondary, + barSelectedColor: dark.barSelectedColor || dark.colorSecondary, + barBg: dark.barBg, + + // Brand logo/text + brand: { + title: dark.brandTitle, + url: dark.brandUrl, + image: dark.brandImage || (dark.brandTitle ? null : undefined), + target: dark.brandTarget, + }, + + // ondevice tokens, text: textOnDark, backgroundColor: tokens.color.offBlack, diff --git a/packages/react-native-ui/package.json b/packages/react-native-ui/package.json new file mode 100644 index 0000000000..d4bed2caab --- /dev/null +++ b/packages/react-native-ui/package.json @@ -0,0 +1,84 @@ +{ + "name": "@storybook/react-native-ui", + "version": "8.0.0-alpha.4", + "description": "ui components for react native storybook", + "keywords": [ + "react", + "react-native", + "storybook" + ], + "homepage": "https://storybook.js.org/", + "bugs": { + "url": "https://github.com/storybookjs/react-native/issues" + }, + "repository": { + "type": "git", + "url": "https://github.com/storybookjs/react-native.git", + "directory": "packages/react-native-ui" + }, + "react-native": "src/index.tsx", + "main": "dist/index.js", + "types": "src/index.tsx", + "license": "MIT", + "files": [ + "dist/**/*", + "README.md", + "*.js", + "*.d.ts", + "src/**/*" + ], + "scripts": { + "dev": "tsup --watch", + "prepare": "tsup", + "test": "jest --passWithNoTests", + "test:ci": "jest" + }, + "jest": { + "modulePathIgnorePatterns": [ + "dist/" + ], + "moduleFileExtensions": [ + "ts", + "tsx", + "js", + "jsx", + "json", + "node" + ], + "preset": "react-native" + }, + "devDependencies": { + "@storybook/react": "^8", + "@storybook/types": "^8", + "@types/jest": "^29.4.3", + "@types/react": "~18.2.14", + "babel-jest": "^29.4.3", + "jest": "^29.7.0", + "react-test-renderer": "18.2.0", + "tsup": "^7.2.0", + "typescript": "^5.3.3" + }, + "dependencies": { + "@storybook/core-events": "^8", + "@storybook/manager-api": "^8", + "@storybook/react-native-theming": "^8.0.0-alpha.4", + "fuse.js": "^7.0.0", + "memoizerific": "^1.11.3", + "polished": "^4.3.1" + }, + "peerDependencies": { + "@gorhom/bottom-sheet": ">=4", + "react": "*", + "react-native": ">=0.57.0", + "react-native-gesture-handler": ">=2", + "react-native-reanimated": ">=3", + "react-native-safe-area-context": "*", + "react-native-svg": ">=14" + }, + "engines": { + "node": ">=18.0.0" + }, + "publishConfig": { + "access": "public" + } +} diff --git a/packages/react-native-ui/src/Button.stories.tsx b/packages/react-native-ui/src/Button.stories.tsx new file mode 100644 index 0000000000..9a57d2d4e8 --- /dev/null +++ b/packages/react-native-ui/src/Button.stories.tsx @@ -0,0 +1,134 @@ +import type { Meta, StoryObj } from '@storybook/react'; +import type { ReactNode } from 'react'; + +import { View } from 'react-native'; + +import { Button } from './Button'; +import { FaceHappyIcon } from './icon/FaceHappyIcon'; + +const meta = { + title: 'UI/Button', + component: Button, + args: {}, +} satisfies Meta; + +export default meta; +type Story = StoryObj; + +const Stack = ({ children }: { children: ReactNode }) => ( + {children} +); + +const Row = ({ children }: { children: ReactNode }) => ( + + {children} + +); + +export const Base: Story = {}; + +export const Variants: Story = { + render: (args) => ( + + + + + addonPanelRef.current.setAddonsPanelOpen(true)} + Icon={BottomBarToggleIcon} + /> + + + + ); +}; + +const Nav = styled.View({ + display: 'flex', + flexDirection: 'row', + alignItems: 'center', + justifyContent: 'space-between', + width: '100%', + height: 40, + paddingHorizontal: 12, +}); + +const Container = styled.View(({ theme }) => ({ + alignSelf: 'flex-end', + width: '100%', + zIndex: 10, + background: theme.barBg, + borderTopColor: theme.appBorderColor, + borderTopWidth: 1, +})); + +const Button = styled.TouchableOpacity(({ theme }) => ({ + display: 'flex', + flexDirection: 'row', + alignItems: 'center', + gap: 10, + color: theme.color.mediumdark, + fontSize: theme.typography.size?.s2 - 1, + paddingHorizontal: 7, + fontWeight: theme.typography.weight.bold, +})); diff --git a/packages/react-native-ui/src/LayoutProvider.tsx b/packages/react-native-ui/src/LayoutProvider.tsx new file mode 100644 index 0000000000..c94c86d9ce --- /dev/null +++ b/packages/react-native-ui/src/LayoutProvider.tsx @@ -0,0 +1,90 @@ +import type { FC, PropsWithChildren } from 'react'; +import React, { createContext, useCallback, useContext, useMemo, useRef } from 'react'; +import { BREAKPOINT } from './constants'; +import { useWindowDimensions } from 'react-native'; +// import { +// BottomSheetModal, +// BottomSheetModalProvider, +// BottomSheetScrollView, +// } from '@gorhom/bottom-sheet'; + +type LayoutContextType = { + // isMobileMenuOpen: boolean; + openMobileMenu: () => void; + closeMobileMenu: () => void; + // isMobileAboutOpen: boolean; + // setMobileAboutOpen: React.Dispatch>; + // isMobilePanelOpen: boolean; + // setMobilePanelOpen: React.Dispatch>; + isDesktop: boolean; + isMobile: boolean; +}; + +const LayoutContext = createContext({ + // isMobileMenuOpen: false, + + openMobileMenu: () => {}, + closeMobileMenu: () => {}, + // isMobileAboutOpen: false, + // setMobileAboutOpen: () => {}, + // isMobilePanelOpen: false, + // setMobilePanelOpen: () => {}, + + isDesktop: false, + isMobile: false, +}); + +export const LayoutProvider: FC = ({ children }) => { + // const [isMobileMenuOpen, setMobileMenuOpen] = useState(false); + // const [isMobileAboutOpen, setMobileAboutOpen] = useState(false); + // const [isMobilePanelOpen, setMobilePanelOpen] = useState(false); + const { width } = useWindowDimensions(); + const isDesktop = width >= BREAKPOINT; + const isMobile = !isDesktop; + // const bottomSheetModalRef = useRef(null); + + const openMobileMenu = useCallback(() => { + // bottomSheetModalRef.current?.present(); + }, []); + const closeMobileMenu = useCallback(() => { + // bottomSheetModalRef.current?.dismiss(); + }, []); + + const contextValue = useMemo( + () => ({ + // isMobileMenuOpen, + openMobileMenu, + closeMobileMenu, + // isMobileAboutOpen, + // setMobileAboutOpen, + // isMobilePanelOpen, + // setMobilePanelOpen, + isDesktop, + isMobile, + }), + [ + // isMobileMenuOpen, + openMobileMenu, + closeMobileMenu, + // // isMobileAboutOpen, + // setMobileAboutOpen, + // isMobilePanelOpen, + // setMobilePanelOpen, + isDesktop, + isMobile, + ] + ); + + return ( + // + <> + {children} + {/* + bla + */} + + // + ); +}; + +export const useLayout = () => useContext(LayoutContext); diff --git a/packages/react-native-ui/src/MobileAddonsPanel.tsx b/packages/react-native-ui/src/MobileAddonsPanel.tsx new file mode 100644 index 0000000000..2404725bda --- /dev/null +++ b/packages/react-native-ui/src/MobileAddonsPanel.tsx @@ -0,0 +1,164 @@ +import { BottomSheetModal, BottomSheetScrollView } from '@gorhom/bottom-sheet'; +import { addons } from '@storybook/manager-api'; +import { styled } from '@storybook/react-native-theming'; +import { Addon_TypesEnum } from '@storybook/types'; +import { forwardRef, useImperativeHandle, useRef, useState } from 'react'; +import { Text, View } from 'react-native'; +import { ScrollView } from 'react-native-gesture-handler'; +import { useReducedMotion } from 'react-native-reanimated'; +import { useSafeAreaInsets } from 'react-native-safe-area-context'; + +import { IconButton } from './IconButton'; +import { CloseIcon } from './icon/CloseIcon'; + +export interface MobileAddonsPanelRef { + isAddonsPanelOpen: boolean; + setAddonsPanelOpen: (isOpen: boolean) => void; +} + +export const MobileAddonsPanel = forwardRef( + ({ storyId }, ref) => { + const [isAddonsPanelOpen, setAddonsPanelOpen] = useState(false); + const reducedMotion = useReducedMotion(); + + const addonsPanelBottomSheetRef = useRef(null); + const insets = useSafeAreaInsets(); + + const panels = addons.getElements(Addon_TypesEnum.PANEL); + const [addonSelected, setAddonSelected] = useState(Object.keys(panels)[0]); + + useImperativeHandle(ref, () => ({ + isAddonsPanelOpen, + setAddonsPanelOpen: (open: boolean) => { + if (open) { + setAddonsPanelOpen(true); + addonsPanelBottomSheetRef.current?.present(); + } else { + setAddonsPanelOpen(false); + addonsPanelBottomSheetRef.current?.dismiss(); + } + }, + })); + + return ( + { + setAddonsPanelOpen(false); + }} + snapPoints={['25%', '50%']} + style={{ + paddingTop: 8, + }} + containerStyle={{}} + backgroundStyle={{ + borderRadius: 0, + borderTopColor: 'lightgrey', + borderTopWidth: 1, + }} + keyboardBehavior="interactive" + keyboardBlurBehavior="restore" + enableDismissOnClose + enableHandlePanningGesture={true} + enableContentPanningGesture={true} + stackBehavior="replace" + > + + + + {Object.values(panels).map(({ id, title }) => { + const resolvedTitle = typeof title === 'function' ? title({}) : title; + + return ( + setAddonSelected(id)} + text={resolvedTitle} + /> + ); + })} + + { + setAddonsPanelOpen(false); + addonsPanelBottomSheetRef.current?.dismiss(); + }} + /> + + + {(() => { + if (!storyId) { + return ( + + No Story Selected + + ); + } + + if (Object.keys(panels).length === 0) { + return ( + + No addons loaded. + + ); + } + + return panels[addonSelected].render({ active: true }); + })()} + + + + ); + } +); + +const Tab = ({ active, onPress, text }: { active: boolean; onPress: () => void; text: string }) => { + return ( + + {text} + + ); +}; + +const TabButton = styled.TouchableOpacity<{ active: boolean }>(({ theme, active }) => ({ + borderBottomWidth: active ? 2 : 0, + borderBottomColor: active ? theme.barSelectedColor : undefined, + + overflow: 'hidden', + paddingHorizontal: 15, + justifyContent: 'center', + alignItems: 'center', +})); + +const TabText = styled.Text<{ active: boolean }>(({ theme, active }) => ({ + color: active ? theme.barSelectedColor : theme.color.mediumdark, + textAlign: 'center', + fontWeight: 'bold', + fontSize: 13, + lineHeight: 12, +})); diff --git a/packages/react-native-ui/src/MobileMenuDrawer.tsx b/packages/react-native-ui/src/MobileMenuDrawer.tsx new file mode 100644 index 0000000000..b39ca5f192 --- /dev/null +++ b/packages/react-native-ui/src/MobileMenuDrawer.tsx @@ -0,0 +1,82 @@ +import { + BottomSheetBackdropProps, + BottomSheetModal, + BottomSheetScrollView, + BottomSheetBackdrop, +} from '@gorhom/bottom-sheet'; +import { ReactNode, forwardRef, useImperativeHandle, useRef, useState } from 'react'; +import { Keyboard } from 'react-native'; +import { useReducedMotion } from 'react-native-reanimated'; +import { useSafeAreaInsets } from 'react-native-safe-area-context'; + +interface MobileMenuDrawerProps { + children: ReactNode | ReactNode[]; +} + +export interface MobileMenuDrawerRef { + isMobileMenuOpen: boolean; + setMobileMenuOpen: (isOpen: boolean) => void; +} + +export const BottomSheetBackdropComponent = (backdropComponentProps: BottomSheetBackdropProps) => ( + +); + +export const MobileMenuDrawer = forwardRef( + ({ children }, ref) => { + const [isMobileMenuOpen, setMobileMenuOpen] = useState(false); + const reducedMotion = useReducedMotion(); + const insets = useSafeAreaInsets(); + + const menuBottomSheetRef = useRef(null); + + useImperativeHandle(ref, () => ({ + isMobileMenuOpen, + setMobileMenuOpen: (open: boolean) => { + if (open) { + setMobileMenuOpen(true); + + menuBottomSheetRef.current?.present(); + } else { + Keyboard.dismiss(); + setMobileMenuOpen(false); + menuBottomSheetRef.current?.dismiss(); + } + }, + })); + + return ( + { + setMobileMenuOpen(false); + }} + snapPoints={['50%', '75%']} + enableDismissOnClose + enableHandlePanningGesture + enableContentPanningGesture + keyboardBehavior="extend" + keyboardBlurBehavior="restore" + stackBehavior="replace" + backdropComponent={BottomSheetBackdropComponent} + > + + {children} + + + ); + } +); diff --git a/packages/react-native-ui/src/Refs.tsx b/packages/react-native-ui/src/Refs.tsx new file mode 100644 index 0000000000..b5075c7a47 --- /dev/null +++ b/packages/react-native-ui/src/Refs.tsx @@ -0,0 +1,82 @@ +import type { FC } from 'react'; +import React, { useMemo, useCallback, useEffect, useState } from 'react'; +import type { State } from '@storybook/manager-api'; +import { styled } from '@storybook/react-native-theming'; +import { Tree } from './Tree'; +import type { RefType } from './types'; +import { getStateType } from './util/tree'; + +export interface RefProps { + isLoading: boolean; + isBrowsing: boolean; + selectedStoryId: string | null; + setSelection: (selection: { refId: string; storyId: string }) => void; +} + +const Wrapper = styled.View<{ isMain: boolean }>(({}) => ({ + position: 'relative', +})); + +export const Ref: FC = React.memo(function Ref( + props +) { + const { + index, + id: refId, + title = refId, + isLoading: isLoadingMain, + isBrowsing, + selectedStoryId, + loginUrl, + type, + expanded = true, + indexError, + previewInitialized, + setSelection, + } = props; + const length = useMemo(() => (index ? Object.keys(index).length : 0), [index]); + + const isLoadingInjected = + (type === 'auto-inject' && !previewInitialized) || type === 'server-checked'; + const isLoading = isLoadingMain || isLoadingInjected || type === 'unknown'; + const isError = !!indexError; + const isEmpty = !isLoading && length === 0; + const isAuthRequired = !!loginUrl && length === 0; + + const state = getStateType(isLoading, isAuthRequired, isError, isEmpty); + const [isExpanded, setExpanded] = useState(expanded); + + useEffect(() => { + if (index && selectedStoryId && index[selectedStoryId]) { + setExpanded(true); + } + }, [setExpanded, index, selectedStoryId]); + + const onSelectStoryId = useCallback( + (storyId: string) => { + setSelection({ refId, storyId }); + }, + [refId, setSelection] + ); + + return ( + <> + {isExpanded && ( + + {state === 'ready' && ( + + )} + + )} + + ); +}); diff --git a/packages/react-native-ui/src/Search.tsx b/packages/react-native-ui/src/Search.tsx new file mode 100644 index 0000000000..ce75f8c1eb --- /dev/null +++ b/packages/react-native-ui/src/Search.tsx @@ -0,0 +1,243 @@ +import { styled } from '@storybook/react-native-theming'; +import type { IFuseOptions } from 'fuse.js'; +import Fuse from 'fuse.js'; +import React, { useRef, useState, useCallback } from 'react'; +import { + type CombinedDataset, + type SearchItem, + type SearchResult, + type SearchChildrenFn, + type Selection, + type GetSearchItemProps, + isExpandType, +} from './types'; +import { searchItem } from './util/tree'; +import { getGroupStatus, getHighestStatus } from './util/status'; +import { SearchIcon } from './icon/SearchIcon'; +import { CloseIcon } from './icon/CloseIcon'; +import { TextInput, View } from 'react-native'; +import { BottomSheetTextInput } from '@gorhom/bottom-sheet'; + +const DEFAULT_MAX_SEARCH_RESULTS = 50; + +const options = { + shouldSort: true, + tokenize: true, + findAllMatches: true, + includeScore: true, + includeMatches: true, + threshold: 0.2, + location: 0, + distance: 100, + maxPatternLength: 32, + minMatchCharLength: 1, + keys: [ + { name: 'name', weight: 0.7 }, + { name: 'path', weight: 0.3 }, + ], +} as IFuseOptions; + +const SearchIconWrapper = styled.View(({ theme }) => ({ + position: 'absolute', + top: 0, + left: 8, + zIndex: 1, + pointerEvents: 'none', + color: theme.textMutedColor, + display: 'flex', + flexDirection: 'row', + alignItems: 'center', + height: '100%', +})); + +const SearchField = styled.View({ + display: 'flex', + flexDirection: 'column', + position: 'relative', +}); + +const Input = styled(BottomSheetTextInput)(({ theme }) => ({ + height: 32, + paddingLeft: 28, + paddingRight: 28, + borderWidth: 1, + borderColor: theme.appBorderColor, + backgroundColor: 'transparent', + borderRadius: 4, + fontSize: theme.typography.size.s1 + 1, + color: theme.color.defaultText, + width: '100%', +})); + +const ClearIcon = styled.TouchableOpacity(({ theme }) => ({ + position: 'absolute', + top: 0, + bottom: 0, + right: 8, + zIndex: 1, + color: theme.textMutedColor, + cursor: 'pointer', + display: 'flex', + alignItems: 'center', + justifyContent: 'center', + height: '100%', +})); + +export const Search = React.memo<{ + children: SearchChildrenFn; + dataset: CombinedDataset; + setSelection: (selection: Selection) => void; + getLastViewed: () => Selection[]; + initialQuery?: string; +}>(function Search({ children, dataset, setSelection, getLastViewed, initialQuery = '' }) { + const inputRef = useRef(null); + const [inputValue, setInputValue] = useState(initialQuery); + const [isOpen, setIsOpen] = useState(false); + const [allComponents, showAllComponents] = useState(false); + + const selectStory = useCallback( + (id: string, refId: string) => { + setSelection({ storyId: id, refId }); + inputRef.current?.blur(); + + showAllComponents(false); + }, + [setSelection] + ); + + const getItemProps: GetSearchItemProps = useCallback( + ({ item: result }) => { + return { + icon: result?.item?.type === 'component' ? 'component' : 'story', + result, + onPress: () => { + if (result?.item?.type === 'story') { + selectStory(result.item.id, result.item.refId); + } else if (result?.item?.type === 'component') { + selectStory(result.item.children[0], result.item.refId); + } else if (isExpandType(result) && result.showAll) { + result.showAll(); + } + }, + score: result.score, + refIndex: result.refIndex, + item: result.item, + matches: result.matches, + isHighlighted: false, + }; + }, + [selectStory] + ); + + const makeFuse = useCallback(() => { + const list = dataset.entries.reduce((acc, [refId, { index, status }]) => { + const groupStatus = getGroupStatus(index || {}, status); + + if (index) { + acc.push( + ...Object.values(index).map((item) => { + const statusValue = + status && status[item.id] + ? getHighestStatus(Object.values(status[item.id] || {}).map((s) => s.status)) + : null; + return { + ...searchItem(item, dataset.hash[refId]), + status: statusValue || groupStatus[item.id] || null, + }; + }) + ); + } + return acc; + }, []); + return new Fuse(list, options); + }, [dataset]); + + const getResults = useCallback( + (input: string) => { + const fuse = makeFuse(); + if (!input) return []; + + let results = []; + const resultIds: Set = new Set(); + const distinctResults = (fuse.search(input) as SearchResult[]).filter(({ item }) => { + if ( + !(item.type === 'component' || item.type === 'docs' || item.type === 'story') || + resultIds.has(item.parent) + ) { + return false; + } + resultIds.add(item.id); + return true; + }); + + if (distinctResults.length) { + results = distinctResults.slice(0, allComponents ? 1000 : DEFAULT_MAX_SEARCH_RESULTS); + if (distinctResults.length > DEFAULT_MAX_SEARCH_RESULTS && !allComponents) { + results.push({ + showAll: () => showAllComponents(true), + totalCount: distinctResults.length, + moreCount: distinctResults.length - DEFAULT_MAX_SEARCH_RESULTS, + }); + } + } + + const lastViewed = !input && getLastViewed(); + if (lastViewed && lastViewed.length) { + results = lastViewed.reduce((acc, { storyId, refId }) => { + const data = dataset.hash[refId]; + if (data && data.index && data.index[storyId]) { + const story = data.index[storyId]; + const item = story.type === 'story' ? data.index[story.parent] : story; + // prevent duplicates + if (!acc.some((res) => res.item.refId === refId && res.item.id === item.id)) { + acc.push({ item: searchItem(item, dataset.hash[refId]), matches: [], score: 0 }); + } + } + return acc; + }, []); + } + + return results; + }, + [allComponents, dataset.hash, getLastViewed, makeFuse] + ); + + const input = inputValue ? inputValue.trim() : ''; + const results = input ? getResults(input) : []; + + return ( + + + + + + + setIsOpen(true)} + onBlur={() => setIsOpen(false)} + /> + {isOpen && ( + { + setInputValue(''); + inputRef.current.clear(); + }} + > + + + )} + + + {children({ + query: input, + results, + isBrowsing: !isOpen || !inputValue.length, + closeMenu: () => {}, + getItemProps, + highlightedIndex: null, + })} + + ); +}); diff --git a/packages/react-native-ui/src/SearchResults.tsx b/packages/react-native-ui/src/SearchResults.tsx new file mode 100644 index 0000000000..8001a1b6ee --- /dev/null +++ b/packages/react-native-ui/src/SearchResults.tsx @@ -0,0 +1,245 @@ +import { styled } from '@storybook/react-native-theming'; +import type { FC, PropsWithChildren, ReactNode } from 'react'; +import React, { useCallback } from 'react'; +import { transparentize } from 'polished'; +import type { GetSearchItemProps, SearchResult, SearchResultProps } from './types'; +import { isExpandType } from './types'; + +import { FuseResultMatch } from 'fuse.js'; +import { PressableProps, Text, View } from 'react-native'; +import { Button } from './Button'; +import { IconButton } from './IconButton'; +import { ComponentIcon } from './icon/ComponentIcon'; +import { StoryIcon } from './icon/StoryIcon'; +import { statusMapping } from './util/status'; + +const ResultsList = styled.View({ + margin: 0, + padding: 0, + marginTop: 8, +}); + +const ResultRow = styled.TouchableOpacity<{ isHighlighted: boolean }>( + ({ theme, isHighlighted }) => ({ + width: '100%', + border: 'none', + cursor: 'pointer', + display: 'flex', + flexDirection: 'row', + alignItems: 'flex-start', + textAlign: 'left', + color: 'inherit', + fontSize: theme.typography.size.s2, + backgroundColor: isHighlighted ? theme.background.hoverable : 'transparent', + minHeight: 28, + borderRadius: 4, + gap: 6, + paddingTop: 7, + paddingBottom: 7, + paddingLeft: 8, + paddingRight: 8, + + '&:hover, &:focus': { + backgroundColor: transparentize(0.93, theme.color.secondary), + outline: 'none', + }, + }) +); + +const IconWrapper = styled.View({ + marginTop: 2, +}); + +const ResultRowContent = styled.View(() => ({ + display: 'flex', + flexDirection: 'column', +})); + +const NoResults = styled.View(({ theme }) => ({ + marginTop: 20, + textAlign: 'center', + fontSize: theme.typography.size.s2, + lineHeight: 18, + color: theme.color.defaultText, +})); + +const Mark = styled.Text(({ theme }) => ({ + backgroundColor: 'transparent', + color: theme.color.secondary, +})); + +const MoreWrapper = styled.View({ + marginTop: 8, +}); + +const RecentlyOpenedTitle = styled.View(({ theme }) => ({ + display: 'flex', + flexDirection: 'row', + justifyContent: 'space-between', + fontSize: theme.typography.size.s1 - 1, + fontWeight: theme.typography.weight.bold, + minHeight: 28, + // letterSpacing: '0.16em', <-- todo + textTransform: 'uppercase', + color: theme.textMutedColor, + marginTop: 16, + marginBottom: 4, + alignItems: 'center', +})); + +const Highlight: FC> = React.memo( + function Highlight({ children, match }) { + if (!match) return children; + const { value, indices } = match; + + const { nodes: result } = indices.reduce<{ cursor: number; nodes: ReactNode[] }>( + ({ cursor, nodes }, [start, end], index, { length }) => { + nodes.push({value.slice(cursor, start)}); + nodes.push({value.slice(start, end + 1)}); + if (index === length - 1) { + nodes.push({value.slice(end + 1)}); + } + return { cursor: end + 1, nodes }; + }, + { cursor: 0, nodes: [] } + ); + return {result}; + } +); + +const Title = styled.Text(({ theme }) => ({ + justifyContent: 'flex-start', + color: theme.textMutedColor, + fontSize: theme.typography.size.s2, +})); + +const Path = styled.View(({ theme }) => ({ + justifyContent: 'flex-start', + marginVertical: 2, + color: theme.textMutedColor, + fontSize: theme.typography.size.s1 - 1, + flexDirection: 'row', +})); + +const PathText = styled.Text(({ theme }) => ({ + fontSize: theme.typography.size.s1 - 1, + color: theme.textMutedColor, +})); + +const Result: FC = React.memo(function Result({ + item, + matches, + icon: _icon, + onPress, + ...props +}) { + const press: PressableProps['onPress'] = useCallback( + (event) => { + event.preventDefault(); + onPress?.(event); + }, + [onPress] + ); + + const nameMatch = matches.find((match: FuseResultMatch) => match.key === 'name'); + const pathMatches = matches.filter((match: FuseResultMatch) => match.key === 'path'); + + const [i] = item.status ? statusMapping[item.status] : []; + + return ( + + + {item.type === 'component' && } + {item.type === 'story' && } + + + + <Highlight key="search-result-item--label-highlight" match={nameMatch}> + {item.name} + </Highlight> + + + {item.path.map((group, index) => ( + + + match.refIndex === index)} + > + {group} + + + + ))} + + + {item.status ? i : null} + + ); +}); + +export const SearchResults: FC<{ + query: string; + results: SearchResult[]; + closeMenu: (cb?: () => void) => void; + getItemProps: GetSearchItemProps; + highlightedIndex: number | null; + isLoading?: boolean; + enableShortcuts?: boolean; + clearLastViewed?: () => void; +}> = React.memo(function SearchResults({ + query, + results, + closeMenu, + getItemProps, + highlightedIndex, + clearLastViewed, +}) { + const handleClearLastViewed = () => { + clearLastViewed(); + closeMenu(); + }; + + return ( + + {results.length > 0 && !query && ( + + Recently opened + + + )} + {results.length === 0 && query && ( + + + No components found + Find components by name or path. + + + )} + {results.map((result, index) => { + if (isExpandType(result)) { + return ( + + + ), + }, + { + id: '2', + type: types.experimental_SIDEBAR_BOTTOM, + render: () =>