@griddle/svelte 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +69 -0
- package/dist/GriddleGrid.svelte +485 -0
- package/dist/GriddleGrid.svelte.d.ts +32 -0
- package/dist/LoopGrid.svelte +521 -0
- package/dist/LoopGrid.svelte.d.ts +42 -0
- package/dist/griddleStore.d.ts +28 -0
- package/dist/griddleStore.js +28 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.js +3 -0
- package/package.json +60 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Trustybits
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
# @griddle/svelte
|
|
2
|
+
|
|
3
|
+
Svelte bindings for [Griddle](https://github.com/Trustybits/griddle) — a headless,
|
|
4
|
+
zero-dependency grid/canvas engine. Provides a `<GriddleGrid />` component and a
|
|
5
|
+
`createGriddle()` store factory that wrap [`@griddle/core`](https://www.npmjs.com/package/@griddle/core)
|
|
6
|
+
with virtualized rendering, drag/resize handles, and animations.
|
|
7
|
+
|
|
8
|
+
Works with **Svelte 4 and Svelte 5** (the components are written in the portable
|
|
9
|
+
Svelte 4 style that Svelte 5 runs in legacy mode).
|
|
10
|
+
|
|
11
|
+
## Install
|
|
12
|
+
|
|
13
|
+
```sh
|
|
14
|
+
npm install @griddle/svelte @griddle/core
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
`@griddle/core` and `svelte` are **peer dependencies**. On npm 7+ the peers are
|
|
18
|
+
installed automatically; with Yarn or pnpm, add `@griddle/core` yourself (as
|
|
19
|
+
shown above). `svelte` (^4 or ^5) is expected to already be in your app.
|
|
20
|
+
|
|
21
|
+
## Usage
|
|
22
|
+
|
|
23
|
+
```svelte
|
|
24
|
+
<script>
|
|
25
|
+
import { GriddleGrid, createGriddle } from '@griddle/svelte';
|
|
26
|
+
|
|
27
|
+
const api = createGriddle({
|
|
28
|
+
config: { cols: 12, rows: 12, unitWidth: 75, unitHeight: 75 },
|
|
29
|
+
tiles: [
|
|
30
|
+
{ id: '1', col: 0, row: 0, w: 2, h: 2 },
|
|
31
|
+
{ id: '2', col: 2, row: 0, w: 1, h: 1 },
|
|
32
|
+
],
|
|
33
|
+
});
|
|
34
|
+
</script>
|
|
35
|
+
|
|
36
|
+
<GriddleGrid {api}>
|
|
37
|
+
<div slot="tile" let:tile>#{tile.id}</div>
|
|
38
|
+
</GriddleGrid>
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
## Loop mode (infinite gallery)
|
|
42
|
+
|
|
43
|
+
Enable `loop` in the config and the content repeats endlessly with drag-to-pan
|
|
44
|
+
physics — no scrollbars:
|
|
45
|
+
|
|
46
|
+
```js
|
|
47
|
+
const api = createGriddle({
|
|
48
|
+
config: {
|
|
49
|
+
cols: 12, rows: 12, unitWidth: 120, unitHeight: 120,
|
|
50
|
+
loop: { enabled: true, interaction: 'pan' }, // 'pan' = viewer, 'edit' = ghost edit
|
|
51
|
+
},
|
|
52
|
+
tiles,
|
|
53
|
+
});
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
`<GriddleGrid>` automatically switches to the loop renderer when
|
|
57
|
+
`config.loop.enabled` is `true`, so most apps never touch the loop component
|
|
58
|
+
directly. For advanced cases where you want to render the loop plane explicitly,
|
|
59
|
+
`GriddleLoopGrid` is also exported and takes the same props as `GriddleGrid`:
|
|
60
|
+
|
|
61
|
+
```js
|
|
62
|
+
import { GriddleLoopGrid } from '@griddle/svelte';
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
See the [main repository](https://github.com/Trustybits/griddle) for full docs.
|
|
66
|
+
|
|
67
|
+
## License
|
|
68
|
+
|
|
69
|
+
MIT © Trustybits
|
|
@@ -0,0 +1,485 @@
|
|
|
1
|
+
<script>import { onMount, onDestroy, tick, createEventDispatcher } from 'svelte';
|
|
2
|
+
import { visibleRange, visibleTiles, gridContentSize, DragController, computeTileLayout, resolveStickyStacking, isOutOfFlow, pixelsToPin, } from '@griddle/core';
|
|
3
|
+
import LoopGrid from './LoopGrid.svelte';
|
|
4
|
+
export let api;
|
|
5
|
+
export let height = '100%';
|
|
6
|
+
export let showGrid = true;
|
|
7
|
+
const dispatch = createEventDispatcher();
|
|
8
|
+
const DEFAULT_DRAG_IGNORE = 'a, button, input, textarea, select, [contenteditable]';
|
|
9
|
+
let scrollEl;
|
|
10
|
+
let viewport = { scrollX: 0, scrollY: 0, width: 1000, height: 800 };
|
|
11
|
+
const cfgStore = api.config;
|
|
12
|
+
const tilesStore = api.tiles;
|
|
13
|
+
const versionStore = api.version;
|
|
14
|
+
$: cfg = $cfgStore;
|
|
15
|
+
$: tilesAll = $tilesStore;
|
|
16
|
+
$: ver = $versionStore;
|
|
17
|
+
// Loop mode delegates rendering to LoopGrid.
|
|
18
|
+
$: loopOn = cfg.loop?.enabled === true;
|
|
19
|
+
$: colSize = cfg.unitWidth + (cfg.gap ?? 0);
|
|
20
|
+
$: rowSize = cfg.unitHeight + (cfg.gap ?? 0);
|
|
21
|
+
$: halfGap = (cfg.gap ?? 0) / 2;
|
|
22
|
+
$: range = visibleRange(cfg, viewport, 4);
|
|
23
|
+
$: rendered = visibleTiles(tilesAll, range);
|
|
24
|
+
$: contentSize = gridContentSize(cfg, tilesAll);
|
|
25
|
+
// ---- drag / resize state ---------------------------------------------
|
|
26
|
+
// Drag uses the live-preview state machine in core: as the cursor crosses
|
|
27
|
+
// cell boundaries we rewind any prior preview and re-attempt the move
|
|
28
|
+
// against a snapshot taken at pickup. The dragged tile follows the cursor
|
|
29
|
+
// freely (unsnapped) for the duration of the gesture; a separate drop-
|
|
30
|
+
// indicator div renders at the snapped candidate cell.
|
|
31
|
+
const dragController = new DragController(api.grid);
|
|
32
|
+
let drag = null;
|
|
33
|
+
let dragStartPointerX = 0;
|
|
34
|
+
let dragStartPointerY = 0;
|
|
35
|
+
let pinDrag = null;
|
|
36
|
+
let resize = null;
|
|
37
|
+
const tileNodes = new Map();
|
|
38
|
+
const prevRects = new Map();
|
|
39
|
+
function registerNode(node, id) {
|
|
40
|
+
tileNodes.set(id, node);
|
|
41
|
+
return {
|
|
42
|
+
update(newId) {
|
|
43
|
+
if (newId !== id) {
|
|
44
|
+
tileNodes.delete(id);
|
|
45
|
+
id = newId;
|
|
46
|
+
tileNodes.set(id, node);
|
|
47
|
+
}
|
|
48
|
+
},
|
|
49
|
+
destroy() {
|
|
50
|
+
tileNodes.delete(id);
|
|
51
|
+
},
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
function onTilePointerDown(e, tile) {
|
|
55
|
+
if (tile.draggable === false)
|
|
56
|
+
return;
|
|
57
|
+
if (e.target.dataset.griddleHandle)
|
|
58
|
+
return;
|
|
59
|
+
const ignoreSelector = cfg.dragIgnoreFrom ?? DEFAULT_DRAG_IGNORE;
|
|
60
|
+
if (ignoreSelector && e.target.closest(ignoreSelector))
|
|
61
|
+
return;
|
|
62
|
+
e.currentTarget.setPointerCapture(e.pointerId);
|
|
63
|
+
if (cfg.enablePositioning && isOutOfFlow(tile)) {
|
|
64
|
+
const layout = computeTileLayout({
|
|
65
|
+
tile,
|
|
66
|
+
config: cfg,
|
|
67
|
+
scrollX: viewport.scrollX,
|
|
68
|
+
scrollY: viewport.scrollY,
|
|
69
|
+
viewportWidth: viewport.width,
|
|
70
|
+
viewportHeight: viewport.height,
|
|
71
|
+
});
|
|
72
|
+
const startPinPx = tile.position === 'fixed'
|
|
73
|
+
? { x: layout.left - viewport.scrollX, y: layout.top - viewport.scrollY }
|
|
74
|
+
: { x: layout.left, y: layout.top };
|
|
75
|
+
pinDrag = {
|
|
76
|
+
tileId: tile.id,
|
|
77
|
+
startPinPx,
|
|
78
|
+
startPointerX: e.clientX,
|
|
79
|
+
startPointerY: e.clientY,
|
|
80
|
+
};
|
|
81
|
+
dispatch('dragStart', { tileId: tile.id });
|
|
82
|
+
e.stopPropagation();
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
if (!dragController.start(tile.id))
|
|
86
|
+
return;
|
|
87
|
+
dragStartPointerX = e.clientX;
|
|
88
|
+
dragStartPointerY = e.clientY;
|
|
89
|
+
drag = {
|
|
90
|
+
tileId: tile.id,
|
|
91
|
+
pickupCol: tile.col,
|
|
92
|
+
pickupRow: tile.row,
|
|
93
|
+
deltaX: 0,
|
|
94
|
+
deltaY: 0,
|
|
95
|
+
indicatorCol: tile.col,
|
|
96
|
+
indicatorRow: tile.row,
|
|
97
|
+
};
|
|
98
|
+
dispatch('dragStart', { tileId: tile.id });
|
|
99
|
+
e.stopPropagation();
|
|
100
|
+
}
|
|
101
|
+
function onResizeHandleDown(e, tile, c) {
|
|
102
|
+
if (tile.resizable === false)
|
|
103
|
+
return;
|
|
104
|
+
e.currentTarget.setPointerCapture(e.pointerId);
|
|
105
|
+
resize = {
|
|
106
|
+
tileId: tile.id, corner: c,
|
|
107
|
+
startPointerX: e.clientX, startPointerY: e.clientY,
|
|
108
|
+
startW: tile.w, startH: tile.h,
|
|
109
|
+
startCol: tile.col, startRow: tile.row,
|
|
110
|
+
previewW: tile.w, previewH: tile.h,
|
|
111
|
+
previewCol: tile.col, previewRow: tile.row,
|
|
112
|
+
};
|
|
113
|
+
dispatch('resizeStart', { tileId: tile.id });
|
|
114
|
+
e.stopPropagation();
|
|
115
|
+
}
|
|
116
|
+
function onPointerMove(e) {
|
|
117
|
+
if (pinDrag) {
|
|
118
|
+
const dx = e.clientX - pinDrag.startPointerX;
|
|
119
|
+
const dy = e.clientY - pinDrag.startPointerY;
|
|
120
|
+
const newPinPx = {
|
|
121
|
+
x: pinDrag.startPinPx.x + dx,
|
|
122
|
+
y: pinDrag.startPinPx.y + dy,
|
|
123
|
+
};
|
|
124
|
+
const newPin = pixelsToPin(newPinPx, cfg);
|
|
125
|
+
api.grid.setTilePinned(pinDrag.tileId, newPin);
|
|
126
|
+
}
|
|
127
|
+
if (drag) {
|
|
128
|
+
const dx = e.clientX - dragStartPointerX;
|
|
129
|
+
const dy = e.clientY - dragStartPointerY;
|
|
130
|
+
const candidateCol = drag.pickupCol + Math.round(dx / colSize);
|
|
131
|
+
const candidateRow = drag.pickupRow + Math.round(dy / rowSize);
|
|
132
|
+
const result = dragController.update({ col: candidateCol, row: candidateRow });
|
|
133
|
+
drag = {
|
|
134
|
+
...drag,
|
|
135
|
+
deltaX: dx,
|
|
136
|
+
deltaY: dy,
|
|
137
|
+
indicatorCol: result.indicatorCell ? result.indicatorCell.col : null,
|
|
138
|
+
indicatorRow: result.indicatorCell ? result.indicatorCell.row : null,
|
|
139
|
+
};
|
|
140
|
+
// restoreTiles() doesn't emit change events, so force-sync the local
|
|
141
|
+
// tile list so FLIP picks up displaced tile resets (e.g. drag back to
|
|
142
|
+
// the pickup cell).
|
|
143
|
+
if (result.changed)
|
|
144
|
+
tilesAll = api.grid.tiles;
|
|
145
|
+
}
|
|
146
|
+
if (resize) {
|
|
147
|
+
const dx = e.clientX - resize.startPointerX;
|
|
148
|
+
const dy = e.clientY - resize.startPointerY;
|
|
149
|
+
let dw = 0, dh = 0, dcol = 0, drow = 0;
|
|
150
|
+
const stepsX = Math.round(dx / colSize);
|
|
151
|
+
const stepsY = Math.round(dy / rowSize);
|
|
152
|
+
if (resize.corner === 'se' || resize.corner === 'ne')
|
|
153
|
+
dw = stepsX;
|
|
154
|
+
if (resize.corner === 'se' || resize.corner === 'sw')
|
|
155
|
+
dh = stepsY;
|
|
156
|
+
if (resize.corner === 'sw' || resize.corner === 'nw') {
|
|
157
|
+
dw = -stepsX;
|
|
158
|
+
dcol = stepsX;
|
|
159
|
+
}
|
|
160
|
+
if (resize.corner === 'ne' || resize.corner === 'nw') {
|
|
161
|
+
dh = -stepsY;
|
|
162
|
+
drow = stepsY;
|
|
163
|
+
}
|
|
164
|
+
const tile = api.grid.getTile(resize.tileId);
|
|
165
|
+
const minW = tile?.minW ?? 1;
|
|
166
|
+
const minH = tile?.minH ?? 1;
|
|
167
|
+
const maxW = tile?.maxW ?? Infinity;
|
|
168
|
+
const maxH = tile?.maxH ?? Infinity;
|
|
169
|
+
const nW = Math.min(maxW, Math.max(minW, resize.startW + dw));
|
|
170
|
+
const nH = Math.min(maxH, Math.max(minH, resize.startH + dh));
|
|
171
|
+
const nC = resize.startCol + dcol;
|
|
172
|
+
const nR = resize.startRow + drow;
|
|
173
|
+
if (nW !== resize.previewW || nH !== resize.previewH || nC !== resize.previewCol || nR !== resize.previewRow) {
|
|
174
|
+
resize = { ...resize, previewW: nW, previewH: nH, previewCol: nC, previewRow: nR };
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
function onPointerUp() {
|
|
179
|
+
if (pinDrag) {
|
|
180
|
+
const tileId = pinDrag.tileId;
|
|
181
|
+
pinDrag = null;
|
|
182
|
+
dispatch('dragEnd', { tileId, committed: true });
|
|
183
|
+
}
|
|
184
|
+
if (drag) {
|
|
185
|
+
const tileId = drag.tileId;
|
|
186
|
+
const { committed } = dragController.end();
|
|
187
|
+
drag = null;
|
|
188
|
+
dispatch('dragEnd', { tileId, committed });
|
|
189
|
+
}
|
|
190
|
+
if (resize) {
|
|
191
|
+
const r = resize;
|
|
192
|
+
const t = api.grid.getTile(r.tileId);
|
|
193
|
+
let committed = false;
|
|
194
|
+
if (t) {
|
|
195
|
+
if (r.previewCol !== t.col || r.previewRow !== t.row) {
|
|
196
|
+
api.moveTile(r.tileId, { col: r.previewCol, row: r.previewRow });
|
|
197
|
+
}
|
|
198
|
+
if (r.previewW !== t.w || r.previewH !== t.h) {
|
|
199
|
+
committed = api.resizeTile(r.tileId, { w: r.previewW, h: r.previewH });
|
|
200
|
+
}
|
|
201
|
+
else {
|
|
202
|
+
committed = r.previewCol !== r.startCol || r.previewRow !== r.startRow
|
|
203
|
+
|| r.previewW !== r.startW || r.previewH !== r.startH;
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
resize = null;
|
|
207
|
+
dispatch('resizeEnd', { tileId: r.tileId, committed });
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
function updateViewport() {
|
|
211
|
+
if (!scrollEl)
|
|
212
|
+
return;
|
|
213
|
+
viewport = {
|
|
214
|
+
scrollX: scrollEl.scrollLeft,
|
|
215
|
+
scrollY: scrollEl.scrollTop,
|
|
216
|
+
width: scrollEl.clientWidth,
|
|
217
|
+
height: scrollEl.clientHeight,
|
|
218
|
+
};
|
|
219
|
+
}
|
|
220
|
+
let ro;
|
|
221
|
+
onMount(() => {
|
|
222
|
+
updateViewport();
|
|
223
|
+
// scrollEl is absent while loop mode delegates to LoopGrid.
|
|
224
|
+
if (scrollEl) {
|
|
225
|
+
scrollEl.addEventListener('scroll', updateViewport, { passive: true });
|
|
226
|
+
ro = new ResizeObserver(updateViewport);
|
|
227
|
+
ro.observe(scrollEl);
|
|
228
|
+
}
|
|
229
|
+
window.addEventListener('pointermove', onPointerMove);
|
|
230
|
+
window.addEventListener('pointerup', onPointerUp);
|
|
231
|
+
window.addEventListener('pointercancel', onPointerUp);
|
|
232
|
+
});
|
|
233
|
+
onDestroy(() => {
|
|
234
|
+
if (scrollEl)
|
|
235
|
+
scrollEl.removeEventListener('scroll', updateViewport);
|
|
236
|
+
ro?.disconnect();
|
|
237
|
+
window.removeEventListener('pointermove', onPointerMove);
|
|
238
|
+
window.removeEventListener('pointerup', onPointerUp);
|
|
239
|
+
window.removeEventListener('pointercancel', onPointerUp);
|
|
240
|
+
});
|
|
241
|
+
// FLIP animation on version change. Skip the active dragger — it has its
|
|
242
|
+
// own pixel-level transform driven by the cursor delta and we don't want
|
|
243
|
+
// FLIP to fight with that.
|
|
244
|
+
$: runFlip(ver);
|
|
245
|
+
async function runFlip(_v) {
|
|
246
|
+
await tick();
|
|
247
|
+
const draggerId = drag?.tileId ?? null;
|
|
248
|
+
for (const t of tilesAll) {
|
|
249
|
+
const x = t.col * colSize + halfGap;
|
|
250
|
+
const y = t.row * rowSize + halfGap;
|
|
251
|
+
const p = prevRects.get(t.id);
|
|
252
|
+
if (t.id === draggerId) {
|
|
253
|
+
// Keep prevRect synced so the post-drop FLIP doesn't double-jump.
|
|
254
|
+
prevRects.set(t.id, { x, y });
|
|
255
|
+
continue;
|
|
256
|
+
}
|
|
257
|
+
const node = tileNodes.get(t.id);
|
|
258
|
+
if (p && node) {
|
|
259
|
+
const dx = p.x - x;
|
|
260
|
+
const dy = p.y - y;
|
|
261
|
+
if (dx !== 0 || dy !== 0) {
|
|
262
|
+
node.style.transition = 'none';
|
|
263
|
+
node.style.transform = `translate(${dx}px, ${dy}px)`;
|
|
264
|
+
requestAnimationFrame(() => {
|
|
265
|
+
node.style.transition = 'transform 220ms cubic-bezier(.2,.7,.2,1)';
|
|
266
|
+
node.style.transform = 'translate(0,0)';
|
|
267
|
+
});
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
prevRects.set(t.id, { x, y });
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
// Compute layouts for ALL rendered tiles in one pass so we can run
|
|
274
|
+
// resolveStickyStacking afterward — that helper needs to see every sticky
|
|
275
|
+
// tile to compute how they push each other. Dragger and resize overrides
|
|
276
|
+
// take precedence over the engine's positioning fields.
|
|
277
|
+
$: tileLayouts = (() => {
|
|
278
|
+
const out = new Map();
|
|
279
|
+
const stickyEntries = [];
|
|
280
|
+
for (const tile of rendered) {
|
|
281
|
+
let layout;
|
|
282
|
+
if (resize?.tileId === tile.id) {
|
|
283
|
+
const w = resize.previewW;
|
|
284
|
+
const h = resize.previewH;
|
|
285
|
+
layout = {
|
|
286
|
+
left: resize.previewCol * colSize + halfGap,
|
|
287
|
+
top: resize.previewRow * rowSize + halfGap,
|
|
288
|
+
width: w * cfg.unitWidth + (w - 1) * (cfg.gap ?? 0),
|
|
289
|
+
height: h * cfg.unitHeight + (h - 1) * (cfg.gap ?? 0),
|
|
290
|
+
zIndex: 10,
|
|
291
|
+
effective: 'static',
|
|
292
|
+
};
|
|
293
|
+
}
|
|
294
|
+
else if (drag?.tileId === tile.id) {
|
|
295
|
+
layout = {
|
|
296
|
+
left: drag.pickupCol * colSize + halfGap,
|
|
297
|
+
top: drag.pickupRow * rowSize + halfGap,
|
|
298
|
+
width: tile.w * cfg.unitWidth + (tile.w - 1) * (cfg.gap ?? 0),
|
|
299
|
+
height: tile.h * cfg.unitHeight + (tile.h - 1) * (cfg.gap ?? 0),
|
|
300
|
+
transform: `translate(${drag.deltaX}px, ${drag.deltaY}px)`,
|
|
301
|
+
zIndex: 20,
|
|
302
|
+
effective: 'static',
|
|
303
|
+
};
|
|
304
|
+
}
|
|
305
|
+
else {
|
|
306
|
+
layout = computeTileLayout({
|
|
307
|
+
tile,
|
|
308
|
+
config: cfg,
|
|
309
|
+
scrollX: viewport.scrollX,
|
|
310
|
+
scrollY: viewport.scrollY,
|
|
311
|
+
viewportWidth: viewport.width,
|
|
312
|
+
viewportHeight: viewport.height,
|
|
313
|
+
});
|
|
314
|
+
if (layout.effective === 'sticky') {
|
|
315
|
+
stickyEntries.push({ tile, layout });
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
out.set(tile.id, layout);
|
|
319
|
+
}
|
|
320
|
+
if (stickyEntries.length > 1)
|
|
321
|
+
resolveStickyStacking(stickyEntries);
|
|
322
|
+
return out;
|
|
323
|
+
})();
|
|
324
|
+
function tileLayoutFor(tile) {
|
|
325
|
+
return tileLayouts.get(tile.id) ?? {
|
|
326
|
+
left: 0, top: 0, width: 0, height: 0, zIndex: 1, effective: 'static',
|
|
327
|
+
};
|
|
328
|
+
}
|
|
329
|
+
function isDragging(id) { return drag?.tileId === id || pinDrag?.tileId === id; }
|
|
330
|
+
function isResizing(id) { return resize?.tileId === id; }
|
|
331
|
+
// Drop indicator size matches the dragger's footprint.
|
|
332
|
+
$: indicatorRect = (() => {
|
|
333
|
+
if (!drag || drag.indicatorCol === null || drag.indicatorRow === null)
|
|
334
|
+
return null;
|
|
335
|
+
const t = api.grid.getTile(drag.tileId);
|
|
336
|
+
if (!t)
|
|
337
|
+
return null;
|
|
338
|
+
return {
|
|
339
|
+
left: drag.indicatorCol * colSize + halfGap,
|
|
340
|
+
top: drag.indicatorRow * rowSize + halfGap,
|
|
341
|
+
width: t.w * cfg.unitWidth + (t.w - 1) * (cfg.gap ?? 0),
|
|
342
|
+
height: t.h * cfg.unitHeight + (t.h - 1) * (cfg.gap ?? 0),
|
|
343
|
+
};
|
|
344
|
+
})();
|
|
345
|
+
</script>
|
|
346
|
+
|
|
347
|
+
{#if loopOn}
|
|
348
|
+
<LoopGrid
|
|
349
|
+
{api}
|
|
350
|
+
{height}
|
|
351
|
+
{showGrid}
|
|
352
|
+
on:dragStart
|
|
353
|
+
on:dragEnd
|
|
354
|
+
on:resizeStart
|
|
355
|
+
on:resizeEnd
|
|
356
|
+
on:cameraChange
|
|
357
|
+
>
|
|
358
|
+
<svelte:fragment slot="tile" let:tile>
|
|
359
|
+
<slot name="tile" {tile} />
|
|
360
|
+
</svelte:fragment>
|
|
361
|
+
</LoopGrid>
|
|
362
|
+
{:else}
|
|
363
|
+
<div
|
|
364
|
+
bind:this={scrollEl}
|
|
365
|
+
class="griddle-scroll"
|
|
366
|
+
style:height={typeof height === 'number' ? height + 'px' : height}
|
|
367
|
+
>
|
|
368
|
+
<div
|
|
369
|
+
class="griddle-content"
|
|
370
|
+
class:grid-bg={showGrid}
|
|
371
|
+
style:width={contentSize.width ? contentSize.width + 'px' : '100%'}
|
|
372
|
+
style:height={contentSize.height ? contentSize.height + 'px' : '100%'}
|
|
373
|
+
style:--colsize={colSize + 'px'}
|
|
374
|
+
style:--rowsize={rowSize + 'px'}
|
|
375
|
+
style:--griddle-tile-radius={(cfg.tileRadius ?? 4) + 'px'}
|
|
376
|
+
>
|
|
377
|
+
{#if indicatorRect}
|
|
378
|
+
<div
|
|
379
|
+
class="griddle-drop-indicator"
|
|
380
|
+
style:left={indicatorRect.left + 'px'}
|
|
381
|
+
style:top={indicatorRect.top + 'px'}
|
|
382
|
+
style:width={indicatorRect.width + 'px'}
|
|
383
|
+
style:height={indicatorRect.height + 'px'}
|
|
384
|
+
></div>
|
|
385
|
+
{/if}
|
|
386
|
+
{#each rendered as tile (tile.id)}
|
|
387
|
+
{@const layout = tileLayoutFor(tile)}
|
|
388
|
+
<div
|
|
389
|
+
class="griddle-tile"
|
|
390
|
+
class:griddle-dragging={isDragging(tile.id)}
|
|
391
|
+
class:griddle-resizing={isResizing(tile.id)}
|
|
392
|
+
class:griddle-relative={layout.effective === 'relative'}
|
|
393
|
+
class:griddle-absolute={layout.effective === 'absolute'}
|
|
394
|
+
class:griddle-fixed={layout.effective === 'fixed'}
|
|
395
|
+
class:griddle-sticky={layout.effective === 'sticky'}
|
|
396
|
+
data-griddle-tile={tile.id}
|
|
397
|
+
data-griddle-position={layout.effective}
|
|
398
|
+
use:registerNode={tile.id}
|
|
399
|
+
on:pointerdown={(e) => onTilePointerDown(e, tile)}
|
|
400
|
+
style:left={layout.left + 'px'}
|
|
401
|
+
style:top={layout.top + 'px'}
|
|
402
|
+
style:width={layout.width + 'px'}
|
|
403
|
+
style:height={layout.height + 'px'}
|
|
404
|
+
style:transform={layout.transform ?? ''}
|
|
405
|
+
style:z-index={layout.zIndex}
|
|
406
|
+
>
|
|
407
|
+
<slot name="tile" {tile} />
|
|
408
|
+
{#if tile.resizable !== false}
|
|
409
|
+
{#each (tile.resizeHandles ?? cfg.resizeHandles ?? ['se']) as c (c)}
|
|
410
|
+
<div
|
|
411
|
+
class="griddle-handle griddle-handle-{c}"
|
|
412
|
+
data-griddle-handle={c}
|
|
413
|
+
on:pointerdown={(e) => onResizeHandleDown(e, tile, c)}
|
|
414
|
+
></div>
|
|
415
|
+
{/each}
|
|
416
|
+
{/if}
|
|
417
|
+
</div>
|
|
418
|
+
{/each}
|
|
419
|
+
</div>
|
|
420
|
+
</div>
|
|
421
|
+
{/if}
|
|
422
|
+
|
|
423
|
+
<style>
|
|
424
|
+
.griddle-scroll {
|
|
425
|
+
position: relative;
|
|
426
|
+
overflow: auto;
|
|
427
|
+
touch-action: none;
|
|
428
|
+
}
|
|
429
|
+
.griddle-content {
|
|
430
|
+
position: relative;
|
|
431
|
+
min-width: 100%;
|
|
432
|
+
min-height: 100%;
|
|
433
|
+
}
|
|
434
|
+
.grid-bg {
|
|
435
|
+
background-image:
|
|
436
|
+
linear-gradient(to right, rgba(0,0,0,0.08) 1px, transparent 1px),
|
|
437
|
+
linear-gradient(to bottom, rgba(0,0,0,0.08) 1px, transparent 1px);
|
|
438
|
+
background-size: var(--colsize) var(--rowsize);
|
|
439
|
+
}
|
|
440
|
+
.griddle-tile {
|
|
441
|
+
position: absolute;
|
|
442
|
+
box-sizing: border-box;
|
|
443
|
+
cursor: grab;
|
|
444
|
+
user-select: none;
|
|
445
|
+
will-change: transform;
|
|
446
|
+
z-index: 1;
|
|
447
|
+
}
|
|
448
|
+
.griddle-tile.griddle-resizing {
|
|
449
|
+
z-index: 10;
|
|
450
|
+
opacity: 0.85;
|
|
451
|
+
cursor: grabbing;
|
|
452
|
+
}
|
|
453
|
+
.griddle-tile.griddle-dragging {
|
|
454
|
+
z-index: 20;
|
|
455
|
+
cursor: grabbing;
|
|
456
|
+
opacity: 0.85;
|
|
457
|
+
/* Lift the tile off the grid: drop shadow + subtle scale. The transform
|
|
458
|
+
is set inline (cursor delta), so the scale is folded into a CSS filter
|
|
459
|
+
to avoid clobbering it. */
|
|
460
|
+
filter: drop-shadow(0 8px 16px rgba(0, 0, 0, 0.18));
|
|
461
|
+
transition: filter 120ms ease-out, opacity 120ms ease-out;
|
|
462
|
+
}
|
|
463
|
+
.griddle-drop-indicator {
|
|
464
|
+
position: absolute;
|
|
465
|
+
box-sizing: border-box;
|
|
466
|
+
border: 2px dashed rgba(59, 91, 219, 0.55);
|
|
467
|
+
background: rgba(59, 91, 219, 0.08);
|
|
468
|
+
border-radius: var(--griddle-tile-radius, 4px);
|
|
469
|
+
pointer-events: none;
|
|
470
|
+
z-index: 5;
|
|
471
|
+
}
|
|
472
|
+
.griddle-handle {
|
|
473
|
+
position: absolute;
|
|
474
|
+
width: 12px;
|
|
475
|
+
height: 12px;
|
|
476
|
+
background: rgba(60,60,60,0.7);
|
|
477
|
+
border: 2px solid white;
|
|
478
|
+
border-radius: 3px;
|
|
479
|
+
touch-action: none;
|
|
480
|
+
}
|
|
481
|
+
.griddle-handle-nw { top: -6px; left: -6px; cursor: nw-resize; }
|
|
482
|
+
.griddle-handle-ne { top: -6px; right: -6px; cursor: ne-resize; }
|
|
483
|
+
.griddle-handle-sw { bottom: -6px; left: -6px; cursor: sw-resize; }
|
|
484
|
+
.griddle-handle-se { bottom: -6px; right: -6px; cursor: se-resize; }
|
|
485
|
+
</style>
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { SvelteComponent } from "svelte";
|
|
2
|
+
import type { Tile } from '@griddle/core';
|
|
3
|
+
import type { GriddleApi } from './griddleStore.js';
|
|
4
|
+
declare const __propDef: {
|
|
5
|
+
props: {
|
|
6
|
+
api: GriddleApi;
|
|
7
|
+
height?: number | string;
|
|
8
|
+
showGrid?: boolean;
|
|
9
|
+
};
|
|
10
|
+
events: {
|
|
11
|
+
dragStart: CustomEvent<any>;
|
|
12
|
+
dragEnd: CustomEvent<any>;
|
|
13
|
+
resizeStart: CustomEvent<any>;
|
|
14
|
+
resizeEnd: CustomEvent<any>;
|
|
15
|
+
cameraChange: CustomEvent<import("@griddle/core").CameraState>;
|
|
16
|
+
} & {
|
|
17
|
+
[evt: string]: CustomEvent<any>;
|
|
18
|
+
};
|
|
19
|
+
slots: {
|
|
20
|
+
tile: {
|
|
21
|
+
tile: Tile;
|
|
22
|
+
};
|
|
23
|
+
};
|
|
24
|
+
exports?: {} | undefined;
|
|
25
|
+
bindings?: string | undefined;
|
|
26
|
+
};
|
|
27
|
+
export type GriddleGridProps = typeof __propDef.props;
|
|
28
|
+
export type GriddleGridEvents = typeof __propDef.events;
|
|
29
|
+
export type GriddleGridSlots = typeof __propDef.slots;
|
|
30
|
+
export default class GriddleGrid extends SvelteComponent<GriddleGridProps, GriddleGridEvents, GriddleGridSlots> {
|
|
31
|
+
}
|
|
32
|
+
export {};
|
|
@@ -0,0 +1,521 @@
|
|
|
1
|
+
<script>// Loop-mode renderer ("object looping") — see @griddle/core loop.ts for the
|
|
2
|
+
// coordinate model. Mirrors the React GriddleLoopGrid: an overflow-hidden
|
|
3
|
+
// viewport, a transform-translated plane driven by an unbounded camera, and
|
|
4
|
+
// NO native scrolling (wheel deltas feed the camera directly).
|
|
5
|
+
import { onMount, onDestroy, tick, createEventDispatcher } from 'svelte';
|
|
6
|
+
import { DragController, PanController, loopInstances, loopPeriod, resolveLoop, } from '@griddle/core';
|
|
7
|
+
export let api;
|
|
8
|
+
export let height = '100%';
|
|
9
|
+
export let showGrid = true;
|
|
10
|
+
const dispatch = createEventDispatcher();
|
|
11
|
+
const DEFAULT_DRAG_IGNORE = 'a, button, input, textarea, select, [contenteditable]';
|
|
12
|
+
const PAN_THRESHOLD_PX = 4;
|
|
13
|
+
let viewportEl;
|
|
14
|
+
let planeEl;
|
|
15
|
+
const cfgStore = api.config;
|
|
16
|
+
const tilesStore = api.tiles;
|
|
17
|
+
const versionStore = api.version;
|
|
18
|
+
$: cfg = $cfgStore;
|
|
19
|
+
$: tilesAll = $tilesStore;
|
|
20
|
+
$: ver = $versionStore;
|
|
21
|
+
$: loop = resolveLoop(cfg);
|
|
22
|
+
$: editable = loop?.interaction === 'edit';
|
|
23
|
+
$: gapPx = cfg.gap ?? 0;
|
|
24
|
+
$: halfGap = gapPx / 2;
|
|
25
|
+
$: colSize = cfg.unitWidth + gapPx;
|
|
26
|
+
$: rowSize = cfg.unitHeight + gapPx;
|
|
27
|
+
$: period = loopPeriod(cfg, tilesAll);
|
|
28
|
+
// ---- camera plumbing ----------------------------------------------------
|
|
29
|
+
const pan = new PanController();
|
|
30
|
+
$: if (loop) {
|
|
31
|
+
pan.setPhysics({ friction: loop.friction, ease: loop.ease, maxVelocity: loop.maxVelocity });
|
|
32
|
+
}
|
|
33
|
+
let view = { cxCell: 0, cyCell: 0, vw: 1000, vh: 800 };
|
|
34
|
+
let lastCam = null;
|
|
35
|
+
let raf = 0;
|
|
36
|
+
let resizeObserver = null;
|
|
37
|
+
function onWheel(e) {
|
|
38
|
+
e.preventDefault();
|
|
39
|
+
const k = e.deltaMode === 1 ? 16 : e.deltaMode === 2 ? 100 : 1;
|
|
40
|
+
pan.scrollBy(e.deltaX * k, e.deltaY * k);
|
|
41
|
+
}
|
|
42
|
+
function frame(now) {
|
|
43
|
+
raf = requestAnimationFrame(frame);
|
|
44
|
+
const st = pan.tick(now);
|
|
45
|
+
if (planeEl) {
|
|
46
|
+
planeEl.style.transform = `translate3d(${-st.x}px, ${-st.y}px, 0)`;
|
|
47
|
+
}
|
|
48
|
+
if (viewportEl && showGrid) {
|
|
49
|
+
viewportEl.style.backgroundPosition = `${-st.x % colSize}px ${-st.y % rowSize}px`;
|
|
50
|
+
}
|
|
51
|
+
const cxCell = Math.floor(st.x / colSize);
|
|
52
|
+
const cyCell = Math.floor(st.y / rowSize);
|
|
53
|
+
if (cxCell !== view.cxCell || cyCell !== view.cyCell) {
|
|
54
|
+
view = { ...view, cxCell, cyCell };
|
|
55
|
+
}
|
|
56
|
+
if (!lastCam || lastCam.x !== st.x || lastCam.y !== st.y ||
|
|
57
|
+
lastCam.isMoving !== st.isMoving || lastCam.isDragging !== st.isDragging) {
|
|
58
|
+
lastCam = st;
|
|
59
|
+
dispatch('cameraChange', st);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
$: instances = (() => {
|
|
63
|
+
void ver; // re-run on grid changes
|
|
64
|
+
const bufX = 2 * colSize;
|
|
65
|
+
const bufY = 2 * rowSize;
|
|
66
|
+
return loopInstances(cfg, tilesAll, {
|
|
67
|
+
x: view.cxCell * colSize - bufX,
|
|
68
|
+
y: view.cyCell * rowSize - bufY,
|
|
69
|
+
width: view.vw + colSize + 2 * bufX,
|
|
70
|
+
height: view.vh + rowSize + 2 * bufY,
|
|
71
|
+
});
|
|
72
|
+
})();
|
|
73
|
+
// ---- pan gesture ----------------------------------------------------------
|
|
74
|
+
let panGesture = null;
|
|
75
|
+
let suppressClick = false;
|
|
76
|
+
function onContainerPointerDown(e) {
|
|
77
|
+
// Tiles that start drags stopPropagation, so reaching here from a tile
|
|
78
|
+
// means it was not draggable — panning is the right fallback.
|
|
79
|
+
if (!loop?.dragPan || e.button !== 0)
|
|
80
|
+
return;
|
|
81
|
+
const ignoreSelector = cfg.dragIgnoreFrom ?? DEFAULT_DRAG_IGNORE;
|
|
82
|
+
if (ignoreSelector && e.target.closest(ignoreSelector))
|
|
83
|
+
return;
|
|
84
|
+
panGesture = { pointerId: e.pointerId, startX: e.clientX, startY: e.clientY, moved: false };
|
|
85
|
+
}
|
|
86
|
+
function onClickCapture(e) {
|
|
87
|
+
if (suppressClick) {
|
|
88
|
+
suppressClick = false;
|
|
89
|
+
e.preventDefault();
|
|
90
|
+
e.stopPropagation();
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
// ---- tile drag / resize ('edit' interaction) ----------------------------
|
|
94
|
+
const dragController = new DragController(api.grid);
|
|
95
|
+
let drag = null;
|
|
96
|
+
let dragStartPointerX = 0;
|
|
97
|
+
let dragStartPointerY = 0;
|
|
98
|
+
let resize = null;
|
|
99
|
+
function isBase(inst) {
|
|
100
|
+
return inst.kx === 0 && inst.ky === 0;
|
|
101
|
+
}
|
|
102
|
+
function onTilePointerDown(e, inst) {
|
|
103
|
+
if (!editable)
|
|
104
|
+
return; // pan mode: container pan handler takes it
|
|
105
|
+
if (!isBase(inst))
|
|
106
|
+
return; // ghosts are display-only
|
|
107
|
+
const tile = inst.tile;
|
|
108
|
+
if (tile.draggable === false)
|
|
109
|
+
return;
|
|
110
|
+
if (e.target.dataset.griddleHandle)
|
|
111
|
+
return;
|
|
112
|
+
const ignoreSelector = cfg.dragIgnoreFrom ?? DEFAULT_DRAG_IGNORE;
|
|
113
|
+
if (ignoreSelector && e.target.closest(ignoreSelector))
|
|
114
|
+
return;
|
|
115
|
+
// No pointer capture: the grabbed instance unmounts (drag renders as an
|
|
116
|
+
// overlay), which would release the capture mid-gesture.
|
|
117
|
+
if (!dragController.start(tile.id))
|
|
118
|
+
return;
|
|
119
|
+
dragStartPointerX = e.clientX;
|
|
120
|
+
dragStartPointerY = e.clientY;
|
|
121
|
+
drag = {
|
|
122
|
+
tileId: tile.id,
|
|
123
|
+
instanceLeft: inst.left,
|
|
124
|
+
instanceTop: inst.top,
|
|
125
|
+
pickupCol: tile.col,
|
|
126
|
+
pickupRow: tile.row,
|
|
127
|
+
deltaX: 0,
|
|
128
|
+
deltaY: 0,
|
|
129
|
+
indicatorCol: tile.col,
|
|
130
|
+
indicatorRow: tile.row,
|
|
131
|
+
};
|
|
132
|
+
dispatch('dragStart', { tileId: tile.id });
|
|
133
|
+
e.stopPropagation();
|
|
134
|
+
}
|
|
135
|
+
function onResizeHandleDown(e, inst, c) {
|
|
136
|
+
if (!editable)
|
|
137
|
+
return;
|
|
138
|
+
const tile = inst.tile;
|
|
139
|
+
if (tile.resizable === false)
|
|
140
|
+
return;
|
|
141
|
+
e.currentTarget.setPointerCapture(e.pointerId);
|
|
142
|
+
const baseLeft = tile.col * colSize + halfGap;
|
|
143
|
+
const baseTop = tile.row * rowSize + halfGap;
|
|
144
|
+
resize = {
|
|
145
|
+
tileId: tile.id,
|
|
146
|
+
instanceKey: inst.key,
|
|
147
|
+
instanceDx: inst.left - baseLeft,
|
|
148
|
+
instanceDy: inst.top - baseTop,
|
|
149
|
+
corner: c,
|
|
150
|
+
startPointerX: e.clientX,
|
|
151
|
+
startPointerY: e.clientY,
|
|
152
|
+
startW: tile.w,
|
|
153
|
+
startH: tile.h,
|
|
154
|
+
startCol: tile.col,
|
|
155
|
+
startRow: tile.row,
|
|
156
|
+
previewW: tile.w,
|
|
157
|
+
previewH: tile.h,
|
|
158
|
+
previewCol: tile.col,
|
|
159
|
+
previewRow: tile.row,
|
|
160
|
+
};
|
|
161
|
+
dispatch('resizeStart', { tileId: tile.id });
|
|
162
|
+
e.stopPropagation();
|
|
163
|
+
}
|
|
164
|
+
function onPointerMove(e) {
|
|
165
|
+
if (panGesture && e.pointerId === panGesture.pointerId) {
|
|
166
|
+
const now = performance.now();
|
|
167
|
+
if (!panGesture.moved) {
|
|
168
|
+
const dist = Math.hypot(e.clientX - panGesture.startX, e.clientY - panGesture.startY);
|
|
169
|
+
if (dist >= PAN_THRESHOLD_PX) {
|
|
170
|
+
panGesture.moved = true;
|
|
171
|
+
pan.dragStart(panGesture.startX, panGesture.startY, now);
|
|
172
|
+
pan.dragMove(e.clientX, e.clientY, now);
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
else {
|
|
176
|
+
pan.dragMove(e.clientX, e.clientY, now);
|
|
177
|
+
}
|
|
178
|
+
return;
|
|
179
|
+
}
|
|
180
|
+
if (drag) {
|
|
181
|
+
// Ghost edit: plain grid semantics in the base copy — no wrapping.
|
|
182
|
+
const dx = e.clientX - dragStartPointerX;
|
|
183
|
+
const dy = e.clientY - dragStartPointerY;
|
|
184
|
+
const candidate = {
|
|
185
|
+
col: drag.pickupCol + Math.round(dx / colSize),
|
|
186
|
+
row: drag.pickupRow + Math.round(dy / rowSize),
|
|
187
|
+
};
|
|
188
|
+
const result = dragController.update(candidate);
|
|
189
|
+
drag = {
|
|
190
|
+
...drag,
|
|
191
|
+
deltaX: dx,
|
|
192
|
+
deltaY: dy,
|
|
193
|
+
indicatorCol: result.indicatorCell ? result.indicatorCell.col : null,
|
|
194
|
+
indicatorRow: result.indicatorCell ? result.indicatorCell.row : null,
|
|
195
|
+
};
|
|
196
|
+
// restoreTiles() doesn't emit change events; force-sync the local list.
|
|
197
|
+
if (result.changed)
|
|
198
|
+
tilesAll = api.grid.tiles;
|
|
199
|
+
}
|
|
200
|
+
if (resize) {
|
|
201
|
+
const dx = e.clientX - resize.startPointerX;
|
|
202
|
+
const dy = e.clientY - resize.startPointerY;
|
|
203
|
+
let dw = 0, dh = 0, dcol = 0, drow = 0;
|
|
204
|
+
const stepsX = Math.round(dx / colSize);
|
|
205
|
+
const stepsY = Math.round(dy / rowSize);
|
|
206
|
+
if (resize.corner === 'se' || resize.corner === 'ne')
|
|
207
|
+
dw = stepsX;
|
|
208
|
+
if (resize.corner === 'se' || resize.corner === 'sw')
|
|
209
|
+
dh = stepsY;
|
|
210
|
+
if (resize.corner === 'sw' || resize.corner === 'nw') {
|
|
211
|
+
dw = -stepsX;
|
|
212
|
+
dcol = stepsX;
|
|
213
|
+
}
|
|
214
|
+
if (resize.corner === 'ne' || resize.corner === 'nw') {
|
|
215
|
+
dh = -stepsY;
|
|
216
|
+
drow = stepsY;
|
|
217
|
+
}
|
|
218
|
+
const tile = api.grid.getTile(resize.tileId);
|
|
219
|
+
const minW = tile?.minW ?? 1;
|
|
220
|
+
const minH = tile?.minH ?? 1;
|
|
221
|
+
const maxW = Math.min(tile?.maxW ?? Infinity, cfg.cols);
|
|
222
|
+
const maxH = Math.min(tile?.maxH ?? Infinity, cfg.rows);
|
|
223
|
+
const nW = Math.min(maxW, Math.max(minW, resize.startW + dw));
|
|
224
|
+
const nH = Math.min(maxH, Math.max(minH, resize.startH + dh));
|
|
225
|
+
const nC = resize.startCol + dcol;
|
|
226
|
+
const nR = resize.startRow + drow;
|
|
227
|
+
if (nW !== resize.previewW || nH !== resize.previewH || nC !== resize.previewCol || nR !== resize.previewRow) {
|
|
228
|
+
resize = { ...resize, previewW: nW, previewH: nH, previewCol: nC, previewRow: nR };
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
function onPointerUp(e) {
|
|
233
|
+
if (panGesture && e.pointerId === panGesture.pointerId) {
|
|
234
|
+
if (panGesture.moved) {
|
|
235
|
+
pan.dragEnd(performance.now());
|
|
236
|
+
suppressClick = true;
|
|
237
|
+
}
|
|
238
|
+
panGesture = null;
|
|
239
|
+
return;
|
|
240
|
+
}
|
|
241
|
+
if (drag) {
|
|
242
|
+
const tileId = drag.tileId;
|
|
243
|
+
const { committed } = dragController.end();
|
|
244
|
+
drag = null;
|
|
245
|
+
tilesAll = api.grid.tiles;
|
|
246
|
+
dispatch('dragEnd', { tileId, committed });
|
|
247
|
+
}
|
|
248
|
+
if (resize) {
|
|
249
|
+
const r = resize;
|
|
250
|
+
const t = api.grid.getTile(r.tileId);
|
|
251
|
+
let committed = false;
|
|
252
|
+
if (t) {
|
|
253
|
+
const target = { col: r.previewCol, row: r.previewRow };
|
|
254
|
+
if (target.col !== t.col || target.row !== t.row) {
|
|
255
|
+
api.moveTile(r.tileId, target);
|
|
256
|
+
}
|
|
257
|
+
if (r.previewW !== t.w || r.previewH !== t.h) {
|
|
258
|
+
committed = api.resizeTile(r.tileId, { w: r.previewW, h: r.previewH });
|
|
259
|
+
}
|
|
260
|
+
else {
|
|
261
|
+
committed = r.previewCol !== r.startCol || r.previewRow !== r.startRow
|
|
262
|
+
|| r.previewW !== r.startW || r.previewH !== r.startH;
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
resize = null;
|
|
266
|
+
dispatch('resizeEnd', { tileId: r.tileId, committed });
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
onMount(() => {
|
|
270
|
+
if (viewportEl) {
|
|
271
|
+
viewportEl.addEventListener('wheel', onWheel, { passive: false });
|
|
272
|
+
const measure = () => {
|
|
273
|
+
if (viewportEl.clientWidth !== view.vw || viewportEl.clientHeight !== view.vh) {
|
|
274
|
+
view = { ...view, vw: viewportEl.clientWidth, vh: viewportEl.clientHeight };
|
|
275
|
+
}
|
|
276
|
+
};
|
|
277
|
+
measure();
|
|
278
|
+
resizeObserver = new ResizeObserver(measure);
|
|
279
|
+
resizeObserver.observe(viewportEl);
|
|
280
|
+
}
|
|
281
|
+
raf = requestAnimationFrame(frame);
|
|
282
|
+
window.addEventListener('pointermove', onPointerMove);
|
|
283
|
+
window.addEventListener('pointerup', onPointerUp);
|
|
284
|
+
window.addEventListener('pointercancel', onPointerUp);
|
|
285
|
+
});
|
|
286
|
+
onDestroy(() => {
|
|
287
|
+
cancelAnimationFrame(raf);
|
|
288
|
+
if (viewportEl)
|
|
289
|
+
viewportEl.removeEventListener('wheel', onWheel);
|
|
290
|
+
resizeObserver?.disconnect();
|
|
291
|
+
window.removeEventListener('pointermove', onPointerMove);
|
|
292
|
+
window.removeEventListener('pointerup', onPointerUp);
|
|
293
|
+
window.removeEventListener('pointercancel', onPointerUp);
|
|
294
|
+
});
|
|
295
|
+
// ---- FLIP for edit-mode repacks (keyed by world key) --------------------
|
|
296
|
+
const tileNodes = new Map();
|
|
297
|
+
const prevRects = new Map();
|
|
298
|
+
function registerNode(node, key) {
|
|
299
|
+
tileNodes.set(key, node);
|
|
300
|
+
return {
|
|
301
|
+
update(newKey) {
|
|
302
|
+
if (newKey !== key) {
|
|
303
|
+
tileNodes.delete(key);
|
|
304
|
+
key = newKey;
|
|
305
|
+
tileNodes.set(key, node);
|
|
306
|
+
}
|
|
307
|
+
},
|
|
308
|
+
destroy() {
|
|
309
|
+
tileNodes.delete(key);
|
|
310
|
+
},
|
|
311
|
+
};
|
|
312
|
+
}
|
|
313
|
+
$: runFlip(ver);
|
|
314
|
+
async function runFlip(_v) {
|
|
315
|
+
await tick();
|
|
316
|
+
const draggerId = drag?.tileId ?? null;
|
|
317
|
+
const seen = new Set();
|
|
318
|
+
for (const inst of instances) {
|
|
319
|
+
const key = inst.key;
|
|
320
|
+
seen.add(key);
|
|
321
|
+
const x = inst.left;
|
|
322
|
+
const y = inst.top;
|
|
323
|
+
if (editable && inst.tile.id !== draggerId) {
|
|
324
|
+
const p = prevRects.get(key);
|
|
325
|
+
const node = tileNodes.get(key);
|
|
326
|
+
if (p && node) {
|
|
327
|
+
const dx = p.x - x;
|
|
328
|
+
const dy = p.y - y;
|
|
329
|
+
if ((dx !== 0 || dy !== 0) &&
|
|
330
|
+
Math.abs(dx) < period.width / 2 &&
|
|
331
|
+
Math.abs(dy) < period.height / 2) {
|
|
332
|
+
node.style.transition = 'none';
|
|
333
|
+
node.style.transform = `translate(${dx}px, ${dy}px)`;
|
|
334
|
+
requestAnimationFrame(() => {
|
|
335
|
+
node.style.transition = 'transform 220ms cubic-bezier(.2,.7,.2,1)';
|
|
336
|
+
node.style.transform = 'translate(0,0)';
|
|
337
|
+
});
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
prevRects.set(key, { x, y });
|
|
342
|
+
}
|
|
343
|
+
for (const key of prevRects.keys()) {
|
|
344
|
+
if (!seen.has(key))
|
|
345
|
+
prevRects.delete(key);
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
// ---- drop indicator + drag overlay ---------------------------------------
|
|
349
|
+
// Indicator renders in the base copy (ghost edit is plain-grid).
|
|
350
|
+
$: indicatorRect = (() => {
|
|
351
|
+
if (!drag || drag.indicatorCol === null || drag.indicatorRow === null)
|
|
352
|
+
return null;
|
|
353
|
+
const t = api.grid.getTile(drag.tileId);
|
|
354
|
+
if (!t)
|
|
355
|
+
return null;
|
|
356
|
+
return {
|
|
357
|
+
left: drag.indicatorCol * colSize + halfGap,
|
|
358
|
+
top: drag.indicatorRow * rowSize + halfGap,
|
|
359
|
+
width: t.w * cfg.unitWidth + (t.w - 1) * gapPx,
|
|
360
|
+
height: t.h * cfg.unitHeight + (t.h - 1) * gapPx,
|
|
361
|
+
};
|
|
362
|
+
})();
|
|
363
|
+
$: draggedTile = drag ? api.grid.getTile(drag.tileId) ?? null : null;
|
|
364
|
+
function instanceLayout(inst, r) {
|
|
365
|
+
if (r && r.instanceKey === inst.key) {
|
|
366
|
+
return {
|
|
367
|
+
left: r.previewCol * colSize + halfGap + r.instanceDx,
|
|
368
|
+
top: r.previewRow * rowSize + halfGap + r.instanceDy,
|
|
369
|
+
width: r.previewW * cfg.unitWidth + (r.previewW - 1) * gapPx,
|
|
370
|
+
height: r.previewH * cfg.unitHeight + (r.previewH - 1) * gapPx,
|
|
371
|
+
};
|
|
372
|
+
}
|
|
373
|
+
return { left: inst.left, top: inst.top, width: inst.width, height: inst.height };
|
|
374
|
+
}
|
|
375
|
+
</script>
|
|
376
|
+
|
|
377
|
+
<!-- svelte-ignore a11y-click-events-have-key-events a11y-no-static-element-interactions -->
|
|
378
|
+
<div
|
|
379
|
+
bind:this={viewportEl}
|
|
380
|
+
class="griddle-viewport"
|
|
381
|
+
class:griddle-pannable={!editable && loop?.dragPan}
|
|
382
|
+
class:grid-bg={showGrid}
|
|
383
|
+
style:height={typeof height === 'number' ? height + 'px' : height}
|
|
384
|
+
style:--colsize={colSize + 'px'}
|
|
385
|
+
style:--rowsize={rowSize + 'px'}
|
|
386
|
+
style:--griddle-tile-radius={(cfg.tileRadius ?? 4) + 'px'}
|
|
387
|
+
on:pointerdown={onContainerPointerDown}
|
|
388
|
+
on:click|capture={onClickCapture}
|
|
389
|
+
>
|
|
390
|
+
<div bind:this={planeEl} class="griddle-plane">
|
|
391
|
+
{#if indicatorRect}
|
|
392
|
+
<div
|
|
393
|
+
class="griddle-drop-indicator"
|
|
394
|
+
style:left={indicatorRect.left + 'px'}
|
|
395
|
+
style:top={indicatorRect.top + 'px'}
|
|
396
|
+
style:width={indicatorRect.width + 'px'}
|
|
397
|
+
style:height={indicatorRect.height + 'px'}
|
|
398
|
+
></div>
|
|
399
|
+
{/if}
|
|
400
|
+
{#each instances.filter((i) => !(drag && i.tile.id === drag.tileId)) as inst (inst.key)}
|
|
401
|
+
{@const layout = instanceLayout(inst, resize)}
|
|
402
|
+
{@const ghost = editable && !isBase(inst)}
|
|
403
|
+
<div
|
|
404
|
+
class="griddle-tile"
|
|
405
|
+
class:griddle-editable={editable && isBase(inst)}
|
|
406
|
+
class:griddle-ghost={ghost}
|
|
407
|
+
class:griddle-resizing={resize?.instanceKey === inst.key}
|
|
408
|
+
data-griddle-tile={inst.tile.id}
|
|
409
|
+
data-griddle-instance={inst.key}
|
|
410
|
+
data-griddle-ghost={ghost ? '' : undefined}
|
|
411
|
+
aria-hidden={isBase(inst) ? undefined : true}
|
|
412
|
+
use:registerNode={inst.key}
|
|
413
|
+
on:pointerdown={(e) => onTilePointerDown(e, inst)}
|
|
414
|
+
style:left={layout.left + 'px'}
|
|
415
|
+
style:top={layout.top + 'px'}
|
|
416
|
+
style:width={layout.width + 'px'}
|
|
417
|
+
style:height={layout.height + 'px'}
|
|
418
|
+
>
|
|
419
|
+
<slot name="tile" tile={inst.tile} />
|
|
420
|
+
{#if editable && isBase(inst) && inst.tile.resizable !== false}
|
|
421
|
+
{#each (inst.tile.resizeHandles ?? cfg.resizeHandles ?? ['se']) as c (c)}
|
|
422
|
+
<div
|
|
423
|
+
class="griddle-handle griddle-handle-{c}"
|
|
424
|
+
data-griddle-handle={c}
|
|
425
|
+
on:pointerdown={(e) => onResizeHandleDown(e, inst, c)}
|
|
426
|
+
></div>
|
|
427
|
+
{/each}
|
|
428
|
+
{/if}
|
|
429
|
+
</div>
|
|
430
|
+
{/each}
|
|
431
|
+
{#if drag && draggedTile}
|
|
432
|
+
<div
|
|
433
|
+
class="griddle-tile griddle-drag-overlay"
|
|
434
|
+
data-griddle-tile={draggedTile.id}
|
|
435
|
+
style:left={drag.instanceLeft + 'px'}
|
|
436
|
+
style:top={drag.instanceTop + 'px'}
|
|
437
|
+
style:width={draggedTile.w * cfg.unitWidth + (draggedTile.w - 1) * gapPx + 'px'}
|
|
438
|
+
style:height={draggedTile.h * cfg.unitHeight + (draggedTile.h - 1) * gapPx + 'px'}
|
|
439
|
+
style:transform={`translate(${drag.deltaX}px, ${drag.deltaY}px)`}
|
|
440
|
+
>
|
|
441
|
+
<slot name="tile" tile={draggedTile} />
|
|
442
|
+
</div>
|
|
443
|
+
{/if}
|
|
444
|
+
</div>
|
|
445
|
+
</div>
|
|
446
|
+
|
|
447
|
+
<style>
|
|
448
|
+
.griddle-viewport {
|
|
449
|
+
position: relative;
|
|
450
|
+
overflow: hidden;
|
|
451
|
+
touch-action: none;
|
|
452
|
+
user-select: none;
|
|
453
|
+
}
|
|
454
|
+
.griddle-viewport.griddle-pannable {
|
|
455
|
+
cursor: grab;
|
|
456
|
+
}
|
|
457
|
+
.grid-bg {
|
|
458
|
+
background-image:
|
|
459
|
+
linear-gradient(to right, rgba(0,0,0,0.08) 1px, transparent 1px),
|
|
460
|
+
linear-gradient(to bottom, rgba(0,0,0,0.08) 1px, transparent 1px);
|
|
461
|
+
background-size: var(--colsize) var(--rowsize);
|
|
462
|
+
}
|
|
463
|
+
.griddle-plane {
|
|
464
|
+
position: absolute;
|
|
465
|
+
top: 0;
|
|
466
|
+
left: 0;
|
|
467
|
+
width: 0;
|
|
468
|
+
height: 0;
|
|
469
|
+
will-change: transform;
|
|
470
|
+
}
|
|
471
|
+
.griddle-tile {
|
|
472
|
+
position: absolute;
|
|
473
|
+
box-sizing: border-box;
|
|
474
|
+
user-select: none;
|
|
475
|
+
z-index: 1;
|
|
476
|
+
border-radius: var(--griddle-tile-radius, 4px);
|
|
477
|
+
}
|
|
478
|
+
.griddle-tile.griddle-editable {
|
|
479
|
+
cursor: grab;
|
|
480
|
+
}
|
|
481
|
+
/* Ghost repeats are display-only: pointer-transparent (the gesture falls
|
|
482
|
+
through to drag-pan) and dimmed to mark the editable copy. */
|
|
483
|
+
.griddle-tile.griddle-ghost {
|
|
484
|
+
pointer-events: none;
|
|
485
|
+
opacity: 0.55;
|
|
486
|
+
}
|
|
487
|
+
.griddle-tile.griddle-resizing {
|
|
488
|
+
z-index: 10;
|
|
489
|
+
opacity: 0.85;
|
|
490
|
+
cursor: grabbing;
|
|
491
|
+
}
|
|
492
|
+
.griddle-tile.griddle-drag-overlay {
|
|
493
|
+
z-index: 20;
|
|
494
|
+
cursor: grabbing;
|
|
495
|
+
opacity: 0.85;
|
|
496
|
+
pointer-events: none;
|
|
497
|
+
filter: drop-shadow(0 8px 16px rgba(0, 0, 0, 0.18));
|
|
498
|
+
}
|
|
499
|
+
.griddle-drop-indicator {
|
|
500
|
+
position: absolute;
|
|
501
|
+
box-sizing: border-box;
|
|
502
|
+
border: 2px dashed rgba(59, 91, 219, 0.55);
|
|
503
|
+
background: rgba(59, 91, 219, 0.08);
|
|
504
|
+
border-radius: var(--griddle-tile-radius, 4px);
|
|
505
|
+
pointer-events: none;
|
|
506
|
+
z-index: 5;
|
|
507
|
+
}
|
|
508
|
+
.griddle-handle {
|
|
509
|
+
position: absolute;
|
|
510
|
+
width: 12px;
|
|
511
|
+
height: 12px;
|
|
512
|
+
background: rgba(60,60,60,0.7);
|
|
513
|
+
border: 2px solid white;
|
|
514
|
+
border-radius: 3px;
|
|
515
|
+
touch-action: none;
|
|
516
|
+
}
|
|
517
|
+
.griddle-handle-nw { top: -6px; left: -6px; cursor: nw-resize; }
|
|
518
|
+
.griddle-handle-ne { top: -6px; right: -6px; cursor: ne-resize; }
|
|
519
|
+
.griddle-handle-sw { bottom: -6px; left: -6px; cursor: sw-resize; }
|
|
520
|
+
.griddle-handle-se { bottom: -6px; right: -6px; cursor: se-resize; }
|
|
521
|
+
</style>
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { SvelteComponent } from "svelte";
|
|
2
|
+
import type { CameraState } from '@griddle/core';
|
|
3
|
+
import type { GriddleApi } from './griddleStore.js';
|
|
4
|
+
declare const __propDef: {
|
|
5
|
+
props: {
|
|
6
|
+
api: GriddleApi;
|
|
7
|
+
height?: number | string;
|
|
8
|
+
showGrid?: boolean;
|
|
9
|
+
};
|
|
10
|
+
events: {
|
|
11
|
+
dragStart: CustomEvent<{
|
|
12
|
+
tileId: string;
|
|
13
|
+
}>;
|
|
14
|
+
dragEnd: CustomEvent<{
|
|
15
|
+
tileId: string;
|
|
16
|
+
committed: boolean;
|
|
17
|
+
}>;
|
|
18
|
+
resizeStart: CustomEvent<{
|
|
19
|
+
tileId: string;
|
|
20
|
+
}>;
|
|
21
|
+
resizeEnd: CustomEvent<{
|
|
22
|
+
tileId: string;
|
|
23
|
+
committed: boolean;
|
|
24
|
+
}>;
|
|
25
|
+
cameraChange: CustomEvent<CameraState>;
|
|
26
|
+
} & {
|
|
27
|
+
[evt: string]: CustomEvent<any>;
|
|
28
|
+
};
|
|
29
|
+
slots: {
|
|
30
|
+
tile: {
|
|
31
|
+
tile: import("@griddle/core").Tile | null;
|
|
32
|
+
};
|
|
33
|
+
};
|
|
34
|
+
exports?: {} | undefined;
|
|
35
|
+
bindings?: string | undefined;
|
|
36
|
+
};
|
|
37
|
+
export type LoopGridProps = typeof __propDef.props;
|
|
38
|
+
export type LoopGridEvents = typeof __propDef.events;
|
|
39
|
+
export type LoopGridSlots = typeof __propDef.slots;
|
|
40
|
+
export default class LoopGrid extends SvelteComponent<LoopGridProps, LoopGridEvents, LoopGridSlots> {
|
|
41
|
+
}
|
|
42
|
+
export {};
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { type Readable } from 'svelte/store';
|
|
2
|
+
import { Grid } from '@griddle/core';
|
|
3
|
+
import type { GridConfig, Tile, GridSnapshot } from '@griddle/core';
|
|
4
|
+
export interface UseGriddleInit {
|
|
5
|
+
config: GridConfig;
|
|
6
|
+
tiles?: Tile[];
|
|
7
|
+
}
|
|
8
|
+
export interface GriddleApi {
|
|
9
|
+
grid: Grid;
|
|
10
|
+
tiles: Readable<Tile[]>;
|
|
11
|
+
config: Readable<GridConfig>;
|
|
12
|
+
version: Readable<number>;
|
|
13
|
+
moveTile: (id: string, target: {
|
|
14
|
+
col: number;
|
|
15
|
+
row: number;
|
|
16
|
+
}) => boolean;
|
|
17
|
+
resizeTile: (id: string, size: {
|
|
18
|
+
w: number;
|
|
19
|
+
h: number;
|
|
20
|
+
}) => boolean;
|
|
21
|
+
addTile: (t: Tile) => void;
|
|
22
|
+
removeTile: (id: string) => void;
|
|
23
|
+
updateConfig: (patch: Partial<GridConfig>) => void;
|
|
24
|
+
toJSON: () => GridSnapshot;
|
|
25
|
+
loadJSON: (snap: GridSnapshot) => void;
|
|
26
|
+
destroy: () => void;
|
|
27
|
+
}
|
|
28
|
+
export declare function createGriddle(init: UseGriddleInit): GriddleApi;
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
// Svelte store that wraps a Grid instance.
|
|
2
|
+
import { writable } from 'svelte/store';
|
|
3
|
+
import { Grid } from '@griddle/core';
|
|
4
|
+
export function createGriddle(init) {
|
|
5
|
+
const grid = new Grid(init.config, init.tiles ?? []);
|
|
6
|
+
const tiles = writable(grid.tiles);
|
|
7
|
+
const config = writable(grid.config);
|
|
8
|
+
const version = writable(0);
|
|
9
|
+
const off = grid.changes.on(() => {
|
|
10
|
+
tiles.set(grid.tiles);
|
|
11
|
+
config.set(grid.config);
|
|
12
|
+
version.update((v) => v + 1);
|
|
13
|
+
});
|
|
14
|
+
return {
|
|
15
|
+
grid,
|
|
16
|
+
tiles: { subscribe: tiles.subscribe },
|
|
17
|
+
config: { subscribe: config.subscribe },
|
|
18
|
+
version: { subscribe: version.subscribe },
|
|
19
|
+
moveTile: (id, target) => grid.moveTile(id, target),
|
|
20
|
+
resizeTile: (id, size) => grid.resizeTile(id, size),
|
|
21
|
+
addTile: (t) => grid.addTile(t),
|
|
22
|
+
removeTile: (id) => grid.removeTile(id),
|
|
23
|
+
updateConfig: (patch) => grid.updateConfig(patch),
|
|
24
|
+
toJSON: () => grid.toJSON(),
|
|
25
|
+
loadJSON: (snap) => grid.loadJSON(snap),
|
|
26
|
+
destroy: () => off(),
|
|
27
|
+
};
|
|
28
|
+
}
|
package/dist/index.d.ts
ADDED
package/dist/index.js
ADDED
package/package.json
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@griddle/svelte",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Svelte bindings for Griddle — a headless, zero-dependency grid/canvas engine.",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"grid",
|
|
7
|
+
"canvas",
|
|
8
|
+
"layout",
|
|
9
|
+
"drag-and-drop",
|
|
10
|
+
"drag",
|
|
11
|
+
"resize",
|
|
12
|
+
"headless",
|
|
13
|
+
"virtualization",
|
|
14
|
+
"tiles",
|
|
15
|
+
"svelte"
|
|
16
|
+
],
|
|
17
|
+
"homepage": "https://github.com/Trustybits/griddle#readme",
|
|
18
|
+
"bugs": {
|
|
19
|
+
"url": "https://github.com/Trustybits/griddle/issues"
|
|
20
|
+
},
|
|
21
|
+
"repository": {
|
|
22
|
+
"type": "git",
|
|
23
|
+
"url": "git+https://github.com/Trustybits/griddle.git",
|
|
24
|
+
"directory": "packages/svelte"
|
|
25
|
+
},
|
|
26
|
+
"license": "MIT",
|
|
27
|
+
"author": "Trustybits",
|
|
28
|
+
"type": "module",
|
|
29
|
+
"sideEffects": ["**/*.svelte", "**/*.css"],
|
|
30
|
+
"svelte": "./dist/index.js",
|
|
31
|
+
"types": "./dist/index.d.ts",
|
|
32
|
+
"exports": {
|
|
33
|
+
".": {
|
|
34
|
+
"types": "./dist/index.d.ts",
|
|
35
|
+
"svelte": "./dist/index.js"
|
|
36
|
+
}
|
|
37
|
+
},
|
|
38
|
+
"files": ["dist"],
|
|
39
|
+
"engines": {
|
|
40
|
+
"node": ">=18"
|
|
41
|
+
},
|
|
42
|
+
"publishConfig": {
|
|
43
|
+
"access": "public"
|
|
44
|
+
},
|
|
45
|
+
"scripts": {
|
|
46
|
+
"build": "svelte-package -i src -o dist",
|
|
47
|
+
"prepublishOnly": "npm run build"
|
|
48
|
+
},
|
|
49
|
+
"peerDependencies": {
|
|
50
|
+
"@griddle/core": "^0.1.0",
|
|
51
|
+
"svelte": "^4.0.0 || ^5.0.0"
|
|
52
|
+
},
|
|
53
|
+
"devDependencies": {
|
|
54
|
+
"@griddle/core": "^0.1.0",
|
|
55
|
+
"@sveltejs/package": "^2.5.8",
|
|
56
|
+
"svelte": "^4.2.0",
|
|
57
|
+
"svelte-check": "^4.7.2",
|
|
58
|
+
"svelte-preprocess": "^5.1.0"
|
|
59
|
+
}
|
|
60
|
+
}
|