@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.
- package/dist/components/drag-drop/{PreviewController.d.ts → DragController.d.ts} +3 -2
- package/dist/components/drag-drop/{PreviewController.js → DragController.js} +3 -2
- package/dist/components/drag-drop/Draggable.svelte +101 -17
- package/dist/components/drag-drop/Draggable.svelte.d.ts +11 -3
- package/dist/components/drag-drop/DropZone.svelte +47 -33
- package/dist/components/drag-drop/DropZone.svelte.d.ts +7 -21
- package/dist/components/layout/grid-layers/GridLayers.svelte +177 -158
- package/dist/components/layout/grid-layers/GridLayers.svelte.d.ts +2 -0
- package/dist/components/layout/grid-layers/GridLayers.svelte__ +167 -0
- package/dist/components/layout/grid-layers/util.d.ts +19 -0
- package/dist/components/layout/grid-layers/util.js +74 -0
- package/dist/themes/hkdev/components/drag-drop/draggable.css +4 -0
- package/dist/themes/hkdev/components/drag-drop/dropzone.css +32 -56
- package/package.json +1 -1
@@ -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
|
-
*
|
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
|
-
//
|
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
|
184
|
+
const previewController = new DragController(event);
|
151
185
|
|
152
186
|
// Function to get the preview controller
|
153
|
-
const
|
154
|
-
|
155
|
-
// Call onDragStart with the
|
156
|
-
onDragStart?.({ event, item, source, group,
|
157
|
-
|
158
|
-
//
|
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
|
-
|
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
|
-
|
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 {
|
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
|
-
*
|
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
|
-
|
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
|
-
//
|
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
|
-
{
|
369
|
-
{
|
370
|
-
|
371
|
-
|
372
|
-
|
373
|
-
{
|
374
|
-
|
375
|
-
|
376
|
-
|
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
|
-
|
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
|
-
|
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
|
-
}, {}, "
|
100
|
+
}, {}, "isDragOver" | "canDrop">;
|
@@ -1,167 +1,186 @@
|
|
1
1
|
<script>
|
2
|
-
|
3
|
-
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
|
90
|
-
|
91
|
-
|
92
|
-
|
93
|
-
|
94
|
-
|
95
|
-
|
96
|
-
|
97
|
-
|
98
|
-
|
99
|
-
|
100
|
-
|
101
|
-
|
102
|
-
|
103
|
-
|
104
|
-
|
105
|
-
|
106
|
-
|
107
|
-
|
108
|
-
|
109
|
-
|
110
|
-
|
111
|
-
|
112
|
-
|
113
|
-
|
114
|
-
|
115
|
-
|
116
|
-
|
117
|
-
|
118
|
-
|
119
|
-
|
120
|
-
|
121
|
-
|
122
|
-
|
123
|
-
|
124
|
-
|
125
|
-
|
126
|
-
|
127
|
-
|
128
|
-
|
129
|
-
|
130
|
-
|
131
|
-
|
132
|
-
|
133
|
-
|
134
|
-
|
135
|
-
|
136
|
-
|
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
|
-
|
141
|
-
|
142
|
-
|
143
|
-
|
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
|
-
|
146
|
-
|
147
|
-
|
148
|
-
|
149
|
-
|
150
|
-
|
151
|
-
|
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
|
-
|
156
|
-
|
157
|
-
|
158
|
-
|
159
|
-
|
160
|
-
|
161
|
-
|
162
|
-
|
163
|
-
|
164
|
-
|
165
|
-
|
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
|
+
}
|
@@ -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
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
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
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
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
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
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
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
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
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
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
|
-
/*
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
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
|
}
|