playroom 0.30.0 → 0.31.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.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,16 @@
1
1
  # playroom
2
2
 
3
+ ## 0.31.0
4
+
5
+ ### Minor Changes
6
+
7
+ - 8ce01ff: Add keyboard shortcuts legend to the settings panel, to help with discoverability.
8
+ - 8ce01ff: Adds keybinding for wrapping the current selection in a tag.
9
+
10
+ Pressing <kbd><kbd>Cmd</kbd>+<kbd>Shift</kbd>+<kbd>,</kbd></kbd> (or, on Windows, <kbd><kbd>Ctrl</kbd>+<kbd>Shift</kbd>+<kbd>,</kbd></kbd>) will wrap the currently selected text in an empty fragment that is ready to be typed in.
11
+
12
+ Works for single cursors (doesn't wrap anything), single line selections, multi-line selections, and multiple cursors.
13
+
3
14
  ## 0.30.0
4
15
 
5
16
  ### Minor Changes
@@ -6,29 +6,22 @@ import {
6
6
  selectNextWords,
7
7
  selectLines,
8
8
  selectNextCharacters,
9
- isMac,
9
+ selectToEndOfLine,
10
10
  } from '../support/utils';
11
+ import { isMac } from '../../src/utils/formatting';
11
12
 
12
13
  const cmdPlus = (keyCombo) => {
13
14
  const platformSpecificKey = isMac() ? 'cmd' : 'ctrl';
14
15
  return `${platformSpecificKey}+${keyCombo}`;
15
16
  };
16
17
 
17
- const moveToStart = isMac() ? '{cmd+upArrow}' : '{ctrl+home}';
18
-
19
18
  describe('Keymaps', () => {
20
19
  beforeEach(() => {
21
- loadPlayroom();
22
-
23
- // The last closing div is automatically inserted by autotag
24
- typeCode(dedent`
20
+ loadPlayroom(`
25
21
  <div>First line</div>
26
22
  <div>Second line</div>
27
- <div>Third line
23
+ <div>Third line</div>
28
24
  `);
29
-
30
- // Reset the cursor to a reliable position at the beginning
31
- typeCode(moveToStart);
32
25
  });
33
26
 
34
27
  describe('swapLine', () => {
@@ -160,6 +153,19 @@ describe('Keymaps', () => {
160
153
  <span>Third line</span>
161
154
  `);
162
155
  });
156
+
157
+ it("should select next occurrence in whole word mode when there's no selection", () => {
158
+ typeCode('{rightArrow}'.repeat(3));
159
+
160
+ typeCode(`{${cmdPlusD}}`.repeat(2));
161
+ typeCode('span');
162
+
163
+ assertCodePaneContains(dedent`
164
+ <span>First line</span>
165
+ <div>Second line</div>
166
+ <div>Third line</div>
167
+ `);
168
+ });
163
169
  });
164
170
 
165
171
  describe('addCursor', () => {
@@ -182,4 +188,115 @@ describe('Keymaps', () => {
182
188
  `);
183
189
  });
184
190
  });
191
+
192
+ describe('wrapTag', () => {
193
+ const modifierKey = isMac() ? 'cmd' : 'ctrl';
194
+
195
+ it("should insert a fragment with cursors when there's no selection", () => {
196
+ typeCode(`{shift+${modifierKey}+,}`);
197
+ typeCode('a');
198
+
199
+ assertCodePaneContains(dedent`
200
+ <a></a><div>First line</div>
201
+ <div>Second line</div>
202
+ <div>Third line</div>
203
+ `);
204
+ });
205
+
206
+ it('should wrap the selection when there is one', () => {
207
+ selectToEndOfLine();
208
+
209
+ typeCode(`{shift+${modifierKey}+,}`);
210
+ typeCode('span');
211
+
212
+ assertCodePaneContains(dedent`
213
+ <span><div>First line</div></span>
214
+ <div>Second line</div>
215
+ <div>Third line</div>
216
+ `);
217
+ });
218
+
219
+ it('should wrap a multi-line selection', () => {
220
+ typeCode('{shift+downArrow}');
221
+ selectToEndOfLine();
222
+
223
+ typeCode(`{shift+${modifierKey}+,}`);
224
+ typeCode('span');
225
+
226
+ assertCodePaneContains(dedent`
227
+ <span>
228
+ <div>First line</div>
229
+ <div>Second line</div>
230
+ </span>
231
+ <div>Third line</div>
232
+ `);
233
+ });
234
+
235
+ it('should wrap a multi-line selection when selected from a different indent level', () => {
236
+ // This is a replay of the previous test, to give us an indent level
237
+ typeCode('{shift+downArrow}');
238
+ selectToEndOfLine();
239
+
240
+ typeCode(`{shift+${modifierKey}+,}`);
241
+ typeCode('span');
242
+
243
+ // Return to the start
244
+ const moveToStart = isMac() ? '{cmd+upArrow}' : '{ctrl+home}';
245
+ typeCode(moveToStart);
246
+
247
+ // Select from the far left and try wrap
248
+ typeCode('{downArrow}');
249
+ typeCode('{shift+downArrow}'.repeat(2));
250
+
251
+ typeCode(`{shift+${modifierKey}+,}`);
252
+ typeCode('a');
253
+
254
+ assertCodePaneContains(dedent`
255
+ <span>
256
+ <a>
257
+ <div>First line</div>
258
+ <div>Second line</div>
259
+ </a>
260
+ </span>
261
+ <div>Third line</div>
262
+ `);
263
+ });
264
+
265
+ it('should wrap a multi-cursor single-line selection', () => {
266
+ typeCode(`{${modifierKey}+alt+downArrow}`);
267
+ selectToEndOfLine();
268
+
269
+ typeCode(`{shift+${modifierKey}+,}`);
270
+ typeCode('span');
271
+
272
+ assertCodePaneContains(dedent`
273
+ <span><div>First line</div></span>
274
+ <span><div>Second line</div></span>
275
+ <div>Third line</div>
276
+ `);
277
+ });
278
+
279
+ it('should wrap a multi-cursor multi-line selection', () => {
280
+ typeCode(`{${modifierKey}+alt+downArrow}`);
281
+ typeCode('{shift+alt+downArrow}{upArrow}');
282
+
283
+ selectLines(1);
284
+ selectToEndOfLine();
285
+
286
+ typeCode(`{shift+${modifierKey}+,}`);
287
+ typeCode('span');
288
+
289
+ assertCodePaneContains(dedent`
290
+ <span>
291
+ <div>First line</div>
292
+ <div>First line</div>
293
+ </span>
294
+ <span>
295
+ <div>Second line</div>
296
+ <div>Second line</div>
297
+ </span>
298
+ <div>Third line</div>
299
+ `);
300
+ });
301
+ });
185
302
  });
@@ -11,8 +11,8 @@ import {
11
11
  assertSnippetsListIsVisible,
12
12
  mouseOverSnippet,
13
13
  loadPlayroom,
14
- isMac,
15
14
  } from '../support/utils';
15
+ import { isMac } from '../../src/utils/formatting';
16
16
 
17
17
  describe('Snippets', () => {
18
18
  beforeEach(() => {
@@ -1,3 +1,10 @@
1
+ // eslint-disable-next-line spaced-comment
2
+ /// <reference types="cypress" />
3
+ import dedent from 'dedent';
4
+
5
+ import { createUrl } from '../../utils';
6
+ import { isMac } from '../../src/utils/formatting';
7
+
1
8
  const WAIT_FOR_FRAME_TO_RENDER = 1000;
2
9
 
3
10
  const getCodeEditor = () => cy.get('.CodeMirror-code');
@@ -8,8 +15,6 @@ export const getPreviewFrameNames = () => cy.get('[data-testid="frameName"]');
8
15
 
9
16
  export const getFirstFrame = () => getPreviewFrames().first();
10
17
 
11
- export const isMac = () => navigator.platform.match('Mac');
12
-
13
18
  export const visit = (url) =>
14
19
  cy
15
20
  .visit(url)
@@ -96,6 +101,10 @@ export const selectNextWords = (numWords) => {
96
101
  typeCode(`{shift+${modifier}+rightArrow}`.repeat(numWords));
97
102
  };
98
103
 
104
+ export const selectToEndOfLine = () => {
105
+ typeCode(isMac() ? '{shift+cmd+rightArrow}' : '{shift+end}');
106
+ };
107
+
99
108
  /**
100
109
  * @typedef {import('../../src/Playroom/CodeEditor/keymaps/types').Direction} Direction
101
110
  */
@@ -146,9 +155,14 @@ export const assertPreviewContains = (text) =>
146
155
  expect(el.get(0).innerText).to.eq(text);
147
156
  });
148
157
 
149
- export const loadPlayroom = () =>
150
- cy
151
- .visit('http://localhost:9000')
158
+ export const loadPlayroom = (initialCode) => {
159
+ const baseUrl = 'http://localhost:9000';
160
+ const visitUrl = initialCode
161
+ ? createUrl({ baseUrl, code: dedent(initialCode) })
162
+ : baseUrl;
163
+
164
+ return cy
165
+ .visit(visitUrl)
152
166
  .window()
153
167
  .then((win) => {
154
168
  const { storageKey } = win.__playroomConfig__;
@@ -161,3 +175,4 @@ export const loadPlayroom = () =>
161
175
  new Cypress.Promise((resolve) => $iframe.on('load', resolve))
162
176
  )
163
177
  );
178
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "playroom",
3
- "version": "0.30.0",
3
+ "version": "0.31.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",
@@ -5,7 +5,7 @@ import 'codemirror/lib/codemirror.css';
5
5
  import 'codemirror/theme/neo.css';
6
6
 
7
7
  import { StoreContext, CursorPosition } from '../../StoreContext/StoreContext';
8
- import { formatCode as format } from '../../utils/formatting';
8
+ import { formatCode as format, isMac } from '../../utils/formatting';
9
9
  import {
10
10
  closeFragmentTag,
11
11
  compileJsx,
@@ -36,6 +36,7 @@ import {
36
36
  addCursorToPrevLine,
37
37
  selectNextOccurrence,
38
38
  } from './keymaps/cursors';
39
+ import { wrapInTag } from './keymaps/wrap';
39
40
 
40
41
  const validateCode = (editorInstance: Editor, code: string) => {
41
42
  editorInstance.clearGutter('errorGutter');
@@ -109,11 +110,9 @@ export const CodeEditor = ({ code, onChange, previewCode, hints }: Props) => {
109
110
  useEffect(() => {
110
111
  const handleKeyDown = (e: KeyboardEvent) => {
111
112
  if (editorInstanceRef && editorInstanceRef.current) {
112
- const cmdOrCtrl = navigator.platform.match('Mac')
113
- ? e.metaKey
114
- : e.ctrlKey;
113
+ const cmdOrCtrl = isMac() ? e.metaKey : e.ctrlKey;
115
114
 
116
- if (cmdOrCtrl && e.keyCode === 83) {
115
+ if (cmdOrCtrl && e.key === 's') {
117
116
  e.preventDefault();
118
117
  const { code: formattedCode, cursor: formattedCursor } = format({
119
118
  code: editorInstanceRef.current.getValue(),
@@ -128,7 +127,7 @@ export const CodeEditor = ({ code, onChange, previewCode, hints }: Props) => {
128
127
  editorInstanceRef.current.setCursor(formattedCursor);
129
128
  }
130
129
 
131
- if (cmdOrCtrl && /^[k]$/.test(e.key)) {
130
+ if (cmdOrCtrl && e.key === 'k') {
132
131
  e.preventDefault();
133
132
  dispatch({ type: 'toggleToolbar', payload: { panel: 'snippets' } });
134
133
  }
@@ -204,7 +203,7 @@ export const CodeEditor = ({ code, onChange, previewCode, hints }: Props) => {
204
203
  }
205
204
  }, [highlightLineNumber]);
206
205
 
207
- const keymapModifierKey = navigator.platform.match('Mac') ? 'Cmd' : 'Ctrl';
206
+ const keymapModifierKey = isMac() ? 'Cmd' : 'Ctrl';
208
207
 
209
208
  return (
210
209
  <ReactCodeMirror
@@ -272,6 +271,7 @@ export const CodeEditor = ({ code, onChange, previewCode, hints }: Props) => {
272
271
  [`${keymapModifierKey}-Alt-Up`]: addCursorToPrevLine,
273
272
  [`${keymapModifierKey}-Alt-Down`]: addCursorToNextLine,
274
273
  [`${keymapModifierKey}-D`]: selectNextOccurrence,
274
+ [`Shift-${keymapModifierKey}-,`]: wrapInTag,
275
275
  },
276
276
  }}
277
277
  />
@@ -0,0 +1,84 @@
1
+ import CodeMirror, { Editor, Pos } from 'codemirror';
2
+ import { Selection } from './types';
3
+
4
+ interface TagRange {
5
+ from: CodeMirror.Position;
6
+ to: CodeMirror.Position;
7
+ multiLine: boolean;
8
+ existingIndent: number;
9
+ }
10
+
11
+ export const wrapInTag = (cm: Editor) => {
12
+ const newSelections: Selection[] = [];
13
+ const tagRanges: TagRange[] = [];
14
+
15
+ let linesAdded = 0;
16
+
17
+ for (const range of cm.listSelections()) {
18
+ const from = range.from();
19
+ let to = range.to();
20
+
21
+ if (to.line !== from.line && to.ch === 0) {
22
+ to = new Pos(to.line - 1);
23
+ }
24
+
25
+ const existingContent = cm.getRange(from, to);
26
+ const existingIndent =
27
+ existingContent.length - existingContent.trimStart().length;
28
+
29
+ const isMultiLineSelection = to.line !== from.line;
30
+
31
+ tagRanges.push({
32
+ from,
33
+ to,
34
+ multiLine: isMultiLineSelection,
35
+ existingIndent,
36
+ });
37
+
38
+ const newStartCursor = new Pos(
39
+ from.line + linesAdded,
40
+ from.ch + existingIndent + 1
41
+ );
42
+ const newEndCursor = isMultiLineSelection
43
+ ? new Pos(to.line + linesAdded + 2, from.ch + existingIndent + 2)
44
+ : new Pos(to.line + linesAdded, to.ch + 4);
45
+
46
+ if (isMultiLineSelection) {
47
+ linesAdded += 2;
48
+ }
49
+
50
+ newSelections.push({ anchor: newStartCursor, head: newStartCursor });
51
+ newSelections.push({ anchor: newEndCursor, head: newEndCursor });
52
+ }
53
+
54
+ cm.operation(() => {
55
+ for (const range of [...tagRanges].reverse()) {
56
+ const existingContent = cm.getRange(range.from, range.to);
57
+
58
+ if (range.multiLine) {
59
+ const formattedExistingContent = existingContent
60
+ .split('\n')
61
+ .map((line, idx) => {
62
+ const indentLevel = ' '.repeat((idx === 0 ? range.from.ch : 0) + 2);
63
+ return `${indentLevel}${line}`;
64
+ })
65
+ .join('\n');
66
+
67
+ const openTagIndentLevel = ' '.repeat(range.existingIndent);
68
+ const closeTagIndentLevel = ' '.repeat(
69
+ range.from.ch + range.existingIndent
70
+ );
71
+
72
+ cm.replaceRange(
73
+ `${openTagIndentLevel}<>\n${formattedExistingContent}\n${closeTagIndentLevel}</>`,
74
+ range.from,
75
+ range.to
76
+ );
77
+ } else {
78
+ cm.replaceRange(`<>${existingContent}</>`, range.from, range.to);
79
+ }
80
+ }
81
+
82
+ cm.setSelections(newSelections);
83
+ });
84
+ };
@@ -1,5 +1,5 @@
1
1
  import { colorPaletteVars, sprinkles, vars } from '../sprinkles.css';
2
- import { style } from '@vanilla-extract/css';
2
+ import { globalStyle, style } from '@vanilla-extract/css';
3
3
 
4
4
  export const fieldset = sprinkles({
5
5
  border: 0,
@@ -12,6 +12,35 @@ export const radioContainer = sprinkles({
12
12
  paddingTop: 'medium',
13
13
  });
14
14
 
15
+ export const keyboardShortcutRow = style({
16
+ display: 'flex',
17
+ alignItems: 'center',
18
+ });
19
+
20
+ globalStyle(`${keyboardShortcutRow} > *:first-child`, {
21
+ flex: 1,
22
+ });
23
+
24
+ globalStyle(`${keyboardShortcutRow} > *:nth-child(2)`, {
25
+ flex: '0 0 43%',
26
+ });
27
+
28
+ export const kbd = style([
29
+ sprinkles({
30
+ borderRadius: 'large',
31
+ paddingY: 'xsmall',
32
+ textAlign: 'center',
33
+ }),
34
+ {
35
+ display: 'inline-block',
36
+ background: colorPaletteVars.background.neutral,
37
+ paddingLeft: 8,
38
+ paddingRight: 8,
39
+ fontFamily: 'system-ui',
40
+ minWidth: 16,
41
+ },
42
+ ]);
43
+
15
44
  export const realRadio = style([
16
45
  sprinkles({
17
46
  position: 'absolute',
@@ -15,6 +15,26 @@ import * as styles from './SettingsPanel.css';
15
15
  import ColorModeSystemIcon from '../icons/ColorModeSystemIcon';
16
16
  import ColorModeLightIcon from '../icons/ColorModeLightIcon';
17
17
  import ColorModeDarkIcon from '../icons/ColorModeDarkIcon';
18
+ import { Text } from '../Text/Text';
19
+ import { Inline } from '../Inline/Inline';
20
+ import { isMac } from '../../utils/formatting';
21
+
22
+ const getKeyBindings = () => {
23
+ const metaKeySymbol = isMac() ? '⌘' : 'Ctrl';
24
+ const altKeySymbol = isMac() ? '⌥' : 'Alt';
25
+
26
+ return {
27
+ 'Format code': [metaKeySymbol, 'S'],
28
+ 'Swap line up': [altKeySymbol, '↑'],
29
+ 'Swap line down': [altKeySymbol, '↓'],
30
+ 'Duplicate line up': ['⇧', altKeySymbol, '↑'],
31
+ 'Duplicate line down': ['⇧', altKeySymbol, '↓'],
32
+ 'Add cursor to prev line': [metaKeySymbol, altKeySymbol, '↑'],
33
+ 'Add cursor to next line': [metaKeySymbol, altKeySymbol, '↓'],
34
+ 'Select next occurrence': [metaKeySymbol, 'D'],
35
+ 'Wrap selection in tag': [metaKeySymbol, '⇧', ','],
36
+ };
37
+ };
18
38
 
19
39
  const positionIcon: Record<EditorPosition, ReactChild> = {
20
40
  undocked: <EditorUndockedIcon />,
@@ -28,11 +48,36 @@ const colorModeIcon: Record<ColorScheme, ReactChild> = {
28
48
  system: <ColorModeSystemIcon />,
29
49
  };
30
50
 
31
- interface SettingsPanelProps {}
51
+ interface KeyboardShortcutProps {
52
+ keybinding: string[];
53
+ description: string;
54
+ }
32
55
 
33
- export default ({}: SettingsPanelProps) => {
56
+ const KeyboardShortcut = ({
57
+ keybinding,
58
+ description,
59
+ }: KeyboardShortcutProps) => {
60
+ const shortcutSegments = keybinding.map((segment) => (
61
+ <kbd className={styles.kbd} key={`${keybinding}-${segment}`}>
62
+ {segment}
63
+ </kbd>
64
+ ));
65
+
66
+ return (
67
+ <div className={styles.keyboardShortcutRow}>
68
+ <Text>{description}</Text>
69
+ <Text size={isMac() ? 'large' : 'standard'}>
70
+ <Inline space="xxsmall">{shortcutSegments}</Inline>
71
+ </Text>
72
+ </div>
73
+ );
74
+ };
75
+
76
+ export default React.memo(() => {
34
77
  const [{ editorPosition, colorScheme }, dispatch] = useContext(StoreContext);
35
78
 
79
+ const keybindings = getKeyBindings();
80
+
36
81
  return (
37
82
  <ToolbarPanel data-testid="frame-panel">
38
83
  <Stack space="large" dividers>
@@ -111,7 +156,18 @@ export default ({}: SettingsPanelProps) => {
111
156
  ))}
112
157
  </div>
113
158
  </fieldset>
159
+
160
+ <Stack space="medium">
161
+ <Heading level="3">Keyboard Shortcuts</Heading>
162
+ {Object.entries(keybindings).map(([description, keybinding]) => (
163
+ <KeyboardShortcut
164
+ description={description}
165
+ keybinding={keybinding}
166
+ key={description}
167
+ />
168
+ ))}
169
+ </Stack>
114
170
  </Stack>
115
171
  </ToolbarPanel>
116
172
  );
117
- };
173
+ });
@@ -1,4 +1,5 @@
1
1
  import { style, globalStyle, keyframes } from '@vanilla-extract/css';
2
+ import { dark } from '../palettes';
2
3
  import { sprinkles, colorPaletteVars } from '../sprinkles.css';
3
4
 
4
5
  export const animationDuration = 1300;
@@ -17,7 +18,7 @@ export const root = style([
17
18
  }),
18
19
  {
19
20
  zIndex: 100,
20
- background: colorPaletteVars.background.neutral,
21
+ background: dark.background.neutral,
21
22
  color: colorPaletteVars.foreground.neutralInverted,
22
23
  },
23
24
  ]);
@@ -3,7 +3,7 @@ import { style } from '@vanilla-extract/css';
3
3
  import { sprinkles, colorPaletteVars } from '../sprinkles.css';
4
4
  import { toolbarItemSize } from '../ToolbarItem/ToolbarItem.css';
5
5
 
6
- export const toolbarOpenSize = 300;
6
+ export const toolbarOpenSize = 320;
7
7
  const toolbarBorderThickness = '1px';
8
8
 
9
9
  export const isOpen = style({});
@@ -15,6 +15,7 @@ import PlayIcon from '../icons/PlayIcon';
15
15
  import * as styles from './Toolbar.css';
16
16
  import SettingsPanel from '../SettingsPanel/SettingsPanel';
17
17
  import SettingsIcon from '../icons/SettingsIcon';
18
+ import { isMac } from '../../utils/formatting';
18
19
 
19
20
  interface Props {
20
21
  themes: PlayroomProps['themes'];
@@ -80,9 +81,7 @@ export default ({ themes: allThemes, widths: allWidths, snippets }: Props) => {
80
81
  {hasSnippets && (
81
82
  <ToolbarItem
82
83
  active={isSnippetsOpen}
83
- title={`Insert snippet (${
84
- navigator.platform.match('Mac') ? '\u2318' : 'Ctrl + '
85
- }K)`}
84
+ title={`Insert snippet (${isMac() ? '\u2318' : 'Ctrl + '}K)`}
86
85
  disabled={!validCursorPosition}
87
86
  data-testid="toggleSnippets"
88
87
  onClick={() => {
@@ -14,7 +14,7 @@ const originalPalette = {
14
14
  purple: '#75438a',
15
15
  white: '#fff',
16
16
  gray1: '#f4f4f4',
17
- gray2: '#e8e8e8',
17
+ gray2: '#eeeeee',
18
18
  gray3: '#a7a7a7',
19
19
  gray4: '#767676',
20
20
  gray5: '#515151',
@@ -46,7 +46,7 @@ export const light = {
46
46
  accent: originalPalette.blue2,
47
47
  positive: originalPalette.green1,
48
48
  critical: originalPalette.red1,
49
- neutral: originalPalette.gray6,
49
+ neutral: originalPalette.gray2,
50
50
  surface: originalPalette.white,
51
51
  body: originalPalette.gray1,
52
52
  selection: originalPalette.blue0,
@@ -8,6 +8,8 @@ export interface CodeWithCursor {
8
8
  cursor: CursorPosition;
9
9
  }
10
10
 
11
+ export const isMac = () => Boolean(navigator.platform.match('Mac'));
12
+
11
13
  export const runPrettier = ({
12
14
  code,
13
15
  cursorOffset,