@meddleware/dashboard-ui 0.1.93 → 0.1.94
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/CHANGELOG.md +4 -0
- package/index.html +2 -1
- package/package.json +23 -16
- package/src/App.vue +117 -232
- package/src/chains.ts +14 -0
- package/src/components/ChainSelector.vue +8 -38
- package/src/components/NetworkSelector.vue +33 -83
- package/src/e2e.ts +8 -0
- package/src/main.ts +8 -9
- package/src/router/index.ts +3 -8
- package/src/views/BlockchainView.vue +18 -40
- package/vite.config.ts +15 -9
- package/src/styles.css +0 -17
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,10 @@ Format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/); version
|
|
|
5
5
|
[Semantic Versioning](https://semver.org/). Before 0.2.0, breaking changes ship as patch releases.
|
|
6
6
|
Releases before 0.1.85 are recorded in the git history only.
|
|
7
7
|
|
|
8
|
+
## [0.1.94] - 2026-11-01
|
|
9
|
+
|
|
10
|
+
- Shared shell, sidebar drawer on phones, tools updated, accessibility gates
|
|
11
|
+
|
|
8
12
|
## [0.1.93] - 2026-10-30
|
|
9
13
|
|
|
10
14
|
- Dependency updates (minor and patch groups) and workflow action bumps. No source change.
|
package/index.html
CHANGED
|
@@ -5,9 +5,10 @@
|
|
|
5
5
|
<link rel="icon" type="image/svg+xml" href="/favicon.svg">
|
|
6
6
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
7
7
|
<title>Meddleware</title>
|
|
8
|
+
<meta name="description" content="The Meddleware dashboard: the organisation's treasury and the Sui tools (Walrus storage, sealed storage, access gates, token issuance) in one place." />
|
|
8
9
|
</head>
|
|
9
10
|
<body>
|
|
10
|
-
<div id="app"></div>
|
|
11
|
+
<div id="app"><p role="status">Loading the Meddleware dashboard…</p></div>
|
|
11
12
|
<script type="module" src="/src/main.ts"></script>
|
|
12
13
|
</body>
|
|
13
14
|
</html>
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@meddleware/dashboard-ui",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.94",
|
|
4
4
|
"homepage": "https://docs.meddleware.co.uk/",
|
|
5
5
|
"description": "Meddleware tools hub — Vue 3 SPA listing hosted services (Walrus, Access Gate) with white-label deploy options.",
|
|
6
6
|
"author": "Meddleware <dev@meddleware.co.uk>",
|
|
@@ -20,6 +20,7 @@
|
|
|
20
20
|
"scripts": {
|
|
21
21
|
"dev": "vite",
|
|
22
22
|
"build": "vue-tsc --noEmit && vite build",
|
|
23
|
+
"build:e2e": "VITE_E2E=1 vite build --mode e2e",
|
|
23
24
|
"preview": "vite preview",
|
|
24
25
|
"type-check": "vue-tsc --noEmit",
|
|
25
26
|
"test": "vitest run",
|
|
@@ -31,27 +32,30 @@
|
|
|
31
32
|
"lint:html": "html-validate \"src/**/*.vue\"",
|
|
32
33
|
"licenses": "node scripts/third-party-licenses.mjs",
|
|
33
34
|
"check:licenses": "node scripts/third-party-licenses.mjs --check",
|
|
34
|
-
"check:dist": "node scripts/check-inline-scripts.mjs"
|
|
35
|
+
"check:dist": "node scripts/check-inline-scripts.mjs",
|
|
36
|
+
"shots": "SHOTS=1 playwright test",
|
|
37
|
+
"lhci": "npm run build && lhci autorun"
|
|
35
38
|
},
|
|
36
39
|
"publishConfig": {
|
|
37
40
|
"access": "public"
|
|
38
41
|
},
|
|
39
42
|
"dependencies": {
|
|
40
|
-
"@meddleware/access-gate-ui": "^0.1.
|
|
41
|
-
"@meddleware/
|
|
42
|
-
"@meddleware/
|
|
43
|
-
"@meddleware/
|
|
44
|
-
"@meddleware/
|
|
45
|
-
"@meddleware/
|
|
46
|
-
"@meddleware/
|
|
47
|
-
"@meddleware/
|
|
48
|
-
"@meddleware/
|
|
43
|
+
"@meddleware/access-gate-ui": "^0.1.44",
|
|
44
|
+
"@meddleware/design-tokens": "^0.1.12",
|
|
45
|
+
"@meddleware/sealed-storage-ui": "^0.0.45",
|
|
46
|
+
"@meddleware/token-issuance-ui": "^0.0.43",
|
|
47
|
+
"@meddleware/treasury-ui": "^0.0.24",
|
|
48
|
+
"@meddleware/ui": "^0.1.37",
|
|
49
|
+
"@meddleware/wallet-adapter": "^0.0.20",
|
|
50
|
+
"@meddleware/walrus-storage-ui": "^0.1.66",
|
|
51
|
+
"@meddleware/web-utils": "^0.0.1",
|
|
49
52
|
"@mysten/sui": "^2.36.0",
|
|
50
53
|
"vue": "^3.5.44",
|
|
51
54
|
"vue-router": "^5.4.0"
|
|
52
55
|
},
|
|
53
56
|
"devDependencies": {
|
|
54
|
-
"@
|
|
57
|
+
"@lhci/cli": "^0.15.1",
|
|
58
|
+
"@meddleware/frontend-config": "^0.0.1",
|
|
55
59
|
"@playwright/test": "~1.64.1",
|
|
56
60
|
"@types/node": "~26.6.5",
|
|
57
61
|
"@vitejs/plugin-vue": "^6.0.10",
|
|
@@ -79,8 +83,11 @@
|
|
|
79
83
|
"node": "^22.18.0 || >=24.12.0"
|
|
80
84
|
},
|
|
81
85
|
"browserslist": [
|
|
82
|
-
"
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
+
"extends @meddleware/frontend-config/browserslist-config"
|
|
87
|
+
],
|
|
88
|
+
"overrides": {
|
|
89
|
+
"basic-ftp": "^6.2.4",
|
|
90
|
+
"tmp": "^0.2.7",
|
|
91
|
+
"zod": "^4.6.7"
|
|
92
|
+
}
|
|
86
93
|
}
|
package/src/App.vue
CHANGED
|
@@ -1,54 +1,41 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
|
+
// The dashboard shell: header, sidebar (a drawer on a phone), the routed tool, footer. The shell hosts the one shared wallet connection and
|
|
3
|
+
// the network choice; every tool view renders inline from its own package and reads them from the @meddleware/wallet-adapter singleton.
|
|
2
4
|
import { computed, ref, watch } from 'vue'
|
|
3
|
-
import {
|
|
5
|
+
import { RouterLink, RouterView, useRoute } from 'vue-router'
|
|
4
6
|
import {
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
7
|
+
AppFooter,
|
|
8
|
+
AppHeader,
|
|
9
|
+
AppSidebar,
|
|
10
|
+
ColorModeControl,
|
|
11
|
+
CopyableAddress,
|
|
12
|
+
CopyrightLine,
|
|
13
|
+
ExplorerLink,
|
|
14
|
+
SidebarGroup,
|
|
15
|
+
SidebarItem,
|
|
16
|
+
StatusWidget,
|
|
17
|
+
UiAppShell,
|
|
18
|
+
UiButton,
|
|
19
|
+
UiStatusDot,
|
|
20
|
+
useColorMode,
|
|
8
21
|
} from '@meddleware/ui'
|
|
9
|
-
import {
|
|
10
|
-
import
|
|
22
|
+
import { suiExplorerUrl } from '@meddleware/web-utils'
|
|
23
|
+
import { useNetwork, useWallet } from '@meddleware/wallet-adapter'
|
|
24
|
+
import { DEFAULT_CHAIN } from './chains.js'
|
|
11
25
|
import ChainSelector from './components/ChainSelector.vue'
|
|
26
|
+
import NetworkSelector from './components/NetworkSelector.vue'
|
|
12
27
|
|
|
13
28
|
const { mode } = useColorMode('dark')
|
|
14
29
|
const route = useRoute()
|
|
15
|
-
const router = useRouter()
|
|
16
30
|
const { network } = useNetwork()
|
|
17
31
|
|
|
18
|
-
// The sidebar has two modes. Top
|
|
19
|
-
//
|
|
20
|
-
//
|
|
32
|
+
// The sidebar has two modes. Top level shows the organisation sections (Treasury, Blockchain). Entering the Blockchain section swaps it to a
|
|
33
|
+
// chain selector with an up-one-level control that returns to the top level without leaving the page. The mode follows the route (so a
|
|
34
|
+
// direct load, a reload or a back-forward lands right); `upOneLevel` sets 'top' without a route change, and that override stands until the
|
|
35
|
+
// next navigation.
|
|
21
36
|
type SidebarView = 'top' | 'blockchain'
|
|
22
|
-
// Derived from the route below (immediate watcher) so a direct load / reload of /blockchain
|
|
23
|
-
// shows the chain selector, not the top-level menu.
|
|
24
37
|
const sidebarView = ref<SidebarView>('top')
|
|
25
|
-
const selectedChain = ref(
|
|
26
|
-
|
|
27
|
-
const isTreasury = computed(() => route.path === '/')
|
|
28
|
-
const isBlockchain = computed(() => route.path === '/blockchain')
|
|
29
|
-
|
|
30
|
-
function goTreasury(navigate?: () => void): void {
|
|
31
|
-
sidebarView.value = 'top'
|
|
32
|
-
if (navigate) navigate()
|
|
33
|
-
else router.push('/')
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
function enterBlockchain(): void {
|
|
37
|
-
// No-op navigation if already on /blockchain (vue-router won't reload the same route);
|
|
38
|
-
// either way the sidebar switches to the chain selector.
|
|
39
|
-
sidebarView.value = 'blockchain'
|
|
40
|
-
if (route.path !== '/blockchain') router.push('/blockchain')
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
function upOneLevel(): void {
|
|
44
|
-
// Return the sidebar to the top level without navigating away from the current page.
|
|
45
|
-
sidebarView.value = 'top'
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
// Derive the sidebar mode from the route so it's correct on direct load / reload / redirect /
|
|
49
|
-
// back-forward — /blockchain always opens the chain selector. `upOneLevel()` sets 'top' without a
|
|
50
|
-
// route change, so this watcher (which fires only on path changes) leaves that transient override
|
|
51
|
-
// intact until the next navigation.
|
|
38
|
+
const selectedChain = ref(DEFAULT_CHAIN)
|
|
52
39
|
watch(
|
|
53
40
|
() => route.path,
|
|
54
41
|
(p) => {
|
|
@@ -56,222 +43,120 @@ watch(
|
|
|
56
43
|
},
|
|
57
44
|
{ immediate: true },
|
|
58
45
|
)
|
|
46
|
+
const isTreasury = computed(() => route.path === '/')
|
|
47
|
+
const isBlockchain = computed(() => route.path === '/blockchain')
|
|
48
|
+
const upOneLevel = (): void => {
|
|
49
|
+
sidebarView.value = 'top'
|
|
50
|
+
}
|
|
51
|
+
// Already on /blockchain: the link does not navigate, but the section still opens its chain selector.
|
|
52
|
+
const openBlockchain = (): void => {
|
|
53
|
+
sidebarView.value = 'blockchain'
|
|
54
|
+
}
|
|
59
55
|
|
|
60
|
-
// The dashboard hosts the single shared wallet connection for every inline tool view. It
|
|
61
|
-
//
|
|
62
|
-
|
|
63
|
-
const { account, disconnect } = useWallet({
|
|
64
|
-
requiredFeatures: ['sui:signTransaction', 'sui:signPersonalMessage'],
|
|
65
|
-
})
|
|
56
|
+
// The dashboard hosts the single shared wallet connection for every inline tool view. It requests the superset of features the tools need so
|
|
57
|
+
// the connect control only offers wallets capable of every operation; individual tools still feature-guard at call time.
|
|
58
|
+
const { account, disconnect } = useWallet({ requiredFeatures: ['sui:signTransaction', 'sui:signPersonalMessage'] })
|
|
66
59
|
|
|
67
60
|
const DOCS_URL = import.meta.env.VITE_DOCS_URL || 'https://docs.meddleware.co.uk/'
|
|
68
61
|
const DEV_URL = import.meta.env.VITE_DEV_URL || 'https://meddleware.dev/'
|
|
69
62
|
|
|
70
|
-
// SuiVision has no localnet explorer, so
|
|
71
|
-
|
|
72
|
-
const
|
|
73
|
-
account.value && network.value !== 'localnet'
|
|
74
|
-
? suiExplorerUrl('account', account.value.address, network.value as 'testnet' | 'mainnet' | 'devnet')
|
|
75
|
-
: null,
|
|
76
|
-
)
|
|
63
|
+
// SuiVision has no localnet explorer, so a link exists only for public networks; otherwise the address is copy-only.
|
|
64
|
+
const accountExplorerNetwork = computed(() => (network.value === 'mainnet' ? 'mainnet' : network.value === 'testnet' ? 'testnet' : null))
|
|
65
|
+
const title = computed(() => (typeof route.meta.title === 'string' ? route.meta.title : 'Meddleware'))
|
|
77
66
|
</script>
|
|
78
67
|
|
|
79
68
|
<template>
|
|
80
|
-
<
|
|
81
|
-
<
|
|
82
|
-
<
|
|
83
|
-
<
|
|
84
|
-
<span class="brand-mark" aria-hidden="true">◆</span>
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
69
|
+
<UiAppShell>
|
|
70
|
+
<template #header>
|
|
71
|
+
<AppHeader variant="dark" sidebar-id="main-nav" menu-label="Open the menu">
|
|
72
|
+
<template #brand>
|
|
73
|
+
<RouterLink to="/" class="brand-link"><span class="brand-mark" aria-hidden="true">◆</span> Meddleware</RouterLink>
|
|
74
|
+
</template>
|
|
75
|
+
<template #actions>
|
|
76
|
+
<ColorModeControl v-model="mode" />
|
|
77
|
+
</template>
|
|
78
|
+
</AppHeader>
|
|
79
|
+
</template>
|
|
80
|
+
|
|
81
|
+
<template #sidebar>
|
|
82
|
+
<AppSidebar id="main-nav" variant="dark" nav-label="Dashboard">
|
|
83
|
+
<!-- Top level: organisation sections -->
|
|
84
|
+
<template v-if="sidebarView === 'top'">
|
|
85
|
+
<SidebarGroup label="Organisation" :level="1">
|
|
86
|
+
<RouterLink v-slot="{ href, navigate }" to="/" custom>
|
|
87
|
+
<SidebarItem label="Treasury" icon="🏦" :href="href" :active="isTreasury" @click="navigate" />
|
|
88
|
+
</RouterLink>
|
|
89
|
+
<RouterLink v-slot="{ href, navigate }" to="/blockchain" custom>
|
|
90
|
+
<SidebarItem
|
|
91
|
+
label="Blockchain"
|
|
92
|
+
icon="⛓"
|
|
93
|
+
:href="href"
|
|
94
|
+
:active="isBlockchain"
|
|
95
|
+
@click="(e: MouseEvent) => { navigate(e); openBlockchain() }"
|
|
96
|
+
/>
|
|
97
|
+
</RouterLink>
|
|
98
|
+
</SidebarGroup>
|
|
99
|
+
</template>
|
|
100
|
+
|
|
101
|
+
<!-- Blockchain section: up-one-level + the chain selector -->
|
|
102
|
+
<template v-else>
|
|
103
|
+
<SidebarItem label="Back" icon="←" @click="upOneLevel" />
|
|
104
|
+
<ChainSelector v-model="selectedChain" />
|
|
105
|
+
</template>
|
|
106
|
+
|
|
107
|
+
<template #body>
|
|
108
|
+
<NetworkSelector v-if="account" />
|
|
109
|
+
</template>
|
|
110
|
+
|
|
111
|
+
<template #foot>
|
|
112
|
+
<p v-if="account" class="foot">
|
|
113
|
+
<CopyableAddress :address="account.address">
|
|
114
|
+
<ExplorerLink v-if="accountExplorerNetwork" :href="suiExplorerUrl('account', account.address, accountExplorerNetwork)" :value="account.address" />
|
|
115
|
+
</CopyableAddress>
|
|
116
|
+
<UiButton variant="link" size="sm" @click="disconnect">Disconnect</UiButton>
|
|
117
|
+
</p>
|
|
118
|
+
<p v-else class="foot"><UiStatusDot status="unknown" size="sm" /> No wallet connected</p>
|
|
119
|
+
</template>
|
|
120
|
+
</AppSidebar>
|
|
121
|
+
</template>
|
|
122
|
+
|
|
123
|
+
<h1 class="mw-visually-hidden">{{ title }}</h1>
|
|
124
|
+
<RouterView v-slot="{ Component, route: current }">
|
|
125
|
+
<component :is="Component" :chain="current.name === 'blockchain' ? selectedChain : undefined" />
|
|
126
|
+
</RouterView>
|
|
127
|
+
|
|
128
|
+
<template #footer>
|
|
129
|
+
<AppFooter variant="dark" :docs-url="DOCS_URL" :dev-url="DEV_URL">
|
|
130
|
+
<template #start>
|
|
131
|
+
<CopyrightLine symbol-variant="kopimi" organisation-name="Meddleware" rights-statement="jam" />
|
|
132
|
+
</template>
|
|
133
|
+
<StatusWidget />
|
|
134
|
+
</AppFooter>
|
|
135
|
+
</template>
|
|
136
|
+
</UiAppShell>
|
|
138
137
|
</template>
|
|
139
138
|
|
|
140
139
|
<style scoped>
|
|
141
|
-
.app-shell {
|
|
142
|
-
display: grid;
|
|
143
|
-
grid-template-columns: var(--mw-sidebar-width, 240px) 1fr;
|
|
144
|
-
grid-template-rows: var(--mw-header-height, 56px) 1fr auto;
|
|
145
|
-
height: 100dvh;
|
|
146
|
-
overflow: hidden;
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
.app-shell__header {
|
|
150
|
-
grid-column: 1 / -1;
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
.app-shell__footer {
|
|
154
|
-
grid-column: 1 / -1;
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
.footer-status {
|
|
158
|
-
font-size: var(--font-size-sm);
|
|
159
|
-
}
|
|
160
|
-
|
|
161
|
-
.app-shell__sidebar {
|
|
162
|
-
min-height: 0;
|
|
163
|
-
overflow: hidden;
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
.app-shell__main {
|
|
167
|
-
display: flex;
|
|
168
|
-
flex-direction: column;
|
|
169
|
-
overflow-y: auto;
|
|
170
|
-
min-height: 0;
|
|
171
|
-
}
|
|
172
|
-
|
|
173
|
-
@media (width <=720px) {
|
|
174
|
-
.app-shell {
|
|
175
|
-
grid-template-columns: 1fr;
|
|
176
|
-
grid-template-rows: var(--mw-header-height, 56px) 1fr auto;
|
|
177
|
-
}
|
|
178
|
-
|
|
179
|
-
.app-shell__sidebar {
|
|
180
|
-
display: none;
|
|
181
|
-
}
|
|
182
|
-
}
|
|
183
|
-
|
|
184
140
|
.brand-link {
|
|
185
141
|
display: flex;
|
|
186
142
|
align-items: center;
|
|
187
|
-
gap:
|
|
143
|
+
gap: var(--space-2xs);
|
|
188
144
|
color: inherit;
|
|
189
145
|
text-decoration: none;
|
|
190
146
|
}
|
|
191
|
-
|
|
192
147
|
.brand-mark {
|
|
193
148
|
color: var(--accent);
|
|
194
149
|
}
|
|
195
|
-
|
|
196
|
-
/* ── Up-one-level control ──────────────────────────────── */
|
|
197
|
-
.sidebar-up {
|
|
198
|
-
display: inline-flex;
|
|
199
|
-
align-items: center;
|
|
200
|
-
gap: 0.35rem;
|
|
201
|
-
background: none;
|
|
202
|
-
border: none;
|
|
203
|
-
/* Left padding matches SidebarItem so "← Back" aligns with the items below. */
|
|
204
|
-
padding: 0.2rem var(--space-sm);
|
|
205
|
-
margin-bottom: 0.35rem;
|
|
206
|
-
color: var(--muted);
|
|
207
|
-
font-size: 0.8rem;
|
|
208
|
-
cursor: pointer;
|
|
209
|
-
border-radius: 0;
|
|
210
|
-
}
|
|
211
|
-
|
|
212
|
-
.sidebar-up:hover {
|
|
213
|
-
color: var(--text);
|
|
214
|
-
}
|
|
215
|
-
|
|
216
|
-
.sidebar-up:focus-visible {
|
|
217
|
-
outline: 2px solid var(--focus-ring);
|
|
218
|
-
outline-offset: 2px;
|
|
219
|
-
}
|
|
220
|
-
|
|
221
|
-
/* ── Sidebar body/foot ──────────────────────────────────
|
|
222
|
-
AppSidebar renders these slots unwrapped, so the shell styles its own content. */
|
|
223
|
-
.sidebar-body {
|
|
224
|
-
flex-shrink: 0;
|
|
225
|
-
}
|
|
226
|
-
|
|
227
|
-
.sidebar-foot {
|
|
228
|
-
flex-shrink: 0;
|
|
229
|
-
margin: 0;
|
|
230
|
-
/* Extra bottom space keeps the wallet control clear of the shell footer's border. */
|
|
231
|
-
padding: var(--space-xs) var(--space-sm) calc(var(--space-xs) + 0.5rem);
|
|
232
|
-
border-top: 1px solid var(--_border);
|
|
233
|
-
color: var(--_muted);
|
|
234
|
-
font-size: var(--font-size-sm);
|
|
235
|
-
}
|
|
236
|
-
|
|
237
|
-
.wallet-connected {
|
|
150
|
+
.foot {
|
|
238
151
|
display: flex;
|
|
152
|
+
flex-wrap: wrap;
|
|
239
153
|
align-items: center;
|
|
240
154
|
justify-content: space-between;
|
|
241
|
-
gap:
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
background: none;
|
|
246
|
-
border: none;
|
|
247
|
-
padding: 0;
|
|
248
|
-
color: var(--muted);
|
|
249
|
-
font-size: 0.8rem;
|
|
250
|
-
cursor: pointer;
|
|
251
|
-
white-space: nowrap;
|
|
252
|
-
text-decoration: underline;
|
|
253
|
-
text-underline-offset: 2px;
|
|
254
|
-
}
|
|
255
|
-
|
|
256
|
-
.wallet-disconnect:hover {
|
|
257
|
-
color: var(--text);
|
|
258
|
-
}
|
|
259
|
-
|
|
260
|
-
.wallet-disconnected {
|
|
261
|
-
display: flex;
|
|
262
|
-
align-items: center;
|
|
263
|
-
gap: 0.45rem;
|
|
264
|
-
font-size: 0.8rem;
|
|
155
|
+
gap: var(--space-2xs);
|
|
156
|
+
margin: 0;
|
|
157
|
+
padding: var(--space-xs) var(--space-sm);
|
|
158
|
+
border-top: 1px solid var(--border);
|
|
265
159
|
color: var(--muted);
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
/* Decorative status dot (the text states the status). */
|
|
269
|
-
.wallet-disconnected::before {
|
|
270
|
-
content: '';
|
|
271
|
-
width: 6px;
|
|
272
|
-
height: 6px;
|
|
273
|
-
border-radius: 50%;
|
|
274
|
-
background: var(--muted, #666);
|
|
275
|
-
flex-shrink: 0;
|
|
160
|
+
font-size: var(--font-size-sm);
|
|
276
161
|
}
|
|
277
162
|
</style>
|
package/src/chains.ts
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
// The chains the Blockchain section offers. Data-driven: onboarding a chain is a new entry here plus a tool set in `views/BlockchainView.vue`.
|
|
2
|
+
export interface Chain {
|
|
3
|
+
id: string
|
|
4
|
+
label: string
|
|
5
|
+
/** A decorative glyph (hidden from assistive technology). */
|
|
6
|
+
icon: string
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export const CHAINS: readonly Chain[] = [
|
|
10
|
+
{ id: 'sui', label: 'Sui', icon: '🌊' },
|
|
11
|
+
// Future chains slot in here — the Blockchain view swaps tool sets by the selected id.
|
|
12
|
+
]
|
|
13
|
+
|
|
14
|
+
export const DEFAULT_CHAIN = CHAINS[0].id
|
|
@@ -1,44 +1,14 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
|
-
//
|
|
3
|
-
//
|
|
4
|
-
import { SidebarItem } from '@meddleware/ui'
|
|
2
|
+
// The chain choice shown in the sidebar while the Blockchain section is open: one entry per chain in `CHAINS`, the chosen one marked as
|
|
3
|
+
// the current item. With a single chain it still names where the tools below belong.
|
|
4
|
+
import { SidebarGroup, SidebarItem } from '@meddleware/ui'
|
|
5
|
+
import { CHAINS, DEFAULT_CHAIN } from '../chains.js'
|
|
5
6
|
|
|
6
|
-
|
|
7
|
-
id: string
|
|
8
|
-
label: string
|
|
9
|
-
icon: string
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
const CHAINS: Chain[] = [
|
|
13
|
-
{ id: 'sui', label: 'Sui', icon: '🌊' },
|
|
14
|
-
// Future chains slot in here — the Blockchain view swaps tool sets by the selected id.
|
|
15
|
-
]
|
|
16
|
-
|
|
17
|
-
const model = defineModel<string>({ default: 'sui' })
|
|
7
|
+
const model = defineModel<string>({ default: DEFAULT_CHAIN })
|
|
18
8
|
</script>
|
|
19
9
|
|
|
20
10
|
<template>
|
|
21
|
-
<
|
|
22
|
-
<
|
|
23
|
-
|
|
24
|
-
v-for="c in CHAINS"
|
|
25
|
-
:key="c.id"
|
|
26
|
-
:label="c.label"
|
|
27
|
-
:icon="c.icon"
|
|
28
|
-
:active="model === c.id"
|
|
29
|
-
@click="model = c.id"
|
|
30
|
-
/>
|
|
31
|
-
</div>
|
|
11
|
+
<SidebarGroup :label="CHAINS.length > 1 ? 'Select chain' : 'Chain'" :level="1">
|
|
12
|
+
<SidebarItem v-for="c in CHAINS" :key="c.id" :label="c.label" :icon="c.icon" :active="model === c.id" @click="model = c.id" />
|
|
13
|
+
</SidebarGroup>
|
|
32
14
|
</template>
|
|
33
|
-
|
|
34
|
-
<style scoped>
|
|
35
|
-
.chain-selector__title {
|
|
36
|
-
font-size: 0.7rem;
|
|
37
|
-
letter-spacing: var(--tracking-wide);
|
|
38
|
-
text-transform: uppercase;
|
|
39
|
-
color: var(--muted);
|
|
40
|
-
opacity: 0.75;
|
|
41
|
-
/* Left margin matches SidebarItem's horizontal padding so the label aligns with the items. */
|
|
42
|
-
margin: 0 0 var(--space-2xs) var(--space-sm);
|
|
43
|
-
}
|
|
44
|
-
</style>
|
|
@@ -1,16 +1,17 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
|
-
// Network selector for the dashboard sidebar. Reads and writes the shared useNetwork()
|
|
3
|
-
//
|
|
4
|
-
import { ref, watch
|
|
5
|
-
import {
|
|
6
|
-
import type
|
|
2
|
+
// Network selector for the dashboard sidebar. Reads and writes the shared useNetwork() singleton so every embedded tool view reacts to the
|
|
3
|
+
// chosen network immediately. Mainnet is listed but not selectable before launch, and a stored mainnet choice is reset to testnet.
|
|
4
|
+
import { onMounted, ref, watch } from 'vue'
|
|
5
|
+
import { UiFormField, UiSelect, UiTextField } from '@meddleware/ui'
|
|
6
|
+
import { useNetwork, type MwNetwork } from '@meddleware/wallet-adapter'
|
|
7
7
|
|
|
8
8
|
const { network, localnetRpc, setNetwork, setLocalnetRpc } = useNetwork()
|
|
9
9
|
|
|
10
10
|
const localnetInput = ref(localnetRpc.value)
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
11
|
+
// Sync the input when the singleton changes externally (for example another tab, through a storage event).
|
|
12
|
+
watch(localnetRpc, (v) => {
|
|
13
|
+
localnetInput.value = v
|
|
14
|
+
})
|
|
14
15
|
|
|
15
16
|
function onNetworkChange(e: Event): void {
|
|
16
17
|
setNetwork((e.target as HTMLSelectElement).value as MwNetwork)
|
|
@@ -27,83 +28,32 @@ onMounted(() => {
|
|
|
27
28
|
</script>
|
|
28
29
|
|
|
29
30
|
<template>
|
|
30
|
-
<
|
|
31
|
-
<
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
</
|
|
31
|
+
<section class="network" aria-label="Network">
|
|
32
|
+
<UiFormField id="ns-select" label="Network">
|
|
33
|
+
<template #default="{ attrs }">
|
|
34
|
+
<UiSelect :id="attrs.id" :model-value="network" @change="onNetworkChange">
|
|
35
|
+
<option value="testnet">Testnet</option>
|
|
36
|
+
<option value="mainnet" disabled title="Coming soon">Mainnet</option>
|
|
37
|
+
<option value="localnet">Localnet</option>
|
|
38
|
+
</UiSelect>
|
|
39
|
+
</template>
|
|
40
|
+
</UiFormField>
|
|
41
|
+
<UiTextField
|
|
42
|
+
v-if="network === 'localnet'"
|
|
43
|
+
id="ns-localnet-url"
|
|
44
|
+
v-model="localnetInput"
|
|
45
|
+
type="url"
|
|
46
|
+
label="RPC URL"
|
|
47
|
+
placeholder="http://127.0.0.1:9000"
|
|
48
|
+
@blur="commitLocalnetUrl"
|
|
49
|
+
@keydown.enter="commitLocalnetUrl"
|
|
50
|
+
/>
|
|
51
|
+
</section>
|
|
51
52
|
</template>
|
|
52
53
|
|
|
53
54
|
<style scoped>
|
|
54
|
-
.
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
gap: 0.35rem;
|
|
58
|
-
padding: 0.75rem 1rem;
|
|
59
|
-
border-top: 1px solid var(--border, #333);
|
|
60
|
-
border-bottom: 1px solid var(--border, #333);
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
.ns-label {
|
|
64
|
-
font-size: 0.7rem;
|
|
65
|
-
text-transform: uppercase;
|
|
66
|
-
letter-spacing: 0.06em;
|
|
67
|
-
color: var(--muted, #888);
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
.ns-label--url {
|
|
71
|
-
margin-top: 0.25rem;
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
.ns-select {
|
|
75
|
-
appearance: none;
|
|
76
|
-
background: var(--surface, transparent)
|
|
77
|
-
url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='10' height='6' viewBox='0 0 10 6'%3E%3Cpath d='M0 0l5 6 5-6z' fill='%23888'/%3E%3C/svg%3E")
|
|
78
|
-
no-repeat right 0.6rem center;
|
|
79
|
-
border: 1px solid var(--border, #444);
|
|
80
|
-
border-radius: 6px;
|
|
81
|
-
color: var(--text, #f0f0f0);
|
|
82
|
-
font-size: 0.85rem;
|
|
83
|
-
padding: 0.35rem 1.75rem 0.35rem 0.6rem;
|
|
84
|
-
cursor: pointer;
|
|
85
|
-
width: 100%;
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
.ns-select:focus {
|
|
89
|
-
outline: 2px solid var(--accent, #6366f1);
|
|
90
|
-
outline-offset: 1px;
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
.ns-input {
|
|
94
|
-
background: var(--surface, transparent);
|
|
95
|
-
border: 1px solid var(--border, #444);
|
|
96
|
-
border-radius: 6px;
|
|
97
|
-
color: var(--text, #f0f0f0);
|
|
98
|
-
font-size: 0.8rem;
|
|
99
|
-
font-family: monospace;
|
|
100
|
-
padding: 0.35rem 0.6rem;
|
|
101
|
-
width: 100%;
|
|
102
|
-
box-sizing: border-box;
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
.ns-input:focus {
|
|
106
|
-
outline: 2px solid var(--accent, #6366f1);
|
|
107
|
-
outline-offset: 1px;
|
|
55
|
+
.network {
|
|
56
|
+
padding: var(--space-2xs) var(--space-sm) var(--space-xs);
|
|
57
|
+
border-block: 1px solid var(--border);
|
|
108
58
|
}
|
|
109
59
|
</style>
|
package/src/e2e.ts
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
// End-to-end build only (`vite build --mode e2e`; main.ts loads this when MODE is `e2e` and VITE_E2E is `1`, both static, so every other
|
|
2
|
+
// build drops it). A mock wallet is registered like an extension and the chain client is a stub with nothing on it, so every embedded tool
|
|
3
|
+
// can be checked connected (each tool's own flows are covered in its own repository).
|
|
4
|
+
import { installE2eWallet } from '@meddleware/wallet-adapter/testing'
|
|
5
|
+
|
|
6
|
+
export function installE2e(): void {
|
|
7
|
+
installE2eWallet()
|
|
8
|
+
}
|
package/src/main.ts
CHANGED
|
@@ -1,21 +1,20 @@
|
|
|
1
1
|
import { createApp } from 'vue'
|
|
2
2
|
|
|
3
|
-
// Shared design system: tokens first (defines CSS vars), then
|
|
3
|
+
// Shared design system: tokens first (defines CSS vars), then the element defaults and utilities (components bring their own CSS).
|
|
4
4
|
import '@meddleware/design-tokens/tokens.css'
|
|
5
5
|
import '@meddleware/design-tokens/seasons.css'
|
|
6
|
-
import '@meddleware/ui/
|
|
7
|
-
import './styles.css'
|
|
6
|
+
import '@meddleware/ui/reset.css'
|
|
8
7
|
|
|
9
|
-
import { loadRuntimeConfig,
|
|
8
|
+
import { loadRuntimeConfig, useSeason } from '@meddleware/ui'
|
|
10
9
|
|
|
11
|
-
//
|
|
12
|
-
// Applied before mount to avoid a light-mode flash.
|
|
13
|
-
useColorMode('dark')
|
|
10
|
+
// The colour mode (dark by default, then the saved choice) is applied before the first paint by the theme-init script `index.html` loads.
|
|
14
11
|
useSeason()
|
|
15
12
|
|
|
16
|
-
// Machine-zone endpoints (the indexer, the Walrus relays) may come from /runtime-config.json (ADR-0005). The
|
|
17
|
-
//
|
|
13
|
+
// Machine-zone endpoints (the indexer, the Walrus relays) may come from /runtime-config.json (ADR-0005). The embedded tools read them when
|
|
14
|
+
// their modules load, so the app and the router are imported only afterwards.
|
|
18
15
|
void loadRuntimeConfig().then(async () => {
|
|
16
|
+
// End-to-end builds only: a mock wallet and a stub chain (`src/e2e.ts`). Both conditions are static, so no other build contains it.
|
|
17
|
+
if (import.meta.env.MODE === 'e2e' && import.meta.env.VITE_E2E === '1') (await import('./e2e.js')).installE2e()
|
|
19
18
|
const [{ default: App }, { default: router }] = await Promise.all([import('./App.vue'), import('./router')])
|
|
20
19
|
const app = createApp(App)
|
|
21
20
|
app.use(router)
|
package/src/router/index.ts
CHANGED
|
@@ -12,21 +12,16 @@ const router = createRouter({
|
|
|
12
12
|
{
|
|
13
13
|
path: '/',
|
|
14
14
|
name: 'treasury',
|
|
15
|
+
meta: { title: 'Treasury' },
|
|
15
16
|
component: () => import('@meddleware/treasury-ui').then((m) => m.TreasuryView),
|
|
16
17
|
},
|
|
17
18
|
{
|
|
18
19
|
path: '/blockchain',
|
|
19
20
|
name: 'blockchain',
|
|
21
|
+
meta: { title: 'Blockchain tools' },
|
|
20
22
|
component: () => import('../views/BlockchainView.vue'),
|
|
21
23
|
},
|
|
22
|
-
// DAO is
|
|
23
|
-
// retained). Re-enable this route once DAO participation ships so users aren't shown a
|
|
24
|
-
// governance surface they can't yet act on.
|
|
25
|
-
// {
|
|
26
|
-
// path: '/dao',
|
|
27
|
-
// name: 'dao',
|
|
28
|
-
// component: () => import('@meddleware/dao-ui').then((m) => m.DaoView),
|
|
29
|
-
// },
|
|
24
|
+
// DAO is retired from the dashboard until governance is usable: there is no route for it (the dao-ui package is not a dependency).
|
|
30
25
|
|
|
31
26
|
// Legacy per-tool paths now live as tabs inside /blockchain — redirect old bookmarks.
|
|
32
27
|
{ path: '/walrus', redirect: '/blockchain' },
|
|
@@ -1,12 +1,14 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
2
|
// Blockchain tools hub — a qt-style tabbed console over one chain's tools. Today the only
|
|
3
|
-
// chain is Sui; the
|
|
4
|
-
// switch tool sets without a rewrite. Each tool view is lazy-loaded (its deps, incl. the Walrus
|
|
3
|
+
// chain is Sui; the `chain` prop is the sidebar's choice, so a second chain switches the tool set without a rewrite. Each tool view is lazy-loaded (its deps, incl. the Walrus
|
|
5
4
|
// wasm, load only when its tab is first opened) and kept alive so switching tabs preserves state.
|
|
6
5
|
import { computed, defineAsyncComponent, ref, watch } from 'vue'
|
|
7
6
|
import { AppTabNav, UiTabPanel, type AppTab } from '@meddleware/ui'
|
|
8
7
|
|
|
9
|
-
|
|
8
|
+
import { CHAINS, DEFAULT_CHAIN } from '../chains.js'
|
|
9
|
+
|
|
10
|
+
const props = withDefaults(defineProps<{ chain?: string }>(), { chain: DEFAULT_CHAIN })
|
|
11
|
+
const chainLabel = computed(() => CHAINS.find((c) => c.id === props.chain)?.label ?? props.chain)
|
|
10
12
|
|
|
11
13
|
interface Tool extends AppTab {
|
|
12
14
|
comp: ReturnType<typeof defineAsyncComponent>
|
|
@@ -38,61 +40,37 @@ const activeComp = computed(() => tools.value.find((t) => t.id === active.value)
|
|
|
38
40
|
</script>
|
|
39
41
|
|
|
40
42
|
<template>
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
<AppTabNav
|
|
45
|
-
v-model="active"
|
|
46
|
-
:tabs="tabs"
|
|
47
|
-
id-prefix="blockchain"
|
|
48
|
-
variant="raised"
|
|
49
|
-
size="lg"
|
|
50
|
-
class="blockchain-view__tabs"
|
|
51
|
-
aria-label="Sui tools"
|
|
52
|
-
/>
|
|
43
|
+
<section aria-labelledby="blockchain-title" class="blockchain-view">
|
|
44
|
+
<h2 id="blockchain-title" class="mw-visually-hidden">{{ chainLabel }} tools</h2>
|
|
45
|
+
<AppTabNav v-model="active" :tabs="tabs" id-prefix="blockchain" variant="raised" size="lg" flush class="tabs" :aria-label="`${chainLabel} tools`" />
|
|
53
46
|
|
|
54
|
-
<UiTabPanel id-prefix="blockchain" :tab="active" class="
|
|
47
|
+
<UiTabPanel id-prefix="blockchain" :tab="active" class="content">
|
|
55
48
|
<KeepAlive>
|
|
56
49
|
<component :is="activeComp" :key="active" />
|
|
57
50
|
</KeepAlive>
|
|
58
51
|
</UiTabPanel>
|
|
59
|
-
</
|
|
52
|
+
</section>
|
|
60
53
|
</template>
|
|
61
54
|
|
|
62
55
|
<style scoped>
|
|
63
56
|
.blockchain-view {
|
|
64
57
|
display: flex;
|
|
65
58
|
flex-direction: column;
|
|
66
|
-
height: 100%;
|
|
67
59
|
min-height: 0;
|
|
68
60
|
}
|
|
69
61
|
|
|
70
|
-
.
|
|
71
|
-
|
|
62
|
+
/* The tool tabs stay in view under the sticky header while the page scrolls. */
|
|
63
|
+
.tabs {
|
|
64
|
+
position: sticky;
|
|
65
|
+
top: var(--mw-header-height);
|
|
66
|
+
z-index: var(--z-raised);
|
|
67
|
+
background: var(--bg);
|
|
72
68
|
}
|
|
73
69
|
|
|
74
|
-
/* The
|
|
75
|
-
|
|
76
|
-
and capped at --mw-tool-content-max by inline padding that grows with the spare width. The
|
|
77
|
-
panel is focusable (UiTabPanel), so keyboard users can scroll it. */
|
|
78
|
-
.blockchain-view__content {
|
|
70
|
+
/* The content column is centred and capped at the tool width; the inline padding grows with the spare width. */
|
|
71
|
+
.content {
|
|
79
72
|
flex: 1;
|
|
80
|
-
display: flex;
|
|
81
|
-
flex-direction: column;
|
|
82
|
-
min-height: 0;
|
|
83
|
-
overflow-y: auto;
|
|
84
|
-
scrollbar-gutter: stable;
|
|
85
|
-
/* Consecutive Fibonacci steps of the spacing scale (13 : 8 : 5 units), so top : bottom and
|
|
86
|
-
bottom : sides are each ≈ φ — generous air under the tabs, tighter at the foot and sides. */
|
|
87
73
|
padding-block: var(--space-lg) var(--space-md);
|
|
88
74
|
padding-inline: max(var(--space-sm), calc((100% - var(--mw-tool-content-max)) / 2 + var(--space-sm)));
|
|
89
75
|
}
|
|
90
|
-
|
|
91
|
-
/* This page only: the tool tabs sit flush — no inset around the strip and no gaps between tabs.
|
|
92
|
-
Overrides AppTabNav's shared raised/lg spacing for this instance; other tab strips keep theirs.
|
|
93
|
-
The modifier classes match the component's own selector so this wins regardless of CSS order. */
|
|
94
|
-
.blockchain-view__tabs.mw-tab-nav--raised.mw-tab-nav--lg {
|
|
95
|
-
gap: 0;
|
|
96
|
-
padding: 0;
|
|
97
|
-
}
|
|
98
76
|
</style>
|
package/vite.config.ts
CHANGED
|
@@ -1,14 +1,20 @@
|
|
|
1
1
|
import { defineConfig } from 'vite'
|
|
2
2
|
import vue from '@vitejs/plugin-vue'
|
|
3
|
+
import { assertE2eOnlyInE2eMode, themeInit, viteTarget } from '@meddleware/frontend-config/vite'
|
|
3
4
|
|
|
4
5
|
// @meddleware/* resolve from node_modules (published packages — no monorepo aliases).
|
|
5
|
-
export default defineConfig({
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
6
|
+
export default defineConfig(({ mode }) => {
|
|
7
|
+
// The end-to-end build (`--mode e2e`) includes a mock wallet; nothing else may.
|
|
8
|
+
assertE2eOnlyInE2eMode(mode)
|
|
9
|
+
return {
|
|
10
|
+
build: { target: viteTarget() },
|
|
11
|
+
plugins: [vue(), themeInit({ default: 'dark' })],
|
|
12
|
+
optimizeDeps: {
|
|
13
|
+
// The Walrus wasm client is dynamically imported inside the inline WalrusStorageView upload flow;
|
|
14
|
+
// keep it out of the eager dep-optimization graph so it stays a lazy chunk.
|
|
15
|
+
// @meddleware/wallet-adapter ships TS + .vue source and holds the shared wallet singleton; if it
|
|
16
|
+
// were pre-bundled, its .vue files (served raw) would load a second copy of the singleton in dev.
|
|
17
|
+
exclude: ['@mysten/walrus', '@mysten/walrus-wasm', '@meddleware/wallet-adapter'],
|
|
18
|
+
},
|
|
19
|
+
}
|
|
14
20
|
})
|
package/src/styles.css
DELETED
|
@@ -1,17 +0,0 @@
|
|
|
1
|
-
/*
|
|
2
|
-
* Dashboard app-layout styles. Design tokens + element defaults come from
|
|
3
|
-
* @meddleware/design-tokens + @meddleware/ui/base.css (imported in main.ts).
|
|
4
|
-
* App shell grid layout lives in App.vue scoped styles.
|
|
5
|
-
*/
|
|
6
|
-
|
|
7
|
-
#app {
|
|
8
|
-
min-height: 100dvh;
|
|
9
|
-
}
|
|
10
|
-
|
|
11
|
-
.prose {
|
|
12
|
-
max-width: 68ch;
|
|
13
|
-
color: var(--muted);
|
|
14
|
-
}
|
|
15
|
-
.prose h2 {
|
|
16
|
-
color: var(--text);
|
|
17
|
-
}
|