mirror of
https://github.com/openclaw/openclaw.git
synced 2026-07-11 06:26:09 +00:00
Aligns the shared OpenClawMascotView with the website hero styling: per-part bounding-box gradients (SVG objectBoundingBox semantics; the claws were nearly flat-colored before), antenna stroke width 2, eye glow r=2, float depth -8% of size, and a light-scheme palette from the site theme variables. The float now offsets the whole canvas like the site floats the hero container, so the antennae no longer clip at the float peak. macOS adopts the hero glow treatment: the About pane mascot gets the coral silhouette drop-shadow with the teal hover glow and 1.1 hover scale, and the onboarding hero replaces its hand-rolled breathing glow circle with the same silhouette glow.
251 lines
9.2 KiB
Swift
251 lines
9.2 KiB
Swift
import AppKit
|
|
import SwiftUI
|
|
|
|
extension OnboardingView {
|
|
var body: some View {
|
|
VStack(spacing: 0) {
|
|
GlowingOpenClawIcon(size: 130)
|
|
.offset(y: 10)
|
|
.frame(height: 145)
|
|
|
|
GeometryReader { _ in
|
|
HStack(spacing: 0) {
|
|
ForEach(self.pageOrder, id: \.self) { pageIndex in
|
|
self.pageView(for: pageIndex)
|
|
.frame(width: self.pageWidth)
|
|
}
|
|
}
|
|
.offset(x: CGFloat(-self.currentPage) * self.pageWidth)
|
|
.animation(
|
|
.interactiveSpring(response: 0.5, dampingFraction: 0.86, blendDuration: 0.25),
|
|
value: self.currentPage)
|
|
.frame(height: self.contentHeight, alignment: .top)
|
|
.clipped()
|
|
}
|
|
.frame(height: self.contentHeight)
|
|
|
|
Spacer(minLength: 0)
|
|
self.navigationBar
|
|
}
|
|
.frame(width: pageWidth, height: Self.windowHeight)
|
|
.background(Color(NSColor.windowBackgroundColor))
|
|
.onAppear {
|
|
self.onboardingVisible = true
|
|
self.currentPage = 0
|
|
self.updateMonitoring(for: 0)
|
|
}
|
|
.onChange(of: currentPage) { _, newValue in
|
|
self.updateMonitoring(for: self.activePageIndex(for: newValue))
|
|
}
|
|
.onChange(of: state.connectionMode) { _, _ in
|
|
let oldActive = self.activePageIndex
|
|
self.reconcilePageForModeChange(previousActivePageIndex: oldActive)
|
|
self.updateDiscoveryMonitoring(for: self.activePageIndex)
|
|
}
|
|
.onChange(of: needsBootstrap) { _, _ in
|
|
if self.currentPage >= self.pageOrder.count {
|
|
self.currentPage = max(0, self.pageOrder.count - 1)
|
|
}
|
|
}
|
|
.onChange(of: cliInstalled) { _, installed in
|
|
guard installed else { return }
|
|
self.updateMonitoring(for: self.activePageIndex)
|
|
}
|
|
.onDisappear {
|
|
self.onboardingVisible = false
|
|
self.stopPermissionMonitoring()
|
|
self.stopDiscovery()
|
|
}
|
|
.task {
|
|
await self.refreshPerms()
|
|
await self.refreshCLIStatus()
|
|
await self.loadWorkspaceDefaults()
|
|
await self.ensureDefaultWorkspace()
|
|
self.refreshBootstrapStatus()
|
|
self.preferredGatewayID = GatewayDiscoveryPreferences.preferredStableID()
|
|
}
|
|
}
|
|
|
|
func activePageIndex(for pageCursor: Int) -> Int {
|
|
guard !pageOrder.isEmpty else { return 0 }
|
|
let clamped = min(max(0, pageCursor), pageOrder.count - 1)
|
|
return pageOrder[clamped]
|
|
}
|
|
|
|
func reconcilePageForModeChange(previousActivePageIndex: Int) {
|
|
if let exact = pageOrder.firstIndex(of: previousActivePageIndex) {
|
|
withAnimation { self.currentPage = exact }
|
|
return
|
|
}
|
|
if let next = pageOrder.firstIndex(where: { $0 > previousActivePageIndex }) {
|
|
withAnimation { self.currentPage = next }
|
|
return
|
|
}
|
|
withAnimation { self.currentPage = max(0, self.pageOrder.count - 1) }
|
|
}
|
|
|
|
var navigationBar: some View {
|
|
let connectionLockIndex = pageOrder.firstIndex(of: connectionPageIndex)
|
|
let cliLockIndex = pageOrder.firstIndex(of: cliPageIndex)
|
|
let crestodianLockIndex = pageOrder.firstIndex(of: crestodianPageIndex)
|
|
return HStack(spacing: 20) {
|
|
ZStack(alignment: .leading) {
|
|
Button(action: {}, label: {
|
|
Label("Back", systemImage: "chevron.left").labelStyle(.iconOnly)
|
|
})
|
|
.buttonStyle(.plain)
|
|
.opacity(0)
|
|
.disabled(true)
|
|
|
|
if self.currentPage > 0 {
|
|
Button(action: self.handleBack, label: {
|
|
Label("Back", systemImage: "chevron.left")
|
|
.labelStyle(.iconOnly)
|
|
})
|
|
.buttonStyle(.plain)
|
|
.foregroundColor(.secondary)
|
|
.opacity(0.8)
|
|
.disabled(self.installingCLI)
|
|
.transition(.opacity.combined(with: .scale(scale: 0.9)))
|
|
}
|
|
}
|
|
.frame(minWidth: 80, alignment: .leading)
|
|
|
|
Spacer()
|
|
|
|
HStack(spacing: 8) {
|
|
ForEach(0..<self.pageCount, id: \.self) { index in
|
|
let isInstallLocked = self.installingCLI && index != self.currentPage
|
|
let isConnectionLocked = self.isConnectionSelectionBlocking &&
|
|
index > (connectionLockIndex ?? 0)
|
|
let isCLILocked = cliLockIndex != nil && !self.cliInstalled && index > (cliLockIndex ?? 0)
|
|
// Dots must honor the same setup gate as Next: no jumping
|
|
// past the Crestodian page before setup authored the config.
|
|
let isCrestodianLocked = crestodianLockIndex != nil &&
|
|
self.state.connectionMode == .local &&
|
|
!self.crestodianSetupComplete &&
|
|
index > (crestodianLockIndex ?? 0)
|
|
let isLocked = isInstallLocked || isConnectionLocked || isCLILocked ||
|
|
isCrestodianLocked
|
|
Button {
|
|
withAnimation { self.currentPage = index }
|
|
} label: {
|
|
Circle()
|
|
.fill(index == self.currentPage ? Color.accentColor : Color.gray.opacity(0.3))
|
|
.frame(width: 8, height: 8)
|
|
}
|
|
.buttonStyle(.plain)
|
|
.disabled(isLocked)
|
|
.opacity(isLocked ? 0.3 : 1)
|
|
}
|
|
}
|
|
|
|
Spacer()
|
|
|
|
Button(action: self.handleNext) {
|
|
Text(self.buttonTitle)
|
|
.frame(minWidth: 88)
|
|
}
|
|
.keyboardShortcut(.return)
|
|
.buttonStyle(.borderedProminent)
|
|
.disabled(!self.canAdvance)
|
|
}
|
|
.padding(.horizontal, 28)
|
|
.padding(.bottom, 13)
|
|
.frame(minHeight: 60, alignment: .bottom)
|
|
}
|
|
|
|
func onboardingPage(@ViewBuilder _ content: () -> some View) -> some View {
|
|
let scrollIndicatorGutter: CGFloat = 18
|
|
return ScrollView {
|
|
VStack(spacing: 16) {
|
|
content()
|
|
Spacer(minLength: 0)
|
|
}
|
|
.frame(maxWidth: .infinity, alignment: .top)
|
|
.padding(.trailing, scrollIndicatorGutter)
|
|
}
|
|
.scrollIndicators(.automatic)
|
|
.padding(.horizontal, 28)
|
|
.frame(width: pageWidth, alignment: .top)
|
|
}
|
|
|
|
func onboardingCard(
|
|
spacing: CGFloat = 12,
|
|
padding: CGFloat = 16,
|
|
@ViewBuilder _ content: () -> some View) -> some View
|
|
{
|
|
VStack(alignment: .leading, spacing: spacing) {
|
|
content()
|
|
}
|
|
.padding(padding)
|
|
.frame(maxWidth: .infinity, alignment: .leading)
|
|
.background(
|
|
RoundedRectangle(cornerRadius: 16, style: .continuous)
|
|
.fill(Color(NSColor.controlBackgroundColor))
|
|
.shadow(color: .black.opacity(0.06), radius: 8, y: 3))
|
|
}
|
|
|
|
func onboardingGlassCard(
|
|
spacing: CGFloat = 12,
|
|
padding: CGFloat = 16,
|
|
@ViewBuilder _ content: () -> some View) -> some View
|
|
{
|
|
let shape = RoundedRectangle(cornerRadius: 16, style: .continuous)
|
|
return VStack(alignment: .leading, spacing: spacing) {
|
|
content()
|
|
}
|
|
.padding(padding)
|
|
.frame(maxWidth: .infinity, alignment: .leading)
|
|
.background(Color.clear)
|
|
.clipShape(shape)
|
|
.overlay(shape.strokeBorder(Color.white.opacity(0.10), lineWidth: 1))
|
|
}
|
|
|
|
func featureRow(title: String, subtitle: String, systemImage: String) -> some View {
|
|
self.featureRowContent(title: title, subtitle: subtitle, systemImage: systemImage)
|
|
}
|
|
|
|
func featureActionRow(
|
|
title: String,
|
|
subtitle: String,
|
|
systemImage: String,
|
|
buttonTitle: String,
|
|
action: @escaping () -> Void) -> some View
|
|
{
|
|
self.featureRowContent(
|
|
title: title,
|
|
subtitle: subtitle,
|
|
systemImage: systemImage,
|
|
action: AnyView(
|
|
Button(buttonTitle, action: action)
|
|
.buttonStyle(.link)
|
|
.padding(.top, 2)))
|
|
}
|
|
|
|
private func featureRowContent(
|
|
title: String,
|
|
subtitle: String,
|
|
systemImage: String,
|
|
action: AnyView? = nil) -> some View
|
|
{
|
|
HStack(alignment: .top, spacing: 12) {
|
|
Image(systemName: systemImage)
|
|
.font(.title3.weight(.semibold))
|
|
.foregroundStyle(Color.accentColor)
|
|
.frame(width: 26)
|
|
VStack(alignment: .leading, spacing: 4) {
|
|
Text(title).font(.headline)
|
|
Text(subtitle)
|
|
.font(.subheadline)
|
|
.foregroundStyle(.secondary)
|
|
if let action {
|
|
action
|
|
}
|
|
}
|
|
Spacer(minLength: 0)
|
|
}
|
|
.padding(.vertical, 4)
|
|
}
|
|
}
|