diff --git a/CLAUDE.md b/CLAUDE.md
index 8fdcb40c..f0f3021c 100644
--- a/CLAUDE.md
+++ b/CLAUDE.md
@@ -87,8 +87,8 @@ npm run dev:remote
The web editor is built with React and WordPress packages:
- **Entry Points**:
- - `src/index.jsx` - Local editor entry
- - `src/remote.jsx` - Remote editor entry (for plugin support)
+ - `src/index.js` - Bundled editor entry
+ - `src/remote.js` - Remote editor entry (supports plugins)
- **Core Components**:
- `src/components/editor/` - Main editor component with host bridge integration
- `src/components/visual-editor/` - Visual editing interface
diff --git a/android/app/src/main/res/values/strings.xml b/android/app/src/main/res/values/strings.xml
index 63651174..6df69395 100644
--- a/android/app/src/main/res/values/strings.xml
+++ b/android/app/src/main/res/values/strings.xml
@@ -4,7 +4,7 @@
GutenbergKit Demo
Bundled editor
- Offline editor with bundled assets
+ Local editor without plugin support
Add remote editor
diff --git a/docs/architecture.md b/docs/architecture.md
index 33b2abc1..66234332 100644
--- a/docs/architecture.md
+++ b/docs/architecture.md
@@ -19,8 +19,8 @@ GutenbergKit/
│ │ └── text-editor/ # HTML text editing interface
│ ├── utils/ # Utility functions
│ │ └── bridge.js # Native-to-web communication
-│ ├── index.jsx # Local editor entry point
-│ └── remote.jsx # Remote editor entry point
+│ ├── index.js # Bundled editor entry point
+│ └── remote.js # Remote editor entry point
├── ios/ # iOS Swift package
│ └── Sources/
│ └── GutenbergKit/
@@ -90,28 +90,28 @@ The `make build` command builds both the local and remote editors by default. To
Additionally, a `make dev-server-remote` command is available for serving the latest remote editor changes through a development server. To load the development server in the Demo app, add an environment variable named `GUTENBERG_EDITOR_REMOTE_URL` with the URL of the development server plus `/remote.html`—i.e., `http://:5173/remote.html`.
> [!TIP]
-> The remote editor redirects to the local editor when loading fails. If you need to debug the failure, disable redirects via the `?dev_mode` query parameter..
+> The remote editor redirects to the bundled editor when loading fails. If you need to debug the failure, disable redirects via the `?dev_mode` query parameter..
-### Local Editor (`index.html`)
+### Bundled Editor (`index.html`)
-The local editor bundles all WordPress packages and runs entirely within the WebView. This variant:
+The bundled editor relies upon local `@wordpress` packages. This variant:
- Provides offline capability
- Has faster initial load times
- Limited to core blocks only
- No plugin support
-**Entry point:** `src/index.jsx`
+**Entry point:** `src/index.js`
### Remote Editor (`remote.html`)
-The remote editor loads WordPress packages and plugins from a remote server. This variant:
+The remote editor loads `@wordpress` packages and plugins from a remote server. This variant:
- Supports custom blocks and plugins
- Requires network connectivity
- Used in production environments with custom implementations
-**Entry point:** `src/remote.jsx`
+**Entry point:** `src/remote.js`
## Testing
diff --git a/ios/Demo-iOS/Gutenberg.xcodeproj/project.pbxproj b/ios/Demo-iOS/Gutenberg.xcodeproj/project.pbxproj
index 18189233..9f837d44 100644
--- a/ios/Demo-iOS/Gutenberg.xcodeproj/project.pbxproj
+++ b/ios/Demo-iOS/Gutenberg.xcodeproj/project.pbxproj
@@ -7,6 +7,10 @@
objects = {
/* Begin PBXBuildFile section */
+ 0C4F59A22BEFF4980028BD96 /* AddSiteView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C4F59A32BEFF4980028BD96 /* AddSiteView.swift */; };
+ 0C4F59A42BEFF4980028BD96 /* AuthenticationManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C4F59A52BEFF4980028BD96 /* AuthenticationManager.swift */; };
+ 0C4F59A62BEFF4980028BD96 /* ConfigurationItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C4F59A72BEFF4980028BD96 /* ConfigurationItem.swift */; };
+ 0C4F59A82BEFF4980028BD96 /* ConfigurationStorage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C4F59A92BEFF4980028BD96 /* ConfigurationStorage.swift */; };
0CE8E78B2C339B0600B9DC67 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 0CE8E7842C339B0600B9DC67 /* Assets.xcassets */; };
0CE8E78C2C339B0600B9DC67 /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CE8E7852C339B0600B9DC67 /* ContentView.swift */; };
0CE8E78D2C339B0600B9DC67 /* EditorView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CE8E7862C339B0600B9DC67 /* EditorView.swift */; };
@@ -17,6 +21,10 @@
/* Begin PBXFileReference section */
0C4F598B2BEFF4970028BD96 /* Gutenberg.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Gutenberg.app; sourceTree = BUILT_PRODUCTS_DIR; };
+ 0C4F59A32BEFF4980028BD96 /* AddSiteView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddSiteView.swift; sourceTree = ""; };
+ 0C4F59A52BEFF4980028BD96 /* AuthenticationManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AuthenticationManager.swift; sourceTree = ""; };
+ 0C4F59A72BEFF4980028BD96 /* ConfigurationItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConfigurationItem.swift; sourceTree = ""; };
+ 0C4F59A92BEFF4980028BD96 /* ConfigurationStorage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConfigurationStorage.swift; sourceTree = ""; };
0CE8E7842C339B0600B9DC67 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; };
0CE8E7852C339B0600B9DC67 /* ContentView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ContentView.swift; sourceTree = ""; };
0CE8E7862C339B0600B9DC67 /* EditorView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = EditorView.swift; sourceTree = ""; };
@@ -68,6 +76,10 @@
0CE8E7882C339B0600B9DC67 /* Sources */ = {
isa = PBXGroup;
children = (
+ 0C4F59A32BEFF4980028BD96 /* AddSiteView.swift */,
+ 0C4F59A52BEFF4980028BD96 /* AuthenticationManager.swift */,
+ 0C4F59A72BEFF4980028BD96 /* ConfigurationItem.swift */,
+ 0C4F59A92BEFF4980028BD96 /* ConfigurationStorage.swift */,
0CE8E7852C339B0600B9DC67 /* ContentView.swift */,
0CE8E7862C339B0600B9DC67 /* EditorView.swift */,
0CE8E7872C339B0600B9DC67 /* GutenbergApp.swift */,
@@ -108,6 +120,7 @@
name = Gutenberg;
packageProductDependencies = (
0CF6E04B2BEFF60E00EDEE8A /* GutenbergKit */,
+ 0C4F59A12BEFF4980028BD96 /* WordPressAPI */,
);
productName = Gutenberg;
productReference = 0C4F598B2BEFF4970028BD96 /* Gutenberg.app */;
@@ -137,6 +150,9 @@
Base,
);
mainGroup = 0C4F59822BEFF4970028BD96;
+ packageReferences = (
+ 0C4F59A02BEFF4980028BD96 /* XCRemoteSwiftPackageReference "wordpress-rs" */,
+ );
productRefGroup = 0C4F598C2BEFF4970028BD96 /* Products */;
projectDirPath = "";
projectRoot = "";
@@ -163,6 +179,10 @@
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
+ 0C4F59A22BEFF4980028BD96 /* AddSiteView.swift in Sources */,
+ 0C4F59A42BEFF4980028BD96 /* AuthenticationManager.swift in Sources */,
+ 0C4F59A62BEFF4980028BD96 /* ConfigurationItem.swift in Sources */,
+ 0C4F59A82BEFF4980028BD96 /* ConfigurationStorage.swift in Sources */,
0CE8E78E2C339B0600B9DC67 /* GutenbergApp.swift in Sources */,
0CE8E78C2C339B0600B9DC67 /* ContentView.swift in Sources */,
0CE8E78D2C339B0600B9DC67 /* EditorView.swift in Sources */,
@@ -304,6 +324,8 @@
"DEVELOPMENT_TEAM[sdk=iphoneos*]" = PZYM8XX95Q;
ENABLE_PREVIEWS = YES;
GENERATE_INFOPLIST_FILE = YES;
+ INFOPLIST_KEY_CFBundleURLTypes = "$(INFOPLIST_KEY_CFBundleURLTypes)";
+ "INFOPLIST_KEY_CFBundleURLTypes[0]" = "{ CFBundleURLSchemes = (gutenbergkit); }";
INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES;
INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES;
INFOPLIST_KEY_UILaunchScreen_Generation = YES;
@@ -336,6 +358,8 @@
DEVELOPMENT_TEAM = "";
ENABLE_PREVIEWS = YES;
GENERATE_INFOPLIST_FILE = YES;
+ INFOPLIST_KEY_CFBundleURLTypes = "$(INFOPLIST_KEY_CFBundleURLTypes)";
+ "INFOPLIST_KEY_CFBundleURLTypes[0]" = "{ CFBundleURLSchemes = (gutenbergkit); }";
INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES;
INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES;
INFOPLIST_KEY_UILaunchScreen_Generation = YES;
@@ -379,7 +403,23 @@
};
/* End XCConfigurationList section */
+/* Begin XCRemoteSwiftPackageReference section */
+ 0C4F59A02BEFF4980028BD96 /* XCRemoteSwiftPackageReference "wordpress-rs" */ = {
+ isa = XCRemoteSwiftPackageReference;
+ repositoryURL = "https://github.com/Automattic/wordpress-rs";
+ requirement = {
+ kind = revision;
+ revision = "alpha-20250926";
+ };
+ };
+/* End XCRemoteSwiftPackageReference section */
+
/* Begin XCSwiftPackageProductDependency section */
+ 0C4F59A12BEFF4980028BD96 /* WordPressAPI */ = {
+ isa = XCSwiftPackageProductDependency;
+ package = 0C4F59A02BEFF4980028BD96 /* XCRemoteSwiftPackageReference "wordpress-rs" */;
+ productName = WordPressAPI;
+ };
0CF6E04B2BEFF60E00EDEE8A /* GutenbergKit */ = {
isa = XCSwiftPackageProductDependency;
productName = GutenbergKit;
diff --git a/ios/Demo-iOS/Gutenberg.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/ios/Demo-iOS/Gutenberg.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved
index 3a469bac..724d44ff 100644
--- a/ios/Demo-iOS/Gutenberg.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved
+++ b/ios/Demo-iOS/Gutenberg.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved
@@ -8,6 +8,15 @@
"revision" : "aa85ee96017a730031bafe411cde24a08a17a9c9",
"version" : "2.8.8"
}
+ },
+ {
+ "identity" : "wordpress-rs",
+ "kind" : "remoteSourceControl",
+ "location" : "https://github.com/Automattic/wordpress-rs",
+ "state" : {
+ "branch" : "alpha-20250926",
+ "revision" : "13c6207d6beeeb66c21cd7c627e13817ca5fdcae"
+ }
}
],
"version" : 2
diff --git a/ios/Demo-iOS/Sources/AddSiteView.swift b/ios/Demo-iOS/Sources/AddSiteView.swift
new file mode 100644
index 00000000..f501db98
--- /dev/null
+++ b/ios/Demo-iOS/Sources/AddSiteView.swift
@@ -0,0 +1,85 @@
+import SwiftUI
+import AuthenticationServices
+
+/// View for adding a new remote editor site
+struct AddSiteView: View {
+ @Binding var siteUrl: String
+ @ObservedObject var authenticationManager: AuthenticationManager
+ let onAdd: (RemoteEditorConfiguration) -> Void
+ let onCancel: () -> Void
+
+ @State private var presentationContextProvider = WebAuthPresentationContextProvider()
+
+ var body: some View {
+ NavigationStack {
+ Form {
+ Section {
+ TextField("Site URL", text: $siteUrl)
+ .textInputAutocapitalization(.never)
+ .autocorrectionDisabled()
+ .keyboardType(.URL)
+ .onSubmit {
+ startAuthentication()
+ }
+ } header: {
+ Text("WordPress Site")
+ } footer: {
+ Text("Enter the URL of your WordPress site (e.g., https://example.com)")
+ }
+
+ if let errorMessage = authenticationManager.errorMessage {
+ Section {
+ Text(errorMessage)
+ .foregroundColor(.red)
+ }
+ }
+ }
+ .navigationTitle("Add Remote Editor")
+ .navigationBarTitleDisplayMode(.inline)
+ .toolbar {
+ ToolbarItem(placement: .cancellationAction) {
+ Button("Cancel") {
+ onCancel()
+ }
+ .disabled(authenticationManager.isAuthenticating)
+ }
+
+ ToolbarItem(placement: .confirmationAction) {
+ if authenticationManager.isAuthenticating {
+ ProgressView()
+ } else {
+ Button("Add") {
+ startAuthentication()
+ }
+ .disabled(siteUrl.trimmingCharacters(in: .whitespaces).isEmpty)
+ }
+ }
+ }
+ }
+ }
+
+ private func startAuthentication() {
+ let trimmedUrl = siteUrl.trimmingCharacters(in: .whitespaces)
+ guard !trimmedUrl.isEmpty else { return }
+
+ authenticationManager.startAuthentication(
+ siteUrl: trimmedUrl,
+ presentationContext: presentationContextProvider
+ ) { configuration in
+ onAdd(configuration)
+ }
+ }
+}
+
+/// Provides the presentation context for web authentication
+class WebAuthPresentationContextProvider: NSObject, ASWebAuthenticationPresentationContextProviding {
+ func presentationAnchor(for session: ASWebAuthenticationSession) -> ASPresentationAnchor {
+ // Return the first window that can be used as a presentation anchor
+ if let windowScene = UIApplication.shared.connectedScenes.first as? UIWindowScene,
+ let window = windowScene.windows.first {
+ return window
+ }
+ // Fallback to a new window (shouldn't normally happen)
+ return ASPresentationAnchor()
+ }
+}
diff --git a/ios/Demo-iOS/Sources/AuthenticationManager.swift b/ios/Demo-iOS/Sources/AuthenticationManager.swift
new file mode 100644
index 00000000..8bab0052
--- /dev/null
+++ b/ios/Demo-iOS/Sources/AuthenticationManager.swift
@@ -0,0 +1,132 @@
+import Foundation
+import AuthenticationServices
+import WordPressAPI
+
+/// Manages WordPress authentication flow
+@MainActor
+class AuthenticationManager: NSObject, ObservableObject {
+ @Published var isAuthenticating = false
+ @Published var errorMessage: String?
+
+ private var currentApiRootUrl: String?
+ private var authSession: ASWebAuthenticationSession?
+ private var currentClient: WordPressLoginClient?
+ private var onAuthenticationComplete: ((RemoteEditorConfiguration) -> Void)?
+
+ private static let appName = "GutenbergKit iOS Demo App"
+ private static let callbackURLScheme = "gutenbergkit"
+
+ /// Start the authentication flow for a WordPress site
+ func startAuthentication(
+ siteUrl: String,
+ presentationContext: ASWebAuthenticationPresentationContextProviding,
+ onComplete: @escaping (RemoteEditorConfiguration) -> Void
+ ) {
+ isAuthenticating = true
+ errorMessage = nil
+ onAuthenticationComplete = onComplete
+
+ Task {
+ do {
+ let client = WordPressLoginClient(urlSession: URLSession(configuration: .ephemeral))
+ let details = try await client.details(ofSite: siteUrl)
+
+ let apiRootUrl = details.apiRootUrl.url()
+ let appId = try! WpUuid.parse(input: "00000000-0000-4000-9000-000000000000")
+ let authUrl = details.loginURL(for: .init(
+ id: appId,
+ name: Self.appName,
+ callbackUrl: "\(Self.callbackURLScheme)://authorized"
+ ))
+
+ currentApiRootUrl = apiRootUrl
+ currentClient = client
+
+ launchAuthenticationFlow(
+ authenticationUrl: authUrl,
+ presentationContext: presentationContext
+ )
+ } catch {
+ isAuthenticating = false
+ errorMessage = "Authentication error: \(error.localizedDescription)"
+ }
+ }
+ }
+
+ /// Launch the web authentication session
+ private func launchAuthenticationFlow(
+ authenticationUrl: URL,
+ presentationContext: ASWebAuthenticationPresentationContextProviding
+ ) {
+ let session = ASWebAuthenticationSession(
+ url: authenticationUrl,
+ callbackURLScheme: Self.callbackURLScheme
+ ) { [weak self] callbackURL, error in
+ guard let self = self else { return }
+
+ Task { @MainActor in
+ if let error = error {
+ if (error as? ASWebAuthenticationSessionError)?.code == .canceledLogin {
+ // User cancelled - just reset state
+ self.isAuthenticating = false
+ } else {
+ self.isAuthenticating = false
+ self.errorMessage = "Authentication failed: \(error.localizedDescription)"
+ }
+ return
+ }
+
+ if let callbackURL = callbackURL {
+ self.processAuthenticationResult(callbackURL: callbackURL)
+ }
+ }
+ }
+
+ session.presentationContextProvider = presentationContext
+ session.prefersEphemeralWebBrowserSession = false
+
+ authSession = session
+ session.start()
+ }
+
+ /// Process the authentication callback URL
+ private func processAuthenticationResult(callbackURL: URL) {
+ guard let client = currentClient,
+ let apiRootUrl = currentApiRootUrl else {
+ isAuthenticating = false
+ errorMessage = "Missing authentication parameters"
+ return
+ }
+
+ do {
+ let credentials = try client.credentials(from: callbackURL)
+
+ // Create Basic Auth header
+ let authString = "\(credentials.userLogin):\(credentials.password)"
+ guard let authData = authString.data(using: .utf8) else {
+ isAuthenticating = false
+ errorMessage = "Failed to encode credentials"
+ return
+ }
+ let authHeader = "Basic \(authData.base64EncodedString())"
+
+ // Extract site name from URL
+ let siteName = URL(string: credentials.siteUrl)?.host ?? credentials.siteUrl
+
+ let configuration = RemoteEditorConfiguration(
+ name: siteName,
+ siteUrl: credentials.siteUrl,
+ siteApiRoot: apiRootUrl,
+ authHeader: authHeader
+ )
+
+ isAuthenticating = false
+ currentApiRootUrl = nil
+ currentClient = nil
+ onAuthenticationComplete?(configuration)
+ } catch {
+ isAuthenticating = false
+ errorMessage = "Failed to parse credentials: \(error.localizedDescription)"
+ }
+ }
+}
diff --git a/ios/Demo-iOS/Sources/ConfigurationItem.swift b/ios/Demo-iOS/Sources/ConfigurationItem.swift
new file mode 100644
index 00000000..d8939778
--- /dev/null
+++ b/ios/Demo-iOS/Sources/ConfigurationItem.swift
@@ -0,0 +1,42 @@
+import Foundation
+
+/// Represents a configuration item for the editor
+enum ConfigurationItem: Codable, Identifiable {
+ case bundledEditor
+ case remoteEditor(RemoteEditorConfiguration)
+
+ var id: String {
+ switch self {
+ case .bundledEditor:
+ return "bundled"
+ case .remoteEditor(let config):
+ return config.id
+ }
+ }
+
+ var displayName: String {
+ switch self {
+ case .bundledEditor:
+ return "Bundled Editor"
+ case .remoteEditor(let config):
+ return config.name
+ }
+ }
+}
+
+/// Configuration for a remote editor
+struct RemoteEditorConfiguration: Codable, Identifiable {
+ let id: String
+ let name: String
+ let siteUrl: String
+ let siteApiRoot: String
+ let authHeader: String
+
+ init(name: String, siteUrl: String, siteApiRoot: String, authHeader: String) {
+ self.id = UUID().uuidString
+ self.name = name
+ self.siteUrl = siteUrl
+ self.siteApiRoot = siteApiRoot
+ self.authHeader = authHeader
+ }
+}
diff --git a/ios/Demo-iOS/Sources/ConfigurationStorage.swift b/ios/Demo-iOS/Sources/ConfigurationStorage.swift
new file mode 100644
index 00000000..dcc62f71
--- /dev/null
+++ b/ios/Demo-iOS/Sources/ConfigurationStorage.swift
@@ -0,0 +1,43 @@
+import Foundation
+
+/// Manages persistence of remote editor configurations
+class ConfigurationStorage {
+ private let userDefaults: UserDefaults
+ private let configurationsKey = "saved_configurations"
+
+ init(userDefaults: UserDefaults = .standard) {
+ self.userDefaults = userDefaults
+ }
+
+ /// Load saved configurations from storage
+ func loadConfigurations() -> [ConfigurationItem] {
+ guard let data = userDefaults.data(forKey: configurationsKey) else {
+ return []
+ }
+
+ do {
+ let remoteConfigs = try JSONDecoder().decode([RemoteEditorConfiguration].self, from: data)
+ return remoteConfigs.map { .remoteEditor($0) }
+ } catch {
+ NSLog("Failed to decode configurations: \(error)")
+ return []
+ }
+ }
+
+ /// Save configurations to storage
+ func saveConfigurations(_ configurations: [ConfigurationItem]) {
+ let remoteConfigs = configurations.compactMap { item -> RemoteEditorConfiguration? in
+ if case .remoteEditor(let config) = item {
+ return config
+ }
+ return nil
+ }
+
+ do {
+ let data = try JSONEncoder().encode(remoteConfigs)
+ userDefaults.set(data, forKey: configurationsKey)
+ } catch {
+ NSLog("Failed to encode configurations: \(error)")
+ }
+ }
+}
diff --git a/ios/Demo-iOS/Sources/ContentView.swift b/ios/Demo-iOS/Sources/ContentView.swift
index 6a47966d..70488d6d 100644
--- a/ios/Demo-iOS/Sources/ContentView.swift
+++ b/ios/Demo-iOS/Sources/ContentView.swift
@@ -1,13 +1,15 @@
import SwiftUI
import GutenbergKit
+import AuthenticationServices
struct ContentView: View {
- private let remoteEditors: [RemoteEditorRow] = [
- .init(id: "template", configuration: .template)
- ]
-
- @State private var isDefaultEditorShown = false
- @State private var selectedRemoteEditor: RemoteEditorRow?
+ @State private var selectedConfiguration: ConfigurationItem?
+ @State private var configurations: [ConfigurationItem] = [.bundledEditor]
+ @State private var showAddDialog = false
+ @State private var siteUrlInput = ""
+ @State private var authenticationManager = AuthenticationManager()
+ @State private var configurationStorage = ConfigurationStorage()
+ @State private var configurationToDelete: ConfigurationItem?
@AppStorage("isNativeInserterEnabled") private var isNativeInserterEnabled = false
@@ -15,65 +17,108 @@ struct ContentView: View {
List {
Section {
Button("Bundled Editor") {
- isDefaultEditorShown = true
+ selectedConfiguration = .bundledEditor
+ }
+ } header: {
+ if ProcessInfo.processInfo.environment["GUTENBERG_EDITOR_URL"] != nil ||
+ ProcessInfo.processInfo.environment["GUTENBERG_EDITOR_REMOTE_URL"] != nil
+ {
+ Text("Note: The editor is backed by the dev server created by `make dev-server` and `make dev-server-remote`.")
+ .textCase(nil)
+ .font(.footnote)
+ .foregroundStyle(.secondary)
+ } else {
+ Text("Note: The editor is backed by the compiled web app created by `make build`.")
+ .textCase(nil)
+ .font(.footnote)
+ .foregroundStyle(.secondary)
}
+ } footer: {
+ Text("Local editor without plugin support")
}
Section {
- ForEach(remoteEditors) { editor in
- Button(editor.title) {
- selectedRemoteEditor = editor
+ ForEach(configurations.filter {
+ if case .remoteEditor = $0 { return true }
+ return false
+ }) { config in
+ Button(config.displayName) {
+ selectedConfiguration = config
+ }
+ .swipeActions(edge: .trailing) {
+ Button(role: .destructive) {
+ configurationToDelete = config
+ } label: {
+ Label("Delete", systemImage: "trash")
+ }
}
}
- if remoteEditors.isEmpty {
- Text("Add `EditorConfiguration` instances to the `remoteEditorConfigurations` array to launch remote editors here.")
+ Button("Add New Remote Editor") {
+ showAddDialog = true
}
} header: {
Text("Remote Editors")
} footer: {
- if ProcessInfo.processInfo.environment["GUTENBERG_EDITOR_REMOTE_URL"] != nil {
- Text("Note: The editor is backed by the dev server created by `make dev-server-remote`.")
- } else {
- Text("Note: The editor is backed by the compiled web app created by `make build`.")
- }
+ Text("Site-specific editor with plugins")
}
Section("Configuration") {
Toggle("Native Inserter", isOn: $isNativeInserterEnabled)
}
}
- .fullScreenCover(isPresented: $isDefaultEditorShown) {
- NavigationView {
- EditorView(configuration: preconfigure(.default))
- }
- }
- .fullScreenCover(item: $selectedRemoteEditor) { editor in
+ .fullScreenCover(item: $selectedConfiguration) { config in
NavigationView {
- EditorView(configuration: preconfigure(editor.configuration))
+ EditorView(configuration: preconfigure(createEditorConfiguration(for: config)))
}
}
+ .navigationTitle("GutenbergKit")
.toolbar {
ToolbarItem(placement: .primaryAction) {
Button {
- Task {
- NSLog("Start to fetch assets")
- for editor in remoteEditors {
- let library = EditorAssetsLibrary(configuration: editor.configuration)
- do {
- try await library.fetchAssets()
- } catch {
- NSLog("Failed to fetch assets for \(editor.configuration.siteURL): \(error)")
- }
- }
- NSLog("Done fetching assets")
- }
+ showAddDialog = true
} label: {
- Image(systemName: "arrow.clockwise")
+ Image(systemName: "plus")
}
-
}
}
+ .sheet(isPresented: $showAddDialog) {
+ AddSiteView(
+ siteUrl: $siteUrlInput,
+ authenticationManager: authenticationManager,
+ onAdd: { config in
+ configurations.append(.remoteEditor(config))
+ configurationStorage.saveConfigurations(configurations)
+ showAddDialog = false
+ siteUrlInput = ""
+ },
+ onCancel: {
+ showAddDialog = false
+ siteUrlInput = ""
+ }
+ )
+ }
+ .onAppear {
+ loadConfigurations()
+ }
+ .alert(
+ "Delete Remote Editor?",
+ isPresented: Binding(
+ get: { configurationToDelete != nil },
+ set: { if !$0 { configurationToDelete = nil } }
+ ),
+ presenting: configurationToDelete
+ ) { config in
+ Button("Delete", role: .destructive) {
+ deleteConfiguration(config)
+ configurationToDelete = nil
+ }
+ Button("Cancel", role: .cancel) {
+ configurationToDelete = nil
+ }
+ } message: { config in
+ Text("Are you sure you want to delete \"\(config.displayName)\"?")
+ }
}
private func preconfigure(_ configuration: EditorConfiguration) -> EditorConfiguration {
@@ -82,37 +127,43 @@ struct ContentView: View {
.setNativeInserterEnabled(isNativeInserterEnabled)
.build()
}
-}
-private struct RemoteEditorRow: Identifiable {
- let id: String
- let configuration: EditorConfiguration
+ private func loadConfigurations() {
+ let saved = configurationStorage.loadConfigurations()
+ configurations = [.bundledEditor] + saved
+ }
- var title: String {
- URL(string: configuration.siteURL)?.host ?? configuration.siteURL
+ private func deleteConfiguration(_ config: ConfigurationItem) {
+ configurations.removeAll { $0.id == config.id }
+ configurationStorage.saveConfigurations(configurations)
}
-}
-private extension EditorConfiguration {
+ private func createEditorConfiguration(for item: ConfigurationItem) -> EditorConfiguration {
+ switch item {
+ case .bundledEditor:
+ return createBundledConfiguration()
+ case .remoteEditor(let config):
+ return createRemoteConfiguration(config)
+ }
+ }
- static var template: Self {
- // Steps:
- // 1. Update the siteURL and authHeader values below
- // 2. Install the Jetpack plugin to the site
- let siteUrl: String = "https://modify-me.com"
- let authHeader: String = "Insert the Authorization header value here"
- let siteApiRoot: String = "\(siteUrl)/wp-json/"
+ private func createBundledConfiguration() -> EditorConfiguration {
+ EditorConfigurationBuilder()
+ .setShouldUsePlugins(false)
+ .setSiteUrl("")
+ .setSiteApiRoot("")
+ .setAuthHeader("")
+ .build()
+ }
- let configuration = EditorConfigurationBuilder()
- .setSiteUrl(siteUrl)
- .setAuthHeader(authHeader)
- .setSiteApiRoot(siteApiRoot)
- .setEditorAssetsEndpoint(URL(string: siteApiRoot)!.appendingPathComponent("wpcom/v2/editor-assets"))
+ private func createRemoteConfiguration(_ config: RemoteEditorConfiguration) -> EditorConfiguration {
+ EditorConfigurationBuilder()
.setShouldUsePlugins(true)
-
- return configuration.build()
+ .setSiteUrl(config.siteUrl)
+ .setSiteApiRoot(config.siteApiRoot)
+ .setAuthHeader(config.authHeader)
+ .build()
}
-
}
#Preview {