@mohamedatia/fly-design-system 2.12.0 → 2.14.0

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.
@@ -1,5 +1,5 @@
1
1
  import * as i0 from '@angular/core';
2
- import { InjectionToken, signal, computed, Injectable, inject, ErrorHandler, PLATFORM_ID, DestroyRef, ChangeDetectionStrategy, Component, Pipe, DOCUMENT, ElementRef, input, output, HostListener, ViewChild, EventEmitter, Output, Input, forwardRef, Injector, viewChild, effect, afterNextRender, ViewEncapsulation, model, Directive, Renderer2 } from '@angular/core';
2
+ import { InjectionToken, signal, computed, Injectable, inject, ErrorHandler, PLATFORM_ID, DestroyRef, ChangeDetectionStrategy, Component, Pipe, ElementRef, input, output, afterNextRender, HostListener, ViewChild, ChangeDetectorRef, forwardRef, DOCUMENT, EventEmitter, Output, Input, Injector, viewChild, effect, ViewEncapsulation, model, Directive, Renderer2 } from '@angular/core';
3
3
  import * as i1$1 from '@angular/common';
4
4
  import { isPlatformBrowser, NgComponentOutlet, CommonModule, DOCUMENT as DOCUMENT$1 } from '@angular/common';
5
5
  import { Router, NavigationEnd } from '@angular/router';
@@ -7,10 +7,82 @@ import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
7
7
  import { of, ReplaySubject, Subject } from 'rxjs';
8
8
  import * as i1 from '@angular/forms';
9
9
  import { FormsModule, NG_VALUE_ACCESSOR, NG_VALIDATORS } from '@angular/forms';
10
+ import { HttpClient, HttpParams, HttpEventType } from '@angular/common/http';
11
+ import { Editor } from '@tiptap/core';
12
+ import StarterKit from '@tiptap/starter-kit';
13
+ import { Markdown } from '@tiptap/markdown';
14
+ import TaskList from '@tiptap/extension-task-list';
15
+ import TaskItem from '@tiptap/extension-task-item';
16
+ import { TiptapEditorDirective } from 'ngx-tiptap';
10
17
  import { switchMap, debounceTime, distinctUntilChanged, filter } from 'rxjs/operators';
11
- import { HttpClient, HttpEventType } from '@angular/common/http';
12
18
  import Cropper from 'cropperjs';
13
19
 
20
+ /**
21
+ * Federation singleton self-check (side-effecting module).
22
+ *
23
+ * Invariant: the design-system MUST resolve to ONE shared instance per browser
24
+ * window. The FlyOS shell shares it from workspace SOURCE (federation.config.js
25
+ * `sharedMappings` + `features.mappingVersion`, advertising the real version),
26
+ * and every federated remote (Circles, …) binds to that single instance as a
27
+ * `singleton`. If a remote instead bundles its OWN copy — `requiredVersion`
28
+ * unsatisfied, `strictVersion: true`, or the host advertising an empty version
29
+ * because `mappingVersion` is off — the platform silently forks. Separate
30
+ * `providedIn: 'root'` singletons then mean:
31
+ * - cross-app deep links break: `FlyRemoteRouter` writes the canonical
32
+ * `?app=&route=` URL on the remote's own instance, so a reload never reaches
33
+ * the shell's launch pipeline (see skills/cross-app-deep-linking.md);
34
+ * - the `/lookup` + slash-command registries diverge, so entities registered
35
+ * in the remote never reach the shell's agent input.
36
+ *
37
+ * Both bugs are invisible until a user trips over them. This check makes the
38
+ * fork LOUD at load time: each DS module evaluation appends its compiled-in
39
+ * version to a `globalThis` registry, and a second entry in the same window is
40
+ * the fork signal. The healthy case (one shared instance) stays silent.
41
+ *
42
+ * Why a plain `globalThis` key and not DI: a forked DS means a *separate* module
43
+ * graph, so any DI token / module-scoped variable is exactly what's duplicated.
44
+ * Only a JS global is shared across module instances in one window — the same
45
+ * reason `__FLYOS_SHELL__` and the pending-launches registry use globals.
46
+ *
47
+ * See skills/business-app-angular-remote.md § "Design-system singleton across
48
+ * the federation boundary".
49
+ */
50
+ // Keep in sync with projects/design-system/package.json "version". Used only for
51
+ // the diagnostic message — the duplicate-instance detection is version-agnostic.
52
+ const FLY_DS_VERSION = '2.12.0';
53
+ const FLY_DS_REGISTRY_KEY = '__FLY_DS_INSTANCES__';
54
+ /**
55
+ * Records this design-system instance on the shared `scope` and returns the
56
+ * running count of instances seen in this window. Emits a single loud
57
+ * `console.error` the moment the count exceeds 1 (the fork signal). Pure aside
58
+ * from the registry mutation + console — callable with a fake scope in tests.
59
+ */
60
+ function registerDesignSystemInstance(scope, version) {
61
+ const registry = (scope[FLY_DS_REGISTRY_KEY] ??= { versions: [] });
62
+ registry.versions.push(version);
63
+ if (registry.versions.length > 1) {
64
+ // eslint-disable-next-line no-console
65
+ console.error(`[fly-design-system] Federation singleton FORKED: ${registry.versions.length} ` +
66
+ `design-system instances loaded in one window (versions: ${registry.versions.join(', ')}). ` +
67
+ `A federated remote is using its own bundled copy instead of the shell's shared ` +
68
+ `singleton — cross-app deep links and /lookup registration WILL break. Fix: align the ` +
69
+ `remote's federation \`requiredVersion\` with the host's advertised version and keep ` +
70
+ `\`features.mappingVersion: true\` in the shell's federation.config.js.`);
71
+ }
72
+ return registry.versions.length;
73
+ }
74
+ // Run once when this module is evaluated — except under the Vitest runner, which
75
+ // re-evaluates module graphs per test file against a shared Node global and would
76
+ // otherwise accumulate phantom "instances". The fork this detects only occurs in a
77
+ // real federated browser runtime, where each bundle evaluates the module exactly once.
78
+ // Reach `process.env.VITEST` via globalThis rather than the bare `process`
79
+ // identifier — the latter needs @types/node, which the production library build
80
+ // (tsconfig.lib.prod.json) deliberately doesn't include, and would fail TS2591.
81
+ const underTest = !!globalThis.process?.env?.['VITEST'];
82
+ if (!underTest) {
83
+ registerDesignSystemInstance(globalThis, FLY_DS_VERSION);
84
+ }
85
+
14
86
  const WINDOW_DATA = new InjectionToken('WINDOW_DATA');
15
87
  /**
16
88
  * Per-window injection token carrying the active `LaunchContext` as a signal.
@@ -196,6 +268,144 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.5", ngImpor
196
268
  args: [{ providedIn: 'root' }]
197
269
  }] });
198
270
 
271
+ /**
272
+ * Baseline UI strings for design-system components (markdown editor, entity
273
+ * lookup, …) in the four platform locales (`en`, `ar`, `fr`, `ur`).
274
+ *
275
+ * These are registered as the **lowest-priority** layer of {@link I18nService}
276
+ * (below the shell layer and any remote bundle), so DS components render real,
277
+ * localized labels even in a **standalone** consumer — a Business App running
278
+ * outside the desktop shell that never called `setShellTranslations()` — which
279
+ * would otherwise see raw keys (`common.label.bold`) as the pipe's fallback.
280
+ *
281
+ * Precedence: any consumer-supplied key with the same name still wins, so this
282
+ * is a safe default that an integrator can override without coordination.
283
+ *
284
+ * Scope: only the keys the **shippable** DS components reference today
285
+ * (`common.*` toolbar/link labels + `agent.lookup.*`). When a new DS component
286
+ * starts using `| translate`, add its keys here so it stays self-sufficient.
287
+ * Values are copied verbatim from the shell's `public/locale/*.json` — keep
288
+ * them in sync if either side changes.
289
+ */
290
+ const DS_BASELINE_LOCALES = {
291
+ en: {
292
+ 'common.action.cancel': 'Cancel',
293
+ 'common.label.apply': 'Apply',
294
+ 'common.label.formatting_toolbar': 'Formatting toolbar',
295
+ 'common.label.https': 'https://…',
296
+ 'common.label.insert_entity_link': 'Insert in-app link',
297
+ 'common.label.link': 'Link',
298
+ 'common.label.blockquote': 'Quote',
299
+ 'common.label.bold': 'Bold',
300
+ 'common.label.bullet_list': 'Bulleted list',
301
+ 'common.label.code_block': 'Code block',
302
+ 'common.label.heading1': 'Heading 1',
303
+ 'common.label.heading2': 'Heading 2',
304
+ 'common.label.heading3': 'Heading 3',
305
+ 'common.label.horizontal_rule': 'Divider',
306
+ 'common.label.inline_code': 'Inline code',
307
+ 'common.label.italic': 'Italic',
308
+ 'common.label.numbered_list': 'Numbered list',
309
+ 'common.label.redo': 'Redo',
310
+ 'common.label.strikethrough': 'Strikethrough',
311
+ 'common.label.task_list': 'Task list',
312
+ 'common.label.underline': 'Underline',
313
+ 'common.label.undo': 'Undo',
314
+ 'agent.lookup.back_aria': 'Back to entity types',
315
+ 'agent.lookup.dialog_aria': 'Entity lookup',
316
+ 'agent.lookup.no_entities': 'No items available to reference.',
317
+ 'agent.lookup.no_results': 'No matches found.',
318
+ 'agent.lookup.searching': 'Searching…',
319
+ },
320
+ ar: {
321
+ 'common.action.cancel': 'إلغاء',
322
+ 'common.label.apply': 'تطبيق',
323
+ 'common.label.formatting_toolbar': 'شريط أدوات التنسيق',
324
+ 'common.label.https': 'https://…',
325
+ 'common.label.insert_entity_link': 'إدراج رابط داخل التطبيق',
326
+ 'common.label.link': 'رابط',
327
+ 'common.label.blockquote': 'اقتباس',
328
+ 'common.label.bold': 'غامق',
329
+ 'common.label.bullet_list': 'قائمة نقطية',
330
+ 'common.label.code_block': 'كتلة شيفرة',
331
+ 'common.label.heading1': 'عنوان 1',
332
+ 'common.label.heading2': 'عنوان 2',
333
+ 'common.label.heading3': 'عنوان 3',
334
+ 'common.label.horizontal_rule': 'فاصل',
335
+ 'common.label.inline_code': 'كود مضمّن',
336
+ 'common.label.italic': 'مائل',
337
+ 'common.label.numbered_list': 'قائمة مرقَّمة',
338
+ 'common.label.redo': 'إعادة',
339
+ 'common.label.strikethrough': 'يتوسطه خط',
340
+ 'common.label.task_list': 'قائمة مهام',
341
+ 'common.label.underline': 'تسطير',
342
+ 'common.label.undo': 'تراجع',
343
+ 'agent.lookup.back_aria': 'العودة إلى أنواع العناصر',
344
+ 'agent.lookup.dialog_aria': 'البحث عن عنصر',
345
+ 'agent.lookup.no_entities': 'لا توجد عناصر متاحة للإشارة إليها.',
346
+ 'agent.lookup.no_results': 'لا توجد نتائج مطابقة.',
347
+ 'agent.lookup.searching': 'جارٍ البحث…',
348
+ },
349
+ fr: {
350
+ 'common.action.cancel': 'Annuler',
351
+ 'common.label.apply': 'Appliquer',
352
+ 'common.label.formatting_toolbar': "Barre d'outils de mise en forme",
353
+ 'common.label.https': 'https://…',
354
+ 'common.label.insert_entity_link': 'Insérer un lien interne',
355
+ 'common.label.link': 'Lien',
356
+ 'common.label.blockquote': 'Citation',
357
+ 'common.label.bold': 'Gras',
358
+ 'common.label.bullet_list': 'Liste à puces',
359
+ 'common.label.code_block': 'Bloc de code',
360
+ 'common.label.heading1': 'Titre 1',
361
+ 'common.label.heading2': 'Titre 2',
362
+ 'common.label.heading3': 'Titre 3',
363
+ 'common.label.horizontal_rule': 'Séparateur',
364
+ 'common.label.inline_code': 'Code en ligne',
365
+ 'common.label.italic': 'Italique',
366
+ 'common.label.numbered_list': 'Liste numérotée',
367
+ 'common.label.redo': 'Rétablir',
368
+ 'common.label.strikethrough': 'Barré',
369
+ 'common.label.task_list': 'Liste de tâches',
370
+ 'common.label.underline': 'Souligner',
371
+ 'common.label.undo': 'Annuler',
372
+ 'agent.lookup.back_aria': "Retour aux types d'entité",
373
+ 'agent.lookup.dialog_aria': "Recherche d'entité",
374
+ 'agent.lookup.no_entities': 'Aucun élément disponible à référencer.',
375
+ 'agent.lookup.no_results': 'Aucun résultat trouvé.',
376
+ 'agent.lookup.searching': 'Recherche en cours…',
377
+ },
378
+ ur: {
379
+ 'common.action.cancel': 'منسوخ کریں',
380
+ 'common.label.apply': 'لاگو کریں',
381
+ 'common.label.formatting_toolbar': 'فارمیٹنگ ٹول بار',
382
+ 'common.label.https': 'https://…',
383
+ 'common.label.insert_entity_link': 'ان-ایپ لنک داخل کریں',
384
+ 'common.label.link': 'لنک',
385
+ 'common.label.blockquote': 'اقتباس',
386
+ 'common.label.bold': 'موٹا',
387
+ 'common.label.bullet_list': 'بلٹ فہرست',
388
+ 'common.label.code_block': 'کوڈ بلاک',
389
+ 'common.label.heading1': 'سرخی 1',
390
+ 'common.label.heading2': 'سرخی 2',
391
+ 'common.label.heading3': 'سرخی 3',
392
+ 'common.label.horizontal_rule': 'تقسیم لائن',
393
+ 'common.label.inline_code': 'ان لائن کوڈ',
394
+ 'common.label.italic': 'ترچھا',
395
+ 'common.label.numbered_list': 'نمبر والی فہرست',
396
+ 'common.label.redo': 'دوبارہ کریں',
397
+ 'common.label.strikethrough': 'خط زدہ',
398
+ 'common.label.task_list': 'ٹاسک فہرست',
399
+ 'common.label.underline': 'خط کشید',
400
+ 'common.label.undo': 'واپس کریں',
401
+ 'agent.lookup.back_aria': 'عناصر کی اقسام پر واپس جائیں',
402
+ 'agent.lookup.dialog_aria': 'عنصر کی تلاش',
403
+ 'agent.lookup.no_entities': 'حوالہ دینے کے لیے کوئی آئٹم دستیاب نہیں۔',
404
+ 'agent.lookup.no_results': 'کوئی مماثل نتیجہ نہیں ملا۔',
405
+ 'agent.lookup.searching': 'تلاش جاری ہے…',
406
+ },
407
+ };
408
+
199
409
  /** Locales that use RTL layout for `dir` and DS `isRtl` / `direction`. */
200
410
  const RTL_LOCALE_SET = new Set(['ar', 'ur']);
