@teambit/compositions 1.0.894 → 1.0.895
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/composition.section.tsx +1 -0
- package/compositions.tsx +30 -5
- package/dist/composition.section.js +2 -1
- package/dist/composition.section.js.map +1 -1
- package/dist/compositions.d.ts +2 -1
- package/dist/compositions.js +36 -2
- package/dist/compositions.js.map +1 -1
- package/dist/index.d.ts +2 -1
- package/dist/index.js +31 -0
- package/dist/index.js.map +1 -1
- package/dist/{preview-1772229083132.js → preview-1772488540837.js} +2 -2
- package/dist/ui/compositions-panel/compositions-panel.js +20 -66
- package/dist/ui/compositions-panel/compositions-panel.js.map +1 -1
- package/dist/ui/compositions-panel/compositions-panel.module.scss +33 -0
- package/dist/ui/compositions-panel/live-control-input.d.ts +1 -0
- package/dist/ui/compositions-panel/live-control-input.js +142 -23
- package/dist/ui/compositions-panel/live-control-input.js.map +1 -1
- package/dist/ui/compositions-panel/live-control-input.module.scss +94 -1
- package/dist/ui/compositions-panel/live-control-panel.d.ts +3 -1
- package/dist/ui/compositions-panel/live-control-panel.js +4 -2
- package/dist/ui/compositions-panel/live-control-panel.js.map +1 -1
- package/dist/ui/compositions-panel/live-controls-diff-panel.d.ts +13 -0
- package/dist/ui/compositions-panel/live-controls-diff-panel.js +220 -0
- package/dist/ui/compositions-panel/live-controls-diff-panel.js.map +1 -0
- package/dist/ui/compositions-panel/live-controls-diff-panel.module.scss +185 -0
- package/dist/ui/compositions-panel/live-controls-renderer.d.ts +1 -0
- package/dist/ui/compositions-panel/live-controls-renderer.js +53 -0
- package/dist/ui/compositions-panel/live-controls-renderer.js.map +1 -0
- package/dist/ui/index.d.ts +4 -0
- package/dist/ui/index.js +39 -0
- package/dist/ui/index.js.map +1 -1
- package/dist/use-default-controls-schema-responder.d.ts +1 -0
- package/dist/use-default-controls-schema-responder.js +89 -0
- package/dist/use-default-controls-schema-responder.js.map +1 -0
- package/package.json +22 -20
- package/ui/compositions-panel/compositions-panel.module.scss +33 -0
- package/ui/compositions-panel/compositions-panel.tsx +21 -74
- package/ui/compositions-panel/live-control-input.module.scss +94 -1
- package/ui/compositions-panel/live-control-input.tsx +133 -30
- package/ui/compositions-panel/live-control-panel.tsx +4 -1
- package/ui/compositions-panel/live-controls-diff-panel.module.scss +185 -0
- package/ui/compositions-panel/live-controls-diff-panel.tsx +206 -0
- package/ui/compositions-panel/live-controls-renderer.tsx +15 -0
- package/ui/index.ts +4 -0
|
@@ -10,6 +10,8 @@ import { MenuItem } from '@teambit/design.inputs.selectors.menu-item';
|
|
|
10
10
|
import { ColorPicker, ColorsBox } from '@teambit/design.ui.input.color-picker';
|
|
11
11
|
import { DatePicker } from '@teambit/design.inputs.date-picker';
|
|
12
12
|
import { Toggle } from '@teambit/design.inputs.toggle-switch';
|
|
13
|
+
import type { SelectOption } from '@teambit/compositions.ui.composition-live-controls';
|
|
14
|
+
|
|
13
15
|
import { useOverlay, BitPortal } from './use-overlay';
|
|
14
16
|
|
|
15
17
|
import styles from './live-control-input.module.scss';
|
|
@@ -24,7 +26,7 @@ type InputComponentProps = {
|
|
|
24
26
|
|
|
25
27
|
type InputComponent = React.FC<InputComponentProps>;
|
|
26
28
|
|
|
27
|
-
function ShortTextInput({ value, onChange }: InputComponentProps) {
|
|
29
|
+
function ShortTextInput({ value, onChange, id }: InputComponentProps) {
|
|
28
30
|
const [inputValue, setInputValue] = React.useState(value || '');
|
|
29
31
|
|
|
30
32
|
React.useEffect(() => {
|
|
@@ -37,10 +39,14 @@ function ShortTextInput({ value, onChange }: InputComponentProps) {
|
|
|
37
39
|
setInputValue(newValue || '');
|
|
38
40
|
};
|
|
39
41
|
|
|
40
|
-
return
|
|
42
|
+
return (
|
|
43
|
+
<div className={styles.wrapper}>
|
|
44
|
+
<InputText className={styles.inputText} id={id} value={inputValue} onChange={handleChange} />
|
|
45
|
+
</div>
|
|
46
|
+
);
|
|
41
47
|
}
|
|
42
48
|
|
|
43
|
-
function LongTextInput({ value, onChange }: InputComponentProps) {
|
|
49
|
+
function LongTextInput({ value, onChange, id }: InputComponentProps) {
|
|
44
50
|
const [inputValue, setInputValue] = React.useState(value || '');
|
|
45
51
|
|
|
46
52
|
React.useEffect(() => {
|
|
@@ -53,13 +59,18 @@ function LongTextInput({ value, onChange }: InputComponentProps) {
|
|
|
53
59
|
setInputValue(newValue || '');
|
|
54
60
|
};
|
|
55
61
|
|
|
56
|
-
return
|
|
62
|
+
return (
|
|
63
|
+
<div className={styles.wrapper}>
|
|
64
|
+
<TextArea id={id} value={inputValue} onChange={handleChange} />
|
|
65
|
+
</div>
|
|
66
|
+
);
|
|
57
67
|
}
|
|
58
68
|
|
|
59
|
-
function SelectInput({ value, onChange, meta }: InputComponentProps) {
|
|
69
|
+
export function SelectInput({ value, onChange, meta }: InputComponentProps) {
|
|
70
|
+
const triggerRef = React.useRef<HTMLDivElement>(null);
|
|
71
|
+
|
|
60
72
|
const [selectedValue, setSelectedValue] = React.useState(value || '');
|
|
61
73
|
const [open, setOpen] = React.useState(false);
|
|
62
|
-
const triggerRef = React.useRef<HTMLParagraphElement>(null);
|
|
63
74
|
|
|
64
75
|
React.useEffect(() => {
|
|
65
76
|
setSelectedValue(value || '');
|
|
@@ -84,14 +95,17 @@ function SelectInput({ value, onChange, meta }: InputComponentProps) {
|
|
|
84
95
|
};
|
|
85
96
|
|
|
86
97
|
return (
|
|
87
|
-
<
|
|
88
|
-
<
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
98
|
+
<div ref={triggerRef} className={classNames(styles.wrapper)}>
|
|
99
|
+
<div className={styles.fullWidthControl}>
|
|
100
|
+
<Dropdown
|
|
101
|
+
className={styles.dropdownField}
|
|
102
|
+
placeholderContent={placeholderContent}
|
|
103
|
+
open={open}
|
|
104
|
+
onChange={(_, isOpen) => setOpen(isOpen)}
|
|
105
|
+
position={position}
|
|
106
|
+
dropClass={overlayStyles.suppressNativeMenu}
|
|
107
|
+
/>
|
|
108
|
+
</div>
|
|
95
109
|
|
|
96
110
|
{open && style && (
|
|
97
111
|
<BitPortal>
|
|
@@ -109,11 +123,11 @@ function SelectInput({ value, onChange, meta }: InputComponentProps) {
|
|
|
109
123
|
</div>
|
|
110
124
|
</BitPortal>
|
|
111
125
|
)}
|
|
112
|
-
</
|
|
126
|
+
</div>
|
|
113
127
|
);
|
|
114
128
|
}
|
|
115
129
|
|
|
116
|
-
function NumberInput({ value, onChange }: InputComponentProps) {
|
|
130
|
+
function NumberInput({ value, onChange, id }: InputComponentProps) {
|
|
117
131
|
const [inputValue, setInputValue] = React.useState(value || 0);
|
|
118
132
|
|
|
119
133
|
React.useEffect(() => {
|
|
@@ -132,7 +146,11 @@ function NumberInput({ value, onChange }: InputComponentProps) {
|
|
|
132
146
|
}
|
|
133
147
|
};
|
|
134
148
|
|
|
135
|
-
return
|
|
149
|
+
return (
|
|
150
|
+
<div className={styles.wrapper}>
|
|
151
|
+
<InputText id={id} type="number" value={inputValue} onChange={handleChange} />
|
|
152
|
+
</div>
|
|
153
|
+
);
|
|
136
154
|
}
|
|
137
155
|
|
|
138
156
|
function ColorPickerPortal(props: any) {
|
|
@@ -185,9 +203,9 @@ function ColorInput({ value, onChange }: InputComponentProps) {
|
|
|
185
203
|
};
|
|
186
204
|
|
|
187
205
|
return (
|
|
188
|
-
<
|
|
206
|
+
<div className={styles.wrapper}>
|
|
189
207
|
<ColorPickerPortal value={inputValue} onColorSelect={handleChange} allowCustomColor />
|
|
190
|
-
</
|
|
208
|
+
</div>
|
|
191
209
|
);
|
|
192
210
|
}
|
|
193
211
|
|
|
@@ -206,9 +224,9 @@ function DateInput({ value, onChange }: InputComponentProps) {
|
|
|
206
224
|
};
|
|
207
225
|
|
|
208
226
|
return (
|
|
209
|
-
<
|
|
227
|
+
<div className={classNames(styles.wrapper)}>
|
|
210
228
|
<DatePicker date={inputValue} onChange={handleChange} />
|
|
211
|
-
</
|
|
229
|
+
</div>
|
|
212
230
|
);
|
|
213
231
|
}
|
|
214
232
|
|
|
@@ -219,19 +237,98 @@ function ToggleInput({ value, onChange }: InputComponentProps) {
|
|
|
219
237
|
setIsChecked(!!value);
|
|
220
238
|
}, [value]);
|
|
221
239
|
|
|
222
|
-
const handleChange = () => {
|
|
223
|
-
|
|
224
|
-
|
|
240
|
+
const handleChange = (event: React.ChangeEvent<HTMLInputElement>) => {
|
|
241
|
+
const nextChecked = event?.target?.checked ?? !isChecked;
|
|
242
|
+
setIsChecked(nextChecked);
|
|
243
|
+
onChange(nextChecked);
|
|
244
|
+
};
|
|
245
|
+
|
|
246
|
+
return (
|
|
247
|
+
<div className={classNames(styles.wrapper, styles.toggleWrapper)}>
|
|
248
|
+
<div className={styles.toggleControl}>
|
|
249
|
+
<Toggle checked={isChecked} onInputChanged={handleChange} />
|
|
250
|
+
</div>
|
|
251
|
+
</div>
|
|
252
|
+
);
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
function RangeInput({ value, onChange, meta, id }: InputComponentProps) {
|
|
256
|
+
const [inputValue, setInputValue] = React.useState<number>(typeof value === 'number' ? value : 0);
|
|
257
|
+
|
|
258
|
+
React.useEffect(() => {
|
|
259
|
+
setInputValue(typeof value === 'number' ? value : 0);
|
|
260
|
+
}, [value]);
|
|
261
|
+
|
|
262
|
+
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
|
263
|
+
const newValue = Number(e.target.value);
|
|
264
|
+
if (!isNaN(newValue)) {
|
|
265
|
+
onChange(newValue);
|
|
266
|
+
setInputValue(newValue);
|
|
267
|
+
}
|
|
268
|
+
};
|
|
269
|
+
|
|
270
|
+
return (
|
|
271
|
+
<div className={classNames(styles.wrapper, styles.rangeWrapper)}>
|
|
272
|
+
<input
|
|
273
|
+
id={id}
|
|
274
|
+
className={styles.rangeInput}
|
|
275
|
+
type="range"
|
|
276
|
+
value={inputValue}
|
|
277
|
+
min={meta?.min}
|
|
278
|
+
max={meta?.max}
|
|
279
|
+
step={meta?.step}
|
|
280
|
+
onChange={handleChange}
|
|
281
|
+
/>
|
|
282
|
+
<div className={styles.rangeValue}>{inputValue}</div>
|
|
283
|
+
</div>
|
|
284
|
+
);
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
function MultiSelectInput({ value, onChange, meta, id }: InputComponentProps) {
|
|
288
|
+
const [selectedValues, setSelectedValues] = React.useState<string[]>(Array.isArray(value) ? value : []);
|
|
289
|
+
|
|
290
|
+
React.useEffect(() => {
|
|
291
|
+
setSelectedValues(Array.isArray(value) ? value : []);
|
|
292
|
+
}, [value]);
|
|
293
|
+
|
|
294
|
+
const options = React.useMemo<{ label: string; value: string }[]>(() => {
|
|
295
|
+
if (!meta?.options) return [];
|
|
296
|
+
return meta.options.map((option: SelectOption) =>
|
|
297
|
+
typeof option === 'string' ? { label: option, value: option } : option
|
|
298
|
+
);
|
|
299
|
+
}, [meta]);
|
|
300
|
+
|
|
301
|
+
const handleChange = (e: React.ChangeEvent<HTMLSelectElement>) => {
|
|
302
|
+
const values = Array.from(e.target.selectedOptions).map((opt) => opt.value);
|
|
303
|
+
onChange(values);
|
|
304
|
+
setSelectedValues(values);
|
|
225
305
|
};
|
|
226
306
|
|
|
227
307
|
return (
|
|
228
|
-
<
|
|
229
|
-
<
|
|
230
|
-
|
|
308
|
+
<div className={classNames(styles.wrapper)}>
|
|
309
|
+
<select id={id} className={styles.multiSelect} multiple value={selectedValues} onChange={handleChange}>
|
|
310
|
+
{options.map((option) => (
|
|
311
|
+
<option key={option.value} value={option.value}>
|
|
312
|
+
{option.label}
|
|
313
|
+
</option>
|
|
314
|
+
))}
|
|
315
|
+
</select>
|
|
316
|
+
</div>
|
|
231
317
|
);
|
|
232
318
|
}
|
|
233
319
|
|
|
234
|
-
function
|
|
320
|
+
function CustomInput({ value, onChange, meta, id }: InputComponentProps) {
|
|
321
|
+
if (typeof meta?.render === 'function') {
|
|
322
|
+
return (
|
|
323
|
+
<div className={classNames(styles.wrapper)}>
|
|
324
|
+
{meta.render({ value, onChange, id, options: meta.renderOptions })}
|
|
325
|
+
</div>
|
|
326
|
+
);
|
|
327
|
+
}
|
|
328
|
+
return <ShortTextInput id={id} value={value} onChange={onChange} />;
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
function JsonInput({ value, onChange, id }: InputComponentProps) {
|
|
235
332
|
const [inputValue, setInputValue] = React.useState(JSON.stringify(value, null, 2));
|
|
236
333
|
|
|
237
334
|
React.useEffect(() => {
|
|
@@ -253,8 +350,8 @@ function JsonInput({ value, onChange }: InputComponentProps) {
|
|
|
253
350
|
};
|
|
254
351
|
|
|
255
352
|
return (
|
|
256
|
-
<div>
|
|
257
|
-
<TextArea value={inputValue} onChange={handleChange} />
|
|
353
|
+
<div className={styles.wrapper}>
|
|
354
|
+
<TextArea id={id} value={inputValue} onChange={handleChange} />
|
|
258
355
|
{message && <div style={{ color: 'red' }}>{message}</div>}
|
|
259
356
|
</div>
|
|
260
357
|
);
|
|
@@ -276,8 +373,14 @@ export function getInputComponent(type: string): InputComponent {
|
|
|
276
373
|
return DateInput;
|
|
277
374
|
case 'boolean':
|
|
278
375
|
return ToggleInput;
|
|
376
|
+
case 'range':
|
|
377
|
+
return RangeInput;
|
|
378
|
+
case 'multiselect':
|
|
379
|
+
return MultiSelectInput;
|
|
279
380
|
case 'json':
|
|
280
381
|
return JsonInput;
|
|
382
|
+
case 'custom':
|
|
383
|
+
return CustomInput;
|
|
281
384
|
default:
|
|
282
385
|
// eslint-disable-next-line no-console
|
|
283
386
|
console.warn(`Unknown input type: ${type}`);
|
|
@@ -9,20 +9,23 @@ export function LiveControls({
|
|
|
9
9
|
defs,
|
|
10
10
|
values,
|
|
11
11
|
onChange,
|
|
12
|
+
renderLabel,
|
|
12
13
|
}: {
|
|
13
14
|
defs: Array<Control>;
|
|
14
15
|
values: Record<string, any>;
|
|
15
16
|
onChange: (key: string, value: any) => void;
|
|
17
|
+
renderLabel?: (field: Control) => React.ReactNode;
|
|
16
18
|
}) {
|
|
17
19
|
return (
|
|
18
20
|
<ul className={classNames(styles.container)}>
|
|
19
21
|
{defs.map((field) => {
|
|
20
22
|
const key = field.id;
|
|
21
23
|
const InputComponent = getInputComponent(field.input || 'text');
|
|
24
|
+
const labelContent = renderLabel ? renderLabel(field) : field.label || field.id;
|
|
22
25
|
return (
|
|
23
26
|
<li key={key} className={classNames(styles.item)}>
|
|
24
27
|
<div className={classNames(styles.label)}>
|
|
25
|
-
<label htmlFor={`control-${key}`}>{
|
|
28
|
+
<label htmlFor={`control-${key}`}>{labelContent}</label>
|
|
26
29
|
</div>
|
|
27
30
|
<div>
|
|
28
31
|
<InputComponent
|
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
.container {
|
|
2
|
+
display: flex;
|
|
3
|
+
flex-direction: column;
|
|
4
|
+
height: 100%;
|
|
5
|
+
min-height: 0;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
.columnsLayout {
|
|
9
|
+
display: flex;
|
|
10
|
+
gap: 0;
|
|
11
|
+
height: 100%;
|
|
12
|
+
min-height: 0;
|
|
13
|
+
overflow: auto;
|
|
14
|
+
align-items: stretch;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
.column {
|
|
18
|
+
flex: 1 1 0;
|
|
19
|
+
min-width: 320px;
|
|
20
|
+
padding: 0 16px;
|
|
21
|
+
overflow: visible;
|
|
22
|
+
|
|
23
|
+
&:first-child {
|
|
24
|
+
padding-left: 0;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
&:last-child {
|
|
28
|
+
padding-right: 0;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
.columnHeader {
|
|
33
|
+
font-size: var(--bit-p-xxs, 12px);
|
|
34
|
+
font-weight: 600;
|
|
35
|
+
text-transform: uppercase;
|
|
36
|
+
letter-spacing: 0.5px;
|
|
37
|
+
line-height: 1.3;
|
|
38
|
+
color: var(--on-background-color, #222222);
|
|
39
|
+
opacity: 0.5;
|
|
40
|
+
margin-bottom: 12px;
|
|
41
|
+
padding-bottom: 8px;
|
|
42
|
+
border-bottom: 1px solid var(--border-color, rgba(0, 0, 0, 0.08));
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
.columnDivider {
|
|
46
|
+
width: 1px;
|
|
47
|
+
background: var(--border-medium-color, rgba(0, 0, 0, 0.14));
|
|
48
|
+
flex-shrink: 0;
|
|
49
|
+
margin: 0 8px;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
.controlsList {
|
|
53
|
+
list-style: none;
|
|
54
|
+
margin: 0;
|
|
55
|
+
padding: 0;
|
|
56
|
+
display: grid;
|
|
57
|
+
grid-template-columns: repeat(2, minmax(0, 1fr));
|
|
58
|
+
gap: 14px 12px;
|
|
59
|
+
align-items: start;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
.controlRow {
|
|
63
|
+
display: block;
|
|
64
|
+
min-width: 0;
|
|
65
|
+
margin: 0;
|
|
66
|
+
padding: 0;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
.controlRowWide {
|
|
70
|
+
grid-column: 1 / -1;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
.controlMain {
|
|
74
|
+
display: grid;
|
|
75
|
+
grid-template-rows: auto minmax(36px, auto);
|
|
76
|
+
align-content: start;
|
|
77
|
+
row-gap: 8px;
|
|
78
|
+
flex: 1;
|
|
79
|
+
min-width: 0;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
.controlInput {
|
|
83
|
+
min-width: 0;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
.controlLabel {
|
|
87
|
+
display: flex;
|
|
88
|
+
align-items: center;
|
|
89
|
+
gap: 6px;
|
|
90
|
+
margin-bottom: 0;
|
|
91
|
+
min-height: 20px;
|
|
92
|
+
|
|
93
|
+
label {
|
|
94
|
+
font-size: var(--bit-p-xs, 13px);
|
|
95
|
+
font-weight: 500;
|
|
96
|
+
color: var(--on-background-color, #222222);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
.sourceTag {
|
|
101
|
+
font-size: 10px;
|
|
102
|
+
line-height: 14px;
|
|
103
|
+
padding: 0 5px;
|
|
104
|
+
border-radius: 999px;
|
|
105
|
+
text-transform: capitalize;
|
|
106
|
+
font-weight: 500;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
.tagCommon {
|
|
110
|
+
background: rgba(106, 87, 253, 0.12);
|
|
111
|
+
color: var(--primary-color, #6a57fd);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
.tagBase {
|
|
115
|
+
background: rgba(255, 152, 0, 0.12);
|
|
116
|
+
color: #e65100;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
.tagCompare {
|
|
120
|
+
background: rgba(33, 150, 243, 0.12);
|
|
121
|
+
color: #1565c0;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
.loader {
|
|
125
|
+
display: flex;
|
|
126
|
+
align-items: center;
|
|
127
|
+
gap: 12px;
|
|
128
|
+
padding: 10px 0;
|
|
129
|
+
color: var(--skeleton-color, #e0e0e0);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
@media (max-width: 1200px) {
|
|
133
|
+
.column {
|
|
134
|
+
min-width: 280px;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
.controlsList {
|
|
138
|
+
grid-template-columns: 1fr;
|
|
139
|
+
gap: 12px;
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
.emptyState {
|
|
144
|
+
display: flex;
|
|
145
|
+
align-items: center;
|
|
146
|
+
gap: 12px;
|
|
147
|
+
padding: 12px;
|
|
148
|
+
border: 1px dashed var(--border-medium-color, rgba(0, 0, 0, 0.12));
|
|
149
|
+
border-radius: 8px;
|
|
150
|
+
background: var(--background-color, #ffffff);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
.emptyStateIconWrap {
|
|
154
|
+
width: 32px;
|
|
155
|
+
height: 32px;
|
|
156
|
+
border-radius: 8px;
|
|
157
|
+
background: rgba(0, 0, 0, 0.04);
|
|
158
|
+
display: flex;
|
|
159
|
+
align-items: center;
|
|
160
|
+
justify-content: center;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
.emptyStateIcon {
|
|
164
|
+
width: 18px;
|
|
165
|
+
height: 18px;
|
|
166
|
+
opacity: 0.7;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
.emptyStateText {
|
|
170
|
+
display: flex;
|
|
171
|
+
flex-direction: column;
|
|
172
|
+
gap: 2px;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
.emptyStateTitle {
|
|
176
|
+
font-size: var(--bit-p-xs, 13px);
|
|
177
|
+
font-weight: 600;
|
|
178
|
+
color: var(--on-background-color, #222222);
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
.emptyStateSubtitle {
|
|
182
|
+
font-size: var(--bit-p-xxs, 12px);
|
|
183
|
+
color: var(--on-background-color, #222222);
|
|
184
|
+
opacity: 0.7;
|
|
185
|
+
}
|
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
|
2
|
+
import {
|
|
3
|
+
DiffControlsModel,
|
|
4
|
+
useLiveControls,
|
|
5
|
+
type ControlWithSource,
|
|
6
|
+
} from '@teambit/compositions.ui.composition-live-controls';
|
|
7
|
+
import { LineSkeleton } from '@teambit/base-ui.loaders.skeleton';
|
|
8
|
+
import { Icon } from '@teambit/evangelist.elements.icon';
|
|
9
|
+
import classNames from 'classnames';
|
|
10
|
+
import { getInputComponent } from './live-control-input';
|
|
11
|
+
|
|
12
|
+
import styles from './live-controls-diff-panel.module.scss';
|
|
13
|
+
|
|
14
|
+
type PanelStatus = 'loading' | 'available' | 'empty';
|
|
15
|
+
const WAIT_FOR_CONTROLS_MS = 1200;
|
|
16
|
+
|
|
17
|
+
export type LiveControlsDiffPanelProps = {
|
|
18
|
+
resetKey?: string;
|
|
19
|
+
baseChannel?: string;
|
|
20
|
+
compareChannel?: string;
|
|
21
|
+
commonLabel?: string;
|
|
22
|
+
baseLabel?: string;
|
|
23
|
+
compareLabel?: string;
|
|
24
|
+
showEmptyState?: boolean;
|
|
25
|
+
onStatusChange?: (status: PanelStatus) => void;
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
export function LiveControlsDiffPanel({
|
|
29
|
+
resetKey,
|
|
30
|
+
baseChannel,
|
|
31
|
+
compareChannel,
|
|
32
|
+
commonLabel = 'Common',
|
|
33
|
+
baseLabel = 'Base',
|
|
34
|
+
compareLabel = 'Compare',
|
|
35
|
+
showEmptyState = true,
|
|
36
|
+
onStatusChange,
|
|
37
|
+
}: LiveControlsDiffPanelProps) {
|
|
38
|
+
const lastResetKeyRef = useRef<string | null>(null);
|
|
39
|
+
const [isWaitingForFreshData, setIsWaitingForFreshData] = useState(true);
|
|
40
|
+
const waitTimeoutRef = useRef<number | null>(null);
|
|
41
|
+
const currentKey = `${baseChannel || ''}-${compareChannel || ''}-${resetKey || ''}`;
|
|
42
|
+
|
|
43
|
+
const model = useMemo(() => new DiffControlsModel(baseChannel, compareChannel), [baseChannel, compareChannel]);
|
|
44
|
+
|
|
45
|
+
const allChannels = useMemo(() => {
|
|
46
|
+
const channels = [model.baseChannel, model.compareChannel];
|
|
47
|
+
if (!channels.includes('default')) channels.push('default');
|
|
48
|
+
return [...new Set(channels)];
|
|
49
|
+
}, [model.baseChannel, model.compareChannel]);
|
|
50
|
+
|
|
51
|
+
const combined = useLiveControls(allChannels);
|
|
52
|
+
const { ready: combinedReady, setTimestamp } = combined;
|
|
53
|
+
|
|
54
|
+
useEffect(() => {
|
|
55
|
+
if (lastResetKeyRef.current !== currentKey) {
|
|
56
|
+
lastResetKeyRef.current = currentKey;
|
|
57
|
+
setIsWaitingForFreshData(true);
|
|
58
|
+
setTimestamp(0);
|
|
59
|
+
}
|
|
60
|
+
}, [currentKey, setTimestamp]);
|
|
61
|
+
|
|
62
|
+
const channelsReady = Boolean(baseChannel && compareChannel);
|
|
63
|
+
const registryReady = combinedReady || model.isReady;
|
|
64
|
+
const controls = model.controls;
|
|
65
|
+
const hasControls = controls.length > 0;
|
|
66
|
+
const hasSubscribers = model.hasSubscribers;
|
|
67
|
+
|
|
68
|
+
const prevRegistryReady = useRef(registryReady);
|
|
69
|
+
useEffect(() => {
|
|
70
|
+
if (registryReady && !prevRegistryReady.current && isWaitingForFreshData) {
|
|
71
|
+
setIsWaitingForFreshData(false);
|
|
72
|
+
}
|
|
73
|
+
prevRegistryReady.current = registryReady;
|
|
74
|
+
}, [registryReady, isWaitingForFreshData]);
|
|
75
|
+
|
|
76
|
+
useEffect(() => {
|
|
77
|
+
if (!channelsReady || registryReady || !isWaitingForFreshData) {
|
|
78
|
+
if (waitTimeoutRef.current !== null) {
|
|
79
|
+
window.clearTimeout(waitTimeoutRef.current);
|
|
80
|
+
waitTimeoutRef.current = null;
|
|
81
|
+
}
|
|
82
|
+
return;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
waitTimeoutRef.current = window.setTimeout(() => {
|
|
86
|
+
setIsWaitingForFreshData(false);
|
|
87
|
+
waitTimeoutRef.current = null;
|
|
88
|
+
}, WAIT_FOR_CONTROLS_MS);
|
|
89
|
+
|
|
90
|
+
return () => {
|
|
91
|
+
if (waitTimeoutRef.current !== null) {
|
|
92
|
+
window.clearTimeout(waitTimeoutRef.current);
|
|
93
|
+
waitTimeoutRef.current = null;
|
|
94
|
+
}
|
|
95
|
+
};
|
|
96
|
+
}, [channelsReady, registryReady, isWaitingForFreshData, currentKey]);
|
|
97
|
+
|
|
98
|
+
const status: PanelStatus = useMemo(() => {
|
|
99
|
+
if (!channelsReady) return 'empty';
|
|
100
|
+
if (isWaitingForFreshData) return 'loading';
|
|
101
|
+
if (!registryReady && !hasSubscribers) return 'empty';
|
|
102
|
+
return hasControls ? 'available' : 'empty';
|
|
103
|
+
}, [channelsReady, isWaitingForFreshData, registryReady, hasSubscribers, hasControls]);
|
|
104
|
+
|
|
105
|
+
useEffect(() => {
|
|
106
|
+
onStatusChange?.(status);
|
|
107
|
+
}, [status, onStatusChange]);
|
|
108
|
+
|
|
109
|
+
const handleChange = useCallback(
|
|
110
|
+
(control: ControlWithSource, value: any) => {
|
|
111
|
+
model.updateControl(control.id, value, control.source);
|
|
112
|
+
},
|
|
113
|
+
[model]
|
|
114
|
+
);
|
|
115
|
+
|
|
116
|
+
if (status === 'loading') {
|
|
117
|
+
return (
|
|
118
|
+
<div className={styles.loader}>
|
|
119
|
+
<LineSkeleton width="68px" />
|
|
120
|
+
<LineSkeleton width="52px" />
|
|
121
|
+
<LineSkeleton width="72px" />
|
|
122
|
+
</div>
|
|
123
|
+
);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
if (status === 'empty') {
|
|
127
|
+
if (!showEmptyState) return null;
|
|
128
|
+
return (
|
|
129
|
+
<div className={styles.emptyState}>
|
|
130
|
+
<div className={styles.emptyStateIconWrap}>
|
|
131
|
+
<Icon of="scan-component" className={styles.emptyStateIcon} aria-hidden />
|
|
132
|
+
</div>
|
|
133
|
+
<div className={styles.emptyStateText}>
|
|
134
|
+
<div className={styles.emptyStateTitle}>No live controls</div>
|
|
135
|
+
<div className={styles.emptyStateSubtitle}>This composition does not expose live controls.</div>
|
|
136
|
+
</div>
|
|
137
|
+
</div>
|
|
138
|
+
);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
const commonControls = controls.filter((c) => c.source === 'common');
|
|
142
|
+
const baseControls = controls.filter((c) => c.source === 'base');
|
|
143
|
+
const compareControls = controls.filter((c) => c.source === 'compare');
|
|
144
|
+
const hasBaseOrCompare = baseControls.length > 0 || compareControls.length > 0;
|
|
145
|
+
const hasBaseAndCompare = baseControls.length > 0 && compareControls.length > 0;
|
|
146
|
+
|
|
147
|
+
const getControlValue = (control: ControlWithSource) => {
|
|
148
|
+
return model.getValueForControl(control.id, control.source);
|
|
149
|
+
};
|
|
150
|
+
|
|
151
|
+
const renderControlList = (list: ControlWithSource[]) => (
|
|
152
|
+
<ul className={styles.controlsList}>
|
|
153
|
+
{list.map((control) => {
|
|
154
|
+
const inputType = control.input || 'text';
|
|
155
|
+
const isWideControl = inputType === 'longtext' || inputType === 'multiselect' || inputType === 'json';
|
|
156
|
+
const InputComponent = getInputComponent(inputType);
|
|
157
|
+
const key = `${control.id}-${control.source}`;
|
|
158
|
+
const value = getControlValue(control);
|
|
159
|
+
return (
|
|
160
|
+
<li key={key} className={classNames(styles.controlRow, isWideControl && styles.controlRowWide)}>
|
|
161
|
+
<div className={styles.controlMain}>
|
|
162
|
+
<div className={styles.controlLabel}>
|
|
163
|
+
<label htmlFor={`control-${key}`}>{control.label || control.id}</label>
|
|
164
|
+
</div>
|
|
165
|
+
<div className={styles.controlInput}>
|
|
166
|
+
<InputComponent
|
|
167
|
+
id={`control-${key}`}
|
|
168
|
+
value={value}
|
|
169
|
+
onChange={(val: any) => handleChange(control, val)}
|
|
170
|
+
meta={control}
|
|
171
|
+
/>
|
|
172
|
+
</div>
|
|
173
|
+
</div>
|
|
174
|
+
</li>
|
|
175
|
+
);
|
|
176
|
+
})}
|
|
177
|
+
</ul>
|
|
178
|
+
);
|
|
179
|
+
|
|
180
|
+
return (
|
|
181
|
+
<div className={styles.container}>
|
|
182
|
+
<div className={styles.columnsLayout}>
|
|
183
|
+
{commonControls.length > 0 && (
|
|
184
|
+
<div className={styles.column}>
|
|
185
|
+
<div className={styles.columnHeader}>{commonLabel}</div>
|
|
186
|
+
{renderControlList(commonControls)}
|
|
187
|
+
</div>
|
|
188
|
+
)}
|
|
189
|
+
{commonControls.length > 0 && hasBaseOrCompare && <div className={styles.columnDivider} />}
|
|
190
|
+
{baseControls.length > 0 && (
|
|
191
|
+
<div className={styles.column}>
|
|
192
|
+
<div className={styles.columnHeader}>{baseLabel}</div>
|
|
193
|
+
{renderControlList(baseControls)}
|
|
194
|
+
</div>
|
|
195
|
+
)}
|
|
196
|
+
{hasBaseAndCompare && <div className={styles.columnDivider} />}
|
|
197
|
+
{compareControls.length > 0 && (
|
|
198
|
+
<div className={styles.column}>
|
|
199
|
+
<div className={styles.columnHeader}>{compareLabel}</div>
|
|
200
|
+
{renderControlList(compareControls)}
|
|
201
|
+
</div>
|
|
202
|
+
)}
|
|
203
|
+
</div>
|
|
204
|
+
</div>
|
|
205
|
+
);
|
|
206
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { LiveControls } from './live-control-panel';
|
|
3
|
+
import { useLiveControls } from '@teambit/compositions.ui.composition-live-controls';
|
|
4
|
+
|
|
5
|
+
export function LiveControlsRenderer() {
|
|
6
|
+
const { hasLiveControls, ready, defs, values, onChange } = useLiveControls();
|
|
7
|
+
|
|
8
|
+
if (!hasLiveControls) return null;
|
|
9
|
+
|
|
10
|
+
if (!ready) {
|
|
11
|
+
return <div style={{ padding: 12, opacity: 0.7 }}>No live controls available.</div>;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
return <LiveControls defs={defs} values={values} onChange={onChange} />;
|
|
15
|
+
}
|
package/ui/index.ts
CHANGED
|
@@ -1 +1,5 @@
|
|
|
1
1
|
export { ComponentComposition } from './composition-preview';
|
|
2
|
+
export { LiveControls } from './compositions-panel/live-control-panel';
|
|
3
|
+
export { LiveControlsDiffPanel } from './compositions-panel/live-controls-diff-panel';
|
|
4
|
+
export type { LiveControlsDiffPanelProps } from './compositions-panel/live-controls-diff-panel';
|
|
5
|
+
export { LiveControlsRenderer } from './compositions-panel/live-controls-renderer';
|