@nascentdigital/funnel-editor 4.5.0 → 4.5.2

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.
@@ -1,13 +1,17 @@
1
- import { RequiredBy } from '@nascentdigital/funnel-core';
1
+ import { RequiredBy, Transform } from '@nascentdigital/funnel-core';
2
+ import { ReactNode } from 'react';
2
3
  import { Popup } from './Popup';
3
- export declare const QuickPicker: import("react").ForwardRefExoticComponent<QuickPicker.Props & import("react").RefAttributes<QuickPicker.Ref<string, QuickPicker.Item<string>>>>;
4
+ export declare const QuickPicker: import("react").ForwardRefExoticComponent<QuickPicker.Props<string, QuickPicker.Item<string>> & import("react").RefAttributes<QuickPicker.Ref<string, QuickPicker.Item<string>>>>;
4
5
  export declare namespace QuickPicker {
5
- type Item<TId extends string = string> = {
6
+ type Item<TId extends string> = {
6
7
  id: TId;
7
8
  };
8
- type Props = {
9
+ type ItemVariant = 'card';
10
+ type Props<TId extends string, TItem extends Item<TId>> = {
11
+ variant: ItemVariant;
9
12
  title: string;
10
13
  description?: string;
14
+ renderItem: Transform<TItem, ReactNode>;
11
15
  };
12
16
  interface Ref<TId extends string, TItem extends Item<TId>> {
13
17
  readonly isOpen: boolean;
@@ -21,7 +21,7 @@ const classnames_1 = __importDefault(require("classnames"));
21
21
  const react_1 = require("react");
22
22
  const Popup_1 = require("./Popup");
23
23
  // component
24
- exports.QuickPicker = (0, react_1.forwardRef)(function QuickPicker({ title, description }, ref) {
24
+ exports.QuickPicker = (0, react_1.forwardRef)(function QuickPicker({ variant, title, description, renderItem }, ref) {
25
25
  // bind state
26
26
  const [opened, setOpened] = (0, react_1.useState)(false);
27
27
  const [deferral, setDeferral] = (0, react_1.useState)();
@@ -38,6 +38,7 @@ exports.QuickPicker = (0, react_1.forwardRef)(function QuickPicker({ title, desc
38
38
  },
39
39
  open: async (_a) => {
40
40
  var { items, selectedItem } = _a, popupProps = __rest(_a, ["items", "selectedItem"]);
41
+ console.debug('[QuickPicker] opening with', { items, selectedItem });
41
42
  // reset
42
43
  setItems(items);
43
44
  setSelectedItem(selectedItem);
@@ -55,19 +56,11 @@ exports.QuickPicker = (0, react_1.forwardRef)(function QuickPicker({ title, desc
55
56
  close: () => { var _a; return (_a = deferralRef.current) === null || _a === void 0 ? void 0 : _a.resolve(undefined); }
56
57
  }), []);
57
58
  // helpers
58
- const onPick = (e) => {
59
- // stop propagation
60
- e.preventDefault();
61
- e.stopPropagation();
59
+ const onPick = (item) => {
62
60
  // resolve
63
- deferral === null || deferral === void 0 ? void 0 : deferral.resolve(selectedItem);
61
+ deferral === null || deferral === void 0 ? void 0 : deferral.resolve(item);
64
62
  };
65
- const onClose = (e) => {
66
- // stop propagation
67
- if (e) {
68
- e.preventDefault();
69
- e.stopPropagation();
70
- }
63
+ const onClose = () => {
71
64
  // ignore if already complete
72
65
  if (deferral === null || deferral === void 0 ? void 0 : deferral.isComplete) {
73
66
  return;
@@ -79,17 +72,37 @@ exports.QuickPicker = (0, react_1.forwardRef)(function QuickPicker({ title, desc
79
72
  // handle keys
80
73
  switch (e.key) {
81
74
  case 'Enter':
82
- onPick(e);
75
+ // stop propagation
76
+ e.preventDefault();
77
+ e.stopPropagation();
78
+ // pick
79
+ onPick(selectedItem);
83
80
  break;
84
81
  case 'Escape':
85
- onClose(e);
82
+ // stop propagation
83
+ e.preventDefault();
84
+ e.stopPropagation();
85
+ // close
86
+ onClose();
86
87
  break;
87
88
  }
88
89
  };
89
90
  // render
90
- return ((0, jsx_runtime_1.jsxs)(Popup_1.Popup, Object.assign({}, (popupProps !== null && popupProps !== void 0 ? popupProps : {}), { className: (0, classnames_1.default)('glass-overlay w-[372px] flex flex-col gap-l8 p-l8 interpolate', 'border-t border-solid border-glass-stroke-l1 rounded-md', {
91
+ return ((0, jsx_runtime_1.jsxs)(Popup_1.Popup, Object.assign({}, (popupProps !== null && popupProps !== void 0 ? popupProps : {}), { className: (0, classnames_1.default)('glass-overlay w-[420px] max-w-full flex flex-col gap-l8 pt-l8 interpolate', 'border-t border-solid border-glass-stroke-l1 rounded-md', {
91
92
  'opacity-none': !opened,
92
93
  'opacity-full': opened
93
- }), animation: "out", open: !!deferral, onOpened: () => setOpened(true), onClose: onClose, onClosed: () => setOpened(false), children: [(0, jsx_runtime_1.jsxs)("section", { children: [(0, jsx_runtime_1.jsx)("h3", { className: "heading-sm text-content-primary", children: title }), description &&
94
- (0, jsx_runtime_1.jsx)("p", { className: "body-sm text-content-secondary", children: description })] }), (0, jsx_runtime_1.jsx)("section", { children: "Items" })] })));
94
+ }), animation: "out", open: !!deferral, onOpened: () => setOpened(true), onClose: onClose, onClosed: () => setOpened(false), children: [(0, jsx_runtime_1.jsxs)("section", { className: "px-l8", children: [(0, jsx_runtime_1.jsx)("h3", { className: "heading-sm text-content-primary", children: title }), description &&
95
+ (0, jsx_runtime_1.jsx)("p", { className: "body-sm text-content-secondary", children: description })] }), (0, jsx_runtime_1.jsx)("section", { className: (0, classnames_1.default)({
96
+ 'flex flex-wrap items-center gap-l5 max-h-[240px] px-l8 pb-l8 overflow-y-scroll': variant === 'card'
97
+ }), children: items.map(item => renderCard({
98
+ item,
99
+ content: renderItem(item),
100
+ selected: item.id === selectedItem,
101
+ onSelect: itemId => onPick(itemId)
102
+ })) })] })));
95
103
  });
104
+ function renderCard({ item, content, selected, onSelect }) {
105
+ return ((0, jsx_runtime_1.jsx)("div", { className: (0, classnames_1.default)('FieldCard w-[116px] h-[88px]', {
106
+ 'FieldCard-selected': selected
107
+ }), onClick: () => onSelect(item.id), children: content }, item.id));
108
+ }
@@ -0,0 +1,26 @@
1
+ import { Callback, Provider, Transform } from '@nascentdigital/funnel-core';
2
+ import { HTMLMotionProps, Variants } from 'framer-motion';
3
+ import { PropsWithChildren, ReactNode } from 'react';
4
+ export declare function SideSheet({ className, title, wide, backdrop, disabled, disableDismiss, disableOverlay, disableSave, canSave, renderSaveLabel, onSave: onSaveRaw, open, onOpened: onOpenedRaw, onClose: onCloseRaw, onClosed: onClosedRaw, onKeyDown: onKeyDownRaw, customActions, children, ...props }: SideSheet.Props): import("react").ReactPortal | null;
5
+ export declare namespace SideSheet {
6
+ type Props = Omit<HTMLMotionProps<'div'>, 'children'> & PropsWithChildren & {
7
+ title: ReactNode;
8
+ wide?: boolean;
9
+ backdrop?: Backdrop;
10
+ disabled?: boolean;
11
+ disableDismiss?: boolean;
12
+ disableOverlay?: boolean;
13
+ disableSave?: boolean;
14
+ canSave?: Provider<boolean> | Provider.Async<boolean>;
15
+ renderSaveLabel?: Transform<State, string>;
16
+ onSave?: Callback.Void | Callback.AsyncVoid;
17
+ customActions?: ReactNode;
18
+ open?: boolean;
19
+ onOpened?: Callback.Void;
20
+ onClose?: Callback.Void;
21
+ onClosed?: Callback.Void;
22
+ };
23
+ type State = 'editing' | 'validating' | 'saving' | 'error' | 'saved';
24
+ type Backdrop = 'none' | 'glass';
25
+ }
26
+ export declare const Animations: Record<'background' | 'slideRight', Variants>;
@@ -0,0 +1,200 @@
1
+ "use strict";
2
+ var __rest = (this && this.__rest) || function (s, e) {
3
+ var t = {};
4
+ for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p) && e.indexOf(p) < 0)
5
+ t[p] = s[p];
6
+ if (s != null && typeof Object.getOwnPropertySymbols === "function")
7
+ for (var i = 0, p = Object.getOwnPropertySymbols(s); i < p.length; i++) {
8
+ if (e.indexOf(p[i]) < 0 && Object.prototype.propertyIsEnumerable.call(s, p[i]))
9
+ t[p[i]] = s[p[i]];
10
+ }
11
+ return t;
12
+ };
13
+ var __importDefault = (this && this.__importDefault) || function (mod) {
14
+ return (mod && mod.__esModule) ? mod : { "default": mod };
15
+ };
16
+ Object.defineProperty(exports, "__esModule", { value: true });
17
+ exports.Animations = void 0;
18
+ exports.SideSheet = SideSheet;
19
+ const jsx_runtime_1 = require("react/jsx-runtime");
20
+ const funnel_core_1 = require("@nascentdigital/funnel-core");
21
+ const classnames_1 = __importDefault(require("classnames"));
22
+ const framer_motion_1 = require("framer-motion");
23
+ const react_1 = require("motion/react");
24
+ const react_2 = require("react");
25
+ const react_dom_1 = require("react-dom");
26
+ const Button_1 = require("./Button");
27
+ // component
28
+ function SideSheet(_a) {
29
+ var { className, title, wide, backdrop = 'none', disabled = false, disableDismiss = false, disableOverlay = false, disableSave = false, canSave = funnel_core_1.Fn.True, renderSaveLabel = onRenderSaveLabel, onSave: onSaveRaw = funnel_core_1.Fn.NoOp, open = false, onOpened: onOpenedRaw = funnel_core_1.Fn.NoOp, onClose: onCloseRaw = funnel_core_1.Fn.NoOp, onClosed: onClosedRaw = funnel_core_1.Fn.NoOp, onKeyDown: onKeyDownRaw, customActions, children } = _a, props = __rest(_a, ["className", "title", "wide", "backdrop", "disabled", "disableDismiss", "disableOverlay", "disableSave", "canSave", "renderSaveLabel", "onSave", "open", "onOpened", "onClose", "onClosed", "onKeyDown", "customActions", "children"]);
30
+ // bind state + hooksb= + refs
31
+ const [state, setState] = (0, react_2.useState)('editing');
32
+ const locked = disabled || state === 'validating' || state === 'saving' || state === 'saved';
33
+ const id = (0, react_2.useId)();
34
+ const openRef = (0, react_2.useRef)(open);
35
+ openRef.current = open;
36
+ const onOpenedRef = (0, react_2.useRef)(onOpenedRaw);
37
+ onOpenedRef.current = onOpenedRaw;
38
+ const onClosedRef = (0, react_2.useRef)(onClosedRaw);
39
+ onClosedRef.current = onClosedRaw;
40
+ // reset state on open
41
+ (0, react_2.useEffect)(() => {
42
+ if (open) {
43
+ setState('editing');
44
+ }
45
+ }, [open]);
46
+ // lock tab focus
47
+ (0, react_2.useEffect)(() => {
48
+ // lock when opened
49
+ let unlock = undefined;
50
+ if (open) {
51
+ unlock = funnel_core_1.Browser.lockFocus(`#${CSS.escape(id)}`);
52
+ }
53
+ // unlock on unmount
54
+ return () => unlock && unlock();
55
+ }, [open, id]);
56
+ // skip for SSR
57
+ if (!funnel_core_1.Browser.available) {
58
+ return null;
59
+ }
60
+ // helpers
61
+ const onSave = async (e) => {
62
+ console.debug('[SideSheet/onSave]', { state, disabled, open });
63
+ // consume event
64
+ if (e) {
65
+ e.preventDefault();
66
+ e.stopPropagation();
67
+ }
68
+ // ignore if state is invalid
69
+ if (state !== 'editing' && state !== 'error') {
70
+ return;
71
+ }
72
+ // try to save
73
+ try {
74
+ // update state
75
+ setState('validating');
76
+ // skip if not valid
77
+ const valid = await Promise.resolve(canSave());
78
+ if (!valid) {
79
+ setState('error');
80
+ return;
81
+ }
82
+ // update state
83
+ setState('saving');
84
+ // try to save
85
+ await Promise.resolve(onSaveRaw());
86
+ // mark as saved
87
+ setState('saved');
88
+ }
89
+ // handle error
90
+ catch (e) {
91
+ console.warn('[SideSheet/onSave] error', e);
92
+ // update state
93
+ setState('error');
94
+ }
95
+ };
96
+ const onClose = (e) => {
97
+ // consume event
98
+ if (e) {
99
+ e.preventDefault();
100
+ e.stopPropagation();
101
+ }
102
+ // close
103
+ onCloseRaw();
104
+ };
105
+ const onKeyDown = async (e) => {
106
+ // ignore if disabled OR state is invalid
107
+ if (disabled || (state !== 'editing' && state !== 'error')) {
108
+ return;
109
+ }
110
+ // cancel on escape
111
+ switch (e.key) {
112
+ case 'Escape':
113
+ if (!disableDismiss) {
114
+ onClose(e);
115
+ }
116
+ break;
117
+ case 'Enter':
118
+ await onSave(e);
119
+ break;
120
+ }
121
+ };
122
+ const onAnimationComplete = (definition) => {
123
+ if (definition === 'visible') {
124
+ onOpenedRef.current();
125
+ }
126
+ else if (definition === 'exit') {
127
+ onClosedRef.current();
128
+ }
129
+ };
130
+ const onBackdropClick = (e) => {
131
+ // close only if the backdrop is directly clicked
132
+ const target = e.target;
133
+ if ((target === null || target === void 0 ? void 0 : target.id) === id) {
134
+ // stop bubbling
135
+ e.stopPropagation();
136
+ // close
137
+ onClose();
138
+ }
139
+ };
140
+ // render
141
+ return (0, react_dom_1.createPortal)((0, jsx_runtime_1.jsx)(react_1.AnimatePresence, { children: open &&
142
+ (0, jsx_runtime_1.jsxs)(framer_motion_1.motion.div, { id: id, className: (0, classnames_1.default)('fixed inset-0 z-popup', {
143
+ 'pointer-events-none': disableOverlay
144
+ }), variants: exports.Animations.background, initial: "initial", animate: "animate", exit: "exit", onAnimationComplete: definition => onAnimationComplete(definition), onClick: disableDismiss || locked ? undefined : onBackdropClick, layout: true, children: [backdrop !== 'none' &&
145
+ (0, jsx_runtime_1.jsx)(framer_motion_1.motion.div, { className: (0, classnames_1.default)('fixed inset-0 z-backdrop pointer-events-none', {
146
+ 'backdrop-blur-[9px] bg-glass/backdrop will-change-[backdrop-filter]': backdrop === 'glass'
147
+ }) }, "backdrop"), (0, jsx_runtime_1.jsx)(framer_motion_1.motion.div, Object.assign({}, props, { className: (0, classnames_1.default)(className, 'fixed right-0 inset-y-0 pointer-events-none', {
148
+ 'w-full sm:w-[380px]': !wide,
149
+ 'w-full md:w-screen-sm': wide,
150
+ }), variants: exports.Animations.slideRight, initial: "initial", animate: "animate", exit: "exit", onKeyDown: onKeyDown, tabIndex: -1, children: (0, jsx_runtime_1.jsxs)("div", { className: (0, classnames_1.default)(className, 'w-full h-full max-h-full pointer-events-auto', 'flex flex-col', 'bg-background border-l border-solid border-border-l1 rounded-tl-md rounded-bl-md'), children: [(0, jsx_runtime_1.jsx)("h2", { className: "heading-md text-content-primary p-l8", children: title }), (0, jsx_runtime_1.jsx)("div", { className: "flex-1 overflow-y-scroll", children: children }), (0, jsx_runtime_1.jsxs)("div", { className: "flex justify-end items-center gap-l4 p-l8", children: [(0, jsx_runtime_1.jsx)(Button_1.Button, { variant: "text", size: "sm", disabled: locked, onClick: onClose, children: "Cancel" }), customActions &&
151
+ (0, jsx_runtime_1.jsx)("div", { className: "flex-1 flex justify-end items-center gap-l4", children: customActions }), (0, jsx_runtime_1.jsx)(Button_1.Button, { variant: "primary", size: "sm", loading: state === 'saving', disabled: disableSave || locked, onClick: onSave, children: renderSaveLabel(state) })] })] }) }))] }, id) }), document.body);
152
+ }
153
+ // helpers
154
+ exports.Animations = {
155
+ background: {
156
+ hidden: {
157
+ opacity: 0
158
+ },
159
+ visible: {
160
+ opacity: 1,
161
+ transition: {
162
+ ease: 'easeIn',
163
+ duration: 0.3,
164
+ when: 'beforeChildren'
165
+ }
166
+ },
167
+ exit: {
168
+ opacity: 0,
169
+ transition: {
170
+ ease: 'easeIn',
171
+ duration: 0.15,
172
+ bounce: 0,
173
+ when: 'afterChildren'
174
+ }
175
+ }
176
+ },
177
+ slideRight: {
178
+ initial: {
179
+ x: '100%'
180
+ },
181
+ animate: {
182
+ x: '0'
183
+ },
184
+ exit: {
185
+ x: '100%'
186
+ }
187
+ }
188
+ };
189
+ function onRenderSaveLabel(state) {
190
+ switch (state) {
191
+ case 'editing':
192
+ case 'validating':
193
+ case 'error':
194
+ return 'Save';
195
+ case 'saving':
196
+ return 'Saving...';
197
+ case 'saved':
198
+ return 'Saved';
199
+ }
200
+ }
@@ -25,6 +25,7 @@ export * from './Profile';
25
25
  export * from './QuickInput';
26
26
  export * from './QuickPicker';
27
27
  export * from './SegmentedButton';
28
+ export * from './SideSheet';
28
29
  export * from './Sortable';
29
30
  export * from './Tabs';
30
31
  export * from './Toggle';
@@ -41,6 +41,7 @@ __exportStar(require("./Profile"), exports);
41
41
  __exportStar(require("./QuickInput"), exports);
42
42
  __exportStar(require("./QuickPicker"), exports);
43
43
  __exportStar(require("./SegmentedButton"), exports);
44
+ __exportStar(require("./SideSheet"), exports);
44
45
  __exportStar(require("./Sortable"), exports);
45
46
  __exportStar(require("./Tabs"), exports);
46
47
  __exportStar(require("./Toggle"), exports);
@@ -16,8 +16,10 @@ export declare class BlockConfiguration {
16
16
  constructor({ block: blockOrSectionBlock, assetStore }: BlockConfiguration.ConstructorArgs);
17
17
  get editableFields(): ReadonlyArray<FieldModel.Config>;
18
18
  set editableFields(fields: ReadonlyArray<FieldModel.Config>);
19
+ get blocksFields(): FieldModel.Config[];
20
+ get blockFieldData(): Schema.Block.Data.ForFields<Schema.Fields>;
19
21
  get desktopFields(): FieldModel.Config[];
20
- get mobileFields(): FieldModel.Config[];
22
+ get mobileFields(): never[];
21
23
  get nameField(): FieldModel.Config | undefined;
22
24
  get themeField(): FieldModel.Config | undefined;
23
25
  get layoutField(): Field<Schema.EnumField<Schema.EnumField.Options<PanelLayout>>> | undefined;
@@ -47,19 +47,41 @@ class BlockConfiguration {
47
47
  this._editableFields = fields;
48
48
  this.raiseEvent('changed');
49
49
  }
50
- get desktopFields() {
50
+ get blocksFields() {
51
51
  return [
52
52
  this.nameField,
53
- this.themeField,
54
- this._editableFields.find(field => field.id === 'contentAlign')
53
+ this.themeField
55
54
  ].filter(funnel_core_1.Value.isDefined);
56
55
  }
57
- get mobileFields() {
56
+ get blockFieldData() {
57
+ const { assetStore, block } = this;
58
+ return block.fields.reduce((data, field) => {
59
+ // link assets
60
+ const fieldValue = field.value;
61
+ if ((fieldValue === null || fieldValue === void 0 ? void 0 : fieldValue.type) === 'asset') {
62
+ const assetData = funnel_core_1.Schema.AssetField.Value.fromData(fieldValue);
63
+ const asset = (assetData === null || assetData === void 0 ? void 0 : assetData.id)
64
+ ? assetStore.getAsset(assetData === null || assetData === void 0 ? void 0 : assetData.id)
65
+ : undefined;
66
+ data[field.id] = assetData && asset
67
+ ? funnel_core_1.Schema.AssetField.LinkedValue.toData(funnel_core_1.Schema.AssetField.Value.toLinked(assetData, asset))
68
+ : fieldValue;
69
+ }
70
+ else {
71
+ data[field.id] = field.value;
72
+ }
73
+ // keep reducing
74
+ return data;
75
+ }, {});
76
+ }
77
+ get desktopFields() {
58
78
  return [
59
- this.nameField,
60
- this.themeField
79
+ this._editableFields.find(field => field.id === 'contentAlign')
61
80
  ].filter(funnel_core_1.Value.isDefined);
62
81
  }
82
+ get mobileFields() {
83
+ return [].filter(funnel_core_1.Value.isDefined);
84
+ }
63
85
  get nameField() {
64
86
  const { block, sectionBlock } = this;
65
87
  return sectionBlock
@@ -175,30 +197,12 @@ class BlockConfiguration {
175
197
  }
176
198
  sync() {
177
199
  // recompute the editable fields
178
- const { assetStore, block, fields, editableFields } = this;
200
+ const { block, blockFieldData: fieldData, fields, editableFields } = this;
179
201
  const configData = editableFields.reduce((data, field) => {
180
202
  data[field.id] = field.value;
181
203
  // keep reducing
182
204
  return data;
183
205
  }, {});
184
- const fieldData = block.fields.reduce((data, field) => {
185
- // link assets
186
- const fieldValue = field.value;
187
- if ((fieldValue === null || fieldValue === void 0 ? void 0 : fieldValue.type) === 'asset') {
188
- const assetData = funnel_core_1.Schema.AssetField.Value.fromData(fieldValue);
189
- const asset = (assetData === null || assetData === void 0 ? void 0 : assetData.id)
190
- ? assetStore.getAsset(assetData === null || assetData === void 0 ? void 0 : assetData.id)
191
- : undefined;
192
- data[field.id] = assetData && asset
193
- ? funnel_core_1.Schema.AssetField.LinkedValue.toData(funnel_core_1.Schema.AssetField.Value.toLinked(assetData, asset))
194
- : fieldValue;
195
- }
196
- else {
197
- data[field.id] = field.value;
198
- }
199
- // keep reducing
200
- return data;
201
- }, {});
202
206
  // get customized schema (if there are effects)
203
207
  const blockSchema = block.schema;
204
208
  const { getConfig, getLayout } = blockSchema;
@@ -9,8 +9,7 @@ const LegacyBlockConfigEditor_1 = require("./LegacyBlockConfigEditor");
9
9
  // component
10
10
  function BlockConfig({ className, block: blockOrSectionBlock }) {
11
11
  // bind ref
12
- const editorRef = (0, react_1.useRef)(null);
13
- const legacyEditorRef = (0, react_1.useRef)(null);
12
+ const [configuration, setConfiguration] = (0, react_1.useState)();
14
13
  // skip if there's no config
15
14
  const block = blockOrSectionBlock instanceof core_1.SectionBlock
16
15
  ? blockOrSectionBlock.block
@@ -19,25 +18,11 @@ function BlockConfig({ className, block: blockOrSectionBlock }) {
19
18
  if (config.length === 0) {
20
19
  return undefined;
21
20
  }
22
- // helpers
23
- const onEdit = async (event) => {
24
- var _a, _b;
25
- // use new configurator (if enabled)
26
- if (process.env.NEXT_PUBLIC_EDITOR_BLOCK_CONFIG === 'experimental') {
27
- await ((_a = editorRef.current) === null || _a === void 0 ? void 0 : _a.edit({
28
- block: blockOrSectionBlock,
29
- anchor: event.currentTarget
30
- }));
31
- }
32
- // or use legacy version
33
- else {
34
- await ((_b = legacyEditorRef.current) === null || _b === void 0 ? void 0 : _b.edit({
35
- block: blockOrSectionBlock,
36
- anchor: event.currentTarget
37
- }));
38
- }
39
- };
40
21
  // render
41
22
  const valid = config.every(field => field.valid);
42
- return ((0, jsx_runtime_1.jsxs)(jsx_runtime_1.Fragment, { children: [(0, jsx_runtime_1.jsx)(core_1.StatusChip, { className: className, variant: valid ? 'success' : 'error', onClick: onEdit, children: "Configure" }), (0, jsx_runtime_1.jsx)(BlockConfigEditor_1.BlockConfigEditor, { ref: editorRef }), (0, jsx_runtime_1.jsx)(LegacyBlockConfigEditor_1.LegacyBlockConfigEditor, { ref: legacyEditorRef })] }));
23
+ return ((0, jsx_runtime_1.jsxs)(jsx_runtime_1.Fragment, { children: [(0, jsx_runtime_1.jsx)(core_1.StatusChip, { className: className, variant: valid ? 'success' : 'error', onClick: () => setConfiguration(block.getConfiguration()), children: "Configure" }), process.env.NEXT_PUBLIC_EDITOR_BLOCK_CONFIG === 'experimental'
24
+ ?
25
+ (0, jsx_runtime_1.jsx)(BlockConfigEditor_1.BlockConfigEditor, { configuration: configuration, onClose: () => setConfiguration(undefined) })
26
+ :
27
+ (0, jsx_runtime_1.jsx)(LegacyBlockConfigEditor_1.LegacyBlockConfigEditor, { configuration: configuration, onClose: () => setConfiguration(undefined) })] }));
43
28
  }
@@ -1,13 +1,9 @@
1
- import { Rect } from '@nascentdigital/funnel-core';
2
- import { Block, SectionBlock } from '../../../core';
3
- export declare const BlockConfigEditor: import("react").ForwardRefExoticComponent<import("react").RefAttributes<BlockConfigEditor.Ref>>;
1
+ import { Callback } from '@nascentdigital/funnel-core';
2
+ import { BlockConfiguration } from '../../../core';
3
+ export declare function BlockConfigEditor({ configuration, onClose }: BlockConfigEditor.Props): import("react/jsx-runtime").JSX.Element | null;
4
4
  export declare namespace BlockConfigEditor {
5
- type Props = {};
6
- interface Ref {
7
- edit(options: Options): Promise<void>;
8
- }
9
- type Options = {
10
- block: Block | SectionBlock;
11
- anchor: HTMLElement | Rect;
5
+ type Props = {
6
+ configuration?: BlockConfiguration;
7
+ onClose: Callback.Void;
12
8
  };
13
9
  }
@@ -1,6 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.BlockConfigEditor = void 0;
3
+ exports.BlockConfigEditor = BlockConfigEditor;
4
4
  const jsx_runtime_1 = require("react/jsx-runtime");
5
5
  const funnel_core_1 = require("@nascentdigital/funnel-core");
6
6
  const client_1 = require("@nascentdigital/funnel-ui/client");
@@ -11,52 +11,30 @@ const core_1 = require("../../../core");
11
11
  const hooks_1 = require("../../hooks");
12
12
  const BlockPreview_1 = require("./BlockPreview");
13
13
  // editor
14
- exports.BlockConfigEditor = (0, react_2.forwardRef)(function BlockConfigEditor(_, ref) {
14
+ function BlockConfigEditor({ configuration, onClose }) {
15
15
  // bind state + hooks
16
- const [deferral, setDeferral] = (0, react_2.useState)(undefined);
17
- const [configuration, setConfiguration] = (0, react_2.useState)();
18
- const [anchor, setAnchor] = (0, react_2.useState)();
19
16
  const [blockView, setBlockView] = (0, react_2.useState)('desktop');
20
17
  const pageController = (0, hooks_1.usePageController)();
21
18
  const touch = (0, client_1.useTouch)();
22
19
  const panelLayoutPickerRef = (0, react_2.useRef)(null);
23
- const { modified = false, valid = false } = configuration !== null && configuration !== void 0 ? configuration : {};
20
+ const { modified = false, valid = false, blocksFields = [], desktopFields = [], mobileFields = [] } = configuration !== null && configuration !== void 0 ? configuration : {};
21
+ const settingsFields = blockView === 'desktop' ? desktopFields : mobileFields;
24
22
  // sync w/ configuration
25
23
  (0, core_1.useBlockConfiguration)(configuration);
26
- // bind ref
27
- (0, react_2.useImperativeHandle)(ref, () => ({
28
- edit: async ({ block, anchor }) => {
29
- // reset state
30
- setConfiguration(block.getConfiguration());
31
- setAnchor(anchor);
24
+ // reset to desktop when open
25
+ (0, react_2.useEffect)(() => {
26
+ if (configuration) {
32
27
  setBlockView('desktop');
33
- // create deferral
34
- const deferral = new funnel_core_1.Deferral();
35
- setDeferral(deferral);
36
- // wait until deferral is resolved
37
- return deferral.promise;
38
28
  }
39
- }), []);
29
+ }, [configuration]);
40
30
  // helpers
41
31
  const onSave = async () => {
42
32
  // save config
43
33
  configuration === null || configuration === void 0 ? void 0 : configuration.save();
44
34
  // wait for page to save
45
35
  await (0, rxjs_1.firstValueFrom)(pageController.saved$);
46
- // complete (we're done)
47
- deferral === null || deferral === void 0 ? void 0 : deferral.resolve();
48
- // refresh
49
- touch();
50
- };
51
- const onCancel = () => {
52
- // skip if missing or already completed
53
- if (!deferral || deferral.isComplete) {
54
- return;
55
- }
56
- // cancel
57
- deferral.resolve(undefined);
58
- // touch
59
- touch();
36
+ // complete
37
+ onClose();
60
38
  };
61
39
  const onChangePanelLayout = async (e) => {
62
40
  var _a;
@@ -65,7 +43,7 @@ exports.BlockConfigEditor = (0, react_2.forwardRef)(function BlockConfigEditor(_
65
43
  if (!layoutField) {
66
44
  return;
67
45
  }
68
- // pick new layout (if ignore if skipped)
46
+ // pick new layout
69
47
  const { schema: layoutSchema, value: layoutData } = layoutField;
70
48
  const selectedLayout = await ((_a = panelLayoutPickerRef.current) === null || _a === void 0 ? void 0 : _a.open({
71
49
  anchor: e.currentTarget,
@@ -77,8 +55,9 @@ exports.BlockConfigEditor = (0, react_2.forwardRef)(function BlockConfigEditor(_
77
55
  })),
78
56
  selectedItem: layoutData === null || layoutData === void 0 ? void 0 : layoutData.value
79
57
  }));
80
- if (!selectedLayout) {
81
- return;
58
+ // update if something was picked
59
+ if (selectedLayout) {
60
+ layoutField.value = funnel_core_1.Schema.EnumField.Value.toData(selectedLayout);
82
61
  }
83
62
  };
84
63
  // skip if there's no config
@@ -86,8 +65,22 @@ exports.BlockConfigEditor = (0, react_2.forwardRef)(function BlockConfigEditor(_
86
65
  return null;
87
66
  }
88
67
  // render
89
- return ((0, jsx_runtime_1.jsxs)(jsx_runtime_1.Fragment, { children: [(0, jsx_runtime_1.jsxs)(core_1.MiniEditor, { className: "w-[550px] max-w-full", title: "Block Configuration", open: !!deferral && !deferral.isComplete, anchor: anchor, anchorOffset: 4, onSave: onSave, onClose: onCancel, disableSave: !modified || !valid, customActions: (0, jsx_runtime_1.jsx)(core_1.Button, { variant: "secondary", size: "sm", disabled: configuration.isConfigInDefault, onClick: () => configuration.reset(), children: "Reset to Default" }), wide: true, children: [(0, jsx_runtime_1.jsxs)("section", { className: "flex items-center gap-l4", children: [(0, jsx_runtime_1.jsx)("span", { className: "label-sm text-content-primary flex-1", children: "Layout" }), (0, jsx_runtime_1.jsx)(core_1.Button, { variant: "secondary", size: "sm", iconLeft: "ArrowLeftRight", children: "Rearrange" }), (0, jsx_runtime_1.jsx)(core_1.Button, { variant: "secondary", size: "sm", iconLeft: "LayoutTemplate", onClick: onChangePanelLayout, children: "Change" })] }), (0, jsx_runtime_1.jsx)(core_1.Tabs, { items: blockViews, selectedItem: blockView, onSelect: setBlockView }), (0, jsx_runtime_1.jsx)("h4", { className: "heading-sm text-content-primary", children: "Settings" }), (0, jsx_runtime_1.jsx)("section", { className: "stacked p-l10 bg-surface1 rounded-md", children: (0, jsx_runtime_1.jsx)(react_1.AnimatePresence, { children: (0, jsx_runtime_1.jsx)(core_1.Animate, { effect: "fade", present: true, children: (0, jsx_runtime_1.jsx)(BlockPreview_1.BlockPreview, { variant: blockView }) }, blockView) }) }), (0, jsx_runtime_1.jsx)("section", { className: "grid grid-cols-2 auto-rows-auto items-start gap-l6", children: (0, jsx_runtime_1.jsx)(core_1.FieldErrorController, { children: (blockView === 'desktop' ? configuration.desktopFields : configuration.mobileFields).map(field => (0, jsx_runtime_1.jsx)(core_1.FieldEditor, { className: "col-span-2", field: field, variant: "wide" }, field.id)) }) }), (0, jsx_runtime_1.jsx)("hr", { className: "divider border-border-l1" })] }), (0, jsx_runtime_1.jsx)(core_1.QuickPicker, { ref: panelLayoutPickerRef, title: "Pick Block Layout", description: "Select an available layout for this block." })] }));
90
- });
68
+ return ((0, jsx_runtime_1.jsxs)(jsx_runtime_1.Fragment, { children: [(0, jsx_runtime_1.jsxs)(core_1.SideSheet, { title: "Block Configuration", open: !!configuration, onSave: onSave, onClose: onClose, disableSave: !modified || !valid, customActions: (0, jsx_runtime_1.jsx)(core_1.Button, { variant: "secondary", size: "sm", disabled: configuration.isConfigInDefault, onClick: () => configuration.reset(), children: "Reset to Default" }), wide: true, children: [(0, jsx_runtime_1.jsxs)("section", { className: "flex items-center gap-l4", children: [(0, jsx_runtime_1.jsx)("span", { className: "label-sm text-content-primary flex-1", children: "Layout" }), (0, jsx_runtime_1.jsx)(core_1.Button, { variant: "secondary", size: "sm", iconLeft: "ArrowLeftRight", children: "Rearrange" }), (0, jsx_runtime_1.jsx)(core_1.Button, { variant: "secondary", size: "sm", iconLeft: "LayoutTemplate", onClick: onChangePanelLayout, children: "Change" })] }), blocksFields.length > 0 &&
69
+ (0, jsx_runtime_1.jsx)("section", { className: "grid grid-cols-2 auto-rows-auto items-start gap-l6", children: (0, jsx_runtime_1.jsx)(core_1.FieldErrorController, { children: blocksFields.map(field => (0, jsx_runtime_1.jsx)(core_1.FieldEditor, { className: "col-span-2", field: field, variant: "wide" }, field.id)) }) }), (0, jsx_runtime_1.jsx)(core_1.Tabs, { items: blockViews, selectedItem: blockView, onSelect: setBlockView }), (0, jsx_runtime_1.jsx)("section", { className: "stacked p-l6 bg-surface1 rounded-md", children: (0, jsx_runtime_1.jsx)(react_1.AnimatePresence, { children: (0, jsx_runtime_1.jsx)(core_1.Animate, { effect: "fade", present: true, children: (0, jsx_runtime_1.jsx)(BlockPreview_1.BlockPreview, { variant: blockView, configuration: configuration }) }, blockView) }) }), settingsFields.length > 0 &&
70
+ (0, jsx_runtime_1.jsx)("section", { className: "grid grid-cols-2 auto-rows-auto items-start gap-l6", children: (0, jsx_runtime_1.jsx)(core_1.FieldErrorController, { children: settingsFields.map(field => (0, jsx_runtime_1.jsx)(core_1.FieldEditor, { className: "col-span-2", field: field, variant: "wide" }, field.id)) }) }), (0, jsx_runtime_1.jsx)("hr", { className: "divider border-border-l1" })] }), (0, jsx_runtime_1.jsx)(core_1.QuickPicker, { ref: panelLayoutPickerRef, variant: "card", title: "Pick Block Layout", description: "Select an available layout for this block.", renderItem: (item) => renderPanelLayoutItem(item) })] }));
71
+ }
72
+ function renderPanelLayoutItem(item) {
73
+ switch (item.id) {
74
+ case 'stack.h':
75
+ return ((0, jsx_runtime_1.jsx)("div", { children: "Stack H" }));
76
+ case 'stack.v':
77
+ return ((0, jsx_runtime_1.jsx)("div", { children: "Stack V" }));
78
+ case 'nested':
79
+ return ((0, jsx_runtime_1.jsx)("div", { children: "Nested" }));
80
+ default:
81
+ return ((0, jsx_runtime_1.jsx)("div", { children: item.id }));
82
+ }
83
+ }
91
84
  const blockViews = [
92
85
  { id: 'desktop', label: 'Desktop' },
93
86
  { id: 'mobile', label: 'Mobile' },
@@ -1,8 +1,10 @@
1
- export declare function BlockPreview({ className, variant }: BlockPreview.Props): import("react/jsx-runtime").JSX.Element;
1
+ import { BlockConfiguration } from '../../../core';
2
+ export declare function BlockPreview({ className, variant, configuration }: BlockPreview.Props): import("react/jsx-runtime").JSX.Element;
2
3
  export declare namespace BlockPreview {
3
4
  type Variant = 'desktop' | 'mobile';
4
5
  type Props = {
5
6
  className?: string;
6
7
  variant: Variant;
8
+ configuration: BlockConfiguration;
7
9
  };
8
10
  }