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 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
@@ -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,
@@ -30,7 +30,11 @@ module.exports = async (playroomConfig, options) => {
30
30
  }
31
31
  );
32
32
  const { version } = JSON.parse(pkgContents);
33
- isLegacyReact = !(version.startsWith('18') || version.startsWith('0.0.0'));
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.39.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": "^18.0.26",
48
- "@types/react-dom": "^18.0.9",
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.9.9",
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.1",
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": "^18.0.1",
109
- "react-dom": "^18.0.1",
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 <div className={className} ref={(self) => (this.ref = self!)} />;
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
 
@@ -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,
@@ -1,4 +1,3 @@
1
- import type React from 'react';
2
1
  import type { ReactNode } from 'react';
3
2
  import { useParams } from '../utils/params';
4
3
  import CatchErrors from './CatchErrors/CatchErrors';
@@ -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(widths, (width) =>
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 ReactChild } from 'react';
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, ReactChild> = {
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, ReactChild> = {
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
- const showStatusTimerRef = useRef<ReturnType<typeof setTimeout>>();
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={`Copy Playroom link (${isMac() ? '⌘⇧C' : 'Ctrl+Shift+C'})`}
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 { ReactChild } from 'react';
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: ReactChild;
8
+ children: ReactElement;
9
9
  title: string;
10
10
  active?: boolean;
11
11
  success?: boolean;
@@ -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: 'rgba(0, 0, 0, .05)',
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 rgba(18, 21, 26, 0.3)',
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: 'rgba(255, 255, 255, .07)',
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: 'light',
441
+ colorScheme: 'system',
442
442
  };
443
443
 
444
444
  export const StoreContext = createContext<StoreContextValues>([
package/src/index.d.ts CHANGED
@@ -32,4 +32,7 @@ interface Window {
32
32
  }
33
33
 
34
34
  declare const __PLAYROOM_GLOBAL__CONFIG__: InternalPlayroomConfig;
35
- declare const __PLAYROOM_GLOBAL__STATIC_TYPES__: any;
35
+ declare const __PLAYROOM_GLOBAL__STATIC_TYPES__: Record<
36
+ string,
37
+ Record<string, string[]>
38
+ >;
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 polyfillIntersectionObserver = () =>
9
- typeof window.IntersectionObserver !== 'undefined'
10
- ? Promise.resolve()
11
- : import('intersection-observer');
12
-
13
- polyfillIntersectionObserver().then(() => {
14
- const widths = playroomConfig.widths || [320, 375, 768, 1024];
15
-
16
- const outlet = document.createElement('div');
17
- document.body.appendChild(outlet);
18
-
19
- const selectedElement = document.head.querySelector('link[rel="icon"]');
20
- const favicon = window.matchMedia('(prefers-color-scheme: dark)').matches
21
- ? faviconInvertedPath
22
- : faviconPath;
23
-
24
- if (selectedElement) {
25
- selectedElement.setAttribute('href', favicon);
26
- }
27
-
28
- const renderPlayroom = ({
29
- themes = require('./themes'),
30
- components = require('./components'),
31
- snippets = require('./snippets'),
32
- } = {}) => {
33
- const themeNames = Object.keys(themes);
34
-
35
- // Exclude undefined components, e.g. an exported TypeScript type.
36
- const filteredComponents = Object.fromEntries(
37
- Object.entries(components).filter(([_, value]) => value)
38
- );
39
-
40
- renderElement(
41
- <StoreProvider themes={themeNames} widths={widths}>
42
- <Playroom
43
- components={filteredComponents}
44
- widths={widths}
45
- themes={themeNames}
46
- snippets={
47
- typeof snippets.default !== 'undefined'
48
- ? snippets.default
49
- : snippets
50
- }
51
- />
52
- </StoreProvider>,
53
- outlet
54
- );
55
- };
56
- renderPlayroom();
57
-
58
- if (module.hot) {
59
- module.hot.accept('./components', () => {
60
- renderPlayroom({ components: require('./components') });
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`. This hack can be removed when support for older
5
- // versions of React is removed.
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') || reactDomVersion.startsWith('0.0.0'));
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
- const staticTypes = __PLAYROOM_GLOBAL__STATIC_TYPES__;
7
-
8
- export default (components: PlayroomProps['components']) => {
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 parsedPropTypes = parsePropTypes(components[componentName]);
27
- const filteredPropTypes = omit(parsedPropTypes, 'children');
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
- });
@@ -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
- });