Skip to content
Permalink
Browse files

feat: Add support for inserting and previewing snippets (#117)

  • Loading branch information
michaeltaranto committed Feb 4, 2020
1 parent b67c7e2 commit 1477830430d3b08704b69d410f26c93654276804
Showing with 4,806 additions and 3,551 deletions.
  1. +20 −0 README.md
  2. +160 −55 cypress/integration/smokeTest.js
  3. +14 −5 cypress/projects/basic/components.js
  4. +1 −0 cypress/projects/basic/playroom.config.js
  5. +22 −0 cypress/projects/basic/snippets.js
  6. +7 −1 cypress/support/index.js
  7. +60 −17 cypress/support/utils.js
  8. +13 −3 examples/typescript/components/Bar/Bar.tsx
  9. +12 −3 examples/typescript/components/Foo/Foo.tsx
  10. +2 −1 examples/typescript/package.json
  11. +2 −1 examples/typescript/playroom.config.js
  12. +108 −0 examples/typescript/snippets/index.ts
  13. +675 −614 examples/typescript/yarn.lock
  14. +1 −0 lib/defaultModules/snippets.js
  15. +3 −0 lib/makeWebpackConfig.js
  16. +41 −39 package.json
  17. +1 −1 src/Playroom/CatchErrors/CatchErrors.less
  18. +0 −164 src/Playroom/CodeEditor/CodeEditor.js
  19. +7 −0 src/Playroom/CodeEditor/CodeEditor.less
  20. +263 −0 src/Playroom/CodeEditor/CodeEditor.tsx
  21. +10 −4 src/Playroom/Playroom.tsx
  22. +1 −1 src/Playroom/Preview/Preview.less
  23. +1 −1 src/Playroom/Preview/Preview.tsx
  24. +18 −0 src/Playroom/Snippets/SearchField/SearchField.less
  25. +38 −0 src/Playroom/Snippets/SearchField/SearchField.tsx
  26. +71 −0 src/Playroom/Snippets/Snippets.less
  27. +194 −0 src/Playroom/Snippets/Snippets.tsx
  28. +1 −1 src/Playroom/Toolbar/Toolbar.less
  29. +117 −122 src/Playroom/Toolbar/Toolbar.tsx
  30. +14 −0 src/Playroom/Toolbar/icons/AddSvg.tsx
  31. +11 −1 src/Playroom/ToolbarItem/ToolbarItem.less
  32. +5 −0 src/Playroom/ToolbarItem/ToolbarItem.tsx
  33. +4 −0 src/Playroom/ViewPreference/ViewPreference.less
  34. +4 −1 src/Playroom/ViewPreference/ViewPreference.tsx
  35. +27 −2 src/Playroom/variables.less
  36. +129 −5 src/StoreContext/StoreContext.tsx
  37. +1 −0 src/index.d.ts
  38. +16 −2 src/index.js
  39. +2 −0 src/snippets.js
  40. +10 −1 src/utils/compileJsx.ts
  41. +56 −0 src/utils/cursor.spec.ts
  42. +36 −0 src/utils/cursor.ts
  43. +0 −78 src/utils/formatting.js
  44. +0 −72 src/utils/formatting.spec.js
  45. +135 −0 src/utils/formatting.spec.ts
  46. +158 −0 src/utils/formatting.ts
  47. +8 −0 utils/index.d.ts
  48. +2,327 −2,356 yarn.lock
@@ -52,6 +52,7 @@ module.exports = {
// Optional:
title: 'My Awesome Library',
themes: './src/themes',
snippets: './playroom/snippets.js',
frameComponent: './playroom/FrameComponent.js',
widths: [320, 375, 768, 1024],
port: 9000,
@@ -89,6 +90,25 @@ To build your assets for production:
$ npm run playroom:build
```

## Snippets

Playroom allows you to quickly insert predefined snippets of code, providing live previews across themes and viewports as you navigate the list. These snippets can be configured via a `snippets` file that looks like this:

```js
export default [
{
group: 'Button',
name: 'Strong',
code: `
<Button weight="strong">
Button
</Button>
`
},
...
];
```

## Custom Frame Component

If your components need to be nested within custom provider components, you can provide a custom React component file via the `frameComponent` option, which is a path to a file that exports a component. For example, if your component library has multiple themes:
@@ -1,3 +1,4 @@
import dedent from 'dedent';
import {
formatCode,
typeCode,
@@ -6,7 +7,14 @@ import {
assertCodePaneContains,
assertCodePaneLineCount,
assertFramesMatch,
selectWidthPreferenceByIndex
selectWidthPreferenceByIndex,
selectSnippetByIndex,
filterSnippets,
toggleSnippets,
assertSnippetCount,
assertSnippetsListIsVisible,
mouseOverSnippet,
visit
} from '../support/utils';

describe('Smoke test', () => {
@@ -15,7 +23,7 @@ describe('Smoke test', () => {
assertFirstFrameContains('Foo');

typeCode('<Bar />');
assertFirstFrameContains('Bar');
assertFirstFrameContains('Foo\nBar');
});

it('autocompletes', () => {
@@ -25,75 +33,172 @@ describe('Smoke test', () => {
});

it('formats', () => {
cy.visit('http://localhost:9000/').reload();

typeCode('<Foo><Foo><Bar/>');
assertCodePaneLineCount(1);
formatCode();
assertCodePaneLineCount(6);
});

it('url handling - code (base64 encoding)', () => {
cy.visit(
'http://localhost:9000/#?code=PEZvbz48Rm9vPjxCYXIvPjwvRm9vPjwvRm9vPg'
).reload();

assertFirstFrameContains('Foo');
assertCodePaneContains('<Foo><Foo><Bar/></Foo></Foo>');
it('frames are interactive', () => {
getFirstFrame().click('center');
});

it('url handling - code (LZ-based compression)', () => {
cy.visit(
'http://localhost:9000/#?code=N4Igxg9gJgpiBcIA8AxCEB8r1YEIEMAnAei2LUyXJxAF8g'
).reload();

assertFirstFrameContains('Foo');
assertCodePaneContains('<Foo><Foo><Bar/></Foo></Foo>');
});
describe('url handling', () => {
it('code (base64 encoding)', () => {
visit(
'http://localhost:9000/#?code=PEZvbz48Rm9vPjxCYXIvPjwvRm9vPjwvRm9vPg'
);

it('url handling - widths', () => {
cy.visit(
'http://localhost:9000/#?code=N4Ig7glgJgLgFgZxALgNoGYDsBWANJgNgA4BdAXyA'
).reload();
assertFirstFrameContains('Foo\nFoo\nBar');
assertCodePaneContains('<Foo><Foo><Bar/></Foo></Foo>');
});

assertFramesMatch(['375px', '768px']);
});
it('code (LZ-based compression)', () => {
visit(
'http://localhost:9000/#?code=N4Igxg9gJgpiBcIA8AxCEB8r1YEIEMAnAei2LUyXJxAF8g'
);

it('toolbar - filter widths', () => {
cy.visit('http://localhost:9000/').reload();
assertFirstFrameContains('Foo\nFoo\nBar');
assertCodePaneContains('<Foo><Foo><Bar/></Foo></Foo>');
});

const frames = ['320px', '375px', '768px', '1024px'];
const widthIndexToSelect = 1;
it('widths', () => {
visit(
'http://localhost:9000/#?code=N4Ig7glgJgLgFgZxALgNoGYDsBWANJgNgA4BdAXyA'
);

assertFramesMatch(frames);
selectWidthPreferenceByIndex(widthIndexToSelect);
assertFramesMatch([frames[widthIndexToSelect]]);
assertFramesMatch(['375px', '768px']);
});
});

it('toolbar - copy to clipboard', () => {
const copySpy = cy.spy();

cy.visit(
'http://localhost:9000/#?code=N4Igxg9gJgpiBcIA8AxCEB8r1YEIEMAnAei2LUyXJxAF8g'
).reload();

cy.document()
.then(doc => {
cy.stub(doc, 'execCommand', args => {
if (args === 'copy') {
copySpy();
return true;
}
});
})
.get('[data-testid="copyToClipboard"]')
.click()
.then(() => expect(copySpy).to.have.been.called);
describe('toolbar', () => {
it('filter widths', () => {
const frames = ['320px', '375px', '768px', '1024px'];
const widthIndexToSelect = 1;

assertFramesMatch(frames);
selectWidthPreferenceByIndex(widthIndexToSelect);
assertFramesMatch([frames[widthIndexToSelect]]);
});

it('copy to clipboard', () => {
const copySpy = cy.spy();

visit(
'http://localhost:9000/#?code=N4Igxg9gJgpiBcIA8AxCEB8r1YEIEMAnAei2LUyXJxAF8g'
);

cy.document()
.then(doc => {
cy.stub(doc, 'execCommand', args => {
if (args === 'copy') {
copySpy();
return true;
}
});
})
.get('[data-testid="copyToClipboard"]')
.click()
.then(() => expect(copySpy).to.have.been.called);
});
});

it('frames are interactive', () => {
cy.visit('http://localhost:9000/').reload();

getFirstFrame().click('center');
describe('snippets', () => {
beforeEach(() => typeCode('<div>Initial <span>code'));

it('driven with mouse', () => {
// Open and format for insertion point
toggleSnippets();
assertSnippetsListIsVisible();
assertCodePaneLineCount(8);

// Browse snippetlist
assertSnippetCount(4);
mouseOverSnippet(0);
assertFirstFrameContains('Initial code\nFoo\nFoo');
mouseOverSnippet(1);
assertFirstFrameContains('Initial code\nFoo\nRed Foo');
mouseOverSnippet(2);
assertFirstFrameContains('Initial code\nBar\nBar');

// Close without persisting
toggleSnippets();
assertCodePaneContains('<div>Initial <span>code</span></div>');
assertCodePaneLineCount(1);

// Re-open and persist
toggleSnippets();
mouseOverSnippet(3);
assertFirstFrameContains('Initial code\nBar\nBlue Bar');
selectSnippetByIndex(3).click();
assertFirstFrameContains('Initial code\nBar\nBlue Bar');
assertCodePaneLineCount(7);
assertCodePaneContains(dedent`
<div>
Initial{" "}
<span>
code<Bar color="blue">Blue Bar</Bar>
</span>
</div>\n
`);
typeCode('cursor position');
assertCodePaneContains(dedent`
<div>
Initial{" "}
<span>
code<Bar color="blue">Blue Bar</Bar>cursor position
</span>
</div>\n
`);
});

it('driven with keyboard', () => {
// Open and format for insertion point
typeCode(`${navigator.platform.match('Mac') ? '{cmd}' : '{ctrl}'}k`);
assertSnippetsListIsVisible();
assertCodePaneLineCount(8);
filterSnippets('{esc}');
assertCodePaneLineCount(1);
typeCode(`${navigator.platform.match('Mac') ? '{cmd}' : '{ctrl}'}k`);
assertSnippetsListIsVisible();
assertCodePaneLineCount(8);

// Browse snippetlist
assertSnippetCount(4);
filterSnippets('{downarrow}');
assertFirstFrameContains('Initial code\nFoo\nFoo');
filterSnippets('{downarrow}');
assertFirstFrameContains('Initial code\nFoo\nRed Foo');
filterSnippets('{downarrow}');
assertFirstFrameContains('Initial code\nBar\nBar');

// Close without persisting
filterSnippets('{esc}');
assertCodePaneContains('<div>Initial <span>code</span></div>');
assertCodePaneLineCount(1);

// Re-open and persist
typeCode(`${navigator.platform.match('Mac') ? '{cmd}' : '{ctrl}'}k`);
filterSnippets('{downarrow}{downarrow}{downarrow}{downarrow}{enter}');
assertFirstFrameContains('Initial code\nBar\nBlue Bar');
assertCodePaneLineCount(7);
assertCodePaneContains(dedent`
<div>
Initial{" "}
<span>
code<Bar color="blue">Blue Bar</Bar>
</span>
</div>\n
`);
typeCode('cursor position');
assertCodePaneContains(dedent`
<div>
Initial{" "}
<span>
code<Bar color="blue">Blue Bar</Bar>cursor position
</span>
</div>\n
`);
});
});
});
@@ -3,16 +3,25 @@ import PropTypes from 'prop-types';

const withPropTypes = component => {
component.propTypes = {
color: PropTypes.oneOf(['red', 'blue'])
color: PropTypes.oneOf(['red', 'blue']),
children: PropTypes.node
};

return component;
};
const parent = {
border: '1px solid currentColor',
padding: '10px 10px 10px 15px'
};

export const Foo = withPropTypes(({ color }) => (
<div style={{ color }}>Foo</div>
export const Foo = withPropTypes(({ color = 'black', children }) => (
<div style={{ color }}>
Foo{children ? <div style={parent}>{children}</div> : null}
</div>
));

export const Bar = withPropTypes(({ color }) => (
<div style={{ color }}>Bar</div>
export const Bar = withPropTypes(({ color = 'black', children }) => (
<div style={{ color }}>
Bar{children ? <div style={parent}>{children}</div> : null}
</div>
));
@@ -1,5 +1,6 @@
module.exports = {
components: './components',
snippets: './snippets',
outputPath: './dist',
openBrowser: false
};
@@ -0,0 +1,22 @@
export default [
{
group: 'Foo',
name: 'Default',
code: '<Foo>Foo</Foo>'
},
{
group: 'Foo',
name: 'Red',
code: '<Foo color="red">Red Foo</Foo>'
},
{
group: 'Bar',
name: 'Default',
code: '<Bar>Bar</Bar>'
},
{
group: 'Bar',
name: 'Blue',
code: '<Bar color="blue">Blue Bar</Bar>'
}
];
@@ -1,4 +1,5 @@
require('./commands');
const { getFirstFrame } = require('./utils');

beforeEach(() => {
cy.visit('http://localhost:9000')
@@ -7,5 +8,10 @@ beforeEach(() => {
const { storageKey } = win.__playroomConfig__;
indexedDB.deleteDatabase(storageKey);
})
.visit('http://localhost:9000');
.reload()
.then(() => {
getFirstFrame().then(
$iframe => new Cypress.Promise(resolve => $iframe.on('load', resolve))
);
});
});

0 comments on commit 1477830

Please sign in to comment.
You can’t perform that action at this time.