@meddleware/dashboard-ui 0.1.93 → 0.1.95

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 CHANGED
@@ -5,6 +5,24 @@ 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.95] - 2026-11-05
9
+
10
+ ### Fixed
11
+
12
+ - The Blockchain view's tab strip sits under the header's real height and publishes its own offset (`--mw-sticky-extra`), so focus is not hidden behind the sticky header and strip (WCAG 2.4.11).
13
+
14
+ ### Security
15
+
16
+ - CI and the image build scan the production bundle for the e2e harness (`check-prod-bundle` from `@meddleware/frontend-config`), and `src/e2e*.ts` is no longer in the published package.
17
+
18
+ ### Changed
19
+
20
+ - Takes the embedded tools' 2026-11-05 releases (access-gate-ui 0.1.45, sealed-storage-ui 0.0.46, token-issuance-ui 0.0.44, treasury-ui 0.0.25, walrus-storage-ui 0.1.67) and `@meddleware/ui` ^0.1.38, `@meddleware/design-tokens` ^0.1.13, `@meddleware/web-utils` ^0.0.2, `@meddleware/frontend-config` ^0.0.2, `@meddleware/wallet-adapter` ^0.0.21.
21
+
22
+ ## [0.1.94] - 2026-11-01
23
+
24
+ - Shared shell, sidebar drawer on phones, tools updated, accessibility gates
25
+
8
26
  ## [0.1.93] - 2026-10-30
9
27
 
10
28
  - 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.93",
3
+ "version": "0.1.95",
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>",
@@ -15,11 +15,13 @@
15
15
  "index.html",
16
16
  "vite.config.ts",
17
17
  "tsconfig.json",
18
- "CHANGELOG.md"
18
+ "CHANGELOG.md",
19
+ "!src/e2e*.ts"
19
20
  ],
20
21
  "scripts": {
21
22
  "dev": "vite",
22
23
  "build": "vue-tsc --noEmit && vite build",
24
+ "build:e2e": "VITE_E2E=1 vite build --mode e2e",
23
25
  "preview": "vite preview",
24
26
  "type-check": "vue-tsc --noEmit",
25
27
  "test": "vitest run",
@@ -31,27 +33,31 @@
31
33
  "lint:html": "html-validate \"src/**/*.vue\"",
32
34
  "licenses": "node scripts/third-party-licenses.mjs",
33
35
  "check:licenses": "node scripts/third-party-licenses.mjs --check",
34
- "check:dist": "node scripts/check-inline-scripts.mjs"
36
+ "check:dist": "node scripts/check-inline-scripts.mjs",
37
+ "shots": "SHOTS=1 playwright test",
38
+ "lhci": "npm run build && lhci autorun",
39
+ "check:bundle": "check-prod-bundle dist"
35
40
  },
36
41
  "publishConfig": {
37
42
  "access": "public"
38
43
  },
39
44
  "dependencies": {
40
- "@meddleware/access-gate-ui": "^0.1.42",
41
- "@meddleware/dao-ui": "^0.1.38",
42
- "@meddleware/design-tokens": "^0.1.11",
43
- "@meddleware/sealed-storage-ui": "^0.0.43",
44
- "@meddleware/token-issuance-ui": "^0.0.41",
45
- "@meddleware/treasury-ui": "^0.0.22",
46
- "@meddleware/ui": "^0.1.35",
47
- "@meddleware/wallet-adapter": "^0.0.19",
48
- "@meddleware/walrus-storage-ui": "^0.1.64",
45
+ "@meddleware/access-gate-ui": "^0.1.45",
46
+ "@meddleware/design-tokens": "^0.1.13",
47
+ "@meddleware/sealed-storage-ui": "^0.0.46",
48
+ "@meddleware/token-issuance-ui": "^0.0.44",
49
+ "@meddleware/treasury-ui": "^0.0.25",
50
+ "@meddleware/ui": "^0.1.38",
51
+ "@meddleware/wallet-adapter": "^0.0.21",
52
+ "@meddleware/walrus-storage-ui": "^0.1.67",
53
+ "@meddleware/web-utils": "^0.0.2",
49
54
  "@mysten/sui": "^2.36.0",
50
55
  "vue": "^3.5.44",
51
56
  "vue-router": "^5.4.0"
52
57
  },
53
58
  "devDependencies": {
54
- "@meddleware/eslint-config": "^0.0.4",
59
+ "@lhci/cli": "^0.15.1",
60
+ "@meddleware/frontend-config": "^0.0.2",
55
61
  "@playwright/test": "~1.64.1",
56
62
  "@types/node": "~26.6.5",
57
63
  "@vitejs/plugin-vue": "^6.0.10",
@@ -79,8 +85,11 @@
79
85
  "node": "^22.18.0 || >=24.12.0"
80
86
  },
