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.
- package/.github/workflows/preview-site.yml +3 -3
- package/.github/workflows/release.yml +3 -3
- package/.github/workflows/snapshot.yml +3 -3
- package/.github/workflows/validate.yml +5 -5
- package/.nvmrc +1 -1
- package/CHANGELOG.md +55 -0
- package/README.md +6 -0
- package/bin/cli.cjs +2 -1
- package/cypress/e2e/editor.cy.js +1 -1
- package/cypress/e2e/keymaps.cy.js +1507 -11
- package/cypress/e2e/scope.cy.js +1 -1
- package/cypress/e2e/smoke.cy.js +2 -2
- package/cypress/e2e/toolbar.cy.js +1 -2
- package/cypress/e2e/urlHandling.cy.js +4 -5
- package/cypress/support/utils.js +62 -54
- package/lib/makeWebpackConfig.js +0 -3
- package/lib/provideDefaultConfig.js +13 -2
- package/package.json +18 -17
- package/src/Playroom/CatchErrors/CatchErrors.tsx +5 -6
- package/src/Playroom/CodeEditor/CodeEditor.tsx +11 -0
- package/src/Playroom/CodeEditor/keymaps/comment.ts +326 -0
- package/src/Playroom/CodeEditor/keymaps/wrap.ts +4 -1
- package/src/Playroom/Frame.tsx +9 -5
- package/src/Playroom/FramesPanel/FramesPanel.css.ts +19 -0
- package/src/Playroom/FramesPanel/FramesPanel.tsx +89 -46
- package/src/Playroom/Preview.tsx +12 -3
- package/src/Playroom/PreviewPanel/PreviewPanel.tsx +1 -1
- package/src/Playroom/SettingsPanel/SettingsPanel.tsx +11 -7
- package/src/Playroom/Stack/Stack.css.ts +4 -35
- package/src/Playroom/Stack/Stack.tsx +2 -9
- package/src/Playroom/Toolbar/Toolbar.tsx +2 -2
- package/src/Playroom/sprinkles.css.ts +1 -0
- package/src/StoreContext/StoreContext.tsx +31 -6
- package/src/index.d.ts +2 -0
- package/src/utils/params.ts +5 -8
- package/src/utils/usePreviewUrl.ts +2 -1
- package/utils/index.d.ts +3 -0
- package/utils/index.js +21 -7
|
@@ -0,0 +1,326 @@
|
|
|
1
|
+
import type CodeMirror from 'codemirror';
|
|
2
|
+
import { type Editor, Pos } from 'codemirror';
|
|
3
|
+
import type { Selection } from './types';
|
|
4
|
+
|
|
5
|
+
const BLOCK_COMMENT_START = '{/*';
|
|
6
|
+
const BLOCK_COMMENT_END = '*/}';
|
|
7
|
+
|
|
8
|
+
const LINE_COMMENT_START = '//';
|
|
9
|
+
|
|
10
|
+
const BLOCK_COMMENT_OFFSET = BLOCK_COMMENT_START.length + 1;
|
|
11
|
+
const LINE_COMMENT_OFFSET = LINE_COMMENT_START.length + 1;
|
|
12
|
+
|
|
13
|
+
const OPENING_AND_CLOSING_BLOCK_COMMENT_SYNTAX = /\{\/\*\s?|\s?\*\/\}/g;
|
|
14
|
+
const OPENING_LINE_COMMENT_SYNTAX_WITH_LEADING_WHITESPACE = /^(\s*)\/\/\s?/gm;
|
|
15
|
+
const LINE_COMMENT_LEADING_WHITESPACE = '$1';
|
|
16
|
+
|
|
17
|
+
interface IsReverseSelectionOptions {
|
|
18
|
+
anchor: CodeMirror.Position;
|
|
19
|
+
head: CodeMirror.Position;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function isReverseSelection({
|
|
23
|
+
anchor,
|
|
24
|
+
head,
|
|
25
|
+
}: IsReverseSelectionOptions): boolean {
|
|
26
|
+
return (
|
|
27
|
+
anchor.line > head.line ||
|
|
28
|
+
(anchor.line === head.line && anchor.ch > head.ch)
|
|
29
|
+
);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function getCommentStartInfo(commentType: CommentType, fullContent: string) {
|
|
33
|
+
const commentStart =
|
|
34
|
+
commentType === 'block' ? BLOCK_COMMENT_START : LINE_COMMENT_START;
|
|
35
|
+
|
|
36
|
+
const commentStartWithSpace = `${commentStart} `;
|
|
37
|
+
const commentStartUsed =
|
|
38
|
+
fullContent.indexOf(commentStartWithSpace) === -1
|
|
39
|
+
? commentStart
|
|
40
|
+
: commentStartWithSpace;
|
|
41
|
+
|
|
42
|
+
const commentStartIndex = fullContent.indexOf(commentStartUsed);
|
|
43
|
+
|
|
44
|
+
return {
|
|
45
|
+
commentStartUsed,
|
|
46
|
+
commentStartIndex,
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function getSelectionPositionRelativeToCommentStart(
|
|
51
|
+
selectionEndpoint: CodeMirror.Position,
|
|
52
|
+
commentStartIndex: number,
|
|
53
|
+
commentStartUsed: string
|
|
54
|
+
): 'before' | 'during' | 'after' {
|
|
55
|
+
if (selectionEndpoint.ch < commentStartIndex) {
|
|
56
|
+
return 'before';
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
if (selectionEndpoint.ch > commentStartIndex + commentStartUsed.length) {
|
|
60
|
+
return 'after';
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
return 'during';
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
interface GetSelectionFromOffsetOptions {
|
|
67
|
+
commentType: CommentType;
|
|
68
|
+
isAlreadyCommented: boolean;
|
|
69
|
+
fullContent: string;
|
|
70
|
+
from: CodeMirror.Position;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function getSelectionFromOffset({
|
|
74
|
+
commentType,
|
|
75
|
+
isAlreadyCommented,
|
|
76
|
+
fullContent,
|
|
77
|
+
from,
|
|
78
|
+
}: GetSelectionFromOffsetOptions) {
|
|
79
|
+
if (!isAlreadyCommented) {
|
|
80
|
+
const totalLeadingWhitespace =
|
|
81
|
+
fullContent.length - fullContent.trimStart().length;
|
|
82
|
+
|
|
83
|
+
const fromPositionBeforeCodeStart = from.ch < totalLeadingWhitespace;
|
|
84
|
+
|
|
85
|
+
if (fromPositionBeforeCodeStart) {
|
|
86
|
+
return 0;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
return commentType === 'block' ? BLOCK_COMMENT_OFFSET : LINE_COMMENT_OFFSET;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
const { commentStartUsed, commentStartIndex } = getCommentStartInfo(
|
|
93
|
+
commentType,
|
|
94
|
+
fullContent
|
|
95
|
+
);
|
|
96
|
+
|
|
97
|
+
const fromPositionRelativeToCommentStart =
|
|
98
|
+
getSelectionPositionRelativeToCommentStart(
|
|
99
|
+
from,
|
|
100
|
+
commentStartIndex,
|
|
101
|
+
commentStartUsed
|
|
102
|
+
);
|
|
103
|
+
|
|
104
|
+
switch (fromPositionRelativeToCommentStart) {
|
|
105
|
+
case 'before':
|
|
106
|
+
return 0;
|
|
107
|
+
case 'during':
|
|
108
|
+
return commentStartIndex - from.ch;
|
|
109
|
+
case 'after':
|
|
110
|
+
return -commentStartUsed.length;
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
interface GetSelectionToOffsetOptions {
|
|
115
|
+
to: CodeMirror.Position;
|
|
116
|
+
commentType: CommentType;
|
|
117
|
+
isAlreadyCommented: boolean;
|
|
118
|
+
isMultiLineSelection: boolean;
|
|
119
|
+
fullContent: string;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function getSelectionToOffset({
|
|
123
|
+
to,
|
|
124
|
+
commentType,
|
|
125
|
+
isAlreadyCommented,
|
|
126
|
+
isMultiLineSelection,
|
|
127
|
+
fullContent,
|
|
128
|
+
}: GetSelectionToOffsetOptions) {
|
|
129
|
+
const commentOffset =
|
|
130
|
+
commentType === 'block' ? BLOCK_COMMENT_OFFSET : LINE_COMMENT_OFFSET;
|
|
131
|
+
|
|
132
|
+
if (isMultiLineSelection && commentType === 'block') {
|
|
133
|
+
return 0;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
const totalLeadingWhitespace =
|
|
137
|
+
fullContent.length - fullContent.trimStart().length;
|
|
138
|
+
const toPositionBeforeCodeStart = to.ch < totalLeadingWhitespace;
|
|
139
|
+
|
|
140
|
+
if (!isAlreadyCommented) {
|
|
141
|
+
if (!isMultiLineSelection && toPositionBeforeCodeStart) {
|
|
142
|
+
return 0;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
return commentOffset;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
const { commentStartUsed, commentStartIndex } = getCommentStartInfo(
|
|
149
|
+
commentType,
|
|
150
|
+
fullContent
|
|
151
|
+
);
|
|
152
|
+
|
|
153
|
+
const toPositionRelativeToCommentStart =
|
|
154
|
+
getSelectionPositionRelativeToCommentStart(
|
|
155
|
+
to,
|
|
156
|
+
commentStartIndex,
|
|
157
|
+
commentStartUsed
|
|
158
|
+
);
|
|
159
|
+
|
|
160
|
+
switch (toPositionRelativeToCommentStart) {
|
|
161
|
+
case 'before':
|
|
162
|
+
return 0;
|
|
163
|
+
case 'during':
|
|
164
|
+
return commentStartIndex - to.ch;
|
|
165
|
+
case 'after':
|
|
166
|
+
return -commentOffset;
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
function getUpdatedContent(existingContent: string, range: TagRange) {
|
|
171
|
+
if (range.isAlreadyCommented) {
|
|
172
|
+
const uncommentType: CommentType = existingContent
|
|
173
|
+
.trimStart()
|
|
174
|
+
.startsWith(BLOCK_COMMENT_START)
|
|
175
|
+
? 'block'
|
|
176
|
+
: 'line';
|
|
177
|
+
|
|
178
|
+
const existingContentWithoutComment = existingContent.replace(
|
|
179
|
+
uncommentType === 'block'
|
|
180
|
+
? OPENING_AND_CLOSING_BLOCK_COMMENT_SYNTAX
|
|
181
|
+
: OPENING_LINE_COMMENT_SYNTAX_WITH_LEADING_WHITESPACE,
|
|
182
|
+
uncommentType === 'block' ? '' : LINE_COMMENT_LEADING_WHITESPACE
|
|
183
|
+
);
|
|
184
|
+
|
|
185
|
+
return existingContentWithoutComment;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
if (range.commentType === 'block') {
|
|
189
|
+
return `${BLOCK_COMMENT_START} ${existingContent} ${BLOCK_COMMENT_END}`;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
if (range.multiLine) {
|
|
193
|
+
return existingContent.replace(/^(\s*)/gm, `$1${LINE_COMMENT_START} `);
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
return `${LINE_COMMENT_START} ${existingContent}`;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
type CommentType = 'line' | 'block';
|
|
200
|
+
|
|
201
|
+
interface TagRange {
|
|
202
|
+
from: CodeMirror.Position;
|
|
203
|
+
to: CodeMirror.Position;
|
|
204
|
+
multiLine: boolean;
|
|
205
|
+
existingIndent: number;
|
|
206
|
+
isAlreadyCommented: boolean;
|
|
207
|
+
commentType: CommentType;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
const determineCommentType = (
|
|
211
|
+
cm: Editor,
|
|
212
|
+
from: CodeMirror.Position,
|
|
213
|
+
to: CodeMirror.Position
|
|
214
|
+
): CommentType => {
|
|
215
|
+
const lineTokens = cm.getLineTokens(from.line);
|
|
216
|
+
|
|
217
|
+
const containsTag = lineTokens.some((token) => token.type === 'tag');
|
|
218
|
+
const containsAttribute = lineTokens.some(
|
|
219
|
+
(token) => token.type === 'attribute'
|
|
220
|
+
);
|
|
221
|
+
|
|
222
|
+
const isJavaScriptMode = cm.getModeAt(from).name === 'javascript';
|
|
223
|
+
const isInlineComment = cm
|
|
224
|
+
.getLine(from.line)
|
|
225
|
+
.trimStart()
|
|
226
|
+
.startsWith(LINE_COMMENT_START);
|
|
227
|
+
const isBlockComment =
|
|
228
|
+
cm.getLine(from.line).trimStart().startsWith(BLOCK_COMMENT_START) &&
|
|
229
|
+
cm.getLine(to.line).trimEnd().endsWith(BLOCK_COMMENT_END);
|
|
230
|
+
|
|
231
|
+
if (isInlineComment) {
|
|
232
|
+
return 'line';
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
if (isJavaScriptMode && !isBlockComment && !containsTag) {
|
|
236
|
+
return 'line';
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
if (
|
|
240
|
+
(!isJavaScriptMode && !containsAttribute) ||
|
|
241
|
+
containsTag ||
|
|
242
|
+
isJavaScriptMode
|
|
243
|
+
) {
|
|
244
|
+
return 'block';
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
return 'line';
|
|
248
|
+
};
|
|
249
|
+
|
|
250
|
+
export const toggleComment = (cm: Editor) => {
|
|
251
|
+
const newSelections: Selection[] = [];
|
|
252
|
+
const tagRanges: TagRange[] = [];
|
|
253
|
+
|
|
254
|
+
for (const range of cm.listSelections()) {
|
|
255
|
+
const from = range.from();
|
|
256
|
+
let to = range.to();
|
|
257
|
+
|
|
258
|
+
if (to.line !== from.line && to.ch === 0) {
|
|
259
|
+
to = new Pos(to.line - 1);
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
const commentType = determineCommentType(cm, from, to);
|
|
263
|
+
|
|
264
|
+
const fullContent = cm.getRange(new Pos(from.line, 0), new Pos(to.line));
|
|
265
|
+
const existingIndent = fullContent.length - fullContent.trimStart().length;
|
|
266
|
+
|
|
267
|
+
const trimmedContent = fullContent.trim();
|
|
268
|
+
|
|
269
|
+
const isAlreadyCommented =
|
|
270
|
+
(trimmedContent.startsWith(BLOCK_COMMENT_START) &&
|
|
271
|
+
trimmedContent.endsWith(BLOCK_COMMENT_END)) ||
|
|
272
|
+
trimmedContent.startsWith(LINE_COMMENT_START);
|
|
273
|
+
|
|
274
|
+
const isMultiLineSelection = to.line !== from.line;
|
|
275
|
+
|
|
276
|
+
tagRanges.push({
|
|
277
|
+
from,
|
|
278
|
+
to,
|
|
279
|
+
multiLine: isMultiLineSelection,
|
|
280
|
+
existingIndent,
|
|
281
|
+
isAlreadyCommented,
|
|
282
|
+
commentType,
|
|
283
|
+
});
|
|
284
|
+
|
|
285
|
+
const fromOffset = getSelectionFromOffset({
|
|
286
|
+
commentType,
|
|
287
|
+
isAlreadyCommented,
|
|
288
|
+
fullContent,
|
|
289
|
+
from,
|
|
290
|
+
});
|
|
291
|
+
|
|
292
|
+
const toOffset = getSelectionToOffset({
|
|
293
|
+
to,
|
|
294
|
+
commentType,
|
|
295
|
+
isAlreadyCommented,
|
|
296
|
+
isMultiLineSelection,
|
|
297
|
+
fullContent,
|
|
298
|
+
});
|
|
299
|
+
|
|
300
|
+
const newSelectionRangeFrom = new Pos(from.line, from.ch + fromOffset);
|
|
301
|
+
const newSelectionRangeTo = new Pos(to.line, to.ch + toOffset);
|
|
302
|
+
|
|
303
|
+
const newSelection = isReverseSelection(range)
|
|
304
|
+
? { anchor: newSelectionRangeTo, head: newSelectionRangeFrom }
|
|
305
|
+
: { anchor: newSelectionRangeFrom, head: newSelectionRangeTo };
|
|
306
|
+
|
|
307
|
+
newSelections.push(newSelection);
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
cm.operation(() => {
|
|
311
|
+
for (const range of [...tagRanges].reverse()) {
|
|
312
|
+
const newRangeFrom = new Pos(range.from.line, range.existingIndent);
|
|
313
|
+
const newRangeTo = new Pos(range.to.line);
|
|
314
|
+
|
|
315
|
+
const existingContent = cm.getRange(newRangeFrom, newRangeTo);
|
|
316
|
+
|
|
317
|
+
cm.replaceRange(
|
|
318
|
+
getUpdatedContent(existingContent, range),
|
|
319
|
+
newRangeFrom,
|
|
320
|
+
newRangeTo
|
|
321
|
+
);
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
cm.setSelections(newSelections);
|
|
325
|
+
});
|
|
326
|
+
};
|
|
@@ -36,10 +36,13 @@ export const wrapInTag = (cm: Editor) => {
|
|
|
36
36
|
existingIndent,
|
|
37
37
|
});
|
|
38
38
|
|
|
39
|
+
const startCursorCharacterPosition =
|
|
40
|
+
from.ch + 1 + (isMultiLineSelection ? existingIndent : 0);
|
|
39
41
|
const newStartCursor = new Pos(
|
|
40
42
|
from.line + linesAdded,
|
|
41
|
-
|
|
43
|
+
startCursorCharacterPosition
|
|
42
44
|
);
|
|
45
|
+
|
|
43
46
|
const newEndCursor = isMultiLineSelection
|
|
44
47
|
? new Pos(to.line + linesAdded + 2, from.ch + existingIndent + 2)
|
|
45
48
|
: new Pos(to.line + linesAdded, to.ch + 4);
|
package/src/Playroom/Frame.tsx
CHANGED
|
@@ -19,11 +19,15 @@ export default function Frame({
|
|
|
19
19
|
components,
|
|
20
20
|
FrameComponent,
|
|
21
21
|
}: FrameProps) {
|
|
22
|
-
const { themeName, code } = useParams((rawParams) =>
|
|
23
|
-
themeName
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
22
|
+
const { themeName, code } = useParams((rawParams) => {
|
|
23
|
+
const rawThemeName = rawParams.get('themeName');
|
|
24
|
+
const rawCode = rawParams.get('code');
|
|
25
|
+
|
|
26
|
+
return {
|
|
27
|
+
themeName: rawThemeName || '',
|
|
28
|
+
code: rawCode || '',
|
|
29
|
+
};
|
|
30
|
+
});
|
|
27
31
|
|
|
28
32
|
const resolvedThemeName =
|
|
29
33
|
themeName === '__PLAYROOM__NO_THEME__' ? null : themeName;
|
|
@@ -144,3 +144,22 @@ globalStyle(`${checkbox}:checked ~ ${fakeCheckbox} > svg `, {
|
|
|
144
144
|
transform: 'none',
|
|
145
145
|
transition: vars.transition.fast,
|
|
146
146
|
});
|
|
147
|
+
|
|
148
|
+
export const textField = style([
|
|
149
|
+
sprinkles({
|
|
150
|
+
font: 'large',
|
|
151
|
+
width: 'full',
|
|
152
|
+
paddingX: 'large',
|
|
153
|
+
boxSizing: 'border-box',
|
|
154
|
+
borderRadius: 'medium',
|
|
155
|
+
}),
|
|
156
|
+
{
|
|
157
|
+
color: colorPaletteVars.foreground.neutral,
|
|
158
|
+
height: vars.touchableSize,
|
|
159
|
+
background: colorPaletteVars.background.surface,
|
|
160
|
+
'::placeholder': {
|
|
161
|
+
color: colorPaletteVars.foreground.neutralSoft,
|
|
162
|
+
},
|
|
163
|
+
border: `1px solid ${colorPaletteVars.border.standard}`,
|
|
164
|
+
},
|
|
165
|
+
]);
|
|
@@ -7,6 +7,21 @@ import { Stack } from '../Stack/Stack';
|
|
|
7
7
|
import { Text } from '../Text/Text';
|
|
8
8
|
|
|
9
9
|
import * as styles from './FramesPanel.css';
|
|
10
|
+
import { Helmet } from 'react-helmet';
|
|
11
|
+
|
|
12
|
+
const getTitle = (title: string | undefined) => {
|
|
13
|
+
if (title) {
|
|
14
|
+
return `${title} | Playroom`;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const configTitle = window?.__playroomConfig__.title;
|
|
18
|
+
|
|
19
|
+
if (configTitle) {
|
|
20
|
+
return `${configTitle} | Playroom`;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
return 'Playroom';
|
|
24
|
+
};
|
|
10
25
|
|
|
11
26
|
interface FramesPanelProps {
|
|
12
27
|
availableWidths: number[];
|
|
@@ -77,7 +92,7 @@ function FrameOption<Option>({
|
|
|
77
92
|
}
|
|
78
93
|
|
|
79
94
|
export default ({ availableWidths, availableThemes }: FramesPanelProps) => {
|
|
80
|
-
const [{ visibleWidths = [], visibleThemes = [] }, dispatch] =
|
|
95
|
+
const [{ visibleWidths = [], visibleThemes = [], title }, dispatch] =
|
|
81
96
|
useContext(StoreContext);
|
|
82
97
|
const hasThemes =
|
|
83
98
|
availableThemes.filter(
|
|
@@ -88,69 +103,97 @@ export default ({ availableWidths, availableThemes }: FramesPanelProps) => {
|
|
|
88
103
|
const hasFilteredThemes =
|
|
89
104
|
visibleThemes.length > 0 && visibleThemes.length <= availableThemes.length;
|
|
90
105
|
|
|
106
|
+
const displayedTitle = getTitle(title);
|
|
107
|
+
|
|
91
108
|
return (
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
<
|
|
95
|
-
<
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
if (newWidths) {
|
|
109
|
+
<>
|
|
110
|
+
{title === undefined ? null : (
|
|
111
|
+
<Helmet>
|
|
112
|
+
<title>{displayedTitle}</title>
|
|
113
|
+
</Helmet>
|
|
114
|
+
)}
|
|
115
|
+
<ToolbarPanel data-testid="frame-panel">
|
|
116
|
+
<Stack space="xxxlarge">
|
|
117
|
+
<label>
|
|
118
|
+
<Stack space="medium">
|
|
119
|
+
<Heading level="3">Title</Heading>
|
|
120
|
+
<input
|
|
121
|
+
type="text"
|
|
122
|
+
id="playroomTitleField"
|
|
123
|
+
placeholder="Enter a title for this Playroom..."
|
|
124
|
+
className={styles.textField}
|
|
125
|
+
value={title}
|
|
126
|
+
onChange={(e) =>
|
|
111
127
|
dispatch({
|
|
112
|
-
type: '
|
|
113
|
-
payload: {
|
|
114
|
-
})
|
|
115
|
-
} else {
|
|
116
|
-
dispatch({ type: 'resetVisibleWidths' });
|
|
128
|
+
type: 'updateTitle',
|
|
129
|
+
payload: { title: e.target.value },
|
|
130
|
+
})
|
|
117
131
|
}
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
</div>
|
|
132
|
+
/>
|
|
133
|
+
</Stack>
|
|
134
|
+
</label>
|
|
122
135
|
|
|
123
|
-
|
|
124
|
-
<div data-testid="themePreferences">
|
|
136
|
+
<div data-testid="widthsPreferences">
|
|
125
137
|
<FrameHeading
|
|
126
|
-
showReset={
|
|
127
|
-
onReset={() => dispatch({ type: '
|
|
138
|
+
showReset={hasFilteredWidths}
|
|
139
|
+
onReset={() => dispatch({ type: 'resetVisibleWidths' })}
|
|
128
140
|
>
|
|
129
|
-
|
|
141
|
+
Widths
|
|
130
142
|
</FrameHeading>
|
|
131
143
|
|
|
132
|
-
{
|
|
144
|
+
{availableWidths.map((option) => (
|
|
133
145
|
<FrameOption
|
|
134
146
|
key={option}
|
|
135
147
|
option={option}
|
|
136
|
-
selected={
|
|
137
|
-
visible={
|
|
138
|
-
available={
|
|
139
|
-
onChange={(
|
|
140
|
-
if (
|
|
148
|
+
selected={hasFilteredWidths && visibleWidths.includes(option)}
|
|
149
|
+
visible={visibleWidths}
|
|
150
|
+
available={availableWidths}
|
|
151
|
+
onChange={(newWidths) => {
|
|
152
|
+
if (newWidths) {
|
|
141
153
|
dispatch({
|
|
142
|
-
type: '
|
|
143
|
-
payload: {
|
|
154
|
+
type: 'updateVisibleWidths',
|
|
155
|
+
payload: { widths: newWidths },
|
|
144
156
|
});
|
|
145
157
|
} else {
|
|
146
|
-
dispatch({ type: '
|
|
158
|
+
dispatch({ type: 'resetVisibleWidths' });
|
|
147
159
|
}
|
|
148
160
|
}}
|
|
149
161
|
/>
|
|
150
162
|
))}
|
|
151
163
|
</div>
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
164
|
+
|
|
165
|
+
{hasThemes ? (
|
|
166
|
+
<div data-testid="themePreferences">
|
|
167
|
+
<FrameHeading
|
|
168
|
+
showReset={hasFilteredThemes}
|
|
169
|
+
onReset={() => dispatch({ type: 'resetVisibleThemes' })}
|
|
170
|
+
>
|
|
171
|
+
Themes
|
|
172
|
+
</FrameHeading>
|
|
173
|
+
|
|
174
|
+
{availableThemes.map((option) => (
|
|
175
|
+
<FrameOption
|
|
176
|
+
key={option}
|
|
177
|
+
option={option}
|
|
178
|
+
selected={hasFilteredThemes && visibleThemes.includes(option)}
|
|
179
|
+
visible={visibleThemes}
|
|
180
|
+
available={availableThemes}
|
|
181
|
+
onChange={(newThemes) => {
|
|
182
|
+
if (newThemes) {
|
|
183
|
+
dispatch({
|
|
184
|
+
type: 'updateVisibleThemes',
|
|
185
|
+
payload: { themes: newThemes },
|
|
186
|
+
});
|
|
187
|
+
} else {
|
|
188
|
+
dispatch({ type: 'resetVisibleThemes' });
|
|
189
|
+
}
|
|
190
|
+
}}
|
|
191
|
+
/>
|
|
192
|
+
))}
|
|
193
|
+
</div>
|
|
194
|
+
) : null}
|
|
195
|
+
</Stack>
|
|
196
|
+
</ToolbarPanel>
|
|
197
|
+
</>
|
|
155
198
|
);
|
|
156
199
|
};
|
package/src/Playroom/Preview.tsx
CHANGED
|
@@ -9,10 +9,12 @@ import CatchErrors from './CatchErrors/CatchErrors';
|
|
|
9
9
|
import RenderCode from './RenderCode/RenderCode';
|
|
10
10
|
|
|
11
11
|
import * as styles from './Preview.css';
|
|
12
|
+
import { Helmet } from 'react-helmet';
|
|
12
13
|
|
|
13
14
|
interface PreviewState {
|
|
14
15
|
code?: string;
|
|
15
16
|
themeName?: string;
|
|
17
|
+
title?: string;
|
|
16
18
|
}
|
|
17
19
|
|
|
18
20
|
export interface PreviewProps {
|
|
@@ -25,15 +27,17 @@ export interface PreviewProps {
|
|
|
25
27
|
}>;
|
|
26
28
|
}
|
|
27
29
|
export default ({ themes, components, FrameComponent }: PreviewProps) => {
|
|
28
|
-
const { themeName, code } = useParams((rawParams): PreviewState => {
|
|
29
|
-
|
|
30
|
+
const { themeName, code, title } = useParams((rawParams): PreviewState => {
|
|
31
|
+
const rawCode = rawParams.get('code');
|
|
32
|
+
if (rawCode) {
|
|
30
33
|
const result = JSON.parse(
|
|
31
|
-
lzString.decompressFromEncodedURIComponent(String(
|
|
34
|
+
lzString.decompressFromEncodedURIComponent(String(rawCode)) ?? ''
|
|
32
35
|
);
|
|
33
36
|
|
|
34
37
|
return {
|
|
35
38
|
code: compileJsx(result.code),
|
|
36
39
|
themeName: result.theme,
|
|
40
|
+
title: result.title,
|
|
37
41
|
};
|
|
38
42
|
}
|
|
39
43
|
|
|
@@ -44,6 +48,11 @@ export default ({ themes, components, FrameComponent }: PreviewProps) => {
|
|
|
44
48
|
|
|
45
49
|
return (
|
|
46
50
|
<CatchErrors code={code}>
|
|
51
|
+
<Helmet>
|
|
52
|
+
<title>
|
|
53
|
+
{title ? `${title} | Playroom Preview` : 'Playroom Preview'}
|
|
54
|
+
</title>
|
|
55
|
+
</Helmet>
|
|
47
56
|
<div className={styles.renderContainer}>
|
|
48
57
|
<FrameComponent
|
|
49
58
|
themeName={themeName || '__PLAYROOM__NO_THEME__'}
|
|
@@ -22,17 +22,21 @@ import { isMac } from '../../utils/formatting';
|
|
|
22
22
|
const getKeyBindings = () => {
|
|
23
23
|
const metaKeySymbol = isMac() ? '⌘' : 'Ctrl';
|
|
24
24
|
const altKeySymbol = isMac() ? '⌥' : 'Alt';
|
|
25
|
+
const shiftKeySymbol = isMac() ? '⇧' : 'Shift';
|
|
25
26
|
|
|
26
27
|
return {
|
|
28
|
+
'Toggle comment': [metaKeySymbol, '/'],
|
|
29
|
+
'Wrap selection in tag': [metaKeySymbol, shiftKeySymbol, ','],
|
|
27
30
|
'Format code': [metaKeySymbol, 'S'],
|
|
31
|
+
'Insert snippet': [metaKeySymbol, 'K'],
|
|
32
|
+
'Copy Playroom link': [metaKeySymbol, shiftKeySymbol, 'C'],
|
|
33
|
+
'Select next occurrence': [metaKeySymbol, 'D'],
|
|
28
34
|
'Swap line up': [altKeySymbol, '↑'],
|
|
29
35
|
'Swap line down': [altKeySymbol, '↓'],
|
|
30
|
-
'Duplicate line up': [
|
|
31
|
-
'Duplicate line down': [
|
|
36
|
+
'Duplicate line up': [shiftKeySymbol, altKeySymbol, '↑'],
|
|
37
|
+
'Duplicate line down': [shiftKeySymbol, altKeySymbol, '↓'],
|
|
32
38
|
'Add cursor to prev line': [metaKeySymbol, altKeySymbol, '↑'],
|
|
33
39
|
'Add cursor to next line': [metaKeySymbol, altKeySymbol, '↓'],
|
|
34
|
-
'Select next occurrence': [metaKeySymbol, 'D'],
|
|
35
|
-
'Wrap selection in tag': [metaKeySymbol, '⇧', ','],
|
|
36
40
|
};
|
|
37
41
|
};
|
|
38
42
|
|
|
@@ -79,8 +83,8 @@ export default React.memo(() => {
|
|
|
79
83
|
const keybindings = getKeyBindings();
|
|
80
84
|
|
|
81
85
|
return (
|
|
82
|
-
<ToolbarPanel data-testid="
|
|
83
|
-
<Stack space="
|
|
86
|
+
<ToolbarPanel data-testid="settings-panel">
|
|
87
|
+
<Stack space="xxxlarge">
|
|
84
88
|
<fieldset className={styles.fieldset}>
|
|
85
89
|
<legend>
|
|
86
90
|
<Heading level="3">Editor Position</Heading>
|
|
@@ -157,7 +161,7 @@ export default React.memo(() => {
|
|
|
157
161
|
</div>
|
|
158
162
|
</fieldset>
|
|
159
163
|
|
|
160
|
-
<Stack space="
|
|
164
|
+
<Stack space="xlarge">
|
|
161
165
|
<Heading level="3">Keyboard Shortcuts</Heading>
|
|
162
166
|
{Object.entries(keybindings).map(([description, keybinding]) => (
|
|
163
167
|
<KeyboardShortcut
|