@meddleware/token-deployer-ui 0.0.10 → 0.0.11

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.
Files changed (87) hide show
  1. package/README.md +8 -5
  2. package/package.json +19 -8
  3. package/src/App.vue +1 -1
  4. package/src/component-styles.css +39 -57
  5. package/src/components/GithubPush.vue +2 -2
  6. package/src/components/IconPicker.vue +44 -75
  7. package/src/components/LicensePicker.vue +2 -2
  8. package/src/components/ResultPanel.vue +2 -2
  9. package/src/components/ReviewPanel.vue +11 -3
  10. package/src/components/TokenDeployerView.vue +52 -48
  11. package/src/components/WalletBar.vue +12 -1
  12. package/src/components/WalrusBlobBrowser.vue +4 -4
  13. package/src/composables/useWalrusRelay.ts +1 -2
  14. package/src/config.ts +19 -17
  15. package/src/lib/buildPublishTx.ts +0 -10
  16. package/src/lib/deploy.ts +9 -3
  17. package/src/lib/deployExecutor.ts +18 -45
  18. package/src/lib/template.ts +3 -0
  19. package/src/lib/walrus-constants.ts +2 -2
  20. package/src/main.ts +4 -6
  21. package/src/styles.css +22 -0
  22. package/.claude/move-language-reference/SKILL.md +0 -445
  23. package/.claude/skills/move-language-reference/SKILL.md +0 -445
  24. package/.claude/skills/sui-move-bootcamp/SKILL.md +0 -128
  25. package/.claude/sui-move-bootcamp/SKILL.md +0 -128
  26. package/.editorconfig +0 -8
  27. package/.env.example +0 -72
  28. package/.gitattributes +0 -1
  29. package/.github/workflows/docker-publish.yml +0 -348
  30. package/.github/workflows/e2e-realchain.yml +0 -64
  31. package/.github/workflows/node-ci.yml +0 -43
  32. package/.github/workflows/npm-publish.yml +0 -83
  33. package/.htmlvalidate.mjs +0 -30
  34. package/.oxfmtrc.json +0 -5
  35. package/.oxlintrc.json +0 -10
  36. package/.vscode/extensions.json +0 -9
  37. package/AGENTS.md +0 -39
  38. package/CLAUDE.md +0 -166
  39. package/Dockerfile +0 -96
  40. package/eslint.config.ts +0 -63
  41. package/index.html +0 -22
  42. package/playwright.config.ts +0 -30
  43. package/postcss.config.js +0 -8
  44. package/public/CNAME +0 -1
  45. package/public/_headers +0 -12
  46. package/public/_redirects +0 -1
  47. package/public/favicon.ico +0 -0
  48. package/public/favicon.svg +0 -5
  49. package/public/og-image.png +0 -0
  50. package/public/robots.txt +0 -3
  51. package/public/sitemap.xml +0 -8
  52. package/scripts/e2e-browser.mjs +0 -15
  53. package/scripts/e2e-deploy.mjs +0 -227
  54. package/scripts/e2e-localnet.mjs +0 -67
  55. package/scripts/e2e-walrus-browser.mjs +0 -112
  56. package/scripts/gen-og-image.mjs +0 -31
  57. package/scripts/publish-localnet.mjs +0 -56
  58. package/scripts/regen-template.sh +0 -45
  59. package/scripts/sync-template-src.sh +0 -36
  60. package/scripts/verify-bytecode.mjs +0 -49
  61. package/src/__tests__/App.spec.js +0 -11
  62. package/src/composables/useAccessGate.ts +0 -127
  63. package/src/lib/accessGate.ts +0 -155
  64. package/src/lib/walrus.ts +0 -155
  65. package/stylelint.config.js +0 -33
  66. package/test-results/.last-run.json +0 -4
  67. package/tests/accessGate.test.ts +0 -113
  68. package/tests/buildPublishTx.test.ts +0 -243
  69. package/tests/deploy.test.ts +0 -149
  70. package/tests/errors.test.ts +0 -32
  71. package/tests/generatePackage.test.ts +0 -193
  72. package/tests/github.test.ts +0 -61
  73. package/tests/licenses.test.ts +0 -81
  74. package/tests/listMyTokens.test.ts +0 -30
  75. package/tests/template.test.ts +0 -93
  76. package/tests/templateArtifact.test.ts +0 -61
  77. package/tests/templateParity.test.ts +0 -55
  78. package/tests/useAccessGate.test.ts +0 -97
  79. package/tests/useWallet.test.ts +0 -74
  80. package/tests/useWalrusRelayGating.test.ts +0 -30
  81. package/tests/validation.test.ts +0 -117
  82. package/tests/walrusUpload.test.ts +0 -93
  83. package/tests/walrus_url_patch.test.ts +0 -26
  84. package/tsconfig.json +0 -19
  85. package/typedoc.json +0 -34
  86. package/vite.config.ts +0 -73
  87. package/vitest.config.ts +0 -10
