svelte-p5-components 0.2.1 → 0.4.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,129 @@
1
+ <script lang="ts">
2
+ import type { Snippet } from 'svelte';
3
+
4
+ /**
5
+ * Layout shell for canvas-driven apps.
6
+ *
7
+ * Every multi-panel p5 app ends up with the same picture: a canvas filling
8
+ * the bulk of the viewport, one or more toolbars anchored to its edges,
9
+ * and overlays (tooltips, FPS readouts, legends) painted on top. Each app
10
+ * reinvents the flex math, the ResizeObserver coordination (so the canvas
11
+ * doesn't thrash when a toolbar's height changes), and the z-index/pointer
12
+ * discipline for overlays that must *not* swallow canvas input.
13
+ *
14
+ * `<CanvasFrame>` is that shell. It exposes snippet slots for each region
15
+ * and otherwise stays out of the way:
16
+ *
17
+ * - `canvas` (required): render your `<Sketch>` / `<P5Canvas>` here.
18
+ * - `top` / `bottom` / `leftRail` / `rightRail` (optional): chrome docked
19
+ * to each edge. Natural content height/width; does not fight the canvas
20
+ * for flex space.
21
+ * - `overlay` (optional): absolutely positioned over the canvas region.
22
+ * `pointer-events: none` by default so it doesn't block canvas input;
23
+ * individual children can opt in with `pointer-events: auto`.
24
+ *
25
+ * The frame itself fills whatever parent it's mounted in — typically
26
+ * `height: 100vh` or a flex child. It assumes a bounded height from the
27
+ * parent; inside it, the canvas region flexes to consume whatever remains
28
+ * after the toolbars.
29
+ *
30
+ * @example
31
+ * ```svelte
32
+ * <div style="height: 100vh">
33
+ * <CanvasFrame>
34
+ * {#snippet top()}<MyMenuBar />{/snippet}
35
+ * {#snippet bottom()}<MyTimeline /><MySpeakerList />{/snippet}
36
+ * {#snippet canvas()}<Sketch {sketch} bind:instance />{/snippet}
37
+ * {#snippet overlay()}<FPSMonitor {instance} />{/snippet}
38
+ * </CanvasFrame>
39
+ * </div>
40
+ * ```
41
+ */
42
+ interface Props {
43
+ canvas: Snippet;
44
+ top?: Snippet;
45
+ bottom?: Snippet;
46
+ leftRail?: Snippet;
47
+ rightRail?: Snippet;
48
+ overlay?: Snippet;
49
+ class?: string;
50
+ style?: string;
51
+ }
52
+
53
+ let {
54
+ canvas,
55
+ top,
56
+ bottom,
57
+ leftRail,
58
+ rightRail,
59
+ overlay,
60
+ class: className = '',
61
+ style
62
+ }: Props = $props();
63
+ </script>
64
+
65
+ <div class="canvas-frame {className}" {style}>
66
+ {#if top}
67
+ <div class="canvas-frame__region canvas-frame__top">{@render top()}</div>
68
+ {/if}
69
+ <div class="canvas-frame__middle">
70
+ {#if leftRail}
71
+ <div class="canvas-frame__region canvas-frame__rail">{@render leftRail()}</div>
72
+ {/if}
73
+ <div class="canvas-frame__stage">
74
+ {@render canvas()}
75
+ {#if overlay}
76
+ <div class="canvas-frame__overlay">{@render overlay()}</div>
77
+ {/if}
78
+ </div>
79
+ {#if rightRail}
80
+ <div class="canvas-frame__region canvas-frame__rail">{@render rightRail()}</div>
81
+ {/if}
82
+ </div>
83
+ {#if bottom}
84
+ <div class="canvas-frame__region canvas-frame__bottom">{@render bottom()}</div>
85
+ {/if}
86
+ </div>
87
+
88
+ <style>
89
+ .canvas-frame {
90
+ display: flex;
91
+ flex-direction: column;
92
+ width: 100%;
93
+ height: 100%;
94
+ min-width: 0;
95
+ min-height: 0;
96
+ overflow: hidden;
97
+ position: relative;
98
+ }
99
+
100
+ .canvas-frame__region {
101
+ flex: 0 0 auto;
102
+ }
103
+
104
+ .canvas-frame__middle {
105
+ display: flex;
106
+ flex: 1 1 auto;
107
+ min-width: 0;
108
+ min-height: 0;
109
+ }
110
+
111
+ .canvas-frame__rail {
112
+ min-height: 0;
113
+ }
114
+
115
+ .canvas-frame__stage {
116
+ flex: 1 1 auto;
117
+ min-width: 0;
118
+ min-height: 0;
119
+ position: relative;
120
+ overflow: hidden;
121
+ }
122
+
123
+ .canvas-frame__overlay {
124
+ position: absolute;
125
+ inset: 0;
126
+ pointer-events: none;
127
+ z-index: 10;
128
+ }
129
+ </style>
@@ -0,0 +1,52 @@
1
+ import type { Snippet } from 'svelte';
2
+ /**
3
+ * Layout shell for canvas-driven apps.
4
+ *
5
+ * Every multi-panel p5 app ends up with the same picture: a canvas filling
6
+ * the bulk of the viewport, one or more toolbars anchored to its edges,
7
+ * and overlays (tooltips, FPS readouts, legends) painted on top. Each app
8
+ * reinvents the flex math, the ResizeObserver coordination (so the canvas
9
+ * doesn't thrash when a toolbar's height changes), and the z-index/pointer
10
+ * discipline for overlays that must *not* swallow canvas input.
11
+ *
12
+ * `<CanvasFrame>` is that shell. It exposes snippet slots for each region
13
+ * and otherwise stays out of the way:
14
+ *
15
+ * - `canvas` (required): render your `<Sketch>` / `<P5Canvas>` here.
16
+ * - `top` / `bottom` / `leftRail` / `rightRail` (optional): chrome docked
17
+ * to each edge. Natural content height/width; does not fight the canvas
18
+ * for flex space.
19
+ * - `overlay` (optional): absolutely positioned over the canvas region.
20
+ * `pointer-events: none` by default so it doesn't block canvas input;
21
+ * individual children can opt in with `pointer-events: auto`.
22
+ *
23
+ * The frame itself fills whatever parent it's mounted in — typically
24
+ * `height: 100vh` or a flex child. It assumes a bounded height from the
25
+ * parent; inside it, the canvas region flexes to consume whatever remains
26
+ * after the toolbars.
27
+ *
28
+ * @example
29
+ * ```svelte
30
+ * <div style="height: 100vh">
31
+ * <CanvasFrame>
32
+ * {#snippet top()}<MyMenuBar />{/snippet}
33
+ * {#snippet bottom()}<MyTimeline /><MySpeakerList />{/snippet}
34
+ * {#snippet canvas()}<Sketch {sketch} bind:instance />{/snippet}
35
+ * {#snippet overlay()}<FPSMonitor {instance} />{/snippet}
36
+ * </CanvasFrame>
37
+ * </div>
38
+ * ```
39
+ */
40
+ interface Props {
41
+ canvas: Snippet;
42
+ top?: Snippet;
43
+ bottom?: Snippet;
44
+ leftRail?: Snippet;
45
+ rightRail?: Snippet;
46
+ overlay?: Snippet;
47
+ class?: string;
48
+ style?: string;
49
+ }
50
+ declare const CanvasFrame: import("svelte").Component<Props, {}, "">;
51
+ type CanvasFrame = ReturnType<typeof CanvasFrame>;
52
+ export default CanvasFrame;
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,60 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import { render } from '@testing-library/svelte';
3
+ import { createRawSnippet } from 'svelte';
4
+ import CanvasFrame from './CanvasFrame.svelte';
5
+ const snippet = (html) => createRawSnippet(() => ({ render: () => html }));
6
+ describe('<CanvasFrame>', () => {
7
+ it('renders the required canvas snippet inside the stage', () => {
8
+ const { container } = render(CanvasFrame, {
9
+ canvas: snippet('<div data-testid="canvas">CANVAS</div>')
10
+ });
11
+ const stage = container.querySelector('.canvas-frame__stage');
12
+ expect(stage).not.toBeNull();
13
+ expect(stage?.textContent).toContain('CANVAS');
14
+ });
15
+ it('omits optional regions when their snippets are absent', () => {
16
+ const { container } = render(CanvasFrame, {
17
+ canvas: snippet('<div>c</div>')
18
+ });
19
+ expect(container.querySelector('.canvas-frame__top')).toBeNull();
20
+ expect(container.querySelector('.canvas-frame__bottom')).toBeNull();
21
+ expect(container.querySelector('.canvas-frame__rail')).toBeNull();
22
+ expect(container.querySelector('.canvas-frame__overlay')).toBeNull();
23
+ });
24
+ it('renders top, bottom, leftRail, rightRail when provided', () => {
25
+ const { container } = render(CanvasFrame, {
26
+ canvas: snippet('<div>c</div>'),
27
+ top: snippet('<span>T</span>'),
28
+ bottom: snippet('<span>B</span>'),
29
+ leftRail: snippet('<span>L</span>'),
30
+ rightRail: snippet('<span>R</span>')
31
+ });
32
+ expect(container.querySelector('.canvas-frame__top')?.textContent).toBe('T');
33
+ expect(container.querySelector('.canvas-frame__bottom')?.textContent).toBe('B');
34
+ const rails = container.querySelectorAll('.canvas-frame__rail');
35
+ expect(rails).toHaveLength(2);
36
+ expect(rails[0]?.textContent).toBe('L');
37
+ expect(rails[1]?.textContent).toBe('R');
38
+ });
39
+ it('overlay region renders with the dedicated class that applies pointer-events:none', () => {
40
+ // happy-dom doesn't resolve Svelte's scoped stylesheet cascade, so we
41
+ // assert on the marker class instead of the computed value. The actual
42
+ // CSS is covered by a Playwright smoke pass on docs/examples.
43
+ const { container } = render(CanvasFrame, {
44
+ canvas: snippet('<div>c</div>'),
45
+ overlay: snippet('<div>o</div>')
46
+ });
47
+ const overlay = container.querySelector('.canvas-frame__overlay');
48
+ expect(overlay).not.toBeNull();
49
+ });
50
+ it('forwards class and style to the root container', () => {
51
+ const { container } = render(CanvasFrame, {
52
+ canvas: snippet('<div>c</div>'),
53
+ class: 'extra-class',
54
+ style: 'background: red;'
55
+ });
56
+ const root = container.querySelector('.canvas-frame');
57
+ expect(root?.classList.contains('extra-class')).toBe(true);
58
+ expect(root.style.background).toBe('red');
59
+ });
60
+ });
@@ -56,14 +56,14 @@
56
56
  return typeof v === 'number' ? `${v}px` : v;
57
57
  }
58
58
 
59
- // Focus management each window gets a reactive z-index slot shared
59
+ // Focus management - each window gets a reactive z-index slot shared
60
60
  // across every DraggableWindow on the page. Clicking brings it to top.
61
61
  const win = registerWindow();
62
62
 
63
63
  // Live position. Neodrag's `position` plugin drives the element via
64
64
  // transform; we update this state from onDrag AND on resize/clamp,
65
65
  // and the Compartment re-evaluates to push changes back into neodrag.
66
- // `untrack` prevents these props from forming a reactive dep they're
66
+ // `untrack` prevents these props from forming a reactive dep - they're
67
67
  // *initial* values only; later position comes from user drag.
68
68
  let pos = $state(untrack(() => ({ x: initialX, y: initialY })));
69
69
 
@@ -74,9 +74,13 @@
74
74
 
75
75
  function clampToViewport() {
76
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;
77
81
  const vw = window.innerWidth;
78
82
  const vh = window.innerHeight;
79
- // Never allow the titlebar above the viewport top that's the
83
+ // Never allow the titlebar above the viewport top - that's the
80
84
  // "unreachable" case. Allow partial horizontal overflow so narrow
81
85
  // screens don't trap wide windows, but always keep `minVisible`
82
86
  // pixels of the window on-screen for a grab handle.
@@ -90,7 +94,7 @@
90
94
  };
91
95
  }
92
96
 
93
- // Keep size fresh (used for clamp math) ResizeObserver on our own
97
+ // Keep size fresh (used for clamp math) - ResizeObserver on our own
94
98
  // root element catches CSS resize-from-corner too.
95
99
  $effect(() => {
96
100
  if (!rootEl) return;
@@ -106,7 +110,7 @@
106
110
  return () => ro.disconnect();
107
111
  });
108
112
 
109
- // Re-clamp on window resize this is the "snap back in when the
113
+ // Re-clamp on window resize - this is the "snap back in when the
110
114
  // viewport shrinks" case.
111
115
  $effect(() => {
112
116
  if (typeof window === 'undefined') return;
@@ -139,6 +143,8 @@
139
143
  <div
140
144
  bind:this={rootEl}
141
145
  class="draggable-window"
146
+ class:dw-fixed={constrained !== 'parent'}
147
+ class:dw-absolute={constrained === 'parent'}
142
148
  style:width={toCssSize(width)}
143
149
  style:height={toCssSize(height)}
144
150
  style:min-width={toCssSize(minWidth)}
@@ -162,7 +168,6 @@
162
168
 
163
169
  <style>
164
170
  .draggable-window {
165
- position: fixed;
166
171
  top: 0;
167
172
  left: 0;
168
173
  display: flex;
@@ -177,6 +182,16 @@
177
182
  /* Native resize from the bottom-right corner. */
178
183
  resize: both;
179
184
  }
185
+ /* Default: float above the viewport (suits constrained="viewport" or "none"). */
186
+ .dw-fixed {
187
+ position: fixed;
188
+ }
189
+ /* When constrained to a parent, position absolute inside that parent so
190
+ the visual position matches the drag bounds. The parent must be a
191
+ positioning context (`position: relative` or similar). */
192
+ .dw-absolute {
193
+ position: absolute;
194
+ }
180
195
  .dw-titlebar {
181
196
  display: flex;
182
197
  align-items: center;
@@ -0,0 +1,269 @@
1
+ <script lang="ts" module>
2
+ /**
3
+ * A single entity (speaker, actor, track, class, series, etc.) rendered
4
+ * as a toggle button in the list. Consumers supply a flat array; optional
5
+ * `group` buckets entities into labeled sections.
6
+ */
7
+ export interface Entity {
8
+ /** Stable id. Used in onToggle/onColorChange callbacks. */
9
+ id: string;
10
+ /** Human-readable label shown on the button. */
11
+ label: string;
12
+ /** Color swatch. Any CSS color string. */
13
+ color: string;
14
+ /** Default: true. When false, the entity renders dimmed. */
15
+ visible?: boolean;
16
+ /** Optional group label. Entities with the same `group` are rendered together. */
17
+ group?: string;
18
+ }
19
+ </script>
20
+
21
+ <script lang="ts">
22
+ // Entity visibility/color toggle panel.
23
+ //
24
+ // Canvas apps that render N separately-colored series (speakers,
25
+ // actors, clusters, channels) nearly always grow a panel like this:
26
+ // a row of buttons with a colored swatch and a label, each toggling
27
+ // the corresponding entity's visibility in the viz. Optional color
28
+ // editing. Optional grouping (family, cluster). Optional overflow
29
+ // expander once the list exceeds a threshold.
30
+ //
31
+ // The list binds `entities[].visible` back via callbacks rather than
32
+ // by mutating the prop in place — consumers keep their own authoritative
33
+ // state and this component stays stateless.
34
+ //
35
+ // See the package README for a usage example.
36
+
37
+ interface Props {
38
+ /** The list of entities. */
39
+ entities: Entity[];
40
+ /** Called when a visibility toggle is clicked. */
41
+ onToggle?: (id: string, visible: boolean) => void;
42
+ /** When provided, a native color input is shown and clicking the swatch opens it. */
43
+ onColorChange?: (id: string, color: string) => void;
44
+ /** Max entities to show before collapsing the remainder behind a "+N more" expander. */
45
+ maxVisible?: number;
46
+ /** Optional heading above the list. */
47
+ heading?: string;
48
+ class?: string;
49
+ }
50
+
51
+ let {
52
+ entities,
53
+ onToggle,
54
+ onColorChange,
55
+ maxVisible,
56
+ heading,
57
+ class: className = ''
58
+ }: Props = $props();
59
+
60
+ let expanded = $state(false);
61
+
62
+ const visibleEntities = $derived(
63
+ expanded || maxVisible === undefined ? entities : entities.slice(0, maxVisible)
64
+ );
65
+
66
+ const groupKeys = $derived.by(() => {
67
+ const keys: string[] = [];
68
+ for (const e of visibleEntities) {
69
+ const k = e.group ?? '';
70
+ if (!keys.includes(k)) keys.push(k);
71
+ }
72
+ return keys;
73
+ });
74
+
75
+ const hasGroupBoundaries = $derived(groupKeys.some((k) => k !== ''));
76
+
77
+ const hiddenCount = $derived(
78
+ maxVisible === undefined || expanded ? 0 : Math.max(0, entities.length - maxVisible)
79
+ );
80
+
81
+ function entitiesInGroup(key: string): Entity[] {
82
+ return visibleEntities.filter((e) => (e.group ?? '') === key);
83
+ }
84
+
85
+ function isVisible(e: Entity) {
86
+ return e.visible !== false;
87
+ }
88
+
89
+ function handleToggle(e: Entity) {
90
+ onToggle?.(e.id, !isVisible(e));
91
+ }
92
+
93
+ function handleColorInput(e: Entity, event: Event) {
94
+ const target = event.currentTarget as HTMLInputElement;
95
+ onColorChange?.(e.id, target.value);
96
+ }
97
+ </script>
98
+
99
+ <div class="entity-toggle-list {className}" data-has-groups={hasGroupBoundaries}>
100
+ {#if heading}
101
+ <div class="entity-toggle-list__heading">{heading}</div>
102
+ {/if}
103
+
104
+ {#each groupKeys as key (key)}
105
+ {#if hasGroupBoundaries && key}
106
+ <div class="entity-toggle-list__group-label">{key}</div>
107
+ {/if}
108
+ <div class="entity-toggle-list__row">
109
+ {#each entitiesInGroup(key) as entity (entity.id)}
110
+ <div class="entity-toggle-list__item" class:is-hidden={!isVisible(entity)}>
111
+ {#if onColorChange}
112
+ <label
113
+ class="entity-toggle-list__swatch-wrap"
114
+ aria-label="Change color for {entity.label}"
115
+ >
116
+ <span class="entity-toggle-list__swatch" style:background-color={entity.color}></span>
117
+ <input
118
+ type="color"
119
+ class="entity-toggle-list__color-input"
120
+ value={entity.color}
121
+ oninput={(e) => handleColorInput(entity, e)}
122
+ />
123
+ </label>
124
+ {:else}
125
+ <span
126
+ class="entity-toggle-list__swatch"
127
+ style:background-color={entity.color}
128
+ aria-hidden="true"
129
+ ></span>
130
+ {/if}
131
+ <button
132
+ type="button"
133
+ class="entity-toggle-list__label"
134
+ aria-pressed={isVisible(entity)}
135
+ title={isVisible(entity) ? `Hide ${entity.label}` : `Show ${entity.label}`}
136
+ onclick={() => handleToggle(entity)}
137
+ >
138
+ {entity.label}
139
+ </button>
140
+ </div>
141
+ {/each}
142
+ </div>
143
+ {/each}
144
+
145
+ {#if hiddenCount > 0}
146
+ <button
147
+ type="button"
148
+ class="entity-toggle-list__expand"
149
+ onclick={() => (expanded = true)}
150
+ title="Show {hiddenCount} more"
151
+ >
152
+ +{hiddenCount} more
153
+ </button>
154
+ {:else if expanded && maxVisible !== undefined && entities.length > maxVisible}
155
+ <button
156
+ type="button"
157
+ class="entity-toggle-list__expand"
158
+ onclick={() => (expanded = false)}
159
+ title="Collapse"
160
+ >
161
+ show less
162
+ </button>
163
+ {/if}
164
+ </div>
165
+
166
+ <style>
167
+ .entity-toggle-list {
168
+ display: flex;
169
+ flex-direction: column;
170
+ gap: 6px;
171
+ font:
172
+ 12px/1.3 system-ui,
173
+ -apple-system,
174
+ Segoe UI,
175
+ sans-serif;
176
+
177
+ --entity-swatch-size: 12px;
178
+ --entity-btn-bg: transparent;
179
+ --entity-btn-bg-hover: rgba(0, 0, 0, 0.05);
180
+ --entity-btn-fg: #111;
181
+ --entity-btn-fg-hidden: #9ca3af;
182
+ --entity-heading-fg: #6b7280;
183
+ }
184
+
185
+ .entity-toggle-list__heading,
186
+ .entity-toggle-list__group-label {
187
+ font-size: 11px;
188
+ font-weight: 600;
189
+ text-transform: uppercase;
190
+ letter-spacing: 0.04em;
191
+ color: var(--entity-heading-fg);
192
+ }
193
+
194
+ .entity-toggle-list__row {
195
+ display: flex;
196
+ flex-wrap: wrap;
197
+ gap: 4px 8px;
198
+ align-items: center;
199
+ }
200
+
201
+ .entity-toggle-list__item {
202
+ display: inline-flex;
203
+ align-items: center;
204
+ gap: 6px;
205
+ padding: 2px 6px 2px 4px;
206
+ border-radius: 4px;
207
+ background: var(--entity-btn-bg);
208
+ }
209
+
210
+ .entity-toggle-list__item:hover {
211
+ background: var(--entity-btn-bg-hover);
212
+ }
213
+
214
+ .entity-toggle-list__item.is-hidden .entity-toggle-list__label {
215
+ color: var(--entity-btn-fg-hidden);
216
+ text-decoration: line-through;
217
+ }
218
+
219
+ .entity-toggle-list__item.is-hidden .entity-toggle-list__swatch {
220
+ opacity: 0.3;
221
+ }
222
+
223
+ .entity-toggle-list__swatch {
224
+ display: inline-block;
225
+ width: var(--entity-swatch-size);
226
+ height: var(--entity-swatch-size);
227
+ border-radius: 50%;
228
+ border: 1px solid rgba(0, 0, 0, 0.15);
229
+ flex: 0 0 auto;
230
+ }
231
+
232
+ .entity-toggle-list__swatch-wrap {
233
+ position: relative;
234
+ display: inline-flex;
235
+ cursor: pointer;
236
+ }
237
+
238
+ .entity-toggle-list__color-input {
239
+ position: absolute;
240
+ inset: 0;
241
+ opacity: 0;
242
+ cursor: pointer;
243
+ width: 100%;
244
+ height: 100%;
245
+ }
246
+
247
+ .entity-toggle-list__label {
248
+ background: none;
249
+ border: none;
250
+ padding: 0;
251
+ margin: 0;
252
+ font: inherit;
253
+ color: var(--entity-btn-fg);
254
+ cursor: pointer;
255
+ text-align: left;
256
+ }
257
+
258
+ .entity-toggle-list__expand {
259
+ align-self: flex-start;
260
+ background: none;
261
+ border: none;
262
+ padding: 0;
263
+ color: var(--entity-heading-fg);
264
+ font: inherit;
265
+ font-size: 11px;
266
+ text-decoration: underline;
267
+ cursor: pointer;
268
+ }
269
+ </style>
@@ -0,0 +1,33 @@
1
+ /**
2
+ * A single entity (speaker, actor, track, class, series, etc.) rendered
3
+ * as a toggle button in the list. Consumers supply a flat array; optional
4
+ * `group` buckets entities into labeled sections.
5
+ */
6
+ export interface Entity {
7
+ /** Stable id. Used in onToggle/onColorChange callbacks. */
8
+ id: string;
9
+ /** Human-readable label shown on the button. */
10
+ label: string;
11
+ /** Color swatch. Any CSS color string. */
12
+ color: string;
13
+ /** Default: true. When false, the entity renders dimmed. */
14
+ visible?: boolean;
15
+ /** Optional group label. Entities with the same `group` are rendered together. */
16
+ group?: string;
17
+ }
18
+ interface Props {
19
+ /** The list of entities. */
20
+ entities: Entity[];
21
+ /** Called when a visibility toggle is clicked. */
22
+ onToggle?: (id: string, visible: boolean) => void;
23
+ /** When provided, a native color input is shown and clicking the swatch opens it. */
24
+ onColorChange?: (id: string, color: string) => void;
25
+ /** Max entities to show before collapsing the remainder behind a "+N more" expander. */
26
+ maxVisible?: number;
27
+ /** Optional heading above the list. */
28
+ heading?: string;
29
+ class?: string;
30
+ }
31
+ declare const EntityToggleList: import("svelte").Component<Props, {}, "">;
32
+ type EntityToggleList = ReturnType<typeof EntityToggleList>;
33
+ export default EntityToggleList;
@@ -0,0 +1 @@
1
+ export {};