Add Mac Home and document windows with native spreadsheet editing
34 files changed,
+1507
−49
docs/superpowers/specs/2026-09-20-mac-document-windows.md+69 −0native/apple/Package.swift+4 −3native/apple/Sources/SheetsApp/SheetsAccountWindow.swift+26 −0native/apple/Sources/SheetsApp/SheetsApp.swift+16 −3native/apple/Sources/SheetsApp/SheetsDocumentWindow.swift+23 −0native/apple/Sources/SheetsApp/SheetsWindowCommands.swift+32 −0native/apple/Sources/SheetsApp/SheetsWindows.swift+47 −0native/apple/Sources/SheetsKit/Resources/ar.lproj/Mac.strings+70 −0native/apple/Sources/SheetsKit/Resources/bn.lproj/Mac.strings+70 −0native/apple/Sources/SheetsKit/Resources/en.lproj/Mac.strings+70 −0native/apple/Sources/SheetsKit/Resources/es.lproj/Mac.strings+70 −0native/apple/Sources/SheetsKit/Resources/ga.lproj/Mac.strings+70 −0native/apple/Sources/SheetsKit/Resources/hi.lproj/Mac.strings+70 −0native/apple/Sources/SheetsKit/Resources/ja.lproj/Mac.strings+70 −0native/apple/Sources/SheetsKit/Resources/pt.lproj/Mac.strings+70 −0native/apple/Sources/SheetsKit/Resources/ru.lproj/Mac.strings+70 −0native/apple/Sources/SheetsKit/Resources/vi.lproj/Mac.strings+70 −0native/apple/Sources/SheetsKit/Resources/yue.lproj/Mac.strings+70 −0native/apple/Sources/SheetsKit/Resources/zh-Hans.lproj/Mac.strings+70 −0native/apple/Sources/SheetsKit/SheetAutoSize.swift+18 −0native/apple/Sources/SheetsKit/SheetCell.swift+47 −5native/apple/Sources/SheetsKit/SheetEditorContext.swift+9 −0native/apple/Sources/SheetsKit/SheetFiles.swift+3 −1native/apple/Sources/SheetsKit/SheetGrid.swift+79 −10native/apple/Sources/SheetsKit/SheetModel.swift+8 −6native/apple/Sources/SheetsKit/SheetPanels.swift+2 −1native/apple/Sources/SheetsKit/SheetTable.swift+1 −1native/apple/Sources/SheetsKit/SheetThumbnail.swift+4 −2native/apple/Sources/SheetsKit/SheetsDocumentMenu.swift+24 −0native/apple/Sources/SheetsKit/SheetsEditorCommands.swift+61 −0native/apple/Sources/SheetsKit/SheetsMacStrings.swift+5 −0native/apple/Sources/SheetsKit/SheetsWorkspace.swift+91 −17native/apple/Tests/SheetsKitTests/SheetNativeInteractionTests.swift+71 −0native/apple/Tests/SheetsWindowTests/WindowRoutingTests.swift+27 −0
CI passed — run details
diff --git a/docs/superpowers/specs/2026-09-20-mac-document-windows.md b/docs/superpowers/specs/2026-09-20-mac-document-windows.md| new file mode 100644 |
| index 0000000..4de6ef9 |
| --- /dev/null |
| +++ b/docs/superpowers/specs/2026-09-20-mac-document-windows.md |
| @@ -0,0 +1,69 @@ |
| +# Mac document windows — 2026-09-20 |
| + |
| +Paul requests a separate Home launcher and document windows for Docs and Sheets. |
| +This extends the native Apple specs; editors remain native and server-backed. |
| + |
| +Home is a singleton with a Recent sidebar and searchable thumbnail grid. Templates |
| +are deferred until template creation exists. Each open document has an independent |
| +model, live stream, selection and save queue. Window identity includes the account |
| +and document; opening an existing document focuses it. Document/session state remains in memory; credentials retain the existing |
| +Keychain handling and are never encoded in scene restoration values. Missing restored sessions return |
| +the user to Home. File > Recent and Window > Home remain available with no editor. |
| +Recent entries are held in memory for this iteration, scoped to the account session. |
| + |
| +Mac document windows have no app navigation sidebar. Existing editing and file |
| +actions move into menu-bar commands targeted at the focused scene. Standard native |
| +text responder actions remain available in text fields. Docs adds a document-only |
| +inspector and zoom starting at 100%; zoom changes display scale, never text wrapping. |
| +No server page-settings API exists: inspector exposes existing personal layout |
| +settings and document information, without pretending to persist new paper settings. |
| + |
| +Sheets edits in the selected cell, preserving drafts on failed save and respecting |
| +existing write serialization. Header buttons select whole rows/columns. Double-click |
| +on column boundaries autosizes using displayed text/font measurements and existing |
| +size endpoint; menu alternative provided. Remove only the grid's outer focus effect, |
| +retain cell selection and keyboard focus. Enter/Tab commit; Escape discards. |
| + |
| +Docs rendering investigation compares web CSS/font and page geometry against TextKit. |
| +Use identical bundled fonts, logical width, sizes, paragraph spacing and line height. |
| +Native optimistic edits must not be overwritten by stale projection generations or |
| +IME composition. Avoid unnecessary complete attributed storage replacement and |
| +selection resets; preserve model render acknowledgement. Exact cross-engine layout |
| +parity requires measured fixture verification, not a compilation claim. |
| + |
| +Validate Mac builds, existing native tests and focused regression cases; exercise |
| +window/menu targeting and editing where UI automation is available. Build iOS to |
| +check shared code. No merge, deployment or app publication is part of this change. |
| + |
| +Review: GPT-6 Astra (high), 2026-09-20. Required invariants incorporated: |
| +window keys combine canonical origin, account identity and document identity; |
| +clients are immutable per window. Sign out is unavailable until that account's |
| +windows close; switching the launcher account leaves those windows intact. |
| +Close/quit guards cover in-flight, queued, failed and composing edits. Window |
| +registry lifecycle ends only after the actual guarded close. Edit commands obey |
| +the first responder, not merely the focused scene. Render reconciliation compares |
| +actual optimistic text storage, updates CRDT metadata and acknowledges visually |
| +equal generations. Zoom keeps logical layout independent of magnified viewport. |
| + |
| +## Verification and remaining coverage |
| + |
| +Implementation reviewed by GPT-6 Astra (high), including a final account-isolation |
| +and localization review. Both native Mac test suites and iOS simulator builds pass. |
| +A synthetic AppKit interaction fixture exercises Sheets typing successive characters, |
| +failed-save draft retention, Escape, and whole-row/column selection. A live SwiftUI |
| +scene probe verified File actions and Window > Home after closing Home. |
| + |
| +Docs regression tests cover minimal text-storage reconciliation, metadata-only |
| +updates and fixed logical page width. Deliberately restoring full storage replacement |
| +and viewport-dependent width made the corresponding tests fail. A synthetic WebKit |
| +fixture matches native heading, bold and body line breaks and body line spacing. |
| + |
| +This verifies the measured typography cases, not universal browser/TextKit parity. |
| +Complex lists, inline code, mixed styles, margin collapsing and multi-page pagination |
| +need a larger shared layout fixture suite before exact parity can be claimed. The |
| +existing native line-based page breaking and web block-based pagination differ. |
| +That remaining work is triggered by the requested exact cross-platform typesetting |
| +acceptance check; it is not considered complete by this branch. |
| + |
| +Builds are local ad-hoc Mac applications, not signed distribution or TestFlight |
| +releases. No server deployment, merge or publication was performed. |
diff --git a/native/apple/Package.swift b/native/apple/Package.swift| index adb01bd..1b1d410 100644 |
| --- a/native/apple/Package.swift |
| +++ b/native/apple/Package.swift |
| @@ -5,10 +5,11 @@ import Foundation |
| let sharedNative: Package.Dependency = ProcessInfo.processInfo.environment["OFICINA_NATIVE_PACKAGE_PATH"].map { .package(name: "native", path: $0) } |
| ?? .package(url: "https://amadan.net/rastrillo/native", revision: "229525909e1d6ac3b6a353d1c4d022798b0e9a24") |
| -let package = Package(name: "SheetsNative", platforms: [.macOS(.v26), .iOS(.v26)], |
| +let package = Package(name: "SheetsNative", defaultLocalization: "en", platforms: [.macOS(.v26), .iOS(.v26)], |
| products: [.library(name:"SheetsKit",targets:["SheetsKit"]), .executable(name:"SheetsMac",targets:["SheetsMac"]), .executable(name:"SheetsReview",targets:["SheetsReview"])], |
| dependencies:[sharedNative], |
| - targets:[.target(name:"SheetsKit",dependencies:[.product(name:"RastrilloApple",package:"native")]), |
| + targets:[.target(name:"SheetsKit",dependencies:[.product(name:"RastrilloApple",package:"native")], resources: [.process("Resources")]), |
| .executableTarget(name:"SheetsMac",dependencies:[.product(name: "RastrilloMacUpdates", package: "native", condition: .when(platforms: [.macOS])), "SheetsKit",.product(name:"RastrilloApple",package:"native")],path:"Sources/SheetsApp"), |
| .executableTarget(name:"SheetsReview",dependencies:["SheetsKit",.product(name:"RastrilloApple",package:"native")]), |
| - .testTarget(name:"SheetsKitTests",dependencies:["SheetsKit"])]) |
| + .testTarget(name:"SheetsKitTests",dependencies:["SheetsKit"]), |
| + .testTarget(name:"SheetsWindowTests",dependencies:["SheetsMac",.product(name:"RastrilloApple",package:"native")])]) |
diff --git a/native/apple/Sources/SheetsApp/SheetsAccountWindow.swift b/native/apple/Sources/SheetsApp/SheetsAccountWindow.swift| index ce074cb..4bbf1c9 100644 |
| --- a/native/apple/Sources/SheetsApp/SheetsAccountWindow.swift |
| +++ b/native/apple/Sources/SheetsApp/SheetsAccountWindow.swift |
| @@ -6,15 +6,41 @@ import SwiftUI |
| /// one window must never discard another window's unconfirmed edits. |
| struct SheetsAccountWindow: View { |
| @StateObject private var connection = NativeConnection(defaultServer: "https://sheets.oficina.works") |
| + #if os(macOS) |
| + @Bindable var windows: SheetsWindows |
| + @Environment(\.openWindow) private var openWindow |
| + #endif |
| var body: some View { |
| Group { |
| if let client = connection.client { |
| + #if os(macOS) |
| + if let account = connection.activeAccountID { |
| + SheetsWorkspace(client: client, accounts: connection.showAccounts, signOut: { |
| + guard connection.activeAccountID == account, windows.canSignOut(account) else { return } |
| + windows.entries.removeAll { $0.account == account } |
| + connection.signOut() |
| + }, canSignOut: windows.canSignOut(account), createRequested: $windows.createRequested, openDocument: { document, title in |
| + guard connection.activeAccountID == account else { return } |
| + let id = windows.reserve(account: account, document: document, title: title, client: client) |
| + openWindow(id: "document", value: id) |
| + }, updateRecents: { documents in |
| + guard connection.activeAccountID == account else { return } |
| + windows.updateRecents(documents, account: account, client: client) |
| + }) |
| + .id(account) |
| + } |
| + #else |
| SheetsWorkspace(client: client, accounts: connection.showAccounts, signOut: connection.signOut) |
| .id(connection.activeAccountID) |
| + #endif |
| } else { |
| NativeAccountsView("Oficina Sheets", systemImage: "tablecells", connection: connection) |
| } |
| }.onOpenURL(perform: connection.handleOpenURL) |
| + #if os(macOS) |
| + .focusedSceneValue(\.sheetsWindowAccount, connection.activeAccountID) |
| + .onChange(of: connection.activeAccountID, initial: true) { _, value in windows.activeAccount = value } |
| + #endif |
| .tint(.green) |
| #if os(macOS) |
| .frame(minWidth: 760, minHeight: 600) |
diff --git a/native/apple/Sources/SheetsApp/SheetsApp.swift b/native/apple/Sources/SheetsApp/SheetsApp.swift| index 5a3f64f..0f425da 100644 |
| --- a/native/apple/Sources/SheetsApp/SheetsApp.swift |
| +++ b/native/apple/Sources/SheetsApp/SheetsApp.swift |
| @@ -10,14 +10,27 @@ import SwiftUI |
| @State private var updater = NativeUpdater() |
| #endif |
| #if os(macOS) |
| + @State private var windows = SheetsWindows() |
| @NSApplicationDelegateAdaptor(SheetsApplicationDelegate.self) private var appDelegate |
| #endif |
| var body: some Scene { |
| - WindowGroup { SheetsAccountWindow() } |
| #if os(macOS) |
| - #if canImport(RastrilloMacUpdates) |
| - .commands { NativeUpdateCommands(updater: updater) } |
| + Window(sheetsMacString("Home"), id: "home") { SheetsAccountWindow(windows: windows) } |
| + .defaultSize(width: 1000, height: 760) |
| + .commands { |
| + SheetsWindowCommands(windows: windows) |
| + SheetsEditorCommands() |
| + #if canImport(RastrilloMacUpdates) |
| + NativeUpdateCommands(updater: updater) |
| #endif |
| + } |
| + WindowGroup("Sheets", id: "document", for: UUID.self) { $id in |
| + SheetsDocumentWindow(windows: windows, id: id) |
| + } |
| + .defaultSize(width: 1120, height: 820) |
| + .restorationBehavior(.disabled) |
| + #else |
| + WindowGroup { SheetsAccountWindow() } |
| #endif |
| } |
| } |
diff --git a/native/apple/Sources/SheetsApp/SheetsDocumentWindow.swift b/native/apple/Sources/SheetsApp/SheetsDocumentWindow.swift| new file mode 100644 |
| index 0000000..125b531 |
| --- /dev/null |
| +++ b/native/apple/Sources/SheetsApp/SheetsDocumentWindow.swift |
| @@ -0,0 +1,23 @@ |
| +#if os(macOS) |
| +import SheetsKit |
| +import SwiftUI |
| + |
| +struct SheetsDocumentWindow: View { |
| + let windows: SheetsWindows |
| + let id: UUID? |
| + @Environment(\.openWindow) private var openWindow |
| + var body: some View { |
| + if let entry = windows.entries.first(where: { $0.id == id }) { |
| + SheetsWorkspace(client: entry.client, signOut: {}, documentID: entry.document) |
| + .id(entry.id) |
| + .focusedSceneValue(\.sheetsWindowAccount, entry.account) |
| + .onAppear { windows.setOpen(entry.id, true) } |
| + .onDisappear { windows.setOpen(entry.id, false) } |
| + } else { |
| + ContentUnavailableView { Label(sheetsMacString("Open a document from Home"), systemImage: "house") } actions: { |
| + Button(sheetsMacString("Home")) { openWindow(id: "home") } |
| + } |
| + } |
| + } |
| +} |
| +#endif |
diff --git a/native/apple/Sources/SheetsApp/SheetsWindowCommands.swift b/native/apple/Sources/SheetsApp/SheetsWindowCommands.swift| new file mode 100644 |
| index 0000000..77b325f |
| --- /dev/null |
| +++ b/native/apple/Sources/SheetsApp/SheetsWindowCommands.swift |
| @@ -0,0 +1,32 @@ |
| +#if os(macOS) |
| +import SwiftUI |
| +import SheetsKit |
| + |
| +struct SheetsWindowCommands: Commands { |
| + let windows: SheetsWindows |
| + @FocusedValue(\.sheetsWindowAccount) private var account |
| + @Environment(\.openWindow) private var openWindow |
| + var body: some Commands { |
| + CommandGroup(replacing: .newItem) { |
| + Button(sheetsMacString("New spreadsheet")) { |
| + windows.createRequested = true |
| + openWindow(id: "home") |
| + }.keyboardShortcut("n") |
| + Menu(sheetsMacString("Recent")) { |
| + ForEach(windows.entries.filter { $0.account == (account ?? windows.activeAccount) }.prefix(20)) { entry in |
| + Button(entry.title) { |
| + windows.setOpen(entry.id, true) |
| + openWindow(id: "document", value: entry.id) |
| + } |
| + } |
| + if !windows.entries.contains(where: { $0.account == (account ?? windows.activeAccount) }) { |
| + Text(sheetsMacString("No recent documents")) |
| + } |
| + } |
| + Divider() |
| + SheetsDocumentMenu() |
| + } |
| + |
| + } |
| +} |
| +#endif |
diff --git a/native/apple/Sources/SheetsApp/SheetsWindows.swift b/native/apple/Sources/SheetsApp/SheetsWindows.swift| new file mode 100644 |
| index 0000000..d8e502b |
| --- /dev/null |
| +++ b/native/apple/Sources/SheetsApp/SheetsWindows.swift |
| @@ -0,0 +1,47 @@ |
| +#if os(macOS) |
| +import Foundation |
| +import Observation |
| +import RastrilloApple |
| +import SwiftUI |
| + |
| +/// Scene values are opaque IDs, never credentials. Reserve before opening a scene. |
| +@MainActor @Observable final class SheetsWindows { |
| + struct Entry: Identifiable { |
| + let id: UUID |
| + let account: String |
| + let document: UInt64 |
| + let client: NativeHTTP |
| + var title: String |
| + var isOpen = false |
| + } |
| + var entries: [Entry] = [] |
| + var activeAccount: String? |
| + var createRequested = false |
| + func reserve(account: String, document: UInt64, title: String, client: NativeHTTP) -> UUID { |
| + if let index = entries.firstIndex(where: { $0.account == account && $0.client.origin == client.origin && $0.document == document }) { |
| + var entry = entries.remove(at: index) |
| + entry.title = title |
| + entry.isOpen = true |
| + entries.insert(entry, at: 0) |
| + return entry.id |
| + } |
| + let entry = Entry(id: UUID(), account: account, document: document, client: client, title: title, isOpen: true) |
| + entries.insert(entry, at: 0) |
| + return entry.id |
| + } |
| + func updateRecents(_ documents: [(UInt64, String)], account: String, client: NativeHTTP) { |
| + for (document, title) in documents { |
| + if let index = entries.firstIndex(where: { $0.account == account && $0.client.origin == client.origin && $0.document == document }) { |
| + entries[index].title = title |
| + } else { |
| + entries.append(Entry(id: UUID(), account: account, document: document, client: client, title: title)) |
| + } |
| + } |
| + } |
| + func setOpen(_ id: UUID, _ value: Bool) { |
| + if let index = entries.firstIndex(where: { $0.id == id }) { entries[index].isOpen = value } |
| + } |
| + func canSignOut(_ account: String?) -> Bool { !entries.contains { $0.account == account && $0.isOpen } } |
| +} |
| +extension FocusedValues { @Entry var sheetsWindowAccount: String? } |
| +#endif |
diff --git a/native/apple/Sources/SheetsKit/Resources/ar.lproj/Mac.strings b/native/apple/Sources/SheetsKit/Resources/ar.lproj/Mac.strings| new file mode 100644 |
| index 0000000..1542309 |
| --- /dev/null |
| +++ b/native/apple/Sources/SheetsKit/Resources/ar.lproj/Mac.strings |
| @@ -0,0 +1,70 @@ |
| +"Accounts" = "الحسابات"; |
| +"Actual size" = "الحجم الفعلي"; |
| +"Alignment" = "المحاذاة"; |
| +"Block quote" = "كتلة اقتباس"; |
| +"Bold" = "عريض"; |
| +"Bullet list" = "قائمة نقطية"; |
| +"Cell contents" = "محتوى الخلية"; |
| +"Clear formatting" = "مسح التنسيق"; |
| +"Close this account’s document windows before signing out." = "أغلق نوافذ مستندات هذا الحساب قبل تسجيل الخروج."; |
| +"Create a document to get started." = "ستظهر المستندات التي تنشئها هنا، بدءًا بالأحدث."; |
| +"Docs" = "Docs"; |
| +"Document" = "المستند"; |
| +"Document inspector" = "خصائص المستند"; |
| +"Double-click to fit column to contents" = "انقر مرتين لملاءمة العمود للمحتوى"; |
| +"Edit %@" = "تحرير %@"; |
| +"Export" = "تصدير"; |
| +"Fit column to contents" = "ملاءمة العمود للمحتوى"; |
| +"Font" = "الخط"; |
| +"Format" = "تنسيق"; |
| +"Heading %d" = "العنوان %d"; |
| +"Home" = "الرئيسية"; |
| +"Indent" = "زيادة المسافة البادئة"; |
| +"Inline code" = "شيفرة"; |
| +"Inspector" = "خصائص المستند"; |
| +"Italic" = "مائل"; |
| +"Markdown…" = "Markdown"; |
| +"New document" = "مستند جديد"; |
| +"No documents" = "لا توجد مستندات بعد"; |
| +"No recent documents" = "لا توجد مستندات بعد"; |
| +"Normal" = "نص أساسي"; |
| +"Numbered list" = "قائمة مرقّمة"; |
| +"Open a document from Home" = "افتح مستندًا من الرئيسية"; |
| +"Outdent" = "تقليل المسافة البادئة"; |
| +"PDF…" = "PDF"; |
| +"Pages" = "صفحات"; |
| +"Paragraph style" = "نمط الفقرة"; |
| +"Print…" = "طباعة"; |
| +"Recent" = "الأخيرة"; |
| +"Recent documents" = "المستندات الأخيرة"; |
| +"Recent spreadsheets" = "جداول البيانات الأخيرة"; |
| +"Retry" = "إعادة المحاولة"; |
| +"Save" = "حفظ"; |
| +"Select column %@" = "تحديد العمود %@"; |
| +"Select row %d" = "تحديد الصف %d"; |
| +"Shared" = "تمت مشاركتها معك"; |
| +"Share…" = "مشاركة"; |
| +"Sign out" = "تسجيل الخروج"; |
| +"Size" = "الحجم"; |
| +"Zoom" = "التكبير"; |
| +"New spreadsheet" = "جدول بيانات جديد"; |
| +"Rename…" = "إعادة تسمية…"; |
| +"Export workbook…" = "تصدير المصنف…"; |
| +"Version history…" = "سجل الإصدارات…"; |
| +"Undo" = "تراجع"; |
| +"Redo" = "إعادة"; |
| +"Cut" = "قص"; |
| +"Copy" = "نسخ"; |
| +"Paste" = "لصق"; |
| +"Select all" = "تحديد الكل"; |
| +"Clear contents" = "مسح المحتويات"; |
| +"Fill down" = "تعبئة لأسفل"; |
| +"Fill right" = "تعبئة لليمين"; |
| +"Row and column sizes…" = "أحجام الصفوف والأعمدة…"; |
| +"Insert" = "إدراج"; |
| +"Sheet" = "ورقة"; |
| +"Sort and filter…" = "فرز وتصفية…"; |
| +"Comments…" = "تعليقات…"; |
| +"Refresh" = "تحديث"; |
| +"Import workbook" = "استيراد مصنف"; |
| +"Search spreadsheets" = "البحث في جداول البيانات"; |
diff --git a/native/apple/Sources/SheetsKit/Resources/bn.lproj/Mac.strings b/native/apple/Sources/SheetsKit/Resources/bn.lproj/Mac.strings| new file mode 100644 |
| index 0000000..c89ba83 |
| --- /dev/null |
| +++ b/native/apple/Sources/SheetsKit/Resources/bn.lproj/Mac.strings |
| @@ -0,0 +1,70 @@ |
| +"Accounts" = "অ্যাকাউন্ট"; |
| +"Actual size" = "প্রকৃত আকার"; |
| +"Alignment" = "প্রান্তিককরণ"; |
| +"Block quote" = "উদ্ধৃতির ব্লক"; |
| +"Bold" = "গাঢ়"; |
| +"Bullet list" = "বুলেটযুক্ত তালিকা"; |
| +"Cell contents" = "ঘরের বিষয়বস্তু"; |
| +"Clear formatting" = "বিন্যাস মুছুন"; |
| +"Close this account’s document windows before signing out." = "সাইন আউট করার আগে এই অ্যাকাউন্টের নথির উইন্ডোগুলি বন্ধ করুন।"; |
| +"Create a document to get started." = "আপনার তৈরি নথি এখানে দেখা যাবে, নতুনগুলো আগে।"; |
| +"Docs" = "Docs"; |
| +"Document" = "নথি"; |
| +"Document inspector" = "নথি পরিদর্শক"; |
| +"Double-click to fit column to contents" = "বিষয়বস্তু অনুযায়ী কলাম মাপতে দুবার ক্লিক করুন"; |
| +"Edit %@" = "%@ সম্পাদনা করুন"; |
| +"Export" = "রপ্তানি"; |
| +"Fit column to contents" = "বিষয়বস্তু অনুযায়ী কলাম মাপুন"; |
| +"Font" = "ফন্ট"; |
| +"Format" = "বিন্যাস"; |
| +"Heading %d" = "শিরোনাম %d"; |
| +"Home" = "হোম"; |
| +"Indent" = "ইন্ডেন্ট বাড়ান"; |
| +"Inline code" = "কোড"; |
| +"Inspector" = "নথি পরিদর্শক"; |
| +"Italic" = "তির্যক"; |
| +"Markdown…" = "Markdown"; |
| +"New document" = "নতুন নথি"; |
| +"No documents" = "এখনও কোনো নথি নেই"; |
| +"No recent documents" = "এখনও কোনো নথি নেই"; |
| +"Normal" = "মূল পাঠ্য"; |
| +"Numbered list" = "সংখ্যাযুক্ত তালিকা"; |
| +"Open a document from Home" = "হোম থেকে একটি নথি খুলুন"; |
| +"Outdent" = "ইন্ডেন্ট কমান"; |
| +"PDF…" = "PDF"; |
| +"Pages" = "পৃষ্ঠাভিত্তিক"; |
| +"Paragraph style" = "অনুচ্ছেদের শৈলী"; |
| +"Print…" = "প্রিন্ট করুন"; |
| +"Recent" = "সাম্প্রতিক"; |
| +"Recent documents" = "সাম্প্রতিক নথি"; |
| +"Recent spreadsheets" = "সাম্প্রতিক স্প্রেডশিট"; |
| +"Retry" = "আবার চেষ্টা করুন"; |
| +"Save" = "সংরক্ষণ করুন"; |
| +"Select column %@" = "কলাম %@ নির্বাচন করুন"; |
| +"Select row %d" = "সারি %d নির্বাচন করুন"; |
| +"Shared" = "আপনার সঙ্গে শেয়ার করা"; |
| +"Share…" = "শেয়ার করুন"; |
| +"Sign out" = "সাইন আউট করুন"; |
| +"Size" = "আকার"; |
| +"Zoom" = "জুম"; |
| +"New spreadsheet" = "নতুন স্প্রেডশিট"; |
| +"Rename…" = "নাম বদলান…"; |
| +"Export workbook…" = "ওয়ার্কবুক রপ্তানি করুন…"; |
| +"Version history…" = "সংস্করণের ইতিহাস…"; |
| +"Undo" = "পূর্বাবস্থায় ফেরান"; |
| +"Redo" = "আবার করুন"; |
| +"Cut" = "কাটুন"; |
| +"Copy" = "কপি করুন"; |
| +"Paste" = "পেস্ট করুন"; |
| +"Select all" = "সব নির্বাচন করুন"; |
| +"Clear contents" = "বিষয়বস্তু মুছুন"; |
| +"Fill down" = "নিচে পূরণ করুন"; |
| +"Fill right" = "ডানে পূরণ করুন"; |
| +"Row and column sizes…" = "সারি ও কলামের মাপ…"; |
| +"Insert" = "ঢোকান"; |
| +"Sheet" = "শিট"; |
| +"Sort and filter…" = "সাজান ও ফিল্টার করুন…"; |
| +"Comments…" = "মন্তব্য…"; |
| +"Refresh" = "রিফ্রেশ করুন"; |
| +"Import workbook" = "ওয়ার্কবুক আমদানি করুন"; |
| +"Search spreadsheets" = "স্প্রেডশিট খুঁজুন"; |
diff --git a/native/apple/Sources/SheetsKit/Resources/en.lproj/Mac.strings b/native/apple/Sources/SheetsKit/Resources/en.lproj/Mac.strings| new file mode 100644 |
| index 0000000..fa760a7 |
| --- /dev/null |
| +++ b/native/apple/Sources/SheetsKit/Resources/en.lproj/Mac.strings |
| @@ -0,0 +1,70 @@ |
| +"Accounts" = "Accounts"; |
| +"Actual size" = "Actual size"; |
| +"Alignment" = "Alignment"; |
| +"Block quote" = "Block quote"; |
| +"Bold" = "Bold"; |
| +"Bullet list" = "Bulleted list"; |
| +"Cell contents" = "Cell contents"; |
| +"Clear formatting" = "Clear formatting"; |
| +"Close this account’s document windows before signing out." = "Close this account’s document windows before signing out."; |
| +"Create a document to get started." = "Documents you create will show up here, newest first."; |
| +"Docs" = "Docs"; |
| +"Document" = "Document"; |
| +"Document inspector" = "Document inspector"; |
| +"Double-click to fit column to contents" = "Double-click to fit column to contents"; |
| +"Edit %@" = "Edit %@"; |
| +"Export" = "Export"; |
| +"Fit column to contents" = "Fit column to contents"; |
| +"Font" = "Font"; |
| +"Format" = "Format"; |
| +"Heading %d" = "Heading %d"; |
| +"Home" = "Home"; |
| +"Indent" = "Increase indent"; |
| +"Inline code" = "Code"; |
| +"Inspector" = "Document inspector"; |
| +"Italic" = "Italic"; |
| +"Markdown…" = "Markdown"; |
| +"New document" = "New document"; |
| +"No documents" = "No documents yet"; |
| +"No recent documents" = "No documents yet"; |
| +"Normal" = "Body text"; |
| +"Numbered list" = "Numbered list"; |
| +"Open a document from Home" = "Open a document from Home"; |
| +"Outdent" = "Decrease indent"; |
| +"PDF…" = "PDF"; |
| +"Pages" = "Paged"; |
| +"Paragraph style" = "Paragraph style"; |
| +"Print…" = "Print"; |
| +"Recent" = "Recent"; |
| +"Recent documents" = "Recent documents"; |
| +"Recent spreadsheets" = "Recent spreadsheets"; |
| +"Retry" = "Try again"; |
| +"Save" = "Save"; |
| +"Select column %@" = "Select column %@"; |
| +"Select row %d" = "Select row %d"; |
| +"Shared" = "Shared with you"; |
| +"Share…" = "Share"; |
| +"Sign out" = "Sign out"; |
| +"Size" = "Size"; |
| +"Zoom" = "Zoom"; |
| +"New spreadsheet" = "New spreadsheet"; |
| +"Rename…" = "Rename…"; |
| +"Export workbook…" = "Export workbook…"; |
| +"Version history…" = "Version history…"; |
| +"Undo" = "Undo"; |
| +"Redo" = "Redo"; |
| +"Cut" = "Cut"; |
| +"Copy" = "Copy"; |
| +"Paste" = "Paste"; |
| +"Select all" = "Select all"; |
| +"Clear contents" = "Clear contents"; |
| +"Fill down" = "Fill down"; |
| +"Fill right" = "Fill right"; |
| +"Row and column sizes…" = "Row and column sizes…"; |
| +"Insert" = "Insert"; |
| +"Sheet" = "Sheet"; |
| +"Sort and filter…" = "Sort and filter…"; |
| +"Comments…" = "Comments…"; |
| +"Refresh" = "Refresh"; |
| +"Import workbook" = "Import workbook"; |
| +"Search spreadsheets" = "Search spreadsheets"; |
diff --git a/native/apple/Sources/SheetsKit/Resources/es.lproj/Mac.strings b/native/apple/Sources/SheetsKit/Resources/es.lproj/Mac.strings| new file mode 100644 |
| index 0000000..da781bb |
| --- /dev/null |
| +++ b/native/apple/Sources/SheetsKit/Resources/es.lproj/Mac.strings |
| @@ -0,0 +1,70 @@ |
| +"Accounts" = "Cuentas"; |
| +"Actual size" = "Tamaño real"; |
| +"Alignment" = "Alineación"; |
| +"Block quote" = "Cita en bloque"; |
| +"Bold" = "Negrita"; |
| +"Bullet list" = "Lista con viñetas"; |
| +"Cell contents" = "Contenido de la celda"; |
| +"Clear formatting" = "Borrar formato"; |
| +"Close this account’s document windows before signing out." = "Cierra las ventanas de documentos de esta cuenta antes de cerrar sesión."; |
| +"Create a document to get started." = "Los documentos que crees aparecerán aquí, empezando por los más recientes."; |
| +"Docs" = "Docs"; |
| +"Document" = "Documento"; |
| +"Document inspector" = "Inspector del documento"; |
| +"Double-click to fit column to contents" = "Haz doble clic para ajustar la columna al contenido"; |
| +"Edit %@" = "Editar %@"; |
| +"Export" = "Exportar"; |
| +"Fit column to contents" = "Ajustar columna al contenido"; |
| +"Font" = "Fuente"; |
| +"Format" = "Formato"; |
| +"Heading %d" = "Título %d"; |
| +"Home" = "Inicio"; |
| +"Indent" = "Aumentar sangría"; |
| +"Inline code" = "Código"; |
| +"Inspector" = "Inspector del documento"; |
| +"Italic" = "Cursiva"; |
| +"Markdown…" = "Markdown"; |
| +"New document" = "Nuevo documento"; |
| +"No documents" = "Aún no hay documentos"; |
| +"No recent documents" = "Aún no hay documentos"; |
| +"Normal" = "Texto del cuerpo"; |
| +"Numbered list" = "Lista numerada"; |
| +"Open a document from Home" = "Abre un documento desde Inicio"; |
| +"Outdent" = "Reducir sangría"; |
| +"PDF…" = "PDF"; |
| +"Pages" = "Por páginas"; |
| +"Paragraph style" = "Estilo de párrafo"; |
| +"Print…" = "Imprimir"; |
| +"Recent" = "Recientes"; |
| +"Recent documents" = "Documentos recientes"; |
| +"Recent spreadsheets" = "Hojas de cálculo recientes"; |
| +"Retry" = "Reintentar"; |
| +"Save" = "Guardar"; |
| +"Select column %@" = "Seleccionar columna %@"; |
| +"Select row %d" = "Seleccionar fila %d"; |
| +"Shared" = "Compartidos contigo"; |
| +"Share…" = "Compartir"; |
| +"Sign out" = "Cerrar sesión"; |
| +"Size" = "Tamaño"; |
| +"Zoom" = "Zoom"; |
| +"New spreadsheet" = "Nueva hoja de cálculo"; |
| +"Rename…" = "Renombrar…"; |
| +"Export workbook…" = "Exportar libro…"; |
| +"Version history…" = "Historial de versiones…"; |
| +"Undo" = "Deshacer"; |
| +"Redo" = "Rehacer"; |
| +"Cut" = "Cortar"; |
| +"Copy" = "Copiar"; |
| +"Paste" = "Pegar"; |
| +"Select all" = "Seleccionar todo"; |
| +"Clear contents" = "Borrar contenido"; |
| +"Fill down" = "Rellenar hacia abajo"; |
| +"Fill right" = "Rellenar hacia la derecha"; |
| +"Row and column sizes…" = "Tamaños de filas y columnas…"; |
| +"Insert" = "Insertar"; |
| +"Sheet" = "Hoja"; |
| +"Sort and filter…" = "Ordenar y filtrar…"; |
| +"Comments…" = "Comentarios…"; |
| +"Refresh" = "Actualizar"; |
| +"Import workbook" = "Importar libro"; |
| +"Search spreadsheets" = "Buscar hojas de cálculo"; |
diff --git a/native/apple/Sources/SheetsKit/Resources/ga.lproj/Mac.strings b/native/apple/Sources/SheetsKit/Resources/ga.lproj/Mac.strings| new file mode 100644 |
| index 0000000..cb1dfa5 |
| --- /dev/null |
| +++ b/native/apple/Sources/SheetsKit/Resources/ga.lproj/Mac.strings |
| @@ -0,0 +1,70 @@ |
| +"Accounts" = "Cuntais"; |
| +"Actual size" = "Fíormhéid"; |
| +"Alignment" = "Ailíniú"; |
| +"Block quote" = "Athfhriotal i mbloc"; |
| +"Bold" = "Trom"; |
| +"Bullet list" = "Liosta le hurchair"; |
| +"Cell contents" = "Ábhar na cille"; |
| +"Clear formatting" = "Glan an fhormáidiú"; |
| +"Close this account’s document windows before signing out." = "Dún fuinneoga cáipéisí an chuntais seo sula síníonn tú amach."; |
| +"Create a document to get started." = "Beidh na doiciméid a chruthaíonn tú anseo, na cinn is nua ar dtús."; |
| +"Docs" = "Docs"; |
| +"Document" = "Cáipéis"; |
| +"Document inspector" = "Cigire cáipéise"; |
| +"Double-click to fit column to contents" = "Déchliceáil chun an colún a oiriúnú don ábhar"; |
| +"Edit %@" = "Cuir %@ in eagar"; |
| +"Export" = "Easpórtáil"; |
| +"Fit column to contents" = "Oiriúnaigh an colún don ábhar"; |
| +"Font" = "Cló"; |
| +"Format" = "Formáid"; |
| +"Heading %d" = "Ceannteideal %d"; |
| +"Home" = "Baile"; |
| +"Indent" = "Méadaigh an eangú"; |
| +"Inline code" = "Cód"; |
| +"Inspector" = "Cigire cáipéise"; |
| +"Italic" = "Iodálach"; |
| +"Markdown…" = "Markdown"; |
| +"New document" = "Doiciméad nua"; |
| +"No documents" = "Níl aon doiciméid ann fós"; |
| +"No recent documents" = "Níl aon doiciméid ann fós"; |
| +"Normal" = "Téacs coirp"; |
| +"Numbered list" = "Liosta uimhrithe"; |
| +"Open a document from Home" = "Oscail cáipéis ón mBaile"; |
| +"Outdent" = "Laghdaigh an eangú"; |
| +"PDF…" = "PDF"; |
| +"Pages" = "Leathanaigh"; |
| +"Paragraph style" = "Stíl ailt"; |
| +"Print…" = "Priontáil"; |
| +"Recent" = "Le déanaí"; |
| +"Recent documents" = "Cáipéisí le déanaí"; |
| +"Recent spreadsheets" = "Scarbhileoga le déanaí"; |
| +"Retry" = "Bain triail eile as"; |
| +"Save" = "Sábháil"; |
| +"Select column %@" = "Roghnaigh colún %@"; |
| +"Select row %d" = "Roghnaigh ró %d"; |
| +"Shared" = "Comhroinnte leat"; |
| +"Share…" = "Comhroinn"; |
| +"Sign out" = "Sínigh amach"; |
| +"Size" = "Méid"; |
| +"Zoom" = "Súmáil"; |
| +"New spreadsheet" = "Scarbhileog nua"; |
| +"Rename…" = "Athainmnigh…"; |
| +"Export workbook…" = "Easpórtáil leabhar oibre…"; |
| +"Version history…" = "Stair na leaganacha…"; |
| +"Undo" = "Cealaigh"; |
| +"Redo" = "Athdhéan"; |
| +"Cut" = "Gearr"; |
| +"Copy" = "Cóipeáil"; |
| +"Paste" = "Greamaigh"; |
| +"Select all" = "Roghnaigh uile"; |
| +"Clear contents" = "Glan an t-ábhar"; |
| +"Fill down" = "Líon síos"; |
| +"Fill right" = "Líon ar dheis"; |
| +"Row and column sizes…" = "Méideanna rónna agus colún…"; |
| +"Insert" = "Ionsáigh"; |
| +"Sheet" = "Bileog"; |
| +"Sort and filter…" = "Sórtáil agus scag…"; |
| +"Comments…" = "Nótaí tráchta…"; |
| +"Refresh" = "Athnuaigh"; |
| +"Import workbook" = "Iompórtáil leabhar oibre"; |
| +"Search spreadsheets" = "Cuardaigh scarbhileoga"; |
diff --git a/native/apple/Sources/SheetsKit/Resources/hi.lproj/Mac.strings b/native/apple/Sources/SheetsKit/Resources/hi.lproj/Mac.strings| new file mode 100644 |
| index 0000000..6299d2e |
| --- /dev/null |
| +++ b/native/apple/Sources/SheetsKit/Resources/hi.lproj/Mac.strings |
| @@ -0,0 +1,70 @@ |
| +"Accounts" = "खाते"; |
| +"Actual size" = "वास्तविक आकार"; |
| +"Alignment" = "संरेखण"; |
| +"Block quote" = "ब्लॉक उद्धरण"; |
| +"Bold" = "बोल्ड"; |
| +"Bullet list" = "बुलेट वाली सूची"; |
| +"Cell contents" = "सेल की सामग्री"; |
| +"Clear formatting" = "फ़ॉर्मैटिंग हटाएँ"; |
| +"Close this account’s document windows before signing out." = "साइन आउट करने से पहले इस खाते की दस्तावेज़ विंडो बंद करें।"; |
| +"Create a document to get started." = "आपके बनाए दस्तावेज़ यहाँ दिखेंगे, सबसे नए पहले।"; |
| +"Docs" = "Docs"; |
| +"Document" = "दस्तावेज़"; |
| +"Document inspector" = "दस्तावेज़ निरीक्षक"; |
| +"Double-click to fit column to contents" = "सामग्री के अनुसार आकार बदलने के लिए डबल-क्लिक करें"; |
| +"Edit %@" = "%@ संपादित करें"; |
| +"Export" = "निर्यात"; |
| +"Fit column to contents" = "सामग्री के अनुसार स्तंभ का आकार बदलें"; |
| +"Font" = "फ़ॉन्ट"; |
| +"Format" = "फ़ॉर्मैट"; |
| +"Heading %d" = "शीर्षक %d"; |
| +"Home" = "होम"; |
| +"Indent" = "इंडेंट बढ़ाएँ"; |
| +"Inline code" = "कोड"; |
| +"Inspector" = "दस्तावेज़ निरीक्षक"; |
| +"Italic" = "इटैलिक"; |
| +"Markdown…" = "Markdown"; |
| +"New document" = "नया दस्तावेज़"; |
| +"No documents" = "अभी कोई दस्तावेज़ नहीं"; |
| +"No recent documents" = "अभी कोई दस्तावेज़ नहीं"; |
| +"Normal" = "मुख्य पाठ"; |
| +"Numbered list" = "क्रमांकित सूची"; |
| +"Open a document from Home" = "होम से दस्तावेज़ खोलें"; |
| +"Outdent" = "इंडेंट घटाएँ"; |
| +"PDF…" = "PDF"; |
| +"Pages" = "पृष्ठों में"; |
| +"Paragraph style" = "अनुच्छेद शैली"; |
| +"Print…" = "प्रिंट करें"; |
| +"Recent" = "हाल के"; |
| +"Recent documents" = "हाल के दस्तावेज़"; |
| +"Recent spreadsheets" = "हाल की स्प्रेडशीट"; |
| +"Retry" = "फिर कोशिश करें"; |
| +"Save" = "सहेजें"; |
| +"Select column %@" = "स्तंभ %@ चुनें"; |
| +"Select row %d" = "पंक्ति %d चुनें"; |
| +"Shared" = "आपके साथ शेयर किए गए"; |
| +"Share…" = "शेयर करें"; |
| +"Sign out" = "साइन आउट करें"; |
| +"Size" = "आकार"; |
| +"Zoom" = "ज़ूम"; |
| +"New spreadsheet" = "नई स्प्रेडशीट"; |
| +"Rename…" = "नाम बदलें…"; |
| +"Export workbook…" = "कार्यपुस्तिका निर्यात करें…"; |
| +"Version history…" = "संस्करण इतिहास…"; |
| +"Undo" = "पूर्ववत करें"; |
| +"Redo" = "फिर करें"; |
| +"Cut" = "काटें"; |
| +"Copy" = "कॉपी करें"; |
| +"Paste" = "चिपकाएँ"; |
| +"Select all" = "सभी चुनें"; |
| +"Clear contents" = "सामग्री मिटाएँ"; |
| +"Fill down" = "नीचे भरें"; |
| +"Fill right" = "दाईं ओर भरें"; |
| +"Row and column sizes…" = "पंक्ति और स्तंभ के आकार…"; |
| +"Insert" = "सम्मिलित करें"; |
| +"Sheet" = "शीट"; |
| +"Sort and filter…" = "क्रमबद्ध और फ़िल्टर करें…"; |
| +"Comments…" = "टिप्पणियाँ…"; |
| +"Refresh" = "रीफ़्रेश करें"; |
| +"Import workbook" = "कार्यपुस्तिका आयात करें"; |
| +"Search spreadsheets" = "स्प्रेडशीट खोजें"; |
diff --git a/native/apple/Sources/SheetsKit/Resources/ja.lproj/Mac.strings b/native/apple/Sources/SheetsKit/Resources/ja.lproj/Mac.strings| new file mode 100644 |
| index 0000000..787ec84 |
| --- /dev/null |
| +++ b/native/apple/Sources/SheetsKit/Resources/ja.lproj/Mac.strings |
| @@ -0,0 +1,70 @@ |
| +"Accounts" = "アカウント"; |
| +"Actual size" = "実際のサイズ"; |
| +"Alignment" = "配置"; |
| +"Block quote" = "引用ブロック"; |
| +"Bold" = "太字"; |
| +"Bullet list" = "箇条書き"; |
| +"Cell contents" = "セルの内容"; |
| +"Clear formatting" = "書式をクリア"; |
| +"Close this account’s document windows before signing out." = "サインアウトする前に、このアカウントの書類ウインドウを閉じてください。"; |
| +"Create a document to get started." = "作成したドキュメントが新しい順にここに表示されます。"; |
| +"Docs" = "Docs"; |
| +"Document" = "書類"; |
| +"Document inspector" = "書類インスペクタ"; |
| +"Double-click to fit column to contents" = "ダブルクリックで列幅を内容に合わせる"; |
| +"Edit %@" = "%@ を編集"; |
| +"Export" = "書き出す"; |
| +"Fit column to contents" = "列幅を内容に合わせる"; |
| +"Font" = "フォント"; |
| +"Format" = "書式"; |
| +"Heading %d" = "見出し %d"; |
| +"Home" = "ホーム"; |
| +"Indent" = "インデントを増やす"; |
| +"Inline code" = "コード"; |
| +"Inspector" = "書類インスペクタ"; |
| +"Italic" = "斜体"; |
| +"Markdown…" = "Markdown"; |
| +"New document" = "新規ドキュメント"; |
| +"No documents" = "ドキュメントはまだありません"; |
| +"No recent documents" = "ドキュメントはまだありません"; |
| +"Normal" = "本文"; |
| +"Numbered list" = "番号付きリスト"; |
| +"Open a document from Home" = "ホームから書類を開く"; |
| +"Outdent" = "インデントを減らす"; |
| +"PDF…" = "PDF"; |
| +"Pages" = "ページ区切り"; |
| +"Paragraph style" = "段落スタイル"; |
| +"Print…" = "プリント"; |
| +"Recent" = "最近使った項目"; |
| +"Recent documents" = "最近の書類"; |
| +"Recent spreadsheets" = "最近のスプレッドシート"; |
| +"Retry" = "再試行"; |
| +"Save" = "保存"; |
| +"Select column %@" = "列 %@ を選択"; |
| +"Select row %d" = "行 %d を選択"; |
| +"Shared" = "あなたと共有"; |
| +"Share…" = "共有"; |
| +"Sign out" = "サインアウト"; |
| +"Size" = "サイズ"; |
| +"Zoom" = "ズーム"; |
| +"New spreadsheet" = "新規スプレッドシート"; |
| +"Rename…" = "名前を変更…"; |
| +"Export workbook…" = "ブックを書き出す…"; |
| +"Version history…" = "バージョン履歴…"; |
| +"Undo" = "取り消す"; |
| +"Redo" = "やり直す"; |
| +"Cut" = "カット"; |
| +"Copy" = "コピー"; |
| +"Paste" = "ペースト"; |
| +"Select all" = "すべてを選択"; |
| +"Clear contents" = "内容を消去"; |
| +"Fill down" = "下方向へコピー"; |
| +"Fill right" = "右方向へコピー"; |
| +"Row and column sizes…" = "行と列のサイズ…"; |
| +"Insert" = "挿入"; |
| +"Sheet" = "シート"; |
| +"Sort and filter…" = "並べ替えとフィルター…"; |
| +"Comments…" = "コメント…"; |
| +"Refresh" = "更新"; |
| +"Import workbook" = "ブックを読み込む"; |
| +"Search spreadsheets" = "スプレッドシートを検索"; |
diff --git a/native/apple/Sources/SheetsKit/Resources/pt.lproj/Mac.strings b/native/apple/Sources/SheetsKit/Resources/pt.lproj/Mac.strings| new file mode 100644 |
| index 0000000..7e008d6 |
| --- /dev/null |
| +++ b/native/apple/Sources/SheetsKit/Resources/pt.lproj/Mac.strings |
| @@ -0,0 +1,70 @@ |
| +"Accounts" = "Contas"; |
| +"Actual size" = "Tamanho real"; |
| +"Alignment" = "Alinhamento"; |
| +"Block quote" = "Citação em bloco"; |
| +"Bold" = "Negrito"; |
| +"Bullet list" = "Lista com marcas"; |
| +"Cell contents" = "Conteúdo da célula"; |
| +"Clear formatting" = "Limpar formatação"; |
| +"Close this account’s document windows before signing out." = "Feche as janelas de documentos desta conta antes de terminar a sessão."; |
| +"Create a document to get started." = "Os documentos que criar aparecem aqui, começando pelos mais recentes."; |
| +"Docs" = "Docs"; |
| +"Document" = "Documento"; |
| +"Document inspector" = "Inspetor do documento"; |
| +"Double-click to fit column to contents" = "Clique duas vezes para ajustar a coluna ao conteúdo"; |
| +"Edit %@" = "Editar %@"; |
| +"Export" = "Exportar"; |
| +"Fit column to contents" = "Ajustar coluna ao conteúdo"; |
| +"Font" = "Tipo de letra"; |
| +"Format" = "Formato"; |
| +"Heading %d" = "Título %d"; |
| +"Home" = "Início"; |
| +"Indent" = "Aumentar avanço"; |
| +"Inline code" = "Código"; |
| +"Inspector" = "Inspetor do documento"; |
| +"Italic" = "Itálico"; |
| +"Markdown…" = "Markdown"; |
| +"New document" = "Novo documento"; |
| +"No documents" = "Ainda não há documentos"; |
| +"No recent documents" = "Ainda não há documentos"; |
| +"Normal" = "Corpo do texto"; |
| +"Numbered list" = "Lista numerada"; |
| +"Open a document from Home" = "Abra um documento no Início"; |
| +"Outdent" = "Diminuir avanço"; |
| +"PDF…" = "PDF"; |
| +"Pages" = "Paginado"; |
| +"Paragraph style" = "Estilo de parágrafo"; |
| +"Print…" = "Imprimir"; |
| +"Recent" = "Recentes"; |
| +"Recent documents" = "Documentos recentes"; |
| +"Recent spreadsheets" = "Folhas de cálculo recentes"; |
| +"Retry" = "Tentar novamente"; |
| +"Save" = "Guardar"; |
| +"Select column %@" = "Selecionar coluna %@"; |
| +"Select row %d" = "Selecionar linha %d"; |
| +"Shared" = "Partilhados consigo"; |
| +"Share…" = "Partilhar"; |
| +"Sign out" = "Terminar sessão"; |
| +"Size" = "Tamanho"; |
| +"Zoom" = "Zoom"; |
| +"New spreadsheet" = "Nova planilha"; |
| +"Rename…" = "Renomear…"; |
| +"Export workbook…" = "Exportar pasta de trabalho…"; |
| +"Version history…" = "Histórico de versões…"; |
| +"Undo" = "Desfazer"; |
| +"Redo" = "Refazer"; |
| +"Cut" = "Recortar"; |
| +"Copy" = "Copiar"; |
| +"Paste" = "Colar"; |
| +"Select all" = "Selecionar tudo"; |
| +"Clear contents" = "Limpar conteúdo"; |
| +"Fill down" = "Preencher para baixo"; |
| +"Fill right" = "Preencher à direita"; |
| +"Row and column sizes…" = "Tamanhos de linhas e colunas…"; |
| +"Insert" = "Inserir"; |
| +"Sheet" = "Planilha"; |
| +"Sort and filter…" = "Ordenar e filtrar…"; |
| +"Comments…" = "Comentários…"; |
| +"Refresh" = "Atualizar"; |
| +"Import workbook" = "Importar pasta de trabalho"; |
| +"Search spreadsheets" = "Pesquisar planilhas"; |
diff --git a/native/apple/Sources/SheetsKit/Resources/ru.lproj/Mac.strings b/native/apple/Sources/SheetsKit/Resources/ru.lproj/Mac.strings| new file mode 100644 |
| index 0000000..510d270 |
| --- /dev/null |
| +++ b/native/apple/Sources/SheetsKit/Resources/ru.lproj/Mac.strings |
| @@ -0,0 +1,70 @@ |
| +"Accounts" = "Учётные записи"; |
| +"Actual size" = "Фактический размер"; |
| +"Alignment" = "Выравнивание"; |
| +"Block quote" = "Блок цитаты"; |
| +"Bold" = "Полужирный"; |
| +"Bullet list" = "Маркированный список"; |
| +"Cell contents" = "Содержимое ячейки"; |
| +"Clear formatting" = "Очистить форматирование"; |
| +"Close this account’s document windows before signing out." = "Перед выходом закройте окна документов этой учётной записи."; |
| +"Create a document to get started." = "Созданные вами документы появятся здесь, начиная с самых новых."; |
| +"Docs" = "Docs"; |
| +"Document" = "Документ"; |
| +"Document inspector" = "Инспектор документа"; |
| +"Double-click to fit column to contents" = "Дважды щёлкните для подгонки ширины столбца"; |
| +"Edit %@" = "Изменить %@"; |
| +"Export" = "Экспорт"; |
| +"Fit column to contents" = "Подогнать ширину столбца"; |
| +"Font" = "Шрифт"; |
| +"Format" = "Формат"; |
| +"Heading %d" = "Заголовок %d"; |
| +"Home" = "Главная"; |
| +"Indent" = "Увеличить отступ"; |
| +"Inline code" = "Код"; |
| +"Inspector" = "Инспектор документа"; |
| +"Italic" = "Курсив"; |
| +"Markdown…" = "Markdown"; |
| +"New document" = "Новый документ"; |
| +"No documents" = "Документов пока нет"; |
| +"No recent documents" = "Документов пока нет"; |
| +"Normal" = "Основной текст"; |
| +"Numbered list" = "Нумерованный список"; |
| +"Open a document from Home" = "Откройте документ на главной"; |
| +"Outdent" = "Уменьшить отступ"; |
| +"PDF…" = "PDF"; |
| +"Pages" = "Постраничный"; |
| +"Paragraph style" = "Стиль абзаца"; |
| +"Print…" = "Печать"; |
| +"Recent" = "Недавние"; |
| +"Recent documents" = "Недавние документы"; |
| +"Recent spreadsheets" = "Недавние таблицы"; |
| +"Retry" = "Повторить попытку"; |
| +"Save" = "Сохранить"; |
| +"Select column %@" = "Выбрать столбец %@"; |
| +"Select row %d" = "Выбрать строку %d"; |
| +"Shared" = "Доступные вам"; |
| +"Share…" = "Поделиться"; |
| +"Sign out" = "Выйти"; |
| +"Size" = "Размер"; |
| +"Zoom" = "Масштаб"; |
| +"New spreadsheet" = "Новая таблица"; |
| +"Rename…" = "Переименовать…"; |
| +"Export workbook…" = "Экспорт книги…"; |
| +"Version history…" = "История версий…"; |
| +"Undo" = "Отменить"; |
| +"Redo" = "Повторить"; |
| +"Cut" = "Вырезать"; |
| +"Copy" = "Копировать"; |
| +"Paste" = "Вставить"; |
| +"Select all" = "Выбрать всё"; |
| +"Clear contents" = "Очистить содержимое"; |
| +"Fill down" = "Заполнить вниз"; |
| +"Fill right" = "Заполнить вправо"; |
| +"Row and column sizes…" = "Размеры строк и столбцов…"; |
| +"Insert" = "Вставка"; |
| +"Sheet" = "Лист"; |
| +"Sort and filter…" = "Сортировка и фильтр…"; |
| +"Comments…" = "Комментарии…"; |
| +"Refresh" = "Обновить"; |
| +"Import workbook" = "Импорт книги"; |
| +"Search spreadsheets" = "Поиск таблиц"; |
diff --git a/native/apple/Sources/SheetsKit/Resources/vi.lproj/Mac.strings b/native/apple/Sources/SheetsKit/Resources/vi.lproj/Mac.strings| new file mode 100644 |
| index 0000000..7a0f646 |
| --- /dev/null |
| +++ b/native/apple/Sources/SheetsKit/Resources/vi.lproj/Mac.strings |
| @@ -0,0 +1,70 @@ |
| +"Accounts" = "Tài khoản"; |
| +"Actual size" = "Kích thước thực"; |
| +"Alignment" = "Căn chỉnh"; |
| +"Block quote" = "Khối trích dẫn"; |
| +"Bold" = "Đậm"; |
| +"Bullet list" = "Danh sách dấu đầu dòng"; |
| +"Cell contents" = "Nội dung ô"; |
| +"Clear formatting" = "Xóa định dạng"; |
| +"Close this account’s document windows before signing out." = "Đóng các cửa sổ tài liệu của tài khoản này trước khi đăng xuất."; |
| +"Create a document to get started." = "Tài liệu bạn tạo sẽ xuất hiện ở đây, mới nhất trước."; |
| +"Docs" = "Docs"; |
| +"Document" = "Tài liệu"; |
| +"Document inspector" = "Thuộc tính tài liệu"; |
| +"Double-click to fit column to contents" = "Nhấp đúp để khớp cột với nội dung"; |
| +"Edit %@" = "Sửa %@"; |
| +"Export" = "Xuất"; |
| +"Fit column to contents" = "Khớp cột với nội dung"; |
| +"Font" = "Phông chữ"; |
| +"Format" = "Định dạng"; |
| +"Heading %d" = "Tiêu đề %d"; |
| +"Home" = "Trang chủ"; |
| +"Indent" = "Tăng thụt lề"; |
| +"Inline code" = "Mã"; |
| +"Inspector" = "Thuộc tính tài liệu"; |
| +"Italic" = "Nghiêng"; |
| +"Markdown…" = "Markdown"; |
| +"New document" = "Tài liệu mới"; |
| +"No documents" = "Chưa có tài liệu"; |
| +"No recent documents" = "Chưa có tài liệu"; |
| +"Normal" = "Văn bản nội dung"; |
| +"Numbered list" = "Danh sách đánh số"; |
| +"Open a document from Home" = "Mở tài liệu từ Trang chủ"; |
| +"Outdent" = "Giảm thụt lề"; |
| +"PDF…" = "PDF"; |
| +"Pages" = "Theo trang"; |
| +"Paragraph style" = "Kiểu đoạn văn"; |
| +"Print…" = "In"; |
| +"Recent" = "Gần đây"; |
| +"Recent documents" = "Tài liệu gần đây"; |
| +"Recent spreadsheets" = "Bảng tính gần đây"; |
| +"Retry" = "Thử lại"; |
| +"Save" = "Lưu"; |
| +"Select column %@" = "Chọn cột %@"; |
| +"Select row %d" = "Chọn hàng %d"; |
| +"Shared" = "Được chia sẻ với bạn"; |
| +"Share…" = "Chia sẻ"; |
| +"Sign out" = "Đăng xuất"; |
| +"Size" = "Kích cỡ"; |
| +"Zoom" = "Thu phóng"; |
| +"New spreadsheet" = "Bảng tính mới"; |
| +"Rename…" = "Đổi tên…"; |
| +"Export workbook…" = "Xuất sổ làm việc…"; |
| +"Version history…" = "Lịch sử phiên bản…"; |
| +"Undo" = "Hoàn tác"; |
| +"Redo" = "Làm lại"; |
| +"Cut" = "Cắt"; |
| +"Copy" = "Sao chép"; |
| +"Paste" = "Dán"; |
| +"Select all" = "Chọn tất cả"; |
| +"Clear contents" = "Xóa nội dung"; |
| +"Fill down" = "Điền xuống"; |
| +"Fill right" = "Điền sang phải"; |
| +"Row and column sizes…" = "Kích thước hàng và cột…"; |
| +"Insert" = "Chèn"; |
| +"Sheet" = "Trang tính"; |
| +"Sort and filter…" = "Sắp xếp và lọc…"; |
| +"Comments…" = "Bình luận…"; |
| +"Refresh" = "Làm mới"; |
| +"Import workbook" = "Nhập sổ làm việc"; |
| +"Search spreadsheets" = "Tìm bảng tính"; |
diff --git a/native/apple/Sources/SheetsKit/Resources/yue.lproj/Mac.strings b/native/apple/Sources/SheetsKit/Resources/yue.lproj/Mac.strings| new file mode 100644 |
| index 0000000..4af29a1 |
| --- /dev/null |
| +++ b/native/apple/Sources/SheetsKit/Resources/yue.lproj/Mac.strings |
| @@ -0,0 +1,70 @@ |
| +"Accounts" = "帳戶"; |
| +"Actual size" = "實際大小"; |
| +"Alignment" = "對齊"; |
| +"Block quote" = "引文區塊"; |
| +"Bold" = "粗體"; |
| +"Bullet list" = "項目符號清單"; |
| +"Cell contents" = "儲存格內容"; |
| +"Clear formatting" = "清除格式"; |
| +"Close this account’s document windows before signing out." = "登出前請先關閉此帳戶嘅文件視窗。"; |
| +"Create a document to get started." = "你建立嘅文件會喺呢度顯示,最新嘅排最前。"; |
| +"Docs" = "Docs"; |
| +"Document" = "文件"; |
| +"Document inspector" = "文件檢查器"; |
| +"Double-click to fit column to contents" = "按兩下以按內容調整欄寬"; |
| +"Edit %@" = "編輯 %@"; |
| +"Export" = "輸出"; |
| +"Fit column to contents" = "按內容調整欄寬"; |
| +"Font" = "字體"; |
| +"Format" = "格式"; |
| +"Heading %d" = "標題 %d"; |
| +"Home" = "主頁"; |
| +"Indent" = "增加縮排"; |
| +"Inline code" = "程式碼"; |
| +"Inspector" = "文件檢查器"; |
| +"Italic" = "斜體"; |
| +"Markdown…" = "Markdown"; |
| +"New document" = "新增文件"; |
| +"No documents" = "仲未有文件"; |
| +"No recent documents" = "仲未有文件"; |
| +"Normal" = "內文"; |
| +"Numbered list" = "編號清單"; |
| +"Open a document from Home" = "由主頁開啟文件"; |
| +"Outdent" = "減少縮排"; |
| +"PDF…" = "PDF"; |
| +"Pages" = "分頁"; |
| +"Paragraph style" = "段落樣式"; |
| +"Print…" = "列印"; |
| +"Recent" = "最近項目"; |
| +"Recent documents" = "最近文件"; |
| +"Recent spreadsheets" = "最近試算表"; |
| +"Retry" = "再試一次"; |
| +"Save" = "儲存"; |
| +"Select column %@" = "選取 %@ 欄"; |
| +"Select row %d" = "選取第 %d 行"; |
| +"Shared" = "同你分享嘅文件"; |
| +"Share…" = "分享"; |
| +"Sign out" = "登出"; |
| +"Size" = "大小"; |
| +"Zoom" = "縮放"; |
| +"New spreadsheet" = "新增試算表"; |
| +"Rename…" = "重新命名…"; |
| +"Export workbook…" = "匯出活頁簿…"; |
| +"Version history…" = "版本記錄…"; |
| +"Undo" = "還原"; |
| +"Redo" = "重做"; |
| +"Cut" = "剪下"; |
| +"Copy" = "複製"; |
| +"Paste" = "貼上"; |
| +"Select all" = "全選"; |
| +"Clear contents" = "清除內容"; |
| +"Fill down" = "向下填滿"; |
| +"Fill right" = "向右填滿"; |
| +"Row and column sizes…" = "列與欄大小…"; |
| +"Insert" = "插入"; |
| +"Sheet" = "工作表"; |
| +"Sort and filter…" = "排序及篩選…"; |
| +"Comments…" = "留言…"; |
| +"Refresh" = "重新整理"; |
| +"Import workbook" = "匯入活頁簿"; |
| +"Search spreadsheets" = "搜尋試算表"; |
diff --git a/native/apple/Sources/SheetsKit/Resources/zh-Hans.lproj/Mac.strings b/native/apple/Sources/SheetsKit/Resources/zh-Hans.lproj/Mac.strings| new file mode 100644 |
| index 0000000..c2abb6d |
| --- /dev/null |
| +++ b/native/apple/Sources/SheetsKit/Resources/zh-Hans.lproj/Mac.strings |
| @@ -0,0 +1,70 @@ |
| +"Accounts" = "账户"; |
| +"Actual size" = "实际大小"; |
| +"Alignment" = "对齐"; |
| +"Block quote" = "块引用"; |
| +"Bold" = "粗体"; |
| +"Bullet list" = "项目符号列表"; |
| +"Cell contents" = "单元格内容"; |
| +"Clear formatting" = "清除格式"; |
| +"Close this account’s document windows before signing out." = "退出登录前请关闭此账户的文档窗口。"; |
| +"Create a document to get started." = "您创建的文档会显示在这里,最新的排在最前面。"; |
| +"Docs" = "Docs"; |
| +"Document" = "文档"; |
| +"Document inspector" = "文档检查器"; |
| +"Double-click to fit column to contents" = "双击以根据内容调整列宽"; |
| +"Edit %@" = "编辑 %@"; |
| +"Export" = "导出"; |
| +"Fit column to contents" = "根据内容调整列宽"; |
| +"Font" = "字体"; |
| +"Format" = "格式"; |
| +"Heading %d" = "标题 %d"; |
| +"Home" = "主页"; |
| +"Indent" = "增加缩进"; |
| +"Inline code" = "代码"; |
| +"Inspector" = "文档检查器"; |
| +"Italic" = "斜体"; |
| +"Markdown…" = "Markdown"; |
| +"New document" = "新建文档"; |
| +"No documents" = "还没有文档"; |
| +"No recent documents" = "还没有文档"; |
| +"Normal" = "正文"; |
| +"Numbered list" = "编号列表"; |
| +"Open a document from Home" = "从主页打开文档"; |
| +"Outdent" = "减少缩进"; |
| +"PDF…" = "PDF"; |
| +"Pages" = "分页"; |
| +"Paragraph style" = "段落样式"; |
| +"Print…" = "打印"; |
| +"Recent" = "最近项目"; |
| +"Recent documents" = "最近文档"; |
| +"Recent spreadsheets" = "最近电子表格"; |
| +"Retry" = "重试"; |
| +"Save" = "保存"; |
| +"Select column %@" = "选择 %@ 列"; |
| +"Select row %d" = "选择第 %d 行"; |
| +"Shared" = "与您共享"; |
| +"Share…" = "共享"; |
| +"Sign out" = "退出登录"; |
| +"Size" = "大小"; |
| +"Zoom" = "缩放"; |
| +"New spreadsheet" = "新建电子表格"; |
| +"Rename…" = "重命名…"; |
| +"Export workbook…" = "导出工作簿…"; |
| +"Version history…" = "版本历史…"; |
| +"Undo" = "撤销"; |
| +"Redo" = "重做"; |
| +"Cut" = "剪切"; |
| +"Copy" = "复制"; |
| +"Paste" = "粘贴"; |
| +"Select all" = "全选"; |
| +"Clear contents" = "清除内容"; |
| +"Fill down" = "向下填充"; |
| +"Fill right" = "向右填充"; |
| +"Row and column sizes…" = "行和列大小…"; |
| +"Insert" = "插入"; |
| +"Sheet" = "工作表"; |
| +"Sort and filter…" = "排序和筛选…"; |
| +"Comments…" = "评论…"; |
| +"Refresh" = "刷新"; |
| +"Import workbook" = "导入工作簿"; |
| +"Search spreadsheets" = "搜索电子表格"; |
diff --git a/native/apple/Sources/SheetsKit/SheetAutoSize.swift b/native/apple/Sources/SheetsKit/SheetAutoSize.swift| new file mode 100644 |
| index 0000000..fd9d26f |
| --- /dev/null |
| +++ b/native/apple/Sources/SheetsKit/SheetAutoSize.swift |
| @@ -0,0 +1,18 @@ |
| +#if os(macOS) |
| +import AppKit |
| + |
| +extension SheetModel { |
| + /// Fits the loaded grid (the same visible rows used by the editor). |
| + func fitColumn(_ column: Int) async { |
| + guard (0..<cols).contains(column), mayWrite, !busy, !hasUnconfirmedChanges else { return } |
| + var width: CGFloat = 40 |
| + for (point, cell) in cells where point.c == column { |
| + let style = styles[cell.f ?? ""] |
| + var font = NSFont.systemFont(ofSize: 13, weight: style?.b == true ? .bold : .regular) |
| + if style?.i == true { font = NSFontManager.shared.convert(font, toHaveTrait: .italicFontMask) } |
| + width = max(width, (cell.display as NSString).size(withAttributes: [.font: font]).width + 14) |
| + } |
| + await resize(axis: "col", size: min(1000, Int(ceil(width))), index: column) |
| + } |
| +} |
| +#endif |
diff --git a/native/apple/Sources/SheetsKit/SheetCell.swift b/native/apple/Sources/SheetsKit/SheetCell.swift| index d3b499e..0c9dcfc 100644 |
| --- a/native/apple/Sources/SheetsKit/SheetCell.swift |
| +++ b/native/apple/Sources/SheetsKit/SheetCell.swift |
| @@ -3,7 +3,12 @@ import SwiftUI |
| struct SheetCell: View { |
| @Bindable var model: SheetModel |
| let point: Coordinate |
| + @Binding var editing: Coordinate? |
| + var select: () -> Void |
| var beginEditing: () -> Void = {} |
| + var endEditing: () -> Void |
| + @State private var textSelection: TextSelection? |
| + @FocusState private var fieldFocused: Bool |
| @Environment(\.colorScheme) private var scheme |
| private func width(_ col: Int) -> Double { model.sizes.cols?[String(col)] ?? 110 } |
| private func height(_ row: Int) -> Double { model.sizes.rows?[String(row)] ?? 32 } |
| @@ -13,10 +18,47 @@ struct SheetCell: View { |
| let alignment: Alignment = |
| style?.a == "center" |
| ? .center : style?.a == "right" || (style?.a == nil && cell?.n == true) ? .trailing : .leading |
| - return Button { |
| - model.select(point) |
| - } label: { |
| - Text(cell?.display ?? "").font(.body.weight(style?.b == true ? .bold : .regular)) |
| + return Group { |
| + #if os(macOS) |
| + if editing == point { |
| + TextField(sheetsMacString("Cell contents"), text: $model.draft, selection: $textSelection) |
| + .textFieldStyle(.plain) |
| + .font(.system(size: 13)) |
| + .padding(.horizontal, 5) |
| + .frame(width: width(point.c), height: height(point.r)) |
| + .background(fill(style)) |
| + .overlay { Rectangle().strokeBorder(Color.accentColor, lineWidth: 2) } |
| + .focused($fieldFocused) |
| + .disabled(!model.mayWrite || model.busy) |
| + .task { |
| + fieldFocused = true |
| + await Task.yield() |
| + textSelection = TextSelection(insertionPoint: model.draft.endIndex) |
| + } |
| + .onSubmit { commit(right: false) } |
| + .onKeyPress(.tab) { commit(right: true); return .handled } |
| + .onKeyPress(.escape) { |
| + guard !model.busy else { return .handled } |
| + model.discardAll(); endEditing(); return .handled |
| + } |
| + .accessibilityLabel(String(format: sheetsMacString("Edit %@"), point.ref)) |
| + } else { display(cell, style: style, alignment: alignment) } |
| + #else |
| + display(cell, style: style, alignment: alignment) |
| + #endif |
| + } |
| + } |
| + private func commit(right: Bool) { |
| + guard !model.busy, model.selection.start == point else { return } |
| + Task { |
| + await model.commit(moveRight: right) |
| + if !model.hasUnconfirmedChanges { endEditing() } |
| + else { fieldFocused = true } |
| + } |
| + } |
| + private func display(_ cell: Cell?, style: CellStyle?, alignment: Alignment) -> some View { |
| + Button(action: select) { |
| + Text(cell?.display ?? "").font(.system(size: 13, weight: style?.b == true ? .bold : .regular)) |
| .italic(style?.i == true).lineLimit(1).padding(.horizontal, 5) |
| .frame(width: width(point.c), height: height(point.r), alignment: alignment) |
| .foregroundStyle(ink(style)).background(fill(style)) |
| @@ -27,7 +69,7 @@ struct SheetCell: View { |
| lineWidth: model.selection.contains(point) ? 2 : 0.5) |
| } |
| }.buttonStyle(.plain) |
| - .simultaneousGesture(TapGesture(count: 2).onEnded { if model.mayWrite { beginEditing() } }) |
| + .simultaneousGesture(TapGesture(count: 2).onEnded { if model.mayWrite, !model.busy, !model.hasUnconfirmedChanges { model.select(point); beginEditing() } }) |
| .accessibilityLabel("\(point.ref), \(cell?.display ?? "Empty")") |
| .accessibilityValue(style?.lc == true ? "Imported colours have low contrast" : "") |
| .accessibilityAddTraits(model.selection.contains(point) ? .isSelected : []) |
diff --git a/native/apple/Sources/SheetsKit/SheetEditorContext.swift b/native/apple/Sources/SheetsKit/SheetEditorContext.swift| new file mode 100644 |
| index 0000000..331a2d0 |
| --- /dev/null |
| +++ b/native/apple/Sources/SheetsKit/SheetEditorContext.swift |
| @@ -0,0 +1,9 @@ |
| +#if os(macOS) |
| +import SwiftUI |
| + |
| +struct SheetEditorContext { |
| + let model: SheetModel |
| + let panel: Binding<String?> |
| +} |
| +extension FocusedValues { @Entry var sheetEditor: SheetEditorContext? } |
| +#endif |
diff --git a/native/apple/Sources/SheetsKit/SheetFiles.swift b/native/apple/Sources/SheetsKit/SheetFiles.swift| index da8ef7b..c8245bc 100644 |
| --- a/native/apple/Sources/SheetsKit/SheetFiles.swift |
| +++ b/native/apple/Sources/SheetsKit/SheetFiles.swift |
| @@ -34,6 +34,7 @@ struct ImportProgress: Decodable { |
| struct SheetFiles: View { |
| @Bindable var model: SheetModel |
| let importing: Bool |
| + var openImported: ((UInt64, String) -> Void)? = nil |
| @State private var picker = false |
| @State private var exporter = false |
| @State private var workbook: WorkbookExport? |
| @@ -147,7 +148,8 @@ struct SheetFiles: View { |
| if let id = UInt64(state.location.split(separator: "/").last ?? ""), |
| let sheet = model.sheets.first(where: { $0.ID == id }) |
| { |
| - await model.open(sheet) |
| + if let openImported { openImported(sheet.ID, sheet.Title) } |
| + else { await model.open(sheet) } |
| } |
| return |
| } |
diff --git a/native/apple/Sources/SheetsKit/SheetGrid.swift b/native/apple/Sources/SheetsKit/SheetGrid.swift| index 74f3f04..c9c63d6 100644 |
| --- a/native/apple/Sources/SheetsKit/SheetGrid.swift |
| +++ b/native/apple/Sources/SheetsKit/SheetGrid.swift |
| @@ -2,6 +2,8 @@ import SwiftUI |
| struct SheetGrid: View { |
| @Bindable var model: SheetModel |
| + @State private var editing: Coordinate? |
| + @FocusState private var focused: Bool |
| var beginEditing: () -> Void = {} |
| var body: some View { |
| ScrollView([.horizontal, .vertical]) { |
| @@ -9,10 +11,15 @@ struct SheetGrid: View { |
| Section { |
| ForEach(0..<model.rows, id: \.self) { row in |
| HStack(spacing: 0) { |
| - Text(String(row + 1)).frame(width: 48, height: height(row)).background(.quaternary) |
| + Button { selectRow(row) } label: { |
| + Text(String(row + 1)).frame(width: 48, height: height(row)).background(.quaternary) |
| + }.buttonStyle(.plain).accessibilityLabel(String(format: sheetsMacString("Select row %d"), row + 1)) |
| ForEach(0..<model.cols, id: \.self) { col in |
| SheetCell( |
| - model: model, point: Coordinate(r: row, c: col), beginEditing: beginEditing) |
| + model: model, point: Coordinate(r: row, c: col), editing: $editing, |
| + select: { select(Coordinate(r: row, c: col)) }, |
| + beginEditing: { edit(Coordinate(r: row, c: col)) }, |
| + endEditing: { editing = nil; focused = true }) |
| } |
| } |
| } |
| @@ -20,38 +27,100 @@ struct SheetGrid: View { |
| HStack(spacing: 0) { |
| Text("").frame(width: 48, height: 32) |
| ForEach(0..<model.cols, id: \.self) { col in |
| - Text(Coordinate(r: 0, c: col).ref.dropLast()).frame(width: width(col), height: 32) |
| + HStack(spacing: 0) { |
| + Button { selectColumn(col) } label: { |
| + Text(Coordinate(r: 0, c: col).ref.dropLast()) |
| + .frame(width: max(0, width(col) - 8), height: 32) |
| + .contentShape(.rect) |
| + }.buttonStyle(.plain) |
| + .accessibilityLabel(String(format: sheetsMacString("Select column %@"), String(Coordinate(r: 0, c: col).ref.dropLast()))) |
| + #if os(macOS) |
| + Rectangle().fill(.secondary.opacity(0.15)).frame(width: 8, height: 32) |
| + .contentShape(.rect) |
| + .onTapGesture(count: 2) { Task { await model.fitColumn(col) } } |
| + .help(sheetsMacString("Double-click to fit column to contents")) |
| + .accessibilityHidden(true) |
| + #else |
| + Color.clear.frame(width: 8, height: 32) |
| + #endif |
| + } |
| + #if os(macOS) |
| + .contextMenu { Button(sheetsMacString("Fit column to contents")) { Task { await model.fitColumn(col) } } } |
| + #endif |
| } |
| }.background(.regularMaterial) |
| } |
| } |
| - }.accessibilityLabel("Spreadsheet grid") |
| + }.defaultScrollAnchor(.topLeading) |
| + .accessibilityLabel("Spreadsheet grid") |
| .focusable() |
| + .focusEffectDisabled() |
| + .focused($focused) |
| + .onChange(of: model.selection.start) { _, _ in editing = nil } |
| + .onChange(of: focused) { _, value in model.gridFocused = value } |
| + .onDisappear { model.gridFocused = false } |
| .onKeyPress(.leftArrow) { move(0, -1) } |
| .onKeyPress(.rightArrow) { move(0, 1) } |
| .onKeyPress(.upArrow) { move(-1, 0) } |
| .onKeyPress(.downArrow) { move(1, 0) } |
| .onKeyPress(.delete) { |
| - guard model.mayWrite else { return .ignored } |
| + guard editing == nil, model.mayWrite else { return .ignored } |
| Task { await model.clear() } |
| return .handled |
| } |
| .onKeyPress(.return) { |
| - guard model.mayWrite else { return .ignored } |
| - beginEditing() |
| + guard editing == nil, model.mayWrite else { return .ignored } |
| + edit(model.selection.start) |
| return .handled |
| } |
| - .onKeyPress(characters: .alphanumerics.union(.punctuationCharacters).union(.symbols)) { |
| + .onKeyPress(characters: .alphanumerics.union(.punctuationCharacters).union(.symbols).union(.whitespaces)) { |
| press in |
| - guard model.mayWrite, !model.busy, !model.hasUnconfirmedChanges, |
| + guard editing == nil, model.mayWrite, !model.busy, !model.hasUnconfirmedChanges, |
| press.modifiers.intersection([.command, .control, .option]).isEmpty |
| else { return .ignored } |
| model.draft = press.characters |
| - beginEditing() |
| + edit(model.selection.start) |
| return .handled |
| } |
| } |
| + private func edit(_ point: Coordinate) { |
| + guard model.mayWrite, !model.busy, model.failedWrites.isEmpty else { return } |
| + #if os(macOS) |
| + editing = point |
| + #else |
| + beginEditing() |
| + #endif |
| + } |
| + private func select(_ point: Coordinate) { |
| + guard !model.busy else { return } |
| + if model.hasUnconfirmedChanges { |
| + Task { |
| + await model.saveAll() |
| + guard !model.hasUnconfirmedChanges, !model.busy else { return } |
| + editing = nil; model.select(point); focused = true |
| + } |
| + } else { |
| + if editing != point { editing = nil } |
| + model.select(point) |
| + focused = true |
| + } |
| + } |
| + private func selectRow(_ row: Int) { |
| + guard !model.hasUnconfirmedChanges, !model.busy else { return } |
| + editing = nil |
| + model.rangeText = "A\(row + 1):" + Coordinate(r: row, c: model.cols - 1).ref |
| + model.selectRange() |
| + focused = true |
| + } |
| + private func selectColumn(_ col: Int) { |
| + guard !model.hasUnconfirmedChanges, !model.busy else { return } |
| + editing = nil |
| + model.rangeText = Coordinate(r: 0, c: col).ref + ":" + Coordinate(r: model.rows - 1, c: col).ref |
| + model.selectRange() |
| + focused = true |
| + } |
| private func move(_ r: Int, _ c: Int) -> KeyPress.Result { |
| + guard editing == nil else { return .ignored } |
| let p = model.selection.start |
| model.select( |
| Coordinate(r: max(0, min(model.rows - 1, p.r + r)), c: max(0, min(model.cols - 1, p.c + c)))) |
diff --git a/native/apple/Sources/SheetsKit/SheetModel.swift b/native/apple/Sources/SheetsKit/SheetModel.swift| index 1cf6c09..2ad4dc7 100644 |
| --- a/native/apple/Sources/SheetsKit/SheetModel.swift |
| +++ b/native/apple/Sources/SheetsKit/SheetModel.swift |
| @@ -11,6 +11,7 @@ import RastrilloApple |
| var version: UInt64 = 0 |
| var selection = CellRange(Coordinate(r: 0, c: 0), Coordinate(r: 0, c: 0)) |
| var rangeText = "A1" |
| + var gridFocused = false |
| var draft = "" |
| var originalDraft = "" |
| var dirty: Bool { draft != originalDraft } |
| @@ -177,7 +178,7 @@ import RastrilloApple |
| await accept(value) |
| } catch { self.error = error.localizedDescription } |
| } |
| - func resize(axis: String, size: Int) async { |
| + func resize(axis: String, size: Int, index: Int? = nil) async { |
| guard mayWrite && !busy && !hasUnconfirmedChanges else { return } |
| busy = true |
| defer { busy = false } |
| @@ -185,7 +186,7 @@ import RastrilloApple |
| let response: GridResponse = try await client.request( |
| grid + "/size", method: "POST", |
| body: JSONSerialization.data(withJSONObject: [ |
| - axis: axis == "col" ? selection.start.c : selection.start.r, "size": size, |
| + axis: index ?? (axis == "col" ? selection.start.c : selection.start.r), "size": size, |
| ])) |
| await accept(response) |
| } catch { self.error = error.localizedDescription } |
| @@ -228,10 +229,10 @@ import RastrilloApple |
| return false |
| } |
| } |
| - func create() async { |
| + @discardableResult func create(openCreated: Bool = true) async -> SheetSummary? { |
| guard !hasUnconfirmedChanges && !busy else { |
| error = "Save or discard the current edit first." |
| - return |
| + return nil |
| } |
| busy = true |
| defer { busy = false } |
| @@ -239,8 +240,9 @@ import RastrilloApple |
| let sheet: SheetSummary = try await form("/sheets", [:]) |
| await loadList() |
| busy = false |
| - await open(sheet) |
| - } catch { self.error = error.localizedDescription } |
| + if openCreated { await open(sheet) } |
| + return sheet |
| + } catch { self.error = error.localizedDescription; return nil } |
| } |
| func loadHistory() async { |
| let sheetID = page?.Sheet.ID |
diff --git a/native/apple/Sources/SheetsKit/SheetPanels.swift b/native/apple/Sources/SheetsKit/SheetPanels.swift| index 5d7feb7..5c25b2b 100644 |
| --- a/native/apple/Sources/SheetsKit/SheetPanels.swift |
| +++ b/native/apple/Sources/SheetsKit/SheetPanels.swift |
| @@ -3,6 +3,7 @@ import SwiftUI |
| struct SheetPanels: View { |
| @Bindable var model: SheetModel |
| let panel: String |
| + var openImported: ((UInt64, String) -> Void)? = nil |
| @Environment(\.dismiss) private var dismiss |
| @State private var title = "" |
| @State private var width = 110 |
| @@ -79,7 +80,7 @@ struct SheetPanels: View { |
| .font(.caption) |
| case "Comments": SheetComments(model: model) |
| case "Share": SheetSharing(model: model) |
| - case "Import", "Export": SheetFiles(model: model, importing: panel == "Import") |
| + case "Import", "Export": SheetFiles(model: model, importing: panel == "Import", openImported: openImported) |
| default: EmptyView() |
| } |
| }.formStyle(.grouped).navigationTitle(panel) |
diff --git a/native/apple/Sources/SheetsKit/SheetTable.swift b/native/apple/Sources/SheetsKit/SheetTable.swift| index 3d6d4b1..0af8392 100644 |
| --- a/native/apple/Sources/SheetsKit/SheetTable.swift |
| +++ b/native/apple/Sources/SheetsKit/SheetTable.swift |
| @@ -22,7 +22,7 @@ struct SheetTable: View { |
| HStack(spacing: 0) { |
| Text(String(row.Number)).frame(width: 48) |
| ForEach(row.Cells, id: \.C) { cell in |
| - SheetCell(model: model, point: Coordinate(r: cell.R, c: cell.C)) |
| + SheetCell(model: model, point: Coordinate(r: cell.R, c: cell.C), editing: .constant(nil), select: { model.select(Coordinate(r: cell.R, c: cell.C)) }, endEditing: {}) |
| } |
| } |
| } |
diff --git a/native/apple/Sources/SheetsKit/SheetThumbnail.swift b/native/apple/Sources/SheetsKit/SheetThumbnail.swift| index d3328dc..d4c6c9c 100644 |
| --- a/native/apple/Sources/SheetsKit/SheetThumbnail.swift |
| +++ b/native/apple/Sources/SheetsKit/SheetThumbnail.swift |
| @@ -16,7 +16,9 @@ struct SheetPreviewCell: Decodable, Hashable, Sendable { |
| /// Renders shape metadata only: no cell text, web view, or external image fetch. |
| struct SheetThumbnail: View { |
| private let preview: SheetPreview? |
| - init(_ preview: SheetPreview?) { self.preview = preview } |
| + var width: CGFloat = 52 |
| + var height: CGFloat = 68 |
| + init(_ preview: SheetPreview?, width: CGFloat = 52, height: CGFloat = 68) { self.preview = preview; self.width = width; self.height = height } |
| var body: some View { |
| Canvas { context, size in |
| @@ -41,7 +43,7 @@ struct SheetThumbnail: View { |
| with: .color(.secondary.opacity(cell.Bold ? 0.8 : 0.4))) |
| } |
| } |
| - .frame(width: 52, height: 68) |
| + .frame(width: width, height: height) |
| .accessibilityHidden(true) |
| } |
| } |
diff --git a/native/apple/Sources/SheetsKit/SheetsDocumentMenu.swift b/native/apple/Sources/SheetsKit/SheetsDocumentMenu.swift| new file mode 100644 |
| index 0000000..894ca7a |
| --- /dev/null |
| +++ b/native/apple/Sources/SheetsKit/SheetsDocumentMenu.swift |
| @@ -0,0 +1,24 @@ |
| +#if os(macOS) |
| +import SwiftUI |
| + |
| +public struct SheetsDocumentMenu: View { |
| + @FocusedValue(\.sheetEditor) private var editor |
| + public init() {} |
| + private var available: Bool { editor?.model.page != nil } |
| + private var writable: Bool { |
| + guard let model = editor?.model else { return false } |
| + return model.mayWrite && !model.busy && !model.hasUnconfirmedChanges |
| + } |
| + public var body: some View { |
| + Group { |
| + Button(sheetsMacString("Save")) { if let model = editor?.model { Task { await model.saveAll() } } } |
| + .keyboardShortcut("s").disabled(!available) |
| + Button(sheetsMacString("Rename…")) { editor?.panel.wrappedValue = "Rename" }.disabled(!writable) |
| + Button(sheetsMacString("Export workbook…")) { editor?.panel.wrappedValue = "Export" }.disabled(!available) |
| + Button(sheetsMacString("Version history…")) { editor?.panel.wrappedValue = "History" }.disabled(!available) |
| + Button(sheetsMacString("Share…")) { editor?.panel.wrappedValue = "Share" }.disabled(editor?.model.page?.MayShare != true) |
| + |
| + } |
| + } |
| +} |
| +#endif |
diff --git a/native/apple/Sources/SheetsKit/SheetsEditorCommands.swift b/native/apple/Sources/SheetsKit/SheetsEditorCommands.swift| new file mode 100644 |
| index 0000000..9151495 |
| --- /dev/null |
| +++ b/native/apple/Sources/SheetsKit/SheetsEditorCommands.swift |
| @@ -0,0 +1,61 @@ |
| +#if os(macOS) |
| +import AppKit |
| +import SwiftUI |
| + |
| +public struct SheetsEditorCommands: Commands { |
| + @FocusedValue(\.sheetEditor) private var editor |
| + public init() {} |
| + private var available: Bool { editor?.model.page != nil } |
| + private var writable: Bool { |
| + guard let model = editor?.model else { return false } |
| + return model.mayWrite && !model.busy && !model.hasUnconfirmedChanges |
| + } |
| + private func route(_ selector: Selector, grid: (SheetModel) -> Void) { |
| + if NSApp.keyWindow?.firstResponder is NSTextView { |
| + NSApp.sendAction(selector, to: nil, from: nil) |
| + } else if let model = editor?.model, model.gridFocused { grid(model) } |
| + else { NSApp.sendAction(selector, to: nil, from: nil) } |
| + } |
| + public var body: some Commands { |
| + |
| + CommandGroup(replacing: .undoRedo) { |
| + Button(sheetsMacString("Undo")) { route(Selector(("undo:"))) { model in Task { await model.command("/undo") } } }.keyboardShortcut("z") |
| + Button(sheetsMacString("Redo")) { NSApp.sendAction(Selector(("redo:")), to: nil, from: nil) }.keyboardShortcut("z", modifiers: [.command, .shift]) |
| + } |
| + CommandGroup(replacing: .pasteboard) { |
| + Button(sheetsMacString("Cut")) { route(#selector(NSText.cut(_:))) { model in |
| + guard model.mayWrite, !model.busy, !model.hasUnconfirmedChanges else { return } |
| + model.copy(); Task { await model.clear() } |
| + } }.keyboardShortcut("x") |
| + Button(sheetsMacString("Copy")) { route(#selector(NSText.copy(_:))) { $0.copy() } }.keyboardShortcut("c") |
| + Button(sheetsMacString("Paste")) { route(#selector(NSText.paste(_:))) { model in Task { await model.paste() } } }.keyboardShortcut("v") |
| + Button(sheetsMacString("Select all")) { route(#selector(NSText.selectAll(_:))) { model in |
| + model.rangeText = "A1:" + Coordinate(r: model.rows - 1, c: model.cols - 1).ref |
| + model.selectRange() |
| + } }.keyboardShortcut("a") |
| + } |
| + CommandGroup(after: .pasteboard) { |
| + Button(sheetsMacString("Clear contents")) { if let model = editor?.model { Task { await model.clear() } } }.disabled(!writable) |
| + Button(sheetsMacString("Fill down")) { if let model = editor?.model { Task { await model.fill(down: true) } } }.keyboardShortcut("d").disabled(!writable) |
| + Button(sheetsMacString("Fill right")) { if let model = editor?.model { Task { await model.fill(down: false) } } }.disabled(!writable) |
| + } |
| + CommandMenu(sheetsMacString("Format")) { |
| + if let model = editor?.model { |
| + SheetFormatMenu(model: model) |
| + Button(sheetsMacString("Row and column sizes…")) { editor?.panel.wrappedValue = "Sizes" }.disabled(!writable) |
| + Button(sheetsMacString("Fit column to contents")) { Task { await model.fitColumn(model.selection.start.c) } }.disabled(!writable) |
| + } |
| + } |
| + CommandMenu(sheetsMacString("Insert")) { |
| + ForEach(["SUM", "AVERAGE", "COUNT", "MIN", "MAX"], id: \.self) { name in |
| + Button(name) { if let model = editor?.model { Task { await model.insertFunction(name) } } }.disabled(!writable) |
| + } |
| + } |
| + CommandMenu(sheetsMacString("Sheet")) { |
| + Button(sheetsMacString("Sort and filter…")) { editor?.panel.wrappedValue = "Data" }.disabled(!available) |
| + Button(sheetsMacString("Comments…")) { editor?.panel.wrappedValue = "Comments" }.disabled(!available) |
| + Button(sheetsMacString("Refresh")) { if let model = editor?.model { Task { await model.refresh() } } }.disabled(!available) |
| + } |
| + } |
| +} |
| +#endif |
diff --git a/native/apple/Sources/SheetsKit/SheetsMacStrings.swift b/native/apple/Sources/SheetsKit/SheetsMacStrings.swift| new file mode 100644 |
| index 0000000..1f5be0f |
| --- /dev/null |
| +++ b/native/apple/Sources/SheetsKit/SheetsMacStrings.swift |
| @@ -0,0 +1,5 @@ |
| +import Foundation |
| + |
| +public func sheetsMacString(_ key: String) -> String { |
| + NSLocalizedString(key, tableName: "Mac", bundle: .module, comment: "") |
| +} |
diff --git a/native/apple/Sources/SheetsKit/SheetsWorkspace.swift b/native/apple/Sources/SheetsKit/SheetsWorkspace.swift| index c894bad..41263ee 100644 |
| --- a/native/apple/Sources/SheetsKit/SheetsWorkspace.swift |
| +++ b/native/apple/Sources/SheetsKit/SheetsWorkspace.swift |
| @@ -15,15 +15,25 @@ import SwiftUI |
| @FocusState private var formulaFocused: Bool |
| @Environment(\.scenePhase) private var scenePhase |
| @AppStorage("sheets.appearance") private var appearance = "auto" |
| + private let updateRecents: (([(UInt64, String)]) -> Void)? |
| + private let canSignOut: Bool |
| + @Binding private var createRequested: Bool |
| + private let documentID: UInt64? |
| + private let openDocument: ((UInt64, String) -> Void)? |
| private let signOut: () -> Void |
| private let accounts: (() -> Void)? |
| - public init(client: NativeHTTP, accounts: (() -> Void)? = nil, signOut: @escaping () -> Void) { |
| + public init(client: NativeHTTP, accounts: (() -> Void)? = nil, signOut: @escaping () -> Void, documentID: UInt64? = nil, canSignOut: Bool = true, createRequested: Binding<Bool> = .constant(false), openDocument: ((UInt64, String) -> Void)? = nil, updateRecents: (([(UInt64, String)]) -> Void)? = nil) { |
| + self.updateRecents = updateRecents; self.canSignOut = canSignOut; self._createRequested = createRequested; self.documentID = documentID; self.openDocument = openDocument |
| _model = State(initialValue: SheetModel(client: client)) |
| self.signOut = signOut |
| self.accounts = accounts |
| } |
| public var body: some View { |
| - NavigationSplitView(preferredCompactColumn: $column) { |
| + Group { |
| + #if os(macOS) |
| + if documentID != nil { editor } else { home } |
| + #else |
| + NavigationSplitView(preferredCompactColumn: $column) { |
| library |
| } detail: { |
| if model.page != nil { |
| @@ -34,7 +44,19 @@ import SwiftUI |
| description: Text("Create a spreadsheet or open one from the list.")) |
| } |
| } |
| - .task { await model.loadList() } |
| + #endif |
| + } |
| + .onChange(of: model.sheets.map { "\($0.ID)\u{0}\($0.Title)" }) { |
| + if query.isEmpty { updateRecents?(model.sheets.map { ($0.ID, $0.Title) }) } |
| + } |
| + .onChange(of: createRequested, initial: true) { _, requested in |
| + if requested && documentID == nil { createRequested = false; Task { await createSheet() } } |
| + } |
| + .task { |
| + if let documentID { |
| + await model.open(SheetSummary(ID: documentID, Title: "", TabID: 0, TabName: "")) |
| + } else { await model.loadList() } |
| + } |
| .onChange(of: model.page?.Sheet.ID) { _, value in if value != nil { column = .detail } } |
| .onChange(of: scenePhase) { _, value in |
| if value != .active { model.clearCopyProvenance() } |
| @@ -46,7 +68,7 @@ import SwiftUI |
| #if os(macOS) |
| .background(SheetCloseGuard(model: model).frame(width: 0, height: 0)) |
| #endif |
| - .sheet(item: $panel) { item in SheetPanels(model: model, panel: item.name) } |
| + .sheet(item: $panel) { item in SheetPanels(model: model, panel: item.name, openImported: openDocument) } |
| .confirmationDialog("Keep your cell edit?", isPresented: $askDiscard, titleVisibility: .visible) |
| { |
| Button("Save and continue") { |
| @@ -62,6 +84,13 @@ import SwiftUI |
| Button("Cancel", role: .cancel) { pendingSheet = nil } |
| } |
| } |
| + private func createSheet() async { |
| + #if os(macOS) |
| + if let sheet = await model.create(openCreated: false) { openDocument?(sheet.ID, sheet.Title) } |
| + #else |
| + await model.create() |
| + #endif |
| + } |
| private var filteredSheets: [SheetSummary] { |
| query.isEmpty ? model.sheets : model.sheets.filter { $0.Title.localizedStandardContains(query) } |
| } |
| @@ -100,12 +129,12 @@ import SwiftUI |
| } |
| } |
| .navigationTitle("Sheets") |
| - .searchable(text: $query, prompt: "Search spreadsheets") |
| + .searchable(text: $query, prompt: sheetsMacString("Search spreadsheets")) |
| .refreshable { await model.loadList() } |
| .toolbar { |
| if let accounts { |
| ToolbarItem(placement: .navigation) { |
| - Button("Accounts", systemImage: "chevron.backward", action: accounts) |
| + Button(sheetsMacString("Accounts"), systemImage: "chevron.backward", action: accounts) |
| .disabled(model.hasUnconfirmedChanges || model.busy) |
| } |
| } |
| @@ -118,7 +147,7 @@ import SwiftUI |
| Text("Light").tag("light") |
| Text("Dark").tag("dark") |
| } |
| - Button("Sign out", systemImage: "rectangle.portrait.and.arrow.right", role: .destructive, |
| + Button(sheetsMacString("Sign out"), systemImage: "rectangle.portrait.and.arrow.right", role: .destructive, |
| action: signOut).disabled(model.hasUnconfirmedChanges || model.busy) |
| } label: { |
| VStack(spacing: 3) { |
| @@ -138,7 +167,7 @@ import SwiftUI |
| } |
| } |
| private var newSheet: some View { |
| - Button("New spreadsheet", systemImage: "plus") { Task { await model.create() } } |
| + Button(sheetsMacString("New spreadsheet"), systemImage: "plus") { Task { await createSheet() } } |
| .disabled(model.hasUnconfirmedChanges || model.busy) |
| } |
| private func show(_ name: String) { |
| @@ -156,18 +185,18 @@ import SwiftUI |
| !model.mayWrite |
| ) |
| .keyboardShortcut("z") |
| - Button("Copy") { model.copy() }.keyboardShortcut("c") |
| - Button("Cut") { |
| + Button(sheetsMacString("Copy")) { model.copy() }.keyboardShortcut("c") |
| + Button(sheetsMacString("Cut")) { |
| model.copy() |
| Task { await model.clear() } |
| }.keyboardShortcut("x").disabled(!model.mayWrite) |
| - Button("Paste") { Task { await model.paste() } }.keyboardShortcut("v").disabled( |
| + Button(sheetsMacString("Paste")) { Task { await model.paste() } }.keyboardShortcut("v").disabled( |
| !model.mayWrite) |
| - Button("Clear contents") { Task { await model.clear() } }.disabled(!model.mayWrite) |
| - Button("Fill down") { Task { await model.fill(down: true) } }.keyboardShortcut("d").disabled( |
| + Button(sheetsMacString("Clear contents")) { Task { await model.clear() } }.disabled(!model.mayWrite) |
| + Button(sheetsMacString("Fill down")) { Task { await model.fill(down: true) } }.keyboardShortcut("d").disabled( |
| !model.mayWrite) |
| - Button("Fill right") { Task { await model.fill(down: false) } }.disabled(!model.mayWrite) |
| - Button("Select all") { |
| + Button(sheetsMacString("Fill right")) { Task { await model.fill(down: false) } }.disabled(!model.mayWrite) |
| + Button(sheetsMacString("Select all")) { |
| model.rangeText = "A1:" + Coordinate(r: model.rows - 1, c: model.cols - 1).ref |
| model.selectRange() |
| }.keyboardShortcut("a") |
| @@ -183,15 +212,17 @@ import SwiftUI |
| Button("Sort and filter") { show("Data") }.disabled( |
| model.hasUnconfirmedChanges || model.busy) |
| Button("Comments") { show("Comments") } |
| - Button("Refresh", systemImage: "arrow.clockwise") { Task { await model.refresh() } } |
| + Button(sheetsMacString("Refresh"), systemImage: "arrow.clockwise") { Task { await model.refresh() } } |
| } |
| private var editor: some View { |
| VStack(spacing: 0) { |
| + #if os(iOS) |
| ViewThatFits(in: .horizontal) { |
| HStack { editorCommands }.fixedSize(horizontal: true, vertical: false) |
| Menu("Sheet actions", systemImage: "ellipsis.circle") { editorCommands } |
| .frame(maxWidth: .infinity, alignment: .leading) |
| }.padding(8) |
| + #endif |
| HStack { |
| TextField("Cell or range", text: $model.rangeText).frame(width: 110).onSubmit { |
| model.selectRange() |
| @@ -214,7 +245,7 @@ import SwiftUI |
| return .handled |
| } |
| if model.dirty { |
| - Button("Save") { Task { await model.commit() } }.disabled(model.busy) |
| + Button(sheetsMacString("Save")) { Task { await model.commit() } }.disabled(model.busy) |
| Button("Discard") { model.discard() }.disabled(model.busy) |
| } |
| }.textFieldStyle(.roundedBorder).padding(8) |
| @@ -254,8 +285,51 @@ import SwiftUI |
| ).font(.caption).padding(4) |
| } |
| }.navigationTitle(model.page?.Sheet.Title ?? "Sheets") |
| + #if os(macOS) |
| + .focusedSceneValue(\.sheetEditor, SheetEditorContext(model: model, panel: Binding(get: { panel?.name }, set: { panel = $0.map { Panel(name: $0) } }))) |
| + #endif |
| #if os(iOS) |
| .navigationBarTitleDisplayMode(.inline) |
| #endif |
| } |
| + #if os(macOS) |
| + private var home: some View { |
| + NavigationSplitView { |
| + List { Label(sheetsMacString("Recent"), systemImage: "clock").listRowBackground(Color.accentColor.opacity(0.12)) } |
| + .navigationSplitViewColumnWidth(min: 160, ideal: 190, max: 240) |
| + } detail: { |
| + ScrollView { |
| + VStack(alignment: .leading, spacing: 24) { |
| + Text(sheetsMacString("Recent spreadsheets")).font(.largeTitle.bold()) |
| + if !model.error.isEmpty { Text(model.error).foregroundStyle(.red) } |
| + LazyVGrid(columns: [GridItem(.adaptive(minimum: 200, maximum: 260))], spacing: 24) { |
| + ForEach(filteredSheets) { sheet in |
| + Button { openDocument?(sheet.ID, sheet.Title) } label: { |
| + VStack(alignment: .leading, spacing: 10) { |
| + SheetThumbnail(sheet.Preview, width: 200, height: 150) |
| + .background(.background).clipShape(.rect(cornerRadius: 5)) |
| + Text(sheet.Title).font(.headline).lineLimit(2) |
| + Text(sheet.TabName).font(.caption).foregroundStyle(.secondary) |
| + }.frame(maxWidth: .infinity, alignment: .leading).padding(12) |
| + }.buttonStyle(.plain) |
| + } |
| + } |
| + if filteredSheets.isEmpty { |
| + ContentUnavailableView("No spreadsheets", systemImage: "tablecells", description: Text("Create a spreadsheet or import a workbook.")) |
| + } |
| + }.padding(28) |
| + } |
| + .navigationTitle(sheetsMacString("Home")) |
| + .searchable(text: $query, prompt: sheetsMacString("Search spreadsheets")) |
| + .toolbar { |
| + if let accounts { Button(sheetsMacString("Accounts"), action: accounts).disabled(model.busy) } |
| + newSheet |
| + Button(sheetsMacString("Import workbook")) { show("Import") } |
| + Button(sheetsMacString("Refresh"), systemImage: "arrow.clockwise") { Task { await model.loadList() } } |
| + Button(sheetsMacString("Sign out"), action: signOut).disabled(!canSignOut || model.busy).help(sheetsMacString("Close this account’s document windows before signing out.")) |
| + } |
| + } |
| + } |
| + #endif |
| + |
| } |
diff --git a/native/apple/Tests/SheetsKitTests/SheetNativeInteractionTests.swift b/native/apple/Tests/SheetsKitTests/SheetNativeInteractionTests.swift| new file mode 100644 |
| index 0000000..cbd7123 |
| --- /dev/null |
| +++ b/native/apple/Tests/SheetsKitTests/SheetNativeInteractionTests.swift |
| @@ -0,0 +1,71 @@ |
| +#if os(macOS) |
| +import AppKit |
| +import SwiftUI |
| +import XCTest |
| +import RastrilloApple |
| +@testable import SheetsKit |
| + |
| +final class SheetNativeInteractionTests: XCTestCase { |
| + @MainActor func testTypingEntersCellAndFailedCommitKeepsDraft() async throws { |
| + let model = SheetModel(client: NativeHTTP(origin: URL(string: "http://127.0.0.1:1")!)) |
| + model.page = SheetPage(Sheet: SheetSummary(ID: 1, Title: "Fixture", TabID: 1, TabName: "Sheet 1"), Rows: 4, Cols: 3, Truncated: false, MayWrite: true, MayComment: true, MayShare: false, FillSwatches: [], InkSwatches: [], NumberFormats: [], Warned: nil) |
| + let host = NSHostingView(rootView: SheetGrid(model: model).background(Color.white).preferredColorScheme(.light)) |
| + let window = NSWindow(contentRect: NSRect(x: 0, y: 0, width: 640, height: 400), styleMask: [.titled], backing: .buffered, defer: false) |
| + window.contentView = host |
| + NSApp.setActivationPolicy(.regular) |
| + window.makeKeyAndOrderFront(nil) |
| + host.layoutSubtreeIfNeeded() |
| + window.displayIfNeeded() |
| + NSApp.activate() |
| + defer { window.orderOut(nil) } |
| + try await Task.sleep(for: .milliseconds(300)) |
| + for type in [NSEvent.EventType.leftMouseDown, .leftMouseUp] { |
| + let event = NSEvent.mouseEvent(with: type, location: NSPoint(x: 100, y: 350), modifierFlags: [], timestamp: 0, windowNumber: window.windowNumber, context: nil, eventNumber: 1, clickCount: 1, pressure: 1)! |
| + window.sendEvent(event) |
| + } |
| + try await Task.sleep(for: .milliseconds(100)) |
| + key("h", code: 4, window: window) |
| + try await Task.sleep(for: .milliseconds(150)) |
| + XCTAssertEqual(model.draft, "h") |
| + XCTAssertTrue(window.firstResponder is NSTextView, "Typing must transfer focus into a native cell text field") |
| + key("e", code: 14, window: window) |
| + try await Task.sleep(for: .milliseconds(100)) |
| + XCTAssertEqual(model.draft, "he", "The initial character must not be selected and overwritten") |
| + key("\t", code: 48, window: window) |
| + try await Task.sleep(for: .milliseconds(500)) |
| + XCTAssertEqual(model.selection.start, Coordinate(r: 0, c: 0)) |
| + XCTAssertEqual(model.draft, "he") |
| + XCTAssertTrue(model.hasUnconfirmedChanges) |
| + XCTAssertFalse(model.failedWrites.isEmpty) |
| + XCTAssertTrue(window.firstResponder is NSTextView) |
| + key("\u{1b}", code: 53, window: window) |
| + try await Task.sleep(for: .milliseconds(100)) |
| + XCTAssertFalse(model.hasUnconfirmedChanges) |
| + XCTAssertEqual(model.selection.start, Coordinate(r: 0, c: 0)) |
| + if ProcessInfo.processInfo.environment["OFICINA_CAPTURE_FIXTURES"] == "1", |
| + let bitmap = host.bitmapImageRepForCachingDisplay(in: host.bounds) { |
| + host.cacheDisplay(in: host.bounds, to: bitmap) |
| + try bitmap.representation(using: .png, properties: [:])?.write(to: URL(fileURLWithPath: "/tmp/oficina-sheets-grid-fixture.png")) |
| + } |
| + // Column B header then row 2 header select complete ranges. |
| + click(NSPoint(x: 200, y: 375), window: window) |
| + try await Task.sleep(for: .milliseconds(100)) |
| + XCTAssertEqual(model.selection.start, Coordinate(r: 0, c: 1)) |
| + XCTAssertEqual(model.selection.end, Coordinate(r: 3, c: 1)) |
| + click(NSPoint(x: 24, y: 320), window: window) |
| + try await Task.sleep(for: .milliseconds(100)) |
| + XCTAssertEqual(model.selection.start, Coordinate(r: 1, c: 0)) |
| + XCTAssertEqual(model.selection.end, Coordinate(r: 1, c: 2)) |
| + } |
| + @MainActor private func click(_ point: NSPoint, window: NSWindow) { |
| + for type in [NSEvent.EventType.leftMouseDown, .leftMouseUp] { |
| + let event = NSEvent.mouseEvent(with: type, location: point, modifierFlags: [], timestamp: 0, windowNumber: window.windowNumber, context: nil, eventNumber: 1, clickCount: 1, pressure: 1)! |
| + window.sendEvent(event) |
| + } |
| + } |
| + @MainActor private func key(_ text: String, code: UInt16, window: NSWindow) { |
| + let event = NSEvent.keyEvent(with: .keyDown, location: .zero, modifierFlags: [], timestamp: 0, windowNumber: window.windowNumber, context: nil, characters: text, charactersIgnoringModifiers: text, isARepeat: false, keyCode: code)! |
| + window.sendEvent(event) |
| + } |
| +} |
| +#endif |
diff --git a/native/apple/Tests/SheetsWindowTests/WindowRoutingTests.swift b/native/apple/Tests/SheetsWindowTests/WindowRoutingTests.swift| new file mode 100644 |
| index 0000000..648936e |
| --- /dev/null |
| +++ b/native/apple/Tests/SheetsWindowTests/WindowRoutingTests.swift |
| @@ -0,0 +1,27 @@ |
| +#if os(macOS) |
| +import Foundation |
| +import Testing |
| +import RastrilloApple |
| +@testable import SheetsMac |
| + |
| +@MainActor @Test func windowsReuseDocumentsWithinAccountAndOriginOnly() { |
| + let windows = SheetsWindows() |
| + let first = NativeHTTP(origin: URL(string: "https://first.invalid")!) |
| + let second = NativeHTTP(origin: URL(string: "https://second.invalid")!) |
| + let document: UInt64 = 7 |
| + let id = windows.reserve(account: "a", document: document, title: "One", client: first) |
| + #expect(windows.reserve(account: "a", document: document, title: "Renamed", client: first) == id) |
| + #expect(windows.entries.count == 1) |
| + #expect(!windows.canSignOut("a")) |
| + #expect(windows.reserve(account: "b", document: document, title: "Other account", client: first) != id) |
| + #expect(windows.reserve(account: "a", document: document, title: "Other server", client: second) != id) |
| + #expect(windows.entries.count == 3) |
| + windows.setOpen(id, false) |
| + #expect(!windows.canSignOut("a")) |
| + for entry in windows.entries where entry.account == "a" { windows.setOpen(entry.id, false) } |
| + #expect(windows.canSignOut("a")) |
| + windows.updateRecents([(document, "Current title")], account: "a", client: first) |
| + #expect(windows.entries.first(where: { $0.id == id })?.title == "Current title") |
| + #expect(windows.entries.first(where: { $0.id == id })?.isOpen == false) |
| +} |
| +#endif |