@developer_tribe/react-builder 1.0.8 → 1.0.9
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/dist/build-components/BIcon/BIconProps.generated.d.ts +3 -0
- package/dist/build-components/BackgroundImage/BackgroundImageProps.generated.d.ts +1 -0
- package/dist/build-components/Button/ButtonProps.generated.d.ts +1 -0
- package/dist/build-components/Carousel/CarouselProps.generated.d.ts +5 -0
- package/dist/build-components/CarouselButtons/CarouselButtonsProps.generated.d.ts +1 -0
- package/dist/build-components/CarouselDots/CarouselDotsProps.generated.d.ts +1 -0
- package/dist/build-components/CarouselItem/CarouselItemProps.generated.d.ts +1 -0
- package/dist/build-components/CarouselProvider/CarouselProviderProps.generated.d.ts +1 -0
- package/dist/build-components/Image/ImageProps.generated.d.ts +1 -0
- package/dist/build-components/Main/MainProps.generated.d.ts +1 -1
- package/dist/build-components/Onboard/OnboardProps.generated.d.ts +1 -0
- package/dist/build-components/OnboardButton/OnboardButtonProps.generated.d.ts +1 -0
- package/dist/build-components/OnboardButtons/OnboardButtonsProps.generated.d.ts +1 -0
- package/dist/build-components/OnboardDot/OnboardDotProps.generated.d.ts +1 -0
- package/dist/build-components/OnboardFooter/OnboardFooterProps.generated.d.ts +3 -0
- package/dist/build-components/OnboardImage/OnboardImageProps.generated.d.ts +1 -0
- package/dist/build-components/OnboardItem/OnboardItemProps.generated.d.ts +1 -0
- package/dist/build-components/OnboardProvider/OnboardProviderProps.generated.d.ts +3 -0
- package/dist/build-components/OnboardSubtitle/OnboardSubtitleProps.generated.d.ts +3 -0
- package/dist/build-components/OnboardTitle/OnboardTitleProps.generated.d.ts +3 -0
- package/dist/build-components/PaywallBackground/PaywallBackgroundProps.generated.d.ts +1 -1
- package/dist/build-components/PaywallCloseButton/PaywallCloseButtonProps.generated.d.ts +3 -1
- package/dist/build-components/PaywallOptions/PaywallOptionsProps.generated.d.ts +1 -1
- package/dist/build-components/PaywallProvider/PaywallContext.d.ts +12 -0
- package/dist/build-components/PaywallProvider/PaywallProviderProps.generated.d.ts +1 -1
- package/dist/build-components/PaywallSubscribeButton/PaywallSubscribeButtonProps.generated.d.ts +1 -0
- package/dist/build-components/RadioButton/RadioButtonProps.generated.d.ts +1 -1
- package/dist/build-components/Text/TextProps.generated.d.ts +3 -0
- package/dist/build-components/View/ViewProps.generated.d.ts +1 -0
- package/dist/build-components/patterns.generated.d.ts +372 -374
- package/dist/components/BuilderProvider.d.ts +2 -0
- package/dist/components/ParamsProvider.d.ts +5 -0
- package/dist/components/RenderErrorBoundary.d.ts +28 -0
- package/dist/hooks/useSyncHtmlThemeClass.d.ts +7 -0
- package/dist/index.cjs.js +5 -5
- package/dist/index.cjs.js.map +1 -1
- package/dist/index.d.ts +2 -0
- package/dist/index.esm.js +3 -3
- package/dist/index.esm.js.map +1 -1
- package/dist/index.native.cjs.js +4 -4
- package/dist/index.native.cjs.js.map +1 -1
- package/dist/index.native.d.ts +1 -0
- package/dist/index.native.esm.js +4 -4
- package/dist/index.native.esm.js.map +1 -1
- package/dist/migrations/migratePipe.d.ts +14 -0
- package/dist/migrations/migrations/1.1.0_normalize_style_attributes.d.ts +2 -0
- package/dist/migrations/semver.d.ts +8 -0
- package/dist/migrations/types.d.ts +8 -0
- package/dist/mockOS/components/SubscriptionModal.d.ts +7 -0
- package/dist/mockOS/context/MockOSContextBase.d.ts +1 -0
- package/dist/mockOS/hooks/useMockIap.d.ts +3 -0
- package/dist/mockOS/index.d.ts +4 -0
- package/dist/mockOS/managers/mockOSIapManager.d.ts +6 -0
- package/dist/mockOS/managers/subscriptionManager.d.ts +10 -0
- package/dist/pages/ProjectDebug.d.ts +14 -0
- package/dist/pages/ProjectMigrationPage.d.ts +23 -0
- package/dist/pages/ProjectValidationPage.d.ts +15 -0
- package/dist/styles.css +1 -1
- package/dist/types/Device.d.ts +5 -0
- package/dist/utils/__special_exceptions.d.ts +7 -0
- package/dist/utils/getImage.d.ts +23 -0
- package/dist/utils/pasteNode.d.ts +15 -0
- package/dist/utils/patterns.d.ts +1 -2
- package/package.json +6 -2
- package/scripts/migrate-patterns-to-v2.mjs +131 -0
- package/scripts/migrate-samples-to-current.ts +79 -0
- package/scripts/prebuild/utils/createGeneratedProps.js +4 -5
- package/scripts/prebuild/utils/validateAllComponentsOrThrow.js +32 -21
- package/scripts/prebuild/utils/validatePatternJson.js +12 -10
- package/src/.DS_Store +0 -0
- package/src/AttributesEditor.tsx +41 -11
- package/src/RenderPage.tsx +55 -0
- package/src/assets/.DS_Store +0 -0
- package/src/assets/devices.json +91 -0
- package/src/assets/samples/carousel-sample.json +141 -29
- package/src/assets/samples/getSamples.ts +9 -0
- package/src/assets/samples/paywall-1.json +119 -71
- package/src/assets/samples/simple-1.json +28 -16
- package/src/assets/samples/simple-2.json +157 -82
- package/src/assets/samples/unmigrated-builder1.json +42 -0
- package/src/assets/samples/unvalidated-builder1.json +49 -0
- package/src/assets/samples/unvalidated-crash1.json +19 -0
- package/src/assets/samples/unvalidated-crashcomponent1.json +16 -0
- package/src/assets/samples/vpn-onboard-1.json +91 -51
- package/src/assets/samples/vpn-onboard-2.json +318 -278
- package/src/assets/samples/vpn-onboard-3.json +286 -252
- package/src/assets/samples/vpn-onboard-4.json +286 -252
- package/src/assets/samples/vpn-onboard-5.json +434 -374
- package/src/assets/samples/vpn-onboard-6.json +290 -250
- package/src/attributes-editor/Field.tsx +1 -1
- package/src/attributes-editor/LayoutPreviewPicker.tsx +5 -2
- package/src/build-components/BIcon/BIconProps.generated.ts +3 -0
- package/src/build-components/BIcon/pattern.json +12 -9
- package/src/build-components/BackgroundImage/BackgroundImage.tsx +3 -1
- package/src/build-components/BackgroundImage/BackgroundImageProps.generated.ts +1 -0
- package/src/build-components/BackgroundImage/pattern.json +25 -16
- package/src/build-components/Button/Button.tsx +26 -3
- package/src/build-components/Button/ButtonProps.generated.ts +1 -0
- package/src/build-components/Button/pattern.json +10 -6
- package/src/build-components/Carousel/CarouselProps.generated.ts +5 -0
- package/src/build-components/Carousel/pattern.json +19 -8
- package/src/build-components/CarouselButtons/CarouselButtonsProps.generated.ts +1 -0
- package/src/build-components/CarouselButtons/pattern.json +11 -5
- package/src/build-components/CarouselDots/CarouselDotsProps.generated.ts +1 -0
- package/src/build-components/CarouselDots/pattern.json +5 -4
- package/src/build-components/CarouselItem/CarouselItemProps.generated.ts +1 -0
- package/src/build-components/CarouselItem/pattern.json +5 -4
- package/src/build-components/CarouselProvider/CarouselProvider.tsx +44 -2
- package/src/build-components/CarouselProvider/CarouselProviderProps.generated.ts +1 -0
- package/src/build-components/Image/Image.tsx +2 -1
- package/src/build-components/Image/ImageProps.generated.ts +1 -0
- package/src/build-components/Image/pattern.json +11 -5
- package/src/build-components/Main/MainProps.generated.ts +1 -1
- package/src/build-components/Main/pattern.json +12 -9
- package/src/build-components/Onboard/OnboardProps.generated.ts +1 -0
- package/src/build-components/Onboard/pattern.json +14 -9
- package/src/build-components/OnboardButton/OnboardButtonProps.generated.ts +1 -0
- package/src/build-components/OnboardButton/pattern.json +5 -4
- package/src/build-components/OnboardButtons/OnboardButtonsProps.generated.ts +1 -0
- package/src/build-components/OnboardButtons/pattern.json +5 -4
- package/src/build-components/OnboardDot/OnboardDotProps.generated.ts +1 -0
- package/src/build-components/OnboardDot/pattern.json +5 -4
- package/src/build-components/OnboardFooter/OnboardFooterProps.generated.ts +3 -0
- package/src/build-components/OnboardFooter/pattern.json +8 -5
- package/src/build-components/OnboardImage/OnboardImageProps.generated.ts +1 -0
- package/src/build-components/OnboardImage/pattern.json +7 -4
- package/src/build-components/OnboardItem/OnboardItemProps.generated.ts +1 -0
- package/src/build-components/OnboardItem/pattern.json +18 -9
- package/src/build-components/OnboardProvider/OnboardProviderProps.generated.ts +3 -0
- package/src/build-components/OnboardProvider/pattern.json +21 -6
- package/src/build-components/OnboardSubtitle/OnboardSubtitleProps.generated.ts +3 -0
- package/src/build-components/OnboardSubtitle/pattern.json +10 -6
- package/src/build-components/OnboardTitle/OnboardTitleProps.generated.ts +3 -0
- package/src/build-components/OnboardTitle/pattern.json +11 -7
- package/src/build-components/PaywallBackground/PaywallBackgroundProps.generated.ts +1 -1
- package/src/build-components/PaywallBackground/pattern.json +5 -4
- package/src/build-components/PaywallCloseButton/PaywallCloseButton.tsx +6 -1
- package/src/build-components/PaywallCloseButton/PaywallCloseButtonProps.generated.ts +3 -1
- package/src/build-components/PaywallCloseButton/pattern.json +15 -12
- package/src/build-components/PaywallOptions/PaywallOptionButton.tsx +0 -1
- package/src/build-components/PaywallOptions/PaywallOptions.tsx +3 -2
- package/src/build-components/PaywallOptions/PaywallOptionsProps.generated.ts +1 -1
- package/src/build-components/PaywallOptions/pattern.json +14 -11
- package/src/build-components/PaywallProvider/PaywallContext.ts +25 -0
- package/src/build-components/PaywallProvider/PaywallProvider.tsx +102 -5
- package/src/build-components/PaywallProvider/PaywallProviderProps.generated.ts +1 -1
- package/src/build-components/PaywallProvider/pattern.json +11 -8
- package/src/build-components/PaywallSubscribeButton/PaywallSubscribeButton.tsx +7 -0
- package/src/build-components/PaywallSubscribeButton/PaywallSubscribeButtonProps.generated.ts +1 -0
- package/src/build-components/PaywallSubscribeButton/pattern.json +16 -13
- package/src/build-components/RadioButton/RadioButtonProps.generated.ts +1 -1
- package/src/build-components/RadioButton/pattern.json +5 -4
- package/src/build-components/Text/Text.tsx +107 -4
- package/src/build-components/Text/TextProps.generated.ts +3 -0
- package/src/build-components/Text/pattern.json +19 -4
- package/src/build-components/View/ViewProps.generated.ts +1 -0
- package/src/build-components/View/pattern.json +28 -13
- package/src/build-components/other.tsx +15 -0
- package/src/build-components/patterns.generated.ts +340 -235
- package/src/build-components/useNode.ts +22 -3
- package/src/components/Builder.tsx +20 -6
- package/src/components/BuilderButton.tsx +75 -38
- package/src/components/BuilderProvider.tsx +22 -2
- package/src/components/DeviceButton.tsx +12 -5
- package/src/components/EditorHeader.tsx +296 -38
- package/src/components/ParamsProvider.tsx +7 -0
- package/src/components/RenderErrorBoundary.tsx +200 -0
- package/src/hooks/useParams.ts +5 -1
- package/src/hooks/useSyncHtmlThemeClass.ts +19 -0
- package/src/index.native.ts +7 -0
- package/src/index.ts +8 -0
- package/src/migrations/migratePipe.ts +59 -0
- package/src/migrations/migrations/1.1.0_normalize_style_attributes.ts +80 -0
- package/src/migrations/semver.ts +24 -0
- package/src/migrations/types.ts +9 -0
- package/src/mockOS/components/PermissionModal.tsx +3 -2
- package/src/mockOS/components/SubscriptionModal.tsx +400 -0
- package/src/mockOS/context/MockOSContext.tsx +61 -10
- package/src/mockOS/context/MockOSContextBase.ts +1 -0
- package/src/mockOS/hooks/useMockIap.ts +11 -0
- package/src/mockOS/index.ts +7 -0
- package/src/mockOS/managers/mockOSIapManager.ts +10 -0
- package/src/mockOS/managers/subscriptionManager.ts +36 -0
- package/src/modals/IconPickerModal.tsx +1 -1
- package/src/pages/ProjectDebug.tsx +331 -0
- package/src/pages/ProjectMigrationPage.tsx +92 -0
- package/src/pages/ProjectPage.tsx +313 -161
- package/src/pages/ProjectValidationPage.tsx +54 -0
- package/src/styles/base/_global.scss +58 -11
- package/src/styles/components/_attributes-editor.scss +1 -1
- package/src/styles/components/_bottom-bar.scss +7 -4
- package/src/styles/components/_editor-shell.scss +126 -4
- package/src/styles/components/_mockos-router.scss +3 -2
- package/src/styles/components/_ui-components.scss +10 -5
- package/src/styles/foundation/_colors.scss +78 -11
- package/src/styles/foundation/_mixins.scss +4 -1
- package/src/styles/foundation/_sizes.scss +4 -2
- package/src/styles/index.scss +1 -0
- package/src/styles/layout/_builder.scss +61 -0
- package/src/styles/layout/_project-validation.scss +214 -0
- package/src/styles/modals/_add-component.scss +4 -2
- package/src/styles/modals/_color-modal.scss +4 -2
- package/src/styles/modals/_modal-shell.scss +3 -1
- package/src/types/Device.ts +5 -0
- package/src/utils/__special_exceptions.ts +88 -0
- package/src/utils/analyseNode.ts +8 -2
- package/src/utils/analyseNodeByPatterns.ts +43 -9
- package/src/utils/extractTextStyle.ts +19 -6
- package/src/utils/extractViewStyle.ts +68 -59
- package/src/utils/getImage.ts +76 -0
- package/src/utils/novaToJson.ts +2 -1
- package/src/utils/pasteNode.ts +172 -0
- package/src/utils/patterns.ts +4 -3
- package/dist/android.svg +0 -43
- package/dist/apple.svg +0 -16
- package/dist/background.jpg +0 -0
|
@@ -7,9 +7,28 @@ export default function useNode<
|
|
|
7
7
|
const type = node?.type;
|
|
8
8
|
const defaults = getDefaultsForType(type) as Partial<T> | undefined;
|
|
9
9
|
if (!defaults) return node;
|
|
10
|
-
const
|
|
11
|
-
|
|
12
|
-
|
|
10
|
+
const nodeAttributes = ((node.attributes as T) ?? ({} as T)) as T & {
|
|
11
|
+
style?: Record<string, unknown>;
|
|
12
|
+
};
|
|
13
|
+
const defaultAttributes = defaults as T as T & {
|
|
14
|
+
style?: Record<string, unknown>;
|
|
13
15
|
};
|
|
16
|
+
const mergedAttributes: T = {
|
|
17
|
+
...(defaultAttributes as T),
|
|
18
|
+
...(nodeAttributes as T),
|
|
19
|
+
// Deep merge `style` so default style values aren't lost when the node provides partial style overrides.
|
|
20
|
+
style: {
|
|
21
|
+
...(defaultAttributes?.style ?? {}),
|
|
22
|
+
...(nodeAttributes?.style ?? {}),
|
|
23
|
+
},
|
|
24
|
+
} as T;
|
|
25
|
+
if (
|
|
26
|
+
mergedAttributes &&
|
|
27
|
+
typeof (mergedAttributes as any).style === 'object' &&
|
|
28
|
+
(mergedAttributes as any).style != null &&
|
|
29
|
+
Object.keys((mergedAttributes as any).style).length === 0
|
|
30
|
+
) {
|
|
31
|
+
delete (mergedAttributes as any).style;
|
|
32
|
+
}
|
|
14
33
|
return { ...node, attributes: mergedAttributes };
|
|
15
34
|
}
|
|
@@ -386,23 +386,37 @@ export function Builder({
|
|
|
386
386
|
function createDefaultNode(type: string): NodeData<NodeDefaultAttribute> {
|
|
387
387
|
const pattern = getPatternByType(type)?.pattern;
|
|
388
388
|
const defaults = getDefaultsForType(type) ?? {};
|
|
389
|
-
let children: Node = '';
|
|
390
389
|
const childrenSchema = pattern?.children as unknown;
|
|
390
|
+
|
|
391
|
+
// Special-case: CarouselProvider MUST contain a Carousel container inside the viewport
|
|
392
|
+
// otherwise embla-carousel will crash (it expects viewport.firstChild.children).
|
|
393
|
+
if (type === 'CarouselProvider') {
|
|
394
|
+
return {
|
|
395
|
+
type,
|
|
396
|
+
children: createDefaultNode('Carousel'),
|
|
397
|
+
attributes: { ...defaults },
|
|
398
|
+
} as NodeData<NodeDefaultAttribute>;
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
let children: Node = null;
|
|
391
402
|
if (childrenSchema === 'never') {
|
|
392
|
-
children =
|
|
403
|
+
children = null;
|
|
393
404
|
} else if (childrenSchema === 'string') {
|
|
394
405
|
children = '';
|
|
395
406
|
} else if (
|
|
396
407
|
childrenSchema === 'node' ||
|
|
397
408
|
(Array.isArray(childrenSchema) && childrenSchema.includes('node'))
|
|
398
409
|
) {
|
|
399
|
-
children
|
|
410
|
+
// Default to "no children yet". Using [] here is truthy and can mount
|
|
411
|
+
// child-dependent widgets (e.g. Embla) with an empty DOM, causing crashes.
|
|
412
|
+
children = null;
|
|
400
413
|
} else if (typeof childrenSchema === 'string') {
|
|
401
|
-
// Specific child type like '
|
|
402
|
-
children = [];
|
|
414
|
+
// Specific child type like 'CarouselItem' – seed with one child to match the pattern.
|
|
415
|
+
children = [createDefaultNode(childrenSchema)];
|
|
403
416
|
} else {
|
|
404
|
-
children =
|
|
417
|
+
children = null;
|
|
405
418
|
}
|
|
419
|
+
|
|
406
420
|
return {
|
|
407
421
|
type,
|
|
408
422
|
children,
|
|
@@ -1,7 +1,8 @@
|
|
|
1
|
-
import { useRef } from 'react';
|
|
1
|
+
import { useEffect, useMemo, useRef, useState } from 'react';
|
|
2
2
|
import { isNodeNullOrUndefined, isNodeString } from '../utils/analyseNode';
|
|
3
3
|
import type { Node, NodeData, NodeDefaultAttribute } from '../types/Node';
|
|
4
4
|
import { getPatternByType } from '../utils/patterns';
|
|
5
|
+
import { Icon } from './Icon.generated';
|
|
5
6
|
|
|
6
7
|
export type BuilderButtonProps = {
|
|
7
8
|
node: Node;
|
|
@@ -26,8 +27,12 @@ export function BuilderButton({
|
|
|
26
27
|
}
|
|
27
28
|
const nodeData = node as NodeData<NodeDefaultAttribute>;
|
|
28
29
|
|
|
29
|
-
const
|
|
30
|
-
const
|
|
30
|
+
const [isMenuOpen, setIsMenuOpen] = useState(false);
|
|
31
|
+
const actionsRef = useRef<HTMLDivElement | null>(null);
|
|
32
|
+
const menuId = useMemo(
|
|
33
|
+
() => `builder-node-actions-${Math.random().toString(36).slice(2, 9)}`,
|
|
34
|
+
[],
|
|
35
|
+
);
|
|
31
36
|
|
|
32
37
|
const handleDelete = () => {
|
|
33
38
|
if (onDelete) {
|
|
@@ -35,34 +40,31 @@ export function BuilderButton({
|
|
|
35
40
|
}
|
|
36
41
|
};
|
|
37
42
|
|
|
38
|
-
|
|
39
|
-
if (longPressTimeoutRef.current !== null) {
|
|
40
|
-
window.clearTimeout(longPressTimeoutRef.current);
|
|
41
|
-
longPressTimeoutRef.current = null;
|
|
42
|
-
}
|
|
43
|
-
};
|
|
43
|
+
// Copy/Paste intentionally removed for now.
|
|
44
44
|
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
longPressTimeoutRef.current = window.setTimeout(() => {
|
|
48
|
-
longPressTriggeredRef.current = true;
|
|
49
|
-
const shouldDelete = window.confirm('Do you want to delete');
|
|
50
|
-
if (shouldDelete) {
|
|
51
|
-
handleDelete();
|
|
52
|
-
}
|
|
53
|
-
}, 600);
|
|
54
|
-
};
|
|
45
|
+
useEffect(() => {
|
|
46
|
+
if (!isMenuOpen) return;
|
|
55
47
|
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
48
|
+
const handlePointerDown = (e: MouseEvent | TouchEvent) => {
|
|
49
|
+
const el = actionsRef.current;
|
|
50
|
+
if (!el) return;
|
|
51
|
+
if (e.target instanceof Element && el.contains(e.target)) return;
|
|
52
|
+
setIsMenuOpen(false);
|
|
53
|
+
};
|
|
62
54
|
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
55
|
+
const handleKeyDown = (e: KeyboardEvent) => {
|
|
56
|
+
if (e.key === 'Escape') setIsMenuOpen(false);
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
document.addEventListener('mousedown', handlePointerDown);
|
|
60
|
+
document.addEventListener('touchstart', handlePointerDown);
|
|
61
|
+
document.addEventListener('keydown', handleKeyDown);
|
|
62
|
+
return () => {
|
|
63
|
+
document.removeEventListener('mousedown', handlePointerDown);
|
|
64
|
+
document.removeEventListener('touchstart', handlePointerDown);
|
|
65
|
+
document.removeEventListener('keydown', handleKeyDown);
|
|
66
|
+
};
|
|
67
|
+
}, [isMenuOpen]);
|
|
66
68
|
|
|
67
69
|
let extra = '';
|
|
68
70
|
if (nodeData.attributes?.condition) {
|
|
@@ -103,17 +105,52 @@ export function BuilderButton({
|
|
|
103
105
|
</button>
|
|
104
106
|
</div>
|
|
105
107
|
)}
|
|
106
|
-
<
|
|
107
|
-
className="builder__button-link"
|
|
108
|
-
onMouseDown={handlePressStart}
|
|
109
|
-
onMouseUp={handlePressEnd}
|
|
110
|
-
onMouseLeave={handlePressCancel}
|
|
111
|
-
onTouchStart={handlePressStart}
|
|
112
|
-
onTouchEnd={handlePressEnd}
|
|
113
|
-
onTouchCancel={handlePressCancel}
|
|
114
|
-
>
|
|
108
|
+
<button type="button" className="builder__button-link" onClick={onClick}>
|
|
115
109
|
{baseLabel}
|
|
116
|
-
</
|
|
110
|
+
</button>
|
|
111
|
+
<div className="builder__button-actions" ref={actionsRef}>
|
|
112
|
+
<button
|
|
113
|
+
type="button"
|
|
114
|
+
className="builder__button-actions-trigger"
|
|
115
|
+
aria-label="Open node actions"
|
|
116
|
+
aria-haspopup="menu"
|
|
117
|
+
aria-expanded={isMenuOpen}
|
|
118
|
+
aria-controls={menuId}
|
|
119
|
+
onClick={(event) => {
|
|
120
|
+
event.stopPropagation();
|
|
121
|
+
setIsMenuOpen((v) => !v);
|
|
122
|
+
}}
|
|
123
|
+
>
|
|
124
|
+
<Icon iconType="chevron-right" size={16} />
|
|
125
|
+
</button>
|
|
126
|
+
{isMenuOpen && (
|
|
127
|
+
<ul
|
|
128
|
+
id={menuId}
|
|
129
|
+
className="builder__button-actions-menu"
|
|
130
|
+
role="menu"
|
|
131
|
+
aria-label="Node actions"
|
|
132
|
+
>
|
|
133
|
+
<li role="none">
|
|
134
|
+
<button
|
|
135
|
+
type="button"
|
|
136
|
+
className="builder__button-actions-item builder__button-actions-item--danger"
|
|
137
|
+
role="menuitem"
|
|
138
|
+
disabled={!onDelete}
|
|
139
|
+
onClick={(event) => {
|
|
140
|
+
event.stopPropagation();
|
|
141
|
+
if (!onDelete) return;
|
|
142
|
+
const shouldDelete = window.confirm('Do you want to delete?');
|
|
143
|
+
if (!shouldDelete) return;
|
|
144
|
+
handleDelete();
|
|
145
|
+
setIsMenuOpen(false);
|
|
146
|
+
}}
|
|
147
|
+
>
|
|
148
|
+
Delete
|
|
149
|
+
</button>
|
|
150
|
+
</li>
|
|
151
|
+
</ul>
|
|
152
|
+
)}
|
|
153
|
+
</div>
|
|
117
154
|
{conditionLabel && (
|
|
118
155
|
<span className="builder__button-condition">{conditionLabel}</span>
|
|
119
156
|
)}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import React, { createContext, useContext, useMemo } from 'react';
|
|
2
2
|
import type { Product } from '../paywall/types/paywall-types';
|
|
3
3
|
import type { PaywallBenefits } from '../paywall/types/benefits';
|
|
4
|
+
import { RenderErrorBoundary } from './RenderErrorBoundary';
|
|
4
5
|
|
|
5
6
|
// NOTE: We keep this context intentionally tiny.
|
|
6
7
|
// IMPORTANT: This provider may be mounted once but consumed by multiple `build-components`
|
|
@@ -12,6 +13,8 @@ export type Products = Product;
|
|
|
12
13
|
export type BuilderProviderParams = {
|
|
13
14
|
products: Products[];
|
|
14
15
|
benefits: PaywallBenefits;
|
|
16
|
+
onPaywallClose?: () => void;
|
|
17
|
+
onPaywallSubscribe?: (product?: Product) => void | boolean | Promise<boolean>;
|
|
15
18
|
};
|
|
16
19
|
|
|
17
20
|
type BuilderProviderProps = {
|
|
@@ -31,12 +34,29 @@ export function BuilderProvider({ params, children }: BuilderProviderProps) {
|
|
|
31
34
|
params?.benefits && typeof params.benefits === 'object'
|
|
32
35
|
? (params.benefits as PaywallBenefits)
|
|
33
36
|
: {},
|
|
37
|
+
onPaywallClose:
|
|
38
|
+
typeof params?.onPaywallClose === 'function'
|
|
39
|
+
? params.onPaywallClose
|
|
40
|
+
: undefined,
|
|
41
|
+
onPaywallSubscribe:
|
|
42
|
+
typeof params?.onPaywallSubscribe === 'function'
|
|
43
|
+
? params.onPaywallSubscribe
|
|
44
|
+
: undefined,
|
|
34
45
|
}),
|
|
35
|
-
[
|
|
46
|
+
[
|
|
47
|
+
params?.benefits,
|
|
48
|
+
params?.products,
|
|
49
|
+
params?.onPaywallClose,
|
|
50
|
+
params?.onPaywallSubscribe,
|
|
51
|
+
],
|
|
36
52
|
);
|
|
37
53
|
|
|
38
54
|
return (
|
|
39
|
-
<BuilderContext.Provider value={value}>
|
|
55
|
+
<BuilderContext.Provider value={value}>
|
|
56
|
+
<RenderErrorBoundary subtitle="caught by BuilderProvider">
|
|
57
|
+
{children}
|
|
58
|
+
</RenderErrorBoundary>
|
|
59
|
+
</BuilderContext.Provider>
|
|
40
60
|
);
|
|
41
61
|
}
|
|
42
62
|
|
|
@@ -1,11 +1,10 @@
|
|
|
1
1
|
import React from 'react';
|
|
2
2
|
import { Device } from '../types/Device';
|
|
3
|
-
import
|
|
4
|
-
import iosIcon from '../assets/images/apple.svg';
|
|
3
|
+
import { getImage, TribeAssetName } from '../utils/getImage';
|
|
5
4
|
|
|
6
5
|
const platformIcons: Record<string, string> = {
|
|
7
|
-
android:
|
|
8
|
-
ios:
|
|
6
|
+
android: getImage(TribeAssetName.Android),
|
|
7
|
+
ios: getImage(TribeAssetName.Apple),
|
|
9
8
|
};
|
|
10
9
|
|
|
11
10
|
type DeviceButtonProps = {
|
|
@@ -20,6 +19,14 @@ export function DeviceButton({
|
|
|
20
19
|
onSelect,
|
|
21
20
|
}: DeviceButtonProps) {
|
|
22
21
|
const platformIcon = platformIcons[device.platform];
|
|
22
|
+
const aspect =
|
|
23
|
+
device.aspect ??
|
|
24
|
+
(() => {
|
|
25
|
+
const r = device.height / device.width;
|
|
26
|
+
if (r >= 2.05) return 'tall' as const;
|
|
27
|
+
if (r <= 1.75) return 'wide' as const;
|
|
28
|
+
return 'regular' as const;
|
|
29
|
+
})();
|
|
23
30
|
|
|
24
31
|
return (
|
|
25
32
|
<button
|
|
@@ -30,7 +37,7 @@ export function DeviceButton({
|
|
|
30
37
|
onClick={() => onSelect(device)}
|
|
31
38
|
>
|
|
32
39
|
{device.name} <br />
|
|
33
|
-
{device.width}
|
|
40
|
+
{device.width}×{device.height} ({aspect})
|
|
34
41
|
{platformIcon && <img src={platformIcon} alt="" aria-hidden="true" />}
|
|
35
42
|
</button>
|
|
36
43
|
);
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { useState } from 'react';
|
|
1
|
+
import { useEffect, useMemo, useRef, useState } from 'react';
|
|
2
2
|
import type { Device } from '../types/Device';
|
|
3
3
|
import type { Node } from '../types/Node';
|
|
4
4
|
import { copyNode } from '../utils/copyNode';
|
|
@@ -7,6 +7,9 @@ import { useRenderStore } from '../store';
|
|
|
7
7
|
import { useLogRender } from '../utils/useLogRender';
|
|
8
8
|
import { DeviceButton } from './DeviceButton';
|
|
9
9
|
import { DeviceSelectorModal } from '../modals/DeviceSelectorModal';
|
|
10
|
+
import { toast } from 'react-toastify';
|
|
11
|
+
import { getSamples } from '../assets/samples/getSamples';
|
|
12
|
+
import type { Project } from '../types/Project';
|
|
10
13
|
|
|
11
14
|
const devices = getDevices();
|
|
12
15
|
interface EditorHeaderProps {
|
|
@@ -26,17 +29,67 @@ export function EditorHeader({
|
|
|
26
29
|
}: EditorHeaderProps) {
|
|
27
30
|
useLogRender('EditorHeader');
|
|
28
31
|
const [isDevicesModalOpen, setIsDevicesModalOpen] = useState(false);
|
|
32
|
+
const [isActionsOpen, setIsActionsOpen] = useState(false);
|
|
33
|
+
const actionsRef = useRef<HTMLDivElement | null>(null);
|
|
29
34
|
const copiedNode = useRenderStore((s) => s.copiedNode);
|
|
30
35
|
const {
|
|
31
36
|
device: selectedDevice,
|
|
32
37
|
setDevice,
|
|
33
38
|
setCurrent,
|
|
39
|
+
setAppConfig,
|
|
40
|
+
setProjectColors,
|
|
34
41
|
} = useRenderStore((s) => ({
|
|
35
42
|
device: s.device,
|
|
36
43
|
setDevice: s.setDevice,
|
|
37
44
|
setCurrent: s.setCurrent,
|
|
45
|
+
setAppConfig: s.setAppConfig,
|
|
46
|
+
setProjectColors: s.setProjectColors,
|
|
38
47
|
}));
|
|
39
48
|
|
|
49
|
+
const sortedSamples = useMemo(() => {
|
|
50
|
+
const weight = (t?: Project['type']) => {
|
|
51
|
+
if (t === 'paywall') return 0;
|
|
52
|
+
if (t === 'onboard') return 1;
|
|
53
|
+
return 2;
|
|
54
|
+
};
|
|
55
|
+
return getSamples()
|
|
56
|
+
.slice()
|
|
57
|
+
.sort((a, b) => {
|
|
58
|
+
const w = weight(a.type) - weight(b.type);
|
|
59
|
+
if (w !== 0) return w;
|
|
60
|
+
return a.name.localeCompare(b.name);
|
|
61
|
+
});
|
|
62
|
+
}, []);
|
|
63
|
+
|
|
64
|
+
const actionsMenuId = useMemo(
|
|
65
|
+
() => `editor-actions-menu-${Math.random().toString(36).slice(2, 9)}`,
|
|
66
|
+
[],
|
|
67
|
+
);
|
|
68
|
+
|
|
69
|
+
useEffect(() => {
|
|
70
|
+
if (!isActionsOpen) return;
|
|
71
|
+
|
|
72
|
+
const handlePointerDown = (e: MouseEvent | TouchEvent) => {
|
|
73
|
+
const el = actionsRef.current;
|
|
74
|
+
if (!el) return;
|
|
75
|
+
if (e.target instanceof Element && el.contains(e.target)) return;
|
|
76
|
+
setIsActionsOpen(false);
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
const handleKeyDown = (e: KeyboardEvent) => {
|
|
80
|
+
if (e.key === 'Escape') setIsActionsOpen(false);
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
document.addEventListener('mousedown', handlePointerDown);
|
|
84
|
+
document.addEventListener('touchstart', handlePointerDown);
|
|
85
|
+
document.addEventListener('keydown', handleKeyDown);
|
|
86
|
+
return () => {
|
|
87
|
+
document.removeEventListener('mousedown', handlePointerDown);
|
|
88
|
+
document.removeEventListener('touchstart', handlePointerDown);
|
|
89
|
+
document.removeEventListener('keydown', handleKeyDown);
|
|
90
|
+
};
|
|
91
|
+
}, [isActionsOpen]);
|
|
92
|
+
|
|
40
93
|
function replaceNode(root: Node, target: Node, next: Node): Node {
|
|
41
94
|
if (root === target) return next;
|
|
42
95
|
if (root === null || root === undefined) return root;
|
|
@@ -50,16 +103,13 @@ export function EditorHeader({
|
|
|
50
103
|
});
|
|
51
104
|
return changed ? arr : root;
|
|
52
105
|
}
|
|
53
|
-
const data = root as
|
|
106
|
+
const data = root as Record<string, unknown>;
|
|
54
107
|
if ('children' in data) {
|
|
55
|
-
const prev = data.children;
|
|
108
|
+
const prev = data.children as Node;
|
|
56
109
|
const replaced = Array.isArray(prev)
|
|
57
|
-
? prev.map((c
|
|
58
|
-
: replaceNode(prev
|
|
59
|
-
if (replaced !== prev) {
|
|
60
|
-
data.children = replaced;
|
|
61
|
-
return { ...data, children: replaced } as Node;
|
|
62
|
-
}
|
|
110
|
+
? prev.map((c) => replaceNode(c, target, next))
|
|
111
|
+
: replaceNode(prev, target, next);
|
|
112
|
+
if (replaced !== prev) return { ...data, children: replaced } as Node;
|
|
63
113
|
}
|
|
64
114
|
return root;
|
|
65
115
|
}
|
|
@@ -81,6 +131,104 @@ export function EditorHeader({
|
|
|
81
131
|
// in sync with what’s rendered/edited.
|
|
82
132
|
setCurrent(cloned);
|
|
83
133
|
};
|
|
134
|
+
|
|
135
|
+
const cloneNode = (node: Node): Node =>
|
|
136
|
+
JSON.parse(JSON.stringify(node)) as Node;
|
|
137
|
+
|
|
138
|
+
const handleReplaceFromSample = (sample: Project) => {
|
|
139
|
+
if (!setEditorData) return;
|
|
140
|
+
const next = cloneNode(sample.data);
|
|
141
|
+
setEditorData(next);
|
|
142
|
+
setCurrent(next);
|
|
143
|
+
if (sample.appConfig) setAppConfig(sample.appConfig);
|
|
144
|
+
setProjectColors(sample.projectColors);
|
|
145
|
+
toast.success(`Replaced with "${sample.name}"`);
|
|
146
|
+
};
|
|
147
|
+
|
|
148
|
+
const handlePasteFromSample = (sample: Project) => {
|
|
149
|
+
if (!current || !editorData || !setEditorData) return;
|
|
150
|
+
const incoming = cloneNode(sample.data);
|
|
151
|
+
|
|
152
|
+
const isRecord = (v: unknown): v is Record<string, unknown> =>
|
|
153
|
+
typeof v === 'object' && v !== null && !Array.isArray(v);
|
|
154
|
+
|
|
155
|
+
const isNodeData = (v: unknown): v is Record<string, unknown> =>
|
|
156
|
+
isRecord(v) && 'type' in v && 'children' in v;
|
|
157
|
+
|
|
158
|
+
const getPasteNodes = (node: Node): Node[] => {
|
|
159
|
+
if (isNodeData(node)) {
|
|
160
|
+
const t = node.type;
|
|
161
|
+
const isMainLike = t === 'main' || node.isMain === true;
|
|
162
|
+
if (isMainLike) {
|
|
163
|
+
const ch = node.children as Node;
|
|
164
|
+
if (!ch) return [];
|
|
165
|
+
return Array.isArray(ch) ? ch : [ch];
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
return [node];
|
|
169
|
+
};
|
|
170
|
+
|
|
171
|
+
// "Paste from sample" should ADD the sample into the selected node (not replace it).
|
|
172
|
+
// We do this by appending the sample's root node into current.children.
|
|
173
|
+
if (!isNodeData(current)) {
|
|
174
|
+
toast.error('Select a component node to paste into');
|
|
175
|
+
return;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
const prevChildren = current.children as Node;
|
|
179
|
+
const pasteNodes = getPasteNodes(incoming);
|
|
180
|
+
if (pasteNodes.length === 0) {
|
|
181
|
+
toast.error('Sample has no children to paste');
|
|
182
|
+
return;
|
|
183
|
+
}
|
|
184
|
+
let nextChildren: Node;
|
|
185
|
+
if (!prevChildren) {
|
|
186
|
+
nextChildren = pasteNodes.length === 1 ? pasteNodes[0] : pasteNodes;
|
|
187
|
+
} else if (Array.isArray(prevChildren)) {
|
|
188
|
+
nextChildren = [...prevChildren, ...pasteNodes];
|
|
189
|
+
} else {
|
|
190
|
+
nextChildren = [prevChildren, ...pasteNodes];
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
const nextNode: Node = { ...current, children: nextChildren } as Node;
|
|
194
|
+
|
|
195
|
+
const updated = replaceNode(editorData, current, nextNode);
|
|
196
|
+
setEditorData(updated);
|
|
197
|
+
setCurrent(nextNode);
|
|
198
|
+
toast.success(`Pasted from "${sample.name}"`);
|
|
199
|
+
};
|
|
200
|
+
|
|
201
|
+
const closeActions = () => setIsActionsOpen(false);
|
|
202
|
+
const handleSaveProject = () => {
|
|
203
|
+
onSaveProject?.();
|
|
204
|
+
closeActions();
|
|
205
|
+
};
|
|
206
|
+
const handleRestoreProject = () => {
|
|
207
|
+
try {
|
|
208
|
+
onRestoreProject?.();
|
|
209
|
+
toast.info('Restored');
|
|
210
|
+
} catch {
|
|
211
|
+
toast.error('Restore failed');
|
|
212
|
+
} finally {
|
|
213
|
+
closeActions();
|
|
214
|
+
}
|
|
215
|
+
};
|
|
216
|
+
const handleCopyAndClose = () => {
|
|
217
|
+
if (!canCopy) return;
|
|
218
|
+
handleCopy();
|
|
219
|
+
toast.info('Copied');
|
|
220
|
+
closeActions();
|
|
221
|
+
};
|
|
222
|
+
const handlePasteAndClose = () => {
|
|
223
|
+
if (!canPaste) return;
|
|
224
|
+
handlePaste();
|
|
225
|
+
toast.success('Pasted');
|
|
226
|
+
closeActions();
|
|
227
|
+
};
|
|
228
|
+
|
|
229
|
+
const canCopy = !!current;
|
|
230
|
+
const canPaste = !!current && !!editorData && !!setEditorData && !!copiedNode;
|
|
231
|
+
|
|
84
232
|
return (
|
|
85
233
|
<div
|
|
86
234
|
className="editor-header"
|
|
@@ -105,38 +253,148 @@ export function EditorHeader({
|
|
|
105
253
|
</button>
|
|
106
254
|
</div>
|
|
107
255
|
<div className="editor-header__actions">
|
|
108
|
-
<
|
|
109
|
-
className="editor-button editor-save-button"
|
|
110
|
-
aria-label="Save project data"
|
|
111
|
-
onClick={() => onSaveProject && onSaveProject()}
|
|
112
|
-
>
|
|
113
|
-
Save
|
|
114
|
-
</button>
|
|
115
|
-
{onRestoreProject && (
|
|
116
|
-
<button
|
|
117
|
-
className="editor-button editor-save-previewconfig-button"
|
|
118
|
-
aria-label="Restore project data"
|
|
119
|
-
onClick={() => onRestoreProject()}
|
|
120
|
-
>
|
|
121
|
-
Restore
|
|
122
|
-
</button>
|
|
123
|
-
)}
|
|
124
|
-
<button
|
|
125
|
-
className="editor-button"
|
|
126
|
-
aria-label="Copy node"
|
|
127
|
-
onClick={handleCopy}
|
|
128
|
-
>
|
|
129
|
-
Copy
|
|
130
|
-
</button>
|
|
131
|
-
{copiedNode && (
|
|
256
|
+
<div className="editor-actions-dropdown" ref={actionsRef}>
|
|
132
257
|
<button
|
|
133
|
-
className="editor-button"
|
|
134
|
-
aria-label="
|
|
135
|
-
|
|
258
|
+
className="editor-button editor-actions-dropdown__trigger"
|
|
259
|
+
aria-label="Open actions menu"
|
|
260
|
+
aria-haspopup="menu"
|
|
261
|
+
aria-expanded={isActionsOpen}
|
|
262
|
+
aria-controls={actionsMenuId}
|
|
263
|
+
onClick={() => setIsActionsOpen((v) => !v)}
|
|
136
264
|
>
|
|
137
|
-
|
|
265
|
+
Actions
|
|
266
|
+
<span className="editor-actions-dropdown__caret" aria-hidden="true">
|
|
267
|
+
▾
|
|
268
|
+
</span>
|
|
138
269
|
</button>
|
|
139
|
-
|
|
270
|
+
{isActionsOpen && (
|
|
271
|
+
<ul
|
|
272
|
+
id={actionsMenuId}
|
|
273
|
+
className="editor-actions-dropdown__menu"
|
|
274
|
+
role="menu"
|
|
275
|
+
aria-label="Editor actions"
|
|
276
|
+
>
|
|
277
|
+
<li role="none" className="editor-actions-dropdown__submenu-root">
|
|
278
|
+
<button
|
|
279
|
+
className="editor-actions-dropdown__item"
|
|
280
|
+
role="menuitem"
|
|
281
|
+
aria-haspopup="menu"
|
|
282
|
+
aria-label="Replace from samples"
|
|
283
|
+
onClick={(e) => e.preventDefault()}
|
|
284
|
+
>
|
|
285
|
+
Replace from samples
|
|
286
|
+
<span
|
|
287
|
+
className="editor-actions-dropdown__submenu-caret"
|
|
288
|
+
aria-hidden="true"
|
|
289
|
+
>
|
|
290
|
+
◂
|
|
291
|
+
</span>
|
|
292
|
+
</button>
|
|
293
|
+
<ul
|
|
294
|
+
className="editor-actions-dropdown__submenu"
|
|
295
|
+
role="menu"
|
|
296
|
+
aria-label="Replace from sample"
|
|
297
|
+
>
|
|
298
|
+
{sortedSamples.map((s) => (
|
|
299
|
+
<li role="none" key={`replace-${s.name}`}>
|
|
300
|
+
<button
|
|
301
|
+
className="editor-actions-dropdown__item editor-actions-dropdown__item--compact"
|
|
302
|
+
role="menuitem"
|
|
303
|
+
onClick={() => {
|
|
304
|
+
handleReplaceFromSample(s);
|
|
305
|
+
closeActions();
|
|
306
|
+
}}
|
|
307
|
+
>
|
|
308
|
+
{s.name}
|
|
309
|
+
</button>
|
|
310
|
+
</li>
|
|
311
|
+
))}
|
|
312
|
+
</ul>
|
|
313
|
+
</li>
|
|
314
|
+
<li role="none" className="editor-actions-dropdown__submenu-root">
|
|
315
|
+
<button
|
|
316
|
+
className="editor-actions-dropdown__item"
|
|
317
|
+
role="menuitem"
|
|
318
|
+
aria-haspopup="menu"
|
|
319
|
+
aria-label="Paste from samples"
|
|
320
|
+
onClick={(e) => e.preventDefault()}
|
|
321
|
+
disabled={!current}
|
|
322
|
+
title={!current ? 'Select a node first' : undefined}
|
|
323
|
+
>
|
|
324
|
+
Paste from samples
|
|
325
|
+
<span
|
|
326
|
+
className="editor-actions-dropdown__submenu-caret"
|
|
327
|
+
aria-hidden="true"
|
|
328
|
+
>
|
|
329
|
+
◂
|
|
330
|
+
</span>
|
|
331
|
+
</button>
|
|
332
|
+
<ul
|
|
333
|
+
className="editor-actions-dropdown__submenu"
|
|
334
|
+
role="menu"
|
|
335
|
+
aria-label="Paste from sample"
|
|
336
|
+
>
|
|
337
|
+
{sortedSamples.map((s) => (
|
|
338
|
+
<li role="none" key={`paste-${s.name}`}>
|
|
339
|
+
<button
|
|
340
|
+
className="editor-actions-dropdown__item editor-actions-dropdown__item--compact"
|
|
341
|
+
role="menuitem"
|
|
342
|
+
onClick={() => {
|
|
343
|
+
handlePasteFromSample(s);
|
|
344
|
+
closeActions();
|
|
345
|
+
}}
|
|
346
|
+
disabled={!current}
|
|
347
|
+
>
|
|
348
|
+
{s.name}
|
|
349
|
+
</button>
|
|
350
|
+
</li>
|
|
351
|
+
))}
|
|
352
|
+
</ul>
|
|
353
|
+
</li>
|
|
354
|
+
<li role="none">
|
|
355
|
+
<button
|
|
356
|
+
className="editor-actions-dropdown__item editor-save-button"
|
|
357
|
+
role="menuitem"
|
|
358
|
+
onClick={handleSaveProject}
|
|
359
|
+
disabled={!onSaveProject}
|
|
360
|
+
>
|
|
361
|
+
Save
|
|
362
|
+
</button>
|
|
363
|
+
</li>
|
|
364
|
+
{onRestoreProject && (
|
|
365
|
+
<li role="none">
|
|
366
|
+
<button
|
|
367
|
+
className="editor-actions-dropdown__item editor-save-previewconfig-button"
|
|
368
|
+
role="menuitem"
|
|
369
|
+
onClick={handleRestoreProject}
|
|
370
|
+
>
|
|
371
|
+
Restore
|
|
372
|
+
</button>
|
|
373
|
+
</li>
|
|
374
|
+
)}
|
|
375
|
+
<li role="none">
|
|
376
|
+
<button
|
|
377
|
+
className="editor-actions-dropdown__item"
|
|
378
|
+
role="menuitem"
|
|
379
|
+
onClick={handleCopyAndClose}
|
|
380
|
+
disabled={!canCopy}
|
|
381
|
+
>
|
|
382
|
+
Copy
|
|
383
|
+
</button>
|
|
384
|
+
</li>
|
|
385
|
+
<li role="none">
|
|
386
|
+
<button
|
|
387
|
+
className="editor-actions-dropdown__item"
|
|
388
|
+
role="menuitem"
|
|
389
|
+
onClick={handlePasteAndClose}
|
|
390
|
+
disabled={!canPaste}
|
|
391
|
+
>
|
|
392
|
+
Paste
|
|
393
|
+
</button>
|
|
394
|
+
</li>
|
|
395
|
+
</ul>
|
|
396
|
+
)}
|
|
397
|
+
</div>
|
|
140
398
|
</div>
|
|
141
399
|
{isDevicesModalOpen && (
|
|
142
400
|
<DeviceSelectorModal
|