@hkdigital/lib-sveltekit 0.1.80 → 0.1.81

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.
@@ -1,7 +1,8 @@
1
1
  /**
2
- * Controller for customizing drag preview images
2
+ * Controller for customizing drag
3
+ * E.g. set a custom preview image
3
4
  */
4
- export class PreviewController {
5
+ export class DragController {
5
6
  /**
6
7
  * @param {DragEvent} event - The drag event
7
8
  */
@@ -1,7 +1,8 @@
1
1
  /**
2
- * Controller for customizing drag preview images
2
+ * Controller for customizing drag
3
+ * E.g. set a custom preview image
3
4
  */
4
- export class PreviewController {
5
+ export class DragController {
5
6
  /**
6
7
  * @param {DragEvent} event - The drag event
7
8
  */
@@ -1,12 +1,9 @@
1
1
  <script>
2
2
  import { toStateClasses } from '../../util/design-system/index.js';
3
-
4
3
  import { createOrGetDragState } from './drag-state.svelte.js';
5
-
6
- import { PreviewController } from './PreviewController.js';
7
-
4
+ import { DragController } from './DragController.js';
8
5
  import { generateLocalId } from '../../util/unique';
9
-
6
+ import { onDestroy } from 'svelte';
10
7
  import {
11
8
  IDLE,
12
9
  DRAGGING,
@@ -25,6 +22,10 @@
25
22
  * base?: string,
26
23
  * classes?: string,
27
24
  * children: import('svelte').Snippet,
25
+ * draggingSnippet?: import('svelte').Snippet<[{
26
+ * element: HTMLElement,
27
+ * rect: DOMRect
28
+ * }]>,
28
29
  * contextKey?: import('../../typedef').ContextKey,
29
30
  * isDragging?: boolean,
30
31
  * isDropping?: boolean,
@@ -34,7 +35,7 @@
34
35
  * item: any,
35
36
  * source: string,
36
37
  * group: string,
37
- * getPreviewController: () => PreviewController
38
+ * getController: () => DragController
38
39
  * }) => void,
39
40
  * onDragging?: (detail: {
40
41
  * event: DragEvent,
@@ -63,6 +64,7 @@
63
64
  base = '',
64
65
  classes = '',
65
66
  children,
67
+ draggingSnippet,
66
68
  contextKey,
67
69
  isDragging = $bindable(false),
68
70
  isDropping = $bindable(false),
@@ -79,11 +81,21 @@
79
81
 
80
82
  const draggableId = generateLocalId();
81
83
 
82
- // console.debug('Draggable contextKey:', contextKey);
84
+ // svelte-ignore non_reactive_update
85
+ let draggableElement;
83
86
 
84
87
  let dragTimeout = null;
85
88
  let currentState = $state(IDLE);
86
89
 
90
+ // Custom preview follower state
91
+ let showPreview = $state(false);
92
+ let previewX = $state(0);
93
+ let previewY = $state(0);
94
+ let dragOffsetX = $state(0);
95
+ let dragOffsetY = $state(0);
96
+ let customPreviewSet = $state(false);
97
+ let elementRect = $state(null);
98
+
87
99
  // Computed state object for CSS classes
88
100
  let stateObject = $derived({
89
101
  idle: currentState === IDLE,
@@ -102,6 +114,28 @@
102
114
  isDragPreview = currentState === DRAG_PREVIEW;
103
115
  });
104
116
 
117
+ // Clean up on component destroy
118
+ onDestroy(() => {
119
+ if (showPreview) {
120
+ document.removeEventListener('dragover', handleDocumentDragOver);
121
+ }
122
+ });
123
+
124
+ /**
125
+ * Handle document level dragover to ensure we get position updates
126
+ * @param {DragEvent} event
127
+ */
128
+ function handleDocumentDragOver(event) {
129
+ if (showPreview && currentState === DRAGGING) {
130
+ // Update position for the custom preview
131
+ previewX = event.clientX - dragOffsetX;
132
+ previewY = event.clientY - dragOffsetY;
133
+
134
+ // Prevent default to allow drop
135
+ event.preventDefault();
136
+ }
137
+ }
138
+
105
139
  /**
106
140
  * Handle drag start
107
141
  * @param {DragEvent} event
@@ -147,20 +181,50 @@
147
181
  event.dataTransfer.setData('application/json', JSON.stringify(dragData));
148
182
 
149
183
  // Create the preview controller with natural offsets already calculated
150
- const previewController = new PreviewController(event);
184
+ const previewController = new DragController(event);
151
185
 
152
186
  // Function to get the preview controller
153
- const getPreviewController = () => previewController;
154
-
155
- // Call onDragStart with the getPreviewController function
156
- onDragStart?.({ event, item, source, group, getPreviewController });
157
-
158
- // Apply default preview if no custom one was set
159
- if (!previewController.hasCustomPreview()) {
187
+ const getController = () => previewController;
188
+
189
+ // Call onDragStart with the getController function
190
+ onDragStart?.({ event, item, source, group, getController });
191
+
192
+ // Check if we have a preview snippet and no custom preview was set by preview controller
193
+ if (draggingSnippet && !previewController.hasCustomPreview()) {
194
+ try {
195
+ // Get the element's bounding rectangle
196
+ const rect = draggableElement.getBoundingClientRect();
197
+ elementRect = rect;
198
+
199
+ // Calculate offsets - this is the natural position where the user grabbed
200
+ dragOffsetX = event.clientX - rect.left;
201
+ dragOffsetY = event.clientY - rect.top;
202
+
203
+ // Set initial position
204
+ previewX = event.clientX - dragOffsetX;
205
+ previewY = event.clientY - dragOffsetY;
206
+
207
+ // Set a transparent 1x1 pixel image as drag preview to hide browser preview
208
+ const emptyImg = new Image();
209
+ emptyImg.src = 'data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7';
210
+ event.dataTransfer.setDragImage(emptyImg, 0, 0);
211
+
212
+ // Add document level event listener to catch all dragover events
213
+ document.addEventListener('dragover', handleDocumentDragOver);
214
+
215
+ // Show our custom preview
216
+ showPreview = true;
217
+ customPreviewSet = true;
218
+ } catch (err) {
219
+ console.error('Error setting up custom preview:', err);
220
+ // Fallback to default preview
221
+ previewController.applyDefaultPreview();
222
+ }
223
+ }
224
+ // Apply default preview if no custom preview was set and no snippet
225
+ else if (!previewController.hasCustomPreview() && !customPreviewSet) {
160
226
  previewController.applyDefaultPreview();
161
227
  }
162
-
163
- // Additional handlers can be left unchanged
164
228
  }
165
229
 
166
230
  /**
@@ -183,6 +247,14 @@
183
247
  // Clear global drag state
184
248
  dragState.end(draggableId);
185
249
 
250
+ // Clean up document event listener
251
+ if (customPreviewSet) {
252
+ document.removeEventListener('dragover', handleDocumentDragOver);
253
+ showPreview = false;
254
+ customPreviewSet = false;
255
+ elementRect = null;
256
+ }
257
+
186
258
  // Check if drop was successful
187
259
  const wasDropped = event.dataTransfer.dropEffect !== 'none';
188
260
 
@@ -226,6 +298,7 @@
226
298
  <div
227
299
  data-component="draggable"
228
300
  data-id={draggableId}
301
+ bind:this={draggableElement}
229
302
  draggable={!disabled && canDrag(item)}
230
303
  ondragstart={handleDragStart}
231
304
  ondrag={handleDrag}
@@ -237,3 +310,14 @@
237
310
  >
238
311
  {@render children()}
239
312
  </div>
313
+
314
+ {#if draggingSnippet && showPreview && elementRect}
315
+ <div
316
+ data-companion="drag-preview-follower"
317
+ style="position: fixed; z-index: 9999; pointer-events: none;"
318
+ style:left="{previewX}px"
319
+ style:top="{previewY}px"
320
+ >
321
+ {@render draggingSnippet({ element: draggableElement, rect: elementRect })}
322
+ </div>
323
+ {/if}
@@ -11,6 +11,10 @@ type Draggable = {
11
11
  base?: string;
12
12
  classes?: string;
13
13
  children: Snippet<[]>;
14
+ draggingSnippet?: Snippet<[{
15
+ element: HTMLElement;
16
+ rect: DOMRect;
17
+ }]>;
14
18
  contextKey?: ContextKey;
15
19
  isDragging?: boolean;
16
20
  isDropping?: boolean;
@@ -20,7 +24,7 @@ type Draggable = {
20
24
  item: any;
21
25
  source: string;
22
26
  group: string;
23
- getPreviewController: () => PreviewController;
27
+ getController: () => DragController;
24
28
  }) => void;
25
29
  onDragging?: (detail: {
26
30
  event: DragEvent;
@@ -49,6 +53,10 @@ declare const Draggable: import("svelte").Component<{
49
53
  base?: string;
50
54
  classes?: string;
51
55
  children: import("svelte").Snippet;
56
+ draggingSnippet?: import("svelte").Snippet<[{
57
+ element: HTMLElement;
58
+ rect: DOMRect;
59
+ }]>;
52
60
  contextKey?: import("../../typedef").ContextKey;
53
61
  isDragging?: boolean;
54
62
  isDropping?: boolean;
@@ -58,7 +66,7 @@ declare const Draggable: import("svelte").Component<{
58
66
  item: any;
59
67
  source: string;
60
68
  group: string;
61
- getPreviewController: () => PreviewController;
69
+ getController: () => DragController;
62
70
  }) => void;
63
71
  onDragging?: (detail: {
64
72
  event: DragEvent;
@@ -76,4 +84,4 @@ declare const Draggable: import("svelte").Component<{
76
84
  }) => void;
77
85
  canDrag?: (item: any) => boolean;
78
86
  }, {}, "isDragging" | "isDropping" | "isDragPreview">;
79
- import { PreviewController } from './PreviewController.js';
87
+ import { DragController } from './DragController.js';
@@ -4,6 +4,8 @@
4
4
 
5
5
  import { createOrGetDragState } from './drag-state.svelte.js';
6
6
 
7
+ import { GridLayers } from '../layout';
8
+
7
9
  import {
8
10
  findDraggableSource,
9
11
  getDraggableIdFromEvent,
@@ -15,32 +17,26 @@
15
17
  DRAG_OVER,
16
18
  CAN_DROP,
17
19
  CANNOT_DROP,
18
- DROP_DISABLED,
19
- ACTIVE_DROP
20
+ DROP_DISABLED
20
21
  } from '../../constants/state-labels/drop-states.js';
21
22
 
23
+ /** @typedef {import('../../typedef').DragData} DragData */
24
+
22
25
  /**
23
26
  * @type {{
24
27
  * zone?: string,
25
28
  * group?: string,
26
29
  * disabled?: boolean,
27
30
  * accepts?: (item: any) => boolean,
28
- * maxItems?: number,
29
31
  * base?: string,
30
32
  * classes?: string,
33
+ * height?: string,
34
+ * autoHeight?: boolean,
31
35
  * children?: import('svelte').Snippet,
32
36
  * contextKey?: import('../../typedef').ContextKey,
33
- * empty?: import('svelte').Snippet,
34
- * preview?: import('svelte').Snippet<[{
35
- * item: any,
36
- * source: string,
37
- * group: string,
38
- * metadata?: any
39
- * }]>,
37
+ * dropPreviewSnippet?: import('svelte').Snippet<[DragData]>,
40
38
  * isDragOver?: boolean,
41
39
  * canDrop?: boolean,
42
- * isDropping?: boolean,
43
- * itemCount?: number,
44
40
  * onDragEnter?: (detail: {
45
41
  * event: DragEvent,
46
42
  * zone: string,
@@ -81,17 +77,15 @@
81
77
  group = 'default',
82
78
  disabled = false,
83
79
  accepts = () => true,
84
- maxItems = Infinity,
85
80
  base = '',
86
81
  classes = '',
82
+ height = 'h-min',
83
+ autoHeight= false,
87
84
  children,
88
85
  contextKey,
89
- empty,
90
- preview,
86
+ dropPreviewSnippet,
91
87
  isDragOver = $bindable(false),
92
88
  canDrop = $bindable(false),
93
- isDropping = $bindable(false),
94
- itemCount = $bindable(0),
95
89
  onDragEnter,
96
90
  onDragOver,
97
91
  onDragLeave,
@@ -140,8 +134,7 @@
140
134
  'drag-over': currentState === DRAG_OVER,
141
135
  'can-drop': currentState === CAN_DROP,
142
136
  'cannot-drop': currentState === CANNOT_DROP,
143
- 'drop-disabled': disabled,
144
- 'active-drop': currentState === ACTIVE_DROP
137
+ 'drop-disabled': disabled
145
138
  });
146
139
 
147
140
  let stateClasses = $derived(toStateClasses(stateObject));
@@ -155,7 +148,6 @@
155
148
  ].includes(currentState);
156
149
 
157
150
  canDrop = currentState === CAN_DROP;
158
- isDropping = currentState === ACTIVE_DROP;
159
151
  });
160
152
 
161
153
  /**
@@ -172,7 +164,6 @@
172
164
  if (!data) return false;
173
165
  if (data.group !== group) return false;
174
166
  if (!accepts(data.item)) return false;
175
- if (itemCount >= maxItems) return false;
176
167
  return true;
177
168
  }
178
169
 
@@ -321,8 +312,7 @@ function handleDrop(event) {
321
312
 
322
313
  // Check if we can accept this drop
323
314
  if (dragData && canAcceptDrop(dragData)) {
324
- // Update state and notify listeners
325
- currentState = ACTIVE_DROP;
315
+ // Notify listener
326
316
  onDropStart?.({ event, zone, data: dragData });
327
317
 
328
318
  // Call the onDrop handler and handle Promise resolution
@@ -361,17 +351,41 @@ function handleDrop(event) {
361
351
  ondragover={handleDragOver}
362
352
  ondragleave={handleDragLeave}
363
353
  ondrop={handleDrop}
364
- class="{base} {classes} {stateClasses}"
354
+ class="{base} {height} {classes} {stateClasses}"
365
355
  data-zone={zone}
366
356
  {...attrs}
367
357
  >
368
- {#if children}
369
- {@render children()}
370
- {:else if currentState === CAN_DROP && preview}
371
- {@render preview(dragState.current)}
372
- {:else if itemCount === 0 && empty}
373
- {@render empty()}
374
- {:else}
375
- <div data-element="drop-zone-empty">Drop items here</div>
376
- {/if}
358
+ <GridLayers heightFrom={autoHeight ? 'content' : null}>
359
+ {#if children}
360
+ <div data-layer="content" class:auto-height={autoHeight}>
361
+ {@render children()}
362
+ </div>
363
+ {/if}
364
+
365
+ {#if currentState === CAN_DROP && dropPreviewSnippet}
366
+ <div data-layer="preview">
367
+ {@render dropPreviewSnippet(dragState.current)}
368
+ </div>
369
+ {/if}
370
+
371
+ </GridLayers>
377
372
  </div>
373
+
374
+ <style>
375
+ [data-layer='content']:not(.auto-height) {
376
+ position: absolute;
377
+ left: 0; right: 0; top: 0; bottom: 0;
378
+ }
379
+
380
+ [data-layer='content'].auto-height {
381
+ position: relative;
382
+ width: 100%;
383
+ }
384
+
385
+ [data-layer='preview']
386
+ {
387
+ position: absolute;
388
+ left: 0; right: 0; top: 0; bottom: 0;
389
+ pointer-events: none;
390
+ }
391
+ </style>
@@ -7,22 +7,15 @@ type DropZone = {
7
7
  group?: string;
8
8
  disabled?: boolean;
9
9
  accepts?: (item: any) => boolean;
10
- maxItems?: number;
11
10
  base?: string;
12
11
  classes?: string;
12
+ height?: string;
13
+ autoHeight?: boolean;
13
14
  children?: Snippet<[]>;
14
15
  contextKey?: ContextKey;
15
- empty?: Snippet<[]>;
16
- preview?: Snippet<[{
17
- item: any;
18
- source: string;
19
- group: string;
20
- metadata?: any;
21
- }]>;
16
+ dropPreviewSnippet?: Snippet<[DragData]>;
22
17
  isDragOver?: boolean;
23
18
  canDrop?: boolean;
24
- isDropping?: boolean;
25
- itemCount?: number;
26
19
  onDragEnter?: (detail: {
27
20
  event: DragEvent;
28
21
  zone: string;
@@ -63,22 +56,15 @@ declare const DropZone: import("svelte").Component<{
63
56
  group?: string;
64
57
  disabled?: boolean;
65
58
  accepts?: (item: any) => boolean;
66
- maxItems?: number;
67
59
  base?: string;
68
60
  classes?: string;
61
+ height?: string;
62
+ autoHeight?: boolean;
69
63
  children?: import("svelte").Snippet;
70
64
  contextKey?: import("../../typedef").ContextKey;
71
- empty?: import("svelte").Snippet;
72
- preview?: import("svelte").Snippet<[{
73
- item: any;
74
- source: string;
75
- group: string;
76
- metadata?: any;
77
- }]>;
65
+ dropPreviewSnippet?: import("svelte").Snippet<[import("../../typedef").DragData]>;
78
66
  isDragOver?: boolean;
79
67
  canDrop?: boolean;
80
- isDropping?: boolean;
81
- itemCount?: number;
82
68
  onDragEnter?: (detail: {
83
69
  event: DragEvent;
84
70
  zone: string;
@@ -111,4 +97,4 @@ declare const DropZone: import("svelte").Component<{
111
97
  success: boolean;
112
98
  error?: Error;
113
99
  }) => void;
114
- }, {}, "isDropping" | "isDragOver" | "canDrop" | "itemCount">;
100
+ }, {}, "isDragOver" | "canDrop">;
@@ -1,167 +1,186 @@
1
1
  <script>
2
- /**
3
- * Grid Layers Component
4
- *
5
- * A component that creates a single-cell grid where all children exist
6
- * in the same grid cell, allowing them to be positioned independently
7
- * and stacked on top of each other. Perfect for complex layouts like
8
- * overlaying text on images, card stacks, positioning UI elements, etc.
9
- *
10
- * Each child can use grid positioning properties (justify-self-*, self-*)
11
- * for precise placement. Children can control stacking order with z-index.
12
- *
13
- * @example Basic usage with 9-position grid
14
- * ```html
15
- * <GridLayers classes="border w-[500px] h-[500px]">
16
- * <!-- Top Row -->
17
- * <div class="justify-self-start self-start">
18
- * <div class="bg-blue-500 w-[100px] h-[100px]">
19
- * Top Left
20
- * </div>
21
- * </div>
22
- * <div class="justify-self-center self-start">
23
- * <div class="bg-blue-300 w-[100px] h-[100px]">
24
- * Top Center
25
- * </div>
26
- * </div>
27
- * <div class="justify-self-end self-start">
28
- * <div class="bg-blue-500 w-[100px] h-[100px]">
29
- * Top Right
30
- * </div>
31
- * </div>
32
- *
33
- * <!-- Middle Row -->
34
- * <div class="justify-self-start self-center">
35
- * <div class="bg-green-500 w-[100px] h-[100px]">
36
- * Middle Left
37
- * </div>
38
- * </div>
39
- * <div class="justify-self-center self-center">
40
- * <div class="bg-green-300 w-[100px] h-[100px]">
41
- * Middle Center
42
- * </div>
43
- * </div>
44
- * <div class="justify-self-end self-center">
45
- * <div class="bg-green-500 w-[100px] h-[100px]">
46
- * Middle Right
47
- * </div>
48
- * </div>
49
- *
50
- * <!-- Bottom Row -->
51
- * <div class="justify-self-start self-end">
52
- * <div class="bg-red-500 w-[100px] h-[100px]">
53
- * Bottom Left
54
- * </div>
55
- * </div>
56
- * <div class="justify-self-center self-end">
57
- * <div class="bg-red-300 w-[100px] h-[100px]">
58
- * Bottom Center
59
- * </div>
60
- * </div>
61
- * <div class="justify-self-end self-end">
62
- * <div class="bg-red-500 w-[100px] h-[100px]">
63
- * Bottom Right
64
- * </div>
65
- * </div>
66
- * </GridLayers>
67
- * ```
68
- *
69
- * @example Text over image
70
- * ```html
71
- * <GridLayers classes="w-full h-[300px]">
72
- * <!-- Background image layer -->
73
- * <div class="justify-self-stretch self-stretch z-0">
74
- * <img
75
- * src="/images/landscape.jpg"
76
- * alt="Landscape"
77
- * class="w-full h-full object-cover"
78
- * />
79
- * </div>
80
- *
81
- * <!-- Text overlay layer -->
82
- * <div class="justify-self-center self-center z-10">
83
- * <div class="bg-black/50 p-16up text-white
84
- * font-ui rounded-md">
85
- * <h2 class="text-2xl">Explore Nature</h2>
86
- * <p>Discover the beauty of the outdoors</p>
87
- * </div>
88
- * </div>
89
- * </GridLayers>
90
- * ```
91
- */
92
-
93
- /**
94
- * @type {{
95
- * base?: string,
96
- * bg?: string,
97
- * padding?: string,
98
- * margin?: string,
99
- * height?: string,
100
- * classes?: string,
101
- * style?: string,
102
- * cellBase?: string,
103
- * cellBg?: string,
104
- * cellPadding?: string,
105
- * cellMargin?: string,
106
- * cellClasses?: string,
107
- * cellStyle?: string,
108
- * children: import('svelte').Snippet,
109
- * cellAttrs?: { [attr: string]: * },
110
- * [attr: string]: any
111
- * }}
112
- */
113
- const {
114
- // Style
115
- base,
116
- bg,
117
- padding,
118
- margin,
119
- height,
120
- classes,
121
- style,
122
- cellBase,
123
- cellBg,
124
- cellPadding,
125
- cellMargin,
126
- cellClasses,
127
- cellStyle,
128
-
129
- cellAttrs,
130
-
131
- // Snippets
132
- children,
133
-
134
- // Attributes
135
- ...attrs
136
- } = $props();
2
+ import { onMount, onDestroy } from 'svelte';
3
+ import { setupLayerObserver, measureTargetLayer } from './util.js';
4
+
5
+ /**
6
+ * @type {{
7
+ * base?: string,
8
+ * bg?: string,
9
+ * padding?: string,
10
+ * margin?: string,
11
+ * height?: string,
12
+ * classes?: string,
13
+ * style?: string,
14
+ * cellBase?: string,
15
+ * cellBg?: string,
16
+ * cellPadding?: string,
17
+ * cellMargin?: string,
18
+ * cellClasses?: string,
19
+ * cellStyle?: string,
20
+ * heightFrom?: string|null,
21
+ * children: import('svelte').Snippet,
22
+ * cellAttrs?: { [attr: string]: any },
23
+ * [attr: string]: any
24
+ * }}
25
+ */
26
+ const {
27
+ // Style
28
+ base = '',
29
+ bg = '',
30
+ padding = '',
31
+ margin = '',
32
+ height = '',
33
+ classes = '',
34
+ style = '',
35
+ cellBase = '',
36
+ cellBg = '',
37
+ cellPadding = '',
38
+ cellMargin = '',
39
+ cellClasses = '',
40
+ cellStyle = '',
41
+
42
+ // Behavior
43
+ heightFrom = null,
44
+
45
+ // Props
46
+ cellAttrs = {},
47
+ children,
48
+
49
+ // Attributes
50
+ ...attrs
51
+ } = $props();
52
+
53
+ // Component state
54
+ let gridContainer = $state(null);
55
+ let gridContent = $state(null);
56
+ let calculatedHeight = $state(0);
57
+ let observer = $state(null);
58
+ let targetLayer = $state(null);
59
+ let isFirstRender = $state(heightFrom !== null); // Start with true if heightFrom is provided
60
+ let preCalculatedHeight = $state(0);
61
+
62
+ // Derived container style that updates reactively when dependencies change
63
+ let containerStyle = $derived.by(() => {
64
+ const styles = [];
65
+
66
+ if (style) {
67
+ styles.push(style);
68
+ }
69
+
70
+ if (heightFrom && calculatedHeight > 0) {
71
+ styles.push(`height: ${calculatedHeight}px;`);
72
+ }
73
+
74
+ return styles.join(' ');
75
+ });
76
+
77
+ /**
78
+ * Handler for height changes detected by the observer
79
+ * @param {number} newHeight - The new calculated height
80
+ */
81
+ function handleHeightChange(newHeight) {
82
+ calculatedHeight = newHeight;
83
+ }
84
+
85
+ /**
86
+ * Initialize height measurement and observation
87
+ */
88
+ function initializeHeightTracking() {
89
+ if (!heightFrom || !gridContent) return;
90
+
91
+ // Measure the layer initially
92
+ const { element, height } = measureTargetLayer(gridContent, heightFrom);
93
+
94
+ if (element) {
95
+ targetLayer = element;
96
+ calculatedHeight = height;
97
+
98
+ // Setup observer for future changes
99
+ observer = setupLayerObserver(element, handleHeightChange);
100
+ }
101
+ }
102
+
103
+ // Initialize on mount with the two-pass rendering approach
104
+ onMount(() => {
105
+ if (heightFrom) {
106
+ // First render: measure invisibly
107
+ requestAnimationFrame(() => {
108
+ if (gridContent) {
109
+ const { element, height } = measureTargetLayer(gridContent, heightFrom);
110
+
111
+ if (element) {
112
+ targetLayer = element;
113
+ preCalculatedHeight = height;
114
+
115
+ // Second render: show with correct height
116
+ requestAnimationFrame(() => {
117
+ calculatedHeight = preCalculatedHeight;
118
+ isFirstRender = false;
119
+
120
+ // Setup observer for future changes
121
+ observer = setupLayerObserver(element, handleHeightChange);
122
+ });
123
+ } else {
124
+ // No target layer found, just show the component
125
+ isFirstRender = false;
126
+ }
127
+ } else {
128
+ // No grid content, just show the component
129
+ isFirstRender = false;
130
+ }
131
+ });
132
+ } else {
133
+ // No heightFrom, no need for measurement
134
+ isFirstRender = false;
135
+ }
136
+ });
137
+
138
+ // Effect to re-setup observer when either the target layer or heightFrom changes
139
+ $effect(() => {
140
+ // Only handle changes after initial setup
141
+ if (!isFirstRender && heightFrom && gridContent && !observer) {
142
+ initializeHeightTracking();
143
+ }
144
+ });
145
+
146
+ // Clean up on destroy
147
+ onDestroy(() => {
148
+ if (observer) {
149
+ observer.disconnect();
150
+ observer = null;
151
+ }
152
+ });
137
153
  </script>
138
154
 
139
155
  <div
140
- data-component="grid-layers"
141
- class="relative {base} {bg} {height} {classes} {margin} {padding}"
142
- {style}
143
- {...attrs}
156
+ data-component="grid-layers"
157
+ bind:this={gridContainer}
158
+ class="relative {isFirstRender ? 'invisible' : ''} {base} {bg} {!heightFrom ? height : ''} {classes} {margin} {padding}"
159
+ style={containerStyle}
160
+ {...attrs}
144
161
  >
145
- <div
146
- data-section="grid"
147
- class="absolute inset-0 grid {cellBase} {cellBg} {cellPadding} {cellMargin} {cellClasses}"
148
- style={cellStyle}
149
- >
150
- {@render children()}
151
- </div>
162
+ <div
163
+ data-section="grid"
164
+ bind:this={gridContent}
165
+ class="absolute inset-0 grid {cellBase} {cellBg} {cellPadding} {cellMargin} {cellClasses}"
166
+ style={cellStyle}
167
+ {...cellAttrs}
168
+ >
169
+ {@render children()}
170
+ </div>
152
171
  </div>
153
172
 
154
173
  <style>
155
- /* All children of the layer share the same grid area
156
- but aren't absolutely positioned */
157
- [data-section='grid'] {
158
- grid-template-columns: 1fr;
159
- grid-template-rows: 1fr;
160
- }
161
-
162
- [data-section='grid'] > :global(*) {
163
- grid-column: 1;
164
- grid-row: 1;
165
- z-index: 0; /* Base z-index to allow explicit stacking order */
166
- }
174
+ /* All children of the layer share the same grid area
175
+ but aren't absolutely positioned */
176
+ [data-section='grid'] {
177
+ grid-template-columns: 1fr;
178
+ grid-template-rows: 1fr;
179
+ }
180
+
181
+ [data-section='grid'] > :global(*) {
182
+ grid-column: 1;
183
+ grid-row: 1;
184
+ z-index: 0; /* Base z-index to allow explicit stacking order */
185
+ }
167
186
  </style>
@@ -16,6 +16,7 @@ type GridLayers = {
16
16
  cellMargin?: string;
17
17
  cellClasses?: string;
18
18
  cellStyle?: string;
19
+ heightFrom?: string;
19
20
  children: Snippet<[]>;
20
21
  cellAttrs?: {
21
22
  [attr: string]: any;
@@ -37,6 +38,7 @@ declare const GridLayers: import("svelte").Component<{
37
38
  cellMargin?: string;
38
39
  cellClasses?: string;
39
40
  cellStyle?: string;
41
+ heightFrom?: string | null;
40
42
  children: import("svelte").Snippet;
41
43
  cellAttrs?: {
42
44
  [attr: string]: any;
@@ -0,0 +1,167 @@
1
+ <script>
2
+ /**
3
+ * Grid Layers Component
4
+ *
5
+ * A component that creates a single-cell grid where all children exist
6
+ * in the same grid cell, allowing them to be positioned independently
7
+ * and stacked on top of each other. Perfect for complex layouts like
8
+ * overlaying text on images, card stacks, positioning UI elements, etc.
9
+ *
10
+ * Each child can use grid positioning properties (justify-self-*, self-*)
11
+ * for precise placement. Children can control stacking order with z-index.
12
+ *
13
+ * @example Basic usage with 9-position grid
14
+ * ```html
15
+ * <GridLayers classes="border w-[500px] h-[500px]">
16
+ * <!-- Top Row -->
17
+ * <div class="justify-self-start self-start">
18
+ * <div class="bg-blue-500 w-[100px] h-[100px]">
19
+ * Top Left
20
+ * </div>
21
+ * </div>
22
+ * <div class="justify-self-center self-start">
23
+ * <div class="bg-blue-300 w-[100px] h-[100px]">
24
+ * Top Center
25
+ * </div>
26
+ * </div>
27
+ * <div class="justify-self-end self-start">
28
+ * <div class="bg-blue-500 w-[100px] h-[100px]">
29
+ * Top Right
30
+ * </div>
31
+ * </div>
32
+ *
33
+ * <!-- Middle Row -->
34
+ * <div class="justify-self-start self-center">
35
+ * <div class="bg-green-500 w-[100px] h-[100px]">
36
+ * Middle Left
37
+ * </div>
38
+ * </div>
39
+ * <div class="justify-self-center self-center">
40
+ * <div class="bg-green-300 w-[100px] h-[100px]">
41
+ * Middle Center
42
+ * </div>
43
+ * </div>
44
+ * <div class="justify-self-end self-center">
45
+ * <div class="bg-green-500 w-[100px] h-[100px]">
46
+ * Middle Right
47
+ * </div>
48
+ * </div>
49
+ *
50
+ * <!-- Bottom Row -->
51
+ * <div class="justify-self-start self-end">
52
+ * <div class="bg-red-500 w-[100px] h-[100px]">
53
+ * Bottom Left
54
+ * </div>
55
+ * </div>
56
+ * <div class="justify-self-center self-end">
57
+ * <div class="bg-red-300 w-[100px] h-[100px]">
58
+ * Bottom Center
59
+ * </div>
60
+ * </div>
61
+ * <div class="justify-self-end self-end">
62
+ * <div class="bg-red-500 w-[100px] h-[100px]">
63
+ * Bottom Right
64
+ * </div>
65
+ * </div>
66
+ * </GridLayers>
67
+ * ```
68
+ *
69
+ * @example Text over image
70
+ * ```html
71
+ * <GridLayers classes="w-full h-[300px]">
72
+ * <!-- Background image layer -->
73
+ * <div class="justify-self-stretch self-stretch z-0">
74
+ * <img
75
+ * src="/images/landscape.jpg"
76
+ * alt="Landscape"
77
+ * class="w-full h-full object-cover"
78
+ * />
79
+ * </div>
80
+ *
81
+ * <!-- Text overlay layer -->
82
+ * <div class="justify-self-center self-center z-10">
83
+ * <div class="bg-black/50 p-16up text-white
84
+ * font-ui rounded-md">
85
+ * <h2 class="text-2xl">Explore Nature</h2>
86
+ * <p>Discover the beauty of the outdoors</p>
87
+ * </div>
88
+ * </div>
89
+ * </GridLayers>
90
+ * ```
91
+ */
92
+
93
+ /**
94
+ * @type {{
95
+ * base?: string,
96
+ * bg?: string,
97
+ * padding?: string,
98
+ * margin?: string,
99
+ * height?: string,
100
+ * classes?: string,
101
+ * style?: string,
102
+ * cellBase?: string,
103
+ * cellBg?: string,
104
+ * cellPadding?: string,
105
+ * cellMargin?: string,
106
+ * cellClasses?: string,
107
+ * cellStyle?: string,
108
+ * children: import('svelte').Snippet,
109
+ * cellAttrs?: { [attr: string]: * },
110
+ * [attr: string]: any
111
+ * }}
112
+ */
113
+ const {
114
+ // Style
115
+ base,
116
+ bg,
117
+ padding,
118
+ margin,
119
+ height,
120
+ classes,
121
+ style,
122
+ cellBase,
123
+ cellBg,
124
+ cellPadding,
125
+ cellMargin,
126
+ cellClasses,
127
+ cellStyle,
128
+
129
+ cellAttrs,
130
+
131
+ // Snippets
132
+ children,
133
+
134
+ // Attributes
135
+ ...attrs
136
+ } = $props();
137
+ </script>
138
+
139
+ <div
140
+ data-component="grid-layers"
141
+ class="relative {base} {bg} {height} {classes} {margin} {padding}"
142
+ {style}
143
+ {...attrs}
144
+ >
145
+ <div
146
+ data-section="grid"
147
+ class="absolute inset-0 grid {cellBase} {cellBg} {cellPadding} {cellMargin} {cellClasses}"
148
+ style={cellStyle}
149
+ >
150
+ {@render children()}
151
+ </div>
152
+ </div>
153
+
154
+ <style>
155
+ /* All children of the layer share the same grid area
156
+ but aren't absolutely positioned */
157
+ [data-section='grid'] {
158
+ grid-template-columns: 1fr;
159
+ grid-template-rows: 1fr;
160
+ }
161
+
162
+ [data-section='grid'] > :global(*) {
163
+ grid-column: 1;
164
+ grid-row: 1;
165
+ z-index: 0; /* Base z-index to allow explicit stacking order */
166
+ }
167
+ </style>
@@ -0,0 +1,19 @@
1
+ /**
2
+ * Sets up a ResizeObserver on the target layer
3
+ *
4
+ * @param {HTMLElement|null} targetLayer - The layer element to observe
5
+ * @param {Function} onHeightChange - Callback when height changes
6
+ * @returns {ResizeObserver|null} - The created observer or null
7
+ */
8
+ export function setupLayerObserver(targetLayer: HTMLElement | null, onHeightChange: Function): ResizeObserver | null;
9
+ /**
10
+ * Measures the height of the specified layer, including margins
11
+ *
12
+ * @param {HTMLElement|null} container - The container to search in
13
+ * @param {string} layerId - The data-layer attribute value to find
14
+ * @returns {{ element: HTMLElement|null, height: number }} The element and its height
15
+ */
16
+ export function measureTargetLayer(container: HTMLElement | null, layerId: string): {
17
+ element: HTMLElement | null;
18
+ height: number;
19
+ };
@@ -0,0 +1,74 @@
1
+ // lib/components/layout/gridLayers.utils.js
2
+
3
+ /**
4
+ * Sets up a ResizeObserver on the target layer
5
+ *
6
+ * @param {HTMLElement|null} targetLayer - The layer element to observe
7
+ * @param {Function} onHeightChange - Callback when height changes
8
+ * @returns {ResizeObserver|null} - The created observer or null
9
+ */
10
+ export function setupLayerObserver(targetLayer, onHeightChange) {
11
+ if (!targetLayer || !window.ResizeObserver) return null;
12
+
13
+ // Create new observer
14
+ const observer = new ResizeObserver(entries => {
15
+ for (const entry of entries) {
16
+ if (entry.target === targetLayer) {
17
+ // Get the computed style to access margin values
18
+ const computedStyle = window.getComputedStyle(targetLayer);
19
+ const marginTop = parseInt(computedStyle.marginTop, 10);
20
+ const marginBottom = parseInt(computedStyle.marginBottom, 10);
21
+
22
+ // Calculate height including border box and margins
23
+ let elementHeight = 0;
24
+
25
+ if (entry.borderBoxSize) {
26
+ const borderBoxSize = Array.isArray(entry.borderBoxSize)
27
+ ? entry.borderBoxSize[0]
28
+ : entry.borderBoxSize;
29
+ elementHeight = borderBoxSize.blockSize;
30
+ } else {
31
+ // Fallback to getBoundingClientRect
32
+ const rect = targetLayer.getBoundingClientRect();
33
+ elementHeight = rect.height;
34
+ }
35
+
36
+ // Add margins to the height
37
+ const totalHeight = elementHeight + marginTop + marginBottom;
38
+ onHeightChange(totalHeight);
39
+ }
40
+ }
41
+ });
42
+
43
+ // Start observing
44
+ observer.observe(targetLayer);
45
+ return observer;
46
+ }
47
+
48
+ /**
49
+ * Measures the height of the specified layer, including margins
50
+ *
51
+ * @param {HTMLElement|null} container - The container to search in
52
+ * @param {string} layerId - The data-layer attribute value to find
53
+ * @returns {{ element: HTMLElement|null, height: number }} The element and its height
54
+ */
55
+ export function measureTargetLayer(container, layerId) {
56
+ if (!layerId || !container) return { element: null, height: 0 };
57
+
58
+ const layerElement = container.querySelector(`[data-layer="${layerId}"]`);
59
+
60
+ if (!layerElement) return { element: null, height: 0 };
61
+
62
+ // Get the computed style to access margin values
63
+ const computedStyle = window.getComputedStyle(layerElement);
64
+ const marginTop = parseInt(computedStyle.marginTop, 10);
65
+ const marginBottom = parseInt(computedStyle.marginBottom, 10);
66
+
67
+ // Get the element's border box height
68
+ const rect = layerElement.getBoundingClientRect();
69
+
70
+ // Calculate total height including margins
71
+ const height = rect.height > 0 ? rect.height + marginTop + marginBottom : 0;
72
+
73
+ return { element: layerElement, height };
74
+ }
@@ -57,4 +57,8 @@
57
57
  opacity: 1;
58
58
  }
59
59
  }
60
+
61
+ & [data-companion="drag-preview-follower"] {
62
+ box-shadow: 0 4px 10px rgba(0,0,0,0.1);
63
+ }
60
64
  }
@@ -1,73 +1,49 @@
1
1
  @define-mixin component-dropzone {
2
+ /* Visual styling and customizable aspects */
2
3
  [data-component='dropzone'] {
3
- min-height: 100px;
4
+ /*min-height: 100px;*/
4
5
  border: 2px dashed rgb(var(--color-surface-400));
5
6
  border-radius: var(--theme-rounded-container);
6
7
  padding: 1rem;
7
8
  transition: all 0.2s ease;
8
9
  position: relative;
9
10
  background-color: rgb(var(--color-surface-50) / 0.5);
10
- }
11
-
12
- /* State-based styling using state classes */
13
- [data-component='dropzone'].state-ready {
14
- border-color: rgb(var(--color-surface-400));
15
- background-color: transparent;
16
- }
17
-
18
- [data-component='dropzone'].state-drag-over {
19
- border-color: rgb(var(--color-primary-400));
20
- background-color: rgb(var(--color-primary-500) / 0.05);
21
- }
22
11
 
23
- [data-component='dropzone'].state-can-drop {
24
- border-color: rgb(var(--color-success-500));
25
- background-color: rgb(var(--color-success-500) / 0.08);
26
- transform: scale(1.01);
27
- box-shadow: 0 0 0 3px rgb(var(--color-success-500) / 0.2);
28
- }
12
+ /* State-based styling using state classes - visual aspects only */
13
+ &.state-ready {
14
+ border-color: rgb(var(--color-surface-400));
15
+ background-color: transparent;
16
+ }
29
17
 
30
- [data-component='dropzone'].state-cannot-drop {
31
- border-color: rgb(var(--color-error-500));
32
- background-color: rgb(var(--color-error-500) / 0.08);
33
- cursor: not-allowed;
34
- }
18
+ &.state-drag-over {
19
+ border-color: rgb(var(--color-primary-400));
20
+ background-color: rgb(var(--color-primary-500) / 0.05);
21
+ }
35
22
 
36
- [data-component='dropzone'].state-active-drop {
37
- animation: drop-pulse 0.3s ease-out;
38
- border-color: rgb(var(--color-success-500));
39
- background-color: rgb(var(--color-success-500) / 0.1);
40
- }
23
+ &.state-can-drop {
24
+ border-color: rgb(var(--color-success-500));
25
+ background-color: rgb(var(--color-success-500) / 0.08);
26
+ transform: scale(1.01);
27
+ box-shadow: 0 0 0 3px rgb(var(--color-success-500) / 0.2);
28
+ }
41
29
 
42
- [data-component='dropzone'].state-drop-disabled {
43
- opacity: 0.5;
44
- cursor: not-allowed;
45
- background-color: rgb(var(--color-surface-100));
46
- }
30
+ &.state-cannot-drop {
31
+ border-color: rgb(var(--color-error-500));
32
+ background-color: rgb(var(--color-error-500) / 0.08);
33
+ cursor: not-allowed;
34
+ }
47
35
 
48
- /* Empty state */
49
- [data-element='drop-zone-empty'] {
50
- display: flex;
51
- align-items: center;
52
- justify-content: center;
53
- height: 100%;
54
- color: rgb(var(--color-surface-500));
55
- font-style: italic;
36
+ &.state-drop-disabled {
37
+ opacity: 0.5;
38
+ cursor: not-allowed;
39
+ background-color: rgb(var(--color-surface-100));
40
+ }
56
41
  }
57
42
 
58
- /* Animations */
59
- @keyframes drop-pulse {
60
- 0% {
61
- transform: scale(1);
62
- box-shadow: 0 0 0 0 rgb(var(--color-success-500) / 0.4);
63
- }
64
- 50% {
65
- transform: scale(1.02);
66
- box-shadow: 0 0 0 8px rgb(var(--color-success-500) / 0);
67
- }
68
- 100% {
69
- transform: scale(1);
70
- box-shadow: 0 0 0 0 rgb(var(--color-success-500) / 0);
71
- }
43
+ /* Default styling for inner elements - all visual/customizable */
44
+ [data-layer='content'],
45
+ [data-layer='preview'],
46
+ [data-layer='empty'] {
47
+ @apply type-base-md;
72
48
  }
73
49
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hkdigital/lib-sveltekit",
3
- "version": "0.1.80",
3
+ "version": "0.1.81",
4
4
  "author": {
5
5
  "name": "HKdigital",
6
6
  "url": "https://hkdigital.nl"