create-nightkit 0.0.0-stage → 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +55 -0
- package/README.md +19 -2
- package/index.mjs +95 -0
- package/package.json +40 -3
- package/template/.env.example +7 -0
- package/template/README.md +42 -0
- package/template/index.html +12 -0
- package/template/package.json +31 -0
- package/template/public/logo-1am-nightkit.svg +8 -0
- package/template/src/App.tsx +69 -0
- package/template/src/evm.tsx +48 -0
- package/template/src/main.tsx +32 -0
- package/template/src/panels/Balances.tsx +78 -0
- package/template/src/panels/Send.tsx +48 -0
- package/template/src/panels/SignMessage.tsx +41 -0
- package/template/src/styles.css +35 -0
- package/template/tsconfig.json +1 -0
- package/template/vite.config.ts +4 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
Apache License
|
|
2
|
+
|
|
3
|
+
Version 2.0, January 2004
|
|
4
|
+
|
|
5
|
+
http://www.apache.org/licenses/
|
|
6
|
+
|
|
7
|
+
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
|
8
|
+
|
|
9
|
+
1. Definitions.
|
|
10
|
+
|
|
11
|
+
"License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document.
|
|
12
|
+
|
|
13
|
+
"Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License.
|
|
14
|
+
|
|
15
|
+
"Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity.
|
|
16
|
+
|
|
17
|
+
"You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License.
|
|
18
|
+
|
|
19
|
+
"Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files.
|
|
20
|
+
|
|
21
|
+
"Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types.
|
|
22
|
+
|
|
23
|
+
"Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below).
|
|
24
|
+
|
|
25
|
+
"Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof.
|
|
26
|
+
|
|
27
|
+
"Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution."
|
|
28
|
+
|
|
29
|
+
"Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work.
|
|
30
|
+
|
|
31
|
+
2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form.
|
|
32
|
+
|
|
33
|
+
3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed.
|
|
34
|
+
|
|
35
|
+
4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions:
|
|
36
|
+
|
|
37
|
+
You must give any other recipients of the Work or Derivative Works a copy of this License; and
|
|
38
|
+
|
|
39
|
+
You must cause any modified files to carry prominent notices stating that You changed the files; and
|
|
40
|
+
|
|
41
|
+
You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and
|
|
42
|
+
|
|
43
|
+
If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License.
|
|
44
|
+
|
|
45
|
+
5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions.
|
|
46
|
+
|
|
47
|
+
6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file.
|
|
48
|
+
|
|
49
|
+
7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License.
|
|
50
|
+
|
|
51
|
+
8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages.
|
|
52
|
+
|
|
53
|
+
9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability.
|
|
54
|
+
|
|
55
|
+
END OF TERMS AND CONDITIONS
|
package/README.md
CHANGED
|
@@ -1,3 +1,20 @@
|
|
|
1
|
-
#
|
|
1
|
+
# create-nightkit
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Start a Midnight app with [NightKit](https://nightkit.1am.xyz) in one command. By [1AM](https://1am.xyz), built by [Webisoft](https://webisoft.com).
|
|
4
|
+
|
|
5
|
+
> **Beta.** Midnight testnets only (ledger 9 and later). It has had no outside security audit; do not put real funds behind it.
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm create nightkit@latest my-app
|
|
9
|
+
cd my-app
|
|
10
|
+
npm install
|
|
11
|
+
npm run vault # the vault, on http://localhost:5174 (leave it running)
|
|
12
|
+
npm run dev # your app, on http://localhost:5173
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
You get a Vite + React app with NightKit's connect button (1AM, MetaMask, Phantom, Coinbase Wallet and more) and RainbowKit for the Ethereum side.
|
|
16
|
+
|
|
17
|
+
## Who makes this
|
|
18
|
+
|
|
19
|
+
NightKit is made by [1AM](https://1am.xyz), the wallet for Midnight, and built by [Webisoft](https://webisoft.com).
|
|
20
|
+
Site and docs: [nightkit.1am.xyz](https://nightkit.1am.xyz). Licence: Apache-2.0.
|
package/index.mjs
ADDED
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// npm create nightkit@latest my-app
|
|
3
|
+
//
|
|
4
|
+
// Copies the NightKit starter (Vite + React, NightKit's connect button, and
|
|
5
|
+
// RainbowKit for the Ethereum side) into a new folder as a standalone project.
|
|
6
|
+
|
|
7
|
+
import { cpSync, existsSync, mkdirSync, readdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";
|
|
8
|
+
import path from "node:path";
|
|
9
|
+
import { fileURLToPath } from "node:url";
|
|
10
|
+
|
|
11
|
+
const here = path.dirname(fileURLToPath(import.meta.url));
|
|
12
|
+
const bundled = path.join(here, "template");
|
|
13
|
+
const inRepo = path.join(here, "..", "..", "templates", "starter");
|
|
14
|
+
const skip = (src) => !/(^|[\\/])(node_modules|dist|\.env)([\\/]|$)/.test(path.relative(inRepo, src)) || src === inRepo;
|
|
15
|
+
|
|
16
|
+
// At publish time: put the starter inside this package.
|
|
17
|
+
if (process.argv.includes("--bundle-template")) {
|
|
18
|
+
rmSync(bundled, { recursive: true, force: true });
|
|
19
|
+
cpSync(inRepo, bundled, { recursive: true, filter: skip });
|
|
20
|
+
console.log("template bundled");
|
|
21
|
+
process.exit(0);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const VERSION = JSON.parse(readFileSync(path.join(here, "package.json"), "utf8")).version;
|
|
25
|
+
const args = process.argv.slice(2).filter((a) => !a.startsWith("-"));
|
|
26
|
+
const name = args[0] ?? "my-midnight-app";
|
|
27
|
+
if (!/^[a-z0-9][a-z0-9._-]*$/.test(path.basename(name))) {
|
|
28
|
+
console.error(`"${name}" is not a usable project name: lower-case letters, digits, dashes.`);
|
|
29
|
+
process.exit(1);
|
|
30
|
+
}
|
|
31
|
+
const target = path.resolve(name);
|
|
32
|
+
if (existsSync(target) && readdirSync(target).length > 0) {
|
|
33
|
+
console.error(`${target} already exists and is not empty.`);
|
|
34
|
+
process.exit(1);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const source = existsSync(bundled) ? bundled : inRepo;
|
|
38
|
+
mkdirSync(target, { recursive: true });
|
|
39
|
+
cpSync(source, target, { recursive: true, filter: (src) => !/(^|[\\/])(node_modules|dist)([\\/]|$)/.test(path.relative(source, src)) });
|
|
40
|
+
|
|
41
|
+
// A standalone project: real versions instead of workspace links, its own
|
|
42
|
+
// TypeScript settings, and the vault as a dev dependency it can serve.
|
|
43
|
+
const pkgPath = path.join(target, "package.json");
|
|
44
|
+
const pkg = JSON.parse(readFileSync(pkgPath, "utf8"));
|
|
45
|
+
pkg.name = path.basename(target);
|
|
46
|
+
pkg.version = "0.1.0";
|
|
47
|
+
for (const group of ["dependencies", "devDependencies"]) {
|
|
48
|
+
for (const [dep, range] of Object.entries(pkg[group] ?? {})) if (String(range).startsWith("workspace:")) pkg[group][dep] = `^${VERSION}`;
|
|
49
|
+
}
|
|
50
|
+
pkg.devDependencies["nightkit-vault"] = `^${VERSION}`;
|
|
51
|
+
pkg.scripts.vault = "nightkit-vault --port 5174";
|
|
52
|
+
writeFileSync(pkgPath, `${JSON.stringify(pkg, null, 2)}\n`);
|
|
53
|
+
|
|
54
|
+
writeFileSync(
|
|
55
|
+
path.join(target, "tsconfig.json"),
|
|
56
|
+
`${JSON.stringify(
|
|
57
|
+
{
|
|
58
|
+
compilerOptions: {
|
|
59
|
+
target: "ES2022",
|
|
60
|
+
lib: ["dom", "dom.iterable", "esnext"],
|
|
61
|
+
module: "esnext",
|
|
62
|
+
moduleResolution: "bundler",
|
|
63
|
+
allowImportingTsExtensions: true,
|
|
64
|
+
verbatimModuleSyntax: true,
|
|
65
|
+
strict: true,
|
|
66
|
+
skipLibCheck: true,
|
|
67
|
+
isolatedModules: true,
|
|
68
|
+
noEmit: true,
|
|
69
|
+
jsx: "react-jsx",
|
|
70
|
+
types: ["vite/client"],
|
|
71
|
+
},
|
|
72
|
+
include: ["src", "vite.config.ts"],
|
|
73
|
+
},
|
|
74
|
+
null,
|
|
75
|
+
2,
|
|
76
|
+
)}\n`,
|
|
77
|
+
);
|
|
78
|
+
if (existsSync(path.join(target, ".env.example"))) cpSync(path.join(target, ".env.example"), path.join(target, ".env"));
|
|
79
|
+
writeFileSync(path.join(target, ".gitignore"), "node_modules/\ndist/\n.env\n.env.local\n");
|
|
80
|
+
|
|
81
|
+
const rel = path.relative(process.cwd(), target) || ".";
|
|
82
|
+
console.log(`
|
|
83
|
+
NightKit app created in ${rel}
|
|
84
|
+
|
|
85
|
+
Next:
|
|
86
|
+
cd ${rel}
|
|
87
|
+
npm install
|
|
88
|
+
npm run vault # the vault, on http://localhost:5174 (leave it running)
|
|
89
|
+
npm run dev # your app, on http://localhost:5173
|
|
90
|
+
|
|
91
|
+
Then press "Connect wallet". The vault must stay on a different
|
|
92
|
+
origin than your app: that separation is what keeps keys out of it.
|
|
93
|
+
|
|
94
|
+
Working with a coding agent? Point it at https://nightkit.1am.xyz/agent.md
|
|
95
|
+
`);
|
package/package.json
CHANGED
|
@@ -1,6 +1,43 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "create-nightkit",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Start a Midnight app with NightKit: `npm create nightkit@latest my-app`.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"license": "Apache-2.0",
|
|
7
|
+
"bin": {
|
|
8
|
+
"create-nightkit": "index.mjs"
|
|
9
|
+
},
|
|
10
|
+
"files": [
|
|
11
|
+
"index.mjs",
|
|
12
|
+
"template",
|
|
13
|
+
"LICENSE"
|
|
14
|
+
],
|
|
15
|
+
"engines": {
|
|
16
|
+
"node": ">=20"
|
|
17
|
+
},
|
|
18
|
+
"publishConfig": {
|
|
19
|
+
"access": "public"
|
|
20
|
+
},
|
|
21
|
+
"homepage": "https://nightkit.1am.xyz",
|
|
22
|
+
"author": {
|
|
23
|
+
"name": "1AM",
|
|
24
|
+
"url": "https://1am.xyz"
|
|
25
|
+
},
|
|
26
|
+
"contributors": [
|
|
27
|
+
{
|
|
28
|
+
"name": "Webisoft",
|
|
29
|
+
"url": "https://webisoft.com"
|
|
30
|
+
}
|
|
31
|
+
],
|
|
32
|
+
"keywords": [
|
|
33
|
+
"midnight",
|
|
34
|
+
"midnight-network",
|
|
35
|
+
"wallet",
|
|
36
|
+
"1am",
|
|
37
|
+
"nightkit",
|
|
38
|
+
"dapp-connector",
|
|
39
|
+
"web3",
|
|
40
|
+
"zero-knowledge"
|
|
41
|
+
],
|
|
42
|
+
"scripts": {}
|
|
6
43
|
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
# The Midnight network the app runs on. The vault must serve it too.
|
|
2
|
+
VITE_NETWORK_ID=stagenet
|
|
3
|
+
# Where the NightKit vault is served. It MUST be a different origin than the app:
|
|
4
|
+
# in development the app is on :5173 and the vault on :5174.
|
|
5
|
+
VITE_VAULT_URL=http://localhost:5174
|
|
6
|
+
# Optional: a Reown (WalletConnect) project id, for mobile wallets on the Ethereum side.
|
|
7
|
+
VITE_WALLETCONNECT_PROJECT_ID=
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
# NightKit starter
|
|
2
|
+
|
|
3
|
+
A Midnight dApp that connects **1AM**, or opens a Midnight account from
|
|
4
|
+
**MetaMask, Coinbase Wallet, Phantom, Rabby** and other wallets, through
|
|
5
|
+
NightKit. Vite + React, with RainbowKit for the Ethereum side.
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
cp .env.example .env # network and vault URL
|
|
9
|
+
pnpm dev # http://localhost:5173
|
|
10
|
+
```
|
|
11
|
+
|
|
12
|
+
The vault must be running on another origin (`pnpm vault` from the repo root
|
|
13
|
+
serves it on http://localhost:5174).
|
|
14
|
+
|
|
15
|
+
## Where things are
|
|
16
|
+
|
|
17
|
+
- `src/main.tsx` — `NightKitProvider` with the network and vault URL. If an
|
|
18
|
+
Ethereum wallet is connected through RainbowKit, it is passed along so the
|
|
19
|
+
vault offers the same wallet first.
|
|
20
|
+
- `src/App.tsx` — `ConnectButton`, and `useNightKit()` for the session.
|
|
21
|
+
- `src/panels/` — three things every app does, written once for every wallet:
|
|
22
|
+
read balances, send tokens (`makeTransfer` + `submitTransaction`), sign a
|
|
23
|
+
message (`signData`).
|
|
24
|
+
- `src/evm.tsx` — RainbowKit and wagmi, only for what your app does on Ethereum.
|
|
25
|
+
|
|
26
|
+
## Adding a contract
|
|
27
|
+
|
|
28
|
+
`session.api` is the DApp Connector API, so the usual midnight-js providers
|
|
29
|
+
work unchanged: use `session.api.getProvingProvider`, `balanceUnsealedTransaction`
|
|
30
|
+
and `submitTransaction` for your wallet and midnight providers, and
|
|
31
|
+
`session.addresses.coinPublicKey` / `encryptionPublicKey` for the keys. Add
|
|
32
|
+
`@midnightntwrk/ledger-v9` to your app's dependencies: proving a contract call
|
|
33
|
+
needs it.
|
|
34
|
+
|
|
35
|
+
For private state that should survive a new browser, derive its key from the
|
|
36
|
+
account: `await session.vault?.appSecret("private-state")` returns 32 bytes
|
|
37
|
+
only this account and your site can derive. (1AM has no equivalent; keep your
|
|
38
|
+
existing fallback for it.)
|
|
39
|
+
|
|
40
|
+
---
|
|
41
|
+
|
|
42
|
+
NightKit is made by [1AM](https://1am.xyz), the wallet for Midnight, and built by [Webisoft](https://webisoft.com). Site: [nightkit.1am.xyz](https://nightkit.1am.xyz).
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
<!doctype html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="utf-8" />
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
6
|
+
<title>1AM NightKit starter</title>
|
|
7
|
+
</head>
|
|
8
|
+
<body>
|
|
9
|
+
<div id="root"></div>
|
|
10
|
+
<script type="module" src="/src/main.tsx"></script>
|
|
11
|
+
</body>
|
|
12
|
+
</html>
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "nightkit-starter",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"private": true,
|
|
5
|
+
"type": "module",
|
|
6
|
+
"description": "A Midnight dApp starter: connect 1AM, or open a Midnight account from MetaMask, Coinbase, Phantom and friends, with NightKit.",
|
|
7
|
+
"scripts": {
|
|
8
|
+
"dev": "vite --port 5173 --strictPort",
|
|
9
|
+
"build": "tsc --noEmit && vite build",
|
|
10
|
+
"preview": "vite preview --port 5173 --strictPort",
|
|
11
|
+
"typecheck": "tsc --noEmit"
|
|
12
|
+
},
|
|
13
|
+
"dependencies": {
|
|
14
|
+
"nightkit": "workspace:*",
|
|
15
|
+
"nightkit-react": "workspace:*",
|
|
16
|
+
"@rainbow-me/rainbowkit": "2.2.11",
|
|
17
|
+
"@tanstack/react-query": "5.100.0",
|
|
18
|
+
"react": "19.1.1",
|
|
19
|
+
"react-dom": "19.1.1",
|
|
20
|
+
"viem": "2.38.0",
|
|
21
|
+
"wagmi": "2.19.5"
|
|
22
|
+
},
|
|
23
|
+
"devDependencies": {
|
|
24
|
+
"@midnightntwrk/dapp-connector-api": "4.1.0-beta.1",
|
|
25
|
+
"@types/react": "19.1.12",
|
|
26
|
+
"@types/react-dom": "19.1.9",
|
|
27
|
+
"@vitejs/plugin-react": "5.0.4",
|
|
28
|
+
"typescript": "5.9.3",
|
|
29
|
+
"vite": "8.3.1"
|
|
30
|
+
}
|
|
31
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
<svg width="7994.1" height="1200" viewBox="0 0 7994.1 1200" fill="none" xmlns="http://www.w3.org/2000/svg">
|
|
2
|
+
<path d="M1792.58 129.816V1052.53H1677.92V244.453H1516.13V129.816H1792.58Z" fill="white"/>
|
|
3
|
+
<path d="M2442.88 129.816L2798.79 1052.53H2685.59L2602.06 835.553H2162.73L2079.21 1052.53H1965.99L2321.9 129.816H2442.88ZM2206.21 720.915H2557.88L2382.77 267.417L2206.21 720.915Z" fill="white"/>
|
|
4
|
+
<path d="M3089.42 129.816L3454.69 852.165L3820.62 129.816H3951V1052.53H3837V371.571L3817.94 366.999L3468.9 1052.53H3441.15L3092.12 366.999L3073.05 371.571V1052.53H2958.4V129.816H3089.42Z" fill="white"/>
|
|
5
|
+
<path fill-rule="evenodd" clip-rule="evenodd" d="M840.949 50.5786C1121.58 173.163 1267.77 491.622 1169.82 788.141C1065.91 1102.7 726.607 1273.47 411.979 1169.57C97.3508 1065.66 -73.4818 726.415 30.4242 411.858C128.279 115.619 434.916 -53.0965 733.097 15.0459L698.213 120.652C458.575 71.4357 214.031 208.779 135.488 446.555C50.8781 702.697 190.478 979.916 446.678 1064.53C702.877 1149.13 980.155 1009.55 1064.77 753.407C1143.4 515.352 1028.4 259.097 806.071 156.165L840.949 50.5786ZM753.58 135.436C771.671 141.41 789.181 148.346 806.071 156.165L635.934 671.226L528.107 635.616L698.213 120.652C716.724 124.454 735.206 129.368 753.58 135.436Z" fill="white"/>
|
|
6
|
+
<path fill-rule="evenodd" clip-rule="evenodd" d="M7994.1 1052H4470V130H7994.1V1052ZM4524 998H7940.1V184H4524V998Z" fill="white"/>
|
|
7
|
+
<path d="M4769.8 805.2L4718 805.2L4718 380.4L4777.6 380.4L4913.6 647.2L4954.4 735.3L4955.7 735.3Q4954.4 703.5 4951.5 668.9Q4948.5 634.2 4948.5 600.6L4948.5 600.6L4948.5 380.4L5000.3 380.4L5000.3 805.2L4940.8 805.2L4804.8 538.4L4764.0 450.3L4762.7 450.3Q4764.0 482.7 4766.9 516.4Q4769.8 550.1 4769.8 583.1L4769.8 583.1L4769.8 805.2ZM5380.5 805.2L5115.0 805.2L5115.0 759.2L5220.5 759.2L5220.5 425.7L5115.0 425.7L5115.0 380.4L5380.5 380.4L5380.5 425.7L5274.9 425.7L5274.9 759.2L5380.5 759.2L5380.5 805.2ZM5660.2 813.0L5660.2 813.0Q5607.1 813.0 5565.4 786.7Q5523.6 760.5 5499.9 711.3Q5476.3 662.1 5476.3 592.8L5476.3 592.8Q5476.3 524.2 5500.6 474.9Q5524.9 425.7 5567.3 399.2Q5609.7 372.6 5664.8 372.6L5664.8 372.6Q5706.8 372.6 5735.3 389.1Q5763.8 405.6 5782.0 424.4L5782.0 424.4L5751.5 458.7Q5736.0 441.9 5715.6 430.9Q5695.2 419.9 5664.8 419.9L5664.8 419.9Q5625.3 419.9 5595.5 440.9Q5565.7 462.0 5549.2 500.2Q5532.7 538.4 5532.7 591.5L5532.7 591.5Q5532.7 671.8 5566.6 718.7Q5600.6 765.7 5663.5 765.7L5663.5 765.7Q5710.7 765.7 5737.3 740.4L5737.3 740.4L5737.3 629.7L5652.5 629.7L5652.5 585.0L5787.1 585.0L5787.1 763.8Q5767.1 783.8 5734.4 798.4Q5701.7 813.0 5660.2 813.0ZM5936.1 805.2L5881.7 805.2L5881.7 380.4L5936.1 380.4L5936.1 558.5L6113.5 558.5L6113.5 380.4L6167.9 380.4L6167.9 805.2L6113.5 805.2L6113.5 605.1L5936.1 605.1L5936.1 805.2ZM6440.6 805.2L6386.2 805.2L6386.2 425.7L6246.3 425.7L6246.3 380.4L6580.4 380.4L6580.4 425.7L6440.6 425.7L6440.6 805.2ZM6725.5 805.2L6671.1 805.2L6671.1 380.4L6725.5 380.4L6725.5 593.4L6727.4 593.4L6906.2 380.4L6967.7 380.4L6833.0 541.6L6983.2 805.2L6922.3 805.2L6798.7 582.4L6725.5 669.9L6725.5 805.2ZM7323.2 805.2L7057.7 805.2L7057.7 759.2L7163.2 759.2L7163.2 425.7L7057.7 425.7L7057.7 380.4L7323.2 380.4L7323.2 425.7L7217.6 425.7L7217.6 759.2L7323.2 759.2L7323.2 805.2ZM7606.2 805.2L7551.8 805.2L7551.8 425.7L7411.9 425.7L7411.9 380.4L7746.1 380.4L7746.1 425.7L7606.2 425.7L7606.2 805.2Z" fill="white"/>
|
|
8
|
+
</svg>
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import { ConnectButton as EvmConnectButton } from "@rainbow-me/rainbowkit";
|
|
2
|
+
import { ConnectButton, useNightKit } from "nightkit-react";
|
|
3
|
+
|
|
4
|
+
import { Balances } from "./panels/Balances.tsx";
|
|
5
|
+
import { Send } from "./panels/Send.tsx";
|
|
6
|
+
import { SignMessage } from "./panels/SignMessage.tsx";
|
|
7
|
+
|
|
8
|
+
export function App() {
|
|
9
|
+
const { session, sync, kit } = useNightKit();
|
|
10
|
+
return (
|
|
11
|
+
<>
|
|
12
|
+
<header className="top">
|
|
13
|
+
<span className="logo">
|
|
14
|
+
<span className="logo-1am" role="img" aria-label="1AM NightKit" /> <span className="product">Starter</span>
|
|
15
|
+
</span>
|
|
16
|
+
<span className="net">{kit.config.networkId}</span>
|
|
17
|
+
<span className="spacer" />
|
|
18
|
+
<EvmConnectButton showBalance={false} chainStatus="none" accountStatus="address" label="Ethereum wallet" />
|
|
19
|
+
<ConnectButton />
|
|
20
|
+
</header>
|
|
21
|
+
|
|
22
|
+
<main>
|
|
23
|
+
{!session ? (
|
|
24
|
+
<section className="hero">
|
|
25
|
+
<h1>One button. Any wallet. Midnight.</h1>
|
|
26
|
+
<p>
|
|
27
|
+
Connect 1AM, or open a Midnight account with the wallet you already have: MetaMask, Coinbase Wallet, Phantom, Rabby and more. Your Midnight keys stay
|
|
28
|
+
in a secure vault this page cannot read.
|
|
29
|
+
</p>
|
|
30
|
+
<ConnectButton label="Connect wallet" />
|
|
31
|
+
</section>
|
|
32
|
+
) : (
|
|
33
|
+
<>
|
|
34
|
+
<section className="panel">
|
|
35
|
+
<h2>Account</h2>
|
|
36
|
+
<dl>
|
|
37
|
+
<dt>Wallet</dt>
|
|
38
|
+
<dd>{session.name}</dd>
|
|
39
|
+
<dt>Private address</dt>
|
|
40
|
+
<dd className="mono" data-testid="shielded-address">
|
|
41
|
+
{session.addresses.shielded}
|
|
42
|
+
</dd>
|
|
43
|
+
<dt>Public address</dt>
|
|
44
|
+
<dd className="mono" data-testid="unshielded-address">
|
|
45
|
+
{session.addresses.unshielded}
|
|
46
|
+
</dd>
|
|
47
|
+
{session.vault ? (
|
|
48
|
+
<>
|
|
49
|
+
<dt>Opened with</dt>
|
|
50
|
+
<dd className="mono">
|
|
51
|
+
{session.vault.signer.walletName} · {session.vault.signer.address}
|
|
52
|
+
</dd>
|
|
53
|
+
<dt>Network fees</dt>
|
|
54
|
+
<dd>{session.vault.fees === "sponsor" ? "Paid for you" : "Paid from this account"}</dd>
|
|
55
|
+
<dt>Sync</dt>
|
|
56
|
+
<dd data-testid="sync">{sync?.synced ? "Up to date" : `Syncing${sync?.progress != null ? ` ${Math.floor(sync.progress * 100)}%` : "…"}`}</dd>
|
|
57
|
+
</>
|
|
58
|
+
) : null}
|
|
59
|
+
</dl>
|
|
60
|
+
</section>
|
|
61
|
+
<Balances />
|
|
62
|
+
<Send />
|
|
63
|
+
<SignMessage />
|
|
64
|
+
</>
|
|
65
|
+
)}
|
|
66
|
+
</main>
|
|
67
|
+
</>
|
|
68
|
+
);
|
|
69
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
// The Ethereum side, with RainbowKit. It is here for whatever your app does on
|
|
2
|
+
// Ethereum (a bridge, a token approval). NightKit does not need it: the vault
|
|
3
|
+
// finds the user's wallet itself. When one is connected here, though, NightKit
|
|
4
|
+
// is told which, so the vault offers the same wallet first.
|
|
5
|
+
|
|
6
|
+
import "@rainbow-me/rainbowkit/styles.css";
|
|
7
|
+
import { connectorsForWallets, darkTheme, lightTheme, RainbowKitProvider } from "@rainbow-me/rainbowkit";
|
|
8
|
+
import { coinbaseWallet, injectedWallet, metaMaskWallet, phantomWallet, rabbyWallet, rainbowWallet, walletConnectWallet } from "@rainbow-me/rainbowkit/wallets";
|
|
9
|
+
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
|
|
10
|
+
import type { ReactNode } from "react";
|
|
11
|
+
import { custom } from "viem";
|
|
12
|
+
import { createConfig, WagmiProvider } from "wagmi";
|
|
13
|
+
import { mainnet, sepolia } from "wagmi/chains";
|
|
14
|
+
|
|
15
|
+
const projectId = import.meta.env.VITE_WALLETCONNECT_PROJECT_ID ?? "";
|
|
16
|
+
|
|
17
|
+
// Coinbase's smart wallet signs with a passkey: nothing repeatable to open a
|
|
18
|
+
// Midnight account from. Its ordinary (EOA) wallet works.
|
|
19
|
+
coinbaseWallet.preference = "eoaOnly";
|
|
20
|
+
|
|
21
|
+
const connectors = connectorsForWallets(
|
|
22
|
+
projectId
|
|
23
|
+
? [{ groupName: "Popular", wallets: [metaMaskWallet, coinbaseWallet, phantomWallet, rabbyWallet, rainbowWallet, walletConnectWallet] }]
|
|
24
|
+
: // Without a project id: the extensions in this browser (they announce themselves) and Coinbase Wallet.
|
|
25
|
+
[{ groupName: "Ethereum wallets", wallets: [injectedWallet, coinbaseWallet] }],
|
|
26
|
+
{ appName: "NightKit starter", projectId: projectId || "unused" },
|
|
27
|
+
);
|
|
28
|
+
|
|
29
|
+
// No RPC of wagmi's own: a default public node would see every visitor's address (and
|
|
30
|
+
// refuses browser calls anyway). Read Ethereum through the connected wallet, or name your own RPC here.
|
|
31
|
+
const noRpc = custom({
|
|
32
|
+
request: async () => {
|
|
33
|
+
throw new Error("no RPC configured: Ethereum is read through the connected wallet");
|
|
34
|
+
},
|
|
35
|
+
});
|
|
36
|
+
const config = createConfig({ chains: [mainnet, sepolia], connectors, transports: { [mainnet.id]: noRpc, [sepolia.id]: noRpc } });
|
|
37
|
+
const queryClient = new QueryClient();
|
|
38
|
+
|
|
39
|
+
export function EvmProviders({ children }: { children: ReactNode }) {
|
|
40
|
+
return (
|
|
41
|
+
<WagmiProvider config={config}>
|
|
42
|
+
<QueryClientProvider client={queryClient}>
|
|
43
|
+
{/* Square and 1AM blue, to sit beside NightKit's button. */}
|
|
44
|
+
<RainbowKitProvider theme={{ lightMode: lightTheme({ borderRadius: "none", accentColor: "#0064d6" }), darkMode: darkTheme({ borderRadius: "none", accentColor: "#0064d6" }) }}>{children}</RainbowKitProvider>
|
|
45
|
+
</QueryClientProvider>
|
|
46
|
+
</WagmiProvider>
|
|
47
|
+
);
|
|
48
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { StrictMode } from "react";
|
|
2
|
+
import { createRoot } from "react-dom/client";
|
|
3
|
+
import { NightKitProvider } from "nightkit-react";
|
|
4
|
+
import { useAccount } from "wagmi";
|
|
5
|
+
|
|
6
|
+
import { App } from "./App.tsx";
|
|
7
|
+
import { EvmProviders } from "./evm.tsx";
|
|
8
|
+
import "./styles.css";
|
|
9
|
+
|
|
10
|
+
const config = {
|
|
11
|
+
networkId: import.meta.env.VITE_NETWORK_ID ?? "stagenet",
|
|
12
|
+
vaultUrl: import.meta.env.VITE_VAULT_URL ?? "http://localhost:5174",
|
|
13
|
+
appName: "NightKit starter",
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
function Root() {
|
|
17
|
+
// If an Ethereum wallet is connected through RainbowKit, tell the vault which.
|
|
18
|
+
const { address, connector } = useAccount();
|
|
19
|
+
return (
|
|
20
|
+
<NightKitProvider config={config} evm={address ? { account: address, walletRdns: connector?.id } : undefined}>
|
|
21
|
+
<App />
|
|
22
|
+
</NightKitProvider>
|
|
23
|
+
);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
createRoot(document.getElementById("root")!).render(
|
|
27
|
+
<StrictMode>
|
|
28
|
+
<EvmProviders>
|
|
29
|
+
<Root />
|
|
30
|
+
</EvmProviders>
|
|
31
|
+
</StrictMode>,
|
|
32
|
+
);
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import { useCallback, useEffect, useState } from "react";
|
|
2
|
+
import { useNightKit } from "nightkit-react";
|
|
3
|
+
|
|
4
|
+
const NIGHT = "0".repeat(64);
|
|
5
|
+
const name = (type: string) => (type === NIGHT ? "NIGHT" : `${type.slice(0, 8)}…`);
|
|
6
|
+
|
|
7
|
+
interface Loaded {
|
|
8
|
+
shielded: Record<string, bigint>;
|
|
9
|
+
unshielded: Record<string, bigint>;
|
|
10
|
+
dust: bigint;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function Balances() {
|
|
14
|
+
const { session, sync } = useNightKit();
|
|
15
|
+
const [data, setData] = useState<Loaded | null>(null);
|
|
16
|
+
const [error, setError] = useState<string | null>(null);
|
|
17
|
+
const [busy, setBusy] = useState(false);
|
|
18
|
+
|
|
19
|
+
// The same three calls for every wallet: this is the DApp Connector API.
|
|
20
|
+
const load = useCallback(async () => {
|
|
21
|
+
if (!session) return;
|
|
22
|
+
setBusy(true);
|
|
23
|
+
setError(null);
|
|
24
|
+
try {
|
|
25
|
+
const [shielded, unshielded, dust] = await Promise.all([session.api.getShieldedBalances(), session.api.getUnshieldedBalances(), session.api.getDustBalance()]);
|
|
26
|
+
setData({ shielded, unshielded, dust: dust.balance });
|
|
27
|
+
} catch (e) {
|
|
28
|
+
setError(e instanceof Error ? e.message : String(e));
|
|
29
|
+
} finally {
|
|
30
|
+
setBusy(false);
|
|
31
|
+
}
|
|
32
|
+
}, [session]);
|
|
33
|
+
|
|
34
|
+
// A vault account answers once it has synced; 1AM answers at once.
|
|
35
|
+
const ready = !session?.vault || sync?.synced === true;
|
|
36
|
+
useEffect(() => {
|
|
37
|
+
if (ready) void load();
|
|
38
|
+
}, [ready, load]);
|
|
39
|
+
|
|
40
|
+
const rows = data ? [...Object.entries(data.shielded).map(([t, v]) => ["Private", t, v] as const), ...Object.entries(data.unshielded).map(([t, v]) => ["Public", t, v] as const)] : [];
|
|
41
|
+
|
|
42
|
+
return (
|
|
43
|
+
<section className="panel">
|
|
44
|
+
<h2>
|
|
45
|
+
Balances
|
|
46
|
+
<button className="link" type="button" onClick={() => void load()} disabled={busy || !ready}>
|
|
47
|
+
{busy ? "Loading…" : "Refresh"}
|
|
48
|
+
</button>
|
|
49
|
+
</h2>
|
|
50
|
+
{!ready ? <p className="dim">Your account is syncing. Balances appear when it is done.</p> : null}
|
|
51
|
+
{error ? <p className="err">{error}</p> : null}
|
|
52
|
+
{data ? (
|
|
53
|
+
<table data-testid="balances">
|
|
54
|
+
<tbody>
|
|
55
|
+
{rows.length === 0 ? (
|
|
56
|
+
<tr>
|
|
57
|
+
<td className="dim">No tokens yet.</td>
|
|
58
|
+
</tr>
|
|
59
|
+
) : (
|
|
60
|
+
rows.map(([kind, type, value]) => (
|
|
61
|
+
<tr key={`${kind}${type}`}>
|
|
62
|
+
<td>{name(type)}</td>
|
|
63
|
+
<td className="dim">{kind}</td>
|
|
64
|
+
<td className="mono num">{value.toString()}</td>
|
|
65
|
+
</tr>
|
|
66
|
+
))
|
|
67
|
+
)}
|
|
68
|
+
<tr>
|
|
69
|
+
<td>Fee balance (DUST)</td>
|
|
70
|
+
<td />
|
|
71
|
+
<td className="mono num">{data.dust.toString()}</td>
|
|
72
|
+
</tr>
|
|
73
|
+
</tbody>
|
|
74
|
+
</table>
|
|
75
|
+
) : null}
|
|
76
|
+
</section>
|
|
77
|
+
);
|
|
78
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { useState } from "react";
|
|
2
|
+
import { isUserRejection } from "nightkit";
|
|
3
|
+
import { useNightKit } from "nightkit-react";
|
|
4
|
+
|
|
5
|
+
const NIGHT = "0".repeat(64);
|
|
6
|
+
|
|
7
|
+
export function Send() {
|
|
8
|
+
const { session } = useNightKit();
|
|
9
|
+
const [recipient, setRecipient] = useState("");
|
|
10
|
+
const [amount, setAmount] = useState("");
|
|
11
|
+
const [status, setStatus] = useState<{ kind: "idle" | "busy" | "done" | "error"; text?: string }>({ kind: "idle" });
|
|
12
|
+
if (!session) return null;
|
|
13
|
+
|
|
14
|
+
const send = async () => {
|
|
15
|
+
setStatus({ kind: "busy", text: "Approve in your wallet…" });
|
|
16
|
+
try {
|
|
17
|
+
const kind = recipient.startsWith("mn_shield-addr") ? "shielded" : "unshielded";
|
|
18
|
+
// makeTransfer builds, balances and signs; submitTransaction sends it. The
|
|
19
|
+
// wallet shows the user what they are approving: 1AM in its popup, a
|
|
20
|
+
// NightKit account in the vault.
|
|
21
|
+
const { tx } = await session.api.makeTransfer([{ kind, type: NIGHT, value: BigInt(amount), recipient: recipient.trim() }]);
|
|
22
|
+
setStatus({ kind: "busy", text: "Sending…" });
|
|
23
|
+
await session.api.submitTransaction(tx);
|
|
24
|
+
setStatus({ kind: "done", text: "Sent." });
|
|
25
|
+
} catch (e) {
|
|
26
|
+
setStatus(isUserRejection(e) ? { kind: "idle" } : { kind: "error", text: e instanceof Error ? e.message : String(e) });
|
|
27
|
+
}
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
return (
|
|
31
|
+
<section className="panel">
|
|
32
|
+
<h2>Send NIGHT</h2>
|
|
33
|
+
<label>
|
|
34
|
+
To
|
|
35
|
+
<input value={recipient} onChange={(e) => setRecipient(e.target.value)} placeholder="mn_addr… or mn_shield-addr…" spellCheck={false} data-testid="send-to" />
|
|
36
|
+
</label>
|
|
37
|
+
<label>
|
|
38
|
+
Amount (smallest units)
|
|
39
|
+
<input value={amount} onChange={(e) => setAmount(e.target.value.replace(/\D/g, ""))} inputMode="numeric" placeholder="1000000" data-testid="send-amount" />
|
|
40
|
+
</label>
|
|
41
|
+
<button className="cta" type="button" disabled={!recipient || !amount || status.kind === "busy"} onClick={() => void send()} data-testid="send">
|
|
42
|
+
{status.kind === "busy" ? status.text : "Send"}
|
|
43
|
+
</button>
|
|
44
|
+
{status.kind === "done" ? <p className="ok">{status.text}</p> : null}
|
|
45
|
+
{status.kind === "error" ? <p className="err">{status.text}</p> : null}
|
|
46
|
+
</section>
|
|
47
|
+
);
|
|
48
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { useState } from "react";
|
|
2
|
+
import { isUserRejection } from "nightkit";
|
|
3
|
+
import { useNightKit } from "nightkit-react";
|
|
4
|
+
|
|
5
|
+
export function SignMessage() {
|
|
6
|
+
const { session } = useNightKit();
|
|
7
|
+
const [message, setMessage] = useState("Hello from Midnight");
|
|
8
|
+
const [result, setResult] = useState<string | null>(null);
|
|
9
|
+
const [error, setError] = useState<string | null>(null);
|
|
10
|
+
if (!session) return null;
|
|
11
|
+
|
|
12
|
+
const sign = async () => {
|
|
13
|
+
setError(null);
|
|
14
|
+
setResult(null);
|
|
15
|
+
try {
|
|
16
|
+
const s = await session.api.signData(message, { encoding: "text", keyType: "unshielded" });
|
|
17
|
+
setResult(s.signature);
|
|
18
|
+
} catch (e) {
|
|
19
|
+
if (!isUserRejection(e)) setError(e instanceof Error ? e.message : String(e));
|
|
20
|
+
}
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
return (
|
|
24
|
+
<section className="panel">
|
|
25
|
+
<h2>Sign a message</h2>
|
|
26
|
+
<label>
|
|
27
|
+
Message
|
|
28
|
+
<input value={message} onChange={(e) => setMessage(e.target.value)} data-testid="sign-message" />
|
|
29
|
+
</label>
|
|
30
|
+
<button className="cta" type="button" disabled={!message} onClick={() => void sign()} data-testid="sign">
|
|
31
|
+
Sign
|
|
32
|
+
</button>
|
|
33
|
+
{result ? (
|
|
34
|
+
<p className="mono wrap" data-testid="signature">
|
|
35
|
+
{result}
|
|
36
|
+
</p>
|
|
37
|
+
) : null}
|
|
38
|
+
{error ? <p className="err">{error}</p> : null}
|
|
39
|
+
</section>
|
|
40
|
+
);
|
|
41
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
:root { --bg: #0c0e12; --surface: #13161b; --panel: #22262f; --border: #373a41; --text: #f7f7f7; --dim: #cecfd2; --faint: #94979c; --accent: #0064d6; --accent-hi: #459dff; --danger: #f97066; --live: #47cd89; --cta: #f7f7f7; --cta-ink: #0c0e12; color-scheme: dark; }
|
|
2
|
+
@media (prefers-color-scheme: light) { :root { --bg: #f5f5f5; --surface: #fff; --panel: #fafafa; --border: #e9eaeb; --text: #181d27; --dim: #414651; --faint: #535862; --accent-hi: #0064d6; --danger: #b42318; --live: #079455; --cta: #0a0d12; --cta-ink: #fff; color-scheme: light; } }
|
|
3
|
+
* { box-sizing: border-box; }
|
|
4
|
+
body { margin: 0; background: var(--bg); color: var(--text); font: 15px/1.5 "Outfit", ui-sans-serif, system-ui, sans-serif; -webkit-font-smoothing: antialiased; }
|
|
5
|
+
.mono { font-family: "JetBrains Mono", ui-monospace, Menlo, monospace; font-size: 13px; }
|
|
6
|
+
.top { display: flex; align-items: center; gap: 12px; padding: 12px 20px; background: var(--surface); border-bottom: 1px solid var(--border); flex-wrap: wrap; }
|
|
7
|
+
.logo { display: flex; align-items: center; gap: 8px; font-weight: 600; }
|
|
8
|
+
/* 1AM's logo, painted in the text colour so it reads in light and dark. */
|
|
9
|
+
.logo-1am { width: 147px; height: 22px; background: currentColor; -webkit-mask: url(/logo-1am-nightkit.svg) center / contain no-repeat; mask: url(/logo-1am-nightkit.svg) center / contain no-repeat; }
|
|
10
|
+
.product { padding-left: 10px; border-left: 1px solid var(--border); }
|
|
11
|
+
.net { font: 500 11px/1 ui-monospace, monospace; text-transform: uppercase; letter-spacing: .08em; color: var(--faint); border: 1px solid var(--border); padding: 5px 6px; }
|
|
12
|
+
.spacer { flex: 1; }
|
|
13
|
+
main { max-width: 760px; margin: 0 auto; padding: 28px 16px 64px; display: grid; gap: 16px; }
|
|
14
|
+
.hero { padding: 48px 0; }
|
|
15
|
+
.hero h1 { margin: 0 0 12px; font: 600 clamp(28px, 5vw, 40px)/1.1 inherit; letter-spacing: -.02em; }
|
|
16
|
+
.hero p { margin: 0 0 24px; max-width: 56ch; color: var(--dim); }
|
|
17
|
+
.panel { background: var(--surface); border: 1px solid var(--border); padding: 18px; }
|
|
18
|
+
.panel h2 { display: flex; align-items: baseline; justify-content: space-between; margin: 0 0 14px; font: 500 11px/1 ui-monospace, monospace; text-transform: uppercase; letter-spacing: .08em; color: var(--faint); }
|
|
19
|
+
dl { display: grid; grid-template-columns: 140px 1fr; gap: 8px 16px; margin: 0; }
|
|
20
|
+
dt { color: var(--dim); }
|
|
21
|
+
dd { margin: 0; overflow-wrap: anywhere; }
|
|
22
|
+
table { width: 100%; border-collapse: collapse; }
|
|
23
|
+
td { padding: 8px 0; border-top: 1px solid var(--border); }
|
|
24
|
+
.num { text-align: right; }
|
|
25
|
+
label { display: grid; gap: 6px; margin-bottom: 12px; color: var(--dim); font-size: 13px; }
|
|
26
|
+
input { height: 42px; padding: 0 12px; background: var(--panel); border: 1px solid var(--border); color: var(--text); font: 14px ui-monospace, Menlo, monospace; border-radius: 0; }
|
|
27
|
+
input:focus-visible, button:focus-visible { outline: 2px solid var(--accent-hi); outline-offset: 2px; }
|
|
28
|
+
.cta { min-height: 42px; padding: 0 18px; border: 1px solid var(--cta); background: var(--cta); color: var(--cta-ink); font: 600 13px/1 inherit; letter-spacing: .06em; text-transform: uppercase; cursor: pointer; border-radius: 0; }
|
|
29
|
+
.cta:disabled { opacity: .5; cursor: default; }
|
|
30
|
+
.link { border: 0; background: none; color: var(--accent-hi); font: inherit; cursor: pointer; padding: 0; }
|
|
31
|
+
.link:disabled { color: var(--faint); cursor: default; }
|
|
32
|
+
.dim { color: var(--dim); }
|
|
33
|
+
.ok { color: var(--live); margin: 12px 0 0; }
|
|
34
|
+
.err { color: var(--danger); margin: 12px 0 0; overflow-wrap: anywhere; }
|
|
35
|
+
.wrap { overflow-wrap: anywhere; margin: 12px 0 0; }
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{ "extends": "../../tsconfig.base.json", "compilerOptions": { "noEmit": true, "jsx": "react-jsx", "types": ["vite/client"] }, "include": ["src", "vite.config.ts"] }
|