svelte-flexiboards 0.3.2 → 0.4.1

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 (49) hide show
  1. package/dist/components/flexi-add.svelte +2 -15
  2. package/dist/components/flexi-add.svelte.d.ts +0 -12
  3. package/dist/components/flexi-delete.svelte +3 -16
  4. package/dist/components/flexi-delete.svelte.d.ts +0 -12
  5. package/dist/components/flexi-grab.svelte +2 -2
  6. package/dist/components/flexi-target.svelte +8 -19
  7. package/dist/components/flexi-widget.svelte +2 -2
  8. package/dist/components/responsive-flexi-board.svelte +83 -0
  9. package/dist/components/responsive-flexi-board.svelte.d.ts +34 -0
  10. package/dist/index.d.ts +4 -1
  11. package/dist/index.js +4 -1
  12. package/dist/system/board/base.svelte.d.ts +15 -0
  13. package/dist/system/board/controller.svelte.d.ts +26 -4
  14. package/dist/system/board/controller.svelte.js +237 -28
  15. package/dist/system/board/types.d.ts +26 -0
  16. package/dist/system/grid/base.svelte.d.ts +9 -0
  17. package/dist/system/grid/base.svelte.js +12 -1
  18. package/dist/system/grid/flow-grid.svelte.js +105 -36
  19. package/dist/system/grid/free-grid.svelte.d.ts +6 -2
  20. package/dist/system/grid/free-grid.svelte.js +139 -20
  21. package/dist/system/misc/deleter.svelte.d.ts +0 -4
  22. package/dist/system/misc/deleter.svelte.js +1 -6
  23. package/dist/system/portal.js +0 -1
  24. package/dist/system/responsive/base.svelte.d.ts +46 -0
  25. package/dist/system/responsive/base.svelte.js +1 -0
  26. package/dist/system/responsive/controller.svelte.d.ts +78 -0
  27. package/dist/system/responsive/controller.svelte.js +264 -0
  28. package/dist/system/responsive/index.d.ts +16 -0
  29. package/dist/system/responsive/index.js +36 -0
  30. package/dist/system/responsive/types.d.ts +56 -0
  31. package/dist/system/responsive/types.js +1 -0
  32. package/dist/system/shared/event-bus.d.ts +3 -1
  33. package/dist/system/shared/utils.svelte.d.ts +2 -0
  34. package/dist/system/shared/utils.svelte.js +39 -22
  35. package/dist/system/target/controller.svelte.d.ts +7 -2
  36. package/dist/system/target/controller.svelte.js +103 -30
  37. package/dist/system/types.d.ts +13 -2
  38. package/dist/system/widget/base.svelte.d.ts +40 -1
  39. package/dist/system/widget/base.svelte.js +84 -2
  40. package/dist/system/widget/controller.svelte.d.ts +4 -1
  41. package/dist/system/widget/controller.svelte.js +106 -17
  42. package/dist/system/widget/events.js +10 -3
  43. package/dist/system/widget/interpolation-utils.d.ts +14 -0
  44. package/dist/system/widget/interpolation-utils.js +32 -0
  45. package/dist/system/widget/interpolator.svelte.d.ts +2 -1
  46. package/dist/system/widget/interpolator.svelte.js +63 -22
  47. package/dist/system/widget/triggers.svelte.js +1 -1
  48. package/dist/system/widget/types.d.ts +51 -6
  49. package/package.json +1 -1
