@franzzemen/brokenstock-shell 0.0.26 → 0.0.28
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,14 +1,17 @@
|
|
|
1
1
|
import * as i0 from '@angular/core';
|
|
2
|
-
import { InjectionToken, makeEnvironmentProviders, signal, computed, Injectable, inject, input, Directive,
|
|
2
|
+
import { InjectionToken, makeEnvironmentProviders, signal, computed, Injectable, inject, input, Directive, isDevMode, effect, untracked, HostListener, ViewChild, ChangeDetectionStrategy, Component, NgZone, viewChild, afterNextRender } from '@angular/core';
|
|
3
3
|
import * as i2$1 from '@angular/router';
|
|
4
|
-
import { Router, RouterLink, RouterLinkActive, RouterOutlet } from '@angular/router';
|
|
4
|
+
import { Router, RouterLink, RouterLinkActive, RouterOutlet, ActivatedRoute } from '@angular/router';
|
|
5
5
|
import { HttpHeader, BEARER_TOKEN_REGEX, USERNAME_REGEX, PASSWORD_REGEX } from '@franzzemen/utility';
|
|
6
6
|
import { validateCredentials } from '@franzzemen/identity';
|
|
7
7
|
import { HttpClient } from '@angular/common/http';
|
|
8
|
-
import { tap, catchError, from, switchMap, throwError } from 'rxjs';
|
|
8
|
+
import { tap, catchError, from, switchMap, throwError, firstValueFrom } from 'rxjs';
|
|
9
9
|
import * as i3 from '@angular/forms';
|
|
10
10
|
import { NG_VALIDATORS, FormsModule } from '@angular/forms';
|
|
11
|
-
import {
|
|
11
|
+
import { Marked } from 'marked';
|
|
12
|
+
import { gfmHeadingId } from 'marked-gfm-heading-id';
|
|
13
|
+
import fm from 'front-matter';
|
|
14
|
+
import { NgClass, DOCUMENT, DatePipe } from '@angular/common';
|
|
12
15
|
import { z } from 'zod/v4';
|
|
13
16
|
import * as i1 from '@angular/material/icon';
|
|
14
17
|
import { MatIconModule } from '@angular/material/icon';
|
|
@@ -62,10 +65,10 @@ class ThinkingService {
|
|
|
62
65
|
reset() {
|
|
63
66
|
this.#activeCount.set(0);
|
|
64
67
|
}
|
|
65
|
-
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.
|
|
66
|
-
static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.
|
|
68
|
+
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.13", ngImport: i0, type: ThinkingService, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
|
|
69
|
+
static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.13", ngImport: i0, type: ThinkingService, providedIn: 'root' }); }
|
|
67
70
|
}
|
|
68
|
-
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.
|
|
71
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.13", ngImport: i0, type: ThinkingService, decorators: [{
|
|
69
72
|
type: Injectable,
|
|
70
73
|
args: [{ providedIn: 'root' }]
|
|
71
74
|
}] });
|
|
@@ -233,21 +236,23 @@ class AuthenticationService {
|
|
|
233
236
|
this.#auth = 'Bearer ..';
|
|
234
237
|
this.#router.navigate(['/login']).catch(console.error);
|
|
235
238
|
}
|
|
236
|
-
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.
|
|
237
|
-
static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.
|
|
239
|
+
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.13", ngImport: i0, type: AuthenticationService, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
|
|
240
|
+
static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.13", ngImport: i0, type: AuthenticationService, providedIn: 'root' }); }
|
|
238
241
|
}
|
|
239
|
-
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.
|
|
242
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.13", ngImport: i0, type: AuthenticationService, decorators: [{
|
|
240
243
|
type: Injectable,
|
|
241
244
|
args: [{ providedIn: 'root' }]
|
|
242
245
|
}] });
|
|
243
246
|
|
|
244
|
-
const authGuard = () => {
|
|
247
|
+
const authGuard = (_route, state) => {
|
|
245
248
|
const authService = inject(AuthenticationService);
|
|
246
249
|
const router = inject(Router);
|
|
247
250
|
if (authService.authenticated()) {
|
|
248
251
|
return true;
|
|
249
252
|
}
|
|
250
|
-
return router.createUrlTree(['/login']
|
|
253
|
+
return router.createUrlTree(['/login'], {
|
|
254
|
+
queryParams: { returnUrl: state.url },
|
|
255
|
+
});
|
|
251
256
|
};
|
|
252
257
|
|
|
253
258
|
const guestGuard = () => {
|
|
@@ -308,10 +313,10 @@ class RegexValidatorDirective {
|
|
|
308
313
|
}
|
|
309
314
|
return null;
|
|
310
315
|
}
|
|
311
|
-
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.
|
|
312
|
-
static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "21.2.
|
|
316
|
+
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.13", ngImport: i0, type: RegexValidatorDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive }); }
|
|
317
|
+
static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "21.2.13", type: RegexValidatorDirective, isStandalone: true, selector: "[regexValidator]", inputs: { regex: { classPropertyName: "regex", publicName: "regex", isSignal: true, isRequired: false, transformFunction: null } }, providers: [{ provide: NG_VALIDATORS, useExisting: RegexValidatorDirective, multi: true }], ngImport: i0 }); }
|
|
313
318
|
}
|
|
314
|
-
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.
|
|
319
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.13", ngImport: i0, type: RegexValidatorDirective, decorators: [{
|
|
315
320
|
type: Directive,
|
|
316
321
|
args: [{
|
|
317
322
|
selector: '[regexValidator]',
|
|
@@ -319,15 +324,927 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.12", ngImpo
|
|
|
319
324
|
}]
|
|
320
325
|
}], propDecorators: { regex: [{ type: i0.Input, args: [{ isSignal: true, alias: "regex", required: false }] }] } });
|
|
321
326
|
|
|
327
|
+
/**
|
|
328
|
+
* Shared types for the FAQ help system.
|
|
329
|
+
*
|
|
330
|
+
* Manifest is generated by the build-time validator (E9) and read at runtime
|
|
331
|
+
* by FaqContentLoader (E1) and FaqService (E2).
|
|
332
|
+
*/
|
|
333
|
+
|
|
334
|
+
const FAQ_CONTENT_LOADER = new InjectionToken('FAQ_CONTENT_LOADER');
|
|
335
|
+
|
|
336
|
+
/**
|
|
337
|
+
* Stateless markdown renderer for FAQ content.
|
|
338
|
+
*
|
|
339
|
+
* Two responsibilities:
|
|
340
|
+
* 1. Split a `.md` file into `{ frontmatter, body }` (front-matter parser).
|
|
341
|
+
* 2. Render the body to HTML via marked with a context-aware link hook:
|
|
342
|
+
* - `faq:<key>` — in-modal nav (modal) | `<a href="/help/<file>">` (page)
|
|
343
|
+
* - `app:/path` — close-modal-and-route (modal) | `<a href="/path">` (page)
|
|
344
|
+
* - `http(s)://...` — external, target=_blank rel=noopener (both)
|
|
345
|
+
* - `#anchor` — intra-page scroll (both)
|
|
346
|
+
*
|
|
347
|
+
* In modal context, links are emitted with `data-faq-href` / `data-faq-kind`
|
|
348
|
+
* sentinels so the modal's click handler (E5) can intercept and dispatch.
|
|
349
|
+
* In page context, links are emitted as plain navigable anchors; the prerendered
|
|
350
|
+
* static HTML can be clicked through directly.
|
|
351
|
+
*/
|
|
352
|
+
class FaqMarkdownRendererService {
|
|
353
|
+
parse(raw) {
|
|
354
|
+
const parsed = fm(raw);
|
|
355
|
+
return {
|
|
356
|
+
frontmatter: parsed.attributes,
|
|
357
|
+
body: parsed.body,
|
|
358
|
+
};
|
|
359
|
+
}
|
|
360
|
+
render(body, opts) {
|
|
361
|
+
const marked = new Marked({ gfm: true, breaks: false });
|
|
362
|
+
marked.use(gfmHeadingId());
|
|
363
|
+
marked.use({ renderer: this.buildRenderer(opts) });
|
|
364
|
+
return marked.parse(body, { async: false });
|
|
365
|
+
}
|
|
366
|
+
buildRenderer(opts) {
|
|
367
|
+
const { context, imagePrefix } = opts;
|
|
368
|
+
return {
|
|
369
|
+
link: ({ href, title, tokens }) => {
|
|
370
|
+
// marked passes child tokens; we render their textual form ourselves.
|
|
371
|
+
const text = tokens.map((t) => ('text' in t ? t.text : '')).join('');
|
|
372
|
+
const titleAttr = title ? ` title="${escapeAttr(title)}"` : '';
|
|
373
|
+
// Intra-page anchor
|
|
374
|
+
if (href.startsWith('#')) {
|
|
375
|
+
return `<a href="${escapeAttr(href)}"${titleAttr} data-faq-kind="anchor">${text}</a>`;
|
|
376
|
+
}
|
|
377
|
+
// faq: protocol — in-modal nav (modal) | static link to /help/<key path> (page)
|
|
378
|
+
if (href.startsWith('faq:')) {
|
|
379
|
+
const key = href.slice('faq:'.length);
|
|
380
|
+
if (context === 'modal') {
|
|
381
|
+
return `<a href="#" data-faq-href="${escapeAttr(href)}" data-faq-kind="faq">${text}</a>`;
|
|
382
|
+
}
|
|
383
|
+
const filePath = key.replace(/\./g, '/');
|
|
384
|
+
return `<a href="/help/${escapeAttr(filePath)}"${titleAttr} data-faq-kind="faq">${text}</a>`;
|
|
385
|
+
}
|
|
386
|
+
// app: protocol — close modal + route (modal) | normal anchor (page)
|
|
387
|
+
if (href.startsWith('app:')) {
|
|
388
|
+
const path = href.slice('app:'.length);
|
|
389
|
+
if (context === 'modal') {
|
|
390
|
+
return `<a href="#" data-faq-href="${escapeAttr(href)}" data-faq-kind="app">${text} <span class="faq-app-arrow" aria-hidden="true">→</span></a>`;
|
|
391
|
+
}
|
|
392
|
+
return `<a href="${escapeAttr(path)}"${titleAttr} data-faq-kind="app">${text} <span class="faq-app-arrow" aria-hidden="true">→</span></a>`;
|
|
393
|
+
}
|
|
394
|
+
// External
|
|
395
|
+
if (/^https?:\/\//i.test(href)) {
|
|
396
|
+
return `<a href="${escapeAttr(href)}"${titleAttr} target="_blank" rel="noopener" data-faq-kind="external">${text}</a>`;
|
|
397
|
+
}
|
|
398
|
+
// Fallback: treat as relative; render plain anchor.
|
|
399
|
+
return `<a href="${escapeAttr(href)}"${titleAttr}>${text}</a>`;
|
|
400
|
+
},
|
|
401
|
+
image: ({ href, title, text }) => {
|
|
402
|
+
const resolved = resolveImageHref(href, imagePrefix);
|
|
403
|
+
const titleAttr = title ? ` title="${escapeAttr(title)}"` : '';
|
|
404
|
+
return `<img src="${escapeAttr(resolved)}" alt="${escapeAttr(text)}"${titleAttr} loading="lazy" />`;
|
|
405
|
+
},
|
|
406
|
+
};
|
|
407
|
+
}
|
|
408
|
+
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.13", ngImport: i0, type: FaqMarkdownRendererService, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
|
|
409
|
+
static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.13", ngImport: i0, type: FaqMarkdownRendererService, providedIn: 'root' }); }
|
|
410
|
+
}
|
|
411
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.13", ngImport: i0, type: FaqMarkdownRendererService, decorators: [{
|
|
412
|
+
type: Injectable,
|
|
413
|
+
args: [{ providedIn: 'root' }]
|
|
414
|
+
}] });
|
|
415
|
+
function escapeAttr(s) {
|
|
416
|
+
return s.replace(/&/g, '&').replace(/"/g, '"').replace(/</g, '<');
|
|
417
|
+
}
|
|
418
|
+
function resolveImageHref(href, prefix) {
|
|
419
|
+
if (!prefix)
|
|
420
|
+
return href;
|
|
421
|
+
if (/^([a-z]+:)?\/\//i.test(href) || href.startsWith('/'))
|
|
422
|
+
return href;
|
|
423
|
+
const cleaned = prefix.endsWith('/') ? prefix : `${prefix}/`;
|
|
424
|
+
return `${cleaned}${href}`;
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
/**
|
|
428
|
+
* Default FaqContentLoader. Reads the build-generated manifest from
|
|
429
|
+
* `/assets/faq/manifest.json` and lazy-fetches individual `.md` files
|
|
430
|
+
* from `/assets/faq/<entry.file>.md`.
|
|
431
|
+
*
|
|
432
|
+
* Manifest is cached after first load; .md fetches are cached per key.
|
|
433
|
+
*/
|
|
434
|
+
class HttpFaqContentLoader {
|
|
435
|
+
constructor() {
|
|
436
|
+
this.http = inject(HttpClient);
|
|
437
|
+
this.manifestPromise = null;
|
|
438
|
+
this.contentCache = new Map();
|
|
439
|
+
}
|
|
440
|
+
loadManifest() {
|
|
441
|
+
if (!this.manifestPromise) {
|
|
442
|
+
this.manifestPromise = firstValueFrom(this.http.get('/assets/faq/manifest.json')).catch((err) => {
|
|
443
|
+
this.manifestPromise = null;
|
|
444
|
+
throw err;
|
|
445
|
+
});
|
|
446
|
+
}
|
|
447
|
+
return this.manifestPromise;
|
|
448
|
+
}
|
|
449
|
+
async loadContent(key) {
|
|
450
|
+
const cached = this.contentCache.get(key);
|
|
451
|
+
if (cached)
|
|
452
|
+
return cached;
|
|
453
|
+
const manifest = await this.loadManifest();
|
|
454
|
+
const entry = manifest.entries.find((e) => e.key === key);
|
|
455
|
+
if (!entry)
|
|
456
|
+
return null;
|
|
457
|
+
const fetchPromise = firstValueFrom(this.http.get(`/assets/faq/${entry.file}.md`, { responseType: 'text' })).catch(() => null);
|
|
458
|
+
this.contentCache.set(key, fetchPromise);
|
|
459
|
+
return fetchPromise;
|
|
460
|
+
}
|
|
461
|
+
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.13", ngImport: i0, type: HttpFaqContentLoader, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
|
|
462
|
+
static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.13", ngImport: i0, type: HttpFaqContentLoader, providedIn: 'root' }); }
|
|
463
|
+
}
|
|
464
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.13", ngImport: i0, type: HttpFaqContentLoader, decorators: [{
|
|
465
|
+
type: Injectable,
|
|
466
|
+
args: [{ providedIn: 'root' }]
|
|
467
|
+
}] });
|
|
468
|
+
|
|
469
|
+
/**
|
|
470
|
+
* E8 — In-modal navigation history.
|
|
471
|
+
*
|
|
472
|
+
* The browser's `history` API is shared by Angular Router and any other
|
|
473
|
+
* code that calls `pushState`. We tag each FAQ entry's state object with
|
|
474
|
+
* `faq: true` so we can distinguish our entries from router/app entries.
|
|
475
|
+
*
|
|
476
|
+
* On open: `pushState({faq:true, faqPosition:0})` once; stack=[initialKey].
|
|
477
|
+
* On in-modal nav: truncate forward, push key, `pushState({faq:true,
|
|
478
|
+
* faqPosition:position})`.
|
|
479
|
+
* On browser back/forward (`popstate`):
|
|
480
|
+
* - If next state has `faq:true` → set position to its `faqPosition`,
|
|
481
|
+
* swap content for `stack[position]`.
|
|
482
|
+
* - If next state lacks `faq:true` (user backed out of the modal entirely)
|
|
483
|
+
* → close modal without further history manipulation.
|
|
484
|
+
* On explicit close (close button / Esc / backdrop): `history.go(-(position+1))`
|
|
485
|
+
* to unwind our entries; suppress in-flight popstates while unwinding;
|
|
486
|
+
* finalize close once we've seen the expected number of popstates.
|
|
487
|
+
*/
|
|
488
|
+
class FaqHistoryService {
|
|
489
|
+
constructor() {
|
|
490
|
+
this._stack = signal([], ...(ngDevMode ? [{ debugName: "_stack" }] : /* istanbul ignore next */ []));
|
|
491
|
+
this._position = signal(0, ...(ngDevMode ? [{ debugName: "_position" }] : /* istanbul ignore next */ []));
|
|
492
|
+
this.stack = this._stack.asReadonly();
|
|
493
|
+
this.position = this._position.asReadonly();
|
|
494
|
+
/** True when popstate is processing the unwind kicked off by closeAndUnwind(). */
|
|
495
|
+
this.unwinding = false;
|
|
496
|
+
this.unwindRemaining = 0;
|
|
497
|
+
this.onUnwindComplete = null;
|
|
498
|
+
this.popstateListener = null;
|
|
499
|
+
this.onInModalNav = null;
|
|
500
|
+
this.onExitModal = null;
|
|
501
|
+
}
|
|
502
|
+
/**
|
|
503
|
+
* Begin a new FAQ history session. Pushes the initial state entry and
|
|
504
|
+
* starts listening for popstate. Callbacks fire for in-modal navs and
|
|
505
|
+
* for popping out of FAQ entirely.
|
|
506
|
+
*/
|
|
507
|
+
begin(initialKey, onNav, onExit) {
|
|
508
|
+
this._stack.set([initialKey]);
|
|
509
|
+
this._position.set(0);
|
|
510
|
+
this.onInModalNav = onNav;
|
|
511
|
+
this.onExitModal = onExit;
|
|
512
|
+
this.pushHistoryState(0);
|
|
513
|
+
this.attachListener();
|
|
514
|
+
}
|
|
515
|
+
/** Push a new key onto the stack. Truncates any forward history. */
|
|
516
|
+
navigate(key) {
|
|
517
|
+
const stack = this._stack();
|
|
518
|
+
const pos = this._position();
|
|
519
|
+
const next = [...stack.slice(0, pos + 1), key];
|
|
520
|
+
const nextPos = next.length - 1;
|
|
521
|
+
this._stack.set(next);
|
|
522
|
+
this._position.set(nextPos);
|
|
523
|
+
this.pushHistoryState(nextPos);
|
|
524
|
+
}
|
|
525
|
+
canGoBack() {
|
|
526
|
+
return this._position() > 0;
|
|
527
|
+
}
|
|
528
|
+
canGoForward() {
|
|
529
|
+
return this._position() < this._stack().length - 1;
|
|
530
|
+
}
|
|
531
|
+
/** Programmatic back — triggers a real browser back so popstate handles UI. */
|
|
532
|
+
back() {
|
|
533
|
+
if (!this.canGoBack())
|
|
534
|
+
return;
|
|
535
|
+
history.back();
|
|
536
|
+
}
|
|
537
|
+
forward() {
|
|
538
|
+
if (!this.canGoForward())
|
|
539
|
+
return;
|
|
540
|
+
history.forward();
|
|
541
|
+
}
|
|
542
|
+
/** Currently rendered key. */
|
|
543
|
+
currentKey() {
|
|
544
|
+
return this._stack()[this._position()] ?? null;
|
|
545
|
+
}
|
|
546
|
+
/**
|
|
547
|
+
* Close the modal cleanly by unwinding our FAQ history entries so the
|
|
548
|
+
* underlying app route is left exactly where it was before the modal opened.
|
|
549
|
+
* `onComplete` fires after the unwind settles.
|
|
550
|
+
*/
|
|
551
|
+
closeAndUnwind(onComplete) {
|
|
552
|
+
const depth = this._position() + 1;
|
|
553
|
+
if (depth === 0) {
|
|
554
|
+
this.teardown();
|
|
555
|
+
onComplete();
|
|
556
|
+
return;
|
|
557
|
+
}
|
|
558
|
+
this.unwinding = true;
|
|
559
|
+
this.unwindRemaining = depth;
|
|
560
|
+
this.onUnwindComplete = () => {
|
|
561
|
+
this.teardown();
|
|
562
|
+
onComplete();
|
|
563
|
+
};
|
|
564
|
+
history.go(-depth);
|
|
565
|
+
}
|
|
566
|
+
teardown() {
|
|
567
|
+
this._stack.set([]);
|
|
568
|
+
this._position.set(0);
|
|
569
|
+
this.detachListener();
|
|
570
|
+
this.onInModalNav = null;
|
|
571
|
+
this.onExitModal = null;
|
|
572
|
+
this.unwinding = false;
|
|
573
|
+
this.unwindRemaining = 0;
|
|
574
|
+
this.onUnwindComplete = null;
|
|
575
|
+
}
|
|
576
|
+
pushHistoryState(position) {
|
|
577
|
+
history.pushState({ faq: true, faqPosition: position }, '');
|
|
578
|
+
}
|
|
579
|
+
attachListener() {
|
|
580
|
+
if (this.popstateListener)
|
|
581
|
+
return;
|
|
582
|
+
this.popstateListener = (e) => this.handlePopState(e);
|
|
583
|
+
window.addEventListener('popstate', this.popstateListener);
|
|
584
|
+
}
|
|
585
|
+
detachListener() {
|
|
586
|
+
if (!this.popstateListener)
|
|
587
|
+
return;
|
|
588
|
+
window.removeEventListener('popstate', this.popstateListener);
|
|
589
|
+
this.popstateListener = null;
|
|
590
|
+
}
|
|
591
|
+
handlePopState(e) {
|
|
592
|
+
if (this.unwinding) {
|
|
593
|
+
this.unwindRemaining--;
|
|
594
|
+
if (this.unwindRemaining <= 0) {
|
|
595
|
+
const cb = this.onUnwindComplete;
|
|
596
|
+
this.onUnwindComplete = null;
|
|
597
|
+
cb?.();
|
|
598
|
+
}
|
|
599
|
+
return;
|
|
600
|
+
}
|
|
601
|
+
const state = e.state;
|
|
602
|
+
if (state?.faq && typeof state.faqPosition === 'number') {
|
|
603
|
+
// In-modal back/forward
|
|
604
|
+
const target = state.faqPosition;
|
|
605
|
+
this._position.set(target);
|
|
606
|
+
const key = this._stack()[target];
|
|
607
|
+
if (key && this.onInModalNav)
|
|
608
|
+
this.onInModalNav(key);
|
|
609
|
+
return;
|
|
610
|
+
}
|
|
611
|
+
// User backed out beyond our entries — close modal, leave history alone.
|
|
612
|
+
const exit = this.onExitModal;
|
|
613
|
+
this.teardown();
|
|
614
|
+
exit?.();
|
|
615
|
+
}
|
|
616
|
+
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.13", ngImport: i0, type: FaqHistoryService, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
|
|
617
|
+
static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.13", ngImport: i0, type: FaqHistoryService, providedIn: 'root' }); }
|
|
618
|
+
}
|
|
619
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.13", ngImport: i0, type: FaqHistoryService, decorators: [{
|
|
620
|
+
type: Injectable,
|
|
621
|
+
args: [{ providedIn: 'root' }]
|
|
622
|
+
}] });
|
|
623
|
+
|
|
624
|
+
/**
|
|
625
|
+
* Central FAQ state + resolution service.
|
|
626
|
+
*
|
|
627
|
+
* Responsibilities:
|
|
628
|
+
* - Track the current route's FAQ key (set by components on activation /
|
|
629
|
+
* tab change via `register()`).
|
|
630
|
+
* - Lazy-load and cache the FAQ manifest from the configured FaqContentLoader.
|
|
631
|
+
* - Resolve a dot-notation key against the manifest with parent fallback,
|
|
632
|
+
* audience filtering, and `index` as the terminal fallback.
|
|
633
|
+
*/
|
|
634
|
+
class FaqService {
|
|
635
|
+
constructor() {
|
|
636
|
+
this.loader = inject(FAQ_CONTENT_LOADER);
|
|
637
|
+
this.history = inject(FaqHistoryService);
|
|
638
|
+
this.auth = inject(AuthenticationService);
|
|
639
|
+
this._currentFaqKey = signal(null, ...(ngDevMode ? [{ debugName: "_currentFaqKey" }] : /* istanbul ignore next */ []));
|
|
640
|
+
/** Component-set current key. Bare `faq` opens at this key. */
|
|
641
|
+
this.currentFaqKey = this._currentFaqKey.asReadonly();
|
|
642
|
+
this._manifest = signal(null, ...(ngDevMode ? [{ debugName: "_manifest" }] : /* istanbul ignore next */ []));
|
|
643
|
+
this.manifest = this._manifest.asReadonly();
|
|
644
|
+
this.entries = computed(() => this._manifest()?.entries ?? [], ...(ngDevMode ? [{ debugName: "entries" }] : /* istanbul ignore next */ []));
|
|
645
|
+
// Modal state (E3 + E8).
|
|
646
|
+
this._modalOpen = signal(false, ...(ngDevMode ? [{ debugName: "_modalOpen" }] : /* istanbul ignore next */ []));
|
|
647
|
+
this.modalOpen = this._modalOpen.asReadonly();
|
|
648
|
+
this._activeEntry = signal(null, ...(ngDevMode ? [{ debugName: "_activeEntry" }] : /* istanbul ignore next */ []));
|
|
649
|
+
this.activeEntry = this._activeEntry.asReadonly();
|
|
650
|
+
this._activeContent = signal(null, ...(ngDevMode ? [{ debugName: "_activeContent" }] : /* istanbul ignore next */ []));
|
|
651
|
+
this.activeContent = this._activeContent.asReadonly();
|
|
652
|
+
this._activeAttempted = signal([], ...(ngDevMode ? [{ debugName: "_activeAttempted" }] : /* istanbul ignore next */ []));
|
|
653
|
+
this.activeAttempted = this._activeAttempted.asReadonly();
|
|
654
|
+
this._navigating = signal(false, ...(ngDevMode ? [{ debugName: "_navigating" }] : /* istanbul ignore next */ []));
|
|
655
|
+
this.navigating = this._navigating.asReadonly();
|
|
656
|
+
// Search pre-fill consumed by E4 search UI on modal open / navigateTo.
|
|
657
|
+
this._initialSearchTerm = signal(null, ...(ngDevMode ? [{ debugName: "_initialSearchTerm" }] : /* istanbul ignore next */ []));
|
|
658
|
+
this.initialSearchTerm = this._initialSearchTerm.asReadonly();
|
|
659
|
+
this.canGoBack = computed(() => this.history.position() > 0, ...(ngDevMode ? [{ debugName: "canGoBack" }] : /* istanbul ignore next */ []));
|
|
660
|
+
this.canGoForward = computed(() => this.history.position() < this.history.stack().length - 1, ...(ngDevMode ? [{ debugName: "canGoForward" }] : /* istanbul ignore next */ []));
|
|
661
|
+
this.manifestPromise = null;
|
|
662
|
+
}
|
|
663
|
+
/** Called by the search UI once it has consumed the pre-fill term. */
|
|
664
|
+
consumeInitialSearchTerm() {
|
|
665
|
+
this._initialSearchTerm.set(null);
|
|
666
|
+
}
|
|
667
|
+
/**
|
|
668
|
+
* Triggers manifest load if not already loaded. Idempotent. Subsequent
|
|
669
|
+
* calls return the cached promise.
|
|
670
|
+
*/
|
|
671
|
+
ensureManifest() {
|
|
672
|
+
if (this._manifest())
|
|
673
|
+
return Promise.resolve(this._manifest());
|
|
674
|
+
if (!this.manifestPromise) {
|
|
675
|
+
this.manifestPromise = this.loader
|
|
676
|
+
.loadManifest()
|
|
677
|
+
.then((m) => {
|
|
678
|
+
this._manifest.set(m);
|
|
679
|
+
return m;
|
|
680
|
+
})
|
|
681
|
+
.catch((err) => {
|
|
682
|
+
this.manifestPromise = null;
|
|
683
|
+
throw err;
|
|
684
|
+
});
|
|
685
|
+
}
|
|
686
|
+
return this.manifestPromise;
|
|
687
|
+
}
|
|
688
|
+
/**
|
|
689
|
+
* Walks the dot-notation fallback chain (`a.b.c` → `a.b` → `a` → `index`)
|
|
690
|
+
* and returns the first entry whose audience allows the caller and whose
|
|
691
|
+
* content loads. Audience filtering: unauthenticated callers only see
|
|
692
|
+
* `audience: public` entries.
|
|
693
|
+
*/
|
|
694
|
+
async resolveKey(key, isAuthenticated) {
|
|
695
|
+
await this.ensureManifest();
|
|
696
|
+
const entries = this.entries();
|
|
697
|
+
const chain = buildFallbackChain(key);
|
|
698
|
+
for (const candidate of chain) {
|
|
699
|
+
const entry = entries.find((e) => e.key === candidate);
|
|
700
|
+
if (!entry)
|
|
701
|
+
continue;
|
|
702
|
+
if (!audienceAllowed(entry.audience, isAuthenticated))
|
|
703
|
+
continue;
|
|
704
|
+
const content = await this.loader.loadContent(entry.key);
|
|
705
|
+
if (content === null)
|
|
706
|
+
continue;
|
|
707
|
+
return { key: entry.key, content, entry, attempted: chain };
|
|
708
|
+
}
|
|
709
|
+
return { key: null, content: null, entry: null, attempted: chain };
|
|
710
|
+
}
|
|
711
|
+
/**
|
|
712
|
+
* Component registration. Sets `currentFaqKey`; on `deregister()` the
|
|
713
|
+
* key is cleared only if it still matches (so a sibling tab activating
|
|
714
|
+
* mid-teardown doesn't get its key clobbered).
|
|
715
|
+
*/
|
|
716
|
+
register(key) {
|
|
717
|
+
this._currentFaqKey.set(key);
|
|
718
|
+
return {
|
|
719
|
+
deregister: () => {
|
|
720
|
+
if (this._currentFaqKey() === key)
|
|
721
|
+
this._currentFaqKey.set(null);
|
|
722
|
+
},
|
|
723
|
+
};
|
|
724
|
+
}
|
|
725
|
+
/**
|
|
726
|
+
* E3 — Open the FAQ modal. If `initialKey` is omitted, falls back to
|
|
727
|
+
* `currentFaqKey` and then to `'index'`. Resolves the key, loads content,
|
|
728
|
+
* and starts an E8 history session so subsequent in-modal nav uses
|
|
729
|
+
* `history.pushState`.
|
|
730
|
+
*/
|
|
731
|
+
async openModal(initialKey, opts) {
|
|
732
|
+
const searchTerm = opts?.searchTerm?.trim();
|
|
733
|
+
this._initialSearchTerm.set(searchTerm && searchTerm.length > 0 ? searchTerm : null);
|
|
734
|
+
if (this._modalOpen()) {
|
|
735
|
+
// Already open — treat as a navigate-to instead.
|
|
736
|
+
if (initialKey)
|
|
737
|
+
await this.navigateTo(initialKey);
|
|
738
|
+
return;
|
|
739
|
+
}
|
|
740
|
+
const target = initialKey ?? this._currentFaqKey() ?? 'index';
|
|
741
|
+
this._modalOpen.set(true);
|
|
742
|
+
await this.loadIntoActive(target);
|
|
743
|
+
const resolved = this._activeEntry()?.key ?? target;
|
|
744
|
+
this.history.begin(resolved, (key) => {
|
|
745
|
+
// popstate-driven in-modal nav: load without pushing a new entry.
|
|
746
|
+
void this.loadIntoActive(key);
|
|
747
|
+
}, () => {
|
|
748
|
+
// User backed out of FAQ history entirely — close without unwinding.
|
|
749
|
+
this.finalizeClose();
|
|
750
|
+
});
|
|
751
|
+
}
|
|
752
|
+
/**
|
|
753
|
+
* Programmatic in-modal nav. Pushes a new history entry and loads `key`
|
|
754
|
+
* into the active slot. Use this for click handlers on `faq:` links and
|
|
755
|
+
* for the TOC sidebar.
|
|
756
|
+
*/
|
|
757
|
+
async navigateTo(key) {
|
|
758
|
+
if (!this._modalOpen()) {
|
|
759
|
+
await this.openModal(key);
|
|
760
|
+
return;
|
|
761
|
+
}
|
|
762
|
+
await this.loadIntoActive(key);
|
|
763
|
+
const resolved = this._activeEntry()?.key ?? key;
|
|
764
|
+
this.history.navigate(resolved);
|
|
765
|
+
}
|
|
766
|
+
back() {
|
|
767
|
+
this.history.back();
|
|
768
|
+
}
|
|
769
|
+
forward() {
|
|
770
|
+
this.history.forward();
|
|
771
|
+
}
|
|
772
|
+
/**
|
|
773
|
+
* Close button / Esc / backdrop close. Unwinds the FAQ history entries
|
|
774
|
+
* so the underlying app route is left untouched.
|
|
775
|
+
*/
|
|
776
|
+
closeModal() {
|
|
777
|
+
if (!this._modalOpen())
|
|
778
|
+
return;
|
|
779
|
+
this.history.closeAndUnwind(() => this.finalizeClose());
|
|
780
|
+
}
|
|
781
|
+
finalizeClose() {
|
|
782
|
+
this._modalOpen.set(false);
|
|
783
|
+
this._activeContent.set(null);
|
|
784
|
+
this._activeEntry.set(null);
|
|
785
|
+
this._activeAttempted.set([]);
|
|
786
|
+
}
|
|
787
|
+
async loadIntoActive(key) {
|
|
788
|
+
this._navigating.set(true);
|
|
789
|
+
try {
|
|
790
|
+
const resolution = await this.resolveKey(key, this.auth.authenticated());
|
|
791
|
+
this._activeEntry.set(resolution.entry);
|
|
792
|
+
this._activeContent.set(resolution.content);
|
|
793
|
+
this._activeAttempted.set(resolution.attempted);
|
|
794
|
+
}
|
|
795
|
+
finally {
|
|
796
|
+
this._navigating.set(false);
|
|
797
|
+
}
|
|
798
|
+
}
|
|
799
|
+
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.13", ngImport: i0, type: FaqService, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
|
|
800
|
+
static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.13", ngImport: i0, type: FaqService, providedIn: 'root' }); }
|
|
801
|
+
}
|
|
802
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.13", ngImport: i0, type: FaqService, decorators: [{
|
|
803
|
+
type: Injectable,
|
|
804
|
+
args: [{ providedIn: 'root' }]
|
|
805
|
+
}] });
|
|
806
|
+
function buildFallbackChain(key) {
|
|
807
|
+
if (!key)
|
|
808
|
+
return ['index'];
|
|
809
|
+
const parts = key.split('.').filter(Boolean);
|
|
810
|
+
if (parts.length === 0)
|
|
811
|
+
return ['index'];
|
|
812
|
+
const chain = [];
|
|
813
|
+
for (let i = parts.length; i > 0; i--) {
|
|
814
|
+
chain.push(parts.slice(0, i).join('.'));
|
|
815
|
+
}
|
|
816
|
+
chain.push('index');
|
|
817
|
+
return chain;
|
|
818
|
+
}
|
|
819
|
+
function audienceAllowed(audience, isAuthenticated) {
|
|
820
|
+
if (audience === 'public')
|
|
821
|
+
return true;
|
|
822
|
+
return isAuthenticated;
|
|
823
|
+
}
|
|
824
|
+
|
|
825
|
+
/**
|
|
826
|
+
* Gate for the dev-only FAQ authoring panel (E11).
|
|
827
|
+
*
|
|
828
|
+
* Defaults to Angular's `isDevMode()` so the panel auto-enables in `ng serve`
|
|
829
|
+
* and auto-disables in production builds. Consumers can override with an
|
|
830
|
+
* explicit value (e.g. `!environment.production`, or `true` for a staging
|
|
831
|
+
* diagnostic build) via `{provide: FAQ_DEV_MODE, useValue: ...}`.
|
|
832
|
+
*
|
|
833
|
+
* The panel is loaded via dynamic `import()` and lives in its own lazy chunk;
|
|
834
|
+
* builds where this resolves to `false` never request the chunk.
|
|
835
|
+
*/
|
|
836
|
+
const FAQ_DEV_MODE = new InjectionToken('FAQ_DEV_MODE', {
|
|
837
|
+
providedIn: 'root',
|
|
838
|
+
factory: () => isDevMode(),
|
|
839
|
+
});
|
|
840
|
+
/**
|
|
841
|
+
* Optional absolute filesystem root of the consuming app's workspace (e.g.
|
|
842
|
+
* `/Users/foo/dev/broken-stock`). When set, the authoring panel emits
|
|
843
|
+
* `webstorm://open?file=<root>/<relative-path>` deeplinks pointing at the
|
|
844
|
+
* exact source file. When unset, the panel falls back to a relative path.
|
|
845
|
+
*/
|
|
846
|
+
const FAQ_DEV_WORKSPACE_ROOT = new InjectionToken('FAQ_DEV_WORKSPACE_ROOT', { providedIn: 'root', factory: () => '' });
|
|
847
|
+
const FAQ_DEV_EDITOR = new InjectionToken('FAQ_DEV_EDITOR', { providedIn: 'root', factory: () => 'webstorm' });
|
|
848
|
+
|
|
849
|
+
/**
|
|
850
|
+
* E3 — FAQ modal. Two-pane layout (TOC sidebar + content pane), 80% viewport.
|
|
851
|
+
*
|
|
852
|
+
* E4 — Search uses a lazy-imported `fuse.js` index over title + body,
|
|
853
|
+
* built on first search activation, filtered by audience, cached for the
|
|
854
|
+
* modal session and invalidated on auth state change.
|
|
855
|
+
*
|
|
856
|
+
* E5 — `app:` link clicks close the modal and route to the app path; the
|
|
857
|
+
* returnUrl-aware `authGuard` funnels unauthenticated users through
|
|
858
|
+
* `/login?returnUrl=...`, and `Login` honors returnUrl on success.
|
|
859
|
+
*/
|
|
860
|
+
class FaqModal {
|
|
861
|
+
constructor() {
|
|
862
|
+
this.devMode = inject(FAQ_DEV_MODE);
|
|
863
|
+
this.faq = inject(FaqService);
|
|
864
|
+
this.renderer = inject(FaqMarkdownRendererService);
|
|
865
|
+
this.auth = inject(AuthenticationService);
|
|
866
|
+
this.router = inject(Router);
|
|
867
|
+
this.loader = inject(FAQ_CONTENT_LOADER);
|
|
868
|
+
this.open = this.faq.modalOpen;
|
|
869
|
+
this.navigating = this.faq.navigating;
|
|
870
|
+
this.activeEntry = this.faq.activeEntry;
|
|
871
|
+
this.attempted = this.faq.activeAttempted;
|
|
872
|
+
this.canGoBack = this.faq.canGoBack;
|
|
873
|
+
this.canGoForward = this.faq.canGoForward;
|
|
874
|
+
this.sidebarCollapsed = signal(false, ...(ngDevMode ? [{ debugName: "sidebarCollapsed" }] : /* istanbul ignore next */ []));
|
|
875
|
+
this.searchMode = signal(false, ...(ngDevMode ? [{ debugName: "searchMode" }] : /* istanbul ignore next */ []));
|
|
876
|
+
this.searchQuery = signal('', ...(ngDevMode ? [{ debugName: "searchQuery" }] : /* istanbul ignore next */ []));
|
|
877
|
+
this.searchLoading = signal(false, ...(ngDevMode ? [{ debugName: "searchLoading" }] : /* istanbul ignore next */ []));
|
|
878
|
+
this.searchResults = signal([], ...(ngDevMode ? [{ debugName: "searchResults" }] : /* istanbul ignore next */ []));
|
|
879
|
+
this.fuse = null;
|
|
880
|
+
this.fuseBuild = null;
|
|
881
|
+
this.renderedHtml = computed(() => {
|
|
882
|
+
const content = this.faq.activeContent();
|
|
883
|
+
const entry = this.faq.activeEntry();
|
|
884
|
+
if (!content)
|
|
885
|
+
return '';
|
|
886
|
+
return this.renderer.render(content, {
|
|
887
|
+
context: 'modal',
|
|
888
|
+
imagePrefix: entry?.imagePrefix,
|
|
889
|
+
});
|
|
890
|
+
}, ...(ngDevMode ? [{ debugName: "renderedHtml" }] : /* istanbul ignore next */ []));
|
|
891
|
+
this.toc = computed(() => {
|
|
892
|
+
const isAuth = this.auth.authenticated();
|
|
893
|
+
const entries = this.faq.entries().filter((e) => {
|
|
894
|
+
if (e.audience === 'public')
|
|
895
|
+
return true;
|
|
896
|
+
return isAuth;
|
|
897
|
+
});
|
|
898
|
+
const groups = new Map();
|
|
899
|
+
for (const e of entries) {
|
|
900
|
+
const list = groups.get(e.category) ?? [];
|
|
901
|
+
list.push(e);
|
|
902
|
+
groups.set(e.category, list);
|
|
903
|
+
}
|
|
904
|
+
return [...groups.entries()]
|
|
905
|
+
.sort(([a], [b]) => a.localeCompare(b))
|
|
906
|
+
.map(([category, list]) => ({
|
|
907
|
+
category,
|
|
908
|
+
entries: list.sort((a, b) => a.order - b.order || a.title.localeCompare(b.title)),
|
|
909
|
+
}));
|
|
910
|
+
}, ...(ngDevMode ? [{ debugName: "toc" }] : /* istanbul ignore next */ []));
|
|
911
|
+
// Scroll content pane to top on key change.
|
|
912
|
+
effect(() => {
|
|
913
|
+
this.activeEntry();
|
|
914
|
+
queueMicrotask(() => {
|
|
915
|
+
this.contentPaneRef?.nativeElement.scrollTo({ top: 0 });
|
|
916
|
+
});
|
|
917
|
+
});
|
|
918
|
+
// Invalidate the search index on auth state change so the audience
|
|
919
|
+
// filter is re-applied on next search activation.
|
|
920
|
+
effect(() => {
|
|
921
|
+
this.auth.authenticated();
|
|
922
|
+
this.fuse = null;
|
|
923
|
+
this.fuseBuild = null;
|
|
924
|
+
if (this.searchMode())
|
|
925
|
+
this.searchResults.set([]);
|
|
926
|
+
});
|
|
927
|
+
// Consume `faq search <term>` pre-fill: when FaqService.initialSearchTerm
|
|
928
|
+
// is non-null while the modal is open, switch to search mode with that
|
|
929
|
+
// query and immediately invoke the index build.
|
|
930
|
+
effect(() => {
|
|
931
|
+
const term = this.faq.initialSearchTerm();
|
|
932
|
+
if (!term || !this.faq.modalOpen())
|
|
933
|
+
return;
|
|
934
|
+
untracked(() => {
|
|
935
|
+
this.searchMode.set(true);
|
|
936
|
+
this.searchQuery.set(term);
|
|
937
|
+
this.faq.consumeInitialSearchTerm();
|
|
938
|
+
void this.ensureSearchIndex().then(() => this.runSearch(term));
|
|
939
|
+
});
|
|
940
|
+
});
|
|
941
|
+
}
|
|
942
|
+
onEscape() {
|
|
943
|
+
if (this.open())
|
|
944
|
+
this.close();
|
|
945
|
+
}
|
|
946
|
+
close() {
|
|
947
|
+
this.faq.closeModal();
|
|
948
|
+
}
|
|
949
|
+
onBackdropClick() {
|
|
950
|
+
this.close();
|
|
951
|
+
}
|
|
952
|
+
stopPropagation(event) {
|
|
953
|
+
event.stopPropagation();
|
|
954
|
+
}
|
|
955
|
+
toggleSidebar() {
|
|
956
|
+
this.sidebarCollapsed.update((v) => !v);
|
|
957
|
+
}
|
|
958
|
+
goBack() {
|
|
959
|
+
this.faq.back();
|
|
960
|
+
}
|
|
961
|
+
goForward() {
|
|
962
|
+
this.faq.forward();
|
|
963
|
+
}
|
|
964
|
+
goIndex() {
|
|
965
|
+
void this.faq.navigateTo('index');
|
|
966
|
+
}
|
|
967
|
+
toggleSearch() {
|
|
968
|
+
const next = !this.searchMode();
|
|
969
|
+
this.searchMode.set(next);
|
|
970
|
+
if (next) {
|
|
971
|
+
void this.ensureSearchIndex().then(() => this.runSearch(this.searchQuery()));
|
|
972
|
+
}
|
|
973
|
+
}
|
|
974
|
+
onSearchInput(event) {
|
|
975
|
+
const value = event.target.value;
|
|
976
|
+
this.searchQuery.set(value);
|
|
977
|
+
void this.ensureSearchIndex().then(() => this.runSearch(value));
|
|
978
|
+
}
|
|
979
|
+
onSearchResultClick(key) {
|
|
980
|
+
void this.faq.navigateTo(key);
|
|
981
|
+
}
|
|
982
|
+
onTocClick(key) {
|
|
983
|
+
void this.faq.navigateTo(key);
|
|
984
|
+
}
|
|
985
|
+
async ensureSearchIndex() {
|
|
986
|
+
if (this.fuse)
|
|
987
|
+
return;
|
|
988
|
+
if (this.fuseBuild)
|
|
989
|
+
return this.fuseBuild;
|
|
990
|
+
this.searchLoading.set(true);
|
|
991
|
+
this.fuseBuild = (async () => {
|
|
992
|
+
const [{ default: Fuse }] = await Promise.all([
|
|
993
|
+
import('fuse.js'),
|
|
994
|
+
this.faq.ensureManifest(),
|
|
995
|
+
]);
|
|
996
|
+
const isAuth = this.auth.authenticated();
|
|
997
|
+
const entries = this.faq.entries().filter((e) => e.audience === 'public' ? true : isAuth);
|
|
998
|
+
const docs = await Promise.all(entries.map(async (e) => {
|
|
999
|
+
const body = (await this.loader.loadContent(e.key)) ?? '';
|
|
1000
|
+
return { key: e.key, title: e.title, category: e.category, body };
|
|
1001
|
+
}));
|
|
1002
|
+
this.fuse = new Fuse(docs, {
|
|
1003
|
+
keys: [
|
|
1004
|
+
{ name: 'title', weight: 0.6 },
|
|
1005
|
+
{ name: 'body', weight: 0.4 },
|
|
1006
|
+
],
|
|
1007
|
+
includeMatches: true,
|
|
1008
|
+
ignoreLocation: true,
|
|
1009
|
+
threshold: 0.4,
|
|
1010
|
+
minMatchCharLength: 2,
|
|
1011
|
+
});
|
|
1012
|
+
})()
|
|
1013
|
+
.catch((err) => {
|
|
1014
|
+
console.error('FAQ search index build failed', err);
|
|
1015
|
+
this.fuseBuild = null;
|
|
1016
|
+
})
|
|
1017
|
+
.finally(() => {
|
|
1018
|
+
this.searchLoading.set(false);
|
|
1019
|
+
});
|
|
1020
|
+
return this.fuseBuild;
|
|
1021
|
+
}
|
|
1022
|
+
runSearch(query) {
|
|
1023
|
+
const q = query.trim();
|
|
1024
|
+
if (!q || !this.fuse) {
|
|
1025
|
+
this.searchResults.set([]);
|
|
1026
|
+
return;
|
|
1027
|
+
}
|
|
1028
|
+
const hits = this.fuse.search(q).slice(0, 30);
|
|
1029
|
+
this.searchResults.set(hits.map((h) => ({
|
|
1030
|
+
key: h.item.key,
|
|
1031
|
+
title: h.item.title,
|
|
1032
|
+
category: h.item.category,
|
|
1033
|
+
snippet: buildSnippet(h.item.body, h.matches),
|
|
1034
|
+
})));
|
|
1035
|
+
}
|
|
1036
|
+
/**
|
|
1037
|
+
* Event-delegated click handler for the rendered content pane.
|
|
1038
|
+
* Intercepts our sentinel anchors emitted by FaqMarkdownRendererService.
|
|
1039
|
+
*/
|
|
1040
|
+
onContentClick(event) {
|
|
1041
|
+
const target = event.target;
|
|
1042
|
+
if (!(target instanceof HTMLElement))
|
|
1043
|
+
return;
|
|
1044
|
+
const anchor = target.closest('a[data-faq-kind]');
|
|
1045
|
+
if (!anchor)
|
|
1046
|
+
return;
|
|
1047
|
+
const kind = anchor.getAttribute('data-faq-kind');
|
|
1048
|
+
if (kind === 'external') {
|
|
1049
|
+
return;
|
|
1050
|
+
}
|
|
1051
|
+
if (kind === 'anchor') {
|
|
1052
|
+
event.preventDefault();
|
|
1053
|
+
const href = anchor.getAttribute('href') ?? '';
|
|
1054
|
+
const id = href.startsWith('#') ? href.slice(1) : href;
|
|
1055
|
+
if (!id)
|
|
1056
|
+
return;
|
|
1057
|
+
const pane = this.contentPaneRef?.nativeElement;
|
|
1058
|
+
const el = pane?.querySelector(`#${CSS.escape(id)}`);
|
|
1059
|
+
el?.scrollIntoView({ behavior: 'smooth', block: 'start' });
|
|
1060
|
+
return;
|
|
1061
|
+
}
|
|
1062
|
+
const faqHref = anchor.getAttribute('data-faq-href') ?? '';
|
|
1063
|
+
if (kind === 'faq' && faqHref.startsWith('faq:')) {
|
|
1064
|
+
event.preventDefault();
|
|
1065
|
+
const key = faqHref.slice('faq:'.length);
|
|
1066
|
+
void this.faq.navigateTo(key);
|
|
1067
|
+
return;
|
|
1068
|
+
}
|
|
1069
|
+
if (kind === 'app' && faqHref.startsWith('app:')) {
|
|
1070
|
+
event.preventDefault();
|
|
1071
|
+
const path = faqHref.slice('app:'.length);
|
|
1072
|
+
this.close();
|
|
1073
|
+
// Defer route until after modal close finishes unwinding history,
|
|
1074
|
+
// so navigation lands as a fresh entry on top of the pre-modal URL.
|
|
1075
|
+
// The returnUrl-aware authGuard handles unauthenticated callers by
|
|
1076
|
+
// redirecting to /login?returnUrl=<path>; Login honors it on success.
|
|
1077
|
+
queueMicrotask(() => {
|
|
1078
|
+
void this.router.navigateByUrl(path);
|
|
1079
|
+
});
|
|
1080
|
+
return;
|
|
1081
|
+
}
|
|
1082
|
+
}
|
|
1083
|
+
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.13", ngImport: i0, type: FaqModal, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
|
|
1084
|
+
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.13", type: FaqModal, isStandalone: true, selector: "faq-modal", host: { listeners: { "document:keydown.escape": "onEscape()" } }, viewQueries: [{ propertyName: "contentPaneRef", first: true, predicate: ["contentPane"], descendants: true }], ngImport: i0, template: "@if (open()) {\n <div class=\"faq-backdrop\" (click)=\"onBackdropClick()\" role=\"presentation\">\n <div\n class=\"faq-modal\"\n role=\"dialog\"\n aria-modal=\"true\"\n [attr.aria-label]=\"activeEntry()?.title ?? 'FAQ'\"\n (click)=\"stopPropagation($event)\"\n >\n <header class=\"faq-toolbar\">\n <div class=\"faq-toolbar-nav\">\n <button\n type=\"button\"\n class=\"faq-icon-btn\"\n (click)=\"toggleSidebar()\"\n [attr.aria-label]=\"sidebarCollapsed() ? 'Show contents' : 'Hide contents'\"\n [attr.aria-pressed]=\"!sidebarCollapsed()\"\n >\u2630</button>\n <button\n type=\"button\"\n class=\"faq-icon-btn\"\n (click)=\"goBack()\"\n [disabled]=\"!canGoBack()\"\n aria-label=\"Back\"\n >\u2039</button>\n <button\n type=\"button\"\n class=\"faq-icon-btn\"\n (click)=\"goForward()\"\n [disabled]=\"!canGoForward()\"\n aria-label=\"Forward\"\n >\u203A</button>\n <button\n type=\"button\"\n class=\"faq-icon-btn\"\n (click)=\"goIndex()\"\n aria-label=\"Index\"\n >\u2302</button>\n </div>\n <div class=\"faq-toolbar-title\">\n @if (activeEntry(); as entry) {\n <span class=\"faq-category\">{{ entry.category }}</span>\n <span class=\"faq-sep\">/</span>\n <span class=\"faq-title\">{{ entry.title }}</span>\n } @else {\n <span class=\"faq-title\">FAQ</span>\n }\n </div>\n <div class=\"faq-toolbar-actions\">\n <button\n type=\"button\"\n class=\"faq-icon-btn\"\n (click)=\"toggleSearch()\"\n [class.active]=\"searchMode()\"\n aria-label=\"Search\"\n >\u2315</button>\n <button\n type=\"button\"\n class=\"faq-icon-btn faq-close-btn\"\n (click)=\"close()\"\n aria-label=\"Close\"\n >\u2715</button>\n </div>\n </header>\n\n <div class=\"faq-body\" [class.sidebar-collapsed]=\"sidebarCollapsed()\">\n <aside class=\"faq-sidebar\" aria-label=\"FAQ contents\">\n @if (searchMode()) {\n <div class=\"faq-search\">\n <input\n type=\"search\"\n class=\"faq-search-input\"\n placeholder=\"Search FAQ\u2026\"\n aria-label=\"Search FAQ\"\n [value]=\"searchQuery()\"\n (input)=\"onSearchInput($event)\"\n />\n @if (searchLoading()) {\n <p class=\"faq-search-status\">Building index\u2026</p>\n } @else if (!searchQuery().trim()) {\n <p class=\"faq-search-status\">Type to search titles and content.</p>\n } @else if (searchResults().length === 0) {\n <p class=\"faq-search-status\">No matches.</p>\n } @else {\n <ul class=\"faq-search-results\">\n @for (r of searchResults(); track r.key) {\n <li>\n <button\n type=\"button\"\n class=\"faq-search-result\"\n [class.active]=\"activeEntry()?.key === r.key\"\n (click)=\"onSearchResultClick(r.key)\"\n >\n <span class=\"faq-search-result-title\">{{ r.title }}</span>\n <span class=\"faq-search-result-category\">{{ r.category }}</span>\n @if (r.snippet) {\n <span class=\"faq-search-result-snippet\">{{ r.snippet }}</span>\n }\n </button>\n </li>\n }\n </ul>\n }\n </div>\n } @else {\n <nav class=\"faq-toc\">\n @for (group of toc(); track group.category) {\n <div class=\"faq-toc-group\">\n <h3 class=\"faq-toc-category\">{{ group.category }}</h3>\n <ul>\n @for (entry of group.entries; track entry.key) {\n <li>\n <button\n type=\"button\"\n class=\"faq-toc-link\"\n [class.active]=\"activeEntry()?.key === entry.key\"\n (click)=\"onTocClick(entry.key)\"\n >{{ entry.title }}</button>\n </li>\n }\n </ul>\n </div>\n } @empty {\n <p class=\"faq-toc-empty\">No FAQ entries yet.</p>\n }\n </nav>\n }\n </aside>\n\n <main\n #contentPane\n class=\"faq-content\"\n (click)=\"onContentClick($event)\"\n >\n @if (navigating()) {\n <div class=\"faq-loading\">Loading\u2026</div>\n } @else if (renderedHtml(); as html) {\n <article class=\"faq-article\" [innerHTML]=\"html\"></article>\n @if (devMode && activeEntry(); as entry) {\n @defer (when devMode) {\n <faq-authoring-panel\n mode=\"existing\"\n [key]=\"entry.key\"\n [attempted]=\"attempted()\"\n [entry]=\"entry\"\n />\n }\n }\n } @else {\n <div class=\"faq-empty\">\n <h2>No FAQ available</h2>\n <p>No content is registered for this route.</p>\n @if (attempted().length) {\n <p class=\"faq-empty-chain\">\n Attempted keys:\n <code>{{ attempted().join(' \u2192 ') }}</code>\n </p>\n }\n @if (devMode && attempted().length) {\n @defer (when devMode) {\n <faq-authoring-panel\n mode=\"missing\"\n [key]=\"attempted()[0]\"\n [attempted]=\"attempted()\"\n />\n } @loading (minimum 100ms) {\n <p class=\"faq-empty-hint\">Loading authoring panel\u2026</p>\n }\n }\n </div>\n }\n </main>\n </div>\n </div>\n </div>\n}\n", styles: [":host{display:contents}.faq-backdrop{position:fixed;inset:0;z-index:1100;background:#0000008c;display:flex;align-items:center;justify-content:center;padding:5vh 5vw}.faq-modal{width:80vw;height:80vh;max-width:1600px;max-height:1100px;background:var(--shell-surface, #1d1f23);color:var(--shell-fg, #e6e6e6);border-radius:8px;box-shadow:0 12px 32px #00000080;display:grid;grid-template-rows:auto 1fr;overflow:hidden;font-family:var(--shell-font, system-ui, sans-serif)}.faq-toolbar{display:grid;grid-template-columns:auto 1fr auto;align-items:center;gap:.5rem;padding:.5rem .75rem;border-bottom:1px solid var(--shell-border, rgba(255, 255, 255, .08));background:var(--shell-surface-2, rgba(255, 255, 255, .03))}.faq-toolbar-nav,.faq-toolbar-actions{display:flex;gap:.25rem}.faq-toolbar-title{text-align:center;font-size:.95rem;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.faq-category{opacity:.7;margin-right:.25rem}.faq-sep{opacity:.4;margin:0 .25rem}.faq-title{font-weight:600}.faq-icon-btn{background:transparent;border:1px solid transparent;color:inherit;width:2rem;height:2rem;border-radius:4px;cursor:pointer;font-size:1rem;line-height:1;display:inline-flex;align-items:center;justify-content:center}.faq-icon-btn:hover:not(:disabled){background:#ffffff14}.faq-icon-btn:disabled{opacity:.35;cursor:not-allowed}.faq-icon-btn.active{background:#ffffff1f;border-color:#ffffff2e}.faq-body{display:grid;grid-template-columns:260px 1fr;min-height:0}.faq-body.sidebar-collapsed{grid-template-columns:0 1fr}.faq-body.sidebar-collapsed .faq-sidebar{display:none}.faq-sidebar{border-right:1px solid var(--shell-border, rgba(255, 255, 255, .08));overflow-y:auto;padding:.75rem .5rem;background:var(--shell-surface, #1d1f23)}.faq-toc-group{margin-bottom:1rem}.faq-toc-category{font-size:.72rem;font-weight:700;letter-spacing:.06em;text-transform:uppercase;opacity:.6;margin:0 0 .4rem .4rem}.faq-toc ul{list-style:none;padding:0;margin:0}.faq-toc-link{display:block;width:100%;text-align:left;background:transparent;border:none;color:inherit;padding:.35rem .5rem;border-radius:4px;cursor:pointer;font-size:.88rem}.faq-toc-link:hover{background:#ffffff0f}.faq-toc-link.active{background:#ffffff1f;font-weight:600}.faq-toc-empty{padding:1rem .5rem;opacity:.7;font-size:.85rem}.faq-search{display:flex;flex-direction:column;gap:.5rem;padding:.25rem}.faq-search-input{width:100%;padding:.5rem .6rem;border-radius:4px;border:1px solid rgba(255,255,255,.18);background:#00000040;color:inherit;font-size:.9rem;box-sizing:border-box}.faq-search-input:focus{outline:none;border-color:#fff6}.faq-search-status{padding:.5rem;opacity:.65;font-size:.82rem;margin:0}.faq-search-results{list-style:none;padding:0;margin:0;display:flex;flex-direction:column;gap:.15rem}.faq-search-result{display:flex;flex-direction:column;gap:.15rem;width:100%;text-align:left;background:transparent;border:none;color:inherit;padding:.45rem .5rem;border-radius:4px;cursor:pointer}.faq-search-result:hover{background:#ffffff0f}.faq-search-result.active{background:#ffffff1f}.faq-search-result-title{font-size:.9rem;font-weight:600}.faq-search-result-category{font-size:.72rem;opacity:.6;text-transform:uppercase;letter-spacing:.04em}.faq-search-result-snippet{font-size:.78rem;opacity:.75;line-height:1.35}.faq-content{overflow-y:auto;padding:1.25rem 2rem;min-width:0}.faq-loading,.faq-empty{opacity:.75;padding:1rem 0}.faq-empty-chain code{background:#ffffff14;padding:.1rem .4rem;border-radius:3px;font-size:.85rem}.faq-empty-hint{margin-top:1rem;opacity:.55;font-size:.85rem}.faq-article{max-width:70ch;line-height:1.55;font-size:.95rem}.faq-article h1,.faq-article h2,.faq-article h3{font-weight:600;margin:1.4rem 0 .6rem}.faq-article h1{font-size:1.4rem;margin-top:0}.faq-article h2{font-size:1.15rem}.faq-article h3{font-size:1rem}.faq-article p{margin:.6rem 0}.faq-article a{color:var(--shell-link, #6ea8fe);text-decoration:none}.faq-article a:hover{text-decoration:underline}.faq-article a[data-faq-kind=external]:after{content:\" \\2197\";opacity:.65;font-size:.85em}.faq-article .faq-app-arrow{opacity:.7;font-size:.9em}.faq-article code{background:#ffffff14;padding:.1rem .35rem;border-radius:3px;font-size:.88em}.faq-article pre{background:#00000059;padding:.75rem 1rem;border-radius:4px;overflow-x:auto}.faq-article pre code{background:transparent;padding:0}.faq-article img{max-width:100%;border-radius:4px;margin:.6rem 0}.faq-article ul,.faq-article ol{padding-left:1.4rem;margin:.6rem 0}.faq-article blockquote{border-left:3px solid var(--shell-border, rgba(255, 255, 255, .15));padding-left:.9rem;margin:.8rem 0;opacity:.85}@media(max-width:900px){.faq-modal{width:100vw;height:100vh;border-radius:0;max-width:none;max-height:none}.faq-backdrop{padding:0}.faq-body{grid-template-columns:1fr}.faq-sidebar{display:none}.faq-body:not(.sidebar-collapsed) .faq-sidebar{display:block;position:absolute;inset:0;z-index:1}}\n"], changeDetection: i0.ChangeDetectionStrategy.OnPush, deferBlockDependencies: [() => [Promise.resolve().then(function () { return faqAuthoringPanel; }).then(m => m.FaqAuthoringPanel)], () => [Promise.resolve().then(function () { return faqAuthoringPanel; }).then(m => m.FaqAuthoringPanel)]] }); }
|
|
1085
|
+
}
|
|
1086
|
+
i0.ɵɵngDeclareClassMetadataAsync({ minVersion: "18.0.0", version: "21.2.13", ngImport: i0, type: FaqModal, resolveDeferredDeps: () => [Promise.resolve().then(function () { return faqAuthoringPanel; }).then(m => m.FaqAuthoringPanel)], resolveMetadata: FaqAuthoringPanel => ({ decorators: [{
|
|
1087
|
+
type: Component,
|
|
1088
|
+
args: [{ selector: 'faq-modal', standalone: true, imports: [FaqAuthoringPanel], changeDetection: ChangeDetectionStrategy.OnPush, template: "@if (open()) {\n <div class=\"faq-backdrop\" (click)=\"onBackdropClick()\" role=\"presentation\">\n <div\n class=\"faq-modal\"\n role=\"dialog\"\n aria-modal=\"true\"\n [attr.aria-label]=\"activeEntry()?.title ?? 'FAQ'\"\n (click)=\"stopPropagation($event)\"\n >\n <header class=\"faq-toolbar\">\n <div class=\"faq-toolbar-nav\">\n <button\n type=\"button\"\n class=\"faq-icon-btn\"\n (click)=\"toggleSidebar()\"\n [attr.aria-label]=\"sidebarCollapsed() ? 'Show contents' : 'Hide contents'\"\n [attr.aria-pressed]=\"!sidebarCollapsed()\"\n >\u2630</button>\n <button\n type=\"button\"\n class=\"faq-icon-btn\"\n (click)=\"goBack()\"\n [disabled]=\"!canGoBack()\"\n aria-label=\"Back\"\n >\u2039</button>\n <button\n type=\"button\"\n class=\"faq-icon-btn\"\n (click)=\"goForward()\"\n [disabled]=\"!canGoForward()\"\n aria-label=\"Forward\"\n >\u203A</button>\n <button\n type=\"button\"\n class=\"faq-icon-btn\"\n (click)=\"goIndex()\"\n aria-label=\"Index\"\n >\u2302</button>\n </div>\n <div class=\"faq-toolbar-title\">\n @if (activeEntry(); as entry) {\n <span class=\"faq-category\">{{ entry.category }}</span>\n <span class=\"faq-sep\">/</span>\n <span class=\"faq-title\">{{ entry.title }}</span>\n } @else {\n <span class=\"faq-title\">FAQ</span>\n }\n </div>\n <div class=\"faq-toolbar-actions\">\n <button\n type=\"button\"\n class=\"faq-icon-btn\"\n (click)=\"toggleSearch()\"\n [class.active]=\"searchMode()\"\n aria-label=\"Search\"\n >\u2315</button>\n <button\n type=\"button\"\n class=\"faq-icon-btn faq-close-btn\"\n (click)=\"close()\"\n aria-label=\"Close\"\n >\u2715</button>\n </div>\n </header>\n\n <div class=\"faq-body\" [class.sidebar-collapsed]=\"sidebarCollapsed()\">\n <aside class=\"faq-sidebar\" aria-label=\"FAQ contents\">\n @if (searchMode()) {\n <div class=\"faq-search\">\n <input\n type=\"search\"\n class=\"faq-search-input\"\n placeholder=\"Search FAQ\u2026\"\n aria-label=\"Search FAQ\"\n [value]=\"searchQuery()\"\n (input)=\"onSearchInput($event)\"\n />\n @if (searchLoading()) {\n <p class=\"faq-search-status\">Building index\u2026</p>\n } @else if (!searchQuery().trim()) {\n <p class=\"faq-search-status\">Type to search titles and content.</p>\n } @else if (searchResults().length === 0) {\n <p class=\"faq-search-status\">No matches.</p>\n } @else {\n <ul class=\"faq-search-results\">\n @for (r of searchResults(); track r.key) {\n <li>\n <button\n type=\"button\"\n class=\"faq-search-result\"\n [class.active]=\"activeEntry()?.key === r.key\"\n (click)=\"onSearchResultClick(r.key)\"\n >\n <span class=\"faq-search-result-title\">{{ r.title }}</span>\n <span class=\"faq-search-result-category\">{{ r.category }}</span>\n @if (r.snippet) {\n <span class=\"faq-search-result-snippet\">{{ r.snippet }}</span>\n }\n </button>\n </li>\n }\n </ul>\n }\n </div>\n } @else {\n <nav class=\"faq-toc\">\n @for (group of toc(); track group.category) {\n <div class=\"faq-toc-group\">\n <h3 class=\"faq-toc-category\">{{ group.category }}</h3>\n <ul>\n @for (entry of group.entries; track entry.key) {\n <li>\n <button\n type=\"button\"\n class=\"faq-toc-link\"\n [class.active]=\"activeEntry()?.key === entry.key\"\n (click)=\"onTocClick(entry.key)\"\n >{{ entry.title }}</button>\n </li>\n }\n </ul>\n </div>\n } @empty {\n <p class=\"faq-toc-empty\">No FAQ entries yet.</p>\n }\n </nav>\n }\n </aside>\n\n <main\n #contentPane\n class=\"faq-content\"\n (click)=\"onContentClick($event)\"\n >\n @if (navigating()) {\n <div class=\"faq-loading\">Loading\u2026</div>\n } @else if (renderedHtml(); as html) {\n <article class=\"faq-article\" [innerHTML]=\"html\"></article>\n @if (devMode && activeEntry(); as entry) {\n @defer (when devMode) {\n <faq-authoring-panel\n mode=\"existing\"\n [key]=\"entry.key\"\n [attempted]=\"attempted()\"\n [entry]=\"entry\"\n />\n }\n }\n } @else {\n <div class=\"faq-empty\">\n <h2>No FAQ available</h2>\n <p>No content is registered for this route.</p>\n @if (attempted().length) {\n <p class=\"faq-empty-chain\">\n Attempted keys:\n <code>{{ attempted().join(' \u2192 ') }}</code>\n </p>\n }\n @if (devMode && attempted().length) {\n @defer (when devMode) {\n <faq-authoring-panel\n mode=\"missing\"\n [key]=\"attempted()[0]\"\n [attempted]=\"attempted()\"\n />\n } @loading (minimum 100ms) {\n <p class=\"faq-empty-hint\">Loading authoring panel\u2026</p>\n }\n }\n </div>\n }\n </main>\n </div>\n </div>\n </div>\n}\n", styles: [":host{display:contents}.faq-backdrop{position:fixed;inset:0;z-index:1100;background:#0000008c;display:flex;align-items:center;justify-content:center;padding:5vh 5vw}.faq-modal{width:80vw;height:80vh;max-width:1600px;max-height:1100px;background:var(--shell-surface, #1d1f23);color:var(--shell-fg, #e6e6e6);border-radius:8px;box-shadow:0 12px 32px #00000080;display:grid;grid-template-rows:auto 1fr;overflow:hidden;font-family:var(--shell-font, system-ui, sans-serif)}.faq-toolbar{display:grid;grid-template-columns:auto 1fr auto;align-items:center;gap:.5rem;padding:.5rem .75rem;border-bottom:1px solid var(--shell-border, rgba(255, 255, 255, .08));background:var(--shell-surface-2, rgba(255, 255, 255, .03))}.faq-toolbar-nav,.faq-toolbar-actions{display:flex;gap:.25rem}.faq-toolbar-title{text-align:center;font-size:.95rem;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.faq-category{opacity:.7;margin-right:.25rem}.faq-sep{opacity:.4;margin:0 .25rem}.faq-title{font-weight:600}.faq-icon-btn{background:transparent;border:1px solid transparent;color:inherit;width:2rem;height:2rem;border-radius:4px;cursor:pointer;font-size:1rem;line-height:1;display:inline-flex;align-items:center;justify-content:center}.faq-icon-btn:hover:not(:disabled){background:#ffffff14}.faq-icon-btn:disabled{opacity:.35;cursor:not-allowed}.faq-icon-btn.active{background:#ffffff1f;border-color:#ffffff2e}.faq-body{display:grid;grid-template-columns:260px 1fr;min-height:0}.faq-body.sidebar-collapsed{grid-template-columns:0 1fr}.faq-body.sidebar-collapsed .faq-sidebar{display:none}.faq-sidebar{border-right:1px solid var(--shell-border, rgba(255, 255, 255, .08));overflow-y:auto;padding:.75rem .5rem;background:var(--shell-surface, #1d1f23)}.faq-toc-group{margin-bottom:1rem}.faq-toc-category{font-size:.72rem;font-weight:700;letter-spacing:.06em;text-transform:uppercase;opacity:.6;margin:0 0 .4rem .4rem}.faq-toc ul{list-style:none;padding:0;margin:0}.faq-toc-link{display:block;width:100%;text-align:left;background:transparent;border:none;color:inherit;padding:.35rem .5rem;border-radius:4px;cursor:pointer;font-size:.88rem}.faq-toc-link:hover{background:#ffffff0f}.faq-toc-link.active{background:#ffffff1f;font-weight:600}.faq-toc-empty{padding:1rem .5rem;opacity:.7;font-size:.85rem}.faq-search{display:flex;flex-direction:column;gap:.5rem;padding:.25rem}.faq-search-input{width:100%;padding:.5rem .6rem;border-radius:4px;border:1px solid rgba(255,255,255,.18);background:#00000040;color:inherit;font-size:.9rem;box-sizing:border-box}.faq-search-input:focus{outline:none;border-color:#fff6}.faq-search-status{padding:.5rem;opacity:.65;font-size:.82rem;margin:0}.faq-search-results{list-style:none;padding:0;margin:0;display:flex;flex-direction:column;gap:.15rem}.faq-search-result{display:flex;flex-direction:column;gap:.15rem;width:100%;text-align:left;background:transparent;border:none;color:inherit;padding:.45rem .5rem;border-radius:4px;cursor:pointer}.faq-search-result:hover{background:#ffffff0f}.faq-search-result.active{background:#ffffff1f}.faq-search-result-title{font-size:.9rem;font-weight:600}.faq-search-result-category{font-size:.72rem;opacity:.6;text-transform:uppercase;letter-spacing:.04em}.faq-search-result-snippet{font-size:.78rem;opacity:.75;line-height:1.35}.faq-content{overflow-y:auto;padding:1.25rem 2rem;min-width:0}.faq-loading,.faq-empty{opacity:.75;padding:1rem 0}.faq-empty-chain code{background:#ffffff14;padding:.1rem .4rem;border-radius:3px;font-size:.85rem}.faq-empty-hint{margin-top:1rem;opacity:.55;font-size:.85rem}.faq-article{max-width:70ch;line-height:1.55;font-size:.95rem}.faq-article h1,.faq-article h2,.faq-article h3{font-weight:600;margin:1.4rem 0 .6rem}.faq-article h1{font-size:1.4rem;margin-top:0}.faq-article h2{font-size:1.15rem}.faq-article h3{font-size:1rem}.faq-article p{margin:.6rem 0}.faq-article a{color:var(--shell-link, #6ea8fe);text-decoration:none}.faq-article a:hover{text-decoration:underline}.faq-article a[data-faq-kind=external]:after{content:\" \\2197\";opacity:.65;font-size:.85em}.faq-article .faq-app-arrow{opacity:.7;font-size:.9em}.faq-article code{background:#ffffff14;padding:.1rem .35rem;border-radius:3px;font-size:.88em}.faq-article pre{background:#00000059;padding:.75rem 1rem;border-radius:4px;overflow-x:auto}.faq-article pre code{background:transparent;padding:0}.faq-article img{max-width:100%;border-radius:4px;margin:.6rem 0}.faq-article ul,.faq-article ol{padding-left:1.4rem;margin:.6rem 0}.faq-article blockquote{border-left:3px solid var(--shell-border, rgba(255, 255, 255, .15));padding-left:.9rem;margin:.8rem 0;opacity:.85}@media(max-width:900px){.faq-modal{width:100vw;height:100vh;border-radius:0;max-width:none;max-height:none}.faq-backdrop{padding:0}.faq-body{grid-template-columns:1fr}.faq-sidebar{display:none}.faq-body:not(.sidebar-collapsed) .faq-sidebar{display:block;position:absolute;inset:0;z-index:1}}\n"] }]
|
|
1089
|
+
}], ctorParameters: () => [], propDecorators: { contentPaneRef: [{
|
|
1090
|
+
type: ViewChild,
|
|
1091
|
+
args: ['contentPane']
|
|
1092
|
+
}], onEscape: [{
|
|
1093
|
+
type: HostListener,
|
|
1094
|
+
args: ['document:keydown.escape']
|
|
1095
|
+
}] } }) });
|
|
1096
|
+
/**
|
|
1097
|
+
* Builds a short contextual snippet around the first body match. Falls back
|
|
1098
|
+
* to the head of the body when the only matches are in the title.
|
|
1099
|
+
*/
|
|
1100
|
+
function buildSnippet(body, matches) {
|
|
1101
|
+
const RADIUS = 60;
|
|
1102
|
+
const MAX = 180;
|
|
1103
|
+
const bodyMatch = matches?.find((m) => m.key === 'body' && m.indices.length);
|
|
1104
|
+
if (!bodyMatch) {
|
|
1105
|
+
return body.replace(/\s+/g, ' ').slice(0, MAX).trim();
|
|
1106
|
+
}
|
|
1107
|
+
const [start, end] = bodyMatch.indices[0];
|
|
1108
|
+
const from = Math.max(0, start - RADIUS);
|
|
1109
|
+
const to = Math.min(body.length, end + RADIUS);
|
|
1110
|
+
let snippet = body.slice(from, to).replace(/\s+/g, ' ').trim();
|
|
1111
|
+
if (from > 0)
|
|
1112
|
+
snippet = '…' + snippet;
|
|
1113
|
+
if (to < body.length)
|
|
1114
|
+
snippet = snippet + '…';
|
|
1115
|
+
return snippet.length > MAX ? snippet.slice(0, MAX) + '…' : snippet;
|
|
1116
|
+
}
|
|
1117
|
+
|
|
1118
|
+
/**
|
|
1119
|
+
* E11 — Dev-only FAQ authoring panel.
|
|
1120
|
+
*
|
|
1121
|
+
* Two modes, both dynamic-imported by `FaqModal` and gated behind
|
|
1122
|
+
* `FAQ_DEV_MODE`:
|
|
1123
|
+
* - `missing`: full scaffold panel rendered when no FAQ resolved.
|
|
1124
|
+
* - `existing`: collapsed source strip rendered below the article when an
|
|
1125
|
+
* entry IS resolved.
|
|
1126
|
+
*
|
|
1127
|
+
* The panel never imports `environment.*` directly — gating is the modal's
|
|
1128
|
+
* responsibility (via the FAQ_DEV_MODE token + dynamic import).
|
|
1129
|
+
*/
|
|
1130
|
+
class FaqAuthoringPanel {
|
|
1131
|
+
constructor() {
|
|
1132
|
+
this.faq = inject(FaqService);
|
|
1133
|
+
this.workspaceRoot = inject(FAQ_DEV_WORKSPACE_ROOT);
|
|
1134
|
+
this.editor = inject(FAQ_DEV_EDITOR);
|
|
1135
|
+
/** `'missing'` shows the full scaffold; `'existing'` shows the collapsed strip. */
|
|
1136
|
+
this.mode = input.required(...(ngDevMode ? [{ debugName: "mode" }] : /* istanbul ignore next */ []));
|
|
1137
|
+
/**
|
|
1138
|
+
* The dot-notation key actually being authored. For `missing` mode this is
|
|
1139
|
+
* the deepest attempted key (chain[0]); for `existing` it's the resolved key.
|
|
1140
|
+
*/
|
|
1141
|
+
this.key = input.required(...(ngDevMode ? [{ debugName: "key" }] : /* istanbul ignore next */ []));
|
|
1142
|
+
/** Fallback chain that was attempted. For `missing` every entry is missing. */
|
|
1143
|
+
this.attempted = input([], ...(ngDevMode ? [{ debugName: "attempted" }] : /* istanbul ignore next */ []));
|
|
1144
|
+
/** Resolved entry — only set in `existing` mode. */
|
|
1145
|
+
this.entry = input(null, ...(ngDevMode ? [{ debugName: "entry" }] : /* istanbul ignore next */ []));
|
|
1146
|
+
this.expanded = signal(false, ...(ngDevMode ? [{ debugName: "expanded" }] : /* istanbul ignore next */ []));
|
|
1147
|
+
this.showAllFiles = signal(false, ...(ngDevMode ? [{ debugName: "showAllFiles" }] : /* istanbul ignore next */ []));
|
|
1148
|
+
this.copyState = signal(null, ...(ngDevMode ? [{ debugName: "copyState" }] : /* istanbul ignore next */ []));
|
|
1149
|
+
this.relPath = computed(() => {
|
|
1150
|
+
const e = this.entry();
|
|
1151
|
+
if (e)
|
|
1152
|
+
return `src/assets/faq/${e.file}.md`;
|
|
1153
|
+
return `src/assets/faq/${keyToFilePath(this.key())}.md`;
|
|
1154
|
+
}, ...(ngDevMode ? [{ debugName: "relPath" }] : /* istanbul ignore next */ []));
|
|
1155
|
+
this.absPath = computed(() => {
|
|
1156
|
+
const rel = this.relPath();
|
|
1157
|
+
return this.workspaceRoot ? joinPosix(this.workspaceRoot, rel) : rel;
|
|
1158
|
+
}, ...(ngDevMode ? [{ debugName: "absPath" }] : /* istanbul ignore next */ []));
|
|
1159
|
+
this.editorHref = computed(() => {
|
|
1160
|
+
const abs = this.absPath();
|
|
1161
|
+
if (this.editor === 'vscode')
|
|
1162
|
+
return `vscode://file/${abs}`;
|
|
1163
|
+
return `webstorm://open?file=${encodeURI(abs)}`;
|
|
1164
|
+
}, ...(ngDevMode ? [{ debugName: "editorHref" }] : /* istanbul ignore next */ []));
|
|
1165
|
+
this.editorLabel = computed(() => this.editor === 'vscode' ? 'Open in VS Code' : 'Open in WebStorm', ...(ngDevMode ? [{ debugName: "editorLabel" }] : /* istanbul ignore next */ []));
|
|
1166
|
+
this.scaffoldCmd = computed(() => `npm run faq:new -- ${this.key()}`, ...(ngDevMode ? [{ debugName: "scaffoldCmd" }] : /* istanbul ignore next */ []));
|
|
1167
|
+
this.frontmatterScaffold = computed(() => {
|
|
1168
|
+
const k = this.key();
|
|
1169
|
+
const title = humanize(k);
|
|
1170
|
+
const category = humanize(k.split('.')[0]);
|
|
1171
|
+
return [
|
|
1172
|
+
'---',
|
|
1173
|
+
`title: "${title}"`,
|
|
1174
|
+
`category: "${category}"`,
|
|
1175
|
+
'audience: authenticated',
|
|
1176
|
+
'seo-ready: false',
|
|
1177
|
+
'description: ""',
|
|
1178
|
+
'---',
|
|
1179
|
+
'',
|
|
1180
|
+
`# ${title}`,
|
|
1181
|
+
'',
|
|
1182
|
+
`TODO: write content for "${title}".`,
|
|
1183
|
+
'',
|
|
1184
|
+
].join('\n');
|
|
1185
|
+
}, ...(ngDevMode ? [{ debugName: "frontmatterScaffold" }] : /* istanbul ignore next */ []));
|
|
1186
|
+
this.allEntries = computed(() => this.faq.entries(), ...(ngDevMode ? [{ debugName: "allEntries" }] : /* istanbul ignore next */ []));
|
|
1187
|
+
this.missedChain = computed(() => {
|
|
1188
|
+
const present = new Set(this.allEntries().map((e) => e.key));
|
|
1189
|
+
return this.attempted().map((k) => ({ key: k, present: present.has(k) }));
|
|
1190
|
+
}, ...(ngDevMode ? [{ debugName: "missedChain" }] : /* istanbul ignore next */ []));
|
|
1191
|
+
}
|
|
1192
|
+
toggleExpanded() {
|
|
1193
|
+
this.expanded.update((v) => !v);
|
|
1194
|
+
}
|
|
1195
|
+
toggleAllFiles() {
|
|
1196
|
+
this.showAllFiles.update((v) => !v);
|
|
1197
|
+
}
|
|
1198
|
+
async copy(text, label) {
|
|
1199
|
+
try {
|
|
1200
|
+
await navigator.clipboard.writeText(text);
|
|
1201
|
+
this.copyState.set(label);
|
|
1202
|
+
setTimeout(() => {
|
|
1203
|
+
if (this.copyState() === label)
|
|
1204
|
+
this.copyState.set(null);
|
|
1205
|
+
}, 1500);
|
|
1206
|
+
}
|
|
1207
|
+
catch {
|
|
1208
|
+
this.copyState.set(`${label}:err`);
|
|
1209
|
+
}
|
|
1210
|
+
}
|
|
1211
|
+
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.13", ngImport: i0, type: FaqAuthoringPanel, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
|
|
1212
|
+
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.13", type: FaqAuthoringPanel, isStandalone: true, selector: "faq-authoring-panel", inputs: { mode: { classPropertyName: "mode", publicName: "mode", isSignal: true, isRequired: true, transformFunction: null }, key: { classPropertyName: "key", publicName: "key", isSignal: true, isRequired: true, transformFunction: null }, attempted: { classPropertyName: "attempted", publicName: "attempted", isSignal: true, isRequired: false, transformFunction: null }, entry: { classPropertyName: "entry", publicName: "entry", isSignal: true, isRequired: false, transformFunction: null } }, ngImport: i0, template: "@if (mode() === 'missing') {\n <section class=\"faq-authoring missing\" aria-label=\"FAQ authoring panel\">\n <header class=\"faq-authoring-header\">\n <span class=\"faq-authoring-tag\">[dev]</span>\n <h3>Start authoring this FAQ</h3>\n </header>\n\n <dl class=\"faq-authoring-meta\">\n <dt>Key</dt>\n <dd><code>{{ key() }}</code></dd>\n\n <dt>Fallback chain</dt>\n <dd>\n <ul class=\"faq-authoring-chain\">\n @for (step of missedChain(); track step.key) {\n <li [ngClass]=\"step.present ? 'present' : 'missed'\">\n <span class=\"mark\">{{ step.present ? '\u2713' : '\u2717' }}</span>\n <code>{{ step.key }}</code>\n </li>\n }\n </ul>\n </dd>\n\n <dt>Target file</dt>\n <dd>\n <code>{{ relPath() }}</code>\n <a\n class=\"faq-authoring-editor\"\n [href]=\"editorHref()\"\n >{{ editorLabel() }} \u2197</a>\n </dd>\n </dl>\n\n <div class=\"faq-authoring-block\">\n <div class=\"faq-authoring-block-head\">\n <span>Scaffold via CLI</span>\n <button type=\"button\" (click)=\"copy(scaffoldCmd(), 'cmd')\">\n {{ copyState() === 'cmd' ? 'Copied \u2713' : 'Copy' }}\n </button>\n </div>\n <pre><code>{{ scaffoldCmd() }}</code></pre>\n </div>\n\n <div class=\"faq-authoring-block\">\n <div class=\"faq-authoring-block-head\">\n <span>Frontmatter scaffold</span>\n <button type=\"button\" (click)=\"copy(frontmatterScaffold(), 'fm')\">\n {{ copyState() === 'fm' ? 'Copied \u2713' : 'Copy' }}\n </button>\n </div>\n <pre><code>{{ frontmatterScaffold() }}</code></pre>\n </div>\n\n <details class=\"faq-authoring-cheat\">\n <summary>Link syntax cheat sheet</summary>\n <ul>\n <li><code>[label](faq:trade.segments)</code> \u2014 in-modal nav</li>\n <li><code>[label](app:/settings)</code> \u2014 navigates the app (modal closes)</li>\n <li><code>[label](https://example.com)</code> \u2014 external (new tab)</li>\n <li><code>[label](#section-id)</code> \u2014 intra-page anchor</li>\n <li><code></code> \u2014 image (drop into <code>src/assets/faq/images/</code>)</li>\n </ul>\n </details>\n\n <details class=\"faq-authoring-all\" [open]=\"showAllFiles()\">\n <summary (click)=\"toggleAllFiles(); $event.preventDefault()\">\n Show all FAQ files ({{ allEntries().length }})\n </summary>\n <ul class=\"faq-authoring-file-list\">\n @for (e of allEntries(); track e.key) {\n <li>\n <code>{{ e.key }}</code>\n <span class=\"faq-authoring-file-path\">src/assets/faq/{{ e.file }}.md</span>\n </li>\n } @empty {\n <li class=\"empty\">Manifest is empty.</li>\n }\n </ul>\n </details>\n </section>\n} @else {\n <aside class=\"faq-authoring existing\" aria-label=\"FAQ source info\">\n <div class=\"faq-authoring-strip\">\n <span class=\"faq-authoring-tag\">[dev]</span>\n <span>Key: <code>{{ key() }}</code></span>\n <span class=\"sep\">\u2022</span>\n <span>Source: <code>{{ relPath() }}</code></span>\n <a class=\"faq-authoring-editor\" [href]=\"editorHref()\">{{ editorLabel() }} \u2197</a>\n <button type=\"button\" class=\"faq-authoring-more\" (click)=\"toggleExpanded()\">\n {{ expanded() ? '\u25B2 Less' : '\u25BC More' }}\n </button>\n </div>\n @if (expanded()) {\n <div class=\"faq-authoring-expanded\">\n <div>\n <strong>Fallback chain attempted:</strong>\n <ul class=\"faq-authoring-chain inline\">\n @for (step of missedChain(); track step.key) {\n <li [ngClass]=\"step.present ? 'present' : 'missed'\">\n <span class=\"mark\">{{ step.present ? '\u2713' : '\u2717' }}</span>\n <code>{{ step.key }}</code>\n </li>\n }\n </ul>\n </div>\n <details class=\"faq-authoring-cheat\" open>\n <summary>Link syntax cheat sheet</summary>\n <ul>\n <li><code>[label](faq:trade.segments)</code> \u2014 in-modal nav</li>\n <li><code>[label](app:/settings)</code> \u2014 navigates the app</li>\n <li><code>[label](https://example.com)</code> \u2014 external</li>\n <li><code>[label](#section-id)</code> \u2014 intra-page anchor</li>\n <li><code></code> \u2014 image</li>\n </ul>\n </details>\n <div class=\"faq-authoring-rescaffold\">\n <span>Re-scaffold (overwrites if you delete first):</span>\n <code>{{ scaffoldCmd() }}</code>\n <button type=\"button\" (click)=\"copy(scaffoldCmd(), 'cmd')\">\n {{ copyState() === 'cmd' ? 'Copied \u2713' : 'Copy' }}\n </button>\n </div>\n </div>\n }\n </aside>\n}\n", styles: [":host{display:block;font-family:var(--shell-font, system-ui, sans-serif);font-size:.875rem;color:var(--shell-fg, #e6e6e6)}.faq-authoring{border:1px dashed var(--shell-accent, #6aa0ff);border-radius:6px;background:#6aa0ff0a}.faq-authoring.missing{padding:1rem 1.25rem;display:grid;gap:.85rem}.faq-authoring.existing{margin-top:1.5rem;padding:.5rem .75rem;background:#6aa0ff08;border-style:dotted}.faq-authoring-header{display:flex;align-items:baseline;gap:.5rem;margin:0}.faq-authoring-header h3{margin:0;font-size:1rem}.faq-authoring-tag{display:inline-block;padding:.05rem .4rem;background:var(--shell-accent, #6aa0ff);color:#0b0d11;border-radius:3px;font-size:.7rem;font-weight:700;letter-spacing:.04em;text-transform:uppercase}.faq-authoring-meta{display:grid;grid-template-columns:max-content 1fr;gap:.35rem .85rem;margin:0}.faq-authoring-meta dt{font-weight:600;opacity:.75}.faq-authoring-meta dd{margin:0}.faq-authoring code{font-family:var(--shell-mono, ui-monospace, monospace);background:#ffffff0f;padding:.05rem .3rem;border-radius:3px;font-size:.85em}.faq-authoring-chain{list-style:none;padding:0;margin:0;display:flex;flex-wrap:wrap;gap:.4rem}.faq-authoring-chain.inline{display:inline-flex;margin-left:.5rem}.faq-authoring-chain li{display:inline-flex;align-items:center;gap:.25rem}.faq-authoring-chain .mark{font-weight:700}.faq-authoring-chain .missed .mark{color:#ff7676}.faq-authoring-chain .present .mark{color:#8ad27a}.faq-authoring-editor{margin-left:.75rem;color:var(--shell-accent, #6aa0ff);text-decoration:none}.faq-authoring-editor:hover{text-decoration:underline}.faq-authoring-block{border:1px solid var(--shell-border, rgba(255, 255, 255, .08));border-radius:4px;overflow:hidden}.faq-authoring-block-head{display:flex;justify-content:space-between;align-items:center;padding:.4rem .6rem;background:#ffffff0a;font-weight:600;font-size:.8rem}.faq-authoring-block-head button,.faq-authoring-more,.faq-authoring-rescaffold button{background:var(--shell-accent, #6aa0ff);color:#0b0d11;border:0;border-radius:3px;padding:.2rem .55rem;font-size:.75rem;cursor:pointer;font-family:inherit}.faq-authoring-block pre{margin:0;padding:.6rem .75rem;overflow-x:auto;background:#00000040;font-size:.8rem}.faq-authoring-cheat,.faq-authoring-all{font-size:.85rem}.faq-authoring-cheat summary,.faq-authoring-all summary{cursor:pointer;opacity:.85}.faq-authoring-cheat ul,.faq-authoring-file-list{margin:.4rem 0 0;padding-left:1.25rem}.faq-authoring-file-list{list-style:none;padding-left:0;max-height:200px;overflow-y:auto}.faq-authoring-file-list li{display:flex;justify-content:space-between;gap:.5rem;padding:.15rem 0;border-bottom:1px solid rgba(255,255,255,.04)}.faq-authoring-file-path{opacity:.6;font-family:var(--shell-mono, ui-monospace, monospace);font-size:.75rem}.faq-authoring-strip{display:flex;flex-wrap:wrap;align-items:center;gap:.5rem;font-size:.8rem}.faq-authoring-strip .sep{opacity:.4}.faq-authoring-more{margin-left:auto}.faq-authoring-expanded{margin-top:.6rem;padding-top:.5rem;border-top:1px solid rgba(255,255,255,.08);display:grid;gap:.6rem}.faq-authoring-rescaffold{display:flex;align-items:center;gap:.5rem;flex-wrap:wrap;font-size:.8rem}\n"], dependencies: [{ kind: "directive", type: NgClass, selector: "[ngClass]", inputs: ["class", "ngClass"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
|
|
1213
|
+
}
|
|
1214
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.13", ngImport: i0, type: FaqAuthoringPanel, decorators: [{
|
|
1215
|
+
type: Component,
|
|
1216
|
+
args: [{ selector: 'faq-authoring-panel', standalone: true, imports: [NgClass], changeDetection: ChangeDetectionStrategy.OnPush, template: "@if (mode() === 'missing') {\n <section class=\"faq-authoring missing\" aria-label=\"FAQ authoring panel\">\n <header class=\"faq-authoring-header\">\n <span class=\"faq-authoring-tag\">[dev]</span>\n <h3>Start authoring this FAQ</h3>\n </header>\n\n <dl class=\"faq-authoring-meta\">\n <dt>Key</dt>\n <dd><code>{{ key() }}</code></dd>\n\n <dt>Fallback chain</dt>\n <dd>\n <ul class=\"faq-authoring-chain\">\n @for (step of missedChain(); track step.key) {\n <li [ngClass]=\"step.present ? 'present' : 'missed'\">\n <span class=\"mark\">{{ step.present ? '\u2713' : '\u2717' }}</span>\n <code>{{ step.key }}</code>\n </li>\n }\n </ul>\n </dd>\n\n <dt>Target file</dt>\n <dd>\n <code>{{ relPath() }}</code>\n <a\n class=\"faq-authoring-editor\"\n [href]=\"editorHref()\"\n >{{ editorLabel() }} \u2197</a>\n </dd>\n </dl>\n\n <div class=\"faq-authoring-block\">\n <div class=\"faq-authoring-block-head\">\n <span>Scaffold via CLI</span>\n <button type=\"button\" (click)=\"copy(scaffoldCmd(), 'cmd')\">\n {{ copyState() === 'cmd' ? 'Copied \u2713' : 'Copy' }}\n </button>\n </div>\n <pre><code>{{ scaffoldCmd() }}</code></pre>\n </div>\n\n <div class=\"faq-authoring-block\">\n <div class=\"faq-authoring-block-head\">\n <span>Frontmatter scaffold</span>\n <button type=\"button\" (click)=\"copy(frontmatterScaffold(), 'fm')\">\n {{ copyState() === 'fm' ? 'Copied \u2713' : 'Copy' }}\n </button>\n </div>\n <pre><code>{{ frontmatterScaffold() }}</code></pre>\n </div>\n\n <details class=\"faq-authoring-cheat\">\n <summary>Link syntax cheat sheet</summary>\n <ul>\n <li><code>[label](faq:trade.segments)</code> \u2014 in-modal nav</li>\n <li><code>[label](app:/settings)</code> \u2014 navigates the app (modal closes)</li>\n <li><code>[label](https://example.com)</code> \u2014 external (new tab)</li>\n <li><code>[label](#section-id)</code> \u2014 intra-page anchor</li>\n <li><code></code> \u2014 image (drop into <code>src/assets/faq/images/</code>)</li>\n </ul>\n </details>\n\n <details class=\"faq-authoring-all\" [open]=\"showAllFiles()\">\n <summary (click)=\"toggleAllFiles(); $event.preventDefault()\">\n Show all FAQ files ({{ allEntries().length }})\n </summary>\n <ul class=\"faq-authoring-file-list\">\n @for (e of allEntries(); track e.key) {\n <li>\n <code>{{ e.key }}</code>\n <span class=\"faq-authoring-file-path\">src/assets/faq/{{ e.file }}.md</span>\n </li>\n } @empty {\n <li class=\"empty\">Manifest is empty.</li>\n }\n </ul>\n </details>\n </section>\n} @else {\n <aside class=\"faq-authoring existing\" aria-label=\"FAQ source info\">\n <div class=\"faq-authoring-strip\">\n <span class=\"faq-authoring-tag\">[dev]</span>\n <span>Key: <code>{{ key() }}</code></span>\n <span class=\"sep\">\u2022</span>\n <span>Source: <code>{{ relPath() }}</code></span>\n <a class=\"faq-authoring-editor\" [href]=\"editorHref()\">{{ editorLabel() }} \u2197</a>\n <button type=\"button\" class=\"faq-authoring-more\" (click)=\"toggleExpanded()\">\n {{ expanded() ? '\u25B2 Less' : '\u25BC More' }}\n </button>\n </div>\n @if (expanded()) {\n <div class=\"faq-authoring-expanded\">\n <div>\n <strong>Fallback chain attempted:</strong>\n <ul class=\"faq-authoring-chain inline\">\n @for (step of missedChain(); track step.key) {\n <li [ngClass]=\"step.present ? 'present' : 'missed'\">\n <span class=\"mark\">{{ step.present ? '\u2713' : '\u2717' }}</span>\n <code>{{ step.key }}</code>\n </li>\n }\n </ul>\n </div>\n <details class=\"faq-authoring-cheat\" open>\n <summary>Link syntax cheat sheet</summary>\n <ul>\n <li><code>[label](faq:trade.segments)</code> \u2014 in-modal nav</li>\n <li><code>[label](app:/settings)</code> \u2014 navigates the app</li>\n <li><code>[label](https://example.com)</code> \u2014 external</li>\n <li><code>[label](#section-id)</code> \u2014 intra-page anchor</li>\n <li><code></code> \u2014 image</li>\n </ul>\n </details>\n <div class=\"faq-authoring-rescaffold\">\n <span>Re-scaffold (overwrites if you delete first):</span>\n <code>{{ scaffoldCmd() }}</code>\n <button type=\"button\" (click)=\"copy(scaffoldCmd(), 'cmd')\">\n {{ copyState() === 'cmd' ? 'Copied \u2713' : 'Copy' }}\n </button>\n </div>\n </div>\n }\n </aside>\n}\n", styles: [":host{display:block;font-family:var(--shell-font, system-ui, sans-serif);font-size:.875rem;color:var(--shell-fg, #e6e6e6)}.faq-authoring{border:1px dashed var(--shell-accent, #6aa0ff);border-radius:6px;background:#6aa0ff0a}.faq-authoring.missing{padding:1rem 1.25rem;display:grid;gap:.85rem}.faq-authoring.existing{margin-top:1.5rem;padding:.5rem .75rem;background:#6aa0ff08;border-style:dotted}.faq-authoring-header{display:flex;align-items:baseline;gap:.5rem;margin:0}.faq-authoring-header h3{margin:0;font-size:1rem}.faq-authoring-tag{display:inline-block;padding:.05rem .4rem;background:var(--shell-accent, #6aa0ff);color:#0b0d11;border-radius:3px;font-size:.7rem;font-weight:700;letter-spacing:.04em;text-transform:uppercase}.faq-authoring-meta{display:grid;grid-template-columns:max-content 1fr;gap:.35rem .85rem;margin:0}.faq-authoring-meta dt{font-weight:600;opacity:.75}.faq-authoring-meta dd{margin:0}.faq-authoring code{font-family:var(--shell-mono, ui-monospace, monospace);background:#ffffff0f;padding:.05rem .3rem;border-radius:3px;font-size:.85em}.faq-authoring-chain{list-style:none;padding:0;margin:0;display:flex;flex-wrap:wrap;gap:.4rem}.faq-authoring-chain.inline{display:inline-flex;margin-left:.5rem}.faq-authoring-chain li{display:inline-flex;align-items:center;gap:.25rem}.faq-authoring-chain .mark{font-weight:700}.faq-authoring-chain .missed .mark{color:#ff7676}.faq-authoring-chain .present .mark{color:#8ad27a}.faq-authoring-editor{margin-left:.75rem;color:var(--shell-accent, #6aa0ff);text-decoration:none}.faq-authoring-editor:hover{text-decoration:underline}.faq-authoring-block{border:1px solid var(--shell-border, rgba(255, 255, 255, .08));border-radius:4px;overflow:hidden}.faq-authoring-block-head{display:flex;justify-content:space-between;align-items:center;padding:.4rem .6rem;background:#ffffff0a;font-weight:600;font-size:.8rem}.faq-authoring-block-head button,.faq-authoring-more,.faq-authoring-rescaffold button{background:var(--shell-accent, #6aa0ff);color:#0b0d11;border:0;border-radius:3px;padding:.2rem .55rem;font-size:.75rem;cursor:pointer;font-family:inherit}.faq-authoring-block pre{margin:0;padding:.6rem .75rem;overflow-x:auto;background:#00000040;font-size:.8rem}.faq-authoring-cheat,.faq-authoring-all{font-size:.85rem}.faq-authoring-cheat summary,.faq-authoring-all summary{cursor:pointer;opacity:.85}.faq-authoring-cheat ul,.faq-authoring-file-list{margin:.4rem 0 0;padding-left:1.25rem}.faq-authoring-file-list{list-style:none;padding-left:0;max-height:200px;overflow-y:auto}.faq-authoring-file-list li{display:flex;justify-content:space-between;gap:.5rem;padding:.15rem 0;border-bottom:1px solid rgba(255,255,255,.04)}.faq-authoring-file-path{opacity:.6;font-family:var(--shell-mono, ui-monospace, monospace);font-size:.75rem}.faq-authoring-strip{display:flex;flex-wrap:wrap;align-items:center;gap:.5rem;font-size:.8rem}.faq-authoring-strip .sep{opacity:.4}.faq-authoring-more{margin-left:auto}.faq-authoring-expanded{margin-top:.6rem;padding-top:.5rem;border-top:1px solid rgba(255,255,255,.08);display:grid;gap:.6rem}.faq-authoring-rescaffold{display:flex;align-items:center;gap:.5rem;flex-wrap:wrap;font-size:.8rem}\n"] }]
|
|
1217
|
+
}], propDecorators: { mode: [{ type: i0.Input, args: [{ isSignal: true, alias: "mode", required: true }] }], key: [{ type: i0.Input, args: [{ isSignal: true, alias: "key", required: true }] }], attempted: [{ type: i0.Input, args: [{ isSignal: true, alias: "attempted", required: false }] }], entry: [{ type: i0.Input, args: [{ isSignal: true, alias: "entry", required: false }] }] } });
|
|
1218
|
+
function keyToFilePath(key) {
|
|
1219
|
+
return key.split('.').join('/');
|
|
1220
|
+
}
|
|
1221
|
+
function humanize(s) {
|
|
1222
|
+
return s
|
|
1223
|
+
.split(/[.\-_/]/)
|
|
1224
|
+
.filter(Boolean)
|
|
1225
|
+
.map((seg) => seg.charAt(0).toUpperCase() + seg.slice(1))
|
|
1226
|
+
.join(' ');
|
|
1227
|
+
}
|
|
1228
|
+
function joinPosix(a, b) {
|
|
1229
|
+
const left = a.endsWith('/') ? a.slice(0, -1) : a;
|
|
1230
|
+
const right = b.startsWith('/') ? b.slice(1) : b;
|
|
1231
|
+
return `${left}/${right}`;
|
|
1232
|
+
}
|
|
1233
|
+
|
|
1234
|
+
var faqAuthoringPanel = /*#__PURE__*/Object.freeze({
|
|
1235
|
+
__proto__: null,
|
|
1236
|
+
FaqAuthoringPanel: FaqAuthoringPanel
|
|
1237
|
+
});
|
|
1238
|
+
|
|
322
1239
|
class ThemeService {
|
|
323
1240
|
constructor() {
|
|
324
1241
|
this.mode = signal('dark', ...(ngDevMode ? [{ debugName: "mode" }] : /* istanbul ignore next */ []));
|
|
325
1242
|
this.isDark = computed(() => this.mode() === 'dark', ...(ngDevMode ? [{ debugName: "isDark" }] : /* istanbul ignore next */ []));
|
|
326
1243
|
}
|
|
327
|
-
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.
|
|
328
|
-
static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.
|
|
1244
|
+
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.13", ngImport: i0, type: ThemeService, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
|
|
1245
|
+
static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.13", ngImport: i0, type: ThemeService, providedIn: 'root' }); }
|
|
329
1246
|
}
|
|
330
|
-
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.
|
|
1247
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.13", ngImport: i0, type: ThemeService, decorators: [{
|
|
331
1248
|
type: Injectable,
|
|
332
1249
|
args: [{ providedIn: 'root' }]
|
|
333
1250
|
}] });
|
|
@@ -339,10 +1256,10 @@ class TerminalFocusService {
|
|
|
339
1256
|
requestFocus() {
|
|
340
1257
|
this.focusRequested.update(v => v + 1);
|
|
341
1258
|
}
|
|
342
|
-
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.
|
|
343
|
-
static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.
|
|
1259
|
+
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.13", ngImport: i0, type: TerminalFocusService, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
|
|
1260
|
+
static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.13", ngImport: i0, type: TerminalFocusService, providedIn: 'root' }); }
|
|
344
1261
|
}
|
|
345
|
-
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.
|
|
1262
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.13", ngImport: i0, type: TerminalFocusService, decorators: [{
|
|
346
1263
|
type: Injectable,
|
|
347
1264
|
args: [{ providedIn: 'root' }]
|
|
348
1265
|
}] });
|
|
@@ -423,17 +1340,75 @@ class KeyboardShortcutService {
|
|
|
423
1340
|
}
|
|
424
1341
|
return false;
|
|
425
1342
|
}
|
|
426
|
-
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.
|
|
427
|
-
static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.
|
|
1343
|
+
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.13", ngImport: i0, type: KeyboardShortcutService, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
|
|
1344
|
+
static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.13", ngImport: i0, type: KeyboardShortcutService, providedIn: 'root' }); }
|
|
428
1345
|
}
|
|
429
|
-
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.
|
|
1346
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.13", ngImport: i0, type: KeyboardShortcutService, decorators: [{
|
|
430
1347
|
type: Injectable,
|
|
431
1348
|
args: [{ providedIn: 'root' }]
|
|
432
1349
|
}], ctorParameters: () => [] });
|
|
433
1350
|
|
|
1351
|
+
const FAQ_USAGE = 'faq\n' +
|
|
1352
|
+
'faq <key>\n' +
|
|
1353
|
+
'faq search <term>\n' +
|
|
1354
|
+
'faq index\n' +
|
|
1355
|
+
'faq help\n\n' +
|
|
1356
|
+
' Opens the in-app FAQ help system. Does not require authentication.\n\n' +
|
|
1357
|
+
' Subcommands:\n' +
|
|
1358
|
+
' (none) Open the FAQ at the current route\'s resolved key.\n' +
|
|
1359
|
+
' <key> Open at a specific dot-notation key (e.g. trade.segments).\n' +
|
|
1360
|
+
' search <term> Open with the search field pre-filled.\n' +
|
|
1361
|
+
' index Open at the index table-of-contents.\n' +
|
|
1362
|
+
' help Show this help.';
|
|
1363
|
+
class FaqCommand {
|
|
1364
|
+
constructor() {
|
|
1365
|
+
this.faqService = inject(FaqService);
|
|
1366
|
+
this.name = 'faq';
|
|
1367
|
+
this.description = 'Open the in-app FAQ help system';
|
|
1368
|
+
this.usage = FAQ_USAGE;
|
|
1369
|
+
this.requiresAuth = false;
|
|
1370
|
+
this.argsSchema = z.array(z.string()).max(50);
|
|
1371
|
+
}
|
|
1372
|
+
execute(args) {
|
|
1373
|
+
const ts = () => new Date();
|
|
1374
|
+
if (args.length === 0) {
|
|
1375
|
+
void this.faqService.openModal();
|
|
1376
|
+
return { success: true, message: '', timestamp: ts() };
|
|
1377
|
+
}
|
|
1378
|
+
const sub = args[0].toLowerCase();
|
|
1379
|
+
if (sub === 'index') {
|
|
1380
|
+
void this.faqService.openModal('index');
|
|
1381
|
+
return { success: true, message: '', timestamp: ts() };
|
|
1382
|
+
}
|
|
1383
|
+
if (sub === 'search') {
|
|
1384
|
+
const term = args.slice(1).join(' ').trim();
|
|
1385
|
+
if (!term) {
|
|
1386
|
+
return {
|
|
1387
|
+
success: false,
|
|
1388
|
+
message: 'Usage: faq search <term>',
|
|
1389
|
+
timestamp: ts(),
|
|
1390
|
+
};
|
|
1391
|
+
}
|
|
1392
|
+
void this.faqService.openModal(undefined, { searchTerm: term });
|
|
1393
|
+
return { success: true, message: '', timestamp: ts() };
|
|
1394
|
+
}
|
|
1395
|
+
void this.faqService.openModal(args[0]);
|
|
1396
|
+
return { success: true, message: '', timestamp: ts() };
|
|
1397
|
+
}
|
|
1398
|
+
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.13", ngImport: i0, type: FaqCommand, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
|
|
1399
|
+
static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.13", ngImport: i0, type: FaqCommand, providedIn: 'root' }); }
|
|
1400
|
+
}
|
|
1401
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.13", ngImport: i0, type: FaqCommand, decorators: [{
|
|
1402
|
+
type: Injectable,
|
|
1403
|
+
args: [{ providedIn: 'root' }]
|
|
1404
|
+
}] });
|
|
1405
|
+
|
|
434
1406
|
class CommandRegistryService {
|
|
435
1407
|
constructor() {
|
|
436
1408
|
this.commands = new Map();
|
|
1409
|
+
// Built-in shell commands registered ahead of app-provided commands so
|
|
1410
|
+
// an app cannot accidentally shadow them.
|
|
1411
|
+
this.register(inject(FaqCommand));
|
|
437
1412
|
const appCommands = inject(SHELL_COMMANDS, { optional: true }) ?? [];
|
|
438
1413
|
for (const cmd of appCommands) {
|
|
439
1414
|
this.register(cmd);
|
|
@@ -456,7 +1431,8 @@ class CommandRegistryService {
|
|
|
456
1431
|
if (!prefix)
|
|
457
1432
|
return [];
|
|
458
1433
|
if (!isAuthenticated) {
|
|
459
|
-
|
|
1434
|
+
const open = ['login', ...[...this.commands.values()].filter(c => c.requiresAuth === false).map(c => c.name)];
|
|
1435
|
+
return open.filter(name => name.startsWith(prefix) && name !== prefix).sort();
|
|
460
1436
|
}
|
|
461
1437
|
const builtIns = ['clear', 'help'];
|
|
462
1438
|
const registered = [...this.commands.values()].map(c => c.name);
|
|
@@ -464,10 +1440,10 @@ class CommandRegistryService {
|
|
|
464
1440
|
.filter(name => name.startsWith(prefix) && name !== prefix)
|
|
465
1441
|
.sort();
|
|
466
1442
|
}
|
|
467
|
-
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.
|
|
468
|
-
static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.
|
|
1443
|
+
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.13", ngImport: i0, type: CommandRegistryService, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
|
|
1444
|
+
static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.13", ngImport: i0, type: CommandRegistryService, providedIn: 'root' }); }
|
|
469
1445
|
}
|
|
470
|
-
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.
|
|
1446
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.13", ngImport: i0, type: CommandRegistryService, decorators: [{
|
|
471
1447
|
type: Injectable,
|
|
472
1448
|
args: [{ providedIn: 'root' }]
|
|
473
1449
|
}], ctorParameters: () => [] });
|
|
@@ -596,16 +1572,22 @@ class CommandService {
|
|
|
596
1572
|
}
|
|
597
1573
|
const hasHelp = args.some(a => a.toLowerCase() === 'help');
|
|
598
1574
|
const isAuthenticated = this.authService.authenticated();
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
1575
|
+
const notLoggedIn = () => ({
|
|
1576
|
+
success: false,
|
|
1577
|
+
message: 'Not logged in. Use "login" to authenticate.',
|
|
1578
|
+
timestamp: new Date(),
|
|
1579
|
+
});
|
|
602
1580
|
if (command === 'clear') {
|
|
1581
|
+
if (!isAuthenticated)
|
|
1582
|
+
return notLoggedIn();
|
|
603
1583
|
if (hasHelp)
|
|
604
1584
|
return { success: true, message: CLEAR_USAGE, timestamp: new Date(), isHelp: true };
|
|
605
1585
|
this.history.set([]);
|
|
606
1586
|
return { success: true, message: '', timestamp: new Date() };
|
|
607
1587
|
}
|
|
608
1588
|
if (command === 'help') {
|
|
1589
|
+
if (!isAuthenticated)
|
|
1590
|
+
return notLoggedIn();
|
|
609
1591
|
const target = args[0]?.toLowerCase();
|
|
610
1592
|
if (target)
|
|
611
1593
|
return this.#commandHelp(target);
|
|
@@ -632,9 +1614,14 @@ class CommandService {
|
|
|
632
1614
|
command = swapped;
|
|
633
1615
|
}
|
|
634
1616
|
else {
|
|
1617
|
+
if (!isAuthenticated && command !== 'login')
|
|
1618
|
+
return notLoggedIn();
|
|
635
1619
|
return { success: false, message: `Unknown command: ${command}`, timestamp: new Date() };
|
|
636
1620
|
}
|
|
637
1621
|
}
|
|
1622
|
+
if (!isAuthenticated && command !== 'login' && def.requiresAuth !== false) {
|
|
1623
|
+
return notLoggedIn();
|
|
1624
|
+
}
|
|
638
1625
|
if (hasHelp) {
|
|
639
1626
|
return { success: true, message: def.usage, timestamp: new Date(), isHelp: true };
|
|
640
1627
|
}
|
|
@@ -654,10 +1641,10 @@ class CommandService {
|
|
|
654
1641
|
return { success: false, message: `Unknown command: ${name}`, timestamp: new Date() };
|
|
655
1642
|
return { success: true, message: def.usage, timestamp: new Date(), isHelp: true };
|
|
656
1643
|
}
|
|
657
|
-
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.
|
|
658
|
-
static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.
|
|
1644
|
+
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.13", ngImport: i0, type: CommandService, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
|
|
1645
|
+
static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.13", ngImport: i0, type: CommandService, providedIn: 'root' }); }
|
|
659
1646
|
}
|
|
660
|
-
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.
|
|
1647
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.13", ngImport: i0, type: CommandService, decorators: [{
|
|
661
1648
|
type: Injectable,
|
|
662
1649
|
args: [{ providedIn: 'root' }]
|
|
663
1650
|
}], ctorParameters: () => [] });
|
|
@@ -682,12 +1669,16 @@ class StatusBar {
|
|
|
682
1669
|
this.#commandService.echoCommand('logout');
|
|
683
1670
|
this.#focusService.requestFocus();
|
|
684
1671
|
}
|
|
685
|
-
|
|
686
|
-
|
|
1672
|
+
openFaq() {
|
|
1673
|
+
this.#commandService.execute('faq');
|
|
1674
|
+
this.#focusService.requestFocus();
|
|
1675
|
+
}
|
|
1676
|
+
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.13", ngImport: i0, type: StatusBar, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
|
|
1677
|
+
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.13", type: StatusBar, isStandalone: true, selector: "shell-status-bar", ngImport: i0, template: "<div class=\"status-bar\">\n <div class=\"status-item\">\n <span class=\"status-label text-secondary-info\">Status:</span>\n @if (isAuthenticated()) {\n <a class=\"status-value text-interactive logout-link\" (click)=\"logout()\">Log Out</a>\n } @else {\n <span class=\"status-value text-loss\">{{ loginStatus() }}</span>\n }\n </div>\n @if (isAuthenticated()) {\n <ng-content />\n }\n <button\n type=\"button\"\n class=\"faq-button text-interactive\"\n (click)=\"openFaq()\"\n title=\"Open FAQ (or type `faq`)\"\n aria-label=\"Open FAQ\"\n >?</button>\n</div>\n", styles: [".status-bar{display:flex;align-items:center;gap:24px;padding:0 16px;height:100%;background-color:var(--color-surface);border-bottom:1px solid var(--color-surface-border);font-size:.8rem}.status-item{display:flex;align-items:baseline;gap:6px}.status-label{font-weight:500;text-transform:uppercase;letter-spacing:.05em;font-size:.7rem}.logout-link{cursor:pointer;text-decoration:underline}.logout-link:hover{opacity:.8}.faq-button{margin-left:auto;background:none;border:1px solid var(--color-surface-border);border-radius:50%;width:22px;height:22px;font-size:.85rem;font-weight:600;line-height:1;cursor:pointer;display:inline-flex;align-items:center;justify-content:center;padding:0}.faq-button:hover{opacity:.8}\n"] }); }
|
|
687
1678
|
}
|
|
688
|
-
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.
|
|
1679
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.13", ngImport: i0, type: StatusBar, decorators: [{
|
|
689
1680
|
type: Component,
|
|
690
|
-
args: [{ selector: 'shell-status-bar', template: "<div class=\"status-bar\">\n <div class=\"status-item\">\n <span class=\"status-label text-secondary-info\">Status:</span>\n @if (isAuthenticated()) {\n <a class=\"status-value text-interactive logout-link\" (click)=\"logout()\">Log Out</a>\n } @else {\n <span class=\"status-value text-loss\">{{ loginStatus() }}</span>\n }\n </div>\n @if (isAuthenticated()) {\n <ng-content />\n }\n</div>\n", styles: [".status-bar{display:flex;align-items:center;gap:24px;padding:0 16px;height:100%;background-color:var(--color-surface);border-bottom:1px solid var(--color-surface-border);font-size:.8rem}.status-item{display:flex;align-items:baseline;gap:6px}.status-label{font-weight:500;text-transform:uppercase;letter-spacing:.05em;font-size:.7rem}.logout-link{cursor:pointer;text-decoration:underline}.logout-link:hover{opacity:.8}\n"] }]
|
|
1681
|
+
args: [{ selector: 'shell-status-bar', template: "<div class=\"status-bar\">\n <div class=\"status-item\">\n <span class=\"status-label text-secondary-info\">Status:</span>\n @if (isAuthenticated()) {\n <a class=\"status-value text-interactive logout-link\" (click)=\"logout()\">Log Out</a>\n } @else {\n <span class=\"status-value text-loss\">{{ loginStatus() }}</span>\n }\n </div>\n @if (isAuthenticated()) {\n <ng-content />\n }\n <button\n type=\"button\"\n class=\"faq-button text-interactive\"\n (click)=\"openFaq()\"\n title=\"Open FAQ (or type `faq`)\"\n aria-label=\"Open FAQ\"\n >?</button>\n</div>\n", styles: [".status-bar{display:flex;align-items:center;gap:24px;padding:0 16px;height:100%;background-color:var(--color-surface);border-bottom:1px solid var(--color-surface-border);font-size:.8rem}.status-item{display:flex;align-items:baseline;gap:6px}.status-label{font-weight:500;text-transform:uppercase;letter-spacing:.05em;font-size:.7rem}.logout-link{cursor:pointer;text-decoration:underline}.logout-link:hover{opacity:.8}.faq-button{margin-left:auto;background:none;border:1px solid var(--color-surface-border);border-radius:50%;width:22px;height:22px;font-size:.85rem;font-weight:600;line-height:1;cursor:pointer;display:inline-flex;align-items:center;justify-content:center;padding:0}.faq-button:hover{opacity:.8}\n"] }]
|
|
691
1682
|
}] });
|
|
692
1683
|
|
|
693
1684
|
class SideMenu {
|
|
@@ -712,10 +1703,10 @@ class SideMenu {
|
|
|
712
1703
|
toggleHelp() {
|
|
713
1704
|
this.#shortcutService.toggleHelp();
|
|
714
1705
|
}
|
|
715
|
-
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.
|
|
716
|
-
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.
|
|
1706
|
+
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.13", ngImport: i0, type: SideMenu, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
|
|
1707
|
+
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.13", type: SideMenu, isStandalone: true, selector: "shell-side-menu", ngImport: i0, template: "<nav class=\"side-menu\">\n @for (item of menuItems(); track item.route) {\n <a\n class=\"menu-item\"\n [routerLink]=\"item.route\"\n routerLinkActive=\"active\"\n [matTooltip]=\"item.label\"\n matTooltipPosition=\"right\"\n (click)=\"onMenuClick(item)\"\n >\n <mat-icon>{{ item.icon }}</mat-icon>\n </a>\n }\n <div class=\"menu-spacer\"></div>\n @if (hasShortcuts()) {\n <button\n class=\"help-button\"\n matTooltip=\"Keyboard shortcuts\"\n matTooltipPosition=\"right\"\n (click)=\"toggleHelp()\"\n >\n <span class=\"help-modifier\">Alt+</span><span class=\"help-key\">?</span>\n </button>\n }\n</nav>\n", styles: [".side-menu{display:flex;flex-direction:column;align-items:center;padding-top:8px;gap:4px;height:100%;background-color:var(--color-surface);border-right:1px solid var(--color-surface-border)}.menu-item{display:flex;align-items:center;justify-content:center;width:40px;height:40px;border-radius:8px;color:var(--color-secondary-info);text-decoration:none;transition:background-color .15s,color .15s}.menu-item:hover{background-color:var(--color-surface-light);color:var(--color-interactive)}.menu-item.active,.menu-item.active:hover{background-color:var(--color-interactive, #00e5ff);color:var(--color-surface-dark, #0d1117)}.menu-spacer{flex:1}.help-button{margin-bottom:8px;padding:4px 6px;border:1px solid var(--color-surface-border);border-radius:6px;background:none;font-size:.7rem;font-family:Roboto Mono,monospace;cursor:pointer;color:var(--color-interactive);transition:background-color .15s}.help-button:hover{background-color:var(--color-surface-light)}.help-modifier{opacity:.6}.help-key{font-weight:700}\n"], dependencies: [{ kind: "directive", type: RouterLink, selector: "[routerLink]", inputs: ["target", "queryParams", "fragment", "queryParamsHandling", "state", "info", "relativeTo", "preserveFragment", "skipLocationChange", "replaceUrl", "routerLink"] }, { kind: "directive", type: RouterLinkActive, selector: "[routerLinkActive]", inputs: ["routerLinkActiveOptions", "ariaCurrentWhenActive", "routerLinkActive"], outputs: ["isActiveChange"], exportAs: ["routerLinkActive"] }, { kind: "ngmodule", type: MatIconModule }, { kind: "component", type: i1.MatIcon, selector: "mat-icon", inputs: ["color", "inline", "svgIcon", "fontSet", "fontIcon"], exportAs: ["matIcon"] }, { kind: "ngmodule", type: MatTooltipModule }, { kind: "directive", type: i2.MatTooltip, selector: "[matTooltip]", inputs: ["matTooltipPosition", "matTooltipPositionAtOrigin", "matTooltipDisabled", "matTooltipShowDelay", "matTooltipHideDelay", "matTooltipTouchGestures", "matTooltip", "matTooltipClass"], exportAs: ["matTooltip"] }] }); }
|
|
717
1708
|
}
|
|
718
|
-
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.
|
|
1709
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.13", ngImport: i0, type: SideMenu, decorators: [{
|
|
719
1710
|
type: Component,
|
|
720
1711
|
args: [{ selector: 'shell-side-menu', imports: [RouterLink, RouterLinkActive, MatIconModule, MatTooltipModule], template: "<nav class=\"side-menu\">\n @for (item of menuItems(); track item.route) {\n <a\n class=\"menu-item\"\n [routerLink]=\"item.route\"\n routerLinkActive=\"active\"\n [matTooltip]=\"item.label\"\n matTooltipPosition=\"right\"\n (click)=\"onMenuClick(item)\"\n >\n <mat-icon>{{ item.icon }}</mat-icon>\n </a>\n }\n <div class=\"menu-spacer\"></div>\n @if (hasShortcuts()) {\n <button\n class=\"help-button\"\n matTooltip=\"Keyboard shortcuts\"\n matTooltipPosition=\"right\"\n (click)=\"toggleHelp()\"\n >\n <span class=\"help-modifier\">Alt+</span><span class=\"help-key\">?</span>\n </button>\n }\n</nav>\n", styles: [".side-menu{display:flex;flex-direction:column;align-items:center;padding-top:8px;gap:4px;height:100%;background-color:var(--color-surface);border-right:1px solid var(--color-surface-border)}.menu-item{display:flex;align-items:center;justify-content:center;width:40px;height:40px;border-radius:8px;color:var(--color-secondary-info);text-decoration:none;transition:background-color .15s,color .15s}.menu-item:hover{background-color:var(--color-surface-light);color:var(--color-interactive)}.menu-item.active,.menu-item.active:hover{background-color:var(--color-interactive, #00e5ff);color:var(--color-surface-dark, #0d1117)}.menu-spacer{flex:1}.help-button{margin-bottom:8px;padding:4px 6px;border:1px solid var(--color-surface-border);border-radius:6px;background:none;font-size:.7rem;font-family:Roboto Mono,monospace;cursor:pointer;color:var(--color-interactive);transition:background-color .15s}.help-button:hover{background-color:var(--color-surface-light)}.help-modifier{opacity:.6}.help-key{font-weight:700}\n"] }]
|
|
721
1712
|
}] });
|
|
@@ -740,10 +1731,10 @@ class MainContent {
|
|
|
740
1731
|
closeShortcutHelp() {
|
|
741
1732
|
this.#shortcutService.helpVisible.set(false);
|
|
742
1733
|
}
|
|
743
|
-
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.
|
|
744
|
-
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.
|
|
1734
|
+
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.13", ngImport: i0, type: MainContent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
|
|
1735
|
+
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.13", type: MainContent, isStandalone: true, selector: "shell-main-content", ngImport: i0, template: "<div class=\"main-content\">\n <div class=\"content-area\">\n <router-outlet />\n </div>\n @if (shortcutHelpVisible() && activeShortcuts()) {\n <div class=\"shortcut-overlay\" (click)=\"closeShortcutHelp()\">\n <div class=\"shortcut-panel\" (click)=\"$event.stopPropagation()\">\n <div class=\"shortcut-title text-secondary-info\">{{ activeShortcuts()!.groupId }} shortcuts</div>\n @for (s of activeShortcuts()!.shortcuts; track s.key) {\n <div class=\"shortcut-row\">\n <kbd class=\"shortcut-key\">Alt+{{ formatKey(s.key) }}</kbd>\n <span class=\"shortcut-desc text-text-primary\">{{ s.description }}</span>\n </div>\n }\n <div class=\"shortcut-row shortcut-dismiss\">\n <kbd class=\"shortcut-key\">Alt+?</kbd>\n <span class=\"shortcut-desc text-secondary-info\">or</span>\n <kbd class=\"shortcut-key\">Esc</kbd>\n <span class=\"shortcut-desc text-secondary-info\">dismiss</span>\n </div>\n </div>\n </div>\n }\n @if (isThinking()) {\n <div class=\"thinking-overlay\">\n <mat-spinner diameter=\"48\" />\n </div>\n }\n</div>\n", styles: [".main-content{position:relative;height:100%;overflow:hidden}.content-area{height:100%;overflow-y:auto;padding:16px}.shortcut-overlay{position:absolute;inset:0;z-index:15;display:flex;align-items:center;justify-content:center;background-color:#0a0e1480}.shortcut-panel{background-color:var(--color-terminal-bg);border:1px solid var(--color-surface-border);border-radius:6px;padding:14px 18px;font-size:.8rem;box-shadow:0 4px 16px #00000080;min-width:220px}.shortcut-title{font-size:.7rem;text-transform:uppercase;letter-spacing:.05em;margin-bottom:8px;font-weight:500}.shortcut-row{display:flex;align-items:center;gap:10px;padding:3px 0}.shortcut-row.shortcut-dismiss{margin-top:6px;border-top:1px solid var(--color-surface-border);padding-top:8px}.shortcut-key{font-family:Roboto Mono,monospace;font-size:.75rem;background-color:#ffffff14;border:1px solid var(--color-surface-border);border-radius:3px;padding:1px 6px;min-width:70px;text-align:center}.shortcut-desc{font-size:.78rem}.thinking-overlay{position:absolute;inset:0;z-index:20;background-color:#0a0e14b3;display:flex;align-items:center;justify-content:center;pointer-events:all;cursor:wait}\n"], dependencies: [{ kind: "directive", type: RouterOutlet, selector: "router-outlet", inputs: ["name", "routerOutletData"], outputs: ["activate", "deactivate", "attach", "detach"], exportAs: ["outlet"] }, { kind: "ngmodule", type: MatProgressSpinnerModule }, { kind: "component", type: i1$1.MatProgressSpinner, selector: "mat-progress-spinner, mat-spinner", inputs: ["color", "mode", "value", "diameter", "strokeWidth"], exportAs: ["matProgressSpinner"] }] }); }
|
|
745
1736
|
}
|
|
746
|
-
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.
|
|
1737
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.13", ngImport: i0, type: MainContent, decorators: [{
|
|
747
1738
|
type: Component,
|
|
748
1739
|
args: [{ selector: 'shell-main-content', imports: [RouterOutlet, MatProgressSpinnerModule], template: "<div class=\"main-content\">\n <div class=\"content-area\">\n <router-outlet />\n </div>\n @if (shortcutHelpVisible() && activeShortcuts()) {\n <div class=\"shortcut-overlay\" (click)=\"closeShortcutHelp()\">\n <div class=\"shortcut-panel\" (click)=\"$event.stopPropagation()\">\n <div class=\"shortcut-title text-secondary-info\">{{ activeShortcuts()!.groupId }} shortcuts</div>\n @for (s of activeShortcuts()!.shortcuts; track s.key) {\n <div class=\"shortcut-row\">\n <kbd class=\"shortcut-key\">Alt+{{ formatKey(s.key) }}</kbd>\n <span class=\"shortcut-desc text-text-primary\">{{ s.description }}</span>\n </div>\n }\n <div class=\"shortcut-row shortcut-dismiss\">\n <kbd class=\"shortcut-key\">Alt+?</kbd>\n <span class=\"shortcut-desc text-secondary-info\">or</span>\n <kbd class=\"shortcut-key\">Esc</kbd>\n <span class=\"shortcut-desc text-secondary-info\">dismiss</span>\n </div>\n </div>\n </div>\n }\n @if (isThinking()) {\n <div class=\"thinking-overlay\">\n <mat-spinner diameter=\"48\" />\n </div>\n }\n</div>\n", styles: [".main-content{position:relative;height:100%;overflow:hidden}.content-area{height:100%;overflow-y:auto;padding:16px}.shortcut-overlay{position:absolute;inset:0;z-index:15;display:flex;align-items:center;justify-content:center;background-color:#0a0e1480}.shortcut-panel{background-color:var(--color-terminal-bg);border:1px solid var(--color-surface-border);border-radius:6px;padding:14px 18px;font-size:.8rem;box-shadow:0 4px 16px #00000080;min-width:220px}.shortcut-title{font-size:.7rem;text-transform:uppercase;letter-spacing:.05em;margin-bottom:8px;font-weight:500}.shortcut-row{display:flex;align-items:center;gap:10px;padding:3px 0}.shortcut-row.shortcut-dismiss{margin-top:6px;border-top:1px solid var(--color-surface-border);padding-top:8px}.shortcut-key{font-family:Roboto Mono,monospace;font-size:.75rem;background-color:#ffffff14;border:1px solid var(--color-surface-border);border-radius:3px;padding:1px 6px;min-width:70px;text-align:center}.shortcut-desc{font-size:.78rem}.thinking-overlay{position:absolute;inset:0;z-index:20;background-color:#0a0e14b3;display:flex;align-items:center;justify-content:center;pointer-events:all;cursor:wait}\n"] }]
|
|
749
1740
|
}] });
|
|
@@ -838,10 +1829,10 @@ class CommandTerminal {
|
|
|
838
1829
|
focusInput() {
|
|
839
1830
|
this.inputEl()?.nativeElement.focus();
|
|
840
1831
|
}
|
|
841
|
-
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.
|
|
842
|
-
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.
|
|
1832
|
+
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.13", ngImport: i0, type: CommandTerminal, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
|
|
1833
|
+
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.13", type: CommandTerminal, isStandalone: true, selector: "shell-command-terminal", viewQueries: [{ propertyName: "inputEl", first: true, predicate: ["terminalInput"], descendants: true, isSignal: true }], ngImport: i0, template: "<div class=\"terminal\">\n <div class=\"terminal-input-row\">\n <span class=\"prompt\">$</span>\n <div class=\"input-wrapper\">\n @if (ghostText()) {\n <input\n class=\"ghost-input\"\n type=\"text\"\n [value]=\"ghostText()\"\n disabled\n tabindex=\"-1\"\n aria-hidden=\"true\"\n />\n }\n <input\n #terminalInput\n class=\"terminal-input\"\n type=\"text\"\n [ngModel]=\"currentInput()\"\n (ngModelChange)=\"onInputChange($event)\"\n (keydown.enter)=\"onSubmit()\"\n (keydown.tab)=\"onTab($any($event))\"\n (keydown.escape)=\"dismissHelp()\"\n placeholder=\"Type a command...\"\n [disabled]=\"isProcessing()\"\n autocomplete=\"off\"\n spellcheck=\"false\"\n />\n </div>\n </div>\n <div class=\"terminal-output\">\n @for (entry of history(); track entry.result.timestamp) {\n @if (entry.input) {\n <div class=\"terminal-line\">\n <span class=\"prompt\">$</span>\n <span class=\"command-text\">{{ entry.input }}@if ((entry.repeatCount ?? 1) > 1) { <span class=\"repeat-count\">(\u00D7{{ entry.repeatCount }})</span>}</span>\n </div>\n }\n @if (entry.result.message) {\n <div class=\"terminal-line\" [class]=\"getSeverityClass(entry.result)\">{{ entry.result.message }}</div>\n }\n }\n </div>\n</div>\n", styles: [".terminal{display:flex;flex-direction:column;height:100%;background-color:var(--color-terminal-bg);border-top:1px solid var(--color-surface-border);font-family:Roboto Mono,Courier New,monospace;font-size:.85rem}.terminal-output{flex:1;overflow-y:auto;padding:8px 12px;display:flex;flex-direction:column}.terminal-line{line-height:1.6;color:var(--color-text-primary);white-space:pre-wrap}.terminal-line.error{color:var(--color-loss)}.terminal-line.warning{color:var(--color-warning)}.terminal-line.info{color:var(--color-info)}.prompt{color:var(--color-terminal-prompt);margin-right:8px;font-weight:700}.command-text{color:var(--color-terminal-text)}.repeat-count{color:var(--color-secondary-info);opacity:.6;margin-left:4px}.terminal-input-row{display:flex;align-items:center;padding:8px 12px;border-bottom:1px solid var(--color-surface-border)}.input-wrapper{flex:1;display:grid}.ghost-input,.terminal-input{grid-area:1 / 1;font-family:inherit;font-size:inherit;line-height:normal;margin:0;padding:0;border:none;background:transparent;outline:none;-webkit-appearance:none;appearance:none;width:100%}.ghost-input{color:var(--color-secondary-info);opacity:.3;pointer-events:none}.terminal-input{color:var(--color-terminal-text);caret-color:var(--color-terminal-prompt)}.terminal-input::placeholder{color:var(--color-secondary-info);opacity:.5}\n"], dependencies: [{ kind: "ngmodule", type: FormsModule }, { kind: "directive", type: i3.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: i3.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i3.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }] }); }
|
|
843
1834
|
}
|
|
844
|
-
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.
|
|
1835
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.13", ngImport: i0, type: CommandTerminal, decorators: [{
|
|
845
1836
|
type: Component,
|
|
846
1837
|
args: [{ selector: 'shell-command-terminal', imports: [FormsModule], template: "<div class=\"terminal\">\n <div class=\"terminal-input-row\">\n <span class=\"prompt\">$</span>\n <div class=\"input-wrapper\">\n @if (ghostText()) {\n <input\n class=\"ghost-input\"\n type=\"text\"\n [value]=\"ghostText()\"\n disabled\n tabindex=\"-1\"\n aria-hidden=\"true\"\n />\n }\n <input\n #terminalInput\n class=\"terminal-input\"\n type=\"text\"\n [ngModel]=\"currentInput()\"\n (ngModelChange)=\"onInputChange($event)\"\n (keydown.enter)=\"onSubmit()\"\n (keydown.tab)=\"onTab($any($event))\"\n (keydown.escape)=\"dismissHelp()\"\n placeholder=\"Type a command...\"\n [disabled]=\"isProcessing()\"\n autocomplete=\"off\"\n spellcheck=\"false\"\n />\n </div>\n </div>\n <div class=\"terminal-output\">\n @for (entry of history(); track entry.result.timestamp) {\n @if (entry.input) {\n <div class=\"terminal-line\">\n <span class=\"prompt\">$</span>\n <span class=\"command-text\">{{ entry.input }}@if ((entry.repeatCount ?? 1) > 1) { <span class=\"repeat-count\">(\u00D7{{ entry.repeatCount }})</span>}</span>\n </div>\n }\n @if (entry.result.message) {\n <div class=\"terminal-line\" [class]=\"getSeverityClass(entry.result)\">{{ entry.result.message }}</div>\n }\n }\n </div>\n</div>\n", styles: [".terminal{display:flex;flex-direction:column;height:100%;background-color:var(--color-terminal-bg);border-top:1px solid var(--color-surface-border);font-family:Roboto Mono,Courier New,monospace;font-size:.85rem}.terminal-output{flex:1;overflow-y:auto;padding:8px 12px;display:flex;flex-direction:column}.terminal-line{line-height:1.6;color:var(--color-text-primary);white-space:pre-wrap}.terminal-line.error{color:var(--color-loss)}.terminal-line.warning{color:var(--color-warning)}.terminal-line.info{color:var(--color-info)}.prompt{color:var(--color-terminal-prompt);margin-right:8px;font-weight:700}.command-text{color:var(--color-terminal-text)}.repeat-count{color:var(--color-secondary-info);opacity:.6;margin-left:4px}.terminal-input-row{display:flex;align-items:center;padding:8px 12px;border-bottom:1px solid var(--color-surface-border)}.input-wrapper{flex:1;display:grid}.ghost-input,.terminal-input{grid-area:1 / 1;font-family:inherit;font-size:inherit;line-height:normal;margin:0;padding:0;border:none;background:transparent;outline:none;-webkit-appearance:none;appearance:none;width:100%}.ghost-input{color:var(--color-secondary-info);opacity:.3;pointer-events:none}.terminal-input{color:var(--color-terminal-text);caret-color:var(--color-terminal-prompt)}.terminal-input::placeholder{color:var(--color-secondary-info);opacity:.5}\n"] }]
|
|
847
1838
|
}], ctorParameters: () => [], propDecorators: { inputEl: [{ type: i0.ViewChild, args: ['terminalInput', { isSignal: true }] }] } });
|
|
@@ -891,18 +1882,19 @@ class Shell {
|
|
|
891
1882
|
document.addEventListener('mousemove', this.#onMouseMove);
|
|
892
1883
|
document.addEventListener('mouseup', this.#onMouseUp);
|
|
893
1884
|
}
|
|
894
|
-
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.
|
|
895
|
-
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.
|
|
1885
|
+
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.13", ngImport: i0, type: Shell, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
|
|
1886
|
+
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.13", type: Shell, isStandalone: true, selector: "shell-root", ngImport: i0, template: "<div class=\"shell\" [style.--terminal-height]=\"terminalHeight()\" [class.is-dragging]=\"isDragging()\">\n <shell-status-bar class=\"shell-status\" [class.disabled-zone]=\"isThinking()\">\n <ng-content />\n </shell-status-bar>\n @if (isAuthenticated()) {\n <shell-side-menu class=\"shell-menu\" [class.disabled-zone]=\"isThinking()\" />\n }\n <shell-main-content class=\"shell-main\" [class.full-width]=\"!isAuthenticated()\" />\n <div class=\"shell-resize\" [class.dragging]=\"isDragging()\" (mousedown)=\"onResizeStart($event)\"></div>\n <shell-command-terminal class=\"shell-terminal\" [class.disabled-zone]=\"isThinking()\" />\n <faq-modal />\n\n @if (helpOverlay(); as helpText) {\n <div class=\"help-backdrop\" (click)=\"dismissHelp()\">\n <div class=\"help-panel\" (click)=\"$event.stopPropagation()\" role=\"dialog\" aria-label=\"Command help\">\n <div class=\"help-panel-header\">\n <span class=\"help-panel-title\">Help</span>\n <button class=\"help-panel-close\" type=\"button\" (click)=\"dismissHelp()\" aria-label=\"Close help\">\u2715</button>\n </div>\n <div class=\"help-panel-content\">{{ helpText }}</div>\n <div class=\"help-panel-footer\">\n <span>Tip: for most commands you can swap the first and second words \u2014 e.g. \"disable user\" works the same as \"user disable\"</span>\n <span class=\"help-panel-footer-dismiss\">Press Escape, click outside, or start typing to close</span>\n </div>\n </div>\n </div>\n }\n</div>\n", styles: [".shell{position:relative;display:grid;grid-template-areas:\"status status\" \"menu main\" \"resize resize\" \"terminal terminal\";grid-template-columns:56px 1fr;grid-template-rows:48px 1fr 4px var(--terminal-height, 160px);height:100vh;width:100vw;overflow:hidden;background-color:var(--color-surface-dark)}.shell-status{grid-area:status}.shell-menu{grid-area:menu}.shell-main{grid-area:main;overflow:hidden}.shell-main.full-width{grid-column:1 / -1}.shell-resize{grid-area:resize;cursor:row-resize;background-color:var(--color-surface-border);transition:background-color .15s ease}.shell-resize:hover,.shell-resize.dragging{background-color:var(--color-interactive)}.shell-terminal{grid-area:terminal}.shell.is-dragging{user-select:none;-webkit-user-select:none}.disabled-zone{pointer-events:none;opacity:.4;transition:opacity .2s ease}.help-backdrop{position:absolute;inset:0;z-index:100;background-color:#000000a6;display:flex;align-items:center;justify-content:center}.help-panel{background-color:var(--color-surface, #161b22);border:2px solid var(--color-interactive, #00e5ff);border-radius:4px;width:70%;max-width:820px;max-height:75vh;display:flex;flex-direction:column;font-family:Roboto Mono,Courier New,monospace;font-size:.85rem}.help-panel-header{display:flex;justify-content:space-between;align-items:center;padding:8px 16px;background-color:var(--color-surface-light, #21262d);border-bottom:1px solid var(--color-interactive, #00e5ff);border-radius:2px 2px 0 0;flex-shrink:0}.help-panel-title{color:var(--color-interactive, #00e5ff);font-weight:700;letter-spacing:.05em;text-transform:uppercase;font-size:.75rem}.help-panel-close{background:none;border:1px solid var(--color-surface-border, #30363d);color:var(--color-secondary-info, #9e9e9e);cursor:pointer;padding:2px 8px;font-size:.9rem;line-height:1.4;border-radius:3px;transition:color .15s ease,border-color .15s ease}.help-panel-close:hover{color:var(--color-text-primary, #ffffff);border-color:var(--color-interactive, #00e5ff)}.help-panel-content{flex:1;overflow-y:auto;padding:16px 20px;color:var(--color-text-primary, #ffffff);white-space:pre-wrap;line-height:1.7}.help-panel-footer{padding:6px 16px;background-color:var(--color-surface-light, #21262d);border-top:1px solid var(--color-surface-border, #30363d);border-radius:0 0 2px 2px;color:var(--color-secondary-info, #9e9e9e);font-size:.75rem;flex-shrink:0;display:flex;flex-direction:column;gap:2px}.help-panel-footer-dismiss{opacity:.6}\n"], dependencies: [{ kind: "component", type: StatusBar, selector: "shell-status-bar" }, { kind: "component", type: SideMenu, selector: "shell-side-menu" }, { kind: "component", type: MainContent, selector: "shell-main-content" }, { kind: "component", type: CommandTerminal, selector: "shell-command-terminal" }, { kind: "component", type: FaqModal, selector: "faq-modal" }] }); }
|
|
896
1887
|
}
|
|
897
|
-
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.
|
|
1888
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.13", ngImport: i0, type: Shell, decorators: [{
|
|
898
1889
|
type: Component,
|
|
899
|
-
args: [{ selector: 'shell-root', imports: [StatusBar, SideMenu, MainContent, CommandTerminal], template: "<div class=\"shell\" [style.--terminal-height]=\"terminalHeight()\" [class.is-dragging]=\"isDragging()\">\n <shell-status-bar class=\"shell-status\" [class.disabled-zone]=\"isThinking()\">\n <ng-content />\n </shell-status-bar>\n @if (isAuthenticated()) {\n <shell-side-menu class=\"shell-menu\" [class.disabled-zone]=\"isThinking()\" />\n }\n <shell-main-content class=\"shell-main\" [class.full-width]=\"!isAuthenticated()\" />\n <div class=\"shell-resize\" [class.dragging]=\"isDragging()\" (mousedown)=\"onResizeStart($event)\"></div>\n <shell-command-terminal class=\"shell-terminal\" [class.disabled-zone]=\"isThinking()\" />\n @if (helpOverlay(); as helpText) {\n <div class=\"help-backdrop\" (click)=\"dismissHelp()\">\n <div class=\"help-panel\" (click)=\"$event.stopPropagation()\" role=\"dialog\" aria-label=\"Command help\">\n <div class=\"help-panel-header\">\n <span class=\"help-panel-title\">Help</span>\n <button class=\"help-panel-close\" type=\"button\" (click)=\"dismissHelp()\" aria-label=\"Close help\">\u2715</button>\n </div>\n <div class=\"help-panel-content\">{{ helpText }}</div>\n <div class=\"help-panel-footer\">\n <span>Tip: for most commands you can swap the first and second words \u2014 e.g. \"disable user\" works the same as \"user disable\"</span>\n <span class=\"help-panel-footer-dismiss\">Press Escape, click outside, or start typing to close</span>\n </div>\n </div>\n </div>\n }\n</div>\n", styles: [".shell{position:relative;display:grid;grid-template-areas:\"status status\" \"menu main\" \"resize resize\" \"terminal terminal\";grid-template-columns:56px 1fr;grid-template-rows:48px 1fr 4px var(--terminal-height, 160px);height:100vh;width:100vw;overflow:hidden;background-color:var(--color-surface-dark)}.shell-status{grid-area:status}.shell-menu{grid-area:menu}.shell-main{grid-area:main;overflow:hidden}.shell-main.full-width{grid-column:1 / -1}.shell-resize{grid-area:resize;cursor:row-resize;background-color:var(--color-surface-border);transition:background-color .15s ease}.shell-resize:hover,.shell-resize.dragging{background-color:var(--color-interactive)}.shell-terminal{grid-area:terminal}.shell.is-dragging{user-select:none;-webkit-user-select:none}.disabled-zone{pointer-events:none;opacity:.4;transition:opacity .2s ease}.help-backdrop{position:absolute;inset:0;z-index:100;background-color:#000000a6;display:flex;align-items:center;justify-content:center}.help-panel{background-color:var(--color-surface, #161b22);border:2px solid var(--color-interactive, #00e5ff);border-radius:4px;width:70%;max-width:820px;max-height:75vh;display:flex;flex-direction:column;font-family:Roboto Mono,Courier New,monospace;font-size:.85rem}.help-panel-header{display:flex;justify-content:space-between;align-items:center;padding:8px 16px;background-color:var(--color-surface-light, #21262d);border-bottom:1px solid var(--color-interactive, #00e5ff);border-radius:2px 2px 0 0;flex-shrink:0}.help-panel-title{color:var(--color-interactive, #00e5ff);font-weight:700;letter-spacing:.05em;text-transform:uppercase;font-size:.75rem}.help-panel-close{background:none;border:1px solid var(--color-surface-border, #30363d);color:var(--color-secondary-info, #9e9e9e);cursor:pointer;padding:2px 8px;font-size:.9rem;line-height:1.4;border-radius:3px;transition:color .15s ease,border-color .15s ease}.help-panel-close:hover{color:var(--color-text-primary, #ffffff);border-color:var(--color-interactive, #00e5ff)}.help-panel-content{flex:1;overflow-y:auto;padding:16px 20px;color:var(--color-text-primary, #ffffff);white-space:pre-wrap;line-height:1.7}.help-panel-footer{padding:6px 16px;background-color:var(--color-surface-light, #21262d);border-top:1px solid var(--color-surface-border, #30363d);border-radius:0 0 2px 2px;color:var(--color-secondary-info, #9e9e9e);font-size:.75rem;flex-shrink:0;display:flex;flex-direction:column;gap:2px}.help-panel-footer-dismiss{opacity:.6}\n"] }]
|
|
1890
|
+
args: [{ selector: 'shell-root', imports: [StatusBar, SideMenu, MainContent, CommandTerminal, FaqModal], template: "<div class=\"shell\" [style.--terminal-height]=\"terminalHeight()\" [class.is-dragging]=\"isDragging()\">\n <shell-status-bar class=\"shell-status\" [class.disabled-zone]=\"isThinking()\">\n <ng-content />\n </shell-status-bar>\n @if (isAuthenticated()) {\n <shell-side-menu class=\"shell-menu\" [class.disabled-zone]=\"isThinking()\" />\n }\n <shell-main-content class=\"shell-main\" [class.full-width]=\"!isAuthenticated()\" />\n <div class=\"shell-resize\" [class.dragging]=\"isDragging()\" (mousedown)=\"onResizeStart($event)\"></div>\n <shell-command-terminal class=\"shell-terminal\" [class.disabled-zone]=\"isThinking()\" />\n <faq-modal />\n\n @if (helpOverlay(); as helpText) {\n <div class=\"help-backdrop\" (click)=\"dismissHelp()\">\n <div class=\"help-panel\" (click)=\"$event.stopPropagation()\" role=\"dialog\" aria-label=\"Command help\">\n <div class=\"help-panel-header\">\n <span class=\"help-panel-title\">Help</span>\n <button class=\"help-panel-close\" type=\"button\" (click)=\"dismissHelp()\" aria-label=\"Close help\">\u2715</button>\n </div>\n <div class=\"help-panel-content\">{{ helpText }}</div>\n <div class=\"help-panel-footer\">\n <span>Tip: for most commands you can swap the first and second words \u2014 e.g. \"disable user\" works the same as \"user disable\"</span>\n <span class=\"help-panel-footer-dismiss\">Press Escape, click outside, or start typing to close</span>\n </div>\n </div>\n </div>\n }\n</div>\n", styles: [".shell{position:relative;display:grid;grid-template-areas:\"status status\" \"menu main\" \"resize resize\" \"terminal terminal\";grid-template-columns:56px 1fr;grid-template-rows:48px 1fr 4px var(--terminal-height, 160px);height:100vh;width:100vw;overflow:hidden;background-color:var(--color-surface-dark)}.shell-status{grid-area:status}.shell-menu{grid-area:menu}.shell-main{grid-area:main;overflow:hidden}.shell-main.full-width{grid-column:1 / -1}.shell-resize{grid-area:resize;cursor:row-resize;background-color:var(--color-surface-border);transition:background-color .15s ease}.shell-resize:hover,.shell-resize.dragging{background-color:var(--color-interactive)}.shell-terminal{grid-area:terminal}.shell.is-dragging{user-select:none;-webkit-user-select:none}.disabled-zone{pointer-events:none;opacity:.4;transition:opacity .2s ease}.help-backdrop{position:absolute;inset:0;z-index:100;background-color:#000000a6;display:flex;align-items:center;justify-content:center}.help-panel{background-color:var(--color-surface, #161b22);border:2px solid var(--color-interactive, #00e5ff);border-radius:4px;width:70%;max-width:820px;max-height:75vh;display:flex;flex-direction:column;font-family:Roboto Mono,Courier New,monospace;font-size:.85rem}.help-panel-header{display:flex;justify-content:space-between;align-items:center;padding:8px 16px;background-color:var(--color-surface-light, #21262d);border-bottom:1px solid var(--color-interactive, #00e5ff);border-radius:2px 2px 0 0;flex-shrink:0}.help-panel-title{color:var(--color-interactive, #00e5ff);font-weight:700;letter-spacing:.05em;text-transform:uppercase;font-size:.75rem}.help-panel-close{background:none;border:1px solid var(--color-surface-border, #30363d);color:var(--color-secondary-info, #9e9e9e);cursor:pointer;padding:2px 8px;font-size:.9rem;line-height:1.4;border-radius:3px;transition:color .15s ease,border-color .15s ease}.help-panel-close:hover{color:var(--color-text-primary, #ffffff);border-color:var(--color-interactive, #00e5ff)}.help-panel-content{flex:1;overflow-y:auto;padding:16px 20px;color:var(--color-text-primary, #ffffff);white-space:pre-wrap;line-height:1.7}.help-panel-footer{padding:6px 16px;background-color:var(--color-surface-light, #21262d);border-top:1px solid var(--color-surface-border, #30363d);border-radius:0 0 2px 2px;color:var(--color-secondary-info, #9e9e9e);font-size:.75rem;flex-shrink:0;display:flex;flex-direction:column;gap:2px}.help-panel-footer-dismiss{opacity:.6}\n"] }]
|
|
900
1891
|
}] });
|
|
901
1892
|
|
|
902
1893
|
class Login {
|
|
903
1894
|
#postLoginRoute;
|
|
904
1895
|
#commandService;
|
|
905
1896
|
#terminalFocusService;
|
|
1897
|
+
#activatedRoute;
|
|
906
1898
|
constructor(authenticationService, router) {
|
|
907
1899
|
this.authenticationService = authenticationService;
|
|
908
1900
|
this.router = router;
|
|
@@ -917,6 +1909,24 @@ class Login {
|
|
|
917
1909
|
this.#postLoginRoute = inject(SHELL_POST_LOGIN_ROUTE, { optional: true }) ?? '/';
|
|
918
1910
|
this.#commandService = inject(CommandService);
|
|
919
1911
|
this.#terminalFocusService = inject(TerminalFocusService);
|
|
1912
|
+
this.#activatedRoute = inject(ActivatedRoute);
|
|
1913
|
+
}
|
|
1914
|
+
/**
|
|
1915
|
+
* Returns the post-login destination. Honors a `returnUrl` query param if
|
|
1916
|
+
* present and safe (same-origin, leading `/`, not protocol-relative);
|
|
1917
|
+
* otherwise falls back to `SHELL_POST_LOGIN_ROUTE`.
|
|
1918
|
+
*/
|
|
1919
|
+
#resolvePostLoginUrl() {
|
|
1920
|
+
const candidate = this.#activatedRoute.snapshot.queryParamMap.get('returnUrl');
|
|
1921
|
+
if (!candidate)
|
|
1922
|
+
return this.#postLoginRoute;
|
|
1923
|
+
if (!candidate.startsWith('/'))
|
|
1924
|
+
return this.#postLoginRoute;
|
|
1925
|
+
if (candidate.startsWith('//'))
|
|
1926
|
+
return this.#postLoginRoute;
|
|
1927
|
+
if (/^https?:\/\//i.test(candidate))
|
|
1928
|
+
return this.#postLoginRoute;
|
|
1929
|
+
return candidate;
|
|
920
1930
|
}
|
|
921
1931
|
onAutofill(event, inputEl) {
|
|
922
1932
|
if (event.isAutofilled) {
|
|
@@ -961,7 +1971,7 @@ class Login {
|
|
|
961
1971
|
.login(this.model, force)
|
|
962
1972
|
.then(() => {
|
|
963
1973
|
this.#commandService.echoCommand(`login ${this.model.username}`);
|
|
964
|
-
this.router.
|
|
1974
|
+
this.router.navigateByUrl(this.#resolvePostLoginUrl()).then(() => {
|
|
965
1975
|
setTimeout(() => this.#terminalFocusService.requestFocus());
|
|
966
1976
|
}).catch(console.error);
|
|
967
1977
|
})
|
|
@@ -972,10 +1982,10 @@ class Login {
|
|
|
972
1982
|
this.submitted = false;
|
|
973
1983
|
});
|
|
974
1984
|
}
|
|
975
|
-
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.
|
|
976
|
-
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.
|
|
1985
|
+
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.13", ngImport: i0, type: Login, deps: [{ token: AuthenticationService }, { token: i2$1.Router }], target: i0.ɵɵFactoryTarget.Component }); }
|
|
1986
|
+
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.13", type: Login, isStandalone: true, selector: "shell-login", ngImport: i0, template: "@let usernameRegex = USERNAME_REGEX;\n@let passwordRegex = PASSWORD_REGEX;\n<div class=\"login-container\">\n <div class=\"login-header\">\n <h2 class=\"text-text-primary\">{{ appName }}</h2>\n <p class=\"text-secondary-info\">Sign in to continue</p>\n </div>\n <form #loginForm=\"ngForm\" (ngSubmit)=\"onSubmit($event)\" class=\"login-form\">\n <mat-form-field appearance=\"outline\" class=\"login-field\">\n <mat-label>Username</mat-label>\n <input\n #usernameInput\n matInput\n placeholder=\"Enter username\"\n [(ngModel)]=\"model.username\"\n name=\"username\"\n [disabled]=\"submitted\"\n required\n regexValidator\n [regex]=\"usernameRegex\"\n (cdkAutofill)=\"onAutofill($event, usernameInput)\"\n />\n </mat-form-field>\n <mat-form-field appearance=\"outline\" class=\"login-field\">\n <mat-label>Password</mat-label>\n <input\n #passwordInput\n matInput\n placeholder=\"Enter password\"\n [type]=\"hide() ? 'password' : 'text'\"\n [(ngModel)]=\"model.password\"\n name=\"password\"\n [disabled]=\"submitted\"\n regexValidator\n [regex]=\"passwordRegex\"\n (cdkAutofill)=\"onAutofill($event, passwordInput)\"\n />\n <button\n mat-icon-button\n matSuffix\n type=\"button\"\n (click)=\"clickEvent($event)\"\n [attr.aria-label]=\"'Hide password'\"\n [attr.aria-pressed]=\"hide()\"\n >\n <mat-icon>{{ hide() ? 'visibility_off' : 'visibility' }}</mat-icon>\n </button>\n </mat-form-field>\n <mat-checkbox\n class=\"login-takeover\"\n name=\"takeover\"\n [checked]=\"takeover()\"\n (change)=\"takeover.set($event.checked)\"\n [disabled]=\"submitted\"\n >\n Takeover session if logged in elsewhere\n </mat-checkbox>\n <button mat-flat-button type=\"submit\" [disabled]=\"submitted\" class=\"login-button\">\n Login\n </button>\n </form>\n @if (errors.length > 0) {\n <div class=\"login-errors\">\n @for (error of errors; track error.ndx) {\n <div class=\"login-error text-loss\">{{ error.message }}</div>\n }\n </div>\n }\n @if (authenticationService.conflictSessions(); as sessions) {\n <div class=\"login-conflict\">\n @for (session of sessions; track $index) {\n <p class=\"text-secondary-info\">\n Already logged in from <strong>{{ session.appContext }}</strong>\n since {{ session.startEpoch | date:'short' }}.\n </p>\n }\n <p class=\"text-secondary-info\">Log in here and end that session?</p>\n <div class=\"login-conflict-actions\">\n <button mat-stroked-button type=\"button\" (click)=\"onCancelConflict()\">Cancel</button>\n <button mat-flat-button type=\"button\" (click)=\"onTakeOver()\">Take Over</button>\n </div>\n </div>\n }\n</div>\n", styles: [".login-container{display:flex;flex-direction:column;align-items:center;justify-content:center;height:100%;text-align:center}.login-header{margin-bottom:32px}.login-header h2{font-size:1.5rem;font-weight:300;margin-bottom:8px}.login-form{display:flex;flex-direction:column;align-items:center;gap:8px;width:320px}.login-field{width:100%;text-align:left}.login-takeover{align-self:flex-start;margin-top:4px}.login-button{margin-top:8px;width:100%}.login-errors{margin-top:16px;max-width:320px}.login-conflict{margin-top:16px;max-width:320px;padding:12px 16px;border-radius:4px;background-color:var(--color-surface-light);font-size:.85rem;text-align:left}.login-conflict p{margin:0 0 8px}.login-conflict-actions{display:flex;gap:8px;margin-top:12px;justify-content:flex-end}.login-error{padding:8px 12px;margin-bottom:4px;font-size:.85rem;border-radius:4px;background-color:var(--color-surface-light)}\n"], dependencies: [{ kind: "ngmodule", type: FormsModule }, { kind: "directive", type: i3.ɵNgNoValidate, selector: "form:not([ngNoForm]):not([ngNativeValidate])" }, { kind: "directive", type: i3.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: i3.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i3.NgControlStatusGroup, selector: "[formGroupName],[formArrayName],[ngModelGroup],[formGroup],[formArray],form:not([ngNoForm]),[ngForm]" }, { kind: "directive", type: i3.RequiredValidator, selector: ":not([type=checkbox])[required][formControlName],:not([type=checkbox])[required][formControl],:not([type=checkbox])[required][ngModel]", inputs: ["required"] }, { kind: "directive", type: i3.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }, { kind: "directive", type: i3.NgForm, selector: "form:not([ngNoForm]):not([formGroup]):not([formArray]),ng-form,[ngForm]", inputs: ["ngFormOptions"], outputs: ["ngSubmit"], exportAs: ["ngForm"] }, { kind: "component", type: MatFormField, selector: "mat-form-field", inputs: ["hideRequiredMarker", "color", "floatLabel", "appearance", "subscriptSizing", "hintLabel"], exportAs: ["matFormField"] }, { kind: "directive", type: MatLabel, selector: "mat-label" }, { kind: "directive", type: MatInput, selector: "input[matInput], textarea[matInput], select[matNativeControl], input[matNativeControl], textarea[matNativeControl]", inputs: ["disabled", "id", "placeholder", "name", "required", "type", "errorStateMatcher", "aria-describedby", "value", "readonly", "disabledInteractive"], exportAs: ["matInput"] }, { kind: "ngmodule", type: MatInputModule }, { kind: "directive", type: i4.MatSuffix, selector: "[matSuffix], [matIconSuffix], [matTextSuffix]", inputs: ["matTextSuffix"] }, { kind: "directive", type: i5.CdkAutofill, selector: "[cdkAutofill]", outputs: ["cdkAutofill"] }, { kind: "ngmodule", type: MatButtonModule }, { kind: "component", type: i6.MatButton, selector: " button[matButton], a[matButton], button[mat-button], button[mat-raised-button], button[mat-flat-button], button[mat-stroked-button], a[mat-button], a[mat-raised-button], a[mat-flat-button], a[mat-stroked-button] ", inputs: ["matButton"], exportAs: ["matButton", "matAnchor"] }, { kind: "component", type: i6.MatIconButton, selector: "button[mat-icon-button], a[mat-icon-button], button[matIconButton], a[matIconButton]", exportAs: ["matButton", "matAnchor"] }, { kind: "ngmodule", type: MatCheckboxModule }, { kind: "component", type: i7.MatCheckbox, selector: "mat-checkbox", inputs: ["aria-label", "aria-labelledby", "aria-describedby", "aria-expanded", "aria-controls", "aria-owns", "id", "required", "labelPosition", "name", "value", "disableRipple", "tabIndex", "color", "disabledInteractive", "checked", "disabled", "indeterminate"], outputs: ["change", "indeterminateChange"], exportAs: ["matCheckbox"] }, { kind: "ngmodule", type: MatIconModule }, { kind: "component", type: i1.MatIcon, selector: "mat-icon", inputs: ["color", "inline", "svgIcon", "fontSet", "fontIcon"], exportAs: ["matIcon"] }, { kind: "directive", type: RegexValidatorDirective, selector: "[regexValidator]", inputs: ["regex"] }, { kind: "ngmodule", type: TextFieldModule }, { kind: "pipe", type: DatePipe, name: "date" }] }); }
|
|
977
1987
|
}
|
|
978
|
-
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.
|
|
1988
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.13", ngImport: i0, type: Login, decorators: [{
|
|
979
1989
|
type: Component,
|
|
980
1990
|
args: [{ selector: 'shell-login', imports: [
|
|
981
1991
|
FormsModule,
|
|
@@ -997,8 +2007,8 @@ class ConfirmDialog {
|
|
|
997
2007
|
this.data = inject(MAT_DIALOG_DATA);
|
|
998
2008
|
this.dialogRef = inject((MatDialogRef));
|
|
999
2009
|
}
|
|
1000
|
-
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.
|
|
1001
|
-
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.
|
|
2010
|
+
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.13", ngImport: i0, type: ConfirmDialog, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
|
|
2011
|
+
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.13", type: ConfirmDialog, isStandalone: true, selector: "shell-confirm-dialog", ngImport: i0, template: `
|
|
1002
2012
|
<h2 mat-dialog-title>{{ data.title }}</h2>
|
|
1003
2013
|
<mat-dialog-content>
|
|
1004
2014
|
<p>{{ data.message }}</p>
|
|
@@ -1012,7 +2022,7 @@ class ConfirmDialog {
|
|
|
1012
2022
|
</mat-dialog-actions>
|
|
1013
2023
|
`, isInline: true, styles: [":host{display:block}.warning-message{color:var(--color-warning);margin-top:8px}\n"], dependencies: [{ kind: "ngmodule", type: MatDialogModule }, { kind: "directive", type: i1$2.MatDialogTitle, selector: "[mat-dialog-title], [matDialogTitle]", inputs: ["id"], exportAs: ["matDialogTitle"] }, { kind: "directive", type: i1$2.MatDialogActions, selector: "[mat-dialog-actions], mat-dialog-actions, [matDialogActions]", inputs: ["align"] }, { kind: "directive", type: i1$2.MatDialogContent, selector: "[mat-dialog-content], mat-dialog-content, [matDialogContent]" }, { kind: "ngmodule", type: MatButtonModule }, { kind: "component", type: i6.MatButton, selector: " button[matButton], a[matButton], button[mat-button], button[mat-raised-button], button[mat-flat-button], button[mat-stroked-button], a[mat-button], a[mat-raised-button], a[mat-flat-button], a[mat-stroked-button] ", inputs: ["matButton"], exportAs: ["matButton", "matAnchor"] }] }); }
|
|
1014
2024
|
}
|
|
1015
|
-
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.
|
|
2025
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.13", ngImport: i0, type: ConfirmDialog, decorators: [{
|
|
1016
2026
|
type: Component,
|
|
1017
2027
|
args: [{ selector: 'shell-confirm-dialog', imports: [MatDialogModule, MatButtonModule], template: `
|
|
1018
2028
|
<h2 mat-dialog-title>{{ data.title }}</h2>
|
|
@@ -1038,5 +2048,5 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.12", ngImpo
|
|
|
1038
2048
|
* Generated bundle index. Do not edit.
|
|
1039
2049
|
*/
|
|
1040
2050
|
|
|
1041
|
-
export { AuthenticationService, CLEAR_USAGE, CommandRegistryService, CommandService, CommandTerminal, ConfirmDialog, HELP_USAGE, KeyboardShortcutService, Login, MainContent, RegexValidatorDirective, SHELL_APP_NAME, SHELL_AUTH_URLS, SHELL_COMMANDS, SHELL_MENU_ITEMS, SHELL_POST_LOGIN_ROUTE, Shell, SideMenu, StatusBar, TerminalFocusService, ThemeService, ThinkingService, authGuard, authInterceptor, guestGuard, provideShellConfig, tokenize };
|
|
2051
|
+
export { AuthenticationService, CLEAR_USAGE, CommandRegistryService, CommandService, CommandTerminal, ConfirmDialog, FAQ_CONTENT_LOADER, FAQ_DEV_EDITOR, FAQ_DEV_MODE, FAQ_DEV_WORKSPACE_ROOT, FaqAuthoringPanel, FaqHistoryService, FaqMarkdownRendererService, FaqModal, FaqService, HELP_USAGE, HttpFaqContentLoader, KeyboardShortcutService, Login, MainContent, RegexValidatorDirective, SHELL_APP_NAME, SHELL_AUTH_URLS, SHELL_COMMANDS, SHELL_MENU_ITEMS, SHELL_POST_LOGIN_ROUTE, Shell, SideMenu, StatusBar, TerminalFocusService, ThemeService, ThinkingService, audienceAllowed, authGuard, authInterceptor, buildFallbackChain, guestGuard, provideShellConfig, tokenize };
|
|
1042
2052
|
//# sourceMappingURL=franzzemen-brokenstock-shell.mjs.map
|