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.
Files changed (36) hide show
  1. package/README.md +1 -0
  2. package/dist/components/ui/runtime.js +2 -1
  3. package/dist/core/for.d.ts +20 -0
  4. package/dist/core/for.js +40 -0
  5. package/dist/runtime/bind.js +400 -15
  6. package/package.json +1 -1
  7. package/src/components/ui/accordion/accordion.css +14 -8
  8. package/src/components/ui/badge/badge.css +13 -7
  9. package/src/components/ui/breadcrumb/breadcrumb.css +3 -3
  10. package/src/components/ui/button/button.css +21 -12
  11. package/src/components/ui/calendar/calendar.css +20 -6
  12. package/src/components/ui/card/card.css +18 -7
  13. package/src/components/ui/checkbox/checkbox.css +7 -5
  14. package/src/components/ui/chip/chip.css +7 -2
  15. package/src/components/ui/collapsible/collapsible.css +10 -4
  16. package/src/components/ui/combobox/combobox.css +18 -10
  17. package/src/components/ui/dialog/dialog.css +13 -2
  18. package/src/components/ui/drawer/drawer.css +11 -2
  19. package/src/components/ui/dropdown/dropdown.css +17 -8
  20. package/src/components/ui/dropzone/dropzone.css +17 -5
  21. package/src/components/ui/empty-state/empty-state.css +2 -2
  22. package/src/components/ui/form/form.css +1 -1
  23. package/src/components/ui/input/input.css +25 -6
  24. package/src/components/ui/pagination/pagination.css +10 -3
  25. package/src/components/ui/popover/popover.css +9 -9
  26. package/src/components/ui/radio/radio.css +5 -3
  27. package/src/components/ui/separator/separator.css +3 -3
  28. package/src/components/ui/skeleton/skeleton.css +3 -13
  29. package/src/components/ui/slider/slider.css +5 -5
  30. package/src/components/ui/table/table.css +4 -4
  31. package/src/components/ui/tabs/tabs.css +5 -1
  32. package/src/components/ui/toast/toast.css +15 -3
  33. package/src/components/ui/toggle/toggle.css +11 -9
  34. package/src/components/ui/tokens/tokens.css +12 -4
  35. package/src/components/ui/tooltip/tooltip.css +21 -5
  36. 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 el = findByUI(mountedRoot, key, "d-drawer");
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
  }
@@ -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)
@@ -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. Nested d-each (inside another
1147
- // d-each template) must be left untouched here — they will be bound when
1148
- // their parent clones are passed to bind() individually.
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: removes templates before TreeWalker visits them
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
- // 4. Text interpolation (template plan cache + lazy parser fallback)
2498
+ // 5. Text interpolation (template plan cache + lazy parser fallback)
2114
2499
  bindTextInterpolation(root, ctx, rawTextSelectors, templatePlanCacheConfig, benchSession);
2115
- // 5. d-attr bindings
2500
+ // 6. d-attr bindings
2116
2501
  bindAttrs(root, ctx, cleanups);
2117
- // 6. d-html bindings
2502
+ // 7. d-html bindings
2118
2503
  bindHtml(root, ctx, cleanups);
2119
- // 7. Form fields — register fields with form instances
2504
+ // 8. Form fields — register fields with form instances
2120
2505
  bindField(root, ctx, cleanups);
2121
- // 8. Event bindings
2506
+ // 9. Event bindings
2122
2507
  bindEvents(root, ctx, events, cleanups);
2123
- // 9. d-when directive
2508
+ // 10. d-when directive
2124
2509
  bindWhen(root, ctx, cleanups);
2125
- // 10. d-match directive
2510
+ // 11. d-match directive
2126
2511
  bindMatch(root, ctx, cleanups);
2127
- // 11. Form error displays — BEFORE d-if to bind errors in conditionally rendered sections
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
- // 12. d-if — must run last: elements are fully bound before conditional removal
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dalila",
3
- "version": "1.9.0",
3
+ "version": "1.9.2",
4
4
  "description": "DOM-first reactive framework based on signals",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -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-md);
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-color);
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-muted);
54
- border-bottom: 2px solid var(--d-text-muted);
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-color);
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: 0.2rem 0.6rem;
8
+ padding: 2px 10px;
9
9
 
10
10
  font-family: var(--d-font-sans);
11
- font-size: 0.7rem;
12
- font-weight: var(--d-font-semibold);
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: 4px;
21
+ border-radius: var(--d-radius-full);
22
22
  }
23
23
 
24
24
  .d-badge-primary {
25
- color: #ffffff;
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: #ffffff;
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: #ffffff;
65
+ color: var(--d-text-inverse);
60
66
  background: var(--d-gradient-brand);
61
67
  border-color: transparent;
62
68
  }