@geeqdev/geeq-sdk 0.0.1 → 0.1.1
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/.idea/geeq_sdk.iml +12 -0
- package/.idea/modules.xml +8 -0
- package/.idea/vcs.xml +6 -0
- package/.nvmrc +1 -0
- package/CONTRIBUTING.md +20 -0
- package/example/.env.example +4 -0
- package/example/README.md +304 -0
- package/example/USAGE.md +74 -0
- package/example/index.html +12 -0
- package/example/package-lock.json +1557 -0
- package/example/package.json +25 -0
- package/example/scripts/browser-check.mjs +1421 -0
- package/example/scripts/match-derivation.mjs +65 -0
- package/example/src/App.vue +138 -0
- package/example/src/components/AuthorityPanel.vue +277 -0
- package/example/src/components/ChainPanel.vue +267 -0
- package/example/src/components/ChainState.vue +59 -0
- package/example/src/components/CopyButton.vue +25 -0
- package/example/src/components/CreateAssetPanel.vue +313 -0
- package/example/src/components/DigitalAssetsPanel.vue +68 -0
- package/example/src/components/EvidencePanel.vue +396 -0
- package/example/src/components/ExplorerPanel.vue +119 -0
- package/example/src/components/KeypairPanel.vue +182 -0
- package/example/src/components/PhrasePanel.vue +105 -0
- package/example/src/components/SocketWarning.vue +18 -0
- package/example/src/components/StepGate.vue +24 -0
- package/example/src/components/TransferPanel.vue +416 -0
- package/example/src/components/TxDeadline.vue +19 -0
- package/example/src/components/TxFee.vue +31 -0
- package/example/src/components/TxStatus.vue +105 -0
- package/example/src/composables/useSocketConnection.ts +80 -0
- package/example/src/env.d.ts +12 -0
- package/example/src/main.ts +5 -0
- package/example/src/pennies.ts +7 -0
- package/example/src/style.css +149 -0
- package/example/tsconfig.json +19 -0
- package/example/vite.config.ts +26 -0
- package/jest.config.js +18 -0
- package/package.json +1 -5
- package/scripts/copy-wasm.mjs +21 -0
- package/scripts/smoke.mjs +382 -0
- package/src/address/record-type.ts +77 -0
- package/src/address/wallet-address.ts +105 -0
- package/src/amount.ts +143 -0
- package/src/assert.ts +30 -0
- package/src/capnp/README.md +15 -0
- package/src/capnp/adminNetTx.capnp.ts +66 -0
- package/src/capnp/appLayBlock.capnp.ts +26 -0
- package/src/capnp/appLayLedger.capnp.ts +35 -0
- package/src/capnp/appLayRec.capnp.ts +159 -0
- package/src/capnp/chainStateView.capnp.ts +49 -0
- package/src/capnp/coinAcctRec.capnp.ts +193 -0
- package/src/capnp/genesisBlock.capnp.ts +56 -0
- package/src/capnp/hubContentStmt.capnp.ts +59 -0
- package/src/capnp/misc.capnp.ts +19 -0
- package/src/capnp/netActorRec.capnp.ts +64 -0
- package/src/capnp/nodeContentStmt.capnp.ts +62 -0
- package/src/capnp/nodePayloadList.capnp.ts +28 -0
- package/src/capnp/nodeTxList.capnp.ts +37 -0
- package/src/capnp/paramAcctRec.capnp.ts +669 -0
- package/src/capnp/payload.capnp.ts +261 -0
- package/src/capnp/proxyAcctRec.capnp.ts +259 -0
- package/src/capnp/relayContentStmt.capnp.ts +85 -0
- package/src/capnp/sysAcctRec.capnp.ts +47 -0
- package/src/capnp/userAppTx.capnp.ts +269 -0
- package/src/capnp/userValTx.capnp.ts +1999 -0
- package/src/capnp/valLayBlock.capnp.ts +93 -0
- package/src/capnp/valLayLedger.capnp.ts +80 -0
- package/src/chain/geeq-chain.ts +1094 -0
- package/src/chain/geeq-node.ts +105 -0
- package/src/chain/nonce-guard.ts +73 -0
- package/src/chain/select.ts +149 -0
- package/src/crypto/account-id.ts +83 -0
- package/src/crypto/keypair.ts +221 -0
- package/src/crypto/mnemonic.ts +69 -0
- package/src/encoding/base64.ts +58 -0
- package/src/encoding/hex.ts +42 -0
- package/src/errors.ts +122 -0
- package/src/index.ts +195 -0
- package/src/ledger/coin-accounts.ts +73 -0
- package/src/ledger/decode.ts +147 -0
- package/src/ledger/geeq-ledger.ts +184 -0
- package/src/ledger/net-actors.ts +129 -0
- package/src/ledger/nft-records.ts +49 -0
- package/src/ledger/params.ts +82 -0
- package/src/ledger/proxy-accounts.ts +51 -0
- package/src/net/discovery.ts +231 -0
- package/src/net/explorer.ts +123 -0
- package/src/net/read-ledger.ts +58 -0
- package/src/net/routes.ts +79 -0
- package/src/net/transport.ts +221 -0
- package/src/net/websocket.ts +94 -0
- package/src/runtime/bindings.ts +38 -0
- package/src/runtime/init.ts +49 -0
- package/src/runtime/wasm-loader.browser.ts +22 -0
- package/src/runtime/wasm-loader.node.ts +16 -0
- package/src/socket/geeq-socket.ts +827 -0
- package/src/socket/tracked-tx.ts +319 -0
- package/src/socket/tx-status.ts +131 -0
- package/src/tx/attestation.ts +222 -0
- package/src/tx/content.ts +82 -0
- package/src/tx/encode.ts +107 -0
- package/src/tx/expiry.ts +11 -0
- package/src/tx/fee.ts +31 -0
- package/src/tx/metadata.ts +100 -0
- package/src/tx/nft.ts +108 -0
- package/src/tx/proxy.ts +156 -0
- package/src/tx/transfer.ts +162 -0
- package/src/types.ts +503 -0
- package/src/wasm/README.md +22 -0
- package/src/wasm/browser/geeq_core.d.ts +1224 -0
- package/src/wasm/browser/geeq_core.js +6014 -0
- package/src/wasm/browser/geeq_core_bg.wasm +0 -0
- package/src/wasm/browser/geeq_core_bg.wasm.d.ts +447 -0
- package/src/wasm/nodejs/geeq_core.d.ts +753 -0
- package/src/wasm/nodejs/geeq_core.js +6016 -0
- package/src/wasm/nodejs/geeq_core_bg.wasm +0 -0
- package/src/wasm/nodejs/geeq_core_bg.wasm.d.ts +447 -0
- package/src/wasm/nodejs/package.json +3 -0
- package/tsconfig.build.json +8 -0
- package/tsconfig.json +20 -0
- package/tsup.config.ts +60 -0
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import { markRaw, ref, shallowRef, watch } from 'vue'
|
|
3
|
+
import { GEEQ_RECORD_TYPES, GeeqError, keypairFromMnemonic, toBase64 } from '@geeqdev/geeq-sdk'
|
|
4
|
+
import type { GeeqCoinType, GeeqKeypair, GeeqRecordType } from '@geeqdev/geeq-sdk'
|
|
5
|
+
import CopyButton from './CopyButton.vue'
|
|
6
|
+
|
|
7
|
+
const props = defineProps<{ mnemonic: string; engineReady: boolean }>()
|
|
8
|
+
|
|
9
|
+
// Evidence must name the chain it targets, and it has to be the chain this
|
|
10
|
+
// account lives on — so the number is owned by App and shared, not duplicated here.
|
|
11
|
+
const chainNumber = defineModel<number>('chainNumber', { required: true })
|
|
12
|
+
// The signing key is shared the same way. Emitted as null whenever it is disposed,
|
|
13
|
+
// so no other panel can reach a freed engine handle.
|
|
14
|
+
const emit = defineEmits<{ (e: 'keypair', keypair: GeeqKeypair | null): void }>()
|
|
15
|
+
// Fixed, deliberately not exposed. It is part of the SLIP-0010 derivation path
|
|
16
|
+
// (BIP-44 coin type: testnet 1, mainnet 1019), so it changes the key itself rather
|
|
17
|
+
// than just labelling the address the way chainNumber does. Note this overrides the
|
|
18
|
+
// SDK's own 'testnet' default — passing it explicitly on every call is what keeps
|
|
19
|
+
// this demo lined up with the Geeq apps.
|
|
20
|
+
// GeeqCoinType is a string union ('mainnet' | 'testnet'), not an enum — the SDK
|
|
21
|
+
// keeps the readable name in its public API and maps it to the engine's numeric
|
|
22
|
+
// CoinType internally, so there is no GeeqCoinType.Mainnet to reference.
|
|
23
|
+
const COIN_TYPE: GeeqCoinType = 'mainnet'
|
|
24
|
+
// BIP-39 passphrase ("25th word"). Folded into the seed, so it changes the key.
|
|
25
|
+
const passphrase = ref('')
|
|
26
|
+
// Record type sits in the derivation path *and* in the address envelope, so it is
|
|
27
|
+
// passed to both calls below — deriving under one record type and encoding under
|
|
28
|
+
// another would yield an address the owner of the key could not reproduce.
|
|
29
|
+
const recordType = ref<GeeqRecordType>('coin-asset')
|
|
30
|
+
// shallowRef + markRaw: a keypair is a class instance holding an engine handle and
|
|
31
|
+
// typed arrays. Deep reactivity would proxy those arrays before they reach
|
|
32
|
+
// WebAssembly, and buys nothing — the object is replaced wholesale, never mutated.
|
|
33
|
+
const keypair = shallowRef<GeeqKeypair | null>(null)
|
|
34
|
+
const address = ref('')
|
|
35
|
+
const error = ref('')
|
|
36
|
+
const busy = ref(false)
|
|
37
|
+
const revealPrivateKey = ref(false)
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
// A keypair holds an engine-side handle; release it before replacing or dropping it.
|
|
41
|
+
function clear() {
|
|
42
|
+
keypair.value?.dispose()
|
|
43
|
+
keypair.value = null
|
|
44
|
+
address.value = ''
|
|
45
|
+
error.value = ''
|
|
46
|
+
revealPrivateKey.value = false
|
|
47
|
+
emit('keypair', null)
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// The phrase or the derivation path changing invalidates any keypair derived from
|
|
51
|
+
// the previous one.
|
|
52
|
+
watch([() => props.mnemonic, passphrase, recordType], clear)
|
|
53
|
+
|
|
54
|
+
async function derive() {
|
|
55
|
+
clear()
|
|
56
|
+
busy.value = true
|
|
57
|
+
try {
|
|
58
|
+
const kp = await keypairFromMnemonic(props.mnemonic, {
|
|
59
|
+
coinType: COIN_TYPE,
|
|
60
|
+
passphrase: passphrase.value,
|
|
61
|
+
recordType: recordType.value,
|
|
62
|
+
})
|
|
63
|
+
keypair.value = markRaw(kp)
|
|
64
|
+
address.value = kp.getAddress({
|
|
65
|
+
chainNumber: chainNumber.value,
|
|
66
|
+
recordType: recordType.value,
|
|
67
|
+
})
|
|
68
|
+
emit('keypair', kp)
|
|
69
|
+
} catch (e) {
|
|
70
|
+
error.value =
|
|
71
|
+
e instanceof GeeqError ? `${e.code}: ${e.message}` : (e as Error).message
|
|
72
|
+
} finally {
|
|
73
|
+
busy.value = false
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// The address encodes the chain, so changing it re-derives the address only —
|
|
78
|
+
// the keypair itself is unaffected.
|
|
79
|
+
watch(chainNumber, (next) => {
|
|
80
|
+
if (keypair.value) {
|
|
81
|
+
address.value = keypair.value.getAddress({
|
|
82
|
+
chainNumber: next,
|
|
83
|
+
recordType: recordType.value,
|
|
84
|
+
})
|
|
85
|
+
}
|
|
86
|
+
})
|
|
87
|
+
</script>
|
|
88
|
+
|
|
89
|
+
<template>
|
|
90
|
+
<section class="card">
|
|
91
|
+
<h2><span class="step">2</span>Keypair & address</h2>
|
|
92
|
+
|
|
93
|
+
<div class="row">
|
|
94
|
+
<span class="ctl">
|
|
95
|
+
<label for="chain">Chain number</label>
|
|
96
|
+
<input id="chain" v-model.number="chainNumber" type="number" min="0" />
|
|
97
|
+
</span>
|
|
98
|
+
<span class="ctl">
|
|
99
|
+
<label for="record">Record type</label>
|
|
100
|
+
<select id="record" v-model="recordType">
|
|
101
|
+
<option v-for="type in GEEQ_RECORD_TYPES" :key="type" :value="type">{{ type }}</option>
|
|
102
|
+
</select>
|
|
103
|
+
</span>
|
|
104
|
+
<span class="ctl">
|
|
105
|
+
<label for="passphrase">Passphrase</label>
|
|
106
|
+
<input
|
|
107
|
+
id="passphrase"
|
|
108
|
+
v-model="passphrase"
|
|
109
|
+
type="text"
|
|
110
|
+
placeholder="empty — type here"
|
|
111
|
+
:class="{ unset: passphrase === '' }"
|
|
112
|
+
/>
|
|
113
|
+
</span>
|
|
114
|
+
<button
|
|
115
|
+
class="primary"
|
|
116
|
+
:disabled="!mnemonic || !engineReady || busy"
|
|
117
|
+
@click="derive"
|
|
118
|
+
>
|
|
119
|
+
{{ busy ? 'Deriving…' : 'Derive keypair' }}
|
|
120
|
+
</button>
|
|
121
|
+
</div>
|
|
122
|
+
|
|
123
|
+
<p v-if="!engineReady" class="empty">Waiting for the engine to finish loading…</p>
|
|
124
|
+
|
|
125
|
+
<div v-if="error" class="warn">{{ error }}</div>
|
|
126
|
+
|
|
127
|
+
<template v-if="keypair">
|
|
128
|
+
<div class="field">
|
|
129
|
+
<div class="k">
|
|
130
|
+
Address · base58 · chain {{ chainNumber }} · {{ COIN_TYPE }} · {{ recordType }} ·
|
|
131
|
+
<template v-if="passphrase">passphrase “{{ passphrase }}”</template>
|
|
132
|
+
<template v-else>no passphrase</template>
|
|
133
|
+
</div>
|
|
134
|
+
<div class="v">{{ address }}</div>
|
|
135
|
+
<div class="row" style="margin-top: 7px"><CopyButton :value="address" /></div>
|
|
136
|
+
</div>
|
|
137
|
+
|
|
138
|
+
<div class="field">
|
|
139
|
+
<div class="k">Public key · base64 · 32 bytes</div>
|
|
140
|
+
<div class="v">{{ toBase64(keypair.publicKey) }}</div>
|
|
141
|
+
</div>
|
|
142
|
+
|
|
143
|
+
<div class="field">
|
|
144
|
+
<div class="k">Account id · base64 · 32 bytes · BLAKE3 of the public key</div>
|
|
145
|
+
<div class="v">{{ toBase64(keypair.accountId) }}</div>
|
|
146
|
+
</div>
|
|
147
|
+
|
|
148
|
+
<div class="field">
|
|
149
|
+
<div class="k">Private key · 32 bytes</div>
|
|
150
|
+
<div v-if="revealPrivateKey" class="v">{{ toBase64(keypair.privateKey) }}</div>
|
|
151
|
+
<div v-else class="v" style="color: var(--muted)">••••••••••••••••••••••••••••••••</div>
|
|
152
|
+
<div class="row" style="margin-top: 7px">
|
|
153
|
+
<button class="small" @click="revealPrivateKey = !revealPrivateKey">
|
|
154
|
+
{{ revealPrivateKey ? 'Hide' : 'Reveal' }}
|
|
155
|
+
</button>
|
|
156
|
+
</div>
|
|
157
|
+
</div>
|
|
158
|
+
|
|
159
|
+
<div class="warn">
|
|
160
|
+
Shown here only to demonstrate the SDK. A real application should never display
|
|
161
|
+
or log a private key. The SDK keeps it out of harm's way by default —
|
|
162
|
+
<code>JSON.stringify(keypair)</code> emits only the public key.
|
|
163
|
+
</div>
|
|
164
|
+
|
|
165
|
+
<p class="hint">
|
|
166
|
+
Changing the chain number only re-encodes the address, since the chain is stamped
|
|
167
|
+
alongside the account id. Record type and passphrase feed the derivation path
|
|
168
|
+
instead, so changing either re-derives the key and the account id with it — which
|
|
169
|
+
is how one phrase yields different addresses in two apps. Coin type is fixed
|
|
170
|
+
at <code>mainnet</code> here.
|
|
171
|
+
</p>
|
|
172
|
+
</template>
|
|
173
|
+
</section>
|
|
174
|
+
</template>
|
|
175
|
+
|
|
176
|
+
<style scoped>
|
|
177
|
+
.ctl { display: inline-flex; align-items: center; gap: 8px; }
|
|
178
|
+
.ctl input, .ctl select { width: 140px; }
|
|
179
|
+
/* An empty passphrase is a real setting, not a missing one — say so rather than
|
|
180
|
+
letting a grey placeholder read as a chosen value. */
|
|
181
|
+
.ctl input.unset { border-style: dashed; }
|
|
182
|
+
</style>
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import { computed, ref } from 'vue'
|
|
3
|
+
import { generateMnemonic, validateMnemonic } from '@geeqdev/geeq-sdk'
|
|
4
|
+
import type { MnemonicWordCount } from '@geeqdev/geeq-sdk'
|
|
5
|
+
import CopyButton from './CopyButton.vue'
|
|
6
|
+
|
|
7
|
+
const emit = defineEmits<{ (e: 'change', mnemonic: string): void }>()
|
|
8
|
+
|
|
9
|
+
const mode = ref<'create' | 'restore'>('create')
|
|
10
|
+
// Fixed, deliberately not exposed. The SDK supports 12/15/18/21/24, but the Geeq
|
|
11
|
+
// apps this demo mirrors issue 12-word phrases, so offering a choice here would let
|
|
12
|
+
// someone generate a phrase those apps cannot produce.
|
|
13
|
+
const WORD_COUNT: MnemonicWordCount = 12
|
|
14
|
+
const phrase = ref('')
|
|
15
|
+
const restoreInput = ref('')
|
|
16
|
+
|
|
17
|
+
const words = computed(() => (phrase.value ? phrase.value.split(' ') : []))
|
|
18
|
+
|
|
19
|
+
// Pure JavaScript in the SDK — no engine needed, so this is instant and works
|
|
20
|
+
// even while the WebAssembly engine is still downloading.
|
|
21
|
+
function create() {
|
|
22
|
+
phrase.value = generateMnemonic({ words: WORD_COUNT })
|
|
23
|
+
emit('change', phrase.value)
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const restoreState = computed(() => {
|
|
27
|
+
if (restoreInput.value.trim() === '') return 'empty'
|
|
28
|
+
return validateMnemonic(restoreInput.value) ? 'valid' : 'invalid'
|
|
29
|
+
})
|
|
30
|
+
|
|
31
|
+
function useRestored() {
|
|
32
|
+
phrase.value = restoreInput.value.trim().replace(/\s+/gu, ' ').toLowerCase()
|
|
33
|
+
emit('change', phrase.value)
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function switchMode(next: 'create' | 'restore') {
|
|
37
|
+
mode.value = next
|
|
38
|
+
phrase.value = ''
|
|
39
|
+
emit('change', '')
|
|
40
|
+
}
|
|
41
|
+
</script>
|
|
42
|
+
|
|
43
|
+
<template>
|
|
44
|
+
<section class="card">
|
|
45
|
+
<h2><span class="step">1</span>Recovery phrase</h2>
|
|
46
|
+
|
|
47
|
+
<div class="row">
|
|
48
|
+
<button :class="{ primary: mode === 'create' }" @click="switchMode('create')">
|
|
49
|
+
Create new
|
|
50
|
+
</button>
|
|
51
|
+
<button :class="{ primary: mode === 'restore' }" @click="switchMode('restore')">
|
|
52
|
+
Restore existing
|
|
53
|
+
</button>
|
|
54
|
+
</div>
|
|
55
|
+
|
|
56
|
+
<template v-if="mode === 'create'">
|
|
57
|
+
<div class="row" style="margin-top: 16px">
|
|
58
|
+
<button class="primary" @click="create">Generate phrase</button>
|
|
59
|
+
<CopyButton v-if="phrase" :value="phrase" />
|
|
60
|
+
</div>
|
|
61
|
+
|
|
62
|
+
<div v-if="words.length" class="phrase" style="margin-top: 16px">
|
|
63
|
+
<div v-for="(word, i) in words" :key="i" class="word">
|
|
64
|
+
<span class="n">{{ i + 1 }}</span><span>{{ word }}</span>
|
|
65
|
+
</div>
|
|
66
|
+
</div>
|
|
67
|
+
<p v-else class="hint">
|
|
68
|
+
Twelve words, generated with <code>generateMnemonic()</code> using the platform's
|
|
69
|
+
secure random source. No engine required, so this works instantly.
|
|
70
|
+
</p>
|
|
71
|
+
|
|
72
|
+
<div v-if="phrase" class="warn">
|
|
73
|
+
<strong>Write these words down in order.</strong>
|
|
74
|
+
They are the only way to recover your account. Anyone who reads them controls it.
|
|
75
|
+
This demo keeps them in memory only — nothing is saved or sent anywhere.
|
|
76
|
+
</div>
|
|
77
|
+
</template>
|
|
78
|
+
|
|
79
|
+
<template v-else>
|
|
80
|
+
<div style="margin-top: 16px">
|
|
81
|
+
<textarea
|
|
82
|
+
v-model="restoreInput"
|
|
83
|
+
spellcheck="false"
|
|
84
|
+
autocapitalize="none"
|
|
85
|
+
placeholder="Enter your 12-word recovery phrase, separated by spaces"
|
|
86
|
+
/>
|
|
87
|
+
<div class="row" style="margin-top: 10px">
|
|
88
|
+
<span
|
|
89
|
+
class="hint grow"
|
|
90
|
+
:class="{ valid: restoreState === 'valid', invalid: restoreState === 'invalid' }"
|
|
91
|
+
>
|
|
92
|
+
<template v-if="restoreState === 'empty'">
|
|
93
|
+
Checked with <code>validateMnemonic()</code> as you type.
|
|
94
|
+
</template>
|
|
95
|
+
<template v-else-if="restoreState === 'valid'">Valid phrase.</template>
|
|
96
|
+
<template v-else>Not a valid phrase — check the words and their order.</template>
|
|
97
|
+
</span>
|
|
98
|
+
<button class="primary" :disabled="restoreState !== 'valid'" @click="useRestored">
|
|
99
|
+
Use this phrase
|
|
100
|
+
</button>
|
|
101
|
+
</div>
|
|
102
|
+
</div>
|
|
103
|
+
</template>
|
|
104
|
+
</section>
|
|
105
|
+
</template>
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import type { SocketConnection } from '../composables/useSocketConnection'
|
|
3
|
+
|
|
4
|
+
defineProps<{ connection: SocketConnection }>()
|
|
5
|
+
</script>
|
|
6
|
+
|
|
7
|
+
<template>
|
|
8
|
+
<div v-if="connection.state === 'failed'" class="warn" data-testid="socket-warning">
|
|
9
|
+
<strong>No confirmation will follow.</strong> The node socket did not open, so you can
|
|
10
|
+
still submit, but nothing will tell you what the chain decided. {{ connection.error }}
|
|
11
|
+
<div class="row" style="margin-top: 8px">
|
|
12
|
+
<button class="small" data-testid="socket-retry" @click="connection.retry()">Retry</button>
|
|
13
|
+
</div>
|
|
14
|
+
</div>
|
|
15
|
+
<p v-else-if="connection.state === 'reconnecting'" class="hint">
|
|
16
|
+
The node socket is reconnecting. Anything the node reports meanwhile is lost.
|
|
17
|
+
</p>
|
|
18
|
+
</template>
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
/**
|
|
3
|
+
* Holds a step until the steps it builds on are done.
|
|
4
|
+
*
|
|
5
|
+
* A held step stays on the page, so what it does can be read ahead, but nothing in it can
|
|
6
|
+
* be used: `inert` takes every control out of reach, keyboard included. The note above it
|
|
7
|
+
* names the step to do first.
|
|
8
|
+
*/
|
|
9
|
+
defineProps<{
|
|
10
|
+
/** What has to happen first, or null once nothing does. */
|
|
11
|
+
waitingFor?: string | null
|
|
12
|
+
/** Whether this step's own work is done, which marks its number. */
|
|
13
|
+
done?: boolean
|
|
14
|
+
}>()
|
|
15
|
+
</script>
|
|
16
|
+
|
|
17
|
+
<template>
|
|
18
|
+
<div class="gate" :class="{ locked: !!waitingFor, done }">
|
|
19
|
+
<p v-if="waitingFor" class="gate-note" data-testid="step-locked">{{ waitingFor }}</p>
|
|
20
|
+
<div :inert="!!waitingFor">
|
|
21
|
+
<slot />
|
|
22
|
+
</div>
|
|
23
|
+
</div>
|
|
24
|
+
</template>
|