Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 25 additions & 0 deletions ExtensionHost/ExtensionJSRuntime.swift
Original file line number Diff line number Diff line change
Expand Up @@ -140,6 +140,7 @@ final class ExtensionJSRuntime {
injectSystem(into: superIsland)
injectFeedback(into: superIsland)
injectMascot(into: superIsland)
injectConstants(into: superIsland)
injectConsole(into: superIsland)
injectTimers()
injectViewHelpers()
Expand Down Expand Up @@ -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)
Comment on lines +529 to +530

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Derive compact layout constants from live notch metrics

The SDK exports compactWidth/compactHeight from Constants.compactSize, but compact geometry is runtime-dependent on notched Macs (compactIslandMetrics and contentSize(for: .compact) in SuperIsland/App/AppState.swift around lines 856–866 and 1057–1069). Because the exported values are static defaults, modules that rely on SuperIsland.constants.layout.compact* can be sized incorrectly (e.g., clipped when actual compact height is reduced).

Useful? React with 👍 / 👎.

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)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Publish runtime fullExpanded height for non-notch Macs

SuperIsland.constants.layout.fullExpandedHeight is always sourced from Constants.fullExpandedSize.height (180), but the host actually renders a taller full-expanded content area on non-notch presentations (AppState.contentSize(for:) adds 50 when !presentationHasNotch, see SuperIsland/App/AppState.swift around lines 871–876). Extensions that size to this SDK value will compute the wrong layout and leave a 50pt mismatch on non-notch machines.

Useful? React with 👍 / 👎.

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 }
Expand Down
52 changes: 52 additions & 0 deletions SUPER-ISLAND-EXTENSION-API.md
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
54 changes: 42 additions & 12 deletions SuperIsland/Utilities/Constants.swift
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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,
Expand All @@ -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"
}