@loomweaver/shell 0.7.4 → 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.
@@ -12,7 +12,7 @@ import { filter, map, of, catchError, forkJoin } from 'rxjs';
12
12
  import { DomSanitizer } from '@angular/platform-browser';
13
13
  import { WindowMessenger, connect } from 'penpal';
14
14
  import DOMPurify from 'dompurify';
15
- import { heroMinus, heroArrowsPointingIn, heroArrowTopRightOnSquare, heroArrowsPointingOut, heroArrowDownTray, heroPuzzlePiece, heroMagnifyingGlass, heroQuestionMarkCircle, heroCog6Tooth, heroXCircle, heroExclamationTriangle, heroCheck, heroLockClosed, heroInformationCircle, heroArrowUturnLeft, heroArrowPath, heroArrowsUpDown, heroEye, heroTrash, heroPencilSquare, heroPlus, heroListBullet, heroBars3, heroDocument, heroRectangleGroup, heroXMark, heroChevronDown, heroChevronDoubleRight, heroChevronDoubleLeft, heroComputerDesktop, heroMoon, heroSun } from '@ng-icons/heroicons/outline';
15
+ import { heroMinus, heroArrowsPointingIn, heroArrowTopRightOnSquare, heroArrowsPointingOut, heroArrowDownTray, heroPuzzlePiece, heroMagnifyingGlass, heroQuestionMarkCircle, heroCog6Tooth, heroXCircle, heroExclamationTriangle, heroCheck, heroLockClosed, heroInformationCircle, heroArrowUturnLeft, heroArrowPath, heroArrowsUpDown, heroEye, heroTrash, heroPencilSquare, heroPlus, heroListBullet, heroBars3, heroViewColumns, heroDocument, heroRectangleGroup, heroXMark, heroChevronDown, heroChevronDoubleRight, heroChevronDoubleLeft, heroComputerDesktop, heroMoon, heroSun } from '@ng-icons/heroicons/outline';
16
16
  import { HttpClient, provideHttpClient } from '@angular/common/http';
17
17
  import { SwUpdate, provideServiceWorker } from '@angular/service-worker';
18
18
  import { marked } from 'marked';
@@ -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;
@@ -7854,6 +7930,7 @@ const LOOM_ICONS = {
7854
7930
  workspaces: heroRectangleGroup,
7855
7931
  menu: heroBars3,
7856
7932
  document: heroDocument,
7933
+ openWork: heroViewColumns,
7857
7934
  navigator: heroBars3,
7858
7935
  outline: heroListBullet,
7859
7936
  add: heroPlus,
@@ -8008,7 +8085,7 @@ class CapabilityGrantService {
8008
8085
  })),
8009
8086
  }))
8010
8087
  .filter((entry) => entry.capabilities.length > 0)
8011
- .sort((a, b) => a.pluginId.localeCompare(b.pluginId));
8088
+ .toSorted((a, b) => a.pluginId.localeCompare(b.pluginId));
8012
8089
  }, /* @ts-ignore */
8013
8090
  ...(ngDevMode ? [{ debugName: "permissions" }] : /* istanbul ignore next */ []));
8014
8091
  constructor() {
@@ -8145,7 +8222,7 @@ class IframeSurface {
8145
8222
  ...(ngDevMode ? [{ debugName: "activeTab" }] : /* istanbul ignore next */ []));
8146
8223
  restPath = computed(() => {
8147
8224
  if (!this.ownsRest) {
8148
- return undefined;
8225
+ return;
8149
8226
  }
8150
8227
  return this.hostMounted
8151
8228
  ? this.hostSub()
@@ -8176,7 +8253,9 @@ class IframeSurface {
8176
8253
  queueMicrotask(() => this.push({ ...snapshot, ...this.readResolved() }));
8177
8254
  });
8178
8255
  inject(DestroyRef).onDestroy(() => {
8179
- this.watched.forEach((entry) => entry.stop());
8256
+ for (const entry of this.watched.values()) {
8257
+ entry.stop();
8258
+ }
8180
8259
  this.watched.clear();
8181
8260
  this.visibility?.disconnect();
8182
8261
  this.connection?.destroy();
@@ -8212,12 +8291,12 @@ class IframeSurface {
8212
8291
  this.tabs.keep(this.tabRoot);
8213
8292
  }
8214
8293
  },
8215
- setDirty: (dirty) => this.dirty.set(dirty === true),
8216
- stateWatch: (key) => this.watchState(String(key)),
8217
- stateSet: (key, value) => this.watched.get(String(key))?.handle.set(value),
8218
- 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(),
8219
8298
  stateUnwatch: (key) => {
8220
- const name = String(key);
8299
+ const name = key;
8221
8300
  this.watched.get(name)?.stop();
8222
8301
  this.watched.delete(name);
8223
8302
  },
@@ -8227,7 +8306,9 @@ class IframeSurface {
8227
8306
  .then((remote) => {
8228
8307
  this.remote = remote;
8229
8308
  this.push({ ...this.reactiveState(), ...this.readResolved() });
8230
- 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
+ }
8231
8312
  })
8232
8313
  .catch(() => undefined);
8233
8314
  }
@@ -8263,7 +8344,7 @@ class IframeSurface {
8263
8344
  return;
8264
8345
  }
8265
8346
  this.visibility = new IntersectionObserver((entries) => {
8266
- const last = entries[entries.length - 1];
8347
+ const last = entries.at(-1);
8267
8348
  if (last) {
8268
8349
  this.shown.set(last.isIntersecting);
8269
8350
  }
@@ -8286,19 +8367,15 @@ class IframeSurface {
8286
8367
  theme: this.theme.resolvedTheme(),
8287
8368
  preview: this.isPreview(),
8288
8369
  shown: this.shown(),
8289
- ...(this.instanceId ? { instanceId: this.instanceId } : {}),
8290
- ...(Object.keys(this.routeParams).length > 0
8291
- ? { params: this.routeParams }
8292
- : {}),
8293
- ...(rest === undefined ? {} : { rest }),
8294
- ...(this.sessionGranted()
8295
- ? {
8296
- session: {
8297
- authenticated: this.auth.authenticated(),
8298
- roles: this.auth.roles(),
8299
- },
8300
- }
8301
- : {}),
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
+ }),
8302
8379
  };
8303
8380
  }
8304
8381
  readResolved() {
@@ -8311,19 +8388,19 @@ class IframeSurface {
8311
8388
  return {
8312
8389
  tokens,
8313
8390
  rootFontSize: styles.fontSize,
8314
- ...(Object.keys(icons).length > 0 ? { icons } : {}),
8391
+ ...(Object.keys(icons).length > 0 && { icons }),
8315
8392
  };
8316
8393
  }
8317
8394
  navigateWithinTabRoot(path) {
8318
8395
  if (this.docked) {
8319
8396
  if (isDevMode()) {
8320
- 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. ` +
8321
8398
  `A docked surface has no address of its own; the channel's navigate is confined to a tab ` +
8322
8399
  `root and there is none. Use ctx.navigateContent (the 'navigation' grant) instead.`);
8323
8400
  }
8324
8401
  return;
8325
8402
  }
8326
- const raw = String(path);
8403
+ const raw = path;
8327
8404
  const suffix = suffixOf(raw);
8328
8405
  const target = normalizePath(raw);
8329
8406
  if (target !== this.tabRoot && !target.startsWith(this.tabRoot + '/')) {
@@ -8392,8 +8469,8 @@ function syntheticDockedRoute(view, instanceId, params = {}) {
8392
8469
  url: [],
8393
8470
  params,
8394
8471
  data: {
8395
- ...(view.iframe !== undefined ? { iframe: view.iframe } : {}),
8396
- ...(view.pluginId ? { pluginId: view.pluginId } : {}),
8472
+ ...(view.iframe !== undefined && { iframe: view.iframe }),
8473
+ ...(view.pluginId && { pluginId: view.pluginId }),
8397
8474
  docked: true,
8398
8475
  instanceId,
8399
8476
  },
@@ -8409,13 +8486,13 @@ function syntheticRouteFor(route, path, options = {}) {
8409
8486
  url: segments.map((segment) => new UrlSegment(segment, {})),
8410
8487
  params,
8411
8488
  data: {
8412
- ...('iframe' in route ? { iframe: route.iframe } : {}),
8413
- ...('container' in route ? { container: route.container } : {}),
8414
- ...(route.pluginId ? { pluginId: route.pluginId } : {}),
8415
- ...(route.rest === true ? { rest: true } : {}),
8416
- ...(sub ? { sub } : {}),
8417
- ...(options.urlDriven ? { urlDriven: true } : {}),
8418
- ...(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 }),
8419
8496
  },
8420
8497
  });
8421
8498
  }
@@ -8651,14 +8728,12 @@ class ContentSecondaryPane {
8651
8728
  mountParams = computed(() => {
8652
8729
  const ctx = this.containerCtx;
8653
8730
  if (!ctx) {
8654
- return undefined;
8731
+ return;
8655
8732
  }
8656
8733
  const match = containerChildForPath(this.registry.contentRoutes(), this.registry.views(), this.path());
8657
8734
  return {
8658
8735
  ...ctx.params,
8659
- ...(match
8660
- ? paramsOfPattern(match.declaration.segment ?? '', match.segmentPath)
8661
- : {}),
8736
+ ...(match && paramsOfPattern(match.declaration.segment ?? '', match.segmentPath)),
8662
8737
  };
8663
8738
  }, /* @ts-ignore */
8664
8739
  ...(ngDevMode ? [{ debugName: "mountParams" }] : /* istanbul ignore next */ []));
@@ -8690,12 +8765,12 @@ class ContentSecondaryPane {
8690
8765
  ...(ngDevMode ? [{ debugName: "activeRoute" }] : /* istanbul ignore next */ []));
8691
8766
  iframeSurface = computed(() => {
8692
8767
  const route = this.activeRoute();
8693
- return route?.iframe !== undefined
8694
- ? {
8768
+ return route?.iframe === undefined
8769
+ ? null
8770
+ : {
8695
8771
  component: IframeSurface,
8696
8772
  injector: this.injectorFor(route, this.path(), this.surfaceKey()),
8697
- }
8698
- : null;
8773
+ };
8699
8774
  }, /* @ts-ignore */
8700
8775
  ...(ngDevMode ? [{ debugName: "iframeSurface" }] : /* istanbul ignore next */ []));
8701
8776
  surface = computed(() => {
@@ -9147,11 +9222,11 @@ class PaneView {
9147
9222
  ];
9148
9223
  }
9149
9224
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.0", ngImport: i0, type: PaneView, deps: [], target: i0.ɵɵFactoryTarget.Component });
9150
- 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" }] });
9151
9226
  }
9152
9227
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.0", ngImport: i0, type: PaneView, decorators: [{
9153
9228
  type: Component,
9154
- 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" }]
9155
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 }] }] } });
9156
9231
 
