@franzzemen/brokenstock-shell 0.9.2 → 0.9.4
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.
package/README.md
CHANGED
|
@@ -15,5 +15,6 @@ Shared Angular shell library for Brokenstock web applications. Provides the foun
|
|
|
15
15
|
- **Injection token configuration:** `SHELL_APP_NAME`, `SHELL_AUTH_URLS`, `SHELL_MENU_ITEMS`, `SHELL_COMMANDS`
|
|
16
16
|
- **Content projection:** App-specific status bar items projected through `<shell-root>` into the status bar
|
|
17
17
|
- **Command pattern:** `@Injectable` classes implementing `CommandDefinition`, registered via `SHELL_COMMANDS` token
|
|
18
|
+
- **Questions and screen keys:** `CommandService.ask` leaves a question open in the terminal (the next line answers it, Esc cancels); `ScreenKeysService` gives a screen keys of its own on its routes (Ctrl allowed, live in the terminal) — see the guide
|
|
18
19
|
- **Signal-based state:** All services use Angular signals; no RxJS in public APIs
|
|
19
20
|
- **Peer dependencies:** Angular 21, Material, CDK, @franzzemen/identity, @franzzemen/utility, zod, rxjs
|
|
@@ -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, DestroyRef, NgZone, viewChild, afterNextRender, output } from '@angular/core';
|
|
2
|
+
import { InjectionToken, makeEnvironmentProviders, signal, computed, Injectable, inject, input, Directive, isDevMode, effect, untracked, HostListener, ViewChild, ViewEncapsulation, ChangeDetectionStrategy, Component, DestroyRef, NgZone, PLATFORM_ID, viewChild, afterNextRender, output } 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';
|
|
@@ -11,9 +11,9 @@ import { NG_VALIDATORS, FormsModule } from '@angular/forms';
|
|
|
11
11
|
import { Marked } from 'marked';
|
|
12
12
|
import { gfmHeadingId } from 'marked-gfm-heading-id';
|
|
13
13
|
import fm from 'front-matter';
|
|
14
|
-
import { NgClass, CommonModule, DOCUMENT, DatePipe } from '@angular/common';
|
|
14
|
+
import { NgClass, CommonModule, DOCUMENT, isPlatformBrowser, DatePipe } from '@angular/common';
|
|
15
15
|
import { Title, Meta } from '@angular/platform-browser';
|
|
16
|
-
import {
|
|
16
|
+
import { array, string } from 'zod/v4';
|
|
17
17
|
import * as i1 from '@angular/material/icon';
|
|
18
18
|
import { MatIconModule } from '@angular/material/icon';
|
|
19
19
|
import * as i2 from '@angular/material/tooltip';
|
|
@@ -2844,6 +2844,111 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.2", ngImpor
|
|
|
2844
2844
|
args: [{ providedIn: 'root' }]
|
|
2845
2845
|
}], ctorParameters: () => [] });
|
|
2846
2846
|
|
|
2847
|
+
/*
|
|
2848
|
+
Created by Franz Zemen 2026-10-18
|
|
2849
|
+
License Type: UNLICENSED
|
|
2850
|
+
|
|
2851
|
+
Keys a screen owns while it is open: trading hotkeys (live-trading-foundation.prd.md D24, E8;
|
|
2852
|
+
live-trading.prd.md D34).
|
|
2853
|
+
|
|
2854
|
+
`KeyboardShortcutService` cannot do this job, and is not changed to: it allows only Alt and Shift
|
|
2855
|
+
and switches itself off inside any input. Hotkeys need the opposite on both counts:
|
|
2856
|
+
|
|
2857
|
+
- **Ctrl is allowed** (the defaults are Shift+F1 and Ctrl+F1). Meta (the Windows/Command key) never
|
|
2858
|
+
matches.
|
|
2859
|
+
- **They work while the cursor is in the command terminal**, the one input that is not a place to
|
|
2860
|
+
type a number. They do NOT fire while focus is in any other input, textarea, select or editable
|
|
2861
|
+
element: a key there is typing.
|
|
2862
|
+
- **Matched on `event.code`**, the physical key, never the character produced.
|
|
2863
|
+
- **Held-down repeats are ignored** (`event.repeat`): one order per press.
|
|
2864
|
+
- **Only on the routes a registration names.**
|
|
2865
|
+
|
|
2866
|
+
When a handler takes a key, the browser's own action for it is prevented and it goes no further.
|
|
2867
|
+
*/
|
|
2868
|
+
/** The class the command terminal's input carries; keys typed there still reach a screen's keys. */
|
|
2869
|
+
const TERMINAL_INPUT_CLASS = 'terminal-input';
|
|
2870
|
+
/** True when the key is typed into a box other than the command terminal. */
|
|
2871
|
+
function isTypingElsewhere(target) {
|
|
2872
|
+
const el = target;
|
|
2873
|
+
if (!el || typeof el.tagName !== 'string')
|
|
2874
|
+
return false;
|
|
2875
|
+
if (el.classList?.contains(TERMINAL_INPUT_CLASS))
|
|
2876
|
+
return false;
|
|
2877
|
+
if (el.isContentEditable)
|
|
2878
|
+
return true;
|
|
2879
|
+
const tag = el.tagName.toUpperCase();
|
|
2880
|
+
if (tag === 'TEXTAREA' || tag === 'SELECT')
|
|
2881
|
+
return true;
|
|
2882
|
+
if (tag !== 'INPUT')
|
|
2883
|
+
return false;
|
|
2884
|
+
const type = (el.type || 'text').toLowerCase();
|
|
2885
|
+
return !['checkbox', 'radio', 'button', 'submit', 'reset', 'range', 'color', 'file', 'image'].includes(type);
|
|
2886
|
+
}
|
|
2887
|
+
/** Exactly this combination: the key, and these modifiers and no others. */
|
|
2888
|
+
function comboMatches(combo, e) {
|
|
2889
|
+
return e.code === combo.code && e.ctrlKey === combo.ctrl && e.shiftKey === combo.shift && e.altKey === combo.alt && !e.metaKey;
|
|
2890
|
+
}
|
|
2891
|
+
/** Whether a keydown may reach a screen's keys at all. */
|
|
2892
|
+
function isEligibleKey(e) {
|
|
2893
|
+
return !e.repeat && !e.isComposing && !isTypingElsewhere(e.target);
|
|
2894
|
+
}
|
|
2895
|
+
class ScreenKeysService {
|
|
2896
|
+
#document = inject(DOCUMENT);
|
|
2897
|
+
#zone = inject(NgZone);
|
|
2898
|
+
#router = inject(Router);
|
|
2899
|
+
#registrations = new Set();
|
|
2900
|
+
#listening = false;
|
|
2901
|
+
constructor() {
|
|
2902
|
+
if (!isPlatformBrowser(inject(PLATFORM_ID)))
|
|
2903
|
+
return;
|
|
2904
|
+
inject(DestroyRef).onDestroy(() => this.#document.removeEventListener('keydown', this.#onKey, true));
|
|
2905
|
+
}
|
|
2906
|
+
/** Registers keys for a screen; returns the function that removes them. */
|
|
2907
|
+
register(registration) {
|
|
2908
|
+
this.#registrations.add(registration);
|
|
2909
|
+
if (!this.#listening) {
|
|
2910
|
+
this.#listening = true;
|
|
2911
|
+
// Capture phase, so a screen's key is seen before a focused control can act on it.
|
|
2912
|
+
this.#zone.runOutsideAngular(() => this.#document.addEventListener('keydown', this.#onKey, true));
|
|
2913
|
+
}
|
|
2914
|
+
return () => this.#registrations.delete(registration);
|
|
2915
|
+
}
|
|
2916
|
+
#onKey = (e) => {
|
|
2917
|
+
if (this.#registrations.size === 0 || !isEligibleKey(e))
|
|
2918
|
+
return;
|
|
2919
|
+
const url = this.#router.url;
|
|
2920
|
+
for (const r of this.#registrations) {
|
|
2921
|
+
if (!matchesRoute(url, r.routes))
|
|
2922
|
+
continue;
|
|
2923
|
+
const taken = this.#zone.run(() => r.handle(e));
|
|
2924
|
+
if (taken) {
|
|
2925
|
+
e.preventDefault();
|
|
2926
|
+
e.stopPropagation();
|
|
2927
|
+
return;
|
|
2928
|
+
}
|
|
2929
|
+
}
|
|
2930
|
+
};
|
|
2931
|
+
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.2", ngImport: i0, type: ScreenKeysService, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
|
|
2932
|
+
static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "22.1.2", ngImport: i0, type: ScreenKeysService, providedIn: 'root' }); }
|
|
2933
|
+
}
|
|
2934
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.2", ngImport: i0, type: ScreenKeysService, decorators: [{
|
|
2935
|
+
type: Injectable,
|
|
2936
|
+
args: [{ providedIn: 'root' }]
|
|
2937
|
+
}], ctorParameters: () => [] });
|
|
2938
|
+
|
|
2939
|
+
/*
|
|
2940
|
+
* The only door to zod in the shell (projects review-defects-2026-10.prd.md E20, D25).
|
|
2941
|
+
*
|
|
2942
|
+
* Library files import `* as z` from HERE, never from 'zod', so call sites read as before
|
|
2943
|
+
* (`z.array(z.string())`) but the app's bundler sees a module re-exporting two functions and can
|
|
2944
|
+
* drop the rest. Importing `{z}` from 'zod/v4' hands it the whole library as one object instead:
|
|
2945
|
+
* 454 kB in the browser's first download in a measured test bundle, against 79 kB this way.
|
|
2946
|
+
*
|
|
2947
|
+
* A command needing another zod function adds it below; the compiler refuses anything not listed.
|
|
2948
|
+
* `zod-door.spec.ts` fails if any other library file imports zod directly. Specs are exempt: they
|
|
2949
|
+
* never reach a bundle.
|
|
2950
|
+
*/
|
|
2951
|
+
|
|
2847
2952
|
const FAQ_USAGE = 'faq\n' +
|
|
2848
2953
|
'faq <key>\n' +
|
|
2849
2954
|
'faq search <term>\n' +
|
|
@@ -2864,7 +2969,7 @@ class FaqCommand {
|
|
|
2864
2969
|
this.usage = FAQ_USAGE;
|
|
2865
2970
|
this.requiresAuth = false;
|
|
2866
2971
|
this.scope = 'global';
|
|
2867
|
-
this.argsSchema =
|
|
2972
|
+
this.argsSchema = array(string()).max(50);
|
|
2868
2973
|
}
|
|
2869
2974
|
execute(args) {
|
|
2870
2975
|
const ts = () => new Date();
|
|
@@ -3065,7 +3170,7 @@ class PendingPromptState {
|
|
|
3065
3170
|
}
|
|
3066
3171
|
}
|
|
3067
3172
|
|
|
3068
|
-
const CommandInputSchema =
|
|
3173
|
+
const CommandInputSchema = string().trim().min(1, 'Command cannot be empty');
|
|
3069
3174
|
class CommandService {
|
|
3070
3175
|
#prompts;
|
|
3071
3176
|
/**
|
|
@@ -4490,5 +4595,5 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.2", ngImpor
|
|
|
4490
4595
|
* Generated bundle index. Do not edit.
|
|
4491
4596
|
*/
|
|
4492
4597
|
|
|
4493
|
-
export { AuthenticationService, AuthenticatorError, AuthenticatorSettings, CLEAR_USAGE, CODE_STEP, CodeDialog, 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, PendingPromptState, PhoneLayoutService, PublicFaqIndex, PublicFaqPage, PublicFaqToc, RecoveryCodes, 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, TOO_MANY_ATTEMPTS, TerminalFocusService, TerminalVisibilityService, ThemeService, ThinkingService, answerEcho, audienceAllowed, authGuard, authInterceptor, authenticatorMessage, buildFallbackChain, buildLoginBody, decideRenewal, guestGuard, isYes, matchesRoute, openCodeDialog, provideShellConfig, recoveryCodesFile, tokenize };
|
|
4598
|
+
export { AuthenticationService, AuthenticatorError, AuthenticatorSettings, CLEAR_USAGE, CODE_STEP, CodeDialog, 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, PendingPromptState, PhoneLayoutService, PublicFaqIndex, PublicFaqPage, PublicFaqToc, RecoveryCodes, 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, ScreenKeysService, SessionTokenStore, Shell, SideMenu, StatusBar, TERMINAL_INPUT_CLASS, TOO_MANY_ATTEMPTS, TerminalFocusService, TerminalVisibilityService, ThemeService, ThinkingService, answerEcho, audienceAllowed, authGuard, authInterceptor, authenticatorMessage, buildFallbackChain, buildLoginBody, comboMatches, decideRenewal, guestGuard, isEligibleKey, isTypingElsewhere, isYes, matchesRoute, openCodeDialog, provideShellConfig, recoveryCodesFile, tokenize };
|
|
4494
4599
|
//# sourceMappingURL=franzzemen-brokenstock-shell.mjs.map
|