cursedbelt-core 2.3.0 → 2.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
|
@@ -80,9 +80,26 @@ export interface FileTreeNode {
|
|
|
80
80
|
*
|
|
81
81
|
* The consumer's veto, for a rule the tree cannot know: collections uses it so a folder
|
|
82
82
|
* cannot be dragged across album boundaries, which the server has no route for. Defaults
|
|
83
|
-
* to true for folders
|
|
83
|
+
* to true for folders.
|
|
84
|
+
*
|
|
85
|
+
* 🔴 On a FILE it is the opposite switch, and it is opt-in: `true` makes the file a
|
|
86
|
+
* container of its own — a drop on the middle half of its row is `{ kind: 'into',
|
|
87
|
+
* parentId: <the file> }`. `apps/desk` is why: a workbook is a leaf in its tree, and
|
|
88
|
+
* dropping a sheet onto one makes the sheet one of its tabs (the owner, desk file `02`).
|
|
89
|
+
* The edges of the row, and the middle whenever {@link ResolveDropOptions.canDrop} vetoes
|
|
90
|
+
* the file, still mean *the folder the file is in* — so a column of such files is never a
|
|
91
|
+
* wall of rows that refuse everything that cannot go inside them. Absent or `false`, a
|
|
92
|
+
* file row means its folder along its whole height, exactly as before.
|
|
84
93
|
*/
|
|
85
94
|
acceptsDrop?: boolean;
|
|
95
|
+
/**
|
|
96
|
+
* This row's own name matched a search the consumer ran — drawn a step heavier.
|
|
97
|
+
*
|
|
98
|
+
* For a consumer that narrows the tree itself (by content, not only by name) and keeps
|
|
99
|
+
* the folders on the way to each hit: the hits are what the eye is scanning for, and the
|
|
100
|
+
* folders kept around them are context. Omit it and nothing is emphasised.
|
|
101
|
+
*/
|
|
102
|
+
matched?: boolean;
|
|
86
103
|
/**
|
|
87
104
|
* Whether this row can be picked up. Default true once `onMove` is wired.
|
|
88
105
|
*
|
|
@@ -156,6 +156,16 @@ export function resolveDrop({ nodes, row, offsetRatio, dragId, allowReorder = fa
|
|
|
156
156
|
return { target: null, refused: true, reason: lockedReason };
|
|
157
157
|
return vetted({ target: { kind: 'into', parentId: target.id }, refused: false, reason: null });
|
|
158
158
|
};
|
|
159
|
+
// A FILE the consumer opted in as a container takes the drop on the middle of its row —
|
|
160
|
+
// see `FileTreeNode.acceptsDrop`. Vetoed, it falls through to meaning its folder below.
|
|
161
|
+
if (target.kind === 'file' && !allowReorder && target.acceptsDrop === true && target.id !== dragId) {
|
|
162
|
+
const band = (1 - INTO_ZONE) / 2;
|
|
163
|
+
if (offsetRatio > band && offsetRatio < 1 - band) {
|
|
164
|
+
const into = vetted({ target: { kind: 'into', parentId: target.id }, refused: false, reason: null });
|
|
165
|
+
if (into.target)
|
|
166
|
+
return into;
|
|
167
|
+
}
|
|
168
|
+
}
|
|
159
169
|
// A FILE row means "the folder that row is in" — what every file explorer does, and what
|
|
160
170
|
// keeps a long list of files from being a wall of rows nothing can be dropped on.
|
|
161
171
|
if (target.kind === 'file' && !allowReorder) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "cursedbelt-core",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.4.0",
|
|
4
4
|
"license": "ISC",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"description": "Framework-agnostic core of the cursedbelt split — isomorphic domain logic. cwip below it, cursedbelt-server and cursedbelt (react) above it.",
|
|
@@ -18,11 +18,12 @@
|
|
|
18
18
|
"scripts": {
|
|
19
19
|
"clean": "rm -rf dist",
|
|
20
20
|
"typecheck": "tsc -p tsconfig.build.json --noEmit",
|
|
21
|
-
"test": "NODE_ENV=development bun test src",
|
|
21
|
+
"test": "NODE_ENV=development bun test --timeout 60000 --parallel=4 src",
|
|
22
22
|
"build": "bun run clean && tsc -p tsconfig.build.json",
|
|
23
23
|
"paths": "bun run scripts/paths.ts",
|
|
24
|
-
"verify": "bun run
|
|
25
|
-
"prepublishOnly": "bun run
|
|
24
|
+
"verify": "bun run gate",
|
|
25
|
+
"prepublishOnly": "bun run gate --force",
|
|
26
|
+
"gate": "bun run scripts/paths.ts --only gate.ts"
|
|
26
27
|
},
|
|
27
28
|
"files": [
|
|
28
29
|
"dist",
|
|
@@ -387,5 +388,25 @@
|
|
|
387
388
|
"zod": "4.4.3",
|
|
388
389
|
"@happy-dom/global-registrator": "^20.10.6",
|
|
389
390
|
"hono": "4.12.28"
|
|
391
|
+
},
|
|
392
|
+
"gate": {
|
|
393
|
+
"stages": [
|
|
394
|
+
{
|
|
395
|
+
"script": "paths"
|
|
396
|
+
},
|
|
397
|
+
{
|
|
398
|
+
"script": "typecheck"
|
|
399
|
+
},
|
|
400
|
+
{
|
|
401
|
+
"script": "build"
|
|
402
|
+
},
|
|
403
|
+
{
|
|
404
|
+
"script": "test",
|
|
405
|
+
"needs": [
|
|
406
|
+
"build"
|
|
407
|
+
],
|
|
408
|
+
"why": "build starts with `rm -rf dist`, and leafSubpathsImportNothing / shippedFilesAreTracked read dist/; racing it they read a directory that is not there yet"
|
|
409
|
+
}
|
|
410
|
+
]
|
|
390
411
|
}
|
|
391
412
|
}
|
|
@@ -234,6 +234,49 @@ describe('resolveDrop', () => {
|
|
|
234
234
|
});
|
|
235
235
|
});
|
|
236
236
|
|
|
237
|
+
/**
|
|
238
|
+
* A FILE that takes drops INTO it — `acceptsDrop: true` on a file, which `apps/desk` sets on
|
|
239
|
+
* a workbook so a sheet dropped on it becomes one of its tabs.
|
|
240
|
+
*/
|
|
241
|
+
describe('resolveDrop onto a file that accepts drops', () => {
|
|
242
|
+
const book: FileTreeNode = { id: 'q3', name: 'Q3 Review', kind: 'file', acceptsDrop: true };
|
|
243
|
+
const nodes: FileTreeNode[] = [
|
|
244
|
+
folder('work', 'Work', [book, { id: 'report', name: 'report.csv', kind: 'file' }]),
|
|
245
|
+
{ id: 'loose', name: 'loose.csv', kind: 'file' },
|
|
246
|
+
];
|
|
247
|
+
const rows = flattenFileTree(nodes, []);
|
|
248
|
+
const on = (id: string, dragId: string, offsetRatio: number, canDrop?: (d: string | null, t: FileTreeDrop) => string | null) =>
|
|
249
|
+
resolveDrop({ nodes, row: rowFor(rows, id), offsetRatio, dragId, ...(canDrop ? { canDrop } : {}) });
|
|
250
|
+
|
|
251
|
+
test('the middle of its row is INTO the file', () => {
|
|
252
|
+
expect(on('q3', 'loose', 0.5).target).toEqual({ kind: 'into', parentId: 'q3' });
|
|
253
|
+
});
|
|
254
|
+
|
|
255
|
+
test('its edges still mean the folder it is in', () => {
|
|
256
|
+
expect(on('q3', 'loose', 0.1).target).toEqual({ kind: 'into', parentId: 'work' });
|
|
257
|
+
expect(on('q3', 'loose', 0.9).target).toEqual({ kind: 'into', parentId: 'work' });
|
|
258
|
+
});
|
|
259
|
+
|
|
260
|
+
test('🔴 a veto on the file falls back to its folder rather than refusing the row', () => {
|
|
261
|
+
const onlySheets = (dragId: string | null, target: FileTreeDrop) =>
|
|
262
|
+
target.kind === 'into' && target.parentId === 'q3' && dragId !== 'loose' ? 'Only a sheet can become a tab' : null;
|
|
263
|
+
expect(on('q3', 'report', 0.5, onlySheets).target).toEqual({ kind: 'into', parentId: 'work' });
|
|
264
|
+
expect(on('q3', 'loose', 0.5, onlySheets).target).toEqual({ kind: 'into', parentId: 'q3' });
|
|
265
|
+
});
|
|
266
|
+
|
|
267
|
+
test('a file never takes itself', () => {
|
|
268
|
+
expect(on('q3', 'q3', 0.5).target).toEqual({ kind: 'into', parentId: 'work' });
|
|
269
|
+
});
|
|
270
|
+
|
|
271
|
+
test('a file WITHOUT the flag means its folder along its whole height, as before', () => {
|
|
272
|
+
expect(on('report', 'loose', 0.5).target).toEqual({ kind: 'into', parentId: 'work' });
|
|
273
|
+
});
|
|
274
|
+
|
|
275
|
+
test('the chip names the file it is going into', () => {
|
|
276
|
+
expect(describeDrop(nodes, { kind: 'into', parentId: 'q3' })).toBe('Into Q3 Review');
|
|
277
|
+
});
|
|
278
|
+
});
|
|
279
|
+
|
|
237
280
|
/**
|
|
238
281
|
* 🔴 The consumer's veto, which is the seam collections needs.
|
|
239
282
|
*
|
|
@@ -83,9 +83,26 @@ export interface FileTreeNode {
|
|
|
83
83
|
*
|
|
84
84
|
* The consumer's veto, for a rule the tree cannot know: collections uses it so a folder
|
|
85
85
|
* cannot be dragged across album boundaries, which the server has no route for. Defaults
|
|
86
|
-
* to true for folders
|
|
86
|
+
* to true for folders.
|
|
87
|
+
*
|
|
88
|
+
* 🔴 On a FILE it is the opposite switch, and it is opt-in: `true` makes the file a
|
|
89
|
+
* container of its own — a drop on the middle half of its row is `{ kind: 'into',
|
|
90
|
+
* parentId: <the file> }`. `apps/desk` is why: a workbook is a leaf in its tree, and
|
|
91
|
+
* dropping a sheet onto one makes the sheet one of its tabs (the owner, desk file `02`).
|
|
92
|
+
* The edges of the row, and the middle whenever {@link ResolveDropOptions.canDrop} vetoes
|
|
93
|
+
* the file, still mean *the folder the file is in* — so a column of such files is never a
|
|
94
|
+
* wall of rows that refuse everything that cannot go inside them. Absent or `false`, a
|
|
95
|
+
* file row means its folder along its whole height, exactly as before.
|
|
87
96
|
*/
|
|
88
97
|
acceptsDrop?: boolean;
|
|
98
|
+
/**
|
|
99
|
+
* This row's own name matched a search the consumer ran — drawn a step heavier.
|
|
100
|
+
*
|
|
101
|
+
* For a consumer that narrows the tree itself (by content, not only by name) and keeps
|
|
102
|
+
* the folders on the way to each hit: the hits are what the eye is scanning for, and the
|
|
103
|
+
* folders kept around them are context. Omit it and nothing is emphasised.
|
|
104
|
+
*/
|
|
105
|
+
matched?: boolean;
|
|
89
106
|
/**
|
|
90
107
|
* Whether this row can be picked up. Default true once `onMove` is wired.
|
|
91
108
|
*
|
|
@@ -330,6 +347,16 @@ export function resolveDrop({
|
|
|
330
347
|
return vetted({ target: { kind: 'into', parentId: target.id }, refused: false, reason: null });
|
|
331
348
|
};
|
|
332
349
|
|
|
350
|
+
// A FILE the consumer opted in as a container takes the drop on the middle of its row —
|
|
351
|
+
// see `FileTreeNode.acceptsDrop`. Vetoed, it falls through to meaning its folder below.
|
|
352
|
+
if (target.kind === 'file' && !allowReorder && target.acceptsDrop === true && target.id !== dragId) {
|
|
353
|
+
const band = (1 - INTO_ZONE) / 2;
|
|
354
|
+
if (offsetRatio > band && offsetRatio < 1 - band) {
|
|
355
|
+
const into = vetted({ target: { kind: 'into', parentId: target.id }, refused: false, reason: null });
|
|
356
|
+
if (into.target) return into;
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
|
|
333
360
|
// A FILE row means "the folder that row is in" — what every file explorer does, and what
|
|
334
361
|
// keeps a long list of files from being a wall of rows nothing can be dropped on.
|
|
335
362
|
if (target.kind === 'file' && !allowReorder) {
|