9157
9232
  class PaneDropZones {
@@ -9189,13 +9264,12 @@ class PaneDropZones {
9189
9264
  ...(ngDevMode ? [{ debugName: "accepts" }] : /* istanbul ignore next */ []));
9190
9265
  constructor() {
9191
9266
  effect((onCleanup) => {
9192
- const ids = this.fills()
9193
- ? this.accepts()
9194
- ? [this.fillZoneId()]
9195
- : []
9196
- : this.edges().map((edge) => this.zoneId(edge));
9197
- const disposers = ids.map((id) => this.drag.registerZone(id));
9198
- 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
+ });
9199
9273
  });
9200
9274
  }
9201
9275
  has(edge) {
@@ -9231,8 +9305,14 @@ class PaneDropZones {
9231
9305
  this.paneMove.moveToEdge(source, String(event.item.data ?? ''), { dock: this.dock(), paneId: this.paneId() }, edge);
9232
9306
  }
9233
9307
  }
9308
+ zoneIds() {
9309
+ if (!this.fills()) {
9310
+ return this.edges().map((edge) => this.zoneId(edge));
9311
+ }
9312
+ return this.accepts() ? [this.fillZoneId()] : [];
9313
+ }
9234
9314
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.0", ngImport: i0, type: PaneDropZones, deps: [], target: i0.ɵɵFactoryTarget.Component });
9235
- 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"] }] });
9236
9316
  }
9237
9317
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.0", ngImport: i0, type: PaneDropZones, decorators: [{
9238
9318
  type: Component,
@@ -9243,7 +9323,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.0", ngImpor
9243
9323
  '[style.grid-template-columns]': '"1fr 2fr 1fr"',
9244
9324
  '[style.grid-template-rows]': '"1fr 2fr 1fr"',
9245
9325
  '[attr.aria-hidden]': 'true',
9246
- }, 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" }]
9247
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 }] }] } });
9248
9328
 
9249
9329
  class PaneMinimizedStrip {
@@ -9316,18 +9396,18 @@ class PaneSplitHandle {
9316
9396
  return;
9317
9397
  }
9318
9398
  const rect = parent.getBoundingClientRect();
9319
- const el = this.host.nativeElement;
9320
- el.setPointerCapture(event.pointerId);
9399
+ const element = this.host.nativeElement;
9400
+ element.setPointerCapture(event.pointerId);
9321
9401
  event.preventDefault();
9322
9402
  const move = (e) => this.ratioStream.emit(this.fraction(e, rect));
9323
9403
  const up = (e) => {
9324
- el.releasePointerCapture(e.pointerId);
9325
- el.removeEventListener('pointermove', move);
9326
- el.removeEventListener('pointerup', up);
9404
+ element.releasePointerCapture(e.pointerId);
9405
+ element.removeEventListener('pointermove', move);
9406
+ element.removeEventListener('pointerup', up);
9327
9407
  this.ratioCommit.emit();
9328
9408
  };
9329
- el.addEventListener('pointermove', move);
9330
- el.addEventListener('pointerup', up);
9409
+ element.addEventListener('pointermove', move);
9410
+ element.addEventListener('pointerup', up);
9331
9411
  }
