@teambit/compositions 1.0.630 → 1.0.632
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/compositions.tsx +2 -1
- package/dist/compositions.js +3 -2
- package/dist/compositions.js.map +1 -1
- package/dist/{preview-1749698416924.js → preview-1750130328202.js} +2 -2
- package/dist/ui/compositions-panel/compositions-panel.js +117 -8
- package/dist/ui/compositions-panel/compositions-panel.js.map +1 -1
- package/dist/ui/compositions-panel/compositions-panel.module.scss +54 -8
- package/dist/ui/compositions-panel/live-control-input.d.ts +10 -0
- package/dist/ui/compositions-panel/live-control-input.js +268 -0
- package/dist/ui/compositions-panel/live-control-input.js.map +1 -0
- package/dist/ui/compositions-panel/live-control-input.module.scss +3 -0
- package/dist/ui/compositions-panel/live-control-panel.d.ts +6 -0
- package/dist/ui/compositions-panel/live-control-panel.js +62 -0
- package/dist/ui/compositions-panel/live-control-panel.js.map +1 -0
- package/dist/ui/compositions-panel/live-control-panel.module.scss +14 -0
- package/package.json +23 -14
- package/ui/compositions-panel/compositions-panel.module.scss +54 -8
- package/ui/compositions-panel/compositions-panel.tsx +132 -36
- package/ui/compositions-panel/live-control-input.module.scss +3 -0
- package/ui/compositions-panel/live-control-input.tsx +169 -0
- package/ui/compositions-panel/live-control-panel.module.scss +14 -0
- package/ui/compositions-panel/live-control-panel.tsx +40 -0
|
@@ -1,12 +1,20 @@
|
|
|
1
1
|
import { Icon } from '@teambit/evangelist.elements.icon';
|
|
2
2
|
import classNames from 'classnames';
|
|
3
3
|
import { useSearchParams } from 'react-router-dom';
|
|
4
|
-
import React, { useCallback } from 'react';
|
|
4
|
+
import React, { useCallback, useEffect, useState } from 'react';
|
|
5
|
+
import { DrawerUI } from '@teambit/ui-foundation.ui.tree.drawer';
|
|
5
6
|
import { MenuWidgetIcon } from '@teambit/ui-foundation.ui.menu-widget-icon';
|
|
6
7
|
import { Tooltip } from '@teambit/design.ui.tooltip';
|
|
7
8
|
import { useNavigate, useLocation } from '@teambit/base-react.navigation.link';
|
|
8
|
-
import {
|
|
9
|
+
import {
|
|
10
|
+
type LiveControlReadyEventData,
|
|
11
|
+
getReadyListener,
|
|
12
|
+
broadcastUpdate,
|
|
13
|
+
} from '@teambit/compositions.ui.composition-live-controls';
|
|
14
|
+
|
|
9
15
|
import styles from './compositions-panel.module.scss';
|
|
16
|
+
import { Composition } from '../../composition';
|
|
17
|
+
import { LiveControls } from './live-control-panel';
|
|
10
18
|
|
|
11
19
|
export type CompositionsPanelProps = {
|
|
12
20
|
/**
|
|
@@ -46,23 +54,47 @@ export function CompositionsPanel({
|
|
|
46
54
|
className,
|
|
47
55
|
...rest
|
|
48
56
|
}: CompositionsPanelProps) {
|
|
57
|
+
// setup drawer state
|
|
58
|
+
// TODO: only for alpha versions of live controls. remove when stable.
|
|
59
|
+
const [hasLiveControls, setHasLiveControls] = useState(false);
|
|
60
|
+
const [openDrawerList, onToggleDrawer] = useState(['COMPOSITIONS', 'LIVE_CONTROLS']);
|
|
61
|
+
const handleDrawerToggle = (id: string) => {
|
|
62
|
+
const isDrawerOpen = openDrawerList.includes(id);
|
|
63
|
+
if (isDrawerOpen) {
|
|
64
|
+
onToggleDrawer((list) => list.filter((drawer) => drawer !== id));
|
|
65
|
+
return;
|
|
66
|
+
}
|
|
67
|
+
onToggleDrawer((list) => list.concat(id));
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
// setup from props
|
|
49
71
|
const shouldAddNameParam = useNameParam || (isScaling && includesEnvTemplate === false);
|
|
50
72
|
|
|
73
|
+
// current composition state
|
|
74
|
+
const location = useLocation();
|
|
75
|
+
const [searchParams] = useSearchParams();
|
|
76
|
+
const versionFromQueryParams = searchParams.get('version');
|
|
77
|
+
const navigate = useNavigate();
|
|
78
|
+
|
|
79
|
+
// live control state
|
|
80
|
+
const [controlsTimestamp, setControlsTimestamp] = useState(0);
|
|
81
|
+
const [controlsDefs, setControlsDefs] = useState<any>(null);
|
|
82
|
+
const [controlsValues, setControlsValues] = useState<any>({});
|
|
83
|
+
const [mounter, setMounter] = useState<Window>();
|
|
84
|
+
|
|
85
|
+
// composition navigation action
|
|
51
86
|
const handleSelect = useCallback(
|
|
52
87
|
(selected: Composition) => {
|
|
53
88
|
onSelect && onSelect(selected);
|
|
89
|
+
if (selected === active) return;
|
|
90
|
+
setControlsTimestamp(0);
|
|
54
91
|
},
|
|
55
92
|
[onSelect]
|
|
56
93
|
);
|
|
57
|
-
|
|
58
|
-
const location = useLocation();
|
|
59
|
-
const [searchParams] = useSearchParams();
|
|
60
|
-
const versionFromQueryParams = searchParams.get('version');
|
|
61
|
-
const navigate = useNavigate();
|
|
62
|
-
|
|
63
94
|
const onCompositionCodeClicked = useCallback(
|
|
64
95
|
(composition: Composition) => (e: React.MouseEvent<HTMLDivElement>) => {
|
|
65
96
|
e.preventDefault();
|
|
97
|
+
setControlsTimestamp(0);
|
|
66
98
|
const queryParams = new URLSearchParams();
|
|
67
99
|
if (versionFromQueryParams) {
|
|
68
100
|
queryParams.set('version', versionFromQueryParams);
|
|
@@ -73,35 +105,99 @@ export function CompositionsPanel({
|
|
|
73
105
|
[location?.pathname, versionFromQueryParams]
|
|
74
106
|
);
|
|
75
107
|
|
|
108
|
+
// listen to the mounter for live control updates
|
|
109
|
+
useEffect(() => {
|
|
110
|
+
// TODO: remove when stable.
|
|
111
|
+
window.addEventListener('message', (e: MessageEvent) => {
|
|
112
|
+
if (e.data.type === 'composition-live-controls:activate') {
|
|
113
|
+
setHasLiveControls(true);
|
|
114
|
+
}
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
function onLiveControlsSetup(e: MessageEvent<LiveControlReadyEventData>) {
|
|
118
|
+
getReadyListener(e, ({ controls, values, timestamp }) => {
|
|
119
|
+
const iframeWindow = e.source;
|
|
120
|
+
setMounter(iframeWindow as Window);
|
|
121
|
+
setControlsDefs(controls);
|
|
122
|
+
setControlsValues(values);
|
|
123
|
+
setControlsTimestamp(timestamp);
|
|
124
|
+
});
|
|
125
|
+
}
|
|
126
|
+
window.addEventListener('message', onLiveControlsSetup);
|
|
127
|
+
return () => {
|
|
128
|
+
window.removeEventListener('message', onLiveControlsSetup);
|
|
129
|
+
};
|
|
130
|
+
}, []);
|
|
131
|
+
|
|
132
|
+
// sync live control updates back to the mounter
|
|
133
|
+
const onLiveControlsUpdate = useCallback(
|
|
134
|
+
(key: string, value: any) => {
|
|
135
|
+
if (mounter) {
|
|
136
|
+
broadcastUpdate(mounter, controlsTimestamp, {
|
|
137
|
+
key,
|
|
138
|
+
value,
|
|
139
|
+
});
|
|
140
|
+
}
|
|
141
|
+
setControlsValues((prev: any) => ({ ...prev, [key]: value }));
|
|
142
|
+
},
|
|
143
|
+
[mounter, controlsValues, controlsTimestamp]
|
|
144
|
+
);
|
|
145
|
+
|
|
76
146
|
return (
|
|
77
|
-
<
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
<
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
onClick={
|
|
95
|
-
|
|
96
|
-
<Tooltip content="Open in new tab" placement="bottom">
|
|
97
|
-
<a className={styles.panelLink} target="_blank" rel="noopener noreferrer" href={href}>
|
|
98
|
-
<Icon className={styles.icon} of="open-tab" />
|
|
147
|
+
<div className={classNames(styles.container)}>
|
|
148
|
+
<DrawerUI
|
|
149
|
+
isOpen={openDrawerList.includes('COMPOSITIONS')}
|
|
150
|
+
onToggle={() => handleDrawerToggle('COMPOSITIONS')}
|
|
151
|
+
name="COMPOSITIONS"
|
|
152
|
+
className={classNames(styles.tab)}
|
|
153
|
+
>
|
|
154
|
+
<ul {...rest} className={classNames(className)}>
|
|
155
|
+
{compositions.map((composition) => {
|
|
156
|
+
const href = shouldAddNameParam
|
|
157
|
+
? `${url}&name=${composition.identifier}`
|
|
158
|
+
: `${url}&${composition.identifier}`;
|
|
159
|
+
return (
|
|
160
|
+
<li
|
|
161
|
+
key={composition.identifier}
|
|
162
|
+
className={classNames(styles.linkWrapper, composition === active && styles.active)}
|
|
163
|
+
>
|
|
164
|
+
<a className={styles.panelLink} onClick={() => handleSelect(composition)}>
|
|
165
|
+
<span className={styles.name}>{composition.displayName}</span>
|
|
99
166
|
</a>
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
167
|
+
<div className={styles.right}>
|
|
168
|
+
<MenuWidgetIcon
|
|
169
|
+
className={styles.codeLink}
|
|
170
|
+
icon="Code"
|
|
171
|
+
tooltipContent="Code"
|
|
172
|
+
onClick={onCompositionCodeClicked(composition)}
|
|
173
|
+
/>
|
|
174
|
+
<Tooltip content="Open in new tab" placement="bottom">
|
|
175
|
+
<a className={styles.iconLink} target="_blank" rel="noopener noreferrer" href={href}>
|
|
176
|
+
<Icon className={styles.icon} of="open-tab" />
|
|
177
|
+
</a>
|
|
178
|
+
</Tooltip>
|
|
179
|
+
</div>
|
|
180
|
+
</li>
|
|
181
|
+
);
|
|
182
|
+
})}
|
|
183
|
+
</ul>
|
|
184
|
+
</DrawerUI>
|
|
185
|
+
{
|
|
186
|
+
/* TODO: remove when stable */ hasLiveControls ? (
|
|
187
|
+
<DrawerUI
|
|
188
|
+
isOpen={openDrawerList.includes('LIVE_CONTROLS')}
|
|
189
|
+
onToggle={() => handleDrawerToggle('LIVE_CONTROLS')}
|
|
190
|
+
className={classNames(styles.tab)}
|
|
191
|
+
name="LIVE CONTROLS"
|
|
192
|
+
>
|
|
193
|
+
{controlsTimestamp ? (
|
|
194
|
+
<LiveControls defs={controlsDefs} values={controlsValues} onChange={onLiveControlsUpdate} />
|
|
195
|
+
) : (
|
|
196
|
+
<div className={styles.noLiveControls}>No live controls available for this composition</div>
|
|
197
|
+
)}
|
|
198
|
+
</DrawerUI>
|
|
199
|
+
) : null
|
|
200
|
+
}
|
|
201
|
+
</div>
|
|
106
202
|
);
|
|
107
203
|
}
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import classNames from 'classnames';
|
|
3
|
+
|
|
4
|
+
import { InputText } from '@teambit/design.inputs.input-text';
|
|
5
|
+
import { TextArea } from '@teambit/design.inputs.text-area';
|
|
6
|
+
import { Dropdown } from '@teambit/design.inputs.dropdown';
|
|
7
|
+
import { MenuItem } from '@teambit/design.inputs.selectors.menu-item';
|
|
8
|
+
import { ColorPicker } from '@teambit/design.ui.input.color-picker';
|
|
9
|
+
import { DatePicker } from '@teambit/design.inputs.date-picker';
|
|
10
|
+
import { Toggle } from '@teambit/design.inputs.toggle-switch';
|
|
11
|
+
|
|
12
|
+
import styles from './live-control-input.module.scss';
|
|
13
|
+
|
|
14
|
+
type InputComponentProps = {
|
|
15
|
+
id: string;
|
|
16
|
+
value: any;
|
|
17
|
+
onChange: (value: any) => void;
|
|
18
|
+
meta?: any;
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
type InputComponent = React.FC<InputComponentProps>;
|
|
22
|
+
|
|
23
|
+
function ShortTextInput({ id, value, onChange }: InputComponentProps) {
|
|
24
|
+
const [inputValue, setInputValue] = React.useState(value);
|
|
25
|
+
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
|
26
|
+
const newValue = e.target.value;
|
|
27
|
+
onChange(newValue);
|
|
28
|
+
setInputValue(newValue);
|
|
29
|
+
};
|
|
30
|
+
return <InputText id={id} value={inputValue} onChange={handleChange} />;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function LongTextInput({ id, value, onChange }: InputComponentProps) {
|
|
34
|
+
const [inputValue, setInputValue] = React.useState(value);
|
|
35
|
+
const handleChange = (e: React.ChangeEvent<HTMLTextAreaElement>) => {
|
|
36
|
+
const newValue = e.target.value;
|
|
37
|
+
onChange(newValue);
|
|
38
|
+
setInputValue(newValue);
|
|
39
|
+
};
|
|
40
|
+
return <TextArea id={id} value={inputValue} onChange={handleChange} />;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function SelectInput({ id, value, onChange, meta }: InputComponentProps) {
|
|
44
|
+
const [selectedValue, setSelectedValue] = React.useState(value);
|
|
45
|
+
const handleChange = (newValue: any) => {
|
|
46
|
+
onChange(newValue);
|
|
47
|
+
setSelectedValue(newValue);
|
|
48
|
+
};
|
|
49
|
+
const placeholderContent = meta.options.find((o: any) => o.value === selectedValue)?.label;
|
|
50
|
+
return (
|
|
51
|
+
<p className={classNames(styles.wrapper)}>
|
|
52
|
+
<Dropdown id={id} placeholderContent={placeholderContent}>
|
|
53
|
+
{meta.options.map((option: any) => (
|
|
54
|
+
<MenuItem
|
|
55
|
+
active={option.value === selectedValue}
|
|
56
|
+
key={option.value}
|
|
57
|
+
onClick={() => handleChange(option.value)}
|
|
58
|
+
>
|
|
59
|
+
{option.label}
|
|
60
|
+
</MenuItem>
|
|
61
|
+
))}
|
|
62
|
+
</Dropdown>
|
|
63
|
+
</p>
|
|
64
|
+
);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function NumberInput({ id, value, onChange }: InputComponentProps) {
|
|
68
|
+
const [inputValue, setInputValue] = React.useState(value);
|
|
69
|
+
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
|
70
|
+
const newValue = e.target.value;
|
|
71
|
+
if (!isNaN(Number(newValue))) {
|
|
72
|
+
onChange(Number(newValue));
|
|
73
|
+
setInputValue(Number(newValue));
|
|
74
|
+
} else {
|
|
75
|
+
// TODO: render error message
|
|
76
|
+
// eslint-disable-next-line no-console
|
|
77
|
+
console.error('Invalid number input', newValue);
|
|
78
|
+
}
|
|
79
|
+
};
|
|
80
|
+
return <InputText id={id} type="number" value={inputValue} onChange={handleChange} />;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function ColorInput({ id, value, onChange }: InputComponentProps) {
|
|
84
|
+
const [inputValue, setInputValue] = React.useState(value);
|
|
85
|
+
const handleChange = (newValue: string) => {
|
|
86
|
+
onChange(newValue);
|
|
87
|
+
setInputValue(newValue);
|
|
88
|
+
};
|
|
89
|
+
return (
|
|
90
|
+
<p className={classNames(styles.wrapper)}>
|
|
91
|
+
<ColorPicker id={id} value={inputValue} onColorSelect={handleChange} />
|
|
92
|
+
</p>
|
|
93
|
+
);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function DateInput({ id, value, onChange }: InputComponentProps) {
|
|
97
|
+
const [inputValue, setInputValue] = React.useState<Date | null>(new Date(value));
|
|
98
|
+
const handleChange = (newValue: Date | null) => {
|
|
99
|
+
if (newValue) {
|
|
100
|
+
onChange(newValue.toISOString().split('T')[0]);
|
|
101
|
+
}
|
|
102
|
+
setInputValue(newValue);
|
|
103
|
+
};
|
|
104
|
+
return (
|
|
105
|
+
<p className={classNames(styles.wrapper)}>
|
|
106
|
+
<DatePicker id={id} date={inputValue} onChange={handleChange} />
|
|
107
|
+
</p>
|
|
108
|
+
);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function ToggleInput({ id, value, onChange }: InputComponentProps) {
|
|
112
|
+
const [isChecked, setIsChecked] = React.useState(value);
|
|
113
|
+
const handleChange = () => {
|
|
114
|
+
setIsChecked(!isChecked);
|
|
115
|
+
onChange(!isChecked);
|
|
116
|
+
};
|
|
117
|
+
return (
|
|
118
|
+
<p className={classNames(styles.wrapper)}>
|
|
119
|
+
<Toggle id={id} defaultChecked={isChecked} onChange={handleChange} />
|
|
120
|
+
</p>
|
|
121
|
+
);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function JsonInput({ id, value, onChange }: InputComponentProps) {
|
|
125
|
+
const [inputValue, setInputValue] = React.useState(JSON.stringify(value, null, 2));
|
|
126
|
+
const [message, setMessage] = React.useState('');
|
|
127
|
+
const handleChange = (e: React.ChangeEvent<HTMLTextAreaElement>) => {
|
|
128
|
+
const newValue = e.target.value;
|
|
129
|
+
try {
|
|
130
|
+
const parsedValue = JSON.parse(newValue);
|
|
131
|
+
onChange(parsedValue);
|
|
132
|
+
setMessage('');
|
|
133
|
+
} catch {
|
|
134
|
+
setMessage('Invalid JSON');
|
|
135
|
+
}
|
|
136
|
+
setInputValue(newValue);
|
|
137
|
+
};
|
|
138
|
+
return (
|
|
139
|
+
<div>
|
|
140
|
+
<TextArea id={id} value={inputValue} onChange={handleChange} />
|
|
141
|
+
{message && <div style={{ color: 'red' }}>{message}</div>}
|
|
142
|
+
</div>
|
|
143
|
+
);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
export function getInputComponent(type: string): InputComponent {
|
|
147
|
+
switch (type) {
|
|
148
|
+
case 'text':
|
|
149
|
+
return ShortTextInput;
|
|
150
|
+
case 'longtext':
|
|
151
|
+
return LongTextInput;
|
|
152
|
+
case 'select':
|
|
153
|
+
return SelectInput;
|
|
154
|
+
case 'number':
|
|
155
|
+
return NumberInput;
|
|
156
|
+
case 'color':
|
|
157
|
+
return ColorInput;
|
|
158
|
+
case 'date':
|
|
159
|
+
return DateInput;
|
|
160
|
+
case 'boolean':
|
|
161
|
+
return ToggleInput;
|
|
162
|
+
case 'json':
|
|
163
|
+
return JsonInput;
|
|
164
|
+
default:
|
|
165
|
+
// eslint-disable-next-line no-console
|
|
166
|
+
console.warn(`Unknown input type: ${type}`);
|
|
167
|
+
return ShortTextInput;
|
|
168
|
+
}
|
|
169
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import classNames from 'classnames';
|
|
3
|
+
import { type Control } from '@teambit/compositions.ui.composition-live-controls';
|
|
4
|
+
|
|
5
|
+
import styles from './live-control-panel.module.scss';
|
|
6
|
+
import { getInputComponent } from './live-control-input';
|
|
7
|
+
|
|
8
|
+
export function LiveControls({
|
|
9
|
+
defs,
|
|
10
|
+
values,
|
|
11
|
+
onChange,
|
|
12
|
+
}: {
|
|
13
|
+
defs: Array<Control>;
|
|
14
|
+
values: Record<string, any>;
|
|
15
|
+
onChange: (key: string, value: any) => void;
|
|
16
|
+
}) {
|
|
17
|
+
return (
|
|
18
|
+
<ul className={classNames(styles.container)}>
|
|
19
|
+
{defs.map((field) => {
|
|
20
|
+
const key = field.id;
|
|
21
|
+
const InputComponent = getInputComponent(field.input || 'text');
|
|
22
|
+
return (
|
|
23
|
+
<li key={key} className={classNames(styles.item)}>
|
|
24
|
+
<div className={classNames(styles.label)}>
|
|
25
|
+
<label htmlFor={`control-${key}`}>{field.label || field.id}</label>
|
|
26
|
+
</div>
|
|
27
|
+
<div>
|
|
28
|
+
<InputComponent
|
|
29
|
+
id={`control-${key}`}
|
|
30
|
+
value={values[key]}
|
|
31
|
+
onChange={(v: any) => onChange(key, v)}
|
|
32
|
+
meta={field}
|
|
33
|
+
/>
|
|
34
|
+
</div>
|
|
35
|
+
</li>
|
|
36
|
+
);
|
|
37
|
+
})}
|
|
38
|
+
</ul>
|
|
39
|
+
);
|
|
40
|
+
}
|