playroom 0.34.2 → 0.36.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (38) hide show
  1. package/.github/workflows/preview-site.yml +3 -3
  2. package/.github/workflows/release.yml +3 -3
  3. package/.github/workflows/snapshot.yml +3 -3
  4. package/.github/workflows/validate.yml +5 -5
  5. package/.nvmrc +1 -1
  6. package/CHANGELOG.md +55 -0
  7. package/README.md +6 -0
  8. package/bin/cli.cjs +2 -1
  9. package/cypress/e2e/editor.cy.js +1 -1
  10. package/cypress/e2e/keymaps.cy.js +1507 -11
  11. package/cypress/e2e/scope.cy.js +1 -1
  12. package/cypress/e2e/smoke.cy.js +2 -2
  13. package/cypress/e2e/toolbar.cy.js +1 -2
  14. package/cypress/e2e/urlHandling.cy.js +4 -5
  15. package/cypress/support/utils.js +62 -54
  16. package/lib/makeWebpackConfig.js +0 -3
  17. package/lib/provideDefaultConfig.js +13 -2
  18. package/package.json +18 -17
  19. package/src/Playroom/CatchErrors/CatchErrors.tsx +5 -6
  20. package/src/Playroom/CodeEditor/CodeEditor.tsx +11 -0
  21. package/src/Playroom/CodeEditor/keymaps/comment.ts +326 -0
  22. package/src/Playroom/CodeEditor/keymaps/wrap.ts +4 -1
  23. package/src/Playroom/Frame.tsx +9 -5
  24. package/src/Playroom/FramesPanel/FramesPanel.css.ts +19 -0
  25. package/src/Playroom/FramesPanel/FramesPanel.tsx +89 -46
  26. package/src/Playroom/Preview.tsx +12 -3
  27. package/src/Playroom/PreviewPanel/PreviewPanel.tsx +1 -1
  28. package/src/Playroom/SettingsPanel/SettingsPanel.tsx +11 -7
  29. package/src/Playroom/Stack/Stack.css.ts +4 -35
  30. package/src/Playroom/Stack/Stack.tsx +2 -9
  31. package/src/Playroom/Toolbar/Toolbar.tsx +2 -2
  32. package/src/Playroom/sprinkles.css.ts +1 -0
  33. package/src/StoreContext/StoreContext.tsx +31 -6
  34. package/src/index.d.ts +2 -0
  35. package/src/utils/params.ts +5 -8
  36. package/src/utils/usePreviewUrl.ts +2 -1
  37. package/utils/index.d.ts +3 -0
  38. package/utils/index.js +21 -7
@@ -10,7 +10,7 @@ describe('useScope', () => {
10
10
  });
11
11
 
12
12
  it('works', () => {
13
- typeCode('{{}hello()} {{}world()}', { delay: 0 });
13
+ typeCode('{{}hello()} {{}world()}');
14
14
  assertFirstFrameContains('HELLO WORLD');
15
15
  });
16
16
  });
@@ -1,13 +1,13 @@
1
1
  import {
2
2
  assertPreviewContains,
3
- getFirstFrame,
3
+ getPreviewFrames,
4
4
  loadPlayroom,
5
5
  } from '../support/utils';
6
6
 
