feat(mesh): device onboarding modal (backlog #6)
Guided prompt that pops up when a mesh radio is detected but not yet connected -- wraps the existing Connect action (mesh.configure with device_path) rather than building a new setup engine. Dismissible per device path (won't re-prompt for the same undismissed-but-ignored device on every poll tick). Not the whole-app identity/seed onboarding system (useOnboarding.ts) -- confirmed unrelated, this is mesh-specific only. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
parent
712df2278f
commit
46dae75a0f
@ -38,6 +38,13 @@ const sendError = ref('')
|
|||||||
const broadcasting = ref(false)
|
const broadcasting = ref(false)
|
||||||
const configuring = ref(false)
|
const configuring = ref(false)
|
||||||
const connectingDevice = ref<string | null>(null)
|
const connectingDevice = ref<string | null>(null)
|
||||||
|
// Onboarding modal (#6): guides a first-time connect for a freshly-detected,
|
||||||
|
// not-yet-connected device — a friendlier wrapper around the same Connect
|
||||||
|
// action the "Detected USB devices" list already offers, not a new setup
|
||||||
|
// engine. `onboardingDismissed` remembers paths the user closed without
|
||||||
|
// connecting, so it doesn't reappear every poll tick for the same device.
|
||||||
|
const showOnboardingModal = ref(false)
|
||||||
|
const onboardingDismissed = ref<Set<string>>(new Set())
|
||||||
const chatScrollEl = ref<HTMLElement | null>(null)
|
const chatScrollEl = ref<HTMLElement | null>(null)
|
||||||
const mobileShowChat = ref(false)
|
const mobileShowChat = ref(false)
|
||||||
// Device status panel starts collapsed on mobile (expandable via its header).
|
// Device status panel starts collapsed on mobile (expandable via its header).
|
||||||
@ -1014,11 +1021,34 @@ async function handleConnectDevice(devicePath: string) {
|
|||||||
connectingDevice.value = devicePath
|
connectingDevice.value = devicePath
|
||||||
try {
|
try {
|
||||||
await mesh.configure({ enabled: true, device_path: devicePath } as Partial<import('@/stores/mesh').MeshStatus>)
|
await mesh.configure({ enabled: true, device_path: devicePath } as Partial<import('@/stores/mesh').MeshStatus>)
|
||||||
|
showOnboardingModal.value = false
|
||||||
} finally {
|
} finally {
|
||||||
connectingDevice.value = null
|
connectingDevice.value = null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const undismissedDetectedDevices = computed(() =>
|
||||||
|
(mesh.status?.detected_devices ?? []).filter((d) => !onboardingDismissed.value.has(d))
|
||||||
|
)
|
||||||
|
|
||||||
|
function dismissOnboarding() {
|
||||||
|
for (const d of undismissedDetectedDevices.value) onboardingDismissed.value.add(d)
|
||||||
|
showOnboardingModal.value = false
|
||||||
|
}
|
||||||
|
|
||||||
|
// Pop the onboarding modal the moment a device is detected but not yet
|
||||||
|
// connected — same trigger condition the inline "Detected USB devices" list
|
||||||
|
// already uses (mesh.status.detected_devices non-empty + not connected),
|
||||||
|
// just surfaced as a guided prompt instead of requiring the user to notice
|
||||||
|
// the collapsed Device card.
|
||||||
|
watch(
|
||||||
|
() => [mesh.status?.device_connected, undismissedDetectedDevices.value.length] as const,
|
||||||
|
([connected, count]) => {
|
||||||
|
if (!connected && count > 0) showOnboardingModal.value = true
|
||||||
|
},
|
||||||
|
{ immediate: true },
|
||||||
|
)
|
||||||
|
|
||||||
function signalBars(rssi: number | null): number {
|
function signalBars(rssi: number | null): number {
|
||||||
if (rssi === null) return 0
|
if (rssi === null) return 0
|
||||||
if (rssi > -60) return 4
|
if (rssi > -60) return 4
|
||||||
@ -2308,6 +2338,32 @@ function isImageMime(mime?: string): boolean {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Onboarding modal (#6): guided first-connect prompt for a freshly
|
||||||
|
detected, not-yet-connected mesh device — wraps the same Connect
|
||||||
|
action the inline "Detected USB devices" list already offers. -->
|
||||||
|
<div v-if="showOnboardingModal" class="mesh-transport-modal-backdrop" @click.self="dismissOnboarding">
|
||||||
|
<div class="glass-card mesh-transport-modal">
|
||||||
|
<h3 class="mesh-transport-title">📡 Mesh Device Found</h3>
|
||||||
|
<p class="mesh-transport-sub">
|
||||||
|
A radio was detected but isn't connected yet. Connect it to start using off-grid mesh chat.
|
||||||
|
</p>
|
||||||
|
<div class="mesh-transport-options">
|
||||||
|
<button
|
||||||
|
v-for="dev in undismissedDetectedDevices"
|
||||||
|
:key="dev"
|
||||||
|
class="mesh-transport-option"
|
||||||
|
:disabled="connectingDevice !== null"
|
||||||
|
@click="handleConnectDevice(dev)"
|
||||||
|
>
|
||||||
|
<span class="mesh-transport-icon">📡</span>
|
||||||
|
<span class="mesh-transport-label">{{ dev }}</span>
|
||||||
|
<span class="mesh-transport-meta">{{ connectingDevice === dev ? 'Connecting…' : 'Connect' }}</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<button class="mesh-transport-cancel" @click="dismissOnboarding">Not now</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user