spb/prisme Public MIT
Navigateur iOS intelligent — chaque page comprise localement avant d'être affichée. SwiftUI · WebKit · Foundation Models, 100% on-device.
Swift 96.7%
JavaScript 3.3%
1//2// BrowserView.swift3// Prisme4//5// Author: Simon-Pierre Boucher <contact@spboucher.ai>6//78import SwiftUI910/// Main screen: the web content fills the screen, a floating glass chrome11/// sits at the bottom. The page always renders immediately — nothing in the12/// chrome may ever block it (CLAUDE.md §8).13struct BrowserView: View {14 let model: BrowserModel1516 @State private var showTabSwitcher = false17 @State private var showReader = false18 @State private var showLibrary = false1920 var body: some View {21 ZStack(alignment: .bottom) {22 content23 .ignoresSafeArea(edges: .bottom)24 chrome25 }26 .sheet(isPresented: $showTabSwitcher) {27 TabSwitcherView(model: model)28 }29 .fullScreenCover(isPresented: $showReader) {30 if let tab = model.tabs.activeTab {31 ReaderView(32 tab: tab,33 intelligence: model.intelligence,34 accent: model.activeContainer.color.color,35 library: model.library,36 onSaveFavorite: { model.saveFavorite(for: tab) }37 )38 }39 }40 .sheet(isPresented: $showLibrary) {41 LibraryView(42 library: model.library,43 containers: model.containers,44 onOpen: { url, containerID in45 model.selectContainer(containerID)46 model.submit(.navigate(url))47 }48 )49 }50 }5152 @ViewBuilder53 private var content: some View {54 if let tab = model.tabs.activeTab, !tab.isBlank {55 BrowserWebView(tab: tab, pool: model.pool)56 .id(tab.id)57 .transition(.opacity)58 } else {59 StartPageView(60 active: model.activeContainer,61 containers: model.containers.all,62 onSelect: { model.selectContainer($0) }63 )64 }65 }6667 // MARK: - Chrome6869 private var chrome: some View {70 VStack(spacing: Spacing.m) {71 if let tab = model.tabs.activeTab,72 let insight = tab.insight,73 !tab.insightDismissed,74 insight.url == tab.page.url {75 InsightStrip(76 insight: insight,77 digest: readyDigest(for: tab),78 accent: model.activeContainer.color.color,79 onRead: { showReader = true },80 onDismiss: {81 withAnimation(.spring(duration: 0.3)) {82 tab.insightDismissed = true83 }84 }85 )86 .transition(.move(edge: .bottom).combined(with: .opacity))87 }8889 if let notice = model.notice {90 Text(notice)91 .font(.footnote.weight(.medium))92 .padding(.horizontal, Spacing.l)93 .padding(.vertical, Spacing.s)94 .glassEffect(.regular, in: Capsule())95 .transition(.move(edge: .bottom).combined(with: .opacity))96 }9798 AddressBar(99 page: model.tabs.activeTab?.page,100 accent: model.activeContainer.color.color,101 history: model.history,102 onIntent: { model.submit($0) },103 onRecall: { model.open($0) },104 onOpenReader: { showReader = true }105 )106 .id(model.tabs.activeTabID)107108 toolbar109 }110 .padding(.horizontal, Spacing.l)111 .padding(.bottom, Spacing.s)112 .animation(.spring(duration: 0.3), value: model.notice)113 .animation(.spring(duration: 0.35), value: model.tabs.activeTab?.insight)114 }115116 private func readyDigest(for tab: Tab) -> PageDigest? {117 if case .ready(let digest, _) = model.intelligence.digestState(for: tab) {118 return digest119 }120 return nil121 }122123 private var toolbar: some View {124 HStack(spacing: 0) {125 toolbarButton("chevron.left", disabled: model.tabs.activeTab?.page.canGoBack != true) {126 model.tabs.activeTab?.page.goBack()127 }128 toolbarButton("chevron.right", disabled: model.tabs.activeTab?.page.canGoForward != true) {129 model.tabs.activeTab?.page.goForward()130 }131132 Spacer(minLength: 0)133 containerMenu134 Spacer(minLength: 0)135136 toolbarButton("books.vertical") {137 showLibrary = true138 }139140 toolbarButton("plus") {141 withAnimation(.spring(duration: 0.3)) { model.newTab() }142 }143 tabCountButton144 }145 .padding(.horizontal, Spacing.s)146 .frame(height: 54)147 .glassEffect(.regular, in: Capsule())148 }149150 private func toolbarButton(151 _ symbol: String,152 disabled: Bool = false,153 action: @escaping () -> Void154 ) -> some View {155 Button(action: action) {156 Image(systemName: symbol)157 .font(.title3.weight(.medium))158 .frame(width: 52, height: 44)159 .contentShape(Rectangle())160 }161 .buttonStyle(.plain)162 .foregroundStyle(disabled ? Color.secondary.opacity(0.4) : Color.primary)163 .disabled(disabled)164 }165166 private var tabCountButton: some View {167 Button {168 showTabSwitcher = true169 } label: {170 let count = model.tabs.tabs(in: model.activeContainerID).count171 ZStack {172 RoundedRectangle(cornerRadius: 7)173 .strokeBorder(Color.primary, lineWidth: 2)174 .frame(width: 24, height: 24)175 Text(count > 0 ? "\(count)" : "")176 .font(.caption.bold())177 .monospacedDigit()178 }179 .frame(width: 52, height: 44)180 .contentShape(Rectangle())181 }182 .buttonStyle(.plain)183 }184185 /// One gesture to change universe (identity container, P0).186 private var containerMenu: some View {187 Menu {188 ForEach(model.containers.all) { container in189 Button {190 withAnimation(.spring(duration: 0.3)) {191 model.selectContainer(container.id)192 }193 } label: {194 Label(container.name, systemImage: container.symbol)195 }196 }197 } label: {198 Image(systemName: model.activeContainer.symbol)199 .font(.subheadline.weight(.semibold))200 .foregroundStyle(.white)201 .frame(width: 40, height: 40)202 .background(model.activeContainer.color.gradient, in: Circle())203 }204 .buttonStyle(.plain)205 .accessibilityLabel(model.activeContainer.name)206 }207}208209// MARK: - Start page210211/// Shown when the active tab has nothing to render yet: the brand mark and212/// a one-tap switch between identity universes.213struct StartPageView: View {214 let active: IdentityContainer215 let containers: [IdentityContainer]216 let onSelect: (IdentityContainer.ID) -> Void217218 var body: some View {219 ZStack {220 LinearGradient(221 colors: [222 active.color.color.opacity(0.28),223 active.color.color.opacity(0.06),224 Color(.systemBackground),225 ],226 startPoint: .top,227 endPoint: .bottom228 )229 .ignoresSafeArea()230231 VStack(spacing: Spacing.l) {232 Spacer()233234 PrismMark()235 .frame(width: 170, height: 170)236237 Text("Prisme")238 .font(.system(size: 46, weight: .bold, design: .rounded))239240 Text("Un univers « \(active.name) » — cookies, sessions et empreinte y sont isolés du reste de vos vies.")241 .font(.callout)242 .foregroundStyle(.secondary)243 .multilineTextAlignment(.center)244 .padding(.horizontal, Spacing.xl * 2)245246 containerChips247 .padding(.top, Spacing.m)248249 Spacer()250 Spacer()251 }252 }253 }254255 private var containerChips: some View {256 HStack(spacing: Spacing.s) {257 ForEach(containers) { container in258 let isActive = container.id == active.id259 Button {260 withAnimation(.spring(duration: 0.3)) { onSelect(container.id) }261 } label: {262 HStack(spacing: Spacing.xs) {263 Image(systemName: container.symbol)264 .font(.caption.weight(.semibold))265 if isActive {266 Text(container.name)267 .font(.caption.weight(.semibold))268 .lineLimit(1)269 .fixedSize()270 }271 }272 .foregroundStyle(isActive ? .white : container.color.color)273 .padding(.horizontal, isActive ? Spacing.l : Spacing.m)274 .padding(.vertical, Spacing.m)275 .background {276 if isActive {277 Capsule().fill(container.color.gradient)278 } else {279 Capsule().fill(container.color.color.opacity(0.14))280 }281 }282 }283 .buttonStyle(.plain)284 }285 }286 .animation(.spring(duration: 0.3), value: active.id)287 }288}289