@franzzemen/brokenstock-shell 0.4.5 → 0.4.7
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.
|
@@ -2224,38 +2224,92 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.2", ngImpor
|
|
|
2224
2224
|
args: [{ providedIn: 'root' }]
|
|
2225
2225
|
}] });
|
|
2226
2226
|
|
|
2227
|
-
|
|
2227
|
+
/**
|
|
2228
|
+
* The choice a NEW window starts with. Shared by the whole browser.
|
|
2229
|
+
* Written on every toggle, read only at construction.
|
|
2230
|
+
*/
|
|
2231
|
+
const DEFAULT_KEY = 'shell.commandTerminalVisible';
|
|
2232
|
+
/**
|
|
2233
|
+
* THIS window's current choice. Per-window by definition, so two windows cannot
|
|
2234
|
+
* fight over it.
|
|
2235
|
+
*/
|
|
2236
|
+
const WINDOW_KEY = 'shell.commandTerminalVisible.window';
|
|
2228
2237
|
/**
|
|
2229
2238
|
* Controls visibility of the command terminal at the bottom of the shell.
|
|
2230
2239
|
*
|
|
2231
|
-
*
|
|
2232
|
-
*
|
|
2233
|
-
*
|
|
2240
|
+
* ⚠ TWO KEYS, AND THE SPLIT IS THE POINT (multiple-windows F6).
|
|
2241
|
+
*
|
|
2242
|
+
* This used to be one `localStorage` value, read once at construction and
|
|
2243
|
+
* written on every change, with nothing listening for another window. That was
|
|
2244
|
+
* correct while the app could only be open once. It is not now: two windows
|
|
2245
|
+
* would each write the shared value on every toggle, so whichever you touched
|
|
2246
|
+
* last decided what BOTH windows got on their next start — a setting that
|
|
2247
|
+
* quietly refuses to stay put, which reads as a bug in the shell rather than in
|
|
2248
|
+
* storage.
|
|
2249
|
+
*
|
|
2250
|
+
* The resolution is not "make them agree". You genuinely may want the terminal
|
|
2251
|
+
* up on the scanner window and out of the way on the other, so forcing them into
|
|
2252
|
+
* step would be the wrong fix as well as a harder one. Instead:
|
|
2253
|
+
*
|
|
2254
|
+
* - the LIVE state is per-window (`sessionStorage`), so the two never collide
|
|
2255
|
+
* and a reload still restores what this window had;
|
|
2256
|
+
* - the DEFAULT for a brand-new window is shared (`localStorage`), so your
|
|
2257
|
+
* last choice anywhere still carries into the next window and across a
|
|
2258
|
+
* browser restart, which is the behaviour the single key was providing.
|
|
2259
|
+
*
|
|
2260
|
+
* Both writes are best-effort: losing a remembered layout must never break the
|
|
2261
|
+
* screen trying to remember it.
|
|
2262
|
+
*
|
|
2263
|
+
* ⚠ `visible` is READ-ONLY to the outside and `toggle()` is the only mutator.
|
|
2264
|
+
* It used to be a writable signal persisted by an `effect`, which meant any
|
|
2265
|
+
* caller could `set()` it — and whether that got remembered depended on an
|
|
2266
|
+
* effect firing somewhere else. Persisting inside the one mutator makes it
|
|
2267
|
+
* impossible to change this without persisting it.
|
|
2234
2268
|
*/
|
|
2235
2269
|
class TerminalVisibilityService {
|
|
2236
2270
|
constructor() {
|
|
2237
|
-
this
|
|
2238
|
-
...(ngDevMode ? [{ debugName: "visible" }] : /* istanbul ignore next */ []));
|
|
2239
|
-
|
|
2240
|
-
|
|
2241
|
-
try {
|
|
2242
|
-
localStorage.setItem(STORAGE_KEY, isVisible ? '1' : '0');
|
|
2243
|
-
}
|
|
2244
|
-
catch {
|
|
2245
|
-
// localStorage unavailable (private mode / SSR) — non-fatal.
|
|
2246
|
-
}
|
|
2247
|
-
});
|
|
2271
|
+
this.#visible = signal(this.#readInitial(), /* @ts-ignore */
|
|
2272
|
+
...(ngDevMode ? [{ debugName: "#visible" }] : /* istanbul ignore next */ []));
|
|
2273
|
+
/** Read-only on purpose — see the header. */
|
|
2274
|
+
this.visible = this.#visible.asReadonly();
|
|
2248
2275
|
}
|
|
2276
|
+
#visible;
|
|
2249
2277
|
toggle() {
|
|
2250
|
-
this
|
|
2278
|
+
const next = !this.#visible();
|
|
2279
|
+
this.#visible.set(next);
|
|
2280
|
+
this.#write(sessionStorage, WINDOW_KEY, next);
|
|
2281
|
+
this.#write(localStorage, DEFAULT_KEY, next);
|
|
2251
2282
|
}
|
|
2283
|
+
/**
|
|
2284
|
+
* This window's own choice wins; otherwise the last choice made anywhere;
|
|
2285
|
+
* otherwise visible.
|
|
2286
|
+
*
|
|
2287
|
+
* The order matters on a reload: a window that had hidden the terminal must
|
|
2288
|
+
* not have it reappear because another window showed one since.
|
|
2289
|
+
*/
|
|
2252
2290
|
#readInitial() {
|
|
2291
|
+
const own = this.#read(sessionStorage, WINDOW_KEY);
|
|
2292
|
+
if (own !== undefined)
|
|
2293
|
+
return own;
|
|
2294
|
+
const shared = this.#read(localStorage, DEFAULT_KEY);
|
|
2295
|
+
return shared ?? true;
|
|
2296
|
+
}
|
|
2297
|
+
#read(store, key) {
|
|
2253
2298
|
try {
|
|
2254
|
-
const raw =
|
|
2255
|
-
return raw === null ?
|
|
2299
|
+
const raw = store.getItem(key);
|
|
2300
|
+
return raw === null ? undefined : raw === '1';
|
|
2256
2301
|
}
|
|
2257
2302
|
catch {
|
|
2258
|
-
|
|
2303
|
+
// Storage unavailable (private mode / prerender) — fall through.
|
|
2304
|
+
return undefined;
|
|
2305
|
+
}
|
|
2306
|
+
}
|
|
2307
|
+
#write(store, key, value) {
|
|
2308
|
+
try {
|
|
2309
|
+
store.setItem(key, value ? '1' : '0');
|
|
2310
|
+
}
|
|
2311
|
+
catch {
|
|
2312
|
+
// Non-fatal by policy: a lost layout preference must not break the screen.
|
|
2259
2313
|
}
|
|
2260
2314
|
}
|
|
2261
2315
|
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.2", ngImport: i0, type: TerminalVisibilityService, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
|
|
@@ -2264,7 +2318,7 @@ class TerminalVisibilityService {
|
|
|
2264
2318
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.2", ngImport: i0, type: TerminalVisibilityService, decorators: [{
|
|
2265
2319
|
type: Injectable,
|
|
2266
2320
|
args: [{ providedIn: 'root' }]
|
|
2267
|
-
}]
|
|
2321
|
+
}] });
|
|
2268
2322
|
|
|
2269
2323
|
/**
|
|
2270
2324
|
* Elements that mean "the user is typing", where an unmodified shortcut must not fire.
|
|
@@ -2777,12 +2831,28 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.2", ngImpor
|
|
|
2777
2831
|
args: [{ providedIn: 'root' }]
|
|
2778
2832
|
}], ctorParameters: () => [] });
|
|
2779
2833
|
|
|
2834
|
+
/** Roomy enough for the scanner beside a chart, small enough for a laptop. */
|
|
2835
|
+
const POP_OUT_WIDTH = 1280;
|
|
2836
|
+
const POP_OUT_HEIGHT = 900;
|
|
2780
2837
|
class StatusBar {
|
|
2781
2838
|
constructor() {
|
|
2782
2839
|
this.#authService = inject(AuthenticationService);
|
|
2783
2840
|
this.#focusService = inject(TerminalFocusService);
|
|
2784
2841
|
this.#commandService = inject(CommandService);
|
|
2842
|
+
this.#sharedSession = inject(SHELL_SHARED_SESSION, { optional: true }) ?? false;
|
|
2785
2843
|
this.isAuthenticated = this.#authService.authenticated;
|
|
2844
|
+
/**
|
|
2845
|
+
* Whether a second window is offered at all (multiple-windows PRD D14).
|
|
2846
|
+
*
|
|
2847
|
+
* ⚠ GATED ON SHARED SESSIONS, and this is a correctness gate rather than a
|
|
2848
|
+
* tidiness one. Without a session shared across windows, the new window starts
|
|
2849
|
+
* with no token, goes to the login screen, and signing in there ends the
|
|
2850
|
+
* session THIS window is using. A pop-out button in an app without sharing is
|
|
2851
|
+
* a button that signs you out — the exact failure the multiple-windows work
|
|
2852
|
+
* exists to remove.
|
|
2853
|
+
*/
|
|
2854
|
+
this.canPopOut = computed(() => this.#sharedSession && this.isAuthenticated(), /* @ts-ignore */
|
|
2855
|
+
...(ngDevMode ? [{ debugName: "canPopOut" }] : /* istanbul ignore next */ []));
|
|
2786
2856
|
this.loginStatus = computed(() => this.#authService.authenticating()
|
|
2787
2857
|
? 'Authenticating...'
|
|
2788
2858
|
: this.#authService.authenticated()
|
|
@@ -2793,21 +2863,41 @@ class StatusBar {
|
|
|
2793
2863
|
#authService;
|
|
2794
2864
|
#focusService;
|
|
2795
2865
|
#commandService;
|
|
2866
|
+
#sharedSession;
|
|
2796
2867
|
logout() {
|
|
2797
2868
|
this.#authService.logout();
|
|
2798
2869
|
this.#commandService.echoCommand('logout');
|
|
2799
2870
|
this.#focusService.requestFocus();
|
|
2800
2871
|
}
|
|
2872
|
+
/**
|
|
2873
|
+
* Open the screen you are on in a second, sized window.
|
|
2874
|
+
*
|
|
2875
|
+
* There is no mechanism by which this differs from the browser's own
|
|
2876
|
+
* "Duplicate tab": every window is a separate copy of the app started at some
|
|
2877
|
+
* address. It exists for the sizing and because it is discoverable — Ctrl+T,
|
|
2878
|
+
* a bookmark and dragging a tab out all work equally well once the session is
|
|
2879
|
+
* shared.
|
|
2880
|
+
*
|
|
2881
|
+
* `noopener` is deliberate. The new window needs nothing from this one — it
|
|
2882
|
+
* reads the session from storage — and leaving the opener reachable would give
|
|
2883
|
+
* two windows script access to each other for no purpose.
|
|
2884
|
+
*/
|
|
2885
|
+
popOut() {
|
|
2886
|
+
if (typeof window === 'undefined')
|
|
2887
|
+
return;
|
|
2888
|
+
window.open(window.location.href, '_blank', `noopener,noreferrer,width=${POP_OUT_WIDTH},height=${POP_OUT_HEIGHT}`);
|
|
2889
|
+
this.#focusService.requestFocus();
|
|
2890
|
+
}
|
|
2801
2891
|
openFaq() {
|
|
2802
2892
|
this.#commandService.execute('faq');
|
|
2803
2893
|
this.#focusService.requestFocus();
|
|
2804
2894
|
}
|
|
2805
2895
|
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.2", ngImport: i0, type: StatusBar, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
|
|
2806
|
-
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 <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 }); }
|
|
2896
|
+
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 }); }
|
|
2807
2897
|
}
|
|
2808
2898
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.2", ngImport: i0, type: StatusBar, decorators: [{
|
|
2809
2899
|
type: Component,
|
|
2810
|
-
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 <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"] }]
|
|
2900
|
+
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"] }]
|
|
2811
2901
|
}] });
|
|
2812
2902
|
|
|
2813
2903
|
class SideMenu {
|