@teambit/compositions 1.0.994 → 1.0.996

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.
@@ -6,6 +6,7 @@ import type {
6
6
  CompositionsMenuSlot,
7
7
  EmptyStateSlot,
8
8
  UsePreviewSandboxSlot,
9
+ UsePreviewPropsSlot,
9
10
  } from './compositions.ui.runtime';
10
11
 
11
12
  type Options = { menuBarWidgetSlot: CompositionsMenuSlot };
@@ -18,7 +19,8 @@ export class CompositionsSection implements Section {
18
19
  private compositions: CompositionsUI,
19
20
  private options: Options,
20
21
  private emptyStateSlot: EmptyStateSlot,
21
- private usePreviewSandboxSlot: UsePreviewSandboxSlot
22
+ private usePreviewSandboxSlot: UsePreviewSandboxSlot,
23
+ private usePreviewPropsSlot: UsePreviewPropsSlot
22
24
  ) {}
23
25
 
24
26
  navigationLink = {
@@ -33,6 +35,7 @@ export class CompositionsSection implements Section {
33
35
  menuBarWidgets={this.options.menuBarWidgetSlot}
34
36
  emptyState={this.emptyStateSlot}
35
37
  usePreviewSandboxSlot={this.usePreviewSandboxSlot}
38
+ usePreviewPropsSlot={this.usePreviewPropsSlot}
36
39
  enableLiveControls
37
40
  />
38
41
  ),
@@ -146,20 +146,37 @@
146
146
  border-top: 1px solid var(--bit-border-color-lightest, #eaeaec);
147
147
  background: var(--bit-bg-color, #ffffff);
148
148
  overflow: hidden;
149
+ min-height: 0;
150
+ // Never overflow the previewArea: if the inline `height` (or a user-set
151
+ // resize) ends up taller than the parent allows (e.g. browser zoom shrinks
152
+ // available space, or there are many controls), `max-height` wins and the
153
+ // tray stays inside its container so `trayBody`'s `overflow-y: auto` keeps
154
+ // working instead of the bottom rows getting clipped off-screen.
155
+ max-height: calc(100% - 80px);
156
+ box-sizing: border-box;
149
157
  }
150
158
 
151
- .trayDragHandle {
159
+ .controlsTrayCollapsed {
160
+ // when collapsed, only show the header bar
161
+ height: auto;
162
+ }
163
+
164
+ // ─── Two-zone header: resize strip on top, click-to-collapse below ─────
165
+ // The two zones are visually separated by a divider so the boundary is
166
+ // obvious: top = "grab to resize", bottom = "click to collapse".
167
+
168
+ .trayResizeStrip {
169
+ position: relative;
152
170
  flex-shrink: 0;
153
- height: 8px;
154
- display: flex;
155
- align-items: center;
156
- justify-content: center;
171
+ height: 12px;
157
172
  cursor: ns-resize;
158
- background: transparent;
159
- transition: background 120ms ease;
173
+ user-select: none;
174
+ background: var(--surface01-color, rgba(0, 0, 0, 0.02));
175
+ border-bottom: 1px solid var(--bit-border-color-lightest, #eaeaec);
176
+ transition: background 140ms ease;
160
177
 
161
178
  &:hover {
162
- background: rgba(0, 0, 0, 0.03);
179
+ background: var(--surface-hover-color, rgba(0, 0, 0, 0.05));
163
180
  }
164
181
 
165
182
  &:hover .trayDragBar {
@@ -169,19 +186,36 @@
169
186
  }
170
187
 
171
188
  .trayDragBar {
189
+ position: absolute;
190
+ top: 50%;
191
+ left: 50%;
192
+ transform: translate(-50%, -50%);
172
193
  width: 32px;
173
194
  height: 3px;
174
195
  border-radius: 2px;
175
196
  background: var(--bit-border-color-lightest, #d0d0d3);
176
197
  transition: all 140ms ease;
198
+ pointer-events: none;
177
199
  }
178
200
 
179
- .trayHeader {
180
- flex-shrink: 0;
201
+ .trayHeaderInner {
181
202
  display: flex;
182
203
  align-items: center;
183
204
  justify-content: space-between;
184
- padding: 4px 14px 8px;
205
+ padding: 8px 14px;
206
+ cursor: pointer;
207
+ user-select: none;
208
+ outline: none;
209
+ transition: background 140ms ease;
210
+
211
+ &:hover {
212
+ background: var(--surface-hover-color, rgba(0, 0, 0, 0.03));
213
+ }
214
+ }
215
+
216
+ // give the collapsed bar a bit more breathing room
217
+ .controlsTrayCollapsed .trayHeaderInner {
218
+ padding: 10px 14px;
185
219
  }
186
220
 
187
221
  .trayTitleRow {
@@ -218,29 +252,31 @@
218
252
  line-height: 1;
219
253
  }
220
254
 
221
- .trayClose {
222
- display: flex;
255
+ .trayCollapseIcon {
256
+ display: inline-flex;
223
257
  align-items: center;
224
258
  justify-content: center;
225
- width: 24px;
226
- height: 24px;
227
- border-radius: 4px;
228
- border: none;
229
- background: transparent;
259
+ width: 20px;
260
+ height: 20px;
261
+ font-size: 10px;
230
262
  color: var(--bit-text-color-light, #8b8d98);
231
- cursor: pointer;
232
- font-size: 12px;
233
- padding: 0;
234
- transition: all 120ms ease;
263
+ transform: rotate(90deg);
264
+ transform-origin: center;
265
+ transition: transform 200ms ease-in-out;
266
+ pointer-events: none;
267
+ }
235
268
 
236
- &:hover {
237
- background: rgba(0, 0, 0, 0.06);
238
- color: var(--bit-text-color-heavy, #1c2024);
239
- }
269
+ .trayCollapseIconCollapsed {
270
+ transform: rotate(-90deg);
240
271
  }
241
272
 
242
273
  .trayBody {
243
- flex: 1;
274
+ // `flex-basis: auto` so the body contributes its natural content height to
275
+ // the tray's intrinsic size — without this, an auto-sized tray collapses to
276
+ // just the header. `min-height: 0` keeps `overflow-y: auto` working when
277
+ // the parent's `max-height` clamps the tray.
278
+ flex: 1 1 auto;
279
+ min-height: 0;
244
280
  overflow-y: auto;
245
281
  overflow-x: hidden;
246
282
  }
package/compositions.tsx CHANGED
@@ -1,6 +1,7 @@
1
1
  import type { ReactNode } from 'react';
2
2
  import React, { useContext, useEffect, useState, useMemo, useRef, useCallback } from 'react';
3
3
  import { useParams, useSearchParams } from 'react-router-dom';
4
+ import classNames from 'classnames';
4
5
  import head from 'lodash.head';
5
6
  import queryString from 'query-string';
6
7
  import { ThemeContext } from '@teambit/documenter.theme.theme-context';
@@ -13,7 +14,11 @@ import { Tab, TabContainer, TabList, TabPanel } from '@teambit/panels';
13
14
  import { useDocs } from '@teambit/docs.ui.queries.get-docs';
14
15
  import { Collapser } from '@teambit/ui-foundation.ui.buttons.collapser';
15
16
  import { EmptyBox } from '@teambit/design.ui.empty-box';
16
- import { SandboxPermissionsAggregator, toPreviewUrl } from '@teambit/preview.ui.component-preview';
17
+ import {
18
+ PreviewPropsAggregator,
19
+ SandboxPermissionsAggregator,
20
+ toPreviewUrl,
21
+ } from '@teambit/preview.ui.component-preview';
17
22
  import { useIsMobile } from '@teambit/ui-foundation.ui.hooks.use-is-mobile';
18
23
  import { CompositionsMenuBar } from '@teambit/compositions.ui.compositions-menu-bar';
19
24
  import { CompositionContextProvider } from '@teambit/compositions.ui.hooks.use-composition';
@@ -26,8 +31,14 @@ import { OptionButton } from '@teambit/design.ui.input.option-button';
26
31
  import { StatusMessageCard } from '@teambit/design.ui.surfaces.status-message-card';
27
32
  import { Tooltip } from '@teambit/design.ui.tooltip';
28
33
  import { Icon } from '@teambit/evangelist.elements.icon';
34
+ import type { UseLiveControlsResult } from '@teambit/compositions.ui.composition-live-controls';
29
35
  import { useLiveControls } from '@teambit/compositions.ui.composition-live-controls';
30
- import type { EmptyStateSlot, CompositionsMenuSlot, UsePreviewSandboxSlot } from './compositions.ui.runtime';
36
+ import type {
37
+ EmptyStateSlot,
38
+ CompositionsMenuSlot,
39
+ UsePreviewSandboxSlot,
40
+ UsePreviewPropsSlot,
41
+ } from './compositions.ui.runtime';
31
42
  import type { Composition } from './composition';
32
43
  import styles from './compositions.module.scss';
33
44
  import { ComponentComposition } from './ui';
@@ -47,6 +58,13 @@ export type CompositionsProp = {
47
58
  menuBarWidgets?: CompositionsMenuSlot;
48
59
  emptyState?: EmptyStateSlot;
49
60
  usePreviewSandboxSlot?: UsePreviewSandboxSlot;
61
+ /**
62
+ * per-component resolvers for iframe attributes on the composition preview (`allow`,
63
+ * `referrerPolicy`, ...). Each resolver gets the current `ComponentModel`; results merge
64
+ * with later registrations winning. Default `allow` (`clipboard-write`) lives on
65
+ * `ComponentPreview` and applies when no resolver overrides it.
66
+ */
67
+ usePreviewPropsSlot?: UsePreviewPropsSlot;
50
68
  enableLiveControls?: boolean;
51
69
  };
52
70
 
@@ -54,6 +72,7 @@ export function Compositions({
54
72
  menuBarWidgets,
55
73
  emptyState,
56
74
  usePreviewSandboxSlot,
75
+ usePreviewPropsSlot,
57
76
  enableLiveControls = true,
58
77
  }: CompositionsProp) {
59
78
  const component = useContext(ComponentContext);
@@ -74,6 +93,7 @@ export function Compositions({
74
93
 
75
94
  const properties = useDocs(component.id);
76
95
  const previewSandboxHooks = usePreviewSandboxSlot?.values() ?? [];
96
+ const previewPropsHooks = usePreviewPropsSlot?.values() ?? [];
77
97
  const isMobile = useIsMobile();
78
98
  const showSidebar = !isMobile && component.compositions.length > 0;
79
99
  const [isSidebarOpen, setSidebarOpenness] = useState(showSidebar);
@@ -97,28 +117,50 @@ export function Compositions({
97
117
 
98
118
  const queryParams = useMemo(() => queryString.stringify(compositionParams), [compositionParams]);
99
119
 
100
- const [controlsTrayOpen, setControlsTrayOpen] = useState(false);
120
+ // Tracks compositions the user has explicitly collapsed. Anything not in
121
+ // this set defaults to expanded, which gives us "auto-open on load" as a
122
+ // pure derivation rather than an effect.
123
+ const [collapsedCompositions, setCollapsedCompositions] = useState<Set<string>>(() => new Set());
101
124
  const [isDraggingTray, setIsDraggingTray] = useState(false);
102
- const trayRef = useRef<HTMLDivElement>(null);
103
- const trayHeightRef = useRef(260);
125
+ const trayRef = useRef<HTMLDivElement | null>(null);
126
+ // `null` = auto-size to content (default). Becomes a number once the user
127
+ // drag-resizes — only then do we pin a fixed height.
128
+ const [trayHeight, setTrayHeight] = useState<number | null>(null);
104
129
  const { ready, defs, values, onChange } = useLiveControls();
105
130
 
106
- const onTrayDragStart = useCallback((e: React.MouseEvent) => {
131
+ const onResizeStripMouseDown = useCallback((e: React.MouseEvent) => {
132
+ if (e.button !== 0) return;
107
133
  e.preventDefault();
108
134
  const startY = e.clientY;
109
- const startHeight = trayHeightRef.current;
135
+ // Seed the drag from the tray's current rendered height — handles the
136
+ // initial auto-sized case (where `trayHeight` is still null).
137
+ const startHeight = trayRef.current?.offsetHeight ?? 0;
138
+ const parentEl = trayRef.current?.parentElement;
139
+ const parentHeight = parentEl?.clientHeight ?? window.innerHeight;
140
+ const maxHeight = Math.max(120, parentHeight - 80);
110
141
  setIsDraggingTray(true);
111
142
  document.body.style.cursor = 'ns-resize';
112
143
  document.body.style.userSelect = 'none';
144
+ // Mutate the tray's inline height directly and rAF-throttle, so dragging
145
+ // doesn't re-render the whole compositions tree (including the iframe)
146
+ // on every mousemove. State is committed once on mouseup.
147
+ let pendingHeight = startHeight;
148
+ let rafScheduled = false;
149
+ const applyHeight = () => {
150
+ rafScheduled = false;
151
+ if (trayRef.current) trayRef.current.style.height = `${pendingHeight}px`;
152
+ };
113
153
  const onMove = (ev: MouseEvent) => {
114
- const next = Math.max(120, Math.min(600, startHeight + (startY - ev.clientY)));
115
- trayHeightRef.current = next;
116
- if (trayRef.current) {
117
- trayRef.current.style.maxHeight = `${next}px`;
154
+ const delta = startY - ev.clientY;
155
+ pendingHeight = Math.max(120, Math.min(maxHeight, startHeight + delta));
156
+ if (!rafScheduled) {
157
+ rafScheduled = true;
158
+ requestAnimationFrame(applyHeight);
118
159
  }
119
160
  };
120
161
  const onUp = () => {
121
162
  setIsDraggingTray(false);
163
+ setTrayHeight(pendingHeight);
122
164
  document.body.style.cursor = '';
123
165
  document.body.style.userSelect = '';
124
166
  document.removeEventListener('mousemove', onMove);
@@ -148,7 +190,21 @@ export function Compositions({
148
190
  }, [enableLiveControls]);
149
191
 
150
192
  const currentCompositionHasControls = ready && defs.length > 0;
151
- const showControlsTray = currentCompositionHasControls && controlsTrayOpen;
193
+ const currentCompositionIdentifier = currentComposition?.identifier;
194
+ const isTrayCollapsedForCurrent =
195
+ !!currentCompositionIdentifier && collapsedCompositions.has(currentCompositionIdentifier);
196
+ const showControlsTray = currentCompositionHasControls;
197
+ const isTrayCollapsed = showControlsTray && isTrayCollapsedForCurrent;
198
+
199
+ const toggleTrayCollapsed = useCallback(() => {
200
+ if (!currentCompositionIdentifier) return;
201
+ setCollapsedCompositions((prev) => {
202
+ const next = new Set(prev);
203
+ if (next.has(currentCompositionIdentifier)) next.delete(currentCompositionIdentifier);
204
+ else next.add(currentCompositionIdentifier);
205
+ return next;
206
+ });
207
+ }, [currentCompositionIdentifier]);
152
208
 
153
209
  return (
154
210
  <CompositionContextProvider queryParams={compositionParams} setQueryParams={setCompositionParams}>
@@ -160,16 +216,6 @@ export function Compositions({
160
216
  <OptionButton icon="open-tab" />
161
217
  </Link>
162
218
  </Tooltip>
163
- {currentCompositionHasControls && (
164
- <Tooltip content={controlsTrayOpen ? 'Hide controls' : 'Show controls'} placement="bottom">
165
- <button
166
- className={`${styles.toolbarButton} ${controlsTrayOpen ? styles.toolbarButtonActive : ''}`}
167
- onClick={() => setControlsTrayOpen((o) => !o)}
168
- >
169
- <Icon of="settings" />
170
- </button>
171
- </Tooltip>
172
- )}
173
219
  </CompositionsMenuBar>
174
220
  <SandboxPermissionsAggregator
175
221
  hooks={previewSandboxHooks}
@@ -178,37 +224,31 @@ export function Compositions({
178
224
  />
179
225
  <div className={styles.previewArea}>
180
226
  {isDraggingTray && <div className={styles.dragOverlay} />}
181
- <CompositionContent
182
- className={styles.compositionPanel}
183
- emptyState={emptyState}
184
- component={component}
185
- selected={currentComposition}
186
- queryParams={queryParams}
187
- sandbox={sandboxValue}
188
- />
227
+ <PreviewPropsAggregator hooks={previewPropsHooks} component={component}>
228
+ {(previewAttrs) => (
229
+ <CompositionContent
230
+ {...previewAttrs}
231
+ className={styles.compositionPanel}
232
+ emptyState={emptyState}
233
+ component={component}
234
+ selected={currentComposition}
235
+ queryParams={queryParams}
236
+ sandbox={sandboxValue}
237
+ />
238
+ )}
239
+ </PreviewPropsAggregator>
189
240
  {showControlsTray && (
190
- <div ref={trayRef} className={styles.controlsTray} style={{ maxHeight: trayHeightRef.current }}>
191
- <div className={styles.trayDragHandle} onMouseDown={onTrayDragStart}>
192
- <div className={styles.trayDragBar} />
193
- </div>
194
- <div className={styles.trayHeader}>
195
- <div className={styles.trayTitleRow}>
196
- <Icon of="settings" className={styles.trayIcon} />
197
- <span className={styles.trayTitle}>Live Controls</span>
198
- {ready && defs.length > 0 && <span className={styles.trayBadge}>{defs.length}</span>}
199
- </div>
200
- <button className={styles.trayClose} onClick={() => setControlsTrayOpen(false)}>
201
- <Icon of="x" />
202
- </button>
203
- </div>
204
- <div className={styles.trayBody}>
205
- {ready ? (
206
- <LiveControls defs={defs} values={values} onChange={onChange} />
207
- ) : (
208
- <div className={styles.trayEmpty}>No live controls available for this composition</div>
209
- )}
210
- </div>
211
- </div>
241
+ <LiveControlsTray
242
+ trayRef={trayRef}
243
+ collapsed={isTrayCollapsed}
244
+ height={trayHeight}
245
+ ready={ready}
246
+ defs={defs}
247
+ values={values}
248
+ onChange={onChange}
249
+ onResizeStripMouseDown={onResizeStripMouseDown}
250
+ onToggleExpanded={toggleTrayCollapsed}
251
+ />
212
252
  )}
213
253
  </div>
214
254
  </Pane>
@@ -234,6 +274,9 @@ export function Compositions({
234
274
  isScaling={isScaling}
235
275
  useNameParam={useNameParam}
236
276
  includesEnvTemplate={component.preview?.includesEnvTemplate}
277
+ hasLiveControls={currentCompositionHasControls}
278
+ liveControlsActive={currentCompositionHasControls && !isTrayCollapsedForCurrent}
279
+ onToggleLiveControls={toggleTrayCollapsed}
237
280
  onSelectComposition={(composition) => {
238
281
  if (!currentComposition || !location) return;
239
282
  const selectedCompositionFromUrl = params['*'];
@@ -252,9 +295,6 @@ export function Compositions({
252
295
  }
253
296
  const newPath = pathSegments.join('/');
254
297
  navigate(`/${newPath}?${urlParams.toString()}`);
255
- // open the tray only on explicit user selection, so the
256
- // tray stays closed on navigation/tab return.
257
- setControlsTrayOpen(true);
258
298
  }}
259
299
  url={compositionUrl}
260
300
  compositions={component.compositions}
@@ -272,6 +312,89 @@ export function Compositions({
272
312
  );
273
313
  }
274
314
 
315
+ type LiveControlsTrayProps = {
316
+ trayRef: React.RefObject<HTMLDivElement | null>;
317
+ collapsed: boolean;
318
+ height: number | null;
319
+ ready: boolean;
320
+ defs: UseLiveControlsResult['defs'];
321
+ values: UseLiveControlsResult['values'];
322
+ onChange: UseLiveControlsResult['onChange'];
323
+ onResizeStripMouseDown: (e: React.MouseEvent) => void;
324
+ onToggleExpanded: () => void;
325
+ };
326
+
327
+ function LiveControlsTray({
328
+ trayRef,
329
+ collapsed,
330
+ height,
331
+ ready,
332
+ defs,
333
+ values,
334
+ onChange,
335
+ onResizeStripMouseDown,
336
+ onToggleExpanded,
337
+ }: LiveControlsTrayProps) {
338
+ // height = null → let CSS size the tray to its content (with the max-height
339
+ // clamp doing the upper bound); height = number → user has drag-resized,
340
+ // pin to that.
341
+ const trayStyle = collapsed || height === null ? undefined : { height };
342
+ return (
343
+ <div
344
+ ref={trayRef}
345
+ className={classNames(styles.controlsTray, collapsed && styles.controlsTrayCollapsed)}
346
+ style={trayStyle}
347
+ >
348
+ {!collapsed && (
349
+ <div
350
+ className={styles.trayResizeStrip}
351
+ onMouseDown={onResizeStripMouseDown}
352
+ role="separator"
353
+ aria-orientation="horizontal"
354
+ aria-label="Resize live controls"
355
+ title="Drag to resize"
356
+ >
357
+ <div className={styles.trayDragBar} />
358
+ </div>
359
+ )}
360
+ <div
361
+ className={styles.trayHeaderInner}
362
+ onClick={onToggleExpanded}
363
+ role="button"
364
+ tabIndex={0}
365
+ onKeyDown={(e) => {
366
+ if (e.key === 'Enter' || e.key === ' ') {
367
+ e.preventDefault();
368
+ onToggleExpanded();
369
+ }
370
+ }}
371
+ title={collapsed ? 'Click to expand' : 'Click to collapse'}
372
+ >
373
+ <div className={styles.trayTitleRow}>
374
+ <Icon of="settings" className={styles.trayIcon} />
375
+ <span className={styles.trayTitle}>Live Controls</span>
376
+ {ready && defs.length > 0 && <span className={styles.trayBadge}>{defs.length}</span>}
377
+ </div>
378
+ <span
379
+ className={classNames(styles.trayCollapseIcon, collapsed && styles.trayCollapseIconCollapsed)}
380
+ aria-hidden
381
+ >
382
+ <Icon of="right-rounded-corners" />
383
+ </span>
384
+ </div>
385
+ {!collapsed && (
386
+ <div className={styles.trayBody}>
387
+ {ready ? (
388
+ <LiveControls defs={defs} values={values} onChange={onChange} />
389
+ ) : (
390
+ <div className={styles.trayEmpty}>No live controls available for this composition</div>
391
+ )}
392
+ </div>
393
+ )}
394
+ </div>
395
+ );
396
+ }
397
+
275
398
  export type CompositionContentProps = {
276
399
  component: ComponentModel;
277
400
  selected?: Composition;
@@ -9,7 +9,7 @@ import { CompositionCompareSection } from '@teambit/compositions.ui.composition-
9
9
  import { CompositionCompare } from '@teambit/compositions.ui.composition-compare';
10
10
  import type { ComponentCompareUI } from '@teambit/component-compare';
11
11
  import { ComponentCompareAspect } from '@teambit/component-compare';
12
- import type { UseSandboxPermission } from '@teambit/preview.ui.component-preview';
12
+ import type { UsePreviewProps, UseSandboxPermission } from '@teambit/preview.ui.component-preview';
13
13
  import { CompositionsSection } from './composition.section';
14
14
  import { CompositionsAspect } from './compositions.aspect';
15
15
  import type { MenuBarWidget } from './compositions';
@@ -18,12 +18,14 @@ import { CompositionContent } from './compositions';
18
18
  export type CompositionsMenuSlot = SlotRegistry<MenuBarWidget[]>;
19
19
  export type EmptyStateSlot = SlotRegistry<ComponentType>;
20
20
  export type UsePreviewSandboxSlot = SlotRegistry<UseSandboxPermission>;
21
+ export type UsePreviewPropsSlot = SlotRegistry<UsePreviewProps>;
21
22
 
22
23
  export class CompositionsUI {
23
24
  constructor(
24
25
  private menuBarWidgetSlot: CompositionsMenuSlot,
25
26
  private emptyStateSlot: EmptyStateSlot,
26
- private usePreviewSandboxSlot: UsePreviewSandboxSlot
27
+ private usePreviewSandboxSlot: UsePreviewSandboxSlot,
28
+ private usePreviewPropsSlot: UsePreviewPropsSlot
27
29
  ) {}
28
30
  /**
29
31
  * register a new tester empty state. this allows to register a different empty state from each environment for example.
@@ -41,6 +43,18 @@ export class CompositionsUI {
41
43
  this.usePreviewSandboxSlot.register(useSandboxPermission);
42
44
  }
43
45
 
46
+ /**
47
+ * register a per-component resolver for iframe attributes on the composition preview
48
+ * (`allow`, `referrerPolicy`, ...). The resolver runs at render time with the current
49
+ * `ComponentModel`; results from multiple resolvers merge with later keys winning. Use
50
+ * this to grant a specific subset of components extra Permissions Policy capabilities
51
+ * (e.g. `fullscreen` for chart/video components) without loosening the default for all
52
+ * components.
53
+ */
54
+ registerPreviewProps(usePreviewProps: UsePreviewProps) {
55
+ this.usePreviewPropsSlot.register(usePreviewProps);
56
+ }
57
+
44
58
  getCompositionsCompare = () => {
45
59
  return (
46
60
  <CompositionCompare
@@ -54,23 +68,35 @@ export class CompositionsUI {
54
68
 
55
69
  static dependencies = [ComponentAspect, ComponentCompareAspect];
56
70
  static runtime = UIRuntime;
57
- static slots = [Slot.withType<ReactNode>(), Slot.withType<ComponentType>(), Slot.withType<UseSandboxPermission>()];
71
+ static slots = [
72
+ Slot.withType<ReactNode>(),
73
+ Slot.withType<ComponentType>(),
74
+ Slot.withType<UseSandboxPermission>(),
75
+ Slot.withType<UsePreviewProps>(),
76
+ ];
58
77
 
59
78
  static async provider(
60
79
  [component, componentCompare]: [ComponentUI, ComponentCompareUI],
61
80
  config: {},
62
- [compositionMenuSlot, emptyStateSlot, usePreviewSandboxSlot]: [
81
+ [compositionMenuSlot, emptyStateSlot, usePreviewSandboxSlot, usePreviewPropsSlot]: [
63
82
  CompositionsMenuSlot,
64
83
  EmptyStateSlot,
65
84
  UsePreviewSandboxSlot,
85
+ UsePreviewPropsSlot,
66
86
  ]
67
87
  ) {
68
- const compositions = new CompositionsUI(compositionMenuSlot, emptyStateSlot, usePreviewSandboxSlot);
88
+ const compositions = new CompositionsUI(
89
+ compositionMenuSlot,
90
+ emptyStateSlot,
91
+ usePreviewSandboxSlot,
92
+ usePreviewPropsSlot
93
+ );
69
94
  const section = new CompositionsSection(
70
95
  compositions,
71
96
  { menuBarWidgetSlot: compositions.menuBarWidgetSlot },
72
97
  emptyStateSlot,
73
- usePreviewSandboxSlot
98
+ usePreviewSandboxSlot,
99
+ usePreviewPropsSlot
74
100
  );
75
101
  const compositionCompare = new CompositionCompareSection(compositions);
76
102
  component.registerRoute(section.route);
@@ -83,6 +109,12 @@ export class CompositionsUI {
83
109
  manager.add('allow-same-origin');
84
110
  }
85
111
  });
112
+ // Default Permissions Policy: allow clipboard writes so copy-to-clipboard buttons in
113
+ // compositions work. Clipboard-read, camera, mic, geolocation, etc. remain denied. Other
114
+ // aspects may layer on (e.g. `fullscreen` for chart/video components).
115
+ compositions.registerPreviewProps((manager) => {
116
+ manager.set('allow', 'clipboard-write');
117
+ });
86
118
  return compositions;
87
119
  }
88
120
  }
@@ -1,5 +1,5 @@
1
1
  import type { Section } from '@teambit/component';
2
- import type { CompositionsUI, CompositionsMenuSlot, EmptyStateSlot, UsePreviewSandboxSlot } from './compositions.ui.runtime';
2
+ import type { CompositionsUI, CompositionsMenuSlot, EmptyStateSlot, UsePreviewSandboxSlot, UsePreviewPropsSlot } from './compositions.ui.runtime';
3
3
  type Options = {
4
4
  menuBarWidgetSlot: CompositionsMenuSlot;
5
5
  };
@@ -11,11 +11,12 @@ export declare class CompositionsSection implements Section {
11
11
  private options;
12
12
  private emptyStateSlot;
13
13
  private usePreviewSandboxSlot;
14
+ private usePreviewPropsSlot;
14
15
  constructor(
15
16
  /**
16
17
  * simulations ui extension.
17
18
  */
18
- compositions: CompositionsUI, options: Options, emptyStateSlot: EmptyStateSlot, usePreviewSandboxSlot: UsePreviewSandboxSlot);
19
+ compositions: CompositionsUI, options: Options, emptyStateSlot: EmptyStateSlot, usePreviewSandboxSlot: UsePreviewSandboxSlot, usePreviewPropsSlot: UsePreviewPropsSlot);
19
20
  navigationLink: {
20
21
  href: string;
21
22
  children: string;
@@ -27,11 +27,12 @@ class CompositionsSection {
27
27
  /**
28
28
  * simulations ui extension.
29
29
  */
30
- compositions, options, emptyStateSlot, usePreviewSandboxSlot) {
30
+ compositions, options, emptyStateSlot, usePreviewSandboxSlot, usePreviewPropsSlot) {
31
31
  this.compositions = compositions;
32
32
  this.options = options;
33
33
  this.emptyStateSlot = emptyStateSlot;
34
34
  this.usePreviewSandboxSlot = usePreviewSandboxSlot;
35
+ this.usePreviewPropsSlot = usePreviewPropsSlot;
35
36
  _defineProperty(this, "navigationLink", {
36
37
  href: '~compositions',
37
38
  children: 'Preview'
@@ -42,6 +43,7 @@ class CompositionsSection {
42
43
  menuBarWidgets: this.options.menuBarWidgetSlot,
43
44
  emptyState: this.emptyStateSlot,
44
45
  usePreviewSandboxSlot: this.usePreviewSandboxSlot,
46
+ usePreviewPropsSlot: this.usePreviewPropsSlot,
45
47
  enableLiveControls: true
46
48
  })
47
49
  });
@@ -1 +1 @@
1
- {"version":3,"names":["_react","data","_interopRequireDefault","require","_compositions","e","__esModule","default","_defineProperty","r","t","_toPropertyKey","Object","defineProperty","value","enumerable","configurable","writable","i","_toPrimitive","Symbol","toPrimitive","call","TypeError","String","Number","CompositionsSection","constructor","compositions","options","emptyStateSlot","usePreviewSandboxSlot","href","children","path","element","createElement","Compositions","menuBarWidgets","menuBarWidgetSlot","emptyState","enableLiveControls","exports"],"sources":["composition.section.tsx"],"sourcesContent":["import type { Section } from '@teambit/component';\nimport React from 'react';\nimport { Compositions } from './compositions';\nimport type {\n CompositionsUI,\n CompositionsMenuSlot,\n EmptyStateSlot,\n UsePreviewSandboxSlot,\n} from './compositions.ui.runtime';\n\ntype Options = { menuBarWidgetSlot: CompositionsMenuSlot };\n\nexport class CompositionsSection implements Section {\n constructor(\n /**\n * simulations ui extension.\n */\n private compositions: CompositionsUI,\n private options: Options,\n private emptyStateSlot: EmptyStateSlot,\n private usePreviewSandboxSlot: UsePreviewSandboxSlot\n ) {}\n\n navigationLink = {\n href: '~compositions',\n children: 'Preview',\n };\n\n route = {\n path: '~compositions/*',\n element: (\n <Compositions\n menuBarWidgets={this.options.menuBarWidgetSlot}\n emptyState={this.emptyStateSlot}\n usePreviewSandboxSlot={this.usePreviewSandboxSlot}\n enableLiveControls\n />\n ),\n };\n\n order = 20;\n}\n"],"mappings":";;;;;;AACA,SAAAA,OAAA;EAAA,MAAAC,IAAA,GAAAC,sBAAA,CAAAC,OAAA;EAAAH,MAAA,YAAAA,CAAA;IAAA,OAAAC,IAAA;EAAA;EAAA,OAAAA,IAAA;AAAA;AACA,SAAAG,cAAA;EAAA,MAAAH,IAAA,GAAAE,OAAA;EAAAC,aAAA,YAAAA,CAAA;IAAA,OAAAH,IAAA;EAAA;EAAA,OAAAA,IAAA;AAAA;AAA8C,SAAAC,uBAAAG,CAAA,WAAAA,CAAA,IAAAA,CAAA,CAAAC,UAAA,GAAAD,CAAA,KAAAE,OAAA,EAAAF,CAAA;AAAA,SAAAG,gBAAAH,CAAA,EAAAI,CAAA,EAAAC,CAAA,YAAAD,CAAA,GAAAE,cAAA,CAAAF,CAAA,MAAAJ,CAAA,GAAAO,MAAA,CAAAC,cAAA,CAAAR,CAAA,EAAAI,CAAA,IAAAK,KAAA,EAAAJ,CAAA,EAAAK,UAAA,MAAAC,YAAA,MAAAC,QAAA,UAAAZ,CAAA,CAAAI,CAAA,IAAAC,CAAA,EAAAL,CAAA;AAAA,SAAAM,eAAAD,CAAA,QAAAQ,CAAA,GAAAC,YAAA,CAAAT,CAAA,uCAAAQ,CAAA,GAAAA,CAAA,GAAAA,CAAA;AAAA,SAAAC,aAAAT,CAAA,EAAAD,CAAA,2BAAAC,CAAA,KAAAA,CAAA,SAAAA,CAAA,MAAAL,CAAA,GAAAK,CAAA,CAAAU,MAAA,CAAAC,WAAA,kBAAAhB,CAAA,QAAAa,CAAA,GAAAb,CAAA,CAAAiB,IAAA,CAAAZ,CAAA,EAAAD,CAAA,uCAAAS,CAAA,SAAAA,CAAA,YAAAK,SAAA,yEAAAd,CAAA,GAAAe,MAAA,GAAAC,MAAA,EAAAf,CAAA;AAUvC,MAAMgB,mBAAmB,CAAoB;EAClDC,WAAWA;EACT;AACJ;AACA;EACYC,YAA4B,EAC5BC,OAAgB,EAChBC,cAA8B,EAC9BC,qBAA4C,EACpD;IAAA,KAJQH,YAA4B,GAA5BA,YAA4B;IAAA,KAC5BC,OAAgB,GAAhBA,OAAgB;IAAA,KAChBC,cAA8B,GAA9BA,cAA8B;IAAA,KAC9BC,qBAA4C,GAA5CA,qBAA4C;IAAAvB,eAAA,yBAGrC;MACfwB,IAAI,EAAE,eAAe;MACrBC,QAAQ,EAAE;IACZ,CAAC;IAAAzB,eAAA,gBAEO;MACN0B,IAAI,EAAE,iBAAiB;MACvBC,OAAO,eACLnC,MAAA,GAAAO,OAAA,CAAA6B,aAAA,CAAChC,aAAA,GAAAiC,YAAY;QACXC,cAAc,EAAE,IAAI,CAACT,OAAO,CAACU,iBAAkB;QAC/CC,UAAU,EAAE,IAAI,CAACV,cAAe;QAChCC,qBAAqB,EAAE,IAAI,CAACA,qBAAsB;QAClDU,kBAAkB;MAAA,CACnB;IAEL,CAAC;IAAAjC,eAAA,gBAEO,EAAE;EAnBP;AAoBL;AAACkC,OAAA,CAAAhB,mBAAA,GAAAA,mBAAA","ignoreList":[]}
1
+ {"version":3,"names":["_react","data","_interopRequireDefault","require","_compositions","e","__esModule","default","_defineProperty","r","t","_toPropertyKey","Object","defineProperty","value","enumerable","configurable","writable","i","_toPrimitive","Symbol","toPrimitive","call","TypeError","String","Number","CompositionsSection","constructor","compositions","options","emptyStateSlot","usePreviewSandboxSlot","usePreviewPropsSlot","href","children","path","element","createElement","Compositions","menuBarWidgets","menuBarWidgetSlot","emptyState","enableLiveControls","exports"],"sources":["composition.section.tsx"],"sourcesContent":["import type { Section } from '@teambit/component';\nimport React from 'react';\nimport { Compositions } from './compositions';\nimport type {\n CompositionsUI,\n CompositionsMenuSlot,\n EmptyStateSlot,\n UsePreviewSandboxSlot,\n UsePreviewPropsSlot,\n} from './compositions.ui.runtime';\n\ntype Options = { menuBarWidgetSlot: CompositionsMenuSlot };\n\nexport class CompositionsSection implements Section {\n constructor(\n /**\n * simulations ui extension.\n */\n private compositions: CompositionsUI,\n private options: Options,\n private emptyStateSlot: EmptyStateSlot,\n private usePreviewSandboxSlot: UsePreviewSandboxSlot,\n private usePreviewPropsSlot: UsePreviewPropsSlot\n ) {}\n\n navigationLink = {\n href: '~compositions',\n children: 'Preview',\n };\n\n route = {\n path: '~compositions/*',\n element: (\n <Compositions\n menuBarWidgets={this.options.menuBarWidgetSlot}\n emptyState={this.emptyStateSlot}\n usePreviewSandboxSlot={this.usePreviewSandboxSlot}\n usePreviewPropsSlot={this.usePreviewPropsSlot}\n enableLiveControls\n />\n ),\n };\n\n order = 20;\n}\n"],"mappings":";;;;;;AACA,SAAAA,OAAA;EAAA,MAAAC,IAAA,GAAAC,sBAAA,CAAAC,OAAA;EAAAH,MAAA,YAAAA,CAAA;IAAA,OAAAC,IAAA;EAAA;EAAA,OAAAA,IAAA;AAAA;AACA,SAAAG,cAAA;EAAA,MAAAH,IAAA,GAAAE,OAAA;EAAAC,aAAA,YAAAA,CAAA;IAAA,OAAAH,IAAA;EAAA;EAAA,OAAAA,IAAA;AAAA;AAA8C,SAAAC,uBAAAG,CAAA,WAAAA,CAAA,IAAAA,CAAA,CAAAC,UAAA,GAAAD,CAAA,KAAAE,OAAA,EAAAF,CAAA;AAAA,SAAAG,gBAAAH,CAAA,EAAAI,CAAA,EAAAC,CAAA,YAAAD,CAAA,GAAAE,cAAA,CAAAF,CAAA,MAAAJ,CAAA,GAAAO,MAAA,CAAAC,cAAA,CAAAR,CAAA,EAAAI,CAAA,IAAAK,KAAA,EAAAJ,CAAA,EAAAK,UAAA,MAAAC,YAAA,MAAAC,QAAA,UAAAZ,CAAA,CAAAI,CAAA,IAAAC,CAAA,EAAAL,CAAA;AAAA,SAAAM,eAAAD,CAAA,QAAAQ,CAAA,GAAAC,YAAA,CAAAT,CAAA,uCAAAQ,CAAA,GAAAA,CAAA,GAAAA,CAAA;AAAA,SAAAC,aAAAT,CAAA,EAAAD,CAAA,2BAAAC,CAAA,KAAAA,CAAA,SAAAA,CAAA,MAAAL,CAAA,GAAAK,CAAA,CAAAU,MAAA,CAAAC,WAAA,kBAAAhB,CAAA,QAAAa,CAAA,GAAAb,CAAA,CAAAiB,IAAA,CAAAZ,CAAA,EAAAD,CAAA,uCAAAS,CAAA,SAAAA,CAAA,YAAAK,SAAA,yEAAAd,CAAA,GAAAe,MAAA,GAAAC,MAAA,EAAAf,CAAA;AAWvC,MAAMgB,mBAAmB,CAAoB;EAClDC,WAAWA;EACT;AACJ;AACA;EACYC,YAA4B,EAC5BC,OAAgB,EAChBC,cAA8B,EAC9BC,qBAA4C,EAC5CC,mBAAwC,EAChD;IAAA,KALQJ,YAA4B,GAA5BA,YAA4B;IAAA,KAC5BC,OAAgB,GAAhBA,OAAgB;IAAA,KAChBC,cAA8B,GAA9BA,cAA8B;IAAA,KAC9BC,qBAA4C,GAA5CA,qBAA4C;IAAA,KAC5CC,mBAAwC,GAAxCA,mBAAwC;IAAAxB,eAAA,yBAGjC;MACfyB,IAAI,EAAE,eAAe;MACrBC,QAAQ,EAAE;IACZ,CAAC;IAAA1B,eAAA,gBAEO;MACN2B,IAAI,EAAE,iBAAiB;MACvBC,OAAO,eACLpC,MAAA,GAAAO,OAAA,CAAA8B,aAAA,CAACjC,aAAA,GAAAkC,YAAY;QACXC,cAAc,EAAE,IAAI,CAACV,OAAO,CAACW,iBAAkB;QAC/CC,UAAU,EAAE,IAAI,CAACX,cAAe;QAChCC,qBAAqB,EAAE,IAAI,CAACA,qBAAsB;QAClDC,mBAAmB,EAAE,IAAI,CAACA,mBAAoB;QAC9CU,kBAAkB;MAAA,CACnB;IAEL,CAAC;IAAAlC,eAAA,gBAEO,EAAE;EApBP;AAqBL;AAACmC,OAAA,CAAAjB,mBAAA,GAAAA,mBAAA","ignoreList":[]}