201
411
  function isRtlLocale(lang) {
@@ -230,8 +440,12 @@ function normalizeLocaleJson(raw) {
230
440
  /**
231
441
  * Shared I18nService for the shell and Business Apps.
232
442
  *
233
- * **Merge order** (later keys win): shell layer → remote bundles in registration order.
443
+ * **Merge order** (later keys win): DS baseline → shell layer → remote bundles
444
+ * in registration order.
234
445
  *
446
+ * - Baseline: {@link DS_BASELINE_LOCALES} — built-in strings for DS components
447
+ * (markdown editor, entity lookup) so they render localized labels even in a
448
+ * standalone consumer that never populated the shell layer. Always overridable.
235
449
  * - Shell: `setShellTranslations()` after loading `locale/{lang}.json` and API overrides.
236
450
  * - Remotes: `loadBundle()` per manifest `localeBaseUrl`.
237
451
  */
@@ -242,11 +456,14 @@ class I18nService {
242
456
  _bundleOrder = signal([], ...(ngDevMode ? [{ debugName: "_bundleOrder" }] : /* istanbul ignore next */ []));
243
457
  _locale = signal('en', ...(ngDevMode ? [{ debugName: "_locale" }] : /* istanbul ignore next */ []));
244
458
  _version = signal(0, ...(ngDevMode ? [{ debugName: "_version" }] : /* istanbul ignore next */ []));
459
+ /** Built-in DS strings for the active locale (falls back to `en` for any
460
+ * locale we don't ship). Lowest-priority layer — always overridable. */
461
+ _baseline = computed(() => DS_BASELINE_LOCALES[this._locale()] ?? DS_BASELINE_LOCALES['en'] ?? {}, ...(ngDevMode ? [{ debugName: "_baseline" }] : /* istanbul ignore next */ []));
245
462
  _merged = computed(() => {
246
463
  const shell = this._shell();
247
464
  const order = this._bundleOrder();
248
465
  const bundles = this._bundles();
249
- let out = { ...shell };
466
+ let out = { ...this._baseline(), ...shell };
250
467
  for (const id of order) {
251
468
  const b = bundles[id];
252
469
  if (b)
@@ -738,16 +955,51 @@ class FlyRemoteRouter {
738
955
  * In embedded mode: push a real browser history entry (so back/forward work)
739
956
  * and update the internal signal synchronously.
740
957
  *
741
- * The state object carries `flyRemoteUrl` so the popstate listener can
742
- * restore the exact URL without relying on window.location.pathname (which
743
- * could be the shell's route, not the remote's logical URL).
958
+ * The browser address bar is written as the shell's **canonical deep-link
959
+ * form** `<shell-path>?app=<appId>&route=<remote-url>` NOT the bare remote
960
+ * URL. This is what makes a hard reload work: on reload the shell's
961
+ * `DeepLinkService` captures `?app=&route=` in its APP_INITIALIZER, re-opens
962
+ * this app, and replays the route through the pending-launch pipeline
963
+ * (`ShellLauncherService.launch` → `FLYOS_LAUNCH_EVENT` / pending registry →
964
+ * the remote's root applies `ctx.route`). Writing the bare remote URL (e.g.
965
+ * `/trends/abc`) instead would leave a host-unroutable path in the bar — on
966
+ * reload the shell router falls through to its `**` wildcard, redirects to
967
+ * `/desktop`, and the deep link is silently lost. See
968
+ * skills/cross-app-deep-linking.md.
969
+ *
970
+ * `flyRemoteUrl` is still stashed in the history state so the popstate
971
+ * listener restores the exact remote URL for back/forward without re-parsing
972
+ * the query string.
744
973
  */
745
974
  _pushEmbedded(url) {
746
975
  if (typeof history !== 'undefined') {
747
- history.pushState({ flyRemoteUrl: url }, '', url);
976
+ history.pushState({ flyRemoteUrl: url }, '', this._buildEmbeddedHistoryUrl(url));
748
977
  }
749
978
  this._url.set(url);
750
979
  }
980
+ /**
981
+ * Build the browser-address-bar URL for an embedded navigation: the shell's
982
+ * current path plus the canonical `?app=&route=` deep-link query (the same
983
+ * contract `DeepLinkService.captureFromCurrentUrl` parses on cold load).
984
+ *
985
+ * The path is left untouched — only the query string carries the remote's
986
+ * logical route — so the shell's own Angular Router (anchored at e.g.
987
+ * `/desktop`) is never handed a path it cannot match.
988
+ *
989
+ * Fallback: when `appId` is unknown (WINDOW_DATA not injected — the Native
990
+ * Federation token-split case where only `__FLYOS_SHELL__` proves embedding)
991
+ * we cannot build a shareable link, so we return the shell path query-less.
992
+ * Back/forward still work via the `flyRemoteUrl` history state; only
993
+ * reload-restore is unavailable — no regression over leaving the bar as-is.
994
+ */
995
+ _buildEmbeddedHistoryUrl(remoteUrl) {
996
+ const shellPath = (typeof window !== 'undefined' && window.location.pathname) || this.basePath || '/';
997
+ const appId = this.windowData?.appId;
998
+ if (!appId)
999
+ return shellPath;
1000
+ const query = new URLSearchParams({ app: appId, route: remoteUrl });
1001
+ return `${shellPath}?${query.toString()}`;
1002
+ }
751
1003
  buildUrl(commands) {
752
1004
  const parts = commands
753
1005
  .map(c => (c == null ? '' : String(c)))
@@ -763,6 +1015,123 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.5", ngImpor
763
1015
  args: [{ providedIn: 'root' }]
764
1016
  }], ctorParameters: () => [] });
765
1017
 
1018
+ /**
1019
+ * Shared cross-bundle store key + sync event. **Public contract** — a remote that
1020
+ * cannot consume a DS new enough to back {@link FlyRemoteContextService.publish} on
1021
+ * `globalThis` itself may write this slot directly (same shape, keyed by appId) and
1022
+ * dispatch {@link FLY_REMOTE_CONTEXT_EVENT} to notify the shell. Keep these literals
1023
+ * stable; they are an integration boundary, not an implementation detail.
1024
+ */
1025
+ const FLY_REMOTE_CONTEXT_STORE_KEY = '__flyRemoteContext__';
1026
+ const FLY_REMOTE_CONTEXT_EVENT = 'fly:remote-context';
1027
+ /** The single cross-bundle store, lazily created on `globalThis`. */
1028
+ function store() {
1029
+ const g = globalThis;
1030
+ return (g[FLY_REMOTE_CONTEXT_STORE_KEY] ??= {});
1031
+ }
1032
+ /** Notify every federated copy of the service that the store changed. */
1033
+ function emitSync() {
1034
+ const g = globalThis;
1035
+ try {
1036
+ g.dispatchEvent?.(new Event(FLY_REMOTE_CONTEXT_EVENT));
1037
+ }
1038
+ catch {
1039
+ /* non-DOM realm (SSR / unit env without Event) — readers fall back to a direct read */
1040
+ }
1041
+ }
1042
+ function toMap(s) {
1043
+ return new Map(Object.entries(s));
1044
+ }
1045
+ /**
1046
+ * Remote → shell route/context channel.
1047
+ *
1048
+ * Why this exists
1049
+ * ---------------
1050
+ * A federated remote (e.g. Circles) renders inside a desktop-shell window and
1051
+ * keeps its own internal Angular route (`/trends/:id`). The shell cannot see that
1052
+ * route: the remote provides {@link FlyRemoteRouter} at its **component** injector
1053
+ * (so it can read per-window `WINDOW_DATA`), which makes the route state invisible
1054
+ * to the host. Shell-side features that need "what is the user looking at right
1055
+ * now" — most importantly Category-B slash commands whose `contextBindings` include
1056
+ * a `lookup`/`route` entity id — had no way to resolve the current entity id, and
1057
+ * degraded to "ask the user".
1058
+ *
1059
+ * The channel — why `globalThis`, not the DI singleton
1060
+ * ----------------------------------------------------
1061
+ * This service is `providedIn: 'root'`, but a root instance is **NOT** reliably
1062
+ * shared across the federation boundary. The shell builds the design system from
1063
+ * workspace SOURCE while remotes consume the PUBLISHED npm package; Native
1064
+ * Federation only collapses those two physical builds into one runtime instance if
1065
+ * version negotiation succeeds perfectly (matching advertised `mappingVersion`,
1066
+ * `singleton`, compatible ranges). That negotiation has silently split before —
1067
+ * giving the shell and a remote *separate* `providedIn:'root'` instances, so a
1068
+ * value published on one was invisible to the other.
1069
+ *
1070
+ * `globalThis` is the one substrate guaranteed shared across every federated bundle
1071
+ * in the same realm (there are no iframes), so the channel stores its state there
1072
+ * (see {@link FLY_REMOTE_CONTEXT_STORE_KEY}). {@link context} / {@link param} read
1073
+ * it directly — split-proof, synchronous, no injector-token gymnastics. A per-
1074
+ * instance signal mirrors the store for reactive consumers and is re-synced from a
1075
+ * `globalThis` event whenever any copy of the service (or a remote writing the slot
1076
+ * directly) mutates it. The same pattern already backs the shell's app-launch
1077
+ * context bridge, so this is an established boundary, not a new hack.
1078
+ *
1079
+ * Contract
1080
+ * --------
1081
+ * - Remote: call {@link publish} whenever its embedded route changes, and
1082
+ * {@link clear} on teardown (window close / component destroy).
1083
+ * - Shell: call {@link context} (or {@link param}) for an app id to resolve bindings.
1084
+ *
1085
+ * Keyed by `appId` (last publisher wins) — a single live window per app is the v1
1086
+ * assumption; a windowId key is the natural extension if multi-window-per-app
1087
+ * dispatch is ever needed.
1088
+ */
1089
+ class FlyRemoteContextService {
1090
+ _byApp = signal(toMap(store()), ...(ngDevMode ? [{ debugName: "_byApp" }] : /* istanbul ignore next */ []));
1091
+ /** All currently-published contexts, keyed by app id. Reactive. */
1092
+ contexts = this._byApp.asReadonly();
1093
+ constructor() {
1094
+ // Keep this instance's reactive mirror current when ANOTHER federated copy of
1095
+ // the service — or a remote writing the store slot directly — mutates it.
1096
+ const g = globalThis;
1097
+ g.addEventListener?.(FLY_REMOTE_CONTEXT_EVENT, () => this._byApp.set(toMap(store())));
1098
+ }
1099
+ /** Publish (or replace) the active route context for an app. */
1100
+ publish(ctx) {
1101
+ store()[ctx.appId] = ctx;
1102
+ this._byApp.set(toMap(store()));
1103
+ emitSync();
1104
+ }
1105
+ /** Drop an app's context (remote unmounted / window closed). No-op if absent. */
1106
+ clear(appId) {
1107
+ const s = store();
1108
+ if (!(appId in s))
1109
+ return;
1110
+ delete s[appId];
1111
+ this._byApp.set(toMap(s));
1112
+ emitSync();
1113
+ }
1114
+ /** Current context for an app, or `null` when nothing is published. */
1115
+ context(appId) {
1116
+ return store()[appId] ?? null;
1117
+ }
1118
+ /**
1119
+ * Resolve a single route param for an app, e.g. `param('circles', 'id')`.
1120
+ * Returns `null` when the app has no published context or the param is absent —
1121
+ * the caller then degrades (the skill asks for the missing id).
1122
+ */
1123
+ param(appId, path) {
1124
+ const value = store()[appId]?.params[path];
1125
+ return value != null && value !== '' ? value : null;
1126
+ }
1127
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.5", ngImport: i0, type: FlyRemoteContextService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
1128
+ static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.5", ngImport: i0, type: FlyRemoteContextService, providedIn: 'root' });
1129
+ }
1130
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.5", ngImport: i0, type: FlyRemoteContextService, decorators: [{
1131
+ type: Injectable,
1132
+ args: [{ providedIn: 'root' }]
1133
+ }], ctorParameters: () => [] });
1134
+
766
1135
  const APP_LOOKUP = new InjectionToken('APP_LOOKUP');
