diff --git a/app/igneum-wallet/ui/app.css b/app/igneum-wallet/ui/app.css index 356b35332..ab7d2e8f7 100644 --- a/app/igneum-wallet/ui/app.css +++ b/app/igneum-wallet/ui/app.css @@ -1,5 +1,10 @@ -/* Igneum Miner dashboard. The site's tokens (site/index.html): obsidian, graphite, ember, molten, bone, ash; - Unbounded for headings, IBM Plex Sans for text, IBM Plex Mono for numbers and labels. Fonts ship in the binary. */ +/* Igneum Wallet window (wallet-ui-3, on the miner's system: app/igneum-app/ui/app.css, miner-ui-3). The site's tokens: + obsidian, graphite, ember, molten, bone, ash; Unbounded for the page title, the balance and the row's amount; IBM Plex + Sans for words; IBM Plex Mono for labels, units, addresses and small numbers. Fonts ship in the binary. One type scale + (--t-*), one spacing scale (--s-*), one accent (ember). Dark by default, light under prefers-color-scheme or + [data-theme=light]. Layout: a rail on the left (five sections), a thin top bar, the status strip under it, the page in + the middle. The window lays out from 900 x 620 (the hosts' minimum); under 720 px the rail becomes a bottom tab bar. + Nothing here is newer than 2022 CSS (the WebView2 host). */ @font-face{font-family:'IBM Plex Mono';font-style:normal;font-weight:400;font-display:swap;src:url(fonts/IBMPlexMono-400.woff2) format('woff2')} @font-face{font-family:'IBM Plex Mono';font-style:normal;font-weight:500;font-display:swap;src:url(fonts/IBMPlexMono-500.woff2) format('woff2')} @font-face{font-family:'IBM Plex Sans';font-style:normal;font-weight:400;font-display:swap;src:url(fonts/IBMPlexSans-400.woff2) format('woff2')} @@ -9,426 +14,310 @@ @font-face{font-family:'Unbounded';font-style:normal;font-weight:700;font-display:swap;src:url(fonts/Unbounded-700.woff2) format('woff2')} @font-face{font-family:'Unbounded';font-style:normal;font-weight:900;font-display:swap;src:url(fonts/Unbounded-900.woff2) format('woff2')} -:root{--obsidian:#0C0C0E;--graphite:#16161A;--line:#2A2A30;--line-2:#3A3A42;--ember:#F2541B;--molten:#FFB35C;--bone:#F4F1EC;--ash:#9A9A9E;--ink-2:#C9C7C2;--ember-ink:#0C0C0E; ---gutter:28px;--card-pad:22px;--card-r:18px;--tile-pad:18px 20px;--tile-r:14px;--gap:20px;--gap-tile:12px; ---sans:'IBM Plex Sans',system-ui,-apple-system,sans-serif;--mono:'IBM Plex Mono',ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;--head:'Unbounded',sans-serif; ---top:60px;--bottom:52px} +:root{ + /* colour, dark */ + --obsidian:#0C0C0E;--graphite:#16161A;--row:#111114;--line:#2A2A30;--line-2:#3A3A42;--ember:#F2541B;--ember-hi:#FF6A2B;--molten:#FFB35C;--bone:#F4F1EC;--ash:#9A9A9E;--ink-2:#C9C7C2;--ember-ink:#0C0C0E; + --ember-12:rgba(242,84,27,.12);--ember-40:rgba(242,84,27,.4);--molten-10:rgba(255,179,92,.1);--molten-40:rgba(255,179,92,.4);--rail-bg:#111114;--hover:rgba(255,255,255,.04);--top-bg:rgba(12,12,14,.86);--shadow:rgba(0,0,0,.6);--scrim:rgba(12,12,14,.72);--qr-bg:#F4F1EC; + /* type scale */ + --t-xs:11px;--t-sm:12px;--t-base:13px;--t-md:14px;--t-lg:15px;--t-xl:17px;--t-num:24px;--t-hero:34px;--t-h2:28px;--t-h1:44px; + /* spacing scale */ + --s-1:4px;--s-2:8px;--s-3:12px;--s-4:16px;--s-5:24px;--s-6:32px; + --gutter:var(--s-6);--card-pad:var(--s-5);--card-r:16px;--row-r:12px;--gap:var(--s-4); + --sans:'IBM Plex Sans',system-ui,-apple-system,sans-serif;--mono:'IBM Plex Mono',ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;--head:'Unbounded',sans-serif; + --top:60px;--bottom:0px;--rail:196px} +@media (prefers-color-scheme:light){:root:not([data-theme="dark"]){ + --obsidian:#F4F1EC;--graphite:#FFFFFF;--row:#FAF8F5;--line:#E2DED8;--line-2:#CFCAC2;--ember:#E04A14;--ember-hi:#F2541B;--molten:#B8731F;--bone:#16161A;--ash:#6B6B70;--ink-2:#3C3C42;--ember-ink:#FFFFFF; + --ember-12:rgba(224,74,20,.1);--ember-40:rgba(224,74,20,.4);--molten-10:rgba(184,115,31,.1);--molten-40:rgba(184,115,31,.4);--rail-bg:#EFEBE4;--hover:rgba(0,0,0,.04);--top-bg:rgba(244,241,236,.88);--shadow:rgba(0,0,0,.18);--scrim:rgba(244,241,236,.72);--qr-bg:#FFFFFF}} +:root[data-theme="light"]{ + --obsidian:#F4F1EC;--graphite:#FFFFFF;--row:#FAF8F5;--line:#E2DED8;--line-2:#CFCAC2;--ember:#E04A14;--ember-hi:#F2541B;--molten:#B8731F;--bone:#16161A;--ash:#6B6B70;--ink-2:#3C3C42;--ember-ink:#FFFFFF; + --ember-12:rgba(224,74,20,.1);--ember-40:rgba(224,74,20,.4);--molten-10:rgba(184,115,31,.1);--molten-40:rgba(184,115,31,.4);--rail-bg:#EFEBE4;--hover:rgba(0,0,0,.04);--top-bg:rgba(244,241,236,.88);--shadow:rgba(0,0,0,.18);--scrim:rgba(244,241,236,.72);--qr-bg:#FFFFFF} *{box-sizing:border-box} html,body{height:100%} -body{margin:0;background:var(--obsidian);color:var(--bone);font-family:var(--sans);font-size:15px;line-height:1.5;-webkit-font-smoothing:antialiased;overflow:hidden;user-select:none;-webkit-user-select:none} -.mono,code,pre{font-family:var(--mono)} +body{margin:0;background:var(--obsidian);color:var(--bone);font-family:var(--sans);font-size:var(--t-md);line-height:1.5;-webkit-font-smoothing:antialiased;overflow:hidden;user-select:none;-webkit-user-select:none;font-variant-numeric:tabular-nums} +.mono,code,pre{font-family:var(--mono);font-variant-numeric:tabular-nums} .dim{color:var(--ash)} h1,h2,h3{font-family:var(--head);margin:0;line-height:1.1;text-wrap:balance} -h1{font-weight:900;font-size:52px;letter-spacing:-.01em} -h2{font-weight:700;font-size:32px} -h3{font-weight:700;font-size:16px} +h1{font-weight:900;font-size:var(--t-h1);letter-spacing:-.01em} +h2{font-weight:700;font-size:var(--t-h2)} +h3{font-weight:700;font-size:var(--t-xl)} p{margin:0} a{color:inherit} button{font:inherit;color:inherit} -.eyebrow{font-family:var(--mono);font-size:11px;letter-spacing:.18em;text-transform:uppercase;color:var(--ash);display:inline-flex;align-items:center;gap:8px} +input,textarea,select{font-variant-numeric:tabular-nums} +.eyebrow{font-family:var(--mono);font-size:var(--t-xs);letter-spacing:.16em;text-transform:uppercase;color:var(--ash);display:inline-flex;align-items:center;gap:var(--s-2);white-space:nowrap} .eyebrow.ember{color:var(--ember)} [hidden]{display:none !important} +::selection{background:rgba(242,84,27,.45)} /* buttons */ -.btn{display:inline-flex;align-items:center;justify-content:center;gap:8px;min-height:44px;padding:10px 20px;border-radius:10px;font-weight:600;font-size:15px;border:1px solid var(--line-2);color:var(--bone);background:transparent;cursor:pointer;transition:transform .15s ease,background .15s ease,border-color .15s ease,opacity .15s ease;white-space:nowrap} +.btn{display:inline-flex;align-items:center;justify-content:center;gap:var(--s-2);min-height:40px;padding:8px 18px;border-radius:10px;font-weight:600;font-size:var(--t-md);border:1px solid var(--line-2);color:var(--bone);background:transparent;cursor:pointer;transition:transform .15s ease,background .15s ease,border-color .15s ease,opacity .15s ease,color .15s ease;white-space:nowrap} .btn:hover{transform:translateY(-1px);border-color:var(--ash)} .btn:active{transform:none} .btn:disabled{opacity:.4;cursor:default;transform:none} .btn.primary{background:var(--ember);color:var(--ember-ink);border-color:var(--ember)} -.btn.primary:hover{background:#FF6A2B;border-color:#FF6A2B} -.btn.big{min-height:52px;padding:12px 28px;font-size:16px} -.btn.small{min-height:34px;padding:6px 14px;font-size:13px;border-radius:8px} -.btn.tiny{min-height:26px;padding:2px 10px;font-size:12px;border-radius:7px;font-family:var(--mono);font-weight:500} +.btn.primary:hover{background:var(--ember-hi);border-color:var(--ember-hi)} +.btn.big{min-height:52px;padding:12px 28px;font-size:var(--t-xl)} +.btn.small{min-height:34px;padding:6px 14px;font-size:var(--t-base);border-radius:8px} +.btn.tiny{min-height:26px;padding:2px 10px;font-size:var(--t-sm);border-radius:7px;font-family:var(--mono);font-weight:500} .btn.ghost{border-color:transparent;color:var(--ink-2)} .btn.ghost:hover{border-color:var(--line-2);color:var(--bone)} .btn.danger:hover{color:var(--ember);border-color:var(--ember)} -.icon-btn{width:38px;height:38px;border-radius:10px;border:1px solid var(--line-2);background:transparent;display:inline-flex;align-items:center;justify-content:center;cursor:pointer;color:var(--ink-2);font-size:20px;line-height:1} -.icon-btn:hover{color:var(--bone);border-color:var(--ash)} +.btn.fp svg{flex:0 0 auto} :focus-visible{outline:2px solid var(--ember);outline-offset:3px;border-radius:6px} +@media (prefers-reduced-motion:reduce){.btn{transition:none}} + +/* the rail */ +.rail{position:fixed;top:0;left:0;bottom:0;width:var(--rail);background:var(--rail-bg);border-right:1px solid var(--line);display:flex;flex-direction:column;z-index:22;padding:14px 12px 14px;-webkit-app-region:drag} +.rail button{-webkit-app-region:no-drag} +.rail-brand{display:flex;align-items:center;gap:10px;padding:6px 10px 18px;min-width:0} +body.mac .rail-brand{padding-top:30px} +.rail-brand .word{font-family:var(--head);font-weight:900;font-size:17px;letter-spacing:.06em} +.rail-nav{display:flex;flex-direction:column;gap:3px} +.nav{position:relative;display:flex;align-items:center;gap:12px;width:100%;min-height:42px;padding:8px 12px;border:1px solid transparent;border-radius:11px;background:transparent;color:var(--ink-2);font-weight:500;font-size:var(--t-md);text-align:left;cursor:pointer;transition:background .15s ease,color .15s ease,border-color .15s ease} +.nav svg{width:19px;height:19px;flex:0 0 19px;fill:none;stroke:currentColor;stroke-width:1.9;stroke-linecap:round;stroke-linejoin:round;color:var(--ash);transition:color .15s ease} +.nav:hover{background:var(--hover);color:var(--bone)} +.nav:hover svg{color:var(--ink-2)} +.nav.on{background:var(--ember-12);border-color:var(--ember-40);color:var(--bone);font-weight:600} +.nav.on svg{color:var(--ember)} +.nav.on::before{content:"";position:absolute;left:-13px;top:10px;bottom:10px;width:3px;border-radius:0 3px 3px 0;background:var(--ember)} +.nav.small{min-height:36px;font-size:var(--t-base);color:var(--ash)} +.nav.small svg{width:16px;height:16px;flex-basis:16px} +.nav.danger:hover{color:var(--ember)} +.nav.danger:hover svg{color:var(--ember)} +.nav-dot{position:absolute;right:12px;top:50%;width:7px;height:7px;margin-top:-3px;border-radius:50%;background:var(--molten);box-shadow:0 0 8px rgba(255,179,92,.7)} +.rail-foot{margin-top:auto;display:flex;flex-direction:column;gap:2px;padding-top:12px;border-top:1px solid var(--line)} +.rail-status{font-size:var(--t-xs);color:var(--ash);padding:0 12px 10px;line-height:1.55;overflow:hidden;text-overflow:ellipsis;white-space:nowrap} +.rail-status b{color:var(--ink-2);font-weight:500} +.rail-version{font-size:var(--t-xs);color:var(--ash);padding:8px 12px 0;letter-spacing:.06em} /* top bar */ -.top{position:fixed;top:0;left:0;right:0;height:var(--top);display:flex;align-items:center;justify-content:space-between;padding:0 var(--gutter);background:rgba(12,12,14,.86);backdrop-filter:blur(12px);-webkit-backdrop-filter:blur(12px);border-bottom:1px solid rgba(42,42,48,.7);z-index:20;-webkit-app-region:drag} +.top{position:fixed;top:0;left:0;right:0;height:var(--top);display:flex;align-items:center;justify-content:space-between;gap:var(--s-4);padding:0 var(--gutter);background:var(--top-bg);backdrop-filter:blur(12px);-webkit-backdrop-filter:blur(12px);border-bottom:1px solid var(--line);z-index:20;-webkit-app-region:drag} .top button,.top .pill{-webkit-app-region:no-drag} -body.mac .top{padding-left:92px} -.brand{display:flex;align-items:center;gap:10px} +body.mac:not(.has-rail) .top{padding-left:92px} +body.has-rail .top{left:var(--rail)} +.brand{display:flex;align-items:center;gap:10px;min-width:0} .brand .word{font-family:var(--head);font-weight:900;font-size:20px;letter-spacing:.06em} -.brand .miner{font-family:var(--mono);font-size:11px;letter-spacing:.22em;color:var(--ash);margin-left:4px;padding-top:3px} -.top-right{display:flex;align-items:center;gap:12px} -.pill{display:inline-flex;align-items:center;gap:8px;font-family:var(--mono);font-size:12px;letter-spacing:.08em;text-transform:uppercase;color:var(--ash);border:1px solid var(--line);border-radius:999px;padding:6px 12px 6px 10px;background:var(--graphite)} -.pill.on{color:var(--molten);border-color:rgba(255,179,92,.35)} +.brand .miner{font-family:var(--mono);font-size:var(--t-xs);letter-spacing:.22em;color:var(--ash);margin-left:var(--s-1);padding-top:3px} +.page-title{display:flex;align-items:baseline;gap:12px;min-width:0} +.page-title h1{font-size:19px;font-weight:700;letter-spacing:0;white-space:nowrap} +.page-sub{font-size:var(--t-base);color:var(--ash);white-space:nowrap;overflow:hidden;text-overflow:ellipsis;min-width:0} +.top-right{display:flex;align-items:center;gap:var(--s-3);flex:0 0 auto;min-width:0} +.narrow-only{display:none} +.pill{display:inline-flex;align-items:center;gap:var(--s-2);font-family:var(--mono);font-size:var(--t-sm);letter-spacing:.08em;text-transform:uppercase;color:var(--ash);border:1px solid var(--line);border-radius:999px;padding:6px 12px 6px 10px;background:var(--graphite);white-space:nowrap;font-variant-numeric:tabular-nums;min-width:112px;justify-content:center} +.pill.on{color:var(--molten);border-color:var(--molten-40)} .pill.warn{color:var(--ember)} .dot{width:8px;height:8px;border-radius:50%;background:var(--ash);display:inline-block;flex:0 0 8px} -.dot.small{width:7px;height:7px;flex-basis:7px} .on .dot,.dot.live{background:var(--molten);animation:pulse 2s ease-in-out infinite} -.warn .dot{background:var(--ember);animation:none} +.warn .dot,.dot.bad{background:var(--ember);animation:none} @keyframes pulse{0%,100%{box-shadow:0 0 0 0 rgba(255,179,92,.5)}50%{box-shadow:0 0 0 7px rgba(255,179,92,0)}} @media (prefers-reduced-motion:reduce){.on .dot,.dot.live{animation:none}} -/* update banner */ -.banner{position:fixed;top:var(--top);left:0;right:0;z-index:19;display:flex;align-items:center;justify-content:center;gap:14px;padding:10px var(--gutter);background:rgba(242,84,27,.12);border-bottom:1px solid rgba(242,84,27,.4);font-size:14px} +/* the status strip under the top bar (the update line): takes no room while empty */ +.notices{position:fixed;top:var(--top);left:0;right:0;z-index:19} +body.has-rail .notices{left:var(--rail)} +.notice{display:flex;flex-wrap:wrap;align-items:center;gap:var(--s-2) var(--s-4);padding:8px calc(var(--gutter) - 6px) 8px var(--gutter);background:var(--molten-10);border-bottom:1px solid var(--molten-40);font-size:var(--t-base);line-height:1.4;color:var(--bone)} +.notice.urgent{background:rgba(242,84,27,.55);border-bottom-color:var(--ember);color:#fff;font-weight:600} +.notice-text{flex:1 1 320px;min-width:0} +.notice-actions{display:flex;align-items:center;gap:var(--s-2);flex:0 0 auto} +.notice-actions:empty{display:none} +.notice-close{flex:0 0 auto;width:30px;height:30px;border-radius:8px;border:1px solid transparent;background:transparent;color:var(--ash);font-size:20px;line-height:1;cursor:pointer;display:inline-flex;align-items:center;justify-content:center;padding:0} +.notice-close:hover{color:var(--bone);border-color:var(--line-2)} +.notice.urgent .notice-close{color:#fff} +.notice .prog{flex-basis:100%;height:3px;background:rgba(127,127,127,.2);border-radius:2px;overflow:hidden;margin-top:-3px} +.notice .prog i{display:block;height:100%;width:0;background:var(--ember);transition:width .5s linear} -.banner.clock{background:rgba(242,84,27,.14);border-bottom-color:rgba(242,84,27,.5);flex-wrap:wrap} -.banner.clock.warn{background:rgba(255,179,92,.1);border-bottom-color:rgba(255,179,92,.4)} -.banner .hint{font-size:11px;color:var(--ash);flex-basis:100%;text-align:center} -.banner.update{flex-wrap:wrap;row-gap:8px} -.banner.update.urgent{background:rgba(242,84,27,.55);border-bottom-color:var(--ember);color:#fff;font-weight:600} -.banner .prog{flex-basis:100%;height:3px;background:rgba(255,255,255,.12);border-radius:2px;overflow:hidden;margin-top:-2px} -.banner .prog i{display:block;height:100%;width:0;background:var(--ember);transition:width .5s linear} -/* remote job strip (src/jobrun.rs): running, then the outcome */ -.banner.job{flex-wrap:wrap;row-gap:8px;background:rgba(255,179,92,.1);border-bottom-color:rgba(255,179,92,.4)} -.banner.job.failed{background:rgba(242,84,27,.14);border-bottom-color:rgba(242,84,27,.5)} -.banner.job .job-results{flex-basis:100%;margin:0;font-size:11px;line-height:1.5;color:var(--ink-2);white-space:pre-wrap;word-break:break-word;max-height:120px;overflow:auto} -.job-history table{margin-top:8px} -.job-history th{text-align:left;font-weight:500;color:var(--ash);font-size:11px;padding:4px 6px 4px 0} -.job-history td{padding:5px 6px 5px 0;border-top:1px solid var(--line);vertical-align:top} -.job-history tr.failed td,.job-history tr.timeout td,.job-history tr.aborted td{color:var(--ember)} -.job-history tr.running td{color:var(--molten)} -.clock-card{margin-top:12px;border:1px solid rgba(242,84,27,.5);background:rgba(242,84,27,.08);border-radius:12px;padding:12px 14px;display:flex;flex-direction:column;gap:8px} -.clock-card.warn{border-color:rgba(255,179,92,.4);background:rgba(255,179,92,.06)} -.clock-msg{font-size:14px;color:var(--bone);line-height:1.45} -.clock-card .note{margin-top:0} +/* a question in place of a dialog: the quit strip over the page, the inline asks inside a card */ +.ask-wrap{position:fixed;left:0;right:0;bottom:0;z-index:36;display:flex;justify-content:center;padding:0 var(--s-4) var(--s-4);pointer-events:none} +body.has-rail .ask-wrap{left:var(--rail)} +.ask{display:flex;align-items:center;gap:var(--s-3);flex-wrap:wrap;background:var(--graphite);border:1px solid var(--ember-40);border-radius:14px;padding:12px 16px;box-shadow:0 14px 40px var(--shadow);pointer-events:auto;max-width:720px;animation:rise .2s ease} +.ask-text{flex:1 1 260px;font-size:var(--t-md);min-width:0;line-height:1.45} +.ask.inline{box-shadow:none;background:var(--ember-12);margin-top:var(--s-3);animation:none;max-width:none} +.ask-foot{flex-basis:100%;font-size:var(--t-sm);color:var(--ink-2);min-height:0} +.ask-foot:empty{display:none} +.ask .short-pw{width:200px;flex:0 0 200px;margin-top:0} -/* screens */ -main{position:absolute;top:var(--top);bottom:0;left:0;right:0;overflow:auto;padding:0 var(--gutter)} -body.has-bottom main{bottom:var(--bottom)} -body.drawer-open main{bottom:calc(var(--bottom) + 260px)} +/* screens and pages */ +main{position:absolute;top:var(--top);bottom:var(--bottom);left:0;right:0;overflow:auto;padding:0 var(--gutter);overscroll-behavior:contain;transition:top .15s ease} +@media (prefers-reduced-motion:reduce){main{transition:none}} +body.has-rail main{left:var(--rail)} .screen{display:none;max-width:1080px;margin:0 auto;animation:rise .45s ease} -body[data-phase="welcome"] #screen-welcome,body[data-phase="cards"] #screen-cards,body[data-phase="address"] #screen-address,body[data-phase="dashboard"] #screen-dashboard{display:block} +body[data-phase="welcome"] #screen-welcome,body[data-phase="create"] #screen-create,body[data-phase="import"] #screen-import,body[data-phase="unlock"] #screen-unlock,body[data-phase="home"] #screen-home{display:block} @keyframes rise{from{opacity:0;transform:translateY(12px)}to{opacity:1;transform:none}} -@media (prefers-reduced-motion:reduce){.screen{animation:none}} +@media (prefers-reduced-motion:reduce){.screen,.page{animation:none}} +#screen-home{padding:22px 0 32px} +.page{display:flex;flex-direction:column;gap:var(--gap);animation:rise .3s ease} /* welcome */ -.hero{min-height:calc(100vh - var(--top));display:flex;flex-direction:column;align-items:center;justify-content:center;text-align:center;gap:18px;padding:40px 0 48px} -.coin-wrap{position:relative;width:148px;height:148px;margin-bottom:6px} -.coin-wrap::before{content:"";position:absolute;inset:-40px;border-radius:50%;background:radial-gradient(circle,rgba(242,84,27,.28) 0,rgba(242,84,27,0) 65%);animation:breathe 4s ease-in-out infinite} -.coin{position:relative;display:block;border-radius:50%;filter:drop-shadow(0 10px 30px rgba(242,84,27,.35))} +.hero{min-height:calc(100vh - var(--top));display:flex;flex-direction:column;align-items:center;justify-content:center;text-align:center;gap:var(--s-4);padding:var(--s-6) 0 48px} +.mark-wrap{position:relative;width:120px;height:120px;border-radius:26px;background:#0C0C0E;display:flex;align-items:center;justify-content:center;margin-bottom:6px;box-shadow:0 20px 50px rgba(242,84,27,.25)} +.mark-wrap::before{content:"";position:absolute;inset:-40px;border-radius:50%;background:radial-gradient(circle,rgba(242,84,27,.28) 0,rgba(242,84,27,0) 65%);animation:breathe 4s ease-in-out infinite;z-index:-1} @keyframes breathe{0%,100%{opacity:.7;transform:scale(1)}50%{opacity:1;transform:scale(1.08)}} -.lead{font-size:18px;color:var(--ink-2);max-width:54ch} -.three{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:var(--gap-tile);width:100%;max-width:860px;margin-top:10px;text-align:left} -.tile{background:var(--graphite);border:1px solid var(--line);border-radius:var(--tile-r);padding:var(--tile-pad);display:flex;flex-direction:column;gap:6px;min-width:0} -.tile .k{font-family:var(--mono);font-size:11px;letter-spacing:.14em;color:var(--ember)} -.tile .t{font-family:var(--head);font-weight:700;font-size:15px;line-height:1.25} -.tile .s{font-size:13px;color:var(--ash);line-height:1.45} -.cta{display:flex;flex-wrap:wrap;gap:12px;align-items:center;justify-content:center;margin-top:10px} -.seedline{font-size:12px;color:var(--ash);letter-spacing:.04em} +@media (prefers-reduced-motion:reduce){.mark-wrap::before{animation:none}} +.lead{font-size:var(--t-xl);color:var(--ink-2);max-width:54ch} +.three{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:var(--s-3);width:100%;max-width:860px;margin-top:10px;text-align:left} +.tile{background:var(--graphite);border:1px solid var(--line);border-radius:14px;padding:18px 20px;display:flex;flex-direction:column;gap:6px;min-width:0} +.tile .k{font-family:var(--mono);font-size:var(--t-xs);letter-spacing:.14em;color:var(--ember)} +.tile .t{font-family:var(--head);font-weight:700;font-size:var(--t-lg);line-height:1.25} +.tile .s{font-size:var(--t-base);color:var(--ash);line-height:1.45} +.cta{display:flex;flex-wrap:wrap;gap:var(--s-3);align-items:center;justify-content:center;margin-top:10px} +.seedline{font-size:var(--t-sm);color:var(--ash);letter-spacing:.04em} -/* steps */ -.step{max-width:720px;margin:0 auto;padding:56px 0 48px;display:flex;flex-direction:column;gap:16px} -.step .sub{font-size:16px;color:var(--ink-2);max-width:60ch} -.step .cta{justify-content:flex-start;margin-top:8px} -.note{font-size:13px;color:var(--ash);line-height:1.5} -.note.small{font-size:12px;word-break:break-all} -.cards{display:flex;flex-direction:column;gap:12px;margin-top:8px} -.card{background:var(--graphite);border:1px solid var(--line);border-radius:var(--card-r);padding:var(--card-pad);min-width:0} -.card.detecting{display:flex;align-items:center;gap:18px} -.card.detecting .t{font-family:var(--head);font-weight:700;font-size:16px} -.card.detecting .s{font-size:12px;color:var(--ash);margin-top:4px} -.spinner{width:28px;height:28px;border-radius:50%;border:3px solid var(--line-2);border-top-color:var(--ember);animation:spin 1s linear infinite;flex:0 0 28px} -@keyframes spin{to{transform:rotate(360deg)}} -.gpu{display:flex;align-items:center;gap:18px} -.gpu .badge{width:52px;height:52px;border-radius:14px;display:flex;align-items:center;justify-content:center;font-family:var(--mono);font-size:11px;letter-spacing:.08em;flex:0 0 52px;border:1px solid var(--line-2);color:var(--molten);background:var(--obsidian)} -.gpu .badge.apple{color:var(--bone)} -.gpu .badge.nvidia{color:#8BE37A} -.gpu .badge.amd{color:var(--ember)} -.gpu .name{font-family:var(--head);font-weight:700;font-size:18px;line-height:1.2} -.gpu .meta{font-family:var(--mono);font-size:12px;color:var(--ash);margin-top:5px;display:flex;flex-wrap:wrap;gap:6px 14px} -.gpu .meta b{color:var(--ink-2);font-weight:500} -.gpu .tick{margin-left:auto;width:36px;height:36px;border-radius:50%;background:var(--ember);display:flex;align-items:center;justify-content:center;flex:0 0 36px} -.gpu.off .tick{background:var(--line-2)} -.gpu .tick svg path{stroke-dasharray:30;stroke-dashoffset:30;animation:draw .8s .2s ease forwards} -@keyframes draw{to{stroke-dashoffset:0}} -.gpu .msg{font-size:12px;color:var(--ember);margin-top:4px} - -/* GPU rows (first run and settings) */ -.gpu-row{display:flex;align-items:center;gap:16px;background:var(--graphite);border:1px solid var(--line);border-radius:var(--card-r);padding:16px 20px;min-width:0} -.gpu-row.off{opacity:.72} -.gpu-row .badge{width:48px;height:48px;border-radius:13px;display:flex;align-items:center;justify-content:center;font-family:var(--mono);font-size:11px;letter-spacing:.08em;flex:0 0 48px;border:1px solid var(--line-2);color:var(--molten);background:var(--obsidian)} -.gpu-row .badge.apple{color:var(--bone)} -.gpu-row .badge.nvidia{color:#8BE37A} -.gpu-row .badge.amd{color:var(--ember)} -.gpu-row .info{flex:1;min-width:0;display:flex;flex-direction:column;gap:4px} -.gpu-row .name{font-family:var(--head);font-weight:700;font-size:16px;line-height:1.2;display:flex;align-items:center;gap:10px;flex-wrap:wrap} -.kind{font-family:var(--mono);font-size:10px;letter-spacing:.14em;text-transform:uppercase;border-radius:999px;padding:2px 8px;border:1px solid var(--line-2);color:var(--ash);font-weight:500} -.kind.discrete,.kind.apple{color:var(--molten);border-color:rgba(255,179,92,.4)} -.kind.external{color:var(--bone)} -.gpu-row .meta{font-family:var(--mono);font-size:12px;color:var(--ash);display:flex;flex-wrap:wrap;gap:4px 14px} -.gpu-row .meta b{color:var(--ink-2);font-weight:500} -.gpu-row .reason{font-size:12px;color:var(--ash)} -.gpu-row .msg{font-size:12px;color:var(--ember)} -.ids{display:flex;align-items:center;gap:6px;flex:0 0 auto} -.ids .k{font-family:var(--mono);font-size:10px;letter-spacing:.12em;text-transform:uppercase;color:var(--ash);margin-right:4px} -.ids button{width:30px;height:30px;border-radius:8px;border:1px solid var(--line-2);background:transparent;color:var(--bone);cursor:pointer;font-size:16px;line-height:1} -.ids button:hover{border-color:var(--ash)} -.ids input{width:44px;text-align:center;background:var(--obsidian);border:1px solid var(--line-2);border-radius:8px;padding:5px 4px;color:var(--bone);font-family:var(--mono);font-size:13px;user-select:text;-webkit-user-select:text} -.ids input:focus{outline:none;border-color:var(--ember)} -.ids input::-webkit-inner-spin-button,.ids input::-webkit-outer-spin-button{-webkit-appearance:none;margin:0} -.gpu-row.off .ids{opacity:.4;pointer-events:none} -.gpu-row .switch{padding:0;flex:0 0 auto} -.cards.compact .gpu-row{padding:12px 14px;gap:12px;border-radius:14px} -.cards.compact .gpu-row .badge{width:38px;height:38px;flex-basis:38px;border-radius:10px} -.cards.compact .gpu-row .name{font-size:14px} -.cards.compact .ids .k{display:none} - -.power{display:flex;align-items:center;gap:8px;flex:0 0 auto} -.power .k{font-family:var(--mono);font-size:10px;letter-spacing:.12em;text-transform:uppercase;color:var(--ash)} -.power input[type=range]{width:110px;accent-color:var(--ember)} -.power .pv{font-size:12px;color:var(--ink-2);min-width:84px} -.cards.compact .power .k{display:none} -.cards.compact .power input[type=range]{width:80px} -.gpu-tile .m.tele .warm,.gpu-tile .m.tele .warm b{color:var(--molten)} -.gpu-tile .m.tele .hot,.gpu-tile .m.tele .hot b{color:var(--ember)} -.gpu-tile .msg.ok,.gpu-row .msg.ok{color:var(--molten)} -.gpu-row .msg.hot,.gpu-tile .msg.hot{color:var(--ember)} -.gpu-tile .msg .btn.tiny,.gpu-row .msg .btn.tiny{margin-left:6px;vertical-align:middle} -.gpu-tile .msg.warm{color:var(--molten)} -.gpu-tile .msg.hot{color:var(--ember)} - -/* per-card tiles on the dashboard */ -.gpu-tiles{display:grid;grid-template-columns:repeat(auto-fit,minmax(220px,1fr));gap:var(--gap-tile)} -.gpu-tile{background:var(--obsidian);border:1px solid var(--line);border-radius:var(--tile-r);padding:14px 16px;display:flex;flex-direction:column;gap:6px;min-width:0} -.gpu-tile .n{font-family:var(--head);font-weight:700;font-size:14px;line-height:1.25;display:flex;align-items:center;gap:8px;flex-wrap:wrap} -.gpu-tile .h{font-family:var(--head);font-weight:700;font-size:24px;color:var(--ember);line-height:1.1;display:flex;align-items:baseline;gap:6px;font-variant-numeric:tabular-nums} -.gpu-tile .h .unit{font-family:var(--mono);font-size:11px;color:var(--ash);font-weight:500;letter-spacing:.08em} -.gpu-tile.idle .h{color:var(--ash)} -.gpu-tile .m{font-family:var(--mono);font-size:12px;color:var(--ash);display:flex;flex-wrap:wrap;gap:4px 12px} -.gpu-tile .m b{color:var(--ink-2);font-weight:500} -.gpu-tile .st{display:inline-flex;align-items:center;gap:7px;font-family:var(--mono);font-size:12px;color:var(--ash)} -.gpu-tile .st.mining{color:var(--molten)} -.gpu-tile .st.bad{color:var(--ember)} -.gpu-tile .st i{width:7px;height:7px;border-radius:50%;background:currentColor;display:inline-block} -.gpu-tile .msg{font-size:12px;color:var(--ash)} -.gpu-off{margin-top:12px;font-size:12px;color:var(--ash)} - -/* address options */ -.options{display:flex;flex-direction:column;gap:12px;margin-top:6px} -.option{display:flex;gap:16px;align-items:flex-start;background:var(--graphite);border:1px solid var(--line);border-radius:var(--card-r);padding:20px 22px;cursor:pointer;transition:border-color .15s ease,background .15s ease} -.option:hover{border-color:var(--line-2)} -.option.on{border-color:var(--ember);background:#1A1614} -.option input[type=radio]{position:absolute;opacity:0;width:0;height:0} -.option .radio{width:20px;height:20px;border-radius:50%;border:2px solid var(--line-2);flex:0 0 20px;margin-top:2px;position:relative} -.option.on .radio{border-color:var(--ember)} -.option.on .radio::after{content:"";position:absolute;inset:4px;border-radius:50%;background:var(--ember)} -.option .body{display:flex;flex-direction:column;gap:6px;min-width:0;flex:1} -.option .t{font-family:var(--head);font-weight:700;font-size:16px;display:flex;align-items:center;gap:10px} -.option .s{font-size:14px;color:var(--ash);line-height:1.5} -.tag{font-family:var(--mono);font-size:10px;letter-spacing:.14em;text-transform:uppercase;color:var(--molten);border:1px solid rgba(255,179,92,.4);border-radius:999px;padding:2px 8px} -.addr-input{width:100%;margin-top:8px;background:var(--obsidian);border:1px solid var(--line-2);border-radius:10px;padding:12px 14px;color:var(--bone);font-size:14px;letter-spacing:.02em;user-select:text;-webkit-user-select:text} -.addr-input:focus{outline:none;border-color:var(--ember)} -.option:not(.on) .addr-input{display:none} -.err{font-size:13px;color:var(--ember)} - -/* dashboard */ -#screen-dashboard{padding:22px 0 28px;display:none;flex-direction:column;gap:var(--gap)} -body[data-phase="dashboard"] #screen-dashboard{display:flex} -.strip{display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:var(--gap-tile)} -.cell{background:var(--graphite);border:1px solid var(--line);border-radius:var(--tile-r);padding:var(--tile-pad);display:flex;flex-direction:column;gap:6px;min-width:0} -.cell .k{font-family:var(--mono);font-size:11px;letter-spacing:.12em;text-transform:uppercase;color:var(--ash)} -.cell .v{font-family:var(--head);font-weight:700;font-size:26px;line-height:1.1;font-variant-numeric:tabular-nums;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;display:flex;align-items:baseline;gap:8px} -.cell.ember .v{color:var(--ember)} -.cell .v .unit{font-family:var(--mono);font-size:12px;color:var(--ash);font-weight:500;letter-spacing:.08em} -.cell .v.state{text-transform:capitalize;font-size:22px} -.cell .s{font-family:var(--mono);font-size:12px;color:var(--ash);white-space:nowrap;overflow:hidden;text-overflow:ellipsis} -.cell.ok .v.state{color:var(--molten)} -.cell.bad .v.state{color:var(--ember)} -.grid{display:grid;grid-template-columns:1.35fr .85fr;gap:var(--gap);align-items:start} -.col-main,.col-side{display:flex;flex-direction:column;gap:var(--gap);min-width:0} -.card-head{display:flex;justify-content:space-between;align-items:center;gap:12px;margin-bottom:12px;flex-wrap:wrap} -.stats{display:flex;flex-wrap:wrap;gap:12px 16px;font-size:12px;color:var(--ash)} -.stats b{color:var(--bone);font-weight:500;font-variant-numeric:tabular-nums} -#dag{display:block;width:100%;height:190px;border-radius:12px;background:var(--obsidian)} -.legend{display:flex;flex-wrap:wrap;gap:14px 18px;margin-top:12px;font-size:12px;color:var(--ink-2)} -.legend span{display:inline-flex;align-items:center;gap:8px} -.sw{width:12px;height:12px;border-radius:3px;display:inline-block;border:1px solid var(--line-2)} -.sw.ember{background:var(--ember);border-color:var(--ember)} -.sw.glow{border-color:var(--molten);box-shadow:0 0 8px rgba(255,179,92,.7)} -.sw.line{width:18px;height:0;border:0;border-top:1px dashed var(--line-2);border-radius:0} -.tbl{overflow-x:auto} -table{border-collapse:collapse;width:100%;font-size:13px} -th,td{padding:9px 8px;text-align:left;border-bottom:1px solid var(--line);white-space:nowrap} -th{font-family:var(--mono);font-size:10px;letter-spacing:.12em;text-transform:uppercase;color:var(--ash);font-weight:500} -td.n,th.n{text-align:right;font-variant-numeric:tabular-nums;font-family:var(--mono)} -tr:last-child td{border-bottom:0} -td .st{display:inline-flex;align-items:center;gap:7px;font-family:var(--mono);font-size:12px;color:var(--ash)} -td .st.mining{color:var(--molten)} -td .st.bad{color:var(--ember)} -td .st i{width:7px;height:7px;border-radius:50%;background:currentColor;display:inline-block} -td .sub{display:block;font-family:var(--mono);font-size:11px;color:var(--ash);white-space:normal;max-width:280px} -.empty{color:var(--ash);font-size:13px;padding:10px 0} -.kv{display:flex;flex-direction:column} -.kv>div{display:flex;justify-content:space-between;align-items:baseline;gap:12px;padding:7px 0;border-bottom:1px solid var(--line)} -.kv>div:last-child{border-bottom:0} -.kv .k{font-family:var(--mono);font-size:11px;letter-spacing:.1em;text-transform:uppercase;color:var(--ash)} -.kv .v{font-size:14px;font-variant-numeric:tabular-nums;color:var(--bone);white-space:nowrap;overflow:hidden;text-overflow:ellipsis} -.card .note{margin-top:10px} -.feed{display:flex;flex-direction:column;font-family:var(--mono);font-size:12px;color:var(--ink-2);max-height:300px;overflow:auto} -.feed>div{display:flex;justify-content:space-between;gap:10px;border-bottom:1px solid var(--line);padding:8px 0;align-items:baseline} -.feed>div:last-child{border-bottom:0} -.feed .t{color:var(--ash);flex:0 0 auto;font-size:11px} -.feed .k{color:var(--molten);margin-right:6px} -.feed .k.error{color:var(--ember)} -.feed .k.block{color:var(--ember)} -.feed .k.build{color:var(--ink-2)} - -/* sheet (the key) */ -.sheet-wrap,.panel-wrap{position:fixed;inset:0;z-index:30;background:rgba(12,12,14,.72);backdrop-filter:blur(6px);-webkit-backdrop-filter:blur(6px);display:flex;align-items:center;justify-content:center;padding:24px} -.sheet{background:var(--graphite);border:1px solid var(--line-2);border-radius:22px;padding:32px;max-width:640px;width:100%;display:flex;flex-direction:column;gap:14px;box-shadow:0 30px 80px rgba(0,0,0,.6);animation:rise .3s ease} -.sheet .sub{font-size:15px;color:var(--ink-2)} -.field{display:flex;flex-direction:column;gap:8px;margin-top:6px} -.field .k{font-family:var(--mono);font-size:11px;letter-spacing:.12em;text-transform:uppercase;color:var(--ash)} -.box{display:flex;align-items:center;gap:10px;background:var(--obsidian);border:1px solid var(--line-2);border-radius:10px;padding:10px 12px;font-size:13px;word-break:break-all;user-select:text;-webkit-user-select:text} +/* steps (create, import) */ +.step{max-width:720px;margin:0 auto;padding:56px 0 48px;display:flex;flex-direction:column;gap:var(--s-4)} +.step .sub{font-size:var(--t-xl);color:var(--ink-2);max-width:60ch} +.step .cta{justify-content:flex-start;margin-top:var(--s-2)} +.note{font-size:var(--t-base);color:var(--ash);line-height:1.5} +.note.small{font-size:var(--t-sm);word-break:break-all} +.help{font-size:var(--t-base);color:var(--ash);line-height:1.5;max-width:64ch} +.line-text{font-size:var(--t-md);color:var(--ink-2);line-height:1.5} +.line-text.ok{color:var(--molten)} +.line-text.bad{color:var(--ember)} +.err{font-size:var(--t-base);color:var(--ember);line-height:1.5} +.err:empty{display:none} +.top-gap{margin-top:var(--s-4)} +.indent{margin-left:52px} +.field{display:flex;flex-direction:column;gap:var(--s-2);margin-top:6px} +.field .k{font-family:var(--mono);font-size:var(--t-xs);letter-spacing:.12em;text-transform:uppercase;color:var(--ash)} +.field input,.field textarea,.addr-input{font:inherit;color:var(--bone);background:var(--obsidian);border:1px solid var(--line-2);border-radius:10px;padding:10px 12px;min-height:42px;font-size:var(--t-md);user-select:text;-webkit-user-select:text} +.field textarea{font-family:var(--mono);font-size:var(--t-md);line-height:1.5;resize:vertical} +.field input.mono,.addr-input.mono{font-family:var(--mono)} +.field input:focus,.field textarea:focus,.addr-input:focus,.select:focus{outline:none;border-color:var(--ember)} +.addr-input{width:100%;min-width:0} +.row .addr-input{flex:1 1 180px;width:auto} +.box{display:flex;align-items:center;gap:10px;background:var(--obsidian);border:1px solid var(--line-2);border-radius:10px;padding:10px 12px;font-size:var(--t-base);word-break:break-all;user-select:text;-webkit-user-select:text} .box span{flex:1;min-width:0} .box.key{color:var(--molten)} -.check{display:flex;align-items:center;gap:10px;font-size:14px;cursor:pointer;margin-top:4px} -.check.small{font-size:12px;color:var(--ash)} -.check input{width:18px;height:18px;accent-color:var(--ember)} -.sheet .cta{justify-content:flex-start} +.words{display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:8px;margin:8px 0 0;padding:0;list-style:none;counter-reset:w} +.words li{counter-increment:w;background:var(--graphite);border:1px solid var(--line);border-radius:10px;padding:8px 10px;font-family:var(--mono);font-size:var(--t-md);display:flex;gap:8px;align-items:baseline;user-select:text;-webkit-user-select:text} +.words li::before{content:counter(w);color:var(--ash);font-size:var(--t-xs);min-width:18px;text-align:right} +.words.small{grid-template-columns:repeat(6,minmax(0,1fr))} +.words.small li{font-size:var(--t-sm);padding:5px 8px;background:var(--obsidian)} +.checks{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:12px} +.checks label{display:flex;flex-direction:column;gap:6px;font-family:var(--mono);font-size:var(--t-sm);color:var(--ash)} +.checks input{font:inherit;font-family:var(--mono);font-size:var(--t-lg);color:var(--bone);background:var(--graphite);border:1px solid var(--line-2);border-radius:10px;padding:10px 12px;user-select:text;-webkit-user-select:text} +.checks input:focus{outline:none;border-color:var(--ember)} -/* settings panel */ -.panel-wrap{justify-content:flex-end;padding:0} -.panel{width:min(440px,100%);height:100%;background:var(--graphite);border-left:1px solid var(--line-2);display:flex;flex-direction:column;animation:slide .25s ease} -@keyframes slide{from{transform:translateX(30px);opacity:0}to{transform:none;opacity:1}} -.panel-head{display:flex;justify-content:space-between;align-items:center;padding:18px 22px;border-bottom:1px solid var(--line)} -.panel-body{padding:14px 22px 28px;overflow:auto;display:flex;flex-direction:column;gap:18px} -.row{display:flex;gap:10px;align-items:center} -.row.between{justify-content:space-between} -.row .addr-input{margin-top:0} -.num{width:90px;background:var(--obsidian);border:1px solid var(--line-2);border-radius:10px;padding:9px 12px;color:var(--bone);font-size:14px;user-select:text;-webkit-user-select:text} -.num:focus{outline:none;border-color:var(--ember)} -.switch{display:flex;align-items:center;gap:12px;font-size:14px;cursor:pointer;padding:6px 0} +/* cards, rows, disclosures, switches, the segmented control, the kv */ +.card{background:var(--graphite);border:1px solid var(--line);border-radius:var(--card-r);padding:var(--card-pad);min-width:0} +.card-head{display:flex;justify-content:space-between;align-items:center;gap:var(--s-3);margin-bottom:var(--s-3);flex-wrap:wrap} +.row{display:flex;gap:10px;align-items:center;min-width:0} +.row.wrap{flex-wrap:wrap} +.lead-row{display:flex;align-items:flex-start;justify-content:space-between;gap:var(--s-4)} +.lead-text{min-width:0;display:flex;flex-direction:column;gap:6px} +.lead-text h3{font-size:var(--t-xl)} +.disclose{display:flex;align-items:center;justify-content:space-between;gap:var(--s-3);width:100%;border:0;background:transparent;padding:0;cursor:pointer;text-align:left;color:var(--bone);font-size:var(--t-md);font-weight:500;min-height:32px} +.disclose .chev{color:var(--ash);flex:0 0 auto;margin-right:4px} +.disclose-right{display:flex;align-items:center;gap:var(--s-3);min-width:0} +.disclose-right .dim{font-size:var(--t-sm);white-space:nowrap;overflow:hidden;text-overflow:ellipsis;min-width:0;max-width:52ch} +.chev{width:9px;height:9px;border-right:2px solid currentColor;border-bottom:2px solid currentColor;transform:rotate(45deg) translateY(-2px);transition:transform .15s ease;display:inline-block} +.open .chev,[aria-expanded="true"] .chev{transform:rotate(225deg) translateY(-2px)} +.card .disclose+.disclose,.card .details+.disclose,.card .srow+.disclose,.card .switch+.disclose,.card .err+.disclose{margin-top:var(--s-3);padding-top:var(--s-3);border-top:1px solid var(--line)} +.details{padding-top:var(--s-3);display:flex;flex-direction:column;gap:var(--s-3)} +.srow{display:flex;align-items:center;justify-content:space-between;gap:var(--s-4);padding:9px 0;min-width:0} +.srow+.srow,.switch+.srow,.srow+.switch{border-top:1px solid var(--line)} +.sw-text{min-width:0} +.sw-text b{font-weight:600} +.sw-text .dim{font-size:var(--t-base)} +.switch{display:flex;align-items:flex-start;gap:var(--s-3);font-size:var(--t-md);cursor:pointer;padding:9px 0;line-height:1.4} +.switch+.switch{border-top:1px solid var(--line)} .switch input{position:absolute;opacity:0;width:0;height:0} -.switch .track{width:40px;height:22px;border-radius:999px;background:var(--line-2);position:relative;flex:0 0 40px;transition:background .15s ease} -.switch .track::after{content:"";position:absolute;top:3px;left:3px;width:16px;height:16px;border-radius:50%;background:var(--bone);transition:transform .15s ease} +.switch .track{width:40px;height:22px;border-radius:999px;background:var(--line-2);position:relative;flex:0 0 40px;transition:background .15s ease;margin-top:1px} +.switch .track::after{content:"";position:absolute;top:3px;left:3px;width:16px;height:16px;border-radius:50%;background:#F4F1EC;transition:transform .15s ease} .switch input:checked+.track{background:var(--ember)} .switch input:checked+.track::after{transform:translateX(18px)} +.switch input:focus-visible+.track{outline:2px solid var(--ember);outline-offset:3px} +.switch input:disabled+.track{opacity:.4} +.seg{display:inline-flex;background:var(--obsidian);border:1px solid var(--line);border-radius:10px;padding:3px;gap:2px} +.seg-b{border:0;background:transparent;color:var(--ash);font-size:var(--t-base);font-weight:500;padding:6px 14px;border-radius:8px;cursor:pointer;transition:background .15s ease,color .15s ease;white-space:nowrap} +.seg-b:hover{color:var(--bone)} +.seg-b.on{background:var(--graphite);color:var(--bone);box-shadow:0 1px 3px rgba(0,0,0,.25);font-weight:600} +.seg-b:disabled{opacity:.4;cursor:default} +.select{font:inherit;font-size:var(--t-md);color:var(--bone);background:var(--obsidian);border:1px solid var(--line-2);border-radius:10px;padding:7px 12px;min-height:38px;cursor:pointer} +.kv{display:flex;flex-direction:column} +.kv>div{display:grid;grid-template-columns:140px auto 1fr;align-items:baseline;gap:var(--s-3);padding:7px 0;border-bottom:1px solid var(--line)} +.kv>div:last-child{border-bottom:0} +.kv .k{font-family:var(--mono);font-size:var(--t-xs);letter-spacing:.1em;text-transform:uppercase;color:var(--ash);white-space:nowrap} +.kv .v{font-size:var(--t-md);font-variant-numeric:tabular-nums;color:var(--bone);white-space:nowrap;overflow:hidden;text-overflow:ellipsis;user-select:text;-webkit-user-select:text} +.kv .v.ok{color:var(--molten)} +.kv .v.warn{color:var(--ember)} +.kv .v.dim{color:var(--ash)} +.kv .m{font-size:var(--t-sm);color:var(--ash);min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap} +.card.adv{padding:0} +.card.adv summary{list-style:none;cursor:pointer;display:flex;align-items:center;justify-content:space-between;gap:12px;padding:var(--card-pad)} +.card.adv summary::-webkit-details-marker{display:none} +.card.adv summary::after{content:"+";font-family:var(--mono);color:var(--ash);font-size:18px;margin-left:auto} +.card.adv[open] summary::after{content:"\2212"} +.card.adv summary .eyebrow{margin-left:0} +.card.adv>:not(summary){margin-left:var(--card-pad);margin-right:var(--card-pad)} +.card.adv>:last-child{margin-bottom:var(--card-pad)} +.card.adv>.note+.note{margin-top:6px} +.empty{color:var(--ash);font-size:var(--t-base);padding:10px 0} -/* bottom bar */ -.bottom{position:fixed;left:0;right:0;bottom:0;height:var(--bottom);display:flex;align-items:center;justify-content:space-between;gap:12px;padding:0 var(--gutter);background:rgba(12,12,14,.92);border-top:1px solid var(--line);z-index:21} -.bottom .left,.bottom .right{display:flex;align-items:center;gap:8px} -.bottom .mid{font-size:12px;color:var(--ash);white-space:nowrap;overflow:hidden;text-overflow:ellipsis;text-align:center;flex:1} -.bottom .right .mono{font-size:12px} +/* Home: the balance first, its money line, the address, the two buttons */ +.hero-card{display:flex;flex-direction:column;gap:var(--s-3)} +.bal-row{display:flex;align-items:baseline;gap:var(--s-4);flex-wrap:wrap;min-width:0} +.bal{display:flex;align-items:baseline;gap:12px;min-width:0} +.bal .v{font-family:var(--head);font-weight:900;font-size:var(--t-h1);letter-spacing:-.01em;line-height:1;white-space:nowrap;user-select:text;-webkit-user-select:text} +.bal .unit{font-size:var(--t-md);color:var(--ash);letter-spacing:.12em} +.bal-line{color:var(--ash);font-size:var(--t-md)} +.bal-line:empty{display:none} +.money-line{font-size:var(--t-md);color:var(--ash);margin-top:-4px} +.money-line b{color:var(--bone);font-family:var(--head);font-weight:700;font-size:var(--t-num);margin-right:6px} +.addr-big{display:flex;align-items:center;gap:12px;background:var(--obsidian);border:1px solid var(--line-2);border-radius:12px;padding:14px 16px;font-size:var(--t-lg);word-break:break-all;user-select:text;-webkit-user-select:text} +.addr-big span{flex:1;min-width:0;color:var(--molten)} +.actions{margin-top:var(--s-1)} +.actions .note{margin-left:var(--s-2)} +.node-line{display:inline-flex;align-items:center;gap:10px;min-width:0;white-space:nowrap;overflow:hidden;text-overflow:ellipsis} +.node-card.bad .node-line{color:var(--ember)} -/* log drawer */ -.drawer{position:fixed;left:0;right:0;bottom:var(--bottom);height:0;overflow:hidden;background:var(--obsidian);border-top:1px solid var(--line);z-index:20;transition:height .2s ease;display:flex;flex-direction:column} -body.drawer-open .drawer{height:260px} -.drawer-head{display:flex;justify-content:space-between;align-items:center;padding:8px var(--gutter);border-bottom:1px solid var(--line);flex:0 0 auto} -.chips{display:flex;gap:6px} -.chip{font-family:var(--mono);font-size:11px;letter-spacing:.08em;text-transform:uppercase;border:1px solid var(--line);border-radius:999px;padding:4px 10px;background:transparent;color:var(--ash);cursor:pointer} -.chip.on{color:var(--molten);border-color:rgba(255,179,92,.4)} -.log{margin:0;flex:1;overflow:auto;padding:10px var(--gutter);font-size:12px;line-height:1.55;color:var(--ink-2);white-space:pre-wrap;word-break:break-all;user-select:text;-webkit-user-select:text} -.log .src{color:var(--ash)} -.log .src.node{color:#7FA7C9} -.log .src.miner1,.log .src.miner2,.log .src.miner3,.log .src.miner4{color:var(--molten)} -.log .src.app{color:var(--ember)} -.log .e{color:var(--ember)} +/* the history row: kind and who, the amount, the state word with its reason, the chevron; the details under */ +.hist{display:flex;flex-direction:column;gap:var(--s-2)} +.hrow{border:1px solid var(--line);border-radius:var(--row-r);background:var(--row);min-width:0} +.hrow.open{border-color:var(--line-2)} +.hr-main{display:grid;grid-template-columns:26px minmax(160px,1.3fr) auto minmax(180px,1fr) 30px;align-items:center;gap:var(--s-4);padding:11px 14px;cursor:pointer} +.hr-main .glyph{width:26px;height:26px;border-radius:8px;border:1px solid var(--line-2);display:flex;align-items:center;justify-content:center;color:var(--ash);font-family:var(--mono);font-size:var(--t-sm)} +.hrow.in .glyph{color:var(--molten);border-color:var(--molten-40)} +.hr-who{min-width:0;display:flex;flex-direction:column;gap:2px} +.hr-who .kind{font-weight:600;font-size:var(--t-md);white-space:nowrap;overflow:hidden;text-overflow:ellipsis} +.hr-who .sub{font-family:var(--mono);font-size:var(--t-sm);color:var(--ash);white-space:nowrap;overflow:hidden;text-overflow:ellipsis} +.hr-amt{font-family:var(--head);font-weight:700;font-size:var(--t-lg);white-space:nowrap;text-align:right;justify-self:end} +.hrow.in .hr-amt{color:var(--molten)} +.hr-amt .unit{font-family:var(--mono);font-size:10px;color:var(--ash);font-weight:500;letter-spacing:.08em;margin-left:4px} +.hr-state{min-width:0;display:flex;flex-direction:column;gap:2px} +.hr-state .st{display:inline-flex;align-items:center;gap:7px;font-family:var(--mono);font-size:var(--t-sm);color:var(--ash);white-space:nowrap} +.hr-state .st i{width:7px;height:7px;border-radius:50%;background:currentColor;display:inline-block;flex:0 0 7px} +.hr-state .st.ink{color:var(--ink-2)} +.hr-state .st.ok{color:var(--molten)} +.hr-state .st.bad{color:var(--ember)} +.hr-state .why{font-size:var(--t-sm);color:var(--ash);white-space:nowrap;overflow:hidden;text-overflow:ellipsis} +.chev-btn{width:30px;height:30px;border-radius:8px;border:1px solid transparent;background:transparent;cursor:pointer;display:inline-flex;align-items:center;justify-content:center;color:var(--ash);justify-self:end} +.chev-btn:hover{border-color:var(--line-2);color:var(--bone)} +.hr-details{padding:12px 14px 14px 56px;border-top:1px solid var(--line);user-select:text;-webkit-user-select:text} +.hr-details .kv>div{grid-template-columns:110px 1fr} +.hr-details .kv .v{white-space:normal;word-break:break-all} +.hrow.sent-row .hr-main{cursor:default;grid-template-columns:26px minmax(160px,1.3fr) auto minmax(180px,1fr)} -.toast{position:fixed;left:50%;bottom:calc(var(--bottom) + 16px);transform:translateX(-50%);background:var(--graphite);border:1px solid var(--line-2);border-radius:10px;padding:10px 16px;font-size:13px;z-index:40;box-shadow:0 10px 30px rgba(0,0,0,.5)} +/* Send: the fee row and the pending row */ +.send-form{display:flex;flex-direction:column;gap:var(--s-3)} +.unit-word{font-size:var(--t-sm);color:var(--ash);letter-spacing:.1em} +.fee-row{display:flex;align-items:center;gap:var(--s-3);flex-wrap:wrap;padding:8px 0 0} +.fee-row .k{font-size:var(--t-xs);letter-spacing:.12em;text-transform:uppercase;color:var(--ash)} +.send-form[data-locked="1"] input{opacity:.6;pointer-events:none} +.sent{display:flex;flex-direction:column;gap:var(--s-3);margin-top:var(--s-3)} +.sent .cta{justify-content:flex-start;margin-top:0} -@media (max-width:860px){ - .strip{grid-template-columns:repeat(2,minmax(0,1fr))} - .grid{grid-template-columns:1fr} - .three{grid-template-columns:1fr} - h1{font-size:40px} -} - -/* ---- Igneum Wallet (added to the miner's tokens and base styles above) ---- */ -body[data-phase="welcome"] #screen-welcome,body[data-phase="create"] #screen-create,body[data-phase="import"] #screen-import,body[data-phase="unlock"] #screen-unlock,body[data-phase="home"] #screen-home{display:block} -.view{display:none} -body[data-view="overview"] #view-overview,body[data-view="send"] #view-send,body[data-view="receive"] #view-receive,body[data-view="tx"] #view-tx,body[data-view="settings"] #view-settings{display:block} -body{user-select:text;-webkit-user-select:text} -.field{display:flex;flex-direction:column;gap:6px;font-size:13px;color:var(--ash)} -.field input,.field textarea,.inline input{font:inherit;color:var(--bone);background:var(--graphite);border:1px solid var(--line-2);border-radius:10px;padding:10px 12px;min-height:44px} -.field textarea{font-family:var(--mono);font-size:14px;line-height:1.5;resize:vertical} -.field input.mono{font-family:var(--mono)} -.field input:focus,.field textarea:focus,.inline input:focus{outline:none;border-color:var(--ember)} -.err{color:var(--ember);font-size:13px;min-height:18px} -.err:empty{display:none} -.words{display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:8px;margin:8px 0 0;padding:0 0 0 0;list-style:none;counter-reset:w} -.words li{counter-increment:w;background:var(--graphite);border:1px solid var(--line);border-radius:10px;padding:8px 10px;font-family:var(--mono);font-size:14px;display:flex;gap:8px;align-items:baseline} -.words li::before{content:counter(w);color:var(--ash);font-size:11px;min-width:18px;text-align:right} -.words.small{grid-template-columns:repeat(6,minmax(0,1fr))} -.words.small li{font-size:12px;padding:5px 8px} -.checks{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:12px} -.checks label{display:flex;flex-direction:column;gap:6px;font-family:var(--mono);font-size:12px;color:var(--ash)} -.checks input{font:inherit;font-family:var(--mono);font-size:15px;color:var(--bone);background:var(--graphite);border:1px solid var(--line-2);border-radius:10px;padding:10px 12px} -.segs{display:flex;gap:6px;background:var(--graphite);border:1px solid var(--line);border-radius:12px;padding:4px;width:max-content} -.seg{font:inherit;font-size:13px;font-weight:600;color:var(--ash);background:transparent;border:0;border-radius:9px;padding:8px 14px;cursor:pointer} -.seg.on{background:var(--obsidian);color:var(--bone)} -.seg:disabled{opacity:.4;cursor:default} -.unlock{display:flex;gap:10px;align-items:center;margin-top:6px} -.unlock input{font:inherit;color:var(--bone);background:var(--graphite);border:1px solid var(--line-2);border-radius:10px;padding:12px 14px;min-width:280px;min-height:52px} -.balance-card{background:var(--graphite);border:1px solid var(--line);border-radius:var(--card-r);padding:28px var(--card-pad);margin-top:28px;display:flex;flex-direction:column;gap:12px;align-items:flex-start} -.balance{display:flex;align-items:baseline;gap:12px;font-family:var(--head);font-weight:900;font-size:48px;letter-spacing:-.01em;line-height:1} -.balance .unit{font-family:var(--mono);font-size:14px;color:var(--ash);letter-spacing:.12em} -.addr-row{display:flex;align-items:center;gap:10px;font-size:13px;color:var(--ink-2)} -.actions{display:flex;gap:12px;margin-top:6px} -.split{display:grid;grid-template-columns:1fr 1fr;gap:var(--gap);margin:var(--gap) 0} -.card+.card{margin-top:var(--gap)} -.card h3{margin-bottom:8px} -.kv{display:grid;grid-template-columns:max-content 1fr;gap:6px 16px;font-size:13px;margin-top:10px} -.kv span{color:var(--ash)} -.kv b{font-weight:500;font-family:var(--mono);word-break:break-all} -.kv b.ok{color:var(--molten)} -.kv b.warn{color:var(--ember)} -.history{margin-top:10px;display:flex;flex-direction:column} -.history .row{display:grid;grid-template-columns:90px 1fr max-content 110px;gap:14px;align-items:center;padding:10px 0;border-top:1px solid var(--line);cursor:pointer;font-size:13px} -.history .row:hover{background:rgba(255,255,255,.02)} -.history .row:first-child{border-top:0} -.history .kind{font-family:var(--mono);font-size:11px;letter-spacing:.1em;text-transform:uppercase;color:var(--ash)} -.history .who{font-family:var(--mono);color:var(--ink-2);overflow:hidden;text-overflow:ellipsis;white-space:nowrap} -.history .amt{font-family:var(--mono);font-weight:500} -.history .amt.in{color:var(--molten)} -.history .fin{font-family:var(--mono);font-size:11px;letter-spacing:.08em;text-transform:uppercase;text-align:right} -.fin.pending{color:var(--ash)}.fin.in_block{color:var(--ink-2)}.fin.final{color:var(--molten)}.fin.failed{color:var(--ember)} -.history .empty{color:var(--ash);font-size:13px;padding:8px 0} -.confirm{background:var(--graphite);border:1px solid var(--line);border-radius:var(--card-r);padding:var(--card-pad);display:flex;flex-direction:column;gap:12px} -.confirm .row{display:flex;justify-content:space-between;gap:16px;font-size:14px;color:var(--ash)} -.confirm .row b{color:var(--bone);font-weight:500;text-align:right} -.confirm .row b.small{font-size:12px;word-break:break-all} -.confirm .row.total{border-top:1px solid var(--line);padding-top:12px;color:var(--ink-2)} -.qr{width:240px;height:240px;background:var(--bone);border-radius:14px;padding:8px} +/* Receive: the QR beside its line */ +.qr-row{display:flex;gap:var(--s-5);align-items:flex-start;flex-wrap:wrap;margin-top:var(--s-4)} +.qr{width:240px;height:240px;background:var(--qr-bg);border-radius:14px;padding:10px;flex:0 0 240px} .qr svg{width:100%;height:100%;display:block} -.addr-big{font-size:14px;word-break:break-all;color:var(--ink-2)} -.finality-line{font-family:var(--mono);font-size:13px;padding:12px 14px;border-radius:12px;border:1px solid var(--line);background:var(--graphite)} -.finality-line.final{border-color:rgba(255,179,92,.4);color:var(--molten)} -.finality-line.failed{border-color:rgba(242,84,27,.5);color:var(--ember)} -.inline{display:flex;gap:10px;align-items:center;flex-wrap:wrap;margin-top:8px} -.inline input{min-width:200px} -.warn-box{margin-top:14px;border:1px solid rgba(242,84,27,.4);background:rgba(242,84,27,.06);border-radius:12px;padding:12px 14px} -.warn-box b{font-size:14px} -.danger-card{border-color:rgba(242,84,27,.35)} -.switch{display:flex;align-items:center;gap:10px;font-size:14px;cursor:pointer} -/* the miner's switch hides its input behind a .track span the wallet does not render: the box itself shows here - (fixed 5 October 2026: the three wallet switches were invisible, only their words could be clicked) */ -.switch input{position:static;opacity:1;width:18px;height:18px;margin:0;accent-color:var(--ember);flex:0 0 18px} -.toast{position:fixed;bottom:24px;left:50%;transform:translateX(-50%);background:var(--bone);color:var(--obsidian);font-family:var(--mono);font-size:13px;padding:10px 16px;border-radius:999px;z-index:30} -.step .card{margin-top:12px} -#view-settings .step{max-width:760px} +.qr-row .help{flex:1 1 240px;padding-top:var(--s-2)} -/* 5 October 2026: the version in the brand band, the coin on the balance card, Touch ID / Windows Hello controls */ -.brand .ver{font-size:11px;letter-spacing:.08em;color:var(--ash);margin-left:10px;align-self:center} -.balance-row{display:flex;align-items:center;gap:18px} -.coin.small{width:56px;height:56px;filter:drop-shadow(0 6px 18px rgba(242,84,27,.35))} -.reading{font-size:12px;color:var(--ash);letter-spacing:.04em;margin-top:8px} -.version-row{font-size:12px;color:var(--ash);letter-spacing:.06em;margin-top:-6px} -.version-row b{color:var(--ink-2);font-weight:500} -.btn.fp svg{flex:0 0 auto} -.bio-row{display:flex;flex-direction:column;align-items:center;gap:10px;margin-top:6px} -.bio-row .note{text-align:center;max-width:46ch} -.unlock-dim .unlock{opacity:.7} -#send-go .fp-ico[hidden]{display:none} -.bio-state{display:inline-flex;align-items:center;gap:7px;font-family:var(--mono);font-size:12px;letter-spacing:.04em;color:var(--ember);min-height:18px} -.bio-state.ok{color:var(--molten)} -.bio-state.wait{color:var(--ash)} -.fp-glyph{flex:0 0 auto;color:var(--ember)} -.err .bio-state{font-family:var(--mono)} - -/* the update card (update-card.js, app.js renderUpdateCard; the miner carries the same block): the mark with its - ring, the name, one line, up to three note lines, the size, Install now and Later. Over every screen. */ -.upd-wrap{position:fixed;inset:0;z-index:35;background:rgba(12,12,14,.72);backdrop-filter:blur(6px);-webkit-backdrop-filter:blur(6px);display:flex;align-items:center;justify-content:center;padding:24px;animation:fade .25s ease} +/* the update card (update-card.js, app.js renderUpdateCard; the miner carries the same block) */ +.upd-wrap{position:fixed;inset:0;z-index:35;background:var(--scrim);backdrop-filter:blur(6px);-webkit-backdrop-filter:blur(6px);display:flex;align-items:center;justify-content:center;padding:24px;animation:fade .25s ease} @keyframes fade{from{opacity:0}to{opacity:1}} -.upd-card{position:relative;width:100%;max-width:500px;max-height:calc(100vh - 48px);overflow:auto;background:var(--graphite);border:1px solid var(--line-2);border-radius:22px;padding:34px 32px 28px;display:flex;flex-direction:column;align-items:center;text-align:center;gap:8px;box-shadow:0 30px 80px rgba(0,0,0,.6),0 0 0 1px rgba(242,84,27,.08);animation:rise .3s ease;outline:none;user-select:none;-webkit-user-select:none} +.upd-card{position:relative;width:100%;max-width:500px;max-height:calc(100vh - 48px);overflow:auto;background:var(--graphite);border:1px solid var(--line-2);border-radius:22px;padding:34px 32px 28px;display:flex;flex-direction:column;align-items:center;text-align:center;gap:var(--s-2);box-shadow:0 30px 80px var(--shadow),0 0 0 1px rgba(242,84,27,.08);animation:rise .3s ease;outline:none} .upd-card::before{content:"";position:absolute;left:50%;top:-80px;width:360px;height:260px;transform:translateX(-50%);border-radius:50%;background:radial-gradient(circle,rgba(242,84,27,.22) 0,rgba(242,84,27,0) 62%);pointer-events:none} -.upd-mark{position:relative;width:96px;height:96px;display:flex;align-items:center;justify-content:center;margin-bottom:12px} +.upd-mark{position:relative;width:96px;height:96px;display:flex;align-items:center;justify-content:center;margin-bottom:var(--s-3)} .upd-mark img{position:relative;display:block;filter:drop-shadow(0 6px 18px rgba(242,84,27,.35))} .upd-ring{position:absolute;inset:0;transform:rotate(-90deg)} .upd-ring .track{fill:none;stroke:var(--line-2);stroke-width:3} @@ -436,63 +325,153 @@ body{user-select:text;-webkit-user-select:text} .upd-mark.none .track{stroke:var(--line)} .upd-mark.full .arc{stroke:var(--molten);stroke-dashoffset:0} .upd-mark.busy .arc{stroke-dasharray:69 207.5;stroke-dashoffset:0;animation:spin 1.1s linear infinite;transform-origin:50% 50%} +@keyframes spin{to{transform:rotate(360deg)}} .upd-mark.failed .arc{stroke:var(--ember);stroke-dashoffset:0;opacity:.55} -.upd-pct{position:absolute;left:50%;bottom:-11px;transform:translateX(-50%);font-size:11px;font-weight:500;letter-spacing:.06em;color:var(--molten);background:var(--obsidian);border:1px solid var(--line-2);border-radius:999px;padding:2px 8px;font-variant-numeric:tabular-nums} +.upd-pct{position:absolute;left:50%;bottom:-11px;transform:translateX(-50%);font-size:var(--t-xs);font-weight:500;letter-spacing:.06em;color:var(--molten);background:var(--obsidian);border:1px solid var(--line-2);border-radius:999px;padding:2px 8px;font-variant-numeric:tabular-nums} .upd-name{font-size:24px;font-weight:700;letter-spacing:-.01em;position:relative} -.upd-line{font-size:16px;color:var(--ink-2);line-height:1.4;position:relative} -.upd-cause{font-size:12px;color:var(--ember);line-height:1.5;max-width:40ch;word-break:break-word} -.upd-notes{list-style:none;margin:8px auto 0;padding:0;width:max-content;max-width:100%;display:flex;flex-direction:column;gap:6px;align-items:flex-start;font-size:14px;color:var(--bone);line-height:1.4} +.upd-line{font-size:var(--t-xl);color:var(--ink-2);line-height:1.4;position:relative} +.upd-cause{font-size:var(--t-sm);color:var(--ember);line-height:1.5;max-width:40ch;word-break:break-word} +.upd-notes{list-style:none;margin:var(--s-2) auto 0;padding:0;width:max-content;max-width:100%;display:flex;flex-direction:column;gap:6px;align-items:flex-start;font-size:var(--t-md);color:var(--bone);line-height:1.4} .upd-notes:empty{display:none} .upd-notes li{display:flex;align-items:baseline;gap:10px;text-align:left} .upd-notes li::before{content:"";width:6px;height:6px;border-radius:50%;background:var(--ember);flex:0 0 6px;position:relative;top:-2px} -.upd-more{font:inherit;font-size:12px;font-family:var(--mono);letter-spacing:.08em;text-transform:uppercase;color:var(--ash);background:transparent;border:0;padding:4px 8px;cursor:pointer;border-radius:6px;margin-top:2px} +.upd-more{font:inherit;font-size:var(--t-sm);font-family:var(--mono);letter-spacing:.08em;text-transform:uppercase;color:var(--ash);background:transparent;border:0;padding:4px 8px;cursor:pointer;border-radius:6px;margin-top:2px} .upd-more:hover{color:var(--bone)} -.upd-all{list-style:none;margin:0;padding:10px 14px;width:100%;max-height:150px;overflow:auto;text-align:left;font-size:13px;color:var(--ink-2);line-height:1.5;background:var(--obsidian);border:1px solid var(--line);border-radius:12px;display:flex;flex-direction:column;gap:4px;user-select:text;-webkit-user-select:text} -.upd-meta{font-size:12px;color:var(--ash);letter-spacing:.04em;margin-top:8px;min-height:18px;line-height:1.5;max-width:44ch} +.upd-all{list-style:none;margin:0;padding:10px 14px;width:100%;max-height:150px;overflow:auto;text-align:left;font-size:var(--t-base);color:var(--ink-2);line-height:1.5;background:var(--obsidian);border:1px solid var(--line);border-radius:12px;display:flex;flex-direction:column;gap:4px;user-select:text;-webkit-user-select:text} +.upd-meta{font-size:var(--t-sm);color:var(--ash);letter-spacing:.04em;margin-top:var(--s-2);min-height:18px;line-height:1.5;max-width:44ch} .upd-meta:empty{display:none} -.upd-actions{display:flex;gap:12px;align-items:center;justify-content:center;flex-wrap:wrap;margin-top:16px;position:relative} +.upd-actions{display:flex;gap:var(--s-3);align-items:center;justify-content:center;flex-wrap:wrap;margin-top:var(--s-4);position:relative} .upd-actions:empty{display:none} .upd-actions .btn.primary{min-width:160px} @media (prefers-reduced-motion:reduce){.upd-wrap,.upd-card{animation:none}.upd-ring .arc{transition:none}.upd-mark.busy .arc{animation:none;stroke-dasharray:207.5 69}} -@media (max-height:620px){.upd-card{padding:24px 24px 20px}.upd-mark{width:72px;height:72px;margin-bottom:8px}.upd-mark img{width:32px;height:32px}.upd-name{font-size:20px}} +@media (max-height:620px){.upd-card{padding:24px 24px 20px}.upd-mark{width:72px;height:72px;margin-bottom:var(--s-2)}.upd-mark img{width:32px;height:32px}.upd-name{font-size:20px}} -/* ---- the lock screen (0.1.4, 5 October 2026; ui/lock-screen.js, index.html #screen-unlock) ---- - The coin large and centred on the obsidian ground with the ember glow, the name in Unbounded, the short address - in mono, one primary control. It lies over the main area (absolute, the header stays), enters in ENTER_MS (250 ms) - and the home screen leaves beneath it in the same 250 ms, so locking is a transition, not a cut. The glow breathes - slowly; reduced motion stops it. */ +/* the lock screen (0.1.4, kept; ui/lock-screen.js, index.html #screen-unlock): the mark on the ember glow, the name, + the short address, one primary control. It lies over the main area (the header stays), enters in ENTER_MS (250 ms) + and the home screen leaves beneath it in the same 250 ms. The glow breathes slowly; reduced motion stops it. */ #screen-unlock{position:absolute;inset:0;z-index:5;max-width:none;margin:0;padding:0 var(--gutter);background:var(--obsidian);overflow:auto;animation:lockin .25s ease both} body.locking #screen-unlock{display:block} body.locking #screen-home{animation:lockout .25s ease both;pointer-events:none} @keyframes lockin{from{opacity:0;transform:scale(1.015)}to{opacity:1;transform:none}} @keyframes lockout{to{opacity:0;transform:scale(.985);filter:blur(4px)}} .lock-body{min-height:100%;display:flex;flex-direction:column;align-items:center;justify-content:center;text-align:center;gap:10px;padding:28px 0 36px} -.lock-coin{position:relative;width:220px;height:220px;margin-bottom:22px;flex:0 0 auto} +.lock-coin{position:relative;width:180px;height:180px;margin-bottom:22px;flex:0 0 auto;display:flex;align-items:center;justify-content:center} .lock-coin::before{content:"";position:absolute;inset:-190px;border-radius:50%;background:radial-gradient(circle,rgba(255,179,92,.10) 0,rgba(242,84,27,.05) 40%,rgba(242,84,27,0) 68%);pointer-events:none} .lock-glow{position:absolute;inset:-96px;border-radius:50%;background:radial-gradient(circle,rgba(242,84,27,.38) 0,rgba(242,84,27,.14) 36%,rgba(242,84,27,0) 66%);animation:lockbreathe 6s ease-in-out infinite;pointer-events:none} @keyframes lockbreathe{0%,100%{opacity:.75;transform:scale(1)}50%{opacity:1;transform:scale(1.06)}} -.lock-coin .coin{position:relative;width:220px;height:220px;filter:drop-shadow(0 18px 48px rgba(242,84,27,.45))} +.lock-mark{width:148px;height:148px;border-radius:32px;margin:0;box-shadow:0 18px 48px rgba(242,84,27,.45)} +.lock-mark::before{display:none} .lock-title{font-family:var(--head);font-weight:700;font-size:30px;letter-spacing:-.01em;line-height:1.1;margin-top:4px} -.lock-address{font-size:14px;color:var(--ash);letter-spacing:.06em;margin-top:2px} -/* the control area keeps one height, so "Use password" reveals the field in place and nothing above it moves */ +.lock-address{font-size:var(--t-md);color:var(--ash);letter-spacing:.06em;margin-top:2px} .lock-controls{display:flex;flex-direction:column;align-items:center;gap:10px;margin-top:22px;min-height:120px} .lock-controls.touch{min-height:200px;justify-content:flex-start} .lock-touch{display:flex;flex-direction:column;align-items:center;gap:10px} .lock-btn{min-width:272px;gap:10px} -.lock-line{min-height:20px;font-family:var(--mono);font-size:12px;letter-spacing:.04em;color:var(--ash);text-align:center;max-width:60ch;line-height:1.5;text-wrap:balance} +.lock-line{min-height:20px;font-family:var(--mono);font-size:var(--t-sm);letter-spacing:.04em;color:var(--ash);text-align:center;max-width:60ch;line-height:1.5;text-wrap:balance} .lock-line .bio-state{display:inline} .lock-line .fp-glyph{display:inline-block;vertical-align:-3px;margin-right:6px} -.lock-link{font:inherit;font-size:13px;color:var(--ash);background:transparent;border:0;cursor:pointer;padding:6px 10px;border-radius:6px;text-decoration:underline;text-underline-offset:4px;text-decoration-color:var(--line-2);transition:color .15s ease} +.lock-link{font:inherit;font-size:var(--t-base);color:var(--ash);background:transparent;border:0;cursor:pointer;padding:6px 10px;border-radius:6px;text-decoration:underline;text-underline-offset:4px;text-decoration-color:var(--line-2);transition:color .15s ease} .lock-link:hover{color:var(--bone);text-decoration-color:var(--ash)} +.unlock{display:flex;gap:10px;align-items:center;margin-top:6px} +.unlock input{font:inherit;color:var(--bone);background:var(--graphite);border:1px solid var(--line-2);border-radius:10px;padding:12px 14px;min-width:280px;min-height:52px;user-select:text;-webkit-user-select:text} +.unlock input:focus{outline:none;border-color:var(--ember)} .lock-form{margin-top:0;animation:rise .2s ease} .lock-form input{min-width:260px} .lock-err{min-height:0} .lock-foot{margin-top:18px;opacity:.85} -@media (max-height:720px){.lock-coin,.lock-coin .coin{width:176px;height:176px}.lock-coin{margin-bottom:14px}.lock-glow{inset:-70px}.lock-coin::before{inset:-150px}.lock-title{font-size:26px}.lock-controls{margin-top:14px}.lock-body{padding:16px 0 24px}} +@media (max-height:720px){.lock-coin{width:140px;height:140px;margin-bottom:14px}.lock-mark{width:112px;height:112px;border-radius:26px}.lock-glow{inset:-70px}.lock-coin::before{inset:-150px}.lock-title{font-size:26px}.lock-controls{margin-top:14px}.lock-body{padding:16px 0 24px}} @media (prefers-reduced-motion:reduce){#screen-unlock,body.locking #screen-home,.lock-form{animation:none}.lock-glow{animation:none}} -/* Settings > Touch ID: the idle lock's choices and "Ask for Touch ID when the wallet opens" */ -.select{font:inherit;font-size:14px;color:var(--bone);background:var(--graphite);border:1px solid var(--line-2);border-radius:10px;padding:7px 12px;min-height:38px;cursor:pointer} -.select:focus{outline:none;border-color:var(--ember)} -.idle-row{margin-top:4px} -.idle-label{font-size:14px} +/* Touch ID lines */ +.bio-state{display:inline-flex;align-items:center;gap:7px;font-family:var(--mono);font-size:var(--t-sm);letter-spacing:.04em;color:var(--ember);min-height:18px} +.bio-state.ok{color:var(--molten)} +.bio-state.wait{color:var(--ash)} +.fp-glyph{flex:0 0 auto;color:var(--ember)} +#send-go .fp-ico[hidden]{display:none} + +.toast{position:fixed;left:50%;bottom:16px;transform:translateX(-50%);background:var(--graphite);border:1px solid var(--line-2);border-radius:10px;padding:10px 16px;font-size:var(--t-base);z-index:40;box-shadow:0 10px 30px var(--shadow);max-width:min(90vw,520px);text-align:center} +body.has-rail .toast{left:calc(50% + var(--rail) / 2)} + +/* narrow windows: 1000 the rail folds to icons; 900 the row's reason goes under the word; 720 a bottom tab bar */ +@media (max-width:1000px){ + :root{--rail:76px;--gutter:var(--s-5)} + .rail{padding:12px 8px} + .rail-brand{justify-content:center;padding:6px 0 14px} + .rail-brand .word{display:none} + .nav{flex-direction:column;gap:4px;padding:8px 4px;font-size:10px;letter-spacing:.06em;text-transform:uppercase;font-family:var(--mono);font-weight:500;text-align:center;min-height:52px;justify-content:center} + .nav.on{font-weight:500} + .nav.on::before{left:-9px} + .nav.small{min-height:44px} + .nav-dot{right:8px;top:8px;margin:0} + .rail-status{display:none} + .rail-version{text-align:center;padding:8px 0 0;font-size:10px;white-space:nowrap} + .page-sub{display:none} + .kv>div{grid-template-columns:120px auto 1fr} + .hr-main{grid-template-columns:26px minmax(140px,1fr) auto 30px} + .hr-state{grid-column:2 / 4;flex-direction:row;align-items:baseline;gap:var(--s-3)} +} +@media (max-width:860px){ + .three{grid-template-columns:1fr} + h1{font-size:40px} + .bal .v{font-size:var(--t-hero)} + .words{grid-template-columns:repeat(3,minmax(0,1fr))} + .words.small{grid-template-columns:repeat(4,minmax(0,1fr))} + .disclose-right .dim{max-width:30ch} +} +@media (max-width:720px){ + :root{--rail:0px;--gutter:var(--s-4);--bottom:64px} + body.has-rail .rail{top:auto;bottom:0;left:0;right:0;width:auto;height:64px;flex-direction:row;align-items:center;border-right:0;border-top:1px solid var(--line);padding:0 var(--s-2)} + .rail-brand{display:none} + .rail-nav{flex-direction:row;flex:1;gap:0;justify-content:space-around} + .nav{min-height:56px;padding:6px 2px;font-size:10px;border-radius:10px;flex:1;max-width:120px} + .nav.on::before{display:none} + .rail-foot{display:none} + body.has-rail .narrow-only{display:inline-flex} + body.has-rail .top,body.has-rail .notices,body.has-rail main,body.has-rail .ask-wrap{left:0} + body.has-rail main{bottom:var(--bottom)} + body.has-rail .toast{left:50%;bottom:calc(var(--bottom) + 12px)} + .ask-wrap{bottom:var(--bottom)} + .top{padding:0 var(--s-4)} + .page-title h1{font-size:17px} + .pill{min-width:0} + .bal-row{flex-direction:column;gap:4px;align-items:flex-start} + .bal .v{font-size:var(--t-h2)} + .actions .btn.big{flex:1 1 40%} + .actions .note{flex-basis:100%;margin-left:0} + .addr-big{font-size:var(--t-base)} + .hr-main{grid-template-columns:26px 1fr 30px;gap:var(--s-2) var(--s-3);padding:12px} + .hr-amt{grid-column:1 / 3;grid-row:2;justify-self:start;text-align:left} + .hr-state{grid-column:1 / -1;grid-row:3;flex-direction:column;align-items:flex-start;gap:2px} + .hr-state .why{white-space:normal} + .hr-details{padding-left:12px;padding-right:12px} + .hrow.sent-row .hr-main{grid-template-columns:26px 1fr} + .kv>div{grid-template-columns:96px 1fr;gap:4px var(--s-3)} + .kv .m{grid-column:1 / -1;white-space:normal} + .disclose-right .dim{display:none} + .node-line{white-space:normal;line-height:1.4} + .lead-row{flex-direction:column} + .srow{flex-direction:column;align-items:flex-start;gap:var(--s-2)} + .indent{margin-left:0} + .seg{width:100%} + .seg-b{flex:1;padding:6px 8px} + .step{padding:32px 0} + .words{grid-template-columns:repeat(2,minmax(0,1fr))} + .words.small{grid-template-columns:repeat(3,minmax(0,1fr))} + .checks{grid-template-columns:1fr} + .qr{width:200px;height:200px;flex-basis:200px} + .ask .short-pw{flex:1 1 160px;width:auto} + .lock-btn{min-width:0;width:100%} + .unlock input,.lock-form input{min-width:0;flex:1} + .unlock{width:100%} +} +/* short windows (the 620 px floor): tighter paddings, so the hero and the rows fit */ +@media (max-height:700px){ + :root{--card-pad:18px;--gap:var(--s-3)} + #screen-home{padding:16px 0 20px} + .hero{gap:var(--s-3);padding:var(--s-5) 0 var(--s-6)} + .mark-wrap{width:88px;height:88px} + .mark-wrap img{width:56px;height:56px} + h1{font-size:40px} + .lead{font-size:var(--t-lg)} + .step{padding:32px 0 32px} +} diff --git a/app/igneum-wallet/ui/app.js b/app/igneum-wallet/ui/app.js index 1fdb9660f..c8e1c5ef7 100644 --- a/app/igneum-wallet/ui/app.js +++ b/app/igneum-wallet/ui/app.js @@ -1,611 +1,938 @@ -// Igneum Wallet window. Talks to the engine on the same origin (the token is in the path); polls /api/state every -// 2 s; never holds a key: words and keys only pass through when the engine shows them once. +// Igneum Wallet window (wallet-ui-3, on the miner's system). Talks to the engine on the same origin (the token is in +// the path); polls /api/state every 2 s; never holds a key: words and keys only pass through when the engine shows +// them once. The first block, View, is pure (the words every page shows for a state) and is what view.test.mjs loads: +// the file is run with `module` defined and no `document`, so only View executes there. The DOM block follows. 'use strict'; -const $ = id => document.getElementById(id); -const base = location.pathname.replace(/\/$/, ''); -const api = async (path, body) => { - const r = await fetch(base + path, body === undefined ? {} : { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(body) }); - const j = await r.json().catch(() => ({ ok: false, error: 'bad answer' })); - if (!r.ok || j.ok === false) throw new Error(j.error || ('http ' + r.status)); - return j; -}; -const post = (path, body = {}) => api(path, body); -let state = null, quote = null, sentHash = null, txHash = null, lastPhase = null; -if (location.search.includes('host=mac')) document.body.classList.add('mac'); -// ---- Touch ID / Windows Hello: the window host's bridge (app/mac/Biometric.swift; app/windows/wallet-host.cpp) ---- -// The page posts {id, op, ...}; the host answers window.__igneumBiometric(id, {ok, code, message}). The secret never -// comes this way: the host posts the password to the engine itself, and confirmations are nonces the engine issued. -const bio = (() => { - const pending = new Map(); let seq = 0; - const mac = !!(window.webkit && window.webkit.messageHandlers && window.webkit.messageHandlers.biometric); - const win = !!(window.chrome && window.chrome.webview); - window.__igneumBiometric = (id, r) => { const p = pending.get(id); if (p) { pending.delete(id); p(r || { ok: false, code: 'bad', message: 'no answer' }); } }; - if (win) window.chrome.webview.addEventListener('message', ev => { let d = ev.data; if (typeof d === 'string') { try { d = JSON.parse(d); } catch (e) { return; } } if (d && d.id != null) window.__igneumBiometric(d.id, d); }); - return { - host: mac ? 'mac' : win ? 'windows' : null, - busy: false, - call(op, params = {}) { - return new Promise(res => { - if (!this.host) return res({ ok: false, code: 'nohost', message: what() + ' needs the Igneum Wallet app window.' }); - const id = ++seq; pending.set(id, res); this.busy = true; - const m = Object.assign({ id, op }, params); - if (mac) window.webkit.messageHandlers.biometric.postMessage(m); else window.chrome.webview.postMessage(JSON.stringify(m)); - setTimeout(() => { if (pending.has(id)) { pending.delete(id); res({ ok: false, code: 'timeout', message: what() + ' did not answer.' }); } }, 180000); - }).then(r => { this.busy = false; return r; }); +var View = (function () { + var PAGES = [ + { id: 'home', title: 'Home', sub: 'your balance, your address, the node' }, + { id: 'send', title: 'Send', sub: 'one payment, confirmed here' }, + { id: 'receive', title: 'Receive', sub: 'your address, two ways' }, + { id: 'history', title: 'History', sub: "every transaction, with the node's word" }, + { id: 'settings', title: 'Settings', sub: 'security, backup, this machine, updates, the node' } + ]; + function page(id) { for (var i = 0; i < PAGES.length; i++) if (PAGES[i].id === id) return PAGES[i]; return PAGES[0]; } + function withCommas(n) { return (Number(n) || 0).toLocaleString('en-GB'); } + function shortHex(a, n, m) { n = n || 6; m = m || 4; return a && a.length > n + m + 2 ? a.slice(0, 2 + n) + '…' + a.slice(-m) : (a || ''); } + // wei (a decimal string) to IGN with up to `places` decimals, trailing zeros cut; never scientific + function ign(wei, places) { + places = places == null ? 6 : places; + var w; try { w = BigInt(String(wei || '0').trim() || '0'); } catch (e) { return '0'; } + var neg = w < 0n; if (neg) w = -w; + var whole = w / 1000000000000000000n, frac = (w % 1000000000000000000n).toString().padStart(18, '0').slice(0, places).replace(/0+$/, ''); + return (neg ? '-' : '') + (frac ? whole + '.' + frac : String(whole)); + } + function ago(s) { s = Math.max(0, Math.round(s || 0)); return s < 60 ? s + ' s ago' : s < 3600 ? Math.floor(s / 60) + ' min ago' : s < 86400 ? Math.floor(s / 3600) + ' h ago' : Math.floor(s / 86400) + ' d ago'; } + function timeWord(t, now) { + if (!t) return ''; + var d = new Date(t * 1000), n = new Date((now || Date.now() / 1000) * 1000); + var hm = d.toLocaleTimeString('en-GB', { hour: '2-digit', minute: '2-digit' }); + var same = d.getFullYear() === n.getFullYear() && d.getMonth() === n.getMonth() && d.getDate() === n.getDate(); + return same ? hm : d.toLocaleDateString('en-GB', { day: 'numeric', month: 'short' }) + ' ' + hm; + } + + // ---------- the balance and its money line ---------- + // the hero shows the number only when it is known; otherwise one line says why (never a 0 for an unknown) + function balanceLine(s) { + var n = s.node || {}; + if (s.balance_known) return { known: true, text: '' }; + if (s.scanning) return { known: false, text: 'reading the chain, ' + withCommas(s.scanned_to || 0) + ' of ' + withCommas(n.block || 0) + ' blocks' }; + if (n.state === 'ok') return { known: false, text: 'reading the balance' }; + if (n.state === 'starting' || n.state === 'connecting') return { known: false, text: 'waiting for the node to start' }; + return { known: false, text: 'waiting for a node' }; + } + // money first when a market price exists (price_gbp_per_ign on the state, owed by the engine); else one grey line + function moneyLine(s) { + var p = Number(s.price_gbp_per_ign || 0); + if (p > 0 && s.balance_known) { + var v = parseFloat(s.balance || '0') * p; + return { money: '£' + v.toLocaleString('en-GB', { minimumFractionDigits: 2, maximumFractionDigits: 2 }), text: "at today's price" }; } - }; + var net = (s.settings && s.settings.network) || 'devnet'; + return { money: '', text: net === 'devnet' ? 'no market price on devnet: coins have no value' : 'no market price yet' }; + } + + // ---------- the node line ---------- + function nodeWords(n) { + switch (n.state) { + case 'ok': return { word: 'synced', tone: 'ok' }; + case 'starting': return { word: 'starting', tone: '' }; + case 'connecting': return { word: 'connecting', tone: '' }; + case 'lost': return { word: 'lost', tone: 'bad' }; + } + return { word: n.source === 'none' || !n.source ? 'not found' : 'off', tone: 'bad' }; + } + function sourceWords(src) { + switch (src) { + case 'miner': return "the miner app's node"; + case 'own': return 'the bundled node'; + case 'public': return 'the public RPC'; + case 'external': return 'the node named by the environment'; + } + return 'no node'; + } + function verifyWords(s) { + var f = s.finality || {}, n = s.node || {}; + if (f.verified_index > 0) return 'verified to checkpoint ' + withCommas(f.verified_index); + if (n.source === 'public') return 'certificates cannot be verified without a node'; + if (n.state !== 'ok') return ''; + return 'waiting for the first certificate'; + } + // "Node synced · the miner app's node · block 140,286 · verified to checkpoint 4674" + function nodeLine(s) { + var n = s.node || {}, w = nodeWords(n), parts = ['Node ' + w.word]; + if (n.source && n.source !== 'none') parts.push(sourceWords(n.source)); + if (n.block > 0) parts.push('block ' + withCommas(n.block)); + var v = verifyWords(s); if (v) parts.push(v); + var sub = n.state === 'ok' ? '' : n.state === 'lost' ? 'the node stopped answering; trying again' : (n.message || ''); + return { text: parts.join(' · '), sub: sub, tone: w.tone }; + } + // the old Node and Finality cards as rows with one-line meanings: [key, value, meaning, class] + function nodeDetails(s, now) { + var n = s.node || {}, f = s.finality || {}, rows = []; + rows.push(['source', sourceWords(n.source), n.message || '', n.state === 'ok' ? 'ok' : 'warn']); + rows.push(['chain', n.chain || (n.chain_id ? 'chain id ' + n.chain_id : 'not read yet'), n.chain_id ? 'chain id ' + n.chain_id : 'the chain the node follows', n.chain ? '' : 'dim']); + rows.push(['block', n.block > 0 ? withCommas(n.block) : 'none yet', 'the newest block this node holds', n.block > 0 ? '' : 'dim']); + rows.push(['endpoint', n.evm || 'none yet', 'where this wallet reads balances and sends', n.evm ? '' : 'dim']); + rows.push(['finality on the node', n.source === 'public' ? 'not checkable without a node' : n.finality_active ? 'active, locked ' + withCommas(n.latest_locked) : 'paused', n.source === 'public' ? 'the public RPC carries no certificates' : 'what the node says; the wallet checks for itself below', n.finality_active ? 'ok' : 'dim']); + if (f.verified_index > 0) { + rows.push(['verified here', 'checkpoint ' + withCommas(f.verified_index), 'the newest certificate this wallet checked itself', 'ok']); + rows.push(['signed by', f.signers + ' of ' + f.voters + ' voters', ((f.fraction_total || 0) * 100).toFixed(1) + '% of all weight; two thirds is the rule', '']); + rows.push(['checkpoint height', f.chain_number == null ? 'pending' : withCommas(f.chain_number), 'blocks at or under it are final', f.chain_number == null ? 'dim' : '']); + rows.push(['weights', f.weights_exact ? 'taken at the checkpoint' : 'the latest table', 'the voter weights the signature was checked against', '']); + rows.push(['checked', f.verified_at ? ago((now || Date.now() / 1000) - f.verified_at) : 'not yet', '', '']); + } else { + rows.push(['verified here', 'nothing yet', f.message || 'waiting', 'dim']); + } + return rows; + } + // the pill: the wallet's own words + function pillWords(s) { + if (s.quitting) return { text: 'stopping', cls: '' }; + if (s.phase === 'unlock') return { text: 'locked', cls: '' }; + var n = s.node || {}; + if (n.state === 'ok') return { text: n.source === 'public' ? 'public rpc' : 'synced', cls: 'on' }; + if (n.state === 'starting' || n.state === 'connecting') return { text: n.state, cls: '' }; + if (n.state === 'lost') return { text: 'node lost', cls: 'warn' }; + return { text: 'no node', cls: 'warn' }; + } + + // ---------- the history row ---------- + function kindWord(e) { return { sent: 'Sent', received: 'Received', self: 'To yourself', reward: 'Reward', proving: 'Proving payout' }[e.kind] || e.kind; } + function incoming(e) { return e.kind === 'received' || e.kind === 'reward' || e.kind === 'proving'; } + function synthetic(e) { return /^(reward|proving)-/.test(e.hash || ''); } + function whoLine(e) { + if (e.kind === 'reward') return 'block reward'; + if (e.kind === 'proving') return 'shard payout'; + if (e.kind === 'self') return 'to yourself'; + return incoming(e) ? 'from ' + shortHex(e.from) : 'to ' + shortHex(e.to); + } + // ONE state word per row, never stronger than the chain's own (ledger P17): the wallet's own verified "final" wins + // as finalised; failed from the receipt; else the node's word (igneum_getTransactionStatus); else the wallet's label + function stateWord(e, nodeState) { + var blk = e.block > 0 ? 'block ' + withCommas(e.block) : 'a block'; + if (e.finality === 'final') return { word: 'finalised', tone: 'ok', why: 'under checkpoint ' + withCommas(e.checkpoint || 0) + ', verified here' }; + if (e.finality === 'failed') return { word: 'failed', tone: 'bad', why: 'the execution failed; the fee was still paid' }; + if (synthetic(e)) return e.finality === 'in_block' ? { word: 'executed', tone: 'ink', why: 'in ' + blk } : { word: 'pending', tone: '', why: 'waiting for a block' }; + switch (nodeState) { + case 'pending': return { word: 'pending', tone: '', why: 'waiting for a block' }; + case 'included': return { word: 'included', tone: '', why: 'in ' + blk + ', not executed yet' }; + case 'executed': return { word: 'executed', tone: 'ink', why: 'in ' + blk }; + case 'proven': return { word: 'proven', tone: 'ink', why: 'in ' + blk + ', proof paid' }; + case 'finalised': return { word: 'finalised', tone: 'ink', why: 'under a locked checkpoint, not yet verified here' }; + case 'finality not active': return { word: 'finality not active', tone: 'ink', why: 'under a locked checkpoint; finality is paused on the network' }; + case 'unknown': return { word: 'pending', tone: '', why: 'not in any block the node holds' }; + } + if (e.finality === 'in_block') return { word: 'included', tone: '', why: 'in ' + blk }; + return { word: 'pending', tone: '', why: 'waiting for a block' }; + } + function rowModel(e, nodeState, now) { + var inn = incoming(e), st = stateWord(e, nodeState); + return { hash: e.hash, kind: kindWord(e), who: whoLine(e), when: timeWord(e.time, now), amount: (inn ? '+' : '−') + ign(e.value, 6), in: inn, state: st, synthetic: synthetic(e), nodeWord: nodeState || '' }; + } + // the rows the page asks the node about: not final, not failed, not a reward; newest first, at most `max` + function needsNodeWord(list, max) { + var out = []; + for (var i = 0; i < (list || []).length && out.length < (max || 12); i++) { + var e = list[i]; + if (e.finality === 'final' || e.finality === 'failed' || synthetic(e)) continue; + out.push(e.hash); + } + return out; + } + + // ---------- send ---------- + function gwei(weiPerGas) { var v = Number(weiPerGas || 0) / 1e9; return (v >= 10 ? Math.round(v) : +v.toFixed(v >= 1 ? 1 : 3)) + ' gwei'; } + function feeWords(q) { + if (!q) return { line: 'shown when you review', detail: '' }; + return { + line: 'at most ' + q.fee_max_ign + ' IGN', + detail: 'Gas ' + withCommas(q.gas) + ' at a base fee of ' + q.base_fee_gwei + ' gwei (burned) plus a ' + q.tip_gwei + ' gwei tip (to the miner), capped at ' + gwei(q.max_fee) + '; what is not used comes back.' + }; + } + function sendAsk(q, touch, bioName) { + return { + text: 'Send ' + q.value_ign + ' IGN to ' + shortHex(q.display_to || q.to) + '? Fee at most ' + q.fee_max_ign + ' IGN; ' + q.total_max_ign + ' IGN leaves the wallet at most.', + button: touch ? 'Confirm with ' + (bioName || 'Touch ID') : 'Send now' + }; + } + + // ---------- settings ---------- + function bioSentence(b, what, host) { + b = b || {}; + if (b.enrolled) return what + ' is on. It unlocks the wallet, confirms each send and shows the backup; the password still works everywhere.'; + if (!host) return what + ' needs the Igneum Wallet app window; this page is open in a browser.'; + if (!b.available) return b.message || (what + (what === 'Touch ID' ? ' is not set up on this Mac.' : ' is not set up on this PC.')); + return 'Unlock, confirm each send and show the backup with ' + what + '. The password still works everywhere.'; + } + function idleSentence(min, what) { return Number(min) > 0 ? 'The key is cleared after ' + LockScreenIdle(min) + ' without you; ' + what + ' or the password opens it again.' : 'The wallet stays open until you lock it.'; } + function LockScreenIdle(min) { var m = Number(min) || 0; return m <= 0 ? 'never' : m === 60 ? '1 hour' : m % 60 === 0 ? (m / 60) + ' hours' : m === 1 ? '1 minute' : m + ' minutes'; } + + // ---------- updates: the strip line and the card's note ---------- + function cap(t) { return t ? t.charAt(0).toUpperCase() + t.slice(1) : ''; } + function updateLine(u) { + var v = 'Igneum Wallet ' + u.version; + if (u.urgent && u.urgent_text) return { text: u.urgent_text + (u.status === 'downloading' ? ' Downloading.' : ''), urgent: true, prog: u.status === 'downloading' }; + switch (u.status) { + case 'available': return { text: v + ' is available. Downloading it.' }; + case 'downloading': return { text: 'Downloading ' + v + (u.size ? ' (' + Math.round(u.size / 1e6) + ' MB)' : '') + ': ' + Math.round((u.progress || 0) * 100) + '%', prog: true }; + case 'staging': return { text: v + ' downloaded and verified. Preparing it.' }; + case 'ready': return { text: v + ' is ready. ' + (u.wait ? cap(u.wait) + '.' : 'It installs as soon as nothing is being sent.'), install: true }; + case 'applying': return { text: 'Installing ' + v + ': ' + (u.wait ? u.wait + '.' : 'the app closes and opens again by itself.') }; + case 'deferred': return { text: v + ' is downloaded. Windows asked for permission and nobody answered; it installs the next time someone is at this PC.', install: true }; + case 'manual': return { text: v + ' is downloaded. ' + cap(u.wait || 'open the download and drag the app over the old one.'), open: true }; + case 'error': return { text: 'Update: ' + (u.error || 'failed') + '.', install: !!(u.ready || u.downloaded) }; + } + return null; + } + // the update card on Settings: "up to date, checked 13 min ago" + function updateNote(u, now) { + var l = updateLine(u), parts = []; + if (u.updated_from) parts.push('Updated from ' + u.updated_from + '.'); + if (u.rolled_back) parts.push('Rolled back: ' + u.rolled_back + '.'); + if (u.status === 'off') parts.push('Updates are off in this build.'); + else if (l && !(u.rolled_back && u.status === 'error')) parts.push(l.text); + else if (u.status === 'checking') parts.push('Checking.'); + else if (u.status === 'current') parts.push('Up to date' + (u.checked_at ? ', checked ' + ago((now || Date.now() / 1000) - u.checked_at) : '') + '.'); + else if (u.error) parts.push(u.error); + else parts.push('Not checked yet.'); + return parts.join(' '); + } + + return { PAGES: PAGES, page: page, withCommas: withCommas, shortHex: shortHex, ign: ign, ago: ago, timeWord: timeWord, balanceLine: balanceLine, moneyLine: moneyLine, nodeWords: nodeWords, sourceWords: sourceWords, verifyWords: verifyWords, nodeLine: nodeLine, nodeDetails: nodeDetails, pillWords: pillWords, kindWord: kindWord, whoLine: whoLine, stateWord: stateWord, rowModel: rowModel, needsNodeWord: needsNodeWord, gwei: gwei, feeWords: feeWords, sendAsk: sendAsk, bioSentence: bioSentence, idleSentence: idleSentence, updateLine: updateLine, updateNote: updateNote, cap: cap }; })(); -function what() { const k = state && state.biometric && state.biometric.kind; return k === 'hello' || (!k && /Win/.test(navigator.platform)) ? 'Windows Hello' : 'Touch ID'; } -// the page's own line after the system prompt: the glyph in ember and what happened, in our words -const FP = ''; -function bioLine(r, okText) { - if (!r) return ''; - if (r.ok) return `${FP}${esc(what() + ' ' + (okText || 'confirmed'))}`; - const w = what(), c = r.code; - let t; - if (c === 'cancelled' || c === 'fallback') t = w + ' cancelled, enter your password'; - else if (c === 'failed') t = w + ' did not match, try again or enter your password'; - else if (c === 'locked') t = w + ' is locked, enter your password'; - else if (c === 'invalidated') t = w + ' was turned off (a fingerprint changed), enter your password and turn it on again in Settings'; - else if (c === 'not_set_up' || c === 'unavailable') t = r.message || (w + ' is not set up on this Mac'); - else if (c === 'nohost') t = w + ' needs the Igneum Wallet app window'; - else t = r.message || (w + ' did not succeed'); - return `${FP}${esc(t)}`; -} -function setLine(el, html) { el.innerHTML = html; } -// ?bio=touch: the enrolled look without a host (screenshots); a tap then gets the "needs the app window" line -const forcedBio = new URLSearchParams(location.search).get('bio') === 'touch'; -function hostHere() { return !!bio.host || forcedBio; } +if (typeof module === 'object' && module && module.exports) { module.exports = { View: View }; } -// ---- the lock screen (ui/lock-screen.js): the sheet on a tap, once by itself on the first arrival ---- -// firstPhase: the first phase this page saw ('unlock' means the app opened locked); autoMem.used: the one chance went -let firstPhase = null, locking = false, autoTimer = 0, pwRevealed = false; -const autoMem = { used: false }; -const reducedMotion = () => !!(window.matchMedia && window.matchMedia('(prefers-reduced-motion: reduce)').matches); -function lockLayout() { - const s = forcedBio && state ? Object.assign({}, state, { biometric: Object.assign({}, state.biometric, { enrolled: true, available: true, kind: 'touchid' }) }) : state; - return LockScreen.layout(s, hostHere()); -} -function renderLock() { - const lay = lockLayout(); - $('unlock-address').textContent = lay.address; - $('unlock-touch-text').textContent = 'Unlock with ' + what(); - $('unlock-bio').hidden = !lay.touch; - document.querySelector('.lock-controls').classList.toggle('touch', lay.touch); - const showForm = !lay.touch || pwRevealed; - $('unlock-form').hidden = !showForm; - $('unlock-use-pw').hidden = !lay.touch || pwRevealed; -} -function showPasswordField() { - pwRevealed = true; renderLock(); - $('unlock-pw').focus({ preventScroll: true }); -} -function hidePasswordField() { - if (!lockLayout().touch) return; - pwRevealed = false; $('unlock-pw').value = ''; $('unlock-err').textContent = ''; renderLock(); - $('unlock-touch').focus({ preventScroll: true }); -} -function focusLock() { - const lay = lockLayout(); - if (lay.touch && !pwRevealed) $('unlock-touch').focus({ preventScroll: true }); else $('unlock-pw').focus({ preventScroll: true }); -} -// the home screen leaves and the lock screen enters together, ENTER_MS (250 ms); then the phase flips -function lockTransition() { - locking = true; document.body.classList.add('locking'); $('main').scrollTop = 0; - pwRevealed = false; $('unlock-pw').value = ''; $('unlock-err').textContent = ''; setLine($('unlock-bio-note'), ''); renderLock(); - setTimeout(() => { locking = false; document.body.classList.remove('locking'); setPhase('unlock'); onLockScreen('lock'); }, reducedMotion() ? 0 : LockScreen.ENTER_MS); -} -// the lock screen is up: reason is arrival (the page opened on it), lock (idle or by hand) or focus -function onLockScreen(reason) { - clearTimeout(autoTimer); - if (reason !== 'lock') { pwRevealed = false; $('unlock-pw').value = ''; $('unlock-err').textContent = ''; setLine($('unlock-bio-note'), ''); } - renderLock(); focusLock(); - const lay = lockLayout(); - const d = LockScreen.autoPrompt({ reason, phase: 'unlock', touch: lay.touch, askOnOpen: !!(state.settings && state.settings.ask_on_open), hidden: document.hidden, busy: bio.busy, firstPhase, updatedFrom: state.update && state.update.updated_from }, autoMem); - if (reason === 'arrival') autoMem.used = true; - if (d.prompt) autoTimer = setTimeout(() => { if (state && state.phase === 'unlock' && !document.hidden && !bio.busy && !pwRevealed) unlockWithTouch(); }, (reducedMotion() ? 0 : LockScreen.ENTER_MS) + d.delay); -} -async function unlockWithTouch() { - if (bio.busy) return; - clearTimeout(autoTimer); - $('unlock-touch').disabled = true; setLine($('unlock-bio-note'), `${FP}${esc('Waiting for ' + what())}`); - const r = await bio.call('unlock'); - $('unlock-touch').disabled = false; - const l = LockScreen.line(r, what()); - setLine($('unlock-bio-note'), l.text ? `${FP}${esc(l.text)}` : ''); - if (r.ok) { await poll(); return; } - // a cancel or a miss: the button again, never another sheet by itself; the password when the sheet cannot help - if (l.password) showPasswordField(); else $('unlock-touch').focus({ preventScroll: true }); -} -$('unlock-use-pw').onclick = showPasswordField; -$('unlock-pw').addEventListener('keydown', e => { if (e.key === 'Escape') { e.preventDefault(); hidePasswordField(); } }); -// the idle lock: the window tells the engine a person is here, every 20 s while there is input -let active = false; -['mousemove', 'keydown', 'mousedown', 'wheel', 'touchstart'].forEach(e => document.addEventListener(e, () => { active = true; }, { passive: true })); -setInterval(() => { if (active && state && state.phase === 'home') { active = false; post('/api/activity').catch(() => {}); } }, 20000); - -function toast(text) { const t = $('toast'); t.textContent = text; t.hidden = false; clearTimeout(t._h); t._h = setTimeout(() => { t.hidden = true; }, 1800); } -function copy(text, what) { navigator.clipboard.writeText(text).then(() => toast((what || 'copied') + ' to the clipboard'), () => toast('could not copy')); } -function ign(wei, places = 6) { - const w = BigInt(wei || 0); const whole = w / 10n ** 18n; let frac = (w % 10n ** 18n).toString().padStart(18, '0').slice(0, places).replace(/0+$/, ''); - return frac ? `${whole}.${frac}` : `${whole}`; -} -function short(a) { return a ? a.slice(0, 8) + '…' + a.slice(-6) : ''; } -function when(t) { if (!t) return ''; const d = new Date(t * 1000); return d.toLocaleDateString(undefined, { day: 'numeric', month: 'short' }) + ' ' + d.toLocaleTimeString(undefined, { hour: '2-digit', minute: '2-digit' }); } -function setPhase(p) { document.body.dataset.phase = p; } -function setView(v) { document.body.dataset.view = v; window.scrollTo(0, 0); $('main').scrollTop = 0; } -function kv(el, rows) { el.innerHTML = rows.map(([k, v, cls]) => `${k}${v}`).join(''); } -const esc = s => String(s).replace(/[&<>"]/g, c => ({ '&': '&', '<': '<', '>': '>', '"': '"' }[c])); - -// ---- state ---- -async function poll() { - try { state = await api('/api/state'); render(); } catch (e) { $('pill-text').textContent = 'engine gone'; $('pill').className = 'pill warn'; } -} -function render() { - const s = state; - const phase = s.phase; - const inFlow = ['create', 'import'].includes(document.body.dataset.phase); - if (firstPhase === null) firstPhase = phase; - if ((!inFlow || phase === 'home') && lastPhase !== phase && !locking) { - if (lastPhase === 'home' && phase === 'unlock') lockTransition(); - else { setPhase(phase); if (phase === 'home') setView('overview'); if (phase === 'unlock') onLockScreen(lastPhase === null ? 'arrival' : 'lock'); } +if (typeof document !== 'undefined') (function () { + var $ = function (id) { return document.getElementById(id); }; + var base = location.pathname.replace(/\/$/, ''); + var api = async function (path, body) { + var r = await fetch(base + path, body === undefined ? {} : { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(body) }); + var j = await r.json().catch(function () { return { ok: false, error: 'bad answer' }; }); + if (!r.ok || j.ok === false) throw new Error(j.error || ('http ' + r.status)); + return j; + }; + var post = function (path, body) { return api(path, body || {}); }; + var esc = function (s) { return String(s == null ? '' : s).replace(/[&<>"]/g, function (c) { return { '&': '&', '<': '<', '>': '>', '"': '"' }[c]; }); }; + var state = null, quote = null, sentHash = null, lastPhase = null, page = 'home', firstHome = true; + var params = new URLSearchParams(location.search); + if (params.get('host') === 'mac') document.body.classList.add('mac'); + function stored(k) { try { return localStorage.getItem(k); } catch (e) { return null; } } + function store(k, v) { try { if (v == null) localStorage.removeItem(k); else localStorage.setItem(k, v); } catch (e) { /* private mode */ } } + function setText(id, t) { var el = $(id); if (el && el.textContent !== t) el.textContent = t; } + function toast(text) { var t = $('toast'); t.textContent = text; t.hidden = false; clearTimeout(t._h); t._h = setTimeout(function () { t.hidden = true; }, 1800); } + function copyText(text, what) { + if (!text) { toast('Nothing to copy yet'); return; } + var done = function () { toast((what || 'Copied') + (what ? ' copied' : '')); }; + if (navigator.clipboard && navigator.clipboard.writeText) navigator.clipboard.writeText(text).then(done, function () { fallbackCopy(text, done); }); else fallbackCopy(text, done); } - lastPhase = phase; - $('ver').textContent = 'v' + s.version; - $('btn-settings').hidden = phase !== 'home'; - $('btn-lock').hidden = phase !== 'home'; - // pill - const n = s.node; - let pillText = 'no node', pillCls = 'pill warn'; - if (n.state === 'ok') { pillText = `${n.source} node · block ${n.block.toLocaleString()}`; pillCls = 'pill on'; } - else if (n.state === 'starting' || n.state === 'connecting') { pillText = n.state; pillCls = 'pill'; } - else if (n.source === 'public') { pillText = 'public rpc'; pillCls = 'pill'; } - if (s.quitting) { pillText = 'stopping'; pillCls = 'pill'; } - $('pill-text').textContent = pillText; $('pill').className = pillCls; - $('welcome-eyebrow').textContent = `${s.settings.network === 'devnet' ? 'devnet v4' : s.settings.network} · nothing is bought or sold`; - if (forcedUpdate) s.update = sampleUpdate(forcedUpdate); - renderUpdate(s); - renderUpdateCard(s); - // the lock screen: the fingerprint button when enrolled and the app window is the host, else the password field - if (phase === 'unlock' || locking) renderLock(); - // home - $('balance').textContent = s.balance_known ? s.balance : '0'; - $('balance').classList.toggle('dim', !s.balance_known); - const note = $('balance-note'); - if (s.scanning) { note.textContent = `reading the chain, ${(s.scanned_to == null ? 0 : s.scanned_to).toLocaleString()} of ${n.block.toLocaleString()} blocks`; note.hidden = false; } - else if (!s.balance_known) { note.textContent = n.state === 'ok' ? 'reading the balance' : 'waiting for a node'; note.hidden = false; } - else note.hidden = true; - $('home-address').textContent = s.display; - $('backup-note').hidden = s.backed_up; - kv($('node-kv'), [ - ['source', esc(n.source === 'miner' ? 'the miner app' : n.source === 'own' ? 'bundled node' : n.source === 'public' ? 'public RPC' : n.source), n.state === 'ok' ? 'ok' : 'warn'], - ['chain', esc(n.chain || (n.chain_id ? 'id ' + n.chain_id : '…'))], - ['block', n.block.toLocaleString()], - ['finality', n.finality_active ? `active, locked ${n.latest_locked}` : (n.source === 'public' ? 'not checkable without a node' : 'paused')], - ['note', esc(n.message)], - ]); - const f = s.finality; - kv($('fin-kv'), f.verified_index ? [ - ['verified here', `checkpoint ${f.verified_index}`, 'ok'], - ['signed', `${f.signers} of ${f.voters} voters, ${(f.fraction_total * 100).toFixed(1)}% of weight`], - ['chain height', f.chain_number == null ? 'pending' : f.chain_number.toLocaleString()], - ['weights', f.weights_exact ? 'at the checkpoint' : 'latest table'], - ['checked', when(f.verified_at)], - ] : [['status', esc(f.message || 'waiting'), 'warn']]); - $('scan-note').textContent = s.scanning ? `· reading blocks (${s.scanned_to == null ? 0 : s.scanned_to.toLocaleString()} of ${n.block.toLocaleString()})` : ''; - renderHistory(s.history); - // settings - $('start-login').checked = !!s.settings.start_at_login; - renderBio(s); - kv($('settings-node-kv'), [['source', esc(n.source)], ['ethereum rpc', esc(n.evm || 'none')], ['grpc', esc(n.grpc || 'none')], ['chain id', n.chain_id || '…'], ['network', esc(s.settings.network)], ['public rpc', esc(s.public_rpc || 'none in this build')]]); - kv($('machine-kv'), [['machine', esc(s.machine_id.slice(0, 8))], ['version', esc(s.version)], ['logs', esc(s.log_dir)], ['miner key file', s.miner_wallet_present ? 'present' : 'none']]); - $('seg-miner').disabled = !s.miner_wallet_present; - if (document.body.dataset.view === 'tx' && txHash) renderTx(); -} -function renderHistory(list) { - const el = $('history'); - if (!list || !list.length) { el.innerHTML = `
${state.scanning ? 'Reading the chain.' : 'Nothing yet. Receive IGN, or point the miner app at this address.'}
`; return; } - el.innerHTML = list.slice(0, 60).map(e => { - const incoming = e.kind === 'received' || e.kind === 'reward' || e.kind === 'proving'; - const who = e.kind === 'reward' ? 'block reward' : e.kind === 'proving' ? 'shard payout' : e.kind === 'self' ? 'to yourself' : incoming ? 'from ' + short(e.from) : 'to ' + short(e.to); - const fin = e.finality === 'final' ? `final · cp ${e.checkpoint}` : e.finality === 'in_block' ? `in block ${e.block}` : e.finality; - return `
${esc(e.kind)}${esc(who)} · ${when(e.time)}${incoming ? '+' : '−'}${ign(e.value)} IGN${esc(fin)}
`; - }).join(''); - el.querySelectorAll('.row').forEach(r => r.addEventListener('click', () => openTx(r.dataset.hash))); -} + function fallbackCopy(text, done) { var ta = document.createElement('textarea'); ta.value = text; ta.style.position = 'fixed'; ta.style.opacity = '0'; document.body.appendChild(ta); ta.select(); try { document.execCommand('copy'); done(); } catch (e) { toast('Select and copy it by hand'); } document.body.removeChild(ta); } + document.addEventListener('click', function (e) { + var b = e.target.closest('[data-copy],[data-copy-text]'); if (!b) return; + if (b.dataset.copyText != null) { copyText(b.dataset.copyText, b.dataset.copyWhat || ''); return; } + var el = $(b.getAttribute('data-copy')), t = el ? el.textContent : ''; + if (!t || /^(not set|none yet|not read)/.test(t)) { toast('Nothing to copy yet'); return; } + copyText(t, b.dataset.copyWhat || ''); + }); -// ---- create ---- -$('go-create').onclick = () => { setPhase('create'); $('create-1').hidden = false; $('create-2').hidden = true; $('create-3').hidden = true; $('create-pw').focus(); }; -$('create-back').onclick = () => setPhase('welcome'); -let words = []; -$('create-next').onclick = async () => { - $('create-err').textContent = ''; - const a = $('create-pw').value, b = $('create-pw2').value; - if (a.length < 8) return $('create-err').textContent = 'at least 8 characters'; - if (a !== b) return $('create-err').textContent = 'the two passwords differ'; - try { - const r = await post('/api/create', { password: a }); - words = r.words; - $('words').innerHTML = words.map(w => `
  • ${esc(w)}
  • `).join(''); - $('create-address').textContent = r.display; - $('create-1').hidden = true; $('create-2').hidden = false; - } catch (e) { $('create-err').textContent = e.message; } -}; -$('create-written').onclick = () => { - const picks = []; while (picks.length < 3) { const p = 1 + Math.floor(Math.random() * 24); if (!picks.includes(p)) picks.push(p); } - picks.sort((a, b) => a - b); - $('checks').innerHTML = picks.map(p => ``).join(''); - $('words').innerHTML = ''; words = []; - $('create-2').hidden = true; $('create-3').hidden = false; - $('checks').querySelector('input').focus(); -}; -$('create-again').onclick = () => { setPhase('create'); $('create-3').hidden = true; $('create-1').hidden = false; $('confirm-err').textContent = 'start again: the words are made fresh each time'; }; -$('create-confirm').onclick = async () => { - $('confirm-err').textContent = ''; - const checks = [...$('checks').querySelectorAll('input')].map(i => ({ position: Number(i.dataset.pos), word: i.value.trim() })); - try { await post('/api/create/confirm', { checks }); $('checks').innerHTML = ''; await poll(); setPhase('home'); setView('overview'); toast('wallet ready'); } - catch (e) { $('confirm-err').textContent = e.message; } -}; + // ---------- Touch ID / Windows Hello: the window host's bridge (unchanged from 0.1.4) ---------- + var bio = (function () { + var pending = new Map(), seq = 0; + var mac = !!(window.webkit && window.webkit.messageHandlers && window.webkit.messageHandlers.biometric); + var win = !!(window.chrome && window.chrome.webview); + window.__igneumBiometric = function (id, r) { var p = pending.get(id); if (p) { pending.delete(id); p(r || { ok: false, code: 'bad', message: 'no answer' }); } }; + if (win) window.chrome.webview.addEventListener('message', function (ev) { var d = ev.data; if (typeof d === 'string') { try { d = JSON.parse(d); } catch (e) { return; } } if (d && d.id != null) window.__igneumBiometric(d.id, d); }); + return { + host: mac ? 'mac' : win ? 'windows' : null, + busy: false, + call: function (op, p) { + var self = this; + return new Promise(function (res) { + if (!self.host) return res({ ok: false, code: 'nohost', message: what() + ' needs the Igneum Wallet app window.' }); + var id = ++seq; pending.set(id, res); self.busy = true; + var m = Object.assign({ id: id, op: op }, p || {}); + if (mac) window.webkit.messageHandlers.biometric.postMessage(m); else window.chrome.webview.postMessage(JSON.stringify(m)); + setTimeout(function () { if (pending.has(id)) { pending.delete(id); res({ ok: false, code: 'timeout', message: what() + ' did not answer.' }); } }, 180000); + }).then(function (r) { self.busy = false; return r; }); + } + }; + })(); + function what() { var k = state && state.biometric && state.biometric.kind; return k === 'hello' || (!k && /Win/.test(navigator.platform)) ? 'Windows Hello' : 'Touch ID'; } + var FP = ''; + function bioLine(r, okText) { + if (!r) return ''; + if (r.ok) return '' + FP + esc(what() + ' ' + (okText || 'confirmed')) + ''; + var w = what(), c = r.code, t; + if (c === 'cancelled' || c === 'fallback') t = w + ' cancelled, enter your password'; + else if (c === 'failed') t = w + ' did not match, try again or enter your password'; + else if (c === 'locked') t = w + ' is locked, enter your password'; + else if (c === 'invalidated') t = w + ' was turned off (a fingerprint changed), enter your password and turn it on again in Settings'; + else if (c === 'not_set_up' || c === 'unavailable') t = r.message || (w + ' is not set up on this Mac'); + else if (c === 'nohost') t = w + ' needs the Igneum Wallet app window'; + else t = r.message || (w + ' did not succeed'); + return '' + FP + esc(t) + ''; + } + function setLine(el, html) { el.innerHTML = html; } + var forcedBio = params.get('bio') === 'touch'; + function hostHere() { return !!bio.host || forcedBio; } -// ---- import ---- -let importMode = 'seed'; -$('go-import').onclick = () => { setPhase('import'); }; -$('import-back').onclick = () => setPhase('welcome'); -$('import-mode').querySelectorAll('.seg').forEach(b => b.onclick = () => { - importMode = b.dataset.mode; - $('import-mode').querySelectorAll('.seg').forEach(x => x.classList.toggle('on', x === b)); - $('import-data-field').hidden = importMode === 'miner'; - $('import-miner-note').hidden = importMode !== 'miner'; - $('import-data-label').textContent = importMode === 'seed' ? 'The words, in order' : 'The private key, 64 hex characters'; -}); -$('import-go').onclick = async () => { - $('import-err').textContent = ''; - const a = $('import-pw').value, b = $('import-pw2').value; - if (a.length < 8) return $('import-err').textContent = 'at least 8 characters'; - if (a !== b) return $('import-err').textContent = 'the two passwords differ'; - try { await post('/api/import', { mode: importMode, data: $('import-data').value, password: a }); $('import-data').value = ''; await poll(); setPhase('home'); setView('overview'); toast('imported'); } - catch (e) { $('import-err').textContent = e.message; } -}; + // ---------- the lock screen (ui/lock-screen.js, 0.1.4, unchanged) ---------- + var firstPhase = null, locking = false, autoTimer = 0, pwRevealed = false; + var autoMem = { used: false }; + var reducedMotion = function () { return !!(window.matchMedia && window.matchMedia('(prefers-reduced-motion: reduce)').matches); }; + function lockLayout() { + var s = forcedBio && state ? Object.assign({}, state, { biometric: Object.assign({}, state.biometric, { enrolled: true, available: true, kind: 'touchid' }) }) : state; + return LockScreen.layout(s, hostHere()); + } + function renderLock() { + var lay = lockLayout(); + $('unlock-address').textContent = lay.address; + $('unlock-touch-text').textContent = 'Unlock with ' + what(); + $('unlock-bio').hidden = !lay.touch; + document.querySelector('.lock-controls').classList.toggle('touch', lay.touch); + var showForm = !lay.touch || pwRevealed; + $('unlock-form').hidden = !showForm; + $('unlock-use-pw').hidden = !lay.touch || pwRevealed; + } + function showPasswordField() { pwRevealed = true; renderLock(); $('unlock-pw').focus({ preventScroll: true }); } + function hidePasswordField() { if (!lockLayout().touch) return; pwRevealed = false; $('unlock-pw').value = ''; $('unlock-err').textContent = ''; renderLock(); $('unlock-touch').focus({ preventScroll: true }); } + function focusLock() { var lay = lockLayout(); if (lay.touch && !pwRevealed) $('unlock-touch').focus({ preventScroll: true }); else $('unlock-pw').focus({ preventScroll: true }); } + function lockTransition() { + locking = true; document.body.classList.add('locking'); $('main').scrollTop = 0; + pwRevealed = false; $('unlock-pw').value = ''; $('unlock-err').textContent = ''; setLine($('unlock-bio-note'), ''); renderLock(); + setTimeout(function () { locking = false; document.body.classList.remove('locking'); setPhase('unlock'); onLockScreen('lock'); }, reducedMotion() ? 0 : LockScreen.ENTER_MS); + } + function onLockScreen(reason) { + clearTimeout(autoTimer); + if (reason !== 'lock') { pwRevealed = false; $('unlock-pw').value = ''; $('unlock-err').textContent = ''; setLine($('unlock-bio-note'), ''); } + renderLock(); focusLock(); + var lay = lockLayout(); + var d = LockScreen.autoPrompt({ reason: reason, phase: 'unlock', touch: lay.touch, askOnOpen: !!(state.settings && state.settings.ask_on_open), hidden: document.hidden, busy: bio.busy, firstPhase: firstPhase, updatedFrom: state.update && state.update.updated_from }, autoMem); + if (reason === 'arrival') autoMem.used = true; + if (d.prompt) autoTimer = setTimeout(function () { if (state && state.phase === 'unlock' && !document.hidden && !bio.busy && !pwRevealed) unlockWithTouch(); }, (reducedMotion() ? 0 : LockScreen.ENTER_MS) + d.delay); + } + async function unlockWithTouch() { + if (bio.busy) return; + clearTimeout(autoTimer); + $('unlock-touch').disabled = true; setLine($('unlock-bio-note'), '' + FP + esc('Waiting for ' + what()) + ''); + var r = await bio.call('unlock'); + $('unlock-touch').disabled = false; + var l = LockScreen.line(r, what()); + setLine($('unlock-bio-note'), l.text ? '' + FP + esc(l.text) + '' : ''); + if (r.ok) { await poll(); return; } + if (l.password) showPasswordField(); else $('unlock-touch').focus({ preventScroll: true }); + } + $('unlock-use-pw').onclick = showPasswordField; + $('unlock-pw').addEventListener('keydown', function (e) { if (e.key === 'Escape') { e.preventDefault(); hidePasswordField(); } }); + $('unlock-touch').onclick = function () { unlockWithTouch(); }; + $('unlock-form').onsubmit = async function (ev) { + ev.preventDefault(); $('unlock-err').textContent = ''; + try { await post('/api/unlock', { password: $('unlock-pw').value }); $('unlock-pw').value = ''; await poll(); } + catch (e) { $('unlock-err').textContent = e.message; } + }; + // the idle lock: the window tells the engine a person is here, every 20 s while there is input + var active = false; + ['mousemove', 'keydown', 'mousedown', 'wheel', 'touchstart'].forEach(function (e) { document.addEventListener(e, function () { active = true; }, { passive: true }); }); + setInterval(function () { if (active && state && state.phase === 'home') { active = false; post('/api/activity').catch(function () {}); } }, 20000); -// ---- unlock / lock ---- -$('unlock-form').onsubmit = async ev => { - ev.preventDefault(); $('unlock-err').textContent = ''; - try { await post('/api/unlock', { password: $('unlock-pw').value }); $('unlock-pw').value = ''; await poll(); } - catch (e) { $('unlock-err').textContent = e.message; } -}; -$('btn-lock').onclick = async () => { await post('/api/lock'); await poll(); }; -$('btn-settings').onclick = () => setView('settings'); -$('settings-back').onclick = () => setView('overview'); -$('copy-address').onclick = () => copy(state.display, 'address'); -$('backup-link').onclick = ev => { ev.preventDefault(); setView('settings'); $('backup-pw').focus(); }; + // ---------- appearance: system, light or dark (the page's own storage) ---------- + function applyTheme(t, keep) { + t = t === 'light' || t === 'dark' ? t : 'system'; + if (t === 'system') document.documentElement.removeAttribute('data-theme'); else document.documentElement.setAttribute('data-theme', t); + document.querySelectorAll('#theme-seg .seg-b').forEach(function (b) { var on = b.dataset.theme === t; b.classList.toggle('on', on); b.setAttribute('aria-checked', on ? 'true' : 'false'); }); + if (keep) store('igneum.wallet.theme', t === 'system' ? null : t); + } + // ?theme=light|dark shows a scheme without storing it (screenshots); otherwise the page's own stored choice + applyTheme(params.get('theme') || stored('igneum.wallet.theme') || 'system', false); + $('theme-seg').addEventListener('click', function (e) { var b = e.target.closest('.seg-b'); if (b) applyTheme(b.dataset.theme, true); }); -// ---- send ---- -$('go-send').onclick = () => { setView('send'); $('send-form').hidden = false; $('send-confirm').hidden = true; $('send-done').hidden = true; $('send-err').textContent = ''; $('send-to').focus(); }; -$('send-cancel').onclick = () => setView('overview'); -$('send-edit').onclick = () => { $('send-form').hidden = false; $('send-confirm').hidden = true; }; -$('send-quote').onclick = async () => { - $('send-err').textContent = ''; - try { - quote = await post('/api/send/quote', { to: $('send-to').value, amount: $('send-amount').value }); - $('c-amount').textContent = `${quote.value_ign} IGN`; - $('c-to').textContent = quote.display_to; - $('c-fee').textContent = `${quote.fee_max_ign} IGN`; - $('c-total').textContent = `${quote.total_max_ign} IGN`; - $('c-fee-note').textContent = `Fee = gas × price. Gas ${quote.gas.toLocaleString()}. Price = base fee ${quote.base_fee_gwei} gwei (burned by the network) + tip ${quote.tip_gwei} gwei (to the miner), capped at ${ign(quote.max_fee, 0) === '0' ? (Number(quote.max_fee) / 1e9).toFixed(3) + ' gwei' : ign(quote.max_fee) + ' IGN'} per gas; what is not used comes back.`; - const needs = !!quote.confirm_needed && !!bio.host; - $('send-go-text').textContent = needs ? 'Confirm with ' + what() : 'Send now'; - $('send-go').querySelector('.fp-ico').hidden = !needs; - $('send-form').hidden = true; $('send-confirm').hidden = false; $('confirm-send-err').textContent = ''; - if (quote.confirm_needed && !bio.host) $('confirm-send-err').textContent = what() + ' is on for this wallet, and only the Igneum Wallet app window can show it.'; - } catch (e) { $('send-err').textContent = e.message; } -}; -$('send-go').onclick = async () => { - $('confirm-send-err').textContent = ''; $('send-go').disabled = true; - try { - const { ok, value_ign, fee_max_ign, total_max_ign, base_fee_gwei, tip_gwei, display_to, confirm_nonce, confirm_reason, confirm_needed, ...q } = quote; - if (confirm_needed) { - // the prompt shows the engine's own line (amount and address); the host confirms the nonce to the engine - setLine($('send-bio-line'), `${FP}${esc('Waiting for ' + what())}`); - const c = await bio.call('confirm', { nonce: confirm_nonce }); - setLine($('send-bio-line'), bioLine(c, 'confirmed, sending')); - if (!c.ok) { $('send-go').disabled = false; return; } + // ---------- phases (welcome, create, import, unlock, home) and the pages behind the rail ---------- + function setPhase(p) { + document.body.dataset.phase = p; + var home = p === 'home'; + document.body.classList.toggle('has-rail', home); + $('rail').hidden = !home; + $('top-brand').hidden = home; + $('page-title').hidden = !home; + $('btn-lock-top').hidden = !home; + if (home) showPage(page); + } + function showPage(id) { + var meta = View.page(id); + page = meta.id; + document.body.dataset.page = page; + document.querySelectorAll('.page').forEach(function (el) { el.hidden = el.dataset.page !== page; }); + document.querySelectorAll('#rail-nav .nav').forEach(function (b) { + var on = b.dataset.page === page; + b.classList.toggle('on', on); + if (on) b.setAttribute('aria-current', 'page'); else b.removeAttribute('aria-current'); + }); + setText('page-title-text', meta.title); setText('page-sub', meta.sub); + $('main').scrollTop = 0; + if (page === 'receive') loadReceive(); + if (page === 'send' && !$('send-form').dataset.locked && $('send-done').hidden) $('send-to').focus({ preventScroll: true }); + if (state) renderPage(state); + } + $('rail-nav').addEventListener('click', function (e) { var b = e.target.closest('.nav[data-page]'); if (b) showPage(b.dataset.page); }); + $('rail-nav').addEventListener('keydown', function (e) { + var items = Array.prototype.slice.call(document.querySelectorAll('#rail-nav .nav')), i = items.indexOf(document.activeElement); + if (i < 0) return; + var j = e.key === 'ArrowDown' || e.key === 'ArrowRight' ? i + 1 : e.key === 'ArrowUp' || e.key === 'ArrowLeft' ? i - 1 : e.key === 'Home' ? 0 : e.key === 'End' ? items.length - 1 : -1; + if (j < 0 || j >= items.length) return; + e.preventDefault(); items[j].focus(); showPage(items[j].dataset.page); + }); + function lockNow() { post('/api/lock').then(poll).catch(function (e) { toast(e.message); }); } + $('btn-lock').onclick = lockNow; + $('btn-lock-top').onclick = lockNow; + // quit: a strip in place of a dialog + $('btn-quit').onclick = function () { $('ask-quit').hidden = false; $('ask-quit-yes').focus(); }; + $('ask-quit-no').onclick = function () { $('ask-quit').hidden = true; }; + $('ask-quit-yes').onclick = function () { $('ask-quit').hidden = true; post('/api/quit').catch(function () {}); toast('Quitting'); }; + document.addEventListener('keydown', function (e) { + if (e.key !== 'Escape') return; + if (!$('upd').hidden) { e.preventDefault(); cardLater(); return; } + if (!$('ask-quit').hidden) { $('ask-quit').hidden = true; return; } + if (!$('ask-send').hidden) { cancelAsk(); return; } + if (!$('ask-remove').hidden) { $('ask-remove').hidden = true; } + }); + // disclosures: aria-expanded flips, the target shows + document.addEventListener('click', function (e) { + var b = e.target.closest('.disclose[aria-controls]'); if (!b) return; + var open = b.getAttribute('aria-expanded') !== 'true'; b.setAttribute('aria-expanded', open ? 'true' : 'false'); + var t = $(b.getAttribute('aria-controls')); if (t) t.hidden = !open; + if (!open && b.id === 'backup-toggle') resetBackup(); + if (!open && b.id === 'export-toggle') resetExport(); + }); + + // ---------- state ---------- + async function poll() { + try { state = await api('/api/state'); render(); } catch (e) { setText('pill-text', 'engine gone'); $('pill').className = 'pill warn'; } + } + function render() { + var s = state, phase = s.phase; + var inFlow = ['create', 'import'].indexOf(document.body.dataset.phase) >= 0; + if (firstPhase === null) firstPhase = phase; + if ((!inFlow || phase === 'home') && lastPhase !== phase && !locking) { + if (lastPhase === 'home' && phase === 'unlock') lockTransition(); + else { setPhase(phase); if (phase === 'unlock') onLockScreen(lastPhase === null ? 'arrival' : 'lock'); } + } + lastPhase = phase; + if (phase === 'home' && firstHome) { firstHome = false; if (params.get('page')) showPage(params.get('page')); } + var pw = View.pillWords(s); + setText('pill-text', pw.text); $('pill').className = 'pill ' + pw.cls; + setText('welcome-eyebrow', (s.settings.network === 'devnet' ? 'devnet v4' : s.settings.network) + ' · nothing is bought or sold'); + if (forcedUpdate) s.update = sampleUpdate(forcedUpdate); + renderNotices(s); + renderUpdateCard(s); + if (phase === 'unlock' || locking) renderLock(); + $('seg-miner').disabled = !s.miner_wallet_present; + if (phase === 'home') { + $('rail-status').innerHTML = '' + esc(View.shortHex(s.display, 6, 4)) + '
    ' + esc(s.settings.network === 'devnet' ? 'devnet v4' : s.settings.network); + setText('foot-version', 'v' + s.version); + $('nav-updates-dot').hidden = !(s.update && (s.update.status === 'ready' || s.update.status === 'deferred' || s.update.status === 'manual')); + renderPage(s); } - const r = await post('/api/send', { quote: q, nonce: confirm_nonce }); - sentHash = r.hash; $('sent-hash').textContent = r.hash; - $('send-confirm').hidden = true; $('send-done').hidden = false; $('send-to').value = ''; $('send-amount').value = ''; - await poll(); - } catch (e) { $('confirm-send-err').textContent = e.message; } - $('send-go').disabled = false; -}; -$('sent-home').onclick = () => setView('overview'); -$('sent-view').onclick = () => openTx(sentHash); - -// ---- receive ---- -$('go-receive').onclick = async () => { - try { const r = await post('/api/receive'); $('qr').innerHTML = r.svg; $('receive-address').textContent = r.display; setView('receive'); } - catch (e) { toast(e.message); } -}; -$('receive-copy').onclick = () => copy(state.display, 'address'); -$('receive-back').onclick = () => setView('overview'); - -// ---- transaction detail ---- -async function openTx(hash) { txHash = hash; setView('tx'); await renderTx(); } -async function renderTx() { - const e = (state.history || []).find(x => x.hash.toLowerCase() === txHash.toLowerCase()); - if (!e) return; - const incoming = ['received', 'reward', 'proving'].includes(e.kind); - $('tx-title').textContent = e.kind === 'reward' ? 'Block reward' : e.kind === 'proving' ? 'Shard payout' : e.kind === 'sent' ? 'Sent' : e.kind === 'self' ? 'To yourself' : 'Received'; - const fl = $('tx-finality'); fl.className = 'finality-line ' + e.finality; - fl.textContent = e.finality === 'final' ? `FINAL · under checkpoint ${e.checkpoint}, certificate verified by this wallet` : e.finality === 'in_block' ? `IN A BLOCK · chain block ${e.block.toLocaleString()}; final once a verified checkpoint covers it` : e.finality === 'failed' ? 'FAILED · the execution failed; the fee was still paid' : 'PENDING · waiting for a block'; - const rows = [['amount', `${incoming ? '+' : '−'}${ign(e.value, 18)} IGN`], ['when', when(e.time)]]; - if (e.kind !== 'reward' && e.kind !== 'proving') rows.push(['from', esc(e.from)], ['to', esc(e.to)], ['fee paid', e.fee ? `${ign(e.fee, 9)} IGN` : 'pending'], ['hash', esc(e.hash)]); - rows.push(['block', e.block ? `${e.block.toLocaleString()} · ${short(e.block_hash)}` : 'none yet']); - if (e.note) rows.push(['note', esc(e.note)]); - const f = state.finality; - rows.push(['verified checkpoint', f.verified_index ? `${f.verified_index} at chain height ${f.chain_number == null ? '…' : f.chain_number.toLocaleString()}` : 'none yet']); - kv($('tx-kv'), rows); -} -$('tx-back').onclick = () => setView('overview'); - -// ---- settings ---- -$('backup-show').onclick = async () => { - $('backup-err').textContent = ''; - try { - const r = await post('/api/reveal', { password: $('backup-pw').value }); $('backup-pw').value = ''; - $('backup-words').innerHTML = (r.words || []).map(w => `
  • ${esc(w)}
  • `).join(''); - $('backup-key').textContent = r.private_key; $('backup-out').hidden = false; - if (!state.backed_up) await post('/api/backed-up'); - } catch (e) { $('backup-err').textContent = e.message; } -}; -$('backup-hide').onclick = () => { $('backup-out').hidden = true; $('backup-words').innerHTML = ''; $('backup-key').textContent = ''; }; -// the backup and the export after a confirmation: the words come from the unlocked key, no password typed -async function revealWithTouch(errEl) { - errEl.textContent = ''; - const c = await post('/api/biometric/challenge', { purpose: 'reveal' }); - setLine(errEl, `${FP}${esc('Waiting for ' + what())}`); - const h = await bio.call('confirm', { nonce: c.nonce }); - setLine(errEl, bioLine(h, 'confirmed')); - if (!h.ok) return null; - return post('/api/reveal', { nonce: c.nonce }); -} -$('backup-touch').onclick = async () => { - try { - const r = await revealWithTouch($('backup-err')); - if (!r) return; - $('backup-words').innerHTML = (r.words || []).map(w => `
  • ${esc(w)}
  • `).join(''); - $('backup-key').textContent = r.private_key; $('backup-out').hidden = false; - if (!state.backed_up) await post('/api/backed-up'); - } catch (e) { $('backup-err').textContent = e.message; } -}; -$('export-touch').onclick = async () => { - try { const r = await revealWithTouch($('export-err')); if (!r) return; $('export-key').textContent = r.private_key; $('export-out').hidden = false; } - catch (e) { $('export-err').textContent = e.message; } -}; -// ---- Touch ID / Windows Hello in Settings: enrol (password once, then the prompt), the idle lock, turn off ---- -function versionLine(s) { - const u = s.update || {}, v = 'Igneum Wallet ' + s.version; - let tail; - switch (u.status) { - case 'current': tail = 'up to date'; break; - case 'available': case 'downloading': tail = u.version + ' downloading'; break; - case 'staging': case 'ready': case 'deferred': case 'manual': tail = u.version + ' downloaded'; break; - case 'applying': tail = 'installing ' + u.version; break; - case 'checking': tail = 'checking for updates'; break; - case 'off': tail = 'updates off in this build'; break; - case 'error': tail = 'update check failed'; break; - default: tail = 'not checked yet'; } - return `${esc(v)} · ${esc(tail)}`; -} -function renderBio(s) { - const b = s.biometric || {}, w = what(); - $('version-row').innerHTML = versionLine(s); - $('bio-title').textContent = w; - $('bio-enrol-text').textContent = 'Turn on ' + w; - $('ask-on-open-text').textContent = `Ask for ${w} when the wallet opens`; - const idleMin = s.settings && s.settings.idle_lock_min != null ? s.settings.idle_lock_min : (b.idle_lock ? (b.idle_lock_min || 5) : 0); - if (document.activeElement !== $('idle-lock')) $('idle-lock').value = String(LockScreen.IDLE_CHOICES.includes(Number(idleMin)) ? idleMin : LockScreen.IDLE_DEFAULT_MIN); - $('idle-lock-note').textContent = Number(idleMin) > 0 ? `The key is cleared after ${LockScreen.idleLabel(idleMin)} without you; ${w} or the password opens it again.` : 'The wallet stays open until you lock it.'; - if (document.activeElement !== $('ask-on-open')) $('ask-on-open').checked = !!(s.settings && s.settings.ask_on_open); - $('backup-touch').hidden = !(b.enrolled && bio.host); $('backup-touch').querySelector('span').textContent = 'Show with ' + w; - $('export-touch').hidden = !(b.enrolled && bio.host); $('export-touch').querySelector('span').textContent = 'Show with ' + w; - $('backup-note-text').textContent = b.enrolled ? `Show the 24 words (or the key) again, with ${w} or the password.` : 'Show the 24 words (or the key) again. Needs the password.'; - $('bio-on').hidden = !b.enrolled; $('bio-off').hidden = b.enrolled; - let off = ''; - if (!bio.host) off = w + ' needs the Igneum Wallet app window; this page is open in a browser.'; - else if (!b.available) off = b.message || (w === 'Touch ID' ? 'Touch ID is not set up on this Mac.' : 'Windows Hello is not set up on this PC.'); - $('bio-off-note').textContent = off; - $('bio-enrol').disabled = !!off; $('bio-pw').disabled = !!off; - $('bio-on-note').textContent = b.needs_enrol ? '' : (b.last_result && b.last_result !== 'ok' && b.last_op ? `last ${b.last_op}: ${b.last_result}` : ''); - if (b.needs_enrol && !$('bio-err').textContent) $('bio-err').textContent = `The password changed, so ${w} was turned off. Turn it on again here.`; -} -$('bio-enrol').onclick = async () => { - $('bio-err').textContent = ''; - const pw = $('bio-pw').value; - if (!pw) return $('bio-err').textContent = 'type the password first'; - $('bio-enrol').disabled = true; - try { - const r = await post('/api/biometric/enrol/begin', { password: pw }); - setLine($('bio-err'), `${FP}${esc('Waiting for ' + what())}`); - const h = await bio.call('enrol', { token: r.token }); - setLine($('bio-err'), bioLine(h, 'is on')); - if (!h.ok) { post('/api/biometric/enrol/cancel').catch(() => {}); $('bio-enrol').disabled = false; return; } - $('bio-pw').value = ''; toast(what() + ' is on'); await poll(); - } catch (e) { $('bio-err').textContent = e.message; } - $('bio-enrol').disabled = false; -}; -$('bio-remove').onclick = async () => { try { await post('/api/biometric/remove'); $('bio-err').textContent = ''; toast(what() + ' is off'); await poll(); } catch (e) { $('bio-err').textContent = e.message; } }; -$('idle-lock').onchange = async ev => { try { await post('/api/settings', { idle_lock_min: Number(ev.target.value) }); await poll(); } catch (e) { toast(e.message); } }; -$('ask-on-open').onchange = async ev => { try { await post('/api/settings', { ask_on_open: ev.target.checked }); } catch (e) { toast(e.message); } }; -$('unlock-touch').onclick = () => unlockWithTouch(); -$('pw-change').onclick = async () => { - $('pw-err').textContent = ''; - try { await post('/api/password', { old: $('pw-old').value, new: $('pw-new').value }); $('pw-old').value = ''; $('pw-new').value = ''; toast('password changed'); } - catch (e) { $('pw-err').textContent = e.message; } -}; -async function network() { return api('/api/network'); } -async function renderNetwork() { - try { - const n = await network(); - kv($('net-kv'), [['network name', esc(n.chain_name)], ['chain id', `${n.chain_id} (${n.chain_id_hex})`], ['rpc url', esc(n.rpc_url || 'none yet')], ['symbol', 'IGN'], ['decimals', '18']]); - $('net-add').disabled = !n.add_network_page; - $('net-add').title = n.add_network_page ? '' : 'the site page is not set in this build; copy the settings instead'; - } catch (e) { kv($('net-kv'), [['network', esc(e.message)]]); } -} -$('net-add').onclick = async () => { const n = await network(); if (n.add_network_page) post('/api/open', { url: n.add_network_page }); }; -$('net-copy').onclick = async () => { const n = await network(); copy(`Network name: ${n.chain_name}\nRPC URL: ${n.rpc_url}\nChain ID: ${n.chain_id}\nCurrency symbol: IGN\nDecimals: 18`, 'network settings'); }; -$('export-show').onclick = async () => { - $('export-err').textContent = ''; - try { const r = await post('/api/reveal', { password: $('export-pw').value }); $('export-pw').value = ''; $('export-key').textContent = r.private_key; $('export-out').hidden = false; } - catch (e) { $('export-err').textContent = e.message; } -}; -$('export-copy').onclick = () => copy($('export-key').textContent, 'private key'); -$('export-hide').onclick = () => { $('export-out').hidden = true; $('export-key').textContent = ''; }; -$('start-login').onchange = async ev => { try { await post('/api/settings', { start_at_login: ev.target.checked }); } catch (e) { toast(e.message); } }; + function renderPage(s) { + if (page === 'home') renderHome(s); + else if (page === 'send') renderSend(s); + else if (page === 'receive') renderReceive(s); + else if (page === 'history') renderRows($('history'), s.history, 0, s); + else if (page === 'settings') renderSettings(s); + refreshNodeWords(s); + } -// ---- over-the-air updates (src/updater.rs): one banner, the settings line ---- -function cap(t) { return t ? t.charAt(0).toUpperCase() + t.slice(1) : ''; } -function updateLine(u) { - const v = 'Igneum Wallet ' + u.version; - if (u.urgent && u.urgent_text) return { text: u.urgent_text + (u.status === 'downloading' ? ' Downloading.' : ''), urgent: true, prog: u.status === 'downloading' }; - switch (u.status) { - case 'available': return { text: v + ' is available. Downloading it.' }; - case 'downloading': return { text: 'Downloading ' + v + (u.size ? ' (' + Math.round(u.size / 1e6) + ' MB)' : '') + ': ' + Math.round((u.progress || 0) * 100) + '%', prog: true }; - case 'staging': return { text: v + ' downloaded and verified. Preparing it.' }; - case 'ready': return { text: v + ' is ready. ' + (u.wait ? cap(u.wait) + '.' : 'It installs as soon as nothing is being sent.'), install: true }; - case 'applying': return { text: 'Installing ' + v + ': ' + (u.wait ? u.wait + '.' : 'the app closes and opens again by itself.') }; - case 'deferred': return { text: v + ' is downloaded. Windows asked for permission and nobody answered; it installs the next time someone is at this PC.', install: true }; - case 'manual': return { text: v + ' is downloaded. ' + cap(u.wait || 'open the download and drag the app over the old one.'), open: true }; - case 'error': return { text: 'Update: ' + (u.error || 'failed') + '.', install: !!(u.ready || u.downloaded) }; - default: return null; + // ---------- Home ---------- + function renderHome(s) { + var bl = View.balanceLine(s); + $('h-bal').hidden = !bl.known; + setText('h-balance', bl.known ? s.balance : ''); + setText('h-balance-line', bl.text); + var ml = View.moneyLine(s); + $('h-money').innerHTML = (ml.money ? '' + esc(ml.money) + '' : '') + esc(ml.text); + setText('home-address', s.display || 'not set'); + $('backup-note').hidden = !!s.backed_up; + var nl = View.nodeLine(s); + setText('node-line-text', nl.text); setText('node-sub', nl.sub); + $('node-dot').className = 'dot' + (nl.tone === 'ok' ? ' live' : nl.tone === 'bad' ? ' bad' : ''); + $('node-card').classList.toggle('bad', nl.tone === 'bad'); + renderKv($('node-kv'), View.nodeDetails(s, s.now)); + renderRows($('recent'), s.history, 5, s); + setText('hist-eyebrow', ''); } -} -function updateNote(u) { - const l = updateLine(u), parts = []; - if (u.updated_from) parts.push('Updated from ' + u.updated_from + '.'); - if (u.rolled_back) parts.push('Rolled back: ' + u.rolled_back + '.'); - if (u.status === 'off') parts.push('Updates are off in this build.'); - else if (l && !(u.rolled_back && u.status === 'error')) parts.push(l.text); - else if (u.status === 'checking') parts.push('Checking.'); - else if (u.status === 'current') parts.push('This is the latest version.'); - else if (u.error) parts.push(u.error); - else parts.push('Not checked yet.'); - return parts.join(' '); -} -function renderUpdate(s) { - const u = s.update, b = $('update-banner'), l = updateLine(u); - const key = u.status + ':' + u.version; - const show = !!l && (u.urgent || u.applying || sessionStorage.getItem('update-later') !== key); - if (show) { - $('update-text').textContent = l.text; - b.classList.toggle('urgent', !!l.urgent); - $('update-install').hidden = !l.install || u.applying; - $('update-open').hidden = !l.open; - $('update-later').hidden = !!l.urgent || !!u.applying; - $('update-prog').hidden = !l.prog; - $('update-prog').firstElementChild.style.width = Math.round((u.progress || 0) * 100) + '%'; + function renderKv(el, rows) { + el.innerHTML = rows.map(function (r) { return '
    ' + esc(r[0]) + '' + esc(r[1]) + '' + esc(r[2] || '') + '
    '; }).join(''); } - b.hidden = !show; - $('update-note').textContent = updateNote(u); - $('update-install-s').hidden = !(l && l.install) || u.applying; - if (document.activeElement !== $('auto-update')) $('auto-update').checked = !!s.settings.auto_update; -} -$('update-check').onclick = () => { post('/api/update/check'); toast('checking for updates'); }; -$('update-open').onclick = () => post('/api/update/open'); -$('update-install').onclick = () => { post('/api/update/install'); toast('installing now'); }; -$('update-install-s').onclick = () => { post('/api/update/install'); toast('installing now'); }; -$('update-later').onclick = () => { sessionStorage.setItem('update-later', state.update.status + ':' + state.update.version); $('update-banner').hidden = true; }; + $('go-send').onclick = function () { resetSend(); showPage('send'); }; + $('go-receive').onclick = function () { showPage('receive'); }; + $('recent-all').onclick = function () { showPage('history'); }; + $('backup-link').onclick = function (ev) { ev.preventDefault(); showPage('settings'); var t = $('backup-toggle'); if (t.getAttribute('aria-expanded') !== 'true') t.click(); t.scrollIntoView({ block: 'center' }); }; -// ---- the update card (update-card.js): one update, centred, over every screen; the banner above stays the small strip ---- -// mem: open (the card is up), later (the key Later was pressed on), seen (the version it was shown for); -// asked: Install now was pressed on this card and the engine has not flipped to applying yet -const updMem = { open: false, later: '', seen: '' }; -let updSig = '', updAsked = '', updMore = false; -// ?update=[&auto=0][&card=1]: the card and the banner for each state, without an engine (screenshots) -const forcedUpdate = new URLSearchParams(location.search).get('update'); -if (forcedUpdate && new URLSearchParams(location.search).get('card') === '1') updMem.open = true; -function sampleUpdate(kind) { - const u = { status: 'ready', version: '0.1.3', url: '', notes: 'The update card: one centred card with Install now and Later. Touch ID confirms every send on the Mac; Windows Hello is written but untested. The coin and the chain line sit on the balance card. The version shows in the header and in Settings.', file: '', error: '', checked_at: Date.now() / 1000 - 40, available: true, downloaded: true, ready: true, applying: false, progress: 1, size: 19479807, auto: new URLSearchParams(location.search).get('auto') !== '0', wait: 'installs as soon as nothing is being sent', urgent: false, urgent_text: '', unsupported: false, min_supported: '', updated_from: '', rolled_back: '' }; - if (!u.auto) u.wait = 'waiting for Install now (automatic updates are off)'; - if (kind === 'available') { u.status = 'available'; u.downloaded = false; u.ready = false; u.progress = 0; } - if (kind === 'downloading') { u.status = 'downloading'; u.downloaded = false; u.ready = false; u.progress = Math.min(0.97, 0.43 + ((Date.now() / 1000) % 60) / 110); } - if (kind === 'staging') { u.status = 'staging'; u.ready = false; } - if (kind === 'applying') { u.status = 'applying'; u.applying = true; } - if (kind === 'deferred') { u.status = 'deferred'; u.wait = 'waits for the next time someone is at this PC (Windows asks for permission)'; } - if (kind === 'urgent') { u.status = 'downloading'; u.ready = false; u.downloaded = false; u.progress = 0.27; u.urgent = true; u.urgent_text = 'Igneum Wallet 0.1.2 is no longer supported by the network. Installing 0.1.3 now.'; } - if (kind === 'manual') { u.status = 'manual'; u.ready = false; u.wait = '/Applications is not writable; open the downloaded disk image and drag the app over the old one'; } - if (kind === 'error') { u.status = 'error'; u.error = 'sha256 mismatch: the file is not what the manifest signed'; u.downloaded = false; u.ready = false; } - return u; -} -function cardCtx(s) { - const bioLine = [...document.querySelectorAll('.bio-state')].some(e => e.offsetParent !== null); - return { phase: document.body.dataset.phase, view: document.body.dataset.view, bioBusy: bio.busy, bioLine, bioName: what(), quitting: !!s.quitting }; -} -function renderUpdateCard(s) { - const m = UpdateCard.model(s.update, s), d = UpdateCard.decide(m, cardCtx(s), updMem), wrap = $('upd'); - if (!d.show) { - if (updMem.open || !wrap.hidden) { updMem.open = false; wrap.hidden = true; updSig = ''; } - return; + // ---------- the history rows (Home shows five, History every one) ---------- + var nodeWord = {}, openRows = {}, fetching = false; + function renderRows(el, list, limit, s) { + list = list || []; + if (limit) list = list.slice(0, limit); + if (!list.length) { el.innerHTML = '
    ' + esc(s.scanning ? 'Reading the chain.' : 'Nothing yet. Receive IGN, or point the miner app at this address.') + '
    '; return; } + el.innerHTML = list.map(function (e) { + var nw = nodeWord[e.hash.toLowerCase()], m = View.rowModel(e, nw && nw.word, s.now), open = !!openRows[e.hash.toLowerCase()]; + return '
    ' + + '
    ' + + '' + esc(m.kind) + '' + esc(m.who) + (m.when ? ' · ' + esc(m.when) : '') + '' + + '' + esc(m.amount) + 'IGN' + + '' + esc(m.state.word) + '' + esc(m.state.why) + '' + + '
    ' + + (open ? rowDetails(e, m) : '') + '
    '; + }).join(''); } - if (!updMem.open) { updMem.open = true; updMem.seen = m.version; updMore = false; if (updAsked !== m.key) updAsked = ''; } - const asked = updAsked === m.key && m.stage !== 'installing' && m.stage !== 'failed'; - const sig = [m.key, m.stage, m.line, m.note, m.cause, m.ring, asked ? 'asked' : '', m.lines.join('|')].join('\u0001'); - if (sig !== updSig) { - updSig = sig; - $('upd-name').textContent = m.name; - $('upd-line').textContent = m.line; - $('upd-cause').textContent = m.cause; $('upd-cause').hidden = !m.cause; - $('upd-notes').innerHTML = m.lines.map(l => `
  • ${esc(l)}
  • `).join(''); - $('upd-all').innerHTML = m.all.map(l => `
  • ${esc(l)}
  • `).join(''); - $('upd-more').hidden = !m.more; if (!m.more) updMore = false; - $('upd-more').textContent = updMore ? 'Less' : 'What changed'; $('upd-more').setAttribute('aria-expanded', updMore ? 'true' : 'false'); - $('upd-all').hidden = !updMore; - $('upd-mark').className = 'upd-mark ' + m.ring; - const acts = asked ? [{ act: 'install', label: m.stage === 'ready' || m.stage === 'deferred' ? 'Installing' : 'Installs when ready', primary: true, disabled: true }, { act: 'later', label: 'Later' }] : m.actions; - $('upd-actions').innerHTML = acts.map(a => ``).join(''); - wrap.dataset.dismiss = m.dismissable ? '1' : ''; + function rowDetails(e, m) { + var rows = []; + if (!m.synthetic) rows.push(['hash', e.hash, 'hash']); + if (e.from) rows.push(['from', e.from, 'address']); + if (e.to) rows.push(['to', e.to, 'address']); + if (!m.synthetic) rows.push(['fee paid', e.fee && e.fee !== '0' ? View.ign(e.fee, 9) + ' IGN' : e.block ? 'none' : 'pending', '']); + rows.push(['block', e.block ? View.withCommas(e.block) + (e.block_hash ? ' · ' + View.shortHex(e.block_hash, 6, 4) : '') : 'none yet', '']); + if (e.checkpoint) rows.push(['checkpoint', View.withCommas(e.checkpoint), 'verified by this wallet']); + if (e.note) rows.push(['note', e.note, '']); + rows.push(['the node says', m.nodeWord || (m.synthetic ? 'a block reward has no transaction' : 'not asked yet'), 'igneum_getTransactionStatus, one word']); + return '
    ' + rows.map(function (r) { + var copy = r[2] === 'hash' || r[2] === 'address' ? ' ' : ''; + return '
    ' + esc(r[0]) + '' + esc(r[1]) + copy + '' + (copy ? '' : '' + esc(r[2] || '') + '') + '
    '; + }).join('') + '
    '; } - // the ring and the percent move every poll; the words above do not - if (m.ring === 'progress') { $('upd-arc').style.strokeDashoffset = (276.5 * (1 - m.pct / 100)).toFixed(1); $('upd-pct').textContent = m.pct + '%'; $('upd-pct').hidden = m.stage !== 'downloading'; } - else { $('upd-arc').style.strokeDashoffset = ''; $('upd-pct').hidden = true; } - const meta = []; - if (m.stage === 'downloading' && m.size) meta.push(m.pct + '% of ' + m.size); - else if (m.size && m.stage === 'available') meta.push(m.size + ' download'); - else if (m.size && (m.stage === 'ready' || m.stage === 'manual' || m.stage === 'deferred' || m.stage === 'staging')) meta.push(m.size + ', downloaded'); - if (m.note) meta.push(m.note); - $('upd-meta').textContent = meta.join(' · '); - if (wrap.hidden) { wrap.hidden = false; $('upd-card').focus({ preventScroll: true }); } -} -function cardLater() { - if (!updMem.open || !$('upd').dataset.dismiss) return; - const m = UpdateCard.model(state && state.update, state || {}); - updMem.later = m ? m.key : updMem.later; updMem.open = false; $('upd').hidden = true; updSig = ''; -} -$('upd').addEventListener('click', e => { - if (e.target === $('upd')) { cardLater(); return; } - const b = e.target.closest('[data-act]'); if (!b || b.disabled) return; - const act = b.dataset.act, m = UpdateCard.model(state && state.update, state || {}); - if (act === 'later') cardLater(); - else if (act === 'install' || act === 'retry') { post('/api/update/install').catch(() => {}); updAsked = m ? m.key : ''; updSig = ''; toast(act === 'retry' ? 'trying the update again' : m && m.stage === 'ready' ? 'installing now' : 'installs as soon as it is downloaded'); if (state) renderUpdateCard(state); } - else if (act === 'open') post('/api/update/open').catch(() => {}); -}); -$('upd-more').onclick = function () { updMore = !updMore; $('upd-all').hidden = !updMore; this.textContent = updMore ? 'Less' : 'What changed'; this.setAttribute('aria-expanded', updMore ? 'true' : 'false'); }; -document.addEventListener('keydown', e => { if (e.key === 'Escape' && !$('upd').hidden) { e.preventDefault(); cardLater(); } }); -$('auto-update').onchange = async ev => { try { await post('/api/update/auto', { on: ev.target.checked }); } catch (e) { toast(e.message); } }; -$('remove-go').onclick = async () => { - $('remove-err').textContent = ''; - if (!confirm('Remove this wallet from this machine? Only your 24 words or the key bring it back.')) return; - try { await post('/api/remove', { password: $('remove-pw').value }); $('remove-pw').value = ''; await poll(); } - catch (e) { $('remove-err').textContent = e.message; } -}; -document.addEventListener('visibilitychange', () => { - if (document.hidden) return; + document.addEventListener('click', function (e) { + if (e.target.closest('[data-copy-text]')) return; + var main = e.target.closest('.hrow:not(.sent-row) .hr-main'); if (!main) return; + var h = main.parentNode.dataset.hash.toLowerCase(); + openRows[h] = !openRows[h]; + if (state) renderPage(state); + }); + // the node's one word per transaction, from GET /api/tx/ (node_status.state), for the rows that are not final + async function refreshNodeWords(s) { + if (fetching) return; + var want = View.needsNodeWord(s.history, 12), now = Date.now() / 1000; + var due = want.filter(function (h) { var w = nodeWord[h.toLowerCase()]; return !w || now - w.at > (w.word ? 4 : 30); }); + if (!due.length) return; + fetching = true; + try { + for (var i = 0; i < due.length; i++) { + var h = due[i].toLowerCase(), word = ''; + try { var r = await api('/api/tx/' + due[i]); word = (r.node_status && r.node_status.state) || ''; } catch (e) { word = ''; } + nodeWord[h] = { word: word, at: Date.now() / 1000 }; + } + } finally { fetching = false; } + if (state) renderRowsOnly(state); + } + function renderRowsOnly(s) { + if (page === 'home') renderRows($('recent'), s.history, 5, s); + else if (page === 'history') renderRows($('history'), s.history, 0, s); + else if (page === 'send') renderSentRow(s); + } + + // ---------- Send: the form, the question in place, the pending row ---------- + function renderSend(s) { + var bl = View.balanceLine(s); + setText('send-balance', bl.known ? 'of ' + s.balance + ' IGN' : ''); + setText('send-eyebrow', s.node && s.node.state === 'ok' ? '' : 'needs a node'); + renderSentRow(s); + } + function resetSend() { + quote = null; sentHash = null; + $('send-form').hidden = false; $('send-form').dataset.locked = ''; + $('send-to').value = ''; $('send-amount').value = ''; + $('send-err').textContent = ''; $('confirm-send-err').textContent = ''; setLine($('send-bio-line'), ''); + setText('fee-line', View.feeWords(null).line); $('fee-toggle').hidden = true; $('fee-details').hidden = true; $('fee-toggle').setAttribute('aria-expanded', 'false'); + $('send-cta').hidden = false; $('ask-send').hidden = true; $('send-done').hidden = true; + } + function cancelAsk() { $('ask-send').hidden = true; $('send-cta').hidden = false; $('send-form').dataset.locked = ''; quote = null; $('send-to').focus({ preventScroll: true }); } + $('ask-send-no').onclick = cancelAsk; + $('send-quote').onclick = async function () { + $('send-err').textContent = ''; + try { + quote = await post('/api/send/quote', { to: $('send-to').value.trim(), amount: $('send-amount').value.trim() }); + var fw = View.feeWords(quote); + setText('fee-line', fw.line); setText('fee-details', fw.detail); $('fee-toggle').hidden = false; + var needs = !!quote.confirm_needed && !!bio.host; + var ask = View.sendAsk(quote, needs, what()); + setText('ask-send-text', ask.text); setText('send-go-text', ask.button); + $('send-go').querySelector('.fp-ico').hidden = !needs; + $('confirm-send-err').textContent = quote.confirm_needed && !bio.host ? what() + ' is on for this wallet, and only the Igneum Wallet app window can show it.' : ''; + setLine($('send-bio-line'), ''); + $('send-form').dataset.locked = '1'; $('send-cta').hidden = true; $('ask-send').hidden = false; $('send-go').disabled = false; + $('send-go').focus({ preventScroll: true }); + } catch (e) { $('send-err').textContent = e.message; } + }; + $('fee-toggle').onclick = function () { var open = this.getAttribute('aria-expanded') !== 'true'; this.setAttribute('aria-expanded', open ? 'true' : 'false'); $('fee-details').hidden = !open; this.textContent = open ? 'Less' : 'Details'; }; + ['send-to', 'send-amount'].forEach(function (id) { $(id).addEventListener('keydown', function (e) { if (e.key === 'Enter' && !$('send-form').dataset.locked) { e.preventDefault(); $('send-quote').click(); } }); }); + $('send-go').onclick = async function () { + if (!quote) return; + $('confirm-send-err').textContent = ''; $('send-go').disabled = true; + try { + var q = Object.assign({}, quote); + ['ok', 'value_ign', 'fee_max_ign', 'total_max_ign', 'base_fee_gwei', 'tip_gwei', 'display_to', 'confirm_nonce', 'confirm_reason', 'confirm_needed'].forEach(function (k) { delete q[k]; }); + if (quote.confirm_needed) { + setLine($('send-bio-line'), '' + FP + esc('Waiting for ' + what()) + ''); + var c = await bio.call('confirm', { nonce: quote.confirm_nonce }); + setLine($('send-bio-line'), bioLine(c, 'confirmed, sending')); + if (!c.ok) { $('send-go').disabled = false; return; } + } + var r = await post('/api/send', { quote: q, nonce: quote.confirm_nonce }); + sentHash = r.hash; setText('sent-hash', r.hash); + $('ask-send').hidden = true; $('send-form').hidden = true; $('send-done').hidden = false; + $('send-to').value = ''; $('send-amount').value = ''; $('send-form').dataset.locked = ''; quote = null; + await poll(); + } catch (e) { $('confirm-send-err').textContent = e.message; } + $('send-go').disabled = false; + }; + function renderSentRow(s) { + if (!sentHash || $('send-done').hidden) return; + var e = (s.history || []).filter(function (x) { return x.hash.toLowerCase() === sentHash.toLowerCase(); })[0]; + var el = $('sent-row'); + if (!e) { el.innerHTML = '
    Senthanded to the nodependingwaiting for a block
    '; return; } + var nw = nodeWord[e.hash.toLowerCase()], m = View.rowModel(e, nw && nw.word, s.now); + el.className = 'hrow sent-row' + (m.in ? ' in' : ''); + el.innerHTML = '
    ' + esc(m.kind) + '' + esc(m.who) + (m.when ? ' · ' + esc(m.when) : '') + '' + esc(m.amount) + 'IGN' + esc(m.state.word) + '' + esc(m.state.why) + '
    '; + } + $('sent-view').onclick = function () { if (sentHash) openRows[sentHash.toLowerCase()] = true; showPage('history'); }; + $('sent-again').onclick = function () { resetSend(); $('send-to').focus({ preventScroll: true }); }; + + // ---------- Receive ---------- + var receiveFor = ''; + function renderReceive(s) { setText('receive-address', s.display || 'not set'); } + async function loadReceive() { + if (!state || !state.display || receiveFor === state.display) return; + try { var r = await post('/api/receive'); $('qr').innerHTML = r.svg; receiveFor = state.display; } + catch (e) { toast(e.message); } + } + + // ---------- create ---------- + $('go-create').onclick = function () { setPhase('create'); $('create-1').hidden = false; $('create-2').hidden = true; $('create-3').hidden = true; $('create-pw').focus(); }; + $('create-back').onclick = function () { setPhase('welcome'); }; + var words = []; + $('create-next').onclick = async function () { + $('create-err').textContent = ''; + var a = $('create-pw').value, b = $('create-pw2').value; + if (a.length < 8) { $('create-err').textContent = 'at least 8 characters'; return; } + if (a !== b) { $('create-err').textContent = 'the two passwords differ'; return; } + try { + var r = await post('/api/create', { password: a }); + words = r.words; + $('words').innerHTML = words.map(function (w) { return '
  • ' + esc(w) + '
  • '; }).join(''); + $('create-address').textContent = r.display; + $('create-1').hidden = true; $('create-2').hidden = false; + } catch (e) { $('create-err').textContent = e.message; } + }; + $('create-written').onclick = function () { + var picks = []; while (picks.length < 3) { var p = 1 + Math.floor(Math.random() * 24); if (picks.indexOf(p) < 0) picks.push(p); } + picks.sort(function (a, b) { return a - b; }); + $('checks').innerHTML = picks.map(function (p) { return ''; }).join(''); + $('words').innerHTML = ''; words = []; + $('create-2').hidden = true; $('create-3').hidden = false; + $('checks').querySelector('input').focus(); + }; + $('create-again').onclick = function () { $('create-3').hidden = true; $('create-1').hidden = false; $('confirm-err').textContent = ''; $('create-err').textContent = 'start again: the words are made fresh each time'; }; + $('create-confirm').onclick = async function () { + $('confirm-err').textContent = ''; + var checks = Array.prototype.map.call($('checks').querySelectorAll('input'), function (i) { return { position: Number(i.dataset.pos), word: i.value.trim() }; }); + try { await post('/api/create/confirm', { checks: checks }); $('checks').innerHTML = ''; await poll(); setPhase('home'); showPage('home'); toast('Wallet ready'); } + catch (e) { $('confirm-err').textContent = e.message; } + }; + + // ---------- import ---------- + var importMode = 'seed'; + $('go-import').onclick = function () { setPhase('import'); }; + $('import-back').onclick = function () { setPhase('welcome'); }; + $('import-mode').addEventListener('click', function (e) { + var b = e.target.closest('.seg-b'); if (!b || b.disabled) return; + importMode = b.dataset.mode; + document.querySelectorAll('#import-mode .seg-b').forEach(function (x) { var on = x === b; x.classList.toggle('on', on); x.setAttribute('aria-checked', on ? 'true' : 'false'); }); + $('import-data-field').hidden = importMode === 'miner'; + $('import-miner-note').hidden = importMode !== 'miner'; + setText('import-data-label', importMode === 'seed' ? 'The words, in order' : 'The private key, 64 hex characters'); + }); + $('import-go').onclick = async function () { + $('import-err').textContent = ''; + var a = $('import-pw').value, b = $('import-pw2').value; + if (a.length < 8) { $('import-err').textContent = 'at least 8 characters'; return; } + if (a !== b) { $('import-err').textContent = 'the two passwords differ'; return; } + try { await post('/api/import', { mode: importMode, data: $('import-data').value, password: a }); $('import-data').value = ''; await poll(); setPhase('home'); showPage('home'); toast('Imported'); } + catch (e) { $('import-err').textContent = e.message; } + }; + + // ---------- Settings ---------- + function renderSettings(s) { + var b = s.biometric || {}, w = what(), host = hostHere(); + setText('bio-title', w); + setText('bio-sentence', View.bioSentence(b, w, host)); + setText('bio-toggle-btn', b.enrolled ? 'Turn off' : 'Turn on'); + $('bio-toggle-btn').disabled = !b.enrolled && (!host || !b.available); + $('bio-toggle-btn').classList.toggle('danger', !!b.enrolled); + if (b.enrolled) $('bio-enrol').hidden = true; + $('bio-off-note').hidden = true; + setText('bio-enrol-text', 'Turn on ' + w); + $('ask-on-open-row').hidden = !b.enrolled; $('idle-row').hidden = !b.enrolled; + setText('ask-on-open-text', 'Ask for ' + w + ' when the wallet opens'); + var idleMin = s.settings && s.settings.idle_lock_min != null ? s.settings.idle_lock_min : (b.idle_lock ? (b.idle_lock_min || 5) : 0); + if (document.activeElement !== $('idle-lock')) $('idle-lock').value = String(LockScreen.IDLE_CHOICES.indexOf(Number(idleMin)) >= 0 ? idleMin : LockScreen.IDLE_DEFAULT_MIN); + setText('idle-lock-note', View.idleSentence(idleMin, w)); + if (document.activeElement !== $('ask-on-open')) $('ask-on-open').checked = !!(s.settings && s.settings.ask_on_open); + if (b.needs_enrol && !$('bio-err').textContent) $('bio-err').textContent = 'The password changed, so ' + w + ' was turned off. Turn it on again here.'; + var touch = !!(b.enrolled && bio.host); + $('backup-touch').hidden = !touch; $('backup-touch').querySelector('span').textContent = 'Show with ' + w; + $('export-touch').hidden = !touch; $('export-touch').querySelector('span').textContent = 'Show with ' + w; + setText('backup-eyebrow', s.backed_up ? 'written down' : 'not written down yet'); + if (document.activeElement !== $('start-login')) $('start-login').checked = !!s.settings.start_at_login; + // the update card + var u = s.update || {}; + setText('s-version', 'Igneum Wallet ' + s.version); + setText('s-update-note', View.updateNote(u, s.now)); + var ul = View.updateLine(u); + $('s-install').hidden = !(ul && ul.install) || u.applying; + if (document.activeElement !== $('auto-update')) $('auto-update').checked = !!s.settings.auto_update; + // the node + var n = s.node || {}; + setText('node-endpoint', n.evm || 'none yet'); + setText('node-source-line', View.cap(n.message || '')); + setText('node-eyebrow', View.sourceWords(n.source)); + setText('node-ids', 'chain id ' + (n.chain_id || '…') + ' · gRPC ' + (n.grpc || 'none') + ' · public RPC ' + (s.public_rpc || 'none in this build')); + // advanced + setText('s-mid', 'machine id ' + (s.machine_id || '').slice(0, 8)); + setText('s-dirs', 'logs ' + s.log_dir + ' · wallet ' + s.app_dir); + setText('s-miner-file', s.miner_wallet_present ? "The miner app's key file is on this machine; Import can read it." : "No miner key file on this machine."); + } + $('bio-toggle-btn').onclick = async function () { + var b = (state && state.biometric) || {}; + if (b.enrolled) { try { await post('/api/biometric/remove'); $('bio-err').textContent = ''; toast(what() + ' is off'); await poll(); } catch (e) { $('bio-err').textContent = e.message; } return; } + $('bio-enrol').hidden = false; $('bio-pw').focus({ preventScroll: true }); + }; + $('bio-enrol-cancel').onclick = function () { $('bio-enrol').hidden = true; $('bio-pw').value = ''; $('bio-err').textContent = ''; }; + $('bio-enrol-go').onclick = async function () { + $('bio-err').textContent = ''; + var pw = $('bio-pw').value; + if (!pw) { $('bio-err').textContent = 'type the password first'; return; } + $('bio-enrol-go').disabled = true; + try { + var r = await post('/api/biometric/enrol/begin', { password: pw }); + setLine($('bio-err'), '' + FP + esc('Waiting for ' + what()) + ''); + var h = await bio.call('enrol', { token: r.token }); + setLine($('bio-err'), bioLine(h, 'is on')); + if (!h.ok) { post('/api/biometric/enrol/cancel').catch(function () {}); $('bio-enrol-go').disabled = false; return; } + $('bio-pw').value = ''; $('bio-enrol').hidden = true; toast(what() + ' is on'); await poll(); + } catch (e) { $('bio-err').textContent = e.message; } + $('bio-enrol-go').disabled = false; + }; + $('idle-lock').onchange = async function (ev) { try { await post('/api/settings', { idle_lock_min: Number(ev.target.value) }); await poll(); } catch (e) { toast(e.message); } }; + $('ask-on-open').onchange = async function (ev) { try { await post('/api/settings', { ask_on_open: ev.target.checked }); } catch (e) { toast(e.message); } }; + $('pw-change').onclick = async function () { + $('pw-err').textContent = ''; + try { await post('/api/password', { old: $('pw-old').value, new: $('pw-new').value }); $('pw-old').value = ''; $('pw-new').value = ''; toast('Password changed'); await poll(); } + catch (e) { $('pw-err').textContent = e.message; } + }; + // backup and export: the ask in place, then the password field or the prompt + function resetBackup() { $('backup-pw-row').hidden = true; $('backup-pw').value = ''; $('backup-err').textContent = ''; $('backup-out').hidden = true; $('backup-words').innerHTML = ''; $('backup-key').textContent = ''; $('ask-backup').hidden = false; } + function resetExport() { $('export-pw-row').hidden = true; $('export-pw').value = ''; $('export-err').textContent = ''; $('export-out').hidden = true; $('export-key').textContent = ''; $('ask-export').hidden = false; } + function closeDisclosure(id) { var t = $(id); t.setAttribute('aria-expanded', 'false'); $(t.getAttribute('aria-controls')).hidden = true; } + $('backup-cancel').onclick = function () { resetBackup(); closeDisclosure('backup-toggle'); }; + $('export-cancel').onclick = function () { resetExport(); closeDisclosure('export-toggle'); }; + $('backup-use-pw').onclick = function () { $('backup-pw-row').hidden = false; $('backup-pw').focus({ preventScroll: true }); }; + $('export-use-pw').onclick = function () { $('export-pw-row').hidden = false; $('export-pw').focus({ preventScroll: true }); }; + function showBackup(r) { + $('backup-words').innerHTML = (r.words || []).map(function (w) { return '
  • ' + esc(w) + '
  • '; }).join(''); + $('backup-key').textContent = r.private_key; $('backup-out').hidden = false; $('ask-backup').hidden = true; $('backup-pw-row').hidden = true; + if (state && !state.backed_up) post('/api/backed-up').then(poll).catch(function () {}); + } + $('backup-show').onclick = async function () { + $('backup-err').textContent = ''; + try { var r = await post('/api/reveal', { password: $('backup-pw').value }); $('backup-pw').value = ''; showBackup(r); } + catch (e) { $('backup-err').textContent = e.message; } + }; + $('backup-pw').addEventListener('keydown', function (e) { if (e.key === 'Enter') { e.preventDefault(); $('backup-show').click(); } }); + $('export-pw').addEventListener('keydown', function (e) { if (e.key === 'Enter') { e.preventDefault(); $('export-show').click(); } }); + async function revealWithTouch(errEl) { + errEl.textContent = ''; + var c = await post('/api/biometric/challenge', { purpose: 'reveal' }); + setLine(errEl, '' + FP + esc('Waiting for ' + what()) + ''); + var h = await bio.call('confirm', { nonce: c.nonce }); + setLine(errEl, bioLine(h, 'confirmed')); + if (!h.ok) return null; + var r = await post('/api/reveal', { nonce: c.nonce }); + errEl.textContent = ''; + return r; + } + $('backup-touch').onclick = async function () { try { var r = await revealWithTouch($('backup-err')); if (r) showBackup(r); } catch (e) { $('backup-err').textContent = e.message; } }; + $('export-touch').onclick = async function () { try { var r = await revealWithTouch($('export-err')); if (r) { $('export-key').textContent = r.private_key; $('export-out').hidden = false; $('ask-export').hidden = true; } } catch (e) { $('export-err').textContent = e.message; } }; + $('export-show').onclick = async function () { + $('export-err').textContent = ''; + try { var r = await post('/api/reveal', { password: $('export-pw').value }); $('export-pw').value = ''; $('export-key').textContent = r.private_key; $('export-out').hidden = false; $('ask-export').hidden = true; $('export-pw-row').hidden = true; } + catch (e) { $('export-err').textContent = e.message; } + }; + $('backup-hide').onclick = function () { resetBackup(); }; + $('export-hide').onclick = function () { resetExport(); }; + // MetaMask + var netCache = null; + async function network() { if (!netCache) netCache = await api('/api/network'); return netCache; } + async function renderNetwork() { + try { + var n = await network(); + setText('net-line', n.chain_name + ' · chain id ' + n.chain_id + ' · ' + (n.rpc_url || 'no RPC yet')); + $('net-add').disabled = !n.add_network_page; + $('net-add').title = n.add_network_page ? '' : 'the site page is not set in this build; copy the settings instead'; + } catch (e) { setText('net-line', e.message); } + } + $('net-add').onclick = async function () { var n = await network(); if (n.add_network_page) post('/api/open', { url: n.add_network_page }); }; + $('net-copy').onclick = async function () { var n = await network(); copyText('Network name: ' + n.chain_name + '\nRPC URL: ' + n.rpc_url + '\nChain ID: ' + n.chain_id + '\nCurrency symbol: IGN\nDecimals: 18', 'Network settings'); }; + $('start-login').onchange = async function (ev) { try { await post('/api/settings', { start_at_login: ev.target.checked }); } catch (e) { toast(e.message); } }; + $('auto-update').onchange = async function (ev) { try { await post('/api/update/auto', { on: ev.target.checked }); } catch (e) { toast(e.message); } }; + $('s-update').onclick = function () { post('/api/update/check').catch(function () {}); toast('Checking for updates'); }; + $('s-install').onclick = function () { post('/api/update/install').catch(function () {}); toast('Installing now'); }; + // remove: the ask in place, never a dialog + $('remove-start').onclick = function () { $('ask-remove').hidden = false; $('remove-pw').focus({ preventScroll: true }); }; + $('ask-remove-no').onclick = function () { $('ask-remove').hidden = true; $('remove-pw').value = ''; $('remove-err').textContent = ''; }; + $('remove-go').onclick = async function () { + $('remove-err').textContent = ''; + try { await post('/api/remove', { password: $('remove-pw').value }); $('remove-pw').value = ''; $('ask-remove').hidden = true; await poll(); } + catch (e) { $('remove-err').textContent = e.message; } + }; + $('remove-pw').addEventListener('keydown', function (e) { if (e.key === 'Enter') { e.preventDefault(); $('remove-go').click(); } }); + new MutationObserver(function () { if (document.body.dataset.page === 'settings' && document.body.dataset.phase === 'home') renderNetwork(); }).observe(document.body, { attributes: true, attributeFilter: ['data-page', 'data-phase'] }); + + // ---------- the update strip (one notice) and the update card ---------- + var stripH = 0, noticeSig = ''; + function renderNotices(s) { + var u = s.update, l = View.updateLine(u), strip = $('notices'); + var key = u.status + ':' + u.version; + var show = !!l && (u.urgent || u.applying || sessionStorage.getItem('update-later') !== key); + if (!show) { if (!strip.hidden) { strip.hidden = true; noticeSig = ''; layoutStrip(); } return; } + var acts = []; + if (l.install && !u.applying) acts.push({ act: 'install', label: 'Install now', primary: true }); + if (l.open) acts.push({ act: 'open', label: 'Open the download', primary: true }); + var sig = [key, l.text, l.urgent ? 'u' : '', l.prog ? 'p' : '', JSON.stringify(acts)].join('|'); + if (sig !== noticeSig) { + noticeSig = sig; + $('notice').className = 'notice' + (l.urgent ? ' urgent' : ''); + $('notice').dataset.key = key; + setText('notice-text', l.text); + $('notice-actions').innerHTML = acts.map(function (a) { return ''; }).join(''); + $('notice-close').hidden = !!l.urgent || !!u.applying; + $('notice-prog').hidden = !l.prog; + strip.hidden = false; + } + if (l.prog) $('notice-prog').firstElementChild.style.width = Math.round((u.progress || 0) * 100) + '%'; + layoutStrip(); + } + function layoutStrip() { + var strip = $('notices'), h = strip.hidden ? 0 : strip.offsetHeight; + if (h === stripH) return; + stripH = h; + var top = parseInt(getComputedStyle(document.documentElement).getPropertyValue('--top'), 10) || 60; + $('main').style.top = h ? (top + h) + 'px' : ''; + } + window.addEventListener('resize', layoutStrip); + $('notice').addEventListener('click', function (e) { + var b = e.target.closest('[data-act]'); if (!b) return; + var act = b.dataset.act; + if (act === 'install') { post('/api/update/install').catch(function () {}); toast('Installing now'); } + else if (act === 'open') post('/api/update/open').catch(function () {}); + }); + $('notice-close').onclick = function () { sessionStorage.setItem('update-later', $('notice').dataset.key || ''); $('notices').hidden = true; noticeSig = ''; layoutStrip(); }; + + // the update card (update-card.js): one update, centred, over every screen; mem: open, later, seen; asked: Install + // now was pressed on this card and the engine has not flipped to applying yet + var updMem = { open: false, later: '', seen: '' }; + var updSig = '', updAsked = '', updMore = false; + var forcedUpdate = params.get('update'); + if (forcedUpdate && params.get('card') === '1') updMem.open = true; + function sampleUpdate(kind) { + var u = { status: 'ready', version: '0.1.6', url: '', notes: "The wallet on the miner's design: money first, one row per transaction with the node's state word, an Appearance setting, a bottom tab bar on a narrow window. Touch ID unchanged.", file: '', error: '', checked_at: Date.now() / 1000 - 40, available: true, downloaded: true, ready: true, applying: false, progress: 1, size: 20080717, auto: params.get('auto') !== '0', wait: 'installs as soon as nothing is being sent', urgent: false, urgent_text: '', unsupported: false, min_supported: '', updated_from: '', rolled_back: '' }; + if (!u.auto) u.wait = 'waiting for Install now (automatic updates are off)'; + if (kind === 'available') { u.status = 'available'; u.downloaded = false; u.ready = false; u.progress = 0; } + if (kind === 'downloading') { u.status = 'downloading'; u.downloaded = false; u.ready = false; u.progress = Math.min(0.97, 0.43 + ((Date.now() / 1000) % 60) / 110); } + if (kind === 'staging') { u.status = 'staging'; u.ready = false; } + if (kind === 'applying') { u.status = 'applying'; u.applying = true; } + if (kind === 'deferred') { u.status = 'deferred'; u.wait = 'waits for the next time someone is at this PC (Windows asks for permission)'; } + if (kind === 'urgent') { u.status = 'downloading'; u.ready = false; u.downloaded = false; u.progress = 0.27; u.urgent = true; u.urgent_text = 'Igneum Wallet 0.1.4 is no longer supported by the network. Installing 0.1.6 now.'; } + if (kind === 'manual') { u.status = 'manual'; u.ready = false; u.wait = '/Applications is not writable; open the downloaded disk image and drag the app over the old one'; } + if (kind === 'error') { u.status = 'error'; u.error = 'sha256 mismatch: the file is not what the manifest signed'; u.downloaded = false; u.ready = false; } + return u; + } + function cardCtx(s) { + var line = Array.prototype.some.call(document.querySelectorAll('.bio-state'), function (e) { return e.offsetParent !== null; }); + return { phase: document.body.dataset.phase, view: document.body.dataset.phase === 'home' ? page : '', bioBusy: bio.busy, bioLine: line, bioName: what(), quitting: !!s.quitting }; + } + function renderUpdateCard(s) { + var m = UpdateCard.model(s.update, s), d = UpdateCard.decide(m, cardCtx(s), updMem), wrap = $('upd'); + if (!d.show) { if (updMem.open || !wrap.hidden) { updMem.open = false; wrap.hidden = true; updSig = ''; } return; } + if (!updMem.open) { updMem.open = true; updMem.seen = m.version; updMore = false; if (updAsked !== m.key) updAsked = ''; } + var asked = updAsked === m.key && m.stage !== 'installing' && m.stage !== 'failed'; + var sig = [m.key, m.stage, m.line, m.note, m.cause, m.ring, asked ? 'asked' : '', m.lines.join('|')].join('\u0001'); + if (sig !== updSig) { + updSig = sig; + $('upd-name').textContent = m.name; + $('upd-line').textContent = m.line; + $('upd-cause').textContent = m.cause; $('upd-cause').hidden = !m.cause; + $('upd-notes').innerHTML = m.lines.map(function (l) { return '
  • ' + esc(l) + '
  • '; }).join(''); + $('upd-all').innerHTML = m.all.map(function (l) { return '
  • ' + esc(l) + '
  • '; }).join(''); + $('upd-more').hidden = !m.more; if (!m.more) updMore = false; + $('upd-more').textContent = updMore ? 'Less' : 'What changed'; $('upd-more').setAttribute('aria-expanded', updMore ? 'true' : 'false'); + $('upd-all').hidden = !updMore; + $('upd-mark').className = 'upd-mark ' + m.ring; + var acts = asked ? [{ act: 'install', label: m.stage === 'ready' || m.stage === 'deferred' ? 'Installing' : 'Installs when ready', primary: true, disabled: true }, { act: 'later', label: 'Later' }] : m.actions; + $('upd-actions').innerHTML = acts.map(function (a) { return ''; }).join(''); + wrap.dataset.dismiss = m.dismissable ? '1' : ''; + } + if (m.ring === 'progress') { $('upd-arc').style.strokeDashoffset = (276.5 * (1 - m.pct / 100)).toFixed(1); $('upd-pct').textContent = m.pct + '%'; $('upd-pct').hidden = m.stage !== 'downloading'; } + else { $('upd-arc').style.strokeDashoffset = ''; $('upd-pct').hidden = true; } + var meta = []; + if (m.stage === 'downloading' && m.size) meta.push(m.pct + '% of ' + m.size); + else if (m.size && m.stage === 'available') meta.push(m.size + ' download'); + else if (m.size && (m.stage === 'ready' || m.stage === 'manual' || m.stage === 'deferred' || m.stage === 'staging')) meta.push(m.size + ', downloaded'); + if (m.note) meta.push(m.note); + $('upd-meta').textContent = meta.join(' · '); + if (wrap.hidden) { wrap.hidden = false; $('upd-card').focus({ preventScroll: true }); } + } + function cardLater() { + if (!updMem.open || !$('upd').dataset.dismiss) return; + var m = UpdateCard.model(state && state.update, state || {}); + updMem.later = m ? m.key : updMem.later; updMem.open = false; $('upd').hidden = true; updSig = ''; + } + $('upd').addEventListener('click', function (e) { + if (e.target === $('upd')) { cardLater(); return; } + var b = e.target.closest('[data-act]'); if (!b || b.disabled) return; + var act = b.dataset.act, m = UpdateCard.model(state && state.update, state || {}); + if (act === 'later') cardLater(); + else if (act === 'install' || act === 'retry') { post('/api/update/install').catch(function () {}); updAsked = m ? m.key : ''; updSig = ''; toast(act === 'retry' ? 'Trying the update again' : m && m.stage === 'ready' ? 'Installing now' : 'Installs as soon as it is downloaded'); if (state) renderUpdateCard(state); } + else if (act === 'open') post('/api/update/open').catch(function () {}); + }); + $('upd-more').onclick = function () { updMore = !updMore; $('upd-all').hidden = !updMore; this.textContent = updMore ? 'Less' : 'What changed'; this.setAttribute('aria-expanded', updMore ? 'true' : 'false'); }; + + document.addEventListener('visibilitychange', function () { + if (document.hidden) return; + poll(); + if (state && state.phase === 'unlock' && !locking) focusLock(); + }); + + resetSend(); poll(); - // the window came back (menu bar, Dock): the button gets the focus so Return unlocks; never a sheet by itself - if (state && state.phase === 'unlock' && !locking) focusLock(); -}); -new MutationObserver(() => { if (document.body.dataset.view === 'settings') renderNetwork(); }).observe(document.body, { attributes: true, attributeFilter: ['data-view'] }); - -poll(); -setInterval(poll, 2000); + setInterval(poll, 2000); +})(); diff --git a/app/igneum-wallet/ui/index.html b/app/igneum-wallet/ui/index.html index c931301cc..281779d17 100644 --- a/app/igneum-wallet/ui/index.html +++ b/app/igneum-wallet/ui/index.html @@ -4,33 +4,67 @@ Igneum Wallet - + - + + + +
    -
    +
    - IGNEUMWALLET + IGNEUMWALLET +
    +
    starting
    - - +
    -
    -
    step 1 of 3
    +
    step 1 of 3

    Choose a password

    It locks the key on this machine. Nobody can reset it for you. At least 8 characters.

    - - + +