@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.
@@ -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 { Composition } from '../../composition';
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
- <ul {...rest} className={classNames(className)}>
78
- {compositions.map((composition) => {
79
- const href = shouldAddNameParam ? `${url}&name=${composition.identifier}` : `${url}&${composition.identifier}`;
80
- return (
81
- <li
82
- key={composition.identifier}
83
- className={classNames(styles.linkWrapper, composition === active && styles.active)}
84
- >
85
- <a className={styles.panelLink} onClick={() => handleSelect(composition)}>
86
- <span className={styles.box}></span>
87
- <span className={styles.name}>{composition.displayName}</span>
88
- </a>
89
- <div className={styles.right}>
90
- <MenuWidgetIcon
91
- className={styles.codeLink}
92
- icon="Code"
93
- tooltipContent="Code"
94
- onClick={onCompositionCodeClicked(composition)}
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
- </Tooltip>
101
- </div>
102
- </li>
103
- );
104
- })}
105
- </ul>
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,3 @@
1
+ .wrapper {
2
+ padding: 4px 0;
3
+ }
@@ -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,14 @@
1
+ .container {
2
+ display: flex;
3
+ flex-direction: column;
4
+ font-size: 12px;
5
+ padding: 8px 0;
6
+ }
7
+
8
+ .item {
9
+ padding: 4px 8px;
10
+ }
11
+
12
+ .label {
13
+ font-weight: bold;
14
+ }
@@ -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
+ }