diff --git a/.storybook/preview.js b/.storybook/preview.js index 55df8a3f8..f3a39928e 100644 --- a/.storybook/preview.js +++ b/.storybook/preview.js @@ -1,8 +1,8 @@ import React, { FC } from 'react' import { addDecorator, addParameters } from '@storybook/react'; -import { Title, Subtitle, Source, Story, Stories, Props, Description } from '@storybook/addon-docs/blocks'; +import { Title, Subtitle, Story, Stories, Props, Description } from '@storybook/addon-docs/blocks'; import { DependenciesTable } from 'storybook-addon-deps/blocks'; -import { ControlsEditorsTable, ThemeProvider } from '@component-controls/storybook'; +import { ControlsEditorsTable, ThemeProvider, Source } from '@component-controls/storybook'; addDecorator((story, ctx ) => { return ( diff --git a/core/editors/package.json b/core/editors/package.json index 2d5dcdbf2..fa61231a5 100644 --- a/core/editors/package.json +++ b/core/editors/package.json @@ -41,7 +41,8 @@ "react-popper-tooltip": "^2.10.1", "react-select": "^3.0.8", "react-tabs": "^3.1.0", - "theme-ui": "^0.3.1" + "theme-ui": "^0.3.1", + "@theme-ui/prism": "^0.3.0" }, "devDependencies": { "@types/jest": "^25.1.2", diff --git a/core/editors/src/blocks/BlockContainer/BlockContainer.tsx b/core/editors/src/blocks/BlockContainer/BlockContainer.tsx new file mode 100644 index 000000000..ead8d30d5 --- /dev/null +++ b/core/editors/src/blocks/BlockContainer/BlockContainer.tsx @@ -0,0 +1,14 @@ +import React, { FC } from 'react'; +import styled from '@emotion/styled'; + +const StyledBlockContainer = styled.div<{}>(() => ({ + position: 'relative', + marginBottom: '25px', + boxSadow: 'rgba(0, 0, 0, 0.1) 0px 1px 3px 0px', + borderRadius: 4, + border: '1px solid rgba(0, 0, 0, 0.1)', +})); + +export const BlockContainer: FC = ({ children }) => ( + {children} +); diff --git a/core/editors/src/blocks/Source/Source.tsx b/core/editors/src/blocks/Source/Source.tsx new file mode 100644 index 000000000..e6fabf26d --- /dev/null +++ b/core/editors/src/blocks/Source/Source.tsx @@ -0,0 +1,99 @@ +/* eslint-disable react/jsx-key */ +import React, { FC, MouseEvent } from 'react'; +import Highlight, { + defaultProps, + Language, + PrismTheme, +} from 'prism-react-renderer'; +import dracula from 'prism-react-renderer/themes/dracula'; +import duotoneDark from 'prism-react-renderer/themes/duotoneDark'; +import duotoneLight from 'prism-react-renderer/themes/duotoneLight'; +import github from 'prism-react-renderer/themes/github'; +import nightOwl from 'prism-react-renderer/themes/nightOwl'; +import nightOwlLight from 'prism-react-renderer/themes/nightOwlLight'; +import oceanicNext from 'prism-react-renderer/themes/oceanicNext'; +import palenight from 'prism-react-renderer/themes/palenight'; +import shadesOfPurple from 'prism-react-renderer/themes/shadesOfPurple'; +import ultramin from 'prism-react-renderer/themes/ultramin'; +import vsDark from 'prism-react-renderer/themes/vsDark'; +import { Styled } from 'theme-ui'; +import copy from 'copy-to-clipboard'; +import { ActionBar } from '../../components/ActionBar/ActionBar'; +import { BlockContainer } from '../BlockContainer/BlockContainer'; + +const themes: { + [key: string]: PrismTheme; +} = { + nightOwlLight, + nightOwl, + github, + vsDark, + oceanicNext, + palenight, + ultramin, + duotoneLight, + duotoneDark, + dracula, + shadesOfPurple, +}; +export interface SourceProps { + children?: string; + language?: Language; +} + +export const Source: FC = ({ + children = '', + language = 'jsx', +}) => { + const [themeName, setThemeName] = React.useState('nightOwlLight'); + let prismTheme = themes[themeName] || defaultProps.theme; + const [copied, setCopied] = React.useState(false); + + const onRotateTheme = () => { + const themeKeys = Object.keys(themes); + const themeIdx = themeKeys.indexOf(themeName); + const newIdx = themeIdx >= themeKeys.length - 1 ? 0 : themeIdx + 1; + setThemeName(themeKeys[newIdx]); + }; + + const onCopy = (e: MouseEvent) => { + e.preventDefault(); + setCopied(true); + copy(children); + window.setTimeout(() => setCopied(false), 1500); + }; + return ( + + + {({ className, style, tokens, getLineProps, getTokenProps }) => ( + + {tokens.map((line, i) => ( + + {line.map((token, key) => ( + + ))} + + ))} + + )} + + + + ); +}; diff --git a/core/editors/src/editors/BooleanEditor/BooleanEditor.tsx b/core/editors/src/editors/BooleanEditor/BooleanEditor.tsx index e3c8449a3..8ba99feeb 100644 --- a/core/editors/src/editors/BooleanEditor/BooleanEditor.tsx +++ b/core/editors/src/editors/BooleanEditor/BooleanEditor.tsx @@ -16,7 +16,6 @@ export const BooleanEditor: PropertyEditor = ({ onChange(name, checked)} checked={prop.value} /> diff --git a/core/editors/src/forms/ControlsTable/ControlEditorsTable.tsx b/core/editors/src/forms/ControlsTable/ControlEditorsTable.tsx index 26e7821f7..a35750108 100644 --- a/core/editors/src/forms/ControlsTable/ControlEditorsTable.tsx +++ b/core/editors/src/forms/ControlsTable/ControlEditorsTable.tsx @@ -11,6 +11,7 @@ import { LoadedComponentControl, } from '@component-controls/core'; +import { BlockContainer } from '../../blocks/BlockContainer/BlockContainer'; import { Tab, Tabs, TabList, TabPanel } from '../../components/Tabs/Tabs'; import { ControlsEditorsTableProps } from '../../editors/types'; import { ActionBar } from '../../components/ActionBar/ActionBar'; @@ -34,13 +35,9 @@ const StyledActionBar = styled(ActionBar)<{}>(() => ({ zIndex: 0, })); -const PropEditorsContainer = styled.div<{}>(() => ({ - position: 'relative', - paddingBottom: '25px', - boxSadow: 'rgba(0, 0, 0, 0.1) 0px 1px 3px 0px', - borderRadius: 4, - border: '1px solid rgba(0, 0, 0, 0.1)', -})); +const StyledContainer = styled.div` + padding-bottom: 25px; +`; const PropEditorsTitle = styled.div<{}>(({ theme }: { theme: Theme }) => ({ padding: `${theme?.space?.[3]}px`, @@ -55,10 +52,6 @@ const PropEditorsTitle = styled.div<{}>(({ theme }: { theme: Theme }) => ({ const DEFAULT_GROUP_ID = 'Other'; -export const BlockWrapper: FC = ({ children }) => ( - {children} -); - const PropGroupTable: FC = ({ controls, storyId, @@ -157,38 +150,40 @@ export const ControlsEditorsTable: FC - {title && {title}} - {groupedItems.length === 1 ? ( - - ) : ( - - + + + {title && {title}} + {groupedItems.length === 1 ? ( + + ) : ( + + + {groupedItems.map(item => ( + {item.label} + ))} + {groupedItems.map(item => ( - {item.label} + + + ))} - - {groupedItems.map(item => ( - - - - ))} - - )} - ({ - title: item.title, - onClick: (e: MouseEvent) => { - e.preventDefault(); - item.onAction(props); - }, - })), - { title: 'Reset', onClick: onReset }, - { title: copied ? 'Copied' : 'Copy', onClick: onCopy }, - ]} - />{' '} - + + )} + ({ + title: item.title, + onClick: (e: MouseEvent) => { + e.preventDefault(); + item.onAction(props); + }, + })), + { title: 'Reset', onClick: onReset }, + { title: copied ? 'Copied' : 'Copy', onClick: onCopy }, + ]} + /> + + ); } return null; diff --git a/core/editors/src/index.ts b/core/editors/src/index.ts index 042e0dbc5..1edebf5ac 100644 --- a/core/editors/src/index.ts +++ b/core/editors/src/index.ts @@ -1,4 +1,5 @@ export * from './editors/prop-factory'; export * from './editors/types'; export * from './forms/ControlsTable/ControlEditorsTable'; +export * from './blocks/Source/Source'; export { FlexContainer } from './components/FlexContainer/FlexContainer'; diff --git a/core/editors/src/typings.d.ts b/core/editors/src/typings.d.ts index 2f4eb9cf4..cc0497a07 100644 --- a/core/editors/src/typings.d.ts +++ b/core/editors/src/typings.d.ts @@ -1 +1,2 @@ declare module 'global'; +declare module '@theme-ui/prism'; diff --git a/integrations/storybook/package.json b/integrations/storybook/package.json index cf29ad2ef..c51c37c4f 100644 --- a/integrations/storybook/package.json +++ b/integrations/storybook/package.json @@ -39,6 +39,7 @@ "@component-controls/loader": "^0.6.0", "@component-controls/specification": "^0.6.0", "@theme-ui/presets": "^0.3.0", + "@theme-ui/prism": "^0.3.0", "copy-to-clipboard": "^3.0.8", "core-js": "^3.0.1", "global": "^4.3.2", diff --git a/integrations/storybook/src/blocks/BlockContext.tsx b/integrations/storybook/src/blocks/BlockContext.tsx new file mode 100644 index 000000000..d60676052 --- /dev/null +++ b/integrations/storybook/src/blocks/BlockContext.tsx @@ -0,0 +1,94 @@ +import React from 'react'; +import { toId, storyNameFromExport } from '@storybook/csf'; +import { LoadedComponentControls } from '@component-controls/core'; +import { CURRENT_SELECTION, DocsContext } from '@storybook/addon-docs/blocks'; +import { ThemeProvider } from '../shared/ThemeProvider'; + +export interface BlockContextProps { + controls: LoadedComponentControls; + story: any; + api?: any; + id?: string; + channel: any; + source?: string; +} +export const BlockContext = React.createContext({ + controls: {}, + story: {}, + channel: {}, + source: '', +}); + +export interface BlockContextProviderProps { + /** id of the story */ + id?: string; + /** name of the story */ + name?: string; +} + +export const BlockContextProvider: React.FC = ({ + id, + name, + children, +}) => { + const context = React.useContext(DocsContext); + const { + id: currentId, + clientApi, + storyStore, + mdxStoryNameToKey, + mdxComponentMeta, + channel, + } = context as any; + const inputId = id === CURRENT_SELECTION ? currentId : id; + const previewId = + inputId || + (mdxStoryNameToKey && + mdxComponentMeta && + name && + toId( + mdxComponentMeta.id || mdxComponentMeta.title, + storyNameFromExport(mdxStoryNameToKey[name]), + )); + if (!previewId) { + return null; + } + const story = storyStore.fromId(previewId) || {}; + const { parameters = {} } = story; + let source: string | undefined; + if (parameters.mdxSource) { + source = parameters.mdxSource; + } else if (parameters.storySource) { + const { source: code, locationsMap } = parameters.storySource; + if (locationsMap) { + const location = locationsMap[previewId]; + if (location) { + const { startBody: start, endBody: end } = location; + const lines = code.split('\n'); + const startLine = lines[start.line - 1]; + const endLine = lines[end.line - 1]; + if (startLine !== undefined && endLine !== undefined) { + source = [ + startLine.substring(start.col), + ...lines.slice(start.line, end.line - 1), + endLine.substring(0, end.col), + ].join('\n'); + } + } + } + } + return ( + + {children} + + ); +}; diff --git a/integrations/storybook/src/blocks/ControlsEditorsTable.tsx b/integrations/storybook/src/blocks/ControlsEditorsTable.tsx index 90c1883de..b49c4c03d 100644 --- a/integrations/storybook/src/blocks/ControlsEditorsTable.tsx +++ b/integrations/storybook/src/blocks/ControlsEditorsTable.tsx @@ -1,103 +1,64 @@ import React, { FC } from 'react'; -import { toId, storyNameFromExport } from '@storybook/csf'; import { FORCE_RE_RENDER } from '@storybook/core-events'; import { SetControlValueFn, ClickControlFn, ComponentControlButton, } from '@component-controls/specification'; +import { mergeControlValues } from '@component-controls/core'; import { - LoadedComponentControls, - mergeControlValues, -} from '@component-controls/core'; -import { CURRENT_SELECTION, DocsContext } from '@storybook/addon-docs/blocks'; + BlockContext, + BlockContextProvider, + BlockContextProviderProps, +} from './BlockContext'; import { ControlsTable } from '../shared/ControlsTable'; import { SET_DATA_MSG } from '../shared/shared'; -interface ControlsEditorsTableProps { +export interface ControlsEditorsTableProps { /** a title to display */ title?: string; - /** id of the story */ - id?: string; - /** name of the story */ - name?: string; } -const getPropertyProps = ( - props: ControlsEditorsTableProps, - { id: currentId, storyStore, mdxStoryNameToKey, mdxComponentMeta }: any, -): { - controls?: LoadedComponentControls; - id: string | null; - storyStore: any; -} | null => { - const { id, name } = props; - const inputId = id === CURRENT_SELECTION ? currentId : id; - const previewId = - inputId || - (mdxStoryNameToKey && - mdxComponentMeta && - name && - toId( - mdxComponentMeta.id || mdxComponentMeta.title, - storyNameFromExport(mdxStoryNameToKey[name]), - )); - if (!previewId) { - return null; - } - const data = storyStore.fromId(previewId); - - const propsParam = - (data && data.parameters && data.parameters.controls) || {}; - - if (!data || propsParam.disable) { - return null; - } - return { - id: data.id, - storyStore, - controls: data.controls || data.parameters.controls, - }; -}; -export const ControlsEditorsTable: FC = ({ - title = 'Property Editors', - ...rest +const ControlsEditorsTableBlock: FC = ({ + title, }) => ( - - {(context: any) => { - const { controls, storyStore, id } = - getPropertyProps(rest, context) || {}; - const api: any = (context as any).clientApi; - const setControlValue: SetControlValueFn = api.setControlValue - ? api.setControlValue - : (storyId: string, propName: string | undefined, propValue: any) => { - const story: any = storyStore._data[storyId]; - if (story) { - const newValues = mergeControlValues( - story.parameters.controls, - propName, - propValue, - ); - story.parameters.controls = newValues; - context.channel.emit(FORCE_RE_RENDER); - context.channel.emit(SET_DATA_MSG, { - storyId, - controls: newValues, - }); - } - }; - const clickControl: ClickControlFn = api.clickControl - ? api.clickControl - : (storyId: string, propName: string) => { - if (controls && controls[propName]) { - const control: ComponentControlButton = controls[ - propName - ] as ComponentControlButton; - if (control && typeof control.onClick === 'function') { - control.onClick(control); + + {({ id, controls, story, api, channel }) => { + const setControlValue: SetControlValueFn = + api && api.setControlValue + ? api.setControlValue + : (storyId: string, propName: string | undefined, propValue: any) => { + if (story) { + const newValues = mergeControlValues( + story.parameters.controls, + propName, + propValue, + ); + story.parameters.controls = newValues; + channel.emit(FORCE_RE_RENDER); + channel.emit(SET_DATA_MSG, { + storyId, + controls: newValues, + }); + } + }; + const clickControl: ClickControlFn = + api && api.clickControl + ? api.clickControl + : (storyId: string, propName: string) => { + if (controls && controls[propName]) { + const control: ComponentControlButton = controls[ + propName + ] as ComponentControlButton; + if (control && typeof control.onClick === 'function') { + control.onClick(control); + } } - } - }; + }; + if (!story || controls.disable) { + return null; + } + return id ? ( = ({ /> ) : null; }} - + +); + +export const ControlsEditorsTable: FC = ({ title = 'Property Editors', ...rest }) => ( + + + ); diff --git a/integrations/storybook/src/blocks/Source.tsx b/integrations/storybook/src/blocks/Source.tsx new file mode 100644 index 000000000..4194198d7 --- /dev/null +++ b/integrations/storybook/src/blocks/Source.tsx @@ -0,0 +1,27 @@ +import React, { FC } from 'react'; +import { Source as SourceBlock } from '@component-controls/editors'; +import { + BlockContext, + BlockContextProvider, + BlockContextProviderProps, +} from './BlockContext'; + +export interface SourceProps { + /** a title to display */ + title?: string; +} + +const SourceConsumner: FC = () => ( + + {({ source }) => {source}} + +); + +export const Source: FC = ({ + title = 'Source code', + ...rest +}) => ( + + + +); diff --git a/integrations/storybook/src/blocks/index.ts b/integrations/storybook/src/blocks/index.ts index 72daa06fd..50fc10e57 100644 --- a/integrations/storybook/src/blocks/index.ts +++ b/integrations/storybook/src/blocks/index.ts @@ -1 +1,2 @@ export * from './ControlsEditorsTable'; +export * from './Source'; diff --git a/integrations/storybook/src/manager/Panel.tsx b/integrations/storybook/src/manager/Panel.tsx index 3e6bf7e71..06ea0f207 100644 --- a/integrations/storybook/src/manager/Panel.tsx +++ b/integrations/storybook/src/manager/Panel.tsx @@ -14,6 +14,7 @@ import { GET_ALL_STORIES, } from '../shared/shared'; import { ControlsTable } from '../shared/ControlsTable'; +import { ThemeProvider } from '../shared/ThemeProvider'; import { NoControls } from './NoControls'; interface StoryInput { @@ -68,12 +69,14 @@ const WrappedControlsTable: React.FC = ({ return story && controls && Object.keys(controls).length ? ( - + + + ) : ( diff --git a/integrations/storybook/src/shared/ControlsTable.tsx b/integrations/storybook/src/shared/ControlsTable.tsx index 941b3cde8..b115ee9f3 100644 --- a/integrations/storybook/src/shared/ControlsTable.tsx +++ b/integrations/storybook/src/shared/ControlsTable.tsx @@ -3,27 +3,25 @@ import { ControlsEditorsTable, ControlsEditorsTableProps, } from '@component-controls/editors'; -import { ThemeProvider } from './ThemeProvider'; + import { randomizeData } from '@component-controls/core'; export const ControlsTable: React.FC = props => ( - - { - if (state.setControlValue && state.controls && state.storyId) { - state.setControlValue( - state.storyId, - undefined, - randomizeData(state.controls), - ); - } - }, + { + if (state.setControlValue && state.controls && state.storyId) { + state.setControlValue( + state.storyId, + undefined, + randomizeData(state.controls), + ); + } }, - ]} - /> - + }, + ]} + /> ); diff --git a/integrations/storybook/src/shared/ThemeProvider.tsx b/integrations/storybook/src/shared/ThemeProvider.tsx index e73022329..ccb28cf04 100644 --- a/integrations/storybook/src/shared/ThemeProvider.tsx +++ b/integrations/storybook/src/shared/ThemeProvider.tsx @@ -1,5 +1,6 @@ import React from 'react'; import { polaris as theme } from '@theme-ui/presets'; +import prismTheme from '@theme-ui/prism/presets/dracula.json'; import { ThemeProvider as ThemeUIProvider } from 'theme-ui'; import { lighten } from 'polished'; @@ -7,8 +8,12 @@ export const ThemeProvider: React.FC = ({ children }) => (