@loomweaver/shell 0.7.5 → 0.7.6

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.
@@ -65,8 +65,15 @@ function withoutQuery(url) {
65
65
  const end = url.search(/[?#]/);
66
66
  return end === -1 ? url : url.slice(0, end);
67
67
  }
68
+ function withoutTrailingSlashes$1(path) {
69
+ let end = path.length;
70
+ while (end > 0 && path[end - 1] === '/') {
71
+ end -= 1;
72
+ }
73
+ return path.slice(0, end);
74
+ }
68
75
  function bare(url) {
69
- return withoutQuery(url).replace(/^\/+/, '').replace(/\/+$/, '');
76
+ return withoutTrailingSlashes$1(withoutQuery(url).replace(/^\/+/, ''));
70
77
  }
71
78
  function isPopoutUrl(url) {
72
79
  const path = bare(url);
@@ -94,7 +101,7 @@ function popoutUrlFor(paneTarget) {
94
101
  }
95
102
 
96
103
  function upsertBy(items, item, matches) {
97
- const index = items.findIndex(matches);
104
+ const index = items.findIndex((existing) => matches(existing));
98
105
  if (index === -1) {
99
106
  return [...items, item];
100
107
  }
@@ -310,7 +317,7 @@ class ContributionRegistry {
310
317
  const visible = omitted.size === 0
311
318
  ? docked
312
319
  : docked.filter((entry) => entry.id === undefined || !omitted.has(entry.id));
313
- return visible.map(entryToView);
320
+ return visible.map((entry) => entryToView(entry));
314
321
  }, /* @ts-ignore */
315
322
  ...(ngDevMode ? [{ debugName: "views" }] : /* istanbul ignore next */ []));
316
323
  barItems = this.visible(this.barItemsSignal);
@@ -321,7 +328,7 @@ class ContributionRegistry {
321
328
  * picker, `matchRoute` — drops them without knowing about omission.
322
329
  */
323
330
  contentRoutes = computed(() => {
324
- const routes = this.routableSurfaces().map(entryToContentRoute);
331
+ const routes = this.routableSurfaces().map((entry) => entryToContentRoute(entry));
325
332
  const omitted = this.omittedSignal();
326
333
  if (omitted.size === 0) {
327
334
  return routes;
@@ -340,7 +347,7 @@ class ContributionRegistry {
340
347
  return NO_ROUTES;
341
348
  }
342
349
  return this.routableSurfaces()
343
- .map(entryToContentRoute)
350
+ .map((entry) => entryToContentRoute(entry))
344
351
  .filter((route) => isRouteOmitted(route, omitted));
345
352
  }, /* @ts-ignore */
346
353
  ...(ngDevMode ? [{ debugName: "omittedContentRoutes" }] : /* istanbul ignore next */ []));
@@ -436,13 +443,13 @@ class ContributionRegistry {
436
443
  }
437
444
  /** Adds a menu-slot item. With an `id` a re-registration replaces in place (last-in wins); without one the item is additive and dispose removes this exact contribution. */
438
445
  addMenuItem(item) {
439
- this.menuItemsSignal.update((items) => upsertBy(items, item, (i) => item.id !== undefined && i.id === item.id));
446
+ this.menuItemsSignal.update((items) => upsertBy(items, item, (index) => item.id !== undefined && index.id === item.id));
440
447
  return {
441
- dispose: () => this.menuItemsSignal.update((items) => items.filter((i) => i !== item)),
448
+ dispose: () => this.menuItemsSignal.update((items) => items.filter((index) => index !== item)),
442
449
  };
443
450
  }
444
451
  removeMenuItemById(id) {
445
- this.menuItemsSignal.update((items) => items.filter((i) => i.id !== id));
452
+ this.menuItemsSignal.update((items) => items.filter((index) => index.id !== id));
446
453
  }
447
454
  addSurface(entry) {
448
455
  this.surfacesSignal.update((entries) => upsertBy(entries, entry, sameSlotAs(entry)));
@@ -462,11 +469,11 @@ class ContributionRegistry {
462
469
  add(target, item) {
463
470
  target.update((items) => upsertById(items, item));
464
471
  return {
465
- dispose: () => target.update((items) => items.filter((i) => i !== item)),
472
+ dispose: () => target.update((items) => items.filter((index) => index !== item)),
466
473
  };
467
474
  }
468
475
  removeById(target, id) {
469
- target.update((items) => items.filter((i) => i.id !== id));
476
+ target.update((items) => items.filter((index) => index.id !== id));
470
477
  }
471
478
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.0", ngImport: i0, type: ContributionRegistry, deps: [], target: i0.ɵɵFactoryTarget.Service });
472
479
  static ɵprov = i0.ɵɵngDeclareService({ minVersion: "22.0.0", version: "22.1.0", ngImport: i0, type: ContributionRegistry });
@@ -485,7 +492,7 @@ function sameSlotAs(entry) {
485
492
  }
486
493
 
487
494
  function normalizePath(url) {
488
- return url.split(/[?#]/)[0].replace(/^\/+/, '');
495
+ return url.split(/[?#]/, 1)[0].replace(/^\/+/, '');
489
496
  }
490
497
  function segmentsOf(path) {
491
498
  return normalizePath(path).split('/').filter(Boolean);
@@ -538,11 +545,11 @@ function paramsOfPattern(pattern, path) {
538
545
  const parts = segmentsOf(pattern);
539
546
  const segments = segmentsOf(path);
540
547
  const params = {};
541
- parts.forEach((part, index) => {
548
+ for (const [index, part] of parts.entries()) {
542
549
  if (part.startsWith(':') && segments[index] !== undefined) {
543
550
  params[part.slice(1)] = segments[index];
544
551
  }
545
- });
552
+ }
546
553
  return params;
547
554
  }
548
555
  function routeParams(route, path) {
@@ -647,7 +654,7 @@ function reusableRoute(route) {
647
654
  return route.routeConfig?.data?.['chromeless'] !== true;
648
655
  }
649
656
  function effectiveRetain(declared, fallback) {
650
- return declared !== undefined ? declared === 'always' : fallback === 'retain';
657
+ return declared === undefined ? fallback === 'retain' : declared === 'always';
651
658
  }
652
659
  function routeRetains(route, fallback) {
653
660
  if (route.container !== undefined) {
@@ -672,7 +679,7 @@ function surfaceRetentionMode(routes, path) {
672
679
  if (!route || route.container !== undefined) {
673
680
  return 'rebuild';
674
681
  }
675
- return route.iframe !== undefined ? 'in-place' : 'move';
682
+ return route.iframe === undefined ? 'move' : 'in-place';
676
683
  }
677
684
  function containerChildInstances(entries, tabPath) {
678
685
  const scoped = containerDockFor(tabPath) + ':';
@@ -732,11 +739,11 @@ class ContentReuseStrategy {
732
739
  changes = signal(0, /* @ts-ignore */
733
740
  ...(ngDevMode ? [{ debugName: "changes" }] : /* istanbul ignore next */ []));
734
741
  version = this.changes.asReadonly();
735
- shouldReuseRoute(future, curr) {
736
- if (!isContentRoute(future) || !isContentRoute(curr)) {
737
- return future.routeConfig === curr.routeConfig;
742
+ shouldReuseRoute(future, current) {
743
+ if (!isContentRoute(future) || !isContentRoute(current)) {
744
+ return future.routeConfig === current.routeConfig;
738
745
  }
739
- return future.routeConfig === curr.routeConfig && sameParams(future, curr);
746
+ return future.routeConfig === current.routeConfig && sameParams(future, current);
740
747
  }
741
748
  shouldDetach(route) {
742
749
  return reusableRoute(route);
@@ -782,11 +789,12 @@ class ContentReuseStrategy {
782
789
  }
783
790
  pruneExcept(isLive) {
784
791
  for (const [key, handle] of this.handles) {
785
- if (!isLive(key)) {
786
- handle.componentRef?.destroy();
787
- this.handles.delete(key);
788
- this.changes.update((value) => value + 1);
792
+ if (isLive(key)) {
793
+ continue;
789
794
  }
795
+ handle.componentRef?.destroy();
796
+ this.handles.delete(key);
797
+ this.changes.update((value) => value + 1);
790
798
  }
791
799
  }
792
800
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.0", ngImport: i0, type: ContentReuseStrategy, deps: [], target: i0.ɵɵFactoryTarget.Service });
@@ -884,7 +892,7 @@ class StateSyncChannel {
884
892
  }
885
893
  try {
886
894
  const opened = new BroadcastChannel(CHANNEL_NAME);
887
- opened.onmessage = (event) => this.receive(event.data);
895
+ opened.addEventListener('message', (event) => this.receive(event.data));
888
896
  return opened;
889
897
  }
890
898
  catch {
@@ -1024,7 +1032,7 @@ function cleanArea(area, options, problems, depth) {
1024
1032
  problems.push(`${options.context}: the arrangement nests deeper than ${MAX_DEPTH} levels — the deeper areas are dropped.`);
1025
1033
  return null;
1026
1034
  }
1027
- const kinds = ['tabs', 'rows', 'columns'].filter((key) => key in area);
1035
+ const kinds = ['tabs', 'rows', 'columns'].filter((key) => Object.hasOwn(area, key));
1028
1036
  if (kinds.length !== 1) {
1029
1037
  problems.push(`${options.context}: a pane area must be exactly one of tabs, rows or columns.`);
1030
1038
  return null;
@@ -1093,7 +1101,7 @@ function cleanTabsArea(area, size, options, problems) {
1093
1101
  function collectTabs$1(area) {
1094
1102
  return area.kind === 'tabs'
1095
1103
  ? [...area.tabs]
1096
- : area.children.flatMap(collectTabs$1);
1104
+ : area.children.flatMap((child) => collectTabs$1(child));
1097
1105
  }
1098
1106
  function buildNode(area, idPrefix, ctx, indexPath) {
1099
1107
  if (area.kind === 'tabs') {
@@ -1122,6 +1130,12 @@ function buildNode(area, idPrefix, ctx, indexPath) {
1122
1130
  };
1123
1131
  return chain(0, 1);
1124
1132
  }
1133
+ function evenShareOf(remainder, declaredSum, children, unspecified) {
1134
+ if (unspecified === 0) {
1135
+ return 0;
1136
+ }
1137
+ return remainder > 0 ? remainder / unspecified : declaredSum / children;
1138
+ }
1125
1139
  function fractionsOf(children) {
1126
1140
  const declared = children
1127
1141
  .map((child) => child.size)
@@ -1129,11 +1143,7 @@ function fractionsOf(children) {
1129
1143
  const declaredSum = declared.reduce((sum, size) => sum + size, 0);
1130
1144
  const unspecified = children.length - declared.length;
1131
1145
  const remainder = Math.max(0, 100 - declaredSum);
1132
- const evenShare = unspecified > 0
1133
- ? remainder > 0
1134
- ? remainder / unspecified
1135
- : declaredSum / children.length
1136
- : 0;
1146
+ const evenShare = evenShareOf(remainder, declaredSum, children.length, unspecified);
1137
1147
  const weights = children.map((child) => child.size ?? evenShare);
1138
1148
  const total = weights.reduce((sum, weight) => sum + weight, 0);
1139
1149
  return weights.map((weight) => weight / total);
@@ -1168,7 +1178,12 @@ function claimFor(claims, path) {
1168
1178
  if (matching.length === 0) {
1169
1179
  return null;
1170
1180
  }
1171
- const best = matching.reduce((winner, claim) => narrower(claim.pattern, winner.pattern) > 0 ? claim : winner);
1181
+ let best = matching[0];
1182
+ for (const claim of matching) {
1183
+ if (narrower(claim.pattern, best.pattern) > 0) {
1184
+ best = claim;
1185
+ }
1186
+ }
1172
1187
  const tied = matching.filter((claim) => claim.workspaceId !== best.workspaceId &&
1173
1188
  narrower(claim.pattern, best.pattern) === 0);
1174
1189
  return tied.length === 0 ? best : null;
@@ -1186,9 +1201,12 @@ function contestedShapes(claims) {
1186
1201
  return new Map([...byShape].filter(([, owners]) => owners.length > 1));
1187
1202
  }
1188
1203
  function conflictingClaims(claims) {
1189
- return [...contestedShapes(claims)].map(([shape, owners]) => `Workspaces ${owners.map((id) => `"${id}"`).join(' and ')} both claim "${shape}" — ` +
1190
- `neither is narrower than the other, so the claim is dropped and that address ` +
1191
- `behaves as though nothing claimed it. Give the address one home.`);
1204
+ return [...contestedShapes(claims)].map(([shape, owners]) => {
1205
+ const named = owners.map((id) => `"${id}"`).join(' and ');
1206
+ return (`Workspaces ${named} both claim "${shape}" ` +
1207
+ `neither is narrower than the other, so the claim is dropped and that address ` +
1208
+ `behaves as though nothing claimed it. Give the address one home.`);
1209
+ });
1192
1210
  }
1193
1211
  function withoutConflicts(claims) {
1194
1212
  const contested = contestedShapes(claims);
@@ -1230,8 +1248,8 @@ function leafWith(id, tabs, candidateActive, declared) {
1230
1248
  kind: 'leaf',
1231
1249
  id,
1232
1250
  tabs,
1233
- ...(active === undefined ? {} : { active }),
1234
- ...(declared ? { declared: true } : {}),
1251
+ ...(active !== undefined && { active }),
1252
+ ...(declared && { declared: true }),
1235
1253
  };
1236
1254
  }
1237
1255
 
@@ -1324,15 +1342,13 @@ function normalizeTab(value) {
1324
1342
  }
1325
1343
  return {
1326
1344
  path: tab['path'],
1327
- ...(tab['pinned'] === true ? { pinned: true } : {}),
1328
- ...(tab['preview'] === true ? { preview: true } : {}),
1329
- ...(tab['closable'] === false ? { closable: false } : {}),
1330
- ...(typeof tab['title'] === 'string' ? { title: tab['title'] } : {}),
1331
- ...(tab['literalTitle'] === true ? { literalTitle: true } : {}),
1332
- ...(typeof tab['icon'] === 'string' ? { icon: tab['icon'] } : {}),
1333
- ...(typeof tab['instance'] === 'string'
1334
- ? { instance: tab['instance'] }
1335
- : {}),
1345
+ ...(tab['pinned'] === true && { pinned: true }),
1346
+ ...(tab['preview'] === true && { preview: true }),
1347
+ ...(tab['closable'] === false && { closable: false }),
1348
+ ...(typeof tab['title'] === 'string' && { title: tab['title'] }),
1349
+ ...(tab['literalTitle'] === true && { literalTitle: true }),
1350
+ ...(typeof tab['icon'] === 'string' && { icon: tab['icon'] }),
1351
+ ...(typeof tab['instance'] === 'string' && { instance: tab['instance'] }),
1336
1352
  };
1337
1353
  }
1338
1354
  function normalizeLeafNode(id, node) {
@@ -1342,7 +1358,7 @@ function normalizeLeafNode(id, node) {
1342
1358
  }
1343
1359
  const rawTabs = node['tabs'];
1344
1360
  const tabs = Array.isArray(rawTabs)
1345
- ? rawTabs.map(normalizeTab).filter((tab) => tab !== null)
1361
+ ? rawTabs.map((value) => normalizeTab(value)).filter((tab) => tab !== null)
1346
1362
  : [];
1347
1363
  const rawActive = node['active'];
1348
1364
  return leafWith(id, tabs, typeof rawActive === 'string' ? rawActive : undefined, node['declared'] === true);
@@ -1415,11 +1431,11 @@ function auditWorkspaceDefinitions(definitions, panelRegions) {
1415
1431
  }
1416
1432
  seen.add(definition.id);
1417
1433
  if (definition.initial) {
1418
- if (initial !== null) {
1419
- problems.push(`Workspace "${definition.id}" also declares initial: true — "${initial}" already does, so this one is ignored.`);
1434
+ if (initial === null) {
1435
+ initial = definition.id;
1420
1436
  }
1421
1437
  else {
1422
- initial = definition.id;
1438
+ problems.push(`Workspace "${definition.id}" also declares initial: true — "${initial}" already does, so this one is ignored.`);
1423
1439
  }
1424
1440
  }
1425
1441
  auditDefinition(definition, panelRegions, problems);
@@ -1477,13 +1493,13 @@ function baselineSidebars(definition, deps) {
1477
1493
  if (sidebars === undefined) {
1478
1494
  return {};
1479
1495
  }
1480
- const listed = deps.panelRegions.filter((region) => region in sidebars);
1496
+ const listed = deps.panelRegions.filter((region) => Object.hasOwn(sidebars, region));
1481
1497
  const hidden = listed
1482
1498
  .flatMap((region) => deps
1483
1499
  .declaredPaths(region)
1484
1500
  .map((path) => path.slice(VIEW_PANE_PREFIX.length))
1485
1501
  .filter((id) => !sidebars[region].includes(id)))
1486
- .sort();
1502
+ .toSorted((a, b) => a.localeCompare(b));
1487
1503
  return hidden.length === 0 ? {} : { hiddenViews: JSON.stringify(hidden) };
1488
1504
  }
1489
1505
  function declaredTabPaths(definition) {
@@ -1511,7 +1527,7 @@ function bakeTab(definitionId, entry, problems) {
1511
1527
  return {
1512
1528
  tab: {
1513
1529
  path: tab.path,
1514
- ...(tab.closable === false ? { closable: false } : {}),
1530
+ ...(tab.closable === false && { closable: false }),
1515
1531
  },
1516
1532
  active: tab.active === true,
1517
1533
  };
@@ -1592,9 +1608,16 @@ function movable(parent) {
1592
1608
  function supportsAtomicMove(document) {
1593
1609
  return movable(document.body ?? document.documentElement);
1594
1610
  }
1611
+ function insertNode(parent, node, before) {
1612
+ if (before === null) {
1613
+ parent.append(node);
1614
+ return;
1615
+ }
1616
+ before.before(node);
1617
+ }
1595
1618
  function moveNode(parent, node, before) {
1596
1619
  if (!movable(parent) || !node.isConnected) {
1597
- parent.insertBefore(node, before);
1620
+ insertNode(parent, node, before);
1598
1621
  return false;
1599
1622
  }
1600
1623
  try {
@@ -1602,7 +1625,7 @@ function moveNode(parent, node, before) {
1602
1625
  return true;
1603
1626
  }
1604
1627
  catch {
1605
- parent.insertBefore(node, before);
1628
+ insertNode(parent, node, before);
1606
1629
  return false;
1607
1630
  }
1608
1631
  }
@@ -1652,10 +1675,11 @@ class RetainedViewStash {
1652
1675
  },
1653
1676
  stale: () => entry.tracked && (!owns() || this.entries.get(key) !== entry),
1654
1677
  describe: (mode, retain) => {
1655
- if (owns()) {
1656
- entry.mode = mode;
1657
- entry.keep = retain;
1678
+ if (!owns()) {
1679
+ return;
1658
1680
  }
1681
+ entry.mode = mode;
1682
+ entry.keep = retain;
1659
1683
  },
1660
1684
  release: (retained) => {
1661
1685
  if (owns()) {
@@ -1722,14 +1746,16 @@ class RetainedViewStash {
1722
1746
  }
1723
1747
  }
1724
1748
  evictWorkspace(workspaceId) {
1725
- for (const entry of [...this.entries.values()]) {
1749
+ const snapshot = [...this.entries.values()];
1750
+ for (const entry of snapshot) {
1726
1751
  if (!entry.inUse && entry.workspace === workspaceId) {
1727
1752
  this.destroyEntry(entry);
1728
1753
  }
1729
1754
  }
1730
1755
  }
1731
1756
  ngOnDestroy() {
1732
- for (const entry of [...this.entries.values()]) {
1757
+ const snapshot = [...this.entries.values()];
1758
+ for (const entry of snapshot) {
1733
1759
  this.destroyEntry(entry);
1734
1760
  }
1735
1761
  this.holdingArea?.remove();
@@ -1835,7 +1861,7 @@ class RetainedViewStash {
1835
1861
  const area = this.document.createElement('div');
1836
1862
  area.dataset['lwRetentionHold'] = '';
1837
1863
  area.style.display = 'none';
1838
- this.document.body.appendChild(area);
1864
+ this.document.body.append(area);
1839
1865
  this.holdingArea = area;
1840
1866
  return area;
1841
1867
  }
@@ -1846,7 +1872,7 @@ class RetainedViewStash {
1846
1872
  }
1847
1873
  pullNodes(entry) {
1848
1874
  for (const node of liveRootNodes(entry)) {
1849
- node.parentNode?.removeChild(node);
1875
+ node.remove();
1850
1876
  }
1851
1877
  }
1852
1878
  destroyEntry(entry) {
@@ -1870,7 +1896,8 @@ class RetainedViewStash {
1870
1896
  this.sweepQueued = true;
1871
1897
  queueMicrotask(() => {
1872
1898
  this.sweepQueued = false;
1873
- for (const entry of [...this.entries.values()]) {
1899
+ const snapshot = [...this.entries.values()];
1900
+ for (const entry of snapshot) {
1874
1901
  if (entry.inUse) {
1875
1902
  continue;
1876
1903
  }
@@ -1950,7 +1977,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.0", ngImpor
1950
1977
  type: Service
1951
1978
  }] });
1952
1979
  function pathOfStashKey(key) {
1953
- return key.split('|')[1] ?? '';
1980
+ return key.split('|', 2)[1] ?? '';
1954
1981
  }
1955
1982
 
1956
1983
  class RetentionUnloadGuard {
@@ -1983,7 +2010,6 @@ class RetentionUnloadGuard {
1983
2010
  return;
1984
2011
  }
1985
2012
  event.preventDefault();
1986
- event.returnValue = '';
1987
2013
  };
1988
2014
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.0", ngImport: i0, type: RetentionUnloadGuard, deps: [], target: i0.ɵɵFactoryTarget.Service });
1989
2015
  static ɵprov = i0.ɵɵngDeclareService({ minVersion: "22.0.0", version: "22.1.0", ngImport: i0, type: RetentionUnloadGuard });
@@ -2169,16 +2195,20 @@ function isMacPlatform() {
2169
2195
  function normaliseKey(key) {
2170
2196
  const lower = key.toLowerCase();
2171
2197
  switch (lower) {
2172
- case 'esc':
2198
+ case 'esc': {
2173
2199
  return 'escape';
2174
- case 'return':
2200
+ }
2201
+ case 'return': {
2175
2202
  return 'enter';
2203
+ }
2176
2204
  case 'space':
2177
2205
  case 'spacebar':
2178
- case ' ':
2206
+ case ' ': {
2179
2207
  return 'space';
2180
- default:
2208
+ }
2209
+ default: {
2181
2210
  return lower;
2211
+ }
2182
2212
  }
2183
2213
  }
2184
2214
  function toSignature(parts) {
@@ -2202,72 +2232,89 @@ function chordSignature(chord, isMac) {
2202
2232
  };
2203
2233
  for (const raw of chord.split('+')) {
2204
2234
  const token = raw.trim().toLowerCase();
2205
- if (!token) {
2206
- continue;
2235
+ if (token) {
2236
+ applyToken(parts, token, isMac);
2207
2237
  }
2208
- switch (token) {
2209
- case 'mod':
2210
- if (isMac)
2211
- parts.meta = true;
2212
- else
2213
- parts.ctrl = true;
2214
- break;
2215
- case 'ctrl':
2216
- case 'control':
2217
- parts.ctrl = true;
2218
- break;
2219
- case 'meta':
2220
- case 'cmd':
2221
- case 'command':
2222
- case 'win':
2238
+ }
2239
+ return parts.key ? toSignature(parts) : null;
2240
+ }
2241
+ function applyToken(parts, token, isMac) {
2242
+ switch (token) {
2243
+ case 'mod': {
2244
+ if (isMac)
2223
2245
  parts.meta = true;
2224
- break;
2225
- case 'alt':
2226
- case 'option':
2227
- parts.alt = true;
2228
- break;
2229
- case 'shift':
2230
- parts.shift = true;
2231
- break;
2232
- default:
2233
- parts.key = normaliseKey(token);
2246
+ else
2247
+ parts.ctrl = true;
2248
+ return;
2249
+ }
2250
+ case 'ctrl':
2251
+ case 'control': {
2252
+ parts.ctrl = true;
2253
+ return;
2254
+ }
2255
+ case 'meta':
2256
+ case 'cmd':
2257
+ case 'command':
2258
+ case 'win': {
2259
+ parts.meta = true;
2260
+ return;
2261
+ }
2262
+ case 'alt':
2263
+ case 'option': {
2264
+ parts.alt = true;
2265
+ return;
2266
+ }
2267
+ case 'shift': {
2268
+ parts.shift = true;
2269
+ return;
2270
+ }
2271
+ default: {
2272
+ parts.key = normaliseKey(token);
2234
2273
  }
2235
2274
  }
2236
- return parts.key ? toSignature(parts) : null;
2237
2275
  }
2238
2276
  function formatShortcut(chord, isMac) {
2239
2277
  const tokens = chord.split('+').map((raw) => {
2240
2278
  const token = raw.trim().toLowerCase();
2241
2279
  switch (token) {
2242
- case 'mod':
2280
+ case 'mod': {
2243
2281
  return isMac ? '⌘' : 'Ctrl';
2282
+ }
2244
2283
  case 'ctrl':
2245
- case 'control':
2284
+ case 'control': {
2246
2285
  return isMac ? '⌃' : 'Ctrl';
2286
+ }
2247
2287
  case 'meta':
2248
2288
  case 'cmd':
2249
2289
  case 'command':
2250
- case 'win':
2290
+ case 'win': {
2251
2291
  return isMac ? '⌘' : 'Meta';
2292
+ }
2252
2293
  case 'alt':
2253
- case 'option':
2294
+ case 'option': {
2254
2295
  return isMac ? '⌥' : 'Alt';
2255
- case 'shift':
2296
+ }
2297
+ case 'shift': {
2256
2298
  return isMac ? '⇧' : 'Shift';
2299
+ }
2257
2300
  case 'enter':
2258
- case 'return':
2301
+ case 'return': {
2259
2302
  return isMac ? '↵' : 'Enter';
2303
+ }
2260
2304
  case 'escape':
2261
- case 'esc':
2305
+ case 'esc': {
2262
2306
  return 'Esc';
2307
+ }
2263
2308
  case 'space':
2264
2309
  case 'spacebar':
2265
- case ' ':
2310
+ case ' ': {
2266
2311
  return 'Space';
2267
- default:
2312
+ }
2313
+ default: {
2268
2314
  return token.length === 1
2269
2315
  ? token.toUpperCase()
2270
2316
  : token.charAt(0).toUpperCase() + token.slice(1);
2317
+ }
2271
2318
  }
2272
2319
  });
2273
2320
  return tokens.join(isMac ? '' : '+');
@@ -2428,20 +2475,20 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.0", ngImpor
2428
2475
  type: Service
2429
2476
  }] });
2430
2477
 
2431
- function upgradeElementProperty(el, name) {
2432
- const self = el;
2433
- if (Object.prototype.hasOwnProperty.call(el, name)) {
2478
+ function upgradeElementProperty(element, name) {
2479
+ const self = element;
2480
+ if (Object.hasOwn(element, name)) {
2434
2481
  const value = self[name];
2435
2482
  delete self[name];
2436
2483
  self[name] = value;
2437
2484
  }
2438
2485
  }
2439
- function reflectAttribute(el, name, value) {
2486
+ function reflectAttribute(element, name, value) {
2440
2487
  if (value === null || value === undefined) {
2441
- el.removeAttribute(name);
2488
+ element.removeAttribute(name);
2442
2489
  }
2443
2490
  else {
2444
- el.setAttribute(name, value);
2491
+ element.setAttribute(name, value);
2445
2492
  }
2446
2493
  }
2447
2494
 
@@ -2568,7 +2615,9 @@ class LwMenuElement extends HTMLElement {
2568
2615
  return;
2569
2616
  }
2570
2617
  this.active = (index + items.length) % items.length;
2571
- items.forEach((item, i) => (item.tabIndex = i === this.active ? 0 : -1));
2618
+ for (const [index_, item] of items.entries()) {
2619
+ item.tabIndex = index_ === this.active ? 0 : -1;
2620
+ }
2572
2621
  const item = items[this.active];
2573
2622
  item.focus();
2574
2623
  item.scrollIntoView?.({ block: 'nearest' });
@@ -2581,18 +2630,22 @@ class LwMenuElement extends HTMLElement {
2581
2630
  }
2582
2631
  handleKeydown(event) {
2583
2632
  switch (event.key) {
2584
- case 'ArrowDown':
2633
+ case 'ArrowDown': {
2585
2634
  this.setActive(this.active < 0 ? 0 : this.active + 1);
2586
2635
  break;
2587
- case 'ArrowUp':
2588
- this.setActive(this.active < 0 ? this.items().length - 1 : this.active - 1);
2636
+ }
2637
+ case 'ArrowUp': {
2638
+ this.setActive((this.active < 0 ? this.items().length : this.active) - 1);
2589
2639
  break;
2590
- case 'Home':
2640
+ }
2641
+ case 'Home': {
2591
2642
  this.setActive(0);
2592
2643
  break;
2593
- case 'End':
2644
+ }
2645
+ case 'End': {
2594
2646
  this.setActive(this.items().length - 1);
2595
2647
  break;
2648
+ }
2596
2649
  case 'Enter':
2597
2650
  case ' ': {
2598
2651
  const item = this.active >= 0 ? this.items()[this.active] : undefined;
@@ -2602,11 +2655,13 @@ class LwMenuElement extends HTMLElement {
2602
2655
  break;
2603
2656
  }
2604
2657
  case 'Escape':
2605
- case 'Tab':
2658
+ case 'Tab': {
2606
2659
  this.dispatchEvent(new CustomEvent(LW_MENU_DISMISS, { bubbles: true }));
2607
2660
  return;
2608
- default:
2661
+ }
2662
+ default: {
2609
2663
  return;
2664
+ }
2610
2665
  }
2611
2666
  event.preventDefault();
2612
2667
  }
@@ -2685,7 +2740,7 @@ class MenuService {
2685
2740
  document.body.append(menu);
2686
2741
  document.body.classList.add('lw-menu-open');
2687
2742
  menu.openAt(at.x, at.y);
2688
- const listenTimer = setTimeout(() => document.addEventListener('pointerdown', onOutside, true));
2743
+ const listenTimer = setTimeout(() => document.addEventListener('pointerdown', onOutside, { capture: true }), 0);
2689
2744
  this.current = { menu, onOutside, restore, listenTimer };
2690
2745
  }
2691
2746
  resolve(menuIds, context) {
@@ -2718,7 +2773,7 @@ class MenuService {
2718
2773
  };
2719
2774
  })
2720
2775
  .filter((entry) => entry !== null)
2721
- .sort((a, b) => a.group.localeCompare(b.group) || a.order - b.order);
2776
+ .toSorted((a, b) => a.group.localeCompare(b.group) || a.order - b.order);
2722
2777
  }
2723
2778
  createMenu(resolved) {
2724
2779
  const menu = document.createElement(LW_MENU_TAG);
@@ -2871,7 +2926,7 @@ class ShellBarItem {
2871
2926
  shortcut = computed(() => {
2872
2927
  const button = this.asButton();
2873
2928
  if (!button?.showShortcut || !button.command) {
2874
- return undefined;
2929
+ return;
2875
2930
  }
2876
2931
  return this.commands.shortcutOf(this.commands.commands().find((entry) => entry.id === button.command));
2877
2932
  }, /* @ts-ignore */
@@ -2911,7 +2966,7 @@ class ShellBar {
2911
2966
  .filter((item) => item.bar === this.region().id && item.slot === slot)
2912
2967
  .filter((item) => this.auth.visible(item.access))
2913
2968
  .filter((item) => 'component' in item || this.commands.triggerable(item))
2914
- .sort((a, b) => (a.order ?? 0) - (b.order ?? 0));
2969
+ .toSorted((a, b) => (a.order ?? 0) - (b.order ?? 0));
2915
2970
  }
2916
2971
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.0", ngImport: i0, type: ShellBar, deps: [], target: i0.ɵɵFactoryTarget.Component });
2917
2972
  static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.1.0", type: ShellBar, isStandalone: true, selector: "lw-shell-bar", inputs: { region: { classPropertyName: "region", publicName: "region", isSignal: true, isRequired: true, transformFunction: null } }, ngImport: i0, template: "<header\n class=\"flex items-center gap-2 overflow-hidden border-border bg-surface px-2 md:px-4\"\n [class.border-b]=\"dock() === 'top'\"\n [class.h-12]=\"dock() === 'top'\"\n [class.border-t]=\"dock() === 'bottom' || footer()\"\n [class.py-1]=\"dock() === 'bottom' || footer()\"\n [class.text-xs]=\"dock() === 'bottom'\"\n [class.text-content-muted]=\"dock() === 'bottom'\"\n>\n <div class=\"flex min-w-0 items-center gap-2\">\n @for (item of startItems(); track item.id) {\n <lw-shell-bar-item [item]=\"item\" [dock]=\"dock()\" />\n }\n </div>\n\n @if (centerItems().length) {\n <div class=\"mx-auto flex shrink-0 items-center gap-2\">\n @for (item of centerItems(); track item.id) {\n <lw-shell-bar-item [item]=\"item\" [dock]=\"dock()\" />\n }\n </div>\n }\n\n <div class=\"ml-auto flex shrink-0 items-center gap-2\">\n @for (item of endItems(); track item.id) {\n <lw-shell-bar-item [item]=\"item\" [dock]=\"dock()\" />\n }\n </div>\n</header>\n", dependencies: [{ kind: "component", type: ShellBarItem, selector: "lw-shell-bar-item", inputs: ["item", "dock"] }] });
@@ -3182,7 +3237,7 @@ class RailItemsService {
3182
3237
  }
3183
3238
  isVisible(itemId) {
3184
3239
  if (isWorkspaceRailItem(itemId)) {
3185
- return itemId in this.state().placed;
3240
+ return Object.hasOwn(this.state().placed, itemId);
3186
3241
  }
3187
3242
  return !this.state().hidden.includes(itemId);
3188
3243
  }
@@ -3318,15 +3373,15 @@ class Reorderable {
3318
3373
  moveByKeyboard(item, direction) {
3319
3374
  const id = item.dataset['reorderId'] ?? '';
3320
3375
  const band = item.dataset['reorderBand'] ?? '';
3321
- const inBand = this.items().filter((el) => (el.dataset['reorderBand'] ?? '') === band);
3322
- const bandIds = inBand.map((el) => el.dataset['reorderId'] ?? '');
3376
+ const inBand = this.items().filter((element) => (element.dataset['reorderBand'] ?? '') === band);
3377
+ const bandIds = inBand.map((element) => element.dataset['reorderId'] ?? '');
3323
3378
  const target = bandIds.indexOf(id) + direction;
3324
3379
  if (target < 0 || target >= bandIds.length) {
3325
3380
  return false;
3326
3381
  }
3327
3382
  const neighbourId = bandIds[target];
3328
3383
  const without = this.items()
3329
- .map((el) => el.dataset['reorderId'] ?? '')
3384
+ .map((element) => element.dataset['reorderId'] ?? '')
3330
3385
  .filter((x) => x !== id);
3331
3386
  const insertAt = without.indexOf(neighbourId) + (direction > 0 ? 1 : 0);
3332
3387
  without.splice(insertAt, 0, id);
@@ -3341,7 +3396,7 @@ class Reorderable {
3341
3396
  }
3342
3397
  focusItem(id) {
3343
3398
  this.items()
3344
- .find((el) => (el.dataset['reorderId'] ?? '') === id)
3399
+ .find((element) => (element.dataset['reorderId'] ?? '') === id)
3345
3400
  ?.focus();
3346
3401
  }
3347
3402
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.0", ngImport: i0, type: Reorderable, deps: [], target: i0.ɵɵFactoryTarget.Directive });
@@ -3392,8 +3447,8 @@ class UserOrderService {
3392
3447
  const rank = new Map(sequence.map((id, index) => [id, index]));
3393
3448
  const ranked = (item) => rank.has(key(item));
3394
3449
  const knownInUserOrder = items
3395
- .filter(ranked)
3396
- .sort((a, b) => (rank.get(key(a)) ?? 0) - (rank.get(key(b)) ?? 0));
3450
+ .filter((item) => ranked(item))
3451
+ .toSorted((a, b) => (rank.get(key(a)) ?? 0) - (rank.get(key(b)) ?? 0));
3397
3452
  let next = 0;
3398
3453
  return items.map((item) => ranked(item) ? knownInUserOrder[next++] : item);
3399
3454
  }
@@ -3513,7 +3568,7 @@ function defaultTabTitle(route, root) {
3513
3568
  function withRefreshedPath(tabs, index, path) {
3514
3569
  return tabs[index].path === path
3515
3570
  ? tabs
3516
- : tabs.map((tab, i) => (i === index ? { ...tab, path } : tab));
3571
+ : tabs.map((tab, index_) => (index_ === index ? { ...tab, path } : tab));
3517
3572
  }
3518
3573
  function autoOpenedTab(route, root, path) {
3519
3574
  if (!route ||
@@ -3553,11 +3608,11 @@ function toPaneTab(tab) {
3553
3608
  return {
3554
3609
  path: tab.path,
3555
3610
  title: tab.title,
3556
- ...(tab.literalTitle ? { literalTitle: true } : {}),
3557
- ...(tab.icon === undefined ? {} : { icon: tab.icon }),
3558
- ...(tab.pinned ? { pinned: true } : {}),
3559
- ...(tab.preview ? { preview: true } : {}),
3560
- ...(tab.closable ? {} : { closable: false }),
3611
+ ...(tab.literalTitle && { literalTitle: true }),
3612
+ ...(tab.icon !== undefined && { icon: tab.icon }),
3613
+ ...(tab.pinned && { pinned: true }),
3614
+ ...(tab.preview && { preview: true }),
3615
+ ...(!tab.closable && { closable: false }),
3561
3616
  };
3562
3617
  }
3563
3618
  function facetTabViews(routes, addressOf = (route) => route.path) {
@@ -3565,7 +3620,7 @@ function facetTabViews(routes, addressOf = (route) => route.path) {
3565
3620
  .filter((route) => route.follows === true)
3566
3621
  .map((route) => ({ route, address: addressOf(route) }))
3567
3622
  .filter((entry) => entry.address !== null)
3568
- .sort((a, b) => (a.route.order ?? 0) - (b.route.order ?? 0) ||
3623
+ .toSorted((a, b) => (a.route.order ?? 0) - (b.route.order ?? 0) ||
3569
3624
  a.route.path.localeCompare(b.route.path))
3570
3625
  .map(({ route, address }, index) => ({
3571
3626
  path: route.path,
@@ -3656,11 +3711,11 @@ function collidingParam(pattern, other) {
3656
3711
  function paramPrefixes(pattern) {
3657
3712
  const prefixes = new Map();
3658
3713
  const segments = segmentsOf(pattern);
3659
- segments.forEach((segment, index) => {
3714
+ for (const [index, segment] of segments.entries()) {
3660
3715
  if (segment.startsWith(':')) {
3661
3716
  prefixes.set(segment.slice(1), segments.slice(0, index).join('/'));
3662
3717
  }
3663
- });
3718
+ }
3664
3719
  return prefixes;
3665
3720
  }
3666
3721
 
@@ -3748,22 +3803,22 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.0", ngImpor
3748
3803
  type: Service
3749
3804
  }] });
3750
3805
 
3751
- function transformLeaf(node, paneId, fn) {
3806
+ function transformLeaf(node, paneId, function_) {
3752
3807
  if (node.kind === 'leaf') {
3753
- return node.id === paneId ? fn(node) : node;
3808
+ return node.id === paneId ? function_(node) : node;
3754
3809
  }
3755
- const first = transformLeaf(node.first, paneId, fn);
3756
- const second = transformLeaf(node.second, paneId, fn);
3810
+ const first = transformLeaf(node.first, paneId, function_);
3811
+ const second = transformLeaf(node.second, paneId, function_);
3757
3812
  return first === node.first && second === node.second
3758
3813
  ? node
3759
3814
  : { ...node, first, second };
3760
3815
  }
3761
- function collapseLeaf(node, paneId, fn) {
3816
+ function collapseLeaf(node, paneId, function_) {
3762
3817
  if (node.kind === 'leaf') {
3763
- return node.id === paneId ? fn(node) : node;
3818
+ return node.id === paneId ? function_(node) : node;
3764
3819
  }
3765
- const first = collapseLeaf(node.first, paneId, fn);
3766
- const second = collapseLeaf(node.second, paneId, fn);
3820
+ const first = collapseLeaf(node.first, paneId, function_);
3821
+ const second = collapseLeaf(node.second, paneId, function_);
3767
3822
  if (first === null) {
3768
3823
  return second;
3769
3824
  }
@@ -3878,8 +3933,8 @@ function pruneEmptyLeaves(node, primaryId, spare) {
3878
3933
  function insertTab(node, paneId, tab, index) {
3879
3934
  return transformLeaf(node, paneId, (leaf) => {
3880
3935
  const existingIndex = leaf.tabs.findIndex((existing) => existing.path === tab.path);
3881
- if (existingIndex >= 0) {
3882
- const tabs = leaf.tabs.map((existing, i) => i === existingIndex ? tab : existing);
3936
+ if (existingIndex !== -1) {
3937
+ const tabs = leaf.tabs.map((existing, index_) => index_ === existingIndex ? tab : existing);
3883
3938
  return { ...leaf, tabs, active: tab.path };
3884
3939
  }
3885
3940
  const at = index === undefined
@@ -3917,7 +3972,7 @@ function refineLeafTitles(leaf, skipLeaf, matches, patch) {
3917
3972
  ...tab,
3918
3973
  title: patch.title,
3919
3974
  literalTitle: patch.literalTitle,
3920
- ...(patch.icon === undefined ? {} : { icon: patch.icon }),
3975
+ ...(patch.icon !== undefined && { icon: patch.icon }),
3921
3976
  };
3922
3977
  });
3923
3978
  return found
@@ -3955,7 +4010,7 @@ function reseatPinned(tabs, index, updated) {
3955
4010
  function pinTab(node, paneId, tabPath) {
3956
4011
  return transformLeaf(node, paneId, (leaf) => {
3957
4012
  const index = leaf.tabs.findIndex((tab) => tab.path === tabPath);
3958
- if (index < 0 || leaf.tabs[index].pinned === true) {
4013
+ if (index === -1 || leaf.tabs[index].pinned === true) {
3959
4014
  return leaf;
3960
4015
  }
3961
4016
  const pinned = {
@@ -3968,7 +4023,7 @@ function pinTab(node, paneId, tabPath) {
3968
4023
  function unpinTab(node, paneId, tabPath) {
3969
4024
  return transformLeaf(node, paneId, (leaf) => {
3970
4025
  const index = leaf.tabs.findIndex((tab) => tab.path === tabPath && tab.pinned);
3971
- if (index < 0) {
4026
+ if (index === -1) {
3972
4027
  return leaf;
3973
4028
  }
3974
4029
  const unpinned = tabWithout(leaf.tabs[index], 'pinned');
@@ -4025,7 +4080,7 @@ class PaneTreeService {
4025
4080
  }
4026
4081
  stackView(dock, viewId) {
4027
4082
  const segments = paneSegments(this.tree(dock));
4028
- const last = segments[segments.length - 1];
4083
+ const last = segments.at(-1);
4029
4084
  if (last) {
4030
4085
  this.splitPane(dock, last.id, 'column', VIEW_PANE_PREFIX + viewId);
4031
4086
  }
@@ -4058,7 +4113,7 @@ class PaneTreeService {
4058
4113
  return;
4059
4114
  }
4060
4115
  const rank = new Map(order.map((path, index) => [path, index]));
4061
- const tabs = [...leaf.tabs].sort((a, b) => (rank.get(a.path) ?? leaf.tabs.indexOf(a)) -
4116
+ const tabs = [...leaf.tabs].toSorted((a, b) => (rank.get(a.path) ?? leaf.tabs.indexOf(a)) -
4062
4117
  (rank.get(b.path) ?? leaf.tabs.indexOf(b)));
4063
4118
  this.commit(dock, setTabs(this.tree(dock), paneId, tabs));
4064
4119
  }
@@ -4110,7 +4165,7 @@ class PaneTreeService {
4110
4165
  const tree = this.tree(dock);
4111
4166
  const primary = this.primaryId(dock);
4112
4167
  const leaf = paneId === primary ? findLeaf(tree, primary) : undefined;
4113
- if (leaf && !leaf.tabs.some((tab) => tab.path === tabPath)) {
4168
+ if (leaf?.tabs.every((tab) => tab.path !== tabPath)) {
4114
4169
  return;
4115
4170
  }
4116
4171
  if (leaf && !leaf.declared && leaf.tabs.length <= 1) {
@@ -4165,7 +4220,7 @@ class PaneTreeService {
4165
4220
  return this.docks()[dock] !== undefined;
4166
4221
  }
4167
4222
  dropDock(dock) {
4168
- if (!this.docks()[dock]) {
4223
+ if (!this.hasDock(dock)) {
4169
4224
  return;
4170
4225
  }
4171
4226
  this.docks.update((docks) => {
@@ -4196,7 +4251,7 @@ class PaneTreeService {
4196
4251
  this.docks.update((current) => {
4197
4252
  const merged = { ...persisted };
4198
4253
  for (const [dock, entry] of Object.entries(current)) {
4199
- if (isContainerDock(dock) && !(dock in persisted)) {
4254
+ if (isContainerDock(dock) && !Object.hasOwn(persisted, dock)) {
4200
4255
  merged[dock] = entry;
4201
4256
  }
4202
4257
  }
@@ -4331,7 +4386,7 @@ class OpenTabsService {
4331
4386
  activeViewInstance = computed(() => {
4332
4387
  const path = this.activeViewPath();
4333
4388
  if (path === null) {
4334
- return undefined;
4389
+ return;
4335
4390
  }
4336
4391
  return this.paneTree
4337
4392
  .primaryTabs(CONTENT_DOCK)
@@ -4367,7 +4422,7 @@ class OpenTabsService {
4367
4422
  const facets = facetTabViews(routes, (route) => this.addressOf(route));
4368
4423
  const open = this.openTabs().filter((tab) => this.strippable(routes, tab));
4369
4424
  const dynamics = dynamicTabViews(routes, open);
4370
- return [...facets, ...dynamics, ...this.viewTabs()].sort((a, b) => a.order - b.order);
4425
+ return [...facets, ...dynamics, ...this.viewTabs()].toSorted((a, b) => a.order - b.order);
4371
4426
  }, /* @ts-ignore */
4372
4427
  ...(ngDevMode ? [{ debugName: "tabs" }] : /* istanbul ignore next */ []));
4373
4428
  viewTabs = computed(() => viewTabViews(this.paneTree.primaryTabs(CONTENT_DOCK), (id) => this.registry.views().find((view) => view.id === id), (access) => this.auth.meets(access), VIEW_PANE_PREFIX), /* @ts-ignore */
@@ -4419,9 +4474,9 @@ class OpenTabsService {
4419
4474
  navigateTo(path) {
4420
4475
  this.navigate(path).catch((error) => console.error('Content navigation failed', error));
4421
4476
  }
4422
- updateOpen(fn) {
4477
+ updateOpen(function_) {
4423
4478
  const current = this.openTabs();
4424
- const next = fn(current);
4479
+ const next = function_(current);
4425
4480
  if (next === current) {
4426
4481
  return;
4427
4482
  }
@@ -4496,7 +4551,7 @@ class OpenTabsService {
4496
4551
  const routes = this.registry.contentRoutes();
4497
4552
  this.updateOpen((tabs) => {
4498
4553
  const index = tabs.findIndex((tab) => tabRootOf(routes, tab.path) === root);
4499
- if (index >= 0) {
4554
+ if (index !== -1) {
4500
4555
  return withRefreshedPath(tabs, index, path);
4501
4556
  }
4502
4557
  const opened = autoOpenedTab(route, root, path);
@@ -4511,7 +4566,7 @@ class OpenTabsService {
4511
4566
  return (route !== undefined &&
4512
4567
  route.chromeless !== true &&
4513
4568
  route.follows !== true &&
4514
- !(normalizePath(route.path) === '' && normalizePath(tab.path) !== ''));
4569
+ (normalizePath(route.path) !== '' || normalizePath(tab.path) === ''));
4515
4570
  }
4516
4571
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.0", ngImport: i0, type: OpenTabsService, deps: [], target: i0.ɵɵFactoryTarget.Service });
4517
4572
  static ɵprov = i0.ɵɵngDeclareService({ minVersion: "22.0.0", version: "22.1.0", ngImport: i0, type: OpenTabsService });
@@ -4927,7 +4982,7 @@ class SurfaceCloseGuard {
4927
4982
  }
4928
4983
  async saveAll(dirty) {
4929
4984
  try {
4930
- await Promise.all(dirty.map((surface) => surface.surfaceSave?.()));
4985
+ await Promise.all(dirty.map(async (surface) => surface.surfaceSave?.()));
4931
4986
  }
4932
4987
  catch (error) {
4933
4988
  console.error('Save before closing failed', error);
@@ -5074,9 +5129,17 @@ class TabClosingService {
5074
5129
  const closing = this.state.openTabs().filter((tab) => roots.has(tabRootOf(routes, tab.path)));
5075
5130
  const activeWentAway = roots.has(this.state.activeTabRoot());
5076
5131
  this.state.updateOpen((tabs) => tabs.filter((tab) => !roots.has(tabRootOf(routes, tab.path))));
5077
- closing.forEach((tab) => this.closeHooks.runSafely(tab.onClose));
5078
- roots.forEach((root) => this.closeHooks.delete(root));
5079
- const evictAll = () => roots.forEach((root) => this.reuse.evict(root));
5132
+ for (const tab of closing) {
5133
+ this.closeHooks.runSafely(tab.onClose);
5134
+ }
5135
+ for (const root of roots) {
5136
+ this.closeHooks.delete(root);
5137
+ }
5138
+ const evictAll = () => {
5139
+ for (const root of roots) {
5140
+ this.reuse.evict(root);
5141
+ }
5142
+ };
5080
5143
  if (activeWentAway) {
5081
5144
  void this.state.navigate(fallbackPath)
5082
5145
  .catch((error) => console.error('Content navigation failed', error))
@@ -5108,7 +5171,7 @@ class TabClosingService {
5108
5171
  neighbourPath(root) {
5109
5172
  const routes = this.registry.contentRoutes();
5110
5173
  const siblings = this.state.openTabs().filter((tab) => tabRootOf(routes, tab.path) !== root);
5111
- return siblings.length > 0 ? siblings[siblings.length - 1].path : '';
5174
+ return siblings.at(-1)?.path ?? '';
5112
5175
  }
5113
5176
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.0", ngImport: i0, type: TabClosingService, deps: [], target: i0.ɵɵFactoryTarget.Service });
5114
5177
  static ɵprov = i0.ɵɵngDeclareService({ minVersion: "22.0.0", version: "22.1.0", ngImport: i0, type: TabClosingService });
@@ -5193,7 +5256,7 @@ class ContentTabsService {
5193
5256
  const seat = (tab, fallback) => rank.get(tabRootOf(routes, tab.path)) ?? fallback;
5194
5257
  this.state.updateOpen((tabs) => tabs
5195
5258
  .map((tab, index) => ({ tab, index }))
5196
- .sort((a, b) => seat(a.tab, a.index) - seat(b.tab, b.index))
5259
+ .toSorted((a, b) => seat(a.tab, a.index) - seat(b.tab, b.index))
5197
5260
  .map((entry) => entry.tab));
5198
5261
  }
5199
5262
  /**
@@ -5209,7 +5272,7 @@ class ContentTabsService {
5209
5272
  const { routes, root } = this.state.rootFor(path);
5210
5273
  this.state.updateOpen((tabs) => {
5211
5274
  const index = tabs.findIndex((tab) => tabRootOf(routes, tab.path) === root);
5212
- return index < 0 || tabs[index].pinned
5275
+ return index === -1 || tabs[index].pinned
5213
5276
  ? tabs
5214
5277
  : reseatPinned(tabs, index, tabs[index]);
5215
5278
  });
@@ -5364,7 +5427,7 @@ class ContentTabsService {
5364
5427
  const { routes, root } = this.state.rootFor(path);
5365
5428
  this.state.updateOpen((tabs) => {
5366
5429
  const index = tabs.findIndex((tab) => tabRootOf(routes, tab.path) === root);
5367
- if (index < 0 || tabs[index].pinned === pinned) {
5430
+ if (index === -1 || tabs[index].pinned === pinned) {
5368
5431
  return tabs;
5369
5432
  }
5370
5433
  const updated = pinned
@@ -5434,7 +5497,7 @@ class PanelViewsService {
5434
5497
  const declared = this.registry
5435
5498
  .views()
5436
5499
  .filter((view) => view.region === regionId)
5437
- .sort((a, b) => (a.order ?? 0) - (b.order ?? 0));
5500
+ .toSorted((a, b) => (a.order ?? 0) - (b.order ?? 0));
5438
5501
  return this.order.applyOrder(panelViewsContainerId(regionId), declared, (view) => view.id);
5439
5502
  }
5440
5503
  candidatesFor(regionId) {
@@ -5449,7 +5512,7 @@ class PanelViewsService {
5449
5512
  (panelRegions.has(view.region) &&
5450
5513
  (holder === null || !panelRegions.has(holder))))
5451
5514
  .map(({ view, holder }) => ({ view, here: holder === regionId }))
5452
- .sort((a, b) => Number(b.here) - Number(a.here) ||
5515
+ .toSorted((a, b) => Number(b.here) - Number(a.here) ||
5453
5516
  (a.view.order ?? 0) - (b.view.order ?? 0));
5454
5517
  }
5455
5518
  holderOf(viewId) {
@@ -5485,7 +5548,7 @@ class HiddenViewsService {
5485
5548
  ...(ngDevMode ? [{ debugName: "ids" }] : /* istanbul ignore next */ []));
5486
5549
  hidden = this.ids.asReadonly();
5487
5550
  constructor() {
5488
- void this.workspace.ready.then(() => hydrateAsync(this.store, this.storageKey(), (raw) => this.ids.set(parseHiddenViews(raw))));
5551
+ this.hydrateWhenWorkspaceReady();
5489
5552
  }
5490
5553
  isHidden(viewId) {
5491
5554
  return this.ids().has(viewId);
@@ -5509,7 +5572,7 @@ class HiddenViewsService {
5509
5572
  void this.store.set(this.storageKey(), this.serialize());
5510
5573
  }
5511
5574
  serialize() {
5512
- return JSON.stringify([...this.ids()].sort());
5575
+ return JSON.stringify([...this.ids()].toSorted((a, b) => a.localeCompare(b)));
5513
5576
  }
5514
5577
  commit(next) {
5515
5578
  this.ids.set(next);
@@ -5518,6 +5581,9 @@ class HiddenViewsService {
5518
5581
  storageKey() {
5519
5582
  return this.workspace.scopedKey(STORAGE_KEY$b);
5520
5583
  }
5584
+ hydrateWhenWorkspaceReady() {
5585
+ void this.workspace.ready.then(() => hydrateAsync(this.store, this.storageKey(), (raw) => this.ids.set(parseHiddenViews(raw))));
5586
+ }
5521
5587
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.0", ngImport: i0, type: HiddenViewsService, deps: [], target: i0.ɵɵFactoryTarget.Service });
5522
5588
  static ɵprov = i0.ɵɵngDeclareService({ minVersion: "22.0.0", version: "22.1.0", ngImport: i0, type: HiddenViewsService });
5523
5589
  }
@@ -5624,9 +5690,9 @@ function tabGaps(definition, routes, id) {
5624
5690
  }
5625
5691
  function sidebarGaps(definition, declaredPaths, id) {
5626
5692
  return Object.entries(definition.sidebars ?? {}).flatMap(([region, visible]) => {
5627
- const declared = declaredPaths(region).map((path) => path.slice(VIEW_PANE_PREFIX.length));
5693
+ const declared = new Set(declaredPaths(region).map((path) => path.slice(VIEW_PANE_PREFIX.length)));
5628
5694
  return visible
5629
- .filter((viewId) => !declared.includes(viewId))
5695
+ .filter((viewId) => !declared.has(viewId))
5630
5696
  .map((viewId) => `Workspace "${id}": sidebar view "${viewId}" is not declared for region "${region}" — the entry has no effect.`);
5631
5697
  });
5632
5698
  }
@@ -5643,7 +5709,7 @@ function canonicalState(read, shape) {
5643
5709
  }
5644
5710
  function canonicalValue(key, raw, hidden, shape) {
5645
5711
  if (key === shape.hiddenViewsKey) {
5646
- return JSON.stringify([...hidden].sort());
5712
+ return JSON.stringify([...hidden].toSorted((a, b) => a.localeCompare(b)));
5647
5713
  }
5648
5714
  if (key === shape.paneTreesKey) {
5649
5715
  return canonicalTrees(raw, hidden, shape);
@@ -5657,7 +5723,8 @@ function canonicalTrees(raw, hidden, shape) {
5657
5723
  try {
5658
5724
  const parsed = JSON.parse(raw);
5659
5725
  const out = {};
5660
- for (const dock of Object.keys(parsed ?? {}).sort()) {
5726
+ const docks = Object.keys(parsed ?? {}).toSorted((a, b) => a.localeCompare(b));
5727
+ for (const dock of docks) {
5661
5728
  const entry = normalizeDockEntry(parsed[dock]);
5662
5729
  if (entry === null) {
5663
5730
  continue;
@@ -5706,7 +5773,7 @@ function comparableTab(tab) {
5706
5773
  }
5707
5774
  function comparableNode(node) {
5708
5775
  if (node.kind === 'leaf') {
5709
- return { ...node, tabs: node.tabs.map(comparableTab) };
5776
+ return { ...node, tabs: node.tabs.map((tab) => comparableTab(tab)) };
5710
5777
  }
5711
5778
  return {
5712
5779
  ...node,
@@ -5732,7 +5799,7 @@ function baseInitials(name) {
5732
5799
  if (letters.length === 1) {
5733
5800
  return letters[0].toUpperCase();
5734
5801
  }
5735
- return (letters[0] + letters[letters.length - 1]).toUpperCase();
5802
+ return (letters[0] + (letters.at(-1) ?? '')).toUpperCase();
5736
5803
  }
5737
5804
  function* candidatesFor(name) {
5738
5805
  const base = baseInitials(name);
@@ -5741,8 +5808,8 @@ function* candidatesFor(name) {
5741
5808
  }
5742
5809
  yield base;
5743
5810
  const letters = [...(wordsOf(name)[0] ?? '')];
5744
- for (let i = 1; i < letters.length; i++) {
5745
- yield (letters[0] + letters[i]).toUpperCase();
5811
+ for (let index = 1; index < letters.length; index++) {
5812
+ yield (letters[0] + letters[index]).toUpperCase();
5746
5813
  }
5747
5814
  for (let digit = 2; digit <= LAST_RESORT_DIGITS; digit++) {
5748
5815
  yield letters[0].toUpperCase() + digit;
@@ -5752,17 +5819,22 @@ function assignWorkspaceInitials(workspaces) {
5752
5819
  const taken = new Set();
5753
5820
  const assigned = new Map();
5754
5821
  for (const workspace of workspaces) {
5755
- for (const candidate of candidatesFor(workspace.name)) {
5756
- if (taken.has(candidate)) {
5757
- continue;
5758
- }
5822
+ const candidate = firstFree(candidatesFor(workspace.name), taken);
5823
+ if (candidate !== undefined) {
5759
5824
  taken.add(candidate);
5760
5825
  assigned.set(workspace.id, candidate);
5761
- break;
5762
5826
  }
5763
5827
  }
5764
5828
  return assigned;
5765
5829
  }
5830
+ function firstFree(candidates, taken) {
5831
+ for (const candidate of candidates) {
5832
+ if (!taken.has(candidate)) {
5833
+ return candidate;
5834
+ }
5835
+ }
5836
+ return undefined;
5837
+ }
5766
5838
 
5767
5839
  const STORAGE_KEY$a = 'lw.shell.workspaces';
5768
5840
  const HIDDEN_VIEWS_KEY = 'lw.shell.hidden-views';
@@ -5849,14 +5921,16 @@ class WorkspaceService {
5849
5921
  }, /* @ts-ignore */
5850
5922
  ...(ngDevMode ? [{ debugName: "changedIds" }] : /* istanbul ignore next */ []));
5851
5923
  constructor() {
5852
- hydrateAsync(this.store, STORAGE_KEY$a, (raw) => this.list.set(parse(raw)));
5853
- this.sync.register('settings', STORAGE_KEY$a, (raw) => this.list.set(parse(raw)));
5924
+ const setList = (raw) => this.list.set(parse(raw));
5925
+ hydrateAsync(this.store, STORAGE_KEY$a, setList);
5926
+ this.sync.register('settings', STORAGE_KEY$a, setList);
5854
5927
  if (isDevMode()) {
5855
- for (const problem of auditWorkspaceDefinitions(this.definitionBatches.flat(), this.panelRegions)) {
5928
+ const all = this.definitionBatches.flat();
5929
+ for (const problem of auditWorkspaceDefinitions(all, this.panelRegions)) {
5856
5930
  console.warn(problem);
5857
5931
  }
5858
5932
  }
5859
- void this.active.ready.then(() => this.layOutAdoptedWorkspace());
5933
+ this.layOutAdoptedWorkspaceWhenReady();
5860
5934
  }
5861
5935
  async saveCurrent(name) {
5862
5936
  const baseline = await this.currentState();
@@ -5864,7 +5938,7 @@ class WorkspaceService {
5864
5938
  const origin = this.originOf(this.active.id());
5865
5939
  this.commit([
5866
5940
  ...this.list(),
5867
- { id, name, baseline, ...(origin === null ? {} : { origin }) },
5941
+ { id, name, baseline, ...(origin !== null && { origin }) },
5868
5942
  ]);
5869
5943
  this.active.set(id);
5870
5944
  this.applyState(baseline);
@@ -5976,10 +6050,8 @@ class WorkspaceService {
5976
6050
  declaredPaths: (region) => this.panelGroups.declaredPaths(region),
5977
6051
  });
5978
6052
  return {
5979
- ...(state.hiddenViews === undefined
5980
- ? {}
5981
- : { [HIDDEN_VIEWS_KEY]: state.hiddenViews }),
5982
- ...(state.trees === undefined ? {} : { [PANE_TREES_KEY]: state.trees }),
6053
+ ...(state.hiddenViews !== undefined && { [HIDDEN_VIEWS_KEY]: state.hiddenViews }),
6054
+ ...(state.trees !== undefined && { [PANE_TREES_KEY]: state.trees }),
5983
6055
  };
5984
6056
  }
5985
6057
  warnDeclarationGaps(id) {
@@ -6043,6 +6115,9 @@ class WorkspaceService {
6043
6115
  this.list.set(next);
6044
6116
  void this.store.set(STORAGE_KEY$a, JSON.stringify(next));
6045
6117
  }
6118
+ layOutAdoptedWorkspaceWhenReady() {
6119
+ void this.active.ready.then(() => this.layOutAdoptedWorkspace());
6120
+ }
6046
6121
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.0", ngImport: i0, type: WorkspaceService, deps: [], target: i0.ɵɵFactoryTarget.Service });
6047
6122
  static ɵprov = i0.ɵɵngDeclareService({ minVersion: "22.0.0", version: "22.1.0", ngImport: i0, type: WorkspaceService });
6048
6123
  }
@@ -6081,7 +6156,7 @@ class ShellRail {
6081
6156
  .filter((item) => this.railItems.regionOf(item.id, item.rail) === this.region().id)
6082
6157
  .filter((item) => this.auth.visible(item.access))
6083
6158
  .filter((item) => item.workspace !== undefined || this.commands.triggerable(item))
6084
- .sort((a, b) => (a.order ?? 0) - (b.order ?? 0)), /* @ts-ignore */
6159
+ .toSorted((a, b) => (a.order ?? 0) - (b.order ?? 0)), /* @ts-ignore */
6085
6160
  ...(ngDevMode ? [{ debugName: "registered" }] : /* istanbul ignore next */ []));
6086
6161
  items = computed(() => {
6087
6162
  const inRail = this.registered().filter((item) => this.railItems.isVisible(item.id));
@@ -6089,7 +6164,7 @@ class ShellRail {
6089
6164
  const id = this.containerId();
6090
6165
  const key = (item) => item.id;
6091
6166
  const top = this.userOrder.applyOrder(id, inRail.filter((item) => !isBottom(item)), key);
6092
- const bottom = this.userOrder.applyOrder(id, inRail.filter(isBottom), key);
6167
+ const bottom = this.userOrder.applyOrder(id, inRail.filter((item) => isBottom(item)), key);
6093
6168
  return [...top, ...bottom];
6094
6169
  }, /* @ts-ignore */
6095
6170
  ...(ngDevMode ? [{ debugName: "items" }] : /* istanbul ignore next */ []));
@@ -6234,10 +6309,11 @@ class ViewStateService {
6234
6309
  hydrateAsync(this.store, key, (raw) => value.set(parseBlob$1(raw)));
6235
6310
  let timer;
6236
6311
  const cancelPendingSave = () => {
6237
- if (timer !== undefined) {
6238
- clearTimeout(timer);
6239
- timer = undefined;
6312
+ if (timer === undefined) {
6313
+ return;
6240
6314
  }
6315
+ clearTimeout(timer);
6316
+ timer = undefined;
6241
6317
  };
6242
6318
  const save = () => {
6243
6319
  cancelPendingSave();
@@ -6275,12 +6351,12 @@ function parseRecord(viewId, raw) {
6275
6351
  }
6276
6352
  const record = parsed;
6277
6353
  const instances = Array.isArray(record.instances)
6278
- ? record.instances.filter((i) => !!i && typeof i.id === 'string' && typeof i.name === 'string')
6354
+ ? record.instances.filter((index) => !!index && typeof index.id === 'string' && typeof index.name === 'string')
6279
6355
  : [];
6280
- const withoutDefault = instances.filter((i) => i.id !== viewId);
6356
+ const withoutDefault = instances.filter((index) => index.id !== viewId);
6281
6357
  const merged = [{ id: viewId, name: '' }, ...withoutDefault];
6282
6358
  const activeId = typeof record.activeId === 'string' &&
6283
- merged.some((i) => i.id === record.activeId)
6359
+ merged.some((index) => index.id === record.activeId)
6284
6360
  ? record.activeId
6285
6361
  : viewId;
6286
6362
  return { instances: merged, activeId };
@@ -6313,7 +6389,7 @@ class ViewInstanceService {
6313
6389
  }
6314
6390
  setActive(viewId, instanceId) {
6315
6391
  const record = this.recordFor(viewId);
6316
- if (record().instances.some((i) => i.id === instanceId)) {
6392
+ if (record().instances.some((index) => index.id === instanceId)) {
6317
6393
  this.commit(viewId, { ...record(), activeId: instanceId });
6318
6394
  }
6319
6395
  }
@@ -6332,7 +6408,7 @@ class ViewInstanceService {
6332
6408
  const record = this.recordFor(viewId);
6333
6409
  this.commit(viewId, {
6334
6410
  ...record(),
6335
- instances: record().instances.map((i) => i.id === instanceId ? { ...i, name } : i),
6411
+ instances: record().instances.map((index) => index.id === instanceId ? { ...index, name } : index),
6336
6412
  });
6337
6413
  }
6338
6414
  remove(viewId, instanceId) {
@@ -6340,7 +6416,7 @@ class ViewInstanceService {
6340
6416
  return;
6341
6417
  }
6342
6418
  const record = this.recordFor(viewId);
6343
- const instances = record().instances.filter((i) => i.id !== instanceId);
6419
+ const instances = record().instances.filter((index) => index.id !== instanceId);
6344
6420
  const activeId = record().activeId === instanceId ? viewId : record().activeId;
6345
6421
  this.commit(viewId, { instances, activeId });
6346
6422
  this.viewStates.clear(instanceId);
@@ -6601,7 +6677,7 @@ class PaneChromeService {
6601
6677
  clearMinimized(dock) {
6602
6678
  const prefix = `${dock}:`;
6603
6679
  const current = this.min();
6604
- if (![...current].some((id) => id.startsWith(prefix))) {
6680
+ if ([...current].every((id) => !id.startsWith(prefix))) {
6605
6681
  return;
6606
6682
  }
6607
6683
  this.min.set(new Set([...current].filter((id) => !id.startsWith(prefix))));
@@ -6668,7 +6744,7 @@ function bakeChild(dock, spec, entry, problems, context) {
6668
6744
  return {
6669
6745
  tab: {
6670
6746
  ...containerChildTab(dock, spec, declared.surface),
6671
- ...(declared.closable === false ? { closable: false } : {}),
6747
+ ...(declared.closable === false && { closable: false }),
6672
6748
  },
6673
6749
  active: declared.active === true,
6674
6750
  };
@@ -6705,13 +6781,11 @@ class PaneContainersService {
6705
6781
  const tab = {
6706
6782
  path,
6707
6783
  instance: `${dock}::${path}`,
6708
- ...(label?.title === undefined
6709
- ? {}
6710
- : {
6711
- title: label.title,
6712
- literalTitle: label.titleIsLiteral ?? false,
6713
- }),
6714
- ...(label?.icon === undefined ? {} : { icon: label.icon }),
6784
+ ...(label?.title !== undefined && {
6785
+ title: label.title,
6786
+ literalTitle: label.titleIsLiteral ?? false,
6787
+ }),
6788
+ ...(label?.icon !== undefined && { icon: label.icon }),
6715
6789
  };
6716
6790
  this.paneTree.commit(dock, setActiveTab(insertTab(tree, target, tab), target, path));
6717
6791
  }
@@ -6737,11 +6811,11 @@ function offRouterPaneTargets(registry, auth) {
6737
6811
  const routes = registry
6738
6812
  .contentRoutes()
6739
6813
  .filter((route) => offRouterMountable(registry, auth, route.path))
6740
- .map(routeTarget);
6814
+ .map((route) => routeTarget(route));
6741
6815
  const views = registry
6742
6816
  .views()
6743
6817
  .filter((view) => auth.meets(view.access))
6744
- .map(viewTarget);
6818
+ .map((view) => viewTarget(view));
6745
6819
  return [...routes, ...views];
6746
6820
  }
6747
6821
  function containerChildTargets(registry, auth, spec) {
@@ -6750,14 +6824,14 @@ function containerChildTargets(registry, auth, spec) {
6750
6824
  .filter((child) => child.segment === undefined || isAddressable(child.segment))
6751
6825
  .map((child) => views.find((view) => view.id === child.surface))
6752
6826
  .filter((view) => view !== undefined && auth.meets(view.access))
6753
- .map(viewTarget);
6827
+ .map((view) => viewTarget(view));
6754
6828
  }
6755
6829
  function routerPaneTargets(registry, auth) {
6756
6830
  return registry
6757
6831
  .contentRoutes()
6758
6832
  .filter((route) => barePathHostableRoute(registry, route.path) !== null &&
6759
6833
  auth.meets(route.access))
6760
- .map(routeTarget);
6834
+ .map((route) => routeTarget(route));
6761
6835
  }
6762
6836
  function paneTargetEntries(targets, translate) {
6763
6837
  return targets.map((target) => ({
@@ -7126,7 +7200,7 @@ class PaneTabStrip {
7126
7200
  targetKind: 'view-tab',
7127
7201
  viewId: tab.path.slice(VIEW_PANE_PREFIX.length),
7128
7202
  region: this.contextGroup(),
7129
- ...(tab.instance ? { instance: tab.instance } : {}),
7203
+ ...(tab.instance && { instance: tab.instance }),
7130
7204
  };
7131
7205
  }
7132
7206
  return {
@@ -7180,22 +7254,22 @@ class PaneTabStrip {
7180
7254
  this.reorderTabs.emit(tabs.filter((tab) => this.canReorder(tab)).map((tab) => tab.path));
7181
7255
  }
7182
7256
  observeResize() {
7183
- const el = this.strip()?.nativeElement;
7184
- if (!el || typeof ResizeObserver === 'undefined') {
7257
+ const element = this.strip()?.nativeElement;
7258
+ if (!element || typeof ResizeObserver === 'undefined') {
7185
7259
  return;
7186
7260
  }
7187
7261
  const observer = new ResizeObserver(() => this.overflow() && this.measureOverflow());
7188
- observer.observe(el);
7262
+ observer.observe(element);
7189
7263
  this.destroyRef.onDestroy(() => observer.disconnect());
7190
7264
  }
7191
7265
  measureOverflow() {
7192
- const el = this.strip()?.nativeElement;
7193
- this.overflowing.set(!!el && el.scrollWidth - el.clientWidth > EDGE_TOLERANCE_PX);
7266
+ const element = this.strip()?.nativeElement;
7267
+ this.overflowing.set(!!element && element.scrollWidth - element.clientWidth > EDGE_TOLERANCE_PX);
7194
7268
  }
7195
7269
  revealActiveTab() {
7196
7270
  const strip = this.strip()?.nativeElement;
7197
7271
  const active = this.activeId();
7198
- const wrapper = strip?.querySelector(`[data-tab-path="${active}"]`)?.parentElement;
7272
+ const wrapper = strip?.querySelector(`[data-tab-path="${CSS.escape(active)}"]`)?.parentElement;
7199
7273
  if (!strip || !wrapper) {
7200
7274
  return;
7201
7275
  }
@@ -7400,12 +7474,14 @@ class PluginStateService {
7400
7474
  };
7401
7475
  }
7402
7476
  removePlugin(pluginId) {
7403
- for (const [storageKey, entry] of [...this.entries]) {
7404
- if (entry.pluginId === pluginId) {
7405
- this.cancelPending(entry);
7406
- entry.value.set(undefined);
7407
- this.entries.delete(storageKey);
7477
+ const snapshot = [...this.entries];
7478
+ for (const [storageKey, entry] of snapshot) {
7479
+ if (entry.pluginId !== pluginId) {
7480
+ continue;
7408
7481
  }
7482
+ this.cancelPending(entry);
7483
+ entry.value.set(undefined);
7484
+ this.entries.delete(storageKey);
7409
7485
  }
7410
7486
  this.keysByPlugin.delete(pluginId);
7411
7487
  void readStoredValue(this.store, INDEX_PREFIX + pluginId).then((raw) => {
@@ -7495,10 +7571,11 @@ class PluginStateService {
7495
7571
  entry.pending = undefined;
7496
7572
  }
7497
7573
  cancelTimer(entry) {
7498
- if (entry.timer !== undefined) {
7499
- clearTimeout(entry.timer);
7500
- entry.timer = undefined;
7574
+ if (entry.timer === undefined) {
7575
+ return;
7501
7576
  }
7577
+ clearTimeout(entry.timer);
7578
+ entry.timer = undefined;
7502
7579
  }
7503
7580
  withinLimits(pluginId, key, serialised) {
7504
7581
  const bytes = serialised.length;
@@ -7660,10 +7737,9 @@ function ownerByToken(registrations) {
7660
7737
  ...Object.keys(registration.dark ?? {}),
7661
7738
  ];
7662
7739
  for (const name of names) {
7663
- if (!known.has(name) || owners.has(name)) {
7664
- continue;
7740
+ if (known.has(name) && !owners.has(name)) {
7741
+ owners.set(name, registration);
7665
7742
  }
7666
- owners.set(name, registration);
7667
7743
  }
7668
7744
  }
7669
7745
  return owners;
@@ -7702,7 +7778,7 @@ function createPluginLayerRules() {
7702
7778
  }
7703
7779
  const element = document.createElement('style');
7704
7780
  element.dataset['lwPluginTheme'] = '';
7705
- document.head.appendChild(element);
7781
+ document.head.append(element);
7706
7782
  const sheet = element.sheet;
7707
7783
  if (!sheet) {
7708
7784
  return null;
@@ -8009,7 +8085,7 @@ class CapabilityGrantService {
8009
8085
  })),
8010
8086
  }))
8011
8087
  .filter((entry) => entry.capabilities.length > 0)
8012
- .sort((a, b) => a.pluginId.localeCompare(b.pluginId));
8088
+ .toSorted((a, b) => a.pluginId.localeCompare(b.pluginId));
8013
8089
  }, /* @ts-ignore */
8014
8090
  ...(ngDevMode ? [{ debugName: "permissions" }] : /* istanbul ignore next */ []));
8015
8091
  constructor() {
@@ -8146,7 +8222,7 @@ class IframeSurface {
8146
8222
  ...(ngDevMode ? [{ debugName: "activeTab" }] : /* istanbul ignore next */ []));
8147
8223
  restPath = computed(() => {
8148
8224
  if (!this.ownsRest) {
8149
- return undefined;
8225
+ return;
8150
8226
  }
8151
8227
  return this.hostMounted
8152
8228
  ? this.hostSub()
@@ -8177,7 +8253,9 @@ class IframeSurface {
8177
8253
  queueMicrotask(() => this.push({ ...snapshot, ...this.readResolved() }));
8178
8254
  });
8179
8255
  inject(DestroyRef).onDestroy(() => {
8180
- this.watched.forEach((entry) => entry.stop());
8256
+ for (const entry of this.watched.values()) {
8257
+ entry.stop();
8258
+ }
8181
8259
  this.watched.clear();
8182
8260
  this.visibility?.disconnect();
8183
8261
  this.connection?.destroy();
@@ -8213,12 +8291,12 @@ class IframeSurface {
8213
8291
  this.tabs.keep(this.tabRoot);
8214
8292
  }
8215
8293
  },
8216
- setDirty: (dirty) => this.dirty.set(dirty === true),
8217
- stateWatch: (key) => this.watchState(String(key)),
8218
- stateSet: (key, value) => this.watched.get(String(key))?.handle.set(value),
8219
- stateClear: (key) => this.watched.get(String(key))?.handle.clear(),
8294
+ setDirty: (dirty) => this.dirty.set(dirty),
8295
+ stateWatch: (key) => this.watchState(key),
8296
+ stateSet: (key, value) => this.watched.get(key)?.handle.set(value),
8297
+ stateClear: (key) => this.watched.get(key)?.handle.clear(),
8220
8298
  stateUnwatch: (key) => {
8221
- const name = String(key);
8299
+ const name = key;
8222
8300
  this.watched.get(name)?.stop();
8223
8301
  this.watched.delete(name);
8224
8302
  },
@@ -8228,7 +8306,9 @@ class IframeSurface {
8228
8306
  .then((remote) => {
8229
8307
  this.remote = remote;
8230
8308
  this.push({ ...this.reactiveState(), ...this.readResolved() });
8231
- this.watched.forEach((entry, key) => this.pushState(key, entry.handle.value(), entry.handle.loaded()));
8309
+ for (const [key, entry] of this.watched) {
8310
+ this.pushState(key, entry.handle.value(), entry.handle.loaded());
8311
+ }
8232
8312
  })
8233
8313
  .catch(() => undefined);
8234
8314
  }
@@ -8264,7 +8344,7 @@ class IframeSurface {
8264
8344
  return;
8265
8345
  }
8266
8346
  this.visibility = new IntersectionObserver((entries) => {
8267
- const last = entries[entries.length - 1];
8347
+ const last = entries.at(-1);
8268
8348
  if (last) {
8269
8349
  this.shown.set(last.isIntersecting);
8270
8350
  }
@@ -8287,19 +8367,15 @@ class IframeSurface {
8287
8367
  theme: this.theme.resolvedTheme(),
8288
8368
  preview: this.isPreview(),
8289
8369
  shown: this.shown(),
8290
- ...(this.instanceId ? { instanceId: this.instanceId } : {}),
8291
- ...(Object.keys(this.routeParams).length > 0
8292
- ? { params: this.routeParams }
8293
- : {}),
8294
- ...(rest === undefined ? {} : { rest }),
8295
- ...(this.sessionGranted()
8296
- ? {
8297
- session: {
8298
- authenticated: this.auth.authenticated(),
8299
- roles: this.auth.roles(),
8300
- },
8301
- }
8302
- : {}),
8370
+ ...(this.instanceId && { instanceId: this.instanceId }),
8371
+ ...(Object.keys(this.routeParams).length > 0 && { params: this.routeParams }),
8372
+ ...(rest !== undefined && { rest }),
8373
+ ...(this.sessionGranted() && {
8374
+ session: {
8375
+ authenticated: this.auth.authenticated(),
8376
+ roles: this.auth.roles(),
8377
+ },
8378
+ }),
8303
8379
  };
8304
8380
  }
8305
8381
  readResolved() {
@@ -8312,19 +8388,19 @@ class IframeSurface {
8312
8388
  return {
8313
8389
  tokens,
8314
8390
  rootFontSize: styles.fontSize,
8315
- ...(Object.keys(icons).length > 0 ? { icons } : {}),
8391
+ ...(Object.keys(icons).length > 0 && { icons }),
8316
8392
  };
8317
8393
  }
8318
8394
  navigateWithinTabRoot(path) {
8319
8395
  if (this.docked) {
8320
8396
  if (isDevMode()) {
8321
- console.warn(`[loom] a docked surface asked to navigate to "${String(path)}" — ignored. ` +
8397
+ console.warn(`[loom] a docked surface asked to navigate to "${path}" — ignored. ` +
8322
8398
  `A docked surface has no address of its own; the channel's navigate is confined to a tab ` +
8323
8399
  `root and there is none. Use ctx.navigateContent (the 'navigation' grant) instead.`);
8324
8400
  }
8325
8401
  return;
8326
8402
  }
8327
- const raw = String(path);
8403
+ const raw = path;
8328
8404
  const suffix = suffixOf(raw);
8329
8405
  const target = normalizePath(raw);
8330
8406
  if (target !== this.tabRoot && !target.startsWith(this.tabRoot + '/')) {
@@ -8393,8 +8469,8 @@ function syntheticDockedRoute(view, instanceId, params = {}) {
8393
8469
  url: [],
8394
8470
  params,
8395
8471
  data: {
8396
- ...(view.iframe !== undefined ? { iframe: view.iframe } : {}),
8397
- ...(view.pluginId ? { pluginId: view.pluginId } : {}),
8472
+ ...(view.iframe !== undefined && { iframe: view.iframe }),
8473
+ ...(view.pluginId && { pluginId: view.pluginId }),
8398
8474
  docked: true,
8399
8475
  instanceId,
8400
8476
  },
@@ -8410,13 +8486,13 @@ function syntheticRouteFor(route, path, options = {}) {
8410
8486
  url: segments.map((segment) => new UrlSegment(segment, {})),
8411
8487
  params,
8412
8488
  data: {
8413
- ...('iframe' in route ? { iframe: route.iframe } : {}),
8414
- ...('container' in route ? { container: route.container } : {}),
8415
- ...(route.pluginId ? { pluginId: route.pluginId } : {}),
8416
- ...(route.rest === true ? { rest: true } : {}),
8417
- ...(sub ? { sub } : {}),
8418
- ...(options.urlDriven ? { urlDriven: true } : {}),
8419
- ...(options.instanceId ? { instanceId: options.instanceId } : {}),
8489
+ ...('iframe' in route && { iframe: route.iframe }),
8490
+ ...('container' in route && { container: route.container }),
8491
+ ...(route.pluginId && { pluginId: route.pluginId }),
8492
+ ...(route.rest === true && { rest: true }),
8493
+ ...(sub && { sub }),
8494
+ ...(options.urlDriven && { urlDriven: true }),
8495
+ ...(options.instanceId && { instanceId: options.instanceId }),
8420
8496
  },
8421
8497
  });
8422
8498
  }
@@ -8652,14 +8728,12 @@ class ContentSecondaryPane {
8652
8728
  mountParams = computed(() => {
8653
8729
  const ctx = this.containerCtx;
8654
8730
  if (!ctx) {
8655
- return undefined;
8731
+ return;
8656
8732
  }
8657
8733
  const match = containerChildForPath(this.registry.contentRoutes(), this.registry.views(), this.path());
8658
8734
  return {
8659
8735
  ...ctx.params,
8660
- ...(match
8661
- ? paramsOfPattern(match.declaration.segment ?? '', match.segmentPath)
8662
- : {}),
8736
+ ...(match && paramsOfPattern(match.declaration.segment ?? '', match.segmentPath)),
8663
8737
  };
8664
8738
  }, /* @ts-ignore */
8665
8739
  ...(ngDevMode ? [{ debugName: "mountParams" }] : /* istanbul ignore next */ []));
@@ -8691,12 +8765,12 @@ class ContentSecondaryPane {
8691
8765
  ...(ngDevMode ? [{ debugName: "activeRoute" }] : /* istanbul ignore next */ []));
8692
8766
  iframeSurface = computed(() => {
8693
8767
  const route = this.activeRoute();
8694
- return route?.iframe !== undefined
8695
- ? {
8768
+ return route?.iframe === undefined
8769
+ ? null
8770
+ : {
8696
8771
  component: IframeSurface,
8697
8772
  injector: this.injectorFor(route, this.path(), this.surfaceKey()),
8698
- }
8699
- : null;
8773
+ };
8700
8774
  }, /* @ts-ignore */
8701
8775
  ...(ngDevMode ? [{ debugName: "iframeSurface" }] : /* istanbul ignore next */ []));
8702
8776
  surface = computed(() => {
@@ -9148,11 +9222,11 @@ class PaneView {
9148
9222
  ];
9149
9223
  }
9150
9224
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.0", ngImport: i0, type: PaneView, deps: [], target: i0.ɵɵFactoryTarget.Component });
9151
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.1.0", type: PaneView, isStandalone: true, selector: "lw-pane-view", inputs: { dock: { classPropertyName: "dock", publicName: "dock", isSignal: true, isRequired: true, transformFunction: null }, leaf: { classPropertyName: "leaf", publicName: "leaf", isSignal: true, isRequired: true, transformFunction: null }, options: { classPropertyName: "options", publicName: "options", isSignal: true, isRequired: false, transformFunction: null } }, host: { classAttribute: "flex min-h-0 min-w-0 flex-1 flex-col" }, ngImport: i0, template: "<lw-pane-tab-strip\n class=\"shrink-0\"\n [variant]=\"options().variant\"\n [tabs]=\"stripTabs()\"\n [activeId]=\"activeTabId()\"\n [urlDriven]=\"pointed()\"\n [source]=\"source()\"\n [reorderable]=\"tabsReorderable()\"\n [draggable]=\"tabsDraggable()\"\n [acceptsTabs]=\"acceptsTabs()\"\n [canAddTab]=\"canAddTab()\"\n [viewContextMenuSlot]=\"viewContextMenu()\"\n [contextGroup]=\"dock()\"\n [paneActions]=\"paneActions\"\n (selectTab)=\"onSelectTab($event)\"\n (escalate)=\"onEscalate($event)\"\n (closeTab)=\"onCloseTab($event)\"\n (unpinTab)=\"onUnpinTab($event)\"\n (addTab)=\"openPicker($event)\"\n (reorderTabs)=\"onReorderTabs($event)\"\n/>\n<ng-template #paneActions>\n <lw-pane-toolbar\n [canSplitRight]=\"canSplitRight()\"\n [canSplitDown]=\"canSplitDown()\"\n [canMinimize]=\"canMinimize()\"\n [canMaximize]=\"canMaximize()\"\n [maximized]=\"maximized()\"\n [canClose]=\"canClose()\"\n [closeLabel]=\"options().closeLabel\"\n (splitRight)=\"splitPane('row')\"\n (splitDown)=\"splitPane('column')\"\n (minimize)=\"minimize()\"\n (toggleMaximize)=\"toggleMaximize()\"\n (closePane)=\"closePane()\"\n />\n</ng-template>\n\n<div class=\"flex min-h-0 min-w-0 flex-1 flex-col\" (pointerdown)=\"onBodyPointerDown()\">\n @if (awaitingContent()) {\n <div\n class=\"flex h-full items-center justify-center p-6 text-center text-sm text-content-faint\"\n data-testid=\"pane-awaiting-content\"\n role=\"status\"\n >\n {{ 'content.split.awaiting' | transloco }}\n </div>\n } @else {\n <lw-content-secondary-pane\n class=\"min-h-0 min-w-0 flex-1\"\n [path]=\"path()\"\n [instanceId]=\"instanceId()\"\n [variant]=\"options().body\"\n [retentionScope]=\"retentionScope()\"\n (instanceReleased)=\"releaseTabInstance()\"\n />\n }\n</div>\n", dependencies: [{ kind: "component", type: ContentSecondaryPane, selector: "lw-content-secondary-pane", inputs: ["path", "variant", "instanceId", "retentionScope"], outputs: ["instanceReleased"] }, { kind: "component", type: PaneTabStrip, selector: "lw-pane-tab-strip", inputs: ["tabs", "activeId", "reorderable", "draggable", "acceptsTabs", "source", "variant", "urlDriven", "contextMenuSlot", "viewContextMenuSlot", "contextGroup", "overflow", "canAddTab", "paneActions"], outputs: ["selectTab", "escalate", "closeTab", "unpinTab", "reorderTabs", "runAction", "addTab", "revealRequest"] }, { kind: "component", type: PaneToolbar, selector: "lw-pane-toolbar", inputs: ["canNewTab", "canSplitRight", "canSplitDown", "canMinimize", "canMaximize", "maximized", "canClose", "closeLabel", "newTabTestId", "splitRightTestId", "splitDownTestId"], outputs: ["newTab", "splitRight", "splitDown", "minimize", "toggleMaximize", "closePane"] }, { kind: "pipe", type: TranslocoPipe, name: "transloco" }] });
9225
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.1.0", type: PaneView, isStandalone: true, selector: "lw-pane-view", inputs: { dock: { classPropertyName: "dock", publicName: "dock", isSignal: true, isRequired: true, transformFunction: null }, leaf: { classPropertyName: "leaf", publicName: "leaf", isSignal: true, isRequired: true, transformFunction: null }, options: { classPropertyName: "options", publicName: "options", isSignal: true, isRequired: false, transformFunction: null } }, host: { classAttribute: "flex min-h-0 min-w-0 flex-1 flex-col" }, ngImport: i0, template: "<lw-pane-tab-strip\n class=\"shrink-0\"\n [variant]=\"options().variant\"\n [tabs]=\"stripTabs()\"\n [activeId]=\"activeTabId()\"\n [urlDriven]=\"pointed()\"\n [source]=\"source()\"\n [reorderable]=\"tabsReorderable()\"\n [draggable]=\"tabsDraggable()\"\n [acceptsTabs]=\"acceptsTabs()\"\n [canAddTab]=\"canAddTab()\"\n [viewContextMenuSlot]=\"viewContextMenu()\"\n [contextGroup]=\"dock()\"\n [paneActions]=\"paneActions\"\n (selectTab)=\"onSelectTab($event)\"\n (escalate)=\"onEscalate($event)\"\n (closeTab)=\"onCloseTab($event)\"\n (unpinTab)=\"onUnpinTab($event)\"\n (addTab)=\"openPicker($event)\"\n (reorderTabs)=\"onReorderTabs($event)\"\n/>\n<ng-template #paneActions>\n <lw-pane-toolbar\n [canSplitRight]=\"canSplitRight()\"\n [canSplitDown]=\"canSplitDown()\"\n [canMinimize]=\"canMinimize()\"\n [canMaximize]=\"canMaximize()\"\n [maximized]=\"maximized()\"\n [canClose]=\"canClose()\"\n [closeLabel]=\"options().closeLabel\"\n (splitRight)=\"splitPane('row')\"\n (splitDown)=\"splitPane('column')\"\n (minimize)=\"minimize()\"\n (toggleMaximize)=\"toggleMaximize()\"\n (closePane)=\"closePane()\"\n />\n</ng-template>\n\n<div class=\"flex min-h-0 min-w-0 flex-1 flex-col\" (pointerdown)=\"onBodyPointerDown()\">\n @if (awaitingContent()) {\n <output\n class=\"flex h-full items-center justify-center p-6 text-center text-sm text-content-faint\"\n data-testid=\"pane-awaiting-content\"\n >\n {{ 'content.split.awaiting' | transloco }}\n </output>\n } @else {\n <lw-content-secondary-pane\n class=\"min-h-0 min-w-0 flex-1\"\n [path]=\"path()\"\n [instanceId]=\"instanceId()\"\n [variant]=\"options().body\"\n [retentionScope]=\"retentionScope()\"\n (instanceReleased)=\"releaseTabInstance()\"\n />\n }\n</div>\n", dependencies: [{ kind: "component", type: ContentSecondaryPane, selector: "lw-content-secondary-pane", inputs: ["path", "variant", "instanceId", "retentionScope"], outputs: ["instanceReleased"] }, { kind: "component", type: PaneTabStrip, selector: "lw-pane-tab-strip", inputs: ["tabs", "activeId", "reorderable", "draggable", "acceptsTabs", "source", "variant", "urlDriven", "contextMenuSlot", "viewContextMenuSlot", "contextGroup", "overflow", "canAddTab", "paneActions"], outputs: ["selectTab", "escalate", "closeTab", "unpinTab", "reorderTabs", "runAction", "addTab", "revealRequest"] }, { kind: "component", type: PaneToolbar, selector: "lw-pane-toolbar", inputs: ["canNewTab", "canSplitRight", "canSplitDown", "canMinimize", "canMaximize", "maximized", "canClose", "closeLabel", "newTabTestId", "splitRightTestId", "splitDownTestId"], outputs: ["newTab", "splitRight", "splitDown", "minimize", "toggleMaximize", "closePane"] }, { kind: "pipe", type: TranslocoPipe, name: "transloco" }] });
9152
9226
  }
9153
9227
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.0", ngImport: i0, type: PaneView, decorators: [{
9154
9228
  type: Component,
9155
- args: [{ selector: 'lw-pane-view', imports: [ContentSecondaryPane, PaneTabStrip, PaneToolbar, TranslocoPipe], schemas: [CUSTOM_ELEMENTS_SCHEMA], host: { class: 'flex min-h-0 min-w-0 flex-1 flex-col' }, template: "<lw-pane-tab-strip\n class=\"shrink-0\"\n [variant]=\"options().variant\"\n [tabs]=\"stripTabs()\"\n [activeId]=\"activeTabId()\"\n [urlDriven]=\"pointed()\"\n [source]=\"source()\"\n [reorderable]=\"tabsReorderable()\"\n [draggable]=\"tabsDraggable()\"\n [acceptsTabs]=\"acceptsTabs()\"\n [canAddTab]=\"canAddTab()\"\n [viewContextMenuSlot]=\"viewContextMenu()\"\n [contextGroup]=\"dock()\"\n [paneActions]=\"paneActions\"\n (selectTab)=\"onSelectTab($event)\"\n (escalate)=\"onEscalate($event)\"\n (closeTab)=\"onCloseTab($event)\"\n (unpinTab)=\"onUnpinTab($event)\"\n (addTab)=\"openPicker($event)\"\n (reorderTabs)=\"onReorderTabs($event)\"\n/>\n<ng-template #paneActions>\n <lw-pane-toolbar\n [canSplitRight]=\"canSplitRight()\"\n [canSplitDown]=\"canSplitDown()\"\n [canMinimize]=\"canMinimize()\"\n [canMaximize]=\"canMaximize()\"\n [maximized]=\"maximized()\"\n [canClose]=\"canClose()\"\n [closeLabel]=\"options().closeLabel\"\n (splitRight)=\"splitPane('row')\"\n (splitDown)=\"splitPane('column')\"\n (minimize)=\"minimize()\"\n (toggleMaximize)=\"toggleMaximize()\"\n (closePane)=\"closePane()\"\n />\n</ng-template>\n\n<div class=\"flex min-h-0 min-w-0 flex-1 flex-col\" (pointerdown)=\"onBodyPointerDown()\">\n @if (awaitingContent()) {\n <div\n class=\"flex h-full items-center justify-center p-6 text-center text-sm text-content-faint\"\n data-testid=\"pane-awaiting-content\"\n role=\"status\"\n >\n {{ 'content.split.awaiting' | transloco }}\n </div>\n } @else {\n <lw-content-secondary-pane\n class=\"min-h-0 min-w-0 flex-1\"\n [path]=\"path()\"\n [instanceId]=\"instanceId()\"\n [variant]=\"options().body\"\n [retentionScope]=\"retentionScope()\"\n (instanceReleased)=\"releaseTabInstance()\"\n />\n }\n</div>\n" }]
9229
+ args: [{ selector: 'lw-pane-view', imports: [ContentSecondaryPane, PaneTabStrip, PaneToolbar, TranslocoPipe], schemas: [CUSTOM_ELEMENTS_SCHEMA], host: { class: 'flex min-h-0 min-w-0 flex-1 flex-col' }, template: "<lw-pane-tab-strip\n class=\"shrink-0\"\n [variant]=\"options().variant\"\n [tabs]=\"stripTabs()\"\n [activeId]=\"activeTabId()\"\n [urlDriven]=\"pointed()\"\n [source]=\"source()\"\n [reorderable]=\"tabsReorderable()\"\n [draggable]=\"tabsDraggable()\"\n [acceptsTabs]=\"acceptsTabs()\"\n [canAddTab]=\"canAddTab()\"\n [viewContextMenuSlot]=\"viewContextMenu()\"\n [contextGroup]=\"dock()\"\n [paneActions]=\"paneActions\"\n (selectTab)=\"onSelectTab($event)\"\n (escalate)=\"onEscalate($event)\"\n (closeTab)=\"onCloseTab($event)\"\n (unpinTab)=\"onUnpinTab($event)\"\n (addTab)=\"openPicker($event)\"\n (reorderTabs)=\"onReorderTabs($event)\"\n/>\n<ng-template #paneActions>\n <lw-pane-toolbar\n [canSplitRight]=\"canSplitRight()\"\n [canSplitDown]=\"canSplitDown()\"\n [canMinimize]=\"canMinimize()\"\n [canMaximize]=\"canMaximize()\"\n [maximized]=\"maximized()\"\n [canClose]=\"canClose()\"\n [closeLabel]=\"options().closeLabel\"\n (splitRight)=\"splitPane('row')\"\n (splitDown)=\"splitPane('column')\"\n (minimize)=\"minimize()\"\n (toggleMaximize)=\"toggleMaximize()\"\n (closePane)=\"closePane()\"\n />\n</ng-template>\n\n<div class=\"flex min-h-0 min-w-0 flex-1 flex-col\" (pointerdown)=\"onBodyPointerDown()\">\n @if (awaitingContent()) {\n <output\n class=\"flex h-full items-center justify-center p-6 text-center text-sm text-content-faint\"\n data-testid=\"pane-awaiting-content\"\n >\n {{ 'content.split.awaiting' | transloco }}\n </output>\n } @else {\n <lw-content-secondary-pane\n class=\"min-h-0 min-w-0 flex-1\"\n [path]=\"path()\"\n [instanceId]=\"instanceId()\"\n [variant]=\"options().body\"\n [retentionScope]=\"retentionScope()\"\n (instanceReleased)=\"releaseTabInstance()\"\n />\n }\n</div>\n" }]
9156
9230
  }], propDecorators: { dock: [{ type: i0.Input, args: [{ isSignal: true, alias: "dock", required: true }] }], leaf: [{ type: i0.Input, args: [{ isSignal: true, alias: "leaf", required: true }] }], options: [{ type: i0.Input, args: [{ isSignal: true, alias: "options", required: false }] }] } });
9157
9231
 
9158
9232
  class PaneDropZones {
@@ -9190,13 +9264,12 @@ class PaneDropZones {
9190
9264
  ...(ngDevMode ? [{ debugName: "accepts" }] : /* istanbul ignore next */ []));
9191
9265
  constructor() {
9192
9266
  effect((onCleanup) => {
9193
- const ids = this.fills()
9194
- ? this.accepts()
9195
- ? [this.fillZoneId()]
9196
- : []
9197
- : this.edges().map((edge) => this.zoneId(edge));
9198
- const disposers = ids.map((id) => this.drag.registerZone(id));
9199
- onCleanup(() => disposers.forEach((dispose) => dispose()));
9267
+ const disposers = this.zoneIds().map((id) => this.drag.registerZone(id));
9268
+ onCleanup(() => {
9269
+ for (const dispose of disposers) {
9270
+ dispose();
9271
+ }
9272
+ });
9200
9273
  });
9201
9274
  }
9202
9275
  has(edge) {
@@ -9232,8 +9305,14 @@ class PaneDropZones {
9232
9305
  this.paneMove.moveToEdge(source, String(event.item.data ?? ''), { dock: this.dock(), paneId: this.paneId() }, edge);
9233
9306
  }
9234
9307
  }
9308
+ zoneIds() {
9309
+ if (!this.fills()) {
9310
+ return this.edges().map((edge) => this.zoneId(edge));
9311
+ }
9312
+ return this.accepts() ? [this.fillZoneId()] : [];
9313
+ }
9235
9314
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.0", ngImport: i0, type: PaneDropZones, deps: [], target: i0.ɵɵFactoryTarget.Component });
9236
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.1.0", type: PaneDropZones, isStandalone: true, selector: "lw-pane-drop-zones", inputs: { dock: { classPropertyName: "dock", publicName: "dock", isSignal: true, isRequired: true, transformFunction: null }, paneId: { classPropertyName: "paneId", publicName: "paneId", isSignal: true, isRequired: true, transformFunction: null } }, host: { properties: { "class.opacity-0": "!active()", "class.pointer-events-none": "!active()", "style.grid-template-columns": "\"1fr 2fr 1fr\"", "style.grid-template-rows": "\"1fr 2fr 1fr\"", "attr.aria-hidden": "true" }, classAttribute: "absolute inset-0 z-20 grid transition-opacity" }, ngImport: i0, template: "@if (fills()) {\n @if (accepts()) {\n <div\n class=\"lw-pane-drop-zone col-span-3 row-span-3 col-start-1 row-start-1\"\n cdkDropList\n [cdkDropListEnterPredicate]=\"enterPredicate\"\n [id]=\"fillZoneId()\"\n (cdkDropListDropped)=\"onFill($event)\"\n >\n <div class=\"lw-pane-drop-preview lw-pane-drop-preview--fill\"></div>\n </div>\n }\n} @else {\n @if (has('left')) {\n <div\n class=\"lw-pane-drop-zone col-start-1 row-span-3 row-start-1\"\n cdkDropList\n [cdkDropListEnterPredicate]=\"enterPredicate\"\n [id]=\"zoneId('left')\"\n (cdkDropListDropped)=\"onDrop('left', $event)\"\n >\n <div class=\"lw-pane-drop-preview lw-pane-drop-preview--left\"></div>\n </div>\n }\n @if (has('right')) {\n <div\n class=\"lw-pane-drop-zone col-start-3 row-span-3 row-start-1\"\n cdkDropList\n [cdkDropListEnterPredicate]=\"enterPredicate\"\n [id]=\"zoneId('right')\"\n (cdkDropListDropped)=\"onDrop('right', $event)\"\n >\n <div class=\"lw-pane-drop-preview lw-pane-drop-preview--right\"></div>\n </div>\n }\n @if (has('top')) {\n <div\n class=\"lw-pane-drop-zone col-start-2 row-start-1\"\n cdkDropList\n [cdkDropListEnterPredicate]=\"enterPredicate\"\n [id]=\"zoneId('top')\"\n (cdkDropListDropped)=\"onDrop('top', $event)\"\n >\n <div class=\"lw-pane-drop-preview lw-pane-drop-preview--top\"></div>\n </div>\n }\n @if (has('bottom')) {\n <div\n class=\"lw-pane-drop-zone col-start-2 row-start-3\"\n cdkDropList\n [cdkDropListEnterPredicate]=\"enterPredicate\"\n [id]=\"zoneId('bottom')\"\n (cdkDropListDropped)=\"onDrop('bottom', $event)\"\n >\n <div class=\"lw-pane-drop-preview lw-pane-drop-preview--bottom\"></div>\n </div>\n }\n}\n", dependencies: [{ kind: "directive", type: CdkDropList, selector: "[cdkDropList], cdk-drop-list", inputs: ["cdkDropListConnectedTo", "cdkDropListData", "cdkDropListOrientation", "id", "cdkDropListLockAxis", "cdkDropListDisabled", "cdkDropListSortingDisabled", "cdkDropListEnterPredicate", "cdkDropListSortPredicate", "cdkDropListAutoScrollDisabled", "cdkDropListAutoScrollStep", "cdkDropListElementContainer", "cdkDropListHasAnchor"], outputs: ["cdkDropListDropped", "cdkDropListEntered", "cdkDropListExited", "cdkDropListSorted"], exportAs: ["cdkDropList"] }] });
9315
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.1.0", type: PaneDropZones, isStandalone: true, selector: "lw-pane-drop-zones", inputs: { dock: { classPropertyName: "dock", publicName: "dock", isSignal: true, isRequired: true, transformFunction: null }, paneId: { classPropertyName: "paneId", publicName: "paneId", isSignal: true, isRequired: true, transformFunction: null } }, host: { properties: { "class.opacity-0": "!active()", "class.pointer-events-none": "!active()", "style.grid-template-columns": "\"1fr 2fr 1fr\"", "style.grid-template-rows": "\"1fr 2fr 1fr\"", "attr.aria-hidden": "true" }, classAttribute: "absolute inset-0 z-20 grid transition-opacity" }, ngImport: i0, template: "@if (fills()) {\n @if (accepts()) {\n <div\n class=\"lw-pane-drop-zone col-span-3 col-start-1 row-span-3 row-start-1\"\n cdkDropList\n [cdkDropListEnterPredicate]=\"enterPredicate\"\n [id]=\"fillZoneId()\"\n (cdkDropListDropped)=\"onFill($event)\"\n >\n <div class=\"lw-pane-drop-preview lw-pane-drop-preview--fill\"></div>\n </div>\n }\n} @else {\n @if (has('left')) {\n <div\n class=\"lw-pane-drop-zone col-start-1 row-span-3 row-start-1\"\n cdkDropList\n [cdkDropListEnterPredicate]=\"enterPredicate\"\n [id]=\"zoneId('left')\"\n (cdkDropListDropped)=\"onDrop('left', $event)\"\n >\n <div class=\"lw-pane-drop-preview lw-pane-drop-preview--left\"></div>\n </div>\n }\n @if (has('right')) {\n <div\n class=\"lw-pane-drop-zone col-start-3 row-span-3 row-start-1\"\n cdkDropList\n [cdkDropListEnterPredicate]=\"enterPredicate\"\n [id]=\"zoneId('right')\"\n (cdkDropListDropped)=\"onDrop('right', $event)\"\n >\n <div class=\"lw-pane-drop-preview lw-pane-drop-preview--right\"></div>\n </div>\n }\n @if (has('top')) {\n <div\n class=\"lw-pane-drop-zone col-start-2 row-start-1\"\n cdkDropList\n [cdkDropListEnterPredicate]=\"enterPredicate\"\n [id]=\"zoneId('top')\"\n (cdkDropListDropped)=\"onDrop('top', $event)\"\n >\n <div class=\"lw-pane-drop-preview lw-pane-drop-preview--top\"></div>\n </div>\n }\n @if (has('bottom')) {\n <div\n class=\"lw-pane-drop-zone col-start-2 row-start-3\"\n cdkDropList\n [cdkDropListEnterPredicate]=\"enterPredicate\"\n [id]=\"zoneId('bottom')\"\n (cdkDropListDropped)=\"onDrop('bottom', $event)\"\n >\n <div class=\"lw-pane-drop-preview lw-pane-drop-preview--bottom\"></div>\n </div>\n }\n}\n", dependencies: [{ kind: "directive", type: CdkDropList, selector: "[cdkDropList], cdk-drop-list", inputs: ["cdkDropListConnectedTo", "cdkDropListData", "cdkDropListOrientation", "id", "cdkDropListLockAxis", "cdkDropListDisabled", "cdkDropListSortingDisabled", "cdkDropListEnterPredicate", "cdkDropListSortPredicate", "cdkDropListAutoScrollDisabled", "cdkDropListAutoScrollStep", "cdkDropListElementContainer", "cdkDropListHasAnchor"], outputs: ["cdkDropListDropped", "cdkDropListEntered", "cdkDropListExited", "cdkDropListSorted"], exportAs: ["cdkDropList"] }] });
9237
9316
  }
9238
9317
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.0", ngImport: i0, type: PaneDropZones, decorators: [{
9239
9318
  type: Component,
@@ -9244,7 +9323,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.0", ngImpor
9244
9323
  '[style.grid-template-columns]': '"1fr 2fr 1fr"',
9245
9324
  '[style.grid-template-rows]': '"1fr 2fr 1fr"',
9246
9325
  '[attr.aria-hidden]': 'true',
9247
- }, template: "@if (fills()) {\n @if (accepts()) {\n <div\n class=\"lw-pane-drop-zone col-span-3 row-span-3 col-start-1 row-start-1\"\n cdkDropList\n [cdkDropListEnterPredicate]=\"enterPredicate\"\n [id]=\"fillZoneId()\"\n (cdkDropListDropped)=\"onFill($event)\"\n >\n <div class=\"lw-pane-drop-preview lw-pane-drop-preview--fill\"></div>\n </div>\n }\n} @else {\n @if (has('left')) {\n <div\n class=\"lw-pane-drop-zone col-start-1 row-span-3 row-start-1\"\n cdkDropList\n [cdkDropListEnterPredicate]=\"enterPredicate\"\n [id]=\"zoneId('left')\"\n (cdkDropListDropped)=\"onDrop('left', $event)\"\n >\n <div class=\"lw-pane-drop-preview lw-pane-drop-preview--left\"></div>\n </div>\n }\n @if (has('right')) {\n <div\n class=\"lw-pane-drop-zone col-start-3 row-span-3 row-start-1\"\n cdkDropList\n [cdkDropListEnterPredicate]=\"enterPredicate\"\n [id]=\"zoneId('right')\"\n (cdkDropListDropped)=\"onDrop('right', $event)\"\n >\n <div class=\"lw-pane-drop-preview lw-pane-drop-preview--right\"></div>\n </div>\n }\n @if (has('top')) {\n <div\n class=\"lw-pane-drop-zone col-start-2 row-start-1\"\n cdkDropList\n [cdkDropListEnterPredicate]=\"enterPredicate\"\n [id]=\"zoneId('top')\"\n (cdkDropListDropped)=\"onDrop('top', $event)\"\n >\n <div class=\"lw-pane-drop-preview lw-pane-drop-preview--top\"></div>\n </div>\n }\n @if (has('bottom')) {\n <div\n class=\"lw-pane-drop-zone col-start-2 row-start-3\"\n cdkDropList\n [cdkDropListEnterPredicate]=\"enterPredicate\"\n [id]=\"zoneId('bottom')\"\n (cdkDropListDropped)=\"onDrop('bottom', $event)\"\n >\n <div class=\"lw-pane-drop-preview lw-pane-drop-preview--bottom\"></div>\n </div>\n }\n}\n" }]
9326
+ }, template: "@if (fills()) {\n @if (accepts()) {\n <div\n class=\"lw-pane-drop-zone col-span-3 col-start-1 row-span-3 row-start-1\"\n cdkDropList\n [cdkDropListEnterPredicate]=\"enterPredicate\"\n [id]=\"fillZoneId()\"\n (cdkDropListDropped)=\"onFill($event)\"\n >\n <div class=\"lw-pane-drop-preview lw-pane-drop-preview--fill\"></div>\n </div>\n }\n} @else {\n @if (has('left')) {\n <div\n class=\"lw-pane-drop-zone col-start-1 row-span-3 row-start-1\"\n cdkDropList\n [cdkDropListEnterPredicate]=\"enterPredicate\"\n [id]=\"zoneId('left')\"\n (cdkDropListDropped)=\"onDrop('left', $event)\"\n >\n <div class=\"lw-pane-drop-preview lw-pane-drop-preview--left\"></div>\n </div>\n }\n @if (has('right')) {\n <div\n class=\"lw-pane-drop-zone col-start-3 row-span-3 row-start-1\"\n cdkDropList\n [cdkDropListEnterPredicate]=\"enterPredicate\"\n [id]=\"zoneId('right')\"\n (cdkDropListDropped)=\"onDrop('right', $event)\"\n >\n <div class=\"lw-pane-drop-preview lw-pane-drop-preview--right\"></div>\n </div>\n }\n @if (has('top')) {\n <div\n class=\"lw-pane-drop-zone col-start-2 row-start-1\"\n cdkDropList\n [cdkDropListEnterPredicate]=\"enterPredicate\"\n [id]=\"zoneId('top')\"\n (cdkDropListDropped)=\"onDrop('top', $event)\"\n >\n <div class=\"lw-pane-drop-preview lw-pane-drop-preview--top\"></div>\n </div>\n }\n @if (has('bottom')) {\n <div\n class=\"lw-pane-drop-zone col-start-2 row-start-3\"\n cdkDropList\n [cdkDropListEnterPredicate]=\"enterPredicate\"\n [id]=\"zoneId('bottom')\"\n (cdkDropListDropped)=\"onDrop('bottom', $event)\"\n >\n <div class=\"lw-pane-drop-preview lw-pane-drop-preview--bottom\"></div>\n </div>\n }\n}\n" }]
9248
9327
  }], ctorParameters: () => [], propDecorators: { dock: [{ type: i0.Input, args: [{ isSignal: true, alias: "dock", required: true }] }], paneId: [{ type: i0.Input, args: [{ isSignal: true, alias: "paneId", required: true }] }] } });
9249
9328
 
9250
9329
  class PaneMinimizedStrip {
@@ -9317,18 +9396,18 @@ class PaneSplitHandle {
9317
9396
  return;
9318
9397
  }
9319
9398
  const rect = parent.getBoundingClientRect();
9320
- const el = this.host.nativeElement;
9321
- el.setPointerCapture(event.pointerId);
9399
+ const element = this.host.nativeElement;
9400
+ element.setPointerCapture(event.pointerId);
9322
9401
  event.preventDefault();
9323
9402
  const move = (e) => this.ratioStream.emit(this.fraction(e, rect));
9324
9403
  const up = (e) => {
9325
- el.releasePointerCapture(e.pointerId);
9326
- el.removeEventListener('pointermove', move);
9327
- el.removeEventListener('pointerup', up);
9404
+ element.releasePointerCapture(e.pointerId);
9405
+ element.removeEventListener('pointermove', move);
9406
+ element.removeEventListener('pointerup', up);
9328
9407
  this.ratioCommit.emit();
9329
9408
  };
9330
- el.addEventListener('pointermove', move);
9331
- el.addEventListener('pointerup', up);
9409
+ element.addEventListener('pointermove', move);
9410
+ element.addEventListener('pointerup', up);
9332
9411
  }
9333
9412
  onKeydown(event) {
9334
9413
  const step = event.shiftKey ? STEP_COARSE$1 : STEP$1;
@@ -9635,11 +9714,12 @@ function parseWidths(raw) {
9635
9714
  }
9636
9715
  const result = {};
9637
9716
  for (const [key, value] of Object.entries(parsed)) {
9638
- if (typeof value === 'number' && Number.isFinite(value)) {
9639
- const clamped = clampWidth(value);
9640
- if (clamped !== DEFAULT_PANEL_WIDTH) {
9641
- result[key] = clamped;
9642
- }
9717
+ if (!(typeof value === 'number' && Number.isFinite(value))) {
9718
+ continue;
9719
+ }
9720
+ const clamped = clampWidth(value);
9721
+ if (clamped !== DEFAULT_PANEL_WIDTH) {
9722
+ result[key] = clamped;
9643
9723
  }
9644
9724
  }
9645
9725
  return result;
@@ -9742,20 +9822,25 @@ class PanelSplitter {
9742
9822
  const step = (event.shiftKey ? STEP_COARSE : STEP) * this.edgeSign();
9743
9823
  let next;
9744
9824
  switch (event.key) {
9745
- case 'ArrowRight':
9825
+ case 'ArrowRight': {
9746
9826
  next = this.width() + step;
9747
9827
  break;
9748
- case 'ArrowLeft':
9828
+ }
9829
+ case 'ArrowLeft': {
9749
9830
  next = this.width() - step;
9750
9831
  break;
9751
- case 'Home':
9832
+ }
9833
+ case 'Home': {
9752
9834
  next = this.size.minWidth;
9753
9835
  break;
9754
- case 'End':
9836
+ }
9837
+ case 'End': {
9755
9838
  next = this.size.maxWidth;
9756
9839
  break;
9757
- default:
9840
+ }
9841
+ default: {
9758
9842
  return;
9843
+ }
9759
9844
  }
9760
9845
  event.preventDefault();
9761
9846
  this.size.setWidth(this.regionId(), next);
@@ -9812,7 +9897,7 @@ class ShellPanel {
9812
9897
  activeView = computed(() => {
9813
9898
  const path = this.activePath();
9814
9899
  if (!path?.startsWith(VIEW_PANE_PREFIX)) {
9815
- return undefined;
9900
+ return;
9816
9901
  }
9817
9902
  return viewForPanePath(this.registry.views(), path);
9818
9903
  }, /* @ts-ignore */
@@ -9826,7 +9911,7 @@ class ShellPanel {
9826
9911
  ...(ngDevMode ? [{ debugName: "activeContentPath" }] : /* istanbul ignore next */ []));
9827
9912
  actions = computed(() => [...(this.activeView()?.actions ?? [])]
9828
9913
  .filter((action) => this.auth.visible(action.access))
9829
- .sort((a, b) => (a.order ?? 0) - (b.order ?? 0)), /* @ts-ignore */
9914
+ .toSorted((a, b) => (a.order ?? 0) - (b.order ?? 0)), /* @ts-ignore */
9830
9915
  ...(ngDevMode ? [{ debugName: "actions" }] : /* istanbul ignore next */ []));
9831
9916
  panelPaneOptions = PANEL_PANE_OPTIONS;
9832
9917
  primaryScope = computed(() => paneRetentionScope(this.region().id, this.paneTree.primaryId(this.region().id)), /* @ts-ignore */
@@ -10120,10 +10205,14 @@ function registerTabContextMenu(registry, tabs, paneMove, popout, shell) {
10120
10205
  },
10121
10206
  ];
10122
10207
  const registered = new Set(commands.map((command) => command.id));
10123
- commands.forEach((command) => registry.addCommand({ ...command, paletteHidden: true }));
10124
- items
10125
- .filter((item) => item.command !== undefined && registered.has(item.command))
10126
- .forEach((item) => registry.addMenuItem(item));
10208
+ for (const command of commands) {
10209
+ registry.addCommand({ ...command, paletteHidden: true });
10210
+ }
10211
+ for (const item of items) {
10212
+ if (item.command !== undefined && registered.has(item.command)) {
10213
+ registry.addMenuItem(item);
10214
+ }
10215
+ }
10127
10216
  }
10128
10217
 
10129
10218
  class ContentArea {
@@ -10329,7 +10418,7 @@ class ContentGrid {
10329
10418
  }, /* @ts-ignore */
10330
10419
  ...(ngDevMode ? [{ debugName: "tree" }] : /* istanbul ignore next */ []));
10331
10420
  constructor() {
10332
- const doc = inject(DOCUMENT);
10421
+ const document_ = inject(DOCUMENT);
10333
10422
  const onKeydown = (event) => {
10334
10423
  if (event.key === 'Escape') {
10335
10424
  this.chrome.restore();
@@ -10339,8 +10428,8 @@ class ContentGrid {
10339
10428
  if (!this.maximized()) {
10340
10429
  return;
10341
10430
  }
10342
- doc.addEventListener('keydown', onKeydown);
10343
- onCleanup(() => doc.removeEventListener('keydown', onKeydown));
10431
+ document_.addEventListener('keydown', onKeydown);
10432
+ onCleanup(() => document_.removeEventListener('keydown', onKeydown));
10344
10433
  });
10345
10434
  effect(() => {
10346
10435
  if (!this.layout.isSplit(CONTENT_DOCK)) {
@@ -10513,8 +10602,8 @@ class DialogOutlet {
10513
10602
  if (!this.dialogs().length || !panels.length) {
10514
10603
  return;
10515
10604
  }
10516
- const top = panels[panels.length - 1].nativeElement;
10517
- if (top.contains(this.document.activeElement)) {
10605
+ const top = panels.at(-1)?.nativeElement;
10606
+ if (!top || top.contains(this.document.activeElement)) {
10518
10607
  return;
10519
10608
  }
10520
10609
  (top.querySelector('[data-lw-autofocus]') ?? top).focus();
@@ -10543,10 +10632,10 @@ class DialogOutlet {
10543
10632
  return;
10544
10633
  }
10545
10634
  const first = focusables[0];
10546
- const last = focusables[focusables.length - 1];
10635
+ const last = focusables.at(-1);
10547
10636
  const active = this.document.activeElement;
10548
10637
  if (backward && active === first) {
10549
- last.focus();
10638
+ last?.focus();
10550
10639
  event.preventDefault();
10551
10640
  }
10552
10641
  else if (!backward && active === last) {
@@ -10618,7 +10707,7 @@ class DialogOutlet {
10618
10707
  }
10619
10708
  topPanel() {
10620
10709
  const panels = this.panels();
10621
- return panels.length ? panels[panels.length - 1].nativeElement : undefined;
10710
+ return panels.at(-1)?.nativeElement;
10622
10711
  }
10623
10712
  focusable(root) {
10624
10713
  const selector = 'button:not(:disabled), a[href], input:not(:disabled), select:not(:disabled), textarea:not(:disabled), [tabindex]:not([tabindex="-1"])';
@@ -10626,7 +10715,7 @@ class DialogOutlet {
10626
10715
  }
10627
10716
  top() {
10628
10717
  const list = this.dialogs();
10629
- return list[list.length - 1];
10718
+ return list.at(-1);
10630
10719
  }
10631
10720
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.0", ngImport: i0, type: DialogOutlet, deps: [], target: i0.ɵɵFactoryTarget.Component });
10632
10721
  static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.1.0", type: DialogOutlet, isStandalone: true, selector: "lw-dialog-outlet", host: { listeners: { "document:keydown.escape": "onEscape()", "document:focusin": "onFocusIn($event)", "document:keydown.tab": "onTab($event, false)", "document:keydown.shift.tab": "onTab($event, true)" } }, viewQueries: [{ propertyName: "panels", predicate: ["panel"], descendants: true, isSignal: true }], ngImport: i0, template: "@for (d of dialogs(); track d.id) {\n <div [class]=\"wrapperClasses(d)\">\n <button\n type=\"button\"\n class=\"lw-scrim absolute inset-0 cursor-default\"\n [disabled]=\"!d.dismissable\"\n [attr.aria-label]=\"'dialog.close' | transloco\"\n (click)=\"dismiss(d)\"\n ></button>\n\n @if (d.bare) {\n <dialog\n #panel\n open\n class=\"relative z-10 m-0 flex max-h-full w-full flex-col overflow-hidden rounded-t-xl border border-border bg-surface-raised shadow-xl sm:rounded-xl\"\n [class.rounded-xl]=\"d.align === 'top'\"\n [class]=\"panelWidth(d)\"\n aria-modal=\"true\"\n [attr.aria-label]=\"d.title ? (d.title | transloco) : null\"\n tabindex=\"-1\"\n >\n <ng-container\n [ngComponentOutlet]=\"d.component ?? null\"\n [ngComponentOutletInjector]=\"d.injector\"\n />\n </dialog>\n } @else {\n <dialog\n #panel\n open\n class=\"relative z-10 m-0 flex max-h-full w-full flex-col gap-4 overflow-y-auto rounded-t-xl border border-border bg-surface-raised p-5 shadow-xl sm:rounded-xl\"\n [class.rounded-xl]=\"d.align === 'top'\"\n [class]=\"panelWidth(d)\"\n aria-modal=\"true\"\n [attr.aria-label]=\"d.title ? (d.title | transloco) : null\"\n tabindex=\"-1\"\n >\n <div class=\"flex gap-3.5\">\n @if (d.icon; as icon) {\n <span\n class=\"grid h-10 w-10 shrink-0 place-items-center rounded-full\"\n [class]=\"toneCircle(d.tone)\"\n aria-hidden=\"true\"\n >\n <lw-icon [name]=\"icon\" size=\"1.25rem\" />\n </span>\n }\n\n <div class=\"flex min-w-0 flex-1 flex-col gap-3\">\n @if (d.title || (d.kind === 'custom' && d.dismissable)) {\n <div class=\"flex items-start justify-between gap-4\">\n <h2 class=\"min-w-0 text-lg font-semibold text-content\">\n @if (d.title) {\n {{ d.title | transloco }}\n }\n </h2>\n <span class=\"flex shrink-0 items-center gap-1\">\n @if (d.maximizable) {\n <button\n lwButton\n variant=\"ghost\"\n size=\"sm\"\n iconOnly\n type=\"button\"\n class=\"-mt-1\"\n [attr.aria-label]=\"\n (d.ref.maximized() ? 'dialog.restore' : 'dialog.maximize') | transloco\n \"\n (click)=\"d.ref.toggleMaximized()\"\n >\n <lw-icon [name]=\"d.ref.maximized() ? 'restore' : 'maximize'\" size=\"1rem\" />\n </button>\n }\n @if (d.kind === 'custom' && d.dismissable) {\n <button\n lwButton\n variant=\"ghost\"\n size=\"sm\"\n iconOnly\n type=\"button\"\n class=\"-mt-1 -mr-1\"\n [attr.aria-label]=\"'dialog.close' | transloco\"\n (click)=\"dismiss(d)\"\n >\n <lw-icon name=\"close\" size=\"1rem\" />\n </button>\n }\n </span>\n </div>\n }\n\n @if (d.component) {\n <ng-container\n [ngComponentOutlet]=\"d.component\"\n [ngComponentOutletInjector]=\"d.injector\"\n />\n } @else if (d.kind === 'progress') {\n <div class=\"flex items-center gap-3 text-sm text-content\">\n <lw-spinner [label]=\"'progress.busy' | transloco\" />\n @if (d.progressMessage) {\n <span>{{ d.progressMessage() | transloco }}</span>\n }\n </div>\n } @else {\n @if (d.message) {\n <lw-markdown [source]=\"d.message | transloco\" />\n }\n @if (d.requireLabel) {\n <lw-markdown [source]=\"d.requireLabel | transloco\" />\n }\n @if (d.promptValue) {\n <input\n type=\"text\"\n class=\"lw-field\"\n [class.lw-field--invalid]=\"guardError(d)\"\n [value]=\"d.promptValue()\"\n [attr.placeholder]=\"d.placeholder ? (d.placeholder | transloco) : null\"\n (input)=\"onPromptInput(d, $event)\"\n (keydown.enter)=\"onEnter(d)\"\n data-lw-autofocus\n />\n @if (guardError(d); as error) {\n <p class=\"text-xs text-negative\">{{ error | transloco }}</p>\n }\n }\n }\n </div>\n </div>\n\n @if (d.buttons.length) {\n <div class=\"flex justify-end gap-2\">\n @for (b of d.buttons; track $index) {\n <button\n lwButton\n [variant]=\"b.variant\"\n type=\"button\"\n [disabled]=\"confirmBlocked(d, b)\"\n [attr.data-lw-autofocus]=\"b.autofocus ? '' : null\"\n (click)=\"onButton(d, b)\"\n >\n {{ b.label | transloco }}\n </button>\n }\n </div>\n }\n </dialog>\n }\n </div>\n}\n", dependencies: [{ kind: "directive", type: NgComponentOutlet, selector: "[ngComponentOutlet]", inputs: ["ngComponentOutlet", "ngComponentOutletInputs", "ngComponentOutletInjector", "ngComponentOutletEnvironmentInjector", "ngComponentOutletContent", "ngComponentOutletNgModule"], exportAs: ["ngComponentOutlet"] }, { kind: "directive", type: LwButton, selector: "button[lwButton], a[lwButton]", inputs: ["variant", "size", "iconOnly"] }, { kind: "component", type: LwSpinner, selector: "lw-spinner", inputs: ["size", "label"] }, { kind: "pipe", type: TranslocoPipe, name: "transloco" }] });
@@ -10776,7 +10865,7 @@ class SettingsRegistry {
10776
10865
  return this.sections()
10777
10866
  .map((section) => visibleSection(section, omitted))
10778
10867
  .filter((section) => section !== null)
10779
- .sort((a, b) => (a.order ?? 0) - (b.order ?? 0));
10868
+ .toSorted((a, b) => (a.order ?? 0) - (b.order ?? 0));
10780
10869
  }, /* @ts-ignore */
10781
10870
  ...(ngDevMode ? [{ debugName: "all" }] : /* istanbul ignore next */ []));
10782
10871
  register(section) {
@@ -11016,6 +11105,13 @@ function provideTranslationNamespaces(...namespaces) {
11016
11105
  /** Directory the distribution serves its overlay bundles from, without a trailing slash. */
11017
11106
  const TRANSLATION_OVERRIDES = new InjectionToken('TRANSLATION_OVERRIDES');
11018
11107
  const DEFAULT_OVERRIDES_PATH = '/i18n/overrides';
11108
+ function withoutTrailingSlashes(path) {
11109
+ let end = path.length;
11110
+ while (end > 0 && path[end - 1] === '/') {
11111
+ end -= 1;
11112
+ }
11113
+ return path.slice(0, end);
11114
+ }
11019
11115
  /**
11020
11116
  * Load `<basePath>/<lang>.json` and merge it over everything else **key by key**, so a product
11021
11117
  * can reword the shell in its own house language ("Save as" rather than "Save as new") without
@@ -11035,7 +11131,7 @@ const DEFAULT_OVERRIDES_PATH = '/i18n/overrides';
11035
11131
  * nothing ships is dev-warned too, since a typo there would otherwise be a string that never appears.
11036
11132
  */
11037
11133
  function provideTranslationOverrides(basePath = DEFAULT_OVERRIDES_PATH) {
11038
- const normalized = basePath.replace(/\/+$/, '');
11134
+ const normalized = withoutTrailingSlashes(basePath);
11039
11135
  if (normalized === '') {
11040
11136
  throw new Error('provideTranslationOverrides() needs a directory to load overlays from; ' +
11041
11137
  `pass one or omit the argument for "${DEFAULT_OVERRIDES_PATH}".`);
@@ -11087,7 +11183,9 @@ class TranslocoHttpLoader {
11087
11183
  return forkJoin([host$, ...namespaced$, this.overrides$(lang)]).pipe(map(([host, ...rest]) => {
11088
11184
  const overlay = rest.pop();
11089
11185
  const merged = { ...host };
11090
- this.namespaces.forEach((name, index) => (merged[name] = rest[index]));
11186
+ for (const [index, name] of this.namespaces.entries()) {
11187
+ merged[name] = rest[index];
11188
+ }
11091
11189
  return this.applyOverrides(merged, overlay, lang);
11092
11190
  }));
11093
11191
  }
@@ -11158,9 +11256,7 @@ class LwIconElement extends HTMLElement {
11158
11256
  this.render();
11159
11257
  }
11160
11258
  attributeChangedCallback() {
11161
- if (this.isConnected) {
11162
- this.render();
11163
- }
11259
+ this.refresh();
11164
11260
  }
11165
11261
  /**
11166
11262
  * Re-draws from the registry without changing the name. A sandboxed surface receives the product's
@@ -11267,7 +11363,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.0", ngImpor
11267
11363
  // GENERATED — do not edit by hand.
11268
11364
  // Written by tools/stamp-version.mjs from <Version> in Directory.Build.props.
11269
11365
  // Single source of truth: Directory.Build.props (bump via scripts/bump-version.sh).
11270
- const APP_VERSION = '0.7.5';
11366
+ const APP_VERSION = '0.7.6';
11271
11367
 
11272
11368
  /**
11273
11369
  * The running build's version, sourced from `<Version>` in Directory.Build.props
@@ -11483,7 +11579,7 @@ class UpdateService {
11483
11579
  await bestEffort(async () => {
11484
11580
  const registrations = (await container?.getRegistrations()) ?? [];
11485
11581
  await Promise.all(registrations
11486
- .filter(isShellWorker)
11582
+ .filter((registration) => isShellWorker(registration))
11487
11583
  .map((registration) => registration.unregister()));
11488
11584
  });
11489
11585
  await bestEffort(async () => {
@@ -11491,14 +11587,14 @@ class UpdateService {
11491
11587
  const keys = (await storage?.keys()) ?? [];
11492
11588
  await Promise.all(keys
11493
11589
  .filter((key) => key.startsWith(WORKER_CACHE_PREFIX))
11494
- .map((key) => storage?.delete(key)));
11590
+ .map(async (key) => storage?.delete(key)));
11495
11591
  });
11496
11592
  }
11497
11593
  onVersionEvent(event) {
11498
11594
  if (event.type === 'VERSION_READY') {
11499
11595
  this.onUpdateReady();
11500
11596
  }
11501
- if (event.type === 'VERSION_INSTALLATION_FAILED') {
11597
+ else if (event.type === 'VERSION_INSTALLATION_FAILED') {
11502
11598
  this.onUpdateFailed();
11503
11599
  }
11504
11600
  }
@@ -11862,7 +11958,7 @@ class CommandInvocationService {
11862
11958
  .filter((entry) => entry.command.callable === true &&
11863
11959
  this.reachable(entry, callerId, granted))
11864
11960
  .map((entry) => this.describe(entry.command))
11865
- .sort((a, b) => a.id.localeCompare(b.id));
11961
+ .toSorted((a, b) => a.id.localeCompare(b.id));
11866
11962
  }
11867
11963
  async invoke(callerId, granted, id, args) {
11868
11964
  const entry = this.registry
@@ -12182,17 +12278,21 @@ class LwTooltipElement extends HTMLElement {
12182
12278
  const centerX = t.left + t.width / 2 - b.width / 2;
12183
12279
  const centerY = t.top + t.height / 2 - b.height / 2;
12184
12280
  switch (this.position) {
12185
- case 'bottom':
12281
+ case 'bottom': {
12186
12282
  [left, top] = [centerX, t.bottom + TOOLTIP_GAP];
12187
12283
  break;
12188
- case 'left':
12284
+ }
12285
+ case 'left': {
12189
12286
  [left, top] = [t.left - b.width - TOOLTIP_GAP, centerY];
12190
12287
  break;
12191
- case 'right':
12288
+ }
12289
+ case 'right': {
12192
12290
  [left, top] = [t.right + TOOLTIP_GAP, centerY];
12193
12291
  break;
12194
- default:
12292
+ }
12293
+ default: {
12195
12294
  [left, top] = [centerX, t.top - b.height - TOOLTIP_GAP];
12295
+ }
12196
12296
  }
12197
12297
  }
12198
12298
  else {
@@ -12217,10 +12317,11 @@ class LwTooltipElement extends HTMLElement {
12217
12317
  }
12218
12318
  }
12219
12319
  clearTimer() {
12220
- if (this.showTimer !== undefined) {
12221
- clearTimeout(this.showTimer);
12222
- this.showTimer = undefined;
12320
+ if (this.showTimer === undefined) {
12321
+ return;
12223
12322
  }
12323
+ clearTimeout(this.showTimer);
12324
+ this.showTimer = undefined;
12224
12325
  }
12225
12326
  }
12226
12327
  /** Registers `<lw-tooltip>` once (idempotent) — called from {@link provideShell} at bootstrap. */
@@ -12231,11 +12332,7 @@ function defineLwTooltip() {
12231
12332
  }
12232
12333
  }
12233
12334
 
12234
- const LW_SELECT_TAG = 'lw-select';
12235
12335
  const LW_OPTION_TAG = 'lw-option';
12236
- const LW_SELECT_CHANGE = 'lw-select-change';
12237
- let nextSelectId = 0;
12238
- const TYPEAHEAD_RESET_MS = 500;
12239
12336
  class LwOptionElement extends HTMLElement {
12240
12337
  get value() {
12241
12338
  return this.getAttribute('value');
@@ -12254,6 +12351,87 @@ class LwOptionElement extends HTMLElement {
12254
12351
  upgradeElementProperty(this, 'icon');
12255
12352
  }
12256
12353
  }
12354
+
12355
+ function readChoices(host) {
12356
+ return [...host.querySelectorAll(LW_OPTION_TAG)].map((option) => ({
12357
+ value: option.getAttribute('value') ?? '',
12358
+ label: (option.textContent ?? '').trim(),
12359
+ icon: option.getAttribute('icon'),
12360
+ disabled: option.hasAttribute('disabled'),
12361
+ }));
12362
+ }
12363
+ function createTrigger(options) {
12364
+ const trigger = document.createElement('button');
12365
+ trigger.type = 'button';
12366
+ trigger.className = 'lw-select-trigger';
12367
+ trigger.setAttribute('aria-haspopup', 'listbox');
12368
+ trigger.setAttribute('aria-expanded', 'false');
12369
+ trigger.setAttribute('aria-controls', options.listboxId);
12370
+ trigger.style.setProperty('anchor-name', options.anchorName);
12371
+ trigger.addEventListener('click', options.onToggle);
12372
+ trigger.addEventListener('keydown', options.onKeydown);
12373
+ const valueSlot = document.createElement('span');
12374
+ valueSlot.className = 'lw-select-value';
12375
+ const chevron = document.createElement('span');
12376
+ chevron.className = 'lw-select-chevron';
12377
+ chevron.setAttribute('aria-hidden', 'true');
12378
+ chevron.textContent = '▾';
12379
+ trigger.append(valueSlot, chevron);
12380
+ return { trigger, valueSlot };
12381
+ }
12382
+ function createListbox(options) {
12383
+ const listbox = document.createElement('div');
12384
+ listbox.id = options.listboxId;
12385
+ listbox.className = 'lw-select-listbox';
12386
+ listbox.setAttribute('role', 'listbox');
12387
+ listbox.hidden = true;
12388
+ listbox.style.setProperty('position-anchor', options.anchorName);
12389
+ listbox.addEventListener('keydown', options.onKeydown);
12390
+ return listbox;
12391
+ }
12392
+ function fillValueSlot(slot, text, icon) {
12393
+ slot.textContent = '';
12394
+ if (icon) {
12395
+ slot.append(createGlyph(icon));
12396
+ }
12397
+ slot.append(document.createTextNode(text));
12398
+ }
12399
+ function createOptionRow(options) {
12400
+ const choice = options.choice;
12401
+ const row = document.createElement('div');
12402
+ row.className = 'lw-select-option';
12403
+ row.setAttribute('role', 'option');
12404
+ row.id = options.id;
12405
+ row.dataset['value'] = choice.value;
12406
+ row.setAttribute('aria-selected', String(options.selected));
12407
+ row.tabIndex = -1;
12408
+ if (choice.disabled) {
12409
+ row.setAttribute('aria-disabled', 'true');
12410
+ }
12411
+ if (choice.icon) {
12412
+ row.append(createGlyph(choice.icon));
12413
+ }
12414
+ row.append(document.createTextNode(choice.label));
12415
+ row.addEventListener('click', () => {
12416
+ if (!choice.disabled) {
12417
+ options.onPick();
12418
+ }
12419
+ });
12420
+ row.addEventListener('pointermove', options.onHover);
12421
+ return row;
12422
+ }
12423
+ function createGlyph(icon) {
12424
+ const glyph = document.createElement('span');
12425
+ glyph.className = 'lw-select-glyph';
12426
+ glyph.setAttribute('aria-hidden', 'true');
12427
+ glyph.textContent = icon;
12428
+ return glyph;
12429
+ }
12430
+
12431
+ const LW_SELECT_TAG = 'lw-select';
12432
+ const LW_SELECT_CHANGE = 'lw-select-change';
12433
+ let nextSelectId = 0;
12434
+ const TYPEAHEAD_RESET_MS = 500;
12257
12435
  class LwSelectElement extends HTMLElement {
12258
12436
  static observedAttributes = [
12259
12437
  'value',
@@ -12298,10 +12476,7 @@ class LwSelectElement extends HTMLElement {
12298
12476
  if (!this.trigger) {
12299
12477
  return;
12300
12478
  }
12301
- if (name === 'value' ||
12302
- name === 'label' ||
12303
- name === 'placeholder' ||
12304
- name === 'disabled') {
12479
+ if (LwSelectElement.observedAttributes.includes(name)) {
12305
12480
  this.syncTrigger();
12306
12481
  }
12307
12482
  if (name === 'disabled' && this.hasAttribute('disabled')) {
@@ -12321,51 +12496,34 @@ class LwSelectElement extends HTMLElement {
12321
12496
  attributes: true,
12322
12497
  });
12323
12498
  }
12324
- write(fn) {
12499
+ write(function_) {
12325
12500
  this.observer?.disconnect();
12326
12501
  try {
12327
- fn();
12502
+ function_();
12328
12503
  }
12329
12504
  finally {
12330
12505
  this.observe();
12331
12506
  }
12332
12507
  }
12333
12508
  choices() {
12334
- return [...this.querySelectorAll(LW_OPTION_TAG)].map((option) => ({
12335
- value: option.getAttribute('value') ?? '',
12336
- label: (option.textContent ?? '').trim(),
12337
- icon: option.getAttribute('icon'),
12338
- disabled: option.hasAttribute('disabled'),
12339
- }));
12509
+ return readChoices(this);
12340
12510
  }
12341
12511
  selectedChoice() {
12342
12512
  const value = this.value;
12343
12513
  return this.choices().find((choice) => choice.value === value);
12344
12514
  }
12345
12515
  buildControl() {
12346
- const trigger = document.createElement('button');
12347
- trigger.type = 'button';
12348
- trigger.className = 'lw-select-trigger';
12349
- trigger.setAttribute('aria-haspopup', 'listbox');
12350
- trigger.setAttribute('aria-expanded', 'false');
12351
- trigger.setAttribute('aria-controls', this.listboxId);
12352
- trigger.style.setProperty('anchor-name', this.anchorName);
12353
- trigger.addEventListener('click', () => this.toggle());
12354
- trigger.addEventListener('keydown', (event) => this.onTriggerKeydown(event));
12355
- const valueSlot = document.createElement('span');
12356
- valueSlot.className = 'lw-select-value';
12357
- const chevron = document.createElement('span');
12358
- chevron.className = 'lw-select-chevron';
12359
- chevron.setAttribute('aria-hidden', 'true');
12360
- chevron.textContent = '▾';
12361
- trigger.append(valueSlot, chevron);
12362
- const listbox = document.createElement('div');
12363
- listbox.id = this.listboxId;
12364
- listbox.className = 'lw-select-listbox';
12365
- listbox.setAttribute('role', 'listbox');
12366
- listbox.hidden = true;
12367
- listbox.style.setProperty('position-anchor', this.anchorName);
12368
- listbox.addEventListener('keydown', (event) => this.onListboxKeydown(event));
12516
+ const { trigger, valueSlot } = createTrigger({
12517
+ anchorName: this.anchorName,
12518
+ listboxId: this.listboxId,
12519
+ onToggle: () => this.toggle(),
12520
+ onKeydown: (event) => this.onTriggerKeydown(event),
12521
+ });
12522
+ const listbox = createListbox({
12523
+ anchorName: this.anchorName,
12524
+ listboxId: this.listboxId,
12525
+ onKeydown: (event) => this.onListboxKeydown(event),
12526
+ });
12369
12527
  this.append(trigger, listbox);
12370
12528
  this.trigger = trigger;
12371
12529
  this.valueSlot = valueSlot;
@@ -12380,22 +12538,13 @@ class LwSelectElement extends HTMLElement {
12380
12538
  const label = this.getAttribute('label');
12381
12539
  const selected = this.selectedChoice();
12382
12540
  const text = selected?.label ?? this.getAttribute('placeholder') ?? '';
12383
- const icon = selected?.icon ?? null;
12384
12541
  this.write(() => {
12385
12542
  trigger.disabled = this.hasAttribute('disabled');
12386
12543
  if (label !== null) {
12387
12544
  trigger.setAttribute('aria-label', label);
12388
12545
  this.listbox?.setAttribute('aria-label', label);
12389
12546
  }
12390
- valueSlot.textContent = '';
12391
- if (icon) {
12392
- const glyph = document.createElement('span');
12393
- glyph.className = 'lw-select-glyph';
12394
- glyph.setAttribute('aria-hidden', 'true');
12395
- glyph.textContent = icon;
12396
- valueSlot.append(glyph);
12397
- }
12398
- valueSlot.append(document.createTextNode(text));
12547
+ fillValueSlot(valueSlot, text, selected?.icon ?? null);
12399
12548
  });
12400
12549
  }
12401
12550
  toggle() {
@@ -12421,7 +12570,9 @@ class LwSelectElement extends HTMLElement {
12421
12570
  const choices = this.choices();
12422
12571
  const selected = choices.findIndex((choice) => choice.value === this.value);
12423
12572
  this.setActive(Math.max(0, selected));
12424
- document.addEventListener('pointerdown', this.onOutsidePointer, true);
12573
+ document.addEventListener('pointerdown', this.onOutsidePointer, {
12574
+ capture: true,
12575
+ });
12425
12576
  }
12426
12577
  close(refocusTrigger = true) {
12427
12578
  const listbox = this.listbox;
@@ -12441,94 +12592,83 @@ class LwSelectElement extends HTMLElement {
12441
12592
  }
12442
12593
  }
12443
12594
  renderOptions() {
12444
- if (!this.listbox) {
12595
+ const listbox = this.listbox;
12596
+ if (!listbox) {
12445
12597
  return;
12446
12598
  }
12447
- const items = this.choices().map((choice, index) => {
12448
- const option = document.createElement('div');
12449
- option.className = 'lw-select-option';
12450
- option.setAttribute('role', 'option');
12451
- option.id = `${this.listboxId}-opt-${index}`;
12452
- option.dataset['value'] = choice.value;
12453
- option.setAttribute('aria-selected', String(choice.value === this.value));
12454
- option.tabIndex = -1;
12455
- if (choice.disabled) {
12456
- option.setAttribute('aria-disabled', 'true');
12457
- }
12458
- if (choice.icon) {
12459
- const glyph = document.createElement('span');
12460
- glyph.className = 'lw-select-glyph';
12461
- glyph.setAttribute('aria-hidden', 'true');
12462
- glyph.textContent = choice.icon;
12463
- option.append(glyph);
12464
- }
12465
- option.append(document.createTextNode(choice.label));
12466
- option.addEventListener('click', () => {
12467
- if (!choice.disabled) {
12468
- this.commit(choice.value);
12469
- }
12470
- });
12471
- option.addEventListener('pointermove', () => this.setActive(index));
12472
- return option;
12473
- });
12474
- const listbox = this.listbox;
12475
- this.write(() => listbox.replaceChildren(...items));
12599
+ const rows = this.choices().map((choice, index) => createOptionRow({
12600
+ choice,
12601
+ id: `${this.listboxId}-opt-${index}`,
12602
+ selected: choice.value === this.value,
12603
+ onPick: () => this.commit(choice.value),
12604
+ onHover: () => this.setActive(index),
12605
+ }));
12606
+ this.write(() => listbox.replaceChildren(...rows));
12476
12607
  }
12477
12608
  setActive(index) {
12478
12609
  if (!this.listbox) {
12479
12610
  return;
12480
12611
  }
12481
- const options = [...this.listbox.children];
12482
- if (options.length === 0) {
12612
+ const rows = [...this.listbox.children];
12613
+ if (rows.length === 0) {
12483
12614
  return;
12484
12615
  }
12485
- this.activeIndex = Math.max(0, Math.min(index, options.length - 1));
12616
+ this.activeIndex = Math.max(0, Math.min(index, rows.length - 1));
12486
12617
  this.write(() => {
12487
- options.forEach((option, i) => {
12488
- option.classList.toggle('is-active', i === this.activeIndex);
12489
- option.tabIndex = i === this.activeIndex ? 0 : -1;
12490
- });
12618
+ for (const [index_, row] of rows.entries()) {
12619
+ row.classList.toggle('is-active', index_ === this.activeIndex);
12620
+ row.tabIndex = index_ === this.activeIndex ? 0 : -1;
12621
+ }
12491
12622
  });
12492
- const active = options[this.activeIndex];
12623
+ const active = rows[this.activeIndex];
12493
12624
  active.focus();
12494
12625
  active.scrollIntoView?.({ block: 'nearest' });
12495
12626
  }
12496
12627
  onTriggerKeydown(event) {
12497
- if (['ArrowDown', 'ArrowUp', 'Enter', ' '].includes(event.key)) {
12498
- event.preventDefault();
12499
- this.openListbox();
12628
+ if (!['ArrowDown', 'ArrowUp', 'Enter', ' '].includes(event.key)) {
12629
+ return;
12500
12630
  }
12631
+ event.preventDefault();
12632
+ this.openListbox();
12501
12633
  }
12502
12634
  onListboxKeydown(event) {
12503
12635
  const last = this.choices().length - 1;
12504
12636
  switch (event.key) {
12505
- case 'ArrowDown':
12637
+ case 'ArrowDown': {
12506
12638
  this.setActive(this.activeIndex >= last ? 0 : this.activeIndex + 1);
12507
12639
  break;
12508
- case 'ArrowUp':
12640
+ }
12641
+ case 'ArrowUp': {
12509
12642
  this.setActive(this.activeIndex <= 0 ? last : this.activeIndex - 1);
12510
12643
  break;
12511
- case 'Home':
12644
+ }
12645
+ case 'Home': {
12512
12646
  this.setActive(0);
12513
12647
  break;
12514
- case 'End':
12648
+ }
12649
+ case 'End': {
12515
12650
  this.setActive(last);
12516
12651
  break;
12652
+ }
12517
12653
  case 'Enter':
12518
- case ' ':
12654
+ case ' ': {
12519
12655
  this.commitActive();
12520
12656
  break;
12521
- case 'Escape':
12657
+ }
12658
+ case 'Escape': {
12522
12659
  this.close();
12523
12660
  break;
12524
- case 'Tab':
12661
+ }
12662
+ case 'Tab': {
12525
12663
  this.close(false);
12526
12664
  return;
12527
- default:
12665
+ }
12666
+ default: {
12528
12667
  if (event.key.length === 1) {
12529
12668
  this.onTypeahead(event.key);
12530
12669
  }
12531
12670
  return;
12671
+ }
12532
12672
  }
12533
12673
  event.preventDefault();
12534
12674
  }
@@ -12540,7 +12680,7 @@ class LwSelectElement extends HTMLElement {
12540
12680
  this.typeaheadTimer = setTimeout(() => (this.typeahead = ''), TYPEAHEAD_RESET_MS);
12541
12681
  const match = this.choices().findIndex((choice) => !choice.disabled &&
12542
12682
  choice.label.toLowerCase().startsWith(this.typeahead));
12543
- if (match >= 0) {
12683
+ if (match !== -1) {
12544
12684
  this.setActive(match);
12545
12685
  }
12546
12686
  }
@@ -12689,10 +12829,11 @@ class LwButtonElement extends HTMLElement {
12689
12829
  }
12690
12830
  }
12691
12831
  onKeydown = (event) => {
12692
- if ((event.key === 'Enter' || event.key === ' ') && !this.disabled) {
12693
- event.preventDefault();
12694
- this.click();
12832
+ if (!(event.key === 'Enter' || event.key === ' ') || this.disabled) {
12833
+ return;
12695
12834
  }
12835
+ event.preventDefault();
12836
+ this.click();
12696
12837
  };
12697
12838
  render() {
12698
12839
  const stale = [...this.classList].filter((cls) => cls.startsWith('lw-btn'));
@@ -12871,7 +13012,7 @@ class ViewVisibilityService {
12871
13012
  return this.stash
12872
13013
  .keyedInstances()
12873
13014
  .filter((entry) => !entry.key.startsWith(CONTAINER_DOCK_PREFIX) &&
12874
- entry.key.split('|')[1] === path)
13015
+ entry.key.split('|', 2)[1] === path)
12875
13016
  .map((entry) => entry.instance);
12876
13017
  }
12877
13018
  removeTabs(path) {
@@ -12943,10 +13084,11 @@ class RailWorkspaceEntries {
12943
13084
  reconcile() {
12944
13085
  const wanted = this.wantedItems();
12945
13086
  for (const [id, registration] of this.registered) {
12946
- if (!wanted.has(id)) {
12947
- registration.disposable.dispose();
12948
- this.registered.delete(id);
13087
+ if (wanted.has(id)) {
13088
+ continue;
12949
13089
  }
13090
+ registration.disposable.dispose();
13091
+ this.registered.delete(id);
12950
13092
  }
12951
13093
  for (const [id, item] of wanted) {
12952
13094
  const current = this.registered.get(id);
@@ -13188,10 +13330,10 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.0", ngImpor
13188
13330
  type: Service
13189
13331
  }] });
13190
13332
  function installCompositionReport(report) {
13191
- if (typeof window === 'undefined') {
13333
+ if (globalThis.window === undefined) {
13192
13334
  return;
13193
13335
  }
13194
- const host = window;
13336
+ const host = globalThis;
13195
13337
  if (host['loomweaver'] !== undefined) {
13196
13338
  return;
13197
13339
  }
@@ -13262,7 +13404,7 @@ class PluginEnablementService {
13262
13404
  const disabled = this.disabledSet();
13263
13405
  return [...this.names().entries()]
13264
13406
  .map(([id, name]) => ({ id, name, enabled: !disabled.has(id) }))
13265
- .sort((a, b) => a.name.localeCompare(b.name));
13407
+ .toSorted((a, b) => a.name.localeCompare(b.name));
13266
13408
  }, /* @ts-ignore */
13267
13409
  ...(ngDevMode ? [{ debugName: "plugins" }] : /* istanbul ignore next */ []));
13268
13410
  constructor() {
@@ -13416,7 +13558,7 @@ function parseCatalogEntry(raw) {
13416
13558
  function dedupeById(items) {
13417
13559
  const result = [];
13418
13560
  for (const item of items) {
13419
- if (item && !result.some((existing) => existing.id === item.id)) {
13561
+ if (item && result.every((existing) => existing.id !== item.id)) {
13420
13562
  result.push(item);
13421
13563
  }
13422
13564
  }
@@ -13431,7 +13573,7 @@ function parseInstalledList(raw) {
13431
13573
  if (!Array.isArray(parsed)) {
13432
13574
  return [];
13433
13575
  }
13434
- return dedupeById(parsed.map(parseInstalledPlugin));
13576
+ return dedupeById(parsed.map((raw) => parseInstalledPlugin(raw)));
13435
13577
  }
13436
13578
  catch {
13437
13579
  return [];
@@ -13441,7 +13583,7 @@ function parseCatalogList(raw) {
13441
13583
  if (!Array.isArray(raw)) {
13442
13584
  return [];
13443
13585
  }
13444
- return dedupeById(raw.map(parseCatalogEntry));
13586
+ return dedupeById(raw.map((entry) => parseCatalogEntry(entry)));
13445
13587
  }
13446
13588
 
13447
13589
  const STORAGE_KEY$2 = 'lw.shell.deployed-plugins';
@@ -13469,7 +13611,7 @@ class PluginDeploymentService {
13469
13611
  adopt(entries) {
13470
13612
  this.persist(entries
13471
13613
  .filter((entry) => entry.deployed === true)
13472
- .map(withoutCatalogMetadata));
13614
+ .map((entry) => withoutCatalogMetadata(entry)));
13473
13615
  }
13474
13616
  isDeployed(id) {
13475
13617
  return this.entries().some((entry) => entry.id === id);
@@ -13587,10 +13729,10 @@ function registerDefaultSettings(settings) {
13587
13729
 
13588
13730
  function fuzzyScore(query, label) {
13589
13731
  const needle = query.toLowerCase();
13590
- const haystack = label.toLowerCase();
13591
13732
  if (!needle) {
13592
13733
  return 0;
13593
13734
  }
13735
+ const haystack = label.toLowerCase();
13594
13736
  let score = 0;
13595
13737
  let searchFrom = 0;
13596
13738
  let previous = -2;
@@ -13620,7 +13762,7 @@ function formatterFor(locale) {
13620
13762
  }
13621
13763
  catch {
13622
13764
  try {
13623
- return new Intl.RelativeTimeFormat(locale.replace(/_/g, '-'), {
13765
+ return new Intl.RelativeTimeFormat(locale.replaceAll('_', '-'), {
13624
13766
  numeric: 'auto',
13625
13767
  });
13626
13768
  }
@@ -13697,7 +13839,7 @@ function ranked(query, entries) {
13697
13839
  return entries
13698
13840
  .map((entry) => ({ entry, score: fuzzyScore(query, entry.label) }))
13699
13841
  .filter((scored) => scored.score !== null)
13700
- .sort((a, b) => b.score - a.score)
13842
+ .toSorted((a, b) => b.score - a.score)
13701
13843
  .map((scored) => scored.entry);
13702
13844
  }
13703
13845
  class CommandPalette {
@@ -13754,9 +13896,9 @@ class CommandPalette {
13754
13896
  pinned: tab.pinned,
13755
13897
  closable: tab.closable,
13756
13898
  lastActive: tab.lastActive,
13757
- time: tab.lastActive !== undefined
13758
- ? formatRelativeTime(locale, tab.lastActive, now)
13759
- : undefined,
13899
+ time: tab.lastActive === undefined
13900
+ ? undefined
13901
+ : formatRelativeTime(locale, tab.lastActive, now),
13760
13902
  }));
13761
13903
  }, /* @ts-ignore */
13762
13904
  ...(ngDevMode ? [{ debugName: "tabEntries" }] : /* istanbul ignore next */ []));
@@ -13785,7 +13927,7 @@ class CommandPalette {
13785
13927
  const query = this.query().trim();
13786
13928
  const entries = this.tabEntries();
13787
13929
  if (!query) {
13788
- return [...entries].sort((a, b) => (b.lastActive ?? 0) - (a.lastActive ?? 0));
13930
+ return [...entries].toSorted((a, b) => (b.lastActive ?? 0) - (a.lastActive ?? 0));
13789
13931
  }
13790
13932
  return ranked(query, entries);
13791
13933
  }, /* @ts-ignore */
@@ -13860,7 +14002,7 @@ class CommandPalette {
13860
14002
  return;
13861
14003
  }
13862
14004
  const entry = this.results()[this.activeIndex()];
13863
- if (!entry || entry.kind !== 'tab') {
14005
+ if (entry?.kind !== 'tab') {
13864
14006
  return;
13865
14007
  }
13866
14008
  event.preventDefault();
@@ -14261,9 +14403,12 @@ function seedBuiltInMenus(registry, layout, deps) {
14261
14403
  return;
14262
14404
  }
14263
14405
  registerTabContextMenu(registry, deps.tabs, deps.paneMove, deps.popout, deps.features);
14406
+ seedRailMenus(registry, layout, deps);
14407
+ seedViewMenus(registry, layout, deps);
14408
+ }
14409
+ function seedRailMenus(registry, layout, deps) {
14264
14410
  const railCount = layout.regions.filter((region) => region.type === 'rail').length;
14265
14411
  const rail = deps.features.rail;
14266
- const sidebar = deps.features.sidebar;
14267
14412
  if (railCount >= 1 && rail.hideItems) {
14268
14413
  registerRailContextMenu(registry, deps.railItems);
14269
14414
  }
@@ -14273,6 +14418,9 @@ function seedBuiltInMenus(registry, layout, deps) {
14273
14418
  if (railCount >= 1 && rail.curate) {
14274
14419
  registerRailCustomizeMenu(registry);
14275
14420
  }
14421
+ }
14422
+ function seedViewMenus(registry, layout, deps) {
14423
+ const sidebar = deps.features.sidebar;
14276
14424
  if (sidebar.resetViewState) {
14277
14425
  registerViewResetMenu(registry, deps.viewStates, deps.viewInstances);
14278
14426
  }
@@ -14421,7 +14569,7 @@ class BootLatchedIdentity {
14421
14569
  return this.latched;
14422
14570
  }
14423
14571
  const id = this.read();
14424
- if (id === null || id === undefined || id === '') {
14572
+ if (!id) {
14425
14573
  return null;
14426
14574
  }
14427
14575
  this.latched = id;
@@ -14711,7 +14859,7 @@ function surfaceRoute(route, retained) {
14711
14859
  function subStub(path, pathMatch) {
14712
14860
  return {
14713
14861
  path,
14714
- ...(pathMatch ? { pathMatch } : {}),
14862
+ ...(pathMatch && { pathMatch }),
14715
14863
  component: ContentSubStub,
14716
14864
  data: { content: true, sub: true },
14717
14865
  };
@@ -15002,13 +15150,13 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.0", ngImpor
15002
15150
  type: Service
15003
15151
  }] });
15004
15152
  function pathOfKey(key) {
15005
- return key.split('|')[1] ?? '';
15153
+ return key.split('|', 2)[1] ?? '';
15006
15154
  }
15007
15155
  function stashKeyLive(key, open, routes, views) {
15008
15156
  if (key.startsWith(PRIMARY_RETENTION_PREFIX)) {
15009
15157
  return true;
15010
15158
  }
15011
- const [scope, path] = key.split('|');
15159
+ const [scope, path] = key.split('|', 2);
15012
15160
  if (!tabOpen(open.get(scope), routes, path)) {
15013
15161
  return false;
15014
15162
  }
@@ -15541,7 +15689,8 @@ class IconRegistry {
15541
15689
  setIcon(name, safe);
15542
15690
  added.push(name);
15543
15691
  }
15544
- return { dispose: () => added.forEach((name) => removeIcon(name)) };
15692
+ return { dispose: () => { for (const name of added)
15693
+ removeIcon(name); } };
15545
15694
  }
15546
15695
  resolve(name) {
15547
15696
  return resolveIcon(name);
@@ -15691,6 +15840,9 @@ function providePlugins(...plugins) {
15691
15840
  ];
15692
15841
  }
15693
15842
 
15843
+ /** Multi-provider token: each contribution adds one sandboxed plugin to load. */
15844
+ const FRAME_PLUGIN = new InjectionToken('FRAME_PLUGIN');
15845
+
15694
15846
  const STORAGE_KEY = 'lw.shell.installed-plugins';
15695
15847
  /**
15696
15848
  * The user's installed community plugins. Holds only the state: which catalog entries the
@@ -15722,7 +15874,7 @@ class PluginInstallService {
15722
15874
  return this.entries().some((entry) => entry.id === id);
15723
15875
  }
15724
15876
  /** The installed entry for an id, or `undefined` — the baseline an update is compared against. */
15725
- find(id) {
15877
+ byId(id) {
15726
15878
  return this.entries().find((entry) => entry.id === id);
15727
15879
  }
15728
15880
  /**
@@ -15787,202 +15939,86 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.0", ngImpor
15787
15939
  type: Service
15788
15940
  }], ctorParameters: () => [] });
15789
15941
 
15790
- const INPUT_TYPES = ['text', 'date', 'email', 'number', 'password'];
15791
- function sanitizeOptions(raw) {
15792
- if (!Array.isArray(raw)) {
15793
- return [];
15794
- }
15795
- return raw
15796
- .filter((option) => typeof option === 'object' &&
15797
- option !== null &&
15798
- typeof option['value'] === 'string' &&
15799
- typeof option['label'] === 'string')
15800
- .map((option) => ({ value: option.value, label: option.label }));
15801
- }
15802
- function optionalString(value) {
15803
- return typeof value === 'string' ? value : undefined;
15804
- }
15805
- function optionalNumber(value) {
15806
- return typeof value === 'number' ? value : undefined;
15807
- }
15808
- function buildTextControl(value, control) {
15809
- return {
15810
- kind: 'text',
15811
- value,
15812
- inputType: INPUT_TYPES.find((type) => type === control['inputType']),
15813
- placeholder: optionalString(control['placeholder']),
15814
- };
15942
+ function levelOf(plugin) {
15943
+ return plugin.level ?? DEFAULT_ISOLATION_LEVEL;
15815
15944
  }
15816
- function buildSelectControl(pluginId, value, control) {
15817
- const options = sanitizeOptions(control['options']);
15818
- if (options.length === 0) {
15819
- throw new Error(`Sandbox plugin "${pluginId}": a select control needs at least one { value, label } option.`);
15945
+ function signatureOf(plugin) {
15946
+ const sorted = (values) => [...(values ?? [])].toSorted((a, b) => a.localeCompare(b)).join(',');
15947
+ return `${plugin.entryUrl}|${sorted(plugin.capabilities)}|${sorted(plugin.granted)}|${plugin.version ?? ''}|${levelOf(plugin)}`;
15948
+ }
15949
+ function runnablePlugins(composed, installed, deployed, catalogCap) {
15950
+ const claimed = new Set(composed.map((plugin) => plugin.id));
15951
+ const provided = new Set(deployed.map((plugin) => plugin.id));
15952
+ const fromCatalog = [];
15953
+ for (const plugin of [...deployed, ...installed]) {
15954
+ if (claimed.has(plugin.id)) {
15955
+ continue;
15956
+ }
15957
+ const asked = plugin.level ?? DEFAULT_ISOLATION_LEVEL;
15958
+ if (exceedsLevel(asked, catalogCap)) {
15959
+ console.error(`Plugin "${plugin.id}" asks to run ${asked}, which this catalog may not confer ` +
15960
+ `(its cap is ${catalogCap}). It is not started.`);
15961
+ continue;
15962
+ }
15963
+ claimed.add(plugin.id);
15964
+ fromCatalog.push({
15965
+ id: plugin.id,
15966
+ entryUrl: plugin.entryUrl,
15967
+ capabilities: plugin.capabilities,
15968
+ name: plugin.name,
15969
+ granted: plugin.capabilities ?? [],
15970
+ version: plugin.version,
15971
+ level: asked,
15972
+ provided: provided.has(plugin.id) || undefined,
15973
+ });
15820
15974
  }
15821
- return { kind: 'select', value, options };
15975
+ return [...composed, ...fromCatalog];
15822
15976
  }
15823
- function buildSliderControl(value, control) {
15824
- return {
15825
- kind: 'slider',
15826
- value,
15827
- min: optionalNumber(control['min']),
15828
- max: optionalNumber(control['max']),
15829
- step: optionalNumber(control['step']),
15830
- };
15831
- }
15832
- function sanitizeControl(pluginId, raw) {
15833
- const control = (raw ?? {});
15834
- const kind = control['kind'];
15835
- const value = control['value'];
15836
- if (kind === 'toggle' && typeof value === 'boolean') {
15837
- return { kind, value };
15838
- }
15839
- if (kind === 'text' && typeof value === 'string') {
15840
- return buildTextControl(value, control);
15841
- }
15842
- if (kind === 'select' && typeof value === 'string') {
15843
- return buildSelectControl(pluginId, value, control);
15844
- }
15845
- if (kind === 'slider' && typeof value === 'number') {
15846
- return buildSliderControl(value, control);
15847
- }
15848
- throw new Error(`Sandbox plugin "${pluginId}": a settings control must be toggle/text/select/slider with a matching default 'value'.`);
15849
- }
15850
- function sanitizeRow(pluginId, raw) {
15851
- const row = (raw ?? {});
15852
- if (typeof row['id'] !== 'string' || row['id'].length === 0) {
15853
- throw new Error(`Sandbox plugin "${pluginId}": every settings row needs a non-empty 'id'.`);
15854
- }
15855
- if (typeof row['label'] !== 'string' || row['label'].length === 0) {
15856
- throw new Error(`Sandbox plugin "${pluginId}": every settings row needs a non-empty 'label'.`);
15857
- }
15858
- return {
15859
- id: row['id'],
15860
- label: row['label'],
15861
- description: typeof row['description'] === 'string' ? row['description'] : undefined,
15862
- control: sanitizeControl(pluginId, row['control']),
15863
- };
15864
- }
15865
- function sanitizeRpcSettingsSection(pluginId, section) {
15866
- const raw = (section ?? {});
15867
- if (typeof raw['id'] !== 'string' || raw['id'].length === 0) {
15868
- throw new Error(`Sandbox plugin "${pluginId}": registerSettingsSection requires a non-empty 'id'.`);
15869
- }
15870
- if (typeof raw['title'] !== 'string' || raw['title'].length === 0) {
15871
- throw new Error(`Sandbox plugin "${pluginId}": registerSettingsSection requires a non-empty 'title'.`);
15872
- }
15873
- if (!Array.isArray(raw['rows']) || raw['rows'].length === 0) {
15874
- throw new Error(`Sandbox plugin "${pluginId}": registerSettingsSection requires at least one row.`);
15875
- }
15876
- return {
15877
- id: raw['id'],
15878
- title: raw['title'],
15879
- order: typeof raw['order'] === 'number' ? raw['order'] : undefined,
15880
- rows: raw['rows'].map((row) => sanitizeRow(pluginId, row)),
15881
- };
15882
- }
15883
- function defaultsOf(wire) {
15884
- const defaults = {};
15885
- for (const row of wire.rows) {
15886
- defaults[row.id] = row.control.value;
15887
- }
15888
- return defaults;
15889
- }
15890
- function typedOverlay(defaults, raw) {
15891
- if (!raw) {
15892
- return defaults;
15893
- }
15894
- try {
15895
- const parsed = JSON.parse(raw);
15896
- if (typeof parsed !== 'object' || parsed === null) {
15897
- return defaults;
15898
- }
15899
- const merged = { ...defaults };
15900
- for (const [key, value] of Object.entries(parsed)) {
15901
- if (key in defaults && typeof value === typeof defaults[key]) {
15902
- merged[key] = value;
15903
- }
15904
- }
15905
- return merged;
15906
- }
15907
- catch {
15908
- return defaults;
15909
- }
15910
- }
15911
- function buildFrameSection(deps) {
15912
- const { pluginId, wire, group, store, sync, notify } = deps;
15913
- const key = `lw.plugin-settings:${pluginId}:${wire.id}`;
15914
- const defaults = defaultsOf(wire);
15915
- const values = signal(typedOverlay(defaults, store.peek?.(key)), /* @ts-ignore */
15916
- ...(ngDevMode ? [{ debugName: "values" }] : /* istanbul ignore next */ []));
15917
- const applyStored = (raw) => {
15918
- values.set(typedOverlay(defaults, raw));
15919
- notify(wire.id, values());
15920
- };
15921
- if (store.peek) {
15922
- notify(wire.id, values());
15923
- }
15924
- else {
15925
- hydrateAsync(store, key, applyStored);
15926
- }
15927
- const disposeSync = sync.register('settings', key, applyStored);
15928
- const set = (rowId, value) => {
15929
- values.update((current) => ({ ...current, [rowId]: value }));
15930
- void store.set(key, JSON.stringify(values()));
15931
- notify(wire.id, values());
15932
- };
15933
- const section = {
15934
- id: `${pluginId}.${wire.id}`,
15935
- title: wire.title,
15936
- group,
15937
- order: wire.order,
15938
- rows: wire.rows.map((row) => ({
15939
- id: `${pluginId}.${wire.id}.${row.id}`,
15940
- label: row.label,
15941
- description: row.description,
15942
- control: hostControl(row, values, set),
15943
- })),
15944
- };
15945
- return { section, disposeSync };
15946
- }
15947
- function hostControl(row, values, set) {
15948
- const control = row.control;
15949
- switch (control.kind) {
15950
- case 'toggle':
15951
- return {
15952
- kind: 'toggle',
15953
- value: () => values()[row.id] === true,
15954
- set: (value) => set(row.id, value),
15955
- };
15956
- case 'text':
15957
- return {
15958
- kind: 'text',
15959
- inputType: control.inputType,
15960
- placeholder: control.placeholder,
15961
- value: () => String(values()[row.id] ?? ''),
15962
- set: (value) => set(row.id, value),
15963
- };
15964
- case 'select':
15965
- return {
15966
- kind: 'select',
15967
- options: control.options,
15968
- value: () => String(values()[row.id] ?? control.value),
15969
- set: (value) => set(row.id, value),
15970
- };
15971
- case 'slider':
15972
- return {
15973
- kind: 'slider',
15974
- min: control.min,
15975
- max: control.max,
15976
- step: control.step,
15977
- value: () => Number(values()[row.id] ?? control.value),
15978
- set: (value) => set(row.id, value),
15979
- };
15980
- }
15977
+
15978
+ const UNCARRIABLE_ARGUMENTS = {
15979
+ outcome: 'refused',
15980
+ reason: 'invalid-arguments',
15981
+ message: 'Arguments must be an object of single values or lists of them; anything else cannot cross the ' +
15982
+ 'sandbox boundary as the value it was.',
15983
+ };
15984
+ function invokeRpcCommand(ctx, id, args) {
15985
+ const carried = args === undefined ? undefined : asCommandArguments(args);
15986
+ return carried === null
15987
+ ? Promise.resolve(UNCARRIABLE_ARGUMENTS)
15988
+ : ctx.invokeCommand(String(id), carried);
15981
15989
  }
15982
15990
 
15983
15991
  const MAX_RPC_AREA_DEPTH = 8;
15984
15992
  function sanitizeRpcSurface(pluginId, surface, permitted) {
15985
15993
  const raw = (surface ?? {});
15994
+ const { id, title } = rpcSurfaceIdentity(pluginId, raw);
15995
+ const container = sanitizeRpcContainer(raw['container']);
15996
+ const iframe = container === undefined
15997
+ ? rpcIframeUrl(pluginId, raw['iframe'], permitted)
15998
+ : undefined;
15999
+ const routable = sanitizeRpcRoutable(raw['routable']);
16000
+ const docks = sanitizeRpcDocks(raw['docks']);
16001
+ assertRpcSurfaceAddress(pluginId, container, routable, docks);
16002
+ const shared = {
16003
+ id,
16004
+ title,
16005
+ icon: typeof raw['icon'] === 'string' ? raw['icon'] : undefined,
16006
+ order: typeof raw['order'] === 'number' ? raw['order'] : undefined,
16007
+ instanceable: raw['instanceable'] === true ? true : undefined,
16008
+ retain: raw['retain'] === 'always' || raw['retain'] === 'never'
16009
+ ? raw['retain']
16010
+ : undefined,
16011
+ saveOn: raw['saveOn'] === 'hide' ? 'hide' : undefined,
16012
+ closable: raw['closable'] === false ? false : undefined,
16013
+ padded: raw['padded'] === false ? false : undefined,
16014
+ routable,
16015
+ docks,
16016
+ };
16017
+ return container === undefined
16018
+ ? { ...shared, iframe: iframe }
16019
+ : { ...shared, container };
16020
+ }
16021
+ function rpcSurfaceIdentity(pluginId, raw) {
15986
16022
  if (typeof raw['id'] !== 'string' || raw['id'].length === 0) {
15987
16023
  throw new Error(`Sandbox plugin "${pluginId}": registerSurface requires a non-empty 'id'.`);
15988
16024
  }
@@ -15997,20 +16033,20 @@ function sanitizeRpcSurface(pluginId, surface, permitted) {
15997
16033
  throw new Error(`Sandbox plugin "${pluginId}": 'access' does not cross the RPC boundary — ` +
15998
16034
  `a sandboxed surface gates itself from the pushed session state.`);
15999
16035
  }
16000
- const container = sanitizeRpcContainer(raw['container']);
16001
- const iframe = raw['iframe'];
16002
- if (container === undefined) {
16003
- if (typeof iframe !== 'string') {
16004
- throw new Error(`Sandbox plugin "${pluginId}": registerSurface needs an { iframe } URL or a { container } spec.`);
16005
- }
16006
- const origin = surfaceOrigin(iframe);
16007
- if (origin === null || !permittedOrigins(permitted).has(origin)) {
16008
- throw new Error(`Sandbox plugin "${pluginId}": the iframe surface must be served from an origin this ` +
16009
- `distribution permitted for it, got "${iframe}".`);
16010
- }
16036
+ return { id: raw['id'], title: raw['title'] };
16037
+ }
16038
+ function rpcIframeUrl(pluginId, value, permitted) {
16039
+ if (typeof value !== 'string') {
16040
+ throw new TypeError(`Sandbox plugin "${pluginId}": registerSurface needs an { iframe } URL or a { container } spec.`);
16011
16041
  }
16012
- const routable = sanitizeRpcRoutable(raw['routable']);
16013
- const docks = sanitizeRpcDocks(raw['docks']);
16042
+ const origin = surfaceOrigin(value);
16043
+ if (origin === null || !permittedOrigins(permitted).has(origin)) {
16044
+ throw new Error(`Sandbox plugin "${pluginId}": the iframe surface must be served from an origin this ` +
16045
+ `distribution permitted for it, got "${value}".`);
16046
+ }
16047
+ return value;
16048
+ }
16049
+ function assertRpcSurfaceAddress(pluginId, container, routable, docks) {
16014
16050
  if (routable === undefined && docks === undefined) {
16015
16051
  throw new Error(`Sandbox plugin "${pluginId}": registerSurface needs 'routable.path' (a URL-addressed surface) ` +
16016
16052
  `or 'docks' (a surface hosted at a dock).`);
@@ -16019,24 +16055,6 @@ function sanitizeRpcSurface(pluginId, surface, permitted) {
16019
16055
  throw new Error(`Sandbox plugin "${pluginId}": a container surface must be routable — a container tab holds ` +
16020
16056
  `its own ':id'.`);
16021
16057
  }
16022
- const shared = {
16023
- id: raw['id'],
16024
- title: raw['title'],
16025
- icon: typeof raw['icon'] === 'string' ? raw['icon'] : undefined,
16026
- order: typeof raw['order'] === 'number' ? raw['order'] : undefined,
16027
- instanceable: raw['instanceable'] === true ? true : undefined,
16028
- retain: raw['retain'] === 'always' || raw['retain'] === 'never'
16029
- ? raw['retain']
16030
- : undefined,
16031
- saveOn: raw['saveOn'] === 'hide' ? 'hide' : undefined,
16032
- closable: raw['closable'] === false ? false : undefined,
16033
- padded: raw['padded'] === false ? false : undefined,
16034
- routable,
16035
- docks,
16036
- };
16037
- return container !== undefined
16038
- ? { ...shared, container }
16039
- : { ...shared, iframe: iframe };
16040
16058
  }
16041
16059
  function sanitizeRpcRoutable(value) {
16042
16060
  if (typeof value !== 'object' || value === null) {
@@ -16089,7 +16107,7 @@ function sanitizeRpcArea(value, depth) {
16089
16107
  const raw = value;
16090
16108
  const size = typeof raw['size'] === 'number' ? { size: raw['size'] } : {};
16091
16109
  if (Array.isArray(raw['tabs'])) {
16092
- return { ...size, tabs: raw['tabs'].flatMap(sanitizeRpcContainerTab) };
16110
+ return { ...size, tabs: raw['tabs'].flatMap((value) => sanitizeRpcContainerTab(value)) };
16093
16111
  }
16094
16112
  for (const kind of ['rows', 'columns']) {
16095
16113
  const declared = raw[kind];
@@ -16116,8 +16134,8 @@ function sanitizeRpcContainerTab(value) {
16116
16134
  return [
16117
16135
  {
16118
16136
  surface: raw['surface'],
16119
- ...(raw['closable'] === false ? { closable: false } : {}),
16120
- ...(raw['active'] === true ? { active: true } : {}),
16137
+ ...(raw['closable'] === false && { closable: false }),
16138
+ ...(raw['active'] === true && { active: true }),
16121
16139
  },
16122
16140
  ];
16123
16141
  }
@@ -16174,11 +16192,14 @@ function sanitizeRpcToastInput(input) {
16174
16192
  id: typeof raw['id'] === 'string' ? raw['id'] : undefined,
16175
16193
  };
16176
16194
  }
16195
+ const NOTIFICATION_KINDS = new Set([
16196
+ 'info',
16197
+ 'success',
16198
+ 'warning',
16199
+ 'error',
16200
+ ]);
16177
16201
  function isNotificationKind(value) {
16178
- return (value === 'info' ||
16179
- value === 'success' ||
16180
- value === 'warning' ||
16181
- value === 'error');
16202
+ return NOTIFICATION_KINDS.has(value);
16182
16203
  }
16183
16204
  function sanitizeRpcMenuItem(item) {
16184
16205
  const raw = (item ?? {});
@@ -16211,29 +16232,267 @@ function sanitizeMenuContext(value) {
16211
16232
  return clean;
16212
16233
  }
16213
16234
 
16214
- const UNCARRIABLE_ARGUMENTS = {
16215
- outcome: 'refused',
16216
- reason: 'invalid-arguments',
16217
- message: 'Arguments must be an object of single values or lists of them; anything else cannot cross the ' +
16218
- 'sandbox boundary as the value it was.',
16219
- };
16220
- function invokeRpcCommand(ctx, id, args) {
16221
- const carried = args === undefined ? undefined : asCommandArguments(args);
16222
- return carried === null
16223
- ? Promise.resolve(UNCARRIABLE_ARGUMENTS)
16224
- : ctx.invokeCommand(String(id), carried);
16235
+ const INPUT_TYPES = ['text', 'date', 'email', 'number', 'password'];
16236
+ function sanitizeOptions(raw) {
16237
+ if (!Array.isArray(raw)) {
16238
+ return [];
16239
+ }
16240
+ return raw
16241
+ .filter((option) => typeof option === 'object' &&
16242
+ option !== null &&
16243
+ typeof option['value'] === 'string' &&
16244
+ typeof option['label'] === 'string')
16245
+ .map((option) => ({ value: option.value, label: option.label }));
16225
16246
  }
16226
-
16227
- /** Multi-provider token: each contribution adds one sandboxed plugin to load. */
16228
- const FRAME_PLUGIN = new InjectionToken('FRAME_PLUGIN');
16229
- function levelOf(plugin) {
16230
- return plugin.level ?? DEFAULT_ISOLATION_LEVEL;
16247
+ function optionalString(value) {
16248
+ return typeof value === 'string' ? value : undefined;
16231
16249
  }
16232
- function signatureOf(plugin) {
16233
- const caps = [...(plugin.capabilities ?? [])].sort().join(',');
16234
- const granted = [...(plugin.granted ?? [])].sort().join(',');
16235
- return `${plugin.entryUrl}|${caps}|${granted}|${plugin.version ?? ''}|${levelOf(plugin)}`;
16250
+ function optionalNumber(value) {
16251
+ return typeof value === 'number' ? value : undefined;
16236
16252
  }
16253
+ function buildTextControl(value, control) {
16254
+ return {
16255
+ kind: 'text',
16256
+ value,
16257
+ inputType: INPUT_TYPES.find((type) => type === control['inputType']),
16258
+ placeholder: optionalString(control['placeholder']),
16259
+ };
16260
+ }
16261
+ function buildSelectControl(pluginId, value, control) {
16262
+ const options = sanitizeOptions(control['options']);
16263
+ if (options.length === 0) {
16264
+ throw new Error(`Sandbox plugin "${pluginId}": a select control needs at least one { value, label } option.`);
16265
+ }
16266
+ return { kind: 'select', value, options };
16267
+ }
16268
+ function buildSliderControl(value, control) {
16269
+ return {
16270
+ kind: 'slider',
16271
+ value,
16272
+ min: optionalNumber(control['min']),
16273
+ max: optionalNumber(control['max']),
16274
+ step: optionalNumber(control['step']),
16275
+ };
16276
+ }
16277
+ function sanitizeControl(pluginId, raw) {
16278
+ const control = (raw ?? {});
16279
+ const kind = control['kind'];
16280
+ const value = control['value'];
16281
+ if (kind === 'toggle' && typeof value === 'boolean') {
16282
+ return { kind, value };
16283
+ }
16284
+ if (kind === 'text' && typeof value === 'string') {
16285
+ return buildTextControl(value, control);
16286
+ }
16287
+ if (kind === 'select' && typeof value === 'string') {
16288
+ return buildSelectControl(pluginId, value, control);
16289
+ }
16290
+ if (kind === 'slider' && typeof value === 'number') {
16291
+ return buildSliderControl(value, control);
16292
+ }
16293
+ throw new Error(`Sandbox plugin "${pluginId}": a settings control must be toggle/text/select/slider with a matching default 'value'.`);
16294
+ }
16295
+ function sanitizeRow(pluginId, raw) {
16296
+ const row = (raw ?? {});
16297
+ if (typeof row['id'] !== 'string' || row['id'].length === 0) {
16298
+ throw new Error(`Sandbox plugin "${pluginId}": every settings row needs a non-empty 'id'.`);
16299
+ }
16300
+ if (typeof row['label'] !== 'string' || row['label'].length === 0) {
16301
+ throw new Error(`Sandbox plugin "${pluginId}": every settings row needs a non-empty 'label'.`);
16302
+ }
16303
+ return {
16304
+ id: row['id'],
16305
+ label: row['label'],
16306
+ description: typeof row['description'] === 'string' ? row['description'] : undefined,
16307
+ control: sanitizeControl(pluginId, row['control']),
16308
+ };
16309
+ }
16310
+ function sanitizeRpcSettingsSection(pluginId, section) {
16311
+ const raw = (section ?? {});
16312
+ if (typeof raw['id'] !== 'string' || raw['id'].length === 0) {
16313
+ throw new Error(`Sandbox plugin "${pluginId}": registerSettingsSection requires a non-empty 'id'.`);
16314
+ }
16315
+ if (typeof raw['title'] !== 'string' || raw['title'].length === 0) {
16316
+ throw new Error(`Sandbox plugin "${pluginId}": registerSettingsSection requires a non-empty 'title'.`);
16317
+ }
16318
+ if (!Array.isArray(raw['rows']) || raw['rows'].length === 0) {
16319
+ throw new Error(`Sandbox plugin "${pluginId}": registerSettingsSection requires at least one row.`);
16320
+ }
16321
+ return {
16322
+ id: raw['id'],
16323
+ title: raw['title'],
16324
+ order: typeof raw['order'] === 'number' ? raw['order'] : undefined,
16325
+ rows: raw['rows'].map((row) => sanitizeRow(pluginId, row)),
16326
+ };
16327
+ }
16328
+ function defaultsOf(wire) {
16329
+ const defaults = {};
16330
+ for (const row of wire.rows) {
16331
+ defaults[row.id] = row.control.value;
16332
+ }
16333
+ return defaults;
16334
+ }
16335
+ function typedOverlay(defaults, raw) {
16336
+ if (!raw) {
16337
+ return defaults;
16338
+ }
16339
+ try {
16340
+ const parsed = JSON.parse(raw);
16341
+ if (typeof parsed !== 'object' || parsed === null) {
16342
+ return defaults;
16343
+ }
16344
+ const merged = { ...defaults };
16345
+ for (const [key, value] of Object.entries(parsed)) {
16346
+ if (Object.hasOwn(defaults, key) && typeof value === typeof defaults[key]) {
16347
+ merged[key] = value;
16348
+ }
16349
+ }
16350
+ return merged;
16351
+ }
16352
+ catch {
16353
+ return defaults;
16354
+ }
16355
+ }
16356
+ function buildFrameSection(deps) {
16357
+ const { pluginId, wire, group, store, sync, notify } = deps;
16358
+ const key = `lw.plugin-settings:${pluginId}:${wire.id}`;
16359
+ const defaults = defaultsOf(wire);
16360
+ const values = signal(typedOverlay(defaults, store.peek?.(key)), /* @ts-ignore */
16361
+ ...(ngDevMode ? [{ debugName: "values" }] : /* istanbul ignore next */ []));
16362
+ const applyStored = (raw) => {
16363
+ values.set(typedOverlay(defaults, raw));
16364
+ notify(wire.id, values());
16365
+ };
16366
+ if (store.peek) {
16367
+ notify(wire.id, values());
16368
+ }
16369
+ else {
16370
+ hydrateAsync(store, key, applyStored);
16371
+ }
16372
+ const disposeSync = sync.register('settings', key, applyStored);
16373
+ const set = (rowId, value) => {
16374
+ values.update((current) => ({ ...current, [rowId]: value }));
16375
+ void store.set(key, JSON.stringify(values()));
16376
+ notify(wire.id, values());
16377
+ };
16378
+ const section = {
16379
+ id: `${pluginId}.${wire.id}`,
16380
+ title: wire.title,
16381
+ group,
16382
+ order: wire.order,
16383
+ rows: wire.rows.map((row) => ({
16384
+ id: `${pluginId}.${wire.id}.${row.id}`,
16385
+ label: row.label,
16386
+ description: row.description,
16387
+ control: hostControl(row, values, set),
16388
+ })),
16389
+ };
16390
+ return { section, disposeSync };
16391
+ }
16392
+ function hostControl(row, values, set) {
16393
+ const control = row.control;
16394
+ switch (control.kind) {
16395
+ case 'toggle': {
16396
+ return {
16397
+ kind: 'toggle',
16398
+ value: () => values()[row.id] === true,
16399
+ set: (value) => set(row.id, value),
16400
+ };
16401
+ }
16402
+ case 'text': {
16403
+ return {
16404
+ kind: 'text',
16405
+ inputType: control.inputType,
16406
+ placeholder: control.placeholder,
16407
+ value: () => String(values()[row.id] ?? ''),
16408
+ set: (value) => set(row.id, value),
16409
+ };
16410
+ }
16411
+ case 'select': {
16412
+ return {
16413
+ kind: 'select',
16414
+ options: control.options,
16415
+ value: () => String(values()[row.id] ?? control.value),
16416
+ set: (value) => set(row.id, value),
16417
+ };
16418
+ }
16419
+ case 'slider': {
16420
+ return {
16421
+ kind: 'slider',
16422
+ min: control.min,
16423
+ max: control.max,
16424
+ step: control.step,
16425
+ value: () => Number(values()[row.id] ?? control.value),
16426
+ set: (value) => set(row.id, value),
16427
+ };
16428
+ }
16429
+ }
16430
+ }
16431
+
16432
+ function frameRpcMethods(deps) {
16433
+ const { pluginId, ctx, origins, watched } = deps;
16434
+ return reportingRefusals({
16435
+ registerSurface: (surface) => {
16436
+ ctx.registerSurface(sanitizeRpcSurface(pluginId, surface, origins));
16437
+ },
16438
+ registerMenuItem: (item) => {
16439
+ ctx.registerMenuItem(sanitizeRpcMenuItem(item));
16440
+ },
16441
+ registerSettingsSection: (section) => {
16442
+ const built = buildFrameSection({
16443
+ pluginId,
16444
+ wire: sanitizeRpcSettingsSection(pluginId, section),
16445
+ group: deps.install.isInstalled(pluginId)
16446
+ ? 'settings.group.community'
16447
+ : 'settings.group.plugins',
16448
+ store: deps.store,
16449
+ sync: deps.sync,
16450
+ notify: (sectionId, values) => deps.notify((remote) => remote.settingsChanged(sectionId, values)),
16451
+ });
16452
+ deps.syncCleanups.push(built.disposeSync);
16453
+ ctx.registerSettingsSection(built.section);
16454
+ },
16455
+ navigateContent: (path) => ctx.navigateContent(path),
16456
+ openContentTab: (input) => {
16457
+ const sanitized = sanitizeRpcTabInput(input);
16458
+ ctx.openContentTab({
16459
+ ...sanitized,
16460
+ onClose: () => deps.notify((remote) => remote.contentTabClosed(sanitized.path)),
16461
+ });
16462
+ },
16463
+ keepContentTab: (path) => ctx.keepContentTab(path),
16464
+ pinContentTab: (path) => ctx.pinContentTab(path),
16465
+ unpinContentTab: (path) => ctx.unpinContentTab(path),
16466
+ closeContentTab: (path) => ctx.closeContentTab(path),
16467
+ revealSurface: (id) => ctx.revealSurface(id),
16468
+ invokeCommand: (id, args) => invokeRpcCommand(ctx, id, args),
16469
+ invocableCommands: () => ctx.invocableCommands(),
16470
+ toast: (input) => ctx.ui.toast(sanitizeRpcToastInput(input)),
16471
+ stateWatch: (key) => deps.watchState(key),
16472
+ stateSet: (key, value) => watched.get(key)?.handle.set(value),
16473
+ stateClear: (key) => watched.get(key)?.handle.clear(),
16474
+ stateUnwatch: (key) => {
16475
+ watched.get(key)?.stop();
16476
+ watched.delete(key);
16477
+ },
16478
+ }, deps.reportRefusal);
16479
+ }
16480
+ function reportingRefusals(methods, report) {
16481
+ const reported = Object.entries(methods).map(([name, method]) => [
16482
+ name,
16483
+ (...args) => {
16484
+ try {
16485
+ return method(...args);
16486
+ }
16487
+ catch (error) {
16488
+ report(error);
16489
+ throw error;
16490
+ }
16491
+ },
16492
+ ]);
16493
+ return Object.fromEntries(reported);
16494
+ }
16495
+
16237
16496
  /**
16238
16497
  * Second {@link PluginRuntime} implementation:
16239
16498
  * runs each plugin in an isolated `<iframe sandbox="allow-scripts">` and hands it `ctx` over **Penpal**
@@ -16291,9 +16550,13 @@ class FramePluginRuntime {
16291
16550
  this.instances.delete(id);
16292
16551
  instance.connection.destroy();
16293
16552
  instance.frame.remove();
16294
- instance.watched.forEach((entry) => entry.stop());
16553
+ for (const entry of instance.watched.values()) {
16554
+ entry.stop();
16555
+ }
16295
16556
  instance.ctx.disposeAll();
16296
- instance.syncCleanups.forEach((cleanup) => cleanup());
16557
+ for (const cleanup of instance.syncCleanups) {
16558
+ cleanup();
16559
+ }
16297
16560
  this.grants.unregister(id);
16298
16561
  this.isolation.unregister(id);
16299
16562
  }
@@ -16305,7 +16568,7 @@ class FramePluginRuntime {
16305
16568
  }
16306
16569
  }
16307
16570
  reconcile(disabled, installed, deployed) {
16308
- const runnable = this.runnablePlugins(installed, deployed);
16571
+ const runnable = runnablePlugins(this.plugins, installed, deployed, this.catalogCap);
16309
16572
  for (const plugin of runnable) {
16310
16573
  this.enablement.register(plugin.id, plugin.name ?? plugin.id);
16311
16574
  const enabled = plugin.provided === true || !disabled.has(plugin.id);
@@ -16323,34 +16586,6 @@ class FramePluginRuntime {
16323
16586
  }
16324
16587
  this.dropUninstalled(runnable);
16325
16588
  }
16326
- runnablePlugins(installed, deployed) {
16327
- const claimed = new Set(this.plugins.map((plugin) => plugin.id));
16328
- const provided = new Set(deployed.map((plugin) => plugin.id));
16329
- const fromCatalog = [];
16330
- for (const plugin of [...deployed, ...installed]) {
16331
- if (claimed.has(plugin.id)) {
16332
- continue;
16333
- }
16334
- const asked = plugin.level ?? DEFAULT_ISOLATION_LEVEL;
16335
- if (exceedsLevel(asked, this.catalogCap)) {
16336
- console.error(`Plugin "${plugin.id}" asks to run ${asked}, which this catalog may not confer ` +
16337
- `(its cap is ${this.catalogCap}). It is not started.`);
16338
- continue;
16339
- }
16340
- claimed.add(plugin.id);
16341
- fromCatalog.push({
16342
- id: plugin.id,
16343
- entryUrl: plugin.entryUrl,
16344
- capabilities: plugin.capabilities,
16345
- name: plugin.name,
16346
- granted: plugin.capabilities ?? [],
16347
- version: plugin.version,
16348
- level: asked,
16349
- provided: provided.has(plugin.id) || undefined,
16350
- });
16351
- }
16352
- return [...this.plugins, ...fromCatalog];
16353
- }
16354
16589
  dropUninstalled(runnable) {
16355
16590
  const known = new Set(runnable.map((plugin) => plugin.id));
16356
16591
  for (const id of this.instances.keys()) {
@@ -16374,7 +16609,19 @@ class FramePluginRuntime {
16374
16609
  const watched = new Map();
16375
16610
  const connection = connect({
16376
16611
  messenger,
16377
- methods: this.reportingRefusals(this.rpcMethods(plugin.id, ctx, syncCleanups, watched, plugin.origins)),
16612
+ methods: frameRpcMethods({
16613
+ pluginId: plugin.id,
16614
+ ctx,
16615
+ origins: plugin.origins,
16616
+ install: this.install,
16617
+ store: this.store,
16618
+ sync: this.sync,
16619
+ syncCleanups,
16620
+ watched,
16621
+ watchState: (key) => this.watchState(plugin.id, ctx, watched, key),
16622
+ notify: (send) => this.notify(plugin.id, send),
16623
+ reportRefusal: (error) => this.refusals.report(error),
16624
+ }),
16378
16625
  });
16379
16626
  this.instances.set(plugin.id, {
16380
16627
  ctx,
@@ -16385,10 +16632,11 @@ class FramePluginRuntime {
16385
16632
  watched,
16386
16633
  });
16387
16634
  connection.promise.catch((error) => {
16388
- if (this.instances.has(plugin.id)) {
16389
- console.error(`Sandbox plugin "${plugin.id}" failed to connect`, error);
16390
- this.deactivate(plugin.id);
16635
+ if (!this.instances.has(plugin.id)) {
16636
+ return;
16391
16637
  }
16638
+ console.error(`Sandbox plugin "${plugin.id}" failed to connect`, error);
16639
+ this.deactivate(plugin.id);
16392
16640
  });
16393
16641
  }
16394
16642
  createFrame(entryUrl, level) {
@@ -16399,71 +16647,9 @@ class FramePluginRuntime {
16399
16647
  frame.setAttribute('aria-hidden', 'true');
16400
16648
  frame.style.display = 'none';
16401
16649
  frame.src = entryUrl;
16402
- document.body.appendChild(frame);
16650
+ document.body.append(frame);
16403
16651
  return frame;
16404
16652
  }
16405
- reportingRefusals(methods) {
16406
- const reported = Object.entries(methods).map(([name, method]) => [
16407
- name,
16408
- (...args) => {
16409
- try {
16410
- return method(...args);
16411
- }
16412
- catch (error) {
16413
- this.refusals.report(error);
16414
- throw error;
16415
- }
16416
- },
16417
- ]);
16418
- return Object.fromEntries(reported);
16419
- }
16420
- rpcMethods(pluginId, ctx, syncCleanups, watched, origins) {
16421
- return {
16422
- registerSurface: (surface) => {
16423
- ctx.registerSurface(sanitizeRpcSurface(pluginId, surface, origins));
16424
- },
16425
- registerMenuItem: (item) => {
16426
- ctx.registerMenuItem(sanitizeRpcMenuItem(item));
16427
- },
16428
- registerSettingsSection: (section) => {
16429
- const built = buildFrameSection({
16430
- pluginId,
16431
- wire: sanitizeRpcSettingsSection(pluginId, section),
16432
- group: this.install.isInstalled(pluginId)
16433
- ? 'settings.group.community'
16434
- : 'settings.group.plugins',
16435
- store: this.store,
16436
- sync: this.sync,
16437
- notify: (sectionId, values) => this.notifySettings(pluginId, sectionId, values),
16438
- });
16439
- syncCleanups.push(built.disposeSync);
16440
- ctx.registerSettingsSection(built.section);
16441
- },
16442
- navigateContent: (path) => ctx.navigateContent(path),
16443
- openContentTab: (input) => {
16444
- const sanitized = sanitizeRpcTabInput(input);
16445
- ctx.openContentTab({
16446
- ...sanitized,
16447
- onClose: () => this.notifyTabClosed(pluginId, sanitized.path),
16448
- });
16449
- },
16450
- keepContentTab: (path) => ctx.keepContentTab(path),
16451
- pinContentTab: (path) => ctx.pinContentTab(path),
16452
- unpinContentTab: (path) => ctx.unpinContentTab(path),
16453
- closeContentTab: (path) => ctx.closeContentTab(path),
16454
- revealSurface: (id) => ctx.revealSurface(id),
16455
- invokeCommand: (id, args) => invokeRpcCommand(ctx, id, args),
16456
- invocableCommands: () => ctx.invocableCommands(),
16457
- toast: (input) => ctx.ui.toast(sanitizeRpcToastInput(input)),
16458
- stateWatch: (key) => this.watchState(pluginId, ctx, watched, key),
16459
- stateSet: (key, value) => watched.get(key)?.handle.set(value),
16460
- stateClear: (key) => watched.get(key)?.handle.clear(),
16461
- stateUnwatch: (key) => {
16462
- watched.get(key)?.stop();
16463
- watched.delete(key);
16464
- },
16465
- };
16466
- }
16467
16653
  watchState(pluginId, ctx, watched, key) {
16468
16654
  if (watched.has(key)) {
16469
16655
  return;
@@ -16472,7 +16658,7 @@ class FramePluginRuntime {
16472
16658
  const ref = effect(() => {
16473
16659
  const value = handle.value();
16474
16660
  const loaded = handle.loaded();
16475
- untracked(() => this.notifyState(pluginId, key, value, loaded));
16661
+ untracked(() => this.notify(pluginId, (remote) => remote.stateChanged(key, value, loaded)));
16476
16662
  }, { ...(ngDevMode ? { debugName: "ref" } : /* istanbul ignore next */ {}), injector: this.injector });
16477
16663
  watched.set(key, {
16478
16664
  handle,
@@ -16482,32 +16668,12 @@ class FramePluginRuntime {
16482
16668
  },
16483
16669
  });
16484
16670
  }
16485
- notifyState(pluginId, key, value, loaded) {
16486
- const instance = this.instances.get(pluginId);
16487
- if (!instance) {
16488
- return;
16489
- }
16490
- void instance.connection.promise
16491
- .then((remote) => remote.stateChanged(key, value, loaded))
16492
- .catch(() => undefined);
16493
- }
16494
- notifyTabClosed(pluginId, path) {
16671
+ notify(pluginId, send) {
16495
16672
  const instance = this.instances.get(pluginId);
16496
16673
  if (!instance) {
16497
16674
  return;
16498
16675
  }
16499
- void instance.connection.promise
16500
- .then((remote) => remote.contentTabClosed(path))
16501
- .catch(() => undefined);
16502
- }
16503
- notifySettings(pluginId, sectionId, values) {
16504
- const instance = this.instances.get(pluginId);
16505
- if (!instance) {
16506
- return;
16507
- }
16508
- void instance.connection.promise
16509
- .then((remote) => remote.settingsChanged(sectionId, values))
16510
- .catch(() => undefined);
16676
+ void instance.connection.promise.then(send).catch(() => undefined);
16511
16677
  }
16512
16678
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.0", ngImport: i0, type: FramePluginRuntime, deps: [], target: i0.ɵɵFactoryTarget.Service });
16513
16679
  static ɵprov = i0.ɵɵngDeclareService({ minVersion: "22.0.0", version: "22.1.0", ngImport: i0, type: FramePluginRuntime });
@@ -16651,12 +16817,12 @@ function availableUpdate(installed, entry) {
16651
16817
  return isNewerVersion(entry.version, installed.version) ? entry : undefined;
16652
16818
  }
16653
16819
  function addedCapabilities(entry, installed) {
16654
- const consented = new Set(installed.capabilities ?? []);
16820
+ const consented = new Set(installed.capabilities);
16655
16821
  return (entry.capabilities ?? []).filter((capability) => !consented.has(capability));
16656
16822
  }
16657
16823
 
16658
16824
  async function confirmUpdate(deps, entry) {
16659
- const installed = deps.installs.find(entry.id);
16825
+ const installed = deps.installs.byId(entry.id);
16660
16826
  if (!installed) {
16661
16827
  return;
16662
16828
  }
@@ -16822,7 +16988,7 @@ class PluginStoreDetail {
16822
16988
  transloco = inject(TranslocoService);
16823
16989
  readme = signal(undefined, /* @ts-ignore */
16824
16990
  ...(ngDevMode ? [{ debugName: "readme" }] : /* istanbul ignore next */ []));
16825
- update = computed(() => availableUpdate(this.installs.find(this.entry().id), this.entry()), /* @ts-ignore */
16991
+ update = computed(() => availableUpdate(this.installs.byId(this.entry().id), this.entry()), /* @ts-ignore */
16826
16992
  ...(ngDevMode ? [{ debugName: "update" }] : /* istanbul ignore next */ []));
16827
16993
  constructor() {
16828
16994
  effect(() => {
@@ -16908,7 +17074,7 @@ class PluginStoreDialog {
16908
17074
  ...(ngDevMode ? [{ debugName: "selectedId" }] : /* istanbul ignore next */ []));
16909
17075
  filtered = computed(() => {
16910
17076
  const list = (this.entries() ?? []).filter((entry) => matchesQuery([entry.name, entry.author, entry.category, entry.description], this.query()));
16911
- return list.sort((a, b) => (b.downloads ?? 0) - (a.downloads ?? 0) || a.name.localeCompare(b.name));
17077
+ return list.toSorted((a, b) => (b.downloads ?? 0) - (a.downloads ?? 0) || a.name.localeCompare(b.name));
16912
17078
  }, /* @ts-ignore */
16913
17079
  ...(ngDevMode ? [{ debugName: "filtered" }] : /* istanbul ignore next */ []));
16914
17080
  selected = computed(() => this.filtered().find((entry) => entry.id === this.selectedId()), /* @ts-ignore */
@@ -16920,7 +17086,7 @@ class PluginStoreDialog {
16920
17086
  void confirmInstall(this.consentDeps, entry);
16921
17087
  }
16922
17088
  hasUpdate(entry) {
16923
- return availableUpdate(this.installs.find(entry.id), entry) !== undefined;
17089
+ return availableUpdate(this.installs.byId(entry.id), entry) !== undefined;
16924
17090
  }
16925
17091
  requestUpdate(entry) {
16926
17092
  void confirmUpdate(this.consentDeps, entry);