@teambit/compositions 1.0.893 → 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.
Files changed (44) hide show
  1. package/composition.section.tsx +1 -0
  2. package/compositions.tsx +30 -5
  3. package/dist/composition.section.js +2 -1
  4. package/dist/composition.section.js.map +1 -1
  5. package/dist/compositions.d.ts +2 -1
  6. package/dist/compositions.js +36 -2
  7. package/dist/compositions.js.map +1 -1
  8. package/dist/index.d.ts +2 -1
  9. package/dist/index.js +31 -0
  10. package/dist/index.js.map +1 -1
  11. package/dist/{preview-1772220644928.js → preview-1772488540837.js} +2 -2
  12. package/dist/ui/compositions-panel/compositions-panel.js +20 -66
  13. package/dist/ui/compositions-panel/compositions-panel.js.map +1 -1
  14. package/dist/ui/compositions-panel/compositions-panel.module.scss +33 -0
  15. package/dist/ui/compositions-panel/live-control-input.d.ts +1 -0
  16. package/dist/ui/compositions-panel/live-control-input.js +142 -23
  17. package/dist/ui/compositions-panel/live-control-input.js.map +1 -1
  18. package/dist/ui/compositions-panel/live-control-input.module.scss +94 -1
  19. package/dist/ui/compositions-panel/live-control-panel.d.ts +3 -1
  20. package/dist/ui/compositions-panel/live-control-panel.js +4 -2
  21. package/dist/ui/compositions-panel/live-control-panel.js.map +1 -1
  22. package/dist/ui/compositions-panel/live-controls-diff-panel.d.ts +13 -0
  23. package/dist/ui/compositions-panel/live-controls-diff-panel.js +220 -0
  24. package/dist/ui/compositions-panel/live-controls-diff-panel.js.map +1 -0
  25. package/dist/ui/compositions-panel/live-controls-diff-panel.module.scss +185 -0
  26. package/dist/ui/compositions-panel/live-controls-renderer.d.ts +1 -0
  27. package/dist/ui/compositions-panel/live-controls-renderer.js +53 -0
  28. package/dist/ui/compositions-panel/live-controls-renderer.js.map +1 -0
  29. package/dist/ui/index.d.ts +4 -0
  30. package/dist/ui/index.js +39 -0
  31. package/dist/ui/index.js.map +1 -1
  32. package/dist/use-default-controls-schema-responder.d.ts +1 -0
  33. package/dist/use-default-controls-schema-responder.js +89 -0
  34. package/dist/use-default-controls-schema-responder.js.map +1 -0
  35. package/package.json +22 -20
  36. package/ui/compositions-panel/compositions-panel.module.scss +33 -0
  37. package/ui/compositions-panel/compositions-panel.tsx +21 -74
  38. package/ui/compositions-panel/live-control-input.module.scss +94 -1
  39. package/ui/compositions-panel/live-control-input.tsx +133 -30
  40. package/ui/compositions-panel/live-control-panel.tsx +4 -1
  41. package/ui/compositions-panel/live-controls-diff-panel.module.scss +185 -0
  42. package/ui/compositions-panel/live-controls-diff-panel.tsx +206 -0
  43. package/ui/compositions-panel/live-controls-renderer.tsx +15 -0
  44. 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 <InputText value={inputValue} onChange={handleChange} />;
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 <TextArea value={inputValue} onChange={handleChange} />;
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
- <p ref={triggerRef} className={classNames(styles.wrapper)}>
88
- <Dropdown
89
- placeholderContent={placeholderContent}
90
- open={open}
91
- onChange={(_, isOpen) => setOpen(isOpen)}
92
- position={position}
93
- dropClass={overlayStyles.suppressNativeMenu}
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
- </p>
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 <InputText type="number" value={inputValue} onChange={handleChange} />;
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
- <p className={styles.wrapper}>
206
+ <div className={styles.wrapper}>
189
207
  <ColorPickerPortal value={inputValue} onColorSelect={handleChange} allowCustomColor />
190
- </p>
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
- <p className={classNames(styles.wrapper)}>
227
+ <div className={classNames(styles.wrapper)}>
210
228
  <DatePicker date={inputValue} onChange={handleChange} />
211
- </p>
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
- setIsChecked(!isChecked);
224
- onChange(!isChecked);
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
- <p className={classNames(styles.wrapper)}>
229
- <Toggle defaultChecked={isChecked} onChange={handleChange} />
230
- </p>
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 JsonInput({ value, onChange }: InputComponentProps) {
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}`}>{field.label || field.id}</label>
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';