@loomweaver/shell 0.7.5 → 0.7.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -65,8 +65,15 @@ function withoutQuery(url) {
65
65
  const end = url.search(/[?#]/);
66
66
  return end === -1 ? url : url.slice(0, end);
67
67
  }
68
+ function withoutTrailingSlashes$1(path) {
69
+ let end = path.length;
70
+ while (end > 0 && path[end - 1] === '/') {
71
+ end -= 1;
72
+ }
73
+ return path.slice(0, end);
74
+ }
68
75
  function bare(url) {
69
- return withoutQuery(url).replace(/^\/+/, '').replace(/\/+$/, '');
76
+ return withoutTrailingSlashes$1(withoutQuery(url).replace(/^\/+/, ''));
70
77
  }
71
78
  function isPopoutUrl(url) {
72
79
  const path = bare(url);
@@ -94,7 +101,7 @@ function popoutUrlFor(paneTarget) {
94
101
  }
95
102
 
96
103
  function upsertBy(items, item, matches) {
97
- const index = items.findIndex(matches);
104
+ const index = items.findIndex((existing) => matches(existing));
98
105
  if (index === -1) {
99
106
  return [...items, item];
100
107
  }
@@ -310,7 +317,7 @@ class ContributionRegistry {
310
317
  const visible = omitted.size === 0
311
318
  ? docked
312
319
  : docked.filter((entry) => entry.id === undefined || !omitted.has(entry.id));
313
- return visible.map(entryToView);
320
+ return visible.map((entry) => entryToView(entry));
314
321
  }, /* @ts-ignore */
315
322
  ...(ngDevMode ? [{ debugName: "views" }] : /* istanbul ignore next */ []));
316
323
  barItems = this.visible(this.barItemsSignal);
@@ -321,7 +328,7 @@ class ContributionRegistry {
321
328
  * picker, `matchRoute` — drops them without knowing about omission.
322
329
  */
323
330
  contentRoutes = computed(() => {
324
- const routes = this.routableSurfaces().map(entryToContentRoute);
331
+ const routes = this.routableSurfaces().map((entry) => entryToContentRoute(entry));
325
332
  const omitted = this.omittedSignal();
326
333
  if (omitted.size === 0) {
327
334
  return routes;
@@ -340,7 +347,7 @@ class ContributionRegistry {
340
347
  return NO_ROUTES;
341
348
  }
342
349
  return this.routableSurfaces()
343
- .map(entryToContentRoute)
350
+ .map((entry) => entryToContentRoute(entry))
344
351
  .filter((route) => isRouteOmitted(route, omitted));
345
352
  }, /* @ts-ignore */
346
353
  ...(ngDevMode ? [{ debugName: "omittedContentRoutes" }] : /* istanbul ignore next */ []));
@@ -436,13 +443,13 @@ class ContributionRegistry {
436
443
  }
437
444
  /** Adds a menu-slot item. With an `id` a re-registration replaces in place (last-in wins); without one the item is additive and dispose removes this exact contribution. */
438
445
  addMenuItem(item) {
439
- this.menuItemsSignal.update((items) => upsertBy(items, item, (i) => item.id !== undefined && i.id === item.id));
446
+ this.menuItemsSignal.update((items) => upsertBy(items, item, (index) => item.id !== undefined && index.id === item.id));
440
447
  return {
441
- dispose: () => this.menuItemsSignal.update((items) => items.filter((i) => i !== item)),
448
+ dispose: () => this.menuItemsSignal.update((items) => items.filter((index) => index !== item)),
442
449
  };
443
450
  }
444
451
  removeMenuItemById(id) {
445
- this.menuItemsSignal.update((items) => items.filter((i) => i.id !== id));
452
+ this.menuItemsSignal.update((items) => items.filter((index) => index.id !== id));
446
453
  }
447
454
  addSurface(entry) {
448
455
  this.surfacesSignal.update((entries) => upsertBy(entries, entry, sameSlotAs(entry)));
@@ -462,11 +469,11 @@ class ContributionRegistry {
462
469
  add(target, item) {
463
470
  target.update((items) => upsertById(items, item));
464
471
  return {
465
- dispose: () => target.update((items) => items.filter((i) => i !== item)),
472
+ dispose: () => target.update((items) => items.filter((index) => index !== item)),
466
473
  };
467
474
  }
468
475
  removeById(target, id) {
469
- target.update((items) => items.filter((i) => i.id !== id));
476
+ target.update((items) => items.filter((index) => index.id !== id));
470
477
  }
471
478
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.0", ngImport: i0, type: ContributionRegistry, deps: [], target: i0.ɵɵFactoryTarget.Service });
472
479
  static ɵprov = i0.ɵɵngDeclareService({ minVersion: "22.0.0", version: "22.1.0", ngImport: i0, type: ContributionRegistry });
@@ -485,7 +492,7 @@ function sameSlotAs(entry) {
485
492
  }
486
493
 
487
494
  function normalizePath(url) {
488
- return url.split(/[?#]/)[0].replace(/^\/+/, '');
495
+ return url.split(/[?#]/, 1)[0].replace(/^\/+/, '');
489
496
  }
490
497
  function segmentsOf(path) {
491
498
  return normalizePath(path).split('/').filter(Boolean);
@@ -538,11 +545,11 @@ function paramsOfPattern(pattern, path) {
538
545
  const parts = segmentsOf(pattern);
539
546
  const segments = segmentsOf(path);
540
547
  const params = {};
541
- parts.forEach((part, index) => {
548
+ for (const [index, part] of parts.entries()) {
542
549
  if (part.startsWith(':') && segments[index] !== undefined) {
543
550
  params[part.slice(1)] = segments[index];
544
551
  }
545
- });
552
+ }
546
553
  return params;
547
554
  }
548
555
  function routeParams(route, path) {
@@ -647,7 +654,7 @@ function reusableRoute(route) {
647
654
  return route.routeConfig?.data?.['chromeless'] !== true;
648
655
  }
649
656
  function effectiveRetain(declared, fallback) {
650
- return declared !== undefined ? declared === 'always' : fallback === 'retain';
657
+ return declared === undefined ? fallback === 'retain' : declared === 'always';
651
658
  }
652
659
  function routeRetains(route, fallback) {
653
660
  if (route.container !== undefined) {
@@ -672,7 +679,7 @@ function surfaceRetentionMode(routes, path) {
672
679
  if (!route || route.container !== undefined) {
673
680
  return 'rebuild';
674
681
  }
675
- return route.iframe !== undefined ? 'in-place' : 'move';
682
+ return route.iframe === undefined ? 'move' : 'in-place';
676
683
  }
677
684
  function containerChildInstances(entries, tabPath) {
678
685
  const scoped = containerDockFor(tabPath) + ':';
@@ -732,11 +739,11 @@ class ContentReuseStrategy {
732
739
  changes = signal(0, /* @ts-ignore */
733
740
  ...(ngDevMode ? [{ debugName: "changes" }] : /* istanbul ignore next */ []));
734
741
  version = this.changes.asReadonly();
735
- shouldReuseRoute(future, curr) {
736
- if (!isContentRoute(future) || !isContentRoute(curr)) {
737
- return future.routeConfig === curr.routeConfig;
742
+ shouldReuseRoute(future, current) {
743
+ if (!isContentRoute(future) || !isContentRoute(current)) {
744
+ return future.routeConfig === current.routeConfig;
738
745
  }
739
- return future.routeConfig === curr.routeConfig && sameParams(future, curr);
746
+ return future.routeConfig === current.routeConfig && sameParams(future, current);
740
747
  }
741
748
  shouldDetach(route) {
742
749
  return reusableRoute(route);
@@ -782,11 +789,12 @@ class ContentReuseStrategy {
782
789
  }
783
790
  pruneExcept(isLive) {
784
791
  for (const [key, handle] of this.handles) {
785
- if (!isLive(key)) {
786
- handle.componentRef?.destroy();
787
- this.handles.delete(key);
788
- this.changes.update((value) => value + 1);
792
+ if (isLive(key)) {
793
+ continue;
789
794
  }
795
+ handle.componentRef?.destroy();
796
+ this.handles.delete(key);
797
+ this.changes.update((value) => value + 1);
790
798
  }
791
799
  }
792
800
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.0", ngImport: i0, type: ContentReuseStrategy, deps: [], target: i0.ɵɵFactoryTarget.Service });
@@ -884,7 +892,7 @@ class StateSyncChannel {
884
892
  }
885
893
  try {
886
894
  const opened = new BroadcastChannel(CHANNEL_NAME);
887
- opened.onmessage = (event) => this.receive(event.data);
895
+ opened.addEventListener('message', (event) => this.receive(event.data));
888
896
  return opened;
889
897
  }
890
898
  catch {
@@ -1024,7 +1032,7 @@ function cleanArea(area, options, problems, depth) {
1024
1032
  problems.push(`${options.context}: the arrangement nests deeper than ${MAX_DEPTH} levels — the deeper areas are dropped.`);
1025
1033
  return null;
1026
1034
  }
1027
- const kinds = ['tabs', 'rows', 'columns'].filter((key) => key in area);
1035
+ const kinds = ['tabs', 'rows', 'columns'].filter((key) => Object.hasOwn(area, key));
1028
1036
  if (kinds.length !== 1) {
1029
1037
  problems.push(`${options.context}: a pane area must be exactly one of tabs, rows or columns.`);
1030
1038
  return null;
@@ -1093,7 +1101,7 @@ function cleanTabsArea(area, size, options, problems) {
1093
1101
  function collectTabs$1(area) {
1094
1102
  return area.kind === 'tabs'
1095
1103
  ? [...area.tabs]
1096
- : area.children.flatMap(collectTabs$1);
1104
+ : area.children.flatMap((child) => collectTabs$1(child));
1097
1105
  }
1098
1106
  function buildNode(area, idPrefix, ctx, indexPath) {
1099
1107
  if (area.kind === 'tabs') {
@@ -1122,6 +1130,12 @@ function buildNode(area, idPrefix, ctx, indexPath) {
1122
1130
  };
1123
1131
  return chain(0, 1);
1124
1132
  }
1133
+ function evenShareOf(remainder, declaredSum, children, unspecified) {
1134
+ if (unspecified === 0) {
1135
+ return 0;
1136
+ }
1137
+ return remainder > 0 ? remainder / unspecified : declaredSum / children;
1138
+ }
1125
1139
  function fractionsOf(children) {
1126
1140
  const declared = children
1127
1141
  .map((child) => child.size)
@@ -1129,11 +1143,7 @@ function fractionsOf(children) {
1129
1143
  const declaredSum = declared.reduce((sum, size) => sum + size, 0);
1130
1144
  const unspecified = children.length - declared.length;
1131
1145
  const remainder = Math.max(0, 100 - declaredSum);
1132
- const evenShare = unspecified > 0
1133
- ? remainder > 0
1134
- ? remainder / unspecified
1135
- : declaredSum / children.length
1136
- : 0;
1146
+ const evenShare = evenShareOf(remainder, declaredSum, children.length, unspecified);
1137
1147
  const weights = children.map((child) => child.size ?? evenShare);
1138
1148
  const total = weights.reduce((sum, weight) => sum + weight, 0);
1139
1149
  return weights.map((weight) => weight / total);
@@ -1168,7 +1178,12 @@ function claimFor(claims, path) {
1168
1178
  if (matching.length === 0) {
1169
1179
  return null;
1170
1180
  }
1171
- const best = matching.reduce((winner, claim) => narrower(claim.pattern, winner.pattern) > 0 ? claim : winner);
1181
+ let best = matching[0];
1182
+ for (const claim of matching) {
1183
+ if (narrower(claim.pattern, best.pattern) > 0) {
1184
+ best = claim;
1185
+ }
1186
+ }
1172
1187
  const tied = matching.filter((claim) => claim.workspaceId !== best.workspaceId &&
1173
1188
  narrower(claim.pattern, best.pattern) === 0);
1174
1189
  return tied.length === 0 ? best : null;
@@ -1186,9 +1201,12 @@ function contestedShapes(claims) {
1186
1201
  return new Map([...byShape].filter(([, owners]) => owners.length > 1));
1187
1202
  }
1188
1203
  function conflictingClaims(claims) {
1189
- return [...contestedShapes(claims)].map(([shape, owners]) => `Workspaces ${owners.map((id) => `"${id}"`).join(' and ')} both claim "${shape}" — ` +
1190
- `neither is narrower than the other, so the claim is dropped and that address ` +
1191
- `behaves as though nothing claimed it. Give the address one home.`);
1204
+ return [...contestedShapes(claims)].map(([shape, owners]) => {
1205
+ const named = owners.map((id) => `"${id}"`).join(' and ');
1206
+ return (`Workspaces ${named} both claim "${shape}" ` +
1207
+ `neither is narrower than the other, so the claim is dropped and that address ` +
1208
+ `behaves as though nothing claimed it. Give the address one home.`);
1209
+ });
1192
1210
  }
1193
1211
  function withoutConflicts(claims) {
1194
1212
  const contested = contestedShapes(claims);
@@ -1230,8 +1248,8 @@ function leafWith(id, tabs, candidateActive, declared) {
1230
1248
  kind: 'leaf',
1231
1249
  id,
1232
1250
  tabs,
1233
- ...(active === undefined ? {} : { active }),
1234
- ...(declared ? { declared: true } : {}),
1251
+ ...(active !== undefined && { active }),
1252
+ ...(declared && { declared: true }),
1235
1253
  };
1236
1254
  }
1237
1255
 
@@ -1324,15 +1342,13 @@ function normalizeTab(value) {
1324
1342
  }
1325
1343
  return {
1326
1344
  path: tab['path'],
1327
- ...(tab['pinned'] === true ? { pinned: true } : {}),
1328
- ...(tab['preview'] === true ? { preview: true } : {}),
1329
- ...(tab['closable'] === false ? { closable: false } : {}),
1330
- ...(typeof tab['title'] === 'string' ? { title: tab['title'] } : {}),
1331
- ...(tab['literalTitle'] === true ? { literalTitle: true } : {}),
1332
- ...(typeof tab['icon'] === 'string' ? { icon: tab['icon'] } : {}),
1333
- ...(typeof tab['instance'] === 'string'
1334
- ? { instance: tab['instance'] }
1335
- : {}),
1345
+ ...(tab['pinned'] === true && { pinned: true }),
1346
+ ...(tab['preview'] === true && { preview: true }),
1347
+ ...(tab['closable'] === false && { closable: false }),
1348
+ ...(typeof tab['title'] === 'string' && { title: tab['title'] }),
1349
+ ...(tab['literalTitle'] === true && { literalTitle: true }),
1350
+ ...(typeof tab['icon'] === 'string' && { icon: tab['icon'] }),
1351
+ ...(typeof tab['instance'] === 'string' && { instance: tab['instance'] }),
1336
1352
  };
1337
1353
  }
1338
1354
  function normalizeLeafNode(id, node) {
@@ -1342,7 +1358,7 @@ function normalizeLeafNode(id, node) {
1342
1358
  }
1343
1359
  const rawTabs = node['tabs'];
1344
1360
  const tabs = Array.isArray(rawTabs)
1345
- ? rawTabs.map(normalizeTab).filter((tab) => tab !== null)
1361
+ ? rawTabs.map((value) => normalizeTab(value)).filter((tab) => tab !== null)
1346
1362
  : [];
1347
1363
  const rawActive = node['active'];
1348
1364
  return leafWith(id, tabs, typeof rawActive === 'string' ? rawActive : undefined, node['declared'] === true);
@@ -1415,11 +1431,11 @@ function auditWorkspaceDefinitions(definitions, panelRegions) {
1415
1431
  }
1416
1432
  seen.add(definition.id);
1417
1433
  if (definition.initial) {
1418
- if (initial !== null) {
1419
- problems.push(`Workspace "${definition.id}" also declares initial: true — "${initial}" already does, so this one is ignored.`);
1434
+ if (initial === null) {
1435
+ initial = definition.id;
1420
1436
  }
1421
1437
  else {
1422
- initial = definition.id;
1438
+ problems.push(`Workspace "${definition.id}" also declares initial: true — "${initial}" already does, so this one is ignored.`);
1423
1439
  }
1424
1440
  }
1425
1441
  auditDefinition(definition, panelRegions, problems);
@@ -1477,13 +1493,13 @@ function baselineSidebars(definition, deps) {
1477
1493
  if (sidebars === undefined) {
1478
1494
  return {};
1479
1495
  }
1480
- const listed = deps.panelRegions.filter((region) => region in sidebars);
1496
+ const listed = deps.panelRegions.filter((region) => Object.hasOwn(sidebars, region));
1481
1497
  const hidden = listed
1482
1498
  .flatMap((region) => deps
1483
1499
  .declaredPaths(region)
1484
1500
  .map((path) => path.slice(VIEW_PANE_PREFIX.length))
1485
1501
  .filter((id) => !sidebars[region].includes(id)))
1486
- .sort();
1502
+ .toSorted((a, b) => a.localeCompare(b));
1487
1503
  return hidden.length === 0 ? {} : { hiddenViews: JSON.stringify(hidden) };
1488
1504
  }
1489
1505
  function declaredTabPaths(definition) {
@@ -1511,7 +1527,7 @@ function bakeTab(definitionId, entry, problems) {
1511
1527
  return {
1512
1528
  tab: {
1513
1529
  path: tab.path,
1514
- ...(tab.closable === false ? { closable: false } : {}),
1530
+ ...(tab.closable === false && { closable: false }),
1515
1531
  },
1516
1532
  active: tab.active === true,
1517
1533
  };
@@ -1592,9 +1608,16 @@ function movable(parent) {
1592
1608
  function supportsAtomicMove(document) {
1593
1609
  return movable(document.body ?? document.documentElement);
1594
1610
  }
1611
+ function insertNode(parent, node, before) {
1612
+ if (before === null) {
1613
+ parent.append(node);
1614
+ return;
1615
+ }
1616
+ before.before(node);
1617
+ }
1595
1618
  function moveNode(parent, node, before) {
1596
1619
  if (!movable(parent) || !node.isConnected) {
1597
- parent.insertBefore(node, before);
1620
+ insertNode(parent, node, before);
1598
1621
  return false;
1599
1622
  }
1600
1623
  try {
@@ -1602,7 +1625,7 @@ function moveNode(parent, node, before) {
1602
1625
  return true;
1603
1626
  }
1604
1627
  catch {
1605
- parent.insertBefore(node, before);
1628
+ insertNode(parent, node, before);
1606
1629
  return false;
1607
1630
  }
1608
1631
  }
@@ -1652,10 +1675,11 @@ class RetainedViewStash {
1652
1675
  },
1653
1676
  stale: () => entry.tracked && (!owns() || this.entries.get(key) !== entry),
1654
1677
  describe: (mode, retain) => {
1655
- if (owns()) {
1656
- entry.mode = mode;
1657
- entry.keep = retain;
1678
+ if (!owns()) {
1679
+ return;
1658
1680
  }
1681
+ entry.mode = mode;
1682
+ entry.keep = retain;
1659
1683
  },
1660
1684
  release: (retained) => {
1661
1685
  if (owns()) {
@@ -1722,14 +1746,16 @@ class RetainedViewStash {
1722
1746
  }
1723
1747
  }
1724
1748
  evictWorkspace(workspaceId) {
1725
- for (const entry of [...this.entries.values()]) {
1749
+ const snapshot = [...this.entries.values()];
1750
+ for (const entry of snapshot) {
1726
1751
  if (!entry.inUse && entry.workspace === workspaceId) {
1727
1752
  this.destroyEntry(entry);
1728
1753
  }
1729
1754
  }
1730
1755
  }
1731
1756
  ngOnDestroy() {
1732
- for (const entry of [...this.entries.values()]) {
1757
+ const snapshot = [...this.entries.values()];
1758
+ for (const entry of snapshot) {
1733
1759
  this.destroyEntry(entry);
1734
1760
  }
1735
1761
  this.holdingArea?.remove();
@@ -1835,7 +1861,7 @@ class RetainedViewStash {
1835
1861
  const area = this.document.createElement('div');
1836
1862
  area.dataset['lwRetentionHold'] = '';
1837
1863
  area.style.display = 'none';
1838
- this.document.body.appendChild(area);
1864
+ this.document.body.append(area);
1839
1865
  this.holdingArea = area;
1840
1866
  return area;
1841
1867
  }
@@ -1846,7 +1872,7 @@ class RetainedViewStash {
1846
1872
  }
1847
1873
  pullNodes(entry) {
1848
1874
  for (const node of liveRootNodes(entry)) {
1849
- node.parentNode?.removeChild(node);
1875
+ node.remove();
1850
1876
  }
1851
1877
  }
1852
1878
  destroyEntry(entry) {
@@ -1870,7 +1896,8 @@ class RetainedViewStash {
1870
1896
  this.sweepQueued = true;
1871
1897
  queueMicrotask(() => {
1872
1898
  this.sweepQueued = false;
1873
- for (const entry of [...this.entries.values()]) {
1899
+ const snapshot = [...this.entries.values()];
1900
+ for (const entry of snapshot) {
1874
1901
  if (entry.inUse) {
1875
1902
  continue;
1876
1903
  }
@@ -1950,7 +1977,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.0", ngImpor
1950
1977
  type: Service
1951
1978
  }] });
1952
1979
  function pathOfStashKey(key) {
1953
- return key.split('|')[1] ?? '';
1980
+ return key.split('|', 2)[1] ?? '';
1954
1981
  }
1955
1982
 
1956
1983
  class RetentionUnloadGuard {
@@ -1983,7 +2010,6 @@ class RetentionUnloadGuard {
1983
2010
  return;
1984
2011
  }
1985
2012
  event.preventDefault();
1986
- event.returnValue = '';
1987
2013
  };
1988
2014
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.0", ngImport: i0, type: RetentionUnloadGuard, deps: [], target: i0.ɵɵFactoryTarget.Service });
1989
2015
  static ɵprov = i0.ɵɵngDeclareService({ minVersion: "22.0.0", version: "22.1.0", ngImport: i0, type: RetentionUnloadGuard });
@@ -2169,16 +2195,20 @@ function isMacPlatform() {
2169
2195
  function normaliseKey(key) {
2170
2196
  const lower = key.toLowerCase();
2171
2197
  switch (lower) {
2172
- case 'esc':
2198
+ case 'esc': {
2173
2199
  return 'escape';
2174
- case 'return':
2200
+ }
2201
+ case 'return': {
2175
2202
  return 'enter';
2203
+ }
2176
2204
  case 'space':
2177
2205
  case 'spacebar':
2178
- case ' ':
2206
+ case ' ': {
2179
2207
  return 'space';
2180
- default:
2208
+ }
2209
+ default: {
2181
2210
  return lower;
2211
+ }
2182
2212
  }
2183
2213
  }
2184
2214
  function toSignature(parts) {
@@ -2202,72 +2232,89 @@ function chordSignature(chord, isMac) {
2202
2232
  };
2203
2233
  for (const raw of chord.split('+')) {
2204
2234
  const token = raw.trim().toLowerCase();
2205
- if (!token) {
2206
- continue;
2235
+ if (token) {
2236
+ applyToken(parts, token, isMac);
2207
2237
  }
2208
- switch (token) {
2209
- case 'mod':
2210
- if (isMac)
2211
- parts.meta = true;
2212
- else
2213
- parts.ctrl = true;
2214
- break;
2215
- case 'ctrl':
2216
- case 'control':
2217
- parts.ctrl = true;
2218
- break;
2219
- case 'meta':
2220
- case 'cmd':
2221
- case 'command':
2222
- case 'win':
2238
+ }
2239
+ return parts.key ? toSignature(parts) : null;
2240
+ }
2241
+ function applyToken(parts, token, isMac) {
2242
+ switch (token) {
2243
+ case 'mod': {
2244
+ if (isMac)
2223
2245
  parts.meta = true;
2224
- break;
2225
- case 'alt':
2226
- case 'option':
2227
- parts.alt = true;
2228
- break;
2229
- case 'shift':
2230
- parts.shift = true;
2231
- break;
2232
- default:
2233
- parts.key = normaliseKey(token);
2246
+ else
2247
+ parts.ctrl = true;
2248
+ return;
2249
+ }
2250
+ case 'ctrl':
2251
+ case 'control': {
2252
+ parts.ctrl = true;
2253
+ return;
2254
+ }
2255
+ case 'meta':
2256
+ case 'cmd':
2257
+ case 'command':
2258
+ case 'win': {
2259
+ parts.meta = true;
2260
+ return;
2261
+ }
2262
+ case 'alt':
2263
+ case 'option': {
2264
+ parts.alt = true;
2265
+ return;
2266
+ }
2267
+ case 'shift': {
2268
+ parts.shift = true;
2269
+ return;
2270
+ }
2271
+ default: {
2272
+ parts.key = normaliseKey(token);
2234
2273
  }
2235
2274
  }
2236
- return parts.key ? toSignature(parts) : null;
2237
2275
  }
2238
2276
  function formatShortcut(chord, isMac) {
2239
2277
  const tokens = chord.split('+').map((raw) => {
2240
2278
  const token = raw.trim().toLowerCase();
2241
2279
  switch (token) {
2242
- case 'mod':
2280
+ case 'mod': {
2243
2281
  return isMac ? '⌘' : 'Ctrl';
2282
+ }
2244
2283
  case 'ctrl':
2245
- case 'control':
2284
+ case 'control': {
2246
2285
  return isMac ? '⌃' : 'Ctrl';
2286
+ }
2247
2287
  case 'meta':
2248
2288
  case 'cmd':
2249
2289
  case 'command':
2250
- case 'win':
2290
+ case 'win': {
2251
2291
  return isMac ? '⌘' : 'Meta';
2292
+ }
2252
2293
  case 'alt':
2253
- case 'option':
2294
+ case 'option': {
2254
2295
  return isMac ? '⌥' : 'Alt';
2255
- case 'shift':
2296
+ }
2297
+ case 'shift': {
2256
2298
  return isMac ? '⇧' : 'Shift';
2299
+ }
2257
2300
  case 'enter':
2258
- case 'return':
2301
+ case 'return': {
2259
2302
  return isMac ? '↵' : 'Enter';
2303
+ }
2260
2304
  case 'escape':
2261
- case 'esc':
2305
+ case 'esc': {
2262
2306
  return 'Esc';
2307
+ }
2263
2308
  case 'space':
2264
2309
  case 'spacebar':
2265
- case ' ':
2310
+ case ' ': {
2266
2311
  return 'Space';
2267
- default:
2312
+ }
2313
+ default: {
2268
2314
  return token.length === 1
2269
2315
  ? token.toUpperCase()
2270
2316
  : token.charAt(0).toUpperCase() + token.slice(1);
2317
+ }
2271
2318
  }
2272
2319
  });
2273
2320
  return tokens.join(isMac ? '' : '+');
@@ -2428,20 +2475,20 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.0", ngImpor
2428
2475
  type: Service
2429
2476
  }] });
2430
2477
 
2431
- function upgradeElementProperty(el, name) {
2432
- const self = el;
2433
- if (Object.prototype.hasOwnProperty.call(el, name)) {
2478
+ function upgradeElementProperty(element, name) {
2479
+ const self = element;
2480
+ if (Object.hasOwn(element, name)) {
2434
2481
  const value = self[name];
2435
2482
  delete self[name];
2436
2483
  self[name] = value;
2437
2484
  }
2438
2485
  }
2439
- function reflectAttribute(el, name, value) {
2486
+ function reflectAttribute(element, name, value) {
2440
2487
  if (value === null || value === undefined) {
2441
- el.removeAttribute(name);
2488
+ element.removeAttribute(name);
2442
2489
  }
2443
2490
  else {
2444
- el.setAttribute(name, value);
2491
+ element.setAttribute(name, value);
2445
2492
  }
2446
2493
  }
2447
2494
 
@@ -2568,7 +2615,9 @@ class LwMenuElement extends HTMLElement {
2568
2615
  return;
2569
2616
  }
2570
2617
  this.active = (index + items.length) % items.length;
2571
- items.forEach((item, i) => (item.tabIndex = i === this.active ? 0 : -1));
2618
+ for (const [index_, item] of items.entries()) {
2619
+ item.tabIndex = index_ === this.active ? 0 : -1;
2620
+ }
2572
2621
  const item = items[this.active];
2573
2622
  item.focus();
2574
2623
  item.scrollIntoView?.({ block: 'nearest' });
@@ -2581,18 +2630,22 @@ class LwMenuElement extends HTMLElement {
2581
2630
  }
2582
2631
  handleKeydown(event) {
2583
2632
  switch (event.key) {
2584
- case 'ArrowDown':
2633
+ case 'ArrowDown': {
2585
2634
  this.setActive(this.active < 0 ? 0 : this.active + 1);
2586
2635
  break;
2587
- case 'ArrowUp':
2588
- this.setActive(this.active < 0 ? this.items().length - 1 : this.active - 1);
2636
+ }
2637
+ case 'ArrowUp': {
2638
+ this.setActive((this.active < 0 ? this.items().length : this.active) - 1);
2589
2639
  break;
2590
- case 'Home':
2640
+ }
2641
+ case 'Home': {
2591
2642
  this.setActive(0);
2592
2643
  break;
2593
- case 'End':
2644
+ }
2645
+ case 'End': {
2594
2646
  this.setActive(this.items().length - 1);
2595
2647
  break;
2648
+ }
2596
2649
  case 'Enter':
2597
2650
  case ' ': {
2598
2651
  const item = this.active >= 0 ? this.items()[this.active] : undefined;
@@ -2602,11 +2655,13 @@ class LwMenuElement extends HTMLElement {
2602
2655
  break;
2603
2656
  }
2604
2657
  case 'Escape':
2605
- case 'Tab':
2658
+ case 'Tab': {
2606
2659
  this.dispatchEvent(new CustomEvent(LW_MENU_DISMISS, { bubbles: true }));
2607
2660
  return;
2608
- default:
2661
+ }
2662
+ default: {
2609
2663
  return;
2664
+ }
2610
2665
  }
2611
2666
  event.preventDefault();
2612
2667
  }
@@ -2685,7 +2740,7 @@ class MenuService {
2685
2740
  document.body.append(menu);
2686
2741
  document.body.classList.add('lw-menu-open');
2687
2742
  menu.openAt(at.x, at.y);
2688
- const listenTimer = setTimeout(() => document.addEventListener('pointerdown', onOutside, true));
2743
+ const listenTimer = setTimeout(() => document.addEventListener('pointerdown', onOutside, { capture: true }), 0);
2689
2744
  this.current = { menu, onOutside, restore, listenTimer };
2690
2745
  }
2691
2746
  resolve(menuIds, context) {
@@ -2718,7 +2773,7 @@ class MenuService {
2718
2773
  };
2719
2774
  })
2720
2775
  .filter((entry) => entry !== null)
2721
- .sort((a, b) => a.group.localeCompare(b.group) || a.order - b.order);
2776
+ .toSorted((a, b) => a.group.localeCompare(b.group) || a.order - b.order);
2722
2777
  }
2723
2778
  createMenu(resolved) {
2724
2779
  const menu = document.createElement(LW_MENU_TAG);
@@ -2871,7 +2926,7 @@ class ShellBarItem {
2871
2926
  shortcut = computed(() => {
2872
2927
  const button = this.asButton();
2873
2928
  if (!button?.showShortcut || !button.command) {
2874
- return undefined;
2929
+ return;
2875
2930
  }
2876
2931
  return this.commands.shortcutOf(this.commands.commands().find((entry) => entry.id === button.command));
2877
2932
  }, /* @ts-ignore */
@@ -2911,7 +2966,7 @@ class ShellBar {
2911
2966
  .filter((item) => item.bar === this.region().id && item.slot === slot)
2912
2967
  .filter((item) => this.auth.visible(item.access))
2913
2968
  .filter((item) => 'component' in item || this.commands.triggerable(item))
2914
- .sort((a, b) => (a.order ?? 0) - (b.order ?? 0));
2969
+ .toSorted((a, b) => (a.order ?? 0) - (b.order ?? 0));
2915
2970
  }
2916
2971
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.0", ngImport: i0, type: ShellBar, deps: [], target: i0.ɵɵFactoryTarget.Component });
2917
2972
  static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.1.0", type: ShellBar, isStandalone: true, selector: "lw-shell-bar", inputs: { region: { classPropertyName: "region", publicName: "region", isSignal: true, isRequired: true, transformFunction: null } }, ngImport: i0, template: "<header\n class=\"flex items-center gap-2 overflow-hidden border-border bg-surface px-2 md:px-4\"\n [class.border-b]=\"dock() === 'top'\"\n [class.h-12]=\"dock() === 'top'\"\n [class.border-t]=\"dock() === 'bottom' || footer()\"\n [class.py-1]=\"dock() === 'bottom' || footer()\"\n [class.text-xs]=\"dock() === 'bottom'\"\n [class.text-content-muted]=\"dock() === 'bottom'\"\n>\n <div class=\"flex min-w-0 items-center gap-2\">\n @for (item of startItems(); track item.id) {\n <lw-shell-bar-item [item]=\"item\" [dock]=\"dock()\" />\n }\n </div>\n\n @if (centerItems().length) {\n <div class=\"mx-auto flex shrink-0 items-center gap-2\">\n @for (item of centerItems(); track item.id) {\n <lw-shell-bar-item [item]=\"item\" [dock]=\"dock()\" />\n }\n </div>\n }\n\n <div class=\"ml-auto flex shrink-0 items-center gap-2\">\n @for (item of endItems(); track item.id) {\n <lw-shell-bar-item [item]=\"item\" [dock]=\"dock()\" />\n }\n </div>\n</header>\n", dependencies: [{ kind: "component", type: ShellBarItem, selector: "lw-shell-bar-item", inputs: ["item", "dock"] }] });
@@ -3182,7 +3237,7 @@ class RailItemsService {
3182
3237
  }
3183
3238
  isVisible(itemId) {
3184
3239
  if (isWorkspaceRailItem(itemId)) {
3185
- return itemId in this.state().placed;
3240
+ return Object.hasOwn(this.state().placed, itemId);
3186
3241
  }
3187
3242
  return !this.state().hidden.includes(itemId);
3188
3243
  }
@@ -3318,15 +3373,15 @@ class Reorderable {
3318
3373
  moveByKeyboard(item, direction) {
3319
3374
  const id = item.dataset['reorderId'] ?? '';
3320
3375
  const band = item.dataset['reorderBand'] ?? '';
3321
- const inBand = this.items().filter((el) => (el.dataset['reorderBand'] ?? '') === band);
3322
- const bandIds = inBand.map((el) => el.dataset['reorderId'] ?? '');
3376
+ const inBand = this.items().filter((element) => (element.dataset['reorderBand'] ?? '') === band);
3377
+ const bandIds = inBand.map((element) => element.dataset['reorderId'] ?? '');
3323
3378
  const target = bandIds.indexOf(id) + direction;
3324
3379
  if (target < 0 || target >= bandIds.length) {
3325
3380
  return false;
3326
3381
  }
3327
3382
  const neighbourId = bandIds[target];
3328
3383
  const without = this.items()
3329
- .map((el) => el.dataset['reorderId'] ?? '')
3384
+ .map((element) => element.dataset['reorderId'] ?? '')
3330
3385
  .filter((x) => x !== id);
3331
3386
  const insertAt = without.indexOf(neighbourId) + (direction > 0 ? 1 : 0);
3332
3387
  without.splice(insertAt, 0, id);
@@ -3341,7 +3396,7 @@ class Reorderable {
3341
3396
  }
3342
3397
  focusItem(id) {
3343
3398
  this.items()
3344
- .find((el) => (el.dataset['reorderId'] ?? '') === id)
3399
+ .find((element) => (element.dataset['reorderId'] ?? '') === id)
3345
3400
  ?.focus();
3346
3401
  }
3347
3402
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.0", ngImport: i0, type: Reorderable, deps: [], target: i0.ɵɵFactoryTarget.Directive });
@@ -3392,8 +3447,8 @@ class UserOrderService {
3392
3447
  const rank = new Map(sequence.map((id, index) => [id, index]));
3393
3448
  const ranked = (item) => rank.has(key(item));
3394
3449
  const knownInUserOrder = items
3395
- .filter(ranked)
3396
- .sort((a, b) => (rank.get(key(a)) ?? 0) - (rank.get(key(b)) ?? 0));
3450
+ .filter((item) => ranked(item))
3451
+ .toSorted((a, b) => (rank.get(key(a)) ?? 0) - (rank.get(key(b)) ?? 0));
3397
3452
  let next = 0;
3398
3453
  return items.map((item) => ranked(item) ? knownInUserOrder[next++] : item);
3399
3454
  }
@@ -3513,7 +3568,7 @@ function defaultTabTitle(route, root) {
3513
3568
  function withRefreshedPath(tabs, index, path) {
3514
3569
  return tabs[index].path === path
3515
3570
  ? tabs
3516
- : tabs.map((tab, i) => (i === index ? { ...tab, path } : tab));
3571
+ : tabs.map((tab, index_) => (index_ === index ? { ...tab, path } : tab));
3517
3572
  }
3518
3573
  function autoOpenedTab(route, root, path) {
3519
3574
  if (!route ||
@@ -3553,11 +3608,11 @@ function toPaneTab(tab) {
3553
3608
  return {
3554
3609
  path: tab.path,
3555
3610
  title: tab.title,
3556
- ...(tab.literalTitle ? { literalTitle: true } : {}),
3557
- ...(tab.icon === undefined ? {} : { icon: tab.icon }),
3558
- ...(tab.pinned ? { pinned: true } : {}),
3559
- ...(tab.preview ? { preview: true } : {}),
3560
- ...(tab.closable ? {} : { closable: false }),
3611
+ ...(tab.literalTitle && { literalTitle: true }),
3612
+ ...(tab.icon !== undefined && { icon: tab.icon }),
3613
+ ...(tab.pinned && { pinned: true }),
3614
+ ...(tab.preview && { preview: true }),
3615
+ ...(!tab.closable && { closable: false }),
3561
3616
  };
3562
3617
  }
3563
3618
  function facetTabViews(routes, addressOf = (route) => route.path) {
@@ -3565,7 +3620,7 @@ function facetTabViews(routes, addressOf = (route) => route.path) {
3565
3620
  .filter((route) => route.follows === true)
3566
3621
  .map((route) => ({ route, address: addressOf(route) }))
3567
3622
  .filter((entry) => entry.address !== null)
3568
- .sort((a, b) => (a.route.order ?? 0) - (b.route.order ?? 0) ||
3623
+ .toSorted((a, b) => (a.route.order ?? 0) - (b.route.order ?? 0) ||
3569
3624
  a.route.path.localeCompare(b.route.path))
3570
3625
  .map(({ route, address }, index) => ({
3571
3626
  path: route.path,
@@ -3656,11 +3711,11 @@ function collidingParam(pattern, other) {
3656
3711
  function paramPrefixes(pattern) {
3657
3712
  const prefixes = new Map();
3658
3713
  const segments = segmentsOf(pattern);
3659
- segments.forEach((segment, index) => {
3714
+ for (const [index, segment] of segments.entries()) {
3660
3715
  if (segment.startsWith(':')) {
3661
3716
  prefixes.set(segment.slice(1), segments.slice(0, index).join('/'));
3662
3717
  }
3663
- });
3718
+ }
3664
3719
  return prefixes;
3665
3720
  }
3666
3721
 
@@ -3748,22 +3803,22 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.0", ngImpor
3748
3803
  type: Service
3749
3804
  }] });
3750
3805
 
3751
- function transformLeaf(node, paneId, fn) {
3806
+ function transformLeaf(node, paneId, function_) {
3752
3807
  if (node.kind === 'leaf') {
3753
- return node.id === paneId ? fn(node) : node;
3808
+ return node.id === paneId ? function_(node) : node;
3754
3809
  }
3755
- const first = transformLeaf(node.first, paneId, fn);
3756
- const second = transformLeaf(node.second, paneId, fn);
3810
+ const first = transformLeaf(node.first, paneId, function_);
3811
+ const second = transformLeaf(node.second, paneId, function_);
3757
3812
  return first === node.first && second === node.second
3758
3813
  ? node
3759
3814
  : { ...node, first, second };
3760
3815
  }
3761
- function collapseLeaf(node, paneId, fn) {
3816
+ function collapseLeaf(node, paneId, function_) {
3762
3817
  if (node.kind === 'leaf') {
3763
- return node.id === paneId ? fn(node) : node;
3818
+ return node.id === paneId ? function_(node) : node;
3764
3819
  }
3765
- const first = collapseLeaf(node.first, paneId, fn);
3766
- const second = collapseLeaf(node.second, paneId, fn);
3820
+ const first = collapseLeaf(node.first, paneId, function_);
3821
+ const second = collapseLeaf(node.second, paneId, function_);
3767
3822
  if (first === null) {
3768
3823
  return second;
3769
3824
  }
@@ -3878,8 +3933,8 @@ function pruneEmptyLeaves(node, primaryId, spare) {
3878
3933
  function insertTab(node, paneId, tab, index) {
3879
3934
  return transformLeaf(node, paneId, (leaf) => {
3880
3935
  const existingIndex = leaf.tabs.findIndex((existing) => existing.path === tab.path);
3881
- if (existingIndex >= 0) {
3882
- const tabs = leaf.tabs.map((existing, i) => i === existingIndex ? tab : existing);
3936
+ if (existingIndex !== -1) {
3937
+ const tabs = leaf.tabs.map((existing, index_) => index_ === existingIndex ? tab : existing);
3883
3938
  return { ...leaf, tabs, active: tab.path };
3884
3939
  }
3885
3940
  const at = index === undefined
@@ -3917,7 +3972,7 @@ function refineLeafTitles(leaf, skipLeaf, matches, patch) {
3917
3972
  ...tab,
3918
3973
  title: patch.title,
3919
3974
  literalTitle: patch.literalTitle,
3920
- ...(patch.icon === undefined ? {} : { icon: patch.icon }),
3975
+ ...(patch.icon !== undefined && { icon: patch.icon }),
3921
3976
  };
3922
3977
  });
3923
3978
  return found
@@ -3955,7 +4010,7 @@ function reseatPinned(tabs, index, updated) {
3955
4010
  function pinTab(node, paneId, tabPath) {
3956
4011
  return transformLeaf(node, paneId, (leaf) => {
3957
4012
  const index = leaf.tabs.findIndex((tab) => tab.path === tabPath);
3958
- if (index < 0 || leaf.tabs[index].pinned === true) {
4013
+ if (index === -1 || leaf.tabs[index].pinned === true) {
3959
4014
  return leaf;
3960
4015
  }
3961
4016
  const pinned = {
@@ -3968,7 +4023,7 @@ function pinTab(node, paneId, tabPath) {
3968
4023
  function unpinTab(node, paneId, tabPath) {
3969
4024
  return transformLeaf(node, paneId, (leaf) => {
3970
4025
  const index = leaf.tabs.findIndex((tab) => tab.path === tabPath && tab.pinned);
3971
- if (index < 0) {
4026
+ if (index === -1) {
3972
4027
  return leaf;
3973
4028
  }
3974
4029
  const unpinned = tabWithout(leaf.tabs[index], 'pinned');
@@ -4025,7 +4080,7 @@ class PaneTreeService {
4025
4080
  }
4026
4081
  stackView(dock, viewId) {
4027
4082
  const segments = paneSegments(this.tree(dock));
4028
- const last = segments[segments.length - 1];
4083
+ const last = segments.at(-1);
4029
4084
  if (last) {
4030
4085
  this.splitPane(dock, last.id, 'column', VIEW_PANE_PREFIX + viewId);
4031
4086
  }
@@ -4058,7 +4113,7 @@ class PaneTreeService {
4058
4113
  return;
4059
4114
  }
4060
4115
  const rank = new Map(order.map((path, index) => [path, index]));
4061
- const tabs = [...leaf.tabs].sort((a, b) => (rank.get(a.path) ?? leaf.tabs.indexOf(a)) -
4116
+ const tabs = [...leaf.tabs].toSorted((a, b) => (rank.get(a.path) ?? leaf.tabs.indexOf(a)) -
4062
4117
  (rank.get(b.path) ?? leaf.tabs.indexOf(b)));
4063
4118
  this.commit(dock, setTabs(this.tree(dock), paneId, tabs));
4064
4119
  }
@@ -4110,7 +4165,7 @@ class PaneTreeService {
4110
4165
  const tree = this.tree(dock);
4111
4166
  const primary = this.primaryId(dock);
4112
4167
  const leaf = paneId === primary ? findLeaf(tree, primary) : undefined;
4113
- if (leaf && !leaf.tabs.some((tab) => tab.path === tabPath)) {
4168
+ if (leaf?.tabs.every((tab) => tab.path !== tabPath)) {
4114
4169
  return;
4115
4170
  }
4116
4171
  if (leaf && !leaf.declared && leaf.tabs.length <= 1) {
@@ -4165,7 +4220,7 @@ class PaneTreeService {
4165
4220
  return this.docks()[dock] !== undefined;
4166
4221
  }
4167
4222
  dropDock(dock) {
4168
- if (!this.docks()[dock]) {
4223
+ if (!this.hasDock(dock)) {
4169
4224
  return;
4170
4225
  }
4171
4226
  this.docks.update((docks) => {
@@ -4196,7 +4251,7 @@ class PaneTreeService {
4196
4251
  this.docks.update((current) => {
4197
4252
  const merged = { ...persisted };
4198
4253
  for (const [dock, entry] of Object.entries(current)) {
4199
- if (isContainerDock(dock) && !(dock in persisted)) {
4254
+ if (isContainerDock(dock) && !Object.hasOwn(persisted, dock)) {
4200
4255
  merged[dock] = entry;
4201
4256
  }
4202
4257
  }
@@ -4245,6 +4300,12 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.0", ngImpor
4245
4300
  type: Service
4246
4301
  }], ctorParameters: () => [] });
4247
4302
 
4303
+ function popoutNavigationRefusal(path) {
4304
+ return (`Content navigation to "${path}" was ignored: this is a pop-out window, which shows one ` +
4305
+ `surface and has no content area to navigate. A command reaches a pop-out's palette only ` +
4306
+ `if it declares popout: true, so leave that off anything that navigates.`);
4307
+ }
4308
+
4248
4309
  class OpenTabsService {
4249
4310
  router = inject(Router);
4250
4311
  registry = inject(ContributionRegistry);
@@ -4331,7 +4392,7 @@ class OpenTabsService {
4331
4392
  activeViewInstance = computed(() => {
4332
4393
  const path = this.activeViewPath();
4333
4394
  if (path === null) {
4334
- return undefined;
4395
+ return;
4335
4396
  }
4336
4397
  return this.paneTree
4337
4398
  .primaryTabs(CONTENT_DOCK)
@@ -4367,7 +4428,7 @@ class OpenTabsService {
4367
4428
  const facets = facetTabViews(routes, (route) => this.addressOf(route));
4368
4429
  const open = this.openTabs().filter((tab) => this.strippable(routes, tab));
4369
4430
  const dynamics = dynamicTabViews(routes, open);
4370
- return [...facets, ...dynamics, ...this.viewTabs()].sort((a, b) => a.order - b.order);
4431
+ return [...facets, ...dynamics, ...this.viewTabs()].toSorted((a, b) => a.order - b.order);
4371
4432
  }, /* @ts-ignore */
4372
4433
  ...(ngDevMode ? [{ debugName: "tabs" }] : /* istanbul ignore next */ []));
4373
4434
  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 */
@@ -4383,9 +4444,11 @@ class OpenTabsService {
4383
4444
  this.paneTree.hydrated();
4384
4445
  untracked(() => {
4385
4446
  if (url !== this.lastUrl) {
4447
+ const previous = this.lastUrl;
4386
4448
  this.lastUrl = url;
4387
4449
  if (this.ownNavigation !== normalizePath(url)) {
4388
4450
  this.viewTabSelection.set(null);
4451
+ this.focusHolderOf(path, this.rootFor(previous).root);
4389
4452
  }
4390
4453
  this.ownNavigation = null;
4391
4454
  }
@@ -4404,14 +4467,12 @@ class OpenTabsService {
4404
4467
  navigate(path) {
4405
4468
  if (this.inPopout) {
4406
4469
  if (isDevMode()) {
4407
- console.warn(`Content navigation to "${path}" was ignored: this is a pop-out window, which shows one ` +
4408
- `surface and has no content area to navigate. A command reaches a pop-out's palette only ` +
4409
- `if it declares popout: true, so leave that off anything that navigates.`);
4470
+ console.warn(popoutNavigationRefusal(path));
4410
4471
  }
4411
4472
  return Promise.resolve(false);
4412
4473
  }
4413
4474
  const target = normalizePath(path);
4414
- this.focusHolderOf(target);
4475
+ this.focusHolderOf(target, this.activeTabRoot());
4415
4476
  this.ownNavigation = target;
4416
4477
  this.viewTabSelection.set(null);
4417
4478
  return this.router.navigateByUrl('/' + target + suffixOf(path));
@@ -4419,9 +4480,9 @@ class OpenTabsService {
4419
4480
  navigateTo(path) {
4420
4481
  this.navigate(path).catch((error) => console.error('Content navigation failed', error));
4421
4482
  }
4422
- updateOpen(fn) {
4483
+ updateOpen(function_) {
4423
4484
  const current = this.openTabs();
4424
- const next = fn(current);
4485
+ const next = function_(current);
4425
4486
  if (next === current) {
4426
4487
  return;
4427
4488
  }
@@ -4467,7 +4528,7 @@ class OpenTabsService {
4467
4528
  return (owner.rest === true ||
4468
4529
  segmentsOf(owner.path).length === segmentsOf(address).length);
4469
4530
  }
4470
- focusHolderOf(target) {
4531
+ focusHolderOf(target, previousContent) {
4471
4532
  const routes = this.registry.contentRoutes();
4472
4533
  const root = tabRootOf(routes, target);
4473
4534
  if (root === '') {
@@ -4485,7 +4546,7 @@ class OpenTabsService {
4485
4546
  return;
4486
4547
  }
4487
4548
  this.paneTree.setActiveTab(CONTENT_DOCK, holder.id, held.path);
4488
- this.paneTree.focusPane(CONTENT_DOCK, holder.id, this.activeTabRoot());
4549
+ this.paneTree.focusPane(CONTENT_DOCK, holder.id, previousContent);
4489
4550
  }
4490
4551
  stampActive(root) {
4491
4552
  const next = new Map(this.lastActive());
@@ -4496,7 +4557,7 @@ class OpenTabsService {
4496
4557
  const routes = this.registry.contentRoutes();
4497
4558
  this.updateOpen((tabs) => {
4498
4559
  const index = tabs.findIndex((tab) => tabRootOf(routes, tab.path) === root);
4499
- if (index >= 0) {
4560
+ if (index !== -1) {
4500
4561
  return withRefreshedPath(tabs, index, path);
4501
4562
  }
4502
4563
  const opened = autoOpenedTab(route, root, path);
@@ -4511,7 +4572,7 @@ class OpenTabsService {
4511
4572
  return (route !== undefined &&
4512
4573
  route.chromeless !== true &&
4513
4574
  route.follows !== true &&
4514
- !(normalizePath(route.path) === '' && normalizePath(tab.path) !== ''));
4575
+ (normalizePath(route.path) !== '' || normalizePath(tab.path) === ''));
4515
4576
  }
4516
4577
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.0", ngImport: i0, type: OpenTabsService, deps: [], target: i0.ɵɵFactoryTarget.Service });
4517
4578
  static ɵprov = i0.ɵɵngDeclareService({ minVersion: "22.0.0", version: "22.1.0", ngImport: i0, type: OpenTabsService });
@@ -4927,7 +4988,7 @@ class SurfaceCloseGuard {
4927
4988
  }
4928
4989
  async saveAll(dirty) {
4929
4990
  try {
4930
- await Promise.all(dirty.map((surface) => surface.surfaceSave?.()));
4991
+ await Promise.all(dirty.map(async (surface) => surface.surfaceSave?.()));
4931
4992
  }
4932
4993
  catch (error) {
4933
4994
  console.error('Save before closing failed', error);
@@ -5074,9 +5135,17 @@ class TabClosingService {
5074
5135
  const closing = this.state.openTabs().filter((tab) => roots.has(tabRootOf(routes, tab.path)));
5075
5136
  const activeWentAway = roots.has(this.state.activeTabRoot());
5076
5137
  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));
5138
+ for (const tab of closing) {
5139
+ this.closeHooks.runSafely(tab.onClose);
5140
+ }
5141
+ for (const root of roots) {
5142
+ this.closeHooks.delete(root);
5143
+ }
5144
+ const evictAll = () => {
5145
+ for (const root of roots) {
5146
+ this.reuse.evict(root);
5147
+ }
5148
+ };
5080
5149
  if (activeWentAway) {
5081
5150
  void this.state.navigate(fallbackPath)
5082
5151
  .catch((error) => console.error('Content navigation failed', error))
@@ -5108,7 +5177,7 @@ class TabClosingService {
5108
5177
  neighbourPath(root) {
5109
5178
  const routes = this.registry.contentRoutes();
5110
5179
  const siblings = this.state.openTabs().filter((tab) => tabRootOf(routes, tab.path) !== root);
5111
- return siblings.length > 0 ? siblings[siblings.length - 1].path : '';
5180
+ return siblings.at(-1)?.path ?? '';
5112
5181
  }
5113
5182
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.0", ngImport: i0, type: TabClosingService, deps: [], target: i0.ɵɵFactoryTarget.Service });
5114
5183
  static ɵprov = i0.ɵɵngDeclareService({ minVersion: "22.0.0", version: "22.1.0", ngImport: i0, type: TabClosingService });
@@ -5193,7 +5262,7 @@ class ContentTabsService {
5193
5262
  const seat = (tab, fallback) => rank.get(tabRootOf(routes, tab.path)) ?? fallback;
5194
5263
  this.state.updateOpen((tabs) => tabs
5195
5264
  .map((tab, index) => ({ tab, index }))
5196
- .sort((a, b) => seat(a.tab, a.index) - seat(b.tab, b.index))
5265
+ .toSorted((a, b) => seat(a.tab, a.index) - seat(b.tab, b.index))
5197
5266
  .map((entry) => entry.tab));
5198
5267
  }
5199
5268
  /**
@@ -5209,7 +5278,7 @@ class ContentTabsService {
5209
5278
  const { routes, root } = this.state.rootFor(path);
5210
5279
  this.state.updateOpen((tabs) => {
5211
5280
  const index = tabs.findIndex((tab) => tabRootOf(routes, tab.path) === root);
5212
- return index < 0 || tabs[index].pinned
5281
+ return index === -1 || tabs[index].pinned
5213
5282
  ? tabs
5214
5283
  : reseatPinned(tabs, index, tabs[index]);
5215
5284
  });
@@ -5364,7 +5433,7 @@ class ContentTabsService {
5364
5433
  const { routes, root } = this.state.rootFor(path);
5365
5434
  this.state.updateOpen((tabs) => {
5366
5435
  const index = tabs.findIndex((tab) => tabRootOf(routes, tab.path) === root);
5367
- if (index < 0 || tabs[index].pinned === pinned) {
5436
+ if (index === -1 || tabs[index].pinned === pinned) {
5368
5437
  return tabs;
5369
5438
  }
5370
5439
  const updated = pinned
@@ -5434,7 +5503,7 @@ class PanelViewsService {
5434
5503
  const declared = this.registry
5435
5504
  .views()
5436
5505
  .filter((view) => view.region === regionId)
5437
- .sort((a, b) => (a.order ?? 0) - (b.order ?? 0));
5506
+ .toSorted((a, b) => (a.order ?? 0) - (b.order ?? 0));
5438
5507
  return this.order.applyOrder(panelViewsContainerId(regionId), declared, (view) => view.id);
5439
5508
  }
5440
5509
  candidatesFor(regionId) {
@@ -5449,7 +5518,7 @@ class PanelViewsService {
5449
5518
  (panelRegions.has(view.region) &&
5450
5519
  (holder === null || !panelRegions.has(holder))))
5451
5520
  .map(({ view, holder }) => ({ view, here: holder === regionId }))
5452
- .sort((a, b) => Number(b.here) - Number(a.here) ||
5521
+ .toSorted((a, b) => Number(b.here) - Number(a.here) ||
5453
5522
  (a.view.order ?? 0) - (b.view.order ?? 0));
5454
5523
  }
5455
5524
  holderOf(viewId) {
@@ -5485,7 +5554,7 @@ class HiddenViewsService {
5485
5554
  ...(ngDevMode ? [{ debugName: "ids" }] : /* istanbul ignore next */ []));
5486
5555
  hidden = this.ids.asReadonly();
5487
5556
  constructor() {
5488
- void this.workspace.ready.then(() => hydrateAsync(this.store, this.storageKey(), (raw) => this.ids.set(parseHiddenViews(raw))));
5557
+ this.hydrateWhenWorkspaceReady();
5489
5558
  }
5490
5559
  isHidden(viewId) {
5491
5560
  return this.ids().has(viewId);
@@ -5509,7 +5578,7 @@ class HiddenViewsService {
5509
5578
  void this.store.set(this.storageKey(), this.serialize());
5510
5579
  }
5511
5580
  serialize() {
5512
- return JSON.stringify([...this.ids()].sort());
5581
+ return JSON.stringify([...this.ids()].toSorted((a, b) => a.localeCompare(b)));
5513
5582
  }
5514
5583
  commit(next) {
5515
5584
  this.ids.set(next);
@@ -5518,6 +5587,9 @@ class HiddenViewsService {
5518
5587
  storageKey() {
5519
5588
  return this.workspace.scopedKey(STORAGE_KEY$b);
5520
5589
  }
5590
+ hydrateWhenWorkspaceReady() {
5591
+ void this.workspace.ready.then(() => hydrateAsync(this.store, this.storageKey(), (raw) => this.ids.set(parseHiddenViews(raw))));
5592
+ }
5521
5593
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.0", ngImport: i0, type: HiddenViewsService, deps: [], target: i0.ɵɵFactoryTarget.Service });
5522
5594
  static ɵprov = i0.ɵɵngDeclareService({ minVersion: "22.0.0", version: "22.1.0", ngImport: i0, type: HiddenViewsService });
5523
5595
  }
@@ -5624,9 +5696,9 @@ function tabGaps(definition, routes, id) {
5624
5696
  }
5625
5697
  function sidebarGaps(definition, declaredPaths, id) {
5626
5698
  return Object.entries(definition.sidebars ?? {}).flatMap(([region, visible]) => {
5627
- const declared = declaredPaths(region).map((path) => path.slice(VIEW_PANE_PREFIX.length));
5699
+ const declared = new Set(declaredPaths(region).map((path) => path.slice(VIEW_PANE_PREFIX.length)));
5628
5700
  return visible
5629
- .filter((viewId) => !declared.includes(viewId))
5701
+ .filter((viewId) => !declared.has(viewId))
5630
5702
  .map((viewId) => `Workspace "${id}": sidebar view "${viewId}" is not declared for region "${region}" — the entry has no effect.`);
5631
5703
  });
5632
5704
  }
@@ -5643,7 +5715,7 @@ function canonicalState(read, shape) {
5643
5715
  }
5644
5716
  function canonicalValue(key, raw, hidden, shape) {
5645
5717
  if (key === shape.hiddenViewsKey) {
5646
- return JSON.stringify([...hidden].sort());
5718
+ return JSON.stringify([...hidden].toSorted((a, b) => a.localeCompare(b)));
5647
5719
  }
5648
5720
  if (key === shape.paneTreesKey) {
5649
5721
  return canonicalTrees(raw, hidden, shape);
@@ -5657,7 +5729,8 @@ function canonicalTrees(raw, hidden, shape) {
5657
5729
  try {
5658
5730
  const parsed = JSON.parse(raw);
5659
5731
  const out = {};
5660
- for (const dock of Object.keys(parsed ?? {}).sort()) {
5732
+ const docks = Object.keys(parsed ?? {}).toSorted((a, b) => a.localeCompare(b));
5733
+ for (const dock of docks) {
5661
5734
  const entry = normalizeDockEntry(parsed[dock]);
5662
5735
  if (entry === null) {
5663
5736
  continue;
@@ -5706,7 +5779,7 @@ function comparableTab(tab) {
5706
5779
  }
5707
5780
  function comparableNode(node) {
5708
5781
  if (node.kind === 'leaf') {
5709
- return { ...node, tabs: node.tabs.map(comparableTab) };
5782
+ return { ...node, tabs: node.tabs.map((tab) => comparableTab(tab)) };
5710
5783
  }
5711
5784
  return {
5712
5785
  ...node,
@@ -5732,7 +5805,7 @@ function baseInitials(name) {
5732
5805
  if (letters.length === 1) {
5733
5806
  return letters[0].toUpperCase();
5734
5807
  }
5735
- return (letters[0] + letters[letters.length - 1]).toUpperCase();
5808
+ return (letters[0] + (letters.at(-1) ?? '')).toUpperCase();
5736
5809
  }
5737
5810
  function* candidatesFor(name) {
5738
5811
  const base = baseInitials(name);
@@ -5741,8 +5814,8 @@ function* candidatesFor(name) {
5741
5814
  }
5742
5815
  yield base;
5743
5816
  const letters = [...(wordsOf(name)[0] ?? '')];
5744
- for (let i = 1; i < letters.length; i++) {
5745
- yield (letters[0] + letters[i]).toUpperCase();
5817
+ for (let index = 1; index < letters.length; index++) {
5818
+ yield (letters[0] + letters[index]).toUpperCase();
5746
5819
  }
5747
5820
  for (let digit = 2; digit <= LAST_RESORT_DIGITS; digit++) {
5748
5821
  yield letters[0].toUpperCase() + digit;
@@ -5752,17 +5825,22 @@ function assignWorkspaceInitials(workspaces) {
5752
5825
  const taken = new Set();
5753
5826
  const assigned = new Map();
5754
5827
  for (const workspace of workspaces) {
5755
- for (const candidate of candidatesFor(workspace.name)) {
5756
- if (taken.has(candidate)) {
5757
- continue;
5758
- }
5828
+ const candidate = firstFree(candidatesFor(workspace.name), taken);
5829
+ if (candidate !== undefined) {
5759
5830
  taken.add(candidate);
5760
5831
  assigned.set(workspace.id, candidate);
5761
- break;
5762
5832
  }
5763
5833
  }
5764
5834
  return assigned;
5765
5835
  }
5836
+ function firstFree(candidates, taken) {
5837
+ for (const candidate of candidates) {
5838
+ if (!taken.has(candidate)) {
5839
+ return candidate;
5840
+ }
5841
+ }
5842
+ return undefined;
5843
+ }
5766
5844
 
5767
5845
  const STORAGE_KEY$a = 'lw.shell.workspaces';
5768
5846
  const HIDDEN_VIEWS_KEY = 'lw.shell.hidden-views';
@@ -5849,14 +5927,16 @@ class WorkspaceService {
5849
5927
  }, /* @ts-ignore */
5850
5928
  ...(ngDevMode ? [{ debugName: "changedIds" }] : /* istanbul ignore next */ []));
5851
5929
  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)));
5930
+ const setList = (raw) => this.list.set(parse(raw));
5931
+ hydrateAsync(this.store, STORAGE_KEY$a, setList);
5932
+ this.sync.register('settings', STORAGE_KEY$a, setList);
5854
5933
  if (isDevMode()) {
5855
- for (const problem of auditWorkspaceDefinitions(this.definitionBatches.flat(), this.panelRegions)) {
5934
+ const all = this.definitionBatches.flat();
5935
+ for (const problem of auditWorkspaceDefinitions(all, this.panelRegions)) {
5856
5936
  console.warn(problem);
5857
5937
  }
5858
5938
  }
5859
- void this.active.ready.then(() => this.layOutAdoptedWorkspace());
5939
+ this.layOutAdoptedWorkspaceWhenReady();
5860
5940
  }
5861
5941
  async saveCurrent(name) {
5862
5942
  const baseline = await this.currentState();
@@ -5864,7 +5944,7 @@ class WorkspaceService {
5864
5944
  const origin = this.originOf(this.active.id());
5865
5945
  this.commit([
5866
5946
  ...this.list(),
5867
- { id, name, baseline, ...(origin === null ? {} : { origin }) },
5947
+ { id, name, baseline, ...(origin !== null && { origin }) },
5868
5948
  ]);
5869
5949
  this.active.set(id);
5870
5950
  this.applyState(baseline);
@@ -5976,10 +6056,8 @@ class WorkspaceService {
5976
6056
  declaredPaths: (region) => this.panelGroups.declaredPaths(region),
5977
6057
  });
5978
6058
  return {
5979
- ...(state.hiddenViews === undefined
5980
- ? {}
5981
- : { [HIDDEN_VIEWS_KEY]: state.hiddenViews }),
5982
- ...(state.trees === undefined ? {} : { [PANE_TREES_KEY]: state.trees }),
6059
+ ...(state.hiddenViews !== undefined && { [HIDDEN_VIEWS_KEY]: state.hiddenViews }),
6060
+ ...(state.trees !== undefined && { [PANE_TREES_KEY]: state.trees }),
5983
6061
  };
5984
6062
  }
5985
6063
  warnDeclarationGaps(id) {
@@ -6043,6 +6121,9 @@ class WorkspaceService {
6043
6121
  this.list.set(next);
6044
6122
  void this.store.set(STORAGE_KEY$a, JSON.stringify(next));
6045
6123
  }
6124
+ layOutAdoptedWorkspaceWhenReady() {
6125
+ void this.active.ready.then(() => this.layOutAdoptedWorkspace());
6126
+ }
6046
6127
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.0", ngImport: i0, type: WorkspaceService, deps: [], target: i0.ɵɵFactoryTarget.Service });
6047
6128
  static ɵprov = i0.ɵɵngDeclareService({ minVersion: "22.0.0", version: "22.1.0", ngImport: i0, type: WorkspaceService });
6048
6129
  }
@@ -6081,7 +6162,7 @@ class ShellRail {
6081
6162
  .filter((item) => this.railItems.regionOf(item.id, item.rail) === this.region().id)
6082
6163
  .filter((item) => this.auth.visible(item.access))
6083
6164
  .filter((item) => item.workspace !== undefined || this.commands.triggerable(item))
6084
- .sort((a, b) => (a.order ?? 0) - (b.order ?? 0)), /* @ts-ignore */
6165
+ .toSorted((a, b) => (a.order ?? 0) - (b.order ?? 0)), /* @ts-ignore */
6085
6166
  ...(ngDevMode ? [{ debugName: "registered" }] : /* istanbul ignore next */ []));
6086
6167
  items = computed(() => {
6087
6168
  const inRail = this.registered().filter((item) => this.railItems.isVisible(item.id));
@@ -6089,7 +6170,7 @@ class ShellRail {
6089
6170
  const id = this.containerId();
6090
6171
  const key = (item) => item.id;
6091
6172
  const top = this.userOrder.applyOrder(id, inRail.filter((item) => !isBottom(item)), key);
6092
- const bottom = this.userOrder.applyOrder(id, inRail.filter(isBottom), key);
6173
+ const bottom = this.userOrder.applyOrder(id, inRail.filter((item) => isBottom(item)), key);
6093
6174
  return [...top, ...bottom];
6094
6175
  }, /* @ts-ignore */
6095
6176
  ...(ngDevMode ? [{ debugName: "items" }] : /* istanbul ignore next */ []));
@@ -6234,10 +6315,11 @@ class ViewStateService {
6234
6315
  hydrateAsync(this.store, key, (raw) => value.set(parseBlob$1(raw)));
6235
6316
  let timer;
6236
6317
  const cancelPendingSave = () => {
6237
- if (timer !== undefined) {
6238
- clearTimeout(timer);
6239
- timer = undefined;
6318
+ if (timer === undefined) {
6319
+ return;
6240
6320
  }
6321
+ clearTimeout(timer);
6322
+ timer = undefined;
6241
6323
  };
6242
6324
  const save = () => {
6243
6325
  cancelPendingSave();
@@ -6275,12 +6357,12 @@ function parseRecord(viewId, raw) {
6275
6357
  }
6276
6358
  const record = parsed;
6277
6359
  const instances = Array.isArray(record.instances)
6278
- ? record.instances.filter((i) => !!i && typeof i.id === 'string' && typeof i.name === 'string')
6360
+ ? record.instances.filter((index) => !!index && typeof index.id === 'string' && typeof index.name === 'string')
6279
6361
  : [];
6280
- const withoutDefault = instances.filter((i) => i.id !== viewId);
6362
+ const withoutDefault = instances.filter((index) => index.id !== viewId);
6281
6363
  const merged = [{ id: viewId, name: '' }, ...withoutDefault];
6282
6364
  const activeId = typeof record.activeId === 'string' &&
6283
- merged.some((i) => i.id === record.activeId)
6365
+ merged.some((index) => index.id === record.activeId)
6284
6366
  ? record.activeId
6285
6367
  : viewId;
6286
6368
  return { instances: merged, activeId };
@@ -6313,7 +6395,7 @@ class ViewInstanceService {
6313
6395
  }
6314
6396
  setActive(viewId, instanceId) {
6315
6397
  const record = this.recordFor(viewId);
6316
- if (record().instances.some((i) => i.id === instanceId)) {
6398
+ if (record().instances.some((index) => index.id === instanceId)) {
6317
6399
  this.commit(viewId, { ...record(), activeId: instanceId });
6318
6400
  }
6319
6401
  }
@@ -6332,7 +6414,7 @@ class ViewInstanceService {
6332
6414
  const record = this.recordFor(viewId);
6333
6415
  this.commit(viewId, {
6334
6416
  ...record(),
6335
- instances: record().instances.map((i) => i.id === instanceId ? { ...i, name } : i),
6417
+ instances: record().instances.map((index) => index.id === instanceId ? { ...index, name } : index),
6336
6418
  });
6337
6419
  }
6338
6420
  remove(viewId, instanceId) {
@@ -6340,7 +6422,7 @@ class ViewInstanceService {
6340
6422
  return;
6341
6423
  }
6342
6424
  const record = this.recordFor(viewId);
6343
- const instances = record().instances.filter((i) => i.id !== instanceId);
6425
+ const instances = record().instances.filter((index) => index.id !== instanceId);
6344
6426
  const activeId = record().activeId === instanceId ? viewId : record().activeId;
6345
6427
  this.commit(viewId, { instances, activeId });
6346
6428
  this.viewStates.clear(instanceId);
@@ -6601,7 +6683,7 @@ class PaneChromeService {
6601
6683
  clearMinimized(dock) {
6602
6684
  const prefix = `${dock}:`;
6603
6685
  const current = this.min();
6604
- if (![...current].some((id) => id.startsWith(prefix))) {
6686
+ if ([...current].every((id) => !id.startsWith(prefix))) {
6605
6687
  return;
6606
6688
  }
6607
6689
  this.min.set(new Set([...current].filter((id) => !id.startsWith(prefix))));
@@ -6668,7 +6750,7 @@ function bakeChild(dock, spec, entry, problems, context) {
6668
6750
  return {
6669
6751
  tab: {
6670
6752
  ...containerChildTab(dock, spec, declared.surface),
6671
- ...(declared.closable === false ? { closable: false } : {}),
6753
+ ...(declared.closable === false && { closable: false }),
6672
6754
  },
6673
6755
  active: declared.active === true,
6674
6756
  };
@@ -6705,13 +6787,11 @@ class PaneContainersService {
6705
6787
  const tab = {
6706
6788
  path,
6707
6789
  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 }),
6790
+ ...(label?.title !== undefined && {
6791
+ title: label.title,
6792
+ literalTitle: label.titleIsLiteral ?? false,
6793
+ }),
6794
+ ...(label?.icon !== undefined && { icon: label.icon }),
6715
6795
  };
6716
6796
  this.paneTree.commit(dock, setActiveTab(insertTab(tree, target, tab), target, path));
6717
6797
  }
@@ -6737,11 +6817,11 @@ function offRouterPaneTargets(registry, auth) {
6737
6817
  const routes = registry
6738
6818
  .contentRoutes()
6739
6819
  .filter((route) => offRouterMountable(registry, auth, route.path))
6740
- .map(routeTarget);
6820
+ .map((route) => routeTarget(route));
6741
6821
  const views = registry
6742
6822
  .views()
6743
6823
  .filter((view) => auth.meets(view.access))
6744
- .map(viewTarget);
6824
+ .map((view) => viewTarget(view));
6745
6825
  return [...routes, ...views];
6746
6826
  }
6747
6827
  function containerChildTargets(registry, auth, spec) {
@@ -6750,14 +6830,14 @@ function containerChildTargets(registry, auth, spec) {
6750
6830
  .filter((child) => child.segment === undefined || isAddressable(child.segment))
6751
6831
  .map((child) => views.find((view) => view.id === child.surface))
6752
6832
  .filter((view) => view !== undefined && auth.meets(view.access))
6753
- .map(viewTarget);
6833
+ .map((view) => viewTarget(view));
6754
6834
  }
6755
6835
  function routerPaneTargets(registry, auth) {
6756
6836
  return registry
6757
6837
  .contentRoutes()
6758
6838
  .filter((route) => barePathHostableRoute(registry, route.path) !== null &&
6759
6839
  auth.meets(route.access))
6760
- .map(routeTarget);
6840
+ .map((route) => routeTarget(route));
6761
6841
  }
6762
6842
  function paneTargetEntries(targets, translate) {
6763
6843
  return targets.map((target) => ({
@@ -7126,7 +7206,7 @@ class PaneTabStrip {
7126
7206
  targetKind: 'view-tab',
7127
7207
  viewId: tab.path.slice(VIEW_PANE_PREFIX.length),
7128
7208
  region: this.contextGroup(),
7129
- ...(tab.instance ? { instance: tab.instance } : {}),
7209
+ ...(tab.instance && { instance: tab.instance }),
7130
7210
  };
7131
7211
  }
7132
7212
  return {
@@ -7180,22 +7260,22 @@ class PaneTabStrip {
7180
7260
  this.reorderTabs.emit(tabs.filter((tab) => this.canReorder(tab)).map((tab) => tab.path));
7181
7261
  }
7182
7262
  observeResize() {
7183
- const el = this.strip()?.nativeElement;
7184
- if (!el || typeof ResizeObserver === 'undefined') {
7263
+ const element = this.strip()?.nativeElement;
7264
+ if (!element || typeof ResizeObserver === 'undefined') {
7185
7265
  return;
7186
7266
  }
7187
7267
  const observer = new ResizeObserver(() => this.overflow() && this.measureOverflow());
7188
- observer.observe(el);
7268
+ observer.observe(element);
7189
7269
  this.destroyRef.onDestroy(() => observer.disconnect());
7190
7270
  }
7191
7271
  measureOverflow() {
7192
- const el = this.strip()?.nativeElement;
7193
- this.overflowing.set(!!el && el.scrollWidth - el.clientWidth > EDGE_TOLERANCE_PX);
7272
+ const element = this.strip()?.nativeElement;
7273
+ this.overflowing.set(!!element && element.scrollWidth - element.clientWidth > EDGE_TOLERANCE_PX);
7194
7274
  }
7195
7275
  revealActiveTab() {
7196
7276
  const strip = this.strip()?.nativeElement;
7197
7277
  const active = this.activeId();
7198
- const wrapper = strip?.querySelector(`[data-tab-path="${active}"]`)?.parentElement;
7278
+ const wrapper = strip?.querySelector(`[data-tab-path="${CSS.escape(active)}"]`)?.parentElement;
7199
7279
  if (!strip || !wrapper) {
7200
7280
  return;
7201
7281
  }
@@ -7400,12 +7480,14 @@ class PluginStateService {
7400
7480
  };
7401
7481
  }
7402
7482
  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);
7483
+ const snapshot = [...this.entries];
7484
+ for (const [storageKey, entry] of snapshot) {
7485
+ if (entry.pluginId !== pluginId) {
7486
+ continue;
7408
7487
  }
7488
+ this.cancelPending(entry);
7489
+ entry.value.set(undefined);
7490
+ this.entries.delete(storageKey);
7409
7491
  }
7410
7492
  this.keysByPlugin.delete(pluginId);
7411
7493
  void readStoredValue(this.store, INDEX_PREFIX + pluginId).then((raw) => {
@@ -7495,10 +7577,11 @@ class PluginStateService {
7495
7577
  entry.pending = undefined;
7496
7578
  }
7497
7579
  cancelTimer(entry) {
7498
- if (entry.timer !== undefined) {
7499
- clearTimeout(entry.timer);
7500
- entry.timer = undefined;
7580
+ if (entry.timer === undefined) {
7581
+ return;
7501
7582
  }
7583
+ clearTimeout(entry.timer);
7584
+ entry.timer = undefined;
7502
7585
  }
7503
7586
  withinLimits(pluginId, key, serialised) {
7504
7587
  const bytes = serialised.length;
@@ -7660,10 +7743,9 @@ function ownerByToken(registrations) {
7660
7743
  ...Object.keys(registration.dark ?? {}),
7661
7744
  ];
7662
7745
  for (const name of names) {
7663
- if (!known.has(name) || owners.has(name)) {
7664
- continue;
7746
+ if (known.has(name) && !owners.has(name)) {
7747
+ owners.set(name, registration);
7665
7748
  }
7666
- owners.set(name, registration);
7667
7749
  }
7668
7750
  }
7669
7751
  return owners;
@@ -7702,7 +7784,7 @@ function createPluginLayerRules() {
7702
7784
  }
7703
7785
  const element = document.createElement('style');
7704
7786
  element.dataset['lwPluginTheme'] = '';
7705
- document.head.appendChild(element);
7787
+ document.head.append(element);
7706
7788
  const sheet = element.sheet;
7707
7789
  if (!sheet) {
7708
7790
  return null;
@@ -8009,7 +8091,7 @@ class CapabilityGrantService {
8009
8091
  })),
8010
8092
  }))
8011
8093
  .filter((entry) => entry.capabilities.length > 0)
8012
- .sort((a, b) => a.pluginId.localeCompare(b.pluginId));
8094
+ .toSorted((a, b) => a.pluginId.localeCompare(b.pluginId));
8013
8095
  }, /* @ts-ignore */
8014
8096
  ...(ngDevMode ? [{ debugName: "permissions" }] : /* istanbul ignore next */ []));
8015
8097
  constructor() {
@@ -8146,7 +8228,7 @@ class IframeSurface {
8146
8228
  ...(ngDevMode ? [{ debugName: "activeTab" }] : /* istanbul ignore next */ []));
8147
8229
  restPath = computed(() => {
8148
8230
  if (!this.ownsRest) {
8149
- return undefined;
8231
+ return;
8150
8232
  }
8151
8233
  return this.hostMounted
8152
8234
  ? this.hostSub()
@@ -8177,7 +8259,9 @@ class IframeSurface {
8177
8259
  queueMicrotask(() => this.push({ ...snapshot, ...this.readResolved() }));
8178
8260
  });
8179
8261
  inject(DestroyRef).onDestroy(() => {
8180
- this.watched.forEach((entry) => entry.stop());
8262
+ for (const entry of this.watched.values()) {
8263
+ entry.stop();
8264
+ }
8181
8265
  this.watched.clear();
8182
8266
  this.visibility?.disconnect();
8183
8267
  this.connection?.destroy();
@@ -8213,12 +8297,12 @@ class IframeSurface {
8213
8297
  this.tabs.keep(this.tabRoot);
8214
8298
  }
8215
8299
  },
8216
- setDirty: (dirty) => this.dirty.set(dirty === true),
8217
- stateWatch: (key) => this.watchState(String(key)),
8218
- stateSet: (key, value) => this.watched.get(String(key))?.handle.set(value),
8219
- stateClear: (key) => this.watched.get(String(key))?.handle.clear(),
8300
+ setDirty: (dirty) => this.dirty.set(dirty),
8301
+ stateWatch: (key) => this.watchState(key),
8302
+ stateSet: (key, value) => this.watched.get(key)?.handle.set(value),
8303
+ stateClear: (key) => this.watched.get(key)?.handle.clear(),
8220
8304
  stateUnwatch: (key) => {
8221
- const name = String(key);
8305
+ const name = key;
8222
8306
  this.watched.get(name)?.stop();
8223
8307
  this.watched.delete(name);
8224
8308
  },
@@ -8228,7 +8312,9 @@ class IframeSurface {
8228
8312
  .then((remote) => {
8229
8313
  this.remote = remote;
8230
8314
  this.push({ ...this.reactiveState(), ...this.readResolved() });
8231
- this.watched.forEach((entry, key) => this.pushState(key, entry.handle.value(), entry.handle.loaded()));
8315
+ for (const [key, entry] of this.watched) {
8316
+ this.pushState(key, entry.handle.value(), entry.handle.loaded());
8317
+ }
8232
8318
  })
8233
8319
  .catch(() => undefined);
8234
8320
  }
@@ -8264,7 +8350,7 @@ class IframeSurface {
8264
8350
  return;
8265
8351
  }
8266
8352
  this.visibility = new IntersectionObserver((entries) => {
8267
- const last = entries[entries.length - 1];
8353
+ const last = entries.at(-1);
8268
8354
  if (last) {
8269
8355
  this.shown.set(last.isIntersecting);
8270
8356
  }
@@ -8287,19 +8373,15 @@ class IframeSurface {
8287
8373
  theme: this.theme.resolvedTheme(),
8288
8374
  preview: this.isPreview(),
8289
8375
  shown: this.shown(),
8290
- ...(this.instanceId ? { instanceId: this.instanceId } : {}),
8291
- ...(Object.keys(this.routeParams).length > 0
8292
- ? { params: this.routeParams }
8293
- : {}),
8294
- ...(rest === undefined ? {} : { rest }),
8295
- ...(this.sessionGranted()
8296
- ? {
8297
- session: {
8298
- authenticated: this.auth.authenticated(),
8299
- roles: this.auth.roles(),
8300
- },
8301
- }
8302
- : {}),
8376
+ ...(this.instanceId && { instanceId: this.instanceId }),
8377
+ ...(Object.keys(this.routeParams).length > 0 && { params: this.routeParams }),
8378
+ ...(rest !== undefined && { rest }),
8379
+ ...(this.sessionGranted() && {
8380
+ session: {
8381
+ authenticated: this.auth.authenticated(),
8382
+ roles: this.auth.roles(),
8383
+ },
8384
+ }),
8303
8385
  };
8304
8386
  }
8305
8387
  readResolved() {
@@ -8312,19 +8394,19 @@ class IframeSurface {
8312
8394
  return {
8313
8395
  tokens,
8314
8396
  rootFontSize: styles.fontSize,
8315
- ...(Object.keys(icons).length > 0 ? { icons } : {}),
8397
+ ...(Object.keys(icons).length > 0 && { icons }),
8316
8398
  };
8317
8399
  }
8318
8400
  navigateWithinTabRoot(path) {
8319
8401
  if (this.docked) {
8320
8402
  if (isDevMode()) {
8321
- console.warn(`[loom] a docked surface asked to navigate to "${String(path)}" — ignored. ` +
8403
+ console.warn(`[loom] a docked surface asked to navigate to "${path}" — ignored. ` +
8322
8404
  `A docked surface has no address of its own; the channel's navigate is confined to a tab ` +
8323
8405
  `root and there is none. Use ctx.navigateContent (the 'navigation' grant) instead.`);
8324
8406
  }
8325
8407
  return;
8326
8408
  }
8327
- const raw = String(path);
8409
+ const raw = path;
8328
8410
  const suffix = suffixOf(raw);
8329
8411
  const target = normalizePath(raw);
8330
8412
  if (target !== this.tabRoot && !target.startsWith(this.tabRoot + '/')) {
@@ -8393,8 +8475,8 @@ function syntheticDockedRoute(view, instanceId, params = {}) {
8393
8475
  url: [],
8394
8476
  params,
8395
8477
  data: {
8396
- ...(view.iframe !== undefined ? { iframe: view.iframe } : {}),
8397
- ...(view.pluginId ? { pluginId: view.pluginId } : {}),
8478
+ ...(view.iframe !== undefined && { iframe: view.iframe }),
8479
+ ...(view.pluginId && { pluginId: view.pluginId }),
8398
8480
  docked: true,
8399
8481
  instanceId,
8400
8482
  },
@@ -8410,13 +8492,13 @@ function syntheticRouteFor(route, path, options = {}) {
8410
8492
  url: segments.map((segment) => new UrlSegment(segment, {})),
8411
8493
  params,
8412
8494
  data: {
8413
- ...('iframe' in route ? { iframe: route.iframe } : {}),
8414
- ...('container' in route ? { container: route.container } : {}),
8415
- ...(route.pluginId ? { pluginId: route.pluginId } : {}),
8416
- ...(route.rest === true ? { rest: true } : {}),
8417
- ...(sub ? { sub } : {}),
8418
- ...(options.urlDriven ? { urlDriven: true } : {}),
8419
- ...(options.instanceId ? { instanceId: options.instanceId } : {}),
8495
+ ...('iframe' in route && { iframe: route.iframe }),
8496
+ ...('container' in route && { container: route.container }),
8497
+ ...(route.pluginId && { pluginId: route.pluginId }),
8498
+ ...(route.rest === true && { rest: true }),
8499
+ ...(sub && { sub }),
8500
+ ...(options.urlDriven && { urlDriven: true }),
8501
+ ...(options.instanceId && { instanceId: options.instanceId }),
8420
8502
  },
8421
8503
  });
8422
8504
  }
@@ -8652,14 +8734,12 @@ class ContentSecondaryPane {
8652
8734
  mountParams = computed(() => {
8653
8735
  const ctx = this.containerCtx;
8654
8736
  if (!ctx) {
8655
- return undefined;
8737
+ return;
8656
8738
  }
8657
8739
  const match = containerChildForPath(this.registry.contentRoutes(), this.registry.views(), this.path());
8658
8740
  return {
8659
8741
  ...ctx.params,
8660
- ...(match
8661
- ? paramsOfPattern(match.declaration.segment ?? '', match.segmentPath)
8662
- : {}),
8742
+ ...(match && paramsOfPattern(match.declaration.segment ?? '', match.segmentPath)),
8663
8743
  };
8664
8744
  }, /* @ts-ignore */
8665
8745
  ...(ngDevMode ? [{ debugName: "mountParams" }] : /* istanbul ignore next */ []));
@@ -8691,12 +8771,12 @@ class ContentSecondaryPane {
8691
8771
  ...(ngDevMode ? [{ debugName: "activeRoute" }] : /* istanbul ignore next */ []));
8692
8772
  iframeSurface = computed(() => {
8693
8773
  const route = this.activeRoute();
8694
- return route?.iframe !== undefined
8695
- ? {
8774
+ return route?.iframe === undefined
8775
+ ? null
8776
+ : {
8696
8777
  component: IframeSurface,
8697
8778
  injector: this.injectorFor(route, this.path(), this.surfaceKey()),
8698
- }
8699
- : null;
8779
+ };
8700
8780
  }, /* @ts-ignore */
8701
8781
  ...(ngDevMode ? [{ debugName: "iframeSurface" }] : /* istanbul ignore next */ []));
8702
8782
  surface = computed(() => {
@@ -9148,11 +9228,11 @@ class PaneView {
9148
9228
  ];
9149
9229
  }
9150
9230
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.0", ngImport: i0, type: PaneView, deps: [], target: i0.ɵɵFactoryTarget.Component });
9151
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.1.0", type: PaneView, isStandalone: true, selector: "lw-pane-view", inputs: { dock: { classPropertyName: "dock", publicName: "dock", isSignal: true, isRequired: true, transformFunction: null }, leaf: { classPropertyName: "leaf", publicName: "leaf", isSignal: true, isRequired: true, transformFunction: null }, options: { classPropertyName: "options", publicName: "options", isSignal: true, isRequired: false, transformFunction: null } }, host: { classAttribute: "flex min-h-0 min-w-0 flex-1 flex-col" }, ngImport: i0, template: "<lw-pane-tab-strip\n class=\"shrink-0\"\n [variant]=\"options().variant\"\n [tabs]=\"stripTabs()\"\n [activeId]=\"activeTabId()\"\n [urlDriven]=\"pointed()\"\n [source]=\"source()\"\n [reorderable]=\"tabsReorderable()\"\n [draggable]=\"tabsDraggable()\"\n [acceptsTabs]=\"acceptsTabs()\"\n [canAddTab]=\"canAddTab()\"\n [viewContextMenuSlot]=\"viewContextMenu()\"\n [contextGroup]=\"dock()\"\n [paneActions]=\"paneActions\"\n (selectTab)=\"onSelectTab($event)\"\n (escalate)=\"onEscalate($event)\"\n (closeTab)=\"onCloseTab($event)\"\n (unpinTab)=\"onUnpinTab($event)\"\n (addTab)=\"openPicker($event)\"\n (reorderTabs)=\"onReorderTabs($event)\"\n/>\n<ng-template #paneActions>\n <lw-pane-toolbar\n [canSplitRight]=\"canSplitRight()\"\n [canSplitDown]=\"canSplitDown()\"\n [canMinimize]=\"canMinimize()\"\n [canMaximize]=\"canMaximize()\"\n [maximized]=\"maximized()\"\n [canClose]=\"canClose()\"\n [closeLabel]=\"options().closeLabel\"\n (splitRight)=\"splitPane('row')\"\n (splitDown)=\"splitPane('column')\"\n (minimize)=\"minimize()\"\n (toggleMaximize)=\"toggleMaximize()\"\n (closePane)=\"closePane()\"\n />\n</ng-template>\n\n<div class=\"flex min-h-0 min-w-0 flex-1 flex-col\" (pointerdown)=\"onBodyPointerDown()\">\n @if (awaitingContent()) {\n <div\n class=\"flex h-full items-center justify-center p-6 text-center text-sm text-content-faint\"\n data-testid=\"pane-awaiting-content\"\n role=\"status\"\n >\n {{ 'content.split.awaiting' | transloco }}\n </div>\n } @else {\n <lw-content-secondary-pane\n class=\"min-h-0 min-w-0 flex-1\"\n [path]=\"path()\"\n [instanceId]=\"instanceId()\"\n [variant]=\"options().body\"\n [retentionScope]=\"retentionScope()\"\n (instanceReleased)=\"releaseTabInstance()\"\n />\n }\n</div>\n", dependencies: [{ kind: "component", type: ContentSecondaryPane, selector: "lw-content-secondary-pane", inputs: ["path", "variant", "instanceId", "retentionScope"], outputs: ["instanceReleased"] }, { kind: "component", type: PaneTabStrip, selector: "lw-pane-tab-strip", inputs: ["tabs", "activeId", "reorderable", "draggable", "acceptsTabs", "source", "variant", "urlDriven", "contextMenuSlot", "viewContextMenuSlot", "contextGroup", "overflow", "canAddTab", "paneActions"], outputs: ["selectTab", "escalate", "closeTab", "unpinTab", "reorderTabs", "runAction", "addTab", "revealRequest"] }, { kind: "component", type: PaneToolbar, selector: "lw-pane-toolbar", inputs: ["canNewTab", "canSplitRight", "canSplitDown", "canMinimize", "canMaximize", "maximized", "canClose", "closeLabel", "newTabTestId", "splitRightTestId", "splitDownTestId"], outputs: ["newTab", "splitRight", "splitDown", "minimize", "toggleMaximize", "closePane"] }, { kind: "pipe", type: TranslocoPipe, name: "transloco" }] });
9231
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.1.0", type: PaneView, isStandalone: true, selector: "lw-pane-view", inputs: { dock: { classPropertyName: "dock", publicName: "dock", isSignal: true, isRequired: true, transformFunction: null }, leaf: { classPropertyName: "leaf", publicName: "leaf", isSignal: true, isRequired: true, transformFunction: null }, options: { classPropertyName: "options", publicName: "options", isSignal: true, isRequired: false, transformFunction: null } }, host: { classAttribute: "flex min-h-0 min-w-0 flex-1 flex-col" }, ngImport: i0, template: "<lw-pane-tab-strip\n class=\"shrink-0\"\n [variant]=\"options().variant\"\n [tabs]=\"stripTabs()\"\n [activeId]=\"activeTabId()\"\n [urlDriven]=\"pointed()\"\n [source]=\"source()\"\n [reorderable]=\"tabsReorderable()\"\n [draggable]=\"tabsDraggable()\"\n [acceptsTabs]=\"acceptsTabs()\"\n [canAddTab]=\"canAddTab()\"\n [viewContextMenuSlot]=\"viewContextMenu()\"\n [contextGroup]=\"dock()\"\n [paneActions]=\"paneActions\"\n (selectTab)=\"onSelectTab($event)\"\n (escalate)=\"onEscalate($event)\"\n (closeTab)=\"onCloseTab($event)\"\n (unpinTab)=\"onUnpinTab($event)\"\n (addTab)=\"openPicker($event)\"\n (reorderTabs)=\"onReorderTabs($event)\"\n/>\n<ng-template #paneActions>\n <lw-pane-toolbar\n [canSplitRight]=\"canSplitRight()\"\n [canSplitDown]=\"canSplitDown()\"\n [canMinimize]=\"canMinimize()\"\n [canMaximize]=\"canMaximize()\"\n [maximized]=\"maximized()\"\n [canClose]=\"canClose()\"\n [closeLabel]=\"options().closeLabel\"\n (splitRight)=\"splitPane('row')\"\n (splitDown)=\"splitPane('column')\"\n (minimize)=\"minimize()\"\n (toggleMaximize)=\"toggleMaximize()\"\n (closePane)=\"closePane()\"\n />\n</ng-template>\n\n<div class=\"flex min-h-0 min-w-0 flex-1 flex-col\" (pointerdown)=\"onBodyPointerDown()\">\n @if (awaitingContent()) {\n <output\n class=\"flex h-full items-center justify-center p-6 text-center text-sm text-content-faint\"\n data-testid=\"pane-awaiting-content\"\n >\n {{ 'content.split.awaiting' | transloco }}\n </output>\n } @else {\n <lw-content-secondary-pane\n class=\"min-h-0 min-w-0 flex-1\"\n [path]=\"path()\"\n [instanceId]=\"instanceId()\"\n [variant]=\"options().body\"\n [retentionScope]=\"retentionScope()\"\n (instanceReleased)=\"releaseTabInstance()\"\n />\n }\n</div>\n", dependencies: [{ kind: "component", type: ContentSecondaryPane, selector: "lw-content-secondary-pane", inputs: ["path", "variant", "instanceId", "retentionScope"], outputs: ["instanceReleased"] }, { kind: "component", type: PaneTabStrip, selector: "lw-pane-tab-strip", inputs: ["tabs", "activeId", "reorderable", "draggable", "acceptsTabs", "source", "variant", "urlDriven", "contextMenuSlot", "viewContextMenuSlot", "contextGroup", "overflow", "canAddTab", "paneActions"], outputs: ["selectTab", "escalate", "closeTab", "unpinTab", "reorderTabs", "runAction", "addTab", "revealRequest"] }, { kind: "component", type: PaneToolbar, selector: "lw-pane-toolbar", inputs: ["canNewTab", "canSplitRight", "canSplitDown", "canMinimize", "canMaximize", "maximized", "canClose", "closeLabel", "newTabTestId", "splitRightTestId", "splitDownTestId"], outputs: ["newTab", "splitRight", "splitDown", "minimize", "toggleMaximize", "closePane"] }, { kind: "pipe", type: TranslocoPipe, name: "transloco" }] });
9152
9232
  }
9153
9233
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.0", ngImport: i0, type: PaneView, decorators: [{
9154
9234
  type: Component,
9155
- args: [{ selector: 'lw-pane-view', imports: [ContentSecondaryPane, PaneTabStrip, PaneToolbar, TranslocoPipe], schemas: [CUSTOM_ELEMENTS_SCHEMA], host: { class: 'flex min-h-0 min-w-0 flex-1 flex-col' }, template: "<lw-pane-tab-strip\n class=\"shrink-0\"\n [variant]=\"options().variant\"\n [tabs]=\"stripTabs()\"\n [activeId]=\"activeTabId()\"\n [urlDriven]=\"pointed()\"\n [source]=\"source()\"\n [reorderable]=\"tabsReorderable()\"\n [draggable]=\"tabsDraggable()\"\n [acceptsTabs]=\"acceptsTabs()\"\n [canAddTab]=\"canAddTab()\"\n [viewContextMenuSlot]=\"viewContextMenu()\"\n [contextGroup]=\"dock()\"\n [paneActions]=\"paneActions\"\n (selectTab)=\"onSelectTab($event)\"\n (escalate)=\"onEscalate($event)\"\n (closeTab)=\"onCloseTab($event)\"\n (unpinTab)=\"onUnpinTab($event)\"\n (addTab)=\"openPicker($event)\"\n (reorderTabs)=\"onReorderTabs($event)\"\n/>\n<ng-template #paneActions>\n <lw-pane-toolbar\n [canSplitRight]=\"canSplitRight()\"\n [canSplitDown]=\"canSplitDown()\"\n [canMinimize]=\"canMinimize()\"\n [canMaximize]=\"canMaximize()\"\n [maximized]=\"maximized()\"\n [canClose]=\"canClose()\"\n [closeLabel]=\"options().closeLabel\"\n (splitRight)=\"splitPane('row')\"\n (splitDown)=\"splitPane('column')\"\n (minimize)=\"minimize()\"\n (toggleMaximize)=\"toggleMaximize()\"\n (closePane)=\"closePane()\"\n />\n</ng-template>\n\n<div class=\"flex min-h-0 min-w-0 flex-1 flex-col\" (pointerdown)=\"onBodyPointerDown()\">\n @if (awaitingContent()) {\n <div\n class=\"flex h-full items-center justify-center p-6 text-center text-sm text-content-faint\"\n data-testid=\"pane-awaiting-content\"\n role=\"status\"\n >\n {{ 'content.split.awaiting' | transloco }}\n </div>\n } @else {\n <lw-content-secondary-pane\n class=\"min-h-0 min-w-0 flex-1\"\n [path]=\"path()\"\n [instanceId]=\"instanceId()\"\n [variant]=\"options().body\"\n [retentionScope]=\"retentionScope()\"\n (instanceReleased)=\"releaseTabInstance()\"\n />\n }\n</div>\n" }]
9235
+ args: [{ selector: 'lw-pane-view', imports: [ContentSecondaryPane, PaneTabStrip, PaneToolbar, TranslocoPipe], schemas: [CUSTOM_ELEMENTS_SCHEMA], host: { class: 'flex min-h-0 min-w-0 flex-1 flex-col' }, template: "<lw-pane-tab-strip\n class=\"shrink-0\"\n [variant]=\"options().variant\"\n [tabs]=\"stripTabs()\"\n [activeId]=\"activeTabId()\"\n [urlDriven]=\"pointed()\"\n [source]=\"source()\"\n [reorderable]=\"tabsReorderable()\"\n [draggable]=\"tabsDraggable()\"\n [acceptsTabs]=\"acceptsTabs()\"\n [canAddTab]=\"canAddTab()\"\n [viewContextMenuSlot]=\"viewContextMenu()\"\n [contextGroup]=\"dock()\"\n [paneActions]=\"paneActions\"\n (selectTab)=\"onSelectTab($event)\"\n (escalate)=\"onEscalate($event)\"\n (closeTab)=\"onCloseTab($event)\"\n (unpinTab)=\"onUnpinTab($event)\"\n (addTab)=\"openPicker($event)\"\n (reorderTabs)=\"onReorderTabs($event)\"\n/>\n<ng-template #paneActions>\n <lw-pane-toolbar\n [canSplitRight]=\"canSplitRight()\"\n [canSplitDown]=\"canSplitDown()\"\n [canMinimize]=\"canMinimize()\"\n [canMaximize]=\"canMaximize()\"\n [maximized]=\"maximized()\"\n [canClose]=\"canClose()\"\n [closeLabel]=\"options().closeLabel\"\n (splitRight)=\"splitPane('row')\"\n (splitDown)=\"splitPane('column')\"\n (minimize)=\"minimize()\"\n (toggleMaximize)=\"toggleMaximize()\"\n (closePane)=\"closePane()\"\n />\n</ng-template>\n\n<div class=\"flex min-h-0 min-w-0 flex-1 flex-col\" (pointerdown)=\"onBodyPointerDown()\">\n @if (awaitingContent()) {\n <output\n class=\"flex h-full items-center justify-center p-6 text-center text-sm text-content-faint\"\n data-testid=\"pane-awaiting-content\"\n >\n {{ 'content.split.awaiting' | transloco }}\n </output>\n } @else {\n <lw-content-secondary-pane\n class=\"min-h-0 min-w-0 flex-1\"\n [path]=\"path()\"\n [instanceId]=\"instanceId()\"\n [variant]=\"options().body\"\n [retentionScope]=\"retentionScope()\"\n (instanceReleased)=\"releaseTabInstance()\"\n />\n }\n</div>\n" }]
9156
9236
  }], propDecorators: { dock: [{ type: i0.Input, args: [{ isSignal: true, alias: "dock", required: true }] }], leaf: [{ type: i0.Input, args: [{ isSignal: true, alias: "leaf", required: true }] }], options: [{ type: i0.Input, args: [{ isSignal: true, alias: "options", required: false }] }] } });
9157
9237
 
9158
9238
  class PaneDropZones {
@@ -9190,13 +9270,12 @@ class PaneDropZones {
9190
9270
  ...(ngDevMode ? [{ debugName: "accepts" }] : /* istanbul ignore next */ []));
9191
9271
  constructor() {
9192
9272
  effect((onCleanup) => {
9193
- const ids = this.fills()
9194
- ? this.accepts()
9195
- ? [this.fillZoneId()]
9196
- : []
9197
- : this.edges().map((edge) => this.zoneId(edge));
9198
- const disposers = ids.map((id) => this.drag.registerZone(id));
9199
- onCleanup(() => disposers.forEach((dispose) => dispose()));
9273
+ const disposers = this.zoneIds().map((id) => this.drag.registerZone(id));
9274
+ onCleanup(() => {
9275
+ for (const dispose of disposers) {
9276
+ dispose();
9277
+ }
9278
+ });
9200
9279
  });
9201
9280
  }
9202
9281
  has(edge) {
@@ -9232,8 +9311,14 @@ class PaneDropZones {
9232
9311
  this.paneMove.moveToEdge(source, String(event.item.data ?? ''), { dock: this.dock(), paneId: this.paneId() }, edge);
9233
9312
  }
9234
9313
  }
9314
+ zoneIds() {
9315
+ if (!this.fills()) {
9316
+ return this.edges().map((edge) => this.zoneId(edge));
9317
+ }
9318
+ return this.accepts() ? [this.fillZoneId()] : [];
9319
+ }
9235
9320
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.0", ngImport: i0, type: PaneDropZones, deps: [], target: i0.ɵɵFactoryTarget.Component });
9236
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.1.0", type: PaneDropZones, isStandalone: true, selector: "lw-pane-drop-zones", inputs: { dock: { classPropertyName: "dock", publicName: "dock", isSignal: true, isRequired: true, transformFunction: null }, paneId: { classPropertyName: "paneId", publicName: "paneId", isSignal: true, isRequired: true, transformFunction: null } }, host: { properties: { "class.opacity-0": "!active()", "class.pointer-events-none": "!active()", "style.grid-template-columns": "\"1fr 2fr 1fr\"", "style.grid-template-rows": "\"1fr 2fr 1fr\"", "attr.aria-hidden": "true" }, classAttribute: "absolute inset-0 z-20 grid transition-opacity" }, ngImport: i0, template: "@if (fills()) {\n @if (accepts()) {\n <div\n class=\"lw-pane-drop-zone col-span-3 row-span-3 col-start-1 row-start-1\"\n cdkDropList\n [cdkDropListEnterPredicate]=\"enterPredicate\"\n [id]=\"fillZoneId()\"\n (cdkDropListDropped)=\"onFill($event)\"\n >\n <div class=\"lw-pane-drop-preview lw-pane-drop-preview--fill\"></div>\n </div>\n }\n} @else {\n @if (has('left')) {\n <div\n class=\"lw-pane-drop-zone col-start-1 row-span-3 row-start-1\"\n cdkDropList\n [cdkDropListEnterPredicate]=\"enterPredicate\"\n [id]=\"zoneId('left')\"\n (cdkDropListDropped)=\"onDrop('left', $event)\"\n >\n <div class=\"lw-pane-drop-preview lw-pane-drop-preview--left\"></div>\n </div>\n }\n @if (has('right')) {\n <div\n class=\"lw-pane-drop-zone col-start-3 row-span-3 row-start-1\"\n cdkDropList\n [cdkDropListEnterPredicate]=\"enterPredicate\"\n [id]=\"zoneId('right')\"\n (cdkDropListDropped)=\"onDrop('right', $event)\"\n >\n <div class=\"lw-pane-drop-preview lw-pane-drop-preview--right\"></div>\n </div>\n }\n @if (has('top')) {\n <div\n class=\"lw-pane-drop-zone col-start-2 row-start-1\"\n cdkDropList\n [cdkDropListEnterPredicate]=\"enterPredicate\"\n [id]=\"zoneId('top')\"\n (cdkDropListDropped)=\"onDrop('top', $event)\"\n >\n <div class=\"lw-pane-drop-preview lw-pane-drop-preview--top\"></div>\n </div>\n }\n @if (has('bottom')) {\n <div\n class=\"lw-pane-drop-zone col-start-2 row-start-3\"\n cdkDropList\n [cdkDropListEnterPredicate]=\"enterPredicate\"\n [id]=\"zoneId('bottom')\"\n (cdkDropListDropped)=\"onDrop('bottom', $event)\"\n >\n <div class=\"lw-pane-drop-preview lw-pane-drop-preview--bottom\"></div>\n </div>\n }\n}\n", dependencies: [{ kind: "directive", type: CdkDropList, selector: "[cdkDropList], cdk-drop-list", inputs: ["cdkDropListConnectedTo", "cdkDropListData", "cdkDropListOrientation", "id", "cdkDropListLockAxis", "cdkDropListDisabled", "cdkDropListSortingDisabled", "cdkDropListEnterPredicate", "cdkDropListSortPredicate", "cdkDropListAutoScrollDisabled", "cdkDropListAutoScrollStep", "cdkDropListElementContainer", "cdkDropListHasAnchor"], outputs: ["cdkDropListDropped", "cdkDropListEntered", "cdkDropListExited", "cdkDropListSorted"], exportAs: ["cdkDropList"] }] });
9321
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.1.0", type: PaneDropZones, isStandalone: true, selector: "lw-pane-drop-zones", inputs: { dock: { classPropertyName: "dock", publicName: "dock", isSignal: true, isRequired: true, transformFunction: null }, paneId: { classPropertyName: "paneId", publicName: "paneId", isSignal: true, isRequired: true, transformFunction: null } }, host: { properties: { "class.opacity-0": "!active()", "class.pointer-events-none": "!active()", "style.grid-template-columns": "\"1fr 2fr 1fr\"", "style.grid-template-rows": "\"1fr 2fr 1fr\"", "attr.aria-hidden": "true" }, classAttribute: "absolute inset-0 z-20 grid transition-opacity" }, ngImport: i0, template: "@if (fills()) {\n @if (accepts()) {\n <div\n class=\"lw-pane-drop-zone col-span-3 col-start-1 row-span-3 row-start-1\"\n cdkDropList\n [cdkDropListEnterPredicate]=\"enterPredicate\"\n [id]=\"fillZoneId()\"\n (cdkDropListDropped)=\"onFill($event)\"\n >\n <div class=\"lw-pane-drop-preview lw-pane-drop-preview--fill\"></div>\n </div>\n }\n} @else {\n @if (has('left')) {\n <div\n class=\"lw-pane-drop-zone col-start-1 row-span-3 row-start-1\"\n cdkDropList\n [cdkDropListEnterPredicate]=\"enterPredicate\"\n [id]=\"zoneId('left')\"\n (cdkDropListDropped)=\"onDrop('left', $event)\"\n >\n <div class=\"lw-pane-drop-preview lw-pane-drop-preview--left\"></div>\n </div>\n }\n @if (has('right')) {\n <div\n class=\"lw-pane-drop-zone col-start-3 row-span-3 row-start-1\"\n cdkDropList\n [cdkDropListEnterPredicate]=\"enterPredicate\"\n [id]=\"zoneId('right')\"\n (cdkDropListDropped)=\"onDrop('right', $event)\"\n >\n <div class=\"lw-pane-drop-preview lw-pane-drop-preview--right\"></div>\n </div>\n }\n @if (has('top')) {\n <div\n class=\"lw-pane-drop-zone col-start-2 row-start-1\"\n cdkDropList\n [cdkDropListEnterPredicate]=\"enterPredicate\"\n [id]=\"zoneId('top')\"\n (cdkDropListDropped)=\"onDrop('top', $event)\"\n >\n <div class=\"lw-pane-drop-preview lw-pane-drop-preview--top\"></div>\n </div>\n }\n @if (has('bottom')) {\n <div\n class=\"lw-pane-drop-zone col-start-2 row-start-3\"\n cdkDropList\n [cdkDropListEnterPredicate]=\"enterPredicate\"\n [id]=\"zoneId('bottom')\"\n (cdkDropListDropped)=\"onDrop('bottom', $event)\"\n >\n <div class=\"lw-pane-drop-preview lw-pane-drop-preview--bottom\"></div>\n </div>\n }\n}\n", dependencies: [{ kind: "directive", type: CdkDropList, selector: "[cdkDropList], cdk-drop-list", inputs: ["cdkDropListConnectedTo", "cdkDropListData", "cdkDropListOrientation", "id", "cdkDropListLockAxis", "cdkDropListDisabled", "cdkDropListSortingDisabled", "cdkDropListEnterPredicate", "cdkDropListSortPredicate", "cdkDropListAutoScrollDisabled", "cdkDropListAutoScrollStep", "cdkDropListElementContainer", "cdkDropListHasAnchor"], outputs: ["cdkDropListDropped", "cdkDropListEntered", "cdkDropListExited", "cdkDropListSorted"], exportAs: ["cdkDropList"] }] });
9237
9322
  }
9238
9323
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.0", ngImport: i0, type: PaneDropZones, decorators: [{
9239
9324
  type: Component,
@@ -9244,7 +9329,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.0", ngImpor
9244
9329
  '[style.grid-template-columns]': '"1fr 2fr 1fr"',
9245
9330
  '[style.grid-template-rows]': '"1fr 2fr 1fr"',
9246
9331
  '[attr.aria-hidden]': 'true',
9247
- }, template: "@if (fills()) {\n @if (accepts()) {\n <div\n class=\"lw-pane-drop-zone col-span-3 row-span-3 col-start-1 row-start-1\"\n cdkDropList\n [cdkDropListEnterPredicate]=\"enterPredicate\"\n [id]=\"fillZoneId()\"\n (cdkDropListDropped)=\"onFill($event)\"\n >\n <div class=\"lw-pane-drop-preview lw-pane-drop-preview--fill\"></div>\n </div>\n }\n} @else {\n @if (has('left')) {\n <div\n class=\"lw-pane-drop-zone col-start-1 row-span-3 row-start-1\"\n cdkDropList\n [cdkDropListEnterPredicate]=\"enterPredicate\"\n [id]=\"zoneId('left')\"\n (cdkDropListDropped)=\"onDrop('left', $event)\"\n >\n <div class=\"lw-pane-drop-preview lw-pane-drop-preview--left\"></div>\n </div>\n }\n @if (has('right')) {\n <div\n class=\"lw-pane-drop-zone col-start-3 row-span-3 row-start-1\"\n cdkDropList\n [cdkDropListEnterPredicate]=\"enterPredicate\"\n [id]=\"zoneId('right')\"\n (cdkDropListDropped)=\"onDrop('right', $event)\"\n >\n <div class=\"lw-pane-drop-preview lw-pane-drop-preview--right\"></div>\n </div>\n }\n @if (has('top')) {\n <div\n class=\"lw-pane-drop-zone col-start-2 row-start-1\"\n cdkDropList\n [cdkDropListEnterPredicate]=\"enterPredicate\"\n [id]=\"zoneId('top')\"\n (cdkDropListDropped)=\"onDrop('top', $event)\"\n >\n <div class=\"lw-pane-drop-preview lw-pane-drop-preview--top\"></div>\n </div>\n }\n @if (has('bottom')) {\n <div\n class=\"lw-pane-drop-zone col-start-2 row-start-3\"\n cdkDropList\n [cdkDropListEnterPredicate]=\"enterPredicate\"\n [id]=\"zoneId('bottom')\"\n (cdkDropListDropped)=\"onDrop('bottom', $event)\"\n >\n <div class=\"lw-pane-drop-preview lw-pane-drop-preview--bottom\"></div>\n </div>\n }\n}\n" }]
9332
+ }, template: "@if (fills()) {\n @if (accepts()) {\n <div\n class=\"lw-pane-drop-zone col-span-3 col-start-1 row-span-3 row-start-1\"\n cdkDropList\n [cdkDropListEnterPredicate]=\"enterPredicate\"\n [id]=\"fillZoneId()\"\n (cdkDropListDropped)=\"onFill($event)\"\n >\n <div class=\"lw-pane-drop-preview lw-pane-drop-preview--fill\"></div>\n </div>\n }\n} @else {\n @if (has('left')) {\n <div\n class=\"lw-pane-drop-zone col-start-1 row-span-3 row-start-1\"\n cdkDropList\n [cdkDropListEnterPredicate]=\"enterPredicate\"\n [id]=\"zoneId('left')\"\n (cdkDropListDropped)=\"onDrop('left', $event)\"\n >\n <div class=\"lw-pane-drop-preview lw-pane-drop-preview--left\"></div>\n </div>\n }\n @if (has('right')) {\n <div\n class=\"lw-pane-drop-zone col-start-3 row-span-3 row-start-1\"\n cdkDropList\n [cdkDropListEnterPredicate]=\"enterPredicate\"\n [id]=\"zoneId('right')\"\n (cdkDropListDropped)=\"onDrop('right', $event)\"\n >\n <div class=\"lw-pane-drop-preview lw-pane-drop-preview--right\"></div>\n </div>\n }\n @if (has('top')) {\n <div\n class=\"lw-pane-drop-zone col-start-2 row-start-1\"\n cdkDropList\n [cdkDropListEnterPredicate]=\"enterPredicate\"\n [id]=\"zoneId('top')\"\n (cdkDropListDropped)=\"onDrop('top', $event)\"\n >\n <div class=\"lw-pane-drop-preview lw-pane-drop-preview--top\"></div>\n </div>\n }\n @if (has('bottom')) {\n <div\n class=\"lw-pane-drop-zone col-start-2 row-start-3\"\n cdkDropList\n [cdkDropListEnterPredicate]=\"enterPredicate\"\n [id]=\"zoneId('bottom')\"\n (cdkDropListDropped)=\"onDrop('bottom', $event)\"\n >\n <div class=\"lw-pane-drop-preview lw-pane-drop-preview--bottom\"></div>\n </div>\n }\n}\n" }]
9248
9333
  }], ctorParameters: () => [], propDecorators: { dock: [{ type: i0.Input, args: [{ isSignal: true, alias: "dock", required: true }] }], paneId: [{ type: i0.Input, args: [{ isSignal: true, alias: "paneId", required: true }] }] } });
9249
9334
 
9250
9335
  class PaneMinimizedStrip {
@@ -9317,18 +9402,18 @@ class PaneSplitHandle {
9317
9402
  return;
9318
9403
  }
9319
9404
  const rect = parent.getBoundingClientRect();
9320
- const el = this.host.nativeElement;
9321
- el.setPointerCapture(event.pointerId);
9405
+ const element = this.host.nativeElement;
9406
+ element.setPointerCapture(event.pointerId);
9322
9407
  event.preventDefault();
9323
9408
  const move = (e) => this.ratioStream.emit(this.fraction(e, rect));
9324
9409
  const up = (e) => {
9325
- el.releasePointerCapture(e.pointerId);
9326
- el.removeEventListener('pointermove', move);
9327
- el.removeEventListener('pointerup', up);
9410
+ element.releasePointerCapture(e.pointerId);
9411
+ element.removeEventListener('pointermove', move);
9412
+ element.removeEventListener('pointerup', up);
9328
9413
  this.ratioCommit.emit();
9329
9414
  };
9330
- el.addEventListener('pointermove', move);
9331
- el.addEventListener('pointerup', up);
9415
+ element.addEventListener('pointermove', move);
9416
+ element.addEventListener('pointerup', up);
9332
9417
  }
9333
9418
  onKeydown(event) {
9334
9419
  const step = event.shiftKey ? STEP_COARSE$1 : STEP$1;
@@ -9635,11 +9720,12 @@ function parseWidths(raw) {
9635
9720
  }
9636
9721
  const result = {};
9637
9722
  for (const [key, value] of Object.entries(parsed)) {
9638
- if (typeof value === 'number' && Number.isFinite(value)) {
9639
- const clamped = clampWidth(value);
9640
- if (clamped !== DEFAULT_PANEL_WIDTH) {
9641
- result[key] = clamped;
9642
- }
9723
+ if (!(typeof value === 'number' && Number.isFinite(value))) {
9724
+ continue;
9725
+ }
9726
+ const clamped = clampWidth(value);
9727
+ if (clamped !== DEFAULT_PANEL_WIDTH) {
9728
+ result[key] = clamped;
9643
9729
  }
9644
9730
  }
9645
9731
  return result;
@@ -9742,20 +9828,25 @@ class PanelSplitter {
9742
9828
  const step = (event.shiftKey ? STEP_COARSE : STEP) * this.edgeSign();
9743
9829
  let next;
9744
9830
  switch (event.key) {
9745
- case 'ArrowRight':
9831
+ case 'ArrowRight': {
9746
9832
  next = this.width() + step;
9747
9833
  break;
9748
- case 'ArrowLeft':
9834
+ }
9835
+ case 'ArrowLeft': {
9749
9836
  next = this.width() - step;
9750
9837
  break;
9751
- case 'Home':
9838
+ }
9839
+ case 'Home': {
9752
9840
  next = this.size.minWidth;
9753
9841
  break;
9754
- case 'End':
9842
+ }
9843
+ case 'End': {
9755
9844
  next = this.size.maxWidth;
9756
9845
  break;
9757
- default:
9846
+ }
9847
+ default: {
9758
9848
  return;
9849
+ }
9759
9850
  }
9760
9851
  event.preventDefault();
9761
9852
  this.size.setWidth(this.regionId(), next);
@@ -9812,7 +9903,7 @@ class ShellPanel {
9812
9903
  activeView = computed(() => {
9813
9904
  const path = this.activePath();
9814
9905
  if (!path?.startsWith(VIEW_PANE_PREFIX)) {
9815
- return undefined;
9906
+ return;
9816
9907
  }
9817
9908
  return viewForPanePath(this.registry.views(), path);
9818
9909
  }, /* @ts-ignore */
@@ -9826,7 +9917,7 @@ class ShellPanel {
9826
9917
  ...(ngDevMode ? [{ debugName: "activeContentPath" }] : /* istanbul ignore next */ []));
9827
9918
  actions = computed(() => [...(this.activeView()?.actions ?? [])]
9828
9919
  .filter((action) => this.auth.visible(action.access))
9829
- .sort((a, b) => (a.order ?? 0) - (b.order ?? 0)), /* @ts-ignore */
9920
+ .toSorted((a, b) => (a.order ?? 0) - (b.order ?? 0)), /* @ts-ignore */
9830
9921
  ...(ngDevMode ? [{ debugName: "actions" }] : /* istanbul ignore next */ []));
9831
9922
  panelPaneOptions = PANEL_PANE_OPTIONS;
9832
9923
  primaryScope = computed(() => paneRetentionScope(this.region().id, this.paneTree.primaryId(this.region().id)), /* @ts-ignore */
@@ -10120,10 +10211,14 @@ function registerTabContextMenu(registry, tabs, paneMove, popout, shell) {
10120
10211
  },
10121
10212
  ];
10122
10213
  const registered = new Set(commands.map((command) => command.id));
10123
- commands.forEach((command) => registry.addCommand({ ...command, paletteHidden: true }));
10124
- items
10125
- .filter((item) => item.command !== undefined && registered.has(item.command))
10126
- .forEach((item) => registry.addMenuItem(item));
10214
+ for (const command of commands) {
10215
+ registry.addCommand({ ...command, paletteHidden: true });
10216
+ }
10217
+ for (const item of items) {
10218
+ if (item.command !== undefined && registered.has(item.command)) {
10219
+ registry.addMenuItem(item);
10220
+ }
10221
+ }
10127
10222
  }
10128
10223
 
10129
10224
  class ContentArea {
@@ -10329,7 +10424,7 @@ class ContentGrid {
10329
10424
  }, /* @ts-ignore */
10330
10425
  ...(ngDevMode ? [{ debugName: "tree" }] : /* istanbul ignore next */ []));
10331
10426
  constructor() {
10332
- const doc = inject(DOCUMENT);
10427
+ const document_ = inject(DOCUMENT);
10333
10428
  const onKeydown = (event) => {
10334
10429
  if (event.key === 'Escape') {
10335
10430
  this.chrome.restore();
@@ -10339,8 +10434,8 @@ class ContentGrid {
10339
10434
  if (!this.maximized()) {
10340
10435
  return;
10341
10436
  }
10342
- doc.addEventListener('keydown', onKeydown);
10343
- onCleanup(() => doc.removeEventListener('keydown', onKeydown));
10437
+ document_.addEventListener('keydown', onKeydown);
10438
+ onCleanup(() => document_.removeEventListener('keydown', onKeydown));
10344
10439
  });
10345
10440
  effect(() => {
10346
10441
  if (!this.layout.isSplit(CONTENT_DOCK)) {
@@ -10513,8 +10608,8 @@ class DialogOutlet {
10513
10608
  if (!this.dialogs().length || !panels.length) {
10514
10609
  return;
10515
10610
  }
10516
- const top = panels[panels.length - 1].nativeElement;
10517
- if (top.contains(this.document.activeElement)) {
10611
+ const top = panels.at(-1)?.nativeElement;
10612
+ if (!top || top.contains(this.document.activeElement)) {
10518
10613
  return;
10519
10614
  }
10520
10615
  (top.querySelector('[data-lw-autofocus]') ?? top).focus();
@@ -10543,10 +10638,10 @@ class DialogOutlet {
10543
10638
  return;
10544
10639
  }
10545
10640
  const first = focusables[0];
10546
- const last = focusables[focusables.length - 1];
10641
+ const last = focusables.at(-1);
10547
10642
  const active = this.document.activeElement;
10548
10643
  if (backward && active === first) {
10549
- last.focus();
10644
+ last?.focus();
10550
10645
  event.preventDefault();
10551
10646
  }
10552
10647
  else if (!backward && active === last) {
@@ -10618,7 +10713,7 @@ class DialogOutlet {
10618
10713
  }
10619
10714
  topPanel() {
10620
10715
  const panels = this.panels();
10621
- return panels.length ? panels[panels.length - 1].nativeElement : undefined;
10716
+ return panels.at(-1)?.nativeElement;
10622
10717
  }
10623
10718
  focusable(root) {
10624
10719
  const selector = 'button:not(:disabled), a[href], input:not(:disabled), select:not(:disabled), textarea:not(:disabled), [tabindex]:not([tabindex="-1"])';
@@ -10626,7 +10721,7 @@ class DialogOutlet {
10626
10721
  }
10627
10722
  top() {
10628
10723
  const list = this.dialogs();
10629
- return list[list.length - 1];
10724
+ return list.at(-1);
10630
10725
  }
10631
10726
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.0", ngImport: i0, type: DialogOutlet, deps: [], target: i0.ɵɵFactoryTarget.Component });
10632
10727
  static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.1.0", type: DialogOutlet, isStandalone: true, selector: "lw-dialog-outlet", host: { listeners: { "document:keydown.escape": "onEscape()", "document:focusin": "onFocusIn($event)", "document:keydown.tab": "onTab($event, false)", "document:keydown.shift.tab": "onTab($event, true)" } }, viewQueries: [{ propertyName: "panels", predicate: ["panel"], descendants: true, isSignal: true }], ngImport: i0, template: "@for (d of dialogs(); track d.id) {\n <div [class]=\"wrapperClasses(d)\">\n <button\n type=\"button\"\n class=\"lw-scrim absolute inset-0 cursor-default\"\n [disabled]=\"!d.dismissable\"\n [attr.aria-label]=\"'dialog.close' | transloco\"\n (click)=\"dismiss(d)\"\n ></button>\n\n @if (d.bare) {\n <dialog\n #panel\n open\n class=\"relative z-10 m-0 flex max-h-full w-full flex-col overflow-hidden rounded-t-xl border border-border bg-surface-raised shadow-xl sm:rounded-xl\"\n [class.rounded-xl]=\"d.align === 'top'\"\n [class]=\"panelWidth(d)\"\n aria-modal=\"true\"\n [attr.aria-label]=\"d.title ? (d.title | transloco) : null\"\n tabindex=\"-1\"\n >\n <ng-container\n [ngComponentOutlet]=\"d.component ?? null\"\n [ngComponentOutletInjector]=\"d.injector\"\n />\n </dialog>\n } @else {\n <dialog\n #panel\n open\n class=\"relative z-10 m-0 flex max-h-full w-full flex-col gap-4 overflow-y-auto rounded-t-xl border border-border bg-surface-raised p-5 shadow-xl sm:rounded-xl\"\n [class.rounded-xl]=\"d.align === 'top'\"\n [class]=\"panelWidth(d)\"\n aria-modal=\"true\"\n [attr.aria-label]=\"d.title ? (d.title | transloco) : null\"\n tabindex=\"-1\"\n >\n <div class=\"flex gap-3.5\">\n @if (d.icon; as icon) {\n <span\n class=\"grid h-10 w-10 shrink-0 place-items-center rounded-full\"\n [class]=\"toneCircle(d.tone)\"\n aria-hidden=\"true\"\n >\n <lw-icon [name]=\"icon\" size=\"1.25rem\" />\n </span>\n }\n\n <div class=\"flex min-w-0 flex-1 flex-col gap-3\">\n @if (d.title || (d.kind === 'custom' && d.dismissable)) {\n <div class=\"flex items-start justify-between gap-4\">\n <h2 class=\"min-w-0 text-lg font-semibold text-content\">\n @if (d.title) {\n {{ d.title | transloco }}\n }\n </h2>\n <span class=\"flex shrink-0 items-center gap-1\">\n @if (d.maximizable) {\n <button\n lwButton\n variant=\"ghost\"\n size=\"sm\"\n iconOnly\n type=\"button\"\n class=\"-mt-1\"\n [attr.aria-label]=\"\n (d.ref.maximized() ? 'dialog.restore' : 'dialog.maximize') | transloco\n \"\n (click)=\"d.ref.toggleMaximized()\"\n >\n <lw-icon [name]=\"d.ref.maximized() ? 'restore' : 'maximize'\" size=\"1rem\" />\n </button>\n }\n @if (d.kind === 'custom' && d.dismissable) {\n <button\n lwButton\n variant=\"ghost\"\n size=\"sm\"\n iconOnly\n type=\"button\"\n class=\"-mt-1 -mr-1\"\n [attr.aria-label]=\"'dialog.close' | transloco\"\n (click)=\"dismiss(d)\"\n >\n <lw-icon name=\"close\" size=\"1rem\" />\n </button>\n }\n </span>\n </div>\n }\n\n @if (d.component) {\n <ng-container\n [ngComponentOutlet]=\"d.component\"\n [ngComponentOutletInjector]=\"d.injector\"\n />\n } @else if (d.kind === 'progress') {\n <div class=\"flex items-center gap-3 text-sm text-content\">\n <lw-spinner [label]=\"'progress.busy' | transloco\" />\n @if (d.progressMessage) {\n <span>{{ d.progressMessage() | transloco }}</span>\n }\n </div>\n } @else {\n @if (d.message) {\n <lw-markdown [source]=\"d.message | transloco\" />\n }\n @if (d.requireLabel) {\n <lw-markdown [source]=\"d.requireLabel | transloco\" />\n }\n @if (d.promptValue) {\n <input\n type=\"text\"\n class=\"lw-field\"\n [class.lw-field--invalid]=\"guardError(d)\"\n [value]=\"d.promptValue()\"\n [attr.placeholder]=\"d.placeholder ? (d.placeholder | transloco) : null\"\n (input)=\"onPromptInput(d, $event)\"\n (keydown.enter)=\"onEnter(d)\"\n data-lw-autofocus\n />\n @if (guardError(d); as error) {\n <p class=\"text-xs text-negative\">{{ error | transloco }}</p>\n }\n }\n }\n </div>\n </div>\n\n @if (d.buttons.length) {\n <div class=\"flex justify-end gap-2\">\n @for (b of d.buttons; track $index) {\n <button\n lwButton\n [variant]=\"b.variant\"\n type=\"button\"\n [disabled]=\"confirmBlocked(d, b)\"\n [attr.data-lw-autofocus]=\"b.autofocus ? '' : null\"\n (click)=\"onButton(d, b)\"\n >\n {{ b.label | transloco }}\n </button>\n }\n </div>\n }\n </dialog>\n }\n </div>\n}\n", dependencies: [{ kind: "directive", type: NgComponentOutlet, selector: "[ngComponentOutlet]", inputs: ["ngComponentOutlet", "ngComponentOutletInputs", "ngComponentOutletInjector", "ngComponentOutletEnvironmentInjector", "ngComponentOutletContent", "ngComponentOutletNgModule"], exportAs: ["ngComponentOutlet"] }, { kind: "directive", type: LwButton, selector: "button[lwButton], a[lwButton]", inputs: ["variant", "size", "iconOnly"] }, { kind: "component", type: LwSpinner, selector: "lw-spinner", inputs: ["size", "label"] }, { kind: "pipe", type: TranslocoPipe, name: "transloco" }] });
@@ -10776,7 +10871,7 @@ class SettingsRegistry {
10776
10871
  return this.sections()
10777
10872
  .map((section) => visibleSection(section, omitted))
10778
10873
  .filter((section) => section !== null)
10779
- .sort((a, b) => (a.order ?? 0) - (b.order ?? 0));
10874
+ .toSorted((a, b) => (a.order ?? 0) - (b.order ?? 0));
10780
10875
  }, /* @ts-ignore */
10781
10876
  ...(ngDevMode ? [{ debugName: "all" }] : /* istanbul ignore next */ []));
10782
10877
  register(section) {
@@ -11016,6 +11111,13 @@ function provideTranslationNamespaces(...namespaces) {
11016
11111
  /** Directory the distribution serves its overlay bundles from, without a trailing slash. */
11017
11112
  const TRANSLATION_OVERRIDES = new InjectionToken('TRANSLATION_OVERRIDES');
11018
11113
  const DEFAULT_OVERRIDES_PATH = '/i18n/overrides';
11114
+ function withoutTrailingSlashes(path) {
11115
+ let end = path.length;
11116
+ while (end > 0 && path[end - 1] === '/') {
11117
+ end -= 1;
11118
+ }
11119
+ return path.slice(0, end);
11120
+ }
11019
11121
  /**
11020
11122
  * Load `<basePath>/<lang>.json` and merge it over everything else **key by key**, so a product
11021
11123
  * can reword the shell in its own house language ("Save as" rather than "Save as new") without
@@ -11035,7 +11137,7 @@ const DEFAULT_OVERRIDES_PATH = '/i18n/overrides';
11035
11137
  * nothing ships is dev-warned too, since a typo there would otherwise be a string that never appears.
11036
11138
  */
11037
11139
  function provideTranslationOverrides(basePath = DEFAULT_OVERRIDES_PATH) {
11038
- const normalized = basePath.replace(/\/+$/, '');
11140
+ const normalized = withoutTrailingSlashes(basePath);
11039
11141
  if (normalized === '') {
11040
11142
  throw new Error('provideTranslationOverrides() needs a directory to load overlays from; ' +
11041
11143
  `pass one or omit the argument for "${DEFAULT_OVERRIDES_PATH}".`);
@@ -11087,7 +11189,9 @@ class TranslocoHttpLoader {
11087
11189
  return forkJoin([host$, ...namespaced$, this.overrides$(lang)]).pipe(map(([host, ...rest]) => {
11088
11190
  const overlay = rest.pop();
11089
11191
  const merged = { ...host };
11090
- this.namespaces.forEach((name, index) => (merged[name] = rest[index]));
11192
+ for (const [index, name] of this.namespaces.entries()) {
11193
+ merged[name] = rest[index];
11194
+ }
11091
11195
  return this.applyOverrides(merged, overlay, lang);
11092
11196
  }));
11093
11197
  }
@@ -11158,9 +11262,7 @@ class LwIconElement extends HTMLElement {
11158
11262
  this.render();
11159
11263
  }
11160
11264
  attributeChangedCallback() {
11161
- if (this.isConnected) {
11162
- this.render();
11163
- }
11265
+ this.refresh();
11164
11266
  }
11165
11267
  /**
11166
11268
  * Re-draws from the registry without changing the name. A sandboxed surface receives the product's
@@ -11267,7 +11369,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.0", ngImpor
11267
11369
  // GENERATED — do not edit by hand.
11268
11370
  // Written by tools/stamp-version.mjs from <Version> in Directory.Build.props.
11269
11371
  // Single source of truth: Directory.Build.props (bump via scripts/bump-version.sh).
11270
- const APP_VERSION = '0.7.5';
11372
+ const APP_VERSION = '0.7.7';
11271
11373
 
11272
11374
  /**
11273
11375
  * The running build's version, sourced from `<Version>` in Directory.Build.props
@@ -11483,7 +11585,7 @@ class UpdateService {
11483
11585
  await bestEffort(async () => {
11484
11586
  const registrations = (await container?.getRegistrations()) ?? [];
11485
11587
  await Promise.all(registrations
11486
- .filter(isShellWorker)
11588
+ .filter((registration) => isShellWorker(registration))
11487
11589
  .map((registration) => registration.unregister()));
11488
11590
  });
11489
11591
  await bestEffort(async () => {
@@ -11491,14 +11593,14 @@ class UpdateService {
11491
11593
  const keys = (await storage?.keys()) ?? [];
11492
11594
  await Promise.all(keys
11493
11595
  .filter((key) => key.startsWith(WORKER_CACHE_PREFIX))
11494
- .map((key) => storage?.delete(key)));
11596
+ .map(async (key) => storage?.delete(key)));
11495
11597
  });
11496
11598
  }
11497
11599
  onVersionEvent(event) {
11498
11600
  if (event.type === 'VERSION_READY') {
11499
11601
  this.onUpdateReady();
11500
11602
  }
11501
- if (event.type === 'VERSION_INSTALLATION_FAILED') {
11603
+ else if (event.type === 'VERSION_INSTALLATION_FAILED') {
11502
11604
  this.onUpdateFailed();
11503
11605
  }
11504
11606
  }
@@ -11862,7 +11964,7 @@ class CommandInvocationService {
11862
11964
  .filter((entry) => entry.command.callable === true &&
11863
11965
  this.reachable(entry, callerId, granted))
11864
11966
  .map((entry) => this.describe(entry.command))
11865
- .sort((a, b) => a.id.localeCompare(b.id));
11967
+ .toSorted((a, b) => a.id.localeCompare(b.id));
11866
11968
  }
11867
11969
  async invoke(callerId, granted, id, args) {
11868
11970
  const entry = this.registry
@@ -12182,17 +12284,21 @@ class LwTooltipElement extends HTMLElement {
12182
12284
  const centerX = t.left + t.width / 2 - b.width / 2;
12183
12285
  const centerY = t.top + t.height / 2 - b.height / 2;
12184
12286
  switch (this.position) {
12185
- case 'bottom':
12287
+ case 'bottom': {
12186
12288
  [left, top] = [centerX, t.bottom + TOOLTIP_GAP];
12187
12289
  break;
12188
- case 'left':
12290
+ }
12291
+ case 'left': {
12189
12292
  [left, top] = [t.left - b.width - TOOLTIP_GAP, centerY];
12190
12293
  break;
12191
- case 'right':
12294
+ }
12295
+ case 'right': {
12192
12296
  [left, top] = [t.right + TOOLTIP_GAP, centerY];
12193
12297
  break;
12194
- default:
12298
+ }
12299
+ default: {
12195
12300
  [left, top] = [centerX, t.top - b.height - TOOLTIP_GAP];
12301
+ }
12196
12302
  }
12197
12303
  }
12198
12304
  else {
@@ -12217,10 +12323,11 @@ class LwTooltipElement extends HTMLElement {
12217
12323
  }
12218
12324
  }
12219
12325
  clearTimer() {
12220
- if (this.showTimer !== undefined) {
12221
- clearTimeout(this.showTimer);
12222
- this.showTimer = undefined;
12326
+ if (this.showTimer === undefined) {
12327
+ return;
12223
12328
  }
12329
+ clearTimeout(this.showTimer);
12330
+ this.showTimer = undefined;
12224
12331
  }
12225
12332
  }
12226
12333
  /** Registers `<lw-tooltip>` once (idempotent) — called from {@link provideShell} at bootstrap. */
@@ -12231,11 +12338,7 @@ function defineLwTooltip() {
12231
12338
  }
12232
12339
  }
12233
12340
 
12234
- const LW_SELECT_TAG = 'lw-select';
12235
12341
  const LW_OPTION_TAG = 'lw-option';
12236
- const LW_SELECT_CHANGE = 'lw-select-change';
12237
- let nextSelectId = 0;
12238
- const TYPEAHEAD_RESET_MS = 500;
12239
12342
  class LwOptionElement extends HTMLElement {
12240
12343
  get value() {
12241
12344
  return this.getAttribute('value');
@@ -12254,6 +12357,87 @@ class LwOptionElement extends HTMLElement {
12254
12357
  upgradeElementProperty(this, 'icon');
12255
12358
  }
12256
12359
  }
12360
+
12361
+ function readChoices(host) {
12362
+ return [...host.querySelectorAll(LW_OPTION_TAG)].map((option) => ({
12363
+ value: option.getAttribute('value') ?? '',
12364
+ label: (option.textContent ?? '').trim(),
12365
+ icon: option.getAttribute('icon'),
12366
+ disabled: option.hasAttribute('disabled'),
12367
+ }));
12368
+ }
12369
+ function createTrigger(options) {
12370
+ const trigger = document.createElement('button');
12371
+ trigger.type = 'button';
12372
+ trigger.className = 'lw-select-trigger';
12373
+ trigger.setAttribute('aria-haspopup', 'listbox');
12374
+ trigger.setAttribute('aria-expanded', 'false');
12375
+ trigger.setAttribute('aria-controls', options.listboxId);
12376
+ trigger.style.setProperty('anchor-name', options.anchorName);
12377
+ trigger.addEventListener('click', options.onToggle);
12378
+ trigger.addEventListener('keydown', options.onKeydown);
12379
+ const valueSlot = document.createElement('span');
12380
+ valueSlot.className = 'lw-select-value';
12381
+ const chevron = document.createElement('span');
12382
+ chevron.className = 'lw-select-chevron';
12383
+ chevron.setAttribute('aria-hidden', 'true');
12384
+ chevron.textContent = '▾';
12385
+ trigger.append(valueSlot, chevron);
12386
+ return { trigger, valueSlot };
12387
+ }
12388
+ function createListbox(options) {
12389
+ const listbox = document.createElement('div');
12390
+ listbox.id = options.listboxId;
12391
+ listbox.className = 'lw-select-listbox';
12392
+ listbox.setAttribute('role', 'listbox');
12393
+ listbox.hidden = true;
12394
+ listbox.style.setProperty('position-anchor', options.anchorName);
12395
+ listbox.addEventListener('keydown', options.onKeydown);
12396
+ return listbox;
12397
+ }
12398
+ function fillValueSlot(slot, text, icon) {
12399
+ slot.textContent = '';
12400
+ if (icon) {
12401
+ slot.append(createGlyph(icon));
12402
+ }
12403
+ slot.append(document.createTextNode(text));
12404
+ }
12405
+ function createOptionRow(options) {
12406
+ const choice = options.choice;
12407
+ const row = document.createElement('div');
12408
+ row.className = 'lw-select-option';
12409
+ row.setAttribute('role', 'option');
12410
+ row.id = options.id;
12411
+ row.dataset['value'] = choice.value;
12412
+ row.setAttribute('aria-selected', String(options.selected));
12413
+ row.tabIndex = -1;
12414
+ if (choice.disabled) {
12415
+ row.setAttribute('aria-disabled', 'true');
12416
+ }
12417
+ if (choice.icon) {
12418
+ row.append(createGlyph(choice.icon));
12419
+ }
12420
+ row.append(document.createTextNode(choice.label));
12421
+ row.addEventListener('click', () => {
12422
+ if (!choice.disabled) {
12423
+ options.onPick();
12424
+ }
12425
+ });
12426
+ row.addEventListener('pointermove', options.onHover);
12427
+ return row;
12428
+ }
12429
+ function createGlyph(icon) {
12430
+ const glyph = document.createElement('span');
12431
+ glyph.className = 'lw-select-glyph';
12432
+ glyph.setAttribute('aria-hidden', 'true');
12433
+ glyph.textContent = icon;
12434
+ return glyph;
12435
+ }
12436
+
12437
+ const LW_SELECT_TAG = 'lw-select';
12438
+ const LW_SELECT_CHANGE = 'lw-select-change';
12439
+ let nextSelectId = 0;
12440
+ const TYPEAHEAD_RESET_MS = 500;
12257
12441
  class LwSelectElement extends HTMLElement {
12258
12442
  static observedAttributes = [
12259
12443
  'value',
@@ -12298,10 +12482,7 @@ class LwSelectElement extends HTMLElement {
12298
12482
  if (!this.trigger) {
12299
12483
  return;
12300
12484
  }
12301
- if (name === 'value' ||
12302
- name === 'label' ||
12303
- name === 'placeholder' ||
12304
- name === 'disabled') {
12485
+ if (LwSelectElement.observedAttributes.includes(name)) {
12305
12486
  this.syncTrigger();
12306
12487
  }
12307
12488
  if (name === 'disabled' && this.hasAttribute('disabled')) {
@@ -12321,51 +12502,34 @@ class LwSelectElement extends HTMLElement {
12321
12502
  attributes: true,
12322
12503
  });
12323
12504
  }
12324
- write(fn) {
12505
+ write(function_) {
12325
12506
  this.observer?.disconnect();
12326
12507
  try {
12327
- fn();
12508
+ function_();
12328
12509
  }
12329
12510
  finally {
12330
12511
  this.observe();
12331
12512
  }
12332
12513
  }
12333
12514
  choices() {
12334
- return [...this.querySelectorAll(LW_OPTION_TAG)].map((option) => ({
12335
- value: option.getAttribute('value') ?? '',
12336
- label: (option.textContent ?? '').trim(),
12337
- icon: option.getAttribute('icon'),
12338
- disabled: option.hasAttribute('disabled'),
12339
- }));
12515
+ return readChoices(this);
12340
12516
  }
12341
12517
  selectedChoice() {
12342
12518
  const value = this.value;
12343
12519
  return this.choices().find((choice) => choice.value === value);
12344
12520
  }
12345
12521
  buildControl() {
12346
- const trigger = document.createElement('button');
12347
- trigger.type = 'button';
12348
- trigger.className = 'lw-select-trigger';
12349
- trigger.setAttribute('aria-haspopup', 'listbox');
12350
- trigger.setAttribute('aria-expanded', 'false');
12351
- trigger.setAttribute('aria-controls', this.listboxId);
12352
- trigger.style.setProperty('anchor-name', this.anchorName);
12353
- trigger.addEventListener('click', () => this.toggle());
12354
- trigger.addEventListener('keydown', (event) => this.onTriggerKeydown(event));
12355
- const valueSlot = document.createElement('span');
12356
- valueSlot.className = 'lw-select-value';
12357
- const chevron = document.createElement('span');
12358
- chevron.className = 'lw-select-chevron';
12359
- chevron.setAttribute('aria-hidden', 'true');
12360
- chevron.textContent = '▾';
12361
- trigger.append(valueSlot, chevron);
12362
- const listbox = document.createElement('div');
12363
- listbox.id = this.listboxId;
12364
- listbox.className = 'lw-select-listbox';
12365
- listbox.setAttribute('role', 'listbox');
12366
- listbox.hidden = true;
12367
- listbox.style.setProperty('position-anchor', this.anchorName);
12368
- listbox.addEventListener('keydown', (event) => this.onListboxKeydown(event));
12522
+ const { trigger, valueSlot } = createTrigger({
12523
+ anchorName: this.anchorName,
12524
+ listboxId: this.listboxId,
12525
+ onToggle: () => this.toggle(),
12526
+ onKeydown: (event) => this.onTriggerKeydown(event),
12527
+ });
12528
+ const listbox = createListbox({
12529
+ anchorName: this.anchorName,
12530
+ listboxId: this.listboxId,
12531
+ onKeydown: (event) => this.onListboxKeydown(event),
12532
+ });
12369
12533
  this.append(trigger, listbox);
12370
12534
  this.trigger = trigger;
12371
12535
  this.valueSlot = valueSlot;
@@ -12380,22 +12544,13 @@ class LwSelectElement extends HTMLElement {
12380
12544
  const label = this.getAttribute('label');
12381
12545
  const selected = this.selectedChoice();
12382
12546
  const text = selected?.label ?? this.getAttribute('placeholder') ?? '';
12383
- const icon = selected?.icon ?? null;
12384
12547
  this.write(() => {
12385
12548
  trigger.disabled = this.hasAttribute('disabled');
12386
12549
  if (label !== null) {
12387
12550
  trigger.setAttribute('aria-label', label);
12388
12551
  this.listbox?.setAttribute('aria-label', label);
12389
12552
  }
12390
- valueSlot.textContent = '';
12391
- if (icon) {
12392
- const glyph = document.createElement('span');
12393
- glyph.className = 'lw-select-glyph';
12394
- glyph.setAttribute('aria-hidden', 'true');
12395
- glyph.textContent = icon;
12396
- valueSlot.append(glyph);
12397
- }
12398
- valueSlot.append(document.createTextNode(text));
12553
+ fillValueSlot(valueSlot, text, selected?.icon ?? null);
12399
12554
  });
12400
12555
  }
12401
12556
  toggle() {
@@ -12421,7 +12576,9 @@ class LwSelectElement extends HTMLElement {
12421
12576
  const choices = this.choices();
12422
12577
  const selected = choices.findIndex((choice) => choice.value === this.value);
12423
12578
  this.setActive(Math.max(0, selected));
12424
- document.addEventListener('pointerdown', this.onOutsidePointer, true);
12579
+ document.addEventListener('pointerdown', this.onOutsidePointer, {
12580
+ capture: true,
12581
+ });
12425
12582
  }
12426
12583
  close(refocusTrigger = true) {
12427
12584
  const listbox = this.listbox;
@@ -12441,94 +12598,83 @@ class LwSelectElement extends HTMLElement {
12441
12598
  }
12442
12599
  }
12443
12600
  renderOptions() {
12444
- if (!this.listbox) {
12601
+ const listbox = this.listbox;
12602
+ if (!listbox) {
12445
12603
  return;
12446
12604
  }
12447
- const items = this.choices().map((choice, index) => {
12448
- const option = document.createElement('div');
12449
- option.className = 'lw-select-option';
12450
- option.setAttribute('role', 'option');
12451
- option.id = `${this.listboxId}-opt-${index}`;
12452
- option.dataset['value'] = choice.value;
12453
- option.setAttribute('aria-selected', String(choice.value === this.value));
12454
- option.tabIndex = -1;
12455
- if (choice.disabled) {
12456
- option.setAttribute('aria-disabled', 'true');
12457
- }
12458
- if (choice.icon) {
12459
- const glyph = document.createElement('span');
12460
- glyph.className = 'lw-select-glyph';
12461
- glyph.setAttribute('aria-hidden', 'true');
12462
- glyph.textContent = choice.icon;
12463
- option.append(glyph);
12464
- }
12465
- option.append(document.createTextNode(choice.label));
12466
- option.addEventListener('click', () => {
12467
- if (!choice.disabled) {
12468
- this.commit(choice.value);
12469
- }
12470
- });
12471
- option.addEventListener('pointermove', () => this.setActive(index));
12472
- return option;
12473
- });
12474
- const listbox = this.listbox;
12475
- this.write(() => listbox.replaceChildren(...items));
12605
+ const rows = this.choices().map((choice, index) => createOptionRow({
12606
+ choice,
12607
+ id: `${this.listboxId}-opt-${index}`,
12608
+ selected: choice.value === this.value,
12609
+ onPick: () => this.commit(choice.value),
12610
+ onHover: () => this.setActive(index),
12611
+ }));
12612
+ this.write(() => listbox.replaceChildren(...rows));
12476
12613
  }
12477
12614
  setActive(index) {
12478
12615
  if (!this.listbox) {
12479
12616
  return;
12480
12617
  }
12481
- const options = [...this.listbox.children];
12482
- if (options.length === 0) {
12618
+ const rows = [...this.listbox.children];
12619
+ if (rows.length === 0) {
12483
12620
  return;
12484
12621
  }
12485
- this.activeIndex = Math.max(0, Math.min(index, options.length - 1));
12622
+ this.activeIndex = Math.max(0, Math.min(index, rows.length - 1));
12486
12623
  this.write(() => {
12487
- options.forEach((option, i) => {
12488
- option.classList.toggle('is-active', i === this.activeIndex);
12489
- option.tabIndex = i === this.activeIndex ? 0 : -1;
12490
- });
12624
+ for (const [index_, row] of rows.entries()) {
12625
+ row.classList.toggle('is-active', index_ === this.activeIndex);
12626
+ row.tabIndex = index_ === this.activeIndex ? 0 : -1;
12627
+ }
12491
12628
  });
12492
- const active = options[this.activeIndex];
12629
+ const active = rows[this.activeIndex];
12493
12630
  active.focus();
12494
12631
  active.scrollIntoView?.({ block: 'nearest' });
12495
12632
  }
12496
12633
  onTriggerKeydown(event) {
12497
- if (['ArrowDown', 'ArrowUp', 'Enter', ' '].includes(event.key)) {
12498
- event.preventDefault();
12499
- this.openListbox();
12634
+ if (!['ArrowDown', 'ArrowUp', 'Enter', ' '].includes(event.key)) {
12635
+ return;
12500
12636
  }
12637
+ event.preventDefault();
12638
+ this.openListbox();
12501
12639
  }
12502
12640
  onListboxKeydown(event) {
12503
12641
  const last = this.choices().length - 1;
12504
12642
  switch (event.key) {
12505
- case 'ArrowDown':
12643
+ case 'ArrowDown': {
12506
12644
  this.setActive(this.activeIndex >= last ? 0 : this.activeIndex + 1);
12507
12645
  break;
12508
- case 'ArrowUp':
12646
+ }
12647
+ case 'ArrowUp': {
12509
12648
  this.setActive(this.activeIndex <= 0 ? last : this.activeIndex - 1);
12510
12649
  break;
12511
- case 'Home':
12650
+ }
12651
+ case 'Home': {
12512
12652
  this.setActive(0);
12513
12653
  break;
12514
- case 'End':
12654
+ }
12655
+ case 'End': {
12515
12656
  this.setActive(last);
12516
12657
  break;
12658
+ }
12517
12659
  case 'Enter':
12518
- case ' ':
12660
+ case ' ': {
12519
12661
  this.commitActive();
12520
12662
  break;
12521
- case 'Escape':
12663
+ }
12664
+ case 'Escape': {
12522
12665
  this.close();
12523
12666
  break;
12524
- case 'Tab':
12667
+ }
12668
+ case 'Tab': {
12525
12669
  this.close(false);
12526
12670
  return;
12527
- default:
12671
+ }
12672
+ default: {
12528
12673
  if (event.key.length === 1) {
12529
12674
  this.onTypeahead(event.key);
12530
12675
  }
12531
12676
  return;
12677
+ }
12532
12678
  }
12533
12679
  event.preventDefault();
12534
12680
  }
@@ -12540,7 +12686,7 @@ class LwSelectElement extends HTMLElement {
12540
12686
  this.typeaheadTimer = setTimeout(() => (this.typeahead = ''), TYPEAHEAD_RESET_MS);
12541
12687
  const match = this.choices().findIndex((choice) => !choice.disabled &&
12542
12688
  choice.label.toLowerCase().startsWith(this.typeahead));
12543
- if (match >= 0) {
12689
+ if (match !== -1) {
12544
12690
  this.setActive(match);
12545
12691
  }
12546
12692
  }
@@ -12689,10 +12835,11 @@ class LwButtonElement extends HTMLElement {
12689
12835
  }
12690
12836
  }
12691
12837
  onKeydown = (event) => {
12692
- if ((event.key === 'Enter' || event.key === ' ') && !this.disabled) {
12693
- event.preventDefault();
12694
- this.click();
12838
+ if (!(event.key === 'Enter' || event.key === ' ') || this.disabled) {
12839
+ return;
12695
12840
  }
12841
+ event.preventDefault();
12842
+ this.click();
12696
12843
  };
12697
12844
  render() {
12698
12845
  const stale = [...this.classList].filter((cls) => cls.startsWith('lw-btn'));
@@ -12871,7 +13018,7 @@ class ViewVisibilityService {
12871
13018
  return this.stash
12872
13019
  .keyedInstances()
12873
13020
  .filter((entry) => !entry.key.startsWith(CONTAINER_DOCK_PREFIX) &&
12874
- entry.key.split('|')[1] === path)
13021
+ entry.key.split('|', 2)[1] === path)
12875
13022
  .map((entry) => entry.instance);
12876
13023
  }
12877
13024
  removeTabs(path) {
@@ -12943,10 +13090,11 @@ class RailWorkspaceEntries {
12943
13090
  reconcile() {
12944
13091
  const wanted = this.wantedItems();
12945
13092
  for (const [id, registration] of this.registered) {
12946
- if (!wanted.has(id)) {
12947
- registration.disposable.dispose();
12948
- this.registered.delete(id);
13093
+ if (wanted.has(id)) {
13094
+ continue;
12949
13095
  }
13096
+ registration.disposable.dispose();
13097
+ this.registered.delete(id);
12950
13098
  }
12951
13099
  for (const [id, item] of wanted) {
12952
13100
  const current = this.registered.get(id);
@@ -13188,10 +13336,10 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.0", ngImpor
13188
13336
  type: Service
13189
13337
  }] });
13190
13338
  function installCompositionReport(report) {
13191
- if (typeof window === 'undefined') {
13339
+ if (globalThis.window === undefined) {
13192
13340
  return;
13193
13341
  }
13194
- const host = window;
13342
+ const host = globalThis;
13195
13343
  if (host['loomweaver'] !== undefined) {
13196
13344
  return;
13197
13345
  }
@@ -13262,7 +13410,7 @@ class PluginEnablementService {
13262
13410
  const disabled = this.disabledSet();
13263
13411
  return [...this.names().entries()]
13264
13412
  .map(([id, name]) => ({ id, name, enabled: !disabled.has(id) }))
13265
- .sort((a, b) => a.name.localeCompare(b.name));
13413
+ .toSorted((a, b) => a.name.localeCompare(b.name));
13266
13414
  }, /* @ts-ignore */
13267
13415
  ...(ngDevMode ? [{ debugName: "plugins" }] : /* istanbul ignore next */ []));
13268
13416
  constructor() {
@@ -13416,7 +13564,7 @@ function parseCatalogEntry(raw) {
13416
13564
  function dedupeById(items) {
13417
13565
  const result = [];
13418
13566
  for (const item of items) {
13419
- if (item && !result.some((existing) => existing.id === item.id)) {
13567
+ if (item && result.every((existing) => existing.id !== item.id)) {
13420
13568
  result.push(item);
13421
13569
  }
13422
13570
  }
@@ -13431,7 +13579,7 @@ function parseInstalledList(raw) {
13431
13579
  if (!Array.isArray(parsed)) {
13432
13580
  return [];
13433
13581
  }
13434
- return dedupeById(parsed.map(parseInstalledPlugin));
13582
+ return dedupeById(parsed.map((raw) => parseInstalledPlugin(raw)));
13435
13583
  }
13436
13584
  catch {
13437
13585
  return [];
@@ -13441,7 +13589,7 @@ function parseCatalogList(raw) {
13441
13589
  if (!Array.isArray(raw)) {
13442
13590
  return [];
13443
13591
  }
13444
- return dedupeById(raw.map(parseCatalogEntry));
13592
+ return dedupeById(raw.map((entry) => parseCatalogEntry(entry)));
13445
13593
  }
13446
13594
 
13447
13595
  const STORAGE_KEY$2 = 'lw.shell.deployed-plugins';
@@ -13469,7 +13617,7 @@ class PluginDeploymentService {
13469
13617
  adopt(entries) {
13470
13618
  this.persist(entries
13471
13619
  .filter((entry) => entry.deployed === true)
13472
- .map(withoutCatalogMetadata));
13620
+ .map((entry) => withoutCatalogMetadata(entry)));
13473
13621
  }
13474
13622
  isDeployed(id) {
13475
13623
  return this.entries().some((entry) => entry.id === id);
@@ -13587,10 +13735,10 @@ function registerDefaultSettings(settings) {
13587
13735
 
13588
13736
  function fuzzyScore(query, label) {
13589
13737
  const needle = query.toLowerCase();
13590
- const haystack = label.toLowerCase();
13591
13738
  if (!needle) {
13592
13739
  return 0;
13593
13740
  }
13741
+ const haystack = label.toLowerCase();
13594
13742
  let score = 0;
13595
13743
  let searchFrom = 0;
13596
13744
  let previous = -2;
@@ -13620,7 +13768,7 @@ function formatterFor(locale) {
13620
13768
  }
13621
13769
  catch {
13622
13770
  try {
13623
- return new Intl.RelativeTimeFormat(locale.replace(/_/g, '-'), {
13771
+ return new Intl.RelativeTimeFormat(locale.replaceAll('_', '-'), {
13624
13772
  numeric: 'auto',
13625
13773
  });
13626
13774
  }
@@ -13697,7 +13845,7 @@ function ranked(query, entries) {
13697
13845
  return entries
13698
13846
  .map((entry) => ({ entry, score: fuzzyScore(query, entry.label) }))
13699
13847
  .filter((scored) => scored.score !== null)
13700
- .sort((a, b) => b.score - a.score)
13848
+ .toSorted((a, b) => b.score - a.score)
13701
13849
  .map((scored) => scored.entry);
13702
13850
  }
13703
13851
  class CommandPalette {
@@ -13719,6 +13867,8 @@ class CommandPalette {
13719
13867
  ? 'tabs'
13720
13868
  : 'commands', /* @ts-ignore */
13721
13869
  ...(ngDevMode ? [{ debugName: "mode" }] : /* istanbul ignore next */ []));
13870
+ title = computed(() => this.mode() === 'tabs' ? 'palette.quickOpenTitle' : 'palette.title', /* @ts-ignore */
13871
+ ...(ngDevMode ? [{ debugName: "title" }] : /* istanbul ignore next */ []));
13722
13872
  query = signal('', /* @ts-ignore */
13723
13873
  ...(ngDevMode ? [{ debugName: "query" }] : /* istanbul ignore next */ []));
13724
13874
  rawIndex = signal(0, /* @ts-ignore */
@@ -13754,9 +13904,9 @@ class CommandPalette {
13754
13904
  pinned: tab.pinned,
13755
13905
  closable: tab.closable,
13756
13906
  lastActive: tab.lastActive,
13757
- time: tab.lastActive !== undefined
13758
- ? formatRelativeTime(locale, tab.lastActive, now)
13759
- : undefined,
13907
+ time: tab.lastActive === undefined
13908
+ ? undefined
13909
+ : formatRelativeTime(locale, tab.lastActive, now),
13760
13910
  }));
13761
13911
  }, /* @ts-ignore */
13762
13912
  ...(ngDevMode ? [{ debugName: "tabEntries" }] : /* istanbul ignore next */ []));
@@ -13785,7 +13935,7 @@ class CommandPalette {
13785
13935
  const query = this.query().trim();
13786
13936
  const entries = this.tabEntries();
13787
13937
  if (!query) {
13788
- return [...entries].sort((a, b) => (b.lastActive ?? 0) - (a.lastActive ?? 0));
13938
+ return [...entries].toSorted((a, b) => (b.lastActive ?? 0) - (a.lastActive ?? 0));
13789
13939
  }
13790
13940
  return ranked(query, entries);
13791
13941
  }, /* @ts-ignore */
@@ -13860,7 +14010,7 @@ class CommandPalette {
13860
14010
  return;
13861
14011
  }
13862
14012
  const entry = this.results()[this.activeIndex()];
13863
- if (!entry || entry.kind !== 'tab') {
14013
+ if (entry?.kind !== 'tab') {
13864
14014
  return;
13865
14015
  }
13866
14016
  event.preventDefault();
@@ -13884,11 +14034,11 @@ class CommandPalette {
13884
14034
  }
13885
14035
  }
13886
14036
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.0", ngImport: i0, type: CommandPalette, deps: [], target: i0.ɵɵFactoryTarget.Component });
13887
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.1.0", type: CommandPalette, isStandalone: true, selector: "lw-command-palette", ngImport: i0, template: "<div class=\"flex max-h-[60vh] w-full flex-col overflow-hidden\">\n <div class=\"flex items-center gap-2 border-b border-border px-4\">\n <lw-icon name=\"search\" size=\"1.1rem\" class=\"text-content-muted\" />\n <input\n data-lw-autofocus\n type=\"text\"\n role=\"combobox\"\n aria-controls=\"lw-palette-list\"\n [attr.aria-expanded]=\"true\"\n [attr.aria-activedescendant]=\"activeId()\"\n [attr.aria-label]=\"'palette.title' | transloco\"\n [value]=\"query()\"\n [placeholder]=\"\n (mode() === 'tabs' ? 'palette.tabsPlaceholder' : 'palette.placeholder')\n | transloco\n \"\n class=\"flex-1 bg-transparent py-3 text-sm text-content outline-none placeholder:text-content-faint\"\n (input)=\"onQuery($event)\"\n (keydown.arrowdown)=\"move($event, 1)\"\n (keydown.arrowup)=\"move($event, -1)\"\n (keydown.arrowright)=\"openTabActions($event)\"\n (keydown.enter)=\"runActive($event)\"\n />\n </div>\n\n <ul\n id=\"lw-palette-list\"\n role=\"listbox\"\n class=\"min-h-0 flex-1 overflow-y-auto p-1\"\n >\n @for (entry of results(); track entry.id; let i = $index) {\n @if (recentCount() > 0 && i === 0) {\n <li\n role=\"presentation\"\n data-testid=\"palette-section-recent\"\n class=\"px-3 pt-2 pb-1 text-xs font-medium text-content-faint\"\n >\n {{ 'palette.recent' | transloco }}\n </li>\n }\n @if (recentCount() > 0 && i === recentCount()) {\n <li\n role=\"presentation\"\n data-testid=\"palette-section-all\"\n class=\"px-3 pt-2 pb-1 text-xs font-medium text-content-faint\"\n >\n {{ 'palette.all' | transloco }}\n </li>\n }\n <!-- eslint-disable-next-line @angular-eslint/template/click-events-have-key-events, @angular-eslint/template/interactive-supports-focus -->\n <li\n role=\"option\"\n [id]=\"optionId(i)\"\n [attr.aria-selected]=\"i === activeIndex()\"\n class=\"flex cursor-pointer items-center gap-3 rounded-md px-3 py-2 text-sm\"\n [class]=\"\n i === activeIndex()\n ? 'bg-brand/10 text-brand-text'\n : 'text-content hover:bg-surface-overlay'\n \"\n (click)=\"select(entry)\"\n (mouseenter)=\"setActive(i)\"\n >\n @if (entry.icon; as icon) {\n <lw-icon [name]=\"icon\" size=\"1rem\" class=\"text-content-muted\" />\n }\n <span class=\"flex-1 truncate\">{{ entry.label }}</span>\n @if (timeOf(entry); as time) {\n <span class=\"shrink-0 text-xs text-content-faint\">{{ time }}</span>\n }\n @if (shortcutOf(entry); as shortcut) {\n <kbd\n class=\"rounded border border-border px-1.5 py-0.5 text-xs text-content-muted\"\n >{{ shortcut }}</kbd\n >\n }\n </li>\n } @empty {\n <li class=\"px-3 py-6 text-center text-sm text-content-muted\">\n {{\n (mode() === 'tabs' ? 'palette.tabsEmpty' : 'palette.empty') | transloco\n }}\n </li>\n }\n </ul>\n\n <div\n data-testid=\"palette-footer\"\n class=\"flex items-center gap-4 border-t border-border px-4 py-2 text-xs text-content-faint\"\n >\n <span class=\"flex items-center gap-1.5\">\n <kbd class=\"rounded border border-border px-1 py-0.5\">\u2191\u2193</kbd>\n {{ 'palette.hint.navigate' | transloco }}\n </span>\n <span class=\"flex items-center gap-1.5\">\n <kbd class=\"rounded border border-border px-1 py-0.5\">\u21B5</kbd>\n {{ 'palette.hint.run' | transloco }}\n </span>\n @if (mode() === 'tabs') {\n <span class=\"flex items-center gap-1.5\" data-testid=\"palette-hint-actions\">\n <kbd class=\"rounded border border-border px-1 py-0.5\">\u2192</kbd>\n {{ 'palette.hint.actions' | transloco }}\n </span>\n }\n <span class=\"ml-auto flex items-center gap-1.5\">\n <kbd class=\"rounded border border-border px-1 py-0.5\">Esc</kbd>\n {{ 'palette.hint.close' | transloco }}\n </span>\n </div>\n</div>\n", dependencies: [{ kind: "pipe", type: TranslocoPipe, name: "transloco" }] });
14037
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.1.0", type: CommandPalette, isStandalone: true, selector: "lw-command-palette", ngImport: i0, template: "<div class=\"flex max-h-[60vh] w-full flex-col overflow-hidden\">\n <div class=\"flex items-center gap-2 border-b border-border px-4\">\n <lw-icon name=\"search\" size=\"1.1rem\" class=\"text-content-muted\" />\n <input\n data-lw-autofocus\n type=\"text\"\n role=\"combobox\"\n aria-controls=\"lw-palette-list\"\n [attr.aria-expanded]=\"true\"\n [attr.aria-activedescendant]=\"activeId()\"\n [attr.aria-label]=\"title() | transloco\"\n [value]=\"query()\"\n [placeholder]=\"\n (mode() === 'tabs' ? 'palette.tabsPlaceholder' : 'palette.placeholder')\n | transloco\n \"\n class=\"flex-1 bg-transparent py-3 text-sm text-content outline-none placeholder:text-content-faint\"\n (input)=\"onQuery($event)\"\n (keydown.arrowdown)=\"move($event, 1)\"\n (keydown.arrowup)=\"move($event, -1)\"\n (keydown.arrowright)=\"openTabActions($event)\"\n (keydown.enter)=\"runActive($event)\"\n />\n </div>\n\n <ul\n id=\"lw-palette-list\"\n role=\"listbox\"\n class=\"min-h-0 flex-1 overflow-y-auto p-1\"\n >\n @for (entry of results(); track entry.id; let i = $index) {\n @if (recentCount() > 0 && i === 0) {\n <li\n role=\"presentation\"\n data-testid=\"palette-section-recent\"\n class=\"px-3 pt-2 pb-1 text-xs font-medium text-content-faint\"\n >\n {{ 'palette.recent' | transloco }}\n </li>\n }\n @if (recentCount() > 0 && i === recentCount()) {\n <li\n role=\"presentation\"\n data-testid=\"palette-section-all\"\n class=\"px-3 pt-2 pb-1 text-xs font-medium text-content-faint\"\n >\n {{ 'palette.all' | transloco }}\n </li>\n }\n <!-- eslint-disable-next-line @angular-eslint/template/click-events-have-key-events, @angular-eslint/template/interactive-supports-focus -->\n <li\n role=\"option\"\n [id]=\"optionId(i)\"\n [attr.aria-selected]=\"i === activeIndex()\"\n class=\"flex cursor-pointer items-center gap-3 rounded-md px-3 py-2 text-sm\"\n [class]=\"\n i === activeIndex()\n ? 'bg-brand/10 text-brand-text'\n : 'text-content hover:bg-surface-overlay'\n \"\n (click)=\"select(entry)\"\n (mouseenter)=\"setActive(i)\"\n >\n @if (entry.icon; as icon) {\n <lw-icon [name]=\"icon\" size=\"1rem\" class=\"text-content-muted\" />\n }\n <span class=\"flex-1 truncate\">{{ entry.label }}</span>\n @if (timeOf(entry); as time) {\n <span class=\"shrink-0 text-xs text-content-faint\">{{ time }}</span>\n }\n @if (shortcutOf(entry); as shortcut) {\n <kbd\n class=\"rounded border border-border px-1.5 py-0.5 text-xs text-content-muted\"\n >{{ shortcut }}</kbd\n >\n }\n </li>\n } @empty {\n <li class=\"px-3 py-6 text-center text-sm text-content-muted\">\n {{\n (mode() === 'tabs' ? 'palette.tabsEmpty' : 'palette.empty') | transloco\n }}\n </li>\n }\n </ul>\n\n <div\n data-testid=\"palette-footer\"\n class=\"flex items-center gap-4 border-t border-border px-4 py-2 text-xs text-content-faint\"\n >\n <span class=\"flex items-center gap-1.5\">\n <kbd class=\"rounded border border-border px-1 py-0.5\">\u2191\u2193</kbd>\n {{ 'palette.hint.navigate' | transloco }}\n </span>\n <span class=\"flex items-center gap-1.5\">\n <kbd class=\"rounded border border-border px-1 py-0.5\">\u21B5</kbd>\n {{ 'palette.hint.run' | transloco }}\n </span>\n @if (mode() === 'tabs') {\n <span class=\"flex items-center gap-1.5\" data-testid=\"palette-hint-actions\">\n <kbd class=\"rounded border border-border px-1 py-0.5\">\u2192</kbd>\n {{ 'palette.hint.actions' | transloco }}\n </span>\n }\n <span class=\"ml-auto flex items-center gap-1.5\">\n <kbd class=\"rounded border border-border px-1 py-0.5\">Esc</kbd>\n {{ 'palette.hint.close' | transloco }}\n </span>\n </div>\n</div>\n", dependencies: [{ kind: "pipe", type: TranslocoPipe, name: "transloco" }] });
13888
14038
  }
13889
14039
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.0", ngImport: i0, type: CommandPalette, decorators: [{
13890
14040
  type: Component,
13891
- args: [{ selector: 'lw-command-palette', schemas: [CUSTOM_ELEMENTS_SCHEMA], imports: [TranslocoPipe], template: "<div class=\"flex max-h-[60vh] w-full flex-col overflow-hidden\">\n <div class=\"flex items-center gap-2 border-b border-border px-4\">\n <lw-icon name=\"search\" size=\"1.1rem\" class=\"text-content-muted\" />\n <input\n data-lw-autofocus\n type=\"text\"\n role=\"combobox\"\n aria-controls=\"lw-palette-list\"\n [attr.aria-expanded]=\"true\"\n [attr.aria-activedescendant]=\"activeId()\"\n [attr.aria-label]=\"'palette.title' | transloco\"\n [value]=\"query()\"\n [placeholder]=\"\n (mode() === 'tabs' ? 'palette.tabsPlaceholder' : 'palette.placeholder')\n | transloco\n \"\n class=\"flex-1 bg-transparent py-3 text-sm text-content outline-none placeholder:text-content-faint\"\n (input)=\"onQuery($event)\"\n (keydown.arrowdown)=\"move($event, 1)\"\n (keydown.arrowup)=\"move($event, -1)\"\n (keydown.arrowright)=\"openTabActions($event)\"\n (keydown.enter)=\"runActive($event)\"\n />\n </div>\n\n <ul\n id=\"lw-palette-list\"\n role=\"listbox\"\n class=\"min-h-0 flex-1 overflow-y-auto p-1\"\n >\n @for (entry of results(); track entry.id; let i = $index) {\n @if (recentCount() > 0 && i === 0) {\n <li\n role=\"presentation\"\n data-testid=\"palette-section-recent\"\n class=\"px-3 pt-2 pb-1 text-xs font-medium text-content-faint\"\n >\n {{ 'palette.recent' | transloco }}\n </li>\n }\n @if (recentCount() > 0 && i === recentCount()) {\n <li\n role=\"presentation\"\n data-testid=\"palette-section-all\"\n class=\"px-3 pt-2 pb-1 text-xs font-medium text-content-faint\"\n >\n {{ 'palette.all' | transloco }}\n </li>\n }\n <!-- eslint-disable-next-line @angular-eslint/template/click-events-have-key-events, @angular-eslint/template/interactive-supports-focus -->\n <li\n role=\"option\"\n [id]=\"optionId(i)\"\n [attr.aria-selected]=\"i === activeIndex()\"\n class=\"flex cursor-pointer items-center gap-3 rounded-md px-3 py-2 text-sm\"\n [class]=\"\n i === activeIndex()\n ? 'bg-brand/10 text-brand-text'\n : 'text-content hover:bg-surface-overlay'\n \"\n (click)=\"select(entry)\"\n (mouseenter)=\"setActive(i)\"\n >\n @if (entry.icon; as icon) {\n <lw-icon [name]=\"icon\" size=\"1rem\" class=\"text-content-muted\" />\n }\n <span class=\"flex-1 truncate\">{{ entry.label }}</span>\n @if (timeOf(entry); as time) {\n <span class=\"shrink-0 text-xs text-content-faint\">{{ time }}</span>\n }\n @if (shortcutOf(entry); as shortcut) {\n <kbd\n class=\"rounded border border-border px-1.5 py-0.5 text-xs text-content-muted\"\n >{{ shortcut }}</kbd\n >\n }\n </li>\n } @empty {\n <li class=\"px-3 py-6 text-center text-sm text-content-muted\">\n {{\n (mode() === 'tabs' ? 'palette.tabsEmpty' : 'palette.empty') | transloco\n }}\n </li>\n }\n </ul>\n\n <div\n data-testid=\"palette-footer\"\n class=\"flex items-center gap-4 border-t border-border px-4 py-2 text-xs text-content-faint\"\n >\n <span class=\"flex items-center gap-1.5\">\n <kbd class=\"rounded border border-border px-1 py-0.5\">\u2191\u2193</kbd>\n {{ 'palette.hint.navigate' | transloco }}\n </span>\n <span class=\"flex items-center gap-1.5\">\n <kbd class=\"rounded border border-border px-1 py-0.5\">\u21B5</kbd>\n {{ 'palette.hint.run' | transloco }}\n </span>\n @if (mode() === 'tabs') {\n <span class=\"flex items-center gap-1.5\" data-testid=\"palette-hint-actions\">\n <kbd class=\"rounded border border-border px-1 py-0.5\">\u2192</kbd>\n {{ 'palette.hint.actions' | transloco }}\n </span>\n }\n <span class=\"ml-auto flex items-center gap-1.5\">\n <kbd class=\"rounded border border-border px-1 py-0.5\">Esc</kbd>\n {{ 'palette.hint.close' | transloco }}\n </span>\n </div>\n</div>\n" }]
14041
+ args: [{ selector: 'lw-command-palette', schemas: [CUSTOM_ELEMENTS_SCHEMA], imports: [TranslocoPipe], template: "<div class=\"flex max-h-[60vh] w-full flex-col overflow-hidden\">\n <div class=\"flex items-center gap-2 border-b border-border px-4\">\n <lw-icon name=\"search\" size=\"1.1rem\" class=\"text-content-muted\" />\n <input\n data-lw-autofocus\n type=\"text\"\n role=\"combobox\"\n aria-controls=\"lw-palette-list\"\n [attr.aria-expanded]=\"true\"\n [attr.aria-activedescendant]=\"activeId()\"\n [attr.aria-label]=\"title() | transloco\"\n [value]=\"query()\"\n [placeholder]=\"\n (mode() === 'tabs' ? 'palette.tabsPlaceholder' : 'palette.placeholder')\n | transloco\n \"\n class=\"flex-1 bg-transparent py-3 text-sm text-content outline-none placeholder:text-content-faint\"\n (input)=\"onQuery($event)\"\n (keydown.arrowdown)=\"move($event, 1)\"\n (keydown.arrowup)=\"move($event, -1)\"\n (keydown.arrowright)=\"openTabActions($event)\"\n (keydown.enter)=\"runActive($event)\"\n />\n </div>\n\n <ul\n id=\"lw-palette-list\"\n role=\"listbox\"\n class=\"min-h-0 flex-1 overflow-y-auto p-1\"\n >\n @for (entry of results(); track entry.id; let i = $index) {\n @if (recentCount() > 0 && i === 0) {\n <li\n role=\"presentation\"\n data-testid=\"palette-section-recent\"\n class=\"px-3 pt-2 pb-1 text-xs font-medium text-content-faint\"\n >\n {{ 'palette.recent' | transloco }}\n </li>\n }\n @if (recentCount() > 0 && i === recentCount()) {\n <li\n role=\"presentation\"\n data-testid=\"palette-section-all\"\n class=\"px-3 pt-2 pb-1 text-xs font-medium text-content-faint\"\n >\n {{ 'palette.all' | transloco }}\n </li>\n }\n <!-- eslint-disable-next-line @angular-eslint/template/click-events-have-key-events, @angular-eslint/template/interactive-supports-focus -->\n <li\n role=\"option\"\n [id]=\"optionId(i)\"\n [attr.aria-selected]=\"i === activeIndex()\"\n class=\"flex cursor-pointer items-center gap-3 rounded-md px-3 py-2 text-sm\"\n [class]=\"\n i === activeIndex()\n ? 'bg-brand/10 text-brand-text'\n : 'text-content hover:bg-surface-overlay'\n \"\n (click)=\"select(entry)\"\n (mouseenter)=\"setActive(i)\"\n >\n @if (entry.icon; as icon) {\n <lw-icon [name]=\"icon\" size=\"1rem\" class=\"text-content-muted\" />\n }\n <span class=\"flex-1 truncate\">{{ entry.label }}</span>\n @if (timeOf(entry); as time) {\n <span class=\"shrink-0 text-xs text-content-faint\">{{ time }}</span>\n }\n @if (shortcutOf(entry); as shortcut) {\n <kbd\n class=\"rounded border border-border px-1.5 py-0.5 text-xs text-content-muted\"\n >{{ shortcut }}</kbd\n >\n }\n </li>\n } @empty {\n <li class=\"px-3 py-6 text-center text-sm text-content-muted\">\n {{\n (mode() === 'tabs' ? 'palette.tabsEmpty' : 'palette.empty') | transloco\n }}\n </li>\n }\n </ul>\n\n <div\n data-testid=\"palette-footer\"\n class=\"flex items-center gap-4 border-t border-border px-4 py-2 text-xs text-content-faint\"\n >\n <span class=\"flex items-center gap-1.5\">\n <kbd class=\"rounded border border-border px-1 py-0.5\">\u2191\u2193</kbd>\n {{ 'palette.hint.navigate' | transloco }}\n </span>\n <span class=\"flex items-center gap-1.5\">\n <kbd class=\"rounded border border-border px-1 py-0.5\">\u21B5</kbd>\n {{ 'palette.hint.run' | transloco }}\n </span>\n @if (mode() === 'tabs') {\n <span class=\"flex items-center gap-1.5\" data-testid=\"palette-hint-actions\">\n <kbd class=\"rounded border border-border px-1 py-0.5\">\u2192</kbd>\n {{ 'palette.hint.actions' | transloco }}\n </span>\n }\n <span class=\"ml-auto flex items-center gap-1.5\">\n <kbd class=\"rounded border border-border px-1 py-0.5\">Esc</kbd>\n {{ 'palette.hint.close' | transloco }}\n </span>\n </div>\n</div>\n" }]
13892
14042
  }] });
13893
14043
 
13894
14044
  class WorkspaceDialog {
@@ -14122,7 +14272,12 @@ function seedHostCommands(registry, layout, deps) {
14122
14272
  shortcut: 'mod+k',
14123
14273
  popout: true,
14124
14274
  run: () => {
14125
- dialogs.open(CommandPalette, { bare: true, size: 'lg', align: 'top' });
14275
+ dialogs.open(CommandPalette, {
14276
+ bare: true,
14277
+ size: 'lg',
14278
+ align: 'top',
14279
+ title: 'palette.title',
14280
+ });
14126
14281
  },
14127
14282
  });
14128
14283
  registry.addCommand({
@@ -14147,6 +14302,7 @@ function seedHostCommands(registry, layout, deps) {
14147
14302
  bare: true,
14148
14303
  size: 'lg',
14149
14304
  align: 'top',
14305
+ title: 'palette.quickOpenTitle',
14150
14306
  data: { mode: 'tabs' },
14151
14307
  });
14152
14308
  },
@@ -14261,9 +14417,12 @@ function seedBuiltInMenus(registry, layout, deps) {
14261
14417
  return;
14262
14418
  }
14263
14419
  registerTabContextMenu(registry, deps.tabs, deps.paneMove, deps.popout, deps.features);
14420
+ seedRailMenus(registry, layout, deps);
14421
+ seedViewMenus(registry, layout, deps);
14422
+ }
14423
+ function seedRailMenus(registry, layout, deps) {
14264
14424
  const railCount = layout.regions.filter((region) => region.type === 'rail').length;
14265
14425
  const rail = deps.features.rail;
14266
- const sidebar = deps.features.sidebar;
14267
14426
  if (railCount >= 1 && rail.hideItems) {
14268
14427
  registerRailContextMenu(registry, deps.railItems);
14269
14428
  }
@@ -14273,6 +14432,9 @@ function seedBuiltInMenus(registry, layout, deps) {
14273
14432
  if (railCount >= 1 && rail.curate) {
14274
14433
  registerRailCustomizeMenu(registry);
14275
14434
  }
14435
+ }
14436
+ function seedViewMenus(registry, layout, deps) {
14437
+ const sidebar = deps.features.sidebar;
14276
14438
  if (sidebar.resetViewState) {
14277
14439
  registerViewResetMenu(registry, deps.viewStates, deps.viewInstances);
14278
14440
  }
@@ -14421,7 +14583,7 @@ class BootLatchedIdentity {
14421
14583
  return this.latched;
14422
14584
  }
14423
14585
  const id = this.read();
14424
- if (id === null || id === undefined || id === '') {
14586
+ if (!id) {
14425
14587
  return null;
14426
14588
  }
14427
14589
  this.latched = id;
@@ -14653,6 +14815,16 @@ function accessCanMatch(access) {
14653
14815
  };
14654
14816
  }
14655
14817
 
14818
+ const keepPopout = (_route, state) => {
14819
+ if (!isPopoutUrl(inject(BootAddress).path)) {
14820
+ return true;
14821
+ }
14822
+ if (isDevMode()) {
14823
+ console.warn(popoutNavigationRefusal(normalizePath(state.url)));
14824
+ }
14825
+ return false;
14826
+ };
14827
+
14656
14828
  const settleWorkspace = async (_route, state) => {
14657
14829
  const claims = inject(WORKSPACE_CLAIMS);
14658
14830
  await claims.settle(normalizePath(state.url));
@@ -14693,6 +14865,7 @@ function buildContentRoutes(contentRoutes, omitted = [], retention = 'destroy')
14693
14865
  const placeholders = omitted.map((route) => ({
14694
14866
  path: route.path,
14695
14867
  component: RouteUnavailableView,
14868
+ canActivate: [keepPopout],
14696
14869
  data: { content: true, routePlaceholder: true },
14697
14870
  }));
14698
14871
  return [...buildRegisteredRoutes(contentRoutes, retention), ...placeholders];
@@ -14711,7 +14884,7 @@ function surfaceRoute(route, retained) {
14711
14884
  function subStub(path, pathMatch) {
14712
14885
  return {
14713
14886
  path,
14714
- ...(pathMatch ? { pathMatch } : {}),
14887
+ ...(pathMatch && { pathMatch }),
14715
14888
  component: ContentSubStub,
14716
14889
  data: { content: true, sub: true },
14717
14890
  };
@@ -14738,7 +14911,7 @@ function buildRegisteredRoutes(contentRoutes, retention) {
14738
14911
  const angular = {
14739
14912
  path: route.path,
14740
14913
  ...surfaceRoute(route, retained),
14741
- canActivate: [settleWorkspace],
14914
+ canActivate: [keepPopout, settleWorkspace],
14742
14915
  data: {
14743
14916
  content: true,
14744
14917
  chromeless: route.chromeless,
@@ -14757,6 +14930,7 @@ function buildRegisteredRoutes(contentRoutes, retention) {
14757
14930
  const placeholder = {
14758
14931
  path: route.path,
14759
14932
  component: AuthRequiredView,
14933
+ canActivate: [keepPopout],
14760
14934
  data: {
14761
14935
  content: true,
14762
14936
  authPlaceholder: true,
@@ -15002,13 +15176,13 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.0", ngImpor
15002
15176
  type: Service
15003
15177
  }] });
15004
15178
  function pathOfKey(key) {
15005
- return key.split('|')[1] ?? '';
15179
+ return key.split('|', 2)[1] ?? '';
15006
15180
  }
15007
15181
  function stashKeyLive(key, open, routes, views) {
15008
15182
  if (key.startsWith(PRIMARY_RETENTION_PREFIX)) {
15009
15183
  return true;
15010
15184
  }
15011
- const [scope, path] = key.split('|');
15185
+ const [scope, path] = key.split('|', 2);
15012
15186
  if (!tabOpen(open.get(scope), routes, path)) {
15013
15187
  return false;
15014
15188
  }
@@ -15541,7 +15715,8 @@ class IconRegistry {
15541
15715
  setIcon(name, safe);
15542
15716
  added.push(name);
15543
15717
  }
15544
- return { dispose: () => added.forEach((name) => removeIcon(name)) };
15718
+ return { dispose: () => { for (const name of added)
15719
+ removeIcon(name); } };
15545
15720
  }
15546
15721
  resolve(name) {
15547
15722
  return resolveIcon(name);
@@ -15691,6 +15866,9 @@ function providePlugins(...plugins) {
15691
15866
  ];
15692
15867
  }
15693
15868
 
15869
+ /** Multi-provider token: each contribution adds one sandboxed plugin to load. */
15870
+ const FRAME_PLUGIN = new InjectionToken('FRAME_PLUGIN');
15871
+
15694
15872
  const STORAGE_KEY = 'lw.shell.installed-plugins';
15695
15873
  /**
15696
15874
  * The user's installed community plugins. Holds only the state: which catalog entries the
@@ -15722,7 +15900,7 @@ class PluginInstallService {
15722
15900
  return this.entries().some((entry) => entry.id === id);
15723
15901
  }
15724
15902
  /** The installed entry for an id, or `undefined` — the baseline an update is compared against. */
15725
- find(id) {
15903
+ byId(id) {
15726
15904
  return this.entries().find((entry) => entry.id === id);
15727
15905
  }
15728
15906
  /**
@@ -15787,202 +15965,86 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.0", ngImpor
15787
15965
  type: Service
15788
15966
  }], ctorParameters: () => [] });
15789
15967
 
15790
- const INPUT_TYPES = ['text', 'date', 'email', 'number', 'password'];
15791
- function sanitizeOptions(raw) {
15792
- if (!Array.isArray(raw)) {
15793
- return [];
15968
+ function levelOf(plugin) {
15969
+ return plugin.level ?? DEFAULT_ISOLATION_LEVEL;
15970
+ }
15971
+ function signatureOf(plugin) {
15972
+ const sorted = (values) => [...(values ?? [])].toSorted((a, b) => a.localeCompare(b)).join(',');
15973
+ return `${plugin.entryUrl}|${sorted(plugin.capabilities)}|${sorted(plugin.granted)}|${plugin.version ?? ''}|${levelOf(plugin)}`;
15974
+ }
15975
+ function runnablePlugins(composed, installed, deployed, catalogCap) {
15976
+ const claimed = new Set(composed.map((plugin) => plugin.id));
15977
+ const provided = new Set(deployed.map((plugin) => plugin.id));
15978
+ const fromCatalog = [];
15979
+ for (const plugin of [...deployed, ...installed]) {
15980
+ if (claimed.has(plugin.id)) {
15981
+ continue;
15982
+ }
15983
+ const asked = plugin.level ?? DEFAULT_ISOLATION_LEVEL;
15984
+ if (exceedsLevel(asked, catalogCap)) {
15985
+ console.error(`Plugin "${plugin.id}" asks to run ${asked}, which this catalog may not confer ` +
15986
+ `(its cap is ${catalogCap}). It is not started.`);
15987
+ continue;
15988
+ }
15989
+ claimed.add(plugin.id);
15990
+ fromCatalog.push({
15991
+ id: plugin.id,
15992
+ entryUrl: plugin.entryUrl,
15993
+ capabilities: plugin.capabilities,
15994
+ name: plugin.name,
15995
+ granted: plugin.capabilities ?? [],
15996
+ version: plugin.version,
15997
+ level: asked,
15998
+ provided: provided.has(plugin.id) || undefined,
15999
+ });
15794
16000
  }
15795
- return raw
15796
- .filter((option) => typeof option === 'object' &&
15797
- option !== null &&
15798
- typeof option['value'] === 'string' &&
15799
- typeof option['label'] === 'string')
15800
- .map((option) => ({ value: option.value, label: option.label }));
16001
+ return [...composed, ...fromCatalog];
15801
16002
  }
15802
- function optionalString(value) {
15803
- return typeof value === 'string' ? value : undefined;
15804
- }
15805
- function optionalNumber(value) {
15806
- return typeof value === 'number' ? value : undefined;
15807
- }
15808
- function buildTextControl(value, control) {
15809
- return {
15810
- kind: 'text',
15811
- value,
15812
- inputType: INPUT_TYPES.find((type) => type === control['inputType']),
15813
- placeholder: optionalString(control['placeholder']),
15814
- };
15815
- }
15816
- function buildSelectControl(pluginId, value, control) {
15817
- const options = sanitizeOptions(control['options']);
15818
- if (options.length === 0) {
15819
- throw new Error(`Sandbox plugin "${pluginId}": a select control needs at least one { value, label } option.`);
15820
- }
15821
- return { kind: 'select', value, options };
15822
- }
15823
- function buildSliderControl(value, control) {
15824
- return {
15825
- kind: 'slider',
15826
- value,
15827
- min: optionalNumber(control['min']),
15828
- max: optionalNumber(control['max']),
15829
- step: optionalNumber(control['step']),
15830
- };
15831
- }
15832
- function sanitizeControl(pluginId, raw) {
15833
- const control = (raw ?? {});
15834
- const kind = control['kind'];
15835
- const value = control['value'];
15836
- if (kind === 'toggle' && typeof value === 'boolean') {
15837
- return { kind, value };
15838
- }
15839
- if (kind === 'text' && typeof value === 'string') {
15840
- return buildTextControl(value, control);
15841
- }
15842
- if (kind === 'select' && typeof value === 'string') {
15843
- return buildSelectControl(pluginId, value, control);
15844
- }
15845
- if (kind === 'slider' && typeof value === 'number') {
15846
- return buildSliderControl(value, control);
15847
- }
15848
- throw new Error(`Sandbox plugin "${pluginId}": a settings control must be toggle/text/select/slider with a matching default 'value'.`);
15849
- }
15850
- function sanitizeRow(pluginId, raw) {
15851
- const row = (raw ?? {});
15852
- if (typeof row['id'] !== 'string' || row['id'].length === 0) {
15853
- throw new Error(`Sandbox plugin "${pluginId}": every settings row needs a non-empty 'id'.`);
15854
- }
15855
- if (typeof row['label'] !== 'string' || row['label'].length === 0) {
15856
- throw new Error(`Sandbox plugin "${pluginId}": every settings row needs a non-empty 'label'.`);
15857
- }
15858
- return {
15859
- id: row['id'],
15860
- label: row['label'],
15861
- description: typeof row['description'] === 'string' ? row['description'] : undefined,
15862
- control: sanitizeControl(pluginId, row['control']),
15863
- };
15864
- }
15865
- function sanitizeRpcSettingsSection(pluginId, section) {
15866
- const raw = (section ?? {});
15867
- if (typeof raw['id'] !== 'string' || raw['id'].length === 0) {
15868
- throw new Error(`Sandbox plugin "${pluginId}": registerSettingsSection requires a non-empty 'id'.`);
15869
- }
15870
- if (typeof raw['title'] !== 'string' || raw['title'].length === 0) {
15871
- throw new Error(`Sandbox plugin "${pluginId}": registerSettingsSection requires a non-empty 'title'.`);
15872
- }
15873
- if (!Array.isArray(raw['rows']) || raw['rows'].length === 0) {
15874
- throw new Error(`Sandbox plugin "${pluginId}": registerSettingsSection requires at least one row.`);
15875
- }
15876
- return {
15877
- id: raw['id'],
15878
- title: raw['title'],
15879
- order: typeof raw['order'] === 'number' ? raw['order'] : undefined,
15880
- rows: raw['rows'].map((row) => sanitizeRow(pluginId, row)),
15881
- };
15882
- }
15883
- function defaultsOf(wire) {
15884
- const defaults = {};
15885
- for (const row of wire.rows) {
15886
- defaults[row.id] = row.control.value;
15887
- }
15888
- return defaults;
15889
- }
15890
- function typedOverlay(defaults, raw) {
15891
- if (!raw) {
15892
- return defaults;
15893
- }
15894
- try {
15895
- const parsed = JSON.parse(raw);
15896
- if (typeof parsed !== 'object' || parsed === null) {
15897
- return defaults;
15898
- }
15899
- const merged = { ...defaults };
15900
- for (const [key, value] of Object.entries(parsed)) {
15901
- if (key in defaults && typeof value === typeof defaults[key]) {
15902
- merged[key] = value;
15903
- }
15904
- }
15905
- return merged;
15906
- }
15907
- catch {
15908
- return defaults;
15909
- }
15910
- }
15911
- function buildFrameSection(deps) {
15912
- const { pluginId, wire, group, store, sync, notify } = deps;
15913
- const key = `lw.plugin-settings:${pluginId}:${wire.id}`;
15914
- const defaults = defaultsOf(wire);
15915
- const values = signal(typedOverlay(defaults, store.peek?.(key)), /* @ts-ignore */
15916
- ...(ngDevMode ? [{ debugName: "values" }] : /* istanbul ignore next */ []));
15917
- const applyStored = (raw) => {
15918
- values.set(typedOverlay(defaults, raw));
15919
- notify(wire.id, values());
15920
- };
15921
- if (store.peek) {
15922
- notify(wire.id, values());
15923
- }
15924
- else {
15925
- hydrateAsync(store, key, applyStored);
15926
- }
15927
- const disposeSync = sync.register('settings', key, applyStored);
15928
- const set = (rowId, value) => {
15929
- values.update((current) => ({ ...current, [rowId]: value }));
15930
- void store.set(key, JSON.stringify(values()));
15931
- notify(wire.id, values());
15932
- };
15933
- const section = {
15934
- id: `${pluginId}.${wire.id}`,
15935
- title: wire.title,
15936
- group,
15937
- order: wire.order,
15938
- rows: wire.rows.map((row) => ({
15939
- id: `${pluginId}.${wire.id}.${row.id}`,
15940
- label: row.label,
15941
- description: row.description,
15942
- control: hostControl(row, values, set),
15943
- })),
15944
- };
15945
- return { section, disposeSync };
15946
- }
15947
- function hostControl(row, values, set) {
15948
- const control = row.control;
15949
- switch (control.kind) {
15950
- case 'toggle':
15951
- return {
15952
- kind: 'toggle',
15953
- value: () => values()[row.id] === true,
15954
- set: (value) => set(row.id, value),
15955
- };
15956
- case 'text':
15957
- return {
15958
- kind: 'text',
15959
- inputType: control.inputType,
15960
- placeholder: control.placeholder,
15961
- value: () => String(values()[row.id] ?? ''),
15962
- set: (value) => set(row.id, value),
15963
- };
15964
- case 'select':
15965
- return {
15966
- kind: 'select',
15967
- options: control.options,
15968
- value: () => String(values()[row.id] ?? control.value),
15969
- set: (value) => set(row.id, value),
15970
- };
15971
- case 'slider':
15972
- return {
15973
- kind: 'slider',
15974
- min: control.min,
15975
- max: control.max,
15976
- step: control.step,
15977
- value: () => Number(values()[row.id] ?? control.value),
15978
- set: (value) => set(row.id, value),
15979
- };
15980
- }
16003
+
16004
+ const UNCARRIABLE_ARGUMENTS = {
16005
+ outcome: 'refused',
16006
+ reason: 'invalid-arguments',
16007
+ message: 'Arguments must be an object of single values or lists of them; anything else cannot cross the ' +
16008
+ 'sandbox boundary as the value it was.',
16009
+ };
16010
+ function invokeRpcCommand(ctx, id, args) {
16011
+ const carried = args === undefined ? undefined : asCommandArguments(args);
16012
+ return carried === null
16013
+ ? Promise.resolve(UNCARRIABLE_ARGUMENTS)
16014
+ : ctx.invokeCommand(String(id), carried);
15981
16015
  }
15982
16016
 
15983
16017
  const MAX_RPC_AREA_DEPTH = 8;
15984
16018
  function sanitizeRpcSurface(pluginId, surface, permitted) {
15985
16019
  const raw = (surface ?? {});
16020
+ const { id, title } = rpcSurfaceIdentity(pluginId, raw);
16021
+ const container = sanitizeRpcContainer(raw['container']);
16022
+ const iframe = container === undefined
16023
+ ? rpcIframeUrl(pluginId, raw['iframe'], permitted)
16024
+ : undefined;
16025
+ const routable = sanitizeRpcRoutable(raw['routable']);
16026
+ const docks = sanitizeRpcDocks(raw['docks']);
16027
+ assertRpcSurfaceAddress(pluginId, container, routable, docks);
16028
+ const shared = {
16029
+ id,
16030
+ title,
16031
+ icon: typeof raw['icon'] === 'string' ? raw['icon'] : undefined,
16032
+ order: typeof raw['order'] === 'number' ? raw['order'] : undefined,
16033
+ instanceable: raw['instanceable'] === true ? true : undefined,
16034
+ retain: raw['retain'] === 'always' || raw['retain'] === 'never'
16035
+ ? raw['retain']
16036
+ : undefined,
16037
+ saveOn: raw['saveOn'] === 'hide' ? 'hide' : undefined,
16038
+ closable: raw['closable'] === false ? false : undefined,
16039
+ padded: raw['padded'] === false ? false : undefined,
16040
+ routable,
16041
+ docks,
16042
+ };
16043
+ return container === undefined
16044
+ ? { ...shared, iframe: iframe }
16045
+ : { ...shared, container };
16046
+ }
16047
+ function rpcSurfaceIdentity(pluginId, raw) {
15986
16048
  if (typeof raw['id'] !== 'string' || raw['id'].length === 0) {
15987
16049
  throw new Error(`Sandbox plugin "${pluginId}": registerSurface requires a non-empty 'id'.`);
15988
16050
  }
@@ -15997,20 +16059,20 @@ function sanitizeRpcSurface(pluginId, surface, permitted) {
15997
16059
  throw new Error(`Sandbox plugin "${pluginId}": 'access' does not cross the RPC boundary — ` +
15998
16060
  `a sandboxed surface gates itself from the pushed session state.`);
15999
16061
  }
16000
- const container = sanitizeRpcContainer(raw['container']);
16001
- const iframe = raw['iframe'];
16002
- if (container === undefined) {
16003
- if (typeof iframe !== 'string') {
16004
- throw new Error(`Sandbox plugin "${pluginId}": registerSurface needs an { iframe } URL or a { container } spec.`);
16005
- }
16006
- const origin = surfaceOrigin(iframe);
16007
- if (origin === null || !permittedOrigins(permitted).has(origin)) {
16008
- throw new Error(`Sandbox plugin "${pluginId}": the iframe surface must be served from an origin this ` +
16009
- `distribution permitted for it, got "${iframe}".`);
16010
- }
16062
+ return { id: raw['id'], title: raw['title'] };
16063
+ }
16064
+ function rpcIframeUrl(pluginId, value, permitted) {
16065
+ if (typeof value !== 'string') {
16066
+ throw new TypeError(`Sandbox plugin "${pluginId}": registerSurface needs an { iframe } URL or a { container } spec.`);
16011
16067
  }
16012
- const routable = sanitizeRpcRoutable(raw['routable']);
16013
- const docks = sanitizeRpcDocks(raw['docks']);
16068
+ const origin = surfaceOrigin(value);
16069
+ if (origin === null || !permittedOrigins(permitted).has(origin)) {
16070
+ throw new Error(`Sandbox plugin "${pluginId}": the iframe surface must be served from an origin this ` +
16071
+ `distribution permitted for it, got "${value}".`);
16072
+ }
16073
+ return value;
16074
+ }
16075
+ function assertRpcSurfaceAddress(pluginId, container, routable, docks) {
16014
16076
  if (routable === undefined && docks === undefined) {
16015
16077
  throw new Error(`Sandbox plugin "${pluginId}": registerSurface needs 'routable.path' (a URL-addressed surface) ` +
16016
16078
  `or 'docks' (a surface hosted at a dock).`);
@@ -16019,24 +16081,6 @@ function sanitizeRpcSurface(pluginId, surface, permitted) {
16019
16081
  throw new Error(`Sandbox plugin "${pluginId}": a container surface must be routable — a container tab holds ` +
16020
16082
  `its own ':id'.`);
16021
16083
  }
16022
- const shared = {
16023
- id: raw['id'],
16024
- title: raw['title'],
16025
- icon: typeof raw['icon'] === 'string' ? raw['icon'] : undefined,
16026
- order: typeof raw['order'] === 'number' ? raw['order'] : undefined,
16027
- instanceable: raw['instanceable'] === true ? true : undefined,
16028
- retain: raw['retain'] === 'always' || raw['retain'] === 'never'
16029
- ? raw['retain']
16030
- : undefined,
16031
- saveOn: raw['saveOn'] === 'hide' ? 'hide' : undefined,
16032
- closable: raw['closable'] === false ? false : undefined,
16033
- padded: raw['padded'] === false ? false : undefined,
16034
- routable,
16035
- docks,
16036
- };
16037
- return container !== undefined
16038
- ? { ...shared, container }
16039
- : { ...shared, iframe: iframe };
16040
16084
  }
16041
16085
  function sanitizeRpcRoutable(value) {
16042
16086
  if (typeof value !== 'object' || value === null) {
@@ -16089,7 +16133,7 @@ function sanitizeRpcArea(value, depth) {
16089
16133
  const raw = value;
16090
16134
  const size = typeof raw['size'] === 'number' ? { size: raw['size'] } : {};
16091
16135
  if (Array.isArray(raw['tabs'])) {
16092
- return { ...size, tabs: raw['tabs'].flatMap(sanitizeRpcContainerTab) };
16136
+ return { ...size, tabs: raw['tabs'].flatMap((value) => sanitizeRpcContainerTab(value)) };
16093
16137
  }
16094
16138
  for (const kind of ['rows', 'columns']) {
16095
16139
  const declared = raw[kind];
@@ -16116,8 +16160,8 @@ function sanitizeRpcContainerTab(value) {
16116
16160
  return [
16117
16161
  {
16118
16162
  surface: raw['surface'],
16119
- ...(raw['closable'] === false ? { closable: false } : {}),
16120
- ...(raw['active'] === true ? { active: true } : {}),
16163
+ ...(raw['closable'] === false && { closable: false }),
16164
+ ...(raw['active'] === true && { active: true }),
16121
16165
  },
16122
16166
  ];
16123
16167
  }
@@ -16174,11 +16218,14 @@ function sanitizeRpcToastInput(input) {
16174
16218
  id: typeof raw['id'] === 'string' ? raw['id'] : undefined,
16175
16219
  };
16176
16220
  }
16221
+ const NOTIFICATION_KINDS = new Set([
16222
+ 'info',
16223
+ 'success',
16224
+ 'warning',
16225
+ 'error',
16226
+ ]);
16177
16227
  function isNotificationKind(value) {
16178
- return (value === 'info' ||
16179
- value === 'success' ||
16180
- value === 'warning' ||
16181
- value === 'error');
16228
+ return NOTIFICATION_KINDS.has(value);
16182
16229
  }
16183
16230
  function sanitizeRpcMenuItem(item) {
16184
16231
  const raw = (item ?? {});
@@ -16211,29 +16258,267 @@ function sanitizeMenuContext(value) {
16211
16258
  return clean;
16212
16259
  }
16213
16260
 
16214
- const UNCARRIABLE_ARGUMENTS = {
16215
- outcome: 'refused',
16216
- reason: 'invalid-arguments',
16217
- message: 'Arguments must be an object of single values or lists of them; anything else cannot cross the ' +
16218
- 'sandbox boundary as the value it was.',
16219
- };
16220
- function invokeRpcCommand(ctx, id, args) {
16221
- const carried = args === undefined ? undefined : asCommandArguments(args);
16222
- return carried === null
16223
- ? Promise.resolve(UNCARRIABLE_ARGUMENTS)
16224
- : ctx.invokeCommand(String(id), carried);
16261
+ const INPUT_TYPES = ['text', 'date', 'email', 'number', 'password'];
16262
+ function sanitizeOptions(raw) {
16263
+ if (!Array.isArray(raw)) {
16264
+ return [];
16265
+ }
16266
+ return raw
16267
+ .filter((option) => typeof option === 'object' &&
16268
+ option !== null &&
16269
+ typeof option['value'] === 'string' &&
16270
+ typeof option['label'] === 'string')
16271
+ .map((option) => ({ value: option.value, label: option.label }));
16225
16272
  }
16226
-
16227
- /** Multi-provider token: each contribution adds one sandboxed plugin to load. */
16228
- const FRAME_PLUGIN = new InjectionToken('FRAME_PLUGIN');
16229
- function levelOf(plugin) {
16230
- return plugin.level ?? DEFAULT_ISOLATION_LEVEL;
16273
+ function optionalString(value) {
16274
+ return typeof value === 'string' ? value : undefined;
16231
16275
  }
16232
- function signatureOf(plugin) {
16233
- const caps = [...(plugin.capabilities ?? [])].sort().join(',');
16234
- const granted = [...(plugin.granted ?? [])].sort().join(',');
16235
- return `${plugin.entryUrl}|${caps}|${granted}|${plugin.version ?? ''}|${levelOf(plugin)}`;
16276
+ function optionalNumber(value) {
16277
+ return typeof value === 'number' ? value : undefined;
16278
+ }
16279
+ function buildTextControl(value, control) {
16280
+ return {
16281
+ kind: 'text',
16282
+ value,
16283
+ inputType: INPUT_TYPES.find((type) => type === control['inputType']),
16284
+ placeholder: optionalString(control['placeholder']),
16285
+ };
16286
+ }
16287
+ function buildSelectControl(pluginId, value, control) {
16288
+ const options = sanitizeOptions(control['options']);
16289
+ if (options.length === 0) {
16290
+ throw new Error(`Sandbox plugin "${pluginId}": a select control needs at least one { value, label } option.`);
16291
+ }
16292
+ return { kind: 'select', value, options };
16293
+ }
16294
+ function buildSliderControl(value, control) {
16295
+ return {
16296
+ kind: 'slider',
16297
+ value,
16298
+ min: optionalNumber(control['min']),
16299
+ max: optionalNumber(control['max']),
16300
+ step: optionalNumber(control['step']),
16301
+ };
16302
+ }
16303
+ function sanitizeControl(pluginId, raw) {
16304
+ const control = (raw ?? {});
16305
+ const kind = control['kind'];
16306
+ const value = control['value'];
16307
+ if (kind === 'toggle' && typeof value === 'boolean') {
16308
+ return { kind, value };
16309
+ }
16310
+ if (kind === 'text' && typeof value === 'string') {
16311
+ return buildTextControl(value, control);
16312
+ }
16313
+ if (kind === 'select' && typeof value === 'string') {
16314
+ return buildSelectControl(pluginId, value, control);
16315
+ }
16316
+ if (kind === 'slider' && typeof value === 'number') {
16317
+ return buildSliderControl(value, control);
16318
+ }
16319
+ throw new Error(`Sandbox plugin "${pluginId}": a settings control must be toggle/text/select/slider with a matching default 'value'.`);
16320
+ }
16321
+ function sanitizeRow(pluginId, raw) {
16322
+ const row = (raw ?? {});
16323
+ if (typeof row['id'] !== 'string' || row['id'].length === 0) {
16324
+ throw new Error(`Sandbox plugin "${pluginId}": every settings row needs a non-empty 'id'.`);
16325
+ }
16326
+ if (typeof row['label'] !== 'string' || row['label'].length === 0) {
16327
+ throw new Error(`Sandbox plugin "${pluginId}": every settings row needs a non-empty 'label'.`);
16328
+ }
16329
+ return {
16330
+ id: row['id'],
16331
+ label: row['label'],
16332
+ description: typeof row['description'] === 'string' ? row['description'] : undefined,
16333
+ control: sanitizeControl(pluginId, row['control']),
16334
+ };
16335
+ }
16336
+ function sanitizeRpcSettingsSection(pluginId, section) {
16337
+ const raw = (section ?? {});
16338
+ if (typeof raw['id'] !== 'string' || raw['id'].length === 0) {
16339
+ throw new Error(`Sandbox plugin "${pluginId}": registerSettingsSection requires a non-empty 'id'.`);
16340
+ }
16341
+ if (typeof raw['title'] !== 'string' || raw['title'].length === 0) {
16342
+ throw new Error(`Sandbox plugin "${pluginId}": registerSettingsSection requires a non-empty 'title'.`);
16343
+ }
16344
+ if (!Array.isArray(raw['rows']) || raw['rows'].length === 0) {
16345
+ throw new Error(`Sandbox plugin "${pluginId}": registerSettingsSection requires at least one row.`);
16346
+ }
16347
+ return {
16348
+ id: raw['id'],
16349
+ title: raw['title'],
16350
+ order: typeof raw['order'] === 'number' ? raw['order'] : undefined,
16351
+ rows: raw['rows'].map((row) => sanitizeRow(pluginId, row)),
16352
+ };
16353
+ }
16354
+ function defaultsOf(wire) {
16355
+ const defaults = {};
16356
+ for (const row of wire.rows) {
16357
+ defaults[row.id] = row.control.value;
16358
+ }
16359
+ return defaults;
16360
+ }
16361
+ function typedOverlay(defaults, raw) {
16362
+ if (!raw) {
16363
+ return defaults;
16364
+ }
16365
+ try {
16366
+ const parsed = JSON.parse(raw);
16367
+ if (typeof parsed !== 'object' || parsed === null) {
16368
+ return defaults;
16369
+ }
16370
+ const merged = { ...defaults };
16371
+ for (const [key, value] of Object.entries(parsed)) {
16372
+ if (Object.hasOwn(defaults, key) && typeof value === typeof defaults[key]) {
16373
+ merged[key] = value;
16374
+ }
16375
+ }
16376
+ return merged;
16377
+ }
16378
+ catch {
16379
+ return defaults;
16380
+ }
16236
16381
  }
16382
+ function buildFrameSection(deps) {
16383
+ const { pluginId, wire, group, store, sync, notify } = deps;
16384
+ const key = `lw.plugin-settings:${pluginId}:${wire.id}`;
16385
+ const defaults = defaultsOf(wire);
16386
+ const values = signal(typedOverlay(defaults, store.peek?.(key)), /* @ts-ignore */
16387
+ ...(ngDevMode ? [{ debugName: "values" }] : /* istanbul ignore next */ []));
16388
+ const applyStored = (raw) => {
16389
+ values.set(typedOverlay(defaults, raw));
16390
+ notify(wire.id, values());
16391
+ };
16392
+ if (store.peek) {
16393
+ notify(wire.id, values());
16394
+ }
16395
+ else {
16396
+ hydrateAsync(store, key, applyStored);
16397
+ }
16398
+ const disposeSync = sync.register('settings', key, applyStored);
16399
+ const set = (rowId, value) => {
16400
+ values.update((current) => ({ ...current, [rowId]: value }));
16401
+ void store.set(key, JSON.stringify(values()));
16402
+ notify(wire.id, values());
16403
+ };
16404
+ const section = {
16405
+ id: `${pluginId}.${wire.id}`,
16406
+ title: wire.title,
16407
+ group,
16408
+ order: wire.order,
16409
+ rows: wire.rows.map((row) => ({
16410
+ id: `${pluginId}.${wire.id}.${row.id}`,
16411
+ label: row.label,
16412
+ description: row.description,
16413
+ control: hostControl(row, values, set),
16414
+ })),
16415
+ };
16416
+ return { section, disposeSync };
16417
+ }
16418
+ function hostControl(row, values, set) {
16419
+ const control = row.control;
16420
+ switch (control.kind) {
16421
+ case 'toggle': {
16422
+ return {
16423
+ kind: 'toggle',
16424
+ value: () => values()[row.id] === true,
16425
+ set: (value) => set(row.id, value),
16426
+ };
16427
+ }
16428
+ case 'text': {
16429
+ return {
16430
+ kind: 'text',
16431
+ inputType: control.inputType,
16432
+ placeholder: control.placeholder,
16433
+ value: () => String(values()[row.id] ?? ''),
16434
+ set: (value) => set(row.id, value),
16435
+ };
16436
+ }
16437
+ case 'select': {
16438
+ return {
16439
+ kind: 'select',
16440
+ options: control.options,
16441
+ value: () => String(values()[row.id] ?? control.value),
16442
+ set: (value) => set(row.id, value),
16443
+ };
16444
+ }
16445
+ case 'slider': {
16446
+ return {
16447
+ kind: 'slider',
16448
+ min: control.min,
16449
+ max: control.max,
16450
+ step: control.step,
16451
+ value: () => Number(values()[row.id] ?? control.value),
16452
+ set: (value) => set(row.id, value),
16453
+ };
16454
+ }
16455
+ }
16456
+ }
16457
+
16458
+ function frameRpcMethods(deps) {
16459
+ const { pluginId, ctx, origins, watched } = deps;
16460
+ return reportingRefusals({
16461
+ registerSurface: (surface) => {
16462
+ ctx.registerSurface(sanitizeRpcSurface(pluginId, surface, origins));
16463
+ },
16464
+ registerMenuItem: (item) => {
16465
+ ctx.registerMenuItem(sanitizeRpcMenuItem(item));
16466
+ },
16467
+ registerSettingsSection: (section) => {
16468
+ const built = buildFrameSection({
16469
+ pluginId,
16470
+ wire: sanitizeRpcSettingsSection(pluginId, section),
16471
+ group: deps.install.isInstalled(pluginId)
16472
+ ? 'settings.group.community'
16473
+ : 'settings.group.plugins',
16474
+ store: deps.store,
16475
+ sync: deps.sync,
16476
+ notify: (sectionId, values) => deps.notify((remote) => remote.settingsChanged(sectionId, values)),
16477
+ });
16478
+ deps.syncCleanups.push(built.disposeSync);
16479
+ ctx.registerSettingsSection(built.section);
16480
+ },
16481
+ navigateContent: (path) => ctx.navigateContent(path),
16482
+ openContentTab: (input) => {
16483
+ const sanitized = sanitizeRpcTabInput(input);
16484
+ ctx.openContentTab({
16485
+ ...sanitized,
16486
+ onClose: () => deps.notify((remote) => remote.contentTabClosed(sanitized.path)),
16487
+ });
16488
+ },
16489
+ keepContentTab: (path) => ctx.keepContentTab(path),
16490
+ pinContentTab: (path) => ctx.pinContentTab(path),
16491
+ unpinContentTab: (path) => ctx.unpinContentTab(path),
16492
+ closeContentTab: (path) => ctx.closeContentTab(path),
16493
+ revealSurface: (id) => ctx.revealSurface(id),
16494
+ invokeCommand: (id, args) => invokeRpcCommand(ctx, id, args),
16495
+ invocableCommands: () => ctx.invocableCommands(),
16496
+ toast: (input) => ctx.ui.toast(sanitizeRpcToastInput(input)),
16497
+ stateWatch: (key) => deps.watchState(key),
16498
+ stateSet: (key, value) => watched.get(key)?.handle.set(value),
16499
+ stateClear: (key) => watched.get(key)?.handle.clear(),
16500
+ stateUnwatch: (key) => {
16501
+ watched.get(key)?.stop();
16502
+ watched.delete(key);
16503
+ },
16504
+ }, deps.reportRefusal);
16505
+ }
16506
+ function reportingRefusals(methods, report) {
16507
+ const reported = Object.entries(methods).map(([name, method]) => [
16508
+ name,
16509
+ (...args) => {
16510
+ try {
16511
+ return method(...args);
16512
+ }
16513
+ catch (error) {
16514
+ report(error);
16515
+ throw error;
16516
+ }
16517
+ },
16518
+ ]);
16519
+ return Object.fromEntries(reported);
16520
+ }
16521
+
16237
16522
  /**
16238
16523
  * Second {@link PluginRuntime} implementation:
16239
16524
  * runs each plugin in an isolated `<iframe sandbox="allow-scripts">` and hands it `ctx` over **Penpal**
@@ -16291,9 +16576,13 @@ class FramePluginRuntime {
16291
16576
  this.instances.delete(id);
16292
16577
  instance.connection.destroy();
16293
16578
  instance.frame.remove();
16294
- instance.watched.forEach((entry) => entry.stop());
16579
+ for (const entry of instance.watched.values()) {
16580
+ entry.stop();
16581
+ }
16295
16582
  instance.ctx.disposeAll();
16296
- instance.syncCleanups.forEach((cleanup) => cleanup());
16583
+ for (const cleanup of instance.syncCleanups) {
16584
+ cleanup();
16585
+ }
16297
16586
  this.grants.unregister(id);
16298
16587
  this.isolation.unregister(id);
16299
16588
  }
@@ -16305,7 +16594,7 @@ class FramePluginRuntime {
16305
16594
  }
16306
16595
  }
16307
16596
  reconcile(disabled, installed, deployed) {
16308
- const runnable = this.runnablePlugins(installed, deployed);
16597
+ const runnable = runnablePlugins(this.plugins, installed, deployed, this.catalogCap);
16309
16598
  for (const plugin of runnable) {
16310
16599
  this.enablement.register(plugin.id, plugin.name ?? plugin.id);
16311
16600
  const enabled = plugin.provided === true || !disabled.has(plugin.id);
@@ -16323,34 +16612,6 @@ class FramePluginRuntime {
16323
16612
  }
16324
16613
  this.dropUninstalled(runnable);
16325
16614
  }
16326
- runnablePlugins(installed, deployed) {
16327
- const claimed = new Set(this.plugins.map((plugin) => plugin.id));
16328
- const provided = new Set(deployed.map((plugin) => plugin.id));
16329
- const fromCatalog = [];
16330
- for (const plugin of [...deployed, ...installed]) {
16331
- if (claimed.has(plugin.id)) {
16332
- continue;
16333
- }
16334
- const asked = plugin.level ?? DEFAULT_ISOLATION_LEVEL;
16335
- if (exceedsLevel(asked, this.catalogCap)) {
16336
- console.error(`Plugin "${plugin.id}" asks to run ${asked}, which this catalog may not confer ` +
16337
- `(its cap is ${this.catalogCap}). It is not started.`);
16338
- continue;
16339
- }
16340
- claimed.add(plugin.id);
16341
- fromCatalog.push({
16342
- id: plugin.id,
16343
- entryUrl: plugin.entryUrl,
16344
- capabilities: plugin.capabilities,
16345
- name: plugin.name,
16346
- granted: plugin.capabilities ?? [],
16347
- version: plugin.version,
16348
- level: asked,
16349
- provided: provided.has(plugin.id) || undefined,
16350
- });
16351
- }
16352
- return [...this.plugins, ...fromCatalog];
16353
- }
16354
16615
  dropUninstalled(runnable) {
16355
16616
  const known = new Set(runnable.map((plugin) => plugin.id));
16356
16617
  for (const id of this.instances.keys()) {
@@ -16374,7 +16635,19 @@ class FramePluginRuntime {
16374
16635
  const watched = new Map();
16375
16636
  const connection = connect({
16376
16637
  messenger,
16377
- methods: this.reportingRefusals(this.rpcMethods(plugin.id, ctx, syncCleanups, watched, plugin.origins)),
16638
+ methods: frameRpcMethods({
16639
+ pluginId: plugin.id,
16640
+ ctx,
16641
+ origins: plugin.origins,
16642
+ install: this.install,
16643
+ store: this.store,
16644
+ sync: this.sync,
16645
+ syncCleanups,
16646
+ watched,
16647
+ watchState: (key) => this.watchState(plugin.id, ctx, watched, key),
16648
+ notify: (send) => this.notify(plugin.id, send),
16649
+ reportRefusal: (error) => this.refusals.report(error),
16650
+ }),
16378
16651
  });
16379
16652
  this.instances.set(plugin.id, {
16380
16653
  ctx,
@@ -16385,10 +16658,11 @@ class FramePluginRuntime {
16385
16658
  watched,
16386
16659
  });
16387
16660
  connection.promise.catch((error) => {
16388
- if (this.instances.has(plugin.id)) {
16389
- console.error(`Sandbox plugin "${plugin.id}" failed to connect`, error);
16390
- this.deactivate(plugin.id);
16661
+ if (!this.instances.has(plugin.id)) {
16662
+ return;
16391
16663
  }
16664
+ console.error(`Sandbox plugin "${plugin.id}" failed to connect`, error);
16665
+ this.deactivate(plugin.id);
16392
16666
  });
16393
16667
  }
16394
16668
  createFrame(entryUrl, level) {
@@ -16399,71 +16673,9 @@ class FramePluginRuntime {
16399
16673
  frame.setAttribute('aria-hidden', 'true');
16400
16674
  frame.style.display = 'none';
16401
16675
  frame.src = entryUrl;
16402
- document.body.appendChild(frame);
16676
+ document.body.append(frame);
16403
16677
  return frame;
16404
16678
  }
16405
- reportingRefusals(methods) {
16406
- const reported = Object.entries(methods).map(([name, method]) => [
16407
- name,
16408
- (...args) => {
16409
- try {
16410
- return method(...args);
16411
- }
16412
- catch (error) {
16413
- this.refusals.report(error);
16414
- throw error;
16415
- }
16416
- },
16417
- ]);
16418
- return Object.fromEntries(reported);
16419
- }
16420
- rpcMethods(pluginId, ctx, syncCleanups, watched, origins) {
16421
- return {
16422
- registerSurface: (surface) => {
16423
- ctx.registerSurface(sanitizeRpcSurface(pluginId, surface, origins));
16424
- },
16425
- registerMenuItem: (item) => {
16426
- ctx.registerMenuItem(sanitizeRpcMenuItem(item));
16427
- },
16428
- registerSettingsSection: (section) => {
16429
- const built = buildFrameSection({
16430
- pluginId,
16431
- wire: sanitizeRpcSettingsSection(pluginId, section),
16432
- group: this.install.isInstalled(pluginId)
16433
- ? 'settings.group.community'
16434
- : 'settings.group.plugins',
16435
- store: this.store,
16436
- sync: this.sync,
16437
- notify: (sectionId, values) => this.notifySettings(pluginId, sectionId, values),
16438
- });
16439
- syncCleanups.push(built.disposeSync);
16440
- ctx.registerSettingsSection(built.section);
16441
- },
16442
- navigateContent: (path) => ctx.navigateContent(path),
16443
- openContentTab: (input) => {
16444
- const sanitized = sanitizeRpcTabInput(input);
16445
- ctx.openContentTab({
16446
- ...sanitized,
16447
- onClose: () => this.notifyTabClosed(pluginId, sanitized.path),
16448
- });
16449
- },
16450
- keepContentTab: (path) => ctx.keepContentTab(path),
16451
- pinContentTab: (path) => ctx.pinContentTab(path),
16452
- unpinContentTab: (path) => ctx.unpinContentTab(path),
16453
- closeContentTab: (path) => ctx.closeContentTab(path),
16454
- revealSurface: (id) => ctx.revealSurface(id),
16455
- invokeCommand: (id, args) => invokeRpcCommand(ctx, id, args),
16456
- invocableCommands: () => ctx.invocableCommands(),
16457
- toast: (input) => ctx.ui.toast(sanitizeRpcToastInput(input)),
16458
- stateWatch: (key) => this.watchState(pluginId, ctx, watched, key),
16459
- stateSet: (key, value) => watched.get(key)?.handle.set(value),
16460
- stateClear: (key) => watched.get(key)?.handle.clear(),
16461
- stateUnwatch: (key) => {
16462
- watched.get(key)?.stop();
16463
- watched.delete(key);
16464
- },
16465
- };
16466
- }
16467
16679
  watchState(pluginId, ctx, watched, key) {
16468
16680
  if (watched.has(key)) {
16469
16681
  return;
@@ -16472,7 +16684,7 @@ class FramePluginRuntime {
16472
16684
  const ref = effect(() => {
16473
16685
  const value = handle.value();
16474
16686
  const loaded = handle.loaded();
16475
- untracked(() => this.notifyState(pluginId, key, value, loaded));
16687
+ untracked(() => this.notify(pluginId, (remote) => remote.stateChanged(key, value, loaded)));
16476
16688
  }, { ...(ngDevMode ? { debugName: "ref" } : /* istanbul ignore next */ {}), injector: this.injector });
16477
16689
  watched.set(key, {
16478
16690
  handle,
@@ -16482,32 +16694,12 @@ class FramePluginRuntime {
16482
16694
  },
16483
16695
  });
16484
16696
  }
16485
- notifyState(pluginId, key, value, loaded) {
16486
- const instance = this.instances.get(pluginId);
16487
- if (!instance) {
16488
- return;
16489
- }
16490
- void instance.connection.promise
16491
- .then((remote) => remote.stateChanged(key, value, loaded))
16492
- .catch(() => undefined);
16493
- }
16494
- notifyTabClosed(pluginId, path) {
16697
+ notify(pluginId, send) {
16495
16698
  const instance = this.instances.get(pluginId);
16496
16699
  if (!instance) {
16497
16700
  return;
16498
16701
  }
16499
- void instance.connection.promise
16500
- .then((remote) => remote.contentTabClosed(path))
16501
- .catch(() => undefined);
16502
- }
16503
- notifySettings(pluginId, sectionId, values) {
16504
- const instance = this.instances.get(pluginId);
16505
- if (!instance) {
16506
- return;
16507
- }
16508
- void instance.connection.promise
16509
- .then((remote) => remote.settingsChanged(sectionId, values))
16510
- .catch(() => undefined);
16702
+ void instance.connection.promise.then(send).catch(() => undefined);
16511
16703
  }
16512
16704
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.0", ngImport: i0, type: FramePluginRuntime, deps: [], target: i0.ɵɵFactoryTarget.Service });
16513
16705
  static ɵprov = i0.ɵɵngDeclareService({ minVersion: "22.0.0", version: "22.1.0", ngImport: i0, type: FramePluginRuntime });
@@ -16651,12 +16843,12 @@ function availableUpdate(installed, entry) {
16651
16843
  return isNewerVersion(entry.version, installed.version) ? entry : undefined;
16652
16844
  }
16653
16845
  function addedCapabilities(entry, installed) {
16654
- const consented = new Set(installed.capabilities ?? []);
16846
+ const consented = new Set(installed.capabilities);
16655
16847
  return (entry.capabilities ?? []).filter((capability) => !consented.has(capability));
16656
16848
  }
16657
16849
 
16658
16850
  async function confirmUpdate(deps, entry) {
16659
- const installed = deps.installs.find(entry.id);
16851
+ const installed = deps.installs.byId(entry.id);
16660
16852
  if (!installed) {
16661
16853
  return;
16662
16854
  }
@@ -16822,7 +17014,7 @@ class PluginStoreDetail {
16822
17014
  transloco = inject(TranslocoService);
16823
17015
  readme = signal(undefined, /* @ts-ignore */
16824
17016
  ...(ngDevMode ? [{ debugName: "readme" }] : /* istanbul ignore next */ []));
16825
- update = computed(() => availableUpdate(this.installs.find(this.entry().id), this.entry()), /* @ts-ignore */
17017
+ update = computed(() => availableUpdate(this.installs.byId(this.entry().id), this.entry()), /* @ts-ignore */
16826
17018
  ...(ngDevMode ? [{ debugName: "update" }] : /* istanbul ignore next */ []));
16827
17019
  constructor() {
16828
17020
  effect(() => {
@@ -16908,7 +17100,7 @@ class PluginStoreDialog {
16908
17100
  ...(ngDevMode ? [{ debugName: "selectedId" }] : /* istanbul ignore next */ []));
16909
17101
  filtered = computed(() => {
16910
17102
  const list = (this.entries() ?? []).filter((entry) => matchesQuery([entry.name, entry.author, entry.category, entry.description], this.query()));
16911
- return list.sort((a, b) => (b.downloads ?? 0) - (a.downloads ?? 0) || a.name.localeCompare(b.name));
17103
+ return list.toSorted((a, b) => (b.downloads ?? 0) - (a.downloads ?? 0) || a.name.localeCompare(b.name));
16912
17104
  }, /* @ts-ignore */
16913
17105
  ...(ngDevMode ? [{ debugName: "filtered" }] : /* istanbul ignore next */ []));
16914
17106
  selected = computed(() => this.filtered().find((entry) => entry.id === this.selectedId()), /* @ts-ignore */
@@ -16920,7 +17112,7 @@ class PluginStoreDialog {
16920
17112
  void confirmInstall(this.consentDeps, entry);
16921
17113
  }
16922
17114
  hasUpdate(entry) {
16923
- return availableUpdate(this.installs.find(entry.id), entry) !== undefined;
17115
+ return availableUpdate(this.installs.byId(entry.id), entry) !== undefined;
16924
17116
  }
16925
17117
  requestUpdate(entry) {
16926
17118
  void confirmUpdate(this.consentDeps, entry);