diff --git a/ExtensionHost/ExtensionJSRuntime.swift b/ExtensionHost/ExtensionJSRuntime.swift index 9a7abbd..166b73a 100644 --- a/ExtensionHost/ExtensionJSRuntime.swift +++ b/ExtensionHost/ExtensionJSRuntime.swift @@ -140,6 +140,7 @@ final class ExtensionJSRuntime { injectSystem(into: superIsland) injectFeedback(into: superIsland) injectMascot(into: superIsland) + injectConstants(into: superIsland) injectConsole(into: superIsland) injectTimers() injectViewHelpers() @@ -517,6 +518,30 @@ final class ExtensionJSRuntime { superIsland.setObject(mascot, forKeyedSubscript: "mascot" as NSString) } + /// Exposes read-only layout and sizing constants to extensions so that + /// content can be sized precisely for each island state without hard-coding + /// magic numbers. Available as `SuperIsland.constants` in JavaScript. + private func injectConstants(into superIsland: JSValue) { + let constants = JSValue(newObjectIn: context)! + + // Layout dimensions (all values in SwiftUI points) + let layout = JSValue(newObjectIn: context)! + layout.setObject(Double(Constants.compactSize.width), forKeyedSubscript: "compactWidth" as NSString) + layout.setObject(Double(Constants.compactSize.height), forKeyedSubscript: "compactHeight" as NSString) + layout.setObject(Double(Constants.nonNotchCompactSize.width), forKeyedSubscript: "nonNotchCompactWidth" as NSString) + layout.setObject(Double(Constants.nonNotchCompactSize.height), forKeyedSubscript: "nonNotchCompactHeight" as NSString) + layout.setObject(Double(Constants.expandedSize.width), forKeyedSubscript: "expandedWidth" as NSString) + layout.setObject(Double(Constants.expandedSize.height), forKeyedSubscript: "expandedHeight" as NSString) + layout.setObject(Double(Constants.fullExpandedSize.width), forKeyedSubscript: "fullExpandedWidth" as NSString) + layout.setObject(Double(Constants.fullExpandedSize.height), forKeyedSubscript: "fullExpandedHeight" as NSString) + layout.setObject(Double(Constants.compactCornerRadius), forKeyedSubscript: "compactCornerRadius" as NSString) + layout.setObject(Double(Constants.expandedCornerRadius), forKeyedSubscript: "expandedCornerRadius" as NSString) + layout.setObject(Double(Constants.fullExpandedCornerRadius), forKeyedSubscript: "fullExpandedCornerRadius" as NSString) + constants.setObject(layout, forKeyedSubscript: "layout" as NSString) + + superIsland.setObject(constants, forKeyedSubscript: "constants" as NSString) + } + private func injectConsole(into superIsland: JSValue) { let logInfo: @convention(block) (String) -> Void = { [weak self] message in guard let self else { return } diff --git a/SUPER-ISLAND-EXTENSION-API.md b/SUPER-ISLAND-EXTENSION-API.md index d36d232..82acb76 100644 --- a/SUPER-ISLAND-EXTENSION-API.md +++ b/SUPER-ISLAND-EXTENSION-API.md @@ -252,6 +252,58 @@ Notes: - Users select their mascot character in **Settings -> General -> Mascot**. - Use `View.mascot()` to render the mascot in your extension UI. +### `SuperIsland.constants` + +Read-only object exposing the host's layout and sizing values. Use these +instead of hard-coding magic numbers so your extension automatically stays +aligned when the host updates its geometry. + +#### `SuperIsland.constants.layout` + +All values are in **SwiftUI points** (logical pixels on a 1× display). + +| Property | Type | Value | Description | +|---|---|---|---| +| `compactWidth` | number | 200 | Compact-pill width on notched MacBooks | +| `compactHeight` | number | 36 | Compact-pill height on notched MacBooks | +| `nonNotchCompactWidth` | number | 220 | Compact-pill width on non-notch Macs | +| `nonNotchCompactHeight` | number | 28 | Compact-pill height on non-notch Macs | +| `expandedWidth` | number | 408 | Expanded-drawer width | +| `expandedHeight` | number | 88 | Expanded-drawer height | +| `fullExpandedWidth` | number | 658 | Full-expanded panel width | +| `fullExpandedHeight` | number | 180 | Full-expanded panel height | +| `compactCornerRadius` | number | 18 | Corner radius for compact pill | +| `expandedCornerRadius` | number | 22 | Corner radius for expanded drawer | +| `fullExpandedCornerRadius` | number | 40 | Corner radius for full-expanded panel | + +Example: + +```js +const { layout } = SuperIsland.constants; + +SuperIsland.registerModule({ + compact() { + // Size an image to fill the compact pill exactly + return View.image(myURL, { width: layout.compactWidth, height: layout.compactHeight }); + }, + expanded() { + return View.frame( + View.text("Hello"), + { maxWidth: layout.expandedWidth, maxHeight: layout.expandedHeight } + ); + } +}); +``` + +Notes: + +- All values are **read-only** — writing to them has no effect on the host. +- The host selects `compactWidth/Height` vs `nonNotchCompactWidth/Height` based on + whether the Mac has a hardware notch. Extension layout can reference both and + the host will clip or letterbox as needed. + +--- + ## 6. Global Timer and Console APIs Available in extension JS context: diff --git a/SuperIsland/Utilities/Constants.swift b/SuperIsland/Utilities/Constants.swift index b68cc1c..97e5c6f 100644 --- a/SuperIsland/Utilities/Constants.swift +++ b/SuperIsland/Utilities/Constants.swift @@ -1,13 +1,49 @@ import SwiftUI enum Constants { - // MARK: - Island Sizes + + // MARK: - User-Tweakable + // These values control timing and behaviour that a regular user may want to + // adjust. They are candidates for exposure in the in-app Settings panel + // (e.g. an "Advanced" or "Behaviour" section). + + /// How long the HUD stays visible before automatically collapsing (seconds). + static let hudAutoDismissDelay: TimeInterval = 1.5 + /// Delay before the island "peeks" when the cursor hovers near it (seconds). + static let hoverPeekDelay: TimeInterval = 0.3 + /// How long a notification banner is displayed inside the island (seconds). + static let notificationDisplayDuration: TimeInterval = 2.0 + /// Window during which hovering over a notification triggers full-expand (seconds). + static let notificationHoverFullExpandWindow: TimeInterval = 10.0 + /// How often weather data is fetched in the background (seconds). Default = 30 min. + static let weatherRefreshInterval: TimeInterval = 1800 + + // MARK: - Developer / SDK Layout Constants + // These describe the island's visual geometry and are surfaced to extension + // authors via `SuperIsland.constants.layout` in the JavaScript SDK. + // Extension developers should use these values to size and position content + // so it fits correctly inside each island state. + + /// Compact-pill dimensions on notched MacBooks (SwiftUI points). static let compactSize = CGSize(width: 200, height: 36) + /// Compact-pill dimensions on non-notch Macs (SwiftUI points). static let nonNotchCompactSize = CGSize(width: 220, height: 28) + /// Expanded-drawer dimensions (SwiftUI points). static let expandedSize = CGSize(width: 408, height: 88) + /// Full-expanded panel dimensions (SwiftUI points). static let fullExpandedSize = CGSize(width: 658, height: 180) + /// Corner radius used on the compact island pill. + static let compactCornerRadius: CGFloat = 18 + /// Corner radius used on the expanded island drawer. + static let expandedCornerRadius: CGFloat = 22 + /// Corner radius used on the full-expanded panel. + static let fullExpandedCornerRadius: CGFloat = 40 + + // MARK: - Internal Renderer Constants + // These fine-tune the host-side rendering pipeline and are not exposed to + // extensions or end-users. Change them only when adjusting low-level + // layout or animation behaviour in the native host. - // MARK: - Window static let windowMaxWidth: CGFloat = 420 static let windowMaxHeight: CGFloat = 260 static let moduleCyclerGutterWidth: CGFloat = 52 @@ -24,11 +60,11 @@ enum Constants { static let compactMinimalHorizontalPadding: CGFloat = 14 static let compactMinimalSafeSideMargin: CGFloat = 24 static let expandedTopInset: CGFloat = 0 - static let compactCornerRadius: CGFloat = 18 - static let expandedCornerRadius: CGFloat = 22 - static let fullExpandedCornerRadius: CGFloat = 40 // MARK: - Animation Springs + // Shared animation curves used by the native host renderer. Not exposed + // to extensions — use the `View.animate(child, kind)` DSL in JS instead. + /// Single unified spring for all expand/shrink transitions (à la NotchDrop). static let notchAnimation: Animation = .interactiveSpring( duration: 0.5, @@ -41,13 +77,7 @@ enum Constants { static let contentSwap: Animation = .smooth(duration: 0.22) static let overshootBounce: Animation = .spring(response: 0.36, dampingFraction: 0.68) - // MARK: - Timing - static let hudAutoDismissDelay: TimeInterval = 1.5 - static let hoverPeekDelay: TimeInterval = 0.3 - static let notificationDisplayDuration: TimeInterval = 2.0 - static let notificationHoverFullExpandWindow: TimeInterval = 10.0 - static let weatherRefreshInterval: TimeInterval = 1800 // 30 minutes - // MARK: - Menu Bar + static let menuBarIconName = "rectangle.on.rectangle.angled" }