767
1136
  /**
768
1137
  * Canonicalizes raw source-app identifiers and resolves display names.
@@ -1410,6 +1779,891 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.5", ngImpor
1410
1779
  type: Injectable
1411
1780
  }], ctorParameters: () => [] });
1412
1781
 
1782
+ /**
1783
+ * Singleton registry of entity lookups offered by the `/lookup` typeahead.
1784
+ *
1785
+ * Mirrors {@link AgentCommandRegistry}'s federation-singleton story
1786
+ * (`sharedMappings: ['@mohamedatia/fly-design-system']`), id-collision
1787
+ * "latest wins" contract, and disposable-handle ergonomics. OS-core entities
1788
+ * (note / calendar event / file) register once at shell bootstrap via
1789
+ * `CORE_APP_LOOKUPS`; federated remotes (Circles: scenario / trend / signal)
1790
+ * register at remote-component boot and dispose on window close.
1791
+ *
1792
+ * **Scope semantics diverge from commands.** Commands HIDE when their `appId`
1793
+ * isn't in `liveAppIds`. Lookups DO NOT — they're always offered, and
1794
+ * `{appId}` is just a *priority hint* that bumps that lookup to the top of
1795
+ * the entity picker when the app is live. See {@link LookupRegistration.scope}
1796
+ * for the rationale.
1797
+ *
1798
+ * Storage is a signal store keyed on {@link LookupRegistration.entity}. Because
1799
+ * `entity` is the collision key, an app re-registering the same entity replaces
1800
+ * the prior descriptor; a stale handle's `dispose()` then no-ops.
1801
+ */
1802
+ class AgentLookupRegistry {
1803
+ _lookups = signal([], ...(ngDevMode ? [{ debugName: "_lookups" }] : /* istanbul ignore next */ []));
1804
+ /** All currently-registered lookups, in insertion order. */
1805
+ all = this._lookups.asReadonly();
1806
+ /**
1807
+ * All registered lookups, sorted by affinity to `liveAppIds`:
1808
+ *
1809
+ * 1. Lookups whose `scope.appId` is in the live app set (in registration
1810
+ * order within that bucket).
1811
+ * 2. Then everything else — `'global'` lookups AND scoped lookups whose
1812
+ * app isn't currently live — in registration order.
1813
+ *
1814
+ * Recomputes when either the registry or `liveAppIds` changes. Pass a
1815
+ * `Signal<ReadonlySet<string>>` from the host's app-registry for reactive
1816
+ * re-sorting. **Always returns the full registry** — see the type doc on
1817
+ * {@link LookupRegistration.scope} for why this differs from
1818
+ * {@link AgentCommandRegistry.visible}.
1819
+ */
1820
+ visible(liveAppIds) {
1821
+ const liveSignal = isSignal$1(liveAppIds)
1822
+ ? liveAppIds
1823
+ : signal(liveAppIds).asReadonly();
1824
+ return computed(() => {
1825
+ const live = liveSignal();
1826
+ const all = this._lookups();
1827
+ const prioritized = [];
1828
+ const rest = [];
1829
+ for (const l of all) {
1830
+ if (l.scope !== 'global' && live.has(l.scope.appId)) {
1831
+ prioritized.push(l);
1832
+ }
1833
+ else {
1834
+ rest.push(l);
1835
+ }
1836
+ }
1837
+ return prioritized.length === 0 ? all : [...prioritized, ...rest];
1838
+ });
1839
+ }
1840
+ /**
1841
+ * Register one lookup. Returns a handle whose `dispose()` removes the row by
1842
+ * `entity`. A later re-registration of the same entity makes the original
1843
+ * handle's `dispose()` a no-op (the newer registration owns the row).
1844
+ */
1845
+ register(lookup) {
1846
+ const generation = ++this._generation;
1847
+ this._lookups.update((rows) => [
1848
+ ...rows.filter((r) => r.entity !== lookup.entity),
1849
+ lookup,
1850
+ ]);
1851
+ this._owners.set(lookup.entity, generation);
1852
+ return {
1853
+ dispose: () => {
1854
+ if (this._owners.get(lookup.entity) === generation) {
1855
+ this._owners.delete(lookup.entity);
1856
+ this._lookups.update((rows) => rows.filter((r) => r.entity !== lookup.entity));
1857
+ }
1858
+ },
1859
+ };
1860
+ }
1861
+ /**
1862
+ * Bulk register. Rolls back on a duplicate entity WITHIN the input batch
1863
+ * (throws before any row lands). Cross-batch duplicates against existing rows
1864
+ * follow the standard "latest wins" rule and do NOT trigger rollback.
1865
+ */
1866
+ registerAll(lookups) {
1867
+ const seen = new Set();
1868
+ for (const l of lookups) {
1869
+ if (seen.has(l.entity)) {
1870
+ throw new Error(`AgentLookupRegistry.registerAll: duplicate entity "${l.entity}" in batch`);
1871
+ }
1872
+ seen.add(l.entity);
1873
+ }
1874
+ const handles = lookups.map((l) => this.register(l));
1875
+ let disposed = false;
1876
+ return {
1877
+ dispose: () => {
1878
+ if (disposed)
1879
+ return;
1880
+ disposed = true;
1881
+ for (const h of handles)
1882
+ h.dispose();
1883
+ },
1884
+ };
1885
+ }
1886
+ /**
1887
+ * Resolve a deep-link anchor to a concrete launch target.
1888
+ *
1889
+ * `kind` is the dotted `<appId>.<entity>` token the agents backend emits
1890
+ * inside `flyos:<kind>/<id>` chat-answer anchors — the same entity-kind
1891
+ * vocabulary as drag-payload kinds and `ref` parts. Returns
1892
+ * `{ appId, route }` when a registered lookup for that `(appId, entity)`
1893
+ * pair carries a {@link LookupDescriptor.deepLinkRoute} template; `null`
1894
+ * otherwise (unknown entity, app mismatch, or no template — e.g. the
1895
+ * owning app isn't installed) so the caller renders plain text rather than
1896
+ * a dead link.
1897
+ *
1898
+ * `appId` and `entity` are both dot-free by their own grammars, so the
1899
+ * FIRST dot is the unambiguous split point; a dotless `kind` can't carry an
1900
+ * app and never resolves. The template's single `{id}` placeholder is
1901
+ * substituted URL-encoded.
1902
+ */
1903
+ resolveDeepLink(kind, id) {
1904
+ if (!kind)
1905
+ return null;
1906
+ const dot = kind.indexOf('.');
1907
+ if (dot <= 0 || dot >= kind.length - 1)
1908
+ return null;
1909
+ const appId = kind.slice(0, dot);
1910
+ const entity = kind.slice(dot + 1);
1911
+ const target = this.resolveDeepLinkForEntity(entity, id);
1912
+ // App must match the one named in the anchor — a `flyos:` href carries
1913
+ // the owning app explicitly, so a stale/wrong app prefix must NOT resolve
1914
+ // even when the bare entity name happens to be registered elsewhere.
1915
+ return target && target.appId === appId ? target : null;
1916
+ }
1917
+ /**
1918
+ * Resolve a deep-link target from a bare `(entity, id)` pair — the shape a
1919
+ * `/lookup` ref carries (it has no `<appId>.<entity>` kind token; the owning
1920
+ * app is implicit in the registered descriptor). `entity` is the registry's
1921
+ * unique storage key, so it identifies the descriptor unambiguously without
1922
+ * an app prefix.
1923
+ *
1924
+ * Returns `{ appId, route }` (the descriptor's {@link LookupDescriptor.appId}
1925
+ * / affinity `scope.appId` as the owner, `{id}` substituted URL-encoded) when
1926
+ * a matching descriptor carries a {@link LookupDescriptor.deepLinkRoute};
1927
+ * `null` otherwise (unknown entity, no template, or the owning app has since
1928
+ * unregistered) so callers render plain text rather than a dead link — the
1929
+ * same graceful-degrade contract as {@link resolveDeepLink}.
1930
+ */
1931
+ resolveDeepLinkForEntity(entity, id) {
1932
+ if (!entity || !id)
1933
+ return null;
1934
+ const reg = this._lookups().find((r) => r.entity === entity && r.deepLinkRoute);
1935
+ const appId = reg ? ownerAppId(reg) : undefined;
1936
+ if (!reg?.deepLinkRoute || !appId)
1937
+ return null;
1938
+ return { appId, route: reg.deepLinkRoute.replace('{id}', encodeURIComponent(id)) };
1939
+ }
1940
+ /** Tear down by entity. Idempotent. */
1941
+ unregister(entity) {
1942
+ if (this._owners.delete(entity)) {
1943
+ this._lookups.update((rows) => rows.filter((r) => r.entity !== entity));
1944
+ }
1945
+ }
1946
+ /** Monotonic counter; identifies which registration call currently owns each entity. */
1947
+ _generation = 0;
1948
+ /** entity → generation. Lets a stale handle's `dispose()` no-op after replacement. */
1949
+ _owners = new Map();
1950
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.5", ngImport: i0, type: AgentLookupRegistry, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
1951
+ static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.5", ngImport: i0, type: AgentLookupRegistry, providedIn: 'root' });
1952
+ }
1953
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.5", ngImport: i0, type: AgentLookupRegistry, decorators: [{
1954
+ type: Injectable,
1955
+ args: [{ providedIn: 'root' }]
1956
+ }] });
1957
+ /** `isSignal` shim — narrows to either `Signal<T>` or a plain value. */
1958
+ function isSignal$1(v) {
1959
+ return typeof v === 'function';
1960
+ }
1961
+ /**
1962
+ * The app that owns a registration, for deep-link resolution. Prefers the
1963
+ * stamped {@link LookupDescriptor.appId} (set by `RemoteManifestService` /
1964
+ * `CORE_APP_LOOKUPS`), falling back to the affinity `scope.appId` so a raw
1965
+ * manifest descriptor still resolves even before the host stamps it.
1966
+ * Returns `undefined` for `'global'`-scoped lookups with no stamped appId.
1967
+ */
1968
+ function ownerAppId(reg) {
1969
+ return reg.appId ?? (typeof reg.scope === 'object' ? reg.scope.appId : undefined);
1970
+ }
1971
+
1972
+ /** Debounce (ms) applied to the typeahead between the user's last keystroke
1973
+ * and the HTTP search. Long enough that a fast typist fires one request, not
1974
+ * one per character. */
1975
+ const SEARCH_DEBOUNCE_MS = 250;
1976
+ /** Hard cap on candidate rows rendered, regardless of how many the endpoint
1977
+ * returns. The descriptor's `extraParams.pageSize` is the primary bound; this
1978
+ * is a defensive client-side backstop so a misconfigured endpoint can't paint
1979
+ * a thousand-row list. */
1980
+ const MAX_RESULTS_RENDERED = 25;
1981
+ /**
1982
+ * Shared floating entity typeahead — the `/lookup` picker promoted out of the
1983
+ * agent composer into the design-system so any app (agent composer, notes,
1984
+ * task comments, federated remotes) can let a user find an entity and act on
1985
+ * it. Editor-agnostic: it emits a {@link LookupResult} ({@link pick}) and,
1986
+ * when the entity resolves to a deep link, an {@link EntityLinkSelection}
1987
+ * ({@link entityLinkSelected}) — the consumer decides whether to make a ref
1988
+ * chip, insert a `flyos:` link into an editor, etc.
1989
+ *
1990
+ * Two-stage cascade:
1991
+ * - **Stage 1 — entity**: a filterable autocomplete of the offered entities.
1992
+ * Skipped when there's one entity or `initialEntity` already names one.
1993
+ * - **Stage 2 — search**: a debounced typeahead against the chosen entity's
1994
+ * search endpoint, with a breadcrumb back to stage 1.
1995
+ *
1996
+ * The HTTP call goes through the host's `HttpClient` so the gateway routing +
1997
+ * auth interceptor apply — the same path the MCP `*_list_brief` tools wrap,
1998
+ * but user-authenticated. The "exposed by app" badge resolves names via the
1999
+ * optional {@link LOOKUP_APP_NAME_RESOLVER} the host provides (the DS has no
2000
+ * app registry); absent it, the badge falls back to `appBadgeKey`.
2001
+ */
2002
+ class EntityLookupComponent {
2003
+ http = inject(HttpClient);
2004
+ i18n = inject(I18nService);
2005
+ host = inject((ElementRef));
2006
+ lookupRegistry = inject(AgentLookupRegistry);
2007
+ /** Host-provided appId → {id,name} adapter for the "exposed by app" badge.
2008
+ * The DS has no app registry; the shell (and remotes) provide APP_LOOKUP.
2009
+ * Optional — absent, the badge falls back to a descriptor's appBadgeKey. */
2010
+ appLookup = inject(APP_LOOKUP, { optional: true });
2011
+ /** Entities the picker may search — already filtered to the live-app scope
2012
+ * by the caller. Empty renders a "nothing to look up" hint. */
2013
+ descriptors = input.required(...(ngDevMode ? [{ debugName: "descriptors" }] : /* istanbul ignore next */ []));
2014
+ /** Entity pre-selected (e.g. from `/lookup <entity>`). Ignored when it
2015
+ * doesn't match any descriptor (falls back to the first). */
2016
+ initialEntity = input(undefined, ...(ngDevMode ? [{ debugName: "initialEntity" }] : /* istanbul ignore next */ []));
2017
+ /** Query text pre-seeded (e.g. from `/lookup <entity> <query>`). */
2018
+ initialQuery = input('', ...(ngDevMode ? [{ debugName: "initialQuery" }] : /* istanbul ignore next */ []));
2019
+ /** Per-instance listbox id so `aria-controls` / `aria-activedescendant`
2020
+ * resolve unambiguously when multiple panels mount. */
2021
+ listboxId = input('fly-entity-lookup-listbox', ...(ngDevMode ? [{ debugName: "listboxId" }] : /* istanbul ignore next */ []));
2022
+ /** Open direction. `'up'` (default) suits a bottom-anchored composer (the
2023
+ * agent input); `'down'` suits a top-anchored toolbar button (notes / task
2024
+ * comments) — the panel then anchors to the host's positioned parent (the
2025
+ * button wrapper) and opens below it with a fixed width. */
2026
+ placement = input('up', ...(ngDevMode ? [{ debugName: "placement" }] : /* istanbul ignore next */ []));
2027
+ /** Raw pick — every consumer gets this (agent ref chips, etc.). */
2028
+ pick = output();
2029
+ /** Deep-link selection — emitted ONLY when the picked entity resolves to a
2030
+ * route via the lookup registry (so the consumer never gets a dead link).
2031
+ * Editors bind this to insert a `flyos:` anchor. */
2032
+ entityLinkSelected = output();
2033
+ dismiss = output();
2034
+ /** Which cascade stage is active: pick an entity, then search within it. */
2035
+ stage = signal('entity', ...(ngDevMode ? [{ debugName: "stage" }] : /* istanbul ignore next */ []));
2036
+ /** entity key of the descriptor currently being searched. */
2037
+ activeEntity = signal(null, ...(ngDevMode ? [{ debugName: "activeEntity" }] : /* istanbul ignore next */ []));
2038
+ /** In stage 1 this filters the entity list; in stage 2 it's the search text. */
2039
+ query = signal('', ...(ngDevMode ? [{ debugName: "query" }] : /* istanbul ignore next */ []));
2040
+ results = signal([], ...(ngDevMode ? [{ debugName: "results" }] : /* istanbul ignore next */ []));
2041
+ loading = signal(false, ...(ngDevMode ? [{ debugName: "loading" }] : /* istanbul ignore next */ []));
2042
+ /** i18n key for an inline error (network / bad endpoint); null when clear. */
2043
+ errorKey = signal(null, ...(ngDevMode ? [{ debugName: "errorKey" }] : /* istanbul ignore next */ []));
2044
+ activeIndex = signal(0, ...(ngDevMode ? [{ debugName: "activeIndex" }] : /* istanbul ignore next */ []));
2045
+ /** Query stashed when no entity matched, so it can seed the search field the
2046
+ * moment the user picks an entity. One-shot. */
2047
+ _seedQuery = '';
2048
+ /** The descriptor matching {@link activeEntity}, or null. */
2049
+ activeDescriptor = computed(() => {
2050
+ const e = this.activeEntity();
2051
+ if (!e)
2052
+ return null;
2053
+ return this.descriptors().find((d) => d.entity === e) ?? null;
2054
+ }, ...(ngDevMode ? [{ debugName: "activeDescriptor" }] : /* istanbul ignore next */ []));
2055
+ /** Stage-1 entity options, narrowed by the typed filter. Touches
2056
+ * `i18n.version()` so a locale switch re-resolves the label match. */
2057
+ filteredEntities = computed(() => {
2058
+ this.i18n.version();
2059
+ const q = this.query().trim().toLowerCase();
2060
+ const descs = this.descriptors();
2061
+ if (q.length === 0)
2062
+ return descs;
2063
+ return descs.filter((d) => d.entity.toLowerCase().includes(q) ||
2064
+ this.i18n.t(d.labelKey).toLowerCase().includes(q));
2065
+ }, ...(ngDevMode ? [{ debugName: "filteredEntities" }] : /* istanbul ignore next */ []));
2066
+ /** Size of the list the keyboard currently navigates (entities vs results). */
2067
+ listSize = computed(() => this.stage() === 'entity' ? this.filteredEntities().length : this.results().length, ...(ngDevMode ? [{ debugName: "listSize" }] : /* istanbul ignore next */ []));
2068
+ /** Show the breadcrumb / back affordance only when there's a real choice of
2069
+ * entity to step back to. */
2070
+ showBack = computed(() => this.descriptors().length > 1, ...(ngDevMode ? [{ debugName: "showBack" }] : /* istanbul ignore next */ []));
2071
+ /** Placeholder reflects the active stage. */
2072
+ searchPlaceholderKey = computed(() => this.stage() === 'entity'
2073
+ ? 'agent.lookup.entity_placeholder'
2074
+ : 'agent.lookup.search_placeholder', ...(ngDevMode ? [{ debugName: "searchPlaceholderKey" }] : /* istanbul ignore next */ []));
2075
+ _debounceTimer = null;
2076
+ _searchSub = null;
2077
+ searchEl;
2078
+ constructor() {
2079
+ afterNextRender(() => this._focusSearch());
2080
+ }
2081
+ ngOnInit() {
2082
+ const descs = this.descriptors();
2083
+ const initial = this.initialEntity();
2084
+ const chosen = (initial ? descs.find((d) => d.entity === initial) : undefined) ??
2085
+ (descs.length === 1 ? descs[0] : undefined);
2086
+ if (chosen) {
2087
+ this.activeEntity.set(chosen.entity);
2088
+ this.query.set(this.initialQuery() ?? '');
2089
+ this.stage.set('search');
2090
+ this._runSearch();
2091
+ }
2092
+ else {
2093
+ this._seedQuery = this.initialQuery() ?? '';
2094
+ this.activeEntity.set(null);
2095
+ this.query.set('');
2096
+ this.stage.set('entity');
2097
+ }
2098
+ }
2099
+ ngOnDestroy() {
2100
+ this._clearTimer();
2101
+ this._searchSub?.unsubscribe();
2102
+ }
2103
+ optionId(index) {
2104
+ return `${this.listboxId()}-opt-${index}`;
2105
+ }
2106
+ activeDescendant = computed(() => {
2107
+ if (this.listSize() === 0)
2108
+ return '';
2109
+ return `${this.listboxId()}-opt-${this._clampedIndex()}`;
2110
+ }, ...(ngDevMode ? [{ debugName: "activeDescendant" }] : /* istanbul ignore next */ []));
2111
+ _clampedIndex() {
2112
+ const size = this.listSize();
2113
+ if (size === 0)
2114
+ return 0;
2115
+ return Math.min(Math.max(0, this.activeIndex()), size - 1);
2116
+ }
2117
+ // ── Input + entity handlers ─────────────────────────────────────────────
2118
+ onQueryInput(value) {
2119
+ this.query.set(value);
2120
+ this.activeIndex.set(0);
2121
+ if (this.stage() === 'search')
2122
+ this._scheduleSearch();
2123
+ }
2124
+ onSelectEntity(entity) {
2125
+ this.activeEntity.set(entity);
2126
+ this.activeIndex.set(0);
2127
+ this.stage.set('search');
2128
+ this.query.set(this._seedQuery);
2129
+ this._seedQuery = '';
2130
+ this.errorKey.set(null);
2131
+ this._runSearch();
2132
+ this._focusSearch();
2133
+ }
2134
+ goBackToEntity() {
2135
+ if (!this.showBack())
2136
+ return;
2137
+ this._clearTimer();
2138
+ this._searchSub?.unsubscribe();
2139
+ this.stage.set('entity');
2140
+ this.activeEntity.set(null);
2141
+ this.activeIndex.set(0);
2142
+ this.query.set('');
2143
+ this.results.set([]);
2144
+ this.loading.set(false);
2145
+ this.errorKey.set(null);
2146
+ this._focusSearch();
2147
+ }
2148
+ onRowClick(row) {
2149
+ this._selectRow(row);
2150
+ }
2151
+ onRowHover(index) {
2152
+ if (index !== this.activeIndex())
2153
+ this.activeIndex.set(index);
2154
+ }
2155
+ /**
2156
+ * Single pick exit. Emits the raw {@link pick} for every consumer, AND —
2157
+ * when the entity resolves to a deep-link route via the registry — an
2158
+ * {@link entityLinkSelected} carrying the `flyos:` href. Resolution failures
2159
+ * (unknown entity / app mismatch / owning app not installed / no
2160
+ * `deepLinkRoute`) simply omit the deep-link event: editor consumers get
2161
+ * nothing to insert rather than a dead link, while the raw `pick` path
2162
+ * (agent ref chips) still fires.
2163
+ */
2164
+ _selectRow(row) {
2165
+ this.pick.emit(row);
2166
+ const target = this.lookupRegistry.resolveDeepLinkForEntity(row.entity, row.id);
2167
+ if (target) {
2168
+ this.entityLinkSelected.emit({
2169
+ label: row.label,
2170
+ kind: `${target.appId}.${row.entity}`,
2171
+ id: row.id,
2172
+ appId: target.appId,
2173
+ route: target.route,
2174
+ href: `flyos:${target.appId}.${row.entity}/${row.id}`,
2175
+ });
2176
+ }
2177
+ }
2178
+ onKeydown(ev) {
2179
+ if (ev.key === 'Escape') {
2180
+ ev.preventDefault();
2181
+ ev.stopPropagation();
2182
+ this.dismiss.emit();
2183
+ return;
2184
+ }
2185
+ if (ev.key === 'Backspace' &&
2186
+ this.stage() === 'search' &&
2187
+ this.query().length === 0 &&
2188
+ this.showBack()) {
2189
+ ev.preventDefault();
2190
+ this.goBackToEntity();
2191
+ return;
2192
+ }
2193
+ const size = this.listSize();
2194
+ if (ev.key === 'ArrowDown') {
2195
+ ev.preventDefault();
2196
+ if (size === 0)
2197
+ return;
2198
+ this.activeIndex.set((this._clampedIndex() + 1) % size);
2199
+ return;
2200
+ }
2201
+ if (ev.key === 'ArrowUp') {
2202
+ ev.preventDefault();
2203
+ if (size === 0)
2204
+ return;
2205
+ this.activeIndex.set((this._clampedIndex() - 1 + size) % size);
2206
+ return;
2207
+ }
2208
+ if (ev.key === 'Enter') {
2209
+ ev.preventDefault();
2210
+ if (this.stage() === 'entity') {
2211
+ const desc = this.filteredEntities()[this._clampedIndex()];
2212
+ if (desc)
2213
+ this.onSelectEntity(desc.entity);
2214
+ }
2215
+ else {
2216
+ const row = this.results()[this._clampedIndex()];
2217
+ if (row)
2218
+ this._selectRow(row);
2219
+ }
2220
+ return;
2221
+ }
2222
+ }
2223
+ onDocumentMouseDown(ev) {
2224
+ const target = ev.target;
2225
+ if (!target)
2226
+ return;
2227
+ if (this.host.nativeElement.contains(target))
2228
+ return;
2229
+ this.dismiss.emit();
2230
+ }
2231
+ onDocumentEscape() {
2232
+ this.dismiss.emit();
2233
+ }
2234
+ // ── Search internals ────────────────────────────────────────────────────
2235
+ _scheduleSearch() {
2236
+ this._clearTimer();
2237
+ this._debounceTimer = setTimeout(() => this._runSearch(), SEARCH_DEBOUNCE_MS);
2238
+ }
2239
+ _clearTimer() {
2240
+ if (this._debounceTimer !== null) {
2241
+ clearTimeout(this._debounceTimer);
2242
+ this._debounceTimer = null;
2243
+ }
2244
+ }
2245
+ _runSearch() {
2246
+ this._clearTimer();
2247
+ const desc = this.activeDescriptor();
2248
+ if (!desc) {
2249
+ this.results.set([]);
2250
+ return;
2251
+ }
2252
+ const endpoint = desc.search.endpoint;
2253
+ // SSRF / shape guard — relative path only (no scheme/host, no protocol-
2254
+ // relative `//host`).
2255
+ if (!endpoint.startsWith('/') || endpoint.startsWith('//')) {
2256
+ this.errorKey.set('agent.lookup.error');
2257
+ this.results.set([]);
2258
+ this.loading.set(false);
2259
+ return;
2260
+ }
2261
+ this._searchSub?.unsubscribe();
2262
+ this.loading.set(true);
2263
+ this.errorKey.set(null);
2264
+ let params = new HttpParams().set(desc.search.queryParam, this.query().trim());
2265
+ for (const [k, v] of Object.entries(desc.search.extraParams ?? {})) {
2266
+ params = params.set(k, v);
2267
+ }
2268
+ this._searchSub = this.http.get(endpoint, { params }).subscribe({
2269
+ next: (body) => {
2270
+ this.results.set(this._mapResults(desc, body));
2271
+ this.loading.set(false);
2272
+ this.activeIndex.set(0);
2273
+ },
2274
+ error: () => {
2275
+ this.errorKey.set('agent.lookup.error');
2276
+ this.results.set([]);
2277
+ this.loading.set(false);
2278
+ },
2279
+ });
2280
+ }
2281
+ _mapResults(desc, body) {
2282
+ const arr = this._extractArray(body, desc.search.resultsPath);
2283
+ const idField = desc.search.idField ?? 'id';
2284
+ const dispField = desc.search.displayField;
2285
+ const secField = desc.search.secondaryField;
2286
+ const appField = desc.search.appIdField;
2287
+ const out = [];
2288
+ for (const item of arr) {
2289
+ if (!item || typeof item !== 'object')
2290
+ continue;
2291
+ const rec = item;
2292
+ const id = rec[idField];
2293
+ const label = rec[dispField];
2294
+ if (id == null || label == null)
2295
+ continue;
2296
+ const rowAppId = appField != null && rec[appField] != null && rec[appField] !== ''
2297
+ ? String(rec[appField])
2298
+ : undefined;
2299
+ out.push({
2300
+ entity: desc.entity,
2301
+ id: String(id),
2302
+ label: String(label),
2303
+ secondary: secField != null && rec[secField] != null
2304
+ ? String(rec[secField])
2305
+ : undefined,
2306
+ appId: rowAppId,
2307
+ });
2308
+ if (out.length >= MAX_RESULTS_RENDERED)
2309
+ break;
2310
+ }
2311
+ return out;
2312
+ }
2313
+ _extractArray(body, path) {
2314
+ if (Array.isArray(body))
2315
+ return body;
2316
+ if (!path)
2317
+ return [];
2318
+ let cur = body;
2319
+ for (const seg of path.split('.')) {
2320
+ if (cur && typeof cur === 'object' && seg in cur) {
2321
+ cur = cur[seg];
2322
+ }
2323
+ else {
2324
+ return [];
2325
+ }
2326
+ }
2327
+ return Array.isArray(cur) ? cur : [];
2328
+ }
2329
+ _focusSearch() {
2330
+ queueMicrotask(() => this.searchEl?.nativeElement.focus());
2331
+ }
2332
+ entityLabel(desc) {
2333
+ this.i18n.version();
2334
+ return this.i18n.t(desc.labelKey);
2335
+ }
2336
+ activeEntityLabel = computed(() => {
2337
+ const desc = this.activeDescriptor();
2338
+ return desc ? this.entityLabel(desc) : '';
2339
+ }, ...(ngDevMode ? [{ debugName: "activeEntityLabel" }] : /* istanbul ignore next */ []));
2340
+ /**
2341
+ * Display name of the app exposing this descriptor, for the "exposed by …"
2342
+ * badge. Resolution order: host-provided {@link LOOKUP_APP_NAME_RESOLVER}
2343
+ * (keyed by `descriptor.appId`) → descriptor `appBadgeKey` (literal i18n
2344
+ * key, for entities not mapped to a single shell app) → '' (no badge).
2345
+ */
2346
+ appLabel(desc) {
2347
+ this.i18n.version();
2348
+ if (desc.appId) {
2349
+ const resolved = this.appLookup?.getById(desc.appId)?.name;
2350
+ if (resolved)
2351
+ return resolved;
2352
+ }
2353
+ if (desc.appBadgeKey)
2354
+ return this.i18n.t(desc.appBadgeKey);
2355
+ return '';
2356
+ }
2357
+ activeAppLabel = computed(() => {
2358
+ const desc = this.activeDescriptor();
2359
+ return desc ? this.appLabel(desc) : '';
2360
+ }, ...(ngDevMode ? [{ debugName: "activeAppLabel" }] : /* istanbul ignore next */ []));
2361
+ /** Per-row source-app label for stage-2 rows. Resolves {@link LookupResult.appId}
2362
+ * through the host resolver. '' when the row carries no `appId`, the resolver
2363
+ * is absent / doesn't know it, or it duplicates {@link activeAppLabel}. */
2364
+ rowAppLabel(row) {
2365
+ this.i18n.version();
2366
+ if (!row.appId)
2367
+ return '';
2368
+ const resolved = this.appLookup?.getById(row.appId)?.name;
2369
+ if (!resolved)
2370
+ return '';
2371
+ if (resolved === this.activeAppLabel())
2372
+ return '';
2373
+ return resolved;
2374
+ }
2375
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.5", ngImport: i0, type: EntityLookupComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
2376
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.5", type: EntityLookupComponent, isStandalone: true, selector: "fly-entity-lookup", inputs: { descriptors: { classPropertyName: "descriptors", publicName: "descriptors", isSignal: true, isRequired: true, transformFunction: null }, initialEntity: { classPropertyName: "initialEntity", publicName: "initialEntity", isSignal: true, isRequired: false, transformFunction: null }, initialQuery: { classPropertyName: "initialQuery", publicName: "initialQuery", isSignal: true, isRequired: false, transformFunction: null }, listboxId: { classPropertyName: "listboxId", publicName: "listboxId", isSignal: true, isRequired: false, transformFunction: null }, placement: { classPropertyName: "placement", publicName: "placement", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { pick: "pick", entityLinkSelected: "entityLinkSelected", dismiss: "dismiss" }, host: { listeners: { "document:mousedown": "onDocumentMouseDown($event)", "document:keydown.escape": "onDocumentEscape()" }, properties: { "class.fly-entity-lookup--down": "placement() === 'down'", "class.fly-entity-lookup--above": "placement() === 'above'" } }, viewQueries: [{ propertyName: "searchEl", first: true, predicate: ["searchRef"], descendants: true }], ngImport: i0, template: "<div class=\"fly-entity-lookup\" role=\"dialog\" [attr.aria-label]=\"'agent.lookup.dialog_aria' | translate\">\n @if (descriptors().length === 0) {\n <div class=\"fly-entity-lookup__empty\" role=\"status\">\n {{ 'agent.lookup.no_entities' | translate }}\n </div>\n } @else {\n <div class=\"fly-entity-lookup__search\" role=\"combobox\" aria-haspopup=\"listbox\" aria-expanded=\"true\"\n [attr.aria-controls]=\"listboxId()\">\n @if (stage() === 'search' && showBack()) {\n <!-- Breadcrumb back to the entity picker (also reachable via Backspace\n on an empty query). Shows the chosen entity so the user always\n knows what they're searching within. -->\n <button\n type=\"button\"\n class=\"fly-entity-lookup__crumb\"\n (click)=\"goBackToEntity()\"\n [attr.aria-label]=\"'agent.lookup.back_aria' | translate\">\n <i class=\"pi pi-angle-left fly-entity-lookup__crumb-back\" aria-hidden=\"true\"></i>\n @if (activeDescriptor()?.icon; as ic) {\n <i class=\"pi {{ ic }}\" aria-hidden=\"true\"></i>\n }\n <span>{{ activeEntityLabel() }}</span>\n </button>\n } @else {\n <i class=\"pi pi-search fly-entity-lookup__search-icon\" aria-hidden=\"true\"></i>\n }\n <input\n #searchRef\n type=\"text\"\n class=\"fly-entity-lookup__search-input\"\n [ngModel]=\"query()\"\n (ngModelChange)=\"onQueryInput($event)\"\n (keydown)=\"onKeydown($event)\"\n [attr.aria-activedescendant]=\"activeDescendant()\"\n [attr.aria-controls]=\"listboxId()\"\n [placeholder]=\"searchPlaceholderKey() | translate\" />\n </div>\n\n <div [id]=\"listboxId()\" class=\"fly-entity-lookup__results\" role=\"listbox\"\n [attr.aria-label]=\"(stage() === 'entity' ? 'agent.lookup.entities_aria' : 'agent.lookup.results_aria') | translate\">\n @if (stage() === 'entity') {\n <!-- Stage 1: entity autocomplete. -->\n @if (filteredEntities().length === 0) {\n <div class=\"fly-entity-lookup__status\" role=\"status\">{{ 'agent.lookup.no_results' | translate }}</div>\n } @else {\n @for (desc of filteredEntities(); track desc.entity; let i = $index) {\n <button\n type=\"button\"\n role=\"option\"\n class=\"fly-entity-lookup__opt fly-entity-lookup__opt--entity\"\n [id]=\"optionId(i)\"\n [attr.aria-selected]=\"i === activeIndex()\"\n [class.is-active]=\"i === activeIndex()\"\n (click)=\"onSelectEntity(desc.entity)\"\n (mouseenter)=\"onRowHover(i)\">\n <span class=\"fly-entity-lookup__opt-label\">\n @if (desc.icon) {\n <i class=\"pi {{ desc.icon }}\" aria-hidden=\"true\"></i>\n }\n {{ entityLabel(desc) }}\n </span>\n @if (appLabel(desc); as app) {\n <span class=\"fly-entity-lookup__opt-app-badge\" [title]=\"app\">{{ app }}</span>\n }\n <i class=\"pi pi-angle-right fly-entity-lookup__opt-chevron\" aria-hidden=\"true\"></i>\n </button>\n }\n }\n } @else {\n <!-- Stage 2: result typeahead within the chosen entity. -->\n @if (loading()) {\n <div class=\"fly-entity-lookup__status\" role=\"status\">{{ 'agent.lookup.searching' | translate }}</div>\n } @else if (errorKey(); as err) {\n <div class=\"fly-entity-lookup__status fly-entity-lookup__status--error\" role=\"alert\">{{ err | translate }}</div>\n } @else if (results().length === 0) {\n <div class=\"fly-entity-lookup__status\" role=\"status\">{{ 'agent.lookup.no_results' | translate }}</div>\n } @else {\n @for (row of results(); track row.id; let i = $index) {\n <button\n type=\"button\"\n role=\"option\"\n class=\"fly-entity-lookup__opt\"\n [id]=\"optionId(i)\"\n [attr.aria-selected]=\"i === activeIndex()\"\n [class.is-active]=\"i === activeIndex()\"\n (click)=\"onRowClick(row)\"\n (mouseenter)=\"onRowHover(i)\">\n <span class=\"fly-entity-lookup__opt-label\" [title]=\"row.label\">{{ row.label }}</span>\n @if (row.secondary) {\n <span class=\"fly-entity-lookup__opt-secondary\">{{ row.secondary }}</span>\n }\n @if (rowAppLabel(row); as rowApp) {\n <!-- Source app of this individual row (e.g. \"Circles\" for a\n Help-Center article seeded by Circles). Rendered before the\n descriptor tag so the per-row signal sits closer to the\n title; suppressed when it would duplicate the descriptor\n label. -->\n <span class=\"fly-entity-lookup__opt-app-tag fly-entity-lookup__opt-app-tag--source\"\n [title]=\"rowApp\">{{ rowApp }}</span>\n }\n @if (activeAppLabel(); as app) {\n <span class=\"fly-entity-lookup__opt-app-tag\" [title]=\"app\">{{ app }}</span>\n }\n </button>\n }\n }\n }\n </div>\n }\n</div>\n", styles: [":host{display:block;position:relative;z-index:6}:host(.fly-entity-lookup--down){position:static}:host(.fly-entity-lookup--down) .fly-entity-lookup{inset-block-end:auto;inset-block-start:100%;inset-inline:auto;inset-inline-end:0;margin-block-end:0;margin-block-start:6px;min-inline-size:320px;max-inline-size:min(380px,90vw);z-index:1000}:host(.fly-entity-lookup--above){position:static}:host(.fly-entity-lookup--above) .fly-entity-lookup{inset-block-start:auto;inset-block-end:100%;inset-inline:0;margin-block-start:0;margin-block-end:6px;z-index:1000}.fly-entity-lookup{position:absolute;inset-block-end:100%;inset-inline:12px;margin-block-end:6px;background:var(--surface-card, rgba(28, 28, 30, .96));backdrop-filter:blur(24px) saturate(180%);-webkit-backdrop-filter:blur(24px) saturate(180%);border:1px solid var(--surface-border, rgba(255, 255, 255, .1));border-radius:12px;box-shadow:0 10px 24px #0000002e}@supports not ((backdrop-filter: blur(1px)) or (-webkit-backdrop-filter: blur(1px))){.fly-entity-lookup{background:#1c1c1efa}}.fly-entity-lookup{padding-block:6px;display:flex;flex-direction:column;gap:6px}.fly-entity-lookup__empty{padding-block:10px;padding-inline:14px;color:var(--text-color-secondary, rgba(255, 255, 255, .5));font-size:12px;line-height:1.3}.fly-entity-lookup__crumb{display:inline-flex;align-items:center;gap:5px;flex-shrink:0;max-inline-size:45%;padding-block:2px;padding-inline:7px;border-radius:6px;border:1px solid var(--surface-border, rgba(255, 255, 255, .12));background:var(--surface-hover, rgba(255, 255, 255, .06));color:var(--text-color-secondary, rgba(255, 255, 255, .75));font-size:11px;font-family:inherit;line-height:1.2;cursor:pointer;transition:background .12s ease,color .12s ease}.fly-entity-lookup__crumb i.pi{font-size:12px}.fly-entity-lookup__crumb .fly-entity-lookup__crumb-back{opacity:.75}.fly-entity-lookup__crumb span{white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.fly-entity-lookup__crumb:hover,.fly-entity-lookup__crumb:focus-visible{background:var(--surface-active, rgba(255, 255, 255, .1));color:inherit;outline:none}.fly-entity-lookup__crumb:focus-visible{outline:2px solid var(--primary-color, #5b8cff);outline-offset:1px}[dir=rtl] .fly-entity-lookup__crumb-back{transform:scaleX(-1)}.fly-entity-lookup__search{display:flex;align-items:center;gap:8px;margin-inline:10px;padding-block:6px;padding-inline:10px;border-radius:8px;background:var(--surface-hover, rgba(255, 255, 255, .06));border:1px solid var(--surface-border, rgba(255, 255, 255, .1))}.fly-entity-lookup__search-icon{color:var(--text-color-secondary, rgba(255, 255, 255, .5));font-size:13px}.fly-entity-lookup__search-input{flex:1 1 auto;min-inline-size:0;background:transparent;border:none;outline:none;color:inherit;font-size:13px;font-family:inherit}.fly-entity-lookup__search-input::placeholder{color:var(--text-color-secondary, rgba(255, 255, 255, .4))}.fly-entity-lookup__results{max-block-size:240px;overflow-y:auto;display:flex;flex-direction:column}.fly-entity-lookup__status{padding-block:10px;padding-inline:14px;color:var(--text-color-secondary, rgba(255, 255, 255, .5));font-size:12px;line-height:1.3}.fly-entity-lookup__status--error{color:var(--red-400, #f87171)}.fly-entity-lookup__opt{display:flex;align-items:center;gap:8px;padding-block:6px;padding-inline:14px;background:transparent;border:none;text-align:start;color:inherit;font-size:13px;font-family:inherit;cursor:pointer;transition:background .12s ease}.fly-entity-lookup__opt.is-active,.fly-entity-lookup__opt:focus-visible{background:var(--surface-hover, rgba(255, 255, 255, .08));outline:none}.fly-entity-lookup__opt-label{flex:1 1 auto;min-inline-size:0;display:inline-flex;align-items:center;gap:7px;font-weight:500;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.fly-entity-lookup__opt-label i.pi{font-size:13px;opacity:.85;flex-shrink:0}.fly-entity-lookup__opt-secondary{flex-shrink:0;font-size:11px;text-transform:uppercase;letter-spacing:.04em;color:var(--text-color-secondary, rgba(255, 255, 255, .5));white-space:nowrap}.fly-entity-lookup__opt-app-badge{flex-shrink:0;padding-block:1px;padding-inline:7px;border-radius:999px;border:1px solid var(--surface-border, rgba(255, 255, 255, .12));background:var(--surface-hover, rgba(255, 255, 255, .05));color:var(--text-color-secondary, rgba(255, 255, 255, .65));font-size:10.5px;line-height:1.5;letter-spacing:.02em;white-space:nowrap;max-inline-size:110px;overflow:hidden;text-overflow:ellipsis}.fly-entity-lookup__opt-app-tag{flex-shrink:0;font-size:10.5px;color:var(--text-color-secondary, rgba(255, 255, 255, .4));white-space:nowrap;max-inline-size:110px;overflow:hidden;text-overflow:ellipsis}.fly-entity-lookup__opt-app-tag--source{color:var(--text-color-secondary, rgba(255, 255, 255, .65));font-weight:500}.fly-entity-lookup__opt-chevron{flex-shrink:0;font-size:12px;opacity:.4}.fly-entity-lookup__opt--entity.is-active .fly-entity-lookup__opt-chevron,.fly-entity-lookup__opt--entity:hover .fly-entity-lookup__opt-chevron{opacity:.75}[dir=rtl] .fly-entity-lookup__opt-chevron{transform:scaleX(-1)}@media(prefers-reduced-motion:reduce){.fly-entity-lookup__crumb,.fly-entity-lookup__opt{transition:none}}@media(forced-colors:active){.fly-entity-lookup{border-color:CanvasText}.fly-entity-lookup__opt.is-active{outline:1px solid Highlight;outline-offset:-1px}}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "ngmodule", type: FormsModule }, { kind: "directive", type: i1.DefaultValueAccessor, selector: "input:not([type=checkbox])[formControlName],textarea[formControlName],input:not([type=checkbox])[formControl],textarea[formControl],input:not([type=checkbox])[ngModel],textarea[ngModel],[ngDefaultControl]" }, { kind: "directive", type: i1.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i1.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }, { kind: "pipe", type: TranslatePipe, name: "translate" }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
2377
+ }
2378
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.5", ngImport: i0, type: EntityLookupComponent, decorators: [{
2379
+ type: Component,
2380
+ args: [{ selector: 'fly-entity-lookup', standalone: true, imports: [CommonModule, FormsModule, TranslatePipe], changeDetection: ChangeDetectionStrategy.OnPush, host: {
2381
+ '[class.fly-entity-lookup--down]': "placement() === 'down'",
2382
+ '[class.fly-entity-lookup--above]': "placement() === 'above'",
2383
+ }, template: "<div class=\"fly-entity-lookup\" role=\"dialog\" [attr.aria-label]=\"'agent.lookup.dialog_aria' | translate\">\n @if (descriptors().length === 0) {\n <div class=\"fly-entity-lookup__empty\" role=\"status\">\n {{ 'agent.lookup.no_entities' | translate }}\n </div>\n } @else {\n <div class=\"fly-entity-lookup__search\" role=\"combobox\" aria-haspopup=\"listbox\" aria-expanded=\"true\"\n [attr.aria-controls]=\"listboxId()\">\n @if (stage() === 'search' && showBack()) {\n <!-- Breadcrumb back to the entity picker (also reachable via Backspace\n on an empty query). Shows the chosen entity so the user always\n knows what they're searching within. -->\n <button\n type=\"button\"\n class=\"fly-entity-lookup__crumb\"\n (click)=\"goBackToEntity()\"\n [attr.aria-label]=\"'agent.lookup.back_aria' | translate\">\n <i class=\"pi pi-angle-left fly-entity-lookup__crumb-back\" aria-hidden=\"true\"></i>\n @if (activeDescriptor()?.icon; as ic) {\n <i class=\"pi {{ ic }}\" aria-hidden=\"true\"></i>\n }\n <span>{{ activeEntityLabel() }}</span>\n </button>\n } @else {\n <i class=\"pi pi-search fly-entity-lookup__search-icon\" aria-hidden=\"true\"></i>\n }\n <input\n #searchRef\n type=\"text\"\n class=\"fly-entity-lookup__search-input\"\n [ngModel]=\"query()\"\n (ngModelChange)=\"onQueryInput($event)\"\n (keydown)=\"onKeydown($event)\"\n [attr.aria-activedescendant]=\"activeDescendant()\"\n [attr.aria-controls]=\"listboxId()\"\n [placeholder]=\"searchPlaceholderKey() | translate\" />\n </div>\n\n <div [id]=\"listboxId()\" class=\"fly-entity-lookup__results\" role=\"listbox\"\n [attr.aria-label]=\"(stage() === 'entity' ? 'agent.lookup.entities_aria' : 'agent.lookup.results_aria') | translate\">\n @if (stage() === 'entity') {\n <!-- Stage 1: entity autocomplete. -->\n @if (filteredEntities().length === 0) {\n <div class=\"fly-entity-lookup__status\" role=\"status\">{{ 'agent.lookup.no_results' | translate }}</div>\n } @else {\n @for (desc of filteredEntities(); track desc.entity; let i = $index) {\n <button\n type=\"button\"\n role=\"option\"\n class=\"fly-entity-lookup__opt fly-entity-lookup__opt--entity\"\n [id]=\"optionId(i)\"\n [attr.aria-selected]=\"i === activeIndex()\"\n [class.is-active]=\"i === activeIndex()\"\n (click)=\"onSelectEntity(desc.entity)\"\n (mouseenter)=\"onRowHover(i)\">\n <span class=\"fly-entity-lookup__opt-label\">\n @if (desc.icon) {\n <i class=\"pi {{ desc.icon }}\" aria-hidden=\"true\"></i>\n }\n {{ entityLabel(desc) }}\n </span>\n @if (appLabel(desc); as app) {\n <span class=\"fly-entity-lookup__opt-app-badge\" [title]=\"app\">{{ app }}</span>\n }\n <i class=\"pi pi-angle-right fly-entity-lookup__opt-chevron\" aria-hidden=\"true\"></i>\n </button>\n }\n }\n } @else {\n <!-- Stage 2: result typeahead within the chosen entity. -->\n @if (loading()) {\n <div class=\"fly-entity-lookup__status\" role=\"status\">{{ 'agent.lookup.searching' | translate }}</div>\n } @else if (errorKey(); as err) {\n <div class=\"fly-entity-lookup__status fly-entity-lookup__status--error\" role=\"alert\">{{ err | translate }}</div>\n } @else if (results().length === 0) {\n <div class=\"fly-entity-lookup__status\" role=\"status\">{{ 'agent.lookup.no_results' | translate }}</div>\n } @else {\n @for (row of results(); track row.id; let i = $index) {\n <button\n type=\"button\"\n role=\"option\"\n class=\"fly-entity-lookup__opt\"\n [id]=\"optionId(i)\"\n [attr.aria-selected]=\"i === activeIndex()\"\n [class.is-active]=\"i === activeIndex()\"\n (click)=\"onRowClick(row)\"\n (mouseenter)=\"onRowHover(i)\">\n <span class=\"fly-entity-lookup__opt-label\" [title]=\"row.label\">{{ row.label }}</span>\n @if (row.secondary) {\n <span class=\"fly-entity-lookup__opt-secondary\">{{ row.secondary }}</span>\n }\n @if (rowAppLabel(row); as rowApp) {\n <!-- Source app of this individual row (e.g. \"Circles\" for a\n Help-Center article seeded by Circles). Rendered before the\n descriptor tag so the per-row signal sits closer to the\n title; suppressed when it would duplicate the descriptor\n label. -->\n <span class=\"fly-entity-lookup__opt-app-tag fly-entity-lookup__opt-app-tag--source\"\n [title]=\"rowApp\">{{ rowApp }}</span>\n }\n @if (activeAppLabel(); as app) {\n <span class=\"fly-entity-lookup__opt-app-tag\" [title]=\"app\">{{ app }}</span>\n }\n </button>\n }\n }\n }\n </div>\n }\n</div>\n", styles: [":host{display:block;position:relative;z-index:6}:host(.fly-entity-lookup--down){position:static}:host(.fly-entity-lookup--down) .fly-entity-lookup{inset-block-end:auto;inset-block-start:100%;inset-inline:auto;inset-inline-end:0;margin-block-end:0;margin-block-start:6px;min-inline-size:320px;max-inline-size:min(380px,90vw);z-index:1000}:host(.fly-entity-lookup--above){position:static}:host(.fly-entity-lookup--above) .fly-entity-lookup{inset-block-start:auto;inset-block-end:100%;inset-inline:0;margin-block-start:0;margin-block-end:6px;z-index:1000}.fly-entity-lookup{position:absolute;inset-block-end:100%;inset-inline:12px;margin-block-end:6px;background:var(--surface-card, rgba(28, 28, 30, .96));backdrop-filter:blur(24px) saturate(180%);-webkit-backdrop-filter:blur(24px) saturate(180%);border:1px solid var(--surface-border, rgba(255, 255, 255, .1));border-radius:12px;box-shadow:0 10px 24px #0000002e}@supports not ((backdrop-filter: blur(1px)) or (-webkit-backdrop-filter: blur(1px))){.fly-entity-lookup{background:#1c1c1efa}}.fly-entity-lookup{padding-block:6px;display:flex;flex-direction:column;gap:6px}.fly-entity-lookup__empty{padding-block:10px;padding-inline:14px;color:var(--text-color-secondary, rgba(255, 255, 255, .5));font-size:12px;line-height:1.3}.fly-entity-lookup__crumb{display:inline-flex;align-items:center;gap:5px;flex-shrink:0;max-inline-size:45%;padding-block:2px;padding-inline:7px;border-radius:6px;border:1px solid var(--surface-border, rgba(255, 255, 255, .12));background:var(--surface-hover, rgba(255, 255, 255, .06));color:var(--text-color-secondary, rgba(255, 255, 255, .75));font-size:11px;font-family:inherit;line-height:1.2;cursor:pointer;transition:background .12s ease,color .12s ease}.fly-entity-lookup__crumb i.pi{font-size:12px}.fly-entity-lookup__crumb .fly-entity-lookup__crumb-back{opacity:.75}.fly-entity-lookup__crumb span{white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.fly-entity-lookup__crumb:hover,.fly-entity-lookup__crumb:focus-visible{background:var(--surface-active, rgba(255, 255, 255, .1));color:inherit;outline:none}.fly-entity-lookup__crumb:focus-visible{outline:2px solid var(--primary-color, #5b8cff);outline-offset:1px}[dir=rtl] .fly-entity-lookup__crumb-back{transform:scaleX(-1)}.fly-entity-lookup__search{display:flex;align-items:center;gap:8px;margin-inline:10px;padding-block:6px;padding-inline:10px;border-radius:8px;background:var(--surface-hover, rgba(255, 255, 255, .06));border:1px solid var(--surface-border, rgba(255, 255, 255, .1))}.fly-entity-lookup__search-icon{color:var(--text-color-secondary, rgba(255, 255, 255, .5));font-size:13px}.fly-entity-lookup__search-input{flex:1 1 auto;min-inline-size:0;background:transparent;border:none;outline:none;color:inherit;font-size:13px;font-family:inherit}.fly-entity-lookup__search-input::placeholder{color:var(--text-color-secondary, rgba(255, 255, 255, .4))}.fly-entity-lookup__results{max-block-size:240px;overflow-y:auto;display:flex;flex-direction:column}.fly-entity-lookup__status{padding-block:10px;padding-inline:14px;color:var(--text-color-secondary, rgba(255, 255, 255, .5));font-size:12px;line-height:1.3}.fly-entity-lookup__status--error{color:var(--red-400, #f87171)}.fly-entity-lookup__opt{display:flex;align-items:center;gap:8px;padding-block:6px;padding-inline:14px;background:transparent;border:none;text-align:start;color:inherit;font-size:13px;font-family:inherit;cursor:pointer;transition:background .12s ease}.fly-entity-lookup__opt.is-active,.fly-entity-lookup__opt:focus-visible{background:var(--surface-hover, rgba(255, 255, 255, .08));outline:none}.fly-entity-lookup__opt-label{flex:1 1 auto;min-inline-size:0;display:inline-flex;align-items:center;gap:7px;font-weight:500;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.fly-entity-lookup__opt-label i.pi{font-size:13px;opacity:.85;flex-shrink:0}.fly-entity-lookup__opt-secondary{flex-shrink:0;font-size:11px;text-transform:uppercase;letter-spacing:.04em;color:var(--text-color-secondary, rgba(255, 255, 255, .5));white-space:nowrap}.fly-entity-lookup__opt-app-badge{flex-shrink:0;padding-block:1px;padding-inline:7px;border-radius:999px;border:1px solid var(--surface-border, rgba(255, 255, 255, .12));background:var(--surface-hover, rgba(255, 255, 255, .05));color:var(--text-color-secondary, rgba(255, 255, 255, .65));font-size:10.5px;line-height:1.5;letter-spacing:.02em;white-space:nowrap;max-inline-size:110px;overflow:hidden;text-overflow:ellipsis}.fly-entity-lookup__opt-app-tag{flex-shrink:0;font-size:10.5px;color:var(--text-color-secondary, rgba(255, 255, 255, .4));white-space:nowrap;max-inline-size:110px;overflow:hidden;text-overflow:ellipsis}.fly-entity-lookup__opt-app-tag--source{color:var(--text-color-secondary, rgba(255, 255, 255, .65));font-weight:500}.fly-entity-lookup__opt-chevron{flex-shrink:0;font-size:12px;opacity:.4}.fly-entity-lookup__opt--entity.is-active .fly-entity-lookup__opt-chevron,.fly-entity-lookup__opt--entity:hover .fly-entity-lookup__opt-chevron{opacity:.75}[dir=rtl] .fly-entity-lookup__opt-chevron{transform:scaleX(-1)}@media(prefers-reduced-motion:reduce){.fly-entity-lookup__crumb,.fly-entity-lookup__opt{transition:none}}@media(forced-colors:active){.fly-entity-lookup{border-color:CanvasText}.fly-entity-lookup__opt.is-active{outline:1px solid Highlight;outline-offset:-1px}}\n"] }]
2384
+ }], ctorParameters: () => [], propDecorators: { descriptors: [{ type: i0.Input, args: [{ isSignal: true, alias: "descriptors", required: true }] }], initialEntity: [{ type: i0.Input, args: [{ isSignal: true, alias: "initialEntity", required: false }] }], initialQuery: [{ type: i0.Input, args: [{ isSignal: true, alias: "initialQuery", required: false }] }], listboxId: [{ type: i0.Input, args: [{ isSignal: true, alias: "listboxId", required: false }] }], placement: [{ type: i0.Input, args: [{ isSignal: true, alias: "placement", required: false }] }], pick: [{ type: i0.Output, args: ["pick"] }], entityLinkSelected: [{ type: i0.Output, args: ["entityLinkSelected"] }], dismiss: [{ type: i0.Output, args: ["dismiss"] }], searchEl: [{
2385
+ type: ViewChild,
2386
+ args: ['searchRef']
2387
+ }], onDocumentMouseDown: [{
2388
+ type: HostListener,
2389
+ args: ['document:mousedown', ['$event']]
2390
+ }], onDocumentEscape: [{
2391
+ type: HostListener,
2392
+ args: ['document:keydown.escape']
2393
+ }] } });
2394
+
2395
+ const ENTITY_LINK_LAUNCHER = new InjectionToken('ENTITY_LINK_LAUNCHER');
2396
+ const MARKDOWN_TOOLBAR_PRESETS = {
2397
+ full: [
2398
+ 'undo', 'redo', '|',
2399
+ 'h1', 'h2', 'h3', '|',
2400
+ 'bold', 'italic', 'underline', 'strike', 'code', '|',
2401
+ 'bulletList', 'orderedList', 'taskList', '|',
2402
+ 'blockquote', 'codeBlock', 'horizontalRule', '|',
2403
+ 'link', 'entityLink',
2404
+ ],
2405
+ compact: [
2406
+ 'bold', 'italic', 'code', '|',
2407
+ 'bulletList', 'orderedList', '|',
2408
+ 'link', 'entityLink',
2409
+ ],
2410
+ };
2411
+
2412
+ /** Parses a live `flyos:<appId>.<entity>/<id>` href into `{kind, id}`. */
2413
+ const FLYOS_HREF_RE = /^flyos:([^/]+)\/(.+)$/;
2414
+ /** Simple command buttons (everything except link / entityLink / divider).
2415
+ * `cmd` runs on the focused chain; `active` is the `isActive` probe. */
2416
+ const TOOLBAR_BUTTONS = {
2417
+ undo: { icon: 'pi-undo', labelKey: 'common.label.undo', cmd: (e) => e.commands.undo() },
2418
+ redo: { icon: 'pi-refresh', labelKey: 'common.label.redo', cmd: (e) => e.commands.redo() },
2419
+ bold: { icon: '', labelKey: 'common.label.bold', cmd: (e) => e.chain().focus().toggleBold().run(), active: { name: 'bold' } },
2420
+ italic: { icon: '', labelKey: 'common.label.italic', cmd: (e) => e.chain().focus().toggleItalic().run(), active: { name: 'italic' } },
2421
+ underline: { icon: '', labelKey: 'common.label.underline', cmd: (e) => e.chain().focus().toggleUnderline().run(), active: { name: 'underline' } },
2422
+ strike: { icon: '', labelKey: 'common.label.strikethrough', cmd: (e) => e.chain().focus().toggleStrike().run(), active: { name: 'strike' } },
2423
+ code: { icon: 'pi-code', labelKey: 'common.label.inline_code', cmd: (e) => e.chain().focus().toggleCode().run(), active: { name: 'code' } },
2424
+ codeBlock: { icon: 'pi-file-edit', labelKey: 'common.label.code_block', cmd: (e) => e.chain().focus().toggleCodeBlock().run(), active: { name: 'codeBlock' } },
2425
+ blockquote: { icon: 'pi-comment', labelKey: 'common.label.blockquote', cmd: (e) => e.chain().focus().toggleBlockquote().run(), active: { name: 'blockquote' } },
2426
+ bulletList: { icon: 'pi-list', labelKey: 'common.label.bullet_list', cmd: (e) => e.chain().focus().toggleBulletList().run(), active: { name: 'bulletList' } },
2427
+ orderedList: { icon: 'pi-sort-numeric-down', labelKey: 'common.label.numbered_list', cmd: (e) => e.chain().focus().toggleOrderedList().run(), active: { name: 'orderedList' } },
2428
+ taskList: { icon: 'pi-check-square', labelKey: 'common.label.task_list', cmd: (e) => e.chain().focus().toggleTaskList().run(), active: { name: 'taskList' } },
2429
+ horizontalRule: { icon: 'pi-minus', labelKey: 'common.label.horizontal_rule', cmd: (e) => e.chain().focus().setHorizontalRule().run() },
2430
+ h1: { icon: '', labelKey: 'common.label.heading1', cmd: (e) => e.chain().focus().toggleHeading({ level: 1 }).run(), active: { name: 'heading', attrs: { level: 1 } } },
2431
+ h2: { icon: '', labelKey: 'common.label.heading2', cmd: (e) => e.chain().focus().toggleHeading({ level: 2 }).run(), active: { name: 'heading', attrs: { level: 2 } } },
2432
+ h3: { icon: '', labelKey: 'common.label.heading3', cmd: (e) => e.chain().focus().toggleHeading({ level: 3 }).run(), active: { name: 'heading', attrs: { level: 3 } } },
2433
+ };
2434
+ /**
2435
+ * Shared rich-text/markdown editor — the single Tiptap-backed editor for FlyOS
2436
+ * apps (notes, task comments, admin) and Business Apps (Circles). Emits a
2437
+ * Markdown string via `ControlValueAccessor`, so it slots into reactive forms
2438
+ * or `[(ngModel)]` with no orchestration.
2439
+ *
2440
+ * Bakes in the platform's `flyos:` entity-deep-link contract: the Link mark
2441
+ * whitelists the `flyos` scheme (so authored/pasted deep links aren't blanked),
2442
+ * clicks on a resolvable `flyos:` anchor launch the owning app via the
2443
+ * host-provided {@link ENTITY_LINK_LAUNCHER}, and the optional "entity link"
2444
+ * toolbar button opens the shared {@link EntityLookupComponent} to insert one.
2445
+ */
2446
+ class FlyMarkdownEditorComponent {
2447
+ cdr = inject(ChangeDetectorRef);
2448
+ lookupRegistry = inject(AgentLookupRegistry);
2449
+ launcher = inject(ENTITY_LINK_LAUNCHER, { optional: true });
2450
+ /** Toolbar preset name or an explicit ordered item list. */
2451
+ toolbar = input('full', ...(ngDevMode ? [{ debugName: "toolbar" }] : /* istanbul ignore next */ []));
2452
+ /** Show the "insert in-app link" (entity-lookup) toolbar button. */
2453
+ enableEntityLink = input(true, ...(ngDevMode ? [{ debugName: "enableEntityLink" }] : /* istanbul ignore next */ []));
2454
+ /** Direction the entity-lookup dropdown opens. `down` (default) suits a
2455
+ * top toolbar (notes); `above` suits a bottom-anchored composer (task
2456
+ * comments) so the panel doesn't run off the viewport. */
2457
+ entityLinkPlacement = input('down', ...(ngDevMode ? [{ debugName: "entityLinkPlacement" }] : /* istanbul ignore next */ []));
2458
+ placeholder = input('', ...(ngDevMode ? [{ debugName: "placeholder" }] : /* istanbul ignore next */ []));
2459
+ readonly = input(false, ...(ngDevMode ? [{ debugName: "readonly" }] : /* istanbul ignore next */ []));
2460
+ ariaLabel = input('', ...(ngDevMode ? [{ debugName: "ariaLabel" }] : /* istanbul ignore next */ []));
2461
+ /** Fired on Ctrl/Cmd+Enter (host can submit a comment, etc.). */
2462
+ submitShortcut = output();
2463
+ // CVA plumbing.
2464
+ onChange = () => undefined;
2465
+ onTouched = () => undefined;
2466
+ currentValue = '';
2467
+ // Toolbar reactive state.
2468
+ activeStates = signal({}, ...(ngDevMode ? [{ debugName: "activeStates" }] : /* istanbul ignore next */ []));
2469
+ canUndo = signal(false, ...(ngDevMode ? [{ debugName: "canUndo" }] : /* istanbul ignore next */ []));
2470
+ canRedo = signal(false, ...(ngDevMode ? [{ debugName: "canRedo" }] : /* istanbul ignore next */ []));
2471
+ // Link (URL) popover.
2472
+ showLinkPopover = signal(false, ...(ngDevMode ? [{ debugName: "showLinkPopover" }] : /* istanbul ignore next */ []));
2473
+ linkInputValue = signal('', ...(ngDevMode ? [{ debugName: "linkInputValue" }] : /* istanbul ignore next */ []));
2474
+ // Entity-link picker.
2475
+ showEntityLookup = signal(false, ...(ngDevMode ? [{ debugName: "showEntityLookup" }] : /* istanbul ignore next */ []));
2476
+ entityLookupDescriptors = computed(() => this.lookupRegistry.all().filter((d) => !!d.deepLinkRoute), ...(ngDevMode ? [{ debugName: "entityLookupDescriptors" }] : /* istanbul ignore next */ []));
2477
+ /** Resolved toolbar items (preset → array), entityLink stripped when disabled. */
2478
+ toolbarItems = computed(() => {
2479
+ const t = this.toolbar();
2480
+ const items = typeof t === 'string' ? MARKDOWN_TOOLBAR_PRESETS[t] : t;
2481
+ return this.enableEntityLink() ? items : items.filter((i) => i !== 'entityLink');
2482
+ }, ...(ngDevMode ? [{ debugName: "toolbarItems" }] : /* istanbul ignore next */ []));
2483
+ editor;
2484
+ constructor() {
2485
+ this.editor = new Editor({
2486
+ extensions: [
2487
+ StarterKit.configure({
2488
+ // Whitelist the platform deep-link scheme so Tiptap keeps the href
2489
+ // instead of blanking it; openOnClick false because flyos: is
2490
+ // non-routable — handleClick launches in-shell below.
2491
+ link: { openOnClick: false, protocols: ['flyos'] },
2492
+ }),
2493
+ Markdown,
2494
+ TaskList,
2495
+ TaskItem.configure({ nested: true }),
2496
+ ],
2497
+ editorProps: {
2498
+ attributes: { class: 'fly-md-editor__prosemirror', spellcheck: 'true' },
2499
+ handleClick: (_view, _pos, event) => this.handleFlyosClick(event),
2500
+ handleKeyDown: (_view, event) => {
2501
+ if ((event.metaKey || event.ctrlKey) && event.key === 'Enter') {
2502
+ event.preventDefault();
2503
+ this.submitShortcut.emit();
2504
+ return true;
2505
+ }
2506
+ return false;
2507
+ },
2508
+ },
2509
+ onUpdate: () => {
2510
+ this.currentValue = this.editor.getMarkdown();
2511
+ this.onChange(this.currentValue);
2512
+ this.refreshState();
2513
+ this.cdr.markForCheck();
2514
+ },
2515
+ onSelectionUpdate: () => {
2516
+ this.refreshState();
2517
+ this.cdr.markForCheck();
2518
+ },
2519
+ onBlur: () => this.onTouched(),
2520
+ });
2521
+ }
2522
+ ngOnDestroy() {
2523
+ this.editor.destroy();
2524
+ }
2525
+ // ── ControlValueAccessor ────────────────────────────────────────────────
2526
+ writeValue(value) {
2527
+ this.currentValue = value ?? '';
2528
+ // contentType:'markdown' parses the string as Markdown; emitUpdate:false so
2529
+ // hydrating the model doesn't echo back through onChange.
2530
+ this.editor.commands.setContent(this.currentValue, { emitUpdate: false, contentType: 'markdown' });
2531
+ this.refreshState();
2532
+ this.cdr.markForCheck();
2533
+ }
2534
+ registerOnChange(fn) { this.onChange = fn; }
2535
+ registerOnTouched(fn) { this.onTouched = fn; }
2536
+ setDisabledState(isDisabled) { this.editor.setEditable(!isDisabled); }
2537
+ // ── Imperative reads (for hosts that persist a rendered HTML snapshot
2538
+ // alongside the markdown CVA value, e.g. notes' contentHtml) ───────────
2539
+ /** Current markdown (same as the CVA value). */
2540
+ getMarkdown() { return this.editor.getMarkdown(); }
2541
+ /** Current rendered HTML snapshot. */
2542
+ getHtml() { return this.editor.getHTML(); }
2543
+ /** Focus the editor surface. */
2544
+ focus() { this.editor.commands.focus(); }
2545
+ // ── Toolbar ──────────────────────────────────────────────────────────────
2546
+ button(id) {
2547
+ const b = TOOLBAR_BUTTONS[id];
2548
+ return b ? { icon: b.icon, labelKey: b.labelKey } : null;
2549
+ }
2550
+ /** Short visual label for buttons rendered as text (B / I / U / S / H1…). */
2551
+ textLabel(id) {
2552
+ switch (id) {
2553
+ case 'bold': return 'B';
2554
+ case 'italic': return 'I';
2555
+ case 'underline': return 'U';
2556
+ case 'strike': return 'S';
2557
+ case 'h1': return 'H1';
2558
+ case 'h2': return 'H2';
2559
+ case 'h3': return 'H3';
2560
+ default: return null;
2561
+ }
2562
+ }
2563
+ run(id) {
2564
+ const b = TOOLBAR_BUTTONS[id];
2565
+ if (!b)
2566
+ return;
2567
+ b.cmd(this.editor);
2568
+ this.refreshState();
2569
+ this.cdr.markForCheck();
2570
+ }
2571
+ isActive(id) {
2572
+ return this.activeStates()[id] ?? false;
2573
+ }
2574
+ disabled(id) {
2575
+ if (id === 'undo')
2576
+ return !this.canUndo();
2577
+ if (id === 'redo')
2578
+ return !this.canRedo();
2579
+ return false;
2580
+ }
2581
+ refreshState() {
2582
+ this.canUndo.set(this.editor.can().undo());
2583
+ this.canRedo.set(this.editor.can().redo());
2584
+ const states = {};
2585
+ for (const [id, b] of Object.entries(TOOLBAR_BUTTONS)) {
2586
+ if (b.active)
2587
+ states[id] = this.editor.isActive(b.active.name, b.active.attrs ?? {});
2588
+ }
2589
+ states['link'] = this.editor.isActive('link');
2590
+ this.activeStates.set(states);
2591
+ }
2592
+ // ── Link (URL) popover ─────────────────────────────────────────────────
2593
+ openLinkPopover() {
2594
+ this.showEntityLookup.set(false);
2595
+ this.linkInputValue.set(this.editor.getAttributes('link')['href'] ?? '');
2596
+ this.showLinkPopover.set(true);
2597
+ }
2598
+ onLinkInput(value) { this.linkInputValue.set(value); }
2599
+ commitLink() {
2600
+ const url = this.linkInputValue().trim();
2601
+ const chain = this.editor.chain().focus().extendMarkRange('link');
2602
+ if (url === '')
2603
+ chain.unsetLink().run();
2604
+ else
2605
+ chain.setLink({ href: url }).run();
2606
+ this.showLinkPopover.set(false);
2607
+ this.refreshState();
2608
+ this.cdr.markForCheck();
2609
+ }
2610
+ cancelLink() { this.showLinkPopover.set(false); }
2611
+ onLinkKeydown(event) {
2612
+ if (event.key === 'Enter') {
2613
+ event.preventDefault();
2614
+ this.commitLink();
2615
+ }
2616
+ if (event.key === 'Escape') {
2617
+ this.cancelLink();
2618
+ }
2619
+ }
2620
+ // ── Entity-link picker ─────────────────────────────────────────────────
2621
+ toggleEntityLookup() {
2622
+ this.showLinkPopover.set(false);
2623
+ this.showEntityLookup.update((open) => !open);
2624
+ }
2625
+ closeEntityLookup() { this.showEntityLookup.set(false); }
2626
+ onEntityLinkSelected(sel) {
2627
+ this.editor
2628
+ .chain()
2629
+ .focus()
2630
+ .insertContent({ type: 'text', text: sel.label, marks: [{ type: 'link', attrs: { href: sel.href } }] })
2631
+ .insertContent(' ')
2632
+ .run();
2633
+ this.showEntityLookup.set(false);
2634
+ this.refreshState();
2635
+ this.cdr.markForCheck();
2636
+ }
2637
+ // ── flyos: click → launch ──────────────────────────────────────────────
2638
+ /** True (click handled) when a resolvable `flyos:` anchor was launched. */
2639
+ handleFlyosClick(event) {
2640
+ const anchor = event.target?.closest('a');
2641
+ const href = anchor?.getAttribute('href');
2642
+ if (!href)
2643
+ return false;
2644
+ const m = FLYOS_HREF_RE.exec(href);
2645
+ if (!m)
2646
+ return false;
2647
+ event.preventDefault();
2648
+ const [, kind, id] = m;
2649
+ const target = this.lookupRegistry.resolveDeepLink(kind, id);
2650
+ if (!target || !this.launcher)
2651
+ return false;
2652
+ this.launcher({ appId: target.appId, route: target.route, kind, id });
2653
+ return true;
2654
+ }
2655
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.5", ngImport: i0, type: FlyMarkdownEditorComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
2656
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.5", type: FlyMarkdownEditorComponent, isStandalone: true, selector: "fly-markdown-editor", inputs: { toolbar: { classPropertyName: "toolbar", publicName: "toolbar", isSignal: true, isRequired: false, transformFunction: null }, enableEntityLink: { classPropertyName: "enableEntityLink", publicName: "enableEntityLink", isSignal: true, isRequired: false, transformFunction: null }, entityLinkPlacement: { classPropertyName: "entityLinkPlacement", publicName: "entityLinkPlacement", isSignal: true, isRequired: false, transformFunction: null }, placeholder: { classPropertyName: "placeholder", publicName: "placeholder", isSignal: true, isRequired: false, transformFunction: null }, readonly: { classPropertyName: "readonly", publicName: "readonly", isSignal: true, isRequired: false, transformFunction: null }, ariaLabel: { classPropertyName: "ariaLabel", publicName: "ariaLabel", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { submitShortcut: "submitShortcut" }, providers: [
2657
+ { provide: NG_VALUE_ACCESSOR, useExisting: forwardRef(() => FlyMarkdownEditorComponent), multi: true },
2658
+ ], ngImport: i0, template: "<div class=\"fly-md-editor\" [class.fly-md-editor--entity-above]=\"entityLinkPlacement() === 'above'\">\n <div class=\"fly-md-editor__toolbar\" role=\"toolbar\" [attr.aria-label]=\"('common.label.formatting_toolbar' | translate)\">\n @for (item of toolbarItems(); track $index) {\n @switch (item) {\n @case ('|') {\n <span class=\"fly-md-editor__divider\" aria-hidden=\"true\"></span>\n }\n @case ('link') {\n <div class=\"fly-md-editor__link-wrap\">\n <button type=\"button\" class=\"fly-md-editor__btn\" [class.is-active]=\"isActive('link')\"\n (click)=\"openLinkPopover()\"\n [title]=\"'common.label.link' | translate\" [attr.aria-label]=\"'common.label.link' | translate\">\n <i class=\"pi pi-link\" aria-hidden=\"true\"></i>\n </button>\n @if (showLinkPopover()) {\n <div class=\"fly-md-editor__link-popover\" role=\"dialog\" [attr.aria-label]=\"'common.label.link' | translate\">\n <input class=\"fly-md-editor__link-input\" type=\"url\"\n [value]=\"linkInputValue()\" (input)=\"onLinkInput($any($event.target).value)\"\n (keydown)=\"onLinkKeydown($event)\"\n [placeholder]=\"'common.label.https' | translate\"\n [attr.aria-label]=\"'common.label.link' | translate\" />\n <button type=\"button\" class=\"fly-md-editor__link-ok\" (click)=\"commitLink()\"\n [title]=\"'common.label.apply' | translate\"><i class=\"pi pi-check\" aria-hidden=\"true\"></i></button>\n <button type=\"button\" class=\"fly-md-editor__link-cancel\" (click)=\"cancelLink()\"\n [title]=\"'common.action.cancel' | translate\"><i class=\"pi pi-times\" aria-hidden=\"true\"></i></button>\n </div>\n }\n </div>\n }\n @case ('entityLink') {\n <div class=\"fly-md-editor__entity-wrap\">\n <button type=\"button\" class=\"fly-md-editor__btn\" [class.is-active]=\"showEntityLookup()\"\n (click)=\"toggleEntityLookup()\" [attr.aria-expanded]=\"showEntityLookup()\"\n [title]=\"'common.label.insert_entity_link' | translate\" [attr.aria-label]=\"'common.label.insert_entity_link' | translate\">\n <i class=\"pi pi-bookmark\" aria-hidden=\"true\"></i>\n </button>\n @if (showEntityLookup()) {\n <fly-entity-lookup\n [placement]=\"entityLinkPlacement()\"\n [descriptors]=\"entityLookupDescriptors()\"\n (entityLinkSelected)=\"onEntityLinkSelected($event)\"\n (dismiss)=\"closeEntityLookup()\" />\n }\n </div>\n }\n @default {\n <button type=\"button\" class=\"fly-md-editor__btn\"\n [class.is-active]=\"isActive(item)\" [disabled]=\"disabled(item)\"\n (click)=\"run(item)\"\n [title]=\"(button(item)?.labelKey ?? '') | translate\"\n [attr.aria-label]=\"(button(item)?.labelKey ?? '') | translate\">\n @if (textLabel(item); as txt) {\n <span class=\"fly-md-editor__btn-text\">{{ txt }}</span>\n } @else {\n <i class=\"pi {{ button(item)?.icon }}\" aria-hidden=\"true\"></i>\n }\n </button>\n }\n }\n }\n </div>\n\n <div class=\"fly-md-editor__body\" tiptap [editor]=\"editor\"\n [attr.aria-label]=\"ariaLabel()\" [attr.data-placeholder]=\"placeholder()\"></div>\n</div>\n", styles: [":host{display:block}.fly-md-editor{display:flex;flex-direction:column;min-block-size:0;position:relative}.fly-md-editor--entity-above .fly-md-editor__entity-wrap{position:static}.fly-md-editor__toolbar{display:flex;align-items:center;flex-wrap:wrap;gap:2px;padding-block:4px}.fly-md-editor__divider{inline-size:1px;block-size:18px;margin-inline:4px;background:var(--surface-border, rgba(255, 255, 255, .12))}.fly-md-editor__btn{min-inline-size:28px;block-size:28px;padding-inline:7px;border-radius:6px;border:1px solid transparent;background:transparent;color:var(--text-color-secondary, #6e6e73);font-size:12px;line-height:1;cursor:pointer;display:inline-flex;align-items:center;justify-content:center;transition:background .12s ease,color .12s ease}.fly-md-editor__btn:hover:not(:disabled){background:var(--surface-hover, rgba(0, 0, 0, .05))}.fly-md-editor__btn.is-active{background:var(--primary-color-subtle, rgba(0, 113, 227, .12));color:var(--primary-color, #0071e3);border-color:var(--primary-color-border, rgba(0, 113, 227, .25))}.fly-md-editor__btn:disabled{opacity:.4;cursor:default}.fly-md-editor__btn i.pi{font-size:13px}.fly-md-editor__btn-text{font-weight:600;font-style:normal}@media(prefers-reduced-motion:reduce){.fly-md-editor__btn{transition:none}}.fly-md-editor__link-wrap,.fly-md-editor__entity-wrap{position:relative;display:inline-flex}.fly-md-editor__link-popover{position:absolute;inset-block-start:calc(100% + 4px);inset-inline-start:0;z-index:1000;display:flex;gap:4px;padding:4px;background:var(--surface-elevated, #fff);border:1px solid var(--surface-border, rgba(0, 0, 0, .12));border-radius:6px;box-shadow:0 4px 12px #0000001f}.fly-md-editor__link-input{inline-size:200px;padding:4px 6px;font-size:12px;border:1px solid var(--surface-border, rgba(0, 0, 0, .12));border-radius:4px;outline:none;background:transparent;color:inherit}.fly-md-editor__link-input:focus{border-color:var(--primary-color, #0071e3)}.fly-md-editor__link-ok,.fly-md-editor__link-cancel{inline-size:24px;block-size:24px;border-radius:4px;border:none;background:transparent;cursor:pointer;color:var(--text-color-secondary, #6e6e73)}.fly-md-editor__link-ok:hover,.fly-md-editor__link-cancel:hover{background:var(--surface-hover, rgba(0, 0, 0, .06))}.fly-md-editor__body{flex:1;min-block-size:56px;overflow-y:auto;padding:8px 4px;font-size:13px;line-height:1.55;color:var(--text-color, inherit);cursor:text}.fly-md-editor__body:focus-within{outline:none}::ng-deep .fly-md-editor__prosemirror{outline:none}::ng-deep .fly-md-editor__prosemirror>*{margin-block:0 .5em}::ng-deep .fly-md-editor__prosemirror>:last-child{margin-block-end:0}::ng-deep .fly-md-editor__prosemirror h1{font-size:1.4em;font-weight:700}::ng-deep .fly-md-editor__prosemirror h2{font-size:1.2em;font-weight:700}::ng-deep .fly-md-editor__prosemirror h3{font-size:1.05em;font-weight:700}::ng-deep .fly-md-editor__prosemirror ul,::ng-deep .fly-md-editor__prosemirror ol{padding-inline-start:1.4em}::ng-deep .fly-md-editor__prosemirror a{color:var(--primary-color, #0071e3);text-decoration:underline;cursor:pointer}::ng-deep .fly-md-editor__prosemirror code{background:var(--surface-hover, rgba(0, 0, 0, .06));padding:1px 4px;border-radius:4px;font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;font-size:.95em}::ng-deep .fly-md-editor__prosemirror pre{background:var(--surface-hover, rgba(0, 0, 0, .06));border-radius:6px;padding:8px 10px;overflow-x:auto}::ng-deep .fly-md-editor__prosemirror pre code{background:transparent;padding:0}::ng-deep .fly-md-editor__prosemirror blockquote{margin-inline:0;padding-inline-start:10px;border-inline-start:3px solid var(--surface-border, rgba(0, 0, 0, .15));color:var(--text-color-secondary, #6e6e73)}::ng-deep .fly-md-editor__prosemirror ul[data-type=taskList]{list-style:none;padding-inline-start:.2em}::ng-deep .fly-md-editor__prosemirror ul[data-type=taskList] li{display:flex;align-items:flex-start;gap:6px}::ng-deep .fly-md-editor__prosemirror ul[data-type=taskList] li>label{margin-block-start:.15em}::ng-deep .fly-md-editor__prosemirror p.is-editor-empty:first-child:before{content:attr(data-placeholder);color:var(--text-color-secondary, rgba(0, 0, 0, .4));float:inline-start;pointer-events:none;block-size:0}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "ngmodule", type: FormsModule }, { kind: "directive", type: TiptapEditorDirective, selector: "tiptap[editor], [tiptap][editor], tiptap-editor[editor], [tiptapEditor][editor]", inputs: ["editor", "outputFormat"] }, { kind: "component", type: EntityLookupComponent, selector: "fly-entity-lookup", inputs: ["descriptors", "initialEntity", "initialQuery", "listboxId", "placement"], outputs: ["pick", "entityLinkSelected", "dismiss"] }, { kind: "pipe", type: TranslatePipe, name: "translate" }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
2659
+ }
2660
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.5", ngImport: i0, type: FlyMarkdownEditorComponent, decorators: [{
2661
+ type: Component,
2662
+ args: [{ selector: 'fly-markdown-editor', standalone: true, imports: [CommonModule, FormsModule, TiptapEditorDirective, TranslatePipe, EntityLookupComponent], changeDetection: ChangeDetectionStrategy.OnPush, providers: [
2663
+ { provide: NG_VALUE_ACCESSOR, useExisting: forwardRef(() => FlyMarkdownEditorComponent), multi: true },
2664
+ ], template: "<div class=\"fly-md-editor\" [class.fly-md-editor--entity-above]=\"entityLinkPlacement() === 'above'\">\n <div class=\"fly-md-editor__toolbar\" role=\"toolbar\" [attr.aria-label]=\"('common.label.formatting_toolbar' | translate)\">\n @for (item of toolbarItems(); track $index) {\n @switch (item) {\n @case ('|') {\n <span class=\"fly-md-editor__divider\" aria-hidden=\"true\"></span>\n }\n @case ('link') {\n <div class=\"fly-md-editor__link-wrap\">\n <button type=\"button\" class=\"fly-md-editor__btn\" [class.is-active]=\"isActive('link')\"\n (click)=\"openLinkPopover()\"\n [title]=\"'common.label.link' | translate\" [attr.aria-label]=\"'common.label.link' | translate\">\n <i class=\"pi pi-link\" aria-hidden=\"true\"></i>\n </button>\n @if (showLinkPopover()) {\n <div class=\"fly-md-editor__link-popover\" role=\"dialog\" [attr.aria-label]=\"'common.label.link' | translate\">\n <input class=\"fly-md-editor__link-input\" type=\"url\"\n [value]=\"linkInputValue()\" (input)=\"onLinkInput($any($event.target).value)\"\n (keydown)=\"onLinkKeydown($event)\"\n [placeholder]=\"'common.label.https' | translate\"\n [attr.aria-label]=\"'common.label.link' | translate\" />\n <button type=\"button\" class=\"fly-md-editor__link-ok\" (click)=\"commitLink()\"\n [title]=\"'common.label.apply' | translate\"><i class=\"pi pi-check\" aria-hidden=\"true\"></i></button>\n <button type=\"button\" class=\"fly-md-editor__link-cancel\" (click)=\"cancelLink()\"\n [title]=\"'common.action.cancel' | translate\"><i class=\"pi pi-times\" aria-hidden=\"true\"></i></button>\n </div>\n }\n </div>\n }\n @case ('entityLink') {\n <div class=\"fly-md-editor__entity-wrap\">\n <button type=\"button\" class=\"fly-md-editor__btn\" [class.is-active]=\"showEntityLookup()\"\n (click)=\"toggleEntityLookup()\" [attr.aria-expanded]=\"showEntityLookup()\"\n [title]=\"'common.label.insert_entity_link' | translate\" [attr.aria-label]=\"'common.label.insert_entity_link' | translate\">\n <i class=\"pi pi-bookmark\" aria-hidden=\"true\"></i>\n </button>\n @if (showEntityLookup()) {\n <fly-entity-lookup\n [placement]=\"entityLinkPlacement()\"\n [descriptors]=\"entityLookupDescriptors()\"\n (entityLinkSelected)=\"onEntityLinkSelected($event)\"\n (dismiss)=\"closeEntityLookup()\" />\n }\n </div>\n }\n @default {\n <button type=\"button\" class=\"fly-md-editor__btn\"\n [class.is-active]=\"isActive(item)\" [disabled]=\"disabled(item)\"\n (click)=\"run(item)\"\n [title]=\"(button(item)?.labelKey ?? '') | translate\"\n [attr.aria-label]=\"(button(item)?.labelKey ?? '') | translate\">\n @if (textLabel(item); as txt) {\n <span class=\"fly-md-editor__btn-text\">{{ txt }}</span>\n } @else {\n <i class=\"pi {{ button(item)?.icon }}\" aria-hidden=\"true\"></i>\n }\n </button>\n }\n }\n }\n </div>\n\n <div class=\"fly-md-editor__body\" tiptap [editor]=\"editor\"\n [attr.aria-label]=\"ariaLabel()\" [attr.data-placeholder]=\"placeholder()\"></div>\n</div>\n", styles: [":host{display:block}.fly-md-editor{display:flex;flex-direction:column;min-block-size:0;position:relative}.fly-md-editor--entity-above .fly-md-editor__entity-wrap{position:static}.fly-md-editor__toolbar{display:flex;align-items:center;flex-wrap:wrap;gap:2px;padding-block:4px}.fly-md-editor__divider{inline-size:1px;block-size:18px;margin-inline:4px;background:var(--surface-border, rgba(255, 255, 255, .12))}.fly-md-editor__btn{min-inline-size:28px;block-size:28px;padding-inline:7px;border-radius:6px;border:1px solid transparent;background:transparent;color:var(--text-color-secondary, #6e6e73);font-size:12px;line-height:1;cursor:pointer;display:inline-flex;align-items:center;justify-content:center;transition:background .12s ease,color .12s ease}.fly-md-editor__btn:hover:not(:disabled){background:var(--surface-hover, rgba(0, 0, 0, .05))}.fly-md-editor__btn.is-active{background:var(--primary-color-subtle, rgba(0, 113, 227, .12));color:var(--primary-color, #0071e3);border-color:var(--primary-color-border, rgba(0, 113, 227, .25))}.fly-md-editor__btn:disabled{opacity:.4;cursor:default}.fly-md-editor__btn i.pi{font-size:13px}.fly-md-editor__btn-text{font-weight:600;font-style:normal}@media(prefers-reduced-motion:reduce){.fly-md-editor__btn{transition:none}}.fly-md-editor__link-wrap,.fly-md-editor__entity-wrap{position:relative;display:inline-flex}.fly-md-editor__link-popover{position:absolute;inset-block-start:calc(100% + 4px);inset-inline-start:0;z-index:1000;display:flex;gap:4px;padding:4px;background:var(--surface-elevated, #fff);border:1px solid var(--surface-border, rgba(0, 0, 0, .12));border-radius:6px;box-shadow:0 4px 12px #0000001f}.fly-md-editor__link-input{inline-size:200px;padding:4px 6px;font-size:12px;border:1px solid var(--surface-border, rgba(0, 0, 0, .12));border-radius:4px;outline:none;background:transparent;color:inherit}.fly-md-editor__link-input:focus{border-color:var(--primary-color, #0071e3)}.fly-md-editor__link-ok,.fly-md-editor__link-cancel{inline-size:24px;block-size:24px;border-radius:4px;border:none;background:transparent;cursor:pointer;color:var(--text-color-secondary, #6e6e73)}.fly-md-editor__link-ok:hover,.fly-md-editor__link-cancel:hover{background:var(--surface-hover, rgba(0, 0, 0, .06))}.fly-md-editor__body{flex:1;min-block-size:56px;overflow-y:auto;padding:8px 4px;font-size:13px;line-height:1.55;color:var(--text-color, inherit);cursor:text}.fly-md-editor__body:focus-within{outline:none}::ng-deep .fly-md-editor__prosemirror{outline:none}::ng-deep .fly-md-editor__prosemirror>*{margin-block:0 .5em}::ng-deep .fly-md-editor__prosemirror>:last-child{margin-block-end:0}::ng-deep .fly-md-editor__prosemirror h1{font-size:1.4em;font-weight:700}::ng-deep .fly-md-editor__prosemirror h2{font-size:1.2em;font-weight:700}::ng-deep .fly-md-editor__prosemirror h3{font-size:1.05em;font-weight:700}::ng-deep .fly-md-editor__prosemirror ul,::ng-deep .fly-md-editor__prosemirror ol{padding-inline-start:1.4em}::ng-deep .fly-md-editor__prosemirror a{color:var(--primary-color, #0071e3);text-decoration:underline;cursor:pointer}::ng-deep .fly-md-editor__prosemirror code{background:var(--surface-hover, rgba(0, 0, 0, .06));padding:1px 4px;border-radius:4px;font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;font-size:.95em}::ng-deep .fly-md-editor__prosemirror pre{background:var(--surface-hover, rgba(0, 0, 0, .06));border-radius:6px;padding:8px 10px;overflow-x:auto}::ng-deep .fly-md-editor__prosemirror pre code{background:transparent;padding:0}::ng-deep .fly-md-editor__prosemirror blockquote{margin-inline:0;padding-inline-start:10px;border-inline-start:3px solid var(--surface-border, rgba(0, 0, 0, .15));color:var(--text-color-secondary, #6e6e73)}::ng-deep .fly-md-editor__prosemirror ul[data-type=taskList]{list-style:none;padding-inline-start:.2em}::ng-deep .fly-md-editor__prosemirror ul[data-type=taskList] li{display:flex;align-items:flex-start;gap:6px}::ng-deep .fly-md-editor__prosemirror ul[data-type=taskList] li>label{margin-block-start:.15em}::ng-deep .fly-md-editor__prosemirror p.is-editor-empty:first-child:before{content:attr(data-placeholder);color:var(--text-color-secondary, rgba(0, 0, 0, .4));float:inline-start;pointer-events:none;block-size:0}\n"] }]
2665
+ }], ctorParameters: () => [], propDecorators: { toolbar: [{ type: i0.Input, args: [{ isSignal: true, alias: "toolbar", required: false }] }], enableEntityLink: [{ type: i0.Input, args: [{ isSignal: true, alias: "enableEntityLink", required: false }] }], entityLinkPlacement: [{ type: i0.Input, args: [{ isSignal: true, alias: "entityLinkPlacement", required: false }] }], placeholder: [{ type: i0.Input, args: [{ isSignal: true, alias: "placeholder", required: false }] }], readonly: [{ type: i0.Input, args: [{ isSignal: true, alias: "readonly", required: false }] }], ariaLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "ariaLabel", required: false }] }], submitShortcut: [{ type: i0.Output, args: ["submitShortcut"] }] } });
2666
+
1413
2667
  class ContextMenuComponent {
1414
2668
  menuEl;
1415
2669
  doc = inject(DOCUMENT);
@@ -3430,7 +4684,7 @@ class AgentCommandRegistry {
3430
4684
  * for reactive filtering.
3431
4685
  */
3432
4686
  visible(liveAppIds) {
3433
- const liveSignal = isSignal$1(liveAppIds) ? liveAppIds : signal(liveAppIds).asReadonly();
4687
+ const liveSignal = isSignal(liveAppIds) ? liveAppIds : signal(liveAppIds).asReadonly();
3434
4688
  return computed(() => {
3435
4689
  const live = liveSignal();
3436
4690
  return this._commands().filter((cmd) => {
@@ -3503,111 +4757,6 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.5", ngImpor
3503
4757
  args: [{ providedIn: 'root' }]
3504
4758
  }] });
3505
4759
  /** `isSignal` shim — narrows to either `Signal<T>` or a plain value. */
3506
- function isSignal$1(v) {
3507
- return typeof v === 'function';
3508
- }
3509
-
3510
- /**
3511
- * Singleton registry of entity lookups offered by the `/lookup` typeahead.
3512
- *
3513
- * Mirrors {@link AgentCommandRegistry} exactly — same federation-singleton story
3514
- * (`sharedMappings: ['@mohamedatia/fly-design-system']`), same id-collision
3515
- * "latest wins" contract, same disposable-handle ergonomics. Federated remotes
3516
- * register their lookupable entities at boot (Circles: scenario / trend / signal)
3517
- * and dispose on window close, so the picker only ever offers entities whose app
3518
- * is currently live.
3519
- *
3520
- * Storage is a signal store keyed on {@link LookupRegistration.entity}. Because
3521
- * `entity` is the collision key, an app re-registering the same entity replaces
3522
- * the prior descriptor; a stale handle's `dispose()` then no-ops.
3523
- */
3524
- class AgentLookupRegistry {
3525
- _lookups = signal([], ...(ngDevMode ? [{ debugName: "_lookups" }] : /* istanbul ignore next */ []));
3526
- /** All currently-registered lookups, in insertion order. */
3527
- all = this._lookups.asReadonly();
3528
- /**
3529
- * Lookups whose scope is `'global'` OR whose `scope.appId` is in the live app
3530
- * set. Recomputes when either the registry or `liveAppIds` changes — pass a
3531
- * `Signal<ReadonlySet<string>>` from the host's app-registry for reactive
3532
- * filtering, exactly like {@link AgentCommandRegistry.visible}.
3533
- */
3534
- visible(liveAppIds) {
3535
- const liveSignal = isSignal(liveAppIds)
3536
- ? liveAppIds
3537
- : signal(liveAppIds).asReadonly();
3538
- return computed(() => {
3539
- const live = liveSignal();
3540
- return this._lookups().filter((l) => {
3541
- if (l.scope === 'global')
3542
- return true;
3543
- return live.has(l.scope.appId);
3544
- });
3545
- });
3546
- }
3547
- /**
3548
- * Register one lookup. Returns a handle whose `dispose()` removes the row by
3549
- * `entity`. A later re-registration of the same entity makes the original
3550
- * handle's `dispose()` a no-op (the newer registration owns the row).
3551
- */
3552
- register(lookup) {
3553
- const generation = ++this._generation;
3554
- this._lookups.update((rows) => [
3555
- ...rows.filter((r) => r.entity !== lookup.entity),
3556
- lookup,
3557
- ]);
3558
- this._owners.set(lookup.entity, generation);
3559
- return {
3560
- dispose: () => {
3561
- if (this._owners.get(lookup.entity) === generation) {
3562
- this._owners.delete(lookup.entity);
3563
- this._lookups.update((rows) => rows.filter((r) => r.entity !== lookup.entity));
3564
- }
3565
- },
3566
- };
3567
- }
3568
- /**
3569
- * Bulk register. Rolls back on a duplicate entity WITHIN the input batch
3570
- * (throws before any row lands). Cross-batch duplicates against existing rows
3571
- * follow the standard "latest wins" rule and do NOT trigger rollback.
3572
- */
3573
- registerAll(lookups) {
3574
- const seen = new Set();
3575
- for (const l of lookups) {
3576
- if (seen.has(l.entity)) {
3577
- throw new Error(`AgentLookupRegistry.registerAll: duplicate entity "${l.entity}" in batch`);
3578
- }
3579
- seen.add(l.entity);
3580
- }
3581
- const handles = lookups.map((l) => this.register(l));
3582
- let disposed = false;
3583
- return {
3584
- dispose: () => {
3585
- if (disposed)
3586
- return;
3587
- disposed = true;
3588
- for (const h of handles)
3589
- h.dispose();
3590
- },
3591
- };
3592
- }
3593
- /** Tear down by entity. Idempotent. */
3594
- unregister(entity) {
3595
- if (this._owners.delete(entity)) {
3596
- this._lookups.update((rows) => rows.filter((r) => r.entity !== entity));
3597
- }
3598
- }
3599
- /** Monotonic counter; identifies which registration call currently owns each entity. */
3600
- _generation = 0;
3601
- /** entity → generation. Lets a stale handle's `dispose()` no-op after replacement. */
3602
- _owners = new Map();
3603
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.5", ngImport: i0, type: AgentLookupRegistry, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
3604
- static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.5", ngImport: i0, type: AgentLookupRegistry, providedIn: 'root' });
3605
- }
3606
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.5", ngImport: i0, type: AgentLookupRegistry, decorators: [{
3607
- type: Injectable,
3608
- args: [{ providedIn: 'root' }]
3609
- }] });
3610
- /** `isSignal` shim — narrows to either `Signal<T>` or a plain value. */
3611
4760
  function isSignal(v) {
3612
4761
  return typeof v === 'function';
3613
4762
  }
