@meddleware/dashboard-ui 0.0.0-stage → 0.1.89

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 ADDED
@@ -0,0 +1,79 @@
1
+ # Changelog
2
+
3
+ All notable changes to `@meddleware/dashboard` are documented here.
4
+ Format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/); versioning follows
5
+ [Semantic Versioning](https://semver.org/). Before 0.2.0, breaking changes ship as patch releases.
6
+ Releases before 0.1.85 are recorded in the git history only.
7
+
8
+ ## [0.1.89] - 2026-10-24
9
+
10
+ ### Changed
11
+
12
+ - Renamed from `dashboard` to `dashboard-ui` (workspace naming convention, `docs/architecture/ADR-0003-naming.md`). No code change beyond names, paths, hostnames and dependency names; the history below keeps the old names.
13
+
14
+ ## [0.1.88] - 2026-10-22
15
+
16
+ ### Changed
17
+
18
+ - Embedded apps on their latest releases (access-gate-client 0.0.10 stale-indexer fallback, walrus-client 0.0.30 flow fix, nft-gate-client 0.0.18 rules); image base static-server 0.1.11 (Go 1.26.10 standard library, CVE-2026-111943)
19
+
20
+ ## [0.1.87] - 2026-10-19
21
+
22
+ ### Changed
23
+
24
+ - Image rebuilt without the `VITE_UPLOAD_RELAY_MAX_TIP_MIST` repository variable (deleted; it still held the old 0.05 SUI ceiling and overrode the 0.005 SUI default introduced in the previous release)
25
+
26
+ ## [0.1.86] - 2026-10-19
27
+
28
+ ### Changed
29
+
30
+ - Embedded apps on their re-priced releases (workspace cost model, D57): token-deployer-ui 0.0.37 (2 SUI deploy fee, 0.005 SUI tip ceiling), walrus-ui 0.1.59, seal-ui 0.0.39; ui ^0.1.32; dependency updates and the Node 26 baseline merged since 0.1.85
31
+
32
+ ## [0.1.85] - 2026-10-18
33
+
34
+ ### Changed
35
+
36
+ - Embeds the 2026-10-18 builds of every tool: `@meddleware/walrus-ui` ^0.1.58 and
37
+ `@meddleware/token-deployer-ui` ^0.0.36 (both adopt `@meddleware/walrus-relay` 0.1.31: the access-gate
38
+ price is read from the chain and shown, with the payees and network, before the wallet opens),
39
+ `@meddleware/access-gate-ui` ^0.1.37, `@meddleware/seal-ui` ^0.0.38, `@meddleware/treasury-ui` ^0.0.18,
40
+ `@meddleware/dao-ui` ^0.1.34 (retained, not mounted) and `@meddleware/design-tokens` ^0.1.10; their
41
+ clients resolve to `walrus-client` 0.0.28, `access-gate-client` 0.0.9, `seal-client` 0.0.20 and
42
+ `sui-token-client` 0.0.11. `@mysten/sui` ^2.36.0, `vue` ^3.5.44, `vue-router` ^5.4.0 and merged tooling
43
+ updates. One copy of `@mysten/sui` and of each `@meddleware/*` package (`npm ls`).
44
+ - Image base: `static-server` 0.1.8 (`sha256:e32cf42b…4000`).
45
+ - `npm-publish.yml` `verify` calls `node-ci.yml` (audit gate, signatures, type-check, all linters, tests,
46
+ build, licences, browser smoke test), as the image release does (audit F15).
47
+ - The four image builds pass the same build arguments (the registry-mirror builds lacked
48
+ `VITE_WALRUS_RELAY_MAINNET`).
49
+ - `.env.example` lists every build input; `.env.production` names the current package;
50
+ `CLAUDE.md` and `SECURITY.md` are current.
51
+
52
+ ### Removed
53
+
54
+ - `VITE_ACCESS_GATE_PRICE_MIST_TESTNET` (Dockerfile argument, release workflow, docs): walrus-ui and
55
+ token-deployer-ui no longer read it. A build that still sets it ignores it. The repository variable of the
56
+ same name can be deleted.
57
+
58
+ ### Added
59
+
60
+ - Unit tests (`npm test`, vitest): route table and tool imports, the network selector's mainnet guard (I7),
61
+ design-token use (no `--warning` text colour, no undefined custom property), the build-input inventory,
62
+ and the inline-script guard.
63
+ - Browser smoke test (`npm run test:e2e`, Playwright): `/`, every tool tab of `/blockchain` and the legacy
64
+ redirects mount in Chromium with no uncaught error and no network.
65
+ - CI builds the image (not pushed) and probes the running container with `scripts/smoke.sh`: non-root user,
66
+ read-only root, the in-image health check, `/` and the SPA fallback with the security headers and the CSP
67
+ (no `'unsafe-inline'` for scripts), immutable `/assets/`, `/THIRD_PARTY_LICENSES`, dot-files refused, POST
68
+ 405 (audit F14). A Trivy filesystem scan (vulnerabilities, Dockerfile misconfiguration, secrets; fixable
69
+ CRITICAL/HIGH fail) runs beside it.
70
+ - `tests/workflows.test.ts` pins the release gate: both release workflows call `node-ci.yml`, CI runs every
71
+ check above, third-party actions are pinned by SHA, the Dockerfile runs `check:dist` and sets the user.
72
+ - `npm run check:dist` (also run by the Dockerfile) fails the build when a built page has an inline script or event-handler attribute
73
+ that the CSP would refuse (audit F13).
74
+ - `CHANGELOG.md` (listed in `files` but absent before).
75
+
76
+ ### Fixed
77
+
78
+ - CI verifies registry signatures with npm 11.20.0: the npm bundled with Node 24.13 (11.6.2) reports
79
+ `EMISSINGSIGNATUREKEY` for the attestations of recently published packages.
package/index.html ADDED
@@ -0,0 +1,13 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <link rel="icon" type="image/svg+xml" href="/favicon.svg">
6
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
7
+ <title>Meddleware</title>
8
+ </head>
9
+ <body>
10
+ <div id="app"></div>
11
+ <script type="module" src="/src/main.ts"></script>
12
+ </body>
13
+ </html>
package/package.json CHANGED
@@ -1,6 +1,86 @@
1
1
  {
2
2
  "name": "@meddleware/dashboard-ui",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "version": "0.1.89",
4
+ "homepage": "https://docs.meddleware.co.uk/",
5
+ "description": "Meddleware tools hub — Vue 3 SPA listing hosted services (Walrus, Access Gate) with white-label deploy options.",
6
+ "author": "Meddleware <dev@meddleware.co.uk>",
7
+ "license": "0BSD",
8
+ "repository": {
9
+ "type": "git",
10
+ "url": "git+https://github.com/meddleware-org/dashboard-ui.git"
11
+ },
12
+ "type": "module",
13
+ "files": [
14
+ "src",
15
+ "index.html",
16
+ "vite.config.ts",
17
+ "tsconfig.json",
18
+ "CHANGELOG.md"
19
+ ],
20
+ "scripts": {
21
+ "dev": "vite",
22
+ "build": "vue-tsc --noEmit && vite build",
23
+ "preview": "vite preview",
24
+ "type-check": "vue-tsc --noEmit",
25
+ "test": "vitest run",
26
+ "test:watch": "vitest",
27
+ "test:e2e": "playwright test",
28
+ "lint:css": "stylelint \"src/**/*.{css,vue}\"",
29
+ "lint:js": "eslint .",
30
+ "lint": "npm run lint:css && npm run lint:js && npm run lint:html",
31
+ "lint:html": "html-validate \"src/**/*.vue\"",
32
+ "licenses": "node scripts/third-party-licenses.mjs",
33
+ "check:licenses": "node scripts/third-party-licenses.mjs --check",
34
+ "check:dist": "node scripts/check-inline-scripts.mjs"
35
+ },
36
+ "publishConfig": {
37
+ "access": "public"
38
+ },
39
+ "dependencies": {
40
+ "@meddleware/access-gate-ui": "^0.1.39",
41
+ "@meddleware/dao-ui": "^0.1.36",
42
+ "@meddleware/design-tokens": "^0.1.10",
43
+ "@meddleware/sealed-storage-ui": "^0.0.41",
44
+ "@meddleware/token-issuance-ui": "^0.0.39",
45
+ "@meddleware/treasury-ui": "^0.0.20",
46
+ "@meddleware/ui": "^0.1.32",
47
+ "@meddleware/wallet-adapter": "^0.0.17",
48
+ "@meddleware/walrus-storage-ui": "^0.1.62",
49
+ "@mysten/sui": "^2.36.0",
50
+ "vue": "^3.5.44",
51
+ "vue-router": "^5.4.0"
52
+ },
53
+ "devDependencies": {
54
+ "@meddleware/eslint-config": "^0.0.3",
55
+ "@playwright/test": "~1.64.1",
56
+ "@types/node": "~26.6.5",
57
+ "@vitejs/plugin-vue": "^6.0.10",
58
+ "@vue/eslint-config-typescript": "^14.9.0",
59
+ "@vue/test-utils": "^2.5.1",
60
+ "autoprefixer": "^10.6.1",
61
+ "eslint": "^10.13.0",
62
+ "eslint-plugin-vue": "^10.11.1",
63
+ "eslint-plugin-vuejs-accessibility": "^2.6.2",
64
+ "html-validate": "^11.16.4",
65
+ "html-validate-vue": "^9.3.1",
66
+ "jiti": "^2.7.0",
67
+ "jsdom": "^30.1.3",
68
+ "postcss": "^8.5.30",
69
+ "postcss-html": "^2.0.0",
70
+ "stylelint": "^17.16.0",
71
+ "stylelint-config-standard": "^40.0.0",
72
+ "typescript": "~6.0.0",
73
+ "vite": "^8.3.4",
74
+ "vitest": "~5.0.2",
75
+ "vue-eslint-parser": "^10.4.1",
76
+ "vue-tsc": "~3.3.13"
77
+ },
78
+ "engines": {
79
+ "node": "^22.18.0 || >=24.12.0"
80
+ },
81
+ "browserslist": [
82
+ "> 0.5%",
83
+ "last 2 versions",
84
+ "not dead"
85
+ ]
86
+ }
package/src/App.vue ADDED
@@ -0,0 +1,277 @@
1
+ <script setup lang="ts">
2
+ import { computed, ref, watch } from 'vue'
3
+ import { useRoute, useRouter, RouterView, RouterLink } from 'vue-router'
4
+ import {
5
+ AppHeader, AppSidebar, AppFooter,
6
+ ColorModeControl, SidebarItem, StatusWidget, CopyableAddress, ExplorerLink, CopyrightLine,
7
+ suiExplorerUrl, useColorMode,
8
+ } from '@meddleware/ui'
9
+ import { useWallet, useNetwork } from '@meddleware/wallet-adapter'
10
+ import NetworkSelector from './components/NetworkSelector.vue'
11
+ import ChainSelector from './components/ChainSelector.vue'
12
+
13
+ const { mode } = useColorMode('dark')
14
+ const route = useRoute()
15
+ const router = useRouter()
16
+ const { network } = useNetwork()
17
+
18
+ // The sidebar has two modes. Top-level shows the organisation sections (Treasury, Blockchain).
19
+ // Entering the Blockchain section swaps the sidebar to a chain selector with an up-one-level
20
+ // control that returns to the top level without leaving the page.
21
+ 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
+ const sidebarView = ref<SidebarView>('top')
25
+ const selectedChain = ref('sui')
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.
52
+ watch(
53
+ () => route.path,
54
+ (p) => {
55
+ sidebarView.value = p === '/blockchain' ? 'blockchain' : 'top'
56
+ },
57
+ { immediate: true },
58
+ )
59
+
60
+ // The dashboard hosts the single shared wallet connection for every inline tool view. It
61
+ // requests the superset of features the tools need so the connect control only offers wallets
62
+ // capable of every operation; individual tools still feature-guard at call time.
63
+ const { account, disconnect } = useWallet({
64
+ requiredFeatures: ['sui:signTransaction', 'sui:signPersonalMessage'],
65
+ })
66
+
67
+ const DOCS_URL = import.meta.env.VITE_DOCS_URL || 'https://docs.meddleware.co.uk/'
68
+ const DEV_URL = import.meta.env.VITE_DEV_URL || 'https://dev.meddleware.co.uk/'
69
+
70
+ // SuiVision has no localnet explorer, so only build a link for public networks; otherwise the
71
+ // address is shown copy-only.
72
+ const accountExplorerHref = computed(() =>
73
+ account.value && network.value !== 'localnet'
74
+ ? suiExplorerUrl('account', account.value.address, network.value as 'testnet' | 'mainnet' | 'devnet')
75
+ : null,
76
+ )
77
+ </script>
78
+
79
+ <template>
80
+ <div class="app-shell">
81
+ <AppHeader class="app-shell__header" variant="dark">
82
+ <template #brand>
83
+ <RouterLink to="/" class="brand-link" @click="goTreasury()">
84
+ <span class="brand-mark" aria-hidden="true">◆</span>
85
+ <span>Meddleware</span>
86
+ </RouterLink>
87
+ </template>
88
+ <template #actions>
89
+ <ColorModeControl v-model="mode" />
90
+ </template>
91
+ </AppHeader>
92
+
93
+ <AppSidebar class="app-shell__sidebar" variant="dark">
94
+ <!-- Top level: organisation sections -->
95
+ <template v-if="sidebarView === 'top'">
96
+ <router-link to="/" custom v-slot="{ navigate }">
97
+ <SidebarItem label="Treasury" icon="🏦" :active="isTreasury" @click="goTreasury(navigate)" />
98
+ </router-link>
99
+ <SidebarItem label="Blockchain" icon="⛓" :active="isBlockchain" @click="enterBlockchain" />
100
+ </template>
101
+
102
+ <!-- Blockchain section: chain selector + up-one-level -->
103
+ <template v-else>
104
+ <button type="button" class="sidebar-up" @click="upOneLevel">
105
+ <span aria-hidden="true">←</span> Back
106
+ </button>
107
+ <ChainSelector v-model="selectedChain" />
108
+ </template>
109
+
110
+ <template #body>
111
+ <NetworkSelector v-if="account" class="sidebar-body" />
112
+ </template>
113
+
114
+ <!-- Sidebar foot holds only the wallet control (contextual). Copyright, docs links, and
115
+ status now live in the shell footer to free vertical space as the sidebar grows. -->
116
+ <template #foot>
117
+ <p v-if="account" class="sidebar-foot wallet-connected">
118
+ <CopyableAddress :address="account.address">
119
+ <ExplorerLink v-if="accountExplorerHref" :href="accountExplorerHref" :value="account.address" />
120
+ </CopyableAddress>
121
+ <button type="button" class="wallet-disconnect" @click="disconnect">Disconnect</button>
122
+ </p>
123
+ <p v-else class="sidebar-foot wallet-disconnected">No wallet connected</p>
124
+ </template>
125
+ </AppSidebar>
126
+
127
+ <main class="app-shell__main">
128
+ <RouterView />
129
+ </main>
130
+
131
+ <AppFooter class="app-shell__footer" variant="dark" :docs-url="DOCS_URL" :dev-url="DEV_URL">
132
+ <template #start>
133
+ <CopyrightLine symbolVariant="kopimi" organisation-name="Meddleware" rightsStatement="jam" />
134
+ </template>
135
+ <StatusWidget class="footer-status" />
136
+ </AppFooter>
137
+ </div>
138
+ </template>
139
+
140
+ <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
+ .brand-link {
185
+ display: flex;
186
+ align-items: center;
187
+ gap: 0.5rem;
188
+ color: inherit;
189
+ text-decoration: none;
190
+ }
191
+
192
+ .brand-mark {
193
+ color: var(--accent);
194
+ }
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 {
238
+ display: flex;
239
+ align-items: center;
240
+ justify-content: space-between;
241
+ gap: 0.5rem;
242
+ }
243
+
244
+ .wallet-disconnect {
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;
265
+ 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;
276
+ }
277
+ </style>
@@ -0,0 +1,44 @@
1
+ <script setup lang="ts">
2
+ // Modular blockchain selector shown in the sidebar while the Blockchain section is open.
3
+ // Data-driven so new chains are a one-line addition; defaults to Sui (the only live chain).
4
+ import { SidebarItem } from '@meddleware/ui'
5
+
6
+ interface Chain {
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' })
18
+ </script>
19
+
20
+ <template>
21
+ <div class="chain-selector">
22
+ <div class="chain-selector__title">Select chain</div>
23
+ <SidebarItem
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>
32
+ </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>
@@ -0,0 +1,109 @@
1
+ <script setup lang="ts">
2
+ // Network selector for the dashboard sidebar. Reads and writes the shared useNetwork()
3
+ // singleton so every embedded tool view reacts to the chosen network immediately.
4
+ import { ref, watch, onMounted } from 'vue'
5
+ import { useNetwork } from '@meddleware/wallet-adapter'
6
+ import type { MwNetwork } from '@meddleware/wallet-adapter'
7
+
8
+ const { network, localnetRpc, setNetwork, setLocalnetRpc } = useNetwork()
9
+
10
+ const localnetInput = ref(localnetRpc.value)
11
+
12
+ // Sync input when singleton changes externally (e.g. another tab via storage event).
13
+ watch(localnetRpc, (v) => { localnetInput.value = v })
14
+
15
+ function onNetworkChange(e: Event): void {
16
+ setNetwork((e.target as HTMLSelectElement).value as MwNetwork)
17
+ }
18
+
19
+ function commitLocalnetUrl(): void {
20
+ const url = localnetInput.value.trim()
21
+ if (url) setLocalnetRpc(url)
22
+ }
23
+
24
+ onMounted(() => {
25
+ if (network.value === 'mainnet') setNetwork('testnet')
26
+ })
27
+ </script>
28
+
29
+ <template>
30
+ <div class="ns-root">
31
+ <label class="ns-label" for="ns-select">Network</label>
32
+ <select id="ns-select" class="ns-select" :value="network" @change="onNetworkChange">
33
+ <option value="testnet">Testnet</option>
34
+ <option value="mainnet" disabled title="Coming soon">Mainnet</option>
35
+ <option value="localnet">Localnet</option>
36
+ </select>
37
+
38
+ <template v-if="network === 'localnet'">
39
+ <label class="ns-label ns-label--url" for="ns-localnet-url">RPC URL</label>
40
+ <input
41
+ id="ns-localnet-url"
42
+ v-model="localnetInput"
43
+ type="url"
44
+ class="ns-input"
45
+ placeholder="http://127.0.0.1:9000"
46
+ @blur="commitLocalnetUrl"
47
+ @keydown.enter="commitLocalnetUrl"
48
+ />
49
+ </template>
50
+ </div>
51
+ </template>
52
+
53
+ <style scoped>
54
+ .ns-root {
55
+ display: flex;
56
+ flex-direction: column;
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;
108
+ }
109
+ </style>
package/src/env.d.ts ADDED
@@ -0,0 +1,7 @@
1
+ /// <reference types="vite/client" />
2
+
3
+ declare module '*.vue' {
4
+ import type { DefineComponent } from 'vue'
5
+ const component: DefineComponent<Record<string, unknown>, Record<string, unknown>, unknown>
6
+ export default component
7
+ }
package/src/main.ts ADDED
@@ -0,0 +1,20 @@
1
+ import { createApp } from 'vue'
2
+
3
+ // Shared design system: tokens first (defines CSS vars), then base element defaults.
4
+ import '@meddleware/design-tokens/tokens.css'
5
+ import '@meddleware/design-tokens/seasons.css'
6
+ import '@meddleware/ui/base.css'
7
+ import './styles.css'
8
+
9
+ import { useColorMode, useSeason } from '@meddleware/ui'
10
+ import App from './App.vue'
11
+ import router from './router'
12
+
13
+ // Default to dark on first load (persisted to localStorage thereafter).
14
+ // Applied before mount to avoid a light-mode flash.
15
+ useColorMode('dark')
16
+ useSeason()
17
+
18
+ const app = createApp(App)
19
+ app.use(router)
20
+ app.mount('#app')
@@ -0,0 +1,41 @@
1
+ import { createRouter, createWebHistory } from 'vue-router'
2
+
3
+ // Two org-level sections:
4
+ // / → Treasury console (organisation accounting), the default landing route.
5
+ // /blockchain → Blockchain tools hub: a qt-style tabbed interface over the Sui tools.
6
+ // Each view/tool renders inline via the component exported from its own package (no iframes),
7
+ // sharing the dashboard's wallet through the @meddleware/wallet-adapter singleton. Routes are
8
+ // lazy so a tool's deps (incl. the Walrus wasm) load only on navigation.
9
+ const router = createRouter({
10
+ history: createWebHistory(import.meta.env.BASE_URL),
11
+ routes: [
12
+ {
13
+ path: '/',
14
+ name: 'treasury',
15
+ component: () => import('@meddleware/treasury-ui').then((m) => m.TreasuryView),
16
+ },
17
+ {
18
+ path: '/blockchain',
19
+ name: 'blockchain',
20
+ component: () => import('../views/BlockchainView.vue'),
21
+ },
22
+ // DAO is temporarily retired from the dashboard (the @meddleware/dao-ui dependency 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
+ // },
30
+
31
+ // Legacy per-tool paths now live as tabs inside /blockchain — redirect old bookmarks.
32
+ { path: '/walrus', redirect: '/blockchain' },
33
+ { path: '/walrus-storage', redirect: '/blockchain' },
34
+ { path: '/sealed-storage', redirect: '/blockchain' },
35
+ { path: '/access-gate', redirect: '/blockchain' },
36
+ { path: '/token-deployer', redirect: '/blockchain' },
37
+ { path: '/token-issuance', redirect: '/blockchain' },
38
+ ],
39
+ })
40
+
41
+ export default router
package/src/styles.css ADDED
@@ -0,0 +1,17 @@
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
+ }
@@ -0,0 +1,98 @@
1
+ <script setup lang="ts">
2
+ // Blockchain tools hub — a qt-style tabbed console over one chain's tools. Today the only
3
+ // chain is Sui; the component takes a `chain` prop so a future multi-chain sidebar selector can
4
+ // switch tool sets without a rewrite. Each tool view is lazy-loaded (its deps, incl. the Walrus
5
+ // wasm, load only when its tab is first opened) and kept alive so switching tabs preserves state.
6
+ import { computed, defineAsyncComponent, ref, watch } from 'vue'
7
+ import { AppTabNav, UiTabPanel, type AppTab } from '@meddleware/ui'
8
+
9
+ const props = withDefaults(defineProps<{ chain?: string }>(), { chain: 'sui' })
10
+
11
+ interface Tool extends AppTab {
12
+ comp: ReturnType<typeof defineAsyncComponent>
13
+ }
14
+
15
+ // Per-chain tool sets. Add a new key when onboarding a chain.
16
+ const TOOLSETS: Record<string, Tool[]> = {
17
+ sui: [
18
+ { id: 'walrus-storage', label: 'Walrus Storage', comp: defineAsyncComponent(() => import('@meddleware/walrus-storage-ui').then((m) => m.WalrusStorageView)) },
19
+ { id: 'sealed-storage', label: 'Sealed Storage', comp: defineAsyncComponent(() => import('@meddleware/sealed-storage-ui').then((m) => m.SealedStorageView)) },
20
+ { id: 'access-gate', label: 'Access Gate', comp: defineAsyncComponent(() => import('@meddleware/access-gate-ui').then((m) => m.AccessGateView)) },
21
+ { id: 'token-issuance', label: 'Token Issuance', comp: defineAsyncComponent(() => import('@meddleware/token-issuance-ui').then((m) => m.TokenIssuanceView)) },
22
+ ],
23
+ }
24
+
25
+ const tools = computed<Tool[]>(() => TOOLSETS[props.chain] ?? [])
26
+ const tabs = computed<AppTab[]>(() => tools.value.map(({ id, label }) => ({ id, label })))
27
+ const active = ref(tools.value[0]?.id ?? '')
28
+
29
+ // Reset to the first tool when the chain changes.
30
+ watch(
31
+ () => props.chain,
32
+ () => {
33
+ active.value = tools.value[0]?.id ?? ''
34
+ },
35
+ )
36
+
37
+ const activeComp = computed(() => tools.value.find((t) => t.id === active.value)?.comp)
38
+ </script>
39
+
40
+ <template>
41
+ <!-- Fills the shell's main area exactly, so main never scrolls on this page: the tool tabs stay
42
+ in view and only the tool panel scrolls (its scrollbar at the right edge of the area). -->
43
+ <div class="blockchain-view">
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
+ />
53
+
54
+ <UiTabPanel id-prefix="blockchain" :tab="active" class="blockchain-view__content">
55
+ <KeepAlive>
56
+ <component :is="activeComp" :key="active" />
57
+ </KeepAlive>
58
+ </UiTabPanel>
59
+ </div>
60
+ </template>
61
+
62
+ <style scoped>
63
+ .blockchain-view {
64
+ display: flex;
65
+ flex-direction: column;
66
+ height: 100%;
67
+ min-height: 0;
68
+ }
69
+
70
+ .blockchain-view__tabs {
71
+ flex: none;
72
+ }
73
+
74
+ /* The only scroll container on this page. It spans the full width of the main area (so its
75
+ scrollbar sits at the area's edge, not beside the content column); the content column is centred
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 {
79
+ 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
+ padding-block: var(--space-lg) var(--space-md);
88
+ padding-inline: max(var(--space-sm), calc((100% - var(--mw-tool-content-max)) / 2 + var(--space-sm)));
89
+ }
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
+ </style>
package/tsconfig.json ADDED
@@ -0,0 +1,18 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ESNext",
4
+ "module": "ESNext",
5
+ "moduleResolution": "Bundler",
6
+ "lib": ["ESNext", "DOM", "DOM.Iterable"],
7
+ "strict": true,
8
+ "esModuleInterop": true,
9
+ "skipLibCheck": true,
10
+ "resolveJsonModule": true,
11
+ "isolatedModules": true,
12
+ "verbatimModuleSyntax": true,
13
+ "jsx": "preserve",
14
+ "types": ["node"],
15
+ "noEmit": true
16
+ },
17
+ "include": ["src/**/*.ts", "src/**/*.vue", "src/**/*.d.ts", "tests/**/*.ts", "e2e/**/*.ts", "vite.config.ts", "vitest.config.ts", "playwright.config.ts"]
18
+ }
package/vite.config.ts ADDED
@@ -0,0 +1,14 @@
1
+ import { defineConfig } from 'vite'
2
+ import vue from '@vitejs/plugin-vue'
3
+
4
+ // @meddleware/* resolve from node_modules (published packages — no monorepo aliases).
5
+ export default defineConfig({
6
+ plugins: [vue()],
7
+ optimizeDeps: {
8
+ // The Walrus wasm client is dynamically imported inside the inline WalrusStorageView upload flow;
9
+ // keep it out of the eager dep-optimization graph so it stays a lazy chunk.
10
+ // @meddleware/wallet-adapter ships TS + .vue source and holds the shared wallet singleton; if it
11
+ // were pre-bundled, its .vue files (served raw) would load a second copy of the singleton in dev.
12
+ exclude: ['@mysten/walrus', '@mysten/walrus-wasm', '@meddleware/wallet-adapter'],
13
+ },
14
+ })
package/README.md DELETED
@@ -1,3 +0,0 @@
1
- # Temporary Holding Version
2
-
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.