@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,
|
|
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
|
|
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
|
|
742
|
-
*
|
|
743
|
-
*
|
|
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
|
|
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
|