@@ -5330,6 +6479,9 @@ function isRtlLocaleEntry(entry) {
5330
6479
  * `.workflow/plans/agent-input-component.md` Phase 2b-1.
5331
6480
  * See docs/ExternalAppsGuide/03-frontend-app.md.
5332
6481
  */
6482
+ // ─── Federation singleton self-check (side effect; must run first) ───────────
6483
+ // Loud console.error if a federated remote forks its own DS copy instead of
6484
+ // binding to the shell's shared singleton. See the module doc.
5333
6485
  /**
5334
6486
  * Stable error codes returned by audience-aware backend endpoints. Mirror of
5335
6487
  * `Fly.Shared.Core.Audience.AudienceErrorCodes`. Compare on these strings, not on HTTP
@@ -5346,5 +6498,5 @@ const AUDIENCE_ERROR_CODES = {
5346
6498
  * Generated bundle index. Do not edit.
5347
6499
  */
5348
6500
 
5349
- export { AGENT_DRAG_MIME, AGENT_PAYLOAD_VERSION, APP_LOOKUP, AUDIENCE_ERROR_CODES, AUDIENCE_LIMITS, AUDIENCE_PRESETS, AUDIENCE_TERM_KINDS, AgentActionBus, AgentActionUnsupportedDispatchError, AgentCommandRegistry, AgentDropRegistry, AgentFlightAnimator, AgentLookupRegistry, AgentPayloadOversizeError, AudienceBuilderComponent, AuthService, ContextMenuComponent, DEFAULT_AGENT_PAYLOAD_LIMITS, DEFAULT_FLY_THEME_MODE, DialogResult, FLYOS_LAUNCH_EVENT, FLY_LOCALE_CATALOG, FLY_REMOTE_BASE_PATH, FLY_REMOTE_ROUTES, FLY_THEME_MODE_IDS, FLY_WINDOW_HELP_HINT_EVENT, FlyAgentDraggableDirective, FlyBlockUiComponent, FlyFileUploadComponent, FlyImageUploadComponent, FlyRemoteRouter, FlyRemoteRouterOutletComponent, FlySecureSrcDirective, FlyThemeService, FlyWindowHelpService, FlyosPendingLaunchesGlobalKey, I18nService, LAUNCH_CONTEXT, MessageBoxButtons, MessageBoxComponent, MessageBoxIcon, MessageBoxService, MockAuthService, RTL_LOCALE_SET, SHARE_ORG_CHART_SYSTEM_KEY_APPS, SHARE_ORG_CHART_SYSTEM_KEY_DEFAULT, SHARE_PANEL_DEFAULT_FILE_LEVELS, SUPPORTED_AGENT_PAYLOAD_VERSIONS, SharePanelComponent, SourceAppResolver, StandaloneWindowManagerService, TranslatePipe, WINDOW_DATA, WINDOW_HELP_HINT, WindowManagerService, findLocaleByDialect, findLocaleByPrefix, isRtlLocale, isRtlLocaleEntry, loadRemoteStyles, matchFlyRoutePattern, normalizeFlyTheme, trimAgentPayload, trimAgentString, unloadRemoteStyles, utf8ByteLength, validateAgentPayload };
6501
+ export { AGENT_DRAG_MIME, AGENT_PAYLOAD_VERSION, APP_LOOKUP, AUDIENCE_ERROR_CODES, AUDIENCE_LIMITS, AUDIENCE_PRESETS, AUDIENCE_TERM_KINDS, AgentActionBus, AgentActionUnsupportedDispatchError, AgentCommandRegistry, AgentDropRegistry, AgentFlightAnimator, AgentLookupRegistry, AgentPayloadOversizeError, AudienceBuilderComponent, AuthService, ContextMenuComponent, DEFAULT_AGENT_PAYLOAD_LIMITS, DEFAULT_FLY_THEME_MODE, DS_BASELINE_LOCALES, DialogResult, ENTITY_LINK_LAUNCHER, EntityLookupComponent, FLYOS_LAUNCH_EVENT, FLY_LOCALE_CATALOG, FLY_REMOTE_BASE_PATH, FLY_REMOTE_CONTEXT_EVENT, FLY_REMOTE_CONTEXT_STORE_KEY, FLY_REMOTE_ROUTES, FLY_THEME_MODE_IDS, FLY_WINDOW_HELP_HINT_EVENT, FlyAgentDraggableDirective, FlyBlockUiComponent, FlyFileUploadComponent, FlyImageUploadComponent, FlyMarkdownEditorComponent, FlyRemoteContextService, FlyRemoteRouter, FlyRemoteRouterOutletComponent, FlySecureSrcDirective, FlyThemeService, FlyWindowHelpService, FlyosPendingLaunchesGlobalKey, I18nService, LAUNCH_CONTEXT, MARKDOWN_TOOLBAR_PRESETS, MessageBoxButtons, MessageBoxComponent, MessageBoxIcon, MessageBoxService, MockAuthService, RTL_LOCALE_SET, SHARE_ORG_CHART_SYSTEM_KEY_APPS, SHARE_ORG_CHART_SYSTEM_KEY_DEFAULT, SHARE_PANEL_DEFAULT_FILE_LEVELS, SUPPORTED_AGENT_PAYLOAD_VERSIONS, SharePanelComponent, SourceAppResolver, StandaloneWindowManagerService, TranslatePipe, WINDOW_DATA, WINDOW_HELP_HINT, WindowManagerService, findLocaleByDialect, findLocaleByPrefix, isRtlLocale, isRtlLocaleEntry, loadRemoteStyles, matchFlyRoutePattern, normalizeFlyTheme, trimAgentPayload, trimAgentString, unloadRemoteStyles, utf8ByteLength, validateAgentPayload };
5350
6502
  //# sourceMappingURL=mohamedatia-fly-design-system.mjs.map