@franzzemen/brokenstock-shell 0.10.1 → 0.10.3
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.
|
@@ -850,7 +850,9 @@ class AuthenticationService {
|
|
|
850
850
|
if (err instanceof PasskeyCancelledError)
|
|
851
851
|
throw new AuthenticatorError(undefined, 'Cancelled — no passkey was added.', 0);
|
|
852
852
|
if (err instanceof DOMException && err.name === 'InvalidStateError') {
|
|
853
|
-
|
|
853
|
+
// The browser found one of this account's passkeys already usable here — often synced from
|
|
854
|
+
// another computer through a Google or Apple account. No second one is needed.
|
|
855
|
+
throw new AuthenticatorError(undefined, 'A passkey for this account is already on this device — it may be synced from another computer through your Google or Apple account. Use "Sign in with a passkey".', 0);
|
|
854
856
|
}
|
|
855
857
|
throw new AuthenticatorError(undefined, 'The browser could not make a passkey.', 0);
|
|
856
858
|
}
|
|
@@ -3962,6 +3964,11 @@ class SideMenu {
|
|
|
3962
3964
|
disabledReasonFor(item) {
|
|
3963
3965
|
return item.disabledReason?.() ?? item.label;
|
|
3964
3966
|
}
|
|
3967
|
+
/** The item's attention badge, or null when there is nothing to draw (no badge, or a count of 0). */
|
|
3968
|
+
badgeFor(item) {
|
|
3969
|
+
const badge = item.badge?.() ?? null;
|
|
3970
|
+
return badge && badge.count > 0 ? badge : null;
|
|
3971
|
+
}
|
|
3965
3972
|
onMenuClick(item) {
|
|
3966
3973
|
if (item.command) {
|
|
3967
3974
|
this.#commandService.echoCommand(item.command);
|
|
@@ -3986,11 +3993,11 @@ class SideMenu {
|
|
|
3986
3993
|
this.#phoneLayout.closeMenu();
|
|
3987
3994
|
}
|
|
3988
3995
|
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.2", ngImport: i0, type: SideMenu, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
|
|
3989
|
-
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 (
|
|
3996
|
+
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]=\"badgeFor(item)?.tooltip ?? item.label\"\n matTooltipPosition=\"right\"\n [matTooltipDisabled]=\"drawer()\"\n (click)=\"onMenuClick(item)\"\n >\n <mat-icon>{{ item.icon }}</mat-icon>\n @if (badgeFor(item); as badge) {\n <span class=\"menu-badge\" [attr.aria-label]=\"badge.tooltip\">{{ badge.count }}</span>\n }\n @if (drawer()) {\n <span class=\"menu-label\">\n {{ item.label }}\n @if (badgeFor(item); as badge) {\n <span class=\"menu-reason\">{{ badge.tooltip }}</span>\n }\n </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{position:relative}.menu-badge{position:absolute;top:2px;right:2px;min-width:16px;height:16px;padding:0 4px;border-radius:8px;background-color:var(--color-loss, #8c0b2b);color:#fff;font-size:10px;font-weight:700;line-height:16px;text-align:center;pointer-events:none}.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 }); }
|
|
3990
3997
|
}
|
|
3991
3998
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.2", ngImport: i0, type: SideMenu, decorators: [{
|
|
3992
3999
|
type: Component,
|
|
3993
|
-
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 (
|
|
4000
|
+
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]=\"badgeFor(item)?.tooltip ?? item.label\"\n matTooltipPosition=\"right\"\n [matTooltipDisabled]=\"drawer()\"\n (click)=\"onMenuClick(item)\"\n >\n <mat-icon>{{ item.icon }}</mat-icon>\n @if (badgeFor(item); as badge) {\n <span class=\"menu-badge\" [attr.aria-label]=\"badge.tooltip\">{{ badge.count }}</span>\n }\n @if (drawer()) {\n <span class=\"menu-label\">\n {{ item.label }}\n @if (badgeFor(item); as badge) {\n <span class=\"menu-reason\">{{ badge.tooltip }}</span>\n }\n </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{position:relative}.menu-badge{position:absolute;top:2px;right:2px;min-width:16px;height:16px;padding:0 4px;border-radius:8px;background-color:var(--color-loss, #8c0b2b);color:#fff;font-size:10px;font-weight:700;line-height:16px;text-align:center;pointer-events:none}.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"] }]
|
|
3994
4001
|
}], propDecorators: { drawer: [{ type: i0.Input, args: [{ isSignal: true, alias: "drawer", required: false }] }] } });
|
|
3995
4002
|
|
|
3996
4003
|
class MainContent {
|
|
@@ -4402,6 +4409,8 @@ class Login {
|
|
|
4402
4409
|
#activatedRoute;
|
|
4403
4410
|
/** The quiet wait for a passkey picked from the username box's suggestions (passkey D9). */
|
|
4404
4411
|
#autofill;
|
|
4412
|
+
/** Settles when the quiet wait has fully ended — the button's prompt waits for it. */
|
|
4413
|
+
#autofillSettled;
|
|
4405
4414
|
constructor(authenticationService, router) {
|
|
4406
4415
|
this.authenticationService = authenticationService;
|
|
4407
4416
|
this.router = router;
|
|
@@ -4451,12 +4460,18 @@ class Login {
|
|
|
4451
4460
|
this.#autofill?.abort();
|
|
4452
4461
|
}
|
|
4453
4462
|
/** "Sign in with a passkey". The quiet wait must stop first: a browser runs one passkey prompt at a time. */
|
|
4454
|
-
onPasskey() {
|
|
4463
|
+
async onPasskey() {
|
|
4455
4464
|
this.errors = [];
|
|
4456
4465
|
this.passkeyHint.set(false);
|
|
4466
|
+
this.submitted = true;
|
|
4457
4467
|
this.#autofill?.abort();
|
|
4458
4468
|
this.#autofill = undefined;
|
|
4459
|
-
|
|
4469
|
+
// ⚠ Wait for the browser to finish cancelling the quiet request before opening the prompt.
|
|
4470
|
+
// Starting a second passkey request while the first is still being torn down is the overlap
|
|
4471
|
+
// Windows is least tolerant of. Bounded, so a request that never settles cannot block the button.
|
|
4472
|
+
const settled = this.#autofillSettled;
|
|
4473
|
+
if (settled)
|
|
4474
|
+
await Promise.race([settled, new Promise(r => setTimeout(r, 1500))]);
|
|
4460
4475
|
this.#passkeySignIn(false);
|
|
4461
4476
|
}
|
|
4462
4477
|
/**
|
|
@@ -4472,10 +4487,10 @@ class Login {
|
|
|
4472
4487
|
this.#autofill?.abort();
|
|
4473
4488
|
const controller = new AbortController();
|
|
4474
4489
|
this.#autofill = controller;
|
|
4475
|
-
this.#passkeySignIn(true, controller.signal);
|
|
4490
|
+
this.#autofillSettled = this.#passkeySignIn(true, controller.signal);
|
|
4476
4491
|
}
|
|
4477
4492
|
#passkeySignIn(conditional, signal) {
|
|
4478
|
-
this.authenticationService
|
|
4493
|
+
return this.authenticationService
|
|
4479
4494
|
.loginWithPasskey({ conditional, ...(signal ? { signal } : {}) })
|
|
4480
4495
|
.then(() => this.#afterSignIn(undefined))
|
|
4481
4496
|
.catch((err) => {
|