diff --git a/packages/core/package.json b/packages/core/package.json index 14be64e85d..13f7af36c8 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -187,7 +187,7 @@ "@typescript-eslint/eslint-plugin": "^5.36.1", "@typescript-eslint/parser": "^5.36.1", "@uiw/codemirror-extensions-langs": "^4.21.25", - "@uiw/codemirror-theme-vscode": "^4.21.25", + "@uiw/codemirror-theme-github": "^4.21.25", "@uiw/react-codemirror": "^4.21.25", "acorn": "^8.7.1", "autoprefixer": "^10.4.0", diff --git a/packages/core/src/storybook/components/canvas-wrapper/CanvasWrapper.module.scss b/packages/core/src/storybook/components/canvas-wrapper/CanvasWrapper.module.scss index 2f70716782..6a83c54483 100644 --- a/packages/core/src/storybook/components/canvas-wrapper/CanvasWrapper.module.scss +++ b/packages/core/src/storybook/components/canvas-wrapper/CanvasWrapper.module.scss @@ -9,7 +9,7 @@ overflow: auto; border-radius: 0 0 4px 4px; box-shadow: rgba(0, 0, 0, 0.1) 0 1px 3px 0; - border: 1px solid hsla(203, 50%, 30%, 0.15); + border: 1px solid #dde3e7; border-top: none; &[data-editor-open="true"] { diff --git a/packages/core/src/storybook/components/canvas-wrapper/CanvasWrapper.tsx b/packages/core/src/storybook/components/canvas-wrapper/CanvasWrapper.tsx index 3463c4d8c5..9de26ffd43 100644 --- a/packages/core/src/storybook/components/canvas-wrapper/CanvasWrapper.tsx +++ b/packages/core/src/storybook/components/canvas-wrapper/CanvasWrapper.tsx @@ -12,7 +12,7 @@ const CanvasWrapper: FC = ({ of }) => { const toggleCodeAction = useMemo( () => ({ - title: "Toggle code editor", + title: "Story Editor", onClick: () => setOpen(prev => !prev) }), [] diff --git a/packages/core/src/storybook/decorators/withLiveEdit/LiveContent/LiveContent.module.scss b/packages/core/src/storybook/decorators/withLiveEdit/components/LiveContent/LiveContent.module.scss similarity index 100% rename from packages/core/src/storybook/decorators/withLiveEdit/LiveContent/LiveContent.module.scss rename to packages/core/src/storybook/decorators/withLiveEdit/components/LiveContent/LiveContent.module.scss diff --git a/packages/core/src/storybook/decorators/withLiveEdit/LiveContent/LiveContent.tsx b/packages/core/src/storybook/decorators/withLiveEdit/components/LiveContent/LiveContent.tsx similarity index 73% rename from packages/core/src/storybook/decorators/withLiveEdit/LiveContent/LiveContent.tsx rename to packages/core/src/storybook/decorators/withLiveEdit/components/LiveContent/LiveContent.tsx index 0476a1c660..e54eae06f6 100644 --- a/packages/core/src/storybook/decorators/withLiveEdit/LiveContent/LiveContent.tsx +++ b/packages/core/src/storybook/decorators/withLiveEdit/components/LiveContent/LiveContent.tsx @@ -1,12 +1,12 @@ import React from "react"; import { LiveProvider } from "react-live"; -import LivePreview from "../../../components/live-preview/LivePreview"; -import useApplyDecorators from "../hooks/useApplyDecorators"; +import LivePreview from "../../../../components/live-preview/LivePreview"; +import useApplyDecorators from "../../hooks/useApplyDecorators"; import { LiveContentProps } from "./LiveContent.types"; import styles from "./LiveContent.module.scss"; -import * as VibeComponents from "../../../../components"; -import * as VibeIcons from "../../../../components/Icon/Icons"; -import * as VibeComponentsNext from "../../../../next"; +import * as VibeComponents from "../../../../../components"; +import * as VibeIcons from "../../../../../components/Icon/Icons"; +import * as VibeComponentsNext from "../../../../../next"; const vibeScope = { ...VibeComponents, VibeIcons, VibeNext: VibeComponentsNext }; const reactCommonHooksScope = { diff --git a/packages/core/src/storybook/decorators/withLiveEdit/LiveContent/LiveContent.types.ts b/packages/core/src/storybook/decorators/withLiveEdit/components/LiveContent/LiveContent.types.ts similarity index 100% rename from packages/core/src/storybook/decorators/withLiveEdit/LiveContent/LiveContent.types.ts rename to packages/core/src/storybook/decorators/withLiveEdit/components/LiveContent/LiveContent.types.ts diff --git a/packages/core/src/storybook/decorators/withLiveEdit/components/LiveEditorAction/LiveEditorAction.module.scss b/packages/core/src/storybook/decorators/withLiveEdit/components/LiveEditorAction/LiveEditorAction.module.scss new file mode 100644 index 0000000000..66e417d954 --- /dev/null +++ b/packages/core/src/storybook/decorators/withLiveEdit/components/LiveEditorAction/LiveEditorAction.module.scss @@ -0,0 +1,20 @@ +.action { + opacity: 0.6; + border: 0; + padding: 4px 8px; + cursor: pointer; + font-size: 9px; + font-weight: bold; + background-color: #fff; +} + +.action:hover:not([disabled]) { + opacity: 0.8; + transition: opacity 0.1s ease; +} + +.action[disabled] { + cursor: not-allowed; + color: #000; + opacity: 0.3; +} diff --git a/packages/core/src/storybook/decorators/withLiveEdit/components/LiveEditorAction/LiveEditorAction.tsx b/packages/core/src/storybook/decorators/withLiveEdit/components/LiveEditorAction/LiveEditorAction.tsx new file mode 100644 index 0000000000..ae3011aecf --- /dev/null +++ b/packages/core/src/storybook/decorators/withLiveEdit/components/LiveEditorAction/LiveEditorAction.tsx @@ -0,0 +1,13 @@ +import React from "react"; +import styles from "./LiveEditorAction.module.scss"; +import { LiveEditorActionProps } from "./LiveEditorAction.types"; + +const LiveEditorAction = ({ onClick, disabled, children }: LiveEditorActionProps) => { + return ( + + ); +}; + +export default LiveEditorAction; diff --git a/packages/core/src/storybook/decorators/withLiveEdit/components/LiveEditorAction/LiveEditorAction.types.ts b/packages/core/src/storybook/decorators/withLiveEdit/components/LiveEditorAction/LiveEditorAction.types.ts new file mode 100644 index 0000000000..8ab99b0f54 --- /dev/null +++ b/packages/core/src/storybook/decorators/withLiveEdit/components/LiveEditorAction/LiveEditorAction.types.ts @@ -0,0 +1,5 @@ +export interface LiveEditorActionProps { + onClick?: () => void; + disabled?: boolean; + children: string; +} diff --git a/packages/core/src/storybook/decorators/withLiveEdit/hooks/useLiveEditorActions.ts b/packages/core/src/storybook/decorators/withLiveEdit/hooks/useLiveEditorActions.ts new file mode 100644 index 0000000000..d1170cb87b --- /dev/null +++ b/packages/core/src/storybook/decorators/withLiveEdit/hooks/useLiveEditorActions.ts @@ -0,0 +1,52 @@ +import { useCallback, useRef, useState } from "react"; +import { formatCode } from "../utils/prettier-utils"; +import { extractRenderAttributeFromCsf } from "../utils/parse-csf-utils"; + +function format(source: string): string { + try { + return formatCode(source); + } catch (e) { + return source; + } +} + +const useLiveEditorActions = (originalSource: string) => { + const originalRenderFromCsf = useRef( + originalSource ? extractRenderAttributeFromCsf(`(${originalSource})`) : "" + ); + const [code, setCode] = useState(format(originalRenderFromCsf.current)); + const [isDirty, setDirty] = useState(false); + const [isCopied, setCopied] = useState(false); + + const onChange = useCallback((newVal: string) => { + setCode(newVal); + setDirty(true); + }, []); + + const onCopyClick = useCallback(() => { + navigator.clipboard.writeText(code); + setCopied(true); + setTimeout(() => setCopied(false), 2000); + }, [code]); + + const onFormatClick = useCallback(() => { + setCode(format(code)); + }, [code]); + + const onResetClick = useCallback(() => { + setCode(format(originalRenderFromCsf.current)); + setDirty(false); + }, []); + + return { + code, + isDirty, + onChange, + onCopyClick, + isCopied, + onFormatClick, + onResetClick + }; +}; + +export default useLiveEditorActions; diff --git a/packages/core/src/storybook/decorators/withLiveEdit/withLiveEdit.module.scss b/packages/core/src/storybook/decorators/withLiveEdit/withLiveEdit.module.scss new file mode 100644 index 0000000000..62c33dab2a --- /dev/null +++ b/packages/core/src/storybook/decorators/withLiveEdit/withLiveEdit.module.scss @@ -0,0 +1,9 @@ +.actions { + display: flex; + gap: 1px; + position: absolute; + bottom: 0; + right: 0; + border-top-left-radius: 4px; + overflow: hidden; +} diff --git a/packages/core/src/storybook/decorators/withLiveEdit/withLiveEdit.tsx b/packages/core/src/storybook/decorators/withLiveEdit/withLiveEdit.tsx index 6b3e7d2481..639c36513e 100644 --- a/packages/core/src/storybook/decorators/withLiveEdit/withLiveEdit.tsx +++ b/packages/core/src/storybook/decorators/withLiveEdit/withLiveEdit.tsx @@ -1,41 +1,23 @@ import { Decorator, StoryContext } from "@storybook/react"; -import { useMemo, useRef, useState } from "react"; -import { vscodeDark } from "@uiw/codemirror-theme-vscode"; +import { useMemo } from "react"; +import { githubDark } from "@uiw/codemirror-theme-github"; import { langs } from "@uiw/codemirror-extensions-langs"; import { createPortal } from "react-dom"; import LiveEditor from "../../components/live-editor/LiveEditor"; -import { formatCode } from "./utils/prettier-utils"; -import LiveContent from "./LiveContent/LiveContent"; -import { extractRenderAttributeFromCsf } from "./utils/parse-csf-utils"; - -function getInitialCodeValue(source: string, shouldPrintError: boolean): string { - try { - // need to wrap with parentheses to avoid syntax errors - const code = extractRenderAttributeFromCsf(`(${source})`); - return formatCode(code); - } catch (e) { - if (shouldPrintError) { - console.error(e); - } - return source; - } -} +import LiveContent from "./components/LiveContent/LiveContent"; +import LiveEditorAction from "./components/LiveEditorAction/LiveEditorAction"; +import styles from "./withLiveEdit.module.scss"; +import useLiveEditorActions from "./hooks/useLiveEditorActions"; const withLiveEdit: Decorator = (Story, context: StoryContext) => { const { id, parameters, viewMode, moduleExport } = context; + const { source: sourceParams, liveEdit: liveEditParams } = parameters.docs || {}; const canvasEditorContainer = useMemo(() => document.getElementById(id), [id]); - const shouldAllowLiveEdit = viewMode === "docs" && parameters.docs?.liveEdit?.isEnabled && !!canvasEditorContainer; + const shouldAllowLiveEdit = viewMode === "docs" && liveEditParams?.isEnabled && !!canvasEditorContainer; - const originalCode = useRef( - getInitialCodeValue(parameters.docs.source?.originalSource || "", shouldAllowLiveEdit) + const { code, isDirty, onChange, onCopyClick, isCopied, onFormatClick, onResetClick } = useLiveEditorActions( + sourceParams?.originalSource ); - const [code, setCode] = useState(originalCode.current); - const [dirty, setDirty] = useState(false); - - const handleChange = (newVal: string) => { - setCode(newVal); - setDirty(true); - }; if (!shouldAllowLiveEdit) { return ; @@ -43,10 +25,10 @@ const withLiveEdit: Decorator = (Story, context: StoryContext) => { return ( <> - {dirty ? ( + {isDirty ? ( @@ -54,20 +36,29 @@ const withLiveEdit: Decorator = (Story, context: StoryContext) => { )} {createPortal( - , + <> + +
+ + {isCopied ? "Copied" : "Copy"} + + Format + Reset +
+ , canvasEditorContainer )} diff --git a/yarn.lock b/yarn.lock index 82d55dc67d..2a121a248a 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5375,17 +5375,17 @@ "@replit/codemirror-lang-svelte" "^6.0.0" codemirror-lang-mermaid "^0.5.0" -"@uiw/codemirror-theme-vscode@^4.21.25": - version "4.21.25" - resolved "https://registry.npmjs.org/@uiw/codemirror-theme-vscode/-/codemirror-theme-vscode-4.21.25.tgz#cf868c219e2ca8253dd2845e451c92920bea27ce" - integrity sha512-1gubCz7kHE5XH3H1IUTSrnyK/G3dQRmOIgPFsefE9e+TizhBJnkbKSDSfRfpm5l7jl1G7v/as0HQvN3cYg/Rkg== +"@uiw/codemirror-theme-github@^4.21.25": + version "4.22.1" + resolved "https://registry.npmjs.org/@uiw/codemirror-theme-github/-/codemirror-theme-github-4.22.1.tgz#7f2288cdbe6c873f2e13029004665a15fd8930f6" + integrity sha512-trotMuMd8PQjvst4hg9b5Mz5qlD5HQzPVAZQFlQKaaF8mEKMk77nFCPhfD3Rg2WEq02lx6fqmOcErQjY0GRHew== dependencies: - "@uiw/codemirror-themes" "4.21.25" + "@uiw/codemirror-themes" "4.22.1" -"@uiw/codemirror-themes@4.21.25": - version "4.21.25" - resolved "https://registry.npmjs.org/@uiw/codemirror-themes/-/codemirror-themes-4.21.25.tgz#562e7e4d36b34329858ffe9c9c06d7f50cfe0d3c" - integrity sha512-C3t/voELxQj0eaVhrlgzaOnSALNf8bOcRbL5xN9r2+RkdsbFOmvNl3VVhlxEB7PSGc1jUZwVO4wQsB2AP178ag== +"@uiw/codemirror-themes@4.22.1": + version "4.22.1" + resolved "https://registry.npmjs.org/@uiw/codemirror-themes/-/codemirror-themes-4.22.1.tgz#e439627572d06ea9840f07d8e80e921aa8ded694" + integrity sha512-5TeB8wCc0aNd3YEhzOvgekpAFQfEm4fCTUcGmEIQqaRNgKAM83HYNpE1JF2j7x2oDFugdiO0yJynS6bo1zVOuw== dependencies: "@codemirror/language" "^6.0.0" "@codemirror/state" "^6.0.0" @@ -18225,7 +18225,16 @@ string-length@^4.0.1: char-regex "^1.0.2" strip-ansi "^6.0.0" -"string-width-cjs@npm:string-width@^4.2.0", "string-width@^1.0.2 || 2 || 3 || 4", string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3: +"string-width-cjs@npm:string-width@^4.2.0": + version "4.2.3" + resolved "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010" + integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== + dependencies: + emoji-regex "^8.0.0" + is-fullwidth-code-point "^3.0.0" + strip-ansi "^6.0.1" + +"string-width@^1.0.2 || 2 || 3 || 4", string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3: version "4.2.3" resolved "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010" integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== @@ -18311,7 +18320,7 @@ stringify-entities@^4.0.0: character-entities-html4 "^2.0.0" character-entities-legacy "^3.0.0" -"strip-ansi-cjs@npm:strip-ansi@^6.0.1", strip-ansi@^6.0.0, strip-ansi@^6.0.1: +"strip-ansi-cjs@npm:strip-ansi@^6.0.1": version "6.0.1" resolved "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9" integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== @@ -18332,6 +18341,13 @@ strip-ansi@^5.2.0: dependencies: ansi-regex "^4.1.0" +strip-ansi@^6.0.0, strip-ansi@^6.0.1: + version "6.0.1" + resolved "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9" + integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== + dependencies: + ansi-regex "^5.0.1" + strip-ansi@^7.0.1: version "7.1.0" resolved "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz#d5b6568ca689d8561370b0707685d22434faff45" @@ -20169,7 +20185,7 @@ workerpool@6.2.1: resolved "https://registry.npmjs.org/workerpool/-/workerpool-6.2.1.tgz#46fc150c17d826b86a008e5a4508656777e9c343" integrity sha512-ILEIE97kDZvF9Wb9f6h5aXK4swSlKGUcOEGiIYb2OOu/IrDU9iwj0fD//SsA6E5ibwJxpEvhullJY4Sl4GcpAw== -"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0", wrap-ansi@^7.0.0: +"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0": version "7.0.0" resolved "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43" integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q== @@ -20187,6 +20203,15 @@ wrap-ansi@^6.0.1, wrap-ansi@^6.2.0: string-width "^4.1.0" strip-ansi "^6.0.0" +wrap-ansi@^7.0.0: + version "7.0.0" + resolved "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43" + integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q== + dependencies: + ansi-styles "^4.0.0" + string-width "^4.1.0" + strip-ansi "^6.0.0" + wrap-ansi@^8.1.0: version "8.1.0" resolved "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz#56dc22368ee570face1b49819975d9b9a5ead214"