@franzzemen/brokenstock-shell 0.8.0 → 0.8.2
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.
|
@@ -2261,12 +2261,94 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.2", ngImpor
|
|
|
2261
2261
|
args: [{ providedIn: 'root' }]
|
|
2262
2262
|
}] });
|
|
2263
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 D10/E11). The console is shown
|
|
2336
|
+
* below 820px, but nothing may put the cursor in it there: focusing a text box
|
|
2337
|
+
* on a phone raises the on-screen keyboard over the screen the user just opened.
|
|
2338
|
+
* The one place every focus request passes through is here, so this is where it
|
|
2339
|
+
* is refused. Refusing it at the source also means a request made on a phone is
|
|
2340
|
+
* not left counted up, to be acted on after a rotation to a wider screen.
|
|
2341
|
+
*/
|
|
2264
2342
|
class TerminalFocusService {
|
|
2265
2343
|
constructor() {
|
|
2344
|
+
this.#phoneLayout = inject(PhoneLayoutService);
|
|
2266
2345
|
this.focusRequested = signal(0, /* @ts-ignore */
|
|
2267
2346
|
...(ngDevMode ? [{ debugName: "focusRequested" }] : /* istanbul ignore next */ []));
|
|
2268
2347
|
}
|
|
2348
|
+
#phoneLayout;
|
|
2269
2349
|
requestFocus() {
|
|
2350
|
+
if (this.#phoneLayout.isPhone())
|
|
2351
|
+
return;
|
|
2270
2352
|
this.focusRequested.update(v => v + 1);
|
|
2271
2353
|
}
|
|
2272
2354
|
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.2", ngImport: i0, type: TerminalFocusService, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
|
|
@@ -2373,73 +2455,6 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.2", ngImpor
|
|
|
2373
2455
|
args: [{ providedIn: 'root' }]
|
|
2374
2456
|
}] });
|
|
2375
2457
|
|
|
2376
|
-
/*
|
|
2377
|
-
Created by Franz Zemen 2026-10-16
|
|
2378
|
-
License Type: UNLICENSED
|
|
2379
|
-
|
|
2380
|
-
Phone layout of the app frame (broken-stock scanner-phone-display.prd.md D9 / E8).
|
|
2381
|
-
*/
|
|
2382
|
-
/**
|
|
2383
|
-
* Below this width the frame is in its phone layout: no icon column, a ☰ menu
|
|
2384
|
-
* button in the top bar, a one-line top bar.
|
|
2385
|
-
*
|
|
2386
|
-
* The same 820px line broken-stock's screens use (scanner-filter-editor.prd.md
|
|
2387
|
-
* D11), written the same way. `819.98px` rather than `819px` so a fractional
|
|
2388
|
-
* width from browser zoom (819.5px) does not fall between the two layouts.
|
|
2389
|
-
*
|
|
2390
|
-
* ⚠ The CSS repeats this query in shell.css and status-bar.css —
|
|
2391
|
-
* a media query cannot read a TypeScript constant. phone-layout.service.spec.ts
|
|
2392
|
-
* fails if any of them drifts from this value.
|
|
2393
|
-
*/
|
|
2394
|
-
const SHELL_PHONE_MEDIA_QUERY = '(max-width: 819.98px)';
|
|
2395
|
-
/**
|
|
2396
|
-
* Whether the frame is in its phone layout, and whether the phone menu is open.
|
|
2397
|
-
*
|
|
2398
|
-
* Desktop never sees any of this: `isPhone` is false there, the menu cannot open,
|
|
2399
|
-
* and the rail renders exactly as before.
|
|
2400
|
-
*/
|
|
2401
|
-
class PhoneLayoutService {
|
|
2402
|
-
#window;
|
|
2403
|
-
#isPhone;
|
|
2404
|
-
#menuOpen;
|
|
2405
|
-
constructor() {
|
|
2406
|
-
this.#window = inject(DOCUMENT).defaultView;
|
|
2407
|
-
this.#isPhone = signal(false, /* @ts-ignore */
|
|
2408
|
-
...(ngDevMode ? [{ debugName: "#isPhone" }] : /* istanbul ignore next */ []));
|
|
2409
|
-
this.#menuOpen = signal(false, /* @ts-ignore */
|
|
2410
|
-
...(ngDevMode ? [{ debugName: "#menuOpen" }] : /* istanbul ignore next */ []));
|
|
2411
|
-
/** True below 820px. Always false where there is no window (server rendering). */
|
|
2412
|
-
this.isPhone = this.#isPhone.asReadonly();
|
|
2413
|
-
/** The ☰ menu's open state. Only ever true while `isPhone` is true. */
|
|
2414
|
-
this.menuOpen = this.#menuOpen.asReadonly();
|
|
2415
|
-
const mql = this.#window?.matchMedia?.(SHELL_PHONE_MEDIA_QUERY);
|
|
2416
|
-
if (!mql)
|
|
2417
|
-
return;
|
|
2418
|
-
this.#isPhone.set(mql.matches);
|
|
2419
|
-
const onChange = (event) => {
|
|
2420
|
-
this.#isPhone.set(event.matches);
|
|
2421
|
-
// Rotating or widening past 820px brings the rail back; a menu left open
|
|
2422
|
-
// would then cover the screen with nothing on it to close it.
|
|
2423
|
-
if (!event.matches)
|
|
2424
|
-
this.#menuOpen.set(false);
|
|
2425
|
-
};
|
|
2426
|
-
mql.addEventListener('change', onChange);
|
|
2427
|
-
inject(DestroyRef).onDestroy(() => mql.removeEventListener('change', onChange));
|
|
2428
|
-
}
|
|
2429
|
-
toggleMenu() {
|
|
2430
|
-
this.#menuOpen.set(this.#isPhone() && !this.#menuOpen());
|
|
2431
|
-
}
|
|
2432
|
-
closeMenu() {
|
|
2433
|
-
this.#menuOpen.set(false);
|
|
2434
|
-
}
|
|
2435
|
-
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.2", ngImport: i0, type: PhoneLayoutService, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
|
|
2436
|
-
static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "22.1.2", ngImport: i0, type: PhoneLayoutService, providedIn: 'root' }); }
|
|
2437
|
-
}
|
|
2438
|
-
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.2", ngImport: i0, type: PhoneLayoutService, decorators: [{
|
|
2439
|
-
type: Injectable,
|
|
2440
|
-
args: [{ providedIn: 'root' }]
|
|
2441
|
-
}], ctorParameters: () => [] });
|
|
2442
|
-
|
|
2443
2458
|
/**
|
|
2444
2459
|
* Elements that mean "the user is typing", where an unmodified shortcut must not fire.
|
|
2445
2460
|
*
|
|
@@ -3147,9 +3162,11 @@ class SideMenu {
|
|
|
3147
3162
|
* ⚠ The keyboard-shortcuts button (Alt+?) is left out here. It opens a list of
|
|
3148
3163
|
* Alt-key bindings, and a phone has no Alt key. The shortcuts themselves are
|
|
3149
3164
|
* untouched — they are document-level listeners and work from a hardware
|
|
3150
|
-
* keyboard whatever the layout.
|
|
3151
|
-
*
|
|
3152
|
-
*
|
|
3165
|
+
* keyboard whatever the layout.
|
|
3166
|
+
*
|
|
3167
|
+
* The console show/hide button stays (Franz, 2026-10-17: the console is back
|
|
3168
|
+
* on phones, reversing 0.8.1). Turning the console ON from here does not put
|
|
3169
|
+
* the cursor in the command box on a phone — see CommandTerminal.focusOnAppear.
|
|
3153
3170
|
*/
|
|
3154
3171
|
this.drawer = input(false, /* @ts-ignore */
|
|
3155
3172
|
...(ngDevMode ? [{ debugName: "drawer" }] : /* istanbul ignore next */ []));
|
|
@@ -3193,15 +3210,17 @@ class SideMenu {
|
|
|
3193
3210
|
}
|
|
3194
3211
|
toggleTerminal() {
|
|
3195
3212
|
this.#terminalVisibility.toggle();
|
|
3213
|
+
// ⚠ No terminal focus here either: on a phone, showing the console is not a
|
|
3214
|
+
// request to type. The menu closes so the user sees the console appear.
|
|
3196
3215
|
if (this.drawer())
|
|
3197
3216
|
this.#phoneLayout.closeMenu();
|
|
3198
3217
|
}
|
|
3199
3218
|
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.2", ngImport: i0, type: SideMenu, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
|
|
3200
|
-
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 <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 [matTooltipDisabled]=\"drawer()\"\n [attr.aria-pressed]=\"terminalVisible()\"\n (click)=\"toggleTerminal()\"\n >\n <mat-icon>terminal</mat-icon>\n @if (drawer()) {\n <span class=\"menu-label\">{{ terminalVisible() ? 'Hide command console' : 'Show command console' }}</span>\n }\n </button>\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}.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.terminal-toggle{padding:0 12px;margin-bottom:0;font:inherit;color:var(--color-secondary-info)}.side-menu.drawer .menu-item.terminal-toggle.active{color:var(--color-interactive)}.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 }); }
|
|
3219
|
+
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 <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 [matTooltipDisabled]=\"drawer()\"\n [attr.aria-pressed]=\"terminalVisible()\"\n (click)=\"toggleTerminal()\"\n >\n <mat-icon>terminal</mat-icon>\n @if (drawer()) {\n <span class=\"menu-label\">{{ terminalVisible() ? 'Hide command console' : 'Show command console' }}</span>\n }\n </button>\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.terminal-toggle{padding:0 12px;margin-bottom:0;font:inherit;color:var(--color-secondary-info)}.side-menu.drawer .menu-item.terminal-toggle.active{color:var(--color-interactive)}.side-menu.drawer .menu-item.terminal-toggle.active:hover{background-color:var(--color-interactive);color:var(--color-surface-dark)}.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 }); }
|
|
3201
3220
|
}
|
|
3202
3221
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.2", ngImport: i0, type: SideMenu, decorators: [{
|
|
3203
3222
|
type: Component,
|
|
3204
|
-
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 <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 [matTooltipDisabled]=\"drawer()\"\n [attr.aria-pressed]=\"terminalVisible()\"\n (click)=\"toggleTerminal()\"\n >\n <mat-icon>terminal</mat-icon>\n @if (drawer()) {\n <span class=\"menu-label\">{{ terminalVisible() ? 'Hide command console' : 'Show command console' }}</span>\n }\n </button>\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}.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.terminal-toggle{padding:0 12px;margin-bottom:0;font:inherit;color:var(--color-secondary-info)}.side-menu.drawer .menu-item.terminal-toggle.active{color:var(--color-interactive)}.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
|
+
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 <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 [matTooltipDisabled]=\"drawer()\"\n [attr.aria-pressed]=\"terminalVisible()\"\n (click)=\"toggleTerminal()\"\n >\n <mat-icon>terminal</mat-icon>\n @if (drawer()) {\n <span class=\"menu-label\">{{ terminalVisible() ? 'Hide command console' : 'Show command console' }}</span>\n }\n </button>\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.terminal-toggle{padding:0 12px;margin-bottom:0;font:inherit;color:var(--color-secondary-info)}.side-menu.drawer .menu-item.terminal-toggle.active{color:var(--color-interactive)}.side-menu.drawer .menu-item.terminal-toggle.active:hover{background-color:var(--color-interactive);color:var(--color-surface-dark)}.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"] }]
|
|
3205
3224
|
}], propDecorators: { drawer: [{ type: i0.Input, args: [{ isSignal: true, alias: "drawer", required: false }] }] } });
|
|
3206
3225
|
|
|
3207
3226
|
class MainContent {
|
|
@@ -3233,11 +3252,24 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.2", ngImpor
|
|
|
3233
3252
|
}] });
|
|
3234
3253
|
|
|
3235
3254
|
class CommandTerminal {
|
|
3255
|
+
#phoneLayout;
|
|
3236
3256
|
constructor() {
|
|
3237
3257
|
this.commandService = inject(CommandService);
|
|
3238
3258
|
this.registry = inject(CommandRegistryService);
|
|
3239
3259
|
this.authService = inject(AuthenticationService);
|
|
3240
3260
|
this.terminalFocusService = inject(TerminalFocusService);
|
|
3261
|
+
this.#phoneLayout = inject(PhoneLayoutService);
|
|
3262
|
+
/**
|
|
3263
|
+
* Whether the command box takes the cursor when the console first appears.
|
|
3264
|
+
*
|
|
3265
|
+
* True for a standalone console. The shell passes false on a phone (below
|
|
3266
|
+
* 820px), including when the user turns the console on from the ☰ menu:
|
|
3267
|
+
* showing the console is not a request to type, and focusing the box would
|
|
3268
|
+
* raise the on-screen keyboard over it (scanner-phone-display D10). On a
|
|
3269
|
+
* phone, focusInput() refuses anyway; this keeps the intent explicit.
|
|
3270
|
+
*/
|
|
3271
|
+
this.focusOnAppear = input(true, /* @ts-ignore */
|
|
3272
|
+
...(ngDevMode ? [{ debugName: "focusOnAppear" }] : /* istanbul ignore next */ []));
|
|
3241
3273
|
this.history = computed(() => [...this.commandService.history()].reverse(), /* @ts-ignore */
|
|
3242
3274
|
...(ngDevMode ? [{ debugName: "history" }] : /* istanbul ignore next */ []));
|
|
3243
3275
|
this.currentInput = this.commandService.currentInput;
|
|
@@ -3255,14 +3287,32 @@ class CommandTerminal {
|
|
|
3255
3287
|
return suggestions.length > 0 ? suggestions[0] : '';
|
|
3256
3288
|
}, /* @ts-ignore */
|
|
3257
3289
|
...(ngDevMode ? [{ debugName: "ghostText" }] : /* istanbul ignore next */ []));
|
|
3258
|
-
afterNextRender(() =>
|
|
3290
|
+
afterNextRender(() => {
|
|
3291
|
+
if (this.focusOnAppear())
|
|
3292
|
+
this.focusInput();
|
|
3293
|
+
});
|
|
3294
|
+
// Both effects below skip their first run. An effect runs once when it is
|
|
3295
|
+
// created, so without the skip each would focus the box on appearance a
|
|
3296
|
+
// second and third time — after `focusOnAppear` had said not to. Appearance
|
|
3297
|
+
// is the afterNextRender above's job alone; these react to CHANGES.
|
|
3298
|
+
let firstProcessingRun = true;
|
|
3259
3299
|
effect(() => {
|
|
3260
|
-
|
|
3300
|
+
const busy = this.isProcessing();
|
|
3301
|
+
if (firstProcessingRun) {
|
|
3302
|
+
firstProcessingRun = false;
|
|
3303
|
+
return;
|
|
3304
|
+
}
|
|
3305
|
+
if (!busy) {
|
|
3261
3306
|
setTimeout(() => this.focusInput());
|
|
3262
3307
|
}
|
|
3263
3308
|
});
|
|
3309
|
+
let firstRequestRun = true;
|
|
3264
3310
|
effect(() => {
|
|
3265
3311
|
this.terminalFocusService.focusRequested();
|
|
3312
|
+
if (firstRequestRun) {
|
|
3313
|
+
firstRequestRun = false;
|
|
3314
|
+
return;
|
|
3315
|
+
}
|
|
3266
3316
|
setTimeout(() => this.focusInput());
|
|
3267
3317
|
});
|
|
3268
3318
|
}
|
|
@@ -3322,16 +3372,25 @@ class CommandTerminal {
|
|
|
3322
3372
|
return 'error';
|
|
3323
3373
|
return '';
|
|
3324
3374
|
}
|
|
3375
|
+
/**
|
|
3376
|
+
* ⚠ A no-op on a phone (scanner-phone-display D10/E11). The console is shown
|
|
3377
|
+
* there, but only the user tapping the box may give it the cursor; every
|
|
3378
|
+
* programmatic path (first appearance, a finished command, a focus request,
|
|
3379
|
+
* an app calling this) ends here and is refused, so nothing on a phone can
|
|
3380
|
+
* raise the on-screen keyboard unasked.
|
|
3381
|
+
*/
|
|
3325
3382
|
focusInput() {
|
|
3383
|
+
if (this.#phoneLayout.isPhone())
|
|
3384
|
+
return;
|
|
3326
3385
|
this.inputEl()?.nativeElement.focus();
|
|
3327
3386
|
}
|
|
3328
3387
|
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.2", ngImport: i0, type: CommandTerminal, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
|
|
3329
|
-
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 }); }
|
|
3388
|
+
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 }); }
|
|
3330
3389
|
}
|
|
3331
3390
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.2", ngImport: i0, type: CommandTerminal, decorators: [{
|
|
3332
3391
|
type: Component,
|
|
3333
3392
|
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"] }]
|
|
3334
|
-
}], ctorParameters: () => [], propDecorators: { inputEl: [{ type: i0.ViewChild, args: ['terminalInput', { isSignal: true }] }] } });
|
|
3393
|
+
}], ctorParameters: () => [], propDecorators: { focusOnAppear: [{ type: i0.Input, args: [{ isSignal: true, alias: "focusOnAppear", required: false }] }], inputEl: [{ type: i0.ViewChild, args: ['terminalInput', { isSignal: true }] }] } });
|
|
3335
3394
|
|
|
3336
3395
|
const MIN_TERMINAL_HEIGHT = 80;
|
|
3337
3396
|
const MAX_TERMINAL_RATIO = 0.6;
|
|
@@ -3350,23 +3409,50 @@ class Shell {
|
|
|
3350
3409
|
/** Below 820px the icon column is replaced by the ☰ menu (scanner-phone-display D9). */
|
|
3351
3410
|
this.isPhone = this.#phoneLayout.isPhone;
|
|
3352
3411
|
this.phoneMenuOpen = this.#phoneLayout.menuOpen;
|
|
3412
|
+
/**
|
|
3413
|
+
* Whether the command box takes the cursor when the console appears: on a
|
|
3414
|
+
* desktop yes, on a phone never (scanner-phone-display D10, Franz 2026-10-17).
|
|
3415
|
+
*
|
|
3416
|
+
* The console is shown on a phone again (reversing 0.8.1), but nothing may
|
|
3417
|
+
* put the cursor in its box there — the on-screen keyboard would cover the
|
|
3418
|
+
* screen. That includes the user turning the console ON from the ☰ menu: a
|
|
3419
|
+
* show is not a request to type, and the keyboard rising over the console the
|
|
3420
|
+
* moment it appears would hide the output the user wanted to see. Tapping the
|
|
3421
|
+
* box is the only way it gets the cursor on a phone.
|
|
3422
|
+
*
|
|
3423
|
+
* Rotating across 820px never mounts or unmounts the console (its visibility
|
|
3424
|
+
* is the user's choice alone), so this is only read when the user turns it on
|
|
3425
|
+
* or at start-up. CommandTerminal.focusInput also refuses on a phone, as a
|
|
3426
|
+
* second guard.
|
|
3427
|
+
*/
|
|
3428
|
+
this.consoleFocusOnAppear = computed(() => !this.isPhone(), /* @ts-ignore */
|
|
3429
|
+
...(ngDevMode ? [{ debugName: "consoleFocusOnAppear" }] : /* istanbul ignore next */ []));
|
|
3353
3430
|
this.terminalHeight = signal(`${DEFAULT_TERMINAL_HEIGHT}px`, /* @ts-ignore */
|
|
3354
3431
|
...(ngDevMode ? [{ debugName: "terminalHeight" }] : /* istanbul ignore next */ []));
|
|
3355
3432
|
this.isDragging = signal(false, /* @ts-ignore */
|
|
3356
3433
|
...(ngDevMode ? [{ debugName: "isDragging" }] : /* istanbul ignore next */ []));
|
|
3357
3434
|
this.#startY = 0;
|
|
3358
3435
|
this.#startHeight = DEFAULT_TERMINAL_HEIGHT;
|
|
3359
|
-
this.#onMouseMove = (event) =>
|
|
3360
|
-
const deltaY = event.clientY - this.#startY;
|
|
3361
|
-
const maxHeight = Math.floor(window.innerHeight * MAX_TERMINAL_RATIO);
|
|
3362
|
-
const newHeight = Math.min(maxHeight, Math.max(MIN_TERMINAL_HEIGHT, this.#startHeight - deltaY));
|
|
3363
|
-
this.terminalHeight.set(`${newHeight}px`);
|
|
3364
|
-
};
|
|
3436
|
+
this.#onMouseMove = (event) => this.#resizeTo(event.clientY);
|
|
3365
3437
|
this.#onMouseUp = () => {
|
|
3366
3438
|
this.isDragging.set(false);
|
|
3367
3439
|
document.removeEventListener('mousemove', this.#onMouseMove);
|
|
3368
3440
|
document.removeEventListener('mouseup', this.#onMouseUp);
|
|
3369
3441
|
};
|
|
3442
|
+
// Touch drag for the resize bar (scanner-phone-display E12): tablets and,
|
|
3443
|
+
// since the console is back there (D10, 2026-10-17), phones. Kept separate from the mouse path so the desktop's behaviour is unchanged;
|
|
3444
|
+
// the bar's `touch-action: none` stops the page scrolling under the finger.
|
|
3445
|
+
this.#onTouchMove = (event) => {
|
|
3446
|
+
const touch = event.touches[0];
|
|
3447
|
+
if (touch)
|
|
3448
|
+
this.#resizeTo(touch.clientY);
|
|
3449
|
+
};
|
|
3450
|
+
this.#onTouchEnd = () => {
|
|
3451
|
+
this.isDragging.set(false);
|
|
3452
|
+
document.removeEventListener('touchmove', this.#onTouchMove);
|
|
3453
|
+
document.removeEventListener('touchend', this.#onTouchEnd);
|
|
3454
|
+
document.removeEventListener('touchcancel', this.#onTouchEnd);
|
|
3455
|
+
};
|
|
3370
3456
|
}
|
|
3371
3457
|
#thinkingService;
|
|
3372
3458
|
#commandService;
|
|
@@ -3381,6 +3467,12 @@ class Shell {
|
|
|
3381
3467
|
}
|
|
3382
3468
|
#startY;
|
|
3383
3469
|
#startHeight;
|
|
3470
|
+
#resizeTo(clientY) {
|
|
3471
|
+
const deltaY = clientY - this.#startY;
|
|
3472
|
+
const maxHeight = Math.floor(window.innerHeight * MAX_TERMINAL_RATIO);
|
|
3473
|
+
const newHeight = Math.min(maxHeight, Math.max(MIN_TERMINAL_HEIGHT, this.#startHeight - deltaY));
|
|
3474
|
+
this.terminalHeight.set(`${newHeight}px`);
|
|
3475
|
+
}
|
|
3384
3476
|
#onMouseMove;
|
|
3385
3477
|
#onMouseUp;
|
|
3386
3478
|
onResizeStart(event) {
|
|
@@ -3391,12 +3483,28 @@ class Shell {
|
|
|
3391
3483
|
document.addEventListener('mousemove', this.#onMouseMove);
|
|
3392
3484
|
document.addEventListener('mouseup', this.#onMouseUp);
|
|
3393
3485
|
}
|
|
3486
|
+
// Touch drag for the resize bar (scanner-phone-display E12): tablets and,
|
|
3487
|
+
// since the console is back there (D10, 2026-10-17), phones. Kept separate from the mouse path so the desktop's behaviour is unchanged;
|
|
3488
|
+
// the bar's `touch-action: none` stops the page scrolling under the finger.
|
|
3489
|
+
#onTouchMove;
|
|
3490
|
+
#onTouchEnd;
|
|
3491
|
+
onResizeTouchStart(event) {
|
|
3492
|
+
const touch = event.touches[0];
|
|
3493
|
+
if (!touch || event.touches.length > 1)
|
|
3494
|
+
return;
|
|
3495
|
+
this.#startY = touch.clientY;
|
|
3496
|
+
this.#startHeight = parseInt(this.terminalHeight(), 10);
|
|
3497
|
+
this.isDragging.set(true);
|
|
3498
|
+
document.addEventListener('touchmove', this.#onTouchMove);
|
|
3499
|
+
document.addEventListener('touchend', this.#onTouchEnd);
|
|
3500
|
+
document.addEventListener('touchcancel', this.#onTouchEnd);
|
|
3501
|
+
}
|
|
3394
3502
|
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.2", ngImport: i0, type: Shell, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
|
|
3395
|
-
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]=\"!terminalVisible()\">\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 (terminalVisible()) {\n <div class=\"shell-resize\" [class.dragging]=\"isDragging()\" (mousedown)=\"onResizeStart($event)\"></div>\n <shell-command-terminal class=\"shell-terminal\" [class.disabled-zone]=\"isThinking()\" />\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;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" }, { kind: "component", type: FaqModal, selector: "faq-modal" }, { kind: "directive", type: CdkTrapFocus, selector: "[cdkTrapFocus]", inputs: ["cdkTrapFocus", "cdkTrapFocusAutoCapture"], exportAs: ["cdkTrapFocus"] }], changeDetection: i0.ChangeDetectionStrategy.Eager }); }
|
|
3503
|
+
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]=\"!terminalVisible()\">\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 (terminalVisible()) {\n <!-- On a phone too (scanner-phone-display D10), but never given the cursor there: see Shell.consoleFocusOnAppear. -->\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]=\"consoleFocusOnAppear()\" />\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 }); }
|
|
3396
3504
|
}
|
|
3397
3505
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.2", ngImport: i0, type: Shell, decorators: [{
|
|
3398
3506
|
type: Component,
|
|
3399
|
-
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]=\"!terminalVisible()\">\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 (terminalVisible()) {\n <div class=\"shell-resize\" [class.dragging]=\"isDragging()\" (mousedown)=\"onResizeStart($event)\"></div>\n <shell-command-terminal class=\"shell-terminal\" [class.disabled-zone]=\"isThinking()\" />\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;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"] }]
|
|
3507
|
+
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]=\"!terminalVisible()\">\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 (terminalVisible()) {\n <!-- On a phone too (scanner-phone-display D10), but never given the cursor there: see Shell.consoleFocusOnAppear. -->\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]=\"consoleFocusOnAppear()\" />\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"] }]
|
|
3400
3508
|
}] });
|
|
3401
3509
|
|
|
3402
3510
|
class Login {
|