@franzzemen/brokenstock-shell 0.7.3 → 0.8.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import * as i0 from '@angular/core';
|
|
2
|
-
import { InjectionToken, makeEnvironmentProviders, signal, computed, Injectable, inject, input, Directive, isDevMode, effect, untracked, HostListener, ViewChild, ViewEncapsulation, ChangeDetectionStrategy, Component, NgZone, viewChild, afterNextRender } from '@angular/core';
|
|
2
|
+
import { InjectionToken, makeEnvironmentProviders, signal, computed, Injectable, inject, input, Directive, isDevMode, effect, untracked, HostListener, ViewChild, ViewEncapsulation, ChangeDetectionStrategy, Component, DestroyRef, NgZone, viewChild, afterNextRender, linkedSignal } from '@angular/core';
|
|
3
3
|
import * as i2$1 from '@angular/router';
|
|
4
4
|
import { Router, RouterLink, RouterLinkActive, ActivatedRoute, RouterOutlet } from '@angular/router';
|
|
5
5
|
import { BEARER_TOKEN_REGEX, HttpHeader, USERNAME_REGEX, PASSWORD_REGEX } from '@franzzemen/utility';
|
|
@@ -20,6 +20,7 @@ import * as i2 from '@angular/material/tooltip';
|
|
|
20
20
|
import { MatTooltipModule } from '@angular/material/tooltip';
|
|
21
21
|
import * as i1$1 from '@angular/material/progress-spinner';
|
|
22
22
|
import { MatProgressSpinnerModule } from '@angular/material/progress-spinner';
|
|
23
|
+
import { CdkTrapFocus } from '@angular/cdk/a11y';
|
|
23
24
|
import * as i5 from '@angular/cdk/text-field';
|
|
24
25
|
import { TextFieldModule } from '@angular/cdk/text-field';
|
|
25
26
|
import { MatFormField, MatLabel } from '@angular/material/form-field';
|
|
@@ -2260,12 +2261,94 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.2", ngImpor
|
|
|
2260
2261
|
args: [{ providedIn: 'root' }]
|
|
2261
2262
|
}] });
|
|
2262
2263
|
|
|
2264
|
+
/*
|
|
2265
|
+
Created by Franz Zemen 2026-10-16
|
|
2266
|
+
License Type: UNLICENSED
|
|
2267
|
+
|
|
2268
|
+
Phone layout of the app frame (broken-stock scanner-phone-display.prd.md D9 / E8).
|
|
2269
|
+
*/
|
|
2270
|
+
/**
|
|
2271
|
+
* Below this width the frame is in its phone layout: no icon column, a ☰ menu
|
|
2272
|
+
* button in the top bar, a one-line top bar.
|
|
2273
|
+
*
|
|
2274
|
+
* The same 820px line broken-stock's screens use (scanner-filter-editor.prd.md
|
|
2275
|
+
* D11), written the same way. `819.98px` rather than `819px` so a fractional
|
|
2276
|
+
* width from browser zoom (819.5px) does not fall between the two layouts.
|
|
2277
|
+
*
|
|
2278
|
+
* ⚠ The CSS repeats this query in shell.css and status-bar.css —
|
|
2279
|
+
* a media query cannot read a TypeScript constant. phone-layout.service.spec.ts
|
|
2280
|
+
* fails if any of them drifts from this value.
|
|
2281
|
+
*/
|
|
2282
|
+
const SHELL_PHONE_MEDIA_QUERY = '(max-width: 819.98px)';
|
|
2283
|
+
/**
|
|
2284
|
+
* Whether the frame is in its phone layout, and whether the phone menu is open.
|
|
2285
|
+
*
|
|
2286
|
+
* Desktop never sees any of this: `isPhone` is false there, the menu cannot open,
|
|
2287
|
+
* and the rail renders exactly as before.
|
|
2288
|
+
*/
|
|
2289
|
+
class PhoneLayoutService {
|
|
2290
|
+
#window;
|
|
2291
|
+
#isPhone;
|
|
2292
|
+
#menuOpen;
|
|
2293
|
+
constructor() {
|
|
2294
|
+
this.#window = inject(DOCUMENT).defaultView;
|
|
2295
|
+
this.#isPhone = signal(false, /* @ts-ignore */
|
|
2296
|
+
...(ngDevMode ? [{ debugName: "#isPhone" }] : /* istanbul ignore next */ []));
|
|
2297
|
+
this.#menuOpen = signal(false, /* @ts-ignore */
|
|
2298
|
+
...(ngDevMode ? [{ debugName: "#menuOpen" }] : /* istanbul ignore next */ []));
|
|
2299
|
+
/** True below 820px. Always false where there is no window (server rendering). */
|
|
2300
|
+
this.isPhone = this.#isPhone.asReadonly();
|
|
2301
|
+
/** The ☰ menu's open state. Only ever true while `isPhone` is true. */
|
|
2302
|
+
this.menuOpen = this.#menuOpen.asReadonly();
|
|
2303
|
+
const mql = this.#window?.matchMedia?.(SHELL_PHONE_MEDIA_QUERY);
|
|
2304
|
+
if (!mql)
|
|
2305
|
+
return;
|
|
2306
|
+
this.#isPhone.set(mql.matches);
|
|
2307
|
+
const onChange = (event) => {
|
|
2308
|
+
this.#isPhone.set(event.matches);
|
|
2309
|
+
// Rotating or widening past 820px brings the rail back; a menu left open
|
|
2310
|
+
// would then cover the screen with nothing on it to close it.
|
|
2311
|
+
if (!event.matches)
|
|
2312
|
+
this.#menuOpen.set(false);
|
|
2313
|
+
};
|
|
2314
|
+
mql.addEventListener('change', onChange);
|
|
2315
|
+
inject(DestroyRef).onDestroy(() => mql.removeEventListener('change', onChange));
|
|
2316
|
+
}
|
|
2317
|
+
toggleMenu() {
|
|
2318
|
+
this.#menuOpen.set(this.#isPhone() && !this.#menuOpen());
|
|
2319
|
+
}
|
|
2320
|
+
closeMenu() {
|
|
2321
|
+
this.#menuOpen.set(false);
|
|
2322
|
+
}
|
|
2323
|
+
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.2", ngImport: i0, type: PhoneLayoutService, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
|
|
2324
|
+
static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "22.1.2", ngImport: i0, type: PhoneLayoutService, providedIn: 'root' }); }
|
|
2325
|
+
}
|
|
2326
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.2", ngImport: i0, type: PhoneLayoutService, decorators: [{
|
|
2327
|
+
type: Injectable,
|
|
2328
|
+
args: [{ providedIn: 'root' }]
|
|
2329
|
+
}], ctorParameters: () => [] });
|
|
2330
|
+
|
|
2331
|
+
/**
|
|
2332
|
+
* Asks the command box to take the cursor. Called by the shell after most
|
|
2333
|
+
* actions (rail choices, Log Out, ?, ⧉, Escape, sign-in) and by apps.
|
|
2334
|
+
*
|
|
2335
|
+
* ⚠ A NO-OP ON A PHONE (scanner-phone-display E11). The console is not rendered
|
|
2336
|
+
* below 820px, and the one place every focus request passes through is here, so
|
|
2337
|
+
* this is where it is refused: focusing a text box on a phone raises the
|
|
2338
|
+
* on-screen keyboard over the screen the user just opened. Refusing it at the
|
|
2339
|
+
* source also means a request made on a phone is not left counted up, waiting to
|
|
2340
|
+
* be acted on when the console appears after a rotation.
|
|
2341
|
+
*/
|
|
2263
2342
|
class TerminalFocusService {
|
|
2264
2343
|
constructor() {
|
|
2344
|
+
this.#phoneLayout = inject(PhoneLayoutService);
|
|
2265
2345
|
this.focusRequested = signal(0, /* @ts-ignore */
|
|
2266
2346
|
...(ngDevMode ? [{ debugName: "focusRequested" }] : /* istanbul ignore next */ []));
|
|
2267
2347
|
}
|
|
2348
|
+
#phoneLayout;
|
|
2268
2349
|
requestFocus() {
|
|
2350
|
+
if (this.#phoneLayout.isPhone())
|
|
2351
|
+
return;
|
|
2269
2352
|
this.focusRequested.update(v => v + 1);
|
|
2270
2353
|
}
|
|
2271
2354
|
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.2", ngImport: i0, type: TerminalFocusService, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
|
|
@@ -2993,7 +3076,12 @@ class StatusBar {
|
|
|
2993
3076
|
this.#focusService = inject(TerminalFocusService);
|
|
2994
3077
|
this.#commandService = inject(CommandService);
|
|
2995
3078
|
this.#sharedSession = inject(SHELL_SHARED_SESSION, { optional: true }) ?? false;
|
|
3079
|
+
this.#phoneLayout = inject(PhoneLayoutService);
|
|
2996
3080
|
this.isAuthenticated = this.#authService.authenticated;
|
|
3081
|
+
/** The ☰ button: phones only, and only when there is a menu to open (signed in). */
|
|
3082
|
+
this.showMenuButton = computed(() => this.#phoneLayout.isPhone() && this.isAuthenticated(), /* @ts-ignore */
|
|
3083
|
+
...(ngDevMode ? [{ debugName: "showMenuButton" }] : /* istanbul ignore next */ []));
|
|
3084
|
+
this.menuOpen = this.#phoneLayout.menuOpen;
|
|
2997
3085
|
/**
|
|
2998
3086
|
* Whether a second window is offered at all (multiple-windows PRD D14).
|
|
2999
3087
|
*
|
|
@@ -3017,6 +3105,7 @@ class StatusBar {
|
|
|
3017
3105
|
#focusService;
|
|
3018
3106
|
#commandService;
|
|
3019
3107
|
#sharedSession;
|
|
3108
|
+
#phoneLayout;
|
|
3020
3109
|
logout() {
|
|
3021
3110
|
this.#authService.logout();
|
|
3022
3111
|
this.#commandService.echoCommand('logout');
|
|
@@ -3041,16 +3130,19 @@ class StatusBar {
|
|
|
3041
3130
|
window.open(window.location.href, '_blank', `noopener,noreferrer,width=${POP_OUT_WIDTH},height=${POP_OUT_HEIGHT}`);
|
|
3042
3131
|
this.#focusService.requestFocus();
|
|
3043
3132
|
}
|
|
3133
|
+
toggleMenu() {
|
|
3134
|
+
this.#phoneLayout.toggleMenu();
|
|
3135
|
+
}
|
|
3044
3136
|
openFaq() {
|
|
3045
3137
|
this.#commandService.execute('faq');
|
|
3046
3138
|
this.#focusService.requestFocus();
|
|
3047
3139
|
}
|
|
3048
3140
|
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.2", ngImport: i0, type: StatusBar, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
|
|
3049
|
-
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.1.2", 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 @if (canPopOut()) {\n <button\n type=\"button\"\n class=\"faq-button text-interactive\"\n (click)=\"popOut()\"\n title=\"Open this screen in a second window\"\n aria-label=\"Open this screen in a second window\"\n >\u29C9</button>\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"], changeDetection: i0.ChangeDetectionStrategy.Eager }); }
|
|
3141
|
+
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.1.2", type: StatusBar, isStandalone: true, selector: "shell-status-bar", ngImport: i0, template: "<div class=\"status-bar\">\n @if (showMenuButton()) {\n <!-- Phone only (scanner-phone-display D9): the icon column is folded into this menu. -->\n <button\n type=\"button\"\n class=\"menu-button text-interactive\"\n (click)=\"toggleMenu()\"\n [attr.aria-expanded]=\"menuOpen()\"\n aria-controls=\"shell-phone-menu\"\n aria-label=\"Menu\"\n ><mat-icon>menu</mat-icon></button>\n }\n <div class=\"status-item status-item-own\">\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 @if (canPopOut()) {\n <button\n type=\"button\"\n class=\"faq-button text-interactive\"\n (click)=\"popOut()\"\n title=\"Open this screen in a second window\"\n aria-label=\"Open this screen in a second window\"\n >\u29C9</button>\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}.menu-button{flex-shrink:0;display:inline-flex;align-items:center;justify-content:center;width:32px;height:32px;padding:0;margin-left:-4px;background:none;border:none;border-radius:6px;cursor:pointer}.menu-button:hover{background-color:var(--color-surface-light)}@media(max-width:819.98px){.status-bar{gap:12px;padding:0 8px;white-space:nowrap;overflow:hidden}:host ::ng-deep .status-item{min-width:0;flex:0 1 auto}.status-item-own,.faq-button{flex-shrink:0}:host ::ng-deep .status-label:not(.shell-keep-on-phone){display:none}:host ::ng-deep .status-value{min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}}\n"], dependencies: [{ kind: "ngmodule", type: MatIconModule }, { kind: "component", type: i1.MatIcon, selector: "mat-icon", inputs: ["color", "inline", "svgIcon", "fontSet", "fontIcon"], exportAs: ["matIcon"] }], changeDetection: i0.ChangeDetectionStrategy.Eager }); }
|
|
3050
3142
|
}
|
|
3051
3143
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.2", ngImport: i0, type: StatusBar, decorators: [{
|
|
3052
3144
|
type: Component,
|
|
3053
|
-
args: [{ selector: 'shell-status-bar', changeDetection: ChangeDetectionStrategy.Eager, 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 @if (canPopOut()) {\n <button\n type=\"button\"\n class=\"faq-button text-interactive\"\n (click)=\"popOut()\"\n title=\"Open this screen in a second window\"\n aria-label=\"Open this screen in a second window\"\n >\u29C9</button>\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"] }]
|
|
3145
|
+
args: [{ selector: 'shell-status-bar', imports: [MatIconModule], changeDetection: ChangeDetectionStrategy.Eager, template: "<div class=\"status-bar\">\n @if (showMenuButton()) {\n <!-- Phone only (scanner-phone-display D9): the icon column is folded into this menu. -->\n <button\n type=\"button\"\n class=\"menu-button text-interactive\"\n (click)=\"toggleMenu()\"\n [attr.aria-expanded]=\"menuOpen()\"\n aria-controls=\"shell-phone-menu\"\n aria-label=\"Menu\"\n ><mat-icon>menu</mat-icon></button>\n }\n <div class=\"status-item status-item-own\">\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 @if (canPopOut()) {\n <button\n type=\"button\"\n class=\"faq-button text-interactive\"\n (click)=\"popOut()\"\n title=\"Open this screen in a second window\"\n aria-label=\"Open this screen in a second window\"\n >\u29C9</button>\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}.menu-button{flex-shrink:0;display:inline-flex;align-items:center;justify-content:center;width:32px;height:32px;padding:0;margin-left:-4px;background:none;border:none;border-radius:6px;cursor:pointer}.menu-button:hover{background-color:var(--color-surface-light)}@media(max-width:819.98px){.status-bar{gap:12px;padding:0 8px;white-space:nowrap;overflow:hidden}:host ::ng-deep .status-item{min-width:0;flex:0 1 auto}.status-item-own,.faq-button{flex-shrink:0}:host ::ng-deep .status-label:not(.shell-keep-on-phone){display:none}:host ::ng-deep .status-value{min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}}\n"] }]
|
|
3054
3146
|
}] });
|
|
3055
3147
|
|
|
3056
3148
|
class SideMenu {
|
|
@@ -3060,7 +3152,27 @@ class SideMenu {
|
|
|
3060
3152
|
this.#shortcutService = inject(KeyboardShortcutService);
|
|
3061
3153
|
this.#terminalVisibility = inject(TerminalVisibilityService);
|
|
3062
3154
|
this.hasShortcuts = this.#shortcutService.activeShortcuts;
|
|
3155
|
+
this.#phoneLayout = inject(PhoneLayoutService);
|
|
3063
3156
|
this.terminalVisible = this.#terminalVisibility.visible;
|
|
3157
|
+
/**
|
|
3158
|
+
* Phone-menu rendering (scanner-phone-display D9): the same items as the icon
|
|
3159
|
+
* column, each with its label as visible text — a tooltip needs a hover a
|
|
3160
|
+
* touch screen does not have.
|
|
3161
|
+
*
|
|
3162
|
+
* ⚠ The keyboard-shortcuts button (Alt+?) is left out here. It opens a list of
|
|
3163
|
+
* Alt-key bindings, and a phone has no Alt key. The shortcuts themselves are
|
|
3164
|
+
* untouched — they are document-level listeners and work from a hardware
|
|
3165
|
+
* keyboard whatever the layout.
|
|
3166
|
+
*
|
|
3167
|
+
* The console show/hide button is left out too: the console is not rendered
|
|
3168
|
+
* on a phone at all (Franz, 2026-10-16, scanner-phone-display E11), so the
|
|
3169
|
+
* button would have nothing to show.
|
|
3170
|
+
*/
|
|
3171
|
+
this.drawer = input(false, /* @ts-ignore */
|
|
3172
|
+
...(ngDevMode ? [{ debugName: "drawer" }] : /* istanbul ignore next */ []));
|
|
3173
|
+
/** The console show/hide button — the icon column only (see `drawer`). */
|
|
3174
|
+
this.showConsoleToggle = computed(() => !this.drawer(), /* @ts-ignore */
|
|
3175
|
+
...(ngDevMode ? [{ debugName: "showConsoleToggle" }] : /* istanbul ignore next */ []));
|
|
3064
3176
|
this.#allMenuItems = inject(SHELL_MENU_ITEMS, { optional: true }) ?? [];
|
|
3065
3177
|
this.menuItems = computed(() => this.#allMenuItems.filter(item => !item.visible || item.visible()), /* @ts-ignore */
|
|
3066
3178
|
...(ngDevMode ? [{ debugName: "menuItems" }] : /* istanbul ignore next */ []));
|
|
@@ -3069,6 +3181,7 @@ class SideMenu {
|
|
|
3069
3181
|
#commandService;
|
|
3070
3182
|
#shortcutService;
|
|
3071
3183
|
#terminalVisibility;
|
|
3184
|
+
#phoneLayout;
|
|
3072
3185
|
#allMenuItems;
|
|
3073
3186
|
/**
|
|
3074
3187
|
* Readiness, not entitlement — an unentitled item never reaches here, having
|
|
@@ -3086,6 +3199,13 @@ class SideMenu {
|
|
|
3086
3199
|
if (item.command) {
|
|
3087
3200
|
this.#commandService.echoCommand(item.command);
|
|
3088
3201
|
}
|
|
3202
|
+
if (this.drawer()) {
|
|
3203
|
+
// ⚠ No terminal focus from the phone menu. Focusing the command box on a
|
|
3204
|
+
// phone raises the on-screen keyboard over half the screen you just
|
|
3205
|
+
// navigated to.
|
|
3206
|
+
this.#phoneLayout.closeMenu();
|
|
3207
|
+
return;
|
|
3208
|
+
}
|
|
3089
3209
|
this.#focusService.requestFocus();
|
|
3090
3210
|
}
|
|
3091
3211
|
toggleHelp() {
|
|
@@ -3095,12 +3215,12 @@ class SideMenu {
|
|
|
3095
3215
|
this.#terminalVisibility.toggle();
|
|
3096
3216
|
}
|
|
3097
3217
|
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.2", ngImport: i0, type: SideMenu, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
|
|
3098
|
-
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.1.2", type: SideMenu, isStandalone: true, selector: "shell-side-menu", ngImport: i0, template: "<nav class=\"side-menu\">\n @for (item of menuItems(); track item.route) {\n @if (isEnabled(item)) {\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 } @else {\n <!-- No routerLink: the item is inert. Pointer events stay live (see the\n .disabled rule) so the tooltip still fires \u2014 it carries the only\n explanation the user gets. -->\n <span\n class=\"menu-item disabled\"\n [matTooltip]=\"disabledReasonFor(item)\"\n matTooltipPosition=\"right\"\n aria-disabled=\"true\"\n >\n <mat-icon>{{ item.icon }}</mat-icon>\n </span>\n }\n }\n <div class=\"menu-spacer\"></div>\n <button\n
|
|
3218
|
+
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.1.2", type: SideMenu, isStandalone: true, selector: "shell-side-menu", inputs: { drawer: { classPropertyName: "drawer", publicName: "drawer", isSignal: true, isRequired: false, transformFunction: null } }, ngImport: i0, template: "<nav class=\"side-menu\" [class.drawer]=\"drawer()\" [attr.aria-label]=\"drawer() ? 'Menu' : null\">\n @for (item of menuItems(); track item.route) {\n @if (isEnabled(item)) {\n <a\n class=\"menu-item\"\n [routerLink]=\"item.route\"\n routerLinkActive=\"active\"\n [matTooltip]=\"item.label\"\n matTooltipPosition=\"right\"\n [matTooltipDisabled]=\"drawer()\"\n (click)=\"onMenuClick(item)\"\n >\n <mat-icon>{{ item.icon }}</mat-icon>\n @if (drawer()) {\n <span class=\"menu-label\">{{ item.label }}</span>\n }\n </a>\n } @else {\n <!-- No routerLink: the item is inert. Pointer events stay live (see the\n .disabled rule) so the tooltip still fires \u2014 it carries the only\n explanation the user gets. In the phone menu the explanation is shown\n as text instead, since there is no hover to fire it. -->\n <span\n class=\"menu-item disabled\"\n [matTooltip]=\"disabledReasonFor(item)\"\n matTooltipPosition=\"right\"\n [matTooltipDisabled]=\"drawer()\"\n aria-disabled=\"true\"\n >\n <mat-icon>{{ item.icon }}</mat-icon>\n @if (drawer()) {\n <span class=\"menu-label\">\n {{ item.label }}\n <span class=\"menu-reason\">{{ disabledReasonFor(item) }}</span>\n </span>\n }\n </span>\n }\n }\n <div class=\"menu-spacer\"></div>\n @if (showConsoleToggle()) {\n <button\n class=\"menu-item terminal-toggle\"\n type=\"button\"\n [class.active]=\"terminalVisible()\"\n [matTooltip]=\"terminalVisible() ? 'Hide command console' : 'Show command console'\"\n matTooltipPosition=\"right\"\n [attr.aria-pressed]=\"terminalVisible()\"\n (click)=\"toggleTerminal()\"\n >\n <mat-icon>terminal</mat-icon>\n </button>\n }\n @if (hasShortcuts() && !drawer()) {\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-item.disabled{opacity:.25;cursor:not-allowed}.menu-item.disabled:hover{background-color:transparent;color:var(--color-secondary-info)}.menu-spacer{flex:1}.menu-item.terminal-toggle{border:none;background:none;padding:0;cursor:pointer;margin-bottom:4px}.menu-item.terminal-toggle.active{color:var(--color-interactive)}.menu-item.terminal-toggle.active:hover{background-color:var(--color-interactive);color:var(--color-surface-dark)}.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}.side-menu.drawer{align-items:stretch;padding:8px;gap:2px;overflow-y:auto}.side-menu.drawer .menu-item{width:auto;height:44px;flex-shrink:0;justify-content:flex-start;gap:14px;padding:0 12px}.side-menu.drawer .menu-item.disabled{opacity:.5;height:auto;min-height:44px;padding-top:4px;padding-bottom:4px}.menu-label{display:block;min-width:0;font-size:.85rem;line-height:1.25;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.menu-reason{display:block;font-size:.7rem;white-space:normal}\n"], dependencies: [{ kind: "directive", type: RouterLink, selector: "[routerLink]", inputs: ["target", "queryParams", "fragment", "queryParamsHandling", "state", "info", "relativeTo", "preserveFragment", "skipLocationChange", "replaceUrl", "browserUrl", "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"] }], changeDetection: i0.ChangeDetectionStrategy.Eager }); }
|
|
3099
3219
|
}
|
|
3100
3220
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.2", ngImport: i0, type: SideMenu, decorators: [{
|
|
3101
3221
|
type: Component,
|
|
3102
|
-
args: [{ selector: 'shell-side-menu', imports: [RouterLink, RouterLinkActive, MatIconModule, MatTooltipModule], changeDetection: ChangeDetectionStrategy.Eager, template: "<nav class=\"side-menu\">\n @for (item of menuItems(); track item.route) {\n @if (isEnabled(item)) {\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 } @else {\n <!-- No routerLink: the item is inert. Pointer events stay live (see the\n .disabled rule) so the tooltip still fires \u2014 it carries the only\n explanation the user gets. -->\n <span\n class=\"menu-item disabled\"\n [matTooltip]=\"disabledReasonFor(item)\"\n matTooltipPosition=\"right\"\n aria-disabled=\"true\"\n >\n <mat-icon>{{ item.icon }}</mat-icon>\n </span>\n }\n }\n <div class=\"menu-spacer\"></div>\n <button\n
|
|
3103
|
-
}] });
|
|
3222
|
+
args: [{ selector: 'shell-side-menu', imports: [RouterLink, RouterLinkActive, MatIconModule, MatTooltipModule], changeDetection: ChangeDetectionStrategy.Eager, template: "<nav class=\"side-menu\" [class.drawer]=\"drawer()\" [attr.aria-label]=\"drawer() ? 'Menu' : null\">\n @for (item of menuItems(); track item.route) {\n @if (isEnabled(item)) {\n <a\n class=\"menu-item\"\n [routerLink]=\"item.route\"\n routerLinkActive=\"active\"\n [matTooltip]=\"item.label\"\n matTooltipPosition=\"right\"\n [matTooltipDisabled]=\"drawer()\"\n (click)=\"onMenuClick(item)\"\n >\n <mat-icon>{{ item.icon }}</mat-icon>\n @if (drawer()) {\n <span class=\"menu-label\">{{ item.label }}</span>\n }\n </a>\n } @else {\n <!-- No routerLink: the item is inert. Pointer events stay live (see the\n .disabled rule) so the tooltip still fires \u2014 it carries the only\n explanation the user gets. In the phone menu the explanation is shown\n as text instead, since there is no hover to fire it. -->\n <span\n class=\"menu-item disabled\"\n [matTooltip]=\"disabledReasonFor(item)\"\n matTooltipPosition=\"right\"\n [matTooltipDisabled]=\"drawer()\"\n aria-disabled=\"true\"\n >\n <mat-icon>{{ item.icon }}</mat-icon>\n @if (drawer()) {\n <span class=\"menu-label\">\n {{ item.label }}\n <span class=\"menu-reason\">{{ disabledReasonFor(item) }}</span>\n </span>\n }\n </span>\n }\n }\n <div class=\"menu-spacer\"></div>\n @if (showConsoleToggle()) {\n <button\n class=\"menu-item terminal-toggle\"\n type=\"button\"\n [class.active]=\"terminalVisible()\"\n [matTooltip]=\"terminalVisible() ? 'Hide command console' : 'Show command console'\"\n matTooltipPosition=\"right\"\n [attr.aria-pressed]=\"terminalVisible()\"\n (click)=\"toggleTerminal()\"\n >\n <mat-icon>terminal</mat-icon>\n </button>\n }\n @if (hasShortcuts() && !drawer()) {\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-item.disabled{opacity:.25;cursor:not-allowed}.menu-item.disabled:hover{background-color:transparent;color:var(--color-secondary-info)}.menu-spacer{flex:1}.menu-item.terminal-toggle{border:none;background:none;padding:0;cursor:pointer;margin-bottom:4px}.menu-item.terminal-toggle.active{color:var(--color-interactive)}.menu-item.terminal-toggle.active:hover{background-color:var(--color-interactive);color:var(--color-surface-dark)}.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}.side-menu.drawer{align-items:stretch;padding:8px;gap:2px;overflow-y:auto}.side-menu.drawer .menu-item{width:auto;height:44px;flex-shrink:0;justify-content:flex-start;gap:14px;padding:0 12px}.side-menu.drawer .menu-item.disabled{opacity:.5;height:auto;min-height:44px;padding-top:4px;padding-bottom:4px}.menu-label{display:block;min-width:0;font-size:.85rem;line-height:1.25;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.menu-reason{display:block;font-size:.7rem;white-space:normal}\n"] }]
|
|
3223
|
+
}], propDecorators: { drawer: [{ type: i0.Input, args: [{ isSignal: true, alias: "drawer", required: false }] }] } });
|
|
3104
3224
|
|
|
3105
3225
|
class MainContent {
|
|
3106
3226
|
constructor() {
|
|
@@ -3131,11 +3251,24 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.2", ngImpor
|
|
|
3131
3251
|
}] });
|
|
3132
3252
|
|
|
3133
3253
|
class CommandTerminal {
|
|
3254
|
+
#phoneLayout;
|
|
3134
3255
|
constructor() {
|
|
3135
3256
|
this.commandService = inject(CommandService);
|
|
3136
3257
|
this.registry = inject(CommandRegistryService);
|
|
3137
3258
|
this.authService = inject(AuthenticationService);
|
|
3138
3259
|
this.terminalFocusService = inject(TerminalFocusService);
|
|
3260
|
+
this.#phoneLayout = inject(PhoneLayoutService);
|
|
3261
|
+
/**
|
|
3262
|
+
* Whether the command box takes the cursor when the console first appears.
|
|
3263
|
+
*
|
|
3264
|
+
* True for a standalone console and for the console showing at start-up or
|
|
3265
|
+
* after the user turns it back on. The shell passes false when the console
|
|
3266
|
+
* appears because the screen widened past 820px (a tablet or phone rotated to
|
|
3267
|
+
* landscape): a layout change is not the user asking to type, and taking the
|
|
3268
|
+
* cursor there would pull it out of whatever they were doing.
|
|
3269
|
+
*/
|
|
3270
|
+
this.focusOnAppear = input(true, /* @ts-ignore */
|
|
3271
|
+
...(ngDevMode ? [{ debugName: "focusOnAppear" }] : /* istanbul ignore next */ []));
|
|
3139
3272
|
this.history = computed(() => [...this.commandService.history()].reverse(), /* @ts-ignore */
|
|
3140
3273
|
...(ngDevMode ? [{ debugName: "history" }] : /* istanbul ignore next */ []));
|
|
3141
3274
|
this.currentInput = this.commandService.currentInput;
|
|
@@ -3153,14 +3286,32 @@ class CommandTerminal {
|
|
|
3153
3286
|
return suggestions.length > 0 ? suggestions[0] : '';
|
|
3154
3287
|
}, /* @ts-ignore */
|
|
3155
3288
|
...(ngDevMode ? [{ debugName: "ghostText" }] : /* istanbul ignore next */ []));
|
|
3156
|
-
afterNextRender(() =>
|
|
3289
|
+
afterNextRender(() => {
|
|
3290
|
+
if (this.focusOnAppear())
|
|
3291
|
+
this.focusInput();
|
|
3292
|
+
});
|
|
3293
|
+
// Both effects below skip their first run. An effect runs once when it is
|
|
3294
|
+
// created, so without the skip each would focus the box on appearance a
|
|
3295
|
+
// second and third time — after `focusOnAppear` had said not to. Appearance
|
|
3296
|
+
// is the afterNextRender above's job alone; these react to CHANGES.
|
|
3297
|
+
let firstProcessingRun = true;
|
|
3157
3298
|
effect(() => {
|
|
3158
|
-
|
|
3299
|
+
const busy = this.isProcessing();
|
|
3300
|
+
if (firstProcessingRun) {
|
|
3301
|
+
firstProcessingRun = false;
|
|
3302
|
+
return;
|
|
3303
|
+
}
|
|
3304
|
+
if (!busy) {
|
|
3159
3305
|
setTimeout(() => this.focusInput());
|
|
3160
3306
|
}
|
|
3161
3307
|
});
|
|
3308
|
+
let firstRequestRun = true;
|
|
3162
3309
|
effect(() => {
|
|
3163
3310
|
this.terminalFocusService.focusRequested();
|
|
3311
|
+
if (firstRequestRun) {
|
|
3312
|
+
firstRequestRun = false;
|
|
3313
|
+
return;
|
|
3314
|
+
}
|
|
3164
3315
|
setTimeout(() => this.focusInput());
|
|
3165
3316
|
});
|
|
3166
3317
|
}
|
|
@@ -3220,16 +3371,23 @@ class CommandTerminal {
|
|
|
3220
3371
|
return 'error';
|
|
3221
3372
|
return '';
|
|
3222
3373
|
}
|
|
3374
|
+
/**
|
|
3375
|
+
* ⚠ A no-op on a phone (scanner-phone-display E11). The shell does not render
|
|
3376
|
+
* the console there at all; this guard covers an app that mounts the console
|
|
3377
|
+
* itself, so nothing on a phone can raise the on-screen keyboard unasked.
|
|
3378
|
+
*/
|
|
3223
3379
|
focusInput() {
|
|
3380
|
+
if (this.#phoneLayout.isPhone())
|
|
3381
|
+
return;
|
|
3224
3382
|
this.inputEl()?.nativeElement.focus();
|
|
3225
3383
|
}
|
|
3226
3384
|
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.2", ngImport: i0, type: CommandTerminal, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
|
|
3227
|
-
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.1.2", 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]):not([ngNoCva])[formControlName],textarea:not([ngNoCva])[formControlName],input:not([type=checkbox]):not([ngNoCva])[formControl],textarea:not([ngNoCva])[formControl],input:not([type=checkbox]):not([ngNoCva])[ngModel],textarea:not([ngNoCva])[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"] }], changeDetection: i0.ChangeDetectionStrategy.Eager }); }
|
|
3385
|
+
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.1.2", type: CommandTerminal, isStandalone: true, selector: "shell-command-terminal", inputs: { focusOnAppear: { classPropertyName: "focusOnAppear", publicName: "focusOnAppear", isSignal: true, isRequired: false, transformFunction: null } }, 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]):not([ngNoCva])[formControlName],textarea:not([ngNoCva])[formControlName],input:not([type=checkbox]):not([ngNoCva])[formControl],textarea:not([ngNoCva])[formControl],input:not([type=checkbox]):not([ngNoCva])[ngModel],textarea:not([ngNoCva])[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"] }], changeDetection: i0.ChangeDetectionStrategy.Eager }); }
|
|
3228
3386
|
}
|
|
3229
3387
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.2", ngImport: i0, type: CommandTerminal, decorators: [{
|
|
3230
3388
|
type: Component,
|
|
3231
3389
|
args: [{ selector: 'shell-command-terminal', imports: [FormsModule], changeDetection: ChangeDetectionStrategy.Eager, 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"] }]
|
|
3232
|
-
}], ctorParameters: () => [], propDecorators: { inputEl: [{ type: i0.ViewChild, args: ['terminalInput', { isSignal: true }] }] } });
|
|
3390
|
+
}], ctorParameters: () => [], propDecorators: { focusOnAppear: [{ type: i0.Input, args: [{ isSignal: true, alias: "focusOnAppear", required: false }] }], inputEl: [{ type: i0.ViewChild, args: ['terminalInput', { isSignal: true }] }] } });
|
|
3233
3391
|
|
|
3234
3392
|
const MIN_TERMINAL_HEIGHT = 80;
|
|
3235
3393
|
const MAX_TERMINAL_RATIO = 0.6;
|
|
@@ -3243,34 +3401,90 @@ class Shell {
|
|
|
3243
3401
|
this.isThinking = this.#thinkingService.isThinking;
|
|
3244
3402
|
this.isAuthenticated = this.#authService.authenticated;
|
|
3245
3403
|
this.helpOverlay = this.#commandService.helpOverlay;
|
|
3404
|
+
this.#phoneLayout = inject(PhoneLayoutService);
|
|
3246
3405
|
this.terminalVisible = this.#terminalVisibility.visible;
|
|
3406
|
+
/** Below 820px the icon column is replaced by the ☰ menu (scanner-phone-display D9). */
|
|
3407
|
+
this.isPhone = this.#phoneLayout.isPhone;
|
|
3408
|
+
this.phoneMenuOpen = this.#phoneLayout.menuOpen;
|
|
3409
|
+
/**
|
|
3410
|
+
* Whether the console (command box, output, and the resize bar above it) is
|
|
3411
|
+
* shown, and whether it takes the cursor when it appears.
|
|
3412
|
+
*
|
|
3413
|
+
* `show`: never on a phone — Franz, 2026-10-16 (scanner-phone-display E11).
|
|
3414
|
+
* The user's show/hide choice is kept untouched, so rotating to a width of
|
|
3415
|
+
* 820px or more brings the console back exactly as it was left. Commands do
|
|
3416
|
+
* not depend on the console: `CommandService.execute` runs from any button
|
|
3417
|
+
* whether or not it is on screen.
|
|
3418
|
+
*
|
|
3419
|
+
* `focusOnAppear`: false only when the console is appearing because the
|
|
3420
|
+
* screen widened past 820px (see CommandTerminal.focusOnAppear); true at
|
|
3421
|
+
* start-up and after the user turns the console on.
|
|
3422
|
+
*
|
|
3423
|
+
* ⚠ ONE SIGNAL, READ UNCONDITIONALLY BY THE TEMPLATE (`[class.terminal-hidden]`),
|
|
3424
|
+
* and that is load-bearing. The focus decision needs the PREVIOUS layout to
|
|
3425
|
+
* tell "widened from a phone" apart from "turned on". A linkedSignal only sees
|
|
3426
|
+
* the state as of its last read, so a value read only inside the console's
|
|
3427
|
+
* `@if` — never read while on a phone — would miss the phone state entirely
|
|
3428
|
+
* and grab the cursor on rotation.
|
|
3429
|
+
*/
|
|
3430
|
+
this.consoleState = linkedSignal({ ...(ngDevMode ? { debugName: "consoleState" } : /* istanbul ignore next */ {}), source: () => ({ visible: this.terminalVisible(), phone: this.isPhone() }),
|
|
3431
|
+
computation: (now, previous) => {
|
|
3432
|
+
const show = now.visible && !now.phone;
|
|
3433
|
+
if (!previous)
|
|
3434
|
+
return { show, focusOnAppear: true };
|
|
3435
|
+
if (previous.source.visible !== now.visible)
|
|
3436
|
+
return { show, focusOnAppear: true };
|
|
3437
|
+
if (previous.source.phone && !now.phone)
|
|
3438
|
+
return { show, focusOnAppear: false };
|
|
3439
|
+
return { show, focusOnAppear: previous.value.focusOnAppear };
|
|
3440
|
+
} });
|
|
3247
3441
|
this.terminalHeight = signal(`${DEFAULT_TERMINAL_HEIGHT}px`, /* @ts-ignore */
|
|
3248
3442
|
...(ngDevMode ? [{ debugName: "terminalHeight" }] : /* istanbul ignore next */ []));
|
|
3249
3443
|
this.isDragging = signal(false, /* @ts-ignore */
|
|
3250
3444
|
...(ngDevMode ? [{ debugName: "isDragging" }] : /* istanbul ignore next */ []));
|
|
3251
3445
|
this.#startY = 0;
|
|
3252
3446
|
this.#startHeight = DEFAULT_TERMINAL_HEIGHT;
|
|
3253
|
-
this.#onMouseMove = (event) =>
|
|
3254
|
-
const deltaY = event.clientY - this.#startY;
|
|
3255
|
-
const maxHeight = Math.floor(window.innerHeight * MAX_TERMINAL_RATIO);
|
|
3256
|
-
const newHeight = Math.min(maxHeight, Math.max(MIN_TERMINAL_HEIGHT, this.#startHeight - deltaY));
|
|
3257
|
-
this.terminalHeight.set(`${newHeight}px`);
|
|
3258
|
-
};
|
|
3447
|
+
this.#onMouseMove = (event) => this.#resizeTo(event.clientY);
|
|
3259
3448
|
this.#onMouseUp = () => {
|
|
3260
3449
|
this.isDragging.set(false);
|
|
3261
3450
|
document.removeEventListener('mousemove', this.#onMouseMove);
|
|
3262
3451
|
document.removeEventListener('mouseup', this.#onMouseUp);
|
|
3263
3452
|
};
|
|
3453
|
+
// Touch drag for the resize bar (scanner-phone-display E12). Only reachable
|
|
3454
|
+
// at 820px and wider — a tablet — since the bar is not rendered on a phone.
|
|
3455
|
+
// Kept separate from the mouse path so the desktop's behaviour is unchanged;
|
|
3456
|
+
// the bar's `touch-action: none` stops the page scrolling under the finger.
|
|
3457
|
+
this.#onTouchMove = (event) => {
|
|
3458
|
+
const touch = event.touches[0];
|
|
3459
|
+
if (touch)
|
|
3460
|
+
this.#resizeTo(touch.clientY);
|
|
3461
|
+
};
|
|
3462
|
+
this.#onTouchEnd = () => {
|
|
3463
|
+
this.isDragging.set(false);
|
|
3464
|
+
document.removeEventListener('touchmove', this.#onTouchMove);
|
|
3465
|
+
document.removeEventListener('touchend', this.#onTouchEnd);
|
|
3466
|
+
document.removeEventListener('touchcancel', this.#onTouchEnd);
|
|
3467
|
+
};
|
|
3264
3468
|
}
|
|
3265
3469
|
#thinkingService;
|
|
3266
3470
|
#commandService;
|
|
3267
3471
|
#authService;
|
|
3268
3472
|
#terminalVisibility;
|
|
3473
|
+
#phoneLayout;
|
|
3474
|
+
closePhoneMenu() {
|
|
3475
|
+
this.#phoneLayout.closeMenu();
|
|
3476
|
+
}
|
|
3269
3477
|
dismissHelp() {
|
|
3270
3478
|
this.#commandService.dismissHelp();
|
|
3271
3479
|
}
|
|
3272
3480
|
#startY;
|
|
3273
3481
|
#startHeight;
|
|
3482
|
+
#resizeTo(clientY) {
|
|
3483
|
+
const deltaY = clientY - this.#startY;
|
|
3484
|
+
const maxHeight = Math.floor(window.innerHeight * MAX_TERMINAL_RATIO);
|
|
3485
|
+
const newHeight = Math.min(maxHeight, Math.max(MIN_TERMINAL_HEIGHT, this.#startHeight - deltaY));
|
|
3486
|
+
this.terminalHeight.set(`${newHeight}px`);
|
|
3487
|
+
}
|
|
3274
3488
|
#onMouseMove;
|
|
3275
3489
|
#onMouseUp;
|
|
3276
3490
|
onResizeStart(event) {
|
|
@@ -3281,12 +3495,29 @@ class Shell {
|
|
|
3281
3495
|
document.addEventListener('mousemove', this.#onMouseMove);
|
|
3282
3496
|
document.addEventListener('mouseup', this.#onMouseUp);
|
|
3283
3497
|
}
|
|
3498
|
+
// Touch drag for the resize bar (scanner-phone-display E12). Only reachable
|
|
3499
|
+
// at 820px and wider — a tablet — since the bar is not rendered on a phone.
|
|
3500
|
+
// Kept separate from the mouse path so the desktop's behaviour is unchanged;
|
|
3501
|
+
// the bar's `touch-action: none` stops the page scrolling under the finger.
|
|
3502
|
+
#onTouchMove;
|
|
3503
|
+
#onTouchEnd;
|
|
3504
|
+
onResizeTouchStart(event) {
|
|
3505
|
+
const touch = event.touches[0];
|
|
3506
|
+
if (!touch || event.touches.length > 1)
|
|
3507
|
+
return;
|
|
3508
|
+
this.#startY = touch.clientY;
|
|
3509
|
+
this.#startHeight = parseInt(this.terminalHeight(), 10);
|
|
3510
|
+
this.isDragging.set(true);
|
|
3511
|
+
document.addEventListener('touchmove', this.#onTouchMove);
|
|
3512
|
+
document.addEventListener('touchend', this.#onTouchEnd);
|
|
3513
|
+
document.addEventListener('touchcancel', this.#onTouchEnd);
|
|
3514
|
+
}
|
|
3284
3515
|
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.2", ngImport: i0, type: Shell, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
|
|
3285
|
-
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.1.2", type: Shell, isStandalone: true, selector: "shell-root", ngImport: i0, template: "<div class=\"shell\" [style.--terminal-height]=\"terminalHeight()\" [class.is-dragging]=\"isDragging()\" [class.terminal-hidden]=\"!
|
|
3516
|
+
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.1.2", type: Shell, isStandalone: true, selector: "shell-root", ngImport: i0, template: "<div class=\"shell\" [style.--terminal-height]=\"terminalHeight()\" [class.is-dragging]=\"isDragging()\" [class.terminal-hidden]=\"!consoleState().show\">\n <shell-status-bar class=\"shell-status\" [class.disabled-zone]=\"isThinking()\">\n <ng-content />\n </shell-status-bar>\n @if (isAuthenticated() && !isPhone()) {\n <shell-side-menu class=\"shell-menu\" [class.disabled-zone]=\"isThinking()\" />\n }\n <shell-main-content class=\"shell-main\" [class.full-width]=\"!isAuthenticated()\" />\n @if (consoleState().show) {\n <!-- Never on a phone (scanner-phone-display E11): see Shell.consoleState. -->\n <div class=\"shell-resize\" [class.dragging]=\"isDragging()\" (mousedown)=\"onResizeStart($event)\" (touchstart)=\"onResizeTouchStart($event)\"></div>\n <shell-command-terminal class=\"shell-terminal\" [class.disabled-zone]=\"isThinking()\" [focusOnAppear]=\"consoleState().focusOnAppear\" />\n }\n @if (isAuthenticated() && phoneMenuOpen()) {\n <!-- Phone menu (scanner-phone-display D9): the icon column's items, opened from the \u2630 button. -->\n <div class=\"phone-menu-backdrop\" (click)=\"closePhoneMenu()\"></div>\n <shell-side-menu\n id=\"shell-phone-menu\"\n class=\"phone-menu\"\n [class.disabled-zone]=\"isThinking()\"\n [drawer]=\"true\"\n role=\"dialog\"\n aria-modal=\"true\"\n aria-label=\"Menu\"\n cdkTrapFocus\n [cdkTrapFocusAutoCapture]=\"true\"\n (keydown.escape)=\"closePhoneMenu()\"\n />\n }\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;height:100dvh;width:100vw;overflow:hidden;background-color:var(--color-surface-dark)}.shell.terminal-hidden{grid-template-rows:48px 1fr 0 0}.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;touch-action:none;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}@media(max-width:819.98px){.shell{grid-template-areas:\"status\" \"main\" \"resize\" \"terminal\";grid-template-columns:minmax(0,1fr)}.shell-menu{display:none}}.phone-menu-backdrop{position:absolute;inset:48px 0 0;z-index:50;background-color:color-mix(in srgb,var(--color-surface-dark) 70%,transparent)}.phone-menu{position:absolute;top:48px;bottom:0;left:0;z-index:51;display:block;width:min(280px,85vw);box-shadow:4px 0 16px color-mix(in srgb,var(--color-surface-dark) 60%,transparent)}\n"], dependencies: [{ kind: "component", type: StatusBar, selector: "shell-status-bar" }, { kind: "component", type: SideMenu, selector: "shell-side-menu", inputs: ["drawer"] }, { kind: "component", type: MainContent, selector: "shell-main-content" }, { kind: "component", type: CommandTerminal, selector: "shell-command-terminal", inputs: ["focusOnAppear"] }, { kind: "component", type: FaqModal, selector: "faq-modal" }, { kind: "directive", type: CdkTrapFocus, selector: "[cdkTrapFocus]", inputs: ["cdkTrapFocus", "cdkTrapFocusAutoCapture"], exportAs: ["cdkTrapFocus"] }], changeDetection: i0.ChangeDetectionStrategy.Eager }); }
|
|
3286
3517
|
}
|
|
3287
3518
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.2", ngImport: i0, type: Shell, decorators: [{
|
|
3288
3519
|
type: Component,
|
|
3289
|
-
args: [{ selector: 'shell-root', imports: [StatusBar, SideMenu, MainContent, CommandTerminal, FaqModal], changeDetection: ChangeDetectionStrategy.Eager, template: "<div class=\"shell\" [style.--terminal-height]=\"terminalHeight()\" [class.is-dragging]=\"isDragging()\" [class.terminal-hidden]=\"!
|
|
3520
|
+
args: [{ selector: 'shell-root', imports: [StatusBar, SideMenu, MainContent, CommandTerminal, FaqModal, CdkTrapFocus], changeDetection: ChangeDetectionStrategy.Eager, template: "<div class=\"shell\" [style.--terminal-height]=\"terminalHeight()\" [class.is-dragging]=\"isDragging()\" [class.terminal-hidden]=\"!consoleState().show\">\n <shell-status-bar class=\"shell-status\" [class.disabled-zone]=\"isThinking()\">\n <ng-content />\n </shell-status-bar>\n @if (isAuthenticated() && !isPhone()) {\n <shell-side-menu class=\"shell-menu\" [class.disabled-zone]=\"isThinking()\" />\n }\n <shell-main-content class=\"shell-main\" [class.full-width]=\"!isAuthenticated()\" />\n @if (consoleState().show) {\n <!-- Never on a phone (scanner-phone-display E11): see Shell.consoleState. -->\n <div class=\"shell-resize\" [class.dragging]=\"isDragging()\" (mousedown)=\"onResizeStart($event)\" (touchstart)=\"onResizeTouchStart($event)\"></div>\n <shell-command-terminal class=\"shell-terminal\" [class.disabled-zone]=\"isThinking()\" [focusOnAppear]=\"consoleState().focusOnAppear\" />\n }\n @if (isAuthenticated() && phoneMenuOpen()) {\n <!-- Phone menu (scanner-phone-display D9): the icon column's items, opened from the \u2630 button. -->\n <div class=\"phone-menu-backdrop\" (click)=\"closePhoneMenu()\"></div>\n <shell-side-menu\n id=\"shell-phone-menu\"\n class=\"phone-menu\"\n [class.disabled-zone]=\"isThinking()\"\n [drawer]=\"true\"\n role=\"dialog\"\n aria-modal=\"true\"\n aria-label=\"Menu\"\n cdkTrapFocus\n [cdkTrapFocusAutoCapture]=\"true\"\n (keydown.escape)=\"closePhoneMenu()\"\n />\n }\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;height:100dvh;width:100vw;overflow:hidden;background-color:var(--color-surface-dark)}.shell.terminal-hidden{grid-template-rows:48px 1fr 0 0}.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;touch-action:none;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}@media(max-width:819.98px){.shell{grid-template-areas:\"status\" \"main\" \"resize\" \"terminal\";grid-template-columns:minmax(0,1fr)}.shell-menu{display:none}}.phone-menu-backdrop{position:absolute;inset:48px 0 0;z-index:50;background-color:color-mix(in srgb,var(--color-surface-dark) 70%,transparent)}.phone-menu{position:absolute;top:48px;bottom:0;left:0;z-index:51;display:block;width:min(280px,85vw);box-shadow:4px 0 16px color-mix(in srgb,var(--color-surface-dark) 60%,transparent)}\n"] }]
|
|
3290
3521
|
}] });
|
|
3291
3522
|
|
|
3292
3523
|
class Login {
|
|
@@ -3511,5 +3742,5 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.2", ngImpor
|
|
|
3511
3742
|
* Generated bundle index. Do not edit.
|
|
3512
3743
|
*/
|
|
3513
3744
|
|
|
3514
|
-
export { AuthenticationService, CLEAR_USAGE, CommandRegistryService, CommandService, CommandTerminal, ConfirmDialog, FAQ_CONTENT_LOADER, FAQ_DEV_EDITOR, FAQ_DEV_MODE, FAQ_DEV_WORKSPACE_ROOT, FAQ_PUBLIC_PUBLISHER, FAQ_PUBLIC_SITE_BASE_URL, FaqAuthoringPanel, FaqHistoryService, FaqMarkdownRendererService, FaqModal, FaqService, HELP_USAGE, HttpFaqContentLoader, KeyboardShortcutService, Login, MainContent, PublicFaqIndex, PublicFaqPage, PublicFaqToc, RegexValidatorDirective, SESSION_TOKEN_KEY, SHELL_APP_NAME, SHELL_AUTH_URLS, SHELL_COMMANDS, SHELL_MENU_ITEMS, SHELL_POST_LOGIN_ROUTE, SHELL_SHARED_SESSION, SessionTokenStore, Shell, SideMenu, StatusBar, TerminalFocusService, TerminalVisibilityService, ThemeService, ThinkingService, audienceAllowed, authGuard, authInterceptor, buildFallbackChain, decideRenewal, guestGuard, matchesRoute, provideShellConfig, tokenize };
|
|
3745
|
+
export { AuthenticationService, CLEAR_USAGE, CommandRegistryService, CommandService, CommandTerminal, ConfirmDialog, FAQ_CONTENT_LOADER, FAQ_DEV_EDITOR, FAQ_DEV_MODE, FAQ_DEV_WORKSPACE_ROOT, FAQ_PUBLIC_PUBLISHER, FAQ_PUBLIC_SITE_BASE_URL, FaqAuthoringPanel, FaqHistoryService, FaqMarkdownRendererService, FaqModal, FaqService, HELP_USAGE, HttpFaqContentLoader, KeyboardShortcutService, Login, MainContent, PhoneLayoutService, PublicFaqIndex, PublicFaqPage, PublicFaqToc, RegexValidatorDirective, SESSION_TOKEN_KEY, SHELL_APP_NAME, SHELL_AUTH_URLS, SHELL_COMMANDS, SHELL_MENU_ITEMS, SHELL_PHONE_MEDIA_QUERY, SHELL_POST_LOGIN_ROUTE, SHELL_SHARED_SESSION, SessionTokenStore, Shell, SideMenu, StatusBar, TerminalFocusService, TerminalVisibilityService, ThemeService, ThinkingService, audienceAllowed, authGuard, authInterceptor, buildFallbackChain, decideRenewal, guestGuard, matchesRoute, provideShellConfig, tokenize };
|
|
3515
3746
|
//# sourceMappingURL=franzzemen-brokenstock-shell.mjs.map
|