7
7
  describe('Smoke', () => {
8
8
  it('frames are interactive', () => {
9
9
  loadPlayroom();
10
- getFirstFrame().click('center');
10
+ getPreviewFrames().first().click('center');
11
11
  });
12
12
 
13
13
  it('preview mode loads correctly', () => {
@@ -4,7 +4,6 @@ import {
4
4
  assertPreviewContains,
5
5
  typeCode,
6
6
  gotoPreview,
7
- visit,
8
7
  loadPlayroom,
9
8
  } from '../support/utils';
10
9
 
@@ -33,7 +32,7 @@ describe('Toolbar', () => {
33
32
  it('copy to clipboard', () => {
34
33
  const copySpy = cy.spy();
35
34
 
36
- visit(
35
+ cy.visit(
37
36
  'http://localhost:9000/#?code=N4Igxg9gJgpiBcIA8AxCEB8r1YEIEMAnAei2LUyXJxAF8g'
38
37
  );
39
38
 
@@ -2,13 +2,12 @@ import {
2
2
  assertFirstFrameContains,
3
3
  assertCodePaneContains,
4
4
  assertFramesMatch,
5
- visit,
6
5
  } from '../support/utils';
7
6
 
8
7
  describe('URL handling', () => {
9
8
  describe('where paramType is hash', () => {
10
9
  it('code', () => {
11
- visit(
10
+ cy.visit(
12
11
  'http://localhost:9000/#?code=N4Igxg9gJgpiBcIA8AxCEB8r1YEIEMAnAei2LUyXJxAF8g'
13
12
  );
14
13
 
@@ -17,7 +16,7 @@ describe('URL handling', () => {
17
16
  });
18
17
 
19
18
  it('widths', () => {
20
- visit(
19
+ cy.visit(
21
20
  'http://localhost:9000/#?code=N4Ig7glgJgLgFgZxALgNoGYDsBWANJgNgA4BdAXyA'
22
21
  );
23
22
 
@@ -27,7 +26,7 @@ describe('URL handling', () => {
27
26
 
28
27
  describe('where paramType is search', () => {
29
28
  it('code', () => {
30
- visit(
29
+ cy.visit(
31
30
  'http://localhost:9001/index.html?code=N4Igxg9gJgpiBcIA8AxCEB8r1YEIEMAnAei2LUyXJxAF8g'
32
31
  );
33
32
 
@@ -36,7 +35,7 @@ describe('URL handling', () => {
36
35
  });
37
36
 
38
37
  it('widths', () => {
39
- visit(
38
+ cy.visit(
40
39
  'http://localhost:9001/index.html?code=N4Ig7glgJgLgFgZxALgNoGYDsBWANJgNgA4BdAXyA'
41
40
  );
42
41
 
@@ -5,38 +5,20 @@ import dedent from 'dedent';
5
5
  import { createUrl } from '../../utils';
6
6
  import { isMac } from '../../src/utils/formatting';
7
7
 
8
- const WAIT_FOR_FRAME_TO_RENDER = 1000;
9
-
10
- const getCodeEditor = () => cy.get('.CodeMirror-code');
8
+ const getCodeEditor = () =>
9
+ cy.get('.CodeMirror-code').then((editor) => cy.wrap(editor));
11
10
 
12
11
  export const getPreviewFrames = () => cy.get('[data-testid="previewFrame"]');
13
12
 
14
13
  export const getPreviewFrameNames = () => cy.get('[data-testid="frameName"]');
15
14
 
16
- export const getFirstFrame = () => getPreviewFrames().first();
17
-
18
- export const visit = (url) =>
19
- cy
20
- .visit(url)
21
- .reload()
22
- .then(() => {
23
- getFirstFrame().then(
24
- ($iframe) =>
25
- new Cypress.Promise((resolve) => $iframe.on('load', resolve))
26
- );
27
- });
28
-
29
- export const typeCode = (code, { delay = 200 } = {}) =>
30
- getCodeEditor()
31
- .focused()
32
- .type(code, { force: true, delay })
33
- .wait(WAIT_FOR_FRAME_TO_RENDER);
15
+ export const typeCode = (code, { delay } = {}) =>
16
+ getCodeEditor().focused().type(code, { delay });
34
17
 
35
18
  export const formatCode = () =>
36
19
  getCodeEditor()
37
20
  .focused()
38
- .type(`${isMac() ? '{cmd}' : '{ctrl}'}s`)
39
- .wait(WAIT_FOR_FRAME_TO_RENDER);
21
+ .type(`${isMac() ? '{cmd}' : '{ctrl}'}s`);
40
22
 
41
23
  export const selectWidthPreferenceByIndex = (index) =>
42
24
  cy
@@ -59,9 +41,7 @@ export const toggleSnippets = () =>
59
41
  cy.get('[data-testid="toggleSnippets"]').click();
60
42
 
61
43
  export const filterSnippets = (search) => {
62
- cy.get('[data-testid="filterSnippets"]').type(search, { force: true });
63
- // eslint-disable-next-line @finsit/cypress/no-unnecessary-waiting
64
- cy.wait(200);
44
+ cy.get('[data-testid="filterSnippets"]').type(search);
65
45
  };
66
46
 
67
47
  export const assertSnippetsListIsVisible = () =>
@@ -72,38 +52,71 @@ const getSnippets = () => cy.get('[data-testid="snippet-list"] li');
72
52
  export const selectSnippetByIndex = (index) => getSnippets().eq(index);
73
53
 
74
54
  export const mouseOverSnippet = (index) =>
75
- selectSnippetByIndex(index)
76
- .trigger('mousemove', { force: true }) // force stops cypress scrolling the panel out of the editor
77
- .wait(WAIT_FOR_FRAME_TO_RENDER);
55
+ // force stops cypress scrolling the panel out of the editor
56
+ selectSnippetByIndex(index).trigger('mousemove', { force: true });
78
57
 
79
58
  export const assertSnippetCount = (count) =>
80
59
  getSnippets().should('have.length', count);
81
60
 
82
- export const assertFirstFrameContains = (text) => {
83
- getFirstFrame().then(($el) =>
84
- // eslint-disable-next-line @finsit/cypress/no-unnecessary-waiting
85
- cy
86
- .wrap($el.contents().find('body'))
87
- .wait(WAIT_FOR_FRAME_TO_RENDER)
88
- .then((el) => {
89
- expect(el.get(0).innerText).to.eq(text);
90
- })
91
- );
92
- };
61
+ export const assertFirstFrameContains = (text) =>
62
+ getPreviewFrames()
63
+ .first()
64
+ .its('0.contentDocument.body')
65
+ .should((frameBody) => {
66
+ expect(frameBody.innerText).to.eq(text);
67
+ });
93
68
 
69
+ /**
70
+ * @param {number} numCharacters
71
+ */
94
72
  export const selectNextCharacters = (numCharacters) => {
95
73
  typeCode('{shift+rightArrow}'.repeat(numCharacters));
96
74
  };
97
75
 
76
+ /**
77
+ * @param {number} numWords
78
+ */
98
79
  export const selectNextWords = (numWords) => {
99
80
  const modifier = isMac() ? 'alt' : 'ctrl';
100
81
  typeCode(`{shift+${modifier}+rightArrow}`.repeat(numWords));
101
82
  };
102
83
 
84
+ export const selectToStartOfLine = () => {
85
+ typeCode(isMac() ? '{shift+cmd+leftArrow}' : '{shift+home}');
86
+ };
87
+
103
88
  export const selectToEndOfLine = () => {
104
89
  typeCode(isMac() ? '{shift+cmd+rightArrow}' : '{shift+end}');
105
90
  };
106
91
 
92
+ /**
93
+ * @param {number} x;
94
+ * @param {number | undefined} y
95
+ */
96
+ export const moveBy = (x, y = 0) => {
97
+ if (x) {
98
+ const xDirection = x >= 0 ? '{rightArrow}' : '{leftArrow}';
99
+ typeCode(xDirection.repeat(x));
100
+ }
101
+
102
+ if (y) {
103
+ const yDirection = y >= 0 ? '{downArrow}' : '{upArrow}';
104
+ typeCode(yDirection.repeat(y));
105
+ }
106
+ };
107
+
108
+ /**
109
+ * @param {number} numWords
110
+ */
111
+ export const moveByWords = (numWords) => {
112
+ const modifier = isMac() ? 'alt' : 'ctrl';
113
+ typeCode(`{${modifier}+rightArrow}`.repeat(numWords));
114
+ };
115
+
116
+ export const moveToEndOfLine = () => {
117
+ typeCode(isMac() ? '{cmd+rightArrow}' : '{end}');
118
+ };
119
+
107
120
  /**
108
121
  * @typedef {import('../../src/Playroom/CodeEditor/keymaps/types').Direction} Direction
109
122
  */
@@ -111,20 +124,22 @@ export const selectToEndOfLine = () => {
111
124
  * @param {number} numLines
112
125
  * @param {Direction} direction
113
126
  */
114
- export const selectLines = (numLines, direction = 'down') => {
127
+ export const selectNextLines = (numLines, direction = 'down') => {
115
128
  const arrowCode = direction === 'down' ? 'downArrow' : 'upArrow';
116
129
  typeCode(`{shift+${arrowCode}}`.repeat(numLines));
117
130
  };
118
131
 
119
132
  export const assertCodePaneContains = (text) => {
120
133
  getCodeEditor().within(() => {
134
+ // Accumulate text from individual line elements as they don't include line numbers
121
135
  const lines = [];
122
136
  cy.get('.CodeMirror-line').each(($el) => lines.push($el.text()));
137
+
123
138
  cy.then(() => {
124
- const code = lines.join('\n');
125
139
  // removes code mirrors invisible last line character placeholder
126
- // which is inserted to preserve prettiers new line at end of string.
127
- expect(code.replace(/[\u200b]$/, '')).to.eq(text);
140
+ // which is inserted to preserve prettier's new line at end of string.
141
+ const code = lines.join('\n').replace(/[\u200b]$/, '');
142
+ expect(code).to.equal(text);
128
143
  });
129
144
  });
130
145
  };
@@ -138,7 +153,7 @@ export const assertCodePaneLineCount = (lines) => {
138
153
  export const assertFramesMatch = (matches) =>
139
154
  getPreviewFrameNames()
140
155
  .should('have.length', matches.length)
141
- .then((frames) => {
156
+ .should((frames) => {
142
157
  const frameNames = frames.map((_, el) => el.innerText).toArray();
143
158
  return expect(frameNames).to.deep.equal(matches);
144
159
  });
@@ -149,7 +164,7 @@ export const assertPreviewContains = (text) =>
149
164
  cy.get('[data-testid="splashscreen"]').should('not.be.visible');
150
165
  })
151
166
  .get('body')
152
- .then((el) => {
167
+ .should((el) => {
153
168
  expect(el.get(0).innerText).to.eq(text);
154
169
  });
155
170
 
@@ -165,12 +180,5 @@ export const loadPlayroom = (initialCode) => {
165
180
  .then((win) => {
166
181
  const { storageKey } = win.__playroomConfig__;
167
182
  indexedDB.deleteDatabase(storageKey);
168
- })
169
- .reload()
170
- .then(() =>
171
- getFirstFrame().then(
172
- ($iframe) =>
173
- new Cypress.Promise((resolve) => $iframe.on('load', resolve))
174
- )
175
- );
183
+ });
176
184
  };
@@ -74,9 +74,6 @@ module.exports = async (playroomConfig, options) => {
74
74
  },
75
75
  },
76
76
  module: {
77
- // This option fixes https://github.com/prettier/prettier/issues/4959
78
- // Once this issue is fixed, we can remove this line:
79
- exprContextCritical: false,
80
77
  rules: [
81
78
  {
82
79
  test: /\.(ts|tsx)$/,
@@ -1,10 +1,21 @@
1
1
  const readPackage = require('read-pkg-up');
2
- const currentGitBranch = require('current-git-branch');
2
+ const { execSync } = require('child_process');
3
+
4
+ /**
5
+ * @returns {string | null} The current git branch name, or null if no branch is found
6
+ */
7
+ const getGitBranch = () => {
8
+ try {
9
+ return execSync('git branch --show-current').toString().trim();
10
+ } catch (e) {
11
+ return null;
12
+ }
13
+ };
3
14
 
4
15
  const generateStorageKey = () => {
5
16
  const pkg = readPackage.sync();
6
17
  const packageName = (pkg && pkg.packageJson && pkg.packageJson.name) || null;
7
- const branchName = currentGitBranch();
18
+ const branchName = getGitBranch();
8
19
 
9
20
  const packageLabel = packageName ? `package:${packageName}` : null;
10
21
  const branchLabel = branchName ? `branch:${branchName}` : null;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "playroom",
3
- "version": "0.34.2",
3
+ "version": "0.36.0",
4
4
  "description": "Design with code, powered by your own component library",
5
5
  "main": "utils/index.js",
6
6
  "types": "utils/index.d.ts",
@@ -34,7 +34,6 @@
34
34
  "@soda/friendly-errors-webpack-plugin": "^1.8.1",
35
35
  "@types/base64-url": "^2.2.0",
36
36
  "@types/codemirror": "^5.60.5",
37
- "@types/dedent": "^0.7.0",
38
37
  "@types/lodash": "^4.14.191",
39
38
  "@types/lz-string": "^1.3.34",
40
39
  "@types/prettier": "^2.7.1",
@@ -43,7 +42,7 @@
43
42
  "@vanilla-extract/css": "^1.9.2",
44
43
  "@vanilla-extract/css-utils": "^0.1.3",
45
44
  "@vanilla-extract/sprinkles": "^1.5.1",
46
- "@vanilla-extract/webpack-plugin": "2.1.12",
45
+ "@vanilla-extract/webpack-plugin": "^2.3.6",
47
46
  "babel-loader": "^9.1.0",
48
47
  "classnames": "^2.3.2",
49
48
  "codemirror": "^5.65.10",
@@ -51,8 +50,7 @@
51
50
  "command-line-usage": "^6.1.3",
52
51
  "copy-to-clipboard": "^3.3.3",
53
52
  "css-loader": "^6.7.2",
54
- "current-git-branch": "^1.1.0",
55
- "dedent": "^0.7.0",
53
+ "dedent": "^1.5.1",
56
54
  "fast-glob": "^3.2.12",
57
55
  "find-up": "^5.0.0",
58
56
  "fuzzy": "^0.1.3",
@@ -69,17 +67,17 @@
69
67
  "portfinder": "^1.0.32",
70
68
  "prettier": "^2.8.1",
71
69
  "prop-types": "^15.8.1",
72
- "query-string": "^7.1.3",
73
70
  "re-resizable": "^6.9.9",
74
71
  "react-docgen-typescript": "^2.2.2",
72
+ "react-helmet": "^6.1.0",
75
73
  "react-use": "^17.4.0",
76
74
  "read-pkg-up": "^7.0.1",
77
75
  "scope-eval": "^1.0.0",
78
76
  "sucrase": "^3.34.0",
79
77
  "typescript": ">=5.0.0",
80
- "use-debounce": "^9.0.4",
78
+ "use-debounce": "^10.0.0",
81
79
  "webpack": "^5.75.0",
82
- "webpack-dev-server": "^4.11.1",
80
+ "webpack-dev-server": "^5.0.2",
83
81
  "webpack-merge": "^5.8.0"
84
82
  },
85
83
  "devDependencies": {
@@ -87,13 +85,13 @@
87
85
  "@changesets/cli": "^2.25.2",
88
86
  "@octokit/rest": "^19.0.5",
89
87
  "@types/jest": "^29.2.4",
90
- "concurrently": "^7.6.0",
91
- "cypress": "^12.0.2",
88
+ "@types/react-helmet": "^6.1.6",
89
+ "cypress": "^13.6.6",
92
90
  "eslint": "^8.44.0",
93
91
  "eslint-config-seek": "^11.3.1",
94
92
  "husky": "^8.0.2",
95
93
  "jest": "^29.3.1",
96
- "lint-staged": "^13.1.0",
94
+ "lint-staged": "^15.2.2",
97
95
  "react": "^18.0.1",
98
96
  "react-dom": "^18.0.1",
99
97
  "serve": "^14.1.2",
@@ -104,9 +102,12 @@
104
102
  "react": "^17 || ^18",
105
103
  "react-dom": "^17 || ^18"
106
104
  },
107
- "packageManager": "pnpm@7.18.1",
105
+ "engines": {
106
+ "node": ">=18.12.0"
107
+ },
108
+ "packageManager": "pnpm@8.15.3",
108
109
  "volta": {
109
- "node": "16.13.0"
110
+ "node": "18.19.1"
110
111
  },
111
112
  "scripts": {
112
113
  "cypress": "start-server-and-test build-and-serve:all '9000|9001|9002' 'cypress run'",
@@ -121,11 +122,11 @@
121
122
  "start:typescript": "./bin/cli.cjs start --config cypress/projects/typescript/playroom.config.js",
122
123
  "build:typescript": "./bin/cli.cjs build --config cypress/projects/typescript/playroom.config.js",
123
124
  "serve:typescript": "PORT=9002 serve --no-request-logging cypress/projects/typescript/dist",
124
- "start:all": "concurrently 'npm:start:*(!all)'",
125
- "build:all": "concurrently 'npm:build:*(!all)'",
126
- "serve:all": "concurrently 'npm:serve:*(!all)'",
125
+ "start:all": "pnpm run '/^start:(?!all)/'",
126
+ "build:all": "pnpm run '/^build:(?!all)/'",
127
+ "serve:all": "pnpm run '/^serve:(?!all)/'",
127
128
  "build-and-serve:all": "pnpm build:all && pnpm serve:all",
128
- "lint": "concurrently 'npm:lint:*'",
129
+ "lint": "pnpm run '/^lint:.*/'",
129
130
  "lint:eslint": "eslint --cache .",
130
131
  "lint:prettier": "prettier --list-different '**/*.{js,md,ts,tsx}'",
131
132
  "lint:tsc": "tsc --noEmit",
@@ -34,12 +34,11 @@ export default class CatchErrors extends Component<Props, State> {
34
34
  }
35
35
 
36
36
  // Ensure the stack only contains user-provided components
37
- const componentStack = errorInfo
38
- ? errorInfo.componentStack
39
- .split('\n')
40
- .filter((line: string) => /RenderCode/.test(line))
41
- .map((line: string) => line.replace(/ \(created by .*/g, ''))
42
- : [];
37
+ const componentStack =
38
+ errorInfo?.componentStack
39
+ ?.split('\n')
40
+ .filter((line: string) => /RenderCode/.test(line))
41
+ .map((line: string) => line.replace(/ \(created by .*/g, '')) ?? [];
43
42
 
44
43
  // Ignore the RenderCode container component
45
44
  const lines = componentStack.slice(0, componentStack.length - 1);
@@ -36,6 +36,7 @@ import {
36
36
  selectNextOccurrence,
37
37
  } from './keymaps/cursors';
38
38
  import { wrapInTag } from './keymaps/wrap';
39
+ import { toggleComment } from './keymaps/comment';
39
40
 
40
41
  const validateCodeInEditor = (editorInstance: Editor, code: string) => {
41
42
  const maybeValid = validateCode(code);
@@ -257,6 +258,16 @@ export const CodeEditor = ({ code, onChange, previewCode, hints }: Props) => {
257
258
  [`${keymapModifierKey}-Alt-Down`]: addCursorToNextLine,
258
259
  [`${keymapModifierKey}-D`]: selectNextOccurrence,
259
260
  [`Shift-${keymapModifierKey}-,`]: wrapInTag,
261
+ [`${keymapModifierKey}-/`]: toggleComment,
262
+ [`Shift-${keymapModifierKey}-C`]: () => {
263
+ dispatch({
264
+ type: 'copyToClipboard',
265
+ payload: {
266
+ url: window.location.href,
267
+ trigger: 'toolbarItem',
268
+ },
269
+ });
270
+ },
260
271
  },
261
272
  }}
262
273
  />