@@ -191,57 +191,61 @@ function restart(): void {
191
191
  </script>
192
192
 
193
193
  <template>
194
- <UiToolIntro
195
- >Deploy your own Sui coin — your wallet signs and pays; no intermediaries ({{
196
- network
197
- }}).</UiToolIntro
198
- >
199
-
200
- <AppTabNav
201
- v-model="activeTab"
202
- :tabs="TABS"
203
- id-prefix="token-deployer"
204
- aria-label="Sections"
205
- class="deployer-tabs"
206
- />
207
-
208
- <WalletGuard message="Connect a Sui wallet to deploy your token.">
194
+ <div class="mw-token-deployer">
195
+ <UiToolIntro
196
+ >Deploy your own Sui coin — your wallet signs and pays; no intermediaries ({{
197
+ network
198
+ }}).</UiToolIntro
199
+ >
200
+
201
+ <AppTabNav
202
+ v-model="activeTab"
203
+ :tabs="TABS"
204
+ id-prefix="token-deployer"
205
+ aria-label="Sections"
206
+ class="deployer-tabs"
207
+ />
208
+
209
+ <!-- The tab list and its panel always render (the selected tab controls a live panel); the wallet
210
+ prompt replaces only the panel's content until a wallet is connected. -->
209
211
  <UiTabPanel id-prefix="token-deployer" :tab="activeTab">
210
- <template v-if="activeTab === 'deploy'">
211
- <UiStepper :steps="STEPS" :model-value="stepIndex" @update:model-value="onStepperBack" />
212
- <ConfigForm
213
- v-if="step === 'identity' || step === 'token' || step === 'settings'"
214
- :form="form"
215
- :errors="errors"
216
- :can-proceed="canProceed"
217
- :network="network"
218
- :connected="connected"
219
- :form-step="formStep"
220
- @next="goNext"
221
- @back="goBack"
222
- />
223
- <ReviewPanel
224
- v-if="step === 'review'"
225
- :config="config"
226
- :network="network"
227
- @back="goBack"
228
- @confirm="confirmDeploy"
229
- />
230
- <DeployProgress v-if="step === 'deploying'" :step="deployStep" />
231
- <ResultPanel
232
- v-if="step === 'done' && result"
233
- :result="result"
234
- :config="config"
235
- @restart="restart"
236
- />
237
- <UiNotice v-if="deployError" type="error">{{ deployError }}</UiNotice>
238
- </template>
239
-
240
- <MyTokens v-else :owner="account?.address ?? null" :network="network" />
212
+ <WalletGuard message="Connect a Sui wallet to deploy your token.">
213
+ <template v-if="activeTab === 'deploy'">
214
+ <UiStepper :steps="STEPS" :model-value="stepIndex" @update:model-value="onStepperBack" />
215
+ <ConfigForm
216
+ v-if="step === 'identity' || step === 'token' || step === 'settings'"
217
+ :form="form"
218
+ :errors="errors"
219
+ :can-proceed="canProceed"
220
+ :network="network"
221
+ :connected="connected"
222
+ :form-step="formStep"
223
+ @next="goNext"
224
+ @back="goBack"
225
+ />
226
+ <ReviewPanel
227
+ v-if="step === 'review'"
228
+ :config="config"
229
+ :network="network"
230
+ @back="goBack"
231
+ @confirm="confirmDeploy"
232
+ />
233
+ <DeployProgress v-if="step === 'deploying'" :step="deployStep" />
234
+ <ResultPanel
235
+ v-if="step === 'done' && result"
236
+ :result="result"
237
+ :config="config"
238
+ @restart="restart"
239
+ />
240
+ <UiNotice v-if="deployError" type="error">{{ deployError }}</UiNotice>
241
+ </template>
242
+
243
+ <MyTokens v-else :owner="account?.address ?? null" :network="network" />
244
+ </WalletGuard>
241
245
  </UiTabPanel>
242
- </WalletGuard>
243
246
 
244
- <TermsDialog :open="showTerms" @accept="onTermsAccept" @cancel="onTermsCancel" />
247
+ <TermsDialog :open="showTerms" @accept="onTermsAccept" @cancel="onTermsCancel" />
248
+ </div>
245
249
  </template>
246
250
 
247
251
  <style scoped>
@@ -3,6 +3,17 @@ import { computed, ref, watch } from 'vue'
3
3
  import { useWallet, useNetwork } from '@meddleware/wallet-adapter'
4
4
  import { SELECTABLE_NETWORKS } from '../config.js'
5
5
  import type { Network } from '../lib/types.js'
6
+
7
+ /** Wallet-supplied icons are untrusted: render only inline images or https URLs. */
8
+ function safeIcon(icon: string | undefined): string | undefined {
9
+ if (!icon) return undefined
10
+ if (/^data:image\/(png|svg\+xml|jpeg|webp|gif);/i.test(icon)) return icon
11
+ try {
12
+ return new URL(icon).protocol === 'https:' ? icon : undefined
13
+ } catch {
14
+ return undefined
15
+ }
16
+ }
6
17
  import { UiDialog, UiFormField, UiNotice, UiSelect } from '@meddleware/ui'
7
18
 
8
19
  const { wallets, currentWallet, account, connecting, error, connect, disconnect } = useWallet()
@@ -66,7 +77,7 @@ watch(
66
77
  <menu class="wallet-list">
67
78
  <li v-for="w in wallets" :key="w.name">
68
79
  <button type="button" class="primary" :disabled="connecting" @click="connect(w)">
69
- <img v-if="w.icon" :src="w.icon" alt="" width="20" height="20" />
80
+ <img v-if="safeIcon(w.icon)" :src="safeIcon(w.icon)" alt="" width="20" height="20" />
70
81
  {{ w.name }}
71
82
  </button>
72
83
  </li>
@@ -2,7 +2,8 @@
2
2
  import { ref, watch } from 'vue'
3
3
  import { useWallet } from '@meddleware/wallet-adapter'
4
4
  import { getReadClient } from '../lib/readClient.js'
5
- import type { OwnedBlob, WalrusNetwork } from '../lib/walrus.js'
5
+ import type { OwnedBlob } from '@meddleware/walrus-client'
6
+ import { WALRUS_RPC_URLS, type WalrusNetwork } from '../config.js'
6
7
  import type { Network } from '../lib/types.js'
7
8
  import { UiNotice } from '@meddleware/ui'
8
9
 
@@ -46,10 +47,9 @@ async function load(): Promise<void> {
46
47
  blobs.value = []
47
48
  hasLoaded.value = false
48
49
  try {
49
- const { createWalrusClient, walrusBlobUrl, fetchOwnedWalrusBlobs } =
50
- await import('../lib/walrus.js')
50
+ const { createWalrusClient, walrusBlobUrl, fetchOwnedWalrusBlobs } = await import('@meddleware/walrus-client')
51
51
  const suiClient = getReadClient(props.network as Network)
52
- const client = createWalrusClient(props.network)
52
+ const client = createWalrusClient({ network: props.network, rpcUrl: WALRUS_RPC_URLS[props.network] })
53
53
  const raw = await fetchOwnedWalrusBlobs(suiClient, client, addr)
54
54
  blobs.value = raw.map((b) => ({ ...b, url: walrusBlobUrl(props.network, b.blobId) }))
55
55
  hasLoaded.value = true
@@ -4,8 +4,7 @@
4
4
  // (`IconPicker.vue`) and tests are unaffected.
5
5
  import { useWalrusRelay as useSharedWalrusRelay } from '@meddleware/walrus-relay'
6
6
  import type { RelayAccessOptions, RelayOption } from '@meddleware/walrus-relay'
7
- import { WALRUS_RELAY_HOSTS, PUBLIC_WALRUS_RELAY_HOSTS } from '../config.js'
8
- import type { WalrusNetwork } from '../lib/walrus.js'
7
+ import { WALRUS_RELAY_HOSTS, PUBLIC_WALRUS_RELAY_HOSTS, type WalrusNetwork } from '../config.js'
9
8
 
10
9
  export type { RelayAccessOptions, RelayOption }
11
10
 
package/src/config.ts CHANGED
@@ -2,7 +2,7 @@
2
2
  // Vite env vars so they can be set at build/deploy time without code changes.
3
3
  // See .env.example.
4
4
  import type { Network } from './lib/types.js'
5
- import type { AccessGateConfig } from './lib/accessGate.js'
5
+ import { relayGateConfig, type RelayGateConfig } from '@meddleware/walrus-relay'
6
6
 
7
7
  const env = (import.meta as unknown as { env?: Record<string, string | undefined> }).env ?? {}
8
8
 
@@ -56,7 +56,7 @@ export const SELECTABLE_NETWORKS: Network[] = ['testnet', 'mainnet']
56
56
  // ---------------------------------------------------------------------------
57
57
  // Walrus is a separate protocol from Sui; only testnet/mainnet exist (no
58
58
  // localnet). Icon uploads go through an upload relay (direct-to-storage-node
59
- // writes fail from browsers). See `src/lib/walrus.ts` and the relay runbook in
59
+ // writes fail from browsers). The upload is `@meddleware/walrus-client/flow`; see the relay runbook in
60
60
  // README.md / CLAUDE.md for the operator side.
61
61
 
62
62
  /** A Walrus network — a strict subset of {@link Network} (no localnet). */
@@ -113,27 +113,29 @@ export const WALRUS_MAX_TIP_MIST: bigint = BigInt(env.VITE_WALRUS_MAX_TIP_MIST |
113
113
  // NFT-gated relay access (optional)
114
114
  // ---------------------------------------------------------------------------
115
115
  // When the operator relay runs behind an `nft-gate` gateway, using it requires
116
- // holding the gate's access NFT. Configure the gate per network via env; when a
117
- // gate is NOT configured the relay behaves exactly as before (no gating). See
118
- // `src/lib/accessGate.ts` and `useAccessGate`.
116
+ // holding the gate's access NFT. The operator sets the gate id, soulbound flag (default true) and
117
+ // price per network via env; `relayGateConfig` (walrus-relay) fixes the package and PlatformConfig
118
+ // to the published access_gate deployment. When no gate id is set the relay is ungated.
119
119
 
120
- function parseAccessGate(network: WalrusNetwork): AccessGateConfig | null {
120
+ /** The gate config for a network, or `null` (ungated) without a gate id or a recorded deployment. */
121
+ export function parseAccessGate(network: WalrusNetwork, envSource: Record<string, string | undefined> = env): RelayGateConfig | null {
121
122
  const NET = network.toUpperCase()
122
- const packageId = env[`VITE_ACCESS_GATE_PACKAGE_${NET}`]
123
- const gateId = env[`VITE_ACCESS_GATE_ID_${NET}`]
124
- const nftType = env[`VITE_ACCESS_GATE_NFT_TYPE_${NET}`]
125
- if (!packageId || !gateId || !nftType) return null
126
- return {
127
- packageId,
128
- gateId,
129
- nftType,
130
- soulbound: (env[`VITE_ACCESS_GATE_SOULBOUND_${NET}`] ?? 'true') !== 'false',
131
- priceMist: BigInt(env[`VITE_ACCESS_GATE_PRICE_MIST_${NET}`] || '0'),
123
+ const gateId = envSource[`VITE_ACCESS_GATE_ID_${NET}`]
124
+ if (!gateId) return null
125
+ try {
126
+ return relayGateConfig(network, {
127
+ gateId,
128
+ soulbound: (envSource[`VITE_ACCESS_GATE_SOULBOUND_${NET}`] ?? 'true') !== 'false',
129
+ priceMist: BigInt(envSource[`VITE_ACCESS_GATE_PRICE_MIST_${NET}`] || '0'),
130
+ })
131
+ } catch (e) {
132
+ console.warn(`[token-deployer-ui] access gate for ${network} disabled: ${e instanceof Error ? e.message : String(e)}`)
133
+ return null
132
134
  }
133
135
  }
134
136
 
135
137
  /** The access gate per Walrus network, or `null` when gating is not configured. */
136
- export const ACCESS_GATE: Record<WalrusNetwork, AccessGateConfig | null> = {
138
+ export const ACCESS_GATE: Record<WalrusNetwork, RelayGateConfig | null> = {
137
139
  testnet: parseAccessGate('testnet'),
138
140
  mainnet: parseAccessGate('mainnet'),
139
141
  }
@@ -73,16 +73,6 @@ export interface BuildFinalizeArgs {
73
73
  gasBudget: bigint
74
74
  }
75
75
 
76
- /** Whether a finalize transaction is needed at all for this config. */
77
- export function needsFinalize(config: TokenConfig, sender: string): boolean {
78
- return (
79
- config.initialSupply > 0n ||
80
- config.supplyPolicy === 'fixed' ||
81
- config.metadataPolicy === 'frozen' ||
82
- config.recipient.toLowerCase() !== sender.toLowerCase()
83
- )
84
- }
85
-
86
76
  /**
87
77
  * Finalize PTB: register the coin in the registry, mint initial supply, apply
88
78
  * supply/metadata policy, and route caps to the recipient.
package/src/lib/deploy.ts CHANGED
@@ -87,10 +87,16 @@ export interface DeployArgs {
87
87
  onStep?: (step: DeployStep) => void
88
88
  }
89
89
 
90
+ /**
91
+ * Only an explicit `success` effects status counts: a missing status (an executor that did not
92
+ * return effects) is treated as failure, never as success.
93
+ */
90
94
  function assertSuccess(res: SuiTxResult, what: string): void {
91
95
  const status = res.effects?.status?.status
92
- if (status && status !== 'success') {
93
- throw new Error(`${what} failed: ${res.effects?.status?.error ?? status}`)
96
+ if (status !== 'success') {
97
+ throw new Error(
98
+ `${what} failed: ${res.effects?.status?.error ?? status ?? 'no effects status returned'}`,
99
+ )
94
100
  }
95
101
  }
96
102
 
@@ -103,7 +109,7 @@ export async function deployToken(args: DeployArgs): Promise<PublishResult> {
103
109
  const structName = deriveStructName(config.moduleName)
104
110
  const normalizedConfig: TokenConfig = { ...config, structName }
105
111
  const recipient = normalizedConfig.recipient || args.sender
106
- const isE2e = import.meta.env.VITE_E2E === '1'
112
+ const isE2e = import.meta.env.MODE === 'e2e' && import.meta.env.VITE_E2E === '1'
107
113
 
108
114
  onStep?.('patching')
109
115
  const moduleBytes = await patchTemplateModule({
@@ -1,63 +1,36 @@
1
- // Builds a deploy Executor using the wallet-adapter's shared signing capability
2
- // + the app's gRPC client for execution (returns the object changes required by
3
- // extractPublishResult).
4
- //
5
- // The wallet-adapter's own Executor only returns { digest } which is not sufficient
6
- // for the token deploy flow; we sign via the shared wallet connection but execute
7
- // via our own client so we can request effects + object types.
1
+ // Builds the deploy Executor from the wallet-adapter's shared executor, executing through this
2
+ // app's gRPC client (`getReadClient`, which E2E builds stub) and asking for the effects and
3
+ // object types `extractPublishResult` needs. Signing, the chain check and the account binding
4
+ // are the wallet-adapter's.
8
5
 
9
- import { fromBase64 } from '@mysten/sui/utils'
10
- import { useWallet } from '@meddleware/wallet-adapter'
6
+ import { buildExecutor } from '@meddleware/wallet-adapter'
11
7
  import type { Transaction } from '@mysten/sui/transactions'
12
8
  import { toSuiTxResult } from './deploy.js'
13
9
  import type { SuiTxResult, Executor } from './deploy.js'
14
10
  import type { Network } from './types.js'
15
11
  import { getReadClient } from './readClient.js'
12
+ import { RPC_URLS } from '../config.js'
16
13
 
17
14
  /**
18
- * Build a deploy Executor bound to the currently connected wallet (via the
19
- * wallet-adapter singleton) and the given network's gRPC client.
15
+ * Build a deploy Executor bound to the currently connected wallet and `network`.
20
16
  *
21
- * The returned executor signs via `sui:signTransaction` and executes via
22
- * `executeTransaction` with `effects + objectTypes`, mapped by {@link toSuiTxResult}
23
- * so the caller can extract the published package ID, coin type, TreasuryCap, etc.
17
+ * `signAndExecute` requests `effects + objectTypes` and maps the result with
18
+ * {@link toSuiTxResult}, so the caller can extract the published package id, coin type,
19
+ * TreasuryCap, MetadataCap and the Currency ref. A failed transaction is returned (not thrown);
20
+ * `deploy.ts` checks its status.
21
+ *
22
+ * @throws {Error} if no wallet is connected, it cannot sign transactions, or its account does not
23
+ * list `sui:<network>`.
24
24
  */
25
25
  export async function buildDeployExecutor(network: Network): Promise<Executor> {
26
- const { currentWallet, account } = useWallet()
27
- const wallet = currentWallet.value
28
- const acct = account.value
29
- if (!wallet || !acct) throw new Error('Connect a wallet first.')
30
-
31
- const suiClient = getReadClient(network)
32
- const chain = `sui:${network}` as const
33
-
34
- const signFeature = wallet.features['sui:signTransaction'] as
35
- | {
36
- signTransaction: (input: {
37
- transaction: Transaction
38
- account: typeof acct
39
- chain: `sui:${string}`
40
- }) => Promise<{ bytes: string; signature: string }>
41
- }
42
- | undefined
43
- if (!signFeature) throw new Error('This wallet cannot sign transactions.')
44
-
26
+ const executor = await buildExecutor(network, RPC_URLS[network], { client: getReadClient(network) })
45
27
  return {
46
28
  async signAndExecute(tx: Transaction): Promise<SuiTxResult> {
47
- const { bytes, signature } = await signFeature.signTransaction({
48
- transaction: tx,
49
- account: acct,
50
- chain,
51
- })
52
- const res = await suiClient.executeTransaction({
53
- transaction: fromBase64(bytes),
54
- signatures: [signature],
55
- include: { effects: true, objectTypes: true },
56
- })
57
- return toSuiTxResult(res)
29
+ const { result } = await executor.signAndExecute(tx, { include: { effects: true, objectTypes: true } })
30
+ return toSuiTxResult(result)
58
31
  },
59
32
  async waitForTransaction(digest: string): Promise<void> {
60
- await suiClient.waitForTransaction({ digest })
33
+ await executor.waitForTransaction(digest)
61
34
  },
62
35
  }
63
36
  }
@@ -38,6 +38,9 @@ export function initTemplateWasm(): Promise<unknown> {
38
38
  if (!initPromise) {
39
39
  initPromise = Promise.resolve()
40
40
  .then(() => (init as unknown as (i?: unknown) => unknown)(wasmInput))
41
+ // Deliberate: under Node the package's nodejs build loads its own wasm and this init call
42
+ // is unnecessary (and may throw). A genuine browser init failure is not hidden — the first
43
+ // bytecode call in patchTemplateModule then fails with the wasm error.
41
44
  .catch(() => undefined)
42
45
  }
43
46
  return initPromise
@@ -1,5 +1,5 @@
1
- // Lightweight Walrus constants with NO heavy imports, so both the (lazily loaded)
2
- // `lib/walrus.ts` client and the eagerly rendered `IconPicker.vue` component
1
+ // Lightweight Walrus constants with NO heavy imports, so the eagerly rendered `IconPicker.vue`
2
+ // component and the lazily loaded upload
3
3
  // can share a single source of truth without pulling `@mysten/walrus` into the
4
4
  // component's chunk.
5
5
 
package/src/main.ts CHANGED
@@ -8,7 +8,7 @@ import './component-styles.css'
8
8
  import { configureWasm } from './lib/template.js'
9
9
  // Static: readClient is already in the main chunk (IconPicker, WalrusBlobBrowser and listMyTokens
10
10
  // import it), so a dynamic import in the E2E block below could not split it out anyway.
11
- import { getReadClient, setReadClient } from './lib/readClient.js'
11
+ import { setReadClient } from './lib/readClient.js'
12
12
  import type { SuiGrpcClient } from '@mysten/sui/grpc'
13
13
  import { useColorMode, useSeason } from '@meddleware/ui'
14
14
 
@@ -28,9 +28,10 @@ configureWasm(wasmUrl)
28
28
 
29
29
  // E2E-only: stub the Sui client, register a mock wallet and expose getSuiClient for headless tests.
30
30
  // Wrapped in async IIFE to ensure wallet is registered BEFORE app mounts (timing critical).
31
- // Tree-shaking removes this entire block from production when VITE_E2E !== '1'.
31
+ // Only the `e2e` build mode may set VITE_E2E (vite.config.ts refuses otherwise); both are static,
32
+ // so every other build tree-shakes this entire block (verified by `npm run check:bundle`).
32
33
  ;(async () => {
33
- if (import.meta.env.VITE_E2E === '1') {
34
+ if (import.meta.env.MODE === 'e2e' && import.meta.env.VITE_E2E === '1') {
34
35
  const { getWallets } = await import('@mysten/wallet-standard')
35
36
 
36
37
  // Stub gRPC client in place of the network (gRPC-web is binary, so it cannot be faked
@@ -137,9 +138,6 @@ configureWasm(wasmUrl)
137
138
  unregister?.()
138
139
  unregister = undefined
139
140
  }
140
-
141
- // Expose getSuiClient for tests to build transactions
142
- w.__getSuiClient = getReadClient
143
141
  }
144
142
 
145
143
  createApp(App).mount('#app')
package/src/styles.css CHANGED
@@ -58,3 +58,25 @@ footer {
58
58
  padding-top: 1.5rem;
59
59
  margin-top: 2rem;
60
60
  }
61
+
62
+ /* Skip link (standalone app shell only — not part of the embeddable library styles). */
63
+ .skip-link {
64
+ position: absolute;
65
+ left: 0.5rem;
66
+ top: -3rem;
67
+ z-index: 100;
68
+ background: var(--accent);
69
+ color: #fff;
70
+ padding: 0.5rem 0.9rem;
71
+ border-radius: 6px;
72
+ text-decoration: none;
73
+ transition: top 0.15s ease;
74
+ }
75
+ .skip-link:focus {
76
+ top: 0.5rem;
77
+ }
78
+ @media (prefers-reduced-motion: reduce) {
79
+ .skip-link {
80
+ transition: none;
81
+ }
82
+ }