From af61668b232d8d56c826b2fdb0275fd341f9905c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=B0=8F=E8=99=8Eoni?= Date: Fri, 11 Sep 2020 19:40:04 +0800 Subject: [PATCH] feat(theme): create mobile theme to dev mobile library (#287) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: add dumi-theme-mobile * fix: test * feat: add test for mobile theme * fix: 1、修复切换页面时不触发预览项目变更的问题 2、不需要出现手机窗口的时候出现了 3、窗口和侧边栏菜单应该2选1,不然页面太拥挤 * feat: 使用 src/content 重构代码 * fix: ci test * test: add device test * ci: support to preview mobile theme site * ci: add 404 page for surge preview * chore: remove iframe onload event * chore: remove useless 404 plugin * chore(theme): rename dumi-theme-mobile to theme-mobile * refactor(theme): update mobile device wrapper * refactor(theme): handle props change for useLocaleProps theme api * refactor(theme): update mobile Previewer & content * fix: qrcode url * fix: test * feat: support dumi@1.1.0-beta.16 * feat: add demos layout * fix: fallback theme builtins path error * test(theme): fix circular effects for useLocaleProps case * chore: remove useless dependencies * test: add demo layout test * test(theme): remove useless import for mobile theme case * chore: fix test * refactor(theme): rename mobile demo layout * test(theme): improve test coverage for mobile theme Co-authored-by: PeachScript --- .github/workflows/preview.yml | 15 ++ package.json | 1 + .../src/theme/hooks/useLocaleProps.test.ts | 4 +- .../src/theme/hooks/useLocaleProps.ts | 2 +- packages/preset-dumi/src/theme/loader.ts | 12 +- .../src/utils/getHostPkgAlias.test.ts | 1 + packages/theme-default/package.json | 1 + packages/theme-mobile/.fatherrc.ts | 4 + packages/theme-mobile/package.json | 40 ++++ .../theme-mobile/src/builtins/Previewer.less | 22 ++ .../theme-mobile/src/builtins/Previewer.tsx | 68 ++++++ .../theme-mobile/src/components/Device.less | 139 ++++++++++++ .../theme-mobile/src/components/Device.tsx | 43 ++++ packages/theme-mobile/src/layouts/demo.tsx | 31 +++ packages/theme-mobile/src/layouts/index.tsx | 40 ++++ packages/theme-mobile/src/style/layout.less | 33 +++ .../theme-mobile/src/style/variables.less | 12 + packages/theme-mobile/src/test/index.test.tsx | 206 ++++++++++++++++++ 18 files changed, 666 insertions(+), 8 deletions(-) create mode 100644 packages/theme-mobile/.fatherrc.ts create mode 100644 packages/theme-mobile/package.json create mode 100644 packages/theme-mobile/src/builtins/Previewer.less create mode 100644 packages/theme-mobile/src/builtins/Previewer.tsx create mode 100644 packages/theme-mobile/src/components/Device.less create mode 100644 packages/theme-mobile/src/components/Device.tsx create mode 100644 packages/theme-mobile/src/layouts/demo.tsx create mode 100644 packages/theme-mobile/src/layouts/index.tsx create mode 100644 packages/theme-mobile/src/style/layout.less create mode 100644 packages/theme-mobile/src/style/variables.less create mode 100644 packages/theme-mobile/src/test/index.test.tsx diff --git a/.github/workflows/preview.yml b/.github/workflows/preview.yml index 85195afdae..8ca12fdf16 100644 --- a/.github/workflows/preview.yml +++ b/.github/workflows/preview.yml @@ -16,3 +16,18 @@ jobs: yarn build yarn doc:build dist: dist + preview-mobile: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + - uses: afc163/surge-preview@v1 + env: + DUMI_THEME: dumi-theme-mobile + with: + surge_token: ${{ secrets.SURGE_TOKEN }} + github_token: ${{ secrets.GITHUB_TOKEN }} + build: | + yarn + yarn build + yarn doc:build + dist: dist diff --git a/package.json b/package.json index 885e1b586a..73d2509ccd 100644 --- a/package.json +++ b/package.json @@ -5,6 +5,7 @@ "private": true, "scripts": { "dev": "cross-env BROWSER=none node ./packages/dumi/bin/dumi.js dev", + "dev:mobile": "cross-env BROWSER=none DUMI_THEME=dumi-theme-mobile node ./packages/dumi/bin/dumi.js dev", "watch": "npm run build -- --watch", "doc:build": "cross-env BROWSER=none node ./packages/dumi/bin/dumi.js build", "build": "father-build", diff --git a/packages/preset-dumi/src/theme/hooks/useLocaleProps.test.ts b/packages/preset-dumi/src/theme/hooks/useLocaleProps.test.ts index 81e24ab41e..a0961f24e5 100644 --- a/packages/preset-dumi/src/theme/hooks/useLocaleProps.test.ts +++ b/packages/preset-dumi/src/theme/hooks/useLocaleProps.test.ts @@ -3,7 +3,9 @@ import useLocaleProps from './useLocaleProps'; describe('theme API: useLocaleProps', () => { it('should transform props by locale', () => { - const { result } = renderHook(() => useLocaleProps('en-US', { title: 2, 'title_en-US': 1 })); + const { result } = renderHook(props => useLocaleProps('en-US', props), { + initialProps: { title: 2, 'title_en-US': 1 }, + }); expect(result.current).toEqual({ title: 1 }); }); diff --git a/packages/preset-dumi/src/theme/hooks/useLocaleProps.ts b/packages/preset-dumi/src/theme/hooks/useLocaleProps.ts index 4df521f8a0..f6bcf41ca1 100644 --- a/packages/preset-dumi/src/theme/hooks/useLocaleProps.ts +++ b/packages/preset-dumi/src/theme/hooks/useLocaleProps.ts @@ -22,7 +22,7 @@ export default (locale: string, props: T) => { useEffect(() => { setLocaleProps(processor(locale, props)); - }, [locale]); + }, [locale, props]); return localeProps; }; diff --git a/packages/preset-dumi/src/theme/loader.ts b/packages/preset-dumi/src/theme/loader.ts index 7cf63b92ce..75abe5410c 100644 --- a/packages/preset-dumi/src/theme/loader.ts +++ b/packages/preset-dumi/src/theme/loader.ts @@ -89,12 +89,12 @@ export default async () => { const builtinPath = path.join(modulePath, 'builtins'); const components = fs.existsSync(builtinPath) ? fs - .readdirSync(builtinPath) - .filter(file => /\.(j|t)sx?$/.test(file)) - .map(file => ({ - identifier: path.parse(file).name, - source: winPath(path.join(theme, 'builtins', file)), - })) + .readdirSync(builtinPath) + .filter(file => /\.(j|t)sx?$/.test(file)) + .map(file => ({ + identifier: path.parse(file).name, + source: winPath(path.join(theme, 'builtins', file)), + })) : []; const fallbacks = REQUIRED_THEME_BUILTINS.reduce((result, name) => { if (components.every(({ identifier }) => identifier !== name)) { diff --git a/packages/preset-dumi/src/utils/getHostPkgAlias.test.ts b/packages/preset-dumi/src/utils/getHostPkgAlias.test.ts index c3edd98707..dc064838f8 100644 --- a/packages/preset-dumi/src/utils/getHostPkgAlias.test.ts +++ b/packages/preset-dumi/src/utils/getHostPkgAlias.test.ts @@ -19,6 +19,7 @@ describe('getHostPkgAlias', () => { 'dumi', '@umijs/preset-dumi', 'dumi-theme-default', + "dumi-theme-mobile", ]); }); }); diff --git a/packages/theme-default/package.json b/packages/theme-default/package.json index 7651484088..f7f45c20c1 100644 --- a/packages/theme-default/package.json +++ b/packages/theme-default/package.json @@ -3,6 +3,7 @@ "version": "1.0.0-beta.6", "description": "The official default theme of dumi", "files": [ + "es", "src" ], "repository": { diff --git a/packages/theme-mobile/.fatherrc.ts b/packages/theme-mobile/.fatherrc.ts new file mode 100644 index 0000000000..ed6a30c2e9 --- /dev/null +++ b/packages/theme-mobile/.fatherrc.ts @@ -0,0 +1,4 @@ +export default { + cjs: false, + esm: 'babel', +}; diff --git a/packages/theme-mobile/package.json b/packages/theme-mobile/package.json new file mode 100644 index 0000000000..1837ffca98 --- /dev/null +++ b/packages/theme-mobile/package.json @@ -0,0 +1,40 @@ +{ + "name": "dumi-theme-mobile", + "version": "1.0.0-beta.0", + "description": "dumi-theme-mobile", + "files": [ + "es", + "src" + ], + "repository": { + "type": "git", + "url": "https://github.com/umijs/dumi" + }, + "keywords": [ + "dumi", + "father-build", + "umi" + ], + "authors": [ + "xiaohuoni <448627663@qq.com> (https://github.com/xiaohuoni)" + ], + "license": "MIT", + "bugs": "http://github.com/umijs/dumi/issues", + "homepage": "https://github.com/umijs/dumi/tree/master/packages/theme-mobile#readme", + "publishConfig": { + "access": "public" + }, + "devDependencies": { + "dumi": "1.x" + }, + "dependencies": { + "dumi-theme-default": "1.0.0-beta.0", + "lodash.debounce": "^4.0.8", + "qrcode.react": "^1.0.0", + "umi-hd": "^5.0.1" + }, + "peerDependencies": { + "@umijs/preset-dumi": "1.x", + "react": "^16.13.1" + } +} diff --git a/packages/theme-mobile/src/builtins/Previewer.less b/packages/theme-mobile/src/builtins/Previewer.less new file mode 100644 index 0000000000..e1f4804193 --- /dev/null +++ b/packages/theme-mobile/src/builtins/Previewer.less @@ -0,0 +1,22 @@ +@import (reference) '../style/variables.less'; + +@media @v-device-show { + .@{prefix}-mobile-previewer { + .@{prefix}-previewer-demo { + display: none; + } + + .@{prefix}-previewer-desc, + .@{prefix}-previewer-desc:not([data-title]) + .@{prefix}-previewer-actions { + border-top: none; + } + + button[role='source'] { + margin-right: -8px; + width: 0; + opacity: 0; + overflow: hidden; + pointer-events: none; + } + } +} diff --git a/packages/theme-mobile/src/builtins/Previewer.tsx b/packages/theme-mobile/src/builtins/Previewer.tsx new file mode 100644 index 0000000000..efdc80b3f9 --- /dev/null +++ b/packages/theme-mobile/src/builtins/Previewer.tsx @@ -0,0 +1,68 @@ +import React, { useRef, useEffect, useState } from 'react'; +import Previewer, { IPreviewerProps } from 'dumi-theme-default/src/builtins/Previewer'; +import debounce from 'lodash.debounce'; +import './Previewer.less'; + +export const ACTIVE_MSG_TYPE = 'dumi:scroll-into-demo'; + +export default (props: IPreviewerProps) => { + const ref = useRef(); + const [previewerProps, setPreviewerProps] = useState(null); + const [isActive, setIsActive] = useState(false); + + useEffect(() => { + const isFirstDemo = document.querySelector('.__dumi-default-mobile-previewer') === ref.current; + const handler = debounce(() => { + const scrollTop = document.documentElement.scrollTop + 128; + + // post message if scroll into current demo + if ( + // fallback to first demo + (isFirstDemo && scrollTop < ref?.current?.offsetTop) || + // detect scroll position + (scrollTop > ref?.current?.offsetTop && + scrollTop < ref?.current?.offsetTop + ref?.current?.offsetHeight) + ) { + window.postMessage({ type: ACTIVE_MSG_TYPE, value: props.identifier }, '*'); + setIsActive(true); + } else { + setIsActive(false); + } + }, 50); + + // only render mobile phone when screen max than 960px + if (window?.outerWidth > 960) { + // active source code wrapper if scroll into demo + handler(); + window.addEventListener('scroll', handler); + + // rewrite props for device mode + setPreviewerProps( + Object.assign({}, props, { + // omit children + children: null, + // show source code + defaultShowCode: true, + // hide external action + hideActions: ['EXTERNAL' as IPreviewerProps['hideActions'][0]].concat(props.hideActions), + }), + ); + } else { + // use standard mode if screen min than 960px + setPreviewerProps(props); + } + + return () => window.removeEventListener('scroll', handler); + }, []); + + return ( +
+ {previewerProps && ( + + )} +
+ ); +}; diff --git a/packages/theme-mobile/src/components/Device.less b/packages/theme-mobile/src/components/Device.less new file mode 100644 index 0000000000..f565ffc336 --- /dev/null +++ b/packages/theme-mobile/src/components/Device.less @@ -0,0 +1,139 @@ +@import (reference) '../style/variables.less'; + +.gen-device-style(@scale) { + width: @s-device-width * @scale; + min-width: @s-device-width * @scale; + height: @s-device-width * @scale * @s-device-ratio; + box-shadow: 0 0 0 @s-device-border-width * @scale #090a0d, + 0 0 0 @s-device-shell-width * @scale #9fa3a8, + 0 4px 20px @s-device-shell-width * @scale rgba(0, 0, 0, 0.1); +} + +.@{prefix}-device { + position: sticky; + top: @s-device-gap-top; + display: flex; + flex-direction: column; + margin-left: @s-content-margin; + width: @s-device-width; + min-width: @s-device-width; + height: @s-device-width * @s-device-ratio; + border-radius: 32px; + overflow: hidden; + .gen-device-style(1); + + @media only screen and (max-width: 1440px) { + .gen-device-style(0.9); + } + + @media only screen and (max-width: 1360px) { + .gen-device-style(0.8); + } + + @media only screen and (max-width: 960px) { + display: none; + } + + &[data-mode='site'] { + top: @s-nav-height + @s-device-shell-width + @s-device-gap-top; + } + + &-status, + &-action { + display: flex; + justify-content: space-between; + align-items: center; + padding: 0 22px; + } + + &-status { + height: 30px; + color: #222; + font-size: 12px; + font-weight: 500; + user-select: none; + + span { + display: inline-block; + width: 60px; + + &:nth-child(2) { + text-align: center; + } + } + + // battery + &::after { + content: ''; + display: inline-block; + margin-left: 42px; + width: 14px; + height: 5px; + border-radius: 1px; + background: #50d664; + box-shadow: 0 0 0 1px #fff, 0 0 0 2px #999; + } + } + + &-action { + height: 40px; + background: #f3f3f3; + border-top: 1px solid #e3e3e3; + + > a, + > button { + padding: 0; + width: 16px; + height: 16px; + box-sizing: content-box; + border: 2px solid transparent; + transition: opacity 0.2s, background 0.2s; + outline: none; + cursor: pointer; + + &:hover { + opacity: 0.8; + } + + &:active { + opacity: 0.9; + } + + &[role='refresh'] { + background-position-x: -144px; + } + + &[role='open-demo'] { + background-position-x: -126px; + } + + &[role='qrcode'] { + position: relative; + z-index: 1; + background-position-x: -218px; + + > canvas { + position: absolute; + bottom: 120%; + left: 50%; + border: 4px solid #fff; + box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1); + box-sizing: content-box; + transition: all 0.2s ease-in-out; + transform: translateX(-50%) scale(0); + transform-origin: center bottom; + } + + &:hover > canvas, + &:focus > canvas { + transform: translateX(-50%) scale(1); + } + } + } + } + + > iframe { + flex: 1; + border: 0; + } +} diff --git a/packages/theme-mobile/src/components/Device.tsx b/packages/theme-mobile/src/components/Device.tsx new file mode 100644 index 0000000000..752069f4fe --- /dev/null +++ b/packages/theme-mobile/src/components/Device.tsx @@ -0,0 +1,43 @@ +import React, { FC, useState, useContext } from 'react'; +import QRCode from 'qrcode.react'; +import { Link, context } from 'dumi/theme'; +import './Device.less'; + +interface IDeviceProps { + className?: string; + url: string; +} + +const Device: FC = ({ url, className }) => { + const [renderKey, setRenderKey] = useState(Math.random()); + const { + config: { mode }, + } = useContext(context); + + return ( +
+
+ dumi + 10:24 +
+