playroom 1.0.2 → 1.0.3-react-18-ref-fix-20251211235722

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,28 @@
1
1
  # playroom
2
2
 
3
+ ## 1.0.3-react-18-ref-fix-20251211235722
4
+
5
+ ### Patch Changes
6
+
7
+ - [#461](https://github.com/seek-oss/playroom/pull/461) [`67c2517`](https://github.com/seek-oss/playroom/commit/67c25170b7673844642df04cb496b13dcbba76c6) Thanks [@michaeltaranto](https://github.com/michaeltaranto)! - Design polish, refinements and fixes to internal system components.
8
+
9
+ - [`49461ea`](https://github.com/seek-oss/playroom/commit/49461ea8763d5d56c73158598b96f252bd53227d) Thanks [@michaeltaranto](https://github.com/michaeltaranto)! - Ensure `ref` and `inert` usage is React 18 compatible
10
+
11
+ - [#461](https://github.com/seek-oss/playroom/pull/461) [`67c2517`](https://github.com/seek-oss/playroom/commit/67c25170b7673844642df04cb496b13dcbba76c6) Thanks [@michaeltaranto](https://github.com/michaeltaranto)! - Introduce scroll afforance for frames area
12
+
13
+ Add subtle gradient to edges of frames container to indicate scrollability when the number of frames exceeds the window width
14
+
15
+ - [#461](https://github.com/seek-oss/playroom/pull/461) [`67c2517`](https://github.com/seek-oss/playroom/commit/67c25170b7673844642df04cb496b13dcbba76c6) Thanks [@michaeltaranto](https://github.com/michaeltaranto)! - Allow selection of frame error
16
+
17
+ Enables a user to select and copy the text from a frame error message.
18
+
19
+ - [#462](https://github.com/seek-oss/playroom/pull/462) [`65f7793`](https://github.com/seek-oss/playroom/commit/65f779344e6cc38fcfc64a2fa56f304476a75dda) Thanks [@michaeltaranto](https://github.com/michaeltaranto)! - Only hide share actions from header without code
20
+
21
+ The header actions in the top right are now available on page load.
22
+ This enables the selection of frames and/or themes before any code is added to the editor.
23
+
24
+ The share and preview actions are still hidden and revealed when code is entered into the editor.
25
+
3
26
  ## 1.0.2
4
27
 
5
28
  ### Patch Changes
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "playroom",
3
- "version": "1.0.2",
3
+ "version": "1.0.3-react-18-ref-fix-20251211235722",
4
4
  "description": "Design with code, powered by your own component library",
5
5
  "bin": {
6
6
  "playroom": "bin/cli.cjs"
@@ -88,7 +88,7 @@ export const height = {
88
88
  height: sizeVar,
89
89
  },
90
90
  ]),
91
- content: sprinkles({ paddingY: 'small' }),
91
+ content: sprinkles({ paddingY: 'medium' }),
92
92
  };
93
93
 
94
94
  export const tone = {
@@ -31,8 +31,8 @@ export const button = style([
31
31
  {
32
32
  background: 'transparent',
33
33
  outline: 'none',
34
- height: sizeVar,
35
- width: sizeVar,
34
+ height: `${sizeVar} !important`,
35
+ width: `${sizeVar} !important`,
36
36
  },
37
37
  ]);
38
38
 
@@ -50,6 +50,7 @@ export const content = style([
50
50
  transition: 'fast',
51
51
  height: 'full',
52
52
  width: 'full',
53
+ pointerEvents: 'none',
53
54
  }),
54
55
  {
55
56
  padding: paddingVar,
@@ -65,6 +66,11 @@ export const content = style([
65
66
  [`${button}:focus-visible &`]: {
66
67
  outline: `2px solid ${colorPaletteVars.outline.focus}`,
67
68
  },
69
+ [`${button}[aria-disabled="true"] &`]: {
70
+ vars: {
71
+ [foreground]: colorPaletteVars.foreground.secondary,
72
+ },
73
+ },
68
74
  },
69
75
  },
70
76
  ]);
@@ -101,7 +107,10 @@ export const variant = styleVariants({
101
107
  outline: `1px solid ${colorPaletteVars.border.standard}`,
102
108
  outlineOffset: -1,
103
109
  selectors: {
104
- [comma('&:hover', '[data-popup-open] > &')]: {
110
+ [comma(
111
+ `${button}:not([aria-disabled="true"]):hover > &`,
112
+ `${button}:not([aria-disabled="true"])[data-popup-open] > &`
113
+ )]: {
105
114
  backgroundColor: colorPaletteVars.background.selection,
106
115
  },
107
116
  },
@@ -114,7 +123,10 @@ export const variant = styleVariants({
114
123
  },
115
124
  backgroundColor: colorPaletteVars.background.secondaryAccent,
116
125
  selectors: {
117
- [comma('&:hover', '[data-popup-open] > &')]: {
126
+ [comma(
127
+ `${button}:not([aria-disabled="true"]):hover > &`,
128
+ `${button}:not([aria-disabled="true"])[data-popup-open] > &`
129
+ )]: {
118
130
  backgroundColor: colorPaletteVars.background.secondaryAccentLight,
119
131
  },
120
132
  },
@@ -123,7 +135,10 @@ export const variant = styleVariants({
123
135
  transparent: [
124
136
  {
125
137
  selectors: {
126
- [comma('&:hover', '[data-popup-open] > &')]: {
138
+ [comma(
139
+ `${button}:not([aria-disabled="true"]):hover > &`,
140
+ `${button}:not([aria-disabled="true"])[data-popup-open] > &`
141
+ )]: {
127
142
  backgroundColor: colorPaletteVars.background.selection,
128
143
  },
129
144
  },
@@ -13,6 +13,7 @@ type ButtonIconBaseProps = {
13
13
  tone?: keyof typeof styles.tone;
14
14
  variant?: keyof typeof styles.variant;
15
15
  bleed?: boolean;
16
+ disabledReason?: string;
16
17
  };
17
18
  export type ButtonIconProps = TooltipTrigger & ButtonIconBaseProps;
18
19
  export const ButtonIcon = forwardRef<HTMLButtonElement, ButtonIconProps>(
@@ -24,16 +25,21 @@ export const ButtonIcon = forwardRef<HTMLButtonElement, ButtonIconProps>(
24
25
  tone = 'neutral',
25
26
  variant = 'standard',
26
27
  bleed,
28
+ disabled,
29
+ disabledReason,
30
+ type = 'button',
27
31
  ...restProps
28
32
  },
29
33
  ref
30
34
  ) => (
31
35
  <Tooltip
32
- label={label}
36
+ label={disabled && disabledReason ? disabledReason : label}
33
37
  trigger={
34
38
  <button
35
39
  {...restProps}
40
+ type={type}
36
41
  ref={ref}
42
+ aria-disabled={disabled}
37
43
  aria-label={label}
38
44
  className={clsx({
39
45
  [styles.button]: true,
@@ -4,7 +4,12 @@ import {
4
4
  Ellipsis,
5
5
  type LucideIcon,
6
6
  } from 'lucide-react';
7
- import { useContext, useRef, type ButtonHTMLAttributes } from 'react';
7
+ import {
8
+ forwardRef,
9
+ useContext,
10
+ useRef,
11
+ type ButtonHTMLAttributes,
12
+ } from 'react';
8
13
 
9
14
  import { useEditor } from '../../contexts/EditorContext';
10
15
  import { StoreContext } from '../../contexts/StoreContext';
@@ -38,7 +43,10 @@ interface EditorActionButtonProps
38
43
  hideTooltip?: boolean;
39
44
  }
40
45
 
41
- const EditorActionButton = (props: EditorActionButtonProps) => {
46
+ const EditorActionButton = forwardRef<
47
+ HTMLButtonElement,
48
+ EditorActionButtonProps
49
+ >((props, ref) => {
42
50
  const {
43
51
  onClick,
44
52
  name,
@@ -77,6 +85,7 @@ const EditorActionButton = (props: EditorActionButtonProps) => {
77
85
  trigger={
78
86
  <button
79
87
  {...restProps}
88
+ ref={ref}
80
89
  onClick={handleClick}
81
90
  aria-disabled={disabled}
82
91
  className={styles.button}
@@ -88,7 +97,7 @@ const EditorActionButton = (props: EditorActionButtonProps) => {
88
97
  }
89
98
  />
90
99
  );
91
- };
100
+ });
92
101
 
93
102
  const menuSideOffset = 2;
94
103
  const overflowCommands = editorCommandList.filter(
@@ -26,6 +26,7 @@ export const message = style([
26
26
  boxShadow: `0 2px 10px -2px ${light.foreground.critical}`,
27
27
  wordBreak: 'break-word',
28
28
  whiteSpace: 'pre-line',
29
+ userSelect: 'all',
29
30
  selectors: {
30
31
  [`&:not(${show})`]: {
31
32
  transform: `translateY(${calc(gutter).negate()})`,
@@ -3,9 +3,10 @@ import { createVar, style } from '@vanilla-extract/css';
3
3
  import { comma } from '../../css/delimiters';
4
4
 
5
5
  import { colorPaletteVars, sprinkles } from '../../css/sprinkles.css';
6
+ import { vars } from '../../css/vars.css';
6
7
 
7
8
  const transitionTiming = '150ms ease';
8
-
9
+ const horizontalPadding = 'xlarge';
9
10
  export const root = style([
10
11
  sprinkles({
11
12
  height: 'full',
@@ -14,9 +15,8 @@ export const root = style([
14
15
  display: 'flex',
15
16
  gap: 'xxlarge',
16
17
  paddingY: 'xxlarge',
17
- paddingX: 'xlarge',
18
+ paddingX: horizontalPadding,
18
19
  textAlign: 'center',
19
- overflow: 'auto',
20
20
  userSelect: 'none',
21
21
  }),
22
22
  {
@@ -39,6 +39,12 @@ export const frameContainer = style([
39
39
  {
40
40
  flexShrink: 0,
41
41
  width: frameWidth,
42
+ selectors: {
43
+ // Workaround for scrolling flex container not accounting for padding
44
+ [`&:last-child`]: {
45
+ paddingRight: vars.space[horizontalPadding],
46
+ },
47
+ },
42
48
  },
43
49
  ]);
44
50
 
@@ -24,6 +24,7 @@ import {
24
24
  // screenshotMessageSender,
25
25
  } from '../Frame/frameMessenger';
26
26
  // import { Menu, MenuItem } from '../Menu/Menu';
27
+ import { ScrollContainer } from '../ScrollContainer/ScrollContainer';
27
28
  import { Strong } from '../Strong/Strong';
28
29
  import { Text } from '../Text/Text';
29
30
  import { SharedTooltipContext } from '../Tooltip/Tooltip';
@@ -197,16 +198,22 @@ export default function Frames({ code }: FramesProps) {
197
198
  renderCode.current = compileJsx(code);
198
199
  } catch {}
199
200
  return (
200
- <div ref={scrollingPanelRef} className={styles.root}>
201
- {frames.map((frame) => (
202
- <Frame
203
- key={`${frame.theme}_${frame.width}`}
204
- frame={frame}
205
- code={renderCode.current}
206
- title={title}
207
- scrollingPanelRef={scrollingPanelRef}
208
- />
209
- ))}
210
- </div>
201
+ <ScrollContainer
202
+ ref={scrollingPanelRef}
203
+ direction="horizontal"
204
+ fadeSize="small"
205
+ >
206
+ <div className={styles.root}>
207
+ {frames.map((frame) => (
208
+ <Frame
209
+ key={`${frame.theme}_${frame.width}`}
210
+ frame={frame}
211
+ code={renderCode.current}
212
+ title={title}
213
+ scrollingPanelRef={scrollingPanelRef}
214
+ />
215
+ ))}
216
+ </div>
217
+ </ScrollContainer>
211
218
  );
212
219
  }
@@ -30,7 +30,6 @@ export const menuContainer = style({
30
30
 
31
31
  export const actionsGap = 'xsmall';
32
32
 
33
- export const actionsReady = style({});
34
33
  export const actionsContainer = style([
35
34
  sprinkles({
36
35
  display: 'flex',
@@ -46,8 +45,20 @@ export const actionsContainer = style([
46
45
  display: 'none',
47
46
  },
48
47
  },
48
+ },
49
+ ]);
50
+
51
+ export const shareActionsReady = style({});
52
+ export const shareActions = style([
53
+ sprinkles({
54
+ display: 'flex',
55
+ alignItems: 'center',
56
+ gap: actionsGap,
57
+ transition: 'fast',
58
+ }),
59
+ {
49
60
  selectors: {
50
- [`&:not(${actionsReady})`]: {
61
+ [`&:not(${shareActionsReady})`]: {
51
62
  opacity: 0,
52
63
  transform: `translateX(${vars.space[actionsGap]})`,
53
64
  pointerEvents: 'none',
@@ -398,62 +398,65 @@ export const Header = () => {
398
398
  </div>
399
399
  <Title />
400
400
  <SharedTooltipContext>
401
- <div
402
- className={clsx({
403
- [styles.actionsContainer]: true,
404
- [styles.actionsReady]: hasCode,
405
- })}
406
- >
407
- <div className={styles.segmentedGroup}>
408
- <button
409
- type="button"
410
- className={styles.segmentedTextButton}
411
- onClick={() => setShareOpen(true)}
412
- >
413
- <Text>Share</Text>
414
- </button>
415
- <Tooltip
416
- label={copying ? 'Copied!' : 'Copy link'}
417
- trigger={
418
- <button
419
- type="button"
420
- aria-label="Copy link"
421
- className={styles.segmentedIconButton}
422
- onClick={() => onCopyClick(window.location.href)}
423
- >
424
- {copying ? (
425
- <span className={styles.copyLinkSuccess}>
426
- <Check size={14} />
427
- </span>
428
- ) : (
429
- <Link size={14} />
430
- )}
431
- </button>
432
- }
433
- />
401
+ <div className={styles.actionsContainer}>
402
+ <div
403
+ className={clsx({
404
+ [styles.shareActions]: true,
405
+ [styles.shareActionsReady]: hasCode,
406
+ })}
407
+ inert={!hasCode ? true : undefined}
408
+ >
409
+ <div className={styles.segmentedGroup}>
410
+ <button
411
+ type="button"
412
+ className={styles.segmentedTextButton}
413
+ onClick={() => setShareOpen(true)}
414
+ >
415
+ <Text>Share</Text>
416
+ </button>
417
+ <Tooltip
418
+ label={copying ? 'Copied!' : 'Copy link'}
419
+ trigger={
420
+ <button
421
+ type="button"
422
+ aria-label="Copy link"
423
+ className={styles.segmentedIconButton}
424
+ onClick={() => onCopyClick(window.location.href)}
425
+ >
426
+ {copying ? (
427
+ <span className={styles.copyLinkSuccess}>
428
+ <Check size={14} />
429
+ </span>
430
+ ) : (
431
+ <Link size={14} />
432
+ )}
433
+ </button>
434
+ }
435
+ />
436
+ </div>
437
+
438
+ {themesEnabled && selectedThemes.length !== 1 ? (
439
+ <Menu
440
+ width="content"
441
+ align="end"
442
+ trigger={<ButtonIcon label="Launch Preview" icon={<Play />} />}
443
+ >
444
+ <MenuGroup label="Choose preview theme">
445
+ {availableThemes.map((theme) => (
446
+ <MenuItemThemedPreviewLink key={theme} theme={theme} />
447
+ ))}
448
+ </MenuGroup>
449
+ </Menu>
450
+ ) : (
451
+ <ButtonIconLink
452
+ label="Launch Preview"
453
+ icon={<Play />}
454
+ href={previewUrl}
455
+ target="_blank"
456
+ />
457
+ )}
434
458
  </div>
435
459
 
436
- {themesEnabled && selectedThemes.length !== 1 ? (
437
- <Menu
438
- width="content"
439
- align="end"
440
- trigger={<ButtonIcon label="Launch Preview" icon={<Play />} />}
441
- >
442
- <MenuGroup label="Choose preview theme">
443
- {availableThemes.map((theme) => (
444
- <MenuItemThemedPreviewLink key={theme} theme={theme} />
445
- ))}
446
- </MenuGroup>
447
- </Menu>
448
- ) : (
449
- <ButtonIconLink
450
- label="Launch Preview"
451
- icon={<Play />}
452
- href={previewUrl}
453
- target="_blank"
454
- />
455
- )}
456
-
457
460
  <Menu
458
461
  width="small"
459
462
  align="end"
@@ -81,6 +81,17 @@ export default () => {
81
81
  lastHidden.current = editorHidden;
82
82
  }, [editorHidden]);
83
83
 
84
+ // Remove in favour of direct DOM attribute when we drop React 18 support
85
+ useEffect(() => {
86
+ if (editorRef.current) {
87
+ if (!editorVisible) {
88
+ editorRef.current.setAttribute('inert', '');
89
+ } else {
90
+ editorRef.current.removeAttribute('inert');
91
+ }
92
+ }
93
+ }, [editorVisible]);
94
+
84
95
  return (
85
96
  <Box
86
97
  component="main"
@@ -124,13 +135,12 @@ export default () => {
124
135
  <Box
125
136
  position="relative"
126
137
  className={styles.editor}
127
- inert={!editorVisible}
128
138
  opacity={!editorVisible ? 0 : undefined}
129
139
  pointerEvents={!editorVisible ? 'none' : undefined}
130
140
  ref={editorRef}
131
141
  >
132
142
  <ResizeHandle
133
- ref={editorRef}
143
+ targetRef={editorRef}
134
144
  position={resizeHandlePosition[editorOrientation]}
135
145
  onResize={(newValue) => {
136
146
  dispatch({
@@ -25,14 +25,14 @@ const resolvePosition = (
25
25
 
26
26
  export const ResizeHandle = ({
27
27
  position,
28
- ref,
28
+ targetRef,
29
29
  onResize,
30
30
  onResizeStart,
31
31
  onResizeEnd,
32
32
  }: {
33
33
  position: 'top' | 'right' | 'left';
34
34
  flip?: boolean;
35
- ref: RefObject<HTMLElement | null>;
35
+ targetRef: RefObject<HTMLElement | null>;
36
36
  onResize: (newValue: number) => void;
37
37
  onResizeStart?: (startValue: number) => void;
38
38
  onResizeEnd?: (endValue: number) => void;
@@ -48,7 +48,7 @@ export const ResizeHandle = ({
48
48
  event: React.MouseEvent<HTMLElement> | React.TouchEvent<HTMLElement>
49
49
  ) => {
50
50
  const startPosition = resolvePosition(event.nativeEvent, pagePos);
51
- const startSize = ref.current?.[elementSize] ?? 0;
51
+ const startSize = targetRef.current?.[elementSize] ?? 0;
52
52
 
53
53
  setResizing(true);
54
54
  onResizeStart?.(startSize);
@@ -61,7 +61,7 @@ export const ResizeHandle = ({
61
61
  };
62
62
 
63
63
  const stopHandler = () => {
64
- const endSize = ref.current?.[elementSize] ?? 0;
64
+ const endSize = targetRef.current?.[elementSize] ?? 0;
65
65
  setResizing(false);
66
66
  onResizeEnd?.(endSize);
67
67
  document.body.classList.remove(styles.resizeCursor[direction]);
@@ -18,9 +18,9 @@ export const spaceScale = styleVariants(vars.space, (space) => ({
18
18
 
19
19
  export const horizontalAlignmentScale = styleVariants(
20
20
  {
21
- top: 'flex-start',
21
+ left: 'flex-start',
22
22
  center: 'center',
23
- bottom: 'flex-end',
23
+ right: 'flex-end',
24
24
  },
25
25
  (alignment) => ({
26
26
  alignItems: alignment,
@@ -4,7 +4,7 @@ const fontFamily = '"Plus Jakarta Sans", sans-serif';
4
4
  export const fontSizeDefinitions = {
5
5
  xsmall: [10, 14],
6
6
  small: [12, 16],
7
- standard: [14, 20],
7
+ standard: [14, 24],
8
8
  large: [16, 22],
9
9
  };
10
10
 
@@ -19,6 +19,5 @@ renderElement(
19
19
  />
20
20
  </>
21
21
  )}
22
- </UrlParams>,
23
- document.body
22
+ </UrlParams>
24
23
  );
@@ -19,6 +19,5 @@ renderElement(
19
19
  <EditorProvider>
20
20
  <Playroom />
21
21
  </EditorProvider>
22
- </StoreProvider>,
23
- document.body
22
+ </StoreProvider>
24
23
  );
@@ -20,6 +20,5 @@ renderElement(
20
20
  {({ code, themeName, title }) => (
21
21
  <Preview title={title} code={code} themeName={themeName} />
22
22
  )}
23
- </UrlParams>,
24
- document.body
23
+ </UrlParams>
25
24
  );
package/src/render.ts CHANGED
@@ -1,7 +1,10 @@
1
1
  import type { ReactNode } from 'react';
2
2
  import { createRoot } from 'react-dom/client';
3
3
 
4
- export const renderElement = async (node: ReactNode, outlet: HTMLElement) => {
4
+ const outlet = document.createElement('div');
5
+ document.body.appendChild(outlet);
6
+
7
+ export const renderElement = async (node: ReactNode) => {
5
8
  const root = createRoot(outlet);
6
9
  root.render(node);
7
10
  };