소스 검색

Improve per-app widgets layouts

Updates Widgets page to only use the green + add control, adds per-app widget layout variants (Photos/Gmail/etc), and keeps previews non-interactive while adding the desktop widget windows with the correct layouts.

Made-with: Cursor
huzaifahayat12 3 달 전
부모
커밋
d5c03df894

+ 125 - 135
google_apps/Widgets/DesktopWidgetView.swift

@@ -5,187 +5,177 @@ struct DesktopWidgetView: View {
     let size: WidgetSize
     var isPreview: Bool = false
 
+    private var actions: [WidgetAction] {
+        WidgetTemplates.actions(for: app, kind: .quickActions)
+    }
+
+    private var layout: WidgetLayoutConfig {
+        WidgetTemplates.layout(for: app, size: size)
+    }
+
     var body: some View {
         ZStack {
-            background
+            RoundedRectangle(cornerRadius: 22, style: .continuous)
+                .fill(Color.white)
+                .shadow(color: .black.opacity(0.12), radius: 10, x: 0, y: 7)
+                .overlay(
+                    RoundedRectangle(cornerRadius: 22, style: .continuous)
+                        .stroke(Color.black.opacity(0.10), lineWidth: 1)
+                )
 
             VStack(alignment: .leading, spacing: 12) {
-                header
-
-                switch size {
-                case .small:
-                    smallBody
-                case .medium:
-                    mediumBody
-                case .large:
-                    largeBody
+                switch layout.mode {
+                case .iconOnly(let title):
+                    iconOnly(title: title)
+
+                case .actionsRow(let maxActions, let columns):
+                    if layout.showHeader {
+                        header
+                    }
+                    rowActions(maxActions: maxActions, columns: columns)
+
+                case .actionsGrid(let columns, let maxActions):
+                    if layout.showHeader {
+                        header
+                    }
+                    gridActions(columns: columns, maxActions: maxActions)
                 }
-
-                Spacer(minLength: 0)
             }
-            .padding(14)
-        }
-        .cornerRadius(20)
-    }
-
-    private var background: some View {
-        ZStack {
-            LinearGradient(
-                colors: [Color.black.opacity(0.55), Color.white.opacity(0.04)],
-                startPoint: .topLeading,
-                endPoint: .bottomTrailing
-            )
-
-            RoundedRectangle(cornerRadius: 20, style: .continuous)
-                .fill(Color.white.opacity(0.03))
+            .padding(16)
         }
-        .overlay(
-            RoundedRectangle(cornerRadius: 20, style: .continuous)
-                .stroke(Color.white.opacity(0.10), lineWidth: 1)
-        )
+        .clipShape(RoundedRectangle(cornerRadius: 22, style: .continuous))
     }
 
     private var header: some View {
         HStack(spacing: 10) {
-            AppIconView(app: app, size: 30, showAppBackground: true, iconPaddingFactor: 0.1)
-            VStack(alignment: .leading, spacing: 1) {
+            AppIconView(app: app, size: 34, showAppBackground: false, iconPaddingFactor: 0.07)
+            VStack(alignment: .leading, spacing: 2) {
                 Text(app.name)
-                    .font(.system(size: 13, weight: .bold))
-                    .foregroundStyle(.white.opacity(0.94))
-                    .lineLimit(1)
-                Text(app.webURL?.host ?? "Quick access")
-                    .font(.system(size: 11, weight: .semibold))
-                    .foregroundStyle(.white.opacity(0.55))
+                    .font(.system(size: 16, weight: .bold))
+                    .foregroundStyle(.black.opacity(0.88))
                     .lineLimit(1)
             }
-            Spacer()
-        }
-    }
-
-    private var smallBody: some View {
-        VStack(alignment: .leading, spacing: 10) {
-            quickActionsRow
-            openButton
+            Spacer(minLength: 0)
         }
     }
 
-    private var mediumBody: some View {
-        VStack(alignment: .leading, spacing: 12) {
-            Text("Shortcuts")
-                .font(.system(size: 12, weight: .bold))
-                .foregroundStyle(.white.opacity(0.8))
-            quickActionsGrid(columns: 2)
-            openButton
-        }
-    }
+    @ViewBuilder
+    private func iconOnly(title: String) -> some View {
+        let fontSize: CGFloat = {
+            switch size {
+            case .small: return 16
+            case .medium: return 22
+            case .large: return 18
+            }
+        }()
 
-    private var largeBody: some View {
-        VStack(alignment: .leading, spacing: 12) {
-            Text("Shortcuts")
-                .font(.system(size: 12, weight: .bold))
-                .foregroundStyle(.white.opacity(0.8))
-            quickActionsGrid(columns: 3)
-            openButton
+        let content = VStack(spacing: 10) {
+            AppIconView(app: app, size: 44, showAppBackground: false, iconPaddingFactor: 0.05)
+            Text(title)
+                .font(.system(size: fontSize, weight: .bold))
+                .foregroundStyle(.black.opacity(0.88))
+                .lineLimit(1)
         }
-    }
 
-    private var quickActionsRow: some View {
-        HStack(spacing: 10) {
-            quickChip(title: "Compose", systemImage: "square.and.pencil")
-            quickChip(title: "Inbox", systemImage: "tray")
-            quickChip(title: "Starred", systemImage: "star")
+        if isPreview || app.webURL == nil {
+            content
+        } else {
+            Button(action: openBase) {
+                content
+            }
+            .buttonStyle(.plain)
         }
     }
 
-    private func quickActionsGrid(columns: Int) -> some View {
-        let items: [(String, String)] = [
-            ("Compose", "square.and.pencil"),
-            ("Inbox", "tray"),
-            ("Important", "exclamationmark.circle"),
-            ("Starred", "star"),
-            ("Sent", "paperplane"),
-            ("Drafts", "doc.plaintext"),
-            ("Spam", "xmark.circle"),
-            ("Trash", "trash"),
-        ]
-
-        return LazyVGrid(columns: Array(repeating: GridItem(.flexible(), spacing: 10), count: columns), spacing: 10) {
-            ForEach(Array(items.enumerated()), id: \.offset) { _, item in
-                quickTile(title: item.0, systemImage: item.1)
+    private func rowActions(maxActions: Int, columns: Int) -> some View {
+        let prefix = actions.prefix(maxActions)
+        return HStack(spacing: 10) {
+            ForEach(prefix) { action in
+                actionChip(action: action)
             }
         }
     }
 
-    private func quickChip(title: String, systemImage: String) -> some View {
-        HStack(spacing: 6) {
-            Image(systemName: systemImage)
-                .font(.system(size: 11, weight: .semibold))
-                .foregroundStyle(.white.opacity(0.82))
-            Text(title)
-                .font(.system(size: 11, weight: .bold))
-                .foregroundStyle(.white.opacity(0.88))
+    private func gridActions(columns: Int, maxActions: Int) -> some View {
+        let grid = Array(repeating: GridItem(.flexible(minimum: 0), spacing: 10), count: columns)
+        return LazyVGrid(columns: grid, spacing: 10) {
+            ForEach(actions.prefix(maxActions)) { action in
+                actionTile(action: action)
+            }
         }
-        .padding(.horizontal, 10)
-        .padding(.vertical, 8)
-        .background(
-            Capsule(style: .continuous)
-                .fill(Color.white.opacity(0.08))
-        )
-        .overlay(
-            Capsule(style: .continuous)
-                .stroke(Color.white.opacity(0.10), lineWidth: 1)
-        )
     }
 
-    private func quickTile(title: String, systemImage: String) -> some View {
-        HStack(spacing: 10) {
-            Image(systemName: systemImage)
-                .font(.system(size: 12, weight: .semibold))
-                .foregroundStyle(.white.opacity(0.85))
-            Text(title)
-                .font(.system(size: 12, weight: .bold))
-                .foregroundStyle(.white.opacity(0.9))
-                .lineLimit(1)
-            Spacer(minLength: 0)
+    private func actionChip(action: WidgetAction) -> some View {
+        Button(action: { open(action: action) }) {
+            HStack(spacing: 8) {
+                Image(systemName: action.systemImage)
+                    .font(.system(size: 12, weight: .semibold))
+                    .foregroundStyle(.black.opacity(0.62))
+                Text(action.title)
+                    .font(.system(size: 12, weight: .bold))
+                    .foregroundStyle(.black.opacity(0.86))
+                    .lineLimit(1)
+            }
+            .padding(.horizontal, 12)
+            .padding(.vertical, 9)
+            .background(Color.white)
+            .overlay(
+                RoundedRectangle(cornerRadius: 999, style: .continuous)
+                    .stroke(Color.black.opacity(0.12), lineWidth: 1)
+            )
         }
-        .padding(.horizontal, 12)
-        .padding(.vertical, 10)
-        .background(
-            RoundedRectangle(cornerRadius: 14, style: .continuous)
-                .fill(Color.white.opacity(0.06))
-        )
-        .overlay(
-            RoundedRectangle(cornerRadius: 14, style: .continuous)
-                .stroke(Color.white.opacity(0.08), lineWidth: 1)
-        )
-        .contentShape(RoundedRectangle(cornerRadius: 14, style: .continuous))
+        .buttonStyle(.plain)
+        .disabled(isPreview || app.webURL == nil)
+        .opacity((isPreview || app.webURL == nil) ? 0.6 : 1)
     }
 
-    private var openButton: some View {
-        Button(action: openApp) {
-            HStack {
-                Text("Open")
-                    .font(.system(size: 13, weight: .bold))
-                Spacer()
-                Image(systemName: "arrow.up.right")
+    private func actionTile(action: WidgetAction) -> some View {
+        Button(action: { open(action: action) }) {
+            HStack(spacing: 10) {
+                Image(systemName: action.systemImage)
+                    .font(.system(size: 12, weight: .semibold))
+                    .foregroundStyle(.black.opacity(0.62))
+                Text(action.title)
                     .font(.system(size: 12, weight: .bold))
+                    .foregroundStyle(.black.opacity(0.86))
+                    .lineLimit(1)
+                Spacer(minLength: 0)
             }
-            .foregroundStyle(.white.opacity(0.92))
             .padding(.horizontal, 12)
             .padding(.vertical, 10)
-            .background(
+            .background(Color.white)
+            .overlay(
                 RoundedRectangle(cornerRadius: 14, style: .continuous)
-                    .fill(Color.blue.opacity(0.75))
+                    .stroke(Color.black.opacity(0.12), lineWidth: 1)
             )
         }
         .buttonStyle(.plain)
         .disabled(isPreview || app.webURL == nil)
-        .opacity((isPreview || app.webURL == nil) ? 0.65 : 1)
+        .opacity((isPreview || app.webURL == nil) ? 0.6 : 1)
     }
 
-    private func openApp() {
+    private func openBase() {
         guard !isPreview, let url = app.webURL else { return }
         InAppBrowserWindowManager.shared.open(url: url, title: app.name)
     }
+
+    private func open(action: WidgetAction) {
+        guard !isPreview, let base = app.webURL else { return }
+        let target = resolvedURL(base: base, actionPath: action.urlPath)
+        InAppBrowserWindowManager.shared.open(url: target, title: app.name)
+    }
+
+    private func resolvedURL(base: URL, actionPath: String?) -> URL {
+        guard let actionPath, !actionPath.isEmpty else { return base }
+        if actionPath.hasPrefix("http://") || actionPath.hasPrefix("https://") {
+            return URL(string: actionPath) ?? base
+        }
+        if actionPath.hasPrefix("/") {
+            var c = URLComponents(url: base, resolvingAgainstBaseURL: false)
+            c?.path = actionPath
+            return c?.url ?? base
+        }
+        return URL(string: actionPath, relativeTo: base)?.absoluteURL ?? base
+    }
 }
 

+ 8 - 31
google_apps/Widgets/WidgetPreviewCard.swift

@@ -5,37 +5,14 @@ struct WidgetPreviewCard: View {
     let size: WidgetSize
 
     var body: some View {
-        VStack(alignment: .leading, spacing: 12) {
-            HStack(spacing: 10) {
-                AppIconView(app: app, size: 34, showAppBackground: true, iconPaddingFactor: 0.1)
-                VStack(alignment: .leading, spacing: 2) {
-                    Text(app.name)
-                        .font(.system(size: 15, weight: .bold))
-                        .foregroundStyle(.white.opacity(0.94))
-                    Text(size.title + " Widget")
-                        .font(.system(size: 12, weight: .semibold))
-                        .foregroundStyle(.white.opacity(0.6))
-                }
-                Spacer()
-            }
-
-            DesktopWidgetView(app: app, size: size, isPreview: true)
-                .frame(width: previewSize.width, height: previewSize.height)
-                .clipShape(RoundedRectangle(cornerRadius: 18, style: .continuous))
-                .overlay(
-                    RoundedRectangle(cornerRadius: 18, style: .continuous)
-                        .stroke(Color.white.opacity(0.10), lineWidth: 1)
-                )
-        }
-        .padding(14)
-        .background(
-            RoundedRectangle(cornerRadius: 18, style: .continuous)
-                .fill(Color.white.opacity(0.04))
-        )
-        .overlay(
-            RoundedRectangle(cornerRadius: 18, style: .continuous)
-                .stroke(Color.white.opacity(0.06), lineWidth: 1)
-        )
+        DesktopWidgetView(app: app, size: size, isPreview: true)
+            .frame(width: previewSize.width, height: previewSize.height)
+            .allowsHitTesting(false)
+            .clipShape(RoundedRectangle(cornerRadius: 18, style: .continuous))
+            .overlay(
+                RoundedRectangle(cornerRadius: 18, style: .continuous)
+                    .stroke(Color.white.opacity(0.10), lineWidth: 1)
+            )
     }
 
     private var previewSize: CGSize {

+ 243 - 0
google_apps/Widgets/WidgetTemplates.swift

@@ -0,0 +1,243 @@
+import Foundation
+
+struct WidgetAction: Identifiable, Hashable {
+    let id: String
+    let title: String
+    let systemImage: String
+    /// If nil, opens the app base URL.
+    let urlPath: String?
+
+    init(id: String, title: String, systemImage: String, urlPath: String? = nil) {
+        self.id = id
+        self.title = title
+        self.systemImage = systemImage
+        self.urlPath = urlPath
+    }
+}
+
+enum WidgetTemplateKind: String, CaseIterable, Codable, Hashable {
+    case quickActions
+}
+
+enum WidgetLayoutMode: Hashable {
+    case iconOnly(title: String)
+    case actionsRow(maxActions: Int, columns: Int = 3)
+    case actionsGrid(columns: Int, maxActions: Int)
+}
+
+struct WidgetLayoutConfig: Hashable {
+    let mode: WidgetLayoutMode
+    let showHeader: Bool
+}
+
+enum WidgetTemplates {
+    static func actions(for app: LauncherApp, kind: WidgetTemplateKind = .quickActions) -> [WidgetAction] {
+        guard kind == .quickActions else { return defaultActions(for: app) }
+
+        let name = app.name.lowercased()
+        let host = app.webURL?.host?.lowercased() ?? ""
+
+        if name.contains("photos") || host.contains("photos.google.com") {
+            // Matches the example UI.
+            return [
+                WidgetAction(id: "photos", title: "Photos", systemImage: "photo.on.rectangle", urlPath: "/"),
+                WidgetAction(id: "albums", title: "Albums", systemImage: "rectangle.stack", urlPath: "/albums"),
+                WidgetAction(id: "explore", title: "Explore", systemImage: "magnifyingglass", urlPath: "/search"),
+                WidgetAction(id: "archive", title: "Archive", systemImage: "archivebox", urlPath: "/archive"),
+                WidgetAction(id: "sharing", title: "Sharing", systemImage: "person.2", urlPath: "/sharing"),
+                WidgetAction(id: "locked", title: "Locked Folder", systemImage: "lock", urlPath: "/lockedfolder"),
+                WidgetAction(id: "favorites", title: "Favorites", systemImage: "star", urlPath: "/favorites"),
+                WidgetAction(id: "trash", title: "Trash", systemImage: "trash", urlPath: "/trash"),
+            ]
+        }
+
+        if name.contains("gmail") || host.contains("mail.google.com") {
+            return [
+                WidgetAction(id: "compose", title: "Compose", systemImage: "square.and.pencil", urlPath: "/mail/u/0/#inbox?compose=new"),
+                WidgetAction(id: "inbox", title: "Inbox", systemImage: "tray", urlPath: "/mail/u/0/#inbox"),
+                WidgetAction(id: "starred", title: "Starred", systemImage: "star", urlPath: "/mail/u/0/#starred"),
+                WidgetAction(id: "sent", title: "Sent", systemImage: "paperplane", urlPath: "/mail/u/0/#sent"),
+                WidgetAction(id: "drafts", title: "Drafts", systemImage: "doc.plaintext", urlPath: "/mail/u/0/#drafts"),
+                WidgetAction(id: "snoozed", title: "Snoozed", systemImage: "clock", urlPath: "/mail/u/0/#snoozed"),
+                WidgetAction(id: "important", title: "Important", systemImage: "exclamationmark.circle", urlPath: "/mail/u/0/#imp"),
+                WidgetAction(id: "trash", title: "Trash", systemImage: "trash", urlPath: "/mail/u/0/#trash"),
+            ]
+        }
+
+        if name.contains("drive") || host.contains("drive.google.com") {
+            return [
+                WidgetAction(id: "myDrive", title: "My Drive", systemImage: "externaldrive", urlPath: "/drive/my-drive"),
+                WidgetAction(id: "recent", title: "Recent", systemImage: "clock.arrow.circlepath", urlPath: "/drive/recent"),
+                WidgetAction(id: "starred", title: "Starred", systemImage: "star", urlPath: "/drive/starred"),
+                WidgetAction(id: "shared", title: "Shared", systemImage: "person.2", urlPath: "/drive/shared-with-me"),
+                WidgetAction(id: "trash", title: "Trash", systemImage: "trash", urlPath: "/drive/trash"),
+                WidgetAction(id: "new", title: "New", systemImage: "plus", urlPath: "/drive/my-drive"),
+            ]
+        }
+
+        if name.contains("docs") || host.contains("docs.google.com") {
+            return [
+                WidgetAction(id: "newDoc", title: "New Doc", systemImage: "doc.badge.plus", urlPath: "/document/u/0/"),
+                WidgetAction(id: "recent", title: "Recent", systemImage: "clock.arrow.circlepath", urlPath: "/document/u/0/"),
+                WidgetAction(id: "templates", title: "Templates", systemImage: "square.grid.2x2", urlPath: "/document/u/0/"),
+                WidgetAction(id: "shared", title: "Shared", systemImage: "person.2", urlPath: "/document/u/0/"),
+            ]
+        }
+
+        if name.contains("sheets") || host.contains("sheets.google.com") {
+            return [
+                WidgetAction(id: "newSheet", title: "New Sheet", systemImage: "tablecells.badge.ellipsis", urlPath: "/spreadsheets/u/0/"),
+                WidgetAction(id: "recent", title: "Recent", systemImage: "clock.arrow.circlepath", urlPath: "/spreadsheets/u/0/"),
+                WidgetAction(id: "templates", title: "Templates", systemImage: "square.grid.2x2", urlPath: "/spreadsheets/u/0/"),
+            ]
+        }
+
+        if name.contains("calendar") || host.contains("calendar.google.com") {
+            return [
+                WidgetAction(id: "today", title: "Today", systemImage: "calendar", urlPath: "/calendar/u/0/r"),
+                WidgetAction(id: "create", title: "Create", systemImage: "plus.circle", urlPath: "/calendar/u/0/r/eventedit"),
+                WidgetAction(id: "schedule", title: "Schedule", systemImage: "list.bullet.rectangle", urlPath: "/calendar/u/0/r"),
+            ]
+        }
+
+        if name.contains("youtube") || host.contains("youtube.com") {
+            return [
+                WidgetAction(id: "home", title: "Home", systemImage: "house", urlPath: "/"),
+                WidgetAction(id: "subscriptions", title: "Subs", systemImage: "play.rectangle", urlPath: "/feed/subscriptions"),
+                WidgetAction(id: "library", title: "Library", systemImage: "books.vertical", urlPath: "/feed/library"),
+                WidgetAction(id: "history", title: "History", systemImage: "clock", urlPath: "/feed/history"),
+                WidgetAction(id: "watchLater", title: "Watch Later", systemImage: "bookmark", urlPath: "/playlist?list=WL"),
+                WidgetAction(id: "shorts", title: "Shorts", systemImage: "bolt", urlPath: "/shorts"),
+            ]
+        }
+
+        if name.contains("maps") || host.contains("maps.google.com") {
+            return [
+                WidgetAction(id: "search", title: "Search", systemImage: "magnifyingglass", urlPath: "/"),
+                WidgetAction(id: "directions", title: "Directions", systemImage: "arrow.triangle.turn.up.right.diamond", urlPath: "/"),
+                WidgetAction(id: "saved", title: "Saved", systemImage: "bookmark", urlPath: "/"),
+            ]
+        }
+
+        return defaultActions(for: app)
+    }
+
+    static func defaultActions(for app: LauncherApp) -> [WidgetAction] {
+        [
+            WidgetAction(id: "open", title: "Open", systemImage: "arrow.up.right", urlPath: nil),
+            WidgetAction(id: "search", title: "Search", systemImage: "magnifyingglass", urlPath: nil),
+            WidgetAction(id: "favorites", title: "Favorites", systemImage: "star", urlPath: nil),
+        ]
+    }
+
+    static func layout(for app: LauncherApp, size: WidgetSize) -> WidgetLayoutConfig {
+        let name = app.name.lowercased()
+        let host = app.webURL?.host?.lowercased() ?? ""
+        let isPhotos = name.contains("photos") || host.contains("photos.google.com")
+        let isGmail = name.contains("gmail") || host.contains("mail.google.com")
+        let isDrive = name.contains("drive") || host.contains("drive.google.com")
+        let isDocs = name.contains("docs") || host.contains("docs.google.com")
+        let isSheets = name.contains("sheets") || host.contains("sheets.google.com")
+        let isCalendar = name.contains("calendar") || host.contains("calendar.google.com")
+        let isYouTube = name.contains("youtube") || host.contains("youtube.com")
+        let isMaps = name.contains("maps") || host.contains("maps.google.com")
+
+        if isPhotos {
+            switch size {
+            case .small:
+                return WidgetLayoutConfig(mode: .iconOnly(title: "Photo"), showHeader: false)
+            case .medium:
+                return WidgetLayoutConfig(mode: .iconOnly(title: "Google Photo"), showHeader: false)
+            case .large:
+                return WidgetLayoutConfig(mode: .actionsGrid(columns: 2, maxActions: 8), showHeader: true)
+            }
+        }
+
+        if isGmail {
+            switch size {
+            case .small:
+                return WidgetLayoutConfig(mode: .actionsRow(maxActions: 3), showHeader: false)
+            case .medium:
+                return WidgetLayoutConfig(mode: .actionsGrid(columns: 2, maxActions: 6), showHeader: true)
+            case .large:
+                return WidgetLayoutConfig(mode: .actionsGrid(columns: 3, maxActions: 9), showHeader: true)
+            }
+        }
+
+        if isDrive {
+            switch size {
+            case .small:
+                return WidgetLayoutConfig(mode: .iconOnly(title: "Drive"), showHeader: false)
+            case .medium:
+                return WidgetLayoutConfig(mode: .actionsRow(maxActions: 3), showHeader: true)
+            case .large:
+                return WidgetLayoutConfig(mode: .actionsGrid(columns: 2, maxActions: 6), showHeader: true)
+            }
+        }
+
+        if isDocs {
+            switch size {
+            case .small:
+                return WidgetLayoutConfig(mode: .iconOnly(title: "Docs"), showHeader: false)
+            case .medium:
+                return WidgetLayoutConfig(mode: .actionsGrid(columns: 2, maxActions: 4), showHeader: true)
+            case .large:
+                return WidgetLayoutConfig(mode: .actionsGrid(columns: 3, maxActions: 6), showHeader: true)
+            }
+        }
+
+        if isSheets {
+            switch size {
+            case .small:
+                return WidgetLayoutConfig(mode: .iconOnly(title: "Sheets"), showHeader: false)
+            case .medium:
+                return WidgetLayoutConfig(mode: .actionsRow(maxActions: 3), showHeader: true)
+            case .large:
+                return WidgetLayoutConfig(mode: .actionsGrid(columns: 2, maxActions: 6), showHeader: true)
+            }
+        }
+
+        if isCalendar {
+            switch size {
+            case .small:
+                return WidgetLayoutConfig(mode: .actionsRow(maxActions: 2), showHeader: false)
+            case .medium:
+                return WidgetLayoutConfig(mode: .actionsGrid(columns: 2, maxActions: 3), showHeader: true)
+            case .large:
+                return WidgetLayoutConfig(mode: .actionsGrid(columns: 3, maxActions: 6), showHeader: true)
+            }
+        }
+
+        if isYouTube {
+            switch size {
+            case .small:
+                return WidgetLayoutConfig(mode: .iconOnly(title: "YouTube"), showHeader: false)
+            case .medium:
+                return WidgetLayoutConfig(mode: .actionsGrid(columns: 2, maxActions: 4), showHeader: true)
+            case .large:
+                return WidgetLayoutConfig(mode: .actionsGrid(columns: 3, maxActions: 6), showHeader: true)
+            }
+        }
+
+        if isMaps {
+            switch size {
+            case .small:
+                return WidgetLayoutConfig(mode: .actionsRow(maxActions: 3), showHeader: false)
+            case .medium:
+                return WidgetLayoutConfig(mode: .actionsGrid(columns: 2, maxActions: 4), showHeader: true)
+            case .large:
+                return WidgetLayoutConfig(mode: .actionsGrid(columns: 3, maxActions: 6), showHeader: true)
+            }
+        }
+
+        switch size {
+        case .small:
+            return WidgetLayoutConfig(mode: .actionsRow(maxActions: 3), showHeader: false)
+        case .medium:
+            return WidgetLayoutConfig(mode: .actionsGrid(columns: 2, maxActions: 6), showHeader: true)
+        case .large:
+            return WidgetLayoutConfig(mode: .actionsGrid(columns: 3, maxActions: 9), showHeader: true)
+        }
+    }
+}
+

+ 21 - 75
google_apps/Widgets/WidgetsRootView.swift

@@ -11,7 +11,6 @@ struct WidgetsRootView: View {
     @Binding var widgetLibraryIDsData: String
 
     @State private var query = ""
-    @State private var selectedSize: WidgetSize = .small
 
     private var filteredApps: [LauncherApp] {
         let q = query.trimmingCharacters(in: .whitespacesAndNewlines)
@@ -97,55 +96,29 @@ struct WidgetsRootView: View {
             if let selectedApp {
                 header(for: selectedApp)
 
-                HStack(spacing: 10) {
-                    Text("Widget Size")
-                        .font(.system(size: 13, weight: .semibold))
-                        .foregroundStyle(.white.opacity(0.8))
-                    Spacer()
-                    WidgetSizePicker(selected: $selectedSize)
-                }
-                .padding(.top, 4)
-
-                HStack(alignment: .top, spacing: 14) {
-                    WidgetPreviewCard(app: selectedApp, size: selectedSize)
-                        .frame(maxWidth: 560)
-                    Spacer(minLength: 0)
-                }
-
-                HStack(spacing: 10) {
-                    Button(action: {
-                        addToDesktop(app: selectedApp, size: selectedSize)
-                    }) {
-                        Text("Add to Desktop")
-                            .font(.system(size: 14, weight: .bold))
-                            .foregroundStyle(.white)
-                            .padding(.horizontal, 14)
-                            .padding(.vertical, 10)
-                            .background(
-                                RoundedRectangle(cornerRadius: 14, style: .continuous)
-                                    .fill(Color.blue.opacity(0.9))
-                            )
-                    }
-                    .buttonStyle(.plain)
+                Text("Widget")
+                    .font(.system(size: 13, weight: .semibold))
+                    .foregroundStyle(.white.opacity(0.75))
+                    .padding(.top, 4)
 
-                    Button(action: {
-                        DesktopWidgetWindowManager.shared.removeAll(forAppID: selectedApp.id)
-                    }) {
-                        Text("Remove")
-                            .font(.system(size: 14, weight: .bold))
-                            .foregroundStyle(.white.opacity(0.9))
-                            .padding(.horizontal, 14)
-                            .padding(.vertical, 10)
-                            .background(
-                                RoundedRectangle(cornerRadius: 14, style: .continuous)
-                                    .fill(Color.white.opacity(0.08))
-                            )
+                HStack(alignment: .top, spacing: 18) {
+                    ForEach(WidgetSize.allCases, id: \.self) { size in
+                        WidgetPreviewCard(app: selectedApp, size: size)
+                            .overlay(alignment: .topTrailing) {
+                                Button(action: { addToDesktop(app: selectedApp, size: size) }) {
+                                    Image(systemName: "plus")
+                                        .font(.system(size: 12, weight: .bold))
+                                        .foregroundStyle(.black.opacity(0.85))
+                                        .frame(width: 26, height: 26)
+                                        .background(Circle().fill(Color.green.opacity(0.95)))
+                                        .shadow(color: .black.opacity(0.28), radius: 10, x: 0, y: 6)
+                                }
+                                .buttonStyle(.plain)
+                                .offset(x: 10, y: -10)
+                            }
                     }
-                    .buttonStyle(.plain)
-
-                    Spacer()
+                    Spacer(minLength: 0)
                 }
-                .padding(.top, 2)
             } else {
                 emptyState
             }
@@ -172,7 +145,7 @@ struct WidgetsRootView: View {
                 Text(app.name)
                     .font(.system(size: 20, weight: .bold))
                     .foregroundStyle(.white.opacity(0.94))
-                Text("Choose a widget size and add it to your desktop.")
+                Text("Click the green + to add widgets to your desktop.")
                     .font(.system(size: 12.5, weight: .medium))
                     .foregroundStyle(.white.opacity(0.65))
             }
@@ -249,33 +222,6 @@ private struct WidgetSidebarRow: View {
     }
 }
 
-private struct WidgetSizePicker: View {
-    @Binding var selected: WidgetSize
-
-    var body: some View {
-        HStack(spacing: 8) {
-            ForEach(WidgetSize.allCases, id: \.self) { size in
-                Button(action: { selected = size }) {
-                    Text(size.title)
-                        .font(.system(size: 12, weight: .bold))
-                        .foregroundStyle(.white.opacity(selected == size ? 0.95 : 0.75))
-                        .padding(.horizontal, 10)
-                        .padding(.vertical, 6)
-                        .background(
-                            Capsule(style: .continuous)
-                                .fill(selected == size ? Color.white.opacity(0.14) : Color.white.opacity(0.06))
-                        )
-                        .overlay(
-                            Capsule(style: .continuous)
-                                .stroke(Color.white.opacity(selected == size ? 0.16 : 0.08), lineWidth: 1)
-                        )
-                }
-                .buttonStyle(.plain)
-            }
-        }
-    }
-}
-
 private func decodeUUIDSet(from dataString: String) -> Set<UUID> {
     guard !dataString.isEmpty, let data = dataString.data(using: .utf8) else { return [] }
     do {