mirror of
https://github.com/BigMarc/grammargem.git
synced 2026-10-13 17:01:55 +02:00
- Management app: sidebar window with Dashboard (usage insights), Devices (slot usage, deactivate this Mac), Writing Modes (custom + app-aware map), Dictionary, Snippets, Shortcuts, AI Model, Privacy, General, License, About. - New local stores: Preferences (launch-at-login, appearance), UsageStats (privacy-friendly local insights), CustomModesStore, SnippetStore. - License model now tracks device name + activation usage/limit for the Devices screen. - README rewritten as a conversion-focused product page (no internal cost/setup). - Legal entity set to FounderGem LLC across README, LICENSE, Info.plist, About. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
180 lines
5.7 KiB
Swift
180 lines
5.7 KiB
Swift
import SwiftUI
|
|
|
|
/// Brand accents for the app UI (mirrors the website CI).
|
|
enum GG {
|
|
static let emerald = Color(red: 0x0E / 255, green: 0x7C / 255, blue: 0x5A / 255)
|
|
static let gold = Color(red: 0xC9 / 255, green: 0xA2 / 255, blue: 0x4B / 255)
|
|
}
|
|
|
|
/// Sections of the management window.
|
|
enum MainSection: String, CaseIterable, Identifiable {
|
|
case dashboard, devices, license
|
|
case modes, dictionary, snippets
|
|
case shortcuts, model, privacy, general
|
|
case about
|
|
|
|
var id: String { rawValue }
|
|
|
|
var title: String {
|
|
switch self {
|
|
case .dashboard: return "Dashboard"
|
|
case .devices: return "Devices"
|
|
case .license: return "License"
|
|
case .modes: return "Writing Modes"
|
|
case .dictionary: return "Dictionary"
|
|
case .snippets: return "Snippets"
|
|
case .shortcuts: return "Shortcuts"
|
|
case .model: return "AI Model"
|
|
case .privacy: return "Privacy"
|
|
case .general: return "General"
|
|
case .about: return "About"
|
|
}
|
|
}
|
|
|
|
var systemImage: String {
|
|
switch self {
|
|
case .dashboard: return "square.grid.2x2"
|
|
case .devices: return "laptopcomputer"
|
|
case .license: return "key"
|
|
case .modes: return "slider.horizontal.3"
|
|
case .dictionary: return "character.book.closed"
|
|
case .snippets: return "text.badge.plus"
|
|
case .shortcuts: return "command"
|
|
case .model: return "cpu"
|
|
case .privacy: return "lock.shield"
|
|
case .general: return "gearshape"
|
|
case .about: return "info.circle"
|
|
}
|
|
}
|
|
|
|
/// Sidebar groups, in order.
|
|
static let groups: [(String, [MainSection])] = [
|
|
("Overview", [.dashboard]),
|
|
("Account", [.devices, .license]),
|
|
("Writing", [.modes, .dictionary, .snippets]),
|
|
("App", [.shortcuts, .model, .privacy, .general]),
|
|
("", [.about]),
|
|
]
|
|
}
|
|
|
|
/// The full GrammaGem management window: a clean sidebar + detail layout.
|
|
struct MainWindowView: View {
|
|
@State private var selection: MainSection?
|
|
|
|
init(initialSection: MainSection = .dashboard) {
|
|
_selection = State(initialValue: initialSection)
|
|
}
|
|
|
|
var body: some View {
|
|
NavigationSplitView {
|
|
List(selection: $selection) {
|
|
ForEach(MainSection.groups, id: \.0) { group, items in
|
|
Section(group) {
|
|
ForEach(items) { section in
|
|
Label(section.title, systemImage: section.systemImage)
|
|
.tag(section)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
.navigationSplitViewColumnWidth(min: 198, ideal: 216, max: 260)
|
|
.listStyle(.sidebar)
|
|
} detail: {
|
|
detail(for: selection ?? .dashboard)
|
|
}
|
|
.tint(GG.emerald)
|
|
.frame(minWidth: 820, minHeight: 560)
|
|
}
|
|
|
|
@ViewBuilder
|
|
private func detail(for section: MainSection) -> some View {
|
|
switch section {
|
|
case .dashboard: DashboardView()
|
|
case .devices: DevicesView()
|
|
case .license: LicenseView()
|
|
case .modes: WritingModesView()
|
|
case .dictionary: DictionaryView()
|
|
case .snippets: SnippetsView()
|
|
case .shortcuts: ShortcutsView()
|
|
case .model: ModelView()
|
|
case .privacy: PrivacyView()
|
|
case .general: GeneralView()
|
|
case .about: AboutView()
|
|
}
|
|
}
|
|
}
|
|
|
|
// MARK: - Reusable layout
|
|
|
|
/// A scrollable detail page with a title/subtitle header and a max content width.
|
|
struct DetailScaffold<Content: View>: View {
|
|
let title: String
|
|
var subtitle: String? = nil
|
|
@ViewBuilder var content: Content
|
|
|
|
var body: some View {
|
|
ScrollView {
|
|
VStack(alignment: .leading, spacing: 22) {
|
|
VStack(alignment: .leading, spacing: 4) {
|
|
Text(title).font(.largeTitle.bold())
|
|
if let subtitle {
|
|
Text(subtitle).font(.body).foregroundStyle(.secondary)
|
|
}
|
|
}
|
|
content
|
|
}
|
|
.frame(maxWidth: 720, alignment: .leading)
|
|
.frame(maxWidth: .infinity, alignment: .leading)
|
|
.padding(28)
|
|
}
|
|
}
|
|
}
|
|
|
|
/// A soft card container used throughout the window.
|
|
struct Card<Content: View>: View {
|
|
var padding: CGFloat = 18
|
|
@ViewBuilder var content: Content
|
|
|
|
var body: some View {
|
|
VStack(alignment: .leading, spacing: 12) { content }
|
|
.padding(padding)
|
|
.frame(maxWidth: .infinity, alignment: .leading)
|
|
.background(Color(nsColor: .controlBackgroundColor), in: RoundedRectangle(cornerRadius: 12))
|
|
.overlay(
|
|
RoundedRectangle(cornerRadius: 12)
|
|
.stroke(Color.primary.opacity(0.07), lineWidth: 1)
|
|
)
|
|
}
|
|
}
|
|
|
|
/// A small labelled metric tile.
|
|
struct StatTile: View {
|
|
let value: String
|
|
let label: String
|
|
var systemImage: String? = nil
|
|
var tint: Color = .primary
|
|
|
|
var body: some View {
|
|
Card {
|
|
HStack(spacing: 8) {
|
|
if let systemImage {
|
|
Image(systemName: systemImage).foregroundStyle(tint)
|
|
}
|
|
Text(value).font(.system(.title, design: .rounded).weight(.semibold)).foregroundStyle(tint)
|
|
}
|
|
Text(label).font(.caption).foregroundStyle(.secondary)
|
|
}
|
|
}
|
|
}
|
|
|
|
/// A locked-feature badge used to surface upgrade triggers cleanly.
|
|
struct UpgradeBadge: View {
|
|
var body: some View {
|
|
Label("Lifetime", systemImage: "lock.fill")
|
|
.font(.caption2.weight(.semibold))
|
|
.padding(.horizontal, 7).padding(.vertical, 3)
|
|
.background(GG.gold.opacity(0.18), in: Capsule())
|
|
.foregroundStyle(GG.gold)
|
|
}
|
|
}
|