9332
9412
  onKeydown(event) {
9333
9413
  const step = event.shiftKey ? STEP_COARSE$1 : STEP$1;
@@ -9634,11 +9714,12 @@ function parseWidths(raw) {
9634
9714
  }
9635
9715
  const result = {};
9636
9716
  for (const [key, value] of Object.entries(parsed)) {
9637
- if (typeof value === 'number' && Number.isFinite(value)) {
9638
- const clamped = clampWidth(value);
9639
- if (clamped !== DEFAULT_PANEL_WIDTH) {
9640
- result[key] = clamped;
9641
- }
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;
9642
9723
  }
9643
9724
  }
9644
9725
  return result;
@@ -9741,20 +9822,25 @@ class PanelSplitter {
9741
9822
  const step = (event.shiftKey ? STEP_COARSE : STEP) * this.edgeSign();
9742
9823
  let next;
9743
9824
  switch (event.key) {
9744
- case 'ArrowRight':
9825
+ case 'ArrowRight': {
9745
9826
  next = this.width() + step;
9746
9827
  break;
9747
- case 'ArrowLeft':
9828
+ }
9829
+ case 'ArrowLeft': {
9748
9830
  next = this.width() - step;
9749
9831
  break;
9750
- case 'Home':
9832
+ }
9833
+ case 'Home': {
9751
9834
  next = this.size.minWidth;
9752
9835
  break;
9753
- case 'End':
9836
+ }
9837
+ case 'End': {
9754
9838
  next = this.size.maxWidth;
9755
9839
  break;
9756
- default:
9840
+ }
9841
+ default: {
9757
9842
  return;
9843
+ }
9758
9844
  }
9759
9845
  event.preventDefault();
9760
9846
  this.size.setWidth(this.regionId(), next);
@@ -9811,7 +9897,7 @@ class ShellPanel {
9811
9897
  activeView = computed(() => {
9812
9898
  const path = this.activePath();
9813
9899
  if (!path?.startsWith(VIEW_PANE_PREFIX)) {
9814
- return undefined;
9900
+ return;
9815
9901
  }
9816
9902
  return viewForPanePath(this.registry.views(), path);
9817
9903
  }, /* @ts-ignore */
@@ -9825,7 +9911,7 @@ class ShellPanel {
9825
9911
  ...(ngDevMode ? [{ debugName: "activeContentPath" }] : /* istanbul ignore next */ []));
9826
9912
  actions = computed(() => [...(this.activeView()?.actions ?? [])]
9827
9913
  .filter((action) => this.auth.visible(action.access))
9828
- .sort((a, b) => (a.order ?? 0) - (b.order ?? 0)), /* @ts-ignore */
9914
+ .toSorted((a, b) => (a.order ?? 0) - (b.order ?? 0)), /* @ts-ignore */
9829
9915
  ...(ngDevMode ? [{ debugName: "actions" }] : /* istanbul ignore next */ []));
9830
9916
  panelPaneOptions = PANEL_PANE_OPTIONS;
9831
9917
  primaryScope = computed(() => paneRetentionScope(this.region().id, this.paneTree.primaryId(this.region().id)), /* @ts-ignore */
@@ -10119,10 +10205,14 @@ function registerTabContextMenu(registry, tabs, paneMove, popout, shell) {
10119
10205
  },
10120
10206
  ];
10121
10207
  const registered = new Set(commands.map((command) => command.id));
10122
- commands.forEach((command) => registry.addCommand({ ...command, paletteHidden: true }));
10123
- items
10124
- .filter((item) => item.command !== undefined && registered.has(item.command))
10125
- .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
+ }
10126
10216
  }
10127
10217
 
10128
10218
  class ContentArea {
@@ -10328,7 +10418,7 @@ class ContentGrid {
10328
10418
  }, /* @ts-ignore */
10329
10419
  ...(ngDevMode ? [{ debugName: "tree" }] : /* istanbul ignore next */ []));
10330
10420
  constructor() {
10331
- const doc = inject(DOCUMENT);
10421
+ const document_ = inject(DOCUMENT);
10332
10422
  const onKeydown = (event) => {
10333
10423
  if (event.key === 'Escape') {
10334
10424
  this.chrome.restore();
@@ -10338,8 +10428,8 @@ class ContentGrid {
10338
10428
  if (!this.maximized()) {
10339
10429
  return;
10340
10430
  }
10341
- doc.addEventListener('keydown', onKeydown);
10342
- onCleanup(() => doc.removeEventListener('keydown', onKeydown));
10431
+ document_.addEventListener('keydown', onKeydown);
10432
+ onCleanup(() => document_.removeEventListener('keydown', onKeydown));
10343
10433
  });
10344
10434
  effect(() => {
10345
10435
  if (!this.layout.isSplit(CONTENT_DOCK)) {
@@ -10512,8 +10602,8 @@ class DialogOutlet {
10512
10602
  if (!this.dialogs().length || !panels.length) {
10513
10603
  return;
10514
10604
  }
10515
- const top = panels[panels.length - 1].nativeElement;
10516
- if (top.contains(this.document.activeElement)) {
10605
+ const top = panels.at(-1)?.nativeElement;
10606
+ if (!top || top.contains(this.document.activeElement)) {
10517
10607
  return;
10518
10608
  }
10519
10609
  (top.querySelector('[data-lw-autofocus]') ?? top).focus();
@@ -10542,10 +10632,10 @@ class DialogOutlet {
10542
10632
  return;
10543
10633
  }
10544
10634
  const first = focusables[0];
10545
- const last = focusables[focusables.length - 1];
10635
+ const last = focusables.at(-1);
10546
10636
  const active = this.document.activeElement;
10547
10637
  if (backward && active === first) {
10548
- last.focus();
10638
+ last?.focus();
10549
10639
  event.preventDefault();
10550
10640
  }
10551
10641
  else if (!backward && active === last) {
@@ -10617,7 +10707,7 @@ class DialogOutlet {
10617
10707
  }
10618
10708
  topPanel() {
10619
10709
  const panels = this.panels();
10620
- return panels.length ? panels[panels.length - 1].nativeElement : undefined;
10710
+ return panels.at(-1)?.nativeElement;
10621
10711
  }
10622
10712
  focusable(root) {
10623
10713
  const selector = 'button:not(:disabled), a[href], input:not(:disabled), select:not(:disabled), textarea:not(:disabled), [tabindex]:not([tabindex="-1"])';
@@ -10625,7 +10715,7 @@ class DialogOutlet {
10625
10715
  }
10626
10716
  top() {
10627
10717
  const list = this.dialogs();
10628
- return list[list.length - 1];
10718
+ return list.at(-1);
10629
10719
  }
10630
10720
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.0", ngImport: i0, type: DialogOutlet, deps: [], target: i0.ɵɵFactoryTarget.Component });
10631
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" }] });
@@ -10775,7 +10865,7 @@ class SettingsRegistry {
10775
10865
  return this.sections()
10776
10866
  .map((section) => visibleSection(section, omitted))
10777
10867
  .filter((section) => section !== null)
10778
- .sort((a, b) => (a.order ?? 0) - (b.order ?? 0));
10868
+ .toSorted((a, b) => (a.order ?? 0) - (b.order ?? 0));
10779
10869
  }, /* @ts-ignore */
10780
10870
  ...(ngDevMode ? [{ debugName: "all" }] : /* istanbul ignore next */ []));
10781
10871
  register(section) {
@@ -11015,6 +11105,13 @@ function provideTranslationNamespaces(...namespaces) {
11015
11105
  /** Directory the distribution serves its overlay bundles from, without a trailing slash. */
11016
11106
  const TRANSLATION_OVERRIDES = new InjectionToken('TRANSLATION_OVERRIDES');
11017
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
+ }
11018
11115
  /**
11019
11116
  * Load `<basePath>/<lang>.json` and merge it over everything else **key by key**, so a product
11020
11117
  * can reword the shell in its own house language ("Save as" rather than "Save as new") without
@@ -11034,7 +11131,7 @@ const DEFAULT_OVERRIDES_PATH = '/i18n/overrides';
11034
11131
  * nothing ships is dev-warned too, since a typo there would otherwise be a string that never appears.
11035
11132
  */
11036
11133
  function provideTranslationOverrides(basePath = DEFAULT_OVERRIDES_PATH) {
11037
- const normalized = basePath.replace(/\/+$/, '');
11134
+ const normalized = withoutTrailingSlashes(basePath);
11038
11135
  if (normalized === '') {
11039
11136
  throw new Error('provideTranslationOverrides() needs a directory to load overlays from; ' +
11040
11137
  `pass one or omit the argument for "${DEFAULT_OVERRIDES_PATH}".`);
@@ -11086,7 +11183,9 @@ class TranslocoHttpLoader {
11086
11183
  return forkJoin([host$, ...namespaced$, this.overrides$(lang)]).pipe(map(([host, ...rest]) => {
11087
11184
  const overlay = rest.pop();
11088
11185
  const merged = { ...host };
11089
- this.namespaces.forEach((name, index) => (merged[name] = rest[index]));
11186
+ for (const [index, name] of this.namespaces.entries()) {
11187
+ merged[name] = rest[index];
11188
+ }
11090
11189
  return this.applyOverrides(merged, overlay, lang);
11091
11190
  }));
11092
11191
  }
@@ -11157,9 +11256,7 @@ class LwIconElement extends HTMLElement {
11157
11256
  this.render();
11158
11257
  }
11159
11258
  attributeChangedCallback() {
11160
- if (this.isConnected) {
11161
- this.render();
11162
- }
11259
+ this.refresh();
11163
11260
  }
11164
11261
  /**
11165
11262
  * Re-draws from the registry without changing the name. A sandboxed surface receives the product's
@@ -11266,7 +11363,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.0", ngImpor
11266
11363
  // GENERATED — do not edit by hand.
11267
11364
  // Written by tools/stamp-version.mjs from <Version> in Directory.Build.props.
11268
11365
  // Single source of truth: Directory.Build.props (bump via scripts/bump-version.sh).
11269
- const APP_VERSION = '0.7.4';
11366
+ const APP_VERSION = '0.7.6';
11270
11367
 
11271
11368
  /**
11272
11369
  * The running build's version, sourced from `<Version>` in Directory.Build.props
@@ -11482,7 +11579,7 @@ class UpdateService {
11482
11579
  await bestEffort(async () => {
11483
11580
  const registrations = (await container?.getRegistrations()) ?? [];
11484
11581
  await Promise.all(registrations
11485
- .filter(isShellWorker)
11582
+ .filter((registration) => isShellWorker(registration))
11486
11583
  .map((registration) => registration.unregister()));
11487
11584
  });
11488
11585
  await bestEffort(async () => {
@@ -11490,14 +11587,14 @@ class UpdateService {
11490
11587
  const keys = (await storage?.keys()) ?? [];
11491
11588
  await Promise.all(keys
11492
11589
  .filter((key) => key.startsWith(WORKER_CACHE_PREFIX))
11493
- .map((key) => storage?.delete(key)));
11590
+ .map(async (key) => storage?.delete(key)));
11494
11591
  });
11495
11592
  }
11496
11593
  onVersionEvent(event) {
11497
11594
  if (event.type === 'VERSION_READY') {
11498
11595
  this.onUpdateReady();
11499
11596
  }
11500
- if (event.type === 'VERSION_INSTALLATION_FAILED') {
11597
+ else if (event.type === 'VERSION_INSTALLATION_FAILED') {
11501
11598
  this.onUpdateFailed();
11502
11599
  }
11503
11600
  }
@@ -11861,7 +11958,7 @@ class CommandInvocationService {
11861
11958
  .filter((entry) => entry.command.callable === true &&
11862
11959
  this.reachable(entry, callerId, granted))
11863
11960
  .map((entry) => this.describe(entry.command))
11864
- .sort((a, b) => a.id.localeCompare(b.id));
11961
+ .toSorted((a, b) => a.id.localeCompare(b.id));
11865
11962
  }
11866
11963
  async invoke(callerId, granted, id, args) {
11867
11964
  const entry = this.registry
@@ -12181,17 +12278,21 @@ class LwTooltipElement extends HTMLElement {
12181
12278
  const centerX = t.left + t.width / 2 - b.width / 2;
12182
12279
  const centerY = t.top + t.height / 2 - b.height / 2;
12183
12280
  switch (this.position) {
12184
- case 'bottom':
12281
+ case 'bottom': {
12185
12282
  [left, top] = [centerX, t.bottom + TOOLTIP_GAP];
12186
12283
  break;
12187
- case 'left':
12284
+ }
12285
+ case 'left': {
12188
12286
  [left, top] = [t.left - b.width - TOOLTIP_GAP, centerY];
12189
12287
  break;
12190
- case 'right':
12288
+ }
12289
+ case 'right': {
12191
12290
  [left, top] = [t.right + TOOLTIP_GAP, centerY];
12192
12291
  break;
12193
- default:
12292
+ }
12293
+ default: {
12194
12294
  [left, top] = [centerX, t.top - b.height - TOOLTIP_GAP];
12295
+ }
12195
12296
  }
12196
12297
  }
12197
12298
  else {
@@ -12216,10 +12317,11 @@ class LwTooltipElement extends HTMLElement {
12216
12317
  }
12217
12318
  }
12218
12319
  clearTimer() {
12219
- if (this.showTimer !== undefined) {
12220
- clearTimeout(this.showTimer);
12221
- this.showTimer = undefined;
12320
+ if (this.showTimer === undefined) {
12321
+ return;
12222
12322
  }
12323
+ clearTimeout(this.showTimer);
12324
+ this.showTimer = undefined;
12223
12325
  }
12224
12326
  }
12225
12327
  /** Registers `<lw-tooltip>` once (idempotent) — called from {@link provideShell} at bootstrap. */
@@ -12230,11 +12332,7 @@ function defineLwTooltip() {
12230
12332
  }
12231
12333
  }
12232
12334
 
12233
- const LW_SELECT_TAG = 'lw-select';
12234
12335
  const LW_OPTION_TAG = 'lw-option';
12235
- const LW_SELECT_CHANGE = 'lw-select-change';
12236
- let nextSelectId = 0;
12237
- const TYPEAHEAD_RESET_MS = 500;
12238
12336
  class LwOptionElement extends HTMLElement {
12239
12337
  get value() {
12240
12338
  return this.getAttribute('value');
@@ -12253,6 +12351,87 @@ class LwOptionElement extends HTMLElement {
12253
12351
  upgradeElementProperty(this, 'icon');
12254
12352
  }
12255
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;
12256
12435
  class LwSelectElement extends HTMLElement {
12257
12436
  static observedAttributes = [
12258
12437
  'value',
@@ -12297,10 +12476,7 @@ class LwSelectElement extends HTMLElement {
12297
12476
  if (!this.trigger) {
12298
12477
  return;
12299
12478
  }
12300
- if (name === 'value' ||
12301
- name === 'label' ||
12302
- name === 'placeholder' ||
12303
- name === 'disabled') {
12479
+ if (LwSelectElement.observedAttributes.includes(name)) {
12304
12480
  this.syncTrigger();
12305
12481
  }
12306
12482
  if (name === 'disabled' && this.hasAttribute('disabled')) {
@@ -12320,51 +12496,34 @@ class LwSelectElement extends HTMLElement {
12320
12496
  attributes: true,
12321
12497
  });
12322
12498
  }
12323
- write(fn) {
12499
+ write(function_) {
12324
12500
  this.observer?.disconnect();
12325
12501
  try {
12326
- fn();
12502
+ function_();
12327
12503
  }
12328
12504
  finally {
12329
12505
  this.observe();
12330
12506
  }
12331
12507
  }
12332
12508
  choices() {
12333
- return [...this.querySelectorAll(LW_OPTION_TAG)].map((option) => ({
12334
- value: option.getAttribute('value') ?? '',
12335
- label: (option.textContent ?? '').trim(),
12336
- icon: option.getAttribute('icon'),
12337
- disabled: option.hasAttribute('disabled'),
12338
- }));
12509
+ return readChoices(this);
12339
12510
  }
12340
12511
  selectedChoice() {
12341
12512
  const value = this.value;
12342
12513
  return this.choices().find((choice) => choice.value === value);
12343
12514
  }
12344
12515
  buildControl() {
12345
- const trigger = document.createElement('button');
12346
- trigger.type = 'button';
12347
- trigger.className = 'lw-select-trigger';
12348
- trigger.setAttribute('aria-haspopup', 'listbox');
12349
- trigger.setAttribute('aria-expanded', 'false');
12350
- trigger.setAttribute('aria-controls', this.listboxId);
12351
- trigger.style.setProperty('anchor-name', this.anchorName);
12352
- trigger.addEventListener('click', () => this.toggle());
12353
- trigger.addEventListener('keydown', (event) => this.onTriggerKeydown(event));
12354
- const valueSlot = document.createElement('span');
12355
- valueSlot.className = 'lw-select-value';
12356
- const chevron = document.createElement('span');
12357
- chevron.className = 'lw-select-chevron';
12358
- chevron.setAttribute('aria-hidden', 'true');
12359
- chevron.textContent = '▾';
12360
- trigger.append(valueSlot, chevron);
12361
- const listbox = document.createElement('div');
12362
- listbox.id = this.listboxId;
12363
- listbox.className = 'lw-select-listbox';
12364
- listbox.setAttribute('role', 'listbox');
12365
- listbox.hidden = true;
12366
- listbox.style.setProperty('position-anchor', this.anchorName);
12367
- 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
+ });
12368
12527
  this.append(trigger, listbox);
12369
12528
  this.trigger = trigger;
12370
12529
  this.valueSlot = valueSlot;
@@ -12379,22 +12538,13 @@ class LwSelectElement extends HTMLElement {
12379
12538
  const label = this.getAttribute('label');
12380
12539
  const selected = this.selectedChoice();
12381
12540
  const text = selected?.label ?? this.getAttribute('placeholder') ?? '';
12382
- const icon = selected?.icon ?? null;
12383
12541
  this.write(() => {
12384
12542
  trigger.disabled = this.hasAttribute('disabled');
12385
12543
  if (label !== null) {
12386
12544
  trigger.setAttribute('aria-label', label);
12387
12545
  this.listbox?.setAttribute('aria-label', label);
12388
12546
  }
12389
- valueSlot.textContent = '';
12390
- if (icon) {
12391
- const glyph = document.createElement('span');
12392
- glyph.className = 'lw-select-glyph';
12393
- glyph.setAttribute('aria-hidden', 'true');
12394
- glyph.textContent = icon;
12395
- valueSlot.append(glyph);
12396
- }
12397
- valueSlot.append(document.createTextNode(text));
12547
+ fillValueSlot(valueSlot, text, selected?.icon ?? null);
12398
12548
  });
12399
12549
  }
12400
12550
  toggle() {
@@ -12420,7 +12570,9 @@ class LwSelectElement extends HTMLElement {
12420
12570
  const choices = this.choices();
12421
12571
  const selected = choices.findIndex((choice) => choice.value === this.value);
12422
12572
  this.setActive(Math.max(0, selected));
12423
- document.addEventListener('pointerdown', this.onOutsidePointer, true);
12573
+ document.addEventListener('pointerdown', this.onOutsidePointer, {
12574
+ capture: true,
12575
+ });
12424
12576
  }
12425
12577
  close(refocusTrigger = true) {
12426
12578
  const listbox = this.listbox;
@@ -12440,94 +12592,83 @@ class LwSelectElement extends HTMLElement {
12440
12592
  }
12441
12593
  }
12442
12594
  renderOptions() {
12443
- if (!this.listbox) {
12595
+ const listbox = this.listbox;
12596
+ if (!listbox) {
12444
12597
  return;
12445
12598
  }
12446
- const items = this.choices().map((choice, index) => {
12447
- const option = document.createElement('div');
12448
- option.className = 'lw-select-option';
12449
- option.setAttribute('role', 'option');
12450
- option.id = `${this.listboxId}-opt-${index}`;
12451
- option.dataset['value'] = choice.value;
12452
- option.setAttribute('aria-selected', String(choice.value === this.value));
12453
- option.tabIndex = -1;
12454
- if (choice.disabled) {
12455
- option.setAttribute('aria-disabled', 'true');
12456
- }
12457
- if (choice.icon) {
12458
- const glyph = document.createElement('span');
12459
- glyph.className = 'lw-select-glyph';
12460
- glyph.setAttribute('aria-hidden', 'true');
12461
- glyph.textContent = choice.icon;
12462
- option.append(glyph);
12463
- }
12464
- option.append(document.createTextNode(choice.label));
12465
- option.addEventListener('click', () => {
12466
- if (!choice.disabled) {
12467
- this.commit(choice.value);
12468
- }
12469
- });
12470
- option.addEventListener('pointermove', () => this.setActive(index));
12471
- return option;
12472
- });
12473
- const listbox = this.listbox;
12474
- 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));
12475
12607
  }
12476
12608
  setActive(index) {
12477
12609
  if (!this.listbox) {
12478
12610
  return;
12479
12611
  }
12480
- const options = [...this.listbox.children];
12481
- if (options.length === 0) {
12612
+ const rows = [...this.listbox.children];
12613
+ if (rows.length === 0) {
12482
12614
  return;
12483
12615
  }
12484
- this.activeIndex = Math.max(0, Math.min(index, options.length - 1));
12616
+ this.activeIndex = Math.max(0, Math.min(index, rows.length - 1));
12485
12617
  this.write(() => {
12486
- options.forEach((option, i) => {
12487
- option.classList.toggle('is-active', i === this.activeIndex);
12488
- option.tabIndex = i === this.activeIndex ? 0 : -1;
12489
- });
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
+ }
12490
12622
  });
12491
- const active = options[this.activeIndex];
12623
+ const active = rows[this.activeIndex];
12492
12624
  active.focus();
12493
12625
  active.scrollIntoView?.({ block: 'nearest' });
12494
12626
  }
12495
12627
  onTriggerKeydown(event) {
12496
- if (['ArrowDown', 'ArrowUp', 'Enter', ' '].includes(event.key)) {
12497
- event.preventDefault();
12498
- this.openListbox();
12628
+ if (!['ArrowDown', 'ArrowUp', 'Enter', ' '].includes(event.key)) {
12629
+ return;
12499
12630
  }
12631
+ event.preventDefault();
12632
+ this.openListbox();
12500
12633
  }
12501
12634
  onListboxKeydown(event) {
12502
12635
  const last = this.choices().length - 1;
12503
12636
  switch (event.key) {
12504
- case 'ArrowDown':
12637
+ case 'ArrowDown': {
12505
12638
  this.setActive(this.activeIndex >= last ? 0 : this.activeIndex + 1);
12506
12639
  break;
12507
- case 'ArrowUp':
12640
+ }
12641
+ case 'ArrowUp': {
12508
12642
  this.setActive(this.activeIndex <= 0 ? last : this.activeIndex - 1);
12509
12643
  break;
12510
- case 'Home':
12644
+ }
12645
+ case 'Home': {
12511
12646
  this.setActive(0);
12512
12647
  break;
12513
- case 'End':
12648
+ }
12649
+ case 'End': {
12514
12650
  this.setActive(last);
12515
12651
  break;
12652
+ }
12516
12653
  case 'Enter':
12517
- case ' ':
12654
+ case ' ': {
12518
12655
  this.commitActive();
12519
12656
  break;
12520
- case 'Escape':
12657
+ }
12658
+ case 'Escape': {
12521
12659
  this.close();
12522
12660
  break;
12523
- case 'Tab':
12661
+ }
12662
+ case 'Tab': {
12524
12663
  this.close(false);
12525
12664
  return;
12526
- default:
12665
+ }
12666
+ default: {
12527
12667
  if (event.key.length === 1) {
12528
12668
  this.onTypeahead(event.key);
12529
12669
  }
12530
12670
  return;
12671
+ }
12531
12672
  }
12532
12673
  event.preventDefault();
12533
12674
  }
@@ -12539,7 +12680,7 @@ class LwSelectElement extends HTMLElement {
12539
12680
  this.typeaheadTimer = setTimeout(() => (this.typeahead = ''), TYPEAHEAD_RESET_MS);
12540
12681
  const match = this.choices().findIndex((choice) => !choice.disabled &&
12541
12682
  choice.label.toLowerCase().startsWith(this.typeahead));
12542
- if (match >= 0) {
12683
+ if (match !== -1) {
12543
12684
  this.setActive(match);
12544
12685
  }
12545
12686
  }
@@ -12688,10 +12829,11 @@ class LwButtonElement extends HTMLElement {
12688
12829
  }
12689
12830
  }
12690
12831
  onKeydown = (event) => {
12691
- if ((event.key === 'Enter' || event.key === ' ') && !this.disabled) {
12692
- event.preventDefault();
12693
- this.click();
12832
+ if (!(event.key === 'Enter' || event.key === ' ') || this.disabled) {
12833
+ return;
12694
12834
  }
12835
+ event.preventDefault();
12836
+ this.click();
12695
12837
  };
12696
12838
  render() {
12697
12839
  const stale = [...this.classList].filter((cls) => cls.startsWith('lw-btn'));
@@ -12870,7 +13012,7 @@ class ViewVisibilityService {
12870
13012
  return this.stash
12871
13013
  .keyedInstances()
12872
13014
  .filter((entry) => !entry.key.startsWith(CONTAINER_DOCK_PREFIX) &&
12873
- entry.key.split('|')[1] === path)
13015
+ entry.key.split('|', 2)[1] === path)
12874
13016
  .map((entry) => entry.instance);
12875
13017
  }
12876
13018
  removeTabs(path) {
@@ -12942,10 +13084,11 @@ class RailWorkspaceEntries {
12942
13084
  reconcile() {
12943
13085
  const wanted = this.wantedItems();
12944
13086
  for (const [id, registration] of this.registered) {
12945
- if (!wanted.has(id)) {
12946
- registration.disposable.dispose();
12947
- this.registered.delete(id);
13087
+ if (wanted.has(id)) {
13088
+ continue;
12948
13089
  }
13090
+ registration.disposable.dispose();
13091
+ this.registered.delete(id);
12949
13092
  }
12950
13093
  for (const [id, item] of wanted) {
12951
13094
  const current = this.registered.get(id);
@@ -13187,10 +13330,10 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.0", ngImpor
13187
13330
  type: Service
13188
13331
  }] });
13189
13332
  function installCompositionReport(report) {
13190
- if (typeof window === 'undefined') {
13333
+ if (globalThis.window === undefined) {
13191
13334
  return;
13192
13335
  }
13193
- const host = window;
13336
+ const host = globalThis;
13194
13337
  if (host['loomweaver'] !== undefined) {
13195
13338
  return;
13196
13339
  }
@@ -13261,7 +13404,7 @@ class PluginEnablementService {
13261
13404
  const disabled = this.disabledSet();
13262
13405
  return [...this.names().entries()]
13263
13406
  .map(([id, name]) => ({ id, name, enabled: !disabled.has(id) }))
13264
- .sort((a, b) => a.name.localeCompare(b.name));
13407
+ .toSorted((a, b) => a.name.localeCompare(b.name));
13265
13408
  }, /* @ts-ignore */
13266
13409
  ...(ngDevMode ? [{ debugName: "plugins" }] : /* istanbul ignore next */ []));
13267
13410
  constructor() {
@@ -13415,7 +13558,7 @@ function parseCatalogEntry(raw) {
13415
13558
  function dedupeById(items) {
13416
13559
  const result = [];
13417
13560
  for (const item of items) {
13418
- if (item && !result.some((existing) => existing.id === item.id)) {
13561
+ if (item && result.every((existing) => existing.id !== item.id)) {
13419
13562
  result.push(item);
13420
13563
  }
13421
13564
  }
@@ -13430,7 +13573,7 @@ function parseInstalledList(raw) {
13430
13573
  if (!Array.isArray(parsed)) {
13431
13574
  return [];
13432
13575
  }
13433
- return dedupeById(parsed.map(parseInstalledPlugin));
13576
+ return dedupeById(parsed.map((raw) => parseInstalledPlugin(raw)));
13434
13577
  }
13435
13578
  catch {
13436
13579
  return [];
@@ -13440,7 +13583,7 @@ function parseCatalogList(raw) {
13440
13583
  if (!Array.isArray(raw)) {
13441
13584
  return [];
13442
13585
  }
13443
- return dedupeById(raw.map(parseCatalogEntry));
13586
+ return dedupeById(raw.map((entry) => parseCatalogEntry(entry)));
13444
13587
  }
13445
13588
 
13446
13589
  const STORAGE_KEY$2 = 'lw.shell.deployed-plugins';
@@ -13468,7 +13611,7 @@ class PluginDeploymentService {
13468
13611
  adopt(entries) {
13469
13612
  this.persist(entries
13470
13613
  .filter((entry) => entry.deployed === true)
13471
- .map(withoutCatalogMetadata));
13614
+ .map((entry) => withoutCatalogMetadata(entry)));
13472
13615
  }
13473
13616
  isDeployed(id) {
13474
13617
  return this.entries().some((entry) => entry.id === id);
@@ -13586,10 +13729,10 @@ function registerDefaultSettings(settings) {
13586
13729
 
13587
13730
  function fuzzyScore(query, label) {
13588
13731
  const needle = query.toLowerCase();
13589
- const haystack = label.toLowerCase();
13590
13732
  if (!needle) {
13591
13733
  return 0;
13592
13734
  }
13735
+ const haystack = label.toLowerCase();
13593
13736
  let score = 0;
13594
13737
  let searchFrom = 0;
13595
13738
  let previous = -2;
@@ -13619,7 +13762,7 @@ function formatterFor(locale) {
13619
13762
  }
13620
13763
  catch {
13621
13764
  try {
13622
- return new Intl.RelativeTimeFormat(locale.replace(/_/g, '-'), {
13765
+ return new Intl.RelativeTimeFormat(locale.replaceAll('_', '-'), {
13623
13766
  numeric: 'auto',
13624
13767
  });
13625
13768
  }
@@ -13696,7 +13839,7 @@ function ranked(query, entries) {
13696
13839
  return entries
13697
13840
  .map((entry) => ({ entry, score: fuzzyScore(query, entry.label) }))
13698
13841
  .filter((scored) => scored.score !== null)
13699
- .sort((a, b) => b.score - a.score)
13842
+ .toSorted((a, b) => b.score - a.score)
13700
13843
  .map((scored) => scored.entry);
13701
13844
  }
13702
13845
  class CommandPalette {
@@ -13753,9 +13896,9 @@ class CommandPalette {
13753
13896
  pinned: tab.pinned,
13754
13897
  closable: tab.closable,
13755
13898
  lastActive: tab.lastActive,
13756
- time: tab.lastActive !== undefined
13757
- ? formatRelativeTime(locale, tab.lastActive, now)
13758
- : undefined,
13899
+ time: tab.lastActive === undefined
13900
+ ? undefined
13901
+ : formatRelativeTime(locale, tab.lastActive, now),
13759
13902
  }));
13760
13903
  }, /* @ts-ignore */
13761
13904
  ...(ngDevMode ? [{ debugName: "tabEntries" }] : /* istanbul ignore next */ []));
@@ -13784,7 +13927,7 @@ class CommandPalette {
13784
13927
  const query = this.query().trim();
13785
13928
  const entries = this.tabEntries();
13786
13929
  if (!query) {
13787
- return [...entries].sort((a, b) => (b.lastActive ?? 0) - (a.lastActive ?? 0));
13930
+ return [...entries].toSorted((a, b) => (b.lastActive ?? 0) - (a.lastActive ?? 0));
13788
13931
  }
13789
13932
  return ranked(query, entries);
13790
13933
  }, /* @ts-ignore */
@@ -13859,7 +14002,7 @@ class CommandPalette {
13859
14002
  return;
13860
14003
  }
13861
14004
  const entry = this.results()[this.activeIndex()];
13862
- if (!entry || entry.kind !== 'tab') {
14005
+ if (entry?.kind !== 'tab') {
13863
14006
  return;
13864
14007
  }
13865
14008
  event.preventDefault();
@@ -14139,7 +14282,7 @@ function seedHostCommands(registry, layout, deps) {
14139
14282
  registry.addCommand({
14140
14283
  id: QUICK_OPEN_COMMAND_ID,
14141
14284
  title: 'palette.quickOpenTitle',
14142
- icon: 'document',
14285
+ icon: 'openWork',
14143
14286
  shortcut: 'mod+p',
14144
14287
  run: () => {
14145
14288
  dialogs.open(CommandPalette, {
@@ -14260,9 +14403,12 @@ function seedBuiltInMenus(registry, layout, deps) {
14260
14403
  return;
14261
14404
  }
14262
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) {
14263
14410
  const railCount = layout.regions.filter((region) => region.type === 'rail').length;
14264
14411
  const rail = deps.features.rail;
14265
- const sidebar = deps.features.sidebar;
14266
14412
  if (railCount >= 1 && rail.hideItems) {
14267
14413
  registerRailContextMenu(registry, deps.railItems);
14268
14414
  }
@@ -14272,6 +14418,9 @@ function seedBuiltInMenus(registry, layout, deps) {
14272
14418
  if (railCount >= 1 && rail.curate) {
14273
14419
  registerRailCustomizeMenu(registry);
14274
14420
  }
14421
+ }
14422
+ function seedViewMenus(registry, layout, deps) {
14423
+ const sidebar = deps.features.sidebar;
14275
14424
  if (sidebar.resetViewState) {
14276
14425
  registerViewResetMenu(registry, deps.viewStates, deps.viewInstances);
14277
14426
  }
@@ -14420,7 +14569,7 @@ class BootLatchedIdentity {
14420
14569
  return this.latched;
14421
14570
  }
14422
14571
  const id = this.read();
14423
- if (id === null || id === undefined || id === '') {
14572
+ if (!id) {
14424
14573
  return null;
14425
14574
  }
14426
14575
  this.latched = id;
@@ -14710,7 +14859,7 @@ function surfaceRoute(route, retained) {
14710
14859
  function subStub(path, pathMatch) {
14711
14860
  return {
14712
14861
  path,
14713
- ...(pathMatch ? { pathMatch } : {}),
14862
+ ...(pathMatch && { pathMatch }),
14714
14863
  component: ContentSubStub,
14715
14864
  data: { content: true, sub: true },
14716
14865
  };
@@ -15001,13 +15150,13 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.0", ngImpor
15001
15150
  type: Service
15002
15151
  }] });
15003
15152
  function pathOfKey(key) {
15004
- return key.split('|')[1] ?? '';
15153
+ return key.split('|', 2)[1] ?? '';
15005
15154
  }
15006
15155
  function stashKeyLive(key, open, routes, views) {
15007
15156
  if (key.startsWith(PRIMARY_RETENTION_PREFIX)) {
15008
15157
  return true;
15009
15158
  }
15010
- const [scope, path] = key.split('|');
15159
+ const [scope, path] = key.split('|', 2);
15011
15160
  if (!tabOpen(open.get(scope), routes, path)) {
15012
15161
  return false;
15013
15162
  }
@@ -15540,7 +15689,8 @@ class IconRegistry {
15540
15689
  setIcon(name, safe);
15541
15690
  added.push(name);
15542
15691
  }
15543
- return { dispose: () => added.forEach((name) => removeIcon(name)) };
15692
+ return { dispose: () => { for (const name of added)
15693
+ removeIcon(name); } };
15544
15694
  }
15545
15695
  resolve(name) {
15546
15696
  return resolveIcon(name);
@@ -15690,6 +15840,9 @@ function providePlugins(...plugins) {
15690
15840
  ];
15691
15841
  }
15692
15842
 
15843
+ /** Multi-provider token: each contribution adds one sandboxed plugin to load. */
15844
+ const FRAME_PLUGIN = new InjectionToken('FRAME_PLUGIN');
15845
+
15693
15846
  const STORAGE_KEY = 'lw.shell.installed-plugins';
15694
15847
  /**
15695
15848
  * The user's installed community plugins. Holds only the state: which catalog entries the
@@ -15721,7 +15874,7 @@ class PluginInstallService {
15721
15874
  return this.entries().some((entry) => entry.id === id);
15722
15875
  }
15723
15876
  /** The installed entry for an id, or `undefined` — the baseline an update is compared against. */
15724
- find(id) {
15877
+ byId(id) {
15725
15878
  return this.entries().find((entry) => entry.id === id);
15726
15879
  }
15727
15880
  /**
@@ -15786,202 +15939,86 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.0", ngImpor
15786
15939
  type: Service
15787
15940
  }], ctorParameters: () => [] });
15788
15941
 
15789
- const INPUT_TYPES = ['text', 'date', 'email', 'number', 'password'];
15790
- function sanitizeOptions(raw) {
15791
- if (!Array.isArray(raw)) {
15792
- return [];
15793
- }
15794
- return raw
15795
- .filter((option) => typeof option === 'object' &&
15796
- option !== null &&
15797
- typeof option['value'] === 'string' &&
15798
- typeof option['label'] === 'string')
15799
- .map((option) => ({ value: option.value, label: option.label }));
15800
- }
15801
- function optionalString(value) {
15802
- return typeof value === 'string' ? value : undefined;
15803
- }
15804
- function optionalNumber(value) {
15805
- return typeof value === 'number' ? value : undefined;
15806
- }
15807
- function buildTextControl(value, control) {
15808
- return {
15809
- kind: 'text',
15810
- value,
15811
- inputType: INPUT_TYPES.find((type) => type === control['inputType']),
15812
- placeholder: optionalString(control['placeholder']),
15813
- };
15942
+ function levelOf(plugin) {
15943
+ return plugin.level ?? DEFAULT_ISOLATION_LEVEL;
15814
15944
  }
15815
- function buildSelectControl(pluginId, value, control) {
15816
- const options = sanitizeOptions(control['options']);
15817
- if (options.length === 0) {
15818
- 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
+ });
15819
15974
  }
15820
- return { kind: 'select', value, options };
15975
+ return [...composed, ...fromCatalog];
15821
15976
  }
15822
- function buildSliderControl(value, control) {
15823
- return {
15824
- kind: 'slider',
15825
- value,
15826
- min: optionalNumber(control['min']),
15827
- max: optionalNumber(control['max']),
15828
- step: optionalNumber(control['step']),
15829
- };
15830
- }
15831
- function sanitizeControl(pluginId, raw) {
15832
- const control = (raw ?? {});
15833
- const kind = control['kind'];
15834
- const value = control['value'];
15835
- if (kind === 'toggle' && typeof value === 'boolean') {
15836
- return { kind, value };
15837
- }
15838
- if (kind === 'text' && typeof value === 'string') {
15839
- return buildTextControl(value, control);
15840
- }
15841
- if (kind === 'select' && typeof value === 'string') {
15842
- return buildSelectControl(pluginId, value, control);
15843
- }
15844
- if (kind === 'slider' && typeof value === 'number') {
15845
- return buildSliderControl(value, control);
15846
- }
15847
- throw new Error(`Sandbox plugin "${pluginId}": a settings control must be toggle/text/select/slider with a matching default 'value'.`);
15848
- }
15849
- function sanitizeRow(pluginId, raw) {
15850
- const row = (raw ?? {});
15851
- if (typeof row['id'] !== 'string' || row['id'].length === 0) {
15852
- throw new Error(`Sandbox plugin "${pluginId}": every settings row needs a non-empty 'id'.`);
15853
- }
15854
- if (typeof row['label'] !== 'string' || row['label'].length === 0) {
15855
- throw new Error(`Sandbox plugin "${pluginId}": every settings row needs a non-empty 'label'.`);
15856
- }
15857
- return {
15858
- id: row['id'],
15859
- label: row['label'],
15860
- description: typeof row['description'] === 'string' ? row['description'] : undefined,
15861
- control: sanitizeControl(pluginId, row['control']),
15862
- };
15863
- }
15864
- function sanitizeRpcSettingsSection(pluginId, section) {
15865
- const raw = (section ?? {});
15866
- if (typeof raw['id'] !== 'string' || raw['id'].length === 0) {
15867
- throw new Error(`Sandbox plugin "${pluginId}": registerSettingsSection requires a non-empty 'id'.`);
15868
- }
15869
- if (typeof raw['title'] !== 'string' || raw['title'].length === 0) {
15870
- throw new Error(`Sandbox plugin "${pluginId}": registerSettingsSection requires a non-empty 'title'.`);
15871
- }
15872
- if (!Array.isArray(raw['rows']) || raw['rows'].length === 0) {
15873
- throw new Error(`Sandbox plugin "${pluginId}": registerSettingsSection requires at least one row.`);
15874
- }
15875
- return {
15876
- id: raw['id'],
15877
- title: raw['title'],
15878
- order: typeof raw['order'] === 'number' ? raw['order'] : undefined,
15879
- rows: raw['rows'].map((row) => sanitizeRow(pluginId, row)),
15880
- };
15881
- }
15882
- function defaultsOf(wire) {
15883
- const defaults = {};
15884
- for (const row of wire.rows) {
15885
- defaults[row.id] = row.control.value;
15886
- }
15887
- return defaults;
15888
- }
15889
- function typedOverlay(defaults, raw) {
15890
- if (!raw) {
15891
- return defaults;
15892
- }
15893
- try {
15894
- const parsed = JSON.parse(raw);
15895
- if (typeof parsed !== 'object' || parsed === null) {
15896
- return defaults;
15897
- }
15898
- const merged = { ...defaults };
15899
- for (const [key, value] of Object.entries(parsed)) {
15900
- if (key in defaults && typeof value === typeof defaults[key]) {
15901
- merged[key] = value;
15902
- }
15903
- }
15904
- return merged;
15905
- }
15906
- catch {
15907
- return defaults;
15908
- }
15909
- }
15910
- function buildFrameSection(deps) {
15911
- const { pluginId, wire, group, store, sync, notify } = deps;
15912
- const key = `lw.plugin-settings:${pluginId}:${wire.id}`;
15913
- const defaults = defaultsOf(wire);
15914
- const values = signal(typedOverlay(defaults, store.peek?.(key)), /* @ts-ignore */
15915
- ...(ngDevMode ? [{ debugName: "values" }] : /* istanbul ignore next */ []));
15916
- const applyStored = (raw) => {
15917
- values.set(typedOverlay(defaults, raw));
15918
- notify(wire.id, values());
15919
- };
15920
- if (store.peek) {
15921
- notify(wire.id, values());
15922
- }
15923
- else {
15924
- hydrateAsync(store, key, applyStored);
15925
- }
15926
- const disposeSync = sync.register('settings', key, applyStored);
15927
- const set = (rowId, value) => {
15928
- values.update((current) => ({ ...current, [rowId]: value }));
15929
- void store.set(key, JSON.stringify(values()));
15930
- notify(wire.id, values());
15931
- };
15932
- const section = {
15933
- id: `${pluginId}.${wire.id}`,
15934
- title: wire.title,
15935
- group,
15936
- order: wire.order,
15937
- rows: wire.rows.map((row) => ({
15938
- id: `${pluginId}.${wire.id}.${row.id}`,
15939
- label: row.label,
15940
- description: row.description,
15941
- control: hostControl(row, values, set),
15942
- })),
15943
- };
15944
- return { section, disposeSync };
15945
- }
15946
- function hostControl(row, values, set) {
15947
- const control = row.control;
15948
- switch (control.kind) {
15949
- case 'toggle':
15950
- return {
15951
- kind: 'toggle',
15952
- value: () => values()[row.id] === true,
15953
- set: (value) => set(row.id, value),
15954
- };
15955
- case 'text':
15956
- return {
15957
- kind: 'text',
15958
- inputType: control.inputType,
15959
- placeholder: control.placeholder,
15960
- value: () => String(values()[row.id] ?? ''),
15961
- set: (value) => set(row.id, value),
15962
- };
15963
- case 'select':
15964
- return {
15965
- kind: 'select',
15966
- options: control.options,
15967
- value: () => String(values()[row.id] ?? control.value),
15968
- set: (value) => set(row.id, value),
15969
- };
15970
- case 'slider':
15971
- return {
15972
- kind: 'slider',
15973
- min: control.min,
15974
- max: control.max,
15975
- step: control.step,
15976
- value: () => Number(values()[row.id] ?? control.value),
15977
- set: (value) => set(row.id, value),
15978
- };
15979
- }
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);
15980
15989
  }
15981
15990
 
15982
15991
  const MAX_RPC_AREA_DEPTH = 8;
15983
15992
  function sanitizeRpcSurface(pluginId, surface, permitted) {
15984
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) {
15985
16022
  if (typeof raw['id'] !== 'string' || raw['id'].length === 0) {
15986
16023
  throw new Error(`Sandbox plugin "${pluginId}": registerSurface requires a non-empty 'id'.`);
15987
16024
  }
@@ -15996,20 +16033,20 @@ function sanitizeRpcSurface(pluginId, surface, permitted) {
15996
16033
  throw new Error(`Sandbox plugin "${pluginId}": 'access' does not cross the RPC boundary — ` +
15997
16034
  `a sandboxed surface gates itself from the pushed session state.`);
15998
16035
  }
15999
- const container = sanitizeRpcContainer(raw['container']);
16000
- const iframe = raw['iframe'];
16001
- if (container === undefined) {
16002
- if (typeof iframe !== 'string') {
16003
- throw new Error(`Sandbox plugin "${pluginId}": registerSurface needs an { iframe } URL or a { container } spec.`);
16004
- }
16005
- const origin = surfaceOrigin(iframe);
16006
- if (origin === null || !permittedOrigins(permitted).has(origin)) {
16007
- throw new Error(`Sandbox plugin "${pluginId}": the iframe surface must be served from an origin this ` +
16008
- `distribution permitted for it, got "${iframe}".`);
16009
- }
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.`);
16010
16041
  }
16011
- const routable = sanitizeRpcRoutable(raw['routable']);
16012
- 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) {
16013
16050
  if (routable === undefined && docks === undefined) {
16014
16051
  throw new Error(`Sandbox plugin "${pluginId}": registerSurface needs 'routable.path' (a URL-addressed surface) ` +
16015
16052
  `or 'docks' (a surface hosted at a dock).`);
@@ -16018,24 +16055,6 @@ function sanitizeRpcSurface(pluginId, surface, permitted) {
16018
16055
  throw new Error(`Sandbox plugin "${pluginId}": a container surface must be routable — a container tab holds ` +
16019
16056
  `its own ':id'.`);
16020
16057
  }
16021
- const shared = {
16022
- id: raw['id'],
16023
- title: raw['title'],
16024
- icon: typeof raw['icon'] === 'string' ? raw['icon'] : undefined,
16025
- order: typeof raw['order'] === 'number' ? raw['order'] : undefined,
16026
- instanceable: raw['instanceable'] === true ? true : undefined,
16027
- retain: raw['retain'] === 'always' || raw['retain'] === 'never'
16028
- ? raw['retain']
16029
- : undefined,
16030
- saveOn: raw['saveOn'] === 'hide' ? 'hide' : undefined,
16031
- closable: raw['closable'] === false ? false : undefined,
16032
- padded: raw['padded'] === false ? false : undefined,
16033
- routable,
16034
- docks,
16035
- };
16036
- return container !== undefined
16037
- ? { ...shared, container }
16038
- : { ...shared, iframe: iframe };
16039
16058
  }
16040
16059
  function sanitizeRpcRoutable(value) {
16041
16060
  if (typeof value !== 'object' || value === null) {
@@ -16088,7 +16107,7 @@ function sanitizeRpcArea(value, depth) {
16088
16107
  const raw = value;
16089
16108
  const size = typeof raw['size'] === 'number' ? { size: raw['size'] } : {};
16090
16109
  if (Array.isArray(raw['tabs'])) {
16091
- return { ...size, tabs: raw['tabs'].flatMap(sanitizeRpcContainerTab) };
16110
+ return { ...size, tabs: raw['tabs'].flatMap((value) => sanitizeRpcContainerTab(value)) };
16092
16111
  }
16093
16112
  for (const kind of ['rows', 'columns']) {
16094
16113
  const declared = raw[kind];
@@ -16115,8 +16134,8 @@ function sanitizeRpcContainerTab(value) {
16115
16134
  return [
16116
16135
  {
16117
16136
  surface: raw['surface'],
16118
- ...(raw['closable'] === false ? { closable: false } : {}),
16119
- ...(raw['active'] === true ? { active: true } : {}),
16137
+ ...(raw['closable'] === false && { closable: false }),
16138
+ ...(raw['active'] === true && { active: true }),
16120
16139
  },
16121
16140
  ];
16122
16141
  }
@@ -16173,11 +16192,14 @@ function sanitizeRpcToastInput(input) {
16173
16192
  id: typeof raw['id'] === 'string' ? raw['id'] : undefined,
16174
16193
  };
16175
16194
  }
16195
+ const NOTIFICATION_KINDS = new Set([
16196
+ 'info',
16197
+ 'success',
16198
+ 'warning',
16199
+ 'error',
16200
+ ]);
16176
16201
  function isNotificationKind(value) {
16177
- return (value === 'info' ||
16178
- value === 'success' ||
16179
- value === 'warning' ||
16180
- value === 'error');
16202
+ return NOTIFICATION_KINDS.has(value);
16181
16203
  }
16182
16204
  function sanitizeRpcMenuItem(item) {
16183
16205
  const raw = (item ?? {});
@@ -16210,29 +16232,267 @@ function sanitizeMenuContext(value) {
16210
16232
  return clean;
16211
16233
  }
16212
16234
 
16213
- const UNCARRIABLE_ARGUMENTS = {
16214
- outcome: 'refused',
16215
- reason: 'invalid-arguments',
16216
- message: 'Arguments must be an object of single values or lists of them; anything else cannot cross the ' +
16217
- 'sandbox boundary as the value it was.',
16218
- };
16219
- function invokeRpcCommand(ctx, id, args) {
16220
- const carried = args === undefined ? undefined : asCommandArguments(args);
16221
- return carried === null
16222
- ? Promise.resolve(UNCARRIABLE_ARGUMENTS)
16223
- : 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 }));
16224
16246
  }
16225
-
16226
- /** Multi-provider token: each contribution adds one sandboxed plugin to load. */
16227
- const FRAME_PLUGIN = new InjectionToken('FRAME_PLUGIN');
16228
- function levelOf(plugin) {
16229
- return plugin.level ?? DEFAULT_ISOLATION_LEVEL;
16247
+ function optionalString(value) {
16248
+ return typeof value === 'string' ? value : undefined;
16230
16249
  }
16231
- function signatureOf(plugin) {
16232
- const caps = [...(plugin.capabilities ?? [])].sort().join(',');
16233
- const granted = [...(plugin.granted ?? [])].sort().join(',');
16234
- return `${plugin.entryUrl}|${caps}|${granted}|${plugin.version ?? ''}|${levelOf(plugin)}`;
16250
+ function optionalNumber(value) {
16251
+ return typeof value === 'number' ? value : undefined;
16235
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
+
16236
16496
  /**
16237
16497
  * Second {@link PluginRuntime} implementation:
16238
16498
  * runs each plugin in an isolated `<iframe sandbox="allow-scripts">` and hands it `ctx` over **Penpal**
@@ -16290,9 +16550,13 @@ class FramePluginRuntime {
16290
16550
  this.instances.delete(id);
16291
16551
  instance.connection.destroy();
16292
16552
  instance.frame.remove();
16293
- instance.watched.forEach((entry) => entry.stop());
16553
+ for (const entry of instance.watched.values()) {
16554
+ entry.stop();
16555
+ }
16294
16556
  instance.ctx.disposeAll();
16295
- instance.syncCleanups.forEach((cleanup) => cleanup());
16557
+ for (const cleanup of instance.syncCleanups) {
16558
+ cleanup();
16559
+ }
16296
16560
  this.grants.unregister(id);
16297
16561
  this.isolation.unregister(id);
16298
16562
  }
@@ -16304,7 +16568,7 @@ class FramePluginRuntime {
16304
16568
  }
16305
16569
  }
16306
16570
  reconcile(disabled, installed, deployed) {
16307
- const runnable = this.runnablePlugins(installed, deployed);
16571
+ const runnable = runnablePlugins(this.plugins, installed, deployed, this.catalogCap);
16308
16572
  for (const plugin of runnable) {
16309
16573
  this.enablement.register(plugin.id, plugin.name ?? plugin.id);
16310
16574
  const enabled = plugin.provided === true || !disabled.has(plugin.id);
@@ -16322,34 +16586,6 @@ class FramePluginRuntime {
16322
16586
  }
16323
16587
  this.dropUninstalled(runnable);
16324
16588
  }
16325
- runnablePlugins(installed, deployed) {
16326
- const claimed = new Set(this.plugins.map((plugin) => plugin.id));
16327
- const provided = new Set(deployed.map((plugin) => plugin.id));
16328
- const fromCatalog = [];
16329
- for (const plugin of [...deployed, ...installed]) {
16330
- if (claimed.has(plugin.id)) {
16331
- continue;
16332
- }
16333
- const asked = plugin.level ?? DEFAULT_ISOLATION_LEVEL;
16334
- if (exceedsLevel(asked, this.catalogCap)) {
16335
- console.error(`Plugin "${plugin.id}" asks to run ${asked}, which this catalog may not confer ` +
16336
- `(its cap is ${this.catalogCap}). It is not started.`);
16337
- continue;
16338
- }
16339
- claimed.add(plugin.id);
16340
- fromCatalog.push({
16341
- id: plugin.id,
16342
- entryUrl: plugin.entryUrl,
16343
- capabilities: plugin.capabilities,
16344
- name: plugin.name,
16345
- granted: plugin.capabilities ?? [],
16346
- version: plugin.version,
16347
- level: asked,
16348
- provided: provided.has(plugin.id) || undefined,
16349
- });
16350
- }
16351
- return [...this.plugins, ...fromCatalog];
16352
- }
16353
16589
  dropUninstalled(runnable) {
16354
16590
  const known = new Set(runnable.map((plugin) => plugin.id));
16355
16591
  for (const id of this.instances.keys()) {
@@ -16373,7 +16609,19 @@ class FramePluginRuntime {
16373
16609
  const watched = new Map();
16374
16610
  const connection = connect({
16375
16611
  messenger,
16376
- 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
+ }),
16377
16625
  });
16378
16626
  this.instances.set(plugin.id, {
16379
16627
  ctx,
@@ -16384,10 +16632,11 @@ class FramePluginRuntime {
16384
16632
  watched,
16385
16633
  });
16386
16634
  connection.promise.catch((error) => {
16387
- if (this.instances.has(plugin.id)) {
16388
- console.error(`Sandbox plugin "${plugin.id}" failed to connect`, error);
16389
- this.deactivate(plugin.id);
16635
+ if (!this.instances.has(plugin.id)) {
16636
+ return;
16390
16637
  }
16638
+ console.error(`Sandbox plugin "${plugin.id}" failed to connect`, error);
16639
+ this.deactivate(plugin.id);
16391
16640
  });
16392
16641
  }
16393
16642
  createFrame(entryUrl, level) {
@@ -16398,71 +16647,9 @@ class FramePluginRuntime {
16398
16647
  frame.setAttribute('aria-hidden', 'true');
16399
16648
  frame.style.display = 'none';
16400
16649
  frame.src = entryUrl;
16401
- document.body.appendChild(frame);
16650
+ document.body.append(frame);
16402
16651
  return frame;
16403
16652
  }
16404
- reportingRefusals(methods) {
16405
- const reported = Object.entries(methods).map(([name, method]) => [
16406
- name,
16407
- (...args) => {
16408
- try {
16409
- return method(...args);
16410
- }
16411
- catch (error) {
16412
- this.refusals.report(error);
16413
- throw error;
16414
- }
16415
- },
16416
- ]);
16417
- return Object.fromEntries(reported);
16418
- }
16419
- rpcMethods(pluginId, ctx, syncCleanups, watched, origins) {
16420
- return {
16421
- registerSurface: (surface) => {
16422
- ctx.registerSurface(sanitizeRpcSurface(pluginId, surface, origins));
16423
- },
16424
- registerMenuItem: (item) => {
16425
- ctx.registerMenuItem(sanitizeRpcMenuItem(item));
16426
- },
16427
- registerSettingsSection: (section) => {
16428
- const built = buildFrameSection({
16429
- pluginId,
16430
- wire: sanitizeRpcSettingsSection(pluginId, section),
16431
- group: this.install.isInstalled(pluginId)
16432
- ? 'settings.group.community'
16433
- : 'settings.group.plugins',
16434
- store: this.store,
16435
- sync: this.sync,
16436
- notify: (sectionId, values) => this.notifySettings(pluginId, sectionId, values),
16437
- });
16438
- syncCleanups.push(built.disposeSync);
16439
- ctx.registerSettingsSection(built.section);
16440
- },
16441
- navigateContent: (path) => ctx.navigateContent(path),
16442
- openContentTab: (input) => {
16443
- const sanitized = sanitizeRpcTabInput(input);
16444
- ctx.openContentTab({
16445
- ...sanitized,
16446
- onClose: () => this.notifyTabClosed(pluginId, sanitized.path),
16447
- });
16448
- },
16449
- keepContentTab: (path) => ctx.keepContentTab(path),
16450
- pinContentTab: (path) => ctx.pinContentTab(path),
16451
- unpinContentTab: (path) => ctx.unpinContentTab(path),
16452
- closeContentTab: (path) => ctx.closeContentTab(path),
16453
- revealSurface: (id) => ctx.revealSurface(id),
16454
- invokeCommand: (id, args) => invokeRpcCommand(ctx, id, args),
16455
- invocableCommands: () => ctx.invocableCommands(),
16456
- toast: (input) => ctx.ui.toast(sanitizeRpcToastInput(input)),
16457
- stateWatch: (key) => this.watchState(pluginId, ctx, watched, key),
16458
- stateSet: (key, value) => watched.get(key)?.handle.set(value),
16459
- stateClear: (key) => watched.get(key)?.handle.clear(),
16460
- stateUnwatch: (key) => {
16461
- watched.get(key)?.stop();
16462
- watched.delete(key);
16463
- },
16464
- };
16465
- }
16466
16653
  watchState(pluginId, ctx, watched, key) {
16467
16654
  if (watched.has(key)) {
16468
16655
  return;
@@ -16471,7 +16658,7 @@ class FramePluginRuntime {
16471
16658
  const ref = effect(() => {
16472
16659
  const value = handle.value();
16473
16660
  const loaded = handle.loaded();
16474
- untracked(() => this.notifyState(pluginId, key, value, loaded));
16661
+ untracked(() => this.notify(pluginId, (remote) => remote.stateChanged(key, value, loaded)));
16475
16662
  }, { ...(ngDevMode ? { debugName: "ref" } : /* istanbul ignore next */ {}), injector: this.injector });
16476
16663
  watched.set(key, {
16477
16664
  handle,
@@ -16481,32 +16668,12 @@ class FramePluginRuntime {
16481
16668
  },
16482
16669
  });
16483
16670
  }
16484
- notifyState(pluginId, key, value, loaded) {
16485
- const instance = this.instances.get(pluginId);
16486
- if (!instance) {
16487
- return;
16488
- }
16489
- void instance.connection.promise
16490
- .then((remote) => remote.stateChanged(key, value, loaded))
16491
- .catch(() => undefined);
16492
- }
16493
- notifyTabClosed(pluginId, path) {
16671
+ notify(pluginId, send) {
16494
16672
  const instance = this.instances.get(pluginId);
16495
16673
  if (!instance) {
16496
16674
  return;
16497
16675
  }
16498
- void instance.connection.promise
16499
- .then((remote) => remote.contentTabClosed(path))
16500
- .catch(() => undefined);
16501
- }
16502
- notifySettings(pluginId, sectionId, values) {
16503
- const instance = this.instances.get(pluginId);
16504
- if (!instance) {
16505
- return;
16506
- }
16507
- void instance.connection.promise
16508
- .then((remote) => remote.settingsChanged(sectionId, values))
16509
- .catch(() => undefined);
16676
+ void instance.connection.promise.then(send).catch(() => undefined);
16510
16677
  }
16511
16678
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.0", ngImport: i0, type: FramePluginRuntime, deps: [], target: i0.ɵɵFactoryTarget.Service });
16512
16679
  static ɵprov = i0.ɵɵngDeclareService({ minVersion: "22.0.0", version: "22.1.0", ngImport: i0, type: FramePluginRuntime });
@@ -16650,12 +16817,12 @@ function availableUpdate(installed, entry) {
16650
16817
  return isNewerVersion(entry.version, installed.version) ? entry : undefined;
16651
16818
  }
16652
16819
  function addedCapabilities(entry, installed) {
16653
- const consented = new Set(installed.capabilities ?? []);
16820
+ const consented = new Set(installed.capabilities);
16654
16821
  return (entry.capabilities ?? []).filter((capability) => !consented.has(capability));
16655
16822
  }
16656
16823
 
16657
16824
  async function confirmUpdate(deps, entry) {
16658
- const installed = deps.installs.find(entry.id);
16825
+ const installed = deps.installs.byId(entry.id);
16659
16826
  if (!installed) {
16660
16827
  return;
16661
16828
  }
@@ -16821,7 +16988,7 @@ class PluginStoreDetail {
16821
16988
  transloco = inject(TranslocoService);
16822
16989
  readme = signal(undefined, /* @ts-ignore */
16823
16990
  ...(ngDevMode ? [{ debugName: "readme" }] : /* istanbul ignore next */ []));
16824
- 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 */
16825
16992
  ...(ngDevMode ? [{ debugName: "update" }] : /* istanbul ignore next */ []));
16826
16993
  constructor() {
16827
16994
  effect(() => {
@@ -16907,7 +17074,7 @@ class PluginStoreDialog {
16907
17074
  ...(ngDevMode ? [{ debugName: "selectedId" }] : /* istanbul ignore next */ []));
16908
17075
  filtered = computed(() => {
16909
17076
  const list = (this.entries() ?? []).filter((entry) => matchesQuery([entry.name, entry.author, entry.category, entry.description], this.query()));
16910
- 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));
16911
17078
  }, /* @ts-ignore */
16912
17079
  ...(ngDevMode ? [{ debugName: "filtered" }] : /* istanbul ignore next */ []));
16913
17080
  selected = computed(() => this.filtered().find((entry) => entry.id === this.selectedId()), /* @ts-ignore */
@@ -16919,7 +17086,7 @@ class PluginStoreDialog {
16919
17086
  void confirmInstall(this.consentDeps, entry);
16920
17087
  }
16921
17088
  hasUpdate(entry) {
16922
- return availableUpdate(this.installs.find(entry.id), entry) !== undefined;
17089
+ return availableUpdate(this.installs.byId(entry.id), entry) !== undefined;
16923
17090
  }
16924
17091
  requestUpdate(entry) {
16925
17092
  void confirmUpdate(this.consentDeps, entry);