dalila 1.9.0 → 1.9.2
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/README.md +1 -0
- package/dist/components/ui/runtime.js +2 -1
- package/dist/core/for.d.ts +20 -0
- package/dist/core/for.js +40 -0
- package/dist/runtime/bind.js +400 -15
- package/package.json +1 -1
- package/src/components/ui/accordion/accordion.css +14 -8
- package/src/components/ui/badge/badge.css +13 -7
- package/src/components/ui/breadcrumb/breadcrumb.css +3 -3
- package/src/components/ui/button/button.css +21 -12
- package/src/components/ui/calendar/calendar.css +20 -6
- package/src/components/ui/card/card.css +18 -7
- package/src/components/ui/checkbox/checkbox.css +7 -5
- package/src/components/ui/chip/chip.css +7 -2
- package/src/components/ui/collapsible/collapsible.css +10 -4
- package/src/components/ui/combobox/combobox.css +18 -10
- package/src/components/ui/dialog/dialog.css +13 -2
- package/src/components/ui/drawer/drawer.css +11 -2
- package/src/components/ui/dropdown/dropdown.css +17 -8
- package/src/components/ui/dropzone/dropzone.css +17 -5
- package/src/components/ui/empty-state/empty-state.css +2 -2
- package/src/components/ui/form/form.css +1 -1
- package/src/components/ui/input/input.css +25 -6
- package/src/components/ui/pagination/pagination.css +10 -3
- package/src/components/ui/popover/popover.css +9 -9
- package/src/components/ui/radio/radio.css +5 -3
- package/src/components/ui/separator/separator.css +3 -3
- package/src/components/ui/skeleton/skeleton.css +3 -13
- package/src/components/ui/slider/slider.css +5 -5
- package/src/components/ui/table/table.css +4 -4
- package/src/components/ui/tabs/tabs.css +5 -1
- package/src/components/ui/toast/toast.css +15 -3
- package/src/components/ui/toggle/toggle.css +11 -9
- package/src/components/ui/tokens/tokens.css +12 -4
- package/src/components/ui/tooltip/tooltip.css +21 -5
- package/src/components/ui/typography/typography.css +1 -1
package/README.md
CHANGED
|
@@ -74,6 +74,7 @@ bind(document.getElementById('app')!, ctx);
|
|
|
74
74
|
- [when](./docs/core/when.md) — Conditional visibility
|
|
75
75
|
- [match](./docs/core/match.md) — Switch-style rendering
|
|
76
76
|
- [for](./docs/core/for.md) — List rendering with keyed diffing
|
|
77
|
+
- [Virtual Lists](./docs/core/virtual.md) — Fixed-height windowed rendering for large datasets
|
|
77
78
|
|
|
78
79
|
### Data
|
|
79
80
|
|
|
@@ -363,7 +363,8 @@ export function mountUI(root, options) {
|
|
|
363
363
|
}
|
|
364
364
|
// Attach drawers
|
|
365
365
|
for (const [key, drawer] of Object.entries(options.drawers ?? {})) {
|
|
366
|
-
const
|
|
366
|
+
const fallbackTag = drawer.side() === "bottom" ? "d-sheet" : "d-drawer";
|
|
367
|
+
const el = findByUI(mountedRoot, key, fallbackTag);
|
|
367
368
|
if (el)
|
|
368
369
|
drawer._attachTo(el);
|
|
369
370
|
}
|
package/dist/core/for.d.ts
CHANGED
|
@@ -1,6 +1,26 @@
|
|
|
1
1
|
interface DisposableFragment extends DocumentFragment {
|
|
2
2
|
dispose(): void;
|
|
3
3
|
}
|
|
4
|
+
export interface VirtualRangeInput {
|
|
5
|
+
itemCount: number;
|
|
6
|
+
itemHeight: number;
|
|
7
|
+
scrollTop: number;
|
|
8
|
+
viewportHeight: number;
|
|
9
|
+
overscan?: number;
|
|
10
|
+
}
|
|
11
|
+
export interface VirtualRange {
|
|
12
|
+
start: number;
|
|
13
|
+
end: number;
|
|
14
|
+
topOffset: number;
|
|
15
|
+
bottomOffset: number;
|
|
16
|
+
totalHeight: number;
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* Compute the visible range for a fixed-height virtualized list.
|
|
20
|
+
*
|
|
21
|
+
* `start`/`end` use the [start, end) convention.
|
|
22
|
+
*/
|
|
23
|
+
export declare function computeVirtualRange(input: VirtualRangeInput): VirtualRange;
|
|
4
24
|
/**
|
|
5
25
|
* Low-level keyed list rendering with fine-grained reactivity.
|
|
6
26
|
*
|
package/dist/core/for.js
CHANGED
|
@@ -1,6 +1,46 @@
|
|
|
1
1
|
import { effect, signal } from './signal.js';
|
|
2
2
|
import { isInDevMode } from './dev.js';
|
|
3
3
|
import { createScope, withScope, getCurrentScope } from './scope.js';
|
|
4
|
+
function clamp(value, min, max) {
|
|
5
|
+
return Math.max(min, Math.min(max, value));
|
|
6
|
+
}
|
|
7
|
+
/**
|
|
8
|
+
* Compute the visible range for a fixed-height virtualized list.
|
|
9
|
+
*
|
|
10
|
+
* `start`/`end` use the [start, end) convention.
|
|
11
|
+
*/
|
|
12
|
+
export function computeVirtualRange(input) {
|
|
13
|
+
const itemCount = Number.isFinite(input.itemCount) ? Math.max(0, Math.floor(input.itemCount)) : 0;
|
|
14
|
+
const itemHeight = Number.isFinite(input.itemHeight) ? Math.max(1, input.itemHeight) : 1;
|
|
15
|
+
const scrollTop = Number.isFinite(input.scrollTop) ? Math.max(0, input.scrollTop) : 0;
|
|
16
|
+
const viewportHeight = Number.isFinite(input.viewportHeight)
|
|
17
|
+
? Math.max(0, input.viewportHeight)
|
|
18
|
+
: 0;
|
|
19
|
+
const overscan = Number.isFinite(input.overscan) ? Math.max(0, Math.floor(input.overscan ?? 0)) : 0;
|
|
20
|
+
const totalHeight = itemCount * itemHeight;
|
|
21
|
+
if (itemCount === 0) {
|
|
22
|
+
return {
|
|
23
|
+
start: 0,
|
|
24
|
+
end: 0,
|
|
25
|
+
topOffset: 0,
|
|
26
|
+
bottomOffset: 0,
|
|
27
|
+
totalHeight,
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
const visibleStart = Math.floor(scrollTop / itemHeight);
|
|
31
|
+
const visibleEnd = Math.ceil((scrollTop + viewportHeight) / itemHeight);
|
|
32
|
+
const start = clamp(visibleStart - overscan, 0, itemCount);
|
|
33
|
+
const end = clamp(visibleEnd + overscan, start, itemCount);
|
|
34
|
+
const topOffset = start * itemHeight;
|
|
35
|
+
const bottomOffset = Math.max(0, totalHeight - (end * itemHeight));
|
|
36
|
+
return {
|
|
37
|
+
start,
|
|
38
|
+
end,
|
|
39
|
+
topOffset,
|
|
40
|
+
bottomOffset,
|
|
41
|
+
totalHeight,
|
|
42
|
+
};
|
|
43
|
+
}
|
|
4
44
|
const autoDisposeByDocument = new WeakMap();
|
|
5
45
|
const getMutationObserverCtor = (doc) => {
|
|
6
46
|
if (doc.defaultView?.MutationObserver)
|
package/dist/runtime/bind.js
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
*
|
|
7
7
|
* @module dalila/runtime
|
|
8
8
|
*/
|
|
9
|
-
import { effect, createScope, withScope, isInDevMode, signal } from '../core/index.js';
|
|
9
|
+
import { effect, createScope, withScope, isInDevMode, signal, computeVirtualRange } from '../core/index.js';
|
|
10
10
|
import { WRAPPED_HANDLER } from '../form/form.js';
|
|
11
11
|
import { linkScopeToDom, withDevtoolsDomTarget } from '../core/devtools.js';
|
|
12
12
|
// ============================================================================
|
|
@@ -1134,6 +1134,389 @@ function bindMatch(root, ctx, cleanups) {
|
|
|
1134
1134
|
}
|
|
1135
1135
|
}
|
|
1136
1136
|
// ============================================================================
|
|
1137
|
+
// d-virtual-each Directive
|
|
1138
|
+
// ============================================================================
|
|
1139
|
+
function readVirtualNumberOption(raw, ctx, label) {
|
|
1140
|
+
if (!raw)
|
|
1141
|
+
return null;
|
|
1142
|
+
const trimmed = raw.trim();
|
|
1143
|
+
if (!trimmed)
|
|
1144
|
+
return null;
|
|
1145
|
+
const asNumber = Number(trimmed);
|
|
1146
|
+
if (Number.isFinite(asNumber))
|
|
1147
|
+
return asNumber;
|
|
1148
|
+
const fromCtx = ctx[trimmed];
|
|
1149
|
+
if (fromCtx === undefined) {
|
|
1150
|
+
warn(`${label}: "${trimmed}" not found in context`);
|
|
1151
|
+
return null;
|
|
1152
|
+
}
|
|
1153
|
+
const resolved = resolve(fromCtx);
|
|
1154
|
+
if (typeof resolved === 'number' && Number.isFinite(resolved))
|
|
1155
|
+
return resolved;
|
|
1156
|
+
const numericFromString = Number(resolved);
|
|
1157
|
+
if (Number.isFinite(numericFromString))
|
|
1158
|
+
return numericFromString;
|
|
1159
|
+
warn(`${label}: "${trimmed}" must resolve to a finite number`);
|
|
1160
|
+
return null;
|
|
1161
|
+
}
|
|
1162
|
+
function readVirtualHeightOption(raw, ctx) {
|
|
1163
|
+
if (!raw)
|
|
1164
|
+
return null;
|
|
1165
|
+
const trimmed = raw.trim();
|
|
1166
|
+
if (!trimmed)
|
|
1167
|
+
return null;
|
|
1168
|
+
const asNumber = Number(trimmed);
|
|
1169
|
+
if (Number.isFinite(asNumber))
|
|
1170
|
+
return `${asNumber}px`;
|
|
1171
|
+
const fromCtx = ctx[trimmed];
|
|
1172
|
+
if (fromCtx !== undefined) {
|
|
1173
|
+
const resolved = resolve(fromCtx);
|
|
1174
|
+
if (typeof resolved === 'number' && Number.isFinite(resolved))
|
|
1175
|
+
return `${resolved}px`;
|
|
1176
|
+
if (typeof resolved === 'string' && resolved.trim())
|
|
1177
|
+
return resolved.trim();
|
|
1178
|
+
}
|
|
1179
|
+
return trimmed;
|
|
1180
|
+
}
|
|
1181
|
+
function createVirtualSpacer(template, kind) {
|
|
1182
|
+
const spacer = template.cloneNode(false);
|
|
1183
|
+
spacer.removeAttribute('id');
|
|
1184
|
+
spacer.removeAttribute('class');
|
|
1185
|
+
for (const attr of Array.from(spacer.attributes)) {
|
|
1186
|
+
if (attr.name.startsWith('d-')) {
|
|
1187
|
+
spacer.removeAttribute(attr.name);
|
|
1188
|
+
}
|
|
1189
|
+
}
|
|
1190
|
+
spacer.textContent = '';
|
|
1191
|
+
spacer.setAttribute('aria-hidden', 'true');
|
|
1192
|
+
spacer.setAttribute('data-dalila-virtual-spacer', kind);
|
|
1193
|
+
spacer.style.height = '0px';
|
|
1194
|
+
spacer.style.margin = '0';
|
|
1195
|
+
spacer.style.padding = '0';
|
|
1196
|
+
spacer.style.border = '0';
|
|
1197
|
+
spacer.style.pointerEvents = 'none';
|
|
1198
|
+
spacer.style.visibility = 'hidden';
|
|
1199
|
+
spacer.style.listStyle = 'none';
|
|
1200
|
+
return spacer;
|
|
1201
|
+
}
|
|
1202
|
+
/**
|
|
1203
|
+
* Bind all [d-virtual-each] directives within root.
|
|
1204
|
+
*
|
|
1205
|
+
* V1 constraints:
|
|
1206
|
+
* - Fixed item height (required via d-virtual-item-height)
|
|
1207
|
+
* - Vertical virtualization only
|
|
1208
|
+
* - Parent element is the scroll container
|
|
1209
|
+
*/
|
|
1210
|
+
function bindVirtualEach(root, ctx, cleanups) {
|
|
1211
|
+
const elements = qsaIncludingRoot(root, '[d-virtual-each]')
|
|
1212
|
+
.filter(el => !el.parentElement?.closest('[d-virtual-each], [d-each]'));
|
|
1213
|
+
for (const el of elements) {
|
|
1214
|
+
const bindingName = normalizeBinding(el.getAttribute('d-virtual-each'));
|
|
1215
|
+
if (!bindingName)
|
|
1216
|
+
continue;
|
|
1217
|
+
const itemHeightBinding = normalizeBinding(el.getAttribute('d-virtual-item-height'));
|
|
1218
|
+
const itemHeightRaw = itemHeightBinding ?? el.getAttribute('d-virtual-item-height');
|
|
1219
|
+
const itemHeightValue = readVirtualNumberOption(itemHeightRaw, ctx, 'd-virtual-item-height');
|
|
1220
|
+
const itemHeight = itemHeightValue == null ? NaN : itemHeightValue;
|
|
1221
|
+
if (!Number.isFinite(itemHeight) || itemHeight <= 0) {
|
|
1222
|
+
warn(`d-virtual-each: invalid item height on "${bindingName}". Falling back to d-each.`);
|
|
1223
|
+
el.setAttribute('d-each', bindingName);
|
|
1224
|
+
el.removeAttribute('d-virtual-each');
|
|
1225
|
+
el.removeAttribute('d-virtual-item-height');
|
|
1226
|
+
el.removeAttribute('d-virtual-overscan');
|
|
1227
|
+
el.removeAttribute('d-virtual-height');
|
|
1228
|
+
continue;
|
|
1229
|
+
}
|
|
1230
|
+
const overscanBinding = normalizeBinding(el.getAttribute('d-virtual-overscan'));
|
|
1231
|
+
const overscanRaw = overscanBinding ?? el.getAttribute('d-virtual-overscan');
|
|
1232
|
+
const overscanValue = readVirtualNumberOption(overscanRaw, ctx, 'd-virtual-overscan');
|
|
1233
|
+
const overscan = Number.isFinite(overscanValue)
|
|
1234
|
+
? Math.max(0, Math.floor(overscanValue))
|
|
1235
|
+
: 6;
|
|
1236
|
+
const viewportHeight = readVirtualHeightOption(normalizeBinding(el.getAttribute('d-virtual-height')) ?? el.getAttribute('d-virtual-height'), ctx);
|
|
1237
|
+
let binding = ctx[bindingName];
|
|
1238
|
+
if (binding === undefined) {
|
|
1239
|
+
warn(`d-virtual-each: "${bindingName}" not found in context`);
|
|
1240
|
+
binding = [];
|
|
1241
|
+
}
|
|
1242
|
+
const comment = document.createComment('d-virtual-each');
|
|
1243
|
+
el.parentNode?.replaceChild(comment, el);
|
|
1244
|
+
el.removeAttribute('d-virtual-each');
|
|
1245
|
+
el.removeAttribute('d-virtual-item-height');
|
|
1246
|
+
el.removeAttribute('d-virtual-overscan');
|
|
1247
|
+
el.removeAttribute('d-virtual-height');
|
|
1248
|
+
const keyBinding = normalizeBinding(el.getAttribute('d-key'));
|
|
1249
|
+
el.removeAttribute('d-key');
|
|
1250
|
+
const template = el;
|
|
1251
|
+
const topSpacer = createVirtualSpacer(template, 'top');
|
|
1252
|
+
const bottomSpacer = createVirtualSpacer(template, 'bottom');
|
|
1253
|
+
comment.parentNode?.insertBefore(topSpacer, comment);
|
|
1254
|
+
comment.parentNode?.insertBefore(bottomSpacer, comment);
|
|
1255
|
+
const scrollContainer = comment.parentElement;
|
|
1256
|
+
if (scrollContainer) {
|
|
1257
|
+
if (viewportHeight)
|
|
1258
|
+
scrollContainer.style.height = viewportHeight;
|
|
1259
|
+
if (!scrollContainer.style.overflowY)
|
|
1260
|
+
scrollContainer.style.overflowY = 'auto';
|
|
1261
|
+
}
|
|
1262
|
+
const clonesByKey = new Map();
|
|
1263
|
+
const disposesByKey = new Map();
|
|
1264
|
+
const metadataByKey = new Map();
|
|
1265
|
+
const itemsByKey = new Map();
|
|
1266
|
+
const objectKeyIds = new WeakMap();
|
|
1267
|
+
const symbolKeyIds = new Map();
|
|
1268
|
+
let nextObjectKeyId = 0;
|
|
1269
|
+
let nextSymbolKeyId = 0;
|
|
1270
|
+
const missingKeyWarned = new Set();
|
|
1271
|
+
let warnedNonArray = false;
|
|
1272
|
+
let warnedViewportFallback = false;
|
|
1273
|
+
const getObjectKeyId = (value) => {
|
|
1274
|
+
const existing = objectKeyIds.get(value);
|
|
1275
|
+
if (existing !== undefined)
|
|
1276
|
+
return existing;
|
|
1277
|
+
const next = ++nextObjectKeyId;
|
|
1278
|
+
objectKeyIds.set(value, next);
|
|
1279
|
+
return next;
|
|
1280
|
+
};
|
|
1281
|
+
const keyValueToString = (value, index) => {
|
|
1282
|
+
if (value === null || value === undefined)
|
|
1283
|
+
return `idx:${index}`;
|
|
1284
|
+
const type = typeof value;
|
|
1285
|
+
if (type === 'string' || type === 'number' || type === 'boolean' || type === 'bigint') {
|
|
1286
|
+
return `${type}:${String(value)}`;
|
|
1287
|
+
}
|
|
1288
|
+
if (type === 'symbol') {
|
|
1289
|
+
const sym = value;
|
|
1290
|
+
let id = symbolKeyIds.get(sym);
|
|
1291
|
+
if (id === undefined) {
|
|
1292
|
+
id = ++nextSymbolKeyId;
|
|
1293
|
+
symbolKeyIds.set(sym, id);
|
|
1294
|
+
}
|
|
1295
|
+
return `sym:${id}`;
|
|
1296
|
+
}
|
|
1297
|
+
if (type === 'object' || type === 'function') {
|
|
1298
|
+
return `obj:${getObjectKeyId(value)}`;
|
|
1299
|
+
}
|
|
1300
|
+
return `idx:${index}`;
|
|
1301
|
+
};
|
|
1302
|
+
const readKeyValue = (item, index) => {
|
|
1303
|
+
if (keyBinding) {
|
|
1304
|
+
if (keyBinding === '$index')
|
|
1305
|
+
return index;
|
|
1306
|
+
if (keyBinding === 'item')
|
|
1307
|
+
return item;
|
|
1308
|
+
if (typeof item === 'object' && item !== null && keyBinding in item) {
|
|
1309
|
+
return item[keyBinding];
|
|
1310
|
+
}
|
|
1311
|
+
const warnId = `${keyBinding}:${index}`;
|
|
1312
|
+
if (!missingKeyWarned.has(warnId)) {
|
|
1313
|
+
warn(`d-virtual-each: key "${keyBinding}" not found on item at index ${index}. Falling back to index key.`);
|
|
1314
|
+
missingKeyWarned.add(warnId);
|
|
1315
|
+
}
|
|
1316
|
+
return index;
|
|
1317
|
+
}
|
|
1318
|
+
if (typeof item === 'object' && item !== null) {
|
|
1319
|
+
const obj = item;
|
|
1320
|
+
if ('id' in obj)
|
|
1321
|
+
return obj.id;
|
|
1322
|
+
if ('key' in obj)
|
|
1323
|
+
return obj.key;
|
|
1324
|
+
}
|
|
1325
|
+
return index;
|
|
1326
|
+
};
|
|
1327
|
+
function createClone(key, item, index, count) {
|
|
1328
|
+
const clone = template.cloneNode(true);
|
|
1329
|
+
const itemCtx = Object.create(ctx);
|
|
1330
|
+
if (typeof item === 'object' && item !== null) {
|
|
1331
|
+
Object.assign(itemCtx, item);
|
|
1332
|
+
}
|
|
1333
|
+
const metadata = {
|
|
1334
|
+
$index: signal(index),
|
|
1335
|
+
$count: signal(count),
|
|
1336
|
+
$first: signal(index === 0),
|
|
1337
|
+
$last: signal(index === count - 1),
|
|
1338
|
+
$odd: signal(index % 2 !== 0),
|
|
1339
|
+
$even: signal(index % 2 === 0),
|
|
1340
|
+
};
|
|
1341
|
+
metadataByKey.set(key, metadata);
|
|
1342
|
+
itemsByKey.set(key, item);
|
|
1343
|
+
itemCtx.item = item;
|
|
1344
|
+
itemCtx.key = key;
|
|
1345
|
+
itemCtx.$index = metadata.$index;
|
|
1346
|
+
itemCtx.$count = metadata.$count;
|
|
1347
|
+
itemCtx.$first = metadata.$first;
|
|
1348
|
+
itemCtx.$last = metadata.$last;
|
|
1349
|
+
itemCtx.$odd = metadata.$odd;
|
|
1350
|
+
itemCtx.$even = metadata.$even;
|
|
1351
|
+
clone.setAttribute('data-dalila-internal-bound', '');
|
|
1352
|
+
const dispose = bind(clone, itemCtx, { _skipLifecycle: true });
|
|
1353
|
+
disposesByKey.set(key, dispose);
|
|
1354
|
+
clonesByKey.set(key, clone);
|
|
1355
|
+
return clone;
|
|
1356
|
+
}
|
|
1357
|
+
function updateCloneMetadata(key, index, count) {
|
|
1358
|
+
const metadata = metadataByKey.get(key);
|
|
1359
|
+
if (metadata) {
|
|
1360
|
+
metadata.$index.set(index);
|
|
1361
|
+
metadata.$count.set(count);
|
|
1362
|
+
metadata.$first.set(index === 0);
|
|
1363
|
+
metadata.$last.set(index === count - 1);
|
|
1364
|
+
metadata.$odd.set(index % 2 !== 0);
|
|
1365
|
+
metadata.$even.set(index % 2 === 0);
|
|
1366
|
+
}
|
|
1367
|
+
}
|
|
1368
|
+
function removeKey(key) {
|
|
1369
|
+
const clone = clonesByKey.get(key);
|
|
1370
|
+
clone?.remove();
|
|
1371
|
+
clonesByKey.delete(key);
|
|
1372
|
+
metadataByKey.delete(key);
|
|
1373
|
+
itemsByKey.delete(key);
|
|
1374
|
+
const dispose = disposesByKey.get(key);
|
|
1375
|
+
if (dispose) {
|
|
1376
|
+
dispose();
|
|
1377
|
+
disposesByKey.delete(key);
|
|
1378
|
+
}
|
|
1379
|
+
}
|
|
1380
|
+
let currentItems = [];
|
|
1381
|
+
function renderVirtualList(items) {
|
|
1382
|
+
const parent = comment.parentNode;
|
|
1383
|
+
if (!parent)
|
|
1384
|
+
return;
|
|
1385
|
+
const viewportHeightValue = scrollContainer?.clientHeight ?? 0;
|
|
1386
|
+
const effectiveViewportHeight = viewportHeightValue > 0 ? viewportHeightValue : itemHeight * 10;
|
|
1387
|
+
const scrollTop = scrollContainer?.scrollTop ?? 0;
|
|
1388
|
+
if (viewportHeightValue <= 0 && !warnedViewportFallback) {
|
|
1389
|
+
warnedViewportFallback = true;
|
|
1390
|
+
warn('d-virtual-each: scroll container has no measurable height. Using fallback viewport size.');
|
|
1391
|
+
}
|
|
1392
|
+
const range = computeVirtualRange({
|
|
1393
|
+
itemCount: items.length,
|
|
1394
|
+
itemHeight,
|
|
1395
|
+
scrollTop,
|
|
1396
|
+
viewportHeight: effectiveViewportHeight,
|
|
1397
|
+
overscan,
|
|
1398
|
+
});
|
|
1399
|
+
topSpacer.style.height = `${range.topOffset}px`;
|
|
1400
|
+
bottomSpacer.style.height = `${range.bottomOffset}px`;
|
|
1401
|
+
const orderedClones = [];
|
|
1402
|
+
const orderedKeys = [];
|
|
1403
|
+
const nextKeys = new Set();
|
|
1404
|
+
const changedKeys = new Set();
|
|
1405
|
+
for (let i = range.start; i < range.end; i++) {
|
|
1406
|
+
const item = items[i];
|
|
1407
|
+
let key = keyValueToString(readKeyValue(item, i), i);
|
|
1408
|
+
if (nextKeys.has(key)) {
|
|
1409
|
+
warn(`d-virtual-each: duplicate visible key "${key}" at index ${i}. Falling back to per-index key.`);
|
|
1410
|
+
key = `${key}:dup:${i}`;
|
|
1411
|
+
}
|
|
1412
|
+
nextKeys.add(key);
|
|
1413
|
+
let clone = clonesByKey.get(key);
|
|
1414
|
+
if (clone) {
|
|
1415
|
+
updateCloneMetadata(key, i, items.length);
|
|
1416
|
+
if (itemsByKey.get(key) !== item) {
|
|
1417
|
+
changedKeys.add(key);
|
|
1418
|
+
}
|
|
1419
|
+
}
|
|
1420
|
+
else {
|
|
1421
|
+
clone = createClone(key, item, i, items.length);
|
|
1422
|
+
}
|
|
1423
|
+
orderedClones.push(clone);
|
|
1424
|
+
orderedKeys.push(key);
|
|
1425
|
+
}
|
|
1426
|
+
for (let i = 0; i < orderedClones.length; i++) {
|
|
1427
|
+
const key = orderedKeys[i];
|
|
1428
|
+
if (!changedKeys.has(key))
|
|
1429
|
+
continue;
|
|
1430
|
+
removeKey(key);
|
|
1431
|
+
orderedClones[i] = createClone(key, items[range.start + i], range.start + i, items.length);
|
|
1432
|
+
}
|
|
1433
|
+
for (const key of Array.from(clonesByKey.keys())) {
|
|
1434
|
+
if (nextKeys.has(key))
|
|
1435
|
+
continue;
|
|
1436
|
+
removeKey(key);
|
|
1437
|
+
}
|
|
1438
|
+
let referenceNode = bottomSpacer;
|
|
1439
|
+
for (let i = orderedClones.length - 1; i >= 0; i--) {
|
|
1440
|
+
const clone = orderedClones[i];
|
|
1441
|
+
if (clone.nextSibling !== referenceNode) {
|
|
1442
|
+
parent.insertBefore(clone, referenceNode);
|
|
1443
|
+
}
|
|
1444
|
+
referenceNode = clone;
|
|
1445
|
+
}
|
|
1446
|
+
}
|
|
1447
|
+
let framePending = false;
|
|
1448
|
+
let pendingRaf = null;
|
|
1449
|
+
let pendingTimeout = null;
|
|
1450
|
+
const scheduleRender = () => {
|
|
1451
|
+
if (framePending)
|
|
1452
|
+
return;
|
|
1453
|
+
framePending = true;
|
|
1454
|
+
const flush = () => {
|
|
1455
|
+
framePending = false;
|
|
1456
|
+
renderVirtualList(currentItems);
|
|
1457
|
+
};
|
|
1458
|
+
if (typeof requestAnimationFrame === 'function') {
|
|
1459
|
+
pendingRaf = requestAnimationFrame(() => {
|
|
1460
|
+
pendingRaf = null;
|
|
1461
|
+
flush();
|
|
1462
|
+
});
|
|
1463
|
+
return;
|
|
1464
|
+
}
|
|
1465
|
+
pendingTimeout = setTimeout(() => {
|
|
1466
|
+
pendingTimeout = null;
|
|
1467
|
+
flush();
|
|
1468
|
+
}, 0);
|
|
1469
|
+
};
|
|
1470
|
+
const onScroll = () => scheduleRender();
|
|
1471
|
+
const onResize = () => scheduleRender();
|
|
1472
|
+
scrollContainer?.addEventListener('scroll', onScroll, { passive: true });
|
|
1473
|
+
if (typeof window !== 'undefined') {
|
|
1474
|
+
window.addEventListener('resize', onResize);
|
|
1475
|
+
}
|
|
1476
|
+
if (isSignal(binding)) {
|
|
1477
|
+
bindEffect(scrollContainer ?? el, () => {
|
|
1478
|
+
const value = binding();
|
|
1479
|
+
if (Array.isArray(value)) {
|
|
1480
|
+
warnedNonArray = false;
|
|
1481
|
+
currentItems = value;
|
|
1482
|
+
}
|
|
1483
|
+
else {
|
|
1484
|
+
if (!warnedNonArray) {
|
|
1485
|
+
warnedNonArray = true;
|
|
1486
|
+
warn(`d-virtual-each: "${bindingName}" is not an array or signal-of-array`);
|
|
1487
|
+
}
|
|
1488
|
+
currentItems = [];
|
|
1489
|
+
}
|
|
1490
|
+
renderVirtualList(currentItems);
|
|
1491
|
+
});
|
|
1492
|
+
}
|
|
1493
|
+
else if (Array.isArray(binding)) {
|
|
1494
|
+
currentItems = binding;
|
|
1495
|
+
renderVirtualList(currentItems);
|
|
1496
|
+
}
|
|
1497
|
+
else {
|
|
1498
|
+
warn(`d-virtual-each: "${bindingName}" is not an array or signal-of-array`);
|
|
1499
|
+
}
|
|
1500
|
+
cleanups.push(() => {
|
|
1501
|
+
scrollContainer?.removeEventListener('scroll', onScroll);
|
|
1502
|
+
if (typeof window !== 'undefined') {
|
|
1503
|
+
window.removeEventListener('resize', onResize);
|
|
1504
|
+
}
|
|
1505
|
+
if (pendingRaf != null && typeof cancelAnimationFrame === 'function') {
|
|
1506
|
+
cancelAnimationFrame(pendingRaf);
|
|
1507
|
+
}
|
|
1508
|
+
pendingRaf = null;
|
|
1509
|
+
if (pendingTimeout != null)
|
|
1510
|
+
clearTimeout(pendingTimeout);
|
|
1511
|
+
pendingTimeout = null;
|
|
1512
|
+
for (const key of Array.from(clonesByKey.keys()))
|
|
1513
|
+
removeKey(key);
|
|
1514
|
+
topSpacer.remove();
|
|
1515
|
+
bottomSpacer.remove();
|
|
1516
|
+
});
|
|
1517
|
+
}
|
|
1518
|
+
}
|
|
1519
|
+
// ============================================================================
|
|
1137
1520
|
// d-each Directive
|
|
1138
1521
|
// ============================================================================
|
|
1139
1522
|
/**
|
|
@@ -1143,11 +1526,11 @@ function bindMatch(root, ctx, cleanups) {
|
|
|
1143
1526
|
* item's properties as its context.
|
|
1144
1527
|
*/
|
|
1145
1528
|
function bindEach(root, ctx, cleanups) {
|
|
1146
|
-
// Only bind top-level d-each elements.
|
|
1147
|
-
// d-each
|
|
1148
|
-
//
|
|
1529
|
+
// Only bind top-level d-each elements. Nested d-each inside d-each or
|
|
1530
|
+
// d-virtual-each templates must be left untouched here — they are bound when
|
|
1531
|
+
// parent clones are passed to bind() individually.
|
|
1149
1532
|
const elements = qsaIncludingRoot(root, '[d-each]')
|
|
1150
|
-
.filter(el => !el.parentElement?.closest('[d-each]'));
|
|
1533
|
+
.filter(el => !el.parentElement?.closest('[d-each], [d-virtual-each]'));
|
|
1151
1534
|
for (const el of elements) {
|
|
1152
1535
|
const bindingName = normalizeBinding(el.getAttribute('d-each'));
|
|
1153
1536
|
if (!bindingName)
|
|
@@ -2108,26 +2491,28 @@ export function bind(root, ctx, options = {}) {
|
|
|
2108
2491
|
bindForm(root, ctx, cleanups);
|
|
2109
2492
|
// 2. d-array — must run before d-each to setup field arrays
|
|
2110
2493
|
bindArray(root, ctx, cleanups);
|
|
2111
|
-
// 3. d-each — must run early
|
|
2494
|
+
// 3. d-virtual-each — must run early for virtual template extraction
|
|
2495
|
+
bindVirtualEach(root, ctx, cleanups);
|
|
2496
|
+
// 4. d-each — must run early: removes templates before TreeWalker visits them
|
|
2112
2497
|
bindEach(root, ctx, cleanups);
|
|
2113
|
-
//
|
|
2498
|
+
// 5. Text interpolation (template plan cache + lazy parser fallback)
|
|
2114
2499
|
bindTextInterpolation(root, ctx, rawTextSelectors, templatePlanCacheConfig, benchSession);
|
|
2115
|
-
//
|
|
2500
|
+
// 6. d-attr bindings
|
|
2116
2501
|
bindAttrs(root, ctx, cleanups);
|
|
2117
|
-
//
|
|
2502
|
+
// 7. d-html bindings
|
|
2118
2503
|
bindHtml(root, ctx, cleanups);
|
|
2119
|
-
//
|
|
2504
|
+
// 8. Form fields — register fields with form instances
|
|
2120
2505
|
bindField(root, ctx, cleanups);
|
|
2121
|
-
//
|
|
2506
|
+
// 9. Event bindings
|
|
2122
2507
|
bindEvents(root, ctx, events, cleanups);
|
|
2123
|
-
//
|
|
2508
|
+
// 10. d-when directive
|
|
2124
2509
|
bindWhen(root, ctx, cleanups);
|
|
2125
|
-
//
|
|
2510
|
+
// 11. d-match directive
|
|
2126
2511
|
bindMatch(root, ctx, cleanups);
|
|
2127
|
-
//
|
|
2512
|
+
// 12. Form error displays — BEFORE d-if to bind errors in conditionally rendered sections
|
|
2128
2513
|
bindError(root, ctx, cleanups);
|
|
2129
2514
|
bindFormError(root, ctx, cleanups);
|
|
2130
|
-
//
|
|
2515
|
+
// 13. d-if — must run last: elements are fully bound before conditional removal
|
|
2131
2516
|
bindIf(root, ctx, cleanups);
|
|
2132
2517
|
});
|
|
2133
2518
|
// Bindings complete: remove loading state and mark as ready.
|
package/package.json
CHANGED
|
@@ -4,12 +4,12 @@
|
|
|
4
4
|
display: flex;
|
|
5
5
|
flex-direction: column;
|
|
6
6
|
border: 1px solid var(--d-border-color);
|
|
7
|
-
border-radius: var(--d-radius-
|
|
7
|
+
border-radius: var(--d-radius-lg);
|
|
8
8
|
overflow: hidden;
|
|
9
9
|
}
|
|
10
10
|
|
|
11
11
|
.d-accordion-item {
|
|
12
|
-
border-bottom: 1px solid var(--d-border-
|
|
12
|
+
border-bottom: 1px solid var(--d-border-light);
|
|
13
13
|
}
|
|
14
14
|
|
|
15
15
|
.d-accordion-item:last-child {
|
|
@@ -33,7 +33,7 @@
|
|
|
33
33
|
cursor: pointer;
|
|
34
34
|
text-align: left;
|
|
35
35
|
list-style: none;
|
|
36
|
-
transition: background var(--d-duration-fast) ease;
|
|
36
|
+
transition: background var(--d-duration-fast) var(--d-ease);
|
|
37
37
|
}
|
|
38
38
|
|
|
39
39
|
.d-accordion-item > summary::-webkit-details-marker {
|
|
@@ -45,13 +45,19 @@
|
|
|
45
45
|
background: var(--d-surface-raised);
|
|
46
46
|
}
|
|
47
47
|
|
|
48
|
+
.d-accordion-item > summary:focus-visible,
|
|
49
|
+
.d-accordion-trigger:focus-visible {
|
|
50
|
+
outline: none;
|
|
51
|
+
box-shadow: inset 0 0 0 2px var(--d-accent);
|
|
52
|
+
}
|
|
53
|
+
|
|
48
54
|
.d-accordion-item > summary::after,
|
|
49
55
|
.d-accordion-trigger::after {
|
|
50
56
|
content: "";
|
|
51
57
|
width: 0.5rem;
|
|
52
58
|
height: 0.5rem;
|
|
53
|
-
border-right: 2px solid var(--d-text-
|
|
54
|
-
border-bottom: 2px solid var(--d-text-
|
|
59
|
+
border-right: 2px solid var(--d-text-tertiary);
|
|
60
|
+
border-bottom: 2px solid var(--d-text-tertiary);
|
|
55
61
|
transform: rotate(45deg);
|
|
56
62
|
transition: transform var(--d-duration) var(--d-ease);
|
|
57
63
|
flex-shrink: 0;
|
|
@@ -71,8 +77,8 @@
|
|
|
71
77
|
transform: translateY(-0.2rem);
|
|
72
78
|
transition:
|
|
73
79
|
max-height var(--d-duration-slow) var(--d-ease),
|
|
74
|
-
opacity var(--d-duration-fast) ease,
|
|
75
|
-
transform var(--d-duration-fast) ease,
|
|
80
|
+
opacity var(--d-duration-fast) var(--d-ease),
|
|
81
|
+
transform var(--d-duration-fast) var(--d-ease),
|
|
76
82
|
padding var(--d-duration-slow) var(--d-ease);
|
|
77
83
|
font-family: var(--d-font-sans);
|
|
78
84
|
font-size: var(--d-text-sm);
|
|
@@ -86,5 +92,5 @@
|
|
|
86
92
|
opacity: 1;
|
|
87
93
|
transform: translateY(0);
|
|
88
94
|
padding: var(--d-space-2) var(--d-space-4) var(--d-space-4);
|
|
89
|
-
border-top: 1px solid var(--d-border-
|
|
95
|
+
border-top: 1px solid var(--d-border-light);
|
|
90
96
|
}
|
|
@@ -5,11 +5,11 @@
|
|
|
5
5
|
align-items: center;
|
|
6
6
|
gap: var(--d-space-1);
|
|
7
7
|
|
|
8
|
-
padding:
|
|
8
|
+
padding: 2px 10px;
|
|
9
9
|
|
|
10
10
|
font-family: var(--d-font-sans);
|
|
11
|
-
font-size:
|
|
12
|
-
font-weight: var(--d-font-
|
|
11
|
+
font-size: var(--d-text-xs);
|
|
12
|
+
font-weight: var(--d-font-medium);
|
|
13
13
|
line-height: 1.5;
|
|
14
14
|
text-transform: uppercase;
|
|
15
15
|
letter-spacing: 0.05em;
|
|
@@ -18,17 +18,17 @@
|
|
|
18
18
|
|
|
19
19
|
background: var(--d-surface-raised);
|
|
20
20
|
border: 1px solid var(--d-border-color);
|
|
21
|
-
border-radius:
|
|
21
|
+
border-radius: var(--d-radius-full);
|
|
22
22
|
}
|
|
23
23
|
|
|
24
24
|
.d-badge-primary {
|
|
25
|
-
color:
|
|
25
|
+
color: var(--d-text-inverse);
|
|
26
26
|
background: var(--d-accent);
|
|
27
27
|
border-color: var(--d-accent);
|
|
28
28
|
}
|
|
29
29
|
|
|
30
30
|
.d-badge-accent {
|
|
31
|
-
color:
|
|
31
|
+
color: var(--d-text-inverse);
|
|
32
32
|
background: var(--d-accent-500);
|
|
33
33
|
border-color: var(--d-accent-500);
|
|
34
34
|
}
|
|
@@ -51,12 +51,18 @@
|
|
|
51
51
|
border-color: transparent;
|
|
52
52
|
}
|
|
53
53
|
|
|
54
|
+
.d-badge-info {
|
|
55
|
+
color: var(--d-info);
|
|
56
|
+
background: var(--d-info-light);
|
|
57
|
+
border-color: transparent;
|
|
58
|
+
}
|
|
59
|
+
|
|
54
60
|
.d-badge-outline {
|
|
55
61
|
background: transparent;
|
|
56
62
|
}
|
|
57
63
|
|
|
58
64
|
.d-badge-solid {
|
|
59
|
-
color:
|
|
65
|
+
color: var(--d-text-inverse);
|
|
60
66
|
background: var(--d-gradient-brand);
|
|
61
67
|
border-color: transparent;
|
|
62
68
|
}
|