@@ -0,0 +1,78 @@
1
+ import type { FlexiLayout } from '../board/types.js';
2
+ import type { ResponsiveFlexiBoardController } from './base.svelte.js';
3
+ import type { ResponsiveFlexiBoardConfiguration, ResponsiveFlexiLayout } from './types.js';
4
+ import type { ResponsiveFlexiBoardProps } from '../../components/responsive-flexi-board.svelte';
5
+ export declare class InternalResponsiveFlexiBoardController implements ResponsiveFlexiBoardController {
6
+ #private;
7
+ config?: ResponsiveFlexiBoardConfiguration;
8
+ /**
9
+ * The currently active breakpoint key.
10
+ * Determined by checking MediaQuery matches in descending order.
11
+ * Falls back to 'default' if no breakpoint matches.
12
+ */
13
+ currentBreakpoint: string;
14
+ constructor(props: ResponsiveFlexiBoardProps);
15
+ /**
16
+ * Gets the layout for the current breakpoint.
17
+ * Falls back to 'default' if no layout exists for the current breakpoint.
18
+ */
19
+ getLayoutForCurrentBreakpoint(): FlexiLayout | undefined;
20
+ /**
21
+ * Updates the layout for the current breakpoint.
22
+ * Called when the active FlexiBoard's layout changes.
23
+ */
24
+ setLayoutForCurrentBreakpoint(layout: FlexiLayout): void;
25
+ /**
26
+ * Gets the layout for a specific breakpoint.
27
+ */
28
+ getLayoutForBreakpoint(breakpoint: string): FlexiLayout | undefined;
29
+ /**
30
+ * Sets the layout for a specific breakpoint.
31
+ */
32
+ setLayoutForBreakpoint(breakpoint: string, layout: FlexiLayout): void;
33
+ /**
34
+ * Checks if a layout exists for a specific breakpoint.
35
+ */
36
+ hasLayoutForBreakpoint(breakpoint: string): boolean;
37
+ /**
38
+ * Imports all responsive layouts.
39
+ * Replaces any existing stored layouts.
40
+ */
41
+ importLayout(layout: ResponsiveFlexiLayout): void;
42
+ /**
43
+ * Exports all responsive layouts.
44
+ * Returns a copy of the stored layouts.
45
+ */
46
+ exportLayout(): ResponsiveFlexiLayout;
47
+ /**
48
+ * Returns all breakpoint keys that have stored layouts.
49
+ */
50
+ get definedBreakpoints(): string[];
51
+ /**
52
+ * Returns all configured breakpoint keys (from config).
53
+ */
54
+ get configuredBreakpoints(): string[];
55
+ /**
56
+ * Returns the sorted breakpoints (largest first, excluding 'default').
57
+ */
58
+ get sortedBreakpoints(): [string, number][];
59
+ /**
60
+ * Called when the responsive board is ready to load layouts.
61
+ * Triggers initial layout loading from the `loadLayouts` config.
62
+ */
63
+ oninitialloadcomplete(): void;
64
+ /**
65
+ * Whether the responsive board has completed initial loading.
66
+ */
67
+ get ready(): boolean;
68
+ /**
69
+ * Whether layouts have been imported via importLayout() or received from child boards.
70
+ * Child boards check this to know whether to auto-load from stored layouts
71
+ * or use their loadLayout callback.
72
+ */
73
+ get hasStoredLayouts(): boolean;
74
+ /**
75
+ * Cleanup method to be called when the responsive board is destroyed.
76
+ */
77
+ destroy(): void;
78
+ }
@@ -0,0 +1,264 @@
1
+ import { MediaQuery, SvelteMap } from 'svelte/reactivity';
2
+ import { getFlexiEventBus } from '../shared/event-bus.js';
3
+ const DEFAULT_BREAKPOINT = 'default';
4
+ export class InternalResponsiveFlexiBoardController {
5
+ #rawProps = $state(undefined);
6
+ config = $derived(this.#rawProps?.config);
7
+ /**
8
+ * Stored layouts for all breakpoints.
9
+ * This is the source of truth - active board syncs to/from this.
10
+ */
11
+ #storedLayouts = $state({});
12
+ /**
13
+ * Whether initial layout loading has completed.
14
+ */
15
+ #ready = false;
16
+ /**
17
+ * Debounce timer for layout change callbacks.
18
+ */
19
+ #layoutChangeTimeout = null;
20
+ #layoutChangeDebounceMs = 150;
21
+ /**
22
+ * Whether initial layouts have been imported via importLayout().
23
+ * Used by child boards to determine whether to load from stored layouts or loadLayout callback.
24
+ */
25
+ #hasStoredLayouts = false;
26
+ /**
27
+ * Event bus for communication between controllers.
28
+ */
29
+ #eventBus;
30
+ /**
31
+ * Cleanup functions for event subscriptions.
32
+ */
33
+ #unsubscribers = [];
34
+ /**
35
+ * MediaQuery instances for each breakpoint.
36
+ * Created lazily when breakpoints config changes.
37
+ */
38
+ #mediaQueries = new SvelteMap();
39
+ /**
40
+ * Breakpoints sorted in descending order by min-width.
41
+ * Largest breakpoint first, so we match the largest applicable one.
42
+ */
43
+ #sortedBreakpoints = $derived.by(() => {
44
+ const breakpoints = this.config?.breakpoints ?? {};
45
+ return Object.entries(breakpoints)
46
+ .filter(([key]) => key !== DEFAULT_BREAKPOINT)
47
+ .sort((a, b) => b[1] - a[1]);
48
+ });
49
+ /**
50
+ * The currently active breakpoint key.
51
+ * Determined by checking MediaQuery matches in descending order.
52
+ * Falls back to 'default' if no breakpoint matches.
53
+ */
54
+ currentBreakpoint = $derived.by(() => {
55
+ // Check breakpoints in descending order (largest first)
56
+ for (const [key] of this.#sortedBreakpoints) {
57
+ const query = this.#mediaQueries.get(key);
58
+ if (query?.current) {
59
+ return key;
60
+ }
61
+ }
62
+ return DEFAULT_BREAKPOINT;
63
+ });
64
+ /**
65
+ * Previous breakpoint for detecting changes.
66
+ */
67
+ #previousBreakpoint = DEFAULT_BREAKPOINT;
68
+ constructor(props) {
69
+ this.#rawProps = props;
70
+ this.#eventBus = getFlexiEventBus();
71
+ // Subscribe to board layout changes
72
+ this.#unsubscribers.push(this.#eventBus.subscribe('board:layoutchange', this.#onBoardLayoutChange.bind(this)));
73
+ // Initialize media queries when breakpoints config changes
74
+ $effect(() => {
75
+ this.#initializeMediaQueries();
76
+ });
77
+ // Detect and handle breakpoint changes
78
+ $effect(() => {
79
+ const current = this.currentBreakpoint;
80
+ const previous = this.#previousBreakpoint;
81
+ if (previous !== current) {
82
+ this.#onBreakpointChange(previous, current);
83
+ this.#previousBreakpoint = current;
84
+ }
85
+ });
86
+ }
87
+ /**
88
+ * Handles layout change events from child boards.
89
+ */
90
+ #onBoardLayoutChange(event) {
91
+ // Only handle events from boards under our control
92
+ if (event.board.responsiveController !== this) {
93
+ return;
94
+ }
95
+ // Store the layout for this breakpoint
96
+ if (event.breakpoint) {
97
+ this.#storedLayouts[event.breakpoint] = event.layout;
98
+ this.#hasStoredLayouts = true;
99
+ this.#notifyLayoutChange();
100
+ }
101
+ }
102
+ /**
103
+ * Creates MediaQuery instances for each configured breakpoint.
104
+ */
105
+ #initializeMediaQueries() {
106
+ const breakpoints = this.config?.breakpoints ?? {};
107
+ // Clear existing queries
108
+ this.#mediaQueries.clear();
109
+ // Create min-width queries for each breakpoint (except 'default')
110
+ for (const [key, minWidth] of Object.entries(breakpoints)) {
111
+ if (key === DEFAULT_BREAKPOINT) {
112
+ continue;
113
+ }
114
+ this.#mediaQueries.set(key, new MediaQuery(`(min-width: ${minWidth}px)`, false));
115
+ }
116
+ }
117
+ /**
118
+ * Called when the active breakpoint changes.
119
+ */
120
+ #onBreakpointChange(oldBreakpoint, newBreakpoint) {
121
+ this.config?.onBreakpointChange?.(newBreakpoint, oldBreakpoint);
122
+ }
123
+ // =========================================================================
124
+ // Layout access for the active breakpoint (used by component)
125
+ // =========================================================================
126
+ /**
127
+ * Gets the layout for the current breakpoint.
128
+ * Falls back to 'default' if no layout exists for the current breakpoint.
129
+ */
130
+ getLayoutForCurrentBreakpoint() {
131
+ return this.#storedLayouts[this.currentBreakpoint] ?? this.#storedLayouts[DEFAULT_BREAKPOINT];
132
+ }
133
+ /**
134
+ * Updates the layout for the current breakpoint.
135
+ * Called when the active FlexiBoard's layout changes.
136
+ */
137
+ setLayoutForCurrentBreakpoint(layout) {
138
+ this.#storedLayouts[this.currentBreakpoint] = layout;
139
+ this.#notifyLayoutChange();
140
+ }
141
+ /**
142
+ * Debounced notification of layout changes.
143
+ */
144
+ #notifyLayoutChange() {
145
+ if (this.#layoutChangeTimeout) {
146
+ clearTimeout(this.#layoutChangeTimeout);
147
+ }
148
+ this.#layoutChangeTimeout = setTimeout(() => {
149
+ this.#layoutChangeTimeout = null;
150
+ this.config?.onLayoutsChange?.(this.exportLayout());
151
+ }, this.#layoutChangeDebounceMs);
152
+ }
153
+ // =========================================================================
154
+ // Per-breakpoint layout access
155
+ // =========================================================================
156
+ /**
157
+ * Gets the layout for a specific breakpoint.
158
+ */
159
+ getLayoutForBreakpoint(breakpoint) {
160
+ return this.#storedLayouts[breakpoint];
161
+ }
162
+ /**
163
+ * Sets the layout for a specific breakpoint.
164
+ */
165
+ setLayoutForBreakpoint(breakpoint, layout) {
166
+ this.#storedLayouts[breakpoint] = layout;
167
+ }
168
+ /**
169
+ * Checks if a layout exists for a specific breakpoint.
170
+ */
171
+ hasLayoutForBreakpoint(breakpoint) {
172
+ return breakpoint in this.#storedLayouts;
173
+ }
174
+ // =========================================================================
175
+ // Import/Export (ResponsiveFlexiBoardController interface)
176
+ // =========================================================================
177
+ /**
178
+ * Imports all responsive layouts.
179
+ * Replaces any existing stored layouts.
180
+ */
181
+ importLayout(layout) {
182
+ this.#storedLayouts = { ...layout };
183
+ this.#hasStoredLayouts = true;
184
+ // Notify child boards to reload their layout from the stored layouts
185
+ this.#eventBus.dispatch('responsive:layoutimport', {
186
+ responsiveController: this
187
+ });
188
+ }
189
+ /**
190
+ * Exports all responsive layouts.
191
+ * Returns a copy of the stored layouts.
192
+ */
193
+ exportLayout() {
194
+ return { ...this.#storedLayouts };
195
+ }
196
+ // =========================================================================
197
+ // Utility getters
198
+ // =========================================================================
199
+ /**
200
+ * Returns all breakpoint keys that have stored layouts.
201
+ */
202
+ get definedBreakpoints() {
203
+ return Object.keys(this.#storedLayouts);
204
+ }
205
+ /**
206
+ * Returns all configured breakpoint keys (from config).
207
+ */
208
+ get configuredBreakpoints() {
209
+ return Object.keys(this.config?.breakpoints ?? {});
210
+ }
211
+ /**
212
+ * Returns the sorted breakpoints (largest first, excluding 'default').
213
+ */
214
+ get sortedBreakpoints() {
215
+ return this.#sortedBreakpoints;
216
+ }
217
+ // =========================================================================
218
+ // Lifecycle
219
+ // =========================================================================
220
+ /**
221
+ * Called when the responsive board is ready to load layouts.
222
+ * Triggers initial layout loading from the `loadLayouts` config.
223
+ */
224
+ oninitialloadcomplete() {
225
+ if (this.#ready) {
226
+ return;
227
+ }
228
+ this.#ready = true;
229
+ const loadLayoutsFn = this.config?.loadLayouts;
230
+ if (loadLayoutsFn) {
231
+ const layouts = loadLayoutsFn();
232
+ if (layouts) {
233
+ this.importLayout(layouts);
234
+ }
235
+ }
236
+ }
237
+ /**
238
+ * Whether the responsive board has completed initial loading.
239
+ */
240
+ get ready() {
241
+ return this.#ready;
242
+ }
243
+ /**
244
+ * Whether layouts have been imported via importLayout() or received from child boards.
245
+ * Child boards check this to know whether to auto-load from stored layouts
246
+ * or use their loadLayout callback.
247
+ */
248
+ get hasStoredLayouts() {
249
+ return this.#hasStoredLayouts;
250
+ }
251
+ /**
252
+ * Cleanup method to be called when the responsive board is destroyed.
253
+ */
254
+ destroy() {
255
+ this.#mediaQueries.clear();
256
+ // Clean up event subscriptions
257
+ this.#unsubscribers.forEach((unsubscribe) => unsubscribe());
258
+ this.#unsubscribers = [];
259
+ if (this.#layoutChangeTimeout) {
260
+ clearTimeout(this.#layoutChangeTimeout);
261
+ this.#layoutChangeTimeout = null;
262
+ }
263
+ }
264
+ }
@@ -0,0 +1,16 @@
1
+ import type { ResponsiveFlexiBoardController } from './base.svelte.js';
2
+ import { InternalResponsiveFlexiBoardController } from './controller.svelte.js';
3
+ import type { ResponsiveFlexiBoardProps } from '../../components/responsive-flexi-board.svelte';
4
+ import type { ResponsiveFlexiBoardConfiguration, ResponsiveFlexiLayout } from './types.js';
5
+ export declare function responsiveflexiboard(props: ResponsiveFlexiBoardProps): InternalResponsiveFlexiBoardController;
6
+ /**
7
+ * Checks if a responsive board context is available.
8
+ * @returns Whether a responsive board context is available.
9
+ */
10
+ export declare function hasInternalResponsiveFlexiboardCtx(): boolean;
11
+ /**
12
+ * Gets the current {@link ResponsiveFlexiBoardController} instance, if any.
13
+ * Throws an error if no responsive board is found.
14
+ */
15
+ export declare function getResponsiveFlexiboardCtx(): ResponsiveFlexiBoardController;
16
+ export { type ResponsiveFlexiBoardController, type ResponsiveFlexiBoardConfiguration, type ResponsiveFlexiLayout };
@@ -0,0 +1,36 @@
1
+ import { getContext, setContext } from 'svelte';
2
+ import { InternalResponsiveFlexiBoardController } from './controller.svelte.js';
3
+ const contextKey = Symbol('responsive-flexiboard');
4
+ export function responsiveflexiboard(props) {
5
+ const board = new InternalResponsiveFlexiBoardController(props);
6
+ setContext(contextKey, board);
7
+ return board;
8
+ }
9
+ /**
10
+ * Gets the current {@link InternalResponsiveFlexiBoardController} instance, if any.
11
+ * Throws an error if no responsive board is found.
12
+ * @internal
13
+ */
14
+ export function getInternalResponsiveFlexiboardCtx() {
15
+ const board = getContext(contextKey);
16
+ if (!board) {
17
+ throw new Error('Cannot get ResponsiveFlexiBoard context outside of a registered board. Ensure that responsiveflexiboard() (or <ResponsiveFlexiBoard>) is called.');
18
+ }
19
+ return board;
20
+ }
21
+ /**
22
+ * Checks if a responsive board context is available.
23
+ * @returns Whether a responsive board context is available.
24
+ */
25
+ export function hasInternalResponsiveFlexiboardCtx() {
26
+ return !!getContext(contextKey);
27
+ }
28
+ /**
29
+ * Gets the current {@link ResponsiveFlexiBoardController} instance, if any.
30
+ * Throws an error if no responsive board is found.
31
+ */
32
+ export function getResponsiveFlexiboardCtx() {
33
+ return getInternalResponsiveFlexiboardCtx();
34
+ }
35
+ /* Exports to go to root index.ts */
36
+ export {};
@@ -0,0 +1,56 @@
1
+ import type { FlexiLayout } from "../board/types.js";
2
+ /**
3
+ * A responsive layout is a map of breakpoint keys to FlexiBoard layouts.
4
+ *
5
+ * @example
6
+ * ```ts
7
+ * const layout: ResponsiveFlexiLayout = {
8
+ * lg: { "target-0": [{ type: "widget", x: 0, y: 0, width: 4, height: 2 }] },
9
+ * md: { "target-0": [{ type: "widget", x: 0, y: 0, width: 6, height: 2 }] },
10
+ * default: { "target-0": [{ type: "widget", x: 0, y: 0, width: 12, height: 2 }] },
11
+ * };
12
+ * ```
13
+ */
14
+ export type ResponsiveFlexiLayout = {
15
+ [breakpoint: string]: FlexiLayout;
16
+ };
17
+ /**
18
+ * Callback fired when any breakpoint's layout changes.
19
+ */
20
+ export type ResponsiveFlexiLayoutChangeFn = (layouts: ResponsiveFlexiLayout) => void;
21
+ /**
22
+ * Function to load initial layouts for all breakpoints.
23
+ */
24
+ export type ResponsiveFlexiLoadLayoutFn = () => ResponsiveFlexiLayout | undefined;
25
+ export type ResponsiveFlexiBoardConfiguration = {
26
+ /**
27
+ * Breakpoint definitions mapping breakpoint keys to minimum viewport widths (in pixels).
28
+ * Breakpoints are evaluated in descending order - the largest matching breakpoint wins.
29
+ * Use 'default' as the fallback when no breakpoint matches.
30
+ *
31
+ * @example
32
+ * ```ts
33
+ * breakpoints: {
34
+ * lg: 1200, // >= 1200px
35
+ * md: 900, // >= 900px
36
+ * sm: 600, // >= 600px
37
+ * // 'default' is implicit for < 600px
38
+ * }
39
+ * ```
40
+ */
41
+ breakpoints?: Record<string, number>;
42
+ /**
43
+ * Callback fired when the active breakpoint changes.
44
+ */
45
+ onBreakpointChange?: (newBreakpoint: string, oldBreakpoint: string) => void;
46
+ /**
47
+ * Callback fired when any layout changes (widget moved, resized, added, or removed).
48
+ * Receives all breakpoint layouts, including the updated current one.
49
+ */
50
+ onLayoutsChange?: ResponsiveFlexiLayoutChangeFn;
51
+ /**
52
+ * Function to load initial layouts on mount.
53
+ * Called once when the responsive board is ready.
54
+ */
55
+ loadLayouts?: ResponsiveFlexiLoadLayoutFn;
56
+ };
@@ -0,0 +1 @@
1
+ export {};
@@ -1,4 +1,4 @@
1
- import type { AdderWidgetReadyEvent, PointerMovedEvent, TargetEvent, WidgetDroppedEvent, WidgetEvent, WidgetGrabbedEvent, WidgetResizingEvent } from '../types.js';
1
+ import type { AdderWidgetReadyEvent, BoardLayoutChangeEvent, PointerMovedEvent, ResponsiveLayoutImportEvent, TargetEvent, WidgetDroppedEvent, WidgetEvent, WidgetGrabbedEvent, WidgetResizingEvent } from '../types.js';
2
2
  export interface EventMap {
3
3
  'widget:grabbed': WidgetGrabbedEvent;
4
4
  'widget:resizing': WidgetResizingEvent;
@@ -12,6 +12,8 @@ export interface EventMap {
12
12
  'widget:leavetarget': WidgetEvent;
13
13
  'adder:widgetready': AdderWidgetReadyEvent;
14
14
  'pointer:moved': PointerMovedEvent;
15
+ 'board:layoutchange': BoardLayoutChangeEvent;
16
+ 'responsive:layoutimport': ResponsiveLayoutImportEvent;
15
17
  }
16
18
  type EventListener<T> = (data: T) => void;
17
19
  export declare class FlexiEventBus {
@@ -60,6 +60,8 @@ export declare class GridDimensionTracker {
60
60
  refreshScrollListeners(): void;
61
61
  getCellFromPointerPosition(clientX: number, clientY: number): CellPosition | null;
62
62
  }
63
+ export declare function contentSize(axisCoordinates: number[], gap: number): number;
64
+ export declare function findCell(pointerLocation: number, start: number, size: number, gap: number, axisCoordinates: number[]): number;
63
65
  export declare function generateUniqueId(prefix?: string): string;
64
66
  export declare const assistiveTextStyle = "\n\tposition: absolute;\n\twidth: 1px;\n\theight: 1px;\n\tpadding: 0;\n\tmargin: -1px;\n\toverflow: hidden;\n\tclip: rect(0, 0, 0, 0);\n\twhite-space: nowrap;\n\tborder-width: 0;\n";
65
67
  export declare function getElementMidpoint(element: HTMLElement): {
@@ -48,7 +48,11 @@ export class PointerService {
48
48
  isPointerInside(element) {
49
49
  const rect = element.getBoundingClientRect();
50
50
  const { x, y } = this.#position;
51
- return x >= rect.left && x <= rect.right && y >= rect.top && y <= rect.bottom;
51
+ // Use scrollWidth/scrollHeight to account for content that overflows the element
52
+ // (e.g. grid columns that extend beyond a scrollable parent with overflow: hidden).
53
+ const width = Math.max(rect.width, element.scrollWidth);
54
+ const height = Math.max(rect.height, element.scrollHeight);
55
+ return x >= rect.left && x <= rect.left + width && y >= rect.top && y <= rect.top + height;
52
56
  }
53
57
  get keyboardControlsActive() {
54
58
  return this.#keyboardController.active;
@@ -557,9 +561,9 @@ export class GridDimensionTracker {
557
561
  const style = window.getComputedStyle(parent);
558
562
  const overflowY = style.getPropertyValue('overflow-y');
559
563
  const overflowX = style.getPropertyValue('overflow-x');
560
- const isScrollable = ((overflowY === 'auto' || overflowY === 'scroll') &&
564
+ const isScrollable = ((overflowY === 'auto' || overflowY === 'scroll' || overflowY === 'hidden') &&
561
565
  parent.scrollHeight > parent.clientHeight) ||
562
- ((overflowX === 'auto' || overflowX === 'scroll') &&
566
+ ((overflowX === 'auto' || overflowX === 'scroll' || overflowX === 'hidden') &&
563
567
  parent.scrollWidth > parent.clientWidth);
564
568
  if (isScrollable) {
565
569
  ancestors.push(parent);
@@ -574,32 +578,45 @@ export class GridDimensionTracker {
574
578
  }
575
579
  this.#pointerPosition.x = clientX;
576
580
  this.#pointerPosition.y = clientY;
577
- let xCell = this.#findCell(clientX, this.#dimensions.left, this.#dimensions.width, this.#dimensions.columnGap, this.#dimensions.columns);
578
- let yCell = this.#findCell(clientY, this.#dimensions.top, this.#dimensions.height, this.#dimensions.rowGap, this.#dimensions.rows);
581
+ // DEBUG: trace scroll offset issue
582
+ const gridRef = this.#grid.ref;
583
+ const freshRect = gridRef.getBoundingClientRect();
584
+ // Walk up to find the scrollable parent (board element)
585
+ let scrollParent = gridRef.parentElement;
586
+ while (scrollParent && scrollParent.scrollLeft === 0 && scrollParent !== document.documentElement) {
587
+ scrollParent = scrollParent.parentElement;
588
+ }
589
+ let xCell = findCell(clientX, this.#dimensions.left, contentSize(this.#dimensions.columns, this.#dimensions.columnGap), this.#dimensions.columnGap, this.#dimensions.columns);
590
+ let yCell = findCell(clientY, this.#dimensions.top, contentSize(this.#dimensions.rows, this.#dimensions.rowGap), this.#dimensions.rowGap, this.#dimensions.rows);
579
591
  return {
580
592
  row: yCell,
581
593
  column: xCell
582
594
  };
583
595
  }
584
- #findCell(pointerLocation, start, size, gap, axisCoordinates) {
585
- // If outside the axis, then return the ends.
586
- if (pointerLocation < start) {
587
- return 0;
588
- }
589
- if (pointerLocation >= start + size) {
590
- return axisCoordinates.length;
591
- }
592
- let subtotal = start - gap / 2;
593
- for (let i = 0; i < axisCoordinates.length; i++) {
594
- const base = subtotal;
595
- subtotal += axisCoordinates[i] + gap;
596
- const proportionAlong = (pointerLocation - base) / (subtotal - base);
597
- if (pointerLocation < subtotal) {
598
- return i + proportionAlong;
599
- }
600
- }
596
+ }
597
+ export function contentSize(axisCoordinates, gap) {
598
+ const totalCells = axisCoordinates.reduce((sum, size) => sum + size, 0);
599
+ const totalGaps = Math.max(0, axisCoordinates.length - 1) * gap;
600
+ return totalCells + totalGaps;
601
+ }
602
+ export function findCell(pointerLocation, start, size, gap, axisCoordinates) {
603
+ // If outside the axis, then return the ends.
604
+ if (pointerLocation < start) {
605
+ return 0;
606
+ }
607
+ if (pointerLocation >= start + size) {
601
608
  return axisCoordinates.length;
602
609
  }
610
+ let subtotal = start - gap / 2;
611
+ for (let i = 0; i < axisCoordinates.length; i++) {
612
+ const base = subtotal;
613
+ subtotal += axisCoordinates[i] + gap;
614
+ const proportionAlong = (pointerLocation - base) / (subtotal - base);
615
+ if (pointerLocation < subtotal) {
616
+ return i + proportionAlong;
617
+ }
618
+ }
619
+ return axisCoordinates.length;
603
620
  }
604
621
  let uniqueIdIndex = 0;
605
622
  export function generateUniqueId(prefix = 'flexi-') {
@@ -7,11 +7,13 @@ import type { InternalFlexiBoardController } from '../board/controller.svelte.js
7
7
  import type { FlexiTargetController } from './base.svelte.js';
8
8
  import { SvelteSet } from 'svelte/reactivity';
9
9
  import type { FlexiTargetActionWidget, FlexiTargetConfiguration, FlexiTargetPartialConfiguration } from './types.js';
10
+ import type { FlexiRegistryEntry, FlexiWidgetLayoutEntry } from '../board/types.js';
10
11
  export declare class InternalFlexiTargetController implements FlexiTargetController {
11
12
  #private;
12
13
  provider: InternalFlexiBoardController;
13
14
  providerWidgetDefaults?: FlexiWidgetDefaults;
14
15
  key: string;
16
+ registry?: Record<string, FlexiRegistryEntry>;
15
17
  config: FlexiTargetConfiguration;
16
18
  constructor(provider: InternalFlexiBoardController, key: string, config?: FlexiTargetPartialConfiguration);
17
19
  createGrid(): FlexiGrid;
@@ -25,18 +27,20 @@ export declare class InternalFlexiTargetController implements FlexiTargetControl
25
27
  deleteWidget(widget: FlexiWidgetController): boolean;
26
28
  /**
27
29
  * Imports a layout of widgets into this target, replacing any existing widgets.
30
+ * Widgets with types not found in the registry will be skipped with a warning.
28
31
  * @param layout The layout to import.
29
32
  */
30
- importLayout(layout: FlexiWidgetConfiguration[]): void;
33
+ importLayout(layout: FlexiWidgetLayoutEntry[]): void;
31
34
  /**
32
35
  * Exports the current layout of widgets from this target.
33
36
  * @returns The layout of widgets.
34
37
  */
35
- exportLayout(): FlexiWidgetConfiguration[];
38
+ exportLayout(): FlexiWidgetLayoutEntry[];
36
39
  onPointerEnterTarget(event: TargetEvent): void;
37
40
  onPointerLeaveTarget(event: TargetEvent): void;
38
41
  restorePreGrabSnapshot(): void;
39
42
  forgetPreGrabSnapshot(): void;
43
+ hasPreGrabSnapshot(): boolean;
40
44
  applyGridPostCompletionOperations(): void;
41
45
  cancelDrop(): void;
42
46
  tryDropWidget(widget: InternalFlexiWidgetController): boolean;
@@ -76,6 +80,7 @@ export declare class InternalFlexiTargetController implements FlexiTargetControl
76
80
  get grid(): FlexiGrid;
77
81
  get dropzoneWidget(): InternalFlexiWidgetController | null;
78
82
  set dropzoneWidget(value: InternalFlexiWidgetController | null);
83
+ get shouldRenderDropzoneWidget(): boolean;
79
84
  get widgets(): SvelteSet<FlexiWidgetController>;
80
85
  get internalWidgets(): SvelteSet<InternalFlexiWidgetController>;
81
86
  get orderedWidgets(): InternalFlexiWidgetController[];