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 +11 -0
- package/cypress/e2e/keymaps.cy.js +128 -11
- package/cypress/e2e/snippets.cy.js +1 -1
- package/cypress/support/utils.js +20 -5
- package/package.json +1 -1
- package/src/Playroom/CodeEditor/CodeEditor.tsx +7 -7
- package/src/Playroom/CodeEditor/keymaps/wrap.ts +84 -0
- package/src/Playroom/SettingsPanel/SettingsPanel.css.ts +30 -1
- package/src/Playroom/SettingsPanel/SettingsPanel.tsx +59 -3
- package/src/Playroom/SplashScreen/SplashScreen.css.ts +2 -1
- package/src/Playroom/Toolbar/Toolbar.css.ts +1 -1
- package/src/Playroom/Toolbar/Toolbar.tsx +2 -3
- package/src/Playroom/palettes.ts +2 -2
- package/src/utils/formatting.ts +2 -0
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
|
-
|
|
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
|
});
|
package/cypress/support/utils.js
CHANGED
|
@@ -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
|
-
|
|
151
|
-
|
|
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
|
@@ -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 =
|
|
113
|
-
? e.metaKey
|
|
114
|
-
: e.ctrlKey;
|
|
113
|
+
const cmdOrCtrl = isMac() ? e.metaKey : e.ctrlKey;
|
|
115
114
|
|
|
116
|
-
if (cmdOrCtrl && e.
|
|
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 &&
|
|
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 =
|
|
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
|
|
51
|
+
interface KeyboardShortcutProps {
|
|
52
|
+
keybinding: string[];
|
|
53
|
+
description: string;
|
|
54
|
+
}
|
|
32
55
|
|
|
33
|
-
|
|
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:
|
|
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 =
|
|
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={() => {
|
package/src/Playroom/palettes.ts
CHANGED
|
@@ -14,7 +14,7 @@ const originalPalette = {
|
|
|
14
14
|
purple: '#75438a',
|
|
15
15
|
white: '#fff',
|
|
16
16
|
gray1: '#f4f4f4',
|
|
17
|
-
gray2: '#
|
|
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.
|
|
49
|
+
neutral: originalPalette.gray2,
|
|
50
50
|
surface: originalPalette.white,
|
|
51
51
|
body: originalPalette.gray1,
|
|
52
52
|
selection: originalPalette.blue0,
|