svelte-p5-components 0.4.0 → 0.5.0

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.
@@ -0,0 +1,439 @@
1
+ <script lang="ts" module>
2
+ import type { Snippet } from 'svelte';
3
+
4
+ /**
5
+ * One entry rendered as a row in the context menu.
6
+ */
7
+ export interface ContextMenuItem {
8
+ /** Stable id passed back via `onSelect`. */
9
+ id: string;
10
+ /** Human-readable label. */
11
+ label: string;
12
+ /** Optional leading icon. */
13
+ icon?: Snippet;
14
+ /** When true the item is rendered but not activatable. */
15
+ disabled?: boolean;
16
+ /** When true the item is rendered with a destructive accent. */
17
+ danger?: boolean;
18
+ }
19
+ </script>
20
+
21
+ <script lang="ts">
22
+ /**
23
+ * Lightweight floating action menu, anchored to a viewport point.
24
+ *
25
+ * Use for Figma-style selection-contextual chrome: click a thing on a
26
+ * canvas, get actions near the cursor. Positioning is viewport-fixed
27
+ * (`position: fixed`) so the menu does not scroll with the page. The
28
+ * menu auto-flips horizontally and/or vertically when it would overflow
29
+ * the viewport — mirroring `HoverTooltip`'s edge-aware layout.
30
+ *
31
+ * Close affordances:
32
+ * - Pointerdown outside the menu
33
+ * - Escape keypress
34
+ * - Window blur, scroll, or resize
35
+ * - Selecting an enabled item
36
+ * - Caller sets `open = false`
37
+ *
38
+ * Keyboard:
39
+ * - Up / Down arrow moves focus between enabled items (wraps)
40
+ * - Home / End jump to first / last enabled item
41
+ * - Enter or Space activates the focused item
42
+ * - Escape closes
43
+ *
44
+ * Public class-name contract (stable across versions):
45
+ * .context-menu
46
+ * .context-menu__list
47
+ * .context-menu__item
48
+ * .context-menu__item--disabled
49
+ * .context-menu__item--danger
50
+ * .context-menu__item-icon
51
+ * .context-menu__item-label
52
+ *
53
+ * @example
54
+ * ```svelte
55
+ * <ContextMenu
56
+ * bind:open
57
+ * anchor={{ x: evt.clientX, y: evt.clientY }}
58
+ * items={[
59
+ * { id: 'seek', label: 'Seek video here' },
60
+ * { id: 'filter', label: 'Filter to speaker' }
61
+ * ]}
62
+ * onSelect={(id) => doAction(id)}
63
+ * onClose={() => (open = false)}
64
+ * />
65
+ * ```
66
+ */
67
+ interface Props {
68
+ /** Whether the menu is visible. Bindable. */
69
+ open: boolean;
70
+ /** Anchor point in client (page) coordinates. Menu top-left aligns here until flipped. */
71
+ anchor: { x: number; y: number } | null;
72
+ /** Menu entries. */
73
+ items: ContextMenuItem[];
74
+ /** Fired with the activated item id. */
75
+ onSelect?: (id: string) => void;
76
+ /** Fired whenever the menu should close (Escape, outside click, blur, etc.). */
77
+ onClose?: () => void;
78
+ class?: string;
79
+ }
80
+
81
+ let {
82
+ open = $bindable(false),
83
+ anchor,
84
+ items,
85
+ onSelect,
86
+ onClose,
87
+ class: className = ''
88
+ }: Props = $props();
89
+
90
+ let menuEl: HTMLDivElement | null = $state(null);
91
+ let measuredWidth = $state(0);
92
+ let measuredHeight = $state(0);
93
+ let focusedIndex = $state(-1);
94
+ let previouslyFocused: HTMLElement | null = null;
95
+
96
+ // Respect prefers-reduced-motion. Detected once per mount; re-reading is
97
+ // cheap but unnecessary — animation duration is the only thing gated on
98
+ // this so the CSS handles the reduced case via a @media rule anyway.
99
+ const prefersReducedMotion =
100
+ typeof window !== 'undefined' &&
101
+ typeof window.matchMedia === 'function' &&
102
+ window.matchMedia('(prefers-reduced-motion: reduce)').matches;
103
+
104
+ // Measure after mount so auto-flip has real dimensions to work with. The
105
+ // ResizeObserver also catches the case where items / labels change while
106
+ // the menu is open.
107
+ $effect(() => {
108
+ if (!menuEl) return;
109
+ const el = menuEl;
110
+ const ro = new ResizeObserver(() => {
111
+ const r = el.getBoundingClientRect();
112
+ measuredWidth = r.width;
113
+ measuredHeight = r.height;
114
+ });
115
+ ro.observe(el);
116
+ const initial = el.getBoundingClientRect();
117
+ measuredWidth = initial.width;
118
+ measuredHeight = initial.height;
119
+ return () => ro.disconnect();
120
+ });
121
+
122
+ // Focus + dismiss wiring. Only attaches while open, and captures the
123
+ // previously-focused element so we can restore focus on close.
124
+ $effect(() => {
125
+ if (!open) return;
126
+ previouslyFocused = (document.activeElement as HTMLElement | null) ?? null;
127
+ const firstEnabled = items.findIndex((it) => !it.disabled);
128
+ focusedIndex = firstEnabled;
129
+
130
+ // Attach after a microtask so our own opening pointerdown doesn't
131
+ // immediately get interpreted as an outside click.
132
+ let attached = false;
133
+ const attach = () => {
134
+ if (attached) return;
135
+ attached = true;
136
+ document.addEventListener('pointerdown', handleDocumentPointerDown, true);
137
+ window.addEventListener('blur', handleWindowBlur);
138
+ window.addEventListener('resize', handleWindowResize);
139
+ window.addEventListener('scroll', handleWindowScroll, true);
140
+ };
141
+ const handle = requestAnimationFrame(attach);
142
+
143
+ return () => {
144
+ cancelAnimationFrame(handle);
145
+ if (attached) {
146
+ document.removeEventListener('pointerdown', handleDocumentPointerDown, true);
147
+ window.removeEventListener('blur', handleWindowBlur);
148
+ window.removeEventListener('resize', handleWindowResize);
149
+ window.removeEventListener('scroll', handleWindowScroll, true);
150
+ }
151
+ // Restore focus to whatever owned it before the menu opened.
152
+ if (previouslyFocused && typeof previouslyFocused.focus === 'function') {
153
+ previouslyFocused.focus({ preventScroll: true });
154
+ }
155
+ previouslyFocused = null;
156
+ };
157
+ });
158
+
159
+ // When the focused index changes, actually move DOM focus to that item so
160
+ // keyboard users see a visible focus ring and activate on Enter.
161
+ $effect(() => {
162
+ if (!open) return;
163
+ if (focusedIndex < 0 || !menuEl) return;
164
+ const buttons = menuEl.querySelectorAll<HTMLButtonElement>('[data-context-menu-item]');
165
+ const target = buttons[focusedIndex];
166
+ if (target && document.activeElement !== target) {
167
+ target.focus({ preventScroll: true });
168
+ }
169
+ });
170
+
171
+ const placement = $derived.by<{ left: number; top: number } | null>(() => {
172
+ if (!anchor) return null;
173
+ if (typeof window === 'undefined') return null;
174
+
175
+ const vpW = window.innerWidth;
176
+ const vpH = window.innerHeight;
177
+ const pad = 4;
178
+ const { x, y } = anchor;
179
+
180
+ // Horizontal: prefer the right of the anchor; flip to the left if it
181
+ // would overflow. Clamp inside the viewport either way.
182
+ let left = x;
183
+ if (measuredWidth > 0 && left + measuredWidth + pad > vpW) {
184
+ left = x - measuredWidth;
185
+ }
186
+ left = Math.max(pad, Math.min(vpW - measuredWidth - pad, left));
187
+
188
+ // Vertical: prefer below the anchor; flip above if it would overflow.
189
+ let top = y;
190
+ if (measuredHeight > 0 && top + measuredHeight + pad > vpH) {
191
+ top = y - measuredHeight;
192
+ }
193
+ top = Math.max(pad, Math.min(vpH - measuredHeight - pad, top));
194
+
195
+ return { left, top };
196
+ });
197
+
198
+ function closeMenu() {
199
+ onClose?.();
200
+ }
201
+
202
+ function handleDocumentPointerDown(event: PointerEvent) {
203
+ if (!menuEl) return;
204
+ const target = event.target as Node | null;
205
+ if (target && menuEl.contains(target)) return;
206
+ closeMenu();
207
+ }
208
+
209
+ function handleWindowBlur() {
210
+ closeMenu();
211
+ }
212
+
213
+ function handleWindowResize() {
214
+ closeMenu();
215
+ }
216
+
217
+ function handleWindowScroll() {
218
+ closeMenu();
219
+ }
220
+
221
+ function nextEnabled(from: number, dir: 1 | -1): number {
222
+ const n = items.length;
223
+ if (n === 0) return -1;
224
+ let idx = from;
225
+ for (let i = 0; i < n; i++) {
226
+ idx = (idx + dir + n) % n;
227
+ const item = items[idx];
228
+ if (item && !item.disabled) return idx;
229
+ }
230
+ return -1;
231
+ }
232
+
233
+ function firstEnabled(): number {
234
+ const idx = items.findIndex((it) => !it.disabled);
235
+ return idx;
236
+ }
237
+
238
+ function lastEnabled(): number {
239
+ for (let i = items.length - 1; i >= 0; i--) {
240
+ const item = items[i];
241
+ if (item && !item.disabled) return i;
242
+ }
243
+ return -1;
244
+ }
245
+
246
+ function handleMenuKeydown(event: KeyboardEvent) {
247
+ switch (event.key) {
248
+ case 'Escape': {
249
+ event.preventDefault();
250
+ event.stopPropagation();
251
+ closeMenu();
252
+ return;
253
+ }
254
+ case 'ArrowDown': {
255
+ event.preventDefault();
256
+ focusedIndex = nextEnabled(focusedIndex, 1);
257
+ return;
258
+ }
259
+ case 'ArrowUp': {
260
+ event.preventDefault();
261
+ focusedIndex = nextEnabled(focusedIndex, -1);
262
+ return;
263
+ }
264
+ case 'Home': {
265
+ event.preventDefault();
266
+ focusedIndex = firstEnabled();
267
+ return;
268
+ }
269
+ case 'End': {
270
+ event.preventDefault();
271
+ focusedIndex = lastEnabled();
272
+ return;
273
+ }
274
+ case 'Enter':
275
+ case ' ': {
276
+ if (focusedIndex < 0) return;
277
+ const it = items[focusedIndex];
278
+ if (!it || it.disabled) return;
279
+ event.preventDefault();
280
+ activate(it.id);
281
+ return;
282
+ }
283
+ }
284
+ }
285
+
286
+ function activate(id: string) {
287
+ onSelect?.(id);
288
+ closeMenu();
289
+ }
290
+
291
+ function handleItemClick(item: ContextMenuItem) {
292
+ if (item.disabled) return;
293
+ activate(item.id);
294
+ }
295
+ </script>
296
+
297
+ {#if open && anchor}
298
+ <div
299
+ bind:this={menuEl}
300
+ class="context-menu {className}"
301
+ class:context-menu--reduced-motion={prefersReducedMotion}
302
+ role="menu"
303
+ tabindex="-1"
304
+ style:left="{placement?.left ?? anchor.x}px"
305
+ style:top="{placement?.top ?? anchor.y}px"
306
+ style:visibility={placement ? 'visible' : 'hidden'}
307
+ onkeydown={handleMenuKeydown}
308
+ >
309
+ <ul class="context-menu__list">
310
+ {#each items as item, i (item.id)}
311
+ <li>
312
+ <button
313
+ type="button"
314
+ data-context-menu-item
315
+ class="context-menu__item"
316
+ class:context-menu__item--disabled={item.disabled}
317
+ class:context-menu__item--danger={item.danger}
318
+ role="menuitem"
319
+ aria-disabled={item.disabled ? 'true' : undefined}
320
+ tabindex={focusedIndex === i ? 0 : -1}
321
+ onclick={() => handleItemClick(item)}
322
+ onmouseenter={() => {
323
+ if (!item.disabled) focusedIndex = i;
324
+ }}
325
+ >
326
+ {#if item.icon}
327
+ <span class="context-menu__item-icon" aria-hidden="true">
328
+ {@render item.icon()}
329
+ </span>
330
+ {/if}
331
+ <span class="context-menu__item-label">{item.label}</span>
332
+ </button>
333
+ </li>
334
+ {/each}
335
+ </ul>
336
+ </div>
337
+ {/if}
338
+
339
+ <style>
340
+ .context-menu {
341
+ position: fixed;
342
+ z-index: 1100;
343
+ min-width: 180px;
344
+ max-width: 320px;
345
+ padding: 4px 0;
346
+ background: var(--context-menu-bg, #ffffff);
347
+ color: var(--context-menu-fg, #111);
348
+ border: 1px solid var(--context-menu-border, rgba(0, 0, 0, 0.1));
349
+ border-radius: 8px;
350
+ box-shadow:
351
+ 0 8px 20px rgba(0, 0, 0, 0.14),
352
+ 0 2px 4px rgba(0, 0, 0, 0.06);
353
+ font:
354
+ 13px/1.3 system-ui,
355
+ -apple-system,
356
+ Segoe UI,
357
+ sans-serif;
358
+ user-select: none;
359
+ animation: context-menu-in 100ms ease-out;
360
+ transform-origin: top left;
361
+ }
362
+
363
+ .context-menu--reduced-motion {
364
+ animation: none;
365
+ }
366
+
367
+ @keyframes context-menu-in {
368
+ from {
369
+ transform: scale(0.96);
370
+ opacity: 0;
371
+ }
372
+ to {
373
+ transform: scale(1);
374
+ opacity: 1;
375
+ }
376
+ }
377
+
378
+ @media (prefers-reduced-motion: reduce) {
379
+ .context-menu {
380
+ animation: none;
381
+ }
382
+ }
383
+
384
+ .context-menu__list {
385
+ list-style: none;
386
+ margin: 0;
387
+ padding: 0;
388
+ }
389
+
390
+ .context-menu__item {
391
+ display: flex;
392
+ align-items: center;
393
+ gap: 8px;
394
+ width: 100%;
395
+ padding: 6px 12px;
396
+ border: none;
397
+ background: transparent;
398
+ color: inherit;
399
+ text-align: left;
400
+ font: inherit;
401
+ cursor: pointer;
402
+ }
403
+
404
+ .context-menu__item:hover:not(.context-menu__item--disabled),
405
+ .context-menu__item:focus-visible:not(.context-menu__item--disabled) {
406
+ background: var(--context-menu-item-hover-bg, rgba(59, 130, 246, 0.12));
407
+ color: var(--context-menu-item-hover-fg, inherit);
408
+ outline: none;
409
+ }
410
+
411
+ .context-menu__item--disabled {
412
+ color: var(--context-menu-item-disabled-fg, rgba(0, 0, 0, 0.38));
413
+ cursor: default;
414
+ }
415
+
416
+ .context-menu__item--danger {
417
+ color: var(--context-menu-item-danger-fg, #dc2626);
418
+ }
419
+
420
+ .context-menu__item--danger:hover:not(.context-menu__item--disabled),
421
+ .context-menu__item--danger:focus-visible:not(.context-menu__item--disabled) {
422
+ background: var(--context-menu-item-danger-hover-bg, rgba(220, 38, 38, 0.1));
423
+ color: var(--context-menu-item-danger-fg, #dc2626);
424
+ }
425
+
426
+ .context-menu__item-icon {
427
+ display: inline-flex;
428
+ align-items: center;
429
+ justify-content: center;
430
+ flex: 0 0 auto;
431
+ width: 16px;
432
+ height: 16px;
433
+ }
434
+
435
+ .context-menu__item-label {
436
+ flex: 1 1 auto;
437
+ min-width: 0;
438
+ }
439
+ </style>
@@ -0,0 +1,80 @@
1
+ import type { Snippet } from 'svelte';
2
+ /**
3
+ * One entry rendered as a row in the context menu.
4
+ */
5
+ export interface ContextMenuItem {
6
+ /** Stable id passed back via `onSelect`. */
7
+ id: string;
8
+ /** Human-readable label. */
9
+ label: string;
10
+ /** Optional leading icon. */
11
+ icon?: Snippet;
12
+ /** When true the item is rendered but not activatable. */
13
+ disabled?: boolean;
14
+ /** When true the item is rendered with a destructive accent. */
15
+ danger?: boolean;
16
+ }
17
+ /**
18
+ * Lightweight floating action menu, anchored to a viewport point.
19
+ *
20
+ * Use for Figma-style selection-contextual chrome: click a thing on a
21
+ * canvas, get actions near the cursor. Positioning is viewport-fixed
22
+ * (`position: fixed`) so the menu does not scroll with the page. The
23
+ * menu auto-flips horizontally and/or vertically when it would overflow
24
+ * the viewport — mirroring `HoverTooltip`'s edge-aware layout.
25
+ *
26
+ * Close affordances:
27
+ * - Pointerdown outside the menu
28
+ * - Escape keypress
29
+ * - Window blur, scroll, or resize
30
+ * - Selecting an enabled item
31
+ * - Caller sets `open = false`
32
+ *
33
+ * Keyboard:
34
+ * - Up / Down arrow moves focus between enabled items (wraps)
35
+ * - Home / End jump to first / last enabled item
36
+ * - Enter or Space activates the focused item
37
+ * - Escape closes
38
+ *
39
+ * Public class-name contract (stable across versions):
40
+ * .context-menu
41
+ * .context-menu__list
42
+ * .context-menu__item
43
+ * .context-menu__item--disabled
44
+ * .context-menu__item--danger
45
+ * .context-menu__item-icon
46
+ * .context-menu__item-label
47
+ *
48
+ * @example
49
+ * ```svelte
50
+ * <ContextMenu
51
+ * bind:open
52
+ * anchor={{ x: evt.clientX, y: evt.clientY }}
53
+ * items={[
54
+ * { id: 'seek', label: 'Seek video here' },
55
+ * { id: 'filter', label: 'Filter to speaker' }
56
+ * ]}
57
+ * onSelect={(id) => doAction(id)}
58
+ * onClose={() => (open = false)}
59
+ * />
60
+ * ```
61
+ */
62
+ interface Props {
63
+ /** Whether the menu is visible. Bindable. */
64
+ open: boolean;
65
+ /** Anchor point in client (page) coordinates. Menu top-left aligns here until flipped. */
66
+ anchor: {
67
+ x: number;
68
+ y: number;
69
+ } | null;
70
+ /** Menu entries. */
71
+ items: ContextMenuItem[];
72
+ /** Fired with the activated item id. */
73
+ onSelect?: (id: string) => void;
74
+ /** Fired whenever the menu should close (Escape, outside click, blur, etc.). */
75
+ onClose?: () => void;
76
+ class?: string;
77
+ }
78
+ declare const ContextMenu: import("svelte").Component<Props, {}, "open">;
79
+ type ContextMenu = ReturnType<typeof ContextMenu>;
80
+ export default ContextMenu;
@@ -72,36 +72,59 @@
72
72
  let size = $state(untrack(() => ({ w: typeof width === 'number' ? width : 480, h: 40 })));
73
73
  let rootEl: HTMLDivElement | null = $state(null);
74
74
 
75
- function clampToViewport() {
76
- if (typeof window === 'undefined') return;
77
- // When constrained to a parent, neodrag's `bounds(BoundsFrom.parent())`
78
- // already keeps the window inside; viewport math here would be wrong
79
- // because positions are relative to the parent, not the viewport.
80
- if (constrained === 'parent') return;
81
- const vw = window.innerWidth;
82
- const vh = window.innerHeight;
83
- // Never allow the titlebar above the viewport top - that's the
84
- // "unreachable" case. Allow partial horizontal overflow so narrow
85
- // screens don't trap wide windows, but always keep `minVisible`
86
- // pixels of the window on-screen for a grab handle.
87
- const minX = -(size.w - minVisible);
88
- const maxX = vw - minVisible;
89
- const minY = 0;
90
- const maxY = vh - minVisible;
91
- pos = {
92
- x: Math.max(minX, Math.min(maxX, pos.x)),
93
- y: Math.max(minY, Math.min(maxY, pos.y))
94
- };
75
+ // Clamp `pos` back inside whatever sandbox `constrained` points at.
76
+ // Runs on drag end (drag might overshoot during pointer-move) AND on
77
+ // any resize of the sandbox (window → viewport mode, parent element →
78
+ // parent mode) so the window snaps back into view when its container
79
+ // shrinks underneath it.
80
+ function clamp() {
81
+ if (typeof window === 'undefined' || !rootEl) return;
82
+
83
+ if (constrained === 'parent') {
84
+ // Position is relative to the parent's padding-box, so bounds
85
+ // are [0, parentSize - windowSize]. Falls through to no-op if
86
+ // the window is somehow larger than the parent (Math.max(0, ...)).
87
+ const parent = rootEl.parentElement;
88
+ if (!parent) return;
89
+ const r = parent.getBoundingClientRect();
90
+ const maxX = Math.max(0, r.width - size.w);
91
+ const maxY = Math.max(0, r.height - size.h);
92
+ pos = {
93
+ x: Math.max(0, Math.min(maxX, pos.x)),
94
+ y: Math.max(0, Math.min(maxY, pos.y))
95
+ };
96
+ return;
97
+ }
98
+
99
+ if (constrained === 'viewport') {
100
+ // Never allow the titlebar above the viewport top - that's the
101
+ // "unreachable" case. Allow partial horizontal overflow so narrow
102
+ // screens don't trap wide windows, but always keep `minVisible`
103
+ // pixels of the window on-screen for a grab handle.
104
+ const vw = window.innerWidth;
105
+ const vh = window.innerHeight;
106
+ const minX = -(size.w - minVisible);
107
+ const maxX = vw - minVisible;
108
+ const minY = 0;
109
+ const maxY = vh - minVisible;
110
+ pos = {
111
+ x: Math.max(minX, Math.min(maxX, pos.x)),
112
+ y: Math.max(minY, Math.min(maxY, pos.y))
113
+ };
114
+ }
115
+ // 'none': no clamping.
95
116
  }
96
117
 
97
118
  // Keep size fresh (used for clamp math) - ResizeObserver on our own
98
- // root element catches CSS resize-from-corner too.
119
+ // root element catches CSS resize-from-corner too. We also re-clamp
120
+ // after each size change so corner-resizing past a bound snaps back.
99
121
  $effect(() => {
100
122
  if (!rootEl) return;
101
123
  const el = rootEl;
102
124
  const ro = new ResizeObserver(() => {
103
125
  const r = el.getBoundingClientRect();
104
126
  size = { w: r.width, h: r.height };
127
+ clamp();
105
128
  });
106
129
  ro.observe(el);
107
130
  // seed
@@ -110,15 +133,30 @@
110
133
  return () => ro.disconnect();
111
134
  });
112
135
 
113
- // Re-clamp on window resize - this is the "snap back in when the
114
- // viewport shrinks" case.
136
+ // Re-clamp on window resize - the "snap back when the viewport shrinks"
137
+ // case. Harmless in parent mode (clamp() handles parent bounds too), so
138
+ // we keep a single global listener instead of branching.
115
139
  $effect(() => {
116
140
  if (typeof window === 'undefined') return;
117
- const onResize = () => clampToViewport();
141
+ const onResize = () => clamp();
118
142
  window.addEventListener('resize', onResize);
119
143
  return () => window.removeEventListener('resize', onResize);
120
144
  });
121
145
 
146
+ // Parent mode: watch the parent element's size too. A flex/grid row
147
+ // resizing around us wouldn't trigger `window.resize`, so without this
148
+ // the window can end up stranded outside a shrunken container until the
149
+ // user nudges it.
150
+ $effect(() => {
151
+ if (typeof window === 'undefined') return;
152
+ if (constrained !== 'parent' || !rootEl) return;
153
+ const parent = rootEl.parentElement;
154
+ if (!parent) return;
155
+ const ro = new ResizeObserver(() => clamp());
156
+ ro.observe(parent);
157
+ return () => ro.disconnect();
158
+ });
159
+
122
160
  // The neodrag `position` plugin takes a `current: {x, y}` and forces
123
161
  // the element to that transform. Wrapped in a Compartment so updates
124
162
  // to `pos` re-run just this plugin (not the whole plugin list).
@@ -132,7 +170,7 @@
132
170
  onDrag: ({ offset }) => {
133
171
  pos = { x: offset.x, y: offset.y };
134
172
  },
135
- onDragEnd: () => clampToViewport()
173
+ onDragEnd: () => clamp()
136
174
  }),
137
175
  ...(constrained === 'viewport' ? [bounds(BoundsFrom.viewport())] : []),
138
176
  ...(constrained === 'parent' ? [bounds(BoundsFrom.parent())] : []),