Refine account connections and provider sign-in UI

This commit is contained in:
JB
2026-07-25 02:17:45 -04:00
parent c3244b0c79
commit 854a7e7cc3
8 changed files with 111 additions and 21 deletions
+31
View File
@@ -65,6 +65,25 @@ Use the spacing tokens for new components. Prefer `8px`, `12px`, and `20px` gaps
portrait or uploading a custom portrait, use a compact secondary tab row. portrait or uploading a custom portrait, use a compact secondary tab row.
Tabs retain native button semantics plus tablist, tab, and tabpanel roles, Tabs retain native button semantics plus tablist, tab, and tabpanel roles,
textual selected state, keyboard focus, and pressed feedback. textual selected state, keyboard focus, and pressed feedback.
- Account Access uses a secondary method navigation for passkeys, verified
email, local password fallback, and linked identity providers. Each tab shows
a compact configured-method count and routes failed actions back to the
relevant panel. External identity connections live inside Access rather than
competing with it as a top-level account domain.
- Future external providers may appear as non-interactive planned rows so the
intended identity architecture is visible before credentials are configured.
Placeholders must be visually subdued, explicitly labeled `PLANNED`, and must
not imitate enabled buttons. Provider-compliant assets and controls replace
these rows only when the corresponding OAuth application is implemented.
- Provider unlinking does not assume that a password exists. The unlink control
is the provider card’s `LINKED` status badge. Hover and keyboard focus turn it
red and replace the label with `UNLINK`; activation replaces the full card
surface with an explicit Cancel / Confirm Unlink decision. Confirmation uses
the square SHD primary action with the orange action accent, while Cancel
remains a dark outlined secondary control. The authenticated
session plus this deliberate second action authorizes the request; the shared
server-side last-method guard remains authoritative. Future provider cards
reuse this pattern rather than adding provider-specific credential prompts.
- Authentication pages use a split frame: contextual SHD network information on - Authentication pages use a split frame: contextual SHD network information on
the left and the actionable secure-access console on the right. the left and the actionable secure-access console on the right.
- OAuth providers appear as connection nodes with a recognizable provider color; - OAuth providers appear as connection nodes with a recognizable provider color;
@@ -108,6 +127,18 @@ Use the spacing tokens for new components. Prefer `8px`, `12px`, and `20px` gaps
with the explicit label “Continue with Discord.” with the explicit label “Continue with Discord.”
- Discord account connection controls use the same official symbol and Blurple - Discord account connection controls use the same official symbol and Blurple
treatment with the explicit label “Link Discord.” treatment with the explicit label “Link Discord.”
- Sign-in pages group external providers into a compact, equal-weight provider
rail. Keep a short visible provider name beneath each mark; accessible names
and hover text are supplementary and do not replace required brand wording.
- Google uses its standard multicolor G on a white icon field, Apple uses a
white Apple mark on black, Discord uses its unmodified white symbol on
Blurple, and Steam uses its white linked-circle mark on Steam navy.
- Providers that are not configured are disabled, desaturated, and marked
`PLANNED`; they must not navigate, submit, or visually imply availability.
- When each provider is implemented, replace its placeholder with the
provider-rendered control or current pre-approved sign-in artwork where the
provider requires it. In particular, use Google Identity Services or its
approved icon-mode asset and Steam's published sign-in image.
- Preserve the provider's required clearspace, logo proportions, approved color, - Preserve the provider's required clearspace, logo proportions, approved color,
readable contrast, and text/logo alignment even when the surrounding surface readable contrast, and text/logo alignment even when the surrounding surface
follows the SHD theme. follows the SHD theme.
+5 -1
View File
@@ -57,15 +57,19 @@
.catalogue-page,.guides-page,.profile-page{padding-top:34px}.catalogue-hero{min-height:230px;display:flex;justify-content:space-between;align-items:center;padding:25px 32px;margin-bottom:12px;border:1px solid var(--shd-border);background:radial-gradient(circle at 80% 50%,#332714 0,transparent 30%),linear-gradient(120deg,#141718,#0d1011);clip-path:polygon(0 0,97% 0,100% 18%,100% 100%,3% 100%,0 82%)}.catalogue-hero h1{font-size:clamp(48px,6vw,82px);line-height:.82;letter-spacing:-3px;margin:14px 0}.catalogue-hero h1 em{font-style:normal;color:var(--shd-orange)}.catalogue-hero>div>p:last-child{max-width:580px;color:var(--shd-text-muted);font:15px/1.55 Georgia,serif}.catalogue-count{min-width:210px;display:grid;text-align:right}.catalogue-count strong{font-size:38px}.catalogue-count span{font-size:7px;color:#6d7476;letter-spacing:1.5px;margin:3px 0 18px}.catalogue-count .primary{justify-self:end}.filter-bar{display:grid;grid-template-columns:2fr 1fr 1fr auto;gap:7px;align-items:end;margin:12px 0 18px;padding:11px;background:#101314;border:1px solid #2d3233}.filter-bar label{font-size:7px;color:#72797b;letter-spacing:1.2px}.filter-bar input,.filter-bar select{height:38px;margin-top:6px;padding:0 10px;font-size:9px}.filter-bar button{height:38px;padding:0 14px}.catalogue-grid{max-width:none}.catalogue-grid article{position:relative}.build-open{width:100%;margin-top:15px;padding:10px;border:1px solid #3a4041;background:#101314;color:#f6a821;font-size:8px;font-weight:bold;letter-spacing:1px}.guide-layout{display:grid;grid-template-columns:1fr 330px;gap:12px}.guide-feature{min-height:360px;padding:35px;position:relative;border:1px solid #303536;border-top:2px solid #f6a821;background:linear-gradient(135deg,#151819,#0d1011)}.guide-code{position:absolute;right:25px;top:22px;color:#4f5658;font-size:8px;letter-spacing:1.5px}.guide-feature h2{font-size:42px;line-height:.95;margin:20px 0}.guide-feature>p:not(.eyebrow){max-width:650px;color:#858b8d;font:15px/1.6 Georgia,serif}.guide-meta{display:flex;gap:8px;position:absolute;left:35px;bottom:30px}.guide-meta span{display:grid;padding:8px 11px;border:1px solid #303536;color:#60686a;font-size:7px;letter-spacing:1px}.guide-meta b{color:#d7d9d5;font-size:9px;margin-top:4px}.guide-index{border:1px solid #303536;background:#111415;padding:20px}.guide-index>div{display:grid;grid-template-columns:30px 1fr auto;align-items:center;min-height:54px;border-top:1px solid #292e2f}.guide-index small{color:#535b5d;font-size:8px}.guide-index span{font-size:9px;letter-spacing:.8px}.guide-index b{color:#f6a821} .catalogue-page,.guides-page,.profile-page{padding-top:34px}.catalogue-hero{min-height:230px;display:flex;justify-content:space-between;align-items:center;padding:25px 32px;margin-bottom:12px;border:1px solid var(--shd-border);background:radial-gradient(circle at 80% 50%,#332714 0,transparent 30%),linear-gradient(120deg,#141718,#0d1011);clip-path:polygon(0 0,97% 0,100% 18%,100% 100%,3% 100%,0 82%)}.catalogue-hero h1{font-size:clamp(48px,6vw,82px);line-height:.82;letter-spacing:-3px;margin:14px 0}.catalogue-hero h1 em{font-style:normal;color:var(--shd-orange)}.catalogue-hero>div>p:last-child{max-width:580px;color:var(--shd-text-muted);font:15px/1.55 Georgia,serif}.catalogue-count{min-width:210px;display:grid;text-align:right}.catalogue-count strong{font-size:38px}.catalogue-count span{font-size:7px;color:#6d7476;letter-spacing:1.5px;margin:3px 0 18px}.catalogue-count .primary{justify-self:end}.filter-bar{display:grid;grid-template-columns:2fr 1fr 1fr auto;gap:7px;align-items:end;margin:12px 0 18px;padding:11px;background:#101314;border:1px solid #2d3233}.filter-bar label{font-size:7px;color:#72797b;letter-spacing:1.2px}.filter-bar input,.filter-bar select{height:38px;margin-top:6px;padding:0 10px;font-size:9px}.filter-bar button{height:38px;padding:0 14px}.catalogue-grid{max-width:none}.catalogue-grid article{position:relative}.build-open{width:100%;margin-top:15px;padding:10px;border:1px solid #3a4041;background:#101314;color:#f6a821;font-size:8px;font-weight:bold;letter-spacing:1px}.guide-layout{display:grid;grid-template-columns:1fr 330px;gap:12px}.guide-feature{min-height:360px;padding:35px;position:relative;border:1px solid #303536;border-top:2px solid #f6a821;background:linear-gradient(135deg,#151819,#0d1011)}.guide-code{position:absolute;right:25px;top:22px;color:#4f5658;font-size:8px;letter-spacing:1.5px}.guide-feature h2{font-size:42px;line-height:.95;margin:20px 0}.guide-feature>p:not(.eyebrow){max-width:650px;color:#858b8d;font:15px/1.6 Georgia,serif}.guide-meta{display:flex;gap:8px;position:absolute;left:35px;bottom:30px}.guide-meta span{display:grid;padding:8px 11px;border:1px solid #303536;color:#60686a;font-size:7px;letter-spacing:1px}.guide-meta b{color:#d7d9d5;font-size:9px;margin-top:4px}.guide-index{border:1px solid #303536;background:#111415;padding:20px}.guide-index>div{display:grid;grid-template-columns:30px 1fr auto;align-items:center;min-height:54px;border-top:1px solid #292e2f}.guide-index small{color:#535b5d;font-size:8px}.guide-index span{font-size:9px;letter-spacing:.8px}.guide-index b{color:#f6a821}
.profile-command{display:flex;justify-content:space-between;align-items:center;padding:0 0 24px;border-bottom:1px solid #303536}.profile-ident{display:flex;align-items:center;gap:18px}.profile-avatar{width:76px;height:76px;display:block;object-fit:cover;border:1px solid #f6a821;border-radius:50%;background:#0a0c0d;box-shadow:0 0 0 6px #0a0c0d,0 0 0 7px #f6a82155}.profile-ident h1{font-size:39px;margin:4px 0}.role-badge{display:inline-block;padding:4px 8px;border:1px solid #6e5226;color:#f6a821;font-size:7px;letter-spacing:1.3px}.profile-metrics{display:flex;gap:7px}.profile-metrics div{min-width:77px;height:59px;display:grid;place-items:center;padding:7px;background:#111415;border:1px solid #303536}.profile-metrics strong{font-size:20px}.profile-metrics span{font-size:7px;color:#697174;letter-spacing:1px}.account-alert{display:flex;gap:16px;margin:12px 0;padding:12px 15px;background:#2a2112;border:1px solid #795c2a;border-left:3px solid #f6a821;font-size:10px}.account-alert b{color:#f6a821}.account-alert span{color:#b8ad99}.profile-dashboard{display:grid;grid-template-columns:minmax(0,1fr) 340px;gap:12px;margin-top:12px}.profile-feed{display:grid;gap:12px}.profile-module,.account-console{background:#111415;border:1px solid #303536;padding:16px}.module-head{display:flex;justify-content:space-between;align-items:center}.module-head h2,.profile-module h2,.account-console h2{font-size:17px;margin:4px 0 12px}.module-head .secondary{padding:10px 12px}.profile-table{border:1px solid #292e2f}.profile-row{display:grid;grid-template-columns:1.5fr .7fr .7fr .3fr;align-items:center;gap:9px;min-height:47px;padding:7px 11px;border-top:1px solid #292e2f;font-size:9px}.profile-row.heading{min-height:30px;border:0;background:#0d1011;color:#636b6d;font-size:7px;letter-spacing:1px}.profile-row>div{display:grid}.profile-row small{font-size:7px;color:#596164}.profile-row>span{text-transform:uppercase;color:#858d8f}.profile-row button{border:0;background:transparent;color:#f6a821;font-size:8px}.profile-empty{text-align:center;padding:38px}.profile-empty>span{font-size:31px;color:#f6a821}.profile-empty b{display:block;font-size:10px;letter-spacing:1px;margin:10px}.profile-empty p{color:#71797b;font:13px Georgia,serif}.profile-empty a{color:#f6a821;font-size:8px;font-weight:bold}.profile-columns{display:grid;grid-template-columns:1fr 1fr;gap:12px}.large-count{font-size:32px}.count-label{display:block;font-size:7px;color:#687073;letter-spacing:1.2px}.account-console{height:max-content}.console-head{display:flex;justify-content:space-between;padding-bottom:11px;margin-bottom:15px;border-bottom:1px solid #303536;color:#747c7e;font-size:7px;letter-spacing:1.3px}.console-head i{width:6px;height:6px;border-radius:50%;background:#75c08c;box-shadow:0 0 6px #75c08c}.console-action{width:100%;margin-top:9px}.connection-card{display:grid;grid-template-columns:35px 1fr auto;align-items:center;padding:10px;border:1px solid #303536}.discord-node{color:#8993ff;font-size:21px}.connection-card div{display:grid}.connection-card b{font-size:9px}.connection-card small{font-size:7px;color:#656d6f;margin-top:3px}.connection-card a{color:#f6a821;font-size:8px;font-weight:bold} .profile-command{display:flex;justify-content:space-between;align-items:center;padding:0 0 24px;border-bottom:1px solid #303536}.profile-ident{display:flex;align-items:center;gap:18px}.profile-avatar{width:76px;height:76px;display:block;object-fit:cover;border:1px solid #f6a821;border-radius:50%;background:#0a0c0d;box-shadow:0 0 0 6px #0a0c0d,0 0 0 7px #f6a82155}.profile-ident h1{font-size:39px;margin:4px 0}.role-badge{display:inline-block;padding:4px 8px;border:1px solid #6e5226;color:#f6a821;font-size:7px;letter-spacing:1.3px}.profile-metrics{display:flex;gap:7px}.profile-metrics div{min-width:77px;height:59px;display:grid;place-items:center;padding:7px;background:#111415;border:1px solid #303536}.profile-metrics strong{font-size:20px}.profile-metrics span{font-size:7px;color:#697174;letter-spacing:1px}.account-alert{display:flex;gap:16px;margin:12px 0;padding:12px 15px;background:#2a2112;border:1px solid #795c2a;border-left:3px solid #f6a821;font-size:10px}.account-alert b{color:#f6a821}.account-alert span{color:#b8ad99}.profile-dashboard{display:grid;grid-template-columns:minmax(0,1fr) 340px;gap:12px;margin-top:12px}.profile-feed{display:grid;gap:12px}.profile-module,.account-console{background:#111415;border:1px solid #303536;padding:16px}.module-head{display:flex;justify-content:space-between;align-items:center}.module-head h2,.profile-module h2,.account-console h2{font-size:17px;margin:4px 0 12px}.module-head .secondary{padding:10px 12px}.profile-table{border:1px solid #292e2f}.profile-row{display:grid;grid-template-columns:1.5fr .7fr .7fr .3fr;align-items:center;gap:9px;min-height:47px;padding:7px 11px;border-top:1px solid #292e2f;font-size:9px}.profile-row.heading{min-height:30px;border:0;background:#0d1011;color:#636b6d;font-size:7px;letter-spacing:1px}.profile-row>div{display:grid}.profile-row small{font-size:7px;color:#596164}.profile-row>span{text-transform:uppercase;color:#858d8f}.profile-row button{border:0;background:transparent;color:#f6a821;font-size:8px}.profile-empty{text-align:center;padding:38px}.profile-empty>span{font-size:31px;color:#f6a821}.profile-empty b{display:block;font-size:10px;letter-spacing:1px;margin:10px}.profile-empty p{color:#71797b;font:13px Georgia,serif}.profile-empty a{color:#f6a821;font-size:8px;font-weight:bold}.profile-columns{display:grid;grid-template-columns:1fr 1fr;gap:12px}.large-count{font-size:32px}.count-label{display:block;font-size:7px;color:#687073;letter-spacing:1.2px}.account-console{height:max-content}.console-head{display:flex;justify-content:space-between;padding-bottom:11px;margin-bottom:15px;border-bottom:1px solid #303536;color:#747c7e;font-size:7px;letter-spacing:1.3px}.console-head i{width:6px;height:6px;border-radius:50%;background:#75c08c;box-shadow:0 0 6px #75c08c}.console-action{width:100%;margin-top:9px}.connection-card{display:grid;grid-template-columns:35px 1fr auto;align-items:center;padding:10px;border:1px solid #303536}.discord-node{color:#8993ff;font-size:21px}.connection-card div{display:grid}.connection-card b{font-size:9px}.connection-card small{font-size:7px;color:#656d6f;margin-top:3px}.connection-card a{color:#f6a821;font-size:8px;font-weight:bold}
.avatar-picker{display:grid;grid-template-columns:repeat(4,1fr);gap:6px;margin-bottom:10px}.avatar-preset-form fieldset{height:100%}.avatar-preset{display:grid;gap:5px;width:100%;height:100%;padding:4px;background:#0b0e0f;border:1px solid #303536;color:#747c7e}.avatar-preset:hover,.avatar-preset:focus-visible{border-color:#8a672b;color:#f6a821}.avatar-preset.selected{border-color:#f6a821;color:#f6a821;box-shadow:inset 0 -2px #f6a821}.avatar-preset img{display:block;width:100%;aspect-ratio:1;object-fit:cover}.avatar-preset span{font-size:6px;line-height:1.25;text-transform:uppercase;letter-spacing:.4px}.avatar-upload{margin-top:8px} .avatar-picker{display:grid;grid-template-columns:repeat(4,1fr);gap:6px;margin-bottom:10px}.avatar-preset-form fieldset{height:100%}.avatar-preset{display:grid;gap:5px;width:100%;height:100%;padding:4px;background:#0b0e0f;border:1px solid #303536;color:#747c7e}.avatar-preset:hover,.avatar-preset:focus-visible{border-color:#8a672b;color:#f6a821}.avatar-preset.selected{border-color:#f6a821;color:#f6a821;box-shadow:inset 0 -2px #f6a821}.avatar-preset img{display:block;width:100%;aspect-ratio:1;object-fit:cover}.avatar-preset span{font-size:6px;line-height:1.25;text-transform:uppercase;letter-spacing:.4px}.avatar-upload{margin-top:8px}
.account-tabs{width:100%;height:auto;margin:0 0 16px;display:grid;grid-template-columns:repeat(4,1fr);border:1px solid var(--shd-border)}.account-tabs button,.account-subtabs button{min-width:0;height:34px;padding:0 5px;border:0;border-left:1px solid var(--shd-border);border-bottom:2px solid transparent;background:#0d1011;color:#6f7779;font-size:7px;font-weight:900;letter-spacing:.8px;cursor:pointer;transition:var(--shd-transition)}.account-tabs button:first-child,.account-subtabs button:first-child{border-left:0}.account-tabs button:hover,.account-tabs button:focus-visible,.account-subtabs button:hover,.account-subtabs button:focus-visible{color:var(--shd-text);background:#171b1c}.account-tabs button.active,.account-subtabs button.active{border-bottom-color:var(--shd-orange);background:#211a0e;color:var(--shd-orange)}.account-tabs button:active,.account-subtabs button:active{transform:translateY(1px);filter:brightness(.85)}.account-tabs button:focus-visible,.account-subtabs button:focus-visible{position:relative;z-index:1;outline:2px solid var(--shd-orange);outline-offset:2px}.account-panel{min-height:290px}.account-subtabs{display:grid;grid-template-columns:1fr 1fr;margin:0 0 10px;border:1px solid var(--shd-border)}.account-subtabs button{height:31px}.account-retention{color:var(--shd-text-muted);font:12px/1.55 Georgia,serif}.danger-eyebrow{color:var(--shd-danger)} .account-tabs{width:100%;height:auto;margin:0 0 16px;display:grid;grid-template-columns:repeat(3,1fr);border:1px solid var(--shd-border)}.account-tabs button,.account-subtabs button{min-width:0;height:34px;padding:0 5px;border:0;border-left:1px solid var(--shd-border);border-bottom:2px solid transparent;background:#0d1011;color:#6f7779;font-size:7px;font-weight:900;letter-spacing:.8px;cursor:pointer;transition:var(--shd-transition)}.account-tabs button:first-child,.account-subtabs button:first-child{border-left:0}.account-tabs button:hover,.account-tabs button:focus-visible,.account-subtabs button:hover,.account-subtabs button:focus-visible{color:var(--shd-text);background:#171b1c}.account-tabs button.active,.account-subtabs button.active{border-bottom-color:var(--shd-orange);background:#211a0e;color:var(--shd-orange)}.account-tabs button:active,.account-subtabs button:active{transform:translateY(1px);filter:brightness(.85)}.account-tabs button:focus-visible,.account-subtabs button:focus-visible{position:relative;z-index:1;outline:2px solid var(--shd-orange);outline-offset:2px}.account-panel{min-height:290px}.account-subtabs{display:grid;grid-template-columns:1fr 1fr;margin:0 0 10px;border:1px solid var(--shd-border)}.account-subtabs button{height:31px}.access-subtabs{display:grid;grid-template-columns:repeat(4,1fr);margin:-2px 0 17px;border:1px solid var(--shd-border);background:#0b0e0f}.access-subtabs button{min-width:0;height:42px;display:grid;place-content:center;gap:3px;padding:4px;border:0;border-left:1px solid var(--shd-border);border-bottom:2px solid transparent;background:transparent;color:var(--shd-text-dim);cursor:pointer;transition:var(--shd-transition)}.access-subtabs button:first-child{border-left:0}.access-subtabs button span{font-size:6px;font-weight:900;letter-spacing:.7px}.access-subtabs button b{font-size:9px}.access-subtabs button:hover,.access-subtabs button:focus-visible{background:#171b1c;color:var(--shd-text)}.access-subtabs button.active{border-bottom-color:var(--shd-orange);background:#211a0e;color:var(--shd-orange)}.access-subtabs button:active{transform:translateY(1px);filter:brightness(.85)}.access-subtabs button:focus-visible{position:relative;z-index:1;outline:2px solid var(--shd-orange);outline-offset:2px}.account-retention{color:var(--shd-text-muted);font:12px/1.55 Georgia,serif}.danger-eyebrow{color:var(--shd-danger)}
.account-alert.error-alert{background:#261513;border-color:#75443b;border-left-color:#df8069}.account-alert.error-alert b{color:#df8069}.account-alert.error-alert span{color:#d2aaa1} .account-alert.error-alert{background:#261513;border-color:#75443b;border-left-color:#df8069}.account-alert.error-alert b{color:#df8069}.account-alert.error-alert span{color:#d2aaa1}
.danger-zone{margin-top:12px;border:1px solid #563734;background:#181112}.danger-zone summary{padding:10px;color:#df8069;cursor:pointer;font-size:8px;font-weight:bold;letter-spacing:1px}.danger-zone>p,.danger-zone>form{margin:0;padding:0 10px 10px}.danger-zone>p{color:#9f8580;font-size:8px;line-height:1.5}.danger-zone form{padding-bottom:10px} .danger-zone{margin-top:12px;border:1px solid #563734;background:#181112}.danger-zone summary{padding:10px;color:#df8069;cursor:pointer;font-size:8px;font-weight:bold;letter-spacing:1px}.danger-zone>p,.danger-zone>form{margin:0;padding:0 10px 10px}.danger-zone>p{color:#9f8580;font-size:8px;line-height:1.5}.danger-zone form{padding-bottom:10px}
.auth-page{display:block;padding:55px clamp(20px,5vw,80px)}.auth-frame{width:min(980px,100%);min-height:520px;margin:auto;display:grid;grid-template-columns:1.15fr .85fr;border:1px solid #34393a;background:#101314;clip-path:polygon(0 0,96% 0,100% 7%,100% 100%,4% 100%,0 93%)}.auth-intel{width:auto!important;padding:65px!important;border:0!important;border-right:1px solid #303536!important;background:radial-gradient(circle at 15% 80%,#392b15 0,transparent 33%),#0e1112!important}.auth-intel h1{font-size:66px!important;line-height:.85;letter-spacing:-3px;margin:24px 0}.auth-intel h1 em{font-style:normal;color:#f6a821}.auth-intel>p:not(.eyebrow){max-width:470px;color:#909698;font:15px/1.6 Georgia,serif}.auth-status{display:flex;justify-content:space-between;margin-top:55px;padding-top:14px;border-top:1px solid #303536;color:#626a6c;font-size:7px;letter-spacing:1.2px}.auth-status i{display:inline-block;width:5px;height:5px;border-radius:50%;background:#75c08c;box-shadow:0 0 6px #75c08c;margin-right:5px}.auth-console{width:auto!important;padding:34px!important;border:0!important;background:#141718!important}.auth-console .console-head b{color:#f6a821}.auth-console .discord{min-height:48px;margin-top:18px;padding:10px 18px;display:flex;align-items:center;justify-content:center;gap:14px;background:#5865f2;color:#fff;border:0;border-radius:4px;font-size:15px;font-weight:600;letter-spacing:0;text-transform:none}.auth-console .discord img{display:block;width:27px;height:21px;object-fit:contain;flex:0 0 auto}.auth-console .discord:hover{background:#4752c4;filter:none}.auth-console .discord:active{background:#3c45a5;filter:none}.auth-console .discord:focus-visible{outline-color:#e0e3ff}.auth-console small{margin-top:18px} .auth-page{display:block;padding:55px clamp(20px,5vw,80px)}.auth-frame{width:min(980px,100%);min-height:520px;margin:auto;display:grid;grid-template-columns:1.15fr .85fr;border:1px solid #34393a;background:#101314;clip-path:polygon(0 0,96% 0,100% 7%,100% 100%,4% 100%,0 93%)}.auth-intel{width:auto!important;padding:65px!important;border:0!important;border-right:1px solid #303536!important;background:radial-gradient(circle at 15% 80%,#392b15 0,transparent 33%),#0e1112!important}.auth-intel h1{font-size:66px!important;line-height:.85;letter-spacing:-3px;margin:24px 0}.auth-intel h1 em{font-style:normal;color:#f6a821}.auth-intel>p:not(.eyebrow){max-width:470px;color:#909698;font:15px/1.6 Georgia,serif}.auth-status{display:flex;justify-content:space-between;margin-top:55px;padding-top:14px;border-top:1px solid #303536;color:#626a6c;font-size:7px;letter-spacing:1.2px}.auth-status i{display:inline-block;width:5px;height:5px;border-radius:50%;background:#75c08c;box-shadow:0 0 6px #75c08c;margin-right:5px}.auth-console{width:auto!important;padding:34px!important;border:0!important;background:#141718!important}.auth-console .console-head b{color:#f6a821}.auth-console .discord{min-height:48px;margin-top:18px;padding:10px 18px;display:flex;align-items:center;justify-content:center;gap:14px;background:#5865f2;color:#fff;border:0;border-radius:4px;font-size:15px;font-weight:600;letter-spacing:0;text-transform:none}.auth-console .discord img{display:block;width:27px;height:21px;object-fit:contain;flex:0 0 auto}.auth-console .discord:hover{background:#4752c4;filter:none}.auth-console .discord:active{background:#3c45a5;filter:none}.auth-console .discord:focus-visible{outline-color:#e0e3ff}.auth-console small{margin-top:18px}
.signin-provider-rail{display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:7px}.signin-provider{position:relative;min-width:0;min-height:76px;margin:0;padding:8px 4px 7px;display:grid;grid-template-rows:34px auto;place-items:center;gap:5px;border:1px solid #424849;border-radius:0;background:#101314;color:#dfe1e2;font-family:Arial,Helvetica,sans-serif;font-size:8px;font-style:normal;font-weight:800;letter-spacing:.3px;text-transform:none;cursor:pointer;transition:var(--shd-transition)}.signin-provider-icon{width:34px;height:34px;display:grid;place-items:center}.signin-provider-icon img{display:block;width:21px;height:21px;object-fit:contain}.signin-provider em{position:absolute;right:3px;top:3px;color:#737a7c;font-size:5px;font-style:normal;font-weight:900;letter-spacing:.5px;text-transform:uppercase}.signin-provider:hover:not(:disabled){transform:translateY(-1px);filter:brightness(1.12)}.signin-provider:active:not(:disabled){transform:translateY(2px) scale(.97)}.signin-provider:focus-visible{outline:2px solid var(--shd-orange);outline-offset:2px}.signin-provider:disabled{cursor:not-allowed;opacity:.46;filter:saturate(.55)}.discord-provider{border-color:#5865f2;background:#5865f2;color:#fff}.discord-provider:hover{background:#4752c4;filter:none!important}.discord-provider .signin-provider-icon img{width:25px;height:19px}.google-signin-provider{border-color:#8e918f;background:#131314;color:#e3e3e3}.google-signin-provider .signin-provider-icon{background:#fff}.apple-signin-provider{border-color:#73777a;background:#000;color:#fff}.apple-signin-provider .signin-provider-icon img{width:22px;height:22px}.steam-signin-provider{border-color:#66c0f4;background:#171d25;color:#fff}.steam-signin-provider .signin-provider-icon{color:#fff}.steam-signin-provider .signin-provider-icon img{width:25px;height:25px}
.passkey-signin{position:relative;min-height:48px;margin-top:18px}.passkey-signin[aria-busy=true]{padding-right:42px}.passkey-signin[aria-busy=true]:after{content:"";position:absolute;right:16px;width:12px;height:12px;border:2px solid #111;border-right-color:transparent;border-radius:50%;animation:interaction-spin .65s linear infinite}.auth-note{margin:0;color:var(--shd-text-muted);font-size:8px}.password-fallback{margin:18px 0;border:1px solid var(--shd-border);background:#101314}.password-fallback summary{padding:12px;color:#8e9698;font-size:8px;font-weight:900;letter-spacing:1px;cursor:pointer}.password-fallback summary:hover{color:var(--shd-orange)}.password-fallback form{padding:0 12px 12px}.password-fallback .full{margin-bottom:0} .passkey-signin{position:relative;min-height:48px;margin-top:18px}.passkey-signin[aria-busy=true]{padding-right:42px}.passkey-signin[aria-busy=true]:after{content:"";position:absolute;right:16px;width:12px;height:12px;border:2px solid #111;border-right-color:transparent;border-radius:50%;animation:interaction-spin .65s linear infinite}.auth-note{margin:0;color:var(--shd-text-muted);font-size:8px}.password-fallback{margin:18px 0;border:1px solid var(--shd-border);background:#101314}.password-fallback summary{padding:12px;color:#8e9698;font-size:8px;font-weight:900;letter-spacing:1px;cursor:pointer}.password-fallback summary:hover{color:var(--shd-orange)}.password-fallback form{padding:0 12px 12px}.password-fallback .full{margin-bottom:0}
.builder-page{padding-top:28px}.builder-command{margin-bottom:14px;border-bottom:1px solid #303536}.builder-command .page-title{margin-bottom:12px}.builder-status{display:flex;gap:24px;padding:9px 2px;color:#61696b;font-size:7px;letter-spacing:1.2px}.builder-status i{display:inline-block;width:5px;height:5px;border-radius:50%;background:#75c08c;box-shadow:0 0 6px #75c08c;margin-right:5px}.builder-page .slot-card{position:relative;overflow:hidden}.builder-page .slot-card:after{content:"";position:absolute;right:-24px;bottom:-24px;width:52px;height:52px;border:1px solid #f6a82122;transform:rotate(45deg)}.builder-page .summary{border-top:2px solid #f6a821} .builder-page{padding-top:28px}.builder-command{margin-bottom:14px;border-bottom:1px solid #303536}.builder-command .page-title{margin-bottom:12px}.builder-status{display:flex;gap:24px;padding:9px 2px;color:#61696b;font-size:7px;letter-spacing:1.2px}.builder-status i{display:inline-block;width:5px;height:5px;border-radius:50%;background:#75c08c;box-shadow:0 0 6px #75c08c;margin-right:5px}.builder-page .slot-card{position:relative;overflow:hidden}.builder-page .slot-card:after{content:"";position:absolute;right:-24px;bottom:-24px;width:52px;height:52px;border:1px solid #f6a82122;transform:rotate(45deg)}.builder-page .summary{border-top:2px solid #f6a821}
@media(max-width:900px){.filter-bar{grid-template-columns:1fr 1fr}.catalogue-hero{align-items:flex-start}.guide-layout,.profile-dashboard,.auth-frame{grid-template-columns:1fr}.profile-command{align-items:flex-start}.profile-metrics{display:grid;grid-template-columns:1fr 1fr}.account-console{order:-1}.auth-intel{border-right:0!important;border-bottom:1px solid #303536!important;padding:42px!important}.auth-intel h1{font-size:52px!important}}@media(max-width:560px){.catalogue-hero,.profile-command{flex-direction:column;align-items:flex-start}.catalogue-count{text-align:left;margin-top:20px}.catalogue-count .primary{justify-self:start}.filter-bar{grid-template-columns:1fr}.guide-meta{position:static;margin-top:25px;flex-wrap:wrap}.profile-metrics{width:100%;margin-top:20px}.profile-columns{grid-template-columns:1fr}.auth-page{padding:25px 14px}.auth-intel,.auth-console{padding:28px!important}} @media(max-width:900px){.filter-bar{grid-template-columns:1fr 1fr}.catalogue-hero{align-items:flex-start}.guide-layout,.profile-dashboard,.auth-frame{grid-template-columns:1fr}.profile-command{align-items:flex-start}.profile-metrics{display:grid;grid-template-columns:1fr 1fr}.account-console{order:-1}.auth-intel{border-right:0!important;border-bottom:1px solid #303536!important;padding:42px!important}.auth-intel h1{font-size:52px!important}}@media(max-width:560px){.catalogue-hero,.profile-command{flex-direction:column;align-items:flex-start}.catalogue-count{text-align:left;margin-top:20px}.catalogue-count .primary{justify-self:start}.filter-bar{grid-template-columns:1fr}.guide-meta{position:static;margin-top:25px;flex-wrap:wrap}.profile-metrics{width:100%;margin-top:20px}.profile-columns{grid-template-columns:1fr}.auth-page{padding:25px 14px}.auth-intel,.auth-console{padding:28px!important}}
@media(max-width:420px){.signin-provider-rail{grid-template-columns:repeat(2,minmax(0,1fr))}}
.discord-connection{display:grid;grid-template-columns:1fr;gap:9px;padding:10px}.connection-copy{display:flex!important;align-items:center;gap:10px}.connection-copy>img{width:28px;height:21px;padding:4px;background:#5865f2;border-radius:3px}.discord-link-button{min-height:42px;padding:8px 14px;display:flex;align-items:center;justify-content:center;gap:12px;background:#5865f2;color:#fff!important;border-radius:4px;font-size:14px!important;font-weight:600!important;letter-spacing:0;text-transform:none}.discord-link-button img{display:block;width:25px;height:19px;object-fit:contain}.discord-link-button:hover{background:#4752c4;filter:none}.discord-link-button:active{background:#3c45a5;filter:none}.discord-link-button:focus-visible{outline:2px solid #e0e3ff;outline-offset:3px}.discord-link-button[aria-busy="true"]::after{content:"";width:11px;height:11px;border:2px solid #fff;border-right-color:transparent;border-radius:50%;animation:interaction-spin .65s linear infinite} .discord-connection{display:grid;grid-template-columns:1fr;gap:9px;padding:10px}.connection-copy{display:flex!important;align-items:center;gap:10px}.connection-copy>img{width:28px;height:21px;padding:4px;background:#5865f2;border-radius:3px}.discord-link-button{min-height:42px;padding:8px 14px;display:flex;align-items:center;justify-content:center;gap:12px;background:#5865f2;color:#fff!important;border-radius:4px;font-size:14px!important;font-weight:600!important;letter-spacing:0;text-transform:none}.discord-link-button img{display:block;width:25px;height:19px;object-fit:contain}.discord-link-button:hover{background:#4752c4;filter:none}.discord-link-button:active{background:#3c45a5;filter:none}.discord-link-button:focus-visible{outline:2px solid #e0e3ff;outline-offset:3px}.discord-link-button[aria-busy="true"]::after{content:"";width:11px;height:11px;border:2px solid #fff;border-right-color:transparent;border-radius:50%;animation:interaction-spin .65s linear infinite}
.provider-stack{display:grid;gap:7px}.provider-placeholder{min-height:52px;display:grid;grid-template-columns:34px 1fr auto;align-items:center;gap:9px;padding:8px 10px;border:1px solid var(--shd-border-soft);background:#0d1011;color:#6d7476;opacity:.72}.provider-placeholder>div{display:grid;gap:3px}.provider-placeholder b{font-size:9px;color:#8a9092}.provider-placeholder small{font-size:6px;letter-spacing:.7px}.provider-placeholder em{font-style:normal;font-size:6px;font-weight:900;letter-spacing:1px;color:#626a6c}.provider-monogram{width:28px;height:28px;display:grid;place-items:center;border:1px solid #3a4041;background:#151819;font-size:11px;font-weight:900}.google-provider{color:#8ab4f8}.apple-provider{color:#d7d9d5}.steam-provider{color:#70b9d6}
.provider-connection-card{position:relative;grid-template-columns:1fr auto!important;min-height:64px;overflow:hidden}.connection-state.provider-unlink-trigger{height:auto;padding:5px 7px;background:transparent;font-family:inherit;cursor:pointer;transition:var(--shd-transition)}.connection-state.provider-unlink-trigger span:last-child{display:none}.connection-state.provider-unlink-trigger:hover:not(:disabled),.connection-state.provider-unlink-trigger:focus-visible{border-color:var(--shd-danger);background:#281716;color:var(--shd-danger)}.connection-state.provider-unlink-trigger:hover:not(:disabled) span:first-child,.connection-state.provider-unlink-trigger:focus-visible span:first-child{display:none}.connection-state.provider-unlink-trigger:hover:not(:disabled) span:last-child,.connection-state.provider-unlink-trigger:focus-visible span:last-child{display:inline}.connection-state.provider-unlink-trigger:focus-visible{outline:2px solid var(--shd-danger);outline-offset:2px}.connection-state.provider-unlink-trigger:disabled{cursor:not-allowed;opacity:.55}.provider-unlink-confirm{position:absolute;z-index:2;inset:0;display:grid!important;grid-template-columns:1fr auto;align-items:center;gap:9px;padding:9px 10px;border:1px solid #9a5043;background:#211414}.provider-unlink-confirm>div:first-child{display:grid!important;gap:3px}.provider-unlink-confirm b{color:var(--shd-danger);font-size:8px;letter-spacing:.8px}.provider-unlink-confirm p{margin:0;color:#d6aaa1;font-size:7px;line-height:1.35}.provider-unlink-confirm>div:last-child{display:grid!important;grid-template-columns:74px 104px;gap:6px}.provider-unlink-confirm form,.provider-unlink-confirm fieldset{height:100%}.provider-unlink-confirm button{width:100%;height:32px;padding:0 7px;border-radius:0;appearance:none;font-size:6px}.provider-unlink-confirm .secondary{border:1px solid #565d5f;background:#101314;color:#d7d9d5}.provider-unlink-confirm .secondary:hover{border-color:#8a672b;color:var(--shd-orange)}.provider-unlink-confirm .primary{border:0;background:var(--shd-orange);color:#111}.provider-unlink-confirm .primary:hover{background:var(--shd-orange-hover)}.provider-card-message{grid-column:1/-1;margin:0}
.linked-connection{grid-template-columns:1fr auto}.connection-state{padding:5px 7px;border:1px solid #3f6a4c;color:#75c08c;font-size:7px;font-weight:bold;letter-spacing:1px}.unlink-console{margin-top:8px}.connection-warning{margin:8px 0;color:#c29a60;font-size:8px;line-height:1.4} .linked-connection{grid-template-columns:1fr auto}.connection-state{padding:5px 7px;border:1px solid #3f6a4c;color:#75c08c;font-size:7px;font-weight:bold;letter-spacing:1px}.unlink-console{margin-top:8px}.connection-warning{margin:8px 0;color:#c29a60;font-size:8px;line-height:1.4}
.isac-identity{margin:10px 0 14px}.isac-identity>.label{margin-bottom:6px}.identity-empty{padding:12px!important;border:1px dashed #343a3b} .isac-identity{margin:10px 0 14px}.isac-identity>.label{margin-bottom:6px}.identity-empty{padding:12px!important;border:1px dashed #343a3b}
.isac-avatar-review{margin:10px 0 14px}.isac-avatar-review>.label{margin-bottom:6px}.avatar-review-card{display:grid;grid-template-columns:58px 1fr auto;align-items:center;gap:10px;padding:8px;border:1px solid #303536;background:#0d1011}.avatar-review-card img{width:58px;height:58px;object-fit:cover;border:1px solid #5a4829}.avatar-review-card>div{display:grid;gap:4px}.avatar-review-card b{font-size:9px}.avatar-review-card small{color:#747c7e;font-size:7px;letter-spacing:.4px}.avatar-review-card>span{color:#75c08c;font-size:7px;letter-spacing:1px}.avatar-review-card>span.custom-avatar{color:#f6a821}.avatar-review-note{padding:9px 0 0} .isac-avatar-review{margin:10px 0 14px}.isac-avatar-review>.label{margin-bottom:6px}.avatar-review-card{display:grid;grid-template-columns:58px 1fr auto;align-items:center;gap:10px;padding:8px;border:1px solid #303536;background:#0d1011}.avatar-review-card img{width:58px;height:58px;object-fit:cover;border:1px solid #5a4829}.avatar-review-card>div{display:grid;gap:4px}.avatar-review-card b{font-size:9px}.avatar-review-card small{color:#747c7e;font-size:7px;letter-spacing:.4px}.avatar-review-card>span{color:#75c08c;font-size:7px;letter-spacing:1px}.avatar-review-card>span.custom-avatar{color:#f6a821}.avatar-review-note{padding:9px 0 0}
+2 -6
View File
@@ -234,19 +234,15 @@ export const actions: Actions = {
cookies.delete('shd_session', { path: '/' }); cookies.delete('shd_session', { path: '/' });
redirect(303, '/?account_deleted=1'); redirect(303, '/?account_deleted=1');
}, },
unlinkDiscord: async ({ request, cookies }) => { unlinkDiscord: async ({ cookies }) => {
const user = await getSessionUser(cookies); const user = await getSessionUser(cookies);
if (!user) redirect(303, '/sign-in'); if (!user) redirect(303, '/sign-in');
const password = String((await request.formData()).get('password') ?? '');
const database = await getDatabase(); const database = await getDatabase();
const credential = await (await database.prepare('SELECT password_hash,discord_id FROM users WHERE id=?')).get(user.id); const credential = await (await database.prepare('SELECT discord_id FROM users WHERE id=?')).get(user.id);
if (!credential?.discord_id) return fail(409, { discordUnlinkError: 'No Discord account is linked.' }); if (!credential?.discord_id) return fail(409, { discordUnlinkError: 'No Discord account is linked.' });
if (!canRemoveAuthenticationMethod(await getAuthenticationMethods(database, user.id), 'discord')) { if (!canRemoveAuthenticationMethod(await getAuthenticationMethods(database, user.id), 'discord')) {
return fail(400, { discordUnlinkError: 'Add another sign-in method before unlinking Discord so you do not lose access.' }); return fail(400, { discordUnlinkError: 'Add another sign-in method before unlinking Discord so you do not lose access.' });
} }
if (typeof credential.password_hash === 'string' && !(await verifyPassword(credential.password_hash, password))) {
return fail(400, { discordUnlinkError: 'Your password is incorrect.' });
}
await (await database.prepare('UPDATE users SET discord_id=NULL WHERE id=?')).run(user.id); await (await database.prepare('UPDATE users SET discord_id=NULL WHERE id=?')).run(user.id);
await (await database.prepare('DELETE FROM discord_profiles WHERE user_id=?')).run(user.id); await (await database.prepare('DELETE FROM discord_profiles WHERE user_id=?')).run(user.id);
return { discordUnlinked: true }; return { discordUnlinked: true };
+40 -13
View File
@@ -5,18 +5,26 @@
let { data, form } = $props(); let { data, form } = $props();
const user = $derived(data.user!); const user = $derived(data.user!);
let discordLinkPending = $state(false); let discordLinkPending = $state(false);
type AccountPanel = 'identity' | 'access' | 'connections' | 'account'; let discordUnlinkConfirm = $state(false);
type AccountPanel = 'identity' | 'access' | 'account';
type AccessPanel = 'passkeys' | 'email' | 'local' | 'connections';
type AvatarPanel = 'presets' | 'upload'; type AvatarPanel = 'presets' | 'upload';
function initialAccountPanel(): AccountPanel { function initialAccountPanel(): AccountPanel {
if (form?.discordUnlinkError || data.oauthError || data.discordLinked || form?.discordUnlinked) return 'connections'; if (form?.discordUnlinkError || data.oauthError || data.discordLinked || form?.discordUnlinked || form?.passwordError || form?.loginUsernameError || form?.localCredentialError || form?.emailError || form?.passkeyError || data.emailVerified) return 'access';
if (form?.passwordError || form?.loginUsernameError || form?.localCredentialError || form?.emailError || form?.passkeyError || data.emailVerified) return 'access';
if (form?.deleteError) return 'account'; if (form?.deleteError) return 'account';
return 'identity'; return 'identity';
} }
function initialAccessPanel(): AccessPanel {
if (form?.discordUnlinkError || data.oauthError || data.discordLinked || form?.discordUnlinked) return 'connections';
if (form?.emailError || form?.emailVerificationSent || form?.emailRemoved || data.emailVerified) return 'email';
if (form?.passwordError || form?.loginUsernameError || form?.localCredentialError || form?.localCredentialsAdded || form?.localCredentialsRemoved) return 'local';
return 'passkeys';
}
function initialAvatarPanel(): AvatarPanel { function initialAvatarPanel(): AvatarPanel {
return form?.avatarError ? 'upload' : 'presets'; return form?.avatarError ? 'upload' : 'presets';
} }
let accountPanel = $state<AccountPanel>(initialAccountPanel()); let accountPanel = $state<AccountPanel>(initialAccountPanel());
let accessPanel = $state<AccessPanel>(initialAccessPanel());
let avatarPanel = $state<AvatarPanel>(initialAvatarPanel()); let avatarPanel = $state<AvatarPanel>(initialAvatarPanel());
let passkeySupported = $state(true); let passkeySupported = $state(true);
let passkeyPending = $state(false); let passkeyPending = $state(false);
@@ -61,7 +69,7 @@
</section> </section>
{#if data.chooseDisplayName}<div class="account-alert"><b>IDENTITY SETUP</b><span>Choose the SHD.build display name other agents will see. Unicode characters are supported.</span></div>{/if} {#if data.chooseDisplayName}<div class="account-alert"><b>IDENTITY SETUP</b><span>Choose the SHD.build display name other agents will see. Unicode characters are supported.</span></div>{/if}
{#if data.discordLinked}<p class="success">Discord account linked successfully.</p>{/if} {#if data.discordLinked}<p class="success">Discord account linked successfully.</p>{/if}
{#if form?.discordUnlinked}<p class="success">Discord account unlinked. You can continue signing in with your username and password.</p>{/if} {#if form?.discordUnlinked}<p class="success">Discord account unlinked. Your other configured sign-in methods remain active.</p>{/if}
{#if data.oauthError}<div class="account-alert error-alert" role="alert"><b>{data.oauthError.title}</b><span>{data.oauthError.message}</span></div>{/if} {#if data.oauthError}<div class="account-alert error-alert" role="alert"><b>{data.oauthError.title}</b><span>{data.oauthError.message}</span></div>{/if}
<div class="profile-dashboard"> <div class="profile-dashboard">
<section class="profile-feed"> <section class="profile-feed">
@@ -75,7 +83,6 @@
<div class="account-tabs" aria-label="Account controls" role="tablist"> <div class="account-tabs" aria-label="Account controls" role="tablist">
<button id="identity-tab" class:active={accountPanel === 'identity'} role="tab" aria-selected={accountPanel === 'identity'} aria-controls="identity-panel" onclick={() => accountPanel = 'identity'}>PROFILE</button> <button id="identity-tab" class:active={accountPanel === 'identity'} role="tab" aria-selected={accountPanel === 'identity'} aria-controls="identity-panel" onclick={() => accountPanel = 'identity'}>PROFILE</button>
<button id="access-tab" class:active={accountPanel === 'access'} role="tab" aria-selected={accountPanel === 'access'} aria-controls="access-panel" onclick={() => accountPanel = 'access'}>ACCESS</button> <button id="access-tab" class:active={accountPanel === 'access'} role="tab" aria-selected={accountPanel === 'access'} aria-controls="access-panel" onclick={() => accountPanel = 'access'}>ACCESS</button>
<button id="connections-tab" class:active={accountPanel === 'connections'} role="tab" aria-selected={accountPanel === 'connections'} aria-controls="connections-panel" onclick={() => accountPanel = 'connections'}>LINKS</button>
<button id="account-tab" class:active={accountPanel === 'account'} role="tab" aria-selected={accountPanel === 'account'} aria-controls="account-panel" onclick={() => accountPanel = 'account'}>ACCOUNT</button> <button id="account-tab" class:active={accountPanel === 'account'} role="tab" aria-selected={accountPanel === 'account'} aria-controls="account-panel" onclick={() => accountPanel = 'account'}>ACCOUNT</button>
</div> </div>
@@ -112,6 +119,13 @@
</div> </div>
{:else if accountPanel === 'access'} {:else if accountPanel === 'access'}
<div class="account-panel" id="access-panel" role="tabpanel" aria-labelledby="access-tab"> <div class="account-panel" id="access-panel" role="tabpanel" aria-labelledby="access-tab">
<div class="access-subtabs" role="tablist" aria-label="Sign-in method controls">
<button class:active={accessPanel === 'passkeys'} role="tab" aria-selected={accessPanel === 'passkeys'} onclick={() => accessPanel = 'passkeys'}><span>PASSKEYS</span><b>{data.passkeys.length}</b></button>
<button class:active={accessPanel === 'email'} role="tab" aria-selected={accessPanel === 'email'} onclick={() => accessPanel = 'email'}><span>EMAIL</span><b>{data.emailIdentity ? 1 : 0}</b></button>
<button class:active={accessPanel === 'local'} role="tab" aria-selected={accessPanel === 'local'} onclick={() => accessPanel = 'local'}><span>PASSWORD</span><b>{data.hasLocalCredentials ? 1 : 0}</b></button>
<button class:active={accessPanel === 'connections'} role="tab" aria-selected={accessPanel === 'connections'} onclick={() => accessPanel = 'connections'}><span>LINKS</span><b>{data.discordConnection ? 1 : 0}</b></button>
</div>
{#if accessPanel === 'passkeys'}
<p class="eyebrow">PREFERRED ACCESS</p><h2>PASSKEYS</h2> <p class="eyebrow">PREFERRED ACCESS</p><h2>PASSKEYS</h2>
<p class="identity-note passkey-intro">Use Face ID, Touch ID, Windows Hello, a security key, or your password manager without entering a SHD.build password.</p> <p class="identity-note passkey-intro">Use Face ID, Touch ID, Windows Hello, a security key, or your password manager without entering a SHD.build password.</p>
{#if data.passkeys.length} {#if data.passkeys.length}
@@ -128,10 +142,12 @@
<label class="field">PASSKEY NAME<input bind:value={passkeyName} maxlength="60" placeholder="e.g. Windows Hello" /></label> <label class="field">PASSKEY NAME<input bind:value={passkeyName} maxlength="60" placeholder="e.g. Windows Hello" /></label>
<button class="primary console-action" disabled={!passkeySupported || passkeyPending || !passkeyName.trim()} aria-busy={passkeyPending} onclick={registerPasskey}>{passkeyPending ? 'REGISTERING…' : 'ADD A PASSKEY →'}</button> <button class="primary console-action" disabled={!passkeySupported || passkeyPending || !passkeyName.trim()} aria-busy={passkeyPending} onclick={registerPasskey}>{passkeyPending ? 'REGISTERING…' : 'ADD A PASSKEY →'}</button>
{#if !passkeySupported}<p class="connection-warning">This browser does not support passkeys.</p>{/if} {#if !passkeySupported}<p class="connection-warning">This browser does not support passkeys.</p>{/if}
<hr/><p class="eyebrow">PASSWORDLESS RECOVERY</p><h2>MAGIC LINK EMAIL</h2> {:else if accessPanel === 'email'}
<p class="eyebrow">PASSWORDLESS RECOVERY</p><h2>MAGIC LINK EMAIL</h2>
{#if data.emailIdentity}<div class="verified-email"><div><b>{data.emailIdentity.email}</b><small>VERIFIED EMAIL IDENTITY</small></div><span>VERIFIED</span></div><PendingForm action="?/removeEmailIdentity"><button class="secondary console-action" disabled={data.authenticationMethods.total <= 1}>REMOVE MAGIC-LINK EMAIL</button></PendingForm>{/if} {#if data.emailIdentity}<div class="verified-email"><div><b>{data.emailIdentity.email}</b><small>VERIFIED EMAIL IDENTITY</small></div><span>VERIFIED</span></div><PendingForm action="?/removeEmailIdentity"><button class="secondary console-action" disabled={data.authenticationMethods.total <= 1}>REMOVE MAGIC-LINK EMAIL</button></PendingForm>{/if}
<PendingForm action="?/requestEmailVerification"><label class="field">{data.emailIdentity ? 'CHANGE VERIFIED EMAIL' : 'EMAIL ADDRESS'}<input name="email" type="email" value={data.emailIdentity?.email ?? ''} autocomplete="email" required /></label><small class="identity-note">We will email a verification link before this address can be used to sign in.</small>{#if form?.emailError}<p class="error">{form.emailError}</p>{/if}{#if form?.emailVerificationSent}<p class="success">Verification link sent. It expires in 15 minutes.</p>{/if}{#if data.emailVerified}<p class="success">Email verified. Magic-link sign-in is now active.</p>{/if}<button class="secondary console-action" disabled={!data.emailDeliveryConfigured}>SEND VERIFICATION LINK →</button></PendingForm> <PendingForm action="?/requestEmailVerification"><label class="field">{data.emailIdentity ? 'CHANGE VERIFIED EMAIL' : 'EMAIL ADDRESS'}<input name="email" type="email" value={data.emailIdentity?.email ?? ''} autocomplete="email" required /></label><small class="identity-note">We will email a verification link before this address can be used to sign in.</small>{#if form?.emailError}<p class="error">{form.emailError}</p>{/if}{#if form?.emailVerificationSent}<p class="success">Verification link sent. It expires in 15 minutes.</p>{/if}{#if data.emailVerified}<p class="success">Email verified. Magic-link sign-in is now active.</p>{/if}<button class="secondary console-action" disabled={!data.emailDeliveryConfigured}>SEND VERIFICATION LINK →</button></PendingForm>
<hr/><p class="eyebrow">OPTIONAL FALLBACK</p><h2>USERNAME &amp; PASSWORD</h2> {:else if accessPanel === 'local'}
<p class="eyebrow">OPTIONAL FALLBACK</p><h2>USERNAME &amp; PASSWORD</h2>
{#if data.hasLocalCredentials} {#if data.hasLocalCredentials}
<PendingForm action="?/changeLoginUsername"><label class="field">PRIVATE LOGIN USERNAME<input name="username" value={user.loginUsername ?? ''} autocomplete="username" required /></label><small class="identity-note">Used only to sign in and visible to you and authorized ISAC operators.</small>{#if form?.loginUsernameError}<p class="error">{form.loginUsernameError}</p>{/if}<button class="secondary console-action">UPDATE LOGIN USERNAME →</button></PendingForm> <PendingForm action="?/changeLoginUsername"><label class="field">PRIVATE LOGIN USERNAME<input name="username" value={user.loginUsername ?? ''} autocomplete="username" required /></label><small class="identity-note">Used only to sign in and visible to you and authorized ISAC operators.</small>{#if form?.loginUsernameError}<p class="error">{form.loginUsernameError}</p>{/if}<button class="secondary console-action">UPDATE LOGIN USERNAME →</button></PendingForm>
<hr/><PendingForm action="?/changePassword"><label class="field">CURRENT PASSWORD<input name="currentPassword" type="password" autocomplete="current-password" required /></label><label class="field">NEW PASSWORD<input name="newPassword" type="password" autocomplete="new-password" minlength="12" required /></label><label class="field">CONFIRM PASSWORD<input name="confirmPassword" type="password" autocomplete="new-password" minlength="12" required /></label>{#if form?.passwordError}<p class="error">{form.passwordError}</p>{/if}<button class="primary console-action">UPDATE PASSWORD →</button></PendingForm> <hr/><PendingForm action="?/changePassword"><label class="field">CURRENT PASSWORD<input name="currentPassword" type="password" autocomplete="current-password" required /></label><label class="field">NEW PASSWORD<input name="newPassword" type="password" autocomplete="new-password" minlength="12" required /></label><label class="field">CONFIRM PASSWORD<input name="confirmPassword" type="password" autocomplete="new-password" minlength="12" required /></label>{#if form?.passwordError}<p class="error">{form.passwordError}</p>{/if}<button class="primary console-action">UPDATE PASSWORD →</button></PendingForm>
@@ -140,16 +156,27 @@
<p class="account-method-empty">USERNAME AND PASSWORD SIGN-IN IS NOT CONFIGURED</p> <p class="account-method-empty">USERNAME AND PASSWORD SIGN-IN IS NOT CONFIGURED</p>
<PendingForm action="?/addLocalCredentials"><label class="field">PRIVATE LOGIN USERNAME<input name="username" autocomplete="username" required /></label><label class="field">PASSWORD<input name="newPassword" type="password" autocomplete="new-password" minlength="12" required /></label><label class="field">CONFIRM PASSWORD<input name="confirmPassword" type="password" autocomplete="new-password" minlength="12" required /></label>{#if form?.localCredentialError}<p class="error">{form.localCredentialError}</p>{/if}{#if form?.localCredentialsAdded}<p class="success">Username and password sign-in added.</p>{/if}<button class="secondary console-action">ADD PASSWORD FALLBACK →</button></PendingForm> <PendingForm action="?/addLocalCredentials"><label class="field">PRIVATE LOGIN USERNAME<input name="username" autocomplete="username" required /></label><label class="field">PASSWORD<input name="newPassword" type="password" autocomplete="new-password" minlength="12" required /></label><label class="field">CONFIRM PASSWORD<input name="confirmPassword" type="password" autocomplete="new-password" minlength="12" required /></label>{#if form?.localCredentialError}<p class="error">{form.localCredentialError}</p>{/if}{#if form?.localCredentialsAdded}<p class="success">Username and password sign-in added.</p>{/if}<button class="secondary console-action">ADD PASSWORD FALLBACK →</button></PendingForm>
{/if} {/if}
</div> {:else}
{:else if accountPanel === 'connections'} <p class="eyebrow">CONNECTIONS</p><h2>SIGN-IN PROVIDERS</h2>
<div class="account-panel" id="connections-panel" role="tabpanel" aria-labelledby="connections-tab"> <div class="provider-stack">
<p class="eyebrow">CONNECTIONS</p><h2>SIGN-IN METHODS</h2>
{#if data.discordConnection} {#if data.discordConnection}
<div class="connection-card discord-connection linked-connection"><div class="connection-copy"><img src="/discord-symbol.svg" alt="" aria-hidden="true" /><div><b>{data.discordConnection.displayName ?? data.discordConnection.username ?? 'DISCORD USER'}</b><small>{data.discordConnection.username ? `@${data.discordConnection.username} · ` : ''}ID {data.discordConnection.id}</small></div></div><span class="connection-state">LINKED</span></div> <div class="connection-card discord-connection linked-connection provider-connection-card" class:confirming={discordUnlinkConfirm}>
<PendingForm action="?/unlinkDiscord" class="unlink-console">{#if data.hasPassword}<label class="field">PASSWORD TO UNLINK<input name="password" type="password" autocomplete="current-password" required/></label>{/if}{#if !data.discordConnection.canUnlink}<p class="connection-warning">Discord is your only sign-in method. Add a passkey, verified email, or password in Access before unlinking.</p>{/if}{#if form?.discordUnlinkError}<p class="error">{form.discordUnlinkError}</p>{/if}<button class="secondary console-action" disabled={!data.discordConnection.canUnlink}>UNLINK DISCORD</button></PendingForm> <div class="connection-copy"><img src="/discord-symbol.svg" alt="" aria-hidden="true" /><div><b>{data.discordConnection.displayName ?? data.discordConnection.username ?? 'DISCORD USER'}</b><small>{data.discordConnection.username ? `@${data.discordConnection.username} · ` : ''}ID {data.discordConnection.id}</small></div></div>
<button class="connection-state provider-unlink-trigger" disabled={!data.discordConnection.canUnlink} aria-label="Unlink Discord" onclick={() => discordUnlinkConfirm = true}><span>LINKED</span><span>UNLINK</span></button>
{#if discordUnlinkConfirm}
<div class="provider-unlink-confirm"><div><b>UNLINK DISCORD?</b><p>Disconnect this identity from your SHD.build account?</p></div><div><button class="secondary" onclick={() => discordUnlinkConfirm = false}>CANCEL</button><PendingForm action="?/unlinkDiscord"><button class="primary">CONFIRM UNLINK</button></PendingForm></div></div>
{/if}
{#if !data.discordConnection.canUnlink}<p class="connection-warning provider-card-message">Discord is your only sign-in method. Add a passkey, verified email, or password before unlinking.</p>{/if}
{#if form?.discordUnlinkError}<p class="error provider-card-message">{form.discordUnlinkError}</p>{/if}
</div>
{:else} {:else}
<div class="connection-card discord-connection"><div class="connection-copy"><img src="/discord-symbol.svg" alt="" aria-hidden="true" /><div><b>DISCORD</b><small>OAuth identity provider</small></div></div><a class="discord-link-button" class:interaction-pending={discordLinkPending} aria-busy={discordLinkPending} href="/auth/discord?mode=link" onclick={beginDiscordLink}><img src="/discord-symbol.svg" alt="" aria-hidden="true" /><span>Link Discord</span></a></div> <div class="connection-card discord-connection"><div class="connection-copy"><img src="/discord-symbol.svg" alt="" aria-hidden="true" /><div><b>DISCORD</b><small>OAuth identity provider</small></div></div><a class="discord-link-button" class:interaction-pending={discordLinkPending} aria-busy={discordLinkPending} href="/auth/discord?mode=link" onclick={beginDiscordLink}><img src="/discord-symbol.svg" alt="" aria-hidden="true" /><span>Link Discord</span></a></div>
{/if} {/if}
<div class="provider-placeholder"><span class="provider-monogram google-provider">G</span><div><b>GOOGLE</b><small>ACCOUNT LINKING PLACEHOLDER</small></div><em>PLANNED</em></div>
<div class="provider-placeholder"><span class="provider-monogram apple-provider">A</span><div><b>APPLE</b><small>ACCOUNT LINKING PLACEHOLDER</small></div><em>PLANNED</em></div>
<div class="provider-placeholder"><span class="provider-monogram steam-provider">S</span><div><b>STEAM</b><small>ACCOUNT LINKING PLACEHOLDER</small></div><em>PLANNED</em></div>
</div>
{/if}
</div> </div>
{:else} {:else}
<div class="account-panel" id="account-panel" role="tabpanel" aria-labelledby="account-tab"> <div class="account-panel" id="account-panel" role="tabpanel" aria-labelledby="account-tab">
+21 -1
View File
@@ -45,7 +45,27 @@
<div class="divider"><span>OR EMAIL A ONE-TIME LINK</span></div> <div class="divider"><span>OR EMAIL A ONE-TIME LINK</span></div>
<PendingForm action="?/magicLink"><label class="field">VERIFIED EMAIL<input name="email" type="email" value={form?.email ?? ''} autocomplete="email" placeholder="agent@example.com" required /></label>{#if form?.magicLinkError}<p class="error">{form.magicLinkError}</p>{/if}{#if form?.magicLinkSent || data.magicLinkSent}<p class="success">If that email is linked to an agent, a sign-in link is on its way.</p>{/if}{#if data.magicError}<p class="error">{data.magicError}</p>{/if}<button class="secondary full" disabled={!data.emailDeliveryConfigured}>EMAIL MAGIC LINK →</button></PendingForm> <PendingForm action="?/magicLink"><label class="field">VERIFIED EMAIL<input name="email" type="email" value={form?.email ?? ''} autocomplete="email" placeholder="agent@example.com" required /></label>{#if form?.magicLinkError}<p class="error">{form.magicLinkError}</p>{/if}{#if form?.magicLinkSent || data.magicLinkSent}<p class="success">If that email is linked to an agent, a sign-in link is on its way.</p>{/if}{#if data.magicError}<p class="error">{data.magicError}</p>{/if}<button class="secondary full" disabled={!data.emailDeliveryConfigured}>EMAIL MAGIC LINK →</button></PendingForm>
<div class="divider"><span>OR CONTINUE WITH</span></div> <div class="divider"><span>OR CONTINUE WITH</span></div>
<a class="discord" class:interaction-pending={discordPending} href="/auth/discord" aria-busy={discordPending} onclick={beginDiscordSignIn}><img src="/discord-symbol.svg" alt="" aria-hidden="true" /><span>Continue with Discord</span></a> <div class="signin-provider-rail" aria-label="External sign-in providers">
<a class="signin-provider discord-provider" class:interaction-pending={discordPending} href="/auth/discord" aria-busy={discordPending} onclick={beginDiscordSignIn}>
<span class="signin-provider-icon"><img src="/discord-symbol.svg" alt="" aria-hidden="true" /></span>
<span>Discord</span>
</a>
<button class="signin-provider google-signin-provider" type="button" disabled title="Google sign-in is planned" aria-label="Google sign-in — planned">
<span class="signin-provider-icon"><img src="/google-g.svg" alt="" aria-hidden="true" /></span>
<span>Google</span>
<em>Planned</em>
</button>
<button class="signin-provider apple-signin-provider" type="button" disabled title="Apple sign-in is planned" aria-label="Apple sign-in — planned">
<span class="signin-provider-icon"><img src="/apple-mark.svg" alt="" aria-hidden="true" /></span>
<span>Apple</span>
<em>Planned</em>
</button>
<button class="signin-provider steam-signin-provider" type="button" disabled title="Steam sign-in is planned" aria-label="Steam sign-in — planned">
<span class="signin-provider-icon"><img src="/steam-mark.svg" alt="" aria-hidden="true" /></span>
<span>Steam</span>
<em>Planned</em>
</button>
</div>
{#if data.oauthError}<p class="error">{data.oauthError}</p>{/if} {#if data.oauthError}<p class="error">{data.oauthError}</p>{/if}
<details class="password-fallback" open={Boolean(form?.loginError)}><summary>USE USERNAME AND PASSWORD</summary><PendingForm action="?/login"><label class="field">USERNAME<input name="username" value={form?.username ?? ''} autocomplete="username webauthn" placeholder="Agent handle" required /></label><label class="field">PASSWORD<input name="password" type="password" autocomplete="current-password" placeholder="••••••••••••" required /></label>{#if form?.loginError}<p class="error">{form.loginError}</p>{/if}<button class="secondary full">SIGN IN WITH PASSWORD →</button></PendingForm></details> <details class="password-fallback" open={Boolean(form?.loginError)}><summary>USE USERNAME AND PASSWORD</summary><PendingForm action="?/login"><label class="field">USERNAME<input name="username" value={form?.username ?? ''} autocomplete="username webauthn" placeholder="Agent handle" required /></label><label class="field">PASSWORD<input name="password" type="password" autocomplete="current-password" placeholder="••••••••••••" required /></label>{#if form?.loginError}<p class="error">{form.loginError}</p>{/if}<button class="secondary full">SIGN IN WITH PASSWORD →</button></PendingForm></details>
<small>Passkeys and one-time email links are preferred. Password sign-in remains available as an optional fallback.</small> <small>Passkeys and one-time email links are preferred. Password sign-in remains available as an optional fallback.</small>
+3
View File
@@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" role="img" aria-label="Apple">
<path fill="currentColor" d="M17.05 12.54c-.03-3.05 2.49-4.53 2.61-4.6a5.6 5.6 0 0 0-4.41-2.39c-1.85-.2-3.65 1.11-4.59 1.11-.96 0-2.41-1.09-3.97-1.06a5.83 5.83 0 0 0-4.91 2.99c-2.13 3.69-.54 9.12 1.5 12.1 1.02 1.46 2.21 3.1 3.77 3.04 1.53-.06 2.11-.98 3.96-.98 1.83 0 2.38.98 3.98.94 1.64-.03 2.68-1.47 3.66-2.94a12.1 12.1 0 0 0 1.67-3.4 5.25 5.25 0 0 1-3.27-4.81ZM14.04 3.59A5.3 5.3 0 0 0 15.25 0a5.39 5.39 0 0 0-3.48 1.71 5.05 5.05 0 0 0-1.25 3.45 4.45 4.45 0 0 0 3.52-1.57Z"/>
</svg>

After

Width:  |  Height:  |  Size: 580 B

+6
View File
@@ -0,0 +1,6 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 18 18" role="img" aria-label="Google">
<path fill="#EA4335" d="M17.64 9.205c0-.638-.057-1.252-.164-1.841H9v3.481h4.844a4.14 4.14 0 0 1-1.797 2.715v2.259h2.909c1.702-1.567 2.684-3.878 2.684-6.614Z"/>
<path fill="#4285F4" d="M9 18c2.43 0 4.468-.806 5.956-2.181l-2.909-2.259c-.806.54-1.836.86-3.047.86-2.344 0-4.328-1.585-5.037-3.714H.956v2.332A8.999 8.999 0 0 0 9 18Z"/>
<path fill="#FBBC05" d="M3.963 10.706A5.41 5.41 0 0 1 3.681 9c0-.592.102-1.168.282-1.706V4.962H.956A8.996 8.996 0 0 0 0 9c0 1.452.347 2.827.956 4.038l3.007-2.332Z"/>
<path fill="#34A853" d="M9 3.58c1.322 0 2.508.454 3.441 1.346l2.581-2.581C13.464.892 11.426 0 9 0A8.999 8.999 0 0 0 .956 4.962l3.007 2.332C4.672 5.165 6.656 3.58 9 3.58Z"/>
</svg>

After

Width:  |  Height:  |  Size: 776 B

+3
View File
@@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" role="img" aria-label="Steam">
<path fill="currentColor" d="M12 0a12 12 0 0 0-12 11.44l6.45 2.67a3.39 3.39 0 0 1 1.92-.58l2.88-4.18v-.06a4.52 4.52 0 1 1 4.52 4.52h-.1l-4.1 2.93a3.4 3.4 0 0 1-6.66.7L.3 15.53A12 12 0 1 0 12 0ZM8.3 18.89l-1.48-.61a2.55 2.55 0 0 0 4.77-.4 2.55 2.55 0 0 0-1.47-3.29 2.49 2.49 0 0 0-1.95.01l1.53.63a1.88 1.88 0 1 1-1.4 3.66Zm7.47-6.59a3.01 3.01 0 1 1 0-6.02 3.01 3.01 0 0 1 0 6.02Zm0-.75a2.26 2.26 0 1 0 0-4.52 2.26 2.26 0 0 0 0 4.52Z"/>
</svg>

After

Width:  |  Height:  |  Size: 535 B