81
87
  "browserslist": [
82
- "> 0.5%",
83
- "last 2 versions",
84
- "not dead"
85
- ]
88
+ "extends @meddleware/frontend-config/browserslist-config"
89
+ ],
90
+ "overrides": {
91
+ "basic-ftp": "^6.2.4",
92
+ "tmp": "^0.2.7",
93
+ "zod": "^4.6.7"
94
+ }
86
95
  }
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 { useRoute, useRouter, RouterView, RouterLink } from 'vue-router'
5
+ import { RouterLink, RouterView, useRoute } from 'vue-router'
4
6
  import {
5
- AppHeader, AppSidebar, AppFooter,
6
- ColorModeControl, SidebarItem, StatusWidget, CopyableAddress, ExplorerLink, CopyrightLine,
7
- suiExplorerUrl, useColorMode,
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 { useWallet, useNetwork } from '@meddleware/wallet-adapter'
10
- import NetworkSelector from './components/NetworkSelector.vue'
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-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.
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('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.
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
- // 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
- })
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 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
- )
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
- <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>
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: 0.5rem;
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: 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;
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
- // 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'
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
- 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' })
7
+ const model = defineModel<string>({ default: DEFAULT_CHAIN })
18
8
  </script>
19
9
 
20
10
  <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>
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
- // 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'
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
- // Sync input when singleton changes externally (e.g. another tab via storage event).
13
- watch(localnetRpc, (v) => { localnetInput.value = v })
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
- <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>
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
- .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;
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/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 base element defaults.
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/base.css'
7
- import './styles.css'
6
+ import '@meddleware/ui/reset.css'
8
7
 
9
- import { loadRuntimeConfig, useColorMode, useSeason } from '@meddleware/ui'
8
+ import { loadRuntimeConfig, useSeason } from '@meddleware/ui'
10
9
 
11
- // Default to dark on first load (persisted to localStorage thereafter).
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
- // embedded tools read them when their modules load, so the app and the router are imported only afterwards.
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)
@@ -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 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
- // },
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 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
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
- import { AppTabNav, UiTabPanel, type AppTab } from '@meddleware/ui'
6
+ import { AppTabNav, UiTabPanel, useStickyOffset, type AppTab } from '@meddleware/ui'
8
7
 
9
- const props = withDefaults(defineProps<{ chain?: string }>(), { chain: 'sui' })
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>
@@ -35,64 +37,45 @@ watch(
35
37
  )
36
38
 
37
39
  const activeComp = computed(() => tools.value.find((t) => t.id === active.value)?.comp)
40
+
41
+ // The tab strip sticks under the header: publish its height so a focused field is never left behind either of them (WCAG 2.4.11).
42
+ const strip = ref<{ $el?: unknown } | null>(null)
43
+ useStickyOffset(strip, '--mw-sticky-extra')
38
44
  </script>
39
45
 
40
46
  <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
- />
47
+ <section aria-labelledby="blockchain-title" class="blockchain-view">
48
+ <h2 id="blockchain-title" class="mw-visually-hidden">{{ chainLabel }} tools</h2>
49
+ <AppTabNav v-model="active" :tabs="tabs" id-prefix="blockchain" variant="raised" size="lg" flush ref="strip" class="tabs" :aria-label="`${chainLabel} tools`" />
53
50
 
54
- <UiTabPanel id-prefix="blockchain" :tab="active" class="blockchain-view__content">
51
+ <UiTabPanel id-prefix="blockchain" :tab="active" class="content">
55
52
  <KeepAlive>
56
53
  <component :is="activeComp" :key="active" />
57
54
  </KeepAlive>
58
55
  </UiTabPanel>
59
- </div>
56
+ </section>
60
57
  </template>
61
58
 
62
59
  <style scoped>
63
60
  .blockchain-view {
64
61
  display: flex;
65
62
  flex-direction: column;
66
- height: 100%;
67
63
  min-height: 0;
68
64
  }
69
65
 
70
- .blockchain-view__tabs {
71
- flex: none;
66
+ /* The tool tabs stay in view under the sticky header while the page scrolls: under its real height, which is more than its minimum when
67
+ the header wraps on a phone. */
68
+ .tabs {
69
+ position: sticky;
70
+ top: var(--mw-header-offset, var(--mw-header-height));
71
+ z-index: var(--z-raised);
72
+ background: var(--bg);
72
73
  }
73
74
 
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 {
75
+ /* The content column is centred and capped at the tool width; the inline padding grows with the spare width. */
76
+ .content {
79
77
  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
78
  padding-block: var(--space-lg) var(--space-md);
88
79
  padding-inline: max(var(--space-sm), calc((100% - var(--mw-tool-content-max)) / 2 + var(--space-sm)));
89
80
  }
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
81
  </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
- 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
- },
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
- }