playroom 0.39.0 → 0.40.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 +30 -0
- package/lib/getStaticTypes.js +18 -2
- package/lib/makeWebpackConfig.js +15 -2
- package/package.json +11 -15
- package/src/Playroom/CodeEditor/CodeEditor.tsx +0 -9
- package/src/Playroom/CodeEditor/CodeMirror2.tsx +8 -1
- package/src/Playroom/CodeEditor/keymaps/comment.ts +0 -1
- package/src/Playroom/CodeEditor/keymaps/wrap.ts +2 -3
- package/src/Playroom/Frame.tsx +0 -1
- package/src/Playroom/Frames/Frames.tsx +1 -2
- package/src/Playroom/Frames/Iframe.tsx +32 -1
- package/src/Playroom/Playroom.tsx +14 -2
- package/src/Playroom/SettingsPanel/SettingsPanel.tsx +3 -4
- package/src/Playroom/StatusMessage/StatusMessage.tsx +6 -2
- package/src/Playroom/Toolbar/Toolbar.tsx +56 -4
- package/src/Playroom/ToolbarItem/ToolbarItem.tsx +2 -2
- package/src/Playroom/palettes.ts +41 -5
- package/src/StoreContext/StoreContext.tsx +1 -1
- package/src/index.d.ts +4 -1
- package/src/index.js +55 -64
- package/src/preview.js +13 -0
- package/src/render.js +6 -3
- package/src/utils/componentsToHints.ts +7 -6
- package/src/utils/compileJsx.test.ts +0 -66
- package/src/utils/cursor.spec.ts +0 -57
- package/src/utils/formatting.spec.ts +0 -135
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,35 @@
|
|
|
1
1
|
# playroom
|
|
2
2
|
|
|
3
|
+
## 0.40.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- b8f89d2: Set default colour scheme to 'system'.
|
|
8
|
+
- 16ec1e7: Update `react` and `react-dom` peer dependency ranges to include `^19`
|
|
9
|
+
- 857feab: Remove keybinding for copying Playroom link to clipboard.
|
|
10
|
+
- fab7863: Drop support for browser versions that do not support the `IntersectionObserver` API
|
|
11
|
+
|
|
12
|
+
Playroom no longer provides a polyfill for [`IntersectionObserver`].
|
|
13
|
+
|
|
14
|
+
[`intersectionobserver`]: https://developer.mozilla.org/en-US/docs/Web/API/Intersection_Observer_API
|
|
15
|
+
|
|
16
|
+
### Patch Changes
|
|
17
|
+
|
|
18
|
+
- 4412ef1: Ensure favicon is displayed on Preview links.
|
|
19
|
+
- 6095dc4: Replace `polished` dependency with CSS relative color syntax and `color-mix`
|
|
20
|
+
- 16ec1e7: Remove `react-use` dependency
|
|
21
|
+
- 67006f0: Fix bug in "Wrap selection in tag" command that caused the start cursor to sometimes be in the wrong position when selecting an empty line.
|
|
22
|
+
- fb14616: Restrict `playroom`'s Vanilla Extract plugin to only process playroom's `.css.ts` files
|
|
23
|
+
- 719c957: Remove `lodash` dependency
|
|
24
|
+
|
|
25
|
+
## 0.39.1
|
|
26
|
+
|
|
27
|
+
### Patch Changes
|
|
28
|
+
|
|
29
|
+
- dbf3310: Update `re-resizable` dependency.
|
|
30
|
+
|
|
31
|
+
Fix issue where resizable handles were stacked below the editor panel and could not be selected.
|
|
32
|
+
|
|
3
33
|
## 0.39.0
|
|
4
34
|
|
|
5
35
|
### Minor Changes
|
package/lib/getStaticTypes.js
CHANGED
|
@@ -1,7 +1,5 @@
|
|
|
1
1
|
const findUp = require('find-up');
|
|
2
2
|
const fastGlob = require('fast-glob');
|
|
3
|
-
const keyBy = require('lodash/keyBy');
|
|
4
|
-
const mapValues = require('lodash/mapValues');
|
|
5
3
|
const fs = require('fs');
|
|
6
4
|
const ts = require('typescript');
|
|
7
5
|
const path = require('path');
|
|
@@ -16,6 +14,24 @@ const parsePropType = (propType) => {
|
|
|
16
14
|
return [];
|
|
17
15
|
};
|
|
18
16
|
|
|
17
|
+
/**
|
|
18
|
+
* Modified from https://github.com/you-dont-need/You-Dont-Need-Lodash-Underscore?tab=readme-ov-file#_keyby.
|
|
19
|
+
* Only supports arrays and expects a `key` to be provided.
|
|
20
|
+
*/
|
|
21
|
+
const keyBy = (array = [], key) =>
|
|
22
|
+
array.reduce(
|
|
23
|
+
(previousValue, currentValue) => ({
|
|
24
|
+
...previousValue,
|
|
25
|
+
[currentValue[key]]: currentValue,
|
|
26
|
+
}),
|
|
27
|
+
{}
|
|
28
|
+
);
|
|
29
|
+
|
|
30
|
+
const mapValues = (object, callback) =>
|
|
31
|
+
Object.fromEntries(
|
|
32
|
+
Object.entries(object).map(([key, value]) => [key, callback(value)])
|
|
33
|
+
);
|
|
34
|
+
|
|
19
35
|
module.exports = async (playroomConfig) => {
|
|
20
36
|
const {
|
|
21
37
|
cwd,
|
package/lib/makeWebpackConfig.js
CHANGED
|
@@ -30,7 +30,11 @@ module.exports = async (playroomConfig, options) => {
|
|
|
30
30
|
}
|
|
31
31
|
);
|
|
32
32
|
const { version } = JSON.parse(pkgContents);
|
|
33
|
-
isLegacyReact = !(
|
|
33
|
+
isLegacyReact = !(
|
|
34
|
+
version.startsWith('18') ||
|
|
35
|
+
version.startsWith('19') ||
|
|
36
|
+
version.startsWith('0.0.0')
|
|
37
|
+
);
|
|
34
38
|
} catch (e) {
|
|
35
39
|
throw new Error('Unable to read `react-dom` package json');
|
|
36
40
|
}
|
|
@@ -178,9 +182,18 @@ module.exports = async (playroomConfig, options) => {
|
|
|
178
182
|
chunksSortMode: 'none',
|
|
179
183
|
chunks: ['preview'],
|
|
180
184
|
filename: 'preview/index.html',
|
|
185
|
+
favicon: path.join(__dirname, '../images/favicon.png'),
|
|
181
186
|
publicPath: '../',
|
|
182
187
|
}),
|
|
183
|
-
new VanillaExtractPlugin(
|
|
188
|
+
new VanillaExtractPlugin({
|
|
189
|
+
test: (filePath) => {
|
|
190
|
+
// Only apply VanillaExtract plugin to playroom's source `.css.ts` files
|
|
191
|
+
return (
|
|
192
|
+
/\.css\.ts$/i.test(filePath) &&
|
|
193
|
+
includePaths.some((includePath) => filePath.startsWith(includePath))
|
|
194
|
+
);
|
|
195
|
+
},
|
|
196
|
+
}),
|
|
184
197
|
new MiniCssExtractPlugin({ ignoreOrder: true }),
|
|
185
198
|
...(options.production ? [] : [new FriendlyErrorsWebpackPlugin()]),
|
|
186
199
|
// If using a version of React earlier than 18, ignore the
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "playroom",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.40.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",
|
|
@@ -42,10 +42,9 @@
|
|
|
42
42
|
"@soda/friendly-errors-webpack-plugin": "^1.8.1",
|
|
43
43
|
"@types/base64-url": "^2.2.0",
|
|
44
44
|
"@types/codemirror": "^5.60.5",
|
|
45
|
-
"@types/lodash": "^4.14.191",
|
|
46
45
|
"@types/prettier": "^2.7.1",
|
|
47
|
-
"@types/react": "^
|
|
48
|
-
"@types/react-dom": "^
|
|
46
|
+
"@types/react": "^19.0.10",
|
|
47
|
+
"@types/react-dom": "^19.0.4",
|
|
49
48
|
"@vanilla-extract/css": "^1.9.2",
|
|
50
49
|
"@vanilla-extract/css-utils": "^0.1.3",
|
|
51
50
|
"@vanilla-extract/dynamic": "^2.1.2",
|
|
@@ -65,22 +64,18 @@
|
|
|
65
64
|
"fuzzy": "^0.1.3",
|
|
66
65
|
"history": "^5.3.0",
|
|
67
66
|
"html-webpack-plugin": "^5.5.0",
|
|
68
|
-
"intersection-observer": "^0.12.2",
|
|
69
67
|
"localforage": "^1.10.0",
|
|
70
|
-
"lodash": "^4.17.21",
|
|
71
68
|
"lz-string": "^1.5.0",
|
|
72
69
|
"memoize-one": "^6.0.0",
|
|
73
70
|
"mini-css-extract-plugin": "^2.7.2",
|
|
74
71
|
"parse-prop-types": "^0.3.0",
|
|
75
|
-
"polished": "^4.2.2",
|
|
76
72
|
"portfinder": "^1.0.32",
|
|
77
73
|
"prettier": "^2.8.1",
|
|
78
74
|
"prop-types": "^15.8.1",
|
|
79
|
-
"re-resizable": "^6.
|
|
75
|
+
"re-resizable": "^6.11.2",
|
|
80
76
|
"react-docgen-typescript": "^2.2.2",
|
|
81
77
|
"react-helmet": "^6.1.0",
|
|
82
78
|
"react-transition-group": "^4.4.5",
|
|
83
|
-
"react-use": "^17.4.0",
|
|
84
79
|
"read-pkg-up": "^7.0.1",
|
|
85
80
|
"scope-eval": "^1.0.0",
|
|
86
81
|
"sucrase": "^3.34.0",
|
|
@@ -94,8 +89,9 @@
|
|
|
94
89
|
"@actions/core": "^1.10.0",
|
|
95
90
|
"@changesets/cli": "^2.25.2",
|
|
96
91
|
"@octokit/rest": "^19.0.5",
|
|
97
|
-
"@testing-library/cypress": "^10.0.
|
|
92
|
+
"@testing-library/cypress": "^10.0.3",
|
|
98
93
|
"@types/jest": "^29.2.4",
|
|
94
|
+
"@types/node": "^18.11.9",
|
|
99
95
|
"@types/react-helmet": "^6.1.6",
|
|
100
96
|
"@types/react-transition-group": "^4.4.10",
|
|
101
97
|
"concurrently": "^7.6.0",
|
|
@@ -105,15 +101,15 @@
|
|
|
105
101
|
"husky": "^8.0.2",
|
|
106
102
|
"jest": "^29.3.1",
|
|
107
103
|
"lint-staged": "^15.2.2",
|
|
108
|
-
"react": "^
|
|
109
|
-
"react-dom": "^
|
|
104
|
+
"react": "^19.0.0",
|
|
105
|
+
"react-dom": "^19.0.0",
|
|
110
106
|
"serve": "^14.1.2",
|
|
111
107
|
"start-server-and-test": "^1.15.2",
|
|
112
108
|
"surge": "^0.23.1"
|
|
113
109
|
},
|
|
114
110
|
"peerDependencies": {
|
|
115
|
-
"react": "^17 || ^18",
|
|
116
|
-
"react-dom": "^17 || ^18"
|
|
111
|
+
"react": "^17 || ^18 || ^19",
|
|
112
|
+
"react-dom": "^17 || ^18 || ^19"
|
|
117
113
|
},
|
|
118
114
|
"engines": {
|
|
119
115
|
"node": ">=18.12.0"
|
|
@@ -148,7 +144,7 @@
|
|
|
148
144
|
"format": "prettier --write '**/*.{js,md,ts,tsx}'",
|
|
149
145
|
"version": "changeset version",
|
|
150
146
|
"release": "changeset publish",
|
|
151
|
-
"test": "jest src",
|
|
147
|
+
"test": "jest src lib",
|
|
152
148
|
"post-commit-status": "node scripts/postCommitStatus.js",
|
|
153
149
|
"deploy-preview": "surge -p ./cypress/projects/themed/dist"
|
|
154
150
|
}
|
|
@@ -298,15 +298,6 @@ export const CodeEditor = ({
|
|
|
298
298
|
['Shift-Ctrl-R']: false, // override default keybinding
|
|
299
299
|
['Cmd-Option-F']: false, // override default keybinding
|
|
300
300
|
['Shift-Cmd-Option-F']: false, // override default keybinding
|
|
301
|
-
[`Shift-${keymapModifierKey}-C`]: () => {
|
|
302
|
-
dispatch({
|
|
303
|
-
type: 'copyToClipboard',
|
|
304
|
-
payload: {
|
|
305
|
-
url: window.location.href,
|
|
306
|
-
trigger: 'toolbarItem',
|
|
307
|
-
},
|
|
308
|
-
});
|
|
309
|
-
},
|
|
310
301
|
},
|
|
311
302
|
}}
|
|
312
303
|
/>
|
|
@@ -743,6 +743,13 @@ export class UnControlled extends React.Component<
|
|
|
743
743
|
? `react-codemirror2 ${this.props.className}`
|
|
744
744
|
: 'react-codemirror2';
|
|
745
745
|
|
|
746
|
-
return
|
|
746
|
+
return (
|
|
747
|
+
<div
|
|
748
|
+
className={className}
|
|
749
|
+
ref={(self) => {
|
|
750
|
+
this.ref = self!;
|
|
751
|
+
}}
|
|
752
|
+
/>
|
|
753
|
+
);
|
|
747
754
|
}
|
|
748
755
|
}
|
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import type CodeMirror from 'codemirror';
|
|
2
1
|
import { type Editor, Pos } from 'codemirror';
|
|
3
2
|
import type { Selection } from './types';
|
|
4
3
|
|
|
@@ -19,6 +18,8 @@ export const wrapInTag = (cm: Editor) => {
|
|
|
19
18
|
const from = range.from();
|
|
20
19
|
let to = range.to();
|
|
21
20
|
|
|
21
|
+
const isMultiLineSelection = to.line !== from.line;
|
|
22
|
+
|
|
22
23
|
if (to.line !== from.line && to.ch === 0) {
|
|
23
24
|
to = new Pos(to.line - 1);
|
|
24
25
|
}
|
|
@@ -27,8 +28,6 @@ export const wrapInTag = (cm: Editor) => {
|
|
|
27
28
|
const existingIndent =
|
|
28
29
|
existingContent.length - existingContent.trimStart().length;
|
|
29
30
|
|
|
30
|
-
const isMultiLineSelection = to.line !== from.line;
|
|
31
|
-
|
|
32
31
|
tagRanges.push({
|
|
33
32
|
from,
|
|
34
33
|
to,
|
package/src/Playroom/Frame.tsx
CHANGED
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import { useRef } from 'react';
|
|
2
|
-
import flatMap from 'lodash/flatMap';
|
|
3
2
|
import Iframe from './Iframe';
|
|
4
3
|
import { compileJsx } from '../../utils/compileJsx';
|
|
5
4
|
import type { PlayroomProps } from '../Playroom';
|
|
@@ -21,7 +20,7 @@ export default function Frames({ code, themes, widths }: FramesProps) {
|
|
|
21
20
|
const scrollingPanelRef = useRef<HTMLDivElement | null>(null);
|
|
22
21
|
const renderCode = useRef<string>('');
|
|
23
22
|
|
|
24
|
-
const frames = flatMap(
|
|
23
|
+
const frames = widths.flatMap((width) =>
|
|
25
24
|
themes.map((theme) => ({
|
|
26
25
|
theme,
|
|
27
26
|
width,
|
|
@@ -4,8 +4,8 @@ import {
|
|
|
4
4
|
useRef,
|
|
5
5
|
type AllHTMLAttributes,
|
|
6
6
|
type MutableRefObject,
|
|
7
|
+
type RefObject,
|
|
7
8
|
} from 'react';
|
|
8
|
-
import { useIntersection } from 'react-use';
|
|
9
9
|
|
|
10
10
|
import playroomConfig from '../../config';
|
|
11
11
|
|
|
@@ -70,3 +70,34 @@ export default function Iframe({
|
|
|
70
70
|
/>
|
|
71
71
|
);
|
|
72
72
|
}
|
|
73
|
+
|
|
74
|
+
// copied directly from `react-use`
|
|
75
|
+
// https://github.com/streamich/react-use/blob/d2028ae44c79628475f0ef1736c4a48ca310247a/src/useIntersection.ts#L3-L28
|
|
76
|
+
function useIntersection(
|
|
77
|
+
ref: RefObject<HTMLElement | null>,
|
|
78
|
+
options: IntersectionObserverInit
|
|
79
|
+
): IntersectionObserverEntry | null {
|
|
80
|
+
const [intersectionObserverEntry, setIntersectionObserverEntry] =
|
|
81
|
+
useState<IntersectionObserverEntry | null>(null);
|
|
82
|
+
|
|
83
|
+
useEffect(() => {
|
|
84
|
+
if (ref.current && typeof IntersectionObserver === 'function') {
|
|
85
|
+
const handler = (entries: IntersectionObserverEntry[]) => {
|
|
86
|
+
setIntersectionObserverEntry(entries[0]);
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
const observer = new IntersectionObserver(handler, options);
|
|
90
|
+
observer.observe(ref.current);
|
|
91
|
+
|
|
92
|
+
return () => {
|
|
93
|
+
setIntersectionObserverEntry(null);
|
|
94
|
+
observer.disconnect();
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
return () => {};
|
|
98
|
+
// disabled in the original implementation
|
|
99
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
100
|
+
}, [ref.current, options.threshold, options.root, options.rootMargin]);
|
|
101
|
+
|
|
102
|
+
return intersectionObserverEntry;
|
|
103
|
+
}
|
|
@@ -22,6 +22,8 @@ import { Box } from './Box/Box';
|
|
|
22
22
|
|
|
23
23
|
import { assignInlineVars } from '@vanilla-extract/dynamic';
|
|
24
24
|
|
|
25
|
+
const staticTypes = __PLAYROOM_GLOBAL__STATIC_TYPES__;
|
|
26
|
+
|
|
25
27
|
const resizableConfig = (position: EditorPosition = 'bottom') => ({
|
|
26
28
|
top: position === 'bottom',
|
|
27
29
|
right: false,
|
|
@@ -59,7 +61,7 @@ const getTitle = (title: string | undefined) => {
|
|
|
59
61
|
};
|
|
60
62
|
|
|
61
63
|
export interface PlayroomProps {
|
|
62
|
-
components: Record<string, ComponentType
|
|
64
|
+
components: Record<string, ComponentType<any>>;
|
|
63
65
|
themes: string[];
|
|
64
66
|
widths: number[];
|
|
65
67
|
snippets: Snippets;
|
|
@@ -122,7 +124,7 @@ export default ({ components, themes, widths, snippets }: PlayroomProps) => {
|
|
|
122
124
|
dispatch({ type: 'updateCode', payload: { code: newCode } })
|
|
123
125
|
}
|
|
124
126
|
previewCode={previewEditorCode}
|
|
125
|
-
hints={componentsToHints(components)}
|
|
127
|
+
hints={componentsToHints(components, staticTypes)}
|
|
126
128
|
/>
|
|
127
129
|
<StatusMessage />
|
|
128
130
|
</div>
|
|
@@ -163,6 +165,16 @@ export default ({ components, themes, widths, snippets }: PlayroomProps) => {
|
|
|
163
165
|
updateEditorSize({ isVerticalEditor, offsetWidth, offsetHeight });
|
|
164
166
|
}}
|
|
165
167
|
enable={resizableConfig(editorPosition)}
|
|
168
|
+
/*
|
|
169
|
+
* Ensures resizable handles are stacked above the `codeEditor` component.
|
|
170
|
+
* By default, handles are stacked below the editor as introduced in:
|
|
171
|
+
* https://github.com/bokuweb/re-resizable/pull/827
|
|
172
|
+
*/
|
|
173
|
+
handleStyles={
|
|
174
|
+
editorPosition === 'bottom'
|
|
175
|
+
? { top: { zIndex: 1 } }
|
|
176
|
+
: { left: { zIndex: 1 } }
|
|
177
|
+
}
|
|
166
178
|
>
|
|
167
179
|
{codeEditor}
|
|
168
180
|
</Resizable>
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import React, { useContext, type
|
|
1
|
+
import React, { useContext, type ReactElement } from 'react';
|
|
2
2
|
import { Heading } from '../Heading/Heading';
|
|
3
3
|
import { ToolbarPanel } from '../ToolbarPanel/ToolbarPanel';
|
|
4
4
|
import {
|
|
@@ -32,7 +32,6 @@ const getKeyBindings = () => {
|
|
|
32
32
|
'Wrap selection in tag': [metaKeySymbol, shiftKeySymbol, ','],
|
|
33
33
|
'Format code': [metaKeySymbol, 'S'],
|
|
34
34
|
'Insert snippet': [metaKeySymbol, 'K'],
|
|
35
|
-
'Copy Playroom link': [metaKeySymbol, shiftKeySymbol, 'C'],
|
|
36
35
|
'Select next occurrence': [metaKeySymbol, 'D'],
|
|
37
36
|
'Jump to line number': [metaKeySymbol, 'G'],
|
|
38
37
|
'Swap line up': [altKeySymbol, '↑'],
|
|
@@ -44,13 +43,13 @@ const getKeyBindings = () => {
|
|
|
44
43
|
};
|
|
45
44
|
};
|
|
46
45
|
|
|
47
|
-
const positionIcon: Record<EditorPosition,
|
|
46
|
+
const positionIcon: Record<EditorPosition, ReactElement> = {
|
|
48
47
|
undocked: <EditorUndockedIcon />,
|
|
49
48
|
right: <EditorRightIcon />,
|
|
50
49
|
bottom: <EditorBottomIcon />,
|
|
51
50
|
};
|
|
52
51
|
|
|
53
|
-
const colorModeIcon: Record<ColorScheme,
|
|
52
|
+
const colorModeIcon: Record<ColorScheme, ReactElement> = {
|
|
54
53
|
light: <ColorModeLightIcon />,
|
|
55
54
|
dark: <ColorModeDarkIcon />,
|
|
56
55
|
system: <ColorModeSystemIcon />,
|
|
@@ -14,8 +14,12 @@ interface Props {
|
|
|
14
14
|
}
|
|
15
15
|
export const StatusMessage = ({ dismissable = false }: Props) => {
|
|
16
16
|
const [{ statusMessage }, dispatch] = useContext(StoreContext);
|
|
17
|
-
const cleanupTimerRef = useRef<ReturnType<typeof setTimeout
|
|
18
|
-
|
|
17
|
+
const cleanupTimerRef = useRef<ReturnType<typeof setTimeout> | undefined>(
|
|
18
|
+
undefined
|
|
19
|
+
);
|
|
20
|
+
const showStatusTimerRef = useRef<ReturnType<typeof setTimeout> | undefined>(
|
|
21
|
+
undefined
|
|
22
|
+
);
|
|
19
23
|
|
|
20
24
|
const [show, setShow] = useState(false);
|
|
21
25
|
const [internalMessage, setInternalMessage] = useState(statusMessage);
|
|
@@ -1,5 +1,4 @@
|
|
|
1
|
-
import { useContext, useState, useCallback, useEffect } from 'react';
|
|
2
|
-
import { useTimeoutFn } from 'react-use';
|
|
1
|
+
import { useContext, useState, useCallback, useEffect, useRef } from 'react';
|
|
3
2
|
import classnames from 'classnames';
|
|
4
3
|
import type { PlayroomProps } from '../Playroom';
|
|
5
4
|
import { StoreContext } from '../../StoreContext/StoreContext';
|
|
@@ -74,6 +73,8 @@ export default ({ themes: allThemes, widths: allWidths, snippets }: Props) => {
|
|
|
74
73
|
visibleThemes.length > 0 || visibleWidths.length > 0;
|
|
75
74
|
const isOpen = Boolean(activeToolbarPanel);
|
|
76
75
|
|
|
76
|
+
const panelRef = useRef<HTMLDivElement>(null);
|
|
77
|
+
|
|
77
78
|
return (
|
|
78
79
|
<div
|
|
79
80
|
className={classnames(styles.root, {
|
|
@@ -135,7 +136,7 @@ export default ({ themes: allThemes, widths: allWidths, snippets }: Props) => {
|
|
|
135
136
|
|
|
136
137
|
<div>
|
|
137
138
|
<ToolbarItem
|
|
138
|
-
title=
|
|
139
|
+
title="Copy Playroom link"
|
|
139
140
|
success={copying}
|
|
140
141
|
onClick={copyHandler}
|
|
141
142
|
>
|
|
@@ -157,13 +158,14 @@ export default ({ themes: allThemes, widths: allWidths, snippets }: Props) => {
|
|
|
157
158
|
</div>
|
|
158
159
|
<CSSTransition
|
|
159
160
|
in={isOpen}
|
|
161
|
+
nodeRef={panelRef}
|
|
160
162
|
timeout={ANIMATION_TIMEOUT}
|
|
161
163
|
classNames={styles.transitionStyles}
|
|
162
164
|
mountOnEnter
|
|
163
165
|
unmountOnExit
|
|
164
166
|
onExited={() => setLastActivePanel(undefined)}
|
|
165
167
|
>
|
|
166
|
-
<div className={styles.panel} id="custom-id">
|
|
168
|
+
<div className={styles.panel} id="custom-id" ref={panelRef}>
|
|
167
169
|
{lastActivePanel === 'snippets' && (
|
|
168
170
|
<Snippets
|
|
169
171
|
isOpen={isOpen}
|
|
@@ -205,3 +207,53 @@ export default ({ themes: allThemes, widths: allWidths, snippets }: Props) => {
|
|
|
205
207
|
</div>
|
|
206
208
|
);
|
|
207
209
|
};
|
|
210
|
+
|
|
211
|
+
// copied directly from `react-use`
|
|
212
|
+
// https://github.com/streamich/react-use/blob/db07ab65bfa48a399e7fd83f172653eb342882b1/src/useTimeoutFn.ts#L3-L40
|
|
213
|
+
type UseTimeoutFnReturn = [() => boolean | null, () => void, () => void];
|
|
214
|
+
|
|
215
|
+
function useTimeoutFn<T extends () => void>(
|
|
216
|
+
fn: T,
|
|
217
|
+
ms: number = 0
|
|
218
|
+
): UseTimeoutFnReturn {
|
|
219
|
+
const ready = useRef<boolean | null>(false);
|
|
220
|
+
const timeout = useRef<ReturnType<typeof setTimeout> | undefined>(undefined);
|
|
221
|
+
const callback = useRef(fn);
|
|
222
|
+
|
|
223
|
+
const isReady = useCallback(() => ready.current, []);
|
|
224
|
+
|
|
225
|
+
const set = useCallback(() => {
|
|
226
|
+
ready.current = false;
|
|
227
|
+
if (timeout.current) {
|
|
228
|
+
clearTimeout(timeout.current);
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
timeout.current = setTimeout(() => {
|
|
232
|
+
ready.current = true;
|
|
233
|
+
callback.current();
|
|
234
|
+
}, ms);
|
|
235
|
+
}, [ms]);
|
|
236
|
+
|
|
237
|
+
const clear = useCallback(() => {
|
|
238
|
+
ready.current = null;
|
|
239
|
+
if (timeout.current) {
|
|
240
|
+
clearTimeout(timeout.current);
|
|
241
|
+
}
|
|
242
|
+
}, []);
|
|
243
|
+
|
|
244
|
+
// update ref when function changes
|
|
245
|
+
useEffect(() => {
|
|
246
|
+
callback.current = fn;
|
|
247
|
+
}, [fn]);
|
|
248
|
+
|
|
249
|
+
// set on mount, clear on unmount
|
|
250
|
+
useEffect(() => {
|
|
251
|
+
set();
|
|
252
|
+
|
|
253
|
+
return clear;
|
|
254
|
+
// disabled in the original implementation
|
|
255
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
256
|
+
}, [ms]);
|
|
257
|
+
|
|
258
|
+
return [isReady, clear, set];
|
|
259
|
+
}
|
|
@@ -1,11 +1,11 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type { ReactElement } from 'react';
|
|
2
2
|
import classnames from 'classnames';
|
|
3
3
|
import TickIcon from '../icons/TickIcon';
|
|
4
4
|
|
|
5
5
|
import * as styles from './ToolbarItem.css';
|
|
6
6
|
|
|
7
7
|
interface Props {
|
|
8
|
-
children:
|
|
8
|
+
children: ReactElement;
|
|
9
9
|
title: string;
|
|
10
10
|
active?: boolean;
|
|
11
11
|
success?: boolean;
|
package/src/Playroom/palettes.ts
CHANGED
|
@@ -1,5 +1,3 @@
|
|
|
1
|
-
import { transparentize, mix, darken } from 'polished';
|
|
2
|
-
|
|
3
1
|
const originalPalette = {
|
|
4
2
|
blue0: '#e5f3ff',
|
|
5
3
|
blue1: '#0088ff',
|
|
@@ -22,6 +20,44 @@ const originalPalette = {
|
|
|
22
20
|
black: '#000',
|
|
23
21
|
};
|
|
24
22
|
|
|
23
|
+
const guard = (amount: number) => {
|
|
24
|
+
if (amount > 1 || amount < 0) {
|
|
25
|
+
throw new Error('Amount must be between 0 and 1 inclusive');
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
return amount;
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Subtracts `amount` from the alpha channel of `color`.
|
|
33
|
+
* Amount must be between 0 and 1 inclusive.
|
|
34
|
+
*
|
|
35
|
+
* Similar to `transparentize` from polished but uses CSS
|
|
36
|
+
* @see https://polished.js.org/docs/#transparentize
|
|
37
|
+
*/
|
|
38
|
+
const transparentize = (amount: number, color: string) =>
|
|
39
|
+
`rgb(from ${color} r g b / calc(alpha - ${guard(amount)}))`;
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Subtracts `amount` from the lightness of `color`.
|
|
43
|
+
* Amount must be between 0 and 1 inclusive.
|
|
44
|
+
*
|
|
45
|
+
* Similar to `darken` from polished but uses CSS
|
|
46
|
+
* @see https://polished.js.org/docs/#darken
|
|
47
|
+
*/
|
|
48
|
+
const darken = (amount: number, color: string) =>
|
|
49
|
+
`hsl(from ${color} h s calc(l - ${guard(amount) * 100}))`;
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Mixes `amount` of `color1` into `color2`.
|
|
53
|
+
* Amount must be between 0 and 1 inclusive.
|
|
54
|
+
*
|
|
55
|
+
* Similar to `mix` from polished but uses CSS
|
|
56
|
+
* @see https://polished.js.org/docs/#mix
|
|
57
|
+
*/
|
|
58
|
+
const mix = (amount: number, color1: string, color2: string) =>
|
|
59
|
+
`color-mix(in srgb, ${color1} ${guard(amount) * 100}%, ${color2})`;
|
|
60
|
+
|
|
25
61
|
export const light = {
|
|
26
62
|
code: {
|
|
27
63
|
text: originalPalette.black,
|
|
@@ -42,7 +78,7 @@ export const light = {
|
|
|
42
78
|
positive: originalPalette.green2,
|
|
43
79
|
},
|
|
44
80
|
background: {
|
|
45
|
-
transparent: '
|
|
81
|
+
transparent: 'rgb(0, 0, 0, .05)',
|
|
46
82
|
accent: originalPalette.blue2,
|
|
47
83
|
positive: originalPalette.green1,
|
|
48
84
|
critical: originalPalette.red1,
|
|
@@ -56,7 +92,7 @@ export const light = {
|
|
|
56
92
|
standard: originalPalette.gray2,
|
|
57
93
|
},
|
|
58
94
|
shadows: {
|
|
59
|
-
small: '0 2px 8px
|
|
95
|
+
small: '0 2px 8px rgb(18, 21, 26, 0.3)',
|
|
60
96
|
focus: `0 0 0 5px ${originalPalette.blue0}`,
|
|
61
97
|
},
|
|
62
98
|
};
|
|
@@ -144,7 +180,7 @@ export const dark = {
|
|
|
144
180
|
positive: seekPalette.mint[500],
|
|
145
181
|
},
|
|
146
182
|
background: {
|
|
147
|
-
transparent: '
|
|
183
|
+
transparent: 'rgb(255, 255, 255, .07)',
|
|
148
184
|
accent: seekPalette.blue[500],
|
|
149
185
|
positive: mix(0.6, seekPalette.grey[900], seekPalette.mint[500]),
|
|
150
186
|
critical: mix(0.7, seekPalette.grey[900], seekPalette.red[600]),
|
|
@@ -438,7 +438,7 @@ const initialState: State = {
|
|
|
438
438
|
editorHeight: defaultEditorSize,
|
|
439
439
|
editorWidth: defaultEditorSize,
|
|
440
440
|
ready: false,
|
|
441
|
-
colorScheme: '
|
|
441
|
+
colorScheme: 'system',
|
|
442
442
|
};
|
|
443
443
|
|
|
444
444
|
export const StoreContext = createContext<StoreContextValues>([
|
package/src/index.d.ts
CHANGED
package/src/index.js
CHANGED
|
@@ -5,67 +5,58 @@ import playroomConfig from './config';
|
|
|
5
5
|
import faviconPath from '../images/favicon.png';
|
|
6
6
|
import faviconInvertedPath from '../images/favicon-inverted.png';
|
|
7
7
|
|
|
8
|
-
const
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
module.hot.accept('./themes', () => {
|
|
64
|
-
renderPlayroom({ themes: require('./themes') });
|
|
65
|
-
});
|
|
66
|
-
|
|
67
|
-
module.hot.accept('./snippets', () => {
|
|
68
|
-
renderPlayroom({ snippets: require('./snippets') });
|
|
69
|
-
});
|
|
70
|
-
}
|
|
71
|
-
});
|
|
8
|
+
const widths = playroomConfig.widths || [320, 375, 768, 1024];
|
|
9
|
+
|
|
10
|
+
const outlet = document.createElement('div');
|
|
11
|
+
document.body.appendChild(outlet);
|
|
12
|
+
|
|
13
|
+
const selectedElement = document.head.querySelector('link[rel="icon"]');
|
|
14
|
+
const favicon = window.matchMedia('(prefers-color-scheme: dark)').matches
|
|
15
|
+
? faviconInvertedPath
|
|
16
|
+
: faviconPath;
|
|
17
|
+
|
|
18
|
+
if (selectedElement) {
|
|
19
|
+
selectedElement.setAttribute('href', favicon);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const renderPlayroom = ({
|
|
23
|
+
themes = require('./themes'),
|
|
24
|
+
components = require('./components'),
|
|
25
|
+
snippets = require('./snippets'),
|
|
26
|
+
} = {}) => {
|
|
27
|
+
const themeNames = Object.keys(themes);
|
|
28
|
+
|
|
29
|
+
// Exclude undefined components, e.g. an exported TypeScript type.
|
|
30
|
+
const filteredComponents = Object.fromEntries(
|
|
31
|
+
Object.entries(components).filter(([_, value]) => value)
|
|
32
|
+
);
|
|
33
|
+
|
|
34
|
+
renderElement(
|
|
35
|
+
<StoreProvider themes={themeNames} widths={widths}>
|
|
36
|
+
<Playroom
|
|
37
|
+
components={filteredComponents}
|
|
38
|
+
widths={widths}
|
|
39
|
+
themes={themeNames}
|
|
40
|
+
snippets={
|
|
41
|
+
typeof snippets.default !== 'undefined' ? snippets.default : snippets
|
|
42
|
+
}
|
|
43
|
+
/>
|
|
44
|
+
</StoreProvider>,
|
|
45
|
+
outlet
|
|
46
|
+
);
|
|
47
|
+
};
|
|
48
|
+
renderPlayroom();
|
|
49
|
+
|
|
50
|
+
if (module.hot) {
|
|
51
|
+
module.hot.accept('./components', () => {
|
|
52
|
+
renderPlayroom({ components: require('./components') });
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
module.hot.accept('./themes', () => {
|
|
56
|
+
renderPlayroom({ themes: require('./themes') });
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
module.hot.accept('./snippets', () => {
|
|
60
|
+
renderPlayroom({ snippets: require('./snippets') });
|
|
61
|
+
});
|
|
62
|
+
}
|
package/src/preview.js
CHANGED
|
@@ -1,9 +1,22 @@
|
|
|
1
1
|
import { renderElement } from './render';
|
|
2
2
|
import Preview from './Playroom/Preview';
|
|
3
|
+
import faviconPath from '../images/favicon.png';
|
|
4
|
+
import faviconInvertedPath from '../images/favicon-inverted.png';
|
|
3
5
|
|
|
4
6
|
const outlet = document.createElement('div');
|
|
5
7
|
document.body.appendChild(outlet);
|
|
6
8
|
|
|
9
|
+
const selectedElement = document.head.querySelector('link[rel="icon"]');
|
|
10
|
+
const favicon = window.matchMedia('(prefers-color-scheme: dark)').matches
|
|
11
|
+
? faviconInvertedPath
|
|
12
|
+
: faviconPath;
|
|
13
|
+
|
|
14
|
+
const formattedFavicon = `../${favicon}`;
|
|
15
|
+
|
|
16
|
+
if (selectedElement) {
|
|
17
|
+
selectedElement.setAttribute('href', formattedFavicon);
|
|
18
|
+
}
|
|
19
|
+
|
|
7
20
|
const renderPreview = ({
|
|
8
21
|
themes = require('./themes'),
|
|
9
22
|
components = require('./components'),
|
package/src/render.js
CHANGED
|
@@ -1,11 +1,13 @@
|
|
|
1
1
|
import ReactDOM, { version as reactDomVersion } from 'react-dom';
|
|
2
2
|
|
|
3
3
|
// Uses the correct render API based on the available version of
|
|
4
|
-
// `react-dom`.
|
|
5
|
-
//
|
|
4
|
+
// `react-dom`.
|
|
5
|
+
// Todo - remove check when support for React 17 is removed
|
|
6
6
|
const canUseNewReactRootApi =
|
|
7
7
|
reactDomVersion &&
|
|
8
|
-
(reactDomVersion.startsWith('18') ||
|
|
8
|
+
(reactDomVersion.startsWith('18') ||
|
|
9
|
+
reactDomVersion.startsWith('19') ||
|
|
10
|
+
reactDomVersion.startsWith('0.0.0'));
|
|
9
11
|
|
|
10
12
|
export const renderElement = (node, outlet) => {
|
|
11
13
|
if (canUseNewReactRootApi) {
|
|
@@ -14,6 +16,7 @@ export const renderElement = (node, outlet) => {
|
|
|
14
16
|
const root = createRoot(outlet);
|
|
15
17
|
root.render(node);
|
|
16
18
|
} else {
|
|
19
|
+
// eslint-disable-next-line react/no-deprecated
|
|
17
20
|
ReactDOM.render(node, outlet);
|
|
18
21
|
}
|
|
19
22
|
};
|
|
@@ -1,11 +1,11 @@
|
|
|
1
|
-
import omit from 'lodash/omit';
|
|
2
1
|
// @ts-expect-error
|
|
3
2
|
import parsePropTypes from 'parse-prop-types';
|
|
4
3
|
import type { PlayroomProps } from '../Playroom/Playroom';
|
|
5
4
|
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
5
|
+
export default (
|
|
6
|
+
components: PlayroomProps['components'],
|
|
7
|
+
staticTypes: typeof __PLAYROOM_GLOBAL__STATIC_TYPES__ = {}
|
|
8
|
+
) => {
|
|
9
9
|
const componentNames = Object.keys(components).sort();
|
|
10
10
|
|
|
11
11
|
return Object.assign(
|
|
@@ -23,8 +23,9 @@ export default (components: PlayroomProps['components']) => {
|
|
|
23
23
|
};
|
|
24
24
|
}
|
|
25
25
|
|
|
26
|
-
const
|
|
27
|
-
|
|
26
|
+
const { children, ...filteredPropTypes } = parsePropTypes(
|
|
27
|
+
components[componentName]
|
|
28
|
+
);
|
|
28
29
|
const propNames = Object.keys(filteredPropTypes);
|
|
29
30
|
|
|
30
31
|
return {
|
|
@@ -1,66 +0,0 @@
|
|
|
1
|
-
import dedent from 'dedent';
|
|
2
|
-
import { type ErrorWithLocation, compileJsx, validateCode } from './compileJsx';
|
|
3
|
-
|
|
4
|
-
describe('compileJsx', () => {
|
|
5
|
-
test('valid code', () => {
|
|
6
|
-
expect(
|
|
7
|
-
compileJsx(dedent`
|
|
8
|
-
<Foo />
|
|
9
|
-
<Bar />
|
|
10
|
-
`)
|
|
11
|
-
).toMatchInlineSnapshot(`
|
|
12
|
-
"R_cE(R_F, null, R_cE(Foo, null )
|
|
13
|
-
, R_cE(Bar, null ))"
|
|
14
|
-
`);
|
|
15
|
-
});
|
|
16
|
-
|
|
17
|
-
test('invalid code - no error', () => {
|
|
18
|
-
expect(
|
|
19
|
-
compileJsx(`
|
|
20
|
-
<Foo--BarBaz ::invalid />
|
|
21
|
-
`)
|
|
22
|
-
).toMatchInlineSnapshot(
|
|
23
|
-
`"R_cE(R_F, null, R_cE(Foo--BarBaz ::invalid, null ))"`
|
|
24
|
-
);
|
|
25
|
-
});
|
|
26
|
-
|
|
27
|
-
test('invalid code - with error', () => {
|
|
28
|
-
expect(() =>
|
|
29
|
-
compileJsx(`
|
|
30
|
-
<Foo />
|
|
31
|
-
<Bar />
|
|
32
|
-
<Foo--BarBaz>
|
|
33
|
-
`)
|
|
34
|
-
).toThrowErrorMatchingInlineSnapshot(`"Unterminated JSX contents (3:25)"`);
|
|
35
|
-
});
|
|
36
|
-
});
|
|
37
|
-
|
|
38
|
-
describe('validateCode', () => {
|
|
39
|
-
test('valid code', () => {
|
|
40
|
-
expect(
|
|
41
|
-
validateCode(`
|
|
42
|
-
<Foo />
|
|
43
|
-
<Bar />
|
|
44
|
-
`)
|
|
45
|
-
).toBe(true);
|
|
46
|
-
});
|
|
47
|
-
|
|
48
|
-
test('invalid code', () => {
|
|
49
|
-
const error = validateCode(`- line 1
|
|
50
|
-
<Foo /> - line 2
|
|
51
|
-
<Bar /> - line 3
|
|
52
|
-
<This is not ::valid /> - line 4
|
|
53
|
-
^ column 20
|
|
54
|
-
`);
|
|
55
|
-
expect(error).toMatchInlineSnapshot(
|
|
56
|
-
`[SyntaxError: Unexpected token (4:20)]`
|
|
57
|
-
);
|
|
58
|
-
expect((error as ErrorWithLocation).loc).toMatchInlineSnapshot(`
|
|
59
|
-
Position {
|
|
60
|
-
"column": 20,
|
|
61
|
-
"index": 113,
|
|
62
|
-
"line": 4,
|
|
63
|
-
}
|
|
64
|
-
`);
|
|
65
|
-
});
|
|
66
|
-
});
|
package/src/utils/cursor.spec.ts
DELETED
|
@@ -1,57 +0,0 @@
|
|
|
1
|
-
import dedent from 'dedent';
|
|
2
|
-
import { isValidLocation } from './cursor';
|
|
3
|
-
|
|
4
|
-
const code = dedent`
|
|
5
|
-
<a>
|
|
6
|
-
<b />
|
|
7
|
-
<c>
|
|
8
|
-
<d>...</d>
|
|
9
|
-
<e />
|
|
10
|
-
...
|
|
11
|
-
<f>
|
|
12
|
-
<g />
|
|
13
|
-
</f>
|
|
14
|
-
<h
|
|
15
|
-
i="j"
|
|
16
|
-
/>
|
|
17
|
-
</c>
|
|
18
|
-
</a>`;
|
|
19
|
-
|
|
20
|
-
describe('cursor', () => {
|
|
21
|
-
describe('isValidLocation', () => {
|
|
22
|
-
describe('with cursor', () => {
|
|
23
|
-
[
|
|
24
|
-
{
|
|
25
|
-
should: 'start of line after before component is valid',
|
|
26
|
-
input: { code, cursor: { line: 1, ch: 0 } },
|
|
27
|
-
output: true,
|
|
28
|
-
},
|
|
29
|
-
{
|
|
30
|
-
should: 'end of line after component is valid',
|
|
31
|
-
input: { code, cursor: { line: 1, ch: 7 } },
|
|
32
|
-
output: true,
|
|
33
|
-
},
|
|
34
|
-
{
|
|
35
|
-
should: 'middle of line inside component is valid',
|
|
36
|
-
input: { code, cursor: { line: 3, ch: 7 } },
|
|
37
|
-
output: true,
|
|
38
|
-
},
|
|
39
|
-
{
|
|
40
|
-
should: 'middle of line inside tag is not valid',
|
|
41
|
-
input: { code, cursor: { line: 3, ch: 5 } },
|
|
42
|
-
output: false,
|
|
43
|
-
},
|
|
44
|
-
{
|
|
45
|
-
should: 'start of line inside between attributes is not valid',
|
|
46
|
-
input: { code, cursor: { line: 10, ch: 0 } },
|
|
47
|
-
output: false,
|
|
48
|
-
},
|
|
49
|
-
].forEach(({ should, input, output }) => {
|
|
50
|
-
// eslint-disable-next-line jest/valid-title
|
|
51
|
-
it(should, () => {
|
|
52
|
-
expect(isValidLocation(input)).toEqual(output);
|
|
53
|
-
});
|
|
54
|
-
});
|
|
55
|
-
});
|
|
56
|
-
});
|
|
57
|
-
});
|
|
@@ -1,135 +0,0 @@
|
|
|
1
|
-
import {
|
|
2
|
-
positionToCursorOffset,
|
|
3
|
-
cursorOffsetToPosition,
|
|
4
|
-
formatCode,
|
|
5
|
-
formatAndInsert,
|
|
6
|
-
} from './formatting';
|
|
7
|
-
|
|
8
|
-
describe('cursor offset to position', () => {
|
|
9
|
-
it('should work for one line', () => {
|
|
10
|
-
const code = `<h1>Title</h1>`;
|
|
11
|
-
const position = 4; // Before the capital T
|
|
12
|
-
|
|
13
|
-
expect(cursorOffsetToPosition(code, position)).toEqual({ line: 0, ch: 4 });
|
|
14
|
-
});
|
|
15
|
-
|
|
16
|
-
it('should work across multiple lines', () => {
|
|
17
|
-
const code = `<div>\n<h1>Title</h1>\n</div>`;
|
|
18
|
-
const position = 10; // Before the capital T
|
|
19
|
-
|
|
20
|
-
expect(cursorOffsetToPosition(code, position)).toEqual({ line: 1, ch: 4 });
|
|
21
|
-
});
|
|
22
|
-
});
|
|
23
|
-
|
|
24
|
-
describe('position to cursor offset', () => {
|
|
25
|
-
it('should work for one line', () => {
|
|
26
|
-
const code = `<h1>Title</h1>`;
|
|
27
|
-
const offset = {
|
|
28
|
-
line: 0,
|
|
29
|
-
ch: 4,
|
|
30
|
-
}; // Before the capital T
|
|
31
|
-
|
|
32
|
-
expect(positionToCursorOffset(code, offset)).toEqual(4);
|
|
33
|
-
});
|
|
34
|
-
|
|
35
|
-
it('should work across multiple lines', () => {
|
|
36
|
-
const code = `<div>\n<h1>Title</h1>\n</div>`;
|
|
37
|
-
const offset = {
|
|
38
|
-
line: 1,
|
|
39
|
-
ch: 4,
|
|
40
|
-
};
|
|
41
|
-
|
|
42
|
-
expect(positionToCursorOffset(code, offset)).toEqual(10);
|
|
43
|
-
});
|
|
44
|
-
});
|
|
45
|
-
|
|
46
|
-
describe('formatting code', () => {
|
|
47
|
-
it('should handle one line', () => {
|
|
48
|
-
const code = `<div><h1>Title</h1></div>`;
|
|
49
|
-
expect(formatCode({ code, cursor: { line: 0, ch: 9 } })).toEqual({
|
|
50
|
-
cursor: { line: 1, ch: 6 },
|
|
51
|
-
code: `<div>\n <h1>Title</h1>\n</div>\n`,
|
|
52
|
-
});
|
|
53
|
-
});
|
|
54
|
-
|
|
55
|
-
it('should handle multiple lines', () => {
|
|
56
|
-
const code = `<div>\n<h1>Title</h1>\n</div>`;
|
|
57
|
-
expect(formatCode({ code, cursor: { line: 1, ch: 4 } })).toEqual({
|
|
58
|
-
cursor: { line: 1, ch: 6 },
|
|
59
|
-
code: `<div>\n <h1>Title</h1>\n</div>\n`,
|
|
60
|
-
});
|
|
61
|
-
});
|
|
62
|
-
|
|
63
|
-
it('should handle multiple lines with cursor at start of line', () => {
|
|
64
|
-
const code = `<div>\n<h1>Title</h1>\n</div>`;
|
|
65
|
-
expect(formatCode({ code, cursor: { line: 1, ch: 0 } })).toEqual({
|
|
66
|
-
cursor: { line: 1, ch: 0 },
|
|
67
|
-
code: `<div>\n <h1>Title</h1>\n</div>\n`,
|
|
68
|
-
});
|
|
69
|
-
});
|
|
70
|
-
|
|
71
|
-
it('should handle multiple root level jsx elements', () => {
|
|
72
|
-
const code = `<div><h1>Title</h1></div><div><h1>Title Two</h1></div>`;
|
|
73
|
-
expect(formatCode({ code, cursor: { line: 0, ch: 34 } })).toEqual({
|
|
74
|
-
cursor: { line: 4, ch: 6 },
|
|
75
|
-
code: `<div>\n <h1>Title</h1>\n</div>\n<div>\n <h1>Title Two</h1>\n</div>\n`,
|
|
76
|
-
});
|
|
77
|
-
});
|
|
78
|
-
});
|
|
79
|
-
|
|
80
|
-
describe('format and insert', () => {
|
|
81
|
-
it('should handle inserting one line into one line', () => {
|
|
82
|
-
const snippet = '<span>added</span>';
|
|
83
|
-
const code = `<div><h1>Title</h1></div>`;
|
|
84
|
-
expect(
|
|
85
|
-
formatAndInsert({ code, cursor: { line: 0, ch: 9 }, snippet })
|
|
86
|
-
).toEqual({
|
|
87
|
-
cursor: { line: 2, ch: 22 },
|
|
88
|
-
code: `<div>\n <h1>\n <span>added</span>Title\n </h1>\n</div>\n`,
|
|
89
|
-
});
|
|
90
|
-
});
|
|
91
|
-
|
|
92
|
-
it('should handle inserting multiple lines into multiple lines', () => {
|
|
93
|
-
const snippet = '<span>\n <strong>second</strong>\n</span>';
|
|
94
|
-
const code = `<div>\n <h1>\n <span>added</span>Title\n </h1>\n</div>\n`;
|
|
95
|
-
expect(
|
|
96
|
-
formatAndInsert({ code, cursor: { line: 2, ch: 15 }, snippet })
|
|
97
|
-
).toEqual({
|
|
98
|
-
cursor: { line: 6, ch: 13 },
|
|
99
|
-
code: `<div>\n <h1>\n <span>\n added\n <span>\n <strong>second</strong>\n </span>\n </span>\n Title\n </h1>\n</div>\n`,
|
|
100
|
-
});
|
|
101
|
-
});
|
|
102
|
-
|
|
103
|
-
it('should handle inserting at the start', () => {
|
|
104
|
-
const snippet = '<span>\n <strong>second</strong>\n</span>';
|
|
105
|
-
const code = `<div>\n <h1>\n <span>added</span>Title\n </h1>\n</div>\n`;
|
|
106
|
-
expect(
|
|
107
|
-
formatAndInsert({ code, cursor: { line: 0, ch: 0 }, snippet })
|
|
108
|
-
).toEqual({
|
|
109
|
-
cursor: { line: 2, ch: 7 },
|
|
110
|
-
code: `<span>\n <strong>second</strong>\n</span>\n<div>\n <h1>\n <span>added</span>Title\n </h1>\n</div>\n`,
|
|
111
|
-
});
|
|
112
|
-
});
|
|
113
|
-
|
|
114
|
-
it('should handle inserting at the end', () => {
|
|
115
|
-
const snippet = '<span>\n <strong>second</strong>\n</span>';
|
|
116
|
-
const code = `<div>\n <h1>\n <span>added</span>Title\n </h1>\n</div>\n`;
|
|
117
|
-
expect(
|
|
118
|
-
formatAndInsert({ code, cursor: { line: 5, ch: 0 }, snippet })
|
|
119
|
-
).toEqual({
|
|
120
|
-
cursor: { line: 7, ch: 7 },
|
|
121
|
-
code: `<div>\n <h1>\n <span>added</span>Title\n </h1>\n</div>\n<span>\n <strong>second</strong>\n</span>\n`,
|
|
122
|
-
});
|
|
123
|
-
});
|
|
124
|
-
|
|
125
|
-
it('should handle inserting at the end after multiple new lines', () => {
|
|
126
|
-
const snippet = '<span>\n <strong>second</strong>\n</span>\n';
|
|
127
|
-
const code = `<div>\n <h1>\n <span>added</span>Title\n </h1>\n</div>\n\n\n\n`;
|
|
128
|
-
expect(
|
|
129
|
-
formatAndInsert({ code, cursor: { line: 8, ch: 0 }, snippet })
|
|
130
|
-
).toEqual({
|
|
131
|
-
cursor: { line: 9, ch: 0 },
|
|
132
|
-
code: `<div>\n <h1>\n <span>added</span>Title\n </h1>\n</div>\n\n<span>\n <strong>second</strong>\n</span>\n`,
|
|
133
|
-
});
|
|
134
|
-
});
|
|
